diff --git a/.cirrus.star b/.cirrus.star index 36233872d1e50..e9bb672b95936 100644 --- a/.cirrus.star +++ b/.cirrus.star @@ -7,7 +7,7 @@ https://github.com/bazelbuild/starlark/blob/master/spec.md See also .cirrus.yml and src/tools/ci/README """ -load("cirrus", "env", "fs") +load("cirrus", "env", "fs", "re", "yaml") def main(): @@ -18,19 +18,36 @@ def main(): 1) the contents of .cirrus.yml - 2) if defined, the contents of the file referenced by the, repository + 2) computed environment variables + + 3) if defined, the contents of the file referenced by the, repository level, REPO_CI_CONFIG_GIT_URL variable (see https://cirrus-ci.org/guide/programming-tasks/#fs for the accepted format) - 3) .cirrus.tasks.yml + 4) .cirrus.tasks.yml """ output = "" # 1) is evaluated implicitly + # Add 2) + additional_env = compute_environment_vars() + env_fmt = """ +### +# Computed environment variables start here +### +{0} +### +# Computed environment variables end here +### +""" + output += env_fmt.format(yaml.dumps({'env': additional_env})) + + + # Add 3) repo_config_url = env.get("REPO_CI_CONFIG_GIT_URL") if repo_config_url != None: print("loading additional configuration from \"{}\"".format(repo_config_url)) @@ -38,12 +55,75 @@ def main(): else: output += "\n# REPO_CI_CONFIG_URL was not set\n" - # Add 3) + + # Add 4) output += config_from(".cirrus.tasks.yml") + return output +def compute_environment_vars(): + cenv = {} + + ### + # Some tasks are manually triggered by default because they might use too + # many resources for users of free Cirrus credits, but they can be + # triggered automatically by naming them in an environment variable e.g. + # REPO_CI_AUTOMATIC_TRIGGER_TASKS="task_name other_task" under "Repository + # Settings" on Cirrus CI's website. + + default_manual_trigger_tasks = ['mingw', 'netbsd', 'openbsd'] + + repo_ci_automatic_trigger_tasks = env.get('REPO_CI_AUTOMATIC_TRIGGER_TASKS', '') + for task in default_manual_trigger_tasks: + name = 'CI_TRIGGER_TYPE_' + task.upper() + if repo_ci_automatic_trigger_tasks.find(task) != -1: + value = 'automatic' + else: + value = 'manual' + cenv[name] = value + ### + + ### + # Parse "ci-os-only:" tag in commit message and set + # CI_{$OS}_ENABLED variable for each OS + + # We want to disable SanityCheck if testing just a specific OS. This + # shortens push-wait-for-ci cycle time a bit when debugging operating + # system specific failures. Just treating it as an OS in that case + # suffices. + + operating_systems = [ + 'compilerwarnings', + 'freebsd', + 'linux', + 'macos', + 'mingw', + 'netbsd', + 'openbsd', + 'sanitycheck', + 'windows', + ] + commit_message = env.get('CIRRUS_CHANGE_MESSAGE') + match_re = r"(^|.*\n)ci-os-only: ([^\n]+)($|\n.*)" + + # re.match() returns an array with a tuple of (matched-string, match_1, ...) + m = re.match(match_re, commit_message) + if m and len(m) > 0: + os_only = m[0][2] + os_only_list = re.split(r'[, ]+', os_only) + else: + os_only_list = operating_systems + + for os in operating_systems: + os_enabled = os in os_only_list + cenv['CI_{0}_ENABLED'.format(os.upper())] = os_enabled + ### + + return cenv + + def config_from(config_src): """return contents of config file `config_src`, surrounded by markers indicating start / end of the included file diff --git a/.cirrus.tasks.yml b/.cirrus.tasks.yml index 92057006c9309..a22cef063f337 100644 --- a/.cirrus.tasks.yml +++ b/.cirrus.tasks.yml @@ -31,6 +31,31 @@ env: TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth + # Postgres config args for the meson builds, shared between all meson tasks + # except the 'SanityCheck' task + MESON_COMMON_PG_CONFIG_ARGS: -Dcassert=true -Dinjection_points=true + + # Meson feature flags shared by all meson tasks, except: + # SanityCheck: uses almost no dependencies. + # Windows - VS: has fewer dependencies than listed here, so defines its own. + # Linux: uses the 'auto' feature option to test meson feature autodetection. + MESON_COMMON_FEATURES: >- + -Dauto_features=disabled + -Dldap=enabled + -Dssl=openssl + -Dtap_tests=enabled + -Dplperl=enabled + -Dplpython=enabled + -Ddocs=enabled + -Dicu=enabled + -Dlibxml=enabled + -Dlibxslt=enabled + -Dlz4=enabled + -Dpltcl=enabled + -Dreadline=enabled + -Dzlib=enabled + -Dzstd=enabled + # What files to preserve in case tests fail on_failure_ac: &on_failure_ac @@ -72,13 +97,13 @@ task: # push-wait-for-ci cycle time a bit when debugging operating system specific # failures. Uses skip instead of only_if, as cirrus otherwise warns about # only_if conditions not matching. - skip: $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:.*' + skip: $CI_SANITYCHECK_ENABLED == false env: CPUS: 4 BUILD_JOBS: 8 TEST_JOBS: 8 - IMAGE_FAMILY: pg-ci-bookworm + IMAGE_FAMILY: pg-ci-trixie CCACHE_DIR: ${CIRRUS_WORKING_DIR}/ccache_dir # no options enabled, should be small CCACHE_MAXSIZE: "150M" @@ -104,14 +129,17 @@ task: configure_script: | su postgres <<-EOF + set -e meson setup \ --buildtype=debug \ --auto-features=disabled \ + -Ddefault_library=shared \ -Dtap_tests=enabled \ build EOF build_script: | su postgres <<-EOF + set -e ninja -C build -j${BUILD_JOBS} ${MBUILD_TARGET} EOF upload_caches: ccache @@ -121,6 +149,7 @@ task: # tap test that exercises both a frontend binary and the backend. test_minimal_script: | su postgres <<-EOF + set -e ulimit -c unlimited meson test $MTEST_ARGS --suite setup meson test $MTEST_ARGS --num-processes ${TEST_JOBS} \ @@ -164,10 +193,19 @@ task: -c debug_parallel_query=regress PG_TEST_PG_UPGRADE_MODE: --link + MESON_FEATURES: >- + -Ddtrace=enabled + -Dgssapi=enabled + -Dlibcurl=enabled + -Dnls=enabled + -Dpam=enabled + -Dtcl_version=tcl86 + -Duuid=bsd + <<: *freebsd_task_template depends_on: SanityCheck - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*freebsd.*' + only_if: $CI_FREEBSD_ENABLED sysinfo_script: | id @@ -195,11 +233,12 @@ task: # already takes longer than other platforms except for windows. configure_script: | su postgres <<-EOF + set -e meson setup \ + ${MESON_COMMON_PG_CONFIG_ARGS} \ --buildtype=debug \ - -Dcassert=true -Dinjection_points=true \ - -Duuid=bsd -Dtcl_version=tcl86 -Ddtrace=auto \ -Dextra_lib_dirs=/usr/local/lib -Dextra_include_dirs=/usr/local/include/ \ + ${MESON_COMMON_FEATURES} ${MESON_FEATURES} \ build EOF build_script: su postgres -c 'ninja -C build -j${BUILD_JOBS} ${MBUILD_TARGET}' @@ -207,6 +246,7 @@ task: test_world_script: | su postgres <<-EOF + set -e ulimit -c unlimited meson test $MTEST_ARGS --num-processes ${TEST_JOBS} EOF @@ -231,6 +271,7 @@ task: # during upload, as it doesn't expect artifacts to change size stop_running_script: | su postgres <<-EOF + set -e build/tmp_install/usr/local/pgsql/bin/pg_ctl -D build/runningcheck stop || true EOF <<: *on_failure_meson @@ -239,7 +280,6 @@ task: task: depends_on: SanityCheck - trigger_type: manual env: # Below are experimentally derived to be a decent choice. @@ -257,7 +297,9 @@ task: matrix: - name: NetBSD - Meson - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*netbsd.*' + # See REPO_CI_AUTOMATIC_TRIGGER_TASKS in .cirrus.star + trigger_type: $CI_TRIGGER_TYPE_NETBSD + only_if: $CI_NETBSD_ENABLED env: OS_NAME: netbsd IMAGE_FAMILY: pg-ci-netbsd-postgres @@ -269,18 +311,32 @@ task: LC_ALL: "C" # -Duuid is not set for the NetBSD, see the comment below, above # configure_script, for more information. + MESON_FEATURES: >- + -Dgssapi=enabled + -Dlibcurl=enabled + -Dnls=enabled + -Dpam=enabled + setup_additional_packages_script: | #pkgin -y install ... <<: *netbsd_task_template - name: OpenBSD - Meson - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*openbsd.*' + # See REPO_CI_AUTOMATIC_TRIGGER_TASKS in .cirrus.star + trigger_type: $CI_TRIGGER_TYPE_OPENBSD + only_if: $CI_OPENBSD_ENABLED env: OS_NAME: openbsd IMAGE_FAMILY: pg-ci-openbsd-postgres PKGCONFIG_PATH: '/usr/lib/pkgconfig:/usr/local/lib/pkgconfig' - UUID: -Duuid=e2fs - TCL: -Dtcl_version=tcl86 + CORE_DUMP_EXECUTABLE_DIR: $CIRRUS_WORKING_DIR/build/tmp_install/usr/local/pgsql/bin + + MESON_FEATURES: >- + -Dbsd_auth=enabled + -Dlibcurl=enabled + -Dtcl_version=tcl86 + -Duuid=e2fs + setup_additional_packages_script: | #pkg_add -I ... # Always core dump to ${CORE_DUMP_DIR} @@ -313,12 +369,12 @@ task: # And other uuid options are not available on NetBSD. configure_script: | su postgres <<-EOF + set -e meson setup \ + ${MESON_COMMON_PG_CONFIG_ARGS} \ --buildtype=debugoptimized \ --pkg-config-path ${PKGCONFIG_PATH} \ - -Dcassert=true -Dinjection_points=true \ - -Dssl=openssl ${UUID} ${TCL} \ - -DPG_TEST_EXTRA="$PG_TEST_EXTRA" \ + ${MESON_COMMON_FEATURES} ${MESON_FEATURES} \ build EOF @@ -327,10 +383,8 @@ task: test_world_script: | su postgres <<-EOF + set -e ulimit -c unlimited - # Otherwise tests will fail on OpenBSD, due to inability to start enough - # processes. - ulimit -p 256 meson test $MTEST_ARGS --num-processes ${TEST_JOBS} EOF @@ -341,7 +395,7 @@ task: # ${CORE_DUMP_DIR}, they may not obey this. So, move core files to the # ${CORE_DUMP_DIR} directory. find build/ -type f -name '*.core' -exec mv '{}' ${CORE_DUMP_DIR} \; - src/tools/ci/cores_backtrace.sh ${OS_NAME} ${CORE_DUMP_DIR} + src/tools/ci/cores_backtrace.sh ${OS_NAME} ${CORE_DUMP_DIR} ${CORE_DUMP_EXECUTABLE_DIR} # configure feature flags, shared between the task running the linux tests and @@ -365,10 +419,6 @@ LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >- --with-uuid=ossp --with-zstd -LINUX_MESON_FEATURES: &LINUX_MESON_FEATURES >- - -Dllvm=enabled - -Duuid=e2fs - # Check SPECIAL in the matrix: below task: @@ -376,7 +426,7 @@ task: CPUS: 4 BUILD_JOBS: 4 TEST_JOBS: 8 # experimentally derived to be a decent choice - IMAGE_FAMILY: pg-ci-bookworm + IMAGE_FAMILY: pg-ci-trixie CCACHE_DIR: /tmp/ccache_dir DEBUGINFOD_URLS: "https://debuginfod.debian.net" @@ -397,7 +447,7 @@ task: # print_stacktraces=1,verbosity=2, duh # detect_leaks=0: too many uninteresting leak errors in short-lived binaries UBSAN_OPTIONS: print_stacktrace=1:disable_coredump=0:abort_on_error=1:verbosity=2 - ASAN_OPTIONS: print_stacktrace=1:disable_coredump=0:abort_on_error=1:detect_leaks=0 + ASAN_OPTIONS: print_stacktrace=1:disable_coredump=0:abort_on_error=1:detect_leaks=0:detect_stack_use_after_return=0 # SANITIZER_FLAGS is set in the tasks below CFLAGS: -Og -ggdb -fno-sanitize-recover=all $SANITIZER_FLAGS @@ -405,16 +455,15 @@ task: LDFLAGS: $SANITIZER_FLAGS CC: ccache gcc CXX: ccache g++ - # GCC emits a warning for llvm-14, so switch to a newer one. - LLVM_CONFIG: llvm-config-16 LINUX_CONFIGURE_FEATURES: *LINUX_CONFIGURE_FEATURES - LINUX_MESON_FEATURES: *LINUX_MESON_FEATURES + LINUX_MESON_FEATURES: >- + -Duuid=e2fs <<: *linux_task_template depends_on: SanityCheck - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*linux.*' + only_if: $CI_LINUX_ENABLED ccache_cache: folder: ${CCACHE_DIR} @@ -453,7 +502,7 @@ task: # - Uses address sanitizer, sanitizer failures are typically printed in # the server log # - Configures postgres with a small segment size - - name: Linux - Debian Bookworm - Autoconf + - name: Linux - Debian Trixie - Autoconf env: SANITIZER_FLAGS: -fsanitize=address @@ -467,6 +516,7 @@ task: # that. configure_script: | su postgres <<-EOF + set -e ./configure \ --enable-cassert --enable-injection-points --enable-debug \ --enable-tap-tests --enable-nls \ @@ -476,13 +526,14 @@ task: \ ${LINUX_CONFIGURE_FEATURES} \ \ - CLANG="ccache clang-16" + CLANG="ccache clang" EOF build_script: su postgres -c "make -s -j${BUILD_JOBS} world-bin" upload_caches: ccache test_world_script: | su postgres <<-EOF + set -e ulimit -c unlimited # default is 0 make -s ${CHECK} ${CHECKFLAGS} -j${TEST_JOBS} EOF @@ -495,7 +546,8 @@ task: # are typically printed in the server log # - Test both 64bit and 32 bit builds # - uses io_method=io_uring - - name: Linux - Debian Bookworm - Meson + # - Uses meson feature autodetection + - name: Linux - Debian Trixie - Meson env: CCACHE_MAXSIZE: "400M" # tests two different builds @@ -505,10 +557,11 @@ task: configure_script: | su postgres <<-EOF + set -e meson setup \ + ${MESON_COMMON_PG_CONFIG_ARGS} \ --buildtype=debug \ - -Dcassert=true -Dinjection_points=true \ - ${LINUX_MESON_FEATURES} \ + ${LINUX_MESON_FEATURES} -Dllvm=enabled \ build EOF @@ -516,26 +569,28 @@ task: # locally. configure_32_script: | su postgres <<-EOF + set -e export CC='ccache gcc -m32' + export CXX='ccache g++ -m32' meson setup \ + ${MESON_COMMON_PG_CONFIG_ARGS} \ --buildtype=debug \ - -Dcassert=true -Dinjection_points=true \ - ${LINUX_MESON_FEATURES} \ - -Dllvm=disabled \ --pkg-config-path /usr/lib/i386-linux-gnu/pkgconfig/ \ - -DPERL=perl5.36-i386-linux-gnu \ - -Dlibnuma=disabled \ + -DPERL=perl5.40-i386-linux-gnu \ + ${LINUX_MESON_FEATURES} -Dlibnuma=disabled \ build-32 EOF build_script: | su postgres <<-EOF + set -e ninja -C build -j${BUILD_JOBS} ${MBUILD_TARGET} ninja -C build -t missingdeps EOF build_32_script: | su postgres <<-EOF + set -e ninja -C build-32 -j${BUILD_JOBS} ${MBUILD_TARGET} ninja -C build -t missingdeps EOF @@ -544,6 +599,7 @@ task: test_world_script: | su postgres <<-EOF + set -e ulimit -c unlimited meson test $MTEST_ARGS --num-processes ${TEST_JOBS} EOF @@ -556,6 +612,7 @@ task: # from C, prevent that with PYTHONCOERCECLOCALE. test_world_32_script: | su postgres <<-EOF + set -e ulimit -c unlimited PYTHONCOERCECLOCALE=0 LANG=C meson test $MTEST_ARGS -C build-32 --num-processes ${TEST_JOBS} EOF @@ -573,21 +630,29 @@ task: # SPECIAL: # - Enables --clone for pg_upgrade and pg_combinebackup task: - name: macOS - Sonoma - Meson + name: macOS - Sequoia - Meson env: CPUS: 4 # always get that much for cirrusci macOS instances BUILD_JOBS: $CPUS - # Test performance regresses noticably when using all cores. 8 seems to + # Test performance regresses noticeably when using all cores. 8 seems to # work OK. See # https://postgr.es/m/20220927040208.l3shfcidovpzqxfh%40awork3.anarazel.de TEST_JOBS: 8 - IMAGE: ghcr.io/cirruslabs/macos-runner:sonoma + IMAGE: ghcr.io/cirruslabs/macos-runner:sequoia CIRRUS_WORKING_DIR: ${HOME}/pgsql/ CCACHE_DIR: ${HOME}/ccache MACPORTS_CACHE: ${HOME}/macports-cache + MESON_FEATURES: >- + -Dbonjour=enabled + -Ddtrace=enabled + -Dgssapi=enabled + -Dlibcurl=enabled + -Dnls=enabled + -Duuid=e2fs + MACOS_PACKAGE_LIST: >- ccache icu @@ -613,7 +678,7 @@ task: <<: *macos_task_template depends_on: SanityCheck - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*(macos|darwin|osx).*' + only_if: $CI_MACOS_ENABLED sysinfo_script: | id @@ -657,11 +722,11 @@ task: configure_script: | export PKG_CONFIG_PATH="/opt/local/lib/pkgconfig/" meson setup \ + ${MESON_COMMON_PG_CONFIG_ARGS} \ --buildtype=debug \ -Dextra_include_dirs=/opt/local/include \ -Dextra_lib_dirs=/opt/local/lib \ - -Dcassert=true -Dinjection_points=true \ - -Duuid=e2fs -Ddtrace=auto \ + ${MESON_COMMON_FEATURES} ${MESON_FEATURES} \ build build_script: ninja -C build -j${BUILD_JOBS} ${MBUILD_TARGET} @@ -701,7 +766,7 @@ WINDOWS_ENVIRONMENT_BASE: &WINDOWS_ENVIRONMENT_BASE task: - name: Windows - Server 2019, VS 2019 - Meson & ninja + name: Windows - Server 2022, VS 2019 - Meson & ninja << : *WINDOWS_ENVIRONMENT_BASE env: @@ -716,10 +781,19 @@ task: # 0x8001 is SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX CIRRUS_WINDOWS_ERROR_MODE: 0x8001 + MESON_FEATURES: + -Dcpp_args=/std:c++20 + -Dauto_features=disabled + -Dldap=enabled + -Dssl=openssl + -Dtap_tests=enabled + -Dplperl=enabled + -Dplpython=enabled + <<: *windows_task_template depends_on: SanityCheck - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*windows.*' + only_if: $CI_WINDOWS_ENABLED setup_additional_packages_script: | REM choco install -y --no-progress ... @@ -730,10 +804,9 @@ task: echo 127.0.0.3 pg-loadbalancetest >> c:\Windows\System32\Drivers\etc\hosts type c:\Windows\System32\Drivers\etc\hosts - # Use /DEBUG:FASTLINK to avoid high memory usage during linking configure_script: | vcvarsall x64 - meson setup --backend ninja --buildtype debug -Dc_link_args=/DEBUG:FASTLINK -Dcassert=true -Dinjection_points=true -Db_pch=true -Dextra_lib_dirs=c:\openssl\1.1\lib -Dextra_include_dirs=c:\openssl\1.1\include -DTAR=%TAR% build + meson setup --backend ninja %MESON_COMMON_PG_CONFIG_ARGS% --buildtype debug -Db_pch=true -Dextra_lib_dirs=c:\openssl\1.1\lib -Dextra_include_dirs=c:\openssl\1.1\include -DTAR=%TAR% %MESON_FEATURES% build build_script: | vcvarsall x64 @@ -753,15 +826,13 @@ task: task: << : *WINDOWS_ENVIRONMENT_BASE - name: Windows - Server 2019, MinGW64 - Meson - - # due to resource constraints we don't run this task by default for now - trigger_type: manual - # worth using only_if despite being manual, otherwise this task will show up - # when e.g. ci-os-only: linux is used. - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' || $CIRRUS_CHANGE_MESSAGE =~ '.*\nci-os-only:[^\n]*mingw.*' - # otherwise it'll be sorted before other tasks + name: Windows - Server 2022, MinGW64 - Meson + + # See REPO_CI_AUTOMATIC_TRIGGER_TASKS in .cirrus.star. + trigger_type: $CI_TRIGGER_TYPE_MINGW + depends_on: SanityCheck + only_if: $CI_MINGW_ENABLED env: TEST_JOBS: 4 # higher concurrency causes occasional failures @@ -777,6 +848,11 @@ task: CHERE_INVOKING: 1 BASH: C:\msys64\usr\bin\bash.exe -l + # Keep -Dnls explicitly disabled, as the number of files it creates causes a + # noticeable slowdown. + MESON_FEATURES: >- + -Dnls=disabled + <<: *windows_task_template ccache_cache: @@ -791,9 +867,8 @@ task: %BASH% -c "where perl" %BASH% -c "perl --version" - # disable -Dnls as the number of files it creates cause a noticable slowdown configure_script: | - %BASH% -c "meson setup -Ddebug=true -Doptimization=g -Dcassert=true -Dinjection_points=true -Db_pch=true -Dnls=disabled -DTAR=%TAR% build" + %BASH% -c "meson setup %MESON_COMMON_PG_CONFIG_ARGS% -Ddebug=true -Doptimization=g -Db_pch=true %MESON_COMMON_FEATURES% %MESON_FEATURES% -DTAR=%TAR% build" build_script: | %BASH% -c "ninja -C build ${MBUILD_TARGET}" @@ -815,15 +890,14 @@ task: # To limit unnecessary work only run this once the SanityCheck # succeeds. This is particularly important for this task as we intentionally - # use always: to continue after failures. Task that did not run count as a - # success, so we need to recheck SanityChecks's condition here ... + # use always: to continue after failures. depends_on: SanityCheck - only_if: $CIRRUS_CHANGE_MESSAGE !=~ '.*\nci-os-only:.*' + only_if: $CI_COMPILERWARNINGS_ENABLED env: CPUS: 4 BUILD_JOBS: 4 - IMAGE_FAMILY: pg-ci-bookworm + IMAGE_FAMILY: pg-ci-trixie # Use larger ccache cache, as this task compiles with multiple compilers / # flag combinations @@ -831,10 +905,6 @@ task: CCACHE_DIR: "/tmp/ccache_dir" LINUX_CONFIGURE_FEATURES: *LINUX_CONFIGURE_FEATURES - LINUX_MESON_FEATURES: *LINUX_MESON_FEATURES - - # GCC emits a warning for llvm-14, so switch to a newer one. - LLVM_CONFIG: llvm-config-16 <<: *linux_task_template @@ -871,7 +941,7 @@ task: --cache gcc.cache \ --enable-dtrace \ ${LINUX_CONFIGURE_FEATURES} \ - CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang-16" + CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang" make -s -j${BUILD_JOBS} clean time make -s -j${BUILD_JOBS} world-bin @@ -882,7 +952,7 @@ task: --cache gcc.cache \ --enable-cassert \ ${LINUX_CONFIGURE_FEATURES} \ - CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang-16" + CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang" make -s -j${BUILD_JOBS} clean time make -s -j${BUILD_JOBS} world-bin @@ -892,7 +962,7 @@ task: time ./configure \ --cache clang.cache \ ${LINUX_CONFIGURE_FEATURES} \ - CC="ccache clang" CXX="ccache clang++-16" CLANG="ccache clang-16" + CC="ccache clang" CXX="ccache clang++" CLANG="ccache clang" make -s -j${BUILD_JOBS} clean time make -s -j${BUILD_JOBS} world-bin @@ -904,7 +974,7 @@ task: --enable-cassert \ --enable-dtrace \ ${LINUX_CONFIGURE_FEATURES} \ - CC="ccache clang" CXX="ccache clang++-16" CLANG="ccache clang-16" + CC="ccache clang" CXX="ccache clang++" CLANG="ccache clang" make -s -j${BUILD_JOBS} clean time make -s -j${BUILD_JOBS} world-bin @@ -912,11 +982,11 @@ task: always: mingw_cross_warning_script: | time ./configure \ - --host=x86_64-w64-mingw32 \ + --host=x86_64-w64-mingw32ucrt \ --enable-cassert \ --without-icu \ - CC="ccache x86_64-w64-mingw32-gcc" \ - CXX="ccache x86_64-w64-mingw32-g++" + CC="ccache x86_64-w64-mingw32ucrt-gcc" \ + CXX="ccache x86_64-w64-mingw32ucrt-g++" make -s -j${BUILD_JOBS} clean time make -s -j${BUILD_JOBS} world-bin @@ -928,30 +998,25 @@ task: docs_build_script: | time ./configure \ --cache gcc.cache \ - CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang-16" + CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang" make -s -j${BUILD_JOBS} clean time make -s -j${BUILD_JOBS} -C doc ### # Verify headerscheck / cpluspluscheck succeed # - # - Don't use ccache, the files are uncacheable, polluting ccache's - # cache + # - Run both in same script to increase parallelism, use -k to get result of both # - Use -fmax-errors, as particularly cpluspluscheck can be very verbose - # - XXX have to disable ICU to avoid errors: - # https://postgr.es/m/20220323002024.f2g6tivduzrktgfa%40alap3.anarazel.de ### always: headers_headerscheck_script: | time ./configure \ ${LINUX_CONFIGURE_FEATURES} \ - --without-icu \ + --cache gcc.cache \ --quiet \ - CC="gcc" CXX"=g++" CLANG="clang-16" + CC="ccache gcc" CXX="ccache g++" CLANG="ccache clang" make -s -j${BUILD_JOBS} clean - time make -s headerscheck EXTRAFLAGS='-fmax-errors=10' - headers_cpluspluscheck_script: | - time make -s cpluspluscheck EXTRAFLAGS='-fmax-errors=10' + time make -s -j${BUILD_JOBS} -k ${CHECKFLAGS} headerscheck cpluspluscheck EXTRAFLAGS='-fmax-errors=10' always: upload_caches: ccache diff --git a/.cirrus.yml b/.cirrus.yml index 33c6e481d746a..3f75852e84ecb 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -10,12 +10,20 @@ # # 1) the contents of this file # -# 2) if defined, the contents of the file referenced by the, repository +# 2) computed environment variables +# +# Used to enable/disable tasks based on the execution environment. See +# .cirrus.star: compute_environment_vars() +# +# 3) if defined, the contents of the file referenced by the, repository # level, REPO_CI_CONFIG_GIT_URL variable (see # https://cirrus-ci.org/guide/programming-tasks/#fs for the accepted # format) # -# 3) .cirrus.tasks.yml +# This allows running tasks in a different execution environment than the +# default, e.g. to have sufficient resources for cfbot. +# +# 4) .cirrus.tasks.yml # # This composition is done by .cirrus.star diff --git a/.editorconfig b/.editorconfig index e20d15d4533a4..0ee9bd28ac47d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -85,6 +85,12 @@ insert_final_newline = true indent_style = unset tab_width = unset +[src/backend/utils/misc/postgresql.conf.sample] +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +tab_width = unset + [*.out] indent_style = unset indent_size = unset diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index d132a32b975ed..34efc7240cf1c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -14,6 +14,57 @@ # # $ git log --pretty=format:"%H # %cd%n# %s" $PGINDENTGITHASH -1 --date=iso +519acd1be59e407a62dbe6a5240d9d1dcb8cd062 # 2026-04-04 21:50:54 +0700 +# Fix indentation + +874da8b1f6143808a4433df645c1e81f6a8bbd1e # 2026-03-26 20:10:13 -0400 +# pg_plan_advice: pgindent + +015d32016d845f8a29b3ec3ab7fa98a69cea1a0f # 2026-03-19 13:42:24 +0900 +# test_saslprep: Apply proper indentation + +b6eb8dde6be32e394a4420dfeb671b5891a87c8b # 2026-03-11 15:14:46 +0100 +# Fix indentation from commit 29a0fb21577 + +7b24959434629970c14fc5ee668585e491e565e4 # 2026-02-23 16:22:49 -0500 +# Fix indentation from commit b380a56a3f9 + +09c37015d49665c52ae7eabd5852af36851aede4 # 2026-01-27 00:26:36 +0100 +# pgindent fix for 3fccbd94cba + +335f2231a30cb5002219888eef14f4dfce5b0391 # 2026-01-15 14:57:45 -0500 +# pgindent fix for 8077649907d + +86b276a4a9b4b2c63ef00765f0e2867e1bcac4ca # 2025-11-19 10:41:28 +0100 +# Fix indentation + +f63ae72bbcea057534144eaf27ffe3f6e9267511 # 2025-11-18 10:28:36 -0600 +# Switch from tabs to spaces in postgresql.conf.sample. + +c2b0e3a0351e021dea9b61fe2f759570d3fedb70 # 2025-11-13 14:25:21 +0900 +# Fix indentation issue + +e94a7afe44bfa1bd8dc929204a2d4ac8b3fa9854 # 2025-10-21 09:56:26 -0500 +# Re-pgindent brin.c. + +7e9c216b5236cc61f677787b35e8c8f28f5f6959 # 2025-09-13 14:50:02 -0500 +# Re-pgindent nbtpreprocesskeys.c after commit 796962922e. + +1d1612aec7688139e1a5506df1366b4b6a69605d # 2025-07-29 09:10:41 -0400 +# Run pgindent. + +73873805fb3627cb23937c750fa83ffd8f16fc6c # 2025-07-25 16:36:44 -0400 +# Run pgindent on the changes of the previous patch. + +9e345415bcd3c4358350b89edfd710469b8bfaf9 # 2025-07-01 15:23:07 +0200 +# Fix indentation in pg_numa code + +b27644bade0348d0dafd3036c47880a349fe9332 # 2025-06-15 13:04:24 -0400 +# Sync typedefs.list with the buildfarm. + +4672b6223910687b2aab075bcd2dd54ce90d5171 # 2025-06-01 14:55:24 -0400 +# Run pgindent on the previous commit. + 918e7287ed20eb1fe280ab6c4056ccf94dcd53a8 # 2025-04-30 19:18:30 +1200 # Fix broken indentation @@ -236,6 +287,9 @@ ce554810329b9b8e862eade08b598148931eb456 # 2017-05-17 19:01:23 -0400 a6fd7b7a5f7bf3a8aa3f3d076cf09d922c1c6dd2 # 2017-05-17 16:31:56 -0400 # Post-PG 10 beta1 pgindent run +fcf70e0dbca1432959be5f3557acd546d639c9ba # 2016-11-17 14:36:59 -0500 +# Re-pgindent src/bin/pg_dump/* + b5bce6c1ec6061c8a4f730d927e162db7e2ce365 # 2016-08-15 13:42:51 -0400 # Final pgindent + perltidy run for 9.6. diff --git a/.gitattributes b/.gitattributes index 8df6b75e653b8..00092168393ae 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,13 +12,14 @@ *.xsl whitespace=space-before-tab,trailing-space,tab-in-indent # Avoid confusing ASCII underlines with leftover merge conflict markers -README conflict-marker-size=32 -README.* conflict-marker-size=32 +README conflict-marker-size=48 +README.* conflict-marker-size=48 # Certain data files that contain special whitespace, and other special cases *.data -whitespace contrib/pgcrypto/sql/pgp-armor.sql whitespace=-blank-at-eol src/backend/catalog/sql_features.txt whitespace=space-before-tab,blank-at-eof,-blank-at-eol +src/backend/utils/misc/postgresql.conf.sample whitespace=space-before-tab,trailing-space,tab-in-indent # Test output files that contain extra whitespace *.out -whitespace diff --git a/COPYRIGHT b/COPYRIGHT index 3b79b1b4ca0db..0a397648dcd3c 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,7 +1,7 @@ PostgreSQL Database Management System (also known as Postgres, formerly known as Postgres95) -Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group Portions Copyright (c) 1994, The Regents of the University of California diff --git a/Makefile b/Makefile index 8a2ec9396b6b4..9bc1a4ec17b3c 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,6 @@ # AIX make defaults to building *every* target of the first rule. Start with # a single-target, empty rule to make the other targets non-default. -# (We don't support AIX anymore, but if someone tries to build on AIX anyway, -# at least they'll get the instructions to run 'configure' first.) all: all check install installdirs installcheck installcheck-parallel uninstall clean distclean maintainer-clean dist distcheck world check-world install-world installcheck-world: diff --git a/config/c-compiler.m4 b/config/c-compiler.m4 index 5f3e1d1faf930..3eab0da9cb6c2 100644 --- a/config/c-compiler.m4 +++ b/config/c-compiler.m4 @@ -7,10 +7,10 @@ # Select the format archetype to be used by gcc to check printf-type functions. # We prefer "gnu_printf", as that most closely matches the features supported # by src/port/snprintf.c (particularly the %m conversion spec). However, -# on some NetBSD versions, that doesn't work while "__syslog__" does. -# If all else fails, use "printf". +# on clang and on some NetBSD versions, that doesn't work while "__syslog__" +# does. If all else fails, use "printf". AC_DEFUN([PGAC_PRINTF_ARCHETYPE], -[AC_CACHE_CHECK([for printf format archetype], pgac_cv_printf_archetype, +[AC_CACHE_CHECK([for C printf format archetype], pgac_cv_printf_archetype, [pgac_cv_printf_archetype=gnu_printf PGAC_TEST_PRINTF_ARCHETYPE if [[ "$ac_archetype_ok" = no ]]; then @@ -20,8 +20,8 @@ if [[ "$ac_archetype_ok" = no ]]; then pgac_cv_printf_archetype=printf fi fi]) -AC_DEFINE_UNQUOTED([PG_PRINTF_ATTRIBUTE], [$pgac_cv_printf_archetype], -[Define to best printf format archetype, usually gnu_printf if available.]) +AC_DEFINE_UNQUOTED([PG_C_PRINTF_ATTRIBUTE], [$pgac_cv_printf_archetype], +[Define to best C printf format archetype, usually gnu_printf if available.]) ])# PGAC_PRINTF_ARCHETYPE # Subroutine: test $pgac_cv_printf_archetype, set $ac_archetype_ok to yes or no @@ -38,6 +38,42 @@ ac_c_werror_flag=$ac_save_c_werror_flag ])# PGAC_TEST_PRINTF_ARCHETYPE +# PGAC_CXX_PRINTF_ARCHETYPE +# ------------------------- +# Because we support using gcc as C compiler with clang as C++ compiler, +# we have to be prepared to use different printf archetypes in C++ code. +# So, do the above test all over in C++. +AC_DEFUN([PGAC_CXX_PRINTF_ARCHETYPE], +[AC_CACHE_CHECK([for C++ printf format archetype], pgac_cv_cxx_printf_archetype, +[pgac_cv_cxx_printf_archetype=gnu_printf +PGAC_TEST_CXX_PRINTF_ARCHETYPE +if [[ "$ac_archetype_ok" = no ]]; then + pgac_cv_cxx_printf_archetype=__syslog__ + PGAC_TEST_CXX_PRINTF_ARCHETYPE + if [[ "$ac_archetype_ok" = no ]]; then + pgac_cv_cxx_printf_archetype=printf + fi +fi]) +AC_DEFINE_UNQUOTED([PG_CXX_PRINTF_ATTRIBUTE], [$pgac_cv_cxx_printf_archetype], +[Define to best C++ printf format archetype, usually gnu_printf if available.]) +])# PGAC_CXX_PRINTF_ARCHETYPE + +# Subroutine: test $pgac_cv_cxx_printf_archetype, set $ac_archetype_ok to yes or no +AC_DEFUN([PGAC_TEST_CXX_PRINTF_ARCHETYPE], +[ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +AC_LANG_PUSH(C++) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM( +[extern void pgac_write(int ignore, const char *fmt,...) +__attribute__((format($pgac_cv_cxx_printf_archetype, 2, 3)));], +[pgac_write(0, "error %s: %m", "foo");])], + [ac_archetype_ok=yes], + [ac_archetype_ok=no]) +AC_LANG_POP([]) +ac_cxx_werror_flag=$ac_save_cxx_werror_flag +])# PGAC_TEST_CXX_PRINTF_ARCHETYPE + + # PGAC_TYPE_128BIT_INT # -------------------- # Check if __int128 is a working 128 bit integer type, and if so @@ -83,7 +119,7 @@ if test x"$pgac_cv__128bit_int" = xyes ; then AC_CACHE_CHECK([for __int128 alignment bug], [pgac_cv__128bit_int_bug], [AC_RUN_IFELSE([AC_LANG_PROGRAM([ /* This must match the corresponding code in c.h: */ -#if defined(__GNUC__) || defined(__SUNPRO_C) +#if defined(__GNUC__) #define pg_attribute_aligned(a) __attribute__((aligned(a))) #elif defined(_MSC_VER) #define pg_attribute_aligned(a) __declspec(align(a)) @@ -114,26 +150,6 @@ fi])# PGAC_TYPE_128BIT_INT -# PGAC_C_STATIC_ASSERT -# -------------------- -# Check if the C compiler understands _Static_assert(), -# and define HAVE__STATIC_ASSERT if so. -# -# We actually check the syntax ({ _Static_assert(...) }), because we need -# gcc-style compound expressions to be able to wrap the thing into macros. -AC_DEFUN([PGAC_C_STATIC_ASSERT], -[AC_CACHE_CHECK(for _Static_assert, pgac_cv__static_assert, -[AC_LINK_IFELSE([AC_LANG_PROGRAM([], -[({ _Static_assert(1, "foo"); })])], -[pgac_cv__static_assert=yes], -[pgac_cv__static_assert=no])]) -if test x"$pgac_cv__static_assert" = xyes ; then -AC_DEFINE(HAVE__STATIC_ASSERT, 1, - [Define to 1 if your compiler understands _Static_assert.]) -fi])# PGAC_C_STATIC_ASSERT - - - # PGAC_C_TYPEOF # ------------- # Check if the C compiler understands typeof or a variant. Define @@ -160,6 +176,96 @@ if test "$pgac_cv_c_typeof" != no; then fi])# PGAC_C_TYPEOF +# PGAC_C_TYPEOF_UNQUAL +# -------------------- +# Check if the C compiler understands typeof_unqual or a variant. Define +# HAVE_TYPEOF_UNQUAL if so, and define 'typeof_unqual' to the actual key word. +# +AC_DEFUN([PGAC_C_TYPEOF_UNQUAL], +[AC_CACHE_CHECK(for typeof_unqual, pgac_cv_c_typeof_unqual, +[pgac_cv_c_typeof_unqual=no +# Test with a void pointer, because MSVC doesn't handle that, and we +# need that for copyObject(). +for pgac_kw in typeof_unqual __typeof_unqual__; do + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], +[int x = 0; +$pgac_kw(x) y; +const void *a; +void *b; +y = x; +b = ($pgac_kw(*a) *) a; +return y;])], +[pgac_cv_c_typeof_unqual=$pgac_kw]) + test "$pgac_cv_c_typeof_unqual" != no && break +done]) +if test "$pgac_cv_c_typeof_unqual" != no; then + AC_DEFINE(HAVE_TYPEOF_UNQUAL, 1, + [Define to 1 if your compiler understands `typeof_unqual' or something similar.]) + if test "$pgac_cv_c_typeof_unqual" != typeof_unqual; then + AC_DEFINE_UNQUOTED(typeof_unqual, $pgac_cv_c_typeof_unqual, [Define to how the compiler spells `typeof_unqual'.]) + fi +fi])# PGAC_C_TYPEOF_UNQUAL + + +# PGAC_CXX_TYPEOF +# ---------------- +# Check if the C++ compiler understands typeof or a variant. Define +# HAVE_CXX_TYPEOF if so, and define 'pg_cxx_typeof' to the actual key word. +# +AC_DEFUN([PGAC_CXX_TYPEOF], +[AC_CACHE_CHECK(for C++ typeof, pgac_cv_cxx_typeof, +[pgac_cv_cxx_typeof=no +AC_LANG_PUSH(C++) +for pgac_kw in typeof __typeof__; do + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], +[int x = 0; +$pgac_kw(x) y; +y = x; +return y;])], +[pgac_cv_cxx_typeof=$pgac_kw]) + test "$pgac_cv_cxx_typeof" != no && break +done +AC_LANG_POP([])]) +if test "$pgac_cv_cxx_typeof" != no; then + AC_DEFINE(HAVE_CXX_TYPEOF, 1, + [Define to 1 if your C++ compiler understands `typeof' or something similar.]) + if test "$pgac_cv_cxx_typeof" != typeof; then + AC_DEFINE_UNQUOTED(pg_cxx_typeof, $pgac_cv_cxx_typeof, [Define to how the C++ compiler spells `typeof'.]) + fi +fi])# PGAC_CXX_TYPEOF + + +# PGAC_CXX_TYPEOF_UNQUAL +# ---------------------- +# Check if the C++ compiler understands typeof_unqual or a variant. Define +# HAVE_CXX_TYPEOF_UNQUAL if so, and define 'pg_cxx_typeof_unqual' to the actual key word. +# +AC_DEFUN([PGAC_CXX_TYPEOF_UNQUAL], +[AC_CACHE_CHECK(for C++ typeof_unqual, pgac_cv_cxx_typeof_unqual, +[pgac_cv_cxx_typeof_unqual=no +AC_LANG_PUSH(C++) +for pgac_kw in typeof_unqual __typeof_unqual__; do + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], +[int x = 0; +$pgac_kw(x) y; +const void *a; +void *b; +y = x; +b = ($pgac_kw(*a) *) a; +return y;])], +[pgac_cv_cxx_typeof_unqual=$pgac_kw]) + test "$pgac_cv_cxx_typeof_unqual" != no && break +done +AC_LANG_POP([])]) +if test "$pgac_cv_cxx_typeof_unqual" != no; then + AC_DEFINE(HAVE_CXX_TYPEOF_UNQUAL, 1, + [Define to 1 if your C++ compiler understands `typeof_unqual' or something similar.]) + if test "$pgac_cv_cxx_typeof_unqual" != typeof_unqual; then + AC_DEFINE_UNQUOTED(pg_cxx_typeof_unqual, $pgac_cv_cxx_typeof_unqual, [Define to how the C++ compiler spells `typeof_unqual'.]) + fi +fi])# PGAC_CXX_TYPEOF_UNQUAL + + # PGAC_C_TYPES_COMPATIBLE # ----------------------- @@ -581,6 +687,31 @@ fi undefine([Ac_cachevar])dnl ])# PGAC_SSE42_CRC32_INTRINSICS +# PGAC_AVX2_SUPPORT +# --------------------------- +# Check if the compiler supports AVX2 as a target +# +# If AVX2 target attribute is supported, sets pgac_avx2_support. +# +# There is deliberately not a guard for __has_attribute here +AC_DEFUN([PGAC_AVX2_SUPPORT], +[define([Ac_cachevar], [AS_TR_SH([pgac_cv_avx2_support])])dnl +AC_CACHE_CHECK([for AVX2 target attribute support], [Ac_cachevar], +[AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + __attribute__((target("avx2"))) + static int avx2_test(void) + { + return 0; + }], + [return avx2_test();])], + [Ac_cachevar=yes], + [Ac_cachevar=no])]) +if test x"$Ac_cachevar" = x"yes"; then + pgac_avx2_support=yes +fi +undefine([Ac_cachevar])dnl +])# PGAC_AVX2_SUPPORT + # PGAC_AVX512_PCLMUL_INTRINSICS # --------------------------- # Check if the compiler supports AVX-512 carryless multiplication @@ -602,6 +733,7 @@ AC_CACHE_CHECK([for _mm512_clmulepi64_epi128], [Ac_cachevar], { __m128i z; + x = _mm512_xor_si512(_mm512_zextsi128_si512(_mm_cvtsi32_si128(0)), x); y = _mm512_clmulepi64_epi128(x, y, 0); z = _mm_ternarylogic_epi64( _mm512_castsi512_si128(y), @@ -652,6 +784,44 @@ fi undefine([Ac_cachevar])dnl ])# PGAC_ARMV8_CRC32C_INTRINSICS +# PGAC_ARM_PLMULL +# --------------------------- +# Check if the compiler supports Arm CRYPTO PMULL (carryless multiplication) +# instructions used for vectorized CRC. +# +# If the instructions are supported, sets pgac_arm_pmull. +AC_DEFUN([PGAC_ARM_PLMULL], +[define([Ac_cachevar], [AS_TR_SH([pgac_cv_arm_pmull_$1])])dnl +AC_CACHE_CHECK([for pmull and pmull2], [Ac_cachevar], +[AC_LINK_IFELSE([AC_LANG_PROGRAM([#include +#include +uint64x2_t a; +uint64x2_t b; +uint64x2_t c; +uint64x2_t r1; +uint64x2_t r2; + + #if defined(__has_attribute) && __has_attribute (target) + __attribute__((target("+crypto"))) + #endif + static int pmull_test(void) + { + __asm("pmull %0.1q, %2.1d, %3.1d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r1), "+w"(c):"w"(a), "w"(b)); + __asm("pmull2 %0.1q, %2.2d, %3.2d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r2), "+w"(c):"w"(a), "w"(b)); + + r1 = veorq_u64(r1, r2); + /* return computed value, to prevent the above being optimized away */ + return (int) vgetq_lane_u64(r1, 0); + }], + [return pmull_test();])], + [Ac_cachevar=yes], + [Ac_cachevar=no])]) +if test x"$Ac_cachevar" = x"yes"; then + pgac_arm_pmull=yes +fi +undefine([Ac_cachevar])dnl +])# PGAC_ARM_PLMULL + # PGAC_LOONGARCH_CRC32C_INTRINSICS # --------------------------- # Check if the compiler supports the LoongArch CRCC instructions, using diff --git a/config/check_modules.pl b/config/check_modules.pl index f3b48a62347c0..c659b7aadeb84 100644 --- a/config/check_modules.pl +++ b/config/check_modules.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # # Verify that required Perl modules are available, diff --git a/config/config.guess b/config/config.guess index 48a684601bd23..a9d01fde46176 100644 --- a/config/config.guess +++ b/config/config.guess @@ -1,10 +1,10 @@ #! /bin/sh # Attempt to guess a canonical system name. -# Copyright 1992-2024 Free Software Foundation, Inc. +# Copyright 1992-2025 Free Software Foundation, Inc. # shellcheck disable=SC2006,SC2268 # see below for rationale -timestamp='2024-07-27' +timestamp='2025-07-10' # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -60,7 +60,7 @@ version="\ GNU config.guess ($timestamp) Originally written by Per Bothner. -Copyright 1992-2024 Free Software Foundation, Inc. +Copyright 1992-2025 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." @@ -1597,8 +1597,11 @@ EOF *:Unleashed:*:*) GUESS=$UNAME_MACHINE-unknown-unleashed$UNAME_RELEASE ;; - *:Ironclad:*:*) - GUESS=$UNAME_MACHINE-unknown-ironclad + x86_64:[Ii]ronclad:*:*|i?86:[Ii]ronclad:*:*) + GUESS=$UNAME_MACHINE-pc-ironclad-mlibc + ;; + *:[Ii]ronclad:*:*) + GUESS=$UNAME_MACHINE-unknown-ironclad-mlibc ;; esac @@ -1808,8 +1811,8 @@ fi exit 1 # Local variables: -# eval: (add-hook 'before-save-hook 'time-stamp) +# eval: (add-hook 'before-save-hook 'time-stamp nil t) # time-stamp-start: "timestamp='" -# time-stamp-format: "%:y-%02m-%02d" +# time-stamp-format: "%Y-%02m-%02d" # time-stamp-end: "'" # End: diff --git a/config/config.sub b/config/config.sub index 4aaae46f6f744..3d35cde174de9 100644 --- a/config/config.sub +++ b/config/config.sub @@ -1,10 +1,10 @@ #! /bin/sh # Configuration validation subroutine script. -# Copyright 1992-2024 Free Software Foundation, Inc. +# Copyright 1992-2025 Free Software Foundation, Inc. # shellcheck disable=SC2006,SC2268,SC2162 # see below for rationale -timestamp='2024-05-27' +timestamp='2025-07-10' # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -76,7 +76,7 @@ Report bugs and patches to ." version="\ GNU config.sub ($timestamp) -Copyright 1992-2024 Free Software Foundation, Inc. +Copyright 1992-2025 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." @@ -145,6 +145,7 @@ case $1 in | kfreebsd*-gnu* \ | knetbsd*-gnu* \ | kopensolaris*-gnu* \ + | ironclad-* \ | linux-* \ | managarm-* \ | netbsd*-eabi* \ @@ -242,7 +243,6 @@ case $1 in | rombug \ | semi \ | sequent* \ - | siemens \ | sgi* \ | siemens \ | sim \ @@ -261,7 +261,7 @@ case $1 in basic_machine=$field1-$field2 basic_os= ;; - zephyr*) + tock* | zephyr*) basic_machine=$field1-unknown basic_os=$field2 ;; @@ -1194,7 +1194,7 @@ case $cpu-$vendor in xscale-* | xscalee[bl]-*) cpu=`echo "$cpu" | sed 's/^xscale/arm/'` ;; - arm64-* | aarch64le-*) + arm64-* | aarch64le-* | arm64_32-*) cpu=aarch64 ;; @@ -1321,6 +1321,7 @@ case $cpu-$vendor in | i960 \ | ia16 \ | ia64 \ + | intelgt \ | ip2k \ | iq2000 \ | javascript \ @@ -1522,6 +1523,10 @@ EOF kernel=nto os=`echo "$basic_os" | sed -e 's|nto|qnx|'` ;; + ironclad*) + kernel=ironclad + os=`echo "$basic_os" | sed -e 's|ironclad|mlibc|'` + ;; linux*) kernel=linux os=`echo "$basic_os" | sed -e 's|linux|gnu|'` @@ -1976,6 +1981,7 @@ case $os in | atheos* \ | auroraux* \ | aux* \ + | banan_os* \ | beos* \ | bitrig* \ | bme* \ @@ -2022,7 +2028,6 @@ case $os in | ios* \ | iris* \ | irix* \ - | ironclad* \ | isc* \ | its* \ | l4re* \ @@ -2118,6 +2123,7 @@ case $os in | sysv* \ | tenex* \ | tirtos* \ + | tock* \ | toppers* \ | tops10* \ | tops20* \ @@ -2214,6 +2220,8 @@ case $kernel-$os-$obj in ;; uclinux-uclibc*- | uclinux-gnu*- ) ;; + ironclad-mlibc*-) + ;; managarm-mlibc*- | managarm-kernel*- ) ;; windows*-msvc*-) @@ -2249,6 +2257,8 @@ case $kernel-$os-$obj in ;; *-eabi*- | *-gnueabi*-) ;; + ios*-simulator- | tvos*-simulator- | watchos*-simulator- ) + ;; none--*) # None (no kernel, i.e. freestanding / bare metal), # can be paired with an machine code file format @@ -2347,8 +2357,8 @@ echo "$cpu-$vendor${kernel:+-$kernel}${os:+-$os}${obj:+-$obj}" exit # Local variables: -# eval: (add-hook 'before-save-hook 'time-stamp) +# eval: (add-hook 'before-save-hook 'time-stamp nil t) # time-stamp-start: "timestamp='" -# time-stamp-format: "%:y-%02m-%02d" +# time-stamp-format: "%Y-%02m-%02d" # time-stamp-end: "'" # End: diff --git a/config/llvm.m4 b/config/llvm.m4 index fa4bedd9370fc..5d4f14cb90060 100644 --- a/config/llvm.m4 +++ b/config/llvm.m4 @@ -4,7 +4,7 @@ # ----------------- # # Look for the LLVM installation, check that it's new enough, set the -# corresponding LLVM_{CFLAGS,CXXFLAGS,BINPATH} and LDFLAGS +# corresponding LLVM_{CFLAGS,CXXFLAGS,BINPATH,LIBS} # variables. Also verify that CLANG is available, to transform C # into bitcode. # @@ -55,7 +55,7 @@ AC_DEFUN([PGAC_LLVM_SUPPORT], for pgac_option in `$LLVM_CONFIG --ldflags`; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LLVM_LIBS="$LLVM_LIBS $pgac_option";; esac done @@ -101,20 +101,3 @@ dnl LLVM_CONFIG, CLANG are already output via AC_ARG_VAR AC_SUBST(LLVM_BINPATH) ])# PGAC_LLVM_SUPPORT - - -# PGAC_CHECK_LLVM_FUNCTIONS -# ------------------------- -# -# Check presence of some optional LLVM functions. -# (This shouldn't happen until we're ready to run AC_CHECK_DECLS tests; -# because PGAC_LLVM_SUPPORT runs very early, it's not an appropriate place.) -# -AC_DEFUN([PGAC_CHECK_LLVM_FUNCTIONS], -[ - # Check which functionality is present - SAVE_CPPFLAGS="$CPPFLAGS" - CPPFLAGS="$CPPFLAGS $LLVM_CPPFLAGS" - AC_CHECK_DECLS([LLVMCreateGDBRegistrationListener, LLVMCreatePerfJITEventListener], [], [], [[#include ]]) - CPPFLAGS="$SAVE_CPPFLAGS" -])# PGAC_CHECK_LLVM_FUNCTIONS diff --git a/config/prep_buildtree b/config/prep_buildtree index a0eabd3dee288..e148535ac112e 100644 --- a/config/prep_buildtree +++ b/config/prep_buildtree @@ -22,18 +22,14 @@ sourcetree=`cd $1 && pwd` buildtree=`cd ${2:-'.'} && pwd` -# We must not auto-create the subdirectories holding built documentation. -# If we did, it would interfere with installation of prebuilt docs from -# the source tree, if a VPATH build is done from a distribution tarball. -# See bug #5595. -for item in `find "$sourcetree" -type d \( \( -name CVS -prune \) -o \( -name .git -prune \) -o -print \) | grep -v "$sourcetree/doc/src/sgml/\+"`; do +for item in `find "$sourcetree"/config "$sourcetree"/contrib "$sourcetree"/doc "$sourcetree"/src -type d -print`; do subdir=`expr "$item" : "$sourcetree\(.*\)"` if test ! -d "$buildtree/$subdir"; then mkdir -p "$buildtree/$subdir" || exit 1 fi done -for item in `find "$sourcetree" -name Makefile -print -o -name GNUmakefile -print | grep -v "$sourcetree/doc/src/sgml/images/"`; do +for item in "$sourcetree"/Makefile `find "$sourcetree"/config "$sourcetree"/contrib "$sourcetree"/doc "$sourcetree"/src -name Makefile -print -o -name GNUmakefile -print`; do filename=`expr "$item" : "$sourcetree\(.*\)"` if test ! -f "${item}.in"; then if cmp "$item" "$buildtree/$filename" >/dev/null 2>&1; then : ; else diff --git a/config/programs.m4 b/config/programs.m4 index 0ad1e58b48d6b..e57fe4907b844 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -284,20 +284,26 @@ AC_DEFUN([PGAC_CHECK_STRIP], AC_DEFUN([PGAC_CHECK_LIBCURL], [ + # libcurl compiler/linker flags are kept separate from the global flags, so + # they have to be added back temporarily for the following tests. + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$CPPFLAGS $LIBCURL_CPPFLAGS" + LDFLAGS="$LDFLAGS $LIBCURL_LDFLAGS" + AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file is required for --with-libcurl])]) + + # LIBCURL_LDLIBS is determined here. Like the compiler flags, it should not + # pollute the global LIBS setting. AC_CHECK_LIB(curl, curl_multi_init, [ AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) AC_SUBST(LIBCURL_LDLIBS, -lcurl) ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) - pgac_save_CPPFLAGS=$CPPFLAGS - pgac_save_LDFLAGS=$LDFLAGS - pgac_save_LIBS=$LIBS - - CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" - LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" LIBS="$LIBCURL_LDLIBS $LIBS" # Check to see whether the current platform supports threadsafe Curl diff --git a/config/python.m4 b/config/python.m4 index b295ad3d3a451..ec3c80cf04479 100644 --- a/config/python.m4 +++ b/config/python.m4 @@ -97,6 +97,11 @@ python_ldlibrary=`${PYTHON} -c "import sysconfig; print(' '.join(filter(None,sys # If LDLIBRARY exists and has a shlib extension, use it verbatim. ldlibrary=`echo "${python_ldlibrary}" | sed -e 's/\.so$//' -e 's/\.dll$//' -e 's/\.dylib$//' -e 's/\.sl$//'` +if test "$PORTNAME" = "aix"; then + # On AIX, '.a' should also be believed to be a shlib. + ldlibrary=`echo "${ldlibrary}" | sed -e 's/\.a$//'` +fi + if test -e "${python_libdir}/${python_ldlibrary}" -a x"${python_ldlibrary}" != x"${ldlibrary}" then ldlibrary=`echo "${ldlibrary}" | sed "s/^lib//"` diff --git a/configure b/configure index 4f15347cc9503..f66c1054a7a1e 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for PostgreSQL 18beta1. +# Generated by GNU Autoconf 2.69 for PostgreSQL 19devel. # # Report bugs to . # @@ -11,7 +11,7 @@ # This configure script is free software; the Free Software Foundation # gives unlimited permission to copy, distribute and modify it. # -# Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Copyright (c) 1996-2026, PostgreSQL Global Development Group ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## @@ -582,8 +582,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='PostgreSQL' PACKAGE_TARNAME='postgresql' -PACKAGE_VERSION='18beta1' -PACKAGE_STRING='PostgreSQL 18beta1' +PACKAGE_VERSION='19devel' +PACKAGE_STRING='PostgreSQL 19devel' PACKAGE_BUGREPORT='pgsql-bugs@lists.postgresql.org' PACKAGE_URL='https://www.postgresql.org/' @@ -682,6 +682,7 @@ FLEXFLAGS FLEX BISONFLAGS BISON +NM MKDIR_P LN_S TAR @@ -739,7 +740,6 @@ PKG_CONFIG_LIBDIR PKG_CONFIG_PATH PKG_CONFIG DLSUFFIX -TAS GCC CPP CFLAGS_SL @@ -760,7 +760,7 @@ CLANG LLVM_CONFIG AWK with_llvm -SUN_STUDIO_CC +have_cxx ac_ct_CXX CXXFLAGS CXX @@ -1468,7 +1468,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures PostgreSQL 18beta1 to adapt to many kinds of systems. +\`configure' configures PostgreSQL 19devel to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1533,7 +1533,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of PostgreSQL 18beta1:";; + short | recursive ) echo "Configuration of PostgreSQL 19devel:";; esac cat <<\_ACEOF @@ -1724,14 +1724,14 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -PostgreSQL configure 18beta1 +PostgreSQL configure 19devel generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. -Copyright (c) 1996-2025, PostgreSQL Global Development Group +Copyright (c) 1996-2026, PostgreSQL Global Development Group _ACEOF exit fi @@ -2477,7 +2477,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by PostgreSQL $as_me 18beta1, which was +It was created by PostgreSQL $as_me 19devel, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -3022,6 +3022,7 @@ else # --with-template not given case $host_os in + aix*) template=aix ;; cygwin*|msys*) template=cygwin ;; darwin*) template=darwin ;; dragonfly*) template=netbsd ;; @@ -3059,12 +3060,6 @@ $as_echo "$template" >&6; } PORTNAME=$template -# Initialize default assumption that we do not need separate assembly code -# for TAS (test-and-set). This can be overridden by the template file -# when it's executed. -need_tas=no -tas_file=dummy.s - # Default, works for most platforms, override in template file if needed DLSUFFIX=".so" @@ -4475,190 +4470,49 @@ ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' ac_compiler_gnu=$ac_cv_c_compiler_gnu - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C99" >&5 -$as_echo_n "checking for $CC option to accept ISO C99... " >&6; } -if ${ac_cv_prog_cc_c99+:} false; then : + +# Detect option needed for C11 +# loosely modeled after code in later Autoconf versions +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C11" >&5 +$as_echo_n "checking for $CC option to accept ISO C11... " >&6; } + +if ${pgac_cv_prog_cc_c11+:} false; then : $as_echo_n "(cached) " >&6 else - ac_cv_prog_cc_c99=no -ac_save_CC=$CC -cat confdefs.h - <<_ACEOF >conftest.$ac_ext + pgac_cv_prog_cc_c11=no +pgac_save_CC=$CC +for pgac_arg in '' '-std=gnu11' '-std=c11'; do + CC="$pgac_save_CC $pgac_arg" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include -#include -#include -#include -#include - -// Check varargs macros. These examples are taken from C99 6.10.3.5. -#define debug(...) fprintf (stderr, __VA_ARGS__) -#define showlist(...) puts (#__VA_ARGS__) -#define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__)) -static void -test_varargs_macros (void) -{ - int x = 1234; - int y = 5678; - debug ("Flag"); - debug ("X = %d\n", x); - showlist (The first, second, and third items.); - report (x>y, "x is %d but y is %d", x, y); -} - -// Check long long types. -#define BIG64 18446744073709551615ull -#define BIG32 4294967295ul -#define BIG_OK (BIG64 / BIG32 == 4294967297ull && BIG64 % BIG32 == 0) -#if !BIG_OK - your preprocessor is broken; -#endif -#if BIG_OK -#else - your preprocessor is broken; +#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L +# error "Compiler does not advertise C11 conformance" #endif -static long long int bignum = -9223372036854775807LL; -static unsigned long long int ubignum = BIG64; - -struct incomplete_array -{ - int datasize; - double data[]; -}; - -struct named_init { - int number; - const wchar_t *name; - double average; -}; - -typedef const char *ccp; - -static inline int -test_restrict (ccp restrict text) -{ - // See if C++-style comments work. - // Iterate through items via the restricted pointer. - // Also check for declarations in for loops. - for (unsigned int i = 0; *(text+i) != '\0'; ++i) - continue; - return 0; -} - -// Check varargs and va_copy. -static void -test_varargs (const char *format, ...) -{ - va_list args; - va_start (args, format); - va_list args_copy; - va_copy (args_copy, args); - - const char *str; - int number; - float fnumber; - - while (*format) - { - switch (*format++) - { - case 's': // string - str = va_arg (args_copy, const char *); - break; - case 'd': // int - number = va_arg (args_copy, int); - break; - case 'f': // float - fnumber = va_arg (args_copy, double); - break; - default: - break; - } - } - va_end (args_copy); - va_end (args); -} - -int -main () -{ - - // Check bool. - _Bool success = false; - - // Check restrict. - if (test_restrict ("String literal") == 0) - success = true; - char *restrict newvar = "Another string"; - - // Check varargs. - test_varargs ("s, d' f .", "string", 65, 34.234); - test_varargs_macros (); - - // Check flexible array members. - struct incomplete_array *ia = - malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10)); - ia->datasize = 10; - for (int i = 0; i < ia->datasize; ++i) - ia->data[i] = i * 1.234; - - // Check named initializers. - struct named_init ni = { - .number = 34, - .name = L"Test wide string", - .average = 543.34343, - }; - - ni.number = 58; - - int dynamic_array[ni.number]; - dynamic_array[ni.number - 1] = 543; - - // work around unused variable warnings - return (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == 'x' - || dynamic_array[ni.number - 1] != 543); - - ; - return 0; -} _ACEOF -for ac_arg in '' -std=gnu99 -std=c99 -c99 -AC99 -D_STDC_C99= -qlanglvl=extc99 -do - CC="$ac_save_CC $ac_arg" - if ac_fn_c_try_compile "$LINENO"; then : - ac_cv_prog_cc_c99=$ac_arg +if ac_fn_c_try_compile "$LINENO"; then : + pgac_cv_prog_cc_c11=$pgac_arg fi -rm -f core conftest.err conftest.$ac_objext - test "x$ac_cv_prog_cc_c99" != "xno" && break +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + test x"$pgac_cv_prog_cc_c11" != x"no" && break done -rm -f conftest.$ac_ext -CC=$ac_save_CC - +CC=$pgac_save_CC fi -# AC_CACHE_VAL -case "x$ac_cv_prog_cc_c99" in - x) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 -$as_echo "none needed" >&6; } ;; - xno) - { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 -$as_echo "unsupported" >&6; } ;; - *) - CC="$CC $ac_cv_prog_cc_c99" - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5 -$as_echo "$ac_cv_prog_cc_c99" >&6; } ;; -esac -if test "x$ac_cv_prog_cc_c99" != xno; then : -fi - - -# Error out if the compiler does not support C99, as the codebase -# relies on that. -if test "$ac_cv_prog_cc_c99" = no; then - as_fn_error $? "C compiler \"$CC\" does not support C99" "$LINENO" 5 +if test x"$pgac_cv_prog_cc_c11" = x"no"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +$as_echo "unsupported" >&6; } + as_fn_error $? "C compiler \"$CC\" does not support C11" "$LINENO" 5 +elif test x"$pgac_cv_prog_cc_c11" = x""; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +$as_echo "none needed" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_cc_c11" >&5 +$as_echo "$pgac_cv_prog_cc_c11" >&6; } + CC="$CC $pgac_cv_prog_cc_c11" fi + ac_ext=cpp ac_cpp='$CXXCPP $CPPFLAGS' ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -4917,9 +4771,13 @@ ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $ ac_compiler_gnu=$ac_cv_c_compiler_gnu -# Check if it's Intel's compiler, which (usually) pretends to be gcc, -# but has idiosyncrasies of its own. We assume icc will define -# __INTEL_COMPILER regardless of CFLAGS. +# Check if it actually found a C++ compiler. +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -4927,29 +4785,93 @@ cat confdefs.h - <<_ACEOF >conftest.$ac_ext int main () { -#ifndef __INTEL_COMPILER -choke me -#endif + ; return 0; } _ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - ICC=yes +if ac_fn_cxx_try_compile "$LINENO"; then : + have_cxx=yes else - ICC=no + have_cxx=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + + +if test "$have_cxx" = yes; then + +# Detect option needed for C++11 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CXX option to accept ISO C++11" >&5 +$as_echo_n "checking for $CXX option to accept ISO C++11... " >&6; } +if ${pgac_cv_prog_cxx_cxx11+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_cv_prog_cxx_cxx11=no +pgac_save_CXX=$CXX +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +for pgac_arg in '' '-std=gnu++11' '-std=c++11'; do + CXX="$pgac_save_CXX $pgac_arg" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#if !defined __cplusplus || __cplusplus < 201103L +# error "Compiler does not advertise C++11 conformance" +#endif +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + pgac_cv_prog_cxx_cxx11=$pgac_arg fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + test x"$pgac_cv_prog_cxx_cxx11" != x"no" && break +done +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +CXX=$pgac_save_CXX +fi -# Check if it's Sun Studio compiler. We assume that -# __SUNPRO_C will be defined for Sun Studio compilers + +if test x"$pgac_cv_prog_cxx_cxx11" = x"no"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +$as_echo "unsupported" >&6; } + { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: C++ compiler \"$CXX\" does not support C++11" >&5 +$as_echo "$as_me: WARNING: C++ compiler \"$CXX\" does not support C++11" >&2;} + have_cxx=no +elif test x"$pgac_cv_prog_cxx_cxx11" = x""; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +$as_echo "none needed" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_cxx_cxx11" >&5 +$as_echo "$pgac_cv_prog_cxx_cxx11" >&6; } + CXX="$CXX $pgac_cv_prog_cxx_cxx11" +fi + +fi # have_cxx + + +# Check if it's Intel's compiler, which (usually) pretends to be gcc, +# but has idiosyncrasies of its own. We assume icc will define +# __INTEL_COMPILER regardless of CFLAGS. cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ int main () { -#ifndef __SUNPRO_C +#ifndef __INTEL_COMPILER choke me #endif ; @@ -4957,15 +4879,13 @@ choke me } _ACEOF if ac_fn_c_try_compile "$LINENO"; then : - SUN_STUDIO_CC=yes + ICC=yes else - SUN_STUDIO_CC=no + ICC=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext - - # # LLVM # @@ -5194,7 +5114,7 @@ fi for pgac_option in `$LLVM_CONFIG --ldflags`; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LLVM_LIBS="$LLVM_LIBS $pgac_option";; esac done @@ -5380,7 +5300,7 @@ fi PERMIT_DECLARATION_AFTER_STATEMENT=-Wno-declaration-after-statement fi - # Really don't want VLAs to be used in our dialect of C + # Really don't want VLAs to be used { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Werror=vla, for CFLAGS" >&5 $as_echo_n "checking whether ${CC} supports -Werror=vla, for CFLAGS... " >&6; } @@ -5421,6 +5341,57 @@ if test x"$pgac_cv_prog_CC_cflags__Werror_vla" = x"yes"; then fi + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Werror=vla, for CXXFLAGS" >&5 +$as_echo_n "checking whether ${CXX} supports -Werror=vla, for CXXFLAGS... " >&6; } +if ${pgac_cv_prog_CXX_cxxflags__Werror_vla+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_save_CXXFLAGS=$CXXFLAGS +pgac_save_CXX=$CXX +CXX=${CXX} +CXXFLAGS="${CXXFLAGS} -Werror=vla" +ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + pgac_cv_prog_CXX_cxxflags__Werror_vla=yes +else + pgac_cv_prog_CXX_cxxflags__Werror_vla=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +ac_cxx_werror_flag=$ac_save_cxx_werror_flag +CXXFLAGS="$pgac_save_CXXFLAGS" +CXX="$pgac_save_CXX" +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Werror_vla" >&5 +$as_echo "$pgac_cv_prog_CXX_cxxflags__Werror_vla" >&6; } +if test x"$pgac_cv_prog_CXX_cxxflags__Werror_vla" = x"yes"; then + CXXFLAGS="${CXXFLAGS} -Werror=vla" +fi + + # On macOS, complain about usage of symbols newer than the deployment target { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Werror=unguarded-availability-new, for CFLAGS" >&5 @@ -5513,17 +5484,16 @@ if test x"$pgac_cv_prog_CXX_cxxflags__Werror_unguarded_availability_new" = x"yes fi - # -Wvla is not applicable for C++ -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wendif-labels, for CFLAGS" >&5 -$as_echo_n "checking whether ${CC} supports -Wendif-labels, for CFLAGS... " >&6; } -if ${pgac_cv_prog_CC_cflags__Wendif_labels+:} false; then : +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wmissing-format-attribute, for CFLAGS" >&5 +$as_echo_n "checking whether ${CC} supports -Wmissing-format-attribute, for CFLAGS... " >&6; } +if ${pgac_cv_prog_CC_cflags__Wmissing_format_attribute+:} false; then : $as_echo_n "(cached) " >&6 else pgac_save_CFLAGS=$CFLAGS pgac_save_CC=$CC CC=${CC} -CFLAGS="${CFLAGS} -Wendif-labels" +CFLAGS="${CFLAGS} -Wmissing-format-attribute" ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes cat confdefs.h - <<_ACEOF >conftest.$ac_ext @@ -5538,31 +5508,31 @@ main () } _ACEOF if ac_fn_c_try_compile "$LINENO"; then : - pgac_cv_prog_CC_cflags__Wendif_labels=yes + pgac_cv_prog_CC_cflags__Wmissing_format_attribute=yes else - pgac_cv_prog_CC_cflags__Wendif_labels=no + pgac_cv_prog_CC_cflags__Wmissing_format_attribute=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="$pgac_save_CFLAGS" CC="$pgac_save_CC" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wendif_labels" >&5 -$as_echo "$pgac_cv_prog_CC_cflags__Wendif_labels" >&6; } -if test x"$pgac_cv_prog_CC_cflags__Wendif_labels" = x"yes"; then - CFLAGS="${CFLAGS} -Wendif-labels" +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wmissing_format_attribute" >&5 +$as_echo "$pgac_cv_prog_CC_cflags__Wmissing_format_attribute" >&6; } +if test x"$pgac_cv_prog_CC_cflags__Wmissing_format_attribute" = x"yes"; then + CFLAGS="${CFLAGS} -Wmissing-format-attribute" fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wendif-labels, for CXXFLAGS" >&5 -$as_echo_n "checking whether ${CXX} supports -Wendif-labels, for CXXFLAGS... " >&6; } -if ${pgac_cv_prog_CXX_cxxflags__Wendif_labels+:} false; then : + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wmissing-format-attribute, for CXXFLAGS" >&5 +$as_echo_n "checking whether ${CXX} supports -Wmissing-format-attribute, for CXXFLAGS... " >&6; } +if ${pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute+:} false; then : $as_echo_n "(cached) " >&6 else pgac_save_CXXFLAGS=$CXXFLAGS pgac_save_CXX=$CXX CXX=${CXX} -CXXFLAGS="${CXXFLAGS} -Wendif-labels" +CXXFLAGS="${CXXFLAGS} -Wmissing-format-attribute" ac_save_cxx_werror_flag=$ac_cxx_werror_flag ac_cxx_werror_flag=yes ac_ext=cpp @@ -5583,9 +5553,9 @@ main () } _ACEOF if ac_fn_cxx_try_compile "$LINENO"; then : - pgac_cv_prog_CXX_cxxflags__Wendif_labels=yes + pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute=yes else - pgac_cv_prog_CXX_cxxflags__Wendif_labels=no + pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext ac_ext=c @@ -5598,23 +5568,23 @@ ac_cxx_werror_flag=$ac_save_cxx_werror_flag CXXFLAGS="$pgac_save_CXXFLAGS" CXX="$pgac_save_CXX" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wendif_labels" >&5 -$as_echo "$pgac_cv_prog_CXX_cxxflags__Wendif_labels" >&6; } -if test x"$pgac_cv_prog_CXX_cxxflags__Wendif_labels" = x"yes"; then - CXXFLAGS="${CXXFLAGS} -Wendif-labels" +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute" >&5 +$as_echo "$pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute" >&6; } +if test x"$pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute" = x"yes"; then + CXXFLAGS="${CXXFLAGS} -Wmissing-format-attribute" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wmissing-format-attribute, for CFLAGS" >&5 -$as_echo_n "checking whether ${CC} supports -Wmissing-format-attribute, for CFLAGS... " >&6; } -if ${pgac_cv_prog_CC_cflags__Wmissing_format_attribute+:} false; then : +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wold-style-declaration, for CFLAGS" >&5 +$as_echo_n "checking whether ${CC} supports -Wold-style-declaration, for CFLAGS... " >&6; } +if ${pgac_cv_prog_CC_cflags__Wold_style_declaration+:} false; then : $as_echo_n "(cached) " >&6 else pgac_save_CFLAGS=$CFLAGS pgac_save_CC=$CC CC=${CC} -CFLAGS="${CFLAGS} -Wmissing-format-attribute" +CFLAGS="${CFLAGS} -Wold-style-declaration" ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes cat confdefs.h - <<_ACEOF >conftest.$ac_ext @@ -5629,39 +5599,41 @@ main () } _ACEOF if ac_fn_c_try_compile "$LINENO"; then : - pgac_cv_prog_CC_cflags__Wmissing_format_attribute=yes + pgac_cv_prog_CC_cflags__Wold_style_declaration=yes else - pgac_cv_prog_CC_cflags__Wmissing_format_attribute=no + pgac_cv_prog_CC_cflags__Wold_style_declaration=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="$pgac_save_CFLAGS" CC="$pgac_save_CC" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wmissing_format_attribute" >&5 -$as_echo "$pgac_cv_prog_CC_cflags__Wmissing_format_attribute" >&6; } -if test x"$pgac_cv_prog_CC_cflags__Wmissing_format_attribute" = x"yes"; then - CFLAGS="${CFLAGS} -Wmissing-format-attribute" +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wold_style_declaration" >&5 +$as_echo "$pgac_cv_prog_CC_cflags__Wold_style_declaration" >&6; } +if test x"$pgac_cv_prog_CC_cflags__Wold_style_declaration" = x"yes"; then + CFLAGS="${CFLAGS} -Wold-style-declaration" fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wmissing-format-attribute, for CXXFLAGS" >&5 -$as_echo_n "checking whether ${CXX} supports -Wmissing-format-attribute, for CXXFLAGS... " >&6; } -if ${pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute+:} false; then : + # -Wold-style-declaration is not applicable for C++ + + # To require fallthrough attribute annotations, use + # -Wimplicit-fallthrough=5 with gcc and -Wimplicit-fallthrough with + # clang. The latter is also accepted on gcc but does not enforce + # attribute annotations, so test the former first. + save_CFLAGS=$CFLAGS + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wimplicit-fallthrough=5, for CFLAGS" >&5 +$as_echo_n "checking whether ${CC} supports -Wimplicit-fallthrough=5, for CFLAGS... " >&6; } +if ${pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_5+:} false; then : $as_echo_n "(cached) " >&6 else - pgac_save_CXXFLAGS=$CXXFLAGS -pgac_save_CXX=$CXX -CXX=${CXX} -CXXFLAGS="${CXXFLAGS} -Wmissing-format-attribute" -ac_save_cxx_werror_flag=$ac_cxx_werror_flag -ac_cxx_werror_flag=yes -ac_ext=cpp -ac_cpp='$CXXCPP $CPPFLAGS' -ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' -ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' -ac_compiler_gnu=$ac_cv_cxx_compiler_gnu - + pgac_save_CFLAGS=$CFLAGS +pgac_save_CC=$CC +CC=${CC} +CFLAGS="${CFLAGS} -Wimplicit-fallthrough=5" +ac_save_c_werror_flag=$ac_c_werror_flag +ac_c_werror_flag=yes cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -5673,39 +5645,34 @@ main () return 0; } _ACEOF -if ac_fn_cxx_try_compile "$LINENO"; then : - pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute=yes +if ac_fn_c_try_compile "$LINENO"; then : + pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_5=yes else - pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute=no + pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_5=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -ac_ext=c -ac_cpp='$CPP $CPPFLAGS' -ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' -ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' -ac_compiler_gnu=$ac_cv_c_compiler_gnu - -ac_cxx_werror_flag=$ac_save_cxx_werror_flag -CXXFLAGS="$pgac_save_CXXFLAGS" -CXX="$pgac_save_CXX" +ac_c_werror_flag=$ac_save_c_werror_flag +CFLAGS="$pgac_save_CFLAGS" +CC="$pgac_save_CC" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute" >&5 -$as_echo "$pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute" >&6; } -if test x"$pgac_cv_prog_CXX_cxxflags__Wmissing_format_attribute" = x"yes"; then - CXXFLAGS="${CXXFLAGS} -Wmissing-format-attribute" +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_5" >&5 +$as_echo "$pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_5" >&6; } +if test x"$pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_5" = x"yes"; then + CFLAGS="${CFLAGS} -Wimplicit-fallthrough=5" fi + if test x"$save_CFLAGS" = x"$CFLAGS"; then -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wimplicit-fallthrough=3, for CFLAGS" >&5 -$as_echo_n "checking whether ${CC} supports -Wimplicit-fallthrough=3, for CFLAGS... " >&6; } -if ${pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_3+:} false; then : +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wimplicit-fallthrough, for CFLAGS" >&5 +$as_echo_n "checking whether ${CC} supports -Wimplicit-fallthrough, for CFLAGS... " >&6; } +if ${pgac_cv_prog_CC_cflags__Wimplicit_fallthrough+:} false; then : $as_echo_n "(cached) " >&6 else pgac_save_CFLAGS=$CFLAGS pgac_save_CC=$CC CC=${CC} -CFLAGS="${CFLAGS} -Wimplicit-fallthrough=3" +CFLAGS="${CFLAGS} -Wimplicit-fallthrough" ac_save_c_werror_flag=$ac_c_werror_flag ac_c_werror_flag=yes cat confdefs.h - <<_ACEOF >conftest.$ac_ext @@ -5720,31 +5687,85 @@ main () } _ACEOF if ac_fn_c_try_compile "$LINENO"; then : - pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_3=yes + pgac_cv_prog_CC_cflags__Wimplicit_fallthrough=yes else - pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_3=no + pgac_cv_prog_CC_cflags__Wimplicit_fallthrough=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext ac_c_werror_flag=$ac_save_c_werror_flag CFLAGS="$pgac_save_CFLAGS" CC="$pgac_save_CC" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_3" >&5 -$as_echo "$pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_3" >&6; } -if test x"$pgac_cv_prog_CC_cflags__Wimplicit_fallthrough_3" = x"yes"; then - CFLAGS="${CFLAGS} -Wimplicit-fallthrough=3" +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wimplicit_fallthrough" >&5 +$as_echo "$pgac_cv_prog_CC_cflags__Wimplicit_fallthrough" >&6; } +if test x"$pgac_cv_prog_CC_cflags__Wimplicit_fallthrough" = x"yes"; then + CFLAGS="${CFLAGS} -Wimplicit-fallthrough" +fi + + + fi + save_CXXFLAGS=$CXXFLAGS + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wimplicit-fallthrough=5, for CXXFLAGS" >&5 +$as_echo_n "checking whether ${CXX} supports -Wimplicit-fallthrough=5, for CXXFLAGS... " >&6; } +if ${pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_5+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_save_CXXFLAGS=$CXXFLAGS +pgac_save_CXX=$CXX +CXX=${CXX} +CXXFLAGS="${CXXFLAGS} -Wimplicit-fallthrough=5" +ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_5=yes +else + pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_5=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +ac_cxx_werror_flag=$ac_save_cxx_werror_flag +CXXFLAGS="$pgac_save_CXXFLAGS" +CXX="$pgac_save_CXX" +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_5" >&5 +$as_echo "$pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_5" >&6; } +if test x"$pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_5" = x"yes"; then + CXXFLAGS="${CXXFLAGS} -Wimplicit-fallthrough=5" fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wimplicit-fallthrough=3, for CXXFLAGS" >&5 -$as_echo_n "checking whether ${CXX} supports -Wimplicit-fallthrough=3, for CXXFLAGS... " >&6; } -if ${pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_3+:} false; then : + if test x"$save_CXXFLAGS" = x"$CXXFLAGS"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wimplicit-fallthrough, for CXXFLAGS" >&5 +$as_echo_n "checking whether ${CXX} supports -Wimplicit-fallthrough, for CXXFLAGS... " >&6; } +if ${pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough+:} false; then : $as_echo_n "(cached) " >&6 else pgac_save_CXXFLAGS=$CXXFLAGS pgac_save_CXX=$CXX CXX=${CXX} -CXXFLAGS="${CXXFLAGS} -Wimplicit-fallthrough=3" +CXXFLAGS="${CXXFLAGS} -Wimplicit-fallthrough" ac_save_cxx_werror_flag=$ac_cxx_werror_flag ac_cxx_werror_flag=yes ac_ext=cpp @@ -5765,9 +5786,9 @@ main () } _ACEOF if ac_fn_cxx_try_compile "$LINENO"; then : - pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_3=yes + pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough=yes else - pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_3=no + pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough=no fi rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext ac_ext=c @@ -5780,13 +5801,15 @@ ac_cxx_werror_flag=$ac_save_cxx_werror_flag CXXFLAGS="$pgac_save_CXXFLAGS" CXX="$pgac_save_CXX" fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_3" >&5 -$as_echo "$pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_3" >&6; } -if test x"$pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough_3" = x"yes"; then - CXXFLAGS="${CXXFLAGS} -Wimplicit-fallthrough=3" +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough" >&5 +$as_echo "$pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough" >&6; } +if test x"$pgac_cv_prog_CXX_cxxflags__Wimplicit_fallthrough" = x"yes"; then + CXXFLAGS="${CXXFLAGS} -Wimplicit-fallthrough" fi + fi + { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wcast-function-type, for CFLAGS" >&5 $as_echo_n "checking whether ${CC} supports -Wcast-function-type, for CFLAGS... " >&6; } @@ -6890,7 +6913,7 @@ fi # __attribute__((visibility("hidden"))) is supported, if we encounter a # compiler that supports one of the supported variants of -fvisibility=hidden # but uses a different syntax to mark a symbol as exported. -if test "$GCC" = yes -o "$SUN_STUDIO_CC" = yes ; then +if test "$GCC" = yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -fvisibility=hidden, for CFLAGS_SL_MODULE" >&5 $as_echo_n "checking whether ${CC} supports -fvisibility=hidden, for CFLAGS_SL_MODULE... " >&6; } if ${pgac_cv_prog_CC_cflags__fvisibility_hidden+:} false; then : @@ -7677,29 +7700,6 @@ fi rm -f core conftest.err conftest.$ac_objext \ conftest$ac_exeext conftest.$ac_ext -# Defend against gcc -ffast-math -if test "$GCC" = yes; then -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -int -main () -{ -#ifdef __FAST_MATH__ -choke me -#endif - ; - return 0; -} -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - -else - as_fn_error $? "do not put -ffast-math in CFLAGS" "$LINENO" 5 -fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext -fi - # Defend against clang being used on x86-32 without SSE2 enabled. As current # versions of clang do not understand -fexcess-precision=standard, the use of # x87 floating point operations leads to problems like isinf possibly returning @@ -7873,20 +7873,6 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu -# -# Set up TAS assembly code if needed; the template file has now had its -# chance to request this. -# -ac_config_links="$ac_config_links src/backend/port/tas.s:src/backend/port/tas/${tas_file}" - - -if test "$need_tas" = yes ; then - TAS=tas.o -else - TAS="" -fi - - cat >>confdefs.h <<_ACEOF #define DLSUFFIX "$DLSUFFIX" @@ -9436,12 +9422,12 @@ fi # Note the user could also set XML2_CFLAGS/XML2_LIBS directly for pgac_option in $XML2_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) INCLUDES="$INCLUDES $pgac_option";; esac done for pgac_option in $XML2_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBDIRS="$LIBDIRS $pgac_option";; esac done fi @@ -9666,12 +9652,12 @@ fi # note that -llz4 will be added by AC_CHECK_LIB below. for pgac_option in $LZ4_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) INCLUDES="$INCLUDES $pgac_option";; esac done for pgac_option in $LZ4_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBDIRS="$LIBDIRS $pgac_option";; esac done fi @@ -9807,12 +9793,12 @@ fi # note that -lzstd will be added by AC_CHECK_LIB below. for pgac_option in $ZSTD_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) INCLUDES="$INCLUDES $pgac_option";; esac done for pgac_option in $ZSTD_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBDIRS="$LIBDIRS $pgac_option";; esac done fi @@ -10328,27 +10314,68 @@ do IFS=$as_save_IFS fi - - test -d ./--version && rmdir ./--version - if test "${ac_cv_path_mkdir+set}" = set; then - MKDIR_P="$ac_cv_path_mkdir -p" - else - # As a last resort, use the slow shell script. Don't cache a - # value for MKDIR_P within a source directory, because that will - # break other packages using the cache if that directory is - # removed, or if the value is a relative name. - MKDIR_P="$ac_install_sh -d" - fi + + test -d ./--version && rmdir ./--version + if test "${ac_cv_path_mkdir+set}" = set; then + MKDIR_P="$ac_cv_path_mkdir -p" + else + # As a last resort, use the slow shell script. Don't cache a + # value for MKDIR_P within a source directory, because that will + # break other packages using the cache if that directory is + # removed, or if the value is a relative name. + MKDIR_P="$ac_install_sh -d" + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $MKDIR_P" >&5 +$as_echo "$MKDIR_P" >&6; } + +# When Autoconf chooses install-sh as mkdir -p program it tries to generate +# a relative path to it in each makefile where it substitutes it. This clashes +# with our Makefile.global concept. This workaround helps. +case $MKDIR_P in + *install-sh*) MKDIR_P='\${SHELL} \${top_srcdir}/config/install-sh -c -d';; +esac + +# Extract the first word of "nm", so it can be a program name with args. +set dummy nm; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_path_NM+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $NM in + [\\/]* | ?:[\\/]*) + ac_cv_path_NM="$NM" # Let the user override the test with a path. + ;; + *) + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_path_NM="$as_dir/$ac_word$ac_exec_ext" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + + ;; +esac +fi +NM=$ac_cv_path_NM +if test -n "$NM"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NM" >&5 +$as_echo "$NM" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $MKDIR_P" >&5 -$as_echo "$MKDIR_P" >&6; } -# When Autoconf chooses install-sh as mkdir -p program it tries to generate -# a relative path to it in each makefile where it substitutes it. This clashes -# with our Makefile.global concept. This workaround helps. -case $MKDIR_P in - *install-sh*) MKDIR_P='\${SHELL} \${top_srcdir}/config/install-sh -c -d';; -esac + if test -z "$BISON"; then for ac_prog in bison @@ -10758,6 +10785,11 @@ python_ldlibrary=`${PYTHON} -c "import sysconfig; print(' '.join(filter(None,sys # If LDLIBRARY exists and has a shlib extension, use it verbatim. ldlibrary=`echo "${python_ldlibrary}" | sed -e 's/\.so$//' -e 's/\.dll$//' -e 's/\.dylib$//' -e 's/\.sl$//'` +if test "$PORTNAME" = "aix"; then + # On AIX, '.a' should also be believed to be a shlib. + ldlibrary=`echo "${ldlibrary}" | sed -e 's/\.a$//'` +fi + if test -e "${python_libdir}/${python_ldlibrary}" -a x"${python_ldlibrary}" != x"${ldlibrary}" then ldlibrary=`echo "${ldlibrary}" | sed "s/^lib//"` @@ -12717,6 +12749,15 @@ fi if test "$with_libcurl" = yes ; then + # libcurl compiler/linker flags are kept separate from the global flags, so + # they have to be added back temporarily for the following tests. + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$CPPFLAGS $LIBCURL_CPPFLAGS" + LDFLAGS="$LDFLAGS $LIBCURL_LDFLAGS" + ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" if test "x$ac_cv_header_curl_curl_h" = xyes; then : @@ -12725,6 +12766,9 @@ else fi + + # LIBCURL_LDLIBS is determined here. Like the compiler flags, it should not + # pollute the global LIBS setting. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_multi_init in -lcurl" >&5 $as_echo_n "checking for curl_multi_init in -lcurl... " >&6; } if ${ac_cv_lib_curl_curl_multi_init+:} false; then : @@ -12774,12 +12818,6 @@ else fi - pgac_save_CPPFLAGS=$CPPFLAGS - pgac_save_LDFLAGS=$LDFLAGS - pgac_save_LIBS=$LIBS - - CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" - LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" LIBS="$LIBCURL_LDLIBS $LIBS" # Check to see whether the current platform supports threadsafe Curl @@ -13139,7 +13177,7 @@ fi done # Function introduced in OpenSSL 1.1.1, not in LibreSSL. - for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback + for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb do : as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" @@ -13309,6 +13347,23 @@ fi fi +if test "$with_liburing" = yes; then + _LIBS="$LIBS" + LIBS="$LIBURING_LIBS $LIBS" + for ac_func in io_uring_queue_init_mem +do : + ac_fn_c_check_func "$LINENO" "io_uring_queue_init_mem" "ac_cv_func_io_uring_queue_init_mem" +if test "x$ac_cv_func_io_uring_queue_init_mem" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_IO_URING_QUEUE_INIT_MEM 1 +_ACEOF + +fi +done + + LIBS="$_LIBS" +fi + if test "$with_lz4" = yes ; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for LZ4_compress_default in -llz4" >&5 $as_echo_n "checking for LZ4_compress_default in -llz4... " >&6; } @@ -13409,7 +13464,8 @@ fi fi -# Note: We can test for libldap_r only after we know PTHREAD_LIBS +# Note: We can test for libldap_r only after we know PTHREAD_LIBS; +# also, on AIX, we may need to have openssl in LIBS for this step. if test "$with_ldap" = yes ; then _LIBS="$LIBS" if test "$PORTNAME" != "win32"; then @@ -13792,7 +13848,7 @@ fi ## Header files ## -for ac_header in atomic.h copyfile.h execinfo.h getopt.h ifaddrs.h mbarrier.h sys/epoll.h sys/event.h sys/personality.h sys/prctl.h sys/procctl.h sys/signalfd.h sys/ucred.h termios.h ucred.h xlocale.h +for ac_header in copyfile.h execinfo.h getopt.h ifaddrs.h sys/epoll.h sys/event.h sys/personality.h sys/prctl.h sys/procctl.h sys/signalfd.h sys/ucred.h termios.h uchar.h ucred.h xlocale.h do : as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` ac_fn_c_check_header_mongrel "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default" @@ -14772,50 +14828,8 @@ $as_echo "#define AC_APPLE_UNIVERSAL_BUILD 1" >>confdefs.h presetting ac_cv_c_bigendian=no (or yes) will help" "$LINENO" 5 ;; esac -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for inline" >&5 -$as_echo_n "checking for inline... " >&6; } -if ${ac_cv_c_inline+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_cv_c_inline=no -for ac_kw in inline __inline__ __inline; do - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ -#ifndef __cplusplus -typedef int foo_t; -static $ac_kw foo_t static_foo () {return 0; } -$ac_kw foo_t foo () {return 0; } -#endif - -_ACEOF -if ac_fn_c_try_compile "$LINENO"; then : - ac_cv_c_inline=$ac_kw -fi -rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext - test "$ac_cv_c_inline" != no && break -done - -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_inline" >&5 -$as_echo "$ac_cv_c_inline" >&6; } - -case $ac_cv_c_inline in - inline | yes) ;; - *) - case $ac_cv_c_inline in - no) ac_val=;; - *) ac_val=$ac_cv_c_inline;; - esac - cat >>confdefs.h <<_ACEOF -#ifndef __cplusplus -#define inline $ac_val -#endif -_ACEOF - ;; -esac - -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for printf format archetype" >&5 -$as_echo_n "checking for printf format archetype... " >&6; } +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C printf format archetype" >&5 +$as_echo_n "checking for C printf format archetype... " >&6; } if ${pgac_cv_printf_archetype+:} false; then : $as_echo_n "(cached) " >&6 else @@ -14875,41 +14889,99 @@ fi $as_echo "$pgac_cv_printf_archetype" >&6; } cat >>confdefs.h <<_ACEOF -#define PG_PRINTF_ATTRIBUTE $pgac_cv_printf_archetype +#define PG_C_PRINTF_ATTRIBUTE $pgac_cv_printf_archetype _ACEOF -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for _Static_assert" >&5 -$as_echo_n "checking for _Static_assert... " >&6; } -if ${pgac_cv__static_assert+:} false; then : +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C++ printf format archetype" >&5 +$as_echo_n "checking for C++ printf format archetype... " >&6; } +if ${pgac_cv_cxx_printf_archetype+:} false; then : $as_echo_n "(cached) " >&6 else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ + pgac_cv_cxx_printf_archetype=gnu_printf +ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +extern void pgac_write(int ignore, const char *fmt,...) +__attribute__((format($pgac_cv_cxx_printf_archetype, 2, 3))); int main () { -({ _Static_assert(1, "foo"); }) +pgac_write(0, "error %s: %m", "foo"); ; return 0; } _ACEOF -if ac_fn_c_try_link "$LINENO"; then : - pgac_cv__static_assert=yes +if ac_fn_cxx_try_compile "$LINENO"; then : + ac_archetype_ok=yes else - pgac_cv__static_assert=no + ac_archetype_ok=no fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +ac_cxx_werror_flag=$ac_save_cxx_werror_flag + +if [ "$ac_archetype_ok" = no ]; then + pgac_cv_cxx_printf_archetype=__syslog__ + ac_save_cxx_werror_flag=$ac_cxx_werror_flag +ac_cxx_werror_flag=yes +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +extern void pgac_write(int ignore, const char *fmt,...) +__attribute__((format($pgac_cv_cxx_printf_archetype, 2, 3))); +int +main () +{ +pgac_write(0, "error %s: %m", "foo"); + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + ac_archetype_ok=yes +else + ac_archetype_ok=no fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__static_assert" >&5 -$as_echo "$pgac_cv__static_assert" >&6; } -if test x"$pgac_cv__static_assert" = xyes ; then +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu -$as_echo "#define HAVE__STATIC_ASSERT 1" >>confdefs.h +ac_cxx_werror_flag=$ac_save_cxx_werror_flag + if [ "$ac_archetype_ok" = no ]; then + pgac_cv_cxx_printf_archetype=printf + fi +fi fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_cxx_printf_archetype" >&5 +$as_echo "$pgac_cv_cxx_printf_archetype" >&6; } + +cat >>confdefs.h <<_ACEOF +#define PG_CXX_PRINTF_ATTRIBUTE $pgac_cv_cxx_printf_archetype +_ACEOF + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for typeof" >&5 $as_echo_n "checking for typeof... " >&6; } if ${pgac_cv_c_typeof+:} false; then : @@ -14938,16 +15010,174 @@ rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext test "$pgac_cv_c_typeof" != no && break done fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_c_typeof" >&5 -$as_echo "$pgac_cv_c_typeof" >&6; } -if test "$pgac_cv_c_typeof" != no; then +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_c_typeof" >&5 +$as_echo "$pgac_cv_c_typeof" >&6; } +if test "$pgac_cv_c_typeof" != no; then + +$as_echo "#define HAVE_TYPEOF 1" >>confdefs.h + + if test "$pgac_cv_c_typeof" != typeof; then + +cat >>confdefs.h <<_ACEOF +#define typeof $pgac_cv_c_typeof +_ACEOF + + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C++ typeof" >&5 +$as_echo_n "checking for C++ typeof... " >&6; } +if ${pgac_cv_cxx_typeof+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_cv_cxx_typeof=no +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +for pgac_kw in typeof __typeof__; do + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ +int x = 0; +$pgac_kw(x) y; +y = x; +return y; + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + pgac_cv_cxx_typeof=$pgac_kw +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + test "$pgac_cv_cxx_typeof" != no && break +done +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_cxx_typeof" >&5 +$as_echo "$pgac_cv_cxx_typeof" >&6; } +if test "$pgac_cv_cxx_typeof" != no; then + +$as_echo "#define HAVE_CXX_TYPEOF 1" >>confdefs.h + + if test "$pgac_cv_cxx_typeof" != typeof; then + +cat >>confdefs.h <<_ACEOF +#define pg_cxx_typeof $pgac_cv_cxx_typeof +_ACEOF + + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for typeof_unqual" >&5 +$as_echo_n "checking for typeof_unqual... " >&6; } +if ${pgac_cv_c_typeof_unqual+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_cv_c_typeof_unqual=no +# Test with a void pointer, because MSVC doesn't handle that, and we +# need that for copyObject(). +for pgac_kw in typeof_unqual __typeof_unqual__; do + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ +int x = 0; +$pgac_kw(x) y; +const void *a; +void *b; +y = x; +b = ($pgac_kw(*a) *) a; +return y; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + pgac_cv_c_typeof_unqual=$pgac_kw +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + test "$pgac_cv_c_typeof_unqual" != no && break +done +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_c_typeof_unqual" >&5 +$as_echo "$pgac_cv_c_typeof_unqual" >&6; } +if test "$pgac_cv_c_typeof_unqual" != no; then + +$as_echo "#define HAVE_TYPEOF_UNQUAL 1" >>confdefs.h + + if test "$pgac_cv_c_typeof_unqual" != typeof_unqual; then + +cat >>confdefs.h <<_ACEOF +#define typeof_unqual $pgac_cv_c_typeof_unqual +_ACEOF + + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for C++ typeof_unqual" >&5 +$as_echo_n "checking for C++ typeof_unqual... " >&6; } +if ${pgac_cv_cxx_typeof_unqual+:} false; then : + $as_echo_n "(cached) " >&6 +else + pgac_cv_cxx_typeof_unqual=no +ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + +for pgac_kw in typeof_unqual __typeof_unqual__; do + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ +int x = 0; +$pgac_kw(x) y; +const void *a; +void *b; +y = x; +b = ($pgac_kw(*a) *) a; +return y; + ; + return 0; +} +_ACEOF +if ac_fn_cxx_try_compile "$LINENO"; then : + pgac_cv_cxx_typeof_unqual=$pgac_kw +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + test "$pgac_cv_cxx_typeof_unqual" != no && break +done +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_cxx_typeof_unqual" >&5 +$as_echo "$pgac_cv_cxx_typeof_unqual" >&6; } +if test "$pgac_cv_cxx_typeof_unqual" != no; then -$as_echo "#define HAVE_TYPEOF 1" >>confdefs.h +$as_echo "#define HAVE_CXX_TYPEOF_UNQUAL 1" >>confdefs.h - if test "$pgac_cv_c_typeof" != typeof; then + if test "$pgac_cv_cxx_typeof_unqual" != typeof_unqual; then cat >>confdefs.h <<_ACEOF -#define typeof $pgac_cv_c_typeof +#define pg_cxx_typeof_unqual $pgac_cv_cxx_typeof_unqual _ACEOF fi @@ -15166,8 +15396,8 @@ fi # MSVC doesn't cope well with defining restrict to __restrict, the # spelling it understands, because it conflicts with -# __declspec(restrict). Therefore we define pg_restrict to the -# appropriate definition, which presumably won't conflict. +# __declspec(restrict) in C++ mode. Therefore we define pg_restrict to +# the appropriate definition, which presumably won't conflict. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for C/C++ restrict keyword" >&5 $as_echo_n "checking for C/C++ restrict keyword... " >&6; } if ${ac_cv_c_restrict+:} false; then : @@ -15562,11 +15792,53 @@ _ACEOF # If we don't have largefile support, can't handle segment size >= 2GB. if test "$ac_cv_sizeof_off_t" -lt 8; then - if expr $RELSEG_SIZE '*' $blocksize '>=' 2 '*' 1024 '*' 1024; then + if expr $RELSEG_SIZE '*' $blocksize '>=' 2 '*' 1024 '*' 1024 >/dev/null; then as_fn_error $? "Large file support is not enabled. Segment size cannot be larger than 1GB." "$LINENO" 5 fi fi +# Check for SA_SIGINFO extended signal handler availability +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for SA_SIGINFO" >&5 +$as_echo_n "checking for SA_SIGINFO... " >&6; } +if ${ac_cv_have_sa_siginfo+:} false; then : + $as_echo_n "(cached) " >&6 +else + + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + + #include + #include + +int +main () +{ + + struct sigaction sa; + sa.sa_flags = SA_SIGINFO; + + ; + return 0; +} + +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_have_sa_siginfo=yes +else + ac_cv_have_sa_siginfo=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_have_sa_siginfo" >&5 +$as_echo "$ac_cv_have_sa_siginfo" >&6; } + +if test "x$ac_cv_have_sa_siginfo" = "xyes"; then + +$as_echo "#define HAVE_SA_SIGINFO 1" >>confdefs.h + +fi ## ## Functions, global variables @@ -15616,7 +15888,7 @@ fi LIBS_including_readline="$LIBS" LIBS=`echo "$LIBS" | sed -e 's/-ledit//g' -e 's/-lreadline//g'` -for ac_func in backtrace_symbols copyfile copy_file_range elf_aux_info getauxval getifaddrs getpeerucred inet_pton kqueue localeconv_l mbstowcs_l posix_fallocate ppoll pthread_is_threaded_np setproctitle setproctitle_fast strsignal syncfs sync_file_range uselocale wcstombs_l +for ac_func in backtrace_symbols copyfile copy_file_range elf_aux_info explicit_memset getauxval getifaddrs getpeerucred inet_pton kqueue localeconv_l mbstowcs_l memset_explicit posix_fallocate ppoll pthread_is_threaded_np setproctitle setproctitle_fast strsignal syncfs sync_file_range uselocale wcstombs_l do : as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" @@ -15820,44 +16092,6 @@ cat >>confdefs.h <<_ACEOF #define HAVE__BUILTIN_CTZ 1 _ACEOF -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for __builtin_popcount" >&5 -$as_echo_n "checking for __builtin_popcount... " >&6; } -if ${pgac_cv__builtin_popcount+:} false; then : - $as_echo_n "(cached) " >&6 -else - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -int -call__builtin_popcount(unsigned int x) -{ - return __builtin_popcount(x); -} -int -main () -{ - - ; - return 0; -} -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - pgac_cv__builtin_popcount=yes -else - pgac_cv__builtin_popcount=no -fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__builtin_popcount" >&5 -$as_echo "$pgac_cv__builtin_popcount" >&6; } -if test x"${pgac_cv__builtin_popcount}" = xyes ; then - -cat >>confdefs.h <<_ACEOF -#define HAVE__BUILTIN_POPCOUNT 1 -_ACEOF - fi # __builtin_frame_address may draw a diagnostic for non-constant argument, # so it needs a different test function. @@ -16122,16 +16356,6 @@ fi cat >>confdefs.h <<_ACEOF #define HAVE_DECL_STRLCPY $ac_have_decl _ACEOF -ac_fn_c_check_decl "$LINENO" "strnlen" "ac_cv_have_decl_strnlen" "$ac_includes_default" -if test "x$ac_cv_have_decl_strnlen" = xyes; then : - ac_have_decl=1 -else - ac_have_decl=0 -fi - -cat >>confdefs.h <<_ACEOF -#define HAVE_DECL_STRNLEN $ac_have_decl -_ACEOF ac_fn_c_check_decl "$LINENO" "strsep" "ac_cv_have_decl_strsep" "$ac_includes_default" if test "x$ac_cv_have_decl_strsep" = xyes; then : ac_have_decl=1 @@ -16311,19 +16535,6 @@ esac fi -ac_fn_c_check_func "$LINENO" "strnlen" "ac_cv_func_strnlen" -if test "x$ac_cv_func_strnlen" = xyes; then : - $as_echo "#define HAVE_STRNLEN 1" >>confdefs.h - -else - case " $LIBOBJS " in - *" strnlen.$ac_objext "* ) ;; - *) LIBOBJS="$LIBOBJS strnlen.$ac_objext" - ;; -esac - -fi - ac_fn_c_check_func "$LINENO" "strsep" "ac_cv_func_strsep" if test "x$ac_cv_func_strsep" = xyes; then : $as_echo "#define HAVE_STRSEP 1" >>confdefs.h @@ -16635,7 +16846,7 @@ fi if test "$with_icu" = yes; then ac_save_CPPFLAGS=$CPPFLAGS - CPPFLAGS="$ICU_CFLAGS $CPPFLAGS" + CPPFLAGS="$CPPFLAGS $ICU_CFLAGS" # Verify we have ICU's header files ac_fn_c_check_header_mongrel "$LINENO" "unicode/ucol.h" "ac_cv_header_unicode_ucol_h" "$ac_includes_default" @@ -16650,38 +16861,6 @@ fi CPPFLAGS=$ac_save_CPPFLAGS fi -if test "$with_llvm" = yes; then - - # Check which functionality is present - SAVE_CPPFLAGS="$CPPFLAGS" - CPPFLAGS="$CPPFLAGS $LLVM_CPPFLAGS" - ac_fn_c_check_decl "$LINENO" "LLVMCreateGDBRegistrationListener" "ac_cv_have_decl_LLVMCreateGDBRegistrationListener" "#include -" -if test "x$ac_cv_have_decl_LLVMCreateGDBRegistrationListener" = xyes; then : - ac_have_decl=1 -else - ac_have_decl=0 -fi - -cat >>confdefs.h <<_ACEOF -#define HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER $ac_have_decl -_ACEOF -ac_fn_c_check_decl "$LINENO" "LLVMCreatePerfJITEventListener" "ac_cv_have_decl_LLVMCreatePerfJITEventListener" "#include -" -if test "x$ac_cv_have_decl_LLVMCreatePerfJITEventListener" = xyes; then : - ac_have_decl=1 -else - ac_have_decl=0 -fi - -cat >>confdefs.h <<_ACEOF -#define HAVE_DECL_LLVMCREATEPERFJITEVENTLISTENER $ac_have_decl -_ACEOF - - CPPFLAGS="$SAVE_CPPFLAGS" - -fi - # Lastly, restore full LIBS list and check for readline/libedit symbols LIBS="$LIBS_including_readline" @@ -16849,6 +17028,14 @@ rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ fi +# These flags are supported in all C11-capable GCC/Clang versions, +# so no capability test is needed. Added here to avoid affecting configure probes, +# particularly PGAC_PRINTF_ARCHETYPE which uses -Werror and would fail to detect +# gnu_printf if -Wstrict-prototypes is active. +if test "$GCC" = yes -a "$ICC" = no; then + CFLAGS="$CFLAGS -Wstrict-prototypes -Wold-style-definition" +fi + # -------------------- # Run tests below here # -------------------- @@ -16986,6 +17173,39 @@ cat >>confdefs.h <<_ACEOF _ACEOF +# The cast to long int works around a bug in the HP C Compiler +# version HP92453-01 B.11.11.23709.GP, which incorrectly rejects +# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'. +# This bug is HP SR number 8606223364. +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking size of intmax_t" >&5 +$as_echo_n "checking size of intmax_t... " >&6; } +if ${ac_cv_sizeof_intmax_t+:} false; then : + $as_echo_n "(cached) " >&6 +else + if ac_fn_c_compute_int "$LINENO" "(long int) (sizeof (intmax_t))" "ac_cv_sizeof_intmax_t" "$ac_includes_default"; then : + +else + if test "$ac_cv_type_intmax_t" = yes; then + { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error 77 "cannot compute sizeof (intmax_t) +See \`config.log' for more details" "$LINENO" 5; } + else + ac_cv_sizeof_intmax_t=0 + fi +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_intmax_t" >&5 +$as_echo "$ac_cv_sizeof_intmax_t" >&6; } + + + +cat >>confdefs.h <<_ACEOF +#define SIZEOF_INTMAX_T $ac_cv_sizeof_intmax_t +_ACEOF + + # Determine memory alignment requirements for the basic C data types. @@ -17059,41 +17279,6 @@ cat >>confdefs.h <<_ACEOF _ACEOF -# The cast to long int works around a bug in the HP C Compiler, -# see AC_CHECK_SIZEOF for more information. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking alignment of long" >&5 -$as_echo_n "checking alignment of long... " >&6; } -if ${ac_cv_alignof_long+:} false; then : - $as_echo_n "(cached) " >&6 -else - if ac_fn_c_compute_int "$LINENO" "(long int) offsetof (ac__type_alignof_, y)" "ac_cv_alignof_long" "$ac_includes_default -#ifndef offsetof -# define offsetof(type, member) ((char *) &((type *) 0)->member - (char *) 0) -#endif -typedef struct { char x; long y; } ac__type_alignof_;"; then : - -else - if test "$ac_cv_type_long" = yes; then - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error 77 "cannot compute alignment of long -See \`config.log' for more details" "$LINENO" 5; } - else - ac_cv_alignof_long=0 - fi -fi - -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_alignof_long" >&5 -$as_echo "$ac_cv_alignof_long" >&6; } - - - -cat >>confdefs.h <<_ACEOF -#define ALIGNOF_LONG $ac_cv_alignof_long -_ACEOF - - # The cast to long int works around a bug in the HP C Compiler, # see AC_CHECK_SIZEOF for more information. { $as_echo "$as_me:${as_lineno-$LINENO}: checking alignment of int64_t" >&5 @@ -17167,27 +17352,18 @@ _ACEOF # Compute maximum alignment of any basic type. # -# We require 'double' to have the strictest alignment among the basic types, -# because otherwise the C ABI might impose 8-byte alignment on some of the -# other C types that correspond to TYPALIGN_DOUBLE SQL types. That could -# cause a mismatch between the tuple layout and the C struct layout of a -# catalog tuple. We used to carefully order catalog columns such that any -# fixed-width, attalign=4 columns were at offsets divisible by 8 regardless -# of MAXIMUM_ALIGNOF to avoid that, but we no longer support any platforms -# where TYPALIGN_DOUBLE != MAXIMUM_ALIGNOF. -# -# We assume without checking that long's alignment is at least as strong as -# char, short, or int. Note that we intentionally do not consider any types -# wider than 64 bits, as allowing MAXIMUM_ALIGNOF to exceed 8 would be too -# much of a penalty for disk and memory space. +# We assume without checking that the maximum alignment requirement is that +# of int64_t and/or double. (On most platforms those are the same, but not +# everywhere.) For historical reasons, both int8 and float8 datatypes have +# typalign 'd', and therefore will be aligned per ALIGNOF_DOUBLE in database +# tuples even if ALIGNOF_INT64_T is more. Note that we intentionally do not +# consider any types wider than 64 bits, as allowing MAXIMUM_ALIGNOF to exceed +# 8 would be too much of a penalty for disk and memory space. -MAX_ALIGNOF=$ac_cv_alignof_double - -if test $ac_cv_alignof_long -gt $MAX_ALIGNOF ; then - as_fn_error $? "alignment of 'long' is greater than the alignment of 'double'" "$LINENO" 5 -fi -if test $ac_cv_alignof_int64_t -gt $MAX_ALIGNOF ; then - as_fn_error $? "alignment of 'int64_t' is greater than the alignment of 'double'" "$LINENO" 5 +if test $ac_cv_alignof_int64_t -gt $ac_cv_alignof_double ; then + MAX_ALIGNOF=$ac_cv_alignof_int64_t +else + MAX_ALIGNOF=$ac_cv_alignof_double fi cat >>confdefs.h <<_ACEOF @@ -17260,7 +17436,7 @@ else /* end confdefs.h. */ /* This must match the corresponding code in c.h: */ -#if defined(__GNUC__) || defined(__SUNPRO_C) +#if defined(__GNUC__) #define pg_attribute_aligned(a) __attribute__((aligned(a))) #elif defined(_MSC_VER) #define pg_attribute_aligned(a) __declspec(align(a)) @@ -17542,7 +17718,7 @@ $as_echo "#define HAVE_GCC__ATOMIC_INT64_CAS 1" >>confdefs.h fi -# Check for x86 cpuid instruction +# Check for __get_cpuid() and __cpuid() { $as_echo "$as_me:${as_lineno-$LINENO}: checking for __get_cpuid" >&5 $as_echo_n "checking for __get_cpuid... " >&6; } if ${pgac_cv__get_cpuid+:} false; then : @@ -17575,76 +17751,79 @@ if test x"$pgac_cv__get_cpuid" = x"yes"; then $as_echo "#define HAVE__GET_CPUID 1" >>confdefs.h -fi - -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for __get_cpuid_count" >&5 -$as_echo_n "checking for __get_cpuid_count... " >&6; } -if ${pgac_cv__get_cpuid_count+:} false; then : +else + # __cpuid() + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for __cpuid" >&5 +$as_echo_n "checking for __cpuid... " >&6; } +if ${pgac_cv__cpuid+:} false; then : $as_echo_n "(cached) " >&6 else cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include +#include int main () { unsigned int exx[4] = {0, 0, 0, 0}; - __get_cpuid_count(7, 0, &exx[0], &exx[1], &exx[2], &exx[3]); + __cpuid(exx, 1); ; return 0; } _ACEOF if ac_fn_c_try_link "$LINENO"; then : - pgac_cv__get_cpuid_count="yes" + pgac_cv__cpuid="yes" else - pgac_cv__get_cpuid_count="no" + pgac_cv__cpuid="no" fi rm -f core conftest.err conftest.$ac_objext \ conftest$ac_exeext conftest.$ac_ext fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__get_cpuid_count" >&5 -$as_echo "$pgac_cv__get_cpuid_count" >&6; } -if test x"$pgac_cv__get_cpuid_count" = x"yes"; then +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__cpuid" >&5 +$as_echo "$pgac_cv__cpuid" >&6; } + if test x"$pgac_cv__cpuid" = x"yes"; then -$as_echo "#define HAVE__GET_CPUID_COUNT 1" >>confdefs.h +$as_echo "#define HAVE__CPUID 1" >>confdefs.h + fi fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for __cpuid" >&5 -$as_echo_n "checking for __cpuid... " >&6; } -if ${pgac_cv__cpuid+:} false; then : +# Check for __get_cpuid_count() +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for __get_cpuid_count" >&5 +$as_echo_n "checking for __get_cpuid_count... " >&6; } +if ${pgac_cv__get_cpuid_count+:} false; then : $as_echo_n "(cached) " >&6 else cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include +#include int main () { unsigned int exx[4] = {0, 0, 0, 0}; - __get_cpuid(exx[0], 1); + __get_cpuid_count(7, 0, &exx[0], &exx[1], &exx[2], &exx[3]); ; return 0; } _ACEOF if ac_fn_c_try_link "$LINENO"; then : - pgac_cv__cpuid="yes" + pgac_cv__get_cpuid_count="yes" else - pgac_cv__cpuid="no" + pgac_cv__get_cpuid_count="no" fi rm -f core conftest.err conftest.$ac_objext \ conftest$ac_exeext conftest.$ac_ext fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__cpuid" >&5 -$as_echo "$pgac_cv__cpuid" >&6; } -if test x"$pgac_cv__cpuid" = x"yes"; then +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__get_cpuid_count" >&5 +$as_echo "$pgac_cv__get_cpuid_count" >&6; } +if test x"$pgac_cv__get_cpuid_count" = x"yes"; then -$as_echo "#define HAVE__CPUID 1" >>confdefs.h +$as_echo "#define HAVE__GET_CPUID_COUNT 1" >>confdefs.h fi +# Check for __cpuidex() { $as_echo "$as_me:${as_lineno-$LINENO}: checking for __cpuidex" >&5 $as_echo_n "checking for __cpuidex... " >&6; } if ${pgac_cv__cpuidex+:} false; then : @@ -17652,12 +17831,16 @@ if ${pgac_cv__cpuidex+:} false; then : else cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include +#ifdef _MSC_VER + #include + #else + #include + #endif int main () { unsigned int exx[4] = {0, 0, 0, 0}; - __get_cpuidex(exx[0], 7, 0); + __cpuidex((int *) exx, 7, 0); ; return 0; @@ -17679,6 +17862,50 @@ $as_echo "#define HAVE__CPUIDEX 1" >>confdefs.h fi +# Check for AVX2 target support +# +if test x"$host_cpu" = x"x86_64"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for AVX2 target attribute support" >&5 +$as_echo_n "checking for AVX2 target attribute support... " >&6; } +if ${pgac_cv_avx2_support+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + __attribute__((target("avx2"))) + static int avx2_test(void) + { + return 0; + } +int +main () +{ +return avx2_test(); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + pgac_cv_avx2_support=yes +else + pgac_cv_avx2_support=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_avx2_support" >&5 +$as_echo "$pgac_cv_avx2_support" >&6; } +if test x"$pgac_cv_avx2_support" = x"yes"; then + pgac_avx2_support=yes +fi + + if test x"$pgac_avx2_support" = x"yes"; then + +$as_echo "#define USE_AVX2_WITH_RUNTIME_CHECK 1" >>confdefs.h + + fi +fi + # Check for XSAVE intrinsics # { $as_echo "$as_me:${as_lineno-$LINENO}: checking for _xgetbv" >&5 @@ -18157,7 +18384,7 @@ if test x"$USE_SSE42_CRC32C" = x"1"; then $as_echo "#define USE_SSE42_CRC32C 1" >>confdefs.h - PG_CRC32C_OBJS="pg_crc32c_sse42.o pg_crc32c_sse42_choose.o" + PG_CRC32C_OBJS="pg_crc32c_sse42.o" { $as_echo "$as_me:${as_lineno-$LINENO}: result: SSE 4.2" >&5 $as_echo "SSE 4.2" >&6; } else @@ -18165,7 +18392,7 @@ else $as_echo "#define USE_SSE42_CRC32C_WITH_RUNTIME_CHECK 1" >>confdefs.h - PG_CRC32C_OBJS="pg_crc32c_sse42.o pg_crc32c_sb8.o pg_crc32c_sse42_choose.o" + PG_CRC32C_OBJS="pg_crc32c_sse42.o pg_crc32c_sb8.o" { $as_echo "$as_me:${as_lineno-$LINENO}: result: SSE 4.2 with runtime check" >&5 $as_echo "SSE 4.2 with runtime check" >&6; } else @@ -18173,7 +18400,7 @@ $as_echo "SSE 4.2 with runtime check" >&6; } $as_echo "#define USE_ARMV8_CRC32C 1" >>confdefs.h - PG_CRC32C_OBJS="pg_crc32c_armv8.o" + PG_CRC32C_OBJS="pg_crc32c_armv8.o pg_crc32c_armv8_choose.o" { $as_echo "$as_me:${as_lineno-$LINENO}: result: ARMv8 CRC instructions" >&5 $as_echo "ARMv8 CRC instructions" >&6; } else @@ -18227,6 +18454,7 @@ else { __m128i z; + x = _mm512_xor_si512(_mm512_zextsi128_si512(_mm_cvtsi32_si128(0)), x); y = _mm512_clmulepi64_epi128(x, y, 0); z = _mm_ternarylogic_epi64( _mm512_castsi512_si128(y), @@ -18257,6 +18485,58 @@ if test x"$pgac_cv_avx512_pclmul_intrinsics" = x"yes"; then pgac_avx512_pclmul_intrinsics=yes fi +else + if test x"$host_cpu" = x"aarch64"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for pmull and pmull2" >&5 +$as_echo_n "checking for pmull and pmull2... " >&6; } +if ${pgac_cv_arm_pmull_+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +#include +uint64x2_t a; +uint64x2_t b; +uint64x2_t c; +uint64x2_t r1; +uint64x2_t r2; + + #if defined(__has_attribute) && __has_attribute (target) + __attribute__((target("+crypto"))) + #endif + static int pmull_test(void) + { + __asm("pmull %0.1q, %2.1d, %3.1d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r1), "+w"(c):"w"(a), "w"(b)); + __asm("pmull2 %0.1q, %2.2d, %3.2d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r2), "+w"(c):"w"(a), "w"(b)); + + r1 = veorq_u64(r1, r2); + /* return computed value, to prevent the above being optimized away */ + return (int) vgetq_lane_u64(r1, 0); + } +int +main () +{ +return pmull_test(); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + pgac_cv_arm_pmull_=yes +else + pgac_cv_arm_pmull_=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_arm_pmull_" >&5 +$as_echo "$pgac_cv_arm_pmull_" >&6; } +if test x"$pgac_cv_arm_pmull_" = x"yes"; then + pgac_arm_pmull=yes +fi + + fi fi { $as_echo "$as_me:${as_lineno-$LINENO}: checking for vectorized CRC-32C" >&5 @@ -18268,8 +18548,16 @@ $as_echo "#define USE_AVX512_CRC32C_WITH_RUNTIME_CHECK 1" >>confdefs.h { $as_echo "$as_me:${as_lineno-$LINENO}: result: AVX-512 with runtime check" >&5 $as_echo "AVX-512 with runtime check" >&6; } else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: none" >&5 + if test x"$pgac_arm_pmull" = x"yes"; then + +$as_echo "#define USE_PMULL_CRC32C_WITH_RUNTIME_CHECK 1" >>confdefs.h + + { $as_echo "$as_me:${as_lineno-$LINENO}: result: CRYPTO PMULL with runtime check" >&5 +$as_echo "CRYPTO PMULL with runtime check" >&6; } + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: none" >&5 $as_echo "none" >&6; } + fi fi # Select semaphore implementation type. @@ -18852,7 +19140,7 @@ Use --without-tcl to disable building PL/Tcl." "$LINENO" 5 fi # now that we have TCL_INCLUDE_SPEC, we can check for ac_save_CPPFLAGS=$CPPFLAGS - CPPFLAGS="$TCL_INCLUDE_SPEC $CPPFLAGS" + CPPFLAGS="$CPPFLAGS $TCL_INCLUDE_SPEC" ac_fn_c_check_header_mongrel "$LINENO" "tcl.h" "ac_cv_header_tcl_h" "$ac_includes_default" if test "x$ac_cv_header_tcl_h" = xyes; then : @@ -18921,7 +19209,7 @@ fi # check for if test "$with_python" = yes; then ac_save_CPPFLAGS=$CPPFLAGS - CPPFLAGS="$python_includespec $CPPFLAGS" + CPPFLAGS="$CPPFLAGS $python_includespec" ac_fn_c_check_header_mongrel "$LINENO" "Python.h" "ac_cv_header_Python_h" "$ac_includes_default" if test "x$ac_cv_header_Python_h" = xyes; then : @@ -19459,8 +19747,6 @@ fi if test x"$GCC" = x"yes" ; then cc_string=`${CC} --version | sed q` case $cc_string in [A-Za-z]*) ;; *) cc_string="GCC $cc_string";; esac -elif test x"$SUN_STUDIO_CC" = x"yes" ; then - cc_string=`${CC} -V 2>&1 | sed q` else cc_string=$CC fi @@ -20062,7 +20348,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by PostgreSQL $as_me 18beta1, which was +This file was extended by PostgreSQL $as_me 19devel, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -20133,7 +20419,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -PostgreSQL config.status 18beta1 +PostgreSQL config.status 19devel configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" @@ -20257,7 +20543,6 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 for ac_config_target in $ac_config_targets do case $ac_config_target in - "src/backend/port/tas.s") CONFIG_LINKS="$CONFIG_LINKS src/backend/port/tas.s:src/backend/port/tas/${tas_file}" ;; "GNUmakefile") CONFIG_FILES="$CONFIG_FILES GNUmakefile" ;; "src/Makefile.global") CONFIG_FILES="$CONFIG_FILES src/Makefile.global" ;; "src/backend/port/pg_sema.c") CONFIG_LINKS="$CONFIG_LINKS src/backend/port/pg_sema.c:${SEMA_IMPLEMENTATION}" ;; diff --git a/configure.ac b/configure.ac index 4b8335dc6138e..8d176bd3468e9 100644 --- a/configure.ac +++ b/configure.ac @@ -17,13 +17,13 @@ dnl Read the Autoconf manual for details. dnl m4_pattern_forbid(^PGAC_)dnl to catch undefined macros -AC_INIT([PostgreSQL], [18beta1], [pgsql-bugs@lists.postgresql.org], [], [https://www.postgresql.org/]) +AC_INIT([PostgreSQL], [19devel], [pgsql-bugs@lists.postgresql.org], [], [https://www.postgresql.org/]) m4_if(m4_defn([m4_PACKAGE_VERSION]), [2.69], [], [m4_fatal([Autoconf version 2.69 is required. Untested combinations of 'autoconf' and PostgreSQL versions are not recommended. You can remove the check from 'configure.ac' but it is then your responsibility whether the result works or not.])]) -AC_COPYRIGHT([Copyright (c) 1996-2025, PostgreSQL Global Development Group]) +AC_COPYRIGHT([Copyright (c) 1996-2026, PostgreSQL Global Development Group]) AC_CONFIG_SRCDIR([src/backend/access/common/heaptuple.c]) AC_CONFIG_AUX_DIR(config) AC_PREFIX_DEFAULT(/usr/local/pgsql) @@ -62,6 +62,7 @@ PGAC_ARG_REQ(with, template, [NAME], [override operating system template], # --with-template not given case $host_os in + aix*) template=aix ;; cygwin*|msys*) template=cygwin ;; darwin*) template=darwin ;; dragonfly*) template=netbsd ;; @@ -95,12 +96,6 @@ AC_MSG_RESULT([$template]) PORTNAME=$template AC_SUBST(PORTNAME) -# Initialize default assumption that we do not need separate assembly code -# for TAS (test-and-set). This can be overridden by the template file -# when it's executed. -need_tas=no -tas_file=dummy.s - # Default, works for most platforms, override in template file if needed DLSUFFIX=".so" @@ -364,16 +359,75 @@ pgac_cc_list="gcc cc" pgac_cxx_list="g++ c++" AC_PROG_CC([$pgac_cc_list]) -AC_PROG_CC_C99() -# Error out if the compiler does not support C99, as the codebase -# relies on that. -if test "$ac_cv_prog_cc_c99" = no; then - AC_MSG_ERROR([C compiler "$CC" does not support C99]) +# Detect option needed for C11 +# loosely modeled after code in later Autoconf versions +AC_MSG_CHECKING([for $CC option to accept ISO C11]) +AC_CACHE_VAL([pgac_cv_prog_cc_c11], +[pgac_cv_prog_cc_c11=no +pgac_save_CC=$CC +for pgac_arg in '' '-std=gnu11' '-std=c11'; do + CC="$pgac_save_CC $pgac_arg" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L +# error "Compiler does not advertise C11 conformance" +#endif]])], [[pgac_cv_prog_cc_c11=$pgac_arg]]) + test x"$pgac_cv_prog_cc_c11" != x"no" && break +done +CC=$pgac_save_CC]) + +if test x"$pgac_cv_prog_cc_c11" = x"no"; then + AC_MSG_RESULT([unsupported]) + AC_MSG_ERROR([C compiler "$CC" does not support C11]) +elif test x"$pgac_cv_prog_cc_c11" = x""; then + AC_MSG_RESULT([none needed]) +else + AC_MSG_RESULT([$pgac_cv_prog_cc_c11]) + CC="$CC $pgac_cv_prog_cc_c11" fi + AC_PROG_CXX([$pgac_cxx_list]) +# Check if it actually found a C++ compiler. +AC_LANG_PUSH([C++]) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [])], + [have_cxx=yes], + [have_cxx=no]) +AC_LANG_POP([C++]) +AC_SUBST(have_cxx) + +if test "$have_cxx" = yes; then + +# Detect option needed for C++11 +AC_MSG_CHECKING([for $CXX option to accept ISO C++11]) +AC_CACHE_VAL([pgac_cv_prog_cxx_cxx11], +[pgac_cv_prog_cxx_cxx11=no +pgac_save_CXX=$CXX +AC_LANG_PUSH([C++]) +for pgac_arg in '' '-std=gnu++11' '-std=c++11'; do + CXX="$pgac_save_CXX $pgac_arg" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([[#if !defined __cplusplus || __cplusplus < 201103L +# error "Compiler does not advertise C++11 conformance" +#endif]])], [[pgac_cv_prog_cxx_cxx11=$pgac_arg]]) + test x"$pgac_cv_prog_cxx_cxx11" != x"no" && break +done +AC_LANG_POP([C++]) +CXX=$pgac_save_CXX]) + +if test x"$pgac_cv_prog_cxx_cxx11" = x"no"; then + AC_MSG_RESULT([unsupported]) + AC_MSG_WARN([C++ compiler "$CXX" does not support C++11]) + have_cxx=no +elif test x"$pgac_cv_prog_cxx_cxx11" = x""; then + AC_MSG_RESULT([none needed]) +else + AC_MSG_RESULT([$pgac_cv_prog_cxx_cxx11]) + CXX="$CXX $pgac_cv_prog_cxx_cxx11" +fi + +fi # have_cxx + + # Check if it's Intel's compiler, which (usually) pretends to be gcc, # but has idiosyncrasies of its own. We assume icc will define # __INTEL_COMPILER regardless of CFLAGS. @@ -381,14 +435,6 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [@%:@ifndef __INTEL_COMPILER choke me @%:@endif])], [ICC=yes], [ICC=no]) -# Check if it's Sun Studio compiler. We assume that -# __SUNPRO_C will be defined for Sun Studio compilers -AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [@%:@ifndef __SUNPRO_C -choke me -@%:@endif])], [SUN_STUDIO_CC=yes], [SUN_STUDIO_CC=no]) - -AC_SUBST(SUN_STUDIO_CC) - # # LLVM @@ -501,18 +547,32 @@ if test "$GCC" = yes -a "$ICC" = no; then PERMIT_DECLARATION_AFTER_STATEMENT=-Wno-declaration-after-statement fi AC_SUBST(PERMIT_DECLARATION_AFTER_STATEMENT) - # Really don't want VLAs to be used in our dialect of C + # Really don't want VLAs to be used PGAC_PROG_CC_CFLAGS_OPT([-Werror=vla]) + PGAC_PROG_CXX_CFLAGS_OPT([-Werror=vla]) # On macOS, complain about usage of symbols newer than the deployment target PGAC_PROG_CC_CFLAGS_OPT([-Werror=unguarded-availability-new]) PGAC_PROG_CXX_CFLAGS_OPT([-Werror=unguarded-availability-new]) - # -Wvla is not applicable for C++ - PGAC_PROG_CC_CFLAGS_OPT([-Wendif-labels]) - PGAC_PROG_CXX_CFLAGS_OPT([-Wendif-labels]) PGAC_PROG_CC_CFLAGS_OPT([-Wmissing-format-attribute]) PGAC_PROG_CXX_CFLAGS_OPT([-Wmissing-format-attribute]) - PGAC_PROG_CC_CFLAGS_OPT([-Wimplicit-fallthrough=3]) - PGAC_PROG_CXX_CFLAGS_OPT([-Wimplicit-fallthrough=3]) + PGAC_PROG_CC_CFLAGS_OPT([-Wold-style-declaration]) + # -Wold-style-declaration is not applicable for C++ + + # To require fallthrough attribute annotations, use + # -Wimplicit-fallthrough=5 with gcc and -Wimplicit-fallthrough with + # clang. The latter is also accepted on gcc but does not enforce + # attribute annotations, so test the former first. + save_CFLAGS=$CFLAGS + PGAC_PROG_CC_CFLAGS_OPT([-Wimplicit-fallthrough=5]) + if test x"$save_CFLAGS" = x"$CFLAGS"; then + PGAC_PROG_CC_CFLAGS_OPT([-Wimplicit-fallthrough]) + fi + save_CXXFLAGS=$CXXFLAGS + PGAC_PROG_CXX_CFLAGS_OPT([-Wimplicit-fallthrough=5]) + if test x"$save_CXXFLAGS" = x"$CXXFLAGS"; then + PGAC_PROG_CXX_CFLAGS_OPT([-Wimplicit-fallthrough]) + fi + PGAC_PROG_CC_CFLAGS_OPT([-Wcast-function-type]) PGAC_PROG_CXX_CFLAGS_OPT([-Wcast-function-type]) PGAC_PROG_CC_CFLAGS_OPT([-Wshadow=compatible-local]) @@ -599,7 +659,7 @@ fi # __attribute__((visibility("hidden"))) is supported, if we encounter a # compiler that supports one of the supported variants of -fvisibility=hidden # but uses a different syntax to mark a symbol as exported. -if test "$GCC" = yes -o "$SUN_STUDIO_CC" = yes ; then +if test "$GCC" = yes; then PGAC_PROG_CC_VAR_OPT(CFLAGS_SL_MODULE, [-fvisibility=hidden]) # For C++ we additionally want -fvisibility-inlines-hidden PGAC_PROG_VARCXX_VARFLAGS_OPT(CXX, CXXFLAGS_SL_MODULE, [-fvisibility=hidden]) @@ -726,13 +786,6 @@ AC_LINK_IFELSE([AC_LANG_PROGRAM([], [return 0;])], [AC_MSG_RESULT(no) AC_MSG_ERROR([cannot proceed])]) -# Defend against gcc -ffast-math -if test "$GCC" = yes; then -AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [@%:@ifdef __FAST_MATH__ -choke me -@%:@endif])], [], [AC_MSG_ERROR([do not put -ffast-math in CFLAGS])]) -fi - # Defend against clang being used on x86-32 without SSE2 enabled. As current # versions of clang do not understand -fexcess-precision=standard, the use of # x87 floating point operations leads to problems like isinf possibly returning @@ -755,19 +808,6 @@ AC_PROG_CPP AC_SUBST(GCC) -# -# Set up TAS assembly code if needed; the template file has now had its -# chance to request this. -# -AC_CONFIG_LINKS([src/backend/port/tas.s:src/backend/port/tas/${tas_file}]) - -if test "$need_tas" = yes ; then - TAS=tas.o -else - TAS="" -fi -AC_SUBST(TAS) - AC_SUBST(DLSUFFIX)dnl AC_DEFINE_UNQUOTED([DLSUFFIX], ["$DLSUFFIX"], [Define to the file name extension of dynamically-loadable modules.]) @@ -1103,12 +1143,12 @@ if test "$with_libxml" = yes ; then # Note the user could also set XML2_CFLAGS/XML2_LIBS directly for pgac_option in $XML2_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) INCLUDES="$INCLUDES $pgac_option";; esac done for pgac_option in $XML2_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBDIRS="$LIBDIRS $pgac_option";; esac done fi @@ -1152,12 +1192,12 @@ if test "$with_lz4" = yes; then # note that -llz4 will be added by AC_CHECK_LIB below. for pgac_option in $LZ4_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) INCLUDES="$INCLUDES $pgac_option";; esac done for pgac_option in $LZ4_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBDIRS="$LIBDIRS $pgac_option";; esac done fi @@ -1177,12 +1217,12 @@ if test "$with_zstd" = yes; then # note that -lzstd will be added by AC_CHECK_LIB below. for pgac_option in $ZSTD_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) INCLUDES="$INCLUDES $pgac_option";; esac done for pgac_option in $ZSTD_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBDIRS="$LIBDIRS $pgac_option";; esac done fi @@ -1222,6 +1262,8 @@ case $MKDIR_P in *install-sh*) MKDIR_P='\${SHELL} \${top_srcdir}/config/install-sh -c -d';; esac +AC_PATH_PROG(NM, nm) +AC_SUBST(NM) PGAC_PATH_BISON PGAC_PATH_FLEX @@ -1401,7 +1443,7 @@ if test "$with_ssl" = openssl ; then # Function introduced in OpenSSL 1.0.2, not in LibreSSL. AC_CHECK_FUNCS([SSL_CTX_set_cert_cb]) # Function introduced in OpenSSL 1.1.1, not in LibreSSL. - AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback]) + AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb]) AC_DEFINE([USE_OPENSSL], 1, [Define to 1 to build with OpenSSL support. (--with-ssl=openssl)]) elif test "$with_ssl" != no ; then AC_MSG_ERROR([--with-ssl must specify openssl]) @@ -1420,6 +1462,13 @@ if test "$with_libxslt" = yes ; then AC_CHECK_LIB(xslt, xsltCleanupGlobals, [], [AC_MSG_ERROR([library 'xslt' is required for XSLT support])]) fi +if test "$with_liburing" = yes; then + _LIBS="$LIBS" + LIBS="$LIBURING_LIBS $LIBS" + AC_CHECK_FUNCS([io_uring_queue_init_mem]) + LIBS="$_LIBS" +fi + if test "$with_lz4" = yes ; then AC_CHECK_LIB(lz4, LZ4_compress_default, [], [AC_MSG_ERROR([library 'lz4' is required for LZ4 support])]) fi @@ -1428,7 +1477,8 @@ if test "$with_zstd" = yes ; then AC_CHECK_LIB(zstd, ZSTD_compress, [], [AC_MSG_ERROR([library 'zstd' is required for ZSTD support])]) fi -# Note: We can test for libldap_r only after we know PTHREAD_LIBS +# Note: We can test for libldap_r only after we know PTHREAD_LIBS; +# also, on AIX, we may need to have openssl in LIBS for this step. if test "$with_ldap" = yes ; then _LIBS="$LIBS" if test "$PORTNAME" != "win32"; then @@ -1500,12 +1550,10 @@ AC_SUBST(UUID_LIBS) ## AC_CHECK_HEADERS(m4_normalize([ - atomic.h copyfile.h execinfo.h getopt.h ifaddrs.h - mbarrier.h sys/epoll.h sys/event.h sys/personality.h @@ -1514,6 +1562,7 @@ AC_CHECK_HEADERS(m4_normalize([ sys/signalfd.h sys/ucred.h termios.h + uchar.h ucred.h xlocale.h ])) @@ -1672,10 +1721,12 @@ fi m4_defun([AC_PROG_CC_STDC], []) dnl We don't want that. AC_C_BIGENDIAN -AC_C_INLINE PGAC_PRINTF_ARCHETYPE -PGAC_C_STATIC_ASSERT +PGAC_CXX_PRINTF_ARCHETYPE PGAC_C_TYPEOF +PGAC_CXX_TYPEOF +PGAC_C_TYPEOF_UNQUAL +PGAC_CXX_TYPEOF_UNQUAL PGAC_C_TYPES_COMPATIBLE PGAC_C_BUILTIN_CONSTANT_P PGAC_C_BUILTIN_OP_OVERFLOW @@ -1688,8 +1739,8 @@ PGAC_STRUCT_SOCKADDR_SA_LEN # MSVC doesn't cope well with defining restrict to __restrict, the # spelling it understands, because it conflicts with -# __declspec(restrict). Therefore we define pg_restrict to the -# appropriate definition, which presumably won't conflict. +# __declspec(restrict) in C++ mode. Therefore we define pg_restrict to +# the appropriate definition, which presumably won't conflict. AC_C_RESTRICT if test "$ac_cv_c_restrict" = "no"; then pg_restrict="" @@ -1761,11 +1812,29 @@ AC_CHECK_SIZEOF([off_t]) # If we don't have largefile support, can't handle segment size >= 2GB. if test "$ac_cv_sizeof_off_t" -lt 8; then - if expr $RELSEG_SIZE '*' $blocksize '>=' 2 '*' 1024 '*' 1024; then + if expr $RELSEG_SIZE '*' $blocksize '>=' 2 '*' 1024 '*' 1024 >/dev/null; then AC_MSG_ERROR([Large file support is not enabled. Segment size cannot be larger than 1GB.]) fi fi +# Check for SA_SIGINFO extended signal handler availability +AC_CACHE_CHECK([for SA_SIGINFO], [ac_cv_have_sa_siginfo], [ + AC_COMPILE_IFELSE([ + AC_LANG_PROGRAM([[ + #include + #include + ]], [[ + struct sigaction sa; + sa.sa_flags = SA_SIGINFO; + ]]) + ], + [ac_cv_have_sa_siginfo=yes], + [ac_cv_have_sa_siginfo=no]) +]) + +if test "x$ac_cv_have_sa_siginfo" = "xyes"; then + AC_DEFINE([HAVE_SA_SIGINFO], 1, [Define to 1 if you have SA_SIGINFO available.]) +fi ## ## Functions, global variables @@ -1785,6 +1854,7 @@ AC_CHECK_FUNCS(m4_normalize([ copyfile copy_file_range elf_aux_info + explicit_memset getauxval getifaddrs getpeerucred @@ -1792,6 +1862,7 @@ AC_CHECK_FUNCS(m4_normalize([ kqueue localeconv_l mbstowcs_l + memset_explicit posix_fallocate ppoll pthread_is_threaded_np @@ -1811,7 +1882,6 @@ PGAC_CHECK_BUILTIN_FUNC([__builtin_bswap64], [long int x]) # We assume that we needn't test all widths of these explicitly: PGAC_CHECK_BUILTIN_FUNC([__builtin_clz], [unsigned int x]) PGAC_CHECK_BUILTIN_FUNC([__builtin_ctz], [unsigned int x]) -PGAC_CHECK_BUILTIN_FUNC([__builtin_popcount], [unsigned int x]) # __builtin_frame_address may draw a diagnostic for non-constant argument, # so it needs a different test function. PGAC_CHECK_BUILTIN_FUNC_PTR([__builtin_frame_address], [0]) @@ -1830,7 +1900,7 @@ AC_CHECK_DECLS(posix_fadvise, [], [], [#include ]) ]) # fi AC_CHECK_DECLS(fdatasync, [], [], [#include ]) -AC_CHECK_DECLS([strlcat, strlcpy, strnlen, strsep, timingsafe_bcmp]) +AC_CHECK_DECLS([strlcat, strlcpy, strsep, timingsafe_bcmp]) # We can't use AC_CHECK_FUNCS to detect these functions, because it # won't handle deployment target restrictions on macOS @@ -1851,7 +1921,6 @@ AC_REPLACE_FUNCS(m4_normalize([ mkdtemp strlcat strlcpy - strnlen strsep timingsafe_bcmp ])) @@ -1937,7 +2006,7 @@ fi if test "$with_icu" = yes; then ac_save_CPPFLAGS=$CPPFLAGS - CPPFLAGS="$ICU_CFLAGS $CPPFLAGS" + CPPFLAGS="$CPPFLAGS $ICU_CFLAGS" # Verify we have ICU's header files AC_CHECK_HEADER(unicode/ucol.h, [], @@ -1946,10 +2015,6 @@ if test "$with_icu" = yes; then CPPFLAGS=$ac_save_CPPFLAGS fi -if test "$with_llvm" = yes; then - PGAC_CHECK_LLVM_FUNCTIONS() -fi - # Lastly, restore full LIBS list and check for readline/libedit symbols LIBS="$LIBS_including_readline" @@ -1980,6 +2045,14 @@ related to locating shared libraries. Check the file 'config.log' for the exact reason.]])], [AC_MSG_RESULT([cross-compiling])]) +# These flags are supported in all C11-capable GCC/Clang versions, +# so no capability test is needed. Added here to avoid affecting configure probes, +# particularly PGAC_PRINTF_ARCHETYPE which uses -Werror and would fail to detect +# gnu_printf if -Wstrict-prototypes is active. +if test "$GCC" = yes -a "$ICC" = no; then + CFLAGS="$CFLAGS -Wstrict-prototypes -Wold-style-definition" +fi + # -------------------- # Run tests below here # -------------------- @@ -1989,38 +2062,29 @@ AC_CHECK_SIZEOF([void *]) AC_CHECK_SIZEOF([size_t]) AC_CHECK_SIZEOF([long]) AC_CHECK_SIZEOF([long long]) +AC_CHECK_SIZEOF([intmax_t]) # Determine memory alignment requirements for the basic C data types. AC_CHECK_ALIGNOF(short) AC_CHECK_ALIGNOF(int) -AC_CHECK_ALIGNOF(long) AC_CHECK_ALIGNOF(int64_t) AC_CHECK_ALIGNOF(double) # Compute maximum alignment of any basic type. # -# We require 'double' to have the strictest alignment among the basic types, -# because otherwise the C ABI might impose 8-byte alignment on some of the -# other C types that correspond to TYPALIGN_DOUBLE SQL types. That could -# cause a mismatch between the tuple layout and the C struct layout of a -# catalog tuple. We used to carefully order catalog columns such that any -# fixed-width, attalign=4 columns were at offsets divisible by 8 regardless -# of MAXIMUM_ALIGNOF to avoid that, but we no longer support any platforms -# where TYPALIGN_DOUBLE != MAXIMUM_ALIGNOF. -# -# We assume without checking that long's alignment is at least as strong as -# char, short, or int. Note that we intentionally do not consider any types -# wider than 64 bits, as allowing MAXIMUM_ALIGNOF to exceed 8 would be too -# much of a penalty for disk and memory space. - -MAX_ALIGNOF=$ac_cv_alignof_double +# We assume without checking that the maximum alignment requirement is that +# of int64_t and/or double. (On most platforms those are the same, but not +# everywhere.) For historical reasons, both int8 and float8 datatypes have +# typalign 'd', and therefore will be aligned per ALIGNOF_DOUBLE in database +# tuples even if ALIGNOF_INT64_T is more. Note that we intentionally do not +# consider any types wider than 64 bits, as allowing MAXIMUM_ALIGNOF to exceed +# 8 would be too much of a penalty for disk and memory space. -if test $ac_cv_alignof_long -gt $MAX_ALIGNOF ; then - AC_MSG_ERROR([alignment of 'long' is greater than the alignment of 'double']) -fi -if test $ac_cv_alignof_int64_t -gt $MAX_ALIGNOF ; then - AC_MSG_ERROR([alignment of 'int64_t' is greater than the alignment of 'double']) +if test $ac_cv_alignof_int64_t -gt $ac_cv_alignof_double ; then + MAX_ALIGNOF=$ac_cv_alignof_int64_t +else + MAX_ALIGNOF=$ac_cv_alignof_double fi AC_DEFINE_UNQUOTED(MAXIMUM_ALIGNOF, $MAX_ALIGNOF, [Define as the maximum alignment requirement of any C data type.]) @@ -2037,7 +2101,7 @@ PGAC_HAVE_GCC__ATOMIC_INT32_CAS PGAC_HAVE_GCC__ATOMIC_INT64_CAS -# Check for x86 cpuid instruction +# Check for __get_cpuid() and __cpuid() AC_CACHE_CHECK([for __get_cpuid], [pgac_cv__get_cpuid], [AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], [[unsigned int exx[4] = {0, 0, 0, 0}; @@ -2047,8 +2111,21 @@ AC_CACHE_CHECK([for __get_cpuid], [pgac_cv__get_cpuid], [pgac_cv__get_cpuid="no"])]) if test x"$pgac_cv__get_cpuid" = x"yes"; then AC_DEFINE(HAVE__GET_CPUID, 1, [Define to 1 if you have __get_cpuid.]) +else + # __cpuid() + AC_CACHE_CHECK([for __cpuid], [pgac_cv__cpuid], + [AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], + [[unsigned int exx[4] = {0, 0, 0, 0}; + __cpuid(exx, 1); + ]])], + [pgac_cv__cpuid="yes"], + [pgac_cv__cpuid="no"])]) + if test x"$pgac_cv__cpuid" = x"yes"; then + AC_DEFINE(HAVE__CPUID, 1, [Define to 1 if you have __cpuid.]) + fi fi +# Check for __get_cpuid_count() AC_CACHE_CHECK([for __get_cpuid_count], [pgac_cv__get_cpuid_count], [AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], [[unsigned int exx[4] = {0, 0, 0, 0}; @@ -2060,21 +2137,15 @@ if test x"$pgac_cv__get_cpuid_count" = x"yes"; then AC_DEFINE(HAVE__GET_CPUID_COUNT, 1, [Define to 1 if you have __get_cpuid_count.]) fi -AC_CACHE_CHECK([for __cpuid], [pgac_cv__cpuid], -[AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], - [[unsigned int exx[4] = {0, 0, 0, 0}; - __get_cpuid(exx[0], 1); - ]])], - [pgac_cv__cpuid="yes"], - [pgac_cv__cpuid="no"])]) -if test x"$pgac_cv__cpuid" = x"yes"; then - AC_DEFINE(HAVE__CPUID, 1, [Define to 1 if you have __cpuid.]) -fi - +# Check for __cpuidex() AC_CACHE_CHECK([for __cpuidex], [pgac_cv__cpuidex], -[AC_LINK_IFELSE([AC_LANG_PROGRAM([#include ], +[AC_LINK_IFELSE([AC_LANG_PROGRAM([#ifdef _MSC_VER + #include + #else + #include + #endif], [[unsigned int exx[4] = {0, 0, 0, 0}; - __get_cpuidex(exx[0], 7, 0); + __cpuidex((int *) exx, 7, 0); ]])], [pgac_cv__cpuidex="yes"], [pgac_cv__cpuidex="no"])]) @@ -2082,6 +2153,15 @@ if test x"$pgac_cv__cpuidex" = x"yes"; then AC_DEFINE(HAVE__CPUIDEX, 1, [Define to 1 if you have __cpuidex.]) fi +# Check for AVX2 target support +# +if test x"$host_cpu" = x"x86_64"; then + PGAC_AVX2_SUPPORT() + if test x"$pgac_avx2_support" = x"yes"; then + AC_DEFINE(USE_AVX2_WITH_RUNTIME_CHECK, 1, [Define to 1 to use AVX2 instructions with a runtime check.]) + fi +fi + # Check for XSAVE intrinsics # PGAC_XSAVE_INTRINSICS() @@ -2205,17 +2285,17 @@ fi AC_MSG_CHECKING([which CRC-32C implementation to use]) if test x"$USE_SSE42_CRC32C" = x"1"; then AC_DEFINE(USE_SSE42_CRC32C, 1, [Define to 1 use Intel SSE 4.2 CRC instructions.]) - PG_CRC32C_OBJS="pg_crc32c_sse42.o pg_crc32c_sse42_choose.o" + PG_CRC32C_OBJS="pg_crc32c_sse42.o" AC_MSG_RESULT(SSE 4.2) else if test x"$USE_SSE42_CRC32C_WITH_RUNTIME_CHECK" = x"1"; then AC_DEFINE(USE_SSE42_CRC32C_WITH_RUNTIME_CHECK, 1, [Define to 1 to use Intel SSE 4.2 CRC instructions with a runtime check.]) - PG_CRC32C_OBJS="pg_crc32c_sse42.o pg_crc32c_sb8.o pg_crc32c_sse42_choose.o" + PG_CRC32C_OBJS="pg_crc32c_sse42.o pg_crc32c_sb8.o" AC_MSG_RESULT(SSE 4.2 with runtime check) else if test x"$USE_ARMV8_CRC32C" = x"1"; then AC_DEFINE(USE_ARMV8_CRC32C, 1, [Define to 1 to use ARMv8 CRC Extension.]) - PG_CRC32C_OBJS="pg_crc32c_armv8.o" + PG_CRC32C_OBJS="pg_crc32c_armv8.o pg_crc32c_armv8_choose.o" AC_MSG_RESULT(ARMv8 CRC instructions) else if test x"$USE_ARMV8_CRC32C_WITH_RUNTIME_CHECK" = x"1"; then @@ -2242,6 +2322,10 @@ AC_SUBST(PG_CRC32C_OBJS) # if test x"$host_cpu" = x"x86_64"; then PGAC_AVX512_PCLMUL_INTRINSICS() +else + if test x"$host_cpu" = x"aarch64"; then + PGAC_ARM_PLMULL() + fi fi AC_MSG_CHECKING([for vectorized CRC-32C]) @@ -2249,7 +2333,12 @@ if test x"$pgac_avx512_pclmul_intrinsics" = x"yes"; then AC_DEFINE(USE_AVX512_CRC32C_WITH_RUNTIME_CHECK, 1, [Define to 1 to use AVX-512 CRC algorithms with a runtime check.]) AC_MSG_RESULT(AVX-512 with runtime check) else - AC_MSG_RESULT(none) + if test x"$pgac_arm_pmull" = x"yes"; then + AC_DEFINE(USE_PMULL_CRC32C_WITH_RUNTIME_CHECK, 1, [Define to 1 to use Arm PMULL CRC algorithms with a runtime check.]) + AC_MSG_RESULT(CRYPTO PMULL with runtime check) + else + AC_MSG_RESULT(none) + fi fi # Select semaphore implementation type. @@ -2337,7 +2426,7 @@ Use --without-tcl to disable building PL/Tcl.]) fi # now that we have TCL_INCLUDE_SPEC, we can check for ac_save_CPPFLAGS=$CPPFLAGS - CPPFLAGS="$TCL_INCLUDE_SPEC $CPPFLAGS" + CPPFLAGS="$CPPFLAGS $TCL_INCLUDE_SPEC" AC_CHECK_HEADER(tcl.h, [], [AC_MSG_ERROR([header file is required for Tcl])]) CPPFLAGS=$ac_save_CPPFLAGS fi @@ -2374,7 +2463,7 @@ fi # check for if test "$with_python" = yes; then ac_save_CPPFLAGS=$CPPFLAGS - CPPFLAGS="$python_includespec $CPPFLAGS" + CPPFLAGS="$CPPFLAGS $python_includespec" AC_CHECK_HEADER(Python.h, [], [AC_MSG_ERROR([header file is required for Python])]) CPPFLAGS=$ac_save_CPPFLAGS fi @@ -2449,8 +2538,6 @@ AC_SUBST(LDFLAGS_EX_BE) if test x"$GCC" = x"yes" ; then cc_string=`${CC} --version | sed q` case $cc_string in [[A-Za-z]]*) ;; *) cc_string="GCC $cc_string";; esac -elif test x"$SUN_STUDIO_CC" = x"yes" ; then - cc_string=`${CC} -V 2>&1 | sed q` else cc_string=$CC fi diff --git a/contrib/Makefile b/contrib/Makefile index 2f0a88d3f7744..7d91fe77db399 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -34,7 +34,9 @@ SUBDIRS = \ pg_freespacemap \ pg_logicalinspect \ pg_overexplain \ + pg_plan_advice \ pg_prewarm \ + pg_stash_advice \ pg_stat_statements \ pg_surgery \ pg_trgm \ diff --git a/contrib/amcheck/expected/check_btree.out b/contrib/amcheck/expected/check_btree.out index c6f4b16c55615..6558f2c5a4ff4 100644 --- a/contrib/amcheck/expected/check_btree.out +++ b/contrib/amcheck/expected/check_btree.out @@ -60,6 +60,14 @@ SELECT bt_index_parent_check('bttest_a_brin_idx'); ERROR: expected "btree" index as targets for verification DETAIL: Relation "bttest_a_brin_idx" is a brin index. ROLLBACK; +-- verify partitioned indexes are rejected (error) +BEGIN; +CREATE TABLE bttest_partitioned (a int, b int) PARTITION BY list (a); +CREATE INDEX bttest_btree_partitioned_idx ON bttest_partitioned USING btree (b); +SELECT bt_index_parent_check('bttest_btree_partitioned_idx'); +ERROR: expected index as targets for verification +DETAIL: This operation is not supported for partitioned indexes. +ROLLBACK; -- normal check outside of xact SELECT bt_index_check('bttest_a_idx'); bt_index_check diff --git a/contrib/amcheck/expected/check_gin.out b/contrib/amcheck/expected/check_gin.out index b4f0b110747c3..8dd01ced8d15f 100644 --- a/contrib/amcheck/expected/check_gin.out +++ b/contrib/amcheck/expected/check_gin.out @@ -76,3 +76,15 @@ SELECT gin_index_check('gin_check_jsonb_idx'); -- cleanup DROP TABLE gin_check_jsonb; +-- Test GIN multicolumn index +CREATE TABLE "gin_check_multicolumn"(a text[], b text[]); +INSERT INTO gin_check_multicolumn (a,b) values ('{a,c,e}','{b,d,f}'); +CREATE INDEX "gin_check_multicolumn_idx" on gin_check_multicolumn USING GIN(a,b); +SELECT gin_index_check('gin_check_multicolumn_idx'); + gin_index_check +----------------- + +(1 row) + +-- cleanup +DROP TABLE gin_check_multicolumn; diff --git a/contrib/amcheck/meson.build b/contrib/amcheck/meson.build index b33e8c9b062fe..d5137ef691d61 100644 --- a/contrib/amcheck/meson.build +++ b/contrib/amcheck/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group amcheck_sources = files( 'verify_common.c', @@ -49,6 +49,7 @@ tests += { 't/003_cic_2pc.pl', 't/004_verify_nbtree_unique.pl', 't/005_pitr.pl', + 't/006_verify_gin.pl', ], }, } diff --git a/contrib/amcheck/sql/check_btree.sql b/contrib/amcheck/sql/check_btree.sql index 0793dbfeebd82..171f7f691ec60 100644 --- a/contrib/amcheck/sql/check_btree.sql +++ b/contrib/amcheck/sql/check_btree.sql @@ -52,6 +52,13 @@ CREATE INDEX bttest_a_brin_idx ON bttest_a USING brin(id); SELECT bt_index_parent_check('bttest_a_brin_idx'); ROLLBACK; +-- verify partitioned indexes are rejected (error) +BEGIN; +CREATE TABLE bttest_partitioned (a int, b int) PARTITION BY list (a); +CREATE INDEX bttest_btree_partitioned_idx ON bttest_partitioned USING btree (b); +SELECT bt_index_parent_check('bttest_btree_partitioned_idx'); +ROLLBACK; + -- normal check outside of xact SELECT bt_index_check('bttest_a_idx'); -- more expansive tests diff --git a/contrib/amcheck/sql/check_gin.sql b/contrib/amcheck/sql/check_gin.sql index 66f42c34311db..11caed3d6a81b 100644 --- a/contrib/amcheck/sql/check_gin.sql +++ b/contrib/amcheck/sql/check_gin.sql @@ -50,3 +50,13 @@ SELECT gin_index_check('gin_check_jsonb_idx'); -- cleanup DROP TABLE gin_check_jsonb; + +-- Test GIN multicolumn index +CREATE TABLE "gin_check_multicolumn"(a text[], b text[]); +INSERT INTO gin_check_multicolumn (a,b) values ('{a,c,e}','{b,d,f}'); +CREATE INDEX "gin_check_multicolumn_idx" on gin_check_multicolumn USING GIN(a,b); + +SELECT gin_index_check('gin_check_multicolumn_idx'); + +-- cleanup +DROP TABLE gin_check_multicolumn; diff --git a/contrib/amcheck/t/001_verify_heapam.pl b/contrib/amcheck/t/001_verify_heapam.pl index 701e27fc76d3e..e3fee19ae5d76 100644 --- a/contrib/amcheck/t/001_verify_heapam.pl +++ b/contrib/amcheck/t/001_verify_heapam.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/amcheck/t/002_cic.pl b/contrib/amcheck/t/002_cic.pl index 6a0c4f611258f..629d00c1d0504 100644 --- a/contrib/amcheck/t/002_cic.pl +++ b/contrib/amcheck/t/002_cic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test CREATE INDEX CONCURRENTLY with concurrent modifications use strict; @@ -64,5 +64,29 @@ ) }); +# Test bt_index_parent_check() with indexes created with +# CREATE INDEX CONCURRENTLY. +$node->safe_psql('postgres', q(CREATE TABLE quebec(i int primary key))); +# Insert two rows into index +$node->safe_psql('postgres', + q(INSERT INTO quebec SELECT i FROM generate_series(1, 2) s(i);)); + +# start background transaction +my $in_progress_h = $node->background_psql('postgres'); +$in_progress_h->query_safe(q(BEGIN; SELECT pg_current_xact_id();)); + +# delete one row from table, while background transaction is in progress +$node->safe_psql('postgres', q(DELETE FROM quebec WHERE i = 1;)); +# create index concurrently, which will skip the deleted row +$node->safe_psql('postgres', + q(CREATE INDEX CONCURRENTLY oscar ON quebec(i);)); + +# check index using bt_index_parent_check +my $result = $node->psql('postgres', + q(SELECT bt_index_parent_check('oscar', heapallindexed => true))); +is($result, '0', 'bt_index_parent_check for CIC after removed row'); + +$in_progress_h->quit; + $node->stop; done_testing(); diff --git a/contrib/amcheck/t/003_cic_2pc.pl b/contrib/amcheck/t/003_cic_2pc.pl index 00a446a381faf..f28eeac17ef49 100644 --- a/contrib/amcheck/t/003_cic_2pc.pl +++ b/contrib/amcheck/t/003_cic_2pc.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test CREATE INDEX CONCURRENTLY with concurrent prepared-xact modifications use strict; diff --git a/contrib/amcheck/t/004_verify_nbtree_unique.pl b/contrib/amcheck/t/004_verify_nbtree_unique.pl index 6be08e3f38f79..7d5c387325ae8 100644 --- a/contrib/amcheck/t/004_verify_nbtree_unique.pl +++ b/contrib/amcheck/t/004_verify_nbtree_unique.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # This regression test checks the behavior of the btree validation in the # presence of breaking sort order changes. @@ -159,7 +159,9 @@ 'postgres', q( SELECT bt_index_check('bttest_unique_idx1', true, true); )); -ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx1"/, +like( + $stderr, + qr/index uniqueness is violated for index "bttest_unique_idx1"/, 'detected uniqueness violation for index "bttest_unique_idx1"'); # @@ -177,7 +179,9 @@ 'postgres', q( SELECT bt_index_check('bttest_unique_idx2', true, true); )); -ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx2"/, +like( + $stderr, + qr/item order invariant violated for index "bttest_unique_idx2"/, 'detected item order invariant violation for index "bttest_unique_idx2"'); $node->safe_psql( @@ -191,7 +195,9 @@ 'postgres', q( SELECT bt_index_check('bttest_unique_idx2', true, true); )); -ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx2"/, +like( + $stderr, + qr/index uniqueness is violated for index "bttest_unique_idx2"/, 'detected uniqueness violation for index "bttest_unique_idx2"'); # @@ -208,7 +214,9 @@ 'postgres', q( SELECT bt_index_check('bttest_unique_idx3', true, true); )); -ok( $stderr =~ /item order invariant violated for index "bttest_unique_idx3"/, +like( + $stderr, + qr/item order invariant violated for index "bttest_unique_idx3"/, 'detected item order invariant violation for index "bttest_unique_idx3"'); # For unique index deduplication is possible only for same values, but @@ -237,7 +245,9 @@ 'postgres', q( SELECT bt_index_check('bttest_unique_idx3', true, true); )); -ok( $stderr =~ /index uniqueness is violated for index "bttest_unique_idx3"/, +like( + $stderr, + qr/index uniqueness is violated for index "bttest_unique_idx3"/, 'detected uniqueness violation for index "bttest_unique_idx3"'); $node->stop; diff --git a/contrib/amcheck/t/005_pitr.pl b/contrib/amcheck/t/005_pitr.pl index 9da468a72cfc3..f973f5d72e326 100644 --- a/contrib/amcheck/t/005_pitr.pl +++ b/contrib/amcheck/t/005_pitr.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test integrity of intermediate states by PITR to those states use strict; diff --git a/contrib/amcheck/t/006_verify_gin.pl b/contrib/amcheck/t/006_verify_gin.pl new file mode 100644 index 0000000000000..93571f1d2ab3b --- /dev/null +++ b/contrib/amcheck/t/006_verify_gin.pl @@ -0,0 +1,293 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; + +use Test::More; + +my $node; +my $blksize; + +# to get the split fast, we want tuples to be as large as possible, but the same time we don't want them to be toasted. +my $filler_size = 1900; + +# +# Test set-up +# +$node = PostgreSQL::Test::Cluster->new('test'); +$node->init(no_data_checksums => 1); +$node->append_conf('postgresql.conf', 'autovacuum=off'); +$node->start; +$blksize = int($node->safe_psql('postgres', 'SHOW block_size;')); +$node->safe_psql('postgres', q(CREATE EXTENSION amcheck)); +$node->safe_psql( + 'postgres', q( + CREATE OR REPLACE FUNCTION random_string( INT ) RETURNS text AS $$ + SELECT string_agg(substring('0123456789abcdefghijklmnopqrstuvwxyz', ceil(random() * 36)::integer, 1), '') from generate_series(1, $1); + $$ LANGUAGE SQL;)); + +# Tests +invalid_entry_order_leaf_page_test(); +invalid_entry_order_inner_page_test(); +invalid_entry_columns_order_test(); +inconsistent_with_parent_key__parent_key_corrupted_test(); +inconsistent_with_parent_key__child_key_corrupted_test(); +inconsistent_with_parent_key__parent_key_corrupted_posting_tree_test(); + +sub invalid_entry_order_leaf_page_test +{ + my $relname = "test"; + my $indexname = "test_gin_idx"; + + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS $relname; + CREATE TABLE $relname (a text[]); + INSERT INTO $relname (a) VALUES ('{aaaaa,bbbbb}'); + CREATE INDEX $indexname ON $relname USING gin (a); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + my $blkno = 1; # root + + # produce wrong order by replacing aaaaa with ccccc + string_replace_block($relpath, 'aaaaa', 'ccccc', $blkno); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + my $expected = + "index \"$indexname\" has wrong tuple order on entry tree page, block 1, offset 2, rightlink 4294967295"; + like($stderr, qr/$expected/); +} + +sub invalid_entry_order_inner_page_test +{ + my $relname = "test"; + my $indexname = "test_gin_idx"; + + # to break the order in the inner page we need at least 3 items (rightmost key in the inner level is not checked for the order) + # so fill table until we have 2 splits + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS $relname; + CREATE TABLE $relname (a text[]); + INSERT INTO $relname (a) VALUES (('{' || 'pppppppppp' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'qqqqqqqqqq' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'rrrrrrrrrr' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'ssssssssss' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'tttttttttt' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'uuuuuuuuuu' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'vvvvvvvvvv' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'wwwwwwwwww' || random_string($filler_size) ||'}')::text[]); + CREATE INDEX $indexname ON $relname USING gin (a); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + my $blkno = 1; # root + + # we have rrrrrrrrr... and tttttttttt... as keys in the root, so produce wrong order by replacing rrrrrrrrrr.... + string_replace_block($relpath, 'rrrrrrrrrr', 'zzzzzzzzzz', $blkno); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + my $expected = + "index \"$indexname\" has wrong tuple order on entry tree page, block 1, offset 2, rightlink 4294967295"; + like($stderr, qr/$expected/); +} + +sub invalid_entry_columns_order_test +{ + my $relname = "test"; + my $indexname = "test_gin_idx"; + + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS $relname; + CREATE TABLE $relname (a text[],b text[]); + INSERT INTO $relname (a,b) VALUES ('{aaa}','{bbb}'); + CREATE INDEX $indexname ON $relname USING gin (a,b); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + my $blkno = 1; # root + + # mess column numbers + # root items order before: (1,aaa), (2,bbb) + # root items order after: (2,aaa), (1,bbb) + my $attrno_1 = pack('s', 1); + my $attrno_2 = pack('s', 2); + + my $find = qr/($attrno_1)(.)(aaa)/s; + my $replace = $attrno_2 . '$2$3'; + string_replace_block($relpath, $find, $replace, $blkno); + + $find = qr/($attrno_2)(.)(bbb)/s; + $replace = $attrno_1 . '$2$3'; + string_replace_block($relpath, $find, $replace, $blkno); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + my $expected = + "index \"$indexname\" has wrong tuple order on entry tree page, block 1, offset 2, rightlink 4294967295"; + like($stderr, qr/$expected/); +} + +sub inconsistent_with_parent_key__parent_key_corrupted_test +{ + my $relname = "test"; + my $indexname = "test_gin_idx"; + + # fill the table until we have a split + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS $relname; + CREATE TABLE $relname (a text[]); + INSERT INTO $relname (a) VALUES (('{' || 'llllllllll' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'mmmmmmmmmm' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'nnnnnnnnnn' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'xxxxxxxxxx' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'yyyyyyyyyy' || random_string($filler_size) ||'}')::text[]); + CREATE INDEX $indexname ON $relname USING gin (a); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + my $blkno = 1; # root + + # we have nnnnnnnnnn... as parent key in the root, so replace it with something smaller then child's keys + string_replace_block($relpath, 'nnnnnnnnnn', 'aaaaaaaaaa', $blkno); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + my $expected = + "index \"$indexname\" has inconsistent records on page 3 offset 3"; + like($stderr, qr/$expected/); +} + +sub inconsistent_with_parent_key__child_key_corrupted_test +{ + my $relname = "test"; + my $indexname = "test_gin_idx"; + + # fill the table until we have a split + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS $relname; + CREATE TABLE $relname (a text[]); + INSERT INTO $relname (a) VALUES (('{' || 'llllllllll' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'mmmmmmmmmm' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'nnnnnnnnnn' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'xxxxxxxxxx' || random_string($filler_size) ||'}')::text[]); + INSERT INTO $relname (a) VALUES (('{' || 'yyyyyyyyyy' || random_string($filler_size) ||'}')::text[]); + CREATE INDEX $indexname ON $relname USING gin (a); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + my $blkno = 3; # leaf + + # we have nnnnnnnnnn... as parent key in the root, so replace child key with something bigger + string_replace_block($relpath, 'nnnnnnnnnn', 'pppppppppp', $blkno); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + my $expected = + "index \"$indexname\" has inconsistent records on page 3 offset 3"; + like($stderr, qr/$expected/); +} + +sub inconsistent_with_parent_key__parent_key_corrupted_posting_tree_test +{ + my $relname = "test"; + my $indexname = "test_gin_idx"; + + $node->safe_psql( + 'postgres', qq( + DROP TABLE IF EXISTS $relname; + CREATE TABLE $relname (a text[]); + INSERT INTO $relname (a) select ('{aaaaa}') from generate_series(1,10000); + CREATE INDEX $indexname ON $relname USING gin (a); + )); + my $relpath = relation_filepath($indexname); + + $node->stop; + + my $blkno = 2; # posting tree root + + # we have a posting tree for 'aaaaa' key with the root at 2nd block + # and two leaf pages 3 and 4. replace 4th page's high key with (1,1) + # so that there are tid's in leaf page that are larger then the new high key. + my $find = pack('S*', 0, 4, 0) . '....'; + my $replace = pack('S*', 0, 4, 0, 1, 1); + string_replace_block($relpath, $find, $replace, $blkno); + + $node->start; + + my ($result, $stdout, $stderr) = + $node->psql('postgres', qq(SELECT gin_index_check('$indexname'))); + my $expected = + "index \"$indexname\": tid exceeds parent's high key in postingTree leaf on block 4"; + like($stderr, qr/$expected/); +} + + +# Returns the filesystem path for the named relation. +sub relation_filepath +{ + my ($relname) = @_; + + my $pgdata = $node->data_dir; + my $rel = $node->safe_psql('postgres', + qq(SELECT pg_relation_filepath('$relname'))); + die "path not found for relation $relname" unless defined $rel; + return "$pgdata/$rel"; +} + +# substitute pattern 'find' with 'replace' within the block with number 'blkno' in the file 'filename' +sub string_replace_block +{ + my ($filename, $find, $replace, $blkno) = @_; + + my $fh; + open($fh, '+<', $filename) or BAIL_OUT("open failed: $!"); + binmode $fh; + + my $offset = $blkno * $blksize; + my $buffer; + + sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!"); + sysread($fh, $buffer, $blksize) or BAIL_OUT("read failed: $!"); + + $buffer =~ s/$find/'"' . $replace . '"'/gee; + + sysseek($fh, $offset, 0) or BAIL_OUT("seek failed: $!"); + syswrite($fh, $buffer) or BAIL_OUT("write failed: $!"); + + close($fh) or BAIL_OUT("close failed: $!"); + + return; +} + +done_testing(); diff --git a/contrib/amcheck/verify_common.c b/contrib/amcheck/verify_common.c index d095e62ce551f..54ce901716bbc 100644 --- a/contrib/amcheck/verify_common.c +++ b/contrib/amcheck/verify_common.c @@ -3,7 +3,7 @@ * verify_common.c * Utility functions common to all access methods. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/amcheck/verify_common.c @@ -18,11 +18,13 @@ #include "verify_common.h" #include "catalog/index.h" #include "catalog/pg_am.h" +#include "commands/defrem.h" #include "commands/tablecmds.h" #include "utils/guc.h" #include "utils/syscache.h" static bool amcheck_index_mainfork_expected(Relation rel); +static bool index_checkable(Relation rel, Oid am_id); /* @@ -155,23 +157,21 @@ amcheck_lock_relation_and_check(Oid indrelid, * callable by non-superusers. If granted, it's useful to be able to check a * whole cluster. */ -bool +static bool index_checkable(Relation rel, Oid am_id) { - if (rel->rd_rel->relkind != RELKIND_INDEX || - rel->rd_rel->relam != am_id) - { - HeapTuple amtup; - HeapTuple amtuprel; + if (rel->rd_rel->relkind != RELKIND_INDEX) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("expected index as targets for verification"), + errdetail_relkind_not_supported(rel->rd_rel->relkind))); - amtup = SearchSysCache1(AMOID, ObjectIdGetDatum(am_id)); - amtuprel = SearchSysCache1(AMOID, ObjectIdGetDatum(rel->rd_rel->relam)); + if (rel->rd_rel->relam != am_id) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("expected \"%s\" index as targets for verification", NameStr(((Form_pg_am) GETSTRUCT(amtup))->amname)), + errmsg("expected \"%s\" index as targets for verification", get_am_name(am_id)), errdetail("Relation \"%s\" is a %s index.", - RelationGetRelationName(rel), NameStr(((Form_pg_am) GETSTRUCT(amtuprel))->amname)))); - } + RelationGetRelationName(rel), get_am_name(rel->rd_rel->relam)))); if (RELATION_IS_OTHER_TEMP(rel)) ereport(ERROR, @@ -182,7 +182,7 @@ index_checkable(Relation rel, Oid am_id) if (!rel->rd_index->indisvalid) ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot check index \"%s\"", RelationGetRelationName(rel)), errdetail("Index is not valid."))); diff --git a/contrib/amcheck/verify_common.h b/contrib/amcheck/verify_common.h index e78adb68808f0..4c4ddc01aa779 100644 --- a/contrib/amcheck/verify_common.h +++ b/contrib/amcheck/verify_common.h @@ -1,12 +1,12 @@ /*------------------------------------------------------------------------- * - * amcheck.h + * verify_common.h * Shared routines for amcheck verifications. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION - * contrib/amcheck/amcheck.h + * contrib/amcheck/verify_common.h * *------------------------------------------------------------------------- */ @@ -16,8 +16,7 @@ #include "utils/relcache.h" #include "miscadmin.h" -/* Typedefs for callback functions for amcheck_lock_relation_and_check */ -typedef void (*IndexCheckableCallback) (Relation index); +/* Typedef for callback function for amcheck_lock_relation_and_check */ typedef void (*IndexDoCheckCallback) (Relation rel, Relation heaprel, void *state, @@ -27,5 +26,3 @@ extern void amcheck_lock_relation_and_check(Oid indrelid, Oid am_id, IndexDoCheckCallback check, LOCKMODE lockmode, void *state); - -extern bool index_checkable(Relation rel, Oid am_id); diff --git a/contrib/amcheck/verify_gin.c b/contrib/amcheck/verify_gin.c index b5f363562e32a..abfad07d5e48f 100644 --- a/contrib/amcheck/verify_gin.c +++ b/contrib/amcheck/verify_gin.c @@ -13,7 +13,7 @@ * can reference either only leaf pages or only internal pages. * * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/amcheck/verify_gin.c @@ -38,7 +38,6 @@ typedef struct GinScanItem int depth; IndexTuple parenttup; BlockNumber parentblk; - XLogRecPtr parentlsn; BlockNumber blkno; struct GinScanItem *next; } GinScanItem; @@ -108,7 +107,7 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems) { if (nipd > 0) { - ipd = ginPostingListDecode((GinPostingList *) ptr, &ndecoded); + ipd = ginPostingListDecode(ptr, &ndecoded); if (nipd != ndecoded) elog(ERROR, "number of items mismatch in GIN entry tuple, %d in tuple header, %d decoded", nipd, ndecoded); @@ -118,7 +117,7 @@ ginReadTupleWithoutState(IndexTuple itup, int *nitems) } else { - ipd = (ItemPointer) palloc(sizeof(ItemPointerData) * nipd); + ipd = palloc_array(ItemPointerData, nipd); memcpy(ipd, ptr, sizeof(ItemPointerData) * nipd); } *nitems = nipd; @@ -153,7 +152,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting leafdepth = -1; /* Start the scan at the root page */ - stack = (GinPostingTreeScanItem *) palloc0(sizeof(GinPostingTreeScanItem)); + stack = palloc0_object(GinPostingTreeScanItem); stack->depth = 0; ItemPointerSetInvalid(&stack->parentkey); stack->parentblk = InvalidBlockNumber; @@ -175,7 +174,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL, strategy); LockBuffer(buffer, GIN_SHARE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); Assert(GinPageIsData(page)); @@ -346,7 +345,7 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting * Check if this tuple is consistent with the downlink in the * parent. */ - if (stack->parentblk != InvalidBlockNumber && i == maxoff && + if (i == maxoff && ItemPointerIsValid(&stack->parentkey) && ItemPointerCompare(&stack->parentkey, &posting_item->key) < 0) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), @@ -355,26 +354,21 @@ gin_check_posting_tree_parent_keys_consistency(Relation rel, BlockNumber posting stack->blkno, i))); /* This is an internal page, recurse into the child. */ - ptr = (GinPostingTreeScanItem *) palloc(sizeof(GinPostingTreeScanItem)); + ptr = palloc_object(GinPostingTreeScanItem); ptr->depth = stack->depth + 1; /* - * Set rightmost parent key to invalid item pointer. Its value - * is 'Infinity' and not explicitly stored. + * The rightmost parent key is always invalid item pointer. + * Its value is 'Infinity' and not explicitly stored. */ - if (rightlink == InvalidBlockNumber) - ItemPointerSetInvalid(&ptr->parentkey); - else - ptr->parentkey = posting_item->key; - + ptr->parentkey = posting_item->key; ptr->parentblk = stack->blkno; ptr->blkno = BlockIdGetBlockNumber(&posting_item->child_blkno); ptr->next = stack->next; stack->next = ptr; } } - LockBuffer(buffer, GIN_UNLOCK); - ReleaseBuffer(buffer); + UnlockReleaseBuffer(buffer); /* Step to next item in the queue */ stack_next = stack->next; @@ -417,11 +411,10 @@ gin_check_parent_keys_consistency(Relation rel, leafdepth = -1; /* Start the scan at the root page */ - stack = (GinScanItem *) palloc0(sizeof(GinScanItem)); + stack = palloc0_object(GinScanItem); stack->depth = 0; stack->parenttup = NULL; stack->parentblk = InvalidBlockNumber; - stack->parentlsn = InvalidXLogRecPtr; stack->blkno = GIN_ROOT_BLKNO; while (stack) @@ -432,7 +425,6 @@ gin_check_parent_keys_consistency(Relation rel, OffsetNumber i, maxoff, prev_attnum; - XLogRecPtr lsn; IndexTuple prev_tuple; BlockNumber rightlink; @@ -441,8 +433,7 @@ gin_check_parent_keys_consistency(Relation rel, buffer = ReadBufferExtended(rel, MAIN_FORKNUM, stack->blkno, RBM_NORMAL, strategy); LockBuffer(buffer, GIN_SHARE); - page = (Page) BufferGetPage(buffer); - lsn = BufferGetLSNAtomic(buffer); + page = BufferGetPage(buffer); maxoff = PageGetMaxOffsetNumber(page); rightlink = GinPageGetOpaque(page)->rightlink; @@ -463,28 +454,28 @@ gin_check_parent_keys_consistency(Relation rel, Datum parent_key = gintuple_get_key(&state, stack->parenttup, &parent_key_category); + OffsetNumber parent_key_attnum = gintuple_get_attrnum(&state, stack->parenttup); ItemId iid = PageGetItemIdCareful(rel, stack->blkno, page, maxoff); IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid); - OffsetNumber attnum = gintuple_get_attrnum(&state, idxtuple); + OffsetNumber page_max_key_attnum = gintuple_get_attrnum(&state, idxtuple); GinNullCategory page_max_key_category; Datum page_max_key = gintuple_get_key(&state, idxtuple, &page_max_key_category); if (rightlink != InvalidBlockNumber && - ginCompareEntries(&state, attnum, page_max_key, - page_max_key_category, parent_key, - parent_key_category) > 0) + ginCompareAttEntries(&state, page_max_key_attnum, page_max_key, + page_max_key_category, parent_key_attnum, + parent_key, parent_key_category) < 0) { /* split page detected, install right link to the stack */ GinScanItem *ptr; elog(DEBUG3, "split detected for blk: %u, parent blk: %u", stack->blkno, stack->parentblk); - ptr = (GinScanItem *) palloc(sizeof(GinScanItem)); + ptr = palloc_object(GinScanItem); ptr->depth = stack->depth; ptr->parenttup = CopyIndexTuple(stack->parenttup); ptr->parentblk = stack->parentblk; - ptr->parentlsn = stack->parentlsn; ptr->blkno = rightlink; ptr->next = stack->next; stack->next = ptr; @@ -513,9 +504,7 @@ gin_check_parent_keys_consistency(Relation rel, { ItemId iid = PageGetItemIdCareful(rel, stack->blkno, page, i); IndexTuple idxtuple = (IndexTuple) PageGetItem(page, iid); - OffsetNumber attnum = gintuple_get_attrnum(&state, idxtuple); - GinNullCategory prev_key_category; - Datum prev_key; + OffsetNumber current_attnum = gintuple_get_attrnum(&state, idxtuple); GinNullCategory current_key_category; Datum current_key; @@ -528,20 +517,24 @@ gin_check_parent_keys_consistency(Relation rel, current_key = gintuple_get_key(&state, idxtuple, ¤t_key_category); /* - * First block is metadata, skip order check. Also, never check - * for high key on rightmost page, as this key is not really - * stored explicitly. + * Compare the entry to the preceding one. + * + * Don't check for high key on the rightmost inner page, as this + * key is not really stored explicitly. * - * Also make sure to not compare entries for different attnums, - * which may be stored on the same page. + * The entries may be for different attributes, so make sure to + * use ginCompareAttEntries for comparison. */ - if (i != FirstOffsetNumber && attnum == prev_attnum && stack->blkno != GIN_ROOT_BLKNO && - !(i == maxoff && rightlink == InvalidBlockNumber)) + if ((i != FirstOffsetNumber) && + !(i == maxoff && rightlink == InvalidBlockNumber && !GinPageIsLeaf(page))) { + Datum prev_key; + GinNullCategory prev_key_category; + prev_key = gintuple_get_key(&state, prev_tuple, &prev_key_category); - if (ginCompareEntries(&state, attnum, prev_key, - prev_key_category, current_key, - current_key_category) >= 0) + if (ginCompareAttEntries(&state, prev_attnum, prev_key, + prev_key_category, current_attnum, + current_key, current_key_category) >= 0) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" has wrong tuple order on entry tree page, block %u, offset %u, rightlink %u", @@ -556,13 +549,14 @@ gin_check_parent_keys_consistency(Relation rel, i == maxoff) { GinNullCategory parent_key_category; + OffsetNumber parent_key_attnum = gintuple_get_attrnum(&state, stack->parenttup); Datum parent_key = gintuple_get_key(&state, stack->parenttup, &parent_key_category); - if (ginCompareEntries(&state, attnum, current_key, - current_key_category, parent_key, - parent_key_category) > 0) + if (ginCompareAttEntries(&state, current_attnum, current_key, + current_key_category, parent_key_attnum, + parent_key, parent_key_category) > 0) { /* * There was a discrepancy between parent and child @@ -581,6 +575,7 @@ gin_check_parent_keys_consistency(Relation rel, stack->blkno, stack->parentblk); else { + parent_key_attnum = gintuple_get_attrnum(&state, stack->parenttup); parent_key = gintuple_get_key(&state, stack->parenttup, &parent_key_category); @@ -589,9 +584,9 @@ gin_check_parent_keys_consistency(Relation rel, * Check if it is properly adjusted. If succeed, * proceed to the next key. */ - if (ginCompareEntries(&state, attnum, current_key, - current_key_category, parent_key, - parent_key_category) > 0) + if (ginCompareAttEntries(&state, current_attnum, current_key, + current_key_category, parent_key_attnum, + parent_key, parent_key_category) > 0) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index \"%s\" has inconsistent records on page %u offset %u", @@ -605,16 +600,15 @@ gin_check_parent_keys_consistency(Relation rel, { GinScanItem *ptr; - ptr = (GinScanItem *) palloc(sizeof(GinScanItem)); + ptr = palloc_object(GinScanItem); ptr->depth = stack->depth + 1; /* last tuple in layer has no high key */ - if (i != maxoff && !GinPageGetOpaque(page)->rightlink) - ptr->parenttup = CopyIndexTuple(idxtuple); - else + if (i == maxoff && rightlink == InvalidBlockNumber) ptr->parenttup = NULL; + else + ptr->parenttup = CopyIndexTuple(idxtuple); ptr->parentblk = stack->blkno; ptr->blkno = GinGetDownlink(idxtuple); - ptr->parentlsn = lsn; ptr->next = stack->next; stack->next = ptr; } @@ -644,11 +638,10 @@ gin_check_parent_keys_consistency(Relation rel, } prev_tuple = CopyIndexTuple(idxtuple); - prev_attnum = attnum; + prev_attnum = current_attnum; } - LockBuffer(buffer, GIN_UNLOCK); - ReleaseBuffer(buffer); + UnlockReleaseBuffer(buffer); /* Step to next item in the queue */ stack_next = stack->next; @@ -749,7 +742,7 @@ gin_refind_parent(Relation rel, BlockNumber parentblkno, ItemId p_iid = PageGetItemIdCareful(rel, parentblkno, parentpage, o); IndexTuple itup = (IndexTuple) PageGetItem(parentpage, p_iid); - if (ItemPointerGetBlockNumber(&(itup->t_tid)) == childblkno) + if (GinGetDownlink(itup) == childblkno) { /* Found it! Make copy and return it */ result = CopyIndexTuple(itup); diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index aa9cccd1da4fe..20ff58aa78259 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -3,7 +3,7 @@ * verify_heapam.c * Functions to check postgresql heap relations for corruption * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * contrib/amcheck/verify_heapam.c *------------------------------------------------------------------------- @@ -24,11 +24,13 @@ #include "funcapi.h" #include "miscadmin.h" #include "storage/bufmgr.h" +#include "storage/lwlock.h" #include "storage/procarray.h" #include "storage/read_stream.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/rel.h" +#include "utils/tuplestore.h" PG_FUNCTION_INFO_V1(verify_heapam); @@ -73,7 +75,7 @@ typedef enum SkipPages */ typedef struct ToastedAttribute { - struct varatt_external toast_pointer; + varatt_external toast_pointer; BlockNumber blkno; /* block in main table */ OffsetNumber offnum; /* offset in main table */ AttrNumber attnum; /* attribute in main table */ @@ -526,17 +528,17 @@ verify_heapam(PG_FUNCTION_ARGS) if (rdoffnum < FirstOffsetNumber) { report_corruption(&ctx, - psprintf("line pointer redirection to item at offset %u precedes minimum offset %u", - (unsigned) rdoffnum, - (unsigned) FirstOffsetNumber)); + psprintf("line pointer redirection to item at offset %d precedes minimum offset %d", + rdoffnum, + FirstOffsetNumber)); continue; } if (rdoffnum > maxoff) { report_corruption(&ctx, - psprintf("line pointer redirection to item at offset %u exceeds maximum offset %u", - (unsigned) rdoffnum, - (unsigned) maxoff)); + psprintf("line pointer redirection to item at offset %d exceeds maximum offset %d", + rdoffnum, + maxoff)); continue; } @@ -550,22 +552,22 @@ verify_heapam(PG_FUNCTION_ARGS) if (!ItemIdIsUsed(rditem)) { report_corruption(&ctx, - psprintf("redirected line pointer points to an unused item at offset %u", - (unsigned) rdoffnum)); + psprintf("redirected line pointer points to an unused item at offset %d", + rdoffnum)); continue; } else if (ItemIdIsDead(rditem)) { report_corruption(&ctx, - psprintf("redirected line pointer points to a dead item at offset %u", - (unsigned) rdoffnum)); + psprintf("redirected line pointer points to a dead item at offset %d", + rdoffnum)); continue; } else if (ItemIdIsRedirected(rditem)) { report_corruption(&ctx, - psprintf("redirected line pointer points to another redirected line pointer at offset %u", - (unsigned) rdoffnum)); + psprintf("redirected line pointer points to another redirected line pointer at offset %d", + rdoffnum)); continue; } @@ -601,10 +603,10 @@ verify_heapam(PG_FUNCTION_ARGS) if (ctx.lp_off + ctx.lp_len > BLCKSZ) { report_corruption(&ctx, - psprintf("line pointer to page offset %u with length %u ends beyond maximum page offset %u", + psprintf("line pointer to page offset %u with length %u ends beyond maximum page offset %d", ctx.lp_off, ctx.lp_len, - (unsigned) BLCKSZ)); + BLCKSZ)); continue; } @@ -678,16 +680,16 @@ verify_heapam(PG_FUNCTION_ARGS) if (!HeapTupleHeaderIsHeapOnly(next_htup)) { report_corruption(&ctx, - psprintf("redirected line pointer points to a non-heap-only tuple at offset %u", - (unsigned) nextoffnum)); + psprintf("redirected line pointer points to a non-heap-only tuple at offset %d", + nextoffnum)); } /* HOT chains should not intersect. */ if (predecessor[nextoffnum] != InvalidOffsetNumber) { report_corruption(&ctx, - psprintf("redirect line pointer points to offset %u, but offset %u also points there", - (unsigned) nextoffnum, (unsigned) predecessor[nextoffnum])); + psprintf("redirect line pointer points to offset %d, but offset %d also points there", + nextoffnum, predecessor[nextoffnum])); continue; } @@ -719,8 +721,8 @@ verify_heapam(PG_FUNCTION_ARGS) if (predecessor[nextoffnum] != InvalidOffsetNumber) { report_corruption(&ctx, - psprintf("tuple points to new version at offset %u, but offset %u also points there", - (unsigned) nextoffnum, (unsigned) predecessor[nextoffnum])); + psprintf("tuple points to new version at offset %d, but offset %d also points there", + nextoffnum, predecessor[nextoffnum])); continue; } @@ -743,15 +745,15 @@ verify_heapam(PG_FUNCTION_ARGS) HeapTupleHeaderIsHeapOnly(next_htup)) { report_corruption(&ctx, - psprintf("non-heap-only update produced a heap-only tuple at offset %u", - (unsigned) nextoffnum)); + psprintf("non-heap-only update produced a heap-only tuple at offset %d", + nextoffnum)); } if ((curr_htup->t_infomask2 & HEAP_HOT_UPDATED) && !HeapTupleHeaderIsHeapOnly(next_htup)) { report_corruption(&ctx, - psprintf("heap-only update produced a non-heap only tuple at offset %u", - (unsigned) nextoffnum)); + psprintf("heap-only update produced a non-heap only tuple at offset %d", + nextoffnum)); } /* @@ -772,10 +774,10 @@ verify_heapam(PG_FUNCTION_ARGS) TransactionIdIsInProgress(curr_xmin)) { report_corruption(&ctx, - psprintf("tuple with in-progress xmin %u was updated to produce a tuple at offset %u with committed xmin %u", - (unsigned) curr_xmin, - (unsigned) ctx.offnum, - (unsigned) next_xmin)); + psprintf("tuple with in-progress xmin %u was updated to produce a tuple at offset %d with committed xmin %u", + curr_xmin, + ctx.offnum, + next_xmin)); } /* @@ -788,16 +790,16 @@ verify_heapam(PG_FUNCTION_ARGS) { if (xmin_commit_status[nextoffnum] == XID_IN_PROGRESS) report_corruption(&ctx, - psprintf("tuple with aborted xmin %u was updated to produce a tuple at offset %u with in-progress xmin %u", - (unsigned) curr_xmin, - (unsigned) ctx.offnum, - (unsigned) next_xmin)); + psprintf("tuple with aborted xmin %u was updated to produce a tuple at offset %d with in-progress xmin %u", + curr_xmin, + ctx.offnum, + next_xmin)); else if (xmin_commit_status[nextoffnum] == XID_COMMITTED) report_corruption(&ctx, - psprintf("tuple with aborted xmin %u was updated to produce a tuple at offset %u with committed xmin %u", - (unsigned) curr_xmin, - (unsigned) ctx.offnum, - (unsigned) next_xmin)); + psprintf("tuple with aborted xmin %u was updated to produce a tuple at offset %d with committed xmin %u", + curr_xmin, + ctx.offnum, + next_xmin)); } } @@ -1660,11 +1662,11 @@ static bool check_tuple_attribute(HeapCheckContext *ctx) { Datum attdatum; - struct varlena *attr; + varlena *attr; char *tp; /* pointer to the tuple data */ uint16 infomask; CompactAttribute *thisatt; - struct varatt_external toast_pointer; + varatt_external toast_pointer; infomask = ctx->tuphdr->t_infomask; thisatt = TupleDescCompactAttr(RelationGetDescr(ctx->rel), ctx->attnum); @@ -1754,7 +1756,7 @@ check_tuple_attribute(HeapCheckContext *ctx) * We go further, because we need to check if the toast datum is corrupt. */ - attr = (struct varlena *) DatumGetPointer(attdatum); + attr = (varlena *) DatumGetPointer(attdatum); /* * Now we follow the logic of detoast_external_attr(), with the same @@ -1838,7 +1840,7 @@ check_tuple_attribute(HeapCheckContext *ctx) { ToastedAttribute *ta; - ta = (ToastedAttribute *) palloc0(sizeof(ToastedAttribute)); + ta = palloc0_object(ToastedAttribute); VARATT_EXTERNAL_GET_POINTER(ta->toast_pointer, attr); ta->blkno = ctx->blkno; @@ -1942,7 +1944,7 @@ check_tuple(HeapCheckContext *ctx, bool *xmin_commit_status_ok, if (RelationGetDescr(ctx->rel)->natts < ctx->natts) { report_corruption(ctx, - psprintf("number of attributes %u exceeds maximum expected for table %u", + psprintf("number of attributes %u exceeds maximum %u expected for table", ctx->natts, RelationGetDescr(ctx->rel)->natts)); return; diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c index f11c43a0ed797..b74ab5f7a057a 100644 --- a/contrib/amcheck/verify_nbtree.c +++ b/contrib/amcheck/verify_nbtree.c @@ -14,7 +14,7 @@ * that every visible heap tuple has a matching index tuple. * * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/amcheck/verify_nbtree.c @@ -92,9 +92,11 @@ typedef struct BtreeCheckState BufferAccessStrategy checkstrategy; /* - * Info for uniqueness checking. Fill these fields once per index check. + * Info for uniqueness checking. Fill this field and the one below once + * per index check. */ IndexInfo *indexinfo; + /* Table scan snapshot for heapallindexed and checkunique */ Snapshot snapshot; /* @@ -382,7 +384,6 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, BTMetaPageData *metad; uint32 previouslevel; BtreeLevel current; - Snapshot snapshot = SnapshotAny; if (!readonly) elog(DEBUG1, "verifying consistency of tree structure for index \"%s\"", @@ -400,7 +401,7 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, /* * Initialize state for entire verification operation */ - state = palloc0(sizeof(BtreeCheckState)); + state = palloc0_object(BtreeCheckState); state->rel = rel; state->heaprel = heaprel; state->heapkeyspace = heapkeyspace; @@ -433,54 +434,46 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, state->heaptuplespresent = 0; /* - * Register our own snapshot in !readonly case, rather than asking + * Register our own snapshot for heapallindexed, rather than asking * table_index_build_scan() to do this for us later. This needs to * happen before index fingerprinting begins, so we can later be * certain that index fingerprinting should have reached all tuples * returned by table_index_build_scan(). */ - if (!state->readonly) - { - snapshot = RegisterSnapshot(GetTransactionSnapshot()); + state->snapshot = RegisterSnapshot(GetTransactionSnapshot()); - /* - * GetTransactionSnapshot() always acquires a new MVCC snapshot in - * READ COMMITTED mode. A new snapshot is guaranteed to have all - * the entries it requires in the index. - * - * We must defend against the possibility that an old xact - * snapshot was returned at higher isolation levels when that - * snapshot is not safe for index scans of the target index. This - * is possible when the snapshot sees tuples that are before the - * index's indcheckxmin horizon. Throwing an error here should be - * very rare. It doesn't seem worth using a secondary snapshot to - * avoid this. - */ - if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin && - !TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data), - snapshot->xmin)) - ereport(ERROR, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("index \"%s\" cannot be verified using transaction snapshot", - RelationGetRelationName(rel)))); - } + /* + * GetTransactionSnapshot() always acquires a new MVCC snapshot in + * READ COMMITTED mode. A new snapshot is guaranteed to have all the + * entries it requires in the index. + * + * We must defend against the possibility that an old xact snapshot + * was returned at higher isolation levels when that snapshot is not + * safe for index scans of the target index. This is possible when + * the snapshot sees tuples that are before the index's indcheckxmin + * horizon. Throwing an error here should be very rare. It doesn't + * seem worth using a secondary snapshot to avoid this. + */ + if (IsolationUsesXactSnapshot() && rel->rd_index->indcheckxmin && + !TransactionIdPrecedes(HeapTupleHeaderGetXmin(rel->rd_indextuple->t_data), + state->snapshot->xmin)) + ereport(ERROR, + errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("index \"%s\" cannot be verified using transaction snapshot", + RelationGetRelationName(rel))); } /* - * We need a snapshot to check the uniqueness of the index. For better - * performance take it once per index check. If snapshot already taken - * reuse it. + * We need a snapshot to check the uniqueness of the index. For better + * performance, take it once per index check. If one was already taken + * above, use that. */ if (state->checkunique) { state->indexinfo = BuildIndexInfo(state->rel); - if (state->indexinfo->ii_Unique) - { - if (snapshot != SnapshotAny) - state->snapshot = snapshot; - else - state->snapshot = RegisterSnapshot(GetTransactionSnapshot()); - } + + if (state->indexinfo->ii_Unique && state->snapshot == InvalidSnapshot) + state->snapshot = RegisterSnapshot(GetTransactionSnapshot()); } Assert(!state->rootdescend || state->readonly); @@ -555,13 +548,12 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, /* * Create our own scan for table_index_build_scan(), rather than * getting it to do so for us. This is required so that we can - * actually use the MVCC snapshot registered earlier in !readonly - * case. + * actually use the MVCC snapshot registered earlier. * * Note that table_index_build_scan() calls heap_endscan() for us. */ scan = table_beginscan_strat(state->heaprel, /* relation */ - snapshot, /* snapshot */ + state->snapshot, /* snapshot */ 0, /* number of keys */ NULL, /* scan key */ true, /* buffer access strategy OK */ @@ -569,16 +561,15 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, /* * Scan will behave as the first scan of a CREATE INDEX CONCURRENTLY - * behaves in !readonly case. + * behaves. * * It's okay that we don't actually use the same lock strength for the - * heap relation as any other ii_Concurrent caller would in !readonly - * case. We have no reason to care about a concurrent VACUUM - * operation, since there isn't going to be a second scan of the heap - * that needs to be sure that there was no concurrent recycling of - * TIDs. + * heap relation as any other ii_Concurrent caller would. We have no + * reason to care about a concurrent VACUUM operation, since there + * isn't going to be a second scan of the heap that needs to be sure + * that there was no concurrent recycling of TIDs. */ - indexinfo->ii_Concurrent = !state->readonly; + indexinfo->ii_Concurrent = true; /* * Don't wait for uncommitted tuple xact commit/abort when index is a @@ -602,14 +593,11 @@ bt_check_every_level(Relation rel, Relation heaprel, bool heapkeyspace, state->heaptuplespresent, RelationGetRelationName(heaprel), 100.0 * bloom_prop_bits_set(state->filter)))); - if (snapshot != SnapshotAny) - UnregisterSnapshot(snapshot); - bloom_free(state->filter); } /* Be tidy: */ - if (snapshot == SnapshotAny && state->snapshot != InvalidSnapshot) + if (state->snapshot != InvalidSnapshot) UnregisterSnapshot(state->snapshot); MemoryContextDelete(state->targetcontext); } @@ -721,7 +709,7 @@ bt_check_level_from_leftmost(BtreeCheckState *state, BtreeLevel level) errmsg("block %u is not leftmost in index \"%s\"", current, RelationGetRelationName(state->rel)))); - if (level.istruerootlevel && !P_ISROOT(opaque)) + if (level.istruerootlevel && (!P_ISROOT(opaque) && !P_INCOMPLETE_SPLIT(opaque))) ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("block %u is not true root in index \"%s\"", @@ -876,7 +864,7 @@ heap_entry_is_visible(BtreeCheckState *state, ItemPointer tid) } /* - * Prepare an error message for unique constrain violation in + * Prepare an error message for unique constraint violation in * a btree index and report ERROR. */ static void @@ -913,7 +901,7 @@ bt_report_duplicate(BtreeCheckState *state, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index uniqueness is violated for index \"%s\"", RelationGetRelationName(state->rel)), - errdetail("Index %s%s and%s%s (point to heap %s and %s) page lsn=%X/%X.", + errdetail("Index %s%s and%s%s (point to heap %s and %s) page lsn=%X/%08X.", itid, pposting, nitid, pnposting, htid, nhtid, LSN_FORMAT_ARGS(state->targetlsn)))); } @@ -1058,7 +1046,7 @@ bt_leftmost_ignoring_half_dead(BtreeCheckState *state, (errcode(ERRCODE_NO_DATA), errmsg_internal("harmless interrupted page deletion detected in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Block=%u right block=%u page lsn=%X/%X.", + errdetail_internal("Block=%u right block=%u page lsn=%X/%08X.", reached, reached_from, LSN_FORMAT_ARGS(pagelsn)))); @@ -1283,7 +1271,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("wrong number of high key index tuple attributes in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Index block=%u natts=%u block type=%s page lsn=%X/%X.", + errdetail_internal("Index block=%u natts=%u block type=%s page lsn=%X/%08X.", state->targetblock, BTreeTupleGetNAtts(itup, state->rel), P_ISLEAF(topaque) ? "heap" : "index", @@ -1332,7 +1320,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index tuple size does not equal lp_len in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Index tid=(%u,%u) tuple size=%zu lp_len=%u page lsn=%X/%X.", + errdetail_internal("Index tid=(%u,%u) tuple size=%zu lp_len=%u page lsn=%X/%08X.", state->targetblock, offset, tupsize, ItemIdGetLength(itemid), LSN_FORMAT_ARGS(state->targetlsn)), @@ -1356,7 +1344,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("wrong number of index tuple attributes in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Index tid=%s natts=%u points to %s tid=%s page lsn=%X/%X.", + errdetail_internal("Index tid=%s natts=%u points to %s tid=%s page lsn=%X/%08X.", itid, BTreeTupleGetNAtts(itup, state->rel), P_ISLEAF(topaque) ? "heap" : "index", @@ -1406,7 +1394,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("could not find tuple using search from root page in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Index tid=%s points to heap tid=%s page lsn=%X/%X.", + errdetail_internal("Index tid=%s points to heap tid=%s page lsn=%X/%08X.", itid, htid, LSN_FORMAT_ARGS(state->targetlsn)))); } @@ -1435,7 +1423,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("posting list contains misplaced TID in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Index tid=%s posting list offset=%d page lsn=%X/%X.", + errdetail_internal("Index tid=%s posting list offset=%d page lsn=%X/%08X.", itid, i, LSN_FORMAT_ARGS(state->targetlsn)))); } @@ -1488,7 +1476,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("index row size %zu exceeds maximum for index \"%s\"", tupsize, RelationGetRelationName(state->rel)), - errdetail_internal("Index tid=%s points to %s tid=%s page lsn=%X/%X.", + errdetail_internal("Index tid=%s points to %s tid=%s page lsn=%X/%08X.", itid, P_ISLEAF(topaque) ? "heap" : "index", htid, @@ -1595,7 +1583,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("high key invariant violated for index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Index tid=%s points to %s tid=%s page lsn=%X/%X.", + errdetail_internal("Index tid=%s points to %s tid=%s page lsn=%X/%08X.", itid, P_ISLEAF(topaque) ? "heap" : "index", htid, @@ -1641,9 +1629,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("item order invariant violated for index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Lower index tid=%s (points to %s tid=%s) " - "higher index tid=%s (points to %s tid=%s) " - "page lsn=%X/%X.", + errdetail_internal("Lower index tid=%s (points to %s tid=%s) higher index tid=%s (points to %s tid=%s) page lsn=%X/%08X.", itid, P_ISLEAF(topaque) ? "heap" : "index", htid, @@ -1760,7 +1746,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("cross page item order invariant violated for index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Last item on page tid=(%u,%u) page lsn=%X/%X.", + errdetail_internal("Last item on page tid=(%u,%u) page lsn=%X/%08X.", state->targetblock, offset, LSN_FORMAT_ARGS(state->targetlsn)))); } @@ -1813,7 +1799,7 @@ bt_target_page_check(BtreeCheckState *state) (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("right block of leaf block is non-leaf for index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Block=%u page lsn=%X/%X.", + errdetail_internal("Block=%u page lsn=%X/%08X.", state->targetblock, LSN_FORMAT_ARGS(state->targetlsn)))); @@ -2237,7 +2223,7 @@ bt_child_highkey_check(BtreeCheckState *state, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("the first child of leftmost target page is not leftmost of its level in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.", + errdetail_internal("Target block=%u child block=%u target page lsn=%X/%08X.", state->targetblock, blkno, LSN_FORMAT_ARGS(state->targetlsn)))); @@ -2270,7 +2256,7 @@ bt_child_highkey_check(BtreeCheckState *state, * If we visit page with high key, check that it is equal to the * target key next to corresponding downlink. */ - if (!rightsplit && !P_RIGHTMOST(opaque)) + if (!rightsplit && !P_RIGHTMOST(opaque) && !P_ISHALFDEAD(opaque)) { BTPageOpaque topaque; IndexTuple highkey; @@ -2323,7 +2309,7 @@ bt_child_highkey_check(BtreeCheckState *state, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("child high key is greater than rightmost pivot key on target level in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.", + errdetail_internal("Target block=%u child block=%u target page lsn=%X/%08X.", state->targetblock, blkno, LSN_FORMAT_ARGS(state->targetlsn)))); pivotkey_offset = P_HIKEY; @@ -2353,7 +2339,7 @@ bt_child_highkey_check(BtreeCheckState *state, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("can't find left sibling high key in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.", + errdetail_internal("Target block=%u child block=%u target page lsn=%X/%08X.", state->targetblock, blkno, LSN_FORMAT_ARGS(state->targetlsn)))); itup = state->lowkey; @@ -2365,7 +2351,7 @@ bt_child_highkey_check(BtreeCheckState *state, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("mismatch between parent key and child high key in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.", + errdetail_internal("Target block=%u child block=%u target page lsn=%X/%08X.", state->targetblock, blkno, LSN_FORMAT_ARGS(state->targetlsn)))); } @@ -2505,7 +2491,7 @@ bt_child_check(BtreeCheckState *state, BTScanInsert targetkey, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("downlink to deleted page found in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Parent block=%u child block=%u parent page lsn=%X/%X.", + errdetail_internal("Parent block=%u child block=%u parent page lsn=%X/%08X.", state->targetblock, childblock, LSN_FORMAT_ARGS(state->targetlsn)))); @@ -2546,7 +2532,7 @@ bt_child_check(BtreeCheckState *state, BTScanInsert targetkey, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("down-link lower bound invariant violated for index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Parent block=%u child index tid=(%u,%u) parent page lsn=%X/%X.", + errdetail_internal("Parent block=%u child index tid=(%u,%u) parent page lsn=%X/%08X.", state->targetblock, childblock, offset, LSN_FORMAT_ARGS(state->targetlsn)))); } @@ -2616,7 +2602,7 @@ bt_downlink_missing_check(BtreeCheckState *state, bool rightsplit, (errcode(ERRCODE_NO_DATA), errmsg_internal("harmless interrupted page split detected in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Block=%u level=%u left sibling=%u page lsn=%X/%X.", + errdetail_internal("Block=%u level=%u left sibling=%u page lsn=%X/%08X.", blkno, opaque->btpo_level, opaque->btpo_prev, LSN_FORMAT_ARGS(pagelsn)))); @@ -2638,7 +2624,7 @@ bt_downlink_missing_check(BtreeCheckState *state, bool rightsplit, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("leaf index block lacks downlink in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Block=%u page lsn=%X/%X.", + errdetail_internal("Block=%u page lsn=%X/%08X.", blkno, LSN_FORMAT_ARGS(pagelsn)))); @@ -2704,7 +2690,7 @@ bt_downlink_missing_check(BtreeCheckState *state, bool rightsplit, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("downlink to deleted leaf page found in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Top parent/target block=%u leaf block=%u top parent/under check lsn=%X/%X.", + errdetail_internal("Top parent/target block=%u leaf block=%u top parent/under check lsn=%X/%08X.", blkno, childblk, LSN_FORMAT_ARGS(pagelsn)))); @@ -2730,7 +2716,7 @@ bt_downlink_missing_check(BtreeCheckState *state, bool rightsplit, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg("internal index block lacks downlink in index \"%s\"", RelationGetRelationName(state->rel)), - errdetail_internal("Block=%u level=%u page lsn=%X/%X.", + errdetail_internal("Block=%u level=%u page lsn=%X/%08X.", blkno, opaque->btpo_level, LSN_FORMAT_ARGS(pagelsn)))); } @@ -3023,7 +3009,6 @@ static bool bt_rootdescend(BtreeCheckState *state, IndexTuple itup) { BTScanInsert key; - BTStack stack; Buffer lbuf; bool exists; @@ -3040,7 +3025,7 @@ bt_rootdescend(BtreeCheckState *state, IndexTuple itup) */ Assert(state->readonly && state->rootdescend); exists = false; - stack = _bt_search(state->rel, NULL, key, &lbuf, BT_READ); + _bt_search(state->rel, NULL, key, &lbuf, BT_READ, false); if (BufferIsValid(lbuf)) { @@ -3067,7 +3052,6 @@ bt_rootdescend(BtreeCheckState *state, IndexTuple itup) _bt_relbuf(state->rel, lbuf); } - _bt_freestack(stack); pfree(key); return exists; diff --git a/contrib/auth_delay/auth_delay.c b/contrib/auth_delay/auth_delay.c index 8681b54fc3a8e..beaa6a22739ce 100644 --- a/contrib/auth_delay/auth_delay.c +++ b/contrib/auth_delay/auth_delay.c @@ -2,7 +2,7 @@ * * auth_delay.c * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/auth_delay/auth_delay.c diff --git a/contrib/auth_delay/meson.build b/contrib/auth_delay/meson.build index 2fecf2359976b..21192992a84e4 100644 --- a/contrib/auth_delay/meson.build +++ b/contrib/auth_delay/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group auth_delay_sources = files( 'auth_delay.c', diff --git a/contrib/auto_explain/Makefile b/contrib/auto_explain/Makefile index efd127d3cae64..1f608b1d73362 100644 --- a/contrib/auto_explain/Makefile +++ b/contrib/auto_explain/Makefile @@ -6,6 +6,9 @@ OBJS = \ auto_explain.o PGFILEDESC = "auto_explain - logging facility for execution plans" +EXTRA_INSTALL = contrib/pg_overexplain +REGRESS = alter_reset extension_options + TAP_TESTS = 1 ifdef USE_PGXS diff --git a/contrib/auto_explain/auto_explain.c b/contrib/auto_explain/auto_explain.c index 1f4badb492840..97ce498d0c1a0 100644 --- a/contrib/auto_explain/auto_explain.c +++ b/contrib/auto_explain/auto_explain.c @@ -3,7 +3,7 @@ * auto_explain.c * * - * Copyright (c) 2008-2025, PostgreSQL Global Development Group + * Copyright (c) 2008-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/auto_explain/auto_explain.c @@ -15,12 +15,17 @@ #include #include "access/parallel.h" +#include "commands/defrem.h" #include "commands/explain.h" #include "commands/explain_format.h" #include "commands/explain_state.h" #include "common/pg_prng.h" #include "executor/instrument.h" +#include "nodes/makefuncs.h" +#include "nodes/value.h" +#include "parser/scansup.h" #include "utils/guc.h" +#include "utils/varlena.h" PG_MODULE_MAGIC_EXT( .name = "auto_explain", @@ -33,6 +38,7 @@ static int auto_explain_log_parameter_max_length = -1; /* bytes or -1 */ static bool auto_explain_log_analyze = false; static bool auto_explain_log_verbose = false; static bool auto_explain_log_buffers = false; +static bool auto_explain_log_io = false; static bool auto_explain_log_wal = false; static bool auto_explain_log_triggers = false; static bool auto_explain_log_timing = true; @@ -41,6 +47,31 @@ static int auto_explain_log_format = EXPLAIN_FORMAT_TEXT; static int auto_explain_log_level = LOG; static bool auto_explain_log_nested_statements = false; static double auto_explain_sample_rate = 1; +static char *auto_explain_log_extension_options = NULL; + +/* + * Parsed form of one option from auto_explain.log_extension_options. + */ +typedef struct auto_explain_option +{ + char *name; + char *value; + NodeTag type; +} auto_explain_option; + +/* + * Parsed form of the entirety of auto_explain.log_extension_options, stored + * as GUC extra. The options[] array will have pointers into the string + * following the array. + */ +typedef struct auto_explain_extension_options +{ + int noptions; + auto_explain_option options[FLEXIBLE_ARRAY_MEMBER]; + /* a null-terminated copy of the GUC string follows the array */ +} auto_explain_extension_options; + +static auto_explain_extension_options *extension_options = NULL; static const struct config_enum_entry format_options[] = { {"text", EXPLAIN_FORMAT_TEXT, false}, @@ -88,6 +119,15 @@ static void explain_ExecutorRun(QueryDesc *queryDesc, static void explain_ExecutorFinish(QueryDesc *queryDesc); static void explain_ExecutorEnd(QueryDesc *queryDesc); +static bool check_log_extension_options(char **newval, void **extra, + GucSource source); +static void assign_log_extension_options(const char *newval, void *extra); +static void apply_extension_options(ExplainState *es, + auto_explain_extension_options *ext); +static char *auto_explain_scan_literal(char **endp, char **nextp); +static int auto_explain_split_options(char *rawstring, + auto_explain_option *options, + int maxoptions, char **errmsg); /* * Module load callback @@ -164,6 +204,17 @@ _PG_init(void) NULL, NULL); + DefineCustomBoolVariable("auto_explain.log_io", + "Log I/O statistics.", + NULL, + &auto_explain_log_io, + false, + PGC_SUSET, + 0, + NULL, + NULL, + NULL); + DefineCustomBoolVariable("auto_explain.log_wal", "Log WAL usage.", NULL, @@ -232,6 +283,17 @@ _PG_init(void) NULL, NULL); + DefineCustomStringVariable("auto_explain.log_extension_options", + "Extension EXPLAIN options to be added.", + NULL, + &auto_explain_log_extension_options, + NULL, + PGC_SUSET, + 0, + check_log_extension_options, + assign_log_extension_options, + NULL); + DefineCustomRealVariable("auto_explain.sample_rate", "Fraction of queries to process.", NULL, @@ -284,6 +346,9 @@ explain_ExecutorStart(QueryDesc *queryDesc, int eflags) if (auto_explain_enabled()) { + /* We're always interested in runtime */ + queryDesc->query_instr_options |= INSTRUMENT_TIMER; + /* Enable per-node instrumentation iff log_analyze is required. */ if (auto_explain_log_analyze && (eflags & EXEC_FLAG_EXPLAIN_ONLY) == 0) { @@ -293,6 +358,8 @@ explain_ExecutorStart(QueryDesc *queryDesc, int eflags) queryDesc->instrument_options |= INSTRUMENT_ROWS; if (auto_explain_log_buffers) queryDesc->instrument_options |= INSTRUMENT_BUFFERS; + if (auto_explain_log_io) + queryDesc->instrument_options |= INSTRUMENT_IO; if (auto_explain_log_wal) queryDesc->instrument_options |= INSTRUMENT_WAL; } @@ -302,23 +369,6 @@ explain_ExecutorStart(QueryDesc *queryDesc, int eflags) prev_ExecutorStart(queryDesc, eflags); else standard_ExecutorStart(queryDesc, eflags); - - if (auto_explain_enabled()) - { - /* - * Set up to track total elapsed time in ExecutorRun. Make sure the - * space is allocated in the per-query context so it will go away at - * ExecutorEnd. - */ - if (queryDesc->totaltime == NULL) - { - MemoryContext oldcxt; - - oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt); - queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false); - MemoryContextSwitchTo(oldcxt); - } - } } /* @@ -370,7 +420,7 @@ explain_ExecutorFinish(QueryDesc *queryDesc) static void explain_ExecutorEnd(QueryDesc *queryDesc) { - if (queryDesc->totaltime && auto_explain_enabled()) + if (queryDesc->query_instr && auto_explain_enabled()) { MemoryContext oldcxt; double msec; @@ -381,14 +431,8 @@ explain_ExecutorEnd(QueryDesc *queryDesc) */ oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt); - /* - * Make sure stats accumulation is done. (Note: it's okay if several - * levels of hook all do this.) - */ - InstrEndLoop(queryDesc->totaltime); - /* Log plan if duration is exceeded. */ - msec = queryDesc->totaltime->total * 1000.0; + msec = INSTR_TIME_GET_MILLISEC(queryDesc->query_instr->total); if (msec >= auto_explain_log_min_duration) { ExplainState *es = NewExplainState(); @@ -396,6 +440,7 @@ explain_ExecutorEnd(QueryDesc *queryDesc) es->analyze = (queryDesc->instrument_options && auto_explain_log_analyze); es->verbose = auto_explain_log_verbose; es->buffers = (es->analyze && auto_explain_log_buffers); + es->io = (es->analyze && auto_explain_log_io); es->wal = (es->analyze && auto_explain_log_wal); es->timing = (es->analyze && auto_explain_log_timing); es->summary = es->analyze; @@ -404,6 +449,8 @@ explain_ExecutorEnd(QueryDesc *queryDesc) es->format = auto_explain_log_format; es->settings = auto_explain_log_settings; + apply_extension_options(es, extension_options); + ExplainBeginOutput(es); ExplainQueryText(es, queryDesc); ExplainQueryParameters(es, queryDesc->params, auto_explain_log_parameter_max_length); @@ -412,6 +459,12 @@ explain_ExecutorEnd(QueryDesc *queryDesc) ExplainPrintTriggers(es, queryDesc); if (es->costs) ExplainPrintJITSummary(es, queryDesc); + if (explain_per_plan_hook) + (*explain_per_plan_hook) (queryDesc->plannedstmt, + NULL, es, + queryDesc->sourceText, + queryDesc->params, + queryDesc->estate->es_queryEnv); ExplainEndOutput(es); /* Remove last line break */ @@ -445,3 +498,332 @@ explain_ExecutorEnd(QueryDesc *queryDesc) else standard_ExecutorEnd(queryDesc); } + +/* + * GUC check hook for auto_explain.log_extension_options. + */ +static bool +check_log_extension_options(char **newval, void **extra, GucSource source) +{ + char *rawstring; + auto_explain_extension_options *result; + auto_explain_option *options; + int maxoptions = 8; + Size rawstring_len; + Size allocsize; + char *errmsg; + + /* NULL or empty string means no options. */ + if (*newval == NULL || (*newval)[0] == '\0') + { + *extra = NULL; + return true; + } + + rawstring_len = strlen(*newval) + 1; + +retry: + /* Try to allocate an auto_explain_extension_options object. */ + allocsize = offsetof(auto_explain_extension_options, options) + + sizeof(auto_explain_option) * maxoptions + + rawstring_len; + result = (auto_explain_extension_options *) guc_malloc(LOG, allocsize); + if (result == NULL) + return false; + + /* Copy the string after the options array. */ + rawstring = (char *) &result->options[maxoptions]; + memcpy(rawstring, *newval, rawstring_len); + + /* Parse. */ + options = result->options; + result->noptions = auto_explain_split_options(rawstring, options, + maxoptions, &errmsg); + if (result->noptions < 0) + { + GUC_check_errdetail("%s", errmsg); + guc_free(result); + return false; + } + + /* + * Retry with a larger array if needed. + * + * It should be impossible for this to loop more than once, because + * auto_explain_split_options tells us how many entries are needed. + */ + if (result->noptions > maxoptions) + { + maxoptions = result->noptions; + guc_free(result); + goto retry; + } + + /* Validate each option against its registered check handler. */ + for (int i = 0; i < result->noptions; i++) + { + if (!GUCCheckExplainExtensionOption(options[i].name, options[i].value, + options[i].type)) + { + guc_free(result); + return false; + } + } + + *extra = result; + return true; +} + +/* + * GUC assign hook for auto_explain.log_extension_options. + */ +static void +assign_log_extension_options(const char *newval, void *extra) +{ + extension_options = (auto_explain_extension_options *) extra; +} + +/* + * Apply parsed extension options to an ExplainState. + */ +static void +apply_extension_options(ExplainState *es, auto_explain_extension_options *ext) +{ + if (ext == NULL) + return; + + for (int i = 0; i < ext->noptions; i++) + { + auto_explain_option *opt = &ext->options[i]; + DefElem *def; + Node *arg; + + if (opt->value == NULL) + arg = NULL; + else if (opt->type == T_Integer) + arg = (Node *) makeInteger(strtol(opt->value, NULL, 0)); + else if (opt->type == T_Float) + arg = (Node *) makeFloat(opt->value); + else + arg = (Node *) makeString(opt->value); + + def = makeDefElem(opt->name, arg, -1); + ApplyExtensionExplainOption(es, def, NULL); + } +} + +/* + * auto_explain_scan_literal - In-place scanner for single-quoted string + * literals. + * + * This is the single-quote analog of scan_quoted_identifier from varlena.c. + */ +static char * +auto_explain_scan_literal(char **endp, char **nextp) +{ + char *token = *nextp + 1; + + for (;;) + { + *endp = strchr(*nextp + 1, '\''); + if (*endp == NULL) + return NULL; /* mismatched quotes */ + if ((*endp)[1] != '\'') + break; /* found end of literal */ + /* Collapse adjacent quotes into one quote, and look again */ + memmove(*endp, *endp + 1, strlen(*endp)); + *nextp = *endp; + } + /* *endp now points at the terminating quote */ + *nextp = *endp + 1; + + return token; +} + +/* + * auto_explain_split_options - Parse an option string into an array of + * auto_explain_option structs. + * + * Much of this logic is similar to SplitIdentifierString and friends, but our + * needs are different enough that we roll our own parsing logic. The goal here + * is to accept the same syntax that the main parser would accept inside of + * an EXPLAIN option list. While we can't do that perfectly without adding a + * lot more code, the goal of this implementation is to be close enough that + * users don't really notice the differences. + * + * The input string is modified in place (null-terminated, downcased, quotes + * collapsed). All name and value pointers in the output array refer into + * this string, so the caller must ensure the string outlives the array. + * + * Returns the full number of options in the input string, but stores no + * more than maxoptions into the caller-provided array. If a syntax error + * occurs, returns -1 and sets *errmsg. + */ +static int +auto_explain_split_options(char *rawstring, auto_explain_option *options, + int maxoptions, char **errmsg) +{ + char *nextp = rawstring; + int noptions = 0; + bool done = false; + + *errmsg = NULL; + + while (scanner_isspace(*nextp)) + nextp++; /* skip leading whitespace */ + + if (*nextp == '\0') + return 0; /* empty string is fine */ + + while (!done) + { + char *name; + char *name_endp; + char *value = NULL; + char *value_endp = NULL; + NodeTag type = T_Invalid; + + /* Parse the option name. */ + name = scan_identifier(&name_endp, &nextp, ',', true); + if (name == NULL || name_endp == name) + { + *errmsg = "option name missing or empty"; + return -1; + } + + /* Skip whitespace after the option name. */ + while (scanner_isspace(*nextp)) + nextp++; + + /* + * Determine whether we have an option value. A comma or end of + * string means no value; otherwise we have one. + */ + if (*nextp != '\0' && *nextp != ',') + { + if (*nextp == '\'') + { + /* Single-quoted string literal. */ + type = T_String; + value = auto_explain_scan_literal(&value_endp, &nextp); + if (value == NULL) + { + *errmsg = "unterminated single-quoted string"; + return -1; + } + } + else if (isdigit((unsigned char) *nextp) || + ((*nextp == '+' || *nextp == '-') && + isdigit((unsigned char) nextp[1]))) + { + char *endptr; + long intval; + char saved; + + /* Remember the start of the next token, and find the end. */ + value = nextp; + while (*nextp && *nextp != ',' && !scanner_isspace(*nextp)) + nextp++; + value_endp = nextp; + + /* Temporarily '\0'-terminate so we can use strtol/strtod. */ + saved = *value_endp; + *value_endp = '\0'; + + /* + * Integer, float, or neither? + * + * NB: Since we use strtol and strtod here rather than + * pg_strtoint64_safe, some syntax that would be accepted by + * the main parser is not accepted here, e.g. 100_000. On the + * plus side, strtol and strtod won't allocate, and + * pg_strtoint64_safe might. For now, it seems better to keep + * things simple here. + */ + errno = 0; + intval = strtol(value, &endptr, 0); + if (errno == 0 && *endptr == '\0' && endptr != value && + intval == (int) intval) + type = T_Integer; + else + { + type = T_Float; + (void) strtod(value, &endptr); + if (*endptr != '\0') + { + *value_endp = saved; + *errmsg = "invalid numeric value"; + return -1; + } + } + + /* Remove temporary terminator. */ + *value_endp = saved; + } + else + { + /* Identifier, possibly double-quoted. */ + type = T_String; + value = scan_identifier(&value_endp, &nextp, ',', true); + if (value == NULL) + { + /* + * scan_identifier will return NULL if it finds an + * unterminated double-quoted identifier or it finds no + * identifier at all because the next character is + * whitespace or the separator character, here a comma. + * But the latter case is impossible here because the code + * above has skipped whitespace and checked for commas. + */ + *errmsg = "unterminated double-quoted string"; + return -1; + } + } + } + + /* Skip trailing whitespace. */ + while (scanner_isspace(*nextp)) + nextp++; + + /* Expect comma or end of string. */ + if (*nextp == ',') + { + nextp++; + while (scanner_isspace(*nextp)) + nextp++; + if (*nextp == '\0') + { + *errmsg = "trailing comma in option list"; + return -1; + } + } + else if (*nextp == '\0') + done = true; + else + { + *errmsg = "expected comma or end of option list"; + return -1; + } + + /* + * Now safe to null-terminate the name and value. We couldn't do this + * earlier because in the unquoted case, the null terminator position + * may coincide with a character that the scanning logic above still + * needed to read. + */ + *name_endp = '\0'; + if (value_endp != NULL) + *value_endp = '\0'; + + /* Always count this option, and store the details if there is room. */ + if (noptions < maxoptions) + { + options[noptions].name = name; + options[noptions].type = type; + options[noptions].value = value; + } + noptions++; + } + + return noptions; +} diff --git a/contrib/auto_explain/expected/alter_reset.out b/contrib/auto_explain/expected/alter_reset.out new file mode 100644 index 0000000000000..ec355189806ae --- /dev/null +++ b/contrib/auto_explain/expected/alter_reset.out @@ -0,0 +1,19 @@ +-- +-- This tests resetting unknown custom GUCs with reserved prefixes. There's +-- nothing specific to auto_explain; this is just a convenient place to put +-- this test. +-- +SELECT current_database() AS datname \gset +CREATE ROLE regress_ae_role; +ALTER DATABASE :"datname" SET auto_explain.bogus = 1; +ALTER ROLE regress_ae_role SET auto_explain.bogus = 1; +ALTER ROLE regress_ae_role IN DATABASE :"datname" SET auto_explain.bogus = 1; +ALTER SYSTEM SET auto_explain.bogus = 1; +LOAD 'auto_explain'; +WARNING: invalid configuration parameter name "auto_explain.bogus", removing it +DETAIL: "auto_explain" is now a reserved prefix. +ALTER DATABASE :"datname" RESET auto_explain.bogus; +ALTER ROLE regress_ae_role RESET auto_explain.bogus; +ALTER ROLE regress_ae_role IN DATABASE :"datname" RESET auto_explain.bogus; +ALTER SYSTEM RESET auto_explain.bogus; +DROP ROLE regress_ae_role; diff --git a/contrib/auto_explain/expected/extension_options.out b/contrib/auto_explain/expected/extension_options.out new file mode 100644 index 0000000000000..b5a667723119d --- /dev/null +++ b/contrib/auto_explain/expected/extension_options.out @@ -0,0 +1,49 @@ +-- +-- Tests for auto_explain.log_extension_options. +-- +LOAD 'auto_explain'; +LOAD 'pg_overexplain'; +-- Various legal values with assorted quoting and whitespace choices. +SET auto_explain.log_extension_options = ''; +SET auto_explain.log_extension_options = 'debug, RANGE_TABLE'; +SET auto_explain.log_extension_options = 'debug TRUE '; +SET auto_explain.log_extension_options = ' debug 1,RAnge_table "off"'; +SET auto_explain.log_extension_options = $$"debug" tRuE, range_table 'false'$$; +-- Syntax errors. +SET auto_explain.log_extension_options = ','; +ERROR: invalid value for parameter "auto_explain.log_extension_options": "," +DETAIL: option name missing or empty +SET auto_explain.log_extension_options = ', range_table'; +ERROR: invalid value for parameter "auto_explain.log_extension_options": ", range_table" +DETAIL: option name missing or empty +SET auto_explain.log_extension_options = 'range_table, '; +ERROR: invalid value for parameter "auto_explain.log_extension_options": "range_table, " +DETAIL: trailing comma in option list +SET auto_explain.log_extension_options = 'range_table true false'; +ERROR: invalid value for parameter "auto_explain.log_extension_options": "range_table true false" +DETAIL: expected comma or end of option list +SET auto_explain.log_extension_options = '"range_table'; +ERROR: invalid value for parameter "auto_explain.log_extension_options": ""range_table" +DETAIL: option name missing or empty +SET auto_explain.log_extension_options = 'range_table 3.1415nine'; +ERROR: invalid value for parameter "auto_explain.log_extension_options": "range_table 3.1415nine" +DETAIL: invalid numeric value +SET auto_explain.log_extension_options = 'range_table "true'; +ERROR: invalid value for parameter "auto_explain.log_extension_options": "range_table "true" +DETAIL: unterminated double-quoted string +SET auto_explain.log_extension_options = $$range_table 'true$$; +ERROR: invalid value for parameter "auto_explain.log_extension_options": "range_table 'true" +DETAIL: unterminated single-quoted string +SET auto_explain.log_extension_options = $$'$$; +ERROR: unrecognized EXPLAIN option "'" +-- Unacceptable option values. +SET auto_explain.log_extension_options = 'range_table maybe'; +ERROR: EXPLAIN option "range_table" requires a Boolean value +SET auto_explain.log_extension_options = 'range_table 2'; +ERROR: EXPLAIN option "range_table" requires a Boolean value +SET auto_explain.log_extension_options = 'range_table "0"'; +ERROR: EXPLAIN option "range_table" requires a Boolean value +SET auto_explain.log_extension_options = 'range_table 3.14159'; +ERROR: EXPLAIN option "range_table" requires a Boolean value +-- Supply enough options to force the option array to be reallocated. +SET auto_explain.log_extension_options = 'debug, debug, debug, debug, debug, debug, debug, debug, debug, debug false'; diff --git a/contrib/auto_explain/meson.build b/contrib/auto_explain/meson.build index 92dc9df6f7cac..d2b0650af1cbf 100644 --- a/contrib/auto_explain/meson.build +++ b/contrib/auto_explain/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group auto_explain_sources = files( 'auto_explain.c', @@ -20,6 +20,12 @@ tests += { 'name': 'auto_explain', 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'alter_reset', + 'extension_options', + ], + }, 'tap': { 'tests': [ 't/001_auto_explain.pl', diff --git a/contrib/auto_explain/sql/alter_reset.sql b/contrib/auto_explain/sql/alter_reset.sql new file mode 100644 index 0000000000000..bf621454ec24a --- /dev/null +++ b/contrib/auto_explain/sql/alter_reset.sql @@ -0,0 +1,22 @@ +-- +-- This tests resetting unknown custom GUCs with reserved prefixes. There's +-- nothing specific to auto_explain; this is just a convenient place to put +-- this test. +-- + +SELECT current_database() AS datname \gset +CREATE ROLE regress_ae_role; + +ALTER DATABASE :"datname" SET auto_explain.bogus = 1; +ALTER ROLE regress_ae_role SET auto_explain.bogus = 1; +ALTER ROLE regress_ae_role IN DATABASE :"datname" SET auto_explain.bogus = 1; +ALTER SYSTEM SET auto_explain.bogus = 1; + +LOAD 'auto_explain'; + +ALTER DATABASE :"datname" RESET auto_explain.bogus; +ALTER ROLE regress_ae_role RESET auto_explain.bogus; +ALTER ROLE regress_ae_role IN DATABASE :"datname" RESET auto_explain.bogus; +ALTER SYSTEM RESET auto_explain.bogus; + +DROP ROLE regress_ae_role; diff --git a/contrib/auto_explain/sql/extension_options.sql b/contrib/auto_explain/sql/extension_options.sql new file mode 100644 index 0000000000000..98920e88c9f64 --- /dev/null +++ b/contrib/auto_explain/sql/extension_options.sql @@ -0,0 +1,33 @@ +-- +-- Tests for auto_explain.log_extension_options. +-- + +LOAD 'auto_explain'; +LOAD 'pg_overexplain'; + +-- Various legal values with assorted quoting and whitespace choices. +SET auto_explain.log_extension_options = ''; +SET auto_explain.log_extension_options = 'debug, RANGE_TABLE'; +SET auto_explain.log_extension_options = 'debug TRUE '; +SET auto_explain.log_extension_options = ' debug 1,RAnge_table "off"'; +SET auto_explain.log_extension_options = $$"debug" tRuE, range_table 'false'$$; + +-- Syntax errors. +SET auto_explain.log_extension_options = ','; +SET auto_explain.log_extension_options = ', range_table'; +SET auto_explain.log_extension_options = 'range_table, '; +SET auto_explain.log_extension_options = 'range_table true false'; +SET auto_explain.log_extension_options = '"range_table'; +SET auto_explain.log_extension_options = 'range_table 3.1415nine'; +SET auto_explain.log_extension_options = 'range_table "true'; +SET auto_explain.log_extension_options = $$range_table 'true$$; +SET auto_explain.log_extension_options = $$'$$; + +-- Unacceptable option values. +SET auto_explain.log_extension_options = 'range_table maybe'; +SET auto_explain.log_extension_options = 'range_table 2'; +SET auto_explain.log_extension_options = 'range_table "0"'; +SET auto_explain.log_extension_options = 'range_table 3.14159'; + +-- Supply enough options to force the option array to be reallocated. +SET auto_explain.log_extension_options = 'debug, debug, debug, debug, debug, debug, debug, debug, debug, debug false'; diff --git a/contrib/auto_explain/t/001_auto_explain.pl b/contrib/auto_explain/t/001_auto_explain.pl index 6af5ac1da1845..b4e8e4b65a1dc 100644 --- a/contrib/auto_explain/t/001_auto_explain.pl +++ b/contrib/auto_explain/t/001_auto_explain.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -30,7 +30,7 @@ sub query_log my $node = PostgreSQL::Test::Cluster->new('main'); $node->init(auth_extra => [ '--create-role' => 'regress_user1' ]); $node->append_conf('postgresql.conf', - "session_preload_libraries = 'auto_explain'"); + "session_preload_libraries = 'pg_overexplain,auto_explain'"); $node->append_conf('postgresql.conf', "auto_explain.log_min_duration = 0"); $node->append_conf('postgresql.conf', "auto_explain.log_analyze = on"); $node->start; @@ -172,6 +172,22 @@ sub query_log qr/"Node Type": "Index Scan"[^}]*"Index Name": "pg_class_relname_nsp_index"/s, "index scan logged, json mode"); +# Extension options. +$log_contents = query_log( + $node, + "SELECT 1;", + { "auto_explain.log_extension_options" => "debug" }); + +like( + $log_contents, + qr/Parallel Safe:/, + "extension option produces per-node output"); + +like( + $log_contents, + qr/Command Type: select/, + "extension option produces per-plan output"); + # Check that PGC_SUSET parameters can be set by non-superuser if granted, # otherwise not diff --git a/contrib/basebackup_to_shell/basebackup_to_shell.c b/contrib/basebackup_to_shell/basebackup_to_shell.c index 8720f5a43727d..4d4099f177d06 100644 --- a/contrib/basebackup_to_shell/basebackup_to_shell.c +++ b/contrib/basebackup_to_shell/basebackup_to_shell.c @@ -3,7 +3,7 @@ * basebackup_to_shell.c * target base backup files to a shell command * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * contrib/basebackup_to_shell/basebackup_to_shell.c *------------------------------------------------------------------------- @@ -136,7 +136,7 @@ shell_get_sink(bbsink *next_sink, void *detail_arg) * We remember the current value of basebackup_to_shell.shell_command to * be certain that it can't change under us during the backup. */ - sink = palloc0(sizeof(bbsink_shell)); + sink = palloc0_object(bbsink_shell); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops; sink->base.bbs_next = next_sink; sink->target_detail = detail_arg; diff --git a/contrib/basebackup_to_shell/meson.build b/contrib/basebackup_to_shell/meson.build index 8c88242456e80..eb23a9fec81fc 100644 --- a/contrib/basebackup_to_shell/meson.build +++ b/contrib/basebackup_to_shell/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group basebackup_to_shell_sources = files( 'basebackup_to_shell.c', @@ -24,7 +24,7 @@ tests += { 'tests': [ 't/001_basic.pl', ], - 'env': {'GZIP_PROGRAM': gzip.found() ? gzip.path() : '', - 'TAR': tar.found() ? tar.path() : '' }, + 'env': {'GZIP_PROGRAM': gzip.found() ? gzip.full_path() : '', + 'TAR': tar.found() ? tar.full_path() : '' }, }, } diff --git a/contrib/basebackup_to_shell/t/001_basic.pl b/contrib/basebackup_to_shell/t/001_basic.pl index 4d540147a6de9..93acc7d4ca844 100644 --- a/contrib/basebackup_to_shell/t/001_basic.pl +++ b/contrib/basebackup_to_shell/t/001_basic.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c index 4a8b8c7ac29c1..914a0b56d162f 100644 --- a/contrib/basic_archive/basic_archive.c +++ b/contrib/basic_archive/basic_archive.c @@ -17,7 +17,7 @@ * a file is successfully archived and then the system crashes before * a durable record of the success has been made. * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/basic_archive/basic_archive.c @@ -65,7 +65,7 @@ void _PG_init(void) { DefineCustomStringVariable("basic_archive.archive_directory", - gettext_noop("Archive file destination directory."), + "Archive file destination directory.", NULL, &archive_directory, "", @@ -90,19 +90,17 @@ _PG_archive_module_init(void) /* * check_archive_directory * - * Checks that the provided archive directory exists. + * Checks that the provided archive directory path isn't too long. */ static bool check_archive_directory(char **newval, void **extra, GucSource source) { - struct stat st; - /* * The default value is an empty string, so we have to accept that value. * Our check_configured callback also checks for this and prevents * archiving from proceeding if it is still empty. */ - if (*newval == NULL || *newval[0] == '\0') + if (*newval == NULL || (*newval)[0] == '\0') return true; /* @@ -115,17 +113,6 @@ check_archive_directory(char **newval, void **extra, GucSource source) return false; } - /* - * Do a basic sanity check that the specified archive directory exists. It - * could be removed at some point in the future, so we still need to be - * prepared for it not to exist in the actual archiving logic. - */ - if (stat(*newval, &st) != 0 || !S_ISDIR(st.st_mode)) - { - GUC_check_errdetail("Specified archive directory does not exist."); - return false; - } - return true; } diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build index c2d8a38c7ca32..3c389037c8f3a 100644 --- a/contrib/basic_archive/meson.build +++ b/contrib/basic_archive/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group basic_archive_sources = files( 'basic_archive.c', diff --git a/contrib/bloom/blcost.c b/contrib/bloom/blcost.c index a38fcf3c57918..5a733dc10ca53 100644 --- a/contrib/bloom/blcost.c +++ b/contrib/bloom/blcost.c @@ -3,7 +3,7 @@ * blcost.c * Cost estimate function for bloom indexes. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/bloom/blcost.c @@ -30,6 +30,9 @@ blcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, /* We have to visit all index tuples anyway */ costs.numIndexTuples = index->tuples; + /* As in btcostestimate, count only the metapage as non-leaf */ + costs.numNonLeafPages = 1; + /* Use generic estimate */ genericcostestimate(root, path, loop_count, &costs); diff --git a/contrib/bloom/blinsert.c b/contrib/bloom/blinsert.c index 7866438122f58..df24856d9ae12 100644 --- a/contrib/bloom/blinsert.c +++ b/contrib/bloom/blinsert.c @@ -3,7 +3,7 @@ * blinsert.c * Bloom index build and insert functions. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/bloom/blinsert.c @@ -151,7 +151,7 @@ blbuild(Relation heap, Relation index, IndexInfo *indexInfo) MemoryContextDelete(buildstate.tmpCtx); - result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + result = palloc_object(IndexBuildResult); result->heap_tuples = reltuples; result->index_tuples = buildstate.indtuples; diff --git a/contrib/bloom/bloom.h b/contrib/bloom/bloom.h index 648167045f4e8..9a4125bdfb1d2 100644 --- a/contrib/bloom/bloom.h +++ b/contrib/bloom/bloom.h @@ -3,7 +3,7 @@ * bloom.h * Header for bloom index. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/bloom/bloom.h @@ -72,7 +72,7 @@ typedef BloomPageOpaqueData *BloomPageOpaque; ((BloomTuple *)(PageGetContents(page) \ + (state)->sizeOfBloomTuple * ((offset) - 1))) #define BloomPageGetNextTuple(state, tuple) \ - ((BloomTuple *)((Pointer)(tuple) + (state)->sizeOfBloomTuple)) + ((BloomTuple *)((char *)(tuple) + (state)->sizeOfBloomTuple)) /* Preserved page numbers */ #define BLOOM_METAPAGE_BLKNO (0) diff --git a/contrib/bloom/blscan.c b/contrib/bloom/blscan.c index d072f47fe28b5..1a0e42021ec1e 100644 --- a/contrib/bloom/blscan.c +++ b/contrib/bloom/blscan.c @@ -3,7 +3,7 @@ * blscan.c * Bloom index scan functions. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/bloom/blscan.c @@ -14,9 +14,11 @@ #include "access/relscan.h" #include "bloom.h" +#include "executor/instrument_node.h" #include "miscadmin.h" #include "pgstat.h" #include "storage/bufmgr.h" +#include "storage/read_stream.h" /* * Begin scan of bloom index. @@ -29,7 +31,7 @@ blbeginscan(Relation r, int nkeys, int norderbys) scan = RelationGetIndexScan(r, nkeys, norderbys); - so = (BloomScanOpaque) palloc(sizeof(BloomScanOpaqueData)); + so = (BloomScanOpaque) palloc_object(BloomScanOpaqueData); initBloomState(&so->state, scan->indexRelation); so->sign = NULL; @@ -75,18 +77,20 @@ int64 blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) { int64 ntids = 0; - BlockNumber blkno = BLOOM_HEAD_BLKNO, + BlockNumber blkno, npages; int i; BufferAccessStrategy bas; BloomScanOpaque so = (BloomScanOpaque) scan->opaque; + BlockRangeReadStreamPrivate p; + ReadStream *stream; if (so->sign == NULL) { /* New search: have to calculate search signature */ ScanKey skey = scan->keyData; - so->sign = palloc0(sizeof(BloomSignatureWord) * so->state.opts.bloomLength); + so->sign = palloc0_array(BloomSignatureWord, so->state.opts.bloomLength); for (i = 0; i < scan->numberOfKeys; i++) { @@ -119,14 +123,29 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) if (scan->instrument) scan->instrument->nsearches++; + /* Scan all blocks except the metapage using streaming reads */ + p.current_blocknum = BLOOM_HEAD_BLKNO; + p.last_exclusive = npages; + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + bas, + scan->indexRelation, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); + for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++) { Buffer buffer; Page page; - buffer = ReadBufferExtended(scan->indexRelation, MAIN_FORKNUM, - blkno, RBM_NORMAL, bas); - + buffer = read_stream_next_buffer(stream, NULL); LockBuffer(buffer, BUFFER_LOCK_SHARE); page = BufferGetPage(buffer); @@ -162,6 +181,9 @@ blgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) UnlockReleaseBuffer(buffer); CHECK_FOR_INTERRUPTS(); } + + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); FreeAccessStrategy(bas); return ntids; diff --git a/contrib/bloom/blutils.c b/contrib/bloom/blutils.c index 2c0e71eedc654..5111cdc6dd62f 100644 --- a/contrib/bloom/blutils.c +++ b/contrib/bloom/blutils.c @@ -3,7 +3,7 @@ * blutils.c * Bloom index utilities. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1990-1993, Regents of the University of California * * IDENTIFICATION @@ -86,7 +86,7 @@ makeDefaultBloomOptions(void) BloomOptions *opts; int i; - opts = (BloomOptions *) palloc0(sizeof(BloomOptions)); + opts = palloc0_object(BloomOptions); /* Convert DEFAULT_BLOOM_LENGTH from # of bits to # of words */ opts->bloomLength = (DEFAULT_BLOOM_LENGTH + SIGNWORDBITS - 1) / SIGNWORDBITS; for (i = 0; i < INDEX_MAX_KEYS; i++) @@ -102,61 +102,62 @@ makeDefaultBloomOptions(void) Datum blhandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = BLOOM_NSTRATEGIES; - amroutine->amsupport = BLOOM_NPROC; - amroutine->amoptsprocnum = BLOOM_OPTIONS_PROC; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = false; - amroutine->amcanhash = false; - amroutine->amconsistentequality = false; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = false; - amroutine->amcanunique = false; - amroutine->amcanmulticol = true; - amroutine->amoptionalkey = true; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = false; - amroutine->amstorage = false; - amroutine->amclusterable = false; - amroutine->ampredlocks = false; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = false; - amroutine->amcaninclude = false; - amroutine->amusemaintenanceworkmem = false; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = blbuild; - amroutine->ambuildempty = blbuildempty; - amroutine->aminsert = blinsert; - amroutine->aminsertcleanup = NULL; - amroutine->ambulkdelete = blbulkdelete; - amroutine->amvacuumcleanup = blvacuumcleanup; - amroutine->amcanreturn = NULL; - amroutine->amcostestimate = blcostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = bloptions; - amroutine->amproperty = NULL; - amroutine->ambuildphasename = NULL; - amroutine->amvalidate = blvalidate; - amroutine->amadjustmembers = NULL; - amroutine->ambeginscan = blbeginscan; - amroutine->amrescan = blrescan; - amroutine->amgettuple = NULL; - amroutine->amgetbitmap = blgetbitmap; - amroutine->amendscan = blendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - amroutine->amtranslatestrategy = NULL; - amroutine->amtranslatecmptype = NULL; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = BLOOM_NSTRATEGIES, + .amsupport = BLOOM_NPROC, + .amoptsprocnum = BLOOM_OPTIONS_PROC, + .amcanorder = false, + .amcanorderbyop = false, + .amcanhash = false, + .amconsistentequality = false, + .amconsistentordering = false, + .amcanbackward = false, + .amcanunique = false, + .amcanmulticol = true, + .amoptionalkey = true, + .amsearcharray = false, + .amsearchnulls = false, + .amstorage = false, + .amclusterable = false, + .ampredlocks = false, + .amcanparallel = false, + .amcanbuildparallel = false, + .amcaninclude = false, + .amusemaintenanceworkmem = false, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP, + .amkeytype = InvalidOid, + + .ambuild = blbuild, + .ambuildempty = blbuildempty, + .aminsert = blinsert, + .aminsertcleanup = NULL, + .ambulkdelete = blbulkdelete, + .amvacuumcleanup = blvacuumcleanup, + .amcanreturn = NULL, + .amcostestimate = blcostestimate, + .amgettreeheight = NULL, + .amoptions = bloptions, + .amproperty = NULL, + .ambuildphasename = NULL, + .amvalidate = blvalidate, + .amadjustmembers = NULL, + .ambeginscan = blbeginscan, + .amrescan = blrescan, + .amgettuple = NULL, + .amgetbitmap = blgetbitmap, + .amendscan = blendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + .amtranslatestrategy = NULL, + .amtranslatecmptype = NULL, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -324,7 +325,7 @@ BloomPageAddItem(BloomState *state, Page page, BloomTuple *tuple) { BloomTuple *itup; BloomPageOpaque opaque; - Pointer ptr; + char *ptr; /* We shouldn't be pointed to an invalid page */ Assert(!PageIsNew(page) && !BloomPageIsDeleted(page)); @@ -336,11 +337,11 @@ BloomPageAddItem(BloomState *state, Page page, BloomTuple *tuple) /* Copy new tuple to the end of page */ opaque = BloomPageGetOpaque(page); itup = BloomPageGetTuple(state, page, opaque->maxoff + 1); - memcpy((Pointer) itup, (Pointer) tuple, state->sizeOfBloomTuple); + memcpy(itup, tuple, state->sizeOfBloomTuple); /* Adjust maxoff and pd_lower */ opaque->maxoff++; - ptr = (Pointer) BloomPageGetTuple(state, page, opaque->maxoff + 1); + ptr = (char *) BloomPageGetTuple(state, page, opaque->maxoff + 1); ((PageHeader) page)->pd_lower = ptr - page; /* Assert we didn't overrun available space */ diff --git a/contrib/bloom/blvacuum.c b/contrib/bloom/blvacuum.c index 86b15a75f6fb9..6beb1c20ebb0d 100644 --- a/contrib/bloom/blvacuum.c +++ b/contrib/bloom/blvacuum.c @@ -3,7 +3,7 @@ * blvacuum.c * Bloom VACUUM functions. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/bloom/blvacuum.c @@ -17,6 +17,7 @@ #include "commands/vacuum.h" #include "storage/bufmgr.h" #include "storage/indexfsm.h" +#include "storage/read_stream.h" /* @@ -40,9 +41,11 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, Page page; BloomMetaPageData *metaData; GenericXLogState *gxlogState; + BlockRangeReadStreamPrivate p; + ReadStream *stream; if (stats == NULL) - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); initBloomState(&state, index); @@ -51,6 +54,25 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, * they can't contain tuples to delete. */ npages = RelationGetNumberOfBlocks(index); + + /* Scan all blocks except the metapage using streaming reads */ + p.current_blocknum = BLOOM_HEAD_BLKNO; + p.last_exclusive = npages; + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE | + READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + info->strategy, + index, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); + for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++) { BloomTuple *itup, @@ -59,8 +81,7 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, vacuum_delay_point(false); - buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, - RBM_NORMAL, info->strategy); + buffer = read_stream_next_buffer(stream, NULL); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); gxlogState = GenericXLogStart(index); @@ -94,8 +115,7 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, { /* No; copy it to itupPtr++, but skip copy if not needed */ if (itupPtr != itup) - memmove((Pointer) itupPtr, (Pointer) itup, - state.sizeOfBloomTuple); + memmove(itupPtr, itup, state.sizeOfBloomTuple); itupPtr = BloomPageGetNextTuple(&state, itupPtr); } @@ -122,7 +142,7 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, if (BloomPageGetMaxOffset(page) == 0) BloomPageSetDeleted(page); /* Adjust pd_lower */ - ((PageHeader) page)->pd_lower = (Pointer) itupPtr - page; + ((PageHeader) page)->pd_lower = (char *) itupPtr - page; /* Finish WAL-logging */ GenericXLogFinish(gxlogState); } @@ -134,6 +154,9 @@ blbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, UnlockReleaseBuffer(buffer); } + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); + /* * Update the metapage's notFullPage list with whatever we found. Our * info could already be out of date at this point, but blinsert() will @@ -167,12 +190,14 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) Relation index = info->index; BlockNumber npages, blkno; + BlockRangeReadStreamPrivate p; + ReadStream *stream; if (info->analyze_only) return stats; if (stats == NULL) - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); /* * Iterate over the pages: insert deleted pages into FSM and collect @@ -182,6 +207,25 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) stats->num_pages = npages; stats->pages_free = 0; stats->num_index_tuples = 0; + + /* Scan all blocks except the metapage using streaming reads */ + p.current_blocknum = BLOOM_HEAD_BLKNO; + p.last_exclusive = npages; + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE | + READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + info->strategy, + index, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); + for (blkno = BLOOM_HEAD_BLKNO; blkno < npages; blkno++) { Buffer buffer; @@ -189,10 +233,9 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) vacuum_delay_point(false); - buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, - RBM_NORMAL, info->strategy); + buffer = read_stream_next_buffer(stream, NULL); LockBuffer(buffer, BUFFER_LOCK_SHARE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (PageIsNew(page) || BloomPageIsDeleted(page)) { @@ -207,6 +250,9 @@ blvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) UnlockReleaseBuffer(buffer); } + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); + IndexFreeSpaceMapVacuum(info->index); return stats; diff --git a/contrib/bloom/blvalidate.c b/contrib/bloom/blvalidate.c index 001c188aeb712..538b039596c4d 100644 --- a/contrib/bloom/blvalidate.c +++ b/contrib/bloom/blvalidate.c @@ -3,7 +3,7 @@ * blvalidate.c * Opclass validator for bloom. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/bloom/blvalidate.c diff --git a/contrib/bloom/meson.build b/contrib/bloom/meson.build index 695712a455e6d..fa4f4ea796ba3 100644 --- a/contrib/bloom/meson.build +++ b/contrib/bloom/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group bloom_sources = files( 'blcost.c', diff --git a/contrib/bloom/t/001_wal.pl b/contrib/bloom/t/001_wal.pl index 612920001cd2a..683b187605558 100644 --- a/contrib/bloom/t/001_wal.pl +++ b/contrib/bloom/t/001_wal.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test generic xlog record work for bloom index replication. use strict; diff --git a/contrib/bool_plperl/meson.build b/contrib/bool_plperl/meson.build index f489d99044ce0..d19abf0dd6ddb 100644 --- a/contrib/bool_plperl/meson.build +++ b/contrib/bool_plperl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not perl_dep.found() subdir_done() diff --git a/contrib/btree_gin/Makefile b/contrib/btree_gin/Makefile index 0a15811516819..ad054598db6c9 100644 --- a/contrib/btree_gin/Makefile +++ b/contrib/btree_gin/Makefile @@ -7,7 +7,7 @@ OBJS = \ EXTENSION = btree_gin DATA = btree_gin--1.0.sql btree_gin--1.0--1.1.sql btree_gin--1.1--1.2.sql \ - btree_gin--1.2--1.3.sql + btree_gin--1.2--1.3.sql btree_gin--1.3--1.4.sql PGFILEDESC = "btree_gin - B-tree equivalent GIN operator classes" REGRESS = install_btree_gin int2 int4 int8 float4 float8 money oid \ diff --git a/contrib/btree_gin/btree_gin--1.3--1.4.sql b/contrib/btree_gin/btree_gin--1.3--1.4.sql new file mode 100644 index 0000000000000..61b5dcbede6c5 --- /dev/null +++ b/contrib/btree_gin/btree_gin--1.3--1.4.sql @@ -0,0 +1,151 @@ +/* contrib/btree_gin/btree_gin--1.3--1.4.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "ALTER EXTENSION btree_gin UPDATE TO '1.4'" to load this file. \quit + +-- +-- Cross-type operator support is new in 1.4. We only need to worry +-- about this for cross-type operators that exist in core. +-- +-- Because the opclass extractQuery and consistent methods don't directly +-- get any information about the datatype of the RHS value, we have to +-- encode that in the operator strategy numbers. The strategy numbers +-- are the operator's normal btree strategy (1-5) plus 16 times a code +-- for the RHS datatype. +-- + +ALTER OPERATOR FAMILY int2_ops USING gin +ADD + -- Code 1: RHS is int4 + OPERATOR 0x11 < (int2, int4), + OPERATOR 0x12 <= (int2, int4), + OPERATOR 0x13 = (int2, int4), + OPERATOR 0x14 >= (int2, int4), + OPERATOR 0x15 > (int2, int4), + -- Code 2: RHS is int8 + OPERATOR 0x21 < (int2, int8), + OPERATOR 0x22 <= (int2, int8), + OPERATOR 0x23 = (int2, int8), + OPERATOR 0x24 >= (int2, int8), + OPERATOR 0x25 > (int2, int8) +; + +ALTER OPERATOR FAMILY int4_ops USING gin +ADD + -- Code 1: RHS is int2 + OPERATOR 0x11 < (int4, int2), + OPERATOR 0x12 <= (int4, int2), + OPERATOR 0x13 = (int4, int2), + OPERATOR 0x14 >= (int4, int2), + OPERATOR 0x15 > (int4, int2), + -- Code 2: RHS is int8 + OPERATOR 0x21 < (int4, int8), + OPERATOR 0x22 <= (int4, int8), + OPERATOR 0x23 = (int4, int8), + OPERATOR 0x24 >= (int4, int8), + OPERATOR 0x25 > (int4, int8) +; + +ALTER OPERATOR FAMILY int8_ops USING gin +ADD + -- Code 1: RHS is int2 + OPERATOR 0x11 < (int8, int2), + OPERATOR 0x12 <= (int8, int2), + OPERATOR 0x13 = (int8, int2), + OPERATOR 0x14 >= (int8, int2), + OPERATOR 0x15 > (int8, int2), + -- Code 2: RHS is int4 + OPERATOR 0x21 < (int8, int4), + OPERATOR 0x22 <= (int8, int4), + OPERATOR 0x23 = (int8, int4), + OPERATOR 0x24 >= (int8, int4), + OPERATOR 0x25 > (int8, int4) +; + +ALTER OPERATOR FAMILY float4_ops USING gin +ADD + -- Code 1: RHS is float8 + OPERATOR 0x11 < (float4, float8), + OPERATOR 0x12 <= (float4, float8), + OPERATOR 0x13 = (float4, float8), + OPERATOR 0x14 >= (float4, float8), + OPERATOR 0x15 > (float4, float8) +; + +ALTER OPERATOR FAMILY float8_ops USING gin +ADD + -- Code 1: RHS is float4 + OPERATOR 0x11 < (float8, float4), + OPERATOR 0x12 <= (float8, float4), + OPERATOR 0x13 = (float8, float4), + OPERATOR 0x14 >= (float8, float4), + OPERATOR 0x15 > (float8, float4) +; + +ALTER OPERATOR FAMILY text_ops USING gin +ADD + -- Code 1: RHS is name + OPERATOR 0x11 < (text, name), + OPERATOR 0x12 <= (text, name), + OPERATOR 0x13 = (text, name), + OPERATOR 0x14 >= (text, name), + OPERATOR 0x15 > (text, name) +; + +ALTER OPERATOR FAMILY name_ops USING gin +ADD + -- Code 1: RHS is text + OPERATOR 0x11 < (name, text), + OPERATOR 0x12 <= (name, text), + OPERATOR 0x13 = (name, text), + OPERATOR 0x14 >= (name, text), + OPERATOR 0x15 > (name, text) +; + +ALTER OPERATOR FAMILY date_ops USING gin +ADD + -- Code 1: RHS is timestamp + OPERATOR 0x11 < (date, timestamp), + OPERATOR 0x12 <= (date, timestamp), + OPERATOR 0x13 = (date, timestamp), + OPERATOR 0x14 >= (date, timestamp), + OPERATOR 0x15 > (date, timestamp), + -- Code 2: RHS is timestamptz + OPERATOR 0x21 < (date, timestamptz), + OPERATOR 0x22 <= (date, timestamptz), + OPERATOR 0x23 = (date, timestamptz), + OPERATOR 0x24 >= (date, timestamptz), + OPERATOR 0x25 > (date, timestamptz) +; + +ALTER OPERATOR FAMILY timestamp_ops USING gin +ADD + -- Code 1: RHS is date + OPERATOR 0x11 < (timestamp, date), + OPERATOR 0x12 <= (timestamp, date), + OPERATOR 0x13 = (timestamp, date), + OPERATOR 0x14 >= (timestamp, date), + OPERATOR 0x15 > (timestamp, date), + -- Code 2: RHS is timestamptz + OPERATOR 0x21 < (timestamp, timestamptz), + OPERATOR 0x22 <= (timestamp, timestamptz), + OPERATOR 0x23 = (timestamp, timestamptz), + OPERATOR 0x24 >= (timestamp, timestamptz), + OPERATOR 0x25 > (timestamp, timestamptz) +; + +ALTER OPERATOR FAMILY timestamptz_ops USING gin +ADD + -- Code 1: RHS is date + OPERATOR 0x11 < (timestamptz, date), + OPERATOR 0x12 <= (timestamptz, date), + OPERATOR 0x13 = (timestamptz, date), + OPERATOR 0x14 >= (timestamptz, date), + OPERATOR 0x15 > (timestamptz, date), + -- Code 2: RHS is timestamp + OPERATOR 0x21 < (timestamptz, timestamp), + OPERATOR 0x22 <= (timestamptz, timestamp), + OPERATOR 0x23 = (timestamptz, timestamp), + OPERATOR 0x24 >= (timestamptz, timestamp), + OPERATOR 0x25 > (timestamptz, timestamp) +; diff --git a/contrib/btree_gin/btree_gin.c b/contrib/btree_gin/btree_gin.c index 98663cb86117e..8dfbaa4781d96 100644 --- a/contrib/btree_gin/btree_gin.c +++ b/contrib/btree_gin/btree_gin.c @@ -6,6 +6,8 @@ #include #include "access/stratnum.h" +#include "mb/pg_wchar.h" +#include "nodes/miscnodes.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/float.h" @@ -13,20 +15,36 @@ #include "utils/numeric.h" #include "utils/timestamp.h" #include "utils/uuid.h" +#include "varatt.h" PG_MODULE_MAGIC_EXT( .name = "btree_gin", .version = PG_VERSION ); +/* + * Our opclasses use the same strategy numbers as btree (1-5) for same-type + * comparison operators. For cross-type comparison operators, the + * low 4 bits of our strategy numbers are the btree strategy number, + * and the upper bits are a code for the right-hand-side data type. + */ +#define BTGIN_GET_BTREE_STRATEGY(strat) ((strat) & 0x0F) +#define BTGIN_GET_RHS_TYPE_CODE(strat) ((strat) >> 4) + +/* extra data passed from gin_btree_extract_query to gin_btree_compare_prefix */ typedef struct QueryInfo { - StrategyNumber strategy; - Datum datum; - bool is_varlena; - Datum (*typecmp) (FunctionCallInfo); + StrategyNumber strategy; /* operator strategy number */ + Datum orig_datum; /* original query (comparison) datum */ + Datum entry_datum; /* datum we reported as the entry value */ + PGFunction typecmp; /* appropriate btree comparison function */ } QueryInfo; +typedef Datum (*btree_gin_convert_function) (Datum input); + +typedef Datum (*btree_gin_leftmost_function) (void); + + /*** GIN support functions shared by all datatypes ***/ static Datum @@ -34,8 +52,9 @@ gin_btree_extract_value(FunctionCallInfo fcinfo, bool is_varlena) { Datum datum = PG_GETARG_DATUM(0); int32 *nentries = (int32 *) PG_GETARG_POINTER(1); - Datum *entries = (Datum *) palloc(sizeof(Datum)); + Datum *entries = palloc_object(Datum); + /* Ensure that values stored in the index are not toasted */ if (is_varlena) datum = PointerGetDatum(PG_DETOAST_DATUM(datum)); entries[0] = datum; @@ -44,42 +63,54 @@ gin_btree_extract_value(FunctionCallInfo fcinfo, bool is_varlena) PG_RETURN_POINTER(entries); } -/* - * For BTGreaterEqualStrategyNumber, BTGreaterStrategyNumber, and - * BTEqualStrategyNumber we want to start the index scan at the - * supplied query datum, and work forward. For BTLessStrategyNumber - * and BTLessEqualStrategyNumber, we need to start at the leftmost - * key, and work forward until the supplied query datum (which must be - * sent along inside the QueryInfo structure). - */ static Datum gin_btree_extract_query(FunctionCallInfo fcinfo, - bool is_varlena, - Datum (*leftmostvalue) (void), - Datum (*typecmp) (FunctionCallInfo)) + btree_gin_leftmost_function leftmostvalue, + const bool *rhs_is_varlena, + const btree_gin_convert_function *cvt_fns, + const PGFunction *cmp_fns) { Datum datum = PG_GETARG_DATUM(0); int32 *nentries = (int32 *) PG_GETARG_POINTER(1); StrategyNumber strategy = PG_GETARG_UINT16(2); bool **partialmatch = (bool **) PG_GETARG_POINTER(3); Pointer **extra_data = (Pointer **) PG_GETARG_POINTER(4); - Datum *entries = (Datum *) palloc(sizeof(Datum)); - QueryInfo *data = (QueryInfo *) palloc(sizeof(QueryInfo)); - bool *ptr_partialmatch; + Datum *entries = palloc_object(Datum); + QueryInfo *data = palloc_object(QueryInfo); + bool *ptr_partialmatch = palloc_object(bool); + int btree_strat, + rhs_code; + + /* + * Extract the btree strategy code and the RHS data type code from the + * given strategy number. + */ + btree_strat = BTGIN_GET_BTREE_STRATEGY(strategy); + rhs_code = BTGIN_GET_RHS_TYPE_CODE(strategy); + /* + * Detoast the comparison datum. This isn't necessary for correctness, + * but it can save repeat detoastings within the comparison function. + */ + if (rhs_is_varlena[rhs_code]) + datum = PointerGetDatum(PG_DETOAST_DATUM(datum)); + + /* Prep single comparison key with possible partial-match flag */ *nentries = 1; - ptr_partialmatch = *partialmatch = (bool *) palloc(sizeof(bool)); + *partialmatch = ptr_partialmatch; *ptr_partialmatch = false; - if (is_varlena) - datum = PointerGetDatum(PG_DETOAST_DATUM(datum)); - data->strategy = strategy; - data->datum = datum; - data->is_varlena = is_varlena; - data->typecmp = typecmp; - *extra_data = (Pointer *) palloc(sizeof(Pointer)); - **extra_data = (Pointer) data; - switch (strategy) + /* + * For BTGreaterEqualStrategyNumber, BTGreaterStrategyNumber, and + * BTEqualStrategyNumber we want to start the index scan at the supplied + * query datum, and work forward. For BTLessStrategyNumber and + * BTLessEqualStrategyNumber, we need to start at the leftmost key, and + * work forward until the supplied query datum (which we'll send along + * inside the QueryInfo structure). Use partial match rules except for + * BTEqualStrategyNumber without a conversion function. (If there is a + * conversion function, comparison to the entry value is not trustworthy.) + */ + switch (btree_strat) { case BTLessStrategyNumber: case BTLessEqualStrategyNumber: @@ -89,77 +120,108 @@ gin_btree_extract_query(FunctionCallInfo fcinfo, case BTGreaterEqualStrategyNumber: case BTGreaterStrategyNumber: *ptr_partialmatch = true; - /* FALLTHROUGH */ + pg_fallthrough; case BTEqualStrategyNumber: - entries[0] = datum; + /* If we have a conversion function, apply it */ + if (cvt_fns && cvt_fns[rhs_code]) + { + entries[0] = (*cvt_fns[rhs_code]) (datum); + *ptr_partialmatch = true; + } + else + entries[0] = datum; break; default: elog(ERROR, "unrecognized strategy number: %d", strategy); } + /* Fill "extra" data */ + data->strategy = strategy; + data->orig_datum = datum; + data->entry_datum = entries[0]; + data->typecmp = cmp_fns[rhs_code]; + *extra_data = palloc_object(Pointer); + **extra_data = (Pointer) data; + PG_RETURN_POINTER(entries); } -/* - * Datum a is a value from extract_query method and for BTLess* - * strategy it is a left-most value. So, use original datum from QueryInfo - * to decide to stop scanning or not. Datum b is always from index. - */ static Datum gin_btree_compare_prefix(FunctionCallInfo fcinfo) { - Datum a = PG_GETARG_DATUM(0); - Datum b = PG_GETARG_DATUM(1); + Datum partial_key PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(0); + Datum key = PG_GETARG_DATUM(1); QueryInfo *data = (QueryInfo *) PG_GETARG_POINTER(3); int32 res, cmp; + /* + * partial_key is only an approximation to the real comparison value, + * especially if it's a leftmost value. We can get an accurate answer by + * doing a possibly-cross-type comparison to the real comparison value. + * (Note that partial_key and key are of the indexed datatype while + * orig_datum is of the query operator's RHS datatype.) + * + * But just to be sure that things are what we expect, let's assert that + * partial_key is indeed what gin_btree_extract_query reported, so that + * we'll notice if anyone ever changes the core code in a way that breaks + * our assumptions. + */ + Assert(partial_key == data->entry_datum); + cmp = DatumGetInt32(CallerFInfoFunctionCall2(data->typecmp, fcinfo->flinfo, PG_GET_COLLATION(), - (data->strategy == BTLessStrategyNumber || - data->strategy == BTLessEqualStrategyNumber) - ? data->datum : a, - b)); + data->orig_datum, + key)); - switch (data->strategy) + /* + * Convert the comparison result to the correct thing for the search + * operator strategy. When dealing with cross-type comparisons, an + * imprecise entry datum could lead GIN to start the scan just before the + * first possible match, so we must continue the scan if the current index + * entry doesn't satisfy the search condition for >= and > cases. But if + * that happens in an = search we can stop, because an imprecise entry + * datum means that the search value is unrepresentable in the indexed + * data type, so that there will be no exact matches. + */ + switch (BTGIN_GET_BTREE_STRATEGY(data->strategy)) { case BTLessStrategyNumber: /* If original datum > indexed one then return match */ if (cmp > 0) res = 0; else - res = 1; + res = 1; /* end scan */ break; case BTLessEqualStrategyNumber: - /* The same except equality */ + /* If original datum >= indexed one then return match */ if (cmp >= 0) res = 0; else - res = 1; + res = 1; /* end scan */ break; case BTEqualStrategyNumber: - if (cmp != 0) - res = 1; - else + /* If original datum = indexed one then return match */ + /* See above about why we can end scan when cmp < 0 */ + if (cmp == 0) res = 0; + else + res = 1; /* end scan */ break; case BTGreaterEqualStrategyNumber: /* If original datum <= indexed one then return match */ if (cmp <= 0) res = 0; else - res = 1; + res = -1; /* keep scanning */ break; case BTGreaterStrategyNumber: - /* If original datum <= indexed one then return match */ - /* If original datum == indexed one then continue scan */ + /* If original datum < indexed one then return match */ if (cmp < 0) res = 0; - else if (cmp == 0) - res = -1; else - res = 1; + res = -1; /* keep scanning */ break; default: elog(ERROR, "unrecognized strategy number: %d", @@ -182,19 +244,20 @@ gin_btree_consistent(PG_FUNCTION_ARGS) /*** GIN_SUPPORT macro defines the datatype specific functions ***/ -#define GIN_SUPPORT(type, is_varlena, leftmostvalue, typecmp) \ +#define GIN_SUPPORT(type, leftmostvalue, is_varlena, cvtfns, cmpfns) \ PG_FUNCTION_INFO_V1(gin_extract_value_##type); \ Datum \ gin_extract_value_##type(PG_FUNCTION_ARGS) \ { \ - return gin_btree_extract_value(fcinfo, is_varlena); \ + return gin_btree_extract_value(fcinfo, is_varlena[0]); \ } \ PG_FUNCTION_INFO_V1(gin_extract_query_##type); \ Datum \ gin_extract_query_##type(PG_FUNCTION_ARGS) \ { \ return gin_btree_extract_query(fcinfo, \ - is_varlena, leftmostvalue, typecmp); \ + leftmostvalue, is_varlena, \ + cvtfns, cmpfns); \ } \ PG_FUNCTION_INFO_V1(gin_compare_prefix_##type); \ Datum \ @@ -206,13 +269,66 @@ gin_compare_prefix_##type(PG_FUNCTION_ARGS) \ /*** Datatype specifications ***/ +/* Function to produce the least possible value of the indexed datatype */ static Datum leftmostvalue_int2(void) { return Int16GetDatum(SHRT_MIN); } -GIN_SUPPORT(int2, false, leftmostvalue_int2, btint2cmp) +/* + * For cross-type support, we must provide conversion functions that produce + * a Datum of the indexed datatype, since GIN requires the "entry" datums to + * be of that type. If an exact conversion is not possible, produce a value + * that will lead GIN to find the first index entry that is greater than + * or equal to the actual comparison value. (But rounding down is OK, so + * sometimes we might find an index entry that's just less than the + * comparison value.) + * + * For integer values, it's sufficient to clamp the input to be in-range. + * + * Note: for out-of-range input values, we could in theory detect that the + * search condition matches all or none of the index, and avoid a useless + * index descent in the latter case. Such searches are probably rare though, + * so we don't contort this code enough to do that. + */ +static Datum +cvt_int4_int2(Datum input) +{ + int32 val = DatumGetInt32(input); + + val = Max(val, SHRT_MIN); + val = Min(val, SHRT_MAX); + return Int16GetDatum((int16) val); +} + +static Datum +cvt_int8_int2(Datum input) +{ + int64 val = DatumGetInt64(input); + + val = Max(val, SHRT_MIN); + val = Min(val, SHRT_MAX); + return Int16GetDatum((int16) val); +} + +/* + * RHS-type-is-varlena flags, conversion and comparison function arrays, + * indexed by high bits of the operator strategy number. A NULL in the + * conversion function array indicates that no conversion is needed, which + * will always be the case for the zero'th entry. Note that the cross-type + * comparison functions should be the ones with the indexed datatype second. + */ +static const bool int2_rhs_is_varlena[] = +{false, false, false}; + +static const btree_gin_convert_function int2_cvt_fns[] = +{NULL, cvt_int4_int2, cvt_int8_int2}; + +static const PGFunction int2_cmp_fns[] = +{btint2cmp, btint42cmp, btint82cmp}; + +GIN_SUPPORT(int2, leftmostvalue_int2, int2_rhs_is_varlena, int2_cvt_fns, int2_cmp_fns) static Datum leftmostvalue_int4(void) @@ -220,7 +336,34 @@ leftmostvalue_int4(void) return Int32GetDatum(INT_MIN); } -GIN_SUPPORT(int4, false, leftmostvalue_int4, btint4cmp) +static Datum +cvt_int2_int4(Datum input) +{ + int16 val = DatumGetInt16(input); + + return Int32GetDatum((int32) val); +} + +static Datum +cvt_int8_int4(Datum input) +{ + int64 val = DatumGetInt64(input); + + val = Max(val, INT_MIN); + val = Min(val, INT_MAX); + return Int32GetDatum((int32) val); +} + +static const bool int4_rhs_is_varlena[] = +{false, false, false}; + +static const btree_gin_convert_function int4_cvt_fns[] = +{NULL, cvt_int2_int4, cvt_int8_int4}; + +static const PGFunction int4_cmp_fns[] = +{btint4cmp, btint24cmp, btint84cmp}; + +GIN_SUPPORT(int4, leftmostvalue_int4, int4_rhs_is_varlena, int4_cvt_fns, int4_cmp_fns) static Datum leftmostvalue_int8(void) @@ -228,7 +371,32 @@ leftmostvalue_int8(void) return Int64GetDatum(PG_INT64_MIN); } -GIN_SUPPORT(int8, false, leftmostvalue_int8, btint8cmp) +static Datum +cvt_int2_int8(Datum input) +{ + int16 val = DatumGetInt16(input); + + return Int64GetDatum((int64) val); +} + +static Datum +cvt_int4_int8(Datum input) +{ + int32 val = DatumGetInt32(input); + + return Int64GetDatum((int64) val); +} + +static const bool int8_rhs_is_varlena[] = +{false, false, false}; + +static const btree_gin_convert_function int8_cvt_fns[] = +{NULL, cvt_int2_int8, cvt_int4_int8}; + +static const PGFunction int8_cmp_fns[] = +{btint8cmp, btint28cmp, btint48cmp}; + +GIN_SUPPORT(int8, leftmostvalue_int8, int8_rhs_is_varlena, int8_cvt_fns, int8_cmp_fns) static Datum leftmostvalue_float4(void) @@ -236,7 +404,34 @@ leftmostvalue_float4(void) return Float4GetDatum(-get_float4_infinity()); } -GIN_SUPPORT(float4, false, leftmostvalue_float4, btfloat4cmp) +static Datum +cvt_float8_float4(Datum input) +{ + float8 val = DatumGetFloat8(input); + float4 result; + + /* + * Assume that ordinary C conversion will produce a usable result. + * (Compare dtof(), which raises error conditions that we don't need.) + * Note that for inputs that aren't exactly representable as float4, it + * doesn't matter whether the conversion rounds up or down. That might + * cause us to scan a few index entries that we'll reject as not matching, + * but we won't miss any that should match. + */ + result = (float4) val; + return Float4GetDatum(result); +} + +static const bool float4_rhs_is_varlena[] = +{false, false}; + +static const btree_gin_convert_function float4_cvt_fns[] = +{NULL, cvt_float8_float4}; + +static const PGFunction float4_cmp_fns[] = +{btfloat4cmp, btfloat84cmp}; + +GIN_SUPPORT(float4, leftmostvalue_float4, float4_rhs_is_varlena, float4_cvt_fns, float4_cmp_fns) static Datum leftmostvalue_float8(void) @@ -244,7 +439,24 @@ leftmostvalue_float8(void) return Float8GetDatum(-get_float8_infinity()); } -GIN_SUPPORT(float8, false, leftmostvalue_float8, btfloat8cmp) +static Datum +cvt_float4_float8(Datum input) +{ + float4 val = DatumGetFloat4(input); + + return Float8GetDatum((float8) val); +} + +static const bool float8_rhs_is_varlena[] = +{false, false}; + +static const btree_gin_convert_function float8_cvt_fns[] = +{NULL, cvt_float4_float8}; + +static const PGFunction float8_cmp_fns[] = +{btfloat8cmp, btfloat48cmp}; + +GIN_SUPPORT(float8, leftmostvalue_float8, float8_rhs_is_varlena, float8_cvt_fns, float8_cmp_fns) static Datum leftmostvalue_money(void) @@ -252,7 +464,13 @@ leftmostvalue_money(void) return Int64GetDatum(PG_INT64_MIN); } -GIN_SUPPORT(money, false, leftmostvalue_money, cash_cmp) +static const bool money_rhs_is_varlena[] = +{false}; + +static const PGFunction money_cmp_fns[] = +{cash_cmp}; + +GIN_SUPPORT(money, leftmostvalue_money, money_rhs_is_varlena, NULL, money_cmp_fns) static Datum leftmostvalue_oid(void) @@ -260,7 +478,13 @@ leftmostvalue_oid(void) return ObjectIdGetDatum(0); } -GIN_SUPPORT(oid, false, leftmostvalue_oid, btoidcmp) +static const bool oid_rhs_is_varlena[] = +{false}; + +static const PGFunction oid_cmp_fns[] = +{btoidcmp}; + +GIN_SUPPORT(oid, leftmostvalue_oid, oid_rhs_is_varlena, NULL, oid_cmp_fns) static Datum leftmostvalue_timestamp(void) @@ -268,9 +492,75 @@ leftmostvalue_timestamp(void) return TimestampGetDatum(DT_NOBEGIN); } -GIN_SUPPORT(timestamp, false, leftmostvalue_timestamp, timestamp_cmp) +static Datum +cvt_date_timestamp(Datum input) +{ + DateADT val = DatumGetDateADT(input); + Timestamp result; + ErrorSaveContext escontext = {T_ErrorSaveContext}; -GIN_SUPPORT(timestamptz, false, leftmostvalue_timestamp, timestamp_cmp) + result = date2timestamp_safe(val, (Node *) &escontext); + /* We can ignore errors, since result is useful as-is */ + return TimestampGetDatum(result); +} + +static Datum +cvt_timestamptz_timestamp(Datum input) +{ + TimestampTz val = DatumGetTimestampTz(input); + ErrorSaveContext escontext = {T_ErrorSaveContext}; + Timestamp result; + + result = timestamptz2timestamp_safe(val, (Node *) &escontext); + /* We can ignore errors, since result is useful as-is */ + return TimestampGetDatum(result); +} + +static const bool timestamp_rhs_is_varlena[] = +{false, false, false}; + +static const btree_gin_convert_function timestamp_cvt_fns[] = +{NULL, cvt_date_timestamp, cvt_timestamptz_timestamp}; + +static const PGFunction timestamp_cmp_fns[] = +{timestamp_cmp, date_cmp_timestamp, timestamptz_cmp_timestamp}; + +GIN_SUPPORT(timestamp, leftmostvalue_timestamp, timestamp_rhs_is_varlena, timestamp_cvt_fns, timestamp_cmp_fns) + +static Datum +cvt_date_timestamptz(Datum input) +{ + DateADT val = DatumGetDateADT(input); + ErrorSaveContext escontext = {T_ErrorSaveContext}; + TimestampTz result; + + result = date2timestamptz_safe(val, (Node *) &escontext); + /* We can ignore errors, since result is useful as-is */ + return TimestampTzGetDatum(result); +} + +static Datum +cvt_timestamp_timestamptz(Datum input) +{ + Timestamp val = DatumGetTimestamp(input); + ErrorSaveContext escontext = {T_ErrorSaveContext}; + TimestampTz result; + + result = timestamp2timestamptz_safe(val, (Node *) &escontext); + /* We can ignore errors, since result is useful as-is */ + return TimestampTzGetDatum(result); +} + +static const bool timestamptz_rhs_is_varlena[] = +{false, false, false}; + +static const btree_gin_convert_function timestamptz_cvt_fns[] = +{NULL, cvt_date_timestamptz, cvt_timestamp_timestamptz}; + +static const PGFunction timestamptz_cmp_fns[] = +{timestamp_cmp, date_cmp_timestamptz, timestamp_cmp_timestamptz}; + +GIN_SUPPORT(timestamptz, leftmostvalue_timestamp, timestamptz_rhs_is_varlena, timestamptz_cvt_fns, timestamptz_cmp_fns) static Datum leftmostvalue_time(void) @@ -278,12 +568,18 @@ leftmostvalue_time(void) return TimeADTGetDatum(0); } -GIN_SUPPORT(time, false, leftmostvalue_time, time_cmp) +static const bool time_rhs_is_varlena[] = +{false}; + +static const PGFunction time_cmp_fns[] = +{time_cmp}; + +GIN_SUPPORT(time, leftmostvalue_time, time_rhs_is_varlena, NULL, time_cmp_fns) static Datum leftmostvalue_timetz(void) { - TimeTzADT *v = palloc(sizeof(TimeTzADT)); + TimeTzADT *v = palloc_object(TimeTzADT); v->time = 0; v->zone = -24 * 3600; /* XXX is that true? */ @@ -291,7 +587,13 @@ leftmostvalue_timetz(void) return TimeTzADTPGetDatum(v); } -GIN_SUPPORT(timetz, false, leftmostvalue_timetz, timetz_cmp) +static const bool timetz_rhs_is_varlena[] = +{false}; + +static const PGFunction timetz_cmp_fns[] = +{timetz_cmp}; + +GIN_SUPPORT(timetz, leftmostvalue_timetz, timetz_rhs_is_varlena, NULL, timetz_cmp_fns) static Datum leftmostvalue_date(void) @@ -299,39 +601,90 @@ leftmostvalue_date(void) return DateADTGetDatum(DATEVAL_NOBEGIN); } -GIN_SUPPORT(date, false, leftmostvalue_date, date_cmp) +static Datum +cvt_timestamp_date(Datum input) +{ + Timestamp val = DatumGetTimestamp(input); + ErrorSaveContext escontext = {T_ErrorSaveContext}; + DateADT result; + + result = timestamp2date_safe(val, (Node *) &escontext); + /* We can ignore errors, since result is useful as-is */ + return DateADTGetDatum(result); +} + +static Datum +cvt_timestamptz_date(Datum input) +{ + TimestampTz val = DatumGetTimestampTz(input); + ErrorSaveContext escontext = {T_ErrorSaveContext}; + DateADT result; + + result = timestamptz2date_safe(val, (Node *) &escontext); + /* We can ignore errors, since result is useful as-is */ + return DateADTGetDatum(result); +} + +static const bool date_rhs_is_varlena[] = +{false, false, false}; + +static const btree_gin_convert_function date_cvt_fns[] = +{NULL, cvt_timestamp_date, cvt_timestamptz_date}; + +static const PGFunction date_cmp_fns[] = +{date_cmp, timestamp_cmp_date, timestamptz_cmp_date}; + +GIN_SUPPORT(date, leftmostvalue_date, date_rhs_is_varlena, date_cvt_fns, date_cmp_fns) static Datum leftmostvalue_interval(void) { - Interval *v = palloc(sizeof(Interval)); + Interval *v = palloc_object(Interval); INTERVAL_NOBEGIN(v); return IntervalPGetDatum(v); } -GIN_SUPPORT(interval, false, leftmostvalue_interval, interval_cmp) +static const bool interval_rhs_is_varlena[] = +{false}; + +static const PGFunction interval_cmp_fns[] = +{interval_cmp}; + +GIN_SUPPORT(interval, leftmostvalue_interval, interval_rhs_is_varlena, NULL, interval_cmp_fns) static Datum leftmostvalue_macaddr(void) { - macaddr *v = palloc0(sizeof(macaddr)); + macaddr *v = palloc0_object(macaddr); return MacaddrPGetDatum(v); } -GIN_SUPPORT(macaddr, false, leftmostvalue_macaddr, macaddr_cmp) +static const bool macaddr_rhs_is_varlena[] = +{false}; + +static const PGFunction macaddr_cmp_fns[] = +{macaddr_cmp}; + +GIN_SUPPORT(macaddr, leftmostvalue_macaddr, macaddr_rhs_is_varlena, NULL, macaddr_cmp_fns) static Datum leftmostvalue_macaddr8(void) { - macaddr8 *v = palloc0(sizeof(macaddr8)); + macaddr8 *v = palloc0_object(macaddr8); return Macaddr8PGetDatum(v); } -GIN_SUPPORT(macaddr8, false, leftmostvalue_macaddr8, macaddr8_cmp) +static const bool macaddr8_rhs_is_varlena[] = +{false}; + +static const PGFunction macaddr8_cmp_fns[] = +{macaddr8_cmp}; + +GIN_SUPPORT(macaddr8, leftmostvalue_macaddr8, macaddr8_rhs_is_varlena, NULL, macaddr8_cmp_fns) static Datum leftmostvalue_inet(void) @@ -339,9 +692,21 @@ leftmostvalue_inet(void) return DirectFunctionCall1(inet_in, CStringGetDatum("0.0.0.0/0")); } -GIN_SUPPORT(inet, true, leftmostvalue_inet, network_cmp) +static const bool inet_rhs_is_varlena[] = +{true}; + +static const PGFunction inet_cmp_fns[] = +{network_cmp}; + +GIN_SUPPORT(inet, leftmostvalue_inet, inet_rhs_is_varlena, NULL, inet_cmp_fns) -GIN_SUPPORT(cidr, true, leftmostvalue_inet, network_cmp) +static const bool cidr_rhs_is_varlena[] = +{true}; + +static const PGFunction cidr_cmp_fns[] = +{network_cmp}; + +GIN_SUPPORT(cidr, leftmostvalue_inet, cidr_rhs_is_varlena, NULL, cidr_cmp_fns) static Datum leftmostvalue_text(void) @@ -349,9 +714,32 @@ leftmostvalue_text(void) return PointerGetDatum(cstring_to_text_with_len("", 0)); } -GIN_SUPPORT(text, true, leftmostvalue_text, bttextcmp) +static Datum +cvt_name_text(Datum input) +{ + Name val = DatumGetName(input); + + return PointerGetDatum(cstring_to_text(NameStr(*val))); +} + +static const bool text_rhs_is_varlena[] = +{true, false}; + +static const btree_gin_convert_function text_cvt_fns[] = +{NULL, cvt_name_text}; + +static const PGFunction text_cmp_fns[] = +{bttextcmp, btnametextcmp}; + +GIN_SUPPORT(text, leftmostvalue_text, text_rhs_is_varlena, text_cvt_fns, text_cmp_fns) + +static const bool bpchar_rhs_is_varlena[] = +{true}; + +static const PGFunction bpchar_cmp_fns[] = +{bpcharcmp}; -GIN_SUPPORT(bpchar, true, leftmostvalue_text, bpcharcmp) +GIN_SUPPORT(bpchar, leftmostvalue_text, bpchar_rhs_is_varlena, NULL, bpchar_cmp_fns) static Datum leftmostvalue_char(void) @@ -359,9 +747,21 @@ leftmostvalue_char(void) return CharGetDatum(0); } -GIN_SUPPORT(char, false, leftmostvalue_char, btcharcmp) +static const bool char_rhs_is_varlena[] = +{false}; -GIN_SUPPORT(bytea, true, leftmostvalue_text, byteacmp) +static const PGFunction char_cmp_fns[] = +{btcharcmp}; + +GIN_SUPPORT(char, leftmostvalue_char, char_rhs_is_varlena, NULL, char_cmp_fns) + +static const bool bytea_rhs_is_varlena[] = +{true}; + +static const PGFunction bytea_cmp_fns[] = +{byteacmp}; + +GIN_SUPPORT(bytea, leftmostvalue_text, bytea_rhs_is_varlena, NULL, bytea_cmp_fns) static Datum leftmostvalue_bit(void) @@ -372,7 +772,13 @@ leftmostvalue_bit(void) Int32GetDatum(-1)); } -GIN_SUPPORT(bit, true, leftmostvalue_bit, bitcmp) +static const bool bit_rhs_is_varlena[] = +{true}; + +static const PGFunction bit_cmp_fns[] = +{bitcmp}; + +GIN_SUPPORT(bit, leftmostvalue_bit, bit_rhs_is_varlena, NULL, bit_cmp_fns) static Datum leftmostvalue_varbit(void) @@ -383,7 +789,13 @@ leftmostvalue_varbit(void) Int32GetDatum(-1)); } -GIN_SUPPORT(varbit, true, leftmostvalue_varbit, bitcmp) +static const bool varbit_rhs_is_varlena[] = +{true}; + +static const PGFunction varbit_cmp_fns[] = +{bitcmp}; + +GIN_SUPPORT(varbit, leftmostvalue_varbit, varbit_rhs_is_varlena, NULL, varbit_cmp_fns) /* * Numeric type hasn't a real left-most value, so we use PointerGetDatum(NULL) @@ -428,7 +840,13 @@ leftmostvalue_numeric(void) return PointerGetDatum(NULL); } -GIN_SUPPORT(numeric, true, leftmostvalue_numeric, gin_numeric_cmp) +static const bool numeric_rhs_is_varlena[] = +{true}; + +static const PGFunction numeric_cmp_fns[] = +{gin_numeric_cmp}; + +GIN_SUPPORT(numeric, leftmostvalue_numeric, numeric_rhs_is_varlena, NULL, numeric_cmp_fns) /* * Use a similar trick to that used for numeric for enums, since we don't @@ -477,7 +895,13 @@ leftmostvalue_enum(void) return ObjectIdGetDatum(InvalidOid); } -GIN_SUPPORT(anyenum, false, leftmostvalue_enum, gin_enum_cmp) +static const bool enum_rhs_is_varlena[] = +{false}; + +static const PGFunction enum_cmp_fns[] = +{gin_enum_cmp}; + +GIN_SUPPORT(anyenum, leftmostvalue_enum, enum_rhs_is_varlena, NULL, enum_cmp_fns) static Datum leftmostvalue_uuid(void) @@ -486,12 +910,18 @@ leftmostvalue_uuid(void) * palloc0 will create the UUID with all zeroes: * "00000000-0000-0000-0000-000000000000" */ - pg_uuid_t *retval = (pg_uuid_t *) palloc0(sizeof(pg_uuid_t)); + pg_uuid_t *retval = palloc0_object(pg_uuid_t); return UUIDPGetDatum(retval); } -GIN_SUPPORT(uuid, false, leftmostvalue_uuid, uuid_cmp) +static const bool uuid_rhs_is_varlena[] = +{false}; + +static const PGFunction uuid_cmp_fns[] = +{uuid_cmp}; + +GIN_SUPPORT(uuid, leftmostvalue_uuid, uuid_rhs_is_varlena, NULL, uuid_cmp_fns) static Datum leftmostvalue_name(void) @@ -501,7 +931,37 @@ leftmostvalue_name(void) return NameGetDatum(result); } -GIN_SUPPORT(name, false, leftmostvalue_name, btnamecmp) +static Datum +cvt_text_name(Datum input) +{ + text *val = DatumGetTextPP(input); + NameData *result = (NameData *) palloc0(NAMEDATALEN); + int len = VARSIZE_ANY_EXHDR(val); + + /* + * Truncate oversize input. We're assuming this will produce a result + * considered less than the original. That could be a bad assumption in + * some collations, but fortunately an index on "name" is generally going + * to use C collation. + */ + if (len >= NAMEDATALEN) + len = pg_mbcliplen(VARDATA_ANY(val), len, NAMEDATALEN - 1); + + memcpy(NameStr(*result), VARDATA_ANY(val), len); + + return NameGetDatum(result); +} + +static const bool name_rhs_is_varlena[] = +{false, true}; + +static const btree_gin_convert_function name_cvt_fns[] = +{NULL, cvt_text_name}; + +static const PGFunction name_cmp_fns[] = +{btnamecmp, bttextnamecmp}; + +GIN_SUPPORT(name, leftmostvalue_name, name_rhs_is_varlena, name_cvt_fns, name_cmp_fns) static Datum leftmostvalue_bool(void) @@ -509,4 +969,10 @@ leftmostvalue_bool(void) return BoolGetDatum(false); } -GIN_SUPPORT(bool, false, leftmostvalue_bool, btboolcmp) +static const bool bool_rhs_is_varlena[] = +{false}; + +static const PGFunction bool_cmp_fns[] = +{btboolcmp}; + +GIN_SUPPORT(bool, leftmostvalue_bool, bool_rhs_is_varlena, NULL, bool_cmp_fns) diff --git a/contrib/btree_gin/btree_gin.control b/contrib/btree_gin/btree_gin.control index 67d0c997d8d26..0c77c81727117 100644 --- a/contrib/btree_gin/btree_gin.control +++ b/contrib/btree_gin/btree_gin.control @@ -1,6 +1,6 @@ # btree_gin extension comment = 'support for indexing common datatypes in GIN' -default_version = '1.3' +default_version = '1.4' module_pathname = '$libdir/btree_gin' relocatable = true trusted = true diff --git a/contrib/btree_gin/expected/date.out b/contrib/btree_gin/expected/date.out index 40dfa308cf753..e69c1da2000f2 100644 --- a/contrib/btree_gin/expected/date.out +++ b/contrib/btree_gin/expected/date.out @@ -49,3 +49,365 @@ SELECT * FROM test_date WHERE i>'2004-10-26'::date ORDER BY i; 10-28-2004 (2 rows) +explain (costs off) +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamp ORDER BY i; + QUERY PLAN +----------------------------------------------------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_date + Recheck Cond: (i < 'Tue Oct 26 00:00:00 2004'::timestamp without time zone) + -> Bitmap Index Scan on idx_date + Index Cond: (i < 'Tue Oct 26 00:00:00 2004'::timestamp without time zone) +(6 rows) + +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamp ORDER BY i; + i +------------ + 10-23-2004 + 10-24-2004 + 10-25-2004 +(3 rows) + +SELECT * FROM test_date WHERE i<='2004-10-26'::timestamp ORDER BY i; + i +------------ + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 +(4 rows) + +SELECT * FROM test_date WHERE i='2004-10-26'::timestamp ORDER BY i; + i +------------ + 10-26-2004 +(1 row) + +SELECT * FROM test_date WHERE i>='2004-10-26'::timestamp ORDER BY i; + i +------------ + 10-26-2004 + 10-27-2004 + 10-28-2004 +(3 rows) + +SELECT * FROM test_date WHERE i>'2004-10-26'::timestamp ORDER BY i; + i +------------ + 10-27-2004 + 10-28-2004 +(2 rows) + +explain (costs off) +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamptz ORDER BY i; + QUERY PLAN +------------------------------------------------------------------------------------------ + Sort + Sort Key: i + -> Bitmap Heap Scan on test_date + Recheck Cond: (i < 'Tue Oct 26 00:00:00 2004 PDT'::timestamp with time zone) + -> Bitmap Index Scan on idx_date + Index Cond: (i < 'Tue Oct 26 00:00:00 2004 PDT'::timestamp with time zone) +(6 rows) + +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamptz ORDER BY i; + i +------------ + 10-23-2004 + 10-24-2004 + 10-25-2004 +(3 rows) + +SELECT * FROM test_date WHERE i<='2004-10-26'::timestamptz ORDER BY i; + i +------------ + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 +(4 rows) + +SELECT * FROM test_date WHERE i='2004-10-26'::timestamptz ORDER BY i; + i +------------ + 10-26-2004 +(1 row) + +SELECT * FROM test_date WHERE i>='2004-10-26'::timestamptz ORDER BY i; + i +------------ + 10-26-2004 + 10-27-2004 + 10-28-2004 +(3 rows) + +SELECT * FROM test_date WHERE i>'2004-10-26'::timestamptz ORDER BY i; + i +------------ + 10-27-2004 + 10-28-2004 +(2 rows) + +-- Check endpoint and out-of-range cases +INSERT INTO test_date VALUES ('-infinity'), ('infinity'); +SELECT gin_clean_pending_list('idx_date'); + gin_clean_pending_list +------------------------ + 1 +(1 row) + +SELECT * FROM test_date WHERE i<'-infinity'::timestamp ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_date WHERE i<='-infinity'::timestamp ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_date WHERE i='-infinity'::timestamp ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_date WHERE i>='-infinity'::timestamp ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(8 rows) + +SELECT * FROM test_date WHERE i>'-infinity'::timestamp ORDER BY i; + i +------------ + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(7 rows) + +SELECT * FROM test_date WHERE i<'infinity'::timestamp ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 +(7 rows) + +SELECT * FROM test_date WHERE i<='infinity'::timestamp ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(8 rows) + +SELECT * FROM test_date WHERE i='infinity'::timestamp ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_date WHERE i>='infinity'::timestamp ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_date WHERE i>'infinity'::timestamp ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_date WHERE i<'-infinity'::timestamptz ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_date WHERE i<='-infinity'::timestamptz ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_date WHERE i='-infinity'::timestamptz ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_date WHERE i>='-infinity'::timestamptz ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(8 rows) + +SELECT * FROM test_date WHERE i>'-infinity'::timestamptz ORDER BY i; + i +------------ + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(7 rows) + +SELECT * FROM test_date WHERE i<'infinity'::timestamptz ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 +(7 rows) + +SELECT * FROM test_date WHERE i<='infinity'::timestamptz ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(8 rows) + +SELECT * FROM test_date WHERE i='infinity'::timestamptz ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_date WHERE i>='infinity'::timestamptz ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_date WHERE i>'infinity'::timestamptz ORDER BY i; + i +--- +(0 rows) + +-- Check rounding cases +-- '2004-10-25 00:00:01' rounds to '2004-10-25' for date. +-- '2004-10-25 23:59:59' also rounds to '2004-10-25', +-- so it's the same case as '2004-10-25 00:00:01' +SELECT * FROM test_date WHERE i < '2004-10-25 00:00:01'::timestamp ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 +(4 rows) + +SELECT * FROM test_date WHERE i <= '2004-10-25 00:00:01'::timestamp ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 +(4 rows) + +SELECT * FROM test_date WHERE i = '2004-10-25 00:00:01'::timestamp ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_date WHERE i > '2004-10-25 00:00:01'::timestamp ORDER BY i; + i +------------ + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(4 rows) + +SELECT * FROM test_date WHERE i >= '2004-10-25 00:00:01'::timestamp ORDER BY i; + i +------------ + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(4 rows) + +SELECT * FROM test_date WHERE i < '2004-10-25 00:00:01'::timestamptz ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 +(4 rows) + +SELECT * FROM test_date WHERE i <= '2004-10-25 00:00:01'::timestamptz ORDER BY i; + i +------------ + -infinity + 10-23-2004 + 10-24-2004 + 10-25-2004 +(4 rows) + +SELECT * FROM test_date WHERE i = '2004-10-25 00:00:01'::timestamptz ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_date WHERE i > '2004-10-25 00:00:01'::timestamptz ORDER BY i; + i +------------ + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(4 rows) + +SELECT * FROM test_date WHERE i >= '2004-10-25 00:00:01'::timestamptz ORDER BY i; + i +------------ + 10-26-2004 + 10-27-2004 + 10-28-2004 + infinity +(4 rows) + diff --git a/contrib/btree_gin/expected/float4.out b/contrib/btree_gin/expected/float4.out index 7b9134fcd4bdc..c8bb04e59be9b 100644 --- a/contrib/btree_gin/expected/float4.out +++ b/contrib/btree_gin/expected/float4.out @@ -42,3 +42,324 @@ SELECT * FROM test_float4 WHERE i>1::float4 ORDER BY i; 3 (2 rows) +explain (costs off) +SELECT * FROM test_float4 WHERE i<1::float8 ORDER BY i; + QUERY PLAN +------------------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_float4 + Recheck Cond: (i < '1'::double precision) + -> Bitmap Index Scan on idx_float4 + Index Cond: (i < '1'::double precision) +(6 rows) + +SELECT * FROM test_float4 WHERE i<1::float8 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_float4 WHERE i<=1::float8 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_float4 WHERE i=1::float8 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_float4 WHERE i>=1::float8 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_float4 WHERE i>1::float8 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + +-- Check endpoint and out-of-range cases +INSERT INTO test_float4 VALUES ('NaN'), ('Inf'), ('-Inf'); +SELECT gin_clean_pending_list('idx_float4'); + gin_clean_pending_list +------------------------ + 1 +(1 row) + +SELECT * FROM test_float4 WHERE i<'-Inf'::float8 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_float4 WHERE i<='-Inf'::float8 ORDER BY i; + i +----------- + -Infinity +(1 row) + +SELECT * FROM test_float4 WHERE i='-Inf'::float8 ORDER BY i; + i +----------- + -Infinity +(1 row) + +SELECT * FROM test_float4 WHERE i>='-Inf'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 + Infinity + NaN +(9 rows) + +SELECT * FROM test_float4 WHERE i>'-Inf'::float8 ORDER BY i; + i +---------- + -2 + -1 + 0 + 1 + 2 + 3 + Infinity + NaN +(8 rows) + +SELECT * FROM test_float4 WHERE i<'Inf'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 +(7 rows) + +SELECT * FROM test_float4 WHERE i<='Inf'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 + Infinity +(8 rows) + +SELECT * FROM test_float4 WHERE i='Inf'::float8 ORDER BY i; + i +---------- + Infinity +(1 row) + +SELECT * FROM test_float4 WHERE i>='Inf'::float8 ORDER BY i; + i +---------- + Infinity + NaN +(2 rows) + +SELECT * FROM test_float4 WHERE i>'Inf'::float8 ORDER BY i; + i +----- + NaN +(1 row) + +SELECT * FROM test_float4 WHERE i<'1e300'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 +(7 rows) + +SELECT * FROM test_float4 WHERE i<='1e300'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 +(7 rows) + +SELECT * FROM test_float4 WHERE i='1e300'::float8 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_float4 WHERE i>='1e300'::float8 ORDER BY i; + i +---------- + Infinity + NaN +(2 rows) + +SELECT * FROM test_float4 WHERE i>'1e300'::float8 ORDER BY i; + i +---------- + Infinity + NaN +(2 rows) + +SELECT * FROM test_float4 WHERE i<'NaN'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 + Infinity +(8 rows) + +SELECT * FROM test_float4 WHERE i<='NaN'::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 + 1 + 2 + 3 + Infinity + NaN +(9 rows) + +SELECT * FROM test_float4 WHERE i='NaN'::float8 ORDER BY i; + i +----- + NaN +(1 row) + +SELECT * FROM test_float4 WHERE i>='NaN'::float8 ORDER BY i; + i +----- + NaN +(1 row) + +SELECT * FROM test_float4 WHERE i>'NaN'::float8 ORDER BY i; + i +--- +(0 rows) + +-- Check rounding cases +-- 1e-300 rounds to 0 for float4 but not for float8 +SELECT * FROM test_float4 WHERE i < -1e-300::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 +(3 rows) + +SELECT * FROM test_float4 WHERE i <= -1e-300::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 +(3 rows) + +SELECT * FROM test_float4 WHERE i = -1e-300::float8 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_float4 WHERE i > -1e-300::float8 ORDER BY i; + i +---------- + 0 + 1 + 2 + 3 + Infinity + NaN +(6 rows) + +SELECT * FROM test_float4 WHERE i >= -1e-300::float8 ORDER BY i; + i +---------- + 0 + 1 + 2 + 3 + Infinity + NaN +(6 rows) + +SELECT * FROM test_float4 WHERE i < 1e-300::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 +(4 rows) + +SELECT * FROM test_float4 WHERE i <= 1e-300::float8 ORDER BY i; + i +----------- + -Infinity + -2 + -1 + 0 +(4 rows) + +SELECT * FROM test_float4 WHERE i = 1e-300::float8 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_float4 WHERE i > 1e-300::float8 ORDER BY i; + i +---------- + 1 + 2 + 3 + Infinity + NaN +(5 rows) + +SELECT * FROM test_float4 WHERE i >= 1e-300::float8 ORDER BY i; + i +---------- + 1 + 2 + 3 + Infinity + NaN +(5 rows) + diff --git a/contrib/btree_gin/expected/float8.out b/contrib/btree_gin/expected/float8.out index a41d4f9f6bb05..b2877dfa3c1c2 100644 --- a/contrib/btree_gin/expected/float8.out +++ b/contrib/btree_gin/expected/float8.out @@ -42,3 +42,53 @@ SELECT * FROM test_float8 WHERE i>1::float8 ORDER BY i; 3 (2 rows) +explain (costs off) +SELECT * FROM test_float8 WHERE i<1::float4 ORDER BY i; + QUERY PLAN +--------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_float8 + Recheck Cond: (i < '1'::real) + -> Bitmap Index Scan on idx_float8 + Index Cond: (i < '1'::real) +(6 rows) + +SELECT * FROM test_float8 WHERE i<1::float4 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_float8 WHERE i<=1::float4 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_float8 WHERE i=1::float4 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_float8 WHERE i>=1::float4 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_float8 WHERE i>1::float4 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + diff --git a/contrib/btree_gin/expected/int2.out b/contrib/btree_gin/expected/int2.out index 20d66a1b05545..bcfa68f671a25 100644 --- a/contrib/btree_gin/expected/int2.out +++ b/contrib/btree_gin/expected/int2.out @@ -42,3 +42,193 @@ SELECT * FROM test_int2 WHERE i>1::int2 ORDER BY i; 3 (2 rows) +explain (costs off) +SELECT * FROM test_int2 WHERE i<1::int4 ORDER BY i; + QUERY PLAN +------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_int2 + Recheck Cond: (i < 1) + -> Bitmap Index Scan on idx_int2 + Index Cond: (i < 1) +(6 rows) + +SELECT * FROM test_int2 WHERE i<1::int4 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_int2 WHERE i<=1::int4 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_int2 WHERE i=1::int4 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_int2 WHERE i>=1::int4 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_int2 WHERE i>1::int4 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + +explain (costs off) +SELECT * FROM test_int2 WHERE i<1::int8 ORDER BY i; + QUERY PLAN +--------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_int2 + Recheck Cond: (i < '1'::bigint) + -> Bitmap Index Scan on idx_int2 + Index Cond: (i < '1'::bigint) +(6 rows) + +SELECT * FROM test_int2 WHERE i<1::int8 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_int2 WHERE i<=1::int8 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_int2 WHERE i=1::int8 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_int2 WHERE i>=1::int8 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_int2 WHERE i>1::int8 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + +-- Check endpoint and out-of-range cases +INSERT INTO test_int2 VALUES ((-32768)::int2),(32767); +SELECT gin_clean_pending_list('idx_int2'); + gin_clean_pending_list +------------------------ + 1 +(1 row) + +SELECT * FROM test_int2 WHERE i<(-32769)::int4 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_int2 WHERE i<=(-32769)::int4 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_int2 WHERE i=(-32769)::int4 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_int2 WHERE i>=(-32769)::int4 ORDER BY i; + i +-------- + -32768 + -2 + -1 + 0 + 1 + 2 + 3 + 32767 +(8 rows) + +SELECT * FROM test_int2 WHERE i>(-32769)::int4 ORDER BY i; + i +-------- + -32768 + -2 + -1 + 0 + 1 + 2 + 3 + 32767 +(8 rows) + +SELECT * FROM test_int2 WHERE i<32768::int4 ORDER BY i; + i +-------- + -32768 + -2 + -1 + 0 + 1 + 2 + 3 + 32767 +(8 rows) + +SELECT * FROM test_int2 WHERE i<=32768::int4 ORDER BY i; + i +-------- + -32768 + -2 + -1 + 0 + 1 + 2 + 3 + 32767 +(8 rows) + +SELECT * FROM test_int2 WHERE i=32768::int4 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_int2 WHERE i>=32768::int4 ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_int2 WHERE i>32768::int4 ORDER BY i; + i +--- +(0 rows) + diff --git a/contrib/btree_gin/expected/int4.out b/contrib/btree_gin/expected/int4.out index 0f0122c6f5e03..e62791e18bdc2 100644 --- a/contrib/btree_gin/expected/int4.out +++ b/contrib/btree_gin/expected/int4.out @@ -42,3 +42,103 @@ SELECT * FROM test_int4 WHERE i>1::int4 ORDER BY i; 3 (2 rows) +explain (costs off) +SELECT * FROM test_int4 WHERE i<1::int2 ORDER BY i; + QUERY PLAN +----------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_int4 + Recheck Cond: (i < '1'::smallint) + -> Bitmap Index Scan on idx_int4 + Index Cond: (i < '1'::smallint) +(6 rows) + +SELECT * FROM test_int4 WHERE i<1::int2 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_int4 WHERE i<=1::int2 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_int4 WHERE i=1::int2 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_int4 WHERE i>=1::int2 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_int4 WHERE i>1::int2 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + +explain (costs off) +SELECT * FROM test_int4 WHERE i<1::int8 ORDER BY i; + QUERY PLAN +--------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_int4 + Recheck Cond: (i < '1'::bigint) + -> Bitmap Index Scan on idx_int4 + Index Cond: (i < '1'::bigint) +(6 rows) + +SELECT * FROM test_int4 WHERE i<1::int8 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_int4 WHERE i<=1::int8 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_int4 WHERE i=1::int8 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_int4 WHERE i>=1::int8 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_int4 WHERE i>1::int8 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + diff --git a/contrib/btree_gin/expected/int8.out b/contrib/btree_gin/expected/int8.out index 307e19e7a056d..c9aceb9d357c6 100644 --- a/contrib/btree_gin/expected/int8.out +++ b/contrib/btree_gin/expected/int8.out @@ -42,3 +42,103 @@ SELECT * FROM test_int8 WHERE i>1::int8 ORDER BY i; 3 (2 rows) +explain (costs off) +SELECT * FROM test_int8 WHERE i<1::int2 ORDER BY i; + QUERY PLAN +----------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_int8 + Recheck Cond: (i < '1'::smallint) + -> Bitmap Index Scan on idx_int8 + Index Cond: (i < '1'::smallint) +(6 rows) + +SELECT * FROM test_int8 WHERE i<1::int2 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_int8 WHERE i<=1::int2 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_int8 WHERE i=1::int2 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_int8 WHERE i>=1::int2 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_int8 WHERE i>1::int2 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + +explain (costs off) +SELECT * FROM test_int8 WHERE i<1::int4 ORDER BY i; + QUERY PLAN +------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_int8 + Recheck Cond: (i < 1) + -> Bitmap Index Scan on idx_int8 + Index Cond: (i < 1) +(6 rows) + +SELECT * FROM test_int8 WHERE i<1::int4 ORDER BY i; + i +---- + -2 + -1 + 0 +(3 rows) + +SELECT * FROM test_int8 WHERE i<=1::int4 ORDER BY i; + i +---- + -2 + -1 + 0 + 1 +(4 rows) + +SELECT * FROM test_int8 WHERE i=1::int4 ORDER BY i; + i +--- + 1 +(1 row) + +SELECT * FROM test_int8 WHERE i>=1::int4 ORDER BY i; + i +--- + 1 + 2 + 3 +(3 rows) + +SELECT * FROM test_int8 WHERE i>1::int4 ORDER BY i; + i +--- + 2 + 3 +(2 rows) + diff --git a/contrib/btree_gin/expected/name.out b/contrib/btree_gin/expected/name.out index 174de6576f0f0..3a30f62519c67 100644 --- a/contrib/btree_gin/expected/name.out +++ b/contrib/btree_gin/expected/name.out @@ -95,3 +95,62 @@ EXPLAIN (COSTS OFF) SELECT * FROM test_name WHERE i>'abc' ORDER BY i; Index Cond: (i > 'abc'::name) (6 rows) +explain (costs off) +SELECT * FROM test_name WHERE i<'abc'::text ORDER BY i; + QUERY PLAN +--------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_name + Recheck Cond: (i < 'abc'::text) + -> Bitmap Index Scan on idx_name + Index Cond: (i < 'abc'::text) +(6 rows) + +SELECT * FROM test_name WHERE i<'abc'::text ORDER BY i; + i +----- + a + ab + abb +(3 rows) + +SELECT * FROM test_name WHERE i<='abc'::text ORDER BY i; + i +----- + a + ab + abb + abc +(4 rows) + +SELECT * FROM test_name WHERE i='abc'::text ORDER BY i; + i +----- + abc +(1 row) + +SELECT * FROM test_name WHERE i>='abc'::text ORDER BY i; + i +----- + abc + axy + xyz +(3 rows) + +SELECT * FROM test_name WHERE i>'abc'::text ORDER BY i; + i +----- + axy + xyz +(2 rows) + +SELECT * FROM test_name WHERE i<=repeat('abc', 100) ORDER BY i; + i +----- + a + ab + abb + abc +(4 rows) + diff --git a/contrib/btree_gin/expected/text.out b/contrib/btree_gin/expected/text.out index 3e31ad744d6aa..7f52f3db7b38e 100644 --- a/contrib/btree_gin/expected/text.out +++ b/contrib/btree_gin/expected/text.out @@ -42,3 +42,53 @@ SELECT * FROM test_text WHERE i>'abc' ORDER BY i; xyz (2 rows) +explain (costs off) +SELECT * FROM test_text WHERE i<'abc'::name COLLATE "default" ORDER BY i; + QUERY PLAN +--------------------------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_text + Recheck Cond: (i < 'abc'::name COLLATE "default") + -> Bitmap Index Scan on idx_text + Index Cond: (i < 'abc'::name COLLATE "default") +(6 rows) + +SELECT * FROM test_text WHERE i<'abc'::name COLLATE "default" ORDER BY i; + i +----- + a + ab + abb +(3 rows) + +SELECT * FROM test_text WHERE i<='abc'::name COLLATE "default" ORDER BY i; + i +----- + a + ab + abb + abc +(4 rows) + +SELECT * FROM test_text WHERE i='abc'::name COLLATE "default" ORDER BY i; + i +----- + abc +(1 row) + +SELECT * FROM test_text WHERE i>='abc'::name COLLATE "default" ORDER BY i; + i +----- + abc + axy + xyz +(3 rows) + +SELECT * FROM test_text WHERE i>'abc'::name COLLATE "default" ORDER BY i; + i +----- + axy + xyz +(2 rows) + diff --git a/contrib/btree_gin/expected/timestamp.out b/contrib/btree_gin/expected/timestamp.out index a236cdc94a9d2..b7565285e68ba 100644 --- a/contrib/btree_gin/expected/timestamp.out +++ b/contrib/btree_gin/expected/timestamp.out @@ -7,8 +7,8 @@ INSERT INTO test_timestamp VALUES ( '2004-10-26 04:55:08' ), ( '2004-10-26 05:55:08' ), ( '2004-10-26 08:55:08' ), - ( '2004-10-26 09:55:08' ), - ( '2004-10-26 10:55:08' ) + ( '2004-10-27 09:55:08' ), + ( '2004-10-27 10:55:08' ) ; CREATE INDEX idx_timestamp ON test_timestamp USING gin (i); SELECT * FROM test_timestamp WHERE i<'2004-10-26 08:55:08'::timestamp ORDER BY i; @@ -38,14 +38,308 @@ SELECT * FROM test_timestamp WHERE i>='2004-10-26 08:55:08'::timestamp ORDER BY i -------------------------- Tue Oct 26 08:55:08 2004 - Tue Oct 26 09:55:08 2004 - Tue Oct 26 10:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 (3 rows) SELECT * FROM test_timestamp WHERE i>'2004-10-26 08:55:08'::timestamp ORDER BY i; i -------------------------- - Tue Oct 26 09:55:08 2004 - Tue Oct 26 10:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 (2 rows) +explain (costs off) +SELECT * FROM test_timestamp WHERE i<'2004-10-27'::date ORDER BY i; + QUERY PLAN +---------------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_timestamp + Recheck Cond: (i < '10-27-2004'::date) + -> Bitmap Index Scan on idx_timestamp + Index Cond: (i < '10-27-2004'::date) +(6 rows) + +SELECT * FROM test_timestamp WHERE i<'2004-10-27'::date ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 +(4 rows) + +SELECT * FROM test_timestamp WHERE i<='2004-10-27'::date ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 +(4 rows) + +SELECT * FROM test_timestamp WHERE i='2004-10-27'::date ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_timestamp WHERE i>='2004-10-27'::date ORDER BY i; + i +-------------------------- + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 +(2 rows) + +SELECT * FROM test_timestamp WHERE i>'2004-10-27'::date ORDER BY i; + i +-------------------------- + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 +(2 rows) + +explain (costs off) +SELECT * FROM test_timestamp WHERE i<'2004-10-26 08:55:08'::timestamptz ORDER BY i; + QUERY PLAN +------------------------------------------------------------------------------------------ + Sort + Sort Key: i + -> Bitmap Heap Scan on test_timestamp + Recheck Cond: (i < 'Tue Oct 26 08:55:08 2004 PDT'::timestamp with time zone) + -> Bitmap Index Scan on idx_timestamp + Index Cond: (i < 'Tue Oct 26 08:55:08 2004 PDT'::timestamp with time zone) +(6 rows) + +SELECT * FROM test_timestamp WHERE i<'2004-10-26 08:55:08'::timestamptz ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 +(3 rows) + +SELECT * FROM test_timestamp WHERE i<='2004-10-26 08:55:08'::timestamptz ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 +(4 rows) + +SELECT * FROM test_timestamp WHERE i='2004-10-26 08:55:08'::timestamptz ORDER BY i; + i +-------------------------- + Tue Oct 26 08:55:08 2004 +(1 row) + +SELECT * FROM test_timestamp WHERE i>='2004-10-26 08:55:08'::timestamptz ORDER BY i; + i +-------------------------- + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 +(3 rows) + +SELECT * FROM test_timestamp WHERE i>'2004-10-26 08:55:08'::timestamptz ORDER BY i; + i +-------------------------- + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 +(2 rows) + +-- Check endpoint and out-of-range cases +INSERT INTO test_timestamp VALUES ('-infinity'), ('infinity'); +SELECT gin_clean_pending_list('idx_timestamp'); + gin_clean_pending_list +------------------------ + 1 +(1 row) + +SELECT * FROM test_timestamp WHERE i<'-infinity'::date ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_timestamp WHERE i<='-infinity'::date ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i='-infinity'::date ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>='-infinity'::date ORDER BY i; + i +-------------------------- + -infinity + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(8 rows) + +SELECT * FROM test_timestamp WHERE i>'-infinity'::date ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(7 rows) + +SELECT * FROM test_timestamp WHERE i<'infinity'::date ORDER BY i; + i +-------------------------- + -infinity + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 +(7 rows) + +SELECT * FROM test_timestamp WHERE i<='infinity'::date ORDER BY i; + i +-------------------------- + -infinity + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(8 rows) + +SELECT * FROM test_timestamp WHERE i='infinity'::date ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>='infinity'::date ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>'infinity'::date ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_timestamp WHERE i<'-infinity'::timestamptz ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_timestamp WHERE i<='-infinity'::timestamptz ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i='-infinity'::timestamptz ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>='-infinity'::timestamptz ORDER BY i; + i +-------------------------- + -infinity + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(8 rows) + +SELECT * FROM test_timestamp WHERE i>'-infinity'::timestamptz ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(7 rows) + +SELECT * FROM test_timestamp WHERE i<'infinity'::timestamptz ORDER BY i; + i +-------------------------- + -infinity + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 +(7 rows) + +SELECT * FROM test_timestamp WHERE i<='infinity'::timestamptz ORDER BY i; + i +-------------------------- + -infinity + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(8 rows) + +SELECT * FROM test_timestamp WHERE i='infinity'::timestamptz ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>='infinity'::timestamptz ORDER BY i; + i +---------- + infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>'infinity'::timestamptz ORDER BY i; + i +--- +(0 rows) + +-- This PST timestamptz will underflow if converted to timestamp +SELECT * FROM test_timestamp WHERE i<='4714-11-23 17:00 BC'::timestamptz ORDER BY i; + i +----------- + -infinity +(1 row) + +SELECT * FROM test_timestamp WHERE i>'4714-11-23 17:00 BC'::timestamptz ORDER BY i; + i +-------------------------- + Tue Oct 26 03:55:08 2004 + Tue Oct 26 04:55:08 2004 + Tue Oct 26 05:55:08 2004 + Tue Oct 26 08:55:08 2004 + Wed Oct 27 09:55:08 2004 + Wed Oct 27 10:55:08 2004 + infinity +(7 rows) + diff --git a/contrib/btree_gin/expected/timestamptz.out b/contrib/btree_gin/expected/timestamptz.out index d53963d2a04b8..0dada0b662cbb 100644 --- a/contrib/btree_gin/expected/timestamptz.out +++ b/contrib/btree_gin/expected/timestamptz.out @@ -7,8 +7,8 @@ INSERT INTO test_timestamptz VALUES ( '2004-10-26 04:55:08' ), ( '2004-10-26 05:55:08' ), ( '2004-10-26 08:55:08' ), - ( '2004-10-26 09:55:08' ), - ( '2004-10-26 10:55:08' ) + ( '2004-10-27 09:55:08' ), + ( '2004-10-27 10:55:08' ) ; CREATE INDEX idx_timestamptz ON test_timestamptz USING gin (i); SELECT * FROM test_timestamptz WHERE i<'2004-10-26 08:55:08'::timestamptz ORDER BY i; @@ -38,14 +38,113 @@ SELECT * FROM test_timestamptz WHERE i>='2004-10-26 08:55:08'::timestamptz ORDER i ------------------------------ Tue Oct 26 08:55:08 2004 PDT - Tue Oct 26 09:55:08 2004 PDT - Tue Oct 26 10:55:08 2004 PDT + Wed Oct 27 09:55:08 2004 PDT + Wed Oct 27 10:55:08 2004 PDT (3 rows) SELECT * FROM test_timestamptz WHERE i>'2004-10-26 08:55:08'::timestamptz ORDER BY i; i ------------------------------ - Tue Oct 26 09:55:08 2004 PDT - Tue Oct 26 10:55:08 2004 PDT + Wed Oct 27 09:55:08 2004 PDT + Wed Oct 27 10:55:08 2004 PDT +(2 rows) + +explain (costs off) +SELECT * FROM test_timestamptz WHERE i<'2004-10-27'::date ORDER BY i; + QUERY PLAN +---------------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_timestamptz + Recheck Cond: (i < '10-27-2004'::date) + -> Bitmap Index Scan on idx_timestamptz + Index Cond: (i < '10-27-2004'::date) +(6 rows) + +SELECT * FROM test_timestamptz WHERE i<'2004-10-27'::date ORDER BY i; + i +------------------------------ + Tue Oct 26 03:55:08 2004 PDT + Tue Oct 26 04:55:08 2004 PDT + Tue Oct 26 05:55:08 2004 PDT + Tue Oct 26 08:55:08 2004 PDT +(4 rows) + +SELECT * FROM test_timestamptz WHERE i<='2004-10-27'::date ORDER BY i; + i +------------------------------ + Tue Oct 26 03:55:08 2004 PDT + Tue Oct 26 04:55:08 2004 PDT + Tue Oct 26 05:55:08 2004 PDT + Tue Oct 26 08:55:08 2004 PDT +(4 rows) + +SELECT * FROM test_timestamptz WHERE i='2004-10-27'::date ORDER BY i; + i +--- +(0 rows) + +SELECT * FROM test_timestamptz WHERE i>='2004-10-27'::date ORDER BY i; + i +------------------------------ + Wed Oct 27 09:55:08 2004 PDT + Wed Oct 27 10:55:08 2004 PDT +(2 rows) + +SELECT * FROM test_timestamptz WHERE i>'2004-10-27'::date ORDER BY i; + i +------------------------------ + Wed Oct 27 09:55:08 2004 PDT + Wed Oct 27 10:55:08 2004 PDT +(2 rows) + +explain (costs off) +SELECT * FROM test_timestamptz WHERE i<'2004-10-26 08:55:08'::timestamp ORDER BY i; + QUERY PLAN +----------------------------------------------------------------------------------------- + Sort + Sort Key: i + -> Bitmap Heap Scan on test_timestamptz + Recheck Cond: (i < 'Tue Oct 26 08:55:08 2004'::timestamp without time zone) + -> Bitmap Index Scan on idx_timestamptz + Index Cond: (i < 'Tue Oct 26 08:55:08 2004'::timestamp without time zone) +(6 rows) + +SELECT * FROM test_timestamptz WHERE i<'2004-10-26 08:55:08'::timestamp ORDER BY i; + i +------------------------------ + Tue Oct 26 03:55:08 2004 PDT + Tue Oct 26 04:55:08 2004 PDT + Tue Oct 26 05:55:08 2004 PDT +(3 rows) + +SELECT * FROM test_timestamptz WHERE i<='2004-10-26 08:55:08'::timestamp ORDER BY i; + i +------------------------------ + Tue Oct 26 03:55:08 2004 PDT + Tue Oct 26 04:55:08 2004 PDT + Tue Oct 26 05:55:08 2004 PDT + Tue Oct 26 08:55:08 2004 PDT +(4 rows) + +SELECT * FROM test_timestamptz WHERE i='2004-10-26 08:55:08'::timestamp ORDER BY i; + i +------------------------------ + Tue Oct 26 08:55:08 2004 PDT +(1 row) + +SELECT * FROM test_timestamptz WHERE i>='2004-10-26 08:55:08'::timestamp ORDER BY i; + i +------------------------------ + Tue Oct 26 08:55:08 2004 PDT + Wed Oct 27 09:55:08 2004 PDT + Wed Oct 27 10:55:08 2004 PDT +(3 rows) + +SELECT * FROM test_timestamptz WHERE i>'2004-10-26 08:55:08'::timestamp ORDER BY i; + i +------------------------------ + Wed Oct 27 09:55:08 2004 PDT + Wed Oct 27 10:55:08 2004 PDT (2 rows) diff --git a/contrib/btree_gin/meson.build b/contrib/btree_gin/meson.build index b2749f6e66951..039cfc52c868a 100644 --- a/contrib/btree_gin/meson.build +++ b/contrib/btree_gin/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group btree_gin_sources = files( 'btree_gin.c', @@ -22,6 +22,7 @@ install_data( 'btree_gin--1.0--1.1.sql', 'btree_gin--1.1--1.2.sql', 'btree_gin--1.2--1.3.sql', + 'btree_gin--1.3--1.4.sql', kwargs: contrib_data_args, ) diff --git a/contrib/btree_gin/sql/date.sql b/contrib/btree_gin/sql/date.sql index 35086f6b81b9b..006f6f528b835 100644 --- a/contrib/btree_gin/sql/date.sql +++ b/contrib/btree_gin/sql/date.sql @@ -20,3 +20,67 @@ SELECT * FROM test_date WHERE i<='2004-10-26'::date ORDER BY i; SELECT * FROM test_date WHERE i='2004-10-26'::date ORDER BY i; SELECT * FROM test_date WHERE i>='2004-10-26'::date ORDER BY i; SELECT * FROM test_date WHERE i>'2004-10-26'::date ORDER BY i; + +explain (costs off) +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamp ORDER BY i; + +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i<='2004-10-26'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i='2004-10-26'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i>='2004-10-26'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i>'2004-10-26'::timestamp ORDER BY i; + +explain (costs off) +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamptz ORDER BY i; + +SELECT * FROM test_date WHERE i<'2004-10-26'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i<='2004-10-26'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i='2004-10-26'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i>='2004-10-26'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i>'2004-10-26'::timestamptz ORDER BY i; + +-- Check endpoint and out-of-range cases + +INSERT INTO test_date VALUES ('-infinity'), ('infinity'); +SELECT gin_clean_pending_list('idx_date'); + +SELECT * FROM test_date WHERE i<'-infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i<='-infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i='-infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i>='-infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i>'-infinity'::timestamp ORDER BY i; + +SELECT * FROM test_date WHERE i<'infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i<='infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i='infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i>='infinity'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i>'infinity'::timestamp ORDER BY i; + +SELECT * FROM test_date WHERE i<'-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i<='-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i='-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i>='-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i>'-infinity'::timestamptz ORDER BY i; + +SELECT * FROM test_date WHERE i<'infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i<='infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i='infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i>='infinity'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i>'infinity'::timestamptz ORDER BY i; + +-- Check rounding cases +-- '2004-10-25 00:00:01' rounds to '2004-10-25' for date. +-- '2004-10-25 23:59:59' also rounds to '2004-10-25', +-- so it's the same case as '2004-10-25 00:00:01' + +SELECT * FROM test_date WHERE i < '2004-10-25 00:00:01'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i <= '2004-10-25 00:00:01'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i = '2004-10-25 00:00:01'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i > '2004-10-25 00:00:01'::timestamp ORDER BY i; +SELECT * FROM test_date WHERE i >= '2004-10-25 00:00:01'::timestamp ORDER BY i; + +SELECT * FROM test_date WHERE i < '2004-10-25 00:00:01'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i <= '2004-10-25 00:00:01'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i = '2004-10-25 00:00:01'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i > '2004-10-25 00:00:01'::timestamptz ORDER BY i; +SELECT * FROM test_date WHERE i >= '2004-10-25 00:00:01'::timestamptz ORDER BY i; diff --git a/contrib/btree_gin/sql/float4.sql b/contrib/btree_gin/sql/float4.sql index 759778ad3c3b4..0707ed6518fa2 100644 --- a/contrib/btree_gin/sql/float4.sql +++ b/contrib/btree_gin/sql/float4.sql @@ -13,3 +13,56 @@ SELECT * FROM test_float4 WHERE i<=1::float4 ORDER BY i; SELECT * FROM test_float4 WHERE i=1::float4 ORDER BY i; SELECT * FROM test_float4 WHERE i>=1::float4 ORDER BY i; SELECT * FROM test_float4 WHERE i>1::float4 ORDER BY i; + +explain (costs off) +SELECT * FROM test_float4 WHERE i<1::float8 ORDER BY i; + +SELECT * FROM test_float4 WHERE i<1::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i<=1::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i=1::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>=1::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>1::float8 ORDER BY i; + +-- Check endpoint and out-of-range cases + +INSERT INTO test_float4 VALUES ('NaN'), ('Inf'), ('-Inf'); +SELECT gin_clean_pending_list('idx_float4'); + +SELECT * FROM test_float4 WHERE i<'-Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i<='-Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i='-Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>='-Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>'-Inf'::float8 ORDER BY i; + +SELECT * FROM test_float4 WHERE i<'Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i<='Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i='Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>='Inf'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>'Inf'::float8 ORDER BY i; + +SELECT * FROM test_float4 WHERE i<'1e300'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i<='1e300'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i='1e300'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>='1e300'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>'1e300'::float8 ORDER BY i; + +SELECT * FROM test_float4 WHERE i<'NaN'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i<='NaN'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i='NaN'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>='NaN'::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i>'NaN'::float8 ORDER BY i; + +-- Check rounding cases +-- 1e-300 rounds to 0 for float4 but not for float8 + +SELECT * FROM test_float4 WHERE i < -1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i <= -1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i = -1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i > -1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i >= -1e-300::float8 ORDER BY i; + +SELECT * FROM test_float4 WHERE i < 1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i <= 1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i = 1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i > 1e-300::float8 ORDER BY i; +SELECT * FROM test_float4 WHERE i >= 1e-300::float8 ORDER BY i; diff --git a/contrib/btree_gin/sql/float8.sql b/contrib/btree_gin/sql/float8.sql index b046ac4e6c4bb..5f393147082b1 100644 --- a/contrib/btree_gin/sql/float8.sql +++ b/contrib/btree_gin/sql/float8.sql @@ -13,3 +13,12 @@ SELECT * FROM test_float8 WHERE i<=1::float8 ORDER BY i; SELECT * FROM test_float8 WHERE i=1::float8 ORDER BY i; SELECT * FROM test_float8 WHERE i>=1::float8 ORDER BY i; SELECT * FROM test_float8 WHERE i>1::float8 ORDER BY i; + +explain (costs off) +SELECT * FROM test_float8 WHERE i<1::float4 ORDER BY i; + +SELECT * FROM test_float8 WHERE i<1::float4 ORDER BY i; +SELECT * FROM test_float8 WHERE i<=1::float4 ORDER BY i; +SELECT * FROM test_float8 WHERE i=1::float4 ORDER BY i; +SELECT * FROM test_float8 WHERE i>=1::float4 ORDER BY i; +SELECT * FROM test_float8 WHERE i>1::float4 ORDER BY i; diff --git a/contrib/btree_gin/sql/int2.sql b/contrib/btree_gin/sql/int2.sql index f06f11702f54e..959e0f6cfde01 100644 --- a/contrib/btree_gin/sql/int2.sql +++ b/contrib/btree_gin/sql/int2.sql @@ -13,3 +13,38 @@ SELECT * FROM test_int2 WHERE i<=1::int2 ORDER BY i; SELECT * FROM test_int2 WHERE i=1::int2 ORDER BY i; SELECT * FROM test_int2 WHERE i>=1::int2 ORDER BY i; SELECT * FROM test_int2 WHERE i>1::int2 ORDER BY i; + +explain (costs off) +SELECT * FROM test_int2 WHERE i<1::int4 ORDER BY i; + +SELECT * FROM test_int2 WHERE i<1::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i<=1::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i=1::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i>=1::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i>1::int4 ORDER BY i; + +explain (costs off) +SELECT * FROM test_int2 WHERE i<1::int8 ORDER BY i; + +SELECT * FROM test_int2 WHERE i<1::int8 ORDER BY i; +SELECT * FROM test_int2 WHERE i<=1::int8 ORDER BY i; +SELECT * FROM test_int2 WHERE i=1::int8 ORDER BY i; +SELECT * FROM test_int2 WHERE i>=1::int8 ORDER BY i; +SELECT * FROM test_int2 WHERE i>1::int8 ORDER BY i; + +-- Check endpoint and out-of-range cases + +INSERT INTO test_int2 VALUES ((-32768)::int2),(32767); +SELECT gin_clean_pending_list('idx_int2'); + +SELECT * FROM test_int2 WHERE i<(-32769)::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i<=(-32769)::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i=(-32769)::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i>=(-32769)::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i>(-32769)::int4 ORDER BY i; + +SELECT * FROM test_int2 WHERE i<32768::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i<=32768::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i=32768::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i>=32768::int4 ORDER BY i; +SELECT * FROM test_int2 WHERE i>32768::int4 ORDER BY i; diff --git a/contrib/btree_gin/sql/int4.sql b/contrib/btree_gin/sql/int4.sql index 6499c29630722..9a45530b63ad7 100644 --- a/contrib/btree_gin/sql/int4.sql +++ b/contrib/btree_gin/sql/int4.sql @@ -13,3 +13,21 @@ SELECT * FROM test_int4 WHERE i<=1::int4 ORDER BY i; SELECT * FROM test_int4 WHERE i=1::int4 ORDER BY i; SELECT * FROM test_int4 WHERE i>=1::int4 ORDER BY i; SELECT * FROM test_int4 WHERE i>1::int4 ORDER BY i; + +explain (costs off) +SELECT * FROM test_int4 WHERE i<1::int2 ORDER BY i; + +SELECT * FROM test_int4 WHERE i<1::int2 ORDER BY i; +SELECT * FROM test_int4 WHERE i<=1::int2 ORDER BY i; +SELECT * FROM test_int4 WHERE i=1::int2 ORDER BY i; +SELECT * FROM test_int4 WHERE i>=1::int2 ORDER BY i; +SELECT * FROM test_int4 WHERE i>1::int2 ORDER BY i; + +explain (costs off) +SELECT * FROM test_int4 WHERE i<1::int8 ORDER BY i; + +SELECT * FROM test_int4 WHERE i<1::int8 ORDER BY i; +SELECT * FROM test_int4 WHERE i<=1::int8 ORDER BY i; +SELECT * FROM test_int4 WHERE i=1::int8 ORDER BY i; +SELECT * FROM test_int4 WHERE i>=1::int8 ORDER BY i; +SELECT * FROM test_int4 WHERE i>1::int8 ORDER BY i; diff --git a/contrib/btree_gin/sql/int8.sql b/contrib/btree_gin/sql/int8.sql index 4d9c2871814c4..b31f27c69b90a 100644 --- a/contrib/btree_gin/sql/int8.sql +++ b/contrib/btree_gin/sql/int8.sql @@ -13,3 +13,21 @@ SELECT * FROM test_int8 WHERE i<=1::int8 ORDER BY i; SELECT * FROM test_int8 WHERE i=1::int8 ORDER BY i; SELECT * FROM test_int8 WHERE i>=1::int8 ORDER BY i; SELECT * FROM test_int8 WHERE i>1::int8 ORDER BY i; + +explain (costs off) +SELECT * FROM test_int8 WHERE i<1::int2 ORDER BY i; + +SELECT * FROM test_int8 WHERE i<1::int2 ORDER BY i; +SELECT * FROM test_int8 WHERE i<=1::int2 ORDER BY i; +SELECT * FROM test_int8 WHERE i=1::int2 ORDER BY i; +SELECT * FROM test_int8 WHERE i>=1::int2 ORDER BY i; +SELECT * FROM test_int8 WHERE i>1::int2 ORDER BY i; + +explain (costs off) +SELECT * FROM test_int8 WHERE i<1::int4 ORDER BY i; + +SELECT * FROM test_int8 WHERE i<1::int4 ORDER BY i; +SELECT * FROM test_int8 WHERE i<=1::int4 ORDER BY i; +SELECT * FROM test_int8 WHERE i=1::int4 ORDER BY i; +SELECT * FROM test_int8 WHERE i>=1::int4 ORDER BY i; +SELECT * FROM test_int8 WHERE i>1::int4 ORDER BY i; diff --git a/contrib/btree_gin/sql/name.sql b/contrib/btree_gin/sql/name.sql index c11580cdf9609..551d928940746 100644 --- a/contrib/btree_gin/sql/name.sql +++ b/contrib/btree_gin/sql/name.sql @@ -19,3 +19,14 @@ EXPLAIN (COSTS OFF) SELECT * FROM test_name WHERE i<='abc' ORDER BY i; EXPLAIN (COSTS OFF) SELECT * FROM test_name WHERE i='abc' ORDER BY i; EXPLAIN (COSTS OFF) SELECT * FROM test_name WHERE i>='abc' ORDER BY i; EXPLAIN (COSTS OFF) SELECT * FROM test_name WHERE i>'abc' ORDER BY i; + +explain (costs off) +SELECT * FROM test_name WHERE i<'abc'::text ORDER BY i; + +SELECT * FROM test_name WHERE i<'abc'::text ORDER BY i; +SELECT * FROM test_name WHERE i<='abc'::text ORDER BY i; +SELECT * FROM test_name WHERE i='abc'::text ORDER BY i; +SELECT * FROM test_name WHERE i>='abc'::text ORDER BY i; +SELECT * FROM test_name WHERE i>'abc'::text ORDER BY i; + +SELECT * FROM test_name WHERE i<=repeat('abc', 100) ORDER BY i; diff --git a/contrib/btree_gin/sql/text.sql b/contrib/btree_gin/sql/text.sql index d5b3b39898988..978b21376fd85 100644 --- a/contrib/btree_gin/sql/text.sql +++ b/contrib/btree_gin/sql/text.sql @@ -13,3 +13,12 @@ SELECT * FROM test_text WHERE i<='abc' ORDER BY i; SELECT * FROM test_text WHERE i='abc' ORDER BY i; SELECT * FROM test_text WHERE i>='abc' ORDER BY i; SELECT * FROM test_text WHERE i>'abc' ORDER BY i; + +explain (costs off) +SELECT * FROM test_text WHERE i<'abc'::name COLLATE "default" ORDER BY i; + +SELECT * FROM test_text WHERE i<'abc'::name COLLATE "default" ORDER BY i; +SELECT * FROM test_text WHERE i<='abc'::name COLLATE "default" ORDER BY i; +SELECT * FROM test_text WHERE i='abc'::name COLLATE "default" ORDER BY i; +SELECT * FROM test_text WHERE i>='abc'::name COLLATE "default" ORDER BY i; +SELECT * FROM test_text WHERE i>'abc'::name COLLATE "default" ORDER BY i; diff --git a/contrib/btree_gin/sql/timestamp.sql b/contrib/btree_gin/sql/timestamp.sql index 56727e81c4aff..1ee4edb5ea4d2 100644 --- a/contrib/btree_gin/sql/timestamp.sql +++ b/contrib/btree_gin/sql/timestamp.sql @@ -9,8 +9,8 @@ INSERT INTO test_timestamp VALUES ( '2004-10-26 04:55:08' ), ( '2004-10-26 05:55:08' ), ( '2004-10-26 08:55:08' ), - ( '2004-10-26 09:55:08' ), - ( '2004-10-26 10:55:08' ) + ( '2004-10-27 09:55:08' ), + ( '2004-10-27 10:55:08' ) ; CREATE INDEX idx_timestamp ON test_timestamp USING gin (i); @@ -20,3 +20,54 @@ SELECT * FROM test_timestamp WHERE i<='2004-10-26 08:55:08'::timestamp ORDER BY SELECT * FROM test_timestamp WHERE i='2004-10-26 08:55:08'::timestamp ORDER BY i; SELECT * FROM test_timestamp WHERE i>='2004-10-26 08:55:08'::timestamp ORDER BY i; SELECT * FROM test_timestamp WHERE i>'2004-10-26 08:55:08'::timestamp ORDER BY i; + +explain (costs off) +SELECT * FROM test_timestamp WHERE i<'2004-10-27'::date ORDER BY i; + +SELECT * FROM test_timestamp WHERE i<'2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i<='2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i='2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i>='2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'2004-10-27'::date ORDER BY i; + +explain (costs off) +SELECT * FROM test_timestamp WHERE i<'2004-10-26 08:55:08'::timestamptz ORDER BY i; + +SELECT * FROM test_timestamp WHERE i<'2004-10-26 08:55:08'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i<='2004-10-26 08:55:08'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i='2004-10-26 08:55:08'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>='2004-10-26 08:55:08'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'2004-10-26 08:55:08'::timestamptz ORDER BY i; + +-- Check endpoint and out-of-range cases + +INSERT INTO test_timestamp VALUES ('-infinity'), ('infinity'); +SELECT gin_clean_pending_list('idx_timestamp'); + +SELECT * FROM test_timestamp WHERE i<'-infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i<='-infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i='-infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i>='-infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'-infinity'::date ORDER BY i; + +SELECT * FROM test_timestamp WHERE i<'infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i<='infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i='infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i>='infinity'::date ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'infinity'::date ORDER BY i; + +SELECT * FROM test_timestamp WHERE i<'-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i<='-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i='-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>='-infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'-infinity'::timestamptz ORDER BY i; + +SELECT * FROM test_timestamp WHERE i<'infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i<='infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i='infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>='infinity'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'infinity'::timestamptz ORDER BY i; + +-- This PST timestamptz will underflow if converted to timestamp +SELECT * FROM test_timestamp WHERE i<='4714-11-23 17:00 BC'::timestamptz ORDER BY i; +SELECT * FROM test_timestamp WHERE i>'4714-11-23 17:00 BC'::timestamptz ORDER BY i; diff --git a/contrib/btree_gin/sql/timestamptz.sql b/contrib/btree_gin/sql/timestamptz.sql index e6cfdb1b07447..40d2d7ed329d2 100644 --- a/contrib/btree_gin/sql/timestamptz.sql +++ b/contrib/btree_gin/sql/timestamptz.sql @@ -9,8 +9,8 @@ INSERT INTO test_timestamptz VALUES ( '2004-10-26 04:55:08' ), ( '2004-10-26 05:55:08' ), ( '2004-10-26 08:55:08' ), - ( '2004-10-26 09:55:08' ), - ( '2004-10-26 10:55:08' ) + ( '2004-10-27 09:55:08' ), + ( '2004-10-27 10:55:08' ) ; CREATE INDEX idx_timestamptz ON test_timestamptz USING gin (i); @@ -20,3 +20,21 @@ SELECT * FROM test_timestamptz WHERE i<='2004-10-26 08:55:08'::timestamptz ORDER SELECT * FROM test_timestamptz WHERE i='2004-10-26 08:55:08'::timestamptz ORDER BY i; SELECT * FROM test_timestamptz WHERE i>='2004-10-26 08:55:08'::timestamptz ORDER BY i; SELECT * FROM test_timestamptz WHERE i>'2004-10-26 08:55:08'::timestamptz ORDER BY i; + +explain (costs off) +SELECT * FROM test_timestamptz WHERE i<'2004-10-27'::date ORDER BY i; + +SELECT * FROM test_timestamptz WHERE i<'2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamptz WHERE i<='2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamptz WHERE i='2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamptz WHERE i>='2004-10-27'::date ORDER BY i; +SELECT * FROM test_timestamptz WHERE i>'2004-10-27'::date ORDER BY i; + +explain (costs off) +SELECT * FROM test_timestamptz WHERE i<'2004-10-26 08:55:08'::timestamp ORDER BY i; + +SELECT * FROM test_timestamptz WHERE i<'2004-10-26 08:55:08'::timestamp ORDER BY i; +SELECT * FROM test_timestamptz WHERE i<='2004-10-26 08:55:08'::timestamp ORDER BY i; +SELECT * FROM test_timestamptz WHERE i='2004-10-26 08:55:08'::timestamp ORDER BY i; +SELECT * FROM test_timestamptz WHERE i>='2004-10-26 08:55:08'::timestamp ORDER BY i; +SELECT * FROM test_timestamptz WHERE i>'2004-10-26 08:55:08'::timestamp ORDER BY i; diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile index 68190ac5e4687..fbbbca9559833 100644 --- a/contrib/btree_gist/Makefile +++ b/contrib/btree_gist/Makefile @@ -31,10 +31,11 @@ OBJS = \ EXTENSION = btree_gist DATA = btree_gist--1.0--1.1.sql \ - btree_gist--1.1--1.2.sql btree_gist--1.2.sql btree_gist--1.2--1.3.sql \ + btree_gist--1.1--1.2.sql btree_gist--1.2--1.3.sql \ btree_gist--1.3--1.4.sql btree_gist--1.4--1.5.sql \ btree_gist--1.5--1.6.sql btree_gist--1.6--1.7.sql \ - btree_gist--1.7--1.8.sql btree_gist--1.8--1.9.sql + btree_gist--1.7--1.8.sql btree_gist--1.8--1.9.sql \ + btree_gist--1.9.sql PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes" REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \ diff --git a/contrib/btree_gist/btree_bit.c b/contrib/btree_gist/btree_bit.c index 0df2ae20d8b21..2b9c18a586f37 100644 --- a/contrib/btree_gist/btree_bit.c +++ b/contrib/btree_gist/btree_bit.c @@ -8,6 +8,7 @@ #include "utils/fmgrprotos.h" #include "utils/sortsupport.h" #include "utils/varbit.h" +#include "varatt.h" /* GiST support functions */ PG_FUNCTION_INFO_V1(gbt_bit_compress); @@ -138,8 +139,9 @@ gbt_bit_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); void *query = DatumGetByteaP(PG_GETARG_DATUM(1)); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool retval; GBT_VARKEY *key = (GBT_VARKEY *) DatumGetPointer(entry->key); diff --git a/contrib/btree_gist/btree_bool.c b/contrib/btree_gist/btree_bool.c index 1127597bb6017..8e59523f474b5 100644 --- a/contrib/btree_gist/btree_bool.c +++ b/contrib/btree_gist/btree_bool.c @@ -5,6 +5,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct boolkey @@ -107,8 +108,9 @@ gbt_bool_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); bool query = PG_GETARG_INT16(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); boolKEY *kkk = (boolKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_bytea.c b/contrib/btree_gist/btree_bytea.c index 26f8710fad5c6..50bb24308e947 100644 --- a/contrib/btree_gist/btree_bytea.c +++ b/contrib/btree_gist/btree_bytea.c @@ -101,8 +101,9 @@ gbt_bytea_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); void *query = DatumGetByteaP(PG_GETARG_DATUM(1)); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool retval; GBT_VARKEY *key = (GBT_VARKEY *) DatumGetPointer(entry->key); diff --git a/contrib/btree_gist/btree_cash.c b/contrib/btree_gist/btree_cash.c index 01c8d5a5f4074..a347b1320e487 100644 --- a/contrib/btree_gist/btree_cash.c +++ b/contrib/btree_gist/btree_cash.c @@ -7,6 +7,7 @@ #include "btree_utils_num.h" #include "common/int.h" #include "utils/cash.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct @@ -138,8 +139,9 @@ gbt_cash_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Cash query = PG_GETARG_CASH(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); cashKEY *kkk = (cashKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -160,8 +162,9 @@ gbt_cash_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Cash query = PG_GETARG_CASH(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif cashKEY *kkk = (cashKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_date.c b/contrib/btree_gist/btree_date.c index c008dc61ba5f5..5e41f4574c5ad 100644 --- a/contrib/btree_gist/btree_date.c +++ b/contrib/btree_gist/btree_date.c @@ -7,6 +7,7 @@ #include "btree_utils_num.h" #include "utils/fmgrprotos.h" #include "utils/date.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct @@ -153,8 +154,9 @@ gbt_date_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); DateADT query = PG_GETARG_DATEADT(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); dateKEY *kkk = (dateKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -175,8 +177,9 @@ gbt_date_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); DateADT query = PG_GETARG_DATEADT(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif dateKEY *kkk = (dateKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_enum.c b/contrib/btree_gist/btree_enum.c index 83c95c7bb0401..0dd91002c956f 100644 --- a/contrib/btree_gist/btree_enum.c +++ b/contrib/btree_gist/btree_enum.c @@ -8,6 +8,7 @@ #include "fmgr.h" #include "utils/fmgrprotos.h" #include "utils/fmgroids.h" +#include "utils/rel.h" #include "utils/sortsupport.h" /* enums are really Oids, so we just use the same structure */ @@ -125,8 +126,9 @@ gbt_enum_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Oid query = PG_GETARG_OID(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); oidKEY *kkk = (oidKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -193,8 +195,8 @@ gbt_enum_ssup_cmp(Datum x, Datum y, SortSupport ssup) return DatumGetInt32(CallerFInfoFunctionCall2(enum_cmp, ssup->ssup_extra, InvalidOid, - arg1->lower, - arg2->lower)); + ObjectIdGetDatum(arg1->lower), + ObjectIdGetDatum(arg2->lower))); } Datum diff --git a/contrib/btree_gist/btree_float4.c b/contrib/btree_gist/btree_float4.c index bec026a923a18..c076918fd48fc 100644 --- a/contrib/btree_gist/btree_float4.c +++ b/contrib/btree_gist/btree_float4.c @@ -6,6 +6,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "utils/float.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct float4key @@ -132,8 +133,9 @@ gbt_float4_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); float4 query = PG_GETARG_FLOAT4(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); float4KEY *kkk = (float4KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -154,8 +156,9 @@ gbt_float4_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); float4 query = PG_GETARG_FLOAT4(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif float4KEY *kkk = (float4KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_float8.c b/contrib/btree_gist/btree_float8.c index 43e7cde2b6958..d7386e885a279 100644 --- a/contrib/btree_gist/btree_float8.c +++ b/contrib/btree_gist/btree_float8.c @@ -6,6 +6,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "utils/float.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct float8key @@ -140,8 +141,9 @@ gbt_float8_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); float8 query = PG_GETARG_FLOAT8(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); float8KEY *kkk = (float8KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -162,8 +164,9 @@ gbt_float8_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); float8 query = PG_GETARG_FLOAT8(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif float8KEY *kkk = (float8KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_gist--1.7--1.8.sql b/contrib/btree_gist/btree_gist--1.7--1.8.sql index 4ff9c43a8ebe5..22316dc3f566c 100644 --- a/contrib/btree_gist/btree_gist--1.7--1.8.sql +++ b/contrib/btree_gist/btree_gist--1.7--1.8.sql @@ -3,85 +3,282 @@ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.8'" to load this file. \quit -CREATE FUNCTION gist_stratnum_btree(int) +-- Add sortsupport functions + +CREATE FUNCTION gbt_bit_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_varbit_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_bool_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_bytea_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_cash_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_date_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_enum_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_float4_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_float8_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_inet_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_int2_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_int4_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_int8_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_intv_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_macaddr_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_macad8_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_numeric_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_oid_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_text_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_bpchar_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_time_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_ts_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +CREATE FUNCTION gbt_uuid_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; + +ALTER OPERATOR FAMILY gist_bit_ops USING gist ADD + FUNCTION 11 (bit, bit) gbt_bit_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD + FUNCTION 11 (varbit, varbit) gbt_varbit_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_bool_ops USING gist ADD + FUNCTION 11 (bool, bool) gbt_bool_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD + FUNCTION 11 (bytea, bytea) gbt_bytea_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD + FUNCTION 11 (money, money) gbt_cash_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_date_ops USING gist ADD + FUNCTION 11 (date, date) gbt_date_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_enum_ops USING gist ADD + FUNCTION 11 (anyenum, anyenum) gbt_enum_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD + FUNCTION 11 (float4, float4) gbt_float4_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD + FUNCTION 11 (float8, float8) gbt_float8_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_inet_ops USING gist ADD + FUNCTION 11 (inet, inet) gbt_inet_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_cidr_ops USING gist ADD + FUNCTION 11 (cidr, cidr) gbt_inet_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD + FUNCTION 11 (int2, int2) gbt_int2_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD + FUNCTION 11 (int4, int4) gbt_int4_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD + FUNCTION 11 (int8, int8) gbt_int8_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD + FUNCTION 11 (interval, interval) gbt_intv_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD + FUNCTION 11 (macaddr, macaddr) gbt_macaddr_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_macaddr8_ops USING gist ADD + FUNCTION 11 (macaddr8, macaddr8) gbt_macad8_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD + FUNCTION 11 (numeric, numeric) gbt_numeric_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD + FUNCTION 11 (oid, oid) gbt_oid_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_text_ops USING gist ADD + FUNCTION 11 (text, text) gbt_text_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD + FUNCTION 11 (bpchar, bpchar) gbt_bpchar_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_time_ops USING gist ADD + FUNCTION 11 (time, time) gbt_time_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD + FUNCTION 11 (timetz, timetz) gbt_time_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_timestamp_ops USING gist ADD + FUNCTION 11 (timestamp, timestamp) gbt_ts_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD + FUNCTION 11 (timestamptz, timestamptz) gbt_ts_sortsupport (internal) ; + +ALTER OPERATOR FAMILY gist_uuid_ops USING gist ADD + FUNCTION 11 (uuid, uuid) gbt_uuid_sortsupport (internal) ; + +-- Add translate_cmptype functions + +CREATE FUNCTION gist_translate_cmptype_btree(int) RETURNS smallint AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_timestamp_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_time_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_date_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_text_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_bit_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_inet_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_cidr_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_uuid_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_macaddr8_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_enum_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; ALTER OPERATOR FAMILY gist_bool_ops USING gist ADD - FUNCTION 12 ("any", "any") gist_stratnum_btree (int) ; + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int) ; diff --git a/contrib/btree_gist/btree_gist--1.8--1.9.sql b/contrib/btree_gist/btree_gist--1.8--1.9.sql index 4b38749bf5f34..c67812f5f5d5b 100644 --- a/contrib/btree_gist/btree_gist--1.8--1.9.sql +++ b/contrib/btree_gist/btree_gist--1.8--1.9.sql @@ -1,197 +1,40 @@ -/* contrib/btree_gist/btree_gist--1.7--1.8.sql */ +/* contrib/btree_gist/btree_gist--1.8--1.9.sql */ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION btree_gist UPDATE TO '1.9'" to load this file. \quit -CREATE FUNCTION gbt_bit_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_varbit_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_bool_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_bytea_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_cash_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_date_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_enum_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_float4_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_float8_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_inet_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_int2_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_int4_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_int8_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_intv_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_macaddr_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_macad8_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_numeric_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_oid_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_text_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_bpchar_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_time_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_ts_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -CREATE FUNCTION gbt_uuid_sortsupport(internal) -RETURNS void -AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE PARALLEL SAFE STRICT; - -ALTER OPERATOR FAMILY gist_bit_ops USING gist ADD - FUNCTION 11 (bit, bit) gbt_bit_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD - FUNCTION 11 (varbit, varbit) gbt_varbit_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_bool_ops USING gist ADD - FUNCTION 11 (bool, bool) gbt_bool_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD - FUNCTION 11 (bytea, bytea) gbt_bytea_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD - FUNCTION 11 (money, money) gbt_cash_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_date_ops USING gist ADD - FUNCTION 11 (date, date) gbt_date_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_enum_ops USING gist ADD - FUNCTION 11 (anyenum, anyenum) gbt_enum_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD - FUNCTION 11 (float4, float4) gbt_float4_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD - FUNCTION 11 (float8, float8) gbt_float8_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_inet_ops USING gist ADD - FUNCTION 11 (inet, inet) gbt_inet_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_cidr_ops USING gist ADD - FUNCTION 11 (cidr, cidr) gbt_inet_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD - FUNCTION 11 (int2, int2) gbt_int2_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD - FUNCTION 11 (int4, int4) gbt_int4_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD - FUNCTION 11 (int8, int8) gbt_int8_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD - FUNCTION 11 (interval, interval) gbt_intv_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD - FUNCTION 11 (macaddr, macaddr) gbt_macaddr_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_macaddr8_ops USING gist ADD - FUNCTION 11 (macaddr8, macaddr8) gbt_macad8_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD - FUNCTION 11 (numeric, numeric) gbt_numeric_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD - FUNCTION 11 (oid, oid) gbt_oid_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_text_ops USING gist ADD - FUNCTION 11 (text, text) gbt_text_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD - FUNCTION 11 (bpchar, bpchar) gbt_bpchar_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_time_ops USING gist ADD - FUNCTION 11 (time, time) gbt_time_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD - FUNCTION 11 (timetz, timetz) gbt_time_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_timestamp_ops USING gist ADD - FUNCTION 11 (timestamp, timestamp) gbt_ts_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD - FUNCTION 11 (timestamptz, timestamptz) gbt_ts_sortsupport (internal) ; - -ALTER OPERATOR FAMILY gist_uuid_ops USING gist ADD - FUNCTION 11 (uuid, uuid) gbt_uuid_sortsupport (internal) ; +-- +-- Mark gist_inet_ops and gist_cidr_ops opclasses as non-default. +-- This is the first step on the way to eventually removing them. +-- +-- There's no SQL command for this, so fake it with a manual update on +-- pg_opclass. +-- +DO LANGUAGE plpgsql +$$ +DECLARE + my_schema pg_catalog.text := pg_catalog.quote_ident(pg_catalog.current_schema()); + old_path pg_catalog.text := pg_catalog.current_setting('search_path'); +BEGIN +-- for safety, transiently set search_path to just pg_catalog+pg_temp +PERFORM pg_catalog.set_config('search_path', 'pg_catalog, pg_temp', true); + +UPDATE pg_catalog.pg_opclass +SET opcdefault = false +WHERE opcmethod = (SELECT oid FROM pg_catalog.pg_am WHERE amname = 'gist') AND + opcname IN ('gist_inet_ops', 'gist_cidr_ops') AND + opcnamespace = my_schema::pg_catalog.regnamespace; + +PERFORM pg_catalog.set_config('search_path', old_path, true); +END +$$; + + +-- Fix parallel-safety markings overlooked in btree_gist--1.6--1.7.sql. +ALTER FUNCTION gbt_bool_consistent(internal, bool, smallint, oid, internal) PARALLEL SAFE; +ALTER FUNCTION gbt_bool_compress(internal) PARALLEL SAFE; +ALTER FUNCTION gbt_bool_fetch(internal) PARALLEL SAFE; +ALTER FUNCTION gbt_bool_penalty(internal, internal, internal) PARALLEL SAFE; +ALTER FUNCTION gbt_bool_picksplit(internal, internal) PARALLEL SAFE; +ALTER FUNCTION gbt_bool_union(internal, internal) PARALLEL SAFE; +ALTER FUNCTION gbt_bool_same(gbtreekey2, gbtreekey2, internal) PARALLEL SAFE; diff --git a/contrib/btree_gist/btree_gist--1.2.sql b/contrib/btree_gist/btree_gist--1.9.sql similarity index 57% rename from contrib/btree_gist/btree_gist--1.2.sql rename to contrib/btree_gist/btree_gist--1.9.sql index 1efe75304384f..504de91289db0 100644 --- a/contrib/btree_gist/btree_gist--1.2.sql +++ b/contrib/btree_gist/btree_gist--1.9.sql @@ -1,17 +1,33 @@ -/* contrib/btree_gist/btree_gist--1.2.sql */ +/* contrib/btree_gist/btree_gist--1.9.sql */ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION btree_gist" to load this file. \quit +CREATE FUNCTION gbtreekey2_in(cstring) +RETURNS gbtreekey2 +AS 'MODULE_PATHNAME', 'gbtreekey_in' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbtreekey2_out(gbtreekey2) +RETURNS cstring +AS 'MODULE_PATHNAME', 'gbtreekey_out' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE TYPE gbtreekey2 ( + INTERNALLENGTH = 2, + INPUT = gbtreekey2_in, + OUTPUT = gbtreekey2_out +); + CREATE FUNCTION gbtreekey4_in(cstring) RETURNS gbtreekey4 AS 'MODULE_PATHNAME', 'gbtreekey_in' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbtreekey4_out(gbtreekey4) RETURNS cstring AS 'MODULE_PATHNAME', 'gbtreekey_out' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE gbtreekey4 ( INTERNALLENGTH = 4, @@ -22,12 +38,12 @@ CREATE TYPE gbtreekey4 ( CREATE FUNCTION gbtreekey8_in(cstring) RETURNS gbtreekey8 AS 'MODULE_PATHNAME', 'gbtreekey_in' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbtreekey8_out(gbtreekey8) RETURNS cstring AS 'MODULE_PATHNAME', 'gbtreekey_out' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE gbtreekey8 ( INTERNALLENGTH = 8, @@ -38,12 +54,12 @@ CREATE TYPE gbtreekey8 ( CREATE FUNCTION gbtreekey16_in(cstring) RETURNS gbtreekey16 AS 'MODULE_PATHNAME', 'gbtreekey_in' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbtreekey16_out(gbtreekey16) RETURNS cstring AS 'MODULE_PATHNAME', 'gbtreekey_out' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE gbtreekey16 ( INTERNALLENGTH = 16, @@ -54,12 +70,12 @@ CREATE TYPE gbtreekey16 ( CREATE FUNCTION gbtreekey32_in(cstring) RETURNS gbtreekey32 AS 'MODULE_PATHNAME', 'gbtreekey_in' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbtreekey32_out(gbtreekey32) RETURNS cstring AS 'MODULE_PATHNAME', 'gbtreekey_out' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE gbtreekey32 ( INTERNALLENGTH = 32, @@ -70,12 +86,12 @@ CREATE TYPE gbtreekey32 ( CREATE FUNCTION gbtreekey_var_in(cstring) RETURNS gbtreekey_var AS 'MODULE_PATHNAME', 'gbtreekey_in' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbtreekey_var_out(gbtreekey_var) RETURNS cstring AS 'MODULE_PATHNAME', 'gbtreekey_out' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE TYPE gbtreekey_var ( INTERNALLENGTH = VARIABLE, @@ -84,12 +100,19 @@ CREATE TYPE gbtreekey_var ( STORAGE = EXTENDED ); +--common support functions + +CREATE FUNCTION gist_translate_cmptype_btree(int) +RETURNS smallint +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + --distance operators CREATE FUNCTION cash_dist(money, money) RETURNS money AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = money, @@ -101,7 +124,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION date_dist(date, date) RETURNS int4 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = date, @@ -113,7 +136,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION float4_dist(float4, float4) RETURNS float4 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = float4, @@ -125,7 +148,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION float8_dist(float8, float8) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = float8, @@ -137,7 +160,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION int2_dist(int2, int2) RETURNS int2 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = int2, @@ -149,7 +172,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION int4_dist(int4, int4) RETURNS int4 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = int4, @@ -161,7 +184,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION int8_dist(int8, int8) RETURNS int8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = int8, @@ -173,7 +196,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION interval_dist(interval, interval) RETURNS interval AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = interval, @@ -185,7 +208,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION oid_dist(oid, oid) RETURNS oid AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = oid, @@ -197,7 +220,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION time_dist(time, time) RETURNS interval AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = time, @@ -209,7 +232,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION ts_dist(timestamp, timestamp) RETURNS interval AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = timestamp, @@ -221,7 +244,7 @@ CREATE OPERATOR <-> ( CREATE FUNCTION tstz_dist(timestamptz, timestamptz) RETURNS interval AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE OPERATOR <-> ( LEFTARG = timestamptz, @@ -242,57 +265,62 @@ CREATE OPERATOR <-> ( CREATE FUNCTION gbt_oid_consistent(internal,oid,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_distance(internal,oid,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_decompress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_var_decompress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_var_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_union(internal, internal) RETURNS gbtreekey8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_oid_same(gbtreekey8, gbtreekey8, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_oid_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_oid_ops @@ -303,6 +331,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.oid_ops , FUNCTION 1 gbt_oid_consistent (internal, oid, int2, oid, internal), FUNCTION 2 gbt_oid_union (internal, internal), FUNCTION 3 gbt_oid_compress (internal), @@ -310,18 +340,12 @@ AS FUNCTION 5 gbt_oid_penalty (internal, internal, internal), FUNCTION 6 gbt_oid_picksplit (internal, internal), FUNCTION 7 gbt_oid_same (gbtreekey8, gbtreekey8, internal), + FUNCTION 8 gbt_oid_distance (internal, oid, int2, oid, internal), + FUNCTION 9 gbt_oid_fetch (internal), + FUNCTION 11 gbt_oid_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey8; --- Add operators that are new in 9.1. We do it like this, leaving them --- "loose" in the operator family rather than bound into the opclass, because --- that's the only state that can be reproduced during an upgrade from 9.0. -ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD - OPERATOR 6 <> (oid, oid) , - OPERATOR 15 <-> (oid, oid) FOR ORDER BY pg_catalog.oid_ops , - FUNCTION 8 (oid, oid) gbt_oid_distance (internal, oid, int2, oid, internal) , - -- Also add support function for index-only-scans, added in 9.5. - FUNCTION 9 (oid, oid) gbt_oid_fetch (internal) ; - -- -- @@ -334,42 +358,47 @@ ALTER OPERATOR FAMILY gist_oid_ops USING gist ADD CREATE FUNCTION gbt_int2_consistent(internal,int2,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_distance(internal,int2,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_union(internal, internal) RETURNS gbtreekey4 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int2_same(gbtreekey4, gbtreekey4, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_int2_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_int2_ops @@ -380,6 +409,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.integer_ops , FUNCTION 1 gbt_int2_consistent (internal, int2, int2, oid, internal), FUNCTION 2 gbt_int2_union (internal, internal), FUNCTION 3 gbt_int2_compress (internal), @@ -387,13 +418,12 @@ AS FUNCTION 5 gbt_int2_penalty (internal, internal, internal), FUNCTION 6 gbt_int2_picksplit (internal, internal), FUNCTION 7 gbt_int2_same (gbtreekey4, gbtreekey4, internal), + FUNCTION 8 gbt_int2_distance (internal, int2, int2, oid, internal), + FUNCTION 9 gbt_int2_fetch (internal), + FUNCTION 11 gbt_int2_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey4; -ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD - OPERATOR 6 <> (int2, int2) , - OPERATOR 15 <-> (int2, int2) FOR ORDER BY pg_catalog.integer_ops , - FUNCTION 8 (int2, int2) gbt_int2_distance (internal, int2, int2, oid, internal) , - FUNCTION 9 (int2, int2) gbt_int2_fetch (internal) ; -- -- @@ -406,42 +436,47 @@ ALTER OPERATOR FAMILY gist_int2_ops USING gist ADD CREATE FUNCTION gbt_int4_consistent(internal,int4,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_distance(internal,int4,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_union(internal, internal) RETURNS gbtreekey8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int4_same(gbtreekey8, gbtreekey8, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_int4_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_int4_ops @@ -452,6 +487,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.integer_ops , FUNCTION 1 gbt_int4_consistent (internal, int4, int2, oid, internal), FUNCTION 2 gbt_int4_union (internal, internal), FUNCTION 3 gbt_int4_compress (internal), @@ -459,14 +496,12 @@ AS FUNCTION 5 gbt_int4_penalty (internal, internal, internal), FUNCTION 6 gbt_int4_picksplit (internal, internal), FUNCTION 7 gbt_int4_same (gbtreekey8, gbtreekey8, internal), + FUNCTION 8 gbt_int4_distance (internal, int4, int2, oid, internal), + FUNCTION 9 gbt_int4_fetch (internal), + FUNCTION 11 gbt_int4_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey8; -ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD - OPERATOR 6 <> (int4, int4) , - OPERATOR 15 <-> (int4, int4) FOR ORDER BY pg_catalog.integer_ops , - FUNCTION 8 (int4, int4) gbt_int4_distance (internal, int4, int2, oid, internal) , - FUNCTION 9 (int4, int4) gbt_int4_fetch (internal) ; - -- -- @@ -479,42 +514,47 @@ ALTER OPERATOR FAMILY gist_int4_ops USING gist ADD CREATE FUNCTION gbt_int8_consistent(internal,int8,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_distance(internal,int8,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_int8_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_int8_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_int8_ops @@ -525,6 +565,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.integer_ops , FUNCTION 1 gbt_int8_consistent (internal, int8, int2, oid, internal), FUNCTION 2 gbt_int8_union (internal, internal), FUNCTION 3 gbt_int8_compress (internal), @@ -532,13 +574,12 @@ AS FUNCTION 5 gbt_int8_penalty (internal, internal, internal), FUNCTION 6 gbt_int8_picksplit (internal, internal), FUNCTION 7 gbt_int8_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 8 gbt_int8_distance (internal, int8, int2, oid, internal), + FUNCTION 9 gbt_int8_fetch (internal), + FUNCTION 11 gbt_int8_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD - OPERATOR 6 <> (int8, int8) , - OPERATOR 15 <-> (int8, int8) FOR ORDER BY pg_catalog.integer_ops , - FUNCTION 8 (int8, int8) gbt_int8_distance (internal, int8, int2, oid, internal) , - FUNCTION 9 (int8, int8) gbt_int8_fetch (internal) ; -- -- @@ -551,42 +592,47 @@ ALTER OPERATOR FAMILY gist_int8_ops USING gist ADD CREATE FUNCTION gbt_float4_consistent(internal,float4,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_distance(internal,float4,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_union(internal, internal) RETURNS gbtreekey8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float4_same(gbtreekey8, gbtreekey8, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_float4_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_float4_ops @@ -597,6 +643,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.float_ops , FUNCTION 1 gbt_float4_consistent (internal, float4, int2, oid, internal), FUNCTION 2 gbt_float4_union (internal, internal), FUNCTION 3 gbt_float4_compress (internal), @@ -604,13 +652,12 @@ AS FUNCTION 5 gbt_float4_penalty (internal, internal, internal), FUNCTION 6 gbt_float4_picksplit (internal, internal), FUNCTION 7 gbt_float4_same (gbtreekey8, gbtreekey8, internal), + FUNCTION 8 gbt_float4_distance (internal, float4, int2, oid, internal), + FUNCTION 9 gbt_float4_fetch (internal), + FUNCTION 11 gbt_float4_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey8; -ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD - OPERATOR 6 <> (float4, float4) , - OPERATOR 15 <-> (float4, float4) FOR ORDER BY pg_catalog.float_ops , - FUNCTION 8 (float4, float4) gbt_float4_distance (internal, float4, int2, oid, internal) , - FUNCTION 9 (float4, float4) gbt_float4_fetch (internal) ; -- -- @@ -623,42 +670,47 @@ ALTER OPERATOR FAMILY gist_float4_ops USING gist ADD CREATE FUNCTION gbt_float8_consistent(internal,float8,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_distance(internal,float8,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_float8_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_float8_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_float8_ops @@ -669,6 +721,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.float_ops , FUNCTION 1 gbt_float8_consistent (internal, float8, int2, oid, internal), FUNCTION 2 gbt_float8_union (internal, internal), FUNCTION 3 gbt_float8_compress (internal), @@ -676,13 +730,12 @@ AS FUNCTION 5 gbt_float8_penalty (internal, internal, internal), FUNCTION 6 gbt_float8_picksplit (internal, internal), FUNCTION 7 gbt_float8_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 8 gbt_float8_distance (internal, float8, int2, oid, internal), + FUNCTION 9 gbt_float8_fetch (internal), + FUNCTION 11 gbt_float8_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD - OPERATOR 6 <> (float8, float8) , - OPERATOR 15 <-> (float8, float8) FOR ORDER BY pg_catalog.float_ops , - FUNCTION 8 (float8, float8) gbt_float8_distance (internal, float8, int2, oid, internal) , - FUNCTION 9 (float8, float8) gbt_float8_fetch (internal) ; -- -- @@ -695,57 +748,62 @@ ALTER OPERATOR FAMILY gist_float8_ops USING gist ADD CREATE FUNCTION gbt_ts_consistent(internal,timestamp,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_distance(internal,timestamp,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_tstz_consistent(internal,timestamptz,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_tstz_distance(internal,timestamptz,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_tstz_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_ts_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_ts_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_timestamp_ops @@ -756,6 +814,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.interval_ops , FUNCTION 1 gbt_ts_consistent (internal, timestamp, int2, oid, internal), FUNCTION 2 gbt_ts_union (internal, internal), FUNCTION 3 gbt_ts_compress (internal), @@ -763,14 +823,12 @@ AS FUNCTION 5 gbt_ts_penalty (internal, internal, internal), FUNCTION 6 gbt_ts_picksplit (internal, internal), FUNCTION 7 gbt_ts_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 8 gbt_ts_distance (internal, timestamp, int2, oid, internal), + FUNCTION 9 gbt_ts_fetch (internal), + FUNCTION 11 gbt_ts_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_timestamp_ops USING gist ADD - OPERATOR 6 <> (timestamp, timestamp) , - OPERATOR 15 <-> (timestamp, timestamp) FOR ORDER BY pg_catalog.interval_ops , - FUNCTION 8 (timestamp, timestamp) gbt_ts_distance (internal, timestamp, int2, oid, internal) , - FUNCTION 9 (timestamp, timestamp) gbt_ts_fetch (internal) ; - -- Create the operator class CREATE OPERATOR CLASS gist_timestamptz_ops DEFAULT FOR TYPE timestamptz USING gist @@ -780,6 +838,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.interval_ops , FUNCTION 1 gbt_tstz_consistent (internal, timestamptz, int2, oid, internal), FUNCTION 2 gbt_ts_union (internal, internal), FUNCTION 3 gbt_tstz_compress (internal), @@ -787,13 +847,12 @@ AS FUNCTION 5 gbt_ts_penalty (internal, internal, internal), FUNCTION 6 gbt_ts_picksplit (internal, internal), FUNCTION 7 gbt_ts_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 8 gbt_tstz_distance (internal, timestamptz, int2, oid, internal), + FUNCTION 9 gbt_ts_fetch (internal), + FUNCTION 11 gbt_ts_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD - OPERATOR 6 <> (timestamptz, timestamptz) , - OPERATOR 15 <-> (timestamptz, timestamptz) FOR ORDER BY pg_catalog.interval_ops , - FUNCTION 8 (timestamptz, timestamptz) gbt_tstz_distance (internal, timestamptz, int2, oid, internal) , - FUNCTION 9 (timestamptz, timestamptz) gbt_ts_fetch (internal) ; -- -- @@ -806,52 +865,57 @@ ALTER OPERATOR FAMILY gist_timestamptz_ops USING gist ADD CREATE FUNCTION gbt_time_consistent(internal,time,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_distance(internal,time,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_timetz_consistent(internal,timetz,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_timetz_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_time_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_time_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_time_ops @@ -862,6 +926,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.interval_ops , FUNCTION 1 gbt_time_consistent (internal, time, int2, oid, internal), FUNCTION 2 gbt_time_union (internal, internal), FUNCTION 3 gbt_time_compress (internal), @@ -869,23 +935,21 @@ AS FUNCTION 5 gbt_time_penalty (internal, internal, internal), FUNCTION 6 gbt_time_picksplit (internal, internal), FUNCTION 7 gbt_time_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 8 gbt_time_distance (internal, time, int2, oid, internal), + FUNCTION 9 gbt_time_fetch (internal), + FUNCTION 11 gbt_time_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_time_ops USING gist ADD - OPERATOR 6 <> (time, time) , - OPERATOR 15 <-> (time, time) FOR ORDER BY pg_catalog.interval_ops , - FUNCTION 8 (time, time) gbt_time_distance (internal, time, int2, oid, internal) , - FUNCTION 9 (time, time) gbt_time_fetch (internal) ; - - CREATE OPERATOR CLASS gist_timetz_ops DEFAULT FOR TYPE timetz USING gist AS - OPERATOR 1 < , - OPERATOR 2 <= , - OPERATOR 3 = , - OPERATOR 4 >= , - OPERATOR 5 > , + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_timetz_consistent (internal, timetz, int2, oid, internal), FUNCTION 2 gbt_time_union (internal, internal), FUNCTION 3 gbt_timetz_compress (internal), @@ -893,11 +957,10 @@ AS FUNCTION 5 gbt_time_penalty (internal, internal, internal), FUNCTION 6 gbt_time_picksplit (internal, internal), FUNCTION 7 gbt_time_same (gbtreekey16, gbtreekey16, internal), - STORAGE gbtreekey16; - -ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD - OPERATOR 6 <> (timetz, timetz) ; -- no 'fetch' function, as the compress function is lossy. + FUNCTION 11 gbt_time_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), + STORAGE gbtreekey16; -- @@ -911,42 +974,47 @@ ALTER OPERATOR FAMILY gist_timetz_ops USING gist ADD CREATE FUNCTION gbt_date_consistent(internal,date,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_distance(internal,date,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_union(internal, internal) RETURNS gbtreekey8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_date_same(gbtreekey8, gbtreekey8, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_date_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_date_ops @@ -957,6 +1025,8 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.integer_ops , FUNCTION 1 gbt_date_consistent (internal, date, int2, oid, internal), FUNCTION 2 gbt_date_union (internal, internal), FUNCTION 3 gbt_date_compress (internal), @@ -964,14 +1034,12 @@ AS FUNCTION 5 gbt_date_penalty (internal, internal, internal), FUNCTION 6 gbt_date_picksplit (internal, internal), FUNCTION 7 gbt_date_same (gbtreekey8, gbtreekey8, internal), + FUNCTION 8 gbt_date_distance (internal, date, int2, oid, internal), + FUNCTION 9 gbt_date_fetch (internal), + FUNCTION 11 gbt_date_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey8; -ALTER OPERATOR FAMILY gist_date_ops USING gist ADD - OPERATOR 6 <> (date, date) , - OPERATOR 15 <-> (date, date) FOR ORDER BY pg_catalog.integer_ops , - FUNCTION 8 (date, date) gbt_date_distance (internal, date, int2, oid, internal) , - FUNCTION 9 (date, date) gbt_date_fetch (internal) ; - -- -- @@ -984,57 +1052,64 @@ ALTER OPERATOR FAMILY gist_date_ops USING gist ADD CREATE FUNCTION gbt_intv_consistent(internal,interval,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_distance(internal,interval,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_decompress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_union(internal, internal) RETURNS gbtreekey32 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_intv_same(gbtreekey32, gbtreekey32, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_intv_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_interval_ops DEFAULT FOR TYPE interval USING gist AS - OPERATOR 1 < , + OPERATOR 1 < , OPERATOR 2 <= , - OPERATOR 3 = , + OPERATOR 3 = , OPERATOR 4 >= , - OPERATOR 5 > , + OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.interval_ops , FUNCTION 1 gbt_intv_consistent (internal, interval, int2, oid, internal), FUNCTION 2 gbt_intv_union (internal, internal), FUNCTION 3 gbt_intv_compress (internal), @@ -1042,14 +1117,12 @@ AS FUNCTION 5 gbt_intv_penalty (internal, internal, internal), FUNCTION 6 gbt_intv_picksplit (internal, internal), FUNCTION 7 gbt_intv_same (gbtreekey32, gbtreekey32, internal), + FUNCTION 8 gbt_intv_distance (internal, interval, int2, oid, internal), + FUNCTION 9 gbt_intv_fetch (internal), + FUNCTION 11 gbt_intv_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey32; -ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD - OPERATOR 6 <> (interval, interval) , - OPERATOR 15 <-> (interval, interval) FOR ORDER BY pg_catalog.interval_ops , - FUNCTION 8 (interval, interval) gbt_intv_distance (internal, interval, int2, oid, internal) , - FUNCTION 9 (interval, interval) gbt_intv_fetch (internal) ; - -- -- @@ -1062,52 +1135,59 @@ ALTER OPERATOR FAMILY gist_interval_ops USING gist ADD CREATE FUNCTION gbt_cash_consistent(internal,money,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_distance(internal,money,int2,oid,internal) RETURNS float8 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_cash_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_cash_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_cash_ops DEFAULT FOR TYPE money USING gist AS - OPERATOR 1 < , + OPERATOR 1 < , OPERATOR 2 <= , - OPERATOR 3 = , + OPERATOR 3 = , OPERATOR 4 >= , - OPERATOR 5 > , + OPERATOR 5 > , + OPERATOR 6 <> , + OPERATOR 15 <-> FOR ORDER BY pg_catalog.money_ops , FUNCTION 1 gbt_cash_consistent (internal, money, int2, oid, internal), FUNCTION 2 gbt_cash_union (internal, internal), FUNCTION 3 gbt_cash_compress (internal), @@ -1115,14 +1195,12 @@ AS FUNCTION 5 gbt_cash_penalty (internal, internal, internal), FUNCTION 6 gbt_cash_picksplit (internal, internal), FUNCTION 7 gbt_cash_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 8 gbt_cash_distance (internal, money, int2, oid, internal), + FUNCTION 9 gbt_cash_fetch (internal), + FUNCTION 11 gbt_cash_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD - OPERATOR 6 <> (money, money) , - OPERATOR 15 <-> (money, money) FOR ORDER BY pg_catalog.money_ops , - FUNCTION 8 (money, money) gbt_cash_distance (internal, money, int2, oid, internal) , - FUNCTION 9 (money, money) gbt_cash_fetch (internal) ; - -- -- @@ -1135,47 +1213,53 @@ ALTER OPERATOR FAMILY gist_cash_ops USING gist ADD CREATE FUNCTION gbt_macad_consistent(internal,macaddr,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_macad_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_macad_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_macad_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_macad_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_macad_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_macad_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macaddr_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_macaddr_ops DEFAULT FOR TYPE macaddr USING gist AS - OPERATOR 1 < , + OPERATOR 1 < , OPERATOR 2 <= , - OPERATOR 3 = , + OPERATOR 3 = , OPERATOR 4 >= , - OPERATOR 5 > , + OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_macad_consistent (internal, macaddr, int2, oid, internal), FUNCTION 2 gbt_macad_union (internal, internal), FUNCTION 3 gbt_macad_compress (internal), @@ -1183,17 +1267,16 @@ AS FUNCTION 5 gbt_macad_penalty (internal, internal, internal), FUNCTION 6 gbt_macad_picksplit (internal, internal), FUNCTION 7 gbt_macad_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 9 gbt_macad_fetch (internal), + FUNCTION 11 gbt_macaddr_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD - OPERATOR 6 <> (macaddr, macaddr) , - FUNCTION 9 (macaddr, macaddr) gbt_macad_fetch (internal); - -- -- -- --- text/ bpchar ops +-- text/bpchar ops -- -- -- @@ -1201,42 +1284,52 @@ ALTER OPERATOR FAMILY gist_macaddr_ops USING gist ADD CREATE FUNCTION gbt_text_consistent(internal,text,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bpchar_consistent(internal,bpchar,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_text_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bpchar_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_text_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_text_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_text_union(internal, internal) RETURNS gbtreekey_var AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_text_same(gbtreekey_var, gbtreekey_var, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_text_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bpchar_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_text_ops @@ -1247,6 +1340,7 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_text_consistent (internal, text, int2, oid, internal), FUNCTION 2 gbt_text_union (internal, internal), FUNCTION 3 gbt_text_compress (internal), @@ -1254,13 +1348,11 @@ AS FUNCTION 5 gbt_text_penalty (internal, internal, internal), FUNCTION 6 gbt_text_picksplit (internal, internal), FUNCTION 7 gbt_text_same (gbtreekey_var, gbtreekey_var, internal), + FUNCTION 9 gbt_var_fetch (internal), + FUNCTION 11 gbt_text_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey_var; -ALTER OPERATOR FAMILY gist_text_ops USING gist ADD - OPERATOR 6 <> (text, text) , - FUNCTION 9 (text, text) gbt_var_fetch (internal) ; - - ---- Create the operator class CREATE OPERATOR CLASS gist_bpchar_ops DEFAULT FOR TYPE bpchar USING gist @@ -1270,6 +1362,7 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_bpchar_consistent (internal, bpchar , int2, oid, internal), FUNCTION 2 gbt_text_union (internal, internal), FUNCTION 3 gbt_bpchar_compress (internal), @@ -1277,11 +1370,11 @@ AS FUNCTION 5 gbt_text_penalty (internal, internal, internal), FUNCTION 6 gbt_text_picksplit (internal, internal), FUNCTION 7 gbt_text_same (gbtreekey_var, gbtreekey_var, internal), + FUNCTION 9 gbt_var_fetch (internal), + FUNCTION 11 gbt_bpchar_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey_var; -ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD - OPERATOR 6 <> (bpchar, bpchar) , - FUNCTION 9 (bpchar, bpchar) gbt_var_fetch (internal) ; -- -- @@ -1293,32 +1386,37 @@ ALTER OPERATOR FAMILY gist_bpchar_ops USING gist ADD CREATE FUNCTION gbt_bytea_consistent(internal,bytea,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bytea_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bytea_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bytea_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bytea_union(internal, internal) RETURNS gbtreekey_var AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bytea_same(gbtreekey_var, gbtreekey_var, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bytea_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_bytea_ops @@ -1329,6 +1427,7 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_bytea_consistent (internal, bytea, int2, oid, internal), FUNCTION 2 gbt_bytea_union (internal, internal), FUNCTION 3 gbt_bytea_compress (internal), @@ -1336,12 +1435,11 @@ AS FUNCTION 5 gbt_bytea_penalty (internal, internal, internal), FUNCTION 6 gbt_bytea_picksplit (internal, internal), FUNCTION 7 gbt_bytea_same (gbtreekey_var, gbtreekey_var, internal), + FUNCTION 9 gbt_var_fetch (internal), + FUNCTION 11 gbt_bytea_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey_var; -ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD - OPERATOR 6 <> (bytea, bytea) , - FUNCTION 9 (bytea, bytea) gbt_var_fetch (internal) ; - -- -- @@ -1354,32 +1452,37 @@ ALTER OPERATOR FAMILY gist_bytea_ops USING gist ADD CREATE FUNCTION gbt_numeric_consistent(internal,numeric,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_numeric_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_numeric_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_numeric_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_numeric_union(internal, internal) RETURNS gbtreekey_var AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_numeric_same(gbtreekey_var, gbtreekey_var, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_numeric_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_numeric_ops @@ -1390,6 +1493,7 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_numeric_consistent (internal, numeric, int2, oid, internal), FUNCTION 2 gbt_numeric_union (internal, internal), FUNCTION 3 gbt_numeric_compress (internal), @@ -1397,12 +1501,11 @@ AS FUNCTION 5 gbt_numeric_penalty (internal, internal, internal), FUNCTION 6 gbt_numeric_picksplit (internal, internal), FUNCTION 7 gbt_numeric_same (gbtreekey_var, gbtreekey_var, internal), + FUNCTION 9 gbt_var_fetch (internal), + FUNCTION 11 gbt_numeric_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey_var; -ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD - OPERATOR 6 <> (numeric, numeric) , - FUNCTION 9 (numeric, numeric) gbt_var_fetch (internal) ; - -- -- @@ -1414,32 +1517,42 @@ ALTER OPERATOR FAMILY gist_numeric_ops USING gist ADD CREATE FUNCTION gbt_bit_consistent(internal,bit,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bit_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bit_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bit_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bit_union(internal, internal) RETURNS gbtreekey_var AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_bit_same(gbtreekey_var, gbtreekey_var, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bit_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_varbit_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; -- Create the operator class CREATE OPERATOR CLASS gist_bit_ops @@ -1450,6 +1563,7 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_bit_consistent (internal, bit, int2, oid, internal), FUNCTION 2 gbt_bit_union (internal, internal), FUNCTION 3 gbt_bit_compress (internal), @@ -1457,13 +1571,11 @@ AS FUNCTION 5 gbt_bit_penalty (internal, internal, internal), FUNCTION 6 gbt_bit_picksplit (internal, internal), FUNCTION 7 gbt_bit_same (gbtreekey_var, gbtreekey_var, internal), + FUNCTION 9 gbt_var_fetch (internal), + FUNCTION 11 gbt_bit_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey_var; -ALTER OPERATOR FAMILY gist_bit_ops USING gist ADD - OPERATOR 6 <> (bit, bit) , - FUNCTION 9 (bit, bit) gbt_var_fetch (internal) ; - - -- Create the operator class CREATE OPERATOR CLASS gist_vbit_ops DEFAULT FOR TYPE varbit USING gist @@ -1473,6 +1585,7 @@ AS OPERATOR 3 = , OPERATOR 4 >= , OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_bit_consistent (internal, bit, int2, oid, internal), FUNCTION 2 gbt_bit_union (internal, internal), FUNCTION 3 gbt_bit_compress (internal), @@ -1480,12 +1593,11 @@ AS FUNCTION 5 gbt_bit_penalty (internal, internal, internal), FUNCTION 6 gbt_bit_picksplit (internal, internal), FUNCTION 7 gbt_bit_same (gbtreekey_var, gbtreekey_var, internal), + FUNCTION 9 gbt_var_fetch (internal), + FUNCTION 11 gbt_varbit_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey_var; -ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD - OPERATOR 6 <> (varbit, varbit) , - FUNCTION 9 (varbit, varbit) gbt_var_fetch (internal) ; - -- -- @@ -1498,42 +1610,48 @@ ALTER OPERATOR FAMILY gist_vbit_ops USING gist ADD CREATE FUNCTION gbt_inet_consistent(internal,inet,int2,oid,internal) RETURNS bool AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_inet_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_inet_penalty(internal,internal,internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_inet_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_inet_union(internal, internal) RETURNS gbtreekey16 AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; CREATE FUNCTION gbt_inet_same(gbtreekey16, gbtreekey16, internal) RETURNS internal AS 'MODULE_PATHNAME' -LANGUAGE C IMMUTABLE STRICT; +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; --- Create the operator class +CREATE FUNCTION gbt_inet_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- Create the operator class (intentionally not DEFAULT) CREATE OPERATOR CLASS gist_inet_ops -DEFAULT FOR TYPE inet USING gist +FOR TYPE inet USING gist AS - OPERATOR 1 < , - OPERATOR 2 <= , - OPERATOR 3 = , - OPERATOR 4 >= , - OPERATOR 5 > , + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + OPERATOR 6 <> , FUNCTION 1 gbt_inet_consistent (internal, inet, int2, oid, internal), FUNCTION 2 gbt_inet_union (internal, internal), FUNCTION 3 gbt_inet_compress (internal), @@ -1541,21 +1659,21 @@ AS FUNCTION 5 gbt_inet_penalty (internal, internal, internal), FUNCTION 6 gbt_inet_picksplit (internal, internal), FUNCTION 7 gbt_inet_same (gbtreekey16, gbtreekey16, internal), - STORAGE gbtreekey16; - -ALTER OPERATOR FAMILY gist_inet_ops USING gist ADD - OPERATOR 6 <> (inet, inet) ; -- no fetch support, the compress function is lossy + FUNCTION 11 gbt_inet_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), + STORAGE gbtreekey16; --- Create the operator class +-- Create the operator class (intentionally not DEFAULT) CREATE OPERATOR CLASS gist_cidr_ops -DEFAULT FOR TYPE cidr USING gist +FOR TYPE cidr USING gist AS - OPERATOR 1 < (inet, inet) , - OPERATOR 2 <= (inet, inet) , - OPERATOR 3 = (inet, inet) , - OPERATOR 4 >= (inet, inet) , - OPERATOR 5 > (inet, inet) , + OPERATOR 1 < (inet, inet) , + OPERATOR 2 <= (inet, inet) , + OPERATOR 3 = (inet, inet) , + OPERATOR 4 >= (inet, inet) , + OPERATOR 5 > (inet, inet) , + OPERATOR 6 <> (inet, inet) , FUNCTION 1 gbt_inet_consistent (internal, inet, int2, oid, internal), FUNCTION 2 gbt_inet_union (internal, internal), FUNCTION 3 gbt_inet_compress (internal), @@ -1563,8 +1681,291 @@ AS FUNCTION 5 gbt_inet_penalty (internal, internal, internal), FUNCTION 6 gbt_inet_picksplit (internal, internal), FUNCTION 7 gbt_inet_same (gbtreekey16, gbtreekey16, internal), + -- no fetch support, the compress function is lossy + FUNCTION 11 gbt_inet_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), STORAGE gbtreekey16; -ALTER OPERATOR FAMILY gist_cidr_ops USING gist ADD - OPERATOR 6 <> (inet, inet) ; - -- no fetch support, the compress function is lossy + +-- +-- +-- +-- uuid ops +-- +-- +-- +-- define the GiST support methods +CREATE FUNCTION gbt_uuid_consistent(internal,uuid,int2,oid,internal) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_fetch(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_compress(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_penalty(internal,internal,internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_picksplit(internal, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_union(internal, internal) +RETURNS gbtreekey32 +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_same(gbtreekey32, gbtreekey32, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_uuid_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- Create the operator class +CREATE OPERATOR CLASS gist_uuid_ops +DEFAULT FOR TYPE uuid USING gist +AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + OPERATOR 6 <> , + FUNCTION 1 gbt_uuid_consistent (internal, uuid, int2, oid, internal), + FUNCTION 2 gbt_uuid_union (internal, internal), + FUNCTION 3 gbt_uuid_compress (internal), + FUNCTION 4 gbt_decompress (internal), + FUNCTION 5 gbt_uuid_penalty (internal, internal, internal), + FUNCTION 6 gbt_uuid_picksplit (internal, internal), + FUNCTION 7 gbt_uuid_same (gbtreekey32, gbtreekey32, internal), + FUNCTION 9 gbt_uuid_fetch (internal), + FUNCTION 11 gbt_uuid_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), + STORAGE gbtreekey32; + + +-- +-- +-- +-- macaddr8 ops +-- +-- +-- +-- define the GiST support methods +CREATE FUNCTION gbt_macad8_consistent(internal,macaddr8,int2,oid,internal) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_compress(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_fetch(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_penalty(internal,internal,internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_picksplit(internal, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_union(internal, internal) +RETURNS gbtreekey16 +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_same(gbtreekey16, gbtreekey16, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_macad8_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- Create the operator class +CREATE OPERATOR CLASS gist_macaddr8_ops +DEFAULT FOR TYPE macaddr8 USING gist +AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + OPERATOR 6 <> , + FUNCTION 1 gbt_macad8_consistent (internal, macaddr8, int2, oid, internal), + FUNCTION 2 gbt_macad8_union (internal, internal), + FUNCTION 3 gbt_macad8_compress (internal), + FUNCTION 4 gbt_decompress (internal), + FUNCTION 5 gbt_macad8_penalty (internal, internal, internal), + FUNCTION 6 gbt_macad8_picksplit (internal, internal), + FUNCTION 7 gbt_macad8_same (gbtreekey16, gbtreekey16, internal), + FUNCTION 9 gbt_macad8_fetch (internal), + FUNCTION 11 gbt_macad8_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), + STORAGE gbtreekey16; + + +-- +-- +-- +-- enum ops +-- +-- +-- +-- define the GiST support methods +CREATE FUNCTION gbt_enum_consistent(internal,anyenum,int2,oid,internal) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_compress(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_fetch(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_penalty(internal,internal,internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_picksplit(internal, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_union(internal, internal) +RETURNS gbtreekey8 +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_same(gbtreekey8, gbtreekey8, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_enum_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- Create the operator class +CREATE OPERATOR CLASS gist_enum_ops +DEFAULT FOR TYPE anyenum USING gist +AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + OPERATOR 6 <> , + FUNCTION 1 gbt_enum_consistent (internal, anyenum, int2, oid, internal), + FUNCTION 2 gbt_enum_union (internal, internal), + FUNCTION 3 gbt_enum_compress (internal), + FUNCTION 4 gbt_decompress (internal), + FUNCTION 5 gbt_enum_penalty (internal, internal, internal), + FUNCTION 6 gbt_enum_picksplit (internal, internal), + FUNCTION 7 gbt_enum_same (gbtreekey8, gbtreekey8, internal), + FUNCTION 9 gbt_enum_fetch (internal), + FUNCTION 11 gbt_enum_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), + STORAGE gbtreekey8; + + +-- +-- +-- +-- bool ops +-- +-- +-- +-- Define the GiST support methods +CREATE FUNCTION gbt_bool_consistent(internal,bool,int2,oid,internal) +RETURNS bool +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_compress(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_fetch(internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_penalty(internal,internal,internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_picksplit(internal, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_union(internal, internal) +RETURNS gbtreekey2 +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_same(gbtreekey2, gbtreekey2, internal) +RETURNS internal +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +CREATE FUNCTION gbt_bool_sortsupport(internal) +RETURNS void +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; + +-- Create the operator class +CREATE OPERATOR CLASS gist_bool_ops +DEFAULT FOR TYPE bool USING gist +AS + OPERATOR 1 < , + OPERATOR 2 <= , + OPERATOR 3 = , + OPERATOR 4 >= , + OPERATOR 5 > , + OPERATOR 6 <> , + FUNCTION 1 gbt_bool_consistent (internal, bool, int2, oid, internal), + FUNCTION 2 gbt_bool_union (internal, internal), + FUNCTION 3 gbt_bool_compress (internal), + FUNCTION 4 gbt_decompress (internal), + FUNCTION 5 gbt_bool_penalty (internal, internal, internal), + FUNCTION 6 gbt_bool_picksplit (internal, internal), + FUNCTION 7 gbt_bool_same (gbtreekey2, gbtreekey2, internal), + FUNCTION 9 gbt_bool_fetch (internal), + FUNCTION 11 gbt_bool_sortsupport (internal), + FUNCTION 12 ("any", "any") gist_translate_cmptype_btree (int), + STORAGE gbtreekey2; diff --git a/contrib/btree_gist/btree_gist.c b/contrib/btree_gist/btree_gist.c index 280ce808456b9..39fcbdad334f0 100644 --- a/contrib/btree_gist/btree_gist.c +++ b/contrib/btree_gist/btree_gist.c @@ -15,7 +15,7 @@ PG_MODULE_MAGIC_EXT( PG_FUNCTION_INFO_V1(gbt_decompress); PG_FUNCTION_INFO_V1(gbtreekey_in); PG_FUNCTION_INFO_V1(gbtreekey_out); -PG_FUNCTION_INFO_V1(gist_stratnum_btree); +PG_FUNCTION_INFO_V1(gist_translate_cmptype_btree); /************************************************** * In/Out for keys @@ -62,7 +62,7 @@ gbt_decompress(PG_FUNCTION_ARGS) * Returns the btree number for supported operators, otherwise invalid. */ Datum -gist_stratnum_btree(PG_FUNCTION_ARGS) +gist_translate_cmptype_btree(PG_FUNCTION_ARGS) { CompareType cmptype = PG_GETARG_INT32(0); diff --git a/contrib/btree_gist/btree_inet.c b/contrib/btree_gist/btree_inet.c index 8b23853bafbb7..9d04b92d3b8d9 100644 --- a/contrib/btree_gist/btree_inet.c +++ b/contrib/btree_gist/btree_inet.c @@ -7,6 +7,7 @@ #include "btree_utils_num.h" #include "catalog/pg_type.h" #include "utils/builtins.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct inetkey @@ -96,10 +97,10 @@ gbt_inet_compress(PG_FUNCTION_ARGS) if (entry->leafkey) { - inetKEY *r = (inetKEY *) palloc(sizeof(inetKEY)); + inetKEY *r = palloc_object(inetKEY); bool failure = false; - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); r->lower = convert_network_to_scalar(entry->key, INETOID, &failure); Assert(!failure); r->upper = r->lower; @@ -119,8 +120,9 @@ gbt_inet_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Datum dquery = PG_GETARG_DATUM(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); inetKEY *kkk = (inetKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_int2.c b/contrib/btree_gist/btree_int2.c index 33eccdedd7049..cc4b33177e343 100644 --- a/contrib/btree_gist/btree_int2.c +++ b/contrib/btree_gist/btree_int2.c @@ -6,6 +6,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "common/int.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct int16key @@ -138,8 +139,9 @@ gbt_int2_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); int16 query = PG_GETARG_INT16(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int16KEY *kkk = (int16KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -159,8 +161,9 @@ gbt_int2_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); int16 query = PG_GETARG_INT16(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif int16KEY *kkk = (int16KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_int4.c b/contrib/btree_gist/btree_int4.c index a82cee9a58a8c..47790578e6bcd 100644 --- a/contrib/btree_gist/btree_int4.c +++ b/contrib/btree_gist/btree_int4.c @@ -5,6 +5,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "common/int.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct int32key @@ -136,8 +137,9 @@ gbt_int4_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); int32 query = PG_GETARG_INT32(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int32KEY *kkk = (int32KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -157,8 +159,9 @@ gbt_int4_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); int32 query = PG_GETARG_INT32(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif int32KEY *kkk = (int32KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_int8.c b/contrib/btree_gist/btree_int8.c index f0c56e017269a..f48122c8d84b6 100644 --- a/contrib/btree_gist/btree_int8.c +++ b/contrib/btree_gist/btree_int8.c @@ -6,6 +6,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "common/int.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct int64key @@ -138,8 +139,9 @@ gbt_int8_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); int64 query = PG_GETARG_INT64(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int64KEY *kkk = (int64KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -159,8 +161,9 @@ gbt_int8_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); int64 query = PG_GETARG_INT64(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif int64KEY *kkk = (int64KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_interval.c b/contrib/btree_gist/btree_interval.c index b5e365c6e09b4..6b81fa2b39bdd 100644 --- a/contrib/btree_gist/btree_interval.c +++ b/contrib/btree_gist/btree_interval.c @@ -6,6 +6,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "utils/fmgrprotos.h" +#include "utils/rel.h" #include "utils/sortsupport.h" #include "utils/timestamp.h" @@ -149,7 +150,7 @@ gbt_intv_compress(PG_FUNCTION_ARGS) { char *r = (char *) palloc(2 * INTERVALSIZE); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); if (entry->leafkey) { @@ -189,10 +190,10 @@ gbt_intv_decompress(PG_FUNCTION_ARGS) if (INTERVALSIZE != sizeof(Interval)) { - intvKEY *r = palloc(sizeof(intvKEY)); + intvKEY *r = palloc_object(intvKEY); char *key = DatumGetPointer(entry->key); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); memcpy(&r->lower, key, INTERVALSIZE); memcpy(&r->upper, key + INTERVALSIZE, INTERVALSIZE); @@ -210,8 +211,9 @@ gbt_intv_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Interval *query = PG_GETARG_INTERVAL_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); intvKEY *kkk = (intvKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -232,8 +234,9 @@ gbt_intv_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Interval *query = PG_GETARG_INTERVAL_P(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif intvKEY *kkk = (intvKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_macaddr.c b/contrib/btree_gist/btree_macaddr.c index 3b2f26719d5dc..df7c0040011a8 100644 --- a/contrib/btree_gist/btree_macaddr.c +++ b/contrib/btree_gist/btree_macaddr.c @@ -7,6 +7,7 @@ #include "btree_utils_num.h" #include "utils/fmgrprotos.h" #include "utils/inet.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct @@ -125,8 +126,9 @@ gbt_macad_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); macaddr *query = (macaddr *) PG_GETARG_POINTER(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); macKEY *kkk = (macKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_macaddr8.c b/contrib/btree_gist/btree_macaddr8.c index f2b104617e680..3b0c9b81e854c 100644 --- a/contrib/btree_gist/btree_macaddr8.c +++ b/contrib/btree_gist/btree_macaddr8.c @@ -7,6 +7,7 @@ #include "btree_utils_num.h" #include "utils/fmgrprotos.h" #include "utils/inet.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct @@ -124,8 +125,9 @@ gbt_macad8_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); macaddr8 *query = (macaddr8 *) PG_GETARG_POINTER(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); mac8KEY *kkk = (mac8KEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_numeric.c b/contrib/btree_gist/btree_numeric.c index a39c05d9da1cf..dba04c3a1b3bd 100644 --- a/contrib/btree_gist/btree_numeric.c +++ b/contrib/btree_gist/btree_numeric.c @@ -3,7 +3,6 @@ */ #include "postgres.h" -#include #include #include "btree_gist.h" @@ -107,8 +106,9 @@ gbt_numeric_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); void *query = DatumGetNumeric(PG_GETARG_DATUM(1)); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool retval; GBT_VARKEY *key = (GBT_VARKEY *) DatumGetPointer(entry->key); @@ -192,7 +192,7 @@ gbt_numeric_penalty(PG_FUNCTION_ARGS) *result = 0.0; - if (DirectFunctionCall2(numeric_gt, NumericGetDatum(ds), NumericGetDatum(nul))) + if (DatumGetBool(DirectFunctionCall2(numeric_gt, NumericGetDatum(ds), NumericGetDatum(nul)))) { *result += FLT_MIN; os = DatumGetNumeric(DirectFunctionCall2(numeric_div, diff --git a/contrib/btree_gist/btree_oid.c b/contrib/btree_gist/btree_oid.c index ffe0d7983e40f..3ddf2a993d416 100644 --- a/contrib/btree_gist/btree_oid.c +++ b/contrib/btree_gist/btree_oid.c @@ -5,6 +5,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct @@ -138,8 +139,9 @@ gbt_oid_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Oid query = PG_GETARG_OID(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); oidKEY *kkk = (oidKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -159,8 +161,9 @@ gbt_oid_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Oid query = PG_GETARG_OID(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif oidKEY *kkk = (oidKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/btree_text.c b/contrib/btree_gist/btree_text.c index ddee42504a192..2ac12f1cab275 100644 --- a/contrib/btree_gist/btree_text.c +++ b/contrib/btree_gist/btree_text.c @@ -193,8 +193,9 @@ gbt_text_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); void *query = DatumGetTextP(PG_GETARG_DATUM(1)); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool retval; GBT_VARKEY *key = (GBT_VARKEY *) DatumGetPointer(entry->key); @@ -220,8 +221,9 @@ gbt_bpchar_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); void *query = DatumGetTextP(PG_GETARG_DATUM(1)); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool retval; GBT_VARKEY *key = (GBT_VARKEY *) DatumGetPointer(entry->key); diff --git a/contrib/btree_gist/btree_time.c b/contrib/btree_gist/btree_time.c index 1dba95057ba9f..9d23fb4b867eb 100644 --- a/contrib/btree_gist/btree_time.c +++ b/contrib/btree_gist/btree_time.c @@ -7,6 +7,7 @@ #include "btree_utils_num.h" #include "utils/fmgrprotos.h" #include "utils/date.h" +#include "utils/rel.h" #include "utils/sortsupport.h" #include "utils/timestamp.h" @@ -31,13 +32,6 @@ PG_FUNCTION_INFO_V1(gbt_time_sortsupport); PG_FUNCTION_INFO_V1(gbt_timetz_sortsupport); -#ifdef USE_FLOAT8_BYVAL -#define TimeADTGetDatumFast(X) TimeADTGetDatum(X) -#else -#define TimeADTGetDatumFast(X) PointerGetDatum(&(X)) -#endif - - static bool gbt_timegt(const void *a, const void *b, FmgrInfo *flinfo) { @@ -45,8 +39,8 @@ gbt_timegt(const void *a, const void *b, FmgrInfo *flinfo) const TimeADT *bb = (const TimeADT *) b; return DatumGetBool(DirectFunctionCall2(time_gt, - TimeADTGetDatumFast(*aa), - TimeADTGetDatumFast(*bb))); + TimeADTGetDatum(*aa), + TimeADTGetDatum(*bb))); } static bool @@ -56,8 +50,8 @@ gbt_timege(const void *a, const void *b, FmgrInfo *flinfo) const TimeADT *bb = (const TimeADT *) b; return DatumGetBool(DirectFunctionCall2(time_ge, - TimeADTGetDatumFast(*aa), - TimeADTGetDatumFast(*bb))); + TimeADTGetDatum(*aa), + TimeADTGetDatum(*bb))); } static bool @@ -67,8 +61,8 @@ gbt_timeeq(const void *a, const void *b, FmgrInfo *flinfo) const TimeADT *bb = (const TimeADT *) b; return DatumGetBool(DirectFunctionCall2(time_eq, - TimeADTGetDatumFast(*aa), - TimeADTGetDatumFast(*bb))); + TimeADTGetDatum(*aa), + TimeADTGetDatum(*bb))); } static bool @@ -78,8 +72,8 @@ gbt_timele(const void *a, const void *b, FmgrInfo *flinfo) const TimeADT *bb = (const TimeADT *) b; return DatumGetBool(DirectFunctionCall2(time_le, - TimeADTGetDatumFast(*aa), - TimeADTGetDatumFast(*bb))); + TimeADTGetDatum(*aa), + TimeADTGetDatum(*bb))); } static bool @@ -89,8 +83,8 @@ gbt_timelt(const void *a, const void *b, FmgrInfo *flinfo) const TimeADT *bb = (const TimeADT *) b; return DatumGetBool(DirectFunctionCall2(time_lt, - TimeADTGetDatumFast(*aa), - TimeADTGetDatumFast(*bb))); + TimeADTGetDatum(*aa), + TimeADTGetDatum(*bb))); } static int @@ -100,9 +94,9 @@ gbt_timekey_cmp(const void *a, const void *b, FmgrInfo *flinfo) timeKEY *ib = (timeKEY *) (((const Nsrt *) b)->t); int res; - res = DatumGetInt32(DirectFunctionCall2(time_cmp, TimeADTGetDatumFast(ia->lower), TimeADTGetDatumFast(ib->lower))); + res = DatumGetInt32(DirectFunctionCall2(time_cmp, TimeADTGetDatum(ia->lower), TimeADTGetDatum(ib->lower))); if (res == 0) - return DatumGetInt32(DirectFunctionCall2(time_cmp, TimeADTGetDatumFast(ia->upper), TimeADTGetDatumFast(ib->upper))); + return DatumGetInt32(DirectFunctionCall2(time_cmp, TimeADTGetDatum(ia->upper), TimeADTGetDatum(ib->upper))); return res; } @@ -115,8 +109,8 @@ gbt_time_dist(const void *a, const void *b, FmgrInfo *flinfo) Interval *i; i = DatumGetIntervalP(DirectFunctionCall2(time_mi_time, - TimeADTGetDatumFast(*aa), - TimeADTGetDatumFast(*bb))); + TimeADTGetDatum(*aa), + TimeADTGetDatum(*bb))); return fabs(INTERVAL_TO_SEC(i)); } @@ -168,11 +162,11 @@ gbt_timetz_compress(PG_FUNCTION_ARGS) if (entry->leafkey) { - timeKEY *r = (timeKEY *) palloc(sizeof(timeKEY)); + timeKEY *r = palloc_object(timeKEY); TimeTzADT *tz = DatumGetTimeTzADTP(entry->key); TimeADT tmp; - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); /* We are using the time + zone only to compress */ tmp = tz->time + (tz->zone * INT64CONST(1000000)); @@ -200,8 +194,9 @@ gbt_time_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TimeADT query = PG_GETARG_TIMEADT(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); timeKEY *kkk = (timeKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -221,8 +216,9 @@ gbt_time_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TimeADT query = PG_GETARG_TIMEADT(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif timeKEY *kkk = (timeKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -239,8 +235,9 @@ gbt_timetz_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TimeTzADT *query = PG_GETARG_TIMETZADT_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); timeKEY *kkk = (timeKEY *) DatumGetPointer(entry->key); TimeADT qqq; @@ -279,14 +276,14 @@ gbt_time_penalty(PG_FUNCTION_ARGS) double res2; intr = DatumGetIntervalP(DirectFunctionCall2(time_mi_time, - TimeADTGetDatumFast(newentry->upper), - TimeADTGetDatumFast(origentry->upper))); + TimeADTGetDatum(newentry->upper), + TimeADTGetDatum(origentry->upper))); res = INTERVAL_TO_SEC(intr); res = Max(res, 0); intr = DatumGetIntervalP(DirectFunctionCall2(time_mi_time, - TimeADTGetDatumFast(origentry->lower), - TimeADTGetDatumFast(newentry->lower))); + TimeADTGetDatum(origentry->lower), + TimeADTGetDatum(newentry->lower))); res2 = INTERVAL_TO_SEC(intr); res2 = Max(res2, 0); @@ -297,8 +294,8 @@ gbt_time_penalty(PG_FUNCTION_ARGS) if (res > 0) { intr = DatumGetIntervalP(DirectFunctionCall2(time_mi_time, - TimeADTGetDatumFast(origentry->upper), - TimeADTGetDatumFast(origentry->lower))); + TimeADTGetDatum(origentry->upper), + TimeADTGetDatum(origentry->lower))); *result += FLT_MIN; *result += (float) (res / (res + INTERVAL_TO_SEC(intr))); *result *= (FLT_MAX / (((GISTENTRY *) PG_GETARG_POINTER(0))->rel->rd_att->natts + 1)); @@ -334,8 +331,8 @@ gbt_timekey_ssup_cmp(Datum x, Datum y, SortSupport ssup) /* for leaf items we expect lower == upper, so only compare lower */ return DatumGetInt32(DirectFunctionCall2(time_cmp, - TimeADTGetDatumFast(arg1->lower), - TimeADTGetDatumFast(arg2->lower))); + TimeADTGetDatum(arg1->lower), + TimeADTGetDatum(arg2->lower))); } Datum diff --git a/contrib/btree_gist/btree_ts.c b/contrib/btree_gist/btree_ts.c index eb899c4d21363..0f88d5d72f4b3 100644 --- a/contrib/btree_gist/btree_ts.c +++ b/contrib/btree_gist/btree_ts.c @@ -10,6 +10,7 @@ #include "utils/fmgrprotos.h" #include "utils/timestamp.h" #include "utils/float.h" +#include "utils/rel.h" #include "utils/sortsupport.h" typedef struct @@ -33,13 +34,6 @@ PG_FUNCTION_INFO_V1(gbt_ts_same); PG_FUNCTION_INFO_V1(gbt_ts_sortsupport); -#ifdef USE_FLOAT8_BYVAL -#define TimestampGetDatumFast(X) TimestampGetDatum(X) -#else -#define TimestampGetDatumFast(X) PointerGetDatum(&(X)) -#endif - - /* define for comparison */ static bool @@ -49,8 +43,8 @@ gbt_tsgt(const void *a, const void *b, FmgrInfo *flinfo) const Timestamp *bb = (const Timestamp *) b; return DatumGetBool(DirectFunctionCall2(timestamp_gt, - TimestampGetDatumFast(*aa), - TimestampGetDatumFast(*bb))); + TimestampGetDatum(*aa), + TimestampGetDatum(*bb))); } static bool @@ -60,8 +54,8 @@ gbt_tsge(const void *a, const void *b, FmgrInfo *flinfo) const Timestamp *bb = (const Timestamp *) b; return DatumGetBool(DirectFunctionCall2(timestamp_ge, - TimestampGetDatumFast(*aa), - TimestampGetDatumFast(*bb))); + TimestampGetDatum(*aa), + TimestampGetDatum(*bb))); } static bool @@ -71,8 +65,8 @@ gbt_tseq(const void *a, const void *b, FmgrInfo *flinfo) const Timestamp *bb = (const Timestamp *) b; return DatumGetBool(DirectFunctionCall2(timestamp_eq, - TimestampGetDatumFast(*aa), - TimestampGetDatumFast(*bb))); + TimestampGetDatum(*aa), + TimestampGetDatum(*bb))); } static bool @@ -82,8 +76,8 @@ gbt_tsle(const void *a, const void *b, FmgrInfo *flinfo) const Timestamp *bb = (const Timestamp *) b; return DatumGetBool(DirectFunctionCall2(timestamp_le, - TimestampGetDatumFast(*aa), - TimestampGetDatumFast(*bb))); + TimestampGetDatum(*aa), + TimestampGetDatum(*bb))); } static bool @@ -93,8 +87,8 @@ gbt_tslt(const void *a, const void *b, FmgrInfo *flinfo) const Timestamp *bb = (const Timestamp *) b; return DatumGetBool(DirectFunctionCall2(timestamp_lt, - TimestampGetDatumFast(*aa), - TimestampGetDatumFast(*bb))); + TimestampGetDatum(*aa), + TimestampGetDatum(*bb))); } static int @@ -104,9 +98,9 @@ gbt_tskey_cmp(const void *a, const void *b, FmgrInfo *flinfo) tsKEY *ib = (tsKEY *) (((const Nsrt *) b)->t); int res; - res = DatumGetInt32(DirectFunctionCall2(timestamp_cmp, TimestampGetDatumFast(ia->lower), TimestampGetDatumFast(ib->lower))); + res = DatumGetInt32(DirectFunctionCall2(timestamp_cmp, TimestampGetDatum(ia->lower), TimestampGetDatum(ib->lower))); if (res == 0) - return DatumGetInt32(DirectFunctionCall2(timestamp_cmp, TimestampGetDatumFast(ia->upper), TimestampGetDatumFast(ib->upper))); + return DatumGetInt32(DirectFunctionCall2(timestamp_cmp, TimestampGetDatum(ia->upper), TimestampGetDatum(ib->upper))); return res; } @@ -122,8 +116,8 @@ gbt_ts_dist(const void *a, const void *b, FmgrInfo *flinfo) return get_float8_infinity(); i = DatumGetIntervalP(DirectFunctionCall2(timestamp_mi, - TimestampGetDatumFast(*aa), - TimestampGetDatumFast(*bb))); + TimestampGetDatum(*aa), + TimestampGetDatum(*bb))); return fabs(INTERVAL_TO_SEC(i)); } @@ -152,7 +146,7 @@ ts_dist(PG_FUNCTION_ARGS) if (TIMESTAMP_NOT_FINITE(a) || TIMESTAMP_NOT_FINITE(b)) { - Interval *p = palloc(sizeof(Interval)); + Interval *p = palloc_object(Interval); p->day = INT_MAX; p->month = INT_MAX; @@ -176,7 +170,7 @@ tstz_dist(PG_FUNCTION_ARGS) if (TIMESTAMP_NOT_FINITE(a) || TIMESTAMP_NOT_FINITE(b)) { - Interval *p = palloc(sizeof(Interval)); + Interval *p = palloc_object(Interval); p->day = INT_MAX; p->month = INT_MAX; @@ -218,13 +212,13 @@ gbt_tstz_compress(PG_FUNCTION_ARGS) if (entry->leafkey) { - tsKEY *r = (tsKEY *) palloc(sizeof(tsKEY)); + tsKEY *r = palloc_object(tsKEY); TimestampTz ts = DatumGetTimestampTz(entry->key); Timestamp gmt; gmt = tstz_to_ts_gmt(ts); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); r->lower = r->upper = gmt; gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, @@ -250,8 +244,9 @@ gbt_ts_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Timestamp query = PG_GETARG_TIMESTAMP(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); tsKEY *kkk = (tsKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -271,8 +266,9 @@ gbt_ts_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Timestamp query = PG_GETARG_TIMESTAMP(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif tsKEY *kkk = (tsKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -289,8 +285,9 @@ gbt_tstz_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TimestampTz query = PG_GETARG_TIMESTAMPTZ(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); char *kkk = (char *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; @@ -312,8 +309,9 @@ gbt_tstz_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TimestampTz query = PG_GETARG_TIMESTAMPTZ(1); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif char *kkk = (char *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; Timestamp qqq; @@ -404,8 +402,8 @@ gbt_ts_ssup_cmp(Datum x, Datum y, SortSupport ssup) /* for leaf items we expect lower == upper, so only compare lower */ return DatumGetInt32(DirectFunctionCall2(timestamp_cmp, - TimestampGetDatumFast(arg1->lower), - TimestampGetDatumFast(arg2->lower))); + TimestampGetDatum(arg1->lower), + TimestampGetDatum(arg2->lower))); } Datum diff --git a/contrib/btree_gist/btree_utils_num.c b/contrib/btree_gist/btree_utils_num.c index 346ee837d75f4..51c8836f27a3d 100644 --- a/contrib/btree_gist/btree_utils_num.c +++ b/contrib/btree_gist/btree_utils_num.c @@ -89,7 +89,7 @@ gbt_num_compress(GISTENTRY *entry, const gbtree_ninfo *tinfo) memcpy(&r[0], leaf, tinfo->size); memcpy(&r[tinfo->size], leaf, tinfo->size); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, false); } @@ -119,44 +119,44 @@ gbt_num_fetch(GISTENTRY *entry, const gbtree_ninfo *tinfo) switch (tinfo->t) { case gbt_t_bool: - datum = BoolGetDatum(*(bool *) entry->key); + datum = BoolGetDatum(*(bool *) DatumGetPointer(entry->key)); break; case gbt_t_int2: - datum = Int16GetDatum(*(int16 *) entry->key); + datum = Int16GetDatum(*(int16 *) DatumGetPointer(entry->key)); break; case gbt_t_int4: - datum = Int32GetDatum(*(int32 *) entry->key); + datum = Int32GetDatum(*(int32 *) DatumGetPointer(entry->key)); break; case gbt_t_int8: - datum = Int64GetDatum(*(int64 *) entry->key); + datum = Int64GetDatum(*(int64 *) DatumGetPointer(entry->key)); break; case gbt_t_oid: case gbt_t_enum: - datum = ObjectIdGetDatum(*(Oid *) entry->key); + datum = ObjectIdGetDatum(*(Oid *) DatumGetPointer(entry->key)); break; case gbt_t_float4: - datum = Float4GetDatum(*(float4 *) entry->key); + datum = Float4GetDatum(*(float4 *) DatumGetPointer(entry->key)); break; case gbt_t_float8: - datum = Float8GetDatum(*(float8 *) entry->key); + datum = Float8GetDatum(*(float8 *) DatumGetPointer(entry->key)); break; case gbt_t_date: - datum = DateADTGetDatum(*(DateADT *) entry->key); + datum = DateADTGetDatum(*(DateADT *) DatumGetPointer(entry->key)); break; case gbt_t_time: - datum = TimeADTGetDatum(*(TimeADT *) entry->key); + datum = TimeADTGetDatum(*(TimeADT *) DatumGetPointer(entry->key)); break; case gbt_t_ts: - datum = TimestampGetDatum(*(Timestamp *) entry->key); + datum = TimestampGetDatum(*(Timestamp *) DatumGetPointer(entry->key)); break; case gbt_t_cash: - datum = CashGetDatum(*(Cash *) entry->key); + datum = CashGetDatum(*(Cash *) DatumGetPointer(entry->key)); break; default: datum = entry->key; } - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, datum, entry->rel, entry->page, entry->offset, false); return retval; @@ -181,8 +181,8 @@ gbt_num_union(GBT_NUMKEY *out, const GistEntryVector *entryvec, const gbtree_nin cur = (GBT_NUMKEY *) DatumGetPointer((entryvec->vector[0].key)); - o.lower = &((GBT_NUMKEY *) out)[0]; - o.upper = &((GBT_NUMKEY *) out)[tinfo->size]; + o.lower = &out[0]; + o.upper = &out[tinfo->size]; memcpy(out, cur, 2 * tinfo->size); diff --git a/contrib/btree_gist/btree_utils_var.c b/contrib/btree_gist/btree_utils_var.c index d9df2356cd1e4..e1945cf808f1d 100644 --- a/contrib/btree_gist/btree_utils_var.c +++ b/contrib/btree_gist/btree_utils_var.c @@ -3,7 +3,6 @@ */ #include "postgres.h" -#include #include #include @@ -11,6 +10,7 @@ #include "btree_utils_var.h" #include "mb/pg_wchar.h" #include "utils/rel.h" +#include "varatt.h" /* used for key sorting */ typedef struct @@ -39,7 +39,7 @@ gbt_var_decompress(PG_FUNCTION_ARGS) if (key != (GBT_VARKEY *) DatumGetPointer(entry->key)) { - GISTENTRY *retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + GISTENTRY *retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, @@ -70,7 +70,7 @@ gbt_var_key_readable(const GBT_VARKEY *k) * Create a leaf-entry to store in the index, from a single Datum. */ static GBT_VARKEY * -gbt_var_key_from_datum(const struct varlena *u) +gbt_var_key_from_datum(const varlena *u) { int32 lowersize = VARSIZE(u); GBT_VARKEY *r; @@ -115,36 +115,47 @@ gbt_var_leaf2node(GBT_VARKEY *leaf, const gbtree_vinfo *tinfo, FmgrInfo *flinfo) /* * returns the common prefix length of a node key + * + * If the underlying type is character data, the prefix length may point in + * the middle of a multibyte character. */ static int32 gbt_var_node_cp_len(const GBT_VARKEY *node, const gbtree_vinfo *tinfo) { GBT_VARKEY_R r = gbt_var_key_readable(node); int32 i = 0; - int32 l = 0; + int32 l_left_to_match = 0; + int32 l_total = 0; int32 t1len = VARSIZE(r.lower) - VARHDRSZ; int32 t2len = VARSIZE(r.upper) - VARHDRSZ; int32 ml = Min(t1len, t2len); char *p1 = VARDATA(r.lower); char *p2 = VARDATA(r.upper); + const char *end1 = p1 + t1len; + const char *end2 = p2 + t2len; if (ml == 0) return 0; while (i < ml) { - if (tinfo->eml > 1 && l == 0) + if (tinfo->eml > 1 && l_left_to_match == 0) { - if ((l = pg_mblen(p1)) != pg_mblen(p2)) + l_total = pg_mblen_range(p1, end1); + if (l_total != pg_mblen_range(p2, end2)) { return i; } + l_left_to_match = l_total; } if (*p1 != *p2) { if (tinfo->eml > 1) { - return (i - l + 1); + int32 l_matched_subset = l_total - l_left_to_match; + + /* end common prefix at final byte of last matching char */ + return i - l_matched_subset; } else { @@ -154,7 +165,7 @@ gbt_var_node_cp_len(const GBT_VARKEY *node, const gbtree_vinfo *tinfo) p1++; p2++; - l--; + l_left_to_match--; i++; } return ml; /* lower == upper */ @@ -283,12 +294,12 @@ gbt_var_compress(GISTENTRY *entry, const gbtree_vinfo *tinfo) if (entry->leafkey) { - struct varlena *leaf = PG_DETOAST_DATUM(entry->key); + varlena *leaf = PG_DETOAST_DATUM(entry->key); GBT_VARKEY *r; r = gbt_var_key_from_datum(leaf); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, true); @@ -308,7 +319,7 @@ gbt_var_fetch(PG_FUNCTION_ARGS) GBT_VARKEY_R r = gbt_var_key_readable(key); GISTENTRY *retval; - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r.lower), entry->rel, entry->page, entry->offset, true); @@ -466,7 +477,7 @@ gbt_var_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v, GBT_VARKEY **sv = NULL; gbt_vsrt_arg varg; - arr = (Vsrt *) palloc((maxoff + 1) * sizeof(Vsrt)); + arr = palloc_array(Vsrt, maxoff + 1); nbytes = (maxoff + 2) * sizeof(OffsetNumber); v->spl_left = (OffsetNumber *) palloc(nbytes); v->spl_right = (OffsetNumber *) palloc(nbytes); @@ -475,7 +486,7 @@ gbt_var_picksplit(const GistEntryVector *entryvec, GIST_SPLITVEC *v, v->spl_nleft = 0; v->spl_nright = 0; - sv = palloc(sizeof(bytea *) * (maxoff + 1)); + sv = palloc_array(GBT_VARKEY *, maxoff + 1); /* Sort entries */ diff --git a/contrib/btree_gist/btree_utils_var.h b/contrib/btree_gist/btree_utils_var.h index 75ad33d24fcd1..f28a5d98ae35d 100644 --- a/contrib/btree_gist/btree_utils_var.h +++ b/contrib/btree_gist/btree_utils_var.h @@ -42,14 +42,14 @@ typedef struct } gbtree_vinfo; /* - * Free ptr1 in case its a copy of ptr2. + * Free ptr1 in case it's a copy of ptr2. * * This is adapted from varlena's PG_FREE_IF_COPY, though doesn't require * fcinfo access. */ #define GBT_FREE_IF_COPY(ptr1, ptr2) \ do { \ - if ((Pointer) (ptr1) != DatumGetPointer(ptr2)) \ + if ((ptr1) != DatumGetPointer(ptr2)) \ pfree(ptr1); \ } while (0) diff --git a/contrib/btree_gist/btree_uuid.c b/contrib/btree_gist/btree_uuid.c index 23a307a6a71d5..c891840fb2541 100644 --- a/contrib/btree_gist/btree_uuid.c +++ b/contrib/btree_gist/btree_uuid.c @@ -6,6 +6,7 @@ #include "btree_gist.h" #include "btree_utils_num.h" #include "port/pg_bswap.h" +#include "utils/rel.h" #include "utils/sortsupport.h" #include "utils/uuid.h" @@ -107,7 +108,7 @@ gbt_uuid_compress(PG_FUNCTION_ARGS) char *r = (char *) palloc(2 * UUID_LEN); pg_uuid_t *key = DatumGetUUIDP(entry->key); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); memcpy(r, key, UUID_LEN); memcpy(r + UUID_LEN, key, UUID_LEN); @@ -135,8 +136,9 @@ gbt_uuid_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); pg_uuid_t *query = PG_GETARG_UUID_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); uuidKEY *kkk = (uuidKEY *) DatumGetPointer(entry->key); GBT_NUMKEY_R key; diff --git a/contrib/btree_gist/expected/cidr.out b/contrib/btree_gist/expected/cidr.out index 6d0995add60ec..e61df27affc71 100644 --- a/contrib/btree_gist/expected/cidr.out +++ b/contrib/btree_gist/expected/cidr.out @@ -32,7 +32,7 @@ SELECT count(*) FROM cidrtmp WHERE a > '121.111.63.82'; 309 (1 row) -CREATE INDEX cidridx ON cidrtmp USING gist ( a ); +CREATE INDEX cidridx ON cidrtmp USING gist ( a gist_cidr_ops ); SET enable_seqscan=off; SELECT count(*) FROM cidrtmp WHERE a < '121.111.63.82'::cidr; count diff --git a/contrib/btree_gist/expected/enum.out b/contrib/btree_gist/expected/enum.out index 5782f43883859..887cf0f7788c0 100644 --- a/contrib/btree_gist/expected/enum.out +++ b/contrib/btree_gist/expected/enum.out @@ -3,7 +3,7 @@ create type rainbow as enum ('r','o','g','b','i','v'); -- enum values added later take some different codepaths internally, -- so make sure we have coverage for those too alter type rainbow add value 'y' before 'g'; -CREATE TABLE enumtmp (a rainbow); +CREATE TEMPORARY TABLE enumtmp (a rainbow); \copy enumtmp from 'data/enum.data' SET enable_seqscan=on; select a, count(*) from enumtmp group by a order by 1; diff --git a/contrib/btree_gist/expected/inet.out b/contrib/btree_gist/expected/inet.out index f15f1435f0ab2..8cf12e3df8e0b 100644 --- a/contrib/btree_gist/expected/inet.out +++ b/contrib/btree_gist/expected/inet.out @@ -32,7 +32,7 @@ SELECT count(*) FROM inettmp WHERE a > '89.225.196.191'; 386 (1 row) -CREATE INDEX inetidx ON inettmp USING gist ( a ); +CREATE INDEX inetidx ON inettmp USING gist ( a gist_inet_ops ); SET enable_seqscan=off; SELECT count(*) FROM inettmp WHERE a < '89.225.196.191'::inet; count diff --git a/contrib/btree_gist/expected/stratnum.out b/contrib/btree_gist/expected/stratnum.out index dd0edaf4a2062..8222b66153833 100644 --- a/contrib/btree_gist/expected/stratnum.out +++ b/contrib/btree_gist/expected/stratnum.out @@ -1,13 +1,13 @@ --- test stratnum support func -SELECT gist_stratnum_btree(7); - gist_stratnum_btree ---------------------- - 0 +-- test stratnum translation support func +SELECT gist_translate_cmptype_btree(7); + gist_translate_cmptype_btree +------------------------------ + 0 (1 row) -SELECT gist_stratnum_btree(3); - gist_stratnum_btree ---------------------- - 3 +SELECT gist_translate_cmptype_btree(3); + gist_translate_cmptype_btree +------------------------------ + 3 (1 row) diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build index 89932dd3844ee..2b1a54632898d 100644 --- a/contrib/btree_gist/meson.build +++ b/contrib/btree_gist/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group btree_gist_sources = files( 'btree_bit.c', @@ -44,7 +44,6 @@ install_data( 'btree_gist.control', 'btree_gist--1.0--1.1.sql', 'btree_gist--1.1--1.2.sql', - 'btree_gist--1.2.sql', 'btree_gist--1.2--1.3.sql', 'btree_gist--1.3--1.4.sql', 'btree_gist--1.4--1.5.sql', @@ -52,6 +51,7 @@ install_data( 'btree_gist--1.6--1.7.sql', 'btree_gist--1.7--1.8.sql', 'btree_gist--1.8--1.9.sql', + 'btree_gist--1.9.sql', kwargs: contrib_data_args, ) diff --git a/contrib/btree_gist/sql/cidr.sql b/contrib/btree_gist/sql/cidr.sql index 9bd77185b96a8..ec1529e3e04d4 100644 --- a/contrib/btree_gist/sql/cidr.sql +++ b/contrib/btree_gist/sql/cidr.sql @@ -15,7 +15,7 @@ SELECT count(*) FROM cidrtmp WHERE a >= '121.111.63.82'; SELECT count(*) FROM cidrtmp WHERE a > '121.111.63.82'; -CREATE INDEX cidridx ON cidrtmp USING gist ( a ); +CREATE INDEX cidridx ON cidrtmp USING gist ( a gist_cidr_ops ); SET enable_seqscan=off; diff --git a/contrib/btree_gist/sql/enum.sql b/contrib/btree_gist/sql/enum.sql index d662cb6322102..0cb5979a1dd7f 100644 --- a/contrib/btree_gist/sql/enum.sql +++ b/contrib/btree_gist/sql/enum.sql @@ -6,7 +6,7 @@ create type rainbow as enum ('r','o','g','b','i','v'); -- so make sure we have coverage for those too alter type rainbow add value 'y' before 'g'; -CREATE TABLE enumtmp (a rainbow); +CREATE TEMPORARY TABLE enumtmp (a rainbow); \copy enumtmp from 'data/enum.data' diff --git a/contrib/btree_gist/sql/inet.sql b/contrib/btree_gist/sql/inet.sql index 249e8085c3b3b..0bb73c9d71580 100644 --- a/contrib/btree_gist/sql/inet.sql +++ b/contrib/btree_gist/sql/inet.sql @@ -16,7 +16,7 @@ SELECT count(*) FROM inettmp WHERE a >= '89.225.196.191'; SELECT count(*) FROM inettmp WHERE a > '89.225.196.191'; -CREATE INDEX inetidx ON inettmp USING gist ( a ); +CREATE INDEX inetidx ON inettmp USING gist ( a gist_inet_ops ); SET enable_seqscan=off; diff --git a/contrib/btree_gist/sql/stratnum.sql b/contrib/btree_gist/sql/stratnum.sql index 75adddad84925..da8bbf883b0cc 100644 --- a/contrib/btree_gist/sql/stratnum.sql +++ b/contrib/btree_gist/sql/stratnum.sql @@ -1,3 +1,3 @@ --- test stratnum support func -SELECT gist_stratnum_btree(7); -SELECT gist_stratnum_btree(3); +-- test stratnum translation support func +SELECT gist_translate_cmptype_btree(7); +SELECT gist_translate_cmptype_btree(3); diff --git a/contrib/citext/meson.build b/contrib/citext/meson.build index 7fff34f236850..1cc49fc999fba 100644 --- a/contrib/citext/meson.build +++ b/contrib/citext/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group citext_sources = files( 'citext.c', diff --git a/contrib/cube/cube.c b/contrib/cube/cube.c index 8d3654ab7aafe..77263ab277f93 100644 --- a/contrib/cube/cube.c +++ b/contrib/cube/cube.c @@ -397,8 +397,9 @@ g_cube_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); NDBOX *query = PG_GETARG_NDBOX_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool res; @@ -471,7 +472,7 @@ g_cube_decompress(PG_FUNCTION_ARGS) if (key != DatumGetNDBOXP(entry->key)) { - GISTENTRY *retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + GISTENTRY *retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, @@ -718,16 +719,16 @@ g_cube_internal_consistent(NDBOX *key, switch (strategy) { case RTOverlapStrategyNumber: - retval = (bool) cube_overlap_v0(key, query); + retval = cube_overlap_v0(key, query); break; case RTSameStrategyNumber: case RTContainsStrategyNumber: case RTOldContainsStrategyNumber: - retval = (bool) cube_contains_v0(key, query); + retval = cube_contains_v0(key, query); break; case RTContainedByStrategyNumber: case RTOldContainedByStrategyNumber: - retval = (bool) cube_overlap_v0(key, query); + retval = cube_overlap_v0(key, query); break; default: retval = false; diff --git a/contrib/cube/cubedata.h b/contrib/cube/cubedata.h index ad1e2bd699810..8bfcc6e99a27d 100644 --- a/contrib/cube/cubedata.h +++ b/contrib/cube/cubedata.h @@ -62,10 +62,7 @@ typedef struct NDBOX /* for cubescan.l and cubeparse.y */ /* All grammar constructs return strings */ #define YYSTYPE char * -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif /* in cubescan.l */ extern int cube_yylex(YYSTYPE *yylval_param, yyscan_t yyscanner); diff --git a/contrib/cube/meson.build b/contrib/cube/meson.build index fd3c057f46920..6526091c688d9 100644 --- a/contrib/cube/meson.build +++ b/contrib/cube/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group cube_sources = files( 'cube.c', diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index 98d4e3d7dac4c..9798cb535bcf4 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -9,7 +9,7 @@ * Shridhar Daithankar * * contrib/dblink/dblink.c - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * ALL RIGHTS RESERVED; * * Permission to use, copy, modify, and distribute this software and its @@ -59,9 +59,11 @@ #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/guc.h" +#include "utils/hsearch.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/tuplestore.h" #include "utils/varlena.h" #include "utils/wait_event.h" @@ -101,11 +103,11 @@ static void materializeQueryResult(FunctionCallInfo fcinfo, const char *conname, const char *sql, bool fail); -static PGresult *storeQueryResult(volatile storeInfo *sinfo, PGconn *conn, const char *sql); -static void storeRow(volatile storeInfo *sinfo, PGresult *res, bool first); +static PGresult *storeQueryResult(storeInfo *sinfo, PGconn *conn, const char *sql); +static void storeRow(storeInfo *sinfo, PGresult *res, bool first); static remoteConn *getConnectionByName(const char *name); static HTAB *createConnHash(void); -static void createNewConnection(const char *name, remoteConn *rconn); +static remoteConn *createNewConnection(const char *name); static void deleteConnection(const char *name); static char **get_pkey_attnames(Relation rel, int16 *indnkeyatts); static char **get_text_array_contents(ArrayType *array, int *numitems); @@ -119,7 +121,8 @@ static Relation get_rel_from_relname(text *relname_text, LOCKMODE lockmode, AclM static char *generate_relation_name(Relation rel); static void dblink_connstr_check(const char *connstr); static bool dblink_connstr_has_pw(const char *connstr); -static void dblink_security_check(PGconn *conn, remoteConn *rconn, const char *connstr); +static void dblink_security_check(PGconn *conn, const char *connname, + const char *connstr); static void dblink_res_error(PGconn *conn, const char *conname, PGresult *res, bool fail, const char *fmt,...) pg_attribute_printf(5, 6); static char *get_connect_string(const char *servername); @@ -147,29 +150,27 @@ static uint32 dblink_we_get_conn = 0; static uint32 dblink_we_get_result = 0; /* - * Following is list that holds multiple remote connections. + * Following is hash that holds multiple remote connections. * Calling convention of each dblink function changes to accept - * connection name as the first parameter. The connection list is + * connection name as the first parameter. The connection hash is * much like ecpg e.g. a mapping between a name and a PGconn object. + * + * To avoid potentially leaking a PGconn object in case of out-of-memory + * errors, we first create the hash entry, then open the PGconn. + * Hence, a hash entry whose rconn.conn pointer is NULL must be + * understood as a leftover from a failed create; it should be ignored + * by lookup operations, and silently replaced by create operations. */ typedef struct remoteConnHashEnt { char name[NAMEDATALEN]; - remoteConn *rconn; + remoteConn rconn; } remoteConnHashEnt; /* initial number of connection hashes */ #define NUMCONN 16 -static char * -xpstrdup(const char *in) -{ - if (in == NULL) - return NULL; - return pstrdup(in); -} - pg_noreturn static void dblink_res_internalerror(PGconn *conn, PGresult *res, const char *p2) { @@ -233,7 +234,11 @@ dblink_get_conn(char *conname_or_str, errmsg("could not establish connection"), errdetail_internal("%s", msg))); } - dblink_security_check(conn, rconn, connstr); + + PQsetNoticeReceiver(conn, libpqsrv_notice_receiver, + "received message via remote connection"); + + dblink_security_check(conn, NULL, connstr); if (PQclientEncoding(conn) != GetDatabaseEncoding()) PQsetClientEncoding(conn, GetDatabaseEncodingName()); freeconn = true; @@ -296,15 +301,6 @@ dblink_connect(PG_FUNCTION_ARGS) else if (PG_NARGS() == 1) conname_or_str = text_to_cstring(PG_GETARG_TEXT_PP(0)); - if (connname) - { - rconn = (remoteConn *) MemoryContextAlloc(TopMemoryContext, - sizeof(remoteConn)); - rconn->conn = NULL; - rconn->openCursorCount = 0; - rconn->newXactForCursor = false; - } - /* first check for valid foreign data server */ connstr = get_connect_string(conname_or_str); if (connstr == NULL) @@ -317,6 +313,13 @@ dblink_connect(PG_FUNCTION_ARGS) if (dblink_we_connect == 0) dblink_we_connect = WaitEventExtensionNew("DblinkConnect"); + /* if we need a hashtable entry, make that first, since it might fail */ + if (connname) + { + rconn = createNewConnection(connname); + Assert(rconn->conn == NULL); + } + /* OK to make connection */ conn = libpqsrv_connect(connstr, dblink_we_connect); @@ -324,8 +327,8 @@ dblink_connect(PG_FUNCTION_ARGS) { msg = pchomp(PQerrorMessage(conn)); libpqsrv_disconnect(conn); - if (rconn) - pfree(rconn); + if (connname) + deleteConnection(connname); ereport(ERROR, (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION), @@ -333,17 +336,20 @@ dblink_connect(PG_FUNCTION_ARGS) errdetail_internal("%s", msg))); } + PQsetNoticeReceiver(conn, libpqsrv_notice_receiver, + "received message via remote connection"); + /* check password actually used if not superuser */ - dblink_security_check(conn, rconn, connstr); + dblink_security_check(conn, connname, connstr); /* attempt to set client encoding to match server encoding, if needed */ if (PQclientEncoding(conn) != GetDatabaseEncoding()) PQsetClientEncoding(conn, GetDatabaseEncodingName()); + /* all OK, save away the conn */ if (connname) { rconn->conn = conn; - createNewConnection(connname, rconn); } else { @@ -383,10 +389,7 @@ dblink_disconnect(PG_FUNCTION_ARGS) libpqsrv_disconnect(conn); if (rconn) - { deleteConnection(conname); - pfree(rconn); - } else pconn->conn = NULL; @@ -861,131 +864,124 @@ static void materializeResult(FunctionCallInfo fcinfo, PGconn *conn, PGresult *res) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + TupleDesc tupdesc; + bool is_sql_cmd; + int ntuples; + int nfields; /* prepTuplestoreResult must have been called previously */ Assert(rsinfo->returnMode == SFRM_Materialize); - PG_TRY(); + if (PQresultStatus(res) == PGRES_COMMAND_OK) { - TupleDesc tupdesc; - bool is_sql_cmd; - int ntuples; - int nfields; + is_sql_cmd = true; - if (PQresultStatus(res) == PGRES_COMMAND_OK) - { - is_sql_cmd = true; + /* + * need a tuple descriptor representing one TEXT column to return the + * command status string as our result tuple + */ + tupdesc = CreateTemplateTupleDesc(1); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "status", + TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); + ntuples = 1; + nfields = 1; + } + else + { + Assert(PQresultStatus(res) == PGRES_TUPLES_OK); - /* - * need a tuple descriptor representing one TEXT column to return - * the command status string as our result tuple - */ - tupdesc = CreateTemplateTupleDesc(1); - TupleDescInitEntry(tupdesc, (AttrNumber) 1, "status", - TEXTOID, -1, 0); - ntuples = 1; - nfields = 1; - } - else - { - Assert(PQresultStatus(res) == PGRES_TUPLES_OK); + is_sql_cmd = false; - is_sql_cmd = false; + /* get a tuple descriptor for our result type */ + switch (get_call_result_type(fcinfo, NULL, &tupdesc)) + { + case TYPEFUNC_COMPOSITE: + /* success */ + break; + case TYPEFUNC_RECORD: + /* failed to determine actual type of RECORD */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function returning record called in context " + "that cannot accept type record"))); + break; + default: + /* result type isn't composite */ + elog(ERROR, "return type must be a row type"); + break; + } - /* get a tuple descriptor for our result type */ - switch (get_call_result_type(fcinfo, NULL, &tupdesc)) - { - case TYPEFUNC_COMPOSITE: - /* success */ - break; - case TYPEFUNC_RECORD: - /* failed to determine actual type of RECORD */ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("function returning record called in context " - "that cannot accept type record"))); - break; - default: - /* result type isn't composite */ - elog(ERROR, "return type must be a row type"); - break; - } + /* make sure we have a persistent copy of the tupdesc */ + tupdesc = CreateTupleDescCopy(tupdesc); + ntuples = PQntuples(res); + nfields = PQnfields(res); + } - /* make sure we have a persistent copy of the tupdesc */ - tupdesc = CreateTupleDescCopy(tupdesc); - ntuples = PQntuples(res); - nfields = PQnfields(res); - } + /* + * check result and tuple descriptor have the same number of columns + */ + if (nfields != tupdesc->natts) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("remote query result rowtype does not match " + "the specified FROM clause rowtype"))); - /* - * check result and tuple descriptor have the same number of columns - */ - if (nfields != tupdesc->natts) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("remote query result rowtype does not match " - "the specified FROM clause rowtype"))); + if (ntuples > 0) + { + AttInMetadata *attinmeta; + int nestlevel = -1; + Tuplestorestate *tupstore; + MemoryContext oldcontext; + int row; + char **values; - if (ntuples > 0) - { - AttInMetadata *attinmeta; - int nestlevel = -1; - Tuplestorestate *tupstore; - MemoryContext oldcontext; - int row; - char **values; + attinmeta = TupleDescGetAttInMetadata(tupdesc); - attinmeta = TupleDescGetAttInMetadata(tupdesc); + /* Set GUCs to ensure we read GUC-sensitive data types correctly */ + if (!is_sql_cmd) + nestlevel = applyRemoteGucs(conn); - /* Set GUCs to ensure we read GUC-sensitive data types correctly */ - if (!is_sql_cmd) - nestlevel = applyRemoteGucs(conn); + oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); + tupstore = tuplestore_begin_heap(true, false, work_mem); + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + MemoryContextSwitchTo(oldcontext); - oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); - tupstore = tuplestore_begin_heap(true, false, work_mem); - rsinfo->setResult = tupstore; - rsinfo->setDesc = tupdesc; - MemoryContextSwitchTo(oldcontext); + values = palloc_array(char *, nfields); - values = palloc_array(char *, nfields); + /* put all tuples into the tuplestore */ + for (row = 0; row < ntuples; row++) + { + HeapTuple tuple; - /* put all tuples into the tuplestore */ - for (row = 0; row < ntuples; row++) + if (!is_sql_cmd) { - HeapTuple tuple; + int i; - if (!is_sql_cmd) + for (i = 0; i < nfields; i++) { - int i; - - for (i = 0; i < nfields; i++) - { - if (PQgetisnull(res, row, i)) - values[i] = NULL; - else - values[i] = PQgetvalue(res, row, i); - } + if (PQgetisnull(res, row, i)) + values[i] = NULL; + else + values[i] = PQgetvalue(res, row, i); } - else - { - values[0] = PQcmdStatus(res); - } - - /* build the tuple and put it into the tuplestore. */ - tuple = BuildTupleFromCStrings(attinmeta, values); - tuplestore_puttuple(tupstore, tuple); + } + else + { + values[0] = PQcmdStatus(res); } - /* clean up GUC settings, if we changed any */ - restoreLocalGucs(nestlevel); + /* build the tuple and put it into the tuplestore. */ + tuple = BuildTupleFromCStrings(attinmeta, values); + tuplestore_puttuple(tupstore, tuple); } + + /* clean up GUC settings, if we changed any */ + restoreLocalGucs(nestlevel); } - PG_FINALLY(); - { - /* be sure to release the libpq result */ - PQclear(res); - } - PG_END_TRY(); + + PQclear(res); } /* @@ -1004,16 +1000,17 @@ materializeQueryResult(FunctionCallInfo fcinfo, bool fail) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; - PGresult *volatile res = NULL; - volatile storeInfo sinfo = {0}; /* prepTuplestoreResult must have been called previously */ Assert(rsinfo->returnMode == SFRM_Materialize); - sinfo.fcinfo = fcinfo; - + /* Use a PG_TRY block to ensure we pump libpq dry of results */ PG_TRY(); { + storeInfo sinfo = {0}; + PGresult *res; + + sinfo.fcinfo = fcinfo; /* Create short-lived memory context for data conversions */ sinfo.tmpcontext = AllocSetContextCreate(CurrentMemoryContext, "dblink temporary context", @@ -1026,14 +1023,7 @@ materializeQueryResult(FunctionCallInfo fcinfo, (PQresultStatus(res) != PGRES_COMMAND_OK && PQresultStatus(res) != PGRES_TUPLES_OK)) { - /* - * dblink_res_error will clear the passed PGresult, so we need - * this ugly dance to avoid doing so twice during error exit - */ - PGresult *res1 = res; - - res = NULL; - dblink_res_error(conn, conname, res1, fail, + dblink_res_error(conn, conname, res, fail, "while executing query"); /* if fail isn't set, we'll return an empty query result */ } @@ -1057,6 +1047,7 @@ materializeQueryResult(FunctionCallInfo fcinfo, tupdesc = CreateTemplateTupleDesc(1); TupleDescInitEntry(tupdesc, (AttrNumber) 1, "status", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); attinmeta = TupleDescGetAttInMetadata(tupdesc); oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); @@ -1072,7 +1063,6 @@ materializeQueryResult(FunctionCallInfo fcinfo, tuplestore_puttuple(tupstore, tuple); PQclear(res); - res = NULL; } else { @@ -1081,26 +1071,20 @@ materializeQueryResult(FunctionCallInfo fcinfo, Assert(rsinfo->setResult != NULL); PQclear(res); - res = NULL; } /* clean up data conversion short-lived memory context */ if (sinfo.tmpcontext != NULL) MemoryContextDelete(sinfo.tmpcontext); - sinfo.tmpcontext = NULL; PQclear(sinfo.last_res); - sinfo.last_res = NULL; PQclear(sinfo.cur_res); - sinfo.cur_res = NULL; } PG_CATCH(); { - /* be sure to release any libpq result we collected */ - PQclear(res); - PQclear(sinfo.last_res); - PQclear(sinfo.cur_res); - /* and clear out any pending data in libpq */ + PGresult *res; + + /* be sure to clear out any pending data in libpq */ while ((res = libpqsrv_get_result(conn, dblink_we_get_result)) != NULL) PQclear(res); @@ -1113,7 +1097,7 @@ materializeQueryResult(FunctionCallInfo fcinfo, * Execute query, and send any result rows to sinfo->tuplestore. */ static PGresult * -storeQueryResult(volatile storeInfo *sinfo, PGconn *conn, const char *sql) +storeQueryResult(storeInfo *sinfo, PGconn *conn, const char *sql) { bool first = true; int nestlevel = -1; @@ -1181,7 +1165,7 @@ storeQueryResult(volatile storeInfo *sinfo, PGconn *conn, const char *sql) * (in this case the PGresult might contain either zero or one row). */ static void -storeRow(volatile storeInfo *sinfo, PGresult *res, bool first) +storeRow(storeInfo *sinfo, PGresult *res, bool first) { int nfields = PQnfields(res); HeapTuple tuple; @@ -1304,6 +1288,9 @@ dblink_get_connections(PG_FUNCTION_ARGS) hash_seq_init(&status, remoteConnHash); while ((hentry = (remoteConnHashEnt *) hash_seq_search(&status)) != NULL) { + /* ignore it if it's not an open connection */ + if (hentry->rconn.conn == NULL) + continue; /* stash away current value */ astate = accumArrayResult(astate, CStringGetTextDatum(hentry->name), @@ -1546,6 +1533,8 @@ dblink_get_pkey(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 2, "colname", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); + /* * Generate attribute metadata needed later to produce tuples from raw * C strings @@ -2086,9 +2075,10 @@ get_text_array_contents(ArrayType *array, int *numitems) int16 typlen; bool typbyval; char typalign; + uint8 typalignby; char **values; char *ptr; - bits8 *bitmap; + uint8 *bitmap; int bitmask; int i; @@ -2098,6 +2088,7 @@ get_text_array_contents(ArrayType *array, int *numitems) get_typlenbyvalalign(ARR_ELEMTYPE(array), &typlen, &typbyval, &typalign); + typalignby = typalign_to_alignby(typalign); values = palloc_array(char *, nitems); @@ -2115,7 +2106,7 @@ get_text_array_contents(ArrayType *array, int *numitems) { values[i] = TextDatumGetCString(PointerGetDatum(ptr)); ptr = att_addlength_pointer(ptr, typlen, ptr); - ptr = (char *) att_align_nominal(ptr, typalign); + ptr = (char *) att_nominal_alignby(ptr, typalignby); } /* advance bitmap pointer if any */ @@ -2477,6 +2468,21 @@ get_tuple_of_interest(Relation rel, int *pkattnums, int pknumatts, char **src_pk return NULL; } +static void +RangeVarCallbackForDblink(const RangeVar *relation, + Oid relId, Oid oldRelId, void *arg) +{ + AclResult aclresult; + + if (!OidIsValid(relId)) + return; + + aclresult = pg_class_aclcheck(relId, GetUserId(), *((AclMode *) arg)); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, get_relkind_objtype(get_rel_relkind(relId)), + relation->relname); +} + /* * Open the relation named by relname_text, acquire specified type of lock, * verify we have specified permissions. @@ -2486,19 +2492,13 @@ static Relation get_rel_from_relname(text *relname_text, LOCKMODE lockmode, AclMode aclmode) { RangeVar *relvar; - Relation rel; - AclResult aclresult; + Oid relid; relvar = makeRangeVarFromNameList(textToQualifiedNameList(relname_text)); - rel = table_openrv(relvar, lockmode); - - aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(), - aclmode); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), - RelationGetRelationName(rel)); + relid = RangeVarGetRelidExtended(relvar, lockmode, 0, + RangeVarCallbackForDblink, &aclmode); - return rel; + return table_open(relid, NoLock); } /* @@ -2539,8 +2539,8 @@ getConnectionByName(const char *name) hentry = (remoteConnHashEnt *) hash_search(remoteConnHash, key, HASH_FIND, NULL); - if (hentry) - return hentry->rconn; + if (hentry && hentry->rconn.conn != NULL) + return &hentry->rconn; return NULL; } @@ -2557,8 +2557,8 @@ createConnHash(void) HASH_ELEM | HASH_STRINGS); } -static void -createNewConnection(const char *name, remoteConn *rconn) +static remoteConn * +createNewConnection(const char *name) { remoteConnHashEnt *hentry; bool found; @@ -2572,17 +2572,15 @@ createNewConnection(const char *name, remoteConn *rconn) hentry = (remoteConnHashEnt *) hash_search(remoteConnHash, key, HASH_ENTER, &found); - if (found) - { - libpqsrv_disconnect(rconn->conn); - pfree(rconn); - + if (found && hentry->rconn.conn != NULL) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("duplicate connection name"))); - } - hentry->rconn = rconn; + /* New, or reusable, so initialize the rconn struct to zeroes */ + memset(&hentry->rconn, 0, sizeof(remoteConn)); + + return &hentry->rconn; } static void @@ -2662,7 +2660,7 @@ dblink_connstr_has_required_scram_options(const char *connstr) PQconninfoFree(options); } - has_scram_keys = has_scram_client_key && has_scram_server_key && MyProcPort->has_scram_keys; + has_scram_keys = has_scram_client_key && has_scram_server_key && MyProcPort != NULL && MyProcPort->has_scram_keys; return (has_scram_keys && has_require_auth); } @@ -2671,9 +2669,12 @@ dblink_connstr_has_required_scram_options(const char *connstr) * We need to make sure that the connection made used credentials * which were provided by the user, so check what credentials were * used to connect and then make sure that they came from the user. + * + * On failure, we close "conn" and also delete the hashtable entry + * identified by "connname" (if that's not NULL). */ static void -dblink_security_check(PGconn *conn, remoteConn *rconn, const char *connstr) +dblink_security_check(PGconn *conn, const char *connname, const char *connstr) { /* Superuser bypasses security check */ if (superuser()) @@ -2692,7 +2693,7 @@ dblink_security_check(PGconn *conn, remoteConn *rconn, const char *connstr) * only added if UseScramPassthrough is set, and the user is not allowed * to add the SCRAM keys on fdw and user mapping options. */ - if (MyProcPort->has_scram_keys && dblink_connstr_has_required_scram_options(connstr)) + if (MyProcPort != NULL && MyProcPort->has_scram_keys && dblink_connstr_has_required_scram_options(connstr)) return; #ifdef ENABLE_GSS @@ -2703,8 +2704,8 @@ dblink_security_check(PGconn *conn, remoteConn *rconn, const char *connstr) /* Otherwise, fail out */ libpqsrv_disconnect(conn); - if (rconn) - pfree(rconn); + if (connname) + deleteConnection(connname); ereport(ERROR, (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), @@ -2765,7 +2766,7 @@ dblink_connstr_check(const char *connstr) if (dblink_connstr_has_pw(connstr)) return; - if (MyProcPort->has_scram_keys && dblink_connstr_has_required_scram_options(connstr)) + if (MyProcPort != NULL && MyProcPort->has_scram_keys && dblink_connstr_has_required_scram_options(connstr)) return; #ifdef ENABLE_GSS @@ -2782,10 +2783,13 @@ dblink_connstr_check(const char *connstr) /* * Report an error received from the remote server * - * res: the received error result (will be freed) + * res: the received error result * fail: true for ERROR ereport, false for NOTICE * fmt and following args: sprintf-style format and values for errcontext; * the resulting string should be worded like "while " + * + * If "res" is not NULL, it'll be PQclear'ed here (unless we throw error, + * in which case memory context cleanup will clear it eventually). */ static void dblink_res_error(PGconn *conn, const char *conname, PGresult *res, @@ -2793,15 +2797,11 @@ dblink_res_error(PGconn *conn, const char *conname, PGresult *res, { int level; char *pg_diag_sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE); - char *pg_diag_message_primary = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); - char *pg_diag_message_detail = PQresultErrorField(res, PG_DIAG_MESSAGE_DETAIL); - char *pg_diag_message_hint = PQresultErrorField(res, PG_DIAG_MESSAGE_HINT); - char *pg_diag_context = PQresultErrorField(res, PG_DIAG_CONTEXT); + char *message_primary = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); + char *message_detail = PQresultErrorField(res, PG_DIAG_MESSAGE_DETAIL); + char *message_hint = PQresultErrorField(res, PG_DIAG_MESSAGE_HINT); + char *message_context = PQresultErrorField(res, PG_DIAG_CONTEXT); int sqlstate; - char *message_primary; - char *message_detail; - char *message_hint; - char *message_context; va_list ap; char dblink_context_msg[512]; @@ -2819,11 +2819,6 @@ dblink_res_error(PGconn *conn, const char *conname, PGresult *res, else sqlstate = ERRCODE_CONNECTION_FAILURE; - message_primary = xpstrdup(pg_diag_message_primary); - message_detail = xpstrdup(pg_diag_message_detail); - message_hint = xpstrdup(pg_diag_message_hint); - message_context = xpstrdup(pg_diag_context); - /* * If we don't get a message from the PGresult, try the PGconn. This is * needed because for connection-level failures, PQgetResult may just @@ -2832,14 +2827,6 @@ dblink_res_error(PGconn *conn, const char *conname, PGresult *res, if (message_primary == NULL) message_primary = pchomp(PQerrorMessage(conn)); - /* - * Now that we've copied all the data we need out of the PGresult, it's - * safe to free it. We must do this to avoid PGresult leakage. We're - * leaking all the strings too, but those are in palloc'd memory that will - * get cleaned up eventually. - */ - PQclear(res); - /* * Format the basic errcontext string. Below, we'll add on something * about the connection name. That's a violation of the translatability @@ -2864,6 +2851,7 @@ dblink_res_error(PGconn *conn, const char *conname, PGresult *res, dblink_context_msg, conname)) : (errcontext("%s on unnamed dblink connection", dblink_context_msg)))); + PQclear(res); } /* @@ -2925,7 +2913,7 @@ get_connect_string(const char *servername) * the user overwrites these options we can ereport on * dblink_connstr_check and dblink_security_check. */ - if (MyProcPort->has_scram_keys && UseScramPassthrough(foreign_server, user_mapping)) + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(foreign_server, user_mapping)) appendSCRAMKeysInfo(&buf); foreach(cell, fdw->options) @@ -3040,7 +3028,7 @@ validate_pkattnums(Relation rel, for (j = 0; j < natts; j++) { /* dropped columns don't count */ - if (TupleDescAttr(tupdesc, j)->attisdropped) + if (TupleDescCompactAttr(tupdesc, j)->attisdropped) continue; if (++lnum == pkattnum) diff --git a/contrib/dblink/meson.build b/contrib/dblink/meson.build index dfd8eb6877e90..e2489f41229fa 100644 --- a/contrib/dblink/meson.build +++ b/contrib/dblink/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group dblink_sources = files( 'dblink.c', @@ -34,7 +34,7 @@ tests += { 'sql': [ 'dblink', ], - 'regress_args': ['--dlpath', meson.build_root() / 'src/test/regress'], + 'regress_args': ['--dlpath', meson.project_build_root() / 'src/test/regress'], }, 'tap': { 'tests': [ diff --git a/contrib/dblink/t/001_auth_scram.pl b/contrib/dblink/t/001_auth_scram.pl index ef3dea6c5ad19..9558ca83b7cc0 100644 --- a/contrib/dblink/t/001_auth_scram.pl +++ b/contrib/dblink/t/001_auth_scram.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Test SCRAM authentication when opening a new connection with a foreign # server. diff --git a/contrib/dict_int/dict_int.c b/contrib/dict_int/dict_int.c index bdad52d202897..aafce8949779a 100644 --- a/contrib/dict_int/dict_int.c +++ b/contrib/dict_int/dict_int.c @@ -3,7 +3,7 @@ * dict_int.c * Text search dictionary for integers * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/dict_int/dict_int.c @@ -38,7 +38,7 @@ dintdict_init(PG_FUNCTION_ARGS) DictInt *d; ListCell *l; - d = (DictInt *) palloc0(sizeof(DictInt)); + d = palloc0_object(DictInt); d->maxlen = 6; d->rejectlong = false; d->absval = false; @@ -83,7 +83,7 @@ dintdict_lexize(PG_FUNCTION_ARGS) char *in = (char *) PG_GETARG_POINTER(1); int len = PG_GETARG_INT32(2); char *txt; - TSLexeme *res = palloc0(sizeof(TSLexeme) * 2); + TSLexeme *res = palloc0_array(TSLexeme, 2); res[1].lexeme = NULL; diff --git a/contrib/dict_int/meson.build b/contrib/dict_int/meson.build index ab41588547bc8..938647f45fda3 100644 --- a/contrib/dict_int/meson.build +++ b/contrib/dict_int/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group dict_int_sources = files( 'dict_int.c', diff --git a/contrib/dict_xsyn/dict_xsyn.c b/contrib/dict_xsyn/dict_xsyn.c index 1ec5285d6d1fc..9e3784e0f4736 100644 --- a/contrib/dict_xsyn/dict_xsyn.c +++ b/contrib/dict_xsyn/dict_xsyn.c @@ -3,7 +3,7 @@ * dict_xsyn.c * Extended synonym dictionary * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/dict_xsyn/dict_xsyn.c @@ -54,14 +54,14 @@ find_word(char *in, char **end) *end = NULL; while (*in && isspace((unsigned char) *in)) - in += pg_mblen(in); + in += pg_mblen_cstr(in); if (!*in || *in == '#') return NULL; start = in; while (*in && !isspace((unsigned char) *in)) - in += pg_mblen(in); + in += pg_mblen_cstr(in); *end = in; @@ -109,9 +109,9 @@ read_dictionary(DictSyn *d, const char *filename) { d->len = (d->len > 0) ? 2 * d->len : 16; if (d->syn) - d->syn = (Syn *) repalloc(d->syn, sizeof(Syn) * d->len); + d->syn = repalloc_array(d->syn, Syn, d->len); else - d->syn = (Syn *) palloc(sizeof(Syn) * d->len); + d->syn = palloc_array(Syn, d->len); } /* Save first word only if we will match it */ @@ -150,7 +150,7 @@ dxsyn_init(PG_FUNCTION_ARGS) ListCell *l; char *filename = NULL; - d = (DictSyn *) palloc0(sizeof(DictSyn)); + d = palloc0_object(DictSyn); d->len = 0; d->syn = NULL; d->matchorig = true; @@ -235,7 +235,7 @@ dxsyn_lexize(PG_FUNCTION_ARGS) char *end; int nsyns = 0; - res = palloc(sizeof(TSLexeme)); + res = palloc_object(TSLexeme); pos = value; while ((syn = find_word(pos, &end)) != NULL) diff --git a/contrib/dict_xsyn/meson.build b/contrib/dict_xsyn/meson.build index 93f41b1f963e0..1485e9e9797e5 100644 --- a/contrib/dict_xsyn/meson.build +++ b/contrib/dict_xsyn/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group dict_xsyn_sources = files( 'dict_xsyn.c', diff --git a/contrib/earthdistance/meson.build b/contrib/earthdistance/meson.build index a5f8d1a3ffa0d..41fb5065afa25 100644 --- a/contrib/earthdistance/meson.build +++ b/contrib/earthdistance/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group earthdistance_sources = files( 'earthdistance.c', diff --git a/contrib/file_fdw/data/multiline_header.csv b/contrib/file_fdw/data/multiline_header.csv new file mode 100644 index 0000000000000..0d5e482a1e427 --- /dev/null +++ b/contrib/file_fdw/data/multiline_header.csv @@ -0,0 +1,4 @@ +first header line +second header line +1,alpha +2,beta diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out index df8d43b374989..640986528ae19 100644 --- a/contrib/file_fdw/expected/file_fdw.out +++ b/contrib/file_fdw/expected/file_fdw.out @@ -48,6 +48,10 @@ SET ROLE regress_file_fdw_superuser; CREATE USER MAPPING FOR regress_file_fdw_superuser SERVER file_server; CREATE USER MAPPING FOR regress_no_priv_user SERVER file_server; -- validator tests +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (foo 'bar'); -- ERROR +ERROR: invalid option "foo" +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS ("a=b" 'true'); -- ERROR +ERROR: invalid option name "a=b": must not contain "=" CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (format 'xml'); -- ERROR ERROR: COPY format "xml" not recognized CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (format 'text', quote ':'); -- ERROR @@ -100,6 +104,12 @@ CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (reject_limit '1'); ERROR: COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (on_error 'ignore', reject_limit '0'); -- ERROR ERROR: REJECT_LIMIT (0) must be greater than zero +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (header '-1'); -- ERROR +ERROR: a negative integer value cannot be specified for header +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (header '2.5'); -- ERROR +ERROR: header requires a Boolean value, an integer value greater than or equal to zero, or the string "match" +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (header 'unsupported'); -- ERROR +ERROR: header requires a Boolean value, an integer value greater than or equal to zero, or the string "match" CREATE FOREIGN TABLE tbl () SERVER file_server; -- ERROR ERROR: either filename or program is required for file_fdw foreign tables \set filename :abs_srcdir '/data/agg.data' @@ -138,6 +148,25 @@ OPTIONS (format 'csv', filename :'filename', delimiter ',', header 'match'); SELECT * FROM header_doesnt_match; -- ERROR ERROR: column name mismatch in header line field 1: got "1", expected "a" CONTEXT: COPY header_doesnt_match, line 1: "1,foo" +-- test multi-line header +\set filename :abs_srcdir '/data/multiline_header.csv' +CREATE FOREIGN TABLE multi_header (a int, b text) SERVER file_server +OPTIONS (format 'csv', filename :'filename', header '2'); +SELECT * FROM multi_header ORDER BY a; + a | b +---+------- + 1 | alpha + 2 | beta +(2 rows) + +CREATE FOREIGN TABLE multi_header_skip (a int, b text) SERVER file_server +OPTIONS (format 'csv', filename :'filename', header '5'); +SELECT count(*) FROM multi_header_skip; + count +------- + 0 +(1 row) + -- per-column options tests \set filename :abs_srcdir '/data/text.csv' CREATE FOREIGN TABLE text_csv ( @@ -217,7 +246,17 @@ SELECT * FROM agg_bad; -- ERROR ERROR: invalid input syntax for type real: "aaa" CONTEXT: COPY agg_bad, line 3, column b: "aaa" -- on_error, log_verbosity and reject_limit tests -ALTER FOREIGN TABLE agg_bad OPTIONS (ADD on_error 'ignore'); +ALTER FOREIGN TABLE agg_bad OPTIONS (ADD on_error 'set_null'); +SELECT * FROM agg_bad; + a | b +-----+-------- + 100 | 99.097 + 0 | _null_ + 42 | 324.78 + 1 | _null_ +(4 rows) + +ALTER FOREIGN TABLE agg_bad OPTIONS (SET on_error 'ignore'); SELECT * FROM agg_bad; NOTICE: 2 rows were skipped due to data type incompatibility a | b @@ -318,6 +357,7 @@ SET constraint_exclusion = 'on'; SELECT explain_filter('EXPLAIN (VERBOSE, COSTS FALSE) SELECT * FROM agg_csv WHERE a < 0'); Result Output: a, b + Replaces: Scan on agg_csv One-Time Filter: false \t off @@ -452,6 +492,21 @@ SELECT tableoid::regclass, * FROM p2; p2 | 2 | xyzzy (3 rows) +-- Test DELETE/UPDATE/MERGE on a partitioned table when all partitions +-- are excluded and only the dummy root result relation remains. The +-- operation is a no-op but should not fail regardless of whether the +-- foreign child was processed (pruning off) or not (pruning on). +DROP TABLE p2; +SET enable_partition_pruning TO off; +DELETE FROM pt WHERE false; +UPDATE pt SET b = 'x' WHERE false; +MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b) + ON false WHEN MATCHED THEN UPDATE SET b = s.b; +SET enable_partition_pruning TO on; +DELETE FROM pt WHERE false; +UPDATE pt SET b = 'x' WHERE false; +MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b) + ON false WHEN MATCHED THEN UPDATE SET b = s.b; DROP TABLE pt; -- generated column tests \set filename :abs_srcdir '/data/list1.csv' @@ -538,7 +593,7 @@ SET ROLE regress_file_fdw_superuser; -- cleanup RESET ROLE; DROP EXTENSION file_fdw CASCADE; -NOTICE: drop cascades to 9 other objects +NOTICE: drop cascades to 11 other objects DETAIL: drop cascades to server file_server drop cascades to user mapping for regress_file_fdw_superuser on server file_server drop cascades to user mapping for regress_no_priv_user on server file_server @@ -547,5 +602,7 @@ drop cascades to foreign table agg_csv drop cascades to foreign table agg_bad drop cascades to foreign table header_match drop cascades to foreign table header_doesnt_match +drop cascades to foreign table multi_header +drop cascades to foreign table multi_header_skip drop cascades to foreign table text_csv DROP ROLE regress_file_fdw_superuser, regress_file_fdw_user, regress_no_priv_user; diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c index a9a5671d95a6f..33a37d832ce58 100644 --- a/contrib/file_fdw/file_fdw.c +++ b/contrib/file_fdw/file_fdw.c @@ -3,7 +3,7 @@ * file_fdw.c * foreign-data wrapper for server-side flat files (or programs). * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/file_fdw/file_fdw.c @@ -531,7 +531,7 @@ fileGetForeignRelSize(PlannerInfo *root, * we might as well get everything and not need to re-fetch it later in * planning. */ - fdw_private = (FileFdwPlanState *) palloc(sizeof(FileFdwPlanState)); + fdw_private = palloc_object(FileFdwPlanState); fileGetOptions(foreigntableid, &fdw_private->filename, &fdw_private->is_program, @@ -712,7 +712,7 @@ fileBeginForeignScan(ForeignScanState *node, int eflags) * Save state in node->fdw_state. We must save enough information to call * BeginCopyFrom() again. */ - festate = (FileFdwExecutionState *) palloc(sizeof(FileFdwExecutionState)); + festate = palloc_object(FileFdwExecutionState); festate->filename = filename; festate->is_program = is_program; festate->options = options; @@ -1026,9 +1026,7 @@ check_selective_binary_conversion(RelOptInfo *baserel, numattrs = 0; for (i = 0; i < tupleDesc->natts; i++) { - Form_pg_attribute attr = TupleDescAttr(tupleDesc, i); - - if (attr->attisdropped) + if (TupleDescCompactAttr(tupleDesc, i)->attisdropped) continue; numattrs++; } diff --git a/contrib/file_fdw/meson.build b/contrib/file_fdw/meson.build index 6db493b3dad66..bc41e1bd97ffd 100644 --- a/contrib/file_fdw/meson.build +++ b/contrib/file_fdw/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group file_fdw_sources = files( 'file_fdw.c', diff --git a/contrib/file_fdw/sql/file_fdw.sql b/contrib/file_fdw/sql/file_fdw.sql index 2cdbe7a8a4c52..56bfc926c004e 100644 --- a/contrib/file_fdw/sql/file_fdw.sql +++ b/contrib/file_fdw/sql/file_fdw.sql @@ -55,6 +55,8 @@ CREATE USER MAPPING FOR regress_file_fdw_superuser SERVER file_server; CREATE USER MAPPING FOR regress_no_priv_user SERVER file_server; -- validator tests +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (foo 'bar'); -- ERROR +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS ("a=b" 'true'); -- ERROR CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (format 'xml'); -- ERROR CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (format 'text', quote ':'); -- ERROR CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (format 'text', escape ':'); -- ERROR @@ -82,6 +84,9 @@ CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (format 'binary', on_erro CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (log_verbosity 'unsupported'); -- ERROR CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (reject_limit '1'); -- ERROR CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (on_error 'ignore', reject_limit '0'); -- ERROR +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (header '-1'); -- ERROR +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (header '2.5'); -- ERROR +CREATE FOREIGN TABLE tbl () SERVER file_server OPTIONS (header 'unsupported'); -- ERROR CREATE FOREIGN TABLE tbl () SERVER file_server; -- ERROR \set filename :abs_srcdir '/data/agg.data' @@ -117,6 +122,16 @@ CREATE FOREIGN TABLE header_doesnt_match (a int, foo text) SERVER file_server OPTIONS (format 'csv', filename :'filename', delimiter ',', header 'match'); SELECT * FROM header_doesnt_match; -- ERROR +-- test multi-line header +\set filename :abs_srcdir '/data/multiline_header.csv' +CREATE FOREIGN TABLE multi_header (a int, b text) SERVER file_server +OPTIONS (format 'csv', filename :'filename', header '2'); +SELECT * FROM multi_header ORDER BY a; + +CREATE FOREIGN TABLE multi_header_skip (a int, b text) SERVER file_server +OPTIONS (format 'csv', filename :'filename', header '5'); +SELECT count(*) FROM multi_header_skip; + -- per-column options tests \set filename :abs_srcdir '/data/text.csv' CREATE FOREIGN TABLE text_csv ( @@ -156,7 +171,9 @@ SELECT * FROM agg_csv c JOIN agg_text t ON (t.a = c.a) ORDER BY c.a; SELECT * FROM agg_bad; -- ERROR -- on_error, log_verbosity and reject_limit tests -ALTER FOREIGN TABLE agg_bad OPTIONS (ADD on_error 'ignore'); +ALTER FOREIGN TABLE agg_bad OPTIONS (ADD on_error 'set_null'); +SELECT * FROM agg_bad; +ALTER FOREIGN TABLE agg_bad OPTIONS (SET on_error 'ignore'); SELECT * FROM agg_bad; ALTER FOREIGN TABLE agg_bad OPTIONS (ADD log_verbosity 'silent'); SELECT * FROM agg_bad; @@ -240,6 +257,24 @@ UPDATE pt set a = 1 where a = 2; -- ERROR SELECT tableoid::regclass, * FROM pt; SELECT tableoid::regclass, * FROM p1; SELECT tableoid::regclass, * FROM p2; + +-- Test DELETE/UPDATE/MERGE on a partitioned table when all partitions +-- are excluded and only the dummy root result relation remains. The +-- operation is a no-op but should not fail regardless of whether the +-- foreign child was processed (pruning off) or not (pruning on). +DROP TABLE p2; +SET enable_partition_pruning TO off; +DELETE FROM pt WHERE false; +UPDATE pt SET b = 'x' WHERE false; +MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b) + ON false WHEN MATCHED THEN UPDATE SET b = s.b; + +SET enable_partition_pruning TO on; +DELETE FROM pt WHERE false; +UPDATE pt SET b = 'x' WHERE false; +MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b) + ON false WHEN MATCHED THEN UPDATE SET b = s.b; + DROP TABLE pt; -- generated column tests diff --git a/contrib/fuzzystrmatch/daitch_mokotoff.c b/contrib/fuzzystrmatch/daitch_mokotoff.c index 07f895ae2bf64..72e556d552d66 100644 --- a/contrib/fuzzystrmatch/daitch_mokotoff.c +++ b/contrib/fuzzystrmatch/daitch_mokotoff.c @@ -1,7 +1,7 @@ /* * Daitch-Mokotoff Soundex * - * Copyright (c) 2023-2025, PostgreSQL Global Development Group + * Copyright (c) 2023-2026, PostgreSQL Global Development Group * * This module was originally sponsored by Finance Norway / * Trafikkforsikringsforeningen, and implemented by Dag Lem diff --git a/contrib/fuzzystrmatch/daitch_mokotoff_header.pl b/contrib/fuzzystrmatch/daitch_mokotoff_header.pl index ff190a33ee0cb..75667a19c7396 100755 --- a/contrib/fuzzystrmatch/daitch_mokotoff_header.pl +++ b/contrib/fuzzystrmatch/daitch_mokotoff_header.pl @@ -2,7 +2,7 @@ # # Generation of types and lookup tables for Daitch-Mokotoff soundex. # -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # This module was originally sponsored by Finance Norway / # Trafikkforsikringsforeningen, and implemented by Dag Lem @@ -58,7 +58,7 @@ /* * Constants and lookup tables for Daitch-Mokotoff Soundex * - * Copyright (c) 2023-2025, PostgreSQL Global Development Group + * Copyright (c) 2023-2026, PostgreSQL Global Development Group * * This file is generated by daitch_mokotoff_header.pl */ diff --git a/contrib/fuzzystrmatch/dmetaphone.c b/contrib/fuzzystrmatch/dmetaphone.c index 6627b2b89433a..062667527c2da 100644 --- a/contrib/fuzzystrmatch/dmetaphone.c +++ b/contrib/fuzzystrmatch/dmetaphone.c @@ -99,6 +99,7 @@ The remaining code is authored by Andrew Dunstan and #include "postgres.h" #include "utils/builtins.h" +#include "utils/formatting.h" /* turn off assertions for embedded function */ #define NDEBUG @@ -117,7 +118,7 @@ The remaining code is authored by Andrew Dunstan and #include /* prototype for the main function we got from the perl module */ -static void DoubleMetaphone(char *str, char **codes); +static void DoubleMetaphone(const char *str, Oid collid, char **codes); #ifndef DMETAPHONE_MAIN @@ -142,7 +143,7 @@ dmetaphone(PG_FUNCTION_ARGS) arg = PG_GETARG_TEXT_PP(0); aptr = text_to_cstring(arg); - DoubleMetaphone(aptr, codes); + DoubleMetaphone(aptr, PG_GET_COLLATION(), codes); code = codes[0]; if (!code) code = ""; @@ -171,7 +172,7 @@ dmetaphone_alt(PG_FUNCTION_ARGS) arg = PG_GETARG_TEXT_PP(0); aptr = text_to_cstring(arg); - DoubleMetaphone(aptr, codes); + DoubleMetaphone(aptr, PG_GET_COLLATION(), codes); code = codes[1]; if (!code) code = ""; @@ -278,13 +279,17 @@ IncreaseBuffer(metastring *s, int chars_needed) } -static void -MakeUpper(metastring *s) +static metastring * +MakeUpper(metastring *s, Oid collid) { - char *i; + char *newstr; + metastring *newms; + + newstr = str_toupper(s->str, s->length, collid); + newms = NewMetaString(newstr); + DestroyMetaString(s); - for (i = s->str; *i; i++) - *i = toupper((unsigned char) *i); + return newms; } @@ -327,7 +332,7 @@ GetAt(metastring *s, int pos) if ((pos < 0) || (pos >= s->length)) return '\0'; - return ((char) *(s->str + pos)); + return *(s->str + pos); } @@ -392,7 +397,7 @@ MetaphAdd(metastring *s, const char *new_str) static void -DoubleMetaphone(char *str, char **codes) +DoubleMetaphone(const char *str, Oid collid, char **codes) { int length; metastring *original; @@ -414,7 +419,7 @@ DoubleMetaphone(char *str, char **codes) primary->free_string_on_destroy = 0; secondary->free_string_on_destroy = 0; - MakeUpper(original); + original = MakeUpper(original, collid); /* skip these when at start of word */ if (StringAt(original, 0, 2, "GN", "KN", "PN", "WR", "PS", "")) @@ -1430,7 +1435,7 @@ main(int argc, char **argv) if (argc > 1) { - DoubleMetaphone(argv[1], codes); + DoubleMetaphone(argv[1], DEFAULT_COLLATION_OID, codes); printf("%s|%s\n", codes[0], codes[1]); } } diff --git a/contrib/fuzzystrmatch/fuzzystrmatch.c b/contrib/fuzzystrmatch/fuzzystrmatch.c index e7cc314b763ee..90a1969b114f2 100644 --- a/contrib/fuzzystrmatch/fuzzystrmatch.c +++ b/contrib/fuzzystrmatch/fuzzystrmatch.c @@ -6,7 +6,7 @@ * Joe Conway * * contrib/fuzzystrmatch/fuzzystrmatch.c - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * ALL RIGHTS RESERVED; * * metaphone() @@ -62,7 +62,7 @@ static const char *const soundex_table = "01230120022455012623010202"; static char soundex_code(char letter) { - letter = toupper((unsigned char) letter); + letter = pg_ascii_toupper((unsigned char) letter); /* Defend against non-ASCII letters */ if (letter >= 'A' && letter <= 'Z') return soundex_table[letter - 'A']; @@ -122,16 +122,21 @@ static const char _codes[26] = { static int getcode(char c) { - if (isalpha((unsigned char) c)) - { - c = toupper((unsigned char) c); - /* Defend against non-ASCII letters */ - if (c >= 'A' && c <= 'Z') - return _codes[c - 'A']; - } + c = pg_ascii_toupper((unsigned char) c); + /* Defend against non-ASCII letters */ + if (c >= 'A' && c <= 'Z') + return _codes[c - 'A']; + return 0; } +static bool +ascii_isalpha(char c) +{ + return (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'); +} + #define isvowel(c) (getcode(c) & 1) /* AEIOU */ /* These letters are passed through unchanged */ @@ -301,18 +306,18 @@ metaphone(PG_FUNCTION_ARGS) * accessing the array directly... */ /* Look at the next letter in the word */ -#define Next_Letter (toupper((unsigned char) word[w_idx+1])) +#define Next_Letter (pg_ascii_toupper((unsigned char) word[w_idx+1])) /* Look at the current letter in the word */ -#define Curr_Letter (toupper((unsigned char) word[w_idx])) +#define Curr_Letter (pg_ascii_toupper((unsigned char) word[w_idx])) /* Go N letters back. */ #define Look_Back_Letter(n) \ - (w_idx >= (n) ? toupper((unsigned char) word[w_idx-(n)]) : '\0') + (w_idx >= (n) ? pg_ascii_toupper((unsigned char) word[w_idx-(n)]) : '\0') /* Previous letter. I dunno, should this return null on failure? */ #define Prev_Letter (Look_Back_Letter(1)) /* Look two letters down. It makes sure you don't walk off the string. */ #define After_Next_Letter \ - (Next_Letter != '\0' ? toupper((unsigned char) word[w_idx+2]) : '\0') -#define Look_Ahead_Letter(n) toupper((unsigned char) Lookahead(word+w_idx, n)) + (Next_Letter != '\0' ? pg_ascii_toupper((unsigned char) word[w_idx+2]) : '\0') +#define Look_Ahead_Letter(n) pg_ascii_toupper((unsigned char) Lookahead(word+w_idx, n)) /* Allows us to safely look ahead an arbitrary # of letters */ @@ -340,7 +345,7 @@ Lookahead(char *word, int how_far) #define Phone_Len (p_idx) /* Note is a letter is a 'break' in the word */ -#define Isbreak(c) (!isalpha((unsigned char) (c))) +#define Isbreak(c) (!ascii_isalpha((unsigned char) (c))) static void @@ -379,7 +384,7 @@ _metaphone(char *word, /* IN */ /*-- The first phoneme has to be processed specially. --*/ /* Find our first letter */ - for (; !isalpha((unsigned char) (Curr_Letter)); w_idx++) + for (; !ascii_isalpha((unsigned char) (Curr_Letter)); w_idx++) { /* On the off chance we were given nothing but crap... */ if (Curr_Letter == '\0') @@ -478,7 +483,7 @@ _metaphone(char *word, /* IN */ */ /* Ignore non-alphas */ - if (!isalpha((unsigned char) (Curr_Letter))) + if (!ascii_isalpha((unsigned char) (Curr_Letter))) continue; /* Drop duplicates, except CC */ @@ -731,7 +736,7 @@ _soundex(const char *instr, char *outstr) Assert(outstr); /* Skip leading non-alphabetic characters */ - while (*instr && !isalpha((unsigned char) *instr)) + while (*instr && !ascii_isalpha((unsigned char) *instr)) ++instr; /* If no string left, return all-zeroes buffer */ @@ -742,12 +747,12 @@ _soundex(const char *instr, char *outstr) } /* Take the first letter as is */ - *outstr++ = (char) toupper((unsigned char) *instr++); + *outstr++ = (char) pg_ascii_toupper((unsigned char) *instr++); count = 1; while (*instr && count < SOUNDEX_LEN) { - if (isalpha((unsigned char) *instr) && + if (ascii_isalpha((unsigned char) *instr) && soundex_code(*instr) != soundex_code(*(instr - 1))) { *outstr = soundex_code(*instr); diff --git a/contrib/fuzzystrmatch/meson.build b/contrib/fuzzystrmatch/meson.build index f52daa4ea1cff..e21a4e2e62bba 100644 --- a/contrib/fuzzystrmatch/meson.build +++ b/contrib/fuzzystrmatch/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group fuzzystrmatch_sources = files( 'daitch_mokotoff.c', diff --git a/contrib/hstore/expected/hstore.out b/contrib/hstore/expected/hstore.out index 1836c9acf39af..acea8806ba47e 100644 --- a/contrib/hstore/expected/hstore.out +++ b/contrib/hstore/expected/hstore.out @@ -7,7 +7,6 @@ WHERE opc.oid >= 16384 AND NOT amvalidate(opc.oid); --------+--------- (0 rows) -set escape_string_warning=off; --hstore; select ''::hstore; hstore diff --git a/contrib/hstore/hstore_compat.c b/contrib/hstore/hstore_compat.c index d75e9cb23f5cd..3a9f7f45cb71c 100644 --- a/contrib/hstore/hstore_compat.c +++ b/contrib/hstore/hstore_compat.c @@ -94,7 +94,7 @@ * etc. are compatible. * * If the above statement isn't true on some bizarre platform, we're - * a bit hosed (see StaticAssertStmt in hstoreValidOldFormat). + * a bit hosed. */ typedef struct { @@ -105,6 +105,9 @@ typedef struct pos:31; } HOldEntry; +StaticAssertDecl(sizeof(HOldEntry) == 2 * sizeof(HEntry), + "old hstore format is not upward-compatible"); + static int hstoreValidNewFormat(HStore *hs); static int hstoreValidOldFormat(HStore *hs); @@ -179,10 +182,6 @@ hstoreValidOldFormat(HStore *hs) if (hs->size_ & HS_FLAG_NEWVERSION) return 0; - /* New format uses an HEntry for key and another for value */ - StaticAssertStmt(sizeof(HOldEntry) == 2 * sizeof(HEntry), - "old hstore format is not upward-compatible"); - if (count == 0) return 2; diff --git a/contrib/hstore/hstore_gin.c b/contrib/hstore/hstore_gin.c index 766c00bb6a735..1365539f95f72 100644 --- a/contrib/hstore/hstore_gin.c +++ b/contrib/hstore/hstore_gin.c @@ -127,7 +127,7 @@ gin_extract_hstore_query(PG_FUNCTION_ARGS) /* Nulls in the array are ignored, cf hstoreArrayToPairs */ if (key_nulls[i]) continue; - item = makeitem(VARDATA(key_datums[i]), VARSIZE(key_datums[i]) - VARHDRSZ, KEYFLAG); + item = makeitem(VARDATA(DatumGetPointer(key_datums[i])), VARSIZE(DatumGetPointer(key_datums[i])) - VARHDRSZ, KEYFLAG); entries[j++] = PointerGetDatum(item); } @@ -152,11 +152,13 @@ gin_consistent_hstore(PG_FUNCTION_ARGS) { bool *check = (bool *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* HStore *query = PG_GETARG_HSTORE_P(2); */ +#ifdef NOT_USED + HStore *query = PG_GETARG_HSTORE_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ +#ifdef NOT_USED + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(5); bool res = true; int32 i; diff --git a/contrib/hstore/hstore_gist.c b/contrib/hstore/hstore_gist.c index a3b08af385016..832a268e0d2f0 100644 --- a/contrib/hstore/hstore_gist.c +++ b/contrib/hstore/hstore_gist.c @@ -175,7 +175,7 @@ ghstore_compress(PG_FUNCTION_ARGS) } } - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, @@ -195,7 +195,7 @@ ghstore_compress(PG_FUNCTION_ARGS) res = ghstore_alloc(true, siglen, NULL); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, @@ -429,7 +429,7 @@ ghstore_picksplit(PG_FUNCTION_ARGS) maxoff = OffsetNumberNext(maxoff); /* sort before ... */ - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) { costvector[j - 1].pos = j; @@ -510,8 +510,9 @@ ghstore_consistent(PG_FUNCTION_ARGS) { GISTTYPE *entry = (GISTTYPE *) DatumGetPointer(((GISTENTRY *) PG_GETARG_POINTER(0))->key); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int siglen = GET_SIGLEN(); bool res = true; @@ -576,7 +577,7 @@ ghstore_consistent(PG_FUNCTION_ARGS) if (key_nulls[i]) continue; - crc = crc32_sz(VARDATA(key_datums[i]), VARSIZE(key_datums[i]) - VARHDRSZ); + crc = crc32_sz(VARDATA(DatumGetPointer(key_datums[i])), VARSIZE(DatumGetPointer(key_datums[i])) - VARHDRSZ); if (!(GETBIT(sign, HASHVAL(crc, siglen)))) res = false; } @@ -599,7 +600,7 @@ ghstore_consistent(PG_FUNCTION_ARGS) if (key_nulls[i]) continue; - crc = crc32_sz(VARDATA(key_datums[i]), VARSIZE(key_datums[i]) - VARHDRSZ); + crc = crc32_sz(VARDATA(DatumGetPointer(key_datums[i])), VARSIZE(DatumGetPointer(key_datums[i])) - VARHDRSZ); if (GETBIT(sign, HASHVAL(crc, siglen))) res = true; } diff --git a/contrib/hstore/hstore_io.c b/contrib/hstore/hstore_io.c index 4f867e4bd1f1c..9b72efb8674a7 100644 --- a/contrib/hstore/hstore_io.c +++ b/contrib/hstore/hstore_io.c @@ -67,7 +67,7 @@ prssyntaxerror(HSParser *state) errsave(state->escontext, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("syntax error in hstore, near \"%.*s\" at position %d", - pg_mblen(state->ptr), state->ptr, + pg_mblen_cstr(state->ptr), state->ptr, (int) (state->ptr - state->begin)))); /* In soft error situation, return false as convenience for caller */ return false; @@ -221,7 +221,7 @@ parse_hstore(HSParser *state) bool escaped = false; state->plen = 16; - state->pairs = (Pairs *) palloc(sizeof(Pairs) * state->plen); + state->pairs = palloc_array(Pairs, state->plen); state->pcur = 0; state->ptr = state->begin; state->word = NULL; @@ -385,7 +385,8 @@ hstoreUniquePairs(Pairs *a, int32 l, int32 *buflen) if (ptr->needfree) { pfree(ptr->key); - pfree(ptr->val); + if (ptr->val != NULL) + pfree(ptr->val); } } else @@ -684,22 +685,22 @@ hstore_from_arrays(PG_FUNCTION_ARGS) if (!value_nulls || value_nulls[i]) { - pairs[i].key = VARDATA(key_datums[i]); + pairs[i].key = VARDATA(DatumGetPointer(key_datums[i])); pairs[i].val = NULL; pairs[i].keylen = - hstoreCheckKeyLen(VARSIZE(key_datums[i]) - VARHDRSZ); + hstoreCheckKeyLen(VARSIZE(DatumGetPointer(key_datums[i])) - VARHDRSZ); pairs[i].vallen = 4; pairs[i].isnull = true; pairs[i].needfree = false; } else { - pairs[i].key = VARDATA(key_datums[i]); - pairs[i].val = VARDATA(value_datums[i]); + pairs[i].key = VARDATA(DatumGetPointer(key_datums[i])); + pairs[i].val = VARDATA(DatumGetPointer(value_datums[i])); pairs[i].keylen = - hstoreCheckKeyLen(VARSIZE(key_datums[i]) - VARHDRSZ); + hstoreCheckKeyLen(VARSIZE(DatumGetPointer(key_datums[i])) - VARHDRSZ); pairs[i].vallen = - hstoreCheckValLen(VARSIZE(value_datums[i]) - VARHDRSZ); + hstoreCheckValLen(VARSIZE(DatumGetPointer(value_datums[i])) - VARHDRSZ); pairs[i].isnull = false; pairs[i].needfree = false; } @@ -778,22 +779,22 @@ hstore_from_array(PG_FUNCTION_ARGS) if (in_nulls[i * 2 + 1]) { - pairs[i].key = VARDATA(in_datums[i * 2]); + pairs[i].key = VARDATA(DatumGetPointer(in_datums[i * 2])); pairs[i].val = NULL; pairs[i].keylen = - hstoreCheckKeyLen(VARSIZE(in_datums[i * 2]) - VARHDRSZ); + hstoreCheckKeyLen(VARSIZE(DatumGetPointer(in_datums[i * 2])) - VARHDRSZ); pairs[i].vallen = 4; pairs[i].isnull = true; pairs[i].needfree = false; } else { - pairs[i].key = VARDATA(in_datums[i * 2]); - pairs[i].val = VARDATA(in_datums[i * 2 + 1]); + pairs[i].key = VARDATA(DatumGetPointer(in_datums[i * 2])); + pairs[i].val = VARDATA(DatumGetPointer(in_datums[i * 2 + 1])); pairs[i].keylen = - hstoreCheckKeyLen(VARSIZE(in_datums[i * 2]) - VARHDRSZ); + hstoreCheckKeyLen(VARSIZE(DatumGetPointer(in_datums[i * 2])) - VARHDRSZ); pairs[i].vallen = - hstoreCheckValLen(VARSIZE(in_datums[i * 2 + 1]) - VARHDRSZ); + hstoreCheckValLen(VARSIZE(DatumGetPointer(in_datums[i * 2 + 1])) - VARHDRSZ); pairs[i].isnull = false; pairs[i].needfree = false; } @@ -1439,10 +1440,9 @@ hstore_to_jsonb(PG_FUNCTION_ARGS) int count = HS_COUNT(in); char *base = STRPTR(in); HEntry *entries = ARRPTR(in); - JsonbParseState *state = NULL; - JsonbValue *res; + JsonbInState state = {0}; - (void) pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL); for (i = 0; i < count; i++) { @@ -1453,7 +1453,7 @@ hstore_to_jsonb(PG_FUNCTION_ARGS) key.val.string.len = HSTORE_KEYLEN(entries, i); key.val.string.val = HSTORE_KEY(entries, base, i); - (void) pushJsonbValue(&state, WJB_KEY, &key); + pushJsonbValue(&state, WJB_KEY, &key); if (HSTORE_VALISNULL(entries, i)) { @@ -1465,12 +1465,12 @@ hstore_to_jsonb(PG_FUNCTION_ARGS) val.val.string.len = HSTORE_VALLEN(entries, i); val.val.string.val = HSTORE_VAL(entries, base, i); } - (void) pushJsonbValue(&state, WJB_VALUE, &val); + pushJsonbValue(&state, WJB_VALUE, &val); } - res = pushJsonbValue(&state, WJB_END_OBJECT, NULL); + pushJsonbValue(&state, WJB_END_OBJECT, NULL); - PG_RETURN_POINTER(JsonbValueToJsonb(res)); + PG_RETURN_POINTER(JsonbValueToJsonb(state.result)); } PG_FUNCTION_INFO_V1(hstore_to_jsonb_loose); @@ -1482,13 +1482,12 @@ hstore_to_jsonb_loose(PG_FUNCTION_ARGS) int count = HS_COUNT(in); char *base = STRPTR(in); HEntry *entries = ARRPTR(in); - JsonbParseState *state = NULL; - JsonbValue *res; + JsonbInState state = {0}; StringInfoData tmp; initStringInfo(&tmp); - (void) pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL); for (i = 0; i < count; i++) { @@ -1499,7 +1498,7 @@ hstore_to_jsonb_loose(PG_FUNCTION_ARGS) key.val.string.len = HSTORE_KEYLEN(entries, i); key.val.string.val = HSTORE_KEY(entries, base, i); - (void) pushJsonbValue(&state, WJB_KEY, &key); + pushJsonbValue(&state, WJB_KEY, &key); if (HSTORE_VALISNULL(entries, i)) { @@ -1541,10 +1540,10 @@ hstore_to_jsonb_loose(PG_FUNCTION_ARGS) val.val.string.val = HSTORE_VAL(entries, base, i); } } - (void) pushJsonbValue(&state, WJB_VALUE, &val); + pushJsonbValue(&state, WJB_VALUE, &val); } - res = pushJsonbValue(&state, WJB_END_OBJECT, NULL); + pushJsonbValue(&state, WJB_END_OBJECT, NULL); - PG_RETURN_POINTER(JsonbValueToJsonb(res)); + PG_RETURN_POINTER(JsonbValueToJsonb(state.result)); } diff --git a/contrib/hstore/hstore_op.c b/contrib/hstore/hstore_op.c index 5e57eceffc817..bcba75f925808 100644 --- a/contrib/hstore/hstore_op.c +++ b/contrib/hstore/hstore_op.c @@ -107,8 +107,8 @@ hstoreArrayToPairs(ArrayType *a, int *npairs) { if (!key_nulls[i]) { - key_pairs[j].key = VARDATA(key_datums[i]); - key_pairs[j].keylen = VARSIZE(key_datums[i]) - VARHDRSZ; + key_pairs[j].key = VARDATA(DatumGetPointer(key_datums[i])); + key_pairs[j].keylen = VARSIZE(DatumGetPointer(key_datums[i])) - VARHDRSZ; key_pairs[j].val = NULL; key_pairs[j].vallen = 0; key_pairs[j].needfree = 0; diff --git a/contrib/hstore/hstore_subs.c b/contrib/hstore/hstore_subs.c index 3d03f66fa0dfb..56e0858c1a67c 100644 --- a/contrib/hstore/hstore_subs.c +++ b/contrib/hstore/hstore_subs.c @@ -12,7 +12,7 @@ * check_subscripts function and just let the fetch and assign functions * do everything. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -23,6 +23,7 @@ */ #include "postgres.h" +#include "catalog/pg_type_d.h" #include "executor/execExpr.h" #include "hstore.h" #include "nodes/nodeFuncs.h" @@ -74,7 +75,7 @@ hstore_subscript_transform(SubscriptingRef *sbsref, errmsg("hstore subscript must have type text"), parser_errposition(pstate, exprLocation(ai->uidx)))); - /* ... and store the transformed subscript into the SubscriptRef node */ + /* ... and store the transformed subscript into the SubscriptingRef node */ sbsref->refupperindexpr = list_make1(subexpr); sbsref->reflowerindexpr = NIL; diff --git a/contrib/hstore/meson.build b/contrib/hstore/meson.build index 39622d1024de6..6175abc708e9b 100644 --- a/contrib/hstore/meson.build +++ b/contrib/hstore/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # .. so that includes of hstore/hstore.h work hstore_inc = include_directories('.', '../') diff --git a/contrib/hstore/sql/hstore.sql b/contrib/hstore/sql/hstore.sql index efef91292a337..8ae95e8a51054 100644 --- a/contrib/hstore/sql/hstore.sql +++ b/contrib/hstore/sql/hstore.sql @@ -5,8 +5,6 @@ SELECT amname, opcname FROM pg_opclass opc LEFT JOIN pg_am am ON am.oid = opcmethod WHERE opc.oid >= 16384 AND NOT amvalidate(opc.oid); -set escape_string_warning=off; - --hstore; select ''::hstore; diff --git a/contrib/hstore_plperl/hstore_plperl.c b/contrib/hstore_plperl/hstore_plperl.c index 31393b4fa504d..69001191cc0ce 100644 --- a/contrib/hstore_plperl/hstore_plperl.c +++ b/contrib/hstore_plperl/hstore_plperl.c @@ -21,6 +21,13 @@ static hstoreCheckKeyLen_t hstoreCheckKeyLen_p; typedef size_t (*hstoreCheckValLen_t) (size_t len); static hstoreCheckValLen_t hstoreCheckValLen_p; +/* Static asserts verify that typedefs above match original declarations */ +StaticAssertVariableIsOfType(&hstoreUpgrade, hstoreUpgrade_t); +StaticAssertVariableIsOfType(&hstoreUniquePairs, hstoreUniquePairs_t); +StaticAssertVariableIsOfType(&hstorePairs, hstorePairs_t); +StaticAssertVariableIsOfType(&hstoreCheckKeyLen, hstoreCheckKeyLen_t); +StaticAssertVariableIsOfType(&hstoreCheckValLen, hstoreCheckValLen_t); + /* * Module initialize function: fetch function pointers for cross-module calls. @@ -28,24 +35,18 @@ static hstoreCheckValLen_t hstoreCheckValLen_p; void _PG_init(void) { - /* Asserts verify that typedefs above match original declarations */ - AssertVariableIsOfType(&hstoreUpgrade, hstoreUpgrade_t); hstoreUpgrade_p = (hstoreUpgrade_t) load_external_function("$libdir/hstore", "hstoreUpgrade", true, NULL); - AssertVariableIsOfType(&hstoreUniquePairs, hstoreUniquePairs_t); hstoreUniquePairs_p = (hstoreUniquePairs_t) load_external_function("$libdir/hstore", "hstoreUniquePairs", true, NULL); - AssertVariableIsOfType(&hstorePairs, hstorePairs_t); hstorePairs_p = (hstorePairs_t) load_external_function("$libdir/hstore", "hstorePairs", true, NULL); - AssertVariableIsOfType(&hstoreCheckKeyLen, hstoreCheckKeyLen_t); hstoreCheckKeyLen_p = (hstoreCheckKeyLen_t) load_external_function("$libdir/hstore", "hstoreCheckKeyLen", true, NULL); - AssertVariableIsOfType(&hstoreCheckValLen, hstoreCheckValLen_t); hstoreCheckValLen_p = (hstoreCheckValLen_t) load_external_function("$libdir/hstore", "hstoreCheckValLen", true, NULL); diff --git a/contrib/hstore_plperl/meson.build b/contrib/hstore_plperl/meson.build index 60b8ad23569b9..705dbe69a4662 100644 --- a/contrib/hstore_plperl/meson.build +++ b/contrib/hstore_plperl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not perl_dep.found() subdir_done() diff --git a/contrib/hstore_plpython/hstore_plpython.c b/contrib/hstore_plpython/hstore_plpython.c index e2bfc6da38e18..d2be030e07cb7 100644 --- a/contrib/hstore_plpython/hstore_plpython.c +++ b/contrib/hstore_plpython/hstore_plpython.c @@ -28,6 +28,15 @@ static hstoreCheckKeyLen_t hstoreCheckKeyLen_p; typedef size_t (*hstoreCheckValLen_t) (size_t len); static hstoreCheckValLen_t hstoreCheckValLen_p; +/* Static asserts verify that typedefs above match original declarations */ +StaticAssertVariableIsOfType(&PLyObject_AsString, PLyObject_AsString_t); +StaticAssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); +StaticAssertVariableIsOfType(&hstoreUpgrade, hstoreUpgrade_t); +StaticAssertVariableIsOfType(&hstoreUniquePairs, hstoreUniquePairs_t); +StaticAssertVariableIsOfType(&hstorePairs, hstorePairs_t); +StaticAssertVariableIsOfType(&hstoreCheckKeyLen, hstoreCheckKeyLen_t); +StaticAssertVariableIsOfType(&hstoreCheckValLen, hstoreCheckValLen_t); + /* * Module initialize function: fetch function pointers for cross-module calls. @@ -35,32 +44,24 @@ static hstoreCheckValLen_t hstoreCheckValLen_p; void _PG_init(void) { - /* Asserts verify that typedefs above match original declarations */ - AssertVariableIsOfType(&PLyObject_AsString, PLyObject_AsString_t); PLyObject_AsString_p = (PLyObject_AsString_t) load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyObject_AsString", true, NULL); - AssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); PLyUnicode_FromStringAndSize_p = (PLyUnicode_FromStringAndSize_t) load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyUnicode_FromStringAndSize", true, NULL); - AssertVariableIsOfType(&hstoreUpgrade, hstoreUpgrade_t); hstoreUpgrade_p = (hstoreUpgrade_t) load_external_function("$libdir/hstore", "hstoreUpgrade", true, NULL); - AssertVariableIsOfType(&hstoreUniquePairs, hstoreUniquePairs_t); hstoreUniquePairs_p = (hstoreUniquePairs_t) load_external_function("$libdir/hstore", "hstoreUniquePairs", true, NULL); - AssertVariableIsOfType(&hstorePairs, hstorePairs_t); hstorePairs_p = (hstorePairs_t) load_external_function("$libdir/hstore", "hstorePairs", true, NULL); - AssertVariableIsOfType(&hstoreCheckKeyLen, hstoreCheckKeyLen_t); hstoreCheckKeyLen_p = (hstoreCheckKeyLen_t) load_external_function("$libdir/hstore", "hstoreCheckKeyLen", true, NULL); - AssertVariableIsOfType(&hstoreCheckValLen, hstoreCheckValLen_t); hstoreCheckValLen_p = (hstoreCheckValLen_t) load_external_function("$libdir/hstore", "hstoreCheckValLen", true, NULL); diff --git a/contrib/hstore_plpython/meson.build b/contrib/hstore_plpython/meson.build index ea8e20a377be4..555811b25b9b5 100644 --- a/contrib/hstore_plpython/meson.build +++ b/contrib/hstore_plpython/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not python3_dep.found() subdir_done() diff --git a/contrib/intagg/meson.build b/contrib/intagg/meson.build index 0978c86dc5bde..51f53874c5f10 100644 --- a/contrib/intagg/meson.build +++ b/contrib/intagg/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group install_data( 'intagg.control', diff --git a/contrib/intarray/_int_bool.c b/contrib/intarray/_int_bool.c index 2b2c3f4029ec5..f45df86d60c38 100644 --- a/contrib/intarray/_int_bool.c +++ b/contrib/intarray/_int_bool.c @@ -135,7 +135,7 @@ gettoken(WORKSTATE *state, int32 *val) static void pushquery(WORKSTATE *state, int32 type, int32 val) { - NODE *tmp = (NODE *) palloc(sizeof(NODE)); + NODE *tmp = palloc_object(NODE); tmp->type = type; tmp->val = val; @@ -346,7 +346,7 @@ gin_bool_consistent(QUERYTYPE *query, bool *check) * extraction code in ginint4_queryextract. */ gcv.first = items; - gcv.mapped_check = (bool *) palloc(sizeof(bool) * query->size); + gcv.mapped_check = palloc_array(bool, query->size); for (i = 0; i < query->size; i++) { if (items[i].type == VAL) @@ -613,7 +613,7 @@ infix(INFIX *in, bool first) nrm.curpol = in->curpol; nrm.buflen = 16; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); /* get right operand */ infix(&nrm, false); @@ -651,7 +651,7 @@ bqarr_out(PG_FUNCTION_ARGS) nrm.curpol = GETQUERY(query) + query->size - 1; nrm.buflen = 32; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); *(nrm.cur) = '\0'; infix(&nrm, true); diff --git a/contrib/intarray/_int_gin.c b/contrib/intarray/_int_gin.c index b7958d8eca5fb..baa1cc902b364 100644 --- a/contrib/intarray/_int_gin.c +++ b/contrib/intarray/_int_gin.c @@ -42,7 +42,7 @@ ginint4_queryextract(PG_FUNCTION_ARGS) /* * Extract all the VAL items as things we want GIN to check for. */ - res = (Datum *) palloc(sizeof(Datum) * query->size); + res = palloc_array(Datum, query->size); *nentries = 0; for (i = 0; i < query->size; i++) @@ -65,7 +65,7 @@ ginint4_queryextract(PG_FUNCTION_ARGS) int32 *arr; int32 i; - res = (Datum *) palloc(sizeof(Datum) * (*nentries)); + res = palloc_array(Datum, *nentries); arr = ARRPTR(query); for (i = 0; i < *nentries; i++) @@ -112,8 +112,9 @@ ginint4_consistent(PG_FUNCTION_ARGS) bool *check = (bool *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ +#ifdef NOT_USED + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(5); bool res = false; int32 i; diff --git a/contrib/intarray/_int_gist.c b/contrib/intarray/_int_gist.c index a09b7fa812cb2..586e19df01b4d 100644 --- a/contrib/intarray/_int_gist.c +++ b/contrib/intarray/_int_gist.c @@ -49,8 +49,9 @@ g_int_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); ArrayType *query = PG_GETARG_ARRAYTYPE_P_COPY(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool retval = false; /* silence compiler warning */ @@ -186,7 +187,7 @@ g_int_compress(PG_FUNCTION_ARGS) errmsg("input array is too big (%d maximum allowed, %d current), use gist__intbig_ops opclass instead", 2 * num_ranges - 1, ARRNELEMS(r)))); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, false); @@ -276,7 +277,7 @@ g_int_compress(PG_FUNCTION_ARGS) errmsg("data is too sparse, recreate index using gist__intbig_ops opclass instead"))); r = resize_intArrayType(r, len); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, false); PG_RETURN_POINTER(retval); @@ -306,7 +307,7 @@ g_int_decompress(PG_FUNCTION_ARGS) { if (in != (ArrayType *) DatumGetPointer(entry->key)) { - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(in), entry->rel, entry->page, entry->offset, false); PG_RETURN_POINTER(retval); @@ -321,7 +322,7 @@ g_int_decompress(PG_FUNCTION_ARGS) { /* not compressed value */ if (in != (ArrayType *) DatumGetPointer(entry->key)) { - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(in), entry->rel, entry->page, entry->offset, false); @@ -350,7 +351,7 @@ g_int_decompress(PG_FUNCTION_ARGS) if (in != (ArrayType *) DatumGetPointer(entry->key)) pfree(in); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, false); @@ -535,7 +536,7 @@ g_int_picksplit(PG_FUNCTION_ARGS) /* * sort entries */ - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) { costvector[i - 1].pos = i; diff --git a/contrib/intarray/_int_op.c b/contrib/intarray/_int_op.c index ba6d0a99995ed..a706e353c6f94 100644 --- a/contrib/intarray/_int_op.c +++ b/contrib/intarray/_int_op.c @@ -108,7 +108,7 @@ _int_overlap(PG_FUNCTION_ARGS) CHECKARRVALID(a); CHECKARRVALID(b); if (ARRISEMPTY(a) || ARRISEMPTY(b)) - return false; + PG_RETURN_BOOL(false); SORT(a); SORT(b); diff --git a/contrib/intarray/_int_selfuncs.c b/contrib/intarray/_int_selfuncs.c index 6c3b7ace146aa..7fce743632fbb 100644 --- a/contrib/intarray/_int_selfuncs.c +++ b/contrib/intarray/_int_selfuncs.c @@ -3,7 +3,7 @@ * _int_selfuncs.c * Functions for selectivity estimation of intarray operators * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,6 +19,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_statistic.h" #include "catalog/pg_type.h" +#include "commands/extension.h" #include "miscadmin.h" #include "utils/fmgrprotos.h" #include "utils/lsyscache.h" @@ -170,14 +171,25 @@ _int_matchsel(PG_FUNCTION_ARGS) PG_RETURN_FLOAT8(0.0); } - /* The caller made sure the const is a query, so get it now */ + /* + * Verify that the Const is a query_int, else return a default estimate. + * (This could only fail if someone attached this estimator to the wrong + * operator.) + */ + if (((Const *) other)->consttype != + get_function_sibling_type(fcinfo->flinfo->fn_oid, "query_int")) + { + ReleaseVariableStats(vardata); + PG_RETURN_FLOAT8(DEFAULT_EQ_SEL); + } + query = DatumGetQueryTypeP(((Const *) other)->constvalue); /* Empty query matches nothing */ if (query->size == 0) { ReleaseVariableStats(vardata); - return (Selectivity) 0.0; + PG_RETURN_FLOAT8(0.0); } /* @@ -210,8 +222,8 @@ _int_matchsel(PG_FUNCTION_ARGS) */ if (sslot.nnumbers == sslot.nvalues + 3) { - /* Grab the lowest frequency. */ - minfreq = sslot.numbers[sslot.nnumbers - (sslot.nnumbers - sslot.nvalues)]; + /* Grab the minimal MCE frequency. */ + minfreq = sslot.numbers[sslot.nvalues]; mcelems = sslot.values; mcefreqs = sslot.numbers; @@ -269,8 +281,11 @@ int_query_opr_selec(ITEM *item, Datum *mcelems, float4 *mcefreqs, else { /* - * The element is not in MCELEM. Punt, but assume that the - * selectivity cannot be more than minfreq / 2. + * The element is not in MCELEM. Estimate its frequency as half + * that of the least-frequent MCE. (We know it cannot be more + * than minfreq, and it could be a great deal less. Half seems + * like a good compromise.) For probably-historical reasons, + * clamp to not more than DEFAULT_EQ_SEL. */ selec = Min(DEFAULT_EQ_SEL, minfreq / 2); } @@ -325,8 +340,13 @@ int_query_opr_selec(ITEM *item, Datum *mcelems, float4 *mcefreqs, static int compare_val_int4(const void *a, const void *b) { - int32 key = *(int32 *) a; - const Datum *t = (const Datum *) b; + int32 key = *(const int32 *) a; + int32 value = DatumGetInt32(*(const Datum *) b); - return key - DatumGetInt32(*t); + if (key < value) + return -1; + else if (key > value) + return 1; + else + return 0; } diff --git a/contrib/intarray/_intbig_gist.c b/contrib/intarray/_intbig_gist.c index 9699fbf3b4fe5..6ffb03dab5826 100644 --- a/contrib/intarray/_intbig_gist.c +++ b/contrib/intarray/_intbig_gist.c @@ -3,8 +3,6 @@ */ #include "postgres.h" -#include - #include "_int.h" #include "access/gist.h" #include "access/reloptions.h" @@ -174,7 +172,7 @@ g_intbig_compress(PG_FUNCTION_ARGS) ptr++; } - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, false); @@ -195,7 +193,7 @@ g_intbig_compress(PG_FUNCTION_ARGS) } res = _intbig_alloc(true, siglen, sign); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, false); @@ -385,7 +383,7 @@ g_intbig_picksplit(PG_FUNCTION_ARGS) maxoff = OffsetNumberNext(maxoff); /* sort before ... */ - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) { costvector[j - 1].pos = j; @@ -467,8 +465,9 @@ g_intbig_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); ArrayType *query = PG_GETARG_ARRAYTYPE_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int siglen = GET_SIGLEN(); bool retval; diff --git a/contrib/intarray/bench/bench.pl b/contrib/intarray/bench/bench.pl index dfad66baea4a1..78750791ee844 100755 --- a/contrib/intarray/bench/bench.pl +++ b/contrib/intarray/bench/bench.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/intarray/bench/create_test.pl b/contrib/intarray/bench/create_test.pl index ed8132b088893..45c888e307436 100755 --- a/contrib/intarray/bench/create_test.pl +++ b/contrib/intarray/bench/create_test.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # contrib/intarray/bench/create_test.pl diff --git a/contrib/intarray/meson.build b/contrib/intarray/meson.build index fae9add981dc6..e49ff77f16799 100644 --- a/contrib/intarray/meson.build +++ b/contrib/intarray/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group intarray_sources = files( '_int_bool.c', diff --git a/contrib/isn/UPC.h b/contrib/isn/UPC.h index 01b9f1559255c..9af19a369c7b8 100644 --- a/contrib/isn/UPC.h +++ b/contrib/isn/UPC.h @@ -1,5 +1,5 @@ /* - * ISSN.h + * UPC.h * PostgreSQL type definitions for ISNs (ISBN, ISMN, ISSN, EAN13, UPC) * * No information available for UPC prefixes diff --git a/contrib/isn/isn.c b/contrib/isn/isn.c index 038c8ed4db7bd..bbe4344e6adb0 100644 --- a/contrib/isn/isn.c +++ b/contrib/isn/isn.c @@ -4,7 +4,7 @@ * PostgreSQL type definitions for ISNs (ISBN, ISMN, ISSN, EAN13, UPC) * * Author: German Mendez Bravo (Kronuz) - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/isn/isn.c @@ -423,19 +423,10 @@ ean2isn(ean13 ean, bool errorOK, ean13 *result, enum isn_type accept) eantoobig: if (!errorOK) - { - char eanbuf[64]; - - /* - * Format the number separately to keep the machine-dependent format - * code out of the translatable message text - */ - snprintf(eanbuf, sizeof(eanbuf), EAN13_FORMAT, ean); ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value \"%s\" is out of range for %s type", - eanbuf, isn_names[type]))); - } + errmsg("value \"%" PRIu64 "\" is out of range for %s type", + ean, isn_names[type]))); return false; } @@ -660,19 +651,10 @@ ean2string(ean13 ean, bool errorOK, char *result, bool shortType) eantoobig: if (!errorOK) - { - char eanbuf[64]; - - /* - * Format the number separately to keep the machine-dependent format - * code out of the translatable message text - */ - snprintf(eanbuf, sizeof(eanbuf), EAN13_FORMAT, ean); ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value \"%s\" is out of range for %s type", - eanbuf, isn_names[type]))); - } + errmsg("value \"%" PRIu64 "\" is out of range for %s type", + ean, isn_names[type]))); return false; } @@ -726,7 +708,7 @@ string2ean(const char *str, struct Node *escontext, ean13 *result, if (type != INVALID) goto eaninvalid; type = ISSN; - *aux1++ = toupper((unsigned char) *aux2); + *aux1++ = pg_ascii_toupper((unsigned char) *aux2); length++; } else if (length == 9 && (digit || *aux2 == 'X' || *aux2 == 'x') && last) @@ -736,7 +718,7 @@ string2ean(const char *str, struct Node *escontext, ean13 *result, goto eaninvalid; if (type == INVALID) type = ISBN; /* ISMN must start with 'M' */ - *aux1++ = toupper((unsigned char) *aux2); + *aux1++ = pg_ascii_toupper((unsigned char) *aux2); length++; } else if (length == 11 && digit && last) @@ -855,6 +837,7 @@ string2ean(const char *str, struct Node *escontext, ean13 *result, case UPC: buf[2] = '0'; valid = (valid && ((rcheck = checkdig(buf + 2, 13)) == check || magic)); + break; default: break; } diff --git a/contrib/isn/isn.h b/contrib/isn/isn.h index 399896ad417c0..09b54db438844 100644 --- a/contrib/isn/isn.h +++ b/contrib/isn/isn.h @@ -4,7 +4,7 @@ * PostgreSQL type definitions for ISNs (ISBN, ISMN, ISSN, EAN13, UPC) * * Author: German Mendez Bravo (Kronuz) - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/isn/isn.h @@ -24,8 +24,6 @@ */ typedef uint64 ean13; -#define EAN13_FORMAT UINT64_FORMAT - #define PG_GETARG_EAN13(n) PG_GETARG_INT64(n) #define PG_RETURN_EAN13(x) PG_RETURN_INT64(x) diff --git a/contrib/isn/meson.build b/contrib/isn/meson.build index 39cf781684eee..cecd70043c8e2 100644 --- a/contrib/isn/meson.build +++ b/contrib/isn/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group isn_sources = files( 'isn.c', diff --git a/contrib/jsonb_plperl/jsonb_plperl.c b/contrib/jsonb_plperl/jsonb_plperl.c index c02e2d41af108..f8e4a584fddf0 100644 --- a/contrib/jsonb_plperl/jsonb_plperl.c +++ b/contrib/jsonb_plperl/jsonb_plperl.c @@ -13,7 +13,7 @@ PG_MODULE_MAGIC_EXT( ); static SV *Jsonb_to_SV(JsonbContainer *jsonb); -static JsonbValue *SV_to_JsonbValue(SV *obj, JsonbParseState **ps, bool is_elem); +static void SV_to_JsonbValue(SV *in, JsonbInState *jsonb_state, bool is_elem); static SV * @@ -127,8 +127,8 @@ Jsonb_to_SV(JsonbContainer *jsonb) } } -static JsonbValue * -AV_to_JsonbValue(AV *in, JsonbParseState **jsonb_state) +static void +AV_to_JsonbValue(AV *in, JsonbInState *jsonb_state) { dTHX; SSize_t pcount = av_len(in) + 1; @@ -141,14 +141,14 @@ AV_to_JsonbValue(AV *in, JsonbParseState **jsonb_state) SV **value = av_fetch(in, i, FALSE); if (value) - (void) SV_to_JsonbValue(*value, jsonb_state, true); + SV_to_JsonbValue(*value, jsonb_state, true); } - return pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL); + pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL); } -static JsonbValue * -HV_to_JsonbValue(HV *obj, JsonbParseState **jsonb_state) +static void +HV_to_JsonbValue(HV *obj, JsonbInState *jsonb_state) { dTHX; JsonbValue key; @@ -167,14 +167,14 @@ HV_to_JsonbValue(HV *obj, JsonbParseState **jsonb_state) key.val.string.val = pnstrdup(kstr, klen); key.val.string.len = klen; pushJsonbValue(jsonb_state, WJB_KEY, &key); - (void) SV_to_JsonbValue(val, jsonb_state, false); + SV_to_JsonbValue(val, jsonb_state, false); } - return pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL); + pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL); } -static JsonbValue * -SV_to_JsonbValue(SV *in, JsonbParseState **jsonb_state, bool is_elem) +static void +SV_to_JsonbValue(SV *in, JsonbInState *jsonb_state, bool is_elem) { dTHX; JsonbValue out; /* result */ @@ -186,10 +186,12 @@ SV_to_JsonbValue(SV *in, JsonbParseState **jsonb_state, bool is_elem) switch (SvTYPE(in)) { case SVt_PVAV: - return AV_to_JsonbValue((AV *) in, jsonb_state); + AV_to_JsonbValue((AV *) in, jsonb_state); + return; case SVt_PVHV: - return HV_to_JsonbValue((HV *) in, jsonb_state); + HV_to_JsonbValue((HV *) in, jsonb_state); + return; default: if (!SvOK(in)) @@ -259,14 +261,24 @@ SV_to_JsonbValue(SV *in, JsonbParseState **jsonb_state, bool is_elem) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot transform this Perl type to jsonb"))); - return NULL; } } - /* Push result into 'jsonb_state' unless it is a raw scalar. */ - return *jsonb_state - ? pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, &out) - : memcpy(palloc(sizeof(JsonbValue)), &out, sizeof(JsonbValue)); + if (jsonb_state->parseState) + { + /* We're in an array or object, so push value as element or field. */ + pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, &out); + } + else + { + /* + * We are at top level, so it's a raw scalar. If we just shove the + * scalar value into jsonb_state->result, JsonbValueToJsonb will take + * care of wrapping it into a dummy array. + */ + jsonb_state->result = palloc_object(JsonbValue); + memcpy(jsonb_state->result, &out, sizeof(JsonbValue)); + } } @@ -289,10 +301,9 @@ Datum plperl_to_jsonb(PG_FUNCTION_ARGS) { dTHX; - JsonbParseState *jsonb_state = NULL; SV *in = (SV *) PG_GETARG_POINTER(0); - JsonbValue *out = SV_to_JsonbValue(in, &jsonb_state, true); - Jsonb *result = JsonbValueToJsonb(out); + JsonbInState jsonb_state = {0}; - PG_RETURN_JSONB_P(result); + SV_to_JsonbValue(in, &jsonb_state, true); + PG_RETURN_JSONB_P(JsonbValueToJsonb(jsonb_state.result)); } diff --git a/contrib/jsonb_plperl/meson.build b/contrib/jsonb_plperl/meson.build index 95a9a7bc08242..8bfabee54555f 100644 --- a/contrib/jsonb_plperl/meson.build +++ b/contrib/jsonb_plperl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not perl_dep.found() subdir_done() diff --git a/contrib/jsonb_plpython/jsonb_plpython.c b/contrib/jsonb_plpython/jsonb_plpython.c index 9383615abbfa3..c2c4ce37c0853 100644 --- a/contrib/jsonb_plpython/jsonb_plpython.c +++ b/contrib/jsonb_plpython/jsonb_plpython.c @@ -26,29 +26,31 @@ static PLy_elog_impl_t PLy_elog_impl_p; static PyObject *decimal_constructor; static PyObject *PLyObject_FromJsonbContainer(JsonbContainer *jsonb); -static JsonbValue *PLyObject_ToJsonbValue(PyObject *obj, - JsonbParseState **jsonb_state, bool is_elem); +static void PLyObject_ToJsonbValue(PyObject *obj, + JsonbInState *jsonb_state, bool is_elem); typedef PyObject *(*PLyUnicode_FromStringAndSize_t) (const char *s, Py_ssize_t size); static PLyUnicode_FromStringAndSize_t PLyUnicode_FromStringAndSize_p; +/* Static asserts verify that typedefs above match original declarations */ +StaticAssertVariableIsOfType(&PLyObject_AsString, PLyObject_AsString_t); +StaticAssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); +StaticAssertVariableIsOfType(&PLy_elog_impl, PLy_elog_impl_t); + + /* * Module initialize function: fetch function pointers for cross-module calls. */ void _PG_init(void) { - /* Asserts verify that typedefs above match original declarations */ - AssertVariableIsOfType(&PLyObject_AsString, PLyObject_AsString_t); PLyObject_AsString_p = (PLyObject_AsString_t) load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyObject_AsString", true, NULL); - AssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); PLyUnicode_FromStringAndSize_p = (PLyUnicode_FromStringAndSize_t) load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyUnicode_FromStringAndSize", true, NULL); - AssertVariableIsOfType(&PLy_elog_impl, PLy_elog_impl_t); PLy_elog_impl_p = (PLy_elog_impl_t) load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLy_elog_impl", true, NULL); @@ -261,12 +263,11 @@ PLyObject_FromJsonbContainer(JsonbContainer *jsonb) * * Transform Python dict to JsonbValue. */ -static JsonbValue * -PLyMapping_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) +static void +PLyMapping_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state) { Py_ssize_t pcount; PyObject *volatile items; - JsonbValue *volatile out; pcount = PyMapping_Size(obj); items = PyMapping_Items(obj); @@ -297,19 +298,17 @@ PLyMapping_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) PLyUnicode_ToJsonbValue(key, &jbvKey); } - (void) pushJsonbValue(jsonb_state, WJB_KEY, &jbvKey); - (void) PLyObject_ToJsonbValue(value, jsonb_state, false); + pushJsonbValue(jsonb_state, WJB_KEY, &jbvKey); + PLyObject_ToJsonbValue(value, jsonb_state, false); } - out = pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL); + pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL); } PG_FINALLY(); { Py_DECREF(items); } PG_END_TRY(); - - return out; } /* @@ -318,8 +317,8 @@ PLyMapping_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) * Transform python list to JsonbValue. Expects transformed PyObject and * a state required for jsonb construction. */ -static JsonbValue * -PLySequence_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) +static void +PLySequence_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state) { Py_ssize_t i; Py_ssize_t pcount; @@ -336,7 +335,7 @@ PLySequence_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) value = PySequence_GetItem(obj, i); Assert(value); - (void) PLyObject_ToJsonbValue(value, jsonb_state, true); + PLyObject_ToJsonbValue(value, jsonb_state, true); Py_XDECREF(value); value = NULL; } @@ -348,7 +347,7 @@ PLySequence_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state) } PG_END_TRY(); - return pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL); + pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL); } /* @@ -406,20 +405,26 @@ PLyNumber_ToJsonbValue(PyObject *obj, JsonbValue *jbvNum) * * Transform python object to JsonbValue. */ -static JsonbValue * -PLyObject_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state, bool is_elem) +static void +PLyObject_ToJsonbValue(PyObject *obj, JsonbInState *jsonb_state, bool is_elem) { JsonbValue *out; if (!PyUnicode_Check(obj)) { if (PySequence_Check(obj)) - return PLySequence_ToJsonbValue(obj, jsonb_state); + { + PLySequence_ToJsonbValue(obj, jsonb_state); + return; + } else if (PyMapping_Check(obj)) - return PLyMapping_ToJsonbValue(obj, jsonb_state); + { + PLyMapping_ToJsonbValue(obj, jsonb_state); + return; + } } - out = palloc(sizeof(JsonbValue)); + out = palloc_object(JsonbValue); if (obj == Py_None) out->type = jbvNull; @@ -443,10 +448,20 @@ PLyObject_ToJsonbValue(PyObject *obj, JsonbParseState **jsonb_state, bool is_ele errmsg("Python type \"%s\" cannot be transformed to jsonb", PLyObject_AsString((PyObject *) obj->ob_type)))); - /* Push result into 'jsonb_state' unless it is raw scalar value. */ - return (*jsonb_state ? - pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, out) : - out); + if (jsonb_state->parseState) + { + /* We're in an array or object, so push value as element or field. */ + pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, out); + } + else + { + /* + * We are at top level, so it's a raw scalar. If we just shove the + * scalar value into jsonb_state->result, JsonbValueToJsonb will take + * care of wrapping it into a dummy array. + */ + jsonb_state->result = out; + } } /* @@ -458,13 +473,11 @@ PG_FUNCTION_INFO_V1(plpython_to_jsonb); Datum plpython_to_jsonb(PG_FUNCTION_ARGS) { - PyObject *obj; - JsonbValue *out; - JsonbParseState *jsonb_state = NULL; + PyObject *obj = (PyObject *) PG_GETARG_POINTER(0); + JsonbInState jsonb_state = {0}; - obj = (PyObject *) PG_GETARG_POINTER(0); - out = PLyObject_ToJsonbValue(obj, &jsonb_state, true); - PG_RETURN_POINTER(JsonbValueToJsonb(out)); + PLyObject_ToJsonbValue(obj, &jsonb_state, true); + PG_RETURN_POINTER(JsonbValueToJsonb(jsonb_state.result)); } /* diff --git a/contrib/jsonb_plpython/meson.build b/contrib/jsonb_plpython/meson.build index 5fe80483e587c..71299707418c8 100644 --- a/contrib/jsonb_plpython/meson.build +++ b/contrib/jsonb_plpython/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not python3_dep.found() subdir_done() diff --git a/contrib/lo/meson.build b/contrib/lo/meson.build index 2d78907ba12a8..e43eb9b2d2849 100644 --- a/contrib/lo/meson.build +++ b/contrib/lo/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group lo_sources = files( 'lo.c', diff --git a/contrib/ltree/_ltree_gist.c b/contrib/ltree/_ltree_gist.c index 286ad24fbe847..07d6682359245 100644 --- a/contrib/ltree/_ltree_gist.c +++ b/contrib/ltree/_ltree_gist.c @@ -7,8 +7,6 @@ */ #include "postgres.h" -#include - #include "access/gist.h" #include "access/reloptions.h" #include "access/stratnum.h" @@ -79,12 +77,12 @@ _ltree_compress(PG_FUNCTION_ARGS) item = NEXTVAL(item); } - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, entry->offset, false); } - else if (!LTG_ISALLTRUE(entry->key)) + else if (!LTG_ISALLTRUE(DatumGetPointer(entry->key))) { int32 i; ltree_gist *key; @@ -97,7 +95,7 @@ _ltree_compress(PG_FUNCTION_ARGS) } key = ltree_gist_alloc(true, sign, siglen, NULL, NULL); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, entry->offset, false); @@ -310,7 +308,7 @@ _ltree_picksplit(PG_FUNCTION_ARGS) maxoff = OffsetNumberNext(maxoff); /* sort before ... */ - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) { costvector[j - 1].pos = j; @@ -508,8 +506,9 @@ _ltree_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); void *query = PG_DETOAST_DATUM(PG_GETARG_DATUM(1)); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int siglen = LTREE_GET_ASIGLEN(); ltree_gist *key = (ltree_gist *) DatumGetPointer(entry->key); diff --git a/contrib/ltree/_ltree_op.c b/contrib/ltree/_ltree_op.c index b4a8097328d3a..4d54ad34bb69f 100644 --- a/contrib/ltree/_ltree_op.c +++ b/contrib/ltree/_ltree_op.c @@ -307,7 +307,7 @@ _lca(PG_FUNCTION_ARGS) (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("array must not contain nulls"))); - a = (ltree **) palloc(sizeof(ltree *) * num); + a = palloc_array(ltree *, num); while (num > 0) { num--; diff --git a/contrib/ltree/crc32.c b/contrib/ltree/crc32.c index 134f46a805e56..d21bed31fdd47 100644 --- a/contrib/ltree/crc32.c +++ b/contrib/ltree/crc32.c @@ -10,31 +10,62 @@ #include "postgres.h" #include "ltree.h" +#include "crc32.h" +#include "utils/pg_crc.h" #ifdef LOWER_NODE -#include -#define TOLOWER(x) tolower((unsigned char) (x)) -#else -#define TOLOWER(x) (x) +#include "utils/pg_locale.h" #endif -#include "crc32.h" -#include "utils/pg_crc.h" +#ifdef LOWER_NODE unsigned int ltree_crc32_sz(const char *buf, int size) { pg_crc32 crc; const char *p = buf; + const char *end = buf + size; + static pg_locale_t locale = NULL; + + if (!locale) + locale = pg_database_locale(); INIT_TRADITIONAL_CRC32(crc); while (size > 0) { - char c = (char) TOLOWER(*p); + char foldstr[UNICODE_CASEMAP_BUFSZ]; + int srclen = pg_mblen_range(p, end); + size_t foldlen; + + /* fold one codepoint at a time */ + foldlen = pg_strfold(foldstr, UNICODE_CASEMAP_BUFSZ, p, srclen, + locale); + + COMP_TRADITIONAL_CRC32(crc, foldstr, foldlen); + + size -= srclen; + p += srclen; + } + FIN_TRADITIONAL_CRC32(crc); + return (unsigned int) crc; +} + +#else - COMP_TRADITIONAL_CRC32(crc, &c, 1); +unsigned int +ltree_crc32_sz(const char *buf, int size) +{ + pg_crc32 crc; + const char *p = buf; + + INIT_TRADITIONAL_CRC32(crc); + while (size > 0) + { + COMP_TRADITIONAL_CRC32(crc, p, 1); size--; p++; } FIN_TRADITIONAL_CRC32(crc); return (unsigned int) crc; } + +#endif /* !LOWER_NODE */ diff --git a/contrib/ltree/expected/ltree.out b/contrib/ltree/expected/ltree.out index c8eac3f6b21bc..d2a566284755b 100644 --- a/contrib/ltree/expected/ltree.out +++ b/contrib/ltree/expected/ltree.out @@ -128,6 +128,8 @@ SELECT subpath('Top.Child1.Child2',1); Child1.Child2 (1 row) +SELECT subpath('Top.Child1.Child2',-4); -- error +ERROR: invalid positions SELECT index('1.2.3.4.5.6','1.2'); index ------- diff --git a/contrib/ltree/lquery_op.c b/contrib/ltree/lquery_op.c index a6466f575fd7d..e6a1969c3d39a 100644 --- a/contrib/ltree/lquery_op.c +++ b/contrib/ltree/lquery_op.c @@ -27,21 +27,21 @@ getlexeme(char *start, char *end, int *len) char *ptr; while (start < end && t_iseq(start, '_')) - start += pg_mblen(start); + start += pg_mblen_range(start, end); ptr = start; if (ptr >= end) return NULL; while (ptr < end && !t_iseq(ptr, '_')) - ptr += pg_mblen(ptr); + ptr += pg_mblen_range(ptr, end); *len = ptr - start; return start; } bool -compare_subnode(ltree_level *t, char *qn, int len, int (*cmpptr) (const char *, const char *, size_t), bool anyend) +compare_subnode(ltree_level *t, char *qn, int len, bool prefix, bool ci) { char *endt = t->name + t->len; char *endq = qn + len; @@ -56,10 +56,8 @@ compare_subnode(ltree_level *t, char *qn, int len, int (*cmpptr) (const char *, isok = false; while ((tn = getlexeme(tn, endt, &lent)) != NULL) { - if ((lent == lenq || (lent > lenq && anyend)) && - (*cmpptr) (qn, tn, lenq) == 0) + if (ltree_label_match(qn, lenq, tn, lent, prefix, ci)) { - isok = true; break; } @@ -74,17 +72,70 @@ compare_subnode(ltree_level *t, char *qn, int len, int (*cmpptr) (const char *, return true; } -int -ltree_strncasecmp(const char *a, const char *b, size_t s) +/* + * Check if the label matches the predicate string. If 'prefix' is true, then + * the predicate string is treated as a prefix. If 'ci' is true, then the + * predicate string is case-insensitive (and locale-aware). + */ +bool +ltree_label_match(const char *pred, size_t pred_len, const char *label, + size_t label_len, bool prefix, bool ci) { - char *al = str_tolower(a, s, DEFAULT_COLLATION_OID); - char *bl = str_tolower(b, s, DEFAULT_COLLATION_OID); - int res; + static pg_locale_t locale = NULL; + char *fpred; /* casefolded predicate */ + size_t fpred_len = pred_len; + char *flabel; /* casefolded label */ + size_t flabel_len = label_len; + size_t len; + bool res; + + /* fast path for binary match or binary prefix match */ + if ((pred_len == label_len || (prefix && pred_len < label_len)) && + strncmp(pred, label, pred_len) == 0) + return true; + else if (!ci) + return false; + + /* + * Slow path for case-insensitive comparison: case fold and then compare. + * This path is necessary even if pred_len > label_len, because the byte + * lengths may change after casefolding. + */ + if (!locale) + locale = pg_database_locale(); + + fpred = palloc(fpred_len + 1); + len = pg_strfold(fpred, fpred_len + 1, pred, pred_len, locale); + if (len > fpred_len) + { + /* grow buffer if needed and retry */ + fpred_len = len; + fpred = repalloc(fpred, fpred_len + 1); + len = pg_strfold(fpred, fpred_len + 1, pred, pred_len, locale); + } + Assert(len <= fpred_len); + fpred_len = len; - res = strncmp(al, bl, s); + flabel = palloc(flabel_len + 1); + len = pg_strfold(flabel, flabel_len + 1, label, label_len, locale); + if (len > flabel_len) + { + /* grow buffer if needed and retry */ + flabel_len = len; + flabel = repalloc(flabel, flabel_len + 1); + len = pg_strfold(flabel, flabel_len + 1, label, label_len, locale); + } + Assert(len <= flabel_len); + flabel_len = len; - pfree(al); - pfree(bl); + if ((fpred_len == flabel_len || (prefix && fpred_len < flabel_len)) && + strncmp(fpred, flabel, fpred_len) == 0) + res = true; + else + res = false; + + pfree(fpred); + pfree(flabel); return res; } @@ -109,19 +160,16 @@ checkLevel(lquery_level *curq, ltree_level *curt) for (int i = 0; i < curq->numvar; i++) { - int (*cmpptr) (const char *, const char *, size_t); - - cmpptr = (curvar->flag & LVAR_INCASE) ? ltree_strncasecmp : strncmp; + bool prefix = (curvar->flag & LVAR_ANYEND); + bool ci = (curvar->flag & LVAR_INCASE); if (curvar->flag & LVAR_SUBLEXEME) { - if (compare_subnode(curt, curvar->name, curvar->len, cmpptr, - (curvar->flag & LVAR_ANYEND))) + if (compare_subnode(curt, curvar->name, curvar->len, prefix, ci)) return success; } - else if ((curvar->len == curt->len || - (curt->len > curvar->len && (curvar->flag & LVAR_ANYEND))) && - (*cmpptr) (curvar->name, curt->name, curvar->len) == 0) + else if (ltree_label_match(curvar->name, curvar->len, curt->name, + curt->len, prefix, ci)) return success; curvar = LVAR_NEXT(curvar); diff --git a/contrib/ltree/ltree.h b/contrib/ltree/ltree.h index 5e0761641d32a..226c1cb211501 100644 --- a/contrib/ltree/ltree.h +++ b/contrib/ltree/ltree.h @@ -127,7 +127,7 @@ typedef struct #define LQUERY_HASNOT 0x01 /* valid label chars are alphanumerics, underscores and hyphens */ -#define ISLABEL(x) ( t_isalnum(x) || t_iseq(x, '_') || t_iseq(x, '-') ) +#define ISLABEL(x) ( t_isalnum_cstr(x) || t_iseq(x, '_') || t_iseq(x, '-') ) /* full text query */ @@ -207,10 +207,11 @@ bool ltree_execute(ITEM *curitem, void *checkval, int ltree_compare(const ltree *a, const ltree *b); bool inner_isparent(const ltree *c, const ltree *p); -bool compare_subnode(ltree_level *t, char *qn, int len, - int (*cmpptr) (const char *, const char *, size_t), bool anyend); +bool compare_subnode(ltree_level *t, char *qn, int len, bool prefix, bool ci); ltree *lca_inner(ltree **a, int len); -int ltree_strncasecmp(const char *a, const char *b, size_t s); +bool ltree_label_match(const char *pred, size_t pred_len, + const char *label, size_t label_len, + bool prefix, bool ci); /* fmgr macros for ltree objects */ #define DatumGetLtreeP(X) ((ltree *) PG_DETOAST_DATUM(X)) diff --git a/contrib/ltree/ltree_gist.c b/contrib/ltree/ltree_gist.c index 932f69bff2d18..78c9505299098 100644 --- a/contrib/ltree/ltree_gist.c +++ b/contrib/ltree/ltree_gist.c @@ -101,7 +101,7 @@ ltree_compress(PG_FUNCTION_ARGS) ltree *val = DatumGetLtreeP(entry->key); ltree_gist *key = ltree_gist_alloc(false, NULL, 0, val, 0); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, entry->offset, false); @@ -117,7 +117,7 @@ ltree_decompress(PG_FUNCTION_ARGS) if (PointerGetDatum(key) != entry->key) { - GISTENTRY *retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + GISTENTRY *retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, @@ -318,7 +318,7 @@ ltree_picksplit(PG_FUNCTION_ARGS) v->spl_right = (OffsetNumber *) palloc(nbytes); v->spl_nleft = 0; v->spl_nright = 0; - array = (RIX *) palloc(sizeof(RIX) * (maxoff + 1)); + array = palloc_array(RIX, maxoff + 1); /* copy the data into RIXes, and sort the RIXes */ for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) @@ -618,8 +618,9 @@ ltree_consistent(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int siglen = LTREE_GET_SIGLEN(); ltree_gist *key = (ltree_gist *) DatumGetPointer(entry->key); diff --git a/contrib/ltree/ltree_io.c b/contrib/ltree/ltree_io.c index b54a15d6c685e..54c4ca3c5c3b5 100644 --- a/contrib/ltree/ltree_io.c +++ b/contrib/ltree/ltree_io.c @@ -54,7 +54,7 @@ parse_ltree(const char *buf, struct Node *escontext) ptr = buf; while (*ptr) { - charlen = pg_mblen(ptr); + charlen = pg_mblen_cstr(ptr); if (t_iseq(ptr, '.')) num++; ptr += charlen; @@ -65,11 +65,11 @@ parse_ltree(const char *buf, struct Node *escontext) (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("number of ltree labels (%d) exceeds the maximum allowed (%d)", num + 1, LTREE_MAX_LEVELS))); - list = lptr = (nodeitem *) palloc(sizeof(nodeitem) * (num + 1)); + list = lptr = palloc_array(nodeitem, num + 1); ptr = buf; while (*ptr) { - charlen = pg_mblen(ptr); + charlen = pg_mblen_cstr(ptr); switch (state) { @@ -291,7 +291,7 @@ parse_lquery(const char *buf, struct Node *escontext) ptr = buf; while (*ptr) { - charlen = pg_mblen(ptr); + charlen = pg_mblen_cstr(ptr); if (t_iseq(ptr, '.')) num++; @@ -311,21 +311,21 @@ parse_lquery(const char *buf, struct Node *escontext) ptr = buf; while (*ptr) { - charlen = pg_mblen(ptr); + charlen = pg_mblen_cstr(ptr); switch (state) { case LQPRS_WAITLEVEL: if (ISLABEL(ptr)) { - GETVAR(curqlevel) = lptr = (nodeitem *) palloc0(sizeof(nodeitem) * (numOR + 1)); + GETVAR(curqlevel) = lptr = palloc0_array(nodeitem, numOR + 1); lptr->start = ptr; state = LQPRS_WAITDELIM; curqlevel->numvar = 1; } else if (t_iseq(ptr, '!')) { - GETVAR(curqlevel) = lptr = (nodeitem *) palloc0(sizeof(nodeitem) * (numOR + 1)); + GETVAR(curqlevel) = lptr = palloc0_array(nodeitem, numOR + 1); lptr->start = ptr + 1; lptr->wlen = -1; /* compensate for counting ! below */ state = LQPRS_WAITDELIM; diff --git a/contrib/ltree/ltree_op.c b/contrib/ltree/ltree_op.c index ce9f4caad4feb..c1fc77fc804c0 100644 --- a/contrib/ltree/ltree_op.c +++ b/contrib/ltree/ltree_op.c @@ -316,23 +316,15 @@ subpath(PG_FUNCTION_ARGS) int32 end; ltree *res; - end = start + len; - - if (start < 0) - { - start = t->numlevel + start; - end = start + len; - } if (start < 0) - { /* start > t->numlevel */ start = t->numlevel + start; - end = start + len; - } if (len < 0) end = t->numlevel + len; else if (len == 0) - end = (fcinfo->nargs == 3) ? start : 0xffff; + end = (fcinfo->nargs == 3) ? start : LTREE_MAX_LEVELS; + else + end = start + len; res = inner_subltree(t, start, end); @@ -574,7 +566,7 @@ lca(PG_FUNCTION_ARGS) ltree **a, *res; - a = (ltree **) palloc(sizeof(ltree *) * fcinfo->nargs); + a = palloc_array(ltree *, fcinfo->nargs); for (i = 0; i < fcinfo->nargs; i++) a[i] = PG_GETARG_LTREE_P(i); res = lca_inner(a, (int) fcinfo->nargs); diff --git a/contrib/ltree/ltxtquery_io.c b/contrib/ltree/ltxtquery_io.c index ec331607793a7..f4296880c035a 100644 --- a/contrib/ltree/ltxtquery_io.c +++ b/contrib/ltree/ltxtquery_io.c @@ -64,7 +64,7 @@ gettoken_query(QPRS_STATE *state, int32 *val, int32 *lenval, char **strval, uint for (;;) { - charlen = pg_mblen(state->buf); + charlen = pg_mblen_cstr(state->buf); switch (state->state) { @@ -154,7 +154,7 @@ gettoken_query(QPRS_STATE *state, int32 *val, int32 *lenval, char **strval, uint static bool pushquery(QPRS_STATE *state, int32 type, int32 val, int32 distance, int32 lenval, uint16 flag) { - NODE *tmp = (NODE *) palloc(sizeof(NODE)); + NODE *tmp = palloc_object(NODE); tmp->type = type; tmp->val = val; @@ -277,7 +277,7 @@ makepol(QPRS_STATE *state) case ERR: if (SOFT_ERROR_OCCURRED(state->escontext)) return ERR; - /* fall through */ + pg_fallthrough; default: ereturn(state->escontext, ERR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -338,11 +338,6 @@ queryin(char *buf, struct Node *escontext) NODE *tmp; int32 pos = 0; -#ifdef BS_DEBUG - char pbuf[16384], - *cur; -#endif - /* init state */ state.buf = buf; state.state = WAITOPERAND; @@ -543,7 +538,7 @@ infix(INFIX *in, bool first) nrm.curpol = in->curpol; nrm.op = in->op; nrm.buflen = 16; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); /* get right operand */ infix(&nrm, false); @@ -582,7 +577,7 @@ ltxtq_out(PG_FUNCTION_ARGS) nrm.curpol = GETQUERY(query); nrm.buflen = 32; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); *(nrm.cur) = '\0'; nrm.op = GETOPERAND(query); infix(&nrm, true); @@ -615,7 +610,7 @@ ltxtq_send(PG_FUNCTION_ARGS) nrm.curpol = GETQUERY(query); nrm.buflen = 32; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); *(nrm.cur) = '\0'; nrm.op = GETOPERAND(query); infix(&nrm, true); diff --git a/contrib/ltree/ltxtquery_op.c b/contrib/ltree/ltxtquery_op.c index 002102c9c75b6..0e6612ff77a42 100644 --- a/contrib/ltree/ltxtquery_op.c +++ b/contrib/ltree/ltxtquery_op.c @@ -58,19 +58,18 @@ checkcondition_str(void *checkval, ITEM *val) ltree_level *level = LTREE_FIRST(((CHKVAL *) checkval)->node); int tlen = ((CHKVAL *) checkval)->node->numlevel; char *op = ((CHKVAL *) checkval)->operand + val->distance; - int (*cmpptr) (const char *, const char *, size_t); + bool prefix = (val->flag & LVAR_ANYEND); + bool ci = (val->flag & LVAR_INCASE); - cmpptr = (val->flag & LVAR_INCASE) ? ltree_strncasecmp : strncmp; while (tlen > 0) { if (val->flag & LVAR_SUBLEXEME) { - if (compare_subnode(level, op, val->length, cmpptr, (val->flag & LVAR_ANYEND))) + if (compare_subnode(level, op, val->length, prefix, ci)) return true; } - else if ((val->length == level->len || - (level->len > val->length && (val->flag & LVAR_ANYEND))) && - (*cmpptr) (op, level->name, val->length) == 0) + else if (ltree_label_match(op, val->length, level->name, level->len, + prefix, ci)) return true; tlen--; diff --git a/contrib/ltree/meson.build b/contrib/ltree/meson.build index f9b063028391e..f78521bfe5594 100644 --- a/contrib/ltree/meson.build +++ b/contrib/ltree/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ltree_sources = files( '_ltree_gist.c', diff --git a/contrib/ltree/sql/ltree.sql b/contrib/ltree/sql/ltree.sql index dd705d9d7ca00..77e6958c62a7b 100644 --- a/contrib/ltree/sql/ltree.sql +++ b/contrib/ltree/sql/ltree.sql @@ -34,6 +34,7 @@ SELECT subpath('Top.Child1.Child2',0,0); SELECT subpath('Top.Child1.Child2',1,0); SELECT subpath('Top.Child1.Child2',0); SELECT subpath('Top.Child1.Child2',1); +SELECT subpath('Top.Child1.Child2',-4); -- error SELECT index('1.2.3.4.5.6','1.2'); diff --git a/contrib/ltree_plpython/ltree_plpython.c b/contrib/ltree_plpython/ltree_plpython.c index 0493aeb2423cc..d4e7b613fa1e2 100644 --- a/contrib/ltree_plpython/ltree_plpython.c +++ b/contrib/ltree_plpython/ltree_plpython.c @@ -13,6 +13,9 @@ PG_MODULE_MAGIC_EXT( typedef PyObject *(*PLyUnicode_FromStringAndSize_t) (const char *s, Py_ssize_t size); static PLyUnicode_FromStringAndSize_t PLyUnicode_FromStringAndSize_p; +/* Static asserts verify that typedefs above match original declarations */ +StaticAssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); + /* * Module initialize function: fetch function pointers for cross-module calls. @@ -20,8 +23,6 @@ static PLyUnicode_FromStringAndSize_t PLyUnicode_FromStringAndSize_p; void _PG_init(void) { - /* Asserts verify that typedefs above match original declarations */ - AssertVariableIsOfType(&PLyUnicode_FromStringAndSize, PLyUnicode_FromStringAndSize_t); PLyUnicode_FromStringAndSize_p = (PLyUnicode_FromStringAndSize_t) load_external_function("$libdir/" PLPYTHON_LIBNAME, "PLyUnicode_FromStringAndSize", true, NULL); diff --git a/contrib/ltree_plpython/meson.build b/contrib/ltree_plpython/meson.build index a37732c486b9d..b72ee5a780a37 100644 --- a/contrib/ltree_plpython/meson.build +++ b/contrib/ltree_plpython/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not python3_dep.found() subdir_done() diff --git a/contrib/meson.build b/contrib/meson.build index ed30ee7d639f6..ebb7f83d8c5ef 100644 --- a/contrib/meson.build +++ b/contrib/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group contrib_mod_args = pg_mod_args @@ -48,8 +48,10 @@ subdir('pgcrypto') subdir('pg_freespacemap') subdir('pg_logicalinspect') subdir('pg_overexplain') +subdir('pg_plan_advice') subdir('pg_prewarm') subdir('pgrowlocks') +subdir('pg_stash_advice') subdir('pg_stat_statements') subdir('pgstattuple') subdir('pg_surgery') diff --git a/contrib/oid2name/meson.build b/contrib/oid2name/meson.build index 074f16acd72e9..82b9ba48989e4 100644 --- a/contrib/oid2name/meson.build +++ b/contrib/oid2name/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group oid2name_sources = files( 'oid2name.c', diff --git a/contrib/oid2name/oid2name.c b/contrib/oid2name/oid2name.c index 51802907138ed..1e9efcd395326 100644 --- a/contrib/oid2name/oid2name.c +++ b/contrib/oid2name/oid2name.c @@ -237,13 +237,13 @@ add_one_elt(char *eltname, eary *eary) if (eary->alloc == 0) { eary ->alloc = 8; - eary ->array = (char **) pg_malloc(8 * sizeof(char *)); + eary ->array = pg_malloc_array(char *, 8); } else if (eary->num >= eary->alloc) { eary ->alloc *= 2; - eary ->array = (char **) pg_realloc(eary->array, - eary->alloc * sizeof(char *)); + eary ->array = pg_realloc_array(eary->array, char *, + eary->alloc); } eary ->array[eary->num] = pg_strdup(eltname); @@ -400,7 +400,7 @@ sql_exec(PGconn *conn, const char *todo, bool quiet) nfields = PQnfields(res); /* for each field, get the needed width */ - length = (int *) pg_malloc(sizeof(int) * nfields); + length = pg_malloc_array(int, nfields); for (j = 0; j < nfields; j++) length[j] = strlen(PQfname(res, j)); @@ -469,7 +469,7 @@ void sql_exec_dumpalltables(PGconn *conn, struct options *opts) { char todo[1024]; - char *addfields = ",c.oid AS \"Oid\", nspname AS \"Schema\", spcname as \"Tablespace\" "; + char *addfields = ",c.oid AS \"Oid\", nspname AS \"Schema\", spcname as \"Tablespace\", pg_relation_filepath(c.oid) as \"Path\" "; snprintf(todo, sizeof(todo), "SELECT pg_catalog.pg_relation_filenode(c.oid) as \"Filenode\", relname as \"Table Name\" %s " @@ -507,7 +507,7 @@ sql_exec_searchtables(PGconn *conn, struct options *opts) *comma_filenumbers, *comma_tables; bool written = false; - char *addfields = ",c.oid AS \"Oid\", nspname AS \"Schema\", spcname as \"Tablespace\" "; + char *addfields = ",c.oid AS \"Oid\", nspname AS \"Schema\", spcname as \"Tablespace\", pg_relation_filepath(c.oid) as \"Path\" "; /* get tables qualifiers, whether names, filenumbers, or OIDs */ comma_oids = get_comma_elts(opts->oids); @@ -585,11 +585,11 @@ main(int argc, char **argv) struct options *my_opts; PGconn *pgconn; - my_opts = (struct options *) pg_malloc(sizeof(struct options)); + my_opts = pg_malloc_object(struct options); - my_opts->oids = (eary *) pg_malloc(sizeof(eary)); - my_opts->tables = (eary *) pg_malloc(sizeof(eary)); - my_opts->filenumbers = (eary *) pg_malloc(sizeof(eary)); + my_opts->oids = pg_malloc_object(eary); + my_opts->tables = pg_malloc_object(eary); + my_opts->filenumbers = pg_malloc_object(eary); my_opts->oids->num = my_opts->oids->alloc = 0; my_opts->tables->num = my_opts->tables->alloc = 0; diff --git a/contrib/oid2name/t/001_basic.pl b/contrib/oid2name/t/001_basic.pl index a0ede3ebf6392..f847c2e69c14e 100644 --- a/contrib/oid2name/t/001_basic.pl +++ b/contrib/oid2name/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/pageinspect/Makefile b/contrib/pageinspect/Makefile index 9dee765331069..eae989569d013 100644 --- a/contrib/pageinspect/Makefile +++ b/contrib/pageinspect/Makefile @@ -23,7 +23,16 @@ DATA = pageinspect--1.12--1.13.sql \ pageinspect--1.0--1.1.sql PGFILEDESC = "pageinspect - functions to inspect contents of database pages" -REGRESS = page btree brin gin gist hash checksum oldextversions +# "page" is first because it creates the extension. +REGRESS = \ + page \ + brin \ + btree \ + checksum \ + gin \ + gist \ + hash \ + oldextversions ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/pageinspect/brinfuncs.c b/contrib/pageinspect/brinfuncs.c index 990c965aa9241..309b9522f9022 100644 --- a/contrib/pageinspect/brinfuncs.c +++ b/contrib/pageinspect/brinfuncs.c @@ -2,7 +2,7 @@ * brinfuncs.c * Functions to investigate BRIN indexes * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/brinfuncs.c @@ -22,6 +22,7 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/tuplestore.h" PG_FUNCTION_INFO_V1(brin_page_type); PG_FUNCTION_INFO_V1(brin_page_items); @@ -186,7 +187,7 @@ brin_page_items(PG_FUNCTION_ARGS) * Initialize output functions for all indexed datatypes; simplifies * calling them later. */ - columns = palloc(sizeof(brin_column_state *) * RelationGetDescr(indexRel)->natts); + columns = palloc_array(brin_column_state *, RelationGetDescr(indexRel)->natts); for (attno = 1; attno <= bdesc->bd_tupdesc->natts; attno++) { Oid output; diff --git a/contrib/pageinspect/btreefuncs.c b/contrib/pageinspect/btreefuncs.c index 294821231fc3b..0585b7cee4028 100644 --- a/contrib/pageinspect/btreefuncs.c +++ b/contrib/pageinspect/btreefuncs.c @@ -27,6 +27,7 @@ #include "postgres.h" +#include "access/htup_details.h" #include "access/nbtree.h" #include "access/relation.h" #include "catalog/namespace.h" @@ -378,7 +379,7 @@ bt_multi_page_stats(PG_FUNCTION_ARGS) /* Save arguments for reuse */ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx); - uargs = palloc(sizeof(ua_page_stats)); + uargs = palloc_object(ua_page_stats); uargs->relid = RelationGetRelid(rel); uargs->blkno = blkno; @@ -506,7 +507,7 @@ bt_page_print_tuples(ua_page_items *uargs) j = 0; memset(nulls, 0, sizeof(nulls)); - values[j++] = DatumGetInt16(offset); + values[j++] = UInt16GetDatum(offset); values[j++] = ItemPointerGetDatum(&itup->t_tid); values[j++] = Int32GetDatum((int) IndexTupleSize(itup)); values[j++] = BoolGetDatum(IndexTupleHasNulls(itup)); @@ -659,7 +660,7 @@ bt_page_items_internal(PG_FUNCTION_ARGS, enum pageinspect_version ext_version) */ mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx); - uargs = palloc(sizeof(ua_page_items)); + uargs = palloc_object(ua_page_items); uargs->page = palloc(BLCKSZ); memcpy(uargs->page, BufferGetPage(buffer), BLCKSZ); @@ -751,7 +752,7 @@ bt_page_items_bytea(PG_FUNCTION_ARGS) fctx = SRF_FIRSTCALL_INIT(); mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx); - uargs = palloc(sizeof(ua_page_items)); + uargs = palloc_object(ua_page_items); uargs->page = get_page_from_raw(raw_page); @@ -900,10 +901,10 @@ bt_metap(PG_FUNCTION_ARGS) j = 0; values[j++] = psprintf("%d", metad->btm_magic); values[j++] = psprintf("%d", metad->btm_version); - values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_root); - values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_level); - values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_fastroot); - values[j++] = psprintf(INT64_FORMAT, (int64) metad->btm_fastlevel); + values[j++] = psprintf("%u", metad->btm_root); + values[j++] = psprintf("%u", metad->btm_level); + values[j++] = psprintf("%u", metad->btm_fastroot); + values[j++] = psprintf("%u", metad->btm_fastlevel); /* * Get values of extended metadata if available, use default values @@ -913,8 +914,7 @@ bt_metap(PG_FUNCTION_ARGS) */ if (metad->btm_version >= BTREE_NOVAC_VERSION) { - values[j++] = psprintf(INT64_FORMAT, - (int64) metad->btm_last_cleanup_num_delpages); + values[j++] = psprintf("%u", metad->btm_last_cleanup_num_delpages); values[j++] = psprintf("%f", metad->btm_last_cleanup_num_heap_tuples); values[j++] = metad->btm_allequalimage ? "t" : "f"; } diff --git a/contrib/pageinspect/expected/gist.out b/contrib/pageinspect/expected/gist.out index 2b1d54a627949..8502f9efb4190 100644 --- a/contrib/pageinspect/expected/gist.out +++ b/contrib/pageinspect/expected/gist.out @@ -5,21 +5,21 @@ CREATE UNLOGGED TABLE test_gist AS SELECT point(i,i) p, i::text t FROM CREATE INDEX test_gist_idx ON test_gist USING gist (p); -- Page 0 is the root, the rest are leaf pages SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 0)); - lsn | nsn | rightlink | flags ------+-----+------------+------- - 0/1 | 0/0 | 4294967295 | {} + lsn | nsn | rightlink | flags +------------+------------+------------+------- + 0/00000001 | 0/00000000 | 4294967295 | {} (1 row) SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 1)); - lsn | nsn | rightlink | flags ------+-----+------------+-------- - 0/1 | 0/0 | 4294967295 | {leaf} + lsn | nsn | rightlink | flags +------------+------------+------------+-------- + 0/00000001 | 0/00000000 | 4294967295 | {leaf} (1 row) SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 2)); - lsn | nsn | rightlink | flags ------+-----+-----------+-------- - 0/1 | 0/0 | 1 | {leaf} + lsn | nsn | rightlink | flags +------------+------------+-----------+-------- + 0/00000001 | 0/00000000 | 1 | {leaf} (1 row) SELECT * FROM gist_page_items(get_raw_page('test_gist_idx', 0), 'test_gist_idx'); diff --git a/contrib/pageinspect/expected/page.out b/contrib/pageinspect/expected/page.out index e42fd9747fd1c..fcf19c5ca5a50 100644 --- a/contrib/pageinspect/expected/page.out +++ b/contrib/pageinspect/expected/page.out @@ -265,9 +265,9 @@ SELECT fsm_page_contents(decode(repeat('00', :block_size), 'hex')); (1 row) SELECT page_header(decode(repeat('00', :block_size), 'hex')); - page_header ------------------------ - (0/0,0,0,0,0,0,0,0,0) + page_header +------------------------------ + (0/00000000,0,0,0,0,0,0,0,0) (1 row) SELECT page_checksum(decode(repeat('00', :block_size), 'hex'), 1); diff --git a/contrib/pageinspect/fsmfuncs.c b/contrib/pageinspect/fsmfuncs.c index 0ccc3ddfbb8f9..001082acd8d16 100644 --- a/contrib/pageinspect/fsmfuncs.c +++ b/contrib/pageinspect/fsmfuncs.c @@ -9,7 +9,7 @@ * there's hardly any use case for using these without superuser-rights * anyway. * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/fsmfuncs.c diff --git a/contrib/pageinspect/ginfuncs.c b/contrib/pageinspect/ginfuncs.c index 09a90957081f1..b6574083b0a14 100644 --- a/contrib/pageinspect/ginfuncs.c +++ b/contrib/pageinspect/ginfuncs.c @@ -2,7 +2,7 @@ * ginfuncs.c * Functions to investigate the content of GIN indexes * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/ginfuncs.c @@ -73,7 +73,7 @@ gin_metapage_info(PG_FUNCTION_ARGS) values[0] = Int64GetDatum(metadata->head); values[1] = Int64GetDatum(metadata->tail); - values[2] = Int32GetDatum(metadata->tailFreeSize); + values[2] = UInt32GetDatum(metadata->tailFreeSize); values[3] = Int64GetDatum(metadata->nPendingPages); values[4] = Int64GetDatum(metadata->nPendingHeapTuples); @@ -222,7 +222,7 @@ gin_leafpage_items(PG_FUNCTION_ARGS) opaq->flags, (GIN_DATA | GIN_LEAF | GIN_COMPRESSED)))); - inter_call_data = palloc(sizeof(gin_leafpage_items_state)); + inter_call_data = palloc_object(gin_leafpage_items_state); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) diff --git a/contrib/pageinspect/gistfuncs.c b/contrib/pageinspect/gistfuncs.c index 7b16e2a1ef33c..89678d377c7bf 100644 --- a/contrib/pageinspect/gistfuncs.c +++ b/contrib/pageinspect/gistfuncs.c @@ -2,15 +2,17 @@ * gistfuncs.c * Functions to investigate the content of GiST indexes * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/gistfuncs.c */ #include "postgres.h" +#include "access/genam.h" #include "access/gist.h" #include "access/htup.h" +#include "access/htup_details.h" #include "access/relation.h" #include "catalog/pg_am_d.h" #include "funcapi.h" @@ -23,6 +25,7 @@ #include "utils/pg_lsn.h" #include "utils/rel.h" #include "utils/ruleutils.h" +#include "utils/tuplestore.h" PG_FUNCTION_INFO_V1(gist_page_opaque_info); PG_FUNCTION_INFO_V1(gist_page_items); @@ -174,7 +177,7 @@ gist_page_items_bytea(PG_FUNCTION_ARGS) memset(nulls, 0, sizeof(nulls)); - values[0] = DatumGetInt16(offset); + values[0] = UInt16GetDatum(offset); values[1] = ItemPointerGetDatum(&itup->t_tid); values[2] = Int32GetDatum((int) IndexTupleSize(itup)); @@ -200,7 +203,7 @@ gist_page_items(PG_FUNCTION_ARGS) TupleDesc tupdesc; Page page; uint16 flagbits; - bits16 printflags = 0; + uint16 printflags = 0; OffsetNumber offset; OffsetNumber maxoff = InvalidOffsetNumber; char *index_columns; @@ -281,7 +284,7 @@ gist_page_items(PG_FUNCTION_ARGS) memset(nulls, 0, sizeof(nulls)); - values[0] = DatumGetInt16(offset); + values[0] = UInt16GetDatum(offset); values[1] = ItemPointerGetDatum(&itup->t_tid); values[2] = Int32GetDatum((int) IndexTupleSize(itup)); values[3] = BoolGetDatum(ItemIdIsDead(id)); @@ -360,7 +363,7 @@ gist_page_items(PG_FUNCTION_ARGS) tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } - relation_close(indexRel, AccessShareLock); + index_close(indexRel, AccessShareLock); return (Datum) 0; } diff --git a/contrib/pageinspect/hashfuncs.c b/contrib/pageinspect/hashfuncs.c index ca7f1f6e7410d..7fc97d043ce13 100644 --- a/contrib/pageinspect/hashfuncs.c +++ b/contrib/pageinspect/hashfuncs.c @@ -2,7 +2,7 @@ * hashfuncs.c * Functions to investigate the content of HASH indexes * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/hashfuncs.c @@ -325,7 +325,7 @@ hash_page_items(PG_FUNCTION_ARGS) page = verify_hash_page(raw_page, LH_BUCKET_PAGE | LH_OVERFLOW_PAGE); - uargs = palloc(sizeof(struct user_args)); + uargs = palloc_object(struct user_args); uargs->page = page; @@ -415,6 +415,10 @@ hash_bitmap_info(PG_FUNCTION_ARGS) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to use raw page functions"))); + /* + * This uses relation_open() and not index_open(). The latter allows + * partitioned indexes, and these are forbidden here. + */ indexRel = relation_open(indexRelid, AccessShareLock); if (!IS_INDEX(indexRel) || !IS_HASH(indexRel)) @@ -486,7 +490,7 @@ hash_bitmap_info(PG_FUNCTION_ARGS) bit = ISSET(freep, bitmapbit) != 0; _hash_relbuf(indexRel, mapbuf); - index_close(indexRel, AccessShareLock); + relation_close(indexRel, AccessShareLock); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupleDesc) != TYPEFUNC_COMPOSITE) diff --git a/contrib/pageinspect/heapfuncs.c b/contrib/pageinspect/heapfuncs.c index 377ae30d1fa71..4f0f3bd53e743 100644 --- a/contrib/pageinspect/heapfuncs.c +++ b/contrib/pageinspect/heapfuncs.c @@ -15,7 +15,7 @@ * there's hardly any use case for using these without superuser-rights * anyway. * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/heapfuncs.c @@ -32,6 +32,7 @@ #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "pageinspect.h" #include "port/pg_bitutils.h" #include "utils/array.h" #include "utils/builtins.h" @@ -46,7 +47,7 @@ static inline Oid HeapTupleHeaderGetOidOld(const HeapTupleHeaderData *tup) { if (tup->t_infomask & HEAP_HASOID_OLD) - return *((Oid *) ((char *) (tup) + (tup)->t_hoff - sizeof(Oid))); + return *((const Oid *) ((const char *) (tup) + (tup)->t_hoff - sizeof(Oid))); else return InvalidOid; } @@ -55,11 +56,11 @@ HeapTupleHeaderGetOidOld(const HeapTupleHeaderData *tup) /* * bits_to_text * - * Converts a bits8-array of 'len' bits to a human-readable + * Converts a uint8-array of 'len' bits to a human-readable * c-string representation. */ static char * -bits_to_text(bits8 *bits, int len) +bits_to_text(uint8 *bits, int len) { int i; char *str; @@ -78,13 +79,13 @@ bits_to_text(bits8 *bits, int len) /* * text_to_bits * - * Converts a c-string representation of bits into a bits8-array. This is + * Converts a c-string representation of bits into a uint8-array. This is * the reverse operation of previous routine. */ -static bits8 * +static uint8 * text_to_bits(char *str, int len) { - bits8 *bits; + uint8 *bits; int off = 0; char byte = 0; @@ -101,7 +102,7 @@ text_to_bits(char *str, int len) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), errmsg("invalid character \"%.*s\" in t_bits string", - pg_mblen(str + off), str + off))); + pg_mblen_cstr(str + off), str + off))); if (off % 8 == 7) bits[off / 8] = byte; @@ -132,29 +133,21 @@ heap_page_items(PG_FUNCTION_ARGS) bytea *raw_page = PG_GETARG_BYTEA_P(0); heap_page_items_state *inter_call_data = NULL; FuncCallContext *fctx; - int raw_page_size; if (!superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to use raw page functions"))); - raw_page_size = VARSIZE(raw_page) - VARHDRSZ; - if (SRF_IS_FIRSTCALL()) { TupleDesc tupdesc; MemoryContext mctx; - if (raw_page_size < SizeOfPageHeaderData) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("input page too small (%d bytes)", raw_page_size))); - fctx = SRF_FIRSTCALL_INIT(); mctx = MemoryContextSwitchTo(fctx->multi_call_memory_ctx); - inter_call_data = palloc(sizeof(heap_page_items_state)); + inter_call_data = palloc_object(heap_page_items_state); /* Build a tuple descriptor for our result type */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) @@ -163,7 +156,7 @@ heap_page_items(PG_FUNCTION_ARGS) inter_call_data->tupd = tupdesc; inter_call_data->offset = FirstOffsetNumber; - inter_call_data->page = VARDATA(raw_page); + inter_call_data->page = get_page_from_raw(raw_page); fctx->max_calls = PageGetMaxOffsetNumber(inter_call_data->page); fctx->user_fctx = inter_call_data; @@ -209,7 +202,7 @@ heap_page_items(PG_FUNCTION_ARGS) if (ItemIdHasStorage(id) && lp_len >= MinHeapTupleSize && lp_offset == MAXALIGN(lp_offset) && - lp_offset + lp_len <= raw_page_size) + lp_offset + lp_len <= BLCKSZ) { HeapTupleHeader tuphdr; @@ -256,7 +249,7 @@ heap_page_items(PG_FUNCTION_ARGS) nulls[11] = true; if (tuphdr->t_infomask & HEAP_HASOID_OLD) - values[12] = HeapTupleHeaderGetOidOld(tuphdr); + values[12] = ObjectIdGetDatum(HeapTupleHeaderGetOidOld(tuphdr)); else nulls[12] = true; @@ -312,7 +305,7 @@ heap_page_items(PG_FUNCTION_ARGS) static Datum tuple_data_split_internal(Oid relid, char *tupdata, uint16 tupdata_len, uint16 t_infomask, - uint16 t_infomask2, bits8 *t_bits, + uint16 t_infomask2, uint8 *t_bits, bool do_detoast) { ArrayBuildState *raw_attrs; @@ -396,7 +389,7 @@ tuple_data_split_internal(Oid relid, char *tupdata, errmsg("unexpected end of tuple data"))); if (attr->attlen == -1 && do_detoast) - attr_data = pg_detoast_datum_copy((struct varlena *) (tupdata + off)); + attr_data = pg_detoast_datum_copy((varlena *) (tupdata + off)); else { attr_data = (bytea *) palloc(len + VARHDRSZ); @@ -441,7 +434,7 @@ tuple_data_split(PG_FUNCTION_ARGS) uint16 t_infomask2; char *t_bits_str; bool do_detoast = false; - bits8 *t_bits = NULL; + uint8 *t_bits = NULL; Datum res; relid = PG_GETARG_OID(0); @@ -463,7 +456,7 @@ tuple_data_split(PG_FUNCTION_ARGS) PG_RETURN_NULL(); /* - * Convert t_bits string back to the bits8 array as represented in the + * Convert t_bits string back to the uint8 array as represented in the * tuple header. */ if (t_infomask & HEAP_HASNULL) @@ -553,7 +546,7 @@ heap_tuple_infomask_flags(PG_FUNCTION_ARGS) } /* build set of raw flags */ - flags = (Datum *) palloc0(sizeof(Datum) * bitcnt); + flags = palloc0_array(Datum, bitcnt); /* decode t_infomask */ if ((t_infomask & HEAP_HASNULL) != 0) diff --git a/contrib/pageinspect/meson.build b/contrib/pageinspect/meson.build index 3e2132f1b2d23..c43ea400a4d7b 100644 --- a/contrib/pageinspect/meson.build +++ b/contrib/pageinspect/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pageinspect_sources = files( 'brinfuncs.c', diff --git a/contrib/pageinspect/pageinspect.h b/contrib/pageinspect/pageinspect.h index 1d0ec67f401f6..b241fdc97b28a 100644 --- a/contrib/pageinspect/pageinspect.h +++ b/contrib/pageinspect/pageinspect.h @@ -3,7 +3,7 @@ * pageinspect.h * Common functions for pageinspect. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/pageinspect.h diff --git a/contrib/pageinspect/rawpage.c b/contrib/pageinspect/rawpage.c index 0d57123aa2669..f3996dc39fcde 100644 --- a/contrib/pageinspect/rawpage.c +++ b/contrib/pageinspect/rawpage.c @@ -5,7 +5,7 @@ * * Access-method specific inspection functions are in separate files. * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pageinspect/rawpage.c @@ -193,8 +193,7 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno) memcpy(raw_page_data, BufferGetPage(buf), BLCKSZ); - LockBuffer(buf, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buf); + UnlockReleaseBuffer(buf); relation_close(rel, AccessShareLock); @@ -208,11 +207,9 @@ get_raw_page_internal(text *relname, ForkNumber forknum, BlockNumber blkno) * Get a palloc'd, maxalign'ed page image from the result of get_raw_page() * * On machines with MAXALIGN = 8, the payload of a bytea is not maxaligned, - * since it will start 4 bytes into a palloc'd value. On alignment-picky - * machines, this will cause failures in accesses to 8-byte-wide values - * within the page. We don't need to worry if accessing only 4-byte or - * smaller fields, but when examining a struct that contains 8-byte fields, - * use this function for safety. + * since it will start 4 bytes into a palloc'd value. PageHeaderData requires + * 8 byte alignment, so always use this function when accessing page header + * fields from a raw page bytea. */ Page get_page_from_raw(bytea *raw_page) @@ -282,7 +279,7 @@ page_header(PG_FUNCTION_ARGS) { char lsnchar[64]; - snprintf(lsnchar, sizeof(lsnchar), "%X/%X", LSN_FORMAT_ARGS(lsn)); + snprintf(lsnchar, sizeof(lsnchar), "%X/%08X", LSN_FORMAT_ARGS(lsn)); values[0] = CStringGetTextDatum(lsnchar); } else diff --git a/contrib/passwordcheck/meson.build b/contrib/passwordcheck/meson.build index 297a24f3e3b30..02be1e1963b7b 100644 --- a/contrib/passwordcheck/meson.build +++ b/contrib/passwordcheck/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group passwordcheck_sources = files( 'passwordcheck.c', diff --git a/contrib/passwordcheck/passwordcheck.c b/contrib/passwordcheck/passwordcheck.c index 39ded17afa413..13fd5c976a014 100644 --- a/contrib/passwordcheck/passwordcheck.c +++ b/contrib/passwordcheck/passwordcheck.c @@ -3,7 +3,7 @@ * passwordcheck.c * * - * Copyright (c) 2009-2025, PostgreSQL Global Development Group + * Copyright (c) 2009-2026, PostgreSQL Global Development Group * * Author: Laurenz Albe * diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile index 5f748543e2ea2..0e618f66aec6e 100644 --- a/contrib/pg_buffercache/Makefile +++ b/contrib/pg_buffercache/Makefile @@ -9,7 +9,7 @@ EXTENSION = pg_buffercache DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \ pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \ pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \ - pg_buffercache--1.5--1.6.sql + pg_buffercache--1.5--1.6.sql pg_buffercache--1.6--1.7.sql PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time" REGRESS = pg_buffercache pg_buffercache_numa diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out index 9a9216dc7b1bf..886dea770f626 100644 --- a/contrib/pg_buffercache/expected/pg_buffercache.out +++ b/contrib/pg_buffercache/expected/pg_buffercache.out @@ -8,6 +8,16 @@ from pg_buffercache; t (1 row) +-- For pg_buffercache_os_pages, we expect at least one entry for each buffer +select count(*) >= (select setting::bigint + from pg_settings + where name = 'shared_buffers') +from pg_buffercache_os_pages; + ?column? +---------- + t +(1 row) + select buffers_used + buffers_unused > 0, buffers_dirty <= buffers_used, buffers_pinned <= buffers_used @@ -28,6 +38,8 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts() WHERE buffers >= 0; SET ROLE pg_database_owner; SELECT * FROM pg_buffercache; ERROR: permission denied for view pg_buffercache +SELECT * FROM pg_buffercache_os_pages; +ERROR: permission denied for view pg_buffercache_os_pages SELECT * FROM pg_buffercache_pages() AS p (wrong int); ERROR: permission denied for function pg_buffercache_pages SELECT * FROM pg_buffercache_summary(); @@ -43,6 +55,12 @@ SELECT count(*) > 0 FROM pg_buffercache; t (1 row) +SELECT count(*) > 0 FROM pg_buffercache_os_pages; + ?column? +---------- + t +(1 row) + SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary(); ?column? ---------- @@ -57,7 +75,7 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts(); RESET role; ------ ----- Test pg_buffercache_evict* functions +---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions ------ CREATE ROLE regress_buffercache_normal; SET ROLE regress_buffercache_normal; @@ -68,6 +86,12 @@ SELECT * FROM pg_buffercache_evict_relation(1); ERROR: must be superuser to use pg_buffercache_evict_relation() SELECT * FROM pg_buffercache_evict_all(); ERROR: must be superuser to use pg_buffercache_evict_all() +SELECT * FROM pg_buffercache_mark_dirty(1); +ERROR: must be superuser to use pg_buffercache_mark_dirty() +SELECT * FROM pg_buffercache_mark_dirty_relation(1); +ERROR: must be superuser to use pg_buffercache_mark_dirty_relation() +SELECT * FROM pg_buffercache_mark_dirty_all(); +ERROR: must be superuser to use pg_buffercache_mark_dirty_all() RESET ROLE; -- These should return nothing, because these are STRICT functions SELECT * FROM pg_buffercache_evict(NULL); @@ -82,6 +106,18 @@ SELECT * FROM pg_buffercache_evict_relation(NULL); | | (1 row) +SELECT * FROM pg_buffercache_mark_dirty(NULL); + buffer_dirtied | buffer_already_dirty +----------------+---------------------- + | +(1 row) + +SELECT * FROM pg_buffercache_mark_dirty_relation(NULL); + buffers_dirtied | buffers_already_dirty | buffers_skipped +-----------------+-----------------------+----------------- + | | +(1 row) + -- These should fail because they are not called by valid range of buffers -- Number of the shared buffers are limited by max integer SELECT 2147483647 max_buffers \gset @@ -91,11 +127,18 @@ SELECT * FROM pg_buffercache_evict(0); ERROR: bad buffer ID: 0 SELECT * FROM pg_buffercache_evict(:max_buffers); ERROR: bad buffer ID: 2147483647 --- This should fail because pg_buffercache_evict_relation() doesn't accept --- local relations +SELECT * FROM pg_buffercache_mark_dirty(-1); +ERROR: bad buffer ID: -1 +SELECT * FROM pg_buffercache_mark_dirty(0); +ERROR: bad buffer ID: 0 +SELECT * FROM pg_buffercache_mark_dirty(:max_buffers); +ERROR: bad buffer ID: 2147483647 +-- These should fail because they don't accept local relations CREATE TEMP TABLE temp_pg_buffercache(); SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache'); ERROR: relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only +SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache'); +ERROR: relation uses local buffers, pg_buffercache_mark_dirty_relation() is intended to be used for shared buffers only DROP TABLE temp_pg_buffercache; -- These shouldn't fail SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1); @@ -117,5 +160,23 @@ SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg t (1 row) +SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_relation('shared_pg_buffercache'); + ?column? +---------- + t +(1 row) + DROP TABLE shared_pg_buffercache; +SELECT pg_buffercache_mark_dirty(1) IS NOT NULL; + ?column? +---------- + t +(1 row) + +SELECT pg_buffercache_mark_dirty_all() IS NOT NULL; + ?column? +---------- + t +(1 row) + DROP ROLE regress_buffercache_normal; diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build index 7cd039a1df9cb..e681205abb2d8 100644 --- a/contrib/pg_buffercache/meson.build +++ b/contrib/pg_buffercache/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_buffercache_sources = files( 'pg_buffercache_pages.c', @@ -24,6 +24,7 @@ install_data( 'pg_buffercache--1.3--1.4.sql', 'pg_buffercache--1.4--1.5.sql', 'pg_buffercache--1.5--1.6.sql', + 'pg_buffercache--1.6--1.7.sql', 'pg_buffercache.control', kwargs: contrib_data_args, ) diff --git a/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql new file mode 100644 index 0000000000000..9a7bf66dab54b --- /dev/null +++ b/contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql @@ -0,0 +1,56 @@ +/* contrib/pg_buffercache/pg_buffercache--1.6--1.7.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.7'" to load this file. \quit + +-- Function to retrieve information about OS pages, with optional NUMA +-- information. +CREATE FUNCTION pg_buffercache_os_pages(IN include_numa boolean, + OUT bufferid integer, + OUT os_page_num bigint, + OUT numa_node integer) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_buffercache_os_pages' +LANGUAGE C PARALLEL SAFE; + +-- View for OS page information, without NUMA. +CREATE VIEW pg_buffercache_os_pages AS + SELECT bufferid, os_page_num + FROM pg_buffercache_os_pages(false); + +-- Re-create view for OS page information, with NUMA. +DROP VIEW pg_buffercache_numa; +CREATE VIEW pg_buffercache_numa AS + SELECT bufferid, os_page_num, numa_node + FROM pg_buffercache_os_pages(true); + +REVOKE ALL ON FUNCTION pg_buffercache_os_pages(boolean) FROM PUBLIC; +REVOKE ALL ON pg_buffercache_os_pages FROM PUBLIC; +REVOKE ALL ON pg_buffercache_numa FROM PUBLIC; + +GRANT EXECUTE ON FUNCTION pg_buffercache_os_pages(boolean) TO pg_monitor; +GRANT SELECT ON pg_buffercache_os_pages TO pg_monitor; +GRANT SELECT ON pg_buffercache_numa TO pg_monitor; + +-- Functions to mark buffers as dirty. +CREATE FUNCTION pg_buffercache_mark_dirty( + IN int, + OUT buffer_dirtied boolean, + OUT buffer_already_dirty boolean) +AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty' +LANGUAGE C PARALLEL SAFE VOLATILE STRICT; + +CREATE FUNCTION pg_buffercache_mark_dirty_relation( + IN regclass, + OUT buffers_dirtied int4, + OUT buffers_already_dirty int4, + OUT buffers_skipped int4) +AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_relation' +LANGUAGE C PARALLEL SAFE VOLATILE STRICT; + +CREATE FUNCTION pg_buffercache_mark_dirty_all( + OUT buffers_dirtied int4, + OUT buffers_already_dirty int4, + OUT buffers_skipped int4) +AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all' +LANGUAGE C PARALLEL SAFE VOLATILE; diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control index b030ba3a6faba..11499550945ee 100644 --- a/contrib/pg_buffercache/pg_buffercache.control +++ b/contrib/pg_buffercache/pg_buffercache.control @@ -1,5 +1,5 @@ # pg_buffercache extension comment = 'examine the shared buffer cache' -default_version = '1.6' +default_version = '1.7' module_pathname = '$libdir/pg_buffercache' relocatable = true diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index 4b007f6e1b06a..1ec2cf0e6f468 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -16,6 +16,7 @@ #include "storage/buf_internals.h" #include "storage/bufmgr.h" #include "utils/rel.h" +#include "utils/tuplestore.h" #define NUM_BUFFERCACHE_PAGES_MIN_ELEM 8 @@ -25,8 +26,11 @@ #define NUM_BUFFERCACHE_EVICT_ELEM 2 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3 +#define NUM_BUFFERCACHE_MARK_DIRTY_ELEM 2 +#define NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM 3 +#define NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM 3 -#define NUM_BUFFERCACHE_NUMA_ELEM 3 +#define NUM_BUFFERCACHE_OS_PAGES_ELEM 3 PG_MODULE_MAGIC_EXT( .name = "pg_buffercache", @@ -34,47 +38,16 @@ PG_MODULE_MAGIC_EXT( ); /* - * Record structure holding the to be exposed cache data. - */ -typedef struct -{ - uint32 bufferid; - RelFileNumber relfilenumber; - Oid reltablespace; - Oid reldatabase; - ForkNumber forknum; - BlockNumber blocknum; - bool isvalid; - bool isdirty; - uint16 usagecount; - - /* - * An int32 is sufficiently large, as MAX_BACKENDS prevents a buffer from - * being pinned by too many backends and each backend will only pin once - * because of bufmgr.c's PrivateRefCount infrastructure. - */ - int32 pinning_backends; -} BufferCachePagesRec; - - -/* - * Function context for data persisting over repeated calls. - */ -typedef struct -{ - TupleDesc tupdesc; - BufferCachePagesRec *record; -} BufferCachePagesContext; - -/* - * Record structure holding the to be exposed cache data. + * Record structure holding the to be exposed cache data for OS pages. This + * structure is used by pg_buffercache_os_pages(), where NUMA information may + * or may not be included. */ typedef struct { uint32 bufferid; int64 page_num; int32 numa_node; -} BufferCacheNumaRec; +} BufferCacheOsPagesRec; /* * Function context for data persisting over repeated calls. @@ -82,11 +55,9 @@ typedef struct typedef struct { TupleDesc tupdesc; - int buffers_per_page; - int pages_per_buffer; - int os_page_size; - BufferCacheNumaRec *record; -} BufferCacheNumaContext; + bool include_numa; + BufferCacheOsPagesRec *record; +} BufferCacheOsPagesContext; /* @@ -94,12 +65,16 @@ typedef struct * relation node/tablespace/database/blocknum and dirty indicator. */ PG_FUNCTION_INFO_V1(pg_buffercache_pages); +PG_FUNCTION_INFO_V1(pg_buffercache_os_pages); PG_FUNCTION_INFO_V1(pg_buffercache_numa_pages); PG_FUNCTION_INFO_V1(pg_buffercache_summary); PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts); PG_FUNCTION_INFO_V1(pg_buffercache_evict); PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation); PG_FUNCTION_INFO_V1(pg_buffercache_evict_all); +PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty); +PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_relation); +PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all); /* Only need to touch memory once per backend process lifetime */ @@ -109,139 +84,89 @@ static bool firstNumaTouch = true; Datum pg_buffercache_pages(PG_FUNCTION_ARGS) { - FuncCallContext *funcctx; - Datum result; - MemoryContext oldcontext; - BufferCachePagesContext *fctx; /* User function context. */ - TupleDesc tupledesc; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc expected_tupledesc; - HeapTuple tuple; - - if (SRF_IS_FIRSTCALL()) - { - int i; - - funcctx = SRF_FIRSTCALL_INIT(); - - /* Switch context when allocating stuff to be used in later calls */ - oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - - /* Create a user function context for cross-call persistence */ - fctx = (BufferCachePagesContext *) palloc(sizeof(BufferCachePagesContext)); - - /* - * To smoothly support upgrades from version 1.0 of this extension - * transparently handle the (non-)existence of the pinning_backends - * column. We unfortunately have to get the result type for that... - - * we can't use the result type determined by the function definition - * without potentially crashing when somebody uses the old (or even - * wrong) function definition though. - */ - if (get_call_result_type(fcinfo, NULL, &expected_tupledesc) != TYPEFUNC_COMPOSITE) - elog(ERROR, "return type must be a row type"); - - if (expected_tupledesc->natts < NUM_BUFFERCACHE_PAGES_MIN_ELEM || - expected_tupledesc->natts > NUM_BUFFERCACHE_PAGES_ELEM) - elog(ERROR, "incorrect number of output arguments"); - - /* Construct a tuple descriptor for the result rows. */ - tupledesc = CreateTemplateTupleDesc(expected_tupledesc->natts); - TupleDescInitEntry(tupledesc, (AttrNumber) 1, "bufferid", - INT4OID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 2, "relfilenode", - OIDOID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 3, "reltablespace", - OIDOID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 4, "reldatabase", - OIDOID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 5, "relforknumber", - INT2OID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 6, "relblocknumber", - INT8OID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 7, "isdirty", - BOOLOID, -1, 0); - TupleDescInitEntry(tupledesc, (AttrNumber) 8, "usage_count", - INT2OID, -1, 0); - - if (expected_tupledesc->natts == NUM_BUFFERCACHE_PAGES_ELEM) - TupleDescInitEntry(tupledesc, (AttrNumber) 9, "pinning_backends", - INT4OID, -1, 0); - - fctx->tupdesc = BlessTupleDesc(tupledesc); + int i; - /* Allocate NBuffers worth of BufferCachePagesRec records. */ - fctx->record = (BufferCachePagesRec *) - MemoryContextAllocHuge(CurrentMemoryContext, - sizeof(BufferCachePagesRec) * NBuffers); - - /* Set max calls and remember the user function context. */ - funcctx->max_calls = NBuffers; - funcctx->user_fctx = fctx; - - /* Return to original context when allocating transient memory */ - MemoryContextSwitchTo(oldcontext); - - /* - * Scan through all the buffers, saving the relevant fields in the - * fctx->record structure. - * - * We don't hold the partition locks, so we don't get a consistent - * snapshot across all buffers, but we do grab the buffer header - * locks, so the information of each buffer is self-consistent. - */ - for (i = 0; i < NBuffers; i++) - { - BufferDesc *bufHdr; - uint32 buf_state; + /* + * To smoothly support upgrades from version 1.0 of this extension + * transparently handle the (non-)existence of the pinning_backends + * column. We unfortunately have to get the result type for that... - we + * can't use the result type determined by the function definition without + * potentially crashing when somebody uses the old (or even wrong) + * function definition though. + */ + if (get_call_result_type(fcinfo, NULL, &expected_tupledesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); - bufHdr = GetBufferDescriptor(i); - /* Lock each buffer header before inspecting. */ - buf_state = LockBufHdr(bufHdr); + if (expected_tupledesc->natts < NUM_BUFFERCACHE_PAGES_MIN_ELEM || + expected_tupledesc->natts > NUM_BUFFERCACHE_PAGES_ELEM) + elog(ERROR, "incorrect number of output arguments"); - fctx->record[i].bufferid = BufferDescriptorGetBuffer(bufHdr); - fctx->record[i].relfilenumber = BufTagGetRelNumber(&bufHdr->tag); - fctx->record[i].reltablespace = bufHdr->tag.spcOid; - fctx->record[i].reldatabase = bufHdr->tag.dbOid; - fctx->record[i].forknum = BufTagGetForkNum(&bufHdr->tag); - fctx->record[i].blocknum = bufHdr->tag.blockNum; - fctx->record[i].usagecount = BUF_STATE_GET_USAGECOUNT(buf_state); - fctx->record[i].pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state); + InitMaterializedSRF(fcinfo, 0); - if (buf_state & BM_DIRTY) - fctx->record[i].isdirty = true; - else - fctx->record[i].isdirty = false; + /* + * Scan through all the buffers, adding one row for each of the buffers to + * the tuplestore. + * + * We don't hold the partition locks, so we don't get a consistent + * snapshot across all buffers, but we do grab the buffer header locks, so + * the information of each buffer is self-consistent. + */ + for (i = 0; i < NBuffers; i++) + { + BufferDesc *bufHdr; + uint64 buf_state; + uint32 bufferid; + RelFileNumber relfilenumber; + Oid reltablespace; + Oid reldatabase; + ForkNumber forknum; + BlockNumber blocknum; + bool isvalid; + bool isdirty; + uint16 usagecount; + int32 pinning_backends; + Datum values[NUM_BUFFERCACHE_PAGES_ELEM]; + bool nulls[NUM_BUFFERCACHE_PAGES_ELEM]; - /* Note if the buffer is valid, and has storage created */ - if ((buf_state & BM_VALID) && (buf_state & BM_TAG_VALID)) - fctx->record[i].isvalid = true; - else - fctx->record[i].isvalid = false; + CHECK_FOR_INTERRUPTS(); - UnlockBufHdr(bufHdr, buf_state); - } - } + bufHdr = GetBufferDescriptor(i); + /* Lock each buffer header before inspecting. */ + buf_state = LockBufHdr(bufHdr); + + bufferid = BufferDescriptorGetBuffer(bufHdr); + relfilenumber = BufTagGetRelNumber(&bufHdr->tag); + reltablespace = bufHdr->tag.spcOid; + reldatabase = bufHdr->tag.dbOid; + forknum = BufTagGetForkNum(&bufHdr->tag); + blocknum = bufHdr->tag.blockNum; + usagecount = BUF_STATE_GET_USAGECOUNT(buf_state); + pinning_backends = BUF_STATE_GET_REFCOUNT(buf_state); - funcctx = SRF_PERCALL_SETUP(); + if (buf_state & BM_DIRTY) + isdirty = true; + else + isdirty = false; - /* Get the saved state */ - fctx = funcctx->user_fctx; + /* Note if the buffer is valid, and has storage created */ + if ((buf_state & BM_VALID) && (buf_state & BM_TAG_VALID)) + isvalid = true; + else + isvalid = false; - if (funcctx->call_cntr < funcctx->max_calls) - { - uint32 i = funcctx->call_cntr; - Datum values[NUM_BUFFERCACHE_PAGES_ELEM]; - bool nulls[NUM_BUFFERCACHE_PAGES_ELEM]; + UnlockBufHdr(bufHdr); - values[0] = Int32GetDatum(fctx->record[i].bufferid); + /* Build the tuple and add it to tuplestore */ + values[0] = Int32GetDatum(bufferid); nulls[0] = false; /* * Set all fields except the bufferid to null if the buffer is unused * or not valid. */ - if (fctx->record[i].blocknum == InvalidBlockNumber || - fctx->record[i].isvalid == false) + if (blocknum == InvalidBlockNumber || isvalid == false) { nulls[1] = true; nulls[2] = true; @@ -255,56 +180,58 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) } else { - values[1] = ObjectIdGetDatum(fctx->record[i].relfilenumber); + values[1] = ObjectIdGetDatum(relfilenumber); nulls[1] = false; - values[2] = ObjectIdGetDatum(fctx->record[i].reltablespace); + values[2] = ObjectIdGetDatum(reltablespace); nulls[2] = false; - values[3] = ObjectIdGetDatum(fctx->record[i].reldatabase); + values[3] = ObjectIdGetDatum(reldatabase); nulls[3] = false; - values[4] = ObjectIdGetDatum(fctx->record[i].forknum); + values[4] = Int16GetDatum(forknum); nulls[4] = false; - values[5] = Int64GetDatum((int64) fctx->record[i].blocknum); + values[5] = Int64GetDatum((int64) blocknum); nulls[5] = false; - values[6] = BoolGetDatum(fctx->record[i].isdirty); + values[6] = BoolGetDatum(isdirty); nulls[6] = false; - values[7] = Int16GetDatum(fctx->record[i].usagecount); + values[7] = UInt16GetDatum(usagecount); nulls[7] = false; /* unused for v1.0 callers, but the array is always long enough */ - values[8] = Int32GetDatum(fctx->record[i].pinning_backends); + values[8] = Int32GetDatum(pinning_backends); nulls[8] = false; } - /* Build and return the tuple. */ - tuple = heap_form_tuple(fctx->tupdesc, values, nulls); - result = HeapTupleGetDatum(tuple); - - SRF_RETURN_NEXT(funcctx, result); + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } - else - SRF_RETURN_DONE(funcctx); + + return (Datum) 0; } /* - * Inquire about NUMA memory mappings for shared buffers. + * Inquire about OS pages mappings for shared buffers, with NUMA information, + * optionally. + * + * When "include_numa" is false, this routines ignores everything related + * to NUMA (returned as NULL values), returning mapping information between + * shared buffers and OS pages. * - * Returns NUMA node ID for each memory page used by the buffer. Buffers may - * be smaller or larger than OS memory pages. For each buffer we return one - * entry for each memory page used by the buffer (if the buffer is smaller, - * it only uses a part of one memory page). + * When "include_numa" is true, NUMA is initialized and numa_node values + * are generated. In order to get reliable results we also need to touch + * memory pages, so that the inquiry about NUMA memory node does not return + * -2, indicating unmapped/unallocated pages. + * + * Buffers may be smaller or larger than OS memory pages. For each buffer we + * return one entry for each memory page used by the buffer (if the buffer is + * smaller, it only uses a part of one memory page). * * We expect both sizes (for buffers and memory pages) to be a power-of-2, so * one is always a multiple of the other. * - * In order to get reliable results we also need to touch memory pages, so - * that the inquiry about NUMA memory node doesn't return -2 (which indicates - * unmapped/unallocated pages). */ -Datum -pg_buffercache_numa_pages(PG_FUNCTION_ARGS) +static Datum +pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa) { FuncCallContext *funcctx; MemoryContext oldcontext; - BufferCacheNumaContext *fctx; /* User function context. */ + BufferCacheOsPagesContext *fctx; /* User function context. */ TupleDesc tupledesc; TupleDesc expected_tupledesc; HeapTuple tuple; @@ -315,16 +242,15 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) int i, idx; Size os_page_size; - void **os_page_ptrs; - int *os_page_status; - uint64 os_page_count; int pages_per_buffer; + int *os_page_status = NULL; + uint64 os_page_count = 0; int max_entries; - volatile uint64 touch pg_attribute_unused(); char *startptr, *endptr; - if (pg_numa_init() == -1) + /* If NUMA information is requested, initialize NUMA support. */ + if (include_numa && pg_numa_init() == -1) elog(ERROR, "libnuma initialization failed or NUMA is not supported on this platform"); /* @@ -352,46 +278,56 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) */ Assert((os_page_size % BLCKSZ == 0) || (BLCKSZ % os_page_size == 0)); - /* - * How many addresses we are going to query? Simply get the page for - * the first buffer, and first page after the last buffer, and count - * the pages from that. - */ - startptr = (char *) TYPEALIGN_DOWN(os_page_size, - BufferGetBlock(1)); - endptr = (char *) TYPEALIGN(os_page_size, - (char *) BufferGetBlock(NBuffers) + BLCKSZ); - os_page_count = (endptr - startptr) / os_page_size; - - /* Used to determine the NUMA node for all OS pages at once */ - os_page_ptrs = palloc0(sizeof(void *) * os_page_count); - os_page_status = palloc(sizeof(uint64) * os_page_count); - - /* Fill pointers for all the memory pages. */ - idx = 0; - for (char *ptr = startptr; ptr < endptr; ptr += os_page_size) + if (include_numa) { - os_page_ptrs[idx++] = ptr; + void **os_page_ptrs = NULL; + + /* + * How many addresses we are going to query? Simply get the page + * for the first buffer, and first page after the last buffer, and + * count the pages from that. + */ + startptr = (char *) TYPEALIGN_DOWN(os_page_size, + BufferGetBlock(1)); + endptr = (char *) TYPEALIGN(os_page_size, + (char *) BufferGetBlock(NBuffers) + BLCKSZ); + os_page_count = (endptr - startptr) / os_page_size; + + /* Used to determine the NUMA node for all OS pages at once */ + os_page_ptrs = palloc0_array(void *, os_page_count); + os_page_status = palloc_array(int, os_page_count); + + /* + * Fill pointers for all the memory pages. This loop stores and + * touches (if needed) addresses into os_page_ptrs[] as input to + * one big move_pages(2) inquiry system call, as done in + * pg_numa_query_pages(). + */ + idx = 0; + for (char *ptr = startptr; ptr < endptr; ptr += os_page_size) + { + os_page_ptrs[idx++] = ptr; - /* Only need to touch memory once per backend process lifetime */ - if (firstNumaTouch) - pg_numa_touch_mem_if_required(touch, ptr); - } + /* Only need to touch memory once per backend process lifetime */ + if (firstNumaTouch) + pg_numa_touch_mem_if_required(ptr); + } - Assert(idx == os_page_count); + Assert(idx == os_page_count); - elog(DEBUG1, "NUMA: NBuffers=%d os_page_count=" UINT64_FORMAT " " - "os_page_size=%zu", NBuffers, os_page_count, os_page_size); + elog(DEBUG1, "NUMA: NBuffers=%d os_page_count=" UINT64_FORMAT " " + "os_page_size=%zu", NBuffers, os_page_count, os_page_size); - /* - * If we ever get 0xff back from kernel inquiry, then we probably have - * bug in our buffers to OS page mapping code here. - */ - memset(os_page_status, 0xff, sizeof(int) * os_page_count); + /* + * If we ever get 0xff back from kernel inquiry, then we probably + * have bug in our buffers to OS page mapping code here. + */ + memset(os_page_status, 0xff, sizeof(int) * os_page_count); - /* Query NUMA status for all the pointers */ - if (pg_numa_query_pages(0, os_page_count, os_page_ptrs, os_page_status) == -1) - elog(ERROR, "failed NUMA pages inquiry: %m"); + /* Query NUMA status for all the pointers */ + if (pg_numa_query_pages(0, os_page_count, os_page_ptrs, os_page_status) == -1) + elog(ERROR, "failed NUMA pages inquiry: %m"); + } /* Initialize the multi-call context, load entries about buffers */ @@ -401,12 +337,12 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* Create a user function context for cross-call persistence */ - fctx = (BufferCacheNumaContext *) palloc(sizeof(BufferCacheNumaContext)); + fctx = palloc_object(BufferCacheOsPagesContext); if (get_call_result_type(fcinfo, NULL, &expected_tupledesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); - if (expected_tupledesc->natts != NUM_BUFFERCACHE_NUMA_ELEM) + if (expected_tupledesc->natts != NUM_BUFFERCACHE_OS_PAGES_ELEM) elog(ERROR, "incorrect number of output arguments"); /* Construct a tuple descriptor for the result rows. */ @@ -418,7 +354,9 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) TupleDescInitEntry(tupledesc, (AttrNumber) 3, "numa_node", INT4OID, -1, 0); + TupleDescFinalize(tupledesc); fctx->tupdesc = BlessTupleDesc(tupledesc); + fctx->include_numa = include_numa; /* * Each buffer needs at least one entry, but it might be offset in @@ -430,15 +368,15 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) pages_per_buffer = Max(1, BLCKSZ / os_page_size) + 1; max_entries = NBuffers * pages_per_buffer; - /* Allocate entries for BufferCachePagesRec records. */ - fctx->record = (BufferCacheNumaRec *) + /* Allocate entries for BufferCacheOsPagesRec records. */ + fctx->record = (BufferCacheOsPagesRec *) MemoryContextAllocHuge(CurrentMemoryContext, - sizeof(BufferCacheNumaRec) * max_entries); + sizeof(BufferCacheOsPagesRec) * max_entries); /* Return to original context when allocating transient memory */ MemoryContextSwitchTo(oldcontext); - if (firstNumaTouch) + if (include_numa && firstNumaTouch) elog(DEBUG1, "NUMA: page-faulting the buffercache for proper NUMA readouts"); /* @@ -448,10 +386,6 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) * We don't hold the partition locks, so we don't get a consistent * snapshot across all buffers, but we do grab the buffer header * locks, so the information of each buffer is self-consistent. - * - * This loop touches and stores addresses into os_page_ptrs[] as input - * to one big move_pages(2) inquiry system call. Basically we ask for - * all memory pages for NBuffers. */ startptr = (char *) TYPEALIGN_DOWN(os_page_size, (char *) BufferGetBlock(1)); idx = 0; @@ -459,7 +393,6 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) { char *buffptr = (char *) BufferGetBlock(i + 1); BufferDesc *bufHdr; - uint32 buf_state; uint32 bufferid; int32 page_num; char *startptr_buff, @@ -470,9 +403,9 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) bufHdr = GetBufferDescriptor(i); /* Lock each buffer header before inspecting. */ - buf_state = LockBufHdr(bufHdr); + LockBufHdr(bufHdr); bufferid = BufferDescriptorGetBuffer(bufHdr); - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); /* start of the first page of this buffer */ startptr_buff = (char *) TYPEALIGN_DOWN(os_page_size, buffptr); @@ -490,7 +423,7 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) { fctx->record[idx].bufferid = bufferid; fctx->record[idx].page_num = page_num; - fctx->record[idx].numa_node = os_page_status[page_num]; + fctx->record[idx].numa_node = include_numa ? os_page_status[page_num] : -1; /* advance to the next entry/page */ ++idx; @@ -498,14 +431,18 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) } } - Assert((idx >= os_page_count) && (idx <= max_entries)); + Assert(idx <= max_entries); + + if (include_numa) + Assert(idx >= os_page_count); /* Set max calls and remember the user function context. */ funcctx->max_calls = idx; funcctx->user_fctx = fctx; - /* Remember this backend touched the pages */ - firstNumaTouch = false; + /* Remember this backend touched the pages (only relevant for NUMA) */ + if (include_numa) + firstNumaTouch = false; } funcctx = SRF_PERCALL_SETUP(); @@ -516,8 +453,8 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) if (funcctx->call_cntr < funcctx->max_calls) { uint32 i = funcctx->call_cntr; - Datum values[NUM_BUFFERCACHE_NUMA_ELEM]; - bool nulls[NUM_BUFFERCACHE_NUMA_ELEM]; + Datum values[NUM_BUFFERCACHE_OS_PAGES_ELEM]; + bool nulls[NUM_BUFFERCACHE_OS_PAGES_ELEM]; values[0] = Int32GetDatum(fctx->record[i].bufferid); nulls[0] = false; @@ -525,8 +462,26 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) values[1] = Int64GetDatum(fctx->record[i].page_num); nulls[1] = false; - values[2] = Int32GetDatum(fctx->record[i].numa_node); - nulls[2] = false; + if (fctx->include_numa) + { + /* status is valid node number */ + if (fctx->record[i].numa_node >= 0) + { + values[2] = Int32GetDatum(fctx->record[i].numa_node); + nulls[2] = false; + } + else + { + /* some kind of error (e.g. pages moved to swap) */ + values[2] = (Datum) 0; + nulls[2] = true; + } + } + else + { + values[2] = (Datum) 0; + nulls[2] = true; + } /* Build and return the tuple. */ tuple = heap_form_tuple(fctx->tupdesc, values, nulls); @@ -538,6 +493,30 @@ pg_buffercache_numa_pages(PG_FUNCTION_ARGS) SRF_RETURN_DONE(funcctx); } +/* + * pg_buffercache_os_pages + * + * Retrieve information about OS pages, with or without NUMA information. + */ +Datum +pg_buffercache_os_pages(PG_FUNCTION_ARGS) +{ + bool include_numa; + + /* Get the boolean parameter that controls the NUMA behavior. */ + include_numa = PG_GETARG_BOOL(0); + + return pg_buffercache_os_pages_internal(fcinfo, include_numa); +} + +/* Backward-compatible wrapper for v1.6. */ +Datum +pg_buffercache_numa_pages(PG_FUNCTION_ARGS) +{ + /* Call internal function with include_numa=true */ + return pg_buffercache_os_pages_internal(fcinfo, true); +} + Datum pg_buffercache_summary(PG_FUNCTION_ARGS) { @@ -559,7 +538,9 @@ pg_buffercache_summary(PG_FUNCTION_ARGS) for (int i = 0; i < NBuffers; i++) { BufferDesc *bufHdr; - uint32 buf_state; + uint64 buf_state; + + CHECK_FOR_INTERRUPTS(); /* * This function summarizes the state of all headers. Locking the @@ -568,7 +549,7 @@ pg_buffercache_summary(PG_FUNCTION_ARGS) * noticeably increase the cost of the function. */ bufHdr = GetBufferDescriptor(i); - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); if (buf_state & BM_VALID) { @@ -618,9 +599,11 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS) for (int i = 0; i < NBuffers; i++) { BufferDesc *bufHdr = GetBufferDescriptor(i); - uint32 buf_state = pg_atomic_read_u32(&bufHdr->state); + uint64 buf_state = pg_atomic_read_u64(&bufHdr->state); int usage_count; + CHECK_FOR_INTERRUPTS(); + usage_count = BUF_STATE_GET_USAGECOUNT(buf_state); usage_counts[usage_count]++; @@ -772,3 +755,119 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS) PG_RETURN_DATUM(result); } + +/* + * Try to mark a shared buffer as dirty. + */ +Datum +pg_buffercache_mark_dirty(PG_FUNCTION_ARGS) +{ + + Datum result; + TupleDesc tupledesc; + HeapTuple tuple; + Datum values[NUM_BUFFERCACHE_MARK_DIRTY_ELEM]; + bool nulls[NUM_BUFFERCACHE_MARK_DIRTY_ELEM] = {0}; + + Buffer buf = PG_GETARG_INT32(0); + bool buffer_already_dirty; + + if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + pg_buffercache_superuser_check("pg_buffercache_mark_dirty"); + + if (buf < 1 || buf > NBuffers) + elog(ERROR, "bad buffer ID: %d", buf); + + values[0] = BoolGetDatum(MarkDirtyUnpinnedBuffer(buf, &buffer_already_dirty)); + values[1] = BoolGetDatum(buffer_already_dirty); + + tuple = heap_form_tuple(tupledesc, values, nulls); + result = HeapTupleGetDatum(tuple); + + PG_RETURN_DATUM(result); +} + +/* + * Try to mark all the shared buffers of a relation as dirty. + */ +Datum +pg_buffercache_mark_dirty_relation(PG_FUNCTION_ARGS) +{ + Datum result; + TupleDesc tupledesc; + HeapTuple tuple; + Datum values[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM]; + bool nulls[NUM_BUFFERCACHE_MARK_DIRTY_RELATION_ELEM] = {0}; + + Oid relOid; + Relation rel; + + int32 buffers_already_dirty = 0; + int32 buffers_dirtied = 0; + int32 buffers_skipped = 0; + + if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + pg_buffercache_superuser_check("pg_buffercache_mark_dirty_relation"); + + relOid = PG_GETARG_OID(0); + + rel = relation_open(relOid, AccessShareLock); + + if (RelationUsesLocalBuffers(rel)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only", + "pg_buffercache_mark_dirty_relation"))); + + MarkDirtyRelUnpinnedBuffers(rel, &buffers_dirtied, &buffers_already_dirty, + &buffers_skipped); + + relation_close(rel, AccessShareLock); + + values[0] = Int32GetDatum(buffers_dirtied); + values[1] = Int32GetDatum(buffers_already_dirty); + values[2] = Int32GetDatum(buffers_skipped); + + tuple = heap_form_tuple(tupledesc, values, nulls); + result = HeapTupleGetDatum(tuple); + + PG_RETURN_DATUM(result); +} + +/* + * Try to mark all the shared buffers as dirty. + */ +Datum +pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS) +{ + Datum result; + TupleDesc tupledesc; + HeapTuple tuple; + Datum values[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM]; + bool nulls[NUM_BUFFERCACHE_MARK_DIRTY_ALL_ELEM] = {0}; + + int32 buffers_already_dirty = 0; + int32 buffers_dirtied = 0; + int32 buffers_skipped = 0; + + if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + pg_buffercache_superuser_check("pg_buffercache_mark_dirty_all"); + + MarkDirtyAllUnpinnedBuffers(&buffers_dirtied, &buffers_already_dirty, + &buffers_skipped); + + values[0] = Int32GetDatum(buffers_dirtied); + values[1] = Int32GetDatum(buffers_already_dirty); + values[2] = Int32GetDatum(buffers_skipped); + + tuple = heap_form_tuple(tupledesc, values, nulls); + result = HeapTupleGetDatum(tuple); + + PG_RETURN_DATUM(result); +} diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql index 47cca1907c74b..127d604905ca0 100644 --- a/contrib/pg_buffercache/sql/pg_buffercache.sql +++ b/contrib/pg_buffercache/sql/pg_buffercache.sql @@ -5,6 +5,12 @@ select count(*) = (select setting::bigint where name = 'shared_buffers') from pg_buffercache; +-- For pg_buffercache_os_pages, we expect at least one entry for each buffer +select count(*) >= (select setting::bigint + from pg_settings + where name = 'shared_buffers') +from pg_buffercache_os_pages; + select buffers_used + buffers_unused > 0, buffers_dirty <= buffers_used, buffers_pinned <= buffers_used @@ -16,6 +22,7 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts() WHERE buffers >= 0; -- having to create a dedicated user, use the pg_database_owner pseudo-role. SET ROLE pg_database_owner; SELECT * FROM pg_buffercache; +SELECT * FROM pg_buffercache_os_pages; SELECT * FROM pg_buffercache_pages() AS p (wrong int); SELECT * FROM pg_buffercache_summary(); SELECT * FROM pg_buffercache_usage_counts(); @@ -24,13 +31,14 @@ RESET role; -- Check that pg_monitor is allowed to query view / function SET ROLE pg_monitor; SELECT count(*) > 0 FROM pg_buffercache; +SELECT count(*) > 0 FROM pg_buffercache_os_pages; SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary(); SELECT count(*) > 0 FROM pg_buffercache_usage_counts(); RESET role; ------ ----- Test pg_buffercache_evict* functions +---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions ------ CREATE ROLE regress_buffercache_normal; @@ -40,12 +48,17 @@ SET ROLE regress_buffercache_normal; SELECT * FROM pg_buffercache_evict(1); SELECT * FROM pg_buffercache_evict_relation(1); SELECT * FROM pg_buffercache_evict_all(); +SELECT * FROM pg_buffercache_mark_dirty(1); +SELECT * FROM pg_buffercache_mark_dirty_relation(1); +SELECT * FROM pg_buffercache_mark_dirty_all(); RESET ROLE; -- These should return nothing, because these are STRICT functions SELECT * FROM pg_buffercache_evict(NULL); SELECT * FROM pg_buffercache_evict_relation(NULL); +SELECT * FROM pg_buffercache_mark_dirty(NULL); +SELECT * FROM pg_buffercache_mark_dirty_relation(NULL); -- These should fail because they are not called by valid range of buffers -- Number of the shared buffers are limited by max integer @@ -53,11 +66,14 @@ SELECT 2147483647 max_buffers \gset SELECT * FROM pg_buffercache_evict(-1); SELECT * FROM pg_buffercache_evict(0); SELECT * FROM pg_buffercache_evict(:max_buffers); +SELECT * FROM pg_buffercache_mark_dirty(-1); +SELECT * FROM pg_buffercache_mark_dirty(0); +SELECT * FROM pg_buffercache_mark_dirty(:max_buffers); --- This should fail because pg_buffercache_evict_relation() doesn't accept --- local relations +-- These should fail because they don't accept local relations CREATE TEMP TABLE temp_pg_buffercache(); SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache'); +SELECT * FROM pg_buffercache_mark_dirty_relation('temp_pg_buffercache'); DROP TABLE temp_pg_buffercache; -- These shouldn't fail @@ -65,6 +81,9 @@ SELECT buffer_evicted IS NOT NULL FROM pg_buffercache_evict(1); SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_all(); CREATE TABLE shared_pg_buffercache(); SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg_buffercache'); +SELECT buffers_dirtied IS NOT NULL FROM pg_buffercache_mark_dirty_relation('shared_pg_buffercache'); DROP TABLE shared_pg_buffercache; +SELECT pg_buffercache_mark_dirty(1) IS NOT NULL; +SELECT pg_buffercache_mark_dirty_all() IS NOT NULL; DROP ROLE regress_buffercache_normal; diff --git a/contrib/pg_freespacemap/meson.build b/contrib/pg_freespacemap/meson.build index ff8eda3580e5a..67b46c77ff3af 100644 --- a/contrib/pg_freespacemap/meson.build +++ b/contrib/pg_freespacemap/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_freespacemap_sources = files( 'pg_freespacemap.c', diff --git a/contrib/pg_logicalinspect/meson.build b/contrib/pg_logicalinspect/meson.build index 5c0528a8c63ed..3e284986e3be0 100644 --- a/contrib/pg_logicalinspect/meson.build +++ b/contrib/pg_logicalinspect/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group pg_logicalinspect_sources = files('pg_logicalinspect.c') diff --git a/contrib/pg_logicalinspect/pg_logicalinspect.c b/contrib/pg_logicalinspect/pg_logicalinspect.c index 50e805d3195e0..9420fa5e0c7c7 100644 --- a/contrib/pg_logicalinspect/pg_logicalinspect.c +++ b/contrib/pg_logicalinspect/pg_logicalinspect.c @@ -3,7 +3,7 @@ * pg_logicalinspect.c * Functions to inspect contents of PostgreSQL logical snapshots * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pg_logicalinspect/pg_logicalinspect.c diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out index cb5c396c51925..12ab92629ab23 100644 --- a/contrib/pg_overexplain/expected/pg_overexplain.out +++ b/contrib/pg_overexplain/expected/pg_overexplain.out @@ -37,16 +37,17 @@ EXPLAIN (DEBUG) SELECT 1; Subplans Needing Rewind: none Relation OIDs: none Executor Parameter Types: none - Parse Location: 16 for 8 bytes + Parse Location: 0 to end (11 rows) EXPLAIN (RANGE_TABLE) SELECT 1; QUERY PLAN ------------------------------------------ Result (cost=0.00..0.01 rows=1 width=4) + RTIs: 1 RTI 1 (result): Eref: "*RESULT*" () -(3 rows) +(4 rows) -- Create a partitioned table. CREATE TABLE vegetables (id serial, name text, genus text) @@ -103,6 +104,7 @@ $$); Parallel Safe: true Plan Node ID: 2 Append RTIs: 1 + Child Append RTIs: none -> Seq Scan on brassica vegetables_1 Disabled Nodes: 0 Parallel Safe: true @@ -119,7 +121,7 @@ $$); Subplans Needing Rewind: none Relation OIDs: NNN... Executor Parameter Types: none - Parse Location: 41 to end + Parse Location: 0 to end RTI 1 (relation, inherited, in-from-clause): Eref: vegetables (id, name, genus) Relation: vegetables @@ -141,7 +143,7 @@ $$); Relation Kind: relation Relation Lock Mode: AccessShareLock Unprunable RTIs: 1 3 4 -(53 rows) +(54 rows) -- Test a different output format. SELECT explain_filter($$ @@ -196,6 +198,7 @@ $$); none + none + 1 + + none + 0 + + + @@ -240,7 +243,7 @@ $$); none + NNN... + none + - 53 to end + + 0 to end + + + + @@ -291,13 +294,131 @@ $$); false + false + + - 1 3 4 + - none + + + 1 3 4 + + none + + (1 row) +-- Test JSON format with RANGE_TABLE to verify valid JSON structure. +SELECT explain_filter($$ +EXPLAIN (RANGE_TABLE, FORMAT JSON, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + explain_filter +---------------------------------------------------------------- + [ + + { + + "Plan": { + + "Node Type": "Aggregate", + + "Strategy": "Sorted", + + "Partial Mode": "Simple", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Group Key": ["vegetables.genus"], + + "Plans": [ + + { + + "Node Type": "Sort", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Sort Key": ["vegetables.genus", "vegetables.name"],+ + "Plans": [ + + { + + "Node Type": "Append", + + "Parent Relationship": "Outer", + + "Parallel Aware": false, + + "Async Capable": false, + + "Disabled": false, + + "Append RTIs": "1", + + "Child Append RTIs": "none", + + "Subplans Removed": 0, + + "Plans": [ + + { + + "Node Type": "Seq Scan", + + "Parent Relationship": "Member", + + "Parallel Aware": false, + + "Async Capable": false, + + "Relation Name": "brassica", + + "Alias": "vegetables_1", + + "Disabled": false, + + "Scan RTI": 3 + + }, + + { + + "Node Type": "Seq Scan", + + "Parent Relationship": "Member", + + "Parallel Aware": false, + + "Async Capable": false, + + "Relation Name": "daucus", + + "Alias": "vegetables_2", + + "Disabled": false, + + "Scan RTI": 4 + + } + + ] + + } + + ] + + } + + ] + + }, + + "Range Table": [ + + { + + "RTI": 1, + + "Kind": "relation", + + "Inherited": true, + + "In From Clause": true, + + "Eref": "vegetables (id, name, genus)", + + "Relation": "vegetables", + + "Relation Kind": "partitioned_table", + + "Relation Lock Mode": "AccessShareLock", + + "Permission Info Index": 1, + + "Security Barrier": false, + + "Lateral": false + + }, + + { + + "RTI": 2, + + "Kind": "group", + + "Inherited": false, + + "In From Clause": false, + + "Eref": "\"*GROUP*\" (genus)", + + "Security Barrier": false, + + "Lateral": false + + }, + + { + + "RTI": 3, + + "Kind": "relation", + + "Inherited": false, + + "In From Clause": true, + + "Alias": "vegetables (id, name, genus)", + + "Eref": "vegetables (id, name, genus)", + + "Relation": "brassica", + + "Relation Kind": "relation", + + "Relation Lock Mode": "AccessShareLock", + + "Security Barrier": false, + + "Lateral": false + + }, + + { + + "RTI": 4, + + "Kind": "relation", + + "Inherited": false, + + "In From Clause": true, + + "Alias": "vegetables (id, name, genus)", + + "Eref": "vegetables (id, name, genus)", + + "Relation": "daucus", + + "Relation Kind": "relation", + + "Relation Lock Mode": "AccessShareLock", + + "Security Barrier": false, + + "Lateral": false + + } + + ], + + "Unprunable RTIs": "1 3 4", + + "Result RTIs": "none" + + } + + ] +(1 row) + -- Test just the DEBUG option. Verify that it shows information about -- disabled nodes, parallel safety, and the parallelModeNeeded flag. SET enable_seqscan = false; @@ -344,7 +465,7 @@ $$); Subplans Needing Rewind: none Relation OIDs: NNN... Executor Parameter Types: none - Parse Location: 28 to end + Parse Location: 0 to end (37 rows) SET debug_parallel_query = false; @@ -372,7 +493,7 @@ $$); Subplans Needing Rewind: none Relation OIDs: NNN... Executor Parameter Types: 0 - Parse Location: 28 to end + Parse Location: 0 to end (15 rows) -- Create an index, and then attempt to force a nested loop with inner index @@ -436,7 +557,7 @@ $$); Subplans Needing Rewind: none Relation OIDs: NNN... Executor Parameter Types: 23 - Parse Location: 75 for 62 bytes + Parse Location: 0 to end (47 rows) RESET enable_hashjoin; @@ -451,6 +572,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus'; Seq Scan on daucus vegetables Filter: (genus = 'daucus'::text) Scan RTI: 2 + Elided Node Type: Append + Elided Node RTIs: 1 RTI 1 (relation, inherited, in-from-clause): Eref: vegetables (id, name, genus) Relation: vegetables @@ -464,7 +587,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus'; Relation Kind: relation Relation Lock Mode: AccessShareLock Unprunable RTIs: 1 2 -(16 rows) +(18 rows) -- Also test a case that involves a write. EXPLAIN (RANGE_TABLE, COSTS OFF) @@ -475,6 +598,7 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica'); Nominal RTI: 1 Exclude Relation RTI: 0 -> Result + RTIs: 2 RTI 1 (relation): Eref: vegetables (id, name, genus) Relation: vegetables @@ -485,5 +609,178 @@ INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica'); Eref: "*RESULT*" () Unprunable RTIs: 1 Result RTIs: 1 -(14 rows) +(15 rows) + +-- should show "Subplan: sub" +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM vegetables v, + (SELECT * FROM vegetables WHERE genus = 'daucus' OFFSET 0) sub; + QUERY PLAN +---------------------------------------------- + Nested Loop + -> Seq Scan on daucus vegetables + Filter: (genus = 'daucus'::text) + Scan RTI: 6 + Elided Node Type: Append + Elided Node RTIs: 5 + Elided Node Type: SubqueryScan + Elided Node RTIs: 2 + -> Append + Append RTIs: 1 + Child Append RTIs: none + -> Seq Scan on brassica v_1 + Scan RTI: 3 + -> Seq Scan on daucus v_2 + Scan RTI: 4 + RTI 1 (relation, inherited, in-from-clause): + Alias: v () + Eref: v (id, name, genus) + Relation: vegetables + Relation Kind: partitioned_table + Relation Lock Mode: AccessShareLock + Permission Info Index: 1 + RTI 2 (subquery, in-from-clause): + Alias: sub () + Eref: sub (id, name, genus) + RTI 3 (relation, in-from-clause): + Alias: v (id, name, genus) + Eref: v (id, name, genus) + Relation: brassica + Relation Kind: relation + Relation Lock Mode: AccessShareLock + RTI 4 (relation, in-from-clause): + Alias: v (id, name, genus) + Eref: v (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + RTI 5 (relation, inherited, in-from-clause): + Subplan: sub + Eref: vegetables (id, name, genus) + Relation: vegetables + Relation Kind: partitioned_table + Relation Lock Mode: AccessShareLock + Permission Info Index: 2 + RTI 6 (relation, in-from-clause): + Subplan: sub + Alias: vegetables (id, name, genus) + Eref: vegetables (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + Unprunable RTIs: 1 3 4 5 6 +(52 rows) + +-- should show "Subplan: unnamed_subquery" +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM vegetables v, + (SELECT * FROM vegetables WHERE genus = 'daucus' OFFSET 0); + QUERY PLAN +---------------------------------------------- + Nested Loop + -> Seq Scan on daucus vegetables + Filter: (genus = 'daucus'::text) + Scan RTI: 6 + Elided Node Type: Append + Elided Node RTIs: 5 + Elided Node Type: SubqueryScan + Elided Node RTIs: 2 + -> Append + Append RTIs: 1 + Child Append RTIs: none + -> Seq Scan on brassica v_1 + Scan RTI: 3 + -> Seq Scan on daucus v_2 + Scan RTI: 4 + RTI 1 (relation, inherited, in-from-clause): + Alias: v () + Eref: v (id, name, genus) + Relation: vegetables + Relation Kind: partitioned_table + Relation Lock Mode: AccessShareLock + Permission Info Index: 1 + RTI 2 (subquery, in-from-clause): + Eref: unnamed_subquery (id, name, genus) + RTI 3 (relation, in-from-clause): + Alias: v (id, name, genus) + Eref: v (id, name, genus) + Relation: brassica + Relation Kind: relation + Relation Lock Mode: AccessShareLock + RTI 4 (relation, in-from-clause): + Alias: v (id, name, genus) + Eref: v (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + RTI 5 (relation, inherited, in-from-clause): + Subplan: unnamed_subquery + Eref: vegetables (id, name, genus) + Relation: vegetables + Relation Kind: partitioned_table + Relation Lock Mode: AccessShareLock + Permission Info Index: 2 + RTI 6 (relation, in-from-clause): + Subplan: unnamed_subquery + Alias: vegetables (id, name, genus) + Eref: vegetables (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + Unprunable RTIs: 1 3 4 5 6 +(51 rows) + +-- Property graph test +CREATE PROPERTY GRAPH vegetables_graph +VERTEX TABLES +( + daucus KEY(name) DEFAULT LABEL LABEL vegetables, + brassica KEY(name) DEFAULT LABEL LABEL vegetables +); +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (vegetables_graph MATCH (v1 IS vegetables) WHERE v1.genus = 'daucus' COLUMNS (v1.name)); + QUERY PLAN +---------------------------------------------- + Append + Append RTIs: 1 + Child Append RTIs: none + -> Seq Scan on daucus + Filter: (genus = 'daucus'::text) + Scan RTI: 4 + Elided Node Type: SubqueryScan + Elided Node RTIs: 2 + -> Seq Scan on brassica + Filter: (genus = 'daucus'::text) + Scan RTI: 5 + Elided Node Type: SubqueryScan + Elided Node RTIs: 3 + RTI 1 (subquery, inherited, in-from-clause): + Eref: "graph_table" (name) + Relation: vegetables_graph + Relation Kind: property_graph + Relation Lock Mode: AccessShareLock + Permission Info Index: 1 + Lateral: true + RTI 2 (subquery): + Eref: unnamed_subquery (name) + Lateral: true + RTI 3 (subquery): + Eref: unnamed_subquery (name) + Lateral: true + RTI 4 (relation): + Subplan: unnamed_subquery + Eref: daucus (id, name, genus) + Relation: daucus + Relation Kind: relation + Relation Lock Mode: AccessShareLock + Permission Info Index: 2 + RTI 5 (relation): + Subplan: unnamed_subquery_1 + Eref: brassica (id, name, genus) + Relation: brassica + Relation Kind: relation + Relation Lock Mode: AccessShareLock + Permission Info Index: 3 + Unprunable RTIs: 1 4 5 +(41 rows) diff --git a/contrib/pg_overexplain/meson.build b/contrib/pg_overexplain/meson.build index 6f52d1e51bc7e..03657f874b4f1 100644 --- a/contrib/pg_overexplain/meson.build +++ b/contrib/pg_overexplain/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_overexplain_sources = files( 'pg_overexplain.c', diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c index de824566f8c90..a93a4dcfed6ef 100644 --- a/contrib/pg_overexplain/pg_overexplain.c +++ b/contrib/pg_overexplain/pg_overexplain.c @@ -3,7 +3,7 @@ * pg_overexplain.c * allow EXPLAIN to dump even more details * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * contrib/pg_overexplain/pg_overexplain.c *------------------------------------------------------------------------- @@ -54,6 +54,8 @@ static void overexplain_alias(const char *qlabel, Alias *alias, ExplainState *es); static void overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es); +static void overexplain_bitmapset_list(const char *qlabel, List *bms_list, + ExplainState *es); static void overexplain_intlist(const char *qlabel, List *list, ExplainState *es); @@ -71,9 +73,11 @@ _PG_init(void) es_extension_id = GetExplainExtensionId("pg_overexplain"); /* Register the new EXPLAIN options implemented by this module. */ - RegisterExtensionExplainOption("debug", overexplain_debug_handler); + RegisterExtensionExplainOption("debug", overexplain_debug_handler, + GUCCheckBooleanExplainOption); RegisterExtensionExplainOption("range_table", - overexplain_range_table_handler); + overexplain_range_table_handler, + GUCCheckBooleanExplainOption); /* Use the per-node and per-plan hooks to make our options do something. */ prev_explain_per_node_hook = explain_per_node_hook; @@ -95,7 +99,7 @@ overexplain_ensure_options(ExplainState *es) if (options == NULL) { - options = palloc0(sizeof(overexplain_options)); + options = palloc0_object(overexplain_options); SetExplainExtensionState(es, es_extension_id, options); } @@ -191,6 +195,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors, */ if (options->range_table) { + bool opened_elided_nodes = false; + switch (nodeTag(plan)) { case T_SeqScan: @@ -230,15 +236,71 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors, overexplain_bitmapset("Append RTIs", ((Append *) plan)->apprelids, es); + overexplain_bitmapset_list("Child Append RTIs", + ((Append *) plan)->child_append_relid_sets, + es); break; case T_MergeAppend: overexplain_bitmapset("Append RTIs", ((MergeAppend *) plan)->apprelids, es); + overexplain_bitmapset_list("Child Append RTIs", + ((MergeAppend *) plan)->child_append_relid_sets, + es); + break; + case T_Result: + + /* + * 'relids' is only meaningful when plan->lefttree is NULL, + * but if somehow it ends up set when plan->lefttree is not + * NULL, print it anyway. + */ + if (plan->lefttree == NULL || + ((Result *) plan)->relids != NULL) + overexplain_bitmapset("RTIs", + ((Result *) plan)->relids, + es); break; default: break; } + + foreach_node(ElidedNode, n, es->pstmt->elidedNodes) + { + char *elidednodetag; + + if (n->plan_node_id != plan->plan_node_id) + continue; + + if (!opened_elided_nodes) + { + ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es); + opened_elided_nodes = true; + } + + switch (n->elided_type) + { + case T_Append: + elidednodetag = "Append"; + break; + case T_MergeAppend: + elidednodetag = "MergeAppend"; + break; + case T_SubqueryScan: + elidednodetag = "SubqueryScan"; + break; + default: + elidednodetag = psprintf("%d", n->elided_type); + break; + } + + ExplainOpenGroup("Elided Node", NULL, true, es); + ExplainPropertyText("Elided Node Type", elidednodetag, es); + overexplain_bitmapset("Elided Node RTIs", n->relids, es); + ExplainCloseGroup("Elided Node", NULL, true, es); + } + if (opened_elided_nodes) + ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es); } } @@ -383,6 +445,8 @@ static void overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) { Index rti; + ListCell *lc_subrtinfo = list_head(plannedstmt->subrtinfos); + SubPlanRTInfo *rtinfo = NULL; /* Open group, one entry per RangeTblEntry */ ExplainOpenGroup("Range Table", "Range Table", false, es); @@ -393,6 +457,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable); char *kind = NULL; char *relkind; + SubPlanRTInfo *next_rtinfo; + + /* Advance to next SubPlanRTInfo, if it's time. */ + if (lc_subrtinfo != NULL) + { + next_rtinfo = lfirst(lc_subrtinfo); + if (rti > next_rtinfo->rtoffset) + { + rtinfo = next_rtinfo; + lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo); + } + } /* NULL entries are possible; skip them */ if (rte == NULL) @@ -431,6 +507,17 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) case RTE_GROUP: kind = "group"; break; + case RTE_GRAPH_TABLE: + + /* + * We should not see RTE of this kind here since property + * graph RTE gets converted to subquery RTE in + * rewriteGraphTable(). In case we decide not to do the + * conversion and leave RTE kind unchanged in future, print + * correct name of RTE kind. + */ + kind = "graph_table"; + break; } /* Begin group for this specific RTE */ @@ -457,6 +544,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) ExplainPropertyBool("In From Clause", rte->inFromCl, es); } + /* + * Indicate which subplan is the origin of which RTE. Note dummy + * subplans. Here again, we crunch more onto one line in text format. + */ + if (rtinfo != NULL) + { + if (es->format == EXPLAIN_FORMAT_TEXT) + { + if (!rtinfo->dummy) + ExplainPropertyText("Subplan", rtinfo->plan_name, es); + else + ExplainPropertyText("Subplan", + psprintf("%s (dummy)", + rtinfo->plan_name), es); + } + else + { + ExplainPropertyText("Subplan", rtinfo->plan_name, es); + ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es); + } + } + /* rte->alias is optional; rte->eref is requested */ if (rte->alias != NULL) overexplain_alias("Alias", rte->alias, es); @@ -522,6 +631,9 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) case RELKIND_PARTITIONED_INDEX: relkind = "partitioned_index"; break; + case RELKIND_PROPGRAPH: + relkind = "property_graph"; + break; case '\0': relkind = NULL; break; @@ -644,6 +756,12 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) ExplainPropertyFloat("ENR Tuples", NULL, rte->enrtuples, 0, es); } + /* + * rewriteGraphTable() clears graph_pattern and graph_table_columns + * fields, so skip them. No graph table specific fields are required + * to be printed. + */ + /* * add_rte_to_flat_rtable will clear groupexprs and securityQuals, so * skip that field. We have handled inFromCl above, so the only thing @@ -658,17 +776,22 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) ExplainCloseGroup("Range Table Entry", NULL, true, es); } - /* Print PlannedStmt fields that contain RTIs. */ + /* Close the Range Table array before emitting PlannedStmt-level fields. */ + ExplainCloseGroup("Range Table", "Range Table", false, es); + + /* + * Print PlannedStmt fields that contain RTIs. These are properties of + * the PlannedStmt, not of individual RTEs, so they belong outside the + * Range Table array. + */ if (es->format != EXPLAIN_FORMAT_TEXT || !bms_is_empty(plannedstmt->unprunableRelids)) overexplain_bitmapset("Unprunable RTIs", plannedstmt->unprunableRelids, es); if (es->format != EXPLAIN_FORMAT_TEXT || - plannedstmt->resultRelations != NIL) - overexplain_intlist("Result RTIs", plannedstmt->resultRelations, es); - - /* Close group, we're all done */ - ExplainCloseGroup("Range Table", "Range Table", false, es); + !bms_is_empty(plannedstmt->resultRelationRelids)) + overexplain_bitmapset("Result RTIs", plannedstmt->resultRelationRelids, + es); } /* @@ -728,6 +851,54 @@ overexplain_bitmapset(const char *qlabel, Bitmapset *bms, ExplainState *es) pfree(buf.data); } +/* + * Emit a text property describing the contents of a list of bitmapsets. + * If a bitmapset contains exactly 1 member, we just print an integer; + * otherwise, we surround the list of members by parentheses. + * + * If there are no bitmapsets in the list, we print the word "none". + */ +static void +overexplain_bitmapset_list(const char *qlabel, List *bms_list, + ExplainState *es) +{ + StringInfoData buf; + + initStringInfo(&buf); + + foreach_node(Bitmapset, bms, bms_list) + { + if (bms_membership(bms) == BMS_SINGLETON) + appendStringInfo(&buf, " %d", bms_singleton_member(bms)); + else + { + int x = -1; + bool first = true; + + appendStringInfoString(&buf, " ("); + while ((x = bms_next_member(bms, x)) >= 0) + { + if (first) + first = false; + else + appendStringInfoChar(&buf, ' '); + appendStringInfo(&buf, "%d", x); + } + appendStringInfoChar(&buf, ')'); + } + } + + if (buf.len == 0) + { + ExplainPropertyText(qlabel, "none", es); + return; + } + + Assert(buf.data[0] == ' '); + ExplainPropertyText(qlabel, buf.data + 1, es); + pfree(buf.data); +} + /* * Emit a text property describing the contents of a list of integers, OIDs, * or XIDs -- either a space-separated list of integer members, or the word diff --git a/contrib/pg_overexplain/sql/pg_overexplain.sql b/contrib/pg_overexplain/sql/pg_overexplain.sql index 42e275ac2f906..3f17b61a2dafd 100644 --- a/contrib/pg_overexplain/sql/pg_overexplain.sql +++ b/contrib/pg_overexplain/sql/pg_overexplain.sql @@ -66,6 +66,12 @@ EXPLAIN (DEBUG, RANGE_TABLE, FORMAT XML, COSTS OFF) SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus $$); +-- Test JSON format with RANGE_TABLE to verify valid JSON structure. +SELECT explain_filter($$ +EXPLAIN (RANGE_TABLE, FORMAT JSON, COSTS OFF) +SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus +$$); + -- Test just the DEBUG option. Verify that it shows information about -- disabled nodes, parallel safety, and the parallelModeNeeded flag. SET enable_seqscan = false; @@ -110,3 +116,24 @@ SELECT * FROM vegetables WHERE genus = 'daucus'; -- Also test a case that involves a write. EXPLAIN (RANGE_TABLE, COSTS OFF) INSERT INTO vegetables (name, genus) VALUES ('broccoflower', 'brassica'); + +-- should show "Subplan: sub" +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM vegetables v, + (SELECT * FROM vegetables WHERE genus = 'daucus' OFFSET 0) sub; + +-- should show "Subplan: unnamed_subquery" +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM vegetables v, + (SELECT * FROM vegetables WHERE genus = 'daucus' OFFSET 0); + +-- Property graph test +CREATE PROPERTY GRAPH vegetables_graph +VERTEX TABLES +( + daucus KEY(name) DEFAULT LABEL LABEL vegetables, + brassica KEY(name) DEFAULT LABEL LABEL vegetables +); + +EXPLAIN (RANGE_TABLE, COSTS OFF) +SELECT * FROM GRAPH_TABLE (vegetables_graph MATCH (v1 IS vegetables) WHERE v1.genus = 'daucus' COLUMNS (v1.name)); diff --git a/contrib/pg_plan_advice/.gitignore b/contrib/pg_plan_advice/.gitignore new file mode 100644 index 0000000000000..3f46c27b147be --- /dev/null +++ b/contrib/pg_plan_advice/.gitignore @@ -0,0 +1,7 @@ +/pgpa_parser.h +/pgpa_parser.c +/pgpa_scanner.c +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/contrib/pg_plan_advice/Makefile b/contrib/pg_plan_advice/Makefile new file mode 100644 index 0000000000000..d016723794dae --- /dev/null +++ b/contrib/pg_plan_advice/Makefile @@ -0,0 +1,47 @@ +# contrib/pg_plan_advice/Makefile + +MODULE_big = pg_plan_advice +OBJS = \ + $(WIN32RES) \ + pg_plan_advice.o \ + pgpa_ast.o \ + pgpa_identifier.o \ + pgpa_join.o \ + pgpa_output.o \ + pgpa_parser.o \ + pgpa_planner.o \ + pgpa_scan.o \ + pgpa_scanner.o \ + pgpa_trove.o \ + pgpa_walker.o + +HEADERS_pg_plan_advice = pg_plan_advice.h + +PGFILEDESC = "pg_plan_advice - help the planner get the right plan" + +REGRESS = alternatives gather join_order join_strategy partitionwise \ + prepared scan semijoin syntax + +EXTRA_INSTALL = contrib/tsm_system_time + +EXTRA_CLEAN = pgpa_parser.h pgpa_parser.c pgpa_scanner.c + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/pg_plan_advice +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +# See notes in src/backend/parser/Makefile about the following two rules +pgpa_parser.h: pgpa_parser.c + touch $@ + +pgpa_parser.c: BISONFLAGS += -d + +# Force these dependencies to be known even without dependency info built: +pgpa_parser.o pgpa_scanner.o: pgpa_parser.h diff --git a/contrib/pg_plan_advice/README b/contrib/pg_plan_advice/README new file mode 100644 index 0000000000000..2ea61e9bc4120 --- /dev/null +++ b/contrib/pg_plan_advice/README @@ -0,0 +1,267 @@ +contrib/pg_plan_advice/README + +Plan Advice +=========== + +This module implements a mini-language for "plan advice" that allows for +control of certain key planner decisions. Goals include (1) enforcing plan +stability (my previous plan was good and I would like to keep getting a +similar one) and (2) allowing users to experiment with plans other than +the one preferred by the optimizer. Non-goals include (1) controlling +every possible planner decision and (2) forcing consideration of plans +that the optimizer rejects for reasons other than cost. (There is some +room for bikeshedding about what exactly this non-goal means: what if +we skip path generation entirely for a certain case on the theory that +we know it cannot win on cost? Does that count as a cost-based rejection +even though no cost was ever computed?) + +Generally, plan advice is a series of whitespace-separated advice items, +each of which applies an advice tag to a list of advice targets. For +example, "SEQ_SCAN(foo) HASH_JOIN(bar@ss)" contains two items of advice, +the first of which applies the SEQ_SCAN tag to "foo" and the second of +which applies the HASH_JOIN tag to "bar@ss". In this simple example, each +target identifies a single relation; see "Relation Identifiers", below. +Advice tags can also be applied to groups of relations; for example, +"HASH_JOIN(baz (bletch quux))" applies the HASH_JOIN tag to the single +relation identifier "baz" as well as to the 2-item list containing +"bletch" and "quux". + +Critically, this module knows both how to generate plan advice from an +already-existing plan, and also how to enforce it during future planning +cycles. Everything it does is intended to be "round-trip safe": if you +generate advice from a plan and then feed that back into a future planning +cycle, each piece of advice should be guaranteed to apply to exactly the +same part of the query from which it was generated without ambiguity or +guesswork, and it should successfully enforce the same planning decision that +led to it being generated in the first place. Note that there is no +intention that these guarantees hold in the presence of intervening DDL; +e.g. if you change the properties of a function so that a subquery is no +longer inlined, or if you drop an index named in the plan advice, the advice +isn't going to work any more. That's expected. + +This module aims to force the planner to follow any provided advice without +regard to whether it appears to be good advice or bad advice. If the +user provides bad advice, whether derived from a previously-generated plan +or manually written, they may get a bad plan. We regard this as user error, +not a defect in this module. It seems likely that applying advice +judiciously and only when truly required to avoid problems will be a more +successful strategy than applying it with a broad brush, but users are free +to experiment with whatever strategies they think best. + +Relation Identifiers +==================== + +Uniquely identifying the part of a query to which a certain piece of +advice applies is harder than it sounds. Our basic approach is to use +relation aliases as a starting point, and then disambiguate. There are +three ways that the same relation alias can occur multiple times: + +1. It can appear in more than one subquery. + +2. It can appear more than once in the same subquery, + e.g. (foo JOIN bar) x JOIN foo. + +3. The table can be partitioned. + +Any combination of these things can occur simultaneously. Therefore, our +general syntax for a relation identifier is: + +alias_name#occurrence_number/partition_schema.partition_name@plan_name + +All components except for the alias_name are optional and included only +when required. When a component is omitted, the associated punctuation +must also be omitted. Occurrence numbers are counted ignoring children of +partitioned tables. When the generated occurrence number is 1, we omit +the occurrence number. The partition schema and partition name are included +only for children of partitioned tables. In generated advice, the +partition_schema is always included whenever there is a partition_name, +but user-written advice may mention the name and omit the schema. The +plan_name is omitted for the top-level PlannerInfo. + +Scan Advice +=========== + +For many types of scan, no advice is generated or possible; for instance, +a subquery is always scanned using a subquery scan. While that scan may be +elided via setrefs processing, this doesn't change the fact that only one +basic approach exists. Hence, scan advice applies mostly to relations, which +can be scanned in multiple ways. + +We tend to think of a scan as targeting a single relation, and that's +normally the case, but it doesn't have to be. For instance, if a join is +proven empty, the whole thing may be replaced with a single Result node +which, in effect, is a degenerate scan of every relation in the collapsed +portion of the join tree. Similarly, it's possible to inject a custom scan +in such a way that it replaces an entire join. If we ever emit advice +for these cases, it would target sets of relation identifiers surrounded +by parentheses, e.g., SOME_SORT_OF_SCAN(foo (bar baz)) would mean that +the given scan type would be used for foo as a single relation and also the +combination of bar and baz as a join product. We have no such cases at +present. + +For index and index-only scans, both the relation being scanned and the +index or indexes being used must be specified. For example, INDEX_SCAN(foo +foo_a_idx bar bar_b_idx) indicates that an index scan (not an index-only +scan) should be used on foo_a_idx when scanning foo, and that an index scan +should be used on bar_b_idx when scanning bar. + +Bitmap heap scans currently do not allow for an index specification: +BITMAP_HEAP_SCAN(foo bar) simply means that each of foo and bar should use +some sort of bitmap heap scan. + +There is a special DO_NOT_SCAN() advice tag which says that a certain +relation shouldn't be scanned at all. This is used to control which of +two choices is selected when an AlternativeSubPlan is resolved, and +whether or not a MinMaxAggPath is chosen. Control over upper planner +behavior is generally out-of-scope at the moment, but these cases had +to be handled to prevent test_plan_advice failures in the buildfarm. + +Join Order Advice +================= + +The JOIN_ORDER tag specifies the order in which several tables that are +part of the same join problem should be joined. Each subquery (except for +those that are inlined) is a separate join problem. Within a subquery, +partitionwise joins can create additional, separate join problems. Hence, +queries involving partitionwise joins may use JOIN_ORDER() many times. + +We take the canonical join structure to be an outer-deep tree, so +JOIN_ORDER(t1 t2 t3) says that t1 is the driving table and should be joined +first to t2 and then to t3. If the join problem involves additional tables, +they can be joined in any order after the join between t1, t2, and t3 has +been constructed. Generated join advice always mentions all tables +in the join problem, but manually written join advice need not do so. + +For trees which are not outer-deep, parentheses can be used. For example, +JOIN_ORDER(t1 (t2 t3)) says that the top-level join should have t1 on the +outer side and a join between t2 and t3 on the inner side. That join should +be constructed so that t2 is on the outer side and t3 is on the inner side. + +In some cases, it's not possible to fully specify the join order in this way. +For example, if t2 and t3 are being scanned by a single custom scan or foreign +scan, or if a partitionwise join is being performed between those tables, then +it's impossible to say that t2 is the outer table and t3 is the inner table, +or the other way around; it's just undefined. In such cases, we generate +join advice that uses curly braces, intending to indicate a lack of ordering: +JOIN_ORDER(t1 {t2 t3}) says that the uppermost join should have t1 on the outer +side and some kind of join between t2 and t3 on the inner side, but without +saying how that join must be performed or anything about which relation should +appear on which side of the join, or even whether this kind of join has sides. + +Join Method Advice +================== + +Tags such as NESTED_LOOP_PLAIN specify the method that should be used to +perform a certain join. More specifically, NESTED_LOOP_PLAIN(x (y z)) says +that the plan should put the relation whose identifier is "x" on the inner +side of a plain nested loop (one without materialization or memoization) +and that it should also put a join between the relation whose identifier is +"y" and the relation whose identifier is "z" on the inner side of a nested +loop. Hence, for an N-table join problem, there will be N-1 pieces of join +method advice; no join method advice is required for the outermost +table in the join problem. + +Considering that we have both join order advice and join method advice, +it might seem natural to say that NESTED_LOOP_PLAIN(x) should be redefined +to mean that x should appear by itself on one side or the other of a nested +loop, rather than specifically on the inner side, but this definition appears +useless in practice. It gives the planner too much freedom to do things that +bear little resemblance to what the user probably had in mind. This makes +only a limited amount of practical difference in the case of a merge join or +unparameterized nested loop, but for a parameterized nested loop or a hash +join, the two sides are treated very differently, and saying that a certain +relation should be involved in one of those operations without saying which +role it should take isn't saying much. + +This choice of definition implies that join method advice also imposes some +join order constraints. For example, given a join between foo and bar, +HASH_JOIN(bar) implies that foo is the driving table. Otherwise, it would +be impossible to put bar beneath the inner side of a Hash Join. + +Note that, given this definition, it's reasonable to consider deleting the +join order advice but applying the join method advice. For example, +consider a star schema with tables fact, dim1, dim2, dim3, dim4, and dim5. +The automatically generated advice might specify JOIN_ORDER(fact dim1 dim3 +dim4 dim2 dim5) HASH_JOIN(dim2 dim4) NESTED_LOOP_PLAIN(dim1 dim3 dim5). +Deleting the JOIN_ORDER advice allows the planner to reorder the joins +however it likes while still forcing the same choice of join method. This +seems potentially useful, and is one reason why a unified syntax that controls +both join order and join method in a single locution was not chosen. + +Advice Completeness +=================== + +An essential guiding principle is that no inference may be made on the basis +of the absence of advice. The user is entitled to remove any portion of the +generated advice which they deem unsuitable or counterproductive and the +result should only be to increase the flexibility afforded to the planner. +This means that if advice can say that a certain optimization or technique +should be used, it should also be able to say that the optimization or +technique should not be used. We should never assume that the absence of an +instruction to do a certain thing means that it should not be done; all +instructions must be explicit. + +Semijoin Uniqueness +=================== + +Faced with a semijoin, the planner considers both a direct implementation +and a plan where the one side is made unique and then an inner join is +performed. We emit SEMIJOIN_UNIQUE() advice when this transformation occurs +and SEMIJOIN_NON_UNIQUE() advice when it doesn't. These items work like +join method advice: the inner side of the relevant join is named, and the +chosen join order must be compatible with the advice having some effect. + +Partitionwise +============= + +PARTITIONWISE() advice can be used to specify both those partitionwise joins +which should be performed and those which should not be performed; the idea +is that each argument to PARTITIONWISE specifies a set of relations that +should be scanned partitionwise after being joined to each other and nothing +else. Hence, for example, PARTITIONWISE((t1 t2) t3) specifies that the +query should contain a partitionwise join between t1 and t2 and that t3 +should not be part of any partitionwise join. If there are no other rels +in the query, specifying just PARTITIONWISE((t1 t2)) would have the same +effect, since there would be no other rels to which t3 could be joined in +a partitionwise fashion. + +Parallel Query (Gather, etc.) +============================= + +Each argument to GATHER() or GATHER_MERGE() is a single relation or an +exact set of relations on top of which a Gather or Gather Merge node, +respectively, should be placed. Each argument to NO_GATHER() is a single +relation that should not appear beneath any Gather or Gather Merge node; +that is, parallelism should not be used. + +Implicit Join Order Constraints +=============================== + +When JOIN_ORDER() advice is not provided for a particular join problem, +other pieces of advice may still incidentally constrain the join order. +For example, a user who specifies HASH_JOIN((foo bar)) is explicitly saying +that there should be a hash join with exactly foo and bar on the inner +side of it, but that also implies that foo and bar must be joined to +each other before either of them is joined to anything else. Otherwise, +the join the user is attempting to constrain won't actually occur in the +query, which ends up looking like the system has just decided to ignore +the advice altogether. + +Future Work +=========== + +We don't handle choice of aggregation: it would be nice to be able to force +sorted or grouped aggregation. I'm guessing this can be left to future work. + +More seriously, we don't know anything about eager aggregation, which could +have a large impact on the shape of the plan tree. XXX: This needs some study +to determine how large a problem it is, and might need to be fixed sooner +rather than later. + +We don't offer any control over estimates, only outcomes. It seems like a +good idea to incorporate that ability at some future point, as pg_hint_plan +does. However, since the primary goal of the initial development work is to be +able to induce the planner to recreate a desired plan that worked well in +the past, this has not been included in the initial development effort. + +XXX Need to investigate whether and how well supplying advice works with GEQO diff --git a/contrib/pg_plan_advice/expected/alternatives.out b/contrib/pg_plan_advice/expected/alternatives.out new file mode 100644 index 0000000000000..a6fb296d4b445 --- /dev/null +++ b/contrib/pg_plan_advice/expected/alternatives.out @@ -0,0 +1,158 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +CREATE TABLE alt_t1 (a int) WITH (autovacuum_enabled = false); +CREATE TABLE alt_t2 (a int) WITH (autovacuum_enabled = false); +CREATE INDEX ON alt_t2(a); +INSERT INTO alt_t1 SELECT generate_series(1, 1000); +INSERT INTO alt_t2 SELECT generate_series(1, 100000); +VACUUM ANALYZE alt_t1; +VACUUM ANALYZE alt_t2; +-- This query uses an OR to prevent the EXISTS from being converted to a +-- semi-join, forcing the planner through the AlternativeSubPlan path. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM alt_t1 +WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0; + QUERY PLAN +------------------------------------------------------------------- + Seq Scan on alt_t1 + Filter: ((ANY (a = (hashed SubPlan exists_2).col1)) OR (a < 0)) + SubPlan exists_2 + -> Seq Scan on alt_t2 + Generated Plan Advice: + SEQ_SCAN(alt_t1 alt_t2@exists_2) + NO_GATHER(alt_t1 alt_t2@exists_2) + DO_NOT_SCAN(alt_t2@exists_1) +(8 rows) + +-- We should be able to force either AlternativeSubPlan by advising against +-- scanning the other relation. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_t2@exists_1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM alt_t1 +WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0; + QUERY PLAN +------------------------------------------------------------------- + Seq Scan on alt_t1 + Filter: ((ANY (a = (hashed SubPlan exists_2).col1)) OR (a < 0)) + SubPlan exists_2 + -> Seq Scan on alt_t2 + Supplied Plan Advice: + DO_NOT_SCAN(alt_t2@exists_1) /* matched */ + Generated Plan Advice: + SEQ_SCAN(alt_t1 alt_t2@exists_2) + NO_GATHER(alt_t1 alt_t2@exists_2) + DO_NOT_SCAN(alt_t2@exists_1) +(10 rows) + +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_t2@exists_2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM alt_t1 +WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0; + QUERY PLAN +-------------------------------------------------------- + Seq Scan on alt_t1 + Filter: (EXISTS(SubPlan exists_1) OR (a < 0)) + SubPlan exists_1 + -> Index Only Scan using alt_t2_a_idx on alt_t2 + Index Cond: (a = alt_t1.a) + Supplied Plan Advice: + DO_NOT_SCAN(alt_t2@exists_2) /* matched */ + Generated Plan Advice: + SEQ_SCAN(alt_t1) + INDEX_ONLY_SCAN(alt_t2@exists_1 public.alt_t2_a_idx) + NO_GATHER(alt_t1 alt_t2@exists_1) + DO_NOT_SCAN(alt_t2@exists_2) +(12 rows) + +COMMIT; +-- Now let's test a case involving MinMaxAggPath, which we treat similarly +-- to the AlternativeSubPlan case. +CREATE TABLE alt_minmax (a int) WITH (autovacuum_enabled = false); +CREATE INDEX ON alt_minmax(a); +INSERT INTO alt_minmax SELECT generate_series(1, 10000); +VACUUM ANALYZE alt_minmax; +-- Using an Index Scan inside of an InitPlan should win over a full table +-- scan. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; + QUERY PLAN +------------------------------------------------------------------------------------------ + Result + Replaces: MinMaxAggregate + InitPlan minmax_1 + -> Limit + -> Index Only Scan using alt_minmax_a_idx on alt_minmax + Index Cond: (a IS NOT NULL) + InitPlan minmax_2 + -> Limit + -> Index Only Scan Backward using alt_minmax_a_idx on alt_minmax alt_minmax_1 + Index Cond: (a IS NOT NULL) + Generated Plan Advice: + INDEX_ONLY_SCAN(alt_minmax@minmax_1 public.alt_minmax_a_idx + alt_minmax@minmax_2 public.alt_minmax_a_idx) + NO_GATHER(alt_minmax@minmax_1 alt_minmax@minmax_2) + DO_NOT_SCAN(alt_minmax) +(15 rows) + +-- Advising against the scan of alt_minmax at the root query level should +-- change nothing, but if we say we don't want either of or both of the +-- minmax-variant scans, the plan should switch to a full table scan. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; + QUERY PLAN +------------------------------------------------------------------------------------------ + Result + Replaces: MinMaxAggregate + InitPlan minmax_1 + -> Limit + -> Index Only Scan using alt_minmax_a_idx on alt_minmax + Index Cond: (a IS NOT NULL) + InitPlan minmax_2 + -> Limit + -> Index Only Scan Backward using alt_minmax_a_idx on alt_minmax alt_minmax_1 + Index Cond: (a IS NOT NULL) + Supplied Plan Advice: + DO_NOT_SCAN(alt_minmax) /* matched */ + Generated Plan Advice: + INDEX_ONLY_SCAN(alt_minmax@minmax_1 public.alt_minmax_a_idx + alt_minmax@minmax_2 public.alt_minmax_a_idx) + NO_GATHER(alt_minmax@minmax_1 alt_minmax@minmax_2) + DO_NOT_SCAN(alt_minmax) +(17 rows) + +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax@minmax_1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; + QUERY PLAN +-------------------------------------------------------- + Aggregate + -> Seq Scan on alt_minmax + Supplied Plan Advice: + DO_NOT_SCAN(alt_minmax@minmax_1) /* matched */ + Generated Plan Advice: + SEQ_SCAN(alt_minmax) + NO_GATHER(alt_minmax) + DO_NOT_SCAN(alt_minmax@minmax_1 alt_minmax@minmax_2) +(8 rows) + +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax@minmax_1) DO_NOT_SCAN(alt_minmax@minmax_2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; + QUERY PLAN +-------------------------------------------------------- + Aggregate + -> Seq Scan on alt_minmax + Supplied Plan Advice: + DO_NOT_SCAN(alt_minmax@minmax_1) /* matched */ + DO_NOT_SCAN(alt_minmax@minmax_2) /* matched */ + Generated Plan Advice: + SEQ_SCAN(alt_minmax) + NO_GATHER(alt_minmax) + DO_NOT_SCAN(alt_minmax@minmax_1 alt_minmax@minmax_2) +(9 rows) + +COMMIT; +DROP TABLE alt_t1, alt_t2, alt_minmax; diff --git a/contrib/pg_plan_advice/expected/gather.out b/contrib/pg_plan_advice/expected/gather.out new file mode 100644 index 0000000000000..0cc0dedf859a1 --- /dev/null +++ b/contrib/pg_plan_advice/expected/gather.out @@ -0,0 +1,371 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 1; +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET debug_parallel_query = off; +CREATE TABLE gt_dim (id serial primary key, dim text) + WITH (autovacuum_enabled = false); +INSERT INTO gt_dim (dim) SELECT random()::text FROM generate_series(1,100) g; +VACUUM ANALYZE gt_dim; +CREATE TABLE gt_fact ( + id int not null, + dim_id integer not null references gt_dim (id) +) WITH (autovacuum_enabled = false); +INSERT INTO gt_fact + SELECT g, (g%3)+1 FROM generate_series(1,100000) g; +VACUUM ANALYZE gt_fact; +-- By default, we expect Gather Merge with a parallel hash join. +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------------- + Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: f.dim_id + -> Parallel Hash Join + Hash Cond: (f.dim_id = d.id) + -> Parallel Seq Scan on gt_fact f + -> Parallel Hash + -> Parallel Seq Scan on gt_dim d + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + GATHER_MERGE((f d)) +(14 rows) + +-- Force Gather or Gather Merge of both relations together. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------------- + Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: f.dim_id + -> Parallel Hash Join + Hash Cond: (f.dim_id = d.id) + -> Parallel Seq Scan on gt_fact f + -> Parallel Hash + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER_MERGE((f d)) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + GATHER_MERGE((f d)) +(16 rows) + +SET LOCAL pg_plan_advice.advice = 'gather((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------------- + Sort + Sort Key: f.dim_id + -> Gather + Workers Planned: 1 + -> Parallel Hash Join + Hash Cond: (f.dim_id = d.id) + -> Parallel Seq Scan on gt_fact f + -> Parallel Hash + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER((f d)) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + GATHER((f d)) +(16 rows) + +COMMIT; +-- Force a separate Gather or Gather Merge operation for each relation. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +-------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: f.dim_id + -> Parallel Seq Scan on gt_fact f + -> Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: d.id + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER_MERGE(f) /* matched */ + GATHER_MERGE(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + SEQ_SCAN(f d) + GATHER_MERGE(f d) +(20 rows) + +SET LOCAL pg_plan_advice.advice = 'gather(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +-------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Sort + Sort Key: f.dim_id + -> Gather + Workers Planned: 1 + -> Parallel Seq Scan on gt_fact f + -> Sort + Sort Key: d.id + -> Gather + Workers Planned: 1 + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER(f) /* matched */ + GATHER(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + SEQ_SCAN(f d) + GATHER(f d) +(20 rows) + +SET LOCAL pg_plan_advice.advice = 'gather((d d/d.d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +-------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: f.dim_id + -> Parallel Seq Scan on gt_fact f + -> Index Scan using gt_dim_pkey on gt_dim d + Supplied Plan Advice: + GATHER((d d/d.d)) /* partially matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + SEQ_SCAN(f) + INDEX_SCAN(d public.gt_dim_pkey) + GATHER_MERGE(f) + NO_GATHER(d) +(17 rows) + +COMMIT; +-- Force a Gather or Gather Merge on one relation but no parallelism on other. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge(f) no_gather(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +-------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: f.dim_id + -> Parallel Seq Scan on gt_fact f + -> Index Scan using gt_dim_pkey on gt_dim d + Supplied Plan Advice: + GATHER_MERGE(f) /* matched */ + NO_GATHER(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + SEQ_SCAN(f) + INDEX_SCAN(d public.gt_dim_pkey) + GATHER_MERGE(f) + NO_GATHER(d) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'gather_merge(d) no_gather(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Sort + Sort Key: f.dim_id + -> Seq Scan on gt_fact f + -> Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: d.id + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER_MERGE(d) /* matched */ + NO_GATHER(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + SEQ_SCAN(f d) + GATHER_MERGE(d) + NO_GATHER(f) +(19 rows) + +SET LOCAL pg_plan_advice.advice = 'gather(f) no_gather(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +-------------------------------------------------- + Merge Join + Merge Cond: (d.id = f.dim_id) + -> Index Scan using gt_dim_pkey on gt_dim d + -> Sort + Sort Key: f.dim_id + -> Gather + Workers Planned: 1 + -> Parallel Seq Scan on gt_fact f + Supplied Plan Advice: + GATHER(f) /* matched */ + NO_GATHER(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + MERGE_JOIN_PLAIN(f) + SEQ_SCAN(f) + INDEX_SCAN(d public.gt_dim_pkey) + GATHER(f) + NO_GATHER(d) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'gather(d) no_gather(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Sort + Sort Key: f.dim_id + -> Seq Scan on gt_fact f + -> Sort + Sort Key: d.id + -> Gather + Workers Planned: 1 + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER(d) /* matched */ + NO_GATHER(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + SEQ_SCAN(f d) + GATHER(d) + NO_GATHER(f) +(19 rows) + +COMMIT; +-- Force no Gather or Gather Merge use at all. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'no_gather(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------ + Merge Join + Merge Cond: (d.id = f.dim_id) + -> Index Scan using gt_dim_pkey on gt_dim d + -> Sort + Sort Key: f.dim_id + -> Seq Scan on gt_fact f + Supplied Plan Advice: + NO_GATHER(f) /* matched */ + NO_GATHER(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + MERGE_JOIN_PLAIN(f) + SEQ_SCAN(f) + INDEX_SCAN(d public.gt_dim_pkey) + NO_GATHER(f d) +(15 rows) + +COMMIT; +-- Can't force Gather Merge without the ORDER BY clause, but just Gather is OK. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------------------- + Gather + Disabled: true + Workers Planned: 1 + -> Parallel Hash Join + Hash Cond: (f.dim_id = d.id) + -> Parallel Seq Scan on gt_fact f + -> Parallel Hash + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER_MERGE((f d)) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + GATHER((f d)) +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'gather((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------------------- + Gather + Workers Planned: 1 + -> Parallel Hash Join + Hash Cond: (f.dim_id = d.id) + -> Parallel Seq Scan on gt_fact f + -> Parallel Hash + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER((f d)) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + GATHER((f d)) +(14 rows) + +COMMIT; +-- Test conflicting advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather((f d)) no_gather(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + QUERY PLAN +------------------------------------------------------- + Gather Merge + Workers Planned: 1 + -> Sort + Sort Key: f.dim_id + -> Parallel Hash Join + Hash Cond: (f.dim_id = d.id) + -> Parallel Seq Scan on gt_fact f + -> Parallel Hash + -> Parallel Seq Scan on gt_dim d + Supplied Plan Advice: + GATHER((f d)) /* matched, conflicting, failed */ + NO_GATHER(f) /* matched, conflicting, failed */ + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + GATHER_MERGE((f d)) +(17 rows) + +COMMIT; diff --git a/contrib/pg_plan_advice/expected/join_order.out b/contrib/pg_plan_advice/expected/join_order.out new file mode 100644 index 0000000000000..a5a9728e3fde6 --- /dev/null +++ b/contrib/pg_plan_advice/expected/join_order.out @@ -0,0 +1,500 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +CREATE TABLE jo_dim1 (id integer primary key, dim1 text, val1 int) + WITH (autovacuum_enabled = false); +INSERT INTO jo_dim1 (id, dim1, val1) + SELECT g, 'some filler text ' || g, (g % 3) + 1 + FROM generate_series(1,100) g; +VACUUM ANALYZE jo_dim1; +CREATE TABLE jo_dim2 (id integer primary key, dim2 text, val2 int) + WITH (autovacuum_enabled = false); +INSERT INTO jo_dim2 (id, dim2, val2) + SELECT g, 'some filler text ' || g, (g % 53) + 1 + FROM generate_series(1,1000) g; +VACUUM ANALYZE jo_dim2; +CREATE TABLE jo_fact ( + id int primary key, + dim1_id integer not null references jo_dim1 (id), + dim2_id integer not null references jo_dim2 (id) +) WITH (autovacuum_enabled = false); +INSERT INTO jo_fact + SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g; +VACUUM ANALYZE jo_fact; +-- We expect to join to d2 first and then d1, since the condition on d2 +-- is more selective. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on jo_fact f + -> Hash + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + Generated Plan Advice: + JOIN_ORDER(f d2 d1) + HASH_JOIN(d2 d1) + SEQ_SCAN(f d2 d1) + NO_GATHER(f d1 d2) +(16 rows) + +-- Force a few different join orders. Some of these are very inefficient, +-- but the planner considers them all viable. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f d1 d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Seq Scan on jo_fact f + -> Hash + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + Supplied Plan Advice: + JOIN_ORDER(f d1 d2) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d1 d2) + HASH_JOIN(d1 d2) + SEQ_SCAN(f d1 d2) + NO_GATHER(f d1 d2) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(f d2 d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on jo_fact f + -> Hash + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + Supplied Plan Advice: + JOIN_ORDER(f d2 d1) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d2 d1) + HASH_JOIN(d2 d1) + SEQ_SCAN(f d2 d1) + NO_GATHER(f d1 d2) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(d1 f d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +----------------------------------------- + Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Hash Join + Hash Cond: (d1.id = f.dim1_id) + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on jo_fact f + -> Hash + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + Supplied Plan Advice: + JOIN_ORDER(d1 f d2) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d1 f d2) + HASH_JOIN(f d2) + SEQ_SCAN(d1 f d2) + NO_GATHER(f d1 d2) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(f (d1 d2))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------------------------ + Hash Join + Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id)) + -> Seq Scan on jo_fact f + -> Hash + -> Nested Loop + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Materialize + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + Supplied Plan Advice: + JOIN_ORDER(f (d1 d2)) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f (d1 d2)) + NESTED_LOOP_MATERIALIZE(d2) + HASH_JOIN((d1 d2)) + SEQ_SCAN(f d1 d2) + NO_GATHER(f d1 d2) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(f {d1 d2})'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------------------------ + Hash Join + Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id)) + -> Seq Scan on jo_fact f + -> Hash + -> Nested Loop + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Materialize + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + Supplied Plan Advice: + JOIN_ORDER(f {d1 d2}) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(f (d1 d2)) + NESTED_LOOP_MATERIALIZE(d2) + HASH_JOIN((d1 d2)) + SEQ_SCAN(f d1 d2) + NO_GATHER(f d1 d2) +(18 rows) + +COMMIT; +-- Force a join order by mentioning just a prefix of the join list. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------------ + Hash Join + Hash Cond: (d2.id = f.dim2_id) + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Seq Scan on jo_fact f + -> Hash + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + Supplied Plan Advice: + JOIN_ORDER(d2) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d2 (f d1)) + HASH_JOIN(d1 (f d1)) + SEQ_SCAN(d2 f d1) + NO_GATHER(f d1 d2) +(18 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(d2 d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------------------------ + Hash Join + Hash Cond: ((d1.id = f.dim1_id) AND (d2.id = f.dim2_id)) + -> Nested Loop + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + -> Materialize + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on jo_fact f + Supplied Plan Advice: + JOIN_ORDER(d2 d1) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d2 d1 f) + NESTED_LOOP_MATERIALIZE(d1) + HASH_JOIN(f) + SEQ_SCAN(d2 d1 f) + NO_GATHER(f d1 d2) +(18 rows) + +COMMIT; +-- jo_fact is not partitioned, but let's try pretending that it is and +-- verifying that the advice does not apply. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f/d1 d1 d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------------------------- + Nested Loop + Disabled: true + -> Nested Loop + Disabled: true + -> Seq Scan on jo_fact f + -> Index Scan using jo_dim1_pkey on jo_dim1 d1 + Index Cond: (id = f.dim1_id) + Filter: (val1 = 1) + -> Index Scan using jo_dim2_pkey on jo_dim2 d2 + Index Cond: (id = f.dim2_id) + Filter: (val2 = 1) + Supplied Plan Advice: + JOIN_ORDER(f/d1 d1 d2) /* partially matched */ + Generated Plan Advice: + JOIN_ORDER(f d1 d2) + NESTED_LOOP_PLAIN(d1 d2) + SEQ_SCAN(f) + INDEX_SCAN(d1 public.jo_dim1_pkey d2 public.jo_dim2_pkey) + NO_GATHER(f d1 d2) +(19 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(f/d1 (d1 d2))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +-------------------------------------------------------------- + Nested Loop + Disabled: true + Join Filter: ((d1.id = f.dim1_id) AND (d2.id = f.dim2_id)) + -> Nested Loop + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Materialize + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + -> Seq Scan on jo_fact f + Supplied Plan Advice: + JOIN_ORDER(f/d1 (d1 d2)) /* partially matched */ + Generated Plan Advice: + JOIN_ORDER(d1 d2 f) + NESTED_LOOP_PLAIN(f) + NESTED_LOOP_MATERIALIZE(d2) + SEQ_SCAN(d1 d2 f) + NO_GATHER(f d1 d2) +(18 rows) + +COMMIT; +-- The unusual formulation of this query is intended to prevent the query +-- planner from reducing the FULL JOIN to some other join type, so that we +-- can test what happens with a join type that cannot be reordered. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + QUERY PLAN +------------------------------------------------------------- + Nested Loop + Join Filter: ((d1.id = f.dim1_id) OR (f.dim1_id IS NULL)) + -> Merge Full Join + Merge Cond: (((d2.id + 0)) = ((f.dim2_id + 0))) + -> Sort + Sort Key: ((d2.id + 0)) + -> Seq Scan on jo_dim2 d2 + -> Sort + Sort Key: ((f.dim2_id + 0)) + -> Seq Scan on jo_fact f + -> Materialize + -> Seq Scan on jo_dim1 d1 + Generated Plan Advice: + JOIN_ORDER(d2 f d1) + MERGE_JOIN_PLAIN(f) + NESTED_LOOP_MATERIALIZE(d1) + SEQ_SCAN(d2 f d1) + NO_GATHER(d1 f d2) +(18 rows) + +-- We should not be able to force the planner to join f to d1 first, because +-- that is not a valid join order, but we should be able to force the planner +-- to make either d2 or f the driving table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f d1 d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + QUERY PLAN +------------------------------------------------------------- + Nested Loop + Disabled: true + Join Filter: ((d1.id = f.dim1_id) OR (f.dim1_id IS NULL)) + -> Merge Full Join + Disabled: true + Merge Cond: (((d2.id + 0)) = ((f.dim2_id + 0))) + -> Sort + Sort Key: ((d2.id + 0)) + -> Seq Scan on jo_dim2 d2 + -> Sort + Sort Key: ((f.dim2_id + 0)) + -> Seq Scan on jo_fact f + -> Seq Scan on jo_dim1 d1 + Supplied Plan Advice: + JOIN_ORDER(f d1 d2) /* partially matched */ + Generated Plan Advice: + JOIN_ORDER(d2 f d1) + MERGE_JOIN_PLAIN(f) + NESTED_LOOP_PLAIN(d1) + SEQ_SCAN(d2 f d1) + NO_GATHER(d1 f d2) +(21 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(f d2 d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + QUERY PLAN +------------------------------------------------------------- + Nested Loop + Join Filter: ((d1.id = f.dim1_id) OR (f.dim1_id IS NULL)) + -> Merge Full Join + Merge Cond: (((f.dim2_id + 0)) = ((d2.id + 0))) + -> Sort + Sort Key: ((f.dim2_id + 0)) + -> Seq Scan on jo_fact f + -> Sort + Sort Key: ((d2.id + 0)) + -> Seq Scan on jo_dim2 d2 + -> Materialize + -> Seq Scan on jo_dim1 d1 + Supplied Plan Advice: + JOIN_ORDER(f d2 d1) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d2 d1) + MERGE_JOIN_PLAIN(d2) + NESTED_LOOP_MATERIALIZE(d1) + SEQ_SCAN(f d2 d1) + NO_GATHER(d1 f d2) +(20 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(d2 f d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + QUERY PLAN +------------------------------------------------------------- + Nested Loop + Join Filter: ((d1.id = f.dim1_id) OR (f.dim1_id IS NULL)) + -> Merge Full Join + Merge Cond: (((d2.id + 0)) = ((f.dim2_id + 0))) + -> Sort + Sort Key: ((d2.id + 0)) + -> Seq Scan on jo_dim2 d2 + -> Sort + Sort Key: ((f.dim2_id + 0)) + -> Seq Scan on jo_fact f + -> Materialize + -> Seq Scan on jo_dim1 d1 + Supplied Plan Advice: + JOIN_ORDER(d2 f d1) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d2 f d1) + MERGE_JOIN_PLAIN(f) + NESTED_LOOP_MATERIALIZE(d1) + SEQ_SCAN(d2 f d1) + NO_GATHER(d1 f d2) +(20 rows) + +COMMIT; +-- Two incompatible join orders should conflict. In the second case, +-- the conflict is implicit: if d1 is on the inner side of a join of any +-- type, it cannot also be the driving table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f) join_order(d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + QUERY PLAN +------------------------------------------------------------- + Nested Loop + Join Filter: ((d1.id = f.dim1_id) OR (f.dim1_id IS NULL)) + -> Merge Full Join + Merge Cond: (((f.dim2_id + 0)) = ((d2.id + 0))) + -> Sort + Sort Key: ((f.dim2_id + 0)) + -> Seq Scan on jo_fact f + -> Sort + Sort Key: ((d2.id + 0)) + -> Seq Scan on jo_dim2 d2 + -> Materialize + -> Seq Scan on jo_dim1 d1 + Supplied Plan Advice: + JOIN_ORDER(f) /* matched, conflicting */ + JOIN_ORDER(d1) /* matched, conflicting, failed */ + Generated Plan Advice: + JOIN_ORDER(f d2 d1) + MERGE_JOIN_PLAIN(d2) + NESTED_LOOP_MATERIALIZE(d1) + SEQ_SCAN(f d2 d1) + NO_GATHER(d1 f d2) +(21 rows) + +SET LOCAL pg_plan_advice.advice = 'join_order(d1) hash_join(d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + QUERY PLAN +--------------------------------------------------------------- + Nested Loop + Join Filter: ((d1.id = f.dim1_id) OR (f.dim1_id IS NULL)) + -> Seq Scan on jo_dim1 d1 + -> Materialize + -> Merge Full Join + Merge Cond: (((d2.id + 0)) = ((f.dim2_id + 0))) + -> Sort + Sort Key: ((d2.id + 0)) + -> Seq Scan on jo_dim2 d2 + -> Sort + Sort Key: ((f.dim2_id + 0)) + -> Seq Scan on jo_fact f + Supplied Plan Advice: + JOIN_ORDER(d1) /* matched, conflicting */ + HASH_JOIN(d1) /* matched, conflicting, failed */ + Generated Plan Advice: + JOIN_ORDER(d1 (d2 f)) + MERGE_JOIN_PLAIN(f) + NESTED_LOOP_MATERIALIZE((f d2)) + SEQ_SCAN(d1 d2 f) + NO_GATHER(d1 f d2) +(21 rows) + +COMMIT; diff --git a/contrib/pg_plan_advice/expected/join_strategy.out b/contrib/pg_plan_advice/expected/join_strategy.out new file mode 100644 index 0000000000000..0f9db69219080 --- /dev/null +++ b/contrib/pg_plan_advice/expected/join_strategy.out @@ -0,0 +1,339 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +CREATE TABLE join_dim (id serial primary key, dim text) + WITH (autovacuum_enabled = false); +INSERT INTO join_dim (dim) SELECT random()::text FROM generate_series(1,100) g; +VACUUM ANALYZE join_dim; +CREATE TABLE join_fact ( + id int primary key, + dim_id integer not null references join_dim (id) +) WITH (autovacuum_enabled = false); +INSERT INTO join_fact + SELECT g, (g%3)+1 FROM generate_series(1,100000) g; +CREATE INDEX join_fact_dim_id ON join_fact (dim_id); +VACUUM ANALYZE join_fact; +-- We expect a hash join by default. +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------ + Hash Join + Hash Cond: (f.dim_id = d.id) + -> Seq Scan on join_fact f + -> Hash + -> Seq Scan on join_dim d + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + NO_GATHER(f d) +(10 rows) + +-- Try forcing each join method in turn with join_dim as the inner table. +-- All of these should work except for MERGE_JOIN_MATERIALIZE; that will +-- fail, because the planner knows that join_dim (id) is unique, and will +-- refuse to add mark/restore overhead. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'HASH_JOIN(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------ + Hash Join + Hash Cond: (f.dim_id = d.id) + -> Seq Scan on join_fact f + -> Hash + -> Seq Scan on join_dim d + Supplied Plan Advice: + HASH_JOIN(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_MATERIALIZE(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------------------- + Merge Join + Disabled: true + Merge Cond: (f.dim_id = d.id) + -> Index Scan using join_fact_dim_id on join_fact f + -> Index Scan using join_dim_pkey on join_dim d + Supplied Plan Advice: + MERGE_JOIN_MATERIALIZE(d) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + INDEX_SCAN(f public.join_fact_dim_id d public.join_dim_pkey) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_PLAIN(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------------------- + Merge Join + Merge Cond: (f.dim_id = d.id) + -> Index Scan using join_fact_dim_id on join_fact f + -> Index Scan using join_dim_pkey on join_dim d + Supplied Plan Advice: + MERGE_JOIN_PLAIN(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + MERGE_JOIN_PLAIN(d) + INDEX_SCAN(f public.join_fact_dim_id d public.join_dim_pkey) + NO_GATHER(f d) +(11 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MATERIALIZE(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +-------------------------------------------- + Nested Loop + Join Filter: (f.dim_id = d.id) + -> Seq Scan on join_fact f + -> Materialize + -> Seq Scan on join_dim d + Supplied Plan Advice: + NESTED_LOOP_MATERIALIZE(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + NESTED_LOOP_MATERIALIZE(d) + SEQ_SCAN(f d) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MEMOIZE(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------------- + Nested Loop + -> Seq Scan on join_fact f + -> Memoize + Cache Key: f.dim_id + Cache Mode: logical + -> Index Scan using join_dim_pkey on join_dim d + Index Cond: (id = f.dim_id) + Supplied Plan Advice: + NESTED_LOOP_MEMOIZE(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + NESTED_LOOP_MEMOIZE(d) + SEQ_SCAN(f) + INDEX_SCAN(d public.join_dim_pkey) + NO_GATHER(f d) +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------- + Nested Loop + -> Seq Scan on join_fact f + -> Index Scan using join_dim_pkey on join_dim d + Index Cond: (id = f.dim_id) + Supplied Plan Advice: + NESTED_LOOP_PLAIN(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + NESTED_LOOP_PLAIN(d) + SEQ_SCAN(f) + INDEX_SCAN(d public.join_dim_pkey) + NO_GATHER(f d) +(12 rows) + +COMMIT; +-- Now try forcing each join method in turn with join_fact as the inner +-- table. All of these should work. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'HASH_JOIN(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------- + Hash Join + Hash Cond: (d.id = f.dim_id) + -> Seq Scan on join_dim d + -> Hash + -> Seq Scan on join_fact f + Supplied Plan Advice: + HASH_JOIN(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + HASH_JOIN(f) + SEQ_SCAN(d f) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_MATERIALIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------------------- + Merge Join + Merge Cond: (d.id = f.dim_id) + -> Index Scan using join_dim_pkey on join_dim d + -> Materialize + -> Index Scan using join_fact_dim_id on join_fact f + Supplied Plan Advice: + MERGE_JOIN_MATERIALIZE(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + MERGE_JOIN_MATERIALIZE(f) + INDEX_SCAN(d public.join_dim_pkey f public.join_fact_dim_id) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_PLAIN(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------------------- + Merge Join + Merge Cond: (d.id = f.dim_id) + -> Index Scan using join_dim_pkey on join_dim d + -> Index Scan using join_fact_dim_id on join_fact f + Supplied Plan Advice: + MERGE_JOIN_PLAIN(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + MERGE_JOIN_PLAIN(f) + INDEX_SCAN(d public.join_dim_pkey f public.join_fact_dim_id) + NO_GATHER(f d) +(11 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MATERIALIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +-------------------------------------------- + Nested Loop + Join Filter: (f.dim_id = d.id) + -> Seq Scan on join_dim d + -> Materialize + -> Seq Scan on join_fact f + Supplied Plan Advice: + NESTED_LOOP_MATERIALIZE(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + NESTED_LOOP_MATERIALIZE(f) + SEQ_SCAN(d f) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MEMOIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +-------------------------------------------------------------- + Nested Loop + -> Seq Scan on join_dim d + -> Memoize + Cache Key: d.id + Cache Mode: logical + -> Index Scan using join_fact_dim_id on join_fact f + Index Cond: (dim_id = d.id) + Supplied Plan Advice: + NESTED_LOOP_MEMOIZE(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + NESTED_LOOP_MEMOIZE(f) + SEQ_SCAN(d) + INDEX_SCAN(f public.join_fact_dim_id) + NO_GATHER(f d) +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +-------------------------------------------------------- + Nested Loop + -> Seq Scan on join_dim d + -> Index Scan using join_fact_dim_id on join_fact f + Index Cond: (dim_id = d.id) + Supplied Plan Advice: + NESTED_LOOP_PLAIN(f) /* matched */ + Generated Plan Advice: + JOIN_ORDER(d f) + NESTED_LOOP_PLAIN(f) + SEQ_SCAN(d) + INDEX_SCAN(f public.join_fact_dim_id) + NO_GATHER(f d) +(12 rows) + +COMMIT; +-- Non-working cases. We can't force a foreign join between these tables, +-- because they aren't foreign tables. We also can't use two different +-- strategies on the same table, nor can we put both tables on the inner +-- side of the same join. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'FOREIGN_JOIN((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------- + Nested Loop + Disabled: true + -> Seq Scan on join_fact f + -> Index Scan using join_dim_pkey on join_dim d + Index Cond: (id = f.dim_id) + Supplied Plan Advice: + FOREIGN_JOIN((f d)) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(f d) + NESTED_LOOP_PLAIN(d) + SEQ_SCAN(f) + INDEX_SCAN(d public.join_dim_pkey) + NO_GATHER(f d) +(13 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f) NESTED_LOOP_MATERIALIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +----------------------------------------------------------------- + Merge Join + Merge Cond: (d.id = f.dim_id) + -> Index Scan using join_dim_pkey on join_dim d + -> Index Scan using join_fact_dim_id on join_fact f + Supplied Plan Advice: + NESTED_LOOP_PLAIN(f) /* matched, conflicting, failed */ + NESTED_LOOP_MATERIALIZE(f) /* matched, conflicting, failed */ + Generated Plan Advice: + JOIN_ORDER(d f) + MERGE_JOIN_PLAIN(f) + INDEX_SCAN(d public.join_dim_pkey f public.join_fact_dim_id) + NO_GATHER(f d) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------- + Nested Loop + Disabled: true + -> Seq Scan on join_fact f + -> Index Scan using join_dim_pkey on join_dim d + Index Cond: (id = f.dim_id) + Supplied Plan Advice: + NESTED_LOOP_PLAIN(f) /* matched, failed */ + NESTED_LOOP_PLAIN(d) /* matched */ + Generated Plan Advice: + JOIN_ORDER(f d) + NESTED_LOOP_PLAIN(d) + SEQ_SCAN(f) + INDEX_SCAN(d public.join_dim_pkey) + NO_GATHER(f d) +(14 rows) + +COMMIT; diff --git a/contrib/pg_plan_advice/expected/partitionwise.out b/contrib/pg_plan_advice/expected/partitionwise.out new file mode 100644 index 0000000000000..2b3d0a82443ef --- /dev/null +++ b/contrib/pg_plan_advice/expected/partitionwise.out @@ -0,0 +1,426 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +SET enable_partitionwise_join = true; +CREATE TABLE pt1 (id integer primary key, dim1 text, val1 int) + PARTITION BY RANGE (id); +CREATE TABLE pt1a PARTITION OF pt1 FOR VALUES FROM (1) to (1001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt1b PARTITION OF pt1 FOR VALUES FROM (1001) to (2001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt1c PARTITION OF pt1 FOR VALUES FROM (2001) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO pt1 (id, dim1, val1) + SELECT g, 'some filler text ' || g, (g % 3) + 1 + FROM generate_series(1,3000) g; +VACUUM ANALYZE pt1; +CREATE TABLE pt2 (id integer primary key, dim2 text, val2 int) + PARTITION BY RANGE (id); +CREATE TABLE pt2a PARTITION OF pt2 FOR VALUES FROM (1) to (1001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt2b PARTITION OF pt2 FOR VALUES FROM (1001) to (2001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt2c PARTITION OF pt2 FOR VALUES FROM (2001) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO pt2 (id, dim2, val2) + SELECT g, 'some other text ' || g, (g % 5) + 1 + FROM generate_series(1,3000,2) g; +VACUUM ANALYZE pt2; +CREATE TABLE pt3 (id integer primary key, dim3 text, val3 int) + PARTITION BY RANGE (id); +CREATE TABLE pt3a PARTITION OF pt3 FOR VALUES FROM (1) to (1001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt3b PARTITION OF pt3 FOR VALUES FROM (1001) to (2001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt3c PARTITION OF pt3 FOR VALUES FROM (2001) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO pt3 (id, dim3, val3) + SELECT g, 'a third random text ' || g, (g % 7) + 1 + FROM generate_series(1,3000,3) g; +VACUUM ANALYZE pt3; +CREATE TABLE ptmismatch (id integer primary key, dimm text, valm int) + PARTITION BY RANGE (id); +CREATE TABLE ptmismatcha PARTITION OF ptmismatch + FOR VALUES FROM (1) to (1501) + WITH (autovacuum_enabled = false); +CREATE TABLE ptmismatchb PARTITION OF ptmismatch + FOR VALUES FROM (1501) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO ptmismatch (id, dimm, valm) + SELECT g, 'yet another text ' || g, (g % 2) + 1 + FROM generate_series(1,3000) g; +VACUUM ANALYZE ptmismatch; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Append + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_1.id = pt3_1.id) + -> Seq Scan on pt2a pt2_1 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3a pt3_1 + Filter: (val3 = 1) + -> Index Scan using pt1a_pkey on pt1a pt1_1 + Index Cond: (id = pt2_1.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_2.id = pt3_2.id) + -> Seq Scan on pt2b pt2_2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3b pt3_2 + Filter: (val3 = 1) + -> Index Scan using pt1b_pkey on pt1b pt1_2 + Index Cond: (id = pt2_2.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_3.id = pt3_3.id) + -> Seq Scan on pt2c pt2_3 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3c pt3_3 + Filter: (val3 = 1) + -> Index Scan using pt1c_pkey on pt1c pt1_3 + Index Cond: (id = pt2_3.id) + Filter: (val1 = 1) + Generated Plan Advice: + JOIN_ORDER(pt2/public.pt2a pt3/public.pt3a pt1/public.pt1a) + JOIN_ORDER(pt2/public.pt2b pt3/public.pt3b pt1/public.pt1b) + JOIN_ORDER(pt2/public.pt2c pt3/public.pt3c pt1/public.pt1c) + NESTED_LOOP_PLAIN(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c) + HASH_JOIN(pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) + SEQ_SCAN(pt2/public.pt2a pt3/public.pt3a pt2/public.pt2b pt3/public.pt3b + pt2/public.pt2c pt3/public.pt3c) + INDEX_SCAN(pt1/public.pt1a public.pt1a_pkey pt1/public.pt1b public.pt1b_pkey + pt1/public.pt1c public.pt1c_pkey) + PARTITIONWISE((pt1 pt2 pt3)) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c pt2/public.pt2a + pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) +(47 rows) + +-- Suppress partitionwise join, or do it just partially. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE(pt1 pt2 pt3)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Nested Loop + -> Hash Join + Hash Cond: (pt2.id = pt3.id) + -> Append + -> Seq Scan on pt2a pt2_1 + Filter: (val2 = 1) + -> Seq Scan on pt2b pt2_2 + Filter: (val2 = 1) + -> Seq Scan on pt2c pt2_3 + Filter: (val2 = 1) + -> Hash + -> Append + -> Seq Scan on pt3a pt3_1 + Filter: (val3 = 1) + -> Seq Scan on pt3b pt3_2 + Filter: (val3 = 1) + -> Seq Scan on pt3c pt3_3 + Filter: (val3 = 1) + -> Append + -> Index Scan using pt1a_pkey on pt1a pt1_1 + Index Cond: (id = pt2.id) + Filter: (val1 = 1) + -> Index Scan using pt1b_pkey on pt1b pt1_2 + Index Cond: (id = pt2.id) + Filter: (val1 = 1) + -> Index Scan using pt1c_pkey on pt1c pt1_3 + Index Cond: (id = pt2.id) + Filter: (val1 = 1) + Supplied Plan Advice: + PARTITIONWISE(pt1) /* matched */ + PARTITIONWISE(pt2) /* matched */ + PARTITIONWISE(pt3) /* matched */ + Generated Plan Advice: + JOIN_ORDER(pt2 pt3 pt1) + NESTED_LOOP_PLAIN(pt1) + HASH_JOIN(pt3) + SEQ_SCAN(pt2/public.pt2a pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a + pt3/public.pt3b pt3/public.pt3c) + INDEX_SCAN(pt1/public.pt1a public.pt1a_pkey pt1/public.pt1b public.pt1b_pkey + pt1/public.pt1c public.pt1c_pkey) + PARTITIONWISE(pt2 pt3 pt1) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c pt2/public.pt2a + pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) +(43 rows) + +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE((pt1 pt2) pt3)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Hash Join + Hash Cond: (pt1.id = pt3.id) + -> Append + -> Hash Join + Hash Cond: (pt1_1.id = pt2_1.id) + -> Seq Scan on pt1a pt1_1 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on pt2a pt2_1 + Filter: (val2 = 1) + -> Hash Join + Hash Cond: (pt1_2.id = pt2_2.id) + -> Seq Scan on pt1b pt1_2 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on pt2b pt2_2 + Filter: (val2 = 1) + -> Hash Join + Hash Cond: (pt1_3.id = pt2_3.id) + -> Seq Scan on pt1c pt1_3 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on pt2c pt2_3 + Filter: (val2 = 1) + -> Hash + -> Append + -> Seq Scan on pt3a pt3_1 + Filter: (val3 = 1) + -> Seq Scan on pt3b pt3_2 + Filter: (val3 = 1) + -> Seq Scan on pt3c pt3_3 + Filter: (val3 = 1) + Supplied Plan Advice: + PARTITIONWISE((pt1 pt2)) /* matched */ + PARTITIONWISE(pt3) /* matched */ + Generated Plan Advice: + JOIN_ORDER(pt1/public.pt1a pt2/public.pt2a) + JOIN_ORDER(pt1/public.pt1b pt2/public.pt2b) + JOIN_ORDER(pt1/public.pt1c pt2/public.pt2c) + JOIN_ORDER({pt1 pt2} pt3) + HASH_JOIN(pt2/public.pt2a pt2/public.pt2b pt2/public.pt2c pt3) + SEQ_SCAN(pt1/public.pt1a pt2/public.pt2a pt1/public.pt1b pt2/public.pt2b + pt1/public.pt1c pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b + pt3/public.pt3c) + PARTITIONWISE((pt1 pt2) pt3) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c pt2/public.pt2a + pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) +(47 rows) + +COMMIT; +-- Test conflicting advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE((pt1 pt2) (pt1 pt3))'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Append + Disabled: true + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_1.id = pt3_1.id) + -> Seq Scan on pt2a pt2_1 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3a pt3_1 + Filter: (val3 = 1) + -> Index Scan using pt1a_pkey on pt1a pt1_1 + Index Cond: (id = pt2_1.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_2.id = pt3_2.id) + -> Seq Scan on pt2b pt2_2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3b pt3_2 + Filter: (val3 = 1) + -> Index Scan using pt1b_pkey on pt1b pt1_2 + Index Cond: (id = pt2_2.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_3.id = pt3_3.id) + -> Seq Scan on pt2c pt2_3 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3c pt3_3 + Filter: (val3 = 1) + -> Index Scan using pt1c_pkey on pt1c pt1_3 + Index Cond: (id = pt2_3.id) + Filter: (val1 = 1) + Supplied Plan Advice: + PARTITIONWISE((pt1 pt2)) /* matched, conflicting, failed */ + PARTITIONWISE((pt1 pt3)) /* matched, conflicting, failed */ + Generated Plan Advice: + JOIN_ORDER(pt2/public.pt2a pt3/public.pt3a pt1/public.pt1a) + JOIN_ORDER(pt2/public.pt2b pt3/public.pt3b pt1/public.pt1b) + JOIN_ORDER(pt2/public.pt2c pt3/public.pt3c pt1/public.pt1c) + NESTED_LOOP_PLAIN(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c) + HASH_JOIN(pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) + SEQ_SCAN(pt2/public.pt2a pt3/public.pt3a pt2/public.pt2b pt3/public.pt3b + pt2/public.pt2c pt3/public.pt3c) + INDEX_SCAN(pt1/public.pt1a public.pt1a_pkey pt1/public.pt1b public.pt1b_pkey + pt1/public.pt1c public.pt1c_pkey) + PARTITIONWISE((pt1 pt2 pt3)) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c pt2/public.pt2a + pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) +(51 rows) + +COMMIT; +-- Can't force a partitionwise join with a mismatched table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE((pt1 ptmismatch))'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, ptmismatch WHERE pt1.id = ptmismatch.id; + QUERY PLAN +--------------------------------------------------------------------------- + Nested Loop + Disabled: true + -> Append + -> Seq Scan on pt1a pt1_1 + -> Seq Scan on pt1b pt1_2 + -> Seq Scan on pt1c pt1_3 + -> Append + -> Index Scan using ptmismatcha_pkey on ptmismatcha ptmismatch_1 + Index Cond: (id = pt1.id) + -> Index Scan using ptmismatchb_pkey on ptmismatchb ptmismatch_2 + Index Cond: (id = pt1.id) + Supplied Plan Advice: + PARTITIONWISE((pt1 ptmismatch)) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(pt1 ptmismatch) + NESTED_LOOP_PLAIN(ptmismatch) + SEQ_SCAN(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c) + INDEX_SCAN(ptmismatch/public.ptmismatcha public.ptmismatcha_pkey + ptmismatch/public.ptmismatchb public.ptmismatchb_pkey) + PARTITIONWISE(pt1 ptmismatch) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c + ptmismatch/public.ptmismatcha ptmismatch/public.ptmismatchb) +(22 rows) + +COMMIT; +-- Force join order for a particular branch of the partitionwise join with +-- and without mentioning the schema name. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'JOIN_ORDER(pt3/public.pt3a pt2/public.pt2a pt1/public.pt1a)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Append + -> Nested Loop + -> Hash Join + Hash Cond: (pt3_1.id = pt2_1.id) + -> Seq Scan on pt3a pt3_1 + Filter: (val3 = 1) + -> Hash + -> Seq Scan on pt2a pt2_1 + Filter: (val2 = 1) + -> Index Scan using pt1a_pkey on pt1a pt1_1 + Index Cond: (id = pt2_1.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_2.id = pt3_2.id) + -> Seq Scan on pt2b pt2_2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3b pt3_2 + Filter: (val3 = 1) + -> Index Scan using pt1b_pkey on pt1b pt1_2 + Index Cond: (id = pt2_2.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_3.id = pt3_3.id) + -> Seq Scan on pt2c pt2_3 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3c pt3_3 + Filter: (val3 = 1) + -> Index Scan using pt1c_pkey on pt1c pt1_3 + Index Cond: (id = pt2_3.id) + Filter: (val1 = 1) + Supplied Plan Advice: + JOIN_ORDER(pt3/public.pt3a pt2/public.pt2a pt1/public.pt1a) /* matched */ + Generated Plan Advice: + JOIN_ORDER(pt3/public.pt3a pt2/public.pt2a pt1/public.pt1a) + JOIN_ORDER(pt2/public.pt2b pt3/public.pt3b pt1/public.pt1b) + JOIN_ORDER(pt2/public.pt2c pt3/public.pt3c pt1/public.pt1c) + NESTED_LOOP_PLAIN(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c) + HASH_JOIN(pt2/public.pt2a pt3/public.pt3b pt3/public.pt3c) + SEQ_SCAN(pt3/public.pt3a pt2/public.pt2a pt2/public.pt2b pt3/public.pt3b + pt2/public.pt2c pt3/public.pt3c) + INDEX_SCAN(pt1/public.pt1a public.pt1a_pkey pt1/public.pt1b public.pt1b_pkey + pt1/public.pt1c public.pt1c_pkey) + PARTITIONWISE((pt1 pt2 pt3)) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c pt2/public.pt2a + pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) +(49 rows) + +SET LOCAL pg_plan_advice.advice = 'JOIN_ORDER(pt3/pt3a pt2/pt2a pt1/pt1a)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Append + -> Nested Loop + -> Hash Join + Hash Cond: (pt3_1.id = pt2_1.id) + -> Seq Scan on pt3a pt3_1 + Filter: (val3 = 1) + -> Hash + -> Seq Scan on pt2a pt2_1 + Filter: (val2 = 1) + -> Index Scan using pt1a_pkey on pt1a pt1_1 + Index Cond: (id = pt2_1.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_2.id = pt3_2.id) + -> Seq Scan on pt2b pt2_2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3b pt3_2 + Filter: (val3 = 1) + -> Index Scan using pt1b_pkey on pt1b pt1_2 + Index Cond: (id = pt2_2.id) + Filter: (val1 = 1) + -> Nested Loop + -> Hash Join + Hash Cond: (pt2_3.id = pt3_3.id) + -> Seq Scan on pt2c pt2_3 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on pt3c pt3_3 + Filter: (val3 = 1) + -> Index Scan using pt1c_pkey on pt1c pt1_3 + Index Cond: (id = pt2_3.id) + Filter: (val1 = 1) + Supplied Plan Advice: + JOIN_ORDER(pt3/pt3a pt2/pt2a pt1/pt1a) /* matched */ + Generated Plan Advice: + JOIN_ORDER(pt3/public.pt3a pt2/public.pt2a pt1/public.pt1a) + JOIN_ORDER(pt2/public.pt2b pt3/public.pt3b pt1/public.pt1b) + JOIN_ORDER(pt2/public.pt2c pt3/public.pt3c pt1/public.pt1c) + NESTED_LOOP_PLAIN(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c) + HASH_JOIN(pt2/public.pt2a pt3/public.pt3b pt3/public.pt3c) + SEQ_SCAN(pt3/public.pt3a pt2/public.pt2a pt2/public.pt2b pt3/public.pt3b + pt2/public.pt2c pt3/public.pt3c) + INDEX_SCAN(pt1/public.pt1a public.pt1a_pkey pt1/public.pt1b public.pt1b_pkey + pt1/public.pt1c public.pt1c_pkey) + PARTITIONWISE((pt1 pt2 pt3)) + NO_GATHER(pt1/public.pt1a pt1/public.pt1b pt1/public.pt1c pt2/public.pt2a + pt2/public.pt2b pt2/public.pt2c pt3/public.pt3a pt3/public.pt3b pt3/public.pt3c) +(49 rows) + +COMMIT; diff --git a/contrib/pg_plan_advice/expected/prepared.out b/contrib/pg_plan_advice/expected/prepared.out new file mode 100644 index 0000000000000..bcab28f03bec7 --- /dev/null +++ b/contrib/pg_plan_advice/expected/prepared.out @@ -0,0 +1,69 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +CREATE TABLE ptab (id integer, val text) WITH (autovacuum_enabled = false); +SET pg_plan_advice.always_store_advice_details = false; +-- Not prepared, so advice should be generated. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM ptab; + QUERY PLAN +------------------------ + Seq Scan on ptab + Generated Plan Advice: + SEQ_SCAN(ptab) + NO_GATHER(ptab) +(4 rows) + +-- Prepared, so advice should not be generated. +PREPARE pt1 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF, PLAN_ADVICE) EXECUTE pt1; + QUERY PLAN +------------------ + Seq Scan on ptab +(1 row) + +SET pg_plan_advice.always_store_advice_details = true; +-- Prepared, but always_store_advice_details = true, so should show advice. +PREPARE pt2 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF, PLAN_ADVICE) EXECUTE pt2; + QUERY PLAN +------------------------ + Seq Scan on ptab + Generated Plan Advice: + SEQ_SCAN(ptab) + NO_GATHER(ptab) +(4 rows) + +-- Not prepared, so feedback should be generated. +SET pg_plan_advice.always_store_advice_details = false; +SET pg_plan_advice.advice = 'SEQ_SCAN(ptab)'; +EXPLAIN (COSTS OFF) +SELECT * FROM ptab; + QUERY PLAN +-------------------------------- + Seq Scan on ptab + Supplied Plan Advice: + SEQ_SCAN(ptab) /* matched */ +(3 rows) + +-- Prepared, so feedback should not be generated. +PREPARE pt3 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF) EXECUTE pt3; + QUERY PLAN +------------------ + Seq Scan on ptab +(1 row) + +SET pg_plan_advice.always_store_advice_details = true; +-- Prepared, but always_store_advice_details = true, so should show feedback. +PREPARE pt4 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF, PLAN_ADVICE) EXECUTE pt4; + QUERY PLAN +-------------------------------- + Seq Scan on ptab + Supplied Plan Advice: + SEQ_SCAN(ptab) /* matched */ + Generated Plan Advice: + SEQ_SCAN(ptab) + NO_GATHER(ptab) +(6 rows) + diff --git a/contrib/pg_plan_advice/expected/scan.out b/contrib/pg_plan_advice/expected/scan.out new file mode 100644 index 0000000000000..f4036e4cbdd05 --- /dev/null +++ b/contrib/pg_plan_advice/expected/scan.out @@ -0,0 +1,810 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +SET seq_page_cost = 0.1; +SET random_page_cost = 0.1; +SET cpu_tuple_cost = 0; +SET cpu_index_tuple_cost = 0; +CREATE TABLE scan_table (a int primary key, b text) + WITH (autovacuum_enabled = false); +INSERT INTO scan_table + SELECT g, 'some text ' || g FROM generate_series(1, 100000) g; +CREATE INDEX scan_table_b ON scan_table USING brin (b); +VACUUM ANALYZE scan_table; +-- Sequential scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + QUERY PLAN +------------------------- + Seq Scan on scan_table + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(4 rows) + +-- Index scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(5 rows) + +-- Index-only scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------ + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(5 rows) + +-- Bitmap heap scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE b > 'some text 8'; + QUERY PLAN +----------------------------------------------- + Bitmap Heap Scan on scan_table + Recheck Cond: (b > 'some text 8'::text) + -> Bitmap Index Scan on scan_table_b + Index Cond: (b > 'some text 8'::text) + Generated Plan Advice: + BITMAP_HEAP_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +-- TID scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)'; + QUERY PLAN +----------------------------------- + Tid Scan on scan_table + TID Cond: (ctid = '(0,1)'::tid) + Generated Plan Advice: + TID_SCAN(scan_table) + NO_GATHER(scan_table) +(5 rows) + +-- TID range scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE ctid > '(1,1)' AND ctid < '(2,1)'; + QUERY PLAN +--------------------------------------------------------------- + Tid Range Scan on scan_table + TID Cond: ((ctid > '(1,1)'::tid) AND (ctid < '(2,1)'::tid)) + Generated Plan Advice: + TID_SCAN(scan_table) + NO_GATHER(scan_table) +(5 rows) + +-- Try forcing each of our test queries to use the scan type they +-- wanted to use anyway. This should succeed. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + QUERY PLAN +-------------------------------------- + Seq Scan on scan_table + Supplied Plan Advice: + SEQ_SCAN(scan_table) /* matched */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(6 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +-------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------------- + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_ONLY_SCAN(scan_table scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE b > 'some text 8'; + QUERY PLAN +----------------------------------------------- + Bitmap Heap Scan on scan_table + Recheck Cond: (b > 'some text 8'::text) + -> Bitmap Index Scan on scan_table_b + Index Cond: (b > 'some text 8'::text) + Supplied Plan Advice: + BITMAP_HEAP_SCAN(scan_table) /* matched */ + Generated Plan Advice: + BITMAP_HEAP_SCAN(scan_table) + NO_GATHER(scan_table) +(9 rows) + +SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)'; + QUERY PLAN +-------------------------------------- + Tid Scan on scan_table + TID Cond: (ctid = '(0,1)'::tid) + Supplied Plan Advice: + TID_SCAN(scan_table) /* matched */ + Generated Plan Advice: + TID_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE ctid > '(1,1)' AND ctid < '(2,1)'; + QUERY PLAN +--------------------------------------------------------------- + Tid Range Scan on scan_table + TID Cond: ((ctid > '(1,1)'::tid) AND (ctid < '(2,1)'::tid)) + Supplied Plan Advice: + TID_SCAN(scan_table) /* matched */ + Generated Plan Advice: + TID_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +COMMIT; +-- Try to force a full scan of the table to use some other scan type. All +-- of these will fail. An index scan or bitmap heap scan could potentially +-- generate the correct answer, but the planner does not even consider these +-- possibilities due to the lack of a WHERE clause. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + QUERY PLAN +---------------------------------------------------------------- + Seq Scan on scan_table + Disabled: true + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_pkey) /* matched, failed */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + QUERY PLAN +--------------------------------------------------------------------- + Seq Scan on scan_table + Disabled: true + Supplied Plan Advice: + INDEX_ONLY_SCAN(scan_table scan_table_pkey) /* matched, failed */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + QUERY PLAN +------------------------------------------------------ + Seq Scan on scan_table + Disabled: true + Supplied Plan Advice: + BITMAP_HEAP_SCAN(scan_table) /* matched, failed */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + QUERY PLAN +---------------------------------------------- + Seq Scan on scan_table + Disabled: true + Supplied Plan Advice: + TID_SCAN(scan_table) /* matched, failed */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +COMMIT; +-- Try again to force index use. This should now succeed for the INDEX_SCAN +-- and BITMAP_HEAP_SCAN, but the INDEX_ONLY_SCAN can't be forced because the +-- query fetches columns not included in the index. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0; + QUERY PLAN +-------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a > 0) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0; + QUERY PLAN +--------------------------------------------------------------------- + Seq Scan on scan_table + Disabled: true + Filter: (a > 0) + Supplied Plan Advice: + INDEX_ONLY_SCAN(scan_table scan_table_pkey) /* matched, failed */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(8 rows) + +SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0; + QUERY PLAN +---------------------------------------------- + Bitmap Heap Scan on scan_table + Recheck Cond: (a > 0) + -> Bitmap Index Scan on scan_table_pkey + Index Cond: (a > 0) + Supplied Plan Advice: + BITMAP_HEAP_SCAN(scan_table) /* matched */ + Generated Plan Advice: + BITMAP_HEAP_SCAN(scan_table) + NO_GATHER(scan_table) +(9 rows) + +COMMIT; +-- We can force a primary key lookup to use a sequential scan, but we +-- can't force it to use an index-only scan (due to the column list) +-- or a TID scan (due to the absence of a TID qual). If we apply DO_NOT_SCAN +-- here, we should get a valid plan anyway, but with the scan disabled. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +-------------------------------------- + Seq Scan on scan_table + Filter: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(scan_table) /* matched */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +--------------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Disabled: true + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_ONLY_SCAN(scan_table scan_table_pkey) /* matched, failed */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Disabled: true + Index Cond: (a = 1) + Supplied Plan Advice: + TID_SCAN(scan_table) /* matched, failed */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Disabled: true + Index Cond: (a = 1) + Supplied Plan Advice: + DO_NOT_SCAN(scan_table) /* matched, failed */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +COMMIT; +-- We can forcibly downgrade an index-only scan to an index scan, but we can't +-- force the use of an index that the planner thinks is inapplicable. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +-------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table public.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +--------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_b)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Disabled: true + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_b) /* matched, failed */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +COMMIT; +-- We can force the use of a sequential scan in place of a bitmap heap scan, +-- but a plain index scan on a BRIN index is not possible. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE b > 'some text 8'; + QUERY PLAN +-------------------------------------- + Seq Scan on scan_table + Filter: (b > 'some text 8'::text) + Supplied Plan Advice: + SEQ_SCAN(scan_table) /* matched */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_b)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Disabled: true + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_b) /* matched, failed */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +COMMIT; +-- We can force the use of a sequential scan rather than a TID scan or +-- TID range scan. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)'; + QUERY PLAN +-------------------------------------- + Seq Scan on scan_table + Filter: (ctid = '(0,1)'::tid) + Supplied Plan Advice: + SEQ_SCAN(scan_table) /* matched */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE ctid > '(1,1)' AND ctid < '(2,1)'; + QUERY PLAN +------------------------------------------------------------- + Seq Scan on scan_table + Filter: ((ctid > '(1,1)'::tid) AND (ctid < '(2,1)'::tid)) + Supplied Plan Advice: + SEQ_SCAN(scan_table) /* matched */ + Generated Plan Advice: + SEQ_SCAN(scan_table) + NO_GATHER(scan_table) +(7 rows) + +COMMIT; +-- Test more complex scenarios with index scans. +BEGIN; +-- Should still work if we mention the schema. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table public.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +--------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +-- But not if we mention the wrong schema. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table cilbup.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table cilbup.scan_table_pkey) /* matched, inapplicable, failed */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +-- It's OK to repeat the same advice. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +-------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_pkey) /* matched */ + INDEX_SCAN(scan_table scan_table_pkey) /* matched */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +-- But it doesn't work if the index target is even notionally different. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey scan_table public.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + QUERY PLAN +---------------------------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table scan_table_pkey) /* matched, conflicting */ + INDEX_SCAN(scan_table public.scan_table_pkey) /* matched, conflicting */ + Generated Plan Advice: + INDEX_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(8 rows) + +COMMIT; +-- Test assorted incorrect advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(nothing)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------ + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(nothing) /* not matched */ + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(nothing whatsoever)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------ + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(nothing whatsoever) /* not matched */ + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table bogus)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +-------------------------------------------------------------------- + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_SCAN(scan_table bogus) /* matched, inapplicable, failed */ + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(nothing whatsoever)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +--------------------------------------------------------- + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_ONLY_SCAN(nothing whatsoever) /* not matched */ + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table bogus)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + QUERY PLAN +------------------------------------------------------------------------- + Index Only Scan using scan_table_pkey on scan_table + Index Cond: (a = 1) + Supplied Plan Advice: + INDEX_ONLY_SCAN(scan_table bogus) /* matched, inapplicable, failed */ + Generated Plan Advice: + INDEX_ONLY_SCAN(scan_table public.scan_table_pkey) + NO_GATHER(scan_table) +(7 rows) + +COMMIT; +-- Test our ability to refer to multiple instances of the same alias. +BEGIN; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; + QUERY PLAN +------------------------------------------------------------------- + Nested Loop Left Join + -> Nested Loop Left Join + -> Function Scan on generate_series g + -> Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = g.g) + -> Index Scan using scan_table_pkey on scan_table s_1 + Index Cond: (a = g.g) + Generated Plan Advice: + JOIN_ORDER(g s s#2) + NESTED_LOOP_PLAIN(s s#2) + INDEX_SCAN(s public.scan_table_pkey s#2 public.scan_table_pkey) + NO_GATHER(g s s#2) +(12 rows) + +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; + QUERY PLAN +---------------------------------------------------------- + Nested Loop Left Join + -> Hash Left Join + Hash Cond: (g.g = s.a) + -> Function Scan on generate_series g + -> Hash + -> Seq Scan on scan_table s + -> Index Scan using scan_table_pkey on scan_table s_1 + Index Cond: (a = g.g) + Supplied Plan Advice: + SEQ_SCAN(s) /* matched */ + Generated Plan Advice: + JOIN_ORDER(g s s#2) + NESTED_LOOP_PLAIN(s#2) + HASH_JOIN(s) + SEQ_SCAN(s) + INDEX_SCAN(s#2 public.scan_table_pkey) + NO_GATHER(g s s#2) +(17 rows) + +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s#2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; + QUERY PLAN +-------------------------------------------------------------- + Hash Left Join + Hash Cond: (g.g = s_1.a) + -> Nested Loop Left Join + -> Function Scan on generate_series g + -> Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = g.g) + -> Hash + -> Seq Scan on scan_table s_1 + Supplied Plan Advice: + SEQ_SCAN(s#2) /* matched */ + Generated Plan Advice: + JOIN_ORDER(g s s#2) + NESTED_LOOP_PLAIN(s) + HASH_JOIN(s#2) + SEQ_SCAN(s#2) + INDEX_SCAN(s public.scan_table_pkey) + NO_GATHER(g s s#2) +(17 rows) + +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s) SEQ_SCAN(s#2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; + QUERY PLAN +------------------------------------------------ + Hash Left Join + Hash Cond: (g.g = s_1.a) + -> Hash Left Join + Hash Cond: (g.g = s.a) + -> Function Scan on generate_series g + -> Hash + -> Seq Scan on scan_table s + -> Hash + -> Seq Scan on scan_table s_1 + Supplied Plan Advice: + SEQ_SCAN(s) /* matched */ + SEQ_SCAN(s#2) /* matched */ + Generated Plan Advice: + JOIN_ORDER(g s s#2) + HASH_JOIN(s s#2) + SEQ_SCAN(s s#2) + NO_GATHER(g s s#2) +(17 rows) + +COMMIT; +-- Test our ability to refer to scans within a subquery. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; + QUERY PLAN +-------------------------------------------------- + Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = 1) + Generated Plan Advice: + INDEX_SCAN(s@x public.scan_table_pkey) + NO_GATHER(x s@x) +(5 rows) + +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); + QUERY PLAN +--------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = 1) + Generated Plan Advice: + INDEX_SCAN(s@unnamed_subquery public.scan_table_pkey) + NO_GATHER(unnamed_subquery s@unnamed_subquery) +(5 rows) + +BEGIN; +-- Should not match. +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; + QUERY PLAN +-------------------------------------------------- + Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(s) /* not matched */ + Generated Plan Advice: + INDEX_SCAN(s@x public.scan_table_pkey) + NO_GATHER(x s@x) +(7 rows) + +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); + QUERY PLAN +--------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(s) /* not matched */ + Generated Plan Advice: + INDEX_SCAN(s@unnamed_subquery public.scan_table_pkey) + NO_GATHER(unnamed_subquery s@unnamed_subquery) +(7 rows) + +-- Should match first query only. +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s@x)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; + QUERY PLAN +------------------------------- + Seq Scan on scan_table s + Filter: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(s@x) /* matched */ + Generated Plan Advice: + SEQ_SCAN(s@x) + NO_GATHER(x s@x) +(7 rows) + +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); + QUERY PLAN +--------------------------------------------------------- + Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(s@x) /* not matched */ + Generated Plan Advice: + INDEX_SCAN(s@unnamed_subquery public.scan_table_pkey) + NO_GATHER(unnamed_subquery s@unnamed_subquery) +(7 rows) + +-- Should match second query only. +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s@unnamed_subquery)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; + QUERY PLAN +-------------------------------------------------- + Index Scan using scan_table_pkey on scan_table s + Index Cond: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(s@unnamed_subquery) /* not matched */ + Generated Plan Advice: + INDEX_SCAN(s@x public.scan_table_pkey) + NO_GATHER(x s@x) +(7 rows) + +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); + QUERY PLAN +-------------------------------------------------- + Seq Scan on scan_table s + Filter: (a = 1) + Supplied Plan Advice: + SEQ_SCAN(s@unnamed_subquery) /* matched */ + Generated Plan Advice: + SEQ_SCAN(s@unnamed_subquery) + NO_GATHER(unnamed_subquery s@unnamed_subquery) +(7 rows) + +COMMIT; +-- Test a non-repeatable tablesample method with a scan-level Materialize. +CREATE EXTENSION tsm_system_time; +CREATE TABLE scan_tsm (i int); +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT 1 FROM (SELECT i FROM scan_tsm TABLESAMPLE system_time (1000)), + LATERAL (SELECT i LIMIT 1); + QUERY PLAN +---------------------------------------------------------------------- + Nested Loop + -> Materialize + -> Sample Scan on scan_tsm + Sampling: system_time ('1000'::double precision) + -> Limit + -> Result + Generated Plan Advice: + JOIN_ORDER(scan_tsm unnamed_subquery#2) + NESTED_LOOP_PLAIN(unnamed_subquery#2) + NO_GATHER(unnamed_subquery#2 scan_tsm "*RESULT*"@unnamed_subquery) +(10 rows) + +-- Same, but with the scan-level Materialize on the inner side of a join. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT 1 FROM (SELECT 1 AS x LIMIT 1), + LATERAL (SELECT x FROM scan_tsm TABLESAMPLE system_time (1000)); + QUERY PLAN +-------------------------------------------------------------------- + Nested Loop + -> Limit + -> Result + -> Materialize + -> Sample Scan on scan_tsm + Sampling: system_time ('1000'::double precision) + Generated Plan Advice: + JOIN_ORDER(unnamed_subquery scan_tsm) + NESTED_LOOP_PLAIN(scan_tsm) + NO_GATHER(unnamed_subquery scan_tsm "*RESULT*"@unnamed_subquery) +(10 rows) + diff --git a/contrib/pg_plan_advice/expected/semijoin.out b/contrib/pg_plan_advice/expected/semijoin.out new file mode 100644 index 0000000000000..db6b069ec8eeb --- /dev/null +++ b/contrib/pg_plan_advice/expected/semijoin.out @@ -0,0 +1,426 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +CREATE TABLE sj_wide ( + id integer primary key, + val1 integer, + padding text storage plain +) WITH (autovacuum_enabled = false); +INSERT INTO sj_wide + SELECT g, g%10+1, repeat(' ', 300) FROM generate_series(1, 1000) g; +CREATE INDEX ON sj_wide (val1); +VACUUM ANALYZE sj_wide; +CREATE TABLE sj_narrow ( + id integer primary key, + val1 integer +) WITH (autovacuum_enabled = false); +INSERT INTO sj_narrow + SELECT g, g%10+1 FROM generate_series(1, 1000) g; +CREATE INDEX ON sj_narrow (val1); +VACUUM ANALYZE sj_narrow; +-- We expect this to make the VALUES list unique and use index lookups to +-- find the rows in sj_wide, so as to avoid a full scan of sj_wide. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_wide + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + QUERY PLAN +----------------------------------------------------------- + Nested Loop + -> HashAggregate + Group Key: "*VALUES*".column1, "*VALUES*".column2 + -> Values Scan on "*VALUES*" + -> Index Scan using sj_wide_pkey on sj_wide + Index Cond: (id = "*VALUES*".column1) + Filter: (val1 = "*VALUES*".column2) + Generated Plan Advice: + JOIN_ORDER("*VALUES*" sj_wide) + NESTED_LOOP_PLAIN(sj_wide) + INDEX_SCAN(sj_wide public.sj_wide_pkey) + SEMIJOIN_UNIQUE("*VALUES*") + NO_GATHER(sj_wide "*VALUES*") +(13 rows) + +-- If we ask for a unique semijoin, we should get the same plan as with +-- no advice. If we ask for a non-unique semijoin, we should see a Semi +-- Join operation in the plan tree. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_wide + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + QUERY PLAN +----------------------------------------------------------- + Nested Loop + -> HashAggregate + Group Key: "*VALUES*".column1, "*VALUES*".column2 + -> Values Scan on "*VALUES*" + -> Index Scan using sj_wide_pkey on sj_wide + Index Cond: (id = "*VALUES*".column1) + Filter: (val1 = "*VALUES*".column2) + Supplied Plan Advice: + SEMIJOIN_UNIQUE("*VALUES*") /* matched */ + Generated Plan Advice: + JOIN_ORDER("*VALUES*" sj_wide) + NESTED_LOOP_PLAIN(sj_wide) + INDEX_SCAN(sj_wide public.sj_wide_pkey) + SEMIJOIN_UNIQUE("*VALUES*") + NO_GATHER(sj_wide "*VALUES*") +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_wide + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + QUERY PLAN +------------------------------------------------------------------------------------------ + Hash Semi Join + Hash Cond: ((sj_wide.id = "*VALUES*".column1) AND (sj_wide.val1 = "*VALUES*".column2)) + -> Seq Scan on sj_wide + -> Hash + -> Values Scan on "*VALUES*" + Supplied Plan Advice: + SEMIJOIN_NON_UNIQUE("*VALUES*") /* matched */ + Generated Plan Advice: + JOIN_ORDER(sj_wide "*VALUES*") + HASH_JOIN("*VALUES*") + SEQ_SCAN(sj_wide) + SEMIJOIN_NON_UNIQUE("*VALUES*") + NO_GATHER(sj_wide "*VALUES*") +(13 rows) + +COMMIT; +-- Because this table is narrower than the previous one, a sequential scan +-- is less expensive, and we choose a straightforward Semi Join plan by +-- default. (Note that this is also very sensitive to the length of the IN +-- list, which affects how many index lookups the alternative plan will need.) +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_narrow + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + QUERY PLAN +---------------------------------------------------------------------------------------------- + Hash Semi Join + Hash Cond: ((sj_narrow.id = "*VALUES*".column1) AND (sj_narrow.val1 = "*VALUES*".column2)) + -> Seq Scan on sj_narrow + -> Hash + -> Values Scan on "*VALUES*" + Generated Plan Advice: + JOIN_ORDER(sj_narrow "*VALUES*") + HASH_JOIN("*VALUES*") + SEQ_SCAN(sj_narrow) + SEMIJOIN_NON_UNIQUE("*VALUES*") + NO_GATHER(sj_narrow "*VALUES*") +(11 rows) + +-- Here, we expect advising a unique semijoin to swith to the same plan that +-- we got with sj_wide, and advising a non-unique semijoin should not change +-- the plan. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_narrow + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + QUERY PLAN +---------------------------------------------------------------------------------------------- + Hash Join + Hash Cond: ((sj_narrow.id = "*VALUES*".column1) AND (sj_narrow.val1 = "*VALUES*".column2)) + -> Seq Scan on sj_narrow + -> Hash + -> HashAggregate + Group Key: "*VALUES*".column1, "*VALUES*".column2 + -> Values Scan on "*VALUES*" + Supplied Plan Advice: + SEMIJOIN_UNIQUE("*VALUES*") /* matched */ + Generated Plan Advice: + JOIN_ORDER(sj_narrow "*VALUES*") + HASH_JOIN("*VALUES*") + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE("*VALUES*") + NO_GATHER(sj_narrow "*VALUES*") +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_narrow + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + QUERY PLAN +---------------------------------------------------------------------------------------------- + Hash Semi Join + Hash Cond: ((sj_narrow.id = "*VALUES*".column1) AND (sj_narrow.val1 = "*VALUES*".column2)) + -> Seq Scan on sj_narrow + -> Hash + -> Values Scan on "*VALUES*" + Supplied Plan Advice: + SEMIJOIN_NON_UNIQUE("*VALUES*") /* matched */ + Generated Plan Advice: + JOIN_ORDER(sj_narrow "*VALUES*") + HASH_JOIN("*VALUES*") + SEQ_SCAN(sj_narrow) + SEMIJOIN_NON_UNIQUE("*VALUES*") + NO_GATHER(sj_narrow "*VALUES*") +(13 rows) + +COMMIT; +-- In the above example, we made the outer side of the join unique, but here, +-- we should make the inner side unique. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (g.g = sj_narrow.val1) + -> Function Scan on generate_series g + -> Hash + -> HashAggregate + Group Key: sj_narrow.val1 + -> Seq Scan on sj_narrow + Generated Plan Advice: + JOIN_ORDER(g sj_narrow) + HASH_JOIN(sj_narrow) + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(13 rows) + +-- We should be able to force a plan with or without the make-unique strategy, +-- with either side as the driving table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +-------------------------------------------- + Hash Join + Hash Cond: (g.g = sj_narrow.val1) + -> Function Scan on generate_series g + -> Hash + -> HashAggregate + Group Key: sj_narrow.val1 + -> Seq Scan on sj_narrow + Supplied Plan Advice: + SEMIJOIN_UNIQUE(sj_narrow) /* matched */ + Generated Plan Advice: + JOIN_ORDER(g sj_narrow) + HASH_JOIN(sj_narrow) + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +------------------------------------------------ + Hash Semi Join + Hash Cond: (g.g = sj_narrow.val1) + -> Function Scan on generate_series g + -> Hash + -> Seq Scan on sj_narrow + Supplied Plan Advice: + SEMIJOIN_NON_UNIQUE(sj_narrow) /* matched */ + Generated Plan Advice: + JOIN_ORDER(g sj_narrow) + HASH_JOIN(sj_narrow) + SEQ_SCAN(sj_narrow) + SEMIJOIN_NON_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(13 rows) + +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(sj_narrow) join_order(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +------------------------------------------------ + Hash Join + Hash Cond: (sj_narrow.val1 = g.g) + -> HashAggregate + Group Key: sj_narrow.val1 + -> Seq Scan on sj_narrow + -> Hash + -> Function Scan on generate_series g + Supplied Plan Advice: + SEMIJOIN_UNIQUE(sj_narrow) /* matched */ + JOIN_ORDER(sj_narrow) /* matched */ + Generated Plan Advice: + JOIN_ORDER(sj_narrow g) + HASH_JOIN(g) + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(16 rows) + +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique(sj_narrow) join_order(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +------------------------------------------------ + Hash Right Semi Join + Hash Cond: (sj_narrow.val1 = g.g) + -> Seq Scan on sj_narrow + -> Hash + -> Function Scan on generate_series g + Supplied Plan Advice: + SEMIJOIN_NON_UNIQUE(sj_narrow) /* matched */ + JOIN_ORDER(sj_narrow) /* matched */ + Generated Plan Advice: + JOIN_ORDER(sj_narrow g) + HASH_JOIN(g) + SEQ_SCAN(sj_narrow) + SEMIJOIN_NON_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(14 rows) + +COMMIT; +-- However, mentioning the wrong side of the join should result in an advice +-- failure. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(g)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +-------------------------------------------- + Nested Loop + Disabled: true + Join Filter: (g.g = sj_narrow.val1) + -> HashAggregate + Group Key: sj_narrow.val1 + -> Seq Scan on sj_narrow + -> Function Scan on generate_series g + Supplied Plan Advice: + SEMIJOIN_UNIQUE(g) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(sj_narrow g) + NESTED_LOOP_PLAIN(g) + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(15 rows) + +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique(g)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +------------------------------------------------ + Nested Loop + Disabled: true + Join Filter: (g.g = sj_narrow.val1) + -> HashAggregate + Group Key: sj_narrow.val1 + -> Seq Scan on sj_narrow + -> Function Scan on generate_series g + Supplied Plan Advice: + SEMIJOIN_NON_UNIQUE(g) /* matched, failed */ + Generated Plan Advice: + JOIN_ORDER(sj_narrow g) + NESTED_LOOP_PLAIN(g) + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(15 rows) + +COMMIT; +-- Test conflicting advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(sj_narrow) semijoin_non_unique(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + QUERY PLAN +--------------------------------------------------------------------- + Hash Join + Hash Cond: (g.g = sj_narrow.val1) + -> Function Scan on generate_series g + -> Hash + -> HashAggregate + Group Key: sj_narrow.val1 + -> Seq Scan on sj_narrow + Supplied Plan Advice: + SEMIJOIN_UNIQUE(sj_narrow) /* matched, conflicting */ + SEMIJOIN_NON_UNIQUE(sj_narrow) /* matched, conflicting, failed */ + Generated Plan Advice: + JOIN_ORDER(g sj_narrow) + HASH_JOIN(sj_narrow) + SEQ_SCAN(sj_narrow) + SEMIJOIN_UNIQUE(sj_narrow) + NO_GATHER(g sj_narrow) +(16 rows) + +COMMIT; +-- Try applying SEMIJOIN_UNIQUE() to a non-semijoin. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(g)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g, sj_narrow s WHERE g = s.val1; + QUERY PLAN +---------------------------------------------------------- + Merge Join + Merge Cond: (s.val1 = g.g) + -> Index Scan using sj_narrow_val1_idx on sj_narrow s + -> Sort + Sort Key: g.g + -> Function Scan on generate_series g + Supplied Plan Advice: + SEMIJOIN_UNIQUE(g) /* matched, inapplicable, failed */ + Generated Plan Advice: + JOIN_ORDER(s g) + MERGE_JOIN_PLAIN(g) + INDEX_SCAN(s public.sj_narrow_val1_idx) + NO_GATHER(g s) +(13 rows) + +COMMIT; +-- Test the case where the subquery containing a semijoin is removed from +-- the query entirely; this test is just to make sure that advice generation +-- does not fail. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM + (SELECT * FROM sj_narrow WHERE id IN (SELECT val1 FROM sj_wide) + LIMIT 1) x, + LATERAL (SELECT 1 WHERE false) y; + QUERY PLAN +-------------------------- + Result + Replaces: Scan on x + One-Time Filter: false + Generated Plan Advice: + NO_GATHER(x) +(5 rows) + +-- Test the case where the planner makes one side of a semijoin unique, and +-- that side contains an outer join; this test is just to make sure that +-- advice generation does not fail. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT 1 FROM generate_series(1, 1000) g WHERE EXISTS + (SELECT 1 FROM + (SELECT 1 FROM (SELECT 1) LEFT JOIN sj_narrow ON true) s, + sj_narrow t2 WHERE g = t2.id); + QUERY PLAN +------------------------------------------------------------------------ + Hash Join + Hash Cond: (t2.id = g.g) + -> Unique + -> Nested Loop + -> Index Only Scan using sj_narrow_pkey on sj_narrow t2 + -> Materialize + -> Nested Loop Left Join + -> Result + -> Seq Scan on sj_narrow + -> Hash + -> Function Scan on generate_series g + Generated Plan Advice: + JOIN_ORDER(t2 ("*RESULT*" sj_narrow) g) + NESTED_LOOP_PLAIN(sj_narrow) + NESTED_LOOP_MATERIALIZE((sj_narrow "*RESULT*")) + HASH_JOIN(g) + SEQ_SCAN(sj_narrow) + INDEX_ONLY_SCAN(t2 public.sj_narrow_pkey) + SEMIJOIN_UNIQUE((t2 sj_narrow "*RESULT*")) + NO_GATHER(g t2 sj_narrow "*RESULT*") +(20 rows) + diff --git a/contrib/pg_plan_advice/expected/syntax.out b/contrib/pg_plan_advice/expected/syntax.out new file mode 100644 index 0000000000000..c3f2cbd6dca84 --- /dev/null +++ b/contrib/pg_plan_advice/expected/syntax.out @@ -0,0 +1,237 @@ +LOAD 'pg_plan_advice'; +-- An empty string is allowed. Empty target lists are allowed for most advice +-- tags, but not for JOIN_ORDER. "Supplied Plan Advice" should be omitted in +-- text format when there is no actual advice, but not in non-text format. +SET pg_plan_advice.advice = ''; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------ + Result +(1 row) + +SET pg_plan_advice.advice = 'SEQ_SCAN()'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------ + Result +(1 row) + +SET pg_plan_advice.advice = 'NESTED_LOOP_PLAIN()'; +EXPLAIN (COSTS OFF, FORMAT JSON) SELECT 1; + QUERY PLAN +-------------------------------- + [ + + { + + "Plan": { + + "Node Type": "Result", + + "Parallel Aware": false,+ + "Async Capable": false, + + "Disabled": false + + }, + + "Supplied Plan Advice": ""+ + } + + ] +(1 row) + +SET pg_plan_advice.advice = 'JOIN_ORDER()'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "JOIN_ORDER()" +DETAIL: Could not parse advice: JOIN_ORDER must have at least one target at or near ")" +-- Test assorted variations in capitalization, whitespace, and which parts of +-- the relation identifier are included. These should all work. +SET pg_plan_advice.advice = 'SEQ_SCAN(x)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +--------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(x) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'seq_scan(x@y)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +----------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(x@y) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'SEQ_scan(x#2)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +----------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(x#2) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'SEQ_SCAN (x/y)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +----------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(x/y) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = ' SEQ_SCAN ( x / y . z ) '; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(x/y.z) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'SEQ_SCAN("x"#2/"y"."z"@"t")'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +----------------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(x#2/y.z@t) /* not matched */ +(3 rows) + +-- Syntax errors. +SET pg_plan_advice.advice = 'SEQUENTIAL_SCAN(x)'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQUENTIAL_SCAN(x)" +DETAIL: Could not parse advice: syntax error at or near "SEQUENTIAL_SCAN" +SET pg_plan_advice.advice = 'SEQ_SCAN'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN" +DETAIL: Could not parse advice: syntax error at end of input +SET pg_plan_advice.advice = 'SEQ_SCAN('; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN(" +DETAIL: Could not parse advice: syntax error at end of input +SET pg_plan_advice.advice = 'SEQ_SCAN("'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN("" +DETAIL: Could not parse advice: unterminated quoted identifier at end of input +SET pg_plan_advice.advice = 'SEQ_SCAN("")'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN("")" +DETAIL: Could not parse advice: zero-length delimited identifier at or near """ +SET pg_plan_advice.advice = 'SEQ_SCAN("a"'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN("a"" +DETAIL: Could not parse advice: syntax error at end of input +SET pg_plan_advice.advice = 'SEQ_SCAN(#'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN(#" +DETAIL: Could not parse advice: syntax error at or near "#" +SET pg_plan_advice.advice = '()'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "()" +DETAIL: Could not parse advice: syntax error at or near "(" +SET pg_plan_advice.advice = '123'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "123" +DETAIL: Could not parse advice: syntax error at or near "123" +-- Tags like SEQ_SCAN and NO_GATHER don't allow sublists at all; other tags, +-- except for JOIN_ORDER, allow at most one level of sublist. Hence, these +-- examples should error out. +SET pg_plan_advice.advice = 'SEQ_SCAN((x))'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "SEQ_SCAN((x))" +DETAIL: Could not parse advice: syntax error at or near "(" +SET pg_plan_advice.advice = 'GATHER(((x)))'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "GATHER(((x)))" +DETAIL: Could not parse advice: syntax error at or near "(" +-- Legal comments. +SET pg_plan_advice.advice = '/**/'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------ + Result +(1 row) + +SET pg_plan_advice.advice = 'HASH_JOIN(_)/***/'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +---------------------------------- + Result + Supplied Plan Advice: + HASH_JOIN(_) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = '/* comment */ HASH_JOIN(/*x*/y)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +---------------------------------- + Result + Supplied Plan Advice: + HASH_JOIN(y) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = '/* comment */ HASH_JOIN(y//*x*/z)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------------------------------ + Result + Supplied Plan Advice: + HASH_JOIN(y/z) /* not matched */ +(3 rows) + +-- Unterminated comments. +SET pg_plan_advice.advice = '/*'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "/*" +DETAIL: Could not parse advice: unterminated comment at end of input +SET pg_plan_advice.advice = 'JOIN_ORDER("fOO") /* oops'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "JOIN_ORDER("fOO") /* oops" +DETAIL: Could not parse advice: unterminated comment at end of input +-- Nested comments are not supported, so the first of these is legal and +-- the second is not. +SET pg_plan_advice.advice = '/*/*/'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------ + Result +(1 row) + +SET pg_plan_advice.advice = '/*/* stuff */*/'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "/*/* stuff */*/" +DETAIL: Could not parse advice: syntax error at or near "*" +-- Foreign join requires multiple relation identifiers. +SET pg_plan_advice.advice = 'FOREIGN_JOIN(a)'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "FOREIGN_JOIN(a)" +DETAIL: Could not parse advice: FOREIGN_JOIN targets must contain more than one relation identifier at or near ")" +SET pg_plan_advice.advice = 'FOREIGN_JOIN((a))'; +ERROR: invalid value for parameter "pg_plan_advice.advice": "FOREIGN_JOIN((a))" +DETAIL: Could not parse advice: FOREIGN_JOIN targets must contain more than one relation identifier at or near ")" +-- Tag keywords used as alias names work fine, because the 'identifier' +-- nonterminal accepts all token types. +SET pg_plan_advice.advice = 'SEQ_SCAN(hash_join)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +----------------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(hash_join) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'SEQ_SCAN(seq_scan)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +---------------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(seq_scan) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'SEQ_SCAN(gather)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +-------------------------------------- + Result + Supplied Plan Advice: + SEQ_SCAN(gather) /* not matched */ +(3 rows) + +SET pg_plan_advice.advice = 'SEQ_SCAN(join_order)'; +EXPLAIN (COSTS OFF) SELECT 1; + QUERY PLAN +------------------------------------------ + Result + Supplied Plan Advice: + SEQ_SCAN(join_order) /* not matched */ +(3 rows) + +-- Tag keywords used as partition names or plan names should also work, +-- since pgpa_identifier_string() can generate these from real partition +-- and subquery names. +SET pg_plan_advice.advice = 'SEQ_SCAN(t/public.hash_join)'; +SET pg_plan_advice.advice = 'SEQ_SCAN(t/hash_join.foo)'; +SET pg_plan_advice.advice = 'SEQ_SCAN(t@hash_join)'; +SET pg_plan_advice.advice = 'SEQ_SCAN(t@seq_scan)'; diff --git a/contrib/pg_plan_advice/meson.build b/contrib/pg_plan_advice/meson.build new file mode 100644 index 0000000000000..f2098947b644b --- /dev/null +++ b/contrib/pg_plan_advice/meson.build @@ -0,0 +1,67 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +pg_plan_advice_sources = files( + 'pg_plan_advice.c', + 'pgpa_ast.c', + 'pgpa_identifier.c', + 'pgpa_join.c', + 'pgpa_output.c', + 'pgpa_planner.c', + 'pgpa_scan.c', + 'pgpa_trove.c', + 'pgpa_walker.c', +) + +pgpa_scanner = custom_target('pgpa_scanner', + input: 'pgpa_scanner.l', + output: 'pgpa_scanner.c', + command: flex_cmd, +) +generated_sources += pgpa_scanner +pg_plan_advice_sources += pgpa_scanner + +pgpa_parser = custom_target('pgpa_parser', + input: 'pgpa_parser.y', + kwargs: bison_kw, +) +generated_sources += pgpa_parser.to_list() +pg_plan_advice_sources += pgpa_parser + +if host_system == 'windows' + pg_plan_advice_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'pg_plan_advice', + '--FILEDESC', 'pg_plan_advice - help the planner get the right plan',]) +endif + +pg_plan_advice_inc = include_directories('.') + +pg_plan_advice = shared_module('pg_plan_advice', + pg_plan_advice_sources, + include_directories: pg_plan_advice_inc, + kwargs: contrib_mod_args, +) +contrib_targets += pg_plan_advice + +install_headers( + 'pg_plan_advice.h', + install_dir: dir_include_server / 'contrib' / 'pg_plan_advice', +) + +tests += { + 'name': 'pg_plan_advice', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'alternatives', + 'gather', + 'join_order', + 'join_strategy', + 'partitionwise', + 'prepared', + 'scan', + 'semijoin', + 'syntax', + ], + }, +} diff --git a/contrib/pg_plan_advice/pg_plan_advice.c b/contrib/pg_plan_advice/pg_plan_advice.c new file mode 100644 index 0000000000000..299b0d02a8612 --- /dev/null +++ b/contrib/pg_plan_advice/pg_plan_advice.c @@ -0,0 +1,457 @@ +/*------------------------------------------------------------------------- + * + * pg_plan_advice.c + * main entrypoints for generating and applying planner advice + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pg_plan_advice.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "pg_plan_advice.h" +#include "pgpa_ast.h" +#include "pgpa_identifier.h" +#include "pgpa_output.h" +#include "pgpa_planner.h" +#include "pgpa_trove.h" +#include "pgpa_walker.h" + +#include "commands/defrem.h" +#include "commands/explain.h" +#include "commands/explain_format.h" +#include "commands/explain_state.h" +#include "funcapi.h" +#include "optimizer/planner.h" +#include "storage/dsm_registry.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +/* GUC variables */ +char *pg_plan_advice_advice = NULL; +bool pg_plan_advice_always_store_advice_details = false; +static bool pg_plan_advice_always_explain_supplied_advice = true; +bool pg_plan_advice_feedback_warnings = false; +bool pg_plan_advice_trace_mask = false; + +/* Saved hook value */ +static explain_per_plan_hook_type prev_explain_per_plan = NULL; + +/* Other file-level globals */ +static int es_extension_id; +static MemoryContext pgpa_memory_context = NULL; +static List *advisor_hook_list = NIL; + +static void pg_plan_advice_explain_option_handler(ExplainState *es, + DefElem *opt, + ParseState *pstate); +static void pg_plan_advice_explain_per_plan_hook(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv); +static bool pg_plan_advice_advice_check_hook(char **newval, void **extra, + GucSource source); +static DefElem *find_defelem_by_defname(List *deflist, char *defname); + +/* + * Initialize this module. + */ +void +_PG_init(void) +{ + DefineCustomStringVariable("pg_plan_advice.advice", + "advice to apply during query planning", + NULL, + &pg_plan_advice_advice, + NULL, + PGC_USERSET, + 0, + pg_plan_advice_advice_check_hook, + NULL, + NULL); + + DefineCustomBoolVariable("pg_plan_advice.always_explain_supplied_advice", + "EXPLAIN output includes supplied advice even without EXPLAIN (PLAN_ADVICE)", + NULL, + &pg_plan_advice_always_explain_supplied_advice, + true, + PGC_USERSET, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("pg_plan_advice.always_store_advice_details", + "Generate advice strings even when seemingly not required", + "Use this option to see generated advice for prepared queries.", + &pg_plan_advice_always_store_advice_details, + false, + PGC_USERSET, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("pg_plan_advice.feedback_warnings", + "Warn when supplied advice does not apply cleanly", + NULL, + &pg_plan_advice_feedback_warnings, + false, + PGC_USERSET, + 0, + NULL, + NULL, + NULL); + + DefineCustomBoolVariable("pg_plan_advice.trace_mask", + "Emit debugging messages showing the computed strategy mask for each relation", + NULL, + &pg_plan_advice_trace_mask, + false, + PGC_USERSET, + 0, + NULL, + NULL, + NULL); + + MarkGUCPrefixReserved("pg_plan_advice"); + + /* Get an ID that we can use to cache data in an ExplainState. */ + es_extension_id = GetExplainExtensionId("pg_plan_advice"); + + /* Register the new EXPLAIN options implemented by this module. */ + RegisterExtensionExplainOption("plan_advice", + pg_plan_advice_explain_option_handler, + GUCCheckBooleanExplainOption); + + /* Install hooks */ + pgpa_planner_install_hooks(); + prev_explain_per_plan = explain_per_plan_hook; + explain_per_plan_hook = pg_plan_advice_explain_per_plan_hook; +} + +/* + * Return a pointer to a memory context where long-lived data managed by this + * module can be stored. + */ +MemoryContext +pg_plan_advice_get_mcxt(void) +{ + if (pgpa_memory_context == NULL) + pgpa_memory_context = AllocSetContextCreate(TopMemoryContext, + "pg_plan_advice", + ALLOCSET_DEFAULT_SIZES); + + return pgpa_memory_context; +} + +/* + * Was the PLAN_ADVICE option specified and not set to false? + */ +bool +pg_plan_advice_should_explain(ExplainState *es) +{ + bool *plan_advice = NULL; + + if (es != NULL) + plan_advice = GetExplainExtensionState(es, es_extension_id); + return plan_advice != NULL && *plan_advice; +} + +/* + * Get the advice that should be used while planning a particular query. + */ +char * +pg_plan_advice_get_supplied_query_advice(PlannerGlobal *glob, + Query *parse, + const char *query_string, + int cursorOptions, + ExplainState *es) +{ + ListCell *lc; + + /* + * If any advisors are loaded, consult them. The first one that produces a + * non-NULL string wins. + */ + foreach(lc, advisor_hook_list) + { + pg_plan_advice_advisor_hook hook = lfirst(lc); + char *advice_string; + + advice_string = (*hook) (glob, parse, query_string, cursorOptions, es); + if (advice_string != NULL) + return advice_string; + } + + /* Otherwise, just use the value of the GUC. */ + return pg_plan_advice_advice; +} + +/* + * Add an advisor, which can supply advice strings to be used during future + * query planning operations. + * + * The advisor should return NULL if it has no advice string to offer for a + * given query. If multiple advisors are added, they will be consulted in the + * order added until one of them returns a non-NULL value. + */ +void +pg_plan_advice_add_advisor(pg_plan_advice_advisor_hook hook) +{ + MemoryContext oldcontext; + + oldcontext = MemoryContextSwitchTo(pg_plan_advice_get_mcxt()); + advisor_hook_list = lappend(advisor_hook_list, hook); + MemoryContextSwitchTo(oldcontext); +} + +/* + * Remove an advisor. + */ +void +pg_plan_advice_remove_advisor(pg_plan_advice_advisor_hook hook) +{ + MemoryContext oldcontext; + + oldcontext = MemoryContextSwitchTo(pg_plan_advice_get_mcxt()); + advisor_hook_list = list_delete_ptr(advisor_hook_list, hook); + MemoryContextSwitchTo(oldcontext); +} + +/* + * Other loadable modules can use this function to trigger advice generation. + * + * Calling this function with activate = true requests that any queries + * planned afterwards should generate plan advice, which will be stored in the + * PlannedStmt. Calling this function with activate = false revokes that + * request. Multiple loadable modules could be using this simultaneously, so + * make sure to only revoke your own requests. + * + * Note that you can't use this function to *suppress* advice generation, + * which can occur for other reasons, such as the use of EXPLAIN (PLAN_ADVICE), + * regardless. It's a way of turning advice generation on, not a way of turning + * it off. + */ +void +pg_plan_advice_request_advice_generation(bool activate) +{ + if (activate) + pgpa_planner_generate_advice++; + else + { + Assert(pgpa_planner_generate_advice > 0); + pgpa_planner_generate_advice--; + } +} + +/* + * Handler for EXPLAIN (PLAN_ADVICE). + */ +static void +pg_plan_advice_explain_option_handler(ExplainState *es, DefElem *opt, + ParseState *pstate) +{ + bool *plan_advice; + + plan_advice = GetExplainExtensionState(es, es_extension_id); + + if (plan_advice == NULL) + { + plan_advice = palloc0_object(bool); + SetExplainExtensionState(es, es_extension_id, plan_advice); + } + + *plan_advice = defGetBoolean(opt); +} + +/* + * Display a string that is likely to consist of multiple lines in EXPLAIN + * output. + */ +static void +pg_plan_advice_explain_text_multiline(ExplainState *es, char *qlabel, + char *value) +{ + char *s; + + /* For non-text formats, it's best not to add any special handling. */ + if (es->format != EXPLAIN_FORMAT_TEXT) + { + ExplainPropertyText(qlabel, value, es); + return; + } + + /* In text format, if there is no data, display nothing. */ + if (*value == '\0') + return; + + /* + * It looks nicest to indent each line of the advice separately, beginning + * on the line below the label. + */ + ExplainIndentText(es); + appendStringInfo(es->str, "%s:\n", qlabel); + es->indent++; + while ((s = strchr(value, '\n')) != NULL) + { + ExplainIndentText(es); + appendBinaryStringInfo(es->str, value, (s - value) + 1); + value = s + 1; + } + + /* Don't interpret a terminal newline as a request for an empty line. */ + if (*value != '\0') + { + ExplainIndentText(es); + appendStringInfo(es->str, "%s\n", value); + } + + es->indent--; +} + +/* + * Add advice feedback to the EXPLAIN output. + */ +static void +pg_plan_advice_explain_feedback(ExplainState *es, List *feedback) +{ + StringInfoData buf; + + initStringInfo(&buf); + foreach_node(DefElem, item, feedback) + { + int flags = defGetInt32(item); + + appendStringInfo(&buf, "%s /* ", item->defname); + pgpa_trove_append_flags(&buf, flags); + appendStringInfoString(&buf, " */\n"); + } + + pg_plan_advice_explain_text_multiline(es, "Supplied Plan Advice", + buf.data); +} + +/* + * Add relevant details, if any, to the EXPLAIN output for a single plan. + */ +static void +pg_plan_advice_explain_per_plan_hook(PlannedStmt *plannedstmt, + IntoClause *into, + ExplainState *es, + const char *queryString, + ParamListInfo params, + QueryEnvironment *queryEnv) +{ + bool should_explain; + DefElem *pgpa_item; + List *pgpa_list; + + if (prev_explain_per_plan) + prev_explain_per_plan(plannedstmt, into, es, queryString, params, + queryEnv); + + /* Should an advice string be part of the EXPLAIN output? */ + should_explain = pg_plan_advice_should_explain(es); + + /* Find any data pgpa_planner_shutdown stashed in the PlannedStmt. */ + pgpa_item = find_defelem_by_defname(plannedstmt->extension_state, + "pg_plan_advice"); + pgpa_list = pgpa_item == NULL ? NULL : (List *) pgpa_item->arg; + + /* + * By default, if there is a record of attempting to apply advice during + * query planning, we always output that information, but the user can set + * pg_plan_advice.always_explain_supplied_advice = false to suppress that + * behavior. If they do, we'll only display it when the PLAN_ADVICE option + * was specified and not set to false. + * + * NB: If we're explaining a query planned beforehand -- i.e. a prepared + * statement -- the application of query advice may not have been + * recorded, and therefore this won't be able to show anything. Use + * pg_plan_advice.always_store_advice_details = true to work around this. + */ + if (pgpa_list != NULL && (pg_plan_advice_always_explain_supplied_advice || + should_explain)) + { + DefElem *feedback; + + feedback = find_defelem_by_defname(pgpa_list, "feedback"); + if (feedback != NULL) + pg_plan_advice_explain_feedback(es, (List *) feedback->arg); + } + + /* + * If the PLAN_ADVICE option was specified -- and not set to FALSE -- show + * generated advice. + */ + if (should_explain) + { + DefElem *advice_string_item; + char *advice_string = NULL; + + advice_string_item = + find_defelem_by_defname(pgpa_list, "advice_string"); + if (advice_string_item != NULL) + { + advice_string = strVal(advice_string_item->arg); + pg_plan_advice_explain_text_multiline(es, "Generated Plan Advice", + advice_string); + } + } +} + +/* + * Check hook for pg_plan_advice.advice + */ +static bool +pg_plan_advice_advice_check_hook(char **newval, void **extra, GucSource source) +{ + MemoryContext oldcontext; + MemoryContext tmpcontext; + char *error; + + if (*newval == NULL) + return true; + + tmpcontext = AllocSetContextCreate(CurrentMemoryContext, + "pg_plan_advice.advice", + ALLOCSET_DEFAULT_SIZES); + oldcontext = MemoryContextSwitchTo(tmpcontext); + + /* + * It would be nice to save the parse tree that we construct here for + * eventual use when planning with this advice, but *extra can only point + * to a single guc_malloc'd chunk, and our parse tree involves an + * arbitrary number of memory allocations. + */ + (void) pgpa_parse(*newval, &error); + + if (error != NULL) + GUC_check_errdetail("Could not parse advice: %s", error); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(tmpcontext); + + return (error == NULL); +} + +/* + * Search a list of DefElem objects for a given defname. + */ +static DefElem * +find_defelem_by_defname(List *deflist, char *defname) +{ + foreach_node(DefElem, item, deflist) + { + if (strcmp(item->defname, defname) == 0) + return item; + } + + return NULL; +} diff --git a/contrib/pg_plan_advice/pg_plan_advice.h b/contrib/pg_plan_advice/pg_plan_advice.h new file mode 100644 index 0000000000000..d78477153508f --- /dev/null +++ b/contrib/pg_plan_advice/pg_plan_advice.h @@ -0,0 +1,74 @@ +/*------------------------------------------------------------------------- + * + * pg_plan_advice.h + * main header file for pg_plan_advice contrib module + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pg_plan_advice.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PLAN_ADVICE_H +#define PG_PLAN_ADVICE_H + +#include "commands/explain_state.h" +#include "nodes/pathnodes.h" + +/* + * Flags used in plan advice feedback. + * + * PGPA_FB_MATCH_PARTIAL means that we found some part of the query that at + * least partially matched the target; e.g. given JOIN_ORDER(a b), this would + * be set if we ever saw any joinrel including either "a" or "b". + * + * PGPA_FB_MATCH_FULL means that we found an exact match for the target; e.g. + * given JOIN_ORDER(a b), this would be set if we saw a joinrel containing + * exactly "a" and "b" and nothing else. + * + * PGPA_FB_INAPPLICABLE means that the advice doesn't properly apply to the + * target; e.g. INDEX_SCAN(foo bar_idx) would be so marked if bar_idx does not + * exist on foo. The fact that this bit has been set does not mean that the + * advice had no effect. + * + * PGPA_FB_CONFLICTING means that a conflict was detected between what this + * advice wants and what some other plan advice wants; e.g. JOIN_ORDER(a b) + * would conflict with HASH_JOIN(a), because the former requires "a" to be the + * outer table while the latter requires it to be the inner table. + * + * PGPA_FB_FAILED means that the resulting plan did not conform to the advice. + */ +#define PGPA_FB_MATCH_PARTIAL 0x0001 +#define PGPA_FB_MATCH_FULL 0x0002 +#define PGPA_FB_INAPPLICABLE 0x0004 +#define PGPA_FB_CONFLICTING 0x0008 +#define PGPA_FB_FAILED 0x0010 + +/* Hook for other plugins to supply advice strings */ +typedef char *(*pg_plan_advice_advisor_hook) (PlannerGlobal *glob, + Query *parse, + const char *query_string, + int cursorOptions, + ExplainState *es); + +/* GUC variables */ +extern char *pg_plan_advice_advice; +extern bool pg_plan_advice_always_store_advice_details; +extern bool pg_plan_advice_feedback_warnings; +extern bool pg_plan_advice_trace_mask; + +/* Function prototypes (for use by pg_plan_advice itself) */ +extern MemoryContext pg_plan_advice_get_mcxt(void); +extern bool pg_plan_advice_should_explain(ExplainState *es); +extern char *pg_plan_advice_get_supplied_query_advice(PlannerGlobal *glob, + Query *parse, + const char *query_string, + int cursorOptions, + ExplainState *es); + +/* Function prototypes (for use by other plugins) */ +extern PGDLLEXPORT void pg_plan_advice_add_advisor(pg_plan_advice_advisor_hook hook); +extern PGDLLEXPORT void pg_plan_advice_remove_advisor(pg_plan_advice_advisor_hook hook); +extern PGDLLEXPORT void pg_plan_advice_request_advice_generation(bool activate); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_ast.c b/contrib/pg_plan_advice/pgpa_ast.c new file mode 100644 index 0000000000000..01db8d24cd03b --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_ast.c @@ -0,0 +1,357 @@ +/*------------------------------------------------------------------------- + * + * pgpa_ast.c + * additional supporting code related to plan advice parsing + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_ast.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "pgpa_ast.h" + +#include "funcapi.h" +#include "utils/array.h" +#include "utils/builtins.h" + +static bool pgpa_identifiers_cover_target(int nrids, pgpa_identifier *rids, + pgpa_advice_target *target, + bool *rids_used); + +/* + * Get a C string that corresponds to the specified advice tag. + */ +char * +pgpa_cstring_advice_tag(pgpa_advice_tag_type advice_tag) +{ + switch (advice_tag) + { + case PGPA_TAG_BITMAP_HEAP_SCAN: + return "BITMAP_HEAP_SCAN"; + case PGPA_TAG_DO_NOT_SCAN: + return "DO_NOT_SCAN"; + case PGPA_TAG_FOREIGN_JOIN: + return "FOREIGN_JOIN"; + case PGPA_TAG_GATHER: + return "GATHER"; + case PGPA_TAG_GATHER_MERGE: + return "GATHER_MERGE"; + case PGPA_TAG_HASH_JOIN: + return "HASH_JOIN"; + case PGPA_TAG_INDEX_ONLY_SCAN: + return "INDEX_ONLY_SCAN"; + case PGPA_TAG_INDEX_SCAN: + return "INDEX_SCAN"; + case PGPA_TAG_JOIN_ORDER: + return "JOIN_ORDER"; + case PGPA_TAG_MERGE_JOIN_MATERIALIZE: + return "MERGE_JOIN_MATERIALIZE"; + case PGPA_TAG_MERGE_JOIN_PLAIN: + return "MERGE_JOIN_PLAIN"; + case PGPA_TAG_NESTED_LOOP_MATERIALIZE: + return "NESTED_LOOP_MATERIALIZE"; + case PGPA_TAG_NESTED_LOOP_MEMOIZE: + return "NESTED_LOOP_MEMOIZE"; + case PGPA_TAG_NESTED_LOOP_PLAIN: + return "NESTED_LOOP_PLAIN"; + case PGPA_TAG_NO_GATHER: + return "NO_GATHER"; + case PGPA_TAG_PARTITIONWISE: + return "PARTITIONWISE"; + case PGPA_TAG_SEMIJOIN_NON_UNIQUE: + return "SEMIJOIN_NON_UNIQUE"; + case PGPA_TAG_SEMIJOIN_UNIQUE: + return "SEMIJOIN_UNIQUE"; + case PGPA_TAG_SEQ_SCAN: + return "SEQ_SCAN"; + case PGPA_TAG_TID_SCAN: + return "TID_SCAN"; + } + + pg_unreachable(); + return NULL; +} + +/* + * Convert an advice tag, formatted as a string that has already been + * downcased as appropriate, to a pgpa_advice_tag_type. + * + * If we succeed, set *fail = false and return the result; if we fail, + * set *fail = true and return an arbitrary value. + */ +pgpa_advice_tag_type +pgpa_parse_advice_tag(const char *tag, bool *fail) +{ + *fail = false; + + switch (tag[0]) + { + case 'b': + if (strcmp(tag, "bitmap_heap_scan") == 0) + return PGPA_TAG_BITMAP_HEAP_SCAN; + break; + case 'd': + if (strcmp(tag, "do_not_scan") == 0) + return PGPA_TAG_DO_NOT_SCAN; + break; + case 'f': + if (strcmp(tag, "foreign_join") == 0) + return PGPA_TAG_FOREIGN_JOIN; + break; + case 'g': + if (strcmp(tag, "gather") == 0) + return PGPA_TAG_GATHER; + if (strcmp(tag, "gather_merge") == 0) + return PGPA_TAG_GATHER_MERGE; + break; + case 'h': + if (strcmp(tag, "hash_join") == 0) + return PGPA_TAG_HASH_JOIN; + break; + case 'i': + if (strcmp(tag, "index_scan") == 0) + return PGPA_TAG_INDEX_SCAN; + if (strcmp(tag, "index_only_scan") == 0) + return PGPA_TAG_INDEX_ONLY_SCAN; + break; + case 'j': + if (strcmp(tag, "join_order") == 0) + return PGPA_TAG_JOIN_ORDER; + break; + case 'm': + if (strcmp(tag, "merge_join_materialize") == 0) + return PGPA_TAG_MERGE_JOIN_MATERIALIZE; + if (strcmp(tag, "merge_join_plain") == 0) + return PGPA_TAG_MERGE_JOIN_PLAIN; + break; + case 'n': + if (strcmp(tag, "nested_loop_materialize") == 0) + return PGPA_TAG_NESTED_LOOP_MATERIALIZE; + if (strcmp(tag, "nested_loop_memoize") == 0) + return PGPA_TAG_NESTED_LOOP_MEMOIZE; + if (strcmp(tag, "nested_loop_plain") == 0) + return PGPA_TAG_NESTED_LOOP_PLAIN; + if (strcmp(tag, "no_gather") == 0) + return PGPA_TAG_NO_GATHER; + break; + case 'p': + if (strcmp(tag, "partitionwise") == 0) + return PGPA_TAG_PARTITIONWISE; + break; + case 's': + if (strcmp(tag, "semijoin_non_unique") == 0) + return PGPA_TAG_SEMIJOIN_NON_UNIQUE; + if (strcmp(tag, "semijoin_unique") == 0) + return PGPA_TAG_SEMIJOIN_UNIQUE; + if (strcmp(tag, "seq_scan") == 0) + return PGPA_TAG_SEQ_SCAN; + break; + case 't': + if (strcmp(tag, "tid_scan") == 0) + return PGPA_TAG_TID_SCAN; + break; + } + + /* didn't work out */ + *fail = true; + + /* return an arbitrary value to unwind the call stack */ + return PGPA_TAG_SEQ_SCAN; +} + +/* + * Format a pgpa_advice_target as a string and append result to a StringInfo. + */ +void +pgpa_format_advice_target(StringInfo str, pgpa_advice_target *target) +{ + if (target->ttype != PGPA_TARGET_IDENTIFIER) + { + bool first = true; + char *delims; + + if (target->ttype == PGPA_TARGET_UNORDERED_LIST) + delims = "{}"; + else + delims = "()"; + + appendStringInfoChar(str, delims[0]); + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + if (first) + first = false; + else + appendStringInfoChar(str, ' '); + pgpa_format_advice_target(str, child_target); + } + appendStringInfoChar(str, delims[1]); + } + else + { + const char *rt_identifier; + + rt_identifier = pgpa_identifier_string(&target->rid); + appendStringInfoString(str, rt_identifier); + } +} + +/* + * Format a pgpa_index_target as a string and append result to a StringInfo. + */ +void +pgpa_format_index_target(StringInfo str, pgpa_index_target *itarget) +{ + if (itarget->indnamespace != NULL) + appendStringInfo(str, "%s.", + quote_identifier(itarget->indnamespace)); + appendStringInfoString(str, quote_identifier(itarget->indname)); +} + +/* + * Determine whether two pgpa_index_target objects are exactly identical. + */ +bool +pgpa_index_targets_equal(pgpa_index_target *i1, pgpa_index_target *i2) +{ + /* indnamespace can be NULL, and two NULL values are equal */ + if ((i1->indnamespace != NULL || i2->indnamespace != NULL) && + (i1->indnamespace == NULL || i2->indnamespace == NULL || + strcmp(i1->indnamespace, i2->indnamespace) != 0)) + return false; + if (strcmp(i1->indname, i2->indname) != 0) + return false; + + return true; +} + +/* + * Check whether an identifier matches an any part of an advice target. + */ +bool +pgpa_identifier_matches_target(pgpa_identifier *rid, pgpa_advice_target *target) +{ + /* For non-identifiers, check all descendants. */ + if (target->ttype != PGPA_TARGET_IDENTIFIER) + { + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + if (pgpa_identifier_matches_target(rid, child_target)) + return true; + } + return false; + } + + /* Straightforward comparisons of alias name and occurrence number. */ + if (strcmp(rid->alias_name, target->rid.alias_name) != 0) + return false; + if (rid->occurrence != target->rid.occurrence) + return false; + + /* + * If a relation identifier mentions a partition name, it should also + * specify a partition schema. But the target may leave the schema NULL to + * match anything. + */ + Assert(rid->partnsp != NULL || rid->partrel == NULL); + if (rid->partnsp != NULL && target->rid.partnsp != NULL && + strcmp(rid->partnsp, target->rid.partnsp) != 0) + return false; + + /* + * These fields can be NULL on either side, but NULL only matches another + * NULL. + */ + if (!strings_equal_or_both_null(rid->partrel, target->rid.partrel)) + return false; + if (!strings_equal_or_both_null(rid->plan_name, target->rid.plan_name)) + return false; + + return true; +} + +/* + * Match identifiers to advice targets and return an enum value indicating + * the relationship between the set of keys and the set of targets. + * + * See the comments for pgpa_itm_type. + */ +pgpa_itm_type +pgpa_identifiers_match_target(int nrids, pgpa_identifier *rids, + pgpa_advice_target *target) +{ + bool all_rids_used = true; + bool any_rids_used = false; + bool all_targets_used; + bool *rids_used = palloc0_array(bool, nrids); + + all_targets_used = + pgpa_identifiers_cover_target(nrids, rids, target, rids_used); + + for (int i = 0; i < nrids; ++i) + { + if (rids_used[i]) + any_rids_used = true; + else + all_rids_used = false; + } + + if (all_rids_used) + { + if (all_targets_used) + return PGPA_ITM_EQUAL; + else + return PGPA_ITM_KEYS_ARE_SUBSET; + } + else + { + if (all_targets_used) + return PGPA_ITM_TARGETS_ARE_SUBSET; + else if (any_rids_used) + return PGPA_ITM_INTERSECTING; + else + return PGPA_ITM_DISJOINT; + } +} + +/* + * Returns true if every target or sub-target is matched by at least one + * identifier, and otherwise false. + * + * Also sets rids_used[i] = true for each identifier that matches at least one + * target. + */ +static bool +pgpa_identifiers_cover_target(int nrids, pgpa_identifier *rids, + pgpa_advice_target *target, bool *rids_used) +{ + bool result = false; + + if (target->ttype != PGPA_TARGET_IDENTIFIER) + { + result = true; + + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + if (!pgpa_identifiers_cover_target(nrids, rids, child_target, + rids_used)) + result = false; + } + } + else + { + for (int i = 0; i < nrids; ++i) + { + if (pgpa_identifier_matches_target(&rids[i], target)) + { + rids_used[i] = true; + result = true; + } + } + } + + return result; +} diff --git a/contrib/pg_plan_advice/pgpa_ast.h b/contrib/pg_plan_advice/pgpa_ast.h new file mode 100644 index 0000000000000..4bd6ffa5e3a58 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_ast.h @@ -0,0 +1,186 @@ +/*------------------------------------------------------------------------- + * + * pgpa_ast.h + * abstract syntax trees for plan advice, plus parser/scanner support + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_ast.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_AST_H +#define PGPA_AST_H + +#include "pgpa_identifier.h" + +#include "nodes/pg_list.h" + +/* + * Advice items generally take the form SOME_TAG(item [...]), where an item + * can take various forms. The simplest case is a relation identifier, but + * some tags allow sublists, and JOIN_ORDER() allows both ordered and unordered + * sublists. + */ +typedef enum +{ + PGPA_TARGET_IDENTIFIER, /* relation identifier */ + PGPA_TARGET_ORDERED_LIST, /* (item ...) */ + PGPA_TARGET_UNORDERED_LIST /* {item ...} */ +} pgpa_target_type; + +/* + * An index specification. + */ +typedef struct pgpa_index_target +{ + /* Index schema and name */ + char *indnamespace; + char *indname; +} pgpa_index_target; + +/* + * A single item about which advice is being given, which could be either + * a relation identifier that we want to break out into its constituent fields, + * or a sublist of some kind. + */ +typedef struct pgpa_advice_target +{ + pgpa_target_type ttype; + + /* + * This field is meaningful when ttype is PGPA_TARGET_IDENTIFIER. + * + * All identifiers must have an alias name and an occurrence number; the + * remaining fields can be NULL. Note that it's possible to specify a + * partition name without a partition schema, but not the reverse. + */ + pgpa_identifier rid; + + /* + * This field is set when ttype is PGPA_TARGET_IDENTIFIER and the advice + * tag is PGPA_TAG_INDEX_SCAN or PGPA_TAG_INDEX_ONLY_SCAN. + */ + pgpa_index_target *itarget; + + /* + * When the ttype is PGPA_TARGET__LIST, this field contains a + * list of additional pgpa_advice_target objects. Otherwise, it is unused. + */ + List *children; +} pgpa_advice_target; + +/* + * These are all the kinds of advice that we know how to parse. If a keyword + * is found at the top level, it must be in this list. + * + * If you change anything here, also update pgpa_parse_advice_tag and + * pgpa_cstring_advice_tag. + */ +typedef enum pgpa_advice_tag_type +{ + PGPA_TAG_BITMAP_HEAP_SCAN, + PGPA_TAG_DO_NOT_SCAN, + PGPA_TAG_FOREIGN_JOIN, + PGPA_TAG_GATHER, + PGPA_TAG_GATHER_MERGE, + PGPA_TAG_HASH_JOIN, + PGPA_TAG_INDEX_ONLY_SCAN, + PGPA_TAG_INDEX_SCAN, + PGPA_TAG_JOIN_ORDER, + PGPA_TAG_MERGE_JOIN_MATERIALIZE, + PGPA_TAG_MERGE_JOIN_PLAIN, + PGPA_TAG_NESTED_LOOP_MATERIALIZE, + PGPA_TAG_NESTED_LOOP_MEMOIZE, + PGPA_TAG_NESTED_LOOP_PLAIN, + PGPA_TAG_NO_GATHER, + PGPA_TAG_PARTITIONWISE, + PGPA_TAG_SEMIJOIN_NON_UNIQUE, + PGPA_TAG_SEMIJOIN_UNIQUE, + PGPA_TAG_SEQ_SCAN, + PGPA_TAG_TID_SCAN +} pgpa_advice_tag_type; + +/* + * An item of advice, meaning a tag and the list of all targets to which + * it is being applied. + * + * "targets" is a list of pgpa_advice_target objects. + * + * The List returned from pgpa_yyparse is list of pgpa_advice_item objects. + */ +typedef struct pgpa_advice_item +{ + pgpa_advice_tag_type tag; + List *targets; +} pgpa_advice_item; + +/* + * Result of comparing an array of pgpa_identifier objects to a + * pgpa_advice_target. + * + * PGPA_ITM_EQUAL means all targets are matched by some identifier, and + * all identifiers were matched to a target. + * + * PGPA_ITM_KEYS_ARE_SUBSET means that all identifiers matched to a target, + * but there were leftover targets. Generally, this means that the advice is + * looking to apply to all of the rels we have plus some additional ones that + * we don't have. + * + * PGPA_ITM_TARGETS_ARE_SUBSET means that all targets are matched by + * identifiers, but there were leftover identifiers. Generally, this means + * that the advice is looking to apply to some but not all of the rels we have. + * + * PGPA_ITM_INTERSECTING means that some identifiers and targets were matched, + * but neither all identifiers nor all targets could be matched to items in + * the other set. + * + * PGPA_ITM_DISJOINT means that no matches between identifiers and targets were + * found. + */ +typedef enum +{ + PGPA_ITM_EQUAL, + PGPA_ITM_KEYS_ARE_SUBSET, + PGPA_ITM_TARGETS_ARE_SUBSET, + PGPA_ITM_INTERSECTING, + PGPA_ITM_DISJOINT +} pgpa_itm_type; + +/* for pgpa_scanner.l and pgpa_parser.y */ +union YYSTYPE; +#ifndef YY_TYPEDEF_YY_SCANNER_T +#define YY_TYPEDEF_YY_SCANNER_T +typedef void *yyscan_t; +#endif + +/* in pgpa_scanner.l */ +extern int pgpa_yylex(union YYSTYPE *yylval_param, List **result, + char **parse_error_msg_p, yyscan_t yyscanner); +extern void pgpa_yyerror(List **result, char **parse_error_msg_p, + yyscan_t yyscanner, + const char *message); +extern void pgpa_scanner_init(const char *str, yyscan_t *yyscannerp); +extern void pgpa_scanner_finish(yyscan_t yyscanner); + +/* in pgpa_parser.y */ +extern int pgpa_yyparse(List **result, char **parse_error_msg_p, + yyscan_t yyscanner); +extern List *pgpa_parse(const char *advice_string, char **error_p); + +/* in pgpa_ast.c */ +extern char *pgpa_cstring_advice_tag(pgpa_advice_tag_type advice_tag); +extern bool pgpa_identifier_matches_target(pgpa_identifier *rid, + pgpa_advice_target *target); +extern pgpa_itm_type pgpa_identifiers_match_target(int nrids, + pgpa_identifier *rids, + pgpa_advice_target *target); +extern bool pgpa_index_targets_equal(pgpa_index_target *i1, + pgpa_index_target *i2); +extern pgpa_advice_tag_type pgpa_parse_advice_tag(const char *tag, bool *fail); +extern void pgpa_format_advice_target(StringInfo str, + pgpa_advice_target *target); +extern void pgpa_format_index_target(StringInfo str, + pgpa_index_target *itarget); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_identifier.c b/contrib/pg_plan_advice/pgpa_identifier.c new file mode 100644 index 0000000000000..21392b8e0d7e4 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_identifier.c @@ -0,0 +1,481 @@ +/*------------------------------------------------------------------------- + * + * pgpa_identifier.c + * create appropriate identifiers for range table entries + * + * The goal of this module is to be able to produce identifiers for range + * table entries that are unique, understandable to human beings, and + * able to be reconstructed during future planning cycles. As an + * exception, we do not care about, or want to produce, identifiers for + * RTE_JOIN entries. This is because (1) we would end up with a ton of + * RTEs with unhelpful names like unnamed_join_17; (2) not all joins have + * RTEs; and (3) we intend to refer to joins by their constituent members + * rather than by reference to the join RTE. + * + * In general, we construct identifiers of the following form: + * + * alias_name#occurrence_number/child_table_name@subquery_name + * + * However, occurrence_number is omitted when it is the first occurrence + * within the same subquery, child_table_name is omitted for relations that + * are not child tables, and subquery_name is omitted for the topmost + * query level. Whenever an item is omitted, the preceding punctuation mark + * is also omitted. Identifier-style escaping is applied to alias_name and + * subquery_name. In generated advice, child table names are always + * schema-qualified, but users can supply advice where the schema name is + * not mentioned. Identifier-style escaping is applied to the schema and to + * the relation name separately. + * + * The upshot of all of these rules is that in simple cases, the relation + * identifier is textually identical to the alias name, making life easier + * for users. However, even in complex cases, every relation identifier + * for a given query will be unique (or at least we hope so: if not, this + * code is buggy and the identifier format might need to be rethought). + * + * A key goal of this system is that we want to be able to reconstruct the + * same identifiers during a future planning cycle for the same query, so + * that if a certain behavior is specified for a certain identifier, we can + * properly identify the RTI for which that behavior is mandated. In order + * for this to work, subquery names must be unique and known before the + * subquery is planned, and the remainder of the identifier must not depend + * on any part of the query outside of the current subquery level. In + * particular, occurrence_number must be calculated relative to the range + * table for the relevant subquery, not the final flattened range table. + * + * NB: All of this code must use rt_fetch(), not planner_rt_fetch()! + * Join removal and self-join elimination remove rels from the arrays + * that planner_rt_fetch() uses; using rt_fetch() is necessary to get + * stable results. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_identifier.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "pgpa_identifier.h" + +#include "parser/parsetree.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +static Index *pgpa_create_top_rti_map(Index rtable_length, List *rtable, + List *appinfos); +static int pgpa_occurrence_number(List *rtable, Index *top_rti_map, + SubPlanRTInfo *rtinfo, Index rti); + +/* + * Create a range table identifier from scratch. + * + * This function leaves the caller to do all the heavy lifting, so it's + * generally better to use one of the functions below instead. + * + * See the file header comments for more details on the format of an + * identifier. + */ +const char * +pgpa_identifier_string(const pgpa_identifier *rid) +{ + const char *result; + + Assert(rid->alias_name != NULL); + result = quote_identifier(rid->alias_name); + + Assert(rid->occurrence >= 0); + if (rid->occurrence > 1) + result = psprintf("%s#%d", result, rid->occurrence); + + if (rid->partrel != NULL) + { + if (rid->partnsp == NULL) + result = psprintf("%s/%s", result, + quote_identifier(rid->partrel)); + else + result = psprintf("%s/%s.%s", result, + quote_identifier(rid->partnsp), + quote_identifier(rid->partrel)); + } + + if (rid->plan_name != NULL) + result = psprintf("%s@%s", result, quote_identifier(rid->plan_name)); + + return result; +} + +/* + * Compute a relation identifier for a particular RTI. + * + * The caller provides root and rti, and gets the necessary details back via + * the remaining parameters. + */ +void +pgpa_compute_identifier_by_rti(PlannerInfo *root, Index rti, + pgpa_identifier *rid) +{ + Index top_rti = rti; + int occurrence = 1; + RangeTblEntry *rte; + RangeTblEntry *top_rte; + char *partnsp = NULL; + char *partrel = NULL; + + /* + * If this is a child RTE, find the topmost parent that is still of type + * RTE_RELATION. We do this because we identify children of partitioned + * tables by the name of the child table, but subqueries can also have + * child rels and we don't care about those here. + */ + for (;;) + { + AppendRelInfo *appinfo; + RangeTblEntry *parent_rte; + + /* append_rel_array can be NULL if there are no children */ + if (root->append_rel_array == NULL || + (appinfo = root->append_rel_array[top_rti]) == NULL) + break; + + parent_rte = rt_fetch(appinfo->parent_relid, root->parse->rtable); + if (parent_rte->rtekind != RTE_RELATION) + break; + + top_rti = appinfo->parent_relid; + } + + /* Get the range table entries for the RTI and top RTI. */ + rte = rt_fetch(rti, root->parse->rtable); + top_rte = rt_fetch(top_rti, root->parse->rtable); + Assert(rte->rtekind != RTE_JOIN); + Assert(top_rte->rtekind != RTE_JOIN); + + /* Work out the correct occurrence number. */ + for (Index prior_rti = 1; prior_rti < top_rti; ++prior_rti) + { + RangeTblEntry *prior_rte; + AppendRelInfo *appinfo; + + /* + * If this is a child rel of a parent that is a relation, skip it. + * + * Such range table entries are disambiguated by mentioning the schema + * and name of the table, not by counting them as separate occurrences + * of the same table. + * + * NB: append_rel_array can be NULL if there are no children + */ + if (root->append_rel_array != NULL && + (appinfo = root->append_rel_array[prior_rti]) != NULL) + { + RangeTblEntry *parent_rte; + + parent_rte = rt_fetch(appinfo->parent_relid, root->parse->rtable); + if (parent_rte->rtekind == RTE_RELATION) + continue; + } + + /* Skip NULL entries and joins. */ + prior_rte = rt_fetch(prior_rti, root->parse->rtable); + if (prior_rte == NULL || prior_rte->rtekind == RTE_JOIN) + continue; + + /* Skip if the alias name differs. */ + if (strcmp(prior_rte->eref->aliasname, rte->eref->aliasname) != 0) + continue; + + /* Looks like a true duplicate. */ + ++occurrence; + } + + /* If this is a child table, get the schema and relation names. */ + if (rti != top_rti) + { + partnsp = get_namespace_name_or_temp(get_rel_namespace(rte->relid)); + partrel = get_rel_name(rte->relid); + } + + /* OK, we have all the answers we need. Return them to the caller. */ + rid->alias_name = top_rte->eref->aliasname; + rid->occurrence = occurrence; + rid->partnsp = partnsp; + rid->partrel = partrel; + rid->plan_name = root->plan_name; +} + +/* + * Compute a relation identifier for a set of RTIs, except for any RTE_JOIN + * RTIs that may be present. + * + * RTE_JOIN entries are excluded because they cannot be mentioned by plan + * advice. + * + * The caller is responsible for making sure that the "rids" array is large + * enough to store the results. + * + * The return value is the number of identifiers computed. + */ +int +pgpa_compute_identifiers_by_relids(PlannerInfo *root, Bitmapset *relids, + pgpa_identifier *rids) +{ + int count = 0; + int rti = -1; + + while ((rti = bms_next_member(relids, rti)) >= 0) + { + RangeTblEntry *rte = rt_fetch(rti, root->parse->rtable); + + if (rte->rtekind == RTE_JOIN) + continue; + pgpa_compute_identifier_by_rti(root, rti, &rids[count++]); + } + + Assert(count > 0); + return count; +} + +/* + * Create an array of range table identifiers for all the non-NULL, + * non-RTE_JOIN entries in the PlannedStmt's range table. + */ +pgpa_identifier * +pgpa_create_identifiers_for_planned_stmt(PlannedStmt *pstmt) +{ + Index rtable_length = list_length(pstmt->rtable); + pgpa_identifier *result = palloc0_array(pgpa_identifier, rtable_length); + Index *top_rti_map; + int rtinfoindex = 0; + SubPlanRTInfo *rtinfo = NULL; + SubPlanRTInfo *nextrtinfo = NULL; + + /* + * Account for relations added by inheritance expansion of partitioned + * tables. + */ + top_rti_map = pgpa_create_top_rti_map(rtable_length, pstmt->rtable, + pstmt->appendRelations); + + /* + * When we begin iterating, we're processing the portion of the range + * table that originated from the top-level PlannerInfo, so subrtinfo is + * NULL. Later, subrtinfo will be the SubPlanRTInfo for the subquery whose + * portion of the range table we are processing. nextrtinfo is always the + * SubPlanRTInfo that follows the current one, if any, so when we're + * processing the top-level query's portion of the range table, the next + * SubPlanRTInfo is the very first one. + */ + if (pstmt->subrtinfos != NULL) + nextrtinfo = linitial(pstmt->subrtinfos); + + /* Main loop over the range table. */ + for (Index rti = 1; rti <= rtable_length; rti++) + { + const char *plan_name; + Index top_rti; + RangeTblEntry *rte; + RangeTblEntry *top_rte; + char *partnsp = NULL; + char *partrel = NULL; + int occurrence; + pgpa_identifier *rid; + + /* + * Advance to the next SubPlanRTInfo, if it's time to do that. + * + * This loop probably shouldn't ever iterate more than once, because + * that would imply that a subquery was planned but added nothing to + * the range table; but let's be defensive and assume it can happen. + */ + while (nextrtinfo != NULL && rti > nextrtinfo->rtoffset) + { + rtinfo = nextrtinfo; + if (++rtinfoindex >= list_length(pstmt->subrtinfos)) + nextrtinfo = NULL; + else + nextrtinfo = list_nth(pstmt->subrtinfos, rtinfoindex); + } + + /* Fetch the range table entry, if any. */ + rte = rt_fetch(rti, pstmt->rtable); + + /* + * We can't and don't need to identify null entries, and we don't want + * to identify join entries. + */ + if (rte == NULL || rte->rtekind == RTE_JOIN) + continue; + + /* + * If this is not a relation added by partitioned table expansion, + * then the top RTI/RTE are just the same as this RTI/RTE. Otherwise, + * we need the information for the top RTI/RTE, and must also fetch + * the partition schema and name. + */ + top_rti = top_rti_map[rti - 1]; + if (rti == top_rti) + top_rte = rte; + else + { + top_rte = rt_fetch(top_rti, pstmt->rtable); + partnsp = + get_namespace_name_or_temp(get_rel_namespace(rte->relid)); + partrel = get_rel_name(rte->relid); + } + + /* Compute the correct occurrence number. */ + occurrence = pgpa_occurrence_number(pstmt->rtable, top_rti_map, + rtinfo, top_rti); + + /* Get the name of the current plan (NULL for toplevel query). */ + plan_name = rtinfo == NULL ? NULL : rtinfo->plan_name; + + /* Save all the details we've derived. */ + rid = &result[rti - 1]; + rid->alias_name = top_rte->eref->aliasname; + rid->occurrence = occurrence; + rid->partnsp = partnsp; + rid->partrel = partrel; + rid->plan_name = plan_name; + } + + return result; +} + +/* + * Search for a pgpa_identifier in the array of identifiers computed for the + * range table. If exactly one match is found, return the matching RTI; else + * return 0. + */ +Index +pgpa_compute_rti_from_identifier(int rtable_length, + pgpa_identifier *rt_identifiers, + pgpa_identifier *rid) +{ + Index result = 0; + + for (Index rti = 1; rti <= rtable_length; ++rti) + { + pgpa_identifier *rti_rid = &rt_identifiers[rti - 1]; + + /* If there's no identifier for this RTI, skip it. */ + if (rti_rid->alias_name == NULL) + continue; + + /* + * If it matches, return this RTI. As usual, an omitted partition + * schema matches anything, but partition and plan names must either + * match exactly or be omitted on both sides. + */ + if (strcmp(rid->alias_name, rti_rid->alias_name) == 0 && + rid->occurrence == rti_rid->occurrence && + (rid->partnsp == NULL || rti_rid->partnsp == NULL || + strcmp(rid->partnsp, rti_rid->partnsp) == 0) && + strings_equal_or_both_null(rid->partrel, rti_rid->partrel) && + strings_equal_or_both_null(rid->plan_name, rti_rid->plan_name)) + { + if (result != 0) + { + /* Multiple matches were found. */ + return 0; + } + result = rti; + } + } + + return result; +} + +/* + * Build a mapping from each RTI to the RTI whose alias_name will be used to + * construct the range table identifier. + * + * For child relations, this is the topmost parent that is still of type + * RTE_RELATION. For other relations, it's just the original RTI. + * + * Since we're eventually going to need this information for every RTI in + * the range table, it's best to compute all the answers in a single pass over + * the AppendRelInfo list. Otherwise, we might end up searching through that + * list repeatedly for entries of interest. + * + * Note that the returned array is uses zero-based indexing, while RTIs use + * 1-based indexing, so subtract 1 from the RTI before looking it up in the + * array. + */ +static Index * +pgpa_create_top_rti_map(Index rtable_length, List *rtable, List *appinfos) +{ + Index *top_rti_map = palloc0_array(Index, rtable_length); + + /* Initially, make every RTI point to itself. */ + for (Index rti = 1; rti <= rtable_length; ++rti) + top_rti_map[rti - 1] = rti; + + /* Update the map for each AppendRelInfo object. */ + foreach_node(AppendRelInfo, appinfo, appinfos) + { + Index parent_rti = appinfo->parent_relid; + RangeTblEntry *parent_rte = rt_fetch(parent_rti, rtable); + + /* If the parent is not RTE_RELATION, ignore this entry. */ + if (parent_rte->rtekind != RTE_RELATION) + continue; + + /* + * Map the child to wherever we mapped the parent. Parents always + * precede their children in the AppendRelInfo list, so this should + * work out. + */ + top_rti_map[appinfo->child_relid - 1] = top_rti_map[parent_rti - 1]; + } + + return top_rti_map; +} + +/* + * Find the occurrence number of a certain relation within a certain subquery. + * + * The same alias name can occur multiple times within a subquery, but we want + * to disambiguate by giving different occurrences different integer indexes. + * However, child tables are disambiguated by including the table name rather + * than by incrementing the occurrence number; and joins are not named and so + * shouldn't increment the occurrence number either. + */ +static int +pgpa_occurrence_number(List *rtable, Index *top_rti_map, + SubPlanRTInfo *rtinfo, Index rti) +{ + Index rtoffset = (rtinfo == NULL) ? 0 : rtinfo->rtoffset; + int occurrence = 1; + RangeTblEntry *rte = rt_fetch(rti, rtable); + + for (Index prior_rti = rtoffset + 1; prior_rti < rti; ++prior_rti) + { + RangeTblEntry *prior_rte; + + /* + * If this is a child rel of a parent that is a relation, skip it. + * + * Such range table entries are disambiguated by mentioning the schema + * and name of the table, not by counting them as separate occurrences + * of the same table. + */ + if (top_rti_map[prior_rti - 1] != prior_rti) + continue; + + /* Skip joins. */ + prior_rte = rt_fetch(prior_rti, rtable); + if (prior_rte->rtekind == RTE_JOIN) + continue; + + /* Skip if the alias name differs. */ + if (strcmp(prior_rte->eref->aliasname, rte->eref->aliasname) != 0) + continue; + + /* Looks like a true duplicate. */ + ++occurrence; + } + + return occurrence; +} diff --git a/contrib/pg_plan_advice/pgpa_identifier.h b/contrib/pg_plan_advice/pgpa_identifier.h new file mode 100644 index 0000000000000..393a83dc78c74 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_identifier.h @@ -0,0 +1,52 @@ +/*------------------------------------------------------------------------- + * + * pgpa_identifier.h + * create appropriate identifiers for range table entries + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_identifier.h + * + *------------------------------------------------------------------------- + */ + +#ifndef PGPA_IDENTIFIER_H +#define PGPA_IDENTIFIER_H + +#include "nodes/pathnodes.h" +#include "nodes/plannodes.h" + +typedef struct pgpa_identifier +{ + const char *alias_name; + int occurrence; + const char *partnsp; + const char *partrel; + const char *plan_name; +} pgpa_identifier; + +/* Convenience function for comparing possibly-NULL strings. */ +static inline bool +strings_equal_or_both_null(const char *a, const char *b) +{ + if (a == b) + return true; + else if (a == NULL || b == NULL) + return false; + else + return strcmp(a, b) == 0; +} + +extern const char *pgpa_identifier_string(const pgpa_identifier *rid); +extern void pgpa_compute_identifier_by_rti(PlannerInfo *root, Index rti, + pgpa_identifier *rid); +extern int pgpa_compute_identifiers_by_relids(PlannerInfo *root, + Bitmapset *relids, + pgpa_identifier *rids); +extern pgpa_identifier *pgpa_create_identifiers_for_planned_stmt(PlannedStmt *pstmt); + +extern Index pgpa_compute_rti_from_identifier(int rtable_length, + pgpa_identifier *rt_identifiers, + pgpa_identifier *rid); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_join.c b/contrib/pg_plan_advice/pgpa_join.c new file mode 100644 index 0000000000000..067321081e7b4 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_join.c @@ -0,0 +1,642 @@ +/*------------------------------------------------------------------------- + * + * pgpa_join.c + * analysis of joins in Plan trees + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_join.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "pgpa_join.h" +#include "pgpa_scan.h" +#include "pgpa_walker.h" + +#include "nodes/pathnodes.h" +#include "nodes/print.h" +#include "parser/parsetree.h" + +/* + * Temporary object used when unrolling a join tree. + */ +struct pgpa_join_unroller +{ + unsigned nallocated; + unsigned nused; + Plan *outer_subplan; + ElidedNode *outer_elided_node; + bool outer_beneath_any_gather; + pgpa_join_strategy *strategy; + Plan **inner_subplans; + ElidedNode **inner_elided_nodes; + pgpa_join_unroller **inner_unrollers; + bool *inner_beneath_any_gather; +}; + +static pgpa_join_strategy pgpa_decompose_join(pgpa_plan_walker_context *walker, + Plan *plan, + Plan **realouter, + Plan **realinner, + ElidedNode **elidedrealouter, + ElidedNode **elidedrealinner, + bool *found_any_outer_gather, + bool *found_any_inner_gather); +static ElidedNode *pgpa_descend_node(PlannedStmt *pstmt, Plan **plan); +static ElidedNode *pgpa_descend_any_gather(PlannedStmt *pstmt, Plan **plan, + bool *found_any_gather); +static bool pgpa_descend_any_unique(PlannedStmt *pstmt, Plan **plan, + ElidedNode **elided_node); + +static bool is_result_node_with_child(Plan *plan); +static bool is_sorting_plan(Plan *plan); + +/* + * Create an initially-empty object for unrolling joins. + * + * This function creates a helper object that can later be used to create a + * pgpa_unrolled_join, after first calling pgpa_unroll_join one or more times. + */ +pgpa_join_unroller * +pgpa_create_join_unroller(void) +{ + pgpa_join_unroller *join_unroller; + + join_unroller = palloc0_object(pgpa_join_unroller); + join_unroller->nallocated = 4; + join_unroller->strategy = + palloc_array(pgpa_join_strategy, join_unroller->nallocated); + join_unroller->inner_subplans = + palloc_array(Plan *, join_unroller->nallocated); + join_unroller->inner_elided_nodes = + palloc_array(ElidedNode *, join_unroller->nallocated); + join_unroller->inner_unrollers = + palloc_array(pgpa_join_unroller *, join_unroller->nallocated); + join_unroller->inner_beneath_any_gather = + palloc_array(bool, join_unroller->nallocated); + + return join_unroller; +} + +/* + * Unroll one level of an unrollable join tree. + * + * Our basic goal here is to unroll join trees as they occur in the Plan + * tree into a simpler and more regular structure that we can more easily + * use for further processing. Unrolling is outer-deep, so if the plan tree + * has Join1(Join2(A,B),Join3(C,D)), the same join unroller object should be + * used for Join1 and Join2, but a different one will be needed for Join3, + * since that involves a join within the *inner* side of another join. + * + * pgpa_plan_walker creates a "top level" join unroller object when it + * encounters a join in a portion of the plan tree in which no join unroller + * is already active. From there, this function is responsible for determining + * to what portion of the plan tree that join unroller applies, and for + * creating any subordinate join unroller objects that are needed as a result + * of non-outer-deep join trees. We do this by returning the join unroller + * objects that should be used for further traversal of the outer and inner + * subtrees of the current plan node via *outer_join_unroller and + * *inner_join_unroller, respectively. + */ +void +pgpa_unroll_join(pgpa_plan_walker_context *walker, Plan *plan, + bool beneath_any_gather, + pgpa_join_unroller *join_unroller, + pgpa_join_unroller **outer_join_unroller, + pgpa_join_unroller **inner_join_unroller) +{ + pgpa_join_strategy strategy; + Plan *realinner, + *realouter; + ElidedNode *elidedinner, + *elidedouter; + int n; + bool found_any_outer_gather = false; + bool found_any_inner_gather = false; + + Assert(join_unroller != NULL); + + /* + * We need to pass the join_unroller object down through certain types of + * plan nodes -- anything that's considered part of the join strategy, and + * any other nodes that can occur in a join tree despite not being scans + * or joins. + * + * This includes: + * + * (1) Materialize, Memoize, and Hash nodes, which are part of the join + * strategy, + * + * (2) Gather and Gather Merge nodes, which can occur at any point in the + * join tree where the planner decided to initiate parallelism, + * + * (3) Sort and IncrementalSort nodes, which can occur beneath MergeJoin + * or GatherMerge, + * + * (4) Agg and Unique nodes, which can occur when we decide to make the + * nullable side of a semijoin unique and then join the result, and + * + * (5) Result nodes with children, which can be added either to project to + * enforce a one-time filter (but Result nodes without children are + * degenerate scans or joins). + */ + if (IsA(plan, Material) || IsA(plan, Memoize) || IsA(plan, Hash) + || IsA(plan, Gather) || IsA(plan, GatherMerge) + || is_sorting_plan(plan) || IsA(plan, Agg) || IsA(plan, Unique) + || is_result_node_with_child(plan)) + { + *outer_join_unroller = join_unroller; + return; + } + + /* + * Since we've already handled nodes that require pass-through treatment, + * this should be an unrollable join. + */ + strategy = pgpa_decompose_join(walker, plan, + &realouter, &realinner, + &elidedouter, &elidedinner, + &found_any_outer_gather, + &found_any_inner_gather); + + /* If our workspace is full, expand it. */ + if (join_unroller->nused >= join_unroller->nallocated) + { + join_unroller->nallocated *= 2; + join_unroller->strategy = + repalloc_array(join_unroller->strategy, + pgpa_join_strategy, + join_unroller->nallocated); + join_unroller->inner_subplans = + repalloc_array(join_unroller->inner_subplans, + Plan *, + join_unroller->nallocated); + join_unroller->inner_elided_nodes = + repalloc_array(join_unroller->inner_elided_nodes, + ElidedNode *, + join_unroller->nallocated); + join_unroller->inner_beneath_any_gather = + repalloc_array(join_unroller->inner_beneath_any_gather, + bool, + join_unroller->nallocated); + join_unroller->inner_unrollers = + repalloc_array(join_unroller->inner_unrollers, + pgpa_join_unroller *, + join_unroller->nallocated); + } + + /* + * Since we're flattening outer-deep join trees, it follows that if the + * outer side is still an unrollable join, it should be unrolled into this + * same object. Otherwise, we've reached the limit of what we can unroll + * into this object and must remember the outer side as the final outer + * subplan. + */ + if (elidedouter == NULL && pgpa_is_join(realouter)) + *outer_join_unroller = join_unroller; + else + { + join_unroller->outer_subplan = realouter; + join_unroller->outer_elided_node = elidedouter; + join_unroller->outer_beneath_any_gather = + beneath_any_gather || found_any_outer_gather; + } + + /* + * Store the inner subplan. If it's an unrollable join, it needs to be + * flattened in turn, but into a new unroller object, not this one. + */ + n = join_unroller->nused++; + join_unroller->strategy[n] = strategy; + join_unroller->inner_subplans[n] = realinner; + join_unroller->inner_elided_nodes[n] = elidedinner; + join_unroller->inner_beneath_any_gather[n] = + beneath_any_gather || found_any_inner_gather; + if (elidedinner == NULL && pgpa_is_join(realinner)) + *inner_join_unroller = pgpa_create_join_unroller(); + else + *inner_join_unroller = NULL; + join_unroller->inner_unrollers[n] = *inner_join_unroller; +} + +/* + * Use the data we've accumulated in a pgpa_join_unroller object to construct + * a pgpa_unrolled_join. + */ +pgpa_unrolled_join * +pgpa_build_unrolled_join(pgpa_plan_walker_context *walker, + pgpa_join_unroller *join_unroller) +{ + pgpa_unrolled_join *ujoin; + int i; + + /* + * We shouldn't have gone even so far as to create a join unroller unless + * we found at least one unrollable join. + */ + Assert(join_unroller->nused > 0); + + /* Allocate result structures. */ + ujoin = palloc0_object(pgpa_unrolled_join); + ujoin->ninner = join_unroller->nused; + ujoin->strategy = palloc0_array(pgpa_join_strategy, join_unroller->nused); + ujoin->inner = palloc0_array(pgpa_join_member, join_unroller->nused); + + /* Handle the outermost join. */ + ujoin->outer.plan = join_unroller->outer_subplan; + ujoin->outer.elided_node = join_unroller->outer_elided_node; + ujoin->outer.scan = + pgpa_build_scan(walker, ujoin->outer.plan, + ujoin->outer.elided_node, + join_unroller->outer_beneath_any_gather, + true); + + /* + * We want the joins from the deepest part of the plan tree to appear + * first in the result object, but the join unroller adds them in exactly + * the reverse of that order, so we need to flip the order of the arrays + * when constructing the final result. + */ + for (i = 0; i < join_unroller->nused; ++i) + { + int k = join_unroller->nused - i - 1; + + /* Copy strategy, Plan, and ElidedNode. */ + ujoin->strategy[i] = join_unroller->strategy[k]; + ujoin->inner[i].plan = join_unroller->inner_subplans[k]; + ujoin->inner[i].elided_node = join_unroller->inner_elided_nodes[k]; + + /* + * Fill in remaining details, using either the nested join unroller, + * or by deriving them from the plan and elided nodes. + */ + if (join_unroller->inner_unrollers[k] != NULL) + ujoin->inner[i].unrolled_join = + pgpa_build_unrolled_join(walker, + join_unroller->inner_unrollers[k]); + else + ujoin->inner[i].scan = + pgpa_build_scan(walker, ujoin->inner[i].plan, + ujoin->inner[i].elided_node, + join_unroller->inner_beneath_any_gather[k], + true); + } + + return ujoin; +} + +/* + * Free memory allocated for pgpa_join_unroller. + */ +void +pgpa_destroy_join_unroller(pgpa_join_unroller *join_unroller) +{ + pfree(join_unroller->strategy); + pfree(join_unroller->inner_subplans); + pfree(join_unroller->inner_elided_nodes); + pfree(join_unroller->inner_unrollers); + pfree(join_unroller->inner_beneath_any_gather); + pfree(join_unroller); +} + +/* + * Identify the join strategy used by a join and the "real" inner and outer + * plans. + * + * For example, a Hash Join always has a Hash node on the inner side, but + * for all intents and purposes the real inner input is the Hash node's child, + * not the Hash node itself. + * + * Likewise, a Merge Join may have Sort node on the inner or outer side; if + * it does, the real input to the join is the Sort node's child, not the + * Sort node itself. + * + * In addition, with a Merge Join or a Nested Loop, the join planning code + * may add additional nodes such as Materialize or Memoize. We regard these + * as an aspect of the join strategy. As in the previous cases, the true input + * to the join is the underlying node. + * + * However, if any involved child node previously had a now-elided node stacked + * on top, then we can't "look through" that node -- indeed, what's going to be + * relevant for our purposes is the ElidedNode on top of that plan node, rather + * than the plan node itself. + * + * If there are multiple elided nodes, we want that one that would have been + * uppermost in the plan tree prior to setrefs processing; we expect to find + * that one last in the list of elided nodes. + * + * On return *realouter and *realinner will have been set to the real inner + * and real outer plans that we identified, and *elidedrealouter and + * *elidedrealinner to the last of any corresponding elided nodes. + * Additionally, *found_any_outer_gather and *found_any_inner_gather will + * be set to true if we looked through a Gather or Gather Merge node on + * that side of the join, and false otherwise. + */ +static pgpa_join_strategy +pgpa_decompose_join(pgpa_plan_walker_context *walker, Plan *plan, + Plan **realouter, Plan **realinner, + ElidedNode **elidedrealouter, ElidedNode **elidedrealinner, + bool *found_any_outer_gather, bool *found_any_inner_gather) +{ + PlannedStmt *pstmt = walker->pstmt; + JoinType jointype = ((Join *) plan)->jointype; + Plan *outerplan = plan->lefttree; + Plan *innerplan = plan->righttree; + ElidedNode *elidedouter; + ElidedNode *elidedinner; + pgpa_join_strategy strategy; + bool uniqueouter; + bool uniqueinner; + + elidedouter = pgpa_last_elided_node(pstmt, outerplan); + elidedinner = pgpa_last_elided_node(pstmt, innerplan); + *found_any_outer_gather = false; + *found_any_inner_gather = false; + + switch (nodeTag(plan)) + { + case T_MergeJoin: + + /* + * The planner may have chosen to place a Material node on the + * inner side of the MergeJoin; if this is present, we record it + * as part of the join strategy. (However, scan-level Materialize + * nodes are an exception.) + */ + if (elidedinner == NULL && IsA(innerplan, Material) && + !pgpa_is_scan_level_materialize(innerplan)) + { + elidedinner = pgpa_descend_node(pstmt, &innerplan); + strategy = JSTRAT_MERGE_JOIN_MATERIALIZE; + } + else + strategy = JSTRAT_MERGE_JOIN_PLAIN; + + /* + * For a MergeJoin, either the outer or the inner subplan, or + * both, may have needed to be sorted; we must disregard any Sort + * or IncrementalSort node to find the real inner or outer + * subplan. + */ + if (elidedouter == NULL && is_sorting_plan(outerplan)) + elidedouter = pgpa_descend_node(pstmt, &outerplan); + if (elidedinner == NULL && is_sorting_plan(innerplan)) + elidedinner = pgpa_descend_node(pstmt, &innerplan); + break; + + case T_NestLoop: + + /* + * The planner may have chosen to place a Material or Memoize node + * on the inner side of the NestLoop; if this is present, we + * record it as part of the join strategy. (However, scan-level + * Materialize nodes are an exception.) + */ + if (elidedinner == NULL && IsA(innerplan, Material) && + !pgpa_is_scan_level_materialize(innerplan)) + { + elidedinner = pgpa_descend_node(pstmt, &innerplan); + strategy = JSTRAT_NESTED_LOOP_MATERIALIZE; + } + else if (elidedinner == NULL && IsA(innerplan, Memoize)) + { + elidedinner = pgpa_descend_node(pstmt, &innerplan); + strategy = JSTRAT_NESTED_LOOP_MEMOIZE; + } + else + strategy = JSTRAT_NESTED_LOOP_PLAIN; + break; + + case T_HashJoin: + + /* + * The inner subplan of a HashJoin is always a Hash node; the real + * inner subplan is the Hash node's child. + */ + Assert(IsA(innerplan, Hash)); + Assert(elidedinner == NULL); + elidedinner = pgpa_descend_node(pstmt, &innerplan); + strategy = JSTRAT_HASH_JOIN; + break; + + default: + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(plan)); + } + + /* + * The planner may have decided to implement a semijoin by first making + * the nullable side of the plan unique, and then performing a normal join + * against the result. Therefore, we might need to descend through a + * unique node on either side of the plan. + */ + uniqueouter = pgpa_descend_any_unique(pstmt, &outerplan, &elidedouter); + uniqueinner = pgpa_descend_any_unique(pstmt, &innerplan, &elidedinner); + + /* + * Can we see a Result node here, to project above a Gather? So far I've + * found no example that behaves that way; rather, the Gather or Gather + * Merge is made to project. Hence, don't test is_result_node_with_child() + * at this point. + */ + + /* + * The planner may have decided to parallelize part of the join tree, so + * we could find a Gather or Gather Merge node here. Note that, if + * present, this will appear below nodes we considered as part of the join + * strategy, but we could find another uniqueness-enforcing node below the + * Gather or Gather Merge, if present. + */ + if (elidedouter == NULL) + { + elidedouter = pgpa_descend_any_gather(pstmt, &outerplan, + found_any_outer_gather); + if (*found_any_outer_gather && + pgpa_descend_any_unique(pstmt, &outerplan, &elidedouter)) + uniqueouter = true; + } + if (elidedinner == NULL) + { + elidedinner = pgpa_descend_any_gather(pstmt, &innerplan, + found_any_inner_gather); + if (*found_any_inner_gather && + pgpa_descend_any_unique(pstmt, &innerplan, &elidedinner)) + uniqueinner = true; + } + + /* + * It's possible that a Result node has been inserted either to project a + * target list or to implement a one-time filter. If so, we can descend + * through it. Note that a Result node without a child would be a + * degenerate scan or join, and not something we could descend through. + */ + if (elidedouter == NULL && is_result_node_with_child(outerplan)) + elidedouter = pgpa_descend_node(pstmt, &outerplan); + if (elidedinner == NULL && is_result_node_with_child(innerplan)) + elidedinner = pgpa_descend_node(pstmt, &innerplan); + + /* + * If this is a semijoin that was converted to an inner join by making one + * side or the other unique, make a note that the inner or outer subplan, + * as appropriate, should be treated as a query plan feature when the main + * tree traversal reaches it. + * + * Conversely, if the planner could have made one side of the join unique + * and thereby converted it to an inner join, and chose not to do so, that + * is also worth noting. + * + * NB: This code could appear slightly higher up in this function, but + * none of the nodes through which we just descended should have + * associated RTIs. + * + * NB: This seems like a somewhat hacky way of passing information up to + * the main tree walk, but I don't currently have a better idea. + */ + if (uniqueouter) + pgpa_add_future_feature(walker, PGPAQF_SEMIJOIN_UNIQUE, outerplan); + else if (jointype == JOIN_RIGHT_SEMI) + pgpa_add_future_feature(walker, PGPAQF_SEMIJOIN_NON_UNIQUE, outerplan); + if (uniqueinner) + pgpa_add_future_feature(walker, PGPAQF_SEMIJOIN_UNIQUE, innerplan); + else if (jointype == JOIN_SEMI) + pgpa_add_future_feature(walker, PGPAQF_SEMIJOIN_NON_UNIQUE, innerplan); + + /* Set output parameters. */ + *realouter = outerplan; + *realinner = innerplan; + *elidedrealouter = elidedouter; + *elidedrealinner = elidedinner; + return strategy; +} + +/* + * Descend through a Plan node in a join tree that the caller has determined + * to be irrelevant. + * + * Updates *plan, and returns the last of any elided nodes pertaining to the + * new plan node. + */ +static ElidedNode * +pgpa_descend_node(PlannedStmt *pstmt, Plan **plan) +{ + *plan = (*plan)->lefttree; + return pgpa_last_elided_node(pstmt, *plan); +} + +/* + * Descend through a Gather or Gather Merge node, if present, and any Sort + * or IncrementalSort node occurring under a Gather Merge. + * + * Caller should have verified that there is no ElidedNode pertaining to + * the initial value of *plan. + * + * Updates *plan, and returns the last of any elided nodes pertaining to the + * new plan node. Sets *found_any_gather = true if either Gather or + * Gather Merge was found, and otherwise leaves it unchanged. + */ +static ElidedNode * +pgpa_descend_any_gather(PlannedStmt *pstmt, Plan **plan, + bool *found_any_gather) +{ + if (IsA(*plan, Gather)) + { + *found_any_gather = true; + return pgpa_descend_node(pstmt, plan); + } + + if (IsA(*plan, GatherMerge)) + { + ElidedNode *elided = pgpa_descend_node(pstmt, plan); + + if (elided == NULL && is_sorting_plan(*plan)) + elided = pgpa_descend_node(pstmt, plan); + + *found_any_gather = true; + return elided; + } + + return NULL; +} + +/* + * If *plan is an Agg or Unique node, we want to descend through it, unless + * it has a corresponding elided node. If its immediate child is a Sort or + * IncrementalSort, we also want to descend through that, unless it has a + * corresponding elided node. + * + * On entry, *elided_node must be the last of any elided nodes corresponding + * to *plan; on exit, this will still be true, but *plan may have been updated. + * + * The reason we don't want to descend through elided nodes is that a single + * join tree can't cross through any sort of elided node: subqueries are + * planned separately, and planning inside an Append or MergeAppend is + * separate from planning outside of it. + * + * The return value is true if we descend through a node that we believe is + * making one side of a semijoin unique, and otherwise false. + */ +static bool +pgpa_descend_any_unique(PlannedStmt *pstmt, Plan **plan, + ElidedNode **elided_node) +{ + bool descend = false; + bool sjunique = false; + + if (*elided_node != NULL) + return sjunique; + + if (IsA(*plan, Unique)) + { + descend = true; + sjunique = true; + } + else if (IsA(*plan, Agg)) + { + /* + * If this is a simple Agg node, then assume it's here to implement + * semijoin uniqueness. Otherwise, assume it's completing an eager + * aggregation or partitionwise aggregation operation that began at a + * higher level of the plan tree. + * + * (Note that when we're using an Agg node for uniqueness, there's no + * need for any case other than AGGSPLIT_SIMPLE, because there's no + * aggregated column being computed. However, the fact that + * AGGSPLIT_SIMPLE is in use doesn't prove that this Agg is here for + * the semijoin uniqueness. Maybe we should adjust an Agg node to + * carry a "purpose" field so that code like this can be more certain + * of its analysis.) + */ + descend = true; + sjunique = (((Agg *) *plan)->aggsplit == AGGSPLIT_SIMPLE); + } + + if (descend) + { + *elided_node = pgpa_descend_node(pstmt, plan); + + if (*elided_node == NULL && is_sorting_plan(*plan)) + *elided_node = pgpa_descend_node(pstmt, plan); + } + + return sjunique; +} + +/* + * Is this a Result node that has a child? + */ +static bool +is_result_node_with_child(Plan *plan) +{ + return IsA(plan, Result) && plan->lefttree != NULL; +} + +/* + * Is this a Plan node whose purpose is to put the data in a certain order? + */ +static bool +is_sorting_plan(Plan *plan) +{ + return IsA(plan, Sort) || IsA(plan, IncrementalSort); +} diff --git a/contrib/pg_plan_advice/pgpa_join.h b/contrib/pg_plan_advice/pgpa_join.h new file mode 100644 index 0000000000000..db8509c07ccbb --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_join.h @@ -0,0 +1,105 @@ +/*------------------------------------------------------------------------- + * + * pgpa_join.h + * analysis of joins in Plan trees + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_join.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_JOIN_H +#define PGPA_JOIN_H + +#include "nodes/plannodes.h" + +typedef struct pgpa_plan_walker_context pgpa_plan_walker_context; +typedef struct pgpa_join_unroller pgpa_join_unroller; +typedef struct pgpa_unrolled_join pgpa_unrolled_join; + +/* + * Although there are three main join strategies, we try to classify things + * more precisely here: merge joins have the option of using materialization + * on the inner side, and nested loops can use either materialization or + * memoization. + */ +typedef enum +{ + JSTRAT_MERGE_JOIN_PLAIN = 0, + JSTRAT_MERGE_JOIN_MATERIALIZE, + JSTRAT_NESTED_LOOP_PLAIN, + JSTRAT_NESTED_LOOP_MATERIALIZE, + JSTRAT_NESTED_LOOP_MEMOIZE, + JSTRAT_HASH_JOIN + /* update NUM_PGPA_JOIN_STRATEGY if you add anything here */ +} pgpa_join_strategy; + +#define NUM_PGPA_JOIN_STRATEGY ((int) JSTRAT_HASH_JOIN + 1) + +/* + * In an outer-deep join tree, every member of an unrolled join will be a scan, + * but join trees with other shapes can contain unrolled joins. + * + * The plan node we store here will be the inner or outer child of the join + * node, as appropriate, except that we look through subnodes that we regard as + * part of the join method itself. For instance, for a Nested Loop that + * materializes the inner input, we'll store the child of the Materialize node, + * not the Materialize node itself. + * + * If setrefs processing elided one or more nodes from the plan tree, then + * we'll store details about the topmost of those in elided_node; otherwise, + * it will be NULL. + * + * Exactly one of scan and unrolled_join will be non-NULL. + */ +typedef struct +{ + Plan *plan; + ElidedNode *elided_node; + struct pgpa_scan *scan; + pgpa_unrolled_join *unrolled_join; +} pgpa_join_member; + +/* + * We convert outer-deep join trees to a flat structure; that is, ((A JOIN B) + * JOIN C) JOIN D gets converted to outer = A, inner = . When joins + * aren't outer-deep, substructure is required, e.g. (A JOIN B) JOIN (C JOIN D) + * is represented as outer = A, inner = , where X is a pgpa_unrolled_join + * covering C-D. + */ +struct pgpa_unrolled_join +{ + /* Outermost member; must not itself be an unrolled join. */ + pgpa_join_member outer; + + /* Number of inner members. Length of the strategy and inner arrays. */ + unsigned ninner; + + /* Array of strategies, one per non-outermost member. */ + pgpa_join_strategy *strategy; + + /* Array of members, excluding the outermost. Deepest first. */ + pgpa_join_member *inner; +}; + +/* + * Does this plan node inherit from Join? + */ +static inline bool +pgpa_is_join(Plan *plan) +{ + return IsA(plan, NestLoop) || IsA(plan, MergeJoin) || IsA(plan, HashJoin); +} + +extern pgpa_join_unroller *pgpa_create_join_unroller(void); +extern void pgpa_unroll_join(pgpa_plan_walker_context *walker, + Plan *plan, bool beneath_any_gather, + pgpa_join_unroller *join_unroller, + pgpa_join_unroller **outer_join_unroller, + pgpa_join_unroller **inner_join_unroller); +extern pgpa_unrolled_join *pgpa_build_unrolled_join(pgpa_plan_walker_context *walker, + pgpa_join_unroller *join_unroller); +extern void pgpa_destroy_join_unroller(pgpa_join_unroller *join_unroller); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_output.c b/contrib/pg_plan_advice/pgpa_output.c new file mode 100644 index 0000000000000..ffced69664a26 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_output.c @@ -0,0 +1,606 @@ +/*------------------------------------------------------------------------- + * + * pgpa_output.c + * produce textual output from the results of a plan tree walk + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_output.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "pgpa_output.h" +#include "pgpa_scan.h" + +#include "nodes/parsenodes.h" +#include "parser/parsetree.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +/* + * Context object for textual advice generation. + * + * rt_identifiers is the caller-provided array of range table identifiers. + * See the comments at the top of pgpa_identifier.c for more details. + * + * buf is the caller-provided output buffer. + * + * wrap_column is the wrap column, so that we don't create output that is + * too wide. See pgpa_maybe_linebreak() and comments in pgpa_output_advice. + */ +typedef struct pgpa_output_context +{ + const char **rid_strings; + StringInfo buf; + int wrap_column; +} pgpa_output_context; + +static void pgpa_output_unrolled_join(pgpa_output_context *context, + pgpa_unrolled_join *join); +static void pgpa_output_join_member(pgpa_output_context *context, + pgpa_join_member *member); +static void pgpa_output_scan_strategy(pgpa_output_context *context, + pgpa_scan_strategy strategy, + List *scans); +static void pgpa_output_relation_name(pgpa_output_context *context, Oid relid); +static void pgpa_output_query_feature(pgpa_output_context *context, + pgpa_qf_type type, + List *query_features); +static void pgpa_output_simple_strategy(pgpa_output_context *context, + char *strategy, + List *relid_sets); +static void pgpa_output_no_gather(pgpa_output_context *context, + Bitmapset *relids); +static void pgpa_output_do_not_scan(pgpa_output_context *context, + List *identifiers); +static void pgpa_output_relations(pgpa_output_context *context, StringInfo buf, + Bitmapset *relids); + +static char *pgpa_cstring_join_strategy(pgpa_join_strategy strategy); +static char *pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy); +static char *pgpa_cstring_query_feature_type(pgpa_qf_type type); + +static void pgpa_maybe_linebreak(StringInfo buf, int wrap_column); + +/* + * Append query advice to the provided buffer. + * + * Before calling this function, 'walker' must be used to iterate over the + * main plan tree and all subplans from the PlannedStmt. + * + * 'rt_identifiers' is a table of unique identifiers, one for each RTI. + * See pgpa_create_identifiers_for_planned_stmt(). + * + * Results will be appended to 'buf'. + */ +void +pgpa_output_advice(StringInfo buf, pgpa_plan_walker_context *walker, + pgpa_identifier *rt_identifiers) +{ + Index rtable_length = list_length(walker->pstmt->rtable); + ListCell *lc; + pgpa_output_context context; + + /* Basic initialization. */ + memset(&context, 0, sizeof(pgpa_output_context)); + context.buf = buf; + + /* + * Convert identifiers to string form. Note that the loop variable here is + * not an RTI, because RTIs are 1-based. Some RTIs will have no + * identifier, either because the reloptkind is RTE_JOIN or because that + * portion of the query didn't make it into the final plan. + */ + context.rid_strings = palloc0_array(const char *, rtable_length); + for (int i = 0; i < rtable_length; ++i) + if (rt_identifiers[i].alias_name != NULL) + context.rid_strings[i] = pgpa_identifier_string(&rt_identifiers[i]); + + /* + * If the user chooses to use EXPLAIN (PLAN_ADVICE) in an 80-column window + * from a psql client with default settings, psql will add one space to + * the left of the output and EXPLAIN will add two more to the left of the + * advice. Thus, lines of more than 77 characters will wrap. We set the + * wrap limit to 76 here so that the output won't reach all the way to the + * very last column of the terminal. + * + * Of course, this is fairly arbitrary set of assumptions, and one could + * well make an argument for a different wrap limit, or for a configurable + * one. + */ + context.wrap_column = 76; + + /* + * Each piece of JOIN_ORDER() advice fully describes the join order for a + * single unrolled join. Merging is not permitted, because that would + * change the meaning, e.g. SEQ_SCAN(a b c d) means simply that sequential + * scans should be used for all of those relations, and is thus equivalent + * to SEQ_SCAN(a b) SEQ_SCAN(c d), but JOIN_ORDER(a b c d) means that "a" + * is the driving table which is then joined to "b" then "c" then "d", + * which is totally different from JOIN_ORDER(a b) and JOIN_ORDER(c d). + */ + foreach(lc, walker->toplevel_unrolled_joins) + { + pgpa_unrolled_join *ujoin = lfirst(lc); + + if (buf->len > 0) + appendStringInfoChar(buf, '\n'); + appendStringInfoString(context.buf, "JOIN_ORDER("); + pgpa_output_unrolled_join(&context, ujoin); + appendStringInfoChar(context.buf, ')'); + pgpa_maybe_linebreak(context.buf, context.wrap_column); + } + + /* Emit join strategy advice. */ + for (int s = 0; s < NUM_PGPA_JOIN_STRATEGY; ++s) + { + char *strategy = pgpa_cstring_join_strategy(s); + + pgpa_output_simple_strategy(&context, + strategy, + walker->join_strategies[s]); + } + + /* + * Emit scan strategy advice (but not for ordinary scans, which are + * definitionally uninteresting). + */ + for (int c = 0; c < NUM_PGPA_SCAN_STRATEGY; ++c) + if (c != PGPA_SCAN_ORDINARY) + pgpa_output_scan_strategy(&context, c, walker->scans[c]); + + /* Emit query feature advice. */ + for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t) + pgpa_output_query_feature(&context, t, walker->query_features[t]); + + /* Emit NO_GATHER advice. */ + pgpa_output_no_gather(&context, walker->no_gather_scans); + + /* Emit DO_NOT_SCAN advice. */ + pgpa_output_do_not_scan(&context, walker->do_not_scan_identifiers); +} + +/* + * Output the members of an unrolled join, first the outermost member, and + * then the inner members one by one, as part of JOIN_ORDER() advice. + */ +static void +pgpa_output_unrolled_join(pgpa_output_context *context, + pgpa_unrolled_join *join) +{ + pgpa_output_join_member(context, &join->outer); + + for (int k = 0; k < join->ninner; ++k) + { + pgpa_join_member *member = &join->inner[k]; + + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + pgpa_output_join_member(context, member); + } +} + +/* + * Output a single member of an unrolled join as part of JOIN_ORDER() advice. + */ +static void +pgpa_output_join_member(pgpa_output_context *context, + pgpa_join_member *member) +{ + if (member->unrolled_join != NULL) + { + appendStringInfoChar(context->buf, '('); + pgpa_output_unrolled_join(context, member->unrolled_join); + appendStringInfoChar(context->buf, ')'); + } + else + { + pgpa_scan *scan = member->scan; + + Assert(scan != NULL); + if (bms_membership(scan->relids) == BMS_SINGLETON) + pgpa_output_relations(context, context->buf, scan->relids); + else + { + appendStringInfoChar(context->buf, '{'); + pgpa_output_relations(context, context->buf, scan->relids); + appendStringInfoChar(context->buf, '}'); + } + } +} + +/* + * Output advice for a List of pgpa_scan objects. + * + * All the scans must use the strategy specified by the "strategy" argument. + */ +static void +pgpa_output_scan_strategy(pgpa_output_context *context, + pgpa_scan_strategy strategy, + List *scans) +{ + bool first = true; + + if (scans == NIL) + return; + + if (context->buf->len > 0) + appendStringInfoChar(context->buf, '\n'); + appendStringInfo(context->buf, "%s(", + pgpa_cstring_scan_strategy(strategy)); + + foreach_ptr(pgpa_scan, scan, scans) + { + Plan *plan = scan->plan; + + if (first) + first = false; + else + { + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + } + + /* Output the relation identifiers. */ + if (bms_membership(scan->relids) == BMS_SINGLETON) + pgpa_output_relations(context, context->buf, scan->relids); + else + { + appendStringInfoChar(context->buf, '('); + pgpa_output_relations(context, context->buf, scan->relids); + appendStringInfoChar(context->buf, ')'); + } + + /* For index or index-only scans, output index information. */ + if (strategy == PGPA_SCAN_INDEX) + { + Assert(IsA(plan, IndexScan)); + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + pgpa_output_relation_name(context, ((IndexScan *) plan)->indexid); + } + else if (strategy == PGPA_SCAN_INDEX_ONLY) + { + Assert(IsA(plan, IndexOnlyScan)); + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + pgpa_output_relation_name(context, + ((IndexOnlyScan *) plan)->indexid); + } + } + + appendStringInfoChar(context->buf, ')'); + pgpa_maybe_linebreak(context->buf, context->wrap_column); +} + +/* + * Output a schema-qualified relation name. + */ +static void +pgpa_output_relation_name(pgpa_output_context *context, Oid relid) +{ + Oid nspoid = get_rel_namespace(relid); + char *relnamespace = get_namespace_name_or_temp(nspoid); + char *relname = get_rel_name(relid); + + appendStringInfoString(context->buf, quote_identifier(relnamespace)); + appendStringInfoChar(context->buf, '.'); + appendStringInfoString(context->buf, quote_identifier(relname)); +} + +/* + * Output advice for a List of pgpa_query_feature objects. + * + * All features must be of the type specified by the "type" argument. + */ +static void +pgpa_output_query_feature(pgpa_output_context *context, pgpa_qf_type type, + List *query_features) +{ + bool first = true; + + if (query_features == NIL) + return; + + if (context->buf->len > 0) + appendStringInfoChar(context->buf, '\n'); + appendStringInfo(context->buf, "%s(", + pgpa_cstring_query_feature_type(type)); + + foreach_ptr(pgpa_query_feature, qf, query_features) + { + if (first) + first = false; + else + { + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + } + + if (bms_membership(qf->relids) == BMS_SINGLETON) + pgpa_output_relations(context, context->buf, qf->relids); + else + { + appendStringInfoChar(context->buf, '('); + pgpa_output_relations(context, context->buf, qf->relids); + appendStringInfoChar(context->buf, ')'); + } + } + + appendStringInfoChar(context->buf, ')'); + pgpa_maybe_linebreak(context->buf, context->wrap_column); +} + +/* + * Output "simple" advice for a List of Bitmapset objects each of which + * contains one or more RTIs. + * + * By simple, we just mean that the advice emitted follows the most + * straightforward pattern: the strategy name, followed by a list of items + * separated by spaces and surrounded by parentheses. Individual items in + * the list are a single relation identifier for a Bitmapset that contains + * just one member, or a sub-list again separated by spaces and surrounded + * by parentheses for a Bitmapset with multiple members. Bitmapsets with + * no members probably shouldn't occur here, but if they do they'll be + * rendered as an empty sub-list. + */ +static void +pgpa_output_simple_strategy(pgpa_output_context *context, char *strategy, + List *relid_sets) +{ + bool first = true; + + if (relid_sets == NIL) + return; + + if (context->buf->len > 0) + appendStringInfoChar(context->buf, '\n'); + appendStringInfo(context->buf, "%s(", strategy); + + foreach_node(Bitmapset, relids, relid_sets) + { + if (first) + first = false; + else + { + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + } + + if (bms_membership(relids) == BMS_SINGLETON) + pgpa_output_relations(context, context->buf, relids); + else + { + appendStringInfoChar(context->buf, '('); + pgpa_output_relations(context, context->buf, relids); + appendStringInfoChar(context->buf, ')'); + } + } + + appendStringInfoChar(context->buf, ')'); + pgpa_maybe_linebreak(context->buf, context->wrap_column); +} + +/* + * Output NO_GATHER advice for all relations not appearing beneath any + * Gather or Gather Merge node. + */ +static void +pgpa_output_no_gather(pgpa_output_context *context, Bitmapset *relids) +{ + if (relids == NULL) + return; + if (context->buf->len > 0) + appendStringInfoChar(context->buf, '\n'); + appendStringInfoString(context->buf, "NO_GATHER("); + pgpa_output_relations(context, context->buf, relids); + appendStringInfoChar(context->buf, ')'); +} + +/* + * Output DO_NOT_SCAN advice for all relations in the provided list of + * identifiers. + */ +static void +pgpa_output_do_not_scan(pgpa_output_context *context, List *identifiers) +{ + bool first = true; + + if (identifiers == NIL) + return; + if (context->buf->len > 0) + appendStringInfoChar(context->buf, '\n'); + appendStringInfoString(context->buf, "DO_NOT_SCAN("); + + foreach_ptr(pgpa_identifier, rid, identifiers) + { + if (first) + first = false; + else + { + pgpa_maybe_linebreak(context->buf, context->wrap_column); + appendStringInfoChar(context->buf, ' '); + } + appendStringInfoString(context->buf, pgpa_identifier_string(rid)); + } + + appendStringInfoChar(context->buf, ')'); +} + +/* + * Output the identifiers for each RTI in the provided set. + * + * Identifiers are separated by spaces, and a line break is possible after + * each one. + */ +static void +pgpa_output_relations(pgpa_output_context *context, StringInfo buf, + Bitmapset *relids) +{ + int rti = -1; + bool first = true; + + while ((rti = bms_next_member(relids, rti)) >= 0) + { + const char *rid_string = context->rid_strings[rti - 1]; + + if (rid_string == NULL) + elog(ERROR, "no identifier for RTI %d", rti); + + if (first) + { + first = false; + appendStringInfoString(buf, rid_string); + } + else + { + pgpa_maybe_linebreak(buf, context->wrap_column); + appendStringInfo(buf, " %s", rid_string); + } + } +} + +/* + * Get a C string that corresponds to the specified join strategy. + */ +static char * +pgpa_cstring_join_strategy(pgpa_join_strategy strategy) +{ + switch (strategy) + { + case JSTRAT_MERGE_JOIN_PLAIN: + return "MERGE_JOIN_PLAIN"; + case JSTRAT_MERGE_JOIN_MATERIALIZE: + return "MERGE_JOIN_MATERIALIZE"; + case JSTRAT_NESTED_LOOP_PLAIN: + return "NESTED_LOOP_PLAIN"; + case JSTRAT_NESTED_LOOP_MATERIALIZE: + return "NESTED_LOOP_MATERIALIZE"; + case JSTRAT_NESTED_LOOP_MEMOIZE: + return "NESTED_LOOP_MEMOIZE"; + case JSTRAT_HASH_JOIN: + return "HASH_JOIN"; + } + + pg_unreachable(); + return NULL; +} + +/* + * Get a C string that corresponds to the specified scan strategy. + */ +static char * +pgpa_cstring_scan_strategy(pgpa_scan_strategy strategy) +{ + switch (strategy) + { + case PGPA_SCAN_ORDINARY: + return "ORDINARY_SCAN"; + case PGPA_SCAN_SEQ: + return "SEQ_SCAN"; + case PGPA_SCAN_BITMAP_HEAP: + return "BITMAP_HEAP_SCAN"; + case PGPA_SCAN_FOREIGN: + return "FOREIGN_JOIN"; + case PGPA_SCAN_INDEX: + return "INDEX_SCAN"; + case PGPA_SCAN_INDEX_ONLY: + return "INDEX_ONLY_SCAN"; + case PGPA_SCAN_PARTITIONWISE: + return "PARTITIONWISE"; + case PGPA_SCAN_TID: + return "TID_SCAN"; + } + + pg_unreachable(); + return NULL; +} + +/* + * Get a C string that corresponds to the query feature type. + */ +static char * +pgpa_cstring_query_feature_type(pgpa_qf_type type) +{ + switch (type) + { + case PGPAQF_GATHER: + return "GATHER"; + case PGPAQF_GATHER_MERGE: + return "GATHER_MERGE"; + case PGPAQF_SEMIJOIN_NON_UNIQUE: + return "SEMIJOIN_NON_UNIQUE"; + case PGPAQF_SEMIJOIN_UNIQUE: + return "SEMIJOIN_UNIQUE"; + } + + + pg_unreachable(); + return NULL; +} + +/* + * Insert a line break into the StringInfoData, if needed. + * + * If wrap_column is zero or negative, this does nothing. Otherwise, we + * consider inserting a newline. We only insert a newline if the length of + * the last line in the buffer exceeds wrap_column, and not if we'd be + * inserting a newline at or before the beginning of the current line. + * + * The position at which the newline is inserted is simply wherever the + * buffer ended the last time this function was called. In other words, + * the caller is expected to call this function every time we reach a good + * place for a line break. + */ +static void +pgpa_maybe_linebreak(StringInfo buf, int wrap_column) +{ + char *trailing_nl; + int line_start; + int save_cursor; + + /* If line wrapping is disabled, exit quickly. */ + if (wrap_column <= 0) + return; + + /* + * Set line_start to the byte offset within buf->data of the first + * character of the current line, where the current line means the last + * one in the buffer. Note that line_start could be the offset of the + * trailing '\0' if the last character in the buffer is a line break. + */ + trailing_nl = strrchr(buf->data, '\n'); + if (trailing_nl == NULL) + line_start = 0; + else + line_start = (trailing_nl - buf->data) + 1; + + /* + * Remember that the current end of the buffer is a potential location to + * insert a line break on a future call to this function. + */ + save_cursor = buf->cursor; + buf->cursor = buf->len; + + /* If we haven't passed the wrap column, we don't need a newline. */ + if (buf->len - line_start <= wrap_column) + return; + + /* + * It only makes sense to insert a newline at a position later than the + * beginning of the current line. + */ + if (save_cursor <= line_start) + return; + + /* Insert a newline at the previous cursor location. */ + enlargeStringInfo(buf, 1); + memmove(&buf->data[save_cursor] + 1, &buf->data[save_cursor], + buf->len - save_cursor); + ++buf->cursor; + buf->data[++buf->len] = '\0'; + buf->data[save_cursor] = '\n'; +} diff --git a/contrib/pg_plan_advice/pgpa_output.h b/contrib/pg_plan_advice/pgpa_output.h new file mode 100644 index 0000000000000..fd35103bfbd3b --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_output.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * pgpa_output.h + * produce textual output from the results of a plan tree walk + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_output.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_OUTPUT_H +#define PGPA_OUTPUT_H + +#include "pgpa_identifier.h" +#include "pgpa_walker.h" + +extern void pgpa_output_advice(StringInfo buf, + pgpa_plan_walker_context *walker, + pgpa_identifier *rt_identifiers); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_parser.y b/contrib/pg_plan_advice/pgpa_parser.y new file mode 100644 index 0000000000000..598974d3f226d --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_parser.y @@ -0,0 +1,301 @@ +%{ +/* + * Parser for plan advice + * + * Copyright (c) 2000-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_parser.y + */ + +#include "postgres.h" + +#include +#include + +#include "fmgr.h" +#include "nodes/miscnodes.h" +#include "utils/builtins.h" +#include "utils/float.h" + +#include "pgpa_ast.h" +#include "pgpa_parser.h" + +/* + * Bison doesn't allocate anything that needs to live across parser calls, + * so we can easily have it use palloc instead of malloc. This prevents + * memory leaks if we error out during parsing. + */ +#define YYMALLOC palloc +#define YYFREE pfree +%} + +/* BISON Declarations */ +%parse-param {List **result} +%parse-param {char **parse_error_msg_p} +%parse-param {yyscan_t yyscanner} +%lex-param {List **result} +%lex-param {char **parse_error_msg_p} +%lex-param {yyscan_t yyscanner} +%pure-parser +%expect 0 +%name-prefix="pgpa_yy" + +%union +{ + char *str; + int integer; + List *list; + pgpa_advice_item *item; + pgpa_advice_target *target; + pgpa_index_target *itarget; +} +%token TOK_IDENT TOK_TAG_JOIN_ORDER TOK_TAG_INDEX +%token TOK_TAG_SIMPLE TOK_TAG_GENERIC +%token TOK_INTEGER + +%type opt_ri_occurrence +%type advice_item +%type advice_item_list generic_target_list +%type index_target_list join_order_target_list +%type opt_partition simple_target_list +%type identifier opt_plan_name +%type generic_sublist join_order_sublist +%type relation_identifier +%type index_name + +%start parse_toplevel + +/* Grammar follows */ +%% + +parse_toplevel: advice_item_list + { + (void) yynerrs; /* suppress compiler warning */ + *result = $1; + } + ; + +advice_item_list: advice_item_list advice_item + { $$ = lappend($1, $2); } + | + { $$ = NIL; } + ; + +advice_item: TOK_TAG_JOIN_ORDER '(' join_order_target_list ')' + { + $$ = palloc0_object(pgpa_advice_item); + $$->tag = PGPA_TAG_JOIN_ORDER; + $$->targets = $3; + if ($3 == NIL) + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "JOIN_ORDER must have at least one target"); + } + | TOK_TAG_INDEX '(' index_target_list ')' + { + $$ = palloc0_object(pgpa_advice_item); + if (strcmp($1, "index_only_scan") == 0) + $$->tag = PGPA_TAG_INDEX_ONLY_SCAN; + else if (strcmp($1, "index_scan") == 0) + $$->tag = PGPA_TAG_INDEX_SCAN; + else + elog(ERROR, "tag parsing failed: %s", $1); + $$->targets = $3; + } + | TOK_TAG_SIMPLE '(' simple_target_list ')' + { + $$ = palloc0_object(pgpa_advice_item); + if (strcmp($1, "bitmap_heap_scan") == 0) + $$->tag = PGPA_TAG_BITMAP_HEAP_SCAN; + else if (strcmp($1, "no_gather") == 0) + $$->tag = PGPA_TAG_NO_GATHER; + else if (strcmp($1, "seq_scan") == 0) + $$->tag = PGPA_TAG_SEQ_SCAN; + else if (strcmp($1, "tid_scan") == 0) + $$->tag = PGPA_TAG_TID_SCAN; + else + elog(ERROR, "tag parsing failed: %s", $1); + $$->targets = $3; + } + | TOK_TAG_GENERIC '(' generic_target_list ')' + { + bool fail; + + $$ = palloc0_object(pgpa_advice_item); + $$->tag = pgpa_parse_advice_tag($1, &fail); + if (fail) + { + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "unrecognized advice tag"); + } + + if ($$->tag == PGPA_TAG_FOREIGN_JOIN) + { + foreach_ptr(pgpa_advice_target, target, $3) + { + if (target->ttype == PGPA_TARGET_IDENTIFIER || + list_length(target->children) == 1) + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "FOREIGN_JOIN targets must contain more than one relation identifier"); + } + } + + $$->targets = $3; + } + ; + +relation_identifier: identifier opt_ri_occurrence opt_partition opt_plan_name + { + $$ = palloc0_object(pgpa_advice_target); + $$->ttype = PGPA_TARGET_IDENTIFIER; + $$->rid.alias_name = $1; + $$->rid.occurrence = $2; + if (list_length($3) == 2) + { + $$->rid.partnsp = linitial($3); + $$->rid.partrel = lsecond($3); + } + else if ($3 != NIL) + $$->rid.partrel = linitial($3); + $$->rid.plan_name = $4; + } + ; + +index_name: identifier + { + $$ = palloc0_object(pgpa_index_target); + $$->indname = $1; + } + | identifier '.' identifier + { + $$ = palloc0_object(pgpa_index_target); + $$->indnamespace = $1; + $$->indname = $3; + } + ; + +opt_ri_occurrence: + '#' TOK_INTEGER + { + if ($2 <= 0) + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "only positive occurrence numbers are permitted"); + $$ = $2; + } + | + { + /* The default occurrence number is 1. */ + $$ = 1; + } + ; + +identifier: TOK_IDENT + | TOK_TAG_JOIN_ORDER + | TOK_TAG_INDEX + | TOK_TAG_SIMPLE + | TOK_TAG_GENERIC + ; + +/* + * When generating advice, we always schema-qualify the partition name, but + * when parsing advice, we accept a specification that lacks one. + */ +opt_partition: + '/' identifier '.' identifier + { $$ = list_make2($2, $4); } + | '/' identifier + { $$ = list_make1($2); } + | + { $$ = NIL; } + ; + +opt_plan_name: + '@' identifier + { $$ = $2; } + | + { $$ = NULL; } + ; + +generic_target_list: generic_target_list relation_identifier + { $$ = lappend($1, $2); } + | generic_target_list generic_sublist + { $$ = lappend($1, $2); } + | + { $$ = NIL; } + ; + +generic_sublist: '(' simple_target_list ')' + { + $$ = palloc0_object(pgpa_advice_target); + $$->ttype = PGPA_TARGET_ORDERED_LIST; + $$->children = $2; + } + ; + +index_target_list: + index_target_list relation_identifier index_name + { + $2->itarget = $3; + $$ = lappend($1, $2); + } + | + { $$ = NIL; } + ; + +join_order_target_list: join_order_target_list relation_identifier + { $$ = lappend($1, $2); } + | join_order_target_list join_order_sublist + { $$ = lappend($1, $2); } + | + { $$ = NIL; } + ; + +join_order_sublist: + '(' join_order_target_list ')' + { + $$ = palloc0_object(pgpa_advice_target); + $$->ttype = PGPA_TARGET_ORDERED_LIST; + $$->children = $2; + } + | '{' simple_target_list '}' + { + $$ = palloc0_object(pgpa_advice_target); + $$->ttype = PGPA_TARGET_UNORDERED_LIST; + $$->children = $2; + } + ; + +simple_target_list: simple_target_list relation_identifier + { $$ = lappend($1, $2); } + | + { $$ = NIL; } + ; + +%% + +/* + * Parse an advice_string and return the resulting list of pgpa_advice_item + * objects. If a parse error occurs, instead return NULL. + * + * If the return value is NULL, *error_p will be set to the error message; + * otherwise, *error_p will be set to NULL. + */ +List * +pgpa_parse(const char *advice_string, char **error_p) +{ + yyscan_t scanner; + List *result; + char *error = NULL; + + pgpa_scanner_init(advice_string, &scanner); + pgpa_yyparse(&result, &error, scanner); + pgpa_scanner_finish(scanner); + + if (error != NULL) + { + *error_p = error; + return NULL; + } + + *error_p = NULL; + return result; +} diff --git a/contrib/pg_plan_advice/pgpa_planner.c b/contrib/pg_plan_advice/pgpa_planner.c new file mode 100644 index 0000000000000..b3329b793aa8e --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_planner.c @@ -0,0 +1,2229 @@ +/*------------------------------------------------------------------------- + * + * pgpa_planner.c + * Use planner hooks to observe and modify planner behavior + * + * All interaction with the core planner happens here. Much of it has to + * do with enforcing supplied advice, but we also need these hooks to + * generate advice strings (though the heavy lifting in that case is + * mostly done by pgpa_walker.c). + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_planner.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "pg_plan_advice.h" +#include "pgpa_identifier.h" +#include "pgpa_output.h" +#include "pgpa_planner.h" +#include "pgpa_trove.h" +#include "pgpa_walker.h" + +#include "commands/defrem.h" +#include "common/hashfn_unstable.h" +#include "nodes/makefuncs.h" +#include "optimizer/extendplan.h" +#include "optimizer/pathnode.h" +#include "optimizer/paths.h" +#include "optimizer/plancat.h" +#include "optimizer/planner.h" +#include "parser/parsetree.h" +#include "utils/lsyscache.h" + +typedef enum pgpa_jo_outcome +{ + PGPA_JO_PERMITTED, /* permit this join order */ + PGPA_JO_DENIED, /* deny this join order */ + PGPA_JO_INDIFFERENT /* do neither */ +} pgpa_jo_outcome; + +typedef struct pgpa_planner_state +{ + MemoryContext mcxt; + bool generate_advice_feedback; + bool generate_advice_string; + pgpa_trove *trove; + List *proots; + pgpa_planner_info *last_proot; +} pgpa_planner_state; + +typedef struct pgpa_join_state +{ + /* Most-recently-considered outer rel. */ + RelOptInfo *outerrel; + + /* Most-recently-considered inner rel. */ + RelOptInfo *innerrel; + + /* + * Array of relation identifiers for all members of this joinrel, with + * outerrel identifiers before innerrel identifiers. + */ + pgpa_identifier *rids; + + /* Number of outer rel identifiers. */ + int outer_count; + + /* Number of inner rel identifiers. */ + int inner_count; + + /* + * Trove lookup results. + * + * join_entries and rel_entries are arrays of entries, and join_indexes + * and rel_indexes are the integer offsets within those arrays of entries + * potentially relevant to us. The "join" fields correspond to a lookup + * using PGPA_TROVE_LOOKUP_JOIN and the "rel" fields to a lookup using + * PGPA_TROVE_LOOKUP_REL. + */ + pgpa_trove_entry *join_entries; + Bitmapset *join_indexes; + pgpa_trove_entry *rel_entries; + Bitmapset *rel_indexes; +} pgpa_join_state; + +/* Saved hook values */ +static build_simple_rel_hook_type prev_build_simple_rel = NULL; +static join_path_setup_hook_type prev_join_path_setup = NULL; +static joinrel_setup_hook_type prev_joinrel_setup = NULL; +static planner_setup_hook_type prev_planner_setup = NULL; +static planner_shutdown_hook_type prev_planner_shutdown = NULL; + +/* Other global variables */ +int pgpa_planner_generate_advice = 0; +static int planner_extension_id = -1; + +/* Function prototypes. */ +static void pgpa_planner_setup(PlannerGlobal *glob, Query *parse, + const char *query_string, + int cursorOptions, + double *tuple_fraction, + ExplainState *es); +static void pgpa_planner_shutdown(PlannerGlobal *glob, Query *parse, + const char *query_string, PlannedStmt *pstmt); +static void pgpa_build_simple_rel(PlannerInfo *root, + RelOptInfo *rel, + RangeTblEntry *rte); +static void pgpa_joinrel_setup(PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + SpecialJoinInfo *sjinfo, + List *restrictlist); +static void pgpa_join_path_setup(PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + JoinType jointype, + JoinPathExtraData *extra); +static pgpa_join_state *pgpa_get_join_state(PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel); +static void pgpa_planner_apply_joinrel_advice(uint64 *pgs_mask_p, + char *plan_name, + pgpa_join_state *pjs); +static void pgpa_planner_apply_join_path_advice(JoinType jointype, + uint64 *pgs_mask_p, + char *plan_name, + pgpa_join_state *pjs); +static void pgpa_planner_apply_scan_advice(RelOptInfo *rel, + pgpa_trove_entry *scan_entries, + Bitmapset *scan_indexes, + pgpa_trove_entry *rel_entries, + Bitmapset *rel_indexes); +static uint64 pgpa_join_strategy_mask_from_advice_tag(pgpa_advice_tag_type tag); +static pgpa_jo_outcome pgpa_join_order_permits_join(int outer_count, + int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry); +static bool pgpa_join_method_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry, + bool *restrict_method); +static bool pgpa_opaque_join_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry, + bool *restrict_method); +static bool pgpa_semijoin_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry, + bool outer_is_nullable, + bool *restrict_method); + +static List *pgpa_planner_append_feedback(List *list, pgpa_trove *trove, + pgpa_trove_lookup_type type, + pgpa_identifier *rt_identifiers, + pgpa_plan_walker_context *walker); + +static pgpa_planner_info *pgpa_planner_get_proot(pgpa_planner_state *pps, + PlannerInfo *root); + +static inline void pgpa_compute_rt_identifier(pgpa_planner_info *proot, + PlannerInfo *root, + RelOptInfo *rel); +static void pgpa_compute_rt_offsets(pgpa_planner_state *pps, + PlannedStmt *pstmt); +static void pgpa_validate_rt_identifiers(pgpa_planner_state *pps, + PlannedStmt *pstmt); + +static char *pgpa_bms_to_cstring(Bitmapset *bms); +static const char *pgpa_jointype_to_cstring(JoinType jointype); + +/* + * Install planner-related hooks. + */ +void +pgpa_planner_install_hooks(void) +{ + planner_extension_id = GetPlannerExtensionId("pg_plan_advice"); + prev_planner_setup = planner_setup_hook; + planner_setup_hook = pgpa_planner_setup; + prev_planner_shutdown = planner_shutdown_hook; + planner_shutdown_hook = pgpa_planner_shutdown; + prev_build_simple_rel = build_simple_rel_hook; + build_simple_rel_hook = pgpa_build_simple_rel; + prev_joinrel_setup = joinrel_setup_hook; + joinrel_setup_hook = pgpa_joinrel_setup; + prev_join_path_setup = join_path_setup_hook; + join_path_setup_hook = pgpa_join_path_setup; +} + +/* + * Carry out whatever setup work we need to do before planning. + */ +static void +pgpa_planner_setup(PlannerGlobal *glob, Query *parse, const char *query_string, + int cursorOptions, double *tuple_fraction, + ExplainState *es) +{ + pgpa_trove *trove = NULL; + pgpa_planner_state *pps; + char *supplied_advice; + bool generate_advice_feedback = false; + bool generate_advice_string = false; + bool needs_pps = false; + + /* + * Decide whether we need to generate an advice string. We must do this if + * the user has told us to do it categorically, or if another loadable + * module has requested it, or if the user has requested it using the + * EXPLAIN (PLAN_ADVICE) option. + */ + generate_advice_string = (pg_plan_advice_always_store_advice_details || + pgpa_planner_generate_advice || + pg_plan_advice_should_explain(es)); + if (generate_advice_string) + needs_pps = true; + + /* + * If any advice was provided, build a trove of advice for use during + * planning. + */ + supplied_advice = pg_plan_advice_get_supplied_query_advice(glob, parse, + query_string, + cursorOptions, + es); + if (supplied_advice != NULL && supplied_advice[0] != '\0') + { + List *advice_items; + char *error; + + /* + * If the supplied advice string comes from pg_plan_advice.advice, + * parsing shouldn't fail here, because we must have previously parsed + * successfully in pg_plan_advice_advice_check_hook. However, it might + * also come from a hook registered via pg_plan_advice_add_advisor, + * and we can't be sure whether that's valid. (Plus, having an error + * check here seems like a good idea anyway, just for safety.) + */ + advice_items = pgpa_parse(supplied_advice, &error); + if (error) + ereport(WARNING, + errmsg("could not parse supplied advice: %s", error)); + + /* + * It's possible that the advice string was non-empty but contained no + * actual advice, e.g. it was all whitespace. + */ + if (advice_items != NIL) + { + trove = pgpa_build_trove(advice_items); + needs_pps = true; + + /* + * If we know that we're running under EXPLAIN, or if the user has + * told us to always do the work, generate advice feedback. + */ + if (es != NULL || pg_plan_advice_feedback_warnings || + pg_plan_advice_always_store_advice_details) + generate_advice_feedback = true; + } + } + + /* + * We only create and initialize a private state object if it's needed for + * some purpose. That could be (1) recording that we will need to generate + * an advice string or (2) storing a trove of supplied advice. + * + * Currently, the active memory context should be one that will last for + * the entire duration of query planning, but if GEQO is in use, it's + * possible that some of our callbacks may be invoked later with + * CurrentMemoryContext set to some shorter-lived context. So, record the + * context that should be used for allocations that need to live as long + * as the pgpa_planner_state itself. + */ + if (needs_pps) + { + pps = palloc0_object(pgpa_planner_state); + pps->mcxt = CurrentMemoryContext; + pps->generate_advice_feedback = generate_advice_feedback; + pps->generate_advice_string = generate_advice_string; + pps->trove = trove; + SetPlannerGlobalExtensionState(glob, planner_extension_id, pps); + } + + /* Pass call to previous hook. */ + if (prev_planner_setup) + (*prev_planner_setup) (glob, parse, query_string, cursorOptions, + tuple_fraction, es); +} + +/* + * Carry out whatever work we want to do after planning is complete. + */ +static void +pgpa_planner_shutdown(PlannerGlobal *glob, Query *parse, + const char *query_string, PlannedStmt *pstmt) +{ + pgpa_planner_state *pps; + pgpa_trove *trove = NULL; + pgpa_plan_walker_context walker = {0}; /* placate compiler */ + bool generate_advice_feedback = false; + bool generate_advice_string = false; + List *pgpa_items = NIL; + pgpa_identifier *rt_identifiers = NULL; + + /* Fetch our private state, set up by pgpa_planner_setup(). */ + pps = GetPlannerGlobalExtensionState(glob, planner_extension_id); + if (pps != NULL) + { + /* Set up some local variables. */ + trove = pps->trove; + generate_advice_feedback = pps->generate_advice_feedback; + generate_advice_string = pps->generate_advice_string; + + /* Compute range table offsets. */ + pgpa_compute_rt_offsets(pps, pstmt); + + /* Cross-check range table identifiers. */ + pgpa_validate_rt_identifiers(pps, pstmt); + } + + /* + * If we're trying to generate an advice string or if we're trying to + * provide advice feedback, then we will need to create range table + * identifiers. + */ + if (generate_advice_string || generate_advice_feedback) + { + pgpa_plan_walker(&walker, pstmt, pps->proots); + rt_identifiers = pgpa_create_identifiers_for_planned_stmt(pstmt); + } + + /* Generate the advice string, if we need to do so. */ + if (generate_advice_string) + { + char *advice_string; + StringInfoData buf; + + /* Generate a textual advice string. */ + initStringInfo(&buf); + pgpa_output_advice(&buf, &walker, rt_identifiers); + advice_string = buf.data; + + /* Save the advice string in the final plan. */ + pgpa_items = lappend(pgpa_items, + makeDefElem("advice_string", + (Node *) makeString(advice_string), + -1)); + } + + /* + * If we're trying to provide advice feedback, then we will need to + * analyze how successful the advice was. + */ + if (generate_advice_feedback) + { + List *feedback = NIL; + + /* + * Inject a Node-tree representation of all the trove-entry flags into + * the PlannedStmt. + */ + feedback = pgpa_planner_append_feedback(feedback, + trove, + PGPA_TROVE_LOOKUP_SCAN, + rt_identifiers, &walker); + feedback = pgpa_planner_append_feedback(feedback, + trove, + PGPA_TROVE_LOOKUP_JOIN, + rt_identifiers, &walker); + feedback = pgpa_planner_append_feedback(feedback, + trove, + PGPA_TROVE_LOOKUP_REL, + rt_identifiers, &walker); + + pgpa_items = lappend(pgpa_items, makeDefElem("feedback", + (Node *) feedback, -1)); + + /* If we were asked to generate feedback warnings, do so. */ + if (pg_plan_advice_feedback_warnings) + pgpa_planner_feedback_warning(feedback); + } + + /* Push whatever data we're saving into the PlannedStmt. */ + if (pgpa_items != NIL) + pstmt->extension_state = + lappend(pstmt->extension_state, + makeDefElem("pg_plan_advice", (Node *) pgpa_items, -1)); + + /* Pass call to previous hook. */ + if (prev_planner_shutdown) + (*prev_planner_shutdown) (glob, parse, query_string, pstmt); +} + +/* + * Hook function for build_simple_rel(). + */ +static void +pgpa_build_simple_rel(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte) +{ + pgpa_planner_state *pps; + pgpa_planner_info *proot = NULL; + + /* Fetch our private state, set up by pgpa_planner_setup(). */ + pps = GetPlannerGlobalExtensionState(root->glob, planner_extension_id); + + /* + * Look up the pgpa_planner_info for this subquery, and make sure we've + * saved a range table identifier. + */ + if (pps != NULL) + { + proot = pgpa_planner_get_proot(pps, root); + pgpa_compute_rt_identifier(proot, root, rel); + } + + /* If query advice was provided, search for relevant entries. */ + if (pps != NULL && pps->trove != NULL) + { + pgpa_identifier *rid; + pgpa_trove_result tresult_scan; + pgpa_trove_result tresult_rel; + + /* Search for scan advice and general rel advice. */ + rid = &proot->rid_array[rel->relid - 1]; + pgpa_trove_lookup(pps->trove, PGPA_TROVE_LOOKUP_SCAN, 1, rid, + &tresult_scan); + pgpa_trove_lookup(pps->trove, PGPA_TROVE_LOOKUP_REL, 1, rid, + &tresult_rel); + + /* If relevant entries were found, apply them. */ + if (tresult_scan.indexes != NULL || tresult_rel.indexes != NULL) + { + uint64 original_mask = rel->pgs_mask; + + pgpa_planner_apply_scan_advice(rel, + tresult_scan.entries, + tresult_scan.indexes, + tresult_rel.entries, + tresult_rel.indexes); + + /* Emit debugging message, if enabled. */ + if (pg_plan_advice_trace_mask && original_mask != rel->pgs_mask) + { + if (root->plan_name != NULL) + ereport(WARNING, + (errmsg("strategy mask for RTI %u in subplan \"%s\" changed from 0x%" PRIx64 " to 0x%" PRIx64, + rel->relid, root->plan_name, + original_mask, rel->pgs_mask))); + else + ereport(WARNING, + (errmsg("strategy mask for RTI %u changed from 0x%" PRIx64 " to 0x%" PRIx64, + rel->relid, original_mask, + rel->pgs_mask))); + } + } + } + + /* Pass call to previous hook. */ + if (prev_build_simple_rel) + (*prev_build_simple_rel) (root, rel, rte); +} + +/* + * Enforce any provided advice that is relevant to any method of implementing + * this join. + * + * Although we're passed the outerrel and innerrel here, those are just + * whatever values happened to prompt the creation of this joinrel; they + * shouldn't really influence our choice of what advice to apply. + */ +static void +pgpa_joinrel_setup(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *outerrel, RelOptInfo *innerrel, + SpecialJoinInfo *sjinfo, List *restrictlist) +{ + pgpa_join_state *pjs; + + Assert(bms_membership(joinrel->relids) == BMS_MULTIPLE); + + /* Get our private state information for this join. */ + pjs = pgpa_get_join_state(root, joinrel, outerrel, innerrel); + + /* If there is relevant advice, call a helper function to apply it. */ + if (pjs != NULL) + { + uint64 original_mask = joinrel->pgs_mask; + + pgpa_planner_apply_joinrel_advice(&joinrel->pgs_mask, + root->plan_name, + pjs); + + /* Emit debugging message, if enabled. */ + if (pg_plan_advice_trace_mask && original_mask != joinrel->pgs_mask) + { + if (root->plan_name != NULL) + ereport(WARNING, + (errmsg("strategy mask for join on RTIs %s in subplan \"%s\" changed from 0x%" PRIx64 " to 0x%" PRIx64, + pgpa_bms_to_cstring(joinrel->relids), + root->plan_name, + original_mask, + joinrel->pgs_mask))); + else + ereport(WARNING, + (errmsg("strategy mask for join on RTIs %s changed from 0x%" PRIx64 " to 0x%" PRIx64, + pgpa_bms_to_cstring(joinrel->relids), + original_mask, + joinrel->pgs_mask))); + } + } + + /* Pass call to previous hook. */ + if (prev_joinrel_setup) + (*prev_joinrel_setup) (root, joinrel, outerrel, innerrel, + sjinfo, restrictlist); +} + +/* + * Enforce any provided advice that is relevant to this particular method of + * implementing this particular join. + */ +static void +pgpa_join_path_setup(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *outerrel, RelOptInfo *innerrel, + JoinType jointype, JoinPathExtraData *extra) +{ + pgpa_join_state *pjs; + + Assert(bms_membership(joinrel->relids) == BMS_MULTIPLE); + + /* + * If we're considering implementing a semijoin by making one side unique, + * make a note of it in the pgpa_planner_state. + */ + if (jointype == JOIN_UNIQUE_OUTER || jointype == JOIN_UNIQUE_INNER) + { + pgpa_planner_state *pps; + RelOptInfo *uniquerel; + + uniquerel = jointype == JOIN_UNIQUE_OUTER ? outerrel : innerrel; + pps = GetPlannerGlobalExtensionState(root->glob, planner_extension_id); + if (pps != NULL && + (pps->generate_advice_string || pps->generate_advice_feedback)) + { + pgpa_planner_info *proot; + MemoryContext oldcontext; + Bitmapset *relids; + + /* + * Get or create a pgpa_planner_info object, and then add the + * relids from the unique side to proot->sj_unique_rels. + * + * We must be careful here to use a sufficiently long-lived + * context, since we might have been called by GEQO. We want all + * the data we store here (including the proot, if we create it) + * to last for as long as the pgpa_planner_state. + * + * pgpa_filter_out_join_relids copies the input Bitmapset whether + * or not it is changed, so 'relids' is part of the long-lived + * context. + */ + oldcontext = MemoryContextSwitchTo(pps->mcxt); + proot = pgpa_planner_get_proot(pps, root); + relids = pgpa_filter_out_join_relids(uniquerel->relids, + root->parse->rtable); + if (!list_member(proot->sj_unique_rels, relids)) + proot->sj_unique_rels = lappend(proot->sj_unique_rels, + relids); + else + bms_free(relids); + MemoryContextSwitchTo(oldcontext); + } + } + + /* Get our private state information for this join. */ + pjs = pgpa_get_join_state(root, joinrel, outerrel, innerrel); + + /* If there is relevant advice, call a helper function to apply it. */ + if (pjs != NULL) + { + uint64 original_mask = extra->pgs_mask; + + pgpa_planner_apply_join_path_advice(jointype, + &extra->pgs_mask, + root->plan_name, + pjs); + + /* Emit debugging message, if enabled. */ + if (pg_plan_advice_trace_mask && original_mask != extra->pgs_mask) + { + if (root->plan_name != NULL) + ereport(WARNING, + (errmsg("strategy mask for %s join on %s with outer %s and inner %s in subplan \"%s\" changed from 0x%" PRIx64 " to 0x%" PRIx64, + pgpa_jointype_to_cstring(jointype), + pgpa_bms_to_cstring(joinrel->relids), + pgpa_bms_to_cstring(outerrel->relids), + pgpa_bms_to_cstring(innerrel->relids), + root->plan_name, + original_mask, + extra->pgs_mask))); + else + ereport(WARNING, + (errmsg("strategy mask for %s join on %s with outer %s and inner %s changed from 0x%" PRIx64 " to 0x%" PRIx64, + pgpa_jointype_to_cstring(jointype), + pgpa_bms_to_cstring(joinrel->relids), + pgpa_bms_to_cstring(outerrel->relids), + pgpa_bms_to_cstring(innerrel->relids), + original_mask, + extra->pgs_mask))); + } + } + + /* Pass call to previous hook. */ + if (prev_join_path_setup) + (*prev_join_path_setup) (root, joinrel, outerrel, innerrel, + jointype, extra); +} + +/* + * Search for advice pertaining to a proposed join. + */ +static pgpa_join_state * +pgpa_get_join_state(PlannerInfo *root, RelOptInfo *joinrel, + RelOptInfo *outerrel, RelOptInfo *innerrel) +{ + pgpa_planner_state *pps; + pgpa_join_state *pjs; + bool new_pjs = false; + + /* Fetch our private state, set up by pgpa_planner_setup(). */ + pps = GetPlannerGlobalExtensionState(root->glob, planner_extension_id); + if (pps == NULL || pps->trove == NULL) + { + /* No advice applies to this query, hence none to this joinrel. */ + return NULL; + } + + /* + * See whether we've previously associated a pgpa_join_state with this + * joinrel. If we have not, we need to try to construct one. If we have, + * then there are two cases: (a) if innerrel and outerrel are unchanged, + * we can simply use it, and (b) if they have changed, we need to rejigger + * the array of identifiers but can still skip the trove lookup. + */ + pjs = GetRelOptInfoExtensionState(joinrel, planner_extension_id); + if (pjs != NULL) + { + if (pjs->join_indexes == NULL && pjs->rel_indexes == NULL) + { + /* + * If there's no potentially relevant advice, then the presence of + * this pgpa_join_state acts like a negative cache entry: it tells + * us not to bother searching the trove for advice, because we + * will not find any. + */ + return NULL; + } + + if (pjs->outerrel == outerrel && pjs->innerrel == innerrel) + { + /* No updates required, so just return. */ + /* XXX. Does this need to do something different under GEQO? */ + return pjs; + } + } + + /* + * If there's no pgpa_join_state yet, we need to allocate one. Trove keys + * will not get built for RTE_JOIN RTEs, so the array may end up being + * larger than needed. It's not worth trying to compute a perfectly + * accurate count here. + */ + if (pjs == NULL) + { + int pessimistic_count = bms_num_members(joinrel->relids); + + pjs = palloc0_object(pgpa_join_state); + pjs->rids = palloc_array(pgpa_identifier, pessimistic_count); + new_pjs = true; + } + + /* + * Either we just allocated a new pgpa_join_state, or the existing one + * needs reconfiguring for a new innerrel and outerrel. The required array + * size can't change, so we can overwrite the existing one. + */ + pjs->outerrel = outerrel; + pjs->innerrel = innerrel; + pjs->outer_count = + pgpa_compute_identifiers_by_relids(root, outerrel->relids, pjs->rids); + pjs->inner_count = + pgpa_compute_identifiers_by_relids(root, innerrel->relids, + pjs->rids + pjs->outer_count); + + /* + * If we allocated a new pgpa_join_state, search our trove of advice for + * relevant entries. The trove lookup will return the same results for + * every outerrel/innerrel combination, so we don't need to repeat that + * work every time. + */ + if (new_pjs) + { + pgpa_trove_result tresult; + + /* Find join entries. */ + pgpa_trove_lookup(pps->trove, PGPA_TROVE_LOOKUP_JOIN, + pjs->outer_count + pjs->inner_count, + pjs->rids, &tresult); + pjs->join_entries = tresult.entries; + pjs->join_indexes = tresult.indexes; + + /* Find rel entries. */ + pgpa_trove_lookup(pps->trove, PGPA_TROVE_LOOKUP_REL, + pjs->outer_count + pjs->inner_count, + pjs->rids, &tresult); + pjs->rel_entries = tresult.entries; + pjs->rel_indexes = tresult.indexes; + + /* Now that the new pgpa_join_state is fully valid, save a pointer. */ + SetRelOptInfoExtensionState(joinrel, planner_extension_id, pjs); + + /* + * If there was no relevant advice found, just return NULL. This + * pgpa_join_state will stick around as a sort of negative cache + * entry, so that future calls for this same joinrel quickly return + * NULL. + */ + if (pjs->join_indexes == NULL && pjs->rel_indexes == NULL) + return NULL; + } + + return pjs; +} + +/* + * Enforce overall restrictions on a join relation that apply uniformly + * regardless of the choice of inner and outer rel. + */ +static void +pgpa_planner_apply_joinrel_advice(uint64 *pgs_mask_p, char *plan_name, + pgpa_join_state *pjs) +{ + int i = -1; + int flags; + bool gather_conflict = false; + uint64 gather_mask = 0; + Bitmapset *gather_partial_match = NULL; + Bitmapset *gather_full_match = NULL; + bool partitionwise_conflict = false; + int partitionwise_outcome = 0; + Bitmapset *partitionwise_partial_match = NULL; + Bitmapset *partitionwise_full_match = NULL; + + /* Iterate over all possibly-relevant advice. */ + while ((i = bms_next_member(pjs->rel_indexes, i)) >= 0) + { + pgpa_trove_entry *entry = &pjs->rel_entries[i]; + pgpa_itm_type itm; + bool full_match = false; + uint64 my_gather_mask = 0; + int my_partitionwise_outcome = 0; /* >0 yes, <0 no */ + + /* + * For GATHER and GATHER_MERGE, if the specified relations exactly + * match this joinrel, do whatever the advice says; otherwise, don't + * allow Gather or Gather Merge at this level. For NO_GATHER, there + * must be a single target relation which must be included in this + * joinrel, so just don't allow Gather or Gather Merge here, full + * stop. + */ + if (entry->tag == PGPA_TAG_NO_GATHER) + { + my_gather_mask = PGS_CONSIDER_NONPARTIAL; + full_match = true; + } + else + { + int total_count; + + total_count = pjs->outer_count + pjs->inner_count; + itm = pgpa_identifiers_match_target(total_count, pjs->rids, + entry->target); + Assert(itm != PGPA_ITM_DISJOINT); + + if (itm == PGPA_ITM_EQUAL) + { + full_match = true; + if (entry->tag == PGPA_TAG_PARTITIONWISE) + my_partitionwise_outcome = 1; + else if (entry->tag == PGPA_TAG_GATHER) + my_gather_mask = PGS_GATHER; + else if (entry->tag == PGPA_TAG_GATHER_MERGE) + my_gather_mask = PGS_GATHER_MERGE; + else + elog(ERROR, "unexpected advice tag: %d", + (int) entry->tag); + } + else + { + /* + * If specified relations don't exactly match this joinrel, + * then we should do the opposite of whatever the advice says. + * For instance, if we have PARTITIONWISE((a b c)) or + * GATHER((a b c)) and this joinrel covers {a, b} or {a, b, c, + * d} or {a, d}, we shouldn't plan it partitionwise or put a + * Gather or Gather Merge on it here. + * + * Also, we can't put a Gather or Gather Merge at this level + * if there is PARTITIONWISE advice that overlaps with it, + * unless the PARTITIONWISE advice covers a subset of the + * relations in the joinrel. To continue the previous example, + * PARTITIONWISE((a b c)) is logically incompatible with + * GATHER((a b)) or GATHER((a d)), but not with GATHER((a b c + * d)). + * + * Conversely, we can't proceed partitionwise at this level if + * there is overlapping GATHER or GATHER_MERGE advice, unless + * that advice covers a superset of the relations in this + * joinrel. This is just the flip side of the preceding point. + */ + if (entry->tag == PGPA_TAG_PARTITIONWISE) + { + my_partitionwise_outcome = -1; + if (itm != PGPA_ITM_TARGETS_ARE_SUBSET) + my_gather_mask = PGS_CONSIDER_NONPARTIAL; + } + else if (entry->tag == PGPA_TAG_GATHER || + entry->tag == PGPA_TAG_GATHER_MERGE) + { + my_gather_mask = PGS_CONSIDER_NONPARTIAL; + if (itm != PGPA_ITM_KEYS_ARE_SUBSET) + my_partitionwise_outcome = -1; + } + else + elog(ERROR, "unexpected advice tag: %d", + (int) entry->tag); + } + } + + /* + * If we set my_gather_mask up above, then we (1) make a note if the + * advice conflicted, (2) remember the mask value, and (3) remember + * whether this was a full or partial match. + */ + if (my_gather_mask != 0) + { + if (gather_mask != 0 && gather_mask != my_gather_mask) + gather_conflict = true; + gather_mask = my_gather_mask; + if (full_match) + gather_full_match = bms_add_member(gather_full_match, i); + else + gather_partial_match = bms_add_member(gather_partial_match, i); + } + + /* + * Likewise, if we set my_partitionwise_outcome up above, then we (1) + * make a note if the advice conflicted, (2) remember what the desired + * outcome was, and (3) remember whether this was a full or partial + * match. + */ + if (my_partitionwise_outcome != 0) + { + if (partitionwise_outcome != 0 && + partitionwise_outcome != my_partitionwise_outcome) + partitionwise_conflict = true; + partitionwise_outcome = my_partitionwise_outcome; + if (full_match) + partitionwise_full_match = + bms_add_member(partitionwise_full_match, i); + else + partitionwise_partial_match = + bms_add_member(partitionwise_partial_match, i); + } + } + + /* + * Mark every Gather-related piece of advice as partially matched, and if + * the set of targets exactly matched this relation, fully matched. If + * there was a conflict, mark them all as conflicting. + */ + flags = PGPA_FB_MATCH_PARTIAL; + if (gather_conflict) + flags |= PGPA_FB_CONFLICTING; + pgpa_trove_set_flags(pjs->rel_entries, gather_partial_match, flags); + flags |= PGPA_FB_MATCH_FULL; + pgpa_trove_set_flags(pjs->rel_entries, gather_full_match, flags); + + /* Likewise for partitionwise advice. */ + flags = PGPA_FB_MATCH_PARTIAL; + if (partitionwise_conflict) + flags |= PGPA_FB_CONFLICTING; + pgpa_trove_set_flags(pjs->rel_entries, partitionwise_partial_match, flags); + flags |= PGPA_FB_MATCH_FULL; + pgpa_trove_set_flags(pjs->rel_entries, partitionwise_full_match, flags); + + /* + * Enforce restrictions on the Gather/Gather Merge. Only clear bits here, + * so that we still respect the enable_* GUCs. Do nothing if the advice + * conflicts. + */ + if (gather_mask != 0 && !gather_conflict) + { + uint64 all_gather_mask; + + all_gather_mask = + PGS_GATHER | PGS_GATHER_MERGE | PGS_CONSIDER_NONPARTIAL; + *pgs_mask_p &= ~(all_gather_mask & ~gather_mask); + } + + /* + * As above, but for partitionwise advice. + * + * To induce a partitionwise join, we disable all the ordinary means of + * performing a join, so that an Append or MergeAppend path will hopefully + * be chosen. + * + * To prevent one, we just disable Append and MergeAppend. Note that we + * must not unset PGS_CONSIDER_PARTITIONWISE even when we don't want a + * partitionwise join here, because we might want one at a higher level + * that will construct its own paths using the ones from this level. + */ + if (partitionwise_outcome != 0 && !partitionwise_conflict) + { + if (partitionwise_outcome > 0) + *pgs_mask_p = (*pgs_mask_p & ~PGS_JOIN_ANY); + else + *pgs_mask_p &= ~(PGS_APPEND | PGS_MERGE_APPEND); + } +} + +/* + * Enforce restrictions on the join order or join method. + */ +static void +pgpa_planner_apply_join_path_advice(JoinType jointype, uint64 *pgs_mask_p, + char *plan_name, + pgpa_join_state *pjs) +{ + int i = -1; + Bitmapset *jo_permit_indexes = NULL; + Bitmapset *jo_deny_indexes = NULL; + Bitmapset *jo_deny_rel_indexes = NULL; + Bitmapset *jm_indexes = NULL; + bool jm_conflict = false; + uint64 join_mask = 0; + Bitmapset *sj_permit_indexes = NULL; + Bitmapset *sj_deny_indexes = NULL; + + /* + * Reconsider PARTITIONWISE(...) advice. + * + * We already thought about this for the joinrel as a whole, but in some + * cases, partitionwise advice can also constrain the join order. For + * instance, if the advice says PARTITIONWISE((t1 t2)), we shouldn't build + * join paths for any joinrel that includes t1 or t2 unless it also + * includes the other. In general, the partitionwise operation must have + * already been completed within one side of the current join or the + * other, else the join order is impermissible. + * + * NB: It might seem tempting to try to deal with PARTITIONWISE advice + * entirely in this function, but that doesn't work. Here, we can only + * affect the pgs_mask within a particular JoinPathExtraData, that is, for + * a particular choice of innerrel and outerrel. Partitionwise paths are + * not built that way, so we must set pgs_mask for the RelOptInfo, which + * is best done in pgpa_planner_apply_joinrel_advice. + */ + while ((i = bms_next_member(pjs->rel_indexes, i)) >= 0) + { + pgpa_trove_entry *entry = &pjs->rel_entries[i]; + pgpa_itm_type inner_itm; + pgpa_itm_type outer_itm; + + if (entry->tag != PGPA_TAG_PARTITIONWISE) + continue; + + outer_itm = pgpa_identifiers_match_target(pjs->outer_count, + pjs->rids, entry->target); + if (outer_itm == PGPA_ITM_EQUAL || + outer_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + continue; + + inner_itm = pgpa_identifiers_match_target(pjs->inner_count, + pjs->rids + pjs->outer_count, + entry->target); + if (inner_itm == PGPA_ITM_EQUAL || + inner_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + continue; + + jo_deny_rel_indexes = bms_add_member(jo_deny_rel_indexes, i); + } + + /* Iterate over advice that pertains to the join order and method. */ + i = -1; + while ((i = bms_next_member(pjs->join_indexes, i)) >= 0) + { + pgpa_trove_entry *entry = &pjs->join_entries[i]; + uint64 my_join_mask; + + /* Handle join order advice. */ + if (entry->tag == PGPA_TAG_JOIN_ORDER) + { + pgpa_jo_outcome jo_outcome; + + jo_outcome = pgpa_join_order_permits_join(pjs->outer_count, + pjs->inner_count, + pjs->rids, + entry); + if (jo_outcome == PGPA_JO_PERMITTED) + jo_permit_indexes = bms_add_member(jo_permit_indexes, i); + else if (jo_outcome == PGPA_JO_DENIED) + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); + continue; + } + + /* Handle join method advice. */ + my_join_mask = pgpa_join_strategy_mask_from_advice_tag(entry->tag); + if (my_join_mask != 0) + { + bool permit; + bool restrict_method; + + if (entry->tag == PGPA_TAG_FOREIGN_JOIN) + permit = pgpa_opaque_join_permits_join(pjs->outer_count, + pjs->inner_count, + pjs->rids, + entry, + &restrict_method); + else + permit = pgpa_join_method_permits_join(pjs->outer_count, + pjs->inner_count, + pjs->rids, + entry, + &restrict_method); + if (!permit) + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); + else if (restrict_method) + { + jm_indexes = bms_add_member(jm_indexes, i); + if (join_mask != 0 && join_mask != my_join_mask) + jm_conflict = true; + join_mask = my_join_mask; + } + continue; + } + + /* Handle semijoin uniqueness advice. */ + if (entry->tag == PGPA_TAG_SEMIJOIN_UNIQUE || + entry->tag == PGPA_TAG_SEMIJOIN_NON_UNIQUE) + { + bool outer_side_nullable; + bool restrict_method; + + /* Planner has nullable side of the semijoin on the outer side? */ + outer_side_nullable = (jointype == JOIN_UNIQUE_OUTER || + jointype == JOIN_RIGHT_SEMI); + + if (!pgpa_semijoin_permits_join(pjs->outer_count, + pjs->inner_count, + pjs->rids, + entry, + outer_side_nullable, + &restrict_method)) + jo_deny_indexes = bms_add_member(jo_deny_indexes, i); + else if (restrict_method) + { + bool advice_unique; + bool jt_unique; + bool jt_non_unique; + + /* Advice wants to unique-ify and use a regular join? */ + advice_unique = (entry->tag == PGPA_TAG_SEMIJOIN_UNIQUE); + + /* Planner is trying to unique-ify and use a regular join? */ + jt_unique = (jointype == JOIN_UNIQUE_INNER || + jointype == JOIN_UNIQUE_OUTER); + + /* Planner is trying a semi-join, without unique-ifying? */ + jt_non_unique = (jointype == JOIN_SEMI || + jointype == JOIN_RIGHT_SEMI); + + if (!jt_unique && !jt_non_unique) + { + /* + * This doesn't seem to be a semijoin to which SJ_UNIQUE + * or SJ_NON_UNIQUE can be applied. + */ + entry->flags |= PGPA_FB_INAPPLICABLE; + } + else if (advice_unique != jt_unique) + sj_deny_indexes = bms_add_member(sj_deny_indexes, i); + else + sj_permit_indexes = bms_add_member(sj_permit_indexes, i); + } + continue; + } + } + + /* + * If the advice indicates both that this join order is permissible and + * also that it isn't, then mark advice related to the join order as + * conflicting. + */ + if (jo_permit_indexes != NULL && + (jo_deny_indexes != NULL || jo_deny_rel_indexes != NULL)) + { + pgpa_trove_set_flags(pjs->join_entries, jo_permit_indexes, + PGPA_FB_CONFLICTING); + pgpa_trove_set_flags(pjs->join_entries, jo_deny_indexes, + PGPA_FB_CONFLICTING); + pgpa_trove_set_flags(pjs->rel_entries, jo_deny_rel_indexes, + PGPA_FB_CONFLICTING); + } + + /* + * If more than one join method specification is relevant here and they + * differ, mark them all as conflicting. + */ + if (jm_conflict) + pgpa_trove_set_flags(pjs->join_entries, jm_indexes, + PGPA_FB_CONFLICTING); + + /* If semijoin advice says both yes and no, mark it all as conflicting. */ + if (sj_permit_indexes != NULL && sj_deny_indexes != NULL) + { + pgpa_trove_set_flags(pjs->join_entries, sj_permit_indexes, + PGPA_FB_CONFLICTING); + pgpa_trove_set_flags(pjs->join_entries, sj_deny_indexes, + PGPA_FB_CONFLICTING); + } + + /* + * Enforce restrictions on the join order and join method, and any + * semijoin-related restrictions. Only clear bits here, so that we still + * respect the enable_* GUCs. Do nothing in cases where the advice on a + * single topic conflicts. + */ + if ((jo_deny_indexes != NULL || jo_deny_rel_indexes != NULL) && + jo_permit_indexes == NULL) + *pgs_mask_p &= ~PGS_JOIN_ANY; + if (join_mask != 0 && !jm_conflict) + *pgs_mask_p &= ~(PGS_JOIN_ANY & ~join_mask); + if (sj_deny_indexes != NULL && sj_permit_indexes == NULL) + *pgs_mask_p &= ~PGS_JOIN_ANY; +} + +/* + * Translate an advice tag into a path generation strategy mask. + * + * This function can be called with tag types that don't represent join + * strategies. In such cases, we just return 0, which can't be confused with + * a valid mask. + */ +static uint64 +pgpa_join_strategy_mask_from_advice_tag(pgpa_advice_tag_type tag) +{ + switch (tag) + { + case PGPA_TAG_FOREIGN_JOIN: + return PGS_FOREIGNJOIN; + case PGPA_TAG_MERGE_JOIN_PLAIN: + return PGS_MERGEJOIN_PLAIN; + case PGPA_TAG_MERGE_JOIN_MATERIALIZE: + return PGS_MERGEJOIN_MATERIALIZE; + case PGPA_TAG_NESTED_LOOP_PLAIN: + return PGS_NESTLOOP_PLAIN; + case PGPA_TAG_NESTED_LOOP_MATERIALIZE: + return PGS_NESTLOOP_MATERIALIZE; + case PGPA_TAG_NESTED_LOOP_MEMOIZE: + return PGS_NESTLOOP_MEMOIZE; + case PGPA_TAG_HASH_JOIN: + return PGS_HASHJOIN; + default: + return 0; + } +} + +/* + * Does a certain item of join order advice permit a certain join? + * + * Returns PGPA_JO_DENIED if the advice is incompatible with the proposed + * join order. + * + * Returns PGPA_JO_PERMITTED if the advice specifies exactly the proposed + * join order. This implies that a partitionwise join should not be + * performed at this level; rather, one of the traditional join methods + * should be used. + * + * Returns PGPA_JO_INDIFFERENT if the advice does not care what happens. + * We use this for unordered JOIN_ORDER sublists, which are compatible with + * partitionwise join but do not mandate it. + */ +static pgpa_jo_outcome +pgpa_join_order_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry) +{ + bool loop = true; + bool sublist = false; + int length; + int outer_length; + pgpa_advice_target *target = entry->target; + pgpa_advice_target *prefix_target; + + /* We definitely have at least a partial match for this trove entry. */ + entry->flags |= PGPA_FB_MATCH_PARTIAL; + + /* + * Find the innermost sublist that contains all keys; if no sublist does, + * then continue processing with the toplevel list. + * + * For example, if the advice says JOIN_ORDER(t1 t2 (t3 t4 t5)), then we + * should evaluate joins that only involve t3, t4, and/or t5 against the + * (t3 t4 t5) sublist, and others against the full list. + * + * Note that (1) outermost sublist is always ordered and (2) whenever we + * zoom into an unordered sublist, we instantly return + * PGPA_JO_INDIFFERENT. + */ + while (loop) + { + Assert(target->ttype == PGPA_TARGET_ORDERED_LIST); + + loop = false; + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + pgpa_itm_type itm; + + if (child_target->ttype == PGPA_TARGET_IDENTIFIER) + continue; + + itm = pgpa_identifiers_match_target(outer_count + inner_count, + rids, child_target); + if (itm == PGPA_ITM_EQUAL || itm == PGPA_ITM_KEYS_ARE_SUBSET) + { + if (child_target->ttype == PGPA_TARGET_ORDERED_LIST) + { + target = child_target; + sublist = true; + loop = true; + break; + } + else + { + Assert(child_target->ttype == PGPA_TARGET_UNORDERED_LIST); + return PGPA_JO_INDIFFERENT; + } + } + } + } + + /* + * Try to find a prefix of the selected join order list that is exactly + * equal to the outer side of the proposed join. + */ + length = list_length(target->children); + prefix_target = palloc0_object(pgpa_advice_target); + prefix_target->ttype = PGPA_TARGET_ORDERED_LIST; + for (outer_length = 1; outer_length <= length; ++outer_length) + { + pgpa_itm_type itm; + + /* Avoid leaking memory in every loop iteration. */ + if (prefix_target->children != NULL) + list_free(prefix_target->children); + prefix_target->children = list_copy_head(target->children, + outer_length); + + /* Search, hoping to find an exact match. */ + itm = pgpa_identifiers_match_target(outer_count, rids, prefix_target); + if (itm == PGPA_ITM_EQUAL) + break; + + /* + * If the prefix of the join order list that we're considering + * includes some but not all of the outer rels, we can make the prefix + * longer to find an exact match. But if the advice hasn't mentioned + * everything that's part of our outer rel yet, but has mentioned + * things that are not, then this join doesn't match the join order + * list. + */ + if (itm != PGPA_ITM_TARGETS_ARE_SUBSET) + return PGPA_JO_DENIED; + } + + /* + * If the previous loop stopped before the prefix_target included the + * entire join order list, then the next member of the join order list + * must exactly match the inner side of the join. + * + * Example: Given JOIN_ORDER(t1 t2 (t3 t4 t5)), if the outer side of the + * current join includes only t1, then the inner side must be exactly t2; + * if the outer side includes both t1 and t2, then the inner side must + * include exactly t3, t4, and t5. + */ + if (outer_length < length) + { + pgpa_advice_target *inner_target; + pgpa_itm_type itm; + + inner_target = list_nth(target->children, outer_length); + + itm = pgpa_identifiers_match_target(inner_count, rids + outer_count, + inner_target); + + /* + * Before returning, consider whether we need to mark this entry as + * fully matched. If we're considering the full list rather than a + * sublist, and if we found every item but one on the outer side of + * the join and the last item on the inner side of the join, then the + * answer is yes. + */ + if (!sublist && outer_length + 1 == length && itm == PGPA_ITM_EQUAL) + entry->flags |= PGPA_FB_MATCH_FULL; + + return (itm == PGPA_ITM_EQUAL) ? PGPA_JO_PERMITTED : PGPA_JO_DENIED; + } + + /* + * If we get here, then the outer side of the join includes the entirety + * of the join order list. In this case, we behave differently depending + * on whether we're looking at the top-level join order list or sublist. + * At the top-level, we treat the specified list as mandating that the + * actual join order has the given list as a prefix, but a sublist + * requires an exact match. + * + * Example: Given JOIN_ORDER(t1 t2 (t3 t4 t5)), we must start by joining + * all five of those relations and in that sequence, but once that is + * done, it's OK to join any other rels that are part of the join problem. + * This allows a user to specify the driving table and perhaps the first + * few things to which it should be joined while leaving the rest of the + * join order up the optimizer. But it seems like it would be surprising, + * given that specification, if the user could add t6 to the (t3 t4 t5) + * sub-join, so we don't allow that. If we did want to allow it, the logic + * earlier in this function would require substantial adjustment: we could + * allow the t3-t4-t5-t6 join to be built here, but the next step of + * joining t1-t2 to the result would still be rejected. + */ + if (!sublist) + entry->flags |= PGPA_FB_MATCH_FULL; + return sublist ? PGPA_JO_DENIED : PGPA_JO_PERMITTED; +} + +/* + * Does a certain item of join method advice permit a certain join? + * + * Advice such as HASH_JOIN((x y)) means that there should be a hash join with + * exactly x and y on the inner side. Obviously, this means that if we are + * considering a join with exactly x and y on the inner side, we should enforce + * the use of a hash join. However, it also means that we must reject some + * incompatible join orders entirely. For example, a join with exactly x + * and y on the outer side shouldn't be allowed, because such paths might win + * over the advice-driven path on cost. + * + * To accommodate these requirements, this function returns true if the join + * should be allowed and false if it should not. Furthermore, *restrict_method + * is set to true if the join method should be enforced and false if not. + */ +static bool +pgpa_join_method_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry, + bool *restrict_method) +{ + pgpa_advice_target *target = entry->target; + pgpa_itm_type inner_itm; + pgpa_itm_type outer_itm; + pgpa_itm_type join_itm; + + /* We definitely have at least a partial match for this trove entry. */ + entry->flags |= PGPA_FB_MATCH_PARTIAL; + + *restrict_method = false; + + /* + * If our inner rel mentions exactly the same relations as the advice + * target, allow the join and enforce the join method restriction. + * + * If our inner rel mentions a superset of the target relations, allow the + * join. The join we care about has already taken place, and this advice + * imposes no further restrictions. + */ + inner_itm = pgpa_identifiers_match_target(inner_count, + rids + outer_count, + target); + if (inner_itm == PGPA_ITM_EQUAL) + { + entry->flags |= PGPA_FB_MATCH_FULL; + *restrict_method = true; + return true; + } + else if (inner_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + return true; + + /* + * If our outer rel mentions a superset of the relations in the advice + * target, no restrictions apply, because the join we care about has + * already taken place. + * + * On the other hand, if our outer rel mentions exactly the relations + * mentioned in the advice target, the planner is trying to reverse the + * sides of the join as compared with our desired outcome. Reject that. + */ + outer_itm = pgpa_identifiers_match_target(outer_count, + rids, target); + if (outer_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + return true; + else if (outer_itm == PGPA_ITM_EQUAL) + return false; + + /* + * If the advice target mentions only a single relation, the test below + * cannot ever pass, so save some work by exiting now. + */ + if (target->ttype == PGPA_TARGET_IDENTIFIER) + return false; + + /* + * If everything in the joinrel appears in the advice target, we're below + * the level of the join we want to control. + * + * For example, HASH_JOIN((x y)) doesn't restrict how x and y can be + * joined. + * + * This lookup shouldn't return PGPA_ITM_DISJOINT, because any such advice + * should not have been returned from the trove in the first place. + */ + join_itm = pgpa_identifiers_match_target(outer_count + inner_count, + rids, target); + Assert(join_itm != PGPA_ITM_DISJOINT); + if (join_itm == PGPA_ITM_KEYS_ARE_SUBSET || + join_itm == PGPA_ITM_EQUAL) + return true; + + /* + * We've already permitted all allowable cases, so reject this. + * + * If we reach this point, then the advice overlaps with this join but + * isn't entirely contained within either side, and there's also at least + * one relation present in the join that isn't mentioned by the advice. + * + * For instance, in the HASH_JOIN((x y)) example, we would reach here if x + * were on one side of the join, y on the other, and at least one of the + * two sides also included some other relation, say t. In that case, + * accepting this join would allow the (x y t) joinrel to contain + * non-disabled paths that do not put (x y) on the inner side of a hash + * join; we could instead end up with something like (x JOIN t) JOIN y. + */ + return false; +} + +/* + * Does advice concerning an opaque join permit a certain join? + * + * By an opaque join, we mean one where the exact mechanism by which the + * join is performed is not visible to PostgreSQL. Currently this is the + * case only for foreign joins: FOREIGN_JOIN((x y z)) means that x, y, and + * z are joined on the remote side, but we know nothing about the join order + * or join methods used over there. + * + * The logic here needs to differ from pgpa_join_method_permits_join because, + * for other join types, the advice target is the set of inner rels; here, it + * includes both inner and outer rels. + */ +static bool +pgpa_opaque_join_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry, + bool *restrict_method) +{ + pgpa_advice_target *target = entry->target; + pgpa_itm_type join_itm; + + /* We definitely have at least a partial match for this trove entry. */ + entry->flags |= PGPA_FB_MATCH_PARTIAL; + + *restrict_method = false; + + join_itm = pgpa_identifiers_match_target(outer_count + inner_count, + rids, target); + if (join_itm == PGPA_ITM_EQUAL) + { + /* + * We have an exact match, and should therefore allow the join and + * enforce the use of the relevant opaque join method. + */ + entry->flags |= PGPA_FB_MATCH_FULL; + *restrict_method = true; + return true; + } + + if (join_itm == PGPA_ITM_KEYS_ARE_SUBSET || + join_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + { + /* + * If join_itm == PGPA_ITM_TARGETS_ARE_SUBSET, then the join we care + * about has already taken place and no further restrictions apply. + * + * If join_itm == PGPA_ITM_KEYS_ARE_SUBSET, we're still building up to + * the join we care about and have not introduced any extraneous + * relations not named in the advice. Note that ForeignScan paths for + * joins are built up from ForeignScan paths from underlying joins and + * scans, so we must not disable this join when considering a subset + * of the relations we ultimately want. + */ + return true; + } + + /* + * The advice overlaps the join, but at least one relation is present in + * the join that isn't mentioned by the advice. We want to disable such + * paths so that we actually push down the join as intended. + */ + return false; +} + +/* + * Does advice concerning a semijoin permit a certain join? + * + * Unlike join method advice, which lists the rels on the inner side of the + * join, semijoin uniqueness advice lists the rels on the nullable side of the + * join. Those can be the same, if the join type is JOIN_UNIQUE_INNER or + * JOIN_SEMI, or they can be different, in case of JOIN_UNIQUE_OUTER or + * JOIN_RIGHT_SEMI. + * + * We don't know here whether the caller specified SEMIJOIN_UNIQUE or + * SEMIJOIN_NON_UNIQUE. The caller should check the join type against the + * advice type if and only if we set *restrict_method to true. + */ +static bool +pgpa_semijoin_permits_join(int outer_count, int inner_count, + pgpa_identifier *rids, + pgpa_trove_entry *entry, + bool outer_is_nullable, + bool *restrict_method) +{ + pgpa_advice_target *target = entry->target; + pgpa_itm_type join_itm; + pgpa_itm_type inner_itm; + pgpa_itm_type outer_itm; + + *restrict_method = false; + + /* We definitely have at least a partial match for this trove entry. */ + entry->flags |= PGPA_FB_MATCH_PARTIAL; + + /* + * If outer rel is the nullable side and contains exactly the same + * relations as the advice target, then the join order is allowable, but + * the caller must check whether the advice tag (either SEMIJOIN_UNIQUE or + * SEMIJOIN_NON_UNIQUE) matches the join type. + * + * If the outer rel is a superset of the target relations, the join we + * care about has already taken place, so we should impose no further + * restrictions. + */ + outer_itm = pgpa_identifiers_match_target(outer_count, + rids, target); + if (outer_itm == PGPA_ITM_EQUAL) + { + entry->flags |= PGPA_FB_MATCH_FULL; + if (outer_is_nullable) + { + *restrict_method = true; + return true; + } + } + else if (outer_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + return true; + + /* As above, but for the inner rel. */ + inner_itm = pgpa_identifiers_match_target(inner_count, + rids + outer_count, + target); + if (inner_itm == PGPA_ITM_EQUAL) + { + entry->flags |= PGPA_FB_MATCH_FULL; + if (!outer_is_nullable) + { + *restrict_method = true; + return true; + } + } + else if (inner_itm == PGPA_ITM_TARGETS_ARE_SUBSET) + return true; + + /* + * If everything in the joinrel appears in the advice target, we're below + * the level of the join we want to control. + */ + join_itm = pgpa_identifiers_match_target(outer_count + inner_count, + rids, target); + Assert(join_itm != PGPA_ITM_DISJOINT); + if (join_itm == PGPA_ITM_KEYS_ARE_SUBSET || + join_itm == PGPA_ITM_EQUAL) + return true; + + /* + * We've tested for all allowable possibilities, and so must reject this + * join order. This can happen in two ways. + * + * First, we might be considering a semijoin that overlaps incompletely + * with one or both sides of the join. For example, if the user has + * specified SEMIJOIN_UNIQUE((t1 t2)) or SEMIJOIN_NON_UNIQUE((t1 t2)), we + * should reject a proposed t2-t3 join, since that could not result in a + * final plan compatible with the advice. + * + * Second, we might be considering a semijoin where the advice target + * perfectly matches one side of the join, but it's the wrong one. For + * example, in the example above, we might see a 3-way join between t1, + * t2, and t3, with (t1 t2) on the non-nullable side. That, too, would be + * incompatible with the advice. + */ + return false; +} + +/* + * Apply scan advice to a RelOptInfo. + */ +static void +pgpa_planner_apply_scan_advice(RelOptInfo *rel, + pgpa_trove_entry *scan_entries, + Bitmapset *scan_indexes, + pgpa_trove_entry *rel_entries, + Bitmapset *rel_indexes) +{ + const uint64 all_scan_mask = PGS_SCAN_ANY | PGS_APPEND | + PGS_MERGE_APPEND | PGS_CONSIDER_INDEXONLY; + bool gather_conflict = false; + Bitmapset *gather_partial_match = NULL; + Bitmapset *gather_full_match = NULL; + int i = -1; + pgpa_trove_entry *scan_entry = NULL; + int flags; + bool scan_type_conflict = false; + Bitmapset *scan_type_indexes = NULL; + Bitmapset *scan_type_rel_indexes = NULL; + uint64 gather_mask = 0; + uint64 scan_type = all_scan_mask; /* sentinel: no advice yet */ + + /* Scrutinize available scan advice. */ + while ((i = bms_next_member(scan_indexes, i)) >= 0) + { + pgpa_trove_entry *my_entry = &scan_entries[i]; + uint64 my_scan_type = all_scan_mask; + + /* Translate our advice tags to a scan strategy advice value. */ + if (my_entry->tag == PGPA_TAG_DO_NOT_SCAN) + my_scan_type = 0; + else if (my_entry->tag == PGPA_TAG_BITMAP_HEAP_SCAN) + { + /* + * Currently, PGS_CONSIDER_INDEXONLY can suppress Bitmap Heap + * Scans, so don't clear it when such a scan is requested. This + * happens because build_index_scankeys() thinks that the + * possibility of an index-only scan is a sufficient reason to + * consider using an otherwise-useless index, and + * get_index_paths() thinks that the same paths that are useful + * for index or index-only scans should also be considered for + * bitmap scans. Perhaps that logic should be tightened up, but + * until then we need to include PGS_CONSIDER_INDEXONLY in + * my_scan_type here. + */ + my_scan_type = PGS_BITMAPSCAN | PGS_CONSIDER_INDEXONLY; + } + else if (my_entry->tag == PGPA_TAG_INDEX_ONLY_SCAN) + my_scan_type = PGS_INDEXONLYSCAN | PGS_CONSIDER_INDEXONLY; + else if (my_entry->tag == PGPA_TAG_INDEX_SCAN) + my_scan_type = PGS_INDEXSCAN; + else if (my_entry->tag == PGPA_TAG_SEQ_SCAN) + my_scan_type = PGS_SEQSCAN; + else if (my_entry->tag == PGPA_TAG_TID_SCAN) + my_scan_type = PGS_TIDSCAN; + + /* + * If this is understandable scan advice, hang on to the entry, the + * inferred scan type, and the index at which we found it. + * + * Also make a note if we see conflicting scan type advice. Note that + * we regard two index specifications as conflicting unless they match + * exactly. In theory, perhaps we could regard INDEX_SCAN(a c) and + * INDEX_SCAN(a b.c) as non-conflicting if it happens that the only + * index named c is in schema b, but it doesn't seem worth the code. + */ + if (my_scan_type != all_scan_mask) + { + if (scan_type != all_scan_mask && scan_type != my_scan_type) + scan_type_conflict = true; + if (!scan_type_conflict && scan_entry != NULL && + my_entry->target->itarget != NULL && + scan_entry->target->itarget != NULL && + !pgpa_index_targets_equal(scan_entry->target->itarget, + my_entry->target->itarget)) + scan_type_conflict = true; + scan_entry = my_entry; + scan_type = my_scan_type; + scan_type_indexes = bms_add_member(scan_type_indexes, i); + } + } + + /* Scrutinize available gather-related and partitionwise advice. */ + i = -1; + while ((i = bms_next_member(rel_indexes, i)) >= 0) + { + pgpa_trove_entry *my_entry = &rel_entries[i]; + uint64 my_gather_mask = 0; + bool just_one_rel; + + just_one_rel = my_entry->target->ttype == PGPA_TARGET_IDENTIFIER + || list_length(my_entry->target->children) == 1; + + /* + * PARTITIONWISE behaves like a scan type, except that if there's more + * than one relation targeted, it has no effect at this level. + */ + if (my_entry->tag == PGPA_TAG_PARTITIONWISE) + { + if (just_one_rel) + { + const uint64 my_scan_type = PGS_APPEND | PGS_MERGE_APPEND; + + if (scan_type != all_scan_mask && scan_type != my_scan_type) + scan_type_conflict = true; + scan_entry = my_entry; + scan_type = my_scan_type; + scan_type_rel_indexes = + bms_add_member(scan_type_rel_indexes, i); + } + continue; + } + + /* + * GATHER and GATHER_MERGE applied to a single rel mean that we should + * use the corresponding strategy here, while applying either to more + * than one rel means we should not use those strategies here, but + * rather at the level of the joinrel that corresponds to what was + * specified. NO_GATHER can only be applied to single rels. + * + * Note that setting PGS_CONSIDER_NONPARTIAL in my_gather_mask is + * equivalent to allowing the non-use of either form of Gather here. + */ + if (my_entry->tag == PGPA_TAG_GATHER || + my_entry->tag == PGPA_TAG_GATHER_MERGE) + { + if (!just_one_rel) + my_gather_mask = PGS_CONSIDER_NONPARTIAL; + else if (my_entry->tag == PGPA_TAG_GATHER) + my_gather_mask = PGS_GATHER; + else + my_gather_mask = PGS_GATHER_MERGE; + } + else if (my_entry->tag == PGPA_TAG_NO_GATHER) + { + Assert(just_one_rel); + my_gather_mask = PGS_CONSIDER_NONPARTIAL; + } + + /* + * If we set my_gather_mask up above, then we (1) make a note if the + * advice conflicted, (2) remember the mask value, and (3) remember + * whether this was a full or partial match. + */ + if (my_gather_mask != 0) + { + if (gather_mask != 0 && gather_mask != my_gather_mask) + gather_conflict = true; + gather_mask = my_gather_mask; + if (just_one_rel) + gather_full_match = bms_add_member(gather_full_match, i); + else + gather_partial_match = bms_add_member(gather_partial_match, i); + } + } + + /* Enforce choice of index. */ + if (scan_entry != NULL && !scan_type_conflict && + (scan_entry->tag == PGPA_TAG_INDEX_SCAN || + scan_entry->tag == PGPA_TAG_INDEX_ONLY_SCAN)) + { + pgpa_index_target *itarget = scan_entry->target->itarget; + IndexOptInfo *matched_index = NULL; + + foreach_node(IndexOptInfo, index, rel->indexlist) + { + char *relname = get_rel_name(index->indexoid); + Oid nspoid = get_rel_namespace(index->indexoid); + char *relnamespace = get_namespace_name_or_temp(nspoid); + + if (strcmp(itarget->indname, relname) == 0 && + (itarget->indnamespace == NULL || + strcmp(itarget->indnamespace, relnamespace) == 0)) + { + matched_index = index; + break; + } + } + + if (matched_index == NULL) + { + /* Don't force the scan type if the index doesn't exist. */ + scan_type = all_scan_mask; + + /* Mark advice as inapplicable. */ + pgpa_trove_set_flags(scan_entries, scan_type_indexes, + PGPA_FB_INAPPLICABLE); + } + else + { + /* Disable every other index. */ + foreach_node(IndexOptInfo, index, rel->indexlist) + { + if (index != matched_index) + index->disabled = true; + } + } + } + + /* + * Mark all the scan method entries as fully matched; and if they specify + * different things, mark them all as conflicting. + */ + flags = PGPA_FB_MATCH_PARTIAL | PGPA_FB_MATCH_FULL; + if (scan_type_conflict) + flags |= PGPA_FB_CONFLICTING; + pgpa_trove_set_flags(scan_entries, scan_type_indexes, flags); + pgpa_trove_set_flags(rel_entries, scan_type_rel_indexes, flags); + + /* + * Mark every Gather-related piece of advice as partially matched. Mark + * the ones that included this relation as a target by itself as fully + * matched. If there was a conflict, mark them all as conflicting. + */ + flags = PGPA_FB_MATCH_PARTIAL; + if (gather_conflict) + flags |= PGPA_FB_CONFLICTING; + pgpa_trove_set_flags(rel_entries, gather_partial_match, flags); + flags |= PGPA_FB_MATCH_FULL; + pgpa_trove_set_flags(rel_entries, gather_full_match, flags); + + /* + * Enforce restrictions on the scan type and use of Gather/Gather Merge. + * Only clear bits here, so that we still respect the enable_* GUCs. Do + * nothing in cases where the advice on a single topic conflicts. + */ + if (scan_type != all_scan_mask && !scan_type_conflict) + rel->pgs_mask &= ~(all_scan_mask & ~scan_type); + if (gather_mask != 0 && !gather_conflict) + { + uint64 all_gather_mask; + + all_gather_mask = + PGS_GATHER | PGS_GATHER_MERGE | PGS_CONSIDER_NONPARTIAL; + rel->pgs_mask &= ~(all_gather_mask & ~gather_mask); + } +} + +/* + * Add feedback entries for one trove slice to the provided list and + * return the resulting list. + * + * Feedback entries are generated from the trove entry's flags. It's assumed + * that the caller has already set all relevant flags with the exception of + * PGPA_FB_FAILED. We set that flag here if appropriate. + */ +static List * +pgpa_planner_append_feedback(List *list, pgpa_trove *trove, + pgpa_trove_lookup_type type, + pgpa_identifier *rt_identifiers, + pgpa_plan_walker_context *walker) +{ + pgpa_trove_entry *entries; + int nentries; + + pgpa_trove_lookup_all(trove, type, &entries, &nentries); + for (int i = 0; i < nentries; ++i) + { + pgpa_trove_entry *entry = &entries[i]; + DefElem *item; + + /* + * If this entry was fully matched, check whether generating advice + * from this plan would produce such an entry. If not, label the entry + * as failed. + */ + if ((entry->flags & PGPA_FB_MATCH_FULL) != 0 && + !pgpa_walker_would_advise(walker, rt_identifiers, + entry->tag, entry->target)) + entry->flags |= PGPA_FB_FAILED; + + item = makeDefElem(pgpa_cstring_trove_entry(entry), + (Node *) makeInteger(entry->flags), -1); + list = lappend(list, item); + } + + return list; +} + +/* + * Emit a WARNING to tell the user about a problem with the supplied plan + * advice. + */ +void +pgpa_planner_feedback_warning(List *feedback) +{ + StringInfoData detailbuf; + StringInfoData flagbuf; + + /* Quick exit if there's no feedback. */ + if (feedback == NIL) + return; + + /* Initialize buffers. */ + initStringInfo(&detailbuf); + initStringInfo(&flagbuf); + + /* Main loop. */ + foreach_node(DefElem, item, feedback) + { + int flags = defGetInt32(item); + + /* + * Don't emit anything if it was fully matched with no problems found. + * + * NB: Feedback should never be marked fully matched without also + * being marked partially matched. + */ + if (flags == (PGPA_FB_MATCH_PARTIAL | PGPA_FB_MATCH_FULL)) + continue; + + /* + * Terminate each detail line except the last with a newline. This is + * also a convenient place to reset flagbuf. + */ + if (detailbuf.len > 0) + { + appendStringInfoChar(&detailbuf, '\n'); + resetStringInfo(&flagbuf); + } + + /* Generate output. */ + pgpa_trove_append_flags(&flagbuf, flags); + appendStringInfo(&detailbuf, "advice %s feedback is \"%s\"", + item->defname, flagbuf.data); + } + + /* Emit the warning, if any problems were found. */ + if (detailbuf.len > 0) + ereport(WARNING, + errmsg("supplied plan advice was not enforced"), + errdetail("%s", detailbuf.data)); +} + +/* + * Get or create the pgpa_planner_info for the given PlannerInfo. + */ +static pgpa_planner_info * +pgpa_planner_get_proot(pgpa_planner_state *pps, PlannerInfo *root) +{ + pgpa_planner_info *new_proot; + + /* + * If pps->last_proot isn't populated, there are no pgpa_planner_info + * objects yet, so we can drop through and create a new one. Otherwise, + * search for an object with a matching name, and drop through only if + * none is found. + */ + if (pps->last_proot != NULL) + { + if (root->plan_name == NULL) + { + if (pps->last_proot->plan_name == NULL) + return pps->last_proot; + + foreach_ptr(pgpa_planner_info, proot, pps->proots) + { + if (proot->plan_name == NULL) + { + pps->last_proot = proot; + return proot; + } + } + } + else + { + if (pps->last_proot->plan_name != NULL && + strcmp(pps->last_proot->plan_name, root->plan_name) == 0) + return pps->last_proot; + + foreach_ptr(pgpa_planner_info, proot, pps->proots) + { + if (proot->plan_name != NULL && + strcmp(proot->plan_name, root->plan_name) == 0) + { + pps->last_proot = proot; + return proot; + } + } + } + } + + /* Create new object. */ + new_proot = palloc0_object(pgpa_planner_info); + + /* Set plan name and alternative plan name. */ + new_proot->plan_name = root->plan_name; + new_proot->alternative_plan_name = root->alternative_plan_name; + + /* + * If the newly-created proot shares an alternative_plan_name with one or + * more others, all should have the is_alternative_plan flag set. + */ + foreach_ptr(pgpa_planner_info, other_proot, pps->proots) + { + if (strings_equal_or_both_null(new_proot->alternative_plan_name, + other_proot->alternative_plan_name)) + { + new_proot->is_alternative_plan = true; + other_proot->is_alternative_plan = true; + } + } + + /* + * Outermost query level always has rtoffset 0; other rtoffset values are + * computed later. + */ + if (root->plan_name == NULL) + { + new_proot->has_rtoffset = true; + new_proot->rtoffset = 0; + } + + /* Add to list and make it most recently used. */ + pps->proots = lappend(pps->proots, new_proot); + pps->last_proot = new_proot; + + return new_proot; +} + +/* + * Compute the range table identifier for one relation and save it for future + * use. + */ +static void +pgpa_compute_rt_identifier(pgpa_planner_info *proot, PlannerInfo *root, + RelOptInfo *rel) +{ + pgpa_identifier *rid; + + /* Allocate or extend the proot's rid_array as necessary. */ + if (proot->rid_array_size < rel->relid) + { + int new_size = pg_nextpower2_32(Max(rel->relid, 8)); + + if (proot->rid_array_size == 0) + proot->rid_array = palloc0_array(pgpa_identifier, new_size); + else + proot->rid_array = repalloc0_array(proot->rid_array, + pgpa_identifier, + proot->rid_array_size, + new_size); + proot->rid_array_size = new_size; + } + + /* Save relation identifier details for this RTI if not already done. */ + rid = &proot->rid_array[rel->relid - 1]; + if (rid->alias_name == NULL) + pgpa_compute_identifier_by_rti(root, rel->relid, rid); +} + +/* + * Compute the range table offset for each pgpa_planner_info for which it + * is possible to meaningfully do so. + * + * For pgpa_planner_info objects for which no RT offset can be computed, + * clear sj_unique_rels, which is meaningless in such cases. + */ +static void +pgpa_compute_rt_offsets(pgpa_planner_state *pps, PlannedStmt *pstmt) +{ + foreach_ptr(pgpa_planner_info, proot, pps->proots) + { + /* For the top query level, we've previously set rtoffset 0. */ + if (proot->plan_name == NULL) + { + Assert(proot->has_rtoffset); + continue; + } + + /* + * It's not guaranteed that every plan name we saw during planning has + * a SubPlanRTInfo, but any that do not certainly don't appear in the + * final range table. + */ + foreach_node(SubPlanRTInfo, rtinfo, pstmt->subrtinfos) + { + if (strcmp(proot->plan_name, rtinfo->plan_name) == 0) + { + /* + * If rtinfo->dummy is set, then the subquery's range table + * will only have been partially copied to the final range + * table. Specifically, only RTE_RELATION entries and + * RTE_SUBQUERY entries that were once RTE_RELATION entries + * will be copied, as per add_rtes_to_flat_rtable. Therefore, + * there's no fixed rtoffset that we can apply to the RTIs + * used during planning to locate the corresponding relations. + */ + if (!rtinfo->dummy) + { + Assert(!proot->has_rtoffset); + proot->has_rtoffset = true; + proot->rtoffset = rtinfo->rtoffset; + } + break; + } + } + + /* + * If we didn't end up setting has_rtoffset, then it will not be + * possible to make any effective use of sj_unique_rels, and it also + * won't be important to do so. So just throw the list away to avoid + * confusing pgpa_plan_walker. + */ + if (!proot->has_rtoffset) + proot->sj_unique_rels = NIL; + } +} + +/* + * Validate that the range table identifiers we were able to generate during + * planning match the ones we generated from the final plan. + */ +static void +pgpa_validate_rt_identifiers(pgpa_planner_state *pps, PlannedStmt *pstmt) +{ +#ifdef USE_ASSERT_CHECKING + pgpa_identifier *rt_identifiers; + Index rtable_length = list_length(pstmt->rtable); + + /* Create identifiers from the planned statement. */ + rt_identifiers = pgpa_create_identifiers_for_planned_stmt(pstmt); + + /* Iterate over identifiers created during planning, so we can compare. */ + foreach_ptr(pgpa_planner_info, proot, pps->proots) + { + if (!proot->has_rtoffset) + continue; + + for (int rti = 1; rti <= proot->rid_array_size; ++rti) + { + Index flat_rti = proot->rtoffset + rti; + pgpa_identifier *rid1 = &proot->rid_array[rti - 1]; + pgpa_identifier *rid2; + + if (rid1->alias_name == NULL) + continue; + + Assert(flat_rti <= rtable_length); + rid2 = &rt_identifiers[flat_rti - 1]; + Assert(strcmp(rid1->alias_name, rid2->alias_name) == 0); + Assert(rid1->occurrence == rid2->occurrence); + Assert(strings_equal_or_both_null(rid1->partnsp, rid2->partnsp)); + Assert(strings_equal_or_both_null(rid1->partrel, rid2->partrel)); + Assert(strings_equal_or_both_null(rid1->plan_name, + rid2->plan_name)); + } + } +#endif +} + +/* + * Convert a bitmapset to a C string of comma-separated integers. + */ +static char * +pgpa_bms_to_cstring(Bitmapset *bms) +{ + StringInfoData buf; + int x = -1; + + if (bms_is_empty(bms)) + return "none"; + + initStringInfo(&buf); + while ((x = bms_next_member(bms, x)) >= 0) + { + if (buf.len > 0) + appendStringInfo(&buf, ", %d", x); + else + appendStringInfo(&buf, "%d", x); + } + + return buf.data; +} + +/* + * Convert a JoinType to a C string. + */ +static const char * +pgpa_jointype_to_cstring(JoinType jointype) +{ + switch (jointype) + { + case JOIN_INNER: + return "inner"; + case JOIN_LEFT: + return "left"; + case JOIN_FULL: + return "full"; + case JOIN_RIGHT: + return "right"; + case JOIN_SEMI: + return "semi"; + case JOIN_ANTI: + return "anti"; + case JOIN_RIGHT_SEMI: + return "right semi"; + case JOIN_RIGHT_ANTI: + return "right anti"; + case JOIN_UNIQUE_OUTER: + return "unique outer"; + case JOIN_UNIQUE_INNER: + return "unique inner"; + } + return "???"; +} diff --git a/contrib/pg_plan_advice/pgpa_planner.h b/contrib/pg_plan_advice/pgpa_planner.h new file mode 100644 index 0000000000000..366142a0c92ef --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_planner.h @@ -0,0 +1,82 @@ +/*------------------------------------------------------------------------- + * + * pgpa_planner.h + * planner integration for pg_plan_advice + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_planner.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_PLANNER_H +#define PGPA_PLANNER_H + +#include "pgpa_identifier.h" + +extern void pgpa_planner_install_hooks(void); + +/* + * Per-PlannerInfo information that we gather during query planning. + */ +typedef struct pgpa_planner_info +{ + /* Plan name taken from the corresponding PlannerInfo; NULL at top level. */ + char *plan_name; + + /* + * If the corresponding PlannerInfo has an alternative_root, then this is + * the plan name from that PlannerInfo; otherwise, it is the same as + * plan_name. + * + * is_alternative_plan is set to true for every pgpa_planner_info that + * shares an alternative_plan_name with at least one other, and to false + * otherwise. + */ + char *alternative_plan_name; + bool is_alternative_plan; + + /* Relation identifiers computed for baserels at this query level. */ + pgpa_identifier *rid_array; + int rid_array_size; + + /* + * If has_rtoffset is true, then rtoffset is the offset required to align + * RTIs for this query level with RTIs from the final, flattened + * rangetable. If has_rtoffset is false, then this subquery's range table + * wasn't copied, or was only partially copied, into the final range + * table. (Note that we can't determine the rtoffset values until the + * final range table actually exists; before that time, has_rtoffset will + * be false everywhere except at the top level.) + */ + bool has_rtoffset; + Index rtoffset; + + /* + * List of Bitmapset objects. Each represents the relid set of a relation + * that the planner considers making unique during semijoin planning. + * + * When generating advice, we should emit either SEMIJOIN_UNIQUE advice or + * SEMIJOIN_NON_UNIQUE advice for each semijoin depending on whether we + * chose to implement it as a semijoin or whether we instead chose to make + * the nullable side unique and then perform an inner join. When the + * make-unique strategy is not chosen, it's not easy to tell from the + * final plan tree whether it was considered. That's awkward, because we + * don't want to emit useless SEMIJOIN_NON_UNIQUE advice when there was no + * decision to be made. This list lets the plan tree walker know in which + * cases that approach was considered, so that it doesn't have to guess. + */ + List *sj_unique_rels; +} pgpa_planner_info; + +/* + * When set to a value greater than zero, indicates that advice should be + * generated during query planning even in the absence of obvious reasons to + * do so. See pg_plan_advice_request_advice_generation(). + */ +extern int pgpa_planner_generate_advice; + +/* Must be exported for use by test_plan_advice */ +extern PGDLLEXPORT void pgpa_planner_feedback_warning(List *feedback); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_scan.c b/contrib/pg_plan_advice/pgpa_scan.c new file mode 100644 index 0000000000000..21b58a0ac4274 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_scan.c @@ -0,0 +1,289 @@ +/*------------------------------------------------------------------------- + * + * pgpa_scan.c + * analysis of scans in Plan trees + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_scan.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "pgpa_scan.h" +#include "pgpa_walker.h" + +#include "nodes/parsenodes.h" +#include "parser/parsetree.h" + +static pgpa_scan *pgpa_make_scan(pgpa_plan_walker_context *walker, Plan *plan, + pgpa_scan_strategy strategy, + Bitmapset *relids); + + +static RTEKind unique_nonjoin_rtekind(Bitmapset *relids, List *rtable); + +/* + * Build a pgpa_scan object for a Plan node and update the plan walker + * context as appropriate. If this is an Append or MergeAppend scan, also + * build pgpa_scan for any scans that were consolidated into this one by + * Append/MergeAppend pull-up. + * + * If there is at least one ElidedNode for this plan node, pass the uppermost + * one as elided_node, else pass NULL. + * + * Set the 'beneath_any_gather' node if we are underneath a Gather or + * Gather Merge node (except for a single-copy Gather node, for which + * GATHER or GATHER_MERGE advice should not be emitted). + * + * Set the 'within_join_problem' flag if we're inside of a join problem and + * not otherwise. + */ +pgpa_scan * +pgpa_build_scan(pgpa_plan_walker_context *walker, Plan *plan, + ElidedNode *elided_node, + bool beneath_any_gather, bool within_join_problem) +{ + pgpa_scan_strategy strategy = PGPA_SCAN_ORDINARY; + Bitmapset *relids = NULL; + int rti = -1; + List *child_append_relid_sets = NIL; + NodeTag nodetype = nodeTag(plan); + + if (elided_node != NULL) + { + nodetype = elided_node->elided_type; + relids = elided_node->relids; + + /* + * If setrefs processing elided an Append or MergeAppend node that had + * only one surviving child, it could be either a partitionwise + * operation or a setop over subqueries, depending on the rtekind. + * + * A setop over subqueries, or a trivial SubqueryScan that was elided, + * is an "ordinary" scan i.e. one for which we do not need to generate + * advice because the planner has not made any meaningful choice. + * + * Note that the PGPA_SCAN_PARTITIONWISE case also includes + * partitionwise joins; this module considers those to be a form of + * scan, since they lack internal structure that we can decompose. + * + * Note also that it's possible for relids to be NULL here, if the + * elided Append node is part of a partitionwise aggregate. In that + * case, it doesn't matter what strategy we choose, but we do need to + * avoid calling unique_nonjoin_rtekind(), which would fail an + * assertion. + */ + if ((nodetype == T_Append || nodetype == T_MergeAppend) && + relids != NULL && + unique_nonjoin_rtekind(relids, + walker->pstmt->rtable) == RTE_RELATION) + strategy = PGPA_SCAN_PARTITIONWISE; + else + strategy = PGPA_SCAN_ORDINARY; + + /* Join RTIs can be present, but advice never refers to them. */ + relids = pgpa_filter_out_join_relids(relids, walker->pstmt->rtable); + } + else if ((rti = pgpa_scanrelid(plan)) != 0) + { + relids = bms_make_singleton(rti); + + switch (nodeTag(plan)) + { + case T_SeqScan: + strategy = PGPA_SCAN_SEQ; + break; + case T_BitmapHeapScan: + strategy = PGPA_SCAN_BITMAP_HEAP; + break; + case T_IndexScan: + strategy = PGPA_SCAN_INDEX; + break; + case T_IndexOnlyScan: + strategy = PGPA_SCAN_INDEX_ONLY; + break; + case T_TidScan: + case T_TidRangeScan: + strategy = PGPA_SCAN_TID; + break; + default: + + /* + * This case includes a ForeignScan targeting a single + * relation; no other strategy is possible in that case, but + * see below, where things are different in multi-relation + * cases. + */ + strategy = PGPA_SCAN_ORDINARY; + break; + } + } + else if (pgpa_is_scan_level_materialize(plan)) + { + /* + * Non-repeatable tablesample methods can be wrapped in a Materialize + * node that must be treated as part of the scan itself. See + * set_tablesample_rel_pathlist(). + */ + rti = pgpa_scanrelid(plan->lefttree); + relids = bms_make_singleton(rti); + strategy = PGPA_SCAN_ORDINARY; + } + else if ((relids = pgpa_relids(plan)) != NULL) + { + switch (nodeTag(plan)) + { + case T_ForeignScan: + + /* + * If multiple relations are being targeted by a single + * foreign scan, then the foreign join has been pushed to the + * remote side, and we want that to be reflected in the + * generated advice. + */ + strategy = PGPA_SCAN_FOREIGN; + break; + case T_Append: + + /* + * Append nodes can represent partitionwise scans of a + * relation, but when they implement a set operation, they are + * just ordinary scans. + */ + if (unique_nonjoin_rtekind(relids, walker->pstmt->rtable) + == RTE_RELATION) + strategy = PGPA_SCAN_PARTITIONWISE; + else + strategy = PGPA_SCAN_ORDINARY; + + /* Be sure to account for pulled-up scans. */ + child_append_relid_sets = + ((Append *) plan)->child_append_relid_sets; + break; + case T_MergeAppend: + /* Same logic here as for Append, above. */ + if (unique_nonjoin_rtekind(relids, walker->pstmt->rtable) + == RTE_RELATION) + strategy = PGPA_SCAN_PARTITIONWISE; + else + strategy = PGPA_SCAN_ORDINARY; + + /* Be sure to account for pulled-up scans. */ + child_append_relid_sets = + ((MergeAppend *) plan)->child_append_relid_sets; + break; + default: + strategy = PGPA_SCAN_ORDINARY; + break; + } + + + /* Join RTIs can be present, but advice never refers to them. */ + relids = pgpa_filter_out_join_relids(relids, walker->pstmt->rtable); + } + + /* + * If this is an Append or MergeAppend node into which subordinate Append + * or MergeAppend paths were merged, each of those merged paths is + * effectively another scan for which we need to account. + */ + foreach_node(Bitmapset, child_relids, child_append_relid_sets) + { + Bitmapset *child_nonjoin_relids; + + child_nonjoin_relids = + pgpa_filter_out_join_relids(child_relids, + walker->pstmt->rtable); + (void) pgpa_make_scan(walker, plan, strategy, + child_nonjoin_relids); + } + + /* + * If this plan node has no associated RTIs, it's not a scan. When the + * 'within_join_problem' flag is set, that's unexpected, so throw an + * error, else return quietly. + */ + if (relids == NULL) + { + if (within_join_problem) + elog(ERROR, "plan node has no RTIs: %d", (int) nodeTag(plan)); + return NULL; + } + + /* + * Add the appropriate set of RTIs to walker->no_gather_scans. + * + * Add nothing if we're beneath a Gather or Gather Merge node, since + * NO_GATHER advice is clearly inappropriate in that situation. + * + * Add nothing if this is an Append or MergeAppend node, whether or not + * elided. We'll emit NO_GATHER() for the underlying scan, which is good + * enough. + */ + if (!beneath_any_gather && nodetype != T_Append && + nodetype != T_MergeAppend) + walker->no_gather_scans = + bms_add_members(walker->no_gather_scans, relids); + + /* Caller tells us whether NO_GATHER() advice for this scan is needed. */ + return pgpa_make_scan(walker, plan, strategy, relids); +} + +/* + * Create a single pgpa_scan object and update the pgpa_plan_walker_context. + */ +static pgpa_scan * +pgpa_make_scan(pgpa_plan_walker_context *walker, Plan *plan, + pgpa_scan_strategy strategy, Bitmapset *relids) +{ + pgpa_scan *scan; + + /* Create the scan object. */ + scan = palloc(sizeof(pgpa_scan)); + scan->plan = plan; + scan->strategy = strategy; + scan->relids = relids; + + /* Add it to the appropriate list. */ + walker->scans[scan->strategy] = lappend(walker->scans[scan->strategy], + scan); + + return scan; +} + +/* + * Determine the unique rtekind of a set of relids. + */ +static RTEKind +unique_nonjoin_rtekind(Bitmapset *relids, List *rtable) +{ + int rti = -1; + bool first = true; + RTEKind rtekind = RTE_RELATION; /* silence compiler warning */ + + Assert(relids != NULL); + + while ((rti = bms_next_member(relids, rti)) >= 0) + { + RangeTblEntry *rte = rt_fetch(rti, rtable); + + if (rte->rtekind == RTE_JOIN) + continue; + + if (first) + { + rtekind = rte->rtekind; + first = false; + } + else if (rtekind != rte->rtekind) + elog(ERROR, "rtekind mismatch: %d vs. %d", + rtekind, rte->rtekind); + } + + if (first) + elog(ERROR, "no non-RTE_JOIN RTEs found"); + + return rtekind; +} diff --git a/contrib/pg_plan_advice/pgpa_scan.h b/contrib/pg_plan_advice/pgpa_scan.h new file mode 100644 index 0000000000000..391c1a4b59656 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_scan.h @@ -0,0 +1,85 @@ +/*------------------------------------------------------------------------- + * + * pgpa_scan.h + * analysis of scans in Plan trees + * + * For purposes of this module, a "scan" includes (1) single plan nodes that + * scan multiple RTIs, such as a degenerate Result node that replaces what + * would otherwise have been a join, and (2) Append and MergeAppend nodes + * implementing a partitionwise scan or a partitionwise join. Said + * differently, scans are the leaves of the join tree for a single join + * problem. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_scan.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_SCAN_H +#define PGPA_SCAN_H + +#include "nodes/plannodes.h" + +typedef struct pgpa_plan_walker_context pgpa_plan_walker_context; + +/* + * Scan strategies. + * + * PGPA_SCAN_ORDINARY is any scan strategy that isn't interesting to us + * because there is no meaningful planner decision involved. For example, + * the only way to scan a subquery is a SubqueryScan, and the only way to + * scan a VALUES construct is a ValuesScan. We need not care exactly which + * type of planner node was used in such cases, because the same thing will + * happen when replanning. + * + * PGPA_SCAN_ORDINARY also includes Result nodes that correspond to scans + * or even joins that are proved empty. We don't know whether or not the scan + * or join will still be provably empty at replanning time, but if it is, + * then no scan-type advice is needed, and if it's not, we can't recommend + * a scan type based on the current plan. + * + * PGPA_SCAN_PARTITIONWISE also lumps together scans and joins: this can + * be either a partitionwise scan of a partitioned table or a partitionwise + * join between several partitioned tables. Note that all decisions about + * whether or not to use partitionwise join are meaningful: no matter what + * we decided this time, we could do more or fewer things partitionwise the + * next time. + * + * PGPA_SCAN_FOREIGN is only used when there's more than one relation involved; + * a single-table foreign scan is classified as ordinary, since there is no + * decision to make in that case. + * + * Other scan strategies map one-to-one to plan nodes. + */ +typedef enum +{ + PGPA_SCAN_ORDINARY = 0, + PGPA_SCAN_SEQ, + PGPA_SCAN_BITMAP_HEAP, + PGPA_SCAN_FOREIGN, + PGPA_SCAN_INDEX, + PGPA_SCAN_INDEX_ONLY, + PGPA_SCAN_PARTITIONWISE, + PGPA_SCAN_TID + /* update NUM_PGPA_SCAN_STRATEGY if you add anything here */ +} pgpa_scan_strategy; + +#define NUM_PGPA_SCAN_STRATEGY ((int) PGPA_SCAN_TID + 1) + +/* + * All of the details we need regarding a scan. + */ +typedef struct pgpa_scan +{ + Plan *plan; + pgpa_scan_strategy strategy; + Bitmapset *relids; +} pgpa_scan; + +extern pgpa_scan *pgpa_build_scan(pgpa_plan_walker_context *walker, Plan *plan, + ElidedNode *elided_node, + bool beneath_any_gather, + bool within_join_problem); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_scanner.l b/contrib/pg_plan_advice/pgpa_scanner.l new file mode 100644 index 0000000000000..3b3be6eb7276b --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_scanner.l @@ -0,0 +1,297 @@ +%top{ +/* + * Scanner for plan advice + * + * Copyright (c) 2000-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_scanner.l + */ +#include "postgres.h" + +#include "common/string.h" +#include "nodes/miscnodes.h" +#include "parser/scansup.h" + +#include "pgpa_ast.h" +#include "pgpa_parser.h" + +/* + * Extra data that we pass around when during scanning. + * + * 'litbuf' is used to implement the exclusive state, which handles + * double-quoted identifiers. + */ +typedef struct pgpa_yy_extra_type +{ + StringInfoData litbuf; +} pgpa_yy_extra_type; + +} + +%{ +/* LCOV_EXCL_START */ + +#define YY_DECL \ + extern int pgpa_yylex(union YYSTYPE *yylval_param, List **result, \ + char **parse_error_msg_p, yyscan_t yyscanner) + +/* No reason to constrain amount of data slurped */ +#define YY_READ_BUF_SIZE 16777216 + +/* Avoid exit() on fatal scanner errors (a bit ugly -- see yy_fatal_error) */ +#undef fprintf +#define fprintf(file, fmt, msg) fprintf_to_ereport(fmt, msg) + +static void +fprintf_to_ereport(const char *fmt, const char *msg) +{ + ereport(ERROR, (errmsg_internal("%s", msg))); +} +%} + +%option reentrant +%option bison-bridge +%option 8bit +%option never-interactive +%option nodefault +%option noinput +%option nounput +%option noyywrap +%option noyyalloc +%option noyyrealloc +%option noyyfree +%option warn +%option prefix="pgpa_yy" +%option extra-type="pgpa_yy_extra_type *" + +/* + * What follows is a severely stripped-down version of the core scanner. We + * only care about recognizing identifiers with or without identifier quoting + * (i.e. double-quoting), decimal integers, and a small handful of other + * things. Keep these rules in sync with src/backend/parser/scan.l. As in that + * file, we use an exclusive state called 'xc' for C-style comments, and an + * exclusive state called 'xd' for double-quoted identifiers. + */ +%x xc +%x xd + +ident_start [A-Za-z\200-\377_] +ident_cont [A-Za-z\200-\377_0-9\$] + +identifier {ident_start}{ident_cont}* + +decdigit [0-9] +decinteger {decdigit}(_?{decdigit})* + +space [ \t\n\r\f\v] +whitespace {space}+ + +dquote \" +xdstart {dquote} +xdstop {dquote} +xddouble {dquote}{dquote} +xdinside [^"]+ + +xcstart \/\* +xcstop \*+\/ +xcinside [^*/]+ + +%% + +{whitespace} { /* ignore */ } + +{identifier} { + char *str; + bool fail; + pgpa_advice_tag_type tag; + + /* + * Unlike the core scanner, we don't truncate identifiers + * here. There is no obvious reason to do so. + */ + str = downcase_identifier(yytext, yyleng, false, false); + yylval->str = str; + + /* + * If it's not a tag, just return TOK_IDENT; else, return + * a token type based on how further parsing should + * proceed. + */ + tag = pgpa_parse_advice_tag(str, &fail); + if (fail) + return TOK_IDENT; + else if (tag == PGPA_TAG_JOIN_ORDER) + return TOK_TAG_JOIN_ORDER; + else if (tag == PGPA_TAG_INDEX_SCAN || + tag == PGPA_TAG_INDEX_ONLY_SCAN) + return TOK_TAG_INDEX; + else if (tag == PGPA_TAG_SEQ_SCAN || + tag == PGPA_TAG_TID_SCAN || + tag == PGPA_TAG_BITMAP_HEAP_SCAN || + tag == PGPA_TAG_NO_GATHER) + return TOK_TAG_SIMPLE; + else + return TOK_TAG_GENERIC; + } + +{decinteger} { + char *endptr; + + errno = 0; + yylval->integer = strtoint(yytext, &endptr, 10); + if (*endptr != '\0' || errno == ERANGE) + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "integer out of range"); + return TOK_INTEGER; + } + +{xcstart} { + BEGIN(xc); + } + +{xdstart} { + BEGIN(xd); + resetStringInfo(&yyextra->litbuf); + } + +. { return yytext[0]; } + +{xcstop} { + BEGIN(INITIAL); + } + +{xcinside} { + /* discard multiple characters without slash or asterisk */ + } + +. { + /* + * Discard any single character. flex prefers longer + * matches, so this rule will never be picked when we could + * have matched xcstop. + * + * NB: At present, we don't bother to support nested + * C-style comments here, but this logic could be extended + * if that restriction poses a problem. + */ + } + +<> { + BEGIN(INITIAL); + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "unterminated comment"); + } + +{xdstop} { + BEGIN(INITIAL); + if (yyextra->litbuf.len == 0) + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "zero-length delimited identifier"); + yylval->str = pstrdup(yyextra->litbuf.data); + return TOK_IDENT; + } + +{xddouble} { + appendStringInfoChar(&yyextra->litbuf, '"'); + } + +{xdinside} { + appendBinaryStringInfo(&yyextra->litbuf, yytext, yyleng); + } + +<> { + BEGIN(INITIAL); + pgpa_yyerror(result, parse_error_msg_p, yyscanner, + "unterminated quoted identifier"); + } + +%% + +/* LCOV_EXCL_STOP */ + +/* + * Handler for errors while scanning or parsing advice. + * + * bison passes the error message to us via 'message', and the context is + * available via the 'yytext' macro. We assemble those values into a final + * error text and then arrange to pass it back to the caller of pgpa_yyparse() + * by storing it into *parse_error_msg_p. + */ +void +pgpa_yyerror(List **result, char **parse_error_msg_p, yyscan_t yyscanner, + const char *message) +{ + struct yyguts_t *yyg = (struct yyguts_t *) yyscanner; /* needed for yytext + * macro */ + + + /* report only the first error in a parse operation */ + if (*parse_error_msg_p) + return; + + if (yytext[0]) + *parse_error_msg_p = psprintf("%s at or near \"%s\"", message, yytext); + else + *parse_error_msg_p = psprintf("%s at end of input", message); +} + +/* + * Initialize the advice scanner. + * + * This should be called before parsing begins. + */ +void +pgpa_scanner_init(const char *str, yyscan_t *yyscannerp) +{ + yyscan_t yyscanner; + pgpa_yy_extra_type *yyext = palloc0_object(pgpa_yy_extra_type); + + if (yylex_init(yyscannerp) != 0) + elog(ERROR, "yylex_init() failed: %m"); + + yyscanner = *yyscannerp; + + initStringInfo(&yyext->litbuf); + pgpa_yyset_extra(yyext, yyscanner); + + yy_scan_string(str, yyscanner); +} + + +/* + * Shut down the advice scanner. + * + * This should be called after parsing is complete. + */ +void +pgpa_scanner_finish(yyscan_t yyscanner) +{ + yylex_destroy(yyscanner); +} + +/* + * Interface functions to make flex use palloc() instead of malloc(). + * It'd be better to make these static, but flex insists otherwise. + */ + +void * +yyalloc(yy_size_t size, yyscan_t yyscanner) +{ + return palloc(size); +} + +void * +yyrealloc(void *ptr, yy_size_t size, yyscan_t yyscanner) +{ + if (ptr) + return repalloc(ptr, size); + else + return palloc(size); +} + +void +yyfree(void *ptr, yyscan_t yyscanner) +{ + if (ptr) + pfree(ptr); +} diff --git a/contrib/pg_plan_advice/pgpa_trove.c b/contrib/pg_plan_advice/pgpa_trove.c new file mode 100644 index 0000000000000..ca69f3bd3df6e --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_trove.c @@ -0,0 +1,518 @@ +/*------------------------------------------------------------------------- + * + * pgpa_trove.c + * All of the advice given for a particular query, appropriately + * organized for convenient access. + * + * This name comes from the English expression "trove of advice", which + * means a collection of wisdom. This slightly unusual term is chosen + * partly because it seems to fit and partly because it's not presently + * used for anything else, making it easy to grep. Note that, while we + * don't know whether the provided advice is actually wise, it's not our + * job to question the user's choices. + * + * The goal of this module is to make it easy to locate the specific + * bits of advice that pertain to any given part of a query, or to + * determine that there are none. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_trove.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "pg_plan_advice.h" +#include "pgpa_trove.h" + +#include "common/hashfn_unstable.h" + +/* + * An advice trove is organized into a series of "slices", each of which + * contains information about one topic e.g. scan methods. Each slice consists + * of an array of trove entries plus a hash table that we can use to determine + * which ones are relevant to a particular part of the query. + */ +typedef struct pgpa_trove_slice +{ + unsigned nallocated; + unsigned nused; + pgpa_trove_entry *entries; + struct pgpa_trove_entry_hash *hash; +} pgpa_trove_slice; + +/* + * Scan advice is stored into 'scan'; join advice is stored into 'join'; and + * advice that can apply to both cases is stored into 'rel'. This lets callers + * ask just for what's relevant. These slices correspond to the possible values + * of pgpa_trove_lookup_type. + */ +struct pgpa_trove +{ + pgpa_trove_slice join; + pgpa_trove_slice rel; + pgpa_trove_slice scan; +}; + +/* + * We're going to build a hash table to allow clients of this module to find + * relevant advice for a given part of the query quickly. However, we're going + * to use only three of the five key fields as hash keys. There are two reasons + * for this. + * + * First, it's allowable to set partition_schema to NULL to match a partition + * with the correct name in any schema. + * + * Second, we expect the "occurrence" and "partition_schema" portions of the + * relation identifiers to be mostly uninteresting. Most of the time, the + * occurrence field will be 1 and the partition_schema values will all be the + * same. Even when there is some variation, the absolute number of entries + * that have the same values for all three of these key fields should be + * quite small. + */ +typedef struct +{ + const char *alias_name; + const char *partition_name; + const char *plan_name; +} pgpa_trove_entry_key; + +typedef struct +{ + pgpa_trove_entry_key key; + int status; + Bitmapset *indexes; +} pgpa_trove_entry_element; + +static uint32 pgpa_trove_entry_hash_key(pgpa_trove_entry_key key); + +static inline bool +pgpa_trove_entry_compare_key(pgpa_trove_entry_key a, pgpa_trove_entry_key b) +{ + if (strcmp(a.alias_name, b.alias_name) != 0) + return false; + + if (!strings_equal_or_both_null(a.partition_name, b.partition_name)) + return false; + + if (!strings_equal_or_both_null(a.plan_name, b.plan_name)) + return false; + + return true; +} + +#define SH_PREFIX pgpa_trove_entry +#define SH_ELEMENT_TYPE pgpa_trove_entry_element +#define SH_KEY_TYPE pgpa_trove_entry_key +#define SH_KEY key +#define SH_HASH_KEY(tb, key) pgpa_trove_entry_hash_key(key) +#define SH_EQUAL(tb, a, b) pgpa_trove_entry_compare_key(a, b) +#define SH_SCOPE static inline +#define SH_DECLARE +#define SH_DEFINE +#include "lib/simplehash.h" + +static void pgpa_init_trove_slice(pgpa_trove_slice *tslice); +static void pgpa_trove_add_to_slice(pgpa_trove_slice *tslice, + pgpa_advice_tag_type tag, + pgpa_advice_target *target); +static void pgpa_trove_add_to_hash(pgpa_trove_entry_hash *hash, + pgpa_advice_target *target, + int index); +static Bitmapset *pgpa_trove_slice_lookup(pgpa_trove_slice *tslice, + pgpa_identifier *rid); + +/* + * Build a trove of advice from a list of advice items. + * + * Caller can obtain a list of advice items to pass to this function by + * calling pgpa_parse(). + */ +pgpa_trove * +pgpa_build_trove(List *advice_items) +{ + pgpa_trove *trove = palloc_object(pgpa_trove); + + pgpa_init_trove_slice(&trove->join); + pgpa_init_trove_slice(&trove->rel); + pgpa_init_trove_slice(&trove->scan); + + foreach_ptr(pgpa_advice_item, item, advice_items) + { + switch (item->tag) + { + case PGPA_TAG_JOIN_ORDER: + { + pgpa_advice_target *target; + + /* + * For most advice types, each element in the top-level + * list is a separate target, but it's most convenient to + * regard the entirety of a JOIN_ORDER specification as a + * single target. Since it wasn't represented that way + * during parsing, build a surrogate object now. + */ + target = palloc0_object(pgpa_advice_target); + target->ttype = PGPA_TARGET_ORDERED_LIST; + target->children = item->targets; + + pgpa_trove_add_to_slice(&trove->join, + item->tag, target); + } + break; + + case PGPA_TAG_BITMAP_HEAP_SCAN: + case PGPA_TAG_DO_NOT_SCAN: + case PGPA_TAG_INDEX_ONLY_SCAN: + case PGPA_TAG_INDEX_SCAN: + case PGPA_TAG_SEQ_SCAN: + case PGPA_TAG_TID_SCAN: + + /* + * Scan advice. + */ + foreach_ptr(pgpa_advice_target, target, item->targets) + { + /* + * For now, all of our scan types target single relations, + * but in the future this might not be true, e.g. a custom + * scan could replace a join. + */ + Assert(target->ttype == PGPA_TARGET_IDENTIFIER); + pgpa_trove_add_to_slice(&trove->scan, + item->tag, target); + } + break; + + case PGPA_TAG_FOREIGN_JOIN: + case PGPA_TAG_HASH_JOIN: + case PGPA_TAG_MERGE_JOIN_MATERIALIZE: + case PGPA_TAG_MERGE_JOIN_PLAIN: + case PGPA_TAG_NESTED_LOOP_MATERIALIZE: + case PGPA_TAG_NESTED_LOOP_MEMOIZE: + case PGPA_TAG_NESTED_LOOP_PLAIN: + case PGPA_TAG_SEMIJOIN_NON_UNIQUE: + case PGPA_TAG_SEMIJOIN_UNIQUE: + + /* + * Join strategy advice. + */ + foreach_ptr(pgpa_advice_target, target, item->targets) + { + pgpa_trove_add_to_slice(&trove->join, + item->tag, target); + } + break; + + case PGPA_TAG_PARTITIONWISE: + case PGPA_TAG_GATHER: + case PGPA_TAG_GATHER_MERGE: + case PGPA_TAG_NO_GATHER: + + /* + * Advice about a RelOptInfo relevant to both scans and joins. + */ + foreach_ptr(pgpa_advice_target, target, item->targets) + { + pgpa_trove_add_to_slice(&trove->rel, + item->tag, target); + } + break; + } + } + + return trove; +} + +/* + * Search a trove of advice for relevant entries. + * + * All parameters are input parameters except for *result, which is an output + * parameter used to return results to the caller. + */ +void +pgpa_trove_lookup(pgpa_trove *trove, pgpa_trove_lookup_type type, + int nrids, pgpa_identifier *rids, pgpa_trove_result *result) +{ + pgpa_trove_slice *tslice; + Bitmapset *indexes; + + Assert(nrids > 0); + + if (type == PGPA_TROVE_LOOKUP_SCAN) + tslice = &trove->scan; + else if (type == PGPA_TROVE_LOOKUP_JOIN) + tslice = &trove->join; + else + tslice = &trove->rel; + + indexes = pgpa_trove_slice_lookup(tslice, &rids[0]); + for (int i = 1; i < nrids; ++i) + { + Bitmapset *other_indexes; + + /* + * If the caller is asking about two relations that aren't part of the + * same subquery, they've messed up. + */ + Assert(strings_equal_or_both_null(rids[0].plan_name, + rids[i].plan_name)); + + other_indexes = pgpa_trove_slice_lookup(tslice, &rids[i]); + indexes = bms_union(indexes, other_indexes); + } + + result->entries = tslice->entries; + result->indexes = indexes; +} + +/* + * Return all entries in a trove slice to the caller. + * + * The first two arguments are input arguments, and the remainder are output + * arguments. + */ +void +pgpa_trove_lookup_all(pgpa_trove *trove, pgpa_trove_lookup_type type, + pgpa_trove_entry **entries, int *nentries) +{ + pgpa_trove_slice *tslice; + + if (type == PGPA_TROVE_LOOKUP_SCAN) + tslice = &trove->scan; + else if (type == PGPA_TROVE_LOOKUP_JOIN) + tslice = &trove->join; + else + tslice = &trove->rel; + + *entries = tslice->entries; + *nentries = tslice->nused; +} + +/* + * Convert a trove entry to an item of plan advice that would produce it. + */ +char * +pgpa_cstring_trove_entry(pgpa_trove_entry *entry) +{ + StringInfoData buf; + + initStringInfo(&buf); + appendStringInfoString(&buf, pgpa_cstring_advice_tag(entry->tag)); + + /* JOIN_ORDER tags are transformed by pgpa_build_trove; undo that here */ + if (entry->tag != PGPA_TAG_JOIN_ORDER) + appendStringInfoChar(&buf, '('); + else + Assert(entry->target->ttype == PGPA_TARGET_ORDERED_LIST); + + pgpa_format_advice_target(&buf, entry->target); + + if (entry->target->itarget != NULL) + { + appendStringInfoChar(&buf, ' '); + pgpa_format_index_target(&buf, entry->target->itarget); + } + + if (entry->tag != PGPA_TAG_JOIN_ORDER) + appendStringInfoChar(&buf, ')'); + + return buf.data; +} + +/* + * Set PGPA_FB_* flags on a set of trove entries. + */ +void +pgpa_trove_set_flags(pgpa_trove_entry *entries, Bitmapset *indexes, int flags) +{ + int i = -1; + + while ((i = bms_next_member(indexes, i)) >= 0) + { + pgpa_trove_entry *entry = &entries[i]; + + entry->flags |= flags; + } +} + +/* + * Append a string representation of the specified PGPA_FB_* flags to the + * given StringInfo. + */ +void +pgpa_trove_append_flags(StringInfo buf, int flags) +{ + if ((flags & PGPA_FB_MATCH_FULL) != 0) + { + Assert((flags & PGPA_FB_MATCH_PARTIAL) != 0); + appendStringInfoString(buf, "matched"); + } + else if ((flags & PGPA_FB_MATCH_PARTIAL) != 0) + appendStringInfoString(buf, "partially matched"); + else + appendStringInfoString(buf, "not matched"); + if ((flags & PGPA_FB_INAPPLICABLE) != 0) + appendStringInfoString(buf, ", inapplicable"); + if ((flags & PGPA_FB_CONFLICTING) != 0) + appendStringInfoString(buf, ", conflicting"); + if ((flags & PGPA_FB_FAILED) != 0) + appendStringInfoString(buf, ", failed"); +} + +/* + * Add a new advice target to an existing pgpa_trove_slice object. + */ +static void +pgpa_trove_add_to_slice(pgpa_trove_slice *tslice, + pgpa_advice_tag_type tag, + pgpa_advice_target *target) +{ + pgpa_trove_entry *entry; + + if (tslice->nused >= tslice->nallocated) + { + int new_allocated; + + new_allocated = tslice->nallocated * 2; + tslice->entries = repalloc_array(tslice->entries, pgpa_trove_entry, + new_allocated); + tslice->nallocated = new_allocated; + } + + entry = &tslice->entries[tslice->nused]; + entry->tag = tag; + entry->target = target; + entry->flags = 0; + + pgpa_trove_add_to_hash(tslice->hash, target, tslice->nused); + + tslice->nused++; +} + +/* + * Update the hash table for a newly-added advice target. + */ +static void +pgpa_trove_add_to_hash(pgpa_trove_entry_hash *hash, pgpa_advice_target *target, + int index) +{ + pgpa_trove_entry_key key; + pgpa_trove_entry_element *element; + bool found; + + /* For non-identifiers, add entries for all descendants. */ + if (target->ttype != PGPA_TARGET_IDENTIFIER) + { + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + pgpa_trove_add_to_hash(hash, child_target, index); + } + return; + } + + /* Sanity checks. */ + Assert(target->rid.occurrence > 0); + Assert(target->rid.alias_name != NULL); + + /* Add an entry for this relation identifier. */ + key.alias_name = target->rid.alias_name; + key.partition_name = target->rid.partrel; + key.plan_name = target->rid.plan_name; + element = pgpa_trove_entry_insert(hash, key, &found); + if (!found) + element->indexes = NULL; + element->indexes = bms_add_member(element->indexes, index); +} + +/* + * Create and initialize a new pgpa_trove_slice object. + */ +static void +pgpa_init_trove_slice(pgpa_trove_slice *tslice) +{ + /* + * In an ideal world, we'll make tslice->nallocated big enough that the + * array and hash table will be large enough to contain the number of + * advice items in this trove slice, but a generous default value is not + * good for performance, because pgpa_init_trove_slice() has to zero an + * amount of memory proportional to tslice->nallocated. Hence, we keep the + * starting value quite small, on the theory that advice strings will + * often be relatively short. + */ + tslice->nallocated = 16; + tslice->nused = 0; + tslice->entries = palloc_array(pgpa_trove_entry, tslice->nallocated); + tslice->hash = pgpa_trove_entry_create(CurrentMemoryContext, + tslice->nallocated, NULL); +} + +/* + * Fast hash function for a key consisting of alias_name, partition_name, + * and plan_name. + */ +static uint32 +pgpa_trove_entry_hash_key(pgpa_trove_entry_key key) +{ + fasthash_state hs; + int sp_len; + + fasthash_init(&hs, 0); + + /* alias_name may not be NULL */ + sp_len = fasthash_accum_cstring(&hs, key.alias_name); + + /* partition_name and plan_name, however, can be NULL */ + if (key.partition_name != NULL) + sp_len += fasthash_accum_cstring(&hs, key.partition_name); + if (key.plan_name != NULL) + sp_len += fasthash_accum_cstring(&hs, key.plan_name); + + /* + * hashfn_unstable.h recommends using string length as tweak. It's not + * clear to me what to do if there are multiple strings, so for now I'm + * just using the total of all of the lengths. + */ + return fasthash_final32(&hs, sp_len); +} + +/* + * Look for matching entries. + */ +static Bitmapset * +pgpa_trove_slice_lookup(pgpa_trove_slice *tslice, pgpa_identifier *rid) +{ + pgpa_trove_entry_key key; + pgpa_trove_entry_element *element; + Bitmapset *result = NULL; + + Assert(rid->occurrence >= 1); + + key.alias_name = rid->alias_name; + key.partition_name = rid->partrel; + key.plan_name = rid->plan_name; + + element = pgpa_trove_entry_lookup(tslice->hash, key); + + if (element != NULL) + { + int i = -1; + + while ((i = bms_next_member(element->indexes, i)) >= 0) + { + pgpa_trove_entry *entry = &tslice->entries[i]; + + /* + * We know that this target or one of its descendants matches the + * identifier on the three key fields above, but we don't know + * which descendant or whether the occurrence and schema also + * match. + */ + if (pgpa_identifier_matches_target(rid, entry->target)) + result = bms_add_member(result, i); + } + } + + return result; +} diff --git a/contrib/pg_plan_advice/pgpa_trove.h b/contrib/pg_plan_advice/pgpa_trove.h new file mode 100644 index 0000000000000..f3afc96d666c9 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_trove.h @@ -0,0 +1,84 @@ +/*------------------------------------------------------------------------- + * + * pgpa_trove.h + * All of the advice given for a particular query, appropriately + * organized for convenient access. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_trove.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_TROVE_H +#define PGPA_TROVE_H + +#include "pgpa_ast.h" + +#include "nodes/bitmapset.h" + +typedef struct pgpa_trove pgpa_trove; + +/* + * Each entry in a trove of advice represents the application of a tag to + * a single target. + */ +typedef struct pgpa_trove_entry +{ + pgpa_advice_tag_type tag; + pgpa_advice_target *target; + int flags; +} pgpa_trove_entry; + +/* + * What kind of information does the caller want to find in a trove? + * + * PGPA_TROVE_LOOKUP_SCAN means we're looking for scan advice. + * + * PGPA_TROVE_LOOKUP_JOIN means we're looking for join-related advice. + * This includes join order advice, join method advice, and semijoin-uniqueness + * advice. + * + * PGPA_TROVE_LOOKUP_REL means we're looking for general advice about this + * a RelOptInfo that may correspond to either a scan or a join. This includes + * gather-related advice and partitionwise advice. Note that partitionwise + * advice might seem like join advice, but that's not a helpful way of viewing + * the matter because (1) partitionwise advice is also relevant at the scan + * level and (2) other types of join advice affect only what to do from + * join_path_setup_hook, but partitionwise advice affects what to do in + * joinrel_setup_hook. + */ +typedef enum pgpa_trove_lookup_type +{ + PGPA_TROVE_LOOKUP_JOIN, + PGPA_TROVE_LOOKUP_REL, + PGPA_TROVE_LOOKUP_SCAN +} pgpa_trove_lookup_type; + +/* + * This struct is used to store the result of a trove lookup. For each member + * of "indexes", the entry at the corresponding offset within "entries" is one + * of the results. + */ +typedef struct pgpa_trove_result +{ + pgpa_trove_entry *entries; + Bitmapset *indexes; +} pgpa_trove_result; + +extern pgpa_trove *pgpa_build_trove(List *advice_items); +extern void pgpa_trove_lookup(pgpa_trove *trove, + pgpa_trove_lookup_type type, + int nrids, + pgpa_identifier *rids, + pgpa_trove_result *result); +extern void pgpa_trove_lookup_all(pgpa_trove *trove, + pgpa_trove_lookup_type type, + pgpa_trove_entry **entries, + int *nentries); +extern char *pgpa_cstring_trove_entry(pgpa_trove_entry *entry); +extern void pgpa_trove_set_flags(pgpa_trove_entry *entries, + Bitmapset *indexes, int flags); +extern void pgpa_trove_append_flags(StringInfo buf, int flags); + +#endif diff --git a/contrib/pg_plan_advice/pgpa_walker.c b/contrib/pg_plan_advice/pgpa_walker.c new file mode 100644 index 0000000000000..e49361ae2661a --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_walker.c @@ -0,0 +1,1174 @@ +/*------------------------------------------------------------------------- + * + * pgpa_walker.c + * Main entrypoints for analyzing a plan to generate an advice string + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_walker.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "pgpa_join.h" +#include "pgpa_planner.h" +#include "pgpa_scan.h" +#include "pgpa_walker.h" + +#include "access/tsmapi.h" +#include "nodes/plannodes.h" +#include "parser/parsetree.h" +#include "utils/lsyscache.h" + +static void pgpa_walk_recursively(pgpa_plan_walker_context *walker, Plan *plan, + bool within_join_problem, + pgpa_join_unroller *join_unroller, + List *active_query_features, + bool beneath_any_gather); +static Bitmapset *pgpa_process_unrolled_join(pgpa_plan_walker_context *walker, + pgpa_unrolled_join *ujoin); + +static pgpa_query_feature *pgpa_add_feature(pgpa_plan_walker_context *walker, + pgpa_qf_type type, + Plan *plan); + +static void pgpa_qf_add_rti(List *active_query_features, Index rti); +static void pgpa_qf_add_rtis(List *active_query_features, Bitmapset *relids); +static void pgpa_qf_add_plan_rtis(List *active_query_features, Plan *plan, + List *rtable); + +static bool pgpa_walker_join_order_matches(pgpa_unrolled_join *ujoin, + Index rtable_length, + pgpa_identifier *rt_identifiers, + pgpa_advice_target *target, + bool toplevel); +static bool pgpa_walker_join_order_matches_member(pgpa_join_member *member, + Index rtable_length, + pgpa_identifier *rt_identifiers, + pgpa_advice_target *target); +static pgpa_scan *pgpa_walker_find_scan(pgpa_plan_walker_context *walker, + pgpa_scan_strategy strategy, + Bitmapset *relids); +static bool pgpa_walker_index_target_matches_plan(pgpa_index_target *itarget, + Plan *plan); +static bool pgpa_walker_contains_feature(pgpa_plan_walker_context *walker, + pgpa_qf_type type, + Bitmapset *relids); +static bool pgpa_walker_contains_join(pgpa_plan_walker_context *walker, + pgpa_join_strategy strategy, + Bitmapset *relids); +static bool pgpa_walker_contains_no_gather(pgpa_plan_walker_context *walker, + Bitmapset *relids); +static void pgpa_classify_alternative_subplans(pgpa_plan_walker_context *walker, + List *proots, + List **chosen_proots, + List **discarded_proots); + +/* + * Top-level entrypoint for the plan tree walk. + * + * Populates walker based on a traversal of the Plan trees in pstmt. + * + * proots is the list of pgpa_planner_info objects that were generated + * during planning. + */ +void +pgpa_plan_walker(pgpa_plan_walker_context *walker, PlannedStmt *pstmt, + List *proots) +{ + ListCell *lc; + List *sj_unique_rtis = NULL; + List *sj_nonunique_qfs = NULL; + List *chosen_proots; + List *discarded_proots; + + /* Initialization. */ + memset(walker, 0, sizeof(pgpa_plan_walker_context)); + walker->pstmt = pstmt; + + /* Walk the main plan tree. */ + pgpa_walk_recursively(walker, pstmt->planTree, false, NULL, NIL, false); + + /* Main plan tree walk won't reach subplans, so walk those. */ + foreach(lc, pstmt->subplans) + { + Plan *plan = lfirst(lc); + + if (plan != NULL) + pgpa_walk_recursively(walker, plan, false, NULL, NIL, false); + } + + /* Adjust RTIs from sj_unique_rels for the flattened range table. */ + foreach_ptr(pgpa_planner_info, proot, proots) + { + /* If there are no sj_unique_rels for this proot, we can skip it. */ + if (proot->sj_unique_rels == NIL) + continue; + + /* If this is a subplan, find the range table offset. */ + if (!proot->has_rtoffset) + elog(ERROR, "no rtoffset for plan %s", proot->plan_name); + + /* Offset each relid set by the proot's rtoffset. */ + foreach_node(Bitmapset, relids, proot->sj_unique_rels) + { + int rtindex = -1; + Bitmapset *flat_relids = NULL; + + while ((rtindex = bms_next_member(relids, rtindex)) >= 0) + flat_relids = bms_add_member(flat_relids, + rtindex + proot->rtoffset); + + sj_unique_rtis = lappend(sj_unique_rtis, flat_relids); + } + } + + /* + * Remove any non-unique semijoin query features for which making the rel + * unique wasn't considered. + */ + foreach_ptr(pgpa_query_feature, qf, + walker->query_features[PGPAQF_SEMIJOIN_NON_UNIQUE]) + { + if (list_member(sj_unique_rtis, qf->relids)) + sj_nonunique_qfs = lappend(sj_nonunique_qfs, qf); + } + walker->query_features[PGPAQF_SEMIJOIN_NON_UNIQUE] = sj_nonunique_qfs; + + /* + * If we find any cases where analysis of the Plan tree shows that the + * semijoin was made unique but this possibility was never observed to be + * considered during planning, then we have a bug somewhere. + */ + foreach_ptr(pgpa_query_feature, qf, + walker->query_features[PGPAQF_SEMIJOIN_UNIQUE]) + { + if (!list_member(sj_unique_rtis, qf->relids)) + { + StringInfoData buf; + + initStringInfo(&buf); + outBitmapset(&buf, qf->relids); + elog(ERROR, + "unique semijoin found for relids %s but not observed during planning", + buf.data); + } + } + + /* + * It's possible for a Gather or Gather Merge query feature to find no + * RTIs when partitionwise aggregation is in use. We shouldn't emit + * something like GATHER_MERGE(()), so instead emit nothing. This means + * that we won't advise either GATHER or GATHER_MERGE or NO_GATHER in such + * cases, which might be something we want to improve in the future. + * + * (Should the Partial Aggregates in such a case be created in an + * UPPERREL_GROUP_AGG with a non-empty relid set? Right now that doesn't + * happen, but it seems like it would make life easier for us if it did.) + */ + for (int t = 0; t < NUM_PGPA_QF_TYPES; ++t) + { + List *query_features = NIL; + + foreach_ptr(pgpa_query_feature, qf, walker->query_features[t]) + { + if (qf->relids != NULL) + query_features = lappend(query_features, qf); + else + Assert(t == PGPAQF_GATHER || t == PGPAQF_GATHER_MERGE); + } + + walker->query_features[t] = query_features; + } + + /* Classify alternative subplans. */ + pgpa_classify_alternative_subplans(walker, proots, + &chosen_proots, &discarded_proots); + + /* + * Figure out which of the discarded alternatives have a non-discarded + * alternative. Those are the ones for which we want to emit DO_NOT_SCAN + * advice. (If every alternative was discarded, then there's no point.) + */ + foreach_ptr(pgpa_planner_info, discarded_proot, discarded_proots) + { + bool some_alternative_chosen = false; + + foreach_ptr(pgpa_planner_info, chosen_proot, chosen_proots) + { + if (strings_equal_or_both_null(discarded_proot->alternative_plan_name, + chosen_proot->alternative_plan_name)) + { + some_alternative_chosen = true; + break; + } + } + + if (some_alternative_chosen) + { + for (int rti = 1; rti <= discarded_proot->rid_array_size; rti++) + { + pgpa_identifier *rid = &discarded_proot->rid_array[rti - 1]; + + if (rid->alias_name != NULL) + walker->do_not_scan_identifiers = + lappend(walker->do_not_scan_identifiers, rid); + } + } + } +} + +/* + * Main workhorse for the plan tree walk. + * + * If within_join_problem is true, we encountered a join at some higher level + * of the tree walk and haven't yet descended out of the portion of the plan + * tree that is part of that same join problem. We're no longer in the same + * join problem if (1) we cross into a different subquery or (2) we descend + * through an Append or MergeAppend node, below which any further joins would + * be partitionwise joins planned separately from the outer join problem. + * + * If join_unroller != NULL, the join unroller code expects us to find a join + * that should be unrolled into that object. This implies that we're within a + * join problem, but the reverse is not true: when we've traversed all the + * joins but are still looking for the scan that is the leaf of the join tree, + * join_unroller will be NULL but within_join_problem will be true. + * + * Each element of active_query_features corresponds to some item of advice + * that needs to enumerate all the relations it affects. We add RTIs we find + * during tree traversal to each of these query features. + * + * If beneath_any_gather == true, some higher level of the tree traversal found + * a Gather or Gather Merge node. + */ +static void +pgpa_walk_recursively(pgpa_plan_walker_context *walker, Plan *plan, + bool within_join_problem, + pgpa_join_unroller *join_unroller, + List *active_query_features, + bool beneath_any_gather) +{ + pgpa_join_unroller *outer_join_unroller = NULL; + pgpa_join_unroller *inner_join_unroller = NULL; + bool join_unroller_toplevel = false; + ListCell *lc; + List *extraplans = NIL; + List *elided_nodes = NIL; + + Assert(within_join_problem || join_unroller == NULL); + + /* + * Check the future_query_features list to see whether this was previously + * identified as a plan node that needs to be treated as a query feature. + * We must do this before handling elided nodes, because if there's an + * elided node associated with a future query feature, the RTIs associated + * with the elided node should be the only ones attributed to the query + * feature. + */ + foreach_ptr(pgpa_query_feature, qf, walker->future_query_features) + { + if (qf->plan == plan) + { + active_query_features = list_copy(active_query_features); + active_query_features = lappend(active_query_features, qf); + walker->future_query_features = + list_delete_ptr(walker->future_query_features, qf); + break; + } + } + + /* + * Find all elided nodes for this Plan node. + */ + foreach_node(ElidedNode, n, walker->pstmt->elidedNodes) + { + if (n->plan_node_id == plan->plan_node_id) + elided_nodes = lappend(elided_nodes, n); + } + + /* If we found any elided_nodes, handle them. */ + if (elided_nodes != NIL) + { + int num_elided_nodes = list_length(elided_nodes); + ElidedNode *last_elided_node; + + /* + * RTIs for the final -- and thus logically uppermost -- elided node + * should be collected for query features passed down by the caller. + * However, elided nodes act as barriers to query features, which + * means that (1) the remaining elided nodes, if any, should be + * ignored for purposes of query features and (2) the list of active + * query features should be reset to empty so that we do not add RTIs + * from the plan node that is logically beneath the elided node to the + * query features passed down from the caller. + */ + last_elided_node = list_nth(elided_nodes, num_elided_nodes - 1); + pgpa_qf_add_rtis(active_query_features, + pgpa_filter_out_join_relids(last_elided_node->relids, + walker->pstmt->rtable)); + active_query_features = NIL; + + /* + * If we're within a join problem, the join_unroller is responsible + * for building the scan for the final elided node, so throw it out. + */ + if (within_join_problem) + elided_nodes = list_truncate(elided_nodes, num_elided_nodes - 1); + + /* Build scans for all (or the remaining) elided nodes. */ + foreach_node(ElidedNode, elided_node, elided_nodes) + { + (void) pgpa_build_scan(walker, plan, elided_node, + beneath_any_gather, within_join_problem); + } + + /* + * If there were any elided nodes, then everything beneath those nodes + * is not part of the same join problem. + * + * In more detail, if an Append or MergeAppend was elided, then a + * partitionwise join was chosen and only a single child survived; if + * a SubqueryScan was elided, the subquery was planned without + * flattening it into the parent. + */ + within_join_problem = false; + join_unroller = NULL; + } + + /* + * If this is a Gather or Gather Merge node, directly add it to the list + * of currently-active query features. We must do this after handling + * elided nodes, since the Gather or Gather Merge node occurs logically + * beneath any associated elided nodes. + * + * Exception: We disregard any single_copy Gather nodes. These are created + * by debug_parallel_query, and having them affect the plan advice is + * counterproductive, as the result will be to advise the use of a real + * Gather node, rather than a single copy one. + */ + if (IsA(plan, Gather) && !((Gather *) plan)->single_copy) + { + active_query_features = + lappend(list_copy(active_query_features), + pgpa_add_feature(walker, PGPAQF_GATHER, plan)); + beneath_any_gather = true; + } + else if (IsA(plan, GatherMerge)) + { + active_query_features = + lappend(list_copy(active_query_features), + pgpa_add_feature(walker, PGPAQF_GATHER_MERGE, plan)); + beneath_any_gather = true; + } + + /* + * If we're within a join problem, the join unroller is responsible for + * building any required scan for this node. If not, we do it here. + */ + if (!within_join_problem) + (void) pgpa_build_scan(walker, plan, NULL, beneath_any_gather, false); + + /* + * If this join needs to be unrolled but there's no join unroller already + * available, create one. + */ + if (join_unroller == NULL && pgpa_is_join(plan)) + { + join_unroller = pgpa_create_join_unroller(); + join_unroller_toplevel = true; + within_join_problem = true; + } + + /* + * If this join is to be unrolled, pgpa_unroll_join() will return the join + * unroller object that should be passed down when we recurse into the + * outer and inner sides of the plan. + */ + if (join_unroller != NULL) + pgpa_unroll_join(walker, plan, beneath_any_gather, join_unroller, + &outer_join_unroller, &inner_join_unroller); + + /* Add RTIs from the plan node to all active query features. */ + pgpa_qf_add_plan_rtis(active_query_features, plan, walker->pstmt->rtable); + + /* + * Recurse into the outer and inner subtrees. + * + * As an exception, if this is a ForeignScan, don't recurse. postgres_fdw + * sometimes stores an EPQ recheck plan in plan->lefttree, but that's + * going to mention the same set of relations as the ForeignScan itself, + * and we have no way to emit advice targeting the EPQ case vs. the + * non-EPQ case. Moreover, it's not entirely clear what other FDWs might + * do with the left and right subtrees. Maybe some better handling is + * needed here, but for now, we just punt. + */ + if (!IsA(plan, ForeignScan)) + { + if (plan->lefttree != NULL) + pgpa_walk_recursively(walker, plan->lefttree, within_join_problem, + outer_join_unroller, active_query_features, + beneath_any_gather); + if (plan->righttree != NULL) + pgpa_walk_recursively(walker, plan->righttree, within_join_problem, + inner_join_unroller, active_query_features, + beneath_any_gather); + } + + /* + * If we created a join unroller up above, then it's also our join to use + * it to build the final pgpa_unrolled_join, and to destroy the object. + */ + if (join_unroller_toplevel) + { + pgpa_unrolled_join *ujoin; + + ujoin = pgpa_build_unrolled_join(walker, join_unroller); + walker->toplevel_unrolled_joins = + lappend(walker->toplevel_unrolled_joins, ujoin); + pgpa_destroy_join_unroller(join_unroller); + (void) pgpa_process_unrolled_join(walker, ujoin); + } + + /* + * Some plan types can have additional children. Nodes like Append that + * can have any number of children store them in a List; a SubqueryScan + * just has a field for a single additional Plan. + */ + switch (nodeTag(plan)) + { + case T_Append: + { + Append *aplan = (Append *) plan; + + extraplans = aplan->appendplans; + } + break; + case T_MergeAppend: + { + MergeAppend *maplan = (MergeAppend *) plan; + + extraplans = maplan->mergeplans; + } + break; + case T_BitmapAnd: + extraplans = ((BitmapAnd *) plan)->bitmapplans; + break; + case T_BitmapOr: + extraplans = ((BitmapOr *) plan)->bitmapplans; + break; + case T_SubqueryScan: + + /* + * We don't pass down active_query_features across here, because + * those are specific to a subquery level. + */ + pgpa_walk_recursively(walker, ((SubqueryScan *) plan)->subplan, + 0, NULL, NIL, beneath_any_gather); + break; + case T_CustomScan: + extraplans = ((CustomScan *) plan)->custom_plans; + break; + default: + break; + } + + /* If we found a list of extra children, iterate over it. */ + foreach(lc, extraplans) + { + Plan *subplan = lfirst(lc); + + pgpa_walk_recursively(walker, subplan, false, NULL, NIL, + beneath_any_gather); + } +} + +/* + * Perform final processing of a newly-constructed pgpa_unrolled_join. This + * only needs to be called for toplevel pgpa_unrolled_join objects, since it + * recurses to sub-joins as needed. + * + * Our goal is to add the set of inner relids to the relevant join_strategies + * list, and to do the same for any sub-joins. To that end, the return value + * is the set of relids found beneath the join, but it is expected that + * the toplevel caller will ignore this. + */ +static Bitmapset * +pgpa_process_unrolled_join(pgpa_plan_walker_context *walker, + pgpa_unrolled_join *ujoin) +{ + Bitmapset *all_relids = bms_copy(ujoin->outer.scan->relids); + + /* If this fails, we didn't unroll properly. */ + Assert(ujoin->outer.unrolled_join == NULL); + + for (int k = 0; k < ujoin->ninner; ++k) + { + pgpa_join_member *member = &ujoin->inner[k]; + Bitmapset *relids; + + if (member->unrolled_join != NULL) + relids = pgpa_process_unrolled_join(walker, + member->unrolled_join); + else + { + Assert(member->scan != NULL); + relids = member->scan->relids; + } + walker->join_strategies[ujoin->strategy[k]] = + lappend(walker->join_strategies[ujoin->strategy[k]], relids); + all_relids = bms_add_members(all_relids, relids); + } + + return all_relids; +} + +/* + * Arrange for the given plan node to be treated as a query feature when the + * tree walk reaches it. + * + * Make sure to only use this for nodes that the tree walk can't have reached + * yet! + */ +void +pgpa_add_future_feature(pgpa_plan_walker_context *walker, + pgpa_qf_type type, Plan *plan) +{ + pgpa_query_feature *qf = pgpa_add_feature(walker, type, plan); + + walker->future_query_features = + lappend(walker->future_query_features, qf); +} + +/* + * Return the last of any elided nodes associated with this plan node ID. + * + * The last elided node is the one that would have been uppermost in the plan + * tree had it not been removed during setrefs processing. + */ +ElidedNode * +pgpa_last_elided_node(PlannedStmt *pstmt, Plan *plan) +{ + ElidedNode *elided_node = NULL; + + foreach_node(ElidedNode, n, pstmt->elidedNodes) + { + if (n->plan_node_id == plan->plan_node_id) + elided_node = n; + } + + return elided_node; +} + +/* + * Certain plan nodes can refer to a set of RTIs. Extract and return the set. + */ +Bitmapset * +pgpa_relids(Plan *plan) +{ + if (IsA(plan, Result)) + return ((Result *) plan)->relids; + else if (IsA(plan, ForeignScan)) + return ((ForeignScan *) plan)->fs_relids; + else if (IsA(plan, Append)) + return ((Append *) plan)->apprelids; + else if (IsA(plan, MergeAppend)) + return ((MergeAppend *) plan)->apprelids; + + return NULL; +} + +/* + * Extract the scanned RTI from a plan node. + * + * Returns 0 if there isn't one. + */ +Index +pgpa_scanrelid(Plan *plan) +{ + switch (nodeTag(plan)) + { + case T_SeqScan: + case T_SampleScan: + case T_BitmapHeapScan: + case T_TidScan: + case T_TidRangeScan: + case T_SubqueryScan: + case T_FunctionScan: + case T_TableFuncScan: + case T_ValuesScan: + case T_CteScan: + case T_NamedTuplestoreScan: + case T_WorkTableScan: + case T_ForeignScan: + case T_CustomScan: + case T_IndexScan: + case T_IndexOnlyScan: + return ((Scan *) plan)->scanrelid; + default: + return 0; + } +} + +/* + * Check whether a plan node is a Material node that should be treated as + * a scan. Currently, this only happens when set_tablesample_rel_pathlist + * inserts a Material node to protect a SampleScan that uses a non-repeatable + * tablesample method. + * + * (Most Material nodes we're likely to encounter are actually part of the + * join strategy: nested loops and merge joins can choose to materialize the + * inner sides of the join. The cases identified here are the rare + * exceptions.) + */ +bool +pgpa_is_scan_level_materialize(Plan *plan) +{ + Plan *child; + SampleScan *sscan; + TsmRoutine *tsm; + + if (!IsA(plan, Material)) + return false; + child = plan->lefttree; + if (child == NULL || !IsA(child, SampleScan)) + return false; + sscan = (SampleScan *) child; + tsm = GetTsmRoutine(sscan->tablesample->tsmhandler); + return !tsm->repeatable_across_scans; +} + +/* + * Construct a new Bitmapset containing non-RTE_JOIN members of 'relids'. + */ +Bitmapset * +pgpa_filter_out_join_relids(Bitmapset *relids, List *rtable) +{ + int rti = -1; + Bitmapset *result = NULL; + + while ((rti = bms_next_member(relids, rti)) >= 0) + { + RangeTblEntry *rte = rt_fetch(rti, rtable); + + if (rte->rtekind != RTE_JOIN) + result = bms_add_member(result, rti); + } + + return result; +} + +/* + * Create a pgpa_query_feature and add it to the list of all query features + * for this plan. + */ +static pgpa_query_feature * +pgpa_add_feature(pgpa_plan_walker_context *walker, + pgpa_qf_type type, Plan *plan) +{ + pgpa_query_feature *qf = palloc0_object(pgpa_query_feature); + + qf->type = type; + qf->plan = plan; + + walker->query_features[qf->type] = + lappend(walker->query_features[qf->type], qf); + + return qf; +} + +/* + * Add a single RTI to each active query feature. + */ +static void +pgpa_qf_add_rti(List *active_query_features, Index rti) +{ + foreach_ptr(pgpa_query_feature, qf, active_query_features) + { + qf->relids = bms_add_member(qf->relids, rti); + } +} + +/* + * Add a set of RTIs to each active query feature. + */ +static void +pgpa_qf_add_rtis(List *active_query_features, Bitmapset *relids) +{ + foreach_ptr(pgpa_query_feature, qf, active_query_features) + { + qf->relids = bms_add_members(qf->relids, relids); + } +} + +/* + * Add RTIs directly contained in a plan node to each active query feature, + * but filter out any join RTIs, since advice doesn't mention those. + */ +static void +pgpa_qf_add_plan_rtis(List *active_query_features, Plan *plan, List *rtable) +{ + Bitmapset *relids; + Index rti; + + if ((relids = pgpa_relids(plan)) != NULL) + { + relids = pgpa_filter_out_join_relids(relids, rtable); + pgpa_qf_add_rtis(active_query_features, relids); + } + else if ((rti = pgpa_scanrelid(plan)) != 0) + pgpa_qf_add_rti(active_query_features, rti); +} + +/* + * If we generated plan advice using the provided walker object and array + * of identifiers, would we generate the specified tag/target combination? + * + * If yes, the plan conforms to the advice; if no, it does not. Note that + * we have no way of knowing whether the planner was forced to emit a plan + * that conformed to the advice or just happened to do so. + */ +bool +pgpa_walker_would_advise(pgpa_plan_walker_context *walker, + pgpa_identifier *rt_identifiers, + pgpa_advice_tag_type tag, + pgpa_advice_target *target) +{ + Index rtable_length = list_length(walker->pstmt->rtable); + Bitmapset *relids = NULL; + + if (tag == PGPA_TAG_JOIN_ORDER) + { + foreach_ptr(pgpa_unrolled_join, ujoin, walker->toplevel_unrolled_joins) + { + if (pgpa_walker_join_order_matches(ujoin, rtable_length, + rt_identifiers, target, true)) + return true; + } + + return false; + } + + /* + * DO_NOT_SCAN advice targets rels that may not be in the flat range table + * (e.g. MinMaxAgg losers), so pgpa_compute_rti_from_identifier won't work + * here. Instead, check directly against the do_not_scan_identifiers list. + */ + if (tag == PGPA_TAG_DO_NOT_SCAN) + { + if (target->ttype != PGPA_TARGET_IDENTIFIER) + return false; + foreach_ptr(pgpa_identifier, rid, walker->do_not_scan_identifiers) + { + if (strcmp(rid->alias_name, target->rid.alias_name) == 0 && + rid->occurrence == target->rid.occurrence && + strings_equal_or_both_null(rid->partnsp, + target->rid.partnsp) && + strings_equal_or_both_null(rid->partrel, + target->rid.partrel) && + strings_equal_or_both_null(rid->plan_name, + target->rid.plan_name)) + return true; + } + return false; + } + + if (target->ttype == PGPA_TARGET_IDENTIFIER) + { + Index rti; + + rti = pgpa_compute_rti_from_identifier(rtable_length, rt_identifiers, + &target->rid); + if (rti == 0) + return false; + relids = bms_make_singleton(rti); + } + else + { + Assert(target->ttype == PGPA_TARGET_ORDERED_LIST); + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + Index rti; + + Assert(child_target->ttype == PGPA_TARGET_IDENTIFIER); + rti = pgpa_compute_rti_from_identifier(rtable_length, + rt_identifiers, + &child_target->rid); + if (rti == 0) + return false; + relids = bms_add_member(relids, rti); + } + } + + switch (tag) + { + case PGPA_TAG_JOIN_ORDER: + /* should have been handled above */ + pg_unreachable(); + break; + case PGPA_TAG_DO_NOT_SCAN: + /* should have been handled above */ + pg_unreachable(); + break; + case PGPA_TAG_BITMAP_HEAP_SCAN: + return pgpa_walker_find_scan(walker, + PGPA_SCAN_BITMAP_HEAP, + relids) != NULL; + case PGPA_TAG_FOREIGN_JOIN: + return pgpa_walker_find_scan(walker, + PGPA_SCAN_FOREIGN, + relids) != NULL; + case PGPA_TAG_INDEX_ONLY_SCAN: + { + pgpa_scan *scan; + + scan = pgpa_walker_find_scan(walker, PGPA_SCAN_INDEX_ONLY, + relids); + if (scan == NULL) + return false; + + return pgpa_walker_index_target_matches_plan(target->itarget, scan->plan); + } + case PGPA_TAG_INDEX_SCAN: + { + pgpa_scan *scan; + + scan = pgpa_walker_find_scan(walker, PGPA_SCAN_INDEX, + relids); + if (scan == NULL) + return false; + + return pgpa_walker_index_target_matches_plan(target->itarget, scan->plan); + } + case PGPA_TAG_PARTITIONWISE: + return pgpa_walker_find_scan(walker, + PGPA_SCAN_PARTITIONWISE, + relids) != NULL; + case PGPA_TAG_SEQ_SCAN: + return pgpa_walker_find_scan(walker, + PGPA_SCAN_SEQ, + relids) != NULL; + case PGPA_TAG_TID_SCAN: + return pgpa_walker_find_scan(walker, + PGPA_SCAN_TID, + relids) != NULL; + case PGPA_TAG_GATHER: + return pgpa_walker_contains_feature(walker, + PGPAQF_GATHER, + relids); + case PGPA_TAG_GATHER_MERGE: + return pgpa_walker_contains_feature(walker, + PGPAQF_GATHER_MERGE, + relids); + case PGPA_TAG_SEMIJOIN_NON_UNIQUE: + return pgpa_walker_contains_feature(walker, + PGPAQF_SEMIJOIN_NON_UNIQUE, + relids); + case PGPA_TAG_SEMIJOIN_UNIQUE: + return pgpa_walker_contains_feature(walker, + PGPAQF_SEMIJOIN_UNIQUE, + relids); + case PGPA_TAG_HASH_JOIN: + return pgpa_walker_contains_join(walker, + JSTRAT_HASH_JOIN, + relids); + case PGPA_TAG_MERGE_JOIN_MATERIALIZE: + return pgpa_walker_contains_join(walker, + JSTRAT_MERGE_JOIN_MATERIALIZE, + relids); + case PGPA_TAG_MERGE_JOIN_PLAIN: + return pgpa_walker_contains_join(walker, + JSTRAT_MERGE_JOIN_PLAIN, + relids); + case PGPA_TAG_NESTED_LOOP_MATERIALIZE: + return pgpa_walker_contains_join(walker, + JSTRAT_NESTED_LOOP_MATERIALIZE, + relids); + case PGPA_TAG_NESTED_LOOP_MEMOIZE: + return pgpa_walker_contains_join(walker, + JSTRAT_NESTED_LOOP_MEMOIZE, + relids); + case PGPA_TAG_NESTED_LOOP_PLAIN: + return pgpa_walker_contains_join(walker, + JSTRAT_NESTED_LOOP_PLAIN, + relids); + case PGPA_TAG_NO_GATHER: + return pgpa_walker_contains_no_gather(walker, relids); + } + + /* should not get here */ + return false; +} + +/* + * Does the index target match the Plan? + * + * Should only be called when we know that itarget mandates an Index Scan or + * Index Only Scan and this corresponds to the type of Plan. Here, our job is + * just to check whether it's the same index. + */ +static bool +pgpa_walker_index_target_matches_plan(pgpa_index_target *itarget, Plan *plan) +{ + Oid indexoid = InvalidOid; + + /* Retrieve the index OID from the plan. */ + if (IsA(plan, IndexScan)) + indexoid = ((IndexScan *) plan)->indexid; + else if (IsA(plan, IndexOnlyScan)) + indexoid = ((IndexOnlyScan *) plan)->indexid; + else + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(plan)); + + /* Check whether schema name matches, if specified in index target. */ + if (itarget->indnamespace != NULL) + { + Oid nspoid = get_rel_namespace(indexoid); + char *relnamespace = get_namespace_name_or_temp(nspoid); + + if (strcmp(itarget->indnamespace, relnamespace) != 0) + return false; + } + + /* Check whether relation name matches. */ + return (strcmp(itarget->indname, get_rel_name(indexoid)) == 0); +} + +/* + * Does an unrolled join match the join order specified by an advice target? + */ +static bool +pgpa_walker_join_order_matches(pgpa_unrolled_join *ujoin, + Index rtable_length, + pgpa_identifier *rt_identifiers, + pgpa_advice_target *target, + bool toplevel) +{ + int nchildren = list_length(target->children); + + Assert(target->ttype == PGPA_TARGET_ORDERED_LIST); + + /* At toplevel, we allow a prefix match. */ + if (toplevel) + { + if (nchildren > ujoin->ninner + 1) + return false; + } + else + { + if (nchildren != ujoin->ninner + 1) + return false; + } + + /* Outermost rel must match. */ + if (!pgpa_walker_join_order_matches_member(&ujoin->outer, + rtable_length, + rt_identifiers, + linitial(target->children))) + return false; + + /* Each inner rel must match. */ + for (int n = 0; n < nchildren - 1; ++n) + { + pgpa_advice_target *child_target = list_nth(target->children, n + 1); + + if (!pgpa_walker_join_order_matches_member(&ujoin->inner[n], + rtable_length, + rt_identifiers, + child_target)) + return false; + } + + return true; +} + +/* + * Does one member of an unrolled join match an advice target? + */ +static bool +pgpa_walker_join_order_matches_member(pgpa_join_member *member, + Index rtable_length, + pgpa_identifier *rt_identifiers, + pgpa_advice_target *target) +{ + Bitmapset *relids = NULL; + + if (member->unrolled_join != NULL) + { + if (target->ttype != PGPA_TARGET_ORDERED_LIST) + return false; + return pgpa_walker_join_order_matches(member->unrolled_join, + rtable_length, + rt_identifiers, + target, + false); + } + + Assert(member->scan != NULL); + switch (target->ttype) + { + case PGPA_TARGET_ORDERED_LIST: + /* Could only match an unrolled join */ + return false; + + case PGPA_TARGET_UNORDERED_LIST: + { + foreach_ptr(pgpa_advice_target, child_target, target->children) + { + Index rti; + + rti = pgpa_compute_rti_from_identifier(rtable_length, + rt_identifiers, + &child_target->rid); + if (rti == 0) + return false; + relids = bms_add_member(relids, rti); + } + break; + } + + case PGPA_TARGET_IDENTIFIER: + { + Index rti; + + rti = pgpa_compute_rti_from_identifier(rtable_length, + rt_identifiers, + &target->rid); + if (rti == 0) + return false; + relids = bms_make_singleton(rti); + break; + } + } + + return bms_equal(member->scan->relids, relids); +} + +/* + * Find the scan where the walker says that the given scan strategy should be + * used for the given relid set, if one exists. + * + * Returns the pgpa_scan object, or NULL if none was found. + */ +static pgpa_scan * +pgpa_walker_find_scan(pgpa_plan_walker_context *walker, + pgpa_scan_strategy strategy, + Bitmapset *relids) +{ + List *scans = walker->scans[strategy]; + + foreach_ptr(pgpa_scan, scan, scans) + { + if (bms_equal(scan->relids, relids)) + return scan; + } + + return NULL; +} + +/* + * Does this walker say that the given query feature applies to the given + * relid set? + */ +static bool +pgpa_walker_contains_feature(pgpa_plan_walker_context *walker, + pgpa_qf_type type, + Bitmapset *relids) +{ + List *query_features = walker->query_features[type]; + + foreach_ptr(pgpa_query_feature, qf, query_features) + { + if (bms_equal(qf->relids, relids)) + return true; + } + + return false; +} + +/* + * Does the walker say that the given join strategy should be used for the + * given relid set? + */ +static bool +pgpa_walker_contains_join(pgpa_plan_walker_context *walker, + pgpa_join_strategy strategy, + Bitmapset *relids) +{ + List *join_strategies = walker->join_strategies[strategy]; + + foreach_ptr(Bitmapset, jsrelids, join_strategies) + { + if (bms_equal(jsrelids, relids)) + return true; + } + + return false; +} + +/* + * Does the walker say that the given relids should be marked as NO_GATHER? + */ +static bool +pgpa_walker_contains_no_gather(pgpa_plan_walker_context *walker, + Bitmapset *relids) +{ + return bms_is_subset(relids, walker->no_gather_scans); +} + +/* + * Classify alternative subplans as chosen or discarded. + */ +static void +pgpa_classify_alternative_subplans(pgpa_plan_walker_context *walker, + List *proots, + List **chosen_proots, + List **discarded_proots) +{ + Bitmapset *all_scan_rtis = NULL; + + /* Initialize both output lists to empty. */ + *chosen_proots = NIL; + *discarded_proots = NIL; + + /* Collect all scan RTIs. */ + for (int s = 0; s < NUM_PGPA_SCAN_STRATEGY; s++) + foreach_ptr(pgpa_scan, scan, walker->scans[s]) + all_scan_rtis = bms_add_members(all_scan_rtis, scan->relids); + + /* Now classify each subplan. */ + foreach_ptr(pgpa_planner_info, proot, proots) + { + bool chosen = false; + + /* + * We're only interested in classifying subplans for which there are + * alternatives. + */ + if (!proot->is_alternative_plan) + continue; + + /* + * A subplan has been chosen if any of its scan RTIs appear in the + * final plan. This cannot be the case if it has no RT offset. + */ + if (proot->has_rtoffset) + { + for (int rti = 1; rti <= proot->rid_array_size; rti++) + { + if (proot->rid_array[rti - 1].alias_name != NULL && + bms_is_member(proot->rtoffset + rti, all_scan_rtis)) + { + chosen = true; + break; + } + } + } + + /* Add it to the correct list. */ + if (chosen) + *chosen_proots = lappend(*chosen_proots, proot); + else + *discarded_proots = lappend(*discarded_proots, proot); + } +} diff --git a/contrib/pg_plan_advice/pgpa_walker.h b/contrib/pg_plan_advice/pgpa_walker.h new file mode 100644 index 0000000000000..ebe850622d3f3 --- /dev/null +++ b/contrib/pg_plan_advice/pgpa_walker.h @@ -0,0 +1,125 @@ +/*------------------------------------------------------------------------- + * + * pgpa_walker.h + * Main entrypoints for analyzing a plan to generate an advice string + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_plan_advice/pgpa_walker.h + * + *------------------------------------------------------------------------- + */ +#ifndef PGPA_WALKER_H +#define PGPA_WALKER_H + +#include "pgpa_ast.h" +#include "pgpa_join.h" +#include "pgpa_scan.h" + +/* + * We use the term "query feature" to refer to plan nodes that are interesting + * in the following way: to generate advice, we'll need to know the set of + * same-subquery, non-join RTIs occurring at or below that plan node, without + * admixture of parent and child RTIs. + * + * For example, Gather nodes, designated by PGPAQF_GATHER, and Gather Merge + * nodes, designated by PGPAQF_GATHER_MERGE, are query features, because we'll + * want to admit some kind of advice that describes the portion of the plan + * tree that appears beneath those nodes. + * + * Each semijoin can be implemented either by directly performing a semijoin, + * or by making one side unique and then performing a normal join. Either way, + * we use a query feature to notice what decision was made, so that we can + * describe it by enumerating the RTIs on that side of the join. + * + * To elaborate on the "no admixture of parent and child RTIs" rule, in all of + * these cases, if the entirety of an inheritance hierarchy appears beneath + * the query feature, we only want to name the parent table. But it's also + * possible to have cases where we must name child tables. This is particularly + * likely to happen when partitionwise join is in use, but could happen for + * Gather or Gather Merge even without that, if one of those appears below + * an Append or MergeAppend node for a single table. + */ +typedef enum pgpa_qf_type +{ + PGPAQF_GATHER, + PGPAQF_GATHER_MERGE, + PGPAQF_SEMIJOIN_NON_UNIQUE, + PGPAQF_SEMIJOIN_UNIQUE + /* update NUM_PGPA_QF_TYPES if you add anything here */ +} pgpa_qf_type; + +#define NUM_PGPA_QF_TYPES ((int) PGPAQF_SEMIJOIN_UNIQUE + 1) + +/* + * For each query feature, we keep track of the feature type and the set of + * relids that we found underneath the relevant plan node. See the comments + * on pgpa_qf_type, above, for additional details. + */ +typedef struct pgpa_query_feature +{ + pgpa_qf_type type; + Plan *plan; + Bitmapset *relids; +} pgpa_query_feature; + +/* + * Context object for plan tree walk. + * + * pstmt is the PlannedStmt we're studying. + * + * scans is an array of lists of pgpa_scan objects. The array is indexed by + * the scan's pgpa_scan_strategy. + * + * no_gather_scans is the set of scan RTIs that do not appear beneath any + * Gather or Gather Merge node. + * + * toplevel_unrolled_joins is a list of all pgpa_unrolled_join objects that + * are not a child of some other pgpa_unrolled_join. + * + * join_strategy is an array of lists of Bitmapset objects. Each Bitmapset + * is the set of relids that appears on the inner side of some join (excluding + * RTIs from partition children and subqueries). The array is indexed by + * pgpa_join_strategy. + * + * query_features is an array lists of pgpa_query_feature objects, indexed + * by pgpa_qf_type. + * + * future_query_features is only used during the plan tree walk and should + * be empty when the tree walk concludes. It is a list of pgpa_query_feature + * objects for Plan nodes that the plan tree walk has not yet encountered; + * when encountered, they will be moved to the list of active query features + * that is propagated via the call stack. + */ +typedef struct pgpa_plan_walker_context +{ + PlannedStmt *pstmt; + List *scans[NUM_PGPA_SCAN_STRATEGY]; + Bitmapset *no_gather_scans; + List *toplevel_unrolled_joins; + List *join_strategies[NUM_PGPA_JOIN_STRATEGY]; + List *query_features[NUM_PGPA_QF_TYPES]; + List *future_query_features; + List *do_not_scan_identifiers; +} pgpa_plan_walker_context; + +extern void pgpa_plan_walker(pgpa_plan_walker_context *walker, + PlannedStmt *pstmt, + List *proots); + +extern void pgpa_add_future_feature(pgpa_plan_walker_context *walker, + pgpa_qf_type type, + Plan *plan); + +extern ElidedNode *pgpa_last_elided_node(PlannedStmt *pstmt, Plan *plan); +extern Bitmapset *pgpa_relids(Plan *plan); +extern Index pgpa_scanrelid(Plan *plan); +extern bool pgpa_is_scan_level_materialize(Plan *plan); +extern Bitmapset *pgpa_filter_out_join_relids(Bitmapset *relids, List *rtable); + +extern bool pgpa_walker_would_advise(pgpa_plan_walker_context *walker, + pgpa_identifier *rt_identifiers, + pgpa_advice_tag_type tag, + pgpa_advice_target *target); + +#endif diff --git a/contrib/pg_plan_advice/sql/alternatives.sql b/contrib/pg_plan_advice/sql/alternatives.sql new file mode 100644 index 0000000000000..16299edd196ac --- /dev/null +++ b/contrib/pg_plan_advice/sql/alternatives.sql @@ -0,0 +1,58 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; + +CREATE TABLE alt_t1 (a int) WITH (autovacuum_enabled = false); +CREATE TABLE alt_t2 (a int) WITH (autovacuum_enabled = false); +CREATE INDEX ON alt_t2(a); +INSERT INTO alt_t1 SELECT generate_series(1, 1000); +INSERT INTO alt_t2 SELECT generate_series(1, 100000); +VACUUM ANALYZE alt_t1; +VACUUM ANALYZE alt_t2; + +-- This query uses an OR to prevent the EXISTS from being converted to a +-- semi-join, forcing the planner through the AlternativeSubPlan path. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM alt_t1 +WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0; + +-- We should be able to force either AlternativeSubPlan by advising against +-- scanning the other relation. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_t2@exists_1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM alt_t1 +WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_t2@exists_2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM alt_t1 +WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0; +COMMIT; + +-- Now let's test a case involving MinMaxAggPath, which we treat similarly +-- to the AlternativeSubPlan case. +CREATE TABLE alt_minmax (a int) WITH (autovacuum_enabled = false); +CREATE INDEX ON alt_minmax(a); +INSERT INTO alt_minmax SELECT generate_series(1, 10000); +VACUUM ANALYZE alt_minmax; + +-- Using an Index Scan inside of an InitPlan should win over a full table +-- scan. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; + +-- Advising against the scan of alt_minmax at the root query level should +-- change nothing, but if we say we don't want either of or both of the +-- minmax-variant scans, the plan should switch to a full table scan. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax@minmax_1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax@minmax_1) DO_NOT_SCAN(alt_minmax@minmax_2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT min(a), max(a) FROM alt_minmax; +COMMIT; + +DROP TABLE alt_t1, alt_t2, alt_minmax; diff --git a/contrib/pg_plan_advice/sql/gather.sql b/contrib/pg_plan_advice/sql/gather.sql new file mode 100644 index 0000000000000..776666bf1965c --- /dev/null +++ b/contrib/pg_plan_advice/sql/gather.sql @@ -0,0 +1,86 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 1; +SET parallel_setup_cost = 0; +SET parallel_tuple_cost = 0; +SET min_parallel_table_scan_size = 0; +SET debug_parallel_query = off; + +CREATE TABLE gt_dim (id serial primary key, dim text) + WITH (autovacuum_enabled = false); +INSERT INTO gt_dim (dim) SELECT random()::text FROM generate_series(1,100) g; +VACUUM ANALYZE gt_dim; + +CREATE TABLE gt_fact ( + id int not null, + dim_id integer not null references gt_dim (id) +) WITH (autovacuum_enabled = false); +INSERT INTO gt_fact + SELECT g, (g%3)+1 FROM generate_series(1,100000) g; +VACUUM ANALYZE gt_fact; + +-- By default, we expect Gather Merge with a parallel hash join. +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; + +-- Force Gather or Gather Merge of both relations together. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +SET LOCAL pg_plan_advice.advice = 'gather((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +COMMIT; + +-- Force a separate Gather or Gather Merge operation for each relation. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +SET LOCAL pg_plan_advice.advice = 'gather(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +SET LOCAL pg_plan_advice.advice = 'gather((d d/d.d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +COMMIT; + +-- Force a Gather or Gather Merge on one relation but no parallelism on other. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge(f) no_gather(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +SET LOCAL pg_plan_advice.advice = 'gather_merge(d) no_gather(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +SET LOCAL pg_plan_advice.advice = 'gather(f) no_gather(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +SET LOCAL pg_plan_advice.advice = 'gather(d) no_gather(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +COMMIT; + +-- Force no Gather or Gather Merge use at all. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'no_gather(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +COMMIT; + +-- Can't force Gather Merge without the ORDER BY clause, but just Gather is OK. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather_merge((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'gather((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id; +COMMIT; + +-- Test conflicting advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'gather((f d)) no_gather(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM gt_fact f JOIN gt_dim d ON f.dim_id = d.id ORDER BY d.id; +COMMIT; diff --git a/contrib/pg_plan_advice/sql/join_order.sql b/contrib/pg_plan_advice/sql/join_order.sql new file mode 100644 index 0000000000000..88d90de9cc683 --- /dev/null +++ b/contrib/pg_plan_advice/sql/join_order.sql @@ -0,0 +1,145 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; + +CREATE TABLE jo_dim1 (id integer primary key, dim1 text, val1 int) + WITH (autovacuum_enabled = false); +INSERT INTO jo_dim1 (id, dim1, val1) + SELECT g, 'some filler text ' || g, (g % 3) + 1 + FROM generate_series(1,100) g; +VACUUM ANALYZE jo_dim1; +CREATE TABLE jo_dim2 (id integer primary key, dim2 text, val2 int) + WITH (autovacuum_enabled = false); +INSERT INTO jo_dim2 (id, dim2, val2) + SELECT g, 'some filler text ' || g, (g % 53) + 1 + FROM generate_series(1,1000) g; +VACUUM ANALYZE jo_dim2; + +CREATE TABLE jo_fact ( + id int primary key, + dim1_id integer not null references jo_dim1 (id), + dim2_id integer not null references jo_dim2 (id) +) WITH (autovacuum_enabled = false); +INSERT INTO jo_fact + SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g; +VACUUM ANALYZE jo_fact; + +-- We expect to join to d2 first and then d1, since the condition on d2 +-- is more selective. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Force a few different join orders. Some of these are very inefficient, +-- but the planner considers them all viable. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f d1 d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SET LOCAL pg_plan_advice.advice = 'join_order(f d2 d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SET LOCAL pg_plan_advice.advice = 'join_order(d1 f d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SET LOCAL pg_plan_advice.advice = 'join_order(f (d1 d2))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SET LOCAL pg_plan_advice.advice = 'join_order(f {d1 d2})'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +COMMIT; + +-- Force a join order by mentioning just a prefix of the join list. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SET LOCAL pg_plan_advice.advice = 'join_order(d2 d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +COMMIT; + +-- jo_fact is not partitioned, but let's try pretending that it is and +-- verifying that the advice does not apply. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f/d1 d1 d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SET LOCAL pg_plan_advice.advice = 'join_order(f/d1 (d1 d2))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +COMMIT; + +-- The unusual formulation of this query is intended to prevent the query +-- planner from reducing the FULL JOIN to some other join type, so that we +-- can test what happens with a join type that cannot be reordered. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; + +-- We should not be able to force the planner to join f to d1 first, because +-- that is not a valid join order, but we should be able to force the planner +-- to make either d2 or f the driving table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f d1 d2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; +SET LOCAL pg_plan_advice.advice = 'join_order(f d2 d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; +SET LOCAL pg_plan_advice.advice = 'join_order(d2 f d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; +COMMIT; + +-- Two incompatible join orders should conflict. In the second case, +-- the conflict is implicit: if d1 is on the inner side of a join of any +-- type, it cannot also be the driving table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'join_order(f) join_order(d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; +SET LOCAL pg_plan_advice.advice = 'join_order(d1) hash_join(d1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM jo_dim1 d1 + INNER JOIN (jo_fact f FULL JOIN jo_dim2 d2 ON f.dim2_id + 0 = d2.id + 0) + ON d1.id = f.dim1_id OR f.dim1_id IS NULL; +COMMIT; diff --git a/contrib/pg_plan_advice/sql/join_strategy.sql b/contrib/pg_plan_advice/sql/join_strategy.sql new file mode 100644 index 0000000000000..edd5c4c0e1452 --- /dev/null +++ b/contrib/pg_plan_advice/sql/join_strategy.sql @@ -0,0 +1,84 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; + +CREATE TABLE join_dim (id serial primary key, dim text) + WITH (autovacuum_enabled = false); +INSERT INTO join_dim (dim) SELECT random()::text FROM generate_series(1,100) g; +VACUUM ANALYZE join_dim; + +CREATE TABLE join_fact ( + id int primary key, + dim_id integer not null references join_dim (id) +) WITH (autovacuum_enabled = false); +INSERT INTO join_fact + SELECT g, (g%3)+1 FROM generate_series(1,100000) g; +CREATE INDEX join_fact_dim_id ON join_fact (dim_id); +VACUUM ANALYZE join_fact; + +-- We expect a hash join by default. +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + +-- Try forcing each join method in turn with join_dim as the inner table. +-- All of these should work except for MERGE_JOIN_MATERIALIZE; that will +-- fail, because the planner knows that join_dim (id) is unique, and will +-- refuse to add mark/restore overhead. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'HASH_JOIN(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_MATERIALIZE(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_PLAIN(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MATERIALIZE(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MEMOIZE(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +COMMIT; + +-- Now try forcing each join method in turn with join_fact as the inner +-- table. All of these should work. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'HASH_JOIN(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_MATERIALIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_PLAIN(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MATERIALIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_MEMOIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +COMMIT; + +-- Non-working cases. We can't force a foreign join between these tables, +-- because they aren't foreign tables. We also can't use two different +-- strategies on the same table, nor can we put both tables on the inner +-- side of the same join. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'FOREIGN_JOIN((f d))'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f) NESTED_LOOP_MATERIALIZE(f)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f d)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; +COMMIT; diff --git a/contrib/pg_plan_advice/sql/partitionwise.sql b/contrib/pg_plan_advice/sql/partitionwise.sql new file mode 100644 index 0000000000000..c51456dbbb56e --- /dev/null +++ b/contrib/pg_plan_advice/sql/partitionwise.sql @@ -0,0 +1,99 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +SET enable_partitionwise_join = true; + +CREATE TABLE pt1 (id integer primary key, dim1 text, val1 int) + PARTITION BY RANGE (id); +CREATE TABLE pt1a PARTITION OF pt1 FOR VALUES FROM (1) to (1001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt1b PARTITION OF pt1 FOR VALUES FROM (1001) to (2001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt1c PARTITION OF pt1 FOR VALUES FROM (2001) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO pt1 (id, dim1, val1) + SELECT g, 'some filler text ' || g, (g % 3) + 1 + FROM generate_series(1,3000) g; +VACUUM ANALYZE pt1; + +CREATE TABLE pt2 (id integer primary key, dim2 text, val2 int) + PARTITION BY RANGE (id); +CREATE TABLE pt2a PARTITION OF pt2 FOR VALUES FROM (1) to (1001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt2b PARTITION OF pt2 FOR VALUES FROM (1001) to (2001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt2c PARTITION OF pt2 FOR VALUES FROM (2001) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO pt2 (id, dim2, val2) + SELECT g, 'some other text ' || g, (g % 5) + 1 + FROM generate_series(1,3000,2) g; +VACUUM ANALYZE pt2; + +CREATE TABLE pt3 (id integer primary key, dim3 text, val3 int) + PARTITION BY RANGE (id); +CREATE TABLE pt3a PARTITION OF pt3 FOR VALUES FROM (1) to (1001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt3b PARTITION OF pt3 FOR VALUES FROM (1001) to (2001) + WITH (autovacuum_enabled = false); +CREATE TABLE pt3c PARTITION OF pt3 FOR VALUES FROM (2001) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO pt3 (id, dim3, val3) + SELECT g, 'a third random text ' || g, (g % 7) + 1 + FROM generate_series(1,3000,3) g; +VACUUM ANALYZE pt3; + +CREATE TABLE ptmismatch (id integer primary key, dimm text, valm int) + PARTITION BY RANGE (id); +CREATE TABLE ptmismatcha PARTITION OF ptmismatch + FOR VALUES FROM (1) to (1501) + WITH (autovacuum_enabled = false); +CREATE TABLE ptmismatchb PARTITION OF ptmismatch + FOR VALUES FROM (1501) to (3001) + WITH (autovacuum_enabled = false); +INSERT INTO ptmismatch (id, dimm, valm) + SELECT g, 'yet another text ' || g, (g % 2) + 1 + FROM generate_series(1,3000) g; +VACUUM ANALYZE ptmismatch; + +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; + +-- Suppress partitionwise join, or do it just partially. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE(pt1 pt2 pt3)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE((pt1 pt2) pt3)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; +COMMIT; + +-- Test conflicting advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE((pt1 pt2) (pt1 pt3))'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; +COMMIT; + +-- Can't force a partitionwise join with a mismatched table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'PARTITIONWISE((pt1 ptmismatch))'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, ptmismatch WHERE pt1.id = ptmismatch.id; +COMMIT; + +-- Force join order for a particular branch of the partitionwise join with +-- and without mentioning the schema name. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'JOIN_ORDER(pt3/public.pt3a pt2/public.pt2a pt1/public.pt1a)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; +SET LOCAL pg_plan_advice.advice = 'JOIN_ORDER(pt3/pt3a pt2/pt2a pt1/pt1a)'; +EXPLAIN (PLAN_ADVICE, COSTS OFF) +SELECT * FROM pt1, pt2, pt3 WHERE pt1.id = pt2.id AND pt2.id = pt3.id + AND val1 = 1 AND val2 = 1 AND val3 = 1; +COMMIT; diff --git a/contrib/pg_plan_advice/sql/prepared.sql b/contrib/pg_plan_advice/sql/prepared.sql new file mode 100644 index 0000000000000..6ff4f03e6c53d --- /dev/null +++ b/contrib/pg_plan_advice/sql/prepared.sql @@ -0,0 +1,36 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; + +CREATE TABLE ptab (id integer, val text) WITH (autovacuum_enabled = false); + +SET pg_plan_advice.always_store_advice_details = false; + +-- Not prepared, so advice should be generated. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM ptab; + +-- Prepared, so advice should not be generated. +PREPARE pt1 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF, PLAN_ADVICE) EXECUTE pt1; + +SET pg_plan_advice.always_store_advice_details = true; + +-- Prepared, but always_store_advice_details = true, so should show advice. +PREPARE pt2 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF, PLAN_ADVICE) EXECUTE pt2; + +-- Not prepared, so feedback should be generated. +SET pg_plan_advice.always_store_advice_details = false; +SET pg_plan_advice.advice = 'SEQ_SCAN(ptab)'; +EXPLAIN (COSTS OFF) +SELECT * FROM ptab; + +-- Prepared, so feedback should not be generated. +PREPARE pt3 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF) EXECUTE pt3; + +SET pg_plan_advice.always_store_advice_details = true; + +-- Prepared, but always_store_advice_details = true, so should show feedback. +PREPARE pt4 AS SELECT * FROM ptab; +EXPLAIN (COSTS OFF, PLAN_ADVICE) EXECUTE pt4; diff --git a/contrib/pg_plan_advice/sql/scan.sql b/contrib/pg_plan_advice/sql/scan.sql new file mode 100644 index 0000000000000..98bee88de913c --- /dev/null +++ b/contrib/pg_plan_advice/sql/scan.sql @@ -0,0 +1,210 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; +SET seq_page_cost = 0.1; +SET random_page_cost = 0.1; +SET cpu_tuple_cost = 0; +SET cpu_index_tuple_cost = 0; + +CREATE TABLE scan_table (a int primary key, b text) + WITH (autovacuum_enabled = false); +INSERT INTO scan_table + SELECT g, 'some text ' || g FROM generate_series(1, 100000) g; +CREATE INDEX scan_table_b ON scan_table USING brin (b); +VACUUM ANALYZE scan_table; + +-- Sequential scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; + +-- Index scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; + +-- Index-only scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; + +-- Bitmap heap scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE b > 'some text 8'; + +-- TID scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)'; + +-- TID range scan +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE ctid > '(1,1)' AND ctid < '(2,1)'; + +-- Try forcing each of our test queries to use the scan type they +-- wanted to use anyway. This should succeed. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE b > 'some text 8'; +SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE ctid > '(1,1)' AND ctid < '(2,1)'; +COMMIT; + +-- Try to force a full scan of the table to use some other scan type. All +-- of these will fail. An index scan or bitmap heap scan could potentially +-- generate the correct answer, but the planner does not even consider these +-- possibilities due to the lack of a WHERE clause. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; +SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; +SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table; +COMMIT; + +-- Try again to force index use. This should now succeed for the INDEX_SCAN +-- and BITMAP_HEAP_SCAN, but the INDEX_ONLY_SCAN can't be forced because the +-- query fetches columns not included in the index. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0; +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0; +SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0; +COMMIT; + +-- We can force a primary key lookup to use a sequential scan, but we +-- can't force it to use an index-only scan (due to the column list) +-- or a TID scan (due to the absence of a TID qual). If we apply DO_NOT_SCAN +-- here, we should get a valid plan anyway, but with the scan disabled. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +COMMIT; + +-- We can forcibly downgrade an index-only scan to an index scan, but we can't +-- force the use of an index that the planner thinks is inapplicable. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table public.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_b)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +COMMIT; + +-- We can force the use of a sequential scan in place of a bitmap heap scan, +-- but a plain index scan on a BRIN index is not possible. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE b > 'some text 8'; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_b)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +COMMIT; + +-- We can force the use of a sequential scan rather than a TID scan or +-- TID range scan. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table + WHERE ctid > '(1,1)' AND ctid < '(2,1)'; +COMMIT; + +-- Test more complex scenarios with index scans. +BEGIN; +-- Should still work if we mention the schema. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table public.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +-- But not if we mention the wrong schema. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table cilbup.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +-- It's OK to repeat the same advice. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey scan_table scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +-- But it doesn't work if the index target is even notionally different. +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey scan_table public.scan_table_pkey)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1; +COMMIT; + +-- Test assorted incorrect advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(nothing)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(nothing whatsoever)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table bogus)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(nothing whatsoever)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; +SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table bogus)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1; +COMMIT; + +-- Test our ability to refer to multiple instances of the same alias. +BEGIN; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s#2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s) SEQ_SCAN(s#2)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x + LEFT JOIN scan_table s ON g = s.a; +COMMIT; + +-- Test our ability to refer to scans within a subquery. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); +BEGIN; +-- Should not match. +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); +-- Should match first query only. +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s@x)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); +-- Should match second query only. +SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s@unnamed_subquery)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0); +COMMIT; + +-- Test a non-repeatable tablesample method with a scan-level Materialize. +CREATE EXTENSION tsm_system_time; +CREATE TABLE scan_tsm (i int); +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT 1 FROM (SELECT i FROM scan_tsm TABLESAMPLE system_time (1000)), + LATERAL (SELECT i LIMIT 1); + +-- Same, but with the scan-level Materialize on the inner side of a join. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT 1 FROM (SELECT 1 AS x LIMIT 1), + LATERAL (SELECT x FROM scan_tsm TABLESAMPLE system_time (1000)); diff --git a/contrib/pg_plan_advice/sql/semijoin.sql b/contrib/pg_plan_advice/sql/semijoin.sql new file mode 100644 index 0000000000000..b4d503f67e3e1 --- /dev/null +++ b/contrib/pg_plan_advice/sql/semijoin.sql @@ -0,0 +1,136 @@ +LOAD 'pg_plan_advice'; +SET max_parallel_workers_per_gather = 0; + +CREATE TABLE sj_wide ( + id integer primary key, + val1 integer, + padding text storage plain +) WITH (autovacuum_enabled = false); +INSERT INTO sj_wide + SELECT g, g%10+1, repeat(' ', 300) FROM generate_series(1, 1000) g; +CREATE INDEX ON sj_wide (val1); +VACUUM ANALYZE sj_wide; + +CREATE TABLE sj_narrow ( + id integer primary key, + val1 integer +) WITH (autovacuum_enabled = false); +INSERT INTO sj_narrow + SELECT g, g%10+1 FROM generate_series(1, 1000) g; +CREATE INDEX ON sj_narrow (val1); +VACUUM ANALYZE sj_narrow; + +-- We expect this to make the VALUES list unique and use index lookups to +-- find the rows in sj_wide, so as to avoid a full scan of sj_wide. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_wide + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + +-- If we ask for a unique semijoin, we should get the same plan as with +-- no advice. If we ask for a non-unique semijoin, we should see a Semi +-- Join operation in the plan tree. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_wide + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_wide + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); +COMMIT; + +-- Because this table is narrower than the previous one, a sequential scan +-- is less expensive, and we choose a straightforward Semi Join plan by +-- default. (Note that this is also very sensitive to the length of the IN +-- list, which affects how many index lookups the alternative plan will need.) +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_narrow + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); + +-- Here, we expect advising a unique semijoin to swith to the same plan that +-- we got with sj_wide, and advising a non-unique semijoin should not change +-- the plan. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_narrow + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique("*VALUES*")'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM sj_narrow + WHERE (id, val1) IN (VALUES (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)); +COMMIT; + +-- In the above example, we made the outer side of the join unique, but here, +-- we should make the inner side unique. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); + +-- We should be able to force a plan with or without the make-unique strategy, +-- with either side as the driving table. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(sj_narrow) join_order(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique(sj_narrow) join_order(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +COMMIT; + +-- However, mentioning the wrong side of the join should result in an advice +-- failure. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(g)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +SET LOCAL pg_plan_advice.advice = 'semijoin_non_unique(g)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +COMMIT; + +-- Test conflicting advice. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(sj_narrow) semijoin_non_unique(sj_narrow)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g + WHERE g in (select val1 from sj_narrow); +COMMIT; + +-- Try applying SEMIJOIN_UNIQUE() to a non-semijoin. +BEGIN; +SET LOCAL pg_plan_advice.advice = 'semijoin_unique(g)'; +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM generate_series(1,1000) g, sj_narrow s WHERE g = s.val1; +COMMIT; + +-- Test the case where the subquery containing a semijoin is removed from +-- the query entirely; this test is just to make sure that advice generation +-- does not fail. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT * FROM + (SELECT * FROM sj_narrow WHERE id IN (SELECT val1 FROM sj_wide) + LIMIT 1) x, + LATERAL (SELECT 1 WHERE false) y; + +-- Test the case where the planner makes one side of a semijoin unique, and +-- that side contains an outer join; this test is just to make sure that +-- advice generation does not fail. +EXPLAIN (COSTS OFF, PLAN_ADVICE) +SELECT 1 FROM generate_series(1, 1000) g WHERE EXISTS + (SELECT 1 FROM + (SELECT 1 FROM (SELECT 1) LEFT JOIN sj_narrow ON true) s, + sj_narrow t2 WHERE g = t2.id); diff --git a/contrib/pg_plan_advice/sql/syntax.sql b/contrib/pg_plan_advice/sql/syntax.sql new file mode 100644 index 0000000000000..f274fa4863657 --- /dev/null +++ b/contrib/pg_plan_advice/sql/syntax.sql @@ -0,0 +1,87 @@ +LOAD 'pg_plan_advice'; + +-- An empty string is allowed. Empty target lists are allowed for most advice +-- tags, but not for JOIN_ORDER. "Supplied Plan Advice" should be omitted in +-- text format when there is no actual advice, but not in non-text format. +SET pg_plan_advice.advice = ''; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_SCAN()'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'NESTED_LOOP_PLAIN()'; +EXPLAIN (COSTS OFF, FORMAT JSON) SELECT 1; +SET pg_plan_advice.advice = 'JOIN_ORDER()'; + +-- Test assorted variations in capitalization, whitespace, and which parts of +-- the relation identifier are included. These should all work. +SET pg_plan_advice.advice = 'SEQ_SCAN(x)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'seq_scan(x@y)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_scan(x#2)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_SCAN (x/y)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = ' SEQ_SCAN ( x / y . z ) '; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_SCAN("x"#2/"y"."z"@"t")'; +EXPLAIN (COSTS OFF) SELECT 1; + +-- Syntax errors. +SET pg_plan_advice.advice = 'SEQUENTIAL_SCAN(x)'; +SET pg_plan_advice.advice = 'SEQ_SCAN'; +SET pg_plan_advice.advice = 'SEQ_SCAN('; +SET pg_plan_advice.advice = 'SEQ_SCAN("'; +SET pg_plan_advice.advice = 'SEQ_SCAN("")'; +SET pg_plan_advice.advice = 'SEQ_SCAN("a"'; +SET pg_plan_advice.advice = 'SEQ_SCAN(#'; +SET pg_plan_advice.advice = '()'; +SET pg_plan_advice.advice = '123'; + +-- Tags like SEQ_SCAN and NO_GATHER don't allow sublists at all; other tags, +-- except for JOIN_ORDER, allow at most one level of sublist. Hence, these +-- examples should error out. +SET pg_plan_advice.advice = 'SEQ_SCAN((x))'; +SET pg_plan_advice.advice = 'GATHER(((x)))'; + +-- Legal comments. +SET pg_plan_advice.advice = '/**/'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'HASH_JOIN(_)/***/'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = '/* comment */ HASH_JOIN(/*x*/y)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = '/* comment */ HASH_JOIN(y//*x*/z)'; +EXPLAIN (COSTS OFF) SELECT 1; + +-- Unterminated comments. +SET pg_plan_advice.advice = '/*'; +SET pg_plan_advice.advice = 'JOIN_ORDER("fOO") /* oops'; + +-- Nested comments are not supported, so the first of these is legal and +-- the second is not. +SET pg_plan_advice.advice = '/*/*/'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = '/*/* stuff */*/'; + +-- Foreign join requires multiple relation identifiers. +SET pg_plan_advice.advice = 'FOREIGN_JOIN(a)'; +SET pg_plan_advice.advice = 'FOREIGN_JOIN((a))'; + +-- Tag keywords used as alias names work fine, because the 'identifier' +-- nonterminal accepts all token types. +SET pg_plan_advice.advice = 'SEQ_SCAN(hash_join)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_SCAN(seq_scan)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_SCAN(gather)'; +EXPLAIN (COSTS OFF) SELECT 1; +SET pg_plan_advice.advice = 'SEQ_SCAN(join_order)'; +EXPLAIN (COSTS OFF) SELECT 1; + +-- Tag keywords used as partition names or plan names should also work, +-- since pgpa_identifier_string() can generate these from real partition +-- and subquery names. +SET pg_plan_advice.advice = 'SEQ_SCAN(t/public.hash_join)'; +SET pg_plan_advice.advice = 'SEQ_SCAN(t/hash_join.foo)'; +SET pg_plan_advice.advice = 'SEQ_SCAN(t@hash_join)'; +SET pg_plan_advice.advice = 'SEQ_SCAN(t@seq_scan)'; diff --git a/contrib/pg_prewarm/Makefile b/contrib/pg_prewarm/Makefile index 9cfde8c4e4fad..617ac8e09b2d8 100644 --- a/contrib/pg_prewarm/Makefile +++ b/contrib/pg_prewarm/Makefile @@ -10,6 +10,8 @@ EXTENSION = pg_prewarm DATA = pg_prewarm--1.1--1.2.sql pg_prewarm--1.1.sql pg_prewarm--1.0--1.1.sql PGFILEDESC = "pg_prewarm - preload relation data into system buffer cache" +REGRESS = pg_prewarm + TAP_TESTS = 1 ifdef USE_PGXS diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c index c52f4d4dc9ea2..ba0bc8e6d4aca 100644 --- a/contrib/pg_prewarm/autoprewarm.c +++ b/contrib/pg_prewarm/autoprewarm.c @@ -16,7 +16,7 @@ * relevant database in turn. The former keeps running after the * initial prewarm is complete to update the dump file periodically. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pg_prewarm/autoprewarm.c @@ -48,6 +48,7 @@ #include "utils/rel.h" #include "utils/relfilenumbermap.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" #define AUTOPREWARM_FILE "autoprewarm.blocks" @@ -370,6 +371,15 @@ apw_load_buffers(void) apw_state->prewarm_start_idx = apw_state->prewarm_stop_idx = 0; apw_state->prewarmed_blocks = 0; + /* Don't prewarm more than we can fit. */ + if (num_elements > NBuffers) + { + num_elements = NBuffers; + ereport(LOG, + (errmsg("autoprewarm capping prewarmed blocks to %d (shared_buffers size)", + NBuffers))); + } + /* Get the info position of the first block of the next database. */ while (apw_state->prewarm_start_idx < num_elements) { @@ -410,10 +420,6 @@ apw_load_buffers(void) apw_state->database = current_db; Assert(apw_state->prewarm_start_idx < apw_state->prewarm_stop_idx); - /* If we've run out of free buffers, don't launch another worker. */ - if (!have_free_buffer()) - break; - /* * Likewise, don't launch if we've already been told to shut down. * (The launch would fail anyway, but we might as well skip it.) @@ -462,12 +468,6 @@ apw_read_stream_next_block(ReadStream *stream, { BlockInfoRecord blk = p->block_info[p->pos]; - if (!have_free_buffer()) - { - p->pos = apw_state->prewarm_stop_idx; - return InvalidBlockNumber; - } - if (blk.tablespace != p->tablespace) return InvalidBlockNumber; @@ -523,10 +523,10 @@ autoprewarm_database_main(Datum main_arg) blk = block_info[i]; /* - * Loop until we run out of blocks to prewarm or until we run out of free + * Loop until we run out of blocks to prewarm or until we run out of * buffers. */ - while (i < apw_state->prewarm_stop_idx && have_free_buffer()) + while (i < apw_state->prewarm_stop_idx) { Oid tablespace = blk.tablespace; RelFileNumber filenumber = blk.filenumber; @@ -568,14 +568,13 @@ autoprewarm_database_main(Datum main_arg) /* * We have a relation; now let's loop until we find a valid fork of - * the relation or we run out of free buffers. Once we've read from - * all valid forks or run out of options, we'll close the relation and + * the relation or we run out of buffers. Once we've read from all + * valid forks or run out of options, we'll close the relation and * move on. */ while (i < apw_state->prewarm_stop_idx && blk.tablespace == tablespace && - blk.filenumber == filenumber && - have_free_buffer()) + blk.filenumber == filenumber) { ForkNumber forknum = blk.forknum; BlockNumber nblocks; @@ -693,12 +692,19 @@ apw_dump_now(bool is_bgworker, bool dump_unlogged) return 0; } - block_info_array = - (BlockInfoRecord *) palloc(sizeof(BlockInfoRecord) * NBuffers); + /* + * With sufficiently large shared_buffers, allocation will exceed 1GB, so + * allow for a huge allocation to prevent outright failure. + * + * (In the future, it might be a good idea to redesign this to use a more + * memory-efficient data structure.) + */ + block_info_array = (BlockInfoRecord *) + palloc_extended((sizeof(BlockInfoRecord) * NBuffers), MCXT_ALLOC_HUGE); for (num_blocks = 0, i = 0; i < NBuffers; i++) { - uint32 buf_state; + uint64 buf_state; CHECK_FOR_INTERRUPTS(); @@ -725,7 +731,7 @@ apw_dump_now(bool is_bgworker, bool dump_unlogged) ++num_blocks; } - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } snprintf(transient_dump_file_path, MAXPGPATH, "%s.tmp", AUTOPREWARM_FILE); @@ -853,11 +859,11 @@ autoprewarm_dump_now(PG_FUNCTION_ARGS) } static void -apw_init_state(void *ptr) +apw_init_state(void *ptr, void *arg) { AutoPrewarmSharedState *state = (AutoPrewarmSharedState *) ptr; - LWLockInitialize(&state->lock, LWLockNewTrancheId()); + LWLockInitialize(&state->lock, LWLockNewTrancheId("autoprewarm")); state->bgworker_pid = InvalidPid; state->pid_using_dumpfile = InvalidPid; } @@ -875,8 +881,7 @@ apw_init_shmem(void) apw_state = GetNamedDSMSegment("autoprewarm", sizeof(AutoPrewarmSharedState), apw_init_state, - &found); - LWLockRegisterTranche(apw_state->lock.tranche, "autoprewarm"); + &found, NULL); return found; } diff --git a/contrib/pg_prewarm/expected/pg_prewarm.out b/contrib/pg_prewarm/expected/pg_prewarm.out new file mode 100644 index 0000000000000..94e4fa1a9d237 --- /dev/null +++ b/contrib/pg_prewarm/expected/pg_prewarm.out @@ -0,0 +1,10 @@ +-- Test pg_prewarm extension +CREATE EXTENSION pg_prewarm; +-- pg_prewarm() should fail if the target relation has no storage. +CREATE TABLE test (c1 int) PARTITION BY RANGE (c1); +SELECT pg_prewarm('test', 'buffer'); +ERROR: relation "test" does not have storage +DETAIL: This operation is not supported for partitioned tables. +-- Cleanup +DROP TABLE test; +DROP EXTENSION pg_prewarm; diff --git a/contrib/pg_prewarm/meson.build b/contrib/pg_prewarm/meson.build index 82b9851303ce3..e70546a451b4f 100644 --- a/contrib/pg_prewarm/meson.build +++ b/contrib/pg_prewarm/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_prewarm_sources = files( 'autoprewarm.c', @@ -29,6 +29,11 @@ tests += { 'name': 'pg_prewarm', 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'pg_prewarm', + ], + }, 'tap': { 'tests': [ 't/001_basic.pl', diff --git a/contrib/pg_prewarm/pg_prewarm.c b/contrib/pg_prewarm/pg_prewarm.c index 50808569bd741..c2716086693d9 100644 --- a/contrib/pg_prewarm/pg_prewarm.c +++ b/contrib/pg_prewarm/pg_prewarm.c @@ -3,7 +3,7 @@ * pg_prewarm.c * prewarming utilities * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pg_prewarm/pg_prewarm.c @@ -16,9 +16,11 @@ #include #include "access/relation.h" +#include "catalog/index.h" #include "fmgr.h" #include "miscadmin.h" #include "storage/bufmgr.h" +#include "storage/lmgr.h" #include "storage/read_stream.h" #include "storage/smgr.h" #include "utils/acl.h" @@ -71,6 +73,8 @@ pg_prewarm(PG_FUNCTION_ARGS) char *ttype; PrewarmType ptype; AclResult aclresult; + char relkind; + Oid privOid; /* Basic sanity checking. */ if (PG_ARGISNULL(0)) @@ -106,12 +110,54 @@ pg_prewarm(PG_FUNCTION_ARGS) forkString = text_to_cstring(forkName); forkNumber = forkname_to_number(forkString); - /* Open relation and check privileges. */ + /* + * Open relation and check privileges. If the relation is an index, we + * must check the privileges on its parent table instead. + */ + relkind = get_rel_relkind(relOid); + if (relkind == RELKIND_INDEX || + relkind == RELKIND_PARTITIONED_INDEX) + { + privOid = IndexGetRelation(relOid, true); + + /* Lock table before index to avoid deadlock. */ + if (OidIsValid(privOid)) + LockRelationOid(privOid, AccessShareLock); + } + else + privOid = relOid; + rel = relation_open(relOid, AccessShareLock); - aclresult = pg_class_aclcheck(relOid, GetUserId(), ACL_SELECT); + + /* + * It's possible that the relation with OID "privOid" was dropped and the + * OID was reused before we locked it. If that happens, we could be left + * with the wrong parent table OID, in which case we must ERROR. It's + * possible that such a race would change the outcome of + * get_rel_relkind(), too, but the worst case scenario there is that we'll + * check privileges on the index instead of its parent table, which isn't + * too terrible. + */ + if (!OidIsValid(privOid) || + (privOid != relOid && + privOid != IndexGetRelation(relOid, true))) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("could not find parent table of index \"%s\"", + RelationGetRelationName(rel)))); + + aclresult = pg_class_aclcheck(privOid, GetUserId(), ACL_SELECT); if (aclresult != ACLCHECK_OK) aclcheck_error(aclresult, get_relkind_objtype(rel->rd_rel->relkind), get_rel_name(relOid)); + /* Check that the relation has storage. */ + if (!RELKIND_HAS_STORAGE(rel->rd_rel->relkind)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("relation \"%s\" does not have storage", + RelationGetRelationName(rel)), + errdetail_relkind_not_supported(rel->rd_rel->relkind))); + /* Check that the fork exists. */ if (!smgrexists(RelationGetSmgr(rel), forkNumber)) ereport(ERROR, @@ -225,8 +271,11 @@ pg_prewarm(PG_FUNCTION_ARGS) read_stream_end(stream); } - /* Close relation, release lock. */ + /* Close relation, release locks. */ relation_close(rel, AccessShareLock); + if (privOid != relOid) + UnlockRelationOid(privOid, AccessShareLock); + PG_RETURN_INT64(blocks_done); } diff --git a/contrib/pg_prewarm/sql/pg_prewarm.sql b/contrib/pg_prewarm/sql/pg_prewarm.sql new file mode 100644 index 0000000000000..c76f2c7916436 --- /dev/null +++ b/contrib/pg_prewarm/sql/pg_prewarm.sql @@ -0,0 +1,10 @@ +-- Test pg_prewarm extension +CREATE EXTENSION pg_prewarm; + +-- pg_prewarm() should fail if the target relation has no storage. +CREATE TABLE test (c1 int) PARTITION BY RANGE (c1); +SELECT pg_prewarm('test', 'buffer'); + +-- Cleanup +DROP TABLE test; +DROP EXTENSION pg_prewarm; diff --git a/contrib/pg_prewarm/t/001_basic.pl b/contrib/pg_prewarm/t/001_basic.pl index 0a8259d367854..a11d1cbfd06f1 100644 --- a/contrib/pg_prewarm/t/001_basic.pl +++ b/contrib/pg_prewarm/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -11,7 +11,7 @@ my $node = PostgreSQL::Test::Cluster->new('main'); -$node->init; +$node->init('auth_extra' => [ '--create-role', 'test_user' ]); $node->append_conf( 'postgresql.conf', qq{shared_preload_libraries = 'pg_prewarm' @@ -23,7 +23,9 @@ $node->safe_psql("postgres", "CREATE EXTENSION pg_prewarm;\n" . "CREATE TABLE test(c1 int);\n" - . "INSERT INTO test SELECT generate_series(1, 100);"); + . "INSERT INTO test SELECT generate_series(1, 100);\n" + . "CREATE INDEX test_idx ON test(c1);\n" + . "CREATE ROLE test_user LOGIN;"); # test read mode my $result = @@ -42,6 +44,31 @@ or $stderr =~ qr/prefetch is not supported by this build/), 'prefetch mode succeeded'); +# test_user should be unable to prewarm table/index without privileges +($cmdret, $stdout, $stderr) = + $node->psql( + "postgres", "SELECT pg_prewarm('test');", + extra_params => [ '--username' => 'test_user' ]); +ok($stderr =~ /permission denied for table test/, 'pg_prewarm failed as expected'); +($cmdret, $stdout, $stderr) = + $node->psql( + "postgres", "SELECT pg_prewarm('test_idx');", + extra_params => [ '--username' => 'test_user' ]); +ok($stderr =~ /permission denied for index test_idx/, 'pg_prewarm failed as expected'); + +# test_user should be able to prewarm table/index with privileges +$node->safe_psql("postgres", "GRANT SELECT ON test TO test_user;"); +$result = + $node->safe_psql( + "postgres", "SELECT pg_prewarm('test');", + extra_params => [ '--username' => 'test_user' ]); +like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected'); +$result = + $node->safe_psql( + "postgres", "SELECT pg_prewarm('test_idx');", + extra_params => [ '--username' => 'test_user' ]); +like($result, qr/^[1-9][0-9]*$/, 'pg_prewarm succeeded as expected'); + # test autoprewarm_dump_now() $result = $node->safe_psql("postgres", "SELECT autoprewarm_dump_now();"); like($result, qr/^[1-9][0-9]*$/, 'autoprewarm_dump_now succeeded'); diff --git a/contrib/pg_stash_advice/.gitignore b/contrib/pg_stash_advice/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/contrib/pg_stash_advice/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/contrib/pg_stash_advice/Makefile b/contrib/pg_stash_advice/Makefile new file mode 100644 index 0000000000000..470c07b9dd7b9 --- /dev/null +++ b/contrib/pg_stash_advice/Makefile @@ -0,0 +1,29 @@ +# contrib/pg_stash_advice/Makefile + +MODULE_big = pg_stash_advice +OBJS = \ + $(WIN32RES) \ + pg_stash_advice.o \ + stashfuncs.o \ + stashpersist.o + +EXTENSION = pg_stash_advice +DATA = pg_stash_advice--1.0.sql +PGFILEDESC = "pg_stash_advice - store and automatically apply plan advice" + +REGRESS = pg_stash_advice pg_stash_advice_utf8 +TAP_TESTS = 1 +EXTRA_INSTALL = contrib/pg_plan_advice + +ifdef USE_PGXS +PG_CPPFLAGS = -I$(includedir_server)/extension +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +PG_CPPFLAGS = -I$(top_srcdir)/contrib/pg_plan_advice +subdir = contrib/pg_stash_advice +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice.out b/contrib/pg_stash_advice/expected/pg_stash_advice.out new file mode 100644 index 0000000000000..788da854aa7c0 --- /dev/null +++ b/contrib/pg_stash_advice/expected/pg_stash_advice.out @@ -0,0 +1,331 @@ +CREATE EXTENSION pg_stash_advice; +SET compute_query_id = on; +SET max_parallel_workers_per_gather = 0; +-- Helper: extract query identifier from EXPLAIN VERBOSE output. +CREATE OR REPLACE FUNCTION get_query_id(query_text text) RETURNS bigint +LANGUAGE plpgsql AS $$ +DECLARE + line text; + qid bigint; +BEGIN + FOR line IN EXECUTE 'EXPLAIN (VERBOSE, FORMAT TEXT) ' || query_text + LOOP + IF line ~ 'Query Identifier:' THEN + qid := regexp_replace(line, '.*Query Identifier:\s*(-?\d+).*', '\1')::bigint; + RETURN qid; + END IF; + END LOOP; + RAISE EXCEPTION 'Query Identifier not found in EXPLAIN output'; +END; +$$; +CREATE TABLE aa_dim1 (id integer primary key, dim1 text, val1 int) + WITH (autovacuum_enabled = false); +INSERT INTO aa_dim1 (id, dim1, val1) + SELECT g, 'some filler text ' || g, (g % 3) + 1 + FROM generate_series(1,100) g; +VACUUM ANALYZE aa_dim1; +CREATE TABLE aa_dim2 (id integer primary key, dim2 text, val2 int) + WITH (autovacuum_enabled = false); +INSERT INTO aa_dim2 (id, dim2, val2) + SELECT g, 'some filler text ' || g, (g % 7) + 1 + FROM generate_series(1,1000) g; +VACUUM ANALYZE aa_dim2; +CREATE TABLE aa_fact ( + id int primary key, + dim1_id integer not null references aa_dim1 (id), + dim2_id integer not null references aa_dim2 (id) +) WITH (autovacuum_enabled = false); +INSERT INTO aa_fact + SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g; +VACUUM ANALYZE aa_fact; +-- Get the query identifier. +SELECT get_query_id($$ +SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +$$) AS qid \gset +-- Create an advice stash and point pg_stash_advice at it. +SELECT pg_create_advice_stash('regress_stash'); + pg_create_advice_stash +------------------------ + +(1 row) + +SET pg_stash_advice.stash_name = 'regress_stash'; +-- Run our test query for the first time with no stashed advice. +EXPLAIN (COSTS OFF) +SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on aa_dim1 d1 + Filter: (val1 = 1) +(11 rows) + +-- Force an index scan on dim1 +SELECT pg_set_stashed_advice('regress_stash', :'qid', + 'INDEX_SCAN(d1 aa_dim1_pkey)'); + pg_set_stashed_advice +----------------------- + +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +--------------------------------------------------------- + Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Index Scan using aa_dim1_pkey on aa_dim1 d1 + Filter: (val1 = 1) + Supplied Plan Advice: + INDEX_SCAN(d1 aa_dim1_pkey) /* matched */ +(13 rows) + +-- Force an alternative join order +SELECT pg_set_stashed_advice('regress_stash', :'qid', + 'join_order(f d1 d2)'); + pg_set_stashed_advice +----------------------- + +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim1 d1 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + Supplied Plan Advice: + JOIN_ORDER(f d1 d2) /* matched */ +(13 rows) + +-- Force an alternative join strategy +SELECT pg_set_stashed_advice('regress_stash', :'qid', + 'NESTED_LOOP_PLAIN(d1)'); + pg_set_stashed_advice +----------------------- + +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +--------------------------------------------------- + Nested Loop + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + -> Index Scan using aa_dim1_pkey on aa_dim1 d1 + Index Cond: (id = f.dim1_id) + Filter: (val1 = 1) + Supplied Plan Advice: + NESTED_LOOP_PLAIN(d1) /* matched */ +(12 rows) + +-- Add a useless extra entry to our test stash. Shouldn't change the result +-- from the previous test. +-- (If we're unlucky enough that this ever fails due to query ID actually +-- being 1, then just put some other constant here. Seems unlikely.) +SELECT pg_set_stashed_advice('regress_stash', 1, 'SEQ_SCAN(d1)'); + pg_set_stashed_advice +----------------------- + +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +--------------------------------------------------- + Nested Loop + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + -> Index Scan using aa_dim1_pkey on aa_dim1 d1 + Index Cond: (id = f.dim1_id) + Filter: (val1 = 1) + Supplied Plan Advice: + NESTED_LOOP_PLAIN(d1) /* matched */ +(12 rows) + +-- Try an empty stash to be sure it does nothing +SELECT pg_create_advice_stash('regress_empty_stash'); + pg_create_advice_stash +------------------------ + +(1 row) + +SET pg_stash_advice.stash_name = 'regress_empty_stash'; +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on aa_dim1 d1 + Filter: (val1 = 1) +(11 rows) + +-- Test that we can list each stash individually and all of them together, +-- but not a nonexistent stash. +SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name; + stash_name | num_entries +---------------------+------------- + regress_empty_stash | 0 + regress_stash | 2 +(2 rows) + +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string; + stash_name | advice_string +---------------+----------------------- + regress_stash | NESTED_LOOP_PLAIN(d1) + regress_stash | SEQ_SCAN(d1) +(2 rows) + +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('regress_empty_stash') + ORDER BY advice_string; + stash_name | advice_string +------------+--------------- +(0 rows) + +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents(NULL) ORDER BY advice_string; + stash_name | advice_string +---------------+----------------------- + regress_stash | NESTED_LOOP_PLAIN(d1) + regress_stash | SEQ_SCAN(d1) +(2 rows) + +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('no_such_stash') + ORDER BY advice_string; +ERROR: advice stash "no_such_stash" does not exist +-- Test that we can remove advice. +SELECT pg_set_stashed_advice('regress_stash', :'qid', null); + pg_set_stashed_advice +----------------------- + +(1 row) + +SET pg_stash_advice.stash_name = 'regress_stash'; +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------ + Hash Join + Hash Cond: (f.dim1_id = d1.id) + -> Hash Join + Hash Cond: (f.dim2_id = d2.id) + -> Seq Scan on aa_fact f + -> Hash + -> Seq Scan on aa_dim2 d2 + Filter: (val2 = 1) + -> Hash + -> Seq Scan on aa_dim1 d1 + Filter: (val1 = 1) +(11 rows) + +SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name; + stash_name | num_entries +---------------------+------------- + regress_empty_stash | 0 + regress_stash | 1 +(2 rows) + +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string; + stash_name | advice_string +---------------+--------------- + regress_stash | SEQ_SCAN(d1) +(1 row) + +-- Can't create a stash that already exists, or drop one that doesn't. +SELECT pg_create_advice_stash('regress_stash'); +ERROR: advice stash "regress_stash" already exists +SELECT pg_drop_advice_stash('no_such_stash'); +ERROR: advice stash "no_such_stash" does not exist +-- Can't add to or remove from a stash that does not exist. +SELECT pg_set_stashed_advice('no_such_stash', 1, 'SEQ_SCAN(t)'); +ERROR: advice stash "no_such_stash" does not exist +SELECT pg_set_stashed_advice('no_such_stash', 1, null); +ERROR: advice stash "no_such_stash" does not exist +-- Can't use query ID 0. +SELECT pg_set_stashed_advice('regress_stash', 0, 'SEQ_SCAN(t)'); +ERROR: cannot set advice string for query ID 0 +-- Stash names must be non-empty, ASCII, and not too long, and must look +-- like identifiers. +SELECT pg_create_advice_stash(''); +ERROR: advice stash name may not be zero length +SELECT pg_create_advice_stash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); +ERROR: advice stash names may not be longer than 63 bytes +SELECT pg_create_advice_stash(' '); +ERROR: advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores +SET pg_stash_advice.stash_name = '99bottles'; +ERROR: invalid value for parameter "pg_stash_advice.stash_name": "99bottles" +DETAIL: advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores +-- Clean up state in dynamic shared memory. +SELECT pg_drop_advice_stash('regress_stash'); + pg_drop_advice_stash +---------------------- + +(1 row) + +SELECT pg_drop_advice_stash('regress_empty_stash'); + pg_drop_advice_stash +---------------------- + +(1 row) + diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice_utf8.out b/contrib/pg_stash_advice/expected/pg_stash_advice_utf8.out new file mode 100644 index 0000000000000..7c532571ed5b1 --- /dev/null +++ b/contrib/pg_stash_advice/expected/pg_stash_advice_utf8.out @@ -0,0 +1,16 @@ +/* + * This test must be run in a database with UTF-8 encoding, + * because other encodings don't support all the characters used. + */ +SELECT getdatabaseencoding() <> 'UTF8' + AS skip_test \gset +\if :skip_test +\quit +\endif +SET client_encoding = utf8; +-- Non-ASCII stash names should be rejected. +SELECT pg_create_advice_stash('café'); +ERROR: advice stash name must not contain non-ASCII characters +SET pg_stash_advice.stash_name = 'café'; +ERROR: invalid value for parameter "pg_stash_advice.stash_name": "café" +DETAIL: advice stash name must not contain non-ASCII characters diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice_utf8_1.out b/contrib/pg_stash_advice/expected/pg_stash_advice_utf8_1.out new file mode 100644 index 0000000000000..37aead89c0c03 --- /dev/null +++ b/contrib/pg_stash_advice/expected/pg_stash_advice_utf8_1.out @@ -0,0 +1,8 @@ +/* + * This test must be run in a database with UTF-8 encoding, + * because other encodings don't support all the characters used. + */ +SELECT getdatabaseencoding() <> 'UTF8' + AS skip_test \gset +\if :skip_test +\quit diff --git a/contrib/pg_stash_advice/meson.build b/contrib/pg_stash_advice/meson.build new file mode 100644 index 0000000000000..96f485b772998 --- /dev/null +++ b/contrib/pg_stash_advice/meson.build @@ -0,0 +1,43 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +pg_stash_advice_sources = files( + 'pg_stash_advice.c', + 'stashfuncs.c', + 'stashpersist.c' +) + +if host_system == 'windows' + pg_stash_advice_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'pg_stash_advice', + '--FILEDESC', 'pg_stash_advice - store and automatically apply plan advice',]) +endif + +pg_stash_advice = shared_module('pg_stash_advice', + pg_stash_advice_sources, + include_directories: [pg_plan_advice_inc, include_directories('.')], + kwargs: contrib_mod_args, +) +contrib_targets += pg_stash_advice + +install_data( + 'pg_stash_advice--1.0.sql', + 'pg_stash_advice.control', + kwargs: contrib_data_args, +) + +tests += { + 'name': 'pg_stash_advice', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'pg_stash_advice', + 'pg_stash_advice_utf8', + ], + }, + 'tap': { + 'tests': [ + 't/001_persist.pl', + ], + }, +} diff --git a/contrib/pg_stash_advice/pg_stash_advice--1.0.sql b/contrib/pg_stash_advice/pg_stash_advice--1.0.sql new file mode 100644 index 0000000000000..50f12dac31364 --- /dev/null +++ b/contrib/pg_stash_advice/pg_stash_advice--1.0.sql @@ -0,0 +1,49 @@ +/* contrib/pg_stash_advice/pg_stash_advice--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION pg_stash_advice" to load this file. \quit + +CREATE FUNCTION pg_create_advice_stash(stash_name text) +RETURNS void +AS 'MODULE_PATHNAME', 'pg_create_advice_stash' +LANGUAGE C STRICT; + +CREATE FUNCTION pg_drop_advice_stash(stash_name text) +RETURNS void +AS 'MODULE_PATHNAME', 'pg_drop_advice_stash' +LANGUAGE C STRICT; + +CREATE FUNCTION pg_set_stashed_advice(stash_name text, query_id bigint, + advice_string text) +RETURNS void +AS 'MODULE_PATHNAME', 'pg_set_stashed_advice' +LANGUAGE C; + +CREATE FUNCTION pg_get_advice_stashes( + OUT stash_name text, + OUT num_entries bigint +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_get_advice_stashes' +LANGUAGE C STRICT; + +CREATE FUNCTION pg_get_advice_stash_contents( + INOUT stash_name text, + OUT query_id bigint, + OUT advice_string text +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_get_advice_stash_contents' +LANGUAGE C; + +CREATE FUNCTION pg_start_stash_advice_worker() +RETURNS void +AS 'MODULE_PATHNAME', 'pg_start_stash_advice_worker' +LANGUAGE C STRICT; + +REVOKE ALL ON FUNCTION pg_create_advice_stash(text) FROM PUBLIC; +REVOKE ALL ON FUNCTION pg_drop_advice_stash(text) FROM PUBLIC; +REVOKE ALL ON FUNCTION pg_get_advice_stash_contents(text) FROM PUBLIC; +REVOKE ALL ON FUNCTION pg_get_advice_stashes() FROM PUBLIC; +REVOKE ALL ON FUNCTION pg_set_stashed_advice(text, bigint, text) FROM PUBLIC; +REVOKE ALL ON FUNCTION pg_start_stash_advice_worker() FROM PUBLIC; diff --git a/contrib/pg_stash_advice/pg_stash_advice.c b/contrib/pg_stash_advice/pg_stash_advice.c new file mode 100644 index 0000000000000..1858c6a135ad6 --- /dev/null +++ b/contrib/pg_stash_advice/pg_stash_advice.c @@ -0,0 +1,773 @@ +/*------------------------------------------------------------------------- + * + * pg_stash_advice.c + * core infrastructure for pg_stash_advice contrib module + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_stash_advice/pg_stash_advice.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/hashfn.h" +#include "common/string.h" +#include "miscadmin.h" +#include "nodes/queryjumble.h" +#include "pg_plan_advice.h" +#include "pg_stash_advice.h" +#include "postmaster/bgworker.h" +#include "storage/dsm_registry.h" +#include "utils/guc.h" +#include "utils/memutils.h" + +PG_MODULE_MAGIC; + +/* Shared memory hash table parameters */ +static dshash_parameters pgsa_stash_dshash_parameters = { + NAMEDATALEN, + sizeof(pgsa_stash), + dshash_strcmp, + dshash_strhash, + dshash_strcpy, + LWTRANCHE_INVALID /* gets set at runtime */ +}; + +static dshash_parameters pgsa_entry_dshash_parameters = { + sizeof(pgsa_entry_key), + sizeof(pgsa_entry), + dshash_memcmp, + dshash_memhash, + dshash_memcpy, + LWTRANCHE_INVALID /* gets set at runtime */ +}; + +/* GUC variables */ +static char *pg_stash_advice_stash_name = ""; +bool pg_stash_advice_persist = true; +int pg_stash_advice_persist_interval = 30; + +/* Shared memory pointers */ +pgsa_shared_state *pgsa_state; +dsa_area *pgsa_dsa_area; +dshash_table *pgsa_stash_dshash; +dshash_table *pgsa_entry_dshash; + +/* Other global variables */ +static MemoryContext pg_stash_advice_mcxt; + +/* Function prototypes */ +static char *pgsa_advisor(PlannerGlobal *glob, + Query *parse, + const char *query_string, + int cursorOptions, + ExplainState *es); +static bool pgsa_check_stash_name_guc(char **newval, void **extra, + GucSource source); +static void pgsa_init_shared_state(void *ptr, void *arg); +static bool pgsa_is_identifier(char *str); + +/* Stash name -> stash ID hash table */ +#define SH_PREFIX pgsa_stash_name_table +#define SH_ELEMENT_TYPE pgsa_stash_name +#define SH_KEY_TYPE uint64 +#define SH_KEY pgsa_stash_id +#define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) &(key), sizeof(uint64)) +#define SH_EQUAL(tb, a, b) (a == b) +#define SH_SCOPE extern +#define SH_DEFINE +#include "lib/simplehash.h" + +/* + * Initialize this module. + */ +void +_PG_init(void) +{ + void (*add_advisor_fn) (pg_plan_advice_advisor_hook hook); + + /* If compute_query_id = 'auto', we would like query IDs. */ + EnableQueryId(); + + /* Define our GUCs. */ + if (process_shared_preload_libraries_in_progress) + DefineCustomBoolVariable("pg_stash_advice.persist", + "Save and restore advice stash contents across restarts.", + NULL, + &pg_stash_advice_persist, + true, + PGC_POSTMASTER, + 0, + NULL, + NULL, + NULL); + else + pg_stash_advice_persist = false; + + DefineCustomIntVariable("pg_stash_advice.persist_interval", + "Interval between advice stash saves, in seconds.", + NULL, + &pg_stash_advice_persist_interval, + 30, + 0, + 3600, + PGC_SIGHUP, + GUC_UNIT_S, + NULL, + NULL, + NULL); + + DefineCustomStringVariable("pg_stash_advice.stash_name", + "Name of the advice stash to be used in this session.", + NULL, + &pg_stash_advice_stash_name, + "", + PGC_USERSET, + 0, + pgsa_check_stash_name_guc, + NULL, + NULL); + + MarkGUCPrefixReserved("pg_stash_advice"); + + /* Start the background worker for persistence, if enabled. */ + if (pg_stash_advice_persist) + pgsa_start_worker(); + + /* Tell pg_plan_advice that we want to provide advice strings. */ + add_advisor_fn = + load_external_function("pg_plan_advice", "pg_plan_advice_add_advisor", + true, NULL); + (*add_advisor_fn) (pgsa_advisor); +} + +/* + * Get the advice string that has been configured for this query, if any, + * and return it. Otherwise, return NULL. + */ +static char * +pgsa_advisor(PlannerGlobal *glob, Query *parse, + const char *query_string, int cursorOptions, + ExplainState *es) +{ + pgsa_entry_key key; + pgsa_entry *entry; + char *advice_string; + uint64 stash_id; + + /* + * Exit quickly if the stash name is empty or there's no query ID. + */ + if (pg_stash_advice_stash_name[0] == '\0' || parse->queryId == 0) + return NULL; + + /* Attach to dynamic shared memory if not already done. */ + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + + /* If stash data is still being restored from disk, ignore. */ + if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready)) + return NULL; + + /* + * Translate pg_stash_advice.stash_name to an integer ID. + * + * pgsa_check_stash_name_guc() has already validated the advice stash + * name, so we don't need to call pgsa_check_stash_name() here. + */ + stash_id = pgsa_lookup_stash_id(pg_stash_advice_stash_name); + if (stash_id == 0) + return NULL; + + /* + * Look up the advice string for the given stash ID + query ID. + * + * If we find an advice string, we copy it into the current memory + * context, presumably short-lived, so that we can release the lock on the + * dshash entry. pg_plan_advice only needs the value to remain allocated + * long enough for it to be parsed, so this should be good enough. + */ + memset(&key, 0, sizeof(pgsa_entry_key)); + key.pgsa_stash_id = stash_id; + key.queryId = parse->queryId; + entry = dshash_find(pgsa_entry_dshash, &key, false); + if (entry == NULL) + return NULL; + if (entry->advice_string == InvalidDsaPointer) + advice_string = NULL; + else + advice_string = pstrdup(dsa_get_address(pgsa_dsa_area, + entry->advice_string)); + dshash_release_lock(pgsa_entry_dshash, entry); + + /* If we found an advice string, emit a debug message. */ + if (advice_string != NULL) + elog(DEBUG2, "supplying automatic advice for stash \"%s\", query ID %" PRId64 ": %s", + pg_stash_advice_stash_name, key.queryId, advice_string); + + return advice_string; +} + +/* + * Attach to various structures in dynamic shared memory. + * + * This function is designed to be resilient against errors. That is, if it + * fails partway through, it should be possible to call it again, repeat no + * work already completed, and potentially succeed or at least get further if + * whatever caused the previous failure has been corrected. + */ +void +pgsa_attach(void) +{ + bool found; + MemoryContext oldcontext; + + /* + * Create a memory context to make sure that any control structures + * allocated in local memory are sufficiently persistent. + */ + if (pg_stash_advice_mcxt == NULL) + pg_stash_advice_mcxt = AllocSetContextCreate(TopMemoryContext, + "pg_stash_advice", + ALLOCSET_DEFAULT_SIZES); + oldcontext = MemoryContextSwitchTo(pg_stash_advice_mcxt); + + /* Attach to the fixed-size state object if not already done. */ + if (pgsa_state == NULL) + pgsa_state = GetNamedDSMSegment("pg_stash_advice", + sizeof(pgsa_shared_state), + pgsa_init_shared_state, + &found, NULL); + + /* Attach to the DSA area if not already done. */ + if (pgsa_dsa_area == NULL) + { + dsa_handle area_handle; + + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + area_handle = pgsa_state->area; + if (area_handle == DSA_HANDLE_INVALID) + { + pgsa_dsa_area = dsa_create(pgsa_state->dsa_tranche); + dsa_pin(pgsa_dsa_area); + pgsa_state->area = dsa_get_handle(pgsa_dsa_area); + LWLockRelease(&pgsa_state->lock); + } + else + { + LWLockRelease(&pgsa_state->lock); + pgsa_dsa_area = dsa_attach(area_handle); + } + dsa_pin_mapping(pgsa_dsa_area); + } + + /* Attach to the stash_name->stash_id hash table if not already done. */ + if (pgsa_stash_dshash == NULL) + { + dshash_table_handle stash_handle; + + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + pgsa_stash_dshash_parameters.tranche_id = pgsa_state->stash_tranche; + stash_handle = pgsa_state->stash_hash; + if (stash_handle == DSHASH_HANDLE_INVALID) + { + pgsa_stash_dshash = dshash_create(pgsa_dsa_area, + &pgsa_stash_dshash_parameters, + NULL); + pgsa_state->stash_hash = + dshash_get_hash_table_handle(pgsa_stash_dshash); + LWLockRelease(&pgsa_state->lock); + } + else + { + LWLockRelease(&pgsa_state->lock); + pgsa_stash_dshash = dshash_attach(pgsa_dsa_area, + &pgsa_stash_dshash_parameters, + stash_handle, NULL); + } + } + + /* Attach to the entry hash table if not already done. */ + if (pgsa_entry_dshash == NULL) + { + dshash_table_handle entry_handle; + + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + pgsa_entry_dshash_parameters.tranche_id = pgsa_state->entry_tranche; + entry_handle = pgsa_state->entry_hash; + if (entry_handle == DSHASH_HANDLE_INVALID) + { + pgsa_entry_dshash = dshash_create(pgsa_dsa_area, + &pgsa_entry_dshash_parameters, + NULL); + pgsa_state->entry_hash = + dshash_get_hash_table_handle(pgsa_entry_dshash); + LWLockRelease(&pgsa_state->lock); + } + else + { + LWLockRelease(&pgsa_state->lock); + pgsa_entry_dshash = dshash_attach(pgsa_dsa_area, + &pgsa_entry_dshash_parameters, + entry_handle, NULL); + } + } + + /* Restore previous memory context. */ + MemoryContextSwitchTo(oldcontext); +} + +/* + * Error out if the stashes have not been loaded from disk yet. + */ +void +pgsa_check_lockout(void) +{ + if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("stash modifications are not allowed because \"%s\" has not been loaded yet", + PGSA_DUMP_FILE))); +} + +/* + * Check whether an advice stash name is legal, and signal an error if not. + * + * Keep this in sync with pgsa_check_stash_name_guc, below. + */ +void +pgsa_check_stash_name(char *stash_name) +{ + /* Reject empty advice stash name. */ + if (stash_name[0] == '\0') + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash name may not be zero length")); + + /* Reject overlong advice stash names. */ + if (strlen(stash_name) + 1 > NAMEDATALEN) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash names may not be longer than %d bytes", + NAMEDATALEN - 1)); + + /* + * Reject non-ASCII advice stash names, since advice stashes are visible + * across all databases and the encodings of those databases might differ. + */ + if (!pg_is_ascii(stash_name)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash name must not contain non-ASCII characters")); + + /* + * Reject things that do not look like identifiers, since the ability to + * create an advice stash with non-printable characters or weird symbols + * in the name is not likely to be useful to anyone. + */ + if (!pgsa_is_identifier(stash_name)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores")); +} + +/* + * As above, but for the GUC check_hook. We allow the empty string here, + * though, as equivalent to disabling the feature. + */ +static bool +pgsa_check_stash_name_guc(char **newval, void **extra, GucSource source) +{ + char *stash_name = *newval; + + /* Reject overlong advice stash names. */ + if (strlen(stash_name) + 1 > NAMEDATALEN) + { + GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE); + GUC_check_errdetail("advice stash names may not be longer than %d bytes", + NAMEDATALEN - 1); + return false; + } + + /* + * Reject non-ASCII advice stash names, since advice stashes are visible + * across all databases and the encodings of those databases might differ. + */ + if (!pg_is_ascii(stash_name)) + { + GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE); + GUC_check_errdetail("advice stash name must not contain non-ASCII characters"); + return false; + } + + /* + * Reject things that do not look like identifiers, since the ability to + * create an advice stash with non-printable characters or weird symbols + * in the name is not likely to be useful to anyone. + */ + if (!pgsa_is_identifier(stash_name)) + { + GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE); + GUC_check_errdetail("advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores"); + return false; + } + + return true; +} + +/* + * Create an advice stash. + */ +void +pgsa_create_stash(char *stash_name) +{ + pgsa_stash *stash; + bool found; + + Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE)); + + /* Create a stash with this name, unless one already exists. */ + stash = dshash_find_or_insert(pgsa_stash_dshash, stash_name, &found); + if (found) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash \"%s\" already exists", stash_name)); + stash->pgsa_stash_id = pgsa_state->next_stash_id++; + dshash_release_lock(pgsa_stash_dshash, stash); + + /* Bump change count. */ + pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1); +} + +/* + * Remove any stored advice string for the given advice stash and query ID. + */ +void +pgsa_clear_advice_string(char *stash_name, int64 queryId) +{ + pgsa_entry *entry; + pgsa_entry_key key; + uint64 stash_id; + dsa_pointer old_dp; + + Assert(LWLockHeldByMe(&pgsa_state->lock)); + + /* Translate the stash name to an integer ID. */ + if ((stash_id = pgsa_lookup_stash_id(stash_name)) == 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash \"%s\" does not exist", stash_name)); + + /* + * Look for an existing entry, and free it. But, be sure to save the + * pointer to the associated advice string, if any. + */ + memset(&key, 0, sizeof(pgsa_entry_key)); + key.pgsa_stash_id = stash_id; + key.queryId = queryId; + entry = dshash_find(pgsa_entry_dshash, &key, true); + if (entry == NULL) + old_dp = InvalidDsaPointer; + else + { + old_dp = entry->advice_string; + dshash_delete_entry(pgsa_entry_dshash, entry); + } + + /* Now we free the advice string as well, if there was one. */ + if (old_dp != InvalidDsaPointer) + dsa_free(pgsa_dsa_area, old_dp); + + /* Bump change count. */ + pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1); +} + +/* + * Drop an advice stash. + */ +void +pgsa_drop_stash(char *stash_name) +{ + pgsa_entry *entry; + pgsa_stash *stash; + dshash_seq_status iterator; + uint64 stash_id; + + Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE)); + + /* Remove the entry for this advice stash. */ + stash = dshash_find(pgsa_stash_dshash, stash_name, true); + if (stash == NULL) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash \"%s\" does not exist", stash_name)); + stash_id = stash->pgsa_stash_id; + dshash_delete_entry(pgsa_stash_dshash, stash); + + /* + * Now remove all the entries. Since pgsa_state->lock must be held at + * least in shared mode to insert entries into pgsa_entry_dshash, it + * doesn't matter whether we do this before or after deleting the entry + * from pgsa_stash_dshash. + */ + dshash_seq_init(&iterator, pgsa_entry_dshash, true); + while ((entry = dshash_seq_next(&iterator)) != NULL) + { + if (stash_id == entry->key.pgsa_stash_id) + { + if (entry->advice_string != InvalidDsaPointer) + dsa_free(pgsa_dsa_area, entry->advice_string); + dshash_delete_current(&iterator); + } + } + dshash_seq_term(&iterator); + + /* Bump change count. */ + pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1); +} + +/* + * Remove all stashes and entries from shared memory. + * + * This is intended to be called before reloading from a dump file, so that + * a failed previous attempt doesn't leave stale data behind. + */ +void +pgsa_reset_all_stashes(void) +{ + dshash_seq_status iter; + pgsa_entry *entry; + + Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE)); + + /* Remove all stashes. */ + dshash_seq_init(&iter, pgsa_stash_dshash, true); + while (dshash_seq_next(&iter) != NULL) + dshash_delete_current(&iter); + dshash_seq_term(&iter); + + /* Remove all entries. */ + dshash_seq_init(&iter, pgsa_entry_dshash, true); + while ((entry = dshash_seq_next(&iter)) != NULL) + { + if (entry->advice_string != InvalidDsaPointer) + dsa_free(pgsa_dsa_area, entry->advice_string); + dshash_delete_current(&iter); + } + dshash_seq_term(&iter); + + /* Reset the stash ID counter. */ + pgsa_state->next_stash_id = UINT64CONST(1); +} + +/* + * Initialize shared state when first created. + */ +static void +pgsa_init_shared_state(void *ptr, void *arg) +{ + pgsa_shared_state *state = (pgsa_shared_state *) ptr; + + LWLockInitialize(&state->lock, + LWLockNewTrancheId("pg_stash_advice_lock")); + state->dsa_tranche = LWLockNewTrancheId("pg_stash_advice_dsa"); + state->stash_tranche = LWLockNewTrancheId("pg_stash_advice_stash"); + state->entry_tranche = LWLockNewTrancheId("pg_stash_advice_entry"); + state->next_stash_id = UINT64CONST(1); + state->area = DSA_HANDLE_INVALID; + state->stash_hash = DSHASH_HANDLE_INVALID; + state->entry_hash = DSHASH_HANDLE_INVALID; + state->bgworker_pid = InvalidPid; + pg_atomic_init_flag(&state->stashes_ready); + pg_atomic_init_u64(&state->change_count, 0); + + /* + * If this module was loaded via shared_preload_libraries, then + * pg_stash_advice_persist is a GUC variable. If it's true, that means + * that we should lock out manual stash modifications until the dump file + * has been successfully loaded. If it's false, there's nothing to load, + * so we set stashes_ready immediately. + * + * If this module was not loaded via shared_preload_libraries, then + * pg_stash_advice_persist is not a GUC variable, but it will be false, + * which leads to the correct behavior. + */ + if (!pg_stash_advice_persist) + pg_atomic_test_set_flag(&state->stashes_ready); +} + +/* + * Check whether a string looks like a valid identifier. It must contain only + * ASCII identifier characters, and must not begin with a digit. + */ +static bool +pgsa_is_identifier(char *str) +{ + if (*str >= '0' && *str <= '9') + return false; + + while (*str != '\0') + { + char c = *str++; + + if ((c < '0' || c > '9') && (c < 'a' || c > 'z') && + (c < 'A' || c > 'Z') && c != '_') + return false; + } + + return true; +} + +/* + * Look up the integer ID that corresponds to the given stash name. + * + * Returns 0 if no such stash exists. + */ +uint64 +pgsa_lookup_stash_id(char *stash_name) +{ + pgsa_stash *stash; + uint64 stash_id; + + /* Search the shared hash table. */ + stash = dshash_find(pgsa_stash_dshash, stash_name, false); + if (stash == NULL) + return 0; + stash_id = stash->pgsa_stash_id; + dshash_release_lock(pgsa_stash_dshash, stash); + + return stash_id; +} + +/* + * Store a new or updated advice string for the given advice stash and query ID. + */ +void +pgsa_set_advice_string(char *stash_name, int64 queryId, char *advice_string) +{ + pgsa_entry *entry; + bool found; + pgsa_entry_key key; + uint64 stash_id; + dsa_pointer new_dp; + dsa_pointer old_dp; + + /* + * The caller must hold our lock, at least in shared mode. This is + * important for two reasons. + * + * First, it holds off interrupts, so that we can't bail out of this code + * after allocating DSA memory for the advice string and before storing + * the resulting pointer somewhere that others can find it. + * + * Second, we need to avoid a race against pgsa_drop_stash(). That + * function removes a stash_name->stash_id mapping and all the entries for + * that stash_id. Without the lock, there's a race condition no matter + * which of those things it does first, because as soon as we've looked up + * the stash ID, that whole function can execute before we do the rest of + * our work, which would result in us adding an entry for a stash that no + * longer exists. + */ + Assert(LWLockHeldByMe(&pgsa_state->lock)); + + /* Look up the stash ID. */ + if ((stash_id = pgsa_lookup_stash_id(stash_name)) == 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash \"%s\" does not exist", stash_name)); + + /* Allocate space for the advice string. */ + new_dp = dsa_allocate(pgsa_dsa_area, strlen(advice_string) + 1); + strcpy(dsa_get_address(pgsa_dsa_area, new_dp), advice_string); + + /* Attempt to insert an entry into the hash table. */ + memset(&key, 0, sizeof(pgsa_entry_key)); + key.pgsa_stash_id = stash_id; + key.queryId = queryId; + entry = dshash_find_or_insert_extended(pgsa_entry_dshash, &key, &found, + DSHASH_INSERT_NO_OOM); + + /* + * If it didn't work, bail out, being careful to free the shared memory + * we've already allocated before, since error cleanup will not do so. + */ + if (entry == NULL) + { + dsa_free(pgsa_dsa_area, new_dp); + ereport(ERROR, + errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("could not insert advice string into shared hash table")); + } + + /* Update the entry and release the lock. */ + old_dp = found ? entry->advice_string : InvalidDsaPointer; + entry->advice_string = new_dp; + dshash_release_lock(pgsa_entry_dshash, entry); + + /* + * We're not safe from leaks yet! + * + * There's now a pointer to new_dp in the entry that we just updated, but + * that means that there's no longer anything pointing to old_dp. + */ + if (DsaPointerIsValid(old_dp)) + dsa_free(pgsa_dsa_area, old_dp); + + /* Bump change count. */ + pg_atomic_add_fetch_u64(&pgsa_state->change_count, 1); +} + +/* + * Start our worker process. + */ +void +pgsa_start_worker(void) +{ + BackgroundWorker worker = {0}; + BackgroundWorkerHandle *handle; + BgwHandleStatus status; + pid_t pid; + + worker.bgw_flags = BGWORKER_SHMEM_ACCESS; + worker.bgw_start_time = BgWorkerStart_ConsistentState; + worker.bgw_restart_time = BGW_DEFAULT_RESTART_INTERVAL; + strcpy(worker.bgw_library_name, "pg_stash_advice"); + strcpy(worker.bgw_function_name, "pg_stash_advice_worker_main"); + strcpy(worker.bgw_name, "pg_stash_advice worker"); + strcpy(worker.bgw_type, "pg_stash_advice worker"); + + /* + * If process_shared_preload_libraries_in_progress = true, we may be in + * the postmaster, in which case this will really register the worker, or + * we may be in a child process in an EXEC_BACKEND build, in which case it + * will silently do nothing (which is the correct behavior). + */ + if (process_shared_preload_libraries_in_progress) + { + RegisterBackgroundWorker(&worker); + return; + } + + /* + * If process_shared_preload_libraries_in_progress = false, we're being + * asked to start the worker after system startup time. In other words, + * unless this is single-user mode, we're not in the postmaster, so we + * should use RegisterDynamicBackgroundWorker and then wait for startup to + * complete. (If we do happen to be in single-user mode, this will error + * out, which is fine.) + */ + worker.bgw_notify_pid = MyProcPid; + if (!RegisterDynamicBackgroundWorker(&worker, &handle)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("could not register background process"), + errhint("You may need to increase \"max_worker_processes\"."))); + status = WaitForBackgroundWorkerStartup(handle, &pid); + if (status != BGWH_STARTED) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("could not start background process"), + errhint("More details may be available in the server log."))); +} diff --git a/contrib/pg_stash_advice/pg_stash_advice.control b/contrib/pg_stash_advice/pg_stash_advice.control new file mode 100644 index 0000000000000..4a0fff5c866d6 --- /dev/null +++ b/contrib/pg_stash_advice/pg_stash_advice.control @@ -0,0 +1,5 @@ +# pg_stash_advice extension +comment = 'store and automatically apply plan advice' +default_version = '1.0' +module_pathname = '$libdir/pg_stash_advice' +relocatable = true diff --git a/contrib/pg_stash_advice/pg_stash_advice.h b/contrib/pg_stash_advice/pg_stash_advice.h new file mode 100644 index 0000000000000..01aded472f347 --- /dev/null +++ b/contrib/pg_stash_advice/pg_stash_advice.h @@ -0,0 +1,111 @@ +/*------------------------------------------------------------------------- + * + * pg_stash_advice.h + * main header for pg_stash_advice contrib module + * + * This module allows plan advice strings (as used and generated by + * pg_plan_advice) to be "stashed" in dynamic shared memory and, from + * there, automatically be applied to queries as they are planned. + * You can create any number of advice stashes, each of which is + * identified by a human-readable, ASCII identifier, and each of them is + * essentially a query ID -> advice_string mapping. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_stash_advice/pg_stash_advice.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_STASH_ADVICE_H +#define PG_STASH_ADVICE_H + +#include "lib/dshash.h" +#include "storage/lwlock.h" + +#define PGSA_DUMP_FILE "pg_stash_advice.tsv" + +/* + * The key that we use to find a particular stash entry. + */ +typedef struct pgsa_entry_key +{ + uint64 pgsa_stash_id; + int64 queryId; +} pgsa_entry_key; + +/* + * A single stash entry. + */ +typedef struct pgsa_entry +{ + pgsa_entry_key key; + dsa_pointer advice_string; +} pgsa_entry; + +/* + * The stash itself is just a mapping from a name to a stash ID. + */ +typedef struct pgsa_stash +{ + char name[NAMEDATALEN]; + uint64 pgsa_stash_id; +} pgsa_stash; + +/* + * Top-level shared state object for pg_stash_advice. + */ +typedef struct pgsa_shared_state +{ + LWLock lock; + int dsa_tranche; + int stash_tranche; + int entry_tranche; + uint64 next_stash_id; + dsa_handle area; + dshash_table_handle stash_hash; + dshash_table_handle entry_hash; + pid_t bgworker_pid; + pg_atomic_flag stashes_ready; + pg_atomic_uint64 change_count; +} pgsa_shared_state; + +/* For stash ID -> stash name hash table */ +typedef struct pgsa_stash_name +{ + uint32 status; + uint64 pgsa_stash_id; + char *name; +} pgsa_stash_name; + +/* Declare stash ID -> stash name hash table */ +#define SH_PREFIX pgsa_stash_name_table +#define SH_ELEMENT_TYPE pgsa_stash_name +#define SH_KEY_TYPE uint64 +#define SH_SCOPE extern +#define SH_DECLARE +#include "lib/simplehash.h" + +/* Shared memory pointers */ +extern pgsa_shared_state *pgsa_state; +extern dsa_area *pgsa_dsa_area; +extern dshash_table *pgsa_stash_dshash; +extern dshash_table *pgsa_entry_dshash; + +/* GUC variables */ +extern bool pg_stash_advice_persist; +extern int pg_stash_advice_persist_interval; + +/* Function prototypes */ +extern void pgsa_attach(void); +extern void pgsa_check_lockout(void); +extern void pgsa_check_stash_name(char *stash_name); +extern void pgsa_clear_advice_string(char *stash_name, int64 queryId); +extern void pgsa_create_stash(char *stash_name); +extern void pgsa_drop_stash(char *stash_name); +extern uint64 pgsa_lookup_stash_id(char *stash_name); +extern void pgsa_reset_all_stashes(void); +extern void pgsa_set_advice_string(char *stash_name, int64 queryId, + char *advice_string); +extern void pgsa_start_worker(void); + +#endif diff --git a/contrib/pg_stash_advice/sql/pg_stash_advice.sql b/contrib/pg_stash_advice/sql/pg_stash_advice.sql new file mode 100644 index 0000000000000..f047a2d1a0976 --- /dev/null +++ b/contrib/pg_stash_advice/sql/pg_stash_advice.sql @@ -0,0 +1,150 @@ +CREATE EXTENSION pg_stash_advice; +SET compute_query_id = on; +SET max_parallel_workers_per_gather = 0; + +-- Helper: extract query identifier from EXPLAIN VERBOSE output. +CREATE OR REPLACE FUNCTION get_query_id(query_text text) RETURNS bigint +LANGUAGE plpgsql AS $$ +DECLARE + line text; + qid bigint; +BEGIN + FOR line IN EXECUTE 'EXPLAIN (VERBOSE, FORMAT TEXT) ' || query_text + LOOP + IF line ~ 'Query Identifier:' THEN + qid := regexp_replace(line, '.*Query Identifier:\s*(-?\d+).*', '\1')::bigint; + RETURN qid; + END IF; + END LOOP; + RAISE EXCEPTION 'Query Identifier not found in EXPLAIN output'; +END; +$$; + +CREATE TABLE aa_dim1 (id integer primary key, dim1 text, val1 int) + WITH (autovacuum_enabled = false); +INSERT INTO aa_dim1 (id, dim1, val1) + SELECT g, 'some filler text ' || g, (g % 3) + 1 + FROM generate_series(1,100) g; +VACUUM ANALYZE aa_dim1; + +CREATE TABLE aa_dim2 (id integer primary key, dim2 text, val2 int) + WITH (autovacuum_enabled = false); +INSERT INTO aa_dim2 (id, dim2, val2) + SELECT g, 'some filler text ' || g, (g % 7) + 1 + FROM generate_series(1,1000) g; +VACUUM ANALYZE aa_dim2; + +CREATE TABLE aa_fact ( + id int primary key, + dim1_id integer not null references aa_dim1 (id), + dim2_id integer not null references aa_dim2 (id) +) WITH (autovacuum_enabled = false); +INSERT INTO aa_fact + SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g; +VACUUM ANALYZE aa_fact; + +-- Get the query identifier. +SELECT get_query_id($$ +SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +$$) AS qid \gset + +-- Create an advice stash and point pg_stash_advice at it. +SELECT pg_create_advice_stash('regress_stash'); +SET pg_stash_advice.stash_name = 'regress_stash'; + +-- Run our test query for the first time with no stashed advice. +EXPLAIN (COSTS OFF) +SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Force an index scan on dim1 +SELECT pg_set_stashed_advice('regress_stash', :'qid', + 'INDEX_SCAN(d1 aa_dim1_pkey)'); +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Force an alternative join order +SELECT pg_set_stashed_advice('regress_stash', :'qid', + 'join_order(f d1 d2)'); +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Force an alternative join strategy +SELECT pg_set_stashed_advice('regress_stash', :'qid', + 'NESTED_LOOP_PLAIN(d1)'); +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Add a useless extra entry to our test stash. Shouldn't change the result +-- from the previous test. +-- (If we're unlucky enough that this ever fails due to query ID actually +-- being 1, then just put some other constant here. Seems unlikely.) +SELECT pg_set_stashed_advice('regress_stash', 1, 'SEQ_SCAN(d1)'); +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Try an empty stash to be sure it does nothing +SELECT pg_create_advice_stash('regress_empty_stash'); +SET pg_stash_advice.stash_name = 'regress_empty_stash'; +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + +-- Test that we can list each stash individually and all of them together, +-- but not a nonexistent stash. +SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name; +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string; +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('regress_empty_stash') + ORDER BY advice_string; +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents(NULL) ORDER BY advice_string; +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('no_such_stash') + ORDER BY advice_string; + +-- Test that we can remove advice. +SELECT pg_set_stashed_advice('regress_stash', :'qid', null); +SET pg_stash_advice.stash_name = 'regress_stash'; +EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f + LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; +SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name; +SELECT stash_name, advice_string + FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string; + +-- Can't create a stash that already exists, or drop one that doesn't. +SELECT pg_create_advice_stash('regress_stash'); +SELECT pg_drop_advice_stash('no_such_stash'); + +-- Can't add to or remove from a stash that does not exist. +SELECT pg_set_stashed_advice('no_such_stash', 1, 'SEQ_SCAN(t)'); +SELECT pg_set_stashed_advice('no_such_stash', 1, null); + +-- Can't use query ID 0. +SELECT pg_set_stashed_advice('regress_stash', 0, 'SEQ_SCAN(t)'); + +-- Stash names must be non-empty, ASCII, and not too long, and must look +-- like identifiers. +SELECT pg_create_advice_stash(''); +SELECT pg_create_advice_stash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); +SELECT pg_create_advice_stash(' '); +SET pg_stash_advice.stash_name = '99bottles'; + +-- Clean up state in dynamic shared memory. +SELECT pg_drop_advice_stash('regress_stash'); +SELECT pg_drop_advice_stash('regress_empty_stash'); diff --git a/contrib/pg_stash_advice/sql/pg_stash_advice_utf8.sql b/contrib/pg_stash_advice/sql/pg_stash_advice_utf8.sql new file mode 100644 index 0000000000000..13ba635267f94 --- /dev/null +++ b/contrib/pg_stash_advice/sql/pg_stash_advice_utf8.sql @@ -0,0 +1,16 @@ +/* + * This test must be run in a database with UTF-8 encoding, + * because other encodings don't support all the characters used. + */ + +SELECT getdatabaseencoding() <> 'UTF8' + AS skip_test \gset +\if :skip_test +\quit +\endif + +SET client_encoding = utf8; + +-- Non-ASCII stash names should be rejected. +SELECT pg_create_advice_stash('café'); +SET pg_stash_advice.stash_name = 'café'; diff --git a/contrib/pg_stash_advice/stashfuncs.c b/contrib/pg_stash_advice/stashfuncs.c new file mode 100644 index 0000000000000..d7aa9f2223f49 --- /dev/null +++ b/contrib/pg_stash_advice/stashfuncs.c @@ -0,0 +1,347 @@ +/*------------------------------------------------------------------------- + * + * stashfuncs.c + * SQL interface to pg_stash_advice + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_stash_advice/stashfuncs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "common/hashfn.h" +#include "fmgr.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "pg_stash_advice.h" +#include "utils/builtins.h" +#include "utils/tuplestore.h" + +PG_FUNCTION_INFO_V1(pg_create_advice_stash); +PG_FUNCTION_INFO_V1(pg_drop_advice_stash); +PG_FUNCTION_INFO_V1(pg_get_advice_stash_contents); +PG_FUNCTION_INFO_V1(pg_get_advice_stashes); +PG_FUNCTION_INFO_V1(pg_set_stashed_advice); +PG_FUNCTION_INFO_V1(pg_start_stash_advice_worker); + +typedef struct pgsa_stash_count +{ + uint32 status; + uint64 pgsa_stash_id; + int64 num_entries; +} pgsa_stash_count; + +#define SH_PREFIX pgsa_stash_count_table +#define SH_ELEMENT_TYPE pgsa_stash_count +#define SH_KEY_TYPE uint64 +#define SH_KEY pgsa_stash_id +#define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) &(key), sizeof(uint64)) +#define SH_EQUAL(tb, a, b) (a == b) +#define SH_SCOPE static inline +#define SH_DEFINE +#define SH_DECLARE +#include "lib/simplehash.h" + +/* + * SQL-callable function to create an advice stash + */ +Datum +pg_create_advice_stash(PG_FUNCTION_ARGS) +{ + char *stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + pgsa_check_stash_name(stash_name); + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + pgsa_check_lockout(); + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + pgsa_create_stash(stash_name); + LWLockRelease(&pgsa_state->lock); + PG_RETURN_VOID(); +} + +/* + * SQL-callable function to drop an advice stash + */ +Datum +pg_drop_advice_stash(PG_FUNCTION_ARGS) +{ + char *stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + pgsa_check_stash_name(stash_name); + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + pgsa_check_lockout(); + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + pgsa_drop_stash(stash_name); + LWLockRelease(&pgsa_state->lock); + PG_RETURN_VOID(); +} + +/* + * SQL-callable function to provide a list of advice stashes + */ +Datum +pg_get_advice_stashes(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dshash_seq_status iterator; + pgsa_entry *entry; + pgsa_stash *stash; + pgsa_stash_count_table_hash *chash; + + InitMaterializedSRF(fcinfo, 0); + + /* Attach to dynamic shared memory if not already done. */ + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + + /* If stash data is still being restored from disk, ignore. */ + if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready)) + return (Datum) 0; + + /* Tally up the number of entries per stash. */ + chash = pgsa_stash_count_table_create(CurrentMemoryContext, 64, NULL); + dshash_seq_init(&iterator, pgsa_entry_dshash, true); + while ((entry = dshash_seq_next(&iterator)) != NULL) + { + pgsa_stash_count *c; + bool found; + + c = pgsa_stash_count_table_insert(chash, + entry->key.pgsa_stash_id, + &found); + if (!found) + c->num_entries = 1; + else + c->num_entries++; + } + dshash_seq_term(&iterator); + + /* Emit results. */ + dshash_seq_init(&iterator, pgsa_stash_dshash, true); + while ((stash = dshash_seq_next(&iterator)) != NULL) + { + Datum values[2]; + bool nulls[2]; + pgsa_stash_count *c; + + values[0] = CStringGetTextDatum(stash->name); + nulls[0] = false; + + c = pgsa_stash_count_table_lookup(chash, stash->pgsa_stash_id); + values[1] = Int64GetDatum(c == NULL ? 0 : c->num_entries); + nulls[1] = false; + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, + nulls); + } + dshash_seq_term(&iterator); + + return (Datum) 0; +} + +/* + * SQL-callable function to provide advice stash contents + */ +Datum +pg_get_advice_stash_contents(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dshash_seq_status iterator; + char *stash_name = NULL; + pgsa_stash_name_table_hash *nhash = NULL; + uint64 stash_id = 0; + pgsa_entry *entry; + + InitMaterializedSRF(fcinfo, 0); + + /* Attach to dynamic shared memory if not already done. */ + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + + /* If stash data is still being restored from disk, ignore. */ + if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready)) + return (Datum) 0; + + /* User can pass NULL for all stashes, or the name of a specific stash. */ + if (!PG_ARGISNULL(0)) + { + stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + pgsa_check_stash_name(stash_name); + stash_id = pgsa_lookup_stash_id(stash_name); + + /* If the user specified a stash name, it should exist. */ + if (stash_id == 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("advice stash \"%s\" does not exist", stash_name)); + } + else + { + pgsa_stash *stash; + + /* + * If we're dumping data about all stashes, we need an ID->name lookup + * table. + */ + nhash = pgsa_stash_name_table_create(CurrentMemoryContext, 64, NULL); + dshash_seq_init(&iterator, pgsa_stash_dshash, true); + while ((stash = dshash_seq_next(&iterator)) != NULL) + { + pgsa_stash_name *n; + bool found; + + n = pgsa_stash_name_table_insert(nhash, + stash->pgsa_stash_id, + &found); + Assert(!found); + n->name = pstrdup(stash->name); + } + dshash_seq_term(&iterator); + } + + /* Now iterate over all the entries. */ + dshash_seq_init(&iterator, pgsa_entry_dshash, false); + while ((entry = dshash_seq_next(&iterator)) != NULL) + { + Datum values[3]; + bool nulls[3]; + char *this_stash_name; + char *advice_string; + + /* Skip incomplete entries where the advice string was never set. */ + if (entry->advice_string == InvalidDsaPointer) + continue; + + if (stash_id != 0) + { + /* + * We're only dumping data for one particular stash, so skip + * entries for any other stash and use the stash name specified by + * the user. + */ + if (stash_id != entry->key.pgsa_stash_id) + continue; + this_stash_name = stash_name; + } + else + { + pgsa_stash_name *n; + + /* + * We're dumping data for all stashes, so look up the correct name + * to use in the hash table. If nothing is found, which is + * possible due to race conditions, make up a string to use. + */ + n = pgsa_stash_name_table_lookup(nhash, entry->key.pgsa_stash_id); + if (n != NULL) + this_stash_name = n->name; + else + this_stash_name = psprintf("", + entry->key.pgsa_stash_id); + } + + /* Work out tuple values. */ + values[0] = CStringGetTextDatum(this_stash_name); + nulls[0] = false; + values[1] = Int64GetDatum(entry->key.queryId); + nulls[1] = false; + advice_string = dsa_get_address(pgsa_dsa_area, entry->advice_string); + values[2] = CStringGetTextDatum(advice_string); + nulls[2] = false; + + /* Emit the tuple. */ + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, + nulls); + } + dshash_seq_term(&iterator); + + return (Datum) 0; +} + +/* + * SQL-callable function to update an advice stash entry for a particular + * query ID + * + * If the second argument is NULL, we delete any existing advice stash + * entry; otherwise, we either create an entry or update it with the new + * advice string. + */ +Datum +pg_set_stashed_advice(PG_FUNCTION_ARGS) +{ + char *stash_name; + int64 queryId; + + if (PG_ARGISNULL(0) || PG_ARGISNULL(1)) + PG_RETURN_NULL(); + + /* Get and check advice stash name. */ + stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + pgsa_check_stash_name(stash_name); + + /* + * Get and check query ID. + * + * Query ID 0 means no query ID was computed, so reject that. + */ + queryId = PG_GETARG_INT64(1); + if (queryId == 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot set advice string for query ID 0")); + + /* Attach to dynamic shared memory if not already done. */ + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + + /* Don't allow writes if stash data is still being restored from disk. */ + pgsa_check_lockout(); + + /* Now call the appropriate function to do the real work. */ + if (PG_ARGISNULL(2)) + { + LWLockAcquire(&pgsa_state->lock, LW_SHARED); + pgsa_clear_advice_string(stash_name, queryId); + LWLockRelease(&pgsa_state->lock); + } + else + { + char *advice_string = text_to_cstring(PG_GETARG_TEXT_PP(2)); + + LWLockAcquire(&pgsa_state->lock, LW_SHARED); + pgsa_set_advice_string(stash_name, queryId, advice_string); + LWLockRelease(&pgsa_state->lock); + } + + PG_RETURN_VOID(); +} + +/* + * SQL-callable function to start the persistence background worker. + */ +Datum +pg_start_stash_advice_worker(PG_FUNCTION_ARGS) +{ + pid_t pid; + + if (unlikely(pgsa_entry_dshash == NULL)) + pgsa_attach(); + + LWLockAcquire(&pgsa_state->lock, LW_SHARED); + pid = pgsa_state->bgworker_pid; + LWLockRelease(&pgsa_state->lock); + + if (pid != InvalidPid) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("pg_stash_advice worker is already running under PID %d", + (int) pid))); + + pgsa_start_worker(); + + PG_RETURN_VOID(); +} diff --git a/contrib/pg_stash_advice/stashpersist.c b/contrib/pg_stash_advice/stashpersist.c new file mode 100644 index 0000000000000..00a0a74f04df3 --- /dev/null +++ b/contrib/pg_stash_advice/stashpersist.c @@ -0,0 +1,800 @@ +/*------------------------------------------------------------------------- + * + * stashpersist.c + * Persistence support for pg_stash_advice. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * contrib/pg_stash_advice/stashpersist.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "common/hashfn.h" +#include "miscadmin.h" +#include "pg_stash_advice.h" +#include "postmaster/bgworker.h" +#include "postmaster/interrupt.h" +#include "storage/fd.h" +#include "storage/ipc.h" +#include "storage/latch.h" +#include "storage/proc.h" +#include "storage/procsignal.h" +#include "utils/backend_status.h" +#include "utils/guc.h" +#include "utils/memutils.h" +#include "utils/timestamp.h" + +typedef struct pgsa_writer_context +{ + char pathname[MAXPGPATH]; + FILE *file; + pgsa_stash_name_table_hash *nhash; + StringInfoData buf; + int entries_written; +} pgsa_writer_context; + +/* + * A parsed entry line, with pointers into the slurp buffer. + */ +typedef struct pgsa_saved_entry +{ + char *stash_name; + int64 queryId; + char *advice_string; +} pgsa_saved_entry; + +/* + * simplehash for detecting duplicate stash names during parsing. + * Keyed by stash name (char *), pointing into the slurp buffer. + */ +typedef struct pgsa_saved_stash +{ + uint32 status; + char *name; +} pgsa_saved_stash; + +#define SH_PREFIX pgsa_saved_stash_table +#define SH_ELEMENT_TYPE pgsa_saved_stash +#define SH_KEY_TYPE char * +#define SH_KEY name +#define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) (key), strlen(key)) +#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0) +#define SH_SCOPE static inline +#define SH_DEFINE +#define SH_DECLARE +#include "lib/simplehash.h" + +extern PGDLLEXPORT void pg_stash_advice_worker_main(Datum main_arg); +static void pgsa_append_tsv_escaped_string(StringInfo buf, const char *str); +static void pgsa_detach_shmem(int code, Datum arg); +static char *pgsa_next_tsv_field(char **cursor); +static void pgsa_read_from_disk(void); +static void pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries); +static void pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes); +static void pgsa_unescape_tsv_field(char *str, const char *filename, + unsigned lineno); +static void pgsa_write_entries(pgsa_writer_context *wctx); +pg_noreturn static void pgsa_write_error(pgsa_writer_context *wctx); +static void pgsa_write_stashes(pgsa_writer_context *wctx); +static void pgsa_write_to_disk(void); + +/* + * Background worker entry point for pg_stash_advice persistence. + * + * On startup, if stashes_ready is set, we load previously saved + * stash data from disk. Then we enter a loop, periodically checking whether + * any changes have been made (via the change_count atomic counter) and + * writing them to disk. On shutdown, we perform a final write. + */ +PGDLLEXPORT void +pg_stash_advice_worker_main(Datum main_arg) +{ + uint64 last_change_count; + TimestampTz last_write_time = 0; + + /* Establish signal handlers; once that's done, unblock signals. */ + pqsignal(SIGTERM, SignalHandlerForShutdownRequest); + pqsignal(SIGHUP, SignalHandlerForConfigReload); + pqsignal(SIGUSR1, procsignal_sigusr1_handler); + BackgroundWorkerUnblockSignals(); + + /* Log a debug message */ + ereport(DEBUG1, + errmsg("pg_stash_advice worker started")); + + /* Set up session user so pgstat can report it. */ + InitializeSessionUserIdStandalone(); + + /* Report this worker in pg_stat_activity. */ + pgstat_beinit(); + pgstat_bestart_initial(); + pgstat_bestart_final(); + + /* Attach to shared memory structures. */ + pgsa_attach(); + + /* Set on-detach hook so that our PID will be cleared on exit. */ + before_shmem_exit(pgsa_detach_shmem, 0); + + /* + * Store our PID in shared memory, unless there's already another worker + * running, in which case just exit. + */ + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + if (pgsa_state->bgworker_pid != InvalidPid) + { + LWLockRelease(&pgsa_state->lock); + ereport(LOG, + (errmsg("pg_stash_advice worker is already running under PID %d", + (int) pgsa_state->bgworker_pid))); + return; + } + pgsa_state->bgworker_pid = MyProcPid; + LWLockRelease(&pgsa_state->lock); + + /* + * If pg_stash_advice.persist was set to true during + * process_shared_preload_libraries() and the data has not yet been + * successfully loaded, load it now. + */ + if (pg_atomic_unlocked_test_flag(&pgsa_state->stashes_ready)) + { + pgsa_read_from_disk(); + pg_atomic_test_set_flag(&pgsa_state->stashes_ready); + } + + /* Note the current change count so we can detect future changes. */ + last_change_count = pg_atomic_read_u64(&pgsa_state->change_count); + + /* Periodically write to disk until terminated. */ + while (!ShutdownRequestPending) + { + /* In case of a SIGHUP, just reload the configuration. */ + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + if (pg_stash_advice_persist_interval <= 0) + { + /* Only writing at shutdown, so just wait forever. */ + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, + -1L, + PG_WAIT_EXTENSION); + } + else + { + TimestampTz next_write_time; + long delay_in_ms; + uint64 current_change_count; + + /* Compute when the next write should happen. */ + next_write_time = + TimestampTzPlusMilliseconds(last_write_time, + pg_stash_advice_persist_interval * 1000); + delay_in_ms = + TimestampDifferenceMilliseconds(GetCurrentTimestamp(), + next_write_time); + + /* + * When we reach next_write_time, we always update last_write_time + * (which is really the time at which we last considered writing), + * but we only actually write to disk if something has changed. + */ + if (delay_in_ms <= 0) + { + current_change_count = + pg_atomic_read_u64(&pgsa_state->change_count); + if (current_change_count != last_change_count) + { + pgsa_write_to_disk(); + last_change_count = current_change_count; + } + last_write_time = GetCurrentTimestamp(); + continue; + } + + /* Sleep until the next write time. */ + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + delay_in_ms, + PG_WAIT_EXTENSION); + } + + ResetLatch(MyLatch); + } + + /* Write one last time before exiting. */ + pgsa_write_to_disk(); +} + +/* + * Clear our PID from shared memory on exit. + */ +static void +pgsa_detach_shmem(int code, Datum arg) +{ + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + if (pgsa_state->bgworker_pid == MyProcPid) + pgsa_state->bgworker_pid = InvalidPid; + LWLockRelease(&pgsa_state->lock); +} + +/* + * Load advice stash data from a dump file on disk, if there is one. + */ +static void +pgsa_read_from_disk(void) +{ + struct stat statbuf; + FILE *file; + char *filebuf; + size_t nread; + char *p; + unsigned lineno; + pgsa_saved_stash_table_hash *saved_stashes; + int num_stashes = 0; + pgsa_saved_entry *entries; + int num_entries = 0; + int max_entries = 64; + MemoryContext tmpcxt; + MemoryContext oldcxt; + + Assert(pgsa_entry_dshash != NULL); + + /* + * Clear any existing shared memory state. + * + * Normally, there won't be any, but if this function was called before + * and failed after beginning to apply changes to shared memory, then we + * need to get rid of any entries created at that time before trying + * again. + */ + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + pgsa_reset_all_stashes(); + LWLockRelease(&pgsa_state->lock); + + /* Open the dump file. If it doesn't exist, we're done. */ + file = AllocateFile(PGSA_DUMP_FILE, "r"); + if (!file) + { + if (errno == ENOENT) + return; + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", PGSA_DUMP_FILE))); + } + + /* Use a temporary context for all parse-phase allocations. */ + tmpcxt = AllocSetContextCreate(CurrentMemoryContext, + "pg_stash_advice load", + ALLOCSET_DEFAULT_SIZES); + oldcxt = MemoryContextSwitchTo(tmpcxt); + + /* Figure out how long the file is. */ + if (fstat(fileno(file), &statbuf) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", PGSA_DUMP_FILE))); + + /* + * Slurp the entire file into memory all at once. + * + * We could avoid this by reading the file incrementally and applying + * changes to pgsa_stash_dshash and pgsa_entry_dshash as we go. Given the + * lockout mechanism implemented by stashes_ready, that shouldn't have any + * user-visible behavioral consequences, but it would consume shared + * memory to no benefit. It seems better to buffer everything in private + * memory first, and then only apply the changes once the file has been + * successfully parsed in its entirety. + * + * That also has the advantage of possibly being more future-proof: if we + * decide to remove the stashes_ready mechanism in the future, or say + * allow for multiple save files, fully validating the file before + * applying any changes will become much more important. + * + * Of course, this approach does have one major disadvantage, which is + * that we'll temporarily use about twice as much memory as we're + * ultimately going to need, but that seems like it shouldn't be a problem + * in practice. If there's so much stashed advice that parsing the disk + * file runs us out of memory, something has gone terribly wrong. In that + * situation, there probably also isn't enough free memory for the + * workload that the advice is attempting to manipulate to run + * successfully. + */ + filebuf = palloc_extended(statbuf.st_size + 1, MCXT_ALLOC_HUGE); + nread = fread(filebuf, 1, statbuf.st_size, file); + if (ferror(file)) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", PGSA_DUMP_FILE))); + FreeFile(file); + filebuf[nread] = '\0'; + + /* Initial memory allocations. */ + saved_stashes = pgsa_saved_stash_table_create(tmpcxt, 64, NULL); + entries = palloc(max_entries * sizeof(pgsa_saved_entry)); + + /* + * For memory and CPU efficiency, we parse the file in place. The end of + * each line gets replaced with a NUL byte, and then the end of each field + * within a line gets the same treatment. The advice string is unescaped + * in place, and stash names and query IDs can't contain any special + * characters. All of the resulting pointers point right back into the + * buffer; we only need additional memory to grow the 'entries' array and + * the 'saved_stashes' hash table. + */ + for (p = filebuf, lineno = 1; *p != '\0'; lineno++) + { + char *cursor = p; + char *eol; + char *line_type; + + /* Find end of line and NUL-terminate. */ + eol = strchr(p, '\n'); + if (eol != NULL) + { + *eol = '\0'; + p = eol + 1; + if (eol > cursor && eol[-1] == '\r') + eol[-1] = '\0'; + } + else + p += strlen(p); + + /* Skip empty lines. */ + if (*cursor == '\0') + continue; + + /* First field is the type of line, either "stash" or "entry". */ + line_type = pgsa_next_tsv_field(&cursor); + if (strcmp(line_type, "stash") == 0) + { + char *name; + bool found; + + /* Second field should be the stash name. */ + name = pgsa_next_tsv_field(&cursor); + if (name == NULL || *name == '\0') + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: expected stash name", + PGSA_DUMP_FILE, lineno))); + + /* No further fields are expected. */ + if (*cursor != '\0') + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: expected end of line", + PGSA_DUMP_FILE, lineno))); + + /* Duplicate check. */ + (void) pgsa_saved_stash_table_insert(saved_stashes, name, &found); + if (found) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: duplicate stash name \"%s\"", + PGSA_DUMP_FILE, lineno, name))); + num_stashes++; + } + else if (strcmp(line_type, "entry") == 0) + { + char *stash_name; + char *queryid_str; + char *advice_str; + char *endptr; + int64 queryId; + + /* Second field should be the stash name. */ + stash_name = pgsa_next_tsv_field(&cursor); + if (stash_name == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: expected stash name", + PGSA_DUMP_FILE, lineno))); + + /* Third field should be the query ID. */ + queryid_str = pgsa_next_tsv_field(&cursor); + if (queryid_str == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: expected query ID", + PGSA_DUMP_FILE, lineno))); + + /* Fourth field should be the advice string. */ + advice_str = pgsa_next_tsv_field(&cursor); + if (advice_str == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: expected advice string", + PGSA_DUMP_FILE, lineno))); + + /* No further fields are expected. */ + if (*cursor != '\0') + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: expected end of line", + PGSA_DUMP_FILE, lineno))); + + /* Make sure the stash is one we've actually seen. */ + if (pgsa_saved_stash_table_lookup(saved_stashes, + stash_name) == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: unknown stash \"%s\"", + PGSA_DUMP_FILE, lineno, stash_name))); + + /* Parse the query ID. */ + errno = 0; + queryId = strtoll(queryid_str, &endptr, 10); + if (*endptr != '\0' || errno != 0 || queryid_str == endptr || + queryId == 0) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: invalid query ID \"%s\"", + PGSA_DUMP_FILE, lineno, queryid_str))); + + /* Unescape the advice string. */ + pgsa_unescape_tsv_field(advice_str, PGSA_DUMP_FILE, lineno); + + /* Append to the entry array. */ + if (num_entries >= max_entries) + { + max_entries *= 2; + entries = repalloc(entries, + max_entries * sizeof(pgsa_saved_entry)); + } + entries[num_entries].stash_name = stash_name; + entries[num_entries].queryId = queryId; + entries[num_entries].advice_string = advice_str; + num_entries++; + } + else + { + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: unrecognized line type", + PGSA_DUMP_FILE, lineno))); + } + } + + /* + * Parsing succeeded. Apply everything to shared memory. + * + * At this point, we know that the file we just read is fully valid, but + * it's still possible for this to fail if, for example, DSA memory cannot + * be allocated. If that happens, the worker will die, the postmaster will + * eventually restart it, and we'll try again after clearing any data that + * we did manage to put into shared memory. (Note that we call + * pgsa_reset_all_stashes() at the top of this function.) + */ + pgsa_restore_stashes(saved_stashes); + pgsa_restore_entries(entries, num_entries); + + /* Hooray, it worked! Notify the user. */ + ereport(LOG, + (errmsg("loaded %d advice stashes and %d entries from \"%s\"", + num_stashes, num_entries, PGSA_DUMP_FILE))); + + /* Clean up. */ + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(tmpcxt); +} + +/* + * Write all advice stash data to disk. + * + * The file format is a simple TSV with a line-type prefix: + * stash\tstash_name + * entry\tstash_name\tquery_id\tadvice_string + */ +static void +pgsa_write_to_disk(void) +{ + pgsa_writer_context wctx = {0}; + MemoryContext tmpcxt; + MemoryContext oldcxt; + + Assert(pgsa_entry_dshash != NULL); + + /* Use a temporary context so all allocations are freed at the end. */ + tmpcxt = AllocSetContextCreate(CurrentMemoryContext, + "pg_stash_advice dump", + ALLOCSET_DEFAULT_SIZES); + oldcxt = MemoryContextSwitchTo(tmpcxt); + + /* Set up the writer context. */ + snprintf(wctx.pathname, MAXPGPATH, "%s.tmp", PGSA_DUMP_FILE); + wctx.file = AllocateFile(wctx.pathname, "w"); + if (!wctx.file) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", wctx.pathname))); + wctx.nhash = pgsa_stash_name_table_create(tmpcxt, 64, NULL); + initStringInfo(&wctx.buf); + + /* Write stash lines, then entry lines. */ + pgsa_write_stashes(&wctx); + pgsa_write_entries(&wctx); + + /* + * If nothing was written, remove both the temp file and any existing dump + * file rather than installing a zero-length file. + */ + if (wctx.nhash->members == 0) + { + ereport(DEBUG1, + errmsg("there are no advice stashes to save")); + FreeFile(wctx.file); + unlink(wctx.pathname); + if (unlink(PGSA_DUMP_FILE) == 0) + ereport(DEBUG1, + errmsg("removed \"%s\"", PGSA_DUMP_FILE)); + } + else + { + if (FreeFile(wctx.file) != 0) + { + int save_errno = errno; + + unlink(wctx.pathname); + errno = save_errno; + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + wctx.pathname))); + } + (void) durable_rename(wctx.pathname, PGSA_DUMP_FILE, ERROR); + + ereport(LOG, + errmsg("saved %d advice stashes and %d entries to \"%s\"", + (int) wctx.nhash->members, wctx.entries_written, + PGSA_DUMP_FILE)); + } + + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(tmpcxt); +} + +/* + * Append the TSV-escaped form of str to buf. + * + * Backslash, tab, newline, and carriage return are escaped with backslash + * sequences. All other characters are passed through unchanged. + */ +static void +pgsa_append_tsv_escaped_string(StringInfo buf, const char *str) +{ + for (const char *p = str; *p != '\0'; p++) + { + switch (*p) + { + case '\\': + appendStringInfoString(buf, "\\\\"); + break; + case '\t': + appendStringInfoString(buf, "\\t"); + break; + case '\n': + appendStringInfoString(buf, "\\n"); + break; + case '\r': + appendStringInfoString(buf, "\\r"); + break; + default: + appendStringInfoChar(buf, *p); + break; + } + } +} + +/* + * Extract the next tab-delimited field from *cursor. + * + * The tab delimiter is replaced with '\0' and *cursor is advanced past it. + * If *cursor already points to '\0' (no more fields), returns NULL. + */ +static char * +pgsa_next_tsv_field(char **cursor) +{ + char *start = *cursor; + char *p = start; + + if (*p == '\0') + return NULL; + + while (*p != '\0' && *p != '\t') + p++; + + if (*p == '\t') + *p++ = '\0'; + + *cursor = p; + return start; +} + +/* + * Insert entries into shared memory from the parsed entry array. + */ +static void +pgsa_restore_entries(pgsa_saved_entry *entries, int num_entries) +{ + LWLockAcquire(&pgsa_state->lock, LW_SHARED); + for (int i = 0; i < num_entries; i++) + { + ereport(DEBUG2, + errmsg("restoring advice stash entry for \"%s\", query ID %" PRId64, + entries[i].stash_name, entries[i].queryId)); + pgsa_set_advice_string(entries[i].stash_name, + entries[i].queryId, + entries[i].advice_string); + } + LWLockRelease(&pgsa_state->lock); +} + +/* + * Create stashes in shared memory from the parsed stash hash table. + */ +static void +pgsa_restore_stashes(pgsa_saved_stash_table_hash *saved_stashes) +{ + pgsa_saved_stash_table_iterator iter; + pgsa_saved_stash *s; + + LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE); + pgsa_saved_stash_table_start_iterate(saved_stashes, &iter); + while ((s = pgsa_saved_stash_table_iterate(saved_stashes, + &iter)) != NULL) + { + ereport(DEBUG2, + errmsg("restoring advice stash \"%s\"", s->name)); + pgsa_create_stash(s->name); + } + LWLockRelease(&pgsa_state->lock); +} + +/* + * Unescape a TSV field in place. + * + * Recognized escape sequences are \\, \t, \n, and \r. A trailing backslash + * or an unrecognized escape sequence is a syntax error. + */ +static void +pgsa_unescape_tsv_field(char *str, const char *filename, unsigned lineno) +{ + char *src = str; + char *dst = str; + + while (*src != '\0') + { + /* Just pass through anything that's not a backslash-escape. */ + if (likely(*src != '\\')) + { + *dst++ = *src++; + continue; + } + + /* Check what sort of escape we've got. */ + switch (src[1]) + { + case '\\': + *dst++ = '\\'; + break; + case 't': + *dst++ = '\t'; + break; + case 'n': + *dst++ = '\n'; + break; + case 'r': + *dst++ = '\r'; + break; + case '\0': + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: trailing backslash", + filename, lineno))); + break; + default: + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("syntax error in file \"%s\" line %u: unrecognized escape \"\\%c\"", + filename, lineno, src[1]))); + break; + } + + /* We consumed the backslash and the following character. */ + src += 2; + } + *dst = '\0'; +} + +/* + * Write an entry line for each advice entry. + */ +static void +pgsa_write_entries(pgsa_writer_context *wctx) +{ + dshash_seq_status iter; + pgsa_entry *entry; + + dshash_seq_init(&iter, pgsa_entry_dshash, false); + while ((entry = dshash_seq_next(&iter)) != NULL) + { + pgsa_stash_name *n; + char *advice_string; + + if (entry->advice_string == InvalidDsaPointer) + continue; + + n = pgsa_stash_name_table_lookup(wctx->nhash, + entry->key.pgsa_stash_id); + if (n == NULL) + continue; /* orphan entry, skip */ + + advice_string = dsa_get_address(pgsa_dsa_area, entry->advice_string); + + resetStringInfo(&wctx->buf); + appendStringInfo(&wctx->buf, "entry\t%s\t%" PRId64 "\t", + n->name, entry->key.queryId); + pgsa_append_tsv_escaped_string(&wctx->buf, advice_string); + appendStringInfoChar(&wctx->buf, '\n'); + fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file); + if (ferror(wctx->file)) + pgsa_write_error(wctx); + wctx->entries_written++; + } + dshash_seq_term(&iter); +} + +/* + * Clean up and report a write error. Does not return. + */ +static void +pgsa_write_error(pgsa_writer_context *wctx) +{ + int save_errno = errno; + + FreeFile(wctx->file); + unlink(wctx->pathname); + errno = save_errno; + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": %m", wctx->pathname))); +} + +/* + * Write a stash line for each advice stash, and populate the ID-to-name + * hash table for use by pgsa_write_entries. + */ +static void +pgsa_write_stashes(pgsa_writer_context *wctx) +{ + dshash_seq_status iter; + pgsa_stash *stash; + + dshash_seq_init(&iter, pgsa_stash_dshash, false); + while ((stash = dshash_seq_next(&iter)) != NULL) + { + pgsa_stash_name *n; + bool found; + + n = pgsa_stash_name_table_insert(wctx->nhash, stash->pgsa_stash_id, + &found); + Assert(!found); + n->name = pstrdup(stash->name); + + resetStringInfo(&wctx->buf); + appendStringInfo(&wctx->buf, "stash\t%s\n", n->name); + fwrite(wctx->buf.data, 1, wctx->buf.len, wctx->file); + if (ferror(wctx->file)) + pgsa_write_error(wctx); + } + dshash_seq_term(&iter); +} diff --git a/contrib/pg_stash_advice/t/001_persist.pl b/contrib/pg_stash_advice/t/001_persist.pl new file mode 100644 index 0000000000000..d146616660212 --- /dev/null +++ b/contrib/pg_stash_advice/t/001_persist.pl @@ -0,0 +1,84 @@ + +# Copyright (c) 2016-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); + +$node->init; +$node->append_conf( + 'postgresql.conf', + qq{shared_preload_libraries = 'pg_plan_advice, pg_stash_advice' +pg_stash_advice.persist = true +pg_stash_advice.persist_interval = 0}); +$node->start; + +$node->safe_psql("postgres", + "CREATE EXTENSION pg_stash_advice;\n"); + +# Create two stashes: one with 2 entries, one with 1 entry. +$node->safe_psql("postgres", qq{ + SELECT pg_create_advice_stash('stash_a'); + SELECT pg_set_stashed_advice('stash_a', 1001, 'IndexScan(t)'); + SELECT pg_set_stashed_advice('stash_a', 1002, E'line1\\nline2\\ttab\\\\backslash'); + SELECT pg_create_advice_stash('stash_b'); + SELECT pg_set_stashed_advice('stash_b', 2001, 'SeqScan(t)'); +}); + +# Verify before restart. +my $result = $node->safe_psql("postgres", + "SELECT stash_name, num_entries FROM pg_get_advice_stashes() ORDER BY stash_name"); +is($result, "stash_a|2\nstash_b|1", 'stashes present before restart'); + +# Restart and verify the data survived. +$node->restart; +$node->wait_for_log("loaded 2 advice stashes and 3 entries"); + +$result = $node->safe_psql("postgres", + "SELECT stash_name, num_entries FROM pg_get_advice_stashes() ORDER BY stash_name"); +is($result, "stash_a|2\nstash_b|1", 'stashes survived restart'); + +# Verify entry contents, including the one with special characters. +$result = $node->safe_psql("postgres", + "SELECT stash_name, query_id, advice_string FROM pg_get_advice_stash_contents(NULL) ORDER BY stash_name, query_id"); +is($result, + "stash_a|1001|IndexScan(t)\nstash_a|1002|line1\nline2\ttab\\backslash\nstash_b|2001|SeqScan(t)", + 'entry contents survived restart with special characters intact'); + +# Add a third stash with 0 entries. +$node->safe_psql("postgres", qq{ + SELECT pg_create_advice_stash('stash_c'); +}); + +# Restart again and verify all three stashes are present. +$node->restart; +$node->wait_for_log("loaded 3 advice stashes and 3 entries"); + +$result = $node->safe_psql("postgres", + "SELECT stash_name, num_entries FROM pg_get_advice_stashes() ORDER BY stash_name"); +is($result, "stash_a|2\nstash_b|1\nstash_c|0", 'all three stashes survived second restart'); + +# Drop all stashes and verify the dump file is removed after restart. +$node->safe_psql("postgres", qq{ + SELECT pg_drop_advice_stash('stash_a'); + SELECT pg_drop_advice_stash('stash_b'); + SELECT pg_drop_advice_stash('stash_c'); +}); + +$node->restart; + +$result = $node->safe_psql("postgres", + "SELECT count(*) FROM pg_get_advice_stashes()"); +is($result, "0", 'no stashes after dropping all and restarting'); + +ok(!-f $node->data_dir . '/pg_stash_advice.tsv', + 'dump file removed after all stashes dropped'); + +$node->stop; + +done_testing(); diff --git a/contrib/pg_stat_statements/Makefile b/contrib/pg_stat_statements/Makefile index b2bd8794d2a14..c27e9529bb60c 100644 --- a/contrib/pg_stat_statements/Makefile +++ b/contrib/pg_stat_statements/Makefile @@ -7,6 +7,7 @@ OBJS = \ EXTENSION = pg_stat_statements DATA = pg_stat_statements--1.4.sql \ + pg_stat_statements--1.12--1.13.sql \ pg_stat_statements--1.11--1.12.sql pg_stat_statements--1.10--1.11.sql \ pg_stat_statements--1.9--1.10.sql pg_stat_statements--1.8--1.9.sql \ pg_stat_statements--1.7--1.8.sql pg_stat_statements--1.6--1.7.sql \ @@ -18,9 +19,26 @@ PGFILEDESC = "pg_stat_statements - execution statistics of SQL statements" LDFLAGS_SL += $(filter -lm, $(LIBS)) REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/pg_stat_statements/pg_stat_statements.conf -REGRESS = select dml cursors utility level_tracking planning \ - user_activity wal entry_timestamp privileges extended \ - parallel cleanup oldextversions squashing + +# Note: Test "cleanup" is kept second to last, removing the extension. +REGRESS = \ + select \ + dml \ + cursors \ + utility \ + level_tracking \ + planning \ + user_activity \ + wal \ + entry_timestamp \ + privileges \ + extended \ + parallel \ + plancache \ + squashing \ + cleanup \ + oldextversions + # Disabled because these tests require "shared_preload_libraries=pg_stat_statements", # which typical installcheck users do not have (e.g. buildfarm clients). NO_INSTALLCHECK = 1 diff --git a/contrib/pg_stat_statements/expected/cursors.out b/contrib/pg_stat_statements/expected/cursors.out index 0fc4b2c098d0e..6afb48ace9220 100644 --- a/contrib/pg_stat_statements/expected/cursors.out +++ b/contrib/pg_stat_statements/expected/cursors.out @@ -57,8 +57,8 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; 1 | 0 | COMMIT 1 | 0 | DECLARE cursor_stats_1 CURSOR WITH HOLD FOR SELECT $1 1 | 0 | DECLARE cursor_stats_2 CURSOR WITH HOLD FOR SELECT $1 - 1 | 1 | FETCH 1 IN cursor_stats_1 - 1 | 1 | FETCH 1 IN cursor_stats_2 + 1 | 1 | FETCH $1 IN cursor_stats_1 + 1 | 1 | FETCH $1 IN cursor_stats_2 1 | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (9 rows) @@ -68,3 +68,140 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t; t (1 row) +-- Normalization of FETCH statements +BEGIN; +DECLARE pgss_cursor CURSOR FOR SELECT FROM generate_series(1, 10); +-- implicit directions +FETCH pgss_cursor; +-- +(1 row) + +FETCH 1 pgss_cursor; +-- +(1 row) + +FETCH 2 pgss_cursor; +-- +(2 rows) + +FETCH -1 pgss_cursor; +-- +(1 row) + +-- explicit NEXT +FETCH NEXT pgss_cursor; +-- +(1 row) + +-- explicit PRIOR +FETCH PRIOR pgss_cursor; +-- +(1 row) + +-- explicit FIRST +FETCH FIRST pgss_cursor; +-- +(1 row) + +-- explicit LAST +FETCH LAST pgss_cursor; +-- +(1 row) + +-- explicit ABSOLUTE +FETCH ABSOLUTE 1 pgss_cursor; +-- +(1 row) + +FETCH ABSOLUTE 2 pgss_cursor; +-- +(1 row) + +FETCH ABSOLUTE -1 pgss_cursor; +-- +(1 row) + +-- explicit RELATIVE +FETCH RELATIVE 1 pgss_cursor; +-- +(0 rows) + +FETCH RELATIVE 2 pgss_cursor; +-- +(0 rows) + +FETCH RELATIVE -1 pgss_cursor; +-- +(1 row) + +-- explicit FORWARD +FETCH ALL pgss_cursor; +-- +(0 rows) + +-- explicit FORWARD ALL +FETCH FORWARD ALL pgss_cursor; +-- +(0 rows) + +-- explicit FETCH FORWARD +FETCH FORWARD pgss_cursor; +-- +(0 rows) + +FETCH FORWARD 1 pgss_cursor; +-- +(0 rows) + +FETCH FORWARD 2 pgss_cursor; +-- +(0 rows) + +FETCH FORWARD -1 pgss_cursor; +-- +(1 row) + +-- explicit FETCH BACKWARD +FETCH BACKWARD pgss_cursor; +-- +(1 row) + +FETCH BACKWARD 1 pgss_cursor; +-- +(1 row) + +FETCH BACKWARD 2 pgss_cursor; +-- +(2 rows) + +FETCH BACKWARD -1 pgss_cursor; +-- +(1 row) + +-- explicit BACKWARD ALL +FETCH BACKWARD ALL pgss_cursor; +-- +(6 rows) + +COMMIT; +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; + calls | query +-------+-------------------------------------------------------------------- + 1 | BEGIN + 1 | COMMIT + 1 | DECLARE pgss_cursor CURSOR FOR SELECT FROM generate_series($1, $2) + 3 | FETCH ABSOLUTE $1 pgss_cursor + 1 | FETCH ALL pgss_cursor + 1 | FETCH BACKWARD ALL pgss_cursor + 4 | FETCH BACKWARD pgss_cursor + 1 | FETCH FIRST pgss_cursor + 1 | FETCH FORWARD ALL pgss_cursor + 4 | FETCH FORWARD pgss_cursor + 1 | FETCH LAST pgss_cursor + 1 | FETCH NEXT pgss_cursor + 1 | FETCH PRIOR pgss_cursor + 3 | FETCH RELATIVE $1 pgss_cursor + 4 | FETCH pgss_cursor + 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t +(16 rows) + diff --git a/contrib/pg_stat_statements/expected/extended.out b/contrib/pg_stat_statements/expected/extended.out index 04a05943372b9..1bfd0c1ca242f 100644 --- a/contrib/pg_stat_statements/expected/extended.out +++ b/contrib/pg_stat_statements/expected/extended.out @@ -68,3 +68,97 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; 1 | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (4 rows) +-- Various parameter numbering patterns +-- Unique query IDs with parameter numbers switched. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT WHERE ($1::int, 7) IN ((8, $2::int), ($3::int, 9)) \bind '1' '2' '3' \g +-- +(0 rows) + +SELECT WHERE ($2::int, 10) IN ((11, $3::int), ($1::int, 12)) \bind '1' '2' '3' \g +-- +(0 rows) + +SELECT WHERE $1::int IN ($2::int, $3::int) \bind '1' '2' '3' \g +-- +(0 rows) + +SELECT WHERE $2::int IN ($3::int, $1::int) \bind '1' '2' '3' \g +-- +(0 rows) + +SELECT WHERE $3::int IN ($1::int, $2::int) \bind '1' '2' '3' \g +-- +(0 rows) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +--------------------------------------------------------------+------- + SELECT WHERE $1::int IN ($2 /*, ... */) | 1 + SELECT WHERE $1::int IN ($2 /*, ... */) | 1 + SELECT WHERE $1::int IN ($2 /*, ... */) | 1 + SELECT WHERE ($1::int, $4) IN (($5, $2::int), ($3::int, $6)) | 1 + SELECT WHERE ($2::int, $4) IN (($5, $3::int), ($1::int, $6)) | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(6 rows) + +-- Two groups of two queries with the same query ID. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT WHERE '1'::int IN ($1::int, '2'::int) \bind '1' \g +-- +(1 row) + +SELECT WHERE '4'::int IN ($1::int, '5'::int) \bind '2' \g +-- +(0 rows) + +SELECT WHERE $2::int IN ($1::int, '1'::int) \bind '1' '2' \g +-- +(0 rows) + +SELECT WHERE $2::int IN ($1::int, '2'::int) \bind '3' '4' \g +-- +(0 rows) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------+------- + SELECT WHERE $1::int IN ($2 /*, ... */) | 2 + SELECT WHERE $1::int IN ($2 /*, ... */) | 2 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- no squashable list, the parameters id's are kept as-is +SELECT WHERE $3 = $1 AND $2 = $4 \bind 1 2 1 2 \g +-- +(1 row) + +-- squashable list, so the parameter IDs will be re-assigned +SELECT WHERE 1 IN (1, 2, 3) AND $3 = $1 AND $2 = $4 \bind 1 2 1 2 \g +-- +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +------------------------------------------------------------+------- + SELECT WHERE $1 IN ($2 /*, ... */) AND $3 = $4 AND $5 = $6 | 1 + SELECT WHERE $3 = $1 AND $2 = $4 | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) + diff --git a/contrib/pg_stat_statements/expected/level_tracking.out b/contrib/pg_stat_statements/expected/level_tracking.out index 75e785e1719ea..832d65e97cad6 100644 --- a/contrib/pg_stat_statements/expected/level_tracking.out +++ b/contrib/pg_stat_statements/expected/level_tracking.out @@ -206,37 +206,37 @@ EXPLAIN (COSTS OFF) SELECT 1 UNION SELECT 2; SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+-------------------------------------------------------------------- - f | 1 | DELETE FROM stats_track_tab + toplevel | calls | query +----------+-------+--------------------------------------------------------------------- t | 1 | EXPLAIN (COSTS OFF) (SELECT $1, $2) + f | 1 | EXPLAIN (COSTS OFF) (SELECT $1, $2); t | 1 | EXPLAIN (COSTS OFF) (TABLE test_table) + f | 1 | EXPLAIN (COSTS OFF) (TABLE test_table); t | 1 | EXPLAIN (COSTS OFF) (VALUES ($1, $2)) + f | 1 | EXPLAIN (COSTS OFF) (VALUES ($1, $2)); t | 1 | EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab + f | 1 | EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab; t | 1 | EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES (($1)) - t | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + - | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id + - | | WHEN MATCHED THEN UPDATE SET x = id + + f | 1 | EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES (($1)); + t | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) + f | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id); t | 1 | EXPLAIN (COSTS OFF) SELECT $1 t | 1 | EXPLAIN (COSTS OFF) SELECT $1 UNION SELECT $2 + f | 1 | EXPLAIN (COSTS OFF) SELECT $1 UNION SELECT $2; + f | 1 | EXPLAIN (COSTS OFF) SELECT $1; t | 1 | EXPLAIN (COSTS OFF) TABLE stats_track_tab + f | 1 | EXPLAIN (COSTS OFF) TABLE stats_track_tab; t | 1 | EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = $1 WHERE x = $2 + f | 1 | EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = $1 WHERE x = $2; t | 1 | EXPLAIN (COSTS OFF) VALUES ($1) - f | 1 | INSERT INTO stats_track_tab VALUES (($1)) - f | 1 | MERGE INTO stats_track_tab + - | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id + - | | WHEN MATCHED THEN UPDATE SET x = id + - | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) - f | 1 | SELECT $1 - f | 1 | SELECT $1 UNION SELECT $2 - f | 1 | SELECT $1, $2 + f | 1 | EXPLAIN (COSTS OFF) VALUES ($1); t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t - f | 1 | TABLE stats_track_tab - f | 1 | TABLE test_table - f | 1 | UPDATE stats_track_tab SET x = $1 WHERE x = $2 - f | 1 | VALUES ($1) - f | 1 | VALUES ($1, $2) (23 rows) -- EXPLAIN - top-level tracking. @@ -405,20 +405,20 @@ EXPLAIN (COSTS OFF) SELECT 1, 2 UNION SELECT 3, 4\; EXPLAIN (COSTS OFF) (SELECT SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+----------------------------------------------------------------- - f | 1 | (SELECT $1, $2, $3) UNION SELECT $4, $5, $6 + toplevel | calls | query +----------+-------+--------------------------------------------------------------------------------------------------------------------- t | 1 | EXPLAIN (COSTS OFF) (SELECT $1, $2, $3) t | 1 | EXPLAIN (COSTS OFF) (SELECT $1, $2, $3) UNION SELECT $4, $5, $6 + f | 1 | EXPLAIN (COSTS OFF) (SELECT $1, $2, $3); EXPLAIN (COSTS OFF) (SELECT 1, 2, 3, 4); t | 1 | EXPLAIN (COSTS OFF) (SELECT $1, $2, $3, $4) + f | 1 | EXPLAIN (COSTS OFF) (SELECT 1, 2, 3); EXPLAIN (COSTS OFF) (SELECT $1, $2, $3, $4); t | 1 | EXPLAIN (COSTS OFF) SELECT $1 t | 1 | EXPLAIN (COSTS OFF) SELECT $1, $2 t | 1 | EXPLAIN (COSTS OFF) SELECT $1, $2 UNION SELECT $3, $4 - f | 1 | SELECT $1 - f | 1 | SELECT $1, $2 - f | 1 | SELECT $1, $2 UNION SELECT $3, $4 - f | 1 | SELECT $1, $2, $3 - f | 1 | SELECT $1, $2, $3, $4 + f | 1 | EXPLAIN (COSTS OFF) SELECT $1, $2 UNION SELECT $3, $4; EXPLAIN (COSTS OFF) (SELECT 1, 2, 3) UNION SELECT 3, 4, 5; + f | 1 | EXPLAIN (COSTS OFF) SELECT $1; EXPLAIN (COSTS OFF) SELECT 1, 2; + f | 1 | EXPLAIN (COSTS OFF) SELECT 1, 2 UNION SELECT 3, 4; EXPLAIN (COSTS OFF) (SELECT $1, $2, $3) UNION SELECT $4, $5, $6; + f | 1 | EXPLAIN (COSTS OFF) SELECT 1; EXPLAIN (COSTS OFF) SELECT $1, $2; t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (13 rows) @@ -494,29 +494,29 @@ EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES ((1))\; EXPLAIN (COSTS OF SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+-------------------------------------------------------------------- - f | 1 | DELETE FROM stats_track_tab - f | 1 | DELETE FROM stats_track_tab WHERE x = $1 + toplevel | calls | query +----------+-------+---------------------------------------------------------------------------------------------------------------------------------- t | 1 | EXPLAIN (COSTS OFF) (TABLE test_table) t | 1 | EXPLAIN (COSTS OFF) (VALUES ($1, $2)) t | 1 | EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab t | 1 | EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab WHERE x = $1 + f | 1 | EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab; EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab WHERE x = $1; + f | 1 | EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab; EXPLAIN (COSTS OFF) DELETE FROM stats_track_tab WHERE x = 1; t | 1 | EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES ($1), ($2) t | 1 | EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES (($1)) + f | 1 | EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES (($1)); EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES (1), (2); + f | 1 | EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES ((1)); EXPLAIN (COSTS OFF) INSERT INTO stats_track_tab VALUES ($1), ($2); t | 1 | EXPLAIN (COSTS OFF) TABLE stats_track_tab + f | 1 | EXPLAIN (COSTS OFF) TABLE stats_track_tab; EXPLAIN (COSTS OFF) (TABLE test_table); + f | 1 | EXPLAIN (COSTS OFF) TABLE stats_track_tab; EXPLAIN (COSTS OFF) (TABLE test_table); t | 1 | EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = $1 t | 1 | EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = $1 WHERE x = $2 + f | 1 | EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = $1 WHERE x = $2; EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = 1; + f | 1 | EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = 1 WHERE x = 1; EXPLAIN (COSTS OFF) UPDATE stats_track_tab SET x = $1; t | 1 | EXPLAIN (COSTS OFF) VALUES ($1) - f | 1 | INSERT INTO stats_track_tab VALUES ($1), ($2) - f | 1 | INSERT INTO stats_track_tab VALUES (($1)) + f | 1 | EXPLAIN (COSTS OFF) VALUES ($1); EXPLAIN (COSTS OFF) (VALUES (1, 2)); + f | 1 | EXPLAIN (COSTS OFF) VALUES (1); EXPLAIN (COSTS OFF) (VALUES ($1, $2)); t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t - f | 1 | TABLE stats_track_tab - f | 1 | TABLE test_table - f | 1 | UPDATE stats_track_tab SET x = $1 - f | 1 | UPDATE stats_track_tab SET x = $1 WHERE x = $2 - f | 1 | VALUES ($1) - f | 1 | VALUES ($1, $2) (21 rows) SELECT pg_stat_statements_reset() IS NOT NULL AS t; @@ -547,18 +547,21 @@ EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+--------------------------------------------------------------- - t | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + - | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id+ - | | WHEN MATCHED THEN UPDATE SET x = id + + toplevel | calls | query +----------+-------+------------------------------------------------------------------------------------------------ + t | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) + f | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id); EXPLAIN (COSTS OFF) SELECT 1, 2, 3, 4, 5; + f | 1 | EXPLAIN (COSTS OFF) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series(1, 10) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id); EXPLAIN (COSTS OFF) SELECT $1, $2, $3, $4, $5; t | 1 | EXPLAIN (COSTS OFF) SELECT $1, $2, $3, $4, $5 - f | 1 | MERGE INTO stats_track_tab + - | | USING (SELECT id FROM generate_series($1, $2) id) ON x = id+ - | | WHEN MATCHED THEN UPDATE SET x = id + - | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) - f | 1 | SELECT $1, $2, $3, $4, $5 t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (5 rows) @@ -786,29 +789,29 @@ EXPLAIN (COSTS OFF) WITH a AS (select 4) SELECT 1 UNION SELECT 2; SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+------------------------------------------------------------------------------------------ + toplevel | calls | query +----------+-------+------------------------------------------------------------------------------------------- t | 1 | EXPLAIN (COSTS OFF) (WITH a AS (SELECT $1) (SELECT $2, $3)) + f | 1 | EXPLAIN (COSTS OFF) (WITH a AS (SELECT $1) (SELECT $2, $3)); t | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) DELETE FROM stats_track_tab + f | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) DELETE FROM stats_track_tab; t | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) INSERT INTO stats_track_tab VALUES (($2)) - t | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) MERGE INTO stats_track_tab + - | | USING (SELECT id FROM generate_series($2, $3) id) ON x = id + - | | WHEN MATCHED THEN UPDATE SET x = id + + f | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) INSERT INTO stats_track_tab VALUES (($2)); + t | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series($2, $3) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) + f | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) MERGE INTO stats_track_tab + + | | USING (SELECT id FROM generate_series($2, $3) id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id); t | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) SELECT $2 + f | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) SELECT $2; t | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) UPDATE stats_track_tab SET x = $2 WHERE x = $3 + f | 1 | EXPLAIN (COSTS OFF) WITH a AS (SELECT $1) UPDATE stats_track_tab SET x = $2 WHERE x = $3; t | 1 | EXPLAIN (COSTS OFF) WITH a AS (select $1) SELECT $2 UNION SELECT $3 + f | 1 | EXPLAIN (COSTS OFF) WITH a AS (select $1) SELECT $2 UNION SELECT $3; t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t - f | 1 | WITH a AS (SELECT $1) (SELECT $2, $3) - f | 1 | WITH a AS (SELECT $1) DELETE FROM stats_track_tab - f | 1 | WITH a AS (SELECT $1) INSERT INTO stats_track_tab VALUES (($2)) - f | 1 | WITH a AS (SELECT $1) MERGE INTO stats_track_tab + - | | USING (SELECT id FROM generate_series($2, $3) id) ON x = id + - | | WHEN MATCHED THEN UPDATE SET x = id + - | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) - f | 1 | WITH a AS (SELECT $1) SELECT $2 - f | 1 | WITH a AS (SELECT $1) UPDATE stats_track_tab SET x = $2 WHERE x = $3 - f | 1 | WITH a AS (select $1) SELECT $2 UNION SELECT $3 (15 rows) -- EXPLAIN with CTEs - top-level tracking @@ -918,13 +921,14 @@ EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+------------------------------------------------------------------------------ - t | 1 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) + + toplevel | calls | query +----------+-------+------------------------------------------------------------------------------- + t | 1 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) + | | DECLARE foocur CURSOR FOR SELECT * FROM stats_track_tab + f | 1 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) + + | | DECLARE foocur CURSOR FOR SELECT * FROM stats_track_tab; t | 1 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT $1 - f | 1 | SELECT $1 - f | 1 | SELECT * FROM stats_track_tab + f | 1 | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT $1; t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (5 rows) @@ -1047,10 +1051,10 @@ SELECT toplevel, calls, query FROM pg_stat_statements toplevel | calls | query ----------+-------+----------------------------------------------------------------- t | 1 | CREATE TEMPORARY TABLE pgss_ctas_1 AS SELECT $1 + f | 1 | CREATE TEMPORARY TABLE pgss_ctas_1 AS SELECT $1; t | 1 | CREATE TEMPORARY TABLE pgss_ctas_2 AS EXECUTE test_prepare_pgss - f | 1 | SELECT $1 + f | 1 | PREPARE test_prepare_pgss AS select generate_series($1, $2) t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t - f | 1 | select generate_series($1, $2) (5 rows) -- CREATE TABLE AS, top-level tracking. @@ -1088,10 +1092,10 @@ EXPLAIN (COSTS OFF) CREATE TEMPORARY TABLE pgss_explain_ctas AS SELECT 1; SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+--------------------------------------------------------------------------- + toplevel | calls | query +----------+-------+---------------------------------------------------------------------------- t | 1 | EXPLAIN (COSTS OFF) CREATE TEMPORARY TABLE pgss_explain_ctas AS SELECT $1 - f | 1 | SELECT $1 + f | 1 | EXPLAIN (COSTS OFF) CREATE TEMPORARY TABLE pgss_explain_ctas AS SELECT $1; t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (3 rows) @@ -1136,14 +1140,14 @@ CLOSE foocur; COMMIT; SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+--------------------------------------------------------- + toplevel | calls | query +----------+-------+---------------------------------------------------------- t | 1 | BEGIN t | 1 | CLOSE foocur t | 1 | COMMIT t | 1 | DECLARE FOOCUR CURSOR FOR SELECT * from stats_track_tab - t | 1 | FETCH FORWARD 1 FROM foocur - f | 1 | SELECT * from stats_track_tab + f | 1 | DECLARE FOOCUR CURSOR FOR SELECT * from stats_track_tab; + t | 1 | FETCH FORWARD $1 FROM foocur t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (7 rows) @@ -1172,7 +1176,7 @@ SELECT toplevel, calls, query FROM pg_stat_statements t | 1 | CLOSE foocur t | 1 | COMMIT t | 1 | DECLARE FOOCUR CURSOR FOR SELECT * FROM stats_track_tab - t | 1 | FETCH FORWARD 1 FROM foocur + t | 1 | FETCH FORWARD $1 FROM foocur t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (6 rows) @@ -1203,25 +1207,25 @@ COPY (DELETE FROM stats_track_tab WHERE x = 2 RETURNING x) TO stdout; 2 SELECT toplevel, calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; - toplevel | calls | query -----------+-------+--------------------------------------------------------------------------- + toplevel | calls | query +----------+-------+----------------------------------------------------------------------------- + f | 1 | COPY (DELETE FROM stats_track_tab WHERE x = $1 RETURNING x) TO stdout t | 1 | COPY (DELETE FROM stats_track_tab WHERE x = 2 RETURNING x) TO stdout + f | 1 | COPY (INSERT INTO stats_track_tab (x) VALUES ($1) RETURNING x) TO stdout t | 1 | COPY (INSERT INTO stats_track_tab (x) VALUES (1) RETURNING x) TO stdout - t | 1 | COPY (MERGE INTO stats_track_tab USING (SELECT 1 id) ON x = id + - | | WHEN MATCHED THEN UPDATE SET x = id + + f | 1 | COPY (MERGE INTO stats_track_tab USING (SELECT $1 id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) RETURNING x) TO stdout + t | 1 | COPY (MERGE INTO stats_track_tab USING (SELECT 1 id) ON x = id + + | | WHEN MATCHED THEN UPDATE SET x = id + | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) RETURNING x) TO stdout + f | 1 | COPY (SELECT $1 UNION SELECT $2) TO stdout + f | 1 | COPY (SELECT $1) TO stdout t | 1 | COPY (SELECT 1 UNION SELECT 2) TO stdout t | 1 | COPY (SELECT 1) TO stdout + f | 1 | COPY (UPDATE stats_track_tab SET x = $1 WHERE x = $2 RETURNING x) TO stdout t | 1 | COPY (UPDATE stats_track_tab SET x = 2 WHERE x = 1 RETURNING x) TO stdout - f | 1 | DELETE FROM stats_track_tab WHERE x = $1 RETURNING x - f | 1 | INSERT INTO stats_track_tab (x) VALUES ($1) RETURNING x - f | 1 | MERGE INTO stats_track_tab USING (SELECT $1 id) ON x = id + - | | WHEN MATCHED THEN UPDATE SET x = id + - | | WHEN NOT MATCHED THEN INSERT (x) VALUES (id) RETURNING x - f | 1 | SELECT $1 - f | 1 | SELECT $1 UNION SELECT $2 t | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t - f | 1 | UPDATE stats_track_tab SET x = $1 WHERE x = $2 RETURNING x (13 rows) -- COPY - top-level tracking. @@ -1496,12 +1500,11 @@ SELECT PLUS_ONE(1); SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; calls | rows | query -------+------+---------------------------------------------------- - 2 | 2 | SELECT (i + $2 + $3)::INTEGER 2 | 2 | SELECT (i + $2)::INTEGER LIMIT $3 2 | 2 | SELECT PLUS_ONE($1) 2 | 2 | SELECT PLUS_TWO($1) 1 | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t -(5 rows) +(4 rows) -- immutable SQL function --- can be executed at plan time CREATE FUNCTION PLUS_THREE(i INTEGER) RETURNS INTEGER AS @@ -1521,16 +1524,88 @@ SELECT PLUS_THREE(10); SELECT toplevel, calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; toplevel | calls | rows | query ----------+-------+------+------------------------------------------------------------------------------ - f | 2 | 2 | SELECT (i + $2 + $3)::INTEGER f | 2 | 2 | SELECT (i + $2)::INTEGER LIMIT $3 t | 2 | 2 | SELECT PLUS_ONE($1) t | 2 | 2 | SELECT PLUS_THREE($1) t | 2 | 2 | SELECT PLUS_TWO($1) - t | 1 | 5 | SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C" + t | 1 | 4 | SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C" f | 2 | 2 | SELECT i + $2 LIMIT $3 t | 1 | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t -(8 rows) +(7 rows) + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- planner - all-level tracking. +SET pg_stat_statements.track_planning = TRUE; +-- Release all cached plans before the first function call. This matters +-- when debug_discard_caches is enabled, which would store a normalized +-- version of the inner query of the function. Forcing a plan rebuild +-- ensures that a normalized version is always stored with the stats entry, +-- while checking that the nesting level is computed correctly in the +-- planner hook. +DISCARD PLANS; +SELECT PLUS_THREE(8); + plus_three +------------ + 11 +(1 row) + +SELECT PLUS_THREE(10); + plus_three +------------ + 13 +(1 row) + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+-------------------------------------------------------------------- + t | 2 | 2 | 2 | SELECT PLUS_THREE($1) + f | 2 | 2 | 2 | SELECT i + $2 LIMIT $3 + t | 1 | 1 | 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t + t | 0 | 0 | 1 | SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements+ + | | | | ORDER BY query COLLATE "C" +(4 rows) + +RESET pg_stat_statements.track_planning; +-- AFTER trigger SQL (ExecutorFinish) - all-level tracking. +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +CREATE TABLE test_trigger (id int, name text); +CREATE TABLE audit_table (table_name text, action text, row_id int); +CREATE OR REPLACE FUNCTION audit_trigger_func() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO audit_table VALUES ('test_trigger', TG_OP, NEW.id); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER audit_after_trigger + AFTER INSERT ON test_trigger + FOR EACH ROW EXECUTE FUNCTION audit_trigger_func(); +INSERT INTO test_trigger VALUES (1, 'test1'); +INSERT INTO test_trigger VALUES (2, 'test2'); +SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + toplevel | calls | rows | plans | query +----------+-------+------+-------+----------------------------------------------------- + f | 2 | 2 | 0 | INSERT INTO audit_table VALUES ($15, TG_OP, NEW.id) + t | 2 | 2 | 0 | INSERT INTO test_trigger VALUES ($1, $2) + t | 1 | 1 | 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t +(3 rows) +DROP TRIGGER audit_after_trigger ON test_trigger; +DROP FUNCTION audit_trigger_func(); +DROP TABLE audit_table, test_trigger; -- -- pg_stat_statements.track = none -- diff --git a/contrib/pg_stat_statements/expected/oldextversions.out b/contrib/pg_stat_statements/expected/oldextversions.out index de679b19711ab..726383a99d7c1 100644 --- a/contrib/pg_stat_statements/expected/oldextversions.out +++ b/contrib/pg_stat_statements/expected/oldextversions.out @@ -407,4 +407,71 @@ SELECT count(*) > 0 AS has_data FROM pg_stat_statements; t (1 row) +-- New functions and views for pg_stat_statements in 1.13 +AlTER EXTENSION pg_stat_statements UPDATE TO '1.13'; +\d pg_stat_statements + View "public.pg_stat_statements" + Column | Type | Collation | Nullable | Default +----------------------------+--------------------------+-----------+----------+--------- + userid | oid | | | + dbid | oid | | | + toplevel | boolean | | | + queryid | bigint | | | + query | text | | | + plans | bigint | | | + total_plan_time | double precision | | | + min_plan_time | double precision | | | + max_plan_time | double precision | | | + mean_plan_time | double precision | | | + stddev_plan_time | double precision | | | + calls | bigint | | | + total_exec_time | double precision | | | + min_exec_time | double precision | | | + max_exec_time | double precision | | | + mean_exec_time | double precision | | | + stddev_exec_time | double precision | | | + rows | bigint | | | + shared_blks_hit | bigint | | | + shared_blks_read | bigint | | | + shared_blks_dirtied | bigint | | | + shared_blks_written | bigint | | | + local_blks_hit | bigint | | | + local_blks_read | bigint | | | + local_blks_dirtied | bigint | | | + local_blks_written | bigint | | | + temp_blks_read | bigint | | | + temp_blks_written | bigint | | | + shared_blk_read_time | double precision | | | + shared_blk_write_time | double precision | | | + local_blk_read_time | double precision | | | + local_blk_write_time | double precision | | | + temp_blk_read_time | double precision | | | + temp_blk_write_time | double precision | | | + wal_records | bigint | | | + wal_fpi | bigint | | | + wal_bytes | numeric | | | + wal_buffers_full | bigint | | | + jit_functions | bigint | | | + jit_generation_time | double precision | | | + jit_inlining_count | bigint | | | + jit_inlining_time | double precision | | | + jit_optimization_count | bigint | | | + jit_optimization_time | double precision | | | + jit_emission_count | bigint | | | + jit_emission_time | double precision | | | + jit_deform_count | bigint | | | + jit_deform_time | double precision | | | + parallel_workers_to_launch | bigint | | | + parallel_workers_launched | bigint | | | + generic_plan_calls | bigint | | | + custom_plan_calls | bigint | | | + stats_since | timestamp with time zone | | | + minmax_stats_since | timestamp with time zone | | | + +SELECT count(*) > 0 AS has_data FROM pg_stat_statements; + has_data +---------- + t +(1 row) + DROP EXTENSION pg_stat_statements; diff --git a/contrib/pg_stat_statements/expected/plancache.out b/contrib/pg_stat_statements/expected/plancache.out new file mode 100644 index 0000000000000..32bf913b28612 --- /dev/null +++ b/contrib/pg_stat_statements/expected/plancache.out @@ -0,0 +1,222 @@ +-- +-- Tests with plan cache +-- +-- Setup +CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$ +DECLARE + ret INT; +BEGIN + SELECT $1 INTO ret; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$ +DECLARE + ret INT; +BEGIN + SELECT $1 INTO ret; +END; +$$ LANGUAGE plpgsql; +-- Prepared statements +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +PREPARE p1 AS SELECT $1 AS a; +SET plan_cache_mode TO force_generic_plan; +EXECUTE p1(1); + a +--- + 1 +(1 row) + +SET plan_cache_mode TO force_custom_plan; +EXECUTE p1(1); + a +--- + 1 +(1 row) + +SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + calls | generic_plan_calls | custom_plan_calls | query +-------+--------------------+-------------------+---------------------------------------------------- + 2 | 1 | 1 | PREPARE p1 AS SELECT $1 AS a + 1 | 0 | 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t + 2 | 0 | 0 | SET plan_cache_mode TO $1 +(3 rows) + +DEALLOCATE p1; +-- Extended query protocol +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT $1 AS a \parse p1 +SET plan_cache_mode TO force_generic_plan; +\bind_named p1 1 +; + a +--- + 1 +(1 row) + +SET plan_cache_mode TO force_custom_plan; +\bind_named p1 1 +; + a +--- + 1 +(1 row) + +SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + calls | generic_plan_calls | custom_plan_calls | query +-------+--------------------+-------------------+---------------------------------------------------- + 2 | 1 | 1 | SELECT $1 AS a + 1 | 0 | 0 | SELECT pg_stat_statements_reset() IS NOT NULL AS t + 2 | 0 | 0 | SET plan_cache_mode TO $1 +(3 rows) + +\close_prepared p1 +-- EXPLAIN [ANALYZE] EXECUTE +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +PREPARE p1 AS SELECT $1; +SET plan_cache_mode TO force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE p1(1); + QUERY PLAN +------------ + Result +(1 row) + +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1); + QUERY PLAN +----------------------------------- + Result (actual rows=1.00 loops=1) +(1 row) + +SET plan_cache_mode TO force_custom_plan; +EXPLAIN (COSTS OFF) EXECUTE p1(1); + QUERY PLAN +------------ + Result +(1 row) + +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1); + QUERY PLAN +----------------------------------- + Result (actual rows=1.00 loops=1) +(1 row) + +SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + calls | generic_plan_calls | custom_plan_calls | toplevel | query +-------+--------------------+-------------------+----------+---------------------------------------------------------------------------------- + 2 | 0 | 0 | t | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1) + 2 | 0 | 0 | t | EXPLAIN (COSTS OFF) EXECUTE p1(1) + 4 | 2 | 2 | f | PREPARE p1 AS SELECT $1 + 1 | 0 | 0 | t | SELECT pg_stat_statements_reset() IS NOT NULL AS t + 2 | 0 | 0 | t | SET plan_cache_mode TO $1 +(5 rows) + +RESET pg_stat_statements.track; +DEALLOCATE p1; +-- Functions/procedures +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SET plan_cache_mode TO force_generic_plan; +SELECT select_one_func(1); + select_one_func +----------------- + +(1 row) + +CALL select_one_proc(1); +SET plan_cache_mode TO force_custom_plan; +SELECT select_one_func(1); + select_one_func +----------------- + +(1 row) + +CALL select_one_proc(1); +SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + calls | generic_plan_calls | custom_plan_calls | toplevel | query +-------+--------------------+-------------------+----------+---------------------------------------------------- + 2 | 0 | 0 | t | CALL select_one_proc($1) + 1 | 0 | 0 | t | SELECT pg_stat_statements_reset() IS NOT NULL AS t + 2 | 0 | 0 | t | SELECT select_one_func($1) + 2 | 0 | 0 | t | SET plan_cache_mode TO $1 +(4 rows) + +-- +-- EXPLAIN [ANALYZE] EXECUTE + functions/procedures +-- +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SET plan_cache_mode TO force_generic_plan; +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1); + QUERY PLAN +----------------------------------- + Result (actual rows=1.00 loops=1) +(1 row) + +EXPLAIN (COSTS OFF) SELECT select_one_func(1); + QUERY PLAN +------------ + Result +(1 row) + +CALL select_one_proc(1); +SET plan_cache_mode TO force_custom_plan; +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1); + QUERY PLAN +----------------------------------- + Result (actual rows=1.00 loops=1) +(1 row) + +EXPLAIN (COSTS OFF) SELECT select_one_func(1); + QUERY PLAN +------------ + Result +(1 row) + +CALL select_one_proc(1); +SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements + ORDER BY query COLLATE "C", toplevel; + calls | generic_plan_calls | custom_plan_calls | toplevel | query +-------+--------------------+-------------------+----------+------------------------------------------------------------------------------------------------ + 2 | 0 | 0 | t | CALL select_one_proc($1) + 2 | 0 | 0 | t | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1) + 4 | 0 | 0 | f | EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func($1); + 2 | 0 | 0 | t | EXPLAIN (COSTS OFF) SELECT select_one_func($1) + 1 | 0 | 0 | t | SELECT pg_stat_statements_reset() IS NOT NULL AS t + 2 | 0 | 0 | t | SET plan_cache_mode TO $1 +(6 rows) + +RESET pg_stat_statements.track; +-- +-- Cleanup +-- +DROP FUNCTION select_one_func(int); +DROP PROCEDURE select_one_proc(int); diff --git a/contrib/pg_stat_statements/expected/planning.out b/contrib/pg_stat_statements/expected/planning.out index 3ee1928cbe94a..9effd11fdc859 100644 --- a/contrib/pg_stat_statements/expected/planning.out +++ b/contrib/pg_stat_statements/expected/planning.out @@ -58,7 +58,7 @@ SELECT 42; (1 row) SELECT plans, calls, rows, query FROM pg_stat_statements - WHERE query NOT LIKE 'SELECT COUNT%' ORDER BY query COLLATE "C"; + WHERE query NOT LIKE 'PREPARE%' ORDER BY query COLLATE "C"; plans | calls | rows | query -------+-------+------+---------------------------------------------------------- 0 | 1 | 0 | ALTER TABLE stats_plan_test ADD COLUMN x int @@ -72,10 +72,10 @@ SELECT plans, calls, rows, query FROM pg_stat_statements -- for the prepared statement we expect at least one replan, but cache -- invalidations could force more SELECT plans >= 2 AND plans <= calls AS plans_ok, calls, rows, query FROM pg_stat_statements - WHERE query LIKE 'SELECT COUNT%' ORDER BY query COLLATE "C"; - plans_ok | calls | rows | query -----------+-------+------+-------------------------------------- - t | 4 | 4 | SELECT COUNT(*) FROM stats_plan_test + WHERE query LIKE 'PREPARE%' ORDER BY query COLLATE "C"; + plans_ok | calls | rows | query +----------+-------+------+------------------------------------------------------- + t | 4 | 4 | PREPARE prep1 AS SELECT COUNT(*) FROM stats_plan_test (1 row) -- Cleanup diff --git a/contrib/pg_stat_statements/expected/select.out b/contrib/pg_stat_statements/expected/select.out index 09476a7b699e9..a069119c7900d 100644 --- a/contrib/pg_stat_statements/expected/select.out +++ b/contrib/pg_stat_statements/expected/select.out @@ -208,6 +208,7 @@ DEALLOCATE pgss_test; SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; calls | rows | query -------+------+------------------------------------------------------------------------------ + 1 | 1 | PREPARE pgss_test (int) AS SELECT $1, $2 LIMIT $3 4 | 4 | SELECT $1 + | | -- but this one will appear + | | AS "text" @@ -221,7 +222,6 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; 2 | 2 | SELECT $1 AS "int" ORDER BY 1 1 | 2 | SELECT $1 AS i UNION SELECT $2 ORDER BY i 1 | 1 | SELECT $1 || $2 - 1 | 1 | SELECT $1, $2 LIMIT $3 2 | 2 | SELECT DISTINCT $1 AS "int" 0 | 0 | SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C" 1 | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t @@ -238,6 +238,65 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t; t (1 row) +-- normalization of constants and parameters, with constant locations +-- recorded one or more times. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT WHERE '1' IN ('1'::int, '3'::int::text); +-- +(1 row) + +SELECT WHERE (1, 2) IN ((1, 2), (2, 3)); +-- +(1 row) + +SELECT WHERE (3, 4) IN ((5, 6), (8, 7)); +-- +(0 rows) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +------------------------------------------------------------------------+------- + SELECT WHERE $1 IN ($2::int, $3::int::text) | 1 + SELECT WHERE ($1, $2) IN (($3, $4), ($5, $6)) | 2 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C" | 0 +(4 rows) + +-- with the last element being an explicit function call with an argument, ensure +-- the normalization of the squashing interval is correct. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT WHERE 1 IN (1, int4(1), int4(2)); +-- +(1 row) + +SELECT WHERE 1 = ANY (ARRAY[1, int4(1), int4(2)]); +-- +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +------------------------------------------------------------------------+------- + SELECT WHERE $1 IN ($2 /*, ... */) | 2 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C" | 0 +(3 rows) + -- -- queries with locking clauses -- @@ -400,6 +459,102 @@ SELECT COUNT(*) FROM pg_stat_statements WHERE query LIKE '%FETCH FIRST%'; 2 (1 row) +-- GROUP BY, HAVING, GROUPING +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a; + count +------- + 1 +(1 row) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b; + count +------- + 1 +(1 row) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a, b; + count +------- + 1 +(1 row) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b, a; + count +------- + 1 +(1 row) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY GROUPING SETS(a, ()); + count +------- + 1 + 1 +(2 rows) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY GROUPING SETS(b, ()); + count +------- + 1 + 1 +(2 rows) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a HAVING a = 1; + count +------- + 1 +(1 row) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a HAVING a = 2; + count +------- +(0 rows) + +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b HAVING b = 1; + count +------- +(0 rows) + +SELECT GROUPING(a) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a; + grouping +---------- + 0 +(1 row) + +SELECT GROUPING(b) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b; + grouping +---------- + 0 +(1 row) + +SELECT GROUPING(b) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a, b; + grouping +---------- + 0 +(1 row) + +SELECT GROUPING(b) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b, a; + grouping +---------- + 0 +(1 row) + +SELECT calls, query FROM pg_stat_statements WHERE query LIKE '%GROUP BY%' ORDER BY query COLLATE "C"; + calls | query +-------+------------------------------------------------------------------------------------------- + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY GROUPING SETS(a, ()) + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY GROUPING SETS(b, ()) + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY a + 2 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY a HAVING a = $3 + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY a, b + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY b + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY b HAVING b = $3 + 1 | SELECT COUNT(*) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY b, a + 1 | SELECT GROUPING(a) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY a + 1 | SELECT GROUPING(b) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY a, b + 1 | SELECT GROUPING(b) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY b + 1 | SELECT GROUPING(b) FROM (VALUES ($1::INT, $2::INT)) AS t(a, b) GROUP BY b, a +(12 rows) + -- GROUP BY [DISTINCT] SELECT a, b, c FROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c) @@ -489,7 +644,7 @@ SELECT ( SELECT COUNT(*) FROM pg_stat_statements WHERE query LIKE '%SELECT GROUPING%'; count ------- - 2 + 6 (1 row) SELECT pg_stat_statements_reset() IS NOT NULL AS t; diff --git a/contrib/pg_stat_statements/expected/squashing.out b/contrib/pg_stat_statements/expected/squashing.out index 7b138af098c9f..8438235a2ce0e 100644 --- a/contrib/pg_stat_statements/expected/squashing.out +++ b/contrib/pg_stat_statements/expected/squashing.out @@ -1,10 +1,11 @@ -- -- Const squashing functionality -- -CREATE EXTENSION pg_stat_statements; +-- +-- Simple Lists +-- CREATE TABLE test_squash (id int, data int); --- IN queries --- Normal scenario, too many simple constants for an IN query +-- single element will not be squashed SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- @@ -16,42 +17,150 @@ SELECT * FROM test_squash WHERE id IN (1); ----+------ (0 rows) +SELECT ARRAY[1]; + array +------- + {1} +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------+------- + SELECT * FROM test_squash WHERE id IN ($1) | 1 + SELECT ARRAY[$1] | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) + +-- more than 1 element in a list will be squashed +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + SELECT * FROM test_squash WHERE id IN (1, 2, 3); id | data ----+------ (0 rows) +SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5); + id | data +----+------ +(0 rows) + +SELECT ARRAY[1, 2, 3]; + array +--------- + {1,2,3} +(1 row) + +SELECT ARRAY[1, 2, 3, 4]; + array +----------- + {1,2,3,4} +(1 row) + +SELECT ARRAY[1, 2, 3, 4, 5]; + array +------------- + {1,2,3,4,5} +(1 row) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls -------------------------------------------------------+------- - SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) | 1 - SELECT * FROM test_squash WHERE id IN ($1) | 1 + SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) | 3 + SELECT ARRAY[$1 /*, ... */] | 3 SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (3 rows) -SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9); +-- built-in functions will be squashed +-- the IN and ARRAY forms of this statement will have the same queryId +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT WHERE 1 IN (1, int4(1), int4(2), 2); +-- +(1 row) + +SELECT WHERE 1 = ANY (ARRAY[1, int4(1), int4(2), 2]); +-- +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------+------- + SELECT WHERE $1 IN ($2 /*, ... */) | 2 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(2 rows) + +-- external parameters will be squashed +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT * FROM test_squash WHERE id IN ($1, $2, $3, $4, $5) \bind 1 2 3 4 5 +; id | data ----+------ (0 rows) -SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); +SELECT * FROM test_squash WHERE id::text = ANY(ARRAY[$1, $2, $3, $4, $5]) \bind 1 2 3 4 5 +; id | data ----+------ (0 rows) -SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------------------------+------- + SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) | 1 + SELECT * FROM test_squash WHERE id::text = ANY(ARRAY[$1 /*, ... */]) | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) + +-- prepared statements will also be squashed +-- the IN and ARRAY forms of this statement will have the same queryId +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +PREPARE p1(int, int, int, int, int) AS +SELECT * FROM test_squash WHERE id IN ($1, $2, $3, $4, $5); +EXECUTE p1(1, 2, 3, 4, 5); + id | data +----+------ +(0 rows) + +DEALLOCATE p1; +PREPARE p1(int, int, int, int, int) AS +SELECT * FROM test_squash WHERE id = ANY(ARRAY[$1, $2, $3, $4, $5]); +EXECUTE p1(1, 2, 3, 4, 5); id | data ----+------ (0 rows) +DEALLOCATE p1; SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - query | calls -------------------------------------------------------------------------+------- - SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) | 4 - SELECT * FROM test_squash WHERE id IN ($1) | 1 - SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 - SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C" | 1 -(4 rows) + query | calls +-------------------------------------------------------+------- + DEALLOCATE $1 | 2 + PREPARE p1(int, int, int, int, int) AS +| 2 + SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) | + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) -- More conditions in the query SELECT pg_stat_statements_reset() IS NOT NULL AS t; @@ -75,10 +184,25 @@ SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) AND da ----+------ (0 rows) +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9]) AND data = 2; + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) AND data = 2; + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) AND data = 2; + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls ---------------------------------------------------------------------+------- - SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) AND data = $2 | 3 + SELECT * FROM test_squash WHERE id IN ($1 /*, ... */) AND data = $2 | 6 SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) @@ -107,24 +231,46 @@ SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) ----+------ (0 rows) +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9]) + AND data = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9]); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + AND data = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + AND data = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls -------------------------------------------------------+------- - SELECT * FROM test_squash WHERE id IN ($1 /*, ... */)+| 3 + SELECT * FROM test_squash WHERE id IN ($1 /*, ... */)+| 6 AND data IN ($2 /*, ... */) | SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) --- No constants simplification for OpExpr SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- t (1 row) --- In the following two queries the operator expressions (+) and (@) have --- different oppno, and will be given different query_id if squashed, even though --- the normalized query will be the same +-- No constants squashing for OpExpr +-- The IN and ARRAY forms of this statement will have the same queryId +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + SELECT * FROM test_squash WHERE id IN (1 + 1, 2 + 2, 3 + 3, 4 + 4, 5 + 5, 6 + 6, 7 + 7, 8 + 8, 9 + 9); id | data @@ -137,19 +283,35 @@ SELECT * FROM test_squash WHERE id IN ----+------ (0 rows) +SELECT * FROM test_squash WHERE id = ANY(ARRAY + [1 + 1, 2 + 2, 3 + 3, 4 + 4, 5 + 5, 6 + 6, 7 + 7, 8 + 8, 9 + 9]); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY(ARRAY + [@ '-1', @ '-2', @ '-3', @ '-4', @ '-5', @ '-6', @ '-7', @ '-8', @ '-9']); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls ----------------------------------------------------------------------------------------------------+------- - SELECT * FROM test_squash WHERE id IN +| 1 + SELECT * FROM test_squash WHERE id IN +| 2 ($1 + $2, $3 + $4, $5 + $6, $7 + $8, $9 + $10, $11 + $12, $13 + $14, $15 + $16, $17 + $18) | - SELECT * FROM test_squash WHERE id IN +| 1 + SELECT * FROM test_squash WHERE id IN +| 2 (@ $1, @ $2, @ $3, @ $4, @ $5, @ $6, @ $7, @ $8, @ $9) | SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (3 rows) +-- -- FuncExpr +-- -- Verify multiple type representation end up with the same query_id CREATE TABLE test_float (data float); +-- The casted ARRAY expressions will have the same queryId as the IN clause +-- form of the query SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- @@ -181,12 +343,38 @@ SELECT data FROM test_float WHERE data IN (1.0, 1.0); ------ (0 rows) +SELECT data FROM test_float WHERE data = ANY(ARRAY['1'::double precision, '2'::double precision]); + data +------ +(0 rows) + +SELECT data FROM test_float WHERE data = ANY(ARRAY[1.0::double precision, 1.0::double precision]); + data +------ +(0 rows) + +SELECT data FROM test_float WHERE data = ANY(ARRAY[1, 2]); + data +------ +(0 rows) + +SELECT data FROM test_float WHERE data = ANY(ARRAY[1, '2']); + data +------ +(0 rows) + +SELECT data FROM test_float WHERE data = ANY(ARRAY['1', 2]); + data +------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - query | calls ------------------------------------------------------------+------- - SELECT data FROM test_float WHERE data IN ($1 /*, ... */) | 5 - SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 -(2 rows) + query | calls +--------------------------------------------------------------------+------- + SELECT data FROM test_float WHERE data = ANY(ARRAY[$1 /*, ... */]) | 3 + SELECT data FROM test_float WHERE data IN ($1 /*, ... */) | 7 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) -- Numeric type, implicit cast is squashed CREATE TABLE test_squash_numeric (id int, data numeric(5, 2)); @@ -201,12 +389,18 @@ SELECT * FROM test_squash_numeric WHERE data IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ----+------ (0 rows) +SELECT * FROM test_squash_numeric WHERE data = ANY(ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - query | calls ------------------------------------------------------------------+------- - SELECT * FROM test_squash_numeric WHERE data IN ($1 /*, ... */) | 1 - SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 -(2 rows) + query | calls +--------------------------------------------------------------------------+------- + SELECT * FROM test_squash_numeric WHERE data = ANY(ARRAY[$1 /*, ... */]) | 1 + SELECT * FROM test_squash_numeric WHERE data IN ($1 /*, ... */) | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) -- Bigint, implicit cast is squashed CREATE TABLE test_squash_bigint (id int, data bigint); @@ -221,14 +415,20 @@ SELECT * FROM test_squash_bigint WHERE data IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1 ----+------ (0 rows) +SELECT * FROM test_squash_bigint WHERE data = ANY(ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - query | calls -----------------------------------------------------------------+------- - SELECT * FROM test_squash_bigint WHERE data IN ($1 /*, ... */) | 1 - SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 -(2 rows) + query | calls +-------------------------------------------------------------------------+------- + SELECT * FROM test_squash_bigint WHERE data = ANY(ARRAY[$1 /*, ... */]) | 1 + SELECT * FROM test_squash_bigint WHERE data IN ($1 /*, ... */) | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) --- Bigint, explicit cast is not squashed +-- Bigint, explicit cast is squashed SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- @@ -242,15 +442,22 @@ SELECT * FROM test_squash_bigint WHERE data IN ----+------ (0 rows) +SELECT * FROM test_squash_bigint WHERE data = ANY(ARRAY[ + 1::bigint, 2::bigint, 3::bigint, 4::bigint, 5::bigint, 6::bigint, + 7::bigint, 8::bigint, 9::bigint, 10::bigint, 11::bigint]); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls ----------------------------------------------------+------- - SELECT * FROM test_squash_bigint WHERE data IN +| 1 - ($1 /*, ... */::bigint) | + SELECT * FROM test_squash_bigint WHERE data IN +| 2 + ($1 /*, ... */) | SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) --- Bigint, long tokens with parenthesis +-- Bigint, long tokens with parenthesis, will not squash SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- @@ -264,44 +471,47 @@ SELECT * FROM test_squash_bigint WHERE id IN ----+------ (0 rows) +SELECT * FROM test_squash_bigint WHERE id = ANY(ARRAY[ + abs(100), abs(200), abs(300), abs(400), abs(500), abs(600), abs(700), + abs(800), abs(900), abs(1000), ((abs(1100)))]); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls -------------------------------------------------------------------------+------- - SELECT * FROM test_squash_bigint WHERE id IN +| 1 + SELECT * FROM test_squash_bigint WHERE id IN +| 2 (abs($1), abs($2), abs($3), abs($4), abs($5), abs($6), abs($7),+| abs($8), abs($9), abs($10), ((abs($11)))) | SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) --- CoerceViaIO, SubLink instead of a Const -CREATE TABLE test_squash_jsonb (id int, data jsonb); +-- Multiple FuncExpr's. Will not squash SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- t (1 row) -SELECT * FROM test_squash_jsonb WHERE data IN - ((SELECT '"1"')::jsonb, (SELECT '"2"')::jsonb, (SELECT '"3"')::jsonb, - (SELECT '"4"')::jsonb, (SELECT '"5"')::jsonb, (SELECT '"6"')::jsonb, - (SELECT '"7"')::jsonb, (SELECT '"8"')::jsonb, (SELECT '"9"')::jsonb, - (SELECT '"10"')::jsonb); - id | data -----+------ -(0 rows) +SELECT WHERE 1 IN (1::int::bigint::int, 2::int::bigint::int); +-- +(1 row) + +SELECT WHERE 1 = ANY(ARRAY[1::int::bigint::int, 2::int::bigint::int]); +-- +(1 row) SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - query | calls -----------------------------------------------------------------------+------- - SELECT * FROM test_squash_jsonb WHERE data IN +| 1 - ((SELECT $1)::jsonb, (SELECT $2)::jsonb, (SELECT $3)::jsonb,+| - (SELECT $4)::jsonb, (SELECT $5)::jsonb, (SELECT $6)::jsonb,+| - (SELECT $7)::jsonb, (SELECT $8)::jsonb, (SELECT $9)::jsonb,+| - (SELECT $10)::jsonb) | - SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 + query | calls +----------------------------------------------------+------- + SELECT WHERE $1 IN ($2 /*, ... */) | 2 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) +-- -- CoerceViaIO +-- -- Create some dummy type to force CoerceViaIO CREATE TYPE casttesttype; CREATE FUNCTION casttesttype_in(cstring) @@ -349,15 +559,25 @@ SELECT * FROM test_squash_cast WHERE data IN ----+------ (0 rows) +SELECT * FROM test_squash_cast WHERE data = ANY (ARRAY + [1::int4::casttesttype, 2::int4::casttesttype, 3::int4::casttesttype, + 4::int4::casttesttype, 5::int4::casttesttype, 6::int4::casttesttype, + 7::int4::casttesttype, 8::int4::casttesttype, 9::int4::casttesttype, + 10::int4::casttesttype, 11::int4::casttesttype]); + id | data +----+------ +(0 rows) + SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls ----------------------------------------------------+------- - SELECT * FROM test_squash_cast WHERE data IN +| 1 - ($1 /*, ... */::int4::casttesttype) | + SELECT * FROM test_squash_cast WHERE data IN +| 2 + ($1 /*, ... */) | SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) -- Some casting expression are simplified to Const +CREATE TABLE test_squash_jsonb (id int, data jsonb); SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- @@ -366,8 +586,16 @@ SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash_jsonb WHERE data IN (('"1"')::jsonb, ('"2"')::jsonb, ('"3"')::jsonb, ('"4"')::jsonb, - ( '"5"')::jsonb, ( '"6"')::jsonb, ( '"7"')::jsonb, ( '"8"')::jsonb, - ( '"9"')::jsonb, ( '"10"')::jsonb); + ('"5"')::jsonb, ('"6"')::jsonb, ('"7"')::jsonb, ('"8"')::jsonb, + ('"9"')::jsonb, ('"10"')::jsonb); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash_jsonb WHERE data = ANY (ARRAY + [('"1"')::jsonb, ('"2"')::jsonb, ('"3"')::jsonb, ('"4"')::jsonb, + ('"5"')::jsonb, ('"6"')::jsonb, ('"7"')::jsonb, ('"8"')::jsonb, + ('"9"')::jsonb, ('"10"')::jsonb]); id | data ----+------ (0 rows) @@ -375,28 +603,152 @@ SELECT * FROM test_squash_jsonb WHERE data IN SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls ----------------------------------------------------+------- - SELECT * FROM test_squash_jsonb WHERE data IN +| 1 - (($1 /*, ... */)::jsonb) | + SELECT * FROM test_squash_jsonb WHERE data IN +| 2 + ($1 /*, ... */) | SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) +-- CoerceViaIO, SubLink instead of a Const. Will not squash +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT * FROM test_squash_jsonb WHERE data IN + ((SELECT '"1"')::jsonb, (SELECT '"2"')::jsonb, (SELECT '"3"')::jsonb, + (SELECT '"4"')::jsonb, (SELECT '"5"')::jsonb, (SELECT '"6"')::jsonb, + (SELECT '"7"')::jsonb, (SELECT '"8"')::jsonb, (SELECT '"9"')::jsonb, + (SELECT '"10"')::jsonb); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash_jsonb WHERE data = ANY(ARRAY + [(SELECT '"1"')::jsonb, (SELECT '"2"')::jsonb, (SELECT '"3"')::jsonb, + (SELECT '"4"')::jsonb, (SELECT '"5"')::jsonb, (SELECT '"6"')::jsonb, + (SELECT '"7"')::jsonb, (SELECT '"8"')::jsonb, (SELECT '"9"')::jsonb, + (SELECT '"10"')::jsonb]); + id | data +----+------ +(0 rows) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------------------------+------- + SELECT * FROM test_squash_jsonb WHERE data IN +| 2 + ((SELECT $1)::jsonb, (SELECT $2)::jsonb, (SELECT $3)::jsonb,+| + (SELECT $4)::jsonb, (SELECT $5)::jsonb, (SELECT $6)::jsonb,+| + (SELECT $7)::jsonb, (SELECT $8)::jsonb, (SELECT $9)::jsonb,+| + (SELECT $10)::jsonb) | + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(2 rows) + +-- Multiple CoerceViaIO are squashed +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT WHERE 1 IN (1::text::int::text::int, 1::text::int::text::int); +-- +(1 row) + +SELECT WHERE 1 = ANY(ARRAY[1::text::int::text::int, 1::text::int::text::int]); +-- +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------+------- + SELECT WHERE $1 IN ($2 /*, ... */) | 2 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(2 rows) + +-- -- RelabelType +-- SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- t (1 row) -SELECT * FROM test_squash WHERE id IN (1::oid, 2::oid, 3::oid, 4::oid, 5::oid, 6::oid, 7::oid, 8::oid, 9::oid); +-- However many layers of RelabelType there are, the list will be squashable. +SELECT * FROM test_squash WHERE id IN + (1::oid, 2::oid, 3::oid, 4::oid, 5::oid, 6::oid, 7::oid, 8::oid, 9::oid); + id | data +----+------ +(0 rows) + +SELECT ARRAY[1::oid, 2::oid, 3::oid, 4::oid, 5::oid, 6::oid, 7::oid, 8::oid, 9::oid]; + array +--------------------- + {1,2,3,4,5,6,7,8,9} +(1 row) + +SELECT * FROM test_squash WHERE id IN (1::oid, 2::oid::int::oid); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY(ARRAY[1::oid, 2::oid::int::oid]); + id | data +----+------ +(0 rows) + +-- RelabelType together with CoerceViaIO is also squashable +SELECT * FROM test_squash WHERE id = ANY(ARRAY[1::oid::text::int::oid, 2::oid::int::oid]); + id | data +----+------ +(0 rows) + +SELECT * FROM test_squash WHERE id = ANY(ARRAY[1::text::int::oid, 2::oid::int::oid]); id | data ----+------ (0 rows) SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - query | calls -------------------------------------------------------------+------- - SELECT * FROM test_squash WHERE id IN ($1 /*, ... */::oid) | 1 - SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 + query | calls +----------------------------------------------------+------- + SELECT * FROM test_squash WHERE id IN +| 5 + ($1 /*, ... */) | + SELECT ARRAY[$1 /*, ... */] | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(3 rows) + +-- +-- edge cases +-- +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- for nested arrays, only constants are squashed +SELECT ARRAY[ + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ]; + array +----------------------------------------------------------------------------------------------- + {{1,2,3,4,5,6,7,8,9,10},{1,2,3,4,5,6,7,8,9,10},{1,2,3,4,5,6,7,8,9,10},{1,2,3,4,5,6,7,8,9,10}} +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------+------- + SELECT ARRAY[ +| 1 + ARRAY[$1 /*, ... */], +| + ARRAY[$2 /*, ... */], +| + ARRAY[$3 /*, ... */], +| + ARRAY[$4 /*, ... */] +| + ] | + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 (2 rows) -- Test constants evaluation in a CTE, which was causing issues in the past @@ -409,23 +761,159 @@ FROM cte; -------- (0 rows) --- Simple array would be squashed as well SELECT pg_stat_statements_reset() IS NOT NULL AS t; t --- t (1 row) -SELECT ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - array ------------------------- - {1,2,3,4,5,6,7,8,9,10} +-- Rewritten as an OpExpr, so it will not be squashed +select where '1' IN ('1'::int, '2'::int::text); +-- +(1 row) + +-- Rewritten as an ArrayExpr, so it will be squashed +select where '1' IN ('1'::int, '2'::int); +-- (1 row) SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; query | calls ----------------------------------------------------+------- - SELECT ARRAY[$1 /*, ... */] | 1 SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 + select where $1 IN ($2 /*, ... */) | 1 + select where $1 IN ($2::int, $3::int::text) | 1 +(3 rows) + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +-- Both of these queries will be rewritten as an ArrayExpr, so they +-- will be squashed, and have a similar queryId +select where '1' IN ('1'::int::text, '2'::int::text); +-- +(1 row) + +select where '1' = ANY (array['1'::int::text, '2'::int::text]); +-- +(1 row) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +----------------------------------------------------+------- + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 + select where $1 IN ($2 /*, ... */) | 2 (2 rows) +-- composite function with row expansion +create table test_composite(x integer); +CREATE FUNCTION composite_f(a integer[], out x integer, out y integer) returns +record as $$ begin + x = a[1]; + y = a[2]; + end; +$$ language plpgsql; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + t +--- + t +(1 row) + +SELECT ((composite_f(array[1, 2]))).* FROM test_composite; + x | y +---+--- +(0 rows) + +SELECT ((composite_f(array[1, 2, 3]))).* FROM test_composite; + x | y +---+--- +(0 rows) + +SELECT ((composite_f(array[1, 2, 3]))).*, 1, 2, 3, ((composite_f(array[1, 2, 3]))).*, 1, 2 +FROM test_composite +WHERE x IN (1, 2, 3); + x | y | ?column? | ?column? | ?column? | x | y | ?column? | ?column? +---+---+----------+----------+----------+---+---+----------+---------- +(0 rows) + +SELECT ((composite_f(array[1, $1, 3]))).*, 1 FROM test_composite \bind 1 +; + x | y | ?column? +---+---+---------- +(0 rows) + +-- ROW() expression with row expansion +SELECT (ROW(ARRAY[1,2])).*; + f1 +------- + {1,2} +(1 row) + +SELECT (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*; + f1 | f2 +-------+--------- + {1,2} | {1,2,3} +(1 row) + +SELECT 1, 2, (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*, 3, 4; + ?column? | ?column? | f1 | f2 | ?column? | ?column? +----------+----------+-------+---------+----------+---------- + 1 | 2 | {1,2} | {1,2,3} | 3 | 4 +(1 row) + +SELECT (ROW(ARRAY[1, 2], ARRAY[1, $1, 3])).*, 1 \bind 1 +; + f1 | f2 | ?column? +-------+---------+---------- + {1,2} | {1,1,3} | 1 +(1 row) + +-- IN and ANY clauses with Vars are not squashed. +SELECT * FROM test_squash a, test_squash b WHERE a.id IN (1, 2, 3, b.id, b.id + 1); + id | data | id | data +----+------+----+------ +(0 rows) + +SELECT * FROM test_squash a, test_squash b WHERE a.id = ANY (array[1, ((b.id + b.id * 2)), 5]); + id | data | id | data +----+------+----+------ +(0 rows) + +SELECT * FROM test_squash a, test_squash b WHERE a.id IN ($1, $2, $3, b.id, b.id + $4) \bind 1 2 3 1 +; + id | data | id | data +----+------+----+------ +(0 rows) + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + query | calls +-------------------------------------------------------------------------------------------------------------+------- + SELECT $1, $2, (ROW(ARRAY[$3 /*, ... */], ARRAY[$4 /*, ... */])).*, $5, $6 | 1 + SELECT ((composite_f(array[$1 /*, ... */]))).* FROM test_composite | 2 + SELECT ((composite_f(array[$1 /*, ... */]))).*, $2 FROM test_composite | 1 + SELECT ((composite_f(array[$1 /*, ... */]))).*, $2, $3, $4, ((composite_f(array[$5 /*, ... */]))).*, $6, $7+| 1 + FROM test_composite +| + WHERE x IN ($8 /*, ... */) | + SELECT (ROW(ARRAY[$1 /*, ... */])).* | 1 + SELECT (ROW(ARRAY[$1 /*, ... */], ARRAY[$2 /*, ... */])).* | 1 + SELECT (ROW(ARRAY[$1 /*, ... */], ARRAY[$2 /*, ... */])).*, $3 | 1 + SELECT * FROM test_squash a, test_squash b WHERE a.id = ANY (array[$1, ((b.id + b.id * $2)), $3]) | 1 + SELECT * FROM test_squash a, test_squash b WHERE a.id IN ($1, $2, $3, b.id, b.id + $4) | 1 + SELECT * FROM test_squash a, test_squash b WHERE a.id IN ($1, $2, $3, b.id, b.id + $4) | 1 + SELECT pg_stat_statements_reset() IS NOT NULL AS t | 1 +(11 rows) + +-- +-- cleanup +-- +DROP TABLE test_squash; +DROP TABLE test_float; +DROP TABLE test_squash_numeric; +DROP TABLE test_squash_bigint; +DROP TABLE test_squash_cast CASCADE; +DROP TABLE test_squash_jsonb; +DROP TABLE test_composite; +DROP FUNCTION composite_f; diff --git a/contrib/pg_stat_statements/expected/utility.out b/contrib/pg_stat_statements/expected/utility.out index aa4f0f7e62805..e4d6564ea5b5a 100644 --- a/contrib/pg_stat_statements/expected/utility.out +++ b/contrib/pg_stat_statements/expected/utility.out @@ -540,7 +540,7 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; -------+------+---------------------------------------------------- 2 | 0 | DEALLOCATE $1 2 | 0 | DEALLOCATE ALL - 2 | 2 | SELECT $1 AS a + 2 | 2 | PREPARE stat_select AS SELECT $1 AS a 1 | 1 | SELECT $1 as a 1 | 1 | SELECT pg_stat_statements_reset() IS NOT NULL AS t (5 rows) @@ -702,7 +702,7 @@ SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; 1 | 13 | CREATE MATERIALIZED VIEW pgss_matv AS SELECT * FROM pgss_ctas 1 | 10 | CREATE TABLE pgss_ctas AS SELECT a, $1 b FROM generate_series($2, $3) a 1 | 0 | DECLARE pgss_cursor CURSOR FOR SELECT * FROM pgss_matv - 1 | 5 | FETCH FORWARD 5 pgss_cursor + 1 | 5 | FETCH FORWARD $1 pgss_cursor 1 | 7 | FETCH FORWARD ALL pgss_cursor 1 | 1 | FETCH NEXT pgss_cursor 1 | 13 | REFRESH MATERIALIZED VIEW pgss_matv diff --git a/contrib/pg_stat_statements/meson.build b/contrib/pg_stat_statements/meson.build index 01a6cbdcf6139..9d78cb88b7d78 100644 --- a/contrib/pg_stat_statements/meson.build +++ b/contrib/pg_stat_statements/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_stat_statements_sources = files( 'pg_stat_statements.c', @@ -21,6 +21,7 @@ contrib_targets += pg_stat_statements install_data( 'pg_stat_statements.control', 'pg_stat_statements--1.4.sql', + 'pg_stat_statements--1.12--1.13.sql', 'pg_stat_statements--1.11--1.12.sql', 'pg_stat_statements--1.10--1.11.sql', 'pg_stat_statements--1.9--1.10.sql', @@ -36,6 +37,7 @@ install_data( kwargs: contrib_data_args, ) +# Note: Test "cleanup" is kept second to last, removing the extension. tests += { 'name': 'pg_stat_statements', 'sd': meson.current_source_dir(), @@ -54,9 +56,10 @@ tests += { 'privileges', 'extended', 'parallel', + 'plancache', + 'squashing', 'cleanup', 'oldextversions', - 'squashing', ], 'regress_args': ['--temp-config', files('pg_stat_statements.conf')], # Disabled because these tests require diff --git a/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql new file mode 100644 index 0000000000000..2f0eaf14ec34d --- /dev/null +++ b/contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql @@ -0,0 +1,78 @@ +/* contrib/pg_stat_statements/pg_stat_statements--1.12--1.13.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pg_stat_statements UPDATE TO '1.13'" to load this file. \quit + +/* First we have to remove them from the extension */ +ALTER EXTENSION pg_stat_statements DROP VIEW pg_stat_statements; +ALTER EXTENSION pg_stat_statements DROP FUNCTION pg_stat_statements(boolean); + +/* Then we can drop them */ +DROP VIEW pg_stat_statements; +DROP FUNCTION pg_stat_statements(boolean); + +/* Now redefine */ +CREATE FUNCTION pg_stat_statements(IN showtext boolean, + OUT userid oid, + OUT dbid oid, + OUT toplevel bool, + OUT queryid bigint, + OUT query text, + OUT plans int8, + OUT total_plan_time float8, + OUT min_plan_time float8, + OUT max_plan_time float8, + OUT mean_plan_time float8, + OUT stddev_plan_time float8, + OUT calls int8, + OUT total_exec_time float8, + OUT min_exec_time float8, + OUT max_exec_time float8, + OUT mean_exec_time float8, + OUT stddev_exec_time float8, + OUT rows int8, + OUT shared_blks_hit int8, + OUT shared_blks_read int8, + OUT shared_blks_dirtied int8, + OUT shared_blks_written int8, + OUT local_blks_hit int8, + OUT local_blks_read int8, + OUT local_blks_dirtied int8, + OUT local_blks_written int8, + OUT temp_blks_read int8, + OUT temp_blks_written int8, + OUT shared_blk_read_time float8, + OUT shared_blk_write_time float8, + OUT local_blk_read_time float8, + OUT local_blk_write_time float8, + OUT temp_blk_read_time float8, + OUT temp_blk_write_time float8, + OUT wal_records int8, + OUT wal_fpi int8, + OUT wal_bytes numeric, + OUT wal_buffers_full int8, + OUT jit_functions int8, + OUT jit_generation_time float8, + OUT jit_inlining_count int8, + OUT jit_inlining_time float8, + OUT jit_optimization_count int8, + OUT jit_optimization_time float8, + OUT jit_emission_count int8, + OUT jit_emission_time float8, + OUT jit_deform_count int8, + OUT jit_deform_time float8, + OUT parallel_workers_to_launch int8, + OUT parallel_workers_launched int8, + OUT generic_plan_calls int8, + OUT custom_plan_calls int8, + OUT stats_since timestamp with time zone, + OUT minmax_stats_since timestamp with time zone +) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pg_stat_statements_1_13' +LANGUAGE C STRICT VOLATILE PARALLEL SAFE; + +CREATE VIEW pg_stat_statements AS + SELECT * FROM pg_stat_statements(true); + +GRANT SELECT ON pg_stat_statements TO PUBLIC; diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c index d8fdf42df7935..95a5411a39d95 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.c +++ b/contrib/pg_stat_statements/pg_stat_statements.c @@ -34,7 +34,7 @@ * in the file to be read or written while holding only shared lock. * * - * Copyright (c) 2008-2025, PostgreSQL Global Development Group + * Copyright (c) 2008-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pg_stat_statements/pg_stat_statements.c @@ -47,9 +47,9 @@ #include #include +#include "access/htup_details.h" #include "access/parallel.h" #include "catalog/pg_authid.h" -#include "common/int.h" #include "executor/instrument.h" #include "funcapi.h" #include "jit/jit.h" @@ -58,7 +58,6 @@ #include "nodes/queryjumble.h" #include "optimizer/planner.h" #include "parser/analyze.h" -#include "parser/scanner.h" #include "pgstat.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -70,6 +69,7 @@ #include "utils/builtins.h" #include "utils/memutils.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" PG_MODULE_MAGIC_EXT( .name = "pg_stat_statements", @@ -85,7 +85,7 @@ PG_MODULE_MAGIC_EXT( #define PGSS_TEXT_FILE PG_STAT_TMP_DIR "/pgss_query_texts.stat" /* Magic number identifying the stats file format */ -static const uint32 PGSS_FILE_HEADER = 0x20220408; +static const uint32 PGSS_FILE_HEADER = 0x20250731; /* PostgreSQL major version number, changes in which invalidate all entries */ static const uint32 PGSS_PG_MAJOR_VERSION = PG_VERSION_NUM / 100; @@ -114,6 +114,7 @@ typedef enum pgssVersion PGSS_V1_10, PGSS_V1_11, PGSS_V1_12, + PGSS_V1_13, } pgssVersion; typedef enum pgssStoreKind @@ -138,13 +139,12 @@ typedef enum pgssStoreKind * If you add a new key to this struct, make sure to teach pgss_store() to * zero the padding bytes. Otherwise, things will break, because pgss_hash is * created using HASH_BLOBS, and thus tag_hash is used to hash this. - */ typedef struct pgssHashKey { Oid userid; /* user OID */ Oid dbid; /* database OID */ - uint64 queryid; /* query identifier */ + int64 queryid; /* query identifier */ bool toplevel; /* query executed at top level */ } pgssHashKey; @@ -210,6 +210,8 @@ typedef struct Counters * to be launched */ int64 parallel_workers_launched; /* # of parallel workers actually * launched */ + int64 generic_plan_calls; /* number of calls using a generic plan */ + int64 custom_plan_calls; /* number of calls using a custom plan */ } Counters; /* @@ -245,7 +247,7 @@ typedef struct pgssEntry */ typedef struct pgssSharedState { - LWLock *lock; /* protects hashtable search/modification */ + LWLockPadded lock; /* protects hashtable search/modification */ double cur_median_usage; /* current median usage in hashtable */ Size mean_query_len; /* current mean entry text length */ slock_t mutex; /* protects following fields only: */ @@ -255,14 +257,24 @@ typedef struct pgssSharedState pgssGlobalStats stats; /* global statistics for pgss */ } pgssSharedState; +/* Links to shared memory state */ +static pgssSharedState *pgss; +static HTAB *pgss_hash; + +static void pgss_shmem_request(void *arg); +static void pgss_shmem_init(void *arg); + +static const ShmemCallbacks pgss_shmem_callbacks = { + .request_fn = pgss_shmem_request, + .init_fn = pgss_shmem_init, +}; + /*---- Local variables ----*/ /* Current nesting depth of planner/ExecutorRun/ProcessUtility calls */ static int nesting_level = 0; /* Saved hook values */ -static shmem_request_hook_type prev_shmem_request_hook = NULL; -static shmem_startup_hook_type prev_shmem_startup_hook = NULL; static post_parse_analyze_hook_type prev_post_parse_analyze_hook = NULL; static planner_hook_type prev_planner_hook = NULL; static ExecutorStart_hook_type prev_ExecutorStart = NULL; @@ -271,10 +283,6 @@ static ExecutorFinish_hook_type prev_ExecutorFinish = NULL; static ExecutorEnd_hook_type prev_ExecutorEnd = NULL; static ProcessUtility_hook_type prev_ProcessUtility = NULL; -/* Links to shared memory state */ -static pgssSharedState *pgss = NULL; -static HTAB *pgss_hash = NULL; - /*---- GUC variables ----*/ typedef enum @@ -323,18 +331,18 @@ PG_FUNCTION_INFO_V1(pg_stat_statements_1_9); PG_FUNCTION_INFO_V1(pg_stat_statements_1_10); PG_FUNCTION_INFO_V1(pg_stat_statements_1_11); PG_FUNCTION_INFO_V1(pg_stat_statements_1_12); +PG_FUNCTION_INFO_V1(pg_stat_statements_1_13); PG_FUNCTION_INFO_V1(pg_stat_statements); PG_FUNCTION_INFO_V1(pg_stat_statements_info); -static void pgss_shmem_request(void); -static void pgss_shmem_startup(void); static void pgss_shmem_shutdown(int code, Datum arg); static void pgss_post_parse_analyze(ParseState *pstate, Query *query, - JumbleState *jstate); + const JumbleState *jstate); static PlannedStmt *pgss_planner(Query *parse, const char *query_string, int cursorOptions, - ParamListInfo boundParams); + ParamListInfo boundParams, + ExplainState *es); static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags); static void pgss_ExecutorRun(QueryDesc *queryDesc, ScanDirection direction, @@ -346,20 +354,20 @@ static void pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc); -static void pgss_store(const char *query, uint64 queryId, +static void pgss_store(const char *query, int64 queryId, int query_location, int query_len, pgssStoreKind kind, double total_time, uint64 rows, const BufferUsage *bufusage, const WalUsage *walusage, const struct JitInstrumentation *jitusage, - JumbleState *jstate, + const JumbleState *jstate, int parallel_workers_to_launch, - int parallel_workers_launched); + int parallel_workers_launched, + PlannedStmtOrigin planOrigin); static void pg_stat_statements_internal(FunctionCallInfo fcinfo, pgssVersion api_version, bool showtext); -static Size pgss_memsize(void); static pgssEntry *entry_alloc(pgssHashKey *key, Size query_offset, int query_len, int encoding, bool sticky); static void entry_dealloc(void); @@ -370,13 +378,10 @@ static char *qtext_fetch(Size query_offset, int query_len, char *buffer, Size buffer_size); static bool need_gc_qtexts(void); static void gc_qtexts(void); -static TimestampTz entry_reset(Oid userid, Oid dbid, uint64 queryid, bool minmax_only); -static char *generate_normalized_query(JumbleState *jstate, const char *query, +static TimestampTz entry_reset(Oid userid, Oid dbid, int64 queryid, bool minmax_only); +static char *generate_normalized_query(const JumbleState *jstate, + const char *query, int query_loc, int *query_len_p); -static void fill_in_constant_lengths(JumbleState *jstate, const char *query, - int query_loc); -static int comp_location(const void *a, const void *b); - /* * Module load callback @@ -464,13 +469,14 @@ _PG_init(void) MarkGUCPrefixReserved("pg_stat_statements"); + /* + * Register our shared memory needs. + */ + RegisterShmemCallbacks(&pgss_shmem_callbacks); + /* * Install hooks. */ - prev_shmem_request_hook = shmem_request_hook; - shmem_request_hook = pgss_shmem_request; - prev_shmem_startup_hook = shmem_startup_hook; - shmem_startup_hook = pgss_shmem_startup; prev_post_parse_analyze_hook = post_parse_analyze_hook; post_parse_analyze_hook = pgss_post_parse_analyze; prev_planner_hook = planner_hook; @@ -488,30 +494,42 @@ _PG_init(void) } /* - * shmem_request hook: request additional shared resources. We'll allocate or - * attach to the shared resources in pgss_shmem_startup(). + * shmem request callback: Request shared memory resources. + * + * This is called at postmaster startup. Note that the shared memory isn't + * allocated here yet, this merely register our needs. + * + * In EXEC_BACKEND mode, this is also called in each backend, to re-attach to + * the shared memory area that was already initialized. */ static void -pgss_shmem_request(void) +pgss_shmem_request(void *arg) { - if (prev_shmem_request_hook) - prev_shmem_request_hook(); - - RequestAddinShmemSpace(pgss_memsize()); - RequestNamedLWLockTranche("pg_stat_statements", 1); + ShmemRequestHash(.name = "pg_stat_statements hash", + .nelems = pgss_max, + .hash_info.keysize = sizeof(pgssHashKey), + .hash_info.entrysize = sizeof(pgssEntry), + .hash_flags = HASH_ELEM | HASH_BLOBS, + .ptr = &pgss_hash, + ); + ShmemRequestStruct(.name = "pg_stat_statements", + .size = sizeof(pgssSharedState), + .ptr = (void **) &pgss, + ); } /* - * shmem_startup hook: allocate or attach to shared memory, - * then load any pre-existing statistics from file. - * Also create and load the query-texts file, which is expected to exist - * (even if empty) while the module is enabled. + * shmem init callback: Initialize our shared memory data structures at + * postmaster startup. + * + * Load any pre-existing statistics from file. Also create and load the + * query-texts file, which is expected to exist (even if empty) while the + * module is enabled. */ static void -pgss_shmem_startup(void) +pgss_shmem_init(void *arg) { - bool found; - HASHCTL info; + int tranche_id; FILE *file = NULL; FILE *qfile = NULL; uint32 header; @@ -521,59 +539,38 @@ pgss_shmem_startup(void) int buffer_size; char *buffer = NULL; - if (prev_shmem_startup_hook) - prev_shmem_startup_hook(); - - /* reset in case this is a restart within the postmaster */ - pgss = NULL; - pgss_hash = NULL; - /* - * Create or attach to the shared memory state, including hash table + * We already checked that we're loaded from shared_preload_libraries in + * _PG_init(), so we should not get here after postmaster startup. */ - LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); - - pgss = ShmemInitStruct("pg_stat_statements", - sizeof(pgssSharedState), - &found); - - if (!found) - { - /* First time through ... */ - pgss->lock = &(GetNamedLWLockTranche("pg_stat_statements"))->lock; - pgss->cur_median_usage = ASSUMED_MEDIAN_INIT; - pgss->mean_query_len = ASSUMED_LENGTH_INIT; - SpinLockInit(&pgss->mutex); - pgss->extent = 0; - pgss->n_writers = 0; - pgss->gc_count = 0; - pgss->stats.dealloc = 0; - pgss->stats.stats_reset = GetCurrentTimestamp(); - } - - info.keysize = sizeof(pgssHashKey); - info.entrysize = sizeof(pgssEntry); - pgss_hash = ShmemInitHash("pg_stat_statements hash", - pgss_max, pgss_max, - &info, - HASH_ELEM | HASH_BLOBS); - - LWLockRelease(AddinShmemInitLock); + Assert(!IsUnderPostmaster); /* - * If we're in the postmaster (or a standalone backend...), set up a shmem - * exit hook to dump the statistics to disk. + * Initialize the shmem area with no statistics. */ - if (!IsUnderPostmaster) - on_shmem_exit(pgss_shmem_shutdown, (Datum) 0); + tranche_id = LWLockNewTrancheId("pg_stat_statements"); + LWLockInitialize(&pgss->lock.lock, tranche_id); + pgss->cur_median_usage = ASSUMED_MEDIAN_INIT; + pgss->mean_query_len = ASSUMED_LENGTH_INIT; + SpinLockInit(&pgss->mutex); + pgss->extent = 0; + pgss->n_writers = 0; + pgss->gc_count = 0; + pgss->stats.dealloc = 0; + pgss->stats.stats_reset = GetCurrentTimestamp(); + + /* The hash table must've also been initialized by now */ + Assert(pgss_hash != NULL); /* - * Done if some other process already completed our initialization. + * Set up a shmem exit hook to dump the statistics to disk on postmaster + * (or standalone backend) exit. */ - if (found) - return; + on_shmem_exit(pgss_shmem_shutdown, (Datum) 0); /* + * Load any pre-existing statistics from file. + * * Note: we don't bother with locks here, because there should be no other * processes running when this code is reached. */ @@ -798,7 +795,7 @@ pgss_shmem_shutdown(int code, Datum arg) if (fwrite(&pgss->stats, sizeof(pgssGlobalStats), 1, file) != 1) goto error; - free(qbuffer); + pfree(qbuffer); qbuffer = NULL; if (FreeFile(file)) @@ -822,7 +819,8 @@ pgss_shmem_shutdown(int code, Datum arg) (errcode_for_file_access(), errmsg("could not write file \"%s\": %m", PGSS_DUMP_FILE ".tmp"))); - free(qbuffer); + if (qbuffer) + pfree(qbuffer); if (file) FreeFile(file); unlink(PGSS_DUMP_FILE ".tmp"); @@ -833,7 +831,7 @@ pgss_shmem_shutdown(int code, Datum arg) * Post-parse-analysis hook: mark query with a queryId */ static void -pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) +pgss_post_parse_analyze(ParseState *pstate, Query *query, const JumbleState *jstate) { if (prev_post_parse_analyze_hook) prev_post_parse_analyze_hook(pstate, query, jstate); @@ -852,7 +850,7 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) { if (pgss_track_utility && IsA(query->utilityStmt, ExecuteStmt)) { - query->queryId = UINT64CONST(0); + query->queryId = INT64CONST(0); return; } } @@ -877,7 +875,8 @@ pgss_post_parse_analyze(ParseState *pstate, Query *query, JumbleState *jstate) NULL, jstate, 0, - 0); + 0, + PLAN_STMT_UNKNOWN); } /* @@ -888,7 +887,8 @@ static PlannedStmt * pgss_planner(Query *parse, const char *query_string, int cursorOptions, - ParamListInfo boundParams) + ParamListInfo boundParams, + ExplainState *es) { PlannedStmt *result; @@ -899,7 +899,7 @@ pgss_planner(Query *parse, */ if (pgss_enabled(nesting_level) && pgss_track_planning && query_string - && parse->queryId != UINT64CONST(0)) + && parse->queryId != INT64CONST(0)) { instr_time start; instr_time duration; @@ -923,10 +923,10 @@ pgss_planner(Query *parse, { if (prev_planner_hook) result = prev_planner_hook(parse, query_string, cursorOptions, - boundParams); + boundParams, es); else result = standard_planner(parse, query_string, cursorOptions, - boundParams); + boundParams, es); } PG_FINALLY(); { @@ -957,7 +957,8 @@ pgss_planner(Query *parse, NULL, NULL, 0, - 0); + 0, + result->planOrigin); } else { @@ -971,10 +972,10 @@ pgss_planner(Query *parse, { if (prev_planner_hook) result = prev_planner_hook(parse, query_string, cursorOptions, - boundParams); + boundParams, es); else result = standard_planner(parse, query_string, cursorOptions, - boundParams); + boundParams, es); } PG_FINALLY(); { @@ -992,32 +993,21 @@ pgss_planner(Query *parse, static void pgss_ExecutorStart(QueryDesc *queryDesc, int eflags) { - if (prev_ExecutorStart) - prev_ExecutorStart(queryDesc, eflags); - else - standard_ExecutorStart(queryDesc, eflags); - /* * If query has queryId zero, don't track it. This prevents double * counting of optimizable statements that are directly contained in * utility statements. */ - if (pgss_enabled(nesting_level) && queryDesc->plannedstmt->queryId != UINT64CONST(0)) + if (pgss_enabled(nesting_level) && queryDesc->plannedstmt->queryId != INT64CONST(0)) { - /* - * Set up to track total elapsed time in ExecutorRun. Make sure the - * space is allocated in the per-query context so it will go away at - * ExecutorEnd. - */ - if (queryDesc->totaltime == NULL) - { - MemoryContext oldcxt; - - oldcxt = MemoryContextSwitchTo(queryDesc->estate->es_query_cxt); - queryDesc->totaltime = InstrAlloc(1, INSTRUMENT_ALL, false); - MemoryContextSwitchTo(oldcxt); - } + /* Request all summary instrumentation, i.e. timing, buffers and WAL */ + queryDesc->query_instr_options |= INSTRUMENT_ALL; } + + if (prev_ExecutorStart) + prev_ExecutorStart(queryDesc, eflags); + else + standard_ExecutorStart(queryDesc, eflags); } /* @@ -1068,30 +1058,25 @@ pgss_ExecutorFinish(QueryDesc *queryDesc) static void pgss_ExecutorEnd(QueryDesc *queryDesc) { - uint64 queryId = queryDesc->plannedstmt->queryId; + int64 queryId = queryDesc->plannedstmt->queryId; - if (queryId != UINT64CONST(0) && queryDesc->totaltime && + if (queryId != INT64CONST(0) && queryDesc->query_instr && pgss_enabled(nesting_level)) { - /* - * Make sure stats accumulation is done. (Note: it's okay if several - * levels of hook all do this.) - */ - InstrEndLoop(queryDesc->totaltime); - pgss_store(queryDesc->sourceText, queryId, queryDesc->plannedstmt->stmt_location, queryDesc->plannedstmt->stmt_len, PGSS_EXEC, - queryDesc->totaltime->total * 1000.0, /* convert to msec */ + INSTR_TIME_GET_MILLISEC(queryDesc->query_instr->total), queryDesc->estate->es_total_processed, - &queryDesc->totaltime->bufusage, - &queryDesc->totaltime->walusage, + &queryDesc->query_instr->bufusage, + &queryDesc->query_instr->walusage, queryDesc->estate->es_jit ? &queryDesc->estate->es_jit->instr : NULL, NULL, queryDesc->estate->es_parallel_workers_to_launch, - queryDesc->estate->es_parallel_workers_launched); + queryDesc->estate->es_parallel_workers_launched, + queryDesc->plannedstmt->planOrigin); } if (prev_ExecutorEnd) @@ -1111,7 +1096,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, DestReceiver *dest, QueryCompletion *qc) { Node *parsetree = pstmt->utilityStmt; - uint64 saved_queryId = pstmt->queryId; + int64 saved_queryId = pstmt->queryId; int saved_stmt_location = pstmt->stmt_location; int saved_stmt_len = pstmt->stmt_len; bool enabled = pgss_track_utility && pgss_enabled(nesting_level); @@ -1131,7 +1116,7 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, * only. */ if (enabled) - pstmt->queryId = UINT64CONST(0); + pstmt->queryId = INT64CONST(0); /* * If it's an EXECUTE statement, we don't track it and don't increment the @@ -1224,7 +1209,8 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, NULL, NULL, 0, - 0); + 0, + pstmt->planOrigin); } else { @@ -1278,16 +1264,17 @@ pgss_ProcessUtility(PlannedStmt *pstmt, const char *queryString, * for the arrays in the Counters field. */ static void -pgss_store(const char *query, uint64 queryId, +pgss_store(const char *query, int64 queryId, int query_location, int query_len, pgssStoreKind kind, double total_time, uint64 rows, const BufferUsage *bufusage, const WalUsage *walusage, const struct JitInstrumentation *jitusage, - JumbleState *jstate, + const JumbleState *jstate, int parallel_workers_to_launch, - int parallel_workers_launched) + int parallel_workers_launched, + PlannedStmtOrigin planOrigin) { pgssHashKey key; pgssEntry *entry; @@ -1304,7 +1291,7 @@ pgss_store(const char *query, uint64 queryId, * Nothing to do if compute_query_id isn't enabled and no other module * computed a query identifier. */ - if (queryId == UINT64CONST(0)) + if (queryId == INT64CONST(0)) return; /* @@ -1325,7 +1312,7 @@ pgss_store(const char *query, uint64 queryId, key.toplevel = (nesting_level == 0); /* Lookup the hash table entry with shared lock. */ - LWLockAcquire(pgss->lock, LW_SHARED); + LWLockAcquire(&pgss->lock.lock, LW_SHARED); entry = (pgssEntry *) hash_search(pgss_hash, &key, HASH_FIND, NULL); @@ -1346,11 +1333,11 @@ pgss_store(const char *query, uint64 queryId, */ if (jstate) { - LWLockRelease(pgss->lock); + LWLockRelease(&pgss->lock.lock); norm_query = generate_normalized_query(jstate, query, query_location, &query_len); - LWLockAcquire(pgss->lock, LW_SHARED); + LWLockAcquire(&pgss->lock.lock, LW_SHARED); } /* Append new query text to file with only shared lock held */ @@ -1365,8 +1352,8 @@ pgss_store(const char *query, uint64 queryId, do_gc = need_gc_qtexts(); /* Need exclusive lock to make a new hashtable entry - promote */ - LWLockRelease(pgss->lock); - LWLockAcquire(pgss->lock, LW_EXCLUSIVE); + LWLockRelease(&pgss->lock.lock); + LWLockAcquire(&pgss->lock.lock, LW_EXCLUSIVE); /* * A garbage collection may have occurred while we weren't holding the @@ -1495,11 +1482,17 @@ pgss_store(const char *query, uint64 queryId, entry->counters.parallel_workers_to_launch += parallel_workers_to_launch; entry->counters.parallel_workers_launched += parallel_workers_launched; + /* plan cache counters */ + if (planOrigin == PLAN_STMT_CACHE_GENERIC) + entry->counters.generic_plan_calls++; + else if (planOrigin == PLAN_STMT_CACHE_CUSTOM) + entry->counters.custom_plan_calls++; + SpinLockRelease(&entry->mutex); } done: - LWLockRelease(pgss->lock); + LWLockRelease(&pgss->lock.lock); /* We postpone this clean-up until we're out of the lock */ if (norm_query) @@ -1514,11 +1507,11 @@ pg_stat_statements_reset_1_7(PG_FUNCTION_ARGS) { Oid userid; Oid dbid; - uint64 queryid; + int64 queryid; userid = PG_GETARG_OID(0); dbid = PG_GETARG_OID(1); - queryid = (uint64) PG_GETARG_INT64(2); + queryid = PG_GETARG_INT64(2); entry_reset(userid, dbid, queryid, false); @@ -1530,12 +1523,12 @@ pg_stat_statements_reset_1_11(PG_FUNCTION_ARGS) { Oid userid; Oid dbid; - uint64 queryid; + int64 queryid; bool minmax_only; userid = PG_GETARG_OID(0); dbid = PG_GETARG_OID(1); - queryid = (uint64) PG_GETARG_INT64(2); + queryid = PG_GETARG_INT64(2); minmax_only = PG_GETARG_BOOL(3); PG_RETURN_TIMESTAMPTZ(entry_reset(userid, dbid, queryid, minmax_only)); @@ -1562,7 +1555,8 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS) #define PG_STAT_STATEMENTS_COLS_V1_10 43 #define PG_STAT_STATEMENTS_COLS_V1_11 49 #define PG_STAT_STATEMENTS_COLS_V1_12 52 -#define PG_STAT_STATEMENTS_COLS 52 /* maximum of above */ +#define PG_STAT_STATEMENTS_COLS_V1_13 54 +#define PG_STAT_STATEMENTS_COLS 54 /* maximum of above */ /* * Retrieve statement statistics. @@ -1574,6 +1568,16 @@ pg_stat_statements_reset(PG_FUNCTION_ARGS) * expected API version is identified by embedding it in the C name of the * function. Unfortunately we weren't bright enough to do that for 1.1. */ +Datum +pg_stat_statements_1_13(PG_FUNCTION_ARGS) +{ + bool showtext = PG_GETARG_BOOL(0); + + pg_stat_statements_internal(fcinfo, PGSS_V1_13, showtext); + + return (Datum) 0; +} + Datum pg_stat_statements_1_12(PG_FUNCTION_ARGS) { @@ -1732,6 +1736,10 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, if (api_version != PGSS_V1_12) elog(ERROR, "incorrect number of output arguments"); break; + case PG_STAT_STATEMENTS_COLS_V1_13: + if (api_version != PGSS_V1_13) + elog(ERROR, "incorrect number of output arguments"); + break; default: elog(ERROR, "incorrect number of output arguments"); } @@ -1773,7 +1781,7 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, * we need to partition the hash table to limit the time spent holding any * one lock. */ - LWLockAcquire(pgss->lock, LW_SHARED); + LWLockAcquire(&pgss->lock.lock, LW_SHARED); if (showtext) { @@ -1791,7 +1799,8 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, pgss->extent != extent || pgss->gc_count != gc_count) { - free(qbuffer); + if (qbuffer) + pfree(qbuffer); qbuffer = qtext_load_file(&qbuffer_size); } } @@ -1984,6 +1993,11 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, values[i++] = Int64GetDatumFast(tmp.parallel_workers_to_launch); values[i++] = Int64GetDatumFast(tmp.parallel_workers_launched); } + if (api_version >= PGSS_V1_13) + { + values[i++] = Int64GetDatumFast(tmp.generic_plan_calls); + values[i++] = Int64GetDatumFast(tmp.custom_plan_calls); + } if (api_version >= PGSS_V1_11) { values[i++] = TimestampTzGetDatum(stats_since); @@ -1999,14 +2013,16 @@ pg_stat_statements_internal(FunctionCallInfo fcinfo, api_version == PGSS_V1_10 ? PG_STAT_STATEMENTS_COLS_V1_10 : api_version == PGSS_V1_11 ? PG_STAT_STATEMENTS_COLS_V1_11 : api_version == PGSS_V1_12 ? PG_STAT_STATEMENTS_COLS_V1_12 : + api_version == PGSS_V1_13 ? PG_STAT_STATEMENTS_COLS_V1_13 : -1 /* fail if you forget to update this assert */ )); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } - LWLockRelease(pgss->lock); + LWLockRelease(&pgss->lock.lock); - free(qbuffer); + if (qbuffer) + pfree(qbuffer); } /* Number of output arguments (columns) for pg_stat_statements_info */ @@ -2043,20 +2059,6 @@ pg_stat_statements_info(PG_FUNCTION_ARGS) PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } -/* - * Estimate shared memory space needed. - */ -static Size -pgss_memsize(void) -{ - Size size; - - size = MAXALIGN(sizeof(pgssSharedState)); - size = add_size(size, hash_estimate_size(pgss_max, sizeof(pgssEntry))); - - return size; -} - /* * Allocate a new hashtable entry. * caller must hold an exclusive lock on pgss->lock @@ -2293,7 +2295,7 @@ qtext_store(const char *query, int query_len, } /* - * Read the external query text file into a malloc'd buffer. + * Read the external query text file into a palloc'd buffer. * * Returns NULL (without throwing an error) if unable to read, eg * file not there or insufficient memory. @@ -2335,7 +2337,7 @@ qtext_load_file(Size *buffer_size) /* Allocate buffer; beware that off_t might be wider than size_t */ if (stat.st_size <= MaxAllocHugeSize) - buf = (char *) malloc(stat.st_size); + buf = (char *) palloc_extended(stat.st_size, MCXT_ALLOC_HUGE | MCXT_ALLOC_NO_OOM); else buf = NULL; if (buf == NULL) @@ -2374,7 +2376,7 @@ qtext_load_file(Size *buffer_size) (errcode_for_file_access(), errmsg("could not read file \"%s\": %m", PGSS_TEXT_FILE))); - free(buf); + pfree(buf); CloseTransientFile(fd); return NULL; } @@ -2585,7 +2587,7 @@ gc_qtexts(void) else pgss->mean_query_len = ASSUMED_LENGTH_INIT; - free(qbuffer); + pfree(qbuffer); /* * OK, count a garbage collection cycle. (Note: even though we have @@ -2602,7 +2604,8 @@ gc_qtexts(void) /* clean up resources */ if (qfile) FreeFile(qfile); - free(qbuffer); + if (qbuffer) + pfree(qbuffer); /* * Since the contents of the external file are now uncertain, mark all @@ -2671,13 +2674,13 @@ if (e) { \ * Reset entries corresponding to parameters passed. */ static TimestampTz -entry_reset(Oid userid, Oid dbid, uint64 queryid, bool minmax_only) +entry_reset(Oid userid, Oid dbid, int64 queryid, bool minmax_only) { HASH_SEQ_STATUS hash_seq; pgssEntry *entry; FILE *qfile; - long num_entries; - long num_remove = 0; + int64 num_entries; + int64 num_remove = 0; pgssHashKey key; TimestampTz stats_reset; @@ -2686,12 +2689,12 @@ entry_reset(Oid userid, Oid dbid, uint64 queryid, bool minmax_only) (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_stat_statements must be loaded via \"shared_preload_libraries\""))); - LWLockAcquire(pgss->lock, LW_EXCLUSIVE); + LWLockAcquire(&pgss->lock.lock, LW_EXCLUSIVE); num_entries = hash_get_num_entries(pgss_hash); stats_reset = GetCurrentTimestamp(); - if (userid != 0 && dbid != 0 && queryid != UINT64CONST(0)) + if (userid != 0 && dbid != 0 && queryid != INT64CONST(0)) { /* If all the parameters are available, use the fast path. */ memset(&key, 0, sizeof(pgssHashKey)); @@ -2714,7 +2717,7 @@ entry_reset(Oid userid, Oid dbid, uint64 queryid, bool minmax_only) SINGLE_ENTRY_RESET(entry); } - else if (userid != 0 || dbid != 0 || queryid != UINT64CONST(0)) + else if (userid != 0 || dbid != 0 || queryid != INT64CONST(0)) { /* Reset entries corresponding to valid parameters. */ hash_seq_init(&hash_seq, pgss_hash); @@ -2780,7 +2783,7 @@ entry_reset(Oid userid, Oid dbid, uint64 queryid, bool minmax_only) record_gc_qtexts(); release_lock: - LWLockRelease(pgss->lock); + LWLockRelease(&pgss->lock.lock); return stats_reset; } @@ -2805,28 +2808,26 @@ entry_reset(Oid userid, Oid dbid, uint64 queryid, bool minmax_only) * Returns a palloc'd string. */ static char * -generate_normalized_query(JumbleState *jstate, const char *query, +generate_normalized_query(const JumbleState *jstate, const char *query, int query_loc, int *query_len_p) { char *norm_query; int query_len = *query_len_p; - int i, - norm_query_buflen, /* Space allowed for norm_query */ + int norm_query_buflen, /* Space allowed for norm_query */ len_to_wrt, /* Length (in bytes) to write */ quer_loc = 0, /* Source query byte location */ n_quer_loc = 0, /* Normalized query byte location */ last_off = 0, /* Offset from start for previous tok */ last_tok_len = 0; /* Length (in bytes) of that tok */ - bool in_squashed = false; /* in a run of squashed consts? */ - int skipped_constants = 0; /* Position adjustment of later - * constants after squashed ones */ - + int num_constants_replaced = 0; + LocationLen *locs = NULL; /* - * Get constants' lengths (core system only gives us locations). Note - * this also ensures the items are sorted by location. + * Determine constants' lengths (core system only gives us locations), and + * return a sorted copy of jstate's LocationLen data with lengths filled + * in. */ - fill_in_constant_lengths(jstate, query, query_loc); + locs = ComputeConstantLengths(jstate, query, query_loc); /* * Allow for $n symbols to be longer than the constants they replace. @@ -2834,96 +2835,64 @@ generate_normalized_query(JumbleState *jstate, const char *query, * certainly isn't more than 11 bytes, even if n reaches INT_MAX. We * could refine that limit based on the max value of n for the current * query, but it hardly seems worth any extra effort to do so. - * - * Note this also gives enough room for the commented-out ", ..." list - * syntax used by constant squashing. */ norm_query_buflen = query_len + jstate->clocations_count * 10; /* Allocate result buffer */ norm_query = palloc(norm_query_buflen + 1); - for (i = 0; i < jstate->clocations_count; i++) + for (int i = 0; i < jstate->clocations_count; i++) { int off, /* Offset from start for cur tok */ tok_len; /* Length (in bytes) of that tok */ - off = jstate->clocations[i].location; + /* + * If we have an external param at this location, but no lists are + * being squashed across the query, then we skip here; this will make + * us print the characters found in the original query that represent + * the parameter in the next iteration (or after the loop is done), + * which is a bit odd but seems to work okay in most cases. + */ + if (locs[i].extern_param && !jstate->has_squashed_lists) + continue; + + off = locs[i].location; /* Adjust recorded location if we're dealing with partial string */ off -= query_loc; - tok_len = jstate->clocations[i].length; + tok_len = locs[i].length; if (tok_len < 0) continue; /* ignore any duplicates */ + /* Copy next chunk (what precedes the next constant) */ + len_to_wrt = off - last_off; + len_to_wrt -= last_tok_len; + Assert(len_to_wrt >= 0); + memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); + n_quer_loc += len_to_wrt; + /* - * What to do next depends on whether we're squashing constant lists, - * and whether we're already in a run of such constants. + * And insert a param symbol in place of the constant token; and, if + * we have a squashable list, insert a placeholder comment starting + * from the list's second value. */ - if (!jstate->clocations[i].squashed) - { - /* - * This location corresponds to a constant not to be squashed. - * Print what comes before the constant ... - */ - len_to_wrt = off - last_off; - len_to_wrt -= last_tok_len; - - Assert(len_to_wrt >= 0); + n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d%s", + num_constants_replaced + 1 + jstate->highest_extern_param_id, + locs[i].squashed ? " /*, ... */" : ""); + num_constants_replaced++; - memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); - n_quer_loc += len_to_wrt; - - /* ... and then a param symbol replacing the constant itself */ - n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d", - i + 1 + jstate->highest_extern_param_id - skipped_constants); - - /* In case previous constants were merged away, stop doing that */ - in_squashed = false; - } - else if (!in_squashed) - { - /* - * This location is the start position of a run of constants to be - * squashed, so we need to print the representation of starting a - * group of stashed constants. - * - * Print what comes before the constant ... - */ - len_to_wrt = off - last_off; - len_to_wrt -= last_tok_len; - Assert(len_to_wrt >= 0); - Assert(i + 1 < jstate->clocations_count); - Assert(jstate->clocations[i + 1].squashed); - memcpy(norm_query + n_quer_loc, query + quer_loc, len_to_wrt); - n_quer_loc += len_to_wrt; - - /* ... and then start a run of squashed constants */ - n_quer_loc += sprintf(norm_query + n_quer_loc, "$%d /*, ... */", - i + 1 + jstate->highest_extern_param_id - skipped_constants); - - /* The next location will match the block below, to end the run */ - in_squashed = true; - - skipped_constants++; - } - else - { - /* - * The second location of a run of squashable elements; this - * indicates its end. - */ - in_squashed = false; - } - - /* Otherwise the constant is squashed away -- move forward */ + /* move forward */ quer_loc = off + tok_len; last_off = off; last_tok_len = tok_len; } + /* Clean up, if needed */ + if (locs) + pfree(locs); + /* * We've copied up until the last ignorable constant. Copy over the * remaining bytes of the original query string. @@ -2940,139 +2909,3 @@ generate_normalized_query(JumbleState *jstate, const char *query, *query_len_p = n_quer_loc; return norm_query; } - -/* - * Given a valid SQL string and an array of constant-location records, - * fill in the textual lengths of those constants. - * - * The constants may use any allowed constant syntax, such as float literals, - * bit-strings, single-quoted strings and dollar-quoted strings. This is - * accomplished by using the public API for the core scanner. - * - * It is the caller's job to ensure that the string is a valid SQL statement - * with constants at the indicated locations. Since in practice the string - * has already been parsed, and the locations that the caller provides will - * have originated from within the authoritative parser, this should not be - * a problem. - * - * Duplicate constant pointers are possible, and will have their lengths - * marked as '-1', so that they are later ignored. (Actually, we assume the - * lengths were initialized as -1 to start with, and don't change them here.) - * - * If query_loc > 0, then "query" has been advanced by that much compared to - * the original string start, so we need to translate the provided locations - * to compensate. (This lets us avoid re-scanning statements before the one - * of interest, so it's worth doing.) - * - * N.B. There is an assumption that a '-' character at a Const location begins - * a negative numeric constant. This precludes there ever being another - * reason for a constant to start with a '-'. - */ -static void -fill_in_constant_lengths(JumbleState *jstate, const char *query, - int query_loc) -{ - LocationLen *locs; - core_yyscan_t yyscanner; - core_yy_extra_type yyextra; - core_YYSTYPE yylval; - YYLTYPE yylloc; - int last_loc = -1; - int i; - - /* - * Sort the records by location so that we can process them in order while - * scanning the query text. - */ - if (jstate->clocations_count > 1) - qsort(jstate->clocations, jstate->clocations_count, - sizeof(LocationLen), comp_location); - locs = jstate->clocations; - - /* initialize the flex scanner --- should match raw_parser() */ - yyscanner = scanner_init(query, - &yyextra, - &ScanKeywords, - ScanKeywordTokens); - - /* we don't want to re-emit any escape string warnings */ - yyextra.escape_string_warning = false; - - /* Search for each constant, in sequence */ - for (i = 0; i < jstate->clocations_count; i++) - { - int loc = locs[i].location; - int tok; - - /* Adjust recorded location if we're dealing with partial string */ - loc -= query_loc; - - Assert(loc >= 0); - - if (loc <= last_loc) - continue; /* Duplicate constant, ignore */ - - /* Lex tokens until we find the desired constant */ - for (;;) - { - tok = core_yylex(&yylval, &yylloc, yyscanner); - - /* We should not hit end-of-string, but if we do, behave sanely */ - if (tok == 0) - break; /* out of inner for-loop */ - - /* - * We should find the token position exactly, but if we somehow - * run past it, work with that. - */ - if (yylloc >= loc) - { - if (query[loc] == '-') - { - /* - * It's a negative value - this is the one and only case - * where we replace more than a single token. - * - * Do not compensate for the core system's special-case - * adjustment of location to that of the leading '-' - * operator in the event of a negative constant. It is - * also useful for our purposes to start from the minus - * symbol. In this way, queries like "select * from foo - * where bar = 1" and "select * from foo where bar = -2" - * will have identical normalized query strings. - */ - tok = core_yylex(&yylval, &yylloc, yyscanner); - if (tok == 0) - break; /* out of inner for-loop */ - } - - /* - * We now rely on the assumption that flex has placed a zero - * byte after the text of the current token in scanbuf. - */ - locs[i].length = strlen(yyextra.scanbuf + loc); - break; /* out of inner for-loop */ - } - } - - /* If we hit end-of-string, give up, leaving remaining lengths -1 */ - if (tok == 0) - break; - - last_loc = loc; - } - - scanner_finish(yyscanner); -} - -/* - * comp_location: comparator for qsorting LocationLen structs by location - */ -static int -comp_location(const void *a, const void *b) -{ - int l = ((const LocationLen *) a)->location; - int r = ((const LocationLen *) b)->location; - - return pg_cmp_s32(l, r); -} diff --git a/contrib/pg_stat_statements/pg_stat_statements.control b/contrib/pg_stat_statements/pg_stat_statements.control index d45ebc12e3605..2eee0ceffa894 100644 --- a/contrib/pg_stat_statements/pg_stat_statements.control +++ b/contrib/pg_stat_statements/pg_stat_statements.control @@ -1,5 +1,5 @@ # pg_stat_statements extension comment = 'track planning and execution statistics of all SQL statements executed' -default_version = '1.12' +default_version = '1.13' module_pathname = '$libdir/pg_stat_statements' relocatable = true diff --git a/contrib/pg_stat_statements/sql/cursors.sql b/contrib/pg_stat_statements/sql/cursors.sql index 61738ac470e82..78bb42284331f 100644 --- a/contrib/pg_stat_statements/sql/cursors.sql +++ b/contrib/pg_stat_statements/sql/cursors.sql @@ -28,3 +28,46 @@ COMMIT; SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +-- Normalization of FETCH statements +BEGIN; +DECLARE pgss_cursor CURSOR FOR SELECT FROM generate_series(1, 10); +-- implicit directions +FETCH pgss_cursor; +FETCH 1 pgss_cursor; +FETCH 2 pgss_cursor; +FETCH -1 pgss_cursor; +-- explicit NEXT +FETCH NEXT pgss_cursor; +-- explicit PRIOR +FETCH PRIOR pgss_cursor; +-- explicit FIRST +FETCH FIRST pgss_cursor; +-- explicit LAST +FETCH LAST pgss_cursor; +-- explicit ABSOLUTE +FETCH ABSOLUTE 1 pgss_cursor; +FETCH ABSOLUTE 2 pgss_cursor; +FETCH ABSOLUTE -1 pgss_cursor; +-- explicit RELATIVE +FETCH RELATIVE 1 pgss_cursor; +FETCH RELATIVE 2 pgss_cursor; +FETCH RELATIVE -1 pgss_cursor; +-- explicit FORWARD +FETCH ALL pgss_cursor; +-- explicit FORWARD ALL +FETCH FORWARD ALL pgss_cursor; +-- explicit FETCH FORWARD +FETCH FORWARD pgss_cursor; +FETCH FORWARD 1 pgss_cursor; +FETCH FORWARD 2 pgss_cursor; +FETCH FORWARD -1 pgss_cursor; +-- explicit FETCH BACKWARD +FETCH BACKWARD pgss_cursor; +FETCH BACKWARD 1 pgss_cursor; +FETCH BACKWARD 2 pgss_cursor; +FETCH BACKWARD -1 pgss_cursor; +-- explicit BACKWARD ALL +FETCH BACKWARD ALL pgss_cursor; +COMMIT; +SELECT calls, query FROM pg_stat_statements ORDER BY query COLLATE "C"; diff --git a/contrib/pg_stat_statements/sql/extended.sql b/contrib/pg_stat_statements/sql/extended.sql index 1af0711020c41..9a6518e2f0487 100644 --- a/contrib/pg_stat_statements/sql/extended.sql +++ b/contrib/pg_stat_statements/sql/extended.sql @@ -19,3 +19,28 @@ SELECT $1 \bind 'unnamed_val1' \g \bind_named stmt1 'stmt1_val1' \g SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- Various parameter numbering patterns +-- Unique query IDs with parameter numbers switched. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT WHERE ($1::int, 7) IN ((8, $2::int), ($3::int, 9)) \bind '1' '2' '3' \g +SELECT WHERE ($2::int, 10) IN ((11, $3::int), ($1::int, 12)) \bind '1' '2' '3' \g +SELECT WHERE $1::int IN ($2::int, $3::int) \bind '1' '2' '3' \g +SELECT WHERE $2::int IN ($3::int, $1::int) \bind '1' '2' '3' \g +SELECT WHERE $3::int IN ($1::int, $2::int) \bind '1' '2' '3' \g +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +-- Two groups of two queries with the same query ID. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT WHERE '1'::int IN ($1::int, '2'::int) \bind '1' \g +SELECT WHERE '4'::int IN ($1::int, '5'::int) \bind '2' \g +SELECT WHERE $2::int IN ($1::int, '1'::int) \bind '1' '2' \g +SELECT WHERE $2::int IN ($1::int, '2'::int) \bind '3' '4' \g +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +-- no squashable list, the parameters id's are kept as-is +SELECT WHERE $3 = $1 AND $2 = $4 \bind 1 2 1 2 \g +-- squashable list, so the parameter IDs will be re-assigned +SELECT WHERE 1 IN (1, 2, 3) AND $3 = $1 AND $2 = $4 \bind 1 2 1 2 \g + +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; diff --git a/contrib/pg_stat_statements/sql/level_tracking.sql b/contrib/pg_stat_statements/sql/level_tracking.sql index 86f007e85524a..003efb8184b12 100644 --- a/contrib/pg_stat_statements/sql/level_tracking.sql +++ b/contrib/pg_stat_statements/sql/level_tracking.sql @@ -431,6 +431,50 @@ SELECT PLUS_THREE(8); SELECT PLUS_THREE(10); SELECT toplevel, calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +-- planner - all-level tracking. +SET pg_stat_statements.track_planning = TRUE; +-- Release all cached plans before the first function call. This matters +-- when debug_discard_caches is enabled, which would store a normalized +-- version of the inner query of the function. Forcing a plan rebuild +-- ensures that a normalized version is always stored with the stats entry, +-- while checking that the nesting level is computed correctly in the +-- planner hook. +DISCARD PLANS; +SELECT PLUS_THREE(8); +SELECT PLUS_THREE(10); + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; +RESET pg_stat_statements.track_planning; + +-- AFTER trigger SQL (ExecutorFinish) - all-level tracking. +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; + +CREATE TABLE test_trigger (id int, name text); +CREATE TABLE audit_table (table_name text, action text, row_id int); +CREATE OR REPLACE FUNCTION audit_trigger_func() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO audit_table VALUES ('test_trigger', TG_OP, NEW.id); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER audit_after_trigger + AFTER INSERT ON test_trigger + FOR EACH ROW EXECUTE FUNCTION audit_trigger_func(); + +INSERT INTO test_trigger VALUES (1, 'test1'); +INSERT INTO test_trigger VALUES (2, 'test2'); + +SELECT toplevel, calls, rows, plans, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + +DROP TRIGGER audit_after_trigger ON test_trigger; +DROP FUNCTION audit_trigger_func(); +DROP TABLE audit_table, test_trigger; -- -- pg_stat_statements.track = none diff --git a/contrib/pg_stat_statements/sql/oldextversions.sql b/contrib/pg_stat_statements/sql/oldextversions.sql index 13b8ca28586d1..e416efe9ffbee 100644 --- a/contrib/pg_stat_statements/sql/oldextversions.sql +++ b/contrib/pg_stat_statements/sql/oldextversions.sql @@ -63,4 +63,9 @@ AlTER EXTENSION pg_stat_statements UPDATE TO '1.12'; \d pg_stat_statements SELECT count(*) > 0 AS has_data FROM pg_stat_statements; +-- New functions and views for pg_stat_statements in 1.13 +AlTER EXTENSION pg_stat_statements UPDATE TO '1.13'; +\d pg_stat_statements +SELECT count(*) > 0 AS has_data FROM pg_stat_statements; + DROP EXTENSION pg_stat_statements; diff --git a/contrib/pg_stat_statements/sql/plancache.sql b/contrib/pg_stat_statements/sql/plancache.sql new file mode 100644 index 0000000000000..160ced7add368 --- /dev/null +++ b/contrib/pg_stat_statements/sql/plancache.sql @@ -0,0 +1,94 @@ +-- +-- Tests with plan cache +-- + +-- Setup +CREATE OR REPLACE FUNCTION select_one_func(int) RETURNS VOID AS $$ +DECLARE + ret INT; +BEGIN + SELECT $1 INTO ret; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE PROCEDURE select_one_proc(int) AS $$ +DECLARE + ret INT; +BEGIN + SELECT $1 INTO ret; +END; +$$ LANGUAGE plpgsql; + +-- Prepared statements +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +PREPARE p1 AS SELECT $1 AS a; +SET plan_cache_mode TO force_generic_plan; +EXECUTE p1(1); +SET plan_cache_mode TO force_custom_plan; +EXECUTE p1(1); +SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; +DEALLOCATE p1; + +-- Extended query protocol +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT $1 AS a \parse p1 +SET plan_cache_mode TO force_generic_plan; +\bind_named p1 1 +; +SET plan_cache_mode TO force_custom_plan; +\bind_named p1 1 +; +SELECT calls, generic_plan_calls, custom_plan_calls, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; +\close_prepared p1 + +-- EXPLAIN [ANALYZE] EXECUTE +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +PREPARE p1 AS SELECT $1; +SET plan_cache_mode TO force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE p1(1); +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1); +SET plan_cache_mode TO force_custom_plan; +EXPLAIN (COSTS OFF) EXECUTE p1(1); +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) EXECUTE p1(1); +SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; +RESET pg_stat_statements.track; +DEALLOCATE p1; + +-- Functions/procedures +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SET plan_cache_mode TO force_generic_plan; +SELECT select_one_func(1); +CALL select_one_proc(1); +SET plan_cache_mode TO force_custom_plan; +SELECT select_one_func(1); +CALL select_one_proc(1); +SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements + ORDER BY query COLLATE "C"; + +-- +-- EXPLAIN [ANALYZE] EXECUTE + functions/procedures +-- +SET pg_stat_statements.track = 'all'; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SET plan_cache_mode TO force_generic_plan; +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1); +EXPLAIN (COSTS OFF) SELECT select_one_func(1); +CALL select_one_proc(1); +SET plan_cache_mode TO force_custom_plan; +EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF, TIMING OFF, BUFFERS OFF) SELECT select_one_func(1); +EXPLAIN (COSTS OFF) SELECT select_one_func(1); +CALL select_one_proc(1); +SELECT calls, generic_plan_calls, custom_plan_calls, toplevel, query FROM pg_stat_statements + ORDER BY query COLLATE "C", toplevel; + +RESET pg_stat_statements.track; + +-- +-- Cleanup +-- +DROP FUNCTION select_one_func(int); +DROP PROCEDURE select_one_proc(int); diff --git a/contrib/pg_stat_statements/sql/planning.sql b/contrib/pg_stat_statements/sql/planning.sql index 9cfe206b3b049..46f5d9b951c45 100644 --- a/contrib/pg_stat_statements/sql/planning.sql +++ b/contrib/pg_stat_statements/sql/planning.sql @@ -20,11 +20,11 @@ SELECT 42; SELECT 42; SELECT 42; SELECT plans, calls, rows, query FROM pg_stat_statements - WHERE query NOT LIKE 'SELECT COUNT%' ORDER BY query COLLATE "C"; + WHERE query NOT LIKE 'PREPARE%' ORDER BY query COLLATE "C"; -- for the prepared statement we expect at least one replan, but cache -- invalidations could force more SELECT plans >= 2 AND plans <= calls AS plans_ok, calls, rows, query FROM pg_stat_statements - WHERE query LIKE 'SELECT COUNT%' ORDER BY query COLLATE "C"; + WHERE query LIKE 'PREPARE%' ORDER BY query COLLATE "C"; -- Cleanup DROP TABLE stats_plan_test; diff --git a/contrib/pg_stat_statements/sql/select.sql b/contrib/pg_stat_statements/sql/select.sql index c5e0b84ee5bf5..a10d618c034ed 100644 --- a/contrib/pg_stat_statements/sql/select.sql +++ b/contrib/pg_stat_statements/sql/select.sql @@ -79,6 +79,22 @@ DEALLOCATE pgss_test; SELECT calls, rows, query FROM pg_stat_statements ORDER BY query COLLATE "C"; SELECT pg_stat_statements_reset() IS NOT NULL AS t; +-- normalization of constants and parameters, with constant locations +-- recorded one or more times. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT WHERE '1' IN ('1'::int, '3'::int::text); +SELECT WHERE (1, 2) IN ((1, 2), (2, 3)); +SELECT WHERE (3, 4) IN ((5, 6), (8, 7)); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- with the last element being an explicit function call with an argument, ensure +-- the normalization of the squashing interval is correct. +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT WHERE 1 IN (1, int4(1), int4(2)); +SELECT WHERE 1 = ANY (ARRAY[1, int4(1), int4(2)]); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + -- -- queries with locking clauses -- @@ -142,6 +158,22 @@ FETCH FIRST 2 ROW ONLY; SELECT COUNT(*) FROM pg_stat_statements WHERE query LIKE '%FETCH FIRST%'; +-- GROUP BY, HAVING, GROUPING +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a; +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b; +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a, b; +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b, a; +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY GROUPING SETS(a, ()); +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY GROUPING SETS(b, ()); +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a HAVING a = 1; +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a HAVING a = 2; +SELECT COUNT(*) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b HAVING b = 1; +SELECT GROUPING(a) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a; +SELECT GROUPING(b) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b; +SELECT GROUPING(b) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY a, b; +SELECT GROUPING(b) FROM (VALUES (1::INT, 2::INT)) AS t(a, b) GROUP BY b, a; +SELECT calls, query FROM pg_stat_statements WHERE query LIKE '%GROUP BY%' ORDER BY query COLLATE "C"; + -- GROUP BY [DISTINCT] SELECT a, b, c FROM (VALUES (1, 2, 3), (4, NULL, 6), (7, 8, 9)) AS t (a, b, c) diff --git a/contrib/pg_stat_statements/sql/squashing.sql b/contrib/pg_stat_statements/sql/squashing.sql index 03efd4b40c8e7..fc9e657387389 100644 --- a/contrib/pg_stat_statements/sql/squashing.sql +++ b/contrib/pg_stat_statements/sql/squashing.sql @@ -1,103 +1,161 @@ -- -- Const squashing functionality -- -CREATE EXTENSION pg_stat_statements; -CREATE TABLE test_squash (id int, data int); +-- +-- Simple Lists +-- --- IN queries +CREATE TABLE test_squash (id int, data int); --- Normal scenario, too many simple constants for an IN query +-- single element will not be squashed SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash WHERE id IN (1); +SELECT ARRAY[1]; +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- more than 1 element in a list will be squashed +SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash WHERE id IN (1, 2, 3); +SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4); +SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5); +SELECT ARRAY[1, 2, 3]; +SELECT ARRAY[1, 2, 3, 4]; +SELECT ARRAY[1, 2, 3, 4, 5]; +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- built-in functions will be squashed +-- the IN and ARRAY forms of this statement will have the same queryId +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT WHERE 1 IN (1, int4(1), int4(2), 2); +SELECT WHERE 1 = ANY (ARRAY[1, int4(1), int4(2), 2]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; -SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9); -SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); -SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); +-- external parameters will be squashed +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT * FROM test_squash WHERE id IN ($1, $2, $3, $4, $5) \bind 1 2 3 4 5 +; +SELECT * FROM test_squash WHERE id::text = ANY(ARRAY[$1, $2, $3, $4, $5]) \bind 1 2 3 4 5 +; SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; --- More conditions in the query +-- prepared statements will also be squashed +-- the IN and ARRAY forms of this statement will have the same queryId SELECT pg_stat_statements_reset() IS NOT NULL AS t; +PREPARE p1(int, int, int, int, int) AS +SELECT * FROM test_squash WHERE id IN ($1, $2, $3, $4, $5); +EXECUTE p1(1, 2, 3, 4, 5); +DEALLOCATE p1; +PREPARE p1(int, int, int, int, int) AS +SELECT * FROM test_squash WHERE id = ANY(ARRAY[$1, $2, $3, $4, $5]); +EXECUTE p1(1, 2, 3, 4, 5); +DEALLOCATE p1; +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +-- More conditions in the query +SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9) AND data = 2; SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) AND data = 2; SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) AND data = 2; +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9]) AND data = 2; +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) AND data = 2; +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) AND data = 2; SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; -- Multiple squashed intervals SELECT pg_stat_statements_reset() IS NOT NULL AS t; - SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9) AND data IN (1, 2, 3, 4, 5, 6, 7, 8, 9); SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) AND data IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); SELECT * FROM test_squash WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) AND data IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9]) + AND data = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9]); +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + AND data = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +SELECT * FROM test_squash WHERE id = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + AND data = ANY (ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; - --- No constants simplification for OpExpr SELECT pg_stat_statements_reset() IS NOT NULL AS t; --- In the following two queries the operator expressions (+) and (@) have --- different oppno, and will be given different query_id if squashed, even though --- the normalized query will be the same +-- No constants squashing for OpExpr +-- The IN and ARRAY forms of this statement will have the same queryId +SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash WHERE id IN (1 + 1, 2 + 2, 3 + 3, 4 + 4, 5 + 5, 6 + 6, 7 + 7, 8 + 8, 9 + 9); SELECT * FROM test_squash WHERE id IN (@ '-1', @ '-2', @ '-3', @ '-4', @ '-5', @ '-6', @ '-7', @ '-8', @ '-9'); +SELECT * FROM test_squash WHERE id = ANY(ARRAY + [1 + 1, 2 + 2, 3 + 3, 4 + 4, 5 + 5, 6 + 6, 7 + 7, 8 + 8, 9 + 9]); +SELECT * FROM test_squash WHERE id = ANY(ARRAY + [@ '-1', @ '-2', @ '-3', @ '-4', @ '-5', @ '-6', @ '-7', @ '-8', @ '-9']); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +-- -- FuncExpr +-- -- Verify multiple type representation end up with the same query_id CREATE TABLE test_float (data float); +-- The casted ARRAY expressions will have the same queryId as the IN clause +-- form of the query SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT data FROM test_float WHERE data IN (1, 2); SELECT data FROM test_float WHERE data IN (1, '2'); SELECT data FROM test_float WHERE data IN ('1', 2); SELECT data FROM test_float WHERE data IN ('1', '2'); SELECT data FROM test_float WHERE data IN (1.0, 1.0); +SELECT data FROM test_float WHERE data = ANY(ARRAY['1'::double precision, '2'::double precision]); +SELECT data FROM test_float WHERE data = ANY(ARRAY[1.0::double precision, 1.0::double precision]); +SELECT data FROM test_float WHERE data = ANY(ARRAY[1, 2]); +SELECT data FROM test_float WHERE data = ANY(ARRAY[1, '2']); +SELECT data FROM test_float WHERE data = ANY(ARRAY['1', 2]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; -- Numeric type, implicit cast is squashed CREATE TABLE test_squash_numeric (id int, data numeric(5, 2)); SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash_numeric WHERE data IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); +SELECT * FROM test_squash_numeric WHERE data = ANY(ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; -- Bigint, implicit cast is squashed CREATE TABLE test_squash_bigint (id int, data bigint); SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash_bigint WHERE data IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); +SELECT * FROM test_squash_bigint WHERE data = ANY(ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; --- Bigint, explicit cast is not squashed +-- Bigint, explicit cast is squashed SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash_bigint WHERE data IN (1::bigint, 2::bigint, 3::bigint, 4::bigint, 5::bigint, 6::bigint, 7::bigint, 8::bigint, 9::bigint, 10::bigint, 11::bigint); +SELECT * FROM test_squash_bigint WHERE data = ANY(ARRAY[ + 1::bigint, 2::bigint, 3::bigint, 4::bigint, 5::bigint, 6::bigint, + 7::bigint, 8::bigint, 9::bigint, 10::bigint, 11::bigint]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; --- Bigint, long tokens with parenthesis +-- Bigint, long tokens with parenthesis, will not squash SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash_bigint WHERE id IN (abs(100), abs(200), abs(300), abs(400), abs(500), abs(600), abs(700), abs(800), abs(900), abs(1000), ((abs(1100)))); +SELECT * FROM test_squash_bigint WHERE id = ANY(ARRAY[ + abs(100), abs(200), abs(300), abs(400), abs(500), abs(600), abs(700), + abs(800), abs(900), abs(1000), ((abs(1100)))]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; --- CoerceViaIO, SubLink instead of a Const -CREATE TABLE test_squash_jsonb (id int, data jsonb); +-- Multiple FuncExpr's. Will not squash SELECT pg_stat_statements_reset() IS NOT NULL AS t; -SELECT * FROM test_squash_jsonb WHERE data IN - ((SELECT '"1"')::jsonb, (SELECT '"2"')::jsonb, (SELECT '"3"')::jsonb, - (SELECT '"4"')::jsonb, (SELECT '"5"')::jsonb, (SELECT '"6"')::jsonb, - (SELECT '"7"')::jsonb, (SELECT '"8"')::jsonb, (SELECT '"9"')::jsonb, - (SELECT '"10"')::jsonb); +SELECT WHERE 1 IN (1::int::bigint::int, 2::int::bigint::int); +SELECT WHERE 1 = ANY(ARRAY[1::int::bigint::int, 2::int::bigint::int]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +-- -- CoerceViaIO +-- -- Create some dummy type to force CoerceViaIO CREATE TYPE casttesttype; @@ -141,19 +199,74 @@ SELECT * FROM test_squash_cast WHERE data IN 4::int4::casttesttype, 5::int4::casttesttype, 6::int4::casttesttype, 7::int4::casttesttype, 8::int4::casttesttype, 9::int4::casttesttype, 10::int4::casttesttype, 11::int4::casttesttype); +SELECT * FROM test_squash_cast WHERE data = ANY (ARRAY + [1::int4::casttesttype, 2::int4::casttesttype, 3::int4::casttesttype, + 4::int4::casttesttype, 5::int4::casttesttype, 6::int4::casttesttype, + 7::int4::casttesttype, 8::int4::casttesttype, 9::int4::casttesttype, + 10::int4::casttesttype, 11::int4::casttesttype]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; -- Some casting expression are simplified to Const +CREATE TABLE test_squash_jsonb (id int, data jsonb); SELECT pg_stat_statements_reset() IS NOT NULL AS t; SELECT * FROM test_squash_jsonb WHERE data IN (('"1"')::jsonb, ('"2"')::jsonb, ('"3"')::jsonb, ('"4"')::jsonb, - ( '"5"')::jsonb, ( '"6"')::jsonb, ( '"7"')::jsonb, ( '"8"')::jsonb, - ( '"9"')::jsonb, ( '"10"')::jsonb); + ('"5"')::jsonb, ('"6"')::jsonb, ('"7"')::jsonb, ('"8"')::jsonb, + ('"9"')::jsonb, ('"10"')::jsonb); +SELECT * FROM test_squash_jsonb WHERE data = ANY (ARRAY + [('"1"')::jsonb, ('"2"')::jsonb, ('"3"')::jsonb, ('"4"')::jsonb, + ('"5"')::jsonb, ('"6"')::jsonb, ('"7"')::jsonb, ('"8"')::jsonb, + ('"9"')::jsonb, ('"10"')::jsonb]); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- CoerceViaIO, SubLink instead of a Const. Will not squash +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT * FROM test_squash_jsonb WHERE data IN + ((SELECT '"1"')::jsonb, (SELECT '"2"')::jsonb, (SELECT '"3"')::jsonb, + (SELECT '"4"')::jsonb, (SELECT '"5"')::jsonb, (SELECT '"6"')::jsonb, + (SELECT '"7"')::jsonb, (SELECT '"8"')::jsonb, (SELECT '"9"')::jsonb, + (SELECT '"10"')::jsonb); +SELECT * FROM test_squash_jsonb WHERE data = ANY(ARRAY + [(SELECT '"1"')::jsonb, (SELECT '"2"')::jsonb, (SELECT '"3"')::jsonb, + (SELECT '"4"')::jsonb, (SELECT '"5"')::jsonb, (SELECT '"6"')::jsonb, + (SELECT '"7"')::jsonb, (SELECT '"8"')::jsonb, (SELECT '"9"')::jsonb, + (SELECT '"10"')::jsonb]); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- Multiple CoerceViaIO are squashed +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT WHERE 1 IN (1::text::int::text::int, 1::text::int::text::int); +SELECT WHERE 1 = ANY(ARRAY[1::text::int::text::int, 1::text::int::text::int]); SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; +-- -- RelabelType +-- + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +-- However many layers of RelabelType there are, the list will be squashable. +SELECT * FROM test_squash WHERE id IN + (1::oid, 2::oid, 3::oid, 4::oid, 5::oid, 6::oid, 7::oid, 8::oid, 9::oid); +SELECT ARRAY[1::oid, 2::oid, 3::oid, 4::oid, 5::oid, 6::oid, 7::oid, 8::oid, 9::oid]; +SELECT * FROM test_squash WHERE id IN (1::oid, 2::oid::int::oid); +SELECT * FROM test_squash WHERE id = ANY(ARRAY[1::oid, 2::oid::int::oid]); +-- RelabelType together with CoerceViaIO is also squashable +SELECT * FROM test_squash WHERE id = ANY(ARRAY[1::oid::text::int::oid, 2::oid::int::oid]); +SELECT * FROM test_squash WHERE id = ANY(ARRAY[1::text::int::oid, 2::oid::int::oid]); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- +-- edge cases +-- + SELECT pg_stat_statements_reset() IS NOT NULL AS t; -SELECT * FROM test_squash WHERE id IN (1::oid, 2::oid, 3::oid, 4::oid, 5::oid, 6::oid, 7::oid, 8::oid, 9::oid); +-- for nested arrays, only constants are squashed +SELECT ARRAY[ + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ]; SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; -- Test constants evaluation in a CTE, which was causing issues in the past @@ -163,7 +276,58 @@ WITH cte AS ( SELECT ARRAY['a', 'b', 'c', const::varchar] AS result FROM cte; --- Simple array would be squashed as well SELECT pg_stat_statements_reset() IS NOT NULL AS t; -SELECT ARRAY[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +-- Rewritten as an OpExpr, so it will not be squashed +select where '1' IN ('1'::int, '2'::int::text); +-- Rewritten as an ArrayExpr, so it will be squashed +select where '1' IN ('1'::int, '2'::int); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +-- Both of these queries will be rewritten as an ArrayExpr, so they +-- will be squashed, and have a similar queryId +select where '1' IN ('1'::int::text, '2'::int::text); +select where '1' = ANY (array['1'::int::text, '2'::int::text]); +SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- composite function with row expansion +create table test_composite(x integer); +CREATE FUNCTION composite_f(a integer[], out x integer, out y integer) returns +record as $$ begin + x = a[1]; + y = a[2]; + end; +$$ language plpgsql; +SELECT pg_stat_statements_reset() IS NOT NULL AS t; +SELECT ((composite_f(array[1, 2]))).* FROM test_composite; +SELECT ((composite_f(array[1, 2, 3]))).* FROM test_composite; +SELECT ((composite_f(array[1, 2, 3]))).*, 1, 2, 3, ((composite_f(array[1, 2, 3]))).*, 1, 2 +FROM test_composite +WHERE x IN (1, 2, 3); +SELECT ((composite_f(array[1, $1, 3]))).*, 1 FROM test_composite \bind 1 +; +-- ROW() expression with row expansion +SELECT (ROW(ARRAY[1,2])).*; +SELECT (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*; +SELECT 1, 2, (ROW(ARRAY[1, 2], ARRAY[1, 2, 3])).*, 3, 4; +SELECT (ROW(ARRAY[1, 2], ARRAY[1, $1, 3])).*, 1 \bind 1 +; + +-- IN and ANY clauses with Vars are not squashed. +SELECT * FROM test_squash a, test_squash b WHERE a.id IN (1, 2, 3, b.id, b.id + 1); +SELECT * FROM test_squash a, test_squash b WHERE a.id = ANY (array[1, ((b.id + b.id * 2)), 5]); +SELECT * FROM test_squash a, test_squash b WHERE a.id IN ($1, $2, $3, b.id, b.id + $4) \bind 1 2 3 1 +; SELECT query, calls FROM pg_stat_statements ORDER BY query COLLATE "C"; + +-- +-- cleanup +-- +DROP TABLE test_squash; +DROP TABLE test_float; +DROP TABLE test_squash_numeric; +DROP TABLE test_squash_bigint; +DROP TABLE test_squash_cast CASCADE; +DROP TABLE test_squash_jsonb; +DROP TABLE test_composite; +DROP FUNCTION composite_f; diff --git a/contrib/pg_stat_statements/t/010_restart.pl b/contrib/pg_stat_statements/t/010_restart.pl index 066345ab8215a..eab26c01f8d8c 100644 --- a/contrib/pg_stat_statements/t/010_restart.pl +++ b/contrib/pg_stat_statements/t/010_restart.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Tests for checking that pg_stat_statements contents are preserved # across restarts. diff --git a/contrib/pg_surgery/heap_surgery.c b/contrib/pg_surgery/heap_surgery.c index 3e86283beb7cf..ae4e7c0136ccd 100644 --- a/contrib/pg_surgery/heap_surgery.c +++ b/contrib/pg_surgery/heap_surgery.c @@ -3,7 +3,7 @@ * heap_surgery.c * Functions to perform surgery on the damaged heap table. * - * Copyright (c) 2020-2025, PostgreSQL Global Development Group + * Copyright (c) 2020-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pg_surgery/heap_surgery.c @@ -356,8 +356,8 @@ heap_force_common(FunctionCallInfo fcinfo, HeapTupleForceOption heap_force_opt) static int32 tidcmp(const void *a, const void *b) { - ItemPointer iptr1 = ((const ItemPointer) a); - ItemPointer iptr2 = ((const ItemPointer) b); + const ItemPointerData *iptr1 = a; + const ItemPointerData *iptr2 = b; return ItemPointerCompare(iptr1, iptr2); } diff --git a/contrib/pg_surgery/meson.build b/contrib/pg_surgery/meson.build index c6cfa9c4694a3..88e16dcc1b226 100644 --- a/contrib/pg_surgery/meson.build +++ b/contrib/pg_surgery/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_surgery_sources = files( 'heap_surgery.c', diff --git a/contrib/pg_trgm/Makefile b/contrib/pg_trgm/Makefile index 1fbdc9ec1ef43..c1756993ec7ba 100644 --- a/contrib/pg_trgm/Makefile +++ b/contrib/pg_trgm/Makefile @@ -14,7 +14,7 @@ DATA = pg_trgm--1.5--1.6.sql pg_trgm--1.4--1.5.sql pg_trgm--1.3--1.4.sql \ pg_trgm--1.0--1.1.sql PGFILEDESC = "pg_trgm - trigram matching" -REGRESS = pg_trgm pg_word_trgm pg_strict_word_trgm +REGRESS = pg_trgm pg_utf8_trgm pg_word_trgm pg_strict_word_trgm ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/pg_trgm/data/trgm_utf8.data b/contrib/pg_trgm/data/trgm_utf8.data new file mode 100644 index 0000000000000..713856e76a625 --- /dev/null +++ b/contrib/pg_trgm/data/trgm_utf8.data @@ -0,0 +1,50 @@ +Mathematics +æ•°å­¦ +गणित +Matemáticas +رياضيات +Mathématiques +গণিত +Matemática +Математика +ریاضی +Matematika +Mathematik +æ•°å­¦ +Mathematics +गणित +గణితం +Matematik +கணிதம௠+數學 +Toán há»c +Matematika +æ•°å­¦ +수학 +ریاضی +Lissafi +Hisabati +Matematika +Matematica +ریاضی +ಗಣಿತ +ગણિત +คณิตศาสตร์ +ሂሳብ +गणित +ਗਣਿਤ +數學 +æ•°å­¦ +Iá¹£iro +數學 +သင်္á€á€»á€¬ +Herrega +رياضي +गणित +Математика +Matematyka +ഗണിതം +Matematika +رياضي +Matematika +Matematică diff --git a/contrib/pg_trgm/expected/pg_trgm.out b/contrib/pg_trgm/expected/pg_trgm.out index 0b70d9de25624..612625f1fda79 100644 --- a/contrib/pg_trgm/expected/pg_trgm.out +++ b/contrib/pg_trgm/expected/pg_trgm.out @@ -7,9 +7,6 @@ WHERE opc.oid >= 16384 AND NOT amvalidate(opc.oid); --------+--------- (0 rows) ---backslash is used in tests below, installcheck will fail if ---standard_conforming_string is off -set standard_conforming_strings=on; -- reduce noise set extra_float_digits = 0; select show_trgm(''); @@ -4693,6 +4690,23 @@ select count(*) from test_trgm where t like '%99%' and t like '%qw%'; 19 (1 row) +explain (costs off) +select count(*) from test_trgm where t %> '' and t %> '%qwerty%'; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on test_trgm + Recheck Cond: ((t %> ''::text) AND (t %> '%qwerty%'::text)) + -> Bitmap Index Scan on trgm_idx + Index Cond: ((t %> ''::text) AND (t %> '%qwerty%'::text)) +(5 rows) + +select count(*) from test_trgm where t %> '' and t %> '%qwerty%'; + count +------- + 0 +(1 row) + -- ensure that pending-list items are handled correctly, too create temp table t_test_trgm(t text COLLATE "C"); create index t_trgm_idx on t_test_trgm using gin (t gin_trgm_ops); @@ -4731,6 +4745,23 @@ select count(*) from t_test_trgm where t like '%99%' and t like '%qw%'; 1 (1 row) +explain (costs off) +select count(*) from t_test_trgm where t %> '' and t %> '%qwerty%'; + QUERY PLAN +------------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on t_test_trgm + Recheck Cond: ((t %> ''::text) AND (t %> '%qwerty%'::text)) + -> Bitmap Index Scan on t_trgm_idx + Index Cond: ((t %> ''::text) AND (t %> '%qwerty%'::text)) +(5 rows) + +select count(*) from t_test_trgm where t %> '' and t %> '%qwerty%'; + count +------- + 0 +(1 row) + -- run the same queries with sequential scan to check the results set enable_bitmapscan=off; set enable_seqscan=on; @@ -4746,6 +4777,12 @@ select count(*) from test_trgm where t like '%99%' and t like '%qw%'; 19 (1 row) +select count(*) from test_trgm where t %> '' and t %> '%qwerty%'; + count +------- + 0 +(1 row) + select count(*) from t_test_trgm where t like '%99%' and t like '%qwerty%'; count ------- @@ -4758,6 +4795,12 @@ select count(*) from t_test_trgm where t like '%99%' and t like '%qw%'; 1 (1 row) +select count(*) from t_test_trgm where t %> '' and t %> '%qwerty%'; + count +------- + 0 +(1 row) + reset enable_bitmapscan; create table test2(t text COLLATE "C"); insert into test2 values ('abcdef'); diff --git a/contrib/pg_trgm/expected/pg_utf8_trgm.out b/contrib/pg_trgm/expected/pg_utf8_trgm.out new file mode 100644 index 0000000000000..0768e7d6a8320 --- /dev/null +++ b/contrib/pg_trgm/expected/pg_utf8_trgm.out @@ -0,0 +1,8 @@ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit +\endif +-- Index 50 translations of the word "Mathematics" +CREATE TEMP TABLE mb (s text); +\copy mb from 'data/trgm_utf8.data' +CREATE INDEX ON mb USING gist(s gist_trgm_ops); diff --git a/contrib/pg_trgm/expected/pg_utf8_trgm_1.out b/contrib/pg_trgm/expected/pg_utf8_trgm_1.out new file mode 100644 index 0000000000000..8505c4fa55262 --- /dev/null +++ b/contrib/pg_trgm/expected/pg_utf8_trgm_1.out @@ -0,0 +1,3 @@ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit diff --git a/contrib/pg_trgm/meson.build b/contrib/pg_trgm/meson.build index a31aa5c574d34..3ecf95ba862e9 100644 --- a/contrib/pg_trgm/meson.build +++ b/contrib/pg_trgm/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_trgm_sources = files( 'trgm_gin.c', @@ -39,6 +39,7 @@ tests += { 'regress': { 'sql': [ 'pg_trgm', + 'pg_utf8_trgm', 'pg_word_trgm', 'pg_strict_word_trgm', ], diff --git a/contrib/pg_trgm/sql/pg_trgm.sql b/contrib/pg_trgm/sql/pg_trgm.sql index 340c9891899f0..49db86caf7d88 100644 --- a/contrib/pg_trgm/sql/pg_trgm.sql +++ b/contrib/pg_trgm/sql/pg_trgm.sql @@ -5,10 +5,6 @@ SELECT amname, opcname FROM pg_opclass opc LEFT JOIN pg_am am ON am.oid = opcmethod WHERE opc.oid >= 16384 AND NOT amvalidate(opc.oid); ---backslash is used in tests below, installcheck will fail if ---standard_conforming_string is off -set standard_conforming_strings=on; - -- reduce noise set extra_float_digits = 0; @@ -80,6 +76,9 @@ select count(*) from test_trgm where t like '%99%' and t like '%qwerty%'; explain (costs off) select count(*) from test_trgm where t like '%99%' and t like '%qw%'; select count(*) from test_trgm where t like '%99%' and t like '%qw%'; +explain (costs off) +select count(*) from test_trgm where t %> '' and t %> '%qwerty%'; +select count(*) from test_trgm where t %> '' and t %> '%qwerty%'; -- ensure that pending-list items are handled correctly, too create temp table t_test_trgm(t text COLLATE "C"); create index t_trgm_idx on t_test_trgm using gin (t gin_trgm_ops); @@ -90,14 +89,19 @@ select count(*) from t_test_trgm where t like '%99%' and t like '%qwerty%'; explain (costs off) select count(*) from t_test_trgm where t like '%99%' and t like '%qw%'; select count(*) from t_test_trgm where t like '%99%' and t like '%qw%'; +explain (costs off) +select count(*) from t_test_trgm where t %> '' and t %> '%qwerty%'; +select count(*) from t_test_trgm where t %> '' and t %> '%qwerty%'; -- run the same queries with sequential scan to check the results set enable_bitmapscan=off; set enable_seqscan=on; select count(*) from test_trgm where t like '%99%' and t like '%qwerty%'; select count(*) from test_trgm where t like '%99%' and t like '%qw%'; +select count(*) from test_trgm where t %> '' and t %> '%qwerty%'; select count(*) from t_test_trgm where t like '%99%' and t like '%qwerty%'; select count(*) from t_test_trgm where t like '%99%' and t like '%qw%'; +select count(*) from t_test_trgm where t %> '' and t %> '%qwerty%'; reset enable_bitmapscan; create table test2(t text COLLATE "C"); diff --git a/contrib/pg_trgm/sql/pg_utf8_trgm.sql b/contrib/pg_trgm/sql/pg_utf8_trgm.sql new file mode 100644 index 0000000000000..0dd962ced8310 --- /dev/null +++ b/contrib/pg_trgm/sql/pg_utf8_trgm.sql @@ -0,0 +1,9 @@ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit +\endif + +-- Index 50 translations of the word "Mathematics" +CREATE TEMP TABLE mb (s text); +\copy mb from 'data/trgm_utf8.data' +CREATE INDEX ON mb USING gist(s gist_trgm_ops); diff --git a/contrib/pg_trgm/trgm.h b/contrib/pg_trgm/trgm.h index ca017585369ad..ca23aad4dd997 100644 --- a/contrib/pg_trgm/trgm.h +++ b/contrib/pg_trgm/trgm.h @@ -47,7 +47,7 @@ typedef char trgm[3]; } while(0) extern int (*CMPTRGM) (const void *a, const void *b); -#define ISWORDCHR(c) (t_isalnum(c)) +#define ISWORDCHR(c, len) (t_isalnum_with_len(c, len)) #define ISPRINTABLECHAR(a) ( isascii( *(unsigned char*)(a) ) && (isalnum( *(unsigned char*)(a) ) || *(unsigned char*)(a)==' ') ) #define ISPRINTABLETRGM(t) ( ISPRINTABLECHAR( ((char*)(t)) ) && ISPRINTABLECHAR( ((char*)(t))+1 ) && ISPRINTABLECHAR( ((char*)(t))+2 ) ) diff --git a/contrib/pg_trgm/trgm_gin.c b/contrib/pg_trgm/trgm_gin.c index 29a52eac7afa4..5766b3e99553a 100644 --- a/contrib/pg_trgm/trgm_gin.c +++ b/contrib/pg_trgm/trgm_gin.c @@ -51,7 +51,7 @@ gin_extract_value_trgm(PG_FUNCTION_ARGS) int32 i; *nentries = trglen; - entries = (Datum *) palloc(sizeof(Datum) * trglen); + entries = palloc_array(Datum, trglen); ptr = GETARR(trg); for (i = 0; i < trglen; i++) @@ -72,11 +72,13 @@ gin_extract_query_trgm(PG_FUNCTION_ARGS) text *val = (text *) PG_GETARG_TEXT_PP(0); int32 *nentries = (int32 *) PG_GETARG_POINTER(1); StrategyNumber strategy = PG_GETARG_UINT16(2); - - /* bool **pmatch = (bool **) PG_GETARG_POINTER(3); */ +#ifdef NOT_USED + bool **pmatch = (bool **) PG_GETARG_POINTER(3); +#endif Pointer **extra_data = (Pointer **) PG_GETARG_POINTER(4); - - /* bool **nullFlags = (bool **) PG_GETARG_POINTER(5); */ +#ifdef NOT_USED + bool **nullFlags = (bool **) PG_GETARG_POINTER(5); +#endif int32 *searchMode = (int32 *) PG_GETARG_POINTER(6); Datum *entries = NULL; TRGM *trg; @@ -97,7 +99,7 @@ gin_extract_query_trgm(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case LikeStrategyNumber: /* @@ -111,7 +113,7 @@ gin_extract_query_trgm(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case RegExpStrategyNumber: trg = createTrgmNFA(val, PG_GET_COLLATION(), &graph, CurrentMemoryContext); @@ -123,7 +125,7 @@ gin_extract_query_trgm(PG_FUNCTION_ARGS) * Pointers, but we just put the same value in each element. */ trglen = ARRNELEM(trg); - *extra_data = (Pointer *) palloc(sizeof(Pointer) * trglen); + *extra_data = palloc_array(Pointer, trglen); for (i = 0; i < trglen; i++) (*extra_data)[i] = (Pointer) graph; } @@ -146,7 +148,7 @@ gin_extract_query_trgm(PG_FUNCTION_ARGS) if (trglen > 0) { - entries = (Datum *) palloc(sizeof(Datum) * trglen); + entries = palloc_array(Datum, trglen); ptr = GETARR(trg); for (i = 0; i < trglen; i++) { @@ -171,8 +173,9 @@ gin_trgm_consistent(PG_FUNCTION_ARGS) { bool *check = (bool *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* text *query = PG_GETARG_TEXT_PP(2); */ +#ifdef NOT_USED + text *query = PG_GETARG_TEXT_PP(2); +#endif int32 nkeys = PG_GETARG_INT32(3); Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); bool *recheck = (bool *) PG_GETARG_POINTER(5); @@ -221,7 +224,7 @@ gin_trgm_consistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case LikeStrategyNumber: case EqualStrategyNumber: /* Check if all extracted trigrams are presented. */ @@ -239,7 +242,7 @@ gin_trgm_consistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case RegExpStrategyNumber: if (nkeys < 1) { @@ -247,8 +250,7 @@ gin_trgm_consistent(PG_FUNCTION_ARGS) res = true; } else - res = trigramsMatchGraph((TrgmPackedGraph *) extra_data[0], - check); + res = trigramsMatchGraph(extra_data[0], check); break; default: elog(ERROR, "unrecognized strategy number: %d", strategy); @@ -270,8 +272,9 @@ gin_trgm_triconsistent(PG_FUNCTION_ARGS) { GinTernaryValue *check = (GinTernaryValue *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* text *query = PG_GETARG_TEXT_PP(2); */ +#ifdef NOT_USED + text *query = PG_GETARG_TEXT_PP(2); +#endif int32 nkeys = PG_GETARG_INT32(3); Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); GinTernaryValue res = GIN_MAYBE; @@ -307,7 +310,7 @@ gin_trgm_triconsistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case LikeStrategyNumber: case EqualStrategyNumber: /* Check if all extracted trigrams are presented. */ @@ -325,7 +328,7 @@ gin_trgm_triconsistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case RegExpStrategyNumber: if (nkeys < 1) { @@ -339,11 +342,10 @@ gin_trgm_triconsistent(PG_FUNCTION_ARGS) * function, promoting all GIN_MAYBE keys to GIN_TRUE will * give a conservative result. */ - boolcheck = (bool *) palloc(sizeof(bool) * nkeys); + boolcheck = palloc_array(bool, nkeys); for (i = 0; i < nkeys; i++) boolcheck[i] = (check[i] != GIN_FALSE); - if (!trigramsMatchGraph((TrgmPackedGraph *) extra_data[0], - boolcheck)) + if (!trigramsMatchGraph(extra_data[0], boolcheck)) res = GIN_FALSE; pfree(boolcheck); } diff --git a/contrib/pg_trgm/trgm_gist.c b/contrib/pg_trgm/trgm_gist.c index 5ba895217b0a9..11812b2984e54 100644 --- a/contrib/pg_trgm/trgm_gist.c +++ b/contrib/pg_trgm/trgm_gist.c @@ -124,7 +124,7 @@ gtrgm_compress(PG_FUNCTION_ARGS) text *val = DatumGetTextPP(entry->key); res = generate_trgm(VARDATA_ANY(val), VARSIZE_ANY_EXHDR(val)); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, false); @@ -143,7 +143,7 @@ gtrgm_compress(PG_FUNCTION_ARGS) } res = gtrgm_alloc(true, siglen, sign); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, false); @@ -163,7 +163,7 @@ gtrgm_decompress(PG_FUNCTION_ARGS) if (key != (text *) DatumGetPointer(entry->key)) { /* need to pass back the decompressed item */ - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, entry->offset, entry->leafkey); PG_RETURN_POINTER(retval); @@ -199,8 +199,9 @@ gtrgm_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); text *query = PG_GETARG_TEXT_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int siglen = GET_SIGLEN(); TRGM *key = (TRGM *) DatumGetPointer(entry->key); @@ -247,7 +248,7 @@ gtrgm_consistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case LikeStrategyNumber: qtrg = generate_wildcard_trgm(VARDATA(query), querysize - VARHDRSZ); @@ -256,7 +257,7 @@ gtrgm_consistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case RegExpStrategyNumber: qtrg = createTrgmNFA(query, PG_GET_COLLATION(), &graph, fcinfo->flinfo->fn_mcxt); @@ -344,7 +345,7 @@ gtrgm_consistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case LikeStrategyNumber: case EqualStrategyNumber: /* Wildcard and equal search are inexact */ @@ -386,7 +387,7 @@ gtrgm_consistent(PG_FUNCTION_ARGS) #ifndef IGNORECASE elog(ERROR, "cannot handle ~* with case-sensitive trigrams"); #endif - /* FALL THRU */ + pg_fallthrough; case RegExpStrategyNumber: /* Regexp search is inexact */ *recheck = true; @@ -454,8 +455,9 @@ gtrgm_distance(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); text *query = PG_GETARG_TEXT_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); int siglen = GET_SIGLEN(); TRGM *key = (TRGM *) DatumGetPointer(entry->key); @@ -699,10 +701,13 @@ gtrgm_penalty(PG_FUNCTION_ARGS) if (ISARRKEY(newval)) { char *cache = (char *) fcinfo->flinfo->fn_extra; - TRGM *cachedVal = (TRGM *) (cache + MAXALIGN(siglen)); + TRGM *cachedVal = NULL; Size newvalsize = VARSIZE(newval); BITVECP sign; + if (cache != NULL) + cachedVal = (TRGM *) (cache + MAXALIGN(siglen)); + /* * Cache the sign data across multiple calls with the same newval. */ @@ -820,7 +825,7 @@ gtrgm_picksplit(PG_FUNCTION_ARGS) SPLITCOST *costvector; /* cache the sign data for each existing item */ - cache = (CACHESIGN *) palloc(sizeof(CACHESIGN) * (maxoff + 1)); + cache = palloc_array(CACHESIGN, maxoff + 1); cache_sign = palloc(siglen * (maxoff + 1)); for (k = FirstOffsetNumber; k <= maxoff; k = OffsetNumberNext(k)) @@ -864,7 +869,7 @@ gtrgm_picksplit(PG_FUNCTION_ARGS) union_r = GETSIGN(datum_r); /* sort before ... */ - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) { costvector[j - 1].pos = j; diff --git a/contrib/pg_trgm/trgm_op.c b/contrib/pg_trgm/trgm_op.c index 29b39ec8a4c25..3fd087852e782 100644 --- a/contrib/pg_trgm/trgm_op.c +++ b/contrib/pg_trgm/trgm_op.c @@ -66,6 +66,78 @@ typedef uint8 TrgmBound; #define WORD_SIMILARITY_STRICT 0x02 /* force bounds of extent to match * word bounds */ +/* + * A growable array of trigrams + * + * The actual array of trigrams is in 'datum'. Note that the other fields in + * 'datum', i.e. datum->flags and the varlena length, are not kept up to date + * when items are added to the growable array. We merely reserve the space + * for them here. You must fill those other fields before using 'datum' as a + * proper TRGM datum. + */ +typedef struct +{ + TRGM *datum; /* trigram array */ + int length; /* number of trigrams in the array */ + int allocated; /* allocated size of 'datum' (# of trigrams) */ +} growable_trgm_array; + +/* + * Allocate a new growable array. + * + * 'slen' is the size of the source string that we're extracting the trigrams + * from. It is used to choose the initial size of the array. + */ +static void +init_trgm_array(growable_trgm_array *arr, int slen) +{ + size_t init_size; + + /* + * In the extreme case, the input string consists entirely of one + * character words, like "a b c", where each word is expanded to two + * trigrams. This is not a strict upper bound though, because when + * IGNORECASE is defined, we convert the input string to lowercase before + * extracting the trigrams, which in rare cases can expand one input + * character into multiple characters. + */ + init_size = (size_t) slen + 1; + + /* + * Guard against possible overflow in the palloc request. (We don't worry + * about the additive constants, since palloc can detect requests that are + * a little above MaxAllocSize --- we just need to prevent integer + * overflow in the multiplications.) + */ + if (init_size > MaxAllocSize / sizeof(trgm)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("out of memory"))); + + arr->datum = palloc(CALCGTSIZE(ARRKEY, init_size)); + arr->allocated = init_size; + arr->length = 0; +} + +/* Make sure the array can hold at least 'needed' more trigrams */ +static void +enlarge_trgm_array(growable_trgm_array *arr, int needed) +{ + size_t new_needed = (size_t) arr->length + needed; + + if (new_needed > arr->allocated) + { + /* Guard against possible overflow, like in init_trgm_array */ + if (new_needed > MaxAllocSize / sizeof(trgm)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("out of memory"))); + + arr->datum = repalloc(arr->datum, CALCGTSIZE(ARRKEY, new_needed)); + arr->allocated = new_needed; + } +} + /* * Module load callback */ @@ -154,6 +226,55 @@ CMPTRGM_CHOOSE(const void *a, const void *b) return CMPTRGM(a, b); } +#define ST_SORT trigram_qsort_signed +#define ST_ELEMENT_TYPE_VOID +#define ST_COMPARE(a, b) CMPTRGM_SIGNED(a, b) +#define ST_SCOPE static +#define ST_DEFINE +#define ST_DECLARE +#include "lib/sort_template.h" + +#define ST_SORT trigram_qsort_unsigned +#define ST_ELEMENT_TYPE_VOID +#define ST_COMPARE(a, b) CMPTRGM_UNSIGNED(a, b) +#define ST_SCOPE static +#define ST_DEFINE +#define ST_DECLARE +#include "lib/sort_template.h" + +/* Sort an array of trigrams, handling signedness correctly */ +static void +trigram_qsort(trgm *array, size_t n) +{ + if (GetDefaultCharSignedness()) + trigram_qsort_signed(array, n, sizeof(trgm)); + else + trigram_qsort_unsigned(array, n, sizeof(trgm)); +} + + +/* + * Compare two trigrams for equality. This has the same signature as + * comparison functions used for sorting, so that this can be used with + * qunique(). This doesn't need separate versions for "signed char" and " + * unsigned char" because equality is the same for both. + */ +static inline int +CMPTRGM_EQ(const void *a, const void *b) +{ + char *aa = (char *) a; + char *bb = (char *) b; + + return aa[0] != bb[0] || aa[1] != bb[1] || aa[2] != bb[2] ? 1 : 0; +} + +/* Deduplicate an array of trigrams */ +static size_t +trigram_qunique(trgm *array, size_t n) +{ + return qunique(array, n, sizeof(trgm), CMPTRGM_EQ); +} + /* * Deprecated function. * Use "pg_trgm.similarity_threshold" GUC variable instead of this function. @@ -209,33 +330,36 @@ show_limit(PG_FUNCTION_ARGS) PG_RETURN_FLOAT4(similarity_threshold); } -static int -comp_trgm(const void *a, const void *b) -{ - return CMPTRGM(a, b); -} - /* * Finds first word in string, returns pointer to the word, * endword points to the character after word */ static char * -find_word(char *str, int lenstr, char **endword, int *charlen) +find_word(char *str, int lenstr, char **endword) { char *beginword = str; + const char *endstr = str + lenstr; - while (beginword - str < lenstr && !ISWORDCHR(beginword)) - beginword += pg_mblen(beginword); + while (beginword < endstr) + { + int clen = pg_mblen_range(beginword, endstr); - if (beginword - str >= lenstr) + if (ISWORDCHR(beginword, clen)) + break; + beginword += clen; + } + + if (beginword >= endstr) return NULL; *endword = beginword; - *charlen = 0; - while (*endword - str < lenstr && ISWORDCHR(*endword)) + while (*endword < endstr) { - *endword += pg_mblen(*endword); - (*charlen)++; + int clen = pg_mblen_range(*endword, endstr); + + if (!ISWORDCHR(*endword, clen)) + break; + *endword += clen; } return beginword; @@ -269,78 +393,138 @@ compact_trigram(trgm *tptr, char *str, int bytelen) } /* - * Adds trigrams from words (already padded). + * Adds trigrams from the word in 'str' (already padded if necessary). */ -static trgm * -make_trigrams(trgm *tptr, char *str, int bytelen, int charlen) +static void +make_trigrams(growable_trgm_array *dst, char *str, int bytelen) { + trgm *tptr; char *ptr = str; - if (charlen < 3) - return tptr; + if (bytelen < 3) + return; - if (bytelen > charlen) - { - /* Find multibyte character boundaries and apply compact_trigram */ - int lenfirst = pg_mblen(str), - lenmiddle = pg_mblen(str + lenfirst), - lenlast = pg_mblen(str + lenfirst + lenmiddle); + /* max number of trigrams = strlen - 2 */ + enlarge_trgm_array(dst, bytelen - 2); + tptr = GETARR(dst->datum) + dst->length; - while ((ptr - str) + lenfirst + lenmiddle + lenlast <= bytelen) + if (pg_encoding_max_length(GetDatabaseEncoding()) == 1) + { + while (ptr < str + bytelen - 2) { - compact_trigram(tptr, ptr, lenfirst + lenmiddle + lenlast); - - ptr += lenfirst; + CPTRGM(tptr, ptr); + ptr++; tptr++; - - lenfirst = lenmiddle; - lenmiddle = lenlast; - lenlast = pg_mblen(ptr + lenfirst + lenmiddle); } } else { - /* Fast path when there are no multibyte characters */ - Assert(bytelen == charlen); + int lenfirst, + lenmiddle, + lenlast; + char *endptr; - while (ptr - str < bytelen - 2 /* number of trigrams = strlen - 2 */ ) + /* + * Fast path as long as there are no multibyte characters + */ + if (!IS_HIGHBIT_SET(ptr[0]) && !IS_HIGHBIT_SET(ptr[1])) { - CPTRGM(tptr, ptr); - ptr++; + while (!IS_HIGHBIT_SET(ptr[2])) + { + CPTRGM(tptr, ptr); + ptr++; + tptr++; + + if (ptr == str + bytelen - 2) + goto done; + } + + lenfirst = 1; + lenmiddle = 1; + lenlast = pg_mblen_unbounded(ptr + 2); + } + else + { + lenfirst = pg_mblen_unbounded(ptr); + if (ptr + lenfirst >= str + bytelen) + goto done; + lenmiddle = pg_mblen_unbounded(ptr + lenfirst); + if (ptr + lenfirst + lenmiddle >= str + bytelen) + goto done; + lenlast = pg_mblen_unbounded(ptr + lenfirst + lenmiddle); + } + + /* + * Slow path to handle any remaining multibyte characters + * + * As we go, 'ptr' points to the beginning of the current + * three-character string and 'endptr' points to just past it. + */ + endptr = ptr + lenfirst + lenmiddle + lenlast; + while (endptr <= str + bytelen) + { + compact_trigram(tptr, ptr, endptr - ptr); tptr++; + + /* Advance to the next character */ + if (endptr == str + bytelen) + break; + ptr += lenfirst; + lenfirst = lenmiddle; + lenmiddle = lenlast; + lenlast = pg_mblen_unbounded(endptr); + endptr += lenlast; } } - return tptr; +done: + dst->length = tptr - GETARR(dst->datum); + Assert(dst->length <= dst->allocated); } /* * Make array of trigrams without sorting and removing duplicate items. * - * trg: where to return the array of trigrams. + * dst: where to return the array of trigrams. * str: source string, of length slen bytes. - * bounds: where to return bounds of trigrams (if needed). - * - * Returns length of the generated array. + * bounds_p: where to return bounds of trigrams (if needed). */ -static int -generate_trgm_only(trgm *trg, char *str, int slen, TrgmBound *bounds) +static void +generate_trgm_only(growable_trgm_array *dst, char *str, int slen, TrgmBound **bounds_p) { - trgm *tptr; + size_t buflen; char *buf; - int charlen, - bytelen; + int bytelen; char *bword, *eword; + TrgmBound *bounds = NULL; + int bounds_allocated = 0; - if (slen + LPADDING + RPADDING < 3 || slen == 0) - return 0; + init_trgm_array(dst, slen); - tptr = trg; + /* + * If requested, allocate an array for the bounds, with the same size as + * the trigram array. + */ + if (bounds_p) + { + bounds_allocated = dst->allocated; + bounds = *bounds_p = palloc0_array(TrgmBound, bounds_allocated); + } - /* Allocate a buffer for case-folded, blank-padded words */ - buf = (char *) palloc(slen * pg_database_encoding_max_length() + 4); + if (slen + LPADDING + RPADDING < 3 || slen == 0) + return; + /* + * Allocate a buffer for case-folded, blank-padded words. + * + * As an initial guess, allocate a buffer large enough to hold the + * original string with padding, which is always enough when compiled with + * !IGNORECASE. If the case-folding produces a string longer than the + * original, we'll grow the buffer. + */ + buflen = (size_t) slen + 4; + buf = (char *) palloc(buflen); if (LPADDING > 0) { *buf = ' '; @@ -349,52 +533,59 @@ generate_trgm_only(trgm *trg, char *str, int slen, TrgmBound *bounds) } eword = str; - while ((bword = find_word(eword, slen - (eword - str), &eword, &charlen)) != NULL) + while ((bword = find_word(eword, slen - (eword - str), &eword)) != NULL) { + int oldlen; + + /* Convert word to lower case before extracting trigrams from it */ #ifdef IGNORECASE - bword = str_tolower(bword, eword - bword, DEFAULT_COLLATION_OID); - bytelen = strlen(bword); + { + char *lowered; + + lowered = str_tolower(bword, eword - bword, DEFAULT_COLLATION_OID); + bytelen = strlen(lowered); + + /* grow the buffer if necessary */ + if (bytelen > buflen - 4) + { + pfree(buf); + buflen = (size_t) bytelen + 4; + buf = (char *) palloc(buflen); + if (LPADDING > 0) + { + *buf = ' '; + if (LPADDING > 1) + *(buf + 1) = ' '; + } + } + memcpy(buf + LPADDING, lowered, bytelen); + pfree(lowered); + } #else bytelen = eword - bword; -#endif - memcpy(buf + LPADDING, bword, bytelen); - -#ifdef IGNORECASE - pfree(bword); #endif buf[LPADDING + bytelen] = ' '; buf[LPADDING + bytelen + 1] = ' '; /* Calculate trigrams marking their bounds if needed */ + oldlen = dst->length; + make_trigrams(dst, buf, bytelen + LPADDING + RPADDING); if (bounds) - bounds[tptr - trg] |= TRGM_BOUND_LEFT; - tptr = make_trigrams(tptr, buf, bytelen + LPADDING + RPADDING, - charlen + LPADDING + RPADDING); - if (bounds) - bounds[tptr - trg - 1] |= TRGM_BOUND_RIGHT; + { + if (bounds_allocated < dst->length) + { + bounds = *bounds_p = repalloc0_array(bounds, TrgmBound, bounds_allocated, dst->allocated); + bounds_allocated = dst->allocated; + } + + bounds[oldlen] |= TRGM_BOUND_LEFT; + bounds[dst->length - 1] |= TRGM_BOUND_RIGHT; + } } pfree(buf); - - return tptr - trg; -} - -/* - * Guard against possible overflow in the palloc requests below. (We - * don't worry about the additive constants, since palloc can detect - * requests that are a little above MaxAllocSize --- we just need to - * prevent integer overflow in the multiplications.) - */ -static void -protect_out_of_mem(int slen) -{ - if ((Size) (slen / 2) >= (MaxAllocSize / (sizeof(trgm) * 3)) || - (Size) slen >= (MaxAllocSize / pg_database_encoding_max_length())) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("out of memory"))); } /* @@ -408,26 +599,21 @@ TRGM * generate_trgm(char *str, int slen) { TRGM *trg; + growable_trgm_array arr; int len; - protect_out_of_mem(slen); - - trg = (TRGM *) palloc(TRGMHDRSIZE + sizeof(trgm) * (slen / 2 + 1) * 3); + generate_trgm_only(&arr, str, slen, NULL); + len = arr.length; + trg = arr.datum; trg->flag = ARRKEY; - len = generate_trgm_only(GETARR(trg), str, slen, NULL); - SET_VARSIZE(trg, CALCGTSIZE(ARRKEY, len)); - - if (len == 0) - return trg; - /* * Make trigrams unique. */ if (len > 1) { - qsort(GETARR(trg), len, sizeof(trgm), comp_trgm); - len = qunique(GETARR(trg), len, sizeof(trgm), comp_trgm); + trigram_qsort(GETARR(trg), len); + len = trigram_qunique(GETARR(trg), len); } SET_VARSIZE(trg, CALCGTSIZE(ARRKEY, len)); @@ -452,7 +638,7 @@ make_positional_trgm(trgm *trg1, int len1, trgm *trg2, int len2) int i, len = len1 + len2; - result = (pos_trgm *) palloc(sizeof(pos_trgm) * len); + result = palloc_array(pos_trgm, len); for (i = 0; i < len1; i++) { @@ -535,7 +721,7 @@ iterate_word_similarity(int *trg2indexes, lower = (flags & WORD_SIMILARITY_STRICT) ? 0 : -1; /* Memorise last position of each trigram */ - lastpos = (int *) palloc(sizeof(int) * len); + lastpos = palloc_array(int, len); memset(lastpos, -1, sizeof(int) * len); for (i = 0; i < len2; i++) @@ -675,8 +861,8 @@ calc_word_similarity(char *str1, int slen1, char *str2, int slen2, { bool *found; pos_trgm *ptrg; - trgm *trg1; - trgm *trg2; + growable_trgm_array trg1; + growable_trgm_array trg2; int len1, len2, len, @@ -685,34 +871,28 @@ calc_word_similarity(char *str1, int slen1, char *str2, int slen2, ulen1; int *trg2indexes; float4 result; - TrgmBound *bounds; - - protect_out_of_mem(slen1 + slen2); + TrgmBound *bounds = NULL; /* Make positional trigrams */ - trg1 = (trgm *) palloc(sizeof(trgm) * (slen1 / 2 + 1) * 3); - trg2 = (trgm *) palloc(sizeof(trgm) * (slen2 / 2 + 1) * 3); - if (flags & WORD_SIMILARITY_STRICT) - bounds = (TrgmBound *) palloc0(sizeof(TrgmBound) * (slen2 / 2 + 1) * 3); - else - bounds = NULL; - len1 = generate_trgm_only(trg1, str1, slen1, NULL); - len2 = generate_trgm_only(trg2, str2, slen2, bounds); + generate_trgm_only(&trg1, str1, slen1, NULL); + len1 = trg1.length; + generate_trgm_only(&trg2, str2, slen2, (flags & WORD_SIMILARITY_STRICT) ? &bounds : NULL); + len2 = trg2.length; - ptrg = make_positional_trgm(trg1, len1, trg2, len2); + ptrg = make_positional_trgm(GETARR(trg1.datum), len1, GETARR(trg2.datum), len2); len = len1 + len2; qsort(ptrg, len, sizeof(pos_trgm), comp_ptrgm); - pfree(trg1); - pfree(trg2); + pfree(trg1.datum); + pfree(trg2.datum); /* * Merge positional trigrams array: enumerate each trigram and find its * presence in required word. */ - trg2indexes = (int *) palloc(sizeof(int) * len2); - found = (bool *) palloc0(sizeof(bool) * len); + trg2indexes = palloc_array(int, len2); + found = palloc0_array(bool, len); ulen1 = 0; j = 0; @@ -761,20 +941,20 @@ calc_word_similarity(char *str1, int slen1, char *str2, int slen2, * str: source string, of length lenstr bytes (need not be null-terminated) * buf: where to return the substring (must be long enough) * *bytelen: receives byte length of the found substring - * *charlen: receives character length of the found substring * * Returns pointer to end+1 of the found substring in the source string. - * Returns NULL if no word found (in which case buf, bytelen, charlen not set) + * Returns NULL if no word found (in which case buf, bytelen is not set) * * If the found word is bounded by non-word characters or string boundaries * then this function will include corresponding padding spaces into buf. */ static const char * get_wildcard_part(const char *str, int lenstr, - char *buf, int *bytelen, int *charlen) + char *buf, int *bytelen) { const char *beginword = str; const char *endword; + const char *endstr = str + lenstr; char *s = buf; bool in_leading_wildcard_meta = false; bool in_trailing_wildcard_meta = false; @@ -787,11 +967,13 @@ get_wildcard_part(const char *str, int lenstr, * from this loop to the next one, since we may exit at a word character * that is in_escape. */ - while (beginword - str < lenstr) + while (beginword < endstr) { + clen = pg_mblen_range(beginword, endstr); + if (in_escape) { - if (ISWORDCHR(beginword)) + if (ISWORDCHR(beginword, clen)) break; in_escape = false; in_leading_wildcard_meta = false; @@ -802,12 +984,12 @@ get_wildcard_part(const char *str, int lenstr, in_escape = true; else if (ISWILDCARDCHAR(beginword)) in_leading_wildcard_meta = true; - else if (ISWORDCHR(beginword)) + else if (ISWORDCHR(beginword, clen)) break; else in_leading_wildcard_meta = false; } - beginword += pg_mblen(beginword); + beginword += clen; } /* @@ -820,18 +1002,13 @@ get_wildcard_part(const char *str, int lenstr, * Add left padding spaces if preceding character wasn't wildcard * meta-character. */ - *charlen = 0; if (!in_leading_wildcard_meta) { if (LPADDING > 0) { *s++ = ' '; - (*charlen)++; if (LPADDING > 1) - { *s++ = ' '; - (*charlen)++; - } } } @@ -840,15 +1017,14 @@ get_wildcard_part(const char *str, int lenstr, * string boundary. Strip escapes during copy. */ endword = beginword; - while (endword - str < lenstr) + while (endword < endstr) { - clen = pg_mblen(endword); + clen = pg_mblen_range(endword, endstr); if (in_escape) { - if (ISWORDCHR(endword)) + if (ISWORDCHR(endword, clen)) { memcpy(s, endword, clen); - (*charlen)++; s += clen; } else @@ -873,10 +1049,9 @@ get_wildcard_part(const char *str, int lenstr, in_trailing_wildcard_meta = true; break; } - else if (ISWORDCHR(endword)) + else if (ISWORDCHR(endword, clen)) { memcpy(s, endword, clen); - (*charlen)++; s += clen; } else @@ -894,12 +1069,8 @@ get_wildcard_part(const char *str, int lenstr, if (RPADDING > 0) { *s++ = ' '; - (*charlen)++; if (RPADDING > 1) - { *s++ = ' '; - (*charlen)++; - } } } @@ -918,66 +1089,65 @@ TRGM * generate_wildcard_trgm(const char *str, int slen) { TRGM *trg; - char *buf, - *buf2; - trgm *tptr; + growable_trgm_array arr; + char *buf; int len, - charlen, bytelen; const char *eword; - protect_out_of_mem(slen); - - trg = (TRGM *) palloc(TRGMHDRSIZE + sizeof(trgm) * (slen / 2 + 1) * 3); - trg->flag = ARRKEY; - SET_VARSIZE(trg, TRGMHDRSIZE); - if (slen + LPADDING + RPADDING < 3 || slen == 0) + { + trg = (TRGM *) palloc(TRGMHDRSIZE); + trg->flag = ARRKEY; + SET_VARSIZE(trg, TRGMHDRSIZE); return trg; + } - tptr = GETARR(trg); + init_trgm_array(&arr, slen); /* Allocate a buffer for blank-padded, but not yet case-folded, words */ - buf = palloc(sizeof(char) * (slen + 4)); + buf = palloc_array(char, slen + 4); /* * Extract trigrams from each substring extracted by get_wildcard_part. */ eword = str; while ((eword = get_wildcard_part(eword, slen - (eword - str), - buf, &bytelen, &charlen)) != NULL) + buf, &bytelen)) != NULL) { + char *word; + #ifdef IGNORECASE - buf2 = str_tolower(buf, bytelen, DEFAULT_COLLATION_OID); - bytelen = strlen(buf2); + word = str_tolower(buf, bytelen, DEFAULT_COLLATION_OID); + bytelen = strlen(word); #else - buf2 = buf; + word = buf; #endif /* * count trigrams */ - tptr = make_trigrams(tptr, buf2, bytelen, charlen); + make_trigrams(&arr, word, bytelen); #ifdef IGNORECASE - pfree(buf2); + pfree(word); #endif } pfree(buf); - if ((len = tptr - GETARR(trg)) == 0) - return trg; - /* * Make trigrams unique. */ + trg = arr.datum; + len = arr.length; if (len > 1) { - qsort(GETARR(trg), len, sizeof(trgm), comp_trgm); - len = qunique(GETARR(trg), len, sizeof(trgm), comp_trgm); + trigram_qsort(GETARR(trg), len); + len = trigram_qunique(GETARR(trg), len); } + trg->flag = ARRKEY; SET_VARSIZE(trg, CALCGTSIZE(ARRKEY, len)); return trg; @@ -1008,7 +1178,7 @@ show_trgm(PG_FUNCTION_ARGS) int i; trg = generate_trgm(VARDATA_ANY(in), VARSIZE_ANY_EXHDR(in)); - d = (Datum *) palloc(sizeof(Datum) * (1 + ARRNELEM(trg))); + d = palloc_array(Datum, 1 + ARRNELEM(trg)); for (i = 0, ptr = GETARR(trg); i < ARRNELEM(trg); i++, ptr++) { @@ -1136,7 +1306,7 @@ trgm_presence_map(TRGM *query, TRGM *key) lenk = ARRNELEM(key), i; - result = (bool *) palloc0(lenq * sizeof(bool)); + result = palloc0_array(bool, lenq); /* for each query trigram, do a binary search in the key array */ for (i = 0; i < lenq; i++) diff --git a/contrib/pg_trgm/trgm_regexp.c b/contrib/pg_trgm/trgm_regexp.c index 149f9eb259c01..b4eaeec60902b 100644 --- a/contrib/pg_trgm/trgm_regexp.c +++ b/contrib/pg_trgm/trgm_regexp.c @@ -181,7 +181,7 @@ * 7) Mark state 3 final because state 5 of source NFA is marked as final. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -483,7 +483,7 @@ static TRGM *createTrgmNFAInternal(regex_t *regex, TrgmPackedGraph **graph, static void RE_compile(regex_t *regex, text *text_re, int cflags, Oid collation); static void getColorInfo(regex_t *regex, TrgmNFA *trgmNFA); -static bool convertPgWchar(pg_wchar c, trgm_mb_char *result); +static int convertPgWchar(pg_wchar c, trgm_mb_char *result); static void transformGraph(TrgmNFA *trgmNFA); static void processState(TrgmNFA *trgmNFA, TrgmState *state); static void addKey(TrgmNFA *trgmNFA, TrgmState *state, TrgmStateKey *key); @@ -791,12 +791,11 @@ getColorInfo(regex_t *regex, TrgmNFA *trgmNFA) colorInfo->expandable = true; colorInfo->containsNonWord = false; - colorInfo->wordChars = (trgm_mb_char *) - palloc(sizeof(trgm_mb_char) * charsCount); + colorInfo->wordChars = palloc_array(trgm_mb_char, charsCount); colorInfo->wordCharsCount = 0; /* Extract all the chars in this color */ - chars = (pg_wchar *) palloc(sizeof(pg_wchar) * charsCount); + chars = palloc_array(pg_wchar, charsCount); pg_reg_getcharacters(regex, i, chars, charsCount); /* @@ -808,10 +807,11 @@ getColorInfo(regex_t *regex, TrgmNFA *trgmNFA) for (j = 0; j < charsCount; j++) { trgm_mb_char c; + int clen = convertPgWchar(chars[j], &c); - if (!convertPgWchar(chars[j], &c)) + if (!clen) continue; /* ok to ignore it altogether */ - if (ISWORDCHR(c.bytes)) + if (ISWORDCHR(c.bytes, clen)) colorInfo->wordChars[colorInfo->wordCharsCount++] = c; else colorInfo->containsNonWord = true; @@ -823,13 +823,15 @@ getColorInfo(regex_t *regex, TrgmNFA *trgmNFA) /* * Convert pg_wchar to multibyte format. - * Returns false if the character should be ignored completely. + * Returns 0 if the character should be ignored completely, else returns its + * byte length. */ -static bool +static int convertPgWchar(pg_wchar c, trgm_mb_char *result) { /* "s" has enough space for a multibyte character and a trailing NUL */ char s[MAX_MULTIBYTE_CHAR_LEN + 1]; + int clen; /* * We can ignore the NUL character, since it can never appear in a PG text @@ -837,11 +839,11 @@ convertPgWchar(pg_wchar c, trgm_mb_char *result) * reconstructing trigrams. */ if (c == 0) - return false; + return 0; /* Do the conversion, making sure the result is NUL-terminated */ memset(s, 0, sizeof(s)); - pg_wchar2mb_with_len(&c, s, 1); + clen = pg_wchar2mb_with_len(&c, s, 1); /* * In IGNORECASE mode, we can ignore uppercase characters. We assume that @@ -858,12 +860,12 @@ convertPgWchar(pg_wchar c, trgm_mb_char *result) */ #ifdef IGNORECASE { - char *lowerCased = str_tolower(s, strlen(s), DEFAULT_COLLATION_OID); + char *lowerCased = str_tolower(s, clen, DEFAULT_COLLATION_OID); if (strcmp(lowerCased, s) != 0) { pfree(lowerCased); - return false; + return 0; } pfree(lowerCased); } @@ -871,7 +873,7 @@ convertPgWchar(pg_wchar c, trgm_mb_char *result) /* Fill result with exactly MAX_MULTIBYTE_CHAR_LEN bytes */ memcpy(result->bytes, s, MAX_MULTIBYTE_CHAR_LEN); - return true; + return clen; } @@ -1063,7 +1065,7 @@ addKey(TrgmNFA *trgmNFA, TrgmState *state, TrgmStateKey *key) * original NFA. */ arcsCount = pg_reg_getnumoutarcs(trgmNFA->regex, key->nstate); - arcs = (regex_arc_t *) palloc(sizeof(regex_arc_t) * arcsCount); + arcs = palloc_array(regex_arc_t, arcsCount); pg_reg_getoutarcs(trgmNFA->regex, key->nstate, arcs, arcsCount); for (i = 0; i < arcsCount; i++) @@ -1177,7 +1179,7 @@ addKey(TrgmNFA *trgmNFA, TrgmState *state, TrgmStateKey *key) static void addKeyToQueue(TrgmNFA *trgmNFA, TrgmStateKey *key) { - TrgmStateKey *keyCopy = (TrgmStateKey *) palloc(sizeof(TrgmStateKey)); + TrgmStateKey *keyCopy = palloc_object(TrgmStateKey); memcpy(keyCopy, key, sizeof(TrgmStateKey)); trgmNFA->keysQueue = lappend(trgmNFA->keysQueue, keyCopy); @@ -1215,7 +1217,7 @@ addArcs(TrgmNFA *trgmNFA, TrgmState *state) TrgmStateKey *key = (TrgmStateKey *) lfirst(cell); arcsCount = pg_reg_getnumoutarcs(trgmNFA->regex, key->nstate); - arcs = (regex_arc_t *) palloc(sizeof(regex_arc_t) * arcsCount); + arcs = palloc_array(regex_arc_t, arcsCount); pg_reg_getoutarcs(trgmNFA->regex, key->nstate, arcs, arcsCount); for (i = 0; i < arcsCount; i++) @@ -1311,7 +1313,7 @@ addArc(TrgmNFA *trgmNFA, TrgmState *state, TrgmStateKey *key, } /* Checks were successful, add new arc */ - arc = (TrgmArc *) palloc(sizeof(TrgmArc)); + arc = palloc_object(TrgmArc); arc->target = getState(trgmNFA, destKey); arc->ctrgm.colors[0] = key->prefix.colors[0]; arc->ctrgm.colors[1] = key->prefix.colors[1]; @@ -1467,7 +1469,7 @@ selectColorTrigrams(TrgmNFA *trgmNFA) int cnumber; /* Collect color trigrams from all arcs */ - colorTrgms = (ColorTrgmInfo *) palloc0(sizeof(ColorTrgmInfo) * arcsCount); + colorTrgms = palloc0_array(ColorTrgmInfo, arcsCount); trgmNFA->colorTrgms = colorTrgms; i = 0; @@ -1479,7 +1481,7 @@ selectColorTrigrams(TrgmNFA *trgmNFA) foreach(cell, state->arcs) { TrgmArc *arc = (TrgmArc *) lfirst(cell); - TrgmArcInfo *arcInfo = (TrgmArcInfo *) palloc(sizeof(TrgmArcInfo)); + TrgmArcInfo *arcInfo = palloc_object(TrgmArcInfo); ColorTrgmInfo *trgmInfo = &colorTrgms[i]; arcInfo->source = state; @@ -1964,8 +1966,7 @@ packGraph(TrgmNFA *trgmNFA, MemoryContext rcontext) } /* Collect array of all arcs */ - arcs = (TrgmPackArcInfo *) - palloc(sizeof(TrgmPackArcInfo) * trgmNFA->arcsCount); + arcs = palloc_array(TrgmPackArcInfo, trgmNFA->arcsCount); arcIndex = 0; hash_seq_init(&scan_status, trgmNFA->states); while ((state = (TrgmState *) hash_seq_search(&scan_status)) != NULL) @@ -2128,18 +2129,15 @@ printSourceNFA(regex_t *regex, TrgmColorInfo *colors, int ncolors) { StringInfoData buf; int nstates = pg_reg_getnumstates(regex); - int state; - int i; initStringInfo(&buf); appendStringInfoString(&buf, "\ndigraph sourceNFA {\n"); - for (state = 0; state < nstates; state++) + for (int state = 0; state < nstates; state++) { regex_arc_t *arcs; - int i, - arcsCount; + int arcsCount; appendStringInfo(&buf, "s%d", state); if (pg_reg_getfinalstate(regex) == state) @@ -2147,10 +2145,10 @@ printSourceNFA(regex_t *regex, TrgmColorInfo *colors, int ncolors) appendStringInfoString(&buf, ";\n"); arcsCount = pg_reg_getnumoutarcs(regex, state); - arcs = (regex_arc_t *) palloc(sizeof(regex_arc_t) * arcsCount); + arcs = palloc_array(regex_arc_t, arcsCount); pg_reg_getoutarcs(regex, state, arcs, arcsCount); - for (i = 0; i < arcsCount; i++) + for (int i = 0; i < arcsCount; i++) { appendStringInfo(&buf, " s%d -> s%d [label = \"%d\"];\n", state, arcs[i].to, arcs[i].co); @@ -2167,15 +2165,14 @@ printSourceNFA(regex_t *regex, TrgmColorInfo *colors, int ncolors) appendStringInfoString(&buf, " { rank = sink;\n"); appendStringInfoString(&buf, " Colors [shape = none, margin=0, label=<\n"); - for (i = 0; i < ncolors; i++) + for (int i = 0; i < ncolors; i++) { TrgmColorInfo *color = &colors[i]; - int j; appendStringInfo(&buf, "
Color %d: ", i); if (color->expandable) { - for (j = 0; j < color->wordCharsCount; j++) + for (int j = 0; j < color->wordCharsCount; j++) { char s[MAX_MULTIBYTE_CHAR_LEN + 1]; diff --git a/contrib/pg_visibility/Makefile b/contrib/pg_visibility/Makefile index d3cb411cc90d9..e5a74f32c486d 100644 --- a/contrib/pg_visibility/Makefile +++ b/contrib/pg_visibility/Makefile @@ -10,6 +10,7 @@ DATA = pg_visibility--1.1.sql pg_visibility--1.1--1.2.sql \ pg_visibility--1.0--1.1.sql PGFILEDESC = "pg_visibility - page visibility information" +EXTRA_INSTALL = contrib/pageinspect REGRESS = pg_visibility TAP_TESTS = 1 diff --git a/contrib/pg_visibility/expected/pg_visibility.out b/contrib/pg_visibility/expected/pg_visibility.out index 09fa5933a35b2..d26f0ab7589f9 100644 --- a/contrib/pg_visibility/expected/pg_visibility.out +++ b/contrib/pg_visibility/expected/pg_visibility.out @@ -1,4 +1,5 @@ CREATE EXTENSION pg_visibility; +CREATE EXTENSION pageinspect; -- -- recently-dropped table -- @@ -204,6 +205,49 @@ select pg_truncate_visibility_map('test_partition'); (1 row) +-- test the case where vacuum phase I does not need to modify the heap buffer +-- and only needs to set the VM +create temp table test_vac_unmodified_heap(a int); +insert into test_vac_unmodified_heap values (1); +vacuum (freeze) test_vac_unmodified_heap; +select pg_visibility_map_summary('test_vac_unmodified_heap'); + pg_visibility_map_summary +--------------------------- + (1,1) +(1 row) + +-- the checkpoint cleans the buffer dirtied by freezing the sole tuple +checkpoint; +-- truncating the VM ensures that the next vacuum will need to set it +select pg_truncate_visibility_map('test_vac_unmodified_heap'); + pg_truncate_visibility_map +---------------------------- + +(1 row) + +select pg_visibility_map_summary('test_vac_unmodified_heap'); + pg_visibility_map_summary +--------------------------- + (0,0) +(1 row) + +-- though the VM is truncated, the heap page-level visibility hint, +-- PD_ALL_VISIBLE should still be set +SELECT (flags & x'0004'::int) <> 0 + FROM page_header(get_raw_page('test_vac_unmodified_heap', 0)); + ?column? +---------- + t +(1 row) + +-- vacuum sets the VM +vacuum test_vac_unmodified_heap; +select pg_visibility_map_summary('test_vac_unmodified_heap'); + pg_visibility_map_summary +--------------------------- + (1,1) +(1 row) + -- test copy freeze create table copyfreeze (a int, b char(1500)); -- load all rows via COPY FREEZE and ensure that all pages are set all-visible diff --git a/contrib/pg_visibility/meson.build b/contrib/pg_visibility/meson.build index 1b14cf037c3f1..8a17050f2ac52 100644 --- a/contrib/pg_visibility/meson.build +++ b/contrib/pg_visibility/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_visibility_sources = files( 'pg_visibility.c', diff --git a/contrib/pg_visibility/pg_visibility.c b/contrib/pg_visibility/pg_visibility.c index d79ef35006bfa..d564bd2a00c7d 100644 --- a/contrib/pg_visibility/pg_visibility.c +++ b/contrib/pg_visibility/pg_visibility.c @@ -3,7 +3,7 @@ * pg_visibility.c * display visibility map information and page-level visibility bits * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * contrib/pg_visibility/pg_visibility.c *------------------------------------------------------------------------- @@ -270,11 +270,8 @@ pg_visibility_map_summary(PG_FUNCTION_ARGS) { Oid relid = PG_GETARG_OID(0); Relation rel; - BlockNumber nblocks; - BlockNumber blkno; - Buffer vmbuffer = InvalidBuffer; - int64 all_visible = 0; - int64 all_frozen = 0; + BlockNumber all_visible = 0; + BlockNumber all_frozen = 0; TupleDesc tupdesc; Datum values[2]; bool nulls[2] = {0}; @@ -284,33 +281,15 @@ pg_visibility_map_summary(PG_FUNCTION_ARGS) /* Only some relkinds have a visibility map */ check_relation_relkind(rel); - nblocks = RelationGetNumberOfBlocks(rel); - - for (blkno = 0; blkno < nblocks; ++blkno) - { - int32 mapbits; - - /* Make sure we are interruptible. */ - CHECK_FOR_INTERRUPTS(); + visibilitymap_count(rel, &all_visible, &all_frozen); - /* Get map info. */ - mapbits = (int32) visibilitymap_get_status(rel, blkno, &vmbuffer); - if ((mapbits & VISIBILITYMAP_ALL_VISIBLE) != 0) - ++all_visible; - if ((mapbits & VISIBILITYMAP_ALL_FROZEN) != 0) - ++all_frozen; - } - - /* Clean up. */ - if (vmbuffer != InvalidBuffer) - ReleaseBuffer(vmbuffer); relation_close(rel, AccessShareLock); if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); - values[0] = Int64GetDatum(all_visible); - values[1] = Int64GetDatum(all_frozen); + values[0] = Int64GetDatum((int64) all_visible); + values[1] = Int64GetDatum((int64) all_frozen); PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } @@ -490,6 +469,8 @@ pg_visibility_tupdesc(bool include_blkno, bool include_pd) TupleDescInitEntry(tupdesc, ++a, "pd_all_visible", BOOLOID, -1, 0); Assert(a == maxattr); + TupleDescFinalize(tupdesc); + return BlessTupleDesc(tupdesc); } @@ -640,7 +621,7 @@ GetStrictOldestNonRemovableTransactionId(Relation rel) else if (rel == NULL || rel->rd_rel->relisshared) { /* Shared relation: take into account all running xids */ - runningTransactions = GetRunningTransactionData(); + runningTransactions = GetRunningTransactionData(InvalidOid); LWLockRelease(ProcArrayLock); LWLockRelease(XidGenLock); return runningTransactions->oldestRunningXid; @@ -651,7 +632,7 @@ GetStrictOldestNonRemovableTransactionId(Relation rel) * Normal relation: take into account xids running within the current * database */ - runningTransactions = GetRunningTransactionData(); + runningTransactions = GetRunningTransactionData(InvalidOid); LWLockRelease(ProcArrayLock); LWLockRelease(XidGenLock); return runningTransactions->oldestDatabaseRunningXid; @@ -741,7 +722,7 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen) * number of entries allocated. We'll repurpose these fields before * returning. */ - items = palloc0(sizeof(corrupt_items)); + items = palloc0_object(corrupt_items); items->next = 0; items->count = 64; items->tids = palloc(items->count * sizeof(ItemPointerData)); @@ -839,7 +820,7 @@ collect_corrupt_items(Oid relid, bool all_visible, bool all_frozen) * * From a concurrency point of view, it sort of sucks to * retake ProcArrayLock here while we're holding the buffer - * exclusively locked, but it should be safe against + * locked in shared mode, but it should be safe against * deadlocks, because surely * GetStrictOldestNonRemovableTransactionId() should never * take a buffer lock. And this shouldn't happen often, so diff --git a/contrib/pg_visibility/sql/pg_visibility.sql b/contrib/pg_visibility/sql/pg_visibility.sql index 5af06ec5b7600..0888adb96a6fe 100644 --- a/contrib/pg_visibility/sql/pg_visibility.sql +++ b/contrib/pg_visibility/sql/pg_visibility.sql @@ -1,4 +1,5 @@ CREATE EXTENSION pg_visibility; +CREATE EXTENSION pageinspect; -- -- recently-dropped table @@ -94,6 +95,25 @@ select count(*) > 0 from pg_visibility_map_summary('test_partition'); select * from pg_check_frozen('test_partition'); -- hopefully none select pg_truncate_visibility_map('test_partition'); +-- test the case where vacuum phase I does not need to modify the heap buffer +-- and only needs to set the VM +create temp table test_vac_unmodified_heap(a int); +insert into test_vac_unmodified_heap values (1); +vacuum (freeze) test_vac_unmodified_heap; +select pg_visibility_map_summary('test_vac_unmodified_heap'); +-- the checkpoint cleans the buffer dirtied by freezing the sole tuple +checkpoint; +-- truncating the VM ensures that the next vacuum will need to set it +select pg_truncate_visibility_map('test_vac_unmodified_heap'); +select pg_visibility_map_summary('test_vac_unmodified_heap'); +-- though the VM is truncated, the heap page-level visibility hint, +-- PD_ALL_VISIBLE should still be set +SELECT (flags & x'0004'::int) <> 0 + FROM page_header(get_raw_page('test_vac_unmodified_heap', 0)); +-- vacuum sets the VM +vacuum test_vac_unmodified_heap; +select pg_visibility_map_summary('test_vac_unmodified_heap'); + -- test copy freeze create table copyfreeze (a int, b char(1500)); diff --git a/contrib/pg_visibility/t/001_concurrent_transaction.pl b/contrib/pg_visibility/t/001_concurrent_transaction.pl index 9911720984e3b..3aa556892a67f 100644 --- a/contrib/pg_visibility/t/001_concurrent_transaction.pl +++ b/contrib/pg_visibility/t/001_concurrent_transaction.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Check that a concurrent transaction doesn't cause false negatives in # pg_check_visible() function diff --git a/contrib/pg_visibility/t/002_corrupt_vm.pl b/contrib/pg_visibility/t/002_corrupt_vm.pl index b9b319564669b..9a7a4b4e00b99 100644 --- a/contrib/pg_visibility/t/002_corrupt_vm.pl +++ b/contrib/pg_visibility/t/002_corrupt_vm.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Check that pg_check_visible() and pg_check_frozen() report correct TIDs for # corruption. @@ -40,7 +40,7 @@ "SELECT relpages FROM pg_class WHERE relname = 'corruption_test';" ); -ok($npages >= 10, 'table has at least 10 pages'); +cmp_ok($npages, '>=', 10, 'table has at least 10 pages'); my $file = $node->safe_psql("postgres", "SELECT pg_relation_filepath('corruption_test');"); diff --git a/contrib/pg_walinspect/expected/pg_walinspect.out b/contrib/pg_walinspect/expected/pg_walinspect.out index c010eed8c5d6e..f955ff5d3c52a 100644 --- a/contrib/pg_walinspect/expected/pg_walinspect.out +++ b/contrib/pg_walinspect/expected/pg_walinspect.out @@ -19,14 +19,14 @@ INSERT INTO sample_tbl SELECT * FROM generate_series(3, 4); -- =================================================================== -- Invalid input LSN. SELECT * FROM pg_get_wal_record_info('0/0'); -ERROR: could not read WAL at LSN 0/0 +ERROR: could not read WAL at LSN 0/00000000 -- Invalid start LSN. SELECT * FROM pg_get_wal_records_info('0/0', :'wal_lsn1'); -ERROR: could not read WAL at LSN 0/0 +ERROR: could not read WAL at LSN 0/00000000 SELECT * FROM pg_get_wal_stats('0/0', :'wal_lsn1'); -ERROR: could not read WAL at LSN 0/0 +ERROR: could not read WAL at LSN 0/00000000 SELECT * FROM pg_get_wal_block_info('0/0', :'wal_lsn1'); -ERROR: could not read WAL at LSN 0/0 +ERROR: could not read WAL at LSN 0/00000000 -- Start LSN > End LSN. SELECT * FROM pg_get_wal_records_info(:'wal_lsn2', :'wal_lsn1'); ERROR: WAL start LSN must be less than end LSN diff --git a/contrib/pg_walinspect/meson.build b/contrib/pg_walinspect/meson.build index 3a82ce09fcf9b..82ab186c34046 100644 --- a/contrib/pg_walinspect/meson.build +++ b/contrib/pg_walinspect/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_walinspect_sources = files('pg_walinspect.c') diff --git a/contrib/pg_walinspect/pg_walinspect.c b/contrib/pg_walinspect/pg_walinspect.c index 64745564cc249..4cf6e41e2f5c8 100644 --- a/contrib/pg_walinspect/pg_walinspect.c +++ b/contrib/pg_walinspect/pg_walinspect.c @@ -3,7 +3,7 @@ * pg_walinspect.c * Functions to inspect contents of PostgreSQL Write-Ahead Log * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pg_walinspect/pg_walinspect.c @@ -12,6 +12,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "access/xlog.h" #include "access/xlog_internal.h" #include "access/xlogreader.h" @@ -20,9 +21,11 @@ #include "access/xlogutils.h" #include "funcapi.h" #include "miscadmin.h" +#include "port/pg_bitutils.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/pg_lsn.h" +#include "utils/tuplestore.h" /* * NOTE: For any code change or issue fix here, it is highly recommended to @@ -82,7 +85,7 @@ GetCurrentLSN(void) else curr_lsn = GetXLogReplayRecPtr(NULL); - Assert(!XLogRecPtrIsInvalid(curr_lsn)); + Assert(XLogRecPtrIsValid(curr_lsn)); return curr_lsn; } @@ -96,6 +99,7 @@ InitXLogReaderState(XLogRecPtr lsn) XLogReaderState *xlogreader; ReadLocalXLogPageNoWaitPrivate *private_data; XLogRecPtr first_valid_record; + char *errormsg; /* * Reading WAL below the first page of the first segments isn't allowed. @@ -105,11 +109,10 @@ InitXLogReaderState(XLogRecPtr lsn) if (lsn < XLOG_BLCKSZ) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("could not read WAL at LSN %X/%X", + errmsg("could not read WAL at LSN %X/%08X", LSN_FORMAT_ARGS(lsn)))); - private_data = (ReadLocalXLogPageNoWaitPrivate *) - palloc0(sizeof(ReadLocalXLogPageNoWaitPrivate)); + private_data = palloc0_object(ReadLocalXLogPageNoWaitPrivate); xlogreader = XLogReaderAllocate(wal_segment_size, NULL, XL_ROUTINE(.page_read = &read_local_xlog_page_no_wait, @@ -124,12 +127,19 @@ InitXLogReaderState(XLogRecPtr lsn) errdetail("Failed while allocating a WAL reading processor."))); /* first find a valid recptr to start from */ - first_valid_record = XLogFindNextRecord(xlogreader, lsn); + first_valid_record = XLogFindNextRecord(xlogreader, lsn, &errormsg); - if (XLogRecPtrIsInvalid(first_valid_record)) - ereport(ERROR, - (errmsg("could not find a valid record after %X/%X", - LSN_FORMAT_ARGS(lsn)))); + if (!XLogRecPtrIsValid(first_valid_record)) + { + if (errormsg) + ereport(ERROR, + errmsg("could not find a valid record after %X/%08X: %s", + LSN_FORMAT_ARGS(lsn), errormsg)); + else + ereport(ERROR, + errmsg("could not find a valid record after %X/%08X", + LSN_FORMAT_ARGS(lsn))); + } return xlogreader; } @@ -168,12 +178,12 @@ ReadNextXLogRecord(XLogReaderState *xlogreader) if (errormsg) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read WAL at %X/%X: %s", + errmsg("could not read WAL at %X/%08X: %s", LSN_FORMAT_ARGS(xlogreader->EndRecPtr), errormsg))); else ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read WAL at %X/%X", + errmsg("could not read WAL at %X/%08X", LSN_FORMAT_ARGS(xlogreader->EndRecPtr)))); } @@ -309,7 +319,7 @@ GetWALBlockInfo(FunctionCallInfo fcinfo, XLogReaderState *record, /* Construct and save block_fpi_info */ bitcnt = pg_popcount((const char *) &blk->bimg_info, sizeof(uint8)); - flags = (Datum *) palloc0(sizeof(Datum) * bitcnt); + flags = palloc0_array(Datum, bitcnt); if ((blk->bimg_info & BKPIMAGE_HAS_HOLE) != 0) flags[cnt++] = CStringGetTextDatum("HAS_HOLE"); if (blk->apply_image) @@ -479,7 +489,7 @@ pg_get_wal_record_info(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("WAL input LSN must be less than current LSN"), - errdetail("Current WAL LSN on the database system is at %X/%X.", + errdetail("Current WAL LSN on the database system is at %X/%08X.", LSN_FORMAT_ARGS(curr_lsn)))); /* Build a tuple descriptor for our result type. */ @@ -491,7 +501,7 @@ pg_get_wal_record_info(PG_FUNCTION_ARGS) if (!ReadNextXLogRecord(xlogreader)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("could not read WAL at %X/%X", + errmsg("could not read WAL at %X/%08X", LSN_FORMAT_ARGS(xlogreader->EndRecPtr)))); GetWALRecordInfo(xlogreader, values, nulls, PG_GET_WAL_RECORD_INFO_COLS); @@ -521,7 +531,7 @@ ValidateInputLSNs(XLogRecPtr start_lsn, XLogRecPtr *end_lsn) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("WAL start LSN must be less than current LSN"), - errdetail("Current WAL LSN on the database system is at %X/%X.", + errdetail("Current WAL LSN on the database system is at %X/%08X.", LSN_FORMAT_ARGS(curr_lsn)))); if (start_lsn > *end_lsn) @@ -827,7 +837,7 @@ pg_get_wal_records_info_till_end_of_wal(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("WAL start LSN must be less than current LSN"), - errdetail("Current WAL LSN on the database system is at %X/%X.", + errdetail("Current WAL LSN on the database system is at %X/%08X.", LSN_FORMAT_ARGS(end_lsn)))); GetWALRecordsInfo(fcinfo, start_lsn, end_lsn); @@ -846,7 +856,7 @@ pg_get_wal_stats_till_end_of_wal(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("WAL start LSN must be less than current LSN"), - errdetail("Current WAL LSN on the database system is at %X/%X.", + errdetail("Current WAL LSN on the database system is at %X/%08X.", LSN_FORMAT_ARGS(end_lsn)))); GetWalStats(fcinfo, start_lsn, end_lsn, stats_per_record); diff --git a/contrib/pgcrypto/Makefile b/contrib/pgcrypto/Makefile index 69afa3750116c..17d2b0c5ed174 100644 --- a/contrib/pgcrypto/Makefile +++ b/contrib/pgcrypto/Makefile @@ -44,7 +44,8 @@ REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \ sha2 des 3des cast5 \ crypt-des crypt-md5 crypt-blowfish crypt-xdes \ pgp-armor pgp-decrypt pgp-encrypt pgp-encrypt-md5 $(CF_PGP_TESTS) \ - pgp-pubkey-decrypt pgp-pubkey-encrypt pgp-info crypt-shacrypt + pgp-pubkey-decrypt pgp-pubkey-encrypt pgp-pubkey-session \ + pgp-info crypt-shacrypt ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/contrib/pgcrypto/crypt-sha.c b/contrib/pgcrypto/crypt-sha.c index 7ec21771a83b7..8191ba02b23c1 100644 --- a/contrib/pgcrypto/crypt-sha.c +++ b/contrib/pgcrypto/crypt-sha.c @@ -147,7 +147,7 @@ px_crypt_shacrypt(const char *pw, const char *salt, char *passwd, unsigned dstle ereport(ERROR, errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid format of salt"), - errhint("magic byte format for shacrypt is either \"$5$\" or \"$6$\"")); + errhint("Magic byte format for shacrypt is either \"$5$\" or \"$6$\".")); /* * Check magic byte for supported shacrypt digest. @@ -328,7 +328,7 @@ px_crypt_shacrypt(const char *pw, const char *salt, char *passwd, unsigned dstle ereport(ERROR, errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid character in salt string: \"%.*s\"", - pg_mblen(ep), ep)); + pg_mblen_cstr(ep), ep)); } else { diff --git a/contrib/pgcrypto/expected/hmac-md5_2.out b/contrib/pgcrypto/expected/hmac-md5_2.out new file mode 100644 index 0000000000000..08cdf95d53245 --- /dev/null +++ b/contrib/pgcrypto/expected/hmac-md5_2.out @@ -0,0 +1,44 @@ +-- +-- HMAC-MD5 +-- +SELECT hmac( +'Hi There', +'\x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'::bytea, +'md5'); +ERROR: Cannot use "md5": No such hash algorithm +-- 2 +SELECT hmac( +'Jefe', +'what do ya want for nothing?', +'md5'); +ERROR: Cannot use "md5": No such hash algorithm +-- 3 +SELECT hmac( +'\xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'::bytea, +'\xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::bytea, +'md5'); +ERROR: Cannot use "md5": No such hash algorithm +-- 4 +SELECT hmac( +'\xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd'::bytea, +'\x0102030405060708090a0b0c0d0e0f10111213141516171819'::bytea, +'md5'); +ERROR: Cannot use "md5": No such hash algorithm +-- 5 +SELECT hmac( +'Test With Truncation', +'\x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c'::bytea, +'md5'); +ERROR: Cannot use "md5": No such hash algorithm +-- 6 +SELECT hmac( +'Test Using Larger Than Block-Size Key - Hash Key First', +'\xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::bytea, +'md5'); +ERROR: Cannot use "md5": No such hash algorithm +-- 7 +SELECT hmac( +'Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data', +'\xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::bytea, +'md5'); +ERROR: Cannot use "md5": No such hash algorithm diff --git a/contrib/pgcrypto/expected/md5_2.out b/contrib/pgcrypto/expected/md5_2.out new file mode 100644 index 0000000000000..51bdaa86f32b4 --- /dev/null +++ b/contrib/pgcrypto/expected/md5_2.out @@ -0,0 +1,17 @@ +-- +-- MD5 message digest +-- +SELECT digest('', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm +SELECT digest('a', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm +SELECT digest('abc', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm +SELECT digest('message digest', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm +SELECT digest('abcdefghijklmnopqrstuvwxyz', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm +SELECT digest('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm +SELECT digest('12345678901234567890123456789012345678901234567890123456789012345678901234567890', 'md5'); +ERROR: Cannot use "md5": No such hash algorithm diff --git a/contrib/pgcrypto/expected/pgp-decrypt.out b/contrib/pgcrypto/expected/pgp-decrypt.out index eb049ba9d4443..8ce6466f2e9a8 100644 --- a/contrib/pgcrypto/expected/pgp-decrypt.out +++ b/contrib/pgcrypto/expected/pgp-decrypt.out @@ -315,7 +315,7 @@ SaV9L04ky1qECNDx3XjnoKLC+H7IOQ== \xda39a3ee5e6b4b0d3255bfef95601890afd80709 (1 row) -select digest(pgp_sym_decrypt(dearmor(' +select digest(pgp_sym_decrypt_bytea(dearmor(' -----BEGIN PGP MESSAGE----- Comment: dat3.aes.sha1.mdc.s2k3.z0 @@ -387,6 +387,28 @@ ERROR: Wrong key or corrupt data select pgp_sym_decrypt(pgp_sym_encrypt_bytea('P', 'key'), 'key', 'debug=1'); NOTICE: dbg: parse_literal_data: data type=b ERROR: Not text data +-- NUL byte in text decrypt. Ciphertext source: +-- printf 'a\x00\xc' | gpg --homedir /nonexistent \ +-- --personal-compress-preferences uncompressed --textmode \ +-- --personal-cipher-preferences aes --no-emit-version --batch \ +-- --symmetric --passphrase key --armor +do $$ +begin + perform pgp_sym_decrypt(dearmor(' +-----BEGIN PGP MESSAGE----- + +jA0EBwMCXLc8pozB10Fg0jQBVUID59TLvWutJp0j6eh9ZgjqIRzdYaIymFB8y4XH +vu0YlJP5D5BX7yqZ+Pry7TlDmiFO +=rV7z +-----END PGP MESSAGE----- +'), 'key', 'debug=1'); +exception when others then + raise '%', + regexp_replace(sqlerrm, 'encoding "[^"]*"', 'encoding [REDACTED]'); +end +$$; +ERROR: invalid byte sequence for encoding [REDACTED]: 0x00 +CONTEXT: PL/pgSQL function inline_code_block line 12 at RAISE -- Decryption with a certain incorrect key yields an apparent BZip2-compressed -- plaintext. Ciphertext source: iterative pgp_sym_encrypt('secret', 'key') -- until the random prefix gave rise to that property. diff --git a/contrib/pgcrypto/expected/pgp-decrypt_1.out b/contrib/pgcrypto/expected/pgp-decrypt_1.out index 80a4c48613d00..ee57ad43cb75f 100644 --- a/contrib/pgcrypto/expected/pgp-decrypt_1.out +++ b/contrib/pgcrypto/expected/pgp-decrypt_1.out @@ -311,7 +311,7 @@ SaV9L04ky1qECNDx3XjnoKLC+H7IOQ== \xda39a3ee5e6b4b0d3255bfef95601890afd80709 (1 row) -select digest(pgp_sym_decrypt(dearmor(' +select digest(pgp_sym_decrypt_bytea(dearmor(' -----BEGIN PGP MESSAGE----- Comment: dat3.aes.sha1.mdc.s2k3.z0 @@ -383,6 +383,28 @@ ERROR: Wrong key or corrupt data select pgp_sym_decrypt(pgp_sym_encrypt_bytea('P', 'key'), 'key', 'debug=1'); NOTICE: dbg: parse_literal_data: data type=b ERROR: Not text data +-- NUL byte in text decrypt. Ciphertext source: +-- printf 'a\x00\xc' | gpg --homedir /nonexistent \ +-- --personal-compress-preferences uncompressed --textmode \ +-- --personal-cipher-preferences aes --no-emit-version --batch \ +-- --symmetric --passphrase key --armor +do $$ +begin + perform pgp_sym_decrypt(dearmor(' +-----BEGIN PGP MESSAGE----- + +jA0EBwMCXLc8pozB10Fg0jQBVUID59TLvWutJp0j6eh9ZgjqIRzdYaIymFB8y4XH +vu0YlJP5D5BX7yqZ+Pry7TlDmiFO +=rV7z +-----END PGP MESSAGE----- +'), 'key', 'debug=1'); +exception when others then + raise '%', + regexp_replace(sqlerrm, 'encoding "[^"]*"', 'encoding [REDACTED]'); +end +$$; +ERROR: invalid byte sequence for encoding [REDACTED]: 0x00 +CONTEXT: PL/pgSQL function inline_code_block line 12 at RAISE -- Decryption with a certain incorrect key yields an apparent BZip2-compressed -- plaintext. Ciphertext source: iterative pgp_sym_encrypt('secret', 'key') -- until the random prefix gave rise to that property. diff --git a/contrib/pgcrypto/expected/pgp-pubkey-session.out b/contrib/pgcrypto/expected/pgp-pubkey-session.out new file mode 100644 index 0000000000000..e57cb8fab99c3 --- /dev/null +++ b/contrib/pgcrypto/expected/pgp-pubkey-session.out @@ -0,0 +1,47 @@ +-- Test for overflow with session key at decrypt. +-- Data automatically generated by scripts/pgp_session_data.py. +-- See this file for details explaining how this data is generated. +SELECT pgp_pub_decrypt_bytea( +'\xc1c04c030000000000000000020800a46f5b9b1905b49457a6485474f71ed9b46c2527e1 +da08e1f7871e12c3d38828f2076b984a595bf60f616599ca5729d547de06a258bfbbcd30 +94a321e4668cd43010f0ca8ecf931e5d39bda1152c50c367b11c723f270729245d3ebdbd +0694d320c5a5aa6a405fb45182acb3d7973cbce398e0c5060af7603cfd9ed186ebadd616 +3b50ae42bea5f6d14dda24e6d4687b434c175084515d562e896742b0ba9a1c87d5642e10 +a5550379c71cc490a052ada483b5d96526c0a600fc51755052aa77fdf72f7b4989b920e7 +b90f4b30787a46482670d5caecc7a515a926055ad5509d135702ce51a0e4c1033f2d939d +8f0075ec3428e17310da37d3d2d7ad1ce99adcc91cd446c366c402ae1ee38250343a7fcc +0f8bc28020e603d7a4795ef0dcc1c04c030000000000000000020800a46f5b9b1905b494 +57a6485474f71ed9b46c2527e1da08e1f7871e12c3d38828f2076b984a595bf60f616599 +ca5729d547de06a258bfbbcd3094a321e4668cd43010f0ca8ecf931e5d39bda1152c50c3 +67b11c723f270729245d3ebdbd0694d320c5a5aa6a405fb45182acb3d7973cbce398e0c5 +060af7603cfd9ed186ebadd6163b50ae42bea5f6d14dda24e6d4687b434c175084515d56 +2e896742b0ba9a1c87d5642e10a5550379c71cc490a052ada483b5d96526c0a600fc5175 +5052aa77fdf72f7b4989b920e7b90f4b30787a46482670d5caecc7a515a926055ad5509d +135702ce51a0e4c1033f2d939d8f0075ec3428e17310da37d3d2d7ad1ce99adc'::bytea, +'\xc7c2d8046965d657020800eef8bf1515adb1a3ee7825f75c668ea8dd3e3f9d13e958f6ad +9c55adc0c931a4bb00abe1d52cf7bb0c95d537949d277a5292ede375c6b2a67a3bf7d19f +f975bb7e7be35c2d8300dacba360a0163567372f7dc24000cc7cb6170bedc8f3b1f98c12 +07a6cb4de870a4bc61319b139dcc0e20c368fd68f8fd346d2c0b69c5aed560504e2ec6f1 +23086fe3c5540dc4dd155c0c67257c4ada862f90fe172ace344089da8135e92aca5c2709 +f1c1bc521798bb8c0365841496e709bd184132d387e0c9d5f26dc00fd06c3a76ef66a75c +138285038684707a847b7bd33cfbefbf1d336be954a8048946af97a66352adef8e8b5ae4 +c4748c6f2510265b7a8267bc370dbb00110100010007ff7e72d4f95d2d39901ac12ca5c5 +18e767e719e72340c3fab51c8c5ab1c40f31db8eaffe43533fa61e2dbca2c3f4396c0847 +e5434756acbb1f68128f4136bb135710c89137d74538908dac77967de9e821c559700dd9 +de5a2727eec1f5d12d5d74869dd1de45ed369d94a8814d23861dd163f8c27744b26b98f0 +239c2e6dd1e3493b8cc976fdc8f9a5e250f715aa4c3d7d5f237f8ee15d242e8fa941d1a0 +ed9550ab632d992a97518d142802cb0a97b251319bf5742db8d9d8cbaa06cdfba2d75bc9 +9d77a51ff20bd5ba7f15d7af6e85b904de2855d19af08d45f39deb85403033c69c767a8e +74a343b1d6c8911d34ea441ac3850e57808ed3d885835cbe6c79d10400ef16256f3d5c4c +3341516a2d2aa888df81b603f48a27f3666b40f992a857c1d11ff639cd764a9b42d5a1f8 +58b4aeee36b85508bb5e8b91ef88a7737770b330224479d9b44eae8c631bc43628b69549 +507c0a1af0be0dd7696015abea722b571eb35eefc4ab95595378ec12814727443f625fcd +183bb9b3bccf53b54dd0e5e7a50400ffe08537b2d4e6074e4a1727b658cfccdec8962302 +25e300c05690de45f7065c3d40d86f544a64d51a3e94424f9851a16d1322ebdb41fa8a45 +3131f3e2dc94e858e6396722643df382680f815e53bcdcde5da622f50530a83b217f1103 +cdd6e5e9babe1e415bbff28d44bd18c95f43bbd04afeb2a2a99af38a571c7540de21df03 +ff62c0a33d9143dd3f639893f47732c11c5a12c6052d1935f4d507b7ae1f76ab0e9a69b8 +7305a7f7c19bd509daf4903bff614bc26d118f03e461469c72c12d3a2bb4f78e4d342ce8 +487723649a01ed2b9eb11c662134502c098d55dfcd361939d8370873422c3da75a515a75 +9ffedfe7df44fb3c20f81650801a30d43b5c90b98b3eee'::bytea); +ERROR: Session key too big diff --git a/contrib/pgcrypto/mbuf.c b/contrib/pgcrypto/mbuf.c index 99f8957b00414..6a23ad9970664 100644 --- a/contrib/pgcrypto/mbuf.c +++ b/contrib/pgcrypto/mbuf.c @@ -115,7 +115,7 @@ mbuf_create(int len) if (!len) len = 8192; - mbuf = palloc(sizeof *mbuf); + mbuf = palloc_object(MBuf); mbuf->data = palloc(len); mbuf->buf_end = mbuf->data + len; mbuf->data_end = mbuf->data; @@ -132,8 +132,8 @@ mbuf_create_from_data(uint8 *data, int len) { MBuf *mbuf; - mbuf = palloc(sizeof *mbuf); - mbuf->data = (uint8 *) data; + mbuf = palloc_object(MBuf); + mbuf->data = data; mbuf->buf_end = mbuf->data + len; mbuf->data_end = mbuf->data + len; mbuf->read_pos = mbuf->data; @@ -206,7 +206,7 @@ pullf_create(PullFilter **pf_p, const PullFilterOps *op, void *init_arg, PullFil res = 0; } - pf = palloc0(sizeof(*pf)); + pf = palloc0_object(PullFilter); pf->buflen = res; pf->op = op; pf->priv = priv; @@ -372,7 +372,7 @@ pushf_create(PushFilter **mp_p, const PushFilterOps *op, void *init_arg, PushFil res = 0; } - mp = palloc0(sizeof(*mp)); + mp = palloc0_object(PushFilter); mp->block_size = res; mp->op = op; mp->priv = priv; diff --git a/contrib/pgcrypto/meson.build b/contrib/pgcrypto/meson.build index 7d5ef9b6d3231..4f255c8cb05dc 100644 --- a/contrib/pgcrypto/meson.build +++ b/contrib/pgcrypto/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not ssl.found() subdir_done() @@ -52,6 +52,7 @@ pgcrypto_regress = [ 'pgp-encrypt-md5', 'pgp-pubkey-decrypt', 'pgp-pubkey-encrypt', + 'pgp-pubkey-session', 'pgp-info', 'crypt-shacrypt' ] diff --git a/contrib/pgcrypto/openssl.c b/contrib/pgcrypto/openssl.c index f179e80c8429e..d3c12e7fda36a 100644 --- a/contrib/pgcrypto/openssl.c +++ b/contrib/pgcrypto/openssl.c @@ -197,7 +197,7 @@ px_find_digest(const char *name, PX_MD **res) ResourceOwnerRememberOSSLDigest(digest->owner, digest); /* The PX_MD object is allocated in the current memory context. */ - h = palloc(sizeof(*h)); + h = palloc_object(PX_MD); h->result_size = digest_result_size; h->block_size = digest_block_size; h->reset = digest_reset; @@ -813,7 +813,7 @@ px_find_cipher(const char *name, PX_Cipher **res) od->evp_ciph = i->ciph->cipher_func(); /* The PX_Cipher is allocated in current memory context */ - c = palloc(sizeof(*c)); + c = palloc_object(PX_Cipher); c->block_size = gen_ossl_block_size; c->key_size = gen_ossl_key_size; c->iv_size = gen_ossl_iv_size; diff --git a/contrib/pgcrypto/pgp-cfb.c b/contrib/pgcrypto/pgp-cfb.c index de41e825b0ce8..d8f1afc3aba42 100644 --- a/contrib/pgcrypto/pgp-cfb.c +++ b/contrib/pgcrypto/pgp-cfb.c @@ -67,7 +67,7 @@ pgp_cfb_create(PGP_CFB **ctx_p, int algo, const uint8 *key, int key_len, return res; } - ctx = palloc0(sizeof(*ctx)); + ctx = palloc0_object(PGP_CFB); ctx->ciph = ciph; ctx->block_size = px_cipher_block_size(ciph); ctx->resync = resync; diff --git a/contrib/pgcrypto/pgp-compress.c b/contrib/pgcrypto/pgp-compress.c index 961cf21e74891..caa80ecdb4596 100644 --- a/contrib/pgcrypto/pgp-compress.c +++ b/contrib/pgcrypto/pgp-compress.c @@ -80,7 +80,7 @@ compress_init(PushFilter *next, void *init_arg, void **priv_p) /* * init */ - st = palloc0(sizeof(*st)); + st = palloc0_object(struct ZipStat); st->buf_len = ZIP_OUT_BUF; st->stream.zalloc = z_alloc; st->stream.zfree = z_free; @@ -211,7 +211,7 @@ decompress_init(void **priv_p, void *arg, PullFilter *src) && ctx->compress_algo != PGP_COMPR_ZIP) return PXE_PGP_UNSUPPORTED_COMPR; - dec = palloc0(sizeof(*dec)); + dec = palloc0_object(struct DecomprData); dec->buf_len = ZIP_OUT_BUF; *priv_p = dec; diff --git a/contrib/pgcrypto/pgp-decrypt.c b/contrib/pgcrypto/pgp-decrypt.c index e1ea5b3e58dcf..52ca7840c6d1e 100644 --- a/contrib/pgcrypto/pgp-decrypt.c +++ b/contrib/pgcrypto/pgp-decrypt.c @@ -224,7 +224,7 @@ pgp_create_pkt_reader(PullFilter **pf_p, PullFilter *src, int len, int pkttype, PGP_Context *ctx) { int res; - struct PktData *pkt = palloc(sizeof(*pkt)); + struct PktData *pkt = palloc_object(struct PktData); pkt->type = pkttype; pkt->len = len; @@ -448,7 +448,7 @@ mdcbuf_init(void **priv_p, void *arg, PullFilter *src) PGP_Context *ctx = arg; struct MDCBufData *st; - st = palloc0(sizeof(*st)); + st = palloc0_object(struct MDCBufData); st->buflen = sizeof(st->buf); st->ctx = ctx; *priv_p = st; diff --git a/contrib/pgcrypto/pgp-encrypt.c b/contrib/pgcrypto/pgp-encrypt.c index f7467c9b1cb1c..2c05980470625 100644 --- a/contrib/pgcrypto/pgp-encrypt.c +++ b/contrib/pgcrypto/pgp-encrypt.c @@ -178,7 +178,7 @@ encrypt_init(PushFilter *next, void *init_arg, void **priv_p) if (res < 0) return res; - st = palloc0(sizeof(*st)); + st = palloc0_object(struct EncStat); st->ciph = ciph; *priv_p = st; @@ -240,7 +240,7 @@ pkt_stream_init(PushFilter *next, void *init_arg, void **priv_p) { struct PktStreamStat *st; - st = palloc(sizeof(*st)); + st = palloc_object(struct PktStreamStat); st->final_done = 0; st->pkt_block = 1 << STREAM_BLOCK_SHIFT; *priv_p = st; diff --git a/contrib/pgcrypto/pgp-info.c b/contrib/pgcrypto/pgp-info.c index 83dc60486bd98..6c2be4713ab71 100644 --- a/contrib/pgcrypto/pgp-info.c +++ b/contrib/pgcrypto/pgp-info.c @@ -169,7 +169,7 @@ pgp_get_keyid(MBuf *pgp_data, char *dst) break; case PGP_PKT_SYMENCRYPTED_SESSKEY: got_symenc_key++; - /* fall through */ + pg_fallthrough; case PGP_PKT_SIGNATURE: case PGP_PKT_MARKER: case PGP_PKT_TRUST: diff --git a/contrib/pgcrypto/pgp-pgsql.c b/contrib/pgcrypto/pgp-pgsql.c index 7c9f4c7b39b88..d3e7895b0d94e 100644 --- a/contrib/pgcrypto/pgp-pgsql.c +++ b/contrib/pgcrypto/pgp-pgsql.c @@ -631,6 +631,7 @@ pgp_sym_decrypt_text(PG_FUNCTION_ARGS) arg = PG_GETARG_TEXT_PP(2); res = decrypt_internal(0, 1, data, key, NULL, arg); + pg_verifymbstr(VARDATA_ANY(res), VARSIZE_ANY_EXHDR(res), false); PG_FREE_IF_COPY(data, 0); PG_FREE_IF_COPY(key, 1); @@ -732,6 +733,7 @@ pgp_pub_decrypt_text(PG_FUNCTION_ARGS) arg = PG_GETARG_TEXT_PP(3); res = decrypt_internal(1, 1, data, key, psw, arg); + pg_verifymbstr(VARDATA_ANY(res), VARSIZE_ANY_EXHDR(res), false); PG_FREE_IF_COPY(data, 0); PG_FREE_IF_COPY(key, 1); @@ -782,8 +784,8 @@ parse_key_value_arrays(ArrayType *key_array, ArrayType *val_array, (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), errmsg("mismatched array dimensions"))); - keys = (char **) palloc(sizeof(char *) * key_count); - values = (char **) palloc(sizeof(char *) * val_count); + keys = palloc_array(char *, key_count); + values = palloc_array(char *, val_count); for (i = 0; i < key_count; i++) { @@ -937,7 +939,7 @@ pgp_armor_headers(PG_FUNCTION_ARGS) attinmeta = TupleDescGetAttInMetadata(tupdesc); funcctx->attinmeta = attinmeta; - state = (pgp_armor_headers_state *) palloc(sizeof(pgp_armor_headers_state)); + state = palloc_object(pgp_armor_headers_state); res = pgp_extract_armor_headers((uint8 *) VARDATA_ANY(data), VARSIZE_ANY_EXHDR(data), diff --git a/contrib/pgcrypto/pgp-pubdec.c b/contrib/pgcrypto/pgp-pubdec.c index a0a5738a40e89..2a13aa3e6adff 100644 --- a/contrib/pgcrypto/pgp-pubdec.c +++ b/contrib/pgcrypto/pgp-pubdec.c @@ -157,6 +157,7 @@ pgp_parse_pubenc_sesskey(PGP_Context *ctx, PullFilter *pkt) uint8 *msg; int msglen; PGP_MPI *m; + unsigned sess_key_len; pk = ctx->pub_key; if (pk == NULL) @@ -220,11 +221,19 @@ pgp_parse_pubenc_sesskey(PGP_Context *ctx, PullFilter *pkt) if (res < 0) goto out; + sess_key_len = msglen - 3; + if (sess_key_len > PGP_MAX_KEY) + { + px_debug("incorrect session key length=%u", sess_key_len); + res = PXE_PGP_KEY_TOO_BIG; + goto out; + } + /* * got sesskey */ ctx->cipher_algo = *msg; - ctx->sess_key_len = msglen - 3; + ctx->sess_key_len = sess_key_len; memcpy(ctx->sess_key, msg + 1, ctx->sess_key_len); out: diff --git a/contrib/pgcrypto/pgp-pubkey.c b/contrib/pgcrypto/pgp-pubkey.c index 9a6561caf9dde..6f1188659178b 100644 --- a/contrib/pgcrypto/pgp-pubkey.c +++ b/contrib/pgcrypto/pgp-pubkey.c @@ -39,7 +39,7 @@ pgp_key_alloc(PGP_PubKey **pk_p) { PGP_PubKey *pk; - pk = palloc0(sizeof(*pk)); + pk = palloc0_object(PGP_PubKey); *pk_p = pk; return 0; } diff --git a/contrib/pgcrypto/px-hmac.c b/contrib/pgcrypto/px-hmac.c index 99174d265517b..68e5cff6d6acd 100644 --- a/contrib/pgcrypto/px-hmac.c +++ b/contrib/pgcrypto/px-hmac.c @@ -157,7 +157,7 @@ px_find_hmac(const char *name, PX_HMAC **res) return PXE_HASH_UNUSABLE_FOR_HMAC; } - h = palloc(sizeof(*h)); + h = palloc_object(PX_HMAC); h->p.ipad = palloc(bs); h->p.opad = palloc(bs); h->md = md; diff --git a/contrib/pgcrypto/px.c b/contrib/pgcrypto/px.c index d35ccca77746d..f08bc498ac876 100644 --- a/contrib/pgcrypto/px.c +++ b/contrib/pgcrypto/px.c @@ -65,6 +65,7 @@ static const struct error_desc px_err_list[] = { {PXE_PGP_UNEXPECTED_PKT, "Unexpected packet in key data"}, {PXE_PGP_MATH_FAILED, "Math operation failed"}, {PXE_PGP_SHORT_ELGAMAL_KEY, "Elgamal keys must be at least 1024 bits long"}, + {PXE_PGP_KEY_TOO_BIG, "Session key too big"}, {PXE_PGP_UNKNOWN_PUBALGO, "Unknown public-key encryption algorithm"}, {PXE_PGP_WRONG_KEY, "Wrong key"}, {PXE_PGP_MULTIPLE_KEYS, @@ -291,7 +292,7 @@ px_find_combo(const char *name, PX_Combo **res) PX_Combo *cx; - cx = palloc0(sizeof(*cx)); + cx = palloc0_object(PX_Combo); buf = pstrdup(name); err = parse_cipher_name(buf, &s_cipher, &s_pad); diff --git a/contrib/pgcrypto/px.h b/contrib/pgcrypto/px.h index 4b81fceab8ec2..a09533a35823a 100644 --- a/contrib/pgcrypto/px.h +++ b/contrib/pgcrypto/px.h @@ -75,7 +75,7 @@ /* -108 is unused */ #define PXE_PGP_MATH_FAILED -109 #define PXE_PGP_SHORT_ELGAMAL_KEY -110 -/* -111 is unused */ +#define PXE_PGP_KEY_TOO_BIG -111 #define PXE_PGP_UNKNOWN_PUBALGO -112 #define PXE_PGP_WRONG_KEY -113 #define PXE_PGP_MULTIPLE_KEYS -114 diff --git a/contrib/pgcrypto/scripts/pgp_session_data.py b/contrib/pgcrypto/scripts/pgp_session_data.py new file mode 100644 index 0000000000000..999350bb2bc91 --- /dev/null +++ b/contrib/pgcrypto/scripts/pgp_session_data.py @@ -0,0 +1,491 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Generate PGP data to check the session key length of the input data provided +# to pgp_pub_decrypt_bytea(). +# +# First, the crafted data is generated from valid RSA data, freshly generated +# by this script each time it is run, see generate_rsa_keypair(). +# Second, the crafted PGP data is built, see build_message_data() and +# build_key_data(). Finally, the resulting SQL script is generated. +# +# This script generates in stdout the SQL file that is used in the regression +# tests of pgcrypto. The following command can be used to regenerate the file +# which should never be manually manipulated: +# python3 scripts/pgp_session_data.py > sql/pgp-pubkey-session.sql + +import os +import re +import struct +import secrets +import sys +import time + +# pwn for binary manipulation (p32, p64) +from pwn import * + +# Cryptographic libraries, to craft the PGP data. +from Crypto.Cipher import AES +from Crypto.PublicKey import RSA +from Crypto.Util.number import inverse + +# AES key used for session key encryption (16 bytes for AES-128) +AES_KEY = b'\x01' * 16 + +def generate_rsa_keypair(key_size: int = 2048) -> dict: + """ + Generate a fresh RSA key pair. + + The generated key includes all components needed for PGP operations: + - n: public modulus (p * q) + - e: public exponent (typically 65537) + - d: private exponent (e^-1 mod phi(n)) + - p, q: prime factors of n + - u: coefficient (p^-1 mod q) for CRT optimization + + The caller can pass the wanted key size in input, for a default of 2048 + bytes. This function returns the RSA key components, after performing + some validation on them. + """ + + start_time = time.time() + + # Generate RSA key + key = RSA.generate(key_size) + + # Extract all key components + rsa_components = { + 'n': key.n, # Public modulus (p * q) + 'e': key.e, # Public exponent (typically 65537) + 'd': key.d, # Private exponent (e^-1 mod phi(n)) + 'p': key.p, # First prime factor + 'q': key.q, # Second prime factor + 'u': inverse(key.p, key.q) # Coefficient for CRT: p^-1 mod q + } + + # Validate key components for correctness + validate_rsa_key(rsa_components) + + return rsa_components + +def validate_rsa_key(rsa: dict) -> None: + """ + Validate a generated RSA key. + + This function performs basic validation to ensure the RSA key is properly + constructed and all components are consistent, at least mathematically. + + Validations performed: + 1. n = p * q (modulus is product of primes) + 2. gcd(e, phi(n)) = 1 (public exponent is coprime to phi(n)) + 3. (d * e) mod(phi(n)) = 1 (private exponent is multiplicative inverse) + 4. (u * p) (mod q) = 1 (coefficient is correct for CRT) + """ + + n, e, d, p, q, u = rsa['n'], rsa['e'], rsa['d'], rsa['p'], rsa['q'], rsa['u'] + + # Check that n = p * q + if n != p * q: + raise ValueError("RSA validation failed: n <> p * q") + + # Check that p and q are different + if p == q: + raise ValueError("RSA validation failed: p = q (not allowed)") + + # Calculate phi(n) = (p-1)(q-1) + phi_n = (p - 1) * (q - 1) + + # Check that gcd(e, phi(n)) = 1 + def gcd(a, b): + while b: + a, b = b, a % b + return a + + if gcd(e, phi_n) != 1: + raise ValueError("RSA validation failed: gcd(e, phi(n)) <> 1") + + # Check that (d * e) mod(phi(n)) = 1 + if (d * e) % phi_n != 1: + raise ValueError("RSA validation failed: d * e <> 1 (mod phi(n))") + + # Check that (u * p) (mod q) = 1 + if (u * p) % q != 1: + raise ValueError("RSA validation failed: u * p <> 1 (mod q)") + +def mpi_encode(x: int) -> bytes: + """ + Encode an integer as an OpenPGP Multi-Precision Integer (MPI). + + Format (RFC 4880, Section 3.2): + - 2 bytes: bit length of the integer (big-endian) + - N bytes: the integer in big-endian format + + This is used to encode RSA key components (n, e, d, p, q, u) in PGP + packets. + + The integer to encode is given in input, returning an MPI-encoded + integer. + + For example: + mpi_encode(65537) -> b'\x00\x11\x01\x00\x01' + (17 bits, value 0x010001) + """ + if x < 0: + raise ValueError("MPI cannot encode negative integers") + + if x == 0: + # Special case: zero has 0 bits and empty magnitude + bits = 0 + mag = b"" + else: + # Calculate bit length and convert to bytes + bits = x.bit_length() + mag = x.to_bytes((bits + 7) // 8, 'big') + + # Pack: 2-byte bit length + magnitude bytes + return struct.pack('>H', bits) + mag + +def new_packet(tag: int, payload: bytes) -> bytes: + """ + Create a new OpenPGP packet with a proper header. + + OpenPGP packet format (RFC 4880, Section 4.2): + - New packet format: 0xC0 | tag + - Length encoding depends on payload size: + * 0-191: single byte + * 192-8383: two bytes (192 + ((length - 192) >> 8), (length - 192) & 0xFF) + * 8384+: five bytes (0xFF + 4-byte big-endian length) + + The packet is built from a "tag" (1-63) and some "payload" data. The + result generated is a complete OpenPGP packet. + + For example: + new_packet(1, b'data') -> b'\xC1\x04data' + (Tag 1, length 4, payload 'data') + """ + # New packet format: set bit 7 and 6, clear bit 5, tag in bits 0-5 + first = 0xC0 | (tag & 0x3F) + ln = len(payload) + + # Encode length according to OpenPGP specification + if ln <= 191: + # Single byte length for small packets + llen = bytes([ln]) + elif ln <= 8383: + # Two-byte length for medium packets + ln2 = ln - 192 + llen = bytes([192 + (ln2 >> 8), ln2 & 0xFF]) + else: + # Five-byte length for large packets + llen = bytes([255]) + struct.pack('>I', ln) + + return bytes([first]) + llen + payload + +def build_key_data(rsa: dict) -> bytes: + """ + Build the key data, containing an RSA private key. + + The RSA contents should have been generated previously. + + Format (see RFC 4880, Section 5.5.3): + - 1 byte: version (4) + - 4 bytes: creation time (current Unix timestamp) + - 1 byte: public key algorithm (2 = RSA encrypt) + - MPI: RSA public modulus n + - MPI: RSA public exponent e + - 1 byte: string-to-key usage (0 = no encryption) + - MPI: RSA private exponent d + - MPI: RSA prime p + - MPI: RSA prime q + - MPI: RSA coefficient u = p^-1 mod q + - 2 bytes: checksum of private key material + + This function takes a set of RSA key components in input (n, e, d, p, q, u) + and returns a secret key packet. + """ + + # Public key portion + ver = bytes([4]) # Version 4 key + ctime = struct.pack('>I', int(time.time())) # Current Unix timestamp + algo = bytes([2]) # RSA encrypt algorithm + n_mpi = mpi_encode(rsa['n']) # Public modulus + e_mpi = mpi_encode(rsa['e']) # Public exponent + pub = ver + ctime + algo + n_mpi + e_mpi + + # Private key portion + hide_type = bytes([0]) # No string-to-key encryption + d_mpi = mpi_encode(rsa['d']) # Private exponent + p_mpi = mpi_encode(rsa['p']) # Prime p + q_mpi = mpi_encode(rsa['q']) # Prime q + u_mpi = mpi_encode(rsa['u']) # Coefficient u = p^-1 mod q + + # Calculate checksum of private key material (simple sum mod 65536) + private_data = d_mpi + p_mpi + q_mpi + u_mpi + cksum = sum(private_data) & 0xFFFF + + secret = hide_type + private_data + struct.pack('>H', cksum) + payload = pub + secret + + return new_packet(7, payload) + +def pgp_cfb_encrypt_resync(key, plaintext): + """ + Implement OpenPGP CFB mode with resync. + + OpenPGP CFB mode is a variant of standard CFB with a resync operation + after the first two blocks. + + Algorithm (RFC 4880, Section 13.9): + 1. Block 1: FR=zeros, encrypt full block_size bytes + 2. Block 2: FR=block1, encrypt only 2 bytes + 3. Resync: FR = block1[2:] + block2 + 4. Remaining blocks: standard CFB mode + + This function uses the following arguments: + - key: AES encryption key (16 bytes for AES-128) + - plaintext: Data to encrypt + """ + block_size = 16 # AES block size + cipher = AES.new(key[:16], AES.MODE_ECB) # Use ECB for manual CFB + ciphertext = b'' + + # Block 1: FR=zeros, encrypt full 16 bytes + FR = b'\x00' * block_size + FRE = cipher.encrypt(FR) # Encrypt the feedback register + block1 = bytes(a ^ b for a, b in zip(FRE, plaintext[0:16])) + ciphertext += block1 + + # Block 2: FR=block1, encrypt only 2 bytes + FR = block1 + FRE = cipher.encrypt(FR) + block2 = bytes(a ^ b for a, b in zip(FRE[0:2], plaintext[16:18])) + ciphertext += block2 + + # Resync: FR = block1[2:16] + block2[0:2] + # This is the key difference from standard CFB mode + FR = block1[2:] + block2 + + # Block 3+: Continue with standard CFB mode + pos = 18 + while pos < len(plaintext): + FRE = cipher.encrypt(FR) + chunk_len = min(block_size, len(plaintext) - pos) + chunk = plaintext[pos:pos+chunk_len] + enc_chunk = bytes(a ^ b for a, b in zip(FRE[:chunk_len], chunk)) + ciphertext += enc_chunk + + # Update feedback register for next iteration + if chunk_len == block_size: + FR = enc_chunk + else: + # Partial block: pad with old FR bytes + FR = enc_chunk + FR[chunk_len:] + pos += chunk_len + + return ciphertext + +def build_literal_data_packet(data: bytes) -> bytes: + """ + Build a literal data packet containing a message. + + Format (RFC 4880, Section 5.9): + - 1 byte: data format ('b' = binary, 't' = text, 'u' = UTF-8 text) + - 1 byte: filename length (0 = no filename) + - N bytes: filename (empty in this case) + - 4 bytes: date (current Unix timestamp) + - M bytes: literal data + + The data used to build the packet is given in input, with the generated + result returned. + """ + body = bytes([ + ord('b'), # Binary data format + 0, # Filename length (0 = no filename) + ]) + struct.pack('>I', int(time.time())) + data # Current timestamp + data + + return new_packet(11, body) + +def build_symenc_data_packet(sess_key: bytes, cipher_algo: int, payload: bytes) -> bytes: + """ + Build a symmetrically-encrypted data packet using AES-128-CFB. + + This packet contains encrypted data using the session key. The format + includes a random prefix, for security (see RFC 4880, Section 5.7). + + Packet structure: + - Random prefix (block_size bytes) + - Prefix repeat (last 2 bytes of prefix repeated) + - Encrypted literal data packet + + This function uses the following set of arguments: + - sess_key: Session key for encryption + - cipher_algo: Cipher algorithm identifier (7 = AES-128) + - payload: Data to encrypt (wrapped in literal data packet) + """ + block_size = 16 # AES-128 block size + key = sess_key[:16] # Use first 16 bytes for AES-128 + + # Create random prefix + repeat last 2 bytes (total 18 bytes) + # This is required by OpenPGP for integrity checking + prefix_random = secrets.token_bytes(block_size) + prefix = prefix_random + prefix_random[-2:] # 18 bytes total + + # Wrap payload in literal data packet + literal_pkt = build_literal_data_packet(payload) + + # Plaintext = prefix + literal data packet + plaintext = prefix + literal_pkt + + # Encrypt using OpenPGP CFB mode with resync + ciphertext = pgp_cfb_encrypt_resync(key, plaintext) + + return new_packet(9, ciphertext) + +def build_tag1_packet(rsa: dict, sess_key: bytes) -> bytes: + """ + Build a public-key encrypted key. + + This is a very important function, as it is able to create the packet + triggering the overflow check. This function can also be used to create + "legit" packet data. + + Format (RFC 4880, Section 5.1): + - 1 byte: version (3) + - 8 bytes: key ID (0 = any key accepted) + - 1 byte: public key algorithm (2 = RSA encrypt) + - MPI: RSA-encrypted session key + + This uses in arguments the generated RSA key pair, and the session key + to encrypt. The latter is manipulated to trigger the overflow. + + This function returns a complete packet encrypted by a session key. + """ + + # Calculate RSA modulus size in bytes + n_bytes = (rsa['n'].bit_length() + 7) // 8 + + # Session key message format: + # - 1 byte: symmetric cipher algorithm (7 = AES-128) + # - N bytes: session key + # - 2 bytes: checksum (simple sum of session key bytes) + algo_byte = bytes([7]) # AES-128 algorithm identifier + cksum = sum(sess_key) & 0xFFFF # 16-bit checksum + M = algo_byte + sess_key + struct.pack('>H', cksum) + + # PKCS#1 v1.5 padding construction + # Format: 0x02 || PS || 0x00 || M + # Total padded message must be exactly n_bytes long. + total_len = n_bytes # Total length must equal modulus size in bytes + ps_len = total_len - len(M) - 2 # Subtract 2 for 0x02 and 0x00 bytes + + if ps_len < 8: + raise ValueError(f"Padding string too short ({ps_len} bytes); need at least 8 bytes. " + f"Message length: {len(M)}, Modulus size: {n_bytes} bytes") + + # Create padding string with *ALL* bytes being 0xFF (no zero separator!) + PS = bytes([0xFF]) * ps_len + + # Construct the complete padded message + # Normal PKCS#1 v1.5 padding: 0x02 || PS || 0x00 || M + padded = bytes([0x02]) + PS + bytes([0x00]) + M + + # Verify padding construction + if len(padded) != n_bytes: + raise ValueError(f"Padded message length ({len(padded)}) doesn't match RSA modulus size ({n_bytes})") + + # Convert padded message to integer and encrypt with RSA + m_int = int.from_bytes(padded, 'big') + + # Ensure message is smaller than modulus (required for RSA) + if m_int >= rsa['n']: + raise ValueError("Padded message is larger than RSA modulus") + + # RSA encryption: c = m^e mod n + c_int = pow(m_int, rsa['e'], rsa['n']) + + # Encode encrypted result as MPI + c_mpi = mpi_encode(c_int) + + # Build complete packet + ver = bytes([3]) # Version 3 packet + key_id = b"\x00" * 8 # Key ID (0 = any key accepted) + algo = bytes([2]) # RSA encrypt algorithm + payload = ver + key_id + algo + c_mpi + + return new_packet(1, payload) + +def build_message_data(rsa: dict) -> bytes: + """ + This function creates a crafted message, with a long session key + length. + + This takes in input the RSA key components generated previously, + returning a concatenated set of PGP packets crafted for the purpose + of this test. + """ + + # Base prefix for session key (AES key + padding + size). + # Note that the crafted size is the important part for this test. + prefix = AES_KEY + b"\x00" * 16 + p32(0x10) + + # Build encrypted data packet, legit. + sedata = build_symenc_data_packet(AES_KEY, cipher_algo=7, payload=b"\x0a\x00") + + # Build multiple packets + packets = [ + # First packet, legit. + build_tag1_packet(rsa, prefix), + + # Encrypted data packet, legit. + sedata, + + # Second packet: information payload. + # + # This packet contains a longer-crafted session key, able to trigger + # the overflow check in pgcrypto. This is the critical part, and + # and you are right to pay a lot of attention here if you are + # reading this code. + build_tag1_packet(rsa, prefix) + ] + + return b"".join(packets) + +def main(): + # Default key size. + # This number can be set to a higher number if wanted, like 4096. We + # just do not need to do that here. + key_size = 2048 + + # Generate fresh RSA key pair + rsa = generate_rsa_keypair(key_size) + + # Generate the message data. + print("### Building message data", file=sys.stderr) + message_data = build_message_data(rsa) + + # Build the key containing the RSA private key + print("### Building key data", file=sys.stderr) + key_data = build_key_data(rsa) + + # Convert to hexadecimal, for the bytea used in the SQL file. + message_data = message_data.hex() + key_data = key_data.hex() + + # Split each value into lines of 72 characters, for readability. + message_data = re.sub("(.{72})", "\\1\n", message_data, 0, re.DOTALL) + key_data = re.sub("(.{72})", "\\1\n", key_data, 0, re.DOTALL) + + # Get the script filename for documentation + file_basename = os.path.basename(__file__) + + # Output the SQL test case + print(f'''-- Test for overflow with session key at decrypt. +-- Data automatically generated by scripts/{file_basename}. +-- See this file for details explaining how this data is generated. +SELECT pgp_pub_decrypt_bytea( +'\\x{message_data}'::bytea, +'\\x{key_data}'::bytea);''', + file=sys.stdout) + +if __name__ == "__main__": + main() diff --git a/contrib/pgcrypto/sql/pgp-decrypt.sql b/contrib/pgcrypto/sql/pgp-decrypt.sql index 49a0267bbcbcc..b499bf757b0d4 100644 --- a/contrib/pgcrypto/sql/pgp-decrypt.sql +++ b/contrib/pgcrypto/sql/pgp-decrypt.sql @@ -228,7 +228,7 @@ SaV9L04ky1qECNDx3XjnoKLC+H7IOQ== -----END PGP MESSAGE----- '), '0123456789abcdefghij'), 'sha1'); -select digest(pgp_sym_decrypt(dearmor(' +select digest(pgp_sym_decrypt_bytea(dearmor(' -----BEGIN PGP MESSAGE----- Comment: dat3.aes.sha1.mdc.s2k3.z0 @@ -282,6 +282,27 @@ VsxxqLSPzNLAeIspJk5G -- Routine text/binary mismatch. select pgp_sym_decrypt(pgp_sym_encrypt_bytea('P', 'key'), 'key', 'debug=1'); +-- NUL byte in text decrypt. Ciphertext source: +-- printf 'a\x00\xc' | gpg --homedir /nonexistent \ +-- --personal-compress-preferences uncompressed --textmode \ +-- --personal-cipher-preferences aes --no-emit-version --batch \ +-- --symmetric --passphrase key --armor +do $$ +begin + perform pgp_sym_decrypt(dearmor(' +-----BEGIN PGP MESSAGE----- + +jA0EBwMCXLc8pozB10Fg0jQBVUID59TLvWutJp0j6eh9ZgjqIRzdYaIymFB8y4XH +vu0YlJP5D5BX7yqZ+Pry7TlDmiFO +=rV7z +-----END PGP MESSAGE----- +'), 'key', 'debug=1'); +exception when others then + raise '%', + regexp_replace(sqlerrm, 'encoding "[^"]*"', 'encoding [REDACTED]'); +end +$$; + -- Decryption with a certain incorrect key yields an apparent BZip2-compressed -- plaintext. Ciphertext source: iterative pgp_sym_encrypt('secret', 'key') -- until the random prefix gave rise to that property. diff --git a/contrib/pgcrypto/sql/pgp-pubkey-session.sql b/contrib/pgcrypto/sql/pgp-pubkey-session.sql new file mode 100644 index 0000000000000..51792f1f4d856 --- /dev/null +++ b/contrib/pgcrypto/sql/pgp-pubkey-session.sql @@ -0,0 +1,46 @@ +-- Test for overflow with session key at decrypt. +-- Data automatically generated by scripts/pgp_session_data.py. +-- See this file for details explaining how this data is generated. +SELECT pgp_pub_decrypt_bytea( +'\xc1c04c030000000000000000020800a46f5b9b1905b49457a6485474f71ed9b46c2527e1 +da08e1f7871e12c3d38828f2076b984a595bf60f616599ca5729d547de06a258bfbbcd30 +94a321e4668cd43010f0ca8ecf931e5d39bda1152c50c367b11c723f270729245d3ebdbd +0694d320c5a5aa6a405fb45182acb3d7973cbce398e0c5060af7603cfd9ed186ebadd616 +3b50ae42bea5f6d14dda24e6d4687b434c175084515d562e896742b0ba9a1c87d5642e10 +a5550379c71cc490a052ada483b5d96526c0a600fc51755052aa77fdf72f7b4989b920e7 +b90f4b30787a46482670d5caecc7a515a926055ad5509d135702ce51a0e4c1033f2d939d +8f0075ec3428e17310da37d3d2d7ad1ce99adcc91cd446c366c402ae1ee38250343a7fcc +0f8bc28020e603d7a4795ef0dcc1c04c030000000000000000020800a46f5b9b1905b494 +57a6485474f71ed9b46c2527e1da08e1f7871e12c3d38828f2076b984a595bf60f616599 +ca5729d547de06a258bfbbcd3094a321e4668cd43010f0ca8ecf931e5d39bda1152c50c3 +67b11c723f270729245d3ebdbd0694d320c5a5aa6a405fb45182acb3d7973cbce398e0c5 +060af7603cfd9ed186ebadd6163b50ae42bea5f6d14dda24e6d4687b434c175084515d56 +2e896742b0ba9a1c87d5642e10a5550379c71cc490a052ada483b5d96526c0a600fc5175 +5052aa77fdf72f7b4989b920e7b90f4b30787a46482670d5caecc7a515a926055ad5509d +135702ce51a0e4c1033f2d939d8f0075ec3428e17310da37d3d2d7ad1ce99adc'::bytea, +'\xc7c2d8046965d657020800eef8bf1515adb1a3ee7825f75c668ea8dd3e3f9d13e958f6ad +9c55adc0c931a4bb00abe1d52cf7bb0c95d537949d277a5292ede375c6b2a67a3bf7d19f +f975bb7e7be35c2d8300dacba360a0163567372f7dc24000cc7cb6170bedc8f3b1f98c12 +07a6cb4de870a4bc61319b139dcc0e20c368fd68f8fd346d2c0b69c5aed560504e2ec6f1 +23086fe3c5540dc4dd155c0c67257c4ada862f90fe172ace344089da8135e92aca5c2709 +f1c1bc521798bb8c0365841496e709bd184132d387e0c9d5f26dc00fd06c3a76ef66a75c +138285038684707a847b7bd33cfbefbf1d336be954a8048946af97a66352adef8e8b5ae4 +c4748c6f2510265b7a8267bc370dbb00110100010007ff7e72d4f95d2d39901ac12ca5c5 +18e767e719e72340c3fab51c8c5ab1c40f31db8eaffe43533fa61e2dbca2c3f4396c0847 +e5434756acbb1f68128f4136bb135710c89137d74538908dac77967de9e821c559700dd9 +de5a2727eec1f5d12d5d74869dd1de45ed369d94a8814d23861dd163f8c27744b26b98f0 +239c2e6dd1e3493b8cc976fdc8f9a5e250f715aa4c3d7d5f237f8ee15d242e8fa941d1a0 +ed9550ab632d992a97518d142802cb0a97b251319bf5742db8d9d8cbaa06cdfba2d75bc9 +9d77a51ff20bd5ba7f15d7af6e85b904de2855d19af08d45f39deb85403033c69c767a8e +74a343b1d6c8911d34ea441ac3850e57808ed3d885835cbe6c79d10400ef16256f3d5c4c +3341516a2d2aa888df81b603f48a27f3666b40f992a857c1d11ff639cd764a9b42d5a1f8 +58b4aeee36b85508bb5e8b91ef88a7737770b330224479d9b44eae8c631bc43628b69549 +507c0a1af0be0dd7696015abea722b571eb35eefc4ab95595378ec12814727443f625fcd +183bb9b3bccf53b54dd0e5e7a50400ffe08537b2d4e6074e4a1727b658cfccdec8962302 +25e300c05690de45f7065c3d40d86f544a64d51a3e94424f9851a16d1322ebdb41fa8a45 +3131f3e2dc94e858e6396722643df382680f815e53bcdcde5da622f50530a83b217f1103 +cdd6e5e9babe1e415bbff28d44bd18c95f43bbd04afeb2a2a99af38a571c7540de21df03 +ff62c0a33d9143dd3f639893f47732c11c5a12c6052d1935f4d507b7ae1f76ab0e9a69b8 +7305a7f7c19bd509daf4903bff614bc26d118f03e461469c72c12d3a2bb4f78e4d342ce8 +487723649a01ed2b9eb11c662134502c098d55dfcd361939d8370873422c3da75a515a75 +9ffedfe7df44fb3c20f81650801a30d43b5c90b98b3eee'::bytea); diff --git a/contrib/pgrowlocks/meson.build b/contrib/pgrowlocks/meson.build index 6007a76ae754c..a645475258891 100644 --- a/contrib/pgrowlocks/meson.build +++ b/contrib/pgrowlocks/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgrowlocks_sources = files( 'pgrowlocks.c', diff --git a/contrib/pgrowlocks/pgrowlocks.c b/contrib/pgrowlocks/pgrowlocks.c index b75d80fa7a9c2..d164c4c03ad84 100644 --- a/contrib/pgrowlocks/pgrowlocks.c +++ b/contrib/pgrowlocks/pgrowlocks.c @@ -40,6 +40,7 @@ #include "utils/fmgrprotos.h" #include "utils/rel.h" #include "utils/snapmgr.h" +#include "utils/tuplestore.h" #include "utils/varlena.h" PG_MODULE_MAGIC_EXT( @@ -114,7 +115,7 @@ pgrowlocks(PG_FUNCTION_ARGS) RelationGetRelationName(rel)); /* Scan the relation */ - scan = table_beginscan(rel, GetActiveSnapshot(), 0, NULL); + scan = table_beginscan(rel, GetActiveSnapshot(), 0, NULL, SO_NONE); hscan = (HeapScanDesc) scan; attinmeta = TupleDescGetAttInMetadata(rsinfo->setDesc); @@ -141,8 +142,8 @@ pgrowlocks(PG_FUNCTION_ARGS) */ if (htsu == TM_BeingModified) { - values[Atnum_tid] = (char *) DirectFunctionCall1(tidout, - PointerGetDatum(&tuple->t_self)); + values[Atnum_tid] = DatumGetCString(DirectFunctionCall1(tidout, + PointerGetDatum(&tuple->t_self))); values[Atnum_xmax] = palloc(NCHARS * sizeof(char)); snprintf(values[Atnum_xmax], NCHARS, "%u", xmax); diff --git a/contrib/pgstattuple/meson.build b/contrib/pgstattuple/meson.build index 654245809e5d3..5e1d44a5bd944 100644 --- a/contrib/pgstattuple/meson.build +++ b/contrib/pgstattuple/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgstattuple_sources = files( 'pgstatapprox.c', diff --git a/contrib/pgstattuple/pgstatapprox.c b/contrib/pgstattuple/pgstatapprox.c index a59ff4e9d4fac..21e0b50fb4bd4 100644 --- a/contrib/pgstattuple/pgstatapprox.c +++ b/contrib/pgstattuple/pgstatapprox.c @@ -3,7 +3,7 @@ * pgstatapprox.c * Bloat estimation functions * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/pgstattuple/pgstatapprox.c @@ -23,6 +23,7 @@ #include "storage/bufmgr.h" #include "storage/freespace.h" #include "storage/procarray.h" +#include "storage/read_stream.h" PG_FUNCTION_INFO_V1(pgstattuple_approx); PG_FUNCTION_INFO_V1(pgstattuple_approx_v1_5); @@ -45,6 +46,62 @@ typedef struct output_type #define NUM_OUTPUT_COLUMNS 10 +/* + * Struct for statapprox_heap read stream callback. + */ +typedef struct StatApproxReadStreamPrivate +{ + Relation rel; + output_type *stat; + BlockNumber current_blocknum; + BlockNumber nblocks; + BlockNumber scanned; /* count of pages actually read */ + Buffer vmbuffer; /* for VM lookups */ +} StatApproxReadStreamPrivate; + +/* + * Read stream callback for statapprox_heap. + * + * This callback checks the visibility map for each block. If the block is + * all-visible, we can get the free space from the FSM without reading the + * actual page, and skip to the next block. Only the blocks that are not + * all-visible are returned for actual reading after being locked. + */ +static BlockNumber +statapprox_heap_read_stream_next(ReadStream *stream, + void *callback_private_data, + void *per_buffer_data) +{ + StatApproxReadStreamPrivate *p = + (StatApproxReadStreamPrivate *) callback_private_data; + + while (p->current_blocknum < p->nblocks) + { + BlockNumber blkno = p->current_blocknum++; + Size freespace; + + CHECK_FOR_INTERRUPTS(); + + /* + * If the page has only visible tuples, then we can find out the free + * space from the FSM and move on without reading the page. + */ + if (VM_ALL_VISIBLE(p->rel, blkno, &p->vmbuffer)) + { + freespace = GetRecordedFreeSpace(p->rel, blkno); + p->stat->tuple_len += BLCKSZ - freespace; + p->stat->free_space += freespace; + continue; + } + + /* This block needs to be read */ + p->scanned++; + return blkno; + } + + return InvalidBlockNumber; +} + /* * This function takes an already open relation and scans its pages, * skipping those that have the corresponding visibility map bit set. @@ -58,53 +115,58 @@ typedef struct output_type static void statapprox_heap(Relation rel, output_type *stat) { - BlockNumber scanned, - nblocks, - blkno; - Buffer vmbuffer = InvalidBuffer; + BlockNumber nblocks; BufferAccessStrategy bstrategy; TransactionId OldestXmin; + StatApproxReadStreamPrivate p; + ReadStream *stream; OldestXmin = GetOldestNonRemovableTransactionId(rel); bstrategy = GetAccessStrategy(BAS_BULKREAD); nblocks = RelationGetNumberOfBlocks(rel); - scanned = 0; - for (blkno = 0; blkno < nblocks; blkno++) + /* Initialize read stream private data */ + p.rel = rel; + p.stat = stat; + p.current_blocknum = 0; + p.nblocks = nblocks; + p.scanned = 0; + p.vmbuffer = InvalidBuffer; + + /* + * Create the read stream. We don't use READ_STREAM_USE_BATCHING because + * the callback accesses the visibility map which may need to read VM + * pages. While this shouldn't cause deadlocks, we err on the side of + * caution. + */ + stream = read_stream_begin_relation(READ_STREAM_FULL, + bstrategy, + rel, + MAIN_FORKNUM, + statapprox_heap_read_stream_next, + &p, + 0); + + for (;;) { Buffer buf; Page page; OffsetNumber offnum, maxoff; - Size freespace; - - CHECK_FOR_INTERRUPTS(); - - /* - * If the page has only visible tuples, then we can find out the free - * space from the FSM and move on. - */ - if (VM_ALL_VISIBLE(rel, blkno, &vmbuffer)) - { - freespace = GetRecordedFreeSpace(rel, blkno); - stat->tuple_len += BLCKSZ - freespace; - stat->free_space += freespace; - continue; - } + BlockNumber blkno; - buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, - RBM_NORMAL, bstrategy); + buf = read_stream_next_buffer(stream, NULL); + if (buf == InvalidBuffer) + break; LockBuffer(buf, BUFFER_LOCK_SHARE); page = BufferGetPage(buf); + blkno = BufferGetBlockNumber(buf); stat->free_space += PageGetExactFreeSpace(page); - /* We may count the page as scanned even if it's new/empty */ - scanned++; - if (PageIsNew(page) || PageIsEmpty(page)) { UnlockReleaseBuffer(buf); @@ -169,6 +231,9 @@ statapprox_heap(Relation rel, output_type *stat) UnlockReleaseBuffer(buf); } + Assert(p.current_blocknum == nblocks); + read_stream_end(stream); + stat->table_len = (uint64) nblocks * BLCKSZ; /* @@ -179,7 +244,7 @@ statapprox_heap(Relation rel, output_type *stat) * tuples in all-visible pages, so no correction is needed for that, and * we already accounted for the space in those pages, too. */ - stat->tuple_count = vac_estimate_reltuples(rel, nblocks, scanned, + stat->tuple_count = vac_estimate_reltuples(rel, nblocks, p.scanned, stat->tuple_count); /* It's not clear if we could get -1 here, but be safe. */ @@ -190,16 +255,16 @@ statapprox_heap(Relation rel, output_type *stat) */ if (nblocks != 0) { - stat->scanned_percent = 100.0 * scanned / nblocks; + stat->scanned_percent = 100.0 * p.scanned / nblocks; stat->tuple_percent = 100.0 * stat->tuple_len / stat->table_len; stat->dead_tuple_percent = 100.0 * stat->dead_tuple_len / stat->table_len; stat->free_percent = 100.0 * stat->free_space / stat->table_len; } - if (BufferIsValid(vmbuffer)) + if (BufferIsValid(p.vmbuffer)) { - ReleaseBuffer(vmbuffer); - vmbuffer = InvalidBuffer; + ReleaseBuffer(p.vmbuffer); + p.vmbuffer = InvalidBuffer; } } diff --git a/contrib/pgstattuple/pgstatindex.c b/contrib/pgstattuple/pgstatindex.c index 4b9d76ec4e4df..3a3f2637bd956 100644 --- a/contrib/pgstattuple/pgstatindex.c +++ b/contrib/pgstattuple/pgstatindex.c @@ -37,6 +37,7 @@ #include "funcapi.h" #include "miscadmin.h" #include "storage/bufmgr.h" +#include "storage/read_stream.h" #include "utils/rel.h" #include "utils/varlena.h" @@ -217,6 +218,9 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo) BlockNumber blkno; BTIndexStat indexStat; BufferAccessStrategy bstrategy = GetAccessStrategy(BAS_BULKREAD); + BlockRangeReadStreamPrivate p; + ReadStream *stream; + BlockNumber startblk; if (!IS_INDEX(rel) || !IS_BTREE(rel)) ereport(ERROR, @@ -273,11 +277,28 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo) indexStat.fragments = 0; /* - * Scan all blocks except the metapage + * Scan all blocks except the metapage (0th page) using streaming reads */ nblocks = RelationGetNumberOfBlocks(rel); + startblk = BTREE_METAPAGE + 1; - for (blkno = 1; blkno < nblocks; blkno++) + p.current_blocknum = startblk; + p.last_exclusive = nblocks; + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + bstrategy, + rel, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); + + for (blkno = startblk; blkno < nblocks; blkno++) { Buffer buffer; Page page; @@ -285,8 +306,7 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo) CHECK_FOR_INTERRUPTS(); - /* Read and lock buffer */ - buffer = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy); + buffer = read_stream_next_buffer(stream, NULL); LockBuffer(buffer, BUFFER_LOCK_SHARE); page = BufferGetPage(buffer); @@ -322,11 +342,12 @@ pgstatindex_impl(Relation rel, FunctionCallInfo fcinfo) else indexStat.internal_pages++; - /* Unlock and release buffer */ - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buffer); + UnlockReleaseBuffer(buffer); } + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); + relation_close(rel, AccessShareLock); /*---------------------------- @@ -514,6 +535,10 @@ pgstatginindex_internal(Oid relid, FunctionCallInfo fcinfo) bool nulls[3] = {false, false, false}; Datum result; + /* + * This uses relation_open() and not index_open(). The latter allows + * partitioned indexes, and these are forbidden here. + */ rel = relation_open(relid, AccessShareLock); if (!IS_INDEX(rel) || !IS_GIN(rel)) @@ -596,7 +621,14 @@ pgstathashindex(PG_FUNCTION_ARGS) HashMetaPage metap; float8 free_percent; uint64 total_space; + BlockRangeReadStreamPrivate p; + ReadStream *stream; + BlockNumber startblk; + /* + * This uses relation_open() and not index_open(). The latter allows + * partitioned indexes, and these are forbidden here. + */ rel = relation_open(relid, AccessShareLock); if (!IS_INDEX(rel) || !IS_HASH(rel)) @@ -636,18 +668,35 @@ pgstathashindex(PG_FUNCTION_ARGS) /* prepare access strategy for this index */ bstrategy = GetAccessStrategy(BAS_BULKREAD); - /* Start from blkno 1 as 0th block is metapage */ - for (blkno = 1; blkno < nblocks; blkno++) + /* Scan all blocks except the metapage (0th page) using streaming reads */ + startblk = HASH_METAPAGE + 1; + + p.current_blocknum = startblk; + p.last_exclusive = nblocks; + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + bstrategy, + rel, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); + + for (blkno = startblk; blkno < nblocks; blkno++) { Buffer buf; Page page; CHECK_FOR_INTERRUPTS(); - buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, - bstrategy); + buf = read_stream_next_buffer(stream, NULL); LockBuffer(buf, BUFFER_LOCK_SHARE); - page = (Page) BufferGetPage(buf); + page = BufferGetPage(buf); if (PageIsNew(page)) stats.unused_pages++; @@ -690,8 +739,11 @@ pgstathashindex(PG_FUNCTION_ARGS) UnlockReleaseBuffer(buf); } + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); + /* Done accessing the index */ - index_close(rel, AccessShareLock); + relation_close(rel, AccessShareLock); /* Count unused pages as free space. */ stats.free_space += (uint64) stats.unused_pages * stats.space_per_page; diff --git a/contrib/pgstattuple/pgstattuple.c b/contrib/pgstattuple/pgstattuple.c index 0d9c2b0b65369..6a7f8cb4a7ca5 100644 --- a/contrib/pgstattuple/pgstattuple.c +++ b/contrib/pgstattuple/pgstattuple.c @@ -378,7 +378,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo) buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block, RBM_NORMAL, hscan->rs_strategy); LockBuffer(buffer, BUFFER_LOCK_SHARE); - stat.free_space += PageGetExactFreeSpace((Page) BufferGetPage(buffer)); + stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer)); UnlockReleaseBuffer(buffer); block++; } @@ -391,7 +391,7 @@ pgstat_heap(Relation rel, FunctionCallInfo fcinfo) buffer = ReadBufferExtended(rel, MAIN_FORKNUM, block, RBM_NORMAL, hscan->rs_strategy); LockBuffer(buffer, BUFFER_LOCK_SHARE); - stat.free_space += PageGetExactFreeSpace((Page) BufferGetPage(buffer)); + stat.free_space += PageGetExactFreeSpace(BufferGetPage(buffer)); UnlockReleaseBuffer(buffer); block++; } @@ -424,7 +424,7 @@ pgstat_btree_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno, /* fully empty page */ stat->free_space += BLCKSZ; } - else + else if (PageGetSpecialSize(page) == MAXALIGN(sizeof(BTPageOpaqueData))) { BTPageOpaque opaque; @@ -458,10 +458,16 @@ pgstat_hash_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno, Buffer buf; Page page; - buf = _hash_getbuf_with_strategy(rel, blkno, HASH_READ, 0, bstrategy); + buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy); + LockBuffer(buf, HASH_READ); page = BufferGetPage(buf); - if (PageGetSpecialSize(page) == MAXALIGN(sizeof(HashPageOpaqueData))) + if (PageIsNew(page)) + { + /* fully empty page */ + stat->free_space += BLCKSZ; + } + else if (PageGetSpecialSize(page) == MAXALIGN(sizeof(HashPageOpaqueData))) { HashPageOpaque opaque; @@ -502,17 +508,23 @@ pgstat_gist_page(pgstattuple_type *stat, Relation rel, BlockNumber blkno, buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, bstrategy); LockBuffer(buf, GIST_SHARE); - gistcheckpage(rel, buf); page = BufferGetPage(buf); - - if (GistPageIsLeaf(page)) + if (PageIsNew(page)) { - pgstat_index_page(stat, page, FirstOffsetNumber, - PageGetMaxOffsetNumber(page)); + /* fully empty page */ + stat->free_space += BLCKSZ; } - else + else if (PageGetSpecialSize(page) == MAXALIGN(sizeof(GISTPageOpaqueData))) { - /* root or node */ + if (GistPageIsLeaf(page)) + { + pgstat_index_page(stat, page, FirstOffsetNumber, + PageGetMaxOffsetNumber(page)); + } + else + { + /* root or node */ + } } UnlockReleaseBuffer(buf); diff --git a/contrib/postgres_fdw/.gitignore b/contrib/postgres_fdw/.gitignore index 5dcb3ff972350..b4903eba657fa 100644 --- a/contrib/postgres_fdw/.gitignore +++ b/contrib/postgres_fdw/.gitignore @@ -1,4 +1,6 @@ # Generated subdirectories /log/ /results/ +/output_iso/ /tmp_check/ +/tmp_check_iso/ diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile index adfbd2ef758e0..b8c78b58804aa 100644 --- a/contrib/postgres_fdw/Makefile +++ b/contrib/postgres_fdw/Makefile @@ -14,9 +14,11 @@ PG_CPPFLAGS = -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) EXTENSION = postgres_fdw -DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql +DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql postgres_fdw--1.2--1.3.sql REGRESS = postgres_fdw query_cancel +ISOLATION = eval_plan_qual +ISOLATION_OPTS = --load-extension=postgres_fdw TAP_TESTS = 1 ifdef USE_PGXS diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 304f3c20f8356..06673017bcfaf 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -3,7 +3,7 @@ * connection.c * Connection management functions for postgres_fdw * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/postgres_fdw/connection.c @@ -16,6 +16,7 @@ #include #endif +#include "access/htup_details.h" #include "access/xact.h" #include "catalog/pg_user_mapping.h" #include "commands/defrem.h" @@ -32,6 +33,7 @@ #include "utils/hsearch.h" #include "utils/inval.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" /* * Connection cache hash table entry @@ -58,6 +60,7 @@ typedef struct ConnCacheEntry /* Remaining fields are invalid when conn is NULL: */ int xact_depth; /* 0 = no xact open, 1 = main xact open, 2 = * one level of subxact open, etc */ + bool xact_read_only; /* xact r/o state */ bool have_prep_stmt; /* have we prepared any stmts in this xact? */ bool have_error; /* have any subxacts aborted in this xact? */ bool changing_xact_state; /* xact state change in process */ @@ -84,6 +87,12 @@ static unsigned int prep_stmt_number = 0; /* tracks whether any work is needed in callback functions */ static bool xact_got_connection = false; +/* + * tracks the topmost read-only local transaction's nesting level determined + * by GetTopReadOnlyTransactionNestLevel() + */ +static int read_only_level = 0; + /* custom wait event values, retrieved from shared memory */ static uint32 pgfdw_we_cleanup_result = 0; static uint32 pgfdw_we_connect = 0; @@ -131,6 +140,7 @@ PG_FUNCTION_INFO_V1(postgres_fdw_get_connections); PG_FUNCTION_INFO_V1(postgres_fdw_get_connections_1_2); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect_all); +PG_FUNCTION_INFO_V1(postgres_fdw_connection); /* prototypes of private functions */ static void make_new_connection(ConnCacheEntry *entry, UserMapping *user); @@ -142,12 +152,15 @@ static void do_sql_command_begin(PGconn *conn, const char *sql); static void do_sql_command_end(PGconn *conn, const char *sql, bool consume_input); static void begin_remote_xact(ConnCacheEntry *entry); +static void pgfdw_report_internal(int elevel, PGresult *res, PGconn *conn, + const char *sql); static void pgfdw_xact_callback(XactEvent event, void *arg); static void pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void *arg); -static void pgfdw_inval_callback(Datum arg, int cacheid, uint32 hashvalue); +static void pgfdw_inval_callback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static void pgfdw_reject_incomplete_xact_state_change(ConnCacheEntry *entry); static void pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel); static bool pgfdw_cancel_query(PGconn *conn); @@ -372,6 +385,7 @@ make_new_connection(ConnCacheEntry *entry, UserMapping *user) /* Reset all transient state fields, to be sure all are clean */ entry->xact_depth = 0; + entry->xact_read_only = false; entry->have_prep_stmt = false; entry->have_error = false; entry->changing_xact_state = false; @@ -462,7 +476,7 @@ pgfdw_security_check(const char **keywords, const char **values, UserMapping *us * assume that UseScramPassthrough is also true since SCRAM options are * only set when UseScramPassthrough is enabled. */ - if (MyProcPort->has_scram_keys && pgfdw_has_required_scram_options(keywords, values)) + if (MyProcPort != NULL && MyProcPort->has_scram_keys && pgfdw_has_required_scram_options(keywords, values)) return; ereport(ERROR, @@ -473,141 +487,159 @@ pgfdw_security_check(const char **keywords, const char **values, UserMapping *us } /* - * Connect to remote server using specified server and user mapping properties. + * Construct connection params from generic options of ForeignServer and + * UserMapping. (Some of them might not be libpq options, in which case we'll + * just waste a few array slots.) */ -static PGconn * -connect_pg_server(ForeignServer *server, UserMapping *user) +static void +construct_connection_params(ForeignServer *server, UserMapping *user, + const char ***p_keywords, const char ***p_values, + char **p_appname) { - PGconn *volatile conn = NULL; + const char **keywords; + const char **values; + char *appname = NULL; + int n; /* - * Use PG_TRY block to ensure closing connection on error. + * Add 4 extra slots for application_name, fallback_application_name, + * client_encoding, end marker, and 3 extra slots for scram keys and + * required scram pass-through options. */ - PG_TRY(); - { - const char **keywords; - const char **values; - char *appname = NULL; - int n; - - /* - * Construct connection params from generic options of ForeignServer - * and UserMapping. (Some of them might not be libpq options, in - * which case we'll just waste a few array slots.) Add 4 extra slots - * for application_name, fallback_application_name, client_encoding, - * end marker, and 3 extra slots for scram keys and required scram - * pass-through options. - */ - n = list_length(server->options) + list_length(user->options) + 4 + 3; - keywords = (const char **) palloc(n * sizeof(char *)); - values = (const char **) palloc(n * sizeof(char *)); + n = list_length(server->options) + list_length(user->options) + 4 + 3; + keywords = (const char **) palloc(n * sizeof(char *)); + values = (const char **) palloc(n * sizeof(char *)); - n = 0; - n += ExtractConnectionOptions(server->options, - keywords + n, values + n); - n += ExtractConnectionOptions(user->options, - keywords + n, values + n); + n = 0; + n += ExtractConnectionOptions(server->options, + keywords + n, values + n); + n += ExtractConnectionOptions(user->options, + keywords + n, values + n); - /* - * Use pgfdw_application_name as application_name if set. - * - * PQconnectdbParams() processes the parameter arrays from start to - * end. If any key word is repeated, the last value is used. Therefore - * note that pgfdw_application_name must be added to the arrays after - * options of ForeignServer are, so that it can override - * application_name set in ForeignServer. - */ - if (pgfdw_application_name && *pgfdw_application_name != '\0') - { - keywords[n] = "application_name"; - values[n] = pgfdw_application_name; - n++; - } + /* + * Use pgfdw_application_name as application_name if set. + * + * PQconnectdbParams() processes the parameter arrays from start to end. + * If any key word is repeated, the last value is used. Therefore note + * that pgfdw_application_name must be added to the arrays after options + * of ForeignServer are, so that it can override application_name set in + * ForeignServer. + */ + if (pgfdw_application_name && *pgfdw_application_name != '\0') + { + keywords[n] = "application_name"; + values[n] = pgfdw_application_name; + n++; + } - /* - * Search the parameter arrays to find application_name setting, and - * replace escape sequences in it with status information if found. - * The arrays are searched backwards because the last value is used if - * application_name is repeatedly set. - */ - for (int i = n - 1; i >= 0; i--) + /* + * Search the parameter arrays to find application_name setting, and + * replace escape sequences in it with status information if found. The + * arrays are searched backwards because the last value is used if + * application_name is repeatedly set. + */ + for (int i = n - 1; i >= 0; i--) + { + if (strcmp(keywords[i], "application_name") == 0 && + *(values[i]) != '\0') { - if (strcmp(keywords[i], "application_name") == 0 && - *(values[i]) != '\0') + /* + * Use this application_name setting if it's not empty string even + * after any escape sequences in it are replaced. + */ + appname = process_pgfdw_appname(values[i]); + if (appname[0] != '\0') { - /* - * Use this application_name setting if it's not empty string - * even after any escape sequences in it are replaced. - */ - appname = process_pgfdw_appname(values[i]); - if (appname[0] != '\0') - { - values[i] = appname; - break; - } - - /* - * This empty application_name is not used, so we set - * values[i] to NULL and keep searching the array to find the - * next one. - */ - values[i] = NULL; - pfree(appname); - appname = NULL; + values[i] = appname; + break; } + + /* + * This empty application_name is not used, so we set values[i] to + * NULL and keep searching the array to find the next one. + */ + values[i] = NULL; + pfree(appname); + appname = NULL; } + } - /* Use "postgres_fdw" as fallback_application_name */ - keywords[n] = "fallback_application_name"; - values[n] = "postgres_fdw"; + *p_appname = appname; + + /* Use "postgres_fdw" as fallback_application_name */ + keywords[n] = "fallback_application_name"; + values[n] = "postgres_fdw"; + n++; + + /* Set client_encoding so that libpq can convert encoding properly. */ + keywords[n] = "client_encoding"; + values[n] = GetDatabaseEncodingName(); + n++; + + /* Add required SCRAM pass-through connection options if it's enabled. */ + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) + { + int len; + int encoded_len; + + keywords[n] = "scram_client_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, + sizeof(MyProcPort->scram_ClientKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM client key"); n++; - /* Set client_encoding so that libpq can convert encoding properly. */ - keywords[n] = "client_encoding"; - values[n] = GetDatabaseEncodingName(); + keywords[n] = "scram_server_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, + sizeof(MyProcPort->scram_ServerKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM server key"); n++; - /* Add required SCRAM pass-through connection options if it's enabled. */ - if (MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) - { - int len; - int encoded_len; - - keywords[n] = "scram_client_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, - sizeof(MyProcPort->scram_ClientKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM client key"); - n++; - - keywords[n] = "scram_server_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, - sizeof(MyProcPort->scram_ServerKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM server key"); - n++; + /* + * Require scram-sha-256 to ensure that no other auth method is used + * when connecting with foreign server. + */ + keywords[n] = "require_auth"; + values[n] = "scram-sha-256"; + n++; + } - /* - * Require scram-sha-256 to ensure that no other auth method is - * used when connecting with foreign server. - */ - keywords[n] = "require_auth"; - values[n] = "scram-sha-256"; - n++; - } + keywords[n] = values[n] = NULL; - keywords[n] = values[n] = NULL; + /* Verify the set of connection parameters. */ + check_conn_params(keywords, values, user); + + *p_keywords = keywords; + *p_values = values; +} + +/* + * Connect to remote server using specified server and user mapping properties. + */ +static PGconn * +connect_pg_server(ForeignServer *server, UserMapping *user) +{ + PGconn *volatile conn = NULL; + + /* + * Use PG_TRY block to ensure closing connection on error. + */ + PG_TRY(); + { + const char **keywords; + const char **values; + char *appname; - /* Verify the set of connection parameters. */ - check_conn_params(keywords, values, user); + construct_connection_params(server, user, &keywords, &values, &appname); /* first time, allocate or get the custom wait event */ if (pgfdw_we_connect == 0) @@ -625,6 +657,9 @@ connect_pg_server(ForeignServer *server, UserMapping *user) server->servername), errdetail_internal("%s", pchomp(PQerrorMessage(conn))))); + PQsetNoticeReceiver(conn, libpqsrv_notice_receiver, + "received message via remote connection"); + /* Perform post-connection security checks. */ pgfdw_security_check(keywords, values, user, conn); @@ -660,8 +695,9 @@ disconnect_pg_server(ConnCacheEntry *entry) } /* - * Return true if the password_required is defined and false for this user - * mapping, otherwise false. The mapping has been pre-validated. + * Check and return the value of password_required, if defined; otherwise, + * return true, which is the default value of it. The mapping has been + * pre-validated. */ static bool UserMappingPasswordRequired(UserMapping *user) @@ -743,7 +779,7 @@ check_conn_params(const char **keywords, const char **values, UserMapping *user) * assume that UseScramPassthrough is also true since SCRAM options are * only set when UseScramPassthrough is enabled. */ - if (MyProcPort->has_scram_keys && pgfdw_has_required_scram_options(keywords, values)) + if (MyProcPort != NULL && MyProcPort->has_scram_keys && pgfdw_has_required_scram_options(keywords, values)) return; ereport(ERROR, @@ -812,7 +848,7 @@ static void do_sql_command_begin(PGconn *conn, const char *sql) { if (!PQsendQuery(conn, sql)) - pgfdw_report_error(ERROR, NULL, conn, false, sql); + pgfdw_report_error(NULL, conn, sql); } static void @@ -827,10 +863,10 @@ do_sql_command_end(PGconn *conn, const char *sql, bool consume_input) * would be large compared to the overhead of PQconsumeInput.) */ if (consume_input && !PQconsumeInput(conn)) - pgfdw_report_error(ERROR, NULL, conn, false, sql); + pgfdw_report_error(NULL, conn, sql); res = pgfdw_get_result(conn); if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, conn, true, sql); + pgfdw_report_error(res, conn, sql); PQclear(res); } @@ -843,29 +879,106 @@ do_sql_command_end(PGconn *conn, const char *sql, bool consume_input) * those scans. A disadvantage is that we can't provide sane emulation of * READ COMMITTED behavior --- it would be nice if we had some other way to * control which remote queries share a snapshot. + * + * Note also that we always start the remote transaction with the same + * read/write and deferrable properties as the local transaction, and start + * the remote subtransaction with the same read/write property as the local + * subtransaction. */ static void begin_remote_xact(ConnCacheEntry *entry) { int curlevel = GetCurrentTransactionNestLevel(); - /* Start main transaction if we haven't yet */ + /* + * If the current local (sub)transaction is read-only, set the topmost + * read-only local transaction's nesting level if we haven't yet. + * + * Note: once it's set, it's retained until the topmost read-only local + * transaction is committed/aborted (see pgfdw_xact_callback and + * pgfdw_subxact_callback). + */ + if (XactReadOnly) + { + if (read_only_level == 0) + read_only_level = GetTopReadOnlyTransactionNestLevel(); + Assert(read_only_level > 0); + } + else + Assert(read_only_level == 0); + + /* + * Start main transaction if we haven't yet; otherwise, change the current + * remote (sub)transaction's read/write mode if needed. + */ if (entry->xact_depth <= 0) { - const char *sql; + /* + * This is the case when we haven't yet started a main transaction. + */ + StringInfoData sql; + bool ro = (read_only_level == 1); elog(DEBUG3, "starting remote transaction on connection %p", entry->conn); + initStringInfo(&sql); + appendStringInfoString(&sql, "START TRANSACTION ISOLATION LEVEL "); if (IsolationIsSerializable()) - sql = "START TRANSACTION ISOLATION LEVEL SERIALIZABLE"; + appendStringInfoString(&sql, "SERIALIZABLE"); else - sql = "START TRANSACTION ISOLATION LEVEL REPEATABLE READ"; + appendStringInfoString(&sql, "REPEATABLE READ"); + if (ro) + appendStringInfoString(&sql, " READ ONLY"); + if (XactDeferrable) + appendStringInfoString(&sql, " DEFERRABLE"); entry->changing_xact_state = true; - do_sql_command(entry->conn, sql); + do_sql_command(entry->conn, sql.data); entry->xact_depth = 1; + if (ro) + { + Assert(!entry->xact_read_only); + entry->xact_read_only = true; + } entry->changing_xact_state = false; } + else if (!entry->xact_read_only) + { + /* + * The remote (sub)transaction has been opened in read-write mode. + */ + Assert(read_only_level == 0 || + entry->xact_depth <= read_only_level); + + /* + * If its nesting depth matches read_only_level, it means that the + * local read-write (sub)transaction that started it has changed to + * read-only after that; in which case change it to read-only as well. + * Otherwise, the local (sub)transaction is still read-write, so there + * is no need to do anything. + */ + if (entry->xact_depth == read_only_level) + { + entry->changing_xact_state = true; + do_sql_command(entry->conn, "SET transaction_read_only = on"); + entry->xact_read_only = true; + entry->changing_xact_state = false; + } + } + else + { + /* + * The remote (sub)transaction has been opened in read-only mode. + */ + Assert(read_only_level > 0 && + entry->xact_depth >= read_only_level); + + /* + * The local read-only (sub)transaction that started it is guaranteed + * to be still read-only (see check_transaction_read_only), so there + * is no need to do anything. + */ + } /* * If we're in a subtransaction, stack up savepoints to match our level. @@ -874,12 +987,21 @@ begin_remote_xact(ConnCacheEntry *entry) */ while (entry->xact_depth < curlevel) { - char sql[64]; + StringInfoData sql; + bool ro = (entry->xact_depth + 1 == read_only_level); - snprintf(sql, sizeof(sql), "SAVEPOINT s%d", entry->xact_depth + 1); + initStringInfo(&sql); + appendStringInfo(&sql, "SAVEPOINT s%d", entry->xact_depth + 1); + if (ro) + appendStringInfoString(&sql, "; SET transaction_read_only = on"); entry->changing_xact_state = true; - do_sql_command(entry->conn, sql); + do_sql_command(entry->conn, sql.data); entry->xact_depth++; + if (ro) + { + Assert(!entry->xact_read_only); + entry->xact_read_only = true; + } entry->changing_xact_state = false; } } @@ -963,63 +1085,73 @@ pgfdw_get_result(PGconn *conn) /* * Report an error we got from the remote server. * - * elevel: error level to use (typically ERROR, but might be less) - * res: PGresult containing the error + * Callers should use pgfdw_report_error() to throw an error, or use + * pgfdw_report() for lesser message levels. (We make this distinction + * so that pgfdw_report_error() can be marked noreturn.) + * + * res: PGresult containing the error (might be NULL) * conn: connection we did the query on - * clear: if true, PQclear the result (otherwise caller will handle it) * sql: NULL, or text of remote command we tried to execute * + * If "res" is not NULL, it'll be PQclear'ed here (unless we throw error, + * in which case memory context cleanup will clear it eventually). + * * Note: callers that choose not to throw ERROR for a remote error are * responsible for making sure that the associated ConnCacheEntry gets * marked with have_error = true. */ void -pgfdw_report_error(int elevel, PGresult *res, PGconn *conn, - bool clear, const char *sql) +pgfdw_report_error(PGresult *res, PGconn *conn, const char *sql) { - /* If requested, PGresult must be released before leaving this function. */ - PG_TRY(); - { - char *diag_sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE); - char *message_primary = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); - char *message_detail = PQresultErrorField(res, PG_DIAG_MESSAGE_DETAIL); - char *message_hint = PQresultErrorField(res, PG_DIAG_MESSAGE_HINT); - char *message_context = PQresultErrorField(res, PG_DIAG_CONTEXT); - int sqlstate; - - if (diag_sqlstate) - sqlstate = MAKE_SQLSTATE(diag_sqlstate[0], - diag_sqlstate[1], - diag_sqlstate[2], - diag_sqlstate[3], - diag_sqlstate[4]); - else - sqlstate = ERRCODE_CONNECTION_FAILURE; + pgfdw_report_internal(ERROR, res, conn, sql); + pg_unreachable(); +} - /* - * If we don't get a message from the PGresult, try the PGconn. This - * is needed because for connection-level failures, PQgetResult may - * just return NULL, not a PGresult at all. - */ - if (message_primary == NULL) - message_primary = pchomp(PQerrorMessage(conn)); - - ereport(elevel, - (errcode(sqlstate), - (message_primary != NULL && message_primary[0] != '\0') ? - errmsg_internal("%s", message_primary) : - errmsg("could not obtain message string for remote error"), - message_detail ? errdetail_internal("%s", message_detail) : 0, - message_hint ? errhint("%s", message_hint) : 0, - message_context ? errcontext("%s", message_context) : 0, - sql ? errcontext("remote SQL command: %s", sql) : 0)); - } - PG_FINALLY(); - { - if (clear) - PQclear(res); - } - PG_END_TRY(); +void +pgfdw_report(int elevel, PGresult *res, PGconn *conn, const char *sql) +{ + Assert(elevel < ERROR); /* use pgfdw_report_error for that */ + pgfdw_report_internal(elevel, res, conn, sql); +} + +static void +pgfdw_report_internal(int elevel, PGresult *res, PGconn *conn, + const char *sql) +{ + char *diag_sqlstate = PQresultErrorField(res, PG_DIAG_SQLSTATE); + char *message_primary = PQresultErrorField(res, PG_DIAG_MESSAGE_PRIMARY); + char *message_detail = PQresultErrorField(res, PG_DIAG_MESSAGE_DETAIL); + char *message_hint = PQresultErrorField(res, PG_DIAG_MESSAGE_HINT); + char *message_context = PQresultErrorField(res, PG_DIAG_CONTEXT); + int sqlstate; + + if (diag_sqlstate) + sqlstate = MAKE_SQLSTATE(diag_sqlstate[0], + diag_sqlstate[1], + diag_sqlstate[2], + diag_sqlstate[3], + diag_sqlstate[4]); + else + sqlstate = ERRCODE_CONNECTION_FAILURE; + + /* + * If we don't get a message from the PGresult, try the PGconn. This is + * needed because for connection-level failures, PQgetResult may just + * return NULL, not a PGresult at all. + */ + if (message_primary == NULL) + message_primary = pchomp(PQerrorMessage(conn)); + + ereport(elevel, + (errcode(sqlstate), + (message_primary != NULL && message_primary[0] != '\0') ? + errmsg_internal("%s", message_primary) : + errmsg("could not obtain message string for remote error"), + message_detail ? errdetail_internal("%s", message_detail) : 0, + message_hint ? errhint("%s", message_hint) : 0, + message_context ? errcontext("%s", message_context) : 0, + sql ? errcontext("remote SQL command: %s", sql) : 0)); + PQclear(res); } /* @@ -1174,6 +1306,9 @@ pgfdw_xact_callback(XactEvent event, void *arg) /* Also reset cursor numbering for next transaction */ cursor_number = 0; + + /* Likewise for read_only_level */ + read_only_level = 0; } /* @@ -1272,6 +1407,10 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid, false); } } + + /* If in read_only_level, reset it */ + if (curlevel == read_only_level) + read_only_level = 0; } /* @@ -1293,7 +1432,7 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid, * individual option values, but it seems too much effort for the gain. */ static void -pgfdw_inval_callback(Datum arg, int cacheid, uint32 hashvalue) +pgfdw_inval_callback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { HASH_SEQ_STATUS scan; ConnCacheEntry *entry; @@ -1374,6 +1513,9 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel) /* Reset state to show we're out of a transaction */ entry->xact_depth = 0; + /* Reset xact r/o state */ + entry->xact_read_only = false; + /* * If the connection isn't in a good idle state, it is marked as * invalid or keep_connections option of its server is disabled, then @@ -1394,6 +1536,10 @@ pgfdw_reset_xact_state(ConnCacheEntry *entry, bool toplevel) { /* Reset state to show we're out of a subtransaction */ entry->xact_depth--; + + /* If in read_only_level, reset xact r/o state */ + if (entry->xact_depth + 1 == read_only_level) + entry->xact_read_only = false; } } @@ -1542,7 +1688,7 @@ pgfdw_exec_cleanup_query_begin(PGconn *conn, const char *query) */ if (!PQsendQuery(conn, query)) { - pgfdw_report_error(WARNING, NULL, conn, false, query); + pgfdw_report(WARNING, NULL, conn, query); return false; } @@ -1567,7 +1713,7 @@ pgfdw_exec_cleanup_query_end(PGconn *conn, const char *query, */ if (consume_input && !PQconsumeInput(conn)) { - pgfdw_report_error(WARNING, NULL, conn, false, query); + pgfdw_report(WARNING, NULL, conn, query); return false; } @@ -1579,7 +1725,7 @@ pgfdw_exec_cleanup_query_end(PGconn *conn, const char *query, (errmsg("could not get query result due to timeout"), errcontext("remote SQL command: %s", query))); else - pgfdw_report_error(WARNING, NULL, conn, false, query); + pgfdw_report(WARNING, NULL, conn, query); return false; } @@ -1587,7 +1733,7 @@ pgfdw_exec_cleanup_query_end(PGconn *conn, const char *query, /* Issue a warning if not successful. */ if (PQresultStatus(result) != PGRES_COMMAND_OK) { - pgfdw_report_error(WARNING, result, conn, true, query); + pgfdw_report(WARNING, result, conn, query); return ignore_errors; } PQclear(result); @@ -1615,103 +1761,90 @@ pgfdw_get_cleanup_result(PGconn *conn, TimestampTz endtime, PGresult **result, bool *timed_out) { - volatile bool failed = false; - PGresult *volatile last_res = NULL; + bool failed = false; + PGresult *last_res = NULL; + int canceldelta = RETRY_CANCEL_TIMEOUT * 2; *result = NULL; *timed_out = false; - - /* In what follows, do not leak any PGresults on an error. */ - PG_TRY(); + for (;;) { - int canceldelta = RETRY_CANCEL_TIMEOUT * 2; + PGresult *res; - for (;;) + while (PQisBusy(conn)) { - PGresult *res; + int wc; + TimestampTz now = GetCurrentTimestamp(); + long cur_timeout; - while (PQisBusy(conn)) + /* If timeout has expired, give up. */ + if (now >= endtime) { - int wc; - TimestampTz now = GetCurrentTimestamp(); - long cur_timeout; - - /* If timeout has expired, give up. */ - if (now >= endtime) - { - *timed_out = true; - failed = true; - goto exit; - } + *timed_out = true; + failed = true; + goto exit; + } - /* If we need to re-issue the cancel request, do that. */ - if (now >= retrycanceltime) - { - /* We ignore failure to issue the repeated request. */ - (void) libpqsrv_cancel(conn, endtime); + /* If we need to re-issue the cancel request, do that. */ + if (now >= retrycanceltime) + { + /* We ignore failure to issue the repeated request. */ + (void) libpqsrv_cancel(conn, endtime); - /* Recompute "now" in case that took measurable time. */ - now = GetCurrentTimestamp(); + /* Recompute "now" in case that took measurable time. */ + now = GetCurrentTimestamp(); - /* Adjust re-cancel timeout in increasing steps. */ - retrycanceltime = TimestampTzPlusMilliseconds(now, - canceldelta); - canceldelta += canceldelta; - } + /* Adjust re-cancel timeout in increasing steps. */ + retrycanceltime = TimestampTzPlusMilliseconds(now, + canceldelta); + canceldelta += canceldelta; + } - /* If timeout has expired, give up, else get sleep time. */ - cur_timeout = TimestampDifferenceMilliseconds(now, - Min(endtime, - retrycanceltime)); - if (cur_timeout <= 0) - { - *timed_out = true; - failed = true; - goto exit; - } + /* If timeout has expired, give up, else get sleep time. */ + cur_timeout = TimestampDifferenceMilliseconds(now, + Min(endtime, + retrycanceltime)); + if (cur_timeout <= 0) + { + *timed_out = true; + failed = true; + goto exit; + } - /* first time, allocate or get the custom wait event */ - if (pgfdw_we_cleanup_result == 0) - pgfdw_we_cleanup_result = WaitEventExtensionNew("PostgresFdwCleanupResult"); + /* first time, allocate or get the custom wait event */ + if (pgfdw_we_cleanup_result == 0) + pgfdw_we_cleanup_result = WaitEventExtensionNew("PostgresFdwCleanupResult"); - /* Sleep until there's something to do */ - wc = WaitLatchOrSocket(MyLatch, - WL_LATCH_SET | WL_SOCKET_READABLE | - WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, - PQsocket(conn), - cur_timeout, pgfdw_we_cleanup_result); - ResetLatch(MyLatch); + /* Sleep until there's something to do */ + wc = WaitLatchOrSocket(MyLatch, + WL_LATCH_SET | WL_SOCKET_READABLE | + WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + PQsocket(conn), + cur_timeout, pgfdw_we_cleanup_result); + ResetLatch(MyLatch); - CHECK_FOR_INTERRUPTS(); + CHECK_FOR_INTERRUPTS(); - /* Data available in socket? */ - if (wc & WL_SOCKET_READABLE) + /* Data available in socket? */ + if (wc & WL_SOCKET_READABLE) + { + if (!PQconsumeInput(conn)) { - if (!PQconsumeInput(conn)) - { - /* connection trouble */ - failed = true; - goto exit; - } + /* connection trouble */ + failed = true; + goto exit; } } + } - res = PQgetResult(conn); - if (res == NULL) - break; /* query is complete */ + res = PQgetResult(conn); + if (res == NULL) + break; /* query is complete */ - PQclear(last_res); - last_res = res; - } -exit: ; - } - PG_CATCH(); - { PQclear(last_res); - PG_RE_THROW(); + last_res = res; } - PG_END_TRY(); - +exit: if (failed) PQclear(last_res); else @@ -2305,6 +2438,56 @@ postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo, } } +/* + * Values in connection strings must be enclosed in single quotes. Single + * quotes and backslashes must be escaped with backslash. NB: these rules are + * different from the rules for escaping a SQL literal. + */ +static void +appendEscapedValue(StringInfo str, const char *val) +{ + appendStringInfoChar(str, '\''); + for (int i = 0; val[i] != '\0'; i++) + { + if (val[i] == '\\' || val[i] == '\'') + appendStringInfoChar(str, '\\'); + appendStringInfoChar(str, val[i]); + } + appendStringInfoChar(str, '\''); +} + +Datum +postgres_fdw_connection(PG_FUNCTION_ARGS) +{ + Oid userid = PG_GETARG_OID(0); + Oid serverid = PG_GETARG_OID(1); + ForeignServer *server = GetForeignServer(serverid); + UserMapping *user = GetUserMapping(userid, serverid); + StringInfoData str; + const char **keywords; + const char **values; + char *appname; + char *sep = ""; + + construct_connection_params(server, user, &keywords, &values, &appname); + + initStringInfo(&str); + for (int i = 0; keywords[i] != NULL; i++) + { + if (values[i] == NULL) + continue; + appendStringInfo(&str, "%s%s = ", sep, keywords[i]); + appendEscapedValue(&str, values[i]); + sep = " "; + } + + if (appname != NULL) + pfree(appname); + pfree(keywords); + pfree(values); + PG_RETURN_TEXT_P(cstring_to_text(str.data)); +} + /* * List active foreign server connections. * @@ -2557,7 +2740,7 @@ pgfdw_has_required_scram_options(const char **keywords, const char **values) } } - has_scram_keys = has_scram_client_key && has_scram_server_key && MyProcPort->has_scram_keys; + has_scram_keys = has_scram_client_key && has_scram_server_key && MyProcPort != NULL && MyProcPort->has_scram_keys; return (has_scram_keys && has_require_auth); } diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index d9970dd675336..2dcc6c8af1b3e 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -24,7 +24,7 @@ * with collations that match the remote table's columns, which we can * consider to be user error. * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/postgres_fdw/deparse.c @@ -39,6 +39,7 @@ #include "catalog/pg_aggregate.h" #include "catalog/pg_authid.h" #include "catalog/pg_collation.h" +#include "catalog/pg_database.h" #include "catalog/pg_namespace.h" #include "catalog/pg_operator.h" #include "catalog/pg_opfamily.h" @@ -160,6 +161,7 @@ static void deparseDistinctExpr(DistinctExpr *node, deparse_expr_cxt *context); static void deparseScalarArrayOpExpr(ScalarArrayOpExpr *node, deparse_expr_cxt *context); static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context); +static void deparseArrayCoerceExpr(ArrayCoerceExpr *node, deparse_expr_cxt *context); static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context); static void deparseNullTest(NullTest *node, deparse_expr_cxt *context); static void deparseCaseExpr(CaseExpr *node, deparse_expr_cxt *context); @@ -455,6 +457,11 @@ foreign_expr_walker(Node *node, AuthIdRelationId, fpinfo)) return false; break; + case REGDATABASEOID: + if (!is_shippable(DatumGetObjectId(c->constvalue), + DatabaseRelationId, fpinfo)) + return false; + break; } } @@ -696,6 +703,34 @@ foreign_expr_walker(Node *node, state = FDW_COLLATE_UNSAFE; } break; + case T_ArrayCoerceExpr: + { + ArrayCoerceExpr *e = (ArrayCoerceExpr *) node; + + /* + * Recurse to input subexpression. + */ + if (!foreign_expr_walker((Node *) e->arg, + glob_cxt, &inner_cxt, case_arg_cxt)) + return false; + + /* + * T_ArrayCoerceExpr must not introduce a collation not + * derived from an input foreign Var (same logic as for a + * function). + */ + collation = e->resultcollid; + if (collation == InvalidOid) + state = FDW_COLLATE_NONE; + else if (inner_cxt.state == FDW_COLLATE_SAFE && + collation == inner_cxt.collation) + state = FDW_COLLATE_SAFE; + else if (collation == DEFAULT_COLLATION_OID) + state = FDW_COLLATE_NONE; + else + state = FDW_COLLATE_UNSAFE; + } + break; case T_BoolExpr: { BoolExpr *b = (BoolExpr *) node; @@ -1154,7 +1189,7 @@ is_foreign_pathkey(PlannerInfo *root, static char * deparse_type_name(Oid type_oid, int32 typemod) { - bits16 flags = FORMAT_TYPE_TYPEMOD_GIVEN; + uint16 flags = FORMAT_TYPE_TYPEMOD_GIVEN; if (!is_builtin(type_oid)) flags |= FORMAT_TYPE_FORCE_QUALIFY; @@ -1423,10 +1458,8 @@ deparseTargetList(StringInfo buf, first = true; for (i = 1; i <= tupdesc->natts; i++) { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i - 1); - /* Ignore dropped attributes. */ - if (attr->attisdropped) + if (TupleDescCompactAttr(tupdesc, i - 1)->attisdropped) continue; if (have_wholerow || @@ -2115,7 +2148,7 @@ deparseInsertSql(StringInfo buf, RangeTblEntry *rte, foreach(lc, targetAttrs) { int attnum = lfirst_int(lc); - Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); + CompactAttribute *attr = TupleDescCompactAttr(tupdesc, attnum - 1); if (!first) appendStringInfoString(buf, ", "); @@ -2181,7 +2214,7 @@ rebuildInsertSql(StringInfo buf, Relation rel, foreach(lc, target_attrs) { int attnum = lfirst_int(lc); - Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); + CompactAttribute *attr = TupleDescCompactAttr(tupdesc, attnum - 1); if (!first) appendStringInfoString(buf, ", "); @@ -2231,7 +2264,7 @@ deparseUpdateSql(StringInfo buf, RangeTblEntry *rte, foreach(lc, targetAttrs) { int attnum = lfirst_int(lc); - Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); + CompactAttribute *attr = TupleDescCompactAttr(tupdesc, attnum - 1); if (!first) appendStringInfoString(buf, ", "); @@ -2508,8 +2541,8 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel) } /* - * Construct SELECT statement to acquire the number of rows and the relkind of - * a relation. + * Construct SELECT statement to acquire the number of pages, the number of + * rows, and the relkind of a relation. * * Note: we just return the remote server's reltuples value, which might * be off a good deal, but it doesn't seem worth working harder. See @@ -2524,7 +2557,7 @@ deparseAnalyzeInfoSql(StringInfo buf, Relation rel) initStringInfo(&relname); deparseRelation(&relname, rel); - appendStringInfoString(buf, "SELECT reltuples, relkind FROM pg_catalog.pg_class WHERE oid = "); + appendStringInfoString(buf, "SELECT relpages, reltuples, relkind FROM pg_catalog.pg_class WHERE oid = "); deparseStringLiteral(buf, relname.data); appendStringInfoString(buf, "::pg_catalog.regclass"); } @@ -2913,6 +2946,9 @@ deparseExpr(Expr *node, deparse_expr_cxt *context) case T_RelabelType: deparseRelabelType((RelabelType *) node, context); break; + case T_ArrayCoerceExpr: + deparseArrayCoerceExpr((ArrayCoerceExpr *) node, context); + break; case T_BoolExpr: deparseBoolExpr((BoolExpr *) node, context); break; @@ -3501,6 +3537,24 @@ deparseRelabelType(RelabelType *node, deparse_expr_cxt *context) node->resulttypmod)); } +/* + * Deparse an ArrayCoerceExpr (array-type conversion) node. + */ +static void +deparseArrayCoerceExpr(ArrayCoerceExpr *node, deparse_expr_cxt *context) +{ + deparseExpr(node->arg, context); + + /* + * No difference how to deparse explicit cast, but if we omit implicit + * cast in the query, it'll be more user-friendly + */ + if (node->coerceformat != COERCE_IMPLICIT_CAST) + appendStringInfo(context->buf, "::%s", + deparse_type_name(node->resulttype, + node->resulttypmod)); +} + /* * Deparse a BoolExpr node. */ diff --git a/contrib/postgres_fdw/expected/eval_plan_qual.out b/contrib/postgres_fdw/expected/eval_plan_qual.out new file mode 100644 index 0000000000000..5361fe6f32989 --- /dev/null +++ b/contrib/postgres_fdw/expected/eval_plan_qual.out @@ -0,0 +1,131 @@ +Parsed test spec with 2 sessions + +starting permutation: s0_update_l s1_tuplock_l_0 s0_commit s1_commit +step s0_update_l: UPDATE l SET i = i + 1; +step s1_tuplock_l_0: + EXPLAIN (VERBOSE, COSTS OFF) + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.i = 123 FOR UPDATE OF l; + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.i = 123 FOR UPDATE OF l; + +step s0_commit: COMMIT; +step s1_tuplock_l_0: <... completed> +QUERY PLAN +--------------------------------------------------------------------- +LockRows + Output: l.i, l.v, l.ctid, ft.* + -> Nested Loop + Output: l.i, l.v, l.ctid, ft.* + -> Seq Scan on public.l + Output: l.i, l.v, l.ctid + Filter: (l.i = 123) + -> Foreign Scan on public.ft + Output: ft.*, ft.i + Remote SQL: SELECT i, v FROM public.t WHERE ((i = 123)) +(10 rows) + +i|v +-+- +(0 rows) + +step s1_commit: COMMIT; + +starting permutation: s0_update_l s1_tuplock_l_1 s0_commit s1_commit +step s0_update_l: UPDATE l SET i = i + 1; +step s1_tuplock_l_1: + EXPLAIN (VERBOSE, COSTS OFF) + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.v = 'foo' FOR UPDATE OF l; + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.v = 'foo' FOR UPDATE OF l; + +step s0_commit: COMMIT; +step s1_tuplock_l_1: <... completed> +QUERY PLAN +----------------------------------------------------------------------------- +LockRows + Output: l.i, l.v, l.ctid, ft.* + -> Nested Loop + Output: l.i, l.v, l.ctid, ft.* + -> Seq Scan on public.l + Output: l.i, l.v, l.ctid + Filter: (l.v = 'foo'::text) + -> Foreign Scan on public.ft + Output: ft.*, ft.i + Remote SQL: SELECT i, v FROM public.t WHERE ((i = $1::integer)) +(10 rows) + +i|v +-+- +(0 rows) + +step s1_commit: COMMIT; + +starting permutation: s0_update_a s1_tuplock_a_0 s0_commit s1_commit +step s0_update_a: UPDATE a SET i = i + 1; +step s1_tuplock_a_0: + EXPLAIN (VERBOSE, COSTS OFF) + SELECT a.i FROM a, fb, fc WHERE a.i = fb.i AND fb.i = fc.i FOR UPDATE OF a; + SELECT a.i FROM a, fb, fc WHERE a.i = fb.i AND fb.i = fc.i FOR UPDATE OF a; + +step s0_commit: COMMIT; +step s1_tuplock_a_0: <... completed> +QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +LockRows + Output: a.i, a.ctid, fb.*, fc.* + -> Nested Loop + Output: a.i, a.ctid, fb.*, fc.* + Join Filter: (fb.i = a.i) + -> Foreign Scan + Output: fb.*, fb.i, fc.*, fc.i + Relations: (public.fb) INNER JOIN (public.fc) + Remote SQL: SELECT CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.i) END, r2.i, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.i) END, r3.i FROM (public.b r2 INNER JOIN public.c r3 ON (((r2.i = r3.i)))) + -> Nested Loop + Output: fb.*, fb.i, fc.*, fc.i + Join Filter: (fb.i = fc.i) + -> Foreign Scan on public.fb + Output: fb.*, fb.i + Remote SQL: SELECT i FROM public.b ORDER BY i ASC NULLS LAST + -> Foreign Scan on public.fc + Output: fc.*, fc.i + Remote SQL: SELECT i FROM public.c + -> Seq Scan on public.a + Output: a.i, a.ctid +(20 rows) + +i +- +(0 rows) + +step s1_commit: COMMIT; + +starting permutation: s0_update_a s1_tuplock_a_1 s0_commit s1_commit +step s0_update_a: UPDATE a SET i = i + 1; +step s1_tuplock_a_1: + EXPLAIN (VERBOSE, COSTS OFF) + SELECT a.i, + (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i) + FROM a FOR UPDATE; + SELECT a.i, + (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i) + FROM a FOR UPDATE; + +step s0_commit: COMMIT; +step s1_tuplock_a_1: <... completed> +QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------- +LockRows + Output: a.i, ((SubPlan expr_1)), a.ctid + -> Seq Scan on public.a + Output: a.i, (SubPlan expr_1), a.ctid + SubPlan expr_1 + -> Foreign Scan + Output: 1 + Relations: (public.fb) INNER JOIN (public.fc) + Remote SQL: SELECT NULL FROM (public.b r1 INNER JOIN public.c r2 ON (((r2.i = $1::integer)) AND ((r1.i = $1::integer)))) +(9 rows) + +i|?column? +-+-------- +2| +(1 row) + +step s1_commit: COMMIT; diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 2185b42bb4f79..10e87acabef1d 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -2,23 +2,16 @@ -- create FDW objects -- =================================================================== CREATE EXTENSION postgres_fdw; +SELECT current_database() AS current_database, + current_setting('port') AS current_port +\gset CREATE SERVER testserver1 FOREIGN DATA WRAPPER postgres_fdw; -DO $d$ - BEGIN - EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - EXECUTE $$CREATE SERVER loopback3 FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - END; -$d$; +CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); +CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); +CREATE SERVER loopback3 FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); CREATE USER MAPPING FOR public SERVER testserver1 OPTIONS (user 'value', password 'value'); CREATE USER MAPPING FOR CURRENT_USER SERVER loopback; @@ -57,11 +50,19 @@ CREATE TABLE "S 1"."T 4" ( c3 text, CONSTRAINT t4_pkey PRIMARY KEY (c1) ); +CREATE TABLE "S 1"."T 5" ( + c1 int4range NOT NULL, + c2 int NOT NULL, + c3 text, + c4 daterange NOT NULL, + CONSTRAINT t5_pkey PRIMARY KEY (c1, c4 WITHOUT OVERLAPS) +); -- Disable autovacuum for these tables to avoid unexpected effects of that ALTER TABLE "S 1"."T 1" SET (autovacuum_enabled = 'false'); ALTER TABLE "S 1"."T 2" SET (autovacuum_enabled = 'false'); ALTER TABLE "S 1"."T 3" SET (autovacuum_enabled = 'false'); ALTER TABLE "S 1"."T 4" SET (autovacuum_enabled = 'false'); +ALTER TABLE "S 1"."T 5" SET (autovacuum_enabled = 'false'); INSERT INTO "S 1"."T 1" SELECT id, id % 10, @@ -88,10 +89,17 @@ INSERT INTO "S 1"."T 4" 'AAA' || to_char(id, 'FM000') FROM generate_series(1, 100) id; DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0; -- delete for outer join tests +INSERT INTO "S 1"."T 5" + SELECT int4range(id, id + 1), + id + 1, + 'AAA' || to_char(id, 'FM000'), + '[2000-01-01,2020-01-01)' + FROM generate_series(1, 100) id; ANALYZE "S 1"."T 1"; ANALYZE "S 1"."T 2"; ANALYZE "S 1"."T 3"; ANALYZE "S 1"."T 4"; +ANALYZE "S 1"."T 5"; -- =================================================================== -- create foreign tables -- =================================================================== @@ -139,6 +147,12 @@ CREATE FOREIGN TABLE ft7 ( c2 int NOT NULL, c3 text ) SERVER loopback3 OPTIONS (schema_name 'S 1', table_name 'T 4'); +CREATE FOREIGN TABLE ft8 ( + c1 int4range NOT NULL, + c2 int NOT NULL, + c3 text, + c4 daterange NOT NULL +) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 5'); -- =================================================================== -- tests for validator -- =================================================================== @@ -221,7 +235,8 @@ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1'); public | ft5 | loopback | (schema_name 'S 1', table_name 'T 4') | public | ft6 | loopback2 | (schema_name 'S 1', table_name 'T 4') | public | ft7 | loopback3 | (schema_name 'S 1', table_name 'T 4') | -(6 rows) + public | ft8 | loopback | (schema_name 'S 1', table_name 'T 5') | +(7 rows) -- Test that alteration of server options causes reconnection -- Remote's errors might be non-English, so hide them to ensure stable results @@ -235,12 +250,7 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work ALTER SERVER loopback OPTIONS (SET dbname 'no such database'); SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should fail ERROR: could not connect to server "loopback" -DO $d$ - BEGIN - EXECUTE $$ALTER SERVER loopback - OPTIONS (SET dbname '$$||current_database()||$$')$$; - END; -$d$; +ALTER SERVER loopback OPTIONS (SET dbname :'current_database'); SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again c3 | c4 -------+------------------------------ @@ -267,6 +277,14 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); -- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 WITH (slot_name = NONE, connect = false); +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. +DROP SUBSCRIPTION regress_pgfdw_subscription; +-- =================================================================== -- test error case for create publication on foreign table -- =================================================================== CREATE PUBLICATION testpub_ftbl FOR TABLE ft1; -- should fail @@ -710,12 +728,12 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- Op Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = (- "C 1"))) (3 rows) -EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr - QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------- +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS DISTINCT FROM c3; -- DistinctExpr + QUERY PLAN +---------------------------------------------------------------------------------------------------------- Foreign Scan on public.ft1 t1 Output: c1, c2, c3, c4, c5, c6, c7, c8 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" IS NOT NULL) IS DISTINCT FROM ("C 1" IS NOT NULL))) + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c3 IS DISTINCT FROM c3)) (3 rows) EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr @@ -1180,6 +1198,27 @@ SELECT * FROM ft1 WHERE CASE c3 COLLATE "C" WHEN c6 THEN true ELSE c3 < 'bar' EN Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" (4 rows) +-- Test array type conversion pushdown +SET plan_cache_mode = force_generic_plan; +PREPARE s(varchar[]) AS SELECT count(*) FROM ft2 WHERE c6 = ANY ($1); +EXPLAIN (VERBOSE, COSTS OFF) +EXECUTE s(ARRAY['1','2']); + QUERY PLAN +--------------------------------------------------------------------------------------------- + Foreign Scan + Output: (count(*)) + Relations: Aggregate on (public.ft2) + Remote SQL: SELECT count(*) FROM "S 1"."T 1" WHERE ((c6 = ANY ($1::character varying[]))) +(4 rows) + +EXECUTE s(ARRAY['1','2']); + count +------- + 200 +(1 row) + +DEALLOCATE s; +RESET plan_cache_mode; -- a regconfig constant referring to this text search configuration -- is initially unshippable CREATE TEXT SEARCH CONFIGURATION public.custom_search @@ -2966,9 +3005,9 @@ select sum(t1.c1), count(t2.c1) from ft1 t1 inner join ft2 t2 on (t1.c1 = t2.c1) QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------- Aggregate - Output: sum(t1.c1), count(t2.c1) + Output: sum(t1.c1), count(*) -> Foreign Scan - Output: t1.c1, t2.c1 + Output: t1.c1 Filter: (((((t1.c1 * t2.c1) / (t1.c1 * t2.c1)))::double precision * random()) <= '1'::double precision) Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) @@ -3064,12 +3103,12 @@ select c2 * (random() <= 1)::int as c2 from ft2 group by c2 * (random() <= 1)::i -- GROUP BY clause in various forms, cardinal, alias and constant expression explain (verbose, costs off) select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2; - QUERY PLAN ------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------- Foreign Scan - Output: (count(c2)), c2, 5, 7.0, 9 + Output: (count(*)), c2, 5, 7.0, 9 Relations: Aggregate on (public.ft1) - Remote SQL: SELECT count(c2), c2, 5, 7.0, 9 FROM "S 1"."T 1" GROUP BY 2, 3, 5 ORDER BY c2 ASC NULLS LAST + Remote SQL: SELECT count(*), c2, 5, 7.0, 9 FROM "S 1"."T 1" GROUP BY 2, 3, 5 ORDER BY c2 ASC NULLS LAST (4 rows) select count(c2) w, c2 x, 5 y, 7.0 z from ft1 group by 2, y, 9.0::int order by 2; @@ -3166,13 +3205,13 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100 -- of an initplan) can be trouble, per bug #15781 explain (verbose, costs off) select exists(select 1 from pg_enum), sum(c1) from ft1; - QUERY PLAN --------------------------------------------------- + QUERY PLAN +--------------------------------------------------- Foreign Scan - Output: (InitPlan 1).col1, (sum(ft1.c1)) + Output: (InitPlan exists_1).col1, (sum(ft1.c1)) Relations: Aggregate on (public.ft1) Remote SQL: SELECT sum("C 1") FROM "S 1"."T 1" - InitPlan 1 + InitPlan exists_1 -> Seq Scan on pg_catalog.pg_enum (6 rows) @@ -3187,8 +3226,8 @@ select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1; QUERY PLAN --------------------------------------------------- GroupAggregate - Output: (InitPlan 1).col1, sum(ft1.c1) - InitPlan 1 + Output: (InitPlan exists_1).col1, sum(ft1.c1) + InitPlan exists_1 -> Seq Scan on pg_catalog.pg_enum -> Foreign Scan on public.ft1 Output: ft1.c1 @@ -3347,15 +3386,15 @@ select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------ Unique - Output: ((SubPlan 1)) + Output: ((SubPlan expr_1)) -> Sort - Output: ((SubPlan 1)) - Sort Key: ((SubPlan 1)) + Output: ((SubPlan expr_1)) + Sort Key: ((SubPlan expr_1)) -> Foreign Scan - Output: (SubPlan 1) + Output: (SubPlan expr_1) Relations: Aggregate on (public.ft2 t2) Remote SQL: SELECT count(*) FILTER (WHERE ((c2 = 6) AND ("C 1" < 10))) FROM "S 1"."T 1" WHERE (((c2 % 6) = 0)) - SubPlan 1 + SubPlan expr_1 -> Foreign Scan on public.ft1 t1 Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10)))) Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 6)) @@ -3370,21 +3409,21 @@ select distinct (select count(*) filter (where t2.c2 = 6 and t2.c1 < 10) from ft -- Inner query is aggregation query explain (verbose, costs off) select distinct (select count(t1.c1) filter (where t2.c2 = 6 and t2.c1 < 10) from ft1 t1 where t1.c1 = 6) from ft2 t2 where t2.c2 % 6 = 0 order by 1; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------- Unique - Output: ((SubPlan 1)) + Output: ((SubPlan expr_1)) -> Sort - Output: ((SubPlan 1)) - Sort Key: ((SubPlan 1)) + Output: ((SubPlan expr_1)) + Sort Key: ((SubPlan expr_1)) -> Foreign Scan on public.ft2 t2 - Output: (SubPlan 1) + Output: (SubPlan expr_1) Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE (((c2 % 6) = 0)) - SubPlan 1 + SubPlan expr_1 -> Foreign Scan - Output: (count(t1.c1) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10)))) + Output: (count(*) FILTER (WHERE ((t2.c2 = 6) AND (t2.c1 < 10)))) Relations: Aggregate on (public.ft1 t1) - Remote SQL: SELECT count("C 1") FILTER (WHERE (($1::integer = 6) AND ($2::integer < 10))) FROM "S 1"."T 1" WHERE (("C 1" = 6)) + Remote SQL: SELECT count(*) FILTER (WHERE (($1::integer = 6) AND ($2::integer < 10))) FROM "S 1"."T 1" WHERE (("C 1" = 6)) (13 rows) select distinct (select count(t1.c1) filter (where t2.c2 = 6 and t2.c1 < 10) from ft1 t1 where t1.c1 = 6) from ft2 t2 where t2.c2 % 6 = 0 order by 1; @@ -3412,14 +3451,14 @@ select sum(c1) filter (where (c1 / c1) * random() <= 1) from ft1 group by c2 ord explain (verbose, costs off) select sum(c2) filter (where c2 in (select c2 from ft1 where c2 < 5)) from ft1; - QUERY PLAN -------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------- Aggregate - Output: sum(ft1.c2) FILTER (WHERE (ANY (ft1.c2 = (hashed SubPlan 1).col1))) + Output: sum(ft1.c2) FILTER (WHERE (ANY (ft1.c2 = (hashed SubPlan any_1).col1))) -> Foreign Scan on public.ft1 Output: ft1.c2 Remote SQL: SELECT c2 FROM "S 1"."T 1" - SubPlan 1 + SubPlan any_1 -> Foreign Scan on public.ft1 ft1_1 Output: ft1_1.c2 Remote SQL: SELECT c2 FROM "S 1"."T 1" WHERE ((c2 < 5)) @@ -3692,30 +3731,33 @@ select count(t1.c3) from ft2 t1 left join ft2 t2 on (t1.c1 = random() * t2.c2); -- Subquery in FROM clause having aggregate explain (verbose, costs off) select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2; - QUERY PLAN ------------------------------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------- Sort - Output: (count(*)), x.b - Sort Key: (count(*)), x.b - -> HashAggregate - Output: count(*), x.b - Group Key: x.b - -> Hash Join - Output: x.b - Inner Unique: true - Hash Cond: (ft1.c2 = x.a) - -> Foreign Scan on public.ft1 - Output: ft1.c2 - Remote SQL: SELECT c2 FROM "S 1"."T 1" - -> Hash - Output: x.b, x.a - -> Subquery Scan on x - Output: x.b, x.a - -> Foreign Scan - Output: ft1_1.c2, (sum(ft1_1.c1)) - Relations: Aggregate on (public.ft1 ft1_1) - Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 -(21 rows) + Output: (count(*)), (sum(ft1_1.c1)) + Sort Key: (count(*)), (sum(ft1_1.c1)) + -> Finalize GroupAggregate + Output: count(*), (sum(ft1_1.c1)) + Group Key: (sum(ft1_1.c1)) + -> Sort + Output: (sum(ft1_1.c1)), (PARTIAL count(*)) + Sort Key: (sum(ft1_1.c1)) + -> Hash Join + Output: (sum(ft1_1.c1)), (PARTIAL count(*)) + Hash Cond: (ft1_1.c2 = ft1.c2) + -> Foreign Scan + Output: ft1_1.c2, (sum(ft1_1.c1)) + Relations: Aggregate on (public.ft1 ft1_1) + Remote SQL: SELECT c2, sum("C 1") FROM "S 1"."T 1" GROUP BY 1 + -> Hash + Output: ft1.c2, (PARTIAL count(*)) + -> Partial HashAggregate + Output: ft1.c2, PARTIAL count(*) + Group Key: ft1.c2 + -> Foreign Scan on public.ft1 + Output: ft1.c2 + Remote SQL: SELECT c2 FROM "S 1"."T 1" +(24 rows) select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2; count | b @@ -4166,9 +4208,12 @@ EXECUTE st1(101, 101); 00101 | 00101 (1 row) -SET enable_hashjoin TO off; +-- These next tests require choosing between remote and local sort, which is +-- a coin flip so long as cost_sort() gives the same results on both sides. +-- To stabilize the expected plans, disable sorting locally. SET enable_sort TO off; -- subquery using stable function (can't be sent to remote) +SET enable_hashjoin TO off; -- this one needs even more help to be stable PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c4) = '1970-01-17'::date) ORDER BY c1; EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st2(10, 20); QUERY PLAN @@ -4200,20 +4245,16 @@ EXECUTE st2(101, 121); (1 row) RESET enable_hashjoin; -RESET enable_sort; -- subquery using immutable function (can be sent to remote) PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1; EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20); - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - Sort + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8 - Sort Key: t1.c1 - -> Foreign Scan - Output: t1.c1, t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8 - Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2) - Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3))) -(7 rows) + Relations: (public.ft1 t1) SEMI JOIN (public.ft2 t2) + Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8 FROM "S 1"."T 1" r1 WHERE ((r1."C 1" < 20)) AND EXISTS (SELECT NULL FROM "S 1"."T 1" r3 WHERE ((r3."C 1" > 10)) AND ((date(r3.c5) = '1970-01-17'::date)) AND ((r3.c3 = r1.c3))) ORDER BY r1."C 1" ASC NULLS LAST +(4 rows) EXECUTE st3(10, 20); c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 @@ -4226,6 +4267,7 @@ EXECUTE st3(20, 30); ----+----+----+----+----+----+----+---- (0 rows) +RESET enable_sort; -- custom plan should be chosen initially PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1; EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st4(1); @@ -4600,11 +4642,13 @@ SELECT * FROM ft1 WHERE 'foo' = c8 LIMIT 1; -- with that remote type SELECT * FROM ft1 WHERE c8 LIKE 'foo' LIMIT 1; -- ERROR ERROR: operator does not exist: public.user_enum ~~ unknown -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. CONTEXT: remote SQL command: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c8 ~~ 'foo')) LIMIT 1::bigint SELECT * FROM ft1 WHERE c8::text LIKE 'foo' LIMIT 1; -- ERROR; cast not pushed down ERROR: operator does not exist: public.user_enum ~~ unknown -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. CONTEXT: remote SQL command: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c8 ~~ 'foo')) LIMIT 1::bigint ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 TYPE user_enum; -- =================================================================== @@ -5077,13 +5121,13 @@ SELECT ft1.c1 FROM ft1 JOIN ft2 on ft1.c1 = ft2.c1 WHERE -- =================================================================== EXPLAIN (verbose, costs off) INSERT INTO ft2 (c1,c2,c3) SELECT c1+1000,c2+100, c3 || c3 FROM ft2 LIMIT 20; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Insert on public.ft2 Remote SQL: INSERT INTO "S 1"."T 1"("C 1", c2, c3, c4, c5, c6, c7, c8) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) Batch Size: 1 - -> Subquery Scan on "*SELECT*" - Output: "*SELECT*"."?column?", "*SELECT*"."?column?_1", NULL::integer, "*SELECT*"."?column?_2", NULL::timestamp with time zone, NULL::timestamp without time zone, NULL::character varying(10), 'ft2 '::character(10), NULL::user_enum + -> Subquery Scan on unnamed_subquery + Output: unnamed_subquery."?column?", unnamed_subquery."?column?_1", NULL::integer, unnamed_subquery."?column?_2", NULL::timestamp with time zone, NULL::timestamp without time zone, NULL::character varying(10), 'ft2 '::character(10), NULL::user_enum -> Foreign Scan on public.ft2 ft2_1 Output: (ft2_1.c1 + 1000), (ft2_1.c2 + 100), (ft2_1.c3 || ft2_1.c3) Remote SQL: SELECT "C 1", c2, c3 FROM "S 1"."T 1" LIMIT 20::bigint @@ -6289,6 +6333,27 @@ DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; ft2 (1 row) +-- Test UPDATE FOR PORTION OF +UPDATE ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' +SET c2 = c2 + 1 +WHERE c1 = '[1,2)'; +ERROR: foreign tables don't support FOR PORTION OF +SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4; + c1 | c2 | c3 | c4 +-------+----+--------+------------------------- + [1,2) | 2 | AAA001 | [01-01-2000,01-01-2020) +(1 row) + +-- Test DELETE FOR PORTION OF +DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' +WHERE c1 = '[2,3)'; +ERROR: foreign tables don't support FOR PORTION OF +SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4; + c1 | c2 | c3 | c4 +-------+----+--------+------------------------- + [2,3) | 3 | AAA002 | [01-01-2000,01-01-2020) +(1 row) + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; @@ -6433,14 +6498,14 @@ UPDATE ft2 AS target SET (c2, c7) = ( FROM ft2 AS src WHERE target.c1 = src.c1 ) WHERE c1 > 1100; - QUERY PLAN ------------------------------------------------------------------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 target Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c7 = $3 WHERE ctid = $1 -> Foreign Scan on public.ft2 target - Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), target.ctid, target.* + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), target.ctid, target.* Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" WHERE (("C 1" > 1100)) FOR UPDATE - SubPlan 1 + SubPlan multiexpr_1 -> Foreign Scan on public.ft2 src Output: (src.c2 * 10), src.c7 Remote SQL: SELECT c2, c7 FROM "S 1"."T 1" WHERE (($1::integer = "C 1")) @@ -6489,20 +6554,31 @@ UPDATE ft2 d SET c2 = CASE WHEN random() >= 0 THEN d.c2 ELSE 0 END ALTER SERVER loopback OPTIONS (DROP extensions); INSERT INTO ft2 (c1,c2,c3) SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(2001, 2010) id; +-- this will do a remote seqscan, causing unstable result order, so sort EXPLAIN (verbose, costs off) -UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING *; -- can't be pushed down - QUERY PLAN ----------------------------------------------------------------------------------------------------------- - Update on public.ft2 - Output: c1, c2, c3, c4, c5, c6, c7, c8 - Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 - -> Foreign Scan on public.ft2 - Output: 'bar'::text, ctid, ft2.* - Filter: (postgres_fdw_abs(ft2.c1) > 2000) - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" FOR UPDATE -(7 rows) +WITH cte AS ( + UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING * +) SELECT * FROM cte ORDER BY c1; -- can't be pushed down + QUERY PLAN +------------------------------------------------------------------------------------------------------------------ + Sort + Output: cte.c1, cte.c2, cte.c3, cte.c4, cte.c5, cte.c6, cte.c7, cte.c8 + Sort Key: cte.c1 + CTE cte + -> Update on public.ft2 + Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8 + Remote SQL: UPDATE "S 1"."T 1" SET c3 = $2 WHERE ctid = $1 RETURNING "C 1", c2, c3, c4, c5, c6, c7, c8 + -> Foreign Scan on public.ft2 + Output: 'bar'::text, ft2.ctid, ft2.* + Filter: (postgres_fdw_abs(ft2.c1) > 2000) + Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8, ctid FROM "S 1"."T 1" FOR UPDATE + -> CTE Scan on cte + Output: cte.c1, cte.c2, cte.c3, cte.c4, cte.c5, cte.c6, cte.c7, cte.c8 +(13 rows) -UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING *; +WITH cte AS ( + UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING * +) SELECT * FROM cte ORDER BY c1; c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 ------+----+-----+----+----+----+------------+---- 2001 | 1 | bar | | | | ft2 | @@ -7148,8 +7224,9 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 < 0; Aggregate Output: count(*) -> Result + Replaces: Scan on ft1 One-Time Filter: false -(4 rows) +(5 rows) SELECT count(*) FROM ft1 WHERE c2 < 0; count @@ -7192,8 +7269,9 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT count(*) FROM ft1 WHERE c2 >= 0; Aggregate Output: count(*) -> Result + Replaces: Scan on ft1 One-Time Filter: false -(4 rows) +(5 rows) SELECT count(*) FROM ft1 WHERE c2 >= 0; count @@ -8021,8 +8099,9 @@ DELETE FROM rem1 WHERE false; -- currently can't be pushed down Remote SQL: DELETE FROM public.loc1 WHERE ctid = $1 -> Result Output: ctid + Replaces: Scan on rem1 One-Time Filter: false -(5 rows) +(6 rows) -- Test with statement-level triggers CREATE TRIGGER trig_stmt_before @@ -8212,6 +8291,119 @@ DELETE FROM rem1; -- can't be pushed down (5 rows) DROP TRIGGER trig_row_after_delete ON rem1; +-- We are allowed to create transition-table triggers on both kinds of +-- inheritance even if they contain foreign tables as children, but currently +-- collecting transition tuples from such foreign tables is not supported. +CREATE TABLE local_tbl (a text, b int); +CREATE FOREIGN TABLE foreign_tbl (a text, b int) + SERVER loopback OPTIONS (table_name 'local_tbl'); +INSERT INTO foreign_tbl VALUES ('AAA', 42); +-- Test case for partition hierarchy +CREATE TABLE parent_tbl (a text, b int) PARTITION BY LIST (a); +ALTER TABLE parent_tbl ATTACH PARTITION foreign_tbl FOR VALUES IN ('AAA'); +CREATE TRIGGER parent_tbl_insert_trig + AFTER INSERT ON parent_tbl REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER parent_tbl_update_trig + AFTER UPDATE ON parent_tbl REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER parent_tbl_delete_trig + AFTER DELETE ON parent_tbl REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +INSERT INTO parent_tbl VALUES ('AAA', 42); +ERROR: cannot collect transition tuples from child foreign tables +COPY parent_tbl (a, b) FROM stdin; +ERROR: cannot collect transition tuples from child foreign tables +CONTEXT: COPY parent_tbl, line 1: "AAA 42" +ALTER SERVER loopback OPTIONS (ADD batch_size '10'); +INSERT INTO parent_tbl VALUES ('AAA', 42); +ERROR: cannot collect transition tuples from child foreign tables +COPY parent_tbl (a, b) FROM stdin; +ERROR: cannot collect transition tuples from child foreign tables +CONTEXT: COPY parent_tbl, line 1: "AAA 42" +ALTER SERVER loopback OPTIONS (DROP batch_size); +EXPLAIN (VERBOSE, COSTS OFF) +UPDATE parent_tbl SET b = b + 1; + QUERY PLAN +------------------------------------------------------------------------------------------------ + Update on public.parent_tbl + Foreign Update on public.foreign_tbl parent_tbl_1 + Remote SQL: UPDATE public.local_tbl SET b = $2 WHERE ctid = $1 + -> Foreign Scan on public.foreign_tbl parent_tbl_1 + Output: (parent_tbl_1.b + 1), parent_tbl_1.tableoid, parent_tbl_1.ctid, parent_tbl_1.* + Remote SQL: SELECT a, b, ctid FROM public.local_tbl FOR UPDATE +(6 rows) + +UPDATE parent_tbl SET b = b + 1; +ERROR: cannot collect transition tuples from child foreign tables +EXPLAIN (VERBOSE, COSTS OFF) +DELETE FROM parent_tbl; + QUERY PLAN +------------------------------------------------------------------ + Delete on public.parent_tbl + Foreign Delete on public.foreign_tbl parent_tbl_1 + Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 + -> Foreign Scan on public.foreign_tbl parent_tbl_1 + Output: parent_tbl_1.tableoid, parent_tbl_1.ctid + Remote SQL: SELECT ctid FROM public.local_tbl FOR UPDATE +(6 rows) + +DELETE FROM parent_tbl; +ERROR: cannot collect transition tuples from child foreign tables +ALTER TABLE parent_tbl DETACH PARTITION foreign_tbl; +DROP TABLE parent_tbl; +-- Test case for non-partition hierarchy +CREATE TABLE parent_tbl (a text, b int); +ALTER FOREIGN TABLE foreign_tbl INHERIT parent_tbl; +CREATE TRIGGER parent_tbl_update_trig + AFTER UPDATE ON parent_tbl REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER parent_tbl_delete_trig + AFTER DELETE ON parent_tbl REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +EXPLAIN (VERBOSE, COSTS OFF) +UPDATE parent_tbl SET b = b + 1; + QUERY PLAN +------------------------------------------------------------------------------------------------------ + Update on public.parent_tbl + Update on public.parent_tbl parent_tbl_1 + Foreign Update on public.foreign_tbl parent_tbl_2 + Remote SQL: UPDATE public.local_tbl SET b = $2 WHERE ctid = $1 + -> Result + Output: (parent_tbl.b + 1), parent_tbl.tableoid, parent_tbl.ctid, (NULL::record) + -> Append + -> Seq Scan on public.parent_tbl parent_tbl_1 + Output: parent_tbl_1.b, parent_tbl_1.tableoid, parent_tbl_1.ctid, NULL::record + -> Foreign Scan on public.foreign_tbl parent_tbl_2 + Output: parent_tbl_2.b, parent_tbl_2.tableoid, parent_tbl_2.ctid, parent_tbl_2.* + Remote SQL: SELECT a, b, ctid FROM public.local_tbl FOR UPDATE +(12 rows) + +UPDATE parent_tbl SET b = b + 1; +ERROR: cannot collect transition tuples from child foreign tables +EXPLAIN (VERBOSE, COSTS OFF) +DELETE FROM parent_tbl; + QUERY PLAN +------------------------------------------------------------------------ + Delete on public.parent_tbl + Delete on public.parent_tbl parent_tbl_1 + Foreign Delete on public.foreign_tbl parent_tbl_2 + Remote SQL: DELETE FROM public.local_tbl WHERE ctid = $1 + -> Append + -> Seq Scan on public.parent_tbl parent_tbl_1 + Output: parent_tbl_1.tableoid, parent_tbl_1.ctid + -> Foreign Scan on public.foreign_tbl parent_tbl_2 + Output: parent_tbl_2.tableoid, parent_tbl_2.ctid + Remote SQL: SELECT ctid FROM public.local_tbl FOR UPDATE +(10 rows) + +DELETE FROM parent_tbl; +ERROR: cannot collect transition tuples from child foreign tables +ALTER FOREIGN TABLE foreign_tbl NO INHERIT parent_tbl; +DROP TABLE parent_tbl; +-- Cleanup +DROP FOREIGN TABLE foreign_tbl; +DROP TABLE local_tbl; -- =================================================================== -- test inheritance features -- =================================================================== @@ -10509,14 +10701,8 @@ SHOW is_superuser; (1 row) -- This will be OK, we can create the FDW -DO $d$ - BEGIN - EXECUTE $$CREATE SERVER loopback_nopw FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - END; -$d$; +CREATE SERVER loopback_nopw FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); -- But creation of user mappings for non-superusers should fail CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; @@ -11475,6 +11661,11 @@ SELECT * FROM result_tbl ORDER BY a; (3 rows) DELETE FROM result_tbl; +-- Test COPY TO when foreign table is partition +COPY async_pt TO stdout; --error +ERROR: cannot copy from foreign table "async_p1" +DETAIL: Partition "async_p1" is a foreign table in partitioned table "async_pt" +HINT: Try the COPY (SELECT ...) TO variant. DROP FOREIGN TABLE async_p3; DROP TABLE base_tbl3; -- Check case where the partitioned table has local/remote partitions @@ -12011,12 +12202,12 @@ INSERT INTO local_tbl VALUES (1505, 505, 'foo'); ANALYZE local_tbl; EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt WHERE a < 3000) FROM async_pt WHERE a < 3000) t2 ON t1.a = t2.a; - QUERY PLAN ----------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------ Nested Loop Left Join - Output: t1.a, t1.b, t1.c, async_pt.a, async_pt.b, async_pt.c, ((InitPlan 1).col1) + Output: t1.a, t1.b, t1.c, async_pt.a, async_pt.b, async_pt.c, ((InitPlan expr_1).col1) Join Filter: (t1.a = async_pt.a) - InitPlan 1 + InitPlan expr_1 -> Aggregate Output: count(*) -> Append @@ -12028,10 +12219,10 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W Output: t1.a, t1.b, t1.c -> Append -> Async Foreign Scan on public.async_p1 async_pt_1 - Output: async_pt_1.a, async_pt_1.b, async_pt_1.c, (InitPlan 1).col1 + Output: async_pt_1.a, async_pt_1.b, async_pt_1.c, (InitPlan expr_1).col1 Remote SQL: SELECT a, b, c FROM public.base_tbl1 WHERE ((a < 3000)) -> Async Foreign Scan on public.async_p2 async_pt_2 - Output: async_pt_2.a, async_pt_2.b, async_pt_2.c, (InitPlan 1).col1 + Output: async_pt_2.a, async_pt_2.b, async_pt_2.c, (InitPlan expr_1).col1 Remote SQL: SELECT a, b, c FROM public.base_tbl2 WHERE ((a < 3000)) (20 rows) @@ -12042,7 +12233,7 @@ SELECT * FROM local_tbl t1 LEFT JOIN (SELECT *, (SELECT count(*) FROM async_pt W Nested Loop Left Join (actual rows=1.00 loops=1) Join Filter: (t1.a = async_pt.a) Rows Removed by Join Filter: 399 - InitPlan 1 + InitPlan expr_1 -> Aggregate (actual rows=1.00 loops=1) -> Append (actual rows=400.00 loops=1) -> Async Foreign Scan on async_p1 async_pt_4 (actual rows=200.00 loops=1) @@ -12265,12 +12456,12 @@ CREATE FOREIGN TABLE foreign_tbl2 () INHERITS (foreign_tbl) SERVER loopback OPTIONS (table_name 'base_tbl'); EXPLAIN (VERBOSE, COSTS OFF) SELECT a FROM base_tbl WHERE (a, random() > 0) IN (SELECT a, random() > 0 FROM foreign_tbl); - QUERY PLAN ---------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------- Seq Scan on public.base_tbl Output: base_tbl.a - Filter: (ANY ((base_tbl.a = (SubPlan 1).col1) AND ((random() > '0'::double precision) = (SubPlan 1).col2))) - SubPlan 1 + Filter: (ANY ((base_tbl.a = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan any_1).col2))) + SubPlan any_1 -> Result Output: base_tbl.a, (random() > '0'::double precision) -> Append @@ -12384,6 +12575,142 @@ SELECT count(*) FROM remote_application_name DROP FOREIGN TABLE remote_application_name; DROP VIEW my_application_name; -- =================================================================== +-- test read-only and/or deferrable transactions +-- =================================================================== +CREATE TABLE loct (f1 int, f2 text); +CREATE FUNCTION locf() RETURNS SETOF loct LANGUAGE SQL AS + 'UPDATE public.loct SET f2 = f2 || f2 RETURNING *'; +CREATE VIEW locv AS SELECT t.* FROM locf() t; +CREATE FOREIGN TABLE remt (f1 int, f2 text) + SERVER loopback OPTIONS (table_name 'locv'); +CREATE FOREIGN TABLE remt2 (f1 int, f2 text) + SERVER loopback2 OPTIONS (table_name 'locv'); +INSERT INTO loct VALUES (1, 'foo'), (2, 'bar'); +START TRANSACTION READ ONLY; +SAVEPOINT s; +SELECT * FROM remt; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SELECT * FROM remt; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK; +START TRANSACTION; +SAVEPOINT s; +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK; +START TRANSACTION; +SAVEPOINT s; +SELECT * FROM remt; -- should work + f1 | f2 +----+-------- + 1 | foofoo + 2 | barbar +(2 rows) + +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SELECT * FROM remt; -- should work + f1 | f2 +----+-------- + 1 | foofoo + 2 | barbar +(2 rows) + +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK; +-- Exercise abort code paths in pgfdw_xact_callback/pgfdw_subxact_callback +-- in situations where multiple connections are involved +START TRANSACTION; +SAVEPOINT s; +SELECT * FROM remt; -- should work + f1 | f2 +----+-------- + 1 | foofoo + 2 | barbar +(2 rows) + +SET transaction_read_only = on; +SELECT * FROM remt2; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SELECT * FROM remt; -- should work + f1 | f2 +----+-------- + 1 | foofoo + 2 | barbar +(2 rows) + +SET transaction_read_only = on; +SELECT * FROM remt2; -- should fail +ERROR: cannot execute UPDATE in a read-only transaction +CONTEXT: SQL function "locf" statement 1 +remote SQL command: SELECT f1, f2 FROM public.locv +ROLLBACK; +DROP FOREIGN TABLE remt; +CREATE FOREIGN TABLE remt (f1 int, f2 text) + SERVER loopback OPTIONS (table_name 'loct'); +START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY; +SELECT * FROM remt; + f1 | f2 +----+----- + 1 | foo + 2 | bar +(2 rows) + +COMMIT; +START TRANSACTION ISOLATION LEVEL SERIALIZABLE DEFERRABLE; +SELECT * FROM remt; + f1 | f2 +----+----- + 1 | foo + 2 | bar +(2 rows) + +COMMIT; +START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE; +SELECT * FROM remt; + f1 | f2 +----+----- + 1 | foo + 2 | bar +(2 rows) + +COMMIT; +-- Clean up +DROP FOREIGN TABLE remt; +DROP FOREIGN TABLE remt2; +DROP VIEW locv; +DROP FUNCTION locf(); +DROP TABLE loct; +-- =================================================================== -- test parallel commit and parallel abort -- =================================================================== ALTER SERVER loopback OPTIONS (ADD parallel_commit 'true'); @@ -12515,7 +12842,7 @@ ALTER SERVER loopback2 OPTIONS (DROP parallel_abort); -- =================================================================== CREATE TABLE analyze_table (id int, a text, b bigint); CREATE FOREIGN TABLE analyze_ftable (id int, a text, b bigint) - SERVER loopback OPTIONS (table_name 'analyze_rtable1'); + SERVER loopback OPTIONS (table_name 'analyze_table'); INSERT INTO analyze_table (SELECT x FROM generate_series(1,1000) x); ANALYZE analyze_table; SET default_statistics_target = 10; @@ -12523,19 +12850,79 @@ ANALYZE analyze_table; ALTER SERVER loopback OPTIONS (analyze_sampling 'invalid'); ERROR: invalid value for string option "analyze_sampling": invalid ALTER SERVER loopback OPTIONS (analyze_sampling 'auto'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'system'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'bernoulli'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'random'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'off'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; -- cleanup DROP FOREIGN TABLE analyze_ftable; DROP TABLE analyze_table; -- =================================================================== +-- test for statistics import +-- =================================================================== +CREATE TABLE simport_table (c1 int, c2 text); +CREATE FOREIGN TABLE simport_ftable (c1 int, c2 text, cx int) + SERVER loopback OPTIONS (table_name 'simport_table'); +ALTER FOREIGN TABLE simport_ftable ALTER COLUMN cx OPTIONS (ADD column_name 'c1'); +ALTER FOREIGN TABLE simport_ftable OPTIONS (ADD restore_stats 'true'); +ANALYZE simport_ftable; -- should fail +WARNING: could not import statistics for foreign table "public.simport_ftable" --- remote table "public.simport_table" has no relation statistics to import +ANALYZE simport_table; +ANALYZE VERBOSE simport_ftable; -- should work +INFO: importing statistics for foreign table "public.simport_ftable" +INFO: finished importing statistics for foreign table "public.simport_ftable" +ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS 0; +ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS 0; +INSERT INTO simport_table VALUES (1, 'foo'), (1, 'foo'), (2, 'bar'), (2, 'bar'); +ANALYZE simport_table; +ANALYZE simport_ftable; -- should fail +WARNING: could not import statistics for foreign table "public.simport_ftable" --- remote table "public.simport_table" has no attribute statistics to import +ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS DEFAULT; +ANALYZE simport_table; +ANALYZE simport_ftable; -- should fail +WARNING: could not import statistics for foreign table "public.simport_ftable" --- no attribute statistics found for column "c2" of remote table "public.simport_table" +ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS DEFAULT; +ANALYZE simport_table; +ANALYZE VERBOSE simport_ftable; -- should work +INFO: importing statistics for foreign table "public.simport_ftable" +INFO: finished importing statistics for foreign table "public.simport_ftable" +ANALYZE VERBOSE simport_ftable (c1); -- should work +INFO: importing statistics for foreign table "public.simport_ftable" +INFO: finished importing statistics for foreign table "public.simport_ftable" +ANALYZE VERBOSE simport_ftable (c2); -- should work +INFO: importing statistics for foreign table "public.simport_ftable" +INFO: finished importing statistics for foreign table "public.simport_ftable" +ANALYZE VERBOSE simport_ftable (c1, cx); -- should work +INFO: importing statistics for foreign table "public.simport_ftable" +INFO: finished importing statistics for foreign table "public.simport_ftable" +ANALYZE VERBOSE simport_ftable (c2, cx); -- should work +INFO: importing statistics for foreign table "public.simport_ftable" +INFO: finished importing statistics for foreign table "public.simport_ftable" +CREATE STATISTICS stats (dependencies) ON c1, c2 FROM simport_ftable; +ANALYZE simport_ftable; -- should fail +WARNING: cannot import statistics for foreign table "public.simport_ftable" --- this foreign table has extended statistics objects +DROP STATISTICS stats; +ANALYZE simport_ftable (cid); -- should fail +ERROR: column "cid" of relation "simport_ftable" does not exist +ANALYZE simport_ftable (c1, c1); -- should fail +ERROR: column "c1" of relation "simport_ftable" appears more than once +CREATE VIEW simport_view AS SELECT * FROM simport_table; +CREATE FOREIGN TABLE simport_fview (c1 int, c2 text) + SERVER loopback OPTIONS (table_name 'simport_view'); +ALTER FOREIGN TABLE simport_fview OPTIONS (ADD restore_stats 'true'); +ANALYZE simport_fview; -- should fail +WARNING: could not import statistics for foreign table "public.simport_fview" --- remote table "public.simport_view" is of relkind "v" which cannot have statistics +-- cleanup +DROP FOREIGN TABLE simport_ftable; +DROP FOREIGN TABLE simport_fview; +DROP VIEW simport_view; +DROP TABLE simport_table; +-- =================================================================== -- test for postgres_fdw_get_connections function with check_conn = true -- =================================================================== -- Disable debug_discard_caches in order to manage remote connections diff --git a/contrib/postgres_fdw/meson.build b/contrib/postgres_fdw/meson.build index 8b29be24deeb7..3e2ed06b7665c 100644 --- a/contrib/postgres_fdw/meson.build +++ b/contrib/postgres_fdw/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group postgres_fdw_sources = files( 'connection.c', @@ -27,6 +27,7 @@ install_data( 'postgres_fdw--1.0.sql', 'postgres_fdw--1.0--1.1.sql', 'postgres_fdw--1.1--1.2.sql', + 'postgres_fdw--1.2--1.3.sql', kwargs: contrib_data_args, ) @@ -39,11 +40,18 @@ tests += { 'postgres_fdw', 'query_cancel', ], - 'regress_args': ['--dlpath', meson.build_root() / 'src/test/regress'], + 'regress_args': ['--dlpath', meson.project_build_root() / 'src/test/regress'], + }, + 'isolation': { + 'specs': [ + 'eval_plan_qual', + ], + 'regress_args': ['--load-extension=postgres_fdw'], }, 'tap': { 'tests': [ 't/001_auth_scram.pl', + 't/010_subscription.pl', ], }, } diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c index c2f936640bca8..3944aedbaccbe 100644 --- a/contrib/postgres_fdw/option.c +++ b/contrib/postgres_fdw/option.c @@ -3,7 +3,7 @@ * option.c * FDW and GUC option handling for postgres_fdw * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/postgres_fdw/option.c @@ -21,6 +21,7 @@ #include "libpq/libpq-be.h" #include "postgres_fdw.h" #include "utils/guc.h" +#include "utils/memutils.h" #include "utils/varlena.h" /* @@ -39,12 +40,6 @@ typedef struct PgFdwOption */ static PgFdwOption *postgres_fdw_options; -/* - * Valid options for libpq. - * Allocated and filled in InitPgFdwOptions. - */ -static PQconninfoOption *libpq_options; - /* * GUC parameters */ @@ -125,7 +120,8 @@ postgres_fdw_validator(PG_FUNCTION_ARGS) strcmp(def->defname, "async_capable") == 0 || strcmp(def->defname, "parallel_commit") == 0 || strcmp(def->defname, "parallel_abort") == 0 || - strcmp(def->defname, "keep_connections") == 0) + strcmp(def->defname, "keep_connections") == 0 || + strcmp(def->defname, "restore_stats") == 0) { /* these accept only boolean values */ (void) defGetBoolean(def); @@ -239,6 +235,7 @@ static void InitPgFdwOptions(void) { int num_libpq_opts; + PQconninfoOption *libpq_options; PQconninfoOption *lopt; PgFdwOption *popt; @@ -278,6 +275,9 @@ InitPgFdwOptions(void) /* sampling is available on both server and table */ {"analyze_sampling", ForeignServerRelationId, false}, {"analyze_sampling", ForeignTableRelationId, false}, + /* restore_stats is available on both server and table */ + {"restore_stats", ForeignServerRelationId, false}, + {"restore_stats", ForeignTableRelationId, false}, {"use_scram_passthrough", ForeignServerRelationId, false}, {"use_scram_passthrough", UserMappingRelationId, false}, @@ -307,8 +307,8 @@ InitPgFdwOptions(void) * Get list of valid libpq options. * * To avoid unnecessary work, we get the list once and use it throughout - * the lifetime of this backend process. We don't need to care about - * memory context issues, because PQconndefaults allocates with malloc. + * the lifetime of this backend process. Hence, we'll allocate it in + * TopMemoryContext. */ libpq_options = PQconndefaults(); if (!libpq_options) /* assume reason for failure is OOM */ @@ -325,19 +325,11 @@ InitPgFdwOptions(void) /* * Construct an array which consists of all valid options for * postgres_fdw, by appending FDW-specific options to libpq options. - * - * We use plain malloc here to allocate postgres_fdw_options because it - * lives as long as the backend process does. Besides, keeping - * libpq_options in memory allows us to avoid copying every keyword - * string. */ postgres_fdw_options = (PgFdwOption *) - malloc(sizeof(PgFdwOption) * num_libpq_opts + - sizeof(non_libpq_options)); - if (postgres_fdw_options == NULL) - ereport(ERROR, - (errcode(ERRCODE_FDW_OUT_OF_MEMORY), - errmsg("out of memory"))); + MemoryContextAlloc(TopMemoryContext, + sizeof(PgFdwOption) * num_libpq_opts + + sizeof(non_libpq_options)); popt = postgres_fdw_options; for (lopt = libpq_options; lopt->keyword; lopt++) @@ -355,8 +347,8 @@ InitPgFdwOptions(void) if (strncmp(lopt->keyword, "oauth_", strlen("oauth_")) == 0) continue; - /* We don't have to copy keyword string, as described above. */ - popt->keyword = lopt->keyword; + popt->keyword = MemoryContextStrdup(TopMemoryContext, + lopt->keyword); /* * "user" and any secret options are allowed only on user mappings. @@ -371,6 +363,9 @@ InitPgFdwOptions(void) popt++; } + /* Done with libpq's output structure. */ + PQconninfoFree(libpq_options); + /* Append FDW-specific options and dummy terminator. */ memcpy(popt, non_libpq_options, sizeof(non_libpq_options)); } @@ -531,7 +526,7 @@ process_pgfdw_appname(const char *appname) appendStringInfoString(&buf, application_name); break; case 'c': - appendStringInfo(&buf, INT64_HEX_FORMAT ".%x", MyStartTime, MyProcPid); + appendStringInfo(&buf, "%" PRIx64 ".%x", MyStartTime, MyProcPid); break; case 'C': appendStringInfoString(&buf, cluster_name); diff --git a/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql new file mode 100644 index 0000000000000..5bcf0ba2e0972 --- /dev/null +++ b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql @@ -0,0 +1,12 @@ +/* contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION postgres_fdw UPDATE TO '1.3'" to load this file. \quit + +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION postgres_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +ALTER FOREIGN DATA WRAPPER postgres_fdw CONNECTION postgres_fdw_connection; diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 331f3fc088d1b..c42cb690c7bc9 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -3,7 +3,7 @@ * postgres_fdw.c * Foreign-data wrapper for remote PostgreSQL servers * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/postgres_fdw/postgres_fdw.c @@ -21,7 +21,10 @@ #include "commands/defrem.h" #include "commands/explain_format.h" #include "commands/explain_state.h" +#include "commands/vacuum.h" #include "executor/execAsync.h" +#include "executor/instrument.h" +#include "executor/spi.h" #include "foreign/fdwapi.h" #include "funcapi.h" #include "miscadmin.h" @@ -39,6 +42,7 @@ #include "optimizer/tlist.h" #include "parser/parsetree.h" #include "postgres_fdw.h" +#include "statistics/statistics.h" #include "storage/latch.h" #include "utils/builtins.h" #include "utils/float.h" @@ -317,6 +321,182 @@ typedef struct List *already_used; /* expressions already dealt with */ } ec_member_foreign_arg; +/* Pairs of remote columns with local columns */ +typedef struct +{ + AttrNumber local_attnum; + char local_attname[NAMEDATALEN]; + char remote_attname[NAMEDATALEN]; + int res_index; +} RemoteAttributeMapping; + +/* Result sets that are returned from a foreign statistics scan */ +typedef struct +{ + PGresult *rel; + PGresult *att; + int server_version_num; +} RemoteStatsResults; + +/* Column order in relation stats query */ +enum RelStatsColumns +{ + RELSTATS_RELPAGES = 0, + RELSTATS_RELTUPLES, + RELSTATS_RELKIND, + RELSTATS_NUM_FIELDS, +}; + +/* Column order in attribute stats query */ +enum AttStatsColumns +{ + ATTSTATS_ATTNAME = 0, + ATTSTATS_NULL_FRAC, + ATTSTATS_AVG_WIDTH, + ATTSTATS_N_DISTINCT, + ATTSTATS_MOST_COMMON_VALS, + ATTSTATS_MOST_COMMON_FREQS, + ATTSTATS_HISTOGRAM_BOUNDS, + ATTSTATS_CORRELATION, + ATTSTATS_MOST_COMMON_ELEMS, + ATTSTATS_MOST_COMMON_ELEM_FREQS, + ATTSTATS_ELEM_COUNT_HISTOGRAM, + ATTSTATS_RANGE_LENGTH_HISTOGRAM, + ATTSTATS_RANGE_EMPTY_FRAC, + ATTSTATS_RANGE_BOUNDS_HISTOGRAM, + ATTSTATS_NUM_FIELDS, +}; + +/* Relation stats import query */ +static const char *relimport_sql = +"SELECT pg_catalog.pg_restore_relation_stats(\n" +"\t'version', $1,\n" +"\t'schemaname', $2,\n" +"\t'relname', $3,\n" +"\t'relpages', $4::integer,\n" +"\t'reltuples', $5::real)"; + +/* Argument order in relation stats import query */ +enum RelImportSqlArgs +{ + RELIMPORT_SQL_VERSION = 0, + RELIMPORT_SQL_SCHEMANAME, + RELIMPORT_SQL_RELNAME, + RELIMPORT_SQL_RELPAGES, + RELIMPORT_SQL_RELTUPLES, + RELIMPORT_SQL_NUM_FIELDS +}; + +/* Argument types in relation stats import query */ +static const Oid relimport_argtypes[RELIMPORT_SQL_NUM_FIELDS] = +{ + INT4OID, TEXTOID, TEXTOID, TEXTOID, + TEXTOID, +}; + +/* Attribute stats import query */ +static const char *attimport_sql = +"SELECT pg_catalog.pg_restore_attribute_stats(\n" +"\t'version', $1,\n" +"\t'schemaname', $2,\n" +"\t'relname', $3,\n" +"\t'attnum', $4,\n" +"\t'inherited', false::boolean,\n" +"\t'null_frac', $5::real,\n" +"\t'avg_width', $6::integer,\n" +"\t'n_distinct', $7::real,\n" +"\t'most_common_vals', $8,\n" +"\t'most_common_freqs', $9::real[],\n" +"\t'histogram_bounds', $10,\n" +"\t'correlation', $11::real,\n" +"\t'most_common_elems', $12,\n" +"\t'most_common_elem_freqs', $13::real[],\n" +"\t'elem_count_histogram', $14::real[],\n" +"\t'range_length_histogram', $15,\n" +"\t'range_empty_frac', $16::real,\n" +"\t'range_bounds_histogram', $17)"; + +/* Argument order in attribute stats import query */ +enum AttImportSqlArgs +{ + ATTIMPORT_SQL_VERSION = 0, + ATTIMPORT_SQL_SCHEMANAME, + ATTIMPORT_SQL_RELNAME, + ATTIMPORT_SQL_ATTNUM, + ATTIMPORT_SQL_NULL_FRAC, + ATTIMPORT_SQL_AVG_WIDTH, + ATTIMPORT_SQL_N_DISTINCT, + ATTIMPORT_SQL_MOST_COMMON_VALS, + ATTIMPORT_SQL_MOST_COMMON_FREQS, + ATTIMPORT_SQL_HISTOGRAM_BOUNDS, + ATTIMPORT_SQL_CORRELATION, + ATTIMPORT_SQL_MOST_COMMON_ELEMS, + ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS, + ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM, + ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM, + ATTIMPORT_SQL_RANGE_EMPTY_FRAC, + ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM, + ATTIMPORT_SQL_NUM_FIELDS +}; + +/* Argument types in attribute stats import query */ +static const Oid attimport_argtypes[ATTIMPORT_SQL_NUM_FIELDS] = +{ + INT4OID, TEXTOID, TEXTOID, INT2OID, + TEXTOID, TEXTOID, TEXTOID, TEXTOID, + TEXTOID, TEXTOID, TEXTOID, TEXTOID, + TEXTOID, TEXTOID, TEXTOID, TEXTOID, + TEXTOID, +}; + +/* + * The mapping of attribute stats query columns to the positional arguments in + * the prepared pg_restore_attribute_stats() statement. + */ +typedef struct +{ + enum AttStatsColumns res_field; + enum AttImportSqlArgs arg_num; +} AttrResultArgMap; + +#define NUM_MAPPED_ATTIMPORT_ARGS 13 + +static const AttrResultArgMap attr_result_arg_map[NUM_MAPPED_ATTIMPORT_ARGS] = +{ + {ATTSTATS_NULL_FRAC, ATTIMPORT_SQL_NULL_FRAC}, + {ATTSTATS_AVG_WIDTH, ATTIMPORT_SQL_AVG_WIDTH}, + {ATTSTATS_N_DISTINCT, ATTIMPORT_SQL_N_DISTINCT}, + {ATTSTATS_MOST_COMMON_VALS, ATTIMPORT_SQL_MOST_COMMON_VALS}, + {ATTSTATS_MOST_COMMON_FREQS, ATTIMPORT_SQL_MOST_COMMON_FREQS}, + {ATTSTATS_HISTOGRAM_BOUNDS, ATTIMPORT_SQL_HISTOGRAM_BOUNDS}, + {ATTSTATS_CORRELATION, ATTIMPORT_SQL_CORRELATION}, + {ATTSTATS_MOST_COMMON_ELEMS, ATTIMPORT_SQL_MOST_COMMON_ELEMS}, + {ATTSTATS_MOST_COMMON_ELEM_FREQS, ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS}, + {ATTSTATS_ELEM_COUNT_HISTOGRAM, ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM}, + {ATTSTATS_RANGE_LENGTH_HISTOGRAM, ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM}, + {ATTSTATS_RANGE_EMPTY_FRAC, ATTIMPORT_SQL_RANGE_EMPTY_FRAC}, + {ATTSTATS_RANGE_BOUNDS_HISTOGRAM, ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM}, +}; + +/* Attribute stats clear query */ +static const char *attclear_sql = +"SELECT pg_catalog.pg_clear_attribute_stats($1, $2, $3, false)"; + +/* Argument order in attribute stats clear query */ +enum AttClearSqlArgs +{ + ATTCLEAR_SQL_SCHEMANAME = 0, + ATTCLEAR_SQL_RELNAME, + ATTCLEAR_SQL_ATTNAME, + ATTCLEAR_SQL_NUM_FIELDS +}; + +/* Argument types in attribute stats clear query */ +static const Oid attclear_argtypes[ATTCLEAR_SQL_NUM_FIELDS] = +{ + TEXTOID, TEXTOID, TEXTOID, +}; + /* * SQL functions */ @@ -402,6 +582,9 @@ static void postgresExecForeignTruncate(List *rels, static bool postgresAnalyzeForeignTable(Relation relation, AcquireSampleRowsFunc *func, BlockNumber *totalpages); +static bool postgresImportForeignStatistics(Relation relation, + List *va_cols, + int elevel); static List *postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid); static void postgresGetForeignJoinPaths(PlannerInfo *root, @@ -507,6 +690,37 @@ static int postgresAcquireSampleRowsFunc(Relation relation, int elevel, double *totaldeadrows); static void analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate); +static bool fetch_remote_statistics(Relation relation, + List *va_cols, + ForeignTable *table, + const char *local_schemaname, + const char *local_relname, + int *p_attrcnt, + RemoteAttributeMapping **p_remattrmap, + RemoteStatsResults *remstats); +static PGresult *fetch_relstats(PGconn *conn, Relation relation); +static PGresult *fetch_attstats(PGconn *conn, int server_version_num, + const char *remote_schemaname, const char *remote_relname, + const char *column_list); +static RemoteAttributeMapping *build_remattrmap(Relation relation, List *va_cols, + int *p_attrcnt, StringInfo column_list); +static bool attname_in_list(const char *attname, List *va_cols); +static int remattrmap_cmp(const void *v1, const void *v2); +static bool match_attrmap(PGresult *res, + const char *local_schemaname, + const char *local_relname, + const char *remote_schemaname, + const char *remote_relname, + int attrcnt, + RemoteAttributeMapping *remattrmap); +static bool import_fetched_statistics(const char *schemaname, + const char *relname, + int attrcnt, + const RemoteAttributeMapping *remattrmap, + RemoteStatsResults *remstats); +static void map_field_to_arg(PGresult *res, int row, int field, + int arg, Datum *values, char *nulls); +static bool import_spi_query_ok(void); static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch); static void fetch_more_data_begin(AsyncRequest *areq); static void complete_pending_request(AsyncRequest *areq); @@ -595,6 +809,7 @@ postgres_fdw_handler(PG_FUNCTION_ARGS) /* Support functions for ANALYZE */ routine->AnalyzeForeignTable = postgresAnalyzeForeignTable; + routine->ImportForeignStatistics = postgresImportForeignStatistics; /* Support functions for IMPORT FOREIGN SCHEMA */ routine->ImportForeignSchema = postgresImportForeignSchema; @@ -633,7 +848,7 @@ postgresGetForeignRelSize(PlannerInfo *root, * We use PgFdwRelationInfo to pass various information to subsequent * functions. */ - fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo)); + fpinfo = palloc0_object(PgFdwRelationInfo); baserel->fdw_private = fpinfo; /* Base foreign tables need to be pushed down always. */ @@ -1517,7 +1732,7 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) /* * We'll save private state in node->fdw_state. */ - fsstate = (PgFdwScanState *) palloc0(sizeof(PgFdwScanState)); + fsstate = palloc0_object(PgFdwScanState); node->fdw_state = fsstate; /* @@ -1702,13 +1917,9 @@ postgresReScanForeignScan(ForeignScanState *node) return; } - /* - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. - */ res = pgfdw_exec_query(fsstate->conn, sql, fsstate->conn_state); if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, fsstate->conn, true, sql); + pgfdw_report_error(res, fsstate->conn, sql); PQclear(res); /* Now force a fresh FETCH. */ @@ -1860,7 +2071,7 @@ postgresPlanForeignModify(PlannerInfo *root, returningList = (List *) list_nth(plan->returningLists, subplan_index); /* - * ON CONFLICT DO UPDATE and DO NOTHING case with inference specification + * ON CONFLICT DO NOTHING/SELECT/UPDATE with inference specification * should have already been rejected in the optimizer, as presently there * is no way to recognize an arbiter index on a foreign table. Only DO * NOTHING is supported without an inference specification. @@ -2667,7 +2878,7 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags) /* * We'll save private state in node->fdw_state. */ - dmstate = (PgFdwDirectModifyState *) palloc0(sizeof(PgFdwDirectModifyState)); + dmstate = palloc0_object(PgFdwDirectModifyState); node->fdw_state = dmstate; /* @@ -2782,7 +2993,7 @@ postgresIterateDirectModify(ForeignScanState *node) if (!resultRelInfo->ri_projectReturning) { TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; - Instrumentation *instr = node->ss.ps.instrument; + NodeInstrumentation *instr = node->ss.ps.instrument; Assert(!dmstate->has_returning); @@ -2845,7 +3056,7 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) */ if (list_length(fdw_private) > FdwScanPrivateRelations) { - StringInfo relations; + StringInfoData relations; char *rawrelations; char *ptr; int minrti, @@ -2879,7 +3090,7 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) rtoffset = bms_next_member(plan->fs_base_relids, -1) - minrti; /* Now we can translate the string */ - relations = makeStringInfo(); + initStringInfo(&relations); ptr = rawrelations; while (*ptr) { @@ -2901,24 +3112,24 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) char *namespace; namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid)); - appendStringInfo(relations, "%s.%s", + appendStringInfo(&relations, "%s.%s", quote_identifier(namespace), quote_identifier(relname)); } else - appendStringInfoString(relations, + appendStringInfoString(&relations, quote_identifier(relname)); refname = (char *) list_nth(es->rtable_names, rti - 1); if (refname == NULL) refname = rte->eref->aliasname; if (strcmp(refname, relname) != 0) - appendStringInfo(relations, " %s", + appendStringInfo(&relations, " %s", quote_identifier(refname)); } else - appendStringInfoChar(relations, *ptr++); + appendStringInfoChar(&relations, *ptr++); } - ExplainPropertyText("Relations", relations->data, es); + ExplainPropertyText("Relations", relations.data, es); } /* @@ -3489,6 +3700,13 @@ estimate_path_cost_size(PlannerInfo *root, { Assert(foreignrel->reloptkind == RELOPT_UPPER_REL && fpinfo->stage == UPPERREL_GROUP_AGG); + + /* + * We can only get here when this function is called from + * add_foreign_ordered_paths() or add_foreign_final_paths(); + * in which cases, the passed-in fpextra should not be NULL. + */ + Assert(fpextra); adjust_foreign_grouping_path_cost(root, pathkeys, retrieved_rows, width, fpextra->limit_tuples, @@ -3601,41 +3819,32 @@ get_remote_estimate(const char *sql, PGconn *conn, double *rows, int *width, Cost *startup_cost, Cost *total_cost) { - PGresult *volatile res = NULL; - - /* PGresult must be released before leaving this function. */ - PG_TRY(); - { - char *line; - char *p; - int n; + PGresult *res; + char *line; + char *p; + int n; - /* - * Execute EXPLAIN remotely. - */ - res = pgfdw_exec_query(conn, sql, NULL); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, sql); + /* + * Execute EXPLAIN remotely. + */ + res = pgfdw_exec_query(conn, sql, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, sql); - /* - * Extract cost numbers for topmost plan node. Note we search for a - * left paren from the end of the line to avoid being confused by - * other uses of parentheses. - */ - line = PQgetvalue(res, 0, 0); - p = strrchr(line, '('); - if (p == NULL) - elog(ERROR, "could not interpret EXPLAIN output: \"%s\"", line); - n = sscanf(p, "(cost=%lf..%lf rows=%lf width=%d)", - startup_cost, total_cost, rows, width); - if (n != 4) - elog(ERROR, "could not interpret EXPLAIN output: \"%s\"", line); - } - PG_FINALLY(); - { - PQclear(res); - } - PG_END_TRY(); + /* + * Extract cost numbers for topmost plan node. Note we search for a left + * paren from the end of the line to avoid being confused by other uses of + * parentheses. + */ + line = PQgetvalue(res, 0, 0); + p = strrchr(line, '('); + if (p == NULL) + elog(ERROR, "could not interpret EXPLAIN output: \"%s\"", line); + n = sscanf(p, "(cost=%lf..%lf rows=%lf width=%d)", + startup_cost, total_cost, rows, width); + if (n != 4) + elog(ERROR, "could not interpret EXPLAIN output: \"%s\"", line); + PQclear(res); } /* @@ -3775,17 +3984,14 @@ create_cursor(ForeignScanState *node) */ if (!PQsendQueryParams(conn, buf.data, numParams, NULL, values, NULL, NULL, 0)) - pgfdw_report_error(ERROR, NULL, conn, false, buf.data); + pgfdw_report_error(NULL, conn, buf.data); /* * Get the result, and check for success. - * - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. */ res = pgfdw_get_result(conn); if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, conn, true, fsstate->query); + pgfdw_report_error(res, conn, fsstate->query); PQclear(res); /* Mark the cursor as created, and show no tuples have been retrieved */ @@ -3807,7 +4013,10 @@ static void fetch_more_data(ForeignScanState *node) { PgFdwScanState *fsstate = (PgFdwScanState *) node->fdw_state; - PGresult *volatile res = NULL; + PGconn *conn = fsstate->conn; + PGresult *res; + int numrows; + int i; MemoryContext oldcontext; /* @@ -3818,74 +4027,63 @@ fetch_more_data(ForeignScanState *node) MemoryContextReset(fsstate->batch_cxt); oldcontext = MemoryContextSwitchTo(fsstate->batch_cxt); - /* PGresult must be released before leaving this function. */ - PG_TRY(); + if (fsstate->async_capable) { - PGconn *conn = fsstate->conn; - int numrows; - int i; + Assert(fsstate->conn_state->pendingAreq); - if (fsstate->async_capable) - { - Assert(fsstate->conn_state->pendingAreq); + /* + * The query was already sent by an earlier call to + * fetch_more_data_begin. So now we just fetch the result. + */ + res = pgfdw_get_result(conn); + /* On error, report the original query, not the FETCH. */ + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, fsstate->query); - /* - * The query was already sent by an earlier call to - * fetch_more_data_begin. So now we just fetch the result. - */ - res = pgfdw_get_result(conn); - /* On error, report the original query, not the FETCH. */ - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, fsstate->query); + /* Reset per-connection state */ + fsstate->conn_state->pendingAreq = NULL; + } + else + { + char sql[64]; - /* Reset per-connection state */ - fsstate->conn_state->pendingAreq = NULL; - } - else - { - char sql[64]; + /* This is a regular synchronous fetch. */ + snprintf(sql, sizeof(sql), "FETCH %d FROM c%u", + fsstate->fetch_size, fsstate->cursor_number); - /* This is a regular synchronous fetch. */ - snprintf(sql, sizeof(sql), "FETCH %d FROM c%u", - fsstate->fetch_size, fsstate->cursor_number); + res = pgfdw_exec_query(conn, sql, fsstate->conn_state); + /* On error, report the original query, not the FETCH. */ + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, fsstate->query); + } - res = pgfdw_exec_query(conn, sql, fsstate->conn_state); - /* On error, report the original query, not the FETCH. */ - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, fsstate->query); - } + /* Convert the data into HeapTuples */ + numrows = PQntuples(res); + fsstate->tuples = (HeapTuple *) palloc0(numrows * sizeof(HeapTuple)); + fsstate->num_tuples = numrows; + fsstate->next_tuple = 0; - /* Convert the data into HeapTuples */ - numrows = PQntuples(res); - fsstate->tuples = (HeapTuple *) palloc0(numrows * sizeof(HeapTuple)); - fsstate->num_tuples = numrows; - fsstate->next_tuple = 0; + for (i = 0; i < numrows; i++) + { + Assert(IsA(node->ss.ps.plan, ForeignScan)); - for (i = 0; i < numrows; i++) - { - Assert(IsA(node->ss.ps.plan, ForeignScan)); - - fsstate->tuples[i] = - make_tuple_from_result_row(res, i, - fsstate->rel, - fsstate->attinmeta, - fsstate->retrieved_attrs, - node, - fsstate->temp_cxt); - } + fsstate->tuples[i] = + make_tuple_from_result_row(res, i, + fsstate->rel, + fsstate->attinmeta, + fsstate->retrieved_attrs, + node, + fsstate->temp_cxt); + } - /* Update fetch_ct_2 */ - if (fsstate->fetch_ct_2 < 2) - fsstate->fetch_ct_2++; + /* Update fetch_ct_2 */ + if (fsstate->fetch_ct_2 < 2) + fsstate->fetch_ct_2++; - /* Must be EOF if we didn't get as many tuples as we asked for. */ - fsstate->eof_reached = (numrows < fsstate->fetch_size); - } - PG_FINALLY(); - { - PQclear(res); - } - PG_END_TRY(); + /* Must be EOF if we didn't get as many tuples as we asked for. */ + fsstate->eof_reached = (numrows < fsstate->fetch_size); + + PQclear(res); MemoryContextSwitchTo(oldcontext); } @@ -3959,14 +4157,9 @@ close_cursor(PGconn *conn, unsigned int cursor_number, PGresult *res; snprintf(sql, sizeof(sql), "CLOSE c%u", cursor_number); - - /* - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. - */ res = pgfdw_exec_query(conn, sql, conn_state); if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, conn, true, sql); + pgfdw_report_error(res, conn, sql); PQclear(res); } @@ -3999,7 +4192,7 @@ create_foreign_modify(EState *estate, ListCell *lc; /* Begin constructing PgFdwModifyState. */ - fmstate = (PgFdwModifyState *) palloc0(sizeof(PgFdwModifyState)); + fmstate = palloc0_object(PgFdwModifyState); fmstate->rel = rel; /* Identify which user to do the remote access as. */ @@ -4036,7 +4229,7 @@ create_foreign_modify(EState *estate, /* Prepare for output conversion of parameters used in prepared stmt. */ n_params = list_length(fmstate->target_attrs) + 1; - fmstate->p_flinfo = (FmgrInfo *) palloc0(sizeof(FmgrInfo) * n_params); + fmstate->p_flinfo = palloc0_array(FmgrInfo, n_params); fmstate->p_nums = 0; if (operation == CMD_UPDATE || operation == CMD_DELETE) @@ -4174,18 +4367,15 @@ execute_foreign_modify(EState *estate, NULL, NULL, 0)) - pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query); + pgfdw_report_error(NULL, fmstate->conn, fmstate->query); /* * Get the result, and check for success. - * - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. */ res = pgfdw_get_result(fmstate->conn); if (PQresultStatus(res) != (fmstate->has_returning ? PGRES_TUPLES_OK : PGRES_COMMAND_OK)) - pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query); + pgfdw_report_error(res, fmstate->conn, fmstate->query); /* Check number of rows affected, and fetch RETURNING tuple if any */ if (fmstate->has_returning) @@ -4244,17 +4434,14 @@ prepare_foreign_modify(PgFdwModifyState *fmstate) fmstate->query, 0, NULL)) - pgfdw_report_error(ERROR, NULL, fmstate->conn, false, fmstate->query); + pgfdw_report_error(NULL, fmstate->conn, fmstate->query); /* * Get the result, and check for success. - * - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. */ res = pgfdw_get_result(fmstate->conn); if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query); + pgfdw_report_error(res, fmstate->conn, fmstate->query); PQclear(res); /* This action shows that the prepare has been done. */ @@ -4345,37 +4532,25 @@ convert_prep_stmt_params(PgFdwModifyState *fmstate, /* * store_returning_result * Store the result of a RETURNING clause - * - * On error, be sure to release the PGresult on the way out. Callers do not - * have PG_TRY blocks to ensure this happens. */ static void store_returning_result(PgFdwModifyState *fmstate, TupleTableSlot *slot, PGresult *res) { - PG_TRY(); - { - HeapTuple newtup; + HeapTuple newtup; - newtup = make_tuple_from_result_row(res, 0, - fmstate->rel, - fmstate->attinmeta, - fmstate->retrieved_attrs, - NULL, - fmstate->temp_cxt); + newtup = make_tuple_from_result_row(res, 0, + fmstate->rel, + fmstate->attinmeta, + fmstate->retrieved_attrs, + NULL, + fmstate->temp_cxt); - /* - * The returning slot will not necessarily be suitable to store - * heaptuples directly, so allow for conversion. - */ - ExecForceStoreHeapTuple(newtup, slot, true); - } - PG_CATCH(); - { - PQclear(res); - PG_RE_THROW(); - } - PG_END_TRY(); + /* + * The returning slot will not necessarily be suitable to store heaptuples + * directly, so allow for conversion. + */ + ExecForceStoreHeapTuple(newtup, slot, true); } /* @@ -4411,14 +4586,9 @@ deallocate_query(PgFdwModifyState *fmstate) return; snprintf(sql, sizeof(sql), "DEALLOCATE %s", fmstate->p_name); - - /* - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. - */ res = pgfdw_exec_query(fmstate->conn, sql, fmstate->conn_state); if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, fmstate->conn, true, sql); + pgfdw_report_error(res, fmstate->conn, sql); PQclear(res); pfree(fmstate->p_name); fmstate->p_name = NULL; @@ -4586,20 +4756,24 @@ execute_dml_stmt(ForeignScanState *node) */ if (!PQsendQueryParams(dmstate->conn, dmstate->query, numParams, NULL, values, NULL, NULL, 0)) - pgfdw_report_error(ERROR, NULL, dmstate->conn, false, dmstate->query); + pgfdw_report_error(NULL, dmstate->conn, dmstate->query); /* * Get the result, and check for success. - * - * We don't use a PG_TRY block here, so be careful not to throw error - * without releasing the PGresult. */ dmstate->result = pgfdw_get_result(dmstate->conn); if (PQresultStatus(dmstate->result) != (dmstate->has_returning ? PGRES_TUPLES_OK : PGRES_COMMAND_OK)) - pgfdw_report_error(ERROR, dmstate->result, dmstate->conn, true, + pgfdw_report_error(dmstate->result, dmstate->conn, dmstate->query); + /* + * The result potentially needs to survive across multiple executor row + * cycles, so move it to the context where the dmstate is. + */ + dmstate->result = libpqsrv_PGresultSetParent(dmstate->result, + GetMemoryChunkContext(dmstate)); + /* Get the number of rows affected. */ if (dmstate->has_returning) dmstate->num_tuples = PQntuples(dmstate->result); @@ -4641,30 +4815,16 @@ get_returning_data(ForeignScanState *node) } else { - /* - * On error, be sure to release the PGresult on the way out. Callers - * do not have PG_TRY blocks to ensure this happens. - */ - PG_TRY(); - { - HeapTuple newtup; - - newtup = make_tuple_from_result_row(dmstate->result, - dmstate->next_tuple, - dmstate->rel, - dmstate->attinmeta, - dmstate->retrieved_attrs, - node, - dmstate->temp_cxt); - ExecStoreHeapTuple(newtup, slot, false); - } - PG_CATCH(); - { - PQclear(dmstate->result); - PG_RE_THROW(); - } - PG_END_TRY(); + HeapTuple newtup; + newtup = make_tuple_from_result_row(dmstate->result, + dmstate->next_tuple, + dmstate->rel, + dmstate->attinmeta, + dmstate->retrieved_attrs, + node, + dmstate->temp_cxt); + ExecStoreHeapTuple(newtup, slot, false); /* Get the updated/deleted tuple. */ if (dmstate->rel) resultSlot = slot; @@ -4869,7 +5029,7 @@ prepare_query_params(PlanState *node, Assert(numParams > 0); /* Prepare for output conversion of parameters used in remote query. */ - *param_flinfo = (FmgrInfo *) palloc0(sizeof(FmgrInfo) * numParams); + *param_flinfo = palloc0_array(FmgrInfo, numParams); i = 0; foreach(lc, fdw_exprs) @@ -4950,7 +5110,7 @@ postgresAnalyzeForeignTable(Relation relation, UserMapping *user; PGconn *conn; StringInfoData sql; - PGresult *volatile res = NULL; + PGresult *res; /* Return the row-analysis function pointer */ *func = postgresAcquireSampleRowsFunc; @@ -4976,22 +5136,14 @@ postgresAnalyzeForeignTable(Relation relation, initStringInfo(&sql); deparseAnalyzeSizeSql(&sql, relation); - /* In what follows, do not risk leaking any PGresults. */ - PG_TRY(); - { - res = pgfdw_exec_query(conn, sql.data, NULL); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, sql.data); + res = pgfdw_exec_query(conn, sql.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, sql.data); - if (PQntuples(res) != 1 || PQnfields(res) != 1) - elog(ERROR, "unexpected result from deparseAnalyzeSizeSql query"); - *totalpages = strtoul(PQgetvalue(res, 0, 0), NULL, 10); - } - PG_FINALLY(); - { - PQclear(res); - } - PG_END_TRY(); + if (PQntuples(res) != 1 || PQnfields(res) != 1) + elog(ERROR, "unexpected result from deparseAnalyzeSizeSql query"); + *totalpages = strtoul(PQgetvalue(res, 0, 0), NULL, 10); + PQclear(res); ReleaseConnection(conn); @@ -5012,9 +5164,9 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample) UserMapping *user; PGconn *conn; StringInfoData sql; - PGresult *volatile res = NULL; - volatile double reltuples = -1; - volatile char relkind = 0; + PGresult *res; + double reltuples; + char relkind; /* assume the remote relation does not support TABLESAMPLE */ *can_tablesample = false; @@ -5033,24 +5185,16 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample) initStringInfo(&sql); deparseAnalyzeInfoSql(&sql, relation); - /* In what follows, do not risk leaking any PGresults. */ - PG_TRY(); - { - res = pgfdw_exec_query(conn, sql.data, NULL); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, sql.data); + res = pgfdw_exec_query(conn, sql.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, sql.data); - if (PQntuples(res) != 1 || PQnfields(res) != 2) - elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query"); - reltuples = strtod(PQgetvalue(res, 0, 0), NULL); - relkind = *(PQgetvalue(res, 0, 1)); - } - PG_FINALLY(); - { - if (res) - PQclear(res); - } - PG_END_TRY(); + if (PQntuples(res) != 1 || PQnfields(res) != RELSTATS_NUM_FIELDS) + elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query"); + /* We don't use relpages here */ + reltuples = strtod(PQgetvalue(res, 0, RELSTATS_RELTUPLES), NULL); + relkind = *(PQgetvalue(res, 0, RELSTATS_RELKIND)); + PQclear(res); ReleaseConnection(conn); @@ -5090,10 +5234,12 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel, int server_version_num; PgFdwSamplingMethod method = ANALYZE_SAMPLE_AUTO; /* auto is default */ double sample_frac = -1.0; - double reltuples; + double reltuples = -1.0; unsigned int cursor_number; StringInfoData sql; - PGresult *volatile res = NULL; + PGresult *res; + char fetch_sql[64]; + int fetch_size; ListCell *lc; /* Initialize workspace state */ @@ -5270,91 +5416,76 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel, deparseAnalyzeSql(&sql, relation, method, sample_frac, &astate.retrieved_attrs); - /* In what follows, do not risk leaking any PGresults. */ - PG_TRY(); - { - char fetch_sql[64]; - int fetch_size; - - res = pgfdw_exec_query(conn, sql.data, NULL); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pgfdw_report_error(ERROR, res, conn, false, sql.data); - PQclear(res); - res = NULL; + res = pgfdw_exec_query(conn, sql.data, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pgfdw_report_error(res, conn, sql.data); + PQclear(res); - /* - * Determine the fetch size. The default is arbitrary, but shouldn't - * be enormous. - */ - fetch_size = 100; - foreach(lc, server->options) - { - DefElem *def = (DefElem *) lfirst(lc); + /* + * Determine the fetch size. The default is arbitrary, but shouldn't be + * enormous. + */ + fetch_size = 100; + foreach(lc, server->options) + { + DefElem *def = (DefElem *) lfirst(lc); - if (strcmp(def->defname, "fetch_size") == 0) - { - (void) parse_int(defGetString(def), &fetch_size, 0, NULL); - break; - } - } - foreach(lc, table->options) + if (strcmp(def->defname, "fetch_size") == 0) { - DefElem *def = (DefElem *) lfirst(lc); - - if (strcmp(def->defname, "fetch_size") == 0) - { - (void) parse_int(defGetString(def), &fetch_size, 0, NULL); - break; - } + (void) parse_int(defGetString(def), &fetch_size, 0, NULL); + break; } + } + foreach(lc, table->options) + { + DefElem *def = (DefElem *) lfirst(lc); - /* Construct command to fetch rows from remote. */ - snprintf(fetch_sql, sizeof(fetch_sql), "FETCH %d FROM c%u", - fetch_size, cursor_number); - - /* Retrieve and process rows a batch at a time. */ - for (;;) + if (strcmp(def->defname, "fetch_size") == 0) { - int numrows; - int i; + (void) parse_int(defGetString(def), &fetch_size, 0, NULL); + break; + } + } - /* Allow users to cancel long query */ - CHECK_FOR_INTERRUPTS(); + /* Construct command to fetch rows from remote. */ + snprintf(fetch_sql, sizeof(fetch_sql), "FETCH %d FROM c%u", + fetch_size, cursor_number); - /* - * XXX possible future improvement: if rowstoskip is large, we - * could issue a MOVE rather than physically fetching the rows, - * then just adjust rowstoskip and samplerows appropriately. - */ + /* Retrieve and process rows a batch at a time. */ + for (;;) + { + int numrows; + int i; - /* Fetch some rows */ - res = pgfdw_exec_query(conn, fetch_sql, NULL); - /* On error, report the original query, not the FETCH. */ - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, sql.data); + /* Allow users to cancel long query */ + CHECK_FOR_INTERRUPTS(); - /* Process whatever we got. */ - numrows = PQntuples(res); - for (i = 0; i < numrows; i++) - analyze_row_processor(res, i, &astate); + /* + * XXX possible future improvement: if rowstoskip is large, we could + * issue a MOVE rather than physically fetching the rows, then just + * adjust rowstoskip and samplerows appropriately. + */ - PQclear(res); - res = NULL; + /* Fetch some rows */ + res = pgfdw_exec_query(conn, fetch_sql, NULL); + /* On error, report the original query, not the FETCH. */ + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, sql.data); - /* Must be EOF if we didn't get all the rows requested. */ - if (numrows < fetch_size) - break; - } + /* Process whatever we got. */ + numrows = PQntuples(res); + for (i = 0; i < numrows; i++) + analyze_row_processor(res, i, &astate); - /* Close the cursor, just to be tidy. */ - close_cursor(conn, cursor_number, NULL); - } - PG_CATCH(); - { PQclear(res); - PG_RE_THROW(); + + /* Must be EOF if we didn't get all the rows requested. */ + if (numrows < fetch_size) + break; } - PG_END_TRY(); + + /* Close the cursor, just to be tidy. */ + close_cursor(conn, cursor_number, NULL); ReleaseConnection(conn); @@ -5452,296 +5583,997 @@ analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate) } /* - * Import a foreign schema + * postgresImportForeignStatistics + * Attempt to fetch/restore remote statistics instead of sampling. */ -static List * -postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) +static bool +postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel) { - List *commands = NIL; - bool import_collate = true; - bool import_default = false; - bool import_generated = true; - bool import_not_null = true; + const char *schemaname = NULL; + const char *relname = NULL; + ForeignTable *table; ForeignServer *server; - UserMapping *mapping; - PGconn *conn; - StringInfoData buf; - PGresult *volatile res = NULL; - int numrows, - i; + RemoteStatsResults remstats = {.rel = NULL,.att = NULL}; + RemoteAttributeMapping *remattrmap = NULL; + int attrcnt = 0; + bool restore_stats = false; + bool ok = false; ListCell *lc; - /* Parse statement options */ - foreach(lc, stmt->options) + schemaname = get_namespace_name(RelationGetNamespace(relation)); + relname = RelationGetRelationName(relation); + table = GetForeignTable(RelationGetRelid(relation)); + server = GetForeignServer(table->serverid); + + /* + * Check whether the restore_stats option is enabled on the foreign table. + * If not, silently ignore the foreign table. + * + * Server-level options can be overridden by table-level options, so check + * server-level first. + */ + foreach(lc, server->options) { DefElem *def = (DefElem *) lfirst(lc); - if (strcmp(def->defname, "import_collate") == 0) - import_collate = defGetBoolean(def); - else if (strcmp(def->defname, "import_default") == 0) - import_default = defGetBoolean(def); - else if (strcmp(def->defname, "import_generated") == 0) - import_generated = defGetBoolean(def); - else if (strcmp(def->defname, "import_not_null") == 0) - import_not_null = defGetBoolean(def); - else - ereport(ERROR, - (errcode(ERRCODE_FDW_INVALID_OPTION_NAME), - errmsg("invalid option \"%s\"", def->defname))); + if (strcmp(def->defname, "restore_stats") == 0) + { + restore_stats = defGetBoolean(def); + break; + } } + foreach(lc, table->options) + { + DefElem *def = (DefElem *) lfirst(lc); + + if (strcmp(def->defname, "restore_stats") == 0) + { + restore_stats = defGetBoolean(def); + break; + } + } + if (!restore_stats) + return false; /* - * Get connection to the foreign server. Connection manager will - * establish new connection if necessary. + * We don't currently support statistics import for foreign tables with + * extended statistics objects. */ - server = GetForeignServer(serverOid); - mapping = GetUserMapping(GetUserId(), server->serverid); - conn = GetConnection(mapping, false, NULL); - - /* Don't attempt to import collation if remote server hasn't got it */ - if (PQserverVersion(conn) < 90100) - import_collate = false; + if (HasRelationExtStatistics(relation)) + { + ereport(WARNING, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot import statistics for foreign table \"%s.%s\" --- this foreign table has extended statistics objects", + schemaname, relname)); + return false; + } - /* Create workspace for strings */ - initStringInfo(&buf); + /* + * OK, let's do it. + */ + ereport(elevel, + (errmsg("importing statistics for foreign table \"%s.%s\"", + schemaname, relname))); + + ok = fetch_remote_statistics(relation, va_cols, + table, schemaname, relname, + &attrcnt, &remattrmap, &remstats); - /* In what follows, do not risk leaking any PGresults. */ - PG_TRY(); + if (ok) + ok = import_fetched_statistics(schemaname, relname, + attrcnt, remattrmap, &remstats); + + if (ok) + ereport(elevel, + (errmsg("finished importing statistics for foreign table \"%s.%s\"", + schemaname, relname))); + + PQclear(remstats.rel); + PQclear(remstats.att); + if (remattrmap) + pfree(remattrmap); + + return ok; +} + +/* + * Attempt to fetch statistics from a remote server. + */ +static bool +fetch_remote_statistics(Relation relation, + List *va_cols, + ForeignTable *table, + const char *local_schemaname, + const char *local_relname, + int *p_attrcnt, + RemoteAttributeMapping **p_remattrmap, + RemoteStatsResults *remstats) +{ + const char *remote_schemaname = NULL; + const char *remote_relname = NULL; + UserMapping *user; + PGconn *conn; + PGresult *relstats = NULL; + PGresult *attstats = NULL; + int server_version_num; + RemoteAttributeMapping *remattrmap = NULL; + int attrcnt = 0; + char relkind; + double reltuples; + bool ok = false; + ListCell *lc; + + /* + * Assume the remote schema/relation names are the same as the local name + * unless the foreign table's options tell us otherwise. + */ + remote_schemaname = local_schemaname; + remote_relname = local_relname; + foreach(lc, table->options) { - /* Check that the schema really exists */ - appendStringInfoString(&buf, "SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = "); - deparseStringLiteral(&buf, stmt->remote_schema); + DefElem *def = (DefElem *) lfirst(lc); - res = pgfdw_exec_query(conn, buf.data, NULL); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, buf.data); + if (strcmp(def->defname, "schema_name") == 0) + remote_schemaname = defGetString(def); + else if (strcmp(def->defname, "table_name") == 0) + remote_relname = defGetString(def); + } - if (PQntuples(res) != 1) - ereport(ERROR, - (errcode(ERRCODE_FDW_SCHEMA_NOT_FOUND), - errmsg("schema \"%s\" is not present on foreign server \"%s\"", - stmt->remote_schema, server->servername))); + /* + * Get connection to the foreign server. Connection manager will + * establish new connection if necessary. + */ + user = GetUserMapping(GetUserId(), table->serverid); + conn = GetConnection(user, false, NULL); + remstats->server_version_num = server_version_num = PQserverVersion(conn); - PQclear(res); - res = NULL; - resetStringInfo(&buf); + /* Fetch relation stats. */ + remstats->rel = relstats = fetch_relstats(conn, relation); - /* - * Fetch all table data from this schema, possibly restricted by - * EXCEPT or LIMIT TO. (We don't actually need to pay any attention - * to EXCEPT/LIMIT TO here, because the core code will filter the - * statements we return according to those lists anyway. But it - * should save a few cycles to not process excluded tables in the - * first place.) - * - * Import table data for partitions only when they are explicitly - * specified in LIMIT TO clause. Otherwise ignore them and only - * include the definitions of the root partitioned tables to allow - * access to the complete remote data set locally in the schema - * imported. - * - * Note: because we run the connection with search_path restricted to - * pg_catalog, the format_type() and pg_get_expr() outputs will always - * include a schema name for types/functions in other schemas, which - * is what we want. - */ - appendStringInfoString(&buf, - "SELECT relname, " - " attname, " - " format_type(atttypid, atttypmod), " - " attnotnull, " - " pg_get_expr(adbin, adrelid), "); - - /* Generated columns are supported since Postgres 12 */ - if (PQserverVersion(conn) >= 120000) - appendStringInfoString(&buf, - " attgenerated, "); - else - appendStringInfoString(&buf, - " NULL, "); + /* + * Verify that the remote table is the sort that can have meaningful stats + * in pg_stats. + * + * Note that while relations of kinds RELKIND_INDEX and + * RELKIND_PARTITIONED_INDEX can have rows in pg_stats, they obviously + * can't support a foreign table. + */ + relkind = *PQgetvalue(relstats, 0, RELSTATS_RELKIND); + switch (relkind) + { + case RELKIND_RELATION: + case RELKIND_FOREIGN_TABLE: + case RELKIND_MATVIEW: + case RELKIND_PARTITIONED_TABLE: + break; + default: + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- remote table \"%s.%s\" is of relkind \"%c\" which cannot have statistics", + local_schemaname, local_relname, + remote_schemaname, remote_relname, relkind)); + goto fetch_cleanup; + } - if (import_collate) - appendStringInfoString(&buf, - " collname, " - " collnsp.nspname "); - else - appendStringInfoString(&buf, - " NULL, NULL "); + /* + * If the reltuples value > 0, then then we can expect to find attribute + * stats for the remote table. + * + * In v14 or latter, if a reltuples value is -1, it means the table has + * never been analyzed, so we wouldn't expect to find the stats for the + * table; fallback to sampling in that case. If the value is 0, it means + * it was empty; in which case skip the stats and import relation stats + * only. + * + * In versions prior to v14, a value of 0 was ambiguous; it could mean + * that the table had never been analyzed, or that it was empty. Either + * way, we wouldn't expect to find the stats for the table, so we fallback + * to sampling. + */ + reltuples = strtod(PQgetvalue(relstats, 0, RELSTATS_RELTUPLES), NULL); + if (((server_version_num < 140000) && (reltuples == 0)) || + ((server_version_num >= 140000) && (reltuples == -1))) + { + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- remote table \"%s.%s\" has no relation statistics to import", + local_schemaname, local_relname, + remote_schemaname, remote_relname)); + goto fetch_cleanup; + } - appendStringInfoString(&buf, - "FROM pg_class c " - " JOIN pg_namespace n ON " - " relnamespace = n.oid " - " LEFT JOIN pg_attribute a ON " - " attrelid = c.oid AND attnum > 0 " - " AND NOT attisdropped " - " LEFT JOIN pg_attrdef ad ON " - " adrelid = c.oid AND adnum = attnum "); - - if (import_collate) - appendStringInfoString(&buf, - " LEFT JOIN pg_collation coll ON " - " coll.oid = attcollation " - " LEFT JOIN pg_namespace collnsp ON " - " collnsp.oid = collnamespace "); - appendStringInfoString(&buf, - "WHERE c.relkind IN (" - CppAsString2(RELKIND_RELATION) "," - CppAsString2(RELKIND_VIEW) "," - CppAsString2(RELKIND_FOREIGN_TABLE) "," - CppAsString2(RELKIND_MATVIEW) "," - CppAsString2(RELKIND_PARTITIONED_TABLE) ") " - " AND n.nspname = "); - deparseStringLiteral(&buf, stmt->remote_schema); + if (reltuples > 0) + { + StringInfoData column_list; - /* Partitions are supported since Postgres 10 */ - if (PQserverVersion(conn) >= 100000 && - stmt->list_type != FDW_IMPORT_SCHEMA_LIMIT_TO) - appendStringInfoString(&buf, " AND NOT c.relispartition "); + *p_remattrmap = remattrmap = build_remattrmap(relation, va_cols, + &attrcnt, &column_list); + *p_attrcnt = attrcnt; - /* Apply restrictions for LIMIT TO and EXCEPT */ - if (stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO || - stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) + if (attrcnt > 0) { - bool first_item = true; + /* Fetch attribute stats. */ + remstats->att = attstats = fetch_attstats(conn, + server_version_num, + remote_schemaname, + remote_relname, + column_list.data); + + /* If any attribute stats are missing, fallback to sampling. */ + if (!match_attrmap(attstats, + local_schemaname, local_relname, + remote_schemaname, remote_relname, + attrcnt, remattrmap)) + goto fetch_cleanup; + } + } - appendStringInfoString(&buf, " AND c.relname "); - if (stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) - appendStringInfoString(&buf, "NOT "); - appendStringInfoString(&buf, "IN ("); + ok = true; - /* Append list of table names within IN clause */ - foreach(lc, stmt->table_list) - { - RangeVar *rv = (RangeVar *) lfirst(lc); +fetch_cleanup: + ReleaseConnection(conn); + return ok; +} - if (first_item) - first_item = false; - else - appendStringInfoString(&buf, ", "); - deparseStringLiteral(&buf, rv->relname); +/* + * Attempt to fetch remote relation stats. + */ +static PGresult * +fetch_relstats(PGconn *conn, Relation relation) +{ + StringInfoData sql; + PGresult *res; + + initStringInfo(&sql); + deparseAnalyzeInfoSql(&sql, relation); + + res = pgfdw_exec_query(conn, sql.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, sql.data); + + if (PQntuples(res) != 1 || PQnfields(res) != RELSTATS_NUM_FIELDS) + elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query"); + + return res; +} + +/* + * Attempt to fetch remote attribute stats. + */ +static PGresult * +fetch_attstats(PGconn *conn, int server_version_num, + const char *remote_schemaname, const char *remote_relname, + const char *column_list) +{ + StringInfoData sql; + PGresult *res; + + initStringInfo(&sql); + appendStringInfoString(&sql, + "SELECT DISTINCT ON (attname COLLATE \"C\") attname," + " null_frac," + " avg_width," + " n_distinct," + " most_common_vals," + " most_common_freqs," + " histogram_bounds," + " correlation,"); + + /* Elements stats are supported since Postgres 9.2 */ + if (server_version_num >= 92000) + appendStringInfoString(&sql, + " most_common_elems," + " most_common_elem_freqs," + " elem_count_histogram,"); + else + appendStringInfoString(&sql, + " NULL, NULL, NULL,"); + + /* Range stats are supported since Postgres 17 */ + if (server_version_num >= 170000) + appendStringInfoString(&sql, + " range_length_histogram," + " range_empty_frac," + " range_bounds_histogram"); + else + appendStringInfoString(&sql, + " NULL, NULL, NULL,"); + + appendStringInfoString(&sql, + " FROM pg_catalog.pg_stats" + " WHERE schemaname = "); + deparseStringLiteral(&sql, remote_schemaname); + appendStringInfoString(&sql, + " AND tablename = "); + deparseStringLiteral(&sql, remote_relname); + appendStringInfo(&sql, + " AND attname = ANY('%s'::text[])", + column_list); + + /* inherited is supported since Postgres 9.0 */ + if (server_version_num >= 90000) + appendStringInfoString(&sql, + " ORDER BY attname COLLATE \"C\", inherited DESC"); + else + appendStringInfoString(&sql, + " ORDER BY attname COLLATE \"C\""); + + res = pgfdw_exec_query(conn, sql.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, sql.data); + + if (PQnfields(res) != ATTSTATS_NUM_FIELDS) + elog(ERROR, "unexpected result from fetch_attstats query"); + + return res; +} + +/* + * Build the mapping of local columns to remote columns and create a column + * list used for constructing the fetch_attstats query. + */ +static RemoteAttributeMapping * +build_remattrmap(Relation relation, List *va_cols, + int *p_attrcnt, StringInfo column_list) +{ + TupleDesc tupdesc = RelationGetDescr(relation); + RemoteAttributeMapping *remattrmap = NULL; + int attrcnt = 0; + + remattrmap = palloc_array(RemoteAttributeMapping, tupdesc->natts); + initStringInfo(column_list); + appendStringInfoChar(column_list, '{'); + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + char *attname = NameStr(attr->attname); + AttrNumber attnum = attr->attnum; + char *remote_attname; + List *fc_options; + ListCell *lc; + + /* If a list is specified, exclude any attnames not in it. */ + if (!attname_in_list(attname, va_cols)) + continue; + + if (!attribute_is_analyzable(relation, attnum, attr, NULL)) + continue; + + /* If the column_name option is not specified, go with attname. */ + remote_attname = attname; + fc_options = GetForeignColumnOptions(RelationGetRelid(relation), attnum); + foreach(lc, fc_options) + { + DefElem *def = (DefElem *) lfirst(lc); + + if (strcmp(def->defname, "column_name") == 0) + { + remote_attname = defGetString(def); + break; } - appendStringInfoChar(&buf, ')'); } - /* Append ORDER BY at the end of query to ensure output ordering */ - appendStringInfoString(&buf, " ORDER BY c.relname, a.attnum"); + if (attrcnt > 0) + appendStringInfoString(column_list, ", "); + appendStringInfoString(column_list, quote_identifier(remote_attname)); - /* Fetch the data */ - res = pgfdw_exec_query(conn, buf.data, NULL); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pgfdw_report_error(ERROR, res, conn, false, buf.data); + remattrmap[attrcnt].local_attnum = attnum; + strncpy(remattrmap[attrcnt].local_attname, attname, NAMEDATALEN); + strncpy(remattrmap[attrcnt].remote_attname, remote_attname, NAMEDATALEN); + remattrmap[attrcnt].res_index = -1; + attrcnt++; + } + appendStringInfoChar(column_list, '}'); - /* Process results */ - numrows = PQntuples(res); - /* note: incrementation of i happens in inner loop's while() test */ - for (i = 0; i < numrows;) + /* Sort mapping by remote attribute name if needed. */ + if (attrcnt > 1) + qsort(remattrmap, attrcnt, sizeof(RemoteAttributeMapping), remattrmap_cmp); + + *p_attrcnt = attrcnt; + return remattrmap; +} + +/* + * Test if an attribute name is in the list. + * + * An empty list means that all attribute names are in the list. + */ +static bool +attname_in_list(const char *attname, List *va_cols) +{ + ListCell *lc; + + if (va_cols == NIL) + return true; + + foreach(lc, va_cols) + { + char *col = strVal(lfirst(lc)); + + if (strcmp(attname, col) == 0) + return true; + } + return false; +} + +/* + * Compare two RemoteAttributeMappings for sorting. + */ +static int +remattrmap_cmp(const void *v1, const void *v2) +{ + const RemoteAttributeMapping *r1 = v1; + const RemoteAttributeMapping *r2 = v2; + + return strncmp(r1->remote_attname, r2->remote_attname, NAMEDATALEN); +} + +/* + * Match local columns to result set rows. + * + * As the result set consists of the attribute stats for some/all of distinct + * mapped remote columns in the RemoteAttributeMapping, every entry in it + * should have at most one match in the result set; which is also ordered by + * attname, so we find such pairs by doing a merge join. + * + * Returns true if every entry in it has a match, and false if not. + */ +static bool +match_attrmap(PGresult *res, + const char *local_schemaname, + const char *local_relname, + const char *remote_schemaname, + const char *remote_relname, + int attrcnt, + RemoteAttributeMapping *remattrmap) +{ + int numrows = PQntuples(res); + int row = -1; + + /* No work if there are no stats rows. */ + if (numrows == 0) + { + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- remote table \"%s.%s\" has no attribute statistics to import", + local_schemaname, local_relname, + remote_schemaname, remote_relname)); + return false; + } + + /* Scan all entries in the RemoteAttributeMapping. */ + for (int mapidx = 0; mapidx < attrcnt; mapidx++) + { + /* + * First, check whether the entry matches the current stats row, if it + * is set. + */ + if (row >= 0 && + strcmp(remattrmap[mapidx].remote_attname, + PQgetvalue(res, row, ATTSTATS_ATTNAME)) == 0) { - char *tablename = PQgetvalue(res, i, 0); - bool first_item = true; + remattrmap[mapidx].res_index = row; + continue; + } - resetStringInfo(&buf); - appendStringInfo(&buf, "CREATE FOREIGN TABLE %s (\n", - quote_identifier(tablename)); + /* + * If we've exhausted all stats rows, it means the stats for the entry + * are missing. + */ + if (row >= numrows - 1) + { + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- no attribute statistics found for column \"%s\" of remote table \"%s.%s\"", + local_schemaname, local_relname, + remattrmap[mapidx].remote_attname, + remote_schemaname, remote_relname)); + return false; + } - /* Scan all rows for this table */ - do - { - char *attname; - char *typename; - char *attnotnull; - char *attgenerated; - char *attdefault; - char *collname; - char *collnamespace; - - /* If table has no columns, we'll see nulls here */ - if (PQgetisnull(res, i, 1)) - continue; + /* Advance to the next stats row. */ + row += 1; - attname = PQgetvalue(res, i, 1); - typename = PQgetvalue(res, i, 2); - attnotnull = PQgetvalue(res, i, 3); - attdefault = PQgetisnull(res, i, 4) ? NULL : - PQgetvalue(res, i, 4); - attgenerated = PQgetisnull(res, i, 5) ? NULL : - PQgetvalue(res, i, 5); - collname = PQgetisnull(res, i, 6) ? NULL : - PQgetvalue(res, i, 6); - collnamespace = PQgetisnull(res, i, 7) ? NULL : - PQgetvalue(res, i, 7); - - if (first_item) - first_item = false; - else - appendStringInfoString(&buf, ",\n"); + /* + * If the attname in the entry is less than that in the next stats + * row, it means the stats for the entry are missing. + */ + if (strcmp(remattrmap[mapidx].remote_attname, + PQgetvalue(res, row, ATTSTATS_ATTNAME)) < 0) + { + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- no attribute statistics found for column \"%s\" of remote table \"%s.%s\"", + local_schemaname, local_relname, + remattrmap[mapidx].remote_attname, + remote_schemaname, remote_relname)); + return false; + } - /* Print column name and type */ - appendStringInfo(&buf, " %s %s", - quote_identifier(attname), - typename); + /* We should not have got a stats row we didn't expect. */ + if (strcmp(remattrmap[mapidx].remote_attname, + PQgetvalue(res, row, ATTSTATS_ATTNAME)) > 0) + elog(ERROR, "unexpected result from fetch_attstats query"); - /* - * Add column_name option so that renaming the foreign table's - * column doesn't break the association to the underlying - * column. - */ - appendStringInfoString(&buf, " OPTIONS (column_name "); - deparseStringLiteral(&buf, attname); - appendStringInfoChar(&buf, ')'); - - /* Add COLLATE if needed */ - if (import_collate && collname != NULL && collnamespace != NULL) - appendStringInfo(&buf, " COLLATE %s.%s", - quote_identifier(collnamespace), - quote_identifier(collname)); - - /* Add DEFAULT if needed */ - if (import_default && attdefault != NULL && - (!attgenerated || !attgenerated[0])) - appendStringInfo(&buf, " DEFAULT %s", attdefault); - - /* Add GENERATED if needed */ - if (import_generated && attgenerated != NULL && - attgenerated[0] == ATTRIBUTE_GENERATED_STORED) - { - Assert(attdefault != NULL); - appendStringInfo(&buf, - " GENERATED ALWAYS AS (%s) STORED", - attdefault); - } + /* We found a match. */ + Assert(strcmp(remattrmap[mapidx].remote_attname, + PQgetvalue(res, row, ATTSTATS_ATTNAME)) == 0); + remattrmap[mapidx].res_index = row; + } - /* Add NOT NULL if needed */ - if (import_not_null && attnotnull[0] == 't') - appendStringInfoString(&buf, " NOT NULL"); - } - while (++i < numrows && - strcmp(PQgetvalue(res, i, 0), tablename) == 0); + /* We should have exhausted all stats rows. */ + if (row < numrows - 1) + elog(ERROR, "unexpected result from fetch_attstats query"); + + return true; +} + +/* + * Import fetched statistics into the local statistics tables. + */ +static bool +import_fetched_statistics(const char *schemaname, + const char *relname, + int attrcnt, + const RemoteAttributeMapping *remattrmap, + RemoteStatsResults *remstats) +{ + SPIPlanPtr attimport_plan = NULL; + SPIPlanPtr attclear_plan = NULL; + Datum values[ATTIMPORT_SQL_NUM_FIELDS]; + char nulls[ATTIMPORT_SQL_NUM_FIELDS]; + int spirc; + bool ok = false; + + /* Assign all the invariant parameters common to relation/attribute stats */ + values[ATTIMPORT_SQL_VERSION] = Int32GetDatum(remstats->server_version_num); + nulls[ATTIMPORT_SQL_VERSION] = ' '; + + values[ATTIMPORT_SQL_SCHEMANAME] = CStringGetTextDatum(schemaname); + nulls[ATTIMPORT_SQL_SCHEMANAME] = ' '; + + values[ATTIMPORT_SQL_RELNAME] = CStringGetTextDatum(relname); + nulls[ATTIMPORT_SQL_RELNAME] = ' '; + + SPI_connect(); + + /* + * We import attribute statistics first, if any, because those are more + * prone to errors. This avoids making a modification of pg_class that + * will just get rolled back by a failed attribute import. + */ + if (remstats->att != NULL) + { + Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS); + Assert(PQntuples(remstats->att) >= 1); + + attimport_plan = SPI_prepare(attimport_sql, ATTIMPORT_SQL_NUM_FIELDS, + (Oid *) attimport_argtypes); + if (attimport_plan == NULL) + elog(ERROR, "failed to prepare attimport_sql query"); + + attclear_plan = SPI_prepare(attclear_sql, ATTCLEAR_SQL_NUM_FIELDS, + (Oid *) attclear_argtypes); + if (attclear_plan == NULL) + elog(ERROR, "failed to prepare attclear_sql query"); + + nulls[ATTIMPORT_SQL_ATTNUM] = ' '; + + for (int mapidx = 0; mapidx < attrcnt; mapidx++) + { + int row = remattrmap[mapidx].res_index; + Datum *values2 = values + 1; + char *nulls2 = nulls + 1; + + /* All mappings should have been assigned a result set row. */ + Assert(row >= 0); /* - * Add server name and table-level options. We specify remote - * schema and table name as options (the latter to ensure that - * renaming the foreign table doesn't break the association). + * Check for user-requested abort. */ - appendStringInfo(&buf, "\n) SERVER %s\nOPTIONS (", - quote_identifier(server->servername)); + CHECK_FOR_INTERRUPTS(); + + /* + * First, clear existing attribute stats. + * + * We can re-use the values/nulls because the number of parameters + * is less and the first two params are the same as the second and + * third ones in attimport_sql. + */ + values2[ATTCLEAR_SQL_ATTNAME] = + CStringGetTextDatum(remattrmap[mapidx].local_attname); + + spirc = SPI_execute_plan(attclear_plan, values2, nulls2, false, 1); + if (spirc != SPI_OK_SELECT) + elog(ERROR, "failed to execute attclear_sql query for column \"%s\" of foreign table \"%s.%s\"", + remattrmap[mapidx].local_attname, schemaname, relname); + + values[ATTIMPORT_SQL_ATTNUM] = + Int16GetDatum(remattrmap[mapidx].local_attnum); + + /* Loop through all mappable columns to set remaining arguments */ + for (int i = 0; i < NUM_MAPPED_ATTIMPORT_ARGS; i++) + map_field_to_arg(remstats->att, row, + attr_result_arg_map[i].res_field, + attr_result_arg_map[i].arg_num, + values, nulls); + + spirc = SPI_execute_plan(attimport_plan, values, nulls, false, 1); + if (spirc != SPI_OK_SELECT) + elog(ERROR, "failed to execute attimport_sql query for column \"%s\" of foreign table \"%s.%s\"", + remattrmap[mapidx].local_attname, schemaname, relname); + + if (!import_spi_query_ok()) + { + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- attribute statistics import failed for column \"%s\" of this foreign table", + schemaname, relname, + remattrmap[mapidx].local_attname)); + goto import_cleanup; + } + } + } + + /* + * Import relation stats. We only perform this once, so there is no point + * in preparing the statement. + * + * We can re-use the values/nulls because the number of parameters is less + * and the first three params are the same as attimport_sql. + */ + Assert(remstats->rel != NULL); + Assert(PQnfields(remstats->rel) == RELSTATS_NUM_FIELDS); + Assert(PQntuples(remstats->rel) == 1); + map_field_to_arg(remstats->rel, 0, RELSTATS_RELPAGES, + RELIMPORT_SQL_RELPAGES, values, nulls); + map_field_to_arg(remstats->rel, 0, RELSTATS_RELTUPLES, + RELIMPORT_SQL_RELTUPLES, values, nulls); + + spirc = SPI_execute_with_args(relimport_sql, + RELIMPORT_SQL_NUM_FIELDS, + (Oid *) relimport_argtypes, + values, nulls, false, 1); + if (spirc != SPI_OK_SELECT) + elog(ERROR, "failed to execute relimport_sql query for foreign table \"%s.%s\"", + schemaname, relname); + + if (!import_spi_query_ok()) + { + ereport(WARNING, + errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table", + schemaname, relname)); + goto import_cleanup; + } + + ok = true; + +import_cleanup: + if (attimport_plan) + SPI_freeplan(attimport_plan); + if (attclear_plan) + SPI_freeplan(attclear_plan); + SPI_finish(); + return ok; +} + +/* + * Move a string value from a result set to a Text value of a Datum array. + */ +static void +map_field_to_arg(PGresult *res, int row, int field, + int arg, Datum *values, char *nulls) +{ + if (PQgetisnull(res, row, field)) + { + values[arg] = (Datum) 0; + nulls[arg] = 'n'; + } + else + { + const char *s = PQgetvalue(res, row, field); + + values[arg] = CStringGetTextDatum(s); + nulls[arg] = ' '; + } +} + +/* + * Check the 1x1 result set of a pg_restore_*_stats() command for success. + */ +static bool +import_spi_query_ok(void) +{ + TupleDesc tupdesc; + Datum dat; + bool isnull; + + Assert(SPI_tuptable != NULL); + Assert(SPI_processed == 1); - appendStringInfoString(&buf, "schema_name "); - deparseStringLiteral(&buf, stmt->remote_schema); - appendStringInfoString(&buf, ", table_name "); - deparseStringLiteral(&buf, tablename); + tupdesc = SPI_tuptable->tupdesc; + Assert(tupdesc->natts == 1); + Assert(TupleDescAttr(tupdesc, 0)->atttypid == BOOLOID); + dat = SPI_getbinval(SPI_tuptable->vals[0], tupdesc, 1, &isnull); + Assert(!isnull); - appendStringInfoString(&buf, ");"); + return DatumGetBool(dat); +} + +/* + * Import a foreign schema + */ +static List * +postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) +{ + List *commands = NIL; + bool import_collate = true; + bool import_default = false; + bool import_generated = true; + bool import_not_null = true; + ForeignServer *server; + UserMapping *mapping; + PGconn *conn; + StringInfoData buf; + PGresult *res; + int numrows, + i; + ListCell *lc; - commands = lappend(commands, pstrdup(buf.data)); + /* Parse statement options */ + foreach(lc, stmt->options) + { + DefElem *def = (DefElem *) lfirst(lc); + + if (strcmp(def->defname, "import_collate") == 0) + import_collate = defGetBoolean(def); + else if (strcmp(def->defname, "import_default") == 0) + import_default = defGetBoolean(def); + else if (strcmp(def->defname, "import_generated") == 0) + import_generated = defGetBoolean(def); + else if (strcmp(def->defname, "import_not_null") == 0) + import_not_null = defGetBoolean(def); + else + ereport(ERROR, + (errcode(ERRCODE_FDW_INVALID_OPTION_NAME), + errmsg("invalid option \"%s\"", def->defname))); + } + + /* + * Get connection to the foreign server. Connection manager will + * establish new connection if necessary. + */ + server = GetForeignServer(serverOid); + mapping = GetUserMapping(GetUserId(), server->serverid); + conn = GetConnection(mapping, false, NULL); + + /* Don't attempt to import collation if remote server hasn't got it */ + if (PQserverVersion(conn) < 90100) + import_collate = false; + + /* Create workspace for strings */ + initStringInfo(&buf); + + /* Check that the schema really exists */ + appendStringInfoString(&buf, "SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = "); + deparseStringLiteral(&buf, stmt->remote_schema); + + res = pgfdw_exec_query(conn, buf.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, buf.data); + + if (PQntuples(res) != 1) + ereport(ERROR, + (errcode(ERRCODE_FDW_SCHEMA_NOT_FOUND), + errmsg("schema \"%s\" is not present on foreign server \"%s\"", + stmt->remote_schema, server->servername))); + + PQclear(res); + resetStringInfo(&buf); + + /* + * Fetch all table data from this schema, possibly restricted by EXCEPT or + * LIMIT TO. (We don't actually need to pay any attention to EXCEPT/LIMIT + * TO here, because the core code will filter the statements we return + * according to those lists anyway. But it should save a few cycles to + * not process excluded tables in the first place.) + * + * Import table data for partitions only when they are explicitly + * specified in LIMIT TO clause. Otherwise ignore them and only include + * the definitions of the root partitioned tables to allow access to the + * complete remote data set locally in the schema imported. + * + * Note: because we run the connection with search_path restricted to + * pg_catalog, the format_type() and pg_get_expr() outputs will always + * include a schema name for types/functions in other schemas, which is + * what we want. + */ + appendStringInfoString(&buf, + "SELECT relname, " + " attname, " + " format_type(atttypid, atttypmod), " + " attnotnull, " + " pg_get_expr(adbin, adrelid), "); + + /* Generated columns are supported since Postgres 12 */ + if (PQserverVersion(conn) >= 120000) + appendStringInfoString(&buf, + " attgenerated, "); + else + appendStringInfoString(&buf, + " NULL, "); + + if (import_collate) + appendStringInfoString(&buf, + " collname, " + " collnsp.nspname "); + else + appendStringInfoString(&buf, + " NULL, NULL "); + + appendStringInfoString(&buf, + "FROM pg_class c " + " JOIN pg_namespace n ON " + " relnamespace = n.oid " + " LEFT JOIN pg_attribute a ON " + " attrelid = c.oid AND attnum > 0 " + " AND NOT attisdropped " + " LEFT JOIN pg_attrdef ad ON " + " adrelid = c.oid AND adnum = attnum "); + + if (import_collate) + appendStringInfoString(&buf, + " LEFT JOIN pg_collation coll ON " + " coll.oid = attcollation " + " LEFT JOIN pg_namespace collnsp ON " + " collnsp.oid = collnamespace "); + + appendStringInfoString(&buf, + "WHERE c.relkind IN (" + CppAsString2(RELKIND_RELATION) "," + CppAsString2(RELKIND_VIEW) "," + CppAsString2(RELKIND_FOREIGN_TABLE) "," + CppAsString2(RELKIND_MATVIEW) "," + CppAsString2(RELKIND_PARTITIONED_TABLE) ") " + " AND n.nspname = "); + deparseStringLiteral(&buf, stmt->remote_schema); + + /* Partitions are supported since Postgres 10 */ + if (PQserverVersion(conn) >= 100000 && + stmt->list_type != FDW_IMPORT_SCHEMA_LIMIT_TO) + appendStringInfoString(&buf, " AND NOT c.relispartition "); + + /* Apply restrictions for LIMIT TO and EXCEPT */ + if (stmt->list_type == FDW_IMPORT_SCHEMA_LIMIT_TO || + stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) + { + bool first_item = true; + + appendStringInfoString(&buf, " AND c.relname "); + if (stmt->list_type == FDW_IMPORT_SCHEMA_EXCEPT) + appendStringInfoString(&buf, "NOT "); + appendStringInfoString(&buf, "IN ("); + + /* Append list of table names within IN clause */ + foreach(lc, stmt->table_list) + { + RangeVar *rv = (RangeVar *) lfirst(lc); + + if (first_item) + first_item = false; + else + appendStringInfoString(&buf, ", "); + deparseStringLiteral(&buf, rv->relname); } + appendStringInfoChar(&buf, ')'); } - PG_FINALLY(); + + /* Append ORDER BY at the end of query to ensure output ordering */ + appendStringInfoString(&buf, " ORDER BY c.relname, a.attnum"); + + /* Fetch the data */ + res = pgfdw_exec_query(conn, buf.data, NULL); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + pgfdw_report_error(res, conn, buf.data); + + /* Process results */ + numrows = PQntuples(res); + /* note: incrementation of i happens in inner loop's while() test */ + for (i = 0; i < numrows;) { - PQclear(res); + char *tablename = PQgetvalue(res, i, 0); + bool first_item = true; + + resetStringInfo(&buf); + appendStringInfo(&buf, "CREATE FOREIGN TABLE %s (\n", + quote_identifier(tablename)); + + /* Scan all rows for this table */ + do + { + char *attname; + char *typename; + char *attnotnull; + char *attgenerated; + char *attdefault; + char *collname; + char *collnamespace; + + /* If table has no columns, we'll see nulls here */ + if (PQgetisnull(res, i, 1)) + continue; + + attname = PQgetvalue(res, i, 1); + typename = PQgetvalue(res, i, 2); + attnotnull = PQgetvalue(res, i, 3); + attdefault = PQgetisnull(res, i, 4) ? NULL : + PQgetvalue(res, i, 4); + attgenerated = PQgetisnull(res, i, 5) ? NULL : + PQgetvalue(res, i, 5); + collname = PQgetisnull(res, i, 6) ? NULL : + PQgetvalue(res, i, 6); + collnamespace = PQgetisnull(res, i, 7) ? NULL : + PQgetvalue(res, i, 7); + + if (first_item) + first_item = false; + else + appendStringInfoString(&buf, ",\n"); + + /* Print column name and type */ + appendStringInfo(&buf, " %s %s", + quote_identifier(attname), + typename); + + /* + * Add column_name option so that renaming the foreign table's + * column doesn't break the association to the underlying column. + */ + appendStringInfoString(&buf, " OPTIONS (column_name "); + deparseStringLiteral(&buf, attname); + appendStringInfoChar(&buf, ')'); + + /* Add COLLATE if needed */ + if (import_collate && collname != NULL && collnamespace != NULL) + appendStringInfo(&buf, " COLLATE %s.%s", + quote_identifier(collnamespace), + quote_identifier(collname)); + + /* Add DEFAULT if needed */ + if (import_default && attdefault != NULL && + (!attgenerated || !attgenerated[0])) + appendStringInfo(&buf, " DEFAULT %s", attdefault); + + /* Add GENERATED if needed */ + if (import_generated && attgenerated != NULL && + attgenerated[0] == ATTRIBUTE_GENERATED_STORED) + { + Assert(attdefault != NULL); + appendStringInfo(&buf, + " GENERATED ALWAYS AS (%s) STORED", + attdefault); + } + + /* Add NOT NULL if needed */ + if (import_not_null && attnotnull[0] == 't') + appendStringInfoString(&buf, " NOT NULL"); + } + while (++i < numrows && + strcmp(PQgetvalue(res, i, 0), tablename) == 0); + + /* + * Add server name and table-level options. We specify remote schema + * and table name as options (the latter to ensure that renaming the + * foreign table doesn't break the association). + */ + appendStringInfo(&buf, "\n) SERVER %s\nOPTIONS (", + quote_identifier(server->servername)); + + appendStringInfoString(&buf, "schema_name "); + deparseStringLiteral(&buf, stmt->remote_schema); + appendStringInfoString(&buf, ", table_name "); + deparseStringLiteral(&buf, tablename); + + appendStringInfoString(&buf, ");"); + + commands = lappend(commands, pstrdup(buf.data)); } - PG_END_TRY(); + PQclear(res); ReleaseConnection(conn); @@ -6394,7 +7226,7 @@ postgresGetForeignJoinPaths(PlannerInfo *root, * if found safe. Once we know that this join can be pushed down, we fill * the entry. */ - fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo)); + fpinfo = palloc0_object(PgFdwRelationInfo); fpinfo->pushdown_safe = false; joinrel->fdw_private = fpinfo; /* attrs_used is only for base relations. */ @@ -6763,7 +7595,7 @@ postgresGetForeignUpperPaths(PlannerInfo *root, UpperRelationKind stage, output_rel->fdw_private) return; - fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo)); + fpinfo = palloc0_object(PgFdwRelationInfo); fpinfo->pushdown_safe = false; fpinfo->stage = stage; output_rel->fdw_private = fpinfo; @@ -6988,7 +7820,7 @@ add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel, fpinfo->pushdown_safe = true; /* Construct PgFdwPathExtraData */ - fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData)); + fpextra = palloc0_object(PgFdwPathExtraData); fpextra->target = root->upper_targets[UPPERREL_ORDERED]; fpextra->has_final_sort = true; @@ -7222,7 +8054,7 @@ add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel, fpinfo->pushdown_safe = true; /* Construct PgFdwPathExtraData */ - fpextra = (PgFdwPathExtraData *) palloc0(sizeof(PgFdwPathExtraData)); + fpextra = palloc0_object(PgFdwPathExtraData); fpextra->target = root->upper_targets[UPPERREL_FINAL]; fpextra->has_final_sort = has_final_sort; fpextra->has_limit = extra->limit_needed; @@ -7409,7 +8241,7 @@ postgresForeignAsyncNotify(AsyncRequest *areq) /* On error, report the original query, not the FETCH. */ if (!PQconsumeInput(fsstate->conn)) - pgfdw_report_error(ERROR, NULL, fsstate->conn, false, fsstate->query); + pgfdw_report_error(NULL, fsstate->conn, fsstate->query); fetch_more_data(node); @@ -7508,7 +8340,7 @@ fetch_more_data_begin(AsyncRequest *areq) fsstate->fetch_size, fsstate->cursor_number); if (!PQsendQuery(fsstate->conn, sql)) - pgfdw_report_error(ERROR, NULL, fsstate->conn, false, fsstate->query); + pgfdw_report_error(NULL, fsstate->conn, fsstate->query); /* Remember that the request is in process */ fsstate->conn_state->pendingAreq = areq; diff --git a/contrib/postgres_fdw/postgres_fdw.control b/contrib/postgres_fdw/postgres_fdw.control index a4b800be4fca0..ae2963d480d3a 100644 --- a/contrib/postgres_fdw/postgres_fdw.control +++ b/contrib/postgres_fdw/postgres_fdw.control @@ -1,5 +1,5 @@ # postgres_fdw extension comment = 'foreign-data wrapper for remote PostgreSQL servers' -default_version = '1.2' +default_version = '1.3' module_pathname = '$libdir/postgres_fdw' relocatable = true diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index 81358f3bde7df..a2bb1ff352c2d 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -3,7 +3,7 @@ * postgres_fdw.h * Foreign-data wrapper for remote PostgreSQL servers * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/postgres_fdw/postgres_fdw.h @@ -15,7 +15,7 @@ #include "foreign/foreign.h" #include "lib/stringinfo.h" -#include "libpq-fe.h" +#include "libpq/libpq-be-fe.h" #include "nodes/execnodes.h" #include "nodes/pathnodes.h" #include "utils/relcache.h" @@ -166,8 +166,10 @@ extern void do_sql_command(PGconn *conn, const char *sql); extern PGresult *pgfdw_get_result(PGconn *conn); extern PGresult *pgfdw_exec_query(PGconn *conn, const char *query, PgFdwConnState *state); -extern void pgfdw_report_error(int elevel, PGresult *res, PGconn *conn, - bool clear, const char *sql); +pg_noreturn extern void pgfdw_report_error(PGresult *res, PGconn *conn, + const char *sql); +extern void pgfdw_report(int elevel, PGresult *res, PGconn *conn, + const char *sql); /* in option.c */ extern int ExtractConnectionOptions(List *defelems, diff --git a/contrib/postgres_fdw/shippable.c b/contrib/postgres_fdw/shippable.c index da3b13b207d17..250f54fea32b7 100644 --- a/contrib/postgres_fdw/shippable.c +++ b/contrib/postgres_fdw/shippable.c @@ -13,7 +13,7 @@ * functions or functions using nonportable collations. Those considerations * need not be accounted for here. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/postgres_fdw/shippable.c @@ -62,7 +62,8 @@ typedef struct * made for them, however. */ static void -InvalidateShippableCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +InvalidateShippableCacheCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { HASH_SEQ_STATUS status; ShippableCacheEntry *entry; diff --git a/contrib/postgres_fdw/specs/eval_plan_qual.spec b/contrib/postgres_fdw/specs/eval_plan_qual.spec new file mode 100644 index 0000000000000..9f52270db6984 --- /dev/null +++ b/contrib/postgres_fdw/specs/eval_plan_qual.spec @@ -0,0 +1,102 @@ +# Tests for the EvalPlanQual mechanism involving foreign tables + +setup +{ + DO $d$ + BEGIN + EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '$$||current_database()||$$', + port '$$||current_setting('port')||$$', + use_remote_estimate 'true' + )$$; + END; + $d$; + CREATE USER MAPPING FOR PUBLIC SERVER loopback; + + CREATE TABLE l (i int, v text); + CREATE TABLE t (i int, v text); + CREATE FOREIGN TABLE ft (i int, v text) SERVER loopback OPTIONS (table_name 't'); + + INSERT INTO l VALUES (123, 'foo'), (456, 'bar'), (789, 'baz'); + INSERT INTO t SELECT i, to_char(i, 'FM0000') FROM generate_series(1, 1000) i; + CREATE INDEX t_idx ON t (i); + ANALYZE l, t; + + CREATE TABLE a (i int); + CREATE TABLE b (i int); + CREATE TABLE c (i int); + CREATE FOREIGN TABLE fb (i int) SERVER loopback OPTIONS (table_name 'b'); + CREATE FOREIGN TABLE fc (i int) SERVER loopback OPTIONS (table_name 'c'); + + INSERT INTO a VALUES (1); + INSERT INTO b VALUES (1); + INSERT INTO c VALUES (1); + ANALYZE a, b, c; +} + +teardown +{ + DROP TABLE l; + DROP TABLE t; + DROP TABLE a; + DROP TABLE b; + DROP TABLE c; + DROP SERVER loopback CASCADE; +} + +session s0 +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s0_update_l { UPDATE l SET i = i + 1; } +step s0_update_a { UPDATE a SET i = i + 1; } +step s0_commit { COMMIT; } + +session s1 +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } + +# Test for EPQ with a foreign scan pushing down a qual +step s1_tuplock_l_0 { + EXPLAIN (VERBOSE, COSTS OFF) + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.i = 123 FOR UPDATE OF l; + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.i = 123 FOR UPDATE OF l; +} + +# Same test, except that the qual is parameterized +step s1_tuplock_l_1 { + EXPLAIN (VERBOSE, COSTS OFF) + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.v = 'foo' FOR UPDATE OF l; + SELECT l.* FROM l, ft WHERE l.i = ft.i AND l.v = 'foo' FOR UPDATE OF l; +} + +# Test for EPQ with a foreign scan pushing down a join +step s1_tuplock_a_0 { + EXPLAIN (VERBOSE, COSTS OFF) + SELECT a.i FROM a, fb, fc WHERE a.i = fb.i AND fb.i = fc.i FOR UPDATE OF a; + SELECT a.i FROM a, fb, fc WHERE a.i = fb.i AND fb.i = fc.i FOR UPDATE OF a; +} + +# Same test, except that the join is contained in a SubLink sub-select, not +# in the main query +step s1_tuplock_a_1 { + EXPLAIN (VERBOSE, COSTS OFF) + SELECT a.i, + (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i) + FROM a FOR UPDATE; + SELECT a.i, + (SELECT 1 FROM fb, fc WHERE a.i = fb.i AND fb.i = fc.i) + FROM a FOR UPDATE; +} + +step s1_commit { COMMIT; } + +# This test checks the case of rechecking a pushed-down qual. +permutation s0_update_l s1_tuplock_l_0 s0_commit s1_commit + +# This test checks the same case, except that the qual is parameterized. +permutation s0_update_l s1_tuplock_l_1 s0_commit s1_commit + +# This test checks the case of rechecking a pushed-down join. +permutation s0_update_a s1_tuplock_a_0 s0_commit s1_commit + +# This test exercises EvalPlanQual with a SubLink sub-select (which should +# be unaffected by any EPQ recheck behavior in the outer query). +permutation s0_update_a s1_tuplock_a_1 s0_commit s1_commit diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index e534b40de3c76..79ad5be8bf919 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4,24 +4,17 @@ CREATE EXTENSION postgres_fdw; -CREATE SERVER testserver1 FOREIGN DATA WRAPPER postgres_fdw; -DO $d$ - BEGIN - EXECUTE $$CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - EXECUTE $$CREATE SERVER loopback3 FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - END; -$d$; +SELECT current_database() AS current_database, + current_setting('port') AS current_port +\gset +CREATE SERVER testserver1 FOREIGN DATA WRAPPER postgres_fdw; +CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); +CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); +CREATE SERVER loopback3 FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); CREATE USER MAPPING FOR public SERVER testserver1 OPTIONS (user 'value', password 'value'); CREATE USER MAPPING FOR CURRENT_USER SERVER loopback; @@ -61,12 +54,20 @@ CREATE TABLE "S 1"."T 4" ( c3 text, CONSTRAINT t4_pkey PRIMARY KEY (c1) ); +CREATE TABLE "S 1"."T 5" ( + c1 int4range NOT NULL, + c2 int NOT NULL, + c3 text, + c4 daterange NOT NULL, + CONSTRAINT t5_pkey PRIMARY KEY (c1, c4 WITHOUT OVERLAPS) +); -- Disable autovacuum for these tables to avoid unexpected effects of that ALTER TABLE "S 1"."T 1" SET (autovacuum_enabled = 'false'); ALTER TABLE "S 1"."T 2" SET (autovacuum_enabled = 'false'); ALTER TABLE "S 1"."T 3" SET (autovacuum_enabled = 'false'); ALTER TABLE "S 1"."T 4" SET (autovacuum_enabled = 'false'); +ALTER TABLE "S 1"."T 5" SET (autovacuum_enabled = 'false'); INSERT INTO "S 1"."T 1" SELECT id, @@ -94,11 +95,18 @@ INSERT INTO "S 1"."T 4" 'AAA' || to_char(id, 'FM000') FROM generate_series(1, 100) id; DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0; -- delete for outer join tests +INSERT INTO "S 1"."T 5" + SELECT int4range(id, id + 1), + id + 1, + 'AAA' || to_char(id, 'FM000'), + '[2000-01-01,2020-01-01)' + FROM generate_series(1, 100) id; ANALYZE "S 1"."T 1"; ANALYZE "S 1"."T 2"; ANALYZE "S 1"."T 3"; ANALYZE "S 1"."T 4"; +ANALYZE "S 1"."T 5"; -- =================================================================== -- create foreign tables @@ -153,6 +161,14 @@ CREATE FOREIGN TABLE ft7 ( c3 text ) SERVER loopback3 OPTIONS (schema_name 'S 1', table_name 'T 4'); +CREATE FOREIGN TABLE ft8 ( + c1 int4range NOT NULL, + c2 int NOT NULL, + c3 text, + c4 daterange NOT NULL +) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 5'); + + -- =================================================================== -- tests for validator -- =================================================================== @@ -233,12 +249,7 @@ ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1'); SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work ALTER SERVER loopback OPTIONS (SET dbname 'no such database'); SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should fail -DO $d$ - BEGIN - EXECUTE $$ALTER SERVER loopback - OPTIONS (SET dbname '$$||current_database()||$$')$$; - END; -$d$; +ALTER SERVER loopback OPTIONS (SET dbname :'current_database'); SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again -- Test that alteration of user mapping options causes reconnection @@ -256,6 +267,13 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); +-- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_pgfdw_subscription; + -- =================================================================== -- test error case for create publication on foreign table -- =================================================================== @@ -352,7 +370,7 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS NULL; -- Nu EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS NOT NULL; -- NullTest EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE round(abs(c1), 0) = 1; -- FuncExpr EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = -c1; -- OpExpr(l) -EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE (c1 IS NOT NULL) IS DISTINCT FROM (c1 IS NOT NULL); -- DistinctExpr +EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c3 IS DISTINCT FROM c3; -- DistinctExpr EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = ANY(ARRAY[c2, 1, c1 + 0]); -- ScalarArrayOpExpr EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c1 = (ARRAY[c1,c2,3])[1]; -- SubscriptingRef EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE c6 = E'foo''s\\bar'; -- check special chars @@ -458,6 +476,15 @@ SELECT * FROM ft1 WHERE CASE c3 WHEN c6 THEN true ELSE c3 < 'bar' END; EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 WHERE CASE c3 COLLATE "C" WHEN c6 THEN true ELSE c3 < 'bar' END; +-- Test array type conversion pushdown +SET plan_cache_mode = force_generic_plan; +PREPARE s(varchar[]) AS SELECT count(*) FROM ft2 WHERE c6 = ANY ($1); +EXPLAIN (VERBOSE, COSTS OFF) +EXECUTE s(ARRAY['1','2']); +EXECUTE s(ARRAY['1','2']); +DEALLOCATE s; +RESET plan_cache_mode; + -- a regconfig constant referring to this text search configuration -- is initially unshippable CREATE TEXT SEARCH CONFIGURATION public.custom_search @@ -1168,20 +1195,28 @@ PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $ EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2); EXECUTE st1(1, 1); EXECUTE st1(101, 101); -SET enable_hashjoin TO off; + +-- These next tests require choosing between remote and local sort, which is +-- a coin flip so long as cost_sort() gives the same results on both sides. +-- To stabilize the expected plans, disable sorting locally. SET enable_sort TO off; + -- subquery using stable function (can't be sent to remote) +SET enable_hashjoin TO off; -- this one needs even more help to be stable PREPARE st2(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c4) = '1970-01-17'::date) ORDER BY c1; EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st2(10, 20); EXECUTE st2(10, 20); EXECUTE st2(101, 121); RESET enable_hashjoin; -RESET enable_sort; + -- subquery using immutable function (can be sent to remote) PREPARE st3(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 < $2 AND t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 > $1 AND date(c5) = '1970-01-17'::date) ORDER BY c1; EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st3(10, 20); EXECUTE st3(10, 20); EXECUTE st3(20, 30); + +RESET enable_sort; + -- custom plan should be chosen initially PREPARE st4(int) AS SELECT * FROM ft1 t1 WHERE t1.c1 = $1; EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st4(1); @@ -1541,6 +1576,17 @@ EXPLAIN (verbose, costs off) DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; -- can be pushed down DELETE FROM ft2 WHERE c1 = 1200 RETURNING tableoid::regclass; +-- Test UPDATE FOR PORTION OF +UPDATE ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' +SET c2 = c2 + 1 +WHERE c1 = '[1,2)'; +SELECT * FROM ft8 WHERE c1 = '[1,2)' ORDER BY c1, c4; + +-- Test DELETE FOR PORTION OF +DELETE FROM ft8 FOR PORTION OF c4 FROM '2005-01-01' TO '2006-01-01' +WHERE c1 = '[2,3)'; +SELECT * FROM ft8 WHERE c1 = '[2,3)' ORDER BY c1, c4; + -- Test UPDATE/DELETE with RETURNING on a three-table join INSERT INTO ft2 (c1,c2,c3) SELECT id, id - 1200, to_char(id, 'FM00000') FROM generate_series(1201, 1300) id; @@ -1608,9 +1654,16 @@ UPDATE ft2 d SET c2 = CASE WHEN random() >= 0 THEN d.c2 ELSE 0 END ALTER SERVER loopback OPTIONS (DROP extensions); INSERT INTO ft2 (c1,c2,c3) SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(2001, 2010) id; + +-- this will do a remote seqscan, causing unstable result order, so sort EXPLAIN (verbose, costs off) -UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING *; -- can't be pushed down -UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING *; +WITH cte AS ( + UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING * +) SELECT * FROM cte ORDER BY c1; -- can't be pushed down +WITH cte AS ( + UPDATE ft2 SET c3 = 'bar' WHERE postgres_fdw_abs(c1) > 2000 RETURNING * +) SELECT * FROM cte ORDER BY c1; + EXPLAIN (verbose, costs off) UPDATE ft2 SET c3 = 'baz' FROM ft4 INNER JOIN ft5 ON (ft4.c1 = ft5.c1) @@ -2272,6 +2325,84 @@ EXPLAIN (verbose, costs off) DELETE FROM rem1; -- can't be pushed down DROP TRIGGER trig_row_after_delete ON rem1; + +-- We are allowed to create transition-table triggers on both kinds of +-- inheritance even if they contain foreign tables as children, but currently +-- collecting transition tuples from such foreign tables is not supported. + +CREATE TABLE local_tbl (a text, b int); +CREATE FOREIGN TABLE foreign_tbl (a text, b int) + SERVER loopback OPTIONS (table_name 'local_tbl'); + +INSERT INTO foreign_tbl VALUES ('AAA', 42); + +-- Test case for partition hierarchy +CREATE TABLE parent_tbl (a text, b int) PARTITION BY LIST (a); +ALTER TABLE parent_tbl ATTACH PARTITION foreign_tbl FOR VALUES IN ('AAA'); + +CREATE TRIGGER parent_tbl_insert_trig + AFTER INSERT ON parent_tbl REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER parent_tbl_update_trig + AFTER UPDATE ON parent_tbl REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER parent_tbl_delete_trig + AFTER DELETE ON parent_tbl REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); + +INSERT INTO parent_tbl VALUES ('AAA', 42); + +COPY parent_tbl (a, b) FROM stdin; +AAA 42 +\. + +ALTER SERVER loopback OPTIONS (ADD batch_size '10'); + +INSERT INTO parent_tbl VALUES ('AAA', 42); + +COPY parent_tbl (a, b) FROM stdin; +AAA 42 +\. + +ALTER SERVER loopback OPTIONS (DROP batch_size); + +EXPLAIN (VERBOSE, COSTS OFF) +UPDATE parent_tbl SET b = b + 1; +UPDATE parent_tbl SET b = b + 1; + +EXPLAIN (VERBOSE, COSTS OFF) +DELETE FROM parent_tbl; +DELETE FROM parent_tbl; + +ALTER TABLE parent_tbl DETACH PARTITION foreign_tbl; +DROP TABLE parent_tbl; + +-- Test case for non-partition hierarchy +CREATE TABLE parent_tbl (a text, b int); +ALTER FOREIGN TABLE foreign_tbl INHERIT parent_tbl; + +CREATE TRIGGER parent_tbl_update_trig + AFTER UPDATE ON parent_tbl REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); +CREATE TRIGGER parent_tbl_delete_trig + AFTER DELETE ON parent_tbl REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE trigger_func(); + +EXPLAIN (VERBOSE, COSTS OFF) +UPDATE parent_tbl SET b = b + 1; +UPDATE parent_tbl SET b = b + 1; + +EXPLAIN (VERBOSE, COSTS OFF) +DELETE FROM parent_tbl; +DELETE FROM parent_tbl; + +ALTER FOREIGN TABLE foreign_tbl NO INHERIT parent_tbl; +DROP TABLE parent_tbl; + +-- Cleanup +DROP FOREIGN TABLE foreign_tbl; +DROP TABLE local_tbl; + -- =================================================================== -- test inheritance features -- =================================================================== @@ -3288,14 +3419,8 @@ SET ROLE regress_nosuper; SHOW is_superuser; -- This will be OK, we can create the FDW -DO $d$ - BEGIN - EXECUTE $$CREATE SERVER loopback_nopw FOREIGN DATA WRAPPER postgres_fdw - OPTIONS (dbname '$$||current_database()||$$', - port '$$||current_setting('port')||$$' - )$$; - END; -$d$; +CREATE SERVER loopback_nopw FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname :'current_database', port :'current_port'); -- But creation of user mappings for non-superusers should fail CREATE USER MAPPING FOR public SERVER loopback_nopw; @@ -3872,6 +3997,9 @@ INSERT INTO result_tbl SELECT * FROM async_pt WHERE b === 505; SELECT * FROM result_tbl ORDER BY a; DELETE FROM result_tbl; +-- Test COPY TO when foreign table is partition +COPY async_pt TO stdout; --error + DROP FOREIGN TABLE async_p3; DROP TABLE base_tbl3; @@ -4200,6 +4328,86 @@ SELECT count(*) FROM remote_application_name DROP FOREIGN TABLE remote_application_name; DROP VIEW my_application_name; +-- =================================================================== +-- test read-only and/or deferrable transactions +-- =================================================================== +CREATE TABLE loct (f1 int, f2 text); +CREATE FUNCTION locf() RETURNS SETOF loct LANGUAGE SQL AS + 'UPDATE public.loct SET f2 = f2 || f2 RETURNING *'; +CREATE VIEW locv AS SELECT t.* FROM locf() t; +CREATE FOREIGN TABLE remt (f1 int, f2 text) + SERVER loopback OPTIONS (table_name 'locv'); +CREATE FOREIGN TABLE remt2 (f1 int, f2 text) + SERVER loopback2 OPTIONS (table_name 'locv'); +INSERT INTO loct VALUES (1, 'foo'), (2, 'bar'); + +START TRANSACTION READ ONLY; +SAVEPOINT s; +SELECT * FROM remt; -- should fail +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SELECT * FROM remt; -- should fail +ROLLBACK; + +START TRANSACTION; +SAVEPOINT s; +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ROLLBACK; + +START TRANSACTION; +SAVEPOINT s; +SELECT * FROM remt; -- should work +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SELECT * FROM remt; -- should work +SET transaction_read_only = on; +SELECT * FROM remt; -- should fail +ROLLBACK; + +-- Exercise abort code paths in pgfdw_xact_callback/pgfdw_subxact_callback +-- in situations where multiple connections are involved +START TRANSACTION; +SAVEPOINT s; +SELECT * FROM remt; -- should work +SET transaction_read_only = on; +SELECT * FROM remt2; -- should fail +ROLLBACK TO s; +RELEASE SAVEPOINT s; +SELECT * FROM remt; -- should work +SET transaction_read_only = on; +SELECT * FROM remt2; -- should fail +ROLLBACK; + +DROP FOREIGN TABLE remt; +CREATE FOREIGN TABLE remt (f1 int, f2 text) + SERVER loopback OPTIONS (table_name 'loct'); + +START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY; +SELECT * FROM remt; +COMMIT; + +START TRANSACTION ISOLATION LEVEL SERIALIZABLE DEFERRABLE; +SELECT * FROM remt; +COMMIT; + +START TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE; +SELECT * FROM remt; +COMMIT; + +-- Clean up +DROP FOREIGN TABLE remt; +DROP FOREIGN TABLE remt2; +DROP VIEW locv; +DROP FUNCTION locf(); +DROP TABLE loct; + -- =================================================================== -- test parallel commit and parallel abort -- =================================================================== @@ -4278,7 +4486,7 @@ ALTER SERVER loopback2 OPTIONS (DROP parallel_abort); CREATE TABLE analyze_table (id int, a text, b bigint); CREATE FOREIGN TABLE analyze_ftable (id int, a text, b bigint) - SERVER loopback OPTIONS (table_name 'analyze_rtable1'); + SERVER loopback OPTIONS (table_name 'analyze_table'); INSERT INTO analyze_table (SELECT x FROM generate_series(1,1000) x); ANALYZE analyze_table; @@ -4289,24 +4497,88 @@ ANALYZE analyze_table; ALTER SERVER loopback OPTIONS (analyze_sampling 'invalid'); ALTER SERVER loopback OPTIONS (analyze_sampling 'auto'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'system'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'bernoulli'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'random'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; ALTER SERVER loopback OPTIONS (SET analyze_sampling 'off'); -ANALYZE analyze_table; +ANALYZE analyze_ftable; -- cleanup DROP FOREIGN TABLE analyze_ftable; DROP TABLE analyze_table; +-- =================================================================== +-- test for statistics import +-- =================================================================== + +CREATE TABLE simport_table (c1 int, c2 text); +CREATE FOREIGN TABLE simport_ftable (c1 int, c2 text, cx int) + SERVER loopback OPTIONS (table_name 'simport_table'); +ALTER FOREIGN TABLE simport_ftable ALTER COLUMN cx OPTIONS (ADD column_name 'c1'); +ALTER FOREIGN TABLE simport_ftable OPTIONS (ADD restore_stats 'true'); + +ANALYZE simport_ftable; -- should fail + +ANALYZE simport_table; + +ANALYZE VERBOSE simport_ftable; -- should work + +ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS 0; +ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS 0; +INSERT INTO simport_table VALUES (1, 'foo'), (1, 'foo'), (2, 'bar'), (2, 'bar'); +ANALYZE simport_table; + +ANALYZE simport_ftable; -- should fail + +ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS DEFAULT; +ANALYZE simport_table; + +ANALYZE simport_ftable; -- should fail + +ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS DEFAULT; +ANALYZE simport_table; + +ANALYZE VERBOSE simport_ftable; -- should work + +ANALYZE VERBOSE simport_ftable (c1); -- should work + +ANALYZE VERBOSE simport_ftable (c2); -- should work + +ANALYZE VERBOSE simport_ftable (c1, cx); -- should work + +ANALYZE VERBOSE simport_ftable (c2, cx); -- should work + +CREATE STATISTICS stats (dependencies) ON c1, c2 FROM simport_ftable; + +ANALYZE simport_ftable; -- should fail + +DROP STATISTICS stats; + +ANALYZE simport_ftable (cid); -- should fail + +ANALYZE simport_ftable (c1, c1); -- should fail + +CREATE VIEW simport_view AS SELECT * FROM simport_table; +CREATE FOREIGN TABLE simport_fview (c1 int, c2 text) + SERVER loopback OPTIONS (table_name 'simport_view'); +ALTER FOREIGN TABLE simport_fview OPTIONS (ADD restore_stats 'true'); + +ANALYZE simport_fview; -- should fail + +-- cleanup +DROP FOREIGN TABLE simport_ftable; +DROP FOREIGN TABLE simport_fview; +DROP VIEW simport_view; +DROP TABLE simport_table; + -- =================================================================== -- test for postgres_fdw_get_connections function with check_conn = true -- =================================================================== diff --git a/contrib/postgres_fdw/t/001_auth_scram.pl b/contrib/postgres_fdw/t/001_auth_scram.pl index b94a6a6293bad..6c18db4f2c86a 100644 --- a/contrib/postgres_fdw/t/001_auth_scram.pl +++ b/contrib/postgres_fdw/t/001_auth_scram.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Test SCRAM authentication when opening a new connection with a foreign # server. diff --git a/contrib/postgres_fdw/t/010_subscription.pl b/contrib/postgres_fdw/t/010_subscription.pl new file mode 100644 index 0000000000000..163c788d20966 --- /dev/null +++ b/contrib/postgres_fdw/t/010_subscription.pl @@ -0,0 +1,102 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +# Test postgres_fdw foreign server for use with a subscription. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +# Create subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Create some preexisting content on publisher +$node_publisher->safe_psql('postgres', + "CREATE TABLE tab_ins AS SELECT a, a + 1 as b FROM generate_series(1,1002) AS a" +); + +# Setup structure on subscriber +$node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw"); +$node_subscriber->safe_psql('postgres', + "CREATE TABLE tab_ins (a int, b int)"); + +# Setup logical replication +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION tap_pub FOR TABLE tab_ins"); + +my $publisher_host = $node_publisher->host; +my $publisher_port = $node_publisher->port; +$node_subscriber->safe_psql('postgres', + "CREATE SERVER tap_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$publisher_host', port '$publisher_port', dbname 'postgres')" +); + +$node_subscriber->safe_psql('postgres', + "CREATE USER MAPPING FOR PUBLIC SERVER tap_server"); + +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub SERVER tap_server PUBLICATION tap_pub WITH (password_required=false)" +); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +my $result = + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); +is($result, qq(1002), 'check that initial data was copied to subscriber'); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1003,1050) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); +is($result, qq(1050), 'check that inserted data was copied to subscriber'); + +# change to CONNECTION and confirm invalidation +my $log_offset = -s $node_subscriber->logfile; +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr'"); +$node_subscriber->wait_for_log( + qr/logical replication worker for subscription "tap_sub" will restart because of a parameter change/, + $log_offset); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1051,1057) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); +is($result, qq(1057), + 'check subscription after ALTER SUBSCRIPTION ... CONNECTION'); + +# change back to SERVER and confirm invalidation +$log_offset = -s $node_subscriber->logfile; +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub SERVER tap_server"); +$node_subscriber->wait_for_log( + qr/logical replication worker for subscription "tap_sub" will restart because of a parameter change/, + $log_offset); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1058,1073) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); +is($result, qq(1073), + 'check subscription after ALTER SUBSCRIPTION ... SERVER'); + +done_testing(); diff --git a/contrib/seg/expected/partition.out b/contrib/seg/expected/partition.out index 90d8397d5d461..8b86a406993c2 100644 --- a/contrib/seg/expected/partition.out +++ b/contrib/seg/expected/partition.out @@ -35,8 +35,9 @@ Indexes: "pti1" btree ((mydouble(category) + 1)) "pti2" btree (sdata) "pti3" btree (tdata COLLATE mycollation) -Partitions: pt12 FOR VALUES IN (1, 2), - pt34 FOR VALUES IN (3, 4) +Partitions: + pt12 FOR VALUES IN (1, 2) + pt34 FOR VALUES IN (3, 4) \d+ pt12 Table "public.pt12" diff --git a/contrib/seg/meson.build b/contrib/seg/meson.build index e331e0972306a..fa3de266bb5d0 100644 --- a/contrib/seg/meson.build +++ b/contrib/seg/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group seg_sources = files( 'seg.c', diff --git a/contrib/seg/seg-validate.pl b/contrib/seg/seg-validate.pl index bf8e47f8c1f2c..1ea215efb4946 100755 --- a/contrib/seg/seg-validate.pl +++ b/contrib/seg/seg-validate.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/seg/seg.c b/contrib/seg/seg.c index 151cbb954b9a1..972265b1bac22 100644 --- a/contrib/seg/seg.c +++ b/contrib/seg/seg.c @@ -107,7 +107,7 @@ Datum seg_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); - SEG *result = palloc(sizeof(SEG)); + SEG *result = palloc_object(SEG); yyscan_t scanner; seg_scanner_init(str, &scanner); @@ -202,8 +202,9 @@ gseg_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Datum query = PG_GETARG_DATUM(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); /* All cases served by this function are exact */ @@ -370,7 +371,7 @@ gseg_picksplit(PG_FUNCTION_ARGS) /* * Emit segments to the left output page, and compute its bounding box. */ - seg_l = (SEG *) palloc(sizeof(SEG)); + seg_l = palloc_object(SEG); memcpy(seg_l, sort_items[0].data, sizeof(SEG)); *left++ = sort_items[0].index; v->spl_nleft++; @@ -388,7 +389,7 @@ gseg_picksplit(PG_FUNCTION_ARGS) /* * Likewise for the right page. */ - seg_r = (SEG *) palloc(sizeof(SEG)); + seg_r = palloc_object(SEG); memcpy(seg_r, sort_items[firstright].data, sizeof(SEG)); *right++ = sort_items[firstright].index; v->spl_nright++; @@ -417,7 +418,7 @@ gseg_same(PG_FUNCTION_ARGS) { bool *result = (bool *) PG_GETARG_POINTER(2); - if (DirectFunctionCall2(seg_same, PG_GETARG_DATUM(0), PG_GETARG_DATUM(1))) + if (DatumGetBool(DirectFunctionCall2(seg_same, PG_GETARG_DATUM(0), PG_GETARG_DATUM(1)))) *result = true; else *result = false; @@ -470,7 +471,7 @@ gseg_leaf_consistent(Datum key, Datum query, StrategyNumber strategy) retval = DirectFunctionCall2(seg_contained, key, query); break; default: - retval = false; + retval = BoolGetDatum(false); } PG_RETURN_DATUM(retval); @@ -632,7 +633,7 @@ seg_union(PG_FUNCTION_ARGS) SEG *b = PG_GETARG_SEG_P(1); SEG *n; - n = (SEG *) palloc(sizeof(*n)); + n = palloc_object(SEG); /* take max of upper endpoints */ if (a->upper > b->upper) @@ -672,7 +673,7 @@ seg_inter(PG_FUNCTION_ARGS) SEG *b = PG_GETARG_SEG_P(1); SEG *n; - n = (SEG *) palloc(sizeof(*n)); + n = palloc_object(SEG); /* take min of upper endpoints */ if (a->upper < b->upper) diff --git a/contrib/seg/segdata.h b/contrib/seg/segdata.h index 4347c31c28e94..7bc7c83dca309 100644 --- a/contrib/seg/segdata.h +++ b/contrib/seg/segdata.h @@ -16,10 +16,7 @@ extern int significant_digits(const char *s); /* for segscan.l and segparse.y */ union YYSTYPE; -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif /* in segscan.l */ extern int seg_yylex(union YYSTYPE *yylval_param, yyscan_t yyscanner); diff --git a/contrib/seg/sort-segments.pl b/contrib/seg/sort-segments.pl index ec94402f302e2..92e7fc791c97d 100755 --- a/contrib/seg/sort-segments.pl +++ b/contrib/seg/sort-segments.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # this script will sort any table with the segment data type in its last column diff --git a/contrib/sepgsql/.gitignore b/contrib/sepgsql/.gitignore index b1778d05bbd0b..7e240e44c3692 100644 --- a/contrib/sepgsql/.gitignore +++ b/contrib/sepgsql/.gitignore @@ -3,5 +3,7 @@ /sepgsql-regtest.if /sepgsql-regtest.pp /tmp -# Generated by test suite +# Generated subdirectories +/log/ +/results/ /tmp_check/ diff --git a/contrib/sepgsql/database.c b/contrib/sepgsql/database.c index 6eeb429a28c08..451c061f5adb7 100644 --- a/contrib/sepgsql/database.c +++ b/contrib/sepgsql/database.c @@ -4,7 +4,7 @@ * * Routines corresponding to database objects * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ @@ -16,7 +16,6 @@ #include "access/table.h" #include "catalog/dependency.h" #include "catalog/pg_database.h" -#include "commands/dbcommands.h" #include "commands/seclabel.h" #include "sepgsql.h" #include "utils/builtins.h" diff --git a/contrib/sepgsql/dml.c b/contrib/sepgsql/dml.c index b437007d67197..b44d09e447fc4 100644 --- a/contrib/sepgsql/dml.c +++ b/contrib/sepgsql/dml.c @@ -4,7 +4,7 @@ * * Routines to handle DML permission checks * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ diff --git a/contrib/sepgsql/expected/ddl.out b/contrib/sepgsql/expected/ddl.out index 7e8deae4f9320..accb903f5cefc 100644 --- a/contrib/sepgsql/expected/ddl.out +++ b/contrib/sepgsql/expected/ddl.out @@ -304,6 +304,8 @@ ALTER TABLE regtest_table_4 ALTER COLUMN y TYPE float; LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="public" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 +LINE 1: ALTER TABLE regtest_table_4 ALTER COLUMN y TYPE float; + ^ LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_4.y" permissive=0 LOG: SELinux: allowed { execute } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_proc_exec_t:s0 tclass=db_procedure name="pg_catalog.float8(integer)" permissive=0 @@ -388,7 +390,11 @@ ALTER TABLE regtest_ptable_4 ALTER COLUMN y TYPE float; LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="public" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 +LINE 1: ALTER TABLE regtest_ptable_4 ALTER COLUMN y TYPE float; + ^ LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 +LINE 1: ALTER TABLE regtest_ptable_4 ALTER COLUMN y TYPE float; + ^ LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 LOG: SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_4.y" permissive=0 LOG: SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0 diff --git a/contrib/sepgsql/hooks.c b/contrib/sepgsql/hooks.c index 7aff15c6aec21..9bbc9e069c077 100644 --- a/contrib/sepgsql/hooks.c +++ b/contrib/sepgsql/hooks.c @@ -4,7 +4,7 @@ * * Entrypoints of the hooks in PostgreSQL, and dispatches the callbacks. * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ diff --git a/contrib/sepgsql/label.c b/contrib/sepgsql/label.c index 996ce174454dc..01c2074a0edfa 100644 --- a/contrib/sepgsql/label.c +++ b/contrib/sepgsql/label.c @@ -4,7 +4,7 @@ * * Routines to support SELinux labels (security context) * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ @@ -23,7 +23,6 @@ #include "catalog/pg_database.h" #include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" -#include "commands/dbcommands.h" #include "commands/seclabel.h" #include "libpq/auth.h" #include "libpq/libpq-be.h" @@ -146,7 +145,7 @@ sepgsql_set_client_label(const char *new_label) */ oldcxt = MemoryContextSwitchTo(CurTransactionContext); - plabel = palloc0(sizeof(pending_label)); + plabel = palloc0_object(pending_label); plabel->subid = GetCurrentSubTransactionId(); if (new_label) plabel->label = pstrdup(new_label); diff --git a/contrib/sepgsql/launcher b/contrib/sepgsql/launcher index 911e082fdeead..0e8294b4705d7 100755 --- a/contrib/sepgsql/launcher +++ b/contrib/sepgsql/launcher @@ -2,7 +2,7 @@ # # A wrapper script to launch psql command in regression test # -# Copyright (c) 2010-2025, PostgreSQL Global Development Group +# Copyright (c) 2010-2026, PostgreSQL Global Development Group # # ------------------------------------------------------------------------- diff --git a/contrib/sepgsql/meson.build b/contrib/sepgsql/meson.build index 6bf69783729a7..70f9d76863038 100644 --- a/contrib/sepgsql/meson.build +++ b/contrib/sepgsql/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not selinux.found() subdir_done() diff --git a/contrib/sepgsql/proc.c b/contrib/sepgsql/proc.c index 0d2723d44596d..2cf8039c704a2 100644 --- a/contrib/sepgsql/proc.c +++ b/contrib/sepgsql/proc.c @@ -4,7 +4,7 @@ * * Routines corresponding to procedure objects * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ diff --git a/contrib/sepgsql/relation.c b/contrib/sepgsql/relation.c index 528a529d4561a..5c61e1f4c8104 100644 --- a/contrib/sepgsql/relation.c +++ b/contrib/sepgsql/relation.c @@ -4,7 +4,7 @@ * * Routines corresponding to relation/attribute objects * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ diff --git a/contrib/sepgsql/schema.c b/contrib/sepgsql/schema.c index f85e544bef467..3f9fce3eadd47 100644 --- a/contrib/sepgsql/schema.c +++ b/contrib/sepgsql/schema.c @@ -4,7 +4,7 @@ * * Routines corresponding to schema objects * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ diff --git a/contrib/sepgsql/selinux.c b/contrib/sepgsql/selinux.c index fcdcf8072d48c..d9313737a08b8 100644 --- a/contrib/sepgsql/selinux.c +++ b/contrib/sepgsql/selinux.c @@ -5,7 +5,7 @@ * Interactions between userspace and selinux in kernelspace, * using libselinux api. * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ diff --git a/contrib/sepgsql/sepgsql.h b/contrib/sepgsql/sepgsql.h index 678d1ffab451e..b379585233b92 100644 --- a/contrib/sepgsql/sepgsql.h +++ b/contrib/sepgsql/sepgsql.h @@ -4,7 +4,7 @@ * * Definitions corresponding to SE-PostgreSQL * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ @@ -274,7 +274,7 @@ extern void sepgsql_object_relabel(const ObjectAddress *object, /* * dml.c */ -extern bool sepgsql_dml_privileges(List *rangeTabls, List *rteperminfos, +extern bool sepgsql_dml_privileges(List *rangeTbls, List *rteperminfos, bool abort_on_violation); /* diff --git a/contrib/sepgsql/t/001_sepgsql.pl b/contrib/sepgsql/t/001_sepgsql.pl index f5e4645e4e6d0..2ef7192f5f3a0 100644 --- a/contrib/sepgsql/t/001_sepgsql.pl +++ b/contrib/sepgsql/t/001_sepgsql.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/sepgsql/uavc.c b/contrib/sepgsql/uavc.c index 65ea8e7946a6e..1c201666f9961 100644 --- a/contrib/sepgsql/uavc.c +++ b/contrib/sepgsql/uavc.c @@ -6,7 +6,7 @@ * access control decisions recently used, and reduce number of kernel * invocations to avoid unnecessary performance hit. * - * Copyright (c) 2011-2025, PostgreSQL Global Development Group + * Copyright (c) 2011-2026, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ @@ -66,8 +66,8 @@ static char *avc_unlabeled; /* system 'unlabeled' label */ static uint32 sepgsql_avc_hash(const char *scontext, const char *tcontext, uint16 tclass) { - return hash_any((const unsigned char *) scontext, strlen(scontext)) - ^ hash_any((const unsigned char *) tcontext, strlen(tcontext)) + return hash_bytes((const unsigned char *) scontext, strlen(scontext)) + ^ hash_bytes((const unsigned char *) tcontext, strlen(tcontext)) ^ tclass; } @@ -257,7 +257,7 @@ sepgsql_avc_compute(const char *scontext, const char *tcontext, uint16 tclass) */ oldctx = MemoryContextSwitchTo(avc_mem_cxt); - cache = palloc0(sizeof(avc_cache)); + cache = palloc0_object(avc_cache); cache->hash = hash; cache->scontext = pstrdup(scontext); diff --git a/contrib/spi/meson.build b/contrib/spi/meson.build index 3832a91019a43..4a9a2bef0a53d 100644 --- a/contrib/spi/meson.build +++ b/contrib/spi/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group autoinc_sources = files( 'autoinc.c', diff --git a/contrib/spi/refint.c b/contrib/spi/refint.c index d5e25e07ae9e2..fbbd558ca1eb3 100644 --- a/contrib/spi/refint.c +++ b/contrib/spi/refint.c @@ -321,7 +321,7 @@ check_foreign_key(PG_FUNCTION_ARGS) if (nrefs < 1) /* internal error */ elog(ERROR, "check_foreign_key: %d (< 1) number of references specified", nrefs); - action = tolower((unsigned char) *(args[1])); + action = pg_ascii_tolower((unsigned char) *(args[1])); if (action != 'r' && action != 'c' && action != 's') /* internal error */ elog(ERROR, "check_foreign_key: invalid action %s", args[1]); @@ -651,7 +651,7 @@ find_plan(char *ident, EPlan **eplan, int *nplans) } else { - newp = *eplan = (EPlan *) palloc(sizeof(EPlan)); + newp = *eplan = palloc_object(EPlan); (*nplans) = i = 0; } diff --git a/contrib/sslinfo/meson.build b/contrib/sslinfo/meson.build index 4c513759200e0..6e9cb96430a02 100644 --- a/contrib/sslinfo/meson.build +++ b/contrib/sslinfo/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not ssl.found() subdir_done() diff --git a/contrib/sslinfo/sslinfo.c b/contrib/sslinfo/sslinfo.c index da70201119317..2b9eb90b09389 100644 --- a/contrib/sslinfo/sslinfo.c +++ b/contrib/sslinfo/sslinfo.c @@ -374,7 +374,7 @@ ssl_extension_info(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* Create a user function context for cross-call persistence */ - fctx = (SSLExtensionInfoContext *) palloc(sizeof(SSLExtensionInfoContext)); + fctx = palloc_object(SSLExtensionInfoContext); /* Construct tuple descriptor */ if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) diff --git a/contrib/tablefunc/meson.build b/contrib/tablefunc/meson.build index ee67272ec0ac3..1bebb92622992 100644 --- a/contrib/tablefunc/meson.build +++ b/contrib/tablefunc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tablefunc_sources = files( 'tablefunc.c', diff --git a/contrib/tablefunc/tablefunc.c b/contrib/tablefunc/tablefunc.c index 74afdc0977f47..31f70b7bc1009 100644 --- a/contrib/tablefunc/tablefunc.c +++ b/contrib/tablefunc/tablefunc.c @@ -10,7 +10,7 @@ * And contributors: * Nabil Sayegh * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * Permission to use, copy, modify, and distribute this software and its * documentation for any purpose, without fee, and without a written agreement @@ -43,6 +43,8 @@ #include "lib/stringinfo.h" #include "miscadmin.h" #include "utils/builtins.h" +#include "utils/hsearch.h" +#include "utils/tuplestore.h" PG_MODULE_MAGIC_EXT( .name = "tablefunc", @@ -207,7 +209,7 @@ normal_rand(PG_FUNCTION_ARGS) funcctx->max_calls = num_tuples; /* allocate memory for user context */ - fctx = (normal_rand_fctx *) palloc(sizeof(normal_rand_fctx)); + fctx = palloc_object(normal_rand_fctx); /* * Use fctx to keep track of upper and lower bounds from call to call. @@ -766,7 +768,7 @@ load_categories_hash(char *cats_sql, MemoryContext per_query_ctx) SPIcontext = MemoryContextSwitchTo(per_query_ctx); - catdesc = (crosstab_cat_desc *) palloc(sizeof(crosstab_cat_desc)); + catdesc = palloc_object(crosstab_cat_desc); catdesc->catname = catname; catdesc->attidx = i; diff --git a/contrib/tcn/meson.build b/contrib/tcn/meson.build index 6ffb136af904d..f7a9b9a505f1f 100644 --- a/contrib/tcn/meson.build +++ b/contrib/tcn/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tcn_sources = files( 'tcn.c', diff --git a/contrib/tcn/tcn.c b/contrib/tcn/tcn.c index 3158dee0f26a9..6b4bc7960d9e8 100644 --- a/contrib/tcn/tcn.c +++ b/contrib/tcn/tcn.c @@ -3,7 +3,7 @@ * tcn.c * triggered change notification support for PostgreSQL * - * Portions Copyright (c) 2011-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2011-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -66,12 +66,13 @@ triggered_change_notification(PG_FUNCTION_ARGS) TupleDesc tupdesc; char *channel; char operation; - StringInfo payload = makeStringInfo(); + StringInfoData payload; bool foundPK; List *indexoidlist; ListCell *indexoidscan; + initStringInfo(&payload); /* make sure it's called as a trigger */ if (!CALLED_AS_TRIGGER(fcinfo)) ereport(ERROR, @@ -149,22 +150,22 @@ triggered_change_notification(PG_FUNCTION_ARGS) foundPK = true; - strcpy_quoted(payload, RelationGetRelationName(rel), '"'); - appendStringInfoCharMacro(payload, ','); - appendStringInfoCharMacro(payload, operation); + strcpy_quoted(&payload, RelationGetRelationName(rel), '"'); + appendStringInfoCharMacro(&payload, ','); + appendStringInfoCharMacro(&payload, operation); for (i = 0; i < indnkeyatts; i++) { int colno = index->indkey.values[i]; Form_pg_attribute attr = TupleDescAttr(tupdesc, colno - 1); - appendStringInfoCharMacro(payload, ','); - strcpy_quoted(payload, NameStr(attr->attname), '"'); - appendStringInfoCharMacro(payload, '='); - strcpy_quoted(payload, SPI_getvalue(trigtuple, tupdesc, colno), '\''); + appendStringInfoCharMacro(&payload, ','); + strcpy_quoted(&payload, NameStr(attr->attname), '"'); + appendStringInfoCharMacro(&payload, '='); + strcpy_quoted(&payload, SPI_getvalue(trigtuple, tupdesc, colno), '\''); } - Async_Notify(channel, payload->data); + Async_Notify(channel, payload.data); } ReleaseSysCache(indexTuple); break; diff --git a/contrib/test_decoding/Makefile b/contrib/test_decoding/Makefile index 02e961f4d3144..acbcaed2febfb 100644 --- a/contrib/test_decoding/Makefile +++ b/contrib/test_decoding/Makefile @@ -9,7 +9,7 @@ REGRESS = ddl xact rewrite toast permissions decoding_in_xact \ ISOLATION = mxact delayed_startup ondisk_startup concurrent_ddl_dml \ oldest_xmin snapshot_transfer subxact_without_top concurrent_stream \ twophase_snapshot slot_creation_error catalog_change_snapshot \ - skip_snapshot_restore invalidation_distribution + skip_snapshot_restore invalidation_distribution parallel_session_origin REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/test_decoding/logical.conf ISOLATION_OPTS = --temp-config $(top_srcdir)/contrib/test_decoding/logical.conf diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out index bcd1f74b2bc53..6819812e806f1 100644 --- a/contrib/test_decoding/expected/ddl.out +++ b/contrib/test_decoding/expected/ddl.out @@ -192,6 +192,58 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc COMMIT (33 rows) +-- FOR PORTION OF setup +CREATE TABLE replication_example_temporal(id int4range, valid_at daterange, somedata int, text varchar(120), PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)); +INSERT INTO replication_example_temporal VALUES ('[1,2)', '[2000-01-01,2020-01-01)', 1, 'aaa'); +INSERT INTO replication_example_temporal VALUES ('[2,3)', '[2000-01-01,2020-01-01)', 1, 'aaa'); +/* display results */ +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + data +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + BEGIN + table public.replication_example_temporal: INSERT: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2000,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa' + COMMIT + BEGIN + table public.replication_example_temporal: INSERT: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2000,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa' + COMMIT +(6 rows) + +-- UPDATE FOR PORTION OF support +BEGIN; + UPDATE replication_example_temporal + FOR PORTION OF valid_at FROM '2010-01-01' TO '2011-01-01' + SET somedata = 2, + text = 'bbb' + WHERE id = '[1,2)'; +COMMIT; +/* display results */ +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + data +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + BEGIN + table public.replication_example_temporal: UPDATE: old-key: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2000,01-01-2020)' new-tuple: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2010,01-01-2011)' somedata[integer]:2 text[character varying]:'bbb' + table public.replication_example_temporal: INSERT: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2000,01-01-2010)' somedata[integer]:1 text[character varying]:'aaa' + table public.replication_example_temporal: INSERT: id[int4range]:'[1,2)' valid_at[daterange]:'[01-01-2011,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa' + COMMIT +(5 rows) + +-- DELETE FOR PORTION OF support +BEGIN; + DELETE FROM replication_example_temporal + FOR PORTION OF valid_at FROM '2012-01-01' TO '2013-01-01' + WHERE id = '[2,3)'; +COMMIT; +/* display results */ +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + data +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + BEGIN + table public.replication_example_temporal: DELETE: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2000,01-01-2020)' + table public.replication_example_temporal: INSERT: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2000,01-01-2012)' somedata[integer]:1 text[character varying]:'aaa' + table public.replication_example_temporal: INSERT: id[int4range]:'[2,3)' valid_at[daterange]:'[01-01-2013,01-01-2020)' somedata[integer]:1 text[character varying]:'aaa' + COMMIT +(5 rows) + -- MERGE support BEGIN; MERGE INTO replication_example t diff --git a/contrib/test_decoding/expected/invalidation_distribution.out b/contrib/test_decoding/expected/invalidation_distribution.out index ad0a944cbf303..ae53b1e61de3e 100644 --- a/contrib/test_decoding/expected/invalidation_distribution.out +++ b/contrib/test_decoding/expected/invalidation_distribution.out @@ -1,4 +1,4 @@ -Parsed test spec with 2 sessions +Parsed test spec with 3 sessions starting permutation: s1_insert_tbl1 s1_begin s1_insert_tbl1 s2_alter_pub_add_tbl s1_commit s1_insert_tbl1 s2_get_binary_changes step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1); @@ -18,3 +18,24 @@ count stop (1 row) + +starting permutation: s1_begin s1_insert_tbl1 s3_begin s3_insert_tbl1 s2_alter_pub_add_tbl s1_insert_tbl1 s1_commit s3_commit s2_get_binary_changes +step s1_begin: BEGIN; +step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1); +step s3_begin: BEGIN; +step s3_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (2, 2); +step s2_alter_pub_add_tbl: ALTER PUBLICATION pub ADD TABLE tbl1; +step s1_insert_tbl1: INSERT INTO tbl1 (val1, val2) VALUES (1, 1); +step s1_commit: COMMIT; +step s3_commit: COMMIT; +step s2_get_binary_changes: SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73; +count +----- + 1 +(1 row) + +?column? +-------- +stop +(1 row) + diff --git a/contrib/test_decoding/expected/parallel_session_origin.out b/contrib/test_decoding/expected/parallel_session_origin.out new file mode 100644 index 0000000000000..8e41831fcbc34 --- /dev/null +++ b/contrib/test_decoding/expected/parallel_session_origin.out @@ -0,0 +1,123 @@ +Parsed test spec with 2 sessions + +starting permutation: s0_setup s0_is_setup s1_setup s1_is_setup s0_add_message s0_store_lsn s1_add_message s1_store_lsn s0_compare s1_reset s0_reset +step s0_setup: SELECT pg_replication_origin_session_setup('origin'); +pg_replication_origin_session_setup +----------------------------------- + +(1 row) + +step s0_is_setup: SELECT pg_replication_origin_session_is_setup(); +pg_replication_origin_session_is_setup +-------------------------------------- +t +(1 row) + +step s1_setup: + SELECT pg_replication_origin_session_setup('origin', pid) + FROM pg_stat_activity + WHERE application_name = 'isolation/parallel_session_origin/s0'; + +pg_replication_origin_session_setup +----------------------------------- + +(1 row) + +step s1_is_setup: SELECT pg_replication_origin_session_is_setup(); +pg_replication_origin_session_is_setup +-------------------------------------- +t +(1 row) + +step s0_add_message: + SELECT 1 + FROM pg_logical_emit_message(true, 'prefix', 'message on s0'); + +?column? +-------- + 1 +(1 row) + +step s0_store_lsn: + INSERT INTO local_lsn_store + SELECT 0, local_lsn FROM pg_replication_origin_status; + +step s1_add_message: + SELECT 1 + FROM pg_logical_emit_message(true, 'prefix', 'message on s1'); + +?column? +-------- + 1 +(1 row) + +step s1_store_lsn: + INSERT INTO local_lsn_store + SELECT 1, local_lsn FROM pg_replication_origin_status; + +step s0_compare: + SELECT s0.lsn < s1.lsn + FROM local_lsn_store as s0, local_lsn_store as s1 + WHERE s0.session = 0 AND s1.session = 1; + +?column? +-------- +t +(1 row) + +step s1_reset: SELECT pg_replication_origin_session_reset(); +pg_replication_origin_session_reset +----------------------------------- + +(1 row) + +step s0_reset: SELECT pg_replication_origin_session_reset(); +pg_replication_origin_session_reset +----------------------------------- + +(1 row) + + +starting permutation: s0_setup s0_is_setup s1_setup s1_is_setup s0_reset s1_reset s0_reset +step s0_setup: SELECT pg_replication_origin_session_setup('origin'); +pg_replication_origin_session_setup +----------------------------------- + +(1 row) + +step s0_is_setup: SELECT pg_replication_origin_session_is_setup(); +pg_replication_origin_session_is_setup +-------------------------------------- +t +(1 row) + +step s1_setup: + SELECT pg_replication_origin_session_setup('origin', pid) + FROM pg_stat_activity + WHERE application_name = 'isolation/parallel_session_origin/s0'; + +pg_replication_origin_session_setup +----------------------------------- + +(1 row) + +step s1_is_setup: SELECT pg_replication_origin_session_is_setup(); +pg_replication_origin_session_is_setup +-------------------------------------- +t +(1 row) + +step s0_reset: SELECT pg_replication_origin_session_reset(); +ERROR: cannot reset replication origin with ID 1 because it is still in use by other processes +step s1_reset: SELECT pg_replication_origin_session_reset(); +pg_replication_origin_session_reset +----------------------------------- + +(1 row) + +step s0_reset: SELECT pg_replication_origin_session_reset(); +pg_replication_origin_session_reset +----------------------------------- + +(1 row) + diff --git a/contrib/test_decoding/expected/replorigin.out b/contrib/test_decoding/expected/replorigin.out index c85e1a01b231c..29a9630c9006b 100644 --- a/contrib/test_decoding/expected/replorigin.out +++ b/contrib/test_decoding/expected/replorigin.out @@ -41,6 +41,9 @@ SELECT pg_replication_origin_create('regress_test_decoding: regression_slot'); SELECT pg_replication_origin_create('regress_test_decoding: regression_slot'); ERROR: duplicate key value violates unique constraint "pg_replication_origin_roname_index" DETAIL: Key (roname)=(regress_test_decoding: regression_slot) already exists. +-- ensure inactive origin cannot be set as session one if pid is specified +SELECT pg_replication_origin_session_setup('regress_test_decoding: regression_slot', -1); +ERROR: cannot use PID -1 for inactive replication origin with ID 1 --ensure deletions work (once) SELECT pg_replication_origin_create('regress_test_decoding: temp'); pg_replication_origin_create diff --git a/contrib/test_decoding/expected/stats.out b/contrib/test_decoding/expected/stats.out index de6dc416130a0..a9ead3c41aa31 100644 --- a/contrib/test_decoding/expected/stats.out +++ b/contrib/test_decoding/expected/stats.out @@ -37,12 +37,12 @@ SELECT pg_stat_force_next_flush(); (1 row) -SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name; - slot_name | spill_txns | spill_count | total_txns | total_bytes -------------------------+------------+-------------+------------+------------- - regression_slot_stats1 | t | t | t | t - regression_slot_stats2 | t | t | t | t - regression_slot_stats3 | t | t | t | t +SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes, mem_exceeded_count = 0 AS mem_exceeded_count FROM pg_stat_replication_slots ORDER BY slot_name; + slot_name | spill_txns | spill_count | total_txns | total_bytes | mem_exceeded_count +------------------------+------------+-------------+------------+-------------+-------------------- + regression_slot_stats1 | t | t | t | t | t + regression_slot_stats2 | t | t | t | t | t + regression_slot_stats3 | t | t | t | t | t (3 rows) RESET logical_decoding_work_mem; @@ -53,12 +53,12 @@ SELECT pg_stat_reset_replication_slot('regression_slot_stats1'); (1 row) -SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name; - slot_name | spill_txns | spill_count | total_txns | total_bytes -------------------------+------------+-------------+------------+------------- - regression_slot_stats1 | t | t | f | f - regression_slot_stats2 | t | t | t | t - regression_slot_stats3 | t | t | t | t +SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes, mem_exceeded_count = 0 AS mem_exceeded_count FROM pg_stat_replication_slots ORDER BY slot_name; + slot_name | spill_txns | spill_count | total_txns | total_bytes | mem_exceeded_count +------------------------+------------+-------------+------------+-------------+-------------------- + regression_slot_stats1 | t | t | f | f | t + regression_slot_stats2 | t | t | t | t | t + regression_slot_stats3 | t | t | t | t | t (3 rows) -- reset stats for all slots @@ -68,27 +68,27 @@ SELECT pg_stat_reset_replication_slot(NULL); (1 row) -SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name; - slot_name | spill_txns | spill_count | total_txns | total_bytes -------------------------+------------+-------------+------------+------------- - regression_slot_stats1 | t | t | f | f - regression_slot_stats2 | t | t | f | f - regression_slot_stats3 | t | t | f | f +SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes, mem_exceeded_count = 0 AS mem_exceeded_count FROM pg_stat_replication_slots ORDER BY slot_name; + slot_name | spill_txns | spill_count | total_txns | total_bytes | mem_exceeded_count +------------------------+------------+-------------+------------+-------------+-------------------- + regression_slot_stats1 | t | t | f | f | t + regression_slot_stats2 | t | t | f | f | t + regression_slot_stats3 | t | t | f | f | t (3 rows) -- verify accessing/resetting stats for non-existent slot does something reasonable SELECT * FROM pg_stat_get_replication_slot('do-not-exist'); - slot_name | spill_txns | spill_count | spill_bytes | stream_txns | stream_count | stream_bytes | total_txns | total_bytes | stats_reset ---------------+------------+-------------+-------------+-------------+--------------+--------------+------------+-------------+------------- - do-not-exist | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + slot_name | spill_txns | spill_count | spill_bytes | stream_txns | stream_count | stream_bytes | mem_exceeded_count | total_txns | total_bytes | slotsync_skip_count | slotsync_last_skip | stats_reset +--------------+------------+-------------+-------------+-------------+--------------+--------------+--------------------+------------+-------------+---------------------+--------------------+------------- + do-not-exist | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | (1 row) SELECT pg_stat_reset_replication_slot('do-not-exist'); ERROR: replication slot "do-not-exist" does not exist SELECT * FROM pg_stat_get_replication_slot('do-not-exist'); - slot_name | spill_txns | spill_count | spill_bytes | stream_txns | stream_count | stream_bytes | total_txns | total_bytes | stats_reset ---------------+------------+-------------+-------------+-------------+--------------+--------------+------------+-------------+------------- - do-not-exist | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + slot_name | spill_txns | spill_count | spill_bytes | stream_txns | stream_count | stream_bytes | mem_exceeded_count | total_txns | total_bytes | slotsync_skip_count | slotsync_last_skip | stats_reset +--------------+------------+-------------+-------------+-------------+--------------+--------------+--------------------+------------+-------------+---------------------+--------------------+------------- + do-not-exist | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | | (1 row) -- spilling the xact @@ -110,12 +110,12 @@ SELECT pg_stat_force_next_flush(); (1 row) -SELECT slot_name, spill_txns > 0 AS spill_txns, spill_count > 0 AS spill_count FROM pg_stat_replication_slots; - slot_name | spill_txns | spill_count -------------------------+------------+------------- - regression_slot_stats1 | t | t - regression_slot_stats2 | f | f - regression_slot_stats3 | f | f +SELECT slot_name, spill_txns > 0 AS spill_txns, spill_count > 0 AS spill_count, mem_exceeded_count > 0 AS mem_exceeded_count FROM pg_stat_replication_slots; + slot_name | spill_txns | spill_count | mem_exceeded_count +------------------------+------------+-------------+-------------------- + regression_slot_stats1 | t | t | t + regression_slot_stats2 | f | f | f + regression_slot_stats3 | f | f | f (3 rows) -- Ensure stats can be repeatedly accessed using the same stats snapshot. See @@ -159,16 +159,19 @@ SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot_stats4_twophas (1 row) -- Verify that the decoding doesn't spill already-aborted transaction's changes. +-- Given that there is no concurrent activities that are capturable by logical decoding, +-- mem_exceeded_count should theoretically be 1 but we check if >0 here since it's +-- more flexible for potential future changes and adequate for the testing purpose. SELECT pg_stat_force_next_flush(); pg_stat_force_next_flush -------------------------- (1 row) -SELECT slot_name, spill_txns, spill_count FROM pg_stat_replication_slots WHERE slot_name = 'regression_slot_stats4_twophase'; - slot_name | spill_txns | spill_count ----------------------------------+------------+------------- - regression_slot_stats4_twophase | 0 | 0 +SELECT slot_name, spill_txns, spill_count, mem_exceeded_count > 0 as mem_exceeded_count FROM pg_stat_replication_slots WHERE slot_name = 'regression_slot_stats4_twophase'; + slot_name | spill_txns | spill_count | mem_exceeded_count +---------------------------------+------------+-------------+-------------------- + regression_slot_stats4_twophase | 0 | 0 | t (1 row) DROP TABLE stats_test; diff --git a/contrib/test_decoding/meson.build b/contrib/test_decoding/meson.build index 25f6b8a90826b..cf5b74cf1abde 100644 --- a/contrib/test_decoding/meson.build +++ b/contrib/test_decoding/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_decoding_sources = files( 'test_decoding.c', @@ -64,6 +64,7 @@ tests += { 'slot_creation_error', 'skip_snapshot_restore', 'invalidation_distribution', + 'parallel_session_origin', ], 'regress_args': [ '--temp-config', files('logical.conf'), diff --git a/contrib/test_decoding/specs/invalidation_distribution.spec b/contrib/test_decoding/specs/invalidation_distribution.spec index decbed627e327..67d41969ac1d6 100644 --- a/contrib/test_decoding/specs/invalidation_distribution.spec +++ b/contrib/test_decoding/specs/invalidation_distribution.spec @@ -28,5 +28,16 @@ setup { SET synchronous_commit=on; } step "s2_alter_pub_add_tbl" { ALTER PUBLICATION pub ADD TABLE tbl1; } step "s2_get_binary_changes" { SELECT count(data) FROM pg_logical_slot_get_binary_changes('isolation_slot', NULL, NULL, 'proto_version', '4', 'publication_names', 'pub') WHERE get_byte(data, 0) = 73; } +session "s3" +setup { SET synchronous_commit=on; } +step "s3_begin" { BEGIN; } +step "s3_insert_tbl1" { INSERT INTO tbl1 (val1, val2) VALUES (2, 2); } +step "s3_commit" { COMMIT; } + # Expect to get one insert change. LOGICAL_REP_MSG_INSERT = 'I' permutation "s1_insert_tbl1" "s1_begin" "s1_insert_tbl1" "s2_alter_pub_add_tbl" "s1_commit" "s1_insert_tbl1" "s2_get_binary_changes" + +# Expect to get one insert change with LOGICAL_REP_MSG_INSERT = 'I' from +# the second "s1_insert_tbl1" executed after adding the table tbl1 to the +# publication in "s2_alter_pub_add_tbl". +permutation "s1_begin" "s1_insert_tbl1" "s3_begin" "s3_insert_tbl1" "s2_alter_pub_add_tbl" "s1_insert_tbl1" "s1_commit" "s3_commit" "s2_get_binary_changes" diff --git a/contrib/test_decoding/specs/parallel_session_origin.spec b/contrib/test_decoding/specs/parallel_session_origin.spec new file mode 100644 index 0000000000000..2253a7a14eba4 --- /dev/null +++ b/contrib/test_decoding/specs/parallel_session_origin.spec @@ -0,0 +1,60 @@ +# Test parallel replication origin manipulations; ensure local_lsn can be +# updated by all attached sessions. + +setup +{ + SELECT pg_replication_origin_create('origin'); + CREATE UNLOGGED TABLE local_lsn_store (session int, lsn pg_lsn); +} + +teardown +{ + SELECT pg_replication_origin_drop('origin'); + DROP TABLE local_lsn_store; +} + +session "s0" +setup { SET synchronous_commit = on; } +step "s0_setup" { SELECT pg_replication_origin_session_setup('origin'); } +step "s0_is_setup" { SELECT pg_replication_origin_session_is_setup(); } +step "s0_add_message" { + SELECT 1 + FROM pg_logical_emit_message(true, 'prefix', 'message on s0'); +} +step "s0_store_lsn" { + INSERT INTO local_lsn_store + SELECT 0, local_lsn FROM pg_replication_origin_status; +} +step "s0_compare" { + SELECT s0.lsn < s1.lsn + FROM local_lsn_store as s0, local_lsn_store as s1 + WHERE s0.session = 0 AND s1.session = 1; +} +step "s0_reset" { SELECT pg_replication_origin_session_reset(); } + +session "s1" +setup { SET synchronous_commit = on; } +step "s1_setup" { + SELECT pg_replication_origin_session_setup('origin', pid) + FROM pg_stat_activity + WHERE application_name = 'isolation/parallel_session_origin/s0'; +} +step "s1_is_setup" { SELECT pg_replication_origin_session_is_setup(); } +step "s1_add_message" { + SELECT 1 + FROM pg_logical_emit_message(true, 'prefix', 'message on s1'); +} +step "s1_store_lsn" { + INSERT INTO local_lsn_store + SELECT 1, local_lsn FROM pg_replication_origin_status; +} +step "s1_reset" { SELECT pg_replication_origin_session_reset(); } + +# Firstly s0 attaches to a origin and s1 attaches to the same. Both sessions +# commits a transaction and store the local_lsn of the replication origin. +# Compare LSNs and expect latter transaction (done by s1) has larger local_lsn. +permutation "s0_setup" "s0_is_setup" "s1_setup" "s1_is_setup" "s0_add_message" "s0_store_lsn" "s1_add_message" "s1_store_lsn" "s0_compare" "s1_reset" "s0_reset" + +# Test that the origin cannot be released if another session is actively using +# it. +permutation "s0_setup" "s0_is_setup" "s1_setup" "s1_is_setup" "s0_reset" "s1_reset" "s0_reset" diff --git a/contrib/test_decoding/sql/ddl.sql b/contrib/test_decoding/sql/ddl.sql index 2f8e4e7f2ccfa..6d0b7d777789e 100644 --- a/contrib/test_decoding/sql/ddl.sql +++ b/contrib/test_decoding/sql/ddl.sql @@ -93,6 +93,36 @@ COMMIT; /* display results */ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); +-- FOR PORTION OF setup +CREATE TABLE replication_example_temporal(id int4range, valid_at daterange, somedata int, text varchar(120), PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)); +INSERT INTO replication_example_temporal VALUES ('[1,2)', '[2000-01-01,2020-01-01)', 1, 'aaa'); +INSERT INTO replication_example_temporal VALUES ('[2,3)', '[2000-01-01,2020-01-01)', 1, 'aaa'); + +/* display results */ +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + +-- UPDATE FOR PORTION OF support +BEGIN; + UPDATE replication_example_temporal + FOR PORTION OF valid_at FROM '2010-01-01' TO '2011-01-01' + SET somedata = 2, + text = 'bbb' + WHERE id = '[1,2)'; +COMMIT; + +/* display results */ +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + +-- DELETE FOR PORTION OF support +BEGIN; + DELETE FROM replication_example_temporal + FOR PORTION OF valid_at FROM '2012-01-01' TO '2013-01-01' + WHERE id = '[2,3)'; +COMMIT; + +/* display results */ +SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); + -- MERGE support BEGIN; MERGE INTO replication_example t diff --git a/contrib/test_decoding/sql/replorigin.sql b/contrib/test_decoding/sql/replorigin.sql index e71ee02d050a0..17f2b888238ee 100644 --- a/contrib/test_decoding/sql/replorigin.sql +++ b/contrib/test_decoding/sql/replorigin.sql @@ -26,6 +26,9 @@ SELECT pg_replication_origin_create('regress_test_decoding: regression_slot'); -- ensure duplicate creations fail SELECT pg_replication_origin_create('regress_test_decoding: regression_slot'); +-- ensure inactive origin cannot be set as session one if pid is specified +SELECT pg_replication_origin_session_setup('regress_test_decoding: regression_slot', -1); + --ensure deletions work (once) SELECT pg_replication_origin_create('regress_test_decoding: temp'); SELECT pg_replication_origin_drop('regress_test_decoding: temp'); diff --git a/contrib/test_decoding/sql/stats.sql b/contrib/test_decoding/sql/stats.sql index a022fe1bf0750..6661dbcb85c3a 100644 --- a/contrib/test_decoding/sql/stats.sql +++ b/contrib/test_decoding/sql/stats.sql @@ -15,16 +15,16 @@ SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot_stats1', NULL, SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot_stats2', NULL, NULL, 'skip-empty-xacts', '1'); SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot_stats3', NULL, NULL, 'skip-empty-xacts', '1'); SELECT pg_stat_force_next_flush(); -SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name; +SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes, mem_exceeded_count = 0 AS mem_exceeded_count FROM pg_stat_replication_slots ORDER BY slot_name; RESET logical_decoding_work_mem; -- reset stats for one slot, others should be unaffected SELECT pg_stat_reset_replication_slot('regression_slot_stats1'); -SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name; +SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes, mem_exceeded_count = 0 AS mem_exceeded_count FROM pg_stat_replication_slots ORDER BY slot_name; -- reset stats for all slots SELECT pg_stat_reset_replication_slot(NULL); -SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes FROM pg_stat_replication_slots ORDER BY slot_name; +SELECT slot_name, spill_txns = 0 AS spill_txns, spill_count = 0 AS spill_count, total_txns > 0 AS total_txns, total_bytes > 0 AS total_bytes, mem_exceeded_count = 0 AS mem_exceeded_count FROM pg_stat_replication_slots ORDER BY slot_name; -- verify accessing/resetting stats for non-existent slot does something reasonable SELECT * FROM pg_stat_get_replication_slot('do-not-exist'); @@ -41,7 +41,7 @@ SELECT count(*) FROM pg_logical_slot_peek_changes('regression_slot_stats1', NULL -- background transaction (say by autovacuum) happens in parallel to the main -- transaction. SELECT pg_stat_force_next_flush(); -SELECT slot_name, spill_txns > 0 AS spill_txns, spill_count > 0 AS spill_count FROM pg_stat_replication_slots; +SELECT slot_name, spill_txns > 0 AS spill_txns, spill_count > 0 AS spill_count, mem_exceeded_count > 0 AS mem_exceeded_count FROM pg_stat_replication_slots; -- Ensure stats can be repeatedly accessed using the same stats snapshot. See -- https://postgr.es/m/20210317230447.c7uc4g3vbs4wi32i%40alap3.anarazel.de @@ -64,8 +64,11 @@ ROLLBACK PREPARED 'test1_abort'; SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot_stats4_twophase', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1'); -- Verify that the decoding doesn't spill already-aborted transaction's changes. +-- Given that there is no concurrent activities that are capturable by logical decoding, +-- mem_exceeded_count should theoretically be 1 but we check if >0 here since it's +-- more flexible for potential future changes and adequate for the testing purpose. SELECT pg_stat_force_next_flush(); -SELECT slot_name, spill_txns, spill_count FROM pg_stat_replication_slots WHERE slot_name = 'regression_slot_stats4_twophase'; +SELECT slot_name, spill_txns, spill_count, mem_exceeded_count > 0 as mem_exceeded_count FROM pg_stat_replication_slots WHERE slot_name = 'regression_slot_stats4_twophase'; DROP TABLE stats_test; SELECT pg_drop_replication_slot('regression_slot_stats1'), diff --git a/contrib/test_decoding/t/001_repl_stats.pl b/contrib/test_decoding/t/001_repl_stats.pl index 0de62edb7d84c..6814c792e2b6c 100644 --- a/contrib/test_decoding/t/001_repl_stats.pl +++ b/contrib/test_decoding/t/001_repl_stats.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test replication statistics data in pg_stat_replication_slots is sane after # drop replication slot and restart. diff --git a/contrib/test_decoding/test_decoding.c b/contrib/test_decoding/test_decoding.c index bb495563200c3..d5cf0fa02b058 100644 --- a/contrib/test_decoding/test_decoding.c +++ b/contrib/test_decoding/test_decoding.c @@ -3,7 +3,7 @@ * test_decoding.c * example logical decoding output plugin * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/test_decoding/test_decoding.c @@ -70,7 +70,7 @@ static void pg_decode_truncate(LogicalDecodingContext *ctx, int nrelations, Relation relations[], ReorderBufferChange *change); static bool pg_decode_filter(LogicalDecodingContext *ctx, - RepOriginId origin_id); + ReplOriginId origin_id); static void pg_decode_message(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, XLogRecPtr lsn, bool transactional, const char *prefix, @@ -163,7 +163,7 @@ pg_decode_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, TestDecodingData *data; bool enable_streaming = false; - data = palloc0(sizeof(TestDecodingData)); + data = palloc0_object(TestDecodingData); data->context = AllocSetContextCreate(ctx->context, "text conversion context", ALLOCSET_DEFAULT_SIZES); @@ -340,7 +340,7 @@ pg_decode_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (data->include_timestamp) appendStringInfo(ctx->out, " (at %s)", - timestamptz_to_str(txn->xact_time.commit_time)); + timestamptz_to_str(txn->commit_time)); OutputPluginWrite(ctx, true); } @@ -391,7 +391,7 @@ pg_decode_prepare_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (data->include_timestamp) appendStringInfo(ctx->out, " (at %s)", - timestamptz_to_str(txn->xact_time.prepare_time)); + timestamptz_to_str(txn->prepare_time)); OutputPluginWrite(ctx, true); } @@ -413,7 +413,7 @@ pg_decode_commit_prepared_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn if (data->include_timestamp) appendStringInfo(ctx->out, " (at %s)", - timestamptz_to_str(txn->xact_time.commit_time)); + timestamptz_to_str(txn->commit_time)); OutputPluginWrite(ctx, true); } @@ -437,7 +437,7 @@ pg_decode_rollback_prepared_txn(LogicalDecodingContext *ctx, if (data->include_timestamp) appendStringInfo(ctx->out, " (at %s)", - timestamptz_to_str(txn->xact_time.commit_time)); + timestamptz_to_str(txn->commit_time)); OutputPluginWrite(ctx, true); } @@ -461,11 +461,11 @@ pg_decode_filter_prepare(LogicalDecodingContext *ctx, TransactionId xid, static bool pg_decode_filter(LogicalDecodingContext *ctx, - RepOriginId origin_id) + ReplOriginId origin_id) { TestDecodingData *data = ctx->output_plugin_private; - if (data->only_local && origin_id != InvalidRepOriginId) + if (data->only_local && origin_id != InvalidReplOriginId) return true; return false; } @@ -474,8 +474,8 @@ pg_decode_filter(LogicalDecodingContext *ctx, * Print literal `outputstr' already represented as string of type `typid' * into stringbuf `s'. * - * Some builtin types aren't quoted, the rest is quoted. Escaping is done as - * if standard_conforming_strings were enabled. + * Some builtin types aren't quoted, the rest is quoted. Escaping is done + * per standard SQL rules. */ static void print_literal(StringInfo s, Oid typid, char *outputstr) @@ -581,7 +581,7 @@ tuple_to_stringinfo(StringInfo s, TupleDesc tupdesc, HeapTuple tuple, bool skip_ /* print data */ if (isnull) appendStringInfoString(s, "null"); - else if (typisvarlena && VARATT_IS_EXTERNAL_ONDISK(origval)) + else if (typisvarlena && VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(origval))) appendStringInfoString(s, "unchanged-toast-datum"); else if (!typisvarlena) print_literal(s, typid, @@ -874,7 +874,7 @@ pg_decode_stream_prepare(LogicalDecodingContext *ctx, if (data->include_timestamp) appendStringInfo(ctx->out, " (at %s)", - timestamptz_to_str(txn->xact_time.prepare_time)); + timestamptz_to_str(txn->prepare_time)); OutputPluginWrite(ctx, true); } @@ -903,7 +903,7 @@ pg_decode_stream_commit(LogicalDecodingContext *ctx, if (data->include_timestamp) appendStringInfo(ctx->out, " (at %s)", - timestamptz_to_str(txn->xact_time.commit_time)); + timestamptz_to_str(txn->commit_time)); OutputPluginWrite(ctx, true); } diff --git a/contrib/tsm_system_rows/meson.build b/contrib/tsm_system_rows/meson.build index b8cece3d80f59..19f6f5f6bf5da 100644 --- a/contrib/tsm_system_rows/meson.build +++ b/contrib/tsm_system_rows/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tsm_system_rows_sources = files( 'tsm_system_rows.c', diff --git a/contrib/tsm_system_rows/tsm_system_rows.c b/contrib/tsm_system_rows/tsm_system_rows.c index f401efa2131fc..a9a2483373001 100644 --- a/contrib/tsm_system_rows/tsm_system_rows.c +++ b/contrib/tsm_system_rows/tsm_system_rows.c @@ -17,7 +17,7 @@ * won't visit blocks added after the first scan, but that is fine since * such blocks shouldn't contain any visible tuples anyway. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -163,7 +163,7 @@ system_rows_samplescangetsamplesize(PlannerInfo *root, static void system_rows_initsamplescan(SampleScanState *node, int eflags) { - node->tsm_state = palloc0(sizeof(SystemRowsSamplerData)); + node->tsm_state = palloc0_object(SystemRowsSamplerData); /* Note the above leaves tsm_state->step equal to zero */ } diff --git a/contrib/tsm_system_time/meson.build b/contrib/tsm_system_time/meson.build index 8a143a8f8e6f4..c5404b266abf4 100644 --- a/contrib/tsm_system_time/meson.build +++ b/contrib/tsm_system_time/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tsm_system_time_sources = files( 'tsm_system_time.c', diff --git a/contrib/tsm_system_time/tsm_system_time.c b/contrib/tsm_system_time/tsm_system_time.c index c9c71d8c3af39..4814c31bc6fe0 100644 --- a/contrib/tsm_system_time/tsm_system_time.c +++ b/contrib/tsm_system_time/tsm_system_time.c @@ -13,7 +13,7 @@ * However, we do what we can to reduce surprising behavior by selecting * the sampling pattern just once per query, much as in tsm_system_rows. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -30,6 +30,7 @@ #include "catalog/pg_type.h" #include "miscadmin.h" #include "optimizer/optimizer.h" +#include "portability/instr_time.h" #include "utils/sampling.h" #include "utils/spccache.h" @@ -179,7 +180,7 @@ system_time_samplescangetsamplesize(PlannerInfo *root, static void system_time_initsamplescan(SampleScanState *node, int eflags) { - node->tsm_state = palloc0(sizeof(SystemTimeSamplerData)); + node->tsm_state = palloc0_object(SystemTimeSamplerData); /* Note the above leaves tsm_state->step equal to zero */ } diff --git a/contrib/unaccent/generate_unaccent_rules.py b/contrib/unaccent/generate_unaccent_rules.py index 40822d0c17616..827fa6766df9c 100644 --- a/contrib/unaccent/generate_unaccent_rules.py +++ b/contrib/unaccent/generate_unaccent_rules.py @@ -236,7 +236,7 @@ def main(args): charactersSet = set() # read file UnicodeData.txt - with codecs.open( + with open( args.unicodeDataFilePath, mode='r', encoding='UTF-8', ) as unicodeDataFile: # read everything we need into memory diff --git a/contrib/unaccent/meson.build b/contrib/unaccent/meson.build index 33d4649bae15d..938d9522da33c 100644 --- a/contrib/unaccent/meson.build +++ b/contrib/unaccent/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group unaccent_sources = files( 'unaccent.c', @@ -28,7 +28,6 @@ install_data( install_dir: dir_data / 'tsearch_data' ) -# XXX: Implement downlo tests += { 'name': 'unaccent', 'sd': meson.current_source_dir(), @@ -39,3 +38,43 @@ tests += { ], }, } + + +# Download CLDR files on demand. + +cldr_baseurl = 'https://raw.githubusercontent.com/unicode-org/cldr/release-@0@/common/transforms/@1@' + +if not wget.found() or not cp.found() + subdir_done() +endif + +foreach f : ['Latin-ASCII.xml'] + # XXX could use .replace when we require meson 0.58 + cldr_version_dashed = '-'.join(CLDR_VERSION.split('.')) + url = cldr_baseurl.format(cldr_version_dashed, f) + target = custom_target(f, + output: f, + command: [wget, wget_flags, url], + build_by_default: false, + ) + unicode_data += {f: target} +endforeach + +unaccent_update_unicode_targets = \ + custom_target('unaccent.rules', + input: [unicode_data['UnicodeData.txt'], unicode_data['Latin-ASCII.xml']], + output: ['unaccent.rules'], + command: [python, files('generate_unaccent_rules.py'), '--unicode-data-file', '@INPUT0@', '--latin-ascii-file', '@INPUT1@'], + build_by_default: false, + capture: true, + ) + +update_unicode_unaccent = custom_target('update-unicode', + output: ['dont-exist'], + input: unaccent_update_unicode_targets, + command: [cp, '@INPUT@', '@SOURCE_ROOT@/contrib/unaccent/'], + build_by_default: false, + build_always_stale: true, +) + +update_unicode_targets += update_unicode_unaccent diff --git a/contrib/unaccent/unaccent.c b/contrib/unaccent/unaccent.c index 336ba31047a4a..69b173e4498df 100644 --- a/contrib/unaccent/unaccent.c +++ b/contrib/unaccent/unaccent.c @@ -3,7 +3,7 @@ * unaccent.c * Text search unaccent dictionary * - * Copyright (c) 2009-2025, PostgreSQL Global Development Group + * Copyright (c) 2009-2026, PostgreSQL Global Development Group * * IDENTIFICATION * contrib/unaccent/unaccent.c @@ -60,7 +60,7 @@ placeChar(TrieChar *node, const unsigned char *str, int lenstr, TrieChar *curnode; if (!node) - node = (TrieChar *) palloc0(sizeof(TrieChar) * 256); + node = palloc0_array(TrieChar, 256); Assert(lenstr > 0); /* else str[0] doesn't exist */ @@ -156,7 +156,7 @@ initTrie(const char *filename) state = 0; for (ptr = line; *ptr; ptr += ptrlen) { - ptrlen = pg_mblen(ptr); + ptrlen = pg_mblen_cstr(ptr); /* ignore whitespace, but end src or trg */ if (isspace((unsigned char) *ptr)) { @@ -239,7 +239,7 @@ initTrie(const char *filename) if (trgquoted && state > 0) { /* Ignore first and end quotes */ - trgstore = (char *) palloc(sizeof(char) * (trglen - 2)); + trgstore = palloc_array(char, trglen - 2); trgstorelen = 0; for (int i = 1; i < trglen - 1; i++) { @@ -252,7 +252,7 @@ initTrie(const char *filename) } else { - trgstore = (char *) palloc(sizeof(char) * trglen); + trgstore = palloc_array(char, trglen); trgstorelen = trglen; memcpy(trgstore, trg, trgstorelen); } @@ -382,6 +382,7 @@ unaccent_lexize(PG_FUNCTION_ARGS) char *srcchar = (char *) PG_GETARG_POINTER(1); int32 len = PG_GETARG_INT32(2); char *srcstart = srcchar; + const char *srcend = srcstart + len; TSLexeme *res; StringInfoData buf; @@ -409,7 +410,7 @@ unaccent_lexize(PG_FUNCTION_ARGS) } else { - matchlen = pg_mblen(srcchar); + matchlen = pg_mblen_range(srcchar, srcend); if (buf.data != NULL) appendBinaryStringInfo(&buf, srcchar, matchlen); } @@ -421,7 +422,7 @@ unaccent_lexize(PG_FUNCTION_ARGS) /* return a result only if we made at least one substitution */ if (buf.data != NULL) { - res = (TSLexeme *) palloc0(sizeof(TSLexeme) * 2); + res = palloc0_array(TSLexeme, 2); res->lexeme = buf.data; res->flags = TSL_FILTER; } diff --git a/contrib/unaccent/unaccent.rules b/contrib/unaccent/unaccent.rules index 35fd246b71f5e..271c7cdadd917 100644 --- a/contrib/unaccent/unaccent.rules +++ b/contrib/unaccent/unaccent.rules @@ -1565,6 +1565,7 @@ Ꞩ S êž© s Ɦ H +꟱ S ꟲ C ꟳ F ꟴ Q diff --git a/contrib/uuid-ossp/meson.build b/contrib/uuid-ossp/meson.build index 982f27c085fc9..b074900dd16f0 100644 --- a/contrib/uuid-ossp/meson.build +++ b/contrib/uuid-ossp/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not uuid.found() subdir_done() diff --git a/contrib/uuid-ossp/uuid-ossp.c b/contrib/uuid-ossp/uuid-ossp.c index 58e312a068279..aa4d0becace2c 100644 --- a/contrib/uuid-ossp/uuid-ossp.c +++ b/contrib/uuid-ossp/uuid-ossp.c @@ -2,7 +2,7 @@ * * UUID generation functions using the BSD, E2FS or OSSP UUID library * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * Portions Copyright (c) 2009 Andrew Gierth * @@ -337,7 +337,7 @@ uuid_generate_internal(int v, unsigned char *ns, const char *ptr, int len) elog(ERROR, "could not initialize %s context: %s", "MD5", pg_cryptohash_error(ctx)); if (pg_cryptohash_update(ctx, ns, sizeof(uu)) < 0 || - pg_cryptohash_update(ctx, (unsigned char *) ptr, len) < 0) + pg_cryptohash_update(ctx, (const unsigned char *) ptr, len) < 0) elog(ERROR, "could not update %s context: %s", "MD5", pg_cryptohash_error(ctx)); /* we assume sizeof MD5 result is 16, same as UUID size */ @@ -356,7 +356,7 @@ uuid_generate_internal(int v, unsigned char *ns, const char *ptr, int len) elog(ERROR, "could not initialize %s context: %s", "SHA1", pg_cryptohash_error(ctx)); if (pg_cryptohash_update(ctx, ns, sizeof(uu)) < 0 || - pg_cryptohash_update(ctx, (unsigned char *) ptr, len) < 0) + pg_cryptohash_update(ctx, (const unsigned char *) ptr, len) < 0) elog(ERROR, "could not update %s context: %s", "SHA1", pg_cryptohash_error(ctx)); if (pg_cryptohash_final(ctx, sha1result, sizeof(sha1result)) < 0) diff --git a/contrib/vacuumlo/meson.build b/contrib/vacuumlo/meson.build index deee1d2832d6a..4ee5b04857573 100644 --- a/contrib/vacuumlo/meson.build +++ b/contrib/vacuumlo/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group vacuumlo_sources = files( 'vacuumlo.c', diff --git a/contrib/vacuumlo/t/001_basic.pl b/contrib/vacuumlo/t/001_basic.pl index 8b2ad24eff1ab..9b4ac9aff5b0d 100644 --- a/contrib/vacuumlo/t/001_basic.pl +++ b/contrib/vacuumlo/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/contrib/vacuumlo/vacuumlo.c b/contrib/vacuumlo/vacuumlo.c index 16a450c4386af..8102569466be3 100644 --- a/contrib/vacuumlo/vacuumlo.c +++ b/contrib/vacuumlo/vacuumlo.c @@ -3,7 +3,7 @@ * vacuumlo.c * This removes orphaned large objects from a database. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/contrib/xml2/expected/xml2.out b/contrib/xml2/expected/xml2.out index 3d97b14c3a1e4..1906fcf33e2a5 100644 --- a/contrib/xml2/expected/xml2.out +++ b/contrib/xml2/expected/xml2.out @@ -261,3 +261,13 @@ $$ $$); ERROR: failed to apply stylesheet +-- empty output +select xslt_process('', +$$ +$$); + xslt_process +-------------- + +(1 row) + diff --git a/contrib/xml2/expected/xml2_1.out b/contrib/xml2/expected/xml2_1.out index 31700040a604b..9a2144d58f577 100644 --- a/contrib/xml2/expected/xml2_1.out +++ b/contrib/xml2/expected/xml2_1.out @@ -205,3 +205,9 @@ $$ $$); ERROR: xslt_process() is not available without libxslt +-- empty output +select xslt_process('', +$$ +$$); +ERROR: xslt_process() is not available without libxslt diff --git a/contrib/xml2/meson.build b/contrib/xml2/meson.build index 08d3c3b8e3085..faae5e5e42826 100644 --- a/contrib/xml2/meson.build +++ b/contrib/xml2/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not libxml.found() subdir_done() diff --git a/contrib/xml2/sql/xml2.sql b/contrib/xml2/sql/xml2.sql index ef99d164f2720..510d18a367996 100644 --- a/contrib/xml2/sql/xml2.sql +++ b/contrib/xml2/sql/xml2.sql @@ -153,3 +153,9 @@ $$ $$); + +-- empty output +select xslt_process('', +$$ +$$); diff --git a/contrib/xml2/xpath.c b/contrib/xml2/xpath.c index 23d3f332dbaa7..14b9e014d74d2 100644 --- a/contrib/xml2/xpath.c +++ b/contrib/xml2/xpath.c @@ -12,6 +12,7 @@ #include "funcapi.h" #include "lib/stringinfo.h" #include "utils/builtins.h" +#include "utils/tuplestore.h" #include "utils/xml.h" /* libxml includes */ @@ -51,8 +52,8 @@ static text *pgxml_result_to_text(xmlXPathObjectPtr res, xmlChar *toptag, static xmlChar *pgxml_texttoxmlchar(text *textstring); -static xmlXPathObjectPtr pgxml_xpath(text *document, xmlChar *xpath, - xpath_workspace *workspace); +static xpath_workspace *pgxml_xpath(text *document, xmlChar *xpath, + PgXmlErrorContext *xmlerrcxt); static void cleanup_workspace(xpath_workspace *workspace); @@ -88,19 +89,41 @@ Datum xml_encode_special_chars(PG_FUNCTION_ARGS) { text *tin = PG_GETARG_TEXT_PP(0); - text *tout; - xmlChar *ts, - *tt; + text *volatile tout = NULL; + xmlChar *volatile tt = NULL; + PgXmlErrorContext *xmlerrcxt; + + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); + + PG_TRY(); + { + xmlChar *ts; - ts = pgxml_texttoxmlchar(tin); + ts = pgxml_texttoxmlchar(tin); + + tt = xmlEncodeSpecialChars(NULL, ts); + if (tt == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate xmlChar"); + pfree(ts); + + tout = cstring_to_text((char *) tt); + } + PG_CATCH(); + { + if (tt != NULL) + xmlFree(tt); - tt = xmlEncodeSpecialChars(NULL, ts); + pg_xml_done(xmlerrcxt, true); - pfree(ts); + PG_RE_THROW(); + } + PG_END_TRY(); - tout = cstring_to_text((char *) tt); + if (tt != NULL) + xmlFree(tt); - xmlFree(tt); + pg_xml_done(xmlerrcxt, false); PG_RETURN_TEXT_P(tout); } @@ -122,62 +145,89 @@ pgxmlNodeSetToText(xmlNodeSetPtr nodeset, xmlChar *septagname, xmlChar *plainsep) { - xmlBufferPtr buf; - xmlChar *result; - int i; + volatile xmlBufferPtr buf = NULL; + xmlChar *volatile result = NULL; + PgXmlErrorContext *xmlerrcxt; - buf = xmlBufferCreate(); + /* spin up some error handling */ + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); - if ((toptagname != NULL) && (xmlStrlen(toptagname) > 0)) - { - xmlBufferWriteChar(buf, "<"); - xmlBufferWriteCHAR(buf, toptagname); - xmlBufferWriteChar(buf, ">"); - } - if (nodeset != NULL) + PG_TRY(); { - for (i = 0; i < nodeset->nodeNr; i++) - { - if (plainsep != NULL) - { - xmlBufferWriteCHAR(buf, - xmlXPathCastNodeToString(nodeset->nodeTab[i])); + buf = xmlBufferCreate(); - /* If this isn't the last entry, write the plain sep. */ - if (i < (nodeset->nodeNr) - 1) - xmlBufferWriteChar(buf, (char *) plainsep); - } - else + if (buf == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate xmlBuffer"); + + if ((toptagname != NULL) && (xmlStrlen(toptagname) > 0)) + { + xmlBufferWriteChar(buf, "<"); + xmlBufferWriteCHAR(buf, toptagname); + xmlBufferWriteChar(buf, ">"); + } + if (nodeset != NULL) + { + for (int i = 0; i < nodeset->nodeNr; i++) { - if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) + if (plainsep != NULL) { - xmlBufferWriteChar(buf, "<"); - xmlBufferWriteCHAR(buf, septagname); - xmlBufferWriteChar(buf, ">"); - } - xmlNodeDump(buf, - nodeset->nodeTab[i]->doc, - nodeset->nodeTab[i], - 1, 0); + xmlBufferWriteCHAR(buf, + xmlXPathCastNodeToString(nodeset->nodeTab[i])); - if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) + /* If this isn't the last entry, write the plain sep. */ + if (i < (nodeset->nodeNr) - 1) + xmlBufferWriteChar(buf, (char *) plainsep); + } + else { - xmlBufferWriteChar(buf, ""); + if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) + { + xmlBufferWriteChar(buf, "<"); + xmlBufferWriteCHAR(buf, septagname); + xmlBufferWriteChar(buf, ">"); + } + xmlNodeDump(buf, + nodeset->nodeTab[i]->doc, + nodeset->nodeTab[i], + 1, 0); + + if ((septagname != NULL) && (xmlStrlen(septagname) > 0)) + { + xmlBufferWriteChar(buf, ""); + } } } } - } - if ((toptagname != NULL) && (xmlStrlen(toptagname) > 0)) + if ((toptagname != NULL) && (xmlStrlen(toptagname) > 0)) + { + xmlBufferWriteChar(buf, ""); + } + + result = xmlStrdup(xmlBufferContent(buf)); + if (result == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate result"); + } + PG_CATCH(); { - xmlBufferWriteChar(buf, ""); + if (buf) + xmlBufferFree(buf); + + pg_xml_done(xmlerrcxt, true); + + PG_RE_THROW(); } - result = xmlStrdup(buf->content); + PG_END_TRY(); + xmlBufferFree(buf); + pg_xml_done(xmlerrcxt, false); + return result; } @@ -207,17 +257,30 @@ xpath_nodeset(PG_FUNCTION_ARGS) xmlChar *toptag = pgxml_texttoxmlchar(PG_GETARG_TEXT_PP(2)); xmlChar *septag = pgxml_texttoxmlchar(PG_GETARG_TEXT_PP(3)); xmlChar *xpath; - text *xpres; - xmlXPathObjectPtr res; - xpath_workspace workspace; + text *volatile xpres = NULL; + xpath_workspace *volatile workspace = NULL; + PgXmlErrorContext *xmlerrcxt; xpath = pgxml_texttoxmlchar(xpathsupp); + xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY); - res = pgxml_xpath(document, xpath, &workspace); + PG_TRY(); + { + workspace = pgxml_xpath(document, xpath, xmlerrcxt); + xpres = pgxml_result_to_text(workspace->res, toptag, septag, NULL); + } + PG_CATCH(); + { + if (workspace) + cleanup_workspace(workspace); - xpres = pgxml_result_to_text(res, toptag, septag, NULL); + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); - cleanup_workspace(&workspace); + cleanup_workspace(workspace); + pg_xml_done(xmlerrcxt, false); pfree(xpath); @@ -239,17 +302,30 @@ xpath_list(PG_FUNCTION_ARGS) text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */ xmlChar *plainsep = pgxml_texttoxmlchar(PG_GETARG_TEXT_PP(2)); xmlChar *xpath; - text *xpres; - xmlXPathObjectPtr res; - xpath_workspace workspace; + text *volatile xpres = NULL; + xpath_workspace *volatile workspace = NULL; + PgXmlErrorContext *xmlerrcxt; xpath = pgxml_texttoxmlchar(xpathsupp); + xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY); - res = pgxml_xpath(document, xpath, &workspace); + PG_TRY(); + { + workspace = pgxml_xpath(document, xpath, xmlerrcxt); + xpres = pgxml_result_to_text(workspace->res, NULL, NULL, plainsep); + } + PG_CATCH(); + { + if (workspace) + cleanup_workspace(workspace); - xpres = pgxml_result_to_text(res, NULL, NULL, plainsep); + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); - cleanup_workspace(&workspace); + cleanup_workspace(workspace); + pg_xml_done(xmlerrcxt, false); pfree(xpath); @@ -268,9 +344,9 @@ xpath_string(PG_FUNCTION_ARGS) text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */ xmlChar *xpath; int32 pathsize; - text *xpres; - xmlXPathObjectPtr res; - xpath_workspace workspace; + text *volatile xpres = NULL; + xpath_workspace *volatile workspace = NULL; + PgXmlErrorContext *xmlerrcxt; pathsize = VARSIZE_ANY_EXHDR(xpathsupp); @@ -286,11 +362,25 @@ xpath_string(PG_FUNCTION_ARGS) xpath[pathsize + 7] = ')'; xpath[pathsize + 8] = '\0'; - res = pgxml_xpath(document, xpath, &workspace); + xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY); + + PG_TRY(); + { + workspace = pgxml_xpath(document, xpath, xmlerrcxt); + xpres = pgxml_result_to_text(workspace->res, NULL, NULL, NULL); + } + PG_CATCH(); + { + if (workspace) + cleanup_workspace(workspace); - xpres = pgxml_result_to_text(res, NULL, NULL, NULL); + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); - cleanup_workspace(&workspace); + cleanup_workspace(workspace); + pg_xml_done(xmlerrcxt, false); pfree(xpath); @@ -308,24 +398,38 @@ xpath_number(PG_FUNCTION_ARGS) text *document = PG_GETARG_TEXT_PP(0); text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */ xmlChar *xpath; - float4 fRes; - xmlXPathObjectPtr res; - xpath_workspace workspace; + volatile float4 fRes = 0.0; + volatile bool isNull = false; + xpath_workspace *volatile workspace = NULL; + PgXmlErrorContext *xmlerrcxt; xpath = pgxml_texttoxmlchar(xpathsupp); + xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY); - res = pgxml_xpath(document, xpath, &workspace); - - pfree(xpath); + PG_TRY(); + { + workspace = pgxml_xpath(document, xpath, xmlerrcxt); + pfree(xpath); - if (res == NULL) - PG_RETURN_NULL(); + if (workspace->res == NULL) + isNull = true; + else + fRes = xmlXPathCastToNumber(workspace->res); + } + PG_CATCH(); + { + if (workspace) + cleanup_workspace(workspace); - fRes = xmlXPathCastToNumber(res); + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); - cleanup_workspace(&workspace); + cleanup_workspace(workspace); + pg_xml_done(xmlerrcxt, false); - if (xmlXPathIsNaN(fRes)) + if (isNull || xmlXPathIsNaN(fRes)) PG_RETURN_NULL(); PG_RETURN_FLOAT4(fRes); @@ -340,22 +444,35 @@ xpath_bool(PG_FUNCTION_ARGS) text *document = PG_GETARG_TEXT_PP(0); text *xpathsupp = PG_GETARG_TEXT_PP(1); /* XPath expression */ xmlChar *xpath; - int bRes; - xmlXPathObjectPtr res; - xpath_workspace workspace; + volatile int bRes = 0; + xpath_workspace *volatile workspace = NULL; + PgXmlErrorContext *xmlerrcxt; xpath = pgxml_texttoxmlchar(xpathsupp); + xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY); - res = pgxml_xpath(document, xpath, &workspace); - - pfree(xpath); + PG_TRY(); + { + workspace = pgxml_xpath(document, xpath, xmlerrcxt); + pfree(xpath); - if (res == NULL) - PG_RETURN_BOOL(false); + if (workspace->res == NULL) + bRes = 0; + else + bRes = xmlXPathCastToBoolean(workspace->res); + } + PG_CATCH(); + { + if (workspace) + cleanup_workspace(workspace); - bRes = xmlXPathCastToBoolean(res); + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); - cleanup_workspace(&workspace); + cleanup_workspace(workspace); + pg_xml_done(xmlerrcxt, false); PG_RETURN_BOOL(bRes); } @@ -364,57 +481,38 @@ xpath_bool(PG_FUNCTION_ARGS) /* Core function to evaluate XPath query */ -static xmlXPathObjectPtr -pgxml_xpath(text *document, xmlChar *xpath, xpath_workspace *workspace) +static xpath_workspace * +pgxml_xpath(text *document, xmlChar *xpath, PgXmlErrorContext *xmlerrcxt) { int32 docsize = VARSIZE_ANY_EXHDR(document); - PgXmlErrorContext *xmlerrcxt; xmlXPathCompExprPtr comppath; + xpath_workspace *workspace = palloc0_object(xpath_workspace); workspace->doctree = NULL; workspace->ctxt = NULL; workspace->res = NULL; - xmlerrcxt = pgxml_parser_init(PG_XML_STRICTNESS_LEGACY); - - PG_TRY(); + workspace->doctree = xmlReadMemory((char *) VARDATA_ANY(document), + docsize, NULL, NULL, + XML_PARSE_NOENT); + if (workspace->doctree != NULL) { - workspace->doctree = xmlReadMemory((char *) VARDATA_ANY(document), - docsize, NULL, NULL, - XML_PARSE_NOENT); - if (workspace->doctree != NULL) - { - workspace->ctxt = xmlXPathNewContext(workspace->doctree); - workspace->ctxt->node = xmlDocGetRootElement(workspace->doctree); - - /* compile the path */ - comppath = xmlXPathCtxtCompile(workspace->ctxt, xpath); - if (comppath == NULL) - xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY, - "XPath Syntax Error"); + workspace->ctxt = xmlXPathNewContext(workspace->doctree); + workspace->ctxt->node = xmlDocGetRootElement(workspace->doctree); - /* Now evaluate the path expression. */ - workspace->res = xmlXPathCompiledEval(comppath, workspace->ctxt); + /* compile the path */ + comppath = xmlXPathCtxtCompile(workspace->ctxt, xpath); + if (comppath == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY, + "XPath Syntax Error"); - xmlXPathFreeCompExpr(comppath); - } - } - PG_CATCH(); - { - cleanup_workspace(workspace); - - pg_xml_done(xmlerrcxt, true); + /* Now evaluate the path expression. */ + workspace->res = xmlXPathCompiledEval(comppath, workspace->ctxt); - PG_RE_THROW(); + xmlXPathFreeCompExpr(comppath); } - PG_END_TRY(); - if (workspace->res == NULL) - cleanup_workspace(workspace); - - pg_xml_done(xmlerrcxt, false); - - return workspace->res; + return workspace; } /* Clean up after processing the result of pgxml_xpath() */ @@ -438,35 +536,60 @@ pgxml_result_to_text(xmlXPathObjectPtr res, xmlChar *septag, xmlChar *plainsep) { - xmlChar *xpresstr; - text *xpres; + xmlChar *volatile xpresstr = NULL; + text *volatile xpres = NULL; + PgXmlErrorContext *xmlerrcxt; if (res == NULL) return NULL; - switch (res->type) - { - case XPATH_NODESET: - xpresstr = pgxmlNodeSetToText(res->nodesetval, - toptag, - septag, plainsep); - break; + /* spin some error handling */ + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); - case XPATH_STRING: - xpresstr = xmlStrdup(res->stringval); - break; + PG_TRY(); + { + switch (res->type) + { + case XPATH_NODESET: + xpresstr = pgxmlNodeSetToText(res->nodesetval, + toptag, + septag, plainsep); + break; + + case XPATH_STRING: + xpresstr = xmlStrdup(res->stringval); + if (xpresstr == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate result"); + break; + + default: + elog(NOTICE, "unsupported XQuery result: %d", res->type); + xpresstr = xmlStrdup((const xmlChar *) ""); + if (xpresstr == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate result"); + } - default: - elog(NOTICE, "unsupported XQuery result: %d", res->type); - xpresstr = xmlStrdup((const xmlChar *) ""); + /* Now convert this result back to text */ + xpres = cstring_to_text((char *) xpresstr); } + PG_CATCH(); + { + if (xpresstr != NULL) + xmlFree(xpresstr); - /* Now convert this result back to text */ - xpres = cstring_to_text((char *) xpresstr); + pg_xml_done(xmlerrcxt, true); + + PG_RE_THROW(); + } + PG_END_TRY(); /* Free various storage */ xmlFree(xpresstr); + pg_xml_done(xmlerrcxt, false); + return xpres; } @@ -648,11 +771,16 @@ xpath_table(PG_FUNCTION_ARGS) for (j = 0; j < numpaths; j++) { ctxt = xmlXPathNewContext(doctree); + if (ctxt == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, + ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate XPath context"); + ctxt->node = xmlDocGetRootElement(doctree); /* compile the path */ comppath = xmlXPathCtxtCompile(ctxt, xpaths[j]); - if (comppath == NULL) + if (comppath == NULL || pg_xml_error_occurred(xmlerrcxt)) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY, "XPath Syntax Error"); @@ -671,6 +799,10 @@ xpath_table(PG_FUNCTION_ARGS) rownr < res->nodesetval->nodeNr) { resstr = xmlXPathCastNodeToString(res->nodesetval->nodeTab[rownr]); + if (resstr == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, + ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate result"); had_values = true; } else @@ -680,11 +812,19 @@ xpath_table(PG_FUNCTION_ARGS) case XPATH_STRING: resstr = xmlStrdup(res->stringval); + if (resstr == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, + ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate result"); break; default: elog(NOTICE, "unsupported XQuery result: %d", res->type); resstr = xmlStrdup((const xmlChar *) ""); + if (resstr == NULL || pg_xml_error_occurred(xmlerrcxt)) + xml_ereport(xmlerrcxt, + ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate result"); } /* diff --git a/contrib/xml2/xslt_proc.c b/contrib/xml2/xslt_proc.c index b720d89f754ae..8ceb8c464942f 100644 --- a/contrib/xml2/xslt_proc.c +++ b/contrib/xml2/xslt_proc.c @@ -10,6 +10,7 @@ #include "fmgr.h" #include "utils/builtins.h" #include "utils/xml.h" +#include "varatt.h" #ifdef USE_LIBXSLT @@ -48,7 +49,7 @@ xslt_process(PG_FUNCTION_ARGS) text *doct = PG_GETARG_TEXT_PP(0); text *ssheet = PG_GETARG_TEXT_PP(1); - text *result; + text *volatile result = NULL; text *paramstr; const char **params; PgXmlErrorContext *xmlerrcxt; @@ -58,8 +59,7 @@ xslt_process(PG_FUNCTION_ARGS) volatile xsltSecurityPrefsPtr xslt_sec_prefs = NULL; volatile xsltTransformContextPtr xslt_ctxt = NULL; volatile int resstat = -1; - xmlChar *resstr = NULL; - int reslen = 0; + xmlChar *volatile resstr = NULL; if (fcinfo->nargs == 3) { @@ -69,7 +69,7 @@ xslt_process(PG_FUNCTION_ARGS) else { /* No parameters */ - params = (const char **) palloc(sizeof(char *)); + params = palloc_object(const char *); params[0] = NULL; } @@ -80,13 +80,14 @@ xslt_process(PG_FUNCTION_ARGS) { xmlDocPtr ssdoc; bool xslt_sec_prefs_error; + int reslen = 0; /* Parse document */ doctree = xmlReadMemory((char *) VARDATA_ANY(doct), VARSIZE_ANY_EXHDR(doct), NULL, NULL, XML_PARSE_NOENT); - if (doctree == NULL) + if (doctree == NULL || pg_xml_error_occurred(xmlerrcxt)) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT, "error parsing XML document"); @@ -95,14 +96,14 @@ xslt_process(PG_FUNCTION_ARGS) VARSIZE_ANY_EXHDR(ssheet), NULL, NULL, XML_PARSE_NOENT); - if (ssdoc == NULL) + if (ssdoc == NULL || pg_xml_error_occurred(xmlerrcxt)) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT, "error parsing stylesheet as XML document"); /* After this call we need not free ssdoc separately */ stylesheet = xsltParseStylesheetDoc(ssdoc); - if (stylesheet == NULL) + if (stylesheet == NULL || pg_xml_error_occurred(xmlerrcxt)) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY, "failed to parse stylesheet"); @@ -137,11 +138,24 @@ xslt_process(PG_FUNCTION_ARGS) restree = xsltApplyStylesheetUser(stylesheet, doctree, params, NULL, NULL, xslt_ctxt); - if (restree == NULL) + if (restree == NULL || pg_xml_error_occurred(xmlerrcxt)) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY, "failed to apply stylesheet"); - resstat = xsltSaveResultToString(&resstr, &reslen, restree, stylesheet); + resstat = xsltSaveResultToString((xmlChar **) &resstr, &reslen, + restree, stylesheet); + + if (resstat >= 0) + { + /* + * If an empty string has been returned, resstr would be NULL. In + * this case, assume that the result is an empty string. + */ + if (reslen == 0) + result = cstring_to_text(""); + else + result = cstring_to_text_with_len((char *) resstr, reslen); + } } PG_CATCH(); { @@ -155,6 +169,8 @@ xslt_process(PG_FUNCTION_ARGS) xsltFreeStylesheet(stylesheet); if (doctree != NULL) xmlFreeDoc(doctree); + if (resstr != NULL) + xmlFree(resstr); xsltCleanupGlobals(); pg_xml_done(xmlerrcxt, true); @@ -170,17 +186,15 @@ xslt_process(PG_FUNCTION_ARGS) xmlFreeDoc(doctree); xsltCleanupGlobals(); + if (resstr) + xmlFree(resstr); + pg_xml_done(xmlerrcxt, false); /* XXX this is pretty dubious, really ought to throw error instead */ if (resstat < 0) PG_RETURN_NULL(); - result = cstring_to_text_with_len((char *) resstr, reslen); - - if (resstr) - xmlFree(resstr); - PG_RETURN_TEXT_P(result); #else /* !USE_LIBXSLT */ diff --git a/doc/src/sgml/Makefile b/doc/src/sgml/Makefile index 11aac91381258..b53b2694a6b7e 100644 --- a/doc/src/sgml/Makefile +++ b/doc/src/sgml/Makefile @@ -59,7 +59,7 @@ GENERATED_SGML = version.sgml \ features-supported.sgml features-unsupported.sgml errcodes-table.sgml \ keywords-table.sgml targets-meson.sgml wait_event_types.sgml -ALL_SGML := $(wildcard $(srcdir)/*.sgml $(srcdir)/ref/*.sgml) $(GENERATED_SGML) +ALL_SGML := $(wildcard $(srcdir)/*.sgml $(srcdir)/func/*.sgml $(srcdir)/ref/*.sgml) $(GENERATED_SGML) ALL_IMAGES := $(wildcard $(srcdir)/images/*.svg) @@ -263,14 +263,14 @@ endif # sqlmansectnum != 7 # tabs are harmless, but it is best to avoid them in SGML files check-tabs: - @( ! grep ' ' $(wildcard $(srcdir)/*.sgml $(srcdir)/ref/*.sgml $(srcdir)/*.xsl) ) || \ + @( ! grep ' ' $(wildcard $(srcdir)/*.sgml $(srcdir)/func/*.sgml $(srcdir)/ref/*.sgml $(srcdir)/*.xsl) ) || \ (echo "Tabs appear in SGML/XML files" 1>&2; exit 1) # Non-breaking spaces are harmless, but it is best to avoid them in SGML files. # Use perl command because non-GNU grep or sed could not have hex escape sequence. check-nbsp: @ ( $(PERL) -ne '/\xC2\xA0/ and print("$$ARGV:$$_"),$$n++; END {exit($$n>0)}' \ - $(wildcard $(srcdir)/*.sgml $(srcdir)/ref/*.sgml $(srcdir)/*.xsl $(srcdir)/images/*.xsl) ) || \ + $(wildcard $(srcdir)/*.sgml $(srcdir)/func/*.sgml $(srcdir)/ref/*.sgml $(srcdir)/*.xsl $(srcdir)/images/*.xsl) ) || \ (echo "Non-breaking spaces appear in SGML/XML files" 1>&2; exit 1) ## diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml index e15a3323dfbfd..3286c2cf0b26f 100644 --- a/doc/src/sgml/advanced.sgml +++ b/doc/src/sgml/advanced.sgml @@ -80,18 +80,18 @@ SELECT * FROM myview; - Recall the weather and - cities tables from weather and + cities tables from . Consider the following problem: You want to make sure that no one can insert rows in the - weather table that do not have a matching - entry in the cities table. This is called + weather table that do not have a matching + entry in the cities table. This is called maintaining the referential integrity of your data. In simplistic database systems this would be implemented (if at all) by first looking at the - cities table to check if a matching record + cities table to check if a matching record exists, and then inserting or rejecting the new - weather records. This approach has a + weather records. This approach has a number of problems and is very inconvenient, so PostgreSQL can do this for you. @@ -101,12 +101,12 @@ SELECT * FROM myview; CREATE TABLE cities ( - name varchar(80) primary key, + name varchar(80) PRIMARY KEY, location point ); CREATE TABLE weather ( - city varchar(80) references cities(name), + city varchar(80) REFERENCES cities (name), temp_lo int, temp_hi int, prcp real, @@ -149,7 +149,7 @@ DETAIL: Key (city)=(Berkeley) is not present in table "cities". systems. The essential point of a transaction is that it bundles multiple steps into a single, all-or-nothing operation. The intermediate states between the steps are not visible to other concurrent transactions, - and if some failure occurs that prevents the transaction from completing, + and if an error occurs that prevents the transaction from completing, then none of the steps affect the database at all. @@ -218,7 +218,8 @@ UPDATE branches SET balance = balance + 100.00 In PostgreSQL, a transaction is set up by surrounding the SQL commands of the transaction with - BEGIN and COMMIT commands. So our banking + and + commands. So our banking transaction would actually look like: @@ -233,7 +234,7 @@ COMMIT; If, partway through the transaction, we decide we do not want to commit (perhaps we just noticed that Alice's balance went negative), - we can issue the command ROLLBACK instead of + we can issue the command instead of COMMIT, and all our updates so far will be canceled. @@ -256,6 +257,17 @@ COMMIT; + + When an error occurs within a transaction block the transaction is not + ended, but instead goes into an aborted state. While in this state all + commands except and + are rejected. Importantly, both those + commands will behave identically — they roll back and close the + failed transaction, returning the session to a state where new commands + can be issued. They will also automatically begin a new transaction if + executed with the AND CHAIN option. + + It's possible to control the statements in a transaction in a more granular fashion through the use of savepoints. Savepoints @@ -578,8 +590,8 @@ SELECT sum(salary) OVER w, avg(salary) OVER w - Let's create two tables: A table cities - and a table capitals. Naturally, capitals + Let's create two tables: A table cities + and a table capitals. Naturally, capitals are also cities, so you want some way to show the capitals implicitly when you list all cities. If you're really clever you might invent some scheme like this: @@ -625,14 +637,14 @@ CREATE TABLE capitals ( - In this case, a row of capitals + In this case, a row of capitals inherits all columns (name, population, and elevation) from its - parent, cities. The + parent, cities. The type of the column name is text, a native PostgreSQL type for variable length character strings. The - capitals table has + capitals table has an additional column, state, which shows its state abbreviation. In PostgreSQL, a table can inherit from @@ -685,8 +697,8 @@ SELECT name, elevation Here the ONLY before cities indicates that the query should be run over only the - cities table, and not tables below - cities in the inheritance hierarchy. Many + cities table, and not tables below + cities in the inheritance hierarchy. Many of the commands that we have already discussed — SELECT, UPDATE, and DELETE — support this ONLY diff --git a/doc/src/sgml/amcheck.sgml b/doc/src/sgml/amcheck.sgml index 211a0ae1945bb..08006856579ad 100644 --- a/doc/src/sgml/amcheck.sgml +++ b/doc/src/sgml/amcheck.sgml @@ -278,8 +278,8 @@ SET client_min_messages = DEBUG1; TOAST table. - This option is known to be slow. Also, if the toast table or its - index is corrupt, checking it against toast values could conceivably + This option is known to be slow. Also, if the TOAST table or its + index is corrupt, checking it against TOAST values could conceivably crash the server, although in many cases this would just produce an error. @@ -382,7 +382,7 @@ SET client_min_messages = DEBUG1; verification functions is true, an additional phase of verification is performed against the table associated with the target index relation. This consists of a dummy - CREATE INDEX operation, which checks for the + CREATE INDEX CONCURRENTLY operation, which checks for the presence of all hypothetical new index tuples against a temporary, in-memory summarizing structure (this is built when needed during the basic first phase of verification). The summarizing structure diff --git a/doc/src/sgml/appendix-obsolete-auth-radius.sgml b/doc/src/sgml/appendix-obsolete-auth-radius.sgml new file mode 100644 index 0000000000000..68d49df004a81 --- /dev/null +++ b/doc/src/sgml/appendix-obsolete-auth-radius.sgml @@ -0,0 +1,20 @@ + + + + + RADIUS authentication removed + + + RADIUS + + + + PostgreSQL 18 and below supported the RADIUS authentication protocol. + Information about an alternative way to configure RADIUS support + is available on the PostgreSQL + wiki. + + + diff --git a/doc/src/sgml/appendix-obsolete.sgml b/doc/src/sgml/appendix-obsolete.sgml index b1a00c8ce67b5..cc00265305251 100644 --- a/doc/src/sgml/appendix-obsolete.sgml +++ b/doc/src/sgml/appendix-obsolete.sgml @@ -38,5 +38,6 @@ &obsolete-pgxlogdump; &obsolete-pgresetxlog; &obsolete-pgreceivexlog; + &obsolete-auth-radius; diff --git a/doc/src/sgml/arch-dev.sgml b/doc/src/sgml/arch-dev.sgml index 976db1e599984..06b6e2a849356 100644 --- a/doc/src/sgml/arch-dev.sgml +++ b/doc/src/sgml/arch-dev.sgml @@ -445,7 +445,7 @@ join sequence. The planner preferentially considers joins between any two relations for which there exists a corresponding join clause in the WHERE qualification (i.e., for - which a restriction like where rel1.attr1=rel2.attr2 + which a restriction like WHERE rel1.attr1 = rel2.attr2 exists). Join pairs with no join clause are considered only when there is no other choice, that is, a particular relation has no available join clauses to any other relation. All possible plans are generated for diff --git a/doc/src/sgml/auto-explain.sgml b/doc/src/sgml/auto-explain.sgml index 15c868021e673..06a8fcc6c5bc3 100644 --- a/doc/src/sgml/auto-explain.sgml +++ b/doc/src/sgml/auto-explain.sgml @@ -128,6 +128,26 @@ LOAD 'auto_explain'; + + + auto_explain.log_io (boolean) + + auto_explain.log_io configuration parameter + + + + + auto_explain.log_io controls whether I/O usage + statistics are printed when an execution plan is logged; it's + equivalent to the IO option of EXPLAIN. + This parameter has no effect + unless auto_explain.log_analyze is enabled. + This parameter is off by default. + Only superusers can change this setting. + + + + auto_explain.log_wal (boolean) @@ -245,6 +265,29 @@ LOAD 'auto_explain'; + + + auto_explain.log_extension_options (string) + + auto_explain.log_extension_options configuration parameter + + + + + Loadable modules can extend the EXPLAIN command with + additional options that affect the output format. Such options can also + be specified here. The value of this parameter is a comma-separated + list of options, each of which is an option name followed optionally by + an associated value. The module that provides the + EXPLAIN option, such as + pg_plan_advice or + pg_overexplain, + should be loaded before this parameter is set. + Only superusers can change this setting. + + + + auto_explain.log_level (enum) diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml index 25b8904baf7cd..168444eccc570 100644 --- a/doc/src/sgml/backup.sgml +++ b/doc/src/sgml/backup.sgml @@ -627,7 +627,7 @@ tar -cf backup.tar /usr/local/pgsql/data character in the command. The simplest useful command is something like: -archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' # Unix +archive_command = 'test ! -f "/mnt/server/archivedir/%f" && cp "%p" "/mnt/server/archivedir/%f"' # Unix archive_command = 'copy "%p" "C:\\server\\archivedir\\%f"' # Windows which will copy archivable WAL segments to the directory @@ -991,7 +991,7 @@ SELECT pg_backup_start(label => 'label', fast => false); usually preferable as it minimizes the impact on the running system. If you want to start the backup as soon as possible, pass true as the second parameter to pg_backup_start and it will - request an immediate checkpoint, which will finish as fast as possible using + request a fast checkpoint, which will finish as fast as possible using as much I/O as possible. @@ -1294,7 +1294,7 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true); character in the command. The simplest useful command is something like: -restore_command = 'cp /mnt/server/archivedir/%f %p' +restore_command = 'cp "/mnt/server/archivedir/%f" "%p"' which will copy previously archived WAL segments from the directory /mnt/server/archivedir. Of course, you can use something @@ -1493,11 +1493,11 @@ restore_command = 'cp /mnt/server/archivedir/%f %p' If archive storage size is a concern, you can use gzip to compress the archive files: -archive_command = 'gzip < %p > /mnt/server/archivedir/%f.gz' +archive_command = 'gzip < "%p" > "/mnt/server/archivedir/%f.gz"' You will then need to use gunzip during recovery: -restore_command = 'gunzip < /mnt/server/archivedir/%f.gz > %p' +restore_command = 'gunzip < "/mnt/server/archivedir/%f.gz" > "%p"' diff --git a/doc/src/sgml/bgworker.sgml b/doc/src/sgml/bgworker.sgml index 2c393385a91f4..2affba743825f 100644 --- a/doc/src/sgml/bgworker.sgml +++ b/doc/src/sgml/bgworker.sgml @@ -108,6 +108,29 @@ typedef struct BackgroundWorker + + BGWORKER_INTERRUPTIBLE + + + BGWORKER_INTERRUPTIBLE + Requests termination of the background worker when its connected database is + dropped, renamed, moved to a different tablespace, or used as a template for + CREATE DATABASE. Specifically, the postmaster sends a + termination signal when any of these commands affect the worker's database: + + DROP DATABASE + ALTER DATABASE RENAME + TO + ALTER DATABASE SET + TABLESPACE + CREATE DATABASE + + Requires both BGWORKER_SHMEM_ACCESS and + BGWORKER_BACKEND_DATABASE_CONNECTION. + + + + @@ -209,6 +232,8 @@ typedef struct BackgroundWorker + A well-behaved background worker must react promptly to standard signals + that the postmaster uses to control its child processes. Signals are initially blocked when control reaches the background worker's main function, and must be unblocked by it; this is to allow the process to customize its signal handlers, if necessary. @@ -217,6 +242,14 @@ typedef struct BackgroundWorker BackgroundWorkerBlockSignals. + + The default signal handlers merely set interrupt flags + that are processed later by CHECK_FOR_INTERRUPTS(). + CHECK_FOR_INTERRUPTS() should be called in any + long-running loop to ensure that the background worker doesn't prevent the + system from shutting down in a timely fashion. + + If bgw_restart_time for a background worker is configured as BGW_NEVER_RESTART, or if it exits with an exit diff --git a/doc/src/sgml/bki.sgml b/doc/src/sgml/bki.sgml index 3cd5bee7ffaf4..087a6827b0016 100644 --- a/doc/src/sgml/bki.sgml +++ b/doc/src/sgml/bki.sgml @@ -271,6 +271,21 @@ + + + There is a special case for values of the + pg_proc.proargdefaults + field, which is of type pg_node_tree. The real + contents of that type are too complex for hand-written entries, + but what we need for proargdefaults is + typically just a list of Const nodes. Therefore, the bootstrap + backend will interpret a value given for that field according to + text array syntax, and then feed the array element values to the + datatype input routines for the corresponding input parameters' data + types, and finally build Const nodes from the datums. + + + Since hashes are unordered data structures, field order and line @@ -817,11 +832,11 @@ $ perl rewrite_dat_with_prokind.pl pg_proc.dat The following column types are supported directly by bootstrap.c: bool, bytea, char (1 byte), - name, int2, - int4, regproc, regclass, - regtype, text, - oid, tid, xid, - cid, int2vector, oidvector, + int2, int4, int8, + float4, float8, + name, regproc, text, + jsonb, oid, pg_node_tree, + int2vector, oidvector, _int4 (array), _text (array), _oid (array), _char (array), _aclitem (array). Although it is possible to create @@ -884,7 +899,7 @@ $ perl rewrite_dat_with_prokind.pl pg_proc.dat - insert ( oid_value value1 value2 ... ) + insert ( value1 value2 ... ) @@ -902,6 +917,13 @@ $ perl rewrite_dat_with_prokind.pl pg_proc.dat (To include a single quote in a value, write it twice. Escape-string-style backslash escapes are allowed in the string, too.) + + + In most cases a value + string is simply fed to the datatype input routine for the column's + data type, after de-quoting if needed. However there are exceptions + for certain fields, as detailed previously. + @@ -1042,7 +1064,7 @@ $ perl rewrite_dat_with_prokind.pl pg_proc.dat - Define indexes and toast tables. + Define indexes and TOAST tables. diff --git a/doc/src/sgml/bloom.sgml b/doc/src/sgml/bloom.sgml index ec5d077679b14..3f6d38f377b7a 100644 --- a/doc/src/sgml/bloom.sgml +++ b/doc/src/sgml/bloom.sgml @@ -101,12 +101,12 @@ CREATE INDEX bloomidx ON tbloom USING bloom (i1,i2,i3) =# CREATE TABLE tbloom AS SELECT - (random() * 1000000)::int as i1, - (random() * 1000000)::int as i2, - (random() * 1000000)::int as i3, - (random() * 1000000)::int as i4, - (random() * 1000000)::int as i5, - (random() * 1000000)::int as i6 + (random() * 1000000)::int AS i1, + (random() * 1000000)::int AS i2, + (random() * 1000000)::int AS i3, + (random() * 1000000)::int AS i4, + (random() * 1000000)::int AS i5, + (random() * 1000000)::int AS i6 FROM generate_series(1,10000000); SELECT 10000000 diff --git a/doc/src/sgml/btree-gist.sgml b/doc/src/sgml/btree-gist.sgml index a4c1b99be1faf..cc09ec8373351 100644 --- a/doc/src/sgml/btree-gist.sgml +++ b/doc/src/sgml/btree-gist.sgml @@ -108,6 +108,53 @@ INSERT 0 1 + + <literal>btree_gist</literal> Indexes + on <type>inet</type>/<type>cidr</type> Columns + + + The gist_inet_ops and gist_cidr_ops + operator classes provided by btree_gist have been + shown to be unreliable: index searches may fail to find relevant rows due + to approximations used in creating the index entries. This is unfixable + without redefining the contents of indexes that use these opclasses. + Therefore, these opclasses are being deprecated in favor of the built-in + GiST inet_ops opclass, which does not share the design + flaw. + + + + As a first step, PostgreSQL version 19 removes + the default-opclass marking from gist_inet_ops + and gist_cidr_ops, instead + marking inet_ops as default for inet + and cidr columns. This will result in transparently + substituting inet_ops for the faulty opclasses in most + contexts. It is still possible to create indexes using the faulty + opclasses, if really necessary, by explicitly specifying which opclass to + use; for example + +CREATE TABLE mytable (addr inet); +CREATE INDEX dubious_index ON mytable USING GIST (addr gist_inet_ops); + + + + + However, pg_upgrade cannot handle this change + due to implementation limitations. If asked to upgrade a pre-v19 + database that contains gist_inet_ops + or gist_cidr_ops + indexes, pg_upgrade will fail and tell you to + replace those indexes before upgrading. This would look approximately + like + +CREATE INDEX good_index ON mytable USING GIST (addr inet_ops); +DROP INDEX bad_index; + + + + + Authors diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index cbd4e40a320b3..4b474c139174d 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -240,6 +240,31 @@ functions and procedures + + pg_propgraph_element + property graph elements (vertices and edges) + + + + pg_propgraph_element_label + property graph links between elements and labels + + + + pg_propgraph_label + property graph labels + + + + pg_propgraph_label_property + property graph label-specific property definitions + + + + pg_propgraph_property + property graph properties + + pg_publication publications for logical replication @@ -596,7 +621,10 @@ Approximate average size (in bytes) of the transition state - data, or zero to use a default estimate + data. A positive value provides an estimate; zero means to + use a default estimate. A negative value indicates the state + data can grow unboundedly in size, such as when the aggregate + accumulates input rows (e.g., array_agg, string_agg). @@ -1582,7 +1610,7 @@ rolpassword text - Password (possibly encrypted); null if none. The format depends + Encrypted password; null if none. The format depends on the form of encryption used. @@ -1627,11 +1655,6 @@ SCRAM-SHA-256$<iteration count>:&l ServerKey are in Base64 encoded format. This format is the same as that specified by RFC 5803. - - - A password that does not follow either of those formats is assumed to be - unencrypted. - @@ -1956,7 +1979,7 @@ SCRAM-SHA-256$<iteration count>:&l The OID of the data type that corresponds to this table's row type, - if any; zero for indexes, sequences, and toast tables, which have + if any; zero for indexes, sequences, and TOAST tables, which have no pg_type entry @@ -2143,7 +2166,8 @@ SCRAM-SHA-256$<iteration count>:&l c = composite type, f = foreign table, p = partitioned table, - I = partitioned index + I = partitioned index, + g = property graph @@ -2629,7 +2653,6 @@ SCRAM-SHA-256$<iteration count>:&l Has the constraint been validated? - Currently, can be false only for foreign keys and CHECK constraints @@ -3164,7 +3187,7 @@ SCRAM-SHA-256$<iteration count>:&l datcollate text - LC_COLLATE for this database + LC_COLLATE for this database (ignored unless datlocprovider is c) @@ -4158,6 +4181,19 @@ SCRAM-SHA-256$<iteration count>:&l + + + fdwconnection oid + (references pg_proc.oid) + + + References a connection function that is responsible for creating a + connection string for a subscription when the subscription uses a + server based on this foreign-data wrapper. Zero if this foreign-data + wrapper does not support subscription connections. + + + fdwacl aclitem[] @@ -6311,6 +6347,498 @@ SCRAM-SHA-256$<iteration count>:&l + + <structname>pg_propgraph_element</structname> + + + pg_propgraph_element + + + + The catalog pg_propgraph_element stores + information about the vertices and edges of a property graph, collectively + called the elements of the property graph. + + + + <structname>pg_propgraph_element</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + pgepgid oid + (references pg_class.oid) + + + Reference to the property graph that this element belongs to + + + + + + pgerelid oid + (references pg_class.oid) + + + Reference to the table that contains the data for this property graph element + + + + + + pgealias name + + + The alias of the element. This is a unique identifier for the element + within the graph. It is set when the property graph is defined and + defaults to the name of the underlying element table. + + + + + + pgekind char + + + v for a vertex, e for an edge + + + + + + pgesrcvertexid oid + (references pg_propgraph_element.oid) + + + For an edge, a link to the source vertex. (Zero for a vertex.) + + + + + + pgedestvertexid oid + (references pg_propgraph_element.oid) + + + For an edge, a link to the destination vertex. (Zero for a vertex.) + + + + + + pgekey int2[] + (references pg_attribute.attnum) + + + An array of column numbers in the table referenced by + pgerelid that defines the key to use for this + element table. (This defaults to the primary key when the property + graph is created.) + + + + + + pgesrckey int2[] + (references pg_attribute.attnum) + + + For an edge, an array of column numbers in the table referenced by + pgerelid that defines the source key to use + for this element table. (Null for a vertex.) The combination of + pgesrckey and + pgesrcref creates the link between the edge + and the source vertex. + + + + + + pgesrcref int2[] + (references pg_attribute.attnum) + + + For an edge, an array of column numbers in the table reached via + pgesrcvertexid. (Null for a vertex.) The + combination of pgesrckey and + pgesrcref creates the link between the edge + and the source vertex. + + + + + + pgesrceqop oid[] + (references pg_operator.oid) + + + For an edge, an array of equality operators for + pgesrcref = + pgesrckey comparison. (Null for a vertex.) + + + + + + pgedestkey int2[] + (references pg_attribute.attnum) + + + For an edge, an array of column numbers in the table referenced by + pgerelid that defines the destination key to use + for this element table. (Null for a vertex.) The combination of + pgedestkey and + pgedestref creates the link between the edge + and the destination vertex. + + + + + + pgedestref int2[] + (references pg_attribute.attnum) + + + For an edge, an array of column numbers in the table reached via + pgedestvertexid. (Null for a vertex.) The + combination of pgedestkey and + pgedestref creates the link between the edge + and the destination vertex. + + + + + + pgedesteqop oid[] + (references pg_operator.oid) + + + For an edge, an array of equality operators for + pgedestref = + pgedestkey comparison. (Null for a vertex.) + + + + +
+
+ + + <structname>pg_propgraph_element_label</structname> + + + pg_propgraph_element_label + + + + The catalog pg_propgraph_element_label stores + information about which labels apply to which elements. + + + + <structname>pg_propgraph_element_label</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + pgellabelid oid + (references pg_propgraph_label.oid) + + + Reference to the label + + + + + + pgelelid oid + (references pg_propgraph_element.oid) + + + Reference to the element + + + + +
+
+ + + <structname>pg_propgraph_label</structname> + + + pg_propgraph_label + + + + The catalog pg_propgraph_label stores + information about the labels in a property graph. + + + + <structname>pg_propgraph_label</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + pglpgid oid + (references pg_class.oid) + + + Reference to the property graph that this label belongs to + + + + + + pgllabel name + + + The name of the label. This is unique among the labels in a graph. + + + + +
+
+ + + <structname>pg_propgraph_label_property</structname> + + + pg_propgraph_label_property + + + + The catalog pg_propgraph_label_property stores + information about the properties in a property graph that are specific to a + label. In particular, this stores the expression that defines the + property. + + + + <structname>pg_propgraph_label_property</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + plppropid oid + (references pg_propgraph_property.oid) + + + Reference to the property + + + + + + plpellabelid oid + (references pg_propgraph_element_label.oid) + + + Reference to the label (indirectly via + pg_propgraph_element_label, which then links + to pg_propgraph_label) + + + + + + plpexpr pg_node_tree + + + Expression tree (in nodeToString() representation) + for the property's definition. The expression references the table + reached via pg_propgraph_element_label and + pg_propgraph_element. + + + + +
+
+ + + <structname>pg_propgraph_property</structname> + + + pg_propgraph_property + + + + The catalog pg_propgraph_property stores + information about the properties in a property graph. This only stores + information that applies to a property throughout the graph, independent of + what label or element it is on. Additional information, including the + actual expressions that define the properties are in the catalog pg_propgraph_label_property. + + + + <structname>pg_propgraph_property</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + pgppgid oid + (references pg_class.oid) + + + Reference to the property graph that this property belongs to + + + + + + pgpname name + + + The name of the property. This is unique among the properties in a + graph. + + + + + + pgptypid oid + (references pg_type.oid) + + + The data type of this property. (This is required to be fixed for a + given property in a property graph, even if the property is defined + multiple times in different elements and labels.) + + + + + + pgptypmod int4 + + + typmod to be applied to the data type of this property. + (This is required to be fixed for a given property in a property graph, + even if the property is defined multiple times in different elements and + labels.) + + + + + + pgpcollation oid + (references pg_collation.oid) + + + The defined collation of this property, or zero if the property is not of + a collatable data type. (This is required to be fixed for a given + property in a property graph, even if the property is defined multiple + times in different elements and labels.) + + + + +
+
+ <structname>pg_publication</structname> @@ -6377,6 +6905,16 @@ SCRAM-SHA-256$<iteration count>:&l
+ + + puballsequences bool + + + If true, this publication automatically includes all sequences + in the database, including any that will be created in the future. + + + pubinsert bool @@ -6561,7 +7099,17 @@ SCRAM-SHA-256$<iteration count>:&l (references pg_class.oid) - Reference to relation + Reference to table + + + + + + prexcept bool + + + True if the table is excluded from the publication. See + EXCEPT. @@ -6669,6 +7217,60 @@ SCRAM-SHA-256$<iteration count>:&l + + + rngconstruct2 regproc + (references pg_proc.oid) + + + OID of the 2-argument range constructor function (lower and upper) + + + + + + rngconstruct3 regproc + (references pg_proc.oid) + + + OID of the 3-argument range constructor function (lower, upper, and + flags) + + + + + + rngmltconstruct0 regproc + (references pg_proc.oid) + + + OID of the 0-argument multirange constructor function (constructs empty + range) + + + + + + rngmltconstruct1 regproc + (references pg_proc.oid) + + + OID of the 1-argument multirange constructor function (constructs + multirange from single range, also used as cast function) + + + + + + rngmltconstruct2 regproc + (references pg_proc.oid) + + + OID of the 2-argument multirange constructor function (constructs + multirange from array of ranges) + + + rngcanonical regproc @@ -7977,7 +8579,7 @@ SCRAM-SHA-256$<iteration count>:&l Finish LSN of the transaction whose changes are to be skipped, if a valid - LSN; otherwise 0/0. + LSN; otherwise 0/0000000. @@ -8088,12 +8690,59 @@ SCRAM-SHA-256$<iteration count>:&l + + + subretaindeadtuples bool + + + If true, the detection of is + enabled and the information (e.g., dead tuples, commit timestamps, and + origins) on the subscriber that is useful for conflict detection is + retained. + + + + + + submaxretention int4 + + + The maximum duration (in milliseconds) for which information (e.g., dead + tuples, commit timestamps, and origins) useful for conflict detection can + be retained. + + + + + + subretentionactive bool + + + The retention status of information (e.g., dead tuples, commit + timestamps, and origins) useful for conflict detection. True if + retain_dead_tuples + is enabled, and the retention duration has not exceeded + max_retention_duration, + when defined. + + + + + + subserver oid + (references pg_foreign_server.oid) + + + Foreign server to use for the connection string. Zero if subconninfo is nonnull. + + + subconninfo text - Connection string to the upstream database + Connection string to the upstream database. NULL if subserver is nonzero. @@ -8118,6 +8767,16 @@ SCRAM-SHA-256$<iteration count>:&l + + + subwalrcvtimeout text + + + The wal_receiver_timeout + setting for the subscription's workers to use + + + subpublications text[] @@ -8155,16 +8814,19 @@ SCRAM-SHA-256$<iteration count>:&l - The catalog pg_subscription_rel contains the - state for each replicated relation in each subscription. This is a - many-to-many mapping. + The catalog pg_subscription_rel stores the + state of each replicated table and sequence for each subscription. This + is a many-to-many mapping. - This catalog only contains tables known to the subscription after running - either CREATE SUBSCRIPTION or - ALTER SUBSCRIPTION ... REFRESH - PUBLICATION. + This catalog contains tables and sequences known to the subscription + after running: + CREATE SUBSCRIPTION, + + ALTER SUBSCRIPTION ... REFRESH PUBLICATION, or + + ALTER SUBSCRIPTION ... REFRESH SEQUENCES. @@ -8198,7 +8860,7 @@ SCRAM-SHA-256$<iteration count>:&l (references pg_class.oid) - Reference to relation + Reference to table or sequence @@ -8207,12 +8869,20 @@ SCRAM-SHA-256$<iteration count>:&l srsubstate char - State code: + State code for the table or sequence. + + + State codes for tables: i = initialize, d = data is being copied, f = finished table copy, s = synchronized, r = ready (normal replication) + + + State codes for sequences: + i = initialize, + r = ready diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml index 5a0e97f6f3158..746e40bb9d2a0 100644 --- a/doc/src/sgml/charset.sgml +++ b/doc/src/sgml/charset.sgml @@ -100,7 +100,7 @@ initdb --locale=sv_SE LC_COLLATE - String sort order + String sort order (ignored unless the provider is libc) LC_CTYPE @@ -570,13 +570,13 @@ CREATE COLLATION CREATE COLLATION mycollation5 (provider = icu, deterministic = false, locale = 'en-US-u-kn-ks-level2'); -SELECT 'aB' = 'Ab' COLLATE mycollation5 as result; +SELECT 'aB' = 'Ab' COLLATE mycollation5 AS result; result -------- t (1 row) -SELECT 'N-45' < 'N-123' COLLATE mycollation5 as result; +SELECT 'N-45' < 'N-123' COLLATE mycollation5 AS result; result -------- t @@ -1763,7 +1763,7 @@ ORDER BY c COLLATE ebcdic; encodings), including single-byte character sets such as the ISO 8859 series and multiple-byte character sets such as EUC (Extended Unix - Code), UTF-8, and Mule internal code. All supported character sets + Code) and UTF-8. All supported character sets can be used transparently by clients, but a few are not supported for use within the server (that is, as a server-side encoding). The default character set is selected while @@ -1876,7 +1876,7 @@ ORDER BY c COLLATE ebcdic; GB18030 - National Standard + National Standard, version 2022 Chinese No No @@ -2045,15 +2045,6 @@ ORDER BY c COLLATE ebcdic; 1 ISO885916 - - MULE_INTERNAL - Mule internal code - Multilingual Emacs - Yes - No - 1–4 - - SJIS Shift JIS @@ -2205,7 +2196,7 @@ ORDER BY c COLLATE ebcdic; Not all client APIs support all the listed character sets. For example, the PostgreSQL - JDBC driver does not support MULE_INTERNAL, LATIN6, + JDBC driver does not support LATIN6, LATIN8, and LATIN10. @@ -2463,14 +2454,12 @@ RESET client_encoding; EUC_CN EUC_CN, - MULE_INTERNAL, UTF8 EUC_JP EUC_JP, - MULE_INTERNAL, SJIS, UTF8 @@ -2485,7 +2474,6 @@ RESET client_encoding; EUC_KR EUC_KR, - MULE_INTERNAL, UTF8 @@ -2493,7 +2481,6 @@ RESET client_encoding; EUC_TW EUC_TW, BIG5, - MULE_INTERNAL, UTF8 @@ -2511,7 +2498,6 @@ RESET client_encoding; ISO_8859_5 ISO_8859_5, KOI8R, - MULE_INTERNAL, UTF8, WIN866, WIN1251 @@ -2544,7 +2530,6 @@ RESET client_encoding; KOI8R KOI8R, ISO_8859_5, - MULE_INTERNAL, UTF8, WIN866, WIN1251 @@ -2559,14 +2544,12 @@ RESET client_encoding; LATIN1 LATIN1, - MULE_INTERNAL, UTF8 LATIN2 LATIN2, - MULE_INTERNAL, UTF8, WIN1250 @@ -2574,14 +2557,12 @@ RESET client_encoding; LATIN3 LATIN3, - MULE_INTERNAL, UTF8 LATIN4 LATIN4, - MULE_INTERNAL, UTF8 @@ -2621,23 +2602,6 @@ RESET client_encoding; UTF8 - - MULE_INTERNAL - MULE_INTERNAL, - BIG5, - EUC_CN, - EUC_JP, - EUC_KR, - EUC_TW, - ISO_8859_5, - KOI8R, - LATIN1 to LATIN4, - SJIS, - WIN866, - WIN1250, - WIN1251 - - SJIS not supported as a server encoding @@ -2668,7 +2632,6 @@ RESET client_encoding; WIN866, ISO_8859_5, KOI8R, - MULE_INTERNAL, UTF8, WIN1251 @@ -2683,7 +2646,6 @@ RESET client_encoding; WIN1250 WIN1250, LATIN2, - MULE_INTERNAL, UTF8 @@ -2692,7 +2654,6 @@ RESET client_encoding; WIN1251, ISO_8859_5, KOI8R, - MULE_INTERNAL, UTF8, WIN866 @@ -2775,31 +2736,16 @@ RESET client_encoding; BIG5EUC_TW - - big5_to_mic - BIG5 - MULE_INTERNAL - big5_to_utf8 BIG5 UTF8 - - euc_cn_to_mic - EUC_CN - MULE_INTERNAL - euc_cn_to_utf8 EUC_CN UTF8 - - euc_jp_to_mic - EUC_JP - MULE_INTERNAL - euc_jp_to_sjis EUC_JP @@ -2810,11 +2756,6 @@ RESET client_encoding; EUC_JP UTF8 - - euc_kr_to_mic - EUC_KR - MULE_INTERNAL - euc_kr_to_utf8 EUC_KR @@ -2825,11 +2766,6 @@ RESET client_encoding; EUC_TW BIG5 - - euc_tw_to_mic - EUC_TW - MULE_INTERNAL - euc_tw_to_utf8 EUC_TW @@ -2870,21 +2806,11 @@ RESET client_encoding; LATIN10 UTF8 - - iso_8859_1_to_mic - LATIN1 - MULE_INTERNAL - iso_8859_1_to_utf8 LATIN1 UTF8 - - iso_8859_2_to_mic - LATIN2 - MULE_INTERNAL - iso_8859_2_to_utf8 LATIN2 @@ -2895,21 +2821,11 @@ RESET client_encoding; LATIN2 WIN1250 - - iso_8859_3_to_mic - LATIN3 - MULE_INTERNAL - iso_8859_3_to_utf8 LATIN3 UTF8 - - iso_8859_4_to_mic - LATIN4 - MULE_INTERNAL - iso_8859_4_to_utf8 LATIN4 @@ -2920,11 +2836,6 @@ RESET client_encoding; ISO_8859_5 KOI8R - - iso_8859_5_to_mic - ISO_8859_5 - MULE_INTERNAL - iso_8859_5_to_utf8 ISO_8859_5 @@ -2970,11 +2881,6 @@ RESET client_encoding; KOI8R ISO_8859_5 - - koi8_r_to_mic - KOI8R - MULE_INTERNAL - koi8_r_to_utf8 KOI8R @@ -2995,91 +2901,11 @@ RESET client_encoding; KOI8U UTF8 - - mic_to_big5 - MULE_INTERNAL - BIG5 - - - mic_to_euc_cn - MULE_INTERNAL - EUC_CN - - - mic_to_euc_jp - MULE_INTERNAL - EUC_JP - - - mic_to_euc_kr - MULE_INTERNAL - EUC_KR - - - mic_to_euc_tw - MULE_INTERNAL - EUC_TW - - - mic_to_iso_8859_1 - MULE_INTERNAL - LATIN1 - - - mic_to_iso_8859_2 - MULE_INTERNAL - LATIN2 - - - mic_to_iso_8859_3 - MULE_INTERNAL - LATIN3 - - - mic_to_iso_8859_4 - MULE_INTERNAL - LATIN4 - - - mic_to_iso_8859_5 - MULE_INTERNAL - ISO_8859_5 - - - mic_to_koi8_r - MULE_INTERNAL - KOI8R - - - mic_to_sjis - MULE_INTERNAL - SJIS - - - mic_to_windows_1250 - MULE_INTERNAL - WIN1250 - - - mic_to_windows_1251 - MULE_INTERNAL - WIN1251 - - - mic_to_windows_866 - MULE_INTERNAL - WIN866 - sjis_to_euc_jp SJIS EUC_JP - - sjis_to_mic - SJIS - MULE_INTERNAL - sjis_to_utf8 SJIS @@ -3285,11 +3111,6 @@ RESET client_encoding; WIN1250 LATIN2 - - windows_1250_to_mic - WIN1250 - MULE_INTERNAL - windows_1250_to_utf8 WIN1250 @@ -3305,11 +3126,6 @@ RESET client_encoding; WIN1251 KOI8R - - windows_1251_to_mic - WIN1251 - MULE_INTERNAL - windows_1251_to_utf8 WIN1251 @@ -3340,11 +3156,6 @@ RESET client_encoding; WIN866 KOI8R - - windows_866_to_mic - WIN866 - MULE_INTERNAL - windows_866_to_utf8 WIN866 diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 832b616a7bbff..e4e65f8feb1d1 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -305,7 +305,7 @@ include_dir directory Specifies which database user name(s) this record matches. The value all specifies that it matches all users. Otherwise, this is either the name of a specific - database user, a regular expression (when starting with a slash + database user, a regular expression when starting with a slash (/), or a group name preceded by +. (Recall that there is no real distinction between users and groups in PostgreSQL; a + mark really means @@ -616,16 +616,6 @@ include_dir directory - - radius - - - Authenticate using a RADIUS server. See for details. - - - - cert @@ -889,16 +879,16 @@ host all all 192.168.0.0/16 ident map=omicro # list of names of administrators. Passwords are required in all cases. # # TYPE DATABASE USER ADDRESS METHOD -local sameuser all md5 -local all /^.*helpdesk$ md5 -local all @admins md5 -local all +support md5 +local sameuser all scram-sha-256 +local all /^.*helpdesk$ scram-sha-256 +local all @admins scram-sha-256 +local all +support scram-sha-256 # The last two lines above can be combined into a single line: -local all @admins,+support md5 +local all @admins,+support scram-sha-256 # The database column can also use lists and file names: -local db1,db2,@demodbs all md5 +local db1,db2,@demodbs all scram-sha-256 @@ -1003,8 +993,9 @@ local db1,db2,@demodbs all md5 the remainder of the field is treated as a regular expression. (See for details of PostgreSQL's regular expression syntax.) The regular - expression can include a single capture, or parenthesized subexpression, - which can then be referenced in the database-username + expression can include a single capture, or parenthesized subexpression. + The portion of the system user name that matched the capture can then + be referenced in the database-username field as \1 (backslash-one). This allows the mapping of multiple user names in a single line, which is particularly useful for simple syntax substitutions. For example, these entries @@ -1022,12 +1013,11 @@ mymap /^(.*)@otherdomain\.com$ guest If the database-username field starts with a slash (/), the remainder of the field is treated - as a regular expression (see - for details of PostgreSQL's regular - expression syntax). It is not possible to use \1 - to use a capture from regular expression on - system-username for a regular expression - on database-username. + as a regular expression. + When the database-username field is a regular + expression, it is not possible to use \1 within it to + refer to a capture from the system-username + field. @@ -1127,12 +1117,6 @@ omicron bryanh guest1 relies on an LDAP authentication server. - - - RADIUS authentication, which - relies on a RADIUS authentication server. - - Certificate authentication, which @@ -2096,118 +2080,6 @@ host ... ldap ldapbasedn="dc=example,dc=net" - - RADIUS Authentication - - - RADIUS - - - - This authentication method operates similarly to - password except that it uses RADIUS - as the password verification method. RADIUS is used only to validate - the user name/password pairs. Therefore the user must already - exist in the database before RADIUS can be used for - authentication. - - - - When using RADIUS authentication, an Access Request message will be sent - to the configured RADIUS server. This request will be of type - Authenticate Only, and include parameters for - user name, password (encrypted) and - NAS Identifier. The request will be encrypted using - a secret shared with the server. The RADIUS server will respond to - this request with either Access Accept or - Access Reject. There is no support for RADIUS accounting. - - - - Multiple RADIUS servers can be specified, in which case they will - be tried sequentially. If a negative response is received from - a server, the authentication will fail. If no response is received, - the next server in the list will be tried. To specify multiple - servers, separate the server names with commas and surround the list - with double quotes. If multiple servers are specified, the other - RADIUS options can also be given as comma-separated lists, to provide - individual values for each server. They can also be specified as - a single value, in which case that value will apply to all servers. - - - - The following configuration options are supported for RADIUS: - - - radiusservers - - - The DNS names or IP addresses of the RADIUS servers to connect to. - This parameter is required. - - - - - - radiussecrets - - - The shared secrets used when talking securely to the RADIUS - servers. This must have exactly the same value on the PostgreSQL - and RADIUS servers. It is recommended that this be a string of - at least 16 characters. This parameter is required. - - - The encryption vector used will only be cryptographically - strong if PostgreSQL is built with support for - OpenSSL. In other cases, the transmission to the - RADIUS server should only be considered obfuscated, not secured, and - external security measures should be applied if necessary. - - - - - - - - radiusports - - - The port numbers to connect to on the RADIUS servers. If no port - is specified, the default RADIUS port (1812) - will be used. - - - - - - radiusidentifiers - - - The strings to be used as NAS Identifier in the - RADIUS requests. This parameter can be used, for example, to - identify which database cluster the user is attempting to connect - to, which can be useful for policy matching on - the RADIUS server. If no identifier is specified, the default - postgresql will be used. - - - - - - - - - If it is necessary to have a comma or whitespace in a RADIUS parameter - value, that can be done by putting double quotes around the value, but - it is tedious because two layers of double-quoting are now required. - An example of putting whitespace into RADIUS secret strings is: - -host ... radius radiusservers="server1,server2" radiussecrets="""secret one"",""secret two""" - - - - Certificate Authentication @@ -2532,6 +2404,45 @@ host ... radius radiusservers="server1,server2" radiussecrets="""secret one"","" + + + validator.option + + + + Validator modules may define + additional configuration options for oauth + HBA entries. These validator-specific options are accessible via the + validator.* "namespace". For example, a module may + register the validator.foo and + validator.bar options and define their effects on + authentication. + + + The name, syntax, and behavior of each option + are not determined by PostgreSQL; consult the + documentation for the validator module in use. + + + + A limitation of the current implementation is that unrecognized + option names will not be caught until + connection time. A pg_ctl reload will succeed, but + matching connections will fail: + +LOG: connection received: host=[local] +WARNING: unrecognized authentication option name: "validator.bad" +DETAIL: The installed validator module ("my_validator") did not define an option named "bad". +HINT: All OAuth connections matching this line will fail. Correct the option and reload the server configuration. +CONTEXT: line 2 of configuration file "data/pg_hba.conf" + + Use caution when making changes to validator-specific HBA options in + production systems. + + + + + map diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index ca2a567b2b19f..73cc04123303d 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -18,6 +18,8 @@ Setting Parameters + GUC + Parameter Names and Values @@ -505,9 +507,10 @@ include_dir 'conf.d' In addition to the postgresql.conf file already mentioned, PostgreSQL uses - two other manually-edited configuration files, which control + three other manually-edited configuration files, which control client authentication (their use is discussed in ). By default, all three + linkend="client-authentication"/>) and SSL host configuration. + By default, all four configuration files are stored in the database cluster's data directory. The parameters described in this section allow the configuration files to be placed elsewhere. (Doing so can ease @@ -577,6 +580,22 @@ include_dir 'conf.d' + + hosts_file (string) + + hosts_file configuration parameter + + + + + Specifies the configuration file for host-based SSL configuration + (customarily called pg_hosts.conf). + This parameter can only be set at server start. See also + . + + + + external_pid_file (string) @@ -618,10 +637,11 @@ include_dir 'conf.d' If you wish, you can specify the configuration file names and locations individually using the parameters config_file, - hba_file and/or ident_file. + hba_file, ident_file and/or + hosts_file. config_file can only be specified on the postgres command line, but the others can be - set within the main configuration file. If all three parameters plus + set within the main configuration file. If all four parameters plus data_directory are explicitly set, then it is not necessary to specify or PGDATA. @@ -1155,6 +1175,28 @@ include_dir 'conf.d' + + password_expiration_warning_threshold (integer) + + password_expiration_warning_threshold configuration parameter + + + + + When this parameter is greater than zero, the server will emit a + WARNING upon successful password authentication if + less than this amount of time remains until the authenticated role's + password expires. Note that a role's password only expires if a date + was specified in a VALID UNTIL clause for + CREATE ROLE or ALTER ROLE. If + this value is specified without units, it is taken as seconds. The + default is 7 days. This parameter can only be set in the + postgresql.conf file or on the server command + line. + + + + md5_password_warnings (boolean) @@ -1164,7 +1206,8 @@ include_dir 'conf.d' Controls whether a WARNING about MD5 password - deprecation is produced when a CREATE ROLE or + deprecation is produced upon successful MD5 password authentication or + when a CREATE ROLE or ALTER ROLE statement sets an MD5-encrypted password. The default value is on. @@ -1234,7 +1277,7 @@ include_dir 'conf.d' - The library/libraries to use for validating OAuth connection tokens. If + Sets the library/libraries to use for validating OAuth connection tokens. If only one validator library is provided, it will be used by default for any OAuth connections; otherwise, all oauth HBA entries @@ -1400,7 +1443,7 @@ include_dir 'conf.d' Specifies a list of cipher suites that are allowed by connections using TLS version 1.3. Multiple cipher suites can be - specified by using a colon separated list. If left blank, the default + specified by using a colon-separated list. If left blank, the default set of cipher suites in OpenSSL will be used. @@ -1539,6 +1582,15 @@ include_dir 'conf.d' The default is X25519:prime256v1. + + + X25519 is not allowed when + OpenSSL is configured for FIPS mode and + must be removed from the server configuration when FIPS mode is + enabled. + + + OpenSSL names for the most common curves are: @@ -1680,7 +1732,7 @@ include_dir 'conf.d' This parameter determines whether the passphrase command set by ssl_passphrase_command will also be called during a configuration reload if a key file needs a passphrase. If this - parameter is off (the default), then + parameter is off (the default), then ssl_passphrase_command will be ignored during a reload and the SSL configuration will not be reloaded if a passphrase is needed. That setting is appropriate for a command that requires a @@ -1688,12 +1740,37 @@ include_dir 'conf.d' running. Setting this parameter to on might be appropriate if the passphrase is obtained from a file, for example. + + This parameter must be set to on when running on + Windows since all connections + will perform a configuration reload due to the different process model + of that platform. + This parameter can only be set in the postgresql.conf file or on the server command line. + + + ssl_sni (boolean) + + ssl_sni configuration parameter + + + + + Enables SNI configuration for SSL connections. When set to on + host configuration from is used, see + for more details. + + + This parameter can only be set in the postgresql.conf + file or on the server command line. The default is off. + + + @@ -1760,7 +1837,8 @@ include_dir 'conf.d' Controls whether huge pages are requested for the main shared memory area. Valid values are try (the default), - on, and off. With + on, and off. + This parameter can only be set at server start. With huge_pages set to try, the server will try to request huge pages, but fall back to the default if that fails. With on, failure to request huge pages @@ -2068,7 +2146,7 @@ include_dir 'conf.d' Specifies the maximum amount of memory to be used by logical decoding, before some of the decoded changes are written to local disk. This - limits the amount of memory used by logical streaming replication + limits the amount of memory used by streaming logical replication connections. It defaults to 64 megabytes (64MB). Since each replication connection only uses a single buffer of this size, and an installation normally doesn't have many such connections @@ -2273,6 +2351,7 @@ include_dir 'conf.d' platform, is generally discouraged because it typically requires non-default kernel settings to allow for large allocations (see ). + This parameter can only be set at server start. @@ -2300,6 +2379,7 @@ include_dir 'conf.d' however, it may be useful for debugging, when the pg_dynshmem directory is stored on a RAM disk, or when other shared memory facilities are not available. + This parameter can only be set at server start. @@ -2363,7 +2443,7 @@ include_dir 'conf.d' - + file_copy_method (enum) file_copy_method configuration parameter @@ -2401,6 +2481,43 @@ include_dir 'conf.d' + + file_extend_method (enum) + + file_extend_method configuration parameter + + + + + Specifies the method used to extend data files during bulk operations + such as COPY. The first available option is used as + the default, depending on the operating system: + + + + posix_fallocate (Unix) uses the standard POSIX + interface for allocating disk space, but is missing on some systems. + If it is present but the underlying file system doesn't support it, + this option silently falls back to write_zeros. + Current versions of BTRFS are known to disable compression when + this option is used. + This is the default on systems that have the function. + + + + + write_zeros extends files by writing out blocks + of zero bytes. This is the default on systems that don't have the + function posix_fallocate. + + + + The write_zeros method is always used when data + files are extended by 8 blocks or fewer. + + + + max_notify_queue_pages (integer) @@ -2413,6 +2530,7 @@ include_dir 'conf.d' / queue. The default value is 1048576. For 8 KB pages it allows to consume up to 8 GB of disk space. + This parameter can only be set at server start. @@ -2432,8 +2550,8 @@ include_dir 'conf.d' - Sets the maximum number of open files each server subprocess is - allowed to open simultaneously; files already opened in the + Sets the maximum number of files each server subprocess is + allowed to have open simultaneously; files already opened in the postmaster are not counted toward this limit. The default is one thousand files. @@ -2452,6 +2570,78 @@ include_dir 'conf.d' + + Timing + + + + timing_clock_source (enum) + + timing_clock_source configuration parameter + + + + + Selects the method for making timing measurements using the OS or + specialized CPU instructions. Possible values are: + + + + auto (automatically chooses TSC + clock source on supported x86-64 CPUs, otherwise uses the OS system + clock) + + + + + system (measures timing using the OS system clock) + + + + + tsc (measures timing with a CPU instruction, e.g. + using RDTSC/RDTSCP on x86-64) + + + + The default is auto. Only superusers can change this + setting. Changing the setting during query execution is not recommended + and may cause interval timings to jump significantly or produce negative + values. + + + + Time-Stamp Counter + TSC + + TSC + If enabled, the TSC clock source, named after the + Time-Stamp Counter on x86-64, will use specialized CPU instructions when + measuring time intervals. This lowers timing overhead compared to reading + the OS system clock, and reduces the measurement error on top of the + actual runtime, for example with EXPLAIN ANALYZE. + + + RDTSC + On x86-64 CPUs the TSC clock source utilizes the + RDTSC instruction for EXPLAIN ANALYZE. + For timings that require higher precision the RDTSCP + instruction is used, which avoids inaccuracies due to CPU instruction + re-ordering. Use of the TSC clock source is not + supported on older x86-64 CPUs and other architectures, and is not + advised on systems that utilize an emulated TSC, as it + is likely slower than the system clock source. + + + To help decide which clock source to use you can run the + utility to check TSC + availability, and perform timing measurements. + + + + + + Background Writer @@ -2647,7 +2837,7 @@ include_dir 'conf.d' Higher values will have the most impact on higher latency storage where queries otherwise experience noticeable I/O stalls and on devices with high IOPs. Unnecessarily high values may increase I/O - latency for all queries on the system + latency for all queries on the system. @@ -2694,9 +2884,9 @@ include_dir 'conf.d' Controls the largest I/O size in operations that combine I/O, and silently limits the user-settable parameter io_combine_limit. - This parameter can only be set in - the postgresql.conf file or on the server - command line. + This parameter can only be set at server start. + If this value is specified without units, it is taken as blocks, + that is BLCKSZ bytes, typically 8kB. The maximum possible size depends on the operating system and block size, but is typically 1MB on Unix and 128kB on Windows. The default is 128kB. @@ -2716,6 +2906,8 @@ include_dir 'conf.d' higher than the io_max_combine_limit parameter, the lower value will silently be used instead, so both may need to be raised to increase the I/O size. + If this value is specified without units, it is taken as blocks, + that is BLCKSZ bytes, typically 8kB. The maximum possible size depends on the operating system and block size, but is typically 1MB on Unix and 128kB on Windows. The default is 128kB. @@ -2779,6 +2971,7 @@ include_dir 'conf.d' + The default is worker. This parameter can only be set at server start. @@ -2786,16 +2979,75 @@ include_dir 'conf.d' - - io_workers (int) + + io_min_workers (integer) + + io_min_workers configuration parameter + + + + + Sets the minimum number of I/O worker processes. The default is + 2. This parameter can only be set in the + postgresql.conf file or on the server command + line. + + + Only has an effect if is set to + worker. + + + + + io_max_workers (integer) + + io_max_workers configuration parameter + + + + + Sets the maximum number of I/O worker processes. The default is + 8. This parameter can only be set in the + postgresql.conf file or on the server command + line. + + + Only has an effect if is set to + worker. + + + + + io_worker_idle_timeout (integer) - io_workers configuration parameter + io_worker_idle_timeout configuration parameter - Selects the number of I/O worker processes to use. The default is - 3. This parameter can only be set in the + Sets the time after which entirely idle I/O worker processes exit, reducing the + size of pool to match demand. The default is 1 minute. This + parameter can only be set in the + postgresql.conf file or on the server command + line. + + + Only has an effect if is set to + worker. + + + + + io_worker_launch_interval (integer) + + io_worker_launch_interval configuration parameter + + + + + Sets the minimum time before another I/O worker can be launched. This avoids + creating too many for an unsustained burst of activity. The default is 100ms. + This parameter can only be set in the postgresql.conf file or on the server command line. @@ -2834,6 +3086,7 @@ include_dir 'conf.d' When changing this value, consider also adjusting , + , , and . @@ -2893,7 +3146,8 @@ include_dir 'conf.d' Sets the maximum number of parallel workers that can be started by a single utility command. Currently, the parallel utility commands that support the use of parallel workers are - CREATE INDEX when building a B-tree or BRIN index, + CREATE INDEX when building a B-tree, + GIN, or BRIN index, and VACUUM without FULL option. Parallel workers are taken from the pool of processes established by , limited @@ -3032,6 +3286,17 @@ include_dir 'conf.d' many UPDATE and DELETE statements are executed. + + It is important to note that when wal_level is set to + replica, the effective WAL level can automatically change + based on the presence of + logical replication slots. The system automatically increases the + effective WAL level to logical when creating the first + logical replication slot, and decreases it back to replica + when dropping or invalidating the last logical replication slot. The current + effective WAL level can be monitored through + parameter. + In releases prior to 9.6, this parameter also allowed the values archive and hot_standby. @@ -3397,8 +3662,9 @@ include_dir 'conf.d' This parameter enables compression of WAL using the specified compression method. When enabled, the PostgreSQL - server compresses full page images written to WAL when - is on or during a base backup. + server compresses full page images written to WAL (e.g. when + is on, during a base backup, + etc.). A compressed page image will be decompressed during WAL replay. The supported methods are pglz, lz4 (if PostgreSQL @@ -3785,7 +4051,7 @@ include_dir 'conf.d' difference between the two modes, but when set to always the WAL archiver is enabled also during archive recovery or standby mode. In always mode, all files restored from the archive - or streamed with streaming replication will be archived (again). See + or streamed with streaming physical replication will be archived (again). See for details. @@ -3891,7 +4157,7 @@ include_dir 'conf.d' full files. Therefore, it is unwise to use a very short archive_timeout — it will bloat your archive storage. archive_timeout settings of a minute or so are - usually reasonable. You should consider using streaming replication, + usually reasonable. You should consider using streaming physical replication, instead of archiving, if you want data to be copied off the primary server more quickly than that. If this value is specified without units, it is taken as seconds. @@ -3916,7 +4182,7 @@ include_dir 'conf.d' This section describes the settings that apply to recovery in general, - affecting crash recovery, streaming replication and archive-based + affecting crash recovery, streaming physical replication and archive-based replication. @@ -3960,6 +4226,7 @@ include_dir 'conf.d' blocks to prefetch. If this value is specified without units, it is taken as bytes. The default is 512kB. + This parameter can only be set at server start. @@ -4026,7 +4293,7 @@ include_dir 'conf.d' The local shell command to execute to retrieve an archived segment of the WAL file series. This parameter is required for archive recovery, - but optional for streaming replication. + but optional for streaming physical replication. Any %f in the string is replaced by the name of the file to retrieve from the archive, and any %p is replaced by the copy destination path name @@ -4034,7 +4301,7 @@ include_dir 'conf.d' (The path name is relative to the current working directory, i.e., the cluster's data directory.) Any %r is replaced by the name of the file containing the - last valid restart point. That is the earliest file that must be kept + last valid restartpoint. That is the earliest file that must be kept to allow a restore to be restartable, so this information can be used to truncate the archive to just the minimum required to support restarting from the current restore. %r is typically only @@ -4049,7 +4316,7 @@ include_dir 'conf.d' names that are not present in the archive; it must return nonzero when so asked. Examples: -restore_command = 'cp /mnt/server/archivedir/%f "%p"' +restore_command = 'cp "/mnt/server/archivedir/%f" "%p"' restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows An exception is that if the command was terminated by a signal (other @@ -4079,7 +4346,7 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows cleaning up old archived WAL files that are no longer needed by the standby server. Any %r is replaced by the name of the file containing the - last valid restart point. + last valid restartpoint. That is the earliest file that must be kept to allow a restore to be restartable, and so all files earlier than %r may be safely removed. @@ -4088,7 +4355,7 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows The module is often used in archive_cleanup_command for single-standby configurations, for example: -archive_cleanup_command = 'pg_archivecleanup /mnt/server/archivedir %r' +archive_cleanup_command = 'pg_archivecleanup /mnt/server/archivedir "%r"' Note however that if multiple standby servers are restoring from the same archive directory, you will need to ensure that you do not delete WAL files until they are no longer needed by any of the servers. @@ -4123,7 +4390,7 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows recovery_end_command is to provide a mechanism for cleanup following replication or recovery. Any %r is replaced by the name of the file containing the - last valid restart point, like in . + last valid restartpoint, like in . If the command returns a nonzero exit status then a warning log @@ -4236,6 +4503,21 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows The precise stopping point is also influenced by . + + + The value can be specified as either a 32-bit transaction ID or a 64-bit + transaction ID (consisting of an epoch and a 32-bit ID), such as the + value returned by pg_current_xact_id(). When a + 64-bit transaction ID is provided, only its 32-bit transaction ID + portion is used as the recovery target. For example, the values + 4294968296 (epoch 1) and 8589935592 (epoch 2) both refer to the same + 32-bit transaction ID, 1000. + + + + The effective transaction ID (the 32-bit portion) must be greater than + or equal to 3. + @@ -4452,15 +4734,16 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows Replication - These settings control the behavior of the built-in - streaming replication feature (see - ), and the built-in - logical replication feature (see + These settings control the behavior of + streaming replication, + both physical replication + (see ) and + logical replication (see ). - For streaming replication, servers will be either a + For physical replication, servers will be either a primary or a standby server. Primaries can send data, while standbys are always receivers of replicated data. When cascading replication (see ) is used, standby servers @@ -4524,6 +4807,21 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows + + max_repack_replication_slots (integer) + + max_repack_replication_slots configuration parameter + + + + + Specifies the maximum number of replication slots for use of + the REPACK command. The default is 5. + This parameter can only be set at server start. + + + + max_replication_slots (integer) @@ -4616,10 +4914,12 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows - Invalidate replication slots that have remained idle longer than this - duration. If this value is specified without units, it is taken as - minutes. A value of zero (the default) disables the idle timeout - invalidation mechanism. This parameter can only be set in the + Invalidate replication slots that have remained inactive (not used by + a replication connection) + for longer than this duration. + If this value is specified without units, it is taken as seconds. + A value of zero (the default) disables the idle timeout + invalidation mechanism. This parameter can only be set in the postgresql.conf file or on the server command line. @@ -4675,56 +4975,59 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows - - track_commit_timestamp (boolean) + + wal_sender_shutdown_timeout (integer) - track_commit_timestamp configuration parameter + wal_sender_shutdown_timeout configuration parameter - Record commit time of transactions. This parameter - can only be set in postgresql.conf file or on the server - command line. The default value is off. + Specifies the maximum time the server waits during shutdown for all + WAL data to be replicated to the receiver. If this value is specified + without units, it is taken as milliseconds. A value of + -1 (the default) disables the timeout mechanism. + + + When replication is in use, the sending server normally waits until + all WAL data has been transferred to the receiver before completing + shutdown. This helps keep sender and receiver in sync after shutdown, + which is especially important for physical replication switchovers, + but it can delay shutdown. + + + If this parameter is set, the server stops waiting and completes + shutdown when the timeout expires. This can shorten shutdown time, + for example, when replication is slow on high-latency networks or + when a logical replication apply worker is blocked waiting for locks. + However, in this case the sender and receiver may be out of sync after + shutdown. + + + This parameter can be set in primary_conninfo and + in the CONNECTION clause of + CREATE SUBSCRIPTION (for example, include + options=-cwal_sender_shutdown_timeout=10s in the + connection string), allowing different timeouts per replication + connection. For example, when both physical and logical replication + are used, it can be disabled for physical replication (e.g., for + switchovers) while enabled for logical replication to limit shutdown + time. - - synchronized_standby_slots (string) + + track_commit_timestamp (boolean) - synchronized_standby_slots configuration parameter + track_commit_timestamp configuration parameter - A comma-separated list of streaming replication standby server slot names - that logical WAL sender processes will wait for. Logical WAL sender processes - will send decoded changes to plugins only after the specified replication - slots confirm receiving WAL. This guarantees that logical replication - failover slots do not consume changes until those changes are received - and flushed to corresponding physical standbys. If a - logical replication connection is meant to switch to a physical standby - after the standby is promoted, the physical replication slot for the - standby should be listed here. Note that logical replication will not - proceed if the slots specified in the - synchronized_standby_slots do not exist or are invalidated. - Additionally, the replication management functions - - pg_replication_slot_advance, - - pg_logical_slot_get_changes, and - - pg_logical_slot_peek_changes, - when used with logical failover slots, will block until all - physical slots specified in synchronized_standby_slots have - confirmed WAL receipt. - - - The standbys corresponding to the physical replication slots in - synchronized_standby_slots must configure - sync_replication_slots = true so they can receive - logical failover slot changes from the primary. + Record commit time of transactions. + This parameter can only be set at server start. + The default value is off. @@ -4877,6 +5180,45 @@ ANY num_sync ( + synchronized_standby_slots (string) + + synchronized_standby_slots configuration parameter + + + + + A comma-separated list of streaming replication standby server slot names + that logical WAL sender processes will wait for. Logical WAL sender processes + will send decoded changes to plugins only after the specified replication + slots confirm receiving WAL. This guarantees that logical replication + failover slots do not consume changes until those changes are received + and flushed to corresponding physical standbys. If a + logical replication connection is meant to switch to a physical standby + after the standby is promoted, the physical replication slot for the + standby should be listed here. Note that logical replication will not + proceed if the slots specified in the + synchronized_standby_slots do not exist or are invalidated. + Additionally, the replication management functions + + pg_replication_slot_advance, + + pg_logical_slot_get_changes, and + + pg_logical_slot_peek_changes, + when used with logical failover slots, will block until all + physical slots specified in synchronized_standby_slots have + confirmed WAL receipt. + + + The standbys corresponding to the physical replication slots in + synchronized_standby_slots must configure + sync_replication_slots = true so they can receive + logical failover slot changes from the primary. + + @@ -4889,7 +5231,7 @@ ANY num_sync ( standby server that is - to receive replication data. Their values on the primary server + to receive physical replication data. Their values on the primary server are irrelevant. @@ -4961,6 +5303,8 @@ ANY num_sync ( num_sync ( . max_standby_streaming_delay applies when WAL data is - being received via streaming replication. + being received via streaming physical replication. If this value is specified without units, it is taken as milliseconds. The default is 30 seconds. A value of -1 allows the standby to wait forever for conflicting @@ -5147,9 +5491,6 @@ ANY num_sync ( num_sync ( num_sync ( num_sync ( num_sync ( num_sync ( num_sync ( + enable_eager_aggregate (boolean) + + enable_eager_aggregate configuration parameter + + + + + Enables or disables the query planner's ability to partially push + aggregation past a join, and finalize it once all the relations are + joined. The default is on. + + + + enable_gathermerge (boolean) @@ -5764,7 +6124,7 @@ ANY num_sync ( + enable_self_join_elimination (boolean) enable_self_join_elimination configuration parameter @@ -5902,24 +6262,24 @@ ANY num_sync ( ( + min_eager_agg_group_size (floating point) + + min_eager_agg_group_size configuration parameter + + + + + Sets the minimum average group size required to consider applying + eager aggregation. This helps avoid the overhead of eager + aggregation when it does not offer significant row count reduction. + The default is 8. + + + + jit_above_cost (floating point) @@ -6356,8 +6732,8 @@ ANY num_sync ( ). - The default is on. + The default is off. @@ -7001,8 +7377,7 @@ local0.* /var/log/postgresql determines the program name used to identify PostgreSQL messages in the log. The default is PostgreSQL. - This parameter can only be set in the postgresql.conf - file or on the server command line. + This parameter can only be set at server start. @@ -7015,27 +7390,58 @@ local0.* /var/log/postgresql - log_min_messages (enum) + log_min_messages (string) log_min_messages configuration parameter - Controls which message - levels are written to the server log. - Valid values are DEBUG5, DEBUG4, - DEBUG3, DEBUG2, DEBUG1, - INFO, NOTICE, WARNING, - ERROR, LOG, FATAL, and - PANIC. Each level includes all the levels that - follow it. The later the level, the fewer messages are sent - to the log. The default is WARNING. Note that - LOG has a different rank here than in + Controls which + message levels + are written to the server log. The value is a comma-separated + list of zero or more + process type:level + entries and exactly one mandatory + level entry, + which becomes the default for process types not listed. + Valid process types are listed in the table below. + + archiver + autovacuum + backend + bgworker + bgwriter + checkpointer + checksums + ioworker + postmaster + slotsyncworker + startup + syslogger + walreceiver + walsender + walsummarizer + walwriter + + Valid level values are DEBUG5, + DEBUG4, DEBUG3, DEBUG2, + DEBUG1, INFO, NOTICE, + WARNING, ERROR, LOG, + FATAL, and PANIC. Each level includes + all the levels that follow it. The later the level, the fewer messages are sent + to the log. The default is WARNING, which + applies that level to all process types. + Note that LOG has a different rank here than in . Only superusers and users with the appropriate SET privilege can change this setting. + + Example: To log walsender and autovacuum + at level DEBUG1 and everything else at ERROR, + set log_min_messages to error, walsender:debug1, autovacuum:debug1. + @@ -7376,6 +7782,11 @@ local0.* /var/log/postgresql + debug_print_raw_parse (boolean) + + debug_print_raw_parse configuration parameter + + debug_print_parse (boolean) debug_print_parse configuration parameter @@ -7394,8 +7805,8 @@ local0.* /var/log/postgresql These parameters enable various debugging output to be emitted. - When set, they print the resulting parse tree, the query rewriter - output, or the execution plan for each executed query. + When set, they print the resulting raw parse tree, the parse tree, the query + rewriter output, or the execution plan for each executed query. These messages are emitted at LOG message level, so by default they will appear in the server log but will not be sent to the client. You can change that by adjusting @@ -7415,7 +7826,8 @@ local0.* /var/log/postgresql When set, debug_pretty_print indents the messages - produced by debug_print_parse, + produced by debug_print_raw_parse, + debug_print_parse, debug_print_rewritten, or debug_print_plan. This results in more readable but much longer output than the compact format used when @@ -7433,17 +7845,44 @@ local0.* /var/log/postgresql - Causes each action executed by autovacuum to be logged if it ran for at + Causes vacuum action executed by autovacuum to be logged if it ran for at least the specified amount of time. Setting this to zero logs - all autovacuum actions. -1 disables logging autovacuum - actions. If this value is specified without units, it is taken as milliseconds. - For example, if you set this to - 250ms then all automatic vacuums and analyzes that run + all vacuum actions by autovacuum. -1 disables logging + vacuum actions by autovacuum. If this value is specified without units, + it is taken as milliseconds. For example, if you set this to + 250ms then all automatic vacuums that run 250ms or longer will be logged. In addition, when this parameter is set to any value other than -1, a message will be - logged if an autovacuum action is skipped due to a conflicting lock or a + logged if a vacuum action by autovacuum is skipped due to a conflicting lock or a concurrently dropped relation. The default is 10min. - Enabling this parameter can be helpful in tracking autovacuum activity. + Enabling this parameter can be helpful in tracking vacuum activity by autovacuum. + This parameter can only be set in the postgresql.conf + file or on the server command line; but the setting can be overridden for + individual tables by changing table storage parameters. + + + + + + log_autoanalyze_min_duration (integer) + + log_autoanalyze_min_duration + configuration parameter + + + + + Causes analyze action executed by autovacuum to be logged if it ran for at + least the specified amount of time. Setting this to zero logs + all analyze actions by autovacuum. -1 disables logging + analyze actions by autovacuum. If this value is specified without units, + it is taken as milliseconds. For example, if you set this to + 250ms then all automatic analyzes that run + 250ms or longer will be logged. In addition, when this parameter is + set to any value other than -1, a message will be + logged if an analyze action by autovacuum is skipped due to a conflicting lock or a + concurrently dropped relation. The default is 10min. + Enabling this parameter can be helpful in tracking analyze activity by autovacuum. This parameter can only be set in the postgresql.conf file or on the server command line; but the setting can be overridden for individual tables by changing table storage parameters. @@ -7527,12 +7966,12 @@ local0.* /var/log/postgresql setup_durations Logs the time spent establishing the connection and setting up the - backend at the time the connection is ready to execute its first - query. The log message includes the total setup duration, starting - from the postmaster accepting the incoming connection and ending - when the connection is ready for query. It also includes the time - it took to fork the new backend and the time it took to - authenticate the user. + backend until the connection is ready to execute its first + query. The log message includes three durations: the total + setup duration (starting from the postmaster accepting the + incoming connection and ending when the connection is ready + for query), the time it took to fork the new backend, and + the time it took to authenticate the user. @@ -7916,17 +8355,17 @@ log_line_prefix = '%m [%p] %q%u@%d/%a ' Controls whether a log message is produced when a session waits longer than to acquire a lock. This is useful in determining if lock waits are causing - poor performance. The default is off. + poor performance. The default is on. Only superusers and users with the appropriate SET privilege can change this setting. - - log_lock_failure (boolean) + + log_lock_failures (boolean) - log_lock_failure configuration parameter + log_lock_failures configuration parameter @@ -8600,7 +9039,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; timing information is displayed in pg_stat_progress_vacuum, pg_stat_progress_analyze, - in the output of when the + in the output of and + when the VERBOSE option is used, and by autovacuum for auto-vacuums and auto-analyzes when is set. @@ -9182,6 +9622,119 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; + + autovacuum_freeze_score_weight (floating point) + + autovacuum_freeze_score_weight + configuration parameter + + + + + Specifies the scaling factor of the transaction ID age component of + the score used by autovacuum for prioritization purposes. The default + is 1.0. This parameter can only be set in the + postgresql.conf file or on the server command + line. See for more information. + + + + + + autovacuum_multixact_freeze_score_weight (floating point) + + autovacuum_multixact_freeze_score_weight + configuration parameter + + + + + Specifies the scaling factor of the multixact ID age component of the + score used by autovacuum for prioritization purposes. The default is + 1.0. This parameter can only be set in the + postgresql.conf file or on the server command + line. See for more information. + + + + + + autovacuum_vacuum_score_weight (floating point) + + autovacuum_vacuum_score_weight + configuration parameter + + + + + Specifies the scaling factor of the vacuum threshold component of the + score used by autovacuum for prioritization purposes. The default is + 1.0. This parameter can only be set in the + postgresql.conf file or on the server command + line. See for more information. + + + + + + autovacuum_vacuum_insert_score_weight (floating point) + + autovacuum_vacuum_insert_score_weight + configuration parameter + + + + + Specifies the scaling factor of the vacuum insert threshold component + of the score used by autovacuum for prioritization purposes. The + default is 1.0. This parameter can only be set in + the postgresql.conf file or on the server command + line. See for more information. + + + + + + autovacuum_analyze_score_weight (floating point) + + autovacuum_analyze_score_weight + configuration parameter + + + + + Specifies the scaling factor of the analyze threshold component of the + score used by autovacuum for prioritization purposes. The default is + 1.0. This parameter can only be set in the + postgresql.conf file or on the server command + line. See for more information. + + + + + + autovacuum_max_parallel_workers (integer) + + autovacuum_max_parallel_workers + configuration parameter + + + + + Sets the maximum number of parallel workers that can be used by a + single autovacuum worker to process indexes. This limit applies + specifically to the index vacuuming and index cleanup phases (for the + details of each autovacuum phase, please refer to ). + The actual number of parallel workers is further limited by + . This is the + per-autovacuum worker equivalent of the PARALLEL + option of the VACUUM + command. Setting this value to 0 disables parallel vacuum during autovacuum. + The default is 0. + + + + @@ -9338,7 +9891,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; vacuum_truncate (boolean) - vacuum_truncate configuration parameter + vacuum_truncate + configuration parameter @@ -9542,7 +10096,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; vacuum_max_eager_freeze_failure_rate (floating point) - vacuum_max_eager_freeze_failure_rate configuration parameter + vacuum_max_eager_freeze_failure_rate + configuration parameter @@ -9820,7 +10375,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; The supported compression methods are pglz and (if PostgreSQL was compiled with ) lz4. - The default is pglz. + The default is lz4 (if available); otherwise, + pglz. @@ -11049,6 +11605,12 @@ extension_control_path = 'C:\tools\postgresql;H:\my_project\share;$system' string, the default '$system' is also assumed. + + If extensions with equal names are present in multiple directories in + the configured path, only the instance found first in the path will be + used. + + This parameter can be changed at run time by superusers and users with the appropriate SET privilege, but a @@ -11158,7 +11720,7 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' can lock more objects as long as the locks of all transactions fit in the lock table. This is not the number of rows that can be locked; that value is unlimited. The default, - 64, has historically proven sufficient, but you might need to + 128, has historically proven sufficient, but you might need to raise this value if you have queries that touch many different tables in a single transaction, e.g., query of a parent table with many children. This parameter can only be set at server start. @@ -11280,8 +11842,9 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' - This controls whether a quote mark can be represented by - \' in a string literal. The preferred, SQL-standard way + This parameter controls whether a quote mark can be represented by + \' in the escape string syntax + (E'...'). The preferred, SQL-standard way to represent a quote mark is by doubling it ('') but PostgreSQL has historically also accepted \'. However, use of \' creates security risks @@ -11300,34 +11863,9 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' - Note that in a standard-conforming string literal, \ just - means \ anyway. This parameter only affects the handling of - non-standard-conforming literals, including - escape string syntax (E'...'). - - - - - - escape_string_warning (boolean) - stringsescape warning - - escape_string_warning configuration parameter - - - - - When on, a warning is issued if a backslash (\) - appears in an ordinary string literal ('...' - syntax) and standard_conforming_strings is off. - The default is on. - - - Applications that wish to use backslash as escape should be - modified to use escape string syntax (E'...'), - because the default behavior of ordinary strings is now to treat - backslash as an ordinary character, per SQL standard. This variable - can be enabled to help locate code that needs to be changed. + Note that in an ordinary string literal, \ just + means \ anyway. This parameter only affects + the handling of escape string syntax. @@ -11383,15 +11921,12 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' - This controls whether ordinary string literals - ('...') treat backslashes literally, as specified in - the SQL standard. - Beginning in PostgreSQL 9.1, the default is - on (prior releases defaulted to off). - Applications can check this - parameter to determine how string literals will be processed. - The presence of this parameter can also be taken as an indication - that the escape string syntax (E'...') is supported. + Beginning in PostgreSQL 19, this + parameter is always on. String literals are + always parsed as specified in the SQL standard (that is, + backslashes are ordinary characters within a string literal). + This parameter continues to exist because applications may consult + it; but it cannot be set to off. Escape string syntax () should be used if an application desires backslashes to be treated as escape characters. @@ -11684,15 +12219,17 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' - data_checksums (boolean) + data_checksums (enum) data_checksums configuration parameter - Reports whether data checksums are enabled for this cluster. - See for more information. + Reports the state of data checksums for this cluster. + Possible values are on, off, + inprogress-on and inprogress-off. + See for more information. @@ -11737,6 +12274,55 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' + + debug_exec_backend (boolean) + + debug_exec_backend configuration parameter + + + + + Reports whether PostgreSQL has been built + with EXEC_BACKEND enabled. That is the case on + Windows or if the + macro EXEC_BACKEND is defined + when PostgreSQL is built. + + + + + + effective_wal_level (enum) + + effective_wal_level configuration parameter + + + + + Reports the actual WAL logging level currently in effect in the + system. This parameter shares the same set of values as + , but reflects the operational WAL + level rather than the configured setting. For descriptions of + possible values, refer to the wal_level + parameter documentation. + + + The effective WAL level can differ from the configured + wal_level in certain situations. For example, + when wal_level is set to replica + and the system has one or more logical replication slots, + effective_wal_level will show logical + to indicate that the system is maintaining WAL records at + logical level equivalent. + + + On standby servers, effective_wal_level matches + the value of effective_wal_level from the most + upstream server in the replication chain. + + + + huge_pages_status (enum) @@ -12189,6 +12775,7 @@ dynamic_library_path = '/usr/local/lib/postgresql:$libdir' main data files, wal for WAL files, and wal_init for WAL files when being initially allocated. + This parameter can only be set at server start. Some operating systems and file systems do not support direct I/O, so @@ -12676,7 +13263,9 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) If LLVM has the required functionality, register generated functions with GDB. This makes debugging easier. The default setting is off. - This parameter can only be set at server start. + Only superusers and users with the appropriate SET + privilege can change this parameter at session start, + and it cannot be changed at all within a session. @@ -12727,7 +13316,9 @@ LOG: CleanUpLock: deleting: lock(0xb7acd844) id(24688,24696,0,0,0,1) This writes out files to ~/.debug/jit/; the user is responsible for performing cleanup when desired. The default setting is off. - This parameter can only be set at server start. + Only superusers and users with the appropriate SET + privilege can change this parameter at session start, + and it cannot be changed at all within a session. diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index 24b706b29adc3..b9b03654aadbd 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -156,8 +156,10 @@ CREATE EXTENSION extension_name; &pgfreespacemap; &pglogicalinspect; &pgoverexplain; + &pgplanadvice; &pgprewarm; &pgrowlocks; + &pgstashadvice; &pgstatstatements; &pgstattuple; &pgsurgery; diff --git a/doc/src/sgml/cube.sgml b/doc/src/sgml/cube.sgml index 0fb7080748673..a11c0cbd767c9 100644 --- a/doc/src/sgml/cube.sgml +++ b/doc/src/sgml/cube.sgml @@ -249,7 +249,7 @@ For example, the nearest neighbor of the 3-D point (0.5, 0.5, 0.5) could be found efficiently with: -SELECT c FROM test ORDER BY c <-> cube(array[0.5,0.5,0.5]) LIMIT 1; +SELECT c FROM test ORDER BY c <-> cube(ARRAY[0.5, 0.5, 0.5]) LIMIT 1; @@ -540,7 +540,7 @@ SELECT c FROM test ORDER BY c ~> 3 DESC LIMIT 5; This union: -select cube_union('(0,5,2),(2,3,1)', '0'); +SELECT cube_union('(0,5,2),(2,3,1)', '0'); cube_union ------------------- (0, 0, 0),(2, 5, 2) @@ -552,7 +552,7 @@ cube_union -select cube_inter('(0,-1),(1,1)', '(-2),(2)'); +SELECT cube_inter('(0,-1),(1,1)', '(-2),(2)'); cube_inter ------------- (0, 0),(1, 0) @@ -579,7 +579,7 @@ cube_inter('(0,-1),(1,1)','(-2,0),(2,0)'); -select cube_contains('(0,0),(1,1)', '0.5,0.5'); +SELECT cube_contains('(0,0),(1,1)', '0.5,0.5'); cube_contains -------------- t diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index 09309ba0390b7..d8d91678e86d4 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -117,7 +117,7 @@ double precision - float8 + float, float8 double precision floating-point number (8 bytes) @@ -315,6 +315,7 @@ character varying, character, varchar, date, double precision, integer, interval, + json, numeric, decimal, real, smallint, time (with or without time zone), timestamp (with or without time zone), @@ -717,7 +718,7 @@ NUMERIC(3, 5) SELECT x, round(x::numeric) AS num_round, round(x::double precision) AS dbl_round -FROM generate_series(-3.5, 3.5, 1) as x; +FROM generate_series(-3.5, 3.5, 1) AS x; x | num_round | dbl_round ------+-----------+----------- -3.5 | -4 | -4 @@ -1259,7 +1260,7 @@ SELECT '52093.89'::money::numeric::float8; semantically insignificant and disregarded when comparing two values of type character. In collations where whitespace is significant, this behavior can produce unexpected results; - for example SELECT 'a '::CHAR(2) collate "C" < + for example SELECT 'a '::CHAR(2) COLLATE "C" < E'a\n'::CHAR(2) returns true, even though C locale would consider a space to be greater than a newline. Trailing spaces are removed when converting a character value @@ -2054,8 +2055,6 @@ MINUTE TO SECOND
Time Input - - Example @@ -4440,6 +4439,17 @@ a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11 Output is always in the standard form. + + It is possible to cast uuid values to and from type + bytea. This is useful for using functions such as + encode() and decode() + with UUID values. For example: + +encode('1ea3d64c-bc40-4cc3-84bb-6b11ee31e5c2'::uuid::bytea, 'base64') +decode('HqPWTLxATMOEu2sR7jHlwg==', 'base64')::uuid + + + See for how to generate a UUID in PostgreSQL. @@ -4725,6 +4735,10 @@ INSERT INTO mytable VALUES(-1); -- fails oid + + oid8 + + regclass @@ -4737,6 +4751,10 @@ INSERT INTO mytable VALUES(-1); -- fails regconfig + + regdatabase + + regdictionary @@ -4804,8 +4822,16 @@ INSERT INTO mytable VALUES(-1); -- fails - The oid type itself has few operations beyond comparison. - It can be cast to integer, however, and then manipulated using the + In some contexts, a 64-bit variant oid8 can be used. + It is implemented as an unsigned eight-byte integer. Unlike its + oid counterpart, it can ensure uniqueness in large + individual tables. + + + + The oid and oid8 types themselves have + few operations beyond comparison. + They can be cast to integer, however, and then manipulated using the standard integer operators. (Beware of possible signed-versus-unsigned confusion if you do this.) @@ -4878,6 +4904,13 @@ SELECT * FROM pg_attribute english + + regdatabase + pg_database + database name + template1 + + regdictionary pg_ts_dict @@ -5049,8 +5082,8 @@ WHERE ... be dropped without first removing the default expression. The alternative of nextval('my_seq'::text) does not create a dependency. - (regrole is an exception to this property. Constants of this - type are not allowed in stored expressions.) + (regdatabase and regrole are exceptions to this + property. Constants of these types are not allowed in stored expressions.) @@ -5110,7 +5143,7 @@ WHERE ... +(pg_lsn,numeric) and -(pg_lsn,numeric) operators, respectively. Note that the calculated LSN should be in the range of pg_lsn type, - i.e., between 0/0 and + i.e., between 0/00000000 and FFFFFFFF/FFFFFFFF. @@ -5234,8 +5267,8 @@ WHERE ...
Pseudo-Types - - + + Name diff --git a/doc/src/sgml/datetime.sgml b/doc/src/sgml/datetime.sgml index 3e24170acbfcc..5905f5fa5506a 100644 --- a/doc/src/sgml/datetime.sgml +++ b/doc/src/sgml/datetime.sgml @@ -942,17 +942,17 @@ $ cal 9 1752 definition when you need it: do the arithmetic in time zone UTC+12. For example, -=> SELECT extract(julian from '2021-06-23 7:00:00-04'::timestamptz at time zone 'UTC+12'); +=> SELECT extract(julian FROM '2021-06-23 7:00:00-04'::timestamptz AT TIME ZONE 'UTC+12'); extract ------------------------------ 2459388.95833333333333333333 (1 row) -=> SELECT extract(julian from '2021-06-23 8:00:00-04'::timestamptz at time zone 'UTC+12'); +=> SELECT extract(julian FROM '2021-06-23 8:00:00-04'::timestamptz AT TIME ZONE 'UTC+12'); extract -------------------------------------- 2459389.0000000000000000000000000000 (1 row) -=> SELECT extract(julian from date '2021-06-23'); +=> SELECT extract(julian FROM date '2021-06-23'); extract --------- 2459389 diff --git a/doc/src/sgml/dblink.sgml b/doc/src/sgml/dblink.sgml index 808c690985b73..dd6778d22a84a 100644 --- a/doc/src/sgml/dblink.sgml +++ b/doc/src/sgml/dblink.sgml @@ -444,7 +444,7 @@ dblink(text sql [, bool fail_on_error]) returns setof record The SQL query that you wish to execute in the remote database, - for example select * from foo. + for example SELECT * FROM foo. @@ -478,7 +478,7 @@ dblink(text sql [, bool fail_on_error]) returns setof record SELECT * FROM dblink('dbname=mydb options=-csearch_path=', - 'select proname, prosrc from pg_proc') + 'SELECT proname, prosrc FROM pg_proc') AS t1(proname name, prosrc text) WHERE proname LIKE 'bytea%'; @@ -513,7 +513,7 @@ SELECT * CREATE VIEW myremote_pg_proc AS SELECT * FROM dblink('dbname=postgres options=-csearch_path=', - 'select proname, prosrc from pg_proc') + 'SELECT proname, prosrc FROM pg_proc') AS t1(proname name, prosrc text); SELECT * FROM myremote_pg_proc WHERE proname LIKE 'bytea%'; @@ -525,7 +525,7 @@ SELECT * FROM myremote_pg_proc WHERE proname LIKE 'bytea%'; SELECT * FROM dblink('dbname=postgres options=-csearch_path=', - 'select proname, prosrc from pg_proc') + 'SELECT proname, prosrc FROM pg_proc') AS t1(proname name, prosrc text) WHERE proname LIKE 'bytea%'; proname | prosrc ------------+------------ @@ -549,7 +549,7 @@ SELECT dblink_connect('dbname=postgres options=-csearch_path='); OK (1 row) -SELECT * FROM dblink('select proname, prosrc from pg_proc') +SELECT * FROM dblink('SELECT proname, prosrc FROM pg_proc') AS t1(proname name, prosrc text) WHERE proname LIKE 'bytea%'; proname | prosrc ------------+------------ @@ -573,7 +573,7 @@ SELECT dblink_connect('myconn', 'dbname=regression options=-csearch_path='); OK (1 row) -SELECT * FROM dblink('myconn', 'select proname, prosrc from pg_proc') +SELECT * FROM dblink('myconn', 'SELECT proname, prosrc FROM pg_proc') AS t1(proname name, prosrc text) WHERE proname LIKE 'bytea%'; proname | prosrc ------------+------------ @@ -666,7 +666,7 @@ dblink_exec(text sql [, bool fail_on_error]) returns text The SQL command that you wish to execute in the remote database, for example - insert into foo values(0, 'a', '{"a0","b0","c0"}'). + INSERT INTO foo VALUES (0, 'a', '{"a0","b0","c0"}'). @@ -793,7 +793,7 @@ dblink_open(text connname, text cursorname, text sql [, bool fail_on_error]) ret The SELECT statement that you wish to execute in the remote - database, for example select * from pg_class. + database, for example SELECT * FROM pg_class. @@ -848,7 +848,7 @@ SELECT dblink_connect('dbname=postgres options=-csearch_path='); OK (1 row) -SELECT dblink_open('foo', 'select proname, prosrc from pg_proc'); +SELECT dblink_open('foo', 'SELECT proname, prosrc FROM pg_proc'); dblink_open ------------- OK @@ -969,7 +969,7 @@ SELECT dblink_connect('dbname=postgres options=-csearch_path='); OK (1 row) -SELECT dblink_open('foo', 'select proname, prosrc from pg_proc where proname like ''bytea%'''); +SELECT dblink_open('foo', 'SELECT proname, prosrc FROM pg_proc WHERE proname LIKE ''bytea%'''); dblink_open ------------- OK @@ -1106,7 +1106,7 @@ SELECT dblink_connect('dbname=postgres options=-csearch_path='); OK (1 row) -SELECT dblink_open('foo', 'select proname, prosrc from pg_proc'); +SELECT dblink_open('foo', 'SELECT proname, prosrc FROM pg_proc'); dblink_open ------------- OK @@ -1301,7 +1301,7 @@ dblink_send_query(text connname, text sql) returns int The SQL statement that you wish to execute in the remote database, - for example select * from pg_class. + for example SELECT * FROM pg_class. @@ -1583,7 +1583,7 @@ contrib_regression=# SELECT dblink_connect('dtest1', 'dbname=contrib_regression' (1 row) contrib_regression=# SELECT * FROM -contrib_regression-# dblink_send_query('dtest1', 'select * from foo where f1 < 3') AS t1; +contrib_regression-# dblink_send_query('dtest1', 'SELECT * FROM foo WHERE f1 < 3') AS t1; t1 ---- 1 @@ -1603,7 +1603,7 @@ contrib_regression=# SELECT * FROM dblink_get_result('dtest1') AS t1(f1 int, f2 (0 rows) contrib_regression=# SELECT * FROM -contrib_regression-# dblink_send_query('dtest1', 'select * from foo where f1 < 3; select * from foo where f1 > 6') AS t1; +contrib_regression-# dblink_send_query('dtest1', 'SELECT * FROM foo WHERE f1 < 3; SELECT * FROM foo WHERE f1 > 6') AS t1; t1 ---- 1 diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index fcd1cb85352fc..747f929aee323 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -419,6 +419,16 @@ CREATE TABLE people ( tableoid. + + + A virtual generated column cannot have a user-defined type, and the + generation expression of a virtual generated column must not reference + user-defined functions or types, that is, it can only use built-in + functions or types. This applies also indirectly, such as for functions + or types that underlie operators or casts. (This restriction does not + exist for stored generated columns.) + + A generated column cannot have a column default or an identity definition. @@ -1343,7 +1353,7 @@ CREATE TABLE posts ( ); Without the specification of the column, the foreign key would also set - the column tenant_id to null, but that column is still + the column tenant_id to null, but that column is still required as part of the primary key. @@ -1491,33 +1501,42 @@ CREATE TABLE circles ( - - cmin + + xmax - cmin + xmax - The command identifier (starting at zero) within the inserting - transaction. + The identity (transaction ID) of the deleting transaction, or + zero for an undeleted row version. It is possible for this column to + be nonzero in a visible row version. That usually indicates that the + deleting transaction hasn't committed yet, or that an attempted + deletion was rolled back. - - xmax + + cmin - xmax + cmin - The identity (transaction ID) of the deleting transaction, or - zero for an undeleted row version. It is possible for this column to - be nonzero in a visible row version. That usually indicates that the - deleting transaction hasn't committed yet, or that an attempted - deletion was rolled back. + Originally, cmin + and cmax were separate fields. + cmin was the inserting command's command + identifier (starting at zero) within the inserting transaction, + while cmax was the deleting command's + command identifier within the deleting transaction, or zero if no + delete attempt had occurred yet. Nowadays these columns refer to + the same field and will always read as the same value. That might + be the inserting command's command identifier, or the deleting + command's command identifier, or a combocid that + reflects both actions when those happened in the same transaction. @@ -1530,7 +1549,7 @@ CREATE TABLE circles ( - The command identifier within the deleting transaction, or zero. + See cmin. @@ -1548,7 +1567,7 @@ CREATE TABLE circles ( locate the row version very quickly, a row's ctid will change if it is updated or moved by VACUUM FULL. Therefore - ctid is useless as a long-term row + ctid should not be used as a row identifier. A primary key should be used to identify logical rows. @@ -1575,6 +1594,297 @@ CREATE TABLE circles ( + + Temporal Tables + + + temporal + + + + Temporal tables allow users to track different + dimensions of history. Application time tracks the + history of a thing out in the world, and system time + tracks the history of the database itself. (A database that does both is + also called bitemporal.) This section describes how + to express and manage such histories in temporal tables. + + + + Application Time + + + application time + + + + Application time refers to a history of the entity + described by a table. In a typical non-temporal table, there is a single + row for each entity. In a temporal table, an entity may have multiple + rows, as long as those rows describe non-overlapping periods from its + history. Application time requires each row to have a start and end time, + expressing when the row is applicable. + + + + The following SQL creates a temporal table that can store application time: + +CREATE TABLE products ( + product_no integer, + price numeric, + valid_at daterange +); + + + + + Records in a temporal table can be imagined on a timeline, as in . Here we show three records + describing two products. Each record is a tuple with three attributes: + the product number, the price, and the application time. So product 5 was + first offered for a price of 5.00 starting January 1, 2020, but then + became 8.00 starting January 1, 2022. Its second record has no specified + end time, indicating that it is true indefinitely, or for all future time. + The last record shows that product 6 was introduced January 1, 2021 for + 9.00, then canceled January 1, 2024. + + +
+ Application Time Example + + + + + +
+ + + In a table, these records would be: + + product_no | price | valid_at +------------+-------+------------------------- + 5 | 5.00 | [2020-01-01,2022-01-01) + 5 | 8.00 | [2022-01-01,) + 6 | 9.00 | [2021-01-01,2024-01-01) + + + + + We show the application time using range-type notation, because it is + stored as a single column (either a range or multirange). Ranges include + their start point but exclude their end point. That way two adjacent + ranges cover all points without overlapping. See for more information about range types. + + + + In principle, a table with application-time ranges/multiranges is + equivalent to a table that stores application-time + instants: one for each second, millisecond, nanosecond, or + whatever finest granularity is available. But such a table would contain + far too many rows, so ranges/multiranges offer an optimization to + represent the same information in a compact form. In addition, ranges and + multiranges offer a more convenient interface for typical temporal + operations, where records change infrequently enough that separate + versions persist for extended periods of time. + + + + Temporal Primary Keys and Unique Constraints + + + A table with application time has a different concept of entity + uniqueness than a non-temporal table. Temporal entity uniqueness can be + enforced with a temporal primary key. A regular primary key has at least + one column, all columns are NOT NULL, and the combined + value of all columns is unique. A temporal primary key also has at least + one such column, but in addition it has a final column that is of a range + type or multirange type that shows when the row is applicable. The + regular parts of the key must be unique for any moment in time, but + non-unique rows are allowed if their application time does not overlap. + + + + The syntax to create a temporal primary key is as follows: + + +CREATE TABLE products ( + product_no integer, + price numeric, + valid_at daterange, + PRIMARY KEY (product_no, valid_at WITHOUT OVERLAPS) +); + + + In this example, product_no is the non-temporal part + of the key, and valid_at is a range column containing + the application time. + + + + The WITHOUT OVERLAPS column is implicitly NOT + NULL (like the other parts of the key). In addition it may not + contain empty values, that is, a range of 'empty' or a + multirange of {}. An empty application time would + have no meaning. + + + + It is also possible to create a temporal unique constraint that is + not a primary key. The syntax is similar: + + +CREATE TABLE products ( + product_no integer, + price numeric, + valid_at daterange, + UNIQUE (product_no, valid_at WITHOUT OVERLAPS) +); + + + Temporal unique constraints also forbid empty ranges/multiranges for + their application time, but that column is permitted to be null (like the + other columns of the unique constraint). + + + + Temporal primary keys and unique constraints are backed by GiST indexes + (see ) rather than B-Tree indexes. In practice, + creating a temporal primary key or constraint requires installing the + extension, so that the database has GiST + operator classes for the non-temporal parts of the key. + + + + Temporal primary keys and unique constraints have the same behavior as + exclusion constraints (see ), + where each regular key part is compared with equality, and the + application time is compared with overlaps, for example EXCLUDE + USING gist (id WITH =, valid_at WITH &&). The only + difference is that they also forbid an empty application time. + + + + + Temporal Foreign Keys + + + A temporal foreign key is a reference from one application-time table to + another application-time table. Just as a non-temporal reference + requires a referenced key to exist, so a temporal reference requires a + referenced key to exist, but during whatever history the reference exists + (at least). So if the products table is referenced by + a variants table, and a variant of product 5 has an + application-time of [2020-01-01,2026-01-01), then + product 5 must exist throughout that period. + + + + We can create the variants table with the following + schema (without a foreign key yet): + + +CREATE TABLE variants ( + id integer, + product_no integer, + name text, + valid_at daterange, + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + + + We have included a temporal primary key as a best practice, but it is not + strictly required by foreign keys. + + + + plots product 5 (in green) + and two variants referencing it (in yellow) on the same timeline. + Variant 8 (Medium) was introduced first, then variant 9 (XXL). Both + satisfy the foreign key constraint, because the referenced product exists + throughout their entire history. + + +
+ Temporal Foreign Key Example + + + + + +
+ + + In a table, these records would be: + + id | product_no | name | valid_at +----+------------+--------+------------------------- + 8 | 5 | Medium | [2021-01-01,2023-06-01) + 9 | 5 | XXL | [2022-03-01,2024-06-01) + + + + + Note that a temporal reference need not be fulfilled by a single row in + the referenced table. Product 5 had a price change in the middle of + variant 8's history, but the reference is still valid. The combination + of all matching rows is used to test whether the referenced history + contains the referencing row. + + + + The syntax to add a temporal foreign key to our table is: + + +CREATE TABLE variants ( + id integer, + product_no integer, + name text, + valid_at daterange, + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + FOREIGN KEY (product_no, PERIOD valid_at) REFERENCES products (product_no, PERIOD valid_at) +); + + + Note that the keyword PERIOD must be used for the + application-time column in both the referencing and referenced table. + + + + A temporal primary key or unique constraint matching the referenced columns + must exist on the referenced table. + + + + PostgreSQL supports temporal foreign keys with + action NO ACTION, but not RESTRICT, + CASCADE, SET NULL, or SET + DEFAULT. + +
+
+ + + System Time + + + system time + + + + System time refers to the history of the database + table, not the entity it describes. It captures when each row was + inserted/updated/deleted. + + + + PostgreSQL does not currently support system + time, but it could be emulated using triggers, and there are external + extensions that provide such functionality. + + +
+ Modifying Tables @@ -1949,6 +2259,8 @@ ALTER TABLE table_name OWNER TO new_owne Superusers can always do this; ordinary roles can only do it if they are both the current owner of the object (or inherit the privileges of the owning role) and able to SET ROLE to the new owning role. + All object privileges of the old owner are transferred to the new owner + along with the ownership. @@ -2012,6 +2324,8 @@ REVOKE ALL ON accounts FROM PUBLIC; This privilege is also needed to reference existing column values in UPDATE, DELETE, or MERGE. + For property graphs, this privilege allows the object to be referenced + in a GRAPH_TABLE clause. For sequences, this privilege also allows use of the currval function. For large objects, this privilege allows the object to be read. @@ -2223,8 +2537,9 @@ REVOKE ALL ON accounts FROM PUBLIC; Allows VACUUM, ANALYZE, CLUSTER, REFRESH MATERIALIZED VIEW, - REINDEX, and LOCK TABLE on a - relation. + REINDEX, LOCK TABLE, + and database object statistics manipulation functions + (see ) on a relation.
@@ -2528,7 +2843,7 @@ REVOKE ALL ON accounts FROM PUBLIC; As an example, suppose that user miriam creates - table mytable and does: + table mytable and does: GRANT SELECT ON mytable TO PUBLIC; GRANT SELECT, UPDATE, INSERT ON mytable TO admin; @@ -2756,7 +3071,7 @@ CREATE POLICY user_mod_policy ON users Below is a larger example of how this feature can be used in production - environments. The table passwd emulates a Unix password + environments. The table passwd emulates a Unix password file: @@ -2823,9 +3138,9 @@ GRANT UPDATE -- admin can view all rows and fields -postgres=> set role admin; +postgres=> SET ROLE admin; SET -postgres=> table passwd; +postgres=> TABLE passwd; user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell -----------+--------+-----+-----+-----------+--------------+------------+-------------+----------- admin | xxx | 0 | 0 | Admin | 111-222-3333 | | /root | /bin/dash @@ -2834,11 +3149,11 @@ postgres=> table passwd; (3 rows) -- Test what Alice is able to do -postgres=> set role alice; +postgres=> SET ROLE alice; SET -postgres=> table passwd; +postgres=> TABLE passwd; ERROR: permission denied for table passwd -postgres=> select user_name,real_name,home_phone,extra_info,home_dir,shell from passwd; +postgres=> SELECT user_name, real_name, home_phone, extra_info, home_dir, shell FROM passwd; user_name | real_name | home_phone | extra_info | home_dir | shell -----------+-----------+--------------+------------+-------------+----------- admin | Admin | 111-222-3333 | | /root | /bin/dash @@ -2846,21 +3161,21 @@ postgres=> select user_name,real_name,home_phone,extra_info,home_dir,shell fr alice | Alice | 098-765-4321 | | /home/alice | /bin/zsh (3 rows) -postgres=> update passwd set user_name = 'joe'; +postgres=> UPDATE passwd SET user_name = 'joe'; ERROR: permission denied for table passwd -- Alice is allowed to change her own real_name, but no others -postgres=> update passwd set real_name = 'Alice Doe'; +postgres=> UPDATE passwd SET real_name = 'Alice Doe'; UPDATE 1 -postgres=> update passwd set real_name = 'John Doe' where user_name = 'admin'; +postgres=> UPDATE passwd SET real_name = 'John Doe' WHERE user_name = 'admin'; UPDATE 0 -postgres=> update passwd set shell = '/bin/xx'; +postgres=> UPDATE passwd SET shell = '/bin/xx'; ERROR: new row violates WITH CHECK OPTION for "passwd" -postgres=> delete from passwd; +postgres=> DELETE FROM passwd; ERROR: permission denied for table passwd -postgres=> insert into passwd (user_name) values ('xxx'); +postgres=> INSERT INTO passwd (user_name) VALUES ('xxx'); ERROR: permission denied for table passwd -- Alice can change her own password; RLS silently prevents updating other rows -postgres=> update passwd set pwhash = 'abc'; +postgres=> UPDATE passwd SET pwhash = 'abc'; UPDATE 1 @@ -2893,7 +3208,7 @@ CREATE POLICY admin_local_only ON passwd AS RESTRICTIVE TO admin admin (1 row) -=> select inet_client_addr(); +=> SELECT inet_client_addr(); inet_client_addr ------------------ 127.0.0.1 @@ -2904,7 +3219,7 @@ CREATE POLICY admin_local_only ON passwd AS RESTRICTIVE TO admin -----------+--------+-----+-----+-----------+------------+------------+----------+------- (0 rows) -=> UPDATE passwd set pwhash = NULL; +=> UPDATE passwd SET pwhash = NULL; UPDATE 0 @@ -4441,6 +4756,44 @@ ALTER INDEX measurement_city_id_logdate_key ... + + + There is also an option for merging multiple table partitions into + a single partition using the + ALTER TABLE ... MERGE PARTITIONS. + This feature simplifies the management of partitioned tables by allowing + users to combine partitions that are no longer needed as + separate entities. It's important to note that this operation is not + supported for hash-partitioned tables and acquires an + ACCESS EXCLUSIVE lock, which could impact high-load + systems due to the lock's restrictive nature. For example, we can + merge three monthly partitions into one quarter partition: + +ALTER TABLE measurement + MERGE PARTITIONS (measurement_y2006m01, + measurement_y2006m02, + measurement_y2006m03) INTO measurement_y2006q1; + + + + + Similarly to merging multiple table partitions, there is an option for + splitting a single partition into multiple using the + ALTER TABLE ... SPLIT PARTITION. + This feature could come in handy when one partition grows too big + and needs to be split into multiple. It's important to note that + this operation is not supported for hash-partitioned tables and acquires + an ACCESS EXCLUSIVE lock, which could impact high-load + systems due to the lock's restrictive nature. For example, we can split + the quarter partition back to monthly partitions: + +ALTER TABLE measurement SPLIT PARTITION measurement_y2006q1 INTO + (PARTITION measurement_y2006m01 FOR VALUES FROM ('2006-01-01') TO ('2006-02-01'), + PARTITION measurement_y2006m02 FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'), + PARTITION measurement_y2006m03 FOR VALUES FROM ('2006-03-01') TO ('2006-04-01')); + + + @@ -5072,13 +5425,9 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; It is possible to determine the number of partitions which were removed during this phase by observing the Subplans Removed property in the - EXPLAIN output. The query planner obtains locks for - all partitions which are part of the plan. However, when the executor - uses a cached plan, locks are only obtained on the partitions which - remain after partition pruning done during the initialization phase of - execution, i.e., the ones shown in the EXPLAIN - output and not the ones referred to by the - Subplans Removed property. + EXPLAIN output. It's important to note that any + partitions removed by the partition pruning done at this stage are + still locked at the beginning of execution. @@ -5351,6 +5700,242 @@ EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01'; + + Property Graphs + + + property graph + + + + A property graph is a way to represent database contents, as an alternative + to the usual (in SQL) approach of representing database contents using + relational structures such as tables. A property graph can then be queried + using graph pattern matching syntax, instead of join queries typical of + relational databases. PostgreSQL implements SQL/PGQHere, PGQ + stands for property graph query. In the jargon of graph + databases, property graph is normally abbreviated as PG, which + is clearly confusing for practioners of PostgreSQL, also usually abbreviated + as PG., which is part of the SQL standard, where a property + graph is defined as a kind of read-only view over relational tables. So the + actual data is still in tables or table-like objects, but is exposed as a + graph for graph querying operations. (This is in contrast to native graph + databases, where the data is stored directly in a graph structure.) + Underneath, both relational queries and graph queries use the same query + planning and execution infrastructure, and in fact relational and graph + queries can be combined and mixed in single queries. + + + + A graph is a set of vertices and edges. Each edge has two distinguishable + associated vertices called the source and destination vertices. (So in + this model, all edges are directed.) Vertices and edges together are + called the elements of the graph. A property graph extends this well-known + mathematical structure with a way to represent user data. In a property + graph, each vertex or edge has one or more associated labels, and each + label has zero or more properties. The labels are similar to table row + types in that they define the kind of the contained data and its structure. + The properties are similar to columns in that they contain the actual data. + In fact, by default, a property graph definition exposes the underlying + tables and columns as labels and properties, but more complicated + definitions are possible. + + + + Consider the following table definitions: + +CREATE TABLE products ( + product_no integer PRIMARY KEY, + name varchar, + price numeric +); + +CREATE TABLE customers ( + customer_id integer PRIMARY KEY, + name varchar, + address varchar +); + +CREATE TABLE orders ( + order_id integer PRIMARY KEY, + ordered_when date +); + +CREATE TABLE order_items ( + order_items_id integer PRIMARY KEY, + order_id integer REFERENCES orders (order_id), + product_no integer REFERENCES products (product_no), + quantity integer +); + +CREATE TABLE customer_orders ( + customer_orders_id integer PRIMARY KEY, + customer_id integer REFERENCES customers (customer_id), + order_id integer REFERENCES orders (order_id) +); + + When mapping this to a graph, the first three tables would be the vertices + and the last two tables would be the edges. The foreign-key definitions + correspond to the fact that edges link two vertices. (Graph definitions + work more naturally with many-to-many relationships, so this example is + organized like that, even though one-to-many relationships might be used + here in a pure relational approach.) + + + + Here is an example how a property graph could be defined on top of these + tables: + +CREATE PROPERTY GRAPH myshop + VERTEX TABLES ( + products, + customers, + orders + ) + EDGE TABLES ( + order_items SOURCE orders DESTINATION products, + customer_orders SOURCE customers DESTINATION orders + ); + + + + + This graph could then be queried like this: + +-- get list of customers active today +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders]->(o IS orders WHERE o.ordered_when = current_date) COLUMNS (c.name AS customer_name)); + + corresponding approximately to this relational query: + +-- get list of customers active today +SELECT customers.name FROM customers JOIN customer_orders USING (customer_id) JOIN orders USING (order_id) WHERE orders.ordered_when = current_date; + + + + + The above definition requires that all tables have primary keys and that + for each edge there is an appropriate foreign key. Otherwise, additional + clauses have to be specified to identify the key columns. For example, + this would be the fully verbose definition that does not rely on primary + and foreign keys: + +CREATE PROPERTY GRAPH myshop + VERTEX TABLES ( + products KEY (product_no), + customers KEY (customer_id), + orders KEY (order_id) + ) + EDGE TABLES ( + order_items KEY (order_items_id) + SOURCE KEY (order_id) REFERENCES orders (order_id) + DESTINATION KEY (product_no) REFERENCES products (product_no), + customer_orders KEY (customer_orders_id) + SOURCE KEY (customer_id) REFERENCES customers (customer_id) + DESTINATION KEY (order_id) REFERENCES orders (order_id) + ); + + + + + As mentioned above, by default, the names of the tables and columns are + exposed as labels and properties, respectively. The clauses IS + customer, IS order, etc. in the + MATCH clause in fact refer to labels, not table names. + + + + One use of labels is to expose a table through a different name in the graph. + For example, in graphs, vertices typically have singular nouns as labels and + edges typically have verbs or phrases derived from verbs as labels, such as + has, contains, or something specific like + approved_by. We can introduce such labels into our example like + this: + +CREATE PROPERTY GRAPH myshop + VERTEX TABLES ( + products LABEL product, + customers LABEL customer, + orders LABEL "order" + ) + EDGE TABLES ( + order_items SOURCE orders DESTINATION products LABEL contains, + customer_orders SOURCE customers DESTINATION orders LABEL has_placed + ); + + + + + With this definition, we can write a query like this: + +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customer)-[IS has_placed]->(o IS "order" WHERE o.ordered_when = current_date) COLUMNS (c.name AS customer_name)); + + With the new labels the MATCH clause is now more intuitive. + + + + Notice that the label order is quoted. If we run above + statements without adding quotes around order, we will get + a syntax error since order is a keyword. + + + + Another use is to apply the same label to multiple element tables. For + example, consider this additional table: + +CREATE TABLE employees ( + employee_id integer PRIMARY KEY, + employee_name varchar, + ... +); + +and the following graph definition: + +CREATE PROPERTY GRAPH myshop + VERTEX TABLES ( + products LABEL product, + customers LABEL customer LABEL person PROPERTIES (name), + orders LABEL order, + employees LABEL employee LABEL person PROPERTIES (employee_name AS name) + ) + EDGE TABLES ( + order_items SOURCE orders DESTINATION products LABEL contains, + customer_orders SOURCE customers DESTINATION orders LABEL has + ); + + (In practice, there ought to be an edge linking the + employees table to something, but it is allowed like + this.) Then we can run a query like this (incomplete): + +SELECT ... FROM GRAPH_TABLE (myshop MATCH (IS person WHERE name = '...')-[]->... COLUMNS (...)); + + This would automatically consider both the customers and + the employees tables when looking for an edge with the + person label. + + + + When more than one element table has the same label, it is required that + the properties match in number, name, and type. In the example, we specify + an explicit property list and in one case override the name of the column + to achieve this. + + + + Using more than one label associated with an element table and each label + exposing a different set of properties, the same relational data, and the + graph structure contained therein, can be exposed through multiple + co-existing logical views, which can be queried using graph pattern matching + constructs. + + + + For more details on the syntax for creating property graphs, see CREATE PROPERTY + GRAPH. More details about the graph query syntax is in + . + + + Other Database Objects diff --git a/doc/src/sgml/dfunc.sgml b/doc/src/sgml/dfunc.sgml index b94aefcd0ca6c..3778efc83ebfa 100644 --- a/doc/src/sgml/dfunc.sgml +++ b/doc/src/sgml/dfunc.sgml @@ -157,19 +157,12 @@ ld -Bshareable -o foo.so foo.o The compiler flag to create PIC is - with the Sun compiler and with GCC. To link shared libraries, the compiler option is - with either compiler or alternatively with GCC. -cc -KPIC -c foo.c -cc -G -o foo.so foo.o - - or - gcc -fPIC -c foo.c -gcc -G -o foo.so foo.o +gcc -shared -o foo.so foo.o diff --git a/doc/src/sgml/dict-int.sgml b/doc/src/sgml/dict-int.sgml index 8dd07b9bc1270..b4ce54848232f 100644 --- a/doc/src/sgml/dict-int.sgml +++ b/doc/src/sgml/dict-int.sgml @@ -80,7 +80,7 @@ ALTER TEXT SEARCH DICTIONARY To test the dictionary, you can try -mydb# select ts_lexize('intdict', '12345678'); +mydb# SELECT ts_lexize('intdict', '12345678'); ts_lexize ----------- {123456} diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml index 458aee788b7fb..429aae9bd7b53 100644 --- a/doc/src/sgml/dml.sgml +++ b/doc/src/sgml/dml.sgml @@ -261,6 +261,179 @@ DELETE FROM products; + + Updating and Deleting Temporal Data + + + Special syntax is available to update and delete from application-time + temporal tables (see ). (No extra + syntax is required to insert into them: the user just provides the + application time values like any other column.) When updating or deleting, + the user can target a specific portion of history. Only rows overlapping + that history are affected, and within those rows only the targeted history + is changed. If a row contains more history beyond what is targeted, its + application time is reduced to fit within the targeted portion, and new + rows are inserted to preserve the history that was not targeted. + + + + Recall the example table from , + containing this data: + + + product_no | price | valid_at +------------+-------+------------------------- + 5 | 5.00 | [2020-01-01,2022-01-01) + 5 | 8.00 | [2022-01-01,) + 6 | 9.00 | [2021-01-01,2024-01-01) + + + A temporal update might look like this: + + +UPDATE products + FOR PORTION OF valid_at FROM '2023-09-01' TO '2025-03-01' + SET price = 12.00 + WHERE product_no = 5; + + + That command will update the second record for product 5. It will set the + price to 12.00 and the application time to + [2023-09-01,2025-03-01). Then, since the row's + application time was originally [2022-01-01,), the + command must insert two temporal leftovers: one + for history before September 1, 2023, and another for history since March + 1, 2025. After the update, the table has four rows for product 5: + + + product_no | price | valid_at +------------+-------+------------------------- + 5 | 5.00 | [2020-01-01,2022-01-01) + 5 | 8.00 | [2022-01-01,2023-09-01) + 5 | 12.00 | [2023-09-01,2025-03-01) + 5 | 8.00 | [2025-03-01,) + 6 | 9.00 | [2021-01-01,2024-01-01) + + + The new history could be plotted as in . + + +
+ Temporal Update Example + + + + + +
+ + + Similarly, a specific portion of history may be targeted when deleting rows + from a table. In that case, the original rows are removed, but new + temporal + leftovers are inserted to preserve the untouched history. The + syntax for a temporal delete is: + + +DELETE FROM products + FOR PORTION OF valid_at FROM '2021-08-01' TO '2023-09-01' + WHERE product_no = 5; + + + Continuing the example, this command would delete two records. The + first record would yield a single temporal leftover, and the second + would be deleted entirely. The rows in the table would now be: + + + product_no | price | valid_at +------------+-------+------------------------- + 5 | 5.00 | [2020-01-01,2021-08-01) + 5 | 12.00 | [2023-09-01,2025-03-01) + 5 | 8.00 | [2025-03-01,) + 6 | 9.00 | [2021-01-01,2024-01-01) + + + The new history could be plotted as in . + + +
+ Temporal Delete Example + + + + + +
+ + + Instead of using the FROM ... TO ... syntax, temporal + update/delete commands can also give the targeted range/multirange + directly, inside parentheses. For example: DELETE FROM products + FOR PORTION OF valid_at ('[2028-01-01,)') .... This syntax is + required when application time is stored in a multirange column. + + + + When application time is stored in a range type column, zero, one or two + temporal leftovers are produced by each row that is updated/deleted. With + a multirange column, only zero or one temporal leftover is produced. The + leftover bounds are computed using range_minus_multi and + multirange_minus_multi (see ). + + + + The bounds given to FOR PORTION OF must be constant. + Functions like now() are allowed, but column references + are not. + + + + When temporal leftovers are inserted, all INSERT + triggers are fired, but permission checks for inserting rows are + skipped. + + + + In READ COMMITTED mode, temporal updates and deletes can + yield unexpected results when they concurrently touch the same row. It is + possible to lose all or part of the second update or delete. The scenario + is illustrated in . Session 2 + searches for rows to change, and it finds one that Session 1 has already + modified. It waits for Session 1 to commit. Then it re-checks whether the + row still matches its search criteria (including the start/end times + targeted by FOR PORTION OF). Session 1 may have changed + those times so that they no longer qualify. + + + + In addition, the temporal leftovers inserted by Session 1 are not visible + within Session 2's transaction, because they are not yet committed. + Therefore there is nothing for Session 2 to update/delete: neither the + modified row nor the leftovers. The portion of history that Session 2 + intended to change is not affected. + + +
+ Temporal Isolation Example + + + + + +
+ + + To solve these problems, precede every temporal update/delete with a + SELECT FOR UPDATE matching the same criteria (including + the targeted portion of application time). That way the actual + update/delete doesn't begin until the lock is held, and all concurrent + leftovers will be visible. In higher transaction isolation levels, this + lock is not required. + +
+ Returning Data from Modified Rows @@ -317,7 +490,7 @@ DELETE FROM products; column to provide unique identifiers, RETURNING can return the ID assigned to a new row: -CREATE TABLE users (firstname text, lastname text, id serial primary key); +CREATE TABLE users (firstname text, lastname text, id serial PRIMARY KEY); INSERT INTO users (firstname, lastname) VALUES ('Joe', 'Cool') RETURNING id; @@ -385,7 +558,7 @@ UPDATE products SET price = price * 1.10 for a DELETE. However, there are situations where it can still be useful for those commands. For example, in an INSERT with an - ON CONFLICT DO UPDATE + ON CONFLICT DO SELECT/UPDATE clause, the old values will be non-NULL for conflicting rows. Similarly, if a DELETE is turned into an UPDATE by a rewrite rule, diff --git a/doc/src/sgml/docguide.sgml b/doc/src/sgml/docguide.sgml index db4bcce56eac6..7b61b4841aa03 100644 --- a/doc/src/sgml/docguide.sgml +++ b/doc/src/sgml/docguide.sgml @@ -60,9 +60,7 @@ maintained by the OASIS group. The official DocBook site has good introductory and reference documentation and - a complete O'Reilly book for your online reading pleasure. The - - NewbieDoc Docbook Guide is very helpful for beginners. + a complete O'Reilly book for your online reading pleasure. The FreeBSD Documentation Project also uses DocBook and has some good information, including a number of style guidelines that might be diff --git a/doc/src/sgml/ecpg.sgml b/doc/src/sgml/ecpg.sgml index e7a53f3c9d00d..6203b2518cf0c 100644 --- a/doc/src/sgml/ecpg.sgml +++ b/doc/src/sgml/ecpg.sgml @@ -75,9 +75,7 @@ EXEC SQL ...; Embedded SQL statements likewise use SQL rules, not C rules, for parsing quoted strings and identifiers. (See and - respectively. Note that - ECPG assumes that standard_conforming_strings - is on.) + respectively.) Of course, the C part of the program follows C quoting rules. @@ -1823,11 +1821,11 @@ while (1) representation of that type is (%f,%f), which is defined in the functions complex_in() - and complex_out() functions + and complex_out() in . The following example inserts the complex type values (1,1) and (3,3) into the - columns a and b, and select + columns a and b, and select them from the table after that. @@ -2042,7 +2040,7 @@ EXEC SQL EXECUTE mystmt INTO :v1, :v2, :v3 USING 37; EXEC SQL BEGIN DECLARE SECTION; char dbaname[128]; char datname[128]; -char *stmt = "SELECT u.usename as dbaname, d.datname " +char *stmt = "SELECT u.usename AS dbaname, d.datname " " FROM pg_database d, pg_user u " " WHERE d.datdba = u.usesysid"; EXEC SQL END DECLARE SECTION; @@ -6685,7 +6683,7 @@ EXEC SQL CONNECT TO 'unix:postgresql://localhost/connectdb' AS main USER :user; EXEC SQL CONNECT TO :db AS :id; EXEC SQL CONNECT TO :db USER connectuser USING :pw; EXEC SQL CONNECT TO @localhost AS main USER connectdb; -EXEC SQL CONNECT TO REGRESSDB1 as main; +EXEC SQL CONNECT TO REGRESSDB1 AS main; EXEC SQL CONNECT TO AS main USER connectdb; EXEC SQL CONNECT TO connectdb AS :id; EXEC SQL CONNECT TO connectdb AS main USER connectuser/connectdb; @@ -6959,7 +6957,7 @@ EXEC SQL [ AT connection_name ] DEC The namespace of the declaration is the precompile unit, and multiple declarations to the same SQL statement identifier are not allowed. Note that if the precompiler runs in Informix compatibility mode and - some SQL statement is declared, "database" can not be used as a cursor + some SQL statement is declared, "database" cannot be used as a cursor name. diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml index 1bd9abb667650..c10627554bdbd 100644 --- a/doc/src/sgml/event-trigger.sgml +++ b/doc/src/sgml/event-trigger.sgml @@ -433,7 +433,7 @@ $$ --- DECLARE table_oid oid := pg_event_trigger_table_rewrite_oid(); - current_hour integer := extract('hour' from current_time); + current_hour integer := extract('hour' FROM current_time); pages integer; max_pages integer := 100; BEGIN diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index b80320504d695..8685a078c5257 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -1320,7 +1320,7 @@ ExplainForeignModify(ModifyTableState *mtstate, ResultRelInfo *rinfo, List *fdw_private, int subplan_index, - struct ExplainState *es); + ExplainState *es); Print additional EXPLAIN output for a foreign table update. @@ -1409,6 +1409,45 @@ AcquireSampleRowsFunc(Relation relation, if the FDW does not have any concept of dead rows.) + + +bool +ImportForeignStatistics(Relation relation, + List *va_cols, + int elevel); + + + This function is called before the AnalyzeForeignTable + callback function when is executed on a + foreign table, and is used to import remotely-calculated statistics (both + table-level and column-level) for the foreign table directly to the local + server. + relation is the Relation struct + describing the target foreign table. va_cols, if not + NIL, contains the columns specified in the ANALYZE + command. elevel contains a flag indicating a logging + level to use. + If the function imports the statistics successfully, it should return + true. Otherwise, return false, in + which case AnalyzeForeignTable callback function is + called on the foreign table to collect statistics locally, if supported. + + + + For reference, the logic for calculating statistics in + PostgreSQL is found in + src/backend/command/analyze.c. + It's recommended to import table-level and column-level statistics for the + foreign table using pg_restore_relation_stats and + pg_restore_attribute_stats, respectively. + + + + If the FDW does not support importing remotely-calculated statistics for + any tables, the ImportForeignStatistics pointer can be + set to NULL. + + @@ -1702,7 +1741,7 @@ ReparameterizeForeignPathByChild(PlannerInfo *root, List *fdw_private, ForeignDataWrapper * -GetForeignDataWrapperExtended(Oid fdwid, bits16 flags); +GetForeignDataWrapperExtended(Oid fdwid, uint16 flags); This function returns a ForeignDataWrapper @@ -1731,7 +1770,7 @@ GetForeignDataWrapper(Oid fdwid); ForeignServer * -GetForeignServerExtended(Oid serverid, bits16 flags); +GetForeignServerExtended(Oid serverid, uint16 flags); This function returns a ForeignServer object @@ -2045,7 +2084,7 @@ GetForeignServerByName(const char *name, bool missing_ok); INSERT with an ON CONFLICT clause does not support specifying the conflict target, as unique constraints or exclusion constraints on remote tables are not locally known. This - in turn implies that ON CONFLICT DO UPDATE is not supported, + in turn implies that ON CONFLICT DO SELECT/UPDATE is not supported, since the specification is mandatory there. diff --git a/doc/src/sgml/features.sgml b/doc/src/sgml/features.sgml index 966fd39882760..1abe6ccd3d50c 100644 --- a/doc/src/sgml/features.sgml +++ b/doc/src/sgml/features.sgml @@ -70,10 +70,10 @@ The PostgreSQL core covers parts 1, 2, 9, - 11, and 14. Part 3 is covered by the ODBC driver, and part 13 is + 11, 14, and 16. Part 3 is covered by the ODBC driver, and part 13 is covered by the PL/Java plug-in, but exact conformance is currently not being verified for these components. There are currently no - implementations of parts 4, 10, 15, and 16 + implementations of parts 4, 10, and 15 for PostgreSQL. diff --git a/doc/src/sgml/file-fdw.sgml b/doc/src/sgml/file-fdw.sgml index 882d9a76d2169..3638689436fd9 100644 --- a/doc/src/sgml/file-fdw.sgml +++ b/doc/src/sgml/file-fdw.sgml @@ -65,7 +65,7 @@ - Specifies whether the data has a header line, + Specifies whether to skip a header line, or how many header lines to skip, the same as COPY's HEADER option. @@ -115,6 +115,17 @@ + + default + + + + Specifies the string that represents a default value, + the same as COPY's DEFAULT option. + + + + encoding @@ -168,7 +179,7 @@ to be specified without a corresponding value, the foreign table option syntax requires a value to be present in all cases. To activate COPY options typically written without a value, you can pass - the value TRUE, since all such options are Booleans. + the value TRUE, since all such options accept Booleans. diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index fef9584f908ec..25a85082759b4 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -17,7 +17,10 @@ - + + +%allfiles_func; + @@ -141,11 +144,13 @@ + + @@ -180,7 +185,7 @@ - + @@ -203,3 +208,4 @@ + diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml deleted file mode 100644 index c67688cbf5f98..0000000000000 --- a/doc/src/sgml/func.sgml +++ /dev/null @@ -1,32009 +0,0 @@ - - - - Functions and Operators - - - function - - - - operator - - - - PostgreSQL provides a large number of - functions and operators for the built-in data types. This chapter - describes most of them, although additional special-purpose functions - appear in relevant sections of the manual. Users can also - define their own functions and operators, as described in - . The - psql commands \df and - \do can be used to list all - available functions and operators, respectively. - - - - The notation used throughout this chapter to describe the argument and - result data types of a function or operator is like this: - -repeat ( text, integer ) text - - which says that the function repeat takes one text and - one integer argument and returns a result of type text. The right arrow - is also used to indicate the result of an example, thus: - -repeat('Pg', 4) PgPgPgPg - - - - - If you are concerned about portability then note that most of - the functions and operators described in this chapter, with the - exception of the most trivial arithmetic and comparison operators - and some explicitly marked functions, are not specified by the - SQL standard. Some of this extended functionality - is present in other SQL database management - systems, and in many cases this functionality is compatible and - consistent between the various implementations. - - - - - Logical Operators - - - operator - logical - - - - Boolean - operators - operators, logical - - - - The usual logical operators are available: - - - AND (operator) - - - - OR (operator) - - - - NOT (operator) - - - - conjunction - - - - disjunction - - - - negation - - - -boolean AND boolean boolean -boolean OR boolean boolean -NOT boolean boolean - - - SQL uses a three-valued logic system with true, - false, and null, which represents unknown. - Observe the following truth tables: - - - -
- - a - b - a AND b - a OR b - - - - - - TRUE - TRUE - TRUE - TRUE - - - - TRUE - FALSE - FALSE - TRUE - - - - TRUE - NULL - NULL - TRUE - - - - FALSE - FALSE - FALSE - FALSE - - - - FALSE - NULL - FALSE - NULL - - - - NULL - NULL - NULL - NULL - - - - - - - - - - a - NOT a - - - - - - TRUE - FALSE - - - - FALSE - TRUE - - - - NULL - NULL - - - - - - - - The operators AND and OR are - commutative, that is, you can switch the left and right operands - without affecting the result. (However, it is not guaranteed that - the left operand is evaluated before the right operand. See for more information about the - order of evaluation of subexpressions.) - - - - - Comparison Functions and Operators - - - comparison - operators - - - - The usual comparison operators are available, as shown in . - - -
- Comparison Operators - - - - Operator - Description - - - - - - - datatype < datatype - boolean - - Less than - - - - - datatype > datatype - boolean - - Greater than - - - - - datatype <= datatype - boolean - - Less than or equal to - - - - - datatype >= datatype - boolean - - Greater than or equal to - - - - - datatype = datatype - boolean - - Equal - - - - - datatype <> datatype - boolean - - Not equal - - - - - datatype != datatype - boolean - - Not equal - - - -
- - - - <> is the standard SQL notation for not - equal. != is an alias, which is converted - to <> at a very early stage of parsing. - Hence, it is not possible to implement != - and <> operators that do different things. - - - - - These comparison operators are available for all built-in data types - that have a natural ordering, including numeric, string, and date/time - types. In addition, arrays, composite types, and ranges can be compared - if their component data types are comparable. - - - - It is usually possible to compare values of related data - types as well; for example integer > - bigint will work. Some cases of this sort are implemented - directly by cross-type comparison operators, but if no - such operator is available, the parser will coerce the less-general type - to the more-general type and apply the latter's comparison operator. - - - - As shown above, all comparison operators are binary operators that - return values of type boolean. Thus, expressions like - 1 < 2 < 3 are not valid (because there is - no < operator to compare a Boolean value with - 3). Use the BETWEEN predicates - shown below to perform range tests. - - - - There are also some comparison predicates, as shown in . These behave much like - operators, but have special syntax mandated by the SQL standard. - - - - Comparison Predicates - - - - - Predicate - - - Description - - - Example(s) - - - - - - - - datatype BETWEEN datatype AND datatype - boolean - - - Between (inclusive of the range endpoints). - - - 2 BETWEEN 1 AND 3 - t - - - 2 BETWEEN 3 AND 1 - f - - - - - - datatype NOT BETWEEN datatype AND datatype - boolean - - - Not between (the negation of BETWEEN). - - - 2 NOT BETWEEN 1 AND 3 - f - - - - - - datatype BETWEEN SYMMETRIC datatype AND datatype - boolean - - - Between, after sorting the two endpoint values. - - - 2 BETWEEN SYMMETRIC 3 AND 1 - t - - - - - - datatype NOT BETWEEN SYMMETRIC datatype AND datatype - boolean - - - Not between, after sorting the two endpoint values. - - - 2 NOT BETWEEN SYMMETRIC 3 AND 1 - f - - - - - - datatype IS DISTINCT FROM datatype - boolean - - - Not equal, treating null as a comparable value. - - - 1 IS DISTINCT FROM NULL - t (rather than NULL) - - - NULL IS DISTINCT FROM NULL - f (rather than NULL) - - - - - - datatype IS NOT DISTINCT FROM datatype - boolean - - - Equal, treating null as a comparable value. - - - 1 IS NOT DISTINCT FROM NULL - f (rather than NULL) - - - NULL IS NOT DISTINCT FROM NULL - t (rather than NULL) - - - - - - datatype IS NULL - boolean - - - Test whether value is null. - - - 1.5 IS NULL - f - - - - - - datatype IS NOT NULL - boolean - - - Test whether value is not null. - - - 'null' IS NOT NULL - t - - - - - - datatype ISNULL - boolean - - - Test whether value is null (nonstandard syntax). - - - - - - datatype NOTNULL - boolean - - - Test whether value is not null (nonstandard syntax). - - - - - - boolean IS TRUE - boolean - - - Test whether boolean expression yields true. - - - true IS TRUE - t - - - NULL::boolean IS TRUE - f (rather than NULL) - - - - - - boolean IS NOT TRUE - boolean - - - Test whether boolean expression yields false or unknown. - - - true IS NOT TRUE - f - - - NULL::boolean IS NOT TRUE - t (rather than NULL) - - - - - - boolean IS FALSE - boolean - - - Test whether boolean expression yields false. - - - true IS FALSE - f - - - NULL::boolean IS FALSE - f (rather than NULL) - - - - - - boolean IS NOT FALSE - boolean - - - Test whether boolean expression yields true or unknown. - - - true IS NOT FALSE - t - - - NULL::boolean IS NOT FALSE - t (rather than NULL) - - - - - - boolean IS UNKNOWN - boolean - - - Test whether boolean expression yields unknown. - - - true IS UNKNOWN - f - - - NULL::boolean IS UNKNOWN - t (rather than NULL) - - - - - - boolean IS NOT UNKNOWN - boolean - - - Test whether boolean expression yields true or false. - - - true IS NOT UNKNOWN - t - - - NULL::boolean IS NOT UNKNOWN - f (rather than NULL) - - - - -
- - - - BETWEEN - - - BETWEEN SYMMETRIC - - The BETWEEN predicate simplifies range tests: - -a BETWEEN x AND y - - is equivalent to - -a >= x AND a <= y - - Notice that BETWEEN treats the endpoint values as included - in the range. - BETWEEN SYMMETRIC is like BETWEEN - except there is no requirement that the argument to the left of - AND be less than or equal to the argument on the right. - If it is not, those two arguments are automatically swapped, so that - a nonempty range is always implied. - - - - The various variants of BETWEEN are implemented in - terms of the ordinary comparison operators, and therefore will work for - any data type(s) that can be compared. - - - - - The use of AND in the BETWEEN - syntax creates an ambiguity with the use of AND as a - logical operator. To resolve this, only a limited set of expression - types are allowed as the second argument of a BETWEEN - clause. If you need to write a more complex sub-expression - in BETWEEN, write parentheses around the - sub-expression. - - - - - - IS DISTINCT FROM - - - IS NOT DISTINCT FROM - - Ordinary comparison operators yield null (signifying unknown), - not true or false, when either input is null. For example, - 7 = NULL yields null, as does 7 <> NULL. When - this behavior is not suitable, use the - IS NOT DISTINCT FROM predicates: - -a IS DISTINCT FROM b -a IS NOT DISTINCT FROM b - - For non-null inputs, IS DISTINCT FROM is - the same as the <> operator. However, if both - inputs are null it returns false, and if only one input is - null it returns true. Similarly, IS NOT DISTINCT - FROM is identical to = for non-null - inputs, but it returns true when both inputs are null, and false when only - one input is null. Thus, these predicates effectively act as though null - were a normal data value, rather than unknown. - - - - - IS NULL - - - IS NOT NULL - - - ISNULL - - - NOTNULL - - To check whether a value is or is not null, use the predicates: - -expression IS NULL -expression IS NOT NULL - - or the equivalent, but nonstandard, predicates: - -expression ISNULL -expression NOTNULL - - null valuecomparing - - - - Do not write - expression = NULL - because NULL is not equal to - NULL. (The null value represents an unknown value, - and it is not known whether two unknown values are equal.) - - - - - Some applications might expect that - expression = NULL - returns true if expression evaluates to - the null value. It is highly recommended that these applications - be modified to comply with the SQL standard. However, if that - cannot be done the - configuration variable is available. If it is enabled, - PostgreSQL will convert x = - NULL clauses to x IS NULL. - - - - - If the expression is row-valued, then - IS NULL is true when the row expression itself is null - or when all the row's fields are null, while - IS NOT NULL is true when the row expression itself is non-null - and all the row's fields are non-null. Because of this behavior, - IS NULL and IS NOT NULL do not always return - inverse results for row-valued expressions; in particular, a row-valued - expression that contains both null and non-null fields will return false - for both tests. For example: - - -SELECT ROW(1,2.5,'this is a test') = ROW(1, 3, 'not the same'); - -SELECT ROW(table.*) IS NULL FROM table; -- detect all-null rows - -SELECT ROW(table.*) IS NOT NULL FROM table; -- detect all-non-null rows - -SELECT NOT(ROW(table.*) IS NOT NULL) FROM TABLE; -- detect at least one null in rows - - - In some cases, it may be preferable to - write row IS DISTINCT FROM NULL - or row IS NOT DISTINCT FROM NULL, - which will simply check whether the overall row value is null without any - additional tests on the row fields. - - - - - IS TRUE - - - IS NOT TRUE - - - IS FALSE - - - IS NOT FALSE - - - IS UNKNOWN - - - IS NOT UNKNOWN - - Boolean values can also be tested using the predicates - -boolean_expression IS TRUE -boolean_expression IS NOT TRUE -boolean_expression IS FALSE -boolean_expression IS NOT FALSE -boolean_expression IS UNKNOWN -boolean_expression IS NOT UNKNOWN - - These will always return true or false, never a null value, even when the - operand is null. - A null input is treated as the logical value unknown. - Notice that IS UNKNOWN and IS NOT UNKNOWN are - effectively the same as IS NULL and - IS NOT NULL, respectively, except that the input - expression must be of Boolean type. - - - - Some comparison-related functions are also available, as shown in . - - - - Comparison Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - num_nonnulls - - num_nonnulls ( VARIADIC "any" ) - integer - - - Returns the number of non-null arguments. - - - num_nonnulls(1, NULL, 2) - 2 - - - - - - num_nulls - - num_nulls ( VARIADIC "any" ) - integer - - - Returns the number of null arguments. - - - num_nulls(1, NULL, 2) - 1 - - - - -
- - - - - Mathematical Functions and Operators - - - Mathematical operators are provided for many - PostgreSQL types. For types without - standard mathematical conventions - (e.g., date/time types) we - describe the actual behavior in subsequent sections. - - - - shows the mathematical - operators that are available for the standard numeric types. - Unless otherwise noted, operators shown as - accepting numeric_type are available for all - the types smallint, integer, - bigint, numeric, real, - and double precision. - Operators shown as accepting integral_type - are available for the types smallint, integer, - and bigint. - Except where noted, each form of an operator returns the same data type - as its argument(s). Calls involving multiple argument data types, such - as integer + numeric, - are resolved by using the type appearing later in these lists. - - - - Mathematical Operators - - - - - - Operator - - - Description - - - Example(s) - - - - - - - - numeric_type + numeric_type - numeric_type - - - Addition - - - 2 + 3 - 5 - - - - - - + numeric_type - numeric_type - - - Unary plus (no operation) - - - + 3.5 - 3.5 - - - - - - numeric_type - numeric_type - numeric_type - - - Subtraction - - - 2 - 3 - -1 - - - - - - - numeric_type - numeric_type - - - Negation - - - - (-4) - 4 - - - - - - numeric_type * numeric_type - numeric_type - - - Multiplication - - - 2 * 3 - 6 - - - - - - numeric_type / numeric_type - numeric_type - - - Division (for integral types, division truncates the result towards - zero) - - - 5.0 / 2 - 2.5000000000000000 - - - 5 / 2 - 2 - - - (-5) / 2 - -2 - - - - - - numeric_type % numeric_type - numeric_type - - - Modulo (remainder); available for smallint, - integer, bigint, and numeric - - - 5 % 4 - 1 - - - - - - numeric ^ numeric - numeric - - - double precision ^ double precision - double precision - - - Exponentiation - - - 2 ^ 3 - 8 - - - Unlike typical mathematical practice, multiple uses of - ^ will associate left to right by default: - - - 2 ^ 3 ^ 3 - 512 - - - 2 ^ (3 ^ 3) - 134217728 - - - - - - |/ double precision - double precision - - - Square root - - - |/ 25.0 - 5 - - - - - - ||/ double precision - double precision - - - Cube root - - - ||/ 64.0 - 4 - - - - - - @ numeric_type - numeric_type - - - Absolute value - - - @ -5.0 - 5.0 - - - - - - integral_type & integral_type - integral_type - - - Bitwise AND - - - 91 & 15 - 11 - - - - - - integral_type | integral_type - integral_type - - - Bitwise OR - - - 32 | 3 - 35 - - - - - - integral_type # integral_type - integral_type - - - Bitwise exclusive OR - - - 17 # 5 - 20 - - - - - - ~ integral_type - integral_type - - - Bitwise NOT - - - ~1 - -2 - - - - - - integral_type << integer - integral_type - - - Bitwise shift left - - - 1 << 4 - 16 - - - - - - integral_type >> integer - integral_type - - - Bitwise shift right - - - 8 >> 2 - 2 - - - - - -
- - - shows the available - mathematical functions. - Many of these functions are provided in multiple forms with different - argument types. - Except where noted, any given form of a function returns the same - data type as its argument(s); cross-type cases are resolved in the - same way as explained above for operators. - The functions working with double precision data are mostly - implemented on top of the host system's C library; accuracy and behavior in - boundary cases can therefore vary depending on the host system. - - - - Mathematical Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - abs - - abs ( numeric_type ) - numeric_type - - - Absolute value - - - abs(-17.4) - 17.4 - - - - - - - cbrt - - cbrt ( double precision ) - double precision - - - Cube root - - - cbrt(64.0) - 4 - - - - - - - ceil - - ceil ( numeric ) - numeric - - - ceil ( double precision ) - double precision - - - Nearest integer greater than or equal to argument - - - ceil(42.2) - 43 - - - ceil(-42.8) - -42 - - - - - - - ceiling - - ceiling ( numeric ) - numeric - - - ceiling ( double precision ) - double precision - - - Nearest integer greater than or equal to argument (same - as ceil) - - - ceiling(95.3) - 96 - - - - - - - degrees - - degrees ( double precision ) - double precision - - - Converts radians to degrees - - - degrees(0.5) - 28.64788975654116 - - - - - - - div - - div ( y numeric, - x numeric ) - numeric - - - Integer quotient of y/x - (truncates towards zero) - - - div(9, 4) - 2 - - - - - - - erf - - erf ( double precision ) - double precision - - - Error function - - - erf(1.0) - 0.8427007929497149 - - - - - - - erfc - - erfc ( double precision ) - double precision - - - Complementary error function (1 - erf(x), without - loss of precision for large inputs) - - - erfc(1.0) - 0.15729920705028513 - - - - - - - exp - - exp ( numeric ) - numeric - - - exp ( double precision ) - double precision - - - Exponential (e raised to the given power) - - - exp(1.0) - 2.7182818284590452 - - - - - - - factorial - - factorial ( bigint ) - numeric - - - Factorial - - - factorial(5) - 120 - - - - - - - floor - - floor ( numeric ) - numeric - - - floor ( double precision ) - double precision - - - Nearest integer less than or equal to argument - - - floor(42.8) - 42 - - - floor(-42.8) - -43 - - - - - - - gamma - - gamma ( double precision ) - double precision - - - Gamma function - - - gamma(0.5) - 1.772453850905516 - - - gamma(6) - 120 - - - - - - - gcd - - gcd ( numeric_type, numeric_type ) - numeric_type - - - Greatest common divisor (the largest positive number that divides both - inputs with no remainder); returns 0 if both inputs - are zero; available for integer, bigint, - and numeric - - - gcd(1071, 462) - 21 - - - - - - - lcm - - lcm ( numeric_type, numeric_type ) - numeric_type - - - Least common multiple (the smallest strictly positive number that is - an integral multiple of both inputs); returns 0 if - either input is zero; available for integer, - bigint, and numeric - - - lcm(1071, 462) - 23562 - - - - - - - lgamma - - lgamma ( double precision ) - double precision - - - Natural logarithm of the absolute value of the gamma function - - - lgamma(1000) - 5905.220423209181 - - - - - - - ln - - ln ( numeric ) - numeric - - - ln ( double precision ) - double precision - - - Natural logarithm - - - ln(2.0) - 0.6931471805599453 - - - - - - - log - - log ( numeric ) - numeric - - - log ( double precision ) - double precision - - - Base 10 logarithm - - - log(100) - 2 - - - - - - - log10 - - log10 ( numeric ) - numeric - - - log10 ( double precision ) - double precision - - - Base 10 logarithm (same as log) - - - log10(1000) - 3 - - - - - - log ( b numeric, - x numeric ) - numeric - - - Logarithm of x to base b - - - log(2.0, 64.0) - 6.0000000000000000 - - - - - - - min_scale - - min_scale ( numeric ) - integer - - - Minimum scale (number of fractional decimal digits) needed - to represent the supplied value precisely - - - min_scale(8.4100) - 2 - - - - - - - mod - - mod ( y numeric_type, - x numeric_type ) - numeric_type - - - Remainder of y/x; - available for smallint, integer, - bigint, and numeric - - - mod(9, 4) - 1 - - - - - - - pi - - pi ( ) - double precision - - - Approximate value of π - - - pi() - 3.141592653589793 - - - - - - - power - - power ( a numeric, - b numeric ) - numeric - - - power ( a double precision, - b double precision ) - double precision - - - a raised to the power of b - - - power(9, 3) - 729 - - - - - - - radians - - radians ( double precision ) - double precision - - - Converts degrees to radians - - - radians(45.0) - 0.7853981633974483 - - - - - - - round - - round ( numeric ) - numeric - - - round ( double precision ) - double precision - - - Rounds to nearest integer. For numeric, ties are - broken by rounding away from zero. For double precision, - the tie-breaking behavior is platform dependent, but - round to nearest even is the most common rule. - - - round(42.4) - 42 - - - - - - round ( v numeric, s integer ) - numeric - - - Rounds v to s decimal - places. Ties are broken by rounding away from zero. - - - round(42.4382, 2) - 42.44 - - - round(1234.56, -1) - 1230 - - - - - - - scale - - scale ( numeric ) - integer - - - Scale of the argument (the number of decimal digits in the fractional part) - - - scale(8.4100) - 4 - - - - - - - sign - - sign ( numeric ) - numeric - - - sign ( double precision ) - double precision - - - Sign of the argument (-1, 0, or +1) - - - sign(-8.4) - -1 - - - - - - - sqrt - - sqrt ( numeric ) - numeric - - - sqrt ( double precision ) - double precision - - - Square root - - - sqrt(2) - 1.4142135623730951 - - - - - - - trim_scale - - trim_scale ( numeric ) - numeric - - - Reduces the value's scale (number of fractional decimal digits) by - removing trailing zeroes - - - trim_scale(8.4100) - 8.41 - - - - - - - trunc - - trunc ( numeric ) - numeric - - - trunc ( double precision ) - double precision - - - Truncates to integer (towards zero) - - - trunc(42.8) - 42 - - - trunc(-42.8) - -42 - - - - - - trunc ( v numeric, s integer ) - numeric - - - Truncates v to s - decimal places - - - trunc(42.4382, 2) - 42.43 - - - - - - - width_bucket - - width_bucket ( operand numeric, low numeric, high numeric, count integer ) - integer - - - width_bucket ( operand double precision, low double precision, high double precision, count integer ) - integer - - - Returns the number of the bucket in - which operand falls in a histogram - having count equal-width buckets spanning the - range low to high. - Returns 0 - or count+1 for an input - outside that range. - - - width_bucket(5.35, 0.024, 10.06, 5) - 3 - - - - - - width_bucket ( operand anycompatible, thresholds anycompatiblearray ) - integer - - - Returns the number of the bucket in - which operand falls given an array listing the - lower bounds of the buckets. Returns 0 for an - input less than the first lower - bound. operand and the array elements can be - of any type having standard comparison operators. - The thresholds array must be - sorted, smallest first, or unexpected results will be - obtained. - - - width_bucket(now(), array['yesterday', 'today', 'tomorrow']::timestamptz[]) - 2 - - - - -
- - - shows functions for - generating random numbers. - - - - Random Functions - - - - - - Function - - - Description - - - Example(s) - - - - - - - - - random - - random ( ) - double precision - - - Returns a random value in the range 0.0 <= x < 1.0 - - - random() - 0.897124072839091 - - - - - - - random - - random ( min integer, max integer ) - integer - - - random ( min bigint, max bigint ) - bigint - - - random ( min numeric, max numeric ) - numeric - - - Returns a random value in the range - min <= x <= max. - For type numeric, the result will have the same number of - fractional decimal digits as min or - max, whichever has more. - - - random(1, 10) - 7 - - - random(-0.499, 0.499) - 0.347 - - - - - - - random_normal - - - random_normal ( - mean double precision - , stddev double precision ) - double precision - - - Returns a random value from the normal distribution with the given - parameters; mean defaults to 0.0 - and stddev defaults to 1.0 - - - random_normal(0.0, 1.0) - 0.051285419 - - - - - - - setseed - - setseed ( double precision ) - void - - - Sets the seed for subsequent random() and - random_normal() calls; - argument must be between -1.0 and 1.0, inclusive - - - setseed(0.12345) - - - - -
- - - The random() and random_normal() - functions listed in use a - deterministic pseudo-random number generator. - It is fast but not suitable for cryptographic - applications; see the module for a more - secure alternative. - If setseed() is called, the series of results of - subsequent calls to these functions in the current session - can be repeated by re-issuing setseed() with the same - argument. - Without any prior setseed() call in the same - session, the first call to any of these functions obtains a seed - from a platform-dependent source of random bits. - - - - shows the - available trigonometric functions. Each of these functions comes in - two variants, one that measures angles in radians and one that - measures angles in degrees. - - - - Trigonometric Functions - - - - - - Function - - - Description - - - Example(s) - - - - - - - - - acos - - acos ( double precision ) - double precision - - - Inverse cosine, result in radians - - - acos(1) - 0 - - - - - - - acosd - - acosd ( double precision ) - double precision - - - Inverse cosine, result in degrees - - - acosd(0.5) - 60 - - - - - - - asin - - asin ( double precision ) - double precision - - - Inverse sine, result in radians - - - asin(1) - 1.5707963267948966 - - - - - - - asind - - asind ( double precision ) - double precision - - - Inverse sine, result in degrees - - - asind(0.5) - 30 - - - - - - - atan - - atan ( double precision ) - double precision - - - Inverse tangent, result in radians - - - atan(1) - 0.7853981633974483 - - - - - - - atand - - atand ( double precision ) - double precision - - - Inverse tangent, result in degrees - - - atand(1) - 45 - - - - - - - atan2 - - atan2 ( y double precision, - x double precision ) - double precision - - - Inverse tangent of - y/x, - result in radians - - - atan2(1, 0) - 1.5707963267948966 - - - - - - - atan2d - - atan2d ( y double precision, - x double precision ) - double precision - - - Inverse tangent of - y/x, - result in degrees - - - atan2d(1, 0) - 90 - - - - - - - cos - - cos ( double precision ) - double precision - - - Cosine, argument in radians - - - cos(0) - 1 - - - - - - - cosd - - cosd ( double precision ) - double precision - - - Cosine, argument in degrees - - - cosd(60) - 0.5 - - - - - - - cot - - cot ( double precision ) - double precision - - - Cotangent, argument in radians - - - cot(0.5) - 1.830487721712452 - - - - - - - cotd - - cotd ( double precision ) - double precision - - - Cotangent, argument in degrees - - - cotd(45) - 1 - - - - - - - sin - - sin ( double precision ) - double precision - - - Sine, argument in radians - - - sin(1) - 0.8414709848078965 - - - - - - - sind - - sind ( double precision ) - double precision - - - Sine, argument in degrees - - - sind(30) - 0.5 - - - - - - - tan - - tan ( double precision ) - double precision - - - Tangent, argument in radians - - - tan(1) - 1.5574077246549023 - - - - - - - tand - - tand ( double precision ) - double precision - - - Tangent, argument in degrees - - - tand(45) - 1 - - - - -
- - - - Another way to work with angles measured in degrees is to use the unit - transformation functions radians() - and degrees() shown earlier. - However, using the degree-based trigonometric functions is preferred, - as that way avoids round-off error for special cases such - as sind(30). - - - - - shows the - available hyperbolic functions. - - - - Hyperbolic Functions - - - - - - Function - - - Description - - - Example(s) - - - - - - - - - sinh - - sinh ( double precision ) - double precision - - - Hyperbolic sine - - - sinh(1) - 1.1752011936438014 - - - - - - - cosh - - cosh ( double precision ) - double precision - - - Hyperbolic cosine - - - cosh(0) - 1 - - - - - - - tanh - - tanh ( double precision ) - double precision - - - Hyperbolic tangent - - - tanh(1) - 0.7615941559557649 - - - - - - - asinh - - asinh ( double precision ) - double precision - - - Inverse hyperbolic sine - - - asinh(1) - 0.881373587019543 - - - - - - - acosh - - acosh ( double precision ) - double precision - - - Inverse hyperbolic cosine - - - acosh(1) - 0 - - - - - - - atanh - - atanh ( double precision ) - double precision - - - Inverse hyperbolic tangent - - - atanh(0.5) - 0.5493061443340548 - - - - -
- -
- - - - String Functions and Operators - - - This section describes functions and operators for examining and - manipulating string values. Strings in this context include values - of the types character, character varying, - and text. Except where noted, these functions and operators - are declared to accept and return type text. They will - interchangeably accept character varying arguments. - Values of type character will be converted - to text before the function or operator is applied, resulting - in stripping any trailing spaces in the character value. - - - - SQL defines some string functions that use - key words, rather than commas, to separate - arguments. Details are in - . - PostgreSQL also provides versions of these functions - that use the regular function invocation syntax - (see ). - - - - - The string concatenation operator (||) will accept - non-string input, so long as at least one input is of string type, as shown - in . For other cases, inserting an - explicit coercion to text can be used to have non-string input - accepted. - - - - - <acronym>SQL</acronym> String Functions and Operators - - - - - Function/Operator - - - Description - - - Example(s) - - - - - - - - - character string - concatenation - - text || text - text - - - Concatenates the two strings. - - - 'Post' || 'greSQL' - PostgreSQL - - - - - - text || anynonarray - text - - - anynonarray || text - text - - - Converts the non-string input to text, then concatenates the two - strings. (The non-string input cannot be of an array type, because - that would create ambiguity with the array || - operators. If you want to concatenate an array's text equivalent, - cast it to text explicitly.) - - - 'Value: ' || 42 - Value: 42 - - - - - - - btrim - - btrim ( string text - , characters text ) - text - - - Removes the longest string containing only characters - in characters (a space by default) - from the start and end of string. - - - btrim('xyxtrimyyx', 'xyz') - trim - - - - - - - normalized - - - Unicode normalization - - text IS NOT form NORMALIZED - boolean - - - Checks whether the string is in the specified Unicode normalization - form. The optional form key word specifies the - form: NFC (the default), NFD, - NFKC, or NFKD. This expression can - only be used when the server encoding is UTF8. Note - that checking for normalization using this expression is often faster - than normalizing possibly already normalized strings. - - - U&'\0061\0308bc' IS NFD NORMALIZED - t - - - - - - - bit_length - - bit_length ( text ) - integer - - - Returns number of bits in the string (8 - times the octet_length). - - - bit_length('jose') - 32 - - - - - - - char_length - - - character string - length - - - length - of a character string - character string, length - - char_length ( text ) - integer - - - - character_length - - character_length ( text ) - integer - - - Returns number of characters in the string. - - - char_length('josé') - 4 - - - - - - - lower - - lower ( text ) - text - - - Converts the string to all lower case, according to the rules of the - database's locale. - - - lower('TOM') - tom - - - - - - - lpad - - lpad ( string text, - length integer - , fill text ) - text - - - Extends the string to length - length by prepending the characters - fill (a space by default). If the - string is already longer than - length then it is truncated (on the right). - - - lpad('hi', 5, 'xy') - xyxhi - - - - - - - ltrim - - ltrim ( string text - , characters text ) - text - - - Removes the longest string containing only characters in - characters (a space by default) from the start of - string. - - - ltrim('zzzytest', 'xyz') - test - - - - - - - normalize - - - Unicode normalization - - normalize ( text - , form ) - text - - - Converts the string to the specified Unicode - normalization form. The optional form key word - specifies the form: NFC (the default), - NFD, NFKC, or - NFKD. This function can only be used when the - server encoding is UTF8. - - - normalize(U&'\0061\0308bc', NFC) - U&'\00E4bc' - - - - - - - octet_length - - octet_length ( text ) - integer - - - Returns number of bytes in the string. - - - octet_length('josé') - 5 (if server encoding is UTF8) - - - - - - - octet_length - - octet_length ( character ) - integer - - - Returns number of bytes in the string. Since this version of the - function accepts type character directly, it will not - strip trailing spaces. - - - octet_length('abc '::character(4)) - 4 - - - - - - - overlay - - overlay ( string text PLACING newsubstring text FROM start integer FOR count integer ) - text - - - Replaces the substring of string that starts at - the start'th character and extends - for count characters - with newsubstring. - If count is omitted, it defaults to the length - of newsubstring. - - - overlay('Txxxxas' placing 'hom' from 2 for 4) - Thomas - - - - - - - position - - position ( substring text IN string text ) - integer - - - Returns first starting index of the specified - substring within - string, or zero if it's not present. - - - position('om' in 'Thomas') - 3 - - - - - - - rpad - - rpad ( string text, - length integer - , fill text ) - text - - - Extends the string to length - length by appending the characters - fill (a space by default). If the - string is already longer than - length then it is truncated. - - - rpad('hi', 5, 'xy') - hixyx - - - - - - - rtrim - - rtrim ( string text - , characters text ) - text - - - Removes the longest string containing only characters in - characters (a space by default) from the end of - string. - - - rtrim('testxxzx', 'xyz') - test - - - - - - - substring - - substring ( string text FROM start integer FOR count integer ) - text - - - Extracts the substring of string starting at - the start'th character if that is specified, - and stopping after count characters if that is - specified. Provide at least one of start - and count. - - - substring('Thomas' from 2 for 3) - hom - - - substring('Thomas' from 3) - omas - - - substring('Thomas' for 2) - Th - - - - - - substring ( string text FROM pattern text ) - text - - - Extracts the first substring matching POSIX regular expression; see - . - - - substring('Thomas' from '...$') - mas - - - - - - substring ( string text SIMILAR pattern text ESCAPE escape text ) - text - - - substring ( string text FROM pattern text FOR escape text ) - text - - - Extracts the first substring matching SQL regular expression; - see . The first form has - been specified since SQL:2003; the second form was only in SQL:1999 - and should be considered obsolete. - - - substring('Thomas' similar '%#"o_a#"_' escape '#') - oma - - - - - - - trim - - trim ( LEADING | TRAILING | BOTH - characters text FROM - string text ) - text - - - Removes the longest string containing only characters in - characters (a space by default) from the - start, end, or both ends (BOTH is the default) - of string. - - - trim(both 'xyz' from 'yxTomxx') - Tom - - - - - - trim ( LEADING | TRAILING | BOTH FROM - string text , - characters text ) - text - - - This is a non-standard syntax for trim(). - - - trim(both from 'yxTomxx', 'xyz') - Tom - - - - - - - unicode_assigned - - unicode_assigned ( text ) - boolean - - - Returns true if all characters in the string are - assigned Unicode codepoints; false otherwise. This - function can only be used when the server encoding is - UTF8. - - - - - - - upper - - upper ( text ) - text - - - Converts the string to all upper case, according to the rules of the - database's locale. - - - upper('tom') - TOM - - - - -
- - - Additional string manipulation functions and operators are available - and are listed in . (Some of - these are used internally to implement - the SQL-standard string functions listed in - .) - There are also pattern-matching operators, which are described in - , and operators for full-text - search, which are described in . - - - - Other String Functions and Operators - - - - - Function/Operator - - - Description - - - Example(s) - - - - - - - - - character string - prefix test - - text ^@ text - boolean - - - Returns true if the first string starts with the second string - (equivalent to the starts_with() function). - - - 'alphabet' ^@ 'alph' - t - - - - - - - ascii - - ascii ( text ) - integer - - - Returns the numeric code of the first character of the argument. - In UTF8 encoding, returns the Unicode code point - of the character. In other multibyte encodings, the argument must - be an ASCII character. - - - ascii('x') - 120 - - - - - - - chr - - chr ( integer ) - text - - - Returns the character with the given code. In UTF8 - encoding the argument is treated as a Unicode code point. In other - multibyte encodings the argument must designate - an ASCII character. chr(0) is - disallowed because text data types cannot store that character. - - - chr(65) - A - - - - - - - concat - - concat ( val1 "any" - , val2 "any" , ... ) - text - - - Concatenates the text representations of all the arguments. - NULL arguments are ignored. - - - concat('abcde', 2, NULL, 22) - abcde222 - - - - - - - concat_ws - - concat_ws ( sep text, - val1 "any" - , val2 "any" , ... ) - text - - - Concatenates all but the first argument, with separators. The first - argument is used as the separator string, and should not be NULL. - Other NULL arguments are ignored. - - - concat_ws(',', 'abcde', 2, NULL, 22) - abcde,2,22 - - - - - - - format - - format ( formatstr text - , formatarg "any" , ... ) - text - - - Formats arguments according to a format string; - see . - This function is similar to the C function sprintf. - - - format('Hello %s, %1$s', 'World') - Hello World, World - - - - - - - initcap - - initcap ( text ) - text - - - Converts the first letter of each word to upper case and the - rest to lower case. Words are sequences of alphanumeric - characters separated by non-alphanumeric characters. - - - initcap('hi THOMAS') - Hi Thomas - - - - - - - casefold - - casefold ( text ) - text - - - Performs case folding of the input string according to the collation. - Case folding is similar to case conversion, but the purpose of case - folding is to facilitate case-insensitive matching of strings, - whereas the purpose of case conversion is to convert to a particular - cased form. This function can only be used when the server encoding - is UTF8. - - - Ordinarily, case folding simply converts to lowercase, but there may - be exceptions depending on the collation. For instance, some - characters have more than two lowercase variants, or fold to uppercase. - - - Case folding may change the length of the string. For instance, in - the PG_UNICODE_FAST collation, ß - (U+00DF) folds to ss. - - - casefold can be used for Unicode Default Caseless - Matching. It does not always preserve the normalized form of the - input string (see ). - - - The libc provider doesn't support case folding, so - casefold is identical to . - - - - - - - left - - left ( string text, - n integer ) - text - - - Returns first n characters in the - string, or when n is negative, returns - all but last |n| characters. - - - left('abcde', 2) - ab - - - - - - - length - - length ( text ) - integer - - - Returns the number of characters in the string. - - - length('jose') - 4 - - - - - - - md5 - - md5 ( text ) - text - - - Computes the MD5 hash of - the argument, with the result written in hexadecimal. - - - md5('abc') - 900150983cd24fb0&zwsp;d6963f7d28e17f72 - - - - - - - parse_ident - - parse_ident ( qualified_identifier text - , strict_mode boolean DEFAULT true ) - text[] - - - Splits qualified_identifier into an array of - identifiers, removing any quoting of individual identifiers. By - default, extra characters after the last identifier are considered an - error; but if the second parameter is false, then such - extra characters are ignored. (This behavior is useful for parsing - names for objects like functions.) Note that this function does not - truncate over-length identifiers. If you want truncation you can cast - the result to name[]. - - - parse_ident('"SomeSchema".someTable') - {SomeSchema,sometable} - - - - - - - pg_client_encoding - - pg_client_encoding ( ) - name - - - Returns current client encoding name. - - - pg_client_encoding() - UTF8 - - - - - - - quote_ident - - quote_ident ( text ) - text - - - Returns the given string suitably quoted to be used as an identifier - in an SQL statement string. - Quotes are added only if necessary (i.e., if the string contains - non-identifier characters or would be case-folded). - Embedded quotes are properly doubled. - See also . - - - quote_ident('Foo bar') - "Foo bar" - - - - - - - quote_literal - - quote_literal ( text ) - text - - - Returns the given string suitably quoted to be used as a string literal - in an SQL statement string. - Embedded single-quotes and backslashes are properly doubled. - Note that quote_literal returns null on null - input; if the argument might be null, - quote_nullable is often more suitable. - See also . - - - quote_literal(E'O\'Reilly') - 'O''Reilly' - - - - - - quote_literal ( anyelement ) - text - - - Converts the given value to text and then quotes it as a literal. - Embedded single-quotes and backslashes are properly doubled. - - - quote_literal(42.5) - '42.5' - - - - - - - quote_nullable - - quote_nullable ( text ) - text - - - Returns the given string suitably quoted to be used as a string literal - in an SQL statement string; or, if the argument - is null, returns NULL. - Embedded single-quotes and backslashes are properly doubled. - See also . - - - quote_nullable(NULL) - NULL - - - - - - quote_nullable ( anyelement ) - text - - - Converts the given value to text and then quotes it as a literal; - or, if the argument is null, returns NULL. - Embedded single-quotes and backslashes are properly doubled. - - - quote_nullable(42.5) - '42.5' - - - - - - - regexp_count - - regexp_count ( string text, pattern text - , start integer - , flags text ) - integer - - - Returns the number of times the POSIX regular - expression pattern matches in - the string; see - . - - - regexp_count('123456789012', '\d\d\d', 2) - 3 - - - - - - - regexp_instr - - regexp_instr ( string text, pattern text - , start integer - , N integer - , endoption integer - , flags text - , subexpr integer ) - integer - - - Returns the position within string where - the N'th match of the POSIX regular - expression pattern occurs, or zero if there is - no such match; see . - - - regexp_instr('ABCDEF', 'c(.)(..)', 1, 1, 0, 'i') - 3 - - - regexp_instr('ABCDEF', 'c(.)(..)', 1, 1, 0, 'i', 2) - 5 - - - - - - - regexp_like - - regexp_like ( string text, pattern text - , flags text ) - boolean - - - Checks whether a match of the POSIX regular - expression pattern occurs - within string; see - . - - - regexp_like('Hello World', 'world$', 'i') - t - - - - - - - regexp_match - - regexp_match ( string text, pattern text , flags text ) - text[] - - - Returns substrings within the first match of the POSIX regular - expression pattern to - the string; see - . - - - regexp_match('foobarbequebaz', '(bar)(beque)') - {bar,beque} - - - - - - - regexp_matches - - regexp_matches ( string text, pattern text , flags text ) - setof text[] - - - Returns substrings within the first match of the POSIX regular - expression pattern to - the string, or substrings within all - such matches if the g flag is used; - see . - - - regexp_matches('foobarbequebaz', 'ba.', 'g') - - - {bar} - {baz} - - - - - - - - regexp_replace - - regexp_replace ( string text, pattern text, replacement text - , flags text ) - text - - - Replaces the substring that is the first match to the POSIX - regular expression pattern, or all such - matches if the g flag is used; see - . - - - regexp_replace('Thomas', '.[mN]a.', 'M') - ThM - - - - - - regexp_replace ( string text, pattern text, replacement text, - start integer - , N integer - , flags text ) - text - - - Replaces the substring that is the N'th - match to the POSIX regular expression pattern, - or all such matches if N is zero, with the - search beginning at the start'th character - of string. If N is - omitted, it defaults to 1. See - . - - - regexp_replace('Thomas', '.', 'X', 3, 2) - ThoXas - - - regexp_replace(string=>'hello world', pattern=>'l', replacement=>'XX', start=>1, "N"=>2) - helXXo world - - - - - - - regexp_split_to_array - - regexp_split_to_array ( string text, pattern text , flags text ) - text[] - - - Splits string using a POSIX regular - expression as the delimiter, producing an array of results; see - . - - - regexp_split_to_array('hello world', '\s+') - {hello,world} - - - - - - - regexp_split_to_table - - regexp_split_to_table ( string text, pattern text , flags text ) - setof text - - - Splits string using a POSIX regular - expression as the delimiter, producing a set of results; see - . - - - regexp_split_to_table('hello world', '\s+') - - - hello - world - - - - - - - - regexp_substr - - regexp_substr ( string text, pattern text - , start integer - , N integer - , flags text - , subexpr integer ) - text - - - Returns the substring within string that - matches the N'th occurrence of the POSIX - regular expression pattern, - or NULL if there is no such match; see - . - - - regexp_substr('ABCDEF', 'c(.)(..)', 1, 1, 'i') - CDEF - - - regexp_substr('ABCDEF', 'c(.)(..)', 1, 1, 'i', 2) - EF - - - - - - - repeat - - repeat ( string text, number integer ) - text - - - Repeats string the specified - number of times. - - - repeat('Pg', 4) - PgPgPgPg - - - - - - - replace - - replace ( string text, - from text, - to text ) - text - - - Replaces all occurrences in string of - substring from with - substring to. - - - replace('abcdefabcdef', 'cd', 'XX') - abXXefabXXef - - - - - - - reverse - - reverse ( text ) - text - - - Reverses the order of the characters in the string. - - - reverse('abcde') - edcba - - - - - - - right - - right ( string text, - n integer ) - text - - - Returns last n characters in the string, - or when n is negative, returns all but - first |n| characters. - - - right('abcde', 2) - de - - - - - - - split_part - - split_part ( string text, - delimiter text, - n integer ) - text - - - Splits string at occurrences - of delimiter and returns - the n'th field (counting from one), - or when n is negative, returns - the |n|'th-from-last field. - - - split_part('abc~@~def~@~ghi', '~@~', 2) - def - - - split_part('abc,def,ghi,jkl', ',', -2) - ghi - - - - - - - starts_with - - starts_with ( string text, prefix text ) - boolean - - - Returns true if string starts - with prefix. - - - starts_with('alphabet', 'alph') - t - - - - - - - string_to_array - - string_to_array ( string text, delimiter text , null_string text ) - text[] - - - Splits the string at occurrences - of delimiter and forms the resulting fields - into a text array. - If delimiter is NULL, - each character in the string will become a - separate element in the array. - If delimiter is an empty string, then - the string is treated as a single field. - If null_string is supplied and is - not NULL, fields matching that string are - replaced by NULL. - See also array_to_string. - - - string_to_array('xx~~yy~~zz', '~~', 'yy') - {xx,NULL,zz} - - - - - - - string_to_table - - string_to_table ( string text, delimiter text , null_string text ) - setof text - - - Splits the string at occurrences - of delimiter and returns the resulting fields - as a set of text rows. - If delimiter is NULL, - each character in the string will become a - separate row of the result. - If delimiter is an empty string, then - the string is treated as a single field. - If null_string is supplied and is - not NULL, fields matching that string are - replaced by NULL. - - - string_to_table('xx~^~yy~^~zz', '~^~', 'yy') - - - xx - NULL - zz - - - - - - - - strpos - - strpos ( string text, substring text ) - integer - - - Returns first starting index of the specified substring - within string, or zero if it's not present. - (Same as position(substring in - string), but note the reversed - argument order.) - - - strpos('high', 'ig') - 2 - - - - - - - substr - - substr ( string text, start integer , count integer ) - text - - - Extracts the substring of string starting at - the start'th character, - and extending for count characters if that is - specified. (Same - as substring(string - from start - for count).) - - - substr('alphabet', 3) - phabet - - - substr('alphabet', 3, 2) - ph - - - - - - - to_ascii - - to_ascii ( string text ) - text - - - to_ascii ( string text, - encoding name ) - text - - - to_ascii ( string text, - encoding integer ) - text - - - Converts string to ASCII - from another encoding, which may be identified by name or number. - If encoding is omitted the database encoding - is assumed (which in practice is the only useful case). - The conversion consists primarily of dropping accents. - Conversion is only supported - from LATIN1, LATIN2, - LATIN9, and WIN1250 encodings. - (See the module for another, more flexible - solution.) - - - to_ascii('Karél') - Karel - - - - - - - to_bin - - to_bin ( integer ) - text - - - to_bin ( bigint ) - text - - - Converts the number to its equivalent two's complement binary - representation. - - - to_bin(2147483647) - 1111111111111111111111111111111 - - - to_bin(-1234) - 11111111111111111111101100101110 - - - - - - - to_hex - - to_hex ( integer ) - text - - - to_hex ( bigint ) - text - - - Converts the number to its equivalent two's complement hexadecimal - representation. - - - to_hex(2147483647) - 7fffffff - - - to_hex(-1234) - fffffb2e - - - - - - - to_oct - - to_oct ( integer ) - text - - - to_oct ( bigint ) - text - - - Converts the number to its equivalent two's complement octal - representation. - - - to_oct(2147483647) - 17777777777 - - - to_oct(-1234) - 37777775456 - - - - - - - translate - - translate ( string text, - from text, - to text ) - text - - - Replaces each character in string that - matches a character in the from set with the - corresponding character in the to - set. If from is longer than - to, occurrences of the extra characters in - from are deleted. - - - translate('12345', '143', 'ax') - a2x5 - - - - - - - unistr - - unistr ( text ) - text - - - Evaluate escaped Unicode characters in the argument. Unicode characters - can be specified as - \XXXX (4 hexadecimal - digits), \+XXXXXX (6 - hexadecimal digits), - \uXXXX (4 hexadecimal - digits), or \UXXXXXXXX - (8 hexadecimal digits). To specify a backslash, write two - backslashes. All other characters are taken literally. - - - - If the server encoding is not UTF-8, the Unicode code point identified - by one of these escape sequences is converted to the actual server - encoding; an error is reported if that's not possible. - - - - This function provides a (non-standard) alternative to string - constants with Unicode escapes (see ). - - - - unistr('d\0061t\+000061') - data - - - unistr('d\u0061t\U00000061') - data - - - - - -
- - - The concat, concat_ws and - format functions are variadic, so it is possible to - pass the values to be concatenated or formatted as an array marked with - the VARIADIC keyword (see ). The array's elements are - treated as if they were separate ordinary arguments to the function. - If the variadic array argument is NULL, concat - and concat_ws return NULL, but - format treats a NULL as a zero-element array. - - - - See also the aggregate function string_agg in - , and the functions for - converting between strings and the bytea type in - . - - - - <function>format</function> - - - format - - - - The function format produces output formatted according to - a format string, in a style similar to the C function - sprintf. - - - - -format(formatstr text , formatarg "any" , ... ) - - formatstr is a format string that specifies how the - result should be formatted. Text in the format string is copied - directly to the result, except where format specifiers are - used. Format specifiers act as placeholders in the string, defining how - subsequent function arguments should be formatted and inserted into the - result. Each formatarg argument is converted to text - according to the usual output rules for its data type, and then formatted - and inserted into the result string according to the format specifier(s). - - - - Format specifiers are introduced by a % character and have - the form - -%[position][flags][width]type - - where the component fields are: - - - - position (optional) - - - A string of the form n$ where - n is the index of the argument to print. - Index 1 means the first argument after - formatstr. If the position is - omitted, the default is to use the next argument in sequence. - - - - - - flags (optional) - - - Additional options controlling how the format specifier's output is - formatted. Currently the only supported flag is a minus sign - (-) which will cause the format specifier's output to be - left-justified. This has no effect unless the width - field is also specified. - - - - - - width (optional) - - - Specifies the minimum number of characters to use to - display the format specifier's output. The output is padded on the - left or right (depending on the - flag) with spaces as - needed to fill the width. A too-small width does not cause - truncation of the output, but is simply ignored. The width may be - specified using any of the following: a positive integer; an - asterisk (*) to use the next function argument as the - width; or a string of the form *n$ to - use the nth function argument as the width. - - - - If the width comes from a function argument, that argument is - consumed before the argument that is used for the format specifier's - value. If the width argument is negative, the result is left - aligned (as if the - flag had been specified) within a - field of length abs(width). - - - - - - type (required) - - - The type of format conversion to use to produce the format - specifier's output. The following types are supported: - - - - s formats the argument value as a simple - string. A null value is treated as an empty string. - - - - - I treats the argument value as an SQL - identifier, double-quoting it if necessary. - It is an error for the value to be null (equivalent to - quote_ident). - - - - - L quotes the argument value as an SQL literal. - A null value is displayed as the string NULL, without - quotes (equivalent to quote_nullable). - - - - - - - - - - - In addition to the format specifiers described above, the special sequence - %% may be used to output a literal % character. - - - - Here are some examples of the basic format conversions: - - -SELECT format('Hello %s', 'World'); -Result: Hello World - -SELECT format('Testing %s, %s, %s, %%', 'one', 'two', 'three'); -Result: Testing one, two, three, % - -SELECT format('INSERT INTO %I VALUES(%L)', 'Foo bar', E'O\'Reilly'); -Result: INSERT INTO "Foo bar" VALUES('O''Reilly') - -SELECT format('INSERT INTO %I VALUES(%L)', 'locations', 'C:\Program Files'); -Result: INSERT INTO locations VALUES('C:\Program Files') - - - - - Here are examples using width fields - and the - flag: - - -SELECT format('|%10s|', 'foo'); -Result: | foo| - -SELECT format('|%-10s|', 'foo'); -Result: |foo | - -SELECT format('|%*s|', 10, 'foo'); -Result: | foo| - -SELECT format('|%*s|', -10, 'foo'); -Result: |foo | - -SELECT format('|%-*s|', 10, 'foo'); -Result: |foo | - -SELECT format('|%-*s|', -10, 'foo'); -Result: |foo | - - - - - These examples show use of position fields: - - -SELECT format('Testing %3$s, %2$s, %1$s', 'one', 'two', 'three'); -Result: Testing three, two, one - -SELECT format('|%*2$s|', 'foo', 10, 'bar'); -Result: | bar| - -SELECT format('|%1$*2$s|', 'foo', 10, 'bar'); -Result: | foo| - - - - - Unlike the standard C function sprintf, - PostgreSQL's format function allows format - specifiers with and without position fields to be mixed - in the same format string. A format specifier without a - position field always uses the next argument after the - last argument consumed. - In addition, the format function does not require all - function arguments to be used in the format string. - For example: - - -SELECT format('Testing %3$s, %2$s, %s', 'one', 'two', 'three'); -Result: Testing three, two, three - - - - - The %I and %L format specifiers are particularly - useful for safely constructing dynamic SQL statements. See - . - - - -
- - - - Binary String Functions and Operators - - - binary data - functions - - - - This section describes functions and operators for examining and - manipulating binary strings, that is values of type bytea. - Many of these are equivalent, in purpose and syntax, to the - text-string functions described in the previous section. - - - - SQL defines some string functions that use - key words, rather than commas, to separate - arguments. Details are in - . - PostgreSQL also provides versions of these functions - that use the regular function invocation syntax - (see ). - - - - <acronym>SQL</acronym> Binary String Functions and Operators - - - - - Function/Operator - - - Description - - - Example(s) - - - - - - - - - binary string - concatenation - - bytea || bytea - bytea - - - Concatenates the two binary strings. - - - '\x123456'::bytea || '\x789a00bcde'::bytea - \x123456789a00bcde - - - - - - - bit_length - - bit_length ( bytea ) - integer - - - Returns number of bits in the binary string (8 - times the octet_length). - - - bit_length('\x123456'::bytea) - 24 - - - - - - - btrim - - btrim ( bytes bytea, - bytesremoved bytea ) - bytea - - - Removes the longest string containing only bytes appearing in - bytesremoved from the start and end of - bytes. - - - btrim('\x1234567890'::bytea, '\x9012'::bytea) - \x345678 - - - - - - - ltrim - - ltrim ( bytes bytea, - bytesremoved bytea ) - bytea - - - Removes the longest string containing only bytes appearing in - bytesremoved from the start of - bytes. - - - ltrim('\x1234567890'::bytea, '\x9012'::bytea) - \x34567890 - - - - - - - octet_length - - octet_length ( bytea ) - integer - - - Returns number of bytes in the binary string. - - - octet_length('\x123456'::bytea) - 3 - - - - - - - overlay - - overlay ( bytes bytea PLACING newsubstring bytea FROM start integer FOR count integer ) - bytea - - - Replaces the substring of bytes that starts at - the start'th byte and extends - for count bytes - with newsubstring. - If count is omitted, it defaults to the length - of newsubstring. - - - overlay('\x1234567890'::bytea placing '\002\003'::bytea from 2 for 3) - \x12020390 - - - - - - - position - - position ( substring bytea IN bytes bytea ) - integer - - - Returns first starting index of the specified - substring within - bytes, or zero if it's not present. - - - position('\x5678'::bytea in '\x1234567890'::bytea) - 3 - - - - - - - rtrim - - rtrim ( bytes bytea, - bytesremoved bytea ) - bytea - - - Removes the longest string containing only bytes appearing in - bytesremoved from the end of - bytes. - - - rtrim('\x1234567890'::bytea, '\x9012'::bytea) - \x12345678 - - - - - - - substring - - substring ( bytes bytea FROM start integer FOR count integer ) - bytea - - - Extracts the substring of bytes starting at - the start'th byte if that is specified, - and stopping after count bytes if that is - specified. Provide at least one of start - and count. - - - substring('\x1234567890'::bytea from 3 for 2) - \x5678 - - - - - - - trim - - trim ( LEADING | TRAILING | BOTH - bytesremoved bytea FROM - bytes bytea ) - bytea - - - Removes the longest string containing only bytes appearing in - bytesremoved from the start, - end, or both ends (BOTH is the default) - of bytes. - - - trim('\x9012'::bytea from '\x1234567890'::bytea) - \x345678 - - - - - - trim ( LEADING | TRAILING | BOTH FROM - bytes bytea, - bytesremoved bytea ) - bytea - - - This is a non-standard syntax for trim(). - - - trim(both from '\x1234567890'::bytea, '\x9012'::bytea) - \x345678 - - - - -
- - - Additional binary string manipulation functions are available and - are listed in . Some - of them are used internally to implement the - SQL-standard string functions listed in . - - - - Other Binary String Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - bit_count - - - popcount - bit_count - - bit_count ( bytes bytea ) - bigint - - - Returns the number of bits set in the binary string (also known as - popcount). - - - bit_count('\x1234567890'::bytea) - 15 - - - - - - - crc32 - - crc32 ( bytea ) - bigint - - - Computes the CRC-32 value of the binary string. - - - crc32('abc'::bytea) - 891568578 - - - - - - - crc32c - - crc32c ( bytea ) - bigint - - - Computes the CRC-32C value of the binary string. - - - crc32c('abc'::bytea) - 910901175 - - - - - - - get_bit - - get_bit ( bytes bytea, - n bigint ) - integer - - - Extracts n'th bit - from binary string. - - - get_bit('\x1234567890'::bytea, 30) - 1 - - - - - - - get_byte - - get_byte ( bytes bytea, - n integer ) - integer - - - Extracts n'th byte - from binary string. - - - get_byte('\x1234567890'::bytea, 4) - 144 - - - - - - - length - - - binary string - length - - - length - of a binary string - binary strings, length - - length ( bytea ) - integer - - - Returns the number of bytes in the binary string. - - - length('\x1234567890'::bytea) - 5 - - - - - - length ( bytes bytea, - encoding name ) - integer - - - Returns the number of characters in the binary string, assuming - that it is text in the given encoding. - - - length('jose'::bytea, 'UTF8') - 4 - - - - - - - md5 - - md5 ( bytea ) - text - - - Computes the MD5 hash of - the binary string, with the result written in hexadecimal. - - - md5('Th\000omas'::bytea) - 8ab2d3c9689aaf18&zwsp;b4958c334c82d8b1 - - - - - - - reverse - - reverse ( bytea ) - bytea - - - Reverses the order of the bytes in the binary string. - - - reverse('\xabcd'::bytea) - \xcdab - - - - - - - set_bit - - set_bit ( bytes bytea, - n bigint, - newvalue integer ) - bytea - - - Sets n'th bit in - binary string to newvalue. - - - set_bit('\x1234567890'::bytea, 30, 0) - \x1234563890 - - - - - - - set_byte - - set_byte ( bytes bytea, - n integer, - newvalue integer ) - bytea - - - Sets n'th byte in - binary string to newvalue. - - - set_byte('\x1234567890'::bytea, 4, 64) - \x1234567840 - - - - - - - sha224 - - sha224 ( bytea ) - bytea - - - Computes the SHA-224 hash - of the binary string. - - - sha224('abc'::bytea) - \x23097d223405d8228642a477bda2&zwsp;55b32aadbce4bda0b3f7e36c9da7 - - - - - - - sha256 - - sha256 ( bytea ) - bytea - - - Computes the SHA-256 hash - of the binary string. - - - sha256('abc'::bytea) - \xba7816bf8f01cfea414140de5dae2223&zwsp;b00361a396177a9cb410ff61f20015ad - - - - - - - sha384 - - sha384 ( bytea ) - bytea - - - Computes the SHA-384 hash - of the binary string. - - - sha384('abc'::bytea) - \xcb00753f45a35e8bb5a03d699ac65007&zwsp;272c32ab0eded1631a8b605a43ff5bed&zwsp;8086072ba1e7cc2358baeca134c825a7 - - - - - - - sha512 - - sha512 ( bytea ) - bytea - - - Computes the SHA-512 hash - of the binary string. - - - sha512('abc'::bytea) - \xddaf35a193617abacc417349ae204131&zwsp;12e6fa4e89a97ea20a9eeee64b55d39a&zwsp;2192992a274fc1a836ba3c23a3feebbd&zwsp;454d4423643ce80e2a9ac94fa54ca49f - - - - - - - substr - - substr ( bytes bytea, start integer , count integer ) - bytea - - - Extracts the substring of bytes starting at - the start'th byte, - and extending for count bytes if that is - specified. (Same - as substring(bytes - from start - for count).) - - - substr('\x1234567890'::bytea, 3, 2) - \x5678 - - - - -
- - - Functions get_byte and set_byte - number the first byte of a binary string as byte 0. - Functions get_bit and set_bit - number bits from the right within each byte; for example bit 0 is the least - significant bit of the first byte, and bit 15 is the most significant bit - of the second byte. - - - - For historical reasons, the function md5 - returns a hex-encoded value of type text whereas the SHA-2 - functions return type bytea. Use the functions - encode - and decode to - convert between the two. For example write encode(sha256('abc'), - 'hex') to get a hex-encoded text representation, - or decode(md5('abc'), 'hex') to get - a bytea value. - - - - - character string - converting to binary string - - - binary string - converting to character string - - Functions for converting strings between different character sets - (encodings), and for representing arbitrary binary data in textual - form, are shown in - . For these - functions, an argument or result of type text is expressed - in the database's default encoding, while arguments or results of - type bytea are in an encoding named by another argument. - - - - Text/Binary String Conversion Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - convert - - convert ( bytes bytea, - src_encoding name, - dest_encoding name ) - bytea - - - Converts a binary string representing text in - encoding src_encoding - to a binary string in encoding dest_encoding - (see for - available conversions). - - - convert('text_in_utf8', 'UTF8', 'LATIN1') - \x746578745f696e5f75746638 - - - - - - - convert_from - - convert_from ( bytes bytea, - src_encoding name ) - text - - - Converts a binary string representing text in - encoding src_encoding - to text in the database encoding - (see for - available conversions). - - - convert_from('text_in_utf8', 'UTF8') - text_in_utf8 - - - - - - - convert_to - - convert_to ( string text, - dest_encoding name ) - bytea - - - Converts a text string (in the database encoding) to a - binary string encoded in encoding dest_encoding - (see for - available conversions). - - - convert_to('some_text', 'UTF8') - \x736f6d655f74657874 - - - - - - - encode - - encode ( bytes bytea, - format text ) - text - - - Encodes binary data into a textual representation; supported - format values are: - base64, - escape, - hex. - - - encode('123\000\001', 'base64') - MTIzAAE= - - - - - - - decode - - decode ( string text, - format text ) - bytea - - - Decodes binary data from a textual representation; supported - format values are the same as - for encode. - - - decode('MTIzAAE=', 'base64') - \x3132330001 - - - - -
- - - The encode and decode - functions support the following textual formats: - - - - base64 - - base64 format - - - - The base64 format is that - of RFC - 2045 Section 6.8. As per the RFC, encoded lines are - broken at 76 characters. However instead of the MIME CRLF - end-of-line marker, only a newline is used for end-of-line. - The decode function ignores carriage-return, - newline, space, and tab characters. Otherwise, an error is - raised when decode is supplied invalid - base64 data — including when trailing padding is incorrect. - - - - - - escape - - escape format - - - - The escape format converts zero bytes and - bytes with the high bit set into octal escape sequences - (\nnn), and it doubles - backslashes. Other byte values are represented literally. - The decode function will raise an error if a - backslash is not followed by either a second backslash or three - octal digits; it accepts other byte values unchanged. - - - - - - hex - - hex format - - - - The hex format represents each 4 bits of - data as one hexadecimal digit, 0 - through f, writing the higher-order digit of - each byte first. The encode function outputs - the a-f hex digits in lower - case. Because the smallest unit of data is 8 bits, there are - always an even number of characters returned - by encode. - The decode function - accepts the a-f characters in - either upper or lower case. An error is raised - when decode is given invalid hex data - — including when given an odd number of characters. - - - - - - - - In addition, it is possible to cast integral values to and from type - bytea. Casting an integer to bytea produces - 2, 4, or 8 bytes, depending on the width of the integer type. The result - is the two's complement representation of the integer, with the most - significant byte first. Some examples: - -1234::smallint::bytea \x04d2 -cast(1234 as bytea) \x000004d2 -cast(-1234 as bytea) \xfffffb2e -'\x8000'::bytea::smallint -32768 -'\x8000'::bytea::integer 32768 - - Casting a bytea to an integer will raise an error if the - length of the bytea exceeds the width of the integer type. - - - - See also the aggregate function string_agg in - and the large object functions - in . - -
- - - - Bit String Functions and Operators - - - bit strings - functions - - - - This section describes functions and operators for examining and - manipulating bit strings, that is values of the types - bit and bit varying. (While only - type bit is mentioned in these tables, values of - type bit varying can be used interchangeably.) - Bit strings support the usual comparison operators shown in - , as well as the - operators shown in . - - - - Bit String Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - bit || bit - bit - - - Concatenation - - - B'10001' || B'011' - 10001011 - - - - - - bit & bit - bit - - - Bitwise AND (inputs must be of equal length) - - - B'10001' & B'01101' - 00001 - - - - - - bit | bit - bit - - - Bitwise OR (inputs must be of equal length) - - - B'10001' | B'01101' - 11101 - - - - - - bit # bit - bit - - - Bitwise exclusive OR (inputs must be of equal length) - - - B'10001' # B'01101' - 11100 - - - - - - ~ bit - bit - - - Bitwise NOT - - - ~ B'10001' - 01110 - - - - - - bit << integer - bit - - - Bitwise shift left - (string length is preserved) - - - B'10001' << 3 - 01000 - - - - - - bit >> integer - bit - - - Bitwise shift right - (string length is preserved) - - - B'10001' >> 2 - 00100 - - - - -
- - - Some of the functions available for binary strings are also available - for bit strings, as shown in . - - - - Bit String Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - bit_count - - bit_count ( bit ) - bigint - - - Returns the number of bits set in the bit string (also known as - popcount). - - - bit_count(B'10111') - 4 - - - - - - - bit_length - - bit_length ( bit ) - integer - - - Returns number of bits in the bit string. - - - bit_length(B'10111') - 5 - - - - - - - length - - - bit string - length - - length ( bit ) - integer - - - Returns number of bits in the bit string. - - - length(B'10111') - 5 - - - - - - - octet_length - - octet_length ( bit ) - integer - - - Returns number of bytes in the bit string. - - - octet_length(B'1011111011') - 2 - - - - - - - overlay - - overlay ( bits bit PLACING newsubstring bit FROM start integer FOR count integer ) - bit - - - Replaces the substring of bits that starts at - the start'th bit and extends - for count bits - with newsubstring. - If count is omitted, it defaults to the length - of newsubstring. - - - overlay(B'01010101010101010' placing B'11111' from 2 for 3) - 0111110101010101010 - - - - - - - position - - position ( substring bit IN bits bit ) - integer - - - Returns first starting index of the specified substring - within bits, or zero if it's not present. - - - position(B'010' in B'000001101011') - 8 - - - - - - - substring - - substring ( bits bit FROM start integer FOR count integer ) - bit - - - Extracts the substring of bits starting at - the start'th bit if that is specified, - and stopping after count bits if that is - specified. Provide at least one of start - and count. - - - substring(B'110010111111' from 3 for 2) - 00 - - - - - - - get_bit - - get_bit ( bits bit, - n integer ) - integer - - - Extracts n'th bit - from bit string; the first (leftmost) bit is bit 0. - - - get_bit(B'101010101010101010', 6) - 1 - - - - - - - set_bit - - set_bit ( bits bit, - n integer, - newvalue integer ) - bit - - - Sets n'th bit in - bit string to newvalue; - the first (leftmost) bit is bit 0. - - - set_bit(B'101010101010101010', 6, 0) - 101010001010101010 - - - - -
- - - In addition, it is possible to cast integral values to and from type - bit. - Casting an integer to bit(n) copies the rightmost - n bits. Casting an integer to a bit string width wider - than the integer itself will sign-extend on the left. - Some examples: - -44::bit(10) 0000101100 -44::bit(3) 100 -cast(-44 as bit(12)) 111111010100 -'1110'::bit(4)::integer 14 - - Note that casting to just bit means casting to - bit(1), and so will deliver only the least significant - bit of the integer. - -
- - - - Pattern Matching - - - pattern matching - - - - There are three separate approaches to pattern matching provided - by PostgreSQL: the traditional - SQL LIKE operator, the - more recent SIMILAR TO operator (added in - SQL:1999), and POSIX-style regular - expressions. Aside from the basic does this string match - this pattern? operators, functions are available to extract - or replace matching substrings and to split a string at matching - locations. - - - - - If you have pattern matching needs that go beyond this, - consider writing a user-defined function in Perl or Tcl. - - - - - - While most regular-expression searches can be executed very quickly, - regular expressions can be contrived that take arbitrary amounts of - time and memory to process. Be wary of accepting regular-expression - search patterns from hostile sources. If you must do so, it is - advisable to impose a statement timeout. - - - - Searches using SIMILAR TO patterns have the same - security hazards, since SIMILAR TO provides many - of the same capabilities as POSIX-style regular - expressions. - - - - LIKE searches, being much simpler than the other - two options, are safer to use with possibly-hostile pattern sources. - - - - - SIMILAR TO and POSIX-style regular - expressions do not support nondeterministic collations. If required, use - LIKE or apply a different collation to the expression - to work around this limitation. - - - - <function>LIKE</function> - - - LIKE - - - -string LIKE pattern ESCAPE escape-character -string NOT LIKE pattern ESCAPE escape-character - - - - The LIKE expression returns true if the - string matches the supplied - pattern. (As - expected, the NOT LIKE expression returns - false if LIKE returns true, and vice versa. - An equivalent expression is - NOT (string LIKE - pattern).) - - - - If pattern does not contain percent - signs or underscores, then the pattern only represents the string - itself; in that case LIKE acts like the - equals operator. An underscore (_) in - pattern stands for (matches) any single - character; a percent sign (%) matches any sequence - of zero or more characters. - - - - Some examples: - -'abc' LIKE 'abc' true -'abc' LIKE 'a%' true -'abc' LIKE '_b_' true -'abc' LIKE 'c' false - - - - - LIKE pattern matching supports nondeterministic - collations (see ), such as - case-insensitive collations or collations that, say, ignore punctuation. - So with a case-insensitive collation, one could have: - -'AbC' LIKE 'abc' COLLATE case_insensitive true -'AbC' LIKE 'a%' COLLATE case_insensitive true - - With collations that ignore certain characters or in general that consider - strings of different lengths equal, the semantics can become a bit more - complicated. Consider these examples: - -'.foo.' LIKE 'foo' COLLATE ign_punct true -'.foo.' LIKE 'f_o' COLLATE ign_punct true -'.foo.' LIKE '_oo' COLLATE ign_punct false - - The way the matching works is that the pattern is partitioned into - sequences of wildcards and non-wildcard strings (wildcards being - _ and %). For example, the pattern - f_o is partitioned into f, _, o, the - pattern _oo is partitioned into _, - oo. The input string matches the pattern if it can be - partitioned in such a way that the wildcards match one character or any - number of characters respectively and the non-wildcard partitions are - equal under the applicable collation. So for example, '.foo.' - LIKE 'f_o' COLLATE ign_punct is true because one can partition - .foo. into .f, o, o., and then - '.f' = 'f' COLLATE ign_punct, 'o' - matches the _ wildcard, and 'o.' = 'o' COLLATE - ign_punct. But '.foo.' LIKE '_oo' COLLATE - ign_punct is false because .foo. cannot be - partitioned in a way that the first character is any character and the - rest of the string compares equal to oo. (Note that - the single-character wildcard always matches exactly one character, - independent of the collation. So in this example, the - _ would match ., but then the rest - of the input string won't match the rest of the pattern.) - - - - LIKE pattern matching always covers the entire - string. Therefore, if it's desired to match a sequence anywhere within - a string, the pattern must start and end with a percent sign. - - - - To match a literal underscore or percent sign without matching - other characters, the respective character in - pattern must be - preceded by the escape character. The default escape - character is the backslash but a different one can be selected by - using the ESCAPE clause. To match the escape - character itself, write two escape characters. - - - - - If you have turned off, - any backslashes you write in literal string constants will need to be - doubled. See for more information. - - - - - It's also possible to select no escape character by writing - ESCAPE ''. This effectively disables the - escape mechanism, which makes it impossible to turn off the - special meaning of underscore and percent signs in the pattern. - - - - According to the SQL standard, omitting ESCAPE - means there is no escape character (rather than defaulting to a - backslash), and a zero-length ESCAPE value is - disallowed. PostgreSQL's behavior in - this regard is therefore slightly nonstandard. - - - - The key word ILIKE can be used instead of - LIKE to make the match case-insensitive according to the - active locale. (But this does not support nondeterministic collations.) - This is not in the SQL standard but is a - PostgreSQL extension. - - - - The operator ~~ is equivalent to - LIKE, and ~~* corresponds to - ILIKE. There are also - !~~ and !~~* operators that - represent NOT LIKE and NOT - ILIKE, respectively. All of these operators are - PostgreSQL-specific. You may see these - operator names in EXPLAIN output and similar - places, since the parser actually translates LIKE - et al. to these operators. - - - - The phrases LIKE, ILIKE, - NOT LIKE, and NOT ILIKE are - generally treated as operators - in PostgreSQL syntax; for example they can - be used in expression - operator ANY - (subquery) constructs, although - an ESCAPE clause cannot be included there. In some - obscure cases it may be necessary to use the underlying operator names - instead. - - - - Also see the starts-with operator ^@ and the - corresponding starts_with() function, which are - useful in cases where simply matching the beginning of a string is - needed. - - - - - - <function>SIMILAR TO</function> Regular Expressions - - - regular expression - - - - - SIMILAR TO - - - substring - - - -string SIMILAR TO pattern ESCAPE escape-character -string NOT SIMILAR TO pattern ESCAPE escape-character - - - - The SIMILAR TO operator returns true or - false depending on whether its pattern matches the given string. - It is similar to LIKE, except that it - interprets the pattern using the SQL standard's definition of a - regular expression. SQL regular expressions are a curious cross - between LIKE notation and common (POSIX) regular - expression notation. - - - - Like LIKE, the SIMILAR TO - operator succeeds only if its pattern matches the entire string; - this is unlike common regular expression behavior where the pattern - can match any part of the string. - Also like - LIKE, SIMILAR TO uses - _ and % as wildcard characters denoting - any single character and any string, respectively (these are - comparable to . and .* in POSIX regular - expressions). - - - - In addition to these facilities borrowed from LIKE, - SIMILAR TO supports these pattern-matching - metacharacters borrowed from POSIX regular expressions: - - - - - | denotes alternation (either of two alternatives). - - - - - * denotes repetition of the previous item zero - or more times. - - - - - + denotes repetition of the previous item one - or more times. - - - - - ? denotes repetition of the previous item zero - or one time. - - - - - {m} denotes repetition - of the previous item exactly m times. - - - - - {m,} denotes repetition - of the previous item m or more times. - - - - - {m,n} - denotes repetition of the previous item at least m and - not more than n times. - - - - - Parentheses () can be used to group items into - a single logical item. - - - - - A bracket expression [...] specifies a character - class, just as in POSIX regular expressions. - - - - - Notice that the period (.) is not a metacharacter - for SIMILAR TO. - - - - As with LIKE, a backslash disables the special - meaning of any of these metacharacters. A different escape character - can be specified with ESCAPE, or the escape - capability can be disabled by writing ESCAPE ''. - - - - According to the SQL standard, omitting ESCAPE - means there is no escape character (rather than defaulting to a - backslash), and a zero-length ESCAPE value is - disallowed. PostgreSQL's behavior in - this regard is therefore slightly nonstandard. - - - - Another nonstandard extension is that following the escape character - with a letter or digit provides access to the escape sequences - defined for POSIX regular expressions; see - , - , and - below. - - - - Some examples: - -'abc' SIMILAR TO 'abc' true -'abc' SIMILAR TO 'a' false -'abc' SIMILAR TO '%(b|d)%' true -'abc' SIMILAR TO '(b|c)%' false -'-abc-' SIMILAR TO '%\mabc\M%' true -'xabcy' SIMILAR TO '%\mabc\M%' false - - - - - The substring function with three parameters - provides extraction of a substring that matches an SQL - regular expression pattern. The function can be written according - to standard SQL syntax: - -substring(string similar pattern escape escape-character) - - or using the now obsolete SQL:1999 syntax: - -substring(string from pattern for escape-character) - - or as a plain three-argument function: - -substring(string, pattern, escape-character) - - As with SIMILAR TO, the - specified pattern must match the entire data string, or else the - function fails and returns null. To indicate the part of the - pattern for which the matching data sub-string is of interest, - the pattern should contain - two occurrences of the escape character followed by a double quote - ("). - The text matching the portion of the pattern - between these separators is returned when the match is successful. - - - - The escape-double-quote separators actually - divide substring's pattern into three independent - regular expressions; for example, a vertical bar (|) - in any of the three sections affects only that section. Also, the first - and third of these regular expressions are defined to match the smallest - possible amount of text, not the largest, when there is any ambiguity - about how much of the data string matches which pattern. (In POSIX - parlance, the first and third regular expressions are forced to be - non-greedy.) - - - - As an extension to the SQL standard, PostgreSQL - allows there to be just one escape-double-quote separator, in which case - the third regular expression is taken as empty; or no separators, in which - case the first and third regular expressions are taken as empty. - - - - Some examples, with #" delimiting the return string: - -substring('foobar' similar '%#"o_b#"%' escape '#') oob -substring('foobar' similar '#"o_b#"%' escape '#') NULL - - - - - - <acronym>POSIX</acronym> Regular Expressions - - - regular expression - pattern matching - - - substring - - - regexp_count - - - regexp_instr - - - regexp_like - - - regexp_match - - - regexp_matches - - - regexp_replace - - - regexp_split_to_table - - - regexp_split_to_array - - - regexp_substr - - - - lists the available - operators for pattern matching using POSIX regular expressions. - - - - Regular Expression Match Operators - - - - - - Operator - - - Description - - - Example(s) - - - - - - - - text ~ text - boolean - - - String matches regular expression, case sensitively - - - 'thomas' ~ 't.*ma' - t - - - - - - text ~* text - boolean - - - String matches regular expression, case-insensitively - - - 'thomas' ~* 'T.*ma' - t - - - - - - text !~ text - boolean - - - String does not match regular expression, case sensitively - - - 'thomas' !~ 't.*max' - t - - - - - - text !~* text - boolean - - - String does not match regular expression, case-insensitively - - - 'thomas' !~* 'T.*ma' - f - - - - -
- - - POSIX regular expressions provide a more - powerful means for pattern matching than the LIKE and - SIMILAR TO operators. - Many Unix tools such as egrep, - sed, or awk use a pattern - matching language that is similar to the one described here. - - - - A regular expression is a character sequence that is an - abbreviated definition of a set of strings (a regular - set). A string is said to match a regular expression - if it is a member of the regular set described by the regular - expression. As with LIKE, pattern characters - match string characters exactly unless they are special characters - in the regular expression language — but regular expressions use - different special characters than LIKE does. - Unlike LIKE patterns, a - regular expression is allowed to match anywhere within a string, unless - the regular expression is explicitly anchored to the beginning or - end of the string. - - - - Some examples: - -'abcd' ~ 'bc' true -'abcd' ~ 'a.c' true — dot matches any character -'abcd' ~ 'a.*d' true — * repeats the preceding pattern item -'abcd' ~ '(b|x)' true — | means OR, parentheses group -'abcd' ~ '^a' true — ^ anchors to start of string -'abcd' ~ '^(b|c)' false — would match except for anchoring - - - - - The POSIX pattern language is described in much - greater detail below. - - - - The substring function with two parameters, - substring(string from - pattern), provides extraction of a - substring - that matches a POSIX regular expression pattern. It returns null if - there is no match, otherwise the first portion of the text that matched the - pattern. But if the pattern contains any parentheses, the portion - of the text that matched the first parenthesized subexpression (the - one whose left parenthesis comes first) is - returned. You can put parentheses around the whole expression - if you want to use parentheses within it without triggering this - exception. If you need parentheses in the pattern before the - subexpression you want to extract, see the non-capturing parentheses - described below. - - - - Some examples: - -substring('foobar' from 'o.b') oob -substring('foobar' from 'o(.)b') o - - - - - The regexp_count function counts the number of - places where a POSIX regular expression pattern matches a string. - It has the syntax - regexp_count(string, - pattern - , start - , flags - ). - pattern is searched for - in string, normally from the beginning of - the string, but if the start parameter is - provided then beginning from that character index. - The flags parameter is an optional text - string containing zero or more single-letter flags that change the - function's behavior. For example, including i in - flags specifies case-insensitive matching. - Supported flags are described in - . - - - - Some examples: - -regexp_count('ABCABCAXYaxy', 'A.') 3 -regexp_count('ABCABCAXYaxy', 'A.', 1, 'i') 4 - - - - - The regexp_instr function returns the starting or - ending position of the N'th match of a - POSIX regular expression pattern to a string, or zero if there is no - such match. It has the syntax - regexp_instr(string, - pattern - , start - , N - , endoption - , flags - , subexpr - ). - pattern is searched for - in string, normally from the beginning of - the string, but if the start parameter is - provided then beginning from that character index. - If N is specified - then the N'th match of the pattern - is located, otherwise the first match is located. - If the endoption parameter is omitted or - specified as zero, the function returns the position of the first - character of the match. Otherwise, endoption - must be one, and the function returns the position of the character - following the match. - The flags parameter is an optional text - string containing zero or more single-letter flags that change the - function's behavior. Supported flags are described - in . - For a pattern containing parenthesized - subexpressions, subexpr is an integer - indicating which subexpression is of interest: the result identifies - the position of the substring matching that subexpression. - Subexpressions are numbered in the order of their leading parentheses. - When subexpr is omitted or zero, the result - identifies the position of the whole match regardless of - parenthesized subexpressions. - - - - Some examples: - -regexp_instr('number of your street, town zip, FR', '[^,]+', 1, 2) - 23 -regexp_instr(string=>'ABCDEFGHI', pattern=>'(c..)(...)', start=>1, "N"=>1, endoption=>0, flags=>'i', subexpr=>2) - 6 - - - - - The regexp_like function checks whether a match - of a POSIX regular expression pattern occurs within a string, - returning boolean true or false. It has the syntax - regexp_like(string, - pattern - , flags ). - The flags parameter is an optional text - string containing zero or more single-letter flags that change the - function's behavior. Supported flags are described - in . - This function has the same results as the ~ - operator if no flags are specified. If only the i - flag is specified, it has the same results as - the ~* operator. - - - - Some examples: - -regexp_like('Hello World', 'world') false -regexp_like('Hello World', 'world', 'i') true - - - - - The regexp_match function returns a text array of - matching substring(s) within the first match of a POSIX - regular expression pattern to a string. It has the syntax - regexp_match(string, - pattern , flags ). - If there is no match, the result is NULL. - If a match is found, and the pattern contains no - parenthesized subexpressions, then the result is a single-element text - array containing the substring matching the whole pattern. - If a match is found, and the pattern contains - parenthesized subexpressions, then the result is a text array - whose n'th element is the substring matching - the n'th parenthesized subexpression of - the pattern (not counting non-capturing - parentheses; see below for details). - The flags parameter is an optional text string - containing zero or more single-letter flags that change the function's - behavior. Supported flags are described - in . - - - - Some examples: - -SELECT regexp_match('foobarbequebaz', 'bar.*que'); - regexp_match --------------- - {barbeque} -(1 row) - -SELECT regexp_match('foobarbequebaz', '(bar)(beque)'); - regexp_match --------------- - {bar,beque} -(1 row) - - - - - - In the common case where you just want the whole matching substring - or NULL for no match, the best solution is to - use regexp_substr(). - However, regexp_substr() only exists - in PostgreSQL version 15 and up. When - working in older versions, you can extract the first element - of regexp_match()'s result, for example: - -SELECT (regexp_match('foobarbequebaz', 'bar.*que'))[1]; - regexp_match --------------- - barbeque -(1 row) - - - - - - The regexp_matches function returns a set of text arrays - of matching substring(s) within matches of a POSIX regular - expression pattern to a string. It has the same syntax as - regexp_match. - This function returns no rows if there is no match, one row if there is - a match and the g flag is not given, or N - rows if there are N matches and the g flag - is given. Each returned row is a text array containing the whole - matched substring or the substrings matching parenthesized - subexpressions of the pattern, just as described above - for regexp_match. - regexp_matches accepts all the flags shown - in , plus - the g flag which commands it to return all matches, not - just the first one. - - - - Some examples: - -SELECT regexp_matches('foo', 'not there'); - regexp_matches ----------------- -(0 rows) - -SELECT regexp_matches('foobarbequebazilbarfbonk', '(b[^b]+)(b[^b]+)', 'g'); - regexp_matches ----------------- - {bar,beque} - {bazil,barf} -(2 rows) - - - - - - In most cases regexp_matches() should be used with - the g flag, since if you only want the first match, it's - easier and more efficient to use regexp_match(). - However, regexp_match() only exists - in PostgreSQL version 10 and up. When working in older - versions, a common trick is to place a regexp_matches() - call in a sub-select, for example: - -SELECT col1, (SELECT regexp_matches(col2, '(bar)(beque)')) FROM tab; - - This produces a text array if there's a match, or NULL if - not, the same as regexp_match() would do. Without the - sub-select, this query would produce no output at all for table rows - without a match, which is typically not the desired behavior. - - - - - The regexp_replace function provides substitution of - new text for substrings that match POSIX regular expression patterns. - It has the syntax - regexp_replace(string, - pattern, replacement - , flags ) - or - regexp_replace(string, - pattern, replacement, - start - , N - , flags ). - The source string is returned unchanged if - there is no match to the pattern. If there is a - match, the string is returned with the - replacement string substituted for the matching - substring. The replacement string can contain - \n, where n is 1 - through 9, to indicate that the source substring matching the - n'th parenthesized subexpression of the pattern should be - inserted, and it can contain \& to indicate that the - substring matching the entire pattern should be inserted. Write - \\ if you need to put a literal backslash in the replacement - text. - pattern is searched for - in string, normally from the beginning of - the string, but if the start parameter is - provided then beginning from that character index. - By default, only the first match of the pattern is replaced. - If N is specified and is greater than zero, - then the N'th match of the pattern - is replaced. - If the g flag is given, or - if N is specified and is zero, then all - matches at or after the start position are - replaced. (The g flag is ignored - when N is specified.) - The flags parameter is an optional text - string containing zero or more single-letter flags that change the - function's behavior. Supported flags (though - not g) are - described in . - - - - Some examples: - -regexp_replace('foobarbaz', 'b..', 'X') - fooXbaz -regexp_replace('foobarbaz', 'b..', 'X', 'g') - fooXX -regexp_replace('foobarbaz', 'b(..)', 'X\1Y', 'g') - fooXarYXazY -regexp_replace('A PostgreSQL function', 'a|e|i|o|u', 'X', 1, 0, 'i') - X PXstgrXSQL fXnctXXn -regexp_replace(string=>'A PostgreSQL function', pattern=>'a|e|i|o|u', replacement=>'X', start=>1, "N"=>3, flags=>'i') - A PostgrXSQL function - - - - - The regexp_split_to_table function splits a string using a POSIX - regular expression pattern as a delimiter. It has the syntax - regexp_split_to_table(string, pattern - , flags ). - If there is no match to the pattern, the function returns the - string. If there is at least one match, for each match it returns - the text from the end of the last match (or the beginning of the string) - to the beginning of the match. When there are no more matches, it - returns the text from the end of the last match to the end of the string. - The flags parameter is an optional text string containing - zero or more single-letter flags that change the function's behavior. - regexp_split_to_table supports the flags described in - . - - - - The regexp_split_to_array function behaves the same as - regexp_split_to_table, except that regexp_split_to_array - returns its result as an array of text. It has the syntax - regexp_split_to_array(string, pattern - , flags ). - The parameters are the same as for regexp_split_to_table. - - - - Some examples: - -SELECT foo FROM regexp_split_to_table('the quick brown fox jumps over the lazy dog', '\s+') AS foo; - foo -------- - the - quick - brown - fox - jumps - over - the - lazy - dog -(9 rows) - -SELECT regexp_split_to_array('the quick brown fox jumps over the lazy dog', '\s+'); - regexp_split_to_array ------------------------------------------------ - {the,quick,brown,fox,jumps,over,the,lazy,dog} -(1 row) - -SELECT foo FROM regexp_split_to_table('the quick brown fox', '\s*') AS foo; - foo ------ - t - h - e - q - u - i - c - k - b - r - o - w - n - f - o - x -(16 rows) - - - - - As the last example demonstrates, the regexp split functions ignore - zero-length matches that occur at the start or end of the string - or immediately after a previous match. This is contrary to the strict - definition of regexp matching that is implemented by - the other regexp functions, but is usually the most convenient behavior - in practice. Other software systems such as Perl use similar definitions. - - - - The regexp_substr function returns the substring - that matches a POSIX regular expression pattern, - or NULL if there is no match. It has the syntax - regexp_substr(string, - pattern - , start - , N - , flags - , subexpr - ). - pattern is searched for - in string, normally from the beginning of - the string, but if the start parameter is - provided then beginning from that character index. - If N is specified - then the N'th match of the pattern - is returned, otherwise the first match is returned. - The flags parameter is an optional text - string containing zero or more single-letter flags that change the - function's behavior. Supported flags are described - in . - For a pattern containing parenthesized - subexpressions, subexpr is an integer - indicating which subexpression is of interest: the result is the - substring matching that subexpression. - Subexpressions are numbered in the order of their leading parentheses. - When subexpr is omitted or zero, the result - is the whole match regardless of parenthesized subexpressions. - - - - Some examples: - -regexp_substr('number of your street, town zip, FR', '[^,]+', 1, 2) - town zip -regexp_substr('ABCDEFGHI', '(c..)(...)', 1, 1, 'i', 2) - FGH - - - - - - - Regular Expression Details - - - PostgreSQL's regular expressions are implemented - using a software package written by Henry Spencer. Much of - the description of regular expressions below is copied verbatim from his - manual. - - - - Regular expressions (REs), as defined in - POSIX 1003.2, come in two forms: - extended REs or EREs - (roughly those of egrep), and - basic REs or BREs - (roughly those of ed). - PostgreSQL supports both forms, and - also implements some extensions - that are not in the POSIX standard, but have become widely used - due to their availability in programming languages such as Perl and Tcl. - REs using these non-POSIX extensions are called - advanced REs or AREs - in this documentation. AREs are almost an exact superset of EREs, - but BREs have several notational incompatibilities (as well as being - much more limited). - We first describe the ARE and ERE forms, noting features that apply - only to AREs, and then describe how BREs differ. - - - - - PostgreSQL always initially presumes that a regular - expression follows the ARE rules. However, the more limited ERE or - BRE rules can be chosen by prepending an embedded option - to the RE pattern, as described in . - This can be useful for compatibility with applications that expect - exactly the POSIX 1003.2 rules. - - - - - A regular expression is defined as one or more - branches, separated by - |. It matches anything that matches one of the - branches. - - - - A branch is zero or more quantified atoms or - constraints, concatenated. - It matches a match for the first, followed by a match for the second, etc.; - an empty branch matches the empty string. - - - - A quantified atom is an atom possibly followed - by a single quantifier. - Without a quantifier, it matches a match for the atom. - With a quantifier, it can match some number of matches of the atom. - An atom can be any of the possibilities - shown in . - The possible quantifiers and their meanings are shown in - . - - - - A constraint matches an empty string, but matches only when - specific conditions are met. A constraint can be used where an atom - could be used, except it cannot be followed by a quantifier. - The simple constraints are shown in - ; - some more constraints are described later. - - - - - Regular Expression Atoms - - - - - Atom - Description - - - - - - (re) - (where re is any regular expression) - matches a match for - re, with the match noted for possible reporting - - - - (?:re) - as above, but the match is not noted for reporting - (a non-capturing set of parentheses) - (AREs only) - - - - . - matches any single character - - - - [chars] - a bracket expression, - matching any one of the chars (see - for more detail) - - - - \k - (where k is a non-alphanumeric character) - matches that character taken as an ordinary character, - e.g., \\ matches a backslash character - - - - \c - where c is alphanumeric - (possibly followed by other characters) - is an escape, see - (AREs only; in EREs and BREs, this matches c) - - - - { - when followed by a character other than a digit, - matches the left-brace character {; - when followed by a digit, it is the beginning of a - bound (see below) - - - - x - where x is a single character with no other - significance, matches that character - - - -
- - - An RE cannot end with a backslash (\). - - - - - If you have turned off, - any backslashes you write in literal string constants will need to be - doubled. See for more information. - - - - - Regular Expression Quantifiers - - - - - Quantifier - Matches - - - - - - * - a sequence of 0 or more matches of the atom - - - - + - a sequence of 1 or more matches of the atom - - - - ? - a sequence of 0 or 1 matches of the atom - - - - {m} - a sequence of exactly m matches of the atom - - - - {m,} - a sequence of m or more matches of the atom - - - - - {m,n} - a sequence of m through n - (inclusive) matches of the atom; m cannot exceed - n - - - - *? - non-greedy version of * - - - - +? - non-greedy version of + - - - - ?? - non-greedy version of ? - - - - {m}? - non-greedy version of {m} - - - - {m,}? - non-greedy version of {m,} - - - - - {m,n}? - non-greedy version of {m,n} - - - -
- - - The forms using {...} - are known as bounds. - The numbers m and n within a bound are - unsigned decimal integers with permissible values from 0 to 255 inclusive. - - - - Non-greedy quantifiers (available in AREs only) match the - same possibilities as their corresponding normal (greedy) - counterparts, but prefer the smallest number rather than the largest - number of matches. - See for more detail. - - - - - A quantifier cannot immediately follow another quantifier, e.g., - ** is invalid. - A quantifier cannot - begin an expression or subexpression or follow - ^ or |. - - - - - Regular Expression Constraints - - - - - Constraint - Description - - - - - - ^ - matches at the beginning of the string - - - - $ - matches at the end of the string - - - - (?=re) - positive lookahead matches at any point - where a substring matching re begins - (AREs only) - - - - (?!re) - negative lookahead matches at any point - where no substring matching re begins - (AREs only) - - - - (?<=re) - positive lookbehind matches at any point - where a substring matching re ends - (AREs only) - - - - (?<!re) - negative lookbehind matches at any point - where no substring matching re ends - (AREs only) - - - -
- - - Lookahead and lookbehind constraints cannot contain back - references (see ), - and all parentheses within them are considered non-capturing. - -
- - - Bracket Expressions - - - A bracket expression is a list of - characters enclosed in []. It normally matches - any single character from the list (but see below). If the list - begins with ^, it matches any single character - not from the rest of the list. - If two characters - in the list are separated by -, this is - shorthand for the full range of characters between those two - (inclusive) in the collating sequence, - e.g., [0-9] in ASCII matches - any decimal digit. It is illegal for two ranges to share an - endpoint, e.g., a-c-e. Ranges are very - collating-sequence-dependent, so portable programs should avoid - relying on them. - - - - To include a literal ] in the list, make it the - first character (after ^, if that is used). To - include a literal -, make it the first or last - character, or the second endpoint of a range. To use a literal - - as the first endpoint of a range, enclose it - in [. and .] to make it a - collating element (see below). With the exception of these characters, - some combinations using [ - (see next paragraphs), and escapes (AREs only), all other special - characters lose their special significance within a bracket expression. - In particular, \ is not special when following - ERE or BRE rules, though it is special (as introducing an escape) - in AREs. - - - - Within a bracket expression, a collating element (a character, a - multiple-character sequence that collates as if it were a single - character, or a collating-sequence name for either) enclosed in - [. and .] stands for the - sequence of characters of that collating element. The sequence is - treated as a single element of the bracket expression's list. This - allows a bracket - expression containing a multiple-character collating element to - match more than one character, e.g., if the collating sequence - includes a ch collating element, then the RE - [[.ch.]]*c matches the first five characters of - chchcc. - - - - - PostgreSQL currently does not support multi-character collating - elements. This information describes possible future behavior. - - - - - Within a bracket expression, a collating element enclosed in - [= and =] is an equivalence - class, standing for the sequences of characters of all collating - elements equivalent to that one, including itself. (If there are - no other equivalent collating elements, the treatment is as if the - enclosing delimiters were [. and - .].) For example, if o and - ^ are the members of an equivalence class, then - [[=o=]], [[=^=]], and - [o^] are all synonymous. An equivalence class - cannot be an endpoint of a range. - - - - Within a bracket expression, the name of a character class - enclosed in [: and :] stands - for the list of all characters belonging to that class. A character - class cannot be used as an endpoint of a range. - The POSIX standard defines these character class - names: - alnum (letters and numeric digits), - alpha (letters), - blank (space and tab), - cntrl (control characters), - digit (numeric digits), - graph (printable characters except space), - lower (lower-case letters), - print (printable characters including space), - punct (punctuation), - space (any white space), - upper (upper-case letters), - and xdigit (hexadecimal digits). - The behavior of these standard character classes is generally - consistent across platforms for characters in the 7-bit ASCII set. - Whether a given non-ASCII character is considered to belong to one - of these classes depends on the collation - that is used for the regular-expression function or operator - (see ), or by default on the - database's LC_CTYPE locale setting (see - ). The classification of non-ASCII - characters can vary across platforms even in similarly-named - locales. (But the C locale never considers any - non-ASCII characters to belong to any of these classes.) - In addition to these standard character - classes, PostgreSQL defines - the word character class, which is the same as - alnum plus the underscore (_) - character, and - the ascii character class, which contains exactly - the 7-bit ASCII set. - - - - There are two special cases of bracket expressions: the bracket - expressions [[:<:]] and - [[:>:]] are constraints, - matching empty strings at the beginning - and end of a word respectively. A word is defined as a sequence - of word characters that is neither preceded nor followed by word - characters. A word character is any character belonging to the - word character class, that is, any letter, digit, - or underscore. This is an extension, compatible with but not - specified by POSIX 1003.2, and should be used with - caution in software intended to be portable to other systems. - The constraint escapes described below are usually preferable; they - are no more standard, but are easier to type. - - - - - Regular Expression Escapes - - - Escapes are special sequences beginning with \ - followed by an alphanumeric character. Escapes come in several varieties: - character entry, class shorthands, constraint escapes, and back references. - A \ followed by an alphanumeric character but not constituting - a valid escape is illegal in AREs. - In EREs, there are no escapes: outside a bracket expression, - a \ followed by an alphanumeric character merely stands for - that character as an ordinary character, and inside a bracket expression, - \ is an ordinary character. - (The latter is the one actual incompatibility between EREs and AREs.) - - - - Character-entry escapes exist to make it easier to specify - non-printing and other inconvenient characters in REs. They are - shown in . - - - - Class-shorthand escapes provide shorthands for certain - commonly-used character classes. They are - shown in . - - - - A constraint escape is a constraint, - matching the empty string if specific conditions are met, - written as an escape. They are - shown in . - - - - A back reference (\n) matches the - same string matched by the previous parenthesized subexpression specified - by the number n - (see ). For example, - ([bc])\1 matches bb or cc - but not bc or cb. - The subexpression must entirely precede the back reference in the RE. - Subexpressions are numbered in the order of their leading parentheses. - Non-capturing parentheses do not define subexpressions. - The back reference considers only the string characters matched by the - referenced subexpression, not any constraints contained in it. For - example, (^\d)\1 will match 22. - - - - Regular Expression Character-Entry Escapes - - - - - Escape - Description - - - - - - \a - alert (bell) character, as in C - - - - \b - backspace, as in C - - - - \B - synonym for backslash (\) to help reduce the need for backslash - doubling - - - - \cX - (where X is any character) the character whose - low-order 5 bits are the same as those of - X, and whose other bits are all zero - - - - \e - the character whose collating-sequence name - is ESC, - or failing that, the character with octal value 033 - - - - \f - form feed, as in C - - - - \n - newline, as in C - - - - \r - carriage return, as in C - - - - \t - horizontal tab, as in C - - - - \uwxyz - (where wxyz is exactly four hexadecimal digits) - the character whose hexadecimal value is - 0xwxyz - - - - - \Ustuvwxyz - (where stuvwxyz is exactly eight hexadecimal - digits) - the character whose hexadecimal value is - 0xstuvwxyz - - - - - \v - vertical tab, as in C - - - - \xhhh - (where hhh is any sequence of hexadecimal - digits) - the character whose hexadecimal value is - 0xhhh - (a single character no matter how many hexadecimal digits are used) - - - - - \0 - the character whose value is 0 (the null byte) - - - - \xy - (where xy is exactly two octal digits, - and is not a back reference) - the character whose octal value is - 0xy - - - - \xyz - (where xyz is exactly three octal digits, - and is not a back reference) - the character whose octal value is - 0xyz - - - -
- - - Hexadecimal digits are 0-9, - a-f, and A-F. - Octal digits are 0-7. - - - - Numeric character-entry escapes specifying values outside the ASCII range - (0–127) have meanings dependent on the database encoding. When the - encoding is UTF-8, escape values are equivalent to Unicode code points, - for example \u1234 means the character U+1234. - For other multibyte encodings, character-entry escapes usually just - specify the concatenation of the byte values for the character. If the - escape value does not correspond to any legal character in the database - encoding, no error will be raised, but it will never match any data. - - - - The character-entry escapes are always taken as ordinary characters. - For example, \135 is ] in ASCII, but - \135 does not terminate a bracket expression. - - - - Regular Expression Class-Shorthand Escapes - - - - - Escape - Description - - - - - - \d - matches any digit, like - [[:digit:]] - - - - \s - matches any whitespace character, like - [[:space:]] - - - - \w - matches any word character, like - [[:word:]] - - - - \D - matches any non-digit, like - [^[:digit:]] - - - - \S - matches any non-whitespace character, like - [^[:space:]] - - - - \W - matches any non-word character, like - [^[:word:]] - - - -
- - - The class-shorthand escapes also work within bracket expressions, - although the definitions shown above are not quite syntactically - valid in that context. - For example, [a-c\d] is equivalent to - [a-c[:digit:]]. - - - - Regular Expression Constraint Escapes - - - - - Escape - Description - - - - - - \A - matches only at the beginning of the string - (see for how this differs from - ^) - - - - \m - matches only at the beginning of a word - - - - \M - matches only at the end of a word - - - - \y - matches only at the beginning or end of a word - - - - \Y - matches only at a point that is not the beginning or end of a - word - - - - \Z - matches only at the end of the string - (see for how this differs from - $) - - - -
- - - A word is defined as in the specification of - [[:<:]] and [[:>:]] above. - Constraint escapes are illegal within bracket expressions. - - - - Regular Expression Back References - - - - - Escape - Description - - - - - - \m - (where m is a nonzero digit) - a back reference to the m'th subexpression - - - - \mnn - (where m is a nonzero digit, and - nn is some more digits, and the decimal value - mnn is not greater than the number of closing capturing - parentheses seen so far) - a back reference to the mnn'th subexpression - - - -
- - - - There is an inherent ambiguity between octal character-entry - escapes and back references, which is resolved by the following heuristics, - as hinted at above. - A leading zero always indicates an octal escape. - A single non-zero digit, not followed by another digit, - is always taken as a back reference. - A multi-digit sequence not starting with a zero is taken as a back - reference if it comes after a suitable subexpression - (i.e., the number is in the legal range for a back reference), - and otherwise is taken as octal. - - -
- - - Regular Expression Metasyntax - - - In addition to the main syntax described above, there are some special - forms and miscellaneous syntactic facilities available. - - - - An RE can begin with one of two special director prefixes. - If an RE begins with ***:, - the rest of the RE is taken as an ARE. (This normally has no effect in - PostgreSQL, since REs are assumed to be AREs; - but it does have an effect if ERE or BRE mode had been specified by - the flags parameter to a regex function.) - If an RE begins with ***=, - the rest of the RE is taken to be a literal string, - with all characters considered ordinary characters. - - - - An ARE can begin with embedded options: - a sequence (?xyz) - (where xyz is one or more alphabetic characters) - specifies options affecting the rest of the RE. - These options override any previously determined options — - in particular, they can override the case-sensitivity behavior implied by - a regex operator, or the flags parameter to a regex - function. - The available option letters are - shown in . - Note that these same option letters are used in the flags - parameters of regex functions. - - - - ARE Embedded-Option Letters - - - - - Option - Description - - - - - - b - rest of RE is a BRE - - - - c - case-sensitive matching (overrides operator type) - - - - e - rest of RE is an ERE - - - - i - case-insensitive matching (see - ) (overrides operator type) - - - - m - historical synonym for n - - - - n - newline-sensitive matching (see - ) - - - - p - partial newline-sensitive matching (see - ) - - - - q - rest of RE is a literal (quoted) string, all ordinary - characters - - - - s - non-newline-sensitive matching (default) - - - - t - tight syntax (default; see below) - - - - w - inverse partial newline-sensitive (weird) matching - (see ) - - - - x - expanded syntax (see below) - - - -
- - - Embedded options take effect at the ) terminating the sequence. - They can appear only at the start of an ARE (after the - ***: director if any). - - - - In addition to the usual (tight) RE syntax, in which all - characters are significant, there is an expanded syntax, - available by specifying the embedded x option. - In the expanded syntax, - white-space characters in the RE are ignored, as are - all characters between a # - and the following newline (or the end of the RE). This - permits paragraphing and commenting a complex RE. - There are three exceptions to that basic rule: - - - - - a white-space character or # preceded by \ is - retained - - - - - white space or # within a bracket expression is retained - - - - - white space and comments cannot appear within multi-character symbols, - such as (?: - - - - - For this purpose, white-space characters are blank, tab, newline, and - any character that belongs to the space character class. - - - - Finally, in an ARE, outside bracket expressions, the sequence - (?#ttt) - (where ttt is any text not containing a )) - is a comment, completely ignored. - Again, this is not allowed between the characters of - multi-character symbols, like (?:. - Such comments are more a historical artifact than a useful facility, - and their use is deprecated; use the expanded syntax instead. - - - - None of these metasyntax extensions is available if - an initial ***= director - has specified that the user's input be treated as a literal string - rather than as an RE. - -
- - - Regular Expression Matching Rules - - - In the event that an RE could match more than one substring of a given - string, the RE matches the one starting earliest in the string. - If the RE could match more than one substring starting at that point, - either the longest possible match or the shortest possible match will - be taken, depending on whether the RE is greedy or - non-greedy. - - - - Whether an RE is greedy or not is determined by the following rules: - - - - Most atoms, and all constraints, have no greediness attribute (because - they cannot match variable amounts of text anyway). - - - - - Adding parentheses around an RE does not change its greediness. - - - - - A quantified atom with a fixed-repetition quantifier - ({m} - or - {m}?) - has the same greediness (possibly none) as the atom itself. - - - - - A quantified atom with other normal quantifiers (including - {m,n} - with m equal to n) - is greedy (prefers longest match). - - - - - A quantified atom with a non-greedy quantifier (including - {m,n}? - with m equal to n) - is non-greedy (prefers shortest match). - - - - - A branch — that is, an RE that has no top-level - | operator — has the same greediness as the first - quantified atom in it that has a greediness attribute. - - - - - An RE consisting of two or more branches connected by the - | operator is always greedy. - - - - - - - The above rules associate greediness attributes not only with individual - quantified atoms, but with branches and entire REs that contain quantified - atoms. What that means is that the matching is done in such a way that - the branch, or whole RE, matches the longest or shortest possible - substring as a whole. Once the length of the entire match - is determined, the part of it that matches any particular subexpression - is determined on the basis of the greediness attribute of that - subexpression, with subexpressions starting earlier in the RE taking - priority over ones starting later. - - - - An example of what this means: - -SELECT SUBSTRING('XY1234Z', 'Y*([0-9]{1,3})'); -Result: 123 -SELECT SUBSTRING('XY1234Z', 'Y*?([0-9]{1,3})'); -Result: 1 - - In the first case, the RE as a whole is greedy because Y* - is greedy. It can match beginning at the Y, and it matches - the longest possible string starting there, i.e., Y123. - The output is the parenthesized part of that, or 123. - In the second case, the RE as a whole is non-greedy because Y*? - is non-greedy. It can match beginning at the Y, and it matches - the shortest possible string starting there, i.e., Y1. - The subexpression [0-9]{1,3} is greedy but it cannot change - the decision as to the overall match length; so it is forced to match - just 1. - - - - In short, when an RE contains both greedy and non-greedy subexpressions, - the total match length is either as long as possible or as short as - possible, according to the attribute assigned to the whole RE. The - attributes assigned to the subexpressions only affect how much of that - match they are allowed to eat relative to each other. - - - - The quantifiers {1,1} and {1,1}? - can be used to force greediness or non-greediness, respectively, - on a subexpression or a whole RE. - This is useful when you need the whole RE to have a greediness attribute - different from what's deduced from its elements. As an example, - suppose that we are trying to separate a string containing some digits - into the digits and the parts before and after them. We might try to - do that like this: - -SELECT regexp_match('abc01234xyz', '(.*)(\d+)(.*)'); -Result: {abc0123,4,xyz} - - That didn't work: the first .* is greedy so - it eats as much as it can, leaving the \d+ to - match at the last possible place, the last digit. We might try to fix - that by making it non-greedy: - -SELECT regexp_match('abc01234xyz', '(.*?)(\d+)(.*)'); -Result: {abc,0,""} - - That didn't work either, because now the RE as a whole is non-greedy - and so it ends the overall match as soon as possible. We can get what - we want by forcing the RE as a whole to be greedy: - -SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}'); -Result: {abc,01234,xyz} - - Controlling the RE's overall greediness separately from its components' - greediness allows great flexibility in handling variable-length patterns. - - - - When deciding what is a longer or shorter match, - match lengths are measured in characters, not collating elements. - An empty string is considered longer than no match at all. - For example: - bb* - matches the three middle characters of abbbc; - (week|wee)(night|knights) - matches all ten characters of weeknights; - when (.*).* - is matched against abc the parenthesized subexpression - matches all three characters; and when - (a*)* is matched against bc - both the whole RE and the parenthesized - subexpression match an empty string. - - - - If case-independent matching is specified, - the effect is much as if all case distinctions had vanished from the - alphabet. - When an alphabetic that exists in multiple cases appears as an - ordinary character outside a bracket expression, it is effectively - transformed into a bracket expression containing both cases, - e.g., x becomes [xX]. - When it appears inside a bracket expression, all case counterparts - of it are added to the bracket expression, e.g., - [x] becomes [xX] - and [^x] becomes [^xX]. - - - - If newline-sensitive matching is specified, . - and bracket expressions using ^ - will never match the newline character - (so that matches will not cross lines unless the RE - explicitly includes a newline) - and ^ and $ - will match the empty string after and before a newline - respectively, in addition to matching at beginning and end of string - respectively. - But the ARE escapes \A and \Z - continue to match beginning or end of string only. - Also, the character class shorthands \D - and \W will match a newline regardless of this mode. - (Before PostgreSQL 14, they did not match - newlines when in newline-sensitive mode. - Write [^[:digit:]] - or [^[:word:]] to get the old behavior.) - - - - If partial newline-sensitive matching is specified, - this affects . and bracket expressions - as with newline-sensitive matching, but not ^ - and $. - - - - If inverse partial newline-sensitive matching is specified, - this affects ^ and $ - as with newline-sensitive matching, but not . - and bracket expressions. - This isn't very useful but is provided for symmetry. - - - - - Limits and Compatibility - - - No particular limit is imposed on the length of REs in this - implementation. However, - programs intended to be highly portable should not employ REs longer - than 256 bytes, - as a POSIX-compliant implementation can refuse to accept such REs. - - - - The only feature of AREs that is actually incompatible with - POSIX EREs is that \ does not lose its special - significance inside bracket expressions. - All other ARE features use syntax which is illegal or has - undefined or unspecified effects in POSIX EREs; - the *** syntax of directors likewise is outside the POSIX - syntax for both BREs and EREs. - - - - Many of the ARE extensions are borrowed from Perl, but some have - been changed to clean them up, and a few Perl extensions are not present. - Incompatibilities of note include \b, \B, - the lack of special treatment for a trailing newline, - the addition of complemented bracket expressions to the things - affected by newline-sensitive matching, - the restrictions on parentheses and back references in lookahead/lookbehind - constraints, and the longest/shortest-match (rather than first-match) - matching semantics. - - - - - Basic Regular Expressions - - - BREs differ from EREs in several respects. - In BREs, |, +, and ? - are ordinary characters and there is no equivalent - for their functionality. - The delimiters for bounds are - \{ and \}, - with { and } - by themselves ordinary characters. - The parentheses for nested subexpressions are - \( and \), - with ( and ) by themselves ordinary characters. - ^ is an ordinary character except at the beginning of the - RE or the beginning of a parenthesized subexpression, - $ is an ordinary character except at the end of the - RE or the end of a parenthesized subexpression, - and * is an ordinary character if it appears at the beginning - of the RE or the beginning of a parenthesized subexpression - (after a possible leading ^). - Finally, single-digit back references are available, and - \< and \> - are synonyms for - [[:<:]] and [[:>:]] - respectively; no other escapes are available in BREs. - - - - - - - Differences from SQL Standard and XQuery - - - LIKE_REGEX - - - - OCCURRENCES_REGEX - - - - POSITION_REGEX - - - - SUBSTRING_REGEX - - - - TRANSLATE_REGEX - - - - XQuery regular expressions - - - - Since SQL:2008, the SQL standard includes regular expression operators - and functions that performs pattern - matching according to the XQuery regular expression - standard: - - LIKE_REGEX - OCCURRENCES_REGEX - POSITION_REGEX - SUBSTRING_REGEX - TRANSLATE_REGEX - - PostgreSQL does not currently implement these - operators and functions. You can get approximately equivalent - functionality in each case as shown in . (Various optional clauses on - both sides have been omitted in this table.) - - - - Regular Expression Functions Equivalencies - - - - - SQL standard - PostgreSQL - - - - - - string LIKE_REGEX pattern - regexp_like(string, pattern) or string ~ pattern - - - - OCCURRENCES_REGEX(pattern IN string) - regexp_count(string, pattern) - - - - POSITION_REGEX(pattern IN string) - regexp_instr(string, pattern) - - - - SUBSTRING_REGEX(pattern IN string) - regexp_substr(string, pattern) - - - - TRANSLATE_REGEX(pattern IN string WITH replacement) - regexp_replace(string, pattern, replacement) - - - -
- - - Regular expression functions similar to those provided by PostgreSQL are - also available in a number of other SQL implementations, whereas the - SQL-standard functions are not as widely implemented. Some of the - details of the regular expression syntax will likely differ in each - implementation. - - - - The SQL-standard operators and functions use XQuery regular expressions, - which are quite close to the ARE syntax described above. - Notable differences between the existing POSIX-based - regular-expression feature and XQuery regular expressions include: - - - - - XQuery character class subtraction is not supported. An example of - this feature is using the following to match only English - consonants: [a-z-[aeiou]]. - - - - - XQuery character class shorthands \c, - \C, \i, - and \I are not supported. - - - - - XQuery character class elements - using \p{UnicodeProperty} or the - inverse \P{UnicodeProperty} are not supported. - - - - - POSIX interprets character classes such as \w - (see ) - according to the prevailing locale (which you can control by - attaching a COLLATE clause to the operator or - function). XQuery specifies these classes by reference to Unicode - character properties, so equivalent behavior is obtained only with - a locale that follows the Unicode rules. - - - - - The SQL standard (not XQuery itself) attempts to cater for more - variants of newline than POSIX does. The - newline-sensitive matching options described above consider only - ASCII NL (\n) to be a newline, but SQL would have - us treat CR (\r), CRLF (\r\n) - (a Windows-style newline), and some Unicode-only characters like - LINE SEPARATOR (U+2028) as newlines as well. - Notably, . and \s should - count \r\n as one character not two according to - SQL. - - - - - Of the character-entry escapes described in - , - XQuery supports only \n, \r, - and \t. - - - - - XQuery does not support - the [:name:] syntax - for character classes within bracket expressions. - - - - - XQuery does not have lookahead or lookbehind constraints, - nor any of the constraint escapes described in - . - - - - - The metasyntax forms described in - do not exist in XQuery. - - - - - The regular expression flag letters defined by XQuery are - related to but not the same as the option letters for POSIX - (). While the - i and q options behave the - same, others do not: - - - - XQuery's s (allow dot to match newline) - and m (allow ^ - and $ to match at newlines) flags provide - access to the same behaviors as - POSIX's n, p - and w flags, but they - do not match the behavior of - POSIX's s and m flags. - Note in particular that dot-matches-newline is the default - behavior in POSIX but not XQuery. - - - - - XQuery's x (ignore whitespace in pattern) flag - is noticeably different from POSIX's expanded-mode flag. - POSIX's x flag also - allows # to begin a comment in the pattern, - and POSIX will not ignore a whitespace character after a - backslash. - - - - - - - - -
-
-
- - - - Data Type Formatting Functions - - - formatting - - - - The PostgreSQL formatting functions - provide a powerful set of tools for converting various data types - (date/time, integer, floating point, numeric) to formatted strings - and for converting from formatted strings to specific data types. - lists them. - These functions all follow a common calling convention: the first - argument is the value to be formatted and the second argument is a - template that defines the output or input format. - - - - Formatting Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - to_char - - to_char ( timestamp, text ) - text - - - to_char ( timestamp with time zone, text ) - text - - - Converts time stamp to string according to the given format. - - - to_char(timestamp '2002-04-20 17:31:12.66', 'HH12:MI:SS') - 05:31:12 - - - - - - to_char ( interval, text ) - text - - - Converts interval to string according to the given format. - - - to_char(interval '15h 2m 12s', 'HH24:MI:SS') - 15:02:12 - - - - - - to_char ( numeric_type, text ) - text - - - Converts number to string according to the given format; available - for integer, bigint, numeric, - real, double precision. - - - to_char(125, '999') - 125 - - - to_char(125.8::real, '999D9') - 125.8 - - - to_char(-125.8, '999D99S') - 125.80- - - - - - - - to_date - - to_date ( text, text ) - date - - - Converts string to date according to the given format. - - - to_date('05 Dec 2000', 'DD Mon YYYY') - 2000-12-05 - - - - - - - to_number - - to_number ( text, text ) - numeric - - - Converts string to numeric according to the given format. - - - to_number('12,454.8-', '99G999D9S') - -12454.8 - - - - - - - to_timestamp - - to_timestamp ( text, text ) - timestamp with time zone - - - Converts string to time stamp according to the given format. - (See also to_timestamp(double precision) in - .) - - - to_timestamp('05 Dec 2000', 'DD Mon YYYY') - 2000-12-05 00:00:00-05 - - - - -
- - - - to_timestamp and to_date - exist to handle input formats that cannot be converted by - simple casting. For most standard date/time formats, simply casting the - source string to the required data type works, and is much easier. - Similarly, to_number is unnecessary for standard numeric - representations. - - - - - In a to_char output template string, there are certain - patterns that are recognized and replaced with appropriately-formatted - data based on the given value. Any text that is not a template pattern is - simply copied verbatim. Similarly, in an input template string (for the - other functions), template patterns identify the values to be supplied by - the input data string. If there are characters in the template string - that are not template patterns, the corresponding characters in the input - data string are simply skipped over (whether or not they are equal to the - template string characters). - - - - shows the - template patterns available for formatting date and time values. - - - - Template Patterns for Date/Time Formatting - - - - Pattern - Description - - - - - HH - hour of day (01–12) - - - HH12 - hour of day (01–12) - - - HH24 - hour of day (00–23) - - - MI - minute (00–59) - - - SS - second (00–59) - - - MS - millisecond (000–999) - - - US - microsecond (000000–999999) - - - FF1 - tenth of second (0–9) - - - FF2 - hundredth of second (00–99) - - - FF3 - millisecond (000–999) - - - FF4 - tenth of a millisecond (0000–9999) - - - FF5 - hundredth of a millisecond (00000–99999) - - - FF6 - microsecond (000000–999999) - - - SSSS, SSSSS - seconds past midnight (0–86399) - - - AM, am, - PM or pm - meridiem indicator (without periods) - - - A.M., a.m., - P.M. or p.m. - meridiem indicator (with periods) - - - Y,YYY - year (4 or more digits) with comma - - - YYYY - year (4 or more digits) - - - YYY - last 3 digits of year - - - YY - last 2 digits of year - - - Y - last digit of year - - - IYYY - ISO 8601 week-numbering year (4 or more digits) - - - IYY - last 3 digits of ISO 8601 week-numbering year - - - IY - last 2 digits of ISO 8601 week-numbering year - - - I - last digit of ISO 8601 week-numbering year - - - BC, bc, - AD or ad - era indicator (without periods) - - - B.C., b.c., - A.D. or a.d. - era indicator (with periods) - - - MONTH - full upper case month name (blank-padded to 9 chars) - - - Month - full capitalized month name (blank-padded to 9 chars) - - - month - full lower case month name (blank-padded to 9 chars) - - - MON - abbreviated upper case month name (3 chars in English, localized lengths vary) - - - Mon - abbreviated capitalized month name (3 chars in English, localized lengths vary) - - - mon - abbreviated lower case month name (3 chars in English, localized lengths vary) - - - MM - month number (01–12) - - - DAY - full upper case day name (blank-padded to 9 chars) - - - Day - full capitalized day name (blank-padded to 9 chars) - - - day - full lower case day name (blank-padded to 9 chars) - - - DY - abbreviated upper case day name (3 chars in English, localized lengths vary) - - - Dy - abbreviated capitalized day name (3 chars in English, localized lengths vary) - - - dy - abbreviated lower case day name (3 chars in English, localized lengths vary) - - - DDD - day of year (001–366) - - - IDDD - day of ISO 8601 week-numbering year (001–371; day 1 of the year is Monday of the first ISO week) - - - DD - day of month (01–31) - - - D - day of the week, Sunday (1) to Saturday (7) - - - ID - ISO 8601 day of the week, Monday (1) to Sunday (7) - - - W - week of month (1–5) (the first week starts on the first day of the month) - - - WW - week number of year (1–53) (the first week starts on the first day of the year) - - - IW - week number of ISO 8601 week-numbering year (01–53; the first Thursday of the year is in week 1) - - - CC - century (2 digits) (the twenty-first century starts on 2001-01-01) - - - J - Julian Date (integer days since November 24, 4714 BC at local - midnight; see ) - - - Q - quarter - - - RM - month in upper case Roman numerals (I–XII; I=January) - - - rm - month in lower case Roman numerals (i–xii; i=January) - - - TZ - upper case time-zone abbreviation - - - tz - lower case time-zone abbreviation - - - TZH - time-zone hours - - - TZM - time-zone minutes - - - OF - time-zone offset from UTC (HH - or HH:MM) - - - -
- - - Modifiers can be applied to any template pattern to alter its - behavior. For example, FMMonth - is the Month pattern with the - FM modifier. - shows the - modifier patterns for date/time formatting. - - - - Template Pattern Modifiers for Date/Time Formatting - - - - Modifier - Description - Example - - - - - FM prefix - fill mode (suppress leading zeroes and padding blanks) - FMMonth - - - TH suffix - upper case ordinal number suffix - DDTH, e.g., 12TH - - - th suffix - lower case ordinal number suffix - DDth, e.g., 12th - - - FX prefix - fixed format global option (see usage notes) - FX Month DD Day - - - TM prefix - translation mode (use localized day and month names based on - ) - TMMonth - - - SP suffix - spell mode (not implemented) - DDSP - - - -
- - - Usage notes for date/time formatting: - - - - - FM suppresses leading zeroes and trailing blanks - that would otherwise be added to make the output of a pattern be - fixed-width. In PostgreSQL, - FM modifies only the next specification, while in - Oracle FM affects all subsequent - specifications, and repeated FM modifiers - toggle fill mode on and off. - - - - - - TM suppresses trailing blanks whether or - not FM is specified. - - - - - - to_timestamp and to_date - ignore letter case in the input; so for - example MON, Mon, - and mon all accept the same strings. When using - the TM modifier, case-folding is done according to - the rules of the function's input collation (see - ). - - - - - - to_timestamp and to_date - skip multiple blank spaces at the beginning of the input string and - around date and time values unless the FX option is used. For example, - to_timestamp(' 2000    JUN', 'YYYY MON') and - to_timestamp('2000 - JUN', 'YYYY-MON') work, but - to_timestamp('2000    JUN', 'FXYYYY MON') returns an error - because to_timestamp expects only a single space. - FX must be specified as the first item in - the template. - - - - - - A separator (a space or non-letter/non-digit character) in the template string of - to_timestamp and to_date - matches any single separator in the input string or is skipped, - unless the FX option is used. - For example, to_timestamp('2000JUN', 'YYYY///MON') and - to_timestamp('2000/JUN', 'YYYY MON') work, but - to_timestamp('2000//JUN', 'YYYY/MON') - returns an error because the number of separators in the input string - exceeds the number of separators in the template. - - - If FX is specified, a separator in the template string - matches exactly one character in the input string. But note that the - input string character is not required to be the same as the separator from the template string. - For example, to_timestamp('2000/JUN', 'FXYYYY MON') - works, but to_timestamp('2000/JUN', 'FXYYYY  MON') - returns an error because the second space in the template string consumes - the letter J from the input string. - - - - - - A TZH template pattern can match a signed number. - Without the FX option, minus signs may be ambiguous, - and could be interpreted as a separator. - This ambiguity is resolved as follows: If the number of separators before - TZH in the template string is less than the number of - separators before the minus sign in the input string, the minus sign - is interpreted as part of TZH. - Otherwise, the minus sign is considered to be a separator between values. - For example, to_timestamp('2000 -10', 'YYYY TZH') matches - -10 to TZH, but - to_timestamp('2000 -10', 'YYYY  TZH') - matches 10 to TZH. - - - - - - Ordinary text is allowed in to_char - templates and will be output literally. You can put a substring - in double quotes to force it to be interpreted as literal text - even if it contains template patterns. For example, in - '"Hello Year "YYYY', the YYYY - will be replaced by the year data, but the single Y in Year - will not be. - In to_date, to_number, - and to_timestamp, literal text and double-quoted - strings result in skipping the number of characters contained in the - string; for example "XX" skips two input characters - (whether or not they are XX). - - - - Prior to PostgreSQL 12, it was possible to - skip arbitrary text in the input string using non-letter or non-digit - characters. For example, - to_timestamp('2000y6m1d', 'yyyy-MM-DD') used to - work. Now you can only use letter characters for this purpose. For example, - to_timestamp('2000y6m1d', 'yyyytMMtDDt') and - to_timestamp('2000y6m1d', 'yyyy"y"MM"m"DD"d"') - skip y, m, and - d. - - - - - - - If you want to have a double quote in the output you must - precede it with a backslash, for example '\"YYYY - Month\"'. - Backslashes are not otherwise special outside of double-quoted - strings. Within a double-quoted string, a backslash causes the - next character to be taken literally, whatever it is (but this - has no special effect unless the next character is a double quote - or another backslash). - - - - - - In to_timestamp and to_date, - if the year format specification is less than four digits, e.g., - YYY, and the supplied year is less than four digits, - the year will be adjusted to be nearest to the year 2020, e.g., - 95 becomes 1995. - - - - - - In to_timestamp and to_date, - negative years are treated as signifying BC. If you write both a - negative year and an explicit BC field, you get AD - again. An input of year zero is treated as 1 BC. - - - - - - In to_timestamp and to_date, - the YYYY conversion has a restriction when - processing years with more than 4 digits. You must - use some non-digit character or template after YYYY, - otherwise the year is always interpreted as 4 digits. For example - (with the year 20000): - to_date('200001130', 'YYYYMMDD') will be - interpreted as a 4-digit year; instead use a non-digit - separator after the year, like - to_date('20000-1130', 'YYYY-MMDD') or - to_date('20000Nov30', 'YYYYMonDD'). - - - - - - In to_timestamp and to_date, - the CC (century) field is accepted but ignored - if there is a YYY, YYYY or - Y,YYY field. If CC is used with - YY or Y then the result is - computed as that year in the specified century. If the century is - specified but the year is not, the first year of the century - is assumed. - - - - - - In to_timestamp and to_date, - weekday names or numbers (DAY, D, - and related field types) are accepted but are ignored for purposes of - computing the result. The same is true for quarter - (Q) fields. - - - - - - In to_timestamp and to_date, - an ISO 8601 week-numbering date (as distinct from a Gregorian date) - can be specified in one of two ways: - - - - Year, week number, and weekday: for - example to_date('2006-42-4', 'IYYY-IW-ID') - returns the date 2006-10-19. - If you omit the weekday it is assumed to be 1 (Monday). - - - - - Year and day of year: for example to_date('2006-291', - 'IYYY-IDDD') also returns 2006-10-19. - - - - - - Attempting to enter a date using a mixture of ISO 8601 week-numbering - fields and Gregorian date fields is nonsensical, and will cause an - error. In the context of an ISO 8601 week-numbering year, the - concept of a month or day of month has no - meaning. In the context of a Gregorian year, the ISO week has no - meaning. - - - - While to_date will reject a mixture of - Gregorian and ISO week-numbering date - fields, to_char will not, since output format - specifications like YYYY-MM-DD (IYYY-IDDD) can be - useful. But avoid writing something like IYYY-MM-DD; - that would yield surprising results near the start of the year. - (See for more - information.) - - - - - - - In to_timestamp, millisecond - (MS) or microsecond (US) - fields are used as the - seconds digits after the decimal point. For example - to_timestamp('12.3', 'SS.MS') is not 3 milliseconds, - but 300, because the conversion treats it as 12 + 0.3 seconds. - So, for the format SS.MS, the input values - 12.3, 12.30, - and 12.300 specify the - same number of milliseconds. To get three milliseconds, one must write - 12.003, which the conversion treats as - 12 + 0.003 = 12.003 seconds. - - - - Here is a more - complex example: - to_timestamp('15:12:02.020.001230', 'HH24:MI:SS.MS.US') - is 15 hours, 12 minutes, and 2 seconds + 20 milliseconds + - 1230 microseconds = 2.021230 seconds. - - - - - - to_char(..., 'ID')'s day of the week numbering - matches the extract(isodow from ...) function, but - to_char(..., 'D')'s does not match - extract(dow from ...)'s day numbering. - - - - - - to_char(interval) formats HH and - HH12 as shown on a 12-hour clock, for example zero hours - and 36 hours both output as 12, while HH24 - outputs the full hour value, which can exceed 23 in - an interval value. - - - - - - - - shows the - template patterns available for formatting numeric values. - - - - Template Patterns for Numeric Formatting - - - - Pattern - Description - - - - - 9 - digit position (can be dropped if insignificant) - - - 0 - digit position (will not be dropped, even if insignificant) - - - . (period) - decimal point - - - , (comma) - group (thousands) separator - - - PR - negative value in angle brackets - - - S - sign anchored to number (uses locale) - - - L - currency symbol (uses locale) - - - D - decimal point (uses locale) - - - G - group separator (uses locale) - - - MI - minus sign in specified position (if number < 0) - - - PL - plus sign in specified position (if number > 0) - - - SG - plus/minus sign in specified position - - - RN or rn - Roman numeral (values between 1 and 3999) - - - TH or th - ordinal number suffix - - - V - shift specified number of digits (see notes) - - - EEEE - exponent for scientific notation - - - -
- - - Usage notes for numeric formatting: - - - - - 0 specifies a digit position that will always be printed, - even if it contains a leading/trailing zero. 9 also - specifies a digit position, but if it is a leading zero then it will - be replaced by a space, while if it is a trailing zero and fill mode - is specified then it will be deleted. (For to_number(), - these two pattern characters are equivalent.) - - - - - - If the format provides fewer fractional digits than the number being - formatted, to_char() will round the number to - the specified number of fractional digits. - - - - - - The pattern characters S, L, D, - and G represent the sign, currency symbol, decimal point, - and thousands separator characters defined by the current locale - (see - and ). The pattern characters period - and comma represent those exact characters, with the meanings of - decimal point and thousands separator, regardless of locale. - - - - - - If no explicit provision is made for a sign - in to_char()'s pattern, one column will be reserved for - the sign, and it will be anchored to (appear just left of) the - number. If S appears just left of some 9's, - it will likewise be anchored to the number. - - - - - - A sign formatted using SG, PL, or - MI is not anchored to - the number; for example, - to_char(-12, 'MI9999') produces '-  12' - but to_char(-12, 'S9999') produces '  -12'. - (The Oracle implementation does not allow the use of - MI before 9, but rather - requires that 9 precede - MI.) - - - - - - TH does not convert values less than zero - and does not convert fractional numbers. - - - - - - PL, SG, and - TH are PostgreSQL - extensions. - - - - - - In to_number, if non-data template patterns such - as L or TH are used, the - corresponding number of input characters are skipped, whether or not - they match the template pattern, unless they are data characters - (that is, digits, sign, decimal point, or comma). For - example, TH would skip two non-data characters. - - - - - - V with to_char - multiplies the input values by - 10^n, where - n is the number of digits following - V. V with - to_number divides in a similar manner. - The V can be thought of as marking the position - of an implicit decimal point in the input or output string. - to_char and to_number - do not support the use of - V combined with a decimal point - (e.g., 99.9V99 is not allowed). - - - - - - EEEE (scientific notation) cannot be used in - combination with any of the other formatting patterns or - modifiers other than digit and decimal point patterns, and must be at the end of the format string - (e.g., 9.99EEEE is a valid pattern). - - - - - - In to_number(), the RN - pattern converts Roman numerals (in standard form) to numbers. - Input is case-insensitive, so RN - and rn are equivalent. RN - cannot be used in combination with any other formatting patterns or - modifiers except FM, which is applicable only - in to_char() and is ignored - in to_number(). - - - - - - - Certain modifiers can be applied to any template pattern to alter its - behavior. For example, FM99.99 - is the 99.99 pattern with the - FM modifier. - shows the - modifier patterns for numeric formatting. - - - - Template Pattern Modifiers for Numeric Formatting - - - - Modifier - Description - Example - - - - - FM prefix - fill mode (suppress trailing zeroes and padding blanks) - FM99.99 - - - TH suffix - upper case ordinal number suffix - 999TH - - - th suffix - lower case ordinal number suffix - 999th - - - -
- - - shows some - examples of the use of the to_char function. - - - - <function>to_char</function> Examples - - - - Expression - Result - - - - - to_char(current_timestamp, 'Day, DD  HH12:MI:SS') - 'Tuesday  , 06  05:39:18' - - - to_char(current_timestamp, 'FMDay, FMDD  HH12:MI:SS') - 'Tuesday, 6  05:39:18' - - - to_char(current_timestamp AT TIME ZONE - 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') - '2022-12-06T05:39:18Z', - ISO 8601 extended format - - - to_char(-0.1, '99.99') - '  -.10' - - - to_char(-0.1, 'FM9.99') - '-.1' - - - to_char(-0.1, 'FM90.99') - '-0.1' - - - to_char(0.1, '0.9') - ' 0.1' - - - to_char(12, '9990999.9') - '    0012.0' - - - to_char(12, 'FM9990999.9') - '0012.' - - - to_char(485, '999') - ' 485' - - - to_char(-485, '999') - '-485' - - - to_char(485, '9 9 9') - ' 4 8 5' - - - to_char(1485, '9,999') - ' 1,485' - - - to_char(1485, '9G999') - ' 1 485' - - - to_char(148.5, '999.999') - ' 148.500' - - - to_char(148.5, 'FM999.999') - '148.5' - - - to_char(148.5, 'FM999.990') - '148.500' - - - to_char(148.5, '999D999') - ' 148,500' - - - to_char(3148.5, '9G999D999') - ' 3 148,500' - - - to_char(-485, '999S') - '485-' - - - to_char(-485, '999MI') - '485-' - - - to_char(485, '999MI') - '485 ' - - - to_char(485, 'FM999MI') - '485' - - - to_char(485, 'PL999') - '+485' - - - to_char(485, 'SG999') - '+485' - - - to_char(-485, 'SG999') - '-485' - - - to_char(-485, '9SG99') - '4-85' - - - to_char(-485, '999PR') - '<485>' - - - to_char(485, 'L999') - 'DM 485' - - - to_char(485, 'RN') - '        CDLXXXV' - - - to_char(485, 'FMRN') - 'CDLXXXV' - - - to_char(5.2, 'FMRN') - 'V' - - - to_char(482, '999th') - ' 482nd' - - - to_char(485, '"Good number:"999') - 'Good number: 485' - - - to_char(485.8, '"Pre:"999" Post:" .999') - 'Pre: 485 Post: .800' - - - to_char(12, '99V999') - ' 12000' - - - to_char(12.4, '99V999') - ' 12400' - - - to_char(12.45, '99V9') - ' 125' - - - to_char(0.0004859, '9.99EEEE') - ' 4.86e-04' - - - -
- -
- - - - Date/Time Functions and Operators - - - shows the available - functions for date/time value processing, with details appearing in - the following subsections. illustrates the behaviors of - the basic arithmetic operators (+, - *, etc.). For formatting functions, refer to - . You should be familiar with - the background information on date/time data types from . - - - - In addition, the usual comparison operators shown in - are available for the - date/time types. Dates and timestamps (with or without time zone) are - all comparable, while times (with or without time zone) and intervals - can only be compared to other values of the same data type. When - comparing a timestamp without time zone to a timestamp with time zone, - the former value is assumed to be given in the time zone specified by - the configuration parameter, and is - rotated to UTC for comparison to the latter value (which is already - in UTC internally). Similarly, a date value is assumed to represent - midnight in the TimeZone zone when comparing it - to a timestamp. - - - - All the functions and operators described below that take time or timestamp - inputs actually come in two variants: one that takes time with time zone or timestamp - with time zone, and one that takes time without time zone or timestamp without time zone. - For brevity, these variants are not shown separately. Also, the - + and * operators come in commutative pairs (for - example both date + integer - and integer + date); we show - only one of each such pair. - - - - Date/Time Operators - - - - - - Operator - - - Description - - - Example(s) - - - - - - - - date + integer - date - - - Add a number of days to a date - - - date '2001-09-28' + 7 - 2001-10-05 - - - - - - date + interval - timestamp - - - Add an interval to a date - - - date '2001-09-28' + interval '1 hour' - 2001-09-28 01:00:00 - - - - - - date + time - timestamp - - - Add a time-of-day to a date - - - date '2001-09-28' + time '03:00' - 2001-09-28 03:00:00 - - - - - - interval + interval - interval - - - Add intervals - - - interval '1 day' + interval '1 hour' - 1 day 01:00:00 - - - - - - timestamp + interval - timestamp - - - Add an interval to a timestamp - - - timestamp '2001-09-28 01:00' + interval '23 hours' - 2001-09-29 00:00:00 - - - - - - time + interval - time - - - Add an interval to a time - - - time '01:00' + interval '3 hours' - 04:00:00 - - - - - - - interval - interval - - - Negate an interval - - - - interval '23 hours' - -23:00:00 - - - - - - date - date - integer - - - Subtract dates, producing the number of days elapsed - - - date '2001-10-01' - date '2001-09-28' - 3 - - - - - - date - integer - date - - - Subtract a number of days from a date - - - date '2001-10-01' - 7 - 2001-09-24 - - - - - - date - interval - timestamp - - - Subtract an interval from a date - - - date '2001-09-28' - interval '1 hour' - 2001-09-27 23:00:00 - - - - - - time - time - interval - - - Subtract times - - - time '05:00' - time '03:00' - 02:00:00 - - - - - - time - interval - time - - - Subtract an interval from a time - - - time '05:00' - interval '2 hours' - 03:00:00 - - - - - - timestamp - interval - timestamp - - - Subtract an interval from a timestamp - - - timestamp '2001-09-28 23:00' - interval '23 hours' - 2001-09-28 00:00:00 - - - - - - interval - interval - interval - - - Subtract intervals - - - interval '1 day' - interval '1 hour' - 1 day -01:00:00 - - - - - - timestamp - timestamp - interval - - - Subtract timestamps (converting 24-hour intervals into days, - similarly to justify_hours()) - - - timestamp '2001-09-29 03:00' - timestamp '2001-07-27 12:00' - 63 days 15:00:00 - - - - - - interval * double precision - interval - - - Multiply an interval by a scalar - - - interval '1 second' * 900 - 00:15:00 - - - interval '1 day' * 21 - 21 days - - - interval '1 hour' * 3.5 - 03:30:00 - - - - - - interval / double precision - interval - - - Divide an interval by a scalar - - - interval '1 hour' / 1.5 - 00:40:00 - - - - -
- - - Date/Time Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - age - - age ( timestamp, timestamp ) - interval - - - Subtract arguments, producing a symbolic result that - uses years and months, rather than just days - - - age(timestamp '2001-04-10', timestamp '1957-06-13') - 43 years 9 mons 27 days - - - - - - age ( timestamp ) - interval - - - Subtract argument from current_date (at midnight) - - - age(timestamp '1957-06-13') - 62 years 6 mons 10 days - - - - - - - clock_timestamp - - clock_timestamp ( ) - timestamp with time zone - - - Current date and time (changes during statement execution); - see - - - clock_timestamp() - 2019-12-23 14:39:53.662522-05 - - - - - - - current_date - - current_date - date - - - Current date; see - - - current_date - 2019-12-23 - - - - - - - current_time - - current_time - time with time zone - - - Current time of day; see - - - current_time - 14:39:53.662522-05 - - - - - - current_time ( integer ) - time with time zone - - - Current time of day, with limited precision; - see - - - current_time(2) - 14:39:53.66-05 - - - - - - - current_timestamp - - current_timestamp - timestamp with time zone - - - Current date and time (start of current transaction); - see - - - current_timestamp - 2019-12-23 14:39:53.662522-05 - - - - - - current_timestamp ( integer ) - timestamp with time zone - - - Current date and time (start of current transaction), with limited precision; - see - - - current_timestamp(0) - 2019-12-23 14:39:53-05 - - - - - - - date_add - - date_add ( timestamp with time zone, interval , text ) - timestamp with time zone - - - Add an interval to a timestamp with time - zone, computing times of day and daylight-savings adjustments - according to the time zone named by the third argument, or the - current setting if that is omitted. - The form with two arguments is equivalent to the timestamp with - time zone + interval operator. - - - date_add('2021-10-31 00:00:00+02'::timestamptz, '1 day'::interval, 'Europe/Warsaw') - 2021-10-31 23:00:00+00 - - - - - - date_bin ( interval, timestamp, timestamp ) - timestamp - - - Bin input into specified interval aligned with specified origin; see - - - date_bin('15 minutes', timestamp '2001-02-16 20:38:40', timestamp '2001-02-16 20:05:00') - 2001-02-16 20:35:00 - - - - - - - date_part - - date_part ( text, timestamp ) - double precision - - - Get timestamp subfield (equivalent to extract); - see - - - date_part('hour', timestamp '2001-02-16 20:38:40') - 20 - - - - - - date_part ( text, interval ) - double precision - - - Get interval subfield (equivalent to extract); - see - - - date_part('month', interval '2 years 3 months') - 3 - - - - - - - date_subtract - - date_subtract ( timestamp with time zone, interval , text ) - timestamp with time zone - - - Subtract an interval from a timestamp with time - zone, computing times of day and daylight-savings adjustments - according to the time zone named by the third argument, or the - current setting if that is omitted. - The form with two arguments is equivalent to the timestamp with - time zone - interval operator. - - - date_subtract('2021-11-01 00:00:00+01'::timestamptz, '1 day'::interval, 'Europe/Warsaw') - 2021-10-30 22:00:00+00 - - - - - - - date_trunc - - date_trunc ( text, timestamp ) - timestamp - - - Truncate to specified precision; see - - - date_trunc('hour', timestamp '2001-02-16 20:38:40') - 2001-02-16 20:00:00 - - - - - - date_trunc ( text, timestamp with time zone, text ) - timestamp with time zone - - - Truncate to specified precision in the specified time zone; see - - - - date_trunc('day', timestamptz '2001-02-16 20:38:40+00', 'Australia/Sydney') - 2001-02-16 13:00:00+00 - - - - - - date_trunc ( text, interval ) - interval - - - Truncate to specified precision; see - - - - date_trunc('hour', interval '2 days 3 hours 40 minutes') - 2 days 03:00:00 - - - - - - - extract - - extract ( field from timestamp ) - numeric - - - Get timestamp subfield; see - - - extract(hour from timestamp '2001-02-16 20:38:40') - 20 - - - - - - extract ( field from interval ) - numeric - - - Get interval subfield; see - - - extract(month from interval '2 years 3 months') - 3 - - - - - - - isfinite - - isfinite ( date ) - boolean - - - Test for finite date (not +/-infinity) - - - isfinite(date '2001-02-16') - true - - - - - - isfinite ( timestamp ) - boolean - - - Test for finite timestamp (not +/-infinity) - - - isfinite(timestamp 'infinity') - false - - - - - - isfinite ( interval ) - boolean - - - Test for finite interval (not +/-infinity) - - - isfinite(interval '4 hours') - true - - - - - - - justify_days - - justify_days ( interval ) - interval - - - Adjust interval, converting 30-day time periods to months - - - justify_days(interval '1 year 65 days') - 1 year 2 mons 5 days - - - - - - - justify_hours - - justify_hours ( interval ) - interval - - - Adjust interval, converting 24-hour time periods to days - - - justify_hours(interval '50 hours 10 minutes') - 2 days 02:10:00 - - - - - - - justify_interval - - justify_interval ( interval ) - interval - - - Adjust interval using justify_days - and justify_hours, with additional sign - adjustments - - - justify_interval(interval '1 mon -1 hour') - 29 days 23:00:00 - - - - - - - localtime - - localtime - time - - - Current time of day; - see - - - localtime - 14:39:53.662522 - - - - - - localtime ( integer ) - time - - - Current time of day, with limited precision; - see - - - localtime(0) - 14:39:53 - - - - - - - localtimestamp - - localtimestamp - timestamp - - - Current date and time (start of current transaction); - see - - - localtimestamp - 2019-12-23 14:39:53.662522 - - - - - - localtimestamp ( integer ) - timestamp - - - Current date and time (start of current - transaction), with limited precision; - see - - - localtimestamp(2) - 2019-12-23 14:39:53.66 - - - - - - - make_date - - make_date ( year int, - month int, - day int ) - date - - - Create date from year, month and day fields - (negative years signify BC) - - - make_date(2013, 7, 15) - 2013-07-15 - - - - - - make_interval - - make_interval ( years int - , months int - , weeks int - , days int - , hours int - , mins int - , secs double precision - ) - interval - - - Create interval from years, months, weeks, days, hours, minutes and - seconds fields, each of which can default to zero - - - make_interval(days => 10) - 10 days - - - - - - - make_time - - make_time ( hour int, - min int, - sec double precision ) - time - - - Create time from hour, minute and seconds fields - - - make_time(8, 15, 23.5) - 08:15:23.5 - - - - - - - make_timestamp - - make_timestamp ( year int, - month int, - day int, - hour int, - min int, - sec double precision ) - timestamp - - - Create timestamp from year, month, day, hour, minute and seconds fields - (negative years signify BC) - - - make_timestamp(2013, 7, 15, 8, 15, 23.5) - 2013-07-15 08:15:23.5 - - - - - - - make_timestamptz - - make_timestamptz ( year int, - month int, - day int, - hour int, - min int, - sec double precision - , timezone text ) - timestamp with time zone - - - Create timestamp with time zone from year, month, day, hour, minute - and seconds fields (negative years signify BC). - If timezone is not - specified, the current time zone is used; the examples assume the - session time zone is Europe/London - - - make_timestamptz(2013, 7, 15, 8, 15, 23.5) - 2013-07-15 08:15:23.5+01 - - - make_timestamptz(2013, 7, 15, 8, 15, 23.5, 'America/New_York') - 2013-07-15 13:15:23.5+01 - - - - - - - now - - now ( ) - timestamp with time zone - - - Current date and time (start of current transaction); - see - - - now() - 2019-12-23 14:39:53.662522-05 - - - - - - - statement_timestamp - - statement_timestamp ( ) - timestamp with time zone - - - Current date and time (start of current statement); - see - - - statement_timestamp() - 2019-12-23 14:39:53.662522-05 - - - - - - - timeofday - - timeofday ( ) - text - - - Current date and time - (like clock_timestamp, but as a text string); - see - - - timeofday() - Mon Dec 23 14:39:53.662522 2019 EST - - - - - - - transaction_timestamp - - transaction_timestamp ( ) - timestamp with time zone - - - Current date and time (start of current transaction); - see - - - transaction_timestamp() - 2019-12-23 14:39:53.662522-05 - - - - - - - to_timestamp - - to_timestamp ( double precision ) - timestamp with time zone - - - Convert Unix epoch (seconds since 1970-01-01 00:00:00+00) to - timestamp with time zone - - - to_timestamp(1284352323) - 2010-09-13 04:32:03+00 - - - - -
- - - - OVERLAPS - - In addition to these functions, the SQL OVERLAPS operator is - supported: - -(start1, end1) OVERLAPS (start2, end2) -(start1, length1) OVERLAPS (start2, length2) - - This expression yields true when two time periods (defined by their - endpoints) overlap, false when they do not overlap. The endpoints - can be specified as pairs of dates, times, or time stamps; or as - a date, time, or time stamp followed by an interval. When a pair - of values is provided, either the start or the end can be written - first; OVERLAPS automatically takes the earlier value - of the pair as the start. Each time period is considered to - represent the half-open interval start <= - time < end, unless - start and end are equal in which case it - represents that single time instant. This means for instance that two - time periods with only an endpoint in common do not overlap. - - - -SELECT (DATE '2001-02-16', DATE '2001-12-21') OVERLAPS - (DATE '2001-10-30', DATE '2002-10-30'); -Result: true -SELECT (DATE '2001-02-16', INTERVAL '100 days') OVERLAPS - (DATE '2001-10-30', DATE '2002-10-30'); -Result: false -SELECT (DATE '2001-10-29', DATE '2001-10-30') OVERLAPS - (DATE '2001-10-30', DATE '2001-10-31'); -Result: false -SELECT (DATE '2001-10-30', DATE '2001-10-30') OVERLAPS - (DATE '2001-10-30', DATE '2001-10-31'); -Result: true - - - - When adding an interval value to (or subtracting an - interval value from) a timestamp - or timestamp with time zone value, the months, days, and - microseconds fields of the interval value are handled in turn. - First, a nonzero months field advances or decrements the date of the - timestamp by the indicated number of months, keeping the day of month the - same unless it would be past the end of the new month, in which case the - last day of that month is used. (For example, March 31 plus 1 month - becomes April 30, but March 31 plus 2 months becomes May 31.) - Then the days field advances or decrements the date of the timestamp by - the indicated number of days. In both these steps the local time of day - is kept the same. Finally, if there is a nonzero microseconds field, it - is added or subtracted literally. - When doing arithmetic on a timestamp with time zone value in - a time zone that recognizes DST, this means that adding or subtracting - (say) interval '1 day' does not necessarily have the - same result as adding or subtracting interval '24 - hours'. - For example, with the session time zone set - to America/Denver: - -SELECT timestamp with time zone '2005-04-02 12:00:00-07' + interval '1 day'; -Result: 2005-04-03 12:00:00-06 -SELECT timestamp with time zone '2005-04-02 12:00:00-07' + interval '24 hours'; -Result: 2005-04-03 13:00:00-06 - - This happens because an hour was skipped due to a change in daylight saving - time at 2005-04-03 02:00:00 in time zone - America/Denver. - - - - Note there can be ambiguity in the months field returned by - age because different months have different numbers of - days. PostgreSQL's approach uses the month from the - earlier of the two dates when calculating partial months. For example, - age('2004-06-01', '2004-04-30') uses April to yield - 1 mon 1 day, while using May would yield 1 mon 2 - days because May has 31 days, while April has only 30. - - - - Subtraction of dates and timestamps can also be complex. One conceptually - simple way to perform subtraction is to convert each value to a number - of seconds using EXTRACT(EPOCH FROM ...), then subtract the - results; this produces the - number of seconds between the two values. This will adjust - for the number of days in each month, timezone changes, and daylight - saving time adjustments. Subtraction of date or timestamp - values with the - operator - returns the number of days (24-hours) and hours/minutes/seconds - between the values, making the same adjustments. The age - function returns years, months, days, and hours/minutes/seconds, - performing field-by-field subtraction and then adjusting for negative - field values. The following queries illustrate the differences in these - approaches. The sample results were produced with timezone - = 'US/Eastern'; there is a daylight saving time change between the - two dates used: - - - -SELECT EXTRACT(EPOCH FROM timestamptz '2013-07-01 12:00:00') - - EXTRACT(EPOCH FROM timestamptz '2013-03-01 12:00:00'); -Result: 10537200.000000 -SELECT (EXTRACT(EPOCH FROM timestamptz '2013-07-01 12:00:00') - - EXTRACT(EPOCH FROM timestamptz '2013-03-01 12:00:00')) - / 60 / 60 / 24; -Result: 121.9583333333333333 -SELECT timestamptz '2013-07-01 12:00:00' - timestamptz '2013-03-01 12:00:00'; -Result: 121 days 23:00:00 -SELECT age(timestamptz '2013-07-01 12:00:00', timestamptz '2013-03-01 12:00:00'); -Result: 4 mons - - - - <function>EXTRACT</function>, <function>date_part</function> - - - date_part - - - extract - - - -EXTRACT(field FROM source) - - - - The extract function retrieves subfields - such as year or hour from date/time values. - source must be a value expression of - type timestamp, date, time, - or interval. (Timestamps and times can be with or - without time zone.) - field is an identifier or - string that selects what field to extract from the source value. - Not all fields are valid for every input data type; for example, fields - smaller than a day cannot be extracted from a date, while - fields of a day or more cannot be extracted from a time. - The extract function returns values of type - numeric. - - - - The following are valid field names: - - - - - century - - - The century; for interval values, the year field - divided by 100 - - - -SELECT EXTRACT(CENTURY FROM TIMESTAMP '2000-12-16 12:21:13'); -Result: 20 -SELECT EXTRACT(CENTURY FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 21 -SELECT EXTRACT(CENTURY FROM DATE '0001-01-01 AD'); -Result: 1 -SELECT EXTRACT(CENTURY FROM DATE '0001-12-31 BC'); -Result: -1 -SELECT EXTRACT(CENTURY FROM INTERVAL '2001 years'); -Result: 20 - - - - - - day - - - The day of the month (1–31); for interval - values, the number of days - - - -SELECT EXTRACT(DAY FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 16 -SELECT EXTRACT(DAY FROM INTERVAL '40 days 1 minute'); -Result: 40 - - - - - - - decade - - - The year field divided by 10 - - - -SELECT EXTRACT(DECADE FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 200 - - - - - - dow - - - The day of the week as Sunday (0) to - Saturday (6) - - - -SELECT EXTRACT(DOW FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 5 - - - Note that extract's day of the week numbering - differs from that of the to_char(..., - 'D') function. - - - - - - - doy - - - The day of the year (1–365/366) - - - -SELECT EXTRACT(DOY FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 47 - - - - - - epoch - - - For timestamp with time zone values, the - number of seconds since 1970-01-01 00:00:00 UTC (negative for - timestamps before that); - for date and timestamp values, the - nominal number of seconds since 1970-01-01 00:00:00, - without regard to timezone or daylight-savings rules; - for interval values, the total number - of seconds in the interval - - - -SELECT EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40.12-08'); -Result: 982384720.120000 -SELECT EXTRACT(EPOCH FROM TIMESTAMP '2001-02-16 20:38:40.12'); -Result: 982355920.120000 -SELECT EXTRACT(EPOCH FROM INTERVAL '5 days 3 hours'); -Result: 442800.000000 - - - - You can convert an epoch value back to a timestamp with time zone - with to_timestamp: - - -SELECT to_timestamp(982384720.12); -Result: 2001-02-17 04:38:40.12+00 - - - - Beware that applying to_timestamp to an epoch - extracted from a date or timestamp value - could produce a misleading result: the result will effectively - assume that the original value had been given in UTC, which might - not be the case. - - - - - - hour - - - The hour field (0–23 in timestamps, unrestricted in - intervals) - - - -SELECT EXTRACT(HOUR FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 20 - - - - - - isodow - - - The day of the week as Monday (1) to - Sunday (7) - - - -SELECT EXTRACT(ISODOW FROM TIMESTAMP '2001-02-18 20:38:40'); -Result: 7 - - - This is identical to dow except for Sunday. This - matches the ISO 8601 day of the week numbering. - - - - - - - isoyear - - - The ISO 8601 week-numbering year that the date - falls in - - - -SELECT EXTRACT(ISOYEAR FROM DATE '2006-01-01'); -Result: 2005 -SELECT EXTRACT(ISOYEAR FROM DATE '2006-01-02'); -Result: 2006 - - - - Each ISO 8601 week-numbering year begins with the - Monday of the week containing the 4th of January, so in early - January or late December the ISO year may be - different from the Gregorian year. See the week - field for more information. - - - - - - julian - - - The Julian Date corresponding to the - date or timestamp. Timestamps - that are not local midnight result in a fractional value. See - for more information. - - - -SELECT EXTRACT(JULIAN FROM DATE '2006-01-01'); -Result: 2453737 -SELECT EXTRACT(JULIAN FROM TIMESTAMP '2006-01-01 12:00'); -Result: 2453737.50000000000000000000 - - - - - - microseconds - - - The seconds field, including fractional parts, multiplied by 1 - 000 000; note that this includes full seconds - - - -SELECT EXTRACT(MICROSECONDS FROM TIME '17:12:28.5'); -Result: 28500000 - - - - - - millennium - - - The millennium; for interval values, the year field - divided by 1000 - - - -SELECT EXTRACT(MILLENNIUM FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 3 -SELECT EXTRACT(MILLENNIUM FROM INTERVAL '2001 years'); -Result: 2 - - - - Years in the 1900s are in the second millennium. - The third millennium started January 1, 2001. - - - - - - milliseconds - - - The seconds field, including fractional parts, multiplied by - 1000. Note that this includes full seconds. - - - -SELECT EXTRACT(MILLISECONDS FROM TIME '17:12:28.5'); -Result: 28500.000 - - - - - - minute - - - The minutes field (0–59) - - - -SELECT EXTRACT(MINUTE FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 38 - - - - - - month - - - The number of the month within the year (1–12); - for interval values, the number of months modulo 12 - (0–11) - - - -SELECT EXTRACT(MONTH FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 2 -SELECT EXTRACT(MONTH FROM INTERVAL '2 years 3 months'); -Result: 3 -SELECT EXTRACT(MONTH FROM INTERVAL '2 years 13 months'); -Result: 1 - - - - - - quarter - - - The quarter of the year (1–4) that the date is in; - for interval values, the month field divided by 3 - plus 1 - - - -SELECT EXTRACT(QUARTER FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 1 -SELECT EXTRACT(QUARTER FROM INTERVAL '1 year 6 months'); -Result: 3 - - - - - - second - - - The seconds field, including any fractional seconds - - - -SELECT EXTRACT(SECOND FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 40.000000 -SELECT EXTRACT(SECOND FROM TIME '17:12:28.5'); -Result: 28.500000 - - - - - timezone - - - The time zone offset from UTC, measured in seconds. Positive values - correspond to time zones east of UTC, negative values to - zones west of UTC. (Technically, - PostgreSQL does not use UTC because - leap seconds are not handled.) - - - - - - timezone_hour - - - The hour component of the time zone offset - - - - - - timezone_minute - - - The minute component of the time zone offset - - - - - - week - - - The number of the ISO 8601 week-numbering week of - the year. By definition, ISO weeks start on Mondays and the first - week of a year contains January 4 of that year. In other words, the - first Thursday of a year is in week 1 of that year. - - - In the ISO week-numbering system, it is possible for early-January - dates to be part of the 52nd or 53rd week of the previous year, and for - late-December dates to be part of the first week of the next year. - For example, 2005-01-01 is part of the 53rd week of year - 2004, and 2006-01-01 is part of the 52nd week of year - 2005, while 2012-12-31 is part of the first week of 2013. - It's recommended to use the isoyear field together with - week to get consistent results. - - - - For interval values, the week field is simply the number - of integral days divided by 7. - - - -SELECT EXTRACT(WEEK FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 7 -SELECT EXTRACT(WEEK FROM INTERVAL '13 days 24 hours'); -Result: 1 - - - - - - year - - - The year field. Keep in mind there is no 0 AD, so subtracting - BC years from AD years should be done with care. - - - -SELECT EXTRACT(YEAR FROM TIMESTAMP '2001-02-16 20:38:40'); -Result: 2001 - - - - - - - - - When processing an interval value, - the extract function produces field values that - match the interpretation used by the interval output function. This - can produce surprising results if one starts with a non-normalized - interval representation, for example: - -SELECT INTERVAL '80 minutes'; -Result: 01:20:00 -SELECT EXTRACT(MINUTES FROM INTERVAL '80 minutes'); -Result: 20 - - - - - - When the input value is +/-Infinity, extract returns - +/-Infinity for monotonically-increasing fields (epoch, - julian, year, isoyear, - decade, century, and millennium - for timestamp inputs; epoch, hour, - day, year, decade, - century, and millennium for - interval inputs). - For other fields, NULL is returned. PostgreSQL - versions before 9.6 returned zero for all cases of infinite input. - - - - - The extract function is primarily intended - for computational processing. For formatting date/time values for - display, see . - - - - The date_part function is modeled on the traditional - Ingres equivalent to the - SQL-standard function extract: - -date_part('field', source) - - Note that here the field parameter needs to - be a string value, not a name. The valid field names for - date_part are the same as for - extract. - For historical reasons, the date_part function - returns values of type double precision. This can result in - a loss of precision in certain uses. Using extract - is recommended instead. - - - -SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40'); -Result: 16 -SELECT date_part('hour', INTERVAL '4 hours 3 minutes'); -Result: 4 - - - - - - <function>date_trunc</function> - - - date_trunc - - - - The function date_trunc is conceptually - similar to the trunc function for numbers. - - - - -date_trunc(field, source , time_zone ) - - source is a value expression of type - timestamp, timestamp with time zone, - or interval. - (Values of type date and - time are cast automatically to timestamp or - interval, respectively.) - field selects to which precision to - truncate the input value. The return value is likewise of type - timestamp, timestamp with time zone, - or interval, - and it has all fields that are less significant than the - selected one set to zero (or one, for day and month). - - - - Valid values for field are: - - microseconds - milliseconds - second - minute - hour - day - week - month - quarter - year - decade - century - millennium - - - - - When the input value is of type timestamp with time zone, - the truncation is performed with respect to a particular time zone; - for example, truncation to day produces a value that - is midnight in that zone. By default, truncation is done with respect - to the current setting, but the - optional time_zone argument can be provided - to specify a different time zone. The time zone name can be specified - in any of the ways described in . - - - - A time zone cannot be specified when processing timestamp without - time zone or interval inputs. These are always - taken at face value. - - - - Examples (assuming the local time zone is America/New_York): - -SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40'); -Result: 2001-02-16 20:00:00 -SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40'); -Result: 2001-01-01 00:00:00 -SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00'); -Result: 2001-02-16 00:00:00-05 -SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', 'Australia/Sydney'); -Result: 2001-02-16 08:00:00-05 -SELECT date_trunc('hour', INTERVAL '3 days 02:47:33'); -Result: 3 days 02:00:00 - - - - - - <function>date_bin</function> - - - date_bin - - - - The function date_bin bins the input - timestamp into the specified interval (the stride) - aligned with a specified origin. - - - - -date_bin(stride, source, origin) - - source is a value expression of type - timestamp or timestamp with time zone. (Values - of type date are cast automatically to - timestamp.) stride is a value - expression of type interval. The return value is likewise - of type timestamp or timestamp with time zone, - and it marks the beginning of the bin into which the - source is placed. - - - - Examples: - -SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01'); -Result: 2020-02-11 15:30:00 -SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01 00:02:30'); -Result: 2020-02-11 15:32:30 - - - - - In the case of full units (1 minute, 1 hour, etc.), it gives the same result as - the analogous date_trunc call, but the difference is - that date_bin can truncate to an arbitrary interval. - - - - The stride interval must be greater than zero and - cannot contain units of month or larger. - - - - - <literal>AT TIME ZONE</literal> and <literal>AT LOCAL</literal> - - - time zone - conversion - - - - AT TIME ZONE - - - - AT LOCAL - - - - The AT TIME ZONE operator converts time - stamp without time zone to/from - time stamp with time zone, and - time with time zone values to different time - zones. shows its - variants. - - - - <literal>AT TIME ZONE</literal> and <literal>AT LOCAL</literal> Variants - - - - - Operator - - - Description - - - Example(s) - - - - - - - - timestamp without time zone AT TIME ZONE zone - timestamp with time zone - - - Converts given time stamp without time zone to - time stamp with time zone, assuming the given - value is in the named time zone. - - - timestamp '2001-02-16 20:38:40' at time zone 'America/Denver' - 2001-02-17 03:38:40+00 - - - - - - timestamp without time zone AT LOCAL - timestamp with time zone - - - Converts given time stamp without time zone to - time stamp with the session's - TimeZone value as time zone. - - - timestamp '2001-02-16 20:38:40' at local - 2001-02-17 03:38:40+00 - - - - - - timestamp with time zone AT TIME ZONE zone - timestamp without time zone - - - Converts given time stamp with time zone to - time stamp without time zone, as the time would - appear in that zone. - - - timestamp with time zone '2001-02-16 20:38:40-05' at time zone 'America/Denver' - 2001-02-16 18:38:40 - - - - - - timestamp with time zone AT LOCAL - timestamp without time zone - - - Converts given time stamp with time zone to - time stamp without time zone, as the time would - appear with the session's TimeZone value as time zone. - - - timestamp with time zone '2001-02-16 20:38:40-05' at local - 2001-02-16 18:38:40 - - - - - - time with time zone AT TIME ZONE zone - time with time zone - - - Converts given time with time zone to a new time - zone. Since no date is supplied, this uses the currently active UTC - offset for the named destination zone. - - - time with time zone '05:34:17-05' at time zone 'UTC' - 10:34:17+00 - - - - - - time with time zone AT LOCAL - time with time zone - - - Converts given time with time zone to a new time - zone. Since no date is supplied, this uses the currently active UTC - offset for the session's TimeZone value. - - - Assuming the session's TimeZone is set to UTC: - - - time with time zone '05:34:17-05' at local - 10:34:17+00 - - - - -
- - - In these expressions, the desired time zone zone can be - specified either as a text value (e.g., 'America/Los_Angeles') - or as an interval (e.g., INTERVAL '-08:00'). - In the text case, a time zone name can be specified in any of the ways - described in . - The interval case is only useful for zones that have fixed offsets from - UTC, so it is not very common in practice. - - - - The syntax AT LOCAL may be used as shorthand for - AT TIME ZONE local, where - local is the session's - TimeZone value. - - - - Examples (assuming the current setting - is America/Los_Angeles): - -SELECT TIMESTAMP '2001-02-16 20:38:40' AT TIME ZONE 'America/Denver'; -Result: 2001-02-16 19:38:40-08 -SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT TIME ZONE 'America/Denver'; -Result: 2001-02-16 18:38:40 -SELECT TIMESTAMP '2001-02-16 20:38:40' AT TIME ZONE 'Asia/Tokyo' AT TIME ZONE 'America/Chicago'; -Result: 2001-02-16 05:38:40 -SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT LOCAL; -Result: 2001-02-16 17:38:40 -SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT TIME ZONE '+05'; -Result: 2001-02-16 20:38:40 -SELECT TIME WITH TIME ZONE '20:38:40-05' AT LOCAL; -Result: 17:38:40 - - The first example adds a time zone to a value that lacks it, and - displays the value using the current TimeZone - setting. The second example shifts the time stamp with time zone value - to the specified time zone, and returns the value without a time zone. - This allows storage and display of values different from the current - TimeZone setting. The third example converts - Tokyo time to Chicago time. The fourth example shifts the time stamp - with time zone value to the time zone currently specified by the - TimeZone setting and returns the value without a - time zone. The fifth example demonstrates that the sign in a POSIX-style - time zone specification has the opposite meaning of the sign in an - ISO-8601 datetime literal, as described in - and . - - - - The sixth example is a cautionary tale. Due to the fact that there is no - date associated with the input value, the conversion is made using the - current date of the session. Therefore, this static example may show a wrong - result depending on the time of the year it is viewed because - 'America/Los_Angeles' observes Daylight Savings Time. - - - - The function timezone(zone, - timestamp) is equivalent to the SQL-conforming construct - timestamp AT TIME ZONE - zone. - - - - The function timezone(zone, - time) is equivalent to the SQL-conforming construct - time AT TIME ZONE - zone. - - - - The function timezone(timestamp) - is equivalent to the SQL-conforming construct timestamp - AT LOCAL. - - - - The function timezone(time) - is equivalent to the SQL-conforming construct time - AT LOCAL. - -
- - - Current Date/Time - - - date - current - - - - time - current - - - - PostgreSQL provides a number of functions - that return values related to the current date and time. These - SQL-standard functions all return values based on the start time of - the current transaction: - -CURRENT_DATE -CURRENT_TIME -CURRENT_TIMESTAMP -CURRENT_TIME(precision) -CURRENT_TIMESTAMP(precision) -LOCALTIME -LOCALTIMESTAMP -LOCALTIME(precision) -LOCALTIMESTAMP(precision) - - - - - CURRENT_TIME and - CURRENT_TIMESTAMP deliver values with time zone; - LOCALTIME and - LOCALTIMESTAMP deliver values without time zone. - - - - CURRENT_TIME, - CURRENT_TIMESTAMP, - LOCALTIME, and - LOCALTIMESTAMP - can optionally take - a precision parameter, which causes the result to be rounded - to that many fractional digits in the seconds field. Without a precision parameter, - the result is given to the full available precision. - - - - Some examples: - -SELECT CURRENT_TIME; -Result: 14:39:53.662522-05 -SELECT CURRENT_DATE; -Result: 2019-12-23 -SELECT CURRENT_TIMESTAMP; -Result: 2019-12-23 14:39:53.662522-05 -SELECT CURRENT_TIMESTAMP(2); -Result: 2019-12-23 14:39:53.66-05 -SELECT LOCALTIMESTAMP; -Result: 2019-12-23 14:39:53.662522 - - - - - Since these functions return - the start time of the current transaction, their values do not - change during the transaction. This is considered a feature: - the intent is to allow a single transaction to have a consistent - notion of the current time, so that multiple - modifications within the same transaction bear the same - time stamp. - - - - - Other database systems might advance these values more - frequently. - - - - - PostgreSQL also provides functions that - return the start time of the current statement, as well as the actual - current time at the instant the function is called. The complete list - of non-SQL-standard time functions is: - -transaction_timestamp() -statement_timestamp() -clock_timestamp() -timeofday() -now() - - - - - transaction_timestamp() is equivalent to - CURRENT_TIMESTAMP, but is named to clearly reflect - what it returns. - statement_timestamp() returns the start time of the current - statement (more specifically, the time of receipt of the latest command - message from the client). - statement_timestamp() and transaction_timestamp() - return the same value during the first command of a transaction, but might - differ during subsequent commands. - clock_timestamp() returns the actual current time, and - therefore its value changes even within a single SQL command. - timeofday() is a historical - PostgreSQL function. Like - clock_timestamp(), it returns the actual current time, - but as a formatted text string rather than a timestamp - with time zone value. - now() is a traditional PostgreSQL - equivalent to transaction_timestamp(). - - - - All the date/time data types also accept the special literal value - now to specify the current date and time (again, - interpreted as the transaction start time). Thus, - the following three all return the same result: - -SELECT CURRENT_TIMESTAMP; -SELECT now(); -SELECT TIMESTAMP 'now'; -- but see tip below - - - - - - Do not use the third form when specifying a value to be evaluated later, - for example in a DEFAULT clause for a table column. - The system will convert now - to a timestamp as soon as the constant is parsed, so that when - the default value is needed, - the time of the table creation would be used! The first two - forms will not be evaluated until the default value is used, - because they are function calls. Thus they will give the desired - behavior of defaulting to the time of row insertion. - (See also .) - - - - - - Delaying Execution - - - pg_sleep - - - pg_sleep_for - - - pg_sleep_until - - - sleep - - - delay - - - - The following functions are available to delay execution of the server - process: - -pg_sleep ( double precision ) -pg_sleep_for ( interval ) -pg_sleep_until ( timestamp with time zone ) - - - pg_sleep makes the current session's process - sleep until the given number of seconds have - elapsed. Fractional-second delays can be specified. - pg_sleep_for is a convenience function to - allow the sleep time to be specified as an interval. - pg_sleep_until is a convenience function for when - a specific wake-up time is desired. - For example: - - -SELECT pg_sleep(1.5); -SELECT pg_sleep_for('5 minutes'); -SELECT pg_sleep_until('tomorrow 03:00'); - - - - - - The effective resolution of the sleep interval is platform-specific; - 0.01 seconds is a common value. The sleep delay will be at least as long - as specified. It might be longer depending on factors such as server load. - In particular, pg_sleep_until is not guaranteed to - wake up exactly at the specified time, but it will not wake up any earlier. - - - - - - Make sure that your session does not hold more locks than necessary - when calling pg_sleep or its variants. Otherwise - other sessions might have to wait for your sleeping process, slowing down - the entire system. - - - - -
- - - - Enum Support Functions - - - For enum types (described in ), - there are several functions that allow cleaner programming without - hard-coding particular values of an enum type. - These are listed in . The examples - assume an enum type created as: - - -CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple'); - - - - - - Enum Support Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - enum_first - - enum_first ( anyenum ) - anyenum - - - Returns the first value of the input enum type. - - - enum_first(null::rainbow) - red - - - - - - enum_last - - enum_last ( anyenum ) - anyenum - - - Returns the last value of the input enum type. - - - enum_last(null::rainbow) - purple - - - - - - enum_range - - enum_range ( anyenum ) - anyarray - - - Returns all values of the input enum type in an ordered array. - - - enum_range(null::rainbow) - {red,orange,yellow,&zwsp;green,blue,purple} - - - - - enum_range ( anyenum, anyenum ) - anyarray - - - Returns the range between the two given enum values, as an ordered - array. The values must be from the same enum type. If the first - parameter is null, the result will start with the first value of - the enum type. - If the second parameter is null, the result will end with the last - value of the enum type. - - - enum_range('orange'::rainbow, 'green'::rainbow) - {orange,yellow,green} - - - enum_range(NULL, 'green'::rainbow) - {red,orange,&zwsp;yellow,green} - - - enum_range('orange'::rainbow, NULL) - {orange,yellow,green,&zwsp;blue,purple} - - - - -
- - - Notice that except for the two-argument form of enum_range, - these functions disregard the specific value passed to them; they care - only about its declared data type. Either null or a specific value of - the type can be passed, with the same result. It is more common to - apply these functions to a table column or function argument than to - a hardwired type name as used in the examples. - -
- - - Geometric Functions and Operators - - - The geometric types point, box, - lseg, line, path, - polygon, and circle have a large set of - native support functions and operators, shown in , , and . - - - - Geometric Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - geometric_type + point - geometric_type - - - Adds the coordinates of the second point to those of each - point of the first argument, thus performing translation. - Available for point, box, path, - circle. - - - box '(1,1),(0,0)' + point '(2,0)' - (3,1),(2,0) - - - - - - path + path - path - - - Concatenates two open paths (returns NULL if either path is closed). - - - path '[(0,0),(1,1)]' + path '[(2,2),(3,3),(4,4)]' - [(0,0),(1,1),(2,2),(3,3),(4,4)] - - - - - - geometric_type - point - geometric_type - - - Subtracts the coordinates of the second point from those - of each point of the first argument, thus performing translation. - Available for point, box, path, - circle. - - - box '(1,1),(0,0)' - point '(2,0)' - (-1,1),(-2,0) - - - - - - geometric_type * point - geometric_type - - - Multiplies each point of the first argument by the second - point (treating a point as being a complex number - represented by real and imaginary parts, and performing standard - complex multiplication). If one interprets - the second point as a vector, this is equivalent to - scaling the object's size and distance from the origin by the length - of the vector, and rotating it counterclockwise around the origin by - the vector's angle from the x axis. - Available for point, box,Rotating a - box with these operators only moves its corner points: the box is - still considered to have sides parallel to the axes. Hence the box's - size is not preserved, as a true rotation would do. - path, circle. - - - path '((0,0),(1,0),(1,1))' * point '(3.0,0)' - ((0,0),(3,0),(3,3)) - - - path '((0,0),(1,0),(1,1))' * point(cosd(45), sind(45)) - ((0,0),&zwsp;(0.7071067811865475,0.7071067811865475),&zwsp;(0,1.414213562373095)) - - - - - - geometric_type / point - geometric_type - - - Divides each point of the first argument by the second - point (treating a point as being a complex number - represented by real and imaginary parts, and performing standard - complex division). If one interprets - the second point as a vector, this is equivalent to - scaling the object's size and distance from the origin down by the - length of the vector, and rotating it clockwise around the origin by - the vector's angle from the x axis. - Available for point, box, path, - circle. - - - path '((0,0),(1,0),(1,1))' / point '(2.0,0)' - ((0,0),(0.5,0),(0.5,0.5)) - - - path '((0,0),(1,0),(1,1))' / point(cosd(45), sind(45)) - ((0,0),&zwsp;(0.7071067811865476,-0.7071067811865476),&zwsp;(1.4142135623730951,0)) - - - - - - @-@ geometric_type - double precision - - - Computes the total length. - Available for lseg, path. - - - @-@ path '[(0,0),(1,0),(1,1)]' - 2 - - - - - - @@ geometric_type - point - - - Computes the center point. - Available for box, lseg, - polygon, circle. - - - @@ box '(2,2),(0,0)' - (1,1) - - - - - - # geometric_type - integer - - - Returns the number of points. - Available for path, polygon. - - - # path '((1,0),(0,1),(-1,0))' - 3 - - - - - - geometric_type # geometric_type - point - - - Computes the point of intersection, or NULL if there is none. - Available for lseg, line. - - - lseg '[(0,0),(1,1)]' # lseg '[(1,0),(0,1)]' - (0.5,0.5) - - - - - - box # box - box - - - Computes the intersection of two boxes, or NULL if there is none. - - - box '(2,2),(-1,-1)' # box '(1,1),(-2,-2)' - (1,1),(-1,-1) - - - - - - geometric_type ## geometric_type - point - - - Computes the closest point to the first object on the second object. - Available for these pairs of types: - (point, box), - (point, lseg), - (point, line), - (lseg, box), - (lseg, lseg), - (line, lseg). - - - point '(0,0)' ## lseg '[(2,0),(0,2)]' - (1,1) - - - - - - geometric_type <-> geometric_type - double precision - - - Computes the distance between the objects. - Available for all seven geometric types, for all combinations - of point with another geometric type, and for - these additional pairs of types: - (box, lseg), - (lseg, line), - (polygon, circle) - (and the commutator cases). - - - circle '<(0,0),1>' <-> circle '<(5,0),1>' - 3 - - - - - - geometric_type @> geometric_type - boolean - - - Does first object contain second? - Available for these pairs of types: - (box, point), - (box, box), - (path, point), - (polygon, point), - (polygon, polygon), - (circle, point), - (circle, circle). - - - circle '<(0,0),2>' @> point '(1,1)' - t - - - - - - geometric_type <@ geometric_type - boolean - - - Is first object contained in or on second? - Available for these pairs of types: - (point, box), - (point, lseg), - (point, line), - (point, path), - (point, polygon), - (point, circle), - (box, box), - (lseg, box), - (lseg, line), - (polygon, polygon), - (circle, circle). - - - point '(1,1)' <@ circle '<(0,0),2>' - t - - - - - - geometric_type && geometric_type - boolean - - - Do these objects overlap? (One point in common makes this true.) - Available for box, polygon, - circle. - - - box '(1,1),(0,0)' && box '(2,2),(0,0)' - t - - - - - - geometric_type << geometric_type - boolean - - - Is first object strictly left of second? - Available for point, box, - polygon, circle. - - - circle '<(0,0),1>' << circle '<(5,0),1>' - t - - - - - - geometric_type >> geometric_type - boolean - - - Is first object strictly right of second? - Available for point, box, - polygon, circle. - - - circle '<(5,0),1>' >> circle '<(0,0),1>' - t - - - - - - geometric_type &< geometric_type - boolean - - - Does first object not extend to the right of second? - Available for box, polygon, - circle. - - - box '(1,1),(0,0)' &< box '(2,2),(0,0)' - t - - - - - - geometric_type &> geometric_type - boolean - - - Does first object not extend to the left of second? - Available for box, polygon, - circle. - - - box '(3,3),(0,0)' &> box '(2,2),(0,0)' - t - - - - - - geometric_type <<| geometric_type - boolean - - - Is first object strictly below second? - Available for point, box, polygon, - circle. - - - box '(3,3),(0,0)' <<| box '(5,5),(3,4)' - t - - - - - - geometric_type |>> geometric_type - boolean - - - Is first object strictly above second? - Available for point, box, polygon, - circle. - - - box '(5,5),(3,4)' |>> box '(3,3),(0,0)' - t - - - - - - geometric_type &<| geometric_type - boolean - - - Does first object not extend above second? - Available for box, polygon, - circle. - - - box '(1,1),(0,0)' &<| box '(2,2),(0,0)' - t - - - - - - geometric_type |&> geometric_type - boolean - - - Does first object not extend below second? - Available for box, polygon, - circle. - - - box '(3,3),(0,0)' |&> box '(2,2),(0,0)' - t - - - - - - box <^ box - boolean - - - Is first object below second (allows edges to touch)? - - - box '((1,1),(0,0))' <^ box '((2,2),(1,1))' - t - - - - - - box >^ box - boolean - - - Is first object above second (allows edges to touch)? - - - box '((2,2),(1,1))' >^ box '((1,1),(0,0))' - t - - - - - - geometric_type ?# geometric_type - boolean - - - Do these objects intersect? - Available for these pairs of types: - (box, box), - (lseg, box), - (lseg, lseg), - (lseg, line), - (line, box), - (line, line), - (path, path). - - - lseg '[(-1,0),(1,0)]' ?# box '(2,2),(-2,-2)' - t - - - - - - ?- line - boolean - - - ?- lseg - boolean - - - Is line horizontal? - - - ?- lseg '[(-1,0),(1,0)]' - t - - - - - - point ?- point - boolean - - - Are points horizontally aligned (that is, have same y coordinate)? - - - point '(1,0)' ?- point '(0,0)' - t - - - - - - ?| line - boolean - - - ?| lseg - boolean - - - Is line vertical? - - - ?| lseg '[(-1,0),(1,0)]' - f - - - - - - point ?| point - boolean - - - Are points vertically aligned (that is, have same x coordinate)? - - - point '(0,1)' ?| point '(0,0)' - t - - - - - - line ?-| line - boolean - - - lseg ?-| lseg - boolean - - - Are lines perpendicular? - - - lseg '[(0,0),(0,1)]' ?-| lseg '[(0,0),(1,0)]' - t - - - - - - line ?|| line - boolean - - - lseg ?|| lseg - boolean - - - Are lines parallel? - - - lseg '[(-1,0),(1,0)]' ?|| lseg '[(-1,2),(1,2)]' - t - - - - - - geometric_type ~= geometric_type - boolean - - - Are these objects the same? - Available for point, box, - polygon, circle. - - - polygon '((0,0),(1,1))' ~= polygon '((1,1),(0,0))' - t - - - - -
- - - - Note that the same as operator, ~=, - represents the usual notion of equality for the point, - box, polygon, and circle types. - Some of the geometric types also have an = operator, but - = compares for equal areas only. - The other scalar comparison operators (<= and so - on), where available for these types, likewise compare areas. - - - - - - Before PostgreSQL 14, the point - is strictly below/above comparison operators point - <<| point and point - |>> point were respectively - called <^ and >^. These - names are still available, but are deprecated and will eventually be - removed. - - - - - Geometric Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - area - - area ( geometric_type ) - double precision - - - Computes area. - Available for box, path, circle. - A path input must be closed, else NULL is returned. - Also, if the path is self-intersecting, the result may be - meaningless. - - - area(box '(2,2),(0,0)') - 4 - - - - - - - center - - center ( geometric_type ) - point - - - Computes center point. - Available for box, circle. - - - center(box '(1,2),(0,0)') - (0.5,1) - - - - - - - diagonal - - diagonal ( box ) - lseg - - - Extracts box's diagonal as a line segment - (same as lseg(box)). - - - diagonal(box '(1,2),(0,0)') - [(1,2),(0,0)] - - - - - - - diameter - - diameter ( circle ) - double precision - - - Computes diameter of circle. - - - diameter(circle '<(0,0),2>') - 4 - - - - - - - height - - height ( box ) - double precision - - - Computes vertical size of box. - - - height(box '(1,2),(0,0)') - 2 - - - - - - - isclosed - - isclosed ( path ) - boolean - - - Is path closed? - - - isclosed(path '((0,0),(1,1),(2,0))') - t - - - - - - - isopen - - isopen ( path ) - boolean - - - Is path open? - - - isopen(path '[(0,0),(1,1),(2,0)]') - t - - - - - - - length - - length ( geometric_type ) - double precision - - - Computes the total length. - Available for lseg, path. - - - length(path '((-1,0),(1,0))') - 4 - - - - - - - npoints - - npoints ( geometric_type ) - integer - - - Returns the number of points. - Available for path, polygon. - - - npoints(path '[(0,0),(1,1),(2,0)]') - 3 - - - - - - - pclose - - pclose ( path ) - path - - - Converts path to closed form. - - - pclose(path '[(0,0),(1,1),(2,0)]') - ((0,0),(1,1),(2,0)) - - - - - - - popen - - popen ( path ) - path - - - Converts path to open form. - - - popen(path '((0,0),(1,1),(2,0))') - [(0,0),(1,1),(2,0)] - - - - - - - radius - - radius ( circle ) - double precision - - - Computes radius of circle. - - - radius(circle '<(0,0),2>') - 2 - - - - - - - slope - - slope ( point, point ) - double precision - - - Computes slope of a line drawn through the two points. - - - slope(point '(0,0)', point '(2,1)') - 0.5 - - - - - - - width - - width ( box ) - double precision - - - Computes horizontal size of box. - - - width(box '(1,2),(0,0)') - 1 - - - - -
- - - Geometric Type Conversion Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - box - - box ( circle ) - box - - - Computes box inscribed within the circle. - - - box(circle '<(0,0),2>') - (1.414213562373095,1.414213562373095),&zwsp;(-1.414213562373095,-1.414213562373095) - - - - - - box ( point ) - box - - - Converts point to empty box. - - - box(point '(1,0)') - (1,0),(1,0) - - - - - - box ( point, point ) - box - - - Converts any two corner points to box. - - - box(point '(0,1)', point '(1,0)') - (1,1),(0,0) - - - - - - box ( polygon ) - box - - - Computes bounding box of polygon. - - - box(polygon '((0,0),(1,1),(2,0))') - (2,1),(0,0) - - - - - - - bound_box - - bound_box ( box, box ) - box - - - Computes bounding box of two boxes. - - - bound_box(box '(1,1),(0,0)', box '(4,4),(3,3)') - (4,4),(0,0) - - - - - - - circle - - circle ( box ) - circle - - - Computes smallest circle enclosing box. - - - circle(box '(1,1),(0,0)') - <(0.5,0.5),0.7071067811865476> - - - - - - circle ( point, double precision ) - circle - - - Constructs circle from center and radius. - - - circle(point '(0,0)', 2.0) - <(0,0),2> - - - - - - circle ( polygon ) - circle - - - Converts polygon to circle. The circle's center is the mean of the - positions of the polygon's points, and the radius is the average - distance of the polygon's points from that center. - - - circle(polygon '((0,0),(1,3),(2,0))') - <(1,1),1.6094757082487299> - - - - - - - line - - line ( point, point ) - line - - - Converts two points to the line through them. - - - line(point '(-1,0)', point '(1,0)') - {0,-1,0} - - - - - - - lseg - - lseg ( box ) - lseg - - - Extracts box's diagonal as a line segment. - - - lseg(box '(1,0),(-1,0)') - [(1,0),(-1,0)] - - - - - - lseg ( point, point ) - lseg - - - Constructs line segment from two endpoints. - - - lseg(point '(-1,0)', point '(1,0)') - [(-1,0),(1,0)] - - - - - - - path - - path ( polygon ) - path - - - Converts polygon to a closed path with the same list of points. - - - path(polygon '((0,0),(1,1),(2,0))') - ((0,0),(1,1),(2,0)) - - - - - - - point - - point ( double precision, double precision ) - point - - - Constructs point from its coordinates. - - - point(23.4, -44.5) - (23.4,-44.5) - - - - - - point ( box ) - point - - - Computes center of box. - - - point(box '(1,0),(-1,0)') - (0,0) - - - - - - point ( circle ) - point - - - Computes center of circle. - - - point(circle '<(0,0),2>') - (0,0) - - - - - - point ( lseg ) - point - - - Computes center of line segment. - - - point(lseg '[(-1,0),(1,0)]') - (0,0) - - - - - - point ( polygon ) - point - - - Computes center of polygon (the mean of the - positions of the polygon's points). - - - point(polygon '((0,0),(1,1),(2,0))') - (1,0.3333333333333333) - - - - - - - polygon - - polygon ( box ) - polygon - - - Converts box to a 4-point polygon. - - - polygon(box '(1,1),(0,0)') - ((0,0),(0,1),(1,1),(1,0)) - - - - - - polygon ( circle ) - polygon - - - Converts circle to a 12-point polygon. - - - polygon(circle '<(0,0),2>') - ((-2,0),&zwsp;(-1.7320508075688774,0.9999999999999999),&zwsp;(-1.0000000000000002,1.7320508075688772),&zwsp;(-1.2246063538223773e-16,2),&zwsp;(0.9999999999999996,1.7320508075688774),&zwsp;(1.732050807568877,1.0000000000000007),&zwsp;(2,2.4492127076447545e-16),&zwsp;(1.7320508075688776,-0.9999999999999994),&zwsp;(1.0000000000000009,-1.7320508075688767),&zwsp;(3.673819061467132e-16,-2),&zwsp;(-0.9999999999999987,-1.732050807568878),&zwsp;(-1.7320508075688767,-1.0000000000000009)) - - - - - - polygon ( integer, circle ) - polygon - - - Converts circle to an n-point polygon. - - - polygon(4, circle '<(3,0),1>') - ((2,0),&zwsp;(3,1),&zwsp;(4,1.2246063538223773e-16),&zwsp;(3,-1)) - - - - - - polygon ( path ) - polygon - - - Converts closed path to a polygon with the same list of points. - - - polygon(path '((0,0),(1,1),(2,0))') - ((0,0),(1,1),(2,0)) - - - - - -
- - - It is possible to access the two component numbers of a point - as though the point were an array with indexes 0 and 1. For example, if - t.p is a point column then - SELECT p[0] FROM t retrieves the X coordinate and - UPDATE t SET p[1] = ... changes the Y coordinate. - In the same way, a value of type box or lseg can be treated - as an array of two point values. - - -
- - - - Network Address Functions and Operators - - - The IP network address types, cidr and inet, - support the usual comparison operators shown in - - as well as the specialized operators and functions shown in - and - . - - - - Any cidr value can be cast to inet implicitly; - therefore, the operators and functions shown below as operating on - inet also work on cidr values. (Where there are - separate functions for inet and cidr, it is - because the behavior should be different for the two cases.) - Also, it is permitted to cast an inet value - to cidr. When this is done, any bits to the right of the - netmask are silently zeroed to create a valid cidr value. - - - - IP Address Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - inet << inet - boolean - - - Is subnet strictly contained by subnet? - This operator, and the next four, test for subnet inclusion. They - consider only the network parts of the two addresses (ignoring any - bits to the right of the netmasks) and determine whether one network - is identical to or a subnet of the other. - - - inet '192.168.1.5' << inet '192.168.1/24' - t - - - inet '192.168.0.5' << inet '192.168.1/24' - f - - - inet '192.168.1/24' << inet '192.168.1/24' - f - - - - - - inet <<= inet - boolean - - - Is subnet contained by or equal to subnet? - - - inet '192.168.1/24' <<= inet '192.168.1/24' - t - - - - - - inet >> inet - boolean - - - Does subnet strictly contain subnet? - - - inet '192.168.1/24' >> inet '192.168.1.5' - t - - - - - - inet >>= inet - boolean - - - Does subnet contain or equal subnet? - - - inet '192.168.1/24' >>= inet '192.168.1/24' - t - - - - - - inet && inet - boolean - - - Does either subnet contain or equal the other? - - - inet '192.168.1/24' && inet '192.168.1.80/28' - t - - - inet '192.168.1/24' && inet '192.168.2.0/28' - f - - - - - - ~ inet - inet - - - Computes bitwise NOT. - - - ~ inet '192.168.1.6' - 63.87.254.249 - - - - - - inet & inet - inet - - - Computes bitwise AND. - - - inet '192.168.1.6' & inet '0.0.0.255' - 0.0.0.6 - - - - - - inet | inet - inet - - - Computes bitwise OR. - - - inet '192.168.1.6' | inet '0.0.0.255' - 192.168.1.255 - - - - - - inet + bigint - inet - - - Adds an offset to an address. - - - inet '192.168.1.6' + 25 - 192.168.1.31 - - - - - - bigint + inet - inet - - - Adds an offset to an address. - - - 200 + inet '::ffff:fff0:1' - ::ffff:255.240.0.201 - - - - - - inet - bigint - inet - - - Subtracts an offset from an address. - - - inet '192.168.1.43' - 36 - 192.168.1.7 - - - - - - inet - inet - bigint - - - Computes the difference of two addresses. - - - inet '192.168.1.43' - inet '192.168.1.19' - 24 - - - inet '::1' - inet '::ffff:1' - -4294901760 - - - - -
- - - IP Address Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - abbrev - - abbrev ( inet ) - text - - - Creates an abbreviated display format as text. - (The result is the same as the inet output function - produces; it is abbreviated only in comparison to the - result of an explicit cast to text, which for historical - reasons will never suppress the netmask part.) - - - abbrev(inet '10.1.0.0/32') - 10.1.0.0 - - - - - - abbrev ( cidr ) - text - - - Creates an abbreviated display format as text. - (The abbreviation consists of dropping all-zero octets to the right - of the netmask; more examples are in - .) - - - abbrev(cidr '10.1.0.0/16') - 10.1/16 - - - - - - - broadcast - - broadcast ( inet ) - inet - - - Computes the broadcast address for the address's network. - - - broadcast(inet '192.168.1.5/24') - 192.168.1.255/24 - - - - - - - family - - family ( inet ) - integer - - - Returns the address's family: 4 for IPv4, - 6 for IPv6. - - - family(inet '::1') - 6 - - - - - - - host - - host ( inet ) - text - - - Returns the IP address as text, ignoring the netmask. - - - host(inet '192.168.1.0/24') - 192.168.1.0 - - - - - - - hostmask - - hostmask ( inet ) - inet - - - Computes the host mask for the address's network. - - - hostmask(inet '192.168.23.20/30') - 0.0.0.3 - - - - - - - inet_merge - - inet_merge ( inet, inet ) - cidr - - - Computes the smallest network that includes both of the given networks. - - - inet_merge(inet '192.168.1.5/24', inet '192.168.2.5/24') - 192.168.0.0/22 - - - - - - - inet_same_family - - inet_same_family ( inet, inet ) - boolean - - - Tests whether the addresses belong to the same IP family. - - - inet_same_family(inet '192.168.1.5/24', inet '::1') - f - - - - - - - masklen - - masklen ( inet ) - integer - - - Returns the netmask length in bits. - - - masklen(inet '192.168.1.5/24') - 24 - - - - - - - netmask - - netmask ( inet ) - inet - - - Computes the network mask for the address's network. - - - netmask(inet '192.168.1.5/24') - 255.255.255.0 - - - - - - - network - - network ( inet ) - cidr - - - Returns the network part of the address, zeroing out - whatever is to the right of the netmask. - (This is equivalent to casting the value to cidr.) - - - network(inet '192.168.1.5/24') - 192.168.1.0/24 - - - - - - - set_masklen - - set_masklen ( inet, integer ) - inet - - - Sets the netmask length for an inet value. - The address part does not change. - - - set_masklen(inet '192.168.1.5/24', 16) - 192.168.1.5/16 - - - - - - set_masklen ( cidr, integer ) - cidr - - - Sets the netmask length for a cidr value. - Address bits to the right of the new netmask are set to zero. - - - set_masklen(cidr '192.168.1.0/24', 16) - 192.168.0.0/16 - - - - - - - text - - text ( inet ) - text - - - Returns the unabbreviated IP address and netmask length as text. - (This has the same result as an explicit cast to text.) - - - text(inet '192.168.1.5') - 192.168.1.5/32 - - - - -
- - - - The abbrev, host, - and text functions are primarily intended to offer - alternative display formats for IP addresses. - - - - - The MAC address types, macaddr and macaddr8, - support the usual comparison operators shown in - - as well as the specialized functions shown in - . - In addition, they support the bitwise logical operators - ~, & and | - (NOT, AND and OR), just as shown above for IP addresses. - - - - MAC Address Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - trunc - - trunc ( macaddr ) - macaddr - - - Sets the last 3 bytes of the address to zero. The remaining prefix - can be associated with a particular manufacturer (using data not - included in PostgreSQL). - - - trunc(macaddr '12:34:56:78:90:ab') - 12:34:56:00:00:00 - - - - - - trunc ( macaddr8 ) - macaddr8 - - - Sets the last 5 bytes of the address to zero. The remaining prefix - can be associated with a particular manufacturer (using data not - included in PostgreSQL). - - - trunc(macaddr8 '12:34:56:78:90:ab:cd:ef') - 12:34:56:00:00:00:00:00 - - - - - - - macaddr8_set7bit - - macaddr8_set7bit ( macaddr8 ) - macaddr8 - - - Sets the 7th bit of the address to one, creating what is known as - modified EUI-64, for inclusion in an IPv6 address. - - - macaddr8_set7bit(macaddr8 '00:34:56:ab:cd:ef') - 02:34:56:ff:fe:ab:cd:ef - - - - -
- -
- - - - Text Search Functions and Operators - - - full text search - functions and operators - - - - text search - functions and operators - - - - , - and - - summarize the functions and operators that are provided - for full text searching. See for a detailed - explanation of PostgreSQL's text search - facility. - - - - Text Search Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - tsvector @@ tsquery - boolean - - - tsquery @@ tsvector - boolean - - - Does tsvector match tsquery? - (The arguments can be given in either order.) - - - to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat') - t - - - - - - text @@ tsquery - boolean - - - Does text string, after implicit invocation - of to_tsvector(), match tsquery? - - - 'fat cats ate rats' @@ to_tsquery('cat & rat') - t - - - - - - tsvector || tsvector - tsvector - - - Concatenates two tsvectors. If both inputs contain - lexeme positions, the second input's positions are adjusted - accordingly. - - - 'a:1 b:2'::tsvector || 'c:1 d:2 b:3'::tsvector - 'a':1 'b':2,5 'c':3 'd':4 - - - - - - tsquery && tsquery - tsquery - - - ANDs two tsquerys together, producing a query that - matches documents that match both input queries. - - - 'fat | rat'::tsquery && 'cat'::tsquery - ( 'fat' | 'rat' ) & 'cat' - - - - - - tsquery || tsquery - tsquery - - - ORs two tsquerys together, producing a query that - matches documents that match either input query. - - - 'fat | rat'::tsquery || 'cat'::tsquery - 'fat' | 'rat' | 'cat' - - - - - - !! tsquery - tsquery - - - Negates a tsquery, producing a query that matches - documents that do not match the input query. - - - !! 'cat'::tsquery - !'cat' - - - - - - tsquery <-> tsquery - tsquery - - - Constructs a phrase query, which matches if the two input queries - match at successive lexemes. - - - to_tsquery('fat') <-> to_tsquery('rat') - 'fat' <-> 'rat' - - - - - - tsquery @> tsquery - boolean - - - Does first tsquery contain the second? (This considers - only whether all the lexemes appearing in one query appear in the - other, ignoring the combining operators.) - - - 'cat'::tsquery @> 'cat & rat'::tsquery - f - - - - - - tsquery <@ tsquery - boolean - - - Is first tsquery contained in the second? (This - considers only whether all the lexemes appearing in one query appear - in the other, ignoring the combining operators.) - - - 'cat'::tsquery <@ 'cat & rat'::tsquery - t - - - 'cat'::tsquery <@ '!cat & rat'::tsquery - t - - - - -
- - - In addition to these specialized operators, the usual comparison - operators shown in are - available for types tsvector and tsquery. - These are not very - useful for text searching but allow, for example, unique indexes to be - built on columns of these types. - - - - Text Search Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - array_to_tsvector - - array_to_tsvector ( text[] ) - tsvector - - - Converts an array of text strings to a tsvector. - The given strings are used as lexemes as-is, without further - processing. Array elements must not be empty strings - or NULL. - - - array_to_tsvector('{fat,cat,rat}'::text[]) - 'cat' 'fat' 'rat' - - - - - - - get_current_ts_config - - get_current_ts_config ( ) - regconfig - - - Returns the OID of the current default text search configuration - (as set by ). - - - get_current_ts_config() - english - - - - - - - length - - length ( tsvector ) - integer - - - Returns the number of lexemes in the tsvector. - - - length('fat:2,4 cat:3 rat:5A'::tsvector) - 3 - - - - - - - numnode - - numnode ( tsquery ) - integer - - - Returns the number of lexemes plus operators in - the tsquery. - - - numnode('(fat & rat) | cat'::tsquery) - 5 - - - - - - - plainto_tsquery - - plainto_tsquery ( - config regconfig, - query text ) - tsquery - - - Converts text to a tsquery, normalizing words according to - the specified or default configuration. Any punctuation in the string - is ignored (it does not determine query operators). The resulting - query matches documents containing all non-stopwords in the text. - - - plainto_tsquery('english', 'The Fat Rats') - 'fat' & 'rat' - - - - - - - phraseto_tsquery - - phraseto_tsquery ( - config regconfig, - query text ) - tsquery - - - Converts text to a tsquery, normalizing words according to - the specified or default configuration. Any punctuation in the string - is ignored (it does not determine query operators). The resulting - query matches phrases containing all non-stopwords in the text. - - - phraseto_tsquery('english', 'The Fat Rats') - 'fat' <-> 'rat' - - - phraseto_tsquery('english', 'The Cat and Rats') - 'cat' <2> 'rat' - - - - - - - websearch_to_tsquery - - websearch_to_tsquery ( - config regconfig, - query text ) - tsquery - - - Converts text to a tsquery, normalizing words according - to the specified or default configuration. Quoted word sequences are - converted to phrase tests. The word or is understood - as producing an OR operator, and a dash produces a NOT operator; - other punctuation is ignored. - This approximates the behavior of some common web search tools. - - - websearch_to_tsquery('english', '"fat rat" or cat dog') - 'fat' <-> 'rat' | 'cat' & 'dog' - - - - - - - querytree - - querytree ( tsquery ) - text - - - Produces a representation of the indexable portion of - a tsquery. A result that is empty or - just T indicates a non-indexable query. - - - querytree('foo & ! bar'::tsquery) - 'foo' - - - - - - - setweight - - setweight ( vector tsvector, weight "char" ) - tsvector - - - Assigns the specified weight to each element - of the vector. - - - setweight('fat:2,4 cat:3 rat:5B'::tsvector, 'A') - 'cat':3A 'fat':2A,4A 'rat':5A - - - - - - - setweight - setweight for specific lexeme(s) - - setweight ( vector tsvector, weight "char", lexemes text[] ) - tsvector - - - Assigns the specified weight to elements - of the vector that are listed - in lexemes. - The strings in lexemes are taken as lexemes - as-is, without further processing. Strings that do not match any - lexeme in vector are ignored. - - - setweight('fat:2,4 cat:3 rat:5,6B'::tsvector, 'A', '{cat,rat}') - 'cat':3A 'fat':2,4 'rat':5A,6A - - - - - - - strip - - strip ( tsvector ) - tsvector - - - Removes positions and weights from the tsvector. - - - strip('fat:2,4 cat:3 rat:5A'::tsvector) - 'cat' 'fat' 'rat' - - - - - - - to_tsquery - - to_tsquery ( - config regconfig, - query text ) - tsquery - - - Converts text to a tsquery, normalizing words according to - the specified or default configuration. The words must be combined - by valid tsquery operators. - - - to_tsquery('english', 'The & Fat & Rats') - 'fat' & 'rat' - - - - - - - to_tsvector - - to_tsvector ( - config regconfig, - document text ) - tsvector - - - Converts text to a tsvector, normalizing words according - to the specified or default configuration. Position information is - included in the result. - - - to_tsvector('english', 'The Fat Rats') - 'fat':2 'rat':3 - - - - - - to_tsvector ( - config regconfig, - document json ) - tsvector - - - to_tsvector ( - config regconfig, - document jsonb ) - tsvector - - - Converts each string value in the JSON document to - a tsvector, normalizing words according to the specified - or default configuration. The results are then concatenated in - document order to produce the output. Position information is - generated as though one stopword exists between each pair of string - values. (Beware that document order of the fields of a - JSON object is implementation-dependent when the input - is jsonb; observe the difference in the examples.) - - - to_tsvector('english', '{"aa": "The Fat Rats", "b": "dog"}'::json) - 'dog':5 'fat':2 'rat':3 - - - to_tsvector('english', '{"aa": "The Fat Rats", "b": "dog"}'::jsonb) - 'dog':1 'fat':4 'rat':5 - - - - - - - json_to_tsvector - - json_to_tsvector ( - config regconfig, - document json, - filter jsonb ) - tsvector - - - - jsonb_to_tsvector - - jsonb_to_tsvector ( - config regconfig, - document jsonb, - filter jsonb ) - tsvector - - - Selects each item in the JSON document that is requested by - the filter and converts each one to - a tsvector, normalizing words according to the specified - or default configuration. The results are then concatenated in - document order to produce the output. Position information is - generated as though one stopword exists between each pair of selected - items. (Beware that document order of the fields of a - JSON object is implementation-dependent when the input - is jsonb.) - The filter must be a jsonb - array containing zero or more of these keywords: - "string" (to include all string values), - "numeric" (to include all numeric values), - "boolean" (to include all boolean values), - "key" (to include all keys), or - "all" (to include all the above). - As a special case, the filter can also be a - simple JSON value that is one of these keywords. - - - json_to_tsvector('english', '{"a": "The Fat Rats", "b": 123}'::json, '["string", "numeric"]') - '123':5 'fat':2 'rat':3 - - - json_to_tsvector('english', '{"cat": "The Fat Rats", "dog": 123}'::json, '"all"') - '123':9 'cat':1 'dog':7 'fat':4 'rat':5 - - - - - - - ts_delete - - ts_delete ( vector tsvector, lexeme text ) - tsvector - - - Removes any occurrence of the given lexeme - from the vector. - The lexeme string is treated as a lexeme as-is, - without further processing. - - - ts_delete('fat:2,4 cat:3 rat:5A'::tsvector, 'fat') - 'cat':3 'rat':5A - - - - - - ts_delete ( vector tsvector, lexemes text[] ) - tsvector - - - Removes any occurrences of the lexemes - in lexemes - from the vector. - The strings in lexemes are taken as lexemes - as-is, without further processing. Strings that do not match any - lexeme in vector are ignored. - - - ts_delete('fat:2,4 cat:3 rat:5A'::tsvector, ARRAY['fat','rat']) - 'cat':3 - - - - - - - ts_filter - - ts_filter ( vector tsvector, weights "char"[] ) - tsvector - - - Selects only elements with the given weights - from the vector. - - - ts_filter('fat:2,4 cat:3b,7c rat:5A'::tsvector, '{a,b}') - 'cat':3B 'rat':5A - - - - - - - ts_headline - - ts_headline ( - config regconfig, - document text, - query tsquery - , options text ) - text - - - Displays, in an abbreviated form, the match(es) for - the query in - the document, which must be raw text not - a tsvector. Words in the document are normalized - according to the specified or default configuration before matching to - the query. Use of this function is discussed in - , which also describes the - available options. - - - ts_headline('The fat cat ate the rat.', 'cat') - The fat <b>cat</b> ate the rat. - - - - - - ts_headline ( - config regconfig, - document json, - query tsquery - , options text ) - text - - - ts_headline ( - config regconfig, - document jsonb, - query tsquery - , options text ) - text - - - Displays, in an abbreviated form, match(es) for - the query that occur in string values - within the JSON document. - See for more details. - - - ts_headline('{"cat":"raining cats and dogs"}'::jsonb, 'cat') - {"cat": "raining <b>cats</b> and dogs"} - - - - - - - ts_rank - - ts_rank ( - weights real[], - vector tsvector, - query tsquery - , normalization integer ) - real - - - Computes a score showing how well - the vector matches - the query. See - for details. - - - ts_rank(to_tsvector('raining cats and dogs'), 'cat') - 0.06079271 - - - - - - - ts_rank_cd - - ts_rank_cd ( - weights real[], - vector tsvector, - query tsquery - , normalization integer ) - real - - - Computes a score showing how well - the vector matches - the query, using a cover density - algorithm. See for details. - - - ts_rank_cd(to_tsvector('raining cats and dogs'), 'cat') - 0.1 - - - - - - - ts_rewrite - - ts_rewrite ( query tsquery, - target tsquery, - substitute tsquery ) - tsquery - - - Replaces occurrences of target - with substitute - within the query. - See for details. - - - ts_rewrite('a & b'::tsquery, 'a'::tsquery, 'foo|bar'::tsquery) - 'b' & ( 'foo' | 'bar' ) - - - - - - ts_rewrite ( query tsquery, - select text ) - tsquery - - - Replaces portions of the query according to - target(s) and substitute(s) obtained by executing - a SELECT command. - See for details. - - - SELECT ts_rewrite('a & b'::tsquery, 'SELECT t,s FROM aliases') - 'b' & ( 'foo' | 'bar' ) - - - - - - - tsquery_phrase - - tsquery_phrase ( query1 tsquery, query2 tsquery ) - tsquery - - - Constructs a phrase query that searches - for matches of query1 - and query2 at successive lexemes (same - as <-> operator). - - - tsquery_phrase(to_tsquery('fat'), to_tsquery('cat')) - 'fat' <-> 'cat' - - - - - - tsquery_phrase ( query1 tsquery, query2 tsquery, distance integer ) - tsquery - - - Constructs a phrase query that searches - for matches of query1 and - query2 that occur exactly - distance lexemes apart. - - - tsquery_phrase(to_tsquery('fat'), to_tsquery('cat'), 10) - 'fat' <10> 'cat' - - - - - - - tsvector_to_array - - tsvector_to_array ( tsvector ) - text[] - - - Converts a tsvector to an array of lexemes. - - - tsvector_to_array('fat:2,4 cat:3 rat:5A'::tsvector) - {cat,fat,rat} - - - - - - - unnest - for tsvector - - unnest ( tsvector ) - setof record - ( lexeme text, - positions smallint[], - weights text ) - - - Expands a tsvector into a set of rows, one per lexeme. - - - select * from unnest('cat:3 fat:2,4 rat:5A'::tsvector) - - - lexeme | positions | weights ---------+-----------+--------- - cat | {3} | {D} - fat | {2,4} | {D,D} - rat | {5} | {A} - - - - - -
- - - - All the text search functions that accept an optional regconfig - argument will use the configuration specified by - - when that argument is omitted. - - - - - The functions in - - are listed separately because they are not usually used in everyday text - searching operations. They are primarily helpful for development and - debugging of new text search configurations. - - - - Text Search Debugging Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - ts_debug - - ts_debug ( - config regconfig, - document text ) - setof record - ( alias text, - description text, - token text, - dictionaries regdictionary[], - dictionary regdictionary, - lexemes text[] ) - - - Extracts and normalizes tokens from - the document according to the specified or - default text search configuration, and returns information about how - each token was processed. - See for details. - - - ts_debug('english', 'The Brightest supernovaes') - (asciiword,"Word, all ASCII",The,{english_stem},english_stem,{}) ... - - - - - - - ts_lexize - - ts_lexize ( dict regdictionary, token text ) - text[] - - - Returns an array of replacement lexemes if the input token is known to - the dictionary, or an empty array if the token is known to the - dictionary but it is a stop word, or NULL if it is not a known word. - See for details. - - - ts_lexize('english_stem', 'stars') - {star} - - - - - - - ts_parse - - ts_parse ( parser_name text, - document text ) - setof record - ( tokid integer, - token text ) - - - Extracts tokens from the document using the - named parser. - See for details. - - - ts_parse('default', 'foo - bar') - (1,foo) ... - - - - - - ts_parse ( parser_oid oid, - document text ) - setof record - ( tokid integer, - token text ) - - - Extracts tokens from the document using a - parser specified by OID. - See for details. - - - ts_parse(3722, 'foo - bar') - (1,foo) ... - - - - - - - ts_token_type - - ts_token_type ( parser_name text ) - setof record - ( tokid integer, - alias text, - description text ) - - - Returns a table that describes each type of token the named parser can - recognize. - See for details. - - - ts_token_type('default') - (1,asciiword,"Word, all ASCII") ... - - - - - - ts_token_type ( parser_oid oid ) - setof record - ( tokid integer, - alias text, - description text ) - - - Returns a table that describes each type of token a parser specified - by OID can recognize. - See for details. - - - ts_token_type(3722) - (1,asciiword,"Word, all ASCII") ... - - - - - - - ts_stat - - ts_stat ( sqlquery text - , weights text ) - setof record - ( word text, - ndoc integer, - nentry integer ) - - - Executes the sqlquery, which must return a - single tsvector column, and returns statistics about each - distinct lexeme contained in the data. - See for details. - - - ts_stat('SELECT vector FROM apod') - (foo,10,15) ... - - - - -
- -
- - - UUID Functions - - - UUID - generating - - - - gen_random_uuid - - - - uuidv4 - - - - uuidv7 - - - - uuid_extract_timestamp - - - - uuid_extract_version - - - - shows the PostgreSQL - functions that can be used to generate UUIDs. - - - - <acronym>UUID</acronym> Generation Functions - - - - - - Function - - - Description - - - Example(s) - - - - - - - - - - gen_random_uuid - uuid - - - uuidv4 - uuid - - - Generate a version 4 (random) UUID. - - - gen_random_uuid() - 5b30857f-0bfa-48b5-ac0b-5c64e28078d1 - - - uuidv4() - b42410ee-132f-42ee-9e4f-09a6485c95b8 - - - - - - - uuidv7 - ( shift interval ) - uuid - - - Generate a version 7 (time-ordered) UUID. The timestamp is computed using UNIX timestamp - with millisecond precision + sub-millisecond timestamp + random. The optional parameter - shift will shift the computed timestamp by the given interval. - - - uuidv7() - 019535d9-3df7-79fb-b466-fa907fa17f9e - - - - - -
- - - - The module provides additional functions that - implement other standard algorithms for generating UUIDs. - - - - - shows the PostgreSQL - functions that can be used to extract information from UUIDs. - - - - <acronym>UUID</acronym> Extraction Functions - - - - - - Function - - - Description - - - Example(s) - - - - - - - - - - uuid_extract_timestamp - ( uuid ) - timestamp with time zone - - - Extracts a timestamp with time zone from UUID - version 1 and 7. For other versions, this function returns null. Note that - the extracted timestamp is not necessarily exactly equal to the time the - UUID was generated; this depends on the implementation that generated the - UUID. - - - uuid_extract_timestamp('019535d9-3df7-79fb-b466-&zwsp;fa907fa17f9e'::uuid) - 2025-02-23 21:46:24.503-05 - - - - - - - uuid_extract_version - ( uuid ) - smallint - - - Extracts the version from a UUID of the variant described by - RFC 9562. For - other variants, this function returns null. For example, for a UUID - generated by gen_random_uuid, this function will - return 4. - - - uuid_extract_version('41db1265-8bc1-4ab3-992f-&zwsp;885799a4af1d'::uuid) - 4 - - - uuid_extract_version('019535d9-3df7-79fb-b466-&zwsp;fa907fa17f9e'::uuid) - 7 - - - - - -
- - - PostgreSQL also provides the usual comparison - operators shown in for - UUIDs. - - - See for details on the data type - uuid in PostgreSQL. - -
- - - - XML Functions - - - XML Functions - - - - The functions and function-like expressions described in this - section operate on values of type xml. See for information about the xml - type. The function-like expressions xmlparse - and xmlserialize for converting to and from - type xml are documented there, not in this section. - - - - Use of most of these functions - requires PostgreSQL to have been built - with configure --with-libxml. - - - - Producing XML Content - - - A set of functions and function-like expressions is available for - producing XML content from SQL data. As such, they are - particularly suitable for formatting query results into XML - documents for processing in client applications. - - - - <literal>xmltext</literal> - - - xmltext - - - -xmltext ( text ) xml - - - - The function xmltext returns an XML value with a single - text node containing the input argument as its content. Predefined entities - like ampersand (), left and right angle brackets - (]]>), and quotation marks () - are escaped. - - - - Example: -'); - xmltext -------------------------- - < foo & bar > -]]> - - - - - <literal>xmlcomment</literal> - - - xmlcomment - - - -xmlcomment ( text ) xml - - - - The function xmlcomment creates an XML value - containing an XML comment with the specified text as content. - The text cannot contain -- or end with a - -, otherwise the resulting construct - would not be a valid XML comment. - If the argument is null, the result is null. - - - - Example: - -]]> - - - - - <literal>xmlconcat</literal> - - - xmlconcat - - - -xmlconcat ( xml , ... ) xml - - - - The function xmlconcat concatenates a list - of individual XML values to create a single value containing an - XML content fragment. Null values are omitted; the result is - only null if there are no nonnull arguments. - - - - Example: -', 'foo'); - - xmlconcat ----------------------- - foo -]]> - - - - XML declarations, if present, are combined as follows. If all - argument values have the same XML version declaration, that - version is used in the result, else no version is used. If all - argument values have the standalone declaration value - yes, then that value is used in the result. If - all argument values have a standalone declaration value and at - least one is no, then that is used in the result. - Else the result will have no standalone declaration. If the - result is determined to require a standalone declaration but no - version declaration, a version declaration with version 1.0 will - be used because XML requires an XML declaration to contain a - version declaration. Encoding declarations are ignored and - removed in all cases. - - - - Example: -', ''); - - xmlconcat ------------------------------------ - -]]> - - - - - <literal>xmlelement</literal> - - - xmlelement - - - -xmlelement ( NAME name , XMLATTRIBUTES ( attvalue AS attname , ... ) , content , ... ) xml - - - - The xmlelement expression produces an XML - element with the given name, attributes, and content. - The name - and attname items shown in the syntax are - simple identifiers, not values. The attvalue - and content items are expressions, which can - yield any PostgreSQL data type. The - argument(s) within XMLATTRIBUTES generate attributes - of the XML element; the content value(s) are - concatenated to form its content. - - - - Examples: - - -SELECT xmlelement(name foo, xmlattributes('xyz' as bar)); - - xmlelement ------------------- - - -SELECT xmlelement(name foo, xmlattributes(current_date as bar), 'cont', 'ent'); - - xmlelement -------------------------------------- - content -]]> - - - - Element and attribute names that are not valid XML names are - escaped by replacing the offending characters by the sequence - _xHHHH_, where - HHHH is the character's Unicode - codepoint in hexadecimal notation. For example: - -]]> - - - - An explicit attribute name need not be specified if the attribute - value is a column reference, in which case the column's name will - be used as the attribute name by default. In other cases, the - attribute must be given an explicit name. So this example is - valid: - -CREATE TABLE test (a xml, b xml); -SELECT xmlelement(name test, xmlattributes(a, b)) FROM test; - - But these are not: - -SELECT xmlelement(name test, xmlattributes('constant'), a, b) FROM test; -SELECT xmlelement(name test, xmlattributes(func(a, b))) FROM test; - - - - - Element content, if specified, will be formatted according to - its data type. If the content is itself of type xml, - complex XML documents can be constructed. For example: - -]]> - - Content of other types will be formatted into valid XML character - data. This means in particular that the characters <, >, - and & will be converted to entities. Binary data (data type - bytea) will be represented in base64 or hex - encoding, depending on the setting of the configuration parameter - . The particular behavior for - individual data types is expected to evolve in order to align the - PostgreSQL mappings with those specified in SQL:2006 and later, - as discussed in . - - - - - <literal>xmlforest</literal> - - - xmlforest - - - -xmlforest ( content AS name , ... ) xml - - - - The xmlforest expression produces an XML - forest (sequence) of elements using the given names and content. - As for xmlelement, - each name must be a simple identifier, while - the content expressions can have any data - type. - - - - Examples: - -SELECT xmlforest('abc' AS foo, 123 AS bar); - - xmlforest ------------------------------- - <foo>abc</foo><bar>123</bar> - - -SELECT xmlforest(table_name, column_name) -FROM information_schema.columns -WHERE table_schema = 'pg_catalog'; - - xmlforest -------------------------------------&zwsp;----------------------------------- - <table_name>pg_authid</table_name>&zwsp;<column_name>rolname</column_name> - <table_name>pg_authid</table_name>&zwsp;<column_name>rolsuper</column_name> - ... - - - As seen in the second example, the element name can be omitted if - the content value is a column reference, in which case the column - name is used by default. Otherwise, a name must be specified. - - - - Element names that are not valid XML names are escaped as shown - for xmlelement above. Similarly, content - data is escaped to make valid XML content, unless it is already - of type xml. - - - - Note that XML forests are not valid XML documents if they consist - of more than one element, so it might be useful to wrap - xmlforest expressions in - xmlelement. - - - - - <literal>xmlpi</literal> - - - xmlpi - - - -xmlpi ( NAME name , content ) xml - - - - The xmlpi expression creates an XML - processing instruction. - As for xmlelement, - the name must be a simple identifier, while - the content expression can have any data type. - The content, if present, must not contain the - character sequence ?>. - - - - Example: - -]]> - - - - - <literal>xmlroot</literal> - - - xmlroot - - - -xmlroot ( xml, VERSION {text|NO VALUE} , STANDALONE {YES|NO|NO VALUE} ) xml - - - - The xmlroot expression alters the properties - of the root node of an XML value. If a version is specified, - it replaces the value in the root node's version declaration; if a - standalone setting is specified, it replaces the value in the - root node's standalone declaration. - - - -abc'), - version '1.0', standalone yes); - - xmlroot ----------------------------------------- - - abc -]]> - - - - - <literal>xmlagg</literal> - - - xmlagg - - - -xmlagg ( xml ) xml - - - - The function xmlagg is, unlike the other - functions described here, an aggregate function. It concatenates the - input values to the aggregate function call, - much like xmlconcat does, except that concatenation - occurs across rows rather than across expressions in a single row. - See for additional information - about aggregate functions. - - - - Example: -abc'); -INSERT INTO test VALUES (2, ''); -SELECT xmlagg(x) FROM test; - xmlagg ----------------------- - abc -]]> - - - - To determine the order of the concatenation, an ORDER BY - clause may be added to the aggregate call as described in - . For example: - -abc -]]> - - - - The following non-standard approach used to be recommended - in previous versions, and may still be useful in specific - cases: - -abc -]]> - - - - - - XML Predicates - - - The expressions described in this section check properties - of xml values. - - - - <literal>IS DOCUMENT</literal> - - - IS DOCUMENT - - - -xml IS DOCUMENT boolean - - - - The expression IS DOCUMENT returns true if the - argument XML value is a proper XML document, false if it is not - (that is, it is a content fragment), or null if the argument is - null. See about the difference - between documents and content fragments. - - - - - <literal>IS NOT DOCUMENT</literal> - - - IS NOT DOCUMENT - - - -xml IS NOT DOCUMENT boolean - - - - The expression IS NOT DOCUMENT returns false if the - argument XML value is a proper XML document, true if it is not (that is, - it is a content fragment), or null if the argument is null. - - - - - <literal>XMLEXISTS</literal> - - - XMLEXISTS - - - -XMLEXISTS ( text PASSING BY {REF|VALUE} xml BY {REF|VALUE} ) boolean - - - - The function xmlexists evaluates an XPath 1.0 - expression (the first argument), with the passed XML value as its context - item. The function returns false if the result of that evaluation - yields an empty node-set, true if it yields any other value. The - function returns null if any argument is null. A nonnull value - passed as the context item must be an XML document, not a content - fragment or any non-XML value. - - - - Example: - TorontoOttawa'); - - xmlexists ------------- - t -(1 row) -]]> - - - - The BY REF and BY VALUE clauses - are accepted in PostgreSQL, but are ignored, - as discussed in . - - - - In the SQL standard, the xmlexists function - evaluates an expression in the XML Query language, - but PostgreSQL allows only an XPath 1.0 - expression, as discussed in - . - - - - - <literal>xml_is_well_formed</literal> - - - xml_is_well_formed - - - - xml_is_well_formed_document - - - - xml_is_well_formed_content - - - -xml_is_well_formed ( text ) boolean -xml_is_well_formed_document ( text ) boolean -xml_is_well_formed_content ( text ) boolean - - - - These functions check whether a text string represents - well-formed XML, returning a Boolean result. - xml_is_well_formed_document checks for a well-formed - document, while xml_is_well_formed_content checks - for well-formed content. xml_is_well_formed does - the former if the configuration - parameter is set to DOCUMENT, or the latter if it is set to - CONTENT. This means that - xml_is_well_formed is useful for seeing whether - a simple cast to type xml will succeed, whereas the other two - functions are useful for seeing whether the corresponding variants of - XMLPARSE will succeed. - - - - Examples: - -'); - xml_is_well_formed --------------------- - f -(1 row) - -SELECT xml_is_well_formed(''); - xml_is_well_formed --------------------- - t -(1 row) - -SET xmloption TO CONTENT; -SELECT xml_is_well_formed('abc'); - xml_is_well_formed --------------------- - t -(1 row) - -SELECT xml_is_well_formed_document('bar'); - xml_is_well_formed_document ------------------------------ - t -(1 row) - -SELECT xml_is_well_formed_document('bar'); - xml_is_well_formed_document ------------------------------ - f -(1 row) -]]> - - The last example shows that the checks include whether - namespaces are correctly matched. - - - - - - Processing XML - - - To process values of data type xml, PostgreSQL offers - the functions xpath and - xpath_exists, which evaluate XPath 1.0 - expressions, and the XMLTABLE - table function. - - - - <literal>xpath</literal> - - - XPath - - - -xpath ( xpath text, xml xml , nsarray text[] ) xml[] - - - - The function xpath evaluates the XPath 1.0 - expression xpath (given as text) - against the XML value - xml. It returns an array of XML values - corresponding to the node-set produced by the XPath expression. - If the XPath expression returns a scalar value rather than a node-set, - a single-element array is returned. - - - - The second argument must be a well formed XML document. In particular, - it must have a single root node element. - - - - The optional third argument of the function is an array of namespace - mappings. This array should be a two-dimensional text array with - the length of the second axis being equal to 2 (i.e., it should be an - array of arrays, each of which consists of exactly 2 elements). - The first element of each array entry is the namespace name (alias), the - second the namespace URI. It is not required that aliases provided in - this array be the same as those being used in the XML document itself (in - other words, both in the XML document and in the xpath - function context, aliases are local). - - - - Example: -test', - ARRAY[ARRAY['my', 'http://example.com']]); - - xpath --------- - {test} -(1 row) -]]> - - - - To deal with default (anonymous) namespaces, do something like this: -test', - ARRAY[ARRAY['mydefns', 'http://example.com']]); - - xpath --------- - {test} -(1 row) -]]> - - - - - <literal>xpath_exists</literal> - - - xpath_exists - - - -xpath_exists ( xpath text, xml xml , nsarray text[] ) boolean - - - - The function xpath_exists is a specialized form - of the xpath function. Instead of returning the - individual XML values that satisfy the XPath 1.0 expression, this function - returns a Boolean indicating whether the query was satisfied or not - (specifically, whether it produced any value other than an empty node-set). - This function is equivalent to the XMLEXISTS predicate, - except that it also offers support for a namespace mapping argument. - - - - Example: -test', - ARRAY[ARRAY['my', 'http://example.com']]); - - xpath_exists --------------- - t -(1 row) -]]> - - - - - <literal>xmltable</literal> - - - xmltable - - - - table function - XMLTABLE - - - -XMLTABLE ( - XMLNAMESPACES ( namespace_uri AS namespace_name , ... ), - row_expression PASSING BY {REF|VALUE} document_expression BY {REF|VALUE} - COLUMNS name { type PATH column_expression DEFAULT default_expression NOT NULL | NULL - | FOR ORDINALITY } - , ... -) setof record - - - - The xmltable expression produces a table based - on an XML value, an XPath filter to extract rows, and a - set of column definitions. - Although it syntactically resembles a function, it can only appear - as a table in a query's FROM clause. - - - - The optional XMLNAMESPACES clause gives a - comma-separated list of namespace definitions, where - each namespace_uri is a text - expression and each namespace_name is a simple - identifier. It specifies the XML namespaces used in the document and - their aliases. A default namespace specification is not currently - supported. - - - - The required row_expression argument is an - XPath 1.0 expression (given as text) that is evaluated, - passing the XML value document_expression as - its context item, to obtain a set of XML nodes. These nodes are what - xmltable transforms into output rows. No rows - will be produced if the document_expression - is null, nor if the row_expression produces - an empty node-set or any value other than a node-set. - - - - document_expression provides the context - item for the row_expression. It must be a - well-formed XML document; fragments/forests are not accepted. - The BY REF and BY VALUE clauses - are accepted but ignored, as discussed in - . - - - - In the SQL standard, the xmltable function - evaluates expressions in the XML Query language, - but PostgreSQL allows only XPath 1.0 - expressions, as discussed in - . - - - - The required COLUMNS clause specifies the - column(s) that will be produced in the output table. - See the syntax summary above for the format. - A name is required for each column, as is a data type - (unless FOR ORDINALITY is specified, in which case - type integer is implicit). The path, default and - nullability clauses are optional. - - - - A column marked FOR ORDINALITY will be populated - with row numbers, starting with 1, in the order of nodes retrieved from - the row_expression's result node-set. - At most one column may be marked FOR ORDINALITY. - - - - - XPath 1.0 does not specify an order for nodes in a node-set, so code - that relies on a particular order of the results will be - implementation-dependent. Details can be found in - . - - - - - The column_expression for a column is an - XPath 1.0 expression that is evaluated for each row, with the current - node from the row_expression result as its - context item, to find the value of the column. If - no column_expression is given, then the - column name is used as an implicit path. - - - - If a column's XPath expression returns a non-XML value (which is limited - to string, boolean, or double in XPath 1.0) and the column has a - PostgreSQL type other than xml, the column will be set - as if by assigning the value's string representation to the PostgreSQL - type. (If the value is a boolean, its string representation is taken - to be 1 or 0 if the output - column's type category is numeric, otherwise true or - false.) - - - - If a column's XPath expression returns a non-empty set of XML nodes - and the column's PostgreSQL type is xml, the column will - be assigned the expression result exactly, if it is of document or - content form. - - - A result containing more than one element node at the top level, or - non-whitespace text outside of an element, is an example of content form. - An XPath result can be of neither form, for example if it returns an - attribute node selected from the element that contains it. Such a result - will be put into content form with each such disallowed node replaced by - its string value, as defined for the XPath 1.0 - string function. - - - - - - A non-XML result assigned to an xml output column produces - content, a single text node with the string value of the result. - An XML result assigned to a column of any other type may not have more than - one node, or an error is raised. If there is exactly one node, the column - will be set as if by assigning the node's string - value (as defined for the XPath 1.0 string function) - to the PostgreSQL type. - - - - The string value of an XML element is the concatenation, in document order, - of all text nodes contained in that element and its descendants. The string - value of an element with no descendant text nodes is an - empty string (not NULL). - Any xsi:nil attributes are ignored. - Note that the whitespace-only text() node between two non-text - elements is preserved, and that leading whitespace on a text() - node is not flattened. - The XPath 1.0 string function may be consulted for the - rules defining the string value of other XML node types and non-XML values. - - - - The conversion rules presented here are not exactly those of the SQL - standard, as discussed in . - - - - If the path expression returns an empty node-set - (typically, when it does not match) - for a given row, the column will be set to NULL, unless - a default_expression is specified; then the - value resulting from evaluating that expression is used. - - - - A default_expression, rather than being - evaluated immediately when xmltable is called, - is evaluated each time a default is needed for the column. - If the expression qualifies as stable or immutable, the repeat - evaluation may be skipped. - This means that you can usefully use volatile functions like - nextval in - default_expression. - - - - Columns may be marked NOT NULL. If the - column_expression for a NOT - NULL column does not match anything and there is - no DEFAULT or - the default_expression also evaluates to null, - an error is reported. - - - - Examples: - - - AU - Australia - - - JP - Japan - Shinzo Abe - 145935 - - - SG - Singapore - 697 - - -$$ AS data; - -SELECT xmltable.* - FROM xmldata, - XMLTABLE('//ROWS/ROW' - PASSING data - COLUMNS id int PATH '@id', - ordinality FOR ORDINALITY, - "COUNTRY_NAME" text, - country_id text PATH 'COUNTRY_ID', - size_sq_km float PATH 'SIZE[@unit = "sq_km"]', - size_other text PATH - 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)', - premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); - - id | ordinality | COUNTRY_NAME | country_id | size_sq_km | size_other | premier_name -----+------------+--------------+------------+------------+--------------+--------------- - 1 | 1 | Australia | AU | | | not specified - 5 | 2 | Japan | JP | | 145935 sq_mi | Shinzo Abe - 6 | 3 | Singapore | SG | 697 | | not specified -]]> - - The following example shows concatenation of multiple text() nodes, - usage of the column name as XPath filter, and the treatment of whitespace, - XML comments and processing instructions: - - - Hello2a2 bbbxxxCC - -$$ AS data; - -SELECT xmltable.* - FROM xmlelements, XMLTABLE('/root' PASSING data COLUMNS element text); - element -------------------------- - Hello2a2 bbbxxxCC -]]> - - - - The following example illustrates how - the XMLNAMESPACES clause can be used to specify - a list of namespaces - used in the XML document as well as in the XPath expressions: - - - - - -'::xml) -) -SELECT xmltable.* - FROM XMLTABLE(XMLNAMESPACES('http://example.com/myns' AS x, - 'http://example.com/b' AS "B"), - '/x:example/x:item' - PASSING (SELECT data FROM xmldata) - COLUMNS foo int PATH '@foo', - bar int PATH '@B:bar'); - foo | bar ------+----- - 1 | 2 - 3 | 4 - 4 | 5 -(3 rows) -]]> - - - - - - Mapping Tables to XML - - - XML export - - - - The following functions map the contents of relational tables to - XML values. They can be thought of as XML export functionality: - -table_to_xml ( table regclass, nulls boolean, - tableforest boolean, targetns text ) xml -query_to_xml ( query text, nulls boolean, - tableforest boolean, targetns text ) xml -cursor_to_xml ( cursor refcursor, count integer, nulls boolean, - tableforest boolean, targetns text ) xml - - - - - table_to_xml maps the content of the named - table, passed as parameter table. The - regclass type accepts strings identifying tables using the - usual notation, including optional schema qualification and - double quotes (see for details). - query_to_xml executes the - query whose text is passed as parameter - query and maps the result set. - cursor_to_xml fetches the indicated number of - rows from the cursor specified by the parameter - cursor. This variant is recommended if - large tables have to be mapped, because the result value is built - up in memory by each function. - - - - If tableforest is false, then the resulting - XML document looks like this: - - - data - data - - - - ... - - - ... - -]]> - - If tableforest is true, the result is an - XML content fragment that looks like this: - - data - data - - - - ... - - -... -]]> - - If no table name is available, that is, when mapping a query or a - cursor, the string table is used in the first - format, row in the second format. - - - - The choice between these formats is up to the user. The first - format is a proper XML document, which will be important in many - applications. The second format tends to be more useful in the - cursor_to_xml function if the result values are to be - reassembled into one document later on. The functions for - producing XML content discussed above, in particular - xmlelement, can be used to alter the results - to taste. - - - - The data values are mapped in the same way as described for the - function xmlelement above. - - - - The parameter nulls determines whether null - values should be included in the output. If true, null values in - columns are represented as: - -]]> - where xsi is the XML namespace prefix for XML - Schema Instance. An appropriate namespace declaration will be - added to the result value. If false, columns containing null - values are simply omitted from the output. - - - - The parameter targetns specifies the - desired XML namespace of the result. If no particular namespace - is wanted, an empty string should be passed. - - - - The following functions return XML Schema documents describing the - mappings performed by the corresponding functions above: - -table_to_xmlschema ( table regclass, nulls boolean, - tableforest boolean, targetns text ) xml -query_to_xmlschema ( query text, nulls boolean, - tableforest boolean, targetns text ) xml -cursor_to_xmlschema ( cursor refcursor, nulls boolean, - tableforest boolean, targetns text ) xml - - It is essential that the same parameters are passed in order to - obtain matching XML data mappings and XML Schema documents. - - - - The following functions produce XML data mappings and the - corresponding XML Schema in one document (or forest), linked - together. They can be useful where self-contained and - self-describing results are wanted: - -table_to_xml_and_xmlschema ( table regclass, nulls boolean, - tableforest boolean, targetns text ) xml -query_to_xml_and_xmlschema ( query text, nulls boolean, - tableforest boolean, targetns text ) xml - - - - - In addition, the following functions are available to produce - analogous mappings of entire schemas or the entire current - database: - -schema_to_xml ( schema name, nulls boolean, - tableforest boolean, targetns text ) xml -schema_to_xmlschema ( schema name, nulls boolean, - tableforest boolean, targetns text ) xml -schema_to_xml_and_xmlschema ( schema name, nulls boolean, - tableforest boolean, targetns text ) xml - -database_to_xml ( nulls boolean, - tableforest boolean, targetns text ) xml -database_to_xmlschema ( nulls boolean, - tableforest boolean, targetns text ) xml -database_to_xml_and_xmlschema ( nulls boolean, - tableforest boolean, targetns text ) xml - - - These functions ignore tables that are not readable by the current user. - The database-wide functions additionally ignore schemas that the current - user does not have USAGE (lookup) privilege for. - - - - Note that these potentially produce a lot of data, which needs to - be built up in memory. When requesting content mappings of large - schemas or databases, it might be worthwhile to consider mapping the - tables separately instead, possibly even through a cursor. - - - - The result of a schema content mapping looks like this: - - - -table1-mapping - -table2-mapping - -... - -]]> - - where the format of a table mapping depends on the - tableforest parameter as explained above. - - - - The result of a database content mapping looks like this: - - - - - ... - - - - ... - - -... - -]]> - - where the schema mapping is as above. - - - - As an example of using the output produced by these functions, - shows an XSLT stylesheet that - converts the output of - table_to_xml_and_xmlschema to an HTML - document containing a tabular rendition of the table data. In a - similar manner, the results from these functions can be - converted into other XML-based formats. - - - - XSLT Stylesheet for Converting SQL/XML Output to HTML - - - - - - - - - - - - - <xsl:value-of select="name(current())"/> - - - - - - - - - - - - - - - - -
- - -
- -
-]]>
-
-
-
- - - JSON Functions and Operators - - - JSON - functions and operators - - - SQL/JSON - functions and expressions - - - - This section describes: - - - - - functions and operators for processing and creating JSON data - - - - - the SQL/JSON path language - - - - - the SQL/JSON query functions - - - - - - - To provide native support for JSON data types within the SQL environment, - PostgreSQL implements the - SQL/JSON data model. - This model comprises sequences of items. Each item can hold SQL scalar - values, with an additional SQL/JSON null value, and composite data structures - that use JSON arrays and objects. The model is a formalization of the implied - data model in the JSON specification - RFC 7159. - - - - SQL/JSON allows you to handle JSON data alongside regular SQL data, - with transaction support, including: - - - - - Uploading JSON data into the database and storing it in - regular SQL columns as character or binary strings. - - - - - Generating JSON objects and arrays from relational data. - - - - - Querying JSON data using SQL/JSON query functions and - SQL/JSON path language expressions. - - - - - - - To learn more about the SQL/JSON standard, see - . For details on JSON types - supported in PostgreSQL, - see . - - - - Processing and Creating JSON Data - - - shows the operators that - are available for use with JSON data types (see ). - In addition, the usual comparison operators shown in are available for - jsonb, though not for json. The comparison - operators follow the ordering rules for B-tree operations outlined in - . - See also for the aggregate - function json_agg which aggregates record - values as JSON, the aggregate function - json_object_agg which aggregates pairs of values - into a JSON object, and their jsonb equivalents, - jsonb_agg and jsonb_object_agg. - - - - <type>json</type> and <type>jsonb</type> Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - json -> integer - json - - - jsonb -> integer - jsonb - - - Extracts n'th element of JSON array - (array elements are indexed from zero, but negative integers count - from the end). - - - '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json -> 2 - {"c":"baz"} - - - '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json -> -3 - {"a":"foo"} - - - - - - json -> text - json - - - jsonb -> text - jsonb - - - Extracts JSON object field with the given key. - - - '{"a": {"b":"foo"}}'::json -> 'a' - {"b":"foo"} - - - - - - json ->> integer - text - - - jsonb ->> integer - text - - - Extracts n'th element of JSON array, - as text. - - - '[1,2,3]'::json ->> 2 - 3 - - - - - - json ->> text - text - - - jsonb ->> text - text - - - Extracts JSON object field with the given key, as text. - - - '{"a":1,"b":2}'::json ->> 'b' - 2 - - - - - - json #> text[] - json - - - jsonb #> text[] - jsonb - - - Extracts JSON sub-object at the specified path, where path elements - can be either field keys or array indexes. - - - '{"a": {"b": ["foo","bar"]}}'::json #> '{a,b,1}' - "bar" - - - - - - json #>> text[] - text - - - jsonb #>> text[] - text - - - Extracts JSON sub-object at the specified path as text. - - - '{"a": {"b": ["foo","bar"]}}'::json #>> '{a,b,1}' - bar - - - - -
- - - - The field/element/path extraction operators return NULL, rather than - failing, if the JSON input does not have the right structure to match - the request; for example if no such key or array element exists. - - - - - Some further operators exist only for jsonb, as shown - in . - - describes how these operators can be used to effectively search indexed - jsonb data. - - - - Additional <type>jsonb</type> Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - jsonb @> jsonb - boolean - - - Does the first JSON value contain the second? - (See for details about containment.) - - - '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb - t - - - - - - jsonb <@ jsonb - boolean - - - Is the first JSON value contained in the second? - - - '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb - t - - - - - - jsonb ? text - boolean - - - Does the text string exist as a top-level key or array element within - the JSON value? - - - '{"a":1, "b":2}'::jsonb ? 'b' - t - - - '["a", "b", "c"]'::jsonb ? 'b' - t - - - - - - jsonb ?| text[] - boolean - - - Do any of the strings in the text array exist as top-level keys or - array elements? - - - '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'd'] - t - - - - - - jsonb ?& text[] - boolean - - - Do all of the strings in the text array exist as top-level keys or - array elements? - - - '["a", "b", "c"]'::jsonb ?& array['a', 'b'] - t - - - - - - jsonb || jsonb - jsonb - - - Concatenates two jsonb values. - Concatenating two arrays generates an array containing all the - elements of each input. Concatenating two objects generates an - object containing the union of their - keys, taking the second object's value when there are duplicate keys. - All other cases are treated by converting a non-array input into a - single-element array, and then proceeding as for two arrays. - Does not operate recursively: only the top-level array or object - structure is merged. - - - '["a", "b"]'::jsonb || '["a", "d"]'::jsonb - ["a", "b", "a", "d"] - - - '{"a": "b"}'::jsonb || '{"c": "d"}'::jsonb - {"a": "b", "c": "d"} - - - '[1, 2]'::jsonb || '3'::jsonb - [1, 2, 3] - - - '{"a": "b"}'::jsonb || '42'::jsonb - [{"a": "b"}, 42] - - - To append an array to another array as a single entry, wrap it - in an additional layer of array, for example: - - - '[1, 2]'::jsonb || jsonb_build_array('[3, 4]'::jsonb) - [1, 2, [3, 4]] - - - - - - jsonb - text - jsonb - - - Deletes a key (and its value) from a JSON object, or matching string - value(s) from a JSON array. - - - '{"a": "b", "c": "d"}'::jsonb - 'a' - {"c": "d"} - - - '["a", "b", "c", "b"]'::jsonb - 'b' - ["a", "c"] - - - - - - jsonb - text[] - jsonb - - - Deletes all matching keys or array elements from the left operand. - - - '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[] - {} - - - - - - jsonb - integer - jsonb - - - Deletes the array element with specified index (negative - integers count from the end). Throws an error if JSON value - is not an array. - - - '["a", "b"]'::jsonb - 1 - ["a"] - - - - - - jsonb #- text[] - jsonb - - - Deletes the field or array element at the specified path, where path - elements can be either field keys or array indexes. - - - '["a", {"b":1}]'::jsonb #- '{1,b}' - ["a", {}] - - - - - - jsonb @? jsonpath - boolean - - - Does JSON path return any item for the specified JSON value? - (This is useful only with SQL-standard JSON path expressions, not - predicate check - expressions, since those always return a value.) - - - '{"a":[1,2,3,4,5]}'::jsonb @? '$.a[*] ? (@ > 2)' - t - - - - - - jsonb @@ jsonpath - boolean - - - Returns the result of a JSON path predicate check for the - specified JSON value. - (This is useful only - with predicate - check expressions, not SQL-standard JSON path expressions, - since it will return NULL if the path result is - not a single boolean value.) - - - '{"a":[1,2,3,4,5]}'::jsonb @@ '$.a[*] > 2' - t - - - - -
- - - - The jsonpath operators @? - and @@ suppress the following errors: missing object - field or array element, unexpected JSON item type, datetime and numeric - errors. The jsonpath-related functions described below can - also be told to suppress these types of errors. This behavior might be - helpful when searching JSON document collections of varying structure. - - - - - shows the functions that are - available for constructing json and jsonb values. - Some functions in this table have a RETURNING clause, - which specifies the data type returned. It must be one of json, - jsonb, bytea, a character string type (text, - char, or varchar), or a type - that can be cast to json. - By default, the json type is returned. - - - - JSON Creation Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - to_json - - to_json ( anyelement ) - json - - - - to_jsonb - - to_jsonb ( anyelement ) - jsonb - - - Converts any SQL value to json or jsonb. - Arrays and composites are converted recursively to arrays and - objects (multidimensional arrays become arrays of arrays in JSON). - Otherwise, if there is a cast from the SQL data type - to json, the cast function will be used to perform the - conversion; - - For example, the extension has a cast - from hstore to json, so that - hstore values converted via the JSON creation functions - will be represented as JSON objects, not as primitive string values. - - - otherwise, a scalar JSON value is produced. For any scalar other than - a number, a Boolean, or a null value, the text representation will be - used, with escaping as necessary to make it a valid JSON string value. - - - to_json('Fred said "Hi."'::text) - "Fred said \"Hi.\"" - - - to_jsonb(row(42, 'Fred said "Hi."'::text)) - {"f1": 42, "f2": "Fred said \"Hi.\""} - - - - - - - array_to_json - - array_to_json ( anyarray , boolean ) - json - - - Converts an SQL array to a JSON array. The behavior is the same - as to_json except that line feeds will be added - between top-level array elements if the optional boolean parameter is - true. - - - array_to_json('{{1,5},{99,100}}'::int[]) - [[1,5],[99,100]] - - - - - - - json_array - json_array ( - { value_expression FORMAT JSON } , ... - { NULL | ABSENT } ON NULL - RETURNING data_type FORMAT JSON ENCODING UTF8 ) - - - json_array ( - query_expression - RETURNING data_type FORMAT JSON ENCODING UTF8 ) - - - Constructs a JSON array from either a series of - value_expression parameters or from the results - of query_expression, - which must be a SELECT query returning a single column. If - ABSENT ON NULL is specified, NULL values are ignored. - This is always the case if a - query_expression is used. - - - json_array(1,true,json '{"a":null}') - [1, true, {"a":null}] - - - json_array(SELECT * FROM (VALUES(1),(2)) t) - [1, 2] - - - - - - - row_to_json - - row_to_json ( record , boolean ) - json - - - Converts an SQL composite value to a JSON object. The behavior is the - same as to_json except that line feeds will be - added between top-level elements if the optional boolean parameter is - true. - - - row_to_json(row(1,'foo')) - {"f1":1,"f2":"foo"} - - - - - - - json_build_array - - json_build_array ( VARIADIC "any" ) - json - - - - jsonb_build_array - - jsonb_build_array ( VARIADIC "any" ) - jsonb - - - Builds a possibly-heterogeneously-typed JSON array out of a variadic - argument list. Each argument is converted as - per to_json or to_jsonb. - - - json_build_array(1, 2, 'foo', 4, 5) - [1, 2, "foo", 4, 5] - - - - - - - json_build_object - - json_build_object ( VARIADIC "any" ) - json - - - - jsonb_build_object - - jsonb_build_object ( VARIADIC "any" ) - jsonb - - - Builds a JSON object out of a variadic argument list. By convention, - the argument list consists of alternating keys and values. Key - arguments are coerced to text; value arguments are converted as - per to_json or to_jsonb. - - - json_build_object('foo', 1, 2, row(3,'bar')) - {"foo" : 1, "2" : {"f1":3,"f2":"bar"}} - - - - - - json_object - json_object ( - { key_expression { VALUE | ':' } - value_expression FORMAT JSON ENCODING UTF8 }, ... - { NULL | ABSENT } ON NULL - { WITH | WITHOUT } UNIQUE KEYS - RETURNING data_type FORMAT JSON ENCODING UTF8 ) - - - Constructs a JSON object of all the key/value pairs given, - or an empty object if none are given. - key_expression is a scalar expression - defining the JSON key, which is - converted to the text type. - It cannot be NULL nor can it - belong to a type that has a cast to the json type. - If WITH UNIQUE KEYS is specified, there must not - be any duplicate key_expression. - Any pair for which the value_expression - evaluates to NULL is omitted from the output - if ABSENT ON NULL is specified; - if NULL ON NULL is specified or the clause - omitted, the key is included with value NULL. - - - json_object('code' VALUE 'P123', 'title': 'Jaws') - {"code" : "P123", "title" : "Jaws"} - - - - - - - json_object - - json_object ( text[] ) - json - - - - jsonb_object - - jsonb_object ( text[] ) - jsonb - - - Builds a JSON object out of a text array. The array must have either - exactly one dimension with an even number of members, in which case - they are taken as alternating key/value pairs, or two dimensions - such that each inner array has exactly two elements, which - are taken as a key/value pair. All values are converted to JSON - strings. - - - json_object('{a, 1, b, "def", c, 3.5}') - {"a" : "1", "b" : "def", "c" : "3.5"} - - json_object('{{a, 1}, {b, "def"}, {c, 3.5}}') - {"a" : "1", "b" : "def", "c" : "3.5"} - - - - - - json_object ( keys text[], values text[] ) - json - - - jsonb_object ( keys text[], values text[] ) - jsonb - - - This form of json_object takes keys and values - pairwise from separate text arrays. Otherwise it is identical to - the one-argument form. - - - json_object('{a,b}', '{1,2}') - {"a": "1", "b": "2"} - - - - - - json constructor - json ( - expression - FORMAT JSON ENCODING UTF8 - { WITH | WITHOUT } UNIQUE KEYS ) - json - - - Converts a given expression specified as text or - bytea string (in UTF8 encoding) into a JSON - value. If expression is NULL, an - SQL null value is returned. - If WITH UNIQUE is specified, the - expression must not contain any duplicate - object keys. - - - json('{"a":123, "b":[true,"foo"], "a":"bar"}') - {"a":123, "b":[true,"foo"], "a":"bar"} - - - - - - - json_scalar - json_scalar ( expression ) - - - Converts a given SQL scalar value into a JSON scalar value. - If the input is NULL, an SQL null is returned. If - the input is number or a boolean value, a corresponding JSON number - or boolean value is returned. For any other value, a JSON string is - returned. - - - json_scalar(123.45) - 123.45 - - - json_scalar(CURRENT_TIMESTAMP) - "2022-05-10T10:51:04.62128-04:00" - - - - - - json_serialize ( - expression FORMAT JSON ENCODING UTF8 - RETURNING data_type FORMAT JSON ENCODING UTF8 ) - - - Converts an SQL/JSON expression into a character or binary string. The - expression can be of any JSON type, any - character string type, or bytea in UTF8 encoding. - The returned type used in RETURNING can be any - character string type or bytea. The default is - text. - - - json_serialize('{ "a" : 1 } ' RETURNING bytea) - \x7b20226122203a2031207d20 - - - - -
- - - details SQL/JSON - facilities for testing JSON. - - - - SQL/JSON Testing Functions - - - - - Function signature - - - Description - - - Example(s) - - - - - - - IS JSON - expression IS NOT JSON - { VALUE | SCALAR | ARRAY | OBJECT } - { WITH | WITHOUT } UNIQUE KEYS - - - This predicate tests whether expression can be - parsed as JSON, possibly of a specified type. - If SCALAR or ARRAY or - OBJECT is specified, the - test is whether or not the JSON is of that particular type. If - WITH UNIQUE KEYS is specified, then any object in the - expression is also tested to see if it - has duplicate keys. - - - -SELECT js, - js IS JSON "json?", - js IS JSON SCALAR "scalar?", - js IS JSON OBJECT "object?", - js IS JSON ARRAY "array?" -FROM (VALUES - ('123'), ('"abc"'), ('{"a": "b"}'), ('[1,2]'),('abc')) foo(js); - js | json? | scalar? | object? | array? -------------+-------+---------+---------+-------- - 123 | t | t | f | f - "abc" | t | t | f | f - {"a": "b"} | t | f | t | f - [1,2] | t | f | f | t - abc | f | f | f | f - - - - -SELECT js, - js IS JSON OBJECT "object?", - js IS JSON ARRAY "array?", - js IS JSON ARRAY WITH UNIQUE KEYS "array w. UK?", - js IS JSON ARRAY WITHOUT UNIQUE KEYS "array w/o UK?" -FROM (VALUES ('[{"a":"1"}, - {"b":"2","b":"3"}]')) foo(js); --[ RECORD 1 ]-+-------------------- -js | [{"a":"1"}, + - | {"b":"2","b":"3"}] -object? | f -array? | t -array w. UK? | f -array w/o UK? | t - - - - - -
- - - shows the functions that - are available for processing json and jsonb values. - - - - JSON Processing Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - json_array_elements - - json_array_elements ( json ) - setof json - - - - jsonb_array_elements - - jsonb_array_elements ( jsonb ) - setof jsonb - - - Expands the top-level JSON array into a set of JSON values. - - - select * from json_array_elements('[1,true, [2,false]]') - - - value ------------ - 1 - true - [2,false] - - - - - - - - json_array_elements_text - - json_array_elements_text ( json ) - setof text - - - - jsonb_array_elements_text - - jsonb_array_elements_text ( jsonb ) - setof text - - - Expands the top-level JSON array into a set of text values. - - - select * from json_array_elements_text('["foo", "bar"]') - - - value ------------ - foo - bar - - - - - - - - json_array_length - - json_array_length ( json ) - integer - - - - jsonb_array_length - - jsonb_array_length ( jsonb ) - integer - - - Returns the number of elements in the top-level JSON array. - - - json_array_length('[1,2,3,{"f1":1,"f2":[5,6]},4]') - 5 - - - jsonb_array_length('[]') - 0 - - - - - - - json_each - - json_each ( json ) - setof record - ( key text, - value json ) - - - - jsonb_each - - jsonb_each ( jsonb ) - setof record - ( key text, - value jsonb ) - - - Expands the top-level JSON object into a set of key/value pairs. - - - select * from json_each('{"a":"foo", "b":"bar"}') - - - key | value ------+------- - a | "foo" - b | "bar" - - - - - - - - json_each_text - - json_each_text ( json ) - setof record - ( key text, - value text ) - - - - jsonb_each_text - - jsonb_each_text ( jsonb ) - setof record - ( key text, - value text ) - - - Expands the top-level JSON object into a set of key/value pairs. - The returned values will be of - type text. - - - select * from json_each_text('{"a":"foo", "b":"bar"}') - - - key | value ------+------- - a | foo - b | bar - - - - - - - - json_extract_path - - json_extract_path ( from_json json, VARIADIC path_elems text[] ) - json - - - - jsonb_extract_path - - jsonb_extract_path ( from_json jsonb, VARIADIC path_elems text[] ) - jsonb - - - Extracts JSON sub-object at the specified path. - (This is functionally equivalent to the #> - operator, but writing the path out as a variadic list can be more - convenient in some cases.) - - - json_extract_path('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}', 'f4', 'f6') - "foo" - - - - - - - json_extract_path_text - - json_extract_path_text ( from_json json, VARIADIC path_elems text[] ) - text - - - - jsonb_extract_path_text - - jsonb_extract_path_text ( from_json jsonb, VARIADIC path_elems text[] ) - text - - - Extracts JSON sub-object at the specified path as text. - (This is functionally equivalent to the #>> - operator.) - - - json_extract_path_text('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}', 'f4', 'f6') - foo - - - - - - - json_object_keys - - json_object_keys ( json ) - setof text - - - - jsonb_object_keys - - jsonb_object_keys ( jsonb ) - setof text - - - Returns the set of keys in the top-level JSON object. - - - select * from json_object_keys('{"f1":"abc","f2":{"f3":"a", "f4":"b"}}') - - - json_object_keys ------------------- - f1 - f2 - - - - - - - - json_populate_record - - json_populate_record ( base anyelement, from_json json ) - anyelement - - - - jsonb_populate_record - - jsonb_populate_record ( base anyelement, from_json jsonb ) - anyelement - - - Expands the top-level JSON object to a row having the composite type - of the base argument. The JSON object - is scanned for fields whose names match column names of the output row - type, and their values are inserted into those columns of the output. - (Fields that do not correspond to any output column name are ignored.) - In typical use, the value of base is just - NULL, which means that any output columns that do - not match any object field will be filled with nulls. However, - if base isn't NULL then - the values it contains will be used for unmatched columns. - - - To convert a JSON value to the SQL type of an output column, the - following rules are applied in sequence: - - - - A JSON null value is converted to an SQL null in all cases. - - - - - If the output column is of type json - or jsonb, the JSON value is just reproduced exactly. - - - - - If the output column is a composite (row) type, and the JSON value - is a JSON object, the fields of the object are converted to columns - of the output row type by recursive application of these rules. - - - - - Likewise, if the output column is an array type and the JSON value - is a JSON array, the elements of the JSON array are converted to - elements of the output array by recursive application of these - rules. - - - - - Otherwise, if the JSON value is a string, the contents of the - string are fed to the input conversion function for the column's - data type. - - - - - Otherwise, the ordinary text representation of the JSON value is - fed to the input conversion function for the column's data type. - - - - - - While the example below uses a constant JSON value, typical use would - be to reference a json or jsonb column - laterally from another table in the query's FROM - clause. Writing json_populate_record in - the FROM clause is good practice, since all of the - extracted columns are available for use without duplicate function - calls. - - - create type subrowtype as (d int, e text); - create type myrowtype as (a int, b text[], c subrowtype); - - - select * from json_populate_record(null::myrowtype, - '{"a": 1, "b": ["2", "a b"], "c": {"d": 4, "e": "a b c"}, "x": "foo"}') - - - a | b | c ----+-----------+------------- - 1 | {2,"a b"} | (4,"a b c") - - - - - - - - jsonb_populate_record_valid - - jsonb_populate_record_valid ( base anyelement, from_json json ) - boolean - - - Function for testing jsonb_populate_record. Returns - true if the input jsonb_populate_record - would finish without an error for the given input JSON object; that is, it's - valid input, false otherwise. - - - create type jsb_char2 as (a char(2)); - - - select jsonb_populate_record_valid(NULL::jsb_char2, '{"a": "aaa"}'); - - - jsonb_populate_record_valid ------------------------------ - f -(1 row) - - - select * from jsonb_populate_record(NULL::jsb_char2, '{"a": "aaa"}') q; - - -ERROR: value too long for type character(2) - - select jsonb_populate_record_valid(NULL::jsb_char2, '{"a": "aa"}'); - - - jsonb_populate_record_valid ------------------------------ - t -(1 row) - - - select * from jsonb_populate_record(NULL::jsb_char2, '{"a": "aa"}') q; - - - a ----- - aa -(1 row) - - - - - - - - json_populate_recordset - - json_populate_recordset ( base anyelement, from_json json ) - setof anyelement - - - - jsonb_populate_recordset - - jsonb_populate_recordset ( base anyelement, from_json jsonb ) - setof anyelement - - - Expands the top-level JSON array of objects to a set of rows having - the composite type of the base argument. - Each element of the JSON array is processed as described above - for json[b]_populate_record. - - - create type twoints as (a int, b int); - - - select * from json_populate_recordset(null::twoints, '[{"a":1,"b":2}, {"a":3,"b":4}]') - - - a | b ----+--- - 1 | 2 - 3 | 4 - - - - - - - - json_to_record - - json_to_record ( json ) - record - - - - jsonb_to_record - - jsonb_to_record ( jsonb ) - record - - - Expands the top-level JSON object to a row having the composite type - defined by an AS clause. (As with all functions - returning record, the calling query must explicitly - define the structure of the record with an AS - clause.) The output record is filled from fields of the JSON object, - in the same way as described above - for json[b]_populate_record. Since there is no - input record value, unmatched columns are always filled with nulls. - - - create type myrowtype as (a int, b text); - - - select * from json_to_record('{"a":1,"b":[1,2,3],"c":[1,2,3],"e":"bar","r": {"a": 123, "b": "a b c"}}') as x(a int, b text, c int[], d text, r myrowtype) - - - a | b | c | d | r ----+---------+---------+---+--------------- - 1 | [1,2,3] | {1,2,3} | | (123,"a b c") - - - - - - - - json_to_recordset - - json_to_recordset ( json ) - setof record - - - - jsonb_to_recordset - - jsonb_to_recordset ( jsonb ) - setof record - - - Expands the top-level JSON array of objects to a set of rows having - the composite type defined by an AS clause. (As - with all functions returning record, the calling query - must explicitly define the structure of the record with - an AS clause.) Each element of the JSON array is - processed as described above - for json[b]_populate_record. - - - select * from json_to_recordset('[{"a":1,"b":"foo"}, {"a":"2","c":"bar"}]') as x(a int, b text) - - - a | b ----+----- - 1 | foo - 2 | - - - - - - - - jsonb_set - - jsonb_set ( target jsonb, path text[], new_value jsonb , create_if_missing boolean ) - jsonb - - - Returns target - with the item designated by path - replaced by new_value, or with - new_value added if - create_if_missing is true (which is the - default) and the item designated by path - does not exist. - All earlier steps in the path must exist, or - the target is returned unchanged. - As with the path oriented operators, negative integers that - appear in the path count from the end - of JSON arrays. - If the last path step is an array index that is out of range, - and create_if_missing is true, the new - value is added at the beginning of the array if the index is negative, - or at the end of the array if it is positive. - - - jsonb_set('[{"f1":1,"f2":null},2,null,3]', '{0,f1}', '[2,3,4]', false) - [{"f1": [2, 3, 4], "f2": null}, 2, null, 3] - - - jsonb_set('[{"f1":1,"f2":null},2]', '{0,f3}', '[2,3,4]') - [{"f1": 1, "f2": null, "f3": [2, 3, 4]}, 2] - - - - - - - jsonb_set_lax - - jsonb_set_lax ( target jsonb, path text[], new_value jsonb , create_if_missing boolean , null_value_treatment text ) - jsonb - - - If new_value is not NULL, - behaves identically to jsonb_set. Otherwise behaves - according to the value - of null_value_treatment which must be one - of 'raise_exception', - 'use_json_null', 'delete_key', or - 'return_target'. The default is - 'use_json_null'. - - - jsonb_set_lax('[{"f1":1,"f2":null},2,null,3]', '{0,f1}', null) - [{"f1": null, "f2": null}, 2, null, 3] - - - jsonb_set_lax('[{"f1":99,"f2":null},2]', '{0,f3}', null, true, 'return_target') - [{"f1": 99, "f2": null}, 2] - - - - - - - jsonb_insert - - jsonb_insert ( target jsonb, path text[], new_value jsonb , insert_after boolean ) - jsonb - - - Returns target - with new_value inserted. If the item - designated by the path is an array - element, new_value will be inserted before - that item if insert_after is false (which - is the default), or after it - if insert_after is true. If the item - designated by the path is an object - field, new_value will be inserted only if - the object does not already contain that key. - All earlier steps in the path must exist, or - the target is returned unchanged. - As with the path oriented operators, negative integers that - appear in the path count from the end - of JSON arrays. - If the last path step is an array index that is out of range, the new - value is added at the beginning of the array if the index is negative, - or at the end of the array if it is positive. - - - jsonb_insert('{"a": [0,1,2]}', '{a, 1}', '"new_value"') - {"a": [0, "new_value", 1, 2]} - - - jsonb_insert('{"a": [0,1,2]}', '{a, 1}', '"new_value"', true) - {"a": [0, 1, "new_value", 2]} - - - - - - - json_strip_nulls - - json_strip_nulls ( target json ,strip_in_arrays boolean ) - json - - - - jsonb_strip_nulls - - jsonb_strip_nulls ( target jsonb ,strip_in_arrays boolean ) - jsonb - - - Deletes all object fields that have null values from the given JSON - value, recursively. - If strip_in_arrays is true (the default is false), - null array elements are also stripped. - Otherwise they are not stripped. Bare null values are never stripped. - - - json_strip_nulls('[{"f1":1, "f2":null}, 2, null, 3]') - [{"f1":1},2,null,3] - - - jsonb_strip_nulls('[1,2,null,3,4]', true); - [1,2,3,4] - - - - - - - - jsonb_path_exists - - jsonb_path_exists ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - boolean - - - Checks whether the JSON path returns any item for the specified JSON - value. - (This is useful only with SQL-standard JSON path expressions, not - predicate check - expressions, since those always return a value.) - If the vars argument is specified, it must - be a JSON object, and its fields provide named values to be - substituted into the jsonpath expression. - If the silent argument is specified and - is true, the function suppresses the same errors - as the @? and @@ operators do. - - - jsonb_path_exists('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') - t - - - - - - - jsonb_path_match - - jsonb_path_match ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - boolean - - - Returns the SQL boolean result of a JSON path predicate check - for the specified JSON value. - (This is useful only - with predicate - check expressions, not SQL-standard JSON path expressions, - since it will either fail or return NULL if the - path result is not a single boolean value.) - The optional vars - and silent arguments act the same as - for jsonb_path_exists. - - - jsonb_path_match('{"a":[1,2,3,4,5]}', 'exists($.a[*] ? (@ >= $min && @ <= $max))', '{"min":2, "max":4}') - t - - - - - - - jsonb_path_query - - jsonb_path_query ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - setof jsonb - - - Returns all JSON items returned by the JSON path for the specified - JSON value. - For SQL-standard JSON path expressions it returns the JSON - values selected from target. - For predicate - check expressions it returns the result of the predicate - check: true, false, - or null. - The optional vars - and silent arguments act the same as - for jsonb_path_exists. - - - select * from jsonb_path_query('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') - - - jsonb_path_query ------------------- - 2 - 3 - 4 - - - - - - - - jsonb_path_query_array - - jsonb_path_query_array ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - jsonb - - - Returns all JSON items returned by the JSON path for the specified - JSON value, as a JSON array. - The parameters are the same as - for jsonb_path_query. - - - jsonb_path_query_array('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') - [2, 3, 4] - - - - - - - jsonb_path_query_first - - jsonb_path_query_first ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - jsonb - - - Returns the first JSON item returned by the JSON path for the - specified JSON value, or NULL if there are no - results. - The parameters are the same as - for jsonb_path_query. - - - jsonb_path_query_first('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') - 2 - - - - - - - jsonb_path_exists_tz - - jsonb_path_exists_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - boolean - - - - jsonb_path_match_tz - - jsonb_path_match_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - boolean - - - - jsonb_path_query_tz - - jsonb_path_query_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - setof jsonb - - - - jsonb_path_query_array_tz - - jsonb_path_query_array_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - jsonb - - - - jsonb_path_query_first_tz - - jsonb_path_query_first_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) - jsonb - - - These functions act like their counterparts described above without - the _tz suffix, except that these functions support - comparisons of date/time values that require timezone-aware - conversions. The example below requires interpretation of the - date-only value 2015-08-02 as a timestamp with time - zone, so the result depends on the current - setting. Due to this dependency, these - functions are marked as stable, which means these functions cannot be - used in indexes. Their counterparts are immutable, and so can be used - in indexes; but they will throw errors if asked to make such - comparisons. - - - jsonb_path_exists_tz('["2015-08-01 12:00:00-05"]', '$[*] ? (@.datetime() < "2015-08-02".datetime())') - t - - - - - - - jsonb_pretty - - jsonb_pretty ( jsonb ) - text - - - Converts the given JSON value to pretty-printed, indented text. - - - jsonb_pretty('[{"f1":1,"f2":null}, 2]') - - -[ - { - "f1": 1, - "f2": null - }, - 2 -] - - - - - - - - json_typeof - - json_typeof ( json ) - text - - - - jsonb_typeof - - jsonb_typeof ( jsonb ) - text - - - Returns the type of the top-level JSON value as a text string. - Possible types are - object, array, - string, number, - boolean, and null. - (The null result should not be confused - with an SQL NULL; see the examples.) - - - json_typeof('-123.4') - number - - - json_typeof('null'::json) - null - - - json_typeof(NULL::json) IS NULL - t - - - - -
-
- - - The SQL/JSON Path Language - - - SQL/JSON path language - - - - SQL/JSON path expressions specify item(s) to be retrieved - from a JSON value, similarly to XPath expressions used - for access to XML content. In PostgreSQL, - path expressions are implemented as the jsonpath - data type and can use any elements described in - . - - - - JSON query functions and operators - pass the provided path expression to the path engine - for evaluation. If the expression matches the queried JSON data, - the corresponding JSON item, or set of items, is returned. - If there is no match, the result will be NULL, - false, or an error, depending on the function. - Path expressions are written in the SQL/JSON path language - and can include arithmetic expressions and functions. - - - - A path expression consists of a sequence of elements allowed - by the jsonpath data type. - The path expression is normally evaluated from left to right, but - you can use parentheses to change the order of operations. - If the evaluation is successful, a sequence of JSON items is produced, - and the evaluation result is returned to the JSON query function - that completes the specified computation. - - - - To refer to the JSON value being queried (the - context item), use the $ variable - in the path expression. The first element of a path must always - be $. It can be followed by one or more - accessor operators, - which go down the JSON structure level by level to retrieve sub-items - of the context item. Each accessor operator acts on the - result(s) of the previous evaluation step, producing zero, one, or more - output items from each input item. - - - - For example, suppose you have some JSON data from a GPS tracker that you - would like to parse, such as: - -SELECT '{ - "track": { - "segments": [ - { - "location": [ 47.763, 13.4034 ], - "start time": "2018-10-14 10:05:14", - "HR": 73 - }, - { - "location": [ 47.706, 13.2635 ], - "start time": "2018-10-14 10:39:21", - "HR": 135 - } - ] - } -}' AS json \gset - - (The above example can be copied-and-pasted - into psql to set things up for the following - examples. Then psql will - expand :'json' into a suitably-quoted string - constant containing the JSON value.) - - - - To retrieve the available track segments, you need to use the - .key accessor - operator to descend through surrounding JSON objects, for example: - -=> select jsonb_path_query(:'json', '$.track.segments'); - jsonb_path_query ------------------------------------------------------------&zwsp;-----------------------------------------------------------&zwsp;--------------------------------------------- - [{"HR": 73, "location": [47.763, 13.4034], "start time": "2018-10-14 10:05:14"}, {"HR": 135, "location": [47.706, 13.2635], "start time": "2018-10-14 10:39:21"}] - - - - - To retrieve the contents of an array, you typically use the - [*] operator. - The following example will return the location coordinates for all - the available track segments: - -=> select jsonb_path_query(:'json', '$.track.segments[*].location'); - jsonb_path_query -------------------- - [47.763, 13.4034] - [47.706, 13.2635] - - Here we started with the whole JSON input value ($), - then the .track accessor selected the JSON object - associated with the "track" object key, then - the .segments accessor selected the JSON array - associated with the "segments" key within that - object, then the [*] accessor selected each element - of that array (producing a series of items), then - the .location accessor selected the JSON array - associated with the "location" key within each of - those objects. In this example, each of those objects had - a "location" key; but if any of them did not, - the .location accessor would have simply produced no - output for that input item. - - - - To return the coordinates of the first segment only, you can - specify the corresponding subscript in the [] - accessor operator. Recall that JSON array indexes are 0-relative: - -=> select jsonb_path_query(:'json', '$.track.segments[0].location'); - jsonb_path_query -------------------- - [47.763, 13.4034] - - - - - The result of each path evaluation step can be processed - by one or more of the jsonpath operators and methods - listed in . - Each method name must be preceded by a dot. For example, - you can get the size of an array: - -=> select jsonb_path_query(:'json', '$.track.segments.size()'); - jsonb_path_query ------------------- - 2 - - More examples of using jsonpath operators - and methods within path expressions appear below in - . - - - - A path can also contain - filter expressions that work similarly to the - WHERE clause in SQL. A filter expression begins with - a question mark and provides a condition in parentheses: - - -? (condition) - - - - - Filter expressions must be written just after the path evaluation step - to which they should apply. The result of that step is filtered to include - only those items that satisfy the provided condition. SQL/JSON defines - three-valued logic, so the condition can - produce true, false, - or unknown. The unknown value - plays the same role as SQL NULL and can be tested - for with the is unknown predicate. Further path - evaluation steps use only those items for which the filter expression - returned true. - - - - The functions and operators that can be used in filter expressions are - listed in . Within a - filter expression, the @ variable denotes the value - being considered (i.e., one result of the preceding path step). You can - write accessor operators after @ to retrieve component - items. - - - - For example, suppose you would like to retrieve all heart rate values higher - than 130. You can achieve this as follows: - -=> select jsonb_path_query(:'json', '$.track.segments[*].HR ? (@ > 130)'); - jsonb_path_query ------------------- - 135 - - - - - To get the start times of segments with such values, you have to - filter out irrelevant segments before selecting the start times, so the - filter expression is applied to the previous step, and the path used - in the condition is different: - -=> select jsonb_path_query(:'json', '$.track.segments[*] ? (@.HR > 130)."start time"'); - jsonb_path_query ------------------------ - "2018-10-14 10:39:21" - - - - - You can use several filter expressions in sequence, if required. - The following example selects start times of all segments that - contain locations with relevant coordinates and high heart rate values: - -=> select jsonb_path_query(:'json', '$.track.segments[*] ? (@.location[1] < 13.4) ? (@.HR > 130)."start time"'); - jsonb_path_query ------------------------ - "2018-10-14 10:39:21" - - - - - Using filter expressions at different nesting levels is also allowed. - The following example first filters all segments by location, and then - returns high heart rate values for these segments, if available: - -=> select jsonb_path_query(:'json', '$.track.segments[*] ? (@.location[1] < 13.4).HR ? (@ > 130)'); - jsonb_path_query ------------------- - 135 - - - - - You can also nest filter expressions within each other. - This example returns the size of the track if it contains any - segments with high heart rate values, or an empty sequence otherwise: - -=> select jsonb_path_query(:'json', '$.track ? (exists(@.segments[*] ? (@.HR > 130))).segments.size()'); - jsonb_path_query ------------------- - 2 - - - - - Deviations from the SQL Standard - - PostgreSQL's implementation of the SQL/JSON path - language has the following deviations from the SQL/JSON standard. - - - - Boolean Predicate Check Expressions - - As an extension to the SQL standard, - a PostgreSQL path expression can be a - Boolean predicate, whereas the SQL standard allows predicates only within - filters. While SQL-standard path expressions return the relevant - element(s) of the queried JSON value, predicate check expressions - return the single three-valued jsonb result of the - predicate: true, - false, or null. - For example, we could write this SQL-standard filter expression: - -=> select jsonb_path_query(:'json', '$.track.segments ?(@[*].HR > 130)'); - jsonb_path_query ------------------------------------------------------------&zwsp;---------------------- - {"HR": 135, "location": [47.706, 13.2635], "start time": "2018-10-14 10:39:21"} - - The similar predicate check expression simply - returns true, indicating that a match exists: - -=> select jsonb_path_query(:'json', '$.track.segments[*].HR > 130'); - jsonb_path_query ------------------- - true - - - - - - Predicate check expressions are required in the - @@ operator (and the - jsonb_path_match function), and should not be used - with the @? operator (or the - jsonb_path_exists function). - - - - - - Regular Expression Interpretation - - There are minor differences in the interpretation of regular - expression patterns used in like_regex filters, as - described in . - - - - - - Strict and Lax Modes - - When you query JSON data, the path expression may not match the - actual JSON data structure. An attempt to access a non-existent - member of an object or element of an array is defined as a - structural error. SQL/JSON path expressions have two modes - of handling structural errors: - - - - - - lax (default) — the path engine implicitly adapts - the queried data to the specified path. - Any structural errors that cannot be fixed as described below - are suppressed, producing no match. - - - - - strict — if a structural error occurs, an error is raised. - - - - - - Lax mode facilitates matching of a JSON document and path - expression when the JSON data does not conform to the expected schema. - If an operand does not match the requirements of a particular operation, - it can be automatically wrapped as an SQL/JSON array, or unwrapped by - converting its elements into an SQL/JSON sequence before performing - the operation. Also, comparison operators automatically unwrap their - operands in lax mode, so you can compare SQL/JSON arrays - out-of-the-box. An array of size 1 is considered equal to its sole element. - Automatic unwrapping is not performed when: - - - - The path expression contains type() or - size() methods that return the type - and the number of elements in the array, respectively. - - - - - The queried JSON data contain nested arrays. In this case, only - the outermost array is unwrapped, while all the inner arrays - remain unchanged. Thus, implicit unwrapping can only go one - level down within each path evaluation step. - - - - - - - For example, when querying the GPS data listed above, you can - abstract from the fact that it stores an array of segments - when using lax mode: - -=> select jsonb_path_query(:'json', 'lax $.track.segments.location'); - jsonb_path_query -------------------- - [47.763, 13.4034] - [47.706, 13.2635] - - - - - In strict mode, the specified path must exactly match the structure of - the queried JSON document, so using this path - expression will cause an error: - -=> select jsonb_path_query(:'json', 'strict $.track.segments.location'); -ERROR: jsonpath member accessor can only be applied to an object - - To get the same result as in lax mode, you have to explicitly unwrap the - segments array: - -=> select jsonb_path_query(:'json', 'strict $.track.segments[*].location'); - jsonb_path_query -------------------- - [47.763, 13.4034] - [47.706, 13.2635] - - - - - The unwrapping behavior of lax mode can lead to surprising results. For - instance, the following query using the .** accessor - selects every HR value twice: - -=> select jsonb_path_query(:'json', 'lax $.**.HR'); - jsonb_path_query ------------------- - 73 - 135 - 73 - 135 - - This happens because the .** accessor selects both - the segments array and each of its elements, while - the .HR accessor automatically unwraps arrays when - using lax mode. To avoid surprising results, we recommend using - the .** accessor only in strict mode. The - following query selects each HR value just once: - -=> select jsonb_path_query(:'json', 'strict $.**.HR'); - jsonb_path_query ------------------- - 73 - 135 - - - - - The unwrapping of arrays can also lead to unexpected results. Consider this - example, which selects all the location arrays: - -=> select jsonb_path_query(:'json', 'lax $.track.segments[*].location'); - jsonb_path_query -------------------- - [47.763, 13.4034] - [47.706, 13.2635] -(2 rows) - - As expected it returns the full arrays. But applying a filter expression - causes the arrays to be unwrapped to evaluate each item, returning only the - items that match the expression: - -=> select jsonb_path_query(:'json', 'lax $.track.segments[*].location ?(@[*] > 15)'); - jsonb_path_query ------------------- - 47.763 - 47.706 -(2 rows) - - This despite the fact that the full arrays are selected by the path - expression. Use strict mode to restore selecting the arrays: - -=> select jsonb_path_query(:'json', 'strict $.track.segments[*].location ?(@[*] > 15)'); - jsonb_path_query -------------------- - [47.763, 13.4034] - [47.706, 13.2635] -(2 rows) - - - - - - SQL/JSON Path Operators and Methods - - - shows the operators and - methods available in jsonpath. Note that while the unary - operators and methods can be applied to multiple values resulting from a - preceding path step, the binary operators (addition etc.) can only be - applied to single values. In lax mode, methods applied to an array will be - executed for each value in the array. The exceptions are - .type() and .size(), which apply to - the array itself. - - - - <type>jsonpath</type> Operators and Methods - - - - - Operator/Method - - - Description - - - Example(s) - - - - - - - - number + number - number - - - Addition - - - jsonb_path_query('[2]', '$[0] + 3') - 5 - - - - - - + number - number - - - Unary plus (no operation); unlike addition, this can iterate over - multiple values - - - jsonb_path_query_array('{"x": [2,3,4]}', '+ $.x') - [2, 3, 4] - - - - - - number - number - number - - - Subtraction - - - jsonb_path_query('[2]', '7 - $[0]') - 5 - - - - - - - number - number - - - Negation; unlike subtraction, this can iterate over - multiple values - - - jsonb_path_query_array('{"x": [2,3,4]}', '- $.x') - [-2, -3, -4] - - - - - - number * number - number - - - Multiplication - - - jsonb_path_query('[4]', '2 * $[0]') - 8 - - - - - - number / number - number - - - Division - - - jsonb_path_query('[8.5]', '$[0] / 2') - 4.2500000000000000 - - - - - - number % number - number - - - Modulo (remainder) - - - jsonb_path_query('[32]', '$[0] % 10') - 2 - - - - - - value . type() - string - - - Type of the JSON item (see json_typeof) - - - jsonb_path_query_array('[1, "2", {}]', '$[*].type()') - ["number", "string", "object"] - - - - - - value . size() - number - - - Size of the JSON item (number of array elements, or 1 if not an - array) - - - jsonb_path_query('{"m": [11, 15]}', '$.m.size()') - 2 - - - - - - value . boolean() - boolean - - - Boolean value converted from a JSON boolean, number, or string - - - jsonb_path_query_array('[1, "yes", false]', '$[*].boolean()') - [true, true, false] - - - - - - value . string() - string - - - String value converted from a JSON boolean, number, string, or - datetime - - - jsonb_path_query_array('[1.23, "xyz", false]', '$[*].string()') - ["1.23", "xyz", "false"] - - - jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp().string()') - "2023-08-15T12:34:56" - - - - - - value . double() - number - - - Approximate floating-point number converted from a JSON number or - string - - - jsonb_path_query('{"len": "1.9"}', '$.len.double() * 2') - 3.8 - - - - - - number . ceiling() - number - - - Nearest integer greater than or equal to the given number - - - jsonb_path_query('{"h": 1.3}', '$.h.ceiling()') - 2 - - - - - - number . floor() - number - - - Nearest integer less than or equal to the given number - - - jsonb_path_query('{"h": 1.7}', '$.h.floor()') - 1 - - - - - - number . abs() - number - - - Absolute value of the given number - - - jsonb_path_query('{"z": -0.3}', '$.z.abs()') - 0.3 - - - - - - value . bigint() - bigint - - - Big integer value converted from a JSON number or string - - - jsonb_path_query('{"len": "9876543219"}', '$.len.bigint()') - 9876543219 - - - - - - value . decimal( [ precision [ , scale ] ] ) - decimal - - - Rounded decimal value converted from a JSON number or string - (precision and scale must be - integer values) - - - jsonb_path_query('1234.5678', '$.decimal(6, 2)') - 1234.57 - - - - - - value . integer() - integer - - - Integer value converted from a JSON number or string - - - jsonb_path_query('{"len": "12345"}', '$.len.integer()') - 12345 - - - - - - value . number() - numeric - - - Numeric value converted from a JSON number or string - - - jsonb_path_query('{"len": "123.45"}', '$.len.number()') - 123.45 - - - - - - string . datetime() - datetime_type - (see note) - - - Date/time value converted from a string - - - jsonb_path_query('["2015-8-1", "2015-08-12"]', '$[*] ? (@.datetime() < "2015-08-2".datetime())') - "2015-8-1" - - - - - - string . datetime(template) - datetime_type - (see note) - - - Date/time value converted from a string using the - specified to_timestamp template - - - jsonb_path_query_array('["12:30", "18:40"]', '$[*].datetime("HH24:MI")') - ["12:30:00", "18:40:00"] - - - - - - string . date() - date - - - Date value converted from a string - - - jsonb_path_query('"2023-08-15"', '$.date()') - "2023-08-15" - - - - - - string . time() - time without time zone - - - Time without time zone value converted from a string - - - jsonb_path_query('"12:34:56"', '$.time()') - "12:34:56" - - - - - - string . time(precision) - time without time zone - - - Time without time zone value converted from a string, with fractional - seconds adjusted to the given precision - - - jsonb_path_query('"12:34:56.789"', '$.time(2)') - "12:34:56.79" - - - - - - string . time_tz() - time with time zone - - - Time with time zone value converted from a string - - - jsonb_path_query('"12:34:56 +05:30"', '$.time_tz()') - "12:34:56+05:30" - - - - - - string . time_tz(precision) - time with time zone - - - Time with time zone value converted from a string, with fractional - seconds adjusted to the given precision - - - jsonb_path_query('"12:34:56.789 +05:30"', '$.time_tz(2)') - "12:34:56.79+05:30" - - - - - - string . timestamp() - timestamp without time zone - - - Timestamp without time zone value converted from a string - - - jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp()') - "2023-08-15T12:34:56" - - - - - - string . timestamp(precision) - timestamp without time zone - - - Timestamp without time zone value converted from a string, with - fractional seconds adjusted to the given precision - - - jsonb_path_query('"2023-08-15 12:34:56.789"', '$.timestamp(2)') - "2023-08-15T12:34:56.79" - - - - - - string . timestamp_tz() - timestamp with time zone - - - Timestamp with time zone value converted from a string - - - jsonb_path_query('"2023-08-15 12:34:56 +05:30"', '$.timestamp_tz()') - "2023-08-15T12:34:56+05:30" - - - - - - string . timestamp_tz(precision) - timestamp with time zone - - - Timestamp with time zone value converted from a string, with fractional - seconds adjusted to the given precision - - - jsonb_path_query('"2023-08-15 12:34:56.789 +05:30"', '$.timestamp_tz(2)') - "2023-08-15T12:34:56.79+05:30" - - - - - - object . keyvalue() - array - - - The object's key-value pairs, represented as an array of objects - containing three fields: "key", - "value", and "id"; - "id" is a unique identifier of the object the - key-value pair belongs to - - - jsonb_path_query_array('{"x": "20", "y": 32}', '$.keyvalue()') - [{"id": 0, "key": "x", "value": "20"}, {"id": 0, "key": "y", "value": 32}] - - - - -
- - - - The result type of the datetime() and - datetime(template) - methods can be date, timetz, time, - timestamptz, or timestamp. - Both methods determine their result type dynamically. - - - The datetime() method sequentially tries to - match its input string to the ISO formats - for date, timetz, time, - timestamptz, and timestamp. It stops on - the first matching format and emits the corresponding data type. - - - The datetime(template) - method determines the result type according to the fields used in the - provided template string. - - - The datetime() and - datetime(template) methods - use the same parsing rules as the to_timestamp SQL - function does (see ), with three - exceptions. First, these methods don't allow unmatched template - patterns. Second, only the following separators are allowed in the - template string: minus sign, period, solidus (slash), comma, apostrophe, - semicolon, colon and space. Third, separators in the template string - must exactly match the input string. - - - If different date/time types need to be compared, an implicit cast is - applied. A date value can be cast to timestamp - or timestamptz, timestamp can be cast to - timestamptz, and time to timetz. - However, all but the first of these conversions depend on the current - setting, and thus can only be performed - within timezone-aware jsonpath functions. Similarly, other - date/time-related methods that convert strings to date/time types - also do this casting, which may involve the current - setting. Therefore, these conversions can - also only be performed within timezone-aware jsonpath - functions. - - - - - shows the available - filter expression elements. - - - - <type>jsonpath</type> Filter Expression Elements - - - - - Predicate/Value - - - Description - - - Example(s) - - - - - - - - value == value - boolean - - - Equality comparison (this, and the other comparison operators, work on - all JSON scalar values) - - - jsonb_path_query_array('[1, "a", 1, 3]', '$[*] ? (@ == 1)') - [1, 1] - - - jsonb_path_query_array('[1, "a", 1, 3]', '$[*] ? (@ == "a")') - ["a"] - - - - - - value != value - boolean - - - value <> value - boolean - - - Non-equality comparison - - - jsonb_path_query_array('[1, 2, 1, 3]', '$[*] ? (@ != 1)') - [2, 3] - - - jsonb_path_query_array('["a", "b", "c"]', '$[*] ? (@ <> "b")') - ["a", "c"] - - - - - - value < value - boolean - - - Less-than comparison - - - jsonb_path_query_array('[1, 2, 3]', '$[*] ? (@ < 2)') - [1] - - - - - - value <= value - boolean - - - Less-than-or-equal-to comparison - - - jsonb_path_query_array('["a", "b", "c"]', '$[*] ? (@ <= "b")') - ["a", "b"] - - - - - - value > value - boolean - - - Greater-than comparison - - - jsonb_path_query_array('[1, 2, 3]', '$[*] ? (@ > 2)') - [3] - - - - - - value >= value - boolean - - - Greater-than-or-equal-to comparison - - - jsonb_path_query_array('[1, 2, 3]', '$[*] ? (@ >= 2)') - [2, 3] - - - - - - true - boolean - - - JSON constant true - - - jsonb_path_query('[{"name": "John", "parent": false}, {"name": "Chris", "parent": true}]', '$[*] ? (@.parent == true)') - {"name": "Chris", "parent": true} - - - - - - false - boolean - - - JSON constant false - - - jsonb_path_query('[{"name": "John", "parent": false}, {"name": "Chris", "parent": true}]', '$[*] ? (@.parent == false)') - {"name": "John", "parent": false} - - - - - - null - value - - - JSON constant null (note that, unlike in SQL, - comparison to null works normally) - - - jsonb_path_query('[{"name": "Mary", "job": null}, {"name": "Michael", "job": "driver"}]', '$[*] ? (@.job == null) .name') - "Mary" - - - - - - boolean && boolean - boolean - - - Boolean AND - - - jsonb_path_query('[1, 3, 7]', '$[*] ? (@ > 1 && @ < 5)') - 3 - - - - - - boolean || boolean - boolean - - - Boolean OR - - - jsonb_path_query('[1, 3, 7]', '$[*] ? (@ < 1 || @ > 5)') - 7 - - - - - - ! boolean - boolean - - - Boolean NOT - - - jsonb_path_query('[1, 3, 7]', '$[*] ? (!(@ < 5))') - 7 - - - - - - boolean is unknown - boolean - - - Tests whether a Boolean condition is unknown. - - - jsonb_path_query('[-1, 2, 7, "foo"]', '$[*] ? ((@ > 0) is unknown)') - "foo" - - - - - - string like_regex string flag string - boolean - - - Tests whether the first operand matches the regular expression - given by the second operand, optionally with modifications - described by a string of flag characters (see - ). - - - jsonb_path_query_array('["abc", "abd", "aBdC", "abdacb", "babc"]', '$[*] ? (@ like_regex "^ab.*c")') - ["abc", "abdacb"] - - - jsonb_path_query_array('["abc", "abd", "aBdC", "abdacb", "babc"]', '$[*] ? (@ like_regex "^ab.*c" flag "i")') - ["abc", "aBdC", "abdacb"] - - - - - - string starts with string - boolean - - - Tests whether the second operand is an initial substring of the first - operand. - - - jsonb_path_query('["John Smith", "Mary Stone", "Bob Johnson"]', '$[*] ? (@ starts with "John")') - "John Smith" - - - - - - exists ( path_expression ) - boolean - - - Tests whether a path expression matches at least one SQL/JSON item. - Returns unknown if the path expression would result - in an error; the second example uses this to avoid a no-such-key error - in strict mode. - - - jsonb_path_query('{"x": [1, 2], "y": [2, 4]}', 'strict $.* ? (exists (@ ? (@[*] > 2)))') - [2, 4] - - - jsonb_path_query_array('{"value": 41}', 'strict $ ? (exists (@.name)) .name') - [] - - - - -
- -
- - - SQL/JSON Regular Expressions - - - LIKE_REGEX - in SQL/JSON - - - - SQL/JSON path expressions allow matching text to a regular expression - with the like_regex filter. For example, the - following SQL/JSON path query would case-insensitively match all - strings in an array that start with an English vowel: - -$[*] ? (@ like_regex "^[aeiou]" flag "i") - - - - - The optional flag string may include one or more of - the characters - i for case-insensitive match, - m to allow ^ - and $ to match at newlines, - s to allow . to match a newline, - and q to quote the whole pattern (reducing the - behavior to a simple substring match). - - - - The SQL/JSON standard borrows its definition for regular expressions - from the LIKE_REGEX operator, which in turn uses the - XQuery standard. PostgreSQL does not currently support the - LIKE_REGEX operator. Therefore, - the like_regex filter is implemented using the - POSIX regular expression engine described in - . This leads to various minor - discrepancies from standard SQL/JSON behavior, which are cataloged in - . - Note, however, that the flag-letter incompatibilities described there - do not apply to SQL/JSON, as it translates the XQuery flag letters to - match what the POSIX engine expects. - - - - Keep in mind that the pattern argument of like_regex - is a JSON path string literal, written according to the rules given in - . This means in particular that any - backslashes you want to use in the regular expression must be doubled. - For example, to match string values of the root document that contain - only digits: - -$.* ? (@ like_regex "^\\d+$") - - - -
- - - SQL/JSON Query Functions - - SQL/JSON functions JSON_EXISTS(), - JSON_QUERY(), and JSON_VALUE() - described in can be used - to query JSON documents. Each of these functions apply a - path_expression (an SQL/JSON path query) to a - context_item (the document). See - for more details on what - the path_expression can contain. The - path_expression can also reference variables, - whose values are specified with their respective names in the - PASSING clause that is supported by each function. - context_item can be a jsonb value - or a character string that can be successfully cast to jsonb. - - - - SQL/JSON Query Functions - - - - - Function signature - - - Description - - - Example(s) - - - - - - - json_exists - -JSON_EXISTS ( -context_item, path_expression - PASSING { value AS varname } , ... -{ TRUE | FALSE | UNKNOWN | ERROR } ON ERROR ) boolean - - - - - - Returns true if the SQL/JSON path_expression - applied to the context_item yields any - items, false otherwise. - - - - - The ON ERROR clause specifies the behavior if - an error occurs during path_expression - evaluation. Specifying ERROR will cause an error to - be thrown with the appropriate message. Other options include - returning boolean values FALSE or - TRUE or the value UNKNOWN which - is actually an SQL NULL. The default when no ON ERROR - clause is specified is to return the boolean value - FALSE. - - - - - Examples: - - - JSON_EXISTS(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > $x)' PASSING 2 AS x) - t - - - JSON_EXISTS(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR) - f - - - JSON_EXISTS(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR) - - -ERROR: jsonpath array subscript is out of bounds - - - - - - json_query - -JSON_QUERY ( -context_item, path_expression - PASSING { value AS varname } , ... - RETURNING data_type FORMAT JSON ENCODING UTF8 - { WITHOUT | WITH { CONDITIONAL | UNCONDITIONAL } } ARRAY WRAPPER - { KEEP | OMIT } QUOTES ON SCALAR STRING - { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON EMPTY - { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON ERROR ) jsonb - - - - - - Returns the result of applying the SQL/JSON - path_expression to the - context_item. - - - - - By default, the result is returned as a value of type jsonb, - though the RETURNING clause can be used to return - as some other type to which it can be successfully coerced. - - - - - If the path expression may return multiple values, it might be necessary - to wrap those values using the WITH WRAPPER clause to - make it a valid JSON string, because the default behavior is to not wrap - them, as if WITHOUT WRAPPER were specified. The - WITH WRAPPER clause is by default taken to mean - WITH UNCONDITIONAL WRAPPER, which means that even a - single result value will be wrapped. To apply the wrapper only when - multiple values are present, specify WITH CONDITIONAL WRAPPER. - Getting multiple values in result will be treated as an error if - WITHOUT WRAPPER is specified. - - - - - If the result is a scalar string, by default, the returned value will - be surrounded by quotes, making it a valid JSON value. It can be made - explicit by specifying KEEP QUOTES. Conversely, - quotes can be omitted by specifying OMIT QUOTES. - To ensure that the result is a valid JSON value, OMIT QUOTES - cannot be specified when WITH WRAPPER is also - specified. - - - - - The ON EMPTY clause specifies the behavior if - evaluating path_expression yields an empty - set. The ON ERROR clause specifies the behavior - if an error occurs when evaluating path_expression, - when coercing the result value to the RETURNING type, - or when evaluating the ON EMPTY expression if the - path_expression evaluation returns an empty - set. - - - - - For both ON EMPTY and ON ERROR, - specifying ERROR will cause an error to be thrown with - the appropriate message. Other options include returning an SQL NULL, an - empty array (EMPTY ARRAY), - an empty object (EMPTY OBJECT), or a user-specified - expression (DEFAULT expression) - that can be coerced to jsonb or the type specified in RETURNING. - The default when ON EMPTY or ON ERROR - is not specified is to return an SQL NULL value. - - - - - Examples: - - - JSON_QUERY(jsonb '[1,[2,3],null]', 'lax $[*][$off]' PASSING 1 AS off WITH CONDITIONAL WRAPPER) - 3 - - - JSON_QUERY(jsonb '{"a": "[1, 2]"}', 'lax $.a' OMIT QUOTES) - [1, 2] - - - JSON_QUERY(jsonb '{"a": "[1, 2]"}', 'lax $.a' RETURNING int[] OMIT QUOTES ERROR ON ERROR) - - -ERROR: malformed array literal: "[1, 2]" -DETAIL: Missing "]" after array dimensions. - - - - - - - json_value - -JSON_VALUE ( -context_item, path_expression - PASSING { value AS varname } , ... - RETURNING data_type - { ERROR | NULL | DEFAULT expression } ON EMPTY - { ERROR | NULL | DEFAULT expression } ON ERROR ) text - - - - - - Returns the result of applying the SQL/JSON - path_expression to the - context_item. - - - - - Only use JSON_VALUE() if the extracted value is - expected to be a single SQL/JSON scalar item; - getting multiple values will be treated as an error. If you expect that - extracted value might be an object or an array, use the - JSON_QUERY function instead. - - - - - By default, the result, which must be a single scalar value, is - returned as a value of type text, though the - RETURNING clause can be used to return as some - other type to which it can be successfully coerced. - - - - - The ON ERROR and ON EMPTY - clauses have similar semantics as mentioned in the description of - JSON_QUERY, except the set of values returned in - lieu of throwing an error is different. - - - - - Note that scalar strings returned by JSON_VALUE - always have their quotes removed, equivalent to specifying - OMIT QUOTES in JSON_QUERY. - - - - - Examples: - - - JSON_VALUE(jsonb '"123.45"', '$' RETURNING float) - 123.45 - - - JSON_VALUE(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI YYYY-MM-DD")' RETURNING date) - 2015-02-01 - - - JSON_VALUE(jsonb '[1,2]', 'strict $[$off]' PASSING 1 as off) - 2 - - - JSON_VALUE(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR) - 9 - - - - - -
- - - The context_item expression is converted to - jsonb by an implicit cast if the expression is not already of - type jsonb. Note, however, that any parsing errors that occur - during that conversion are thrown unconditionally, that is, are not - handled according to the (specified or implicit) ON ERROR - clause. - - - - - JSON_VALUE() returns an SQL NULL if - path_expression returns a JSON - null, whereas JSON_QUERY() returns - the JSON null as is. - - -
- - - JSON_TABLE - - json_table - - - - JSON_TABLE is an SQL/JSON function which - queries JSON data - and presents the results as a relational view, which can be accessed as a - regular SQL table. You can use JSON_TABLE inside - the FROM clause of a SELECT, - UPDATE, or DELETE and as data source - in a MERGE statement. - - - - Taking JSON data as input, JSON_TABLE uses a JSON path - expression to extract a part of the provided data to use as a - row pattern for the constructed view. Each SQL/JSON - value given by the row pattern serves as source for a separate row in the - constructed view. - - - - To split the row pattern into columns, JSON_TABLE - provides the COLUMNS clause that defines the - schema of the created view. For each column, a separate JSON path expression - can be specified to be evaluated against the row pattern to get an SQL/JSON - value that will become the value for the specified column in a given output - row. - - - - JSON data stored at a nested level of the row pattern can be extracted using - the NESTED PATH clause. Each - NESTED PATH clause can be used to generate one or more - columns using the data from a nested level of the row pattern. Those - columns can be specified using a COLUMNS clause that - looks similar to the top-level COLUMNS clause. Rows constructed from - NESTED COLUMNS are called child rows and are joined - against the row constructed from the columns specified in the parent - COLUMNS clause to get the row in the final view. Child - columns themselves may contain a NESTED PATH - specification thus allowing to extract data located at arbitrary nesting - levels. Columns produced by multiple NESTED PATHs at the - same level are considered to be siblings of each - other and their rows after joining with the parent row are combined using - UNION. - - - - The rows produced by JSON_TABLE are laterally - joined to the row that generated them, so you do not have to explicitly join - the constructed view with the original table holding JSON - data. - - - - The syntax is: - - - -JSON_TABLE ( - context_item, path_expression AS json_path_name PASSING { value AS varname } , ... - COLUMNS ( json_table_column , ... ) - { ERROR | EMPTY ARRAY} ON ERROR -) - - -where json_table_column is: - - name FOR ORDINALITY - | name type - FORMAT JSON ENCODING UTF8 - PATH path_expression - { WITHOUT | WITH { CONDITIONAL | UNCONDITIONAL } } ARRAY WRAPPER - { KEEP | OMIT } QUOTES ON SCALAR STRING - { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON EMPTY - { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON ERROR - | name type EXISTS PATH path_expression - { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR - | NESTED PATH path_expression AS json_path_name COLUMNS ( json_table_column , ... ) - - - - Each syntax element is described below in more detail. - - - - - - context_item, path_expression AS json_path_name PASSING { value AS varname } , ... - - - - The context_item specifies the input document - to query, the path_expression is an SQL/JSON - path expression defining the query, and json_path_name - is an optional name for the path_expression. - The optional PASSING clause provides data values for - the variables mentioned in the path_expression. - The result of the input data evaluation using the aforementioned elements - is called the row pattern, which is used as the - source for row values in the constructed view. - - - - - - - COLUMNS ( json_table_column , ... ) - - - - - The COLUMNS clause defining the schema of the - constructed view. In this clause, you can specify each column to be - filled with an SQL/JSON value obtained by applying a JSON path expression - against the row pattern. json_table_column has - the following variants: - - - - - - name FOR ORDINALITY - - - - Adds an ordinality column that provides sequential row numbering starting - from 1. Each NESTED PATH (see below) gets its own - counter for any nested ordinality columns. - - - - - - - name type - FORMAT JSON ENCODING UTF8 - PATH path_expression - - - - Inserts an SQL/JSON value obtained by applying - path_expression against the row pattern into - the view's output row after coercing it to specified - type. - - - Specifying FORMAT JSON makes it explicit that you - expect the value to be a valid json object. It only - makes sense to specify FORMAT JSON if - type is one of bpchar, - bytea, character varying, name, - json, jsonb, text, or a domain over - these types. - - - Optionally, you can specify WRAPPER and - QUOTES clauses to format the output. Note that - specifying OMIT QUOTES overrides - FORMAT JSON if also specified, because unquoted - literals do not constitute valid json values. - - - Optionally, you can use ON EMPTY and - ON ERROR clauses to specify whether to throw the error - or return the specified value when the result of JSON path evaluation is - empty and when an error occurs during JSON path evaluation or when - coercing the SQL/JSON value to the specified type, respectively. The - default for both is to return a NULL value. - - - - This clause is internally turned into and has the same semantics as - JSON_VALUE or JSON_QUERY. - The latter if the specified type is not a scalar type or if either of - FORMAT JSON, WRAPPER, or - QUOTES clause is present. - - - - - - - - name type - EXISTS PATH path_expression - - - - Inserts a boolean value obtained by applying - path_expression against the row pattern - into the view's output row after coercing it to specified - type. - - - The value corresponds to whether applying the PATH - expression to the row pattern yields any values. - - - The specified type should have a cast from the - boolean type. - - - Optionally, you can use ON ERROR to specify whether to - throw the error or return the specified value when an error occurs during - JSON path evaluation or when coercing SQL/JSON value to the specified - type. The default is to return a boolean value - FALSE. - - - - This clause is internally turned into and has the same semantics as - JSON_EXISTS. - - - - - - - - NESTED PATH path_expression AS json_path_name - COLUMNS ( json_table_column , ... ) - - - - - Extracts SQL/JSON values from nested levels of the row pattern, - generates one or more columns as defined by the COLUMNS - subclause, and inserts the extracted SQL/JSON values into those - columns. The json_table_column - expression in the COLUMNS subclause uses the same - syntax as in the parent COLUMNS clause. - - - - The NESTED PATH syntax is recursive, - so you can go down multiple nested levels by specifying several - NESTED PATH subclauses within each other. - It allows to unnest the hierarchy of JSON objects and arrays - in a single function invocation rather than chaining several - JSON_TABLE expressions in an SQL statement. - - - - - - - - In each variant of json_table_column described - above, if the PATH clause is omitted, path expression - $.name is used, where - name is the provided column name. - - - - - - - - - AS json_path_name - - - - - The optional json_path_name serves as an - identifier of the provided path_expression. - The name must be unique and distinct from the column names. - - - - - - - { ERROR | EMPTY } ON ERROR - - - - - The optional ON ERROR can be used to specify how to - handle errors when evaluating the top-level - path_expression. Use ERROR - if you want the errors to be thrown and EMPTY to - return an empty table, that is, a table containing 0 rows. Note that - this clause does not affect the errors that occur when evaluating - columns, for which the behavior depends on whether the - ON ERROR clause is specified against a given column. - - - - - - Examples - - - In the examples that follow, the following table containing JSON data - will be used: - - -CREATE TABLE my_films ( js jsonb ); - -INSERT INTO my_films VALUES ( -'{ "favorites" : [ - { "kind" : "comedy", "films" : [ - { "title" : "Bananas", - "director" : "Woody Allen"}, - { "title" : "The Dinner Game", - "director" : "Francis Veber" } ] }, - { "kind" : "horror", "films" : [ - { "title" : "Psycho", - "director" : "Alfred Hitchcock" } ] }, - { "kind" : "thriller", "films" : [ - { "title" : "Vertigo", - "director" : "Alfred Hitchcock" } ] }, - { "kind" : "drama", "films" : [ - { "title" : "Yojimbo", - "director" : "Akira Kurosawa" } ] } - ] }'); - - - - - The following query shows how to use JSON_TABLE to - turn the JSON objects in the my_films table - to a view containing columns for the keys kind, - title, and director contained in - the original JSON along with an ordinality column: - - -SELECT jt.* FROM - my_films, - JSON_TABLE (js, '$.favorites[*]' COLUMNS ( - id FOR ORDINALITY, - kind text PATH '$.kind', - title text PATH '$.films[*].title' WITH WRAPPER, - director text PATH '$.films[*].director' WITH WRAPPER)) AS jt; - - - - id | kind | title | director -----+----------+--------------------------------+---------------------------------- - 1 | comedy | ["Bananas", "The Dinner Game"] | ["Woody Allen", "Francis Veber"] - 2 | horror | ["Psycho"] | ["Alfred Hitchcock"] - 3 | thriller | ["Vertigo"] | ["Alfred Hitchcock"] - 4 | drama | ["Yojimbo"] | ["Akira Kurosawa"] -(4 rows) - - - - - The following is a modified version of the above query to show the - usage of PASSING arguments in the filter specified in - the top-level JSON path expression and the various options for the - individual columns: - - -SELECT jt.* FROM - my_films, - JSON_TABLE (js, '$.favorites[*] ? (@.films[*].director == $filter)' - PASSING 'Alfred Hitchcock' AS filter - COLUMNS ( - id FOR ORDINALITY, - kind text PATH '$.kind', - title text FORMAT JSON PATH '$.films[*].title' OMIT QUOTES, - director text PATH '$.films[*].director' KEEP QUOTES)) AS jt; - - - - id | kind | title | director -----+----------+---------+-------------------- - 1 | horror | Psycho | "Alfred Hitchcock" - 2 | thriller | Vertigo | "Alfred Hitchcock" -(2 rows) - - - - - The following is a modified version of the above query to show the usage - of NESTED PATH for populating title and director - columns, illustrating how they are joined to the parent columns id and - kind: - - -SELECT jt.* FROM - my_films, - JSON_TABLE ( js, '$.favorites[*] ? (@.films[*].director == $filter)' - PASSING 'Alfred Hitchcock' AS filter - COLUMNS ( - id FOR ORDINALITY, - kind text PATH '$.kind', - NESTED PATH '$.films[*]' COLUMNS ( - title text FORMAT JSON PATH '$.title' OMIT QUOTES, - director text PATH '$.director' KEEP QUOTES))) AS jt; - - - - id | kind | title | director -----+----------+---------+-------------------- - 1 | horror | Psycho | "Alfred Hitchcock" - 2 | thriller | Vertigo | "Alfred Hitchcock" -(2 rows) - - - - - - The following is the same query but without the filter in the root - path: - - -SELECT jt.* FROM - my_films, - JSON_TABLE ( js, '$.favorites[*]' - COLUMNS ( - id FOR ORDINALITY, - kind text PATH '$.kind', - NESTED PATH '$.films[*]' COLUMNS ( - title text FORMAT JSON PATH '$.title' OMIT QUOTES, - director text PATH '$.director' KEEP QUOTES))) AS jt; - - - - id | kind | title | director -----+----------+-----------------+-------------------- - 1 | comedy | Bananas | "Woody Allen" - 1 | comedy | The Dinner Game | "Francis Veber" - 2 | horror | Psycho | "Alfred Hitchcock" - 3 | thriller | Vertigo | "Alfred Hitchcock" - 4 | drama | Yojimbo | "Akira Kurosawa" -(5 rows) - - - - - - The following shows another query using a different JSON - object as input. It shows the UNION "sibling join" between - NESTED paths $.movies[*] and - $.books[*] and also the usage of - FOR ORDINALITY column at NESTED - levels (columns movie_id, book_id, - and author_id): - - -SELECT * FROM JSON_TABLE ( -'{"favorites": - [{"movies": - [{"name": "One", "director": "John Doe"}, - {"name": "Two", "director": "Don Joe"}], - "books": - [{"name": "Mystery", "authors": [{"name": "Brown Dan"}]}, - {"name": "Wonder", "authors": [{"name": "Jun Murakami"}, {"name":"Craig Doe"}]}] -}]}'::json, '$.favorites[*]' -COLUMNS ( - user_id FOR ORDINALITY, - NESTED '$.movies[*]' - COLUMNS ( - movie_id FOR ORDINALITY, - mname text PATH '$.name', - director text), - NESTED '$.books[*]' - COLUMNS ( - book_id FOR ORDINALITY, - bname text PATH '$.name', - NESTED '$.authors[*]' - COLUMNS ( - author_id FOR ORDINALITY, - author_name text PATH '$.name')))); - - - - user_id | movie_id | mname | director | book_id | bname | author_id | author_name ----------+----------+-------+----------+---------+---------+-----------+-------------- - 1 | 1 | One | John Doe | | | | - 1 | 2 | Two | Don Joe | | | | - 1 | | | | 1 | Mystery | 1 | Brown Dan - 1 | | | | 2 | Wonder | 1 | Jun Murakami - 1 | | | | 2 | Wonder | 2 | Craig Doe -(5 rows) - - - - -
- - - Sequence Manipulation Functions - - - sequence - - - - This section describes functions for operating on sequence - objects, also called sequence generators or just sequences. - Sequence objects are special single-row tables created with . - Sequence objects are commonly used to generate unique identifiers - for rows of a table. The sequence functions, listed in , provide simple, multiuser-safe - methods for obtaining successive sequence values from sequence - objects. - - - - Sequence Functions - - - - - Function - - - Description - - - - - - - - - nextval - - nextval ( regclass ) - bigint - - - Advances the sequence object to its next value and returns that value. - This is done atomically: even if multiple sessions - execute nextval concurrently, each will safely - receive a distinct sequence value. - If the sequence object has been created with default parameters, - successive nextval calls will return successive - values beginning with 1. Other behaviors can be obtained by using - appropriate parameters in the - command. - - - This function requires USAGE - or UPDATE privilege on the sequence. - - - - - - - setval - - setval ( regclass, bigint , boolean ) - bigint - - - Sets the sequence object's current value, and optionally - its is_called flag. The two-parameter - form sets the sequence's last_value field to the - specified value and sets its is_called field to - true, meaning that the next - nextval will advance the sequence before - returning a value. The value that will be reported - by currval is also set to the specified value. - In the three-parameter form, is_called can be set - to either true - or false. true has the same - effect as the two-parameter form. If it is set - to false, the next nextval - will return exactly the specified value, and sequence advancement - commences with the following nextval. - Furthermore, the value reported by currval is not - changed in this case. For example, - -SELECT setval('myseq', 42); Next nextval will return 43 -SELECT setval('myseq', 42, true); Same as above -SELECT setval('myseq', 42, false); Next nextval will return 42 - - The result returned by setval is just the value of its - second argument. - - - This function requires UPDATE privilege on the - sequence. - - - - - - - currval - - currval ( regclass ) - bigint - - - Returns the value most recently obtained - by nextval for this sequence in the current - session. (An error is reported if nextval has - never been called for this sequence in this session.) Because this is - returning a session-local value, it gives a predictable answer whether - or not other sessions have executed nextval since - the current session did. - - - This function requires USAGE - or SELECT privilege on the sequence. - - - - - - - lastval - - lastval () - bigint - - - Returns the value most recently returned by - nextval in the current session. This function is - identical to currval, except that instead - of taking the sequence name as an argument it refers to whichever - sequence nextval was most recently applied to - in the current session. It is an error to call - lastval if nextval - has not yet been called in the current session. - - - This function requires USAGE - or SELECT privilege on the last used sequence. - - - - -
- - - - To avoid blocking concurrent transactions that obtain numbers from - the same sequence, the value obtained by nextval - is not reclaimed for re-use if the calling transaction later aborts. - This means that transaction aborts or database crashes can result in - gaps in the sequence of assigned values. That can happen without a - transaction abort, too. For example an INSERT with - an ON CONFLICT clause will compute the to-be-inserted - tuple, including doing any required nextval - calls, before detecting any conflict that would cause it to follow - the ON CONFLICT rule instead. - Thus, PostgreSQL sequence - objects cannot be used to obtain gapless - sequences. - - - - Likewise, sequence state changes made by setval - are immediately visible to other transactions, and are not undone if - the calling transaction rolls back. - - - - If the database cluster crashes before committing a transaction - containing a nextval - or setval call, the sequence state change might - not have made its way to persistent storage, so that it is uncertain - whether the sequence will have its original or updated state after the - cluster restarts. This is harmless for usage of the sequence within - the database, since other effects of uncommitted transactions will not - be visible either. However, if you wish to use a sequence value for - persistent outside-the-database purposes, make sure that the - nextval call has been committed before doing so. - - - - - The sequence to be operated on by a sequence function is specified by - a regclass argument, which is simply the OID of the sequence in the - pg_class system catalog. You do not have to look up the - OID by hand, however, since the regclass data type's input - converter will do the work for you. See - for details. - -
- - - - Conditional Expressions - - - CASE - - - - conditional expression - - - - This section describes the SQL-compliant conditional expressions - available in PostgreSQL. - - - - - If your needs go beyond the capabilities of these conditional - expressions, you might want to consider writing a server-side function - in a more expressive programming language. - - - - - - Although COALESCE, GREATEST, and - LEAST are syntactically similar to functions, they are - not ordinary functions, and thus cannot be used with explicit - VARIADIC array arguments. - - - - - <literal>CASE</literal> - - - The SQL CASE expression is a - generic conditional expression, similar to if/else statements in - other programming languages: - - -CASE WHEN condition THEN result - WHEN ... - ELSE result -END - - - CASE clauses can be used wherever - an expression is valid. Each condition is an - expression that returns a boolean result. If the condition's - result is true, the value of the CASE expression is the - result that follows the condition, and the - remainder of the CASE expression is not processed. If the - condition's result is not true, any subsequent WHEN clauses - are examined in the same manner. If no WHEN - condition yields true, the value of the - CASE expression is the result of the - ELSE clause. If the ELSE clause is - omitted and no condition is true, the result is null. - - - - An example: - -SELECT * FROM test; - - a ---- - 1 - 2 - 3 - - -SELECT a, - CASE WHEN a=1 THEN 'one' - WHEN a=2 THEN 'two' - ELSE 'other' - END - FROM test; - - a | case ----+------- - 1 | one - 2 | two - 3 | other - - - - - The data types of all the result - expressions must be convertible to a single output type. - See for more details. - - - - There is a simple form of CASE expression - that is a variant of the general form above: - - -CASE expression - WHEN value THEN result - WHEN ... - ELSE result -END - - - The first - expression is computed, then compared to - each of the value expressions in the - WHEN clauses until one is found that is equal to it. If - no match is found, the result of the - ELSE clause (or a null value) is returned. This is similar - to the switch statement in C. - - - - The example above can be written using the simple - CASE syntax: - -SELECT a, - CASE a WHEN 1 THEN 'one' - WHEN 2 THEN 'two' - ELSE 'other' - END - FROM test; - - a | case ----+------- - 1 | one - 2 | two - 3 | other - - - - - A CASE expression does not evaluate any subexpressions - that are not needed to determine the result. For example, this is a - possible way of avoiding a division-by-zero failure: - -SELECT ... WHERE CASE WHEN x <> 0 THEN y/x > 1.5 ELSE false END; - - - - - - As described in , there are various - situations in which subexpressions of an expression are evaluated at - different times, so that the principle that CASE - evaluates only necessary subexpressions is not ironclad. For - example a constant 1/0 subexpression will usually result in - a division-by-zero failure at planning time, even if it's within - a CASE arm that would never be entered at run time. - - - - - - <literal>COALESCE</literal> - - - COALESCE - - - - NVL - - - - IFNULL - - - -COALESCE(value , ...) - - - - The COALESCE function returns the first of its - arguments that is not null. Null is returned only if all arguments - are null. It is often used to substitute a default value for - null values when data is retrieved for display, for example: - -SELECT COALESCE(description, short_description, '(none)') ... - - This returns description if it is not null, otherwise - short_description if it is not null, otherwise (none). - - - - The arguments must all be convertible to a common data type, which - will be the type of the result (see - for details). - - - - Like a CASE expression, COALESCE only - evaluates the arguments that are needed to determine the result; - that is, arguments to the right of the first non-null argument are - not evaluated. This SQL-standard function provides capabilities similar - to NVL and IFNULL, which are used in some other - database systems. - - - - - <literal>NULLIF</literal> - - - NULLIF - - - -NULLIF(value1, value2) - - - - The NULLIF function returns a null value if - value1 equals value2; - otherwise it returns value1. - This can be used to perform the inverse operation of the - COALESCE example given above: - -SELECT NULLIF(value, '(none)') ... - - In this example, if value is (none), - null is returned, otherwise the value of value - is returned. - - - - The two arguments must be of comparable types. - To be specific, they are compared exactly as if you had - written value1 - = value2, so there must be a - suitable = operator available. - - - - The result has the same type as the first argument — but there is - a subtlety. What is actually returned is the first argument of the - implied = operator, and in some cases that will have - been promoted to match the second argument's type. For - example, NULLIF(1, 2.2) yields numeric, - because there is no integer = - numeric operator, - only numeric = numeric. - - - - - - <literal>GREATEST</literal> and <literal>LEAST</literal> - - - GREATEST - - - LEAST - - - -GREATEST(value , ...) - - -LEAST(value , ...) - - - - The GREATEST and LEAST functions select the - largest or smallest value from a list of any number of expressions. - The expressions must all be convertible to a common data type, which - will be the type of the result - (see for details). - - - - NULL values in the argument list are ignored. The result will be NULL - only if all the expressions evaluate to NULL. (This is a deviation from - the SQL standard. According to the standard, the return value is NULL if - any argument is NULL. Some other databases behave this way.) - - - - - - Array Functions and Operators - - - shows the specialized operators - available for array types. - In addition to those, the usual comparison operators shown in are available for - arrays. The comparison operators compare the array contents - element-by-element, using the default B-tree comparison function for - the element data type, and sort based on the first difference. - In multidimensional arrays the elements are visited in row-major order - (last subscript varies most rapidly). - If the contents of two arrays are equal but the dimensionality is - different, the first difference in the dimensionality information - determines the sort order. - - - - Array Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - anyarray @> anyarray - boolean - - - Does the first array contain the second, that is, does each element - appearing in the second array equal some element of the first array? - (Duplicates are not treated specially, - thus ARRAY[1] and ARRAY[1,1] are - each considered to contain the other.) - - - ARRAY[1,4,3] @> ARRAY[3,1,3] - t - - - - - - anyarray <@ anyarray - boolean - - - Is the first array contained by the second? - - - ARRAY[2,2,7] <@ ARRAY[1,7,4,2,6] - t - - - - - - anyarray && anyarray - boolean - - - Do the arrays overlap, that is, have any elements in common? - - - ARRAY[1,4,3] && ARRAY[2,1] - t - - - - - - anycompatiblearray || anycompatiblearray - anycompatiblearray - - - Concatenates the two arrays. Concatenating a null or empty array is a - no-op; otherwise the arrays must have the same number of dimensions - (as illustrated by the first example) or differ in number of - dimensions by one (as illustrated by the second). - If the arrays are not of identical element types, they will be coerced - to a common type (see ). - - - ARRAY[1,2,3] || ARRAY[4,5,6,7] - {1,2,3,4,5,6,7} - - - ARRAY[1,2,3] || ARRAY[[4,5,6],[7,8,9.9]] - {{1,2,3},{4,5,6},{7,8,9.9}} - - - - - - anycompatible || anycompatiblearray - anycompatiblearray - - - Concatenates an element onto the front of an array (which must be - empty or one-dimensional). - - - 3 || ARRAY[4,5,6] - {3,4,5,6} - - - - - - anycompatiblearray || anycompatible - anycompatiblearray - - - Concatenates an element onto the end of an array (which must be - empty or one-dimensional). - - - ARRAY[4,5,6] || 7 - {4,5,6,7} - - - - -
- - - See for more details about array operator - behavior. See for more details about - which operators support indexed operations. - - - - shows the functions - available for use with array types. See - for more information and examples of the use of these functions. - - - - Array Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - array_append - - array_append ( anycompatiblearray, anycompatible ) - anycompatiblearray - - - Appends an element to the end of an array (same as - the anycompatiblearray || anycompatible - operator). - - - array_append(ARRAY[1,2], 3) - {1,2,3} - - - - - - - array_cat - - array_cat ( anycompatiblearray, anycompatiblearray ) - anycompatiblearray - - - Concatenates two arrays (same as - the anycompatiblearray || anycompatiblearray - operator). - - - array_cat(ARRAY[1,2,3], ARRAY[4,5]) - {1,2,3,4,5} - - - - - - - array_dims - - array_dims ( anyarray ) - text - - - Returns a text representation of the array's dimensions. - - - array_dims(ARRAY[[1,2,3], [4,5,6]]) - [1:2][1:3] - - - - - - - array_fill - - array_fill ( anyelement, integer[] - , integer[] ) - anyarray - - - Returns an array filled with copies of the given value, having - dimensions of the lengths specified by the second argument. - The optional third argument supplies lower-bound values for each - dimension (which default to all 1). - - - array_fill(11, ARRAY[2,3]) - {{11,11,11},{11,11,11}} - - - array_fill(7, ARRAY[3], ARRAY[2]) - [2:4]={7,7,7} - - - - - - - array_length - - array_length ( anyarray, integer ) - integer - - - Returns the length of the requested array dimension. - (Produces NULL instead of 0 for empty or missing array dimensions.) - - - array_length(array[1,2,3], 1) - 3 - - - array_length(array[]::int[], 1) - NULL - - - array_length(array['text'], 2) - NULL - - - - - - - array_lower - - array_lower ( anyarray, integer ) - integer - - - Returns the lower bound of the requested array dimension. - - - array_lower('[0:2]={1,2,3}'::integer[], 1) - 0 - - - - - - - array_ndims - - array_ndims ( anyarray ) - integer - - - Returns the number of dimensions of the array. - - - array_ndims(ARRAY[[1,2,3], [4,5,6]]) - 2 - - - - - - - array_position - - array_position ( anycompatiblearray, anycompatible , integer ) - integer - - - Returns the subscript of the first occurrence of the second argument - in the array, or NULL if it's not present. - If the third argument is given, the search begins at that subscript. - The array must be one-dimensional. - Comparisons are done using IS NOT DISTINCT FROM - semantics, so it is possible to search for NULL. - - - array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') - 2 - - - - - - - array_positions - - array_positions ( anycompatiblearray, anycompatible ) - integer[] - - - Returns an array of the subscripts of all occurrences of the second - argument in the array given as first argument. - The array must be one-dimensional. - Comparisons are done using IS NOT DISTINCT FROM - semantics, so it is possible to search for NULL. - NULL is returned only if the array - is NULL; if the value is not found in the array, an - empty array is returned. - - - array_positions(ARRAY['A','A','B','A'], 'A') - {1,2,4} - - - - - - - array_prepend - - array_prepend ( anycompatible, anycompatiblearray ) - anycompatiblearray - - - Prepends an element to the beginning of an array (same as - the anycompatible || anycompatiblearray - operator). - - - array_prepend(1, ARRAY[2,3]) - {1,2,3} - - - - - - - array_remove - - array_remove ( anycompatiblearray, anycompatible ) - anycompatiblearray - - - Removes all elements equal to the given value from the array. - The array must be one-dimensional. - Comparisons are done using IS NOT DISTINCT FROM - semantics, so it is possible to remove NULLs. - - - array_remove(ARRAY[1,2,3,2], 2) - {1,3} - - - - - - - array_replace - - array_replace ( anycompatiblearray, anycompatible, anycompatible ) - anycompatiblearray - - - Replaces each array element equal to the second argument with the - third argument. - - - array_replace(ARRAY[1,2,5,4], 5, 3) - {1,2,3,4} - - - - - - - array_reverse - - array_reverse ( anyarray ) - anyarray - - - Reverses the first dimension of the array. - - - array_reverse(ARRAY[[1,2],[3,4],[5,6]]) - {{5,6},{3,4},{1,2}} - - - - - - - array_sample - - array_sample ( array anyarray, n integer ) - anyarray - - - Returns an array of n items randomly selected - from array. n may not - exceed the length of array's first dimension. - If array is multi-dimensional, - an item is a slice having a given first subscript. - - - array_sample(ARRAY[1,2,3,4,5,6], 3) - {2,6,1} - - - array_sample(ARRAY[[1,2],[3,4],[5,6]], 2) - {{5,6},{1,2}} - - - - - - - array_shuffle - - array_shuffle ( anyarray ) - anyarray - - - Randomly shuffles the first dimension of the array. - - - array_shuffle(ARRAY[[1,2],[3,4],[5,6]]) - {{5,6},{1,2},{3,4}} - - - - - - - array_sort - - array_sort ( - array anyarray - , descending boolean - , nulls_first boolean - ) - anyarray - - - Sorts the first dimension of the array. - The sort order is determined by the default sort ordering of the - array's element type; however, if the element type is collatable, - the collation to use can be specified by adding - a COLLATE clause to - the array argument. - - - If descending is true then sort in - descending order, otherwise ascending order. If omitted, the - default is ascending order. - If nulls_first is true then nulls appear - before non-null values, otherwise nulls appear after non-null - values. - If omitted, nulls_first is taken to have - the same value as descending. - - - array_sort(ARRAY[[2,4],[2,1],[6,5]]) - {{2,1},{2,4},{6,5}} - - - - - - - array_to_string - - array_to_string ( array anyarray, delimiter text , null_string text ) - text - - - Converts each array element to its text representation, and - concatenates those separated by - the delimiter string. - If null_string is given and is - not NULL, then NULL array - entries are represented by that string; otherwise, they are omitted. - See also string_to_array. - - - array_to_string(ARRAY[1, 2, 3, NULL, 5], ',', '*') - 1,2,3,*,5 - - - - - - - array_upper - - array_upper ( anyarray, integer ) - integer - - - Returns the upper bound of the requested array dimension. - - - array_upper(ARRAY[1,8,3,7], 1) - 4 - - - - - - - cardinality - - cardinality ( anyarray ) - integer - - - Returns the total number of elements in the array, or 0 if the array - is empty. - - - cardinality(ARRAY[[1,2],[3,4]]) - 4 - - - - - - - trim_array - - trim_array ( array anyarray, n integer ) - anyarray - - - Trims an array by removing the last n elements. - If the array is multidimensional, only the first dimension is trimmed. - - - trim_array(ARRAY[1,2,3,4,5,6], 2) - {1,2,3,4} - - - - - - - unnest - - unnest ( anyarray ) - setof anyelement - - - Expands an array into a set of rows. - The array's elements are read out in storage order. - - - unnest(ARRAY[1,2]) - - - 1 - 2 - - - - unnest(ARRAY[['foo','bar'],['baz','quux']]) - - - foo - bar - baz - quux - - - - - - - unnest ( anyarray, anyarray , ... ) - setof anyelement, anyelement [, ... ] - - - Expands multiple arrays (possibly of different data types) into a set of - rows. If the arrays are not all the same length then the shorter ones - are padded with NULLs. This form is only allowed - in a query's FROM clause; see . - - - select * from unnest(ARRAY[1,2], ARRAY['foo','bar','baz']) as x(a,b) - - - a | b ----+----- - 1 | foo - 2 | bar - | baz - - - - - -
- - - See also about the aggregate - function array_agg for use with arrays. - -
- - - Range/Multirange Functions and Operators - - - See for an overview of range types. - - - - shows the specialized operators - available for range types. - shows the specialized operators - available for multirange types. - In addition to those, the usual comparison operators shown in - are available for range - and multirange types. The comparison operators order first by the range lower - bounds, and only if those are equal do they compare the upper bounds. The - multirange operators compare each range until one is unequal. This - does not usually result in a useful overall ordering, but the operators are - provided to allow unique indexes to be constructed on ranges. - - - - Range Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - anyrange @> anyrange - boolean - - - Does the first range contain the second? - - - int4range(2,4) @> int4range(2,3) - t - - - - - - anyrange @> anyelement - boolean - - - Does the range contain the element? - - - '[2011-01-01,2011-03-01)'::tsrange @> '2011-01-10'::timestamp - t - - - - - - anyrange <@ anyrange - boolean - - - Is the first range contained by the second? - - - int4range(2,4) <@ int4range(1,7) - t - - - - - - anyelement <@ anyrange - boolean - - - Is the element contained in the range? - - - 42 <@ int4range(1,7) - f - - - - - - anyrange && anyrange - boolean - - - Do the ranges overlap, that is, have any elements in common? - - - int8range(3,7) && int8range(4,12) - t - - - - - - anyrange << anyrange - boolean - - - Is the first range strictly left of the second? - - - int8range(1,10) << int8range(100,110) - t - - - - - - anyrange >> anyrange - boolean - - - Is the first range strictly right of the second? - - - int8range(50,60) >> int8range(20,30) - t - - - - - - anyrange &< anyrange - boolean - - - Does the first range not extend to the right of the second? - - - int8range(1,20) &< int8range(18,20) - t - - - - - - anyrange &> anyrange - boolean - - - Does the first range not extend to the left of the second? - - - int8range(7,20) &> int8range(5,10) - t - - - - - - anyrange -|- anyrange - boolean - - - Are the ranges adjacent? - - - numrange(1.1,2.2) -|- numrange(2.2,3.3) - t - - - - - - anyrange + anyrange - anyrange - - - Computes the union of the ranges. The ranges must overlap or be - adjacent, so that the union is a single range (but - see range_merge()). - - - numrange(5,15) + numrange(10,20) - [5,20) - - - - - - anyrange * anyrange - anyrange - - - Computes the intersection of the ranges. - - - int8range(5,15) * int8range(10,20) - [10,15) - - - - - - anyrange - anyrange - anyrange - - - Computes the difference of the ranges. The second range must not be - contained in the first in such a way that the difference would not be - a single range. - - - int8range(5,15) - int8range(10,20) - [5,10) - - - - -
- - - Multirange Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - anymultirange @> anymultirange - boolean - - - Does the first multirange contain the second? - - - '{[2,4)}'::int4multirange @> '{[2,3)}'::int4multirange - t - - - - - - anymultirange @> anyrange - boolean - - - Does the multirange contain the range? - - - '{[2,4)}'::int4multirange @> int4range(2,3) - t - - - - - - anymultirange @> anyelement - boolean - - - Does the multirange contain the element? - - - '{[2011-01-01,2011-03-01)}'::tsmultirange @> '2011-01-10'::timestamp - t - - - - - - anyrange @> anymultirange - boolean - - - Does the range contain the multirange? - - - '[2,4)'::int4range @> '{[2,3)}'::int4multirange - t - - - - - - anymultirange <@ anymultirange - boolean - - - Is the first multirange contained by the second? - - - '{[2,4)}'::int4multirange <@ '{[1,7)}'::int4multirange - t - - - - - - anymultirange <@ anyrange - boolean - - - Is the multirange contained by the range? - - - '{[2,4)}'::int4multirange <@ int4range(1,7) - t - - - - - - anyrange <@ anymultirange - boolean - - - Is the range contained by the multirange? - - - int4range(2,4) <@ '{[1,7)}'::int4multirange - t - - - - - - anyelement <@ anymultirange - boolean - - - Is the element contained by the multirange? - - - 4 <@ '{[1,7)}'::int4multirange - t - - - - - - anymultirange && anymultirange - boolean - - - Do the multiranges overlap, that is, have any elements in common? - - - '{[3,7)}'::int8multirange && '{[4,12)}'::int8multirange - t - - - - - - anymultirange && anyrange - boolean - - - Does the multirange overlap the range? - - - '{[3,7)}'::int8multirange && int8range(4,12) - t - - - - - - anyrange && anymultirange - boolean - - - Does the range overlap the multirange? - - - int8range(3,7) && '{[4,12)}'::int8multirange - t - - - - - - anymultirange << anymultirange - boolean - - - Is the first multirange strictly left of the second? - - - '{[1,10)}'::int8multirange << '{[100,110)}'::int8multirange - t - - - - - - anymultirange << anyrange - boolean - - - Is the multirange strictly left of the range? - - - '{[1,10)}'::int8multirange << int8range(100,110) - t - - - - - - anyrange << anymultirange - boolean - - - Is the range strictly left of the multirange? - - - int8range(1,10) << '{[100,110)}'::int8multirange - t - - - - - - anymultirange >> anymultirange - boolean - - - Is the first multirange strictly right of the second? - - - '{[50,60)}'::int8multirange >> '{[20,30)}'::int8multirange - t - - - - - - anymultirange >> anyrange - boolean - - - Is the multirange strictly right of the range? - - - '{[50,60)}'::int8multirange >> int8range(20,30) - t - - - - - - anyrange >> anymultirange - boolean - - - Is the range strictly right of the multirange? - - - int8range(50,60) >> '{[20,30)}'::int8multirange - t - - - - - - anymultirange &< anymultirange - boolean - - - Does the first multirange not extend to the right of the second? - - - '{[1,20)}'::int8multirange &< '{[18,20)}'::int8multirange - t - - - - - - anymultirange &< anyrange - boolean - - - Does the multirange not extend to the right of the range? - - - '{[1,20)}'::int8multirange &< int8range(18,20) - t - - - - - - anyrange &< anymultirange - boolean - - - Does the range not extend to the right of the multirange? - - - int8range(1,20) &< '{[18,20)}'::int8multirange - t - - - - - - anymultirange &> anymultirange - boolean - - - Does the first multirange not extend to the left of the second? - - - '{[7,20)}'::int8multirange &> '{[5,10)}'::int8multirange - t - - - - - - anymultirange &> anyrange - boolean - - - Does the multirange not extend to the left of the range? - - - '{[7,20)}'::int8multirange &> int8range(5,10) - t - - - - - - anyrange &> anymultirange - boolean - - - Does the range not extend to the left of the multirange? - - - int8range(7,20) &> '{[5,10)}'::int8multirange - t - - - - - - anymultirange -|- anymultirange - boolean - - - Are the multiranges adjacent? - - - '{[1.1,2.2)}'::nummultirange -|- '{[2.2,3.3)}'::nummultirange - t - - - - - - anymultirange -|- anyrange - boolean - - - Is the multirange adjacent to the range? - - - '{[1.1,2.2)}'::nummultirange -|- numrange(2.2,3.3) - t - - - - - - anyrange -|- anymultirange - boolean - - - Is the range adjacent to the multirange? - - - numrange(1.1,2.2) -|- '{[2.2,3.3)}'::nummultirange - t - - - - - - anymultirange + anymultirange - anymultirange - - - Computes the union of the multiranges. The multiranges need not overlap - or be adjacent. - - - '{[5,10)}'::nummultirange + '{[15,20)}'::nummultirange - {[5,10), [15,20)} - - - - - - anymultirange * anymultirange - anymultirange - - - Computes the intersection of the multiranges. - - - '{[5,15)}'::int8multirange * '{[10,20)}'::int8multirange - {[10,15)} - - - - - - anymultirange - anymultirange - anymultirange - - - Computes the difference of the multiranges. - - - '{[5,20)}'::int8multirange - '{[10,15)}'::int8multirange - {[5,10), [15,20)} - - - - -
- - - The left-of/right-of/adjacent operators always return false when an empty - range or multirange is involved; that is, an empty range is not considered to - be either before or after any other range. - - - - Elsewhere empty ranges and multiranges are treated as the additive identity: - anything unioned with an empty value is itself. Anything minus an empty - value is itself. An empty multirange has exactly the same points as an empty - range. Every range contains the empty range. Every multirange contains as many - empty ranges as you like. - - - - The range union and difference operators will fail if the resulting range would - need to contain two disjoint sub-ranges, as such a range cannot be - represented. There are separate operators for union and difference that take - multirange parameters and return a multirange, and they do not fail even if - their arguments are disjoint. So if you need a union or difference operation - for ranges that may be disjoint, you can avoid errors by first casting your - ranges to multiranges. - - - - shows the functions - available for use with range types. - shows the functions - available for use with multirange types. - - - - Range Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - lower - - lower ( anyrange ) - anyelement - - - Extracts the lower bound of the range (NULL if the - range is empty or has no lower bound). - - - lower(numrange(1.1,2.2)) - 1.1 - - - - - - - upper - - upper ( anyrange ) - anyelement - - - Extracts the upper bound of the range (NULL if the - range is empty or has no upper bound). - - - upper(numrange(1.1,2.2)) - 2.2 - - - - - - - isempty - - isempty ( anyrange ) - boolean - - - Is the range empty? - - - isempty(numrange(1.1,2.2)) - f - - - - - - - lower_inc - - lower_inc ( anyrange ) - boolean - - - Is the range's lower bound inclusive? - - - lower_inc(numrange(1.1,2.2)) - t - - - - - - - upper_inc - - upper_inc ( anyrange ) - boolean - - - Is the range's upper bound inclusive? - - - upper_inc(numrange(1.1,2.2)) - f - - - - - - - lower_inf - - lower_inf ( anyrange ) - boolean - - - Does the range have no lower bound? (A lower bound of - -Infinity returns false.) - - - lower_inf('(,)'::daterange) - t - - - - - - - upper_inf - - upper_inf ( anyrange ) - boolean - - - Does the range have no upper bound? (An upper bound of - Infinity returns false.) - - - upper_inf('(,)'::daterange) - t - - - - - - - range_merge - - range_merge ( anyrange, anyrange ) - anyrange - - - Computes the smallest range that includes both of the given ranges. - - - range_merge('[1,2)'::int4range, '[3,4)'::int4range) - [1,4) - - - - -
- - - Multirange Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - lower - - lower ( anymultirange ) - anyelement - - - Extracts the lower bound of the multirange (NULL if the - multirange is empty or has no lower bound). - - - lower('{[1.1,2.2)}'::nummultirange) - 1.1 - - - - - - - upper - - upper ( anymultirange ) - anyelement - - - Extracts the upper bound of the multirange (NULL if the - multirange is empty or has no upper bound). - - - upper('{[1.1,2.2)}'::nummultirange) - 2.2 - - - - - - - isempty - - isempty ( anymultirange ) - boolean - - - Is the multirange empty? - - - isempty('{[1.1,2.2)}'::nummultirange) - f - - - - - - - lower_inc - - lower_inc ( anymultirange ) - boolean - - - Is the multirange's lower bound inclusive? - - - lower_inc('{[1.1,2.2)}'::nummultirange) - t - - - - - - - upper_inc - - upper_inc ( anymultirange ) - boolean - - - Is the multirange's upper bound inclusive? - - - upper_inc('{[1.1,2.2)}'::nummultirange) - f - - - - - - - lower_inf - - lower_inf ( anymultirange ) - boolean - - - Does the multirange have no lower bound? (A lower bound of - -Infinity returns false.) - - - lower_inf('{(,)}'::datemultirange) - t - - - - - - - upper_inf - - upper_inf ( anymultirange ) - boolean - - - Does the multirange have no upper bound? (An upper bound of - Infinity returns false.) - - - upper_inf('{(,)}'::datemultirange) - t - - - - - - - range_merge - - range_merge ( anymultirange ) - anyrange - - - Computes the smallest range that includes the entire multirange. - - - range_merge('{[1,2), [3,4)}'::int4multirange) - [1,4) - - - - - - - multirange (function) - - multirange ( anyrange ) - anymultirange - - - Returns a multirange containing just the given range. - - - multirange('[1,2)'::int4range) - {[1,2)} - - - - - - - unnest - for multirange - - unnest ( anymultirange ) - setof anyrange - - - Expands a multirange into a set of ranges in ascending order. - - - unnest('{[1,2), [3,4)}'::int4multirange) - - - [1,2) - [3,4) - - - - - -
- - - The lower_inc, upper_inc, - lower_inf, and upper_inf - functions all return false for an empty range or multirange. - -
- - - Aggregate Functions - - - aggregate function - built-in - - - - Aggregate functions compute a single result - from a set of input values. The built-in general-purpose aggregate - functions are listed in - while statistical aggregates are in . - The built-in within-group ordered-set aggregate functions - are listed in - while the built-in within-group hypothetical-set ones are in . Grouping operations, - which are closely related to aggregate functions, are listed in - . - The special syntax considerations for aggregate - functions are explained in . - Consult for additional introductory - information. - - - - Aggregate functions that support Partial Mode - are eligible to participate in various optimizations, such as parallel - aggregation. - - - - While all aggregates below accept an optional - ORDER BY clause (as outlined in ), the clause has only been added to - aggregates whose output is affected by ordering. - - - - General-Purpose Aggregate Functions - - - - - - - Function - - - Description - - Partial Mode - - - - - - - - any_value - - any_value ( anyelement ) - same as input type - - - Returns an arbitrary value from the non-null input values. - - Yes - - - - - - array_agg - - array_agg ( anynonarray ORDER BY input_sort_columns ) - anyarray - - - Collects all the input values, including nulls, into an array. - - Yes - - - - - array_agg ( anyarray ORDER BY input_sort_columns ) - anyarray - - - Concatenates all the input arrays into an array of one higher - dimension. (The inputs must all have the same dimensionality, and - cannot be empty or null.) - - Yes - - - - - - average - - - avg - - avg ( smallint ) - numeric - - - avg ( integer ) - numeric - - - avg ( bigint ) - numeric - - - avg ( numeric ) - numeric - - - avg ( real ) - double precision - - - avg ( double precision ) - double precision - - - avg ( interval ) - interval - - - Computes the average (arithmetic mean) of all the non-null input - values. - - Yes - - - - - - bit_and - - bit_and ( smallint ) - smallint - - - bit_and ( integer ) - integer - - - bit_and ( bigint ) - bigint - - - bit_and ( bit ) - bit - - - Computes the bitwise AND of all non-null input values. - - Yes - - - - - - bit_or - - bit_or ( smallint ) - smallint - - - bit_or ( integer ) - integer - - - bit_or ( bigint ) - bigint - - - bit_or ( bit ) - bit - - - Computes the bitwise OR of all non-null input values. - - Yes - - - - - - bit_xor - - bit_xor ( smallint ) - smallint - - - bit_xor ( integer ) - integer - - - bit_xor ( bigint ) - bigint - - - bit_xor ( bit ) - bit - - - Computes the bitwise exclusive OR of all non-null input values. - Can be useful as a checksum for an unordered set of values. - - Yes - - - - - - bool_and - - bool_and ( boolean ) - boolean - - - Returns true if all non-null input values are true, otherwise false. - - Yes - - - - - - bool_or - - bool_or ( boolean ) - boolean - - - Returns true if any non-null input value is true, otherwise false. - - Yes - - - - - - count - - count ( * ) - bigint - - - Computes the number of input rows. - - Yes - - - - - count ( "any" ) - bigint - - - Computes the number of input rows in which the input value is not - null. - - Yes - - - - - - every - - every ( boolean ) - boolean - - - This is the SQL standard's equivalent to bool_and. - - Yes - - - - - - json_agg - - json_agg ( anyelement ORDER BY input_sort_columns ) - json - - - - jsonb_agg - - jsonb_agg ( anyelement ORDER BY input_sort_columns ) - jsonb - - - Collects all the input values, including nulls, into a JSON array. - Values are converted to JSON as per to_json - or to_jsonb. - - No - - - - - - json_agg_strict - - json_agg_strict ( anyelement ) - json - - - - jsonb_agg_strict - - jsonb_agg_strict ( anyelement ) - jsonb - - - Collects all the input values, skipping nulls, into a JSON array. - Values are converted to JSON as per to_json - or to_jsonb. - - No - - - - - json_arrayagg - json_arrayagg ( - value_expression - ORDER BY sort_expression - { NULL | ABSENT } ON NULL - RETURNING data_type FORMAT JSON ENCODING UTF8 ) - - - Behaves in the same way as json_array - but as an aggregate function so it only takes one - value_expression parameter. - If ABSENT ON NULL is specified, any NULL - values are omitted. - If ORDER BY is specified, the elements will - appear in the array in that order rather than in the input order. - - - SELECT json_arrayagg(v) FROM (VALUES(2),(1)) t(v) - [2, 1] - - No - - - - - json_objectagg - json_objectagg ( - { key_expression { VALUE | ':' } value_expression } - { NULL | ABSENT } ON NULL - { WITH | WITHOUT } UNIQUE KEYS - RETURNING data_type FORMAT JSON ENCODING UTF8 ) - - - Behaves like json_object, but as an - aggregate function, so it only takes one - key_expression and one - value_expression parameter. - - - SELECT json_objectagg(k:v) FROM (VALUES ('a'::text,current_date),('b',current_date + 1)) AS t(k,v) - { "a" : "2022-05-10", "b" : "2022-05-11" } - - No - - - - - - json_object_agg - - json_object_agg ( key - "any", value - "any" - ORDER BY input_sort_columns ) - json - - - - jsonb_object_agg - - jsonb_object_agg ( key - "any", value - "any" - ORDER BY input_sort_columns ) - jsonb - - - Collects all the key/value pairs into a JSON object. Key arguments - are coerced to text; value arguments are converted as per - to_json or to_jsonb. - Values can be null, but keys cannot. - - No - - - - - - json_object_agg_strict - - json_object_agg_strict ( - key "any", - value "any" ) - json - - - - jsonb_object_agg_strict - - jsonb_object_agg_strict ( - key "any", - value "any" ) - jsonb - - - Collects all the key/value pairs into a JSON object. Key arguments - are coerced to text; value arguments are converted as per - to_json or to_jsonb. - The key can not be null. If the - value is null then the entry is skipped, - - No - - - - - - json_object_agg_unique - - json_object_agg_unique ( - key "any", - value "any" ) - json - - - - jsonb_object_agg_unique - - jsonb_object_agg_unique ( - key "any", - value "any" ) - jsonb - - - Collects all the key/value pairs into a JSON object. Key arguments - are coerced to text; value arguments are converted as per - to_json or to_jsonb. - Values can be null, but keys cannot. - If there is a duplicate key an error is thrown. - - No - - - - - - json_object_agg_unique_strict - - json_object_agg_unique_strict ( - key "any", - value "any" ) - json - - - - jsonb_object_agg_unique_strict - - jsonb_object_agg_unique_strict ( - key "any", - value "any" ) - jsonb - - - Collects all the key/value pairs into a JSON object. Key arguments - are coerced to text; value arguments are converted as per - to_json or to_jsonb. - The key can not be null. If the - value is null then the entry is skipped. - If there is a duplicate key an error is thrown. - - No - - - - - - max - - max ( see text ) - same as input type - - - Computes the maximum of the non-null input - values. Available for any numeric, string, date/time, or enum type, - as well as bytea, inet, interval, - money, oid, pg_lsn, - tid, xid8, - and also arrays and composite types containing sortable data types. - - Yes - - - - - - min - - min ( see text ) - same as input type - - - Computes the minimum of the non-null input - values. Available for any numeric, string, date/time, or enum type, - as well as bytea, inet, interval, - money, oid, pg_lsn, - tid, xid8, - and also arrays and composite types containing sortable data types. - - Yes - - - - - - range_agg - - range_agg ( value - anyrange ) - anymultirange - - - range_agg ( value - anymultirange ) - anymultirange - - - Computes the union of the non-null input values. - - No - - - - - - range_intersect_agg - - range_intersect_agg ( value - anyrange ) - anyrange - - - range_intersect_agg ( value - anymultirange ) - anymultirange - - - Computes the intersection of the non-null input values. - - No - - - - - - string_agg - - string_agg ( value - text, delimiter text ) - text - - - string_agg ( value - bytea, delimiter bytea - ORDER BY input_sort_columns ) - bytea - - - Concatenates the non-null input values into a string. Each value - after the first is preceded by the - corresponding delimiter (if it's not null). - - Yes - - - - - - sum - - sum ( smallint ) - bigint - - - sum ( integer ) - bigint - - - sum ( bigint ) - numeric - - - sum ( numeric ) - numeric - - - sum ( real ) - real - - - sum ( double precision ) - double precision - - - sum ( interval ) - interval - - - sum ( money ) - money - - - Computes the sum of the non-null input values. - - Yes - - - - - - xmlagg - - xmlagg ( xml ORDER BY input_sort_columns ) - xml - - - Concatenates the non-null XML input values (see - ). - - No - - - -
- - - It should be noted that except for count, - these functions return a null value when no rows are selected. In - particular, sum of no rows returns null, not - zero as one might expect, and array_agg - returns null rather than an empty array when there are no input - rows. The coalesce function can be used to - substitute zero or an empty array for null when necessary. - - - - The aggregate functions array_agg, - json_agg, jsonb_agg, - json_agg_strict, jsonb_agg_strict, - json_object_agg, jsonb_object_agg, - json_object_agg_strict, jsonb_object_agg_strict, - json_object_agg_unique, jsonb_object_agg_unique, - json_object_agg_unique_strict, - jsonb_object_agg_unique_strict, - string_agg, - and xmlagg, as well as similar user-defined - aggregate functions, produce meaningfully different result values - depending on the order of the input values. This ordering is - unspecified by default, but can be controlled by writing an - ORDER BY clause within the aggregate call, as shown in - . - Alternatively, supplying the input values from a sorted subquery - will usually work. For example: - - - - Beware that this approach can fail if the outer query level contains - additional processing, such as a join, because that might cause the - subquery's output to be reordered before the aggregate is computed. - - - - - ANY - - - SOME - - - The boolean aggregates bool_and and - bool_or correspond to the standard SQL aggregates - every and any or - some. - PostgreSQL - supports every, but not any - or some, because there is an ambiguity built into - the standard syntax: - -SELECT b1 = ANY((SELECT b2 FROM t2 ...)) FROM t1 ...; - - Here ANY can be considered either as introducing - a subquery, or as being an aggregate function, if the subquery - returns one row with a Boolean value. - Thus the standard name cannot be given to these aggregates. - - - - - - Users accustomed to working with other SQL database management - systems might be disappointed by the performance of the - count aggregate when it is applied to the - entire table. A query like: - -SELECT count(*) FROM sometable; - - will require effort proportional to the size of the table: - PostgreSQL will need to scan either the - entire table or the entirety of an index that includes all rows in - the table. - - - - - shows - aggregate functions typically used in statistical analysis. - (These are separated out merely to avoid cluttering the listing - of more-commonly-used aggregates.) Functions shown as - accepting numeric_type are available for all - the types smallint, integer, - bigint, numeric, real, - and double precision. - Where the description mentions - N, it means the - number of input rows for which all the input expressions are non-null. - In all cases, null is returned if the computation is meaningless, - for example when N is zero. - - - - statistics - - - linear regression - - - - Aggregate Functions for Statistics - - - - - - - Function - - - Description - - Partial Mode - - - - - - - - correlation - - - corr - - corr ( Y double precision, X double precision ) - double precision - - - Computes the correlation coefficient. - - Yes - - - - - - covariance - population - - - covar_pop - - covar_pop ( Y double precision, X double precision ) - double precision - - - Computes the population covariance. - - Yes - - - - - - covariance - sample - - - covar_samp - - covar_samp ( Y double precision, X double precision ) - double precision - - - Computes the sample covariance. - - Yes - - - - - - regr_avgx - - regr_avgx ( Y double precision, X double precision ) - double precision - - - Computes the average of the independent variable, - sum(X)/N. - - Yes - - - - - - regr_avgy - - regr_avgy ( Y double precision, X double precision ) - double precision - - - Computes the average of the dependent variable, - sum(Y)/N. - - Yes - - - - - - regr_count - - regr_count ( Y double precision, X double precision ) - bigint - - - Computes the number of rows in which both inputs are non-null. - - Yes - - - - - - regression intercept - - - regr_intercept - - regr_intercept ( Y double precision, X double precision ) - double precision - - - Computes the y-intercept of the least-squares-fit linear equation - determined by the - (X, Y) pairs. - - Yes - - - - - - regr_r2 - - regr_r2 ( Y double precision, X double precision ) - double precision - - - Computes the square of the correlation coefficient. - - Yes - - - - - - regression slope - - - regr_slope - - regr_slope ( Y double precision, X double precision ) - double precision - - - Computes the slope of the least-squares-fit linear equation determined - by the (X, Y) - pairs. - - Yes - - - - - - regr_sxx - - regr_sxx ( Y double precision, X double precision ) - double precision - - - Computes the sum of squares of the independent - variable, - sum(X^2) - sum(X)^2/N. - - Yes - - - - - - regr_sxy - - regr_sxy ( Y double precision, X double precision ) - double precision - - - Computes the sum of products of independent times - dependent variables, - sum(X*Y) - sum(X) * sum(Y)/N. - - Yes - - - - - - regr_syy - - regr_syy ( Y double precision, X double precision ) - double precision - - - Computes the sum of squares of the dependent - variable, - sum(Y^2) - sum(Y)^2/N. - - Yes - - - - - - standard deviation - - - stddev - - stddev ( numeric_type ) - double precision - for real or double precision, - otherwise numeric - - - This is a historical alias for stddev_samp. - - Yes - - - - - - standard deviation - population - - - stddev_pop - - stddev_pop ( numeric_type ) - double precision - for real or double precision, - otherwise numeric - - - Computes the population standard deviation of the input values. - - Yes - - - - - - standard deviation - sample - - - stddev_samp - - stddev_samp ( numeric_type ) - double precision - for real or double precision, - otherwise numeric - - - Computes the sample standard deviation of the input values. - - Yes - - - - - - variance - - variance ( numeric_type ) - double precision - for real or double precision, - otherwise numeric - - - This is a historical alias for var_samp. - - Yes - - - - - - variance - population - - - var_pop - - var_pop ( numeric_type ) - double precision - for real or double precision, - otherwise numeric - - - Computes the population variance of the input values (square of the - population standard deviation). - - Yes - - - - - - variance - sample - - - var_samp - - var_samp ( numeric_type ) - double precision - for real or double precision, - otherwise numeric - - - Computes the sample variance of the input values (square of the sample - standard deviation). - - Yes - - - -
- - - shows some - aggregate functions that use the ordered-set aggregate - syntax. These functions are sometimes referred to as inverse - distribution functions. Their aggregated input is introduced by - ORDER BY, and they may also take a direct - argument that is not aggregated, but is computed only once. - All these functions ignore null values in their aggregated input. - For those that take a fraction parameter, the - fraction value must be between 0 and 1; an error is thrown if not. - However, a null fraction value simply produces a - null result. - - - - ordered-set aggregate - built-in - - - inverse distribution - - - - Ordered-Set Aggregate Functions - - - - - - - Function - - - Description - - Partial Mode - - - - - - - - mode - statistical - - mode () WITHIN GROUP ( ORDER BY anyelement ) - anyelement - - - Computes the mode, the most frequent - value of the aggregated argument (arbitrarily choosing the first one - if there are multiple equally-frequent values). The aggregated - argument must be of a sortable type. - - No - - - - - - percentile - continuous - - percentile_cont ( fraction double precision ) WITHIN GROUP ( ORDER BY double precision ) - double precision - - - percentile_cont ( fraction double precision ) WITHIN GROUP ( ORDER BY interval ) - interval - - - Computes the continuous percentile, a value - corresponding to the specified fraction - within the ordered set of aggregated argument values. This will - interpolate between adjacent input items if needed. - - No - - - - - percentile_cont ( fractions double precision[] ) WITHIN GROUP ( ORDER BY double precision ) - double precision[] - - - percentile_cont ( fractions double precision[] ) WITHIN GROUP ( ORDER BY interval ) - interval[] - - - Computes multiple continuous percentiles. The result is an array of - the same dimensions as the fractions - parameter, with each non-null element replaced by the (possibly - interpolated) value corresponding to that percentile. - - No - - - - - - percentile - discrete - - percentile_disc ( fraction double precision ) WITHIN GROUP ( ORDER BY anyelement ) - anyelement - - - Computes the discrete percentile, the first - value within the ordered set of aggregated argument values whose - position in the ordering equals or exceeds the - specified fraction. The aggregated - argument must be of a sortable type. - - No - - - - - percentile_disc ( fractions double precision[] ) WITHIN GROUP ( ORDER BY anyelement ) - anyarray - - - Computes multiple discrete percentiles. The result is an array of the - same dimensions as the fractions parameter, - with each non-null element replaced by the input value corresponding - to that percentile. - The aggregated argument must be of a sortable type. - - No - - - -
- - - hypothetical-set aggregate - built-in - - - - Each of the hypothetical-set aggregates listed in - is associated with a - window function of the same name defined in - . In each case, the aggregate's result - is the value that the associated window function would have - returned for the hypothetical row constructed from - args, if such a row had been added to the sorted - group of rows represented by the sorted_args. - For each of these functions, the list of direct arguments - given in args must match the number and types of - the aggregated arguments given in sorted_args. - Unlike most built-in aggregates, these aggregates are not strict, that is - they do not drop input rows containing nulls. Null values sort according - to the rule specified in the ORDER BY clause. - - - - Hypothetical-Set Aggregate Functions - - - - - - - Function - - - Description - - Partial Mode - - - - - - - - rank - hypothetical - - rank ( args ) WITHIN GROUP ( ORDER BY sorted_args ) - bigint - - - Computes the rank of the hypothetical row, with gaps; that is, the row - number of the first row in its peer group. - - No - - - - - - dense_rank - hypothetical - - dense_rank ( args ) WITHIN GROUP ( ORDER BY sorted_args ) - bigint - - - Computes the rank of the hypothetical row, without gaps; this function - effectively counts peer groups. - - No - - - - - - percent_rank - hypothetical - - percent_rank ( args ) WITHIN GROUP ( ORDER BY sorted_args ) - double precision - - - Computes the relative rank of the hypothetical row, that is - (rank - 1) / (total rows - 1). - The value thus ranges from 0 to 1 inclusive. - - No - - - - - - cume_dist - hypothetical - - cume_dist ( args ) WITHIN GROUP ( ORDER BY sorted_args ) - double precision - - - Computes the cumulative distribution, that is (number of rows - preceding or peers with hypothetical row) / (total rows). The value - thus ranges from 1/N to 1. - - No - - - -
- - - Grouping Operations - - - - - Function - - - Description - - - - - - - - - GROUPING - - GROUPING ( group_by_expression(s) ) - integer - - - Returns a bit mask indicating which GROUP BY - expressions are not included in the current grouping set. - Bits are assigned with the rightmost argument corresponding to the - least-significant bit; each bit is 0 if the corresponding expression - is included in the grouping criteria of the grouping set generating - the current result row, and 1 if it is not included. - - - - -
- - - The grouping operations shown in - are used in conjunction with - grouping sets (see ) to distinguish - result rows. The arguments to the GROUPING function - are not actually evaluated, but they must exactly match expressions given - in the GROUP BY clause of the associated query level. - For example: - -=> SELECT * FROM items_sold; - make | model | sales --------+-------+------- - Foo | GT | 10 - Foo | Tour | 20 - Bar | City | 15 - Bar | Sport | 5 -(4 rows) - -=> SELECT make, model, GROUPING(make,model), sum(sales) FROM items_sold GROUP BY ROLLUP(make,model); - make | model | grouping | sum --------+-------+----------+----- - Foo | GT | 0 | 10 - Foo | Tour | 0 | 20 - Bar | City | 0 | 15 - Bar | Sport | 0 | 5 - Foo | | 1 | 30 - Bar | | 1 | 20 - | | 3 | 50 -(7 rows) - - Here, the grouping value 0 in the - first four rows shows that those have been grouped normally, over both the - grouping columns. The value 1 indicates - that model was not grouped by in the next-to-last two - rows, and the value 3 indicates that - neither make nor model was grouped - by in the last row (which therefore is an aggregate over all the input - rows). - - -
- - - Window Functions - - - window function - built-in - - - - Window functions provide the ability to perform - calculations across sets of rows that are related to the current query - row. See for an introduction to this - feature, and for syntax - details. - - - - The built-in window functions are listed in - . Note that these functions - must be invoked using window function syntax, i.e., an - OVER clause is required. - - - - In addition to these functions, any built-in or user-defined - ordinary aggregate (i.e., not ordered-set or hypothetical-set aggregates) - can be used as a window function; see - for a list of the built-in aggregates. - Aggregate functions act as window functions only when an OVER - clause follows the call; otherwise they act as plain aggregates - and return a single row for the entire set. - - - - General-Purpose Window Functions - - - - - Function - - - Description - - - - - - - - - row_number - - row_number () - bigint - - - Returns the number of the current row within its partition, counting - from 1. - - - - - - - rank - - rank () - bigint - - - Returns the rank of the current row, with gaps; that is, - the row_number of the first row in its peer - group. - - - - - - - dense_rank - - dense_rank () - bigint - - - Returns the rank of the current row, without gaps; this function - effectively counts peer groups. - - - - - - - percent_rank - - percent_rank () - double precision - - - Returns the relative rank of the current row, that is - (rank - 1) / (total partition rows - 1). - The value thus ranges from 0 to 1 inclusive. - - - - - - - cume_dist - - cume_dist () - double precision - - - Returns the cumulative distribution, that is (number of partition rows - preceding or peers with current row) / (total partition rows). - The value thus ranges from 1/N to 1. - - - - - - - ntile - - ntile ( num_buckets integer ) - integer - - - Returns an integer ranging from 1 to the argument value, dividing the - partition as equally as possible. - - - - - - - lag - - lag ( value anycompatible - , offset integer - , default anycompatible ) - anycompatible - - - Returns value evaluated at - the row that is offset - rows before the current row within the partition; if there is no such - row, instead returns default - (which must be of a type compatible with - value). - Both offset and - default are evaluated - with respect to the current row. If omitted, - offset defaults to 1 and - default to NULL. - - - - - - - lead - - lead ( value anycompatible - , offset integer - , default anycompatible ) - anycompatible - - - Returns value evaluated at - the row that is offset - rows after the current row within the partition; if there is no such - row, instead returns default - (which must be of a type compatible with - value). - Both offset and - default are evaluated - with respect to the current row. If omitted, - offset defaults to 1 and - default to NULL. - - - - - - - first_value - - first_value ( value anyelement ) - anyelement - - - Returns value evaluated - at the row that is the first row of the window frame. - - - - - - - last_value - - last_value ( value anyelement ) - anyelement - - - Returns value evaluated - at the row that is the last row of the window frame. - - - - - - - nth_value - - nth_value ( value anyelement, n integer ) - anyelement - - - Returns value evaluated - at the row that is the n'th - row of the window frame (counting from 1); - returns NULL if there is no such row. - - - - -
- - - All of the functions listed in - depend on the sort ordering - specified by the ORDER BY clause of the associated window - definition. Rows that are not distinct when considering only the - ORDER BY columns are said to be peers. - The four ranking functions (including cume_dist) are - defined so that they give the same answer for all rows of a peer group. - - - - Note that first_value, last_value, and - nth_value consider only the rows within the window - frame, which by default contains the rows from the start of the - partition through the last peer of the current row. This is - likely to give unhelpful results for last_value and - sometimes also nth_value. You can redefine the frame by - adding a suitable frame specification (RANGE, - ROWS or GROUPS) to - the OVER clause. - See for more information - about frame specifications. - - - - When an aggregate function is used as a window function, it aggregates - over the rows within the current row's window frame. - An aggregate used with ORDER BY and the default window frame - definition produces a running sum type of behavior, which may or - may not be what's wanted. To obtain - aggregation over the whole partition, omit ORDER BY or use - ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. - Other frame specifications can be used to obtain other effects. - - - - - The SQL standard defines a RESPECT NULLS or - IGNORE NULLS option for lead, lag, - first_value, last_value, and - nth_value. This is not implemented in - PostgreSQL: the behavior is always the - same as the standard's default, namely RESPECT NULLS. - Likewise, the standard's FROM FIRST or FROM LAST - option for nth_value is not implemented: only the - default FROM FIRST behavior is supported. (You can achieve - the result of FROM LAST by reversing the ORDER BY - ordering.) - - - -
- - - Merge Support Functions - - - MERGE - RETURNING - - - - PostgreSQL includes one merge support function - that may be used in the RETURNING list of a - command to identify the action taken for each - row; see . - - - - Merge Support Functions - - - - - - Function - - - Description - - - - - - - - - merge_action - - merge_action ( ) - text - - - Returns the merge action command executed for the current row. This - will be 'INSERT', 'UPDATE', or - 'DELETE'. - - - - -
- - - Example: - 0 THEN - UPDATE SET in_stock = true, quantity = s.quantity - WHEN MATCHED THEN - UPDATE SET in_stock = false, quantity = 0 - WHEN NOT MATCHED THEN - INSERT (product_id, in_stock, quantity) - VALUES (s.product_id, true, s.quantity) - RETURNING merge_action(), p.*; - - merge_action | product_id | in_stock | quantity ---------------+------------+----------+---------- - UPDATE | 1001 | t | 50 - UPDATE | 1002 | f | 0 - INSERT | 1003 | t | 10 -]]> - - - - Note that this function can only be used in the RETURNING - list of a MERGE command. It is an error to use it in any - other part of a query. - - -
- - - Subquery Expressions - - - EXISTS - - - - IN - - - - NOT IN - - - - ANY - - - - ALL - - - - SOME - - - - subquery - - - - This section describes the SQL-compliant subquery - expressions available in PostgreSQL. - All of the expression forms documented in this section return - Boolean (true/false) results. - - - - <literal>EXISTS</literal> - - -EXISTS (subquery) - - - - The argument of EXISTS is an arbitrary SELECT statement, - or subquery. The - subquery is evaluated to determine whether it returns any rows. - If it returns at least one row, the result of EXISTS is - true; if the subquery returns no rows, the result of EXISTS - is false. - - - - The subquery can refer to variables from the surrounding query, - which will act as constants during any one evaluation of the subquery. - - - - The subquery will generally only be executed long enough to determine - whether at least one row is returned, not all the way to completion. - It is unwise to write a subquery that has side effects (such as - calling sequence functions); whether the side effects occur - might be unpredictable. - - - - Since the result depends only on whether any rows are returned, - and not on the contents of those rows, the output list of the - subquery is normally unimportant. A common coding convention is - to write all EXISTS tests in the form - EXISTS(SELECT 1 WHERE ...). There are exceptions to - this rule however, such as subqueries that use INTERSECT. - - - - This simple example is like an inner join on col2, but - it produces at most one output row for each tab1 row, - even if there are several matching tab2 rows: - -SELECT col1 -FROM tab1 -WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2); - - - - - - <literal>IN</literal> - - -expression IN (subquery) - - - - The right-hand side is a parenthesized - subquery, which must return exactly one column. The left-hand expression - is evaluated and compared to each row of the subquery result. - The result of IN is true if any equal subquery row is found. - The result is false if no equal row is found (including the - case where the subquery returns no rows). - - - - Note that if the left-hand expression yields null, or if there are - no equal right-hand values and at least one right-hand row yields - null, the result of the IN construct will be null, not false. - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - As with EXISTS, it's unwise to assume that the subquery will - be evaluated completely. - - - -row_constructor IN (subquery) - - - - The left-hand side of this form of IN is a row constructor, - as described in . - The right-hand side is a parenthesized - subquery, which must return exactly as many columns as there are - expressions in the left-hand row. The left-hand expressions are - evaluated and compared row-wise to each row of the subquery result. - The result of IN is true if any equal subquery row is found. - The result is false if no equal row is found (including the - case where the subquery returns no rows). - - - - As usual, null values in the rows are combined per - the normal rules of SQL Boolean expressions. Two rows are considered - equal if all their corresponding members are non-null and equal; the rows - are unequal if any corresponding members are non-null and unequal; - otherwise the result of that row comparison is unknown (null). - If all the per-row results are either unequal or null, with at least one - null, then the result of IN is null. - - - - - <literal>NOT IN</literal> - - -expression NOT IN (subquery) - - - - The right-hand side is a parenthesized - subquery, which must return exactly one column. The left-hand expression - is evaluated and compared to each row of the subquery result. - The result of NOT IN is true if only unequal subquery rows - are found (including the case where the subquery returns no rows). - The result is false if any equal row is found. - - - - Note that if the left-hand expression yields null, or if there are - no equal right-hand values and at least one right-hand row yields - null, the result of the NOT IN construct will be null, not true. - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - As with EXISTS, it's unwise to assume that the subquery will - be evaluated completely. - - - -row_constructor NOT IN (subquery) - - - - The left-hand side of this form of NOT IN is a row constructor, - as described in . - The right-hand side is a parenthesized - subquery, which must return exactly as many columns as there are - expressions in the left-hand row. The left-hand expressions are - evaluated and compared row-wise to each row of the subquery result. - The result of NOT IN is true if only unequal subquery rows - are found (including the case where the subquery returns no rows). - The result is false if any equal row is found. - - - - As usual, null values in the rows are combined per - the normal rules of SQL Boolean expressions. Two rows are considered - equal if all their corresponding members are non-null and equal; the rows - are unequal if any corresponding members are non-null and unequal; - otherwise the result of that row comparison is unknown (null). - If all the per-row results are either unequal or null, with at least one - null, then the result of NOT IN is null. - - - - - <literal>ANY</literal>/<literal>SOME</literal> - - -expression operator ANY (subquery) -expression operator SOME (subquery) - - - - The right-hand side is a parenthesized - subquery, which must return exactly one column. The left-hand expression - is evaluated and compared to each row of the subquery result using the - given operator, which must yield a Boolean - result. - The result of ANY is true if any true result is obtained. - The result is false if no true result is found (including the - case where the subquery returns no rows). - - - - SOME is a synonym for ANY. - IN is equivalent to = ANY. - - - - Note that if there are no successes and at least one right-hand row yields - null for the operator's result, the result of the ANY construct - will be null, not false. - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - As with EXISTS, it's unwise to assume that the subquery will - be evaluated completely. - - - -row_constructor operator ANY (subquery) -row_constructor operator SOME (subquery) - - - - The left-hand side of this form of ANY is a row constructor, - as described in . - The right-hand side is a parenthesized - subquery, which must return exactly as many columns as there are - expressions in the left-hand row. The left-hand expressions are - evaluated and compared row-wise to each row of the subquery result, - using the given operator. - The result of ANY is true if the comparison - returns true for any subquery row. - The result is false if the comparison returns false for every - subquery row (including the case where the subquery returns no - rows). - The result is NULL if no comparison with a subquery row returns true, - and at least one comparison returns NULL. - - - - See for details about the meaning - of a row constructor comparison. - - - - - <literal>ALL</literal> - - -expression operator ALL (subquery) - - - - The right-hand side is a parenthesized - subquery, which must return exactly one column. The left-hand expression - is evaluated and compared to each row of the subquery result using the - given operator, which must yield a Boolean - result. - The result of ALL is true if all rows yield true - (including the case where the subquery returns no rows). - The result is false if any false result is found. - The result is NULL if no comparison with a subquery row returns false, - and at least one comparison returns NULL. - - - - NOT IN is equivalent to <> ALL. - - - - As with EXISTS, it's unwise to assume that the subquery will - be evaluated completely. - - - -row_constructor operator ALL (subquery) - - - - The left-hand side of this form of ALL is a row constructor, - as described in . - The right-hand side is a parenthesized - subquery, which must return exactly as many columns as there are - expressions in the left-hand row. The left-hand expressions are - evaluated and compared row-wise to each row of the subquery result, - using the given operator. - The result of ALL is true if the comparison - returns true for all subquery rows (including the - case where the subquery returns no rows). - The result is false if the comparison returns false for any - subquery row. - The result is NULL if no comparison with a subquery row returns false, - and at least one comparison returns NULL. - - - - See for details about the meaning - of a row constructor comparison. - - - - - Single-Row Comparison - - - comparison - subquery result row - - - -row_constructor operator (subquery) - - - - The left-hand side is a row constructor, - as described in . - The right-hand side is a parenthesized subquery, which must return exactly - as many columns as there are expressions in the left-hand row. Furthermore, - the subquery cannot return more than one row. (If it returns zero rows, - the result is taken to be null.) The left-hand side is evaluated and - compared row-wise to the single subquery result row. - - - - See for details about the meaning - of a row constructor comparison. - - - - - - - Row and Array Comparisons - - - IN - - - - NOT IN - - - - ANY - - - - ALL - - - - SOME - - - - composite type - comparison - - - - row-wise comparison - - - - comparison - composite type - - - - comparison - row constructor - - - - IS DISTINCT FROM - - - - IS NOT DISTINCT FROM - - - - This section describes several specialized constructs for making - multiple comparisons between groups of values. These forms are - syntactically related to the subquery forms of the previous section, - but do not involve subqueries. - The forms involving array subexpressions are - PostgreSQL extensions; the rest are - SQL-compliant. - All of the expression forms documented in this section return - Boolean (true/false) results. - - - - <literal>IN</literal> - - -expression IN (value , ...) - - - - The right-hand side is a parenthesized list - of expressions. The result is true if the left-hand expression's - result is equal to any of the right-hand expressions. This is a shorthand - notation for - - -expression = value1 -OR -expression = value2 -OR -... - - - - - Note that if the left-hand expression yields null, or if there are - no equal right-hand values and at least one right-hand expression yields - null, the result of the IN construct will be null, not false. - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - - <literal>NOT IN</literal> - - -expression NOT IN (value , ...) - - - - The right-hand side is a parenthesized list - of expressions. The result is true if the left-hand expression's - result is unequal to all of the right-hand expressions. This is a shorthand - notation for - - -expression <> value1 -AND -expression <> value2 -AND -... - - - - - Note that if the left-hand expression yields null, or if there are - no equal right-hand values and at least one right-hand expression yields - null, the result of the NOT IN construct will be null, not true - as one might naively expect. - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - - x NOT IN y is equivalent to NOT (x IN y) in all - cases. However, null values are much more likely to trip up the novice when - working with NOT IN than when working with IN. - It is best to express your condition positively if possible. - - - - - - <literal>ANY</literal>/<literal>SOME</literal> (array) - - -expression operator ANY (array expression) -expression operator SOME (array expression) - - - - The right-hand side is a parenthesized expression, which must yield an - array value. - The left-hand expression - is evaluated and compared to each element of the array using the - given operator, which must yield a Boolean - result. - The result of ANY is true if any true result is obtained. - The result is false if no true result is found (including the - case where the array has zero elements). - - - - If the array expression yields a null array, the result of - ANY will be null. If the left-hand expression yields null, - the result of ANY is ordinarily null (though a non-strict - comparison operator could possibly yield a different result). - Also, if the right-hand array contains any null elements and no true - comparison result is obtained, the result of ANY - will be null, not false (again, assuming a strict comparison operator). - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - SOME is a synonym for ANY. - - - - - <literal>ALL</literal> (array) - - -expression operator ALL (array expression) - - - - The right-hand side is a parenthesized expression, which must yield an - array value. - The left-hand expression - is evaluated and compared to each element of the array using the - given operator, which must yield a Boolean - result. - The result of ALL is true if all comparisons yield true - (including the case where the array has zero elements). - The result is false if any false result is found. - - - - If the array expression yields a null array, the result of - ALL will be null. If the left-hand expression yields null, - the result of ALL is ordinarily null (though a non-strict - comparison operator could possibly yield a different result). - Also, if the right-hand array contains any null elements and no false - comparison result is obtained, the result of ALL - will be null, not true (again, assuming a strict comparison operator). - This is in accordance with SQL's normal rules for Boolean combinations - of null values. - - - - - Row Constructor Comparison - - -row_constructor operator row_constructor - - - - Each side is a row constructor, - as described in . - The two row constructors must have the same number of fields. - The given operator is applied to each pair - of corresponding fields. (Since the fields could be of different - types, this means that a different specific operator could be selected - for each pair.) - All the selected operators must be members of some B-tree operator - class, or be the negator of an = member of a B-tree - operator class, meaning that row constructor comparison is only - possible when the operator is - =, - <>, - <, - <=, - >, or - >=, - or has semantics similar to one of these. - - - - The = and <> cases work slightly differently - from the others. Two rows are considered - equal if all their corresponding members are non-null and equal; the rows - are unequal if any corresponding members are non-null and unequal; - otherwise the result of the row comparison is unknown (null). - - - - For the <, <=, > and - >= cases, the row elements are compared left-to-right, - stopping as soon as an unequal or null pair of elements is found. - If either of this pair of elements is null, the result of the - row comparison is unknown (null); otherwise comparison of this pair - of elements determines the result. For example, - ROW(1,2,NULL) < ROW(1,3,0) - yields true, not null, because the third pair of elements are not - considered. - - - -row_constructor IS DISTINCT FROM row_constructor - - - - This construct is similar to a <> row comparison, - but it does not yield null for null inputs. Instead, any null value is - considered unequal to (distinct from) any non-null value, and any two - nulls are considered equal (not distinct). Thus the result will - either be true or false, never null. - - - -row_constructor IS NOT DISTINCT FROM row_constructor - - - - This construct is similar to a = row comparison, - but it does not yield null for null inputs. Instead, any null value is - considered unequal to (distinct from) any non-null value, and any two - nulls are considered equal (not distinct). Thus the result will always - be either true or false, never null. - - - - - - Composite Type Comparison - - -record operator record - - - - The SQL specification requires row-wise comparison to return NULL if the - result depends on comparing two NULL values or a NULL and a non-NULL. - PostgreSQL does this only when comparing the - results of two row constructors (as in - ) or comparing a row constructor - to the output of a subquery (as in ). - In other contexts where two composite-type values are compared, two - NULL field values are considered equal, and a NULL is considered larger - than a non-NULL. This is necessary in order to have consistent sorting - and indexing behavior for composite types. - - - - Each side is evaluated and they are compared row-wise. Composite type - comparisons are allowed when the operator is - =, - <>, - <, - <=, - > or - >=, - or has semantics similar to one of these. (To be specific, an operator - can be a row comparison operator if it is a member of a B-tree operator - class, or is the negator of the = member of a B-tree operator - class.) The default behavior of the above operators is the same as for - IS [ NOT ] DISTINCT FROM for row constructors (see - ). - - - - To support matching of rows which include elements without a default - B-tree operator class, the following operators are defined for composite - type comparison: - *=, - *<>, - *<, - *<=, - *>, and - *>=. - These operators compare the internal binary representation of the two - rows. Two rows might have a different binary representation even - though comparisons of the two rows with the equality operator is true. - The ordering of rows under these comparison operators is deterministic - but not otherwise meaningful. These operators are used internally - for materialized views and might be useful for other specialized - purposes such as replication and B-Tree deduplication (see ). They are not intended to be - generally useful for writing queries, though. - - - - - - Set Returning Functions - - - set returning functions - functions - - - - This section describes functions that possibly return more than one row. - The most widely used functions in this class are series generating - functions, as detailed in and - . Other, more specialized - set-returning functions are described elsewhere in this manual. - See for ways to combine multiple - set-returning functions. - - - - Series Generating Functions - - - - - Function - - - Description - - - - - - - - - generate_series - - generate_series ( start integer, stop integer , step integer ) - setof integer - - - generate_series ( start bigint, stop bigint , step bigint ) - setof bigint - - - generate_series ( start numeric, stop numeric , step numeric ) - setof numeric - - - Generates a series of values from start - to stop, with a step size - of step. step - defaults to 1. - - - - - - generate_series ( start timestamp, stop timestamp, step interval ) - setof timestamp - - - generate_series ( start timestamp with time zone, stop timestamp with time zone, step interval , timezone text ) - setof timestamp with time zone - - - Generates a series of values from start - to stop, with a step size - of step. - In the timezone-aware form, times of day and daylight-savings - adjustments are computed according to the time zone named by - the timezone argument, or the current - setting if that is omitted. - - - - -
- - - When step is positive, zero rows are returned if - start is greater than stop. - Conversely, when step is negative, zero rows are - returned if start is less than stop. - Zero rows are also returned if any input is NULL. - It is an error - for step to be zero. Some examples follow: - -SELECT * FROM generate_series(2,4); - generate_series ------------------ - 2 - 3 - 4 -(3 rows) - -SELECT * FROM generate_series(5,1,-2); - generate_series ------------------ - 5 - 3 - 1 -(3 rows) - -SELECT * FROM generate_series(4,3); - generate_series ------------------ -(0 rows) - -SELECT generate_series(1.1, 4, 1.3); - generate_series ------------------ - 1.1 - 2.4 - 3.7 -(3 rows) - --- this example relies on the date-plus-integer operator: -SELECT current_date + s.a AS dates FROM generate_series(0,14,7) AS s(a); - dates ------------- - 2004-02-05 - 2004-02-12 - 2004-02-19 -(3 rows) - -SELECT * FROM generate_series('2008-03-01 00:00'::timestamp, - '2008-03-04 12:00', '10 hours'); - generate_series ---------------------- - 2008-03-01 00:00:00 - 2008-03-01 10:00:00 - 2008-03-01 20:00:00 - 2008-03-02 06:00:00 - 2008-03-02 16:00:00 - 2008-03-03 02:00:00 - 2008-03-03 12:00:00 - 2008-03-03 22:00:00 - 2008-03-04 08:00:00 -(9 rows) - --- this example assumes that TimeZone is set to UTC; note the DST transition: -SELECT * FROM generate_series('2001-10-22 00:00 -04:00'::timestamptz, - '2001-11-01 00:00 -05:00'::timestamptz, - '1 day'::interval, 'America/New_York'); - generate_series ------------------------- - 2001-10-22 04:00:00+00 - 2001-10-23 04:00:00+00 - 2001-10-24 04:00:00+00 - 2001-10-25 04:00:00+00 - 2001-10-26 04:00:00+00 - 2001-10-27 04:00:00+00 - 2001-10-28 04:00:00+00 - 2001-10-29 05:00:00+00 - 2001-10-30 05:00:00+00 - 2001-10-31 05:00:00+00 - 2001-11-01 05:00:00+00 -(11 rows) - - - - - Subscript Generating Functions - - - - - Function - - - Description - - - - - - - - - generate_subscripts - - generate_subscripts ( array anyarray, dim integer ) - setof integer - - - Generates a series comprising the valid subscripts of - the dim'th dimension of the given array. - - - - - - generate_subscripts ( array anyarray, dim integer, reverse boolean ) - setof integer - - - Generates a series comprising the valid subscripts of - the dim'th dimension of the given array. - When reverse is true, returns the series in - reverse order. - - - - -
- - - generate_subscripts is a convenience function that generates - the set of valid subscripts for the specified dimension of the given - array. - Zero rows are returned for arrays that do not have the requested dimension, - or if any input is NULL. - Some examples follow: - --- basic usage: -SELECT generate_subscripts('{NULL,1,NULL,2}'::int[], 1) AS s; - s ---- - 1 - 2 - 3 - 4 -(4 rows) - --- presenting an array, the subscript and the subscripted --- value requires a subquery: -SELECT * FROM arrays; - a --------------------- - {-1,-2} - {100,200,300} -(2 rows) - -SELECT a AS array, s AS subscript, a[s] AS value -FROM (SELECT generate_subscripts(a, 1) AS s, a FROM arrays) foo; - array | subscript | value ----------------+-----------+------- - {-1,-2} | 1 | -1 - {-1,-2} | 2 | -2 - {100,200,300} | 1 | 100 - {100,200,300} | 2 | 200 - {100,200,300} | 3 | 300 -(5 rows) - --- unnest a 2D array: -CREATE OR REPLACE FUNCTION unnest2(anyarray) -RETURNS SETOF anyelement AS $$ -select $1[i][j] - from generate_subscripts($1,1) g1(i), - generate_subscripts($1,2) g2(j); -$$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION -SELECT * FROM unnest2(ARRAY[[1,2],[3,4]]); - unnest2 ---------- - 1 - 2 - 3 - 4 -(4 rows) - - - - - ordinality - - - - When a function in the FROM clause is suffixed - by WITH ORDINALITY, a bigint column is - appended to the function's output column(s), which starts from 1 and - increments by 1 for each row of the function's output. - This is most useful in the case of set returning - functions such as unnest(). - - --- set returning function WITH ORDINALITY: -SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n); - ls | n ------------------+---- - pg_serial | 1 - pg_twophase | 2 - postmaster.opts | 3 - pg_notify | 4 - postgresql.conf | 5 - pg_tblspc | 6 - logfile | 7 - base | 8 - postmaster.pid | 9 - pg_ident.conf | 10 - global | 11 - pg_xact | 12 - pg_snapshots | 13 - pg_multixact | 14 - PG_VERSION | 15 - pg_wal | 16 - pg_hba.conf | 17 - pg_stat_tmp | 18 - pg_subtrans | 19 -(19 rows) - - - -
- - - System Information Functions and Operators - - - The functions described in this section are used to obtain various - information about a PostgreSQL installation. - - - - Session Information Functions - - - shows several - functions that extract session and system information. - - - - In addition to the functions listed in this section, there are a number of - functions related to the statistics system that also provide system - information. See for more - information. - - - - Session Information Functions - - - - - Function - - - Description - - - - - - - - - current_catalog - - current_catalog - name - - - - current_database - - current_database () - name - - - Returns the name of the current database. (Databases are - called catalogs in the SQL standard, - so current_catalog is the standard's - spelling.) - - - - - - - current_query - - current_query () - text - - - Returns the text of the currently executing query, as submitted - by the client (which might contain more than one statement). - - - - - - - current_role - - current_role - name - - - This is equivalent to current_user. - - - - - - - current_schema - - - schema - current - - current_schema - name - - - current_schema () - name - - - Returns the name of the schema that is first in the search path (or a - null value if the search path is empty). This is the schema that will - be used for any tables or other named objects that are created without - specifying a target schema. - - - - - - - current_schemas - - - search path - current - - current_schemas ( include_implicit boolean ) - name[] - - - Returns an array of the names of all schemas presently in the - effective search path, in their priority order. (Items in the current - setting that do not correspond to - existing, searchable schemas are omitted.) If the Boolean argument - is true, then implicitly-searched system schemas - such as pg_catalog are included in the result. - - - - - - - current_user - - - user - current - - current_user - name - - - Returns the user name of the current execution context. - - - - - - - inet_client_addr - - inet_client_addr () - inet - - - Returns the IP address of the current client, - or NULL if the current connection is via a - Unix-domain socket. - - - - - - - inet_client_port - - inet_client_port () - integer - - - Returns the IP port number of the current client, - or NULL if the current connection is via a - Unix-domain socket. - - - - - - - inet_server_addr - - inet_server_addr () - inet - - - Returns the IP address on which the server accepted the current - connection, - or NULL if the current connection is via a - Unix-domain socket. - - - - - - - inet_server_port - - inet_server_port () - integer - - - Returns the IP port number on which the server accepted the current - connection, - or NULL if the current connection is via a - Unix-domain socket. - - - - - - - pg_backend_pid - - pg_backend_pid () - integer - - - Returns the process ID of the server process attached to the current - session. - - - - - - - pg_blocking_pids - - pg_blocking_pids ( integer ) - integer[] - - - Returns an array of the process ID(s) of the sessions that are - blocking the server process with the specified process ID from - acquiring a lock, or an empty array if there is no such server process - or it is not blocked. - - - One server process blocks another if it either holds a lock that - conflicts with the blocked process's lock request (hard block), or is - waiting for a lock that would conflict with the blocked process's lock - request and is ahead of it in the wait queue (soft block). When using - parallel queries the result always lists client-visible process IDs - (that is, pg_backend_pid results) even if the - actual lock is held or awaited by a child worker process. As a result - of that, there may be duplicated PIDs in the result. Also note that - when a prepared transaction holds a conflicting lock, it will be - represented by a zero process ID. - - - Frequent calls to this function could have some impact on database - performance, because it needs exclusive access to the lock manager's - shared state for a short time. - - - - - - - pg_conf_load_time - - pg_conf_load_time () - timestamp with time zone - - - Returns the time when the server configuration files were last loaded. - If the current session was alive at the time, this will be the time - when the session itself re-read the configuration files (so the - reading will vary a little in different sessions). Otherwise it is - the time when the postmaster process re-read the configuration files. - - - - - - - pg_current_logfile - - - Logging - pg_current_logfile function - - - current_logfiles - and the pg_current_logfile function - - - Logging - current_logfiles file and the pg_current_logfile - function - - pg_current_logfile ( text ) - text - - - Returns the path name of the log file currently in use by the logging - collector. The path includes the - directory and the individual log file name. The result - is NULL if the logging collector is disabled. - When multiple log files exist, each in a different - format, pg_current_logfile without an argument - returns the path of the file having the first format found in the - ordered list: stderr, - csvlog, jsonlog. - NULL is returned if no log file has any of these - formats. - To request information about a specific log file format, supply - either csvlog, jsonlog or - stderr as the - value of the optional parameter. The result is NULL - if the log format requested is not configured in - . - The result reflects the contents of - the current_logfiles file. - - - This function is restricted to superusers and roles with privileges of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_get_loaded_modules - - pg_get_loaded_modules () - setof record - ( module_name text, - version text, - file_name text ) - - - Returns a list of the loadable modules that are loaded into the - current server session. The module_name - and version fields are NULL unless the - module author supplied values for them using - the PG_MODULE_MAGIC_EXT macro. - The file_name field gives the file - name of the module (shared library). - - - - - - - pg_my_temp_schema - - pg_my_temp_schema () - oid - - - Returns the OID of the current session's temporary schema, or zero if - it has none (because it has not created any temporary tables). - - - - - - - pg_is_other_temp_schema - - pg_is_other_temp_schema ( oid ) - boolean - - - Returns true if the given OID is the OID of another session's - temporary schema. (This can be useful, for example, to exclude other - sessions' temporary tables from a catalog display.) - - - - - - - pg_jit_available - - pg_jit_available () - boolean - - - Returns true if a JIT compiler extension is - available (see ) and the - configuration parameter is set to - on. - - - - - - - pg_numa_available - - pg_numa_available () - boolean - - - Returns true if the server has been compiled with NUMA support. - - - - - - - pg_listening_channels - - pg_listening_channels () - setof text - - - Returns the set of names of asynchronous notification channels that - the current session is listening to. - - - - - - - pg_notification_queue_usage - - pg_notification_queue_usage () - double precision - - - Returns the fraction (0–1) of the asynchronous notification - queue's maximum size that is currently occupied by notifications that - are waiting to be processed. - See and - for more information. - - - - - - - pg_postmaster_start_time - - pg_postmaster_start_time () - timestamp with time zone - - - Returns the time when the server started. - - - - - - - pg_safe_snapshot_blocking_pids - - pg_safe_snapshot_blocking_pids ( integer ) - integer[] - - - Returns an array of the process ID(s) of the sessions that are blocking - the server process with the specified process ID from acquiring a safe - snapshot, or an empty array if there is no such server process or it - is not blocked. - - - A session running a SERIALIZABLE transaction blocks - a SERIALIZABLE READ ONLY DEFERRABLE transaction - from acquiring a snapshot until the latter determines that it is safe - to avoid taking any predicate locks. See - for more information about - serializable and deferrable transactions. - - - Frequent calls to this function could have some impact on database - performance, because it needs access to the predicate lock manager's - shared state for a short time. - - - - - - - pg_trigger_depth - - pg_trigger_depth () - integer - - - Returns the current nesting level - of PostgreSQL triggers (0 if not called, - directly or indirectly, from inside a trigger). - - - - - - - session_user - - session_user - name - - - Returns the session user's name. - - - - - - - system_user - - system_user - text - - - Returns the authentication method and the identity (if any) that the - user presented during the authentication cycle before they were - assigned a database role. It is represented as - auth_method:identity or - NULL if the user has not been authenticated (for - example if Trust authentication has - been used). - - - - - - - user - - user - name - - - This is equivalent to current_user. - - - - -
- - - - current_catalog, - current_role, - current_schema, - current_user, - session_user, - and user have special syntactic status - in SQL: they must be called without trailing - parentheses. In PostgreSQL, parentheses can optionally be used with - current_schema, but not with the others. - - - - - The session_user is normally the user who initiated - the current database connection; but superusers can change this setting - with . - The current_user is the user identifier - that is applicable for permission checking. Normally it is equal - to the session user, but it can be changed with - . - It also changes during the execution of - functions with the attribute SECURITY DEFINER. - In Unix parlance, the session user is the real user and - the current user is the effective user. - current_role and user are - synonyms for current_user. (The SQL standard draws - a distinction between current_role - and current_user, but PostgreSQL - does not, since it unifies users and roles into a single kind of entity.) - - -
- - - Access Privilege Inquiry Functions - - - privilege - querying - - - - lists functions that - allow querying object access privileges programmatically. - (See for more information about - privileges.) - In these functions, the user whose privileges are being inquired about - can be specified by name or by OID - (pg_authid.oid), or if - the name is given as public then the privileges of the - PUBLIC pseudo-role are checked. Also, the user - argument can be omitted entirely, in which case - the current_user is assumed. - The object that is being inquired about can be specified either by name or - by OID, too. When specifying by name, a schema name can be included if - relevant. - The access privilege of interest is specified by a text string, which must - evaluate to one of the appropriate privilege keywords for the object's type - (e.g., SELECT). Optionally, WITH GRANT - OPTION can be added to a privilege type to test whether the - privilege is held with grant option. Also, multiple privilege types can be - listed separated by commas, in which case the result will be true if any of - the listed privileges is held. (Case of the privilege string is not - significant, and extra whitespace is allowed between but not within - privilege names.) - Some examples: - -SELECT has_table_privilege('myschema.mytable', 'select'); -SELECT has_table_privilege('joe', 'mytable', 'INSERT, SELECT WITH GRANT OPTION'); - - - - - Access Privilege Inquiry Functions - - - - - Function - - - Description - - - - - - - - - has_any_column_privilege - - has_any_column_privilege ( - user name or oid, - table text or oid, - privilege text ) - boolean - - - Does user have privilege for any column of table? - This succeeds either if the privilege is held for the whole table, or - if there is a column-level grant of the privilege for at least one - column. - Allowable privilege types are - SELECT, INSERT, - UPDATE, and REFERENCES. - - - - - - - has_column_privilege - - has_column_privilege ( - user name or oid, - table text or oid, - column text or smallint, - privilege text ) - boolean - - - Does user have privilege for the specified table column? - This succeeds either if the privilege is held for the whole table, or - if there is a column-level grant of the privilege for the column. - The column can be specified by name or by attribute number - (pg_attribute.attnum). - Allowable privilege types are - SELECT, INSERT, - UPDATE, and REFERENCES. - - - - - - - has_database_privilege - - has_database_privilege ( - user name or oid, - database text or oid, - privilege text ) - boolean - - - Does user have privilege for database? - Allowable privilege types are - CREATE, - CONNECT, - TEMPORARY, and - TEMP (which is equivalent to - TEMPORARY). - - - - - - - has_foreign_data_wrapper_privilege - - has_foreign_data_wrapper_privilege ( - user name or oid, - fdw text or oid, - privilege text ) - boolean - - - Does user have privilege for foreign-data wrapper? - The only allowable privilege type is USAGE. - - - - - - - has_function_privilege - - has_function_privilege ( - user name or oid, - function text or oid, - privilege text ) - boolean - - - Does user have privilege for function? - The only allowable privilege type is EXECUTE. - - - When specifying a function by name rather than by OID, the allowed - input is the same as for the regprocedure data type (see - ). - An example is: - -SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); - - - - - - - - has_language_privilege - - has_language_privilege ( - user name or oid, - language text or oid, - privilege text ) - boolean - - - Does user have privilege for language? - The only allowable privilege type is USAGE. - - - - - - - has_largeobject_privilege - - has_largeobject_privilege ( - user name or oid, - largeobject oid, - privilege text ) - boolean - - - Does user have privilege for large object? - Allowable privilege types are - SELECT and UPDATE. - - - - - - - has_parameter_privilege - - has_parameter_privilege ( - user name or oid, - parameter text, - privilege text ) - boolean - - - Does user have privilege for configuration parameter? - The parameter name is case-insensitive. - Allowable privilege types are SET - and ALTER SYSTEM. - - - - - - - has_schema_privilege - - has_schema_privilege ( - user name or oid, - schema text or oid, - privilege text ) - boolean - - - Does user have privilege for schema? - Allowable privilege types are - CREATE and - USAGE. - - - - - - - has_sequence_privilege - - has_sequence_privilege ( - user name or oid, - sequence text or oid, - privilege text ) - boolean - - - Does user have privilege for sequence? - Allowable privilege types are - USAGE, - SELECT, and - UPDATE. - - - - - - - has_server_privilege - - has_server_privilege ( - user name or oid, - server text or oid, - privilege text ) - boolean - - - Does user have privilege for foreign server? - The only allowable privilege type is USAGE. - - - - - - - has_table_privilege - - has_table_privilege ( - user name or oid, - table text or oid, - privilege text ) - boolean - - - Does user have privilege for table? - Allowable privilege types - are SELECT, INSERT, - UPDATE, DELETE, - TRUNCATE, REFERENCES, - TRIGGER, and MAINTAIN. - - - - - - - has_tablespace_privilege - - has_tablespace_privilege ( - user name or oid, - tablespace text or oid, - privilege text ) - boolean - - - Does user have privilege for tablespace? - The only allowable privilege type is CREATE. - - - - - - - has_type_privilege - - has_type_privilege ( - user name or oid, - type text or oid, - privilege text ) - boolean - - - Does user have privilege for data type? - The only allowable privilege type is USAGE. - When specifying a type by name rather than by OID, the allowed input - is the same as for the regtype data type (see - ). - - - - - - - pg_has_role - - pg_has_role ( - user name or oid, - role text or oid, - privilege text ) - boolean - - - Does user have privilege for role? - Allowable privilege types are - MEMBER, USAGE, - and SET. - MEMBER denotes direct or indirect membership in - the role without regard to what specific privileges may be conferred. - USAGE denotes whether the privileges of the role - are immediately available without doing SET ROLE, - while SET denotes whether it is possible to change - to the role using the SET ROLE command. - WITH ADMIN OPTION or WITH GRANT - OPTION can be added to any of these privilege types to - test whether the ADMIN privilege is held (all - six spellings test the same thing). - This function does not allow the special case of - setting user to public, - because the PUBLIC pseudo-role can never be a member of real roles. - - - - - - - row_security_active - - row_security_active ( - table text or oid ) - boolean - - - Is row-level security active for the specified table in the context of - the current user and current environment? - - - - -
- - - shows the operators - available for the aclitem type, which is the catalog - representation of access privileges. See - for information about how to read access privilege values. - - - - <type>aclitem</type> Operators - - - - - Operator - - - Description - - - Example(s) - - - - - - - - - aclitemeq - - aclitem = aclitem - boolean - - - Are aclitems equal? (Notice that - type aclitem lacks the usual set of comparison - operators; it has only equality. In turn, aclitem - arrays can only be compared for equality.) - - - 'calvin=r*w/hobbes'::aclitem = 'calvin=r*w*/hobbes'::aclitem - f - - - - - - - aclcontains - - aclitem[] @> aclitem - boolean - - - Does array contain the specified privileges? (This is true if there - is an array entry that matches the aclitem's grantee and - grantor, and has at least the specified set of privileges.) - - - '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] @> 'calvin=r*/hobbes'::aclitem - t - - - - - - aclitem[] ~ aclitem - boolean - - - This is a deprecated alias for @>. - - - '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] ~ 'calvin=r*/hobbes'::aclitem - t - - - - -
- - - shows some additional - functions to manage the aclitem type. - - - - <type>aclitem</type> Functions - - - - - Function - - - Description - - - - - - - - - acldefault - - acldefault ( - type "char", - ownerId oid ) - aclitem[] - - - Constructs an aclitem array holding the default access - privileges for an object of type type belonging - to the role with OID ownerId. This represents - the access privileges that will be assumed when an object's - ACL entry is null. (The default access privileges - are described in .) - The type parameter must be one of - 'c' for COLUMN, - 'r' for TABLE and table-like objects, - 's' for SEQUENCE, - 'd' for DATABASE, - 'f' for FUNCTION or PROCEDURE, - 'l' for LANGUAGE, - 'L' for LARGE OBJECT, - 'n' for SCHEMA, - 'p' for PARAMETER, - 't' for TABLESPACE, - 'F' for FOREIGN DATA WRAPPER, - 'S' for FOREIGN SERVER, - or - 'T' for TYPE or DOMAIN. - - - - - - - aclexplode - - aclexplode ( aclitem[] ) - setof record - ( grantor oid, - grantee oid, - privilege_type text, - is_grantable boolean ) - - - Returns the aclitem array as a set of rows. - If the grantee is the pseudo-role PUBLIC, it is represented by zero in - the grantee column. Each granted privilege is - represented as SELECT, INSERT, - etc (see for a full list). - Note that each privilege is broken out as a separate row, so - only one keyword appears in the privilege_type - column. - - - - - - - makeaclitem - - makeaclitem ( - grantee oid, - grantor oid, - privileges text, - is_grantable boolean ) - aclitem - - - Constructs an aclitem with the given properties. - privileges is a comma-separated list of - privilege names such as SELECT, - INSERT, etc, all of which are set in the - result. (Case of the privilege string is not significant, and - extra whitespace is allowed between but not within privilege - names.) - - - - -
- -
- - - Schema Visibility Inquiry Functions - - - shows functions that - determine whether a certain object is visible in the - current schema search path. - For example, a table is said to be visible if its - containing schema is in the search path and no table of the same - name appears earlier in the search path. This is equivalent to the - statement that the table can be referenced by name without explicit - schema qualification. Thus, to list the names of all visible tables: - -SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); - - For functions and operators, an object in the search path is said to be - visible if there is no object of the same name and argument data - type(s) earlier in the path. For operator classes and families, - both the name and the associated index access method are considered. - - - - search path - object visibility - - - - Schema Visibility Inquiry Functions - - - - - Function - - - Description - - - - - - - - - pg_collation_is_visible - - pg_collation_is_visible ( collation oid ) - boolean - - - Is collation visible in search path? - - - - - - - pg_conversion_is_visible - - pg_conversion_is_visible ( conversion oid ) - boolean - - - Is conversion visible in search path? - - - - - - - pg_function_is_visible - - pg_function_is_visible ( function oid ) - boolean - - - Is function visible in search path? - (This also works for procedures and aggregates.) - - - - - - - pg_opclass_is_visible - - pg_opclass_is_visible ( opclass oid ) - boolean - - - Is operator class visible in search path? - - - - - - - pg_operator_is_visible - - pg_operator_is_visible ( operator oid ) - boolean - - - Is operator visible in search path? - - - - - - - pg_opfamily_is_visible - - pg_opfamily_is_visible ( opclass oid ) - boolean - - - Is operator family visible in search path? - - - - - - - pg_statistics_obj_is_visible - - pg_statistics_obj_is_visible ( stat oid ) - boolean - - - Is statistics object visible in search path? - - - - - - - pg_table_is_visible - - pg_table_is_visible ( table oid ) - boolean - - - Is table visible in search path? - (This works for all types of relations, including views, materialized - views, indexes, sequences and foreign tables.) - - - - - - - pg_ts_config_is_visible - - pg_ts_config_is_visible ( config oid ) - boolean - - - Is text search configuration visible in search path? - - - - - - - pg_ts_dict_is_visible - - pg_ts_dict_is_visible ( dict oid ) - boolean - - - Is text search dictionary visible in search path? - - - - - - - pg_ts_parser_is_visible - - pg_ts_parser_is_visible ( parser oid ) - boolean - - - Is text search parser visible in search path? - - - - - - - pg_ts_template_is_visible - - pg_ts_template_is_visible ( template oid ) - boolean - - - Is text search template visible in search path? - - - - - - - pg_type_is_visible - - pg_type_is_visible ( type oid ) - boolean - - - Is type (or domain) visible in search path? - - - - -
- - - All these functions require object OIDs to identify the object to be - checked. If you want to test an object by name, it is convenient to use - the OID alias types (regclass, regtype, - regprocedure, regoperator, regconfig, - or regdictionary), - for example: - -SELECT pg_type_is_visible('myschema.widget'::regtype); - - Note that it would not make much sense to test a non-schema-qualified - type name in this way — if the name can be recognized at all, it must be visible. - - -
- - - System Catalog Information Functions - - - lists functions that - extract information from the system catalogs. - - - - System Catalog Information Functions - - - - - Function - - - Description - - - - - - - - - format_type - - format_type ( type oid, typemod integer ) - text - - - Returns the SQL name for a data type that is identified by its type - OID and possibly a type modifier. Pass NULL for the type modifier if - no specific modifier is known. - - - - - - - pg_basetype - - pg_basetype ( regtype ) - regtype - - - Returns the OID of the base type of a domain identified by its - type OID. If the argument is the OID of a non-domain type, - returns the argument as-is. Returns NULL if the argument is - not a valid type OID. If there's a chain of domain dependencies, - it will recurse until finding the base type. - - - Assuming CREATE DOMAIN mytext AS text: - - - pg_basetype('mytext'::regtype) - text - - - - - - - pg_char_to_encoding - - pg_char_to_encoding ( encoding name ) - integer - - - Converts the supplied encoding name into an integer representing the - internal identifier used in some system catalog tables. - Returns -1 if an unknown encoding name is provided. - - - - - - - pg_encoding_to_char - - pg_encoding_to_char ( encoding integer ) - name - - - Converts the integer used as the internal identifier of an encoding in some - system catalog tables into a human-readable string. - Returns an empty string if an invalid encoding number is provided. - - - - - - - pg_get_catalog_foreign_keys - - pg_get_catalog_foreign_keys () - setof record - ( fktable regclass, - fkcols text[], - pktable regclass, - pkcols text[], - is_array boolean, - is_opt boolean ) - - - Returns a set of records describing the foreign key relationships - that exist within the PostgreSQL system - catalogs. - The fktable column contains the name of the - referencing catalog, and the fkcols column - contains the name(s) of the referencing column(s). Similarly, - the pktable column contains the name of the - referenced catalog, and the pkcols column - contains the name(s) of the referenced column(s). - If is_array is true, the last referencing - column is an array, each of whose elements should match some entry - in the referenced catalog. - If is_opt is true, the referencing column(s) - are allowed to contain zeroes instead of a valid reference. - - - - - - - pg_get_constraintdef - - pg_get_constraintdef ( constraint oid , pretty boolean ) - text - - - Reconstructs the creating command for a constraint. - (This is a decompiled reconstruction, not the original text - of the command.) - - - - - - - pg_get_expr - - pg_get_expr ( expr pg_node_tree, relation oid , pretty boolean ) - text - - - Decompiles the internal form of an expression stored in the system - catalogs, such as the default value for a column. If the expression - might contain Vars, specify the OID of the relation they refer to as - the second parameter; if no Vars are expected, passing zero is - sufficient. - - - - - - - pg_get_functiondef - - pg_get_functiondef ( func oid ) - text - - - Reconstructs the creating command for a function or procedure. - (This is a decompiled reconstruction, not the original text - of the command.) - The result is a complete CREATE OR REPLACE FUNCTION - or CREATE OR REPLACE PROCEDURE statement. - - - - - - - pg_get_function_arguments - - pg_get_function_arguments ( func oid ) - text - - - Reconstructs the argument list of a function or procedure, in the form - it would need to appear in within CREATE FUNCTION - (including default values). - - - - - - - pg_get_function_identity_arguments - - pg_get_function_identity_arguments ( func oid ) - text - - - Reconstructs the argument list necessary to identify a function or - procedure, in the form it would need to appear in within commands such - as ALTER FUNCTION. This form omits default values. - - - - - - - pg_get_function_result - - pg_get_function_result ( func oid ) - text - - - Reconstructs the RETURNS clause of a function, in - the form it would need to appear in within CREATE - FUNCTION. Returns NULL for a procedure. - - - - - - - pg_get_indexdef - - pg_get_indexdef ( index oid , column integer, pretty boolean ) - text - - - Reconstructs the creating command for an index. - (This is a decompiled reconstruction, not the original text - of the command.) If column is supplied and is - not zero, only the definition of that column is reconstructed. - - - - - - - pg_get_keywords - - pg_get_keywords () - setof record - ( word text, - catcode "char", - barelabel boolean, - catdesc text, - baredesc text ) - - - Returns a set of records describing the SQL keywords recognized by the - server. The word column contains the - keyword. The catcode column contains a - category code: U for an unreserved - keyword, C for a keyword that can be a column - name, T for a keyword that can be a type or - function name, or R for a fully reserved keyword. - The barelabel column - contains true if the keyword can be used as - a bare column label in SELECT lists, - or false if it can only be used - after AS. - The catdesc column contains a - possibly-localized string describing the keyword's category. - The baredesc column contains a - possibly-localized string describing the keyword's column label status. - - - - - - - pg_get_partkeydef - - pg_get_partkeydef ( table oid ) - text - - - Reconstructs the definition of a partitioned table's partition - key, in the form it would have in the PARTITION - BY clause of CREATE TABLE. - (This is a decompiled reconstruction, not the original text - of the command.) - - - - - - - pg_get_ruledef - - pg_get_ruledef ( rule oid , pretty boolean ) - text - - - Reconstructs the creating command for a rule. - (This is a decompiled reconstruction, not the original text - of the command.) - - - - - - - pg_get_serial_sequence - - pg_get_serial_sequence ( table text, column text ) - text - - - Returns the name of the sequence associated with a column, - or NULL if no sequence is associated with the column. - If the column is an identity column, the associated sequence is the - sequence internally created for that column. - For columns created using one of the serial types - (serial, smallserial, bigserial), - it is the sequence created for that serial column definition. - In the latter case, the association can be modified or removed - with ALTER SEQUENCE OWNED BY. - (This function probably should have been - called pg_get_owned_sequence; its current name - reflects the fact that it has historically been used with serial-type - columns.) The first parameter is a table name with optional - schema, and the second parameter is a column name. Because the first - parameter potentially contains both schema and table names, it is - parsed per usual SQL rules, meaning it is lower-cased by default. - The second parameter, being just a column name, is treated literally - and so has its case preserved. The result is suitably formatted - for passing to the sequence functions (see - ). - - - A typical use is in reading the current value of the sequence for an - identity or serial column, for example: - -SELECT currval(pg_get_serial_sequence('sometable', 'id')); - - - - - - - - pg_get_statisticsobjdef - - pg_get_statisticsobjdef ( statobj oid ) - text - - - Reconstructs the creating command for an extended statistics object. - (This is a decompiled reconstruction, not the original text - of the command.) - - - - - - - pg_get_triggerdef - -pg_get_triggerdef ( trigger oid , pretty boolean ) - text - - - Reconstructs the creating command for a trigger. - (This is a decompiled reconstruction, not the original text - of the command.) - - - - - - - pg_get_userbyid - - pg_get_userbyid ( role oid ) - name - - - Returns a role's name given its OID. - - - - - - - pg_get_viewdef - - pg_get_viewdef ( view oid , pretty boolean ) - text - - - Reconstructs the underlying SELECT command for a - view or materialized view. (This is a decompiled reconstruction, not - the original text of the command.) - - - - - - pg_get_viewdef ( view oid, wrap_column integer ) - text - - - Reconstructs the underlying SELECT command for a - view or materialized view. (This is a decompiled reconstruction, not - the original text of the command.) In this form of the function, - pretty-printing is always enabled, and long lines are wrapped to try - to keep them shorter than the specified number of columns. - - - - - - pg_get_viewdef ( view text , pretty boolean ) - text - - - Reconstructs the underlying SELECT command for a - view or materialized view, working from a textual name for the view - rather than its OID. (This is deprecated; use the OID variant - instead.) - - - - - - - pg_index_column_has_property - - pg_index_column_has_property ( index regclass, column integer, property text ) - boolean - - - Tests whether an index column has the named property. - Common index column properties are listed in - . - (Note that extension access methods can define additional property - names for their indexes.) - NULL is returned if the property name is not known - or does not apply to the particular object, or if the OID or column - number does not identify a valid object. - - - - - - - pg_index_has_property - - pg_index_has_property ( index regclass, property text ) - boolean - - - Tests whether an index has the named property. - Common index properties are listed in - . - (Note that extension access methods can define additional property - names for their indexes.) - NULL is returned if the property name is not known - or does not apply to the particular object, or if the OID does not - identify a valid object. - - - - - - - pg_indexam_has_property - - pg_indexam_has_property ( am oid, property text ) - boolean - - - Tests whether an index access method has the named property. - Access method properties are listed in - . - NULL is returned if the property name is not known - or does not apply to the particular object, or if the OID does not - identify a valid object. - - - - - - - pg_options_to_table - - pg_options_to_table ( options_array text[] ) - setof record - ( option_name text, - option_value text ) - - - Returns the set of storage options represented by a value from - pg_class.reloptions or - pg_attribute.attoptions. - - - - - - - pg_settings_get_flags - - pg_settings_get_flags ( guc text ) - text[] - - - Returns an array of the flags associated with the given GUC, or - NULL if it does not exist. The result is - an empty array if the GUC exists but there are no flags to show. - Only the most useful flags listed in - are exposed. - - - - - - - pg_tablespace_databases - - pg_tablespace_databases ( tablespace oid ) - setof oid - - - Returns the set of OIDs of databases that have objects stored in the - specified tablespace. If this function returns any rows, the - tablespace is not empty and cannot be dropped. To identify the specific - objects populating the tablespace, you will need to connect to the - database(s) identified by pg_tablespace_databases - and query their pg_class catalogs. - - - - - - - pg_tablespace_location - - pg_tablespace_location ( tablespace oid ) - text - - - Returns the file system path that this tablespace is located in. - - - - - - - pg_typeof - - pg_typeof ( "any" ) - regtype - - - Returns the OID of the data type of the value that is passed to it. - This can be helpful for troubleshooting or dynamically constructing - SQL queries. The function is declared as - returning regtype, which is an OID alias type (see - ); this means that it is the same as an - OID for comparison purposes but displays as a type name. - - - pg_typeof(33) - integer - - - - - - - COLLATION FOR - - COLLATION FOR ( "any" ) - text - - - Returns the name of the collation of the value that is passed to it. - The value is quoted and schema-qualified if necessary. If no - collation was derived for the argument expression, - then NULL is returned. If the argument is not of a - collatable data type, then an error is raised. - - - collation for ('foo'::text) - "default" - - - collation for ('foo' COLLATE "de_DE") - "de_DE" - - - - - - - to_regclass - - to_regclass ( text ) - regclass - - - Translates a textual relation name to its OID. A similar result is - obtained by casting the string to type regclass (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found. - - - - - - - to_regcollation - - to_regcollation ( text ) - regcollation - - - Translates a textual collation name to its OID. A similar result is - obtained by casting the string to type regcollation (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found. - - - - - - - to_regnamespace - - to_regnamespace ( text ) - regnamespace - - - Translates a textual schema name to its OID. A similar result is - obtained by casting the string to type regnamespace (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found. - - - - - - - to_regoper - - to_regoper ( text ) - regoper - - - Translates a textual operator name to its OID. A similar result is - obtained by casting the string to type regoper (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found or is ambiguous. - - - - - - - to_regoperator - - to_regoperator ( text ) - regoperator - - - Translates a textual operator name (with parameter types) to its OID. A similar result is - obtained by casting the string to type regoperator (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found. - - - - - - - to_regproc - - to_regproc ( text ) - regproc - - - Translates a textual function or procedure name to its OID. A similar result is - obtained by casting the string to type regproc (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found or is ambiguous. - - - - - - - to_regprocedure - - to_regprocedure ( text ) - regprocedure - - - Translates a textual function or procedure name (with argument types) to its OID. A similar result is - obtained by casting the string to type regprocedure (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found. - - - - - - - to_regrole - - to_regrole ( text ) - regrole - - - Translates a textual role name to its OID. A similar result is - obtained by casting the string to type regrole (see - ); however, this function will return - NULL rather than throwing an error if the name is - not found. - - - - - - - to_regtype - - to_regtype ( text ) - regtype - - - Parses a string of text, extracts a potential type name from it, - and translates that name into a type OID. A syntax error in the - string will result in an error; but if the string is a - syntactically valid type name that happens not to be found in the - catalogs, the result is NULL. A similar result - is obtained by casting the string to type regtype - (see ), except that that will throw - error for name not found. - - - - - - - to_regtypemod - - to_regtypemod ( text ) - integer - - - Parses a string of text, extracts a potential type name from it, - and translates its type modifier, if any. A syntax error in the - string will result in an error; but if the string is a - syntactically valid type name that happens not to be found in the - catalogs, the result is NULL. The result is - -1 if no type modifier is present. - - - to_regtypemod can be combined with - to produce appropriate inputs for - , allowing a string representing a - type name to be canonicalized. - - - format_type(to_regtype('varchar(32)'), to_regtypemod('varchar(32)')) - character varying(32) - - - - -
- - - Most of the functions that reconstruct (decompile) database objects - have an optional pretty flag, which - if true causes the result to - be pretty-printed. Pretty-printing suppresses unnecessary - parentheses and adds whitespace for legibility. - The pretty-printed format is more readable, but the default format - is more likely to be interpreted the same way by future versions of - PostgreSQL; so avoid using pretty-printed output - for dump purposes. Passing false for - the pretty parameter yields the same result as - omitting the parameter. - - - - Index Column Properties - - - NameDescription - - - - asc - Does the column sort in ascending order on a forward scan? - - - - desc - Does the column sort in descending order on a forward scan? - - - - nulls_first - Does the column sort with nulls first on a forward scan? - - - - nulls_last - Does the column sort with nulls last on a forward scan? - - - - orderable - Does the column possess any defined sort ordering? - - - - distance_orderable - Can the column be scanned in order by a distance - operator, for example ORDER BY col <-> constant ? - - - - returnable - Can the column value be returned by an index-only scan? - - - - search_array - Does the column natively support col = ANY(array) - searches? - - - - search_nulls - Does the column support IS NULL and - IS NOT NULL searches? - - - - -
- - - Index Properties - - - NameDescription - - - - clusterable - Can the index be used in a CLUSTER command? - - - - index_scan - Does the index support plain (non-bitmap) scans? - - - - bitmap_scan - Does the index support bitmap scans? - - - - backward_scan - Can the scan direction be changed in mid-scan (to - support FETCH BACKWARD on a cursor without - needing materialization)? - - - - -
- - - Index Access Method Properties - - - NameDescription - - - - can_order - Does the access method support ASC, - DESC and related keywords in - CREATE INDEX? - - - - can_unique - Does the access method support unique indexes? - - - - can_multi_col - Does the access method support indexes with multiple columns? - - - - can_exclude - Does the access method support exclusion constraints? - - - - can_include - Does the access method support the INCLUDE - clause of CREATE INDEX? - - - - -
- - - GUC Flags - - - FlagDescription - - - - EXPLAIN - Parameters with this flag are included in - EXPLAIN (SETTINGS) commands. - - - - NO_SHOW_ALL - Parameters with this flag are excluded from - SHOW ALL commands. - - - - NO_RESET - Parameters with this flag do not support - RESET commands. - - - - NO_RESET_ALL - Parameters with this flag are excluded from - RESET ALL commands. - - - - NOT_IN_SAMPLE - Parameters with this flag are not included in - postgresql.conf by default. - - - - RUNTIME_COMPUTED - Parameters with this flag are runtime-computed ones. - - - - -
- -
- - - Object Information and Addressing Functions - - - lists functions related to - database object identification and addressing. - - - - Object Information and Addressing Functions - - - - - Function - - - Description - - - - - - - - - pg_get_acl - - pg_get_acl ( classid oid, objid oid, objsubid integer ) - aclitem[] - - - Returns the ACL for a database object, specified - by catalog OID, object OID and sub-object ID. This function returns - NULL values for undefined objects. - - - - - - - pg_describe_object - - pg_describe_object ( classid oid, objid oid, objsubid integer ) - text - - - Returns a textual description of a database object identified by - catalog OID, object OID, and sub-object ID (such as a column number - within a table; the sub-object ID is zero when referring to a whole - object). This description is intended to be human-readable, and might - be translated, depending on server configuration. This is especially - useful to determine the identity of an object referenced in the - pg_depend catalog. This function returns - NULL values for undefined objects. - - - - - - - pg_identify_object - - pg_identify_object ( classid oid, objid oid, objsubid integer ) - record - ( type text, - schema text, - name text, - identity text ) - - - Returns a row containing enough information to uniquely identify the - database object specified by catalog OID, object OID and sub-object - ID. - This information is intended to be machine-readable, and is never - translated. - type identifies the type of database object; - schema is the schema name that the object - belongs in, or NULL for object types that do not - belong to schemas; - name is the name of the object, quoted if - necessary, if the name (along with schema name, if pertinent) is - sufficient to uniquely identify the object, - otherwise NULL; - identity is the complete object identity, with - the precise format depending on object type, and each name within the - format being schema-qualified and quoted as necessary. Undefined - objects are identified with NULL values. - - - - - - - pg_identify_object_as_address - - pg_identify_object_as_address ( classid oid, objid oid, objsubid integer ) - record - ( type text, - object_names text[], - object_args text[] ) - - - Returns a row containing enough information to uniquely identify the - database object specified by catalog OID, object OID and sub-object - ID. - The returned information is independent of the current server, that - is, it could be used to identify an identically named object in - another server. - type identifies the type of database object; - object_names and - object_args - are text arrays that together form a reference to the object. - These three values can be passed - to pg_get_object_address to obtain the internal - address of the object. - - - - - - - pg_get_object_address - - pg_get_object_address ( type text, object_names text[], object_args text[] ) - record - ( classid oid, - objid oid, - objsubid integer ) - - - Returns a row containing enough information to uniquely identify the - database object specified by a type code and object name and argument - arrays. - The returned values are the ones that would be used in system catalogs - such as pg_depend; they can be passed to - other system functions such as pg_describe_object - or pg_identify_object. - classid is the OID of the system catalog - containing the object; - objid is the OID of the object itself, and - objsubid is the sub-object ID, or zero if none. - This function is the inverse - of pg_identify_object_as_address. - Undefined objects are identified with NULL values. - - - - -
- - - pg_get_acl is useful for retrieving and inspecting - the privileges associated with database objects without looking at - specific catalogs. For example, to retrieve all the granted privileges - on objects in the current database: - -postgres=# SELECT - (pg_identify_object(s.classid,s.objid,s.objsubid)).*, - pg_catalog.pg_get_acl(s.classid,s.objid,s.objsubid) AS acl -FROM pg_catalog.pg_shdepend AS s -JOIN pg_catalog.pg_database AS d - ON d.datname = current_database() AND - d.oid = s.dbid -JOIN pg_catalog.pg_authid AS a - ON a.oid = s.refobjid AND - s.refclassid = 'pg_authid'::regclass -WHERE s.deptype = 'a'; --[ RECORD 1 ]----------------------------------------- -type | table -schema | public -name | testtab -identity | public.testtab -acl | {postgres=arwdDxtm/postgres,foo=r/postgres} - - - -
- - - Comment Information Functions - - - comment - about database objects - - - - The functions shown in - extract comments previously stored with the - command. A null value is returned if no - comment could be found for the specified parameters. - - - - Comment Information Functions - - - - - Function - - - Description - - - - - - - - - col_description - - col_description ( table oid, column integer ) - text - - - Returns the comment for a table column, which is specified by the OID - of its table and its column number. - (obj_description cannot be used for table - columns, since columns do not have OIDs of their own.) - - - - - - - obj_description - - obj_description ( object oid, catalog name ) - text - - - Returns the comment for a database object specified by its OID and the - name of the containing system catalog. For - example, obj_description(123456, 'pg_class') would - retrieve the comment for the table with OID 123456. - - - - - - obj_description ( object oid ) - text - - - Returns the comment for a database object specified by its OID alone. - This is deprecated since there is no guarantee - that OIDs are unique across different system catalogs; therefore, the - wrong comment might be returned. - - - - - - - shobj_description - - shobj_description ( object oid, catalog name ) - text - - - Returns the comment for a shared database object specified by its OID - and the name of the containing system catalog. This is just - like obj_description except that it is used for - retrieving comments on shared objects (that is, databases, roles, and - tablespaces). Some system catalogs are global to all databases within - each cluster, and the descriptions for objects in them are stored - globally as well. - - - - -
- -
- - - Data Validity Checking Functions - - - The functions shown in - can be helpful for checking validity of proposed input data. - - - - Data Validity Checking Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - pg_input_is_valid - - pg_input_is_valid ( - string text, - type text - ) - boolean - - - Tests whether the given string is valid - input for the specified data type, returning true or false. - - - This function will only work as desired if the data type's input - function has been updated to report invalid input as - a soft error. Otherwise, invalid input will abort - the transaction, just as if the string had been cast to the type - directly. - - - pg_input_is_valid('42', 'integer') - t - - - pg_input_is_valid('42000000000', 'integer') - f - - - pg_input_is_valid('1234.567', 'numeric(7,4)') - f - - - - - - pg_input_error_info - - pg_input_error_info ( - string text, - type text - ) - record - ( message text, - detail text, - hint text, - sql_error_code text ) - - - Tests whether the given string is valid - input for the specified data type; if not, return the details of - the error that would have been thrown. If the input is valid, the - results are NULL. The inputs are the same as - for pg_input_is_valid. - - - This function will only work as desired if the data type's input - function has been updated to report invalid input as - a soft error. Otherwise, invalid input will abort - the transaction, just as if the string had been cast to the type - directly. - - - SELECT * FROM pg_input_error_info('42000000000', 'integer') - - - message | detail | hint | sql_error_code -------------------------------------------------------+--------+------+---------------- - value "42000000000" is out of range for type integer | | | 22003 - - - - - -
- -
- - - Transaction ID and Snapshot Information Functions - - - The functions shown in - provide server transaction information in an exportable form. The main - use of these functions is to determine which transactions were committed - between two snapshots. - - - - Transaction ID and Snapshot Information Functions - - - - - Function - - - Description - - - - - - - - - age - - age ( xid ) - integer - - - Returns the number of transactions between the supplied - transaction id and the current transaction counter. - - - - - - - mxid_age - - mxid_age ( xid ) - integer - - - Returns the number of multixacts IDs between the supplied - multixact ID and the current multixacts counter. - - - - - - - pg_current_xact_id - - pg_current_xact_id () - xid8 - - - Returns the current transaction's ID. It will assign a new one if the - current transaction does not have one already (because it has not - performed any database updates); see for details. If executed in a - subtransaction, this will return the top-level transaction ID; - see for details. - - - - - - - pg_current_xact_id_if_assigned - - pg_current_xact_id_if_assigned () - xid8 - - - Returns the current transaction's ID, or NULL if no - ID is assigned yet. (It's best to use this variant if the transaction - might otherwise be read-only, to avoid unnecessary consumption of an - XID.) - If executed in a subtransaction, this will return the top-level - transaction ID. - - - - - - - pg_xact_status - - pg_xact_status ( xid8 ) - text - - - Reports the commit status of a recent transaction. - The result is one of in progress, - committed, or aborted, - provided that the transaction is recent enough that the system retains - the commit status of that transaction. - If it is old enough that no references to the transaction survive in - the system and the commit status information has been discarded, the - result is NULL. - Applications might use this function, for example, to determine - whether their transaction committed or aborted after the application - and database server become disconnected while - a COMMIT is in progress. - Note that prepared transactions are reported as in - progress; applications must check pg_prepared_xacts - if they need to determine whether a transaction ID belongs to a - prepared transaction. - - - - - - - pg_current_snapshot - - pg_current_snapshot () - pg_snapshot - - - Returns a current snapshot, a data structure - showing which transaction IDs are now in-progress. - Only top-level transaction IDs are included in the snapshot; - subtransaction IDs are not shown; see - for details. - - - - - - - pg_snapshot_xip - - pg_snapshot_xip ( pg_snapshot ) - setof xid8 - - - Returns the set of in-progress transaction IDs contained in a snapshot. - - - - - - - pg_snapshot_xmax - - pg_snapshot_xmax ( pg_snapshot ) - xid8 - - - Returns the xmax of a snapshot. - - - - - - - pg_snapshot_xmin - - pg_snapshot_xmin ( pg_snapshot ) - xid8 - - - Returns the xmin of a snapshot. - - - - - - - pg_visible_in_snapshot - - pg_visible_in_snapshot ( xid8, pg_snapshot ) - boolean - - - Is the given transaction ID visible according - to this snapshot (that is, was it completed before the snapshot was - taken)? Note that this function will not give the correct answer for - a subtransaction ID (subxid); see for - details. - - - - -
- - - The internal transaction ID type xid is 32 bits wide and - wraps around every 4 billion transactions. However, - the functions shown in , except - age and mxid_age, use a - 64-bit type xid8 that does not wrap around during the life - of an installation and can be converted to xid by casting if - required; see for details. - The data type pg_snapshot stores information about - transaction ID visibility at a particular moment in time. Its components - are described in . - pg_snapshot's textual representation is - xmin:xmax:xip_list. - For example 10:20:10,14,15 means - xmin=10, xmax=20, xip_list=10, 14, 15. - - - - Snapshot Components - - - - Name - Description - - - - - - xmin - - Lowest transaction ID that was still active. All transaction IDs - less than xmin are either committed and visible, - or rolled back and dead. - - - - - xmax - - One past the highest completed transaction ID. All transaction IDs - greater than or equal to xmax had not yet - completed as of the time of the snapshot, and thus are invisible. - - - - - xip_list - - Transactions in progress at the time of the snapshot. A transaction - ID that is xmin <= X < - xmax and not in this list was already completed at the time - of the snapshot, and thus is either visible or dead according to its - commit status. This list does not include the transaction IDs of - subtransactions (subxids). - - - - -
- - - In releases of PostgreSQL before 13 there was - no xid8 type, so variants of these functions were provided - that used bigint to represent a 64-bit XID, with a - correspondingly distinct snapshot data type txid_snapshot. - These older functions have txid in their names. They - are still supported for backward compatibility, but may be removed from a - future release. See . - - - - Deprecated Transaction ID and Snapshot Information Functions - - - - - Function - - - Description - - - - - - - - - - txid_current - - txid_current () - bigint - - - See pg_current_xact_id(). - - - - - - - txid_current_if_assigned - - txid_current_if_assigned () - bigint - - - See pg_current_xact_id_if_assigned(). - - - - - - - txid_current_snapshot - - txid_current_snapshot () - txid_snapshot - - - See pg_current_snapshot(). - - - - - - - txid_snapshot_xip - - txid_snapshot_xip ( txid_snapshot ) - setof bigint - - - See pg_snapshot_xip(). - - - - - - - txid_snapshot_xmax - - txid_snapshot_xmax ( txid_snapshot ) - bigint - - - See pg_snapshot_xmax(). - - - - - - - txid_snapshot_xmin - - txid_snapshot_xmin ( txid_snapshot ) - bigint - - - See pg_snapshot_xmin(). - - - - - - - txid_visible_in_snapshot - - txid_visible_in_snapshot ( bigint, txid_snapshot ) - boolean - - - See pg_visible_in_snapshot(). - - - - - - - txid_status - - txid_status ( bigint ) - text - - - See pg_xact_status(). - - - - -
- -
- - - Committed Transaction Information Functions - - - The functions shown in - provide information about when past transactions were committed. - They only provide useful data when the - configuration option is - enabled, and only for transactions that were committed after it was - enabled. Commit timestamp information is routinely removed during - vacuum. - - - - Committed Transaction Information Functions - - - - - Function - - - Description - - - - - - - - - pg_xact_commit_timestamp - - pg_xact_commit_timestamp ( xid ) - timestamp with time zone - - - Returns the commit timestamp of a transaction. - - - - - - - pg_xact_commit_timestamp_origin - - pg_xact_commit_timestamp_origin ( xid ) - record - ( timestamp timestamp with time zone, - roident oid) - - - Returns the commit timestamp and replication origin of a transaction. - - - - - - - pg_last_committed_xact - - pg_last_committed_xact () - record - ( xid xid, - timestamp timestamp with time zone, - roident oid ) - - - Returns the transaction ID, commit timestamp and replication origin - of the latest committed transaction. - - - - -
- -
- - - Control Data Functions - - - The functions shown in - print information initialized during initdb, such - as the catalog version. They also show information about write-ahead - logging and checkpoint processing. This information is cluster-wide, - not specific to any one database. These functions provide most of the same - information, from the same source, as the - application. - - - - Control Data Functions - - - - - Function - - - Description - - - - - - - - - pg_control_checkpoint - - pg_control_checkpoint () - record - - - Returns information about current checkpoint state, as shown in - . - - - - - - - pg_control_system - - pg_control_system () - record - - - Returns information about current control file state, as shown in - . - - - - - - - pg_control_init - - pg_control_init () - record - - - Returns information about cluster initialization state, as shown in - . - - - - - - - pg_control_recovery - - pg_control_recovery () - record - - - Returns information about recovery state, as shown in - . - - - - -
- - - <function>pg_control_checkpoint</function> Output Columns - - - - Column Name - Data Type - - - - - - - checkpoint_lsn - pg_lsn - - - - redo_lsn - pg_lsn - - - - redo_wal_file - text - - - - timeline_id - integer - - - - prev_timeline_id - integer - - - - full_page_writes - boolean - - - - next_xid - text - - - - next_oid - oid - - - - next_multixact_id - xid - - - - next_multi_offset - xid - - - - oldest_xid - xid - - - - oldest_xid_dbid - oid - - - - oldest_active_xid - xid - - - - oldest_multi_xid - xid - - - - oldest_multi_dbid - oid - - - - oldest_commit_ts_xid - xid - - - - newest_commit_ts_xid - xid - - - - checkpoint_time - timestamp with time zone - - - - -
- - - <function>pg_control_system</function> Output Columns - - - - Column Name - Data Type - - - - - - - pg_control_version - integer - - - - catalog_version_no - integer - - - - system_identifier - bigint - - - - pg_control_last_modified - timestamp with time zone - - - - -
- - - <function>pg_control_init</function> Output Columns - - - - Column Name - Data Type - - - - - - - max_data_alignment - integer - - - - database_block_size - integer - - - - blocks_per_segment - integer - - - - wal_block_size - integer - - - - bytes_per_wal_segment - integer - - - - max_identifier_length - integer - - - - max_index_columns - integer - - - - max_toast_chunk_size - integer - - - - large_object_chunk_size - integer - - - - float8_pass_by_value - boolean - - - - data_page_checksum_version - integer - - - - default_char_signedness - boolean - - - - -
- - - <function>pg_control_recovery</function> Output Columns - - - - Column Name - Data Type - - - - - - - min_recovery_end_lsn - pg_lsn - - - - min_recovery_end_timeline - integer - - - - backup_start_lsn - pg_lsn - - - - backup_end_lsn - pg_lsn - - - - end_of_backup_record_required - boolean - - - - -
- -
- - - Version Information Functions - - - The functions shown in - print version information. - - - - Version Information Functions - - - - - Function - - - Description - - - - - - - - - version - - version () - text - - - Returns a string describing the PostgreSQL - server's version. You can also get this information from - , or for a machine-readable - version use . Software - developers should use server_version_num (available - since 8.2) or instead of - parsing the text version. - - - - - - - unicode_version - - unicode_version () - text - - - Returns a string representing the version of Unicode used by - PostgreSQL. - - - - - - icu_unicode_version - - icu_unicode_version () - text - - - Returns a string representing the version of Unicode used by ICU, if - the server was built with ICU support; otherwise returns - NULL - - - -
- -
- - - WAL Summarization Information Functions - - - The functions shown in - print information about the status of WAL summarization. - See . - - - - WAL Summarization Information Functions - - - - - Function - - - Description - - - - - - - - - pg_available_wal_summaries - - pg_available_wal_summaries () - setof record - ( tli bigint, - start_lsn pg_lsn, - end_lsn pg_lsn ) - - - Returns information about the WAL summary files present in the - data directory, under pg_wal/summaries. - One row will be returned per WAL summary file. Each file summarizes - WAL on the indicated TLI within the indicated LSN range. This function - might be useful to determine whether enough WAL summaries are present - on the server to take an incremental backup based on some prior - backup whose start LSN is known. - - - - - - - pg_wal_summary_contents - - pg_wal_summary_contents ( tli bigint, start_lsn pg_lsn, end_lsn pg_lsn ) - setof record - ( relfilenode oid, - reltablespace oid, - reldatabase oid, - relforknumber smallint, - relblocknumber bigint, - is_limit_block boolean ) - - - Returns one information about the contents of a single WAL summary file - identified by TLI and starting and ending LSNs. Each row with - is_limit_block false indicates that the block - identified by the remaining output columns was modified by at least - one WAL record within the range of records summarized by this file. - Each row with is_limit_block true indicates either - that (a) the relation fork was truncated to the length given by - relblocknumber within the relevant range of WAL - records or (b) that the relation fork was created or dropped within - the relevant range of WAL records; in such cases, - relblocknumber will be zero. - - - - - - - pg_get_wal_summarizer_state - - pg_get_wal_summarizer_state () - record - ( summarized_tli bigint, - summarized_lsn pg_lsn, - pending_lsn pg_lsn, - summarizer_pid int ) - - - Returns information about the progress of the WAL summarizer. If the - WAL summarizer has never run since the instance was started, then - summarized_tli and summarized_lsn - will be 0 and 0/0 respectively; - otherwise, they will be the TLI and ending LSN of the last WAL summary - file written to disk. If the WAL summarizer is currently running, - pending_lsn will be the ending LSN of the last - record that it has consumed, which must always be greater than or - equal to summarized_lsn; if the WAL summarizer is - not running, it will be equal to summarized_lsn. - summarizer_pid is the PID of the WAL summarizer - process, if it is running, and otherwise NULL. - - - As a special exception, the WAL summarizer will refuse to generate - WAL summary files if run on WAL generated under - wal_level=minimal, since such summaries would be - unsafe to use as the basis for an incremental backup. In this case, - the fields above will continue to advance as if summaries were being - generated, but nothing will be written to disk. Once the summarizer - reaches WAL generated while wal_level was set - to replica or higher, it will resume writing - summaries to disk. - - - - -
- -
- -
- - - System Administration Functions - - - The functions described in this section are used to control and - monitor a PostgreSQL installation. - - - - Configuration Settings Functions - - - SET - - - - SHOW - - - - configuration - of the server - functions - - - - shows the functions - available to query and alter run-time configuration parameters. - - - - Configuration Settings Functions - - - - - Function - - - Description - - - Example(s) - - - - - - - - - current_setting - - current_setting ( setting_name text , missing_ok boolean ) - text - - - Returns the current value of the - setting setting_name. If there is no such - setting, current_setting throws an error - unless missing_ok is supplied and - is true (in which case NULL is returned). - This function corresponds to - the SQL command . - - - current_setting('datestyle') - ISO, MDY - - - - - - - set_config - - set_config ( - setting_name text, - new_value text, - is_local boolean ) - text - - - Sets the parameter setting_name - to new_value, and returns that value. - If is_local is true, the new - value will only apply during the current transaction. If you want the - new value to apply for the rest of the current session, - use false instead. This function corresponds to - the SQL command . - - - set_config accepts the NULL value for - new_value, but as settings cannot be null, it - is interpreted as a request to reset the setting to its default value. - - - set_config('log_statement_stats', 'off', false) - off - - - - -
- -
- - - Server Signaling Functions - - - signal - backend processes - - - - The functions shown in send control signals to - other server processes. Use of these functions is restricted to - superusers by default but access may be granted to others using - GRANT, with noted exceptions. - - - - Each of these functions returns true if - the signal was successfully sent and false - if sending the signal failed. - - - - Server Signaling Functions - - - - - Function - - - Description - - - - - - - - - pg_cancel_backend - - pg_cancel_backend ( pid integer ) - boolean - - - Cancels the current query of the session whose backend process has the - specified process ID. This is also allowed if the - calling role is a member of the role whose backend is being canceled or - the calling role has privileges of pg_signal_backend, - however only superusers can cancel superuser backends. - As an exception, roles with privileges of - pg_signal_autovacuum_worker are permitted to - cancel autovacuum worker processes, which are otherwise considered - superuser backends. - - - - - - - pg_log_backend_memory_contexts - - pg_log_backend_memory_contexts ( pid integer ) - boolean - - - Requests to log the memory contexts of the backend with the - specified process ID. This function can send the request to - backends and auxiliary processes except logger. These memory contexts - will be logged at - LOG message level. They will appear in - the server log based on the log configuration set - (see for more information), - but will not be sent to the client regardless of - . - - - - - - - pg_reload_conf - - pg_reload_conf () - boolean - - - Causes all processes of the PostgreSQL - server to reload their configuration files. (This is initiated by - sending a SIGHUP signal to the postmaster - process, which in turn sends SIGHUP to each - of its children.) You can use the - pg_file_settings, - pg_hba_file_rules and - pg_ident_file_mappings views - to check the configuration files for possible errors, before reloading. - - - - - - - pg_rotate_logfile - - pg_rotate_logfile () - boolean - - - Signals the log-file manager to switch to a new output file - immediately. This works only when the built-in log collector is - running, since otherwise there is no log-file manager subprocess. - - - - - - - pg_terminate_backend - - pg_terminate_backend ( pid integer, timeout bigint DEFAULT 0 ) - boolean - - - Terminates the session whose backend process has the - specified process ID. This is also allowed if the calling role - is a member of the role whose backend is being terminated or the - calling role has privileges of pg_signal_backend, - however only superusers can terminate superuser backends. - As an exception, roles with privileges of - pg_signal_autovacuum_worker are permitted to - terminate autovacuum worker processes, which are otherwise considered - superuser backends. - - - If timeout is not specified or zero, this - function returns true whether the process actually - terminates or not, indicating only that the sending of the signal was - successful. If the timeout is specified (in - milliseconds) and greater than zero, the function waits until the - process is actually terminated or until the given time has passed. If - the process is terminated, the function - returns true. On timeout, a warning is emitted and - false is returned. - - - - -
- - - pg_cancel_backend and pg_terminate_backend - send signals (SIGINT or SIGTERM - respectively) to backend processes identified by process ID. - The process ID of an active backend can be found from - the pid column of the - pg_stat_activity view, or by listing the - postgres processes on the server (using - ps on Unix or the Task - Manager on Windows). - The role of an active backend can be found from the - usename column of the - pg_stat_activity view. - - - - pg_log_backend_memory_contexts can be used - to log the memory contexts of a backend process. For example: - -postgres=# SELECT pg_log_backend_memory_contexts(pg_backend_pid()); - pg_log_backend_memory_contexts --------------------------------- - t -(1 row) - -One message for each memory context will be logged. For example: - -LOG: logging memory contexts of PID 10377 -STATEMENT: SELECT pg_log_backend_memory_contexts(pg_backend_pid()); -LOG: level: 1; TopMemoryContext: 80800 total in 6 blocks; 14432 free (5 chunks); 66368 used -LOG: level: 2; pgstat TabStatusArray lookup hash table: 8192 total in 1 blocks; 1408 free (0 chunks); 6784 used -LOG: level: 2; TopTransactionContext: 8192 total in 1 blocks; 7720 free (1 chunks); 472 used -LOG: level: 2; RowDescriptionContext: 8192 total in 1 blocks; 6880 free (0 chunks); 1312 used -LOG: level: 2; MessageContext: 16384 total in 2 blocks; 5152 free (0 chunks); 11232 used -LOG: level: 2; Operator class cache: 8192 total in 1 blocks; 512 free (0 chunks); 7680 used -LOG: level: 2; smgr relation table: 16384 total in 2 blocks; 4544 free (3 chunks); 11840 used -LOG: level: 2; TransactionAbortContext: 32768 total in 1 blocks; 32504 free (0 chunks); 264 used -... -LOG: level: 2; ErrorContext: 8192 total in 1 blocks; 7928 free (3 chunks); 264 used -LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 used - - If there are more than 100 child contexts under the same parent, the first - 100 child contexts are logged, along with a summary of the remaining contexts. - Note that frequent calls to this function could incur significant overhead, - because it may generate a large number of log messages. - - -
- - - Backup Control Functions - - - backup - - - - The functions shown in assist in making on-line backups. - These functions cannot be executed during recovery (except - pg_backup_start, - pg_backup_stop, - and pg_wal_lsn_diff). - - - - For details about proper usage of these functions, see - . - - - - Backup Control Functions - - - - - Function - - - Description - - - - - - - - - pg_create_restore_point - - pg_create_restore_point ( name text ) - pg_lsn - - - Creates a named marker record in the write-ahead log that can later be - used as a recovery target, and returns the corresponding write-ahead - log location. The given name can then be used with - to specify the point up to - which recovery will proceed. Avoid creating multiple restore points - with the same name, since recovery will stop at the first one whose - name matches the recovery target. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_current_wal_flush_lsn - - pg_current_wal_flush_lsn () - pg_lsn - - - Returns the current write-ahead log flush location (see notes below). - - - - - - - pg_current_wal_insert_lsn - - pg_current_wal_insert_lsn () - pg_lsn - - - Returns the current write-ahead log insert location (see notes below). - - - - - - - pg_current_wal_lsn - - pg_current_wal_lsn () - pg_lsn - - - Returns the current write-ahead log write location (see notes below). - - - - - - - pg_backup_start - - pg_backup_start ( - label text - , fast boolean - ) - pg_lsn - - - Prepares the server to begin an on-line backup. The only required - parameter is an arbitrary user-defined label for the backup. - (Typically this would be the name under which the backup dump file - will be stored.) - If the optional second parameter is given as true, - it specifies executing pg_backup_start as quickly - as possible. This forces an immediate checkpoint which will cause a - spike in I/O operations, slowing any concurrently executing queries. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_backup_stop - - pg_backup_stop ( - wait_for_archive boolean - ) - record - ( lsn pg_lsn, - labelfile text, - spcmapfile text ) - - - Finishes performing an on-line backup. The desired contents of the - backup label file and the tablespace map file are returned as part of - the result of the function and must be written to files in the - backup area. These files must not be written to the live data directory - (doing so will cause PostgreSQL to fail to restart in the event of a - crash). - - - There is an optional parameter of type boolean. - If false, the function will return immediately after the backup is - completed, without waiting for WAL to be archived. This behavior is - only useful with backup software that independently monitors WAL - archiving. Otherwise, WAL required to make the backup consistent might - be missing and make the backup useless. By default or when this - parameter is true, pg_backup_stop will wait for - WAL to be archived when archiving is enabled. (On a standby, this - means that it will wait only when archive_mode = - always. If write activity on the primary is low, - it may be useful to run pg_switch_wal on the - primary in order to trigger an immediate segment switch.) - - - When executed on a primary, this function also creates a backup - history file in the write-ahead log archive area. The history file - includes the label given to pg_backup_start, the - starting and ending write-ahead log locations for the backup, and the - starting and ending times of the backup. After recording the ending - location, the current write-ahead log insertion point is automatically - advanced to the next write-ahead log file, so that the ending - write-ahead log file can be archived immediately to complete the - backup. - - - The result of the function is a single record. - The lsn column holds the backup's ending - write-ahead log location (which again can be ignored). The second - column returns the contents of the backup label file, and the third - column returns the contents of the tablespace map file. These must be - stored as part of the backup and are required as part of the restore - process. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_switch_wal - - pg_switch_wal () - pg_lsn - - - Forces the server to switch to a new write-ahead log file, which - allows the current file to be archived (assuming you are using - continuous archiving). The result is the ending write-ahead log - location plus 1 within the just-completed write-ahead log file. If - there has been no write-ahead log activity since the last write-ahead - log switch, pg_switch_wal does nothing and - returns the start location of the write-ahead log file currently in - use. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_walfile_name - - pg_walfile_name ( lsn pg_lsn ) - text - - - Converts a write-ahead log location to the name of the WAL file - holding that location. - - - - - - - pg_walfile_name_offset - - pg_walfile_name_offset ( lsn pg_lsn ) - record - ( file_name text, - file_offset integer ) - - - Converts a write-ahead log location to a WAL file name and byte offset - within that file. - - - - - - - pg_split_walfile_name - - pg_split_walfile_name ( file_name text ) - record - ( segment_number numeric, - timeline_id bigint ) - - - Extracts the sequence number and timeline ID from a WAL file - name. - - - - - - - pg_wal_lsn_diff - - pg_wal_lsn_diff ( lsn1 pg_lsn, lsn2 pg_lsn ) - numeric - - - Calculates the difference in bytes (lsn1 - lsn2) between two write-ahead log - locations. This can be used - with pg_stat_replication or some of the - functions shown in to - get the replication lag. - - - - -
- - - pg_current_wal_lsn displays the current write-ahead - log write location in the same format used by the above functions. - Similarly, pg_current_wal_insert_lsn displays the - current write-ahead log insertion location - and pg_current_wal_flush_lsn displays the current - write-ahead log flush location. The insertion location is - the logical end of the write-ahead log at any instant, - while the write location is the end of what has actually been written out - from the server's internal buffers, and the flush location is the last - location known to be written to durable storage. The write location is the - end of what can be examined from outside the server, and is usually what - you want if you are interested in archiving partially-complete write-ahead - log files. The insertion and flush locations are made available primarily - for server debugging purposes. These are all read-only operations and do - not require superuser permissions. - - - - You can use pg_walfile_name_offset to extract the - corresponding write-ahead log file name and byte offset from - a pg_lsn value. For example: - -postgres=# SELECT * FROM pg_walfile_name_offset((pg_backup_stop()).lsn); - file_name | file_offset ---------------------------+------------- - 00000001000000000000000D | 4039624 -(1 row) - - Similarly, pg_walfile_name extracts just the write-ahead log file name. - - - - pg_split_walfile_name is useful to compute a - LSN from a file offset and WAL file name, for example: - -postgres=# \set file_name '000000010000000100C000AB' -postgres=# \set offset 256 -postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset AS lsn - FROM pg_split_walfile_name(:'file_name') pd, - pg_show_all_settings() ps - WHERE ps.name = 'wal_segment_size'; - lsn ---------------- - C001/AB000100 -(1 row) - - - -
- - - Recovery Control Functions - - - The functions shown in provide information - about the current status of a standby server. - These functions may be executed both during recovery and in normal running. - - - - Recovery Information Functions - - - - - Function - - - Description - - - - - - - - - pg_is_in_recovery - - pg_is_in_recovery () - boolean - - - Returns true if recovery is still in progress. - - - - - - - pg_last_wal_receive_lsn - - pg_last_wal_receive_lsn () - pg_lsn - - - Returns the last write-ahead log location that has been received and - synced to disk by streaming replication. While streaming replication - is in progress this will increase monotonically. If recovery has - completed then this will remain static at the location of the last WAL - record received and synced to disk during recovery. If streaming - replication is disabled, or if it has not yet started, the function - returns NULL. - - - - - - - pg_last_wal_replay_lsn - - pg_last_wal_replay_lsn () - pg_lsn - - - Returns the last write-ahead log location that has been replayed - during recovery. If recovery is still in progress this will increase - monotonically. If recovery has completed then this will remain - static at the location of the last WAL record applied during recovery. - When the server has been started normally without recovery, the - function returns NULL. - - - - - - - pg_last_xact_replay_timestamp - - pg_last_xact_replay_timestamp () - timestamp with time zone - - - Returns the time stamp of the last transaction replayed during - recovery. This is the time at which the commit or abort WAL record - for that transaction was generated on the primary. If no transactions - have been replayed during recovery, the function - returns NULL. Otherwise, if recovery is still in - progress this will increase monotonically. If recovery has completed - then this will remain static at the time of the last transaction - applied during recovery. When the server has been started normally - without recovery, the function returns NULL. - - - - - - - pg_get_wal_resource_managers - - pg_get_wal_resource_managers () - setof record - ( rm_id integer, - rm_name text, - rm_builtin boolean ) - - - Returns the currently-loaded WAL resource managers in the system. The - column rm_builtin indicates whether it's a - built-in resource manager, or a custom resource manager loaded by an - extension. - - - - -
- - - The functions shown in control the progress of recovery. - These functions may be executed only during recovery. - - - - Recovery Control Functions - - - - - Function - - - Description - - - - - - - - - pg_is_wal_replay_paused - - pg_is_wal_replay_paused () - boolean - - - Returns true if recovery pause is requested. - - - - - - - pg_get_wal_replay_pause_state - - pg_get_wal_replay_pause_state () - text - - - Returns recovery pause state. The return values are - not paused if pause is not requested, - pause requested if pause is requested but recovery is - not yet paused, and paused if the recovery is - actually paused. - - - - - - - pg_promote - - pg_promote ( wait boolean DEFAULT true, wait_seconds integer DEFAULT 60 ) - boolean - - - Promotes a standby server to primary status. - With wait set to true (the - default), the function waits until promotion is completed - or wait_seconds seconds have passed, and - returns true if promotion is successful - and false otherwise. - If wait is set to false, the - function returns true immediately after sending a - SIGUSR1 signal to the postmaster to trigger - promotion. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_wal_replay_pause - - pg_wal_replay_pause () - void - - - Request to pause recovery. A request doesn't mean that recovery stops - right away. If you want a guarantee that recovery is actually paused, - you need to check for the recovery pause state returned by - pg_get_wal_replay_pause_state(). Note that - pg_is_wal_replay_paused() returns whether a request - is made. While recovery is paused, no further database changes are applied. - If hot standby is active, all new queries will see the same consistent - snapshot of the database, and no further query conflicts will be generated - until recovery is resumed. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_wal_replay_resume - - pg_wal_replay_resume () - void - - - Restarts recovery if it was paused. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - -
- - - pg_wal_replay_pause and - pg_wal_replay_resume cannot be executed while - a promotion is ongoing. If a promotion is triggered while recovery - is paused, the paused state ends and promotion continues. - - - - If streaming replication is disabled, the paused state may continue - indefinitely without a problem. If streaming replication is in - progress then WAL records will continue to be received, which will - eventually fill available disk space, depending upon the duration of - the pause, the rate of WAL generation and available disk space. - - -
- - - Snapshot Synchronization Functions - - - PostgreSQL allows database sessions to synchronize their - snapshots. A snapshot determines which data is visible to the - transaction that is using the snapshot. Synchronized snapshots are - necessary when two or more sessions need to see identical content in the - database. If two sessions just start their transactions independently, - there is always a possibility that some third transaction commits - between the executions of the two START TRANSACTION commands, - so that one session sees the effects of that transaction and the other - does not. - - - - To solve this problem, PostgreSQL allows a transaction to - export the snapshot it is using. As long as the exporting - transaction remains open, other transactions can import its - snapshot, and thereby be guaranteed that they see exactly the same view - of the database that the first transaction sees. But note that any - database changes made by any one of these transactions remain invisible - to the other transactions, as is usual for changes made by uncommitted - transactions. So the transactions are synchronized with respect to - pre-existing data, but act normally for changes they make themselves. - - - - Snapshots are exported with the pg_export_snapshot function, - shown in , and - imported with the command. - - - - Snapshot Synchronization Functions - - - - - Function - - - Description - - - - - - - - - pg_export_snapshot - - pg_export_snapshot () - text - - - Saves the transaction's current snapshot and returns - a text string identifying the snapshot. This string must - be passed (outside the database) to clients that want to import the - snapshot. The snapshot is available for import only until the end of - the transaction that exported it. - - - A transaction can export more than one snapshot, if needed. Note that - doing so is only useful in READ COMMITTED - transactions, since in REPEATABLE READ and higher - isolation levels, transactions use the same snapshot throughout their - lifetime. Once a transaction has exported any snapshots, it cannot be - prepared with . - - - - - - pg_log_standby_snapshot - - pg_log_standby_snapshot () - pg_lsn - - - Take a snapshot of running transactions and write it to WAL, without - having to wait for bgwriter or checkpointer to log one. This is useful - for logical decoding on standby, as logical slot creation has to wait - until such a record is replayed on the standby. - - - - -
- -
- - - Replication Management Functions - - - The functions shown - in are for - controlling and interacting with replication features. - See , - , and - - for information about the underlying features. - Use of functions for replication origin is only allowed to the - superuser by default, but may be allowed to other users by using the - GRANT command. - Use of functions for replication slots is restricted to superusers - and users having REPLICATION privilege. - - - - Many of these functions have equivalent commands in the replication - protocol; see . - - - - The functions described in - , - , and - - are also relevant for replication. - - - - Replication Management Functions - - - - - Function - - - Description - - - - - - - - - pg_create_physical_replication_slot - - pg_create_physical_replication_slot ( slot_name name , immediately_reserve boolean, temporary boolean ) - record - ( slot_name name, - lsn pg_lsn ) - - - Creates a new physical replication slot named - slot_name. The optional second parameter, - when true, specifies that the LSN for this - replication slot be reserved immediately; otherwise - the LSN is reserved on first connection from a streaming - replication client. Streaming changes from a physical slot is only - possible with the streaming-replication protocol — - see . The optional third - parameter, temporary, when set to true, specifies that - the slot should not be permanently stored to disk and is only meant - for use by the current session. Temporary slots are also - released upon any error. This function corresponds - to the replication protocol command CREATE_REPLICATION_SLOT - ... PHYSICAL. - - - - - - - pg_drop_replication_slot - - pg_drop_replication_slot ( slot_name name ) - void - - - Drops the physical or logical replication slot - named slot_name. Same as replication protocol - command DROP_REPLICATION_SLOT. - - - - - - - pg_create_logical_replication_slot - - pg_create_logical_replication_slot ( slot_name name, plugin name , temporary boolean, twophase boolean, failover boolean ) - record - ( slot_name name, - lsn pg_lsn ) - - - Creates a new logical (decoding) replication slot named - slot_name using the output plugin - plugin. The optional third - parameter, temporary, when set to true, specifies that - the slot should not be permanently stored to disk and is only meant - for use by the current session. Temporary slots are also - released upon any error. The optional fourth parameter, - twophase, when set to true, specifies - that the decoding of prepared transactions is enabled for this - slot. The optional fifth parameter, - failover, when set to true, - specifies that this slot is enabled to be synced to the - standbys so that logical replication can be resumed after - failover. A call to this function has the same effect as - the replication protocol command - CREATE_REPLICATION_SLOT ... LOGICAL. - - - - - - - pg_copy_physical_replication_slot - - pg_copy_physical_replication_slot ( src_slot_name name, dst_slot_name name , temporary boolean ) - record - ( slot_name name, - lsn pg_lsn ) - - - Copies an existing physical replication slot named src_slot_name - to a physical replication slot named dst_slot_name. - The copied physical slot starts to reserve WAL from the same LSN as the - source slot. - temporary is optional. If temporary - is omitted, the same value as the source slot is used. Copy of an - invalidated slot is not allowed. - - - - - - - pg_copy_logical_replication_slot - - pg_copy_logical_replication_slot ( src_slot_name name, dst_slot_name name , temporary boolean , plugin name ) - record - ( slot_name name, - lsn pg_lsn ) - - - Copies an existing logical replication slot - named src_slot_name to a logical replication - slot named dst_slot_name, optionally changing - the output plugin and persistence. The copied logical slot starts - from the same LSN as the source logical slot. Both - temporary and plugin are - optional; if they are omitted, the values of the source slot are used. - The failover option of the source logical slot - is not copied and is set to false by default. This - is to avoid the risk of being unable to continue logical replication - after failover to standby where the slot is being synchronized. Copy of - an invalidated slot is not allowed. - - - - - - - pg_logical_slot_get_changes - - pg_logical_slot_get_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) - setof record - ( lsn pg_lsn, - xid xid, - data text ) - - - Returns changes in the slot slot_name, starting - from the point from which changes have been consumed last. If - upto_lsn - and upto_nchanges are NULL, - logical decoding will continue until end of WAL. If - upto_lsn is non-NULL, decoding will include only - those transactions which commit prior to the specified LSN. If - upto_nchanges is non-NULL, decoding will - stop when the number of rows produced by decoding exceeds - the specified value. Note, however, that the actual number of - rows returned may be larger, since this limit is only checked after - adding the rows produced when decoding each new transaction commit. - If the specified slot is a logical failover slot then the function will - not return until all physical slots specified in - synchronized_standby_slots - have confirmed WAL receipt. - - - - - - - pg_logical_slot_peek_changes - - pg_logical_slot_peek_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) - setof record - ( lsn pg_lsn, - xid xid, - data text ) - - - Behaves just like - the pg_logical_slot_get_changes() function, - except that changes are not consumed; that is, they will be returned - again on future calls. - - - - - - - pg_logical_slot_get_binary_changes - - pg_logical_slot_get_binary_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) - setof record - ( lsn pg_lsn, - xid xid, - data bytea ) - - - Behaves just like - the pg_logical_slot_get_changes() function, - except that changes are returned as bytea. - - - - - - - pg_logical_slot_peek_binary_changes - - pg_logical_slot_peek_binary_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) - setof record - ( lsn pg_lsn, - xid xid, - data bytea ) - - - Behaves just like - the pg_logical_slot_peek_changes() function, - except that changes are returned as bytea. - - - - - - - pg_replication_slot_advance - - pg_replication_slot_advance ( slot_name name, upto_lsn pg_lsn ) - record - ( slot_name name, - end_lsn pg_lsn ) - - - Advances the current confirmed position of a replication slot named - slot_name. The slot will not be moved backwards, - and it will not be moved beyond the current insert location. Returns - the name of the slot and the actual position that it was advanced to. - The updated slot position information is written out at the next - checkpoint if any advancing is done. So in the event of a crash, the - slot may return to an earlier position. If the specified slot is a - logical failover slot then the function will not return until all - physical slots specified in - synchronized_standby_slots - have confirmed WAL receipt. - - - - - - - pg_replication_origin_create - - pg_replication_origin_create ( node_name text ) - oid - - - Creates a replication origin with the given external - name, and returns the internal ID assigned to it. - The name must be no longer than 512 bytes. - - - - - - - pg_replication_origin_drop - - pg_replication_origin_drop ( node_name text ) - void - - - Deletes a previously-created replication origin, including any - associated replay progress. - - - - - - - pg_replication_origin_oid - - pg_replication_origin_oid ( node_name text ) - oid - - - Looks up a replication origin by name and returns the internal ID. If - no such replication origin is found, NULL is - returned. - - - - - - - pg_replication_origin_session_setup - - pg_replication_origin_session_setup ( node_name text ) - void - - - Marks the current session as replaying from the given - origin, allowing replay progress to be tracked. - Can only be used if no origin is currently selected. - Use pg_replication_origin_session_reset to undo. - - - - - - - pg_replication_origin_session_reset - - pg_replication_origin_session_reset () - void - - - Cancels the effects - of pg_replication_origin_session_setup(). - - - - - - - pg_replication_origin_session_is_setup - - pg_replication_origin_session_is_setup () - boolean - - - Returns true if a replication origin has been selected in the - current session. - - - - - - - pg_replication_origin_session_progress - - pg_replication_origin_session_progress ( flush boolean ) - pg_lsn - - - Returns the replay location for the replication origin selected in - the current session. The parameter flush - determines whether the corresponding local transaction will be - guaranteed to have been flushed to disk or not. - - - - - - - pg_replication_origin_xact_setup - - pg_replication_origin_xact_setup ( origin_lsn pg_lsn, origin_timestamp timestamp with time zone ) - void - - - Marks the current transaction as replaying a transaction that has - committed at the given LSN and timestamp. Can - only be called when a replication origin has been selected - using pg_replication_origin_session_setup. - - - - - - - pg_replication_origin_xact_reset - - pg_replication_origin_xact_reset () - void - - - Cancels the effects of - pg_replication_origin_xact_setup(). - - - - - - - pg_replication_origin_advance - - pg_replication_origin_advance ( node_name text, lsn pg_lsn ) - void - - - Sets replication progress for the given node to the given - location. This is primarily useful for setting up the initial - location, or setting a new location after configuration changes and - similar. Be aware that careless use of this function can lead to - inconsistently replicated data. - - - - - - - pg_replication_origin_progress - - pg_replication_origin_progress ( node_name text, flush boolean ) - pg_lsn - - - Returns the replay location for the given replication origin. The - parameter flush determines whether the - corresponding local transaction will be guaranteed to have been - flushed to disk or not. - - - - - - - pg_logical_emit_message - - pg_logical_emit_message ( transactional boolean, prefix text, content text , flush boolean DEFAULT false ) - pg_lsn - - - pg_logical_emit_message ( transactional boolean, prefix text, content bytea , flush boolean DEFAULT false ) - pg_lsn - - - Emits a logical decoding message. This can be used to pass generic - messages to logical decoding plugins through - WAL. The transactional parameter specifies if - the message should be part of the current transaction, or if it should - be written immediately and decoded as soon as the logical decoder - reads the record. The prefix parameter is a - textual prefix that can be used by logical decoding plugins to easily - recognize messages that are interesting for them. - The content parameter is the content of the - message, given either in text or binary form. - The flush parameter (default set to - false) controls if the message is immediately - flushed to WAL or not. flush has no effect - with transactional, as the message's WAL - record is flushed along with its transaction. - - - - - - - pg_sync_replication_slots - - pg_sync_replication_slots () - void - - - Synchronize the logical failover replication slots from the primary - server to the standby server. This function can only be executed on the - standby server. Temporary synced slots, if any, cannot be used for - logical decoding and must be dropped after promotion. See - for details. - Note that this function cannot be executed if - - sync_replication_slots is enabled and the slotsync - worker is already running to perform the synchronization of slots. - - - - - If, after executing the function, - - hot_standby_feedback is disabled on - the standby or the physical slot configured in - - primary_slot_name is - removed, then it is possible that the necessary rows of the - synchronized slot will be removed by the VACUUM process on the primary - server, resulting in the synchronized slot becoming invalidated. - - - - - - - -
- -
- - - Database Object Management Functions - - - The functions shown in calculate - the disk space usage of database objects, or assist in presentation - or understanding of usage results. bigint results - are measured in bytes. If an OID that does - not represent an existing object is passed to one of these - functions, NULL is returned. - - - - Database Object Size Functions - - - - - Function - - - Description - - - - - - - - - pg_column_size - - pg_column_size ( "any" ) - integer - - - Shows the number of bytes used to store any individual data value. If - applied directly to a table column value, this reflects any - compression that was done. - - - - - - - pg_column_compression - - pg_column_compression ( "any" ) - text - - - Shows the compression algorithm that was used to compress - an individual variable-length value. Returns NULL - if the value is not compressed. - - - - - - - pg_column_toast_chunk_id - - pg_column_toast_chunk_id ( "any" ) - oid - - - Shows the chunk_id of an on-disk - TOASTed value. Returns NULL - if the value is un-TOASTed or not on-disk. See - for more information about - TOAST. - - - - - - - pg_database_size - - pg_database_size ( name ) - bigint - - - pg_database_size ( oid ) - bigint - - - Computes the total disk space used by the database with the specified - name or OID. To use this function, you must - have CONNECT privilege on the specified database - (which is granted by default) or have privileges of - the pg_read_all_stats role. - - - - - - - pg_indexes_size - - pg_indexes_size ( regclass ) - bigint - - - Computes the total disk space used by indexes attached to the - specified table. - - - - - - - pg_relation_size - - pg_relation_size ( relation regclass , fork text ) - bigint - - - Computes the disk space used by one fork of the - specified relation. (Note that for most purposes it is more - convenient to use the higher-level - functions pg_total_relation_size - or pg_table_size, which sum the sizes of all - forks.) With one argument, this returns the size of the main data - fork of the relation. The second argument can be provided to specify - which fork to examine: - - - - main returns the size of the main - data fork of the relation. - - - - - fsm returns the size of the Free Space Map - (see ) associated with the relation. - - - - - vm returns the size of the Visibility Map - (see ) associated with the relation. - - - - - init returns the size of the initialization - fork, if any, associated with the relation. - - - - - - - - - - pg_size_bytes - - pg_size_bytes ( text ) - bigint - - - Converts a size in human-readable format (as returned - by pg_size_pretty) into bytes. Valid units are - bytes, B, kB, - MB, GB, TB, - and PB. - - - - - - - pg_size_pretty - - pg_size_pretty ( bigint ) - text - - - pg_size_pretty ( numeric ) - text - - - Converts a size in bytes into a more easily human-readable format with - size units (bytes, kB, MB, GB, TB, or PB as appropriate). Note that the - units are powers of 2 rather than powers of 10, so 1kB is 1024 bytes, - 1MB is 10242 = 1048576 bytes, and so on. - - - - - - - pg_table_size - - pg_table_size ( regclass ) - bigint - - - Computes the disk space used by the specified table, excluding indexes - (but including its TOAST table if any, free space map, and visibility - map). - - - - - - - pg_tablespace_size - - pg_tablespace_size ( name ) - bigint - - - pg_tablespace_size ( oid ) - bigint - - - Computes the total disk space used in the tablespace with the - specified name or OID. To use this function, you must - have CREATE privilege on the specified tablespace - or have privileges of the pg_read_all_stats role, - unless it is the default tablespace for the current database. - - - - - - - pg_total_relation_size - - pg_total_relation_size ( regclass ) - bigint - - - Computes the total disk space used by the specified table, including - all indexes and TOAST data. The result is - equivalent to pg_table_size - + pg_indexes_size. - - - - -
- - - The functions above that operate on tables or indexes accept a - regclass argument, which is simply the OID of the table or index - in the pg_class system catalog. You do not have to look up - the OID by hand, however, since the regclass data type's input - converter will do the work for you. See - for details. - - - - The functions shown in assist - in identifying the specific disk files associated with database objects. - - - - Database Object Location Functions - - - - - Function - - - Description - - - - - - - - - pg_relation_filenode - - pg_relation_filenode ( relation regclass ) - oid - - - Returns the filenode number currently assigned to the - specified relation. The filenode is the base component of the file - name(s) used for the relation (see - for more information). - For most relations the result is the same as - pg_class.relfilenode, - but for certain system catalogs relfilenode - is zero and this function must be used to get the correct value. The - function returns NULL if passed a relation that does not have storage, - such as a view. - - - - - - - pg_relation_filepath - - pg_relation_filepath ( relation regclass ) - text - - - Returns the entire file path name (relative to the database cluster's - data directory, PGDATA) of the relation. - - - - - - - pg_filenode_relation - - pg_filenode_relation ( tablespace oid, filenode oid ) - regclass - - - Returns a relation's OID given the tablespace OID and filenode it is - stored under. This is essentially the inverse mapping of - pg_relation_filepath. For a relation in the - database's default tablespace, the tablespace can be specified as zero. - Returns NULL if no relation in the current database - is associated with the given values. - - - - -
- - - lists functions used to manage - collations. - - - - Collation Management Functions - - - - - Function - - - Description - - - - - - - - - pg_collation_actual_version - - pg_collation_actual_version ( oid ) - text - - - Returns the actual version of the collation object as it is currently - installed in the operating system. If this is different from the - value in - pg_collation.collversion, - then objects depending on the collation might need to be rebuilt. See - also . - - - - - - - pg_database_collation_actual_version - - pg_database_collation_actual_version ( oid ) - text - - - Returns the actual version of the database's collation as it is currently - installed in the operating system. If this is different from the - value in - pg_database.datcollversion, - then objects depending on the collation might need to be rebuilt. See - also . - - - - - - - pg_import_system_collations - - pg_import_system_collations ( schema regnamespace ) - integer - - - Adds collations to the system - catalog pg_collation based on all the locales - it finds in the operating system. This is - what initdb uses; see - for more details. If additional - locales are installed into the operating system later on, this - function can be run again to add collations for the new locales. - Locales that match existing entries - in pg_collation will be skipped. (But - collation objects based on locales that are no longer present in the - operating system are not removed by this function.) - The schema parameter would typically - be pg_catalog, but that is not a requirement; the - collations could be installed into some other schema as well. The - function returns the number of new collation objects it created. - Use of this function is restricted to superusers. - - - - -
- - - lists functions used to - manipulate statistics. - These functions cannot be executed during recovery. - - - Changes made by these statistics manipulation functions are likely to be - overwritten by autovacuum (or manual - VACUUM or ANALYZE) and should be - considered temporary. - - - - - - Database Object Statistics Manipulation Functions - - - - - Function - - - Description - - - - - - - - - pg_restore_relation_stats - - pg_restore_relation_stats ( - VARIADIC kwargs "any" ) - boolean - - - Updates table-level statistics. Ordinarily, these statistics are - collected automatically or updated as a part of or , so it's not - necessary to call this function. However, it is useful after a - restore to enable the optimizer to choose better plans if - ANALYZE has not been run yet. - - - The tracked statistics may change from version to version, so - arguments are passed as pairs of argname - and argvalue in the form: - -SELECT pg_restore_relation_stats( - 'arg1name', 'arg1value'::arg1type, - 'arg2name', 'arg2value'::arg2type, - 'arg3name', 'arg3value'::arg3type); - - - - For example, to set the relpages and - reltuples values for the table - mytable: - -SELECT pg_restore_relation_stats( - 'schemaname', 'myschema', - 'relname', 'mytable', - 'relpages', 173::integer, - 'reltuples', 10000::real); - - - - The arguments schemaname and - relname are required, and specify the table. Other - arguments are the names and values of statistics corresponding to - certain columns in pg_class. - The currently-supported relation statistics are - relpages with a value of type - integer, reltuples with a value of - type real, relallvisible with a value - of type integer, and relallfrozen - with a value of type integer. - - - Additionally, this function accepts argument name - version of type integer, which - specifies the server version from which the statistics originated. - This is anticipated to be helpful in porting statistics from older - versions of PostgreSQL. - - - Minor errors are reported as a WARNING and - ignored, and remaining statistics will still be restored. If all - specified statistics are successfully restored, returns - true, otherwise false. - - - The caller must have the MAINTAIN privilege on the - table or be the owner of the database. - - - - - - - - - pg_clear_relation_stats - - pg_clear_relation_stats ( schemaname text, relname text ) - void - - - Clears table-level statistics for the given relation, as though the - table was newly created. - - - The caller must have the MAINTAIN privilege on the - table or be the owner of the database. - - - - - - - - pg_restore_attribute_stats - - pg_restore_attribute_stats ( - VARIADIC kwargs "any" ) - boolean - - - Creates or updates column-level statistics. Ordinarily, these - statistics are collected automatically or updated as a part of or , so it's not - necessary to call this function. However, it is useful after a - restore to enable the optimizer to choose better plans if - ANALYZE has not been run yet. - - - The tracked statistics may change from version to version, so - arguments are passed as pairs of argname - and argvalue in the form: - -SELECT pg_restore_attribute_stats( - 'arg1name', 'arg1value'::arg1type, - 'arg2name', 'arg2value'::arg2type, - 'arg3name', 'arg3value'::arg3type); - - - - For example, to set the avg_width and - null_frac values for the attribute - col1 of the table - mytable: - -SELECT pg_restore_attribute_stats( - 'schemaname', 'myschema', - 'relname', 'mytable', - 'attname', 'col1', - 'inherited', false, - 'avg_width', 125::integer, - 'null_frac', 0.5::real); - - - - The required arguments are schemaname and - relname with a value of type text - which specify the table; either attname with a - value of type text or attnum with a - value of type smallint, which specifies the column; and - inherited, which specifies whether the statistics - include values from child tables. Other arguments are the names and - values of statistics corresponding to columns in pg_stats. - - - Additionally, this function accepts argument name - version of type integer, which - specifies the server version from which the statistics originated. - This is anticipated to be helpful in porting statistics from older - versions of PostgreSQL. - - - Minor errors are reported as a WARNING and - ignored, and remaining statistics will still be restored. If all - specified statistics are successfully restored, returns - true, otherwise false. - - - The caller must have the MAINTAIN privilege on the - table or be the owner of the database. - - - - - - - - - pg_clear_attribute_stats - - pg_clear_attribute_stats ( - schemaname text, - relname text, - attname text, - inherited boolean ) - void - - - Clears column-level statistics for the given relation and - attribute, as though the table was newly created. - - - The caller must have the MAINTAIN privilege on - the table or be the owner of the database. - - - - - -
- - - lists functions that provide - information about the structure of partitioned tables. - - - - Partitioning Information Functions - - - - - Function - - - Description - - - - - - - - - pg_partition_tree - - pg_partition_tree ( regclass ) - setof record - ( relid regclass, - parentrelid regclass, - isleaf boolean, - level integer ) - - - Lists the tables or indexes in the partition tree of the - given partitioned table or partitioned index, with one row for each - partition. Information provided includes the OID of the partition, - the OID of its immediate parent, a boolean value telling if the - partition is a leaf, and an integer telling its level in the hierarchy. - The level value is 0 for the input table or index, 1 for its - immediate child partitions, 2 for their partitions, and so on. - Returns no rows if the relation does not exist or is not a partition - or partitioned table. - - - - - - - pg_partition_ancestors - - pg_partition_ancestors ( regclass ) - setof regclass - - - Lists the ancestor relations of the given partition, - including the relation itself. Returns no rows if the relation - does not exist or is not a partition or partitioned table. - - - - - - - pg_partition_root - - pg_partition_root ( regclass ) - regclass - - - Returns the top-most parent of the partition tree to which the given - relation belongs. Returns NULL if the relation - does not exist or is not a partition or partitioned table. - - - - -
- - - For example, to check the total size of the data contained in a - partitioned table measurement, one could use the - following query: - -SELECT pg_size_pretty(sum(pg_relation_size(relid))) AS total_size - FROM pg_partition_tree('measurement'); - - - -
- - - Index Maintenance Functions - - - shows the functions - available for index maintenance tasks. (Note that these maintenance - tasks are normally done automatically by autovacuum; use of these - functions is only required in special cases.) - These functions cannot be executed during recovery. - Use of these functions is restricted to superusers and the owner - of the given index. - - - - Index Maintenance Functions - - - - - Function - - - Description - - - - - - - - - brin_summarize_new_values - - brin_summarize_new_values ( index regclass ) - integer - - - Scans the specified BRIN index to find page ranges in the base table - that are not currently summarized by the index; for any such range it - creates a new summary index tuple by scanning those table pages. - Returns the number of new page range summaries that were inserted - into the index. - - - - - - - brin_summarize_range - - brin_summarize_range ( index regclass, blockNumber bigint ) - integer - - - Summarizes the page range covering the given block, if not already - summarized. This is - like brin_summarize_new_values except that it - only processes the page range that covers the given table block number. - - - - - - - brin_desummarize_range - - brin_desummarize_range ( index regclass, blockNumber bigint ) - void - - - Removes the BRIN index tuple that summarizes the page range covering - the given table block, if there is one. - - - - - - - gin_clean_pending_list - - gin_clean_pending_list ( index regclass ) - bigint - - - Cleans up the pending list of the specified GIN index - by moving entries in it, in bulk, to the main GIN data structure. - Returns the number of pages removed from the pending list. - If the argument is a GIN index built with - the fastupdate option disabled, no cleanup happens - and the result is zero, because the index doesn't have a pending list. - See and - for details about the pending list and fastupdate - option. - - - - -
- -
- - - Generic File Access Functions - - - The functions shown in provide native access to - files on the machine hosting the server. Only files within the - database cluster directory and the log_directory can be - accessed, unless the user is a superuser or is granted the role - pg_read_server_files. Use a relative path for files in - the cluster directory, and a path matching the log_directory - configuration setting for log files. - - - - Note that granting users the EXECUTE privilege on - pg_read_file(), or related functions, allows them the - ability to read any file on the server that the database server process can - read; these functions bypass all in-database privilege checks. This means - that, for example, a user with such access is able to read the contents of - the pg_authid table where authentication - information is stored, as well as read any table data in the database. - Therefore, granting access to these functions should be carefully - considered. - - - - When granting privilege on these functions, note that the table entries - showing optional parameters are mostly implemented as several physical - functions with different parameter lists. Privilege must be granted - separately on each such function, if it is to be - used. psql's \df command - can be useful to check what the actual function signatures are. - - - - Some of these functions take an optional missing_ok - parameter, which specifies the behavior when the file or directory does - not exist. If true, the function - returns NULL or an empty result set, as appropriate. - If false, an error is raised. (Failure conditions - other than file not found are reported as errors in any - case.) The default is false. - - - - Generic File Access Functions - - - - - Function - - - Description - - - - - - - - - pg_ls_dir - - pg_ls_dir ( dirname text , missing_ok boolean, include_dot_dirs boolean ) - setof text - - - Returns the names of all files (and directories and other special - files) in the specified - directory. The include_dot_dirs parameter - indicates whether . and .. are to be - included in the result set; the default is to exclude them. Including - them can be useful when missing_ok - is true, to distinguish an empty directory from a - non-existent directory. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_ls_logdir - - pg_ls_logdir () - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's log directory. Filenames beginning with - a dot, directories, and other special files are excluded. - - - This function is restricted to superusers and roles with privileges of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_ls_waldir - - pg_ls_waldir () - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's write-ahead log (WAL) directory. - Filenames beginning with a dot, directories, and other special files - are excluded. - - - This function is restricted to superusers and roles with privileges of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_ls_logicalmapdir - - pg_ls_logicalmapdir () - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's pg_logical/mappings - directory. Filenames beginning with a dot, directories, and other - special files are excluded. - - - This function is restricted to superusers and members of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_ls_logicalsnapdir - - pg_ls_logicalsnapdir () - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's pg_logical/snapshots - directory. Filenames beginning with a dot, directories, and other - special files are excluded. - - - This function is restricted to superusers and members of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_ls_replslotdir - - pg_ls_replslotdir ( slot_name text ) - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's pg_replslot/slot_name - directory, where slot_name is the name of the - replication slot provided as input of the function. Filenames beginning - with a dot, directories, and other special files are excluded. - - - This function is restricted to superusers and members of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_ls_summariesdir - - pg_ls_summariesdir () - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's WAL summaries directory - (pg_wal/summaries). Filenames beginning - with a dot, directories, and other special files are excluded. - - - This function is restricted to superusers and members of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_ls_archive_statusdir - - pg_ls_archive_statusdir () - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the server's WAL archive status directory - (pg_wal/archive_status). Filenames beginning - with a dot, directories, and other special files are excluded. - - - This function is restricted to superusers and members of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - - pg_ls_tmpdir - - pg_ls_tmpdir ( tablespace oid ) - setof record - ( name text, - size bigint, - modification timestamp with time zone ) - - - Returns the name, size, and last modification time (mtime) of each - ordinary file in the temporary file directory for the - specified tablespace. - If tablespace is not provided, - the pg_default tablespace is examined. Filenames - beginning with a dot, directories, and other special files are - excluded. - - - This function is restricted to superusers and members of - the pg_monitor role by default, but other users can - be granted EXECUTE to run the function. - - - - - - - pg_read_file - - pg_read_file ( filename text , offset bigint, length bigint , missing_ok boolean ) - text - - - Returns all or part of a text file, starting at the - given byte offset, returning at - most length bytes (less if the end of file is - reached first). If offset is negative, it is - relative to the end of the file. If offset - and length are omitted, the entire file is - returned. The bytes read from the file are interpreted as a string in - the database's encoding; an error is thrown if they are not valid in - that encoding. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - - - pg_read_binary_file - - pg_read_binary_file ( filename text , offset bigint, length bigint , missing_ok boolean ) - bytea - - - Returns all or part of a file. This function is identical to - pg_read_file except that it can read arbitrary - binary data, returning the result as bytea - not text; accordingly, no encoding checks are performed. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - In combination with the convert_from function, - this function can be used to read a text file in a specified encoding - and convert to the database's encoding: - -SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8'); - - - - - - - - pg_stat_file - - pg_stat_file ( filename text , missing_ok boolean ) - record - ( size bigint, - access timestamp with time zone, - modification timestamp with time zone, - change timestamp with time zone, - creation timestamp with time zone, - isdir boolean ) - - - Returns a record containing the file's size, last access time stamp, - last modification time stamp, last file status change time stamp (Unix - platforms only), file creation time stamp (Windows only), and a flag - indicating if it is a directory. - - - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. - - - - - -
- -
- - - Advisory Lock Functions - - - The functions shown in - manage advisory locks. For details about proper use of these functions, - see . - - - - All these functions are intended to be used to lock application-defined - resources, which can be identified either by a single 64-bit key value or - two 32-bit key values (note that these two key spaces do not overlap). - If another session already holds a conflicting lock on the same resource - identifier, the functions will either wait until the resource becomes - available, or return a false result, as appropriate for - the function. - Locks can be either shared or exclusive: a shared lock does not conflict - with other shared locks on the same resource, only with exclusive locks. - Locks can be taken at session level (so that they are held until released - or the session ends) or at transaction level (so that they are held until - the current transaction ends; there is no provision for manual release). - Multiple session-level lock requests stack, so that if the same resource - identifier is locked three times there must then be three unlock requests - to release the resource in advance of session end. - - - - Advisory Lock Functions - - - - - Function - - - Description - - - - - - - - - pg_advisory_lock - - pg_advisory_lock ( key bigint ) - void - - - pg_advisory_lock ( key1 integer, key2 integer ) - void - - - Obtains an exclusive session-level advisory lock, waiting if necessary. - - - - - - - pg_advisory_lock_shared - - pg_advisory_lock_shared ( key bigint ) - void - - - pg_advisory_lock_shared ( key1 integer, key2 integer ) - void - - - Obtains a shared session-level advisory lock, waiting if necessary. - - - - - - - pg_advisory_unlock - - pg_advisory_unlock ( key bigint ) - boolean - - - pg_advisory_unlock ( key1 integer, key2 integer ) - boolean - - - Releases a previously-acquired exclusive session-level advisory lock. - Returns true if the lock is successfully released. - If the lock was not held, false is returned, and in - addition, an SQL warning will be reported by the server. - - - - - - - pg_advisory_unlock_all - - pg_advisory_unlock_all () - void - - - Releases all session-level advisory locks held by the current session. - (This function is implicitly invoked at session end, even if the - client disconnects ungracefully.) - - - - - - - pg_advisory_unlock_shared - - pg_advisory_unlock_shared ( key bigint ) - boolean - - - pg_advisory_unlock_shared ( key1 integer, key2 integer ) - boolean - - - Releases a previously-acquired shared session-level advisory lock. - Returns true if the lock is successfully released. - If the lock was not held, false is returned, and in - addition, an SQL warning will be reported by the server. - - - - - - - pg_advisory_xact_lock - - pg_advisory_xact_lock ( key bigint ) - void - - - pg_advisory_xact_lock ( key1 integer, key2 integer ) - void - - - Obtains an exclusive transaction-level advisory lock, waiting if - necessary. - - - - - - - pg_advisory_xact_lock_shared - - pg_advisory_xact_lock_shared ( key bigint ) - void - - - pg_advisory_xact_lock_shared ( key1 integer, key2 integer ) - void - - - Obtains a shared transaction-level advisory lock, waiting if - necessary. - - - - - - - pg_try_advisory_lock - - pg_try_advisory_lock ( key bigint ) - boolean - - - pg_try_advisory_lock ( key1 integer, key2 integer ) - boolean - - - Obtains an exclusive session-level advisory lock if available. - This will either obtain the lock immediately and - return true, or return false - without waiting if the lock cannot be acquired immediately. - - - - - - - pg_try_advisory_lock_shared - - pg_try_advisory_lock_shared ( key bigint ) - boolean - - - pg_try_advisory_lock_shared ( key1 integer, key2 integer ) - boolean - - - Obtains a shared session-level advisory lock if available. - This will either obtain the lock immediately and - return true, or return false - without waiting if the lock cannot be acquired immediately. - - - - - - - pg_try_advisory_xact_lock - - pg_try_advisory_xact_lock ( key bigint ) - boolean - - - pg_try_advisory_xact_lock ( key1 integer, key2 integer ) - boolean - - - Obtains an exclusive transaction-level advisory lock if available. - This will either obtain the lock immediately and - return true, or return false - without waiting if the lock cannot be acquired immediately. - - - - - - - pg_try_advisory_xact_lock_shared - - pg_try_advisory_xact_lock_shared ( key bigint ) - boolean - - - pg_try_advisory_xact_lock_shared ( key1 integer, key2 integer ) - boolean - - - Obtains a shared transaction-level advisory lock if available. - This will either obtain the lock immediately and - return true, or return false - without waiting if the lock cannot be acquired immediately. - - - - -
- -
- -
- - - Trigger Functions - - - While many uses of triggers involve user-written trigger functions, - PostgreSQL provides a few built-in trigger - functions that can be used directly in user-defined triggers. These - are summarized in . - (Additional built-in trigger functions exist, which implement foreign - key constraints and deferred index constraints. Those are not documented - here since users need not use them directly.) - - - - For more information about creating triggers, see - . - - - - Built-In Trigger Functions - - - - - Function - - - Description - - - Example Usage - - - - - - - - - suppress_redundant_updates_trigger - - suppress_redundant_updates_trigger ( ) - trigger - - - Suppresses do-nothing update operations. See below for details. - - - CREATE TRIGGER ... suppress_redundant_updates_trigger() - - - - - - - tsvector_update_trigger - - tsvector_update_trigger ( ) - trigger - - - Automatically updates a tsvector column from associated - plain-text document column(s). The text search configuration to use - is specified by name as a trigger argument. See - for details. - - - CREATE TRIGGER ... tsvector_update_trigger(tsvcol, 'pg_catalog.swedish', title, body) - - - - - - - tsvector_update_trigger_column - - tsvector_update_trigger_column ( ) - trigger - - - Automatically updates a tsvector column from associated - plain-text document column(s). The text search configuration to use - is taken from a regconfig column of the table. See - for details. - - - CREATE TRIGGER ... tsvector_update_trigger_column(tsvcol, tsconfigcol, title, body) - - - - -
- - - The suppress_redundant_updates_trigger function, - when applied as a row-level BEFORE UPDATE trigger, - will prevent any update that does not actually change the data in the - row from taking place. This overrides the normal behavior which always - performs a physical row update - regardless of whether or not the data has changed. (This normal behavior - makes updates run faster, since no checking is required, and is also - useful in certain cases.) - - - - Ideally, you should avoid running updates that don't actually - change the data in the record. Redundant updates can cost considerable - unnecessary time, especially if there are lots of indexes to alter, - and space in dead rows that will eventually have to be vacuumed. - However, detecting such situations in client code is not - always easy, or even possible, and writing expressions to detect - them can be error-prone. An alternative is to use - suppress_redundant_updates_trigger, which will skip - updates that don't change the data. You should use this with care, - however. The trigger takes a small but non-trivial time for each record, - so if most of the records affected by updates do actually change, - use of this trigger will make updates run slower on average. - - - - The suppress_redundant_updates_trigger function can be - added to a table like this: - -CREATE TRIGGER z_min_update -BEFORE UPDATE ON tablename -FOR EACH ROW EXECUTE FUNCTION suppress_redundant_updates_trigger(); - - In most cases, you need to fire this trigger last for each row, so that - it does not override other triggers that might wish to alter the row. - Bearing in mind that triggers fire in name order, you would therefore - choose a trigger name that comes after the name of any other trigger - you might have on the table. (Hence the z prefix in the - example.) - -
- - - Event Trigger Functions - - - PostgreSQL provides these helper functions - to retrieve information from event triggers. - - - - For more information about event triggers, - see . - - - - Capturing Changes at Command End - - - pg_event_trigger_ddl_commands - - - -pg_event_trigger_ddl_commands () setof record - - - - pg_event_trigger_ddl_commands returns a list of - DDL commands executed by each user action, - when invoked in a function attached to a - ddl_command_end event trigger. If called in any other - context, an error is raised. - pg_event_trigger_ddl_commands returns one row for each - base command executed; some commands that are a single SQL sentence - may return more than one row. This function returns the following - columns: - - - - - - Name - Type - Description - - - - - - classid - oid - OID of catalog the object belongs in - - - objid - oid - OID of the object itself - - - objsubid - integer - Sub-object ID (e.g., attribute number for a column) - - - command_tag - text - Command tag - - - object_type - text - Type of the object - - - schema_name - text - - Name of the schema the object belongs in, if any; otherwise NULL. - No quoting is applied. - - - - object_identity - text - - Text rendering of the object identity, schema-qualified. Each - identifier included in the identity is quoted if necessary. - - - - in_extension - boolean - True if the command is part of an extension script - - - command - pg_ddl_command - - A complete representation of the command, in internal format. - This cannot be output directly, but it can be passed to other - functions to obtain different pieces of information about the - command. - - - - - - - - - - Processing Objects Dropped by a DDL Command - - - pg_event_trigger_dropped_objects - - - -pg_event_trigger_dropped_objects () setof record - - - - pg_event_trigger_dropped_objects returns a list of all objects - dropped by the command in whose sql_drop event it is called. - If called in any other context, an error is raised. - This function returns the following columns: - - - - - - Name - Type - Description - - - - - - classid - oid - OID of catalog the object belonged in - - - objid - oid - OID of the object itself - - - objsubid - integer - Sub-object ID (e.g., attribute number for a column) - - - original - boolean - True if this was one of the root object(s) of the deletion - - - normal - boolean - - True if there was a normal dependency relationship - in the dependency graph leading to this object - - - - is_temporary - boolean - - True if this was a temporary object - - - - object_type - text - Type of the object - - - schema_name - text - - Name of the schema the object belonged in, if any; otherwise NULL. - No quoting is applied. - - - - object_name - text - - Name of the object, if the combination of schema and name can be - used as a unique identifier for the object; otherwise NULL. - No quoting is applied, and name is never schema-qualified. - - - - object_identity - text - - Text rendering of the object identity, schema-qualified. Each - identifier included in the identity is quoted if necessary. - - - - address_names - text[] - - An array that, together with object_type and - address_args, can be used by - the pg_get_object_address function to - recreate the object address in a remote server containing an - identically named object of the same kind. - - - - address_args - text[] - - Complement for address_names - - - - - - - - - The pg_event_trigger_dropped_objects function can be used - in an event trigger like this: - -CREATE FUNCTION test_event_trigger_for_drops() - RETURNS event_trigger LANGUAGE plpgsql AS $$ -DECLARE - obj record; -BEGIN - FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() - LOOP - RAISE NOTICE '% dropped object: % %.% %', - tg_tag, - obj.object_type, - obj.schema_name, - obj.object_name, - obj.object_identity; - END LOOP; -END; -$$; -CREATE EVENT TRIGGER test_event_trigger_for_drops - ON sql_drop - EXECUTE FUNCTION test_event_trigger_for_drops(); - - - - - - Handling a Table Rewrite Event - - - The functions shown in - - provide information about a table for which a - table_rewrite event has just been called. - If called in any other context, an error is raised. - - - - Table Rewrite Information Functions - - - - - Function - - - Description - - - - - - - - - pg_event_trigger_table_rewrite_oid - - pg_event_trigger_table_rewrite_oid () - oid - - - Returns the OID of the table about to be rewritten. - - - - - - - pg_event_trigger_table_rewrite_reason - - pg_event_trigger_table_rewrite_reason () - integer - - - Returns a code explaining the reason(s) for rewriting. The value is - a bitmap built from the following values: 1 - (the table has changed its persistence), 2 - (default value of a column has changed), 4 - (a column has a new data type) and 8 - (the table access method has changed). - - - - -
- - - These functions can be used in an event trigger like this: - -CREATE FUNCTION test_event_trigger_table_rewrite_oid() - RETURNS event_trigger - LANGUAGE plpgsql AS -$$ -BEGIN - RAISE NOTICE 'rewriting table % for reason %', - pg_event_trigger_table_rewrite_oid()::regclass, - pg_event_trigger_table_rewrite_reason(); -END; -$$; - -CREATE EVENT TRIGGER test_table_rewrite_oid - ON table_rewrite - EXECUTE FUNCTION test_event_trigger_table_rewrite_oid(); - - -
-
- - - Statistics Information Functions - - - function - statistics - - - - PostgreSQL provides a function to inspect complex - statistics defined using the CREATE STATISTICS command. - - - - Inspecting MCV Lists - - - pg_mcv_list_items - - - -pg_mcv_list_items ( pg_mcv_list ) setof record - - - - pg_mcv_list_items returns a set of records describing - all items stored in a multi-column MCV list. It - returns the following columns: - - - - - - Name - Type - Description - - - - - - index - integer - index of the item in the MCV list - - - values - text[] - values stored in the MCV item - - - nulls - boolean[] - flags identifying NULL values - - - frequency - double precision - frequency of this MCV item - - - base_frequency - double precision - base frequency of this MCV item - - - - - - - - The pg_mcv_list_items function can be used like this: - - -SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid), - pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts'; - - - Values of the pg_mcv_list type can be obtained only from the - pg_statistic_ext_data.stxdmcv - column. - - - - - - diff --git a/doc/src/sgml/func/allfiles.sgml b/doc/src/sgml/func/allfiles.sgml new file mode 100644 index 0000000000000..f5e3f0085378c --- /dev/null +++ b/doc/src/sgml/func/allfiles.sgml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml new file mode 100644 index 0000000000000..24ecb46542ee2 --- /dev/null +++ b/doc/src/sgml/func/func-admin.sgml @@ -0,0 +1,3204 @@ + + System Administration Functions + + + The functions described in this section are used to control and + monitor a PostgreSQL installation. + + + + Configuration Settings Functions + + + SET + + + + SHOW + + + + configuration + of the server + functions + + + + shows the functions + available to query and alter run-time configuration parameters. + + + + Configuration Settings Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + current_setting + + current_setting ( setting_name text , missing_ok boolean ) + text + + + Returns the current value of the + setting setting_name. If there is no such + setting, current_setting throws an error + unless missing_ok is supplied and + is true (in which case NULL is returned). + This function corresponds to + the SQL command . + + + current_setting('datestyle') + ISO, MDY + + + + + + + set_config + + set_config ( + setting_name text, + new_value text, + is_local boolean ) + text + + + Sets the parameter setting_name + to new_value, and returns that value. + If is_local is true, the new + value will only apply during the current transaction. If you want the + new value to apply for the rest of the current session, + use false instead. This function corresponds to + the SQL command . + + + set_config accepts the NULL value for + new_value, but as settings cannot be null, it + is interpreted as a request to reset the setting to its default value. + + + set_config('log_statement_stats', 'off', false) + off + + + + +
+ +
+ + + Server Signaling Functions + + + signal + backend processes + + + + The functions shown in send control signals to + other server processes. Use of these functions is restricted to + superusers by default but access may be granted to others using + GRANT, with noted exceptions. + + + + Each of these functions returns true if + the signal was successfully sent and false + if sending the signal failed. + + + + Server Signaling Functions + + + + + Function + + + Description + + + + + + + + + pg_cancel_backend + + pg_cancel_backend ( pid integer ) + boolean + + + Cancels the current query of the session whose backend process has the + specified process ID. This is also allowed if the + calling role is a member of the role whose backend is being canceled or + the calling role has privileges of pg_signal_backend, + however only superusers can cancel superuser backends. + As an exception, roles with privileges of + pg_signal_autovacuum_worker are permitted to + cancel autovacuum worker processes, which are otherwise considered + superuser backends. + + + + + + + pg_log_backend_memory_contexts + + pg_log_backend_memory_contexts ( pid integer ) + boolean + + + Requests to log the memory contexts of the backend with the + specified process ID. This function can send the request to + backends and auxiliary processes except logger. These memory contexts + will be logged at + LOG message level. They will appear in + the server log based on the log configuration set + (see for more information), + but will not be sent to the client regardless of + . + + + + + + + pg_reload_conf + + pg_reload_conf () + boolean + + + Causes all processes of the PostgreSQL + server to reload their configuration files. (This is initiated by + sending a SIGHUP signal to the postmaster + process, which in turn sends SIGHUP to each + of its children.) You can use the + pg_file_settings, + pg_hba_file_rules and + pg_ident_file_mappings views + to check the configuration files for possible errors, before reloading. + + + + + + + pg_rotate_logfile + + pg_rotate_logfile () + boolean + + + Signals the log-file manager to switch to a new output file + immediately. This works only when the built-in log collector is + running, since otherwise there is no log-file manager subprocess. + + + + + + + pg_terminate_backend + + pg_terminate_backend ( pid integer, timeout bigint DEFAULT 0 ) + boolean + + + Terminates the session whose backend process has the + specified process ID. This is also allowed if the calling role + is a member of the role whose backend is being terminated or the + calling role has privileges of pg_signal_backend, + however only superusers can terminate superuser backends. + As an exception, roles with privileges of + pg_signal_autovacuum_worker are permitted to + terminate autovacuum worker processes, which are otherwise considered + superuser backends. + + + If timeout is not specified or zero, this + function returns true whether the process actually + terminates or not, indicating only that the sending of the signal was + successful. If the timeout is specified (in + milliseconds) and greater than zero, the function waits until the + process is actually terminated or until the given time has passed. If + the process is terminated, the function + returns true. On timeout, a warning is emitted and + false is returned. + + + + +
+ + + pg_cancel_backend and pg_terminate_backend + send signals (SIGINT or SIGTERM + respectively) to backend processes identified by process ID. + The process ID of an active backend can be found from + the pid column of the + pg_stat_activity view, or by listing the + postgres processes on the server (using + ps on Unix or the Task + Manager on Windows). + The role of an active backend can be found from the + usename column of the + pg_stat_activity view. + + + + pg_log_backend_memory_contexts can be used + to log the memory contexts of a backend process. For example: + +postgres=# SELECT pg_log_backend_memory_contexts(pg_backend_pid()); + pg_log_backend_memory_contexts +-------------------------------- + t +(1 row) + +One message for each memory context will be logged. For example: + +LOG: logging memory contexts of PID 10377 +STATEMENT: SELECT pg_log_backend_memory_contexts(pg_backend_pid()); +LOG: level: 1; TopMemoryContext: 80800 total in 6 blocks; 14432 free (5 chunks); 66368 used +LOG: level: 2; pgstat TabStatusArray lookup hash table: 8192 total in 1 blocks; 1408 free (0 chunks); 6784 used +LOG: level: 2; TopTransactionContext: 8192 total in 1 blocks; 7720 free (1 chunks); 472 used +LOG: level: 2; RowDescriptionContext: 8192 total in 1 blocks; 6880 free (0 chunks); 1312 used +LOG: level: 2; MessageContext: 16384 total in 2 blocks; 5152 free (0 chunks); 11232 used +LOG: level: 2; Operator class cache: 8192 total in 1 blocks; 512 free (0 chunks); 7680 used +LOG: level: 2; smgr relation table: 16384 total in 2 blocks; 4544 free (3 chunks); 11840 used +LOG: level: 2; TransactionAbortContext: 32768 total in 1 blocks; 32504 free (0 chunks); 264 used +... +LOG: level: 2; ErrorContext: 8192 total in 1 blocks; 7928 free (3 chunks); 264 used +LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 used + + If there are more than 100 child contexts under the same parent, the first + 100 child contexts are logged, along with a summary of the remaining contexts. + Note that frequent calls to this function could incur significant overhead, + because it may generate a large number of log messages. + + +
+ + + Backup Control Functions + + + backup + + + + The functions shown in assist in making on-line backups. + These functions cannot be executed during recovery (except + pg_backup_start, + pg_backup_stop, + and pg_wal_lsn_diff). + + + + For details about proper usage of these functions, see + . + + + + Backup Control Functions + + + + + Function + + + Description + + + + + + + + + pg_create_restore_point + + pg_create_restore_point ( name text ) + pg_lsn + + + Creates a named marker record in the write-ahead log that can later be + used as a recovery target, and returns the corresponding write-ahead + log location. The given name can then be used with + to specify the point up to + which recovery will proceed. Avoid creating multiple restore points + with the same name, since recovery will stop at the first one whose + name matches the recovery target. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_current_wal_flush_lsn + + pg_current_wal_flush_lsn () + pg_lsn + + + Returns the current write-ahead log flush location (see notes below). + + + + + + + pg_current_wal_insert_lsn + + pg_current_wal_insert_lsn () + pg_lsn + + + Returns the current write-ahead log insert location (see notes below). + + + + + + + pg_current_wal_lsn + + pg_current_wal_lsn () + pg_lsn + + + Returns the current write-ahead log write location (see notes below). + + + + + + + pg_backup_start + + pg_backup_start ( + label text + , fast boolean + ) + pg_lsn + + + Prepares the server to begin an on-line backup. The only required + parameter is an arbitrary user-defined label for the backup. + (Typically this would be the name under which the backup dump file + will be stored.) + If the optional second parameter is given as true, + it specifies executing pg_backup_start as quickly + as possible. This forces a fast checkpoint which will cause a + spike in I/O operations, slowing any concurrently executing queries. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_backup_stop + + pg_backup_stop ( + wait_for_archive boolean + ) + record + ( lsn pg_lsn, + labelfile text, + spcmapfile text ) + + + Finishes performing an on-line backup. The desired contents of the + backup label file and the tablespace map file are returned as part of + the result of the function and must be written to files in the + backup area. These files must not be written to the live data directory + (doing so will cause PostgreSQL to fail to restart in the event of a + crash). + + + There is an optional parameter of type boolean. + If false, the function will return immediately after the backup is + completed, without waiting for WAL to be archived. This behavior is + only useful with backup software that independently monitors WAL + archiving. Otherwise, WAL required to make the backup consistent might + be missing and make the backup useless. By default or when this + parameter is true, pg_backup_stop will wait for + WAL to be archived when archiving is enabled. (On a standby, this + means that it will wait only when archive_mode = + always. If write activity on the primary is low, + it may be useful to run pg_switch_wal on the + primary in order to trigger an immediate segment switch.) + + + When executed on a primary, this function also creates a backup + history file in the write-ahead log archive area. The history file + includes the label given to pg_backup_start, the + starting and ending write-ahead log locations for the backup, and the + starting and ending times of the backup. After recording the ending + location, the current write-ahead log insertion point is automatically + advanced to the next write-ahead log file, so that the ending + write-ahead log file can be archived immediately to complete the + backup. + + + The result of the function is a single record. + The lsn column holds the backup's ending + write-ahead log location (which again can be ignored). The second + column returns the contents of the backup label file, and the third + column returns the contents of the tablespace map file. These must be + stored as part of the backup and are required as part of the restore + process. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_switch_wal + + pg_switch_wal () + pg_lsn + + + Forces the server to switch to a new write-ahead log file, which + allows the current file to be archived (assuming you are using + continuous archiving). The result is the ending write-ahead log + location plus 1 within the just-completed write-ahead log file. If + there has been no write-ahead log activity since the last write-ahead + log switch, pg_switch_wal does nothing and + returns the start location of the write-ahead log file currently in + use. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_walfile_name + + pg_walfile_name ( lsn pg_lsn ) + text + + + Converts a write-ahead log location to the name of the WAL file + holding that location. + + + + + + + pg_walfile_name_offset + + pg_walfile_name_offset ( lsn pg_lsn ) + record + ( file_name text, + file_offset integer ) + + + Converts a write-ahead log location to a WAL file name and byte offset + within that file. + + + + + + + pg_split_walfile_name + + pg_split_walfile_name ( file_name text ) + record + ( segment_number numeric, + timeline_id bigint ) + + + Extracts the sequence number and timeline ID from a WAL file + name. + + + + + + + pg_wal_lsn_diff + + pg_wal_lsn_diff ( lsn1 pg_lsn, lsn2 pg_lsn ) + numeric + + + Calculates the difference in bytes (lsn1 - lsn2) between two write-ahead log + locations. This can be used + with pg_stat_replication or some of the + functions shown in to + get the replication lag. + + + + +
+ + + pg_current_wal_lsn displays the current write-ahead + log write location in the same format used by the above functions. + Similarly, pg_current_wal_insert_lsn displays the + current write-ahead log insertion location + and pg_current_wal_flush_lsn displays the current + write-ahead log flush location. The insertion location is + the logical end of the write-ahead log at any instant, + while the write location is the end of what has actually been written out + from the server's internal buffers, and the flush location is the last + location known to be written to durable storage. The write location is the + end of what can be examined from outside the server, and is usually what + you want if you are interested in archiving partially-complete write-ahead + log files. The insertion and flush locations are made available primarily + for server debugging purposes. These are all read-only operations and do + not require superuser permissions. + + + + You can use pg_walfile_name_offset to extract the + corresponding write-ahead log file name and byte offset from + a pg_lsn value. For example: + +postgres=# SELECT * FROM pg_walfile_name_offset((pg_backup_stop()).lsn); + file_name | file_offset +--------------------------+------------- + 00000001000000000000000D | 4039624 +(1 row) + + Similarly, pg_walfile_name extracts just the write-ahead log file name. + + + + pg_split_walfile_name is useful to compute a + LSN from a file offset and WAL file name, for example: + +postgres=# \set file_name '000000010000000100C000AB' +postgres=# \set offset 256 +postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset AS lsn + FROM pg_split_walfile_name(:'file_name') pd, + pg_show_all_settings() ps + WHERE ps.name = 'wal_segment_size'; + lsn +--------------- + C001/AB000100 +(1 row) + + + +
+ + + Recovery Control Functions + + + The functions shown in provide information + about the current status of a standby server. + These functions may be executed both during recovery and in normal running. + + + + Recovery Information Functions + + + + + Function + + + Description + + + + + + + + + pg_is_in_recovery + + pg_is_in_recovery () + boolean + + + Returns true if recovery is still in progress. + + + + + + + pg_last_wal_receive_lsn + + pg_last_wal_receive_lsn () + pg_lsn + + + Returns the last write-ahead log location that has been received and + synced to disk by streaming replication. While streaming replication + is in progress this will increase monotonically. If recovery has + completed then this will remain static at the location of the last WAL + record received and synced to disk during recovery. If streaming + replication is disabled, or if it has not yet started, the function + returns NULL. + + + + + + + pg_last_wal_replay_lsn + + pg_last_wal_replay_lsn () + pg_lsn + + + Returns the last write-ahead log location that has been replayed + during recovery. If recovery is still in progress this will increase + monotonically. If recovery has completed then this will remain + static at the location of the last WAL record applied during recovery. + When the server has been started normally without recovery, the + function returns NULL. + + + + + + + pg_last_xact_replay_timestamp + + pg_last_xact_replay_timestamp () + timestamp with time zone + + + Returns the time stamp of the last transaction replayed during + recovery. This is the time at which the commit or abort WAL record + for that transaction was generated on the primary. If no transactions + have been replayed during recovery, the function + returns NULL. Otherwise, if recovery is still in + progress this will increase monotonically. If recovery has completed + then this will remain static at the time of the last transaction + applied during recovery. When the server has been started normally + without recovery, the function returns NULL. + + + + + + + pg_get_wal_resource_managers + + pg_get_wal_resource_managers () + setof record + ( rm_id integer, + rm_name text, + rm_builtin boolean ) + + + Returns the currently-loaded WAL resource managers in the system. The + column rm_builtin indicates whether it's a + built-in resource manager, or a custom resource manager loaded by an + extension. + + + + +
+ + + The functions shown in control the progress of recovery. + These functions may be executed only during recovery. + + + + Recovery Control Functions + + + + + Function + + + Description + + + + + + + + + pg_is_wal_replay_paused + + pg_is_wal_replay_paused () + boolean + + + Returns true if recovery pause is requested. + + + + + + + pg_get_wal_replay_pause_state + + pg_get_wal_replay_pause_state () + text + + + Returns recovery pause state. The return values are + not paused if pause is not requested, + pause requested if pause is requested but recovery is + not yet paused, and paused if the recovery is + actually paused. + + + + + + + pg_promote + + pg_promote ( wait boolean DEFAULT true, wait_seconds integer DEFAULT 60 ) + boolean + + + Promotes a standby server to primary status. + With wait set to true (the + default), the function waits until promotion is completed + or wait_seconds seconds have passed, and + returns true if promotion is successful + and false otherwise. + If wait is set to false, the + function returns true immediately after sending a + SIGUSR1 signal to the postmaster to trigger + promotion. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_wal_replay_pause + + pg_wal_replay_pause () + void + + + Request to pause recovery. A request doesn't mean that recovery stops + right away. If you want a guarantee that recovery is actually paused, + you need to check for the recovery pause state returned by + pg_get_wal_replay_pause_state(). Note that + pg_is_wal_replay_paused() returns whether a request + is made. While recovery is paused, no further database changes are applied. + If hot standby is active, all new queries will see the same consistent + snapshot of the database, and no further query conflicts will be generated + until recovery is resumed. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_wal_replay_resume + + pg_wal_replay_resume () + void + + + Restarts recovery if it was paused. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + +
+ + + pg_wal_replay_pause and + pg_wal_replay_resume cannot be executed while + a promotion is ongoing. If a promotion is triggered while recovery + is paused, the paused state ends and promotion continues. + + + + If streaming replication is disabled, the paused state may continue + indefinitely without a problem. If streaming replication is in + progress then WAL records will continue to be received, which will + eventually fill available disk space, depending upon the duration of + the pause, the rate of WAL generation and available disk space. + + +
+ + + Snapshot Synchronization Functions + + + PostgreSQL allows database sessions to synchronize their + snapshots. A snapshot determines which data is visible to the + transaction that is using the snapshot. Synchronized snapshots are + necessary when two or more sessions need to see identical content in the + database. If two sessions just start their transactions independently, + there is always a possibility that some third transaction commits + between the executions of the two START TRANSACTION commands, + so that one session sees the effects of that transaction and the other + does not. + + + + To solve this problem, PostgreSQL allows a transaction to + export the snapshot it is using. As long as the exporting + transaction remains open, other transactions can import its + snapshot, and thereby be guaranteed that they see exactly the same view + of the database that the first transaction sees. But note that any + database changes made by any one of these transactions remain invisible + to the other transactions, as is usual for changes made by uncommitted + transactions. So the transactions are synchronized with respect to + pre-existing data, but act normally for changes they make themselves. + + + + Snapshots are exported with the pg_export_snapshot function, + shown in , and + imported with the command. + + + + Snapshot Synchronization Functions + + + + + Function + + + Description + + + + + + + + + pg_export_snapshot + + pg_export_snapshot () + text + + + Saves the transaction's current snapshot and returns + a text string identifying the snapshot. This string must + be passed (outside the database) to clients that want to import the + snapshot. The snapshot is available for import only until the end of + the transaction that exported it. + + + A transaction can export more than one snapshot, if needed. Note that + doing so is only useful in READ COMMITTED + transactions, since in REPEATABLE READ and higher + isolation levels, transactions use the same snapshot throughout their + lifetime. Once a transaction has exported any snapshots, it cannot be + prepared with . + + + + + + pg_log_standby_snapshot + + pg_log_standby_snapshot () + pg_lsn + + + Take a snapshot of running transactions and write it to WAL, without + having to wait for bgwriter or checkpointer to log one. This is useful + for logical decoding on standby, as logical slot creation has to wait + until such a record is replayed on the standby. + + + + +
+ +
+ + + Replication Management Functions + + + The functions shown + in are for + controlling and interacting with replication features. + See , + , and + + for information about the underlying features. + Use of functions for replication origin is only allowed to the + superuser by default, but may be allowed to other users by using the + GRANT command. + Use of functions for replication slots is restricted to superusers + and users having REPLICATION privilege. + + + + Many of these functions have equivalent commands in the replication + protocol; see . + + + + The functions described in + , + , and + + are also relevant for replication. + + + + Replication Management Functions + + + + + Function + + + Description + + + + + + + + + pg_create_physical_replication_slot + + pg_create_physical_replication_slot ( slot_name name , immediately_reserve boolean, temporary boolean ) + record + ( slot_name name, + lsn pg_lsn ) + + + Creates a new physical replication slot named + slot_name. The name cannot be + pg_conflict_detection as it is reserved for the + conflict detection slot. The optional second parameter, + when true, specifies that the LSN for this + replication slot be reserved immediately; otherwise + the LSN is reserved on first connection from a streaming + replication client. Streaming changes from a physical slot is only + possible with the streaming-replication protocol — + see . The optional third + parameter, temporary, when set to true, specifies that + the slot should not be permanently stored to disk and is only meant + for use by the current session. Temporary slots are also + released upon any error. This function corresponds + to the replication protocol command CREATE_REPLICATION_SLOT + ... PHYSICAL. + + + + + + + pg_drop_replication_slot + + pg_drop_replication_slot ( slot_name name ) + void + + + Drops the physical or logical replication slot + named slot_name. Same as replication protocol + command DROP_REPLICATION_SLOT. + + + + + + + pg_create_logical_replication_slot + + pg_create_logical_replication_slot ( slot_name name, plugin name , temporary boolean, twophase boolean, failover boolean ) + record + ( slot_name name, + lsn pg_lsn ) + + + Creates a new logical (decoding) replication slot named + slot_name using the output plugin + plugin. The name cannot be + pg_conflict_detection as it is reserved for + the conflict detection slot. The optional third + parameter, temporary, when set to true, specifies that + the slot should not be permanently stored to disk and is only meant + for use by the current session. Temporary slots are also + released upon any error. The optional fourth parameter, + twophase, when set to true, specifies + that the decoding of prepared transactions is enabled for this + slot. The optional fifth parameter, + failover, when set to true, + specifies that this slot is enabled to be synced to the + standbys so that logical replication can be resumed after + failover. A call to this function has the same effect as + the replication protocol command + CREATE_REPLICATION_SLOT ... LOGICAL. + + + + + + + pg_copy_physical_replication_slot + + pg_copy_physical_replication_slot ( src_slot_name name, dst_slot_name name , temporary boolean ) + record + ( slot_name name, + lsn pg_lsn ) + + + Copies an existing physical replication slot named src_slot_name + to a physical replication slot named dst_slot_name. + The new slot name cannot be pg_conflict_detection, + as it is reserved for the conflict detection. + The copied physical slot starts to reserve WAL from the same LSN as the + source slot. + temporary is optional. If temporary + is omitted, the same value as the source slot is used. Copy of an + invalidated slot is not allowed. + + + + + + + pg_copy_logical_replication_slot + + pg_copy_logical_replication_slot ( src_slot_name name, dst_slot_name name , temporary boolean , plugin name ) + record + ( slot_name name, + lsn pg_lsn ) + + + Copies an existing logical replication slot + named src_slot_name to a logical replication + slot named dst_slot_name, optionally changing + the output plugin and persistence. The new slot name cannot be + pg_conflict_detection as it is reserved for + the conflict detection. The copied logical slot starts from the same + LSN as the source logical slot. Both + temporary and plugin are + optional; if they are omitted, the values of the source slot are used. + The failover option of the source logical slot + is not copied and is set to false by default. This + is to avoid the risk of being unable to continue logical replication + after failover to standby where the slot is being synchronized. Copy of + an invalidated slot is not allowed. + + + + + + + pg_logical_slot_get_changes + + pg_logical_slot_get_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) + setof record + ( lsn pg_lsn, + xid xid, + data text ) + + + Returns changes in the slot slot_name, starting + from the point from which changes have been consumed last. If + upto_lsn + and upto_nchanges are NULL, + logical decoding will continue until end of WAL. If + upto_lsn is non-NULL, decoding will include only + those transactions which commit prior to the specified LSN. If + upto_nchanges is non-NULL, decoding will + stop when the number of rows produced by decoding exceeds + the specified value. Note, however, that the actual number of + rows returned may be larger, since this limit is only checked after + adding the rows produced when decoding each new transaction commit. + If the specified slot is a logical failover slot then the function will + not return until all physical slots specified in + synchronized_standby_slots + have confirmed WAL receipt. + + + + + + + pg_logical_slot_peek_changes + + pg_logical_slot_peek_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) + setof record + ( lsn pg_lsn, + xid xid, + data text ) + + + Behaves just like + the pg_logical_slot_get_changes() function, + except that changes are not consumed; that is, they will be returned + again on future calls. + + + + + + + pg_logical_slot_get_binary_changes + + pg_logical_slot_get_binary_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) + setof record + ( lsn pg_lsn, + xid xid, + data bytea ) + + + Behaves just like + the pg_logical_slot_get_changes() function, + except that changes are returned as bytea. + + + + + + + pg_logical_slot_peek_binary_changes + + pg_logical_slot_peek_binary_changes ( slot_name name, upto_lsn pg_lsn, upto_nchanges integer, VARIADIC options text[] ) + setof record + ( lsn pg_lsn, + xid xid, + data bytea ) + + + Behaves just like + the pg_logical_slot_peek_changes() function, + except that changes are returned as bytea. + + + + + + + pg_replication_slot_advance + + pg_replication_slot_advance ( slot_name name, upto_lsn pg_lsn ) + record + ( slot_name name, + end_lsn pg_lsn ) + + + Advances the current confirmed position of a replication slot named + slot_name. The slot will not be moved backwards, + and it will not be moved beyond the current insert location. Returns + the name of the slot and the actual position that it was advanced to. + The updated slot position information is written out at the next + checkpoint if any advancing is done. So in the event of a crash, the + slot may return to an earlier position. If the specified slot is a + logical failover slot then the function will not return until all + physical slots specified in + synchronized_standby_slots + have confirmed WAL receipt. + + + + + + + pg_replication_origin_create + + pg_replication_origin_create ( node_name text ) + oid + + + Creates a replication origin with the given external + name, and returns the internal ID assigned to it. + The name must be no longer than 512 bytes. + + + + + + + pg_replication_origin_drop + + pg_replication_origin_drop ( node_name text ) + void + + + Deletes a previously-created replication origin, including any + associated replay progress. + + + + + + + pg_replication_origin_oid + + pg_replication_origin_oid ( node_name text ) + oid + + + Looks up a replication origin by name and returns the internal ID. If + no such replication origin is found, NULL is + returned. + + + + + + + pg_replication_origin_session_setup + + pg_replication_origin_session_setup ( node_name text , pid integer DEFAULT 0 ) + void + + + Marks the current session as replaying from the given + origin, allowing replay progress to be tracked. + Can only be used if no origin is currently selected. + Use pg_replication_origin_session_reset to undo. + If multiple processes can safely use the same replication origin (for + example, parallel apply processes), the optional pid + parameter can be used to specify the process ID of the first process. + The first process must provide pid equals to + 0 and the other processes that share the same + replication origin should provide the process ID of the first process. + + + + When multiple processes share the same replication origin, it is critical + to maintain commit order to prevent data inconsistency. While processes + may send operations out of order, they must commit transactions in the + correct sequence to ensure proper replication consistency. The recommended workflow + for each worker is: set up the replication origin session with the first process's PID, + apply changes within transactions, call pg_replication_origin_xact_setup + with the LSN and commit timestamp before committing, then commit the + transaction only if everything succeeded. + + + + + + + + + pg_replication_origin_session_reset + + pg_replication_origin_session_reset () + void + + + Cancels the effects + of pg_replication_origin_session_setup(). + + + + + + + pg_replication_origin_session_is_setup + + pg_replication_origin_session_is_setup () + boolean + + + Returns true if a replication origin has been selected in the + current session. + + + + + + + pg_replication_origin_session_progress + + pg_replication_origin_session_progress ( flush boolean ) + pg_lsn + + + Returns the replay location for the replication origin selected in + the current session. The parameter flush + determines whether the corresponding local transaction will be + guaranteed to have been flushed to disk or not. + + + + + + + pg_replication_origin_xact_setup + + pg_replication_origin_xact_setup ( origin_lsn pg_lsn, origin_timestamp timestamp with time zone ) + void + + + Marks the current transaction as replaying a transaction that has + committed at the given LSN and timestamp. Can + only be called when a replication origin has been selected + using pg_replication_origin_session_setup. + + + + + + + pg_replication_origin_xact_reset + + pg_replication_origin_xact_reset () + void + + + Cancels the effects of + pg_replication_origin_xact_setup(). + + + + + + + pg_replication_origin_advance + + pg_replication_origin_advance ( node_name text, lsn pg_lsn ) + void + + + Sets replication progress for the given node to the given + location. This is primarily useful for setting up the initial + location, or setting a new location after configuration changes and + similar. Be aware that careless use of this function can lead to + inconsistently replicated data. + + + + + + + pg_replication_origin_progress + + pg_replication_origin_progress ( node_name text, flush boolean ) + pg_lsn + + + Returns the replay location for the given replication origin. The + parameter flush determines whether the + corresponding local transaction will be guaranteed to have been + flushed to disk or not. + + + + + + + pg_logical_emit_message + + pg_logical_emit_message ( transactional boolean, prefix text, content text , flush boolean DEFAULT false ) + pg_lsn + + + pg_logical_emit_message ( transactional boolean, prefix text, content bytea , flush boolean DEFAULT false ) + pg_lsn + + + Emits a logical decoding message. This can be used to pass generic + messages to logical decoding plugins through + WAL. The transactional parameter specifies if + the message should be part of the current transaction, or if it should + be written immediately and decoded as soon as the logical decoder + reads the record. The prefix parameter is a + textual prefix that can be used by logical decoding plugins to easily + recognize messages that are interesting for them. + The content parameter is the content of the + message, given either in text or binary form. + The flush parameter (default set to + false) controls if the message is immediately + flushed to WAL or not. flush has no effect + with transactional, as the message's WAL + record is flushed along with its transaction. + + + + + + + pg_sync_replication_slots + + pg_sync_replication_slots () + void + + + Synchronize the logical failover replication slots from the primary + server to the standby server. This function can only be executed on the + standby server. Temporary synced slots, if any, cannot be used for + logical decoding and must be dropped after promotion. This function + retries cyclically until all the failover slots that existed on + primary at the start of the function call are synchronized. See + for details. + Note that this function cannot be executed if + + sync_replication_slots is enabled and the slotsync + worker is already running to perform the synchronization of slots. + + + + + If, after executing the function, + + hot_standby_feedback is disabled on + the standby or the physical slot configured in + + primary_slot_name is + removed, then it is possible that the necessary rows of the + synchronized slot will be removed by the VACUUM process on the primary + server, resulting in the synchronized slot becoming invalidated. + + + + + + + +
+ +
+ + + Database Object Management Functions + + + The functions shown in calculate + the disk space usage of database objects, or assist in presentation + or understanding of usage results. bigint results + are measured in bytes. If an OID that does + not represent an existing object is passed to one of these + functions, NULL is returned. + + + + Database Object Size Functions + + + + + Function + + + Description + + + + + + + + + pg_column_size + + pg_column_size ( "any" ) + integer + + + Shows the number of bytes used to store any individual data value. If + applied directly to a table column value, this reflects any + compression that was done. + + + + + + + pg_column_compression + + pg_column_compression ( "any" ) + text + + + Shows the compression algorithm that was used to compress + an individual variable-length value. Returns NULL + if the value is not compressed. + + + + + + + pg_column_toast_chunk_id + + pg_column_toast_chunk_id ( "any" ) + oid + + + Shows the chunk_id of an on-disk + TOASTed value. Returns NULL + if the value is un-TOASTed or not on-disk. See + for more information about + TOAST. + + + + + + + pg_database_size + + pg_database_size ( name ) + bigint + + + pg_database_size ( oid ) + bigint + + + Computes the total disk space used by the database with the specified + name or OID. To use this function, you must + have CONNECT privilege on the specified database + (which is granted by default) or have privileges of + the pg_read_all_stats role. + + + + + + + pg_indexes_size + + pg_indexes_size ( regclass ) + bigint + + + Computes the total disk space used by indexes attached to the + specified table. + + + + + + + pg_relation_size + + pg_relation_size ( relation regclass , fork text ) + bigint + + + Computes the disk space used by one fork of the + specified relation. (Note that for most purposes it is more + convenient to use the higher-level + functions pg_total_relation_size + or pg_table_size, which sum the sizes of all + forks.) With one argument, this returns the size of the main data + fork of the relation. The second argument can be provided to specify + which fork to examine: + + + + main returns the size of the main + data fork of the relation. + + + + + fsm returns the size of the Free Space Map + (see ) associated with the relation. + + + + + vm returns the size of the Visibility Map + (see ) associated with the relation. + + + + + init returns the size of the initialization + fork, if any, associated with the relation. + + + + + + + + + + pg_size_bytes + + pg_size_bytes ( text ) + bigint + + + Converts a size in human-readable format (as returned + by pg_size_pretty) into bytes. Valid units are + bytes, B, kB, + MB, GB, TB, + and PB. + + + + + + + pg_size_pretty + + pg_size_pretty ( bigint ) + text + + + pg_size_pretty ( numeric ) + text + + + Converts a size in bytes into a more easily human-readable format with + size units (bytes, kB, MB, GB, TB, or PB as appropriate). Note that the + units are powers of 2 rather than powers of 10, so 1kB is 1024 bytes, + 1MB is 10242 = 1048576 bytes, and so on. + + + + + + + pg_table_size + + pg_table_size ( regclass ) + bigint + + + Computes the disk space used by the specified table, excluding indexes + (but including its TOAST table if any, free space map, and visibility + map). + + + + + + + pg_tablespace_size + + pg_tablespace_size ( name ) + bigint + + + pg_tablespace_size ( oid ) + bigint + + + Computes the total disk space used in the tablespace with the + specified name or OID. To use this function, you must + have CREATE privilege on the specified tablespace + or have privileges of the pg_read_all_stats role, + unless it is the default tablespace for the current database. + + + + + + + pg_total_relation_size + + pg_total_relation_size ( regclass ) + bigint + + + Computes the total disk space used by the specified table, including + all indexes and TOAST data. The result is + equivalent to pg_table_size + + pg_indexes_size. + + + + +
+ + + The functions above that operate on tables or indexes accept a + regclass argument, which is simply the OID of the table or index + in the pg_class system catalog. You do not have to look up + the OID by hand, however, since the regclass data type's input + converter will do the work for you. See + for details. + + + + The functions shown in assist + in identifying the specific disk files associated with database objects. + + + + Database Object Location Functions + + + + + Function + + + Description + + + + + + + + + pg_relation_filenode + + pg_relation_filenode ( relation regclass ) + oid + + + Returns the filenode number currently assigned to the + specified relation. The filenode is the base component of the file + name(s) used for the relation (see + for more information). + For most relations the result is the same as + pg_class.relfilenode, + but for certain system catalogs relfilenode + is zero and this function must be used to get the correct value. The + function returns NULL if passed a relation that does not have storage, + such as a view. + + + + + + + pg_relation_filepath + + pg_relation_filepath ( relation regclass ) + text + + + Returns the entire file path name (relative to the database cluster's + data directory, PGDATA) of the relation. + + + + + + + pg_filenode_relation + + pg_filenode_relation ( tablespace oid, filenode oid ) + regclass + + + Returns a relation's OID given the tablespace OID and filenode it is + stored under. This is essentially the inverse mapping of + pg_relation_filepath. For a relation in the + database's default tablespace, the tablespace can be specified as zero. + Returns NULL if no relation in the current database + is associated with the given values, or if dealing with a temporary + relation. + + + + +
+ + + lists functions used to manage + collations. + + + + Collation Management Functions + + + + + Function + + + Description + + + + + + + + + pg_collation_actual_version + + pg_collation_actual_version ( oid ) + text + + + Returns the actual version of the collation object as it is currently + installed in the operating system. If this is different from the + value in + pg_collation.collversion, + then objects depending on the collation might need to be rebuilt. See + also . + + + + + + + pg_database_collation_actual_version + + pg_database_collation_actual_version ( oid ) + text + + + Returns the actual version of the database's collation as it is currently + installed in the operating system. If this is different from the + value in + pg_database.datcollversion, + then objects depending on the collation might need to be rebuilt. See + also . + + + + + + + pg_import_system_collations + + pg_import_system_collations ( schema regnamespace ) + integer + + + Adds collations to the system + catalog pg_collation based on all the locales + it finds in the operating system. This is + what initdb uses; see + for more details. If additional + locales are installed into the operating system later on, this + function can be run again to add collations for the new locales. + Locales that match existing entries + in pg_collation will be skipped. (But + collation objects based on locales that are no longer present in the + operating system are not removed by this function.) + The schema parameter would typically + be pg_catalog, but that is not a requirement; the + collations could be installed into some other schema as well. The + function returns the number of new collation objects it created. + Use of this function is restricted to superusers. + + + + +
+ + + lists functions used to + manipulate statistics. + These functions cannot be executed during recovery. + + + Changes made by these statistics manipulation functions are likely to be + overwritten by autovacuum (or manual + VACUUM or ANALYZE) and should be + considered temporary. + + + + + + Database Object Statistics Manipulation Functions + + + + + Function + + + Description + + + + + + + + + pg_restore_relation_stats + + pg_restore_relation_stats ( + VARIADIC kwargs "any" ) + boolean + + + Updates table-level statistics. Ordinarily, these statistics are + collected automatically or updated as a part of or , so it's not + necessary to call this function. However, it is useful after a + restore to enable the optimizer to choose better plans if + ANALYZE has not been run yet. + + + The tracked statistics may change from version to version, so + arguments are passed as pairs of argname + and argvalue in the form: + +SELECT pg_restore_relation_stats( + 'arg1name', 'arg1value'::arg1type, + 'arg2name', 'arg2value'::arg2type, + 'arg3name', 'arg3value'::arg3type); + + + + For example, to set the relpages and + reltuples values for the table + mytable: + +SELECT pg_restore_relation_stats( + 'schemaname', 'myschema', + 'relname', 'mytable', + 'relpages', 173::integer, + 'reltuples', 10000::real); + + + + The arguments schemaname and + relname are required, and specify the table. Other + arguments are the names and values of statistics corresponding to + certain columns in pg_class. + The currently-supported relation statistics are + relpages with a value of type + integer, reltuples with a value of + type real, relallvisible with a value + of type integer, and relallfrozen + with a value of type integer. + + + Additionally, this function accepts argument name + version of type integer, which + specifies the server version from which the statistics originated. + This is anticipated to be helpful in porting statistics from older + versions of PostgreSQL. + + + Minor errors are reported as a WARNING and + ignored, and remaining statistics will still be restored. If all + specified statistics are successfully restored, returns + true, otherwise false. + + + The caller must have the MAINTAIN privilege on the + table or be the owner of the database. + + + + + + + + + pg_clear_relation_stats + + pg_clear_relation_stats ( schemaname text, relname text ) + void + + + Clears table-level statistics for the given relation, as though the + table was newly created. + + + The caller must have the MAINTAIN privilege on the + table or be the owner of the database. + + + + + + + + pg_restore_attribute_stats + + pg_restore_attribute_stats ( + VARIADIC kwargs "any" ) + boolean + + + Creates or updates column-level statistics. Ordinarily, these + statistics are collected automatically or updated as a part of or , so it's not + necessary to call this function. However, it is useful after a + restore to enable the optimizer to choose better plans if + ANALYZE has not been run yet. + + + The tracked statistics may change from version to version, so + arguments are passed as pairs of argname + and argvalue in the form: + +SELECT pg_restore_attribute_stats( + 'arg1name', 'arg1value'::arg1type, + 'arg2name', 'arg2value'::arg2type, + 'arg3name', 'arg3value'::arg3type); + + + + For example, to set the avg_width and + null_frac values for the attribute + col1 of the table + mytable: + +SELECT pg_restore_attribute_stats( + 'schemaname', 'myschema', + 'relname', 'mytable', + 'attname', 'col1', + 'inherited', false, + 'avg_width', 125::integer, + 'null_frac', 0.5::real); + + + + The required arguments are schemaname and + relname with a value of type text + which specify the table; either attname with a + value of type text or attnum with a + value of type smallint, which specifies the column; and + inherited, which specifies whether the statistics + include values from child tables. Other arguments are the names and + values of statistics corresponding to columns in pg_stats. + + + Additionally, this function accepts argument name + version of type integer, which + specifies the server version from which the statistics originated. + This is anticipated to be helpful in porting statistics from older + versions of PostgreSQL. + + + Minor errors are reported as a WARNING and + ignored, and remaining statistics will still be restored. If all + specified statistics are successfully restored, returns + true, otherwise false. + + + The caller must have the MAINTAIN privilege on the + table or be the owner of the database. + + + + + + + + + pg_clear_attribute_stats + + pg_clear_attribute_stats ( + schemaname text, + relname text, + attname text, + inherited boolean ) + void + + + Clears column-level statistics for the given relation and + attribute, as though the table was newly created. + + + The caller must have the MAINTAIN privilege on + the table or be the owner of the database. + + + + + + + pg_restore_extended_stats + + pg_restore_extended_stats ( + VARIADIC kwargs "any" ) + boolean + + + Creates or updates statistics for statistics objects. Ordinarily, + these statistics are collected automatically or updated as a part of + or , so + it's not necessary to call this function. However, it is useful + after a restore to enable the optimizer to choose better plans if + ANALYZE has not been run yet. + + + The tracked statistics may change from version to version, so + arguments are passed as pairs of argname + and argvalue in the form: + + SELECT pg_restore_extended_stats( + 'arg1name', 'arg1value'::arg1type, + 'arg2name', 'arg2value'::arg2type, + 'arg3name', 'arg3value'::arg3type); + + + + For example, to set some values for the statistics object + myschema.mystatsobj: + + SELECT pg_restore_extended_stats( + 'schemaname', 'tab_schema', + 'relname', 'tab_name', + 'statistics_schemaname', 'stats_schema', + 'statistics_name', 'stats_name', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); + 'dependencies', '{"2 => 1": 1.000000, "2 => -1": 1.000000, "2 => -2": 1.000000}'::pg_dependencies, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0.5", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{1}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); + + + + The required arguments are schemaname with a value + of type name, for the schema of the table to which the + statistics are related to, relname with a value + of type name, for the table to which the statistics are + related to, statistics_schemaname + with a value of type name, which specifies the statistics + object's schema, statistics_name with a value of + type name, which specifies the name of the statistics + object and inherited, which specifies whether + the statistics include values from child tables. + + + Other arguments are the names and values of statistics corresponding + to columns in pg_stats_ext + . + This function currently supports n_distinct, + dependencies, most_common_vals, + most_common_freqs, + and most_common_base_freqs. + To accept statistics for any expressions in the extended + statistics object, the parameter exprs with a type + jsonb is available. This should be an one-dimension array + with a number of expressions matching the definition of the extended + statistics object, made of json elements for each of the statistical + columns in + pg_stats_ext_exprs. + + + Additionally, this function accepts argument name + version of type integer, which + specifies the server version from which the statistics originated. + This is anticipated to be helpful in porting statistics from older + versions of PostgreSQL. + + + Minor errors are reported as a WARNING and + ignored, and remaining statistics will still be restored. If all + specified statistics are successfully restored, returns + true, otherwise false. + + + The caller must have the MAINTAIN privilege on the + table or be the owner of the database. + + + + + + + + pg_clear_extended_stats + + pg_clear_extended_stats ( + schemaname name, + relname name, + statistics_schemaname name, + statistics_name name, + inherited boolean ) + void + + + Clears data of an extended statistics object, as though the object + was newly-created. The required arguments are + schemaname and relname to + specify the schema and table name of the relation whose statistics + are cleared, as well as statistics_schemaname + and statistics_name to specify the schema and + extended statistics name of the extended statistics object to clear. + + + The caller must have the MAINTAIN privilege on + the table or be the owner of the database. + + + + + +
+ + + lists functions that provide + information about the structure of partitioned tables. + + + + Partitioning Information Functions + + + + + Function + + + Description + + + + + + + + + pg_partition_tree + + pg_partition_tree ( regclass ) + setof record + ( relid regclass, + parentrelid regclass, + isleaf boolean, + level integer ) + + + Lists the tables or indexes in the partition tree of the + given partitioned table or partitioned index, with one row for each + partition. Information provided includes the OID of the partition, + the OID of its immediate parent, a boolean value telling if the + partition is a leaf, and an integer telling its level in the hierarchy. + The level value is 0 for the input table or index, 1 for its + immediate child partitions, 2 for their partitions, and so on. + Returns no rows if the relation does not exist or is not a partition + or partitioned table. + + + + + + + pg_partition_ancestors + + pg_partition_ancestors ( regclass ) + setof regclass + + + Lists the ancestor relations of the given partition, + including the relation itself. Returns no rows if the relation + does not exist or is not a partition or partitioned table. + + + + + + + pg_partition_root + + pg_partition_root ( regclass ) + regclass + + + Returns the top-most parent of the partition tree to which the given + relation belongs. Returns NULL if the relation + does not exist or is not a partition or partitioned table. + + + + +
+ + + For example, to check the total size of the data contained in a + partitioned table measurement, one could use the + following query: + +SELECT pg_size_pretty(sum(pg_relation_size(relid))) AS total_size + FROM pg_partition_tree('measurement'); + + + +
+ + + Index Maintenance Functions + + + shows the functions + available for index maintenance tasks. (Note that these maintenance + tasks are normally done automatically by autovacuum; use of these + functions is only required in special cases.) + These functions cannot be executed during recovery. + Use of these functions is restricted to superusers and the owner + of the given index. + + + + Index Maintenance Functions + + + + + Function + + + Description + + + + + + + + + brin_summarize_new_values + + brin_summarize_new_values ( index regclass ) + integer + + + Scans the specified BRIN index to find page ranges in the base table + that are not currently summarized by the index; for any such range it + creates a new summary index tuple by scanning those table pages. + Returns the number of new page range summaries that were inserted + into the index. + + + + + + + brin_summarize_range + + brin_summarize_range ( index regclass, blockNumber bigint ) + integer + + + Summarizes the page range covering the given block, if not already + summarized. This is + like brin_summarize_new_values except that it + only processes the page range that covers the given table block number. + + + + + + + brin_desummarize_range + + brin_desummarize_range ( index regclass, blockNumber bigint ) + void + + + Removes the BRIN index tuple that summarizes the page range covering + the given table block, if there is one. + + + + + + + gin_clean_pending_list + + gin_clean_pending_list ( index regclass ) + bigint + + + Cleans up the pending list of the specified GIN index + by moving entries in it, in bulk, to the main GIN data structure. + Returns the number of pages removed from the pending list. + If the argument is a GIN index built with + the fastupdate option disabled, no cleanup happens + and the result is zero, because the index doesn't have a pending list. + See and + for details about the pending list and fastupdate + option. + + + + +
+ +
+ + + Generic File Access Functions + + + The functions shown in provide native access to + files on the machine hosting the server. Only files within the + database cluster directory and the log_directory can be + accessed, unless the user is a superuser or is granted the role + pg_read_server_files. Use a relative path for files in + the cluster directory, and a path matching the log_directory + configuration setting for log files. + + + + Note that granting users the EXECUTE privilege on + pg_read_file(), or related functions, allows them the + ability to read any file on the server that the database server process can + read; these functions bypass all in-database privilege checks. This means + that, for example, a user with such access is able to read the contents of + the pg_authid table where authentication + information is stored, as well as read any table data in the database. + Therefore, granting access to these functions should be carefully + considered. + + + + When granting privilege on these functions, note that the table entries + showing optional parameters are mostly implemented as several physical + functions with different parameter lists. Privilege must be granted + separately on each such function, if it is to be + used. psql's \df command + can be useful to check what the actual function signatures are. + + + + Some of these functions take an optional missing_ok + parameter, which specifies the behavior when the file or directory does + not exist. If true, the function + returns NULL or an empty result set, as appropriate. + If false, an error is raised. (Failure conditions + other than file not found are reported as errors in any + case.) The default is false. + + + + Generic File Access Functions + + + + + Function + + + Description + + + + + + + + + pg_ls_dir + + pg_ls_dir ( dirname text , missing_ok boolean, include_dot_dirs boolean ) + setof text + + + Returns the names of all files (and directories and other special + files) in the specified + directory. The include_dot_dirs parameter + indicates whether . and .. are to be + included in the result set; the default is to exclude them. Including + them can be useful when missing_ok + is true, to distinguish an empty directory from a + non-existent directory. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_ls_logdir + + pg_ls_logdir () + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's log directory. Filenames beginning with + a dot, directories, and other special files are excluded. + + + This function is restricted to superusers and roles with privileges of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_ls_waldir + + pg_ls_waldir () + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's write-ahead log (WAL) directory. + Filenames beginning with a dot, directories, and other special files + are excluded. + + + This function is restricted to superusers and roles with privileges of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_ls_logicalmapdir + + pg_ls_logicalmapdir () + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's pg_logical/mappings + directory. Filenames beginning with a dot, directories, and other + special files are excluded. + + + This function is restricted to superusers and members of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_ls_logicalsnapdir + + pg_ls_logicalsnapdir () + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's pg_logical/snapshots + directory. Filenames beginning with a dot, directories, and other + special files are excluded. + + + This function is restricted to superusers and members of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_ls_replslotdir + + pg_ls_replslotdir ( slot_name text ) + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's pg_replslot/slot_name + directory, where slot_name is the name of the + replication slot provided as input of the function. Filenames beginning + with a dot, directories, and other special files are excluded. + + + This function is restricted to superusers and members of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_ls_summariesdir + + pg_ls_summariesdir () + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's WAL summaries directory + (pg_wal/summaries). Filenames beginning + with a dot, directories, and other special files are excluded. + + + This function is restricted to superusers and members of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_ls_archive_statusdir + + pg_ls_archive_statusdir () + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the server's WAL archive status directory + (pg_wal/archive_status). Filenames beginning + with a dot, directories, and other special files are excluded. + + + This function is restricted to superusers and members of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + + pg_ls_tmpdir + + pg_ls_tmpdir ( tablespace oid ) + setof record + ( name text, + size bigint, + modification timestamp with time zone ) + + + Returns the name, size, and last modification time (mtime) of each + ordinary file in the temporary file directory for the + specified tablespace. + If tablespace is not provided, + the pg_default tablespace is examined. Filenames + beginning with a dot, directories, and other special files are + excluded. + + + This function is restricted to superusers and members of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_read_file + + pg_read_file ( filename text , offset bigint, length bigint , missing_ok boolean ) + text + + + Returns all or part of a text file, starting at the + given byte offset, returning at + most length bytes (less if the end of file is + reached first). If offset is negative, it is + relative to the end of the file. If offset + and length are omitted, the entire file is + returned. The bytes read from the file are interpreted as a string in + the database's encoding; an error is thrown if they are not valid in + that encoding. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + + + pg_read_binary_file + + pg_read_binary_file ( filename text , offset bigint, length bigint , missing_ok boolean ) + bytea + + + Returns all or part of a file. This function is identical to + pg_read_file except that it can read arbitrary + binary data, returning the result as bytea + not text; accordingly, no encoding checks are performed. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + In combination with the convert_from function, + this function can be used to read a text file in a specified encoding + and convert to the database's encoding: + +SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8'); + + + + + + + + pg_stat_file + + pg_stat_file ( filename text , missing_ok boolean ) + record + ( size bigint, + access timestamp with time zone, + modification timestamp with time zone, + change timestamp with time zone, + creation timestamp with time zone, + isdir boolean ) + + + Returns a record containing the file's size, last access time stamp, + last modification time stamp, last file status change time stamp (Unix + platforms only), file creation time stamp (Windows only), and a flag + indicating if it is a directory. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + + +
+ +
+ + + Advisory Lock Functions + + + The functions shown in + manage advisory locks. For details about proper use of these functions, + see . + + + + All these functions are intended to be used to lock application-defined + resources, which can be identified either by a single 64-bit key value or + two 32-bit key values (note that these two key spaces do not overlap). + If another session already holds a conflicting lock on the same resource + identifier, the functions will either wait until the resource becomes + available, or return a false result, as appropriate for + the function. + Locks can be either shared or exclusive: a shared lock does not conflict + with other shared locks on the same resource, only with exclusive locks. + Locks can be taken at session level (so that they are held until released + or the session ends) or at transaction level (so that they are held until + the current transaction ends; there is no provision for manual release). + Multiple session-level lock requests stack, so that if the same resource + identifier is locked three times there must then be three unlock requests + to release the resource in advance of session end. + + + + Advisory Lock Functions + + + + + Function + + + Description + + + + + + + + + pg_advisory_lock + + pg_advisory_lock ( key bigint ) + void + + + pg_advisory_lock ( key1 integer, key2 integer ) + void + + + Obtains an exclusive session-level advisory lock, waiting if necessary. + + + + + + + pg_advisory_lock_shared + + pg_advisory_lock_shared ( key bigint ) + void + + + pg_advisory_lock_shared ( key1 integer, key2 integer ) + void + + + Obtains a shared session-level advisory lock, waiting if necessary. + + + + + + + pg_advisory_unlock + + pg_advisory_unlock ( key bigint ) + boolean + + + pg_advisory_unlock ( key1 integer, key2 integer ) + boolean + + + Releases a previously-acquired exclusive session-level advisory lock. + Returns true if the lock is successfully released. + If the lock was not held, false is returned, and in + addition, an SQL warning will be reported by the server. + + + + + + + pg_advisory_unlock_all + + pg_advisory_unlock_all () + void + + + Releases all session-level advisory locks held by the current session. + (This function is implicitly invoked at session end, even if the + client disconnects ungracefully.) + + + + + + + pg_advisory_unlock_shared + + pg_advisory_unlock_shared ( key bigint ) + boolean + + + pg_advisory_unlock_shared ( key1 integer, key2 integer ) + boolean + + + Releases a previously-acquired shared session-level advisory lock. + Returns true if the lock is successfully released. + If the lock was not held, false is returned, and in + addition, an SQL warning will be reported by the server. + + + + + + + pg_advisory_xact_lock + + pg_advisory_xact_lock ( key bigint ) + void + + + pg_advisory_xact_lock ( key1 integer, key2 integer ) + void + + + Obtains an exclusive transaction-level advisory lock, waiting if + necessary. + + + + + + + pg_advisory_xact_lock_shared + + pg_advisory_xact_lock_shared ( key bigint ) + void + + + pg_advisory_xact_lock_shared ( key1 integer, key2 integer ) + void + + + Obtains a shared transaction-level advisory lock, waiting if + necessary. + + + + + + + pg_try_advisory_lock + + pg_try_advisory_lock ( key bigint ) + boolean + + + pg_try_advisory_lock ( key1 integer, key2 integer ) + boolean + + + Obtains an exclusive session-level advisory lock if available. + This will either obtain the lock immediately and + return true, or return false + without waiting if the lock cannot be acquired immediately. + + + + + + + pg_try_advisory_lock_shared + + pg_try_advisory_lock_shared ( key bigint ) + boolean + + + pg_try_advisory_lock_shared ( key1 integer, key2 integer ) + boolean + + + Obtains a shared session-level advisory lock if available. + This will either obtain the lock immediately and + return true, or return false + without waiting if the lock cannot be acquired immediately. + + + + + + + pg_try_advisory_xact_lock + + pg_try_advisory_xact_lock ( key bigint ) + boolean + + + pg_try_advisory_xact_lock ( key1 integer, key2 integer ) + boolean + + + Obtains an exclusive transaction-level advisory lock if available. + This will either obtain the lock immediately and + return true, or return false + without waiting if the lock cannot be acquired immediately. + + + + + + + pg_try_advisory_xact_lock_shared + + pg_try_advisory_xact_lock_shared ( key bigint ) + boolean + + + pg_try_advisory_xact_lock_shared ( key1 integer, key2 integer ) + boolean + + + Obtains a shared transaction-level advisory lock if available. + This will either obtain the lock immediately and + return true, or return false + without waiting if the lock cannot be acquired immediately. + + + + +
+ +
+ + + Data Checksum Functions + + + The functions shown in can + be used to enable or disable data checksums in a running cluster. + + + Changing data checksums can be done in a cluster with concurrent activity + without blocking queries, but overall system performance will be affected. + See for further details on how changing the + data checksums state can affect a system and possible mitigations for how + to reduce the impact. + + + + Data Checksum Functions + + + + + Function + + + Description + + + + + + + + + pg_enable_data_checksums + + pg_enable_data_checksums ( cost_delay int, cost_limit int ) + void + + + Initiates the process of enabling data checksums for the cluster. This + will set the data checksums state to inprogress-on + as well as start a background worker that will process all pages in all + databases and enable data checksums on them. When all pages have + been processed, the cluster will automatically set data checksums state + to on. This operation is WAL logged and replicated + to all standby nodes. + + + If cost_delay and cost_limit are + specified, the process is throttled using the same principles as + Cost-based Vacuum Delay. + + + + + + + + pg_disable_data_checksums + + pg_disable_data_checksums () + void + + + Disables data checksum calculation and validation for the cluster. This + will set the data checksum state to inprogress-off + while data checksums are being disabled. When all active backends have + stopped validating data checksums, the data checksum state will be + set to off. + + + + + +
+ +
+ +
diff --git a/doc/src/sgml/func/func-aggregate.sgml b/doc/src/sgml/func/func-aggregate.sgml new file mode 100644 index 0000000000000..8b5eaeb2e94b6 --- /dev/null +++ b/doc/src/sgml/func/func-aggregate.sgml @@ -0,0 +1,1418 @@ + + Aggregate Functions + + + aggregate function + built-in + + + + Aggregate functions compute a single result + from a set of input values. The built-in general-purpose aggregate + functions are listed in + while statistical aggregates are in . + The built-in within-group ordered-set aggregate functions + are listed in + while the built-in within-group hypothetical-set ones are in . Grouping operations, + which are closely related to aggregate functions, are listed in + . + The special syntax considerations for aggregate + functions are explained in . + Consult for additional introductory + information. + + + + Aggregate functions that support Partial Mode + are eligible to participate in various optimizations, such as parallel + aggregation. + + + + While all aggregates below accept an optional + ORDER BY clause (as outlined in ), the clause has only been added to + aggregates whose output is affected by ordering. + + + + General-Purpose Aggregate Functions + + + + + + + Function + + + Description + + Partial Mode + + + + + + + + any_value + + any_value ( anyelement ) + same as input type + + + Returns an arbitrary value from the non-null input values. + + Yes + + + + + + array_agg + + array_agg ( anynonarray ORDER BY input_sort_columns ) + anyarray + + + Collects all the input values, including nulls, into an array. + + Yes + + + + + array_agg ( anyarray ORDER BY input_sort_columns ) + anyarray + + + Concatenates all the input arrays into an array of one higher + dimension. (The inputs must all have the same dimensionality, and + cannot be empty or null.) + + Yes + + + + + + average + + + avg + + avg ( smallint ) + numeric + + + avg ( integer ) + numeric + + + avg ( bigint ) + numeric + + + avg ( numeric ) + numeric + + + avg ( real ) + double precision + + + avg ( double precision ) + double precision + + + avg ( interval ) + interval + + + Computes the average (arithmetic mean) of all the non-null input + values. + + Yes + + + + + + bit_and + + bit_and ( smallint ) + smallint + + + bit_and ( integer ) + integer + + + bit_and ( bigint ) + bigint + + + bit_and ( bit ) + bit + + + Computes the bitwise AND of all non-null input values. + + Yes + + + + + + bit_or + + bit_or ( smallint ) + smallint + + + bit_or ( integer ) + integer + + + bit_or ( bigint ) + bigint + + + bit_or ( bit ) + bit + + + Computes the bitwise OR of all non-null input values. + + Yes + + + + + + bit_xor + + bit_xor ( smallint ) + smallint + + + bit_xor ( integer ) + integer + + + bit_xor ( bigint ) + bigint + + + bit_xor ( bit ) + bit + + + Computes the bitwise exclusive OR of all non-null input values. + Can be useful as a checksum for an unordered set of values. + + Yes + + + + + + bool_and + + bool_and ( boolean ) + boolean + + + Returns true if all non-null input values are true, otherwise false. + + Yes + + + + + + bool_or + + bool_or ( boolean ) + boolean + + + Returns true if any non-null input value is true, otherwise false. + + Yes + + + + + + count + + count ( * ) + bigint + + + Computes the number of input rows. + + Yes + + + + + count ( "any" ) + bigint + + + Computes the number of input rows in which the input value is not + null. + + Yes + + + + + + every + + every ( boolean ) + boolean + + + This is the SQL standard's equivalent to bool_and. + + Yes + + + + + + json_agg + + json_agg ( anyelement ORDER BY input_sort_columns ) + json + + + + jsonb_agg + + jsonb_agg ( anyelement ORDER BY input_sort_columns ) + jsonb + + + Collects all the input values, including nulls, into a JSON array. + Values are converted to JSON as per to_json + or to_jsonb. + + No + + + + + + json_agg_strict + + json_agg_strict ( anyelement ) + json + + + + jsonb_agg_strict + + jsonb_agg_strict ( anyelement ) + jsonb + + + Collects all the input values, skipping nulls, into a JSON array. + Values are converted to JSON as per to_json + or to_jsonb. + + No + + + + + json_arrayagg + json_arrayagg ( + value_expression + ORDER BY sort_expression + { NULL | ABSENT } ON NULL + RETURNING data_type FORMAT JSON ENCODING UTF8 ) + + + Behaves in the same way as json_array + but as an aggregate function so it only takes one + value_expression parameter. + If ABSENT ON NULL is specified, any NULL + values are omitted. + If ORDER BY is specified, the elements will + appear in the array in that order rather than in the input order. + + + SELECT json_arrayagg(v) FROM (VALUES(2),(1)) t(v) + [2, 1] + + No + + + + + json_objectagg + json_objectagg ( + { key_expression { VALUE | ':' } value_expression } + { NULL | ABSENT } ON NULL + { WITH | WITHOUT } UNIQUE KEYS + RETURNING data_type FORMAT JSON ENCODING UTF8 ) + + + Behaves like json_object, but as an + aggregate function, so it only takes one + key_expression and one + value_expression parameter. + + + SELECT json_objectagg(k:v) FROM (VALUES ('a'::text,current_date),('b',current_date + 1)) AS t(k,v) + { "a" : "2022-05-10", "b" : "2022-05-11" } + + No + + + + + + json_object_agg + + json_object_agg ( key + "any", value + "any" + ORDER BY input_sort_columns ) + json + + + + jsonb_object_agg + + jsonb_object_agg ( key + "any", value + "any" + ORDER BY input_sort_columns ) + jsonb + + + Collects all the key/value pairs into a JSON object. Key arguments + are coerced to text; value arguments are converted as per + to_json or to_jsonb. + Values can be null, but keys cannot. + + No + + + + + + json_object_agg_strict + + json_object_agg_strict ( + key "any", + value "any" ) + json + + + + jsonb_object_agg_strict + + jsonb_object_agg_strict ( + key "any", + value "any" ) + jsonb + + + Collects all the key/value pairs into a JSON object. Key arguments + are coerced to text; value arguments are converted as per + to_json or to_jsonb. + The key cannot be null. If the + value is null then the entry is skipped, + + No + + + + + + json_object_agg_unique + + json_object_agg_unique ( + key "any", + value "any" ) + json + + + + jsonb_object_agg_unique + + jsonb_object_agg_unique ( + key "any", + value "any" ) + jsonb + + + Collects all the key/value pairs into a JSON object. Key arguments + are coerced to text; value arguments are converted as per + to_json or to_jsonb. + Values can be null, but keys cannot. + If there is a duplicate key an error is thrown. + + No + + + + + + json_object_agg_unique_strict + + json_object_agg_unique_strict ( + key "any", + value "any" ) + json + + + + jsonb_object_agg_unique_strict + + jsonb_object_agg_unique_strict ( + key "any", + value "any" ) + jsonb + + + Collects all the key/value pairs into a JSON object. Key arguments + are coerced to text; value arguments are converted as per + to_json or to_jsonb. + The key cannot be null. If the + value is null then the entry is skipped. + If there is a duplicate key an error is thrown. + + No + + + + + + max + + max ( see text ) + same as input type + + + Computes the maximum of the non-null input + values. Available for any numeric, string, date/time, or enum type, + as well as bytea, inet, interval, + money, oid, oid8, + pg_lsn, tid, xid8, + and also arrays and composite types containing sortable data types. + + Yes + + + + + + min + + min ( see text ) + same as input type + + + Computes the minimum of the non-null input + values. Available for any numeric, string, date/time, or enum type, + as well as bytea, inet, interval, + money, oid, oid8, + pg_lsn, tid, xid8, + and also arrays and composite types containing sortable data types. + + Yes + + + + + + range_agg + + range_agg ( value + anyrange ) + anymultirange + + + range_agg ( value + anymultirange ) + anymultirange + + + Computes the union of the non-null input values. + + No + + + + + + range_intersect_agg + + range_intersect_agg ( value + anyrange ) + anyrange + + + range_intersect_agg ( value + anymultirange ) + anymultirange + + + Computes the intersection of the non-null input values. + + No + + + + + + string_agg + + string_agg ( value + text, delimiter text ) + text + + + string_agg ( value + bytea, delimiter bytea + ORDER BY input_sort_columns ) + bytea + + + Concatenates the non-null input values into a string. Each value + after the first is preceded by the + corresponding delimiter (if it's not null). + + Yes + + + + + + sum + + sum ( smallint ) + bigint + + + sum ( integer ) + bigint + + + sum ( bigint ) + numeric + + + sum ( numeric ) + numeric + + + sum ( real ) + real + + + sum ( double precision ) + double precision + + + sum ( interval ) + interval + + + sum ( money ) + money + + + Computes the sum of the non-null input values. + + Yes + + + + + + xmlagg + + xmlagg ( xml ORDER BY input_sort_columns ) + xml + + + Concatenates the non-null XML input values (see + ). + + No + + + +
+ + + It should be noted that except for count, + these functions return a null value when no rows are selected. In + particular, sum of no rows returns null, not + zero as one might expect, and array_agg + returns null rather than an empty array when there are no input + rows. The coalesce function can be used to + substitute zero or an empty array for null when necessary. + + + + The aggregate functions array_agg, + json_agg, jsonb_agg, + json_agg_strict, jsonb_agg_strict, + json_object_agg, jsonb_object_agg, + json_object_agg_strict, jsonb_object_agg_strict, + json_object_agg_unique, jsonb_object_agg_unique, + json_object_agg_unique_strict, + jsonb_object_agg_unique_strict, + string_agg, + and xmlagg, as well as similar user-defined + aggregate functions, produce meaningfully different result values + depending on the order of the input values. This ordering is + unspecified by default, but can be controlled by writing an + ORDER BY clause within the aggregate call, as shown in + . + Alternatively, supplying the input values from a sorted subquery + will usually work. For example: + + + + Beware that this approach can fail if the outer query level contains + additional processing, such as a join, because that might cause the + subquery's output to be reordered before the aggregate is computed. + + + + + ANY + + + SOME + + + The boolean aggregates bool_and and + bool_or correspond to the standard SQL aggregates + every and any or + some. + PostgreSQL + supports every, but not any + or some, because there is an ambiguity built into + the standard syntax: + +SELECT b1 = ANY((SELECT b2 FROM t2 ...)) FROM t1 ...; + + Here ANY can be considered either as introducing + a subquery, or as being an aggregate function, if the subquery + returns one row with a Boolean value. + Thus the standard name cannot be given to these aggregates. + + + + + + Users accustomed to working with other SQL database management + systems might be disappointed by the performance of the + count aggregate when it is applied to the + entire table. A query like: + +SELECT count(*) FROM sometable; + + will require effort proportional to the size of the table: + PostgreSQL will need to scan either the + entire table or the entirety of an index that includes all rows in + the table. + + + + + shows + aggregate functions typically used in statistical analysis. + (These are separated out merely to avoid cluttering the listing + of more-commonly-used aggregates.) Functions shown as + accepting numeric_type are available for all + the types smallint, integer, + bigint, numeric, real, + and double precision. + Where the description mentions + N, it means the + number of input rows for which all the input expressions are non-null. + In all cases, null is returned if the computation is meaningless, + for example when N is zero. + + + + statistics + + + linear regression + + + + Aggregate Functions for Statistics + + + + + + + Function + + + Description + + Partial Mode + + + + + + + + correlation + + + corr + + corr ( Y double precision, X double precision ) + double precision + + + Computes the correlation coefficient. + + Yes + + + + + + covariance + population + + + covar_pop + + covar_pop ( Y double precision, X double precision ) + double precision + + + Computes the population covariance. + + Yes + + + + + + covariance + sample + + + covar_samp + + covar_samp ( Y double precision, X double precision ) + double precision + + + Computes the sample covariance. + + Yes + + + + + + regr_avgx + + regr_avgx ( Y double precision, X double precision ) + double precision + + + Computes the average of the independent variable, + sum(X)/N. + + Yes + + + + + + regr_avgy + + regr_avgy ( Y double precision, X double precision ) + double precision + + + Computes the average of the dependent variable, + sum(Y)/N. + + Yes + + + + + + regr_count + + regr_count ( Y double precision, X double precision ) + bigint + + + Computes the number of rows in which both inputs are non-null. + + Yes + + + + + + regression intercept + + + regr_intercept + + regr_intercept ( Y double precision, X double precision ) + double precision + + + Computes the y-intercept of the least-squares-fit linear equation + determined by the + (X, Y) pairs. + + Yes + + + + + + regr_r2 + + regr_r2 ( Y double precision, X double precision ) + double precision + + + Computes the square of the correlation coefficient. + + Yes + + + + + + regression slope + + + regr_slope + + regr_slope ( Y double precision, X double precision ) + double precision + + + Computes the slope of the least-squares-fit linear equation determined + by the (X, Y) + pairs. + + Yes + + + + + + regr_sxx + + regr_sxx ( Y double precision, X double precision ) + double precision + + + Computes the sum of squares of the independent + variable, + sum(X^2) - sum(X)^2/N. + + Yes + + + + + + regr_sxy + + regr_sxy ( Y double precision, X double precision ) + double precision + + + Computes the sum of products of independent times + dependent variables, + sum(X*Y) - sum(X) * sum(Y)/N. + + Yes + + + + + + regr_syy + + regr_syy ( Y double precision, X double precision ) + double precision + + + Computes the sum of squares of the dependent + variable, + sum(Y^2) - sum(Y)^2/N. + + Yes + + + + + + standard deviation + + + stddev + + stddev ( numeric_type ) + double precision + for real or double precision, + otherwise numeric + + + This is a historical alias for stddev_samp. + + Yes + + + + + + standard deviation + population + + + stddev_pop + + stddev_pop ( numeric_type ) + double precision + for real or double precision, + otherwise numeric + + + Computes the population standard deviation of the input values. + + Yes + + + + + + standard deviation + sample + + + stddev_samp + + stddev_samp ( numeric_type ) + double precision + for real or double precision, + otherwise numeric + + + Computes the sample standard deviation of the input values. + + Yes + + + + + + variance + + variance ( numeric_type ) + double precision + for real or double precision, + otherwise numeric + + + This is a historical alias for var_samp. + + Yes + + + + + + variance + population + + + var_pop + + var_pop ( numeric_type ) + double precision + for real or double precision, + otherwise numeric + + + Computes the population variance of the input values (square of the + population standard deviation). + + Yes + + + + + + variance + sample + + + var_samp + + var_samp ( numeric_type ) + double precision + for real or double precision, + otherwise numeric + + + Computes the sample variance of the input values (square of the sample + standard deviation). + + Yes + + + +
+ + + shows some + aggregate functions that use the ordered-set aggregate + syntax. These functions are sometimes referred to as inverse + distribution functions. Their aggregated input is introduced by + ORDER BY, and they may also take a direct + argument that is not aggregated, but is computed only once. + All these functions ignore null values in their aggregated input. + For those that take a fraction parameter, the + fraction value must be between 0 and 1; an error is thrown if not. + However, a null fraction value simply produces a + null result. + + + + ordered-set aggregate + built-in + + + inverse distribution + + + + Ordered-Set Aggregate Functions + + + + + + + Function + + + Description + + Partial Mode + + + + + + + + mode + statistical + + mode () WITHIN GROUP ( ORDER BY anyelement ) + anyelement + + + Computes the mode, the most frequent + value of the aggregated argument (arbitrarily choosing the first one + if there are multiple equally-frequent values). The aggregated + argument must be of a sortable type. + + No + + + + + + percentile + continuous + + percentile_cont ( fraction double precision ) WITHIN GROUP ( ORDER BY double precision ) + double precision + + + percentile_cont ( fraction double precision ) WITHIN GROUP ( ORDER BY interval ) + interval + + + Computes the continuous percentile, a value + corresponding to the specified fraction + within the ordered set of aggregated argument values. This will + interpolate between adjacent input items if needed. + + No + + + + + percentile_cont ( fractions double precision[] ) WITHIN GROUP ( ORDER BY double precision ) + double precision[] + + + percentile_cont ( fractions double precision[] ) WITHIN GROUP ( ORDER BY interval ) + interval[] + + + Computes multiple continuous percentiles. The result is an array of + the same dimensions as the fractions + parameter, with each non-null element replaced by the (possibly + interpolated) value corresponding to that percentile. + + No + + + + + + percentile + discrete + + percentile_disc ( fraction double precision ) WITHIN GROUP ( ORDER BY anyelement ) + anyelement + + + Computes the discrete percentile, the first + value within the ordered set of aggregated argument values whose + position in the ordering equals or exceeds the + specified fraction. The aggregated + argument must be of a sortable type. + + No + + + + + percentile_disc ( fractions double precision[] ) WITHIN GROUP ( ORDER BY anyelement ) + anyarray + + + Computes multiple discrete percentiles. The result is an array of the + same dimensions as the fractions parameter, + with each non-null element replaced by the input value corresponding + to that percentile. + The aggregated argument must be of a sortable type. + + No + + + +
+ + + hypothetical-set aggregate + built-in + + + + Each of the hypothetical-set aggregates listed in + is associated with a + window function of the same name defined in + . In each case, the aggregate's result + is the value that the associated window function would have + returned for the hypothetical row constructed from + args, if such a row had been added to the sorted + group of rows represented by the sorted_args. + For each of these functions, the list of direct arguments + given in args must match the number and types of + the aggregated arguments given in sorted_args. + Unlike most built-in aggregates, these aggregates are not strict, that is + they do not drop input rows containing nulls. Null values sort according + to the rule specified in the ORDER BY clause. + + + + Hypothetical-Set Aggregate Functions + + + + + + + Function + + + Description + + Partial Mode + + + + + + + + rank + hypothetical + + rank ( args ) WITHIN GROUP ( ORDER BY sorted_args ) + bigint + + + Computes the rank of the hypothetical row, with gaps; that is, the row + number of the first row in its peer group. + + No + + + + + + dense_rank + hypothetical + + dense_rank ( args ) WITHIN GROUP ( ORDER BY sorted_args ) + bigint + + + Computes the rank of the hypothetical row, without gaps; this function + effectively counts peer groups. + + No + + + + + + percent_rank + hypothetical + + percent_rank ( args ) WITHIN GROUP ( ORDER BY sorted_args ) + double precision + + + Computes the relative rank of the hypothetical row, that is + (rank - 1) / (total rows - 1). + The value thus ranges from 0 to 1 inclusive. + + No + + + + + + cume_dist + hypothetical + + cume_dist ( args ) WITHIN GROUP ( ORDER BY sorted_args ) + double precision + + + Computes the cumulative distribution, that is (number of rows + preceding or peers with hypothetical row) / (total rows). The value + thus ranges from 1/N to 1. + + No + + + +
+ + + Grouping Operations + + + + + Function + + + Description + + + + + + + + + GROUPING + + GROUPING ( group_by_expression(s) ) + integer + + + Returns a bit mask indicating which GROUP BY + expressions are not included in the current grouping set. + Bits are assigned with the rightmost argument corresponding to the + least-significant bit; each bit is 0 if the corresponding expression + is included in the grouping criteria of the grouping set generating + the current result row, and 1 if it is not included. + + + + +
+ + + The grouping operations shown in + are used in conjunction with + grouping sets (see ) to distinguish + result rows. The arguments to the GROUPING function + are not actually evaluated, but they must exactly match expressions given + in the GROUP BY clause of the associated query level. + For example: + +=> SELECT * FROM items_sold; + make | model | sales +-------+-------+------- + Foo | GT | 10 + Foo | Tour | 20 + Bar | City | 15 + Bar | Sport | 5 +(4 rows) + +=> SELECT make, model, GROUPING(make,model), sum(sales) FROM items_sold GROUP BY ROLLUP(make,model); + make | model | grouping | sum +-------+-------+----------+----- + Foo | GT | 0 | 10 + Foo | Tour | 0 | 20 + Bar | City | 0 | 15 + Bar | Sport | 0 | 5 + Foo | | 1 | 30 + Bar | | 1 | 20 + | | 3 | 50 +(7 rows) + + Here, the grouping value 0 in the + first four rows shows that those have been grouped normally, over both the + grouping columns. The value 1 indicates + that model was not grouped by in the next-to-last two + rows, and the value 3 indicates that + neither make nor model was grouped + by in the last row (which therefore is an aggregate over all the input + rows). + + +
diff --git a/doc/src/sgml/func/func-array.sgml b/doc/src/sgml/func/func-array.sgml new file mode 100644 index 0000000000000..7f162bd767023 --- /dev/null +++ b/doc/src/sgml/func/func-array.sgml @@ -0,0 +1,646 @@ + + Array Functions and Operators + + + shows the specialized operators + available for array types. + In addition to those, the usual comparison operators shown in are available for + arrays. The comparison operators compare the array contents + element-by-element, using the default B-tree comparison function for + the element data type, and sort based on the first difference. + In multidimensional arrays the elements are visited in row-major order + (last subscript varies most rapidly). + If the contents of two arrays are equal but the dimensionality is + different, the first difference in the dimensionality information + determines the sort order. + + + + Array Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + anyarray @> anyarray + boolean + + + Does the first array contain the second, that is, does each element + appearing in the second array equal some element of the first array? + (Duplicates are not treated specially, + thus ARRAY[1] and ARRAY[1,1] are + each considered to contain the other.) + + + ARRAY[1,4,3] @> ARRAY[3,1,3] + t + + + + + + anyarray <@ anyarray + boolean + + + Is the first array contained by the second? + + + ARRAY[2,2,7] <@ ARRAY[1,7,4,2,6] + t + + + + + + anyarray && anyarray + boolean + + + Do the arrays overlap, that is, have any elements in common? + + + ARRAY[1,4,3] && ARRAY[2,1] + t + + + + + + anycompatiblearray || anycompatiblearray + anycompatiblearray + + + Concatenates the two arrays. Concatenating a null or empty array is a + no-op; otherwise the arrays must have the same number of dimensions + (as illustrated by the first example) or differ in number of + dimensions by one (as illustrated by the second). + If the arrays are not of identical element types, they will be coerced + to a common type (see ). + + + ARRAY[1,2,3] || ARRAY[4,5,6,7] + {1,2,3,4,5,6,7} + + + ARRAY[1,2,3] || ARRAY[[4,5,6],[7,8,9.9]] + {{1,2,3},{4,5,6},{7,8,9.9}} + + + + + + anycompatible || anycompatiblearray + anycompatiblearray + + + Concatenates an element onto the front of an array (which must be + empty or one-dimensional). + + + 3 || ARRAY[4,5,6] + {3,4,5,6} + + + + + + anycompatiblearray || anycompatible + anycompatiblearray + + + Concatenates an element onto the end of an array (which must be + empty or one-dimensional). + + + ARRAY[4,5,6] || 7 + {4,5,6,7} + + + + +
+ + + See for more details about array operator + behavior. See for more details about + which operators support indexed operations. + + + + shows the functions + available for use with array types. See + for more information and examples of the use of these functions. + + + + Array Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + array_append + + array_append ( anycompatiblearray, anycompatible ) + anycompatiblearray + + + Appends an element to the end of an array (same as + the anycompatiblearray || anycompatible + operator). + + + array_append(ARRAY[1,2], 3) + {1,2,3} + + + + + + + array_cat + + array_cat ( anycompatiblearray, anycompatiblearray ) + anycompatiblearray + + + Concatenates two arrays (same as + the anycompatiblearray || anycompatiblearray + operator). + + + array_cat(ARRAY[1,2,3], ARRAY[4,5]) + {1,2,3,4,5} + + + + + + + array_dims + + array_dims ( anyarray ) + text + + + Returns a text representation of the array's dimensions. + + + array_dims(ARRAY[[1,2,3], [4,5,6]]) + [1:2][1:3] + + + + + + + array_fill + + array_fill ( anyelement, integer[] + , integer[] ) + anyarray + + + Returns an array filled with copies of the given value, having + dimensions of the lengths specified by the second argument. + The optional third argument supplies lower-bound values for each + dimension (which default to all 1). + + + array_fill(11, ARRAY[2,3]) + {{11,11,11},{11,11,11}} + + + array_fill(7, ARRAY[3], ARRAY[2]) + [2:4]={7,7,7} + + + + + + + array_length + + array_length ( anyarray, integer ) + integer + + + Returns the length of the requested array dimension. + (Produces NULL instead of 0 for empty or missing array dimensions.) + + + array_length(array[1,2,3], 1) + 3 + + + array_length(array[]::int[], 1) + NULL + + + array_length(array['text'], 2) + NULL + + + + + + + array_lower + + array_lower ( anyarray, integer ) + integer + + + Returns the lower bound of the requested array dimension. + + + array_lower('[0:2]={1,2,3}'::integer[], 1) + 0 + + + + + + + array_ndims + + array_ndims ( anyarray ) + integer + + + Returns the number of dimensions of the array. + + + array_ndims(ARRAY[[1,2,3], [4,5,6]]) + 2 + + + + + + + array_position + + array_position ( anycompatiblearray, anycompatible , integer ) + integer + + + Returns the subscript of the first occurrence of the second argument + in the array, or NULL if it's not present. + If the third argument is given, the search begins at that subscript. + The array must be one-dimensional. + Comparisons are done using IS NOT DISTINCT FROM + semantics, so it is possible to search for NULL. + + + array_position(ARRAY['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], 'mon') + 2 + + + + + + + array_positions + + array_positions ( anycompatiblearray, anycompatible ) + integer[] + + + Returns an array of the subscripts of all occurrences of the second + argument in the array given as first argument. + The array must be one-dimensional. + Comparisons are done using IS NOT DISTINCT FROM + semantics, so it is possible to search for NULL. + NULL is returned only if the array + is NULL; if the value is not found in the array, an + empty array is returned. + + + array_positions(ARRAY['A','A','B','A'], 'A') + {1,2,4} + + + + + + + array_prepend + + array_prepend ( anycompatible, anycompatiblearray ) + anycompatiblearray + + + Prepends an element to the beginning of an array (same as + the anycompatible || anycompatiblearray + operator). + + + array_prepend(1, ARRAY[2,3]) + {1,2,3} + + + + + + + array_remove + + array_remove ( anycompatiblearray, anycompatible ) + anycompatiblearray + + + Removes all elements equal to the given value from the array. + The array must be one-dimensional. + Comparisons are done using IS NOT DISTINCT FROM + semantics, so it is possible to remove NULLs. + + + array_remove(ARRAY[1,2,3,2], 2) + {1,3} + + + + + + + array_replace + + array_replace ( anycompatiblearray, anycompatible, anycompatible ) + anycompatiblearray + + + Replaces each array element equal to the second argument with the + third argument. + + + array_replace(ARRAY[1,2,5,4], 5, 3) + {1,2,3,4} + + + + + + + array_reverse + + array_reverse ( anyarray ) + anyarray + + + Reverses the first dimension of the array. + + + array_reverse(ARRAY[[1,2],[3,4],[5,6]]) + {{5,6},{3,4},{1,2}} + + + + + + + array_sample + + array_sample ( array anyarray, n integer ) + anyarray + + + Returns an array of n items randomly selected + from array. n may not + exceed the length of array's first dimension. + If array is multi-dimensional, + an item is a slice having a given first subscript. + + + array_sample(ARRAY[1,2,3,4,5,6], 3) + {2,6,1} + + + array_sample(ARRAY[[1,2],[3,4],[5,6]], 2) + {{5,6},{1,2}} + + + + + + + array_shuffle + + array_shuffle ( anyarray ) + anyarray + + + Randomly shuffles the first dimension of the array. + + + array_shuffle(ARRAY[[1,2],[3,4],[5,6]]) + {{5,6},{1,2},{3,4}} + + + + + + + array_sort + + array_sort ( + array anyarray + , descending boolean + , nulls_first boolean + ) + anyarray + + + Sorts the first dimension of the array. + The sort order is determined by the default sort ordering of the + array's element type; however, if the element type is collatable, + the collation to use can be specified by adding + a COLLATE clause to + the array argument. + + + If descending is true then sort in + descending order, otherwise ascending order. If omitted, the + default is ascending order. + If nulls_first is true then nulls appear + before non-null values, otherwise nulls appear after non-null + values. + If omitted, nulls_first is taken to have + the same value as descending. + + + array_sort(ARRAY[[2,4],[2,1],[6,5]]) + {{2,1},{2,4},{6,5}} + + + + + + + array_to_string + + array_to_string ( array anyarray, delimiter text , null_string text ) + text + + + Converts each array element to its text representation, and + concatenates those separated by + the delimiter string. + If null_string is given and is + not NULL, then NULL array + entries are represented by that string; otherwise, they are omitted. + See also string_to_array. + + + array_to_string(ARRAY[1, 2, 3, NULL, 5], ',', '*') + 1,2,3,*,5 + + + + + + + array_upper + + array_upper ( anyarray, integer ) + integer + + + Returns the upper bound of the requested array dimension. + + + array_upper(ARRAY[1,8,3,7], 1) + 4 + + + + + + + cardinality + + cardinality ( anyarray ) + integer + + + Returns the total number of elements in the array, or 0 if the array + is empty. + + + cardinality(ARRAY[[1,2],[3,4]]) + 4 + + + + + + + trim_array + + trim_array ( array anyarray, n integer ) + anyarray + + + Trims an array by removing the last n elements. + If the array is multidimensional, only the first dimension is trimmed. + + + trim_array(ARRAY[1,2,3,4,5,6], 2) + {1,2,3,4} + + + + + + + unnest + + unnest ( anyarray ) + setof anyelement + + + Expands an array into a set of rows. + The array's elements are read out in storage order. + + + unnest(ARRAY[1,2]) + + + 1 + 2 + + + + unnest(ARRAY[['foo','bar'],['baz','quux']]) + + + foo + bar + baz + quux + + + + + + + unnest ( anyarray, anyarray , ... ) + setof anyelement, anyelement [, ... ] + + + Expands multiple arrays (possibly of different data types) into a set of + rows. If the arrays are not all the same length then the shorter ones + are padded with NULLs. This form is only allowed + in a query's FROM clause; see . + + + SELECT * FROM unnest(ARRAY[1, 2], ARRAY['foo', 'bar', 'baz']) AS x(a, b) + + + a | b +---+----- + 1 | foo + 2 | bar + | baz + + + + + +
+ + + See also about the aggregate + function array_agg for use with arrays. + +
diff --git a/doc/src/sgml/func/func-binarystring.sgml b/doc/src/sgml/func/func-binarystring.sgml new file mode 100644 index 0000000000000..dc6b7e57ea754 --- /dev/null +++ b/doc/src/sgml/func/func-binarystring.sgml @@ -0,0 +1,908 @@ + + Binary String Functions and Operators + + + binary data + functions + + + + This section describes functions and operators for examining and + manipulating binary strings, that is values of type bytea. + Many of these are equivalent, in purpose and syntax, to the + text-string functions described in the previous section. + + + + SQL defines some string functions that use + key words, rather than commas, to separate + arguments. Details are in + . + PostgreSQL also provides versions of these functions + that use the regular function invocation syntax + (see ). + + + + <acronym>SQL</acronym> Binary String Functions and Operators + + + + + Function/Operator + + + Description + + + Example(s) + + + + + + + + + binary string + concatenation + + bytea || bytea + bytea + + + Concatenates the two binary strings. + + + '\x123456'::bytea || '\x789a00bcde'::bytea + \x123456789a00bcde + + + + + + + bit_length + + bit_length ( bytea ) + integer + + + Returns number of bits in the binary string (8 + times the octet_length). + + + bit_length('\x123456'::bytea) + 24 + + + + + + + btrim + + btrim ( bytes bytea, + bytesremoved bytea ) + bytea + + + Removes the longest string containing only bytes appearing in + bytesremoved from the start and end of + bytes. + + + btrim('\x1234567890'::bytea, '\x9012'::bytea) + \x345678 + + + + + + + ltrim + + ltrim ( bytes bytea, + bytesremoved bytea ) + bytea + + + Removes the longest string containing only bytes appearing in + bytesremoved from the start of + bytes. + + + ltrim('\x1234567890'::bytea, '\x9012'::bytea) + \x34567890 + + + + + + + octet_length + + octet_length ( bytea ) + integer + + + Returns number of bytes in the binary string. + + + octet_length('\x123456'::bytea) + 3 + + + + + + + overlay + + overlay ( bytes bytea PLACING newsubstring bytea FROM start integer FOR count integer ) + bytea + + + Replaces the substring of bytes that starts at + the start'th byte and extends + for count bytes + with newsubstring. + If count is omitted, it defaults to the length + of newsubstring. + + + overlay('\x1234567890'::bytea PLACING '\002\003'::bytea FROM 2 FOR 3) + \x12020390 + + + + + + + position + + position ( substring bytea IN bytes bytea ) + integer + + + Returns first starting index of the specified + substring within + bytes, or zero if it's not present. + + + position('\x5678'::bytea IN '\x1234567890'::bytea) + 3 + + + + + + + rtrim + + rtrim ( bytes bytea, + bytesremoved bytea ) + bytea + + + Removes the longest string containing only bytes appearing in + bytesremoved from the end of + bytes. + + + rtrim('\x1234567890'::bytea, '\x9012'::bytea) + \x12345678 + + + + + + + substring + + substring ( bytes bytea FROM start integer FOR count integer ) + bytea + + + Extracts the substring of bytes starting at + the start'th byte if that is specified, + and stopping after count bytes if that is + specified. Provide at least one of start + and count. + + + substring('\x1234567890'::bytea FROM 3 FOR 2) + \x5678 + + + + + + + trim + + trim ( LEADING | TRAILING | BOTH + bytesremoved bytea FROM + bytes bytea ) + bytea + + + Removes the longest string containing only bytes appearing in + bytesremoved from the start, + end, or both ends (BOTH is the default) + of bytes. + + + trim('\x9012'::bytea from '\x1234567890'::bytea) + \x345678 + + + + + + trim ( LEADING | TRAILING | BOTH FROM + bytes bytea, + bytesremoved bytea ) + bytea + + + This is a non-standard syntax for trim(). + + + trim(both from '\x1234567890'::bytea, '\x9012'::bytea) + \x345678 + + + + +
+ + + Additional binary string manipulation functions are available and + are listed in . Some + of them are used internally to implement the + SQL-standard string functions listed in . + + + + Other Binary String Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + bit_count + + + popcount + bit_count + + bit_count ( bytes bytea ) + bigint + + + Returns the number of bits set in the binary string (also known as + popcount). + + + bit_count('\x1234567890'::bytea) + 15 + + + + + + + crc32 + + crc32 ( bytea ) + bigint + + + Computes the CRC-32 value of the binary string. + + + crc32('abc'::bytea) + 891568578 + + + + + + + crc32c + + crc32c ( bytea ) + bigint + + + Computes the CRC-32C value of the binary string. + + + crc32c('abc'::bytea) + 910901175 + + + + + + + get_bit + + get_bit ( bytes bytea, + n bigint ) + integer + + + Extracts n'th bit + from binary string. + + + get_bit('\x1234567890'::bytea, 30) + 1 + + + + + + + get_byte + + get_byte ( bytes bytea, + n integer ) + integer + + + Extracts n'th byte + from binary string. + + + get_byte('\x1234567890'::bytea, 4) + 144 + + + + + + + length + + + binary string + length + + + length + of a binary string + binary strings, length + + length ( bytea ) + integer + + + Returns the number of bytes in the binary string. + + + length('\x1234567890'::bytea) + 5 + + + + + + length ( bytes bytea, + encoding name ) + integer + + + Returns the number of characters in the binary string, assuming + that it is text in the given encoding. + + + length('jose'::bytea, 'UTF8') + 4 + + + + + + + md5 + + md5 ( bytea ) + text + + + Computes the MD5 hash of + the binary string, with the result written in hexadecimal. + + + md5('Th\000omas'::bytea) + 8ab2d3c9689aaf18&zwsp;b4958c334c82d8b1 + + + + + + + reverse + + reverse ( bytea ) + bytea + + + Reverses the order of the bytes in the binary string. + + + reverse('\xabcd'::bytea) + \xcdab + + + + + + + set_bit + + set_bit ( bytes bytea, + n bigint, + newvalue integer ) + bytea + + + Sets n'th bit in + binary string to newvalue. + + + set_bit('\x1234567890'::bytea, 30, 0) + \x1234563890 + + + + + + + set_byte + + set_byte ( bytes bytea, + n integer, + newvalue integer ) + bytea + + + Sets n'th byte in + binary string to newvalue. + + + set_byte('\x1234567890'::bytea, 4, 64) + \x1234567840 + + + + + + + sha224 + + sha224 ( bytea ) + bytea + + + Computes the SHA-224 hash + of the binary string. + + + sha224('abc'::bytea) + \x23097d223405d8228642a477bda2&zwsp;55b32aadbce4bda0b3f7e36c9da7 + + + + + + + sha256 + + sha256 ( bytea ) + bytea + + + Computes the SHA-256 hash + of the binary string. + + + sha256('abc'::bytea) + \xba7816bf8f01cfea414140de5dae2223&zwsp;b00361a396177a9cb410ff61f20015ad + + + + + + + sha384 + + sha384 ( bytea ) + bytea + + + Computes the SHA-384 hash + of the binary string. + + + sha384('abc'::bytea) + \xcb00753f45a35e8bb5a03d699ac65007&zwsp;272c32ab0eded1631a8b605a43ff5bed&zwsp;8086072ba1e7cc2358baeca134c825a7 + + + + + + + sha512 + + sha512 ( bytea ) + bytea + + + Computes the SHA-512 hash + of the binary string. + + + sha512('abc'::bytea) + \xddaf35a193617abacc417349ae204131&zwsp;12e6fa4e89a97ea20a9eeee64b55d39a&zwsp;2192992a274fc1a836ba3c23a3feebbd&zwsp;454d4423643ce80e2a9ac94fa54ca49f + + + + + + + substr + + substr ( bytes bytea, start integer , count integer ) + bytea + + + Extracts the substring of bytes starting at + the start'th byte, + and extending for count bytes if that is + specified. (Same + as substring(bytes + from start + for count).) + + + substr('\x1234567890'::bytea, 3, 2) + \x5678 + + + + +
+ + + Functions get_byte and set_byte + number the first byte of a binary string as byte 0. + Functions get_bit and set_bit + number bits from the right within each byte; for example bit 0 is the least + significant bit of the first byte, and bit 15 is the most significant bit + of the second byte. + + + + For historical reasons, the function md5 + returns a hex-encoded value of type text whereas the SHA-2 + functions return type bytea. Use the functions + encode + and decode to + convert between the two. For example write encode(sha256('abc'), + 'hex') to get a hex-encoded text representation, + or decode(md5('abc'), 'hex') to get + a bytea value. + + + + + character string + converting to binary string + + + binary string + converting to character string + + Functions for converting strings between different character sets + (encodings), and for representing arbitrary binary data in textual + form, are shown in + . For these + functions, an argument or result of type text is expressed + in the database's default encoding, while arguments or results of + type bytea are in an encoding named by another argument. + + + + Text/Binary String Conversion Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + convert + + convert ( bytes bytea, + src_encoding name, + dest_encoding name ) + bytea + + + Converts a binary string representing text in + encoding src_encoding + to a binary string in encoding dest_encoding + (see for + available conversions). + + + convert('text_in_utf8', 'UTF8', 'LATIN1') + \x746578745f696e5f75746638 + + + + + + + convert_from + + convert_from ( bytes bytea, + src_encoding name ) + text + + + Converts a binary string representing text in + encoding src_encoding + to text in the database encoding + (see for + available conversions). + + + convert_from('text_in_utf8', 'UTF8') + text_in_utf8 + + + + + + + convert_to + + convert_to ( string text, + dest_encoding name ) + bytea + + + Converts a text string (in the database encoding) to a + binary string encoded in encoding dest_encoding + (see for + available conversions). + + + convert_to('some_text', 'UTF8') + \x736f6d655f74657874 + + + + + + + encode + + encode ( bytes bytea, + format text ) + text + + + Encodes binary data into a textual representation; supported + format values are: + base32hex, + base64, + base64url, + escape, + hex. + + + encode('123\000\001', 'base64') + MTIzAAE= + + + + + + + decode + + decode ( string text, + format text ) + bytea + + + Decodes binary data from a textual representation; supported + format values are the same as + for encode. + + + decode('MTIzAAE=', 'base64') + \x3132330001 + + + + +
+ + + The encode and decode + functions support the following textual formats: + + + + base32hex + + base32hex format + + + + The base32hex format is that of + + RFC 4648 Section 7. It uses the extended hex alphabet + (0-9 and + A-V) which preserves the sort order of + the encoded data when compared byte-wise. The encode function + produces output padded with '=', while decode + accepts both padded and unpadded input. Decoding is case-insensitive and ignores + whitespace characters. + + + This format is useful for encoding UUIDs in a compact, byte-wise sortable format: + rtrim(encode(uuid_value::bytea, 'base32hex'), '=') + produces a 26-character string compared to the standard 36-character + UUID representation. + + + + To maintain the lexicographical sort order of the encoded data, + ensure that the text is sorted using the C collation + (e.g., using COLLATE "C"). Natural language + collations may sort characters differently and break the ordering. + + + + + + + base64 + + base64 format + + + + The base64 format is that + of RFC + 2045 Section 6.8. As per the RFC, encoded lines are + broken at 76 characters. However instead of the MIME CRLF + end-of-line marker, only a newline is used for end-of-line. + The decode function ignores carriage-return, + newline, space, and tab characters. Otherwise, an error is + raised when decode is supplied invalid + base64 data — including when trailing padding is incorrect. + + + + + + base64url + + base64url format + + + + The base64url format is that of + + RFC 4648 Section 5, a base64 variant safe to + use in filenames and URLs. The base64url alphabet + uses '-' instead of '+' and + '_' instead of '/' and also omits + the '=' padding character. + + + + + + escape + + escape format + + + + The escape format converts zero bytes and + bytes with the high bit set into octal escape sequences + (\nnn), and it doubles + backslashes. Other byte values are represented literally. + The decode function will raise an error if a + backslash is not followed by either a second backslash or three + octal digits; it accepts other byte values unchanged. + + + + + + hex + + hex format + + + + The hex format represents each 4 bits of + data as one hexadecimal digit, 0 + through f, writing the higher-order digit of + each byte first. The encode function outputs + the a-f hex digits in lower + case. Because the smallest unit of data is 8 bits, there are + always an even number of characters returned + by encode. + The decode function + accepts the a-f characters in + either upper or lower case. An error is raised + when decode is given invalid hex data + — including when given an odd number of characters. + + + + + + + + In addition, it is possible to cast integral values to and from type + bytea. Casting an integer to bytea produces + 2, 4, or 8 bytes, depending on the width of the integer type. The result + is the two's complement representation of the integer, with the most + significant byte first. Some examples: + +1234::smallint::bytea \x04d2 +cast(1234 AS bytea) \x000004d2 +cast(-1234 AS bytea) \xfffffb2e +'\x8000'::bytea::smallint -32768 +'\x8000'::bytea::integer 32768 + + Casting a bytea to an integer will raise an error if the + length of the bytea exceeds the width of the integer type. + + + + See also the aggregate function string_agg in + and the large object functions + in . + +
diff --git a/doc/src/sgml/func/func-bitstring.sgml b/doc/src/sgml/func/func-bitstring.sgml new file mode 100644 index 0000000000000..3f59de464a44d --- /dev/null +++ b/doc/src/sgml/func/func-bitstring.sgml @@ -0,0 +1,358 @@ + + Bit String Functions and Operators + + + bit strings + functions + + + + This section describes functions and operators for examining and + manipulating bit strings, that is values of the types + bit and bit varying. (While only + type bit is mentioned in these tables, values of + type bit varying can be used interchangeably.) + Bit strings support the usual comparison operators shown in + , as well as the + operators shown in . + + + + Bit String Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + bit || bit + bit + + + Concatenation + + + B'10001' || B'011' + 10001011 + + + + + + bit & bit + bit + + + Bitwise AND (inputs must be of equal length) + + + B'10001' & B'01101' + 00001 + + + + + + bit | bit + bit + + + Bitwise OR (inputs must be of equal length) + + + B'10001' | B'01101' + 11101 + + + + + + bit # bit + bit + + + Bitwise exclusive OR (inputs must be of equal length) + + + B'10001' # B'01101' + 11100 + + + + + + ~ bit + bit + + + Bitwise NOT + + + ~ B'10001' + 01110 + + + + + + bit << integer + bit + + + Bitwise shift left + (string length is preserved) + + + B'10001' << 3 + 01000 + + + + + + bit >> integer + bit + + + Bitwise shift right + (string length is preserved) + + + B'10001' >> 2 + 00100 + + + + +
+ + + Some of the functions available for binary strings are also available + for bit strings, as shown in . + + + + Bit String Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + bit_count + + bit_count ( bit ) + bigint + + + Returns the number of bits set in the bit string (also known as + popcount). + + + bit_count(B'10111') + 4 + + + + + + + bit_length + + bit_length ( bit ) + integer + + + Returns number of bits in the bit string. + + + bit_length(B'10111') + 5 + + + + + + + length + + + bit string + length + + length ( bit ) + integer + + + Returns number of bits in the bit string. + + + length(B'10111') + 5 + + + + + + + octet_length + + octet_length ( bit ) + integer + + + Returns number of bytes in the bit string. + + + octet_length(B'1011111011') + 2 + + + + + + + overlay + + overlay ( bits bit PLACING newsubstring bit FROM start integer FOR count integer ) + bit + + + Replaces the substring of bits that starts at + the start'th bit and extends + for count bits + with newsubstring. + If count is omitted, it defaults to the length + of newsubstring. + + + overlay(B'01010101010101010' PLACING B'11111' FROM 2 FOR 3) + 0111110101010101010 + + + + + + + position + + position ( substring bit IN bits bit ) + integer + + + Returns first starting index of the specified substring + within bits, or zero if it's not present. + + + position(B'010' IN B'000001101011') + 8 + + + + + + + substring + + substring ( bits bit FROM start integer FOR count integer ) + bit + + + Extracts the substring of bits starting at + the start'th bit if that is specified, + and stopping after count bits if that is + specified. Provide at least one of start + and count. + + + substring(B'110010111111' FROM 3 FOR 2) + 00 + + + + + + + get_bit + + get_bit ( bits bit, + n integer ) + integer + + + Extracts n'th bit + from bit string; the first (leftmost) bit is bit 0. + + + get_bit(B'101010101010101010', 6) + 1 + + + + + + + set_bit + + set_bit ( bits bit, + n integer, + newvalue integer ) + bit + + + Sets n'th bit in + bit string to newvalue; + the first (leftmost) bit is bit 0. + + + set_bit(B'101010101010101010', 6, 0) + 101010001010101010 + + + + +
+ + + In addition, it is possible to cast integral values to and from type + bit. + Casting an integer to bit(n) copies the rightmost + n bits. Casting an integer to a bit string width wider + than the integer itself will sign-extend on the left. + Some examples: + +44::bit(10) 0000101100 +44::bit(3) 100 +cast(-44 AS bit(12)) 111111010100 +'1110'::bit(4)::integer 14 + + Note that casting to just bit means casting to + bit(1), and so will deliver only the least significant + bit of the integer. + +
diff --git a/doc/src/sgml/func/func-comparison.sgml b/doc/src/sgml/func/func-comparison.sgml new file mode 100644 index 0000000000000..ecb1d89463a1e --- /dev/null +++ b/doc/src/sgml/func/func-comparison.sgml @@ -0,0 +1,660 @@ + + Comparison Functions and Operators + + + comparison + operators + + + + The usual comparison operators are available, as shown in . + + + + Comparison Operators + + + + Operator + Description + + + + + + + datatype < datatype + boolean + + Less than + + + + + datatype > datatype + boolean + + Greater than + + + + + datatype <= datatype + boolean + + Less than or equal to + + + + + datatype >= datatype + boolean + + Greater than or equal to + + + + + datatype = datatype + boolean + + Equal + + + + + datatype <> datatype + boolean + + Not equal + + + + + datatype != datatype + boolean + + Not equal + + + +
+ + + + <> is the standard SQL notation for not + equal. != is an alias, which is converted + to <> at a very early stage of parsing. + Hence, it is not possible to implement != + and <> operators that do different things. + + + + + These comparison operators are available for all built-in data types + that have a natural ordering, including numeric, string, and date/time + types. In addition, arrays, composite types, and ranges can be compared + if their component data types are comparable. + + + + It is usually possible to compare values of related data + types as well; for example integer > + bigint will work. Some cases of this sort are implemented + directly by cross-type comparison operators, but if no + such operator is available, the parser will coerce the less-general type + to the more-general type and apply the latter's comparison operator. + + + + As shown above, all comparison operators are binary operators that + return values of type boolean. Thus, expressions like + 1 < 2 < 3 are not valid (because there is + no < operator to compare a Boolean value with + 3). Use the BETWEEN predicates + shown below to perform range tests. + + + + There are also some comparison predicates, as shown in . These behave much like + operators, but have special syntax mandated by the SQL standard. + + + + Comparison Predicates + + + + + Predicate + + + Description + + + Example(s) + + + + + + + + datatype BETWEEN datatype AND datatype + boolean + + + Between (inclusive of the range endpoints). + + + 2 BETWEEN 1 AND 3 + t + + + 2 BETWEEN 3 AND 1 + f + + + + + + datatype NOT BETWEEN datatype AND datatype + boolean + + + Not between (the negation of BETWEEN). + + + 2 NOT BETWEEN 1 AND 3 + f + + + + + + datatype BETWEEN SYMMETRIC datatype AND datatype + boolean + + + Between, after sorting the two endpoint values. + + + 2 BETWEEN SYMMETRIC 3 AND 1 + t + + + + + + datatype NOT BETWEEN SYMMETRIC datatype AND datatype + boolean + + + Not between, after sorting the two endpoint values. + + + 2 NOT BETWEEN SYMMETRIC 3 AND 1 + f + + + + + + datatype IS DISTINCT FROM datatype + boolean + + + Not equal, treating null as a comparable value. + + + 1 IS DISTINCT FROM NULL + t (rather than NULL) + + + NULL IS DISTINCT FROM NULL + f (rather than NULL) + + + + + + datatype IS NOT DISTINCT FROM datatype + boolean + + + Equal, treating null as a comparable value. + + + 1 IS NOT DISTINCT FROM NULL + f (rather than NULL) + + + NULL IS NOT DISTINCT FROM NULL + t (rather than NULL) + + + + + + datatype IS NULL + boolean + + + Test whether value is null. + + + 1.5 IS NULL + f + + + + + + datatype IS NOT NULL + boolean + + + Test whether value is not null. + + + 'null' IS NOT NULL + t + + + + + + datatype ISNULL + boolean + + + Test whether value is null (nonstandard syntax). + + + + + + datatype NOTNULL + boolean + + + Test whether value is not null (nonstandard syntax). + + + + + + boolean IS TRUE + boolean + + + Test whether boolean expression yields true. + + + true IS TRUE + t + + + NULL::boolean IS TRUE + f (rather than NULL) + + + + + + boolean IS NOT TRUE + boolean + + + Test whether boolean expression yields false or unknown. + + + true IS NOT TRUE + f + + + NULL::boolean IS NOT TRUE + t (rather than NULL) + + + + + + boolean IS FALSE + boolean + + + Test whether boolean expression yields false. + + + true IS FALSE + f + + + NULL::boolean IS FALSE + f (rather than NULL) + + + + + + boolean IS NOT FALSE + boolean + + + Test whether boolean expression yields true or unknown. + + + true IS NOT FALSE + t + + + NULL::boolean IS NOT FALSE + t (rather than NULL) + + + + + + boolean IS UNKNOWN + boolean + + + Test whether boolean expression yields unknown. + + + true IS UNKNOWN + f + + + NULL::boolean IS UNKNOWN + t (rather than NULL) + + + + + + boolean IS NOT UNKNOWN + boolean + + + Test whether boolean expression yields true or false. + + + true IS NOT UNKNOWN + t + + + NULL::boolean IS NOT UNKNOWN + f (rather than NULL) + + + + +
+ + + + BETWEEN + + + BETWEEN SYMMETRIC + + The BETWEEN predicate simplifies range tests: + +a BETWEEN x AND y + + is equivalent to + +a >= x AND a <= y + + Notice that BETWEEN treats the endpoint values as included + in the range. + BETWEEN SYMMETRIC is like BETWEEN + except there is no requirement that the argument to the left of + AND be less than or equal to the argument on the right. + If it is not, those two arguments are automatically swapped, so that + a nonempty range is always implied. + + + + The various variants of BETWEEN are implemented in + terms of the ordinary comparison operators, and therefore will work for + any data type(s) that can be compared. + + + + + The use of AND in the BETWEEN + syntax creates an ambiguity with the use of AND as a + logical operator. To resolve this, only a limited set of expression + types are allowed as the second argument of a BETWEEN + clause. If you need to write a more complex sub-expression + in BETWEEN, write parentheses around the + sub-expression. + + + + + + IS DISTINCT FROM + + + IS NOT DISTINCT FROM + + Ordinary comparison operators yield null (signifying unknown), + not true or false, when either input is null. For example, + 7 = NULL yields null, as does 7 <> NULL. When + this behavior is not suitable, use the + IS NOT DISTINCT FROM predicates: + +a IS DISTINCT FROM b +a IS NOT DISTINCT FROM b + + For non-null inputs, IS DISTINCT FROM is + the same as the <> operator. However, if both + inputs are null it returns false, and if only one input is + null it returns true. Similarly, IS NOT DISTINCT + FROM is identical to = for non-null + inputs, but it returns true when both inputs are null, and false when only + one input is null. Thus, these predicates effectively act as though null + were a normal data value, rather than unknown. + + + + + IS NULL + + + IS NOT NULL + + + ISNULL + + + NOTNULL + + To check whether a value is or is not null, use the predicates: + +expression IS NULL +expression IS NOT NULL + + or the equivalent, but nonstandard, predicates: + +expression ISNULL +expression NOTNULL + + null valuecomparing + + + + Do not write + expression = NULL + because NULL is not equal to + NULL. (The null value represents an unknown value, + and it is not known whether two unknown values are equal.) + + + + + Some applications might expect that + expression = NULL + returns true if expression evaluates to + the null value. It is highly recommended that these applications + be modified to comply with the SQL standard. However, if that + cannot be done the + configuration variable is available. If it is enabled, + PostgreSQL will convert x = + NULL clauses to x IS NULL. + + + + + If the expression is row-valued, then + IS NULL is true when the row expression itself is null + or when all the row's fields are null, while + IS NOT NULL is true when the row expression itself is non-null + and all the row's fields are non-null. Because of this behavior, + IS NULL and IS NOT NULL do not always return + inverse results for row-valued expressions; in particular, a row-valued + expression that contains both null and non-null fields will return false + for both tests. For example: + + +SELECT ROW(1,2.5,'this is a test') = ROW(1, 3, 'not the same'); + +SELECT ROW(table.*) IS NULL FROM table; -- detect all-null rows + +SELECT ROW(table.*) IS NOT NULL FROM table; -- detect all-non-null rows + +SELECT NOT(ROW(table.*) IS NOT NULL) FROM TABLE; -- detect at least one null in rows + + + In some cases, it may be preferable to + write row IS DISTINCT FROM NULL + or row IS NOT DISTINCT FROM NULL, + which will simply check whether the overall row value is null without any + additional tests on the row fields. + + + + + IS TRUE + + + IS NOT TRUE + + + IS FALSE + + + IS NOT FALSE + + + IS UNKNOWN + + + IS NOT UNKNOWN + + Boolean values can also be tested using the predicates + +boolean_expression IS TRUE +boolean_expression IS NOT TRUE +boolean_expression IS FALSE +boolean_expression IS NOT FALSE +boolean_expression IS UNKNOWN +boolean_expression IS NOT UNKNOWN + + These will always return true or false, never a null value, even when the + operand is null. + A null input is treated as the logical value unknown. + Notice that IS UNKNOWN and IS NOT UNKNOWN are + effectively the same as IS NULL and + IS NOT NULL, respectively, except that the input + expression must be of Boolean type. + + + + Some comparison-related functions are also available, as shown in . + + + + Comparison Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + error_on_null + + error_on_null ( anyelement ) + anyelement + + + Checks if the input is the null value, generating an error if so; + otherwise, returns the input. + + + error_on_null(42) + 42 + + + error_on_null(row(null,null)) + (,) + + + + + + + num_nonnulls + + num_nonnulls ( VARIADIC "any" ) + integer + + + Returns the number of non-null arguments. + + + num_nonnulls(1, NULL, 2) + 2 + + + + + + num_nulls + + num_nulls ( VARIADIC "any" ) + integer + + + Returns the number of null arguments. + + + num_nulls(1, NULL, 2) + 1 + + + + +
+ +
diff --git a/doc/src/sgml/func/func-comparisons.sgml b/doc/src/sgml/func/func-comparisons.sgml new file mode 100644 index 0000000000000..6a6e0bd401920 --- /dev/null +++ b/doc/src/sgml/func/func-comparisons.sgml @@ -0,0 +1,336 @@ + + Row and Array Comparisons + + + IN + + + + NOT IN + + + + ANY + + + + ALL + + + + SOME + + + + composite type + comparison + + + + row-wise comparison + + + + comparison + composite type + + + + comparison + row constructor + + + + IS DISTINCT FROM + + + + IS NOT DISTINCT FROM + + + + This section describes several specialized constructs for making + multiple comparisons between groups of values. These forms are + syntactically related to the subquery forms of the previous section, + but do not involve subqueries. + The forms involving array subexpressions are + PostgreSQL extensions; the rest are + SQL-compliant. + All of the expression forms documented in this section return + Boolean (true/false) results. + + + + <literal>IN</literal> + + +expression IN (value , ...) + + + + The right-hand side is a parenthesized list + of expressions. The result is true if the left-hand expression's + result is equal to any of the right-hand expressions. This is a shorthand + notation for + + +expression = value1 +OR +expression = value2 +OR +... + + + + + Note that if the left-hand expression yields null, or if there are + no equal right-hand values and at least one right-hand expression yields + null, the result of the IN construct will be null, not false. + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + + <literal>NOT IN</literal> + + +expression NOT IN (value , ...) + + + + The right-hand side is a parenthesized list + of expressions. The result is true if the left-hand expression's + result is unequal to all of the right-hand expressions. This is a shorthand + notation for + + +expression <> value1 +AND +expression <> value2 +AND +... + + + + + Note that if the left-hand expression yields null, or if there are + no equal right-hand values and at least one right-hand expression yields + null, the result of the NOT IN construct will be null, not true + as one might naively expect. + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + + x NOT IN y is equivalent to NOT (x IN y) in all + cases. However, null values are much more likely to trip up the novice when + working with NOT IN than when working with IN. + It is best to express your condition positively if possible. + + + + + + <literal>ANY</literal>/<literal>SOME</literal> (array) + + +expression operator ANY (array expression) +expression operator SOME (array expression) + + + + The right-hand side is a parenthesized expression, which must yield an + array value. + The left-hand expression + is evaluated and compared to each element of the array using the + given operator, which must yield a Boolean + result. + The result of ANY is true if any true result is obtained. + The result is false if no true result is found (including the + case where the array has zero elements). + + + + If the array expression yields a null array, the result of + ANY will be null. If the left-hand expression yields null, + the result of ANY is ordinarily null (though a non-strict + comparison operator could possibly yield a different result). + Also, if the right-hand array contains any null elements and no true + comparison result is obtained, the result of ANY + will be null, not false (again, assuming a strict comparison operator). + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + SOME is a synonym for ANY. + + + + + <literal>ALL</literal> (array) + + +expression operator ALL (array expression) + + + + The right-hand side is a parenthesized expression, which must yield an + array value. + The left-hand expression + is evaluated and compared to each element of the array using the + given operator, which must yield a Boolean + result. + The result of ALL is true if all comparisons yield true + (including the case where the array has zero elements). + The result is false if any false result is found. + + + + If the array expression yields a null array, the result of + ALL will be null. If the left-hand expression yields null, + the result of ALL is ordinarily null (though a non-strict + comparison operator could possibly yield a different result). + Also, if the right-hand array contains any null elements and no false + comparison result is obtained, the result of ALL + will be null, not true (again, assuming a strict comparison operator). + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + + Row Constructor Comparison + + +row_constructor operator row_constructor + + + + Each side is a row constructor, + as described in . + The two row constructors must have the same number of fields. + The given operator is applied to each pair + of corresponding fields. (Since the fields could be of different + types, this means that a different specific operator could be selected + for each pair.) + All the selected operators must be members of some B-tree operator + class, or be the negator of an = member of a B-tree + operator class, meaning that row constructor comparison is only + possible when the operator is + =, + <>, + <, + <=, + >, or + >=, + or has semantics similar to one of these. + + + + The = and <> cases work slightly differently + from the others. Two rows are considered + equal if all their corresponding members are non-null and equal; the rows + are unequal if any corresponding members are non-null and unequal; + otherwise the result of the row comparison is unknown (null). + + + + For the <, <=, > and + >= cases, the row elements are compared left-to-right, + stopping as soon as an unequal or null pair of elements is found. + If either of this pair of elements is null, the result of the + row comparison is unknown (null); otherwise comparison of this pair + of elements determines the result. For example, + ROW(1,2,NULL) < ROW(1,3,0) + yields true, not null, because the third pair of elements are not + considered. + + + +row_constructor IS DISTINCT FROM row_constructor + + + + This construct is similar to a <> row comparison, + but it does not yield null for null inputs. Instead, any null value is + considered unequal to (distinct from) any non-null value, and any two + nulls are considered equal (not distinct). Thus the result will + either be true or false, never null. + + + +row_constructor IS NOT DISTINCT FROM row_constructor + + + + This construct is similar to a = row comparison, + but it does not yield null for null inputs. Instead, any null value is + considered unequal to (distinct from) any non-null value, and any two + nulls are considered equal (not distinct). Thus the result will always + be either true or false, never null. + + + + + + Composite Type Comparison + + +record operator record + + + + The SQL specification requires row-wise comparison to return NULL if the + result depends on comparing two NULL values or a NULL and a non-NULL. + PostgreSQL does this only when comparing the + results of two row constructors (as in + ) or comparing a row constructor + to the output of a subquery (as in ). + In other contexts where two composite-type values are compared, two + NULL field values are considered equal, and a NULL is considered larger + than a non-NULL. This is necessary in order to have consistent sorting + and indexing behavior for composite types. + + + + Each side is evaluated and they are compared row-wise. Composite type + comparisons are allowed when the operator is + =, + <>, + <, + <=, + > or + >=, + or has semantics similar to one of these. (To be specific, an operator + can be a row comparison operator if it is a member of a B-tree operator + class, or is the negator of the = member of a B-tree operator + class.) The default behavior of the above operators is the same as for + IS [ NOT ] DISTINCT FROM for row constructors (see + ). + + + + To support matching of rows which include elements without a default + B-tree operator class, the following operators are defined for composite + type comparison: + *=, + *<>, + *<, + *<=, + *>, and + *>=. + These operators compare the internal binary representation of the two + rows. Two rows might have a different binary representation even + though comparisons of the two rows with the equality operator is true. + The ordering of rows under these comparison operators is deterministic + but not otherwise meaningful. These operators are used internally + for materialized views and might be useful for other specialized + purposes such as replication and B-Tree deduplication (see ). They are not intended to be + generally useful for writing queries, though. + + + diff --git a/doc/src/sgml/func/func-conditional.sgml b/doc/src/sgml/func/func-conditional.sgml new file mode 100644 index 0000000000000..7ca53dbf1ab03 --- /dev/null +++ b/doc/src/sgml/func/func-conditional.sgml @@ -0,0 +1,283 @@ + + Conditional Expressions + + + CASE + + + + conditional expression + + + + This section describes the SQL-compliant conditional expressions + available in PostgreSQL. + + + + + If your needs go beyond the capabilities of these conditional + expressions, you might want to consider writing a server-side function + in a more expressive programming language. + + + + + + Although COALESCE, GREATEST, and + LEAST are syntactically similar to functions, they are + not ordinary functions, and thus cannot be used with explicit + VARIADIC array arguments. + + + + + <literal>CASE</literal> + + + The SQL CASE expression is a + generic conditional expression, similar to if/else statements in + other programming languages: + + +CASE WHEN condition THEN result + WHEN ... + ELSE result +END + + + CASE clauses can be used wherever + an expression is valid. Each condition is an + expression that returns a boolean result. If the condition's + result is true, the value of the CASE expression is the + result that follows the condition, and the + remainder of the CASE expression is not processed. If the + condition's result is not true, any subsequent WHEN clauses + are examined in the same manner. If no WHEN + condition yields true, the value of the + CASE expression is the result of the + ELSE clause. If the ELSE clause is + omitted and no condition is true, the result is null. + + + + An example: + +SELECT * FROM test; + + a +--- + 1 + 2 + 3 + + +SELECT a, + CASE WHEN a=1 THEN 'one' + WHEN a=2 THEN 'two' + ELSE 'other' + END + FROM test; + + a | case +---+------- + 1 | one + 2 | two + 3 | other + + + + + The data types of all the result + expressions must be convertible to a single output type. + See for more details. + + + + There is a simple form of CASE expression + that is a variant of the general form above: + + +CASE expression + WHEN value THEN result + WHEN ... + ELSE result +END + + + The first + expression is computed, then compared to + each of the value expressions in the + WHEN clauses until one is found that is equal to it. If + no match is found, the result of the + ELSE clause (or a null value) is returned. This is similar + to the switch statement in C. + + + + The example above can be written using the simple + CASE syntax: + +SELECT a, + CASE a WHEN 1 THEN 'one' + WHEN 2 THEN 'two' + ELSE 'other' + END + FROM test; + + a | case +---+------- + 1 | one + 2 | two + 3 | other + + + + + A CASE expression does not evaluate any subexpressions + that are not needed to determine the result. For example, this is a + possible way of avoiding a division-by-zero failure: + +SELECT ... WHERE CASE WHEN x <> 0 THEN y/x > 1.5 ELSE false END; + + + + + + As described in , there are various + situations in which subexpressions of an expression are evaluated at + different times, so that the principle that CASE + evaluates only necessary subexpressions is not ironclad. For + example a constant 1/0 subexpression will usually result in + a division-by-zero failure at planning time, even if it's within + a CASE arm that would never be entered at run time. + + + + + + <literal>COALESCE</literal> + + + COALESCE + + + + NVL + + + + IFNULL + + + +COALESCE(value , ...) + + + + The COALESCE function returns the first of its + arguments that is not null. Null is returned only if all arguments + are null. It is often used to substitute a default value for + null values when data is retrieved for display, for example: + +SELECT COALESCE(description, short_description, '(none)') ... + + This returns description if it is not null, otherwise + short_description if it is not null, otherwise (none). + + + + The arguments must all be convertible to a common data type, which + will be the type of the result (see + for details). + + + + Like a CASE expression, COALESCE only + evaluates the arguments that are needed to determine the result; + that is, arguments to the right of the first non-null argument are + not evaluated. This SQL-standard function provides capabilities similar + to NVL and IFNULL, which are used in some other + database systems. + + + + + <literal>NULLIF</literal> + + + NULLIF + + + +NULLIF(value1, value2) + + + + The NULLIF function returns a null value if + value1 equals value2; + otherwise it returns value1. + This can be used to perform the inverse operation of the + COALESCE example given above: + +SELECT NULLIF(value, '(none)') ... + + In this example, if value is (none), + null is returned, otherwise the value of value + is returned. + + + + The two arguments must be of comparable types. + To be specific, they are compared exactly as if you had + written value1 + = value2, so there must be a + suitable = operator available. + + + + The result has the same type as the first argument — but there is + a subtlety. What is actually returned is the first argument of the + implied = operator, and in some cases that will have + been promoted to match the second argument's type. For + example, NULLIF(1, 2.2) yields numeric, + because there is no integer = + numeric operator, + only numeric = numeric. + + + + + + <literal>GREATEST</literal> and <literal>LEAST</literal> + + + GREATEST + + + LEAST + + + +GREATEST(value , ...) + + +LEAST(value , ...) + + + + The GREATEST and LEAST functions select the + largest or smallest value from a list of any number of expressions. + The expressions must all be convertible to a common data type, which + will be the type of the result + (see for details). + + + + NULL values in the argument list are ignored. The result will be NULL + only if all the expressions evaluate to NULL. (This is a deviation from + the SQL standard. According to the standard, the return value is NULL if + any argument is NULL. Some other databases behave this way.) + + + diff --git a/doc/src/sgml/func/func-datetime.sgml b/doc/src/sgml/func/func-datetime.sgml new file mode 100644 index 0000000000000..39dddde4fe126 --- /dev/null +++ b/doc/src/sgml/func/func-datetime.sgml @@ -0,0 +1,2236 @@ + + Date/Time Functions and Operators + + + shows the available + functions for date/time value processing, with details appearing in + the following subsections. illustrates the behaviors of + the basic arithmetic operators (+, + *, etc.). For formatting functions, refer to + . You should be familiar with + the background information on date/time data types from . + + + + In addition, the usual comparison operators shown in + are available for the + date/time types. Dates and timestamps (with or without time zone) are + all comparable, while times (with or without time zone) and intervals + can only be compared to other values of the same data type. When + comparing a timestamp without time zone to a timestamp with time zone, + the former value is assumed to be given in the time zone specified by + the configuration parameter, and is + rotated to UTC for comparison to the latter value (which is already + in UTC internally). Similarly, a date value is assumed to represent + midnight in the TimeZone zone when comparing it + to a timestamp. + + + + All the functions and operators described below that take time or timestamp + inputs actually come in two variants: one that takes time with time zone or timestamp + with time zone, and one that takes time without time zone or timestamp without time zone. + For brevity, these variants are not shown separately. Also, the + + and * operators come in commutative pairs (for + example both date + integer + and integer + date); we show + only one of each such pair. + + + + Date/Time Operators + + + + + + Operator + + + Description + + + Example(s) + + + + + + + + date + integer + date + + + Add a number of days to a date + + + date '2001-09-28' + 7 + 2001-10-05 + + + + + + date + interval + timestamp + + + Add an interval to a date + + + date '2001-09-28' + interval '1 hour' + 2001-09-28 01:00:00 + + + + + + date + time + timestamp + + + Add a time-of-day to a date + + + date '2001-09-28' + time '03:00' + 2001-09-28 03:00:00 + + + + + + interval + interval + interval + + + Add intervals + + + interval '1 day' + interval '1 hour' + 1 day 01:00:00 + + + + + + timestamp + interval + timestamp + + + Add an interval to a timestamp + + + timestamp '2001-09-28 01:00' + interval '23 hours' + 2001-09-29 00:00:00 + + + + + + time + interval + time + + + Add an interval to a time + + + time '01:00' + interval '3 hours' + 04:00:00 + + + + + + - interval + interval + + + Negate an interval + + + - interval '23 hours' + -23:00:00 + + + + + + date - date + integer + + + Subtract dates, producing the number of days elapsed + + + date '2001-10-01' - date '2001-09-28' + 3 + + + + + + date - integer + date + + + Subtract a number of days from a date + + + date '2001-10-01' - 7 + 2001-09-24 + + + + + + date - interval + timestamp + + + Subtract an interval from a date + + + date '2001-09-28' - interval '1 hour' + 2001-09-27 23:00:00 + + + + + + time - time + interval + + + Subtract times + + + time '05:00' - time '03:00' + 02:00:00 + + + + + + time - interval + time + + + Subtract an interval from a time + + + time '05:00' - interval '2 hours' + 03:00:00 + + + + + + timestamp - interval + timestamp + + + Subtract an interval from a timestamp + + + timestamp '2001-09-28 23:00' - interval '23 hours' + 2001-09-28 00:00:00 + + + + + + interval - interval + interval + + + Subtract intervals + + + interval '1 day' - interval '1 hour' + 1 day -01:00:00 + + + + + + timestamp - timestamp + interval + + + Subtract timestamps (converting 24-hour intervals into days, + similarly to justify_hours()) + + + timestamp '2001-09-29 03:00' - timestamp '2001-07-27 12:00' + 63 days 15:00:00 + + + + + + interval * double precision + interval + + + Multiply an interval by a scalar + + + interval '1 second' * 900 + 00:15:00 + + + interval '1 day' * 21 + 21 days + + + interval '1 hour' * 3.5 + 03:30:00 + + + + + + interval / double precision + interval + + + Divide an interval by a scalar + + + interval '1 hour' / 1.5 + 00:40:00 + + + + +
+ + + Date/Time Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + age + + age ( timestamp, timestamp ) + interval + + + Subtract arguments, producing a symbolic result that + uses years and months, rather than just days + + + age(timestamp '2001-04-10', timestamp '1957-06-13') + 43 years 9 mons 27 days + + + + + + age ( timestamp ) + interval + + + Subtract argument from current_date (at midnight) + + + age(timestamp '1957-06-13') + 62 years 6 mons 10 days + + + + + + + clock_timestamp + + clock_timestamp ( ) + timestamp with time zone + + + Current date and time (changes during statement execution); + see + + + clock_timestamp() + 2019-12-23 14:39:53.662522-05 + + + + + + + current_date + + current_date + date + + + Current date; see + + + current_date + 2019-12-23 + + + + + + + current_time + + current_time + time with time zone + + + Current time of day; see + + + current_time + 14:39:53.662522-05 + + + + + + current_time ( integer ) + time with time zone + + + Current time of day, with limited precision; + see + + + current_time(2) + 14:39:53.66-05 + + + + + + + current_timestamp + + current_timestamp + timestamp with time zone + + + Current date and time (start of current transaction); + see + + + current_timestamp + 2019-12-23 14:39:53.662522-05 + + + + + + current_timestamp ( integer ) + timestamp with time zone + + + Current date and time (start of current transaction), with limited precision; + see + + + current_timestamp(0) + 2019-12-23 14:39:53-05 + + + + + + + date_add + + date_add ( timestamp with time zone, interval , text ) + timestamp with time zone + + + Add an interval to a timestamp with time + zone, computing times of day and daylight-savings adjustments + according to the time zone named by the third argument, or the + current setting if that is omitted. + The form with two arguments is equivalent to the timestamp with + time zone + interval operator. + + + date_add('2021-10-31 00:00:00+02'::timestamptz, '1 day'::interval, 'Europe/Warsaw') + 2021-10-31 23:00:00+00 + + + + + + date_bin ( interval, timestamp, timestamp ) + timestamp + + + Bin input into specified interval aligned with specified origin; see + + + date_bin('15 minutes', timestamp '2001-02-16 20:38:40', timestamp '2001-02-16 20:05:00') + 2001-02-16 20:35:00 + + + + + + + date_part + + date_part ( text, timestamp ) + double precision + + + Get timestamp subfield (equivalent to extract); + see + + + date_part('hour', timestamp '2001-02-16 20:38:40') + 20 + + + + + + date_part ( text, interval ) + double precision + + + Get interval subfield (equivalent to extract); + see + + + date_part('month', interval '2 years 3 months') + 3 + + + + + + + date_subtract + + date_subtract ( timestamp with time zone, interval , text ) + timestamp with time zone + + + Subtract an interval from a timestamp with time + zone, computing times of day and daylight-savings adjustments + according to the time zone named by the third argument, or the + current setting if that is omitted. + The form with two arguments is equivalent to the timestamp with + time zone - interval operator. + + + date_subtract('2021-11-01 00:00:00+01'::timestamptz, '1 day'::interval, 'Europe/Warsaw') + 2021-10-30 22:00:00+00 + + + + + + + date_trunc + + date_trunc ( text, timestamp ) + timestamp + + + Truncate to specified precision; see + + + date_trunc('hour', timestamp '2001-02-16 20:38:40') + 2001-02-16 20:00:00 + + + + + + date_trunc ( text, timestamp with time zone, text ) + timestamp with time zone + + + Truncate to specified precision in the specified time zone; see + + + + date_trunc('day', timestamptz '2001-02-16 20:38:40+00', 'Australia/Sydney') + 2001-02-16 13:00:00+00 + + + + + + date_trunc ( text, interval ) + interval + + + Truncate to specified precision; see + + + + date_trunc('hour', interval '2 days 3 hours 40 minutes') + 2 days 03:00:00 + + + + + + + extract + + extract ( field FROM timestamp ) + numeric + + + Get timestamp subfield; see + + + extract(hour FROM timestamp '2001-02-16 20:38:40') + 20 + + + + + + extract ( field FROM interval ) + numeric + + + Get interval subfield; see + + + extract(month FROM interval '2 years 3 months') + 3 + + + + + + + isfinite + + isfinite ( date ) + boolean + + + Test for finite date (not +/-infinity) + + + isfinite(date '2001-02-16') + true + + + + + + isfinite ( timestamp ) + boolean + + + Test for finite timestamp (not +/-infinity) + + + isfinite(timestamp 'infinity') + false + + + + + + isfinite ( interval ) + boolean + + + Test for finite interval (not +/-infinity) + + + isfinite(interval '4 hours') + true + + + + + + + justify_days + + justify_days ( interval ) + interval + + + Adjust interval, converting 30-day time periods to months + + + justify_days(interval '1 year 65 days') + 1 year 2 mons 5 days + + + + + + + justify_hours + + justify_hours ( interval ) + interval + + + Adjust interval, converting 24-hour time periods to days + + + justify_hours(interval '50 hours 10 minutes') + 2 days 02:10:00 + + + + + + + justify_interval + + justify_interval ( interval ) + interval + + + Adjust interval using justify_days + and justify_hours, with additional sign + adjustments + + + justify_interval(interval '1 mon -1 hour') + 29 days 23:00:00 + + + + + + + localtime + + localtime + time + + + Current time of day; + see + + + localtime + 14:39:53.662522 + + + + + + localtime ( integer ) + time + + + Current time of day, with limited precision; + see + + + localtime(0) + 14:39:53 + + + + + + + localtimestamp + + localtimestamp + timestamp + + + Current date and time (start of current transaction); + see + + + localtimestamp + 2019-12-23 14:39:53.662522 + + + + + + localtimestamp ( integer ) + timestamp + + + Current date and time (start of current + transaction), with limited precision; + see + + + localtimestamp(2) + 2019-12-23 14:39:53.66 + + + + + + + make_date + + make_date ( year int, + month int, + day int ) + date + + + Create date from year, month and day fields + (negative years signify BC) + + + make_date(2013, 7, 15) + 2013-07-15 + + + + + + make_interval + + make_interval ( years int + , months int + , weeks int + , days int + , hours int + , mins int + , secs double precision + ) + interval + + + Create interval from years, months, weeks, days, hours, minutes and + seconds fields, each of which can default to zero + + + make_interval(days => 10) + 10 days + + + + + + + make_time + + make_time ( hour int, + min int, + sec double precision ) + time + + + Create time from hour, minute and seconds fields + + + make_time(8, 15, 23.5) + 08:15:23.5 + + + + + + + make_timestamp + + make_timestamp ( year int, + month int, + day int, + hour int, + min int, + sec double precision ) + timestamp + + + Create timestamp from year, month, day, hour, minute and seconds fields + (negative years signify BC) + + + make_timestamp(2013, 7, 15, 8, 15, 23.5) + 2013-07-15 08:15:23.5 + + + + + + + make_timestamptz + + make_timestamptz ( year int, + month int, + day int, + hour int, + min int, + sec double precision + , timezone text ) + timestamp with time zone + + + Create timestamp with time zone from year, month, day, hour, minute + and seconds fields (negative years signify BC). + If timezone is not + specified, the current time zone is used; the examples assume the + session time zone is Europe/London + + + make_timestamptz(2013, 7, 15, 8, 15, 23.5) + 2013-07-15 08:15:23.5+01 + + + make_timestamptz(2013, 7, 15, 8, 15, 23.5, 'America/New_York') + 2013-07-15 13:15:23.5+01 + + + + + + + now + + now ( ) + timestamp with time zone + + + Current date and time (start of current transaction); + see + + + now() + 2019-12-23 14:39:53.662522-05 + + + + + + + random + + random ( min date, max date ) + date + + + random ( min timestamp, max timestamp ) + timestamp + + + random ( min timestamptz, max timestamptz ) + timestamptz + + + Returns a random value in the range + min <= x <= max. + + + Note that these functions use the same pseudo-random number generator + as the functions listed in , + and respond in the same way to calling + setseed(). + + + random('1979-02-08'::date,'2025-07-03'::date) + 1983-04-21 + + + random('2000-01-01'::timestamptz, now()) + 2015-09-27 09:11:33.732707+00 + + + + + + + statement_timestamp + + statement_timestamp ( ) + timestamp with time zone + + + Current date and time (start of current statement); + see + + + statement_timestamp() + 2019-12-23 14:39:53.662522-05 + + + + + + + timeofday + + timeofday ( ) + text + + + Current date and time + (like clock_timestamp, but as a text string); + see + + + timeofday() + Mon Dec 23 14:39:53.662522 2019 EST + + + + + + + transaction_timestamp + + transaction_timestamp ( ) + timestamp with time zone + + + Current date and time (start of current transaction); + see + + + transaction_timestamp() + 2019-12-23 14:39:53.662522-05 + + + + + + + to_timestamp + + to_timestamp ( double precision ) + timestamp with time zone + + + Convert Unix epoch (seconds since 1970-01-01 00:00:00+00) to + timestamp with time zone + + + to_timestamp(1284352323) + 2010-09-13 04:32:03+00 + + + + +
+ + + + OVERLAPS + + In addition to these functions, the SQL OVERLAPS operator is + supported: + +(start1, end1) OVERLAPS (start2, end2) +(start1, length1) OVERLAPS (start2, length2) + + This expression yields true when two time periods (defined by their + endpoints) overlap, false when they do not overlap. The endpoints + can be specified as pairs of dates, times, or time stamps; or as + a date, time, or time stamp followed by an interval. When a pair + of values is provided, either the start or the end can be written + first; OVERLAPS automatically takes the earlier value + of the pair as the start. Each time period is considered to + represent the half-open interval start <= + time < end, unless + start and end are equal in which case it + represents that single time instant. This means for instance that two + time periods with only an endpoint in common do not overlap. + + + +SELECT (DATE '2001-02-16', DATE '2001-12-21') OVERLAPS + (DATE '2001-10-30', DATE '2002-10-30'); +Result: true +SELECT (DATE '2001-02-16', INTERVAL '100 days') OVERLAPS + (DATE '2001-10-30', DATE '2002-10-30'); +Result: false +SELECT (DATE '2001-10-29', DATE '2001-10-30') OVERLAPS + (DATE '2001-10-30', DATE '2001-10-31'); +Result: false +SELECT (DATE '2001-10-30', DATE '2001-10-30') OVERLAPS + (DATE '2001-10-30', DATE '2001-10-31'); +Result: true + + + + When adding an interval value to (or subtracting an + interval value from) a timestamp + or timestamp with time zone value, the months, days, and + microseconds fields of the interval value are handled in turn. + First, a nonzero months field advances or decrements the date of the + timestamp by the indicated number of months, keeping the day of month the + same unless it would be past the end of the new month, in which case the + last day of that month is used. (For example, March 31 plus 1 month + becomes April 30, but March 31 plus 2 months becomes May 31.) + Then the days field advances or decrements the date of the timestamp by + the indicated number of days. In both these steps the local time of day + is kept the same. Finally, if there is a nonzero microseconds field, it + is added or subtracted literally. + When doing arithmetic on a timestamp with time zone value in + a time zone that recognizes DST, this means that adding or subtracting + (say) interval '1 day' does not necessarily have the + same result as adding or subtracting interval '24 + hours'. + For example, with the session time zone set + to America/Denver: + +SELECT timestamp with time zone '2005-04-02 12:00:00-07' + interval '1 day'; +Result: 2005-04-03 12:00:00-06 +SELECT timestamp with time zone '2005-04-02 12:00:00-07' + interval '24 hours'; +Result: 2005-04-03 13:00:00-06 + + This happens because an hour was skipped due to a change in daylight saving + time at 2005-04-03 02:00:00 in time zone + America/Denver. + + + + Note there can be ambiguity in the months field returned by + age because different months have different numbers of + days. PostgreSQL's approach uses the month from the + earlier of the two dates when calculating partial months. For example, + age('2004-06-01', '2004-04-30') uses April to yield + 1 mon 1 day, while using May would yield 1 mon 2 + days because May has 31 days, while April has only 30. + + + + Subtraction of dates and timestamps can also be complex. One conceptually + simple way to perform subtraction is to convert each value to a number + of seconds using EXTRACT(EPOCH FROM ...), then subtract the + results; this produces the + number of seconds between the two values. This will adjust + for the number of days in each month, timezone changes, and daylight + saving time adjustments. Subtraction of date or timestamp + values with the - operator + returns the number of days (24-hours) and hours/minutes/seconds + between the values, making the same adjustments. The age + function returns years, months, days, and hours/minutes/seconds, + performing field-by-field subtraction and then adjusting for negative + field values. The following queries illustrate the differences in these + approaches. The sample results were produced with timezone + = 'US/Eastern'; there is a daylight saving time change between the + two dates used: + + + +SELECT EXTRACT(EPOCH FROM timestamptz '2013-07-01 12:00:00') - + EXTRACT(EPOCH FROM timestamptz '2013-03-01 12:00:00'); +Result: 10537200.000000 +SELECT (EXTRACT(EPOCH FROM timestamptz '2013-07-01 12:00:00') - + EXTRACT(EPOCH FROM timestamptz '2013-03-01 12:00:00')) + / 60 / 60 / 24; +Result: 121.9583333333333333 +SELECT timestamptz '2013-07-01 12:00:00' - timestamptz '2013-03-01 12:00:00'; +Result: 121 days 23:00:00 +SELECT age(timestamptz '2013-07-01 12:00:00', timestamptz '2013-03-01 12:00:00'); +Result: 4 mons + + + + <function>EXTRACT</function>, <function>date_part</function> + + + date_part + + + extract + + + +EXTRACT(field FROM source) + + + + The extract function retrieves subfields + such as year or hour from date/time values. + source must be a value expression of + type timestamp, date, time, + or interval. (Timestamps and times can be with or + without time zone.) + field is an identifier or + string that selects what field to extract from the source value. + Not all fields are valid for every input data type; for example, fields + smaller than a day cannot be extracted from a date, while + fields of a day or more cannot be extracted from a time. + The extract function returns values of type + numeric. + + + + The following are valid field names: + + + + + century + + + The century; for interval values, the year field + divided by 100 + + + +SELECT EXTRACT(CENTURY FROM TIMESTAMP '2000-12-16 12:21:13'); +Result: 20 +SELECT EXTRACT(CENTURY FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 21 +SELECT EXTRACT(CENTURY FROM DATE '0001-01-01 AD'); +Result: 1 +SELECT EXTRACT(CENTURY FROM DATE '0001-12-31 BC'); +Result: -1 +SELECT EXTRACT(CENTURY FROM INTERVAL '2001 years'); +Result: 20 + + + + + + day + + + The day of the month (1–31); for interval + values, the number of days + + + +SELECT EXTRACT(DAY FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 16 +SELECT EXTRACT(DAY FROM INTERVAL '40 days 1 minute'); +Result: 40 + + + + + + + decade + + + The year field divided by 10 + + + +SELECT EXTRACT(DECADE FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 200 + + + + + + dow + + + The day of the week as Sunday (0) to + Saturday (6) + + + +SELECT EXTRACT(DOW FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 5 + + + Note that extract's day of the week numbering + differs from that of the to_char(..., + 'D') function. + + + + + + + doy + + + The day of the year (1–365/366) + + + +SELECT EXTRACT(DOY FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 47 + + + + + + epoch + + + For timestamp with time zone values, the + number of seconds since 1970-01-01 00:00:00 UTC (negative for + timestamps before that); + for date and timestamp values, the + nominal number of seconds since 1970-01-01 00:00:00, + without regard to timezone or daylight-savings rules; + for interval values, the total number + of seconds in the interval + + + +SELECT EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40.12-08'); +Result: 982384720.120000 +SELECT EXTRACT(EPOCH FROM TIMESTAMP '2001-02-16 20:38:40.12'); +Result: 982355920.120000 +SELECT EXTRACT(EPOCH FROM INTERVAL '5 days 3 hours'); +Result: 442800.000000 + + + + You can convert an epoch value back to a timestamp with time zone + with to_timestamp: + + +SELECT to_timestamp(982384720.12); +Result: 2001-02-17 04:38:40.12+00 + + + + Beware that applying to_timestamp to an epoch + extracted from a date or timestamp value + could produce a misleading result: the result will effectively + assume that the original value had been given in UTC, which might + not be the case. + + + + + + hour + + + The hour field (0–23 in timestamps, unrestricted in + intervals) + + + +SELECT EXTRACT(HOUR FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 20 + + + + + + isodow + + + The day of the week as Monday (1) to + Sunday (7) + + + +SELECT EXTRACT(ISODOW FROM TIMESTAMP '2001-02-18 20:38:40'); +Result: 7 + + + This is identical to dow except for Sunday. This + matches the ISO 8601 day of the week numbering. + + + + + + + isoyear + + + The ISO 8601 week-numbering year that the date + falls in + + + +SELECT EXTRACT(ISOYEAR FROM DATE '2006-01-01'); +Result: 2005 +SELECT EXTRACT(ISOYEAR FROM DATE '2006-01-02'); +Result: 2006 + + + + Each ISO 8601 week-numbering year begins with the + Monday of the week containing the 4th of January, so in early + January or late December the ISO year may be + different from the Gregorian year. See the week + field for more information. + + + + + + julian + + + The Julian Date corresponding to the + date or timestamp. Timestamps + that are not local midnight result in a fractional value. See + for more information. + + + +SELECT EXTRACT(JULIAN FROM DATE '2006-01-01'); +Result: 2453737 +SELECT EXTRACT(JULIAN FROM TIMESTAMP '2006-01-01 12:00'); +Result: 2453737.50000000000000000000 + + + + + + microseconds + + + The seconds field, including fractional parts, multiplied by 1 + 000 000; note that this includes full seconds + + + +SELECT EXTRACT(MICROSECONDS FROM TIME '17:12:28.5'); +Result: 28500000 + + + + + + millennium + + + The millennium; for interval values, the year field + divided by 1000 + + + +SELECT EXTRACT(MILLENNIUM FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 3 +SELECT EXTRACT(MILLENNIUM FROM INTERVAL '2001 years'); +Result: 2 + + + + Years in the 1900s are in the second millennium. + The third millennium started January 1, 2001. + + + + + + milliseconds + + + The seconds field, including fractional parts, multiplied by + 1000. Note that this includes full seconds. + + + +SELECT EXTRACT(MILLISECONDS FROM TIME '17:12:28.5'); +Result: 28500.000 + + + + + + minute + + + The minutes field (0–59) + + + +SELECT EXTRACT(MINUTE FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 38 + + + + + + month + + + The number of the month within the year (1–12); + for interval values, the number of months modulo 12 + (0–11) + + + +SELECT EXTRACT(MONTH FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 2 +SELECT EXTRACT(MONTH FROM INTERVAL '2 years 3 months'); +Result: 3 +SELECT EXTRACT(MONTH FROM INTERVAL '2 years 13 months'); +Result: 1 + + + + + + quarter + + + The quarter of the year (1–4) that the date is in; + for interval values, the month field divided by 3 + plus 1 + + + +SELECT EXTRACT(QUARTER FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 1 +SELECT EXTRACT(QUARTER FROM INTERVAL '1 year 6 months'); +Result: 3 + + + + + + second + + + The seconds field, including any fractional seconds + + + +SELECT EXTRACT(SECOND FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 40.000000 +SELECT EXTRACT(SECOND FROM TIME '17:12:28.5'); +Result: 28.500000 + + + + + timezone + + + The time zone offset from UTC, measured in seconds. Positive values + correspond to time zones east of UTC, negative values to + zones west of UTC. (Technically, + PostgreSQL does not use UTC because + leap seconds are not handled.) + + + + + + timezone_hour + + + The hour component of the time zone offset + + + + + + timezone_minute + + + The minute component of the time zone offset + + + + + + week + + + The number of the ISO 8601 week-numbering week of + the year. By definition, ISO weeks start on Mondays and the first + week of a year contains January 4 of that year. In other words, the + first Thursday of a year is in week 1 of that year. + + + In the ISO week-numbering system, it is possible for early-January + dates to be part of the 52nd or 53rd week of the previous year, and for + late-December dates to be part of the first week of the next year. + For example, 2005-01-01 is part of the 53rd week of year + 2004, and 2006-01-01 is part of the 52nd week of year + 2005, while 2012-12-31 is part of the first week of 2013. + It's recommended to use the isoyear field together with + week to get consistent results. + + + + For interval values, the week field is simply the number + of integral days divided by 7. + + + +SELECT EXTRACT(WEEK FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 7 +SELECT EXTRACT(WEEK FROM INTERVAL '13 days 24 hours'); +Result: 1 + + + + + + year + + + The year field. Keep in mind there is no 0 AD, so subtracting + BC years from AD years should be done with care. + + + +SELECT EXTRACT(YEAR FROM TIMESTAMP '2001-02-16 20:38:40'); +Result: 2001 + + + + + + + + + When processing an interval value, + the extract function produces field values that + match the interpretation used by the interval output function. This + can produce surprising results if one starts with a non-normalized + interval representation, for example: + +SELECT INTERVAL '80 minutes'; +Result: 01:20:00 +SELECT EXTRACT(MINUTES FROM INTERVAL '80 minutes'); +Result: 20 + + + + + + When the input value is +/-Infinity, extract returns + +/-Infinity for monotonically-increasing fields (epoch, + julian, year, isoyear, + decade, century, and millennium + for timestamp inputs; epoch, hour, + day, year, decade, + century, and millennium for + interval inputs). + For other fields, NULL is returned. PostgreSQL + versions before 9.6 returned zero for all cases of infinite input. + + + + + The extract function is primarily intended + for computational processing. For formatting date/time values for + display, see . + + + + The date_part function is modeled on the traditional + Ingres equivalent to the + SQL-standard function extract: + +date_part('field', source) + + Note that here the field parameter needs to + be a string value, not a name. The valid field names for + date_part are the same as for + extract. + For historical reasons, the date_part function + returns values of type double precision. This can result in + a loss of precision in certain uses. Using extract + is recommended instead. + + + +SELECT date_part('day', TIMESTAMP '2001-02-16 20:38:40'); +Result: 16 +SELECT date_part('hour', INTERVAL '4 hours 3 minutes'); +Result: 4 + + + + + + <function>date_trunc</function> + + + date_trunc + + + + The function date_trunc is conceptually + similar to the trunc function for numbers. + + + + +date_trunc(field, source , time_zone ) + + source is a value expression of type + timestamp, timestamp with time zone, + or interval. + (Values of type date and + time are cast automatically to timestamp or + interval, respectively.) + field selects to which precision to + truncate the input value. The return value is likewise of type + timestamp, timestamp with time zone, + or interval, + and it has all fields that are less significant than the + selected one set to zero (or one, for day and month). + + + + Valid values for field are: + + microseconds + milliseconds + second + minute + hour + day + week + month + quarter + year + decade + century + millennium + + + + + When the input value is of type timestamp with time zone, + the truncation is performed with respect to a particular time zone; + for example, truncation to day produces a value that + is midnight in that zone. By default, truncation is done with respect + to the current setting, but the + optional time_zone argument can be provided + to specify a different time zone. The time zone name can be specified + in any of the ways described in . + + + + A time zone cannot be specified when processing timestamp without + time zone or interval inputs. These are always + taken at face value. + + + + Examples (assuming the local time zone is America/New_York): + +SELECT date_trunc('hour', TIMESTAMP '2001-02-16 20:38:40'); +Result: 2001-02-16 20:00:00 +SELECT date_trunc('year', TIMESTAMP '2001-02-16 20:38:40'); +Result: 2001-01-01 00:00:00 +SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00'); +Result: 2001-02-16 00:00:00-05 +SELECT date_trunc('day', TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40+00', 'Australia/Sydney'); +Result: 2001-02-16 08:00:00-05 +SELECT date_trunc('hour', INTERVAL '3 days 02:47:33'); +Result: 3 days 02:00:00 + + + + + + <function>date_bin</function> + + + date_bin + + + + The function date_bin bins the input + timestamp into the specified interval (the stride) + aligned with a specified origin. + + + + +date_bin(stride, source, origin) + + source is a value expression of type + timestamp or timestamp with time zone. (Values + of type date are cast automatically to + timestamp.) stride is a value + expression of type interval. The return value is likewise + of type timestamp or timestamp with time zone, + and it marks the beginning of the bin into which the + source is placed. + + + + Examples: + +SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01'); +Result: 2020-02-11 15:30:00 +SELECT date_bin('15 minutes', TIMESTAMP '2020-02-11 15:44:17', TIMESTAMP '2001-01-01 00:02:30'); +Result: 2020-02-11 15:32:30 + + + + + In the case of full units (1 minute, 1 hour, etc.), it gives the same result as + the analogous date_trunc call, but the difference is + that date_bin can truncate to an arbitrary interval. + + + + The stride interval must be greater than zero and + cannot contain units of month or larger. + + + + + <literal>AT TIME ZONE</literal> and <literal>AT LOCAL</literal> + + + time zone + conversion + + + + AT TIME ZONE + + + + AT LOCAL + + + + The AT TIME ZONE operator converts time + stamp without time zone to/from + time stamp with time zone, and + time with time zone values to different time + zones. shows its + variants. + + + + <literal>AT TIME ZONE</literal> and <literal>AT LOCAL</literal> Variants + + + + + Operator + + + Description + + + Example(s) + + + + + + + + timestamp without time zone AT TIME ZONE zone + timestamp with time zone + + + Converts given time stamp without time zone to + time stamp with time zone, assuming the given + value is in the named time zone. + + + timestamp '2001-02-16 20:38:40' AT TIME ZONE 'America/Denver' + 2001-02-17 03:38:40+00 + + + + + + timestamp without time zone AT LOCAL + timestamp with time zone + + + Converts given time stamp without time zone to + time stamp with the session's + TimeZone value as time zone. + + + timestamp '2001-02-16 20:38:40' at local + 2001-02-17 03:38:40+00 + + + + + + timestamp with time zone AT TIME ZONE zone + timestamp without time zone + + + Converts given time stamp with time zone to + time stamp without time zone, as the time would + appear in that zone. + + + timestamp with time zone '2001-02-16 20:38:40-05' AT TIME ZONE 'America/Denver' + 2001-02-16 18:38:40 + + + + + + timestamp with time zone AT LOCAL + timestamp without time zone + + + Converts given time stamp with time zone to + time stamp without time zone, as the time would + appear with the session's TimeZone value as time zone. + + + timestamp with time zone '2001-02-16 20:38:40-05' at local + 2001-02-16 18:38:40 + + + + + + time with time zone AT TIME ZONE zone + time with time zone + + + Converts given time with time zone to a new time + zone. Since no date is supplied, this uses the currently active UTC + offset for the named destination zone. + + + time with time zone '05:34:17-05' AT TIME ZONE 'UTC' + 10:34:17+00 + + + + + + time with time zone AT LOCAL + time with time zone + + + Converts given time with time zone to a new time + zone. Since no date is supplied, this uses the currently active UTC + offset for the session's TimeZone value. + + + Assuming the session's TimeZone is set to UTC: + + + time with time zone '05:34:17-05' at local + 10:34:17+00 + + + + +
+ + + In these expressions, the desired time zone zone can be + specified either as a text value (e.g., 'America/Los_Angeles') + or as an interval (e.g., INTERVAL '-08:00'). + In the text case, a time zone name can be specified in any of the ways + described in . + The interval case is only useful for zones that have fixed offsets from + UTC, so it is not very common in practice. + + + + The syntax AT LOCAL may be used as shorthand for + AT TIME ZONE local, where + local is the session's + TimeZone value. + + + + Examples (assuming the current setting + is America/Los_Angeles): + +SELECT TIMESTAMP '2001-02-16 20:38:40' AT TIME ZONE 'America/Denver'; +Result: 2001-02-16 19:38:40-08 +SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT TIME ZONE 'America/Denver'; +Result: 2001-02-16 18:38:40 +SELECT TIMESTAMP '2001-02-16 20:38:40' AT TIME ZONE 'Asia/Tokyo' AT TIME ZONE 'America/Chicago'; +Result: 2001-02-16 05:38:40 +SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT LOCAL; +Result: 2001-02-16 17:38:40 +SELECT TIMESTAMP WITH TIME ZONE '2001-02-16 20:38:40-05' AT TIME ZONE '+05'; +Result: 2001-02-16 20:38:40 +SELECT TIME WITH TIME ZONE '20:38:40-05' AT LOCAL; +Result: 17:38:40 + + The first example adds a time zone to a value that lacks it, and + displays the value using the current TimeZone + setting. The second example shifts the time stamp with time zone value + to the specified time zone, and returns the value without a time zone. + This allows storage and display of values different from the current + TimeZone setting. The third example converts + Tokyo time to Chicago time. The fourth example shifts the time stamp + with time zone value to the time zone currently specified by the + TimeZone setting and returns the value without a + time zone. The fifth example demonstrates that the sign in a POSIX-style + time zone specification has the opposite meaning of the sign in an + ISO-8601 datetime literal, as described in + and . + + + + The sixth example is a cautionary tale. Due to the fact that there is no + date associated with the input value, the conversion is made using the + current date of the session. Therefore, this static example may show a wrong + result depending on the time of the year it is viewed because + 'America/Los_Angeles' observes Daylight Savings Time. + + + + The function timezone(zone, + timestamp) is equivalent to the SQL-conforming construct + timestamp AT TIME ZONE + zone. + + + + The function timezone(zone, + time) is equivalent to the SQL-conforming construct + time AT TIME ZONE + zone. + + + + The function timezone(timestamp) + is equivalent to the SQL-conforming construct timestamp + AT LOCAL. + + + + The function timezone(time) + is equivalent to the SQL-conforming construct time + AT LOCAL. + +
+ + + Current Date/Time + + + date + current + + + + time + current + + + + PostgreSQL provides a number of functions + that return values related to the current date and time. These + SQL-standard functions all return values based on the start time of + the current transaction: + +CURRENT_DATE +CURRENT_TIME +CURRENT_TIMESTAMP +CURRENT_TIME(precision) +CURRENT_TIMESTAMP(precision) +LOCALTIME +LOCALTIMESTAMP +LOCALTIME(precision) +LOCALTIMESTAMP(precision) + + + + + CURRENT_TIME and + CURRENT_TIMESTAMP deliver values with time zone; + LOCALTIME and + LOCALTIMESTAMP deliver values without time zone. + + + + CURRENT_TIME, + CURRENT_TIMESTAMP, + LOCALTIME, and + LOCALTIMESTAMP + can optionally take + a precision parameter, which causes the result to be rounded + to that many fractional digits in the seconds field. Without a precision parameter, + the result is given to the full available precision. + + + + Some examples: + +SELECT CURRENT_TIME; +Result: 14:39:53.662522-05 +SELECT CURRENT_DATE; +Result: 2019-12-23 +SELECT CURRENT_TIMESTAMP; +Result: 2019-12-23 14:39:53.662522-05 +SELECT CURRENT_TIMESTAMP(2); +Result: 2019-12-23 14:39:53.66-05 +SELECT LOCALTIMESTAMP; +Result: 2019-12-23 14:39:53.662522 + + + + + Since these functions return + the start time of the current transaction, their values do not + change during the transaction. This is considered a feature: + the intent is to allow a single transaction to have a consistent + notion of the current time, so that multiple + modifications within the same transaction bear the same + time stamp. + + + + + Other database systems might advance these values more + frequently. + + + + + PostgreSQL also provides functions that + return the start time of the current statement, as well as the actual + current time at the instant the function is called. The complete list + of non-SQL-standard time functions is: + +transaction_timestamp() +statement_timestamp() +clock_timestamp() +timeofday() +now() + + + + + transaction_timestamp() is equivalent to + CURRENT_TIMESTAMP, but is named to clearly reflect + what it returns. + statement_timestamp() returns the start time of the current + statement (more specifically, the time of receipt of the latest command + message from the client). + statement_timestamp() and transaction_timestamp() + return the same value during the first statement of a transaction, but might + differ during subsequent statements. + clock_timestamp() returns the actual current time, and + therefore its value changes even within a single SQL statement. + timeofday() is a historical + PostgreSQL function. Like + clock_timestamp(), it returns the actual current time, + but as a formatted text string rather than a timestamp + with time zone value. + now() is a traditional PostgreSQL + equivalent to transaction_timestamp(). + + + + All the date/time data types also accept the special literal value + now to specify the current date and time (again, + interpreted as the transaction start time). Thus, + the following three all return the same result: + +SELECT CURRENT_TIMESTAMP; +SELECT now(); +SELECT TIMESTAMP 'now'; -- but see tip below + + + + + + Do not use the third form when specifying a value to be evaluated later, + for example in a DEFAULT clause for a table column. + The system will convert now + to a timestamp as soon as the constant is parsed, so that when + the default value is needed, + the time of the table creation would be used! The first two + forms will not be evaluated until the default value is used, + because they are function calls. Thus they will give the desired + behavior of defaulting to the time of row insertion. + (See also .) + + + + + + Delaying Execution + + + pg_sleep + + + pg_sleep_for + + + pg_sleep_until + + + sleep + + + delay + + + + The following functions are available to delay execution of the server + process: + +pg_sleep ( double precision ) +pg_sleep_for ( interval ) +pg_sleep_until ( timestamp with time zone ) + + + pg_sleep makes the current session's process + sleep until the given number of seconds have + elapsed. Fractional-second delays can be specified. + pg_sleep_for is a convenience function to + allow the sleep time to be specified as an interval. + pg_sleep_until is a convenience function for when + a specific wake-up time is desired. + For example: + + +SELECT pg_sleep(1.5); +SELECT pg_sleep_for('5 minutes'); +SELECT pg_sleep_until('tomorrow 03:00'); + + + + + + The effective resolution of the sleep interval is platform-specific; + 0.01 seconds is a common value. The sleep delay will be at least as long + as specified. It might be longer depending on factors such as server load. + In particular, pg_sleep_until is not guaranteed to + wake up exactly at the specified time, but it will not wake up any earlier. + + + + + + Make sure that your session does not hold more locks than necessary + when calling pg_sleep or its variants. Otherwise + other sessions might have to wait for your sleeping process, slowing down + the entire system. + + + + +
diff --git a/doc/src/sgml/func/func-enum.sgml b/doc/src/sgml/func/func-enum.sgml new file mode 100644 index 0000000000000..6227afe4057ba --- /dev/null +++ b/doc/src/sgml/func/func-enum.sgml @@ -0,0 +1,121 @@ + + Enum Support Functions + + + For enum types (described in ), + there are several functions that allow cleaner programming without + hard-coding particular values of an enum type. + These are listed in . The examples + assume an enum type created as: + + +CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow', 'green', 'blue', 'purple'); + + + + + + Enum Support Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + enum_first + + enum_first ( anyenum ) + anyenum + + + Returns the first value of the input enum type. + + + enum_first(null::rainbow) + red + + + + + + enum_last + + enum_last ( anyenum ) + anyenum + + + Returns the last value of the input enum type. + + + enum_last(null::rainbow) + purple + + + + + + enum_range + + enum_range ( anyenum ) + anyarray + + + Returns all values of the input enum type in an ordered array. + + + enum_range(null::rainbow) + {red,orange,yellow,&zwsp;green,blue,purple} + + + + + enum_range ( anyenum, anyenum ) + anyarray + + + Returns the range between the two given enum values, as an ordered + array. The values must be from the same enum type. If the first + parameter is null, the result will start with the first value of + the enum type. + If the second parameter is null, the result will end with the last + value of the enum type. + + + enum_range('orange'::rainbow, 'green'::rainbow) + {orange,yellow,green} + + + enum_range(NULL, 'green'::rainbow) + {red,orange,&zwsp;yellow,green} + + + enum_range('orange'::rainbow, NULL) + {orange,yellow,green,&zwsp;blue,purple} + + + + +
+ + + Notice that except for the two-argument form of enum_range, + these functions disregard the specific value passed to them; they care + only about its declared data type. Either null or a specific value of + the type can be passed, with the same result. It is more common to + apply these functions to a table column or function argument than to + a hardwired type name as used in the examples. + +
diff --git a/doc/src/sgml/func/func-event-triggers.sgml b/doc/src/sgml/func/func-event-triggers.sgml new file mode 100644 index 0000000000000..9f3f51e9f5133 --- /dev/null +++ b/doc/src/sgml/func/func-event-triggers.sgml @@ -0,0 +1,332 @@ + + Event Trigger Functions + + + PostgreSQL provides these helper functions + to retrieve information from event triggers. + + + + For more information about event triggers, + see . + + + + Capturing Changes at Command End + + + pg_event_trigger_ddl_commands + + + +pg_event_trigger_ddl_commands () setof record + + + + pg_event_trigger_ddl_commands returns a list of + DDL commands executed by each user action, + when invoked in a function attached to a + ddl_command_end event trigger. If called in any other + context, an error is raised. + pg_event_trigger_ddl_commands returns one row for each + base command executed; some commands that are a single SQL sentence + may return more than one row. This function returns the following + columns: + + + + + + Name + Type + Description + + + + + + classid + oid + OID of catalog the object belongs in + + + objid + oid + OID of the object itself + + + objsubid + integer + Sub-object ID (e.g., attribute number for a column) + + + command_tag + text + Command tag + + + object_type + text + Type of the object + + + schema_name + text + + Name of the schema the object belongs in, if any; otherwise NULL. + No quoting is applied. + + + + object_identity + text + + Text rendering of the object identity, schema-qualified. Each + identifier included in the identity is quoted if necessary. + + + + in_extension + boolean + True if the command is part of an extension script + + + command + pg_ddl_command + + A complete representation of the command, in internal format. + This cannot be output directly, but it can be passed to other + functions to obtain different pieces of information about the + command. + + + + + + + + + + Processing Objects Dropped by a DDL Command + + + pg_event_trigger_dropped_objects + + + +pg_event_trigger_dropped_objects () setof record + + + + pg_event_trigger_dropped_objects returns a list of all objects + dropped by the command in whose sql_drop event it is called. + If called in any other context, an error is raised. + This function returns the following columns: + + + + + + Name + Type + Description + + + + + + classid + oid + OID of catalog the object belonged in + + + objid + oid + OID of the object itself + + + objsubid + integer + Sub-object ID (e.g., attribute number for a column) + + + original + boolean + True if this was one of the root object(s) of the deletion + + + normal + boolean + + True if there was a normal dependency relationship + in the dependency graph leading to this object + + + + is_temporary + boolean + + True if this was a temporary object + + + + object_type + text + Type of the object + + + schema_name + text + + Name of the schema the object belonged in, if any; otherwise NULL. + No quoting is applied. + + + + object_name + text + + Name of the object, if the combination of schema and name can be + used as a unique identifier for the object; otherwise NULL. + No quoting is applied, and name is never schema-qualified. + + + + object_identity + text + + Text rendering of the object identity, schema-qualified. Each + identifier included in the identity is quoted if necessary. + + + + address_names + text[] + + An array that, together with object_type and + address_args, can be used by + the pg_get_object_address function to + recreate the object address in a remote server containing an + identically named object of the same kind. + + + + address_args + text[] + + Complement for address_names + + + + + + + + + The pg_event_trigger_dropped_objects function can be used + in an event trigger like this: + +CREATE FUNCTION test_event_trigger_for_drops() + RETURNS event_trigger LANGUAGE plpgsql AS $$ +DECLARE + obj record; +BEGIN + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + RAISE NOTICE '% dropped object: % %.% %', + tg_tag, + obj.object_type, + obj.schema_name, + obj.object_name, + obj.object_identity; + END LOOP; +END; +$$; +CREATE EVENT TRIGGER test_event_trigger_for_drops + ON sql_drop + EXECUTE FUNCTION test_event_trigger_for_drops(); + + + + + + Handling a Table Rewrite Event + + + The functions shown in + + provide information about a table for which a + table_rewrite event has just been called. + If called in any other context, an error is raised. + + + + Table Rewrite Information Functions + + + + + Function + + + Description + + + + + + + + + pg_event_trigger_table_rewrite_oid + + pg_event_trigger_table_rewrite_oid () + oid + + + Returns the OID of the table about to be rewritten. + + + + + + + pg_event_trigger_table_rewrite_reason + + pg_event_trigger_table_rewrite_reason () + integer + + + Returns a code explaining the reason(s) for rewriting. The value is + a bitmap built from the following values: 1 + (the table has changed its persistence), 2 + (default value of a column has changed), 4 + (a column has a new data type) and 8 + (the table access method has changed). + + + + +
+ + + These functions can be used in an event trigger like this: + +CREATE FUNCTION test_event_trigger_table_rewrite_oid() + RETURNS event_trigger + LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE 'rewriting table % for reason %', + pg_event_trigger_table_rewrite_oid()::regclass, + pg_event_trigger_table_rewrite_reason(); +END; +$$; + +CREATE EVENT TRIGGER test_table_rewrite_oid + ON table_rewrite + EXECUTE FUNCTION test_event_trigger_table_rewrite_oid(); + + +
+
diff --git a/doc/src/sgml/func/func-formatting.sgml b/doc/src/sgml/func/func-formatting.sgml new file mode 100644 index 0000000000000..af9e2223998ad --- /dev/null +++ b/doc/src/sgml/func/func-formatting.sgml @@ -0,0 +1,1197 @@ + + Data Type Formatting Functions + + + formatting + + + + The PostgreSQL formatting functions + provide a powerful set of tools for converting various data types + (date/time, integer, floating point, numeric) to formatted strings + and for converting from formatted strings to specific data types. + lists them. + These functions all follow a common calling convention: the first + argument is the value to be formatted and the second argument is a + template that defines the output or input format. + + + + Formatting Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + to_char + + to_char ( timestamp, text ) + text + + + to_char ( timestamp with time zone, text ) + text + + + Converts time stamp to string according to the given format. + + + to_char(timestamp '2002-04-20 17:31:12.66', 'HH12:MI:SS') + 05:31:12 + + + + + + to_char ( interval, text ) + text + + + Converts interval to string according to the given format. + + + to_char(interval '15h 2m 12s', 'HH24:MI:SS') + 15:02:12 + + + + + + to_char ( numeric_type, text ) + text + + + Converts number to string according to the given format; available + for integer, bigint, numeric, + real, double precision. + + + to_char(125, '999') + 125 + + + to_char(125.8::real, '999D9') + 125.8 + + + to_char(-125.8, '999D99S') + 125.80- + + + + + + + to_date + + to_date ( text, text ) + date + + + Converts string to date according to the given format. + + + to_date('05 Dec 2000', 'DD Mon YYYY') + 2000-12-05 + + + + + + + to_number + + to_number ( text, text ) + numeric + + + Converts string to numeric according to the given format. + + + to_number('12,454.8-', '99G999D9S') + -12454.8 + + + + + + + to_timestamp + + to_timestamp ( text, text ) + timestamp with time zone + + + Converts string to time stamp according to the given format. + (See also to_timestamp(double precision) in + .) + + + to_timestamp('05 Dec 2000', 'DD Mon YYYY') + 2000-12-05 00:00:00-05 + + + + +
+ + + + to_timestamp and to_date + exist to handle input formats that cannot be converted by + simple casting. For most standard date/time formats, simply casting the + source string to the required data type works, and is much easier. + Similarly, to_number is unnecessary for standard numeric + representations. + + + + + In a to_char output template string, there are certain + patterns that are recognized and replaced with appropriately-formatted + data based on the given value. Any text that is not a template pattern is + simply copied verbatim. Similarly, in an input template string (for the + other functions), template patterns identify the values to be supplied by + the input data string. If there are characters in the template string + that are not template patterns, the corresponding characters in the input + data string are simply skipped over (whether or not they are equal to the + template string characters). + + + + shows the + template patterns available for formatting date and time values. + + + + Template Patterns for Date/Time Formatting + + + + + + Pattern + Description + + + + + HH + hour of day (01–12) + + + HH12 + hour of day (01–12) + + + HH24 + hour of day (00–23) + + + MI + minute (00–59) + + + SS + second (00–59) + + + MS + millisecond (000–999) + + + US + microsecond (000000–999999) + + + FF1 + tenth of second (0–9) + + + FF2 + hundredth of second (00–99) + + + FF3 + millisecond (000–999) + + + FF4 + tenth of a millisecond (0000–9999) + + + FF5 + hundredth of a millisecond (00000–99999) + + + FF6 + microsecond (000000–999999) + + + SSSS, SSSSS + seconds past midnight (0–86399) + + + AM, am, + PM or pm + meridiem indicator (without periods) + + + A.M., a.m., + P.M. or p.m. + meridiem indicator (with periods) + + + Y,YYY + year (4 or more digits) with comma + + + YYYY + year (4 or more digits) + + + YYY + last 3 digits of year + + + YY + last 2 digits of year + + + Y + last digit of year + + + IYYY + ISO 8601 week-numbering year (4 or more digits) + + + IYY + last 3 digits of ISO 8601 week-numbering year + + + IY + last 2 digits of ISO 8601 week-numbering year + + + I + last digit of ISO 8601 week-numbering year + + + BC, bc, + AD or ad + era indicator (without periods) + + + B.C., b.c., + A.D. or a.d. + era indicator (with periods) + + + MONTH + full upper case month name (blank-padded to 9 chars) + + + Month + full capitalized month name (blank-padded to 9 chars) + + + month + full lower case month name (blank-padded to 9 chars) + + + MON + abbreviated upper case month name (3 chars in English, localized lengths vary) + + + Mon + abbreviated capitalized month name (3 chars in English, localized lengths vary) + + + mon + abbreviated lower case month name (3 chars in English, localized lengths vary) + + + MM + month number (01–12) + + + DAY + full upper case day name (blank-padded to 9 chars) + + + Day + full capitalized day name (blank-padded to 9 chars) + + + day + full lower case day name (blank-padded to 9 chars) + + + DY + abbreviated upper case day name (3 chars in English, localized lengths vary) + + + Dy + abbreviated capitalized day name (3 chars in English, localized lengths vary) + + + dy + abbreviated lower case day name (3 chars in English, localized lengths vary) + + + DDD + day of year (001–366) + + + IDDD + day of ISO 8601 week-numbering year (001–371; day 1 of the year is Monday of the first ISO week) + + + DD + day of month (01–31) + + + D + day of the week, Sunday (1) to Saturday (7) + + + ID + ISO 8601 day of the week, Monday (1) to Sunday (7) + + + W + week of month (1–5) (the first week starts on the first day of the month) + + + WW + week number of year (1–53) (the first week starts on the first day of the year) + + + IW + week number of ISO 8601 week-numbering year (01–53; the first Thursday of the year is in week 1) + + + CC + century (2 digits) (the twenty-first century starts on 2001-01-01) + + + J + Julian Date (integer days since November 24, 4714 BC at local + midnight; see ) + + + Q + quarter + + + RM + month in upper case Roman numerals (I–XII; I=January) + + + rm + month in lower case Roman numerals (i–xii; i=January) + + + TZ + upper case time-zone abbreviation + + + tz + lower case time-zone abbreviation + + + TZH + time-zone hours + + + TZM + time-zone minutes + + + OF + time-zone offset from UTC (HH + or HH:MM) + + + +
+ + + Modifiers can be applied to any template pattern to alter its + behavior. For example, FMMonth + is the Month pattern with the + FM modifier. + shows the + modifier patterns for date/time formatting. + + + + Template Pattern Modifiers for Date/Time Formatting + + + + Modifier + Description + Example + + + + + FM prefix + fill mode (suppress leading zeroes and padding blanks) + FMMonth + + + TH suffix + upper case ordinal number suffix + DDTH, e.g., 12TH + + + th suffix + lower case ordinal number suffix + DDth, e.g., 12th + + + FX prefix + fixed format global option (see usage notes) + FX Month DD Day + + + TM prefix + translation mode (use localized day and month names based on + ) + TMMonth + + + SP suffix + spell mode (not implemented) + DDSP + + + +
+ + + Usage notes for date/time formatting: + + + + + FM suppresses leading zeroes and trailing blanks + that would otherwise be added to make the output of a pattern be + fixed-width. In PostgreSQL, + FM modifies only the next specification, while in + Oracle FM affects all subsequent + specifications, and repeated FM modifiers + toggle fill mode on and off. + + + + + + TM suppresses trailing blanks whether or + not FM is specified. + + + + + + to_timestamp and to_date + ignore letter case in the input; so for + example MON, Mon, + and mon all accept the same strings. When using + the TM modifier, case-folding is done according to + the rules of the function's input collation (see + ). + + + + + + to_timestamp and to_date + skip multiple blank spaces at the beginning of the input string and + around date and time values unless the FX option is used. For example, + to_timestamp(' 2000    JUN', 'YYYY MON') and + to_timestamp('2000 - JUN', 'YYYY-MON') work, but + to_timestamp('2000    JUN', 'FXYYYY MON') returns an error + because to_timestamp expects only a single space. + FX must be specified as the first item in + the template. + + + + + + A separator (a space or non-letter/non-digit character) in the template string of + to_timestamp and to_date + matches any single separator in the input string or is skipped, + unless the FX option is used. + For example, to_timestamp('2000JUN', 'YYYY///MON') and + to_timestamp('2000/JUN', 'YYYY MON') work, but + to_timestamp('2000//JUN', 'YYYY/MON') + returns an error because the number of separators in the input string + exceeds the number of separators in the template. + + + If FX is specified, a separator in the template string + matches exactly one character in the input string. But note that the + input string character is not required to be the same as the separator from the template string. + For example, to_timestamp('2000/JUN', 'FXYYYY MON') + works, but to_timestamp('2000/JUN', 'FXYYYY  MON') + returns an error because the second space in the template string consumes + the letter J from the input string. + + + + + + A TZH template pattern can match a signed number. + Without the FX option, minus signs may be ambiguous, + and could be interpreted as a separator. + This ambiguity is resolved as follows: If the number of separators before + TZH in the template string is less than the number of + separators before the minus sign in the input string, the minus sign + is interpreted as part of TZH. + Otherwise, the minus sign is considered to be a separator between values. + For example, to_timestamp('2000 -10', 'YYYY TZH') matches + -10 to TZH, but + to_timestamp('2000 -10', 'YYYY  TZH') + matches 10 to TZH. + + + + + + Ordinary text is allowed in to_char + templates and will be output literally. You can put a substring + in double quotes to force it to be interpreted as literal text + even if it contains template patterns. For example, in + '"Hello Year "YYYY', the YYYY + will be replaced by the year data, but the single Y in Year + will not be. + In to_date, to_number, + and to_timestamp, literal text and double-quoted + strings result in skipping the number of characters contained in the + string; for example "XX" skips two input characters + (whether or not they are XX). + + + + Prior to PostgreSQL 12, it was possible to + skip arbitrary text in the input string using non-letter or non-digit + characters. For example, + to_timestamp('2000y6m1d', 'yyyy-MM-DD') used to + work. Now you can only use letter characters for this purpose. For example, + to_timestamp('2000y6m1d', 'yyyytMMtDDt') and + to_timestamp('2000y6m1d', 'yyyy"y"MM"m"DD"d"') + skip y, m, and + d. + + + + + + + If you want to have a double quote in the output you must + precede it with a backslash, for example '\"YYYY + Month\"'. + Backslashes are not otherwise special outside of double-quoted + strings. Within a double-quoted string, a backslash causes the + next character to be taken literally, whatever it is (but this + has no special effect unless the next character is a double quote + or another backslash). + + + + + + In to_timestamp and to_date, + if the year format specification is less than four digits, e.g., + YYY, and the supplied year is less than four digits, + the year will be adjusted to be nearest to the year 2020, e.g., + 95 becomes 1995. + + + + + + In to_timestamp and to_date, + negative years are treated as signifying BC. If you write both a + negative year and an explicit BC field, you get AD + again. An input of year zero is treated as 1 BC. + + + + + + In to_timestamp and to_date, + the YYYY conversion has a restriction when + processing years with more than 4 digits. You must + use some non-digit character or template after YYYY, + otherwise the year is always interpreted as 4 digits. For example + (with the year 20000): + to_date('200001130', 'YYYYMMDD') will be + interpreted as a 4-digit year; instead use a non-digit + separator after the year, like + to_date('20000-1130', 'YYYY-MMDD') or + to_date('20000Nov30', 'YYYYMonDD'). + + + + + + In to_timestamp and to_date, + the CC (century) field is accepted but ignored + if there is a YYY, YYYY or + Y,YYY field. If CC is used with + YY or Y then the result is + computed as that year in the specified century. If the century is + specified but the year is not, the first year of the century + is assumed. + + + + + + In to_timestamp and to_date, + weekday names or numbers (DAY, D, + and related field types) are accepted but are ignored for purposes of + computing the result. The same is true for quarter + (Q) fields. + + + + + + In to_timestamp and to_date, + an ISO 8601 week-numbering date (as distinct from a Gregorian date) + can be specified in one of two ways: + + + + Year, week number, and weekday: for + example to_date('2006-42-4', 'IYYY-IW-ID') + returns the date 2006-10-19. + If you omit the weekday it is assumed to be 1 (Monday). + + + + + Year and day of year: for example to_date('2006-291', + 'IYYY-IDDD') also returns 2006-10-19. + + + + + + Attempting to enter a date using a mixture of ISO 8601 week-numbering + fields and Gregorian date fields is nonsensical, and will cause an + error. In the context of an ISO 8601 week-numbering year, the + concept of a month or day of month has no + meaning. In the context of a Gregorian year, the ISO week has no + meaning. + + + + While to_date will reject a mixture of + Gregorian and ISO week-numbering date + fields, to_char will not, since output format + specifications like YYYY-MM-DD (IYYY-IDDD) can be + useful. But avoid writing something like IYYY-MM-DD; + that would yield surprising results near the start of the year. + (See for more + information.) + + + + + + + In to_timestamp, millisecond + (MS) or microsecond (US) + fields are used as the + seconds digits after the decimal point. For example + to_timestamp('12.3', 'SS.MS') is not 3 milliseconds, + but 300, because the conversion treats it as 12 + 0.3 seconds. + So, for the format SS.MS, the input values + 12.3, 12.30, + and 12.300 specify the + same number of milliseconds. To get three milliseconds, one must write + 12.003, which the conversion treats as + 12 + 0.003 = 12.003 seconds. + + + + Here is a more + complex example: + to_timestamp('15:12:02.020.001230', 'HH24:MI:SS.MS.US') + is 15 hours, 12 minutes, and 2 seconds + 20 milliseconds + + 1230 microseconds = 2.021230 seconds. + + + + + + to_char(..., 'ID')'s day of the week numbering + matches the extract(isodow FROM ...) function, but + to_char(..., 'D')'s does not match + extract(dow FROM ...)'s day numbering. + + + + + + to_char(interval) formats HH and + HH12 as shown on a 12-hour clock, for example zero hours + and 36 hours both output as 12, while HH24 + outputs the full hour value, which can exceed 23 in + an interval value. + + + + + + + + shows the + template patterns available for formatting numeric values. + + + + Template Patterns for Numeric Formatting + + + + + + Pattern + Description + + + + + 9 + digit position (can be dropped if insignificant) + + + 0 + digit position (will not be dropped, even if insignificant) + + + . (period) + decimal point + + + , (comma) + group (thousands) separator + + + PR + negative value in angle brackets + + + S + sign anchored to number (uses locale) + + + L + currency symbol (uses locale) + + + D + decimal point (uses locale) + + + G + group separator (uses locale) + + + MI + minus sign in specified position (if number < 0) + + + PL + plus sign in specified position (if number > 0) + + + SG + plus/minus sign in specified position + + + RN or rn + Roman numeral (values between 1 and 3999) + + + TH or th + ordinal number suffix + + + V + shift specified number of digits (see notes) + + + EEEE + exponent for scientific notation + + + +
+ + + Usage notes for numeric formatting: + + + + + 0 specifies a digit position that will always be printed, + even if it contains a leading/trailing zero. 9 also + specifies a digit position, but if it is a leading zero then it will + be replaced by a space, while if it is a trailing zero and fill mode + is specified then it will be deleted. (For to_number(), + these two pattern characters are equivalent.) + + + + + + If the format provides fewer fractional digits than the number being + formatted, to_char() will round the number to + the specified number of fractional digits. + + + + + + The pattern characters S, L, D, + and G represent the sign, currency symbol, decimal point, + and thousands separator characters defined by the current locale + (see + and ). The pattern characters period + and comma represent those exact characters, with the meanings of + decimal point and thousands separator, regardless of locale. + + + + + + If no explicit provision is made for a sign + in to_char()'s pattern, one column will be reserved for + the sign, and it will be anchored to (appear just left of) the + number. If S appears just left of some 9's, + it will likewise be anchored to the number. + + + + + + A sign formatted using SG, PL, or + MI is not anchored to + the number; for example, + to_char(-12, 'MI9999') produces '-  12' + but to_char(-12, 'S9999') produces '  -12'. + (The Oracle implementation does not allow the use of + MI before 9, but rather + requires that 9 precede + MI.) + + + + + + TH does not convert values less than zero + and does not convert fractional numbers. + + + + + + PL, SG, and + TH are PostgreSQL + extensions. + + + + + + In to_number, if non-data template patterns such + as L or TH are used, the + corresponding number of input characters are skipped, whether or not + they match the template pattern, unless they are data characters + (that is, digits, sign, decimal point, or comma). For + example, TH would skip two non-data characters. + + + + + + V with to_char + multiplies the input values by + 10^n, where + n is the number of digits following + V. V with + to_number divides in a similar manner. + The V can be thought of as marking the position + of an implicit decimal point in the input or output string. + to_char and to_number + do not support the use of + V combined with a decimal point + (e.g., 99.9V99 is not allowed). + + + + + + EEEE (scientific notation) cannot be used in + combination with any of the other formatting patterns or + modifiers other than digit and decimal point patterns, and must be at the end of the format string + (e.g., 9.99EEEE is a valid pattern). + + + + + + In to_number(), the RN + pattern converts Roman numerals (in standard form) to numbers. + Input is case-insensitive, so RN + and rn are equivalent. RN + cannot be used in combination with any other formatting patterns or + modifiers except FM, which is applicable only + in to_char() and is ignored + in to_number(). + + + + + + + Certain modifiers can be applied to any template pattern to alter its + behavior. For example, FM99.99 + is the 99.99 pattern with the + FM modifier. + shows the + modifier patterns for numeric formatting. + + + + Template Pattern Modifiers for Numeric Formatting + + + + Modifier + Description + Example + + + + + FM prefix + fill mode (suppress trailing zeroes and padding blanks) + FM99.99 + + + TH suffix + upper case ordinal number suffix + 999TH + + + th suffix + lower case ordinal number suffix + 999th + + + +
+ + + shows some + examples of the use of the to_char function. + + + + <function>to_char</function> Examples + + + + Expression + Result + + + + + to_char(current_timestamp, 'Day, DD  HH12:MI:SS') + 'Tuesday  , 06  05:39:18' + + + to_char(current_timestamp, 'FMDay, FMDD  HH12:MI:SS') + 'Tuesday, 6  05:39:18' + + + to_char(current_timestamp AT TIME ZONE + 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') + '2022-12-06T05:39:18Z', + ISO 8601 extended format + + + to_char(-0.1, '99.99') + '  -.10' + + + to_char(-0.1, 'FM9.99') + '-.1' + + + to_char(-0.1, 'FM90.99') + '-0.1' + + + to_char(0.1, '0.9') + ' 0.1' + + + to_char(12, '9990999.9') + '    0012.0' + + + to_char(12, 'FM9990999.9') + '0012.' + + + to_char(485, '999') + ' 485' + + + to_char(-485, '999') + '-485' + + + to_char(485, '9 9 9') + ' 4 8 5' + + + to_char(1485, '9,999') + ' 1,485' + + + to_char(1485, '9G999') + ' 1 485' + + + to_char(148.5, '999.999') + ' 148.500' + + + to_char(148.5, 'FM999.999') + '148.5' + + + to_char(148.5, 'FM999.990') + '148.500' + + + to_char(148.5, '999D999') + ' 148,500' + + + to_char(3148.5, '9G999D999') + ' 3 148,500' + + + to_char(-485, '999S') + '485-' + + + to_char(-485, '999MI') + '485-' + + + to_char(485, '999MI') + '485 ' + + + to_char(485, 'FM999MI') + '485' + + + to_char(485, 'PL999') + '+485' + + + to_char(485, 'SG999') + '+485' + + + to_char(-485, 'SG999') + '-485' + + + to_char(-485, '9SG99') + '4-85' + + + to_char(-485, '999PR') + '<485>' + + + to_char(485, 'L999') + 'DM 485' + + + to_char(485, 'RN') + '        CDLXXXV' + + + to_char(485, 'FMRN') + 'CDLXXXV' + + + to_char(5.2, 'FMRN') + 'V' + + + to_char(482, '999th') + ' 482nd' + + + to_char(485, '"Good number:"999') + 'Good number: 485' + + + to_char(485.8, '"Pre:"999" Post:" .999') + 'Pre: 485 Post: .800' + + + to_char(12, '99V999') + ' 12000' + + + to_char(12.4, '99V999') + ' 12400' + + + to_char(12.45, '99V9') + ' 125' + + + to_char(0.0004859, '9.99EEEE') + ' 4.86e-04' + + + +
+ +
diff --git a/doc/src/sgml/func/func-geometry.sgml b/doc/src/sgml/func/func-geometry.sgml new file mode 100644 index 0000000000000..ba203af3bd289 --- /dev/null +++ b/doc/src/sgml/func/func-geometry.sgml @@ -0,0 +1,1261 @@ + + Geometric Functions and Operators + + + The geometric types point, box, + lseg, line, path, + polygon, and circle have a large set of + native support functions and operators, shown in , , and . + + + + Geometric Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + geometric_type + point + geometric_type + + + Adds the coordinates of the second point to those of each + point of the first argument, thus performing translation. + Available for point, box, path, + circle. + + + box '(1,1),(0,0)' + point '(2,0)' + (3,1),(2,0) + + + + + + path + path + path + + + Concatenates two open paths (returns NULL if either path is closed). + + + path '[(0,0),(1,1)]' + path '[(2,2),(3,3),(4,4)]' + [(0,0),(1,1),(2,2),(3,3),(4,4)] + + + + + + geometric_type - point + geometric_type + + + Subtracts the coordinates of the second point from those + of each point of the first argument, thus performing translation. + Available for point, box, path, + circle. + + + box '(1,1),(0,0)' - point '(2,0)' + (-1,1),(-2,0) + + + + + + geometric_type * point + geometric_type + + + Multiplies each point of the first argument by the second + point (treating a point as being a complex number + represented by real and imaginary parts, and performing standard + complex multiplication). If one interprets + the second point as a vector, this is equivalent to + scaling the object's size and distance from the origin by the length + of the vector, and rotating it counterclockwise around the origin by + the vector's angle from the x axis. + Available for point, box,Rotating a + box with these operators only moves its corner points: the box is + still considered to have sides parallel to the axes. Hence the box's + size is not preserved, as a true rotation would do. + path, circle. + + + path '((0,0),(1,0),(1,1))' * point '(3.0,0)' + ((0,0),(3,0),(3,3)) + + + path '((0,0),(1,0),(1,1))' * point(cosd(45), sind(45)) + ((0,0),&zwsp;(0.7071067811865475,0.7071067811865475),&zwsp;(0,1.414213562373095)) + + + + + + geometric_type / point + geometric_type + + + Divides each point of the first argument by the second + point (treating a point as being a complex number + represented by real and imaginary parts, and performing standard + complex division). If one interprets + the second point as a vector, this is equivalent to + scaling the object's size and distance from the origin down by the + length of the vector, and rotating it clockwise around the origin by + the vector's angle from the x axis. + Available for point, box, path, + circle. + + + path '((0,0),(1,0),(1,1))' / point '(2.0,0)' + ((0,0),(0.5,0),(0.5,0.5)) + + + path '((0,0),(1,0),(1,1))' / point(cosd(45), sind(45)) + ((0,0),&zwsp;(0.7071067811865476,-0.7071067811865476),&zwsp;(1.4142135623730951,0)) + + + + + + @-@ geometric_type + double precision + + + Computes the total length. + Available for lseg, path. + + + @-@ path '[(0,0),(1,0),(1,1)]' + 2 + + + + + + @@ geometric_type + point + + + Computes the center point. + Available for box, lseg, + polygon, circle. + + + @@ box '(2,2),(0,0)' + (1,1) + + + + + + # geometric_type + integer + + + Returns the number of points. + Available for path, polygon. + + + # path '((1,0),(0,1),(-1,0))' + 3 + + + + + + geometric_type # geometric_type + point + + + Computes the point of intersection, or NULL if there is none. + Available for lseg, line. + + + lseg '[(0,0),(1,1)]' # lseg '[(1,0),(0,1)]' + (0.5,0.5) + + + + + + box # box + box + + + Computes the intersection of two boxes, or NULL if there is none. + + + box '(2,2),(-1,-1)' # box '(1,1),(-2,-2)' + (1,1),(-1,-1) + + + + + + geometric_type ## geometric_type + point + + + Computes the closest point to the first object on the second object. + Available for these pairs of types: + (point, box), + (point, lseg), + (point, line), + (lseg, box), + (lseg, lseg), + (line, lseg). + + + point '(0,0)' ## lseg '[(2,0),(0,2)]' + (1,1) + + + + + + geometric_type <-> geometric_type + double precision + + + Computes the distance between the objects. + Available for all seven geometric types, for all combinations + of point with another geometric type, and for + these additional pairs of types: + (box, lseg), + (lseg, line), + (polygon, circle) + (and the commutator cases). + + + circle '<(0,0),1>' <-> circle '<(5,0),1>' + 3 + + + + + + geometric_type @> geometric_type + boolean + + + Does first object contain second? + Available for these pairs of types: + (box, point), + (box, box), + (path, point), + (polygon, point), + (polygon, polygon), + (circle, point), + (circle, circle). + + + circle '<(0,0),2>' @> point '(1,1)' + t + + + + + + geometric_type <@ geometric_type + boolean + + + Is first object contained in or on second? + Available for these pairs of types: + (point, box), + (point, lseg), + (point, line), + (point, path), + (point, polygon), + (point, circle), + (box, box), + (lseg, box), + (lseg, line), + (polygon, polygon), + (circle, circle). + + + point '(1,1)' <@ circle '<(0,0),2>' + t + + + + + + geometric_type && geometric_type + boolean + + + Do these objects overlap? (One point in common makes this true.) + Available for box, polygon, + circle. + + + box '(1,1),(0,0)' && box '(2,2),(0,0)' + t + + + + + + geometric_type << geometric_type + boolean + + + Is first object strictly left of second? + Available for point, box, + polygon, circle. + + + circle '<(0,0),1>' << circle '<(5,0),1>' + t + + + + + + geometric_type >> geometric_type + boolean + + + Is first object strictly right of second? + Available for point, box, + polygon, circle. + + + circle '<(5,0),1>' >> circle '<(0,0),1>' + t + + + + + + geometric_type &< geometric_type + boolean + + + Does first object not extend to the right of second? + Available for box, polygon, + circle. + + + box '(1,1),(0,0)' &< box '(2,2),(0,0)' + t + + + + + + geometric_type &> geometric_type + boolean + + + Does first object not extend to the left of second? + Available for box, polygon, + circle. + + + box '(3,3),(0,0)' &> box '(2,2),(0,0)' + t + + + + + + geometric_type <<| geometric_type + boolean + + + Is first object strictly below second? + Available for point, box, polygon, + circle. + + + box '(3,3),(0,0)' <<| box '(5,5),(3,4)' + t + + + + + + geometric_type |>> geometric_type + boolean + + + Is first object strictly above second? + Available for point, box, polygon, + circle. + + + box '(5,5),(3,4)' |>> box '(3,3),(0,0)' + t + + + + + + geometric_type &<| geometric_type + boolean + + + Does first object not extend above second? + Available for box, polygon, + circle. + + + box '(1,1),(0,0)' &<| box '(2,2),(0,0)' + t + + + + + + geometric_type |&> geometric_type + boolean + + + Does first object not extend below second? + Available for box, polygon, + circle. + + + box '(3,3),(0,0)' |&> box '(2,2),(0,0)' + t + + + + + + box <^ box + boolean + + + Is first object below second (allows edges to touch)? + + + box '((1,1),(0,0))' <^ box '((2,2),(1,1))' + t + + + + + + box >^ box + boolean + + + Is first object above second (allows edges to touch)? + + + box '((2,2),(1,1))' >^ box '((1,1),(0,0))' + t + + + + + + geometric_type ?# geometric_type + boolean + + + Do these objects intersect? + Available for these pairs of types: + (box, box), + (lseg, box), + (lseg, lseg), + (lseg, line), + (line, box), + (line, line), + (path, path). + + + lseg '[(-1,0),(1,0)]' ?# box '(2,2),(-2,-2)' + t + + + + + + ?- line + boolean + + + ?- lseg + boolean + + + Is line horizontal? + + + ?- lseg '[(-1,0),(1,0)]' + t + + + + + + point ?- point + boolean + + + Are points horizontally aligned (that is, have same y coordinate)? + + + point '(1,0)' ?- point '(0,0)' + t + + + + + + ?| line + boolean + + + ?| lseg + boolean + + + Is line vertical? + + + ?| lseg '[(-1,0),(1,0)]' + f + + + + + + point ?| point + boolean + + + Are points vertically aligned (that is, have same x coordinate)? + + + point '(0,1)' ?| point '(0,0)' + t + + + + + + line ?-| line + boolean + + + lseg ?-| lseg + boolean + + + Are lines perpendicular? + + + lseg '[(0,0),(0,1)]' ?-| lseg '[(0,0),(1,0)]' + t + + + + + + line ?|| line + boolean + + + lseg ?|| lseg + boolean + + + Are lines parallel? + + + lseg '[(-1,0),(1,0)]' ?|| lseg '[(-1,2),(1,2)]' + t + + + + + + geometric_type ~= geometric_type + boolean + + + Are these objects the same? + Available for point, box, + polygon, circle. + + + polygon '((0,0),(1,1))' ~= polygon '((1,1),(0,0))' + t + + + + +
+ + + + Note that the same as operator, ~=, + represents the usual notion of equality for the point, + box, polygon, and circle types. + Some of the geometric types also have an = operator, but + = compares for equal areas only. + The other scalar comparison operators (<= and so + on), where available for these types, likewise compare areas. + + + + + + Before PostgreSQL 14, the point + is strictly below/above comparison operators point + <<| point and point + |>> point were respectively + called <^ and >^. These + names are still available, but are deprecated and will eventually be + removed. + + + + + Geometric Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + area + + area ( geometric_type ) + double precision + + + Computes area. + Available for box, path, circle. + A path input must be closed, else NULL is returned. + Also, if the path is self-intersecting, the result may be + meaningless. + + + area(box '(2,2),(0,0)') + 4 + + + + + + + center + + center ( geometric_type ) + point + + + Computes center point. + Available for box, circle. + + + center(box '(1,2),(0,0)') + (0.5,1) + + + + + + + diagonal + + diagonal ( box ) + lseg + + + Extracts box's diagonal as a line segment + (same as lseg(box)). + + + diagonal(box '(1,2),(0,0)') + [(1,2),(0,0)] + + + + + + + diameter + + diameter ( circle ) + double precision + + + Computes diameter of circle. + + + diameter(circle '<(0,0),2>') + 4 + + + + + + + height + + height ( box ) + double precision + + + Computes vertical size of box. + + + height(box '(1,2),(0,0)') + 2 + + + + + + + isclosed + + isclosed ( path ) + boolean + + + Is path closed? + + + isclosed(path '((0,0),(1,1),(2,0))') + t + + + + + + + isopen + + isopen ( path ) + boolean + + + Is path open? + + + isopen(path '[(0,0),(1,1),(2,0)]') + t + + + + + + + length + + length ( geometric_type ) + double precision + + + Computes the total length. + Available for lseg, path. + + + length(path '((-1,0),(1,0))') + 4 + + + + + + + npoints + + npoints ( geometric_type ) + integer + + + Returns the number of points. + Available for path, polygon. + + + npoints(path '[(0,0),(1,1),(2,0)]') + 3 + + + + + + + pclose + + pclose ( path ) + path + + + Converts path to closed form. + + + pclose(path '[(0,0),(1,1),(2,0)]') + ((0,0),(1,1),(2,0)) + + + + + + + popen + + popen ( path ) + path + + + Converts path to open form. + + + popen(path '((0,0),(1,1),(2,0))') + [(0,0),(1,1),(2,0)] + + + + + + + radius + + radius ( circle ) + double precision + + + Computes radius of circle. + + + radius(circle '<(0,0),2>') + 2 + + + + + + + slope + + slope ( point, point ) + double precision + + + Computes slope of a line drawn through the two points. + + + slope(point '(0,0)', point '(2,1)') + 0.5 + + + + + + + width + + width ( box ) + double precision + + + Computes horizontal size of box. + + + width(box '(1,2),(0,0)') + 1 + + + + +
+ + + Geometric Type Conversion Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + box + + box ( circle ) + box + + + Computes box inscribed within the circle. + + + box(circle '<(0,0),2>') + (1.414213562373095,1.414213562373095),&zwsp;(-1.414213562373095,-1.414213562373095) + + + + + + box ( point ) + box + + + Converts point to empty box. + + + box(point '(1,0)') + (1,0),(1,0) + + + + + + box ( point, point ) + box + + + Converts any two corner points to box. + + + box(point '(0,1)', point '(1,0)') + (1,1),(0,0) + + + + + + box ( polygon ) + box + + + Computes bounding box of polygon. + + + box(polygon '((0,0),(1,1),(2,0))') + (2,1),(0,0) + + + + + + + bound_box + + bound_box ( box, box ) + box + + + Computes bounding box of two boxes. + + + bound_box(box '(1,1),(0,0)', box '(4,4),(3,3)') + (4,4),(0,0) + + + + + + + circle + + circle ( box ) + circle + + + Computes smallest circle enclosing box. + + + circle(box '(1,1),(0,0)') + <(0.5,0.5),0.7071067811865476> + + + + + + circle ( point, double precision ) + circle + + + Constructs circle from center and radius. + + + circle(point '(0,0)', 2.0) + <(0,0),2> + + + + + + circle ( polygon ) + circle + + + Converts polygon to circle. The circle's center is the mean of the + positions of the polygon's points, and the radius is the average + distance of the polygon's points from that center. + + + circle(polygon '((0,0),(1,3),(2,0))') + <(1,1),1.6094757082487299> + + + + + + + line + + line ( point, point ) + line + + + Converts two points to the line through them. + + + line(point '(-1,0)', point '(1,0)') + {0,-1,0} + + + + + + + lseg + + lseg ( box ) + lseg + + + Extracts box's diagonal as a line segment. + + + lseg(box '(1,0),(-1,0)') + [(1,0),(-1,0)] + + + + + + lseg ( point, point ) + lseg + + + Constructs line segment from two endpoints. + + + lseg(point '(-1,0)', point '(1,0)') + [(-1,0),(1,0)] + + + + + + + path + + path ( polygon ) + path + + + Converts polygon to a closed path with the same list of points. + + + path(polygon '((0,0),(1,1),(2,0))') + ((0,0),(1,1),(2,0)) + + + + + + + point + + point ( double precision, double precision ) + point + + + Constructs point from its coordinates. + + + point(23.4, -44.5) + (23.4,-44.5) + + + + + + point ( box ) + point + + + Computes center of box. + + + point(box '(1,0),(-1,0)') + (0,0) + + + + + + point ( circle ) + point + + + Computes center of circle. + + + point(circle '<(0,0),2>') + (0,0) + + + + + + point ( lseg ) + point + + + Computes center of line segment. + + + point(lseg '[(-1,0),(1,0)]') + (0,0) + + + + + + point ( polygon ) + point + + + Computes center of polygon (the mean of the + positions of the polygon's points). + + + point(polygon '((0,0),(1,1),(2,0))') + (1,0.3333333333333333) + + + + + + + polygon + + polygon ( box ) + polygon + + + Converts box to a 4-point polygon. + + + polygon(box '(1,1),(0,0)') + ((0,0),(0,1),(1,1),(1,0)) + + + + + + polygon ( circle ) + polygon + + + Converts circle to a 12-point polygon. + + + polygon(circle '<(0,0),2>') + ((-2,0),&zwsp;(-1.7320508075688774,0.9999999999999999),&zwsp;(-1.0000000000000002,1.7320508075688772),&zwsp;(-1.2246063538223773e-16,2),&zwsp;(0.9999999999999996,1.7320508075688774),&zwsp;(1.732050807568877,1.0000000000000007),&zwsp;(2,2.4492127076447545e-16),&zwsp;(1.7320508075688776,-0.9999999999999994),&zwsp;(1.0000000000000009,-1.7320508075688767),&zwsp;(3.673819061467132e-16,-2),&zwsp;(-0.9999999999999987,-1.732050807568878),&zwsp;(-1.7320508075688767,-1.0000000000000009)) + + + + + + polygon ( integer, circle ) + polygon + + + Converts circle to an n-point polygon. + + + polygon(4, circle '<(3,0),1>') + ((2,0),&zwsp;(3,1),&zwsp;(4,1.2246063538223773e-16),&zwsp;(3,-1)) + + + + + + polygon ( path ) + polygon + + + Converts closed path to a polygon with the same list of points. + + + polygon(path '((0,0),(1,1),(2,0))') + ((0,0),(1,1),(2,0)) + + + + + +
+ + + It is possible to access the two component numbers of a point + as though the point were an array with indexes 0 and 1. For example, if + t.p is a point column then + SELECT p[0] FROM t retrieves the X coordinate and + UPDATE t SET p[1] = ... changes the Y coordinate. + In the same way, a value of type box or lseg can be treated + as an array of two point values. + + +
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml new file mode 100644 index 0000000000000..00f64f50ceb8c --- /dev/null +++ b/doc/src/sgml/func/func-info.sgml @@ -0,0 +1,3970 @@ + + System Information Functions and Operators + + + The functions described in this section are used to obtain various + information about a PostgreSQL installation. + + + + Session Information Functions + + + shows several + functions that extract session and system information. + + + + In addition to the functions listed in this section, there are a number of + functions related to the statistics system that also provide system + information. See for more + information. + + + + Session Information Functions + + + + + Function + + + Description + + + + + + + + + current_catalog + + current_catalog + name + + + + current_database + + current_database () + name + + + Returns the name of the current database. (Databases are + called catalogs in the SQL standard, + so current_catalog is the standard's + spelling.) + + + + + + + current_query + + current_query () + text + + + Returns the text of the currently executing query, as submitted + by the client (which might contain more than one statement). + + + + + + + current_role + + current_role + name + + + This is equivalent to current_user. + + + + + + + current_schema + + + schema + current + + current_schema + name + + + current_schema () + name + + + Returns the name of the schema that is first in the search path (or a + null value if the search path is empty). This is the schema that will + be used for any tables or other named objects that are created without + specifying a target schema. + + + + + + + current_schemas + + + search path + current + + current_schemas ( include_implicit boolean ) + name[] + + + Returns an array of the names of all schemas presently in the + effective search path, in their priority order. (Items in the current + setting that do not correspond to + existing, searchable schemas are omitted.) If the Boolean argument + is true, then implicitly-searched system schemas + such as pg_catalog are included in the result. + + + + + + + current_user + + + user + current + + current_user + name + + + Returns the user name of the current execution context. + + + + + + + inet_client_addr + + inet_client_addr () + inet + + + Returns the IP address of the current client, + or NULL if the current connection is via a + Unix-domain socket. + + + + + + + inet_client_port + + inet_client_port () + integer + + + Returns the IP port number of the current client, + or NULL if the current connection is via a + Unix-domain socket. + + + + + + + inet_server_addr + + inet_server_addr () + inet + + + Returns the IP address on which the server accepted the current + connection, + or NULL if the current connection is via a + Unix-domain socket. + + + + + + + inet_server_port + + inet_server_port () + integer + + + Returns the IP port number on which the server accepted the current + connection, + or NULL if the current connection is via a + Unix-domain socket. + + + + + + + pg_backend_pid + + pg_backend_pid () + integer + + + Returns the process ID of the server process attached to the current + session. + + + + + + + pg_blocking_pids + + pg_blocking_pids ( integer ) + integer[] + + + Returns an array of the process ID(s) of the sessions that are + blocking the server process with the specified process ID from + acquiring a lock, or an empty array if there is no such server process + or it is not blocked. + + + One server process blocks another if it either holds a lock that + conflicts with the blocked process's lock request (hard block), or is + waiting for a lock that would conflict with the blocked process's lock + request and is ahead of it in the wait queue (soft block). When using + parallel queries the result always lists client-visible process IDs + (that is, pg_backend_pid results) even if the + actual lock is held or awaited by a child worker process. As a result + of that, there may be duplicated PIDs in the result. Also note that + when a prepared transaction holds a conflicting lock, it will be + represented by a zero process ID. + + + Frequent calls to this function could have some impact on database + performance, because it needs exclusive access to the lock manager's + shared state for a short time. + + + + + + + pg_conf_load_time + + pg_conf_load_time () + timestamp with time zone + + + Returns the time when the server configuration files were last loaded. + If the current session was alive at the time, this will be the time + when the session itself re-read the configuration files (so the + reading will vary a little in different sessions). Otherwise it is + the time when the postmaster process re-read the configuration files. + + + + + + + pg_current_logfile + + + Logging + pg_current_logfile function + + + current_logfiles + and the pg_current_logfile function + + + Logging + current_logfiles file and the pg_current_logfile + function + + pg_current_logfile ( text ) + text + + + Returns the path name of the log file currently in use by the logging + collector. The path includes the + directory and the individual log file name. The result + is NULL if the logging collector is disabled. + When multiple log files exist, each in a different + format, pg_current_logfile without an argument + returns the path of the file having the first format found in the + ordered list: stderr, + csvlog, jsonlog. + NULL is returned if no log file has any of these + formats. + To request information about a specific log file format, supply + either csvlog, jsonlog or + stderr as the + value of the optional parameter. The result is NULL + if the log format requested is not configured in + . + The result reflects the contents of + the current_logfiles file. + + + This function is restricted to superusers and roles with privileges of + the pg_monitor role by default, but other users can + be granted EXECUTE to run the function. + + + + + + + pg_get_loaded_modules + + pg_get_loaded_modules () + setof record + ( module_name text, + version text, + file_name text ) + + + Returns a list of the loadable modules that are loaded into the + current server session. The module_name + and version fields are NULL unless the + module author supplied values for them using + the PG_MODULE_MAGIC_EXT macro. + The file_name field gives the file + name of the module (shared library). + + + + + + + pg_my_temp_schema + + pg_my_temp_schema () + oid + + + Returns the OID of the current session's temporary schema, or zero if + it has none (because it has not created any temporary tables). + + + + + + + pg_is_other_temp_schema + + pg_is_other_temp_schema ( oid ) + boolean + + + Returns true if the given OID is the OID of another session's + temporary schema. (This can be useful, for example, to exclude other + sessions' temporary tables from a catalog display.) + + + + + + + pg_jit_available + + pg_jit_available () + boolean + + + Returns true if a JIT compiler extension is + available (see ) and the + configuration parameter is set to + on. + + + + + + + pg_numa_available + + pg_numa_available () + boolean + + + Returns true if the server has been compiled with NUMA support. + + + + + + + pg_listening_channels + + pg_listening_channels () + setof text + + + Returns the set of names of asynchronous notification channels that + the current session is listening to. + + + + + + + pg_notification_queue_usage + + pg_notification_queue_usage () + double precision + + + Returns the fraction (0–1) of the asynchronous notification + queue's maximum size that is currently occupied by notifications that + are waiting to be processed. + See and + for more information. + + + + + + + pg_postmaster_start_time + + pg_postmaster_start_time () + timestamp with time zone + + + Returns the time when the server started. + + + + + + + pg_safe_snapshot_blocking_pids + + pg_safe_snapshot_blocking_pids ( integer ) + integer[] + + + Returns an array of the process ID(s) of the sessions that are blocking + the server process with the specified process ID from acquiring a safe + snapshot, or an empty array if there is no such server process or it + is not blocked. + + + A session running a SERIALIZABLE transaction blocks + a SERIALIZABLE READ ONLY DEFERRABLE transaction + from acquiring a snapshot until the latter determines that it is safe + to avoid taking any predicate locks. See + for more information about + serializable and deferrable transactions. + + + Frequent calls to this function could have some impact on database + performance, because it needs access to the predicate lock manager's + shared state for a short time. + + + + + + + pg_trigger_depth + + pg_trigger_depth () + integer + + + Returns the current nesting level + of PostgreSQL triggers (0 if not called, + directly or indirectly, from inside a trigger). + + + + + + + session_user + + session_user + name + + + Returns the session user's name. + + + + + + + system_user + + system_user + text + + + Returns the authentication method and the identity (if any) that the + user presented during the authentication cycle before they were + assigned a database role. It is represented as + auth_method:identity or + NULL if the user has not been authenticated (for + example if Trust authentication has + been used). + + + + + + + user + + user + name + + + This is equivalent to current_user. + + + + +
+ + + + current_catalog, + current_role, + current_schema, + current_user, + session_user, + and user have special syntactic status + in SQL: they must be called without trailing + parentheses. In PostgreSQL, parentheses can optionally be used with + current_schema, but not with the others. + + + + + The session_user is normally the user who initiated + the current database connection; but superusers can change this setting + with . + The current_user is the user identifier + that is applicable for permission checking. Normally it is equal + to the session user, but it can be changed with + . + It also changes during the execution of + functions with the attribute SECURITY DEFINER. + In Unix parlance, the session user is the real user and + the current user is the effective user. + current_role and user are + synonyms for current_user. (The SQL standard draws + a distinction between current_role + and current_user, but PostgreSQL + does not, since it unifies users and roles into a single kind of entity.) + + +
+ + + Access Privilege Inquiry Functions + + + privilege + querying + + + + lists functions that + allow querying object access privileges programmatically. + (See for more information about + privileges.) + In these functions, the user whose privileges are being inquired about + can be specified by name or by OID + (pg_authid.oid), or if + the name is given as public then the privileges of the + PUBLIC pseudo-role are checked. Also, the user + argument can be omitted entirely, in which case + the current_user is assumed. + The object that is being inquired about can be specified either by name or + by OID, too. When specifying by name, a schema name can be included if + relevant. + The access privilege of interest is specified by a text string, which must + evaluate to one of the appropriate privilege keywords for the object's type + (e.g., SELECT). Optionally, WITH GRANT + OPTION can be added to a privilege type to test whether the + privilege is held with grant option. Also, multiple privilege types can be + listed separated by commas, in which case the result will be true if any of + the listed privileges is held. (Case of the privilege string is not + significant, and extra whitespace is allowed between but not within + privilege names.) + Some examples: + +SELECT has_table_privilege('myschema.mytable', 'select'); +SELECT has_table_privilege('joe', 'mytable', 'INSERT, SELECT WITH GRANT OPTION'); + + + + + Access Privilege Inquiry Functions + + + + + Function + + + Description + + + + + + + + + has_any_column_privilege + + has_any_column_privilege ( + user name or oid, + table text or oid, + privilege text ) + boolean + + + Does user have privilege for any column of table? + This succeeds either if the privilege is held for the whole table, or + if there is a column-level grant of the privilege for at least one + column. + Allowable privilege types are + SELECT, INSERT, + UPDATE, and REFERENCES. + + + + + + + has_column_privilege + + has_column_privilege ( + user name or oid, + table text or oid, + column text or smallint, + privilege text ) + boolean + + + Does user have privilege for the specified table column? + This succeeds either if the privilege is held for the whole table, or + if there is a column-level grant of the privilege for the column. + The column can be specified by name or by attribute number + (pg_attribute.attnum). + Allowable privilege types are + SELECT, INSERT, + UPDATE, and REFERENCES. + + + + + + + has_database_privilege + + has_database_privilege ( + user name or oid, + database text or oid, + privilege text ) + boolean + + + Does user have privilege for database? + Allowable privilege types are + CREATE, + CONNECT, + TEMPORARY, and + TEMP (which is equivalent to + TEMPORARY). + + + + + + + has_foreign_data_wrapper_privilege + + has_foreign_data_wrapper_privilege ( + user name or oid, + fdw text or oid, + privilege text ) + boolean + + + Does user have privilege for foreign-data wrapper? + The only allowable privilege type is USAGE. + + + + + + + has_function_privilege + + has_function_privilege ( + user name or oid, + function text or oid, + privilege text ) + boolean + + + Does user have privilege for function? + The only allowable privilege type is EXECUTE. + + + When specifying a function by name rather than by OID, the allowed + input is the same as for the regprocedure data type (see + ). + An example is: + +SELECT has_function_privilege('joeuser', 'myfunc(int, text)', 'execute'); + + + + + + + + has_language_privilege + + has_language_privilege ( + user name or oid, + language text or oid, + privilege text ) + boolean + + + Does user have privilege for language? + The only allowable privilege type is USAGE. + + + + + + + has_largeobject_privilege + + has_largeobject_privilege ( + user name or oid, + largeobject oid, + privilege text ) + boolean + + + Does user have privilege for large object? + Allowable privilege types are + SELECT and UPDATE. + + + + + + + has_parameter_privilege + + has_parameter_privilege ( + user name or oid, + parameter text, + privilege text ) + boolean + + + Does user have privilege for configuration parameter? + The parameter name is case-insensitive. + Allowable privilege types are SET + and ALTER SYSTEM. + + + + + + + has_schema_privilege + + has_schema_privilege ( + user name or oid, + schema text or oid, + privilege text ) + boolean + + + Does user have privilege for schema? + Allowable privilege types are + CREATE and + USAGE. + + + + + + + has_sequence_privilege + + has_sequence_privilege ( + user name or oid, + sequence text or oid, + privilege text ) + boolean + + + Does user have privilege for sequence? + Allowable privilege types are + USAGE, + SELECT, and + UPDATE. + + + + + + + has_server_privilege + + has_server_privilege ( + user name or oid, + server text or oid, + privilege text ) + boolean + + + Does user have privilege for foreign server? + The only allowable privilege type is USAGE. + + + + + + + has_table_privilege + + has_table_privilege ( + user name or oid, + table text or oid, + privilege text ) + boolean + + + Does user have privilege for table? + Allowable privilege types + are SELECT, INSERT, + UPDATE, DELETE, + TRUNCATE, REFERENCES, + TRIGGER, and MAINTAIN. + + + + + + + has_tablespace_privilege + + has_tablespace_privilege ( + user name or oid, + tablespace text or oid, + privilege text ) + boolean + + + Does user have privilege for tablespace? + The only allowable privilege type is CREATE. + + + + + + + has_type_privilege + + has_type_privilege ( + user name or oid, + type text or oid, + privilege text ) + boolean + + + Does user have privilege for data type? + The only allowable privilege type is USAGE. + When specifying a type by name rather than by OID, the allowed input + is the same as for the regtype data type (see + ). + + + + + + + pg_has_role + + pg_has_role ( + user name or oid, + role text or oid, + privilege text ) + boolean + + + Does user have privilege for role? + Allowable privilege types are + MEMBER, USAGE, + and SET. + MEMBER denotes direct or indirect membership in + the role without regard to what specific privileges may be conferred. + USAGE denotes whether the privileges of the role + are immediately available without doing SET ROLE, + while SET denotes whether it is possible to change + to the role using the SET ROLE command. + WITH ADMIN OPTION or WITH GRANT + OPTION can be added to any of these privilege types to + test whether the ADMIN privilege is held (all + six spellings test the same thing). + This function does not allow the special case of + setting user to public, + because the PUBLIC pseudo-role can never be a member of real roles. + + + + + + + row_security_active + + row_security_active ( + table text or oid ) + boolean + + + Is row-level security active for the specified table in the context of + the current user and current environment? + + + + +
+ + + shows the operators + available for the aclitem type, which is the catalog + representation of access privileges. See + for information about how to read access privilege values. + + + + <type>aclitem</type> Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + + aclitemeq + + aclitem = aclitem + boolean + + + Are aclitems equal? (Notice that + type aclitem lacks the usual set of comparison + operators; it has only equality. In turn, aclitem + arrays can only be compared for equality.) + + + 'calvin=r*w/hobbes'::aclitem = 'calvin=r*w*/hobbes'::aclitem + f + + + + + + + aclcontains + + aclitem[] @> aclitem + boolean + + + Does array contain the specified privileges? (This is true if there + is an array entry that matches the aclitem's grantee and + grantor, and has at least the specified set of privileges.) + + + '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] @> 'calvin=r*/hobbes'::aclitem + t + + + + + + aclitem[] ~ aclitem + boolean + + + This is a deprecated alias for @>. + + + '{calvin=r*w/hobbes,hobbes=r*w*/postgres}'::aclitem[] ~ 'calvin=r*/hobbes'::aclitem + t + + + + +
+ + + shows some additional + functions to manage the aclitem type. + + + + <type>aclitem</type> Functions + + + + + Function + + + Description + + + + + + + + + acldefault + + acldefault ( + type "char", + ownerId oid ) + aclitem[] + + + Constructs an aclitem array holding the default access + privileges for an object of type type belonging + to the role with OID ownerId. This represents + the access privileges that will be assumed when an object's + ACL entry is null. (The default access privileges + are described in .) + The type parameter must be one of + 'c' for COLUMN, + 'r' for TABLE and table-like objects, + 's' for SEQUENCE, + 'd' for DATABASE, + 'f' for FUNCTION or PROCEDURE, + 'l' for LANGUAGE, + 'L' for LARGE OBJECT, + 'n' for SCHEMA, + 'p' for PARAMETER, + 't' for TABLESPACE, + 'F' for FOREIGN DATA WRAPPER, + 'S' for FOREIGN SERVER, + or + 'T' for TYPE or DOMAIN. + + + + + + + aclexplode + + aclexplode ( aclitem[] ) + setof record + ( grantor oid, + grantee oid, + privilege_type text, + is_grantable boolean ) + + + Returns the aclitem array as a set of rows. + If the grantee is the pseudo-role PUBLIC, it is represented by zero in + the grantee column. Each granted privilege is + represented as SELECT, INSERT, + etc (see for a full list). + Note that each privilege is broken out as a separate row, so + only one keyword appears in the privilege_type + column. + + + + + + + makeaclitem + + makeaclitem ( + grantee oid, + grantor oid, + privileges text, + is_grantable boolean ) + aclitem + + + Constructs an aclitem with the given properties. + privileges is a comma-separated list of + privilege names such as SELECT, + INSERT, etc, all of which are set in the + result. (Case of the privilege string is not significant, and + extra whitespace is allowed between but not within privilege + names.) + + + + +
+ +
+ + + Schema Visibility Inquiry Functions + + + shows functions that + determine whether a certain object is visible in the + current schema search path. + For example, a table is said to be visible if its + containing schema is in the search path and no table of the same + name appears earlier in the search path. This is equivalent to the + statement that the table can be referenced by name without explicit + schema qualification. Thus, to list the names of all visible tables: + +SELECT relname FROM pg_class WHERE pg_table_is_visible(oid); + + For functions and operators, an object in the search path is said to be + visible if there is no object of the same name and argument data + type(s) earlier in the path. For operator classes and families, + both the name and the associated index access method are considered. + + + + search path + object visibility + + + + Schema Visibility Inquiry Functions + + + + + Function + + + Description + + + + + + + + + pg_collation_is_visible + + pg_collation_is_visible ( collation oid ) + boolean + + + Is collation visible in search path? + + + + + + + pg_conversion_is_visible + + pg_conversion_is_visible ( conversion oid ) + boolean + + + Is conversion visible in search path? + + + + + + + pg_function_is_visible + + pg_function_is_visible ( function oid ) + boolean + + + Is function visible in search path? + (This also works for procedures and aggregates.) + + + + + + + pg_opclass_is_visible + + pg_opclass_is_visible ( opclass oid ) + boolean + + + Is operator class visible in search path? + + + + + + + pg_operator_is_visible + + pg_operator_is_visible ( operator oid ) + boolean + + + Is operator visible in search path? + + + + + + + pg_opfamily_is_visible + + pg_opfamily_is_visible ( opclass oid ) + boolean + + + Is operator family visible in search path? + + + + + + + pg_statistics_obj_is_visible + + pg_statistics_obj_is_visible ( stat oid ) + boolean + + + Is statistics object visible in search path? + + + + + + + pg_table_is_visible + + pg_table_is_visible ( table oid ) + boolean + + + Is table visible in search path? + (This works for all types of relations, including views, materialized + views, indexes, sequences and foreign tables.) + + + + + + + pg_ts_config_is_visible + + pg_ts_config_is_visible ( config oid ) + boolean + + + Is text search configuration visible in search path? + + + + + + + pg_ts_dict_is_visible + + pg_ts_dict_is_visible ( dict oid ) + boolean + + + Is text search dictionary visible in search path? + + + + + + + pg_ts_parser_is_visible + + pg_ts_parser_is_visible ( parser oid ) + boolean + + + Is text search parser visible in search path? + + + + + + + pg_ts_template_is_visible + + pg_ts_template_is_visible ( template oid ) + boolean + + + Is text search template visible in search path? + + + + + + + pg_type_is_visible + + pg_type_is_visible ( type oid ) + boolean + + + Is type (or domain) visible in search path? + + + + +
+ + + All these functions require object OIDs to identify the object to be + checked. If you want to test an object by name, it is convenient to use + the OID alias types (regclass, regtype, + regprocedure, regoperator, regconfig, + or regdictionary), + for example: + +SELECT pg_type_is_visible('myschema.widget'::regtype); + + Note that it would not make much sense to test a non-schema-qualified + type name in this way — if the name can be recognized at all, it must be visible. + + +
+ + + System Catalog Information Functions + + + lists functions that + extract information from the system catalogs. + + + + System Catalog Information Functions + + + + + Function + + + Description + + + + + + + + + format_type + + format_type ( type oid, typemod integer ) + text + + + Returns the SQL name for a data type that is identified by its type + OID and possibly a type modifier. Pass NULL for the type modifier if + no specific modifier is known. + + + + + + + pg_basetype + + pg_basetype ( regtype ) + regtype + + + Returns the OID of the base type of a domain identified by its + type OID. If the argument is the OID of a non-domain type, + returns the argument as-is. Returns NULL if the argument is + not a valid type OID. If there's a chain of domain dependencies, + it will recurse until finding the base type. + + + Assuming CREATE DOMAIN mytext AS text: + + + pg_basetype('mytext'::regtype) + text + + + + + + + pg_char_to_encoding + + pg_char_to_encoding ( encoding name ) + integer + + + Converts the supplied encoding name into an integer representing the + internal identifier used in some system catalog tables. + Returns -1 if an unknown encoding name is provided. + + + + + + + pg_encoding_to_char + + pg_encoding_to_char ( encoding integer ) + name + + + Converts the integer used as the internal identifier of an encoding in some + system catalog tables into a human-readable string. + Returns an empty string if an invalid encoding number is provided. + + + + + + + pg_get_catalog_foreign_keys + + pg_get_catalog_foreign_keys () + setof record + ( fktable regclass, + fkcols text[], + pktable regclass, + pkcols text[], + is_array boolean, + is_opt boolean ) + + + Returns a set of records describing the foreign key relationships + that exist within the PostgreSQL system + catalogs. + The fktable column contains the name of the + referencing catalog, and the fkcols column + contains the name(s) of the referencing column(s). Similarly, + the pktable column contains the name of the + referenced catalog, and the pkcols column + contains the name(s) of the referenced column(s). + If is_array is true, the last referencing + column is an array, each of whose elements should match some entry + in the referenced catalog. + If is_opt is true, the referencing column(s) + are allowed to contain zeroes instead of a valid reference. + + + + + + + pg_get_constraintdef + + pg_get_constraintdef ( constraint oid , pretty boolean ) + text + + + Reconstructs the creating command for a constraint. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_expr + + pg_get_expr ( expr pg_node_tree, relation oid , pretty boolean ) + text + + + Decompiles the internal form of an expression stored in the system + catalogs, such as the default value for a column. If the expression + might contain Vars, specify the OID of the relation they refer to as + the second parameter; if no Vars are expected, passing zero is + sufficient. + + + + + + + pg_get_functiondef + + pg_get_functiondef ( func oid ) + text + + + Reconstructs the creating command for a function or procedure. + (This is a decompiled reconstruction, not the original text + of the command.) + The result is a complete CREATE OR REPLACE FUNCTION + or CREATE OR REPLACE PROCEDURE statement. + + + + + + + pg_get_function_arguments + + pg_get_function_arguments ( func oid ) + text + + + Reconstructs the argument list of a function or procedure, in the form + it would need to appear in within CREATE FUNCTION + (including default values). + + + + + + + pg_get_function_identity_arguments + + pg_get_function_identity_arguments ( func oid ) + text + + + Reconstructs the argument list necessary to identify a function or + procedure, in the form it would need to appear in within commands such + as ALTER FUNCTION. This form omits default values. + + + + + + + pg_get_function_result + + pg_get_function_result ( func oid ) + text + + + Reconstructs the RETURNS clause of a function, in + the form it would need to appear in within CREATE + FUNCTION. Returns NULL for a procedure. + + + + + + + pg_get_indexdef + + pg_get_indexdef ( index oid , column integer, pretty boolean ) + text + + + Reconstructs the creating command for an index. + (This is a decompiled reconstruction, not the original text + of the command.) If column is supplied and is + not zero, only the definition of that column is reconstructed. + + + + + + + pg_get_keywords + + pg_get_keywords () + setof record + ( word text, + catcode "char", + barelabel boolean, + catdesc text, + baredesc text ) + + + Returns a set of records describing the SQL keywords recognized by the + server. The word column contains the + keyword. The catcode column contains a + category code: U for an unreserved + keyword, C for a keyword that can be a column + name, T for a keyword that can be a type or + function name, or R for a fully reserved keyword. + The barelabel column + contains true if the keyword can be used as + a bare column label in SELECT lists, + or false if it can only be used + after AS. + The catdesc column contains a + possibly-localized string describing the keyword's category. + The baredesc column contains a + possibly-localized string describing the keyword's column label status. + + + + + + + pg_get_partition_constraintdef + + pg_get_partition_constraintdef ( table oid ) + text + + + Reconstructs the definition of a partition constraint. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_partkeydef + + pg_get_partkeydef ( table oid ) + text + + + Reconstructs the definition of a partitioned table's partition + key, in the form it would have in the PARTITION + BY clause of CREATE TABLE. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_propgraphdef + + pg_get_propgraphdef ( propgraph oid ) + text + + + Reconstructs the creating command for a property graph. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_ruledef + + pg_get_ruledef ( rule oid , pretty boolean ) + text + + + Reconstructs the creating command for a rule. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_serial_sequence + + pg_get_serial_sequence ( table text, column text ) + text + + + Returns the name of the sequence associated with a column, + or NULL if no sequence is associated with the column. + If the column is an identity column, the associated sequence is the + sequence internally created for that column. + For columns created using one of the serial types + (serial, smallserial, bigserial), + it is the sequence created for that serial column definition. + In the latter case, the association can be modified or removed + with ALTER SEQUENCE OWNED BY. + (This function probably should have been + called pg_get_owned_sequence; its current name + reflects the fact that it has historically been used with serial-type + columns.) The first parameter is a table name with optional + schema, and the second parameter is a column name. Because the first + parameter potentially contains both schema and table names, it is + parsed per usual SQL rules, meaning it is lower-cased by default. + The second parameter, being just a column name, is treated literally + and so has its case preserved. The result is suitably formatted + for passing to the sequence functions (see + ). + + + A typical use is in reading the current value of the sequence for an + identity or serial column, for example: + +SELECT currval(pg_get_serial_sequence('sometable', 'id')); + + + + + + + + pg_get_statisticsobjdef + + pg_get_statisticsobjdef ( statobj oid ) + text + + + Reconstructs the creating command for an extended statistics object. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_triggerdef + +pg_get_triggerdef ( trigger oid , pretty boolean ) + text + + + Reconstructs the creating command for a trigger. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + + + + pg_get_userbyid + + pg_get_userbyid ( role oid ) + name + + + Returns a role's name given its OID. + + + + + + + pg_get_viewdef + + pg_get_viewdef ( view oid , pretty boolean ) + text + + + Reconstructs the underlying SELECT command for a + view or materialized view. (This is a decompiled reconstruction, not + the original text of the command.) + + + + + + pg_get_viewdef ( view oid, wrap_column integer ) + text + + + Reconstructs the underlying SELECT command for a + view or materialized view. (This is a decompiled reconstruction, not + the original text of the command.) In this form of the function, + pretty-printing is always enabled, and long lines are wrapped to try + to keep them shorter than the specified number of columns. + + + + + + pg_get_viewdef ( view text , pretty boolean ) + text + + + Reconstructs the underlying SELECT command for a + view or materialized view, working from a textual name for the view + rather than its OID. (This is deprecated; use the OID variant + instead.) + + + + + + + pg_index_column_has_property + + pg_index_column_has_property ( index regclass, column integer, property text ) + boolean + + + Tests whether an index column has the named property. + Common index column properties are listed in + . + (Note that extension access methods can define additional property + names for their indexes.) + NULL is returned if the property name is not known + or does not apply to the particular object, or if the OID or column + number does not identify a valid object. + + + + + + + pg_index_has_property + + pg_index_has_property ( index regclass, property text ) + boolean + + + Tests whether an index has the named property. + Common index properties are listed in + . + (Note that extension access methods can define additional property + names for their indexes.) + NULL is returned if the property name is not known + or does not apply to the particular object, or if the OID does not + identify a valid object. + + + + + + + pg_indexam_has_property + + pg_indexam_has_property ( am oid, property text ) + boolean + + + Tests whether an index access method has the named property. + Access method properties are listed in + . + NULL is returned if the property name is not known + or does not apply to the particular object, or if the OID does not + identify a valid object. + + + + + + + pg_options_to_table + + pg_options_to_table ( options_array text[] ) + setof record + ( option_name text, + option_value text ) + + + Returns the set of storage options represented by a value from + pg_class.reloptions or + pg_attribute.attoptions. + + + + + + + pg_settings_get_flags + + pg_settings_get_flags ( guc text ) + text[] + + + Returns an array of the flags associated with the given GUC, or + NULL if it does not exist. The result is + an empty array if the GUC exists but there are no flags to show. + Only the most useful flags listed in + are exposed. + + + + + + + pg_tablespace_databases + + pg_tablespace_databases ( tablespace oid ) + setof oid + + + Returns the set of OIDs of databases that have objects stored in the + specified tablespace. If this function returns any rows, the + tablespace is not empty and cannot be dropped. To identify the specific + objects populating the tablespace, you will need to connect to the + database(s) identified by pg_tablespace_databases + and query their pg_class catalogs. + + + + + + + pg_tablespace_location + + pg_tablespace_location ( tablespace oid ) + text + + + Returns the file system path that this tablespace is located in. + + + + + + + pg_typeof + + pg_typeof ( "any" ) + regtype + + + Returns the OID of the data type of the value that is passed to it. + This can be helpful for troubleshooting or dynamically constructing + SQL queries. The function is declared as + returning regtype, which is an OID alias type (see + ); this means that it is the same as an + OID for comparison purposes but displays as a type name. + + + pg_typeof(33) + integer + + + + + + + COLLATION FOR + + COLLATION FOR ( "any" ) + text + + + Returns the name of the collation of the value that is passed to it. + The value is quoted and schema-qualified if necessary. If no + collation was derived for the argument expression, + then NULL is returned. If the argument is not of a + collatable data type, then an error is raised. + + + COLLATION FOR ('foo'::text) + "default" + + + COLLATION FOR ('foo' COLLATE "de_DE") + "de_DE" + + + + + + + to_regclass + + to_regclass ( text ) + regclass + + + Translates a textual relation name to its OID. A similar result is + obtained by casting the string to type regclass (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regdatabase + + to_regdatabase ( text ) + regdatabase + + + Translates a textual database name to its OID. A similar result is + obtained by casting the string to type regdatabase (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regcollation + + to_regcollation ( text ) + regcollation + + + Translates a textual collation name to its OID. A similar result is + obtained by casting the string to type regcollation (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regnamespace + + to_regnamespace ( text ) + regnamespace + + + Translates a textual schema name to its OID. A similar result is + obtained by casting the string to type regnamespace (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regoper + + to_regoper ( text ) + regoper + + + Translates a textual operator name to its OID. A similar result is + obtained by casting the string to type regoper (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found or is ambiguous. + + + + + + + to_regoperator + + to_regoperator ( text ) + regoperator + + + Translates a textual operator name (with parameter types) to its OID. A similar result is + obtained by casting the string to type regoperator (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regproc + + to_regproc ( text ) + regproc + + + Translates a textual function or procedure name to its OID. A similar result is + obtained by casting the string to type regproc (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found or is ambiguous. + + + + + + + to_regprocedure + + to_regprocedure ( text ) + regprocedure + + + Translates a textual function or procedure name (with argument types) to its OID. A similar result is + obtained by casting the string to type regprocedure (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regrole + + to_regrole ( text ) + regrole + + + Translates a textual role name to its OID. A similar result is + obtained by casting the string to type regrole (see + ); however, this function will return + NULL rather than throwing an error if the name is + not found. + + + + + + + to_regtype + + to_regtype ( text ) + regtype + + + Parses a string of text, extracts a potential type name from it, + and translates that name into a type OID. A syntax error in the + string will result in an error; but if the string is a + syntactically valid type name that happens not to be found in the + catalogs, the result is NULL. A similar result + is obtained by casting the string to type regtype + (see ), except that that will throw + error for name not found. + + + + + + + to_regtypemod + + to_regtypemod ( text ) + integer + + + Parses a string of text, extracts a potential type name from it, + and translates its type modifier, if any. A syntax error in the + string will result in an error; but if the string is a + syntactically valid type name that happens not to be found in the + catalogs, the result is NULL. The result is + -1 if no type modifier is present. + + + to_regtypemod can be combined with + to produce appropriate inputs for + , allowing a string representing a + type name to be canonicalized. + + + format_type(to_regtype('varchar(32)'), to_regtypemod('varchar(32)')) + character varying(32) + + + + +
+ + + Most of the functions that reconstruct (decompile) database objects + have an optional pretty flag, which + if true causes the result to + be pretty-printed. Pretty-printing suppresses unnecessary + parentheses and adds whitespace for legibility. + The pretty-printed format is more readable, but the default format + is more likely to be interpreted the same way by future versions of + PostgreSQL; so avoid using pretty-printed output + for dump purposes. Passing false for + the pretty parameter yields the same result as + omitting the parameter. + + + + Index Column Properties + + + + + NameDescription + + + + asc + Does the column sort in ascending order on a forward scan? + + + + desc + Does the column sort in descending order on a forward scan? + + + + nulls_first + Does the column sort with nulls first on a forward scan? + + + + nulls_last + Does the column sort with nulls last on a forward scan? + + + + orderable + Does the column possess any defined sort ordering? + + + + distance_orderable + Can the column be scanned in order by a distance + operator, for example ORDER BY col <-> constant ? + + + + returnable + Can the column value be returned by an index-only scan? + + + + search_array + Does the column natively support col = ANY(array) + searches? + + + + search_nulls + Does the column support IS NULL and + IS NOT NULL searches? + + + + +
+ + + Index Properties + + + + + NameDescription + + + + clusterable + Can the index be used in a CLUSTER command? + + + + index_scan + Does the index support plain (non-bitmap) scans? + + + + bitmap_scan + Does the index support bitmap scans? + + + + backward_scan + Can the scan direction be changed in mid-scan (to + support FETCH BACKWARD on a cursor without + needing materialization)? + + + + +
+ + + Index Access Method Properties + + + + + NameDescription + + + + can_order + Does the access method support ASC, + DESC and related keywords in + CREATE INDEX? + + + + can_unique + Does the access method support unique indexes? + + + + can_multi_col + Does the access method support indexes with multiple columns? + + + + can_exclude + Does the access method support exclusion constraints? + + + + can_include + Does the access method support the INCLUDE + clause of CREATE INDEX? + + + + +
+ + + GUC Flags + + + + + FlagDescription + + + + EXPLAIN + Parameters with this flag are included in + EXPLAIN (SETTINGS) commands. + + + + NO_SHOW_ALL + Parameters with this flag are excluded from + SHOW ALL commands. + + + + NO_RESET + Parameters with this flag do not support + RESET commands. + + + + NO_RESET_ALL + Parameters with this flag are excluded from + RESET ALL commands. + + + + NOT_IN_SAMPLE + Parameters with this flag are not included in + postgresql.conf by default. + + + + RUNTIME_COMPUTED + Parameters with this flag are runtime-computed ones. + + + + +
+ +
+ + + Object Information and Addressing Functions + + + lists functions related to + database object identification and addressing. + + + + Object Information and Addressing Functions + + + + + Function + + + Description + + + + + + + + + pg_get_acl + + pg_get_acl ( classid oid, objid oid, objsubid integer ) + aclitem[] + + + Returns the ACL for a database object, specified + by catalog OID, object OID and sub-object ID. This function returns + NULL values for undefined objects. + + + + + + + pg_describe_object + + pg_describe_object ( classid oid, objid oid, objsubid integer ) + text + + + Returns a textual description of a database object identified by + catalog OID, object OID, and sub-object ID (such as a column number + within a table; the sub-object ID is zero when referring to a whole + object). This description is intended to be human-readable, and might + be translated, depending on server configuration. This is especially + useful to determine the identity of an object referenced in the + pg_depend catalog. This function returns + NULL values for undefined objects. + + + + + + + pg_identify_object + + pg_identify_object ( classid oid, objid oid, objsubid integer ) + record + ( type text, + schema text, + name text, + identity text ) + + + Returns a row containing enough information to uniquely identify the + database object specified by catalog OID, object OID and sub-object + ID. + This information is intended to be machine-readable, and is never + translated. + type identifies the type of database object; + schema is the schema name that the object + belongs in, or NULL for object types that do not + belong to schemas; + name is the name of the object, quoted if + necessary, if the name (along with schema name, if pertinent) is + sufficient to uniquely identify the object, + otherwise NULL; + identity is the complete object identity, with + the precise format depending on object type, and each name within the + format being schema-qualified and quoted as necessary. Undefined + objects are identified with NULL values. + + + + + + + pg_identify_object_as_address + + pg_identify_object_as_address ( classid oid, objid oid, objsubid integer ) + record + ( type text, + object_names text[], + object_args text[] ) + + + Returns a row containing enough information to uniquely identify the + database object specified by catalog OID, object OID and sub-object + ID. + The returned information is independent of the current server, that + is, it could be used to identify an identically named object in + another server. + type identifies the type of database object; + object_names and + object_args + are text arrays that together form a reference to the object. + These three values can be passed + to pg_get_object_address to obtain the internal + address of the object. + + + + + + + pg_get_object_address + + pg_get_object_address ( type text, object_names text[], object_args text[] ) + record + ( classid oid, + objid oid, + objsubid integer ) + + + Returns a row containing enough information to uniquely identify the + database object specified by a type code and object name and argument + arrays. + The returned values are the ones that would be used in system catalogs + such as pg_depend; they can be passed to + other system functions such as pg_describe_object + or pg_identify_object. + classid is the OID of the system catalog + containing the object; + objid is the OID of the object itself, and + objsubid is the sub-object ID, or zero if none. + This function is the inverse + of pg_identify_object_as_address. + Undefined objects are identified with NULL values. + + + + +
+ + + pg_get_acl is useful for retrieving and inspecting + the privileges associated with database objects without looking at + specific catalogs. For example, to retrieve all the granted privileges + on objects in the current database: + +postgres=# SELECT + (pg_identify_object(s.classid,s.objid,s.objsubid)).*, + pg_catalog.pg_get_acl(s.classid,s.objid,s.objsubid) AS acl +FROM pg_catalog.pg_shdepend AS s +JOIN pg_catalog.pg_database AS d + ON d.datname = current_database() AND + d.oid = s.dbid +JOIN pg_catalog.pg_authid AS a + ON a.oid = s.refobjid AND + s.refclassid = 'pg_authid'::regclass +WHERE s.deptype = 'a'; +-[ RECORD 1 ]----------------------------------------- +type | table +schema | public +name | testtab +identity | public.testtab +acl | {postgres=arwdDxtm/postgres,foo=r/postgres} + + + +
+ + + Comment Information Functions + + + comment + about database objects + + + + The functions shown in + extract comments previously stored with the + command. A null value is returned if no + comment could be found for the specified parameters. + + + + Comment Information Functions + + + + + Function + + + Description + + + + + + + + + col_description + + col_description ( table oid, column integer ) + text + + + Returns the comment for a table column, which is specified by the OID + of its table and its column number. + (obj_description cannot be used for table + columns, since columns do not have OIDs of their own.) + + + + + + + obj_description + + obj_description ( object oid, catalog name ) + text + + + Returns the comment for a database object specified by its OID and the + name of the containing system catalog. For + example, obj_description(123456, 'pg_class') would + retrieve the comment for the table with OID 123456. + + + + + + obj_description ( object oid ) + text + + + Returns the comment for a database object specified by its OID alone. + This is deprecated since there is no guarantee + that OIDs are unique across different system catalogs; therefore, the + wrong comment might be returned. + + + + + + + shobj_description + + shobj_description ( object oid, catalog name ) + text + + + Returns the comment for a shared database object specified by its OID + and the name of the containing system catalog. This is just + like obj_description except that it is used for + retrieving comments on shared objects (that is, databases, roles, and + tablespaces). Some system catalogs are global to all databases within + each cluster, and the descriptions for objects in them are stored + globally as well. + + + + +
+ +
+ + + Data Validity Checking Functions + + + The functions shown in + can be helpful for checking validity of proposed input data. + + + + Data Validity Checking Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + pg_input_is_valid + + pg_input_is_valid ( + string text, + type text + ) + boolean + + + Tests whether the given string is valid + input for the specified data type, returning true or false. + + + This function will only work as desired if the data type's input + function has been updated to report invalid input as + a soft error. Otherwise, invalid input will abort + the transaction, just as if the string had been cast to the type + directly. + + + pg_input_is_valid('42', 'integer') + t + + + pg_input_is_valid('42000000000', 'integer') + f + + + pg_input_is_valid('1234.567', 'numeric(7,4)') + f + + + + + + pg_input_error_info + + pg_input_error_info ( + string text, + type text + ) + record + ( message text, + detail text, + hint text, + sql_error_code text ) + + + Tests whether the given string is valid + input for the specified data type; if not, return the details of + the error that would have been thrown. If the input is valid, the + results are NULL. The inputs are the same as + for pg_input_is_valid. + + + This function will only work as desired if the data type's input + function has been updated to report invalid input as + a soft error. Otherwise, invalid input will abort + the transaction, just as if the string had been cast to the type + directly. + + + SELECT * FROM pg_input_error_info('42000000000', 'integer') + + + message | detail | hint | sql_error_code +------------------------------------------------------+--------+------+---------------- + value "42000000000" is out of range for type integer | | | 22003 + + + + + +
+ +
+ + + Transaction ID and Snapshot Information Functions + + + The functions shown in + provide server transaction information in an exportable form. The main + use of these functions is to determine which transactions were committed + between two snapshots. + + + + Transaction ID and Snapshot Information Functions + + + + + Function + + + Description + + + + + + + + + age + + age ( xid ) + integer + + + Returns the number of transactions between the supplied + transaction id and the current transaction counter. + + + + + + + mxid_age + + mxid_age ( xid ) + integer + + + Returns the number of multixacts IDs between the supplied + multixact ID and the current multixacts counter. + + + + + + + pg_current_xact_id + + pg_current_xact_id () + xid8 + + + Returns the current transaction's ID. It will assign a new one if the + current transaction does not have one already (because it has not + performed any database updates); see for details. If executed in a + subtransaction, this will return the top-level transaction ID; + see for details. + + + + + + + pg_current_xact_id_if_assigned + + pg_current_xact_id_if_assigned () + xid8 + + + Returns the current transaction's ID, or NULL if no + ID is assigned yet. (It's best to use this variant if the transaction + might otherwise be read-only, to avoid unnecessary consumption of an + XID.) + If executed in a subtransaction, this will return the top-level + transaction ID. + + + + + + + pg_xact_status + + pg_xact_status ( xid8 ) + text + + + Reports the commit status of a recent transaction. + The result is one of in progress, + committed, or aborted, + provided that the transaction is recent enough that the system retains + the commit status of that transaction. + If it is old enough that no references to the transaction survive in + the system and the commit status information has been discarded, the + result is NULL. + Applications might use this function, for example, to determine + whether their transaction committed or aborted after the application + and database server become disconnected while + a COMMIT is in progress. + Note that prepared transactions are reported as in + progress; applications must check pg_prepared_xacts + if they need to determine whether a transaction ID belongs to a + prepared transaction. + + + + + + + pg_current_snapshot + + pg_current_snapshot () + pg_snapshot + + + Returns a current snapshot, a data structure + showing which transaction IDs are now in-progress. + Only top-level transaction IDs are included in the snapshot; + subtransaction IDs are not shown; see + for details. + + + + + + + pg_snapshot_xip + + pg_snapshot_xip ( pg_snapshot ) + setof xid8 + + + Returns the set of in-progress transaction IDs contained in a snapshot. + + + + + + + pg_snapshot_xmax + + pg_snapshot_xmax ( pg_snapshot ) + xid8 + + + Returns the xmax of a snapshot. + + + + + + + pg_snapshot_xmin + + pg_snapshot_xmin ( pg_snapshot ) + xid8 + + + Returns the xmin of a snapshot. + + + + + + + pg_visible_in_snapshot + + pg_visible_in_snapshot ( xid8, pg_snapshot ) + boolean + + + Is the given transaction ID visible according + to this snapshot (that is, was it completed before the snapshot was + taken)? Note that this function will not give the correct answer for + a subtransaction ID (subxid); see for + details. + + + + + + + pg_get_multixact_members + + pg_get_multixact_members ( multixid xid ) + setof record + ( xid xid, + mode text ) + + + Returns the transaction ID and lock mode for each member of the + specified multixact ID. The lock modes forupd, + fornokeyupd, sh, and + keysh correspond to the row-level locks + FOR UPDATE, FOR NO KEY UPDATE, + FOR SHARE, and FOR KEY SHARE, + respectively, as described in . Two + additional modes are specific to multixacts: + nokeyupd, used by updates that do not modify key + columns, and upd, used by updates or deletes that + modify key columns. + + + + + + + pg_get_multixact_stats + + pg_get_multixact_stats () + record + ( num_mxids integer, + num_members bigint, + members_size bigint, + oldest_multixact xid ) + + + Returns statistics about current multixact usage: + num_mxids is the total number of multixact IDs + currently present in the system, num_members is + the total number of multixact member entries currently present in + the system, members_size is the storage occupied + by num_members in the + pg_multixact/members directory, + oldest_multixact is the oldest multixact ID still + in use. + + + The function reports statistics at the time it is invoked. Values may + vary between calls, even within a single transaction. + + + To use this function, you must have privileges of the + pg_read_all_stats role. + + + + +
+ + + The internal transaction ID type xid is 32 bits wide and + wraps around every 4 billion transactions. However, + the functions shown in , except + age, mxid_age, and + pg_get_multixact_members, use a + 64-bit type xid8 that does not wrap around during the life + of an installation and can be converted to xid by casting if + required; see for details. + The data type pg_snapshot stores information about + transaction ID visibility at a particular moment in time. Its components + are described in . + pg_snapshot's textual representation is + xmin:xmax:xip_list. + For example 10:20:10,14,15 means + xmin=10, xmax=20, xip_list=10, 14, 15. + + + + Snapshot Components + + + + + + Name + Description + + + + + + xmin + + Lowest transaction ID that was still active. All transaction IDs + less than xmin are either committed and visible, + or rolled back and dead. + + + + + xmax + + One past the highest completed transaction ID. All transaction IDs + greater than or equal to xmax had not yet + completed as of the time of the snapshot, and thus are invisible. + + + + + xip_list + + Transactions in progress at the time of the snapshot. A transaction + ID that is xmin <= X < + xmax and not in this list was already completed at the time + of the snapshot, and thus is either visible or dead according to its + commit status. This list does not include the transaction IDs of + subtransactions (subxids). + + + + +
+ + + In releases of PostgreSQL before 13 there was + no xid8 type, so variants of these functions were provided + that used bigint to represent a 64-bit XID, with a + correspondingly distinct snapshot data type txid_snapshot. + These older functions have txid in their names. They + are still supported for backward compatibility, but may be removed from a + future release. See . + + + + Deprecated Transaction ID and Snapshot Information Functions + + + + + Function + + + Description + + + + + + + + + + txid_current + + txid_current () + bigint + + + See pg_current_xact_id(). + + + + + + + txid_current_if_assigned + + txid_current_if_assigned () + bigint + + + See pg_current_xact_id_if_assigned(). + + + + + + + txid_current_snapshot + + txid_current_snapshot () + txid_snapshot + + + See pg_current_snapshot(). + + + + + + + txid_snapshot_xip + + txid_snapshot_xip ( txid_snapshot ) + setof bigint + + + See pg_snapshot_xip(). + + + + + + + txid_snapshot_xmax + + txid_snapshot_xmax ( txid_snapshot ) + bigint + + + See pg_snapshot_xmax(). + + + + + + + txid_snapshot_xmin + + txid_snapshot_xmin ( txid_snapshot ) + bigint + + + See pg_snapshot_xmin(). + + + + + + + txid_visible_in_snapshot + + txid_visible_in_snapshot ( bigint, txid_snapshot ) + boolean + + + See pg_visible_in_snapshot(). + + + + + + + txid_status + + txid_status ( bigint ) + text + + + See pg_xact_status(). + + + + +
+ +
+ + + Committed Transaction Information Functions + + + The functions shown in + provide information about when past transactions were committed. + They only provide useful data when the + configuration option is + enabled, and only for transactions that were committed after it was + enabled. Commit timestamp information is routinely removed during + vacuum. + + + + Committed Transaction Information Functions + + + + + Function + + + Description + + + + + + + + + pg_xact_commit_timestamp + + pg_xact_commit_timestamp ( xid ) + timestamp with time zone + + + Returns the commit timestamp of a transaction. + + + + + + + pg_xact_commit_timestamp_origin + + pg_xact_commit_timestamp_origin ( xid ) + record + ( timestamp timestamp with time zone, + roident oid) + + + Returns the commit timestamp and replication origin of a transaction. + + + + + + + pg_last_committed_xact + + pg_last_committed_xact () + record + ( xid xid, + timestamp timestamp with time zone, + roident oid ) + + + Returns the transaction ID, commit timestamp and replication origin + of the latest committed transaction. + + + + +
+ +
+ + + Control Data Functions + + + The functions shown in + print information initialized during initdb, such + as the catalog version. They also show information about write-ahead + logging and checkpoint processing. This information is cluster-wide, + not specific to any one database. These functions provide most of the same + information, from the same source, as the + application. + + + + Control Data Functions + + + + + Function + + + Description + + + + + + + + + pg_control_checkpoint + + pg_control_checkpoint () + record + + + Returns information about current checkpoint state, as shown in + . + + + + + + + pg_control_system + + pg_control_system () + record + + + Returns information about current control file state, as shown in + . + + + + + + + pg_control_init + + pg_control_init () + record + + + Returns information about cluster initialization state, as shown in + . + + + + + + + pg_control_recovery + + pg_control_recovery () + record + + + Returns information about recovery state, as shown in + . + + + + +
+ + + <function>pg_control_checkpoint</function> Output Columns + + + + Column Name + Data Type + + + + + + + checkpoint_lsn + pg_lsn + + + + redo_lsn + pg_lsn + + + + redo_wal_file + text + + + + timeline_id + integer + + + + prev_timeline_id + integer + + + + full_page_writes + boolean + + + + next_xid + text + + + + next_oid + oid + + + + next_multixact_id + xid + + + + next_multi_offset + xid + + + + oldest_xid + xid + + + + oldest_xid_dbid + oid + + + + oldest_active_xid + xid + + + + oldest_multi_xid + xid + + + + oldest_multi_dbid + oid + + + + oldest_commit_ts_xid + xid + + + + newest_commit_ts_xid + xid + + + + checkpoint_time + timestamp with time zone + + + + +
+ + + <function>pg_control_system</function> Output Columns + + + + Column Name + Data Type + + + + + + + pg_control_version + integer + + + + catalog_version_no + integer + + + + system_identifier + bigint + + + + pg_control_last_modified + timestamp with time zone + + + + +
+ + + <function>pg_control_init</function> Output Columns + + + + Column Name + Data Type + + + + + + + max_data_alignment + integer + + + + database_block_size + integer + + + + blocks_per_segment + integer + + + + wal_block_size + integer + + + + bytes_per_wal_segment + integer + + + + max_identifier_length + integer + + + + max_index_columns + integer + + + + max_toast_chunk_size + integer + + + + large_object_chunk_size + integer + + + + float8_pass_by_value + boolean + + + + data_page_checksum_version + integer + + + + default_char_signedness + boolean + + + + +
+ + + <function>pg_control_recovery</function> Output Columns + + + + Column Name + Data Type + + + + + + + min_recovery_end_lsn + pg_lsn + + + + min_recovery_end_timeline + integer + + + + backup_start_lsn + pg_lsn + + + + backup_end_lsn + pg_lsn + + + + end_of_backup_record_required + boolean + + + + +
+ +
+ + + Version Information Functions + + + The functions shown in + print version information. + + + + Version Information Functions + + + + + Function + + + Description + + + + + + + + + version + + version () + text + + + Returns a string describing the PostgreSQL + server's version. You can also get this information from + , or for a machine-readable + version use . Software + developers should use server_version_num (available + since 8.2) or instead of + parsing the text version. + + + + + + + unicode_version + + unicode_version () + text + + + Returns a string representing the version of Unicode used by + PostgreSQL. + + + + + + icu_unicode_version + + icu_unicode_version () + text + + + Returns a string representing the version of Unicode used by ICU, if + the server was built with ICU support; otherwise returns + NULL + + + +
+ +
+ + + WAL Summarization Information Functions + + + The functions shown in + print information about the status of WAL summarization. + See . + + + + WAL Summarization Information Functions + + + + + Function + + + Description + + + + + + + + + pg_available_wal_summaries + + pg_available_wal_summaries () + setof record + ( tli bigint, + start_lsn pg_lsn, + end_lsn pg_lsn ) + + + Returns information about the WAL summary files present in the + data directory, under pg_wal/summaries. + One row will be returned per WAL summary file. Each file summarizes + WAL on the indicated TLI within the indicated LSN range. This function + might be useful to determine whether enough WAL summaries are present + on the server to take an incremental backup based on some prior + backup whose start LSN is known. + + + + + + + pg_wal_summary_contents + + pg_wal_summary_contents ( tli bigint, start_lsn pg_lsn, end_lsn pg_lsn ) + setof record + ( relfilenode oid, + reltablespace oid, + reldatabase oid, + relforknumber smallint, + relblocknumber bigint, + is_limit_block boolean ) + + + Returns one information about the contents of a single WAL summary file + identified by TLI and starting and ending LSNs. Each row with + is_limit_block false indicates that the block + identified by the remaining output columns was modified by at least + one WAL record within the range of records summarized by this file. + Each row with is_limit_block true indicates either + that (a) the relation fork was truncated to the length given by + relblocknumber within the relevant range of WAL + records or (b) that the relation fork was created or dropped within + the relevant range of WAL records; in such cases, + relblocknumber will be zero. + + + + + + + pg_get_wal_summarizer_state + + pg_get_wal_summarizer_state () + record + ( summarized_tli bigint, + summarized_lsn pg_lsn, + pending_lsn pg_lsn, + summarizer_pid int ) + + + Returns information about the progress of the WAL summarizer. If the + WAL summarizer has never run since the instance was started, then + summarized_tli and summarized_lsn + will be 0 and 0/00000000 respectively; + otherwise, they will be the TLI and ending LSN of the last WAL summary + file written to disk. If the WAL summarizer is currently running, + pending_lsn will be the ending LSN of the last + record that it has consumed, which must always be greater than or + equal to summarized_lsn; if the WAL summarizer is + not running, it will be equal to summarized_lsn. + summarizer_pid is the PID of the WAL summarizer + process, if it is running, and otherwise NULL. + + + As a special exception, the WAL summarizer will refuse to generate + WAL summary files if run on WAL generated under + wal_level=minimal, since such summaries would be + unsafe to use as the basis for an incremental backup. In this case, + the fields above will continue to advance as if summaries were being + generated, but nothing will be written to disk. Once the summarizer + reaches WAL generated while wal_level was set + to replica or higher, it will resume writing + summaries to disk. + + + + +
+ +
+ + + Get Object DDL Functions + + + The functions shown in + reconstruct DDL statements for various global database objects. + Each function returns a set of text rows, one SQL statement per row. + (This is a decompiled reconstruction, not the original text of the + command.) Functions that accept VARIADIC options + take alternating name/value text pairs; values are parsed as boolean, + integer or text. + + + + Get Object DDL Functions + + + + + Function + + + Description + + + + + + + + + pg_get_role_ddl + + pg_get_role_ddl + ( role regrole + , VARIADIC options + text ) + setof text + + + Reconstructs the CREATE ROLE statement and any + ALTER ROLE ... SET statements for the given role. + Each statement is returned as a separate row. + Password information is never included in the output. + The following options are supported: pretty (boolean) + for pretty-printed output and memberships (boolean, + default true) to include GRANT statements for + role memberships and their options. + + + + + + pg_get_tablespace_ddl + + pg_get_tablespace_ddl + ( tablespace oid + , VARIADIC options + text ) + setof text + + + pg_get_tablespace_ddl + ( tablespace name + , VARIADIC options + text ) + setof text + + + Reconstructs the CREATE TABLESPACE statement for + the specified tablespace (by OID or name). If the tablespace has + options set, an ALTER TABLESPACE ... SET statement + is also returned. Each statement is returned as a separate row. + The following options are supported: pretty (boolean) + for formatted output and owner (boolean) to include + OWNER. + + + + + + pg_get_database_ddl + + pg_get_database_ddl + ( database regdatabase + , VARIADIC options + text ) + setof text + + + Reconstructs the CREATE DATABASE statement for the + specified database, followed by ALTER DATABASE + statements for connection limit, template status, and configuration + settings. Each statement is returned as a separate row. + The following options are supported: + pretty (boolean) for formatted output, + owner (boolean) to include OWNER, + and tablespace (boolean) to include + TABLESPACE. + + + + +
+ +
+ +
diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml new file mode 100644 index 0000000000000..4cd338fe6e32d --- /dev/null +++ b/doc/src/sgml/func/func-json.sgml @@ -0,0 +1,4085 @@ + + JSON Functions and Operators + + + JSON + functions and operators + + + SQL/JSON + functions and expressions + + + + This section describes: + + + + + functions and operators for processing and creating JSON data + + + + + the SQL/JSON path language + + + + + the SQL/JSON query functions + + + + + + + To provide native support for JSON data types within the SQL environment, + PostgreSQL implements the + SQL/JSON data model. + This model comprises sequences of items. Each item can hold SQL scalar + values, with an additional SQL/JSON null value, and composite data structures + that use JSON arrays and objects. The model is a formalization of the implied + data model in the JSON specification + RFC 7159. + + + + SQL/JSON allows you to handle JSON data alongside regular SQL data, + with transaction support, including: + + + + + Uploading JSON data into the database and storing it in + regular SQL columns as character or binary strings. + + + + + Generating JSON objects and arrays from relational data. + + + + + Querying JSON data using SQL/JSON query functions and + SQL/JSON path language expressions. + + + + + + + To learn more about the SQL/JSON standard, see + . For details on JSON types + supported in PostgreSQL, + see . + + + + Processing and Creating JSON Data + + + shows the operators that + are available for use with JSON data types (see ). + In addition, the usual comparison operators shown in are available for + jsonb, though not for json. The comparison + operators follow the ordering rules for B-tree operations outlined in + . + See also for the aggregate + function json_agg which aggregates record + values as JSON, the aggregate function + json_object_agg which aggregates pairs of values + into a JSON object, and their jsonb equivalents, + jsonb_agg and jsonb_object_agg. + + + + <type>json</type> and <type>jsonb</type> Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + json -> integer + json + + + jsonb -> integer + jsonb + + + Extracts n'th element of JSON array + (array elements are indexed from zero, but negative integers count + from the end). + + + '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json -> 2 + {"c":"baz"} + + + '[{"a":"foo"},{"b":"bar"},{"c":"baz"}]'::json -> -3 + {"a":"foo"} + + + + + + json -> text + json + + + jsonb -> text + jsonb + + + Extracts JSON object field with the given key. + + + '{"a": {"b":"foo"}}'::json -> 'a' + {"b":"foo"} + + + + + + json ->> integer + text + + + jsonb ->> integer + text + + + Extracts n'th element of JSON array, + as text. + + + '[1,2,3]'::json ->> 2 + 3 + + + + + + json ->> text + text + + + jsonb ->> text + text + + + Extracts JSON object field with the given key, as text. + + + '{"a":1,"b":2}'::json ->> 'b' + 2 + + + + + + json #> text[] + json + + + jsonb #> text[] + jsonb + + + Extracts JSON sub-object at the specified path, where path elements + can be either field keys or array indexes. + + + '{"a": {"b": ["foo","bar"]}}'::json #> '{a,b,1}' + "bar" + + + + + + json #>> text[] + text + + + jsonb #>> text[] + text + + + Extracts JSON sub-object at the specified path as text. + + + '{"a": {"b": ["foo","bar"]}}'::json #>> '{a,b,1}' + bar + + + + +
+ + + + The field/element/path extraction operators return NULL, rather than + failing, if the JSON input does not have the right structure to match + the request; for example if no such key or array element exists. + + + + + Some further operators exist only for jsonb, as shown + in . + + describes how these operators can be used to effectively search indexed + jsonb data. + + + + Additional <type>jsonb</type> Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + jsonb @> jsonb + boolean + + + Does the first JSON value contain the second? + (See for details about containment.) + + + '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb + t + + + + + + jsonb <@ jsonb + boolean + + + Is the first JSON value contained in the second? + + + '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb + t + + + + + + jsonb ? text + boolean + + + Does the text string exist as a top-level key or array element within + the JSON value? + + + '{"a":1, "b":2}'::jsonb ? 'b' + t + + + '["a", "b", "c"]'::jsonb ? 'b' + t + + + + + + jsonb ?| text[] + boolean + + + Do any of the strings in the text array exist as top-level keys or + array elements? + + + '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'd'] + t + + + + + + jsonb ?& text[] + boolean + + + Do all of the strings in the text array exist as top-level keys or + array elements? + + + '["a", "b", "c"]'::jsonb ?& array['a', 'b'] + t + + + + + + jsonb || jsonb + jsonb + + + Concatenates two jsonb values. + Concatenating two arrays generates an array containing all the + elements of each input. Concatenating two objects generates an + object containing the union of their + keys, taking the second object's value when there are duplicate keys. + All other cases are treated by converting a non-array input into a + single-element array, and then proceeding as for two arrays. + Does not operate recursively: only the top-level array or object + structure is merged. + + + '["a", "b"]'::jsonb || '["a", "d"]'::jsonb + ["a", "b", "a", "d"] + + + '{"a": "b"}'::jsonb || '{"c": "d"}'::jsonb + {"a": "b", "c": "d"} + + + '[1, 2]'::jsonb || '3'::jsonb + [1, 2, 3] + + + '{"a": "b"}'::jsonb || '42'::jsonb + [{"a": "b"}, 42] + + + To append an array to another array as a single entry, wrap it + in an additional layer of array, for example: + + + '[1, 2]'::jsonb || jsonb_build_array('[3, 4]'::jsonb) + [1, 2, [3, 4]] + + + + + + jsonb - text + jsonb + + + Deletes a key (and its value) from a JSON object, or matching string + value(s) from a JSON array. + + + '{"a": "b", "c": "d"}'::jsonb - 'a' + {"c": "d"} + + + '["a", "b", "c", "b"]'::jsonb - 'b' + ["a", "c"] + + + + + + jsonb - text[] + jsonb + + + Deletes all matching keys or array elements from the left operand. + + + '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[] + {} + + + + + + jsonb - integer + jsonb + + + Deletes the array element with specified index (negative + integers count from the end). Throws an error if JSON value + is not an array. + + + '["a", "b"]'::jsonb - 1 + ["a"] + + + + + + jsonb #- text[] + jsonb + + + Deletes the field or array element at the specified path, where path + elements can be either field keys or array indexes. + + + '["a", {"b":1}]'::jsonb #- '{1,b}' + ["a", {}] + + + + + + jsonb @? jsonpath + boolean + + + Does JSON path return any item for the specified JSON value? + (This is useful only with SQL-standard JSON path expressions, not + predicate check + expressions, since those always return a value.) + + + '{"a":[1,2,3,4,5]}'::jsonb @? '$.a[*] ? (@ > 2)' + t + + + + + + jsonb @@ jsonpath + boolean + + + Returns the result of a JSON path predicate check for the + specified JSON value. + (This is useful only + with predicate + check expressions, not SQL-standard JSON path expressions, + since it will return NULL if the path result is + not a single boolean value.) + + + '{"a":[1,2,3,4,5]}'::jsonb @@ '$.a[*] > 2' + t + + + + +
+ + + + The jsonpath operators @? + and @@ suppress the following errors: missing object + field or array element, unexpected JSON item type, datetime and numeric + errors. The jsonpath-related functions described below can + also be told to suppress these types of errors. This behavior might be + helpful when searching JSON document collections of varying structure. + + + + + shows the functions that are + available for constructing json and jsonb values. + Some functions in this table have a RETURNING clause, + which specifies the data type returned. It must be one of json, + jsonb, bytea, a character string type (text, + char, or varchar), or a type + that can be cast to json. + By default, the json type is returned. + + + + JSON Creation Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + to_json + + to_json ( anyelement ) + json + + + + to_jsonb + + to_jsonb ( anyelement ) + jsonb + + + Converts any SQL value to json or jsonb. + Arrays and composites are converted recursively to arrays and + objects (multidimensional arrays become arrays of arrays in JSON). + Otherwise, if there is a cast from the SQL data type + to json, the cast function will be used to perform the + conversion; + + For example, the extension has a cast + from hstore to json, so that + hstore values converted via the JSON creation functions + will be represented as JSON objects, not as primitive string values. + + + otherwise, a scalar JSON value is produced. For any scalar other than + a number, a Boolean, or a null value, the text representation will be + used, with escaping as necessary to make it a valid JSON string value. + + + to_json('Fred said "Hi."'::text) + "Fred said \"Hi.\"" + + + to_jsonb(row(42, 'Fred said "Hi."'::text)) + {"f1": 42, "f2": "Fred said \"Hi.\""} + + + + + + + array_to_json + + array_to_json ( anyarray , boolean ) + json + + + Converts an SQL array to a JSON array. The behavior is the same + as to_json except that line feeds will be added + between top-level array elements if the optional boolean parameter is + true. + + + array_to_json('{{1,5},{99,100}}'::int[]) + [[1,5],[99,100]] + + + + + + + json_array + json_array ( + { value_expression FORMAT JSON } , ... + { NULL | ABSENT } ON NULL + RETURNING data_type FORMAT JSON ENCODING UTF8 ) + + + json_array ( + query_expression + RETURNING data_type FORMAT JSON ENCODING UTF8 ) + + + Constructs a JSON array from either a series of + value_expression parameters or from the results + of query_expression, + which must be a SELECT query returning a single column. If + ABSENT ON NULL is specified, NULL values are ignored. + This is always the case if a + query_expression is used. + + + json_array(1,true,json '{"a":null}') + [1, true, {"a":null}] + + + json_array(SELECT * FROM (VALUES(1),(2)) t) + [1, 2] + + + + + + + row_to_json + + row_to_json ( record , boolean ) + json + + + Converts an SQL composite value to a JSON object. The behavior is the + same as to_json except that line feeds will be + added between top-level elements if the optional boolean parameter is + true. + + + row_to_json(row(1,'foo')) + {"f1":1,"f2":"foo"} + + + + + + + json_build_array + + json_build_array ( VARIADIC "any" ) + json + + + + jsonb_build_array + + jsonb_build_array ( VARIADIC "any" ) + jsonb + + + Builds a possibly-heterogeneously-typed JSON array out of a variadic + argument list. Each argument is converted as + per to_json or to_jsonb. + + + json_build_array(1, 2, 'foo', 4, 5) + [1, 2, "foo", 4, 5] + + + + + + + json_build_object + + json_build_object ( VARIADIC "any" ) + json + + + + jsonb_build_object + + jsonb_build_object ( VARIADIC "any" ) + jsonb + + + Builds a JSON object out of a variadic argument list. By convention, + the argument list consists of alternating keys and values. Key + arguments are coerced to text; value arguments are converted as + per to_json or to_jsonb. + + + json_build_object('foo', 1, 2, row(3,'bar')) + {"foo" : 1, "2" : {"f1":3,"f2":"bar"}} + + + + + + json_object + json_object ( + { key_expression { VALUE | ':' } + value_expression FORMAT JSON ENCODING UTF8 }, ... + { NULL | ABSENT } ON NULL + { WITH | WITHOUT } UNIQUE KEYS + RETURNING data_type FORMAT JSON ENCODING UTF8 ) + + + Constructs a JSON object of all the key/value pairs given, + or an empty object if none are given. + key_expression is a scalar expression + defining the JSON key, which is + converted to the text type. + It cannot be NULL nor can it + belong to a type that has a cast to the json type. + If WITH UNIQUE KEYS is specified, there must not + be any duplicate key_expression. + Any pair for which the value_expression + evaluates to NULL is omitted from the output + if ABSENT ON NULL is specified; + if NULL ON NULL is specified or the clause + omitted, the key is included with value NULL. + + + json_object('code' VALUE 'P123', 'title': 'Jaws') + {"code" : "P123", "title" : "Jaws"} + + + + + + + json_object + + json_object ( text[] ) + json + + + + jsonb_object + + jsonb_object ( text[] ) + jsonb + + + Builds a JSON object out of a text array. The array must have either + exactly one dimension with an even number of members, in which case + they are taken as alternating key/value pairs, or two dimensions + such that each inner array has exactly two elements, which + are taken as a key/value pair. All values are converted to JSON + strings. + + + json_object('{a, 1, b, "def", c, 3.5}') + {"a" : "1", "b" : "def", "c" : "3.5"} + + json_object('{{a, 1}, {b, "def"}, {c, 3.5}}') + {"a" : "1", "b" : "def", "c" : "3.5"} + + + + + + json_object ( keys text[], values text[] ) + json + + + jsonb_object ( keys text[], values text[] ) + jsonb + + + This form of json_object takes keys and values + pairwise from separate text arrays. Otherwise it is identical to + the one-argument form. + + + json_object('{a,b}', '{1,2}') + {"a": "1", "b": "2"} + + + + + + json constructor + json ( + expression + FORMAT JSON ENCODING UTF8 + { WITH | WITHOUT } UNIQUE KEYS ) + json + + + Converts a given expression specified as text or + bytea string (in UTF8 encoding) into a JSON + value. If expression is NULL, an + SQL null value is returned. + If WITH UNIQUE is specified, the + expression must not contain any duplicate + object keys. + + + json('{"a":123, "b":[true,"foo"], "a":"bar"}') + {"a":123, "b":[true,"foo"], "a":"bar"} + + + + + + + json_scalar + json_scalar ( expression ) + + + Converts a given SQL scalar value into a JSON scalar value. + If the input is NULL, an SQL null is returned. If + the input is number or a boolean value, a corresponding JSON number + or boolean value is returned. For any other value, a JSON string is + returned. + + + json_scalar(123.45) + 123.45 + + + json_scalar(CURRENT_TIMESTAMP) + "2022-05-10T10:51:04.62128-04:00" + + + + + + json_serialize ( + expression FORMAT JSON ENCODING UTF8 + RETURNING data_type FORMAT JSON ENCODING UTF8 ) + + + Converts an SQL/JSON expression into a character or binary string. The + expression can be of any JSON type, any + character string type, or bytea in UTF8 encoding. + The returned type used in RETURNING can be any + character string type or bytea. The default is + text. + + + json_serialize('{ "a" : 1 } ' RETURNING bytea) + \x7b20226122203a2031207d20 + + + + +
+ + + details SQL/JSON + facilities for testing JSON. + + + + SQL/JSON Testing Functions + + + + + Function signature + + + Description + + + Example(s) + + + + + + + IS JSON + expression IS NOT JSON + { VALUE | SCALAR | ARRAY | OBJECT } + { WITH | WITHOUT } UNIQUE KEYS + + + This predicate tests whether expression can be + parsed as JSON, possibly of a specified type. + If SCALAR or ARRAY or + OBJECT is specified, the + test is whether or not the JSON is of that particular type. If + WITH UNIQUE KEYS is specified, then any object in the + expression is also tested to see if it + has duplicate keys. + + + +SELECT js, + js IS JSON "json?", + js IS JSON SCALAR "scalar?", + js IS JSON OBJECT "object?", + js IS JSON ARRAY "array?" +FROM (VALUES + ('123'), ('"abc"'), ('{"a": "b"}'), ('[1,2]'),('abc')) foo(js); + js | json? | scalar? | object? | array? +------------+-------+---------+---------+-------- + 123 | t | t | f | f + "abc" | t | t | f | f + {"a": "b"} | t | f | t | f + [1,2] | t | f | f | t + abc | f | f | f | f + + + + +SELECT js, + js IS JSON OBJECT "object?", + js IS JSON ARRAY "array?", + js IS JSON ARRAY WITH UNIQUE KEYS "array w. UK?", + js IS JSON ARRAY WITHOUT UNIQUE KEYS "array w/o UK?" +FROM (VALUES ('[{"a":"1"}, + {"b":"2","b":"3"}]')) foo(js); +-[ RECORD 1 ]-+-------------------- +js | [{"a":"1"}, + + | {"b":"2","b":"3"}] +object? | f +array? | t +array w. UK? | f +array w/o UK? | t + + + + + +
+ + + shows the functions that + are available for processing json and jsonb values. + + + + JSON Processing Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + json_array_elements + + json_array_elements ( json ) + setof json + + + + jsonb_array_elements + + jsonb_array_elements ( jsonb ) + setof jsonb + + + Expands the top-level JSON array into a set of JSON values. + + + SELECT * FROM json_array_elements('[1,true, [2,false]]') + + + value +----------- + 1 + true + [2,false] + + + + + + + + json_array_elements_text + + json_array_elements_text ( json ) + setof text + + + + jsonb_array_elements_text + + jsonb_array_elements_text ( jsonb ) + setof text + + + Expands the top-level JSON array into a set of text values. + + + SELECT * FROM json_array_elements_text('["foo", "bar"]') + + + value +----------- + foo + bar + + + + + + + + json_array_length + + json_array_length ( json ) + integer + + + + jsonb_array_length + + jsonb_array_length ( jsonb ) + integer + + + Returns the number of elements in the top-level JSON array. + + + json_array_length('[1,2,3,{"f1":1,"f2":[5,6]},4]') + 5 + + + jsonb_array_length('[]') + 0 + + + + + + + json_each + + json_each ( json ) + setof record + ( key text, + value json ) + + + + jsonb_each + + jsonb_each ( jsonb ) + setof record + ( key text, + value jsonb ) + + + Expands the top-level JSON object into a set of key/value pairs. + + + SELECT * FROM json_each('{"a":"foo", "b":"bar"}') + + + key | value +-----+------- + a | "foo" + b | "bar" + + + + + + + + json_each_text + + json_each_text ( json ) + setof record + ( key text, + value text ) + + + + jsonb_each_text + + jsonb_each_text ( jsonb ) + setof record + ( key text, + value text ) + + + Expands the top-level JSON object into a set of key/value pairs. + The returned values will be of + type text. + + + SELECT * FROM json_each_text('{"a":"foo", "b":"bar"}') + + + key | value +-----+------- + a | foo + b | bar + + + + + + + + json_extract_path + + json_extract_path ( from_json json, VARIADIC path_elems text[] ) + json + + + + jsonb_extract_path + + jsonb_extract_path ( from_json jsonb, VARIADIC path_elems text[] ) + jsonb + + + Extracts JSON sub-object at the specified path. + (This is functionally equivalent to the #> + operator, but writing the path out as a variadic list can be more + convenient in some cases.) + + + json_extract_path('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}', 'f4', 'f6') + "foo" + + + + + + + json_extract_path_text + + json_extract_path_text ( from_json json, VARIADIC path_elems text[] ) + text + + + + jsonb_extract_path_text + + jsonb_extract_path_text ( from_json jsonb, VARIADIC path_elems text[] ) + text + + + Extracts JSON sub-object at the specified path as text. + (This is functionally equivalent to the #>> + operator.) + + + json_extract_path_text('{"f2":{"f3":1},"f4":{"f5":99,"f6":"foo"}}', 'f4', 'f6') + foo + + + + + + + json_object_keys + + json_object_keys ( json ) + setof text + + + + jsonb_object_keys + + jsonb_object_keys ( jsonb ) + setof text + + + Returns the set of keys in the top-level JSON object. + + + SELECT * FROM json_object_keys('{"f1":"abc","f2":{"f3":"a", "f4":"b"}}') + + + json_object_keys +------------------ + f1 + f2 + + + + + + + + json_populate_record + + json_populate_record ( base anyelement, from_json json ) + anyelement + + + + jsonb_populate_record + + jsonb_populate_record ( base anyelement, from_json jsonb ) + anyelement + + + Expands the top-level JSON object to a row having the composite type + of the base argument. The JSON object + is scanned for fields whose names match column names of the output row + type, and their values are inserted into those columns of the output. + (Fields that do not correspond to any output column name are ignored.) + In typical use, the value of base is just + NULL, which means that any output columns that do + not match any object field will be filled with nulls. However, + if base isn't NULL then + the values it contains will be used for unmatched columns. + + + To convert a JSON value to the SQL type of an output column, the + following rules are applied in sequence: + + + + A JSON null value is converted to an SQL null in all cases. + + + + + If the output column is of type json + or jsonb, the JSON value is just reproduced exactly. + + + + + If the output column is a composite (row) type, and the JSON value + is a JSON object, the fields of the object are converted to columns + of the output row type by recursive application of these rules. + + + + + Likewise, if the output column is an array type and the JSON value + is a JSON array, the elements of the JSON array are converted to + elements of the output array by recursive application of these + rules. + + + + + Otherwise, if the JSON value is a string, the contents of the + string are fed to the input conversion function for the column's + data type. + + + + + Otherwise, the ordinary text representation of the JSON value is + fed to the input conversion function for the column's data type. + + + + + + While the example below uses a constant JSON value, typical use would + be to reference a json or jsonb column + laterally from another table in the query's FROM + clause. Writing json_populate_record in + the FROM clause is good practice, since all of the + extracted columns are available for use without duplicate function + calls. + + + CREATE TYPE subrowtype AS (d int, e text); + CREATE TYPE myrowtype AS (a int, b text[], c subrowtype); + + + SELECT * FROM json_populate_record(NULL::myrowtype, + '{"a": 1, "b": ["2", "a b"], "c": {"d": 4, "e": "a b c"}, "x": "foo"}') + + + a | b | c +---+-----------+------------- + 1 | {2,"a b"} | (4,"a b c") + + + + + + + + jsonb_populate_record_valid + + jsonb_populate_record_valid ( base anyelement, from_json json ) + boolean + + + Function for testing jsonb_populate_record. Returns + true if the input jsonb_populate_record + would finish without an error for the given input JSON object; that is, it's + valid input, false otherwise. + + + CREATE TYPE jsb_char2 AS (a char(2)); + + + SELECT jsonb_populate_record_valid(NULL::jsb_char2, '{"a": "aaa"}'); + + + jsonb_populate_record_valid +----------------------------- + f +(1 row) + + + SELECT * FROM jsonb_populate_record(NULL::jsb_char2, '{"a": "aaa"}') q; + + +ERROR: value too long for type character(2) + + SELECT jsonb_populate_record_valid(NULL::jsb_char2, '{"a": "aa"}'); + + + jsonb_populate_record_valid +----------------------------- + t +(1 row) + + + SELECT * FROM jsonb_populate_record(NULL::jsb_char2, '{"a": "aa"}') q; + + + a +---- + aa +(1 row) + + + + + + + + json_populate_recordset + + json_populate_recordset ( base anyelement, from_json json ) + setof anyelement + + + + jsonb_populate_recordset + + jsonb_populate_recordset ( base anyelement, from_json jsonb ) + setof anyelement + + + Expands the top-level JSON array of objects to a set of rows having + the composite type of the base argument. + Each element of the JSON array is processed as described above + for json[b]_populate_record. + + + CREATE TYPE twoints AS (a int, b int); + + + SELECT * FROM json_populate_recordset(NULL::twoints, '[{"a":1,"b":2}, {"a":3,"b":4}]') + + + a | b +---+--- + 1 | 2 + 3 | 4 + + + + + + + + json_to_record + + json_to_record ( json ) + record + + + + jsonb_to_record + + jsonb_to_record ( jsonb ) + record + + + Expands the top-level JSON object to a row having the composite type + defined by an AS clause. (As with all functions + returning record, the calling query must explicitly + define the structure of the record with an AS + clause.) The output record is filled from fields of the JSON object, + in the same way as described above + for json[b]_populate_record. Since there is no + input record value, unmatched columns are always filled with nulls. + + + CREATE TYPE myrowtype AS (a int, b text); + + + SELECT * FROM json_to_record('{"a":1,"b":[1,2,3],"c":[1,2,3],"e":"bar","r": {"a": 123, "b": "a b c"}}') AS x(a int, b text, c int[], d text, r myrowtype) + + + a | b | c | d | r +---+---------+---------+---+--------------- + 1 | [1,2,3] | {1,2,3} | | (123,"a b c") + + + + + + + + json_to_recordset + + json_to_recordset ( json ) + setof record + + + + jsonb_to_recordset + + jsonb_to_recordset ( jsonb ) + setof record + + + Expands the top-level JSON array of objects to a set of rows having + the composite type defined by an AS clause. (As + with all functions returning record, the calling query + must explicitly define the structure of the record with + an AS clause.) Each element of the JSON array is + processed as described above + for json[b]_populate_record. + + + SELECT * FROM json_to_recordset('[{"a":1,"b":"foo"}, {"a":"2","c":"bar"}]') AS x(a int, b text) + + + a | b +---+----- + 1 | foo + 2 | + + + + + + + + jsonb_set + + jsonb_set ( target jsonb, path text[], new_value jsonb , create_if_missing boolean ) + jsonb + + + Returns target + with the item designated by path + replaced by new_value, or with + new_value added if + create_if_missing is true (which is the + default) and the item designated by path + does not exist. + All earlier steps in the path must exist, or + the target is returned unchanged. + As with the path oriented operators, negative integers that + appear in the path count from the end + of JSON arrays. + If the last path step is an array index that is out of range, + and create_if_missing is true, the new + value is added at the beginning of the array if the index is negative, + or at the end of the array if it is positive. + + + jsonb_set('[{"f1":1,"f2":null},2,null,3]', '{0,f1}', '[2,3,4]', false) + [{"f1": [2, 3, 4], "f2": null}, 2, null, 3] + + + jsonb_set('[{"f1":1,"f2":null},2]', '{0,f3}', '[2,3,4]') + [{"f1": 1, "f2": null, "f3": [2, 3, 4]}, 2] + + + + + + + jsonb_set_lax + + jsonb_set_lax ( target jsonb, path text[], new_value jsonb , create_if_missing boolean , null_value_treatment text ) + jsonb + + + If new_value is not NULL, + behaves identically to jsonb_set. Otherwise behaves + according to the value + of null_value_treatment which must be one + of 'raise_exception', + 'use_json_null', 'delete_key', or + 'return_target'. The default is + 'use_json_null'. + + + jsonb_set_lax('[{"f1":1,"f2":null},2,null,3]', '{0,f1}', null) + [{"f1": null, "f2": null}, 2, null, 3] + + + jsonb_set_lax('[{"f1":99,"f2":null},2]', '{0,f3}', null, true, 'return_target') + [{"f1": 99, "f2": null}, 2] + + + + + + + jsonb_insert + + jsonb_insert ( target jsonb, path text[], new_value jsonb , insert_after boolean ) + jsonb + + + Returns target + with new_value inserted. If the item + designated by the path is an array + element, new_value will be inserted before + that item if insert_after is false (which + is the default), or after it + if insert_after is true. If the item + designated by the path is an object + field, new_value will be inserted only if + the object does not already contain that key. + All earlier steps in the path must exist, or + the target is returned unchanged. + As with the path oriented operators, negative integers that + appear in the path count from the end + of JSON arrays. + If the last path step is an array index that is out of range, the new + value is added at the beginning of the array if the index is negative, + or at the end of the array if it is positive. + + + jsonb_insert('{"a": [0,1,2]}', '{a, 1}', '"new_value"') + {"a": [0, "new_value", 1, 2]} + + + jsonb_insert('{"a": [0,1,2]}', '{a, 1}', '"new_value"', true) + {"a": [0, 1, "new_value", 2]} + + + + + + + json_strip_nulls + + json_strip_nulls ( target json ,strip_in_arrays boolean ) + json + + + + jsonb_strip_nulls + + jsonb_strip_nulls ( target jsonb ,strip_in_arrays boolean ) + jsonb + + + Deletes all object fields that have null values from the given JSON + value, recursively. + If strip_in_arrays is true (the default is false), + null array elements are also stripped. + Otherwise they are not stripped. Bare null values are never stripped. + + + json_strip_nulls('[{"f1":1, "f2":null}, 2, null, 3]') + [{"f1":1},2,null,3] + + + jsonb_strip_nulls('[1,2,null,3,4]', true) + [1,2,3,4] + + + + + + + + jsonb_path_exists + + jsonb_path_exists ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + boolean + + + Checks whether the JSON path returns any item for the specified JSON + value. + (This is useful only with SQL-standard JSON path expressions, not + predicate check + expressions, since those always return a value.) + If the vars argument is specified, it must + be a JSON object, and its fields provide named values to be + substituted into the jsonpath expression. + If the silent argument is specified and + is true, the function suppresses the same errors + as the @? and @@ operators do. + + + jsonb_path_exists('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') + t + + + + + + + jsonb_path_match + + jsonb_path_match ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + boolean + + + Returns the SQL boolean result of a JSON path predicate check + for the specified JSON value. + (This is useful only + with predicate + check expressions, not SQL-standard JSON path expressions, + since it will either fail or return NULL if the + path result is not a single boolean value.) + The optional vars + and silent arguments act the same as + for jsonb_path_exists. + + + jsonb_path_match('{"a":[1,2,3,4,5]}', 'exists($.a[*] ? (@ >= $min && @ <= $max))', '{"min":2, "max":4}') + t + + + + + + + jsonb_path_query + + jsonb_path_query ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + setof jsonb + + + Returns all JSON items returned by the JSON path for the specified + JSON value. + For SQL-standard JSON path expressions it returns the JSON + values selected from target. + For predicate + check expressions it returns the result of the predicate + check: true, false, + or null. + The optional vars + and silent arguments act the same as + for jsonb_path_exists. + + + SELECT * FROM jsonb_path_query('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') + + + jsonb_path_query +------------------ + 2 + 3 + 4 + + + + + + + + jsonb_path_query_array + + jsonb_path_query_array ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + jsonb + + + Returns all JSON items returned by the JSON path for the specified + JSON value, as a JSON array. + The parameters are the same as + for jsonb_path_query. + + + jsonb_path_query_array('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') + [2, 3, 4] + + + + + + + jsonb_path_query_first + + jsonb_path_query_first ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + jsonb + + + Returns the first JSON item returned by the JSON path for the + specified JSON value, or NULL if there are no + results. + The parameters are the same as + for jsonb_path_query. + + + jsonb_path_query_first('{"a":[1,2,3,4,5]}', '$.a[*] ? (@ >= $min && @ <= $max)', '{"min":2, "max":4}') + 2 + + + + + + + jsonb_path_exists_tz + + jsonb_path_exists_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + boolean + + + + jsonb_path_match_tz + + jsonb_path_match_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + boolean + + + + jsonb_path_query_tz + + jsonb_path_query_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + setof jsonb + + + + jsonb_path_query_array_tz + + jsonb_path_query_array_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + jsonb + + + + jsonb_path_query_first_tz + + jsonb_path_query_first_tz ( target jsonb, path jsonpath , vars jsonb , silent boolean ) + jsonb + + + These functions act like their counterparts described above without + the _tz suffix, except that these functions support + comparisons of date/time values that require timezone-aware + conversions. The example below requires interpretation of the + date-only value 2015-08-02 as a timestamp with time + zone, so the result depends on the current + setting. Due to this dependency, these + functions are marked as stable, which means these functions cannot be + used in indexes. Their counterparts are immutable, and so can be used + in indexes; but they will throw errors if asked to make such + comparisons. + + + jsonb_path_exists_tz('["2015-08-01 12:00:00-05"]', '$[*] ? (@.datetime() < "2015-08-02".datetime())') + t + + + + + + + jsonb_pretty + + jsonb_pretty ( jsonb ) + text + + + Converts the given JSON value to pretty-printed, indented text. + + + jsonb_pretty('[{"f1":1,"f2":null}, 2]') + + +[ + { + "f1": 1, + "f2": null + }, + 2 +] + + + + + + + + json_typeof + + json_typeof ( json ) + text + + + + jsonb_typeof + + jsonb_typeof ( jsonb ) + text + + + Returns the type of the top-level JSON value as a text string. + Possible types are + object, array, + string, number, + boolean, and null. + (The null result should not be confused + with an SQL NULL; see the examples.) + + + json_typeof('-123.4') + number + + + json_typeof('null'::json) + null + + + json_typeof(NULL::json) IS NULL + t + + + + +
+
+ + + The SQL/JSON Path Language + + + SQL/JSON path language + + + + SQL/JSON path expressions specify item(s) to be retrieved + from a JSON value, similarly to XPath expressions used + for access to XML content. In PostgreSQL, + path expressions are implemented as the jsonpath + data type and can use any elements described in + . + + + + JSON query functions and operators + pass the provided path expression to the path engine + for evaluation. If the expression matches the queried JSON data, + the corresponding JSON item, or set of items, is returned. + If there is no match, the result will be NULL, + false, or an error, depending on the function. + Path expressions are written in the SQL/JSON path language + and can include arithmetic expressions and functions. + + + + A path expression consists of a sequence of elements allowed + by the jsonpath data type. + The path expression is normally evaluated from left to right, but + you can use parentheses to change the order of operations. + If the evaluation is successful, a sequence of JSON items is produced, + and the evaluation result is returned to the JSON query function + that completes the specified computation. + + + + To refer to the JSON value being queried (the + context item), use the $ variable + in the path expression. The first element of a path must always + be $. It can be followed by one or more + accessor operators, + which go down the JSON structure level by level to retrieve sub-items + of the context item. Each accessor operator acts on the + result(s) of the previous evaluation step, producing zero, one, or more + output items from each input item. + + + + For example, suppose you have some JSON data from a GPS tracker that you + would like to parse, such as: + +SELECT '{ + "track": { + "segments": [ + { + "location": [ 47.763, 13.4034 ], + "start time": "2018-10-14 10:05:14", + "HR": 73 + }, + { + "location": [ 47.706, 13.2635 ], + "start time": "2018-10-14 10:39:21", + "HR": 135 + } + ] + } +}' AS json \gset + + (The above example can be copied-and-pasted + into psql to set things up for the following + examples. Then psql will + expand :'json' into a suitably-quoted string + constant containing the JSON value.) + + + + To retrieve the available track segments, you need to use the + .key accessor + operator to descend through surrounding JSON objects, for example: + +=> SELECT jsonb_path_query(:'json', '$.track.segments'); + jsonb_path_query +-----------------------------------------------------------&zwsp;-----------------------------------------------------------&zwsp;--------------------------------------------- + [{"HR": 73, "location": [47.763, 13.4034], "start time": "2018-10-14 10:05:14"}, {"HR": 135, "location": [47.706, 13.2635], "start time": "2018-10-14 10:39:21"}] + + + + + To retrieve the contents of an array, you typically use the + [*] operator. + The following example will return the location coordinates for all + the available track segments: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[*].location'); + jsonb_path_query +------------------- + [47.763, 13.4034] + [47.706, 13.2635] + + Here we started with the whole JSON input value ($), + then the .track accessor selected the JSON object + associated with the "track" object key, then + the .segments accessor selected the JSON array + associated with the "segments" key within that + object, then the [*] accessor selected each element + of that array (producing a series of items), then + the .location accessor selected the JSON array + associated with the "location" key within each of + those objects. In this example, each of those objects had + a "location" key; but if any of them did not, + the .location accessor would have simply produced no + output for that input item. + + + + To return the coordinates of the first segment only, you can + specify the corresponding subscript in the [] + accessor operator. Recall that JSON array indexes are 0-relative: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[0].location'); + jsonb_path_query +------------------- + [47.763, 13.4034] + + + + + The result of each path evaluation step can be processed + by one or more of the jsonpath operators and methods + listed in . + Each method name must be preceded by a dot. For example, + you can get the size of an array: + +=> SELECT jsonb_path_query(:'json', '$.track.segments.size()'); + jsonb_path_query +------------------ + 2 + + More examples of using jsonpath operators + and methods within path expressions appear below in + . + + + + A path can also contain + filter expressions that work similarly to the + WHERE clause in SQL. A filter expression begins with + a question mark and provides a condition in parentheses: + + +? (condition) + + + + + Filter expressions must be written just after the path evaluation step + to which they should apply. The result of that step is filtered to include + only those items that satisfy the provided condition. SQL/JSON defines + three-valued logic, so the condition can + produce true, false, + or unknown. The unknown value + plays the same role as SQL NULL and can be tested + for with the IS UNKNOWN predicate. Further path + evaluation steps use only those items for which the filter expression + returned true. + + + + The functions and operators that can be used in filter expressions are + listed in . Within a + filter expression, the @ variable denotes the value + being considered (i.e., one result of the preceding path step). You can + write accessor operators after @ to retrieve component + items. + + + + For example, suppose you would like to retrieve all heart rate values higher + than 130. You can achieve this as follows: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[*].HR ? (@ > 130)'); + jsonb_path_query +------------------ + 135 + + + + + To get the start times of segments with such values, you have to + filter out irrelevant segments before selecting the start times, so the + filter expression is applied to the previous step, and the path used + in the condition is different: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[*] ? (@.HR > 130)."start time"'); + jsonb_path_query +----------------------- + "2018-10-14 10:39:21" + + + + + You can use several filter expressions in sequence, if required. + The following example selects start times of all segments that + contain locations with relevant coordinates and high heart rate values: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[*] ? (@.location[1] < 13.4) ? (@.HR > 130)."start time"'); + jsonb_path_query +----------------------- + "2018-10-14 10:39:21" + + + + + Using filter expressions at different nesting levels is also allowed. + The following example first filters all segments by location, and then + returns high heart rate values for these segments, if available: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[*] ? (@.location[1] < 13.4).HR ? (@ > 130)'); + jsonb_path_query +------------------ + 135 + + + + + You can also nest filter expressions within each other. + This example returns the size of the track if it contains any + segments with high heart rate values, or an empty sequence otherwise: + +=> SELECT jsonb_path_query(:'json', '$.track ? (exists(@.segments[*] ? (@.HR > 130))).segments.size()'); + jsonb_path_query +------------------ + 2 + + + + + Deviations from the SQL Standard + + PostgreSQL's implementation of the SQL/JSON path + language has the following deviations from the SQL/JSON standard. + + + + Boolean Predicate Check Expressions + + As an extension to the SQL standard, + a PostgreSQL path expression can be a + Boolean predicate, whereas the SQL standard allows predicates only within + filters. While SQL-standard path expressions return the relevant + element(s) of the queried JSON value, predicate check expressions + return the single three-valued jsonb result of the + predicate: true, + false, or null. + For example, we could write this SQL-standard filter expression: + +=> SELECT jsonb_path_query(:'json', '$.track.segments ?(@[*].HR > 130)'); + jsonb_path_query +-----------------------------------------------------------&zwsp;---------------------- + {"HR": 135, "location": [47.706, 13.2635], "start time": "2018-10-14 10:39:21"} + + The similar predicate check expression simply + returns true, indicating that a match exists: + +=> SELECT jsonb_path_query(:'json', '$.track.segments[*].HR > 130'); + jsonb_path_query +------------------ + true + + + + + + Predicate check expressions are required in the + @@ operator (and the + jsonb_path_match function), and should not be used + with the @? operator (or the + jsonb_path_exists function). + + + + + + Regular Expression Interpretation + + There are minor differences in the interpretation of regular + expression patterns used in like_regex filters, as + described in . + + + + + + Strict and Lax Modes + + When you query JSON data, the path expression may not match the + actual JSON data structure. An attempt to access a non-existent + member of an object or element of an array is defined as a + structural error. SQL/JSON path expressions have two modes + of handling structural errors: + + + + + + lax (default) — the path engine implicitly adapts + the queried data to the specified path. + Any structural errors that cannot be fixed as described below + are suppressed, producing no match. + + + + + strict — if a structural error occurs, an error is raised. + + + + + + Lax mode facilitates matching of a JSON document and path + expression when the JSON data does not conform to the expected schema. + If an operand does not match the requirements of a particular operation, + it can be automatically wrapped as an SQL/JSON array, or unwrapped by + converting its elements into an SQL/JSON sequence before performing + the operation. Also, comparison operators automatically unwrap their + operands in lax mode, so you can compare SQL/JSON arrays + out-of-the-box. An array of size 1 is considered equal to its sole element. + Automatic unwrapping is not performed when: + + + + The path expression contains type() or + size() methods that return the type + and the number of elements in the array, respectively. + + + + + The queried JSON data contain nested arrays. In this case, only + the outermost array is unwrapped, while all the inner arrays + remain unchanged. Thus, implicit unwrapping can only go one + level down within each path evaluation step. + + + + + + + For example, when querying the GPS data listed above, you can + abstract from the fact that it stores an array of segments + when using lax mode: + +=> SELECT jsonb_path_query(:'json', 'lax $.track.segments.location'); + jsonb_path_query +------------------- + [47.763, 13.4034] + [47.706, 13.2635] + + + + + In strict mode, the specified path must exactly match the structure of + the queried JSON document, so using this path + expression will cause an error: + +=> SELECT jsonb_path_query(:'json', 'strict $.track.segments.location'); +ERROR: jsonpath member accessor can only be applied to an object + + To get the same result as in lax mode, you have to explicitly unwrap the + segments array: + +=> SELECT jsonb_path_query(:'json', 'strict $.track.segments[*].location'); + jsonb_path_query +------------------- + [47.763, 13.4034] + [47.706, 13.2635] + + + + + The unwrapping behavior of lax mode can lead to surprising results. For + instance, the following query using the .** accessor + selects every HR value twice: + +=> SELECT jsonb_path_query(:'json', 'lax $.**.HR'); + jsonb_path_query +------------------ + 73 + 135 + 73 + 135 + + This happens because the .** accessor selects both + the segments array and each of its elements, while + the .HR accessor automatically unwraps arrays when + using lax mode. To avoid surprising results, we recommend using + the .** accessor only in strict mode. The + following query selects each HR value just once: + +=> SELECT jsonb_path_query(:'json', 'strict $.**.HR'); + jsonb_path_query +------------------ + 73 + 135 + + + + + The unwrapping of arrays can also lead to unexpected results. Consider this + example, which selects all the location arrays: + +=> SELECT jsonb_path_query(:'json', 'lax $.track.segments[*].location'); + jsonb_path_query +------------------- + [47.763, 13.4034] + [47.706, 13.2635] +(2 rows) + + As expected it returns the full arrays. But applying a filter expression + causes the arrays to be unwrapped to evaluate each item, returning only the + items that match the expression: + +=> SELECT jsonb_path_query(:'json', 'lax $.track.segments[*].location ?(@[*] > 15)'); + jsonb_path_query +------------------ + 47.763 + 47.706 +(2 rows) + + This despite the fact that the full arrays are selected by the path + expression. Use strict mode to restore selecting the arrays: + +=> SELECT jsonb_path_query(:'json', 'strict $.track.segments[*].location ?(@[*] > 15)'); + jsonb_path_query +------------------- + [47.763, 13.4034] + [47.706, 13.2635] +(2 rows) + + + + + + SQL/JSON Path Operators and Methods + + + shows the operators and + methods available in jsonpath. Note that while the unary + operators and methods can be applied to multiple values resulting from a + preceding path step, the binary operators (addition etc.) can only be + applied to single values. In lax mode, methods applied to an array will be + executed for each value in the array. The exceptions are + .type() and .size(), which apply to + the array itself. + + + + <type>jsonpath</type> Operators and Methods + + + + + Operator/Method + + + Description + + + Example(s) + + + + + + + + number + number + number + + + Addition + + + jsonb_path_query('[2]', '$[0] + 3') + 5 + + + + + + + number + number + + + Unary plus (no operation); unlike addition, this can iterate over + multiple values + + + jsonb_path_query_array('{"x": [2,3,4]}', '+ $.x') + [2, 3, 4] + + + + + + number - number + number + + + Subtraction + + + jsonb_path_query('[2]', '7 - $[0]') + 5 + + + + + + - number + number + + + Negation; unlike subtraction, this can iterate over + multiple values + + + jsonb_path_query_array('{"x": [2,3,4]}', '- $.x') + [-2, -3, -4] + + + + + + number * number + number + + + Multiplication + + + jsonb_path_query('[4]', '2 * $[0]') + 8 + + + + + + number / number + number + + + Division + + + jsonb_path_query('[8.5]', '$[0] / 2') + 4.2500000000000000 + + + + + + number % number + number + + + Modulo (remainder) + + + jsonb_path_query('[32]', '$[0] % 10') + 2 + + + + + + value . type() + string + + + Type of the JSON item (see json_typeof) + + + jsonb_path_query_array('[1, "2", {}]', '$[*].type()') + ["number", "string", "object"] + + + + + + value . size() + number + + + Size of the JSON item (number of array elements, or 1 if not an + array) + + + jsonb_path_query('{"m": [11, 15]}', '$.m.size()') + 2 + + + + + + value . boolean() + boolean + + + Boolean value converted from a JSON boolean, number, or string + + + jsonb_path_query_array('[1, "yes", false]', '$[*].boolean()') + [true, true, false] + + + + + + value . string() + string + + + String value converted from a JSON boolean, number, string, or + datetime + + + jsonb_path_query_array('[1.23, "xyz", false]', '$[*].string()') + ["1.23", "xyz", "false"] + + + jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp().string()') + "2023-08-15T12:34:56" + + + + + + value . double() + number + + + Approximate floating-point number converted from a JSON number or + string + + + jsonb_path_query('{"len": "1.9"}', '$.len.double() * 2') + 3.8 + + + + + + number . ceiling() + number + + + Nearest integer greater than or equal to the given number + + + jsonb_path_query('{"h": 1.3}', '$.h.ceiling()') + 2 + + + + + + number . floor() + number + + + Nearest integer less than or equal to the given number + + + jsonb_path_query('{"h": 1.7}', '$.h.floor()') + 1 + + + + + + number . abs() + number + + + Absolute value of the given number + + + jsonb_path_query('{"z": -0.3}', '$.z.abs()') + 0.3 + + + + + + value . bigint() + bigint + + + Big integer value converted from a JSON number or string + + + jsonb_path_query('{"len": "9876543219"}', '$.len.bigint()') + 9876543219 + + + + + + value . decimal( [ precision [ , scale ] ] ) + decimal + + + Rounded decimal value converted from a JSON number or string + (precision and scale must be + integer values) + + + jsonb_path_query('1234.5678', '$.decimal(6, 2)') + 1234.57 + + + + + + value . integer() + integer + + + Integer value converted from a JSON number or string + + + jsonb_path_query('{"len": "12345"}', '$.len.integer()') + 12345 + + + + + + value . number() + numeric + + + Numeric value converted from a JSON number or string + + + jsonb_path_query('{"len": "123.45"}', '$.len.number()') + 123.45 + + + + + + string . datetime() + datetime_type + (see note) + + + Date/time value converted from a string + + + jsonb_path_query('["2015-8-1", "2015-08-12"]', '$[*] ? (@.datetime() < "2015-08-2".datetime())') + "2015-8-1" + + + + + + string . datetime(template) + datetime_type + (see note) + + + Date/time value converted from a string using the + specified to_timestamp template + + + jsonb_path_query_array('["12:30", "18:40"]', '$[*].datetime("HH24:MI")') + ["12:30:00", "18:40:00"] + + + + + + string . date() + date + + + Date value converted from a string + + + jsonb_path_query('"2023-08-15"', '$.date()') + "2023-08-15" + + + + + + string . time() + time without time zone + + + Time without time zone value converted from a string + + + jsonb_path_query('"12:34:56"', '$.time()') + "12:34:56" + + + + + + string . time(precision) + time without time zone + + + Time without time zone value converted from a string, with fractional + seconds adjusted to the given precision + + + jsonb_path_query('"12:34:56.789"', '$.time(2)') + "12:34:56.79" + + + + + + string . time_tz() + time with time zone + + + Time with time zone value converted from a string + + + jsonb_path_query('"12:34:56 +05:30"', '$.time_tz()') + "12:34:56+05:30" + + + + + + string . time_tz(precision) + time with time zone + + + Time with time zone value converted from a string, with fractional + seconds adjusted to the given precision + + + jsonb_path_query('"12:34:56.789 +05:30"', '$.time_tz(2)') + "12:34:56.79+05:30" + + + + + + string . timestamp() + timestamp without time zone + + + Timestamp without time zone value converted from a string + + + jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp()') + "2023-08-15T12:34:56" + + + + + + string . timestamp(precision) + timestamp without time zone + + + Timestamp without time zone value converted from a string, with + fractional seconds adjusted to the given precision + + + jsonb_path_query('"2023-08-15 12:34:56.789"', '$.timestamp(2)') + "2023-08-15T12:34:56.79" + + + + + + string . timestamp_tz() + timestamp with time zone + + + Timestamp with time zone value converted from a string + + + jsonb_path_query('"2023-08-15 12:34:56 +05:30"', '$.timestamp_tz()') + "2023-08-15T12:34:56+05:30" + + + + + + string . timestamp_tz(precision) + timestamp with time zone + + + Timestamp with time zone value converted from a string, with fractional + seconds adjusted to the given precision + + + jsonb_path_query('"2023-08-15 12:34:56.789 +05:30"', '$.timestamp_tz(2)') + "2023-08-15T12:34:56.79+05:30" + + + + + + object . keyvalue() + array + + + The object's key-value pairs, represented as an array of objects + containing three fields: "key", + "value", and "id"; + "id" is a unique identifier of the object the + key-value pair belongs to + + + jsonb_path_query_array('{"x": "20", "y": 32}', '$.keyvalue()') + [{"id": 0, "key": "x", "value": "20"}, {"id": 0, "key": "y", "value": 32}] + + + + + + string . lower() + string + + + String converted to all lower case according to the rules of the database's locale. + + + jsonb_path_query('"TOM"', '$.lower()') + "tom" + + + + + + string . upper() + string + + + String converted to all upper case according to the rules of the database's locale. + + + jsonb_path_query('"tom"', '$.upper()') + "TOM" + + + + + + string . initcap() + string + + + String with the first letter of each word converted to upper case + according to the rules of the database's locale. Words are sequences + of alphanumeric characters separated by non-alphanumeric characters. + + + jsonb_path_query('"hi THOMAS"', '$.initcap()') + "Hi Thomas" + + + + + + string . replace(from, to) + string + + + String with all occurrences of substring from replaced with substring to. + + + jsonb_path_query('"abcdefabcdef"', '$.replace("cd", "XX")') + "abXXefabXXef" + + + + + + string . split_part(delimiter, n) + string + + + String split at occurrences of delimiter + and returns the n'th field (counting from + one) or, when n is negative, returns the + |n|'th-from-last field. + + + jsonb_path_query('"abc~@~def~@~ghi"', '$.split_part("~@~", 2)') + "def" + + + jsonb_path_query('"abc,def,ghi,jkl"', '$.split_part(",", 3)') + "ghi" + + + + + + string . ltrim([ characters ]) + string + + + String with the longest string containing only spaces or the + characters in characters removed from the + start of string + + + jsonb_path_query('" hello"', '$.ltrim()') + "hello" + + + jsonb_path_query('"zzzytest"', '$.ltrim("xyz")') + "test" + + + + + + string . rtrim([ characters ]) + string + + + String with the longest string containing only spaces or the + characters in characters removed from the + end of string + + + jsonb_path_query('"hello "', '$.rtrim()') + "hello" + + + jsonb_path_query('"testxxzx"', '$.rtrim("xyz")') + "test" + + + + + + string . btrim([ characters ]) + string + + + String with the longest string containing only spaces or the + characters in characters removed from the + start and end of string + + + jsonb_path_query('" hello "', '$.btrim()') + "hello" + + + jsonb_path_query('"xyxtrimyyx"', '$.btrim("xyz")') + "trim" + + + + + +
+ + + + The result type of the datetime() and + datetime(template) + methods can be date, timetz, time, + timestamptz, or timestamp. + Both methods determine their result type dynamically. + + + The datetime() method sequentially tries to + match its input string to the ISO formats + for date, timetz, time, + timestamptz, and timestamp. It stops on + the first matching format and emits the corresponding data type. + + + The datetime(template) + method determines the result type according to the fields used in the + provided template string. + + + The datetime() and + datetime(template) methods + use the same parsing rules as the to_timestamp SQL + function does (see ), with three + exceptions. First, these methods don't allow unmatched template + patterns. Second, only the following separators are allowed in the + template string: minus sign, period, solidus (slash), comma, apostrophe, + semicolon, colon and space. Third, separators in the template string + must exactly match the input string. + + + If different date/time types need to be compared, an implicit cast is + applied. A date value can be cast to timestamp + or timestamptz, timestamp can be cast to + timestamptz, and time to timetz. + However, all but the first of these conversions depend on the current + setting, and thus can only be performed + within timezone-aware jsonpath functions. Similarly, other + date/time-related methods that convert strings to date/time types + also do this casting, which may involve the current + setting. Therefore, these conversions can + also only be performed within timezone-aware jsonpath + functions. + + + + + shows the available + filter expression elements. + + + + <type>jsonpath</type> Filter Expression Elements + + + + + Predicate/Value + + + Description + + + Example(s) + + + + + + + + value == value + boolean + + + Equality comparison (this, and the other comparison operators, work on + all JSON scalar values) + + + jsonb_path_query_array('[1, "a", 1, 3]', '$[*] ? (@ == 1)') + [1, 1] + + + jsonb_path_query_array('[1, "a", 1, 3]', '$[*] ? (@ == "a")') + ["a"] + + + + + + value != value + boolean + + + value <> value + boolean + + + Non-equality comparison + + + jsonb_path_query_array('[1, 2, 1, 3]', '$[*] ? (@ != 1)') + [2, 3] + + + jsonb_path_query_array('["a", "b", "c"]', '$[*] ? (@ <> "b")') + ["a", "c"] + + + + + + value < value + boolean + + + Less-than comparison + + + jsonb_path_query_array('[1, 2, 3]', '$[*] ? (@ < 2)') + [1] + + + + + + value <= value + boolean + + + Less-than-or-equal-to comparison + + + jsonb_path_query_array('["a", "b", "c"]', '$[*] ? (@ <= "b")') + ["a", "b"] + + + + + + value > value + boolean + + + Greater-than comparison + + + jsonb_path_query_array('[1, 2, 3]', '$[*] ? (@ > 2)') + [3] + + + + + + value >= value + boolean + + + Greater-than-or-equal-to comparison + + + jsonb_path_query_array('[1, 2, 3]', '$[*] ? (@ >= 2)') + [2, 3] + + + + + + true + boolean + + + JSON constant true + + + jsonb_path_query('[{"name": "John", "parent": false}, {"name": "Chris", "parent": true}]', '$[*] ? (@.parent == true)') + {"name": "Chris", "parent": true} + + + + + + false + boolean + + + JSON constant false + + + jsonb_path_query('[{"name": "John", "parent": false}, {"name": "Chris", "parent": true}]', '$[*] ? (@.parent == false)') + {"name": "John", "parent": false} + + + + + + null + value + + + JSON constant null (note that, unlike in SQL, + comparison to null works normally) + + + jsonb_path_query('[{"name": "Mary", "job": null}, {"name": "Michael", "job": "driver"}]', '$[*] ? (@.job == null) .name') + "Mary" + + + + + + boolean && boolean + boolean + + + Boolean AND + + + jsonb_path_query('[1, 3, 7]', '$[*] ? (@ > 1 && @ < 5)') + 3 + + + + + + boolean || boolean + boolean + + + Boolean OR + + + jsonb_path_query('[1, 3, 7]', '$[*] ? (@ < 1 || @ > 5)') + 7 + + + + + + ! boolean + boolean + + + Boolean NOT + + + jsonb_path_query('[1, 3, 7]', '$[*] ? (!(@ < 5))') + 7 + + + + + + boolean is unknown + boolean + + + Tests whether a Boolean condition is unknown. + + + jsonb_path_query('[-1, 2, 7, "foo"]', '$[*] ? ((@ > 0) is unknown)') + "foo" + + + + + + string like_regex string flag string + boolean + + + Tests whether the first operand matches the regular expression + given by the second operand, optionally with modifications + described by a string of flag characters (see + ). + + + jsonb_path_query_array('["abc", "abd", "aBdC", "abdacb", "babc"]', '$[*] ? (@ like_regex "^ab.*c")') + ["abc", "abdacb"] + + + jsonb_path_query_array('["abc", "abd", "aBdC", "abdacb", "babc"]', '$[*] ? (@ like_regex "^ab.*c" flag "i")') + ["abc", "aBdC", "abdacb"] + + + + + + string starts with string + boolean + + + Tests whether the second operand is an initial substring of the first + operand. + + + jsonb_path_query('["John Smith", "Mary Stone", "Bob Johnson"]', '$[*] ? (@ starts with "John")') + "John Smith" + + + + + + exists ( path_expression ) + boolean + + + Tests whether a path expression matches at least one SQL/JSON item. + Returns unknown if the path expression would result + in an error; the second example uses this to avoid a no-such-key error + in strict mode. + + + jsonb_path_query('{"x": [1, 2], "y": [2, 4]}', 'strict $.* ? (exists (@ ? (@[*] > 2)))') + [2, 4] + + + jsonb_path_query_array('{"value": 41}', 'strict $ ? (exists (@.name)) .name') + [] + + + + +
+ +
+ + + SQL/JSON Regular Expressions + + + LIKE_REGEX + in SQL/JSON + + + + SQL/JSON path expressions allow matching text to a regular expression + with the like_regex filter. For example, the + following SQL/JSON path query would case-insensitively match all + strings in an array that start with an English vowel: + +$[*] ? (@ like_regex "^[aeiou]" flag "i") + + + + + The optional flag string may include one or more of + the characters + i for case-insensitive match, + m to allow ^ + and $ to match at newlines, + s to allow . to match a newline, + and q to quote the whole pattern (reducing the + behavior to a simple substring match). + + + + The SQL/JSON standard borrows its definition for regular expressions + from the LIKE_REGEX operator, which in turn uses the + XQuery standard. PostgreSQL does not currently support the + LIKE_REGEX operator. Therefore, + the like_regex filter is implemented using the + POSIX regular expression engine described in + . This leads to various minor + discrepancies from standard SQL/JSON behavior, which are cataloged in + . + Note, however, that the flag-letter incompatibilities described there + do not apply to SQL/JSON, as it translates the XQuery flag letters to + match what the POSIX engine expects. + + + + Keep in mind that the pattern argument of like_regex + is a JSON path string literal, written according to the rules given in + . This means in particular that any + backslashes you want to use in the regular expression must be doubled. + For example, to match string values of the root document that contain + only digits: + +$.* ? (@ like_regex "^\\d+$") + + + +
+ + + SQL/JSON Query Functions + + SQL/JSON functions JSON_EXISTS(), + JSON_QUERY(), and JSON_VALUE() + described in can be used + to query JSON documents. Each of these functions apply a + path_expression (an SQL/JSON path query) to a + context_item (the document). See + for more details on what + the path_expression can contain. The + path_expression can also reference variables, + whose values are specified with their respective names in the + PASSING clause that is supported by each function. + context_item can be a jsonb value + or a character string that can be successfully cast to jsonb. + + + + SQL/JSON Query Functions + + + + + Function signature + + + Description + + + Example(s) + + + + + + + json_exists + +JSON_EXISTS ( +context_item, path_expression + PASSING { value AS varname } , ... +{ TRUE | FALSE | UNKNOWN | ERROR } ON ERROR ) boolean + + + + + + Returns true if the SQL/JSON path_expression + applied to the context_item yields any + items, false otherwise. + + + + + The ON ERROR clause specifies the behavior if + an error occurs during path_expression + evaluation. Specifying ERROR will cause an error to + be thrown with the appropriate message. Other options include + returning boolean values FALSE or + TRUE or the value UNKNOWN which + is actually an SQL NULL. The default when no ON ERROR + clause is specified is to return the boolean value + FALSE. + + + + + Examples: + + + JSON_EXISTS(jsonb '{"key1": [1,2,3]}', 'strict $.key1[*] ? (@ > $x)' PASSING 2 AS x) + t + + + JSON_EXISTS(jsonb '{"a": [1,2,3]}', 'lax $.a[5]' ERROR ON ERROR) + f + + + JSON_EXISTS(jsonb '{"a": [1,2,3]}', 'strict $.a[5]' ERROR ON ERROR) + + +ERROR: jsonpath array subscript is out of bounds + + + + + + json_query + +JSON_QUERY ( +context_item, path_expression + PASSING { value AS varname } , ... + RETURNING data_type FORMAT JSON ENCODING UTF8 + { WITHOUT | WITH { CONDITIONAL | UNCONDITIONAL } } ARRAY WRAPPER + { KEEP | OMIT } QUOTES ON SCALAR STRING + { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON EMPTY + { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON ERROR ) jsonb + + + + + + Returns the result of applying the SQL/JSON + path_expression to the + context_item. + + + + + By default, the result is returned as a value of type jsonb, + though the RETURNING clause can be used to return + as some other type to which it can be successfully coerced. + + + + + If the path expression may return multiple values, it might be necessary + to wrap those values using the WITH WRAPPER clause to + make it a valid JSON string, because the default behavior is to not wrap + them, as if WITHOUT WRAPPER were specified. The + WITH WRAPPER clause is by default taken to mean + WITH UNCONDITIONAL WRAPPER, which means that even a + single result value will be wrapped. To apply the wrapper only when + multiple values are present, specify WITH CONDITIONAL WRAPPER. + Getting multiple values in result will be treated as an error if + WITHOUT WRAPPER is specified. + + + + + If the result is a scalar string, by default, the returned value will + be surrounded by quotes, making it a valid JSON value. It can be made + explicit by specifying KEEP QUOTES. Conversely, + quotes can be omitted by specifying OMIT QUOTES. + To ensure that the result is a valid JSON value, OMIT QUOTES + cannot be specified when WITH WRAPPER is also + specified. + + + + + The ON EMPTY clause specifies the behavior if + evaluating path_expression yields an empty + set. The ON ERROR clause specifies the behavior + if an error occurs when evaluating path_expression, + when coercing the result value to the RETURNING type, + or when evaluating the ON EMPTY expression if the + path_expression evaluation returns an empty + set. + + + + + For both ON EMPTY and ON ERROR, + specifying ERROR will cause an error to be thrown with + the appropriate message. Other options include returning an SQL NULL, an + empty array (EMPTY ARRAY), + an empty object (EMPTY OBJECT), or a user-specified + expression (DEFAULT expression) + that can be coerced to jsonb or the type specified in RETURNING. + The default when ON EMPTY or ON ERROR + is not specified is to return an SQL NULL value. + + + + + Examples: + + + JSON_QUERY(jsonb '[1,[2,3],null]', 'lax $[*][$off]' PASSING 1 AS off WITH CONDITIONAL WRAPPER) + 3 + + + JSON_QUERY(jsonb '{"a": "[1, 2]"}', 'lax $.a' OMIT QUOTES) + [1, 2] + + + JSON_QUERY(jsonb '{"a": "[1, 2]"}', 'lax $.a' RETURNING int[] OMIT QUOTES ERROR ON ERROR) + + +ERROR: malformed array literal: "[1, 2]" +DETAIL: Missing "]" after array dimensions. + + + + + + + json_value + +JSON_VALUE ( +context_item, path_expression + PASSING { value AS varname } , ... + RETURNING data_type + { ERROR | NULL | DEFAULT expression } ON EMPTY + { ERROR | NULL | DEFAULT expression } ON ERROR ) text + + + + + + Returns the result of applying the SQL/JSON + path_expression to the + context_item. + + + + + Only use JSON_VALUE() if the extracted value is + expected to be a single SQL/JSON scalar item; + getting multiple values will be treated as an error. If you expect that + extracted value might be an object or an array, use the + JSON_QUERY function instead. + + + + + By default, the result, which must be a single scalar value, is + returned as a value of type text, though the + RETURNING clause can be used to return as some + other type to which it can be successfully coerced. + + + + + The ON ERROR and ON EMPTY + clauses have similar semantics as mentioned in the description of + JSON_QUERY, except the set of values returned in + lieu of throwing an error is different. + + + + + Note that scalar strings returned by JSON_VALUE + always have their quotes removed, equivalent to specifying + OMIT QUOTES in JSON_QUERY. + + + + + Examples: + + + JSON_VALUE(jsonb '"123.45"', '$' RETURNING float) + 123.45 + + + JSON_VALUE(jsonb '"03:04 2015-02-01"', '$.datetime("HH24:MI YYYY-MM-DD")' RETURNING date) + 2015-02-01 + + + JSON_VALUE(jsonb '[1,2]', 'strict $[$off]' PASSING 1 AS off) + 2 + + + JSON_VALUE(jsonb '[1,2]', 'strict $[*]' DEFAULT 9 ON ERROR) + 9 + + + + + +
+ + + The context_item expression is converted to + jsonb by an implicit cast if the expression is not already of + type jsonb. Note, however, that any parsing errors that occur + during that conversion are thrown unconditionally, that is, are not + handled according to the (specified or implicit) ON ERROR + clause. + + + + + JSON_VALUE() returns an SQL NULL if + path_expression returns a JSON + null, whereas JSON_QUERY() returns + the JSON null as is. + + +
+ + + JSON_TABLE + + json_table + + + + JSON_TABLE is an SQL/JSON function which + queries JSON data + and presents the results as a relational view, which can be accessed as a + regular SQL table. You can use JSON_TABLE inside + the FROM clause of a SELECT, + UPDATE, or DELETE and as data source + in a MERGE statement. + + + + Taking JSON data as input, JSON_TABLE uses a JSON path + expression to extract a part of the provided data to use as a + row pattern for the constructed view. Each SQL/JSON + value given by the row pattern serves as source for a separate row in the + constructed view. + + + + To split the row pattern into columns, JSON_TABLE + provides the COLUMNS clause that defines the + schema of the created view. For each column, a separate JSON path expression + can be specified to be evaluated against the row pattern to get an SQL/JSON + value that will become the value for the specified column in a given output + row. + + + + JSON data stored at a nested level of the row pattern can be extracted using + the NESTED PATH clause. Each + NESTED PATH clause can be used to generate one or more + columns using the data from a nested level of the row pattern. Those + columns can be specified using a COLUMNS clause that + looks similar to the top-level COLUMNS clause. Rows constructed from + NESTED COLUMNS are called child rows and are joined + against the row constructed from the columns specified in the parent + COLUMNS clause to get the row in the final view. Child + columns themselves may contain a NESTED PATH + specification thus allowing to extract data located at arbitrary nesting + levels. Columns produced by multiple NESTED PATHs at the + same level are considered to be siblings of each + other and their rows after joining with the parent row are combined using + UNION. + + + + The rows produced by JSON_TABLE are laterally + joined to the row that generated them, so you do not have to explicitly join + the constructed view with the original table holding JSON + data. + + + + The syntax is: + + + +JSON_TABLE ( + context_item, path_expression AS json_path_name PASSING { value AS varname } , ... + COLUMNS ( json_table_column , ... ) + { ERROR | EMPTY ARRAY} ON ERROR +) + + +where json_table_column is: + + name FOR ORDINALITY + | name type + FORMAT JSON ENCODING UTF8 + PATH path_expression + { WITHOUT | WITH { CONDITIONAL | UNCONDITIONAL } } ARRAY WRAPPER + { KEEP | OMIT } QUOTES ON SCALAR STRING + { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON EMPTY + { ERROR | NULL | EMPTY { ARRAY | OBJECT } | DEFAULT expression } ON ERROR + | name type EXISTS PATH path_expression + { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR + | NESTED PATH path_expression AS json_path_name COLUMNS ( json_table_column , ... ) + + + + Each syntax element is described below in more detail. + + + + + + context_item, path_expression AS json_path_name PASSING { value AS varname } , ... + + + + The context_item specifies the input document + to query, the path_expression is an SQL/JSON + path expression defining the query, and json_path_name + is an optional name for the path_expression. + The optional PASSING clause provides data values for + the variables mentioned in the path_expression. + The result of the input data evaluation using the aforementioned elements + is called the row pattern, which is used as the + source for row values in the constructed view. + + + + + + + COLUMNS ( json_table_column , ... ) + + + + + The COLUMNS clause defining the schema of the + constructed view. In this clause, you can specify each column to be + filled with an SQL/JSON value obtained by applying a JSON path expression + against the row pattern. json_table_column has + the following variants: + + + + + + name FOR ORDINALITY + + + + Adds an ordinality column that provides sequential row numbering starting + from 1. Each NESTED PATH (see below) gets its own + counter for any nested ordinality columns. + + + + + + + name type + FORMAT JSON ENCODING UTF8 + PATH path_expression + + + + Inserts an SQL/JSON value obtained by applying + path_expression against the row pattern into + the view's output row after coercing it to specified + type. + + + Specifying FORMAT JSON makes it explicit that you + expect the value to be a valid json object. It only + makes sense to specify FORMAT JSON if + type is one of bpchar, + bytea, character varying, name, + json, jsonb, text, or a domain over + these types. + + + Optionally, you can specify WRAPPER and + QUOTES clauses to format the output. Note that + specifying OMIT QUOTES overrides + FORMAT JSON if also specified, because unquoted + literals do not constitute valid json values. + + + Optionally, you can use ON EMPTY and + ON ERROR clauses to specify whether to throw the error + or return the specified value when the result of JSON path evaluation is + empty and when an error occurs during JSON path evaluation or when + coercing the SQL/JSON value to the specified type, respectively. The + default for both is to return a NULL value. + + + + This clause is internally turned into and has the same semantics as + JSON_VALUE or JSON_QUERY. + The latter if the specified type is not a scalar type or if either of + FORMAT JSON, WRAPPER, or + QUOTES clause is present. + + + + + + + + name type + EXISTS PATH path_expression + + + + Inserts a boolean value obtained by applying + path_expression against the row pattern + into the view's output row after coercing it to specified + type. + + + The value corresponds to whether applying the PATH + expression to the row pattern yields any values. + + + The specified type should have a cast from the + boolean type. + + + Optionally, you can use ON ERROR to specify whether to + throw the error or return the specified value when an error occurs during + JSON path evaluation or when coercing SQL/JSON value to the specified + type. The default is to return a boolean value + FALSE. + + + + This clause is internally turned into and has the same semantics as + JSON_EXISTS. + + + + + + + + NESTED PATH path_expression AS json_path_name + COLUMNS ( json_table_column , ... ) + + + + + Extracts SQL/JSON values from nested levels of the row pattern, + generates one or more columns as defined by the COLUMNS + subclause, and inserts the extracted SQL/JSON values into those + columns. The json_table_column + expression in the COLUMNS subclause uses the same + syntax as in the parent COLUMNS clause. + + + + The NESTED PATH syntax is recursive, + so you can go down multiple nested levels by specifying several + NESTED PATH subclauses within each other. + It allows to unnest the hierarchy of JSON objects and arrays + in a single function invocation rather than chaining several + JSON_TABLE expressions in an SQL statement. + + + + + + + + In each variant of json_table_column described + above, if the PATH clause is omitted, path expression + $.name is used, where + name is the provided column name. + + + + + + + + + AS json_path_name + + + + + The optional json_path_name serves as an + identifier of the provided path_expression. + The name must be unique and distinct from the column names. + + + + + + + { ERROR | EMPTY } ON ERROR + + + + + The optional ON ERROR can be used to specify how to + handle errors when evaluating the top-level + path_expression. Use ERROR + if you want the errors to be thrown and EMPTY to + return an empty table, that is, a table containing 0 rows. Note that + this clause does not affect the errors that occur when evaluating + columns, for which the behavior depends on whether the + ON ERROR clause is specified against a given column. + + + + + + Examples + + + In the examples that follow, the following table containing JSON data + will be used: + + +CREATE TABLE my_films ( js jsonb ); + +INSERT INTO my_films VALUES ( +'{ "favorites" : [ + { "kind" : "comedy", "films" : [ + { "title" : "Bananas", + "director" : "Woody Allen"}, + { "title" : "The Dinner Game", + "director" : "Francis Veber" } ] }, + { "kind" : "horror", "films" : [ + { "title" : "Psycho", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "thriller", "films" : [ + { "title" : "Vertigo", + "director" : "Alfred Hitchcock" } ] }, + { "kind" : "drama", "films" : [ + { "title" : "Yojimbo", + "director" : "Akira Kurosawa" } ] } + ] }'); + + + + + The following query shows how to use JSON_TABLE to + turn the JSON objects in the my_films table + to a view containing columns for the keys kind, + title, and director contained in + the original JSON along with an ordinality column: + + +SELECT jt.* FROM + my_films, + JSON_TABLE (js, '$.favorites[*]' COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + title text PATH '$.films[*].title' WITH WRAPPER, + director text PATH '$.films[*].director' WITH WRAPPER)) AS jt; + + + + id | kind | title | director +----+----------+--------------------------------+---------------------------------- + 1 | comedy | ["Bananas", "The Dinner Game"] | ["Woody Allen", "Francis Veber"] + 2 | horror | ["Psycho"] | ["Alfred Hitchcock"] + 3 | thriller | ["Vertigo"] | ["Alfred Hitchcock"] + 4 | drama | ["Yojimbo"] | ["Akira Kurosawa"] +(4 rows) + + + + + The following is a modified version of the above query to show the + usage of PASSING arguments in the filter specified in + the top-level JSON path expression and the various options for the + individual columns: + + +SELECT jt.* FROM + my_films, + JSON_TABLE (js, '$.favorites[*] ? (@.films[*].director == $filter)' + PASSING 'Alfred Hitchcock' AS filter + COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + title text FORMAT JSON PATH '$.films[*].title' OMIT QUOTES, + director text PATH '$.films[*].director' KEEP QUOTES)) AS jt; + + + + id | kind | title | director +----+----------+---------+-------------------- + 1 | horror | Psycho | "Alfred Hitchcock" + 2 | thriller | Vertigo | "Alfred Hitchcock" +(2 rows) + + + + + The following is a modified version of the above query to show the usage + of NESTED PATH for populating title and director + columns, illustrating how they are joined to the parent columns id and + kind: + + +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*] ? (@.films[*].director == $filter)' + PASSING 'Alfred Hitchcock' AS filter + COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text FORMAT JSON PATH '$.title' OMIT QUOTES, + director text PATH '$.director' KEEP QUOTES))) AS jt; + + + + id | kind | title | director +----+----------+---------+-------------------- + 1 | horror | Psycho | "Alfred Hitchcock" + 2 | thriller | Vertigo | "Alfred Hitchcock" +(2 rows) + + + + + + The following is the same query but without the filter in the root + path: + + +SELECT jt.* FROM + my_films, + JSON_TABLE ( js, '$.favorites[*]' + COLUMNS ( + id FOR ORDINALITY, + kind text PATH '$.kind', + NESTED PATH '$.films[*]' COLUMNS ( + title text FORMAT JSON PATH '$.title' OMIT QUOTES, + director text PATH '$.director' KEEP QUOTES))) AS jt; + + + + id | kind | title | director +----+----------+-----------------+-------------------- + 1 | comedy | Bananas | "Woody Allen" + 1 | comedy | The Dinner Game | "Francis Veber" + 2 | horror | Psycho | "Alfred Hitchcock" + 3 | thriller | Vertigo | "Alfred Hitchcock" + 4 | drama | Yojimbo | "Akira Kurosawa" +(5 rows) + + + + + + The following shows another query using a different JSON + object as input. It shows the UNION "sibling join" between + NESTED paths $.movies[*] and + $.books[*] and also the usage of + FOR ORDINALITY column at NESTED + levels (columns movie_id, book_id, + and author_id): + + +SELECT * FROM JSON_TABLE ( +'{"favorites": + [{"movies": + [{"name": "One", "director": "John Doe"}, + {"name": "Two", "director": "Don Joe"}], + "books": + [{"name": "Mystery", "authors": [{"name": "Brown Dan"}]}, + {"name": "Wonder", "authors": [{"name": "Jun Murakami"}, {"name":"Craig Doe"}]}] +}]}'::json, '$.favorites[*]' +COLUMNS ( + user_id FOR ORDINALITY, + NESTED '$.movies[*]' + COLUMNS ( + movie_id FOR ORDINALITY, + mname text PATH '$.name', + director text), + NESTED '$.books[*]' + COLUMNS ( + book_id FOR ORDINALITY, + bname text PATH '$.name', + NESTED '$.authors[*]' + COLUMNS ( + author_id FOR ORDINALITY, + author_name text PATH '$.name')))); + + + + user_id | movie_id | mname | director | book_id | bname | author_id | author_name +---------+----------+-------+----------+---------+---------+-----------+-------------- + 1 | 1 | One | John Doe | | | | + 1 | 2 | Two | Don Joe | | | | + 1 | | | | 1 | Mystery | 1 | Brown Dan + 1 | | | | 2 | Wonder | 1 | Jun Murakami + 1 | | | | 2 | Wonder | 2 | Craig Doe +(5 rows) + + + + +
diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml new file mode 100644 index 0000000000000..65e50e65a8117 --- /dev/null +++ b/doc/src/sgml/func/func-logical.sgml @@ -0,0 +1,146 @@ + + Logical Operators + + + operator + logical + + + + Boolean + operators + operators, logical + + + + The usual logical operators are available: + + + AND (operator) + + + + OR (operator) + + + + NOT (operator) + + + + conjunction + + + + disjunction + + + + negation + + + +boolean AND boolean boolean +boolean OR boolean boolean +NOT boolean boolean + + + SQL uses a three-valued logic system with true, + false, and null, which represents unknown. + Observe the following truth tables: + + + + + + a + b + a AND b + a OR b + + + + + + TRUE + TRUE + TRUE + TRUE + + + + TRUE + FALSE + FALSE + TRUE + + + + TRUE + NULL + NULL + TRUE + + + + FALSE + FALSE + FALSE + FALSE + + + + FALSE + NULL + FALSE + NULL + + + + NULL + NULL + NULL + NULL + + + + + + + + + + a + NOT a + + + + + + TRUE + FALSE + + + + FALSE + TRUE + + + + NULL + NULL + + + + + + + + The operators AND and OR are + commutative, that is, you can switch the left and right operands + without affecting the result. (However, it is not guaranteed that + the left operand is evaluated before the right operand. See for more information about the + order of evaluation of subexpressions.) + + diff --git a/doc/src/sgml/func/func-matching.sgml b/doc/src/sgml/func/func-matching.sgml new file mode 100644 index 0000000000000..af60e9898defa --- /dev/null +++ b/doc/src/sgml/func/func-matching.sgml @@ -0,0 +1,2526 @@ + + Pattern Matching + + + pattern matching + + + + There are three separate approaches to pattern matching provided + by PostgreSQL: the traditional + SQL LIKE operator, the + more recent SIMILAR TO operator (added in + SQL:1999), and POSIX-style regular + expressions. Aside from the basic does this string match + this pattern? operators, functions are available to extract + or replace matching substrings and to split a string at matching + locations. + + + + + If you have pattern matching needs that go beyond this, + consider writing a user-defined function in Perl or Tcl. + + + + + + While most regular-expression searches can be executed very quickly, + regular expressions can be contrived that take arbitrary amounts of + time and memory to process. Be wary of accepting regular-expression + search patterns from hostile sources. If you must do so, it is + advisable to impose a statement timeout. + + + + Searches using SIMILAR TO patterns have the same + security hazards, since SIMILAR TO provides many + of the same capabilities as POSIX-style regular + expressions. + + + + LIKE searches, being much simpler than the other + two options, are safer to use with possibly-hostile pattern sources. + + + + + SIMILAR TO and POSIX-style regular + expressions do not support nondeterministic collations. If required, use + LIKE or apply a different collation to the expression + to work around this limitation. + + + + <function>LIKE</function> + + + LIKE + + + +string LIKE pattern ESCAPE escape-character +string NOT LIKE pattern ESCAPE escape-character + + + + The LIKE expression returns true if the + string matches the supplied + pattern. (As + expected, the NOT LIKE expression returns + false if LIKE returns true, and vice versa. + An equivalent expression is + NOT (string LIKE + pattern).) + + + + If pattern does not contain percent + signs or underscores, then the pattern only represents the string + itself; in that case LIKE acts like the + equals operator. An underscore (_) in + pattern stands for (matches) any single + character; a percent sign (%) matches any sequence + of zero or more characters. + + + + Some examples: + +'abc' LIKE 'abc' true +'abc' LIKE 'a%' true +'abc' LIKE '_b_' true +'abc' LIKE 'c' false + + + + + LIKE pattern matching supports nondeterministic + collations (see ), such as + case-insensitive collations or collations that, say, ignore punctuation. + So with a case-insensitive collation, one could have: + +'AbC' LIKE 'abc' COLLATE case_insensitive true +'AbC' LIKE 'a%' COLLATE case_insensitive true + + With collations that ignore certain characters or in general that consider + strings of different lengths equal, the semantics can become a bit more + complicated. Consider these examples: + +'.foo.' LIKE 'foo' COLLATE ign_punct true +'.foo.' LIKE 'f_o' COLLATE ign_punct true +'.foo.' LIKE '_oo' COLLATE ign_punct false + + The way the matching works is that the pattern is partitioned into + sequences of wildcards and non-wildcard strings (wildcards being + _ and %). For example, the pattern + f_o is partitioned into f, _, o, the + pattern _oo is partitioned into _, + oo. The input string matches the pattern if it can be + partitioned in such a way that the wildcards match one character or any + number of characters respectively and the non-wildcard partitions are + equal under the applicable collation. So for example, '.foo.' + LIKE 'f_o' COLLATE ign_punct is true because one can partition + .foo. into .f, o, o., and then + '.f' = 'f' COLLATE ign_punct, 'o' + matches the _ wildcard, and 'o.' = 'o' COLLATE + ign_punct. But '.foo.' LIKE '_oo' COLLATE + ign_punct is false because .foo. cannot be + partitioned in a way that the first character is any character and the + rest of the string compares equal to oo. (Note that + the single-character wildcard always matches exactly one character, + independent of the collation. So in this example, the + _ would match ., but then the rest + of the input string won't match the rest of the pattern.) + + + + LIKE pattern matching always covers the entire + string. Therefore, if it's desired to match a sequence anywhere within + a string, the pattern must start and end with a percent sign. + + + + To match a literal underscore or percent sign without matching + other characters, the respective character in + pattern must be + preceded by the escape character. The default escape + character is the backslash but a different one can be selected by + using the ESCAPE clause. To match the escape + character itself, write two escape characters. + + + + It's also possible to select no escape character by writing + ESCAPE ''. This effectively disables the + escape mechanism, which makes it impossible to turn off the + special meaning of underscore and percent signs in the pattern. + + + + According to the SQL standard, omitting ESCAPE + means there is no escape character (rather than defaulting to a + backslash), and a zero-length ESCAPE value is + disallowed. PostgreSQL's behavior in + this regard is therefore slightly nonstandard. + + + + The key word ILIKE can be used instead of + LIKE to make the match case-insensitive according to the + active locale. (But this does not support nondeterministic collations.) + This is not in the SQL standard but is a + PostgreSQL extension. + + + + The operator ~~ is equivalent to + LIKE, and ~~* corresponds to + ILIKE. There are also + !~~ and !~~* operators that + represent NOT LIKE and NOT + ILIKE, respectively. All of these operators are + PostgreSQL-specific. You may see these + operator names in EXPLAIN output and similar + places, since the parser actually translates LIKE + et al. to these operators. + + + + The phrases LIKE, ILIKE, + NOT LIKE, and NOT ILIKE are + generally treated as operators + in PostgreSQL syntax; for example they can + be used in expression + operator ANY + (subquery) constructs, although + an ESCAPE clause cannot be included there. In some + obscure cases it may be necessary to use the underlying operator names + instead. + + + + Also see the starts-with operator ^@ and the + corresponding starts_with() function, which are + useful in cases where simply matching the beginning of a string is + needed. + + + + + + <function>SIMILAR TO</function> Regular Expressions + + + regular expression + + + + + SIMILAR TO + + + substring + + + +string SIMILAR TO pattern ESCAPE escape-character +string NOT SIMILAR TO pattern ESCAPE escape-character + + + + The SIMILAR TO operator returns true or + false depending on whether its pattern matches the given string. + It is similar to LIKE, except that it + interprets the pattern using the SQL standard's definition of a + regular expression. SQL regular expressions are a curious cross + between LIKE notation and common (POSIX) regular + expression notation. + + + + Like LIKE, the SIMILAR TO + operator succeeds only if its pattern matches the entire string; + this is unlike common regular expression behavior where the pattern + can match any part of the string. + Also like + LIKE, SIMILAR TO uses + _ and % as wildcard characters denoting + any single character and any string, respectively (these are + comparable to . and .* in POSIX regular + expressions). + + + + In addition to these facilities borrowed from LIKE, + SIMILAR TO supports these pattern-matching + metacharacters borrowed from POSIX regular expressions: + + + + + | denotes alternation (either of two alternatives). + + + + + * denotes repetition of the previous item zero + or more times. + + + + + + denotes repetition of the previous item one + or more times. + + + + + ? denotes repetition of the previous item zero + or one time. + + + + + {m} denotes repetition + of the previous item exactly m times. + + + + + {m,} denotes repetition + of the previous item m or more times. + + + + + {m,n} + denotes repetition of the previous item at least m and + not more than n times. + + + + + Parentheses () can be used to group items into + a single logical item. + + + + + A bracket expression [...] specifies a character + class, just as in POSIX regular expressions. + + + + + Notice that the period (.) is not a metacharacter + for SIMILAR TO. + + + + As with LIKE, a backslash disables the special + meaning of any of these metacharacters. A different escape character + can be specified with ESCAPE, or the escape + capability can be disabled by writing ESCAPE ''. + + + + According to the SQL standard, omitting ESCAPE + means there is no escape character (rather than defaulting to a + backslash), and a zero-length ESCAPE value is + disallowed. PostgreSQL's behavior in + this regard is therefore slightly nonstandard. + + + + Another nonstandard extension is that following the escape character + with a letter or digit provides access to the escape sequences + defined for POSIX regular expressions; see + , + , and + below. + + + + Some examples: + +'abc' SIMILAR TO 'abc' true +'abc' SIMILAR TO 'a' false +'abc' SIMILAR TO '%(b|d)%' true +'abc' SIMILAR TO '(b|c)%' false +'-abc-' SIMILAR TO '%\mabc\M%' true +'xabcy' SIMILAR TO '%\mabc\M%' false + + + + + The substring function with three parameters + provides extraction of a substring that matches an SQL + regular expression pattern. The function can be written according + to standard SQL syntax: + +substring(string SIMILAR pattern ESCAPE escape-character) + + or using the now obsolete SQL:1999 syntax: + +substring(string FROM pattern FOR escape-character) + + or as a plain three-argument function: + +substring(string, pattern, escape-character) + + As with SIMILAR TO, the + specified pattern must match the entire data string, or else the + function fails and returns null. To indicate the part of the + pattern for which the matching data sub-string is of interest, + the pattern should contain + two occurrences of the escape character followed by a double quote + ("). + The text matching the portion of the pattern + between these separators is returned when the match is successful. + + + + The escape-double-quote separators actually + divide substring's pattern into three independent + regular expressions; for example, a vertical bar (|) + in any of the three sections affects only that section. Also, the first + and third of these regular expressions are defined to match the smallest + possible amount of text, not the largest, when there is any ambiguity + about how much of the data string matches which pattern. (In POSIX + parlance, the first and third regular expressions are forced to be + non-greedy.) + + + + As an extension to the SQL standard, PostgreSQL + allows there to be just one escape-double-quote separator, in which case + the third regular expression is taken as empty; or no separators, in which + case the first and third regular expressions are taken as empty. + + + + Some examples, with #" delimiting the return string: + +substring('foobar' SIMILAR '%#"o_b#"%' ESCAPE '#') oob +substring('foobar' SIMILAR '#"o_b#"%' ESCAPE '#') NULL + + + + + + <acronym>POSIX</acronym> Regular Expressions + + + regular expression + pattern matching + + + + lists the available + operators for pattern matching using POSIX regular expressions. + + + + Regular Expression Match Operators + + + + + + Operator + + + Description + + + Example(s) + + + + + + + + text ~ text + boolean + + + String matches regular expression, case sensitively + + + 'thomas' ~ 't.*ma' + t + + + + + + text ~* text + boolean + + + String matches regular expression, case-insensitively + + + 'thomas' ~* 'T.*ma' + t + + + + + + text !~ text + boolean + + + String does not match regular expression, case sensitively + + + 'thomas' !~ 't.*max' + t + + + + + + text !~* text + boolean + + + String does not match regular expression, case-insensitively + + + 'thomas' !~* 'T.*ma' + f + + + + +
+ + + POSIX regular expressions provide a more + powerful means for pattern matching than the LIKE and + SIMILAR TO operators. + Many Unix tools such as egrep, + sed, or awk use a pattern + matching language that is similar to the one described here. + + + + A regular expression is a character sequence that is an + abbreviated definition of a set of strings (a regular + set). A string is said to match a regular expression + if it is a member of the regular set described by the regular + expression. As with LIKE, pattern characters + match string characters exactly unless they are special characters + in the regular expression language — but regular expressions use + different special characters than LIKE does. + Unlike LIKE patterns, a + regular expression is allowed to match anywhere within a string, unless + the regular expression is explicitly anchored to the beginning or + end of the string. + + + + Some examples: + +'abcd' ~ 'bc' true +'abcd' ~ 'a.c' true — dot matches any character +'abcd' ~ 'a.*d' true — * repeats the preceding pattern item +'abcd' ~ '(b|x)' true — | means OR, parentheses group +'abcd' ~ '^a' true — ^ anchors to start of string +'abcd' ~ '^(b|c)' false — would match except for anchoring + + + + + The POSIX pattern language is described in much + greater detail in . + + + + POSIX Regular Expression Functions + + + This section describes the available functions for pattern matching + using POSIX regular expressions. + + + + <function>substring</function> + + substring + + + + The substring function with two parameters + provides extraction of a substring that matches a POSIX regular + expression pattern. It has the syntax: + +substring(string from pattern) text +substring(string, pattern) text + + (The syntax with from is SQL-standard, but + PostgreSQL also accepts a comma.) + It returns null if + there is no match, otherwise the first portion of the text that matched the + pattern. But if the pattern contains any parentheses, the portion + of the text that matched the first parenthesized subexpression (the + one whose left parenthesis comes first) is + returned. You can put parentheses around the whole expression + if you want to use parentheses within it without triggering this + exception. If you need parentheses in the pattern before the + subexpression you want to extract, see the non-capturing parentheses + described in . + + + + Some examples: + +substring('foobar' FROM 'o.b') oob +substring('foobar' FROM 'o(.)b') o + + + + + + <function>regexp_count</function> + + regexp_count + + + + The regexp_count function counts the number of + places where a POSIX regular expression pattern matches a string. + It has the syntax: + +regexp_count(string, pattern , start , flags ) integer + + pattern is searched for + in string, normally from the beginning of + the string, but if the start parameter is + provided then beginning from that character index. + The flags parameter is an optional text + string containing zero or more single-letter flags that change the + function's behavior. For example, including i in + flags specifies case-insensitive matching. + Supported flags are described in + . + + + + Some examples: + +regexp_count('ABCABCAXYaxy', 'A.') 3 +regexp_count('ABCABCAXYaxy', 'A.', 1, 'i') 4 + + + + + + <function>regexp_instr</function> + + regexp_instr + + + + The regexp_instr function returns the starting or + ending position of the N'th match of a + POSIX regular expression pattern to a string, or zero if there is no + such match. It has the syntax: + +regexp_instr(string, pattern , start , N , endoption , flags , subexpr ) integer + + pattern is searched for + in string, normally from the beginning of + the string, but if the start parameter is + provided then beginning from that character index. + If N is specified + then the N'th match of the pattern + is located, otherwise the first match is located. + If the endoption parameter is omitted or + specified as zero, the function returns the position of the first + character of the match. Otherwise, endoption + must be one, and the function returns the position of the character + following the match. + The flags parameter is an optional text + string containing zero or more single-letter flags that change the + function's behavior. Supported flags are described + in . + For a pattern containing parenthesized + subexpressions, subexpr is an integer + indicating which subexpression is of interest: the result identifies + the position of the substring matching that subexpression. + Subexpressions are numbered in the order of their leading parentheses. + When subexpr is omitted or zero, the result + identifies the position of the whole match regardless of + parenthesized subexpressions. + + + + Some examples: + +regexp_instr('number of your street, town zip, FR', '[^,]+', 1, 2) + 23 +regexp_instr(string=>'ABCDEFGHI', pattern=>'(c..)(...)', start=>1, "N"=>1, endoption=>0, flags=>'i', subexpr=>2) + 6 + + + + + + <function>regexp_like</function> + + regexp_like + + + + The regexp_like function checks whether a match + of a POSIX regular expression pattern occurs within a string, + returning boolean true or false. It has the syntax: + +regexp_like(string, pattern , flags ) boolean + + The flags parameter is an optional text + string containing zero or more single-letter flags that change the + function's behavior. Supported flags are described + in . + This function has the same results as the ~ + operator if no flags are specified. If only the i + flag is specified, it has the same results as + the ~* operator. + + + + Some examples: + +regexp_like('Hello World', 'world') false +regexp_like('Hello World', 'world', 'i') true + + + + + + <function>regexp_match</function> + + regexp_match + + + + The regexp_match function returns a text array of + matching substring(s) within the first match of a POSIX + regular expression pattern to a string. It has the syntax: + +regexp_match(string, pattern , flags ) text[] + + If there is no match, the result is NULL. + If a match is found, and the pattern contains no + parenthesized subexpressions, then the result is a single-element text + array containing the substring matching the whole pattern. + If a match is found, and the pattern contains + parenthesized subexpressions, then the result is a text array + whose n'th element is the substring matching + the n'th parenthesized subexpression of + the pattern (not counting non-capturing + parentheses; see for details). + The flags parameter is an optional text string + containing zero or more single-letter flags that change the function's + behavior. Supported flags are described + in . + + + + Some examples: + +SELECT regexp_match('foobarbequebaz', 'bar.*que'); + regexp_match +-------------- + {barbeque} +(1 row) + +SELECT regexp_match('foobarbequebaz', '(bar)(beque)'); + regexp_match +-------------- + {bar,beque} +(1 row) + + + + + + In the common case where you just want the whole matching substring + or NULL for no match, the best solution is to + use regexp_substr(). + However, regexp_substr() only exists + in PostgreSQL version 15 and up. When + working in older versions, you can extract the first element + of regexp_match()'s result, for example: + +SELECT (regexp_match('foobarbequebaz', 'bar.*que'))[1]; + regexp_match +-------------- + barbeque +(1 row) + + + + + + + <function>regexp_matches</function> + + regexp_matches + + + + The regexp_matches function returns a set of text arrays + of matching substring(s) within matches of a POSIX regular + expression pattern to a string. It has the syntax: + +regexp_matches(string, pattern , flags ) setof text[] + + The parameters are the same as + for regexp_match. + This function returns no rows if there is no match, one row if there is + a match and the g flag is not given, or N + rows if there are N matches and the g flag + is given. Each returned row is a text array containing the whole + matched substring or the substrings matching parenthesized + subexpressions of the pattern, just as described above + for regexp_match. + regexp_matches accepts all the flags shown + in , plus + the g flag which commands it to return all matches, not + just the first one. + + + + Some examples: + +SELECT regexp_matches('foo', 'not there'); + regexp_matches +---------------- +(0 rows) + +SELECT regexp_matches('foobarbequebazilbarfbonk', '(b[^b]+)(b[^b]+)', 'g'); + regexp_matches +---------------- + {bar,beque} + {bazil,barf} +(2 rows) + + + + + + In most cases regexp_matches() should be used with + the g flag, since if you only want the first match, it's + easier and more efficient to use regexp_match(). + However, regexp_match() only exists + in PostgreSQL version 10 and up. When working in older + versions, a common trick is to place a regexp_matches() + call in a sub-select, for example: + +SELECT col1, (SELECT regexp_matches(col2, '(bar)(beque)')) FROM tab; + + This produces a text array if there's a match, or NULL if + not, the same as regexp_match() would do. Without the + sub-select, this query would produce no output at all for table rows + without a match, which is typically not the desired behavior. + + + + + + <function>regexp_replace</function> + + regexp_replace + + + + The regexp_replace function provides substitution of + new text for substrings that match POSIX regular expression patterns. + It has the syntax: + +regexp_replace(string, pattern, replacement , flags ) text +regexp_replace(string, pattern, replacement, start , N , flags ) text + + The source string is returned unchanged if + there is no match to the pattern. If there is a + match, the string is returned with the + replacement string substituted for the matching + substring. The replacement string can contain + \n, where n is 1 + through 9, to indicate that the source substring matching the + n'th parenthesized subexpression of the pattern should be + inserted, and it can contain \& to indicate that the + substring matching the entire pattern should be inserted. Write + \\ if you need to put a literal backslash in the replacement + text. + pattern is searched for + in string, normally from the beginning of + the string, but if the start parameter is + provided then beginning from that character index. + By default, only the first match of the pattern is replaced. + If N is specified and is greater than zero, + then the N'th match of the pattern + is replaced. + If the g flag is given, or + if N is specified and is zero, then all + matches at or after the start position are + replaced. (The g flag is ignored + when N is specified.) + The flags parameter is an optional text + string containing zero or more single-letter flags that change the + function's behavior. Supported flags (though + not g) are + described in . + + + + Some examples: + +regexp_replace('foobarbaz', 'b..', 'X') + fooXbaz +regexp_replace('foobarbaz', 'b..', 'X', 'g') + fooXX +regexp_replace('foobarbaz', 'b(..)', 'X\1Y', 'g') + fooXarYXazY +regexp_replace('A PostgreSQL function', 'a|e|i|o|u', 'X', 1, 0, 'i') + X PXstgrXSQL fXnctXXn +regexp_replace(string=>'A PostgreSQL function', pattern=>'a|e|i|o|u', replacement=>'X', start=>1, "N"=>3, flags=>'i') + A PostgrXSQL function + + + + + + <function>regexp_split_to_table</function> + + regexp_split_to_table + + + + The regexp_split_to_table function splits a string using a POSIX + regular expression pattern as a delimiter. It has the syntax: + +regexp_split_to_table(string, pattern , flags ) setof text + + If there is no match to the pattern, the function returns the + string. If there is at least one match, for each match it returns + the text from the end of the last match (or the beginning of the string) + to the beginning of the match. When there are no more matches, it + returns the text from the end of the last match to the end of the string. + The flags parameter is an optional text string containing + zero or more single-letter flags that change the function's behavior. + regexp_split_to_table supports the flags described in + . + + + + Some examples: + +SELECT foo FROM regexp_split_to_table('the quick brown fox jumps over the lazy dog', '\s+') AS foo; + foo +------- + the + quick + brown + fox + jumps + over + the + lazy + dog +(9 rows) + +SELECT foo FROM regexp_split_to_table('the quick brown fox', '\s*') AS foo; + foo +----- + t + h + e + q + u + i + c + k + b + r + o + w + n + f + o + x +(16 rows) + + + + + As the last example demonstrates, + regexp_split_to_table ignores + zero-length matches that occur at the start or end of the string + or immediately after a previous match. This is contrary to the strict + definition of regexp matching that is implemented by + the other regexp functions, but is usually the most convenient behavior + in practice. Other software systems such as Perl use similar definitions. + + + + + <function>regexp_split_to_array</function> + + regexp_split_to_array + + + + The regexp_split_to_array function behaves the + same as + regexp_split_to_table, + except that regexp_split_to_array returns its + result as an array of text rather than a set. It has + the syntax: + +regexp_split_to_array(string, pattern , flags ) text[] + + The parameters are the same as + for regexp_split_to_table. + + + + An example: + +SELECT regexp_split_to_array('the quick brown fox jumps over the lazy dog', '\s+'); + regexp_split_to_array +----------------------------------------------- + {the,quick,brown,fox,jumps,over,the,lazy,dog} +(1 row) + + + + + + <function>regexp_substr</function> + + regexp_substr + + + + The regexp_substr function returns the substring + that matches a POSIX regular expression pattern, + or NULL if there is no match. It has the syntax: + +regexp_substr(string, pattern , start , N , flags , subexpr ) text + + pattern is searched for + in string, normally from the beginning of + the string, but if the start parameter is + provided then beginning from that character index. + If N is specified + then the N'th match of the pattern + is returned, otherwise the first match is returned. + The flags parameter is an optional text + string containing zero or more single-letter flags that change the + function's behavior. Supported flags are described + in . + For a pattern containing parenthesized + subexpressions, subexpr is an integer + indicating which subexpression is of interest: the result is the + substring matching that subexpression. + Subexpressions are numbered in the order of their leading parentheses. + When subexpr is omitted or zero, the result + is the whole match regardless of parenthesized subexpressions. + + + + Some examples: + +regexp_substr('number of your street, town zip, FR', '[^,]+', 1, 2) + town zip +regexp_substr('ABCDEFGHI', '(c..)(...)', 1, 1, 'i', 2) + FGH + + + + + + + + + POSIX Regular Expression Details + + + PostgreSQL's regular expressions are implemented + using a software package written by Henry Spencer. Much of + the description of regular expressions below is copied verbatim from his + manual. + + + + Regular expressions (REs), as defined in + POSIX 1003.2, come in two forms: + extended REs or EREs + (roughly those of egrep), and + basic REs or BREs + (roughly those of ed). + PostgreSQL supports both forms, and + also implements some extensions + that are not in the POSIX standard, but have become widely used + due to their availability in programming languages such as Perl and Tcl. + REs using these non-POSIX extensions are called + advanced REs or AREs + in this documentation. AREs are almost an exact superset of EREs, + but BREs have several notational incompatibilities (as well as being + much more limited). + We first describe the ARE and ERE forms, noting features that apply + only to AREs, and then describe how BREs differ. + + + + + PostgreSQL always initially presumes that a regular + expression follows the ARE rules. However, the more limited ERE or + BRE rules can be chosen by prepending an embedded option + to the RE pattern, as described in . + This can be useful for compatibility with applications that expect + exactly the POSIX 1003.2 rules. + + + + + A regular expression is defined as one or more + branches, separated by + |. It matches anything that matches one of the + branches. + + + + A branch is zero or more quantified atoms or + constraints, concatenated. + It matches a match for the first, followed by a match for the second, etc.; + an empty branch matches the empty string. + + + + A quantified atom is an atom possibly followed + by a single quantifier. + Without a quantifier, it matches a match for the atom. + With a quantifier, it can match some number of matches of the atom. + An atom can be any of the possibilities + shown in . + The possible quantifiers and their meanings are shown in + . + + + + A constraint matches an empty string, but matches only when + specific conditions are met. A constraint can be used where an atom + could be used, except it cannot be followed by a quantifier. + The simple constraints are shown in + ; + some more constraints are described later. + + + + + Regular Expression Atoms + + + + + + + Atom + Description + + + + + + (re) + (where re is any regular expression) + matches a match for + re, with the match noted for possible reporting + + + + (?:re) + as above, but the match is not noted for reporting + (a non-capturing set of parentheses) + (AREs only) + + + + . + matches any single character + + + + [chars] + a bracket expression, + matching any one of the chars (see + for more detail) + + + + \k + (where k is a non-alphanumeric character) + matches that character taken as an ordinary character, + e.g., \\ matches a backslash character + + + + \c + where c is alphanumeric + (possibly followed by other characters) + is an escape, see + (AREs only; in EREs and BREs, this matches c) + + + + { + when followed by a character other than a digit, + matches the left-brace character {; + when followed by a digit, it is the beginning of a + bound (see below) + + + + x + where x is a single character with no other + significance, matches that character + + + +
+ + + An RE cannot end with a backslash (\). + + + + Regular Expression Quantifiers + + + + + + + Quantifier + Matches + + + + + + * + a sequence of 0 or more matches of the atom + + + + + + a sequence of 1 or more matches of the atom + + + + ? + a sequence of 0 or 1 matches of the atom + + + + {m} + a sequence of exactly m matches of the atom + + + + {m,} + a sequence of m or more matches of the atom + + + + + {m,n} + a sequence of m through n + (inclusive) matches of the atom; m cannot exceed + n + + + + *? + non-greedy version of * + + + + +? + non-greedy version of + + + + + ?? + non-greedy version of ? + + + + {m}? + non-greedy version of {m} + + + + {m,}? + non-greedy version of {m,} + + + + + {m,n}? + non-greedy version of {m,n} + + + +
+ + + The forms using {...} + are known as bounds. + The numbers m and n within a bound are + unsigned decimal integers with permissible values from 0 to 255 inclusive. + + + + Non-greedy quantifiers (available in AREs only) match the + same possibilities as their corresponding normal (greedy) + counterparts, but prefer the smallest number rather than the largest + number of matches. + See for more detail. + + + + + A quantifier cannot immediately follow another quantifier, e.g., + ** is invalid. + A quantifier cannot + begin an expression or subexpression or follow + ^ or |. + + + + + Regular Expression Constraints + + + + + + + Constraint + Description + + + + + + ^ + matches at the beginning of the string + + + + $ + matches at the end of the string + + + + (?=re) + positive lookahead matches at any point + where a substring matching re begins + (AREs only) + + + + (?!re) + negative lookahead matches at any point + where no substring matching re begins + (AREs only) + + + + (?<=re) + positive lookbehind matches at any point + where a substring matching re ends + (AREs only) + + + + (?<!re) + negative lookbehind matches at any point + where no substring matching re ends + (AREs only) + + + +
+ + + Lookahead and lookbehind constraints cannot contain back + references (see ), + and all parentheses within them are considered non-capturing. + +
+ + + Bracket Expressions + + + A bracket expression is a list of + characters enclosed in []. It normally matches + any single character from the list (but see below). If the list + begins with ^, it matches any single character + not from the rest of the list. + If two characters + in the list are separated by -, this is + shorthand for the full range of characters between those two + (inclusive) in the collating sequence, + e.g., [0-9] in ASCII matches + any decimal digit. It is illegal for two ranges to share an + endpoint, e.g., a-c-e. Ranges are very + collating-sequence-dependent, so portable programs should avoid + relying on them. + + + + To include a literal ] in the list, make it the + first character (after ^, if that is used). To + include a literal -, make it the first or last + character, or the second endpoint of a range. To use a literal + - as the first endpoint of a range, enclose it + in [. and .] to make it a + collating element (see below). With the exception of these characters, + some combinations using [ + (see next paragraphs), and escapes (AREs only), all other special + characters lose their special significance within a bracket expression. + In particular, \ is not special when following + ERE or BRE rules, though it is special (as introducing an escape) + in AREs. + + + + Within a bracket expression, a collating element (a character, a + multiple-character sequence that collates as if it were a single + character, or a collating-sequence name for either) enclosed in + [. and .] stands for the + sequence of characters of that collating element. The sequence is + treated as a single element of the bracket expression's list. This + allows a bracket + expression containing a multiple-character collating element to + match more than one character, e.g., if the collating sequence + includes a ch collating element, then the RE + [[.ch.]]*c matches the first five characters of + chchcc. + + + + + PostgreSQL currently does not support multi-character collating + elements. This information describes possible future behavior. + + + + + Within a bracket expression, a collating element enclosed in + [= and =] is an equivalence + class, standing for the sequences of characters of all collating + elements equivalent to that one, including itself. (If there are + no other equivalent collating elements, the treatment is as if the + enclosing delimiters were [. and + .].) For example, if o and + ^ are the members of an equivalence class, then + [[=o=]], [[=^=]], and + [o^] are all synonymous. An equivalence class + cannot be an endpoint of a range. + + + + Within a bracket expression, the name of a character class + enclosed in [: and :] stands + for the list of all characters belonging to that class. A character + class cannot be used as an endpoint of a range. + The POSIX standard defines these character class + names: + alnum (letters and numeric digits), + alpha (letters), + blank (space and tab), + cntrl (control characters), + digit (numeric digits), + graph (printable characters except space), + lower (lower-case letters), + print (printable characters including space), + punct (punctuation), + space (any white space), + upper (upper-case letters), + and xdigit (hexadecimal digits). + The behavior of these standard character classes is generally + consistent across platforms for characters in the 7-bit ASCII set. + Whether a given non-ASCII character is considered to belong to one + of these classes depends on the collation + that is used for the regular-expression function or operator + (see ), or by default on the + database's LC_CTYPE locale setting (see + ). The classification of non-ASCII + characters can vary across platforms even in similarly-named + locales. (But the C locale never considers any + non-ASCII characters to belong to any of these classes.) + In addition to these standard character + classes, PostgreSQL defines + the word character class, which is the same as + alnum plus the underscore (_) + character, and + the ascii character class, which contains exactly + the 7-bit ASCII set. + + + + There are two special cases of bracket expressions: the bracket + expressions [[:<:]] and + [[:>:]] are constraints, + matching empty strings at the beginning + and end of a word respectively. A word is defined as a sequence + of word characters that is neither preceded nor followed by word + characters. A word character is any character belonging to the + word character class, that is, any letter, digit, + or underscore. This is an extension, compatible with but not + specified by POSIX 1003.2, and should be used with + caution in software intended to be portable to other systems. + The constraint escapes described below are usually preferable; they + are no more standard, but are easier to type. + + + + + Regular Expression Escapes + + + Escapes are special sequences beginning with \ + followed by an alphanumeric character. Escapes come in several varieties: + character entry, class shorthands, constraint escapes, and back references. + A \ followed by an alphanumeric character but not constituting + a valid escape is illegal in AREs. + In EREs, there are no escapes: outside a bracket expression, + a \ followed by an alphanumeric character merely stands for + that character as an ordinary character, and inside a bracket expression, + \ is an ordinary character. + (The latter is the one actual incompatibility between EREs and AREs.) + + + + Character-entry escapes exist to make it easier to specify + non-printing and other inconvenient characters in REs. They are + shown in . + + + + Class-shorthand escapes provide shorthands for certain + commonly-used character classes. They are + shown in . + + + + A constraint escape is a constraint, + matching the empty string if specific conditions are met, + written as an escape. They are + shown in . + + + + A back reference (\n) matches the + same string matched by the previous parenthesized subexpression specified + by the number n + (see ). For example, + ([bc])\1 matches bb or cc + but not bc or cb. + The subexpression must entirely precede the back reference in the RE. + Subexpressions are numbered in the order of their leading parentheses. + Non-capturing parentheses do not define subexpressions. + The back reference considers only the string characters matched by the + referenced subexpression, not any constraints contained in it. For + example, (^\d)\1 will match 22. + + + + Regular Expression Character-Entry Escapes + + + + + + + Escape + Description + + + + + + \a + alert (bell) character, as in C + + + + \b + backspace, as in C + + + + \B + synonym for backslash (\) to help reduce the need for backslash + doubling + + + + \cX + (where X is any character) the character whose + low-order 5 bits are the same as those of + X, and whose other bits are all zero + + + + \e + the character whose collating-sequence name + is ESC, + or failing that, the character with octal value 033 + + + + \f + form feed, as in C + + + + \n + newline, as in C + + + + \r + carriage return, as in C + + + + \t + horizontal tab, as in C + + + + \uwxyz + (where wxyz is exactly four hexadecimal digits) + the character whose hexadecimal value is + 0xwxyz + + + + + \Ustuvwxyz + (where stuvwxyz is exactly eight hexadecimal + digits) + the character whose hexadecimal value is + 0xstuvwxyz + + + + + \v + vertical tab, as in C + + + + \xhhh + (where hhh is any sequence of hexadecimal + digits) + the character whose hexadecimal value is + 0xhhh + (a single character no matter how many hexadecimal digits are used) + + + + + \0 + the character whose value is 0 (the null byte) + + + + \xy + (where xy is exactly two octal digits, + and is not a back reference) + the character whose octal value is + 0xy + + + + \xyz + (where xyz is exactly three octal digits, + and is not a back reference) + the character whose octal value is + 0xyz + + + +
+ + + Hexadecimal digits are 0-9, + a-f, and A-F. + Octal digits are 0-7. + + + + Numeric character-entry escapes specifying values outside the ASCII range + (0–127) have meanings dependent on the database encoding. When the + encoding is UTF-8, escape values are equivalent to Unicode code points, + for example \u1234 means the character U+1234. + For other multibyte encodings, character-entry escapes usually just + specify the concatenation of the byte values for the character. If the + escape value does not correspond to any legal character in the database + encoding, no error will be raised, but it will never match any data. + + + + The character-entry escapes are always taken as ordinary characters. + For example, \135 is ] in ASCII, but + \135 does not terminate a bracket expression. + + + + Regular Expression Class-Shorthand Escapes + + + + + + + Escape + Description + + + + + + \d + matches any digit, like + [[:digit:]] + + + + \s + matches any whitespace character, like + [[:space:]] + + + + \w + matches any word character, like + [[:word:]] + + + + \D + matches any non-digit, like + [^[:digit:]] + + + + \S + matches any non-whitespace character, like + [^[:space:]] + + + + \W + matches any non-word character, like + [^[:word:]] + + + +
+ + + The class-shorthand escapes also work within bracket expressions, + although the definitions shown above are not quite syntactically + valid in that context. + For example, [a-c\d] is equivalent to + [a-c[:digit:]]. + + + + Regular Expression Constraint Escapes + + + + + + + Escape + Description + + + + + + \A + matches only at the beginning of the string + (see for how this differs from + ^) + + + + \m + matches only at the beginning of a word + + + + \M + matches only at the end of a word + + + + \y + matches only at the beginning or end of a word + + + + \Y + matches only at a point that is not the beginning or end of a + word + + + + \Z + matches only at the end of the string + (see for how this differs from + $) + + + +
+ + + A word is defined as in the specification of + [[:<:]] and [[:>:]] above. + Constraint escapes are illegal within bracket expressions. + + + + Regular Expression Back References + + + + + + + Escape + Description + + + + + + \m + (where m is a nonzero digit) + a back reference to the m'th subexpression + + + + \mnn + (where m is a nonzero digit, and + nn is some more digits, and the decimal value + mnn is not greater than the number of closing capturing + parentheses seen so far) + a back reference to the mnn'th subexpression + + + +
+ + + + There is an inherent ambiguity between octal character-entry + escapes and back references, which is resolved by the following heuristics, + as hinted at above. + A leading zero always indicates an octal escape. + A single non-zero digit, not followed by another digit, + is always taken as a back reference. + A multi-digit sequence not starting with a zero is taken as a back + reference if it comes after a suitable subexpression + (i.e., the number is in the legal range for a back reference), + and otherwise is taken as octal. + + +
+ + + Regular Expression Metasyntax + + + In addition to the main syntax described above, there are some special + forms and miscellaneous syntactic facilities available. + + + + An RE can begin with one of two special director prefixes. + If an RE begins with ***:, + the rest of the RE is taken as an ARE. (This normally has no effect in + PostgreSQL, since REs are assumed to be AREs; + but it does have an effect if ERE or BRE mode had been specified by + the flags parameter to a regex function.) + If an RE begins with ***=, + the rest of the RE is taken to be a literal string, + with all characters considered ordinary characters. + + + + An ARE can begin with embedded options: + a sequence (?xyz) + (where xyz is one or more alphabetic characters) + specifies options affecting the rest of the RE. + These options override any previously determined options — + in particular, they can override the case-sensitivity behavior implied by + a regex operator, or the flags parameter to a regex + function. + The available option letters are + shown in . + Note that these same option letters are used in the flags + parameters of regex functions. + + + + ARE Embedded-Option Letters + + + + + + + Option + Description + + + + + + b + rest of RE is a BRE + + + + c + case-sensitive matching (overrides operator type) + + + + e + rest of RE is an ERE + + + + i + case-insensitive matching (see + ) (overrides operator type) + + + + m + historical synonym for n + + + + n + newline-sensitive matching (see + ) + + + + p + partial newline-sensitive matching (see + ) + + + + q + rest of RE is a literal (quoted) string, all ordinary + characters + + + + s + non-newline-sensitive matching (default) + + + + t + tight syntax (default; see below) + + + + w + inverse partial newline-sensitive (weird) matching + (see ) + + + + x + expanded syntax (see below) + + + +
+ + + Embedded options take effect at the ) terminating the sequence. + They can appear only at the start of an ARE (after the + ***: director if any). + + + + In addition to the usual (tight) RE syntax, in which all + characters are significant, there is an expanded syntax, + available by specifying the embedded x option. + In the expanded syntax, + white-space characters in the RE are ignored, as are + all characters between a # + and the following newline (or the end of the RE). This + permits paragraphing and commenting a complex RE. + There are three exceptions to that basic rule: + + + + + a white-space character or # preceded by \ is + retained + + + + + white space or # within a bracket expression is retained + + + + + white space and comments cannot appear within multi-character symbols, + such as (?: + + + + + For this purpose, white-space characters are blank, tab, newline, and + any character that belongs to the space character class. + + + + Finally, in an ARE, outside bracket expressions, the sequence + (?#ttt) + (where ttt is any text not containing a )) + is a comment, completely ignored. + Again, this is not allowed between the characters of + multi-character symbols, like (?:. + Such comments are more a historical artifact than a useful facility, + and their use is deprecated; use the expanded syntax instead. + + + + None of these metasyntax extensions is available if + an initial ***= director + has specified that the user's input be treated as a literal string + rather than as an RE. + +
+ + + Regular Expression Matching Rules + + + In the event that an RE could match more than one substring of a given + string, the RE matches the one starting earliest in the string. + If the RE could match more than one substring starting at that point, + either the longest possible match or the shortest possible match will + be taken, depending on whether the RE is greedy or + non-greedy. + + + + Whether an RE is greedy or not is determined by the following rules: + + + + Most atoms, and all constraints, have no greediness attribute (because + they cannot match variable amounts of text anyway). + + + + + Adding parentheses around an RE does not change its greediness. + + + + + A quantified atom with a fixed-repetition quantifier + ({m} + or + {m}?) + has the same greediness (possibly none) as the atom itself. + + + + + A quantified atom with other normal quantifiers (including + {m,n} + with m equal to n) + is greedy (prefers longest match). + + + + + A quantified atom with a non-greedy quantifier (including + {m,n}? + with m equal to n) + is non-greedy (prefers shortest match). + + + + + A branch — that is, an RE that has no top-level + | operator — has the same greediness as the first + quantified atom in it that has a greediness attribute. + + + + + An RE consisting of two or more branches connected by the + | operator is always greedy. + + + + + + + The above rules associate greediness attributes not only with individual + quantified atoms, but with branches and entire REs that contain quantified + atoms. What that means is that the matching is done in such a way that + the branch, or whole RE, matches the longest or shortest possible + substring as a whole. Once the length of the entire match + is determined, the part of it that matches any particular subexpression + is determined on the basis of the greediness attribute of that + subexpression, with subexpressions starting earlier in the RE taking + priority over ones starting later. + + + + An example of what this means: + +SELECT SUBSTRING('XY1234Z', 'Y*([0-9]{1,3})'); +Result: 123 +SELECT SUBSTRING('XY1234Z', 'Y*?([0-9]{1,3})'); +Result: 1 + + In the first case, the RE as a whole is greedy because Y* + is greedy. It can match beginning at the Y, and it matches + the longest possible string starting there, i.e., Y123. + The output is the parenthesized part of that, or 123. + In the second case, the RE as a whole is non-greedy because Y*? + is non-greedy. It can match beginning at the Y, and it matches + the shortest possible string starting there, i.e., Y1. + The subexpression [0-9]{1,3} is greedy but it cannot change + the decision as to the overall match length; so it is forced to match + just 1. + + + + In short, when an RE contains both greedy and non-greedy subexpressions, + the total match length is either as long as possible or as short as + possible, according to the attribute assigned to the whole RE. The + attributes assigned to the subexpressions only affect how much of that + match they are allowed to eat relative to each other. + + + + The quantifiers {1,1} and {1,1}? + can be used to force greediness or non-greediness, respectively, + on a subexpression or a whole RE. + This is useful when you need the whole RE to have a greediness attribute + different from what's deduced from its elements. As an example, + suppose that we are trying to separate a string containing some digits + into the digits and the parts before and after them. We might try to + do that like this: + +SELECT regexp_match('abc01234xyz', '(.*)(\d+)(.*)'); +Result: {abc0123,4,xyz} + + That didn't work: the first .* is greedy so + it eats as much as it can, leaving the \d+ to + match at the last possible place, the last digit. We might try to fix + that by making it non-greedy: + +SELECT regexp_match('abc01234xyz', '(.*?)(\d+)(.*)'); +Result: {abc,0,""} + + That didn't work either, because now the RE as a whole is non-greedy + and so it ends the overall match as soon as possible. We can get what + we want by forcing the RE as a whole to be greedy: + +SELECT regexp_match('abc01234xyz', '(?:(.*?)(\d+)(.*)){1,1}'); +Result: {abc,01234,xyz} + + Controlling the RE's overall greediness separately from its components' + greediness allows great flexibility in handling variable-length patterns. + + + + When deciding what is a longer or shorter match, + match lengths are measured in characters, not collating elements. + An empty string is considered longer than no match at all. + For example: + bb* + matches the three middle characters of abbbc; + (week|wee)(night|knights) + matches all ten characters of weeknights; + when (.*).* + is matched against abc the parenthesized subexpression + matches all three characters; and when + (a*)* is matched against bc + both the whole RE and the parenthesized + subexpression match an empty string. + + + + If case-independent matching is specified, + the effect is much as if all case distinctions had vanished from the + alphabet. + When an alphabetic that exists in multiple cases appears as an + ordinary character outside a bracket expression, it is effectively + transformed into a bracket expression containing both cases, + e.g., x becomes [xX]. + When it appears inside a bracket expression, all case counterparts + of it are added to the bracket expression, e.g., + [x] becomes [xX] + and [^x] becomes [^xX]. + + + + If newline-sensitive matching is specified, . + and bracket expressions using ^ + will never match the newline character + (so that matches will not cross lines unless the RE + explicitly includes a newline) + and ^ and $ + will match the empty string after and before a newline + respectively, in addition to matching at beginning and end of string + respectively. + But the ARE escapes \A and \Z + continue to match beginning or end of string only. + Also, the character class shorthands \D + and \W will match a newline regardless of this mode. + (Before PostgreSQL 14, they did not match + newlines when in newline-sensitive mode. + Write [^[:digit:]] + or [^[:word:]] to get the old behavior.) + + + + If partial newline-sensitive matching is specified, + this affects . and bracket expressions + as with newline-sensitive matching, but not ^ + and $. + + + + If inverse partial newline-sensitive matching is specified, + this affects ^ and $ + as with newline-sensitive matching, but not . + and bracket expressions. + This isn't very useful but is provided for symmetry. + + + + + Limits and Compatibility + + + No particular limit is imposed on the length of REs in this + implementation. However, + programs intended to be highly portable should not employ REs longer + than 256 bytes, + as a POSIX-compliant implementation can refuse to accept such REs. + + + + The only feature of AREs that is actually incompatible with + POSIX EREs is that \ does not lose its special + significance inside bracket expressions. + All other ARE features use syntax which is illegal or has + undefined or unspecified effects in POSIX EREs; + the *** syntax of directors likewise is outside the POSIX + syntax for both BREs and EREs. + + + + Many of the ARE extensions are borrowed from Perl, but some have + been changed to clean them up, and a few Perl extensions are not present. + Incompatibilities of note include \b, \B, + the lack of special treatment for a trailing newline, + the addition of complemented bracket expressions to the things + affected by newline-sensitive matching, + the restrictions on parentheses and back references in lookahead/lookbehind + constraints, and the longest/shortest-match (rather than first-match) + matching semantics. + + + + + Basic Regular Expressions + + + BREs differ from EREs in several respects. + In BREs, |, +, and ? + are ordinary characters and there is no equivalent + for their functionality. + The delimiters for bounds are + \{ and \}, + with { and } + by themselves ordinary characters. + The parentheses for nested subexpressions are + \( and \), + with ( and ) by themselves ordinary characters. + ^ is an ordinary character except at the beginning of the + RE or the beginning of a parenthesized subexpression, + $ is an ordinary character except at the end of the + RE or the end of a parenthesized subexpression, + and * is an ordinary character if it appears at the beginning + of the RE or the beginning of a parenthesized subexpression + (after a possible leading ^). + Finally, single-digit back references are available, and + \< and \> + are synonyms for + [[:<:]] and [[:>:]] + respectively; no other escapes are available in BREs. + + + + + + + Differences from SQL Standard and XQuery + + + LIKE_REGEX + + + + OCCURRENCES_REGEX + + + + POSITION_REGEX + + + + SUBSTRING_REGEX + + + + TRANSLATE_REGEX + + + + XQuery regular expressions + + + + Since SQL:2008, the SQL standard includes regular expression operators + and functions that performs pattern + matching according to the XQuery regular expression + standard: + + LIKE_REGEX + OCCURRENCES_REGEX + POSITION_REGEX + SUBSTRING_REGEX + TRANSLATE_REGEX + + PostgreSQL does not currently implement these + operators and functions. You can get approximately equivalent + functionality in each case as shown in . (Various optional clauses on + both sides have been omitted in this table.) + + + + Regular Expression Functions Equivalencies + + + + + SQL standard + PostgreSQL + + + + + + string LIKE_REGEX pattern + regexp_like(string, pattern) or string ~ pattern + + + + OCCURRENCES_REGEX(pattern IN string) + regexp_count(string, pattern) + + + + POSITION_REGEX(pattern IN string) + regexp_instr(string, pattern) + + + + SUBSTRING_REGEX(pattern IN string) + regexp_substr(string, pattern) + + + + TRANSLATE_REGEX(pattern IN string WITH replacement) + regexp_replace(string, pattern, replacement) + + + +
+ + + Regular expression functions similar to those provided by PostgreSQL are + also available in a number of other SQL implementations, whereas the + SQL-standard functions are not as widely implemented. Some of the + details of the regular expression syntax will likely differ in each + implementation. + + + + The SQL-standard operators and functions use XQuery regular expressions, + which are quite close to the ARE syntax described above. + Notable differences between the existing POSIX-based + regular-expression feature and XQuery regular expressions include: + + + + + XQuery character class subtraction is not supported. An example of + this feature is using the following to match only English + consonants: [a-z-[aeiou]]. + + + + + XQuery character class shorthands \c, + \C, \i, + and \I are not supported. + + + + + XQuery character class elements + using \p{UnicodeProperty} or the + inverse \P{UnicodeProperty} are not supported. + + + + + POSIX interprets character classes such as \w + (see ) + according to the prevailing locale (which you can control by + attaching a COLLATE clause to the operator or + function). XQuery specifies these classes by reference to Unicode + character properties, so equivalent behavior is obtained only with + a locale that follows the Unicode rules. + + + + + The SQL standard (not XQuery itself) attempts to cater for more + variants of newline than POSIX does. The + newline-sensitive matching options described above consider only + ASCII NL (\n) to be a newline, but SQL would have + us treat CR (\r), CRLF (\r\n) + (a Windows-style newline), and some Unicode-only characters like + LINE SEPARATOR (U+2028) as newlines as well. + Notably, . and \s should + count \r\n as one character not two according to + SQL. + + + + + Of the character-entry escapes described in + , + XQuery supports only \n, \r, + and \t. + + + + + XQuery does not support + the [:name:] syntax + for character classes within bracket expressions. + + + + + XQuery does not have lookahead or lookbehind constraints, + nor any of the constraint escapes described in + . + + + + + The metasyntax forms described in + do not exist in XQuery. + + + + + The regular expression flag letters defined by XQuery are + related to but not the same as the option letters for POSIX + (). While the + i and q options behave the + same, others do not: + + + + XQuery's s (allow dot to match newline) + and m (allow ^ + and $ to match at newlines) flags provide + access to the same behaviors as + POSIX's n, p + and w flags, but they + do not match the behavior of + POSIX's s and m flags. + Note in particular that dot-matches-newline is the default + behavior in POSIX but not XQuery. + + + + + XQuery's x (ignore whitespace in pattern) flag + is noticeably different from POSIX's expanded-mode flag. + POSIX's x flag also + allows # to begin a comment in the pattern, + and POSIX will not ignore a whitespace character after a + backslash. + + + + + + + + +
+
+
diff --git a/doc/src/sgml/func/func-math.sgml b/doc/src/sgml/func/func-math.sgml new file mode 100644 index 0000000000000..9dcf97e7c9e06 --- /dev/null +++ b/doc/src/sgml/func/func-math.sgml @@ -0,0 +1,1616 @@ + + Mathematical Functions and Operators + + + Mathematical operators are provided for many + PostgreSQL types. For types without + standard mathematical conventions + (e.g., date/time types) we + describe the actual behavior in subsequent sections. + + + + shows the mathematical + operators that are available for the standard numeric types. + Unless otherwise noted, operators shown as + accepting numeric_type are available for all + the types smallint, integer, + bigint, numeric, real, + and double precision. + Operators shown as accepting integral_type + are available for the types smallint, integer, + and bigint. + Except where noted, each form of an operator returns the same data type + as its argument(s). Calls involving multiple argument data types, such + as integer + numeric, + are resolved by using the type appearing later in these lists. + + + + Mathematical Operators + + + + + + Operator + + + Description + + + Example(s) + + + + + + + + numeric_type + numeric_type + numeric_type + + + Addition + + + 2 + 3 + 5 + + + + + + + numeric_type + numeric_type + + + Unary plus (no operation) + + + + 3.5 + 3.5 + + + + + + numeric_type - numeric_type + numeric_type + + + Subtraction + + + 2 - 3 + -1 + + + + + + - numeric_type + numeric_type + + + Negation + + + - (-4) + 4 + + + + + + numeric_type * numeric_type + numeric_type + + + Multiplication + + + 2 * 3 + 6 + + + + + + numeric_type / numeric_type + numeric_type + + + Division (for integral types, division truncates the result towards + zero) + + + 5.0 / 2 + 2.5000000000000000 + + + 5 / 2 + 2 + + + (-5) / 2 + -2 + + + + + + numeric_type % numeric_type + numeric_type + + + Modulo (remainder); available for smallint, + integer, bigint, and numeric + + + 5 % 4 + 1 + + + + + + numeric ^ numeric + numeric + + + double precision ^ double precision + double precision + + + Exponentiation + + + 2 ^ 3 + 8 + + + Unlike typical mathematical practice, multiple uses of + ^ will associate left to right by default: + + + 2 ^ 3 ^ 3 + 512 + + + 2 ^ (3 ^ 3) + 134217728 + + + + + + |/ double precision + double precision + + + Square root + + + |/ 25.0 + 5 + + + + + + ||/ double precision + double precision + + + Cube root + + + ||/ 64.0 + 4 + + + + + + @ numeric_type + numeric_type + + + Absolute value + + + @ -5.0 + 5.0 + + + + + + integral_type & integral_type + integral_type + + + Bitwise AND + + + 91 & 15 + 11 + + + + + + integral_type | integral_type + integral_type + + + Bitwise OR + + + 32 | 3 + 35 + + + + + + integral_type # integral_type + integral_type + + + Bitwise exclusive OR + + + 17 # 5 + 20 + + + + + + ~ integral_type + integral_type + + + Bitwise NOT + + + ~1 + -2 + + + + + + integral_type << integer + integral_type + + + Bitwise shift left + + + 1 << 4 + 16 + + + + + + integral_type >> integer + integral_type + + + Bitwise shift right + + + 8 >> 2 + 2 + + + + + +
+ + + shows the available + mathematical functions. + Many of these functions are provided in multiple forms with different + argument types. + Except where noted, any given form of a function returns the same + data type as its argument(s); cross-type cases are resolved in the + same way as explained above for operators. + The functions working with double precision data are mostly + implemented on top of the host system's C library; accuracy and behavior in + boundary cases can therefore vary depending on the host system. + + + + Mathematical Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + abs + + abs ( numeric_type ) + numeric_type + + + Absolute value + + + abs(-17.4) + 17.4 + + + + + + + cbrt + + cbrt ( double precision ) + double precision + + + Cube root + + + cbrt(64.0) + 4 + + + + + + + ceil + + ceil ( numeric ) + numeric + + + ceil ( double precision ) + double precision + + + Nearest integer greater than or equal to argument + + + ceil(42.2) + 43 + + + ceil(-42.8) + -42 + + + + + + + ceiling + + ceiling ( numeric ) + numeric + + + ceiling ( double precision ) + double precision + + + Nearest integer greater than or equal to argument (same + as ceil) + + + ceiling(95.3) + 96 + + + + + + + degrees + + degrees ( double precision ) + double precision + + + Converts radians to degrees + + + degrees(0.5) + 28.64788975654116 + + + + + + + div + + div ( y numeric, + x numeric ) + numeric + + + Integer quotient of y/x + (truncates towards zero) + + + div(9, 4) + 2 + + + + + + + erf + + erf ( double precision ) + double precision + + + Error function + + + erf(1.0) + 0.8427007929497149 + + + + + + + erfc + + erfc ( double precision ) + double precision + + + Complementary error function (1 - erf(x), without + loss of precision for large inputs) + + + erfc(1.0) + 0.15729920705028513 + + + + + + + exp + + exp ( numeric ) + numeric + + + exp ( double precision ) + double precision + + + Exponential (e raised to the given power) + + + exp(1.0) + 2.7182818284590452 + + + + + + + factorial + + factorial ( bigint ) + numeric + + + Factorial + + + factorial(5) + 120 + + + + + + + floor + + floor ( numeric ) + numeric + + + floor ( double precision ) + double precision + + + Nearest integer less than or equal to argument + + + floor(42.8) + 42 + + + floor(-42.8) + -43 + + + + + + + gamma + + gamma ( double precision ) + double precision + + + Gamma function + + + gamma(0.5) + 1.772453850905516 + + + gamma(6) + 120 + + + + + + + gcd + + gcd ( numeric_type, numeric_type ) + numeric_type + + + Greatest common divisor (the largest positive number that divides both + inputs with no remainder); returns 0 if both inputs + are zero; available for integer, bigint, + and numeric + + + gcd(1071, 462) + 21 + + + + + + + lcm + + lcm ( numeric_type, numeric_type ) + numeric_type + + + Least common multiple (the smallest strictly positive number that is + an integral multiple of both inputs); returns 0 if + either input is zero; available for integer, + bigint, and numeric + + + lcm(1071, 462) + 23562 + + + + + + + lgamma + + lgamma ( double precision ) + double precision + + + Natural logarithm of the absolute value of the gamma function + + + lgamma(1000) + 5905.220423209181 + + + + + + + ln + + ln ( numeric ) + numeric + + + ln ( double precision ) + double precision + + + Natural logarithm + + + ln(2.0) + 0.6931471805599453 + + + + + + + log + + log ( numeric ) + numeric + + + log ( double precision ) + double precision + + + Base 10 logarithm + + + log(100) + 2 + + + + + + + log10 + + log10 ( numeric ) + numeric + + + log10 ( double precision ) + double precision + + + Base 10 logarithm (same as log) + + + log10(1000) + 3 + + + + + + log ( b numeric, + x numeric ) + numeric + + + Logarithm of x to base b + + + log(2.0, 64.0) + 6.0000000000000000 + + + + + + + min_scale + + min_scale ( numeric ) + integer + + + Minimum scale (number of fractional decimal digits) needed + to represent the supplied value precisely + + + min_scale(8.4100) + 2 + + + + + + + mod + + mod ( y numeric_type, + x numeric_type ) + numeric_type + + + Remainder of y/x; + available for smallint, integer, + bigint, and numeric + + + mod(9, 4) + 1 + + + + + + + pi + + pi ( ) + double precision + + + Approximate value of π + + + pi() + 3.141592653589793 + + + + + + + power + + power ( a numeric, + b numeric ) + numeric + + + power ( a double precision, + b double precision ) + double precision + + + a raised to the power of b + + + power(9, 3) + 729 + + + + + + + radians + + radians ( double precision ) + double precision + + + Converts degrees to radians + + + radians(45.0) + 0.7853981633974483 + + + + + + + round + + round ( numeric ) + numeric + + + round ( double precision ) + double precision + + + Rounds to nearest integer. For numeric, ties are + broken by rounding away from zero. For double precision, + the tie-breaking behavior is platform dependent, but + round to nearest even is the most common rule. + + + round(42.4) + 42 + + + + + + round ( v numeric, s integer ) + numeric + + + Rounds v to s decimal + places. Ties are broken by rounding away from zero. + + + round(42.4382, 2) + 42.44 + + + round(1234.56, -1) + 1230 + + + + + + + scale + + scale ( numeric ) + integer + + + Scale of the argument (the number of decimal digits in the fractional part) + + + scale(8.4100) + 4 + + + + + + + sign + + sign ( numeric ) + numeric + + + sign ( double precision ) + double precision + + + Sign of the argument (-1, 0, or +1) + + + sign(-8.4) + -1 + + + + + + + sqrt + + sqrt ( numeric ) + numeric + + + sqrt ( double precision ) + double precision + + + Square root + + + sqrt(2) + 1.4142135623730951 + + + + + + + trim_scale + + trim_scale ( numeric ) + numeric + + + Reduces the value's scale (number of fractional decimal digits) by + removing trailing zeroes + + + trim_scale(8.4100) + 8.41 + + + + + + + trunc + + trunc ( numeric ) + numeric + + + trunc ( double precision ) + double precision + + + Truncates to integer (towards zero) + + + trunc(42.8) + 42 + + + trunc(-42.8) + -42 + + + + + + trunc ( v numeric, s integer ) + numeric + + + Truncates v to s + decimal places + + + trunc(42.4382, 2) + 42.43 + + + + + + + width_bucket + + width_bucket ( operand numeric, low numeric, high numeric, count integer ) + integer + + + width_bucket ( operand double precision, low double precision, high double precision, count integer ) + integer + + + Returns the number of the bucket in + which operand falls in a histogram + having count equal-width buckets spanning the + range low to high. + The buckets have inclusive lower bounds and exclusive upper bounds. + Returns 0 for an input less + than low, + or count+1 for an input + greater than or equal to high. + If low > high, + the behavior is mirror-reversed, with bucket 1 + now being the one just below low, and the + inclusive bounds now being on the upper side. + + + width_bucket(5.35, 0.024, 10.06, 5) + 3 + + + width_bucket(9, 10, 0, 10) + 2 + + + + + + width_bucket ( operand anycompatible, thresholds anycompatiblearray ) + integer + + + Returns the number of the bucket in + which operand falls given an array listing the + inclusive lower bounds of the buckets. + Returns 0 for an input less than the first lower + bound. operand and the array elements can be + of any type having standard comparison operators. + The thresholds array must be + sorted, smallest first, or unexpected results will be + obtained. + + + width_bucket(now(), array['yesterday', 'today', 'tomorrow']::timestamptz[]) + 2 + + + + +
+ + + shows functions for + generating random numbers. + + + + Random Functions + + + + + + Function + + + Description + + + Example(s) + + + + + + + + + random + + random ( ) + double precision + + + Returns a random value in the range 0.0 <= x < 1.0 + + + random() + 0.897124072839091 + + + + + + + random + + random ( min integer, max integer ) + integer + + + random ( min bigint, max bigint ) + bigint + + + random ( min numeric, max numeric ) + numeric + + + Returns a random value in the range + min <= x <= max. + For type numeric, the result will have the same number of + fractional decimal digits as min or + max, whichever has more. + + + random(1, 10) + 7 + + + random(-0.499, 0.499) + 0.347 + + + + + + + random_normal + + + random_normal ( + mean double precision + , stddev double precision ) + double precision + + + Returns a random value from the normal distribution with the given + parameters; mean defaults to 0.0 + and stddev defaults to 1.0 + + + random_normal(0.0, 1.0) + 0.051285419 + + + + + + + setseed + + setseed ( double precision ) + void + + + Sets the seed for subsequent random() and + random_normal() calls; + argument must be between -1.0 and 1.0, inclusive + + + setseed(0.12345) + + + + +
+ + + The random() and random_normal() + functions listed in and + use a + deterministic pseudo-random number generator. + It is fast but not suitable for cryptographic + applications; see the module for a more + secure alternative. + If setseed() is called, the series of results of + subsequent calls to these functions in the current session + can be repeated by re-issuing setseed() with the same + argument. + Without any prior setseed() call in the same + session, the first call to any of these functions obtains a seed + from a platform-dependent source of random bits. + + + + shows the + available trigonometric functions. Each of these functions comes in + two variants, one that measures angles in radians and one that + measures angles in degrees. + + + + Trigonometric Functions + + + + + + Function + + + Description + + + Example(s) + + + + + + + + + acos + + acos ( double precision ) + double precision + + + Inverse cosine, result in radians + + + acos(1) + 0 + + + + + + + acosd + + acosd ( double precision ) + double precision + + + Inverse cosine, result in degrees + + + acosd(0.5) + 60 + + + + + + + asin + + asin ( double precision ) + double precision + + + Inverse sine, result in radians + + + asin(1) + 1.5707963267948966 + + + + + + + asind + + asind ( double precision ) + double precision + + + Inverse sine, result in degrees + + + asind(0.5) + 30 + + + + + + + atan + + atan ( double precision ) + double precision + + + Inverse tangent, result in radians + + + atan(1) + 0.7853981633974483 + + + + + + + atand + + atand ( double precision ) + double precision + + + Inverse tangent, result in degrees + + + atand(1) + 45 + + + + + + + atan2 + + atan2 ( y double precision, + x double precision ) + double precision + + + Inverse tangent of + y/x, + result in radians + + + atan2(1, 0) + 1.5707963267948966 + + + + + + + atan2d + + atan2d ( y double precision, + x double precision ) + double precision + + + Inverse tangent of + y/x, + result in degrees + + + atan2d(1, 0) + 90 + + + + + + + cos + + cos ( double precision ) + double precision + + + Cosine, argument in radians + + + cos(0) + 1 + + + + + + + cosd + + cosd ( double precision ) + double precision + + + Cosine, argument in degrees + + + cosd(60) + 0.5 + + + + + + + cot + + cot ( double precision ) + double precision + + + Cotangent, argument in radians + + + cot(0.5) + 1.830487721712452 + + + + + + + cotd + + cotd ( double precision ) + double precision + + + Cotangent, argument in degrees + + + cotd(45) + 1 + + + + + + + sin + + sin ( double precision ) + double precision + + + Sine, argument in radians + + + sin(1) + 0.8414709848078965 + + + + + + + sind + + sind ( double precision ) + double precision + + + Sine, argument in degrees + + + sind(30) + 0.5 + + + + + + + tan + + tan ( double precision ) + double precision + + + Tangent, argument in radians + + + tan(1) + 1.5574077246549023 + + + + + + + tand + + tand ( double precision ) + double precision + + + Tangent, argument in degrees + + + tand(45) + 1 + + + + +
+ + + + Another way to work with angles measured in degrees is to use the unit + transformation functions radians() + and degrees() shown earlier. + However, using the degree-based trigonometric functions is preferred, + as that way avoids round-off error for special cases such + as sind(30). + + + + + shows the + available hyperbolic functions. + + + + Hyperbolic Functions + + + + + + Function + + + Description + + + Example(s) + + + + + + + + + sinh + + sinh ( double precision ) + double precision + + + Hyperbolic sine + + + sinh(1) + 1.1752011936438014 + + + + + + + cosh + + cosh ( double precision ) + double precision + + + Hyperbolic cosine + + + cosh(0) + 1 + + + + + + + tanh + + tanh ( double precision ) + double precision + + + Hyperbolic tangent + + + tanh(1) + 0.7615941559557649 + + + + + + + asinh + + asinh ( double precision ) + double precision + + + Inverse hyperbolic sine + + + asinh(1) + 0.881373587019543 + + + + + + + acosh + + acosh ( double precision ) + double precision + + + Inverse hyperbolic cosine + + + acosh(1) + 0 + + + + + + + atanh + + atanh ( double precision ) + double precision + + + Inverse hyperbolic tangent + + + atanh(0.5) + 0.5493061443340548 + + + + +
+ +
diff --git a/doc/src/sgml/func/func-merge-support.sgml b/doc/src/sgml/func/func-merge-support.sgml new file mode 100644 index 0000000000000..7f084271c13ae --- /dev/null +++ b/doc/src/sgml/func/func-merge-support.sgml @@ -0,0 +1,78 @@ + + Merge Support Functions + + + MERGE + RETURNING + + + + PostgreSQL includes one merge support function + that may be used in the RETURNING list of a + command to identify the action taken for each + row; see . + + + + Merge Support Functions + + + + + + Function + + + Description + + + + + + + + + merge_action + + merge_action ( ) + text + + + Returns the merge action command executed for the current row. This + will be 'INSERT', 'UPDATE', or + 'DELETE'. + + + + +
+ + + Example: + 0 THEN + UPDATE SET in_stock = true, quantity = s.quantity + WHEN MATCHED THEN + UPDATE SET in_stock = false, quantity = 0 + WHEN NOT MATCHED THEN + INSERT (product_id, in_stock, quantity) + VALUES (s.product_id, true, s.quantity) + RETURNING merge_action(), p.*; + + merge_action | product_id | in_stock | quantity +--------------+------------+----------+---------- + UPDATE | 1001 | t | 50 + UPDATE | 1002 | f | 0 + INSERT | 1003 | t | 10 +]]> + + + + Note that this function can only be used in the RETURNING + list of a MERGE command. It is an error to use it in any + other part of a query. + + +
diff --git a/doc/src/sgml/func/func-net.sgml b/doc/src/sgml/func/func-net.sgml new file mode 100644 index 0000000000000..1361a44c19767 --- /dev/null +++ b/doc/src/sgml/func/func-net.sgml @@ -0,0 +1,592 @@ + + Network Address Functions and Operators + + + The IP network address types, cidr and inet, + support the usual comparison operators shown in + + as well as the specialized operators and functions shown in + and + . + + + + Any cidr value can be cast to inet implicitly; + therefore, the operators and functions shown below as operating on + inet also work on cidr values. (Where there are + separate functions for inet and cidr, it is + because the behavior should be different for the two cases.) + Also, it is permitted to cast an inet value + to cidr. When this is done, any bits to the right of the + netmask are silently zeroed to create a valid cidr value. + + + + IP Address Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + inet << inet + boolean + + + Is subnet strictly contained by subnet? + This operator, and the next four, test for subnet inclusion. They + consider only the network parts of the two addresses (ignoring any + bits to the right of the netmasks) and determine whether one network + is identical to or a subnet of the other. + + + inet '192.168.1.5' << inet '192.168.1/24' + t + + + inet '192.168.0.5' << inet '192.168.1/24' + f + + + inet '192.168.1/24' << inet '192.168.1/24' + f + + + + + + inet <<= inet + boolean + + + Is subnet contained by or equal to subnet? + + + inet '192.168.1/24' <<= inet '192.168.1/24' + t + + + + + + inet >> inet + boolean + + + Does subnet strictly contain subnet? + + + inet '192.168.1/24' >> inet '192.168.1.5' + t + + + + + + inet >>= inet + boolean + + + Does subnet contain or equal subnet? + + + inet '192.168.1/24' >>= inet '192.168.1/24' + t + + + + + + inet && inet + boolean + + + Does either subnet contain or equal the other? + + + inet '192.168.1/24' && inet '192.168.1.80/28' + t + + + inet '192.168.1/24' && inet '192.168.2.0/28' + f + + + + + + ~ inet + inet + + + Computes bitwise NOT. + + + ~ inet '192.168.1.6' + 63.87.254.249 + + + + + + inet & inet + inet + + + Computes bitwise AND. + + + inet '192.168.1.6' & inet '0.0.0.255' + 0.0.0.6 + + + + + + inet | inet + inet + + + Computes bitwise OR. + + + inet '192.168.1.6' | inet '0.0.0.255' + 192.168.1.255 + + + + + + inet + bigint + inet + + + Adds an offset to an address. + + + inet '192.168.1.6' + 25 + 192.168.1.31 + + + + + + bigint + inet + inet + + + Adds an offset to an address. + + + 200 + inet '::ffff:fff0:1' + ::ffff:255.240.0.201 + + + + + + inet - bigint + inet + + + Subtracts an offset from an address. + + + inet '192.168.1.43' - 36 + 192.168.1.7 + + + + + + inet - inet + bigint + + + Computes the difference of two addresses. + + + inet '192.168.1.43' - inet '192.168.1.19' + 24 + + + inet '::1' - inet '::ffff:1' + -4294901760 + + + + +
+ + + IP Address Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + abbrev + + abbrev ( inet ) + text + + + Creates an abbreviated display format as text. + (The result is the same as the inet output function + produces; it is abbreviated only in comparison to the + result of an explicit cast to text, which for historical + reasons will never suppress the netmask part.) + + + abbrev(inet '10.1.0.0/32') + 10.1.0.0 + + + + + + abbrev ( cidr ) + text + + + Creates an abbreviated display format as text. + (The abbreviation consists of dropping all-zero octets to the right + of the netmask; more examples are in + .) + + + abbrev(cidr '10.1.0.0/16') + 10.1/16 + + + + + + + broadcast + + broadcast ( inet ) + inet + + + Computes the broadcast address for the address's network. + + + broadcast(inet '192.168.1.5/24') + 192.168.1.255/24 + + + + + + + family + + family ( inet ) + integer + + + Returns the address's family: 4 for IPv4, + 6 for IPv6. + + + family(inet '::1') + 6 + + + + + + + host + + host ( inet ) + text + + + Returns the IP address as text, ignoring the netmask. + + + host(inet '192.168.1.0/24') + 192.168.1.0 + + + + + + + hostmask + + hostmask ( inet ) + inet + + + Computes the host mask for the address's network. + + + hostmask(inet '192.168.23.20/30') + 0.0.0.3 + + + + + + + inet_merge + + inet_merge ( inet, inet ) + cidr + + + Computes the smallest network that includes both of the given networks. + + + inet_merge(inet '192.168.1.5/24', inet '192.168.2.5/24') + 192.168.0.0/22 + + + + + + + inet_same_family + + inet_same_family ( inet, inet ) + boolean + + + Tests whether the addresses belong to the same IP family. + + + inet_same_family(inet '192.168.1.5/24', inet '::1') + f + + + + + + + masklen + + masklen ( inet ) + integer + + + Returns the netmask length in bits. + + + masklen(inet '192.168.1.5/24') + 24 + + + + + + + netmask + + netmask ( inet ) + inet + + + Computes the network mask for the address's network. + + + netmask(inet '192.168.1.5/24') + 255.255.255.0 + + + + + + + network + + network ( inet ) + cidr + + + Returns the network part of the address, zeroing out + whatever is to the right of the netmask. + (This is equivalent to casting the value to cidr.) + + + network(inet '192.168.1.5/24') + 192.168.1.0/24 + + + + + + + set_masklen + + set_masklen ( inet, integer ) + inet + + + Sets the netmask length for an inet value. + The address part does not change. + + + set_masklen(inet '192.168.1.5/24', 16) + 192.168.1.5/16 + + + + + + set_masklen ( cidr, integer ) + cidr + + + Sets the netmask length for a cidr value. + Address bits to the right of the new netmask are set to zero. + + + set_masklen(cidr '192.168.1.0/24', 16) + 192.168.0.0/16 + + + + + + + text + + text ( inet ) + text + + + Returns the unabbreviated IP address and netmask length as text. + (This has the same result as an explicit cast to text.) + + + text(inet '192.168.1.5') + 192.168.1.5/32 + + + + +
+ + + + The abbrev, host, + and text functions are primarily intended to offer + alternative display formats for IP addresses. + + + + + The MAC address types, macaddr and macaddr8, + support the usual comparison operators shown in + + as well as the specialized functions shown in + . + In addition, they support the bitwise logical operators + ~, & and | + (NOT, AND and OR), just as shown above for IP addresses. + + + + MAC Address Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + trunc + + trunc ( macaddr ) + macaddr + + + Sets the last 3 bytes of the address to zero. The remaining prefix + can be associated with a particular manufacturer (using data not + included in PostgreSQL). + + + trunc(macaddr '12:34:56:78:90:ab') + 12:34:56:00:00:00 + + + + + + trunc ( macaddr8 ) + macaddr8 + + + Sets the last 5 bytes of the address to zero. The remaining prefix + can be associated with a particular manufacturer (using data not + included in PostgreSQL). + + + trunc(macaddr8 '12:34:56:78:90:ab:cd:ef') + 12:34:56:00:00:00:00:00 + + + + + + + macaddr8_set7bit + + macaddr8_set7bit ( macaddr8 ) + macaddr8 + + + Sets the 7th bit of the address to one, creating what is known as + modified EUI-64, for inclusion in an IPv6 address. + + + macaddr8_set7bit(macaddr8 '00:34:56:ab:cd:ef') + 02:34:56:ff:fe:ab:cd:ef + + + + +
+ +
diff --git a/doc/src/sgml/func/func-range.sgml b/doc/src/sgml/func/func-range.sgml new file mode 100644 index 0000000000000..3c5a34796a1d6 --- /dev/null +++ b/doc/src/sgml/func/func-range.sgml @@ -0,0 +1,1095 @@ + + Range/Multirange Functions and Operators + + + See for an overview of range types. + + + + shows the specialized operators + available for range types. + shows the specialized operators + available for multirange types. + In addition to those, the usual comparison operators shown in + are available for range + and multirange types. The comparison operators order first by the range lower + bounds, and only if those are equal do they compare the upper bounds. The + multirange operators compare each range until one is unequal. This + does not usually result in a useful overall ordering, but the operators are + provided to allow unique indexes to be constructed on ranges. + + + + Range Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + anyrange @> anyrange + boolean + + + Does the first range contain the second? + + + int4range(2,4) @> int4range(2,3) + t + + + + + + anyrange @> anyelement + boolean + + + Does the range contain the element? + + + '[2011-01-01,2011-03-01)'::tsrange @> '2011-01-10'::timestamp + t + + + + + + anyrange <@ anyrange + boolean + + + Is the first range contained by the second? + + + int4range(2,4) <@ int4range(1,7) + t + + + + + + anyelement <@ anyrange + boolean + + + Is the element contained in the range? + + + 42 <@ int4range(1,7) + f + + + + + + anyrange && anyrange + boolean + + + Do the ranges overlap, that is, have any elements in common? + + + int8range(3,7) && int8range(4,12) + t + + + + + + anyrange << anyrange + boolean + + + Is the first range strictly left of the second? + + + int8range(1,10) << int8range(100,110) + t + + + + + + anyrange >> anyrange + boolean + + + Is the first range strictly right of the second? + + + int8range(50,60) >> int8range(20,30) + t + + + + + + anyrange &< anyrange + boolean + + + Does the first range not extend to the right of the second? + + + int8range(1,20) &< int8range(18,20) + t + + + + + + anyrange &> anyrange + boolean + + + Does the first range not extend to the left of the second? + + + int8range(7,20) &> int8range(5,10) + t + + + + + + anyrange -|- anyrange + boolean + + + Are the ranges adjacent? + + + numrange(1.1,2.2) -|- numrange(2.2,3.3) + t + + + + + + anyrange + anyrange + anyrange + + + Computes the union of the ranges. The ranges must overlap or be + adjacent, so that the union is a single range (but + see range_merge()). + + + numrange(5,15) + numrange(10,20) + [5,20) + + + + + + anyrange * anyrange + anyrange + + + Computes the intersection of the ranges. + + + int8range(5,15) * int8range(10,20) + [10,15) + + + + + + anyrange - anyrange + anyrange + + + Computes the difference of the ranges. The second range must not be + contained in the first in such a way that the difference would not be + a single range. + + + int8range(5,15) - int8range(10,20) + [5,10) + + + + +
+ + + Multirange Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + anymultirange @> anymultirange + boolean + + + Does the first multirange contain the second? + + + '{[2,4)}'::int4multirange @> '{[2,3)}'::int4multirange + t + + + + + + anymultirange @> anyrange + boolean + + + Does the multirange contain the range? + + + '{[2,4)}'::int4multirange @> int4range(2,3) + t + + + + + + anymultirange @> anyelement + boolean + + + Does the multirange contain the element? + + + '{[2011-01-01,2011-03-01)}'::tsmultirange @> '2011-01-10'::timestamp + t + + + + + + anyrange @> anymultirange + boolean + + + Does the range contain the multirange? + + + '[2,4)'::int4range @> '{[2,3)}'::int4multirange + t + + + + + + anymultirange <@ anymultirange + boolean + + + Is the first multirange contained by the second? + + + '{[2,4)}'::int4multirange <@ '{[1,7)}'::int4multirange + t + + + + + + anymultirange <@ anyrange + boolean + + + Is the multirange contained by the range? + + + '{[2,4)}'::int4multirange <@ int4range(1,7) + t + + + + + + anyrange <@ anymultirange + boolean + + + Is the range contained by the multirange? + + + int4range(2,4) <@ '{[1,7)}'::int4multirange + t + + + + + + anyelement <@ anymultirange + boolean + + + Is the element contained by the multirange? + + + 4 <@ '{[1,7)}'::int4multirange + t + + + + + + anymultirange && anymultirange + boolean + + + Do the multiranges overlap, that is, have any elements in common? + + + '{[3,7)}'::int8multirange && '{[4,12)}'::int8multirange + t + + + + + + anymultirange && anyrange + boolean + + + Does the multirange overlap the range? + + + '{[3,7)}'::int8multirange && int8range(4,12) + t + + + + + + anyrange && anymultirange + boolean + + + Does the range overlap the multirange? + + + int8range(3,7) && '{[4,12)}'::int8multirange + t + + + + + + anymultirange << anymultirange + boolean + + + Is the first multirange strictly left of the second? + + + '{[1,10)}'::int8multirange << '{[100,110)}'::int8multirange + t + + + + + + anymultirange << anyrange + boolean + + + Is the multirange strictly left of the range? + + + '{[1,10)}'::int8multirange << int8range(100,110) + t + + + + + + anyrange << anymultirange + boolean + + + Is the range strictly left of the multirange? + + + int8range(1,10) << '{[100,110)}'::int8multirange + t + + + + + + anymultirange >> anymultirange + boolean + + + Is the first multirange strictly right of the second? + + + '{[50,60)}'::int8multirange >> '{[20,30)}'::int8multirange + t + + + + + + anymultirange >> anyrange + boolean + + + Is the multirange strictly right of the range? + + + '{[50,60)}'::int8multirange >> int8range(20,30) + t + + + + + + anyrange >> anymultirange + boolean + + + Is the range strictly right of the multirange? + + + int8range(50,60) >> '{[20,30)}'::int8multirange + t + + + + + + anymultirange &< anymultirange + boolean + + + Does the first multirange not extend to the right of the second? + + + '{[1,20)}'::int8multirange &< '{[18,20)}'::int8multirange + t + + + + + + anymultirange &< anyrange + boolean + + + Does the multirange not extend to the right of the range? + + + '{[1,20)}'::int8multirange &< int8range(18,20) + t + + + + + + anyrange &< anymultirange + boolean + + + Does the range not extend to the right of the multirange? + + + int8range(1,20) &< '{[18,20)}'::int8multirange + t + + + + + + anymultirange &> anymultirange + boolean + + + Does the first multirange not extend to the left of the second? + + + '{[7,20)}'::int8multirange &> '{[5,10)}'::int8multirange + t + + + + + + anymultirange &> anyrange + boolean + + + Does the multirange not extend to the left of the range? + + + '{[7,20)}'::int8multirange &> int8range(5,10) + t + + + + + + anyrange &> anymultirange + boolean + + + Does the range not extend to the left of the multirange? + + + int8range(7,20) &> '{[5,10)}'::int8multirange + t + + + + + + anymultirange -|- anymultirange + boolean + + + Are the multiranges adjacent? + + + '{[1.1,2.2)}'::nummultirange -|- '{[2.2,3.3)}'::nummultirange + t + + + + + + anymultirange -|- anyrange + boolean + + + Is the multirange adjacent to the range? + + + '{[1.1,2.2)}'::nummultirange -|- numrange(2.2,3.3) + t + + + + + + anyrange -|- anymultirange + boolean + + + Is the range adjacent to the multirange? + + + numrange(1.1,2.2) -|- '{[2.2,3.3)}'::nummultirange + t + + + + + + anymultirange + anymultirange + anymultirange + + + Computes the union of the multiranges. The multiranges need not overlap + or be adjacent. + + + '{[5,10)}'::nummultirange + '{[15,20)}'::nummultirange + {[5,10), [15,20)} + + + + + + anymultirange * anymultirange + anymultirange + + + Computes the intersection of the multiranges. + + + '{[5,15)}'::int8multirange * '{[10,20)}'::int8multirange + {[10,15)} + + + + + + anymultirange - anymultirange + anymultirange + + + Computes the difference of the multiranges. + + + '{[5,20)}'::int8multirange - '{[10,15)}'::int8multirange + {[5,10), [15,20)} + + + + +
+ + + The left-of/right-of/adjacent operators always return false when an empty + range or multirange is involved; that is, an empty range is not considered to + be either before or after any other range. + + + + Elsewhere empty ranges and multiranges are treated as the additive identity: + anything unioned with an empty value is itself. Anything minus an empty + value is itself. An empty multirange has exactly the same points as an empty + range. Every range contains the empty range. Every multirange contains as many + empty ranges as you like. + + + + The range union and difference operators will fail if the resulting range would + need to contain two disjoint sub-ranges, as such a range cannot be + represented. There are separate operators for union and difference that take + multirange parameters and return a multirange, and they do not fail even if + their arguments are disjoint. So if you need a union or difference operation + for ranges that may be disjoint, you can avoid errors by first casting your + ranges to multiranges. + + + + shows the functions + available for use with range types. + shows the functions + available for use with multirange types. + + + + Range Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + lower + + lower ( anyrange ) + anyelement + + + Extracts the lower bound of the range (NULL if the + range is empty or has no lower bound). + + + lower(numrange(1.1,2.2)) + 1.1 + + + + + + + upper + + upper ( anyrange ) + anyelement + + + Extracts the upper bound of the range (NULL if the + range is empty or has no upper bound). + + + upper(numrange(1.1,2.2)) + 2.2 + + + + + + + isempty + + isempty ( anyrange ) + boolean + + + Is the range empty? + + + isempty(numrange(1.1,2.2)) + f + + + + + + + lower_inc + + lower_inc ( anyrange ) + boolean + + + Is the range's lower bound inclusive? + + + lower_inc(numrange(1.1,2.2)) + t + + + + + + + upper_inc + + upper_inc ( anyrange ) + boolean + + + Is the range's upper bound inclusive? + + + upper_inc(numrange(1.1,2.2)) + f + + + + + + + lower_inf + + lower_inf ( anyrange ) + boolean + + + Does the range have no lower bound? (A lower bound of + -Infinity returns false.) + + + lower_inf('(,)'::daterange) + t + + + + + + + upper_inf + + upper_inf ( anyrange ) + boolean + + + Does the range have no upper bound? (An upper bound of + Infinity returns false.) + + + upper_inf('(,)'::daterange) + t + + + + + + + range_merge + + range_merge ( anyrange, anyrange ) + anyrange + + + Computes the smallest range that includes both of the given ranges. + + + range_merge('[1,2)'::int4range, '[3,4)'::int4range) + [1,4) + + + + + + + range_minus_multi + + range_minus_multi ( anyrange, anyrange ) + setof anyrange + + + Returns the non-empty range(s) remaining after subtracting the second range from the first. + One row is returned for each range, so if the second range splits the first into two parts, + there will be two results. If the subtraction yields an empty range, no rows are returned. + + + range_minus_multi('[0,10)'::int4range, '[3,4)'::int4range) + + + [0,3) + [4,10) + + + + + +
+ + + Multirange Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + lower + + lower ( anymultirange ) + anyelement + + + Extracts the lower bound of the multirange (NULL if the + multirange is empty or has no lower bound). + + + lower('{[1.1,2.2)}'::nummultirange) + 1.1 + + + + + + + upper + + upper ( anymultirange ) + anyelement + + + Extracts the upper bound of the multirange (NULL if the + multirange is empty or has no upper bound). + + + upper('{[1.1,2.2)}'::nummultirange) + 2.2 + + + + + + + isempty + + isempty ( anymultirange ) + boolean + + + Is the multirange empty? + + + isempty('{[1.1,2.2)}'::nummultirange) + f + + + + + + + lower_inc + + lower_inc ( anymultirange ) + boolean + + + Is the multirange's lower bound inclusive? + + + lower_inc('{[1.1,2.2)}'::nummultirange) + t + + + + + + + upper_inc + + upper_inc ( anymultirange ) + boolean + + + Is the multirange's upper bound inclusive? + + + upper_inc('{[1.1,2.2)}'::nummultirange) + f + + + + + + + lower_inf + + lower_inf ( anymultirange ) + boolean + + + Does the multirange have no lower bound? (A lower bound of + -Infinity returns false.) + + + lower_inf('{(,)}'::datemultirange) + t + + + + + + + upper_inf + + upper_inf ( anymultirange ) + boolean + + + Does the multirange have no upper bound? (An upper bound of + Infinity returns false.) + + + upper_inf('{(,)}'::datemultirange) + t + + + + + + + range_merge + + range_merge ( anymultirange ) + anyrange + + + Computes the smallest range that includes the entire multirange. + + + range_merge('{[1,2), [3,4)}'::int4multirange) + [1,4) + + + + + + + multirange (function) + + multirange ( anyrange ) + anymultirange + + + Returns a multirange containing just the given range. + + + multirange('[1,2)'::int4range) + {[1,2)} + + + + + + + unnest + for multirange + + unnest ( anymultirange ) + setof anyrange + + + Expands a multirange into a set of ranges in ascending order. + + + unnest('{[1,2), [3,4)}'::int4multirange) + + + [1,2) + [3,4) + + + + + + + + multirange_minus_multi + + multirange_minus_multi ( anymultirange, anymultirange ) + setof anymultirange + + + Returns the non-empty multirange(s) remaining after subtracting the second multirange from the first. + If the subtraction yields an empty multirange, no rows are returned. + Two rows are never returned, because a single multirange can always accommodate any result. + + + multirange_minus_multi('{[0,10)}'::int4multirange, '{[3,4)}'::int4multirange) + {[0,3), [4,10)} + + + + +
+ + + The lower_inc, upper_inc, + lower_inf, and upper_inf + functions all return false for an empty range or multirange. + +
diff --git a/doc/src/sgml/func/func-sequence.sgml b/doc/src/sgml/func/func-sequence.sgml new file mode 100644 index 0000000000000..4a2a6dc9369d6 --- /dev/null +++ b/doc/src/sgml/func/func-sequence.sgml @@ -0,0 +1,223 @@ + + Sequence Manipulation Functions + + + sequence + + + + This section describes functions for operating on sequence + objects, also called sequence generators or just sequences. + Sequence objects are special single-row tables created with . + Sequence objects are commonly used to generate unique identifiers + for rows of a table. The sequence functions, listed in , provide simple, multiuser-safe + methods for obtaining successive sequence values from sequence + objects. + + + + Sequence Functions + + + + + Function + + + Description + + + + + + + + + nextval + + nextval ( regclass ) + bigint + + + Advances the sequence object to its next value and returns that value. + This is done atomically: even if multiple sessions + execute nextval concurrently, each will safely + receive a distinct sequence value. + If the sequence object has been created with default parameters, + successive nextval calls will return successive + values beginning with 1. Other behaviors can be obtained by using + appropriate parameters in the + command. + + + This function requires USAGE + or UPDATE privilege on the sequence. + + + + + + + setval + + setval ( regclass, bigint , boolean ) + bigint + + + Sets the sequence object's current value, and optionally + its is_called flag. The two-parameter + form sets the sequence's last_value field to the + specified value and sets its is_called field to + true, meaning that the next + nextval will advance the sequence before + returning a value. The value that will be reported + by currval is also set to the specified value. + In the three-parameter form, is_called can be set + to either true + or false. true has the same + effect as the two-parameter form. If it is set + to false, the next nextval + will return exactly the specified value, and sequence advancement + commences with the following nextval. + Furthermore, the value reported by currval is not + changed in this case. For example, + +SELECT setval('myseq', 42); Next nextval will return 43 +SELECT setval('myseq', 42, true); Same as above +SELECT setval('myseq', 42, false); Next nextval will return 42 + + The result returned by setval is just the value of its + second argument. + + + This function requires UPDATE privilege on the + sequence. + + + + + + + currval + + currval ( regclass ) + bigint + + + Returns the value most recently obtained + by nextval for this sequence in the current + session. (An error is reported if nextval has + never been called for this sequence in this session.) Because this is + returning a session-local value, it gives a predictable answer whether + or not other sessions have executed nextval since + the current session did. + + + This function requires USAGE + or SELECT privilege on the sequence. + + + + + + + lastval + + lastval () + bigint + + + Returns the value most recently returned by + nextval in the current session. This function is + identical to currval, except that instead + of taking the sequence name as an argument it refers to whichever + sequence nextval was most recently applied to + in the current session. It is an error to call + lastval if nextval + has not yet been called in the current session. + + + This function requires USAGE + or SELECT privilege on the last used sequence. + + + + + + + pg_get_sequence_data + + pg_get_sequence_data ( regclass ) + record + ( last_value bigint, + is_called bool, + page_lsn pg_lsn ) + + + Returns information about the sequence. + last_value is the last sequence value + written to disk. If caching is used, this value can be greater than the + last value handed out from the sequence. + is_called indicates whether the sequence has + been used. page_lsn is the LSN corresponding + to the most recent WAL record that modified this sequence relation. + + + This function is primarily intended for internal use by pg_dump and by + logical replication to synchronize sequences. It requires + USAGE or SELECT privilege on the + sequence. + + + + +
+ + + + To avoid blocking concurrent transactions that obtain numbers from + the same sequence, the value obtained by nextval + is not reclaimed for re-use if the calling transaction later aborts. + This means that transaction aborts or database crashes can result in + gaps in the sequence of assigned values. That can happen without a + transaction abort, too. For example an INSERT with + an ON CONFLICT clause will compute the to-be-inserted + tuple, including doing any required nextval + calls, before detecting any conflict that would cause it to follow + the ON CONFLICT rule instead. + Thus, PostgreSQL sequence + objects cannot be used to obtain gapless + sequences. + + + + Likewise, sequence state changes made by setval + are immediately visible to other transactions, and are not undone if + the calling transaction rolls back. + + + + If the database cluster crashes before committing a transaction + containing a nextval + or setval call, the sequence state change might + not have made its way to persistent storage, so that it is uncertain + whether the sequence will have its original or updated state after the + cluster restarts. This is harmless for usage of the sequence within + the database, since other effects of uncommitted transactions will not + be visible either. However, if you wish to use a sequence value for + persistent outside-the-database purposes, make sure that the + nextval call has been committed before doing so. + + + + + The sequence to be operated on by a sequence function is specified by + a regclass argument, which is simply the OID of the sequence in the + pg_class system catalog. You do not have to look up the + OID by hand, however, since the regclass data type's input + converter will do the work for you. See + for details. + +
diff --git a/doc/src/sgml/func/func-srf.sgml b/doc/src/sgml/func/func-srf.sgml new file mode 100644 index 0000000000000..34a45971aadf5 --- /dev/null +++ b/doc/src/sgml/func/func-srf.sgml @@ -0,0 +1,306 @@ + + Set Returning Functions + + + set returning functions + functions + + + + This section describes functions that possibly return more than one row. + The most widely used functions in this class are series generating + functions, as detailed in and + . Other, more specialized + set-returning functions are described elsewhere in this manual. + See for ways to combine multiple + set-returning functions. + + + + Series Generating Functions + + + + + Function + + + Description + + + + + + + + + generate_series + + generate_series ( start integer, stop integer , step integer ) + setof integer + + + generate_series ( start bigint, stop bigint , step bigint ) + setof bigint + + + generate_series ( start numeric, stop numeric , step numeric ) + setof numeric + + + Generates a series of values from start + to stop, with a step size + of step. step + defaults to 1. + + + + + + generate_series ( start timestamp, stop timestamp, step interval ) + setof timestamp + + + generate_series ( start timestamp with time zone, stop timestamp with time zone, step interval , timezone text ) + setof timestamp with time zone + + + Generates a series of values from start + to stop, with a step size + of step. + In the timezone-aware form, times of day and daylight-savings + adjustments are computed according to the time zone named by + the timezone argument, or the current + setting if that is omitted. + + + + +
+ + + When step is positive, zero rows are returned if + start is greater than stop. + Conversely, when step is negative, zero rows are + returned if start is less than stop. + Zero rows are also returned if any input is NULL. + It is an error + for step to be zero. Some examples follow: + +SELECT * FROM generate_series(2,4); + generate_series +----------------- + 2 + 3 + 4 +(3 rows) + +SELECT * FROM generate_series(5,1,-2); + generate_series +----------------- + 5 + 3 + 1 +(3 rows) + +SELECT * FROM generate_series(4,3); + generate_series +----------------- +(0 rows) + +SELECT generate_series(1.1, 4, 1.3); + generate_series +----------------- + 1.1 + 2.4 + 3.7 +(3 rows) + +-- this example relies on the date-plus-integer operator: +SELECT current_date + s.a AS dates FROM generate_series(0,14,7) AS s(a); + dates +------------ + 2004-02-05 + 2004-02-12 + 2004-02-19 +(3 rows) + +SELECT * FROM generate_series('2008-03-01 00:00'::timestamp, + '2008-03-04 12:00', '10 hours'); + generate_series +--------------------- + 2008-03-01 00:00:00 + 2008-03-01 10:00:00 + 2008-03-01 20:00:00 + 2008-03-02 06:00:00 + 2008-03-02 16:00:00 + 2008-03-03 02:00:00 + 2008-03-03 12:00:00 + 2008-03-03 22:00:00 + 2008-03-04 08:00:00 +(9 rows) + +-- this example assumes that TimeZone is set to UTC; note the DST transition: +SELECT * FROM generate_series('2001-10-22 00:00 -04:00'::timestamptz, + '2001-11-01 00:00 -05:00'::timestamptz, + '1 day'::interval, 'America/New_York'); + generate_series +------------------------ + 2001-10-22 04:00:00+00 + 2001-10-23 04:00:00+00 + 2001-10-24 04:00:00+00 + 2001-10-25 04:00:00+00 + 2001-10-26 04:00:00+00 + 2001-10-27 04:00:00+00 + 2001-10-28 04:00:00+00 + 2001-10-29 05:00:00+00 + 2001-10-30 05:00:00+00 + 2001-10-31 05:00:00+00 + 2001-11-01 05:00:00+00 +(11 rows) + + + + + Subscript Generating Functions + + + + + Function + + + Description + + + + + + + + + generate_subscripts + + generate_subscripts ( array anyarray, dim integer ) + setof integer + + + Generates a series comprising the valid subscripts of + the dim'th dimension of the given array. + + + + + + generate_subscripts ( array anyarray, dim integer, reverse boolean ) + setof integer + + + Generates a series comprising the valid subscripts of + the dim'th dimension of the given array. + When reverse is true, returns the series in + reverse order. + + + + +
+ + + generate_subscripts is a convenience function that generates + the set of valid subscripts for the specified dimension of the given + array. + Zero rows are returned for arrays that do not have the requested dimension, + or if any input is NULL. + Some examples follow: + +-- basic usage: +SELECT generate_subscripts('{NULL,1,NULL,2}'::int[], 1) AS s; + s +--- + 1 + 2 + 3 + 4 +(4 rows) + +-- presenting an array, the subscript and the subscripted +-- value requires a subquery: +SELECT * FROM arrays; + a +-------------------- + {-1,-2} + {100,200,300} +(2 rows) + +SELECT a AS array, s AS subscript, a[s] AS value +FROM (SELECT generate_subscripts(a, 1) AS s, a FROM arrays) foo; + array | subscript | value +---------------+-----------+------- + {-1,-2} | 1 | -1 + {-1,-2} | 2 | -2 + {100,200,300} | 1 | 100 + {100,200,300} | 2 | 200 + {100,200,300} | 3 | 300 +(5 rows) + +-- unnest a 2D array: +CREATE OR REPLACE FUNCTION unnest2(anyarray) +RETURNS SETOF anyelement AS $$ +SELECT $1[i][j] + FROM generate_subscripts($1,1) g1(i), + generate_subscripts($1,2) g2(j); +$$ LANGUAGE sql IMMUTABLE; +CREATE FUNCTION +SELECT * FROM unnest2(ARRAY[[1,2],[3,4]]); + unnest2 +--------- + 1 + 2 + 3 + 4 +(4 rows) + + + + + ordinality + + + + When a function in the FROM clause is suffixed + by WITH ORDINALITY, a bigint column is + appended to the function's output column(s), which starts from 1 and + increments by 1 for each row of the function's output. + This is most useful in the case of set returning + functions such as unnest(). + + +-- set returning function WITH ORDINALITY: +SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n); + ls | n +-----------------+---- + pg_serial | 1 + pg_twophase | 2 + postmaster.opts | 3 + pg_notify | 4 + postgresql.conf | 5 + pg_tblspc | 6 + logfile | 7 + base | 8 + postmaster.pid | 9 + pg_ident.conf | 10 + global | 11 + pg_xact | 12 + pg_snapshots | 13 + pg_multixact | 14 + PG_VERSION | 15 + pg_wal | 16 + pg_hba.conf | 17 + pg_stat_tmp | 18 + pg_subtrans | 19 +(19 rows) + + + +
diff --git a/doc/src/sgml/func/func-statistics.sgml b/doc/src/sgml/func/func-statistics.sgml new file mode 100644 index 0000000000000..22dee263cc2a0 --- /dev/null +++ b/doc/src/sgml/func/func-statistics.sgml @@ -0,0 +1,85 @@ + + Statistics Information Functions + + + function + statistics + + + + PostgreSQL provides a function to inspect complex + statistics defined using the CREATE STATISTICS command. + + + + Inspecting MCV Lists + + + pg_mcv_list_items + + + +pg_mcv_list_items ( pg_mcv_list ) setof record + + + + pg_mcv_list_items returns a set of records describing + all items stored in a multi-column MCV list. It + returns the following columns: + + + + + + Name + Type + Description + + + + + + index + integer + index of the item in the MCV list + + + values + text[] + values stored in the MCV item + + + nulls + boolean[] + flags identifying NULL values + + + frequency + double precision + frequency of this MCV item + + + base_frequency + double precision + base frequency of this MCV item + + + + + + + + The pg_mcv_list_items function can be used like this: + + +SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid), + pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts'; + + + Values of the pg_mcv_list type can be obtained only from the + pg_statistic_ext_data.stxdmcv + column. + + + + diff --git a/doc/src/sgml/func/func-string.sgml b/doc/src/sgml/func/func-string.sgml new file mode 100644 index 0000000000000..0786573d7be15 --- /dev/null +++ b/doc/src/sgml/func/func-string.sgml @@ -0,0 +1,1827 @@ + + String Functions and Operators + + + This section describes functions and operators for examining and + manipulating string values. Strings in this context include values + of the types character, character varying, + and text. Except where noted, these functions and operators + are declared to accept and return type text. They will + interchangeably accept character varying arguments. + Values of type character will be converted + to text before the function or operator is applied, resulting + in stripping any trailing spaces in the character value. + + + + SQL defines some string functions that use + key words, rather than commas, to separate + arguments. Details are in + . + PostgreSQL also provides versions of these functions + that use the regular function invocation syntax + (see ). + + + + + The string concatenation operator (||) will accept + non-string input, so long as at least one input is of string type, as shown + in . For other cases, inserting an + explicit coercion to text can be used to have non-string input + accepted. + + + + + <acronym>SQL</acronym> String Functions and Operators + + + + + Function/Operator + + + Description + + + Example(s) + + + + + + + + + character string + concatenation + + text || text + text + + + Concatenates the two strings. + + + 'Post' || 'greSQL' + PostgreSQL + + + + + + text || anynonarray + text + + + anynonarray || text + text + + + Converts the non-string input to text, then concatenates the two + strings. (The non-string input cannot be of an array type, because + that would create ambiguity with the array || + operators. If you want to concatenate an array's text equivalent, + cast it to text explicitly.) + + + 'Value: ' || 42 + Value: 42 + + + + + + + btrim + + btrim ( string text + , characters text ) + text + + + Removes the longest string containing only characters + in characters (a space by default) + from the start and end of string. + + + btrim('xyxtrimyyx', 'xyz') + trim + + + + + + + normalized + + + Unicode normalization + + text IS NOT form NORMALIZED + boolean + + + Checks whether the string is in the specified Unicode normalization + form. The optional form key word specifies the + form: NFC (the default), NFD, + NFKC, or NFKD. This expression can + only be used when the server encoding is UTF8. Note + that checking for normalization using this expression is often faster + than normalizing possibly already normalized strings. + + + U&'\0061\0308bc' IS NFD NORMALIZED + t + + + + + + + bit_length + + bit_length ( text ) + integer + + + Returns number of bits in the string (8 + times the octet_length). + + + bit_length('jose') + 32 + + + + + + + char_length + + + character string + length + + + length + of a character string + character string, length + + char_length ( text ) + integer + + + + character_length + + character_length ( text ) + integer + + + Returns number of characters in the string. + + + char_length('josé') + 4 + + + + + + + lower + + lower ( text ) + text + + + Converts the string to all lower case, according to the rules of the + database's locale. + + + lower('TOM') + tom + + + + + + + lpad + + lpad ( string text, + length integer + , fill text ) + text + + + Extends the string to length + length by prepending the characters + fill (a space by default). If the + string is already longer than + length then it is truncated (on the right). + + + lpad('hi', 5, 'xy') + xyxhi + + + + + + + ltrim + + ltrim ( string text + , characters text ) + text + + + Removes the longest string containing only characters in + characters (a space by default) from the start of + string. + + + ltrim('zzzytest', 'xyz') + test + + + + + + + normalize + + + Unicode normalization + + normalize ( text + , form ) + text + + + Converts the string to the specified Unicode + normalization form. The optional form key word + specifies the form: NFC (the default), + NFD, NFKC, or + NFKD. This function can only be used when the + server encoding is UTF8. + + + normalize(U&'\0061\0308bc', NFC) + U&'\00E4bc' + + + + + + + octet_length + + octet_length ( text ) + integer + + + Returns number of bytes in the string. + + + octet_length('josé') + 5 (if server encoding is UTF8) + + + + + + + octet_length + + octet_length ( character ) + integer + + + Returns number of bytes in the string. Since this version of the + function accepts type character directly, it will not + strip trailing spaces. + + + octet_length('abc '::character(4)) + 4 + + + + + + + overlay + + overlay ( string text PLACING newsubstring text FROM start integer FOR count integer ) + text + + + Replaces the substring of string that starts at + the start'th character and extends + for count characters + with newsubstring. + If count is omitted, it defaults to the length + of newsubstring. + + + overlay('Txxxxas' PLACING 'hom' FROM 2 FOR 4) + Thomas + + + + + + + position + + position ( substring text IN string text ) + integer + + + Returns first starting index of the specified + substring within + string, or zero if it's not present. + + + position('om' IN 'Thomas') + 3 + + + + + + + rpad + + rpad ( string text, + length integer + , fill text ) + text + + + Extends the string to length + length by appending the characters + fill (a space by default). If the + string is already longer than + length then it is truncated. + + + rpad('hi', 5, 'xy') + hixyx + + + + + + + rtrim + + rtrim ( string text + , characters text ) + text + + + Removes the longest string containing only characters in + characters (a space by default) from the end of + string. + + + rtrim('testxxzx', 'xyz') + test + + + + + + + substring + + substring ( string text FROM start integer FOR count integer ) + text + + + Extracts the substring of string starting at + the start'th character if that is specified, + and stopping after count characters if that is + specified. Provide at least one of start + and count. + + + substring('Thomas' FROM 2 FOR 3) + hom + + + substring('Thomas' FROM 3) + omas + + + substring('Thomas' FOR 2) + Th + + + + + + substring ( string text FROM pattern text ) + text + + + Extracts the first substring matching POSIX regular expression; see + . + + + substring('Thomas' FROM '...$') + mas + + + + + + substring ( string text SIMILAR pattern text ESCAPE escape text ) + text + + + substring ( string text FROM pattern text FOR escape text ) + text + + + Extracts the first substring matching SQL regular expression; + see . The first form has + been specified since SQL:2003; the second form was only in SQL:1999 + and should be considered obsolete. + + + substring('Thomas' SIMILAR '%#"o_a#"_' ESCAPE '#') + oma + + + + + + + trim + + trim ( LEADING | TRAILING | BOTH + characters text FROM + string text ) + text + + + Removes the longest string containing only characters in + characters (a space by default) from the + start, end, or both ends (BOTH is the default) + of string. + + + trim(both 'xyz' from 'yxTomxx') + Tom + + + + + + trim ( LEADING | TRAILING | BOTH FROM + string text , + characters text ) + text + + + This is a non-standard syntax for trim(). + + + trim(both from 'yxTomxx', 'xyz') + Tom + + + + + + + unicode_assigned + + unicode_assigned ( text ) + boolean + + + Returns true if all characters in the string are + assigned Unicode codepoints; false otherwise. This + function can only be used when the server encoding is + UTF8. + + + + + + + upper + + upper ( text ) + text + + + Converts the string to all upper case, according to the rules of the + database's locale. + + + upper('tom') + TOM + + + + +
+ + + Additional string manipulation functions and operators are available + and are listed in . (Some of + these are used internally to implement + the SQL-standard string functions listed in + .) + There are also pattern-matching operators, which are described in + , and operators for full-text + search, which are described in . + + + + Other String Functions and Operators + + + + + Function/Operator + + + Description + + + Example(s) + + + + + + + + + character string + prefix test + + text ^@ text + boolean + + + Returns true if the first string starts with the second string + (equivalent to the starts_with() function). + + + 'alphabet' ^@ 'alph' + t + + + + + + + ascii + + ascii ( text ) + integer + + + Returns the numeric code of the first character of the argument. + In UTF8 encoding, returns the Unicode code point + of the character. In other multibyte encodings, the argument must + be an ASCII character. + + + ascii('x') + 120 + + + + + + + chr + + chr ( integer ) + text + + + Returns the character with the given code. In UTF8 + encoding the argument is treated as a Unicode code point. In other + multibyte encodings the argument must designate + an ASCII character. chr(0) is + disallowed because text data types cannot store that character. + + + chr(65) + A + + + + + + + concat + + concat ( val1 "any" + , val2 "any" , ... ) + text + + + Concatenates the text representations of all the arguments. + NULL arguments are ignored. + + + concat('abcde', 2, NULL, 22) + abcde222 + + + + + + + concat_ws + + concat_ws ( sep text, + val1 "any" + , val2 "any" , ... ) + text + + + Concatenates all but the first argument, with separators. The first + argument is used as the separator string, and should not be NULL. + Other NULL arguments are ignored. + + + concat_ws(',', 'abcde', 2, NULL, 22) + abcde,2,22 + + + + + + + format + + format ( formatstr text + , formatarg "any" , ... ) + text + + + Formats arguments according to a format string; + see . + This function is similar to the C function sprintf. + + + format('Hello %s, %1$s', 'World') + Hello World, World + + + + + + + initcap + + initcap ( text ) + text + + + Converts the first letter of each word to upper case (or title case + if the letter is a digraph and locale is ICU or + builtin PG_UNICODE_FAST) + and the rest to lower case. When using the libc or + builtin locale provider, words are sequences of + alphanumeric characters separated by non-alphanumeric characters; + when using the ICU locale provider, words are separated according to + u_strToTitle ICU function. + + + This function is primarily used for convenient + display, and the specific result should not be relied upon because of + the differences between locale providers and between different + ICU versions. If specific word boundary rules are desired, + it is recommended to write a custom function. + + + initcap('hi THOMAS') + Hi Thomas + + + + + + + casefold + + casefold ( text ) + text + + + Performs case folding of the input string according to the collation. + Case folding is similar to case conversion, but the purpose of case + folding is to facilitate case-insensitive matching of strings, + whereas the purpose of case conversion is to convert to a particular + cased form. This function can only be used when the server encoding + is UTF8. + + + Ordinarily, case folding simply converts to lowercase, but there may + be exceptions depending on the collation. For instance, some + characters have more than two lowercase variants, or fold to uppercase. + + + Case folding may change the length of the string. For instance, in + the PG_UNICODE_FAST collation, ß + (U+00DF) folds to ss. + + + casefold can be used for Unicode Default Caseless + Matching. It does not always preserve the normalized form of the + input string (see ). + + + The libc provider doesn't support case folding, so + casefold is identical to . + + + + + + + left + + left ( string text, + n integer ) + text + + + Returns first n characters in the + string, or when n is negative, returns + all but last |n| characters. + + + left('abcde', 2) + ab + + + + + + + length + + length ( text ) + integer + + + Returns the number of characters in the string. + + + length('jose') + 4 + + + + + + + md5 + + md5 ( text ) + text + + + Computes the MD5 hash of + the argument, with the result written in hexadecimal. + + + md5('abc') + 900150983cd24fb0&zwsp;d6963f7d28e17f72 + + + + + + + parse_ident + + parse_ident ( qualified_identifier text + , strict_mode boolean DEFAULT true ) + text[] + + + Splits qualified_identifier into an array of + identifiers, removing any quoting of individual identifiers. By + default, extra characters after the last identifier are considered an + error; but if the second parameter is false, then such + extra characters are ignored. (This behavior is useful for parsing + names for objects like functions.) Note that this function does not + truncate over-length identifiers. If you want truncation you can cast + the result to name[]. + + + parse_ident('"SomeSchema".someTable') + {SomeSchema,sometable} + + + + + + + pg_client_encoding + + pg_client_encoding ( ) + name + + + Returns current client encoding name. + + + pg_client_encoding() + UTF8 + + + + + + + quote_ident + + quote_ident ( text ) + text + + + Returns the given string suitably quoted to be used as an identifier + in an SQL statement string. + Quotes are added only if necessary (i.e., if the string contains + non-identifier characters or would be case-folded). + Embedded quotes are properly doubled. + See also . + + + quote_ident('Foo bar') + "Foo bar" + + + + + + + quote_literal + + quote_literal ( text ) + text + + + Returns the given string suitably quoted to be used as a string literal + in an SQL statement string. + Embedded single-quotes and backslashes are properly doubled. + Note that quote_literal returns null on null + input; if the argument might be null, + quote_nullable is often more suitable. + See also . + + + quote_literal(E'O\'Reilly') + 'O''Reilly' + + + + + + quote_literal ( anyelement ) + text + + + Converts the given value to text and then quotes it as a literal. + Embedded single-quotes and backslashes are properly doubled. + + + quote_literal(42.5) + '42.5' + + + + + + + quote_nullable + + quote_nullable ( text ) + text + + + Returns the given string suitably quoted to be used as a string literal + in an SQL statement string; or, if the argument + is null, returns NULL. + Embedded single-quotes and backslashes are properly doubled. + See also . + + + quote_nullable(NULL) + NULL + + + + + + quote_nullable ( anyelement ) + text + + + Converts the given value to text and then quotes it as a literal; + or, if the argument is null, returns NULL. + Embedded single-quotes and backslashes are properly doubled. + + + quote_nullable(42.5) + '42.5' + + + + + + + regexp_count + + regexp_count ( string text, pattern text + , start integer + , flags text ) + integer + + + Returns the number of times the POSIX regular + expression pattern matches in + the string; see + . + + + regexp_count('123456789012', '\d\d\d', 2) + 3 + + + + + + + regexp_instr + + regexp_instr ( string text, pattern text + , start integer + , N integer + , endoption integer + , flags text + , subexpr integer ) + integer + + + Returns the position within string where + the N'th match of the POSIX regular + expression pattern occurs, or zero if there is + no such match; see . + + + regexp_instr('ABCDEF', 'c(.)(..)', 1, 1, 0, 'i') + 3 + + + regexp_instr('ABCDEF', 'c(.)(..)', 1, 1, 0, 'i', 2) + 5 + + + + + + + regexp_like + + regexp_like ( string text, pattern text + , flags text ) + boolean + + + Checks whether a match of the POSIX regular + expression pattern occurs + within string; see + . + + + regexp_like('Hello World', 'world$', 'i') + t + + + + + + + regexp_match + + regexp_match ( string text, pattern text , flags text ) + text[] + + + Returns substrings within the first match of the POSIX regular + expression pattern to + the string; see + . + + + regexp_match('foobarbequebaz', '(bar)(beque)') + {bar,beque} + + + + + + + regexp_matches + + regexp_matches ( string text, pattern text , flags text ) + setof text[] + + + Returns substrings within the first match of the POSIX regular + expression pattern to + the string, or substrings within all + such matches if the g flag is used; + see . + + + regexp_matches('foobarbequebaz', 'ba.', 'g') + + + {bar} + {baz} + + + + + + + + regexp_replace + + regexp_replace ( string text, pattern text, replacement text + , flags text ) + text + + + Replaces the substring that is the first match to the POSIX + regular expression pattern, or all such + matches if the g flag is used; see + . + + + regexp_replace('Thomas', '.[mN]a.', 'M') + ThM + + + + + + regexp_replace ( string text, pattern text, replacement text, + start integer + , N integer + , flags text ) + text + + + Replaces the substring that is the N'th + match to the POSIX regular expression pattern, + or all such matches if N is zero, with the + search beginning at the start'th character + of string. If N is + omitted, it defaults to 1. See + . + + + regexp_replace('Thomas', '.', 'X', 3, 2) + ThoXas + + + regexp_replace(string=>'hello world', pattern=>'l', replacement=>'XX', start=>1, "N"=>2) + helXXo world + + + + + + + regexp_split_to_array + + regexp_split_to_array ( string text, pattern text , flags text ) + text[] + + + Splits string using a POSIX regular + expression as the delimiter, producing an array of results; see + . + + + regexp_split_to_array('hello world', '\s+') + {hello,world} + + + + + + + regexp_split_to_table + + regexp_split_to_table ( string text, pattern text , flags text ) + setof text + + + Splits string using a POSIX regular + expression as the delimiter, producing a set of results; see + . + + + regexp_split_to_table('hello world', '\s+') + + + hello + world + + + + + + + + regexp_substr + + regexp_substr ( string text, pattern text + , start integer + , N integer + , flags text + , subexpr integer ) + text + + + Returns the substring within string that + matches the N'th occurrence of the POSIX + regular expression pattern, + or NULL if there is no such match; see + . + + + regexp_substr('ABCDEF', 'c(.)(..)', 1, 1, 'i') + CDEF + + + regexp_substr('ABCDEF', 'c(.)(..)', 1, 1, 'i', 2) + EF + + + + + + + repeat + + repeat ( string text, number integer ) + text + + + Repeats string the specified + number of times. + + + repeat('Pg', 4) + PgPgPgPg + + + + + + + replace + + replace ( string text, + from text, + to text ) + text + + + Replaces all occurrences in string of + substring from with + substring to. + + + replace('abcdefabcdef', 'cd', 'XX') + abXXefabXXef + + + + + + + reverse + + reverse ( text ) + text + + + Reverses the order of the characters in the string. + + + reverse('abcde') + edcba + + + + + + + right + + right ( string text, + n integer ) + text + + + Returns last n characters in the string, + or when n is negative, returns all but + first |n| characters. + + + right('abcde', 2) + de + + + + + + + split_part + + split_part ( string text, + delimiter text, + n integer ) + text + + + Splits string at occurrences + of delimiter and returns + the n'th field (counting from one), + or when n is negative, returns + the |n|'th-from-last field. + + + split_part('abc~@~def~@~ghi', '~@~', 2) + def + + + split_part('abc,def,ghi,jkl', ',', -2) + ghi + + + + + + + starts_with + + starts_with ( string text, prefix text ) + boolean + + + Returns true if string starts + with prefix. + + + starts_with('alphabet', 'alph') + t + + + + + + + string_to_array + + string_to_array ( string text, delimiter text , null_string text ) + text[] + + + Splits the string at occurrences + of delimiter and forms the resulting fields + into a text array. + If delimiter is NULL, + each character in the string will become a + separate element in the array. + If delimiter is an empty string, then + the string is treated as a single field. + If null_string is supplied and is + not NULL, fields matching that string are + replaced by NULL. + See also array_to_string. + + + string_to_array('xx~~yy~~zz', '~~', 'yy') + {xx,NULL,zz} + + + + + + + string_to_table + + string_to_table ( string text, delimiter text , null_string text ) + setof text + + + Splits the string at occurrences + of delimiter and returns the resulting fields + as a set of text rows. + If delimiter is NULL, + each character in the string will become a + separate row of the result. + If delimiter is an empty string, then + the string is treated as a single field. + If null_string is supplied and is + not NULL, fields matching that string are + replaced by NULL. + + + string_to_table('xx~^~yy~^~zz', '~^~', 'yy') + + + xx + NULL + zz + + + + + + + + strpos + + strpos ( string text, substring text ) + integer + + + Returns first starting index of the specified substring + within string, or zero if it's not present. + (Same as position(substring in + string), but note the reversed + argument order.) + + + strpos('high', 'ig') + 2 + + + + + + + substr + + substr ( string text, start integer , count integer ) + text + + + Extracts the substring of string starting at + the start'th character, + and extending for count characters if that is + specified. (Same + as substring(string + from start + for count).) + + + substr('alphabet', 3) + phabet + + + substr('alphabet', 3, 2) + ph + + + + + + + to_ascii + + to_ascii ( string text ) + text + + + to_ascii ( string text, + encoding name ) + text + + + to_ascii ( string text, + encoding integer ) + text + + + Converts string to ASCII + from another encoding, which may be identified by name or number. + If encoding is omitted the database encoding + is assumed (which in practice is the only useful case). + The conversion consists primarily of dropping accents. + Conversion is only supported + from LATIN1, LATIN2, + LATIN9, and WIN1250 encodings. + (See the module for another, more flexible + solution.) + + + to_ascii('Karél') + Karel + + + + + + + to_bin + + to_bin ( integer ) + text + + + to_bin ( bigint ) + text + + + Converts the number to its equivalent two's complement binary + representation. + + + to_bin(2147483647) + 1111111111111111111111111111111 + + + to_bin(-1234) + 11111111111111111111101100101110 + + + + + + + to_hex + + to_hex ( integer ) + text + + + to_hex ( bigint ) + text + + + Converts the number to its equivalent two's complement hexadecimal + representation. + + + to_hex(2147483647) + 7fffffff + + + to_hex(-1234) + fffffb2e + + + + + + + to_oct + + to_oct ( integer ) + text + + + to_oct ( bigint ) + text + + + Converts the number to its equivalent two's complement octal + representation. + + + to_oct(2147483647) + 17777777777 + + + to_oct(-1234) + 37777775456 + + + + + + + translate + + translate ( string text, + from text, + to text ) + text + + + Replaces each character in string that + matches a character in the from set with the + corresponding character in the to + set. If from is longer than + to, occurrences of the extra characters in + from are deleted. + + + translate('12345', '143', 'ax') + a2x5 + + + + + + + unistr + + unistr ( text ) + text + + + Evaluate escaped Unicode characters in the argument. Unicode characters + can be specified as + \XXXX (4 hexadecimal + digits), \+XXXXXX (6 + hexadecimal digits), + \uXXXX (4 hexadecimal + digits), or \UXXXXXXXX + (8 hexadecimal digits). To specify a backslash, write two + backslashes. All other characters are taken literally. + + + + If the server encoding is not UTF-8, the Unicode code point identified + by one of these escape sequences is converted to the actual server + encoding; an error is reported if that's not possible. + + + + This function provides a (non-standard) alternative to string + constants with Unicode escapes (see ). + + + + unistr('d\0061t\+000061') + data + + + unistr('d\u0061t\U00000061') + data + + + + + +
+ + + The concat, concat_ws and + format functions are variadic, so it is possible to + pass the values to be concatenated or formatted as an array marked with + the VARIADIC keyword (see ). The array's elements are + treated as if they were separate ordinary arguments to the function. + If the variadic array argument is NULL, concat + and concat_ws return NULL, but + format treats a NULL as a zero-element array. + + + + See also the aggregate function string_agg in + , and the functions for + converting between strings and the bytea type in + . + + + + <function>format</function> + + + format + + + + The function format produces output formatted according to + a format string, in a style similar to the C function + sprintf. + + + + +format(formatstr text , formatarg "any" , ... ) + + formatstr is a format string that specifies how the + result should be formatted. Text in the format string is copied + directly to the result, except where format specifiers are + used. Format specifiers act as placeholders in the string, defining how + subsequent function arguments should be formatted and inserted into the + result. Each formatarg argument is converted to text + according to the usual output rules for its data type, and then formatted + and inserted into the result string according to the format specifier(s). + + + + Format specifiers are introduced by a % character and have + the form + +%[position][flags][width]type + + where the component fields are: + + + + position (optional) + + + A string of the form n$ where + n is the index of the argument to print. + Index 1 means the first argument after + formatstr. If the position is + omitted, the default is to use the next argument in sequence. + + + + + + flags (optional) + + + Additional options controlling how the format specifier's output is + formatted. Currently the only supported flag is a minus sign + (-) which will cause the format specifier's output to be + left-justified. This has no effect unless the width + field is also specified. + + + + + + width (optional) + + + Specifies the minimum number of characters to use to + display the format specifier's output. The output is padded on the + left or right (depending on the - flag) with spaces as + needed to fill the width. A too-small width does not cause + truncation of the output, but is simply ignored. The width may be + specified using any of the following: a positive integer; an + asterisk (*) to use the next function argument as the + width; or a string of the form *n$ to + use the nth function argument as the width. + + + + If the width comes from a function argument, that argument is + consumed before the argument that is used for the format specifier's + value. If the width argument is negative, the result is left + aligned (as if the - flag had been specified) within a + field of length abs(width). + + + + + + type (required) + + + The type of format conversion to use to produce the format + specifier's output. The following types are supported: + + + + s formats the argument value as a simple + string. A null value is treated as an empty string. + + + + + I treats the argument value as an SQL + identifier, double-quoting it if necessary. + It is an error for the value to be null (equivalent to + quote_ident). + + + + + L quotes the argument value as an SQL literal. + A null value is displayed as the string NULL, without + quotes (equivalent to quote_nullable). + + + + + + + + + + + In addition to the format specifiers described above, the special sequence + %% may be used to output a literal % character. + + + + Here are some examples of the basic format conversions: + + +SELECT format('Hello %s', 'World'); +Result: Hello World + +SELECT format('Testing %s, %s, %s, %%', 'one', 'two', 'three'); +Result: Testing one, two, three, % + +SELECT format('INSERT INTO %I VALUES(%L)', 'Foo bar', E'O\'Reilly'); +Result: INSERT INTO "Foo bar" VALUES('O''Reilly') + +SELECT format('INSERT INTO %I VALUES(%L)', 'locations', 'C:\Program Files'); +Result: INSERT INTO locations VALUES('C:\Program Files') + + + + + Here are examples using width fields + and the - flag: + + +SELECT format('|%10s|', 'foo'); +Result: | foo| + +SELECT format('|%-10s|', 'foo'); +Result: |foo | + +SELECT format('|%*s|', 10, 'foo'); +Result: | foo| + +SELECT format('|%*s|', -10, 'foo'); +Result: |foo | + +SELECT format('|%-*s|', 10, 'foo'); +Result: |foo | + +SELECT format('|%-*s|', -10, 'foo'); +Result: |foo | + + + + + These examples show use of position fields: + + +SELECT format('Testing %3$s, %2$s, %1$s', 'one', 'two', 'three'); +Result: Testing three, two, one + +SELECT format('|%*2$s|', 'foo', 10, 'bar'); +Result: | bar| + +SELECT format('|%1$*2$s|', 'foo', 10, 'bar'); +Result: | foo| + + + + + Unlike the standard C function sprintf, + PostgreSQL's format function allows format + specifiers with and without position fields to be mixed + in the same format string. A format specifier without a + position field always uses the next argument after the + last argument consumed. + In addition, the format function does not require all + function arguments to be used in the format string. + For example: + + +SELECT format('Testing %3$s, %2$s, %s', 'one', 'two', 'three'); +Result: Testing three, two, three + + + + + The %I and %L format specifiers are particularly + useful for safely constructing dynamic SQL statements. See + . + + + +
diff --git a/doc/src/sgml/func/func-subquery.sgml b/doc/src/sgml/func/func-subquery.sgml new file mode 100644 index 0000000000000..f954f3bf1339e --- /dev/null +++ b/doc/src/sgml/func/func-subquery.sgml @@ -0,0 +1,355 @@ + + Subquery Expressions + + + EXISTS + + + + IN + + + + NOT IN + + + + ANY + + + + ALL + + + + SOME + + + + subquery + + + + This section describes the SQL-compliant subquery + expressions available in PostgreSQL. + All of the expression forms documented in this section return + Boolean (true/false) results. + + + + <literal>EXISTS</literal> + + +EXISTS (subquery) + + + + The argument of EXISTS is an arbitrary SELECT statement, + or subquery. The + subquery is evaluated to determine whether it returns any rows. + If it returns at least one row, the result of EXISTS is + true; if the subquery returns no rows, the result of EXISTS + is false. + + + + The subquery can refer to variables from the surrounding query, + which will act as constants during any one evaluation of the subquery. + + + + The subquery will generally only be executed long enough to determine + whether at least one row is returned, not all the way to completion. + It is unwise to write a subquery that has side effects (such as + calling sequence functions); whether the side effects occur + might be unpredictable. + + + + Since the result depends only on whether any rows are returned, + and not on the contents of those rows, the output list of the + subquery is normally unimportant. A common coding convention is + to write all EXISTS tests in the form + EXISTS(SELECT * FROM ... WHERE ...), another common + convention is to write EXISTS(SELECT 1 FROM ... WHERE + ...) or some other dummy constant. These conventions are + actually equivalent in PostgreSQL, which + will optimize away evaluation of the subquery's output list altogether + when it cannot affect the number of rows returned. (An example + that cannot be optimized away is an output list containing a + set-returning function, since the function might return zero rows.) + + + + This simple example is like an inner join on col2, but + it produces at most one output row for each tab1 row, + even if there are several matching tab2 rows: + +SELECT col1 +FROM tab1 +WHERE EXISTS (SELECT 1 FROM tab2 WHERE col2 = tab1.col2); + + + + + + <literal>IN</literal> + + +expression IN (subquery) + + + + The right-hand side is a parenthesized + subquery, which must return exactly one column. The left-hand expression + is evaluated and compared to each row of the subquery result. + The result of IN is true if any equal subquery row is found. + The result is false if no equal row is found (including the + case where the subquery returns no rows). + + + + Note that if the left-hand expression yields null, or if there are + no equal right-hand values and at least one right-hand row yields + null, the result of the IN construct will be null, not false. + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + As with EXISTS, it's unwise to assume that the subquery will + be evaluated completely. + + + +row_constructor IN (subquery) + + + + The left-hand side of this form of IN is a row constructor, + as described in . + The right-hand side is a parenthesized + subquery, which must return exactly as many columns as there are + expressions in the left-hand row. The left-hand expressions are + evaluated and compared row-wise to each row of the subquery result. + The result of IN is true if any equal subquery row is found. + The result is false if no equal row is found (including the + case where the subquery returns no rows). + + + + As usual, null values in the rows are combined per + the normal rules of SQL Boolean expressions. Two rows are considered + equal if all their corresponding members are non-null and equal; the rows + are unequal if any corresponding members are non-null and unequal; + otherwise the result of that row comparison is unknown (null). + If all the per-row results are either unequal or null, with at least one + null, then the result of IN is null. + + + + + <literal>NOT IN</literal> + + +expression NOT IN (subquery) + + + + The right-hand side is a parenthesized + subquery, which must return exactly one column. The left-hand expression + is evaluated and compared to each row of the subquery result. + The result of NOT IN is true if only unequal subquery rows + are found (including the case where the subquery returns no rows). + The result is false if any equal row is found. + + + + Note that if the left-hand expression yields null, or if there are + no equal right-hand values and at least one right-hand row yields + null, the result of the NOT IN construct will be null, not true. + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + As with EXISTS, it's unwise to assume that the subquery will + be evaluated completely. + + + +row_constructor NOT IN (subquery) + + + + The left-hand side of this form of NOT IN is a row constructor, + as described in . + The right-hand side is a parenthesized + subquery, which must return exactly as many columns as there are + expressions in the left-hand row. The left-hand expressions are + evaluated and compared row-wise to each row of the subquery result. + The result of NOT IN is true if only unequal subquery rows + are found (including the case where the subquery returns no rows). + The result is false if any equal row is found. + + + + As usual, null values in the rows are combined per + the normal rules of SQL Boolean expressions. Two rows are considered + equal if all their corresponding members are non-null and equal; the rows + are unequal if any corresponding members are non-null and unequal; + otherwise the result of that row comparison is unknown (null). + If all the per-row results are either unequal or null, with at least one + null, then the result of NOT IN is null. + + + + + <literal>ANY</literal>/<literal>SOME</literal> + + +expression operator ANY (subquery) +expression operator SOME (subquery) + + + + The right-hand side is a parenthesized + subquery, which must return exactly one column. The left-hand expression + is evaluated and compared to each row of the subquery result using the + given operator, which must yield a Boolean + result. + The result of ANY is true if any true result is obtained. + The result is false if no true result is found (including the + case where the subquery returns no rows). + + + + SOME is a synonym for ANY. + IN is equivalent to = ANY. + + + + Note that if there are no successes and at least one right-hand row yields + null for the operator's result, the result of the ANY construct + will be null, not false. + This is in accordance with SQL's normal rules for Boolean combinations + of null values. + + + + As with EXISTS, it's unwise to assume that the subquery will + be evaluated completely. + + + +row_constructor operator ANY (subquery) +row_constructor operator SOME (subquery) + + + + The left-hand side of this form of ANY is a row constructor, + as described in . + The right-hand side is a parenthesized + subquery, which must return exactly as many columns as there are + expressions in the left-hand row. The left-hand expressions are + evaluated and compared row-wise to each row of the subquery result, + using the given operator. + The result of ANY is true if the comparison + returns true for any subquery row. + The result is false if the comparison returns false for every + subquery row (including the case where the subquery returns no + rows). + The result is NULL if no comparison with a subquery row returns true, + and at least one comparison returns NULL. + + + + See for details about the meaning + of a row constructor comparison. + + + + + <literal>ALL</literal> + + +expression operator ALL (subquery) + + + + The right-hand side is a parenthesized + subquery, which must return exactly one column. The left-hand expression + is evaluated and compared to each row of the subquery result using the + given operator, which must yield a Boolean + result. + The result of ALL is true if all rows yield true + (including the case where the subquery returns no rows). + The result is false if any false result is found. + The result is NULL if no comparison with a subquery row returns false, + and at least one comparison returns NULL. + + + + NOT IN is equivalent to <> ALL. + + + + As with EXISTS, it's unwise to assume that the subquery will + be evaluated completely. + + + +row_constructor operator ALL (subquery) + + + + The left-hand side of this form of ALL is a row constructor, + as described in . + The right-hand side is a parenthesized + subquery, which must return exactly as many columns as there are + expressions in the left-hand row. The left-hand expressions are + evaluated and compared row-wise to each row of the subquery result, + using the given operator. + The result of ALL is true if the comparison + returns true for all subquery rows (including the + case where the subquery returns no rows). + The result is false if the comparison returns false for any + subquery row. + The result is NULL if no comparison with a subquery row returns false, + and at least one comparison returns NULL. + + + + See for details about the meaning + of a row constructor comparison. + + + + + Single-Row Comparison + + + comparison + subquery result row + + + +row_constructor operator (subquery) + + + + The left-hand side is a row constructor, + as described in . + The right-hand side is a parenthesized subquery, which must return exactly + as many columns as there are expressions in the left-hand row. Furthermore, + the subquery cannot return more than one row. (If it returns zero rows, + the result is taken to be null.) The left-hand side is evaluated and + compared row-wise to the single subquery result row. + + + + See for details about the meaning + of a row constructor comparison. + + + diff --git a/doc/src/sgml/func/func-textsearch.sgml b/doc/src/sgml/func/func-textsearch.sgml new file mode 100644 index 0000000000000..290ad81d6979b --- /dev/null +++ b/doc/src/sgml/func/func-textsearch.sgml @@ -0,0 +1,1046 @@ + + Text Search Functions and Operators + + + full text search + functions and operators + + + + text search + functions and operators + + + + , + and + + summarize the functions and operators that are provided + for full text searching. See for a detailed + explanation of PostgreSQL's text search + facility. + + + + Text Search Operators + + + + + Operator + + + Description + + + Example(s) + + + + + + + + tsvector @@ tsquery + boolean + + + tsquery @@ tsvector + boolean + + + Does tsvector match tsquery? + (The arguments can be given in either order.) + + + to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat') + t + + + + + + text @@ tsquery + boolean + + + Does text string, after implicit invocation + of to_tsvector(), match tsquery? + + + 'fat cats ate rats' @@ to_tsquery('cat & rat') + t + + + + + + tsvector || tsvector + tsvector + + + Concatenates two tsvectors. If both inputs contain + lexeme positions, the second input's positions are adjusted + accordingly. + + + 'a:1 b:2'::tsvector || 'c:1 d:2 b:3'::tsvector + 'a':1 'b':2,5 'c':3 'd':4 + + + + + + tsquery && tsquery + tsquery + + + ANDs two tsquerys together, producing a query that + matches documents that match both input queries. + + + 'fat | rat'::tsquery && 'cat'::tsquery + ( 'fat' | 'rat' ) & 'cat' + + + + + + tsquery || tsquery + tsquery + + + ORs two tsquerys together, producing a query that + matches documents that match either input query. + + + 'fat | rat'::tsquery || 'cat'::tsquery + 'fat' | 'rat' | 'cat' + + + + + + !! tsquery + tsquery + + + Negates a tsquery, producing a query that matches + documents that do not match the input query. + + + !! 'cat'::tsquery + !'cat' + + + + + + tsquery <-> tsquery + tsquery + + + Constructs a phrase query, which matches if the two input queries + match at successive lexemes. + + + to_tsquery('fat') <-> to_tsquery('rat') + 'fat' <-> 'rat' + + + + + + tsquery @> tsquery + boolean + + + Does first tsquery contain the second? (This considers + only whether all the lexemes appearing in one query appear in the + other, ignoring the combining operators.) + + + 'cat'::tsquery @> 'cat & rat'::tsquery + f + + + + + + tsquery <@ tsquery + boolean + + + Is first tsquery contained in the second? (This + considers only whether all the lexemes appearing in one query appear + in the other, ignoring the combining operators.) + + + 'cat'::tsquery <@ 'cat & rat'::tsquery + t + + + 'cat'::tsquery <@ '!cat & rat'::tsquery + t + + + + +
+ + + In addition to these specialized operators, the usual comparison + operators shown in are + available for types tsvector and tsquery. + These are not very + useful for text searching but allow, for example, unique indexes to be + built on columns of these types. + + + + Text Search Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + array_to_tsvector + + array_to_tsvector ( text[] ) + tsvector + + + Converts an array of text strings to a tsvector. + The given strings are used as lexemes as-is, without further + processing. Array elements must not be empty strings + or NULL. + + + array_to_tsvector('{fat,cat,rat}'::text[]) + 'cat' 'fat' 'rat' + + + + + + + get_current_ts_config + + get_current_ts_config ( ) + regconfig + + + Returns the OID of the current default text search configuration + (as set by ). + + + get_current_ts_config() + english + + + + + + + length + + length ( tsvector ) + integer + + + Returns the number of lexemes in the tsvector. + + + length('fat:2,4 cat:3 rat:5A'::tsvector) + 3 + + + + + + + numnode + + numnode ( tsquery ) + integer + + + Returns the number of lexemes plus operators in + the tsquery. + + + numnode('(fat & rat) | cat'::tsquery) + 5 + + + + + + + plainto_tsquery + + plainto_tsquery ( + config regconfig, + query text ) + tsquery + + + Converts text to a tsquery, normalizing words according to + the specified or default configuration. Any punctuation in the string + is ignored (it does not determine query operators). The resulting + query matches documents containing all non-stopwords in the text. + + + plainto_tsquery('english', 'The Fat Rats') + 'fat' & 'rat' + + + + + + + phraseto_tsquery + + phraseto_tsquery ( + config regconfig, + query text ) + tsquery + + + Converts text to a tsquery, normalizing words according to + the specified or default configuration. Any punctuation in the string + is ignored (it does not determine query operators). The resulting + query matches phrases containing all non-stopwords in the text. + + + phraseto_tsquery('english', 'The Fat Rats') + 'fat' <-> 'rat' + + + phraseto_tsquery('english', 'The Cat and Rats') + 'cat' <2> 'rat' + + + + + + + websearch_to_tsquery + + websearch_to_tsquery ( + config regconfig, + query text ) + tsquery + + + Converts text to a tsquery, normalizing words according + to the specified or default configuration. Quoted word sequences are + converted to phrase tests. The word or is understood + as producing an OR operator, and a dash produces a NOT operator; + other punctuation is ignored. + This approximates the behavior of some common web search tools. + + + websearch_to_tsquery('english', '"fat rat" or cat dog') + 'fat' <-> 'rat' | 'cat' & 'dog' + + + + + + + querytree + + querytree ( tsquery ) + text + + + Produces a representation of the indexable portion of + a tsquery. A result that is empty or + just T indicates a non-indexable query. + + + querytree('foo & ! bar'::tsquery) + 'foo' + + + + + + + setweight + + setweight ( vector tsvector, weight "char" ) + tsvector + + + Assigns the specified weight to each element + of the vector. + + + setweight('fat:2,4 cat:3 rat:5B'::tsvector, 'A') + 'cat':3A 'fat':2A,4A 'rat':5A + + + + + + + setweight + setweight for specific lexeme(s) + + setweight ( vector tsvector, weight "char", lexemes text[] ) + tsvector + + + Assigns the specified weight to elements + of the vector that are listed + in lexemes. + The strings in lexemes are taken as lexemes + as-is, without further processing. Strings that do not match any + lexeme in vector are ignored. + + + setweight('fat:2,4 cat:3 rat:5,6B'::tsvector, 'A', '{cat,rat}') + 'cat':3A 'fat':2,4 'rat':5A,6A + + + + + + + strip + + strip ( tsvector ) + tsvector + + + Removes positions and weights from the tsvector. + + + strip('fat:2,4 cat:3 rat:5A'::tsvector) + 'cat' 'fat' 'rat' + + + + + + + to_tsquery + + to_tsquery ( + config regconfig, + query text ) + tsquery + + + Converts text to a tsquery, normalizing words according to + the specified or default configuration. The words must be combined + by valid tsquery operators. + + + to_tsquery('english', 'The & Fat & Rats') + 'fat' & 'rat' + + + + + + + to_tsvector + + to_tsvector ( + config regconfig, + document text ) + tsvector + + + Converts text to a tsvector, normalizing words according + to the specified or default configuration. Position information is + included in the result. + + + to_tsvector('english', 'The Fat Rats') + 'fat':2 'rat':3 + + + + + + to_tsvector ( + config regconfig, + document json ) + tsvector + + + to_tsvector ( + config regconfig, + document jsonb ) + tsvector + + + Converts each string value in the JSON document to + a tsvector, normalizing words according to the specified + or default configuration. The results are then concatenated in + document order to produce the output. Position information is + generated as though one stopword exists between each pair of string + values. (Beware that document order of the fields of a + JSON object is implementation-dependent when the input + is jsonb; observe the difference in the examples.) + + + to_tsvector('english', '{"aa": "The Fat Rats", "b": "dog"}'::json) + 'dog':5 'fat':2 'rat':3 + + + to_tsvector('english', '{"aa": "The Fat Rats", "b": "dog"}'::jsonb) + 'dog':1 'fat':4 'rat':5 + + + + + + + json_to_tsvector + + json_to_tsvector ( + config regconfig, + document json, + filter jsonb ) + tsvector + + + + jsonb_to_tsvector + + jsonb_to_tsvector ( + config regconfig, + document jsonb, + filter jsonb ) + tsvector + + + Selects each item in the JSON document that is requested by + the filter and converts each one to + a tsvector, normalizing words according to the specified + or default configuration. The results are then concatenated in + document order to produce the output. Position information is + generated as though one stopword exists between each pair of selected + items. (Beware that document order of the fields of a + JSON object is implementation-dependent when the input + is jsonb.) + The filter must be a jsonb + array containing zero or more of these keywords: + "string" (to include all string values), + "numeric" (to include all numeric values), + "boolean" (to include all boolean values), + "key" (to include all keys), or + "all" (to include all the above). + As a special case, the filter can also be a + simple JSON value that is one of these keywords. + + + json_to_tsvector('english', '{"a": "The Fat Rats", "b": 123}'::json, '["string", "numeric"]') + '123':5 'fat':2 'rat':3 + + + json_to_tsvector('english', '{"cat": "The Fat Rats", "dog": 123}'::json, '"all"') + '123':9 'cat':1 'dog':7 'fat':4 'rat':5 + + + + + + + ts_delete + + ts_delete ( vector tsvector, lexeme text ) + tsvector + + + Removes any occurrence of the given lexeme + from the vector. + The lexeme string is treated as a lexeme as-is, + without further processing. + + + ts_delete('fat:2,4 cat:3 rat:5A'::tsvector, 'fat') + 'cat':3 'rat':5A + + + + + + ts_delete ( vector tsvector, lexemes text[] ) + tsvector + + + Removes any occurrences of the lexemes + in lexemes + from the vector. + The strings in lexemes are taken as lexemes + as-is, without further processing. Strings that do not match any + lexeme in vector are ignored. + + + ts_delete('fat:2,4 cat:3 rat:5A'::tsvector, ARRAY['fat','rat']) + 'cat':3 + + + + + + + ts_filter + + ts_filter ( vector tsvector, weights "char"[] ) + tsvector + + + Selects only elements with the given weights + from the vector. + + + ts_filter('fat:2,4 cat:3b,7c rat:5A'::tsvector, '{a,b}') + 'cat':3B 'rat':5A + + + + + + + ts_headline + + ts_headline ( + config regconfig, + document text, + query tsquery + , options text ) + text + + + Displays, in an abbreviated form, the match(es) for + the query in + the document, which must be raw text not + a tsvector. Words in the document are normalized + according to the specified or default configuration before matching to + the query. Use of this function is discussed in + , which also describes the + available options. + + + ts_headline('The fat cat ate the rat.', 'cat') + The fat <b>cat</b> ate the rat. + + + + + + ts_headline ( + config regconfig, + document json, + query tsquery + , options text ) + text + + + ts_headline ( + config regconfig, + document jsonb, + query tsquery + , options text ) + text + + + Displays, in an abbreviated form, match(es) for + the query that occur in string values + within the JSON document. + See for more details. + + + ts_headline('{"cat":"raining cats and dogs"}'::jsonb, 'cat') + {"cat": "raining <b>cats</b> and dogs"} + + + + + + + ts_rank + + ts_rank ( + weights real[], + vector tsvector, + query tsquery + , normalization integer ) + real + + + Computes a score showing how well + the vector matches + the query. See + for details. + + + ts_rank(to_tsvector('raining cats and dogs'), 'cat') + 0.06079271 + + + + + + + ts_rank_cd + + ts_rank_cd ( + weights real[], + vector tsvector, + query tsquery + , normalization integer ) + real + + + Computes a score showing how well + the vector matches + the query, using a cover density + algorithm. See for details. + + + ts_rank_cd(to_tsvector('raining cats and dogs'), 'cat') + 0.1 + + + + + + + ts_rewrite + + ts_rewrite ( query tsquery, + target tsquery, + substitute tsquery ) + tsquery + + + Replaces occurrences of target + with substitute + within the query. + See for details. + + + ts_rewrite('a & b'::tsquery, 'a'::tsquery, 'foo|bar'::tsquery) + 'b' & ( 'foo' | 'bar' ) + + + + + + ts_rewrite ( query tsquery, + select text ) + tsquery + + + Replaces portions of the query according to + target(s) and substitute(s) obtained by executing + a SELECT command. + See for details. + + + SELECT ts_rewrite('a & b'::tsquery, 'SELECT t,s FROM aliases') + 'b' & ( 'foo' | 'bar' ) + + + + + + + tsquery_phrase + + tsquery_phrase ( query1 tsquery, query2 tsquery ) + tsquery + + + Constructs a phrase query that searches + for matches of query1 + and query2 at successive lexemes (same + as <-> operator). + + + tsquery_phrase(to_tsquery('fat'), to_tsquery('cat')) + 'fat' <-> 'cat' + + + + + + tsquery_phrase ( query1 tsquery, query2 tsquery, distance integer ) + tsquery + + + Constructs a phrase query that searches + for matches of query1 and + query2 that occur exactly + distance lexemes apart. + + + tsquery_phrase(to_tsquery('fat'), to_tsquery('cat'), 10) + 'fat' <10> 'cat' + + + + + + + tsvector_to_array + + tsvector_to_array ( tsvector ) + text[] + + + Converts a tsvector to an array of lexemes. + + + tsvector_to_array('fat:2,4 cat:3 rat:5A'::tsvector) + {cat,fat,rat} + + + + + + + unnest + for tsvector + + unnest ( tsvector ) + setof record + ( lexeme text, + positions smallint[], + weights text ) + + + Expands a tsvector into a set of rows, one per lexeme. + + + SELECT * FROM unnest('cat:3 fat:2,4 rat:5A'::tsvector) + + + lexeme | positions | weights +--------+-----------+--------- + cat | {3} | {D} + fat | {2,4} | {D,D} + rat | {5} | {A} + + + + + +
+ + + + All the text search functions that accept an optional regconfig + argument will use the configuration specified by + + when that argument is omitted. + + + + + The functions in + + are listed separately because they are not usually used in everyday text + searching operations. They are primarily helpful for development and + debugging of new text search configurations. + + + + Text Search Debugging Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + + ts_debug + + ts_debug ( + config regconfig, + document text ) + setof record + ( alias text, + description text, + token text, + dictionaries regdictionary[], + dictionary regdictionary, + lexemes text[] ) + + + Extracts and normalizes tokens from + the document according to the specified or + default text search configuration, and returns information about how + each token was processed. + See for details. + + + ts_debug('english', 'The Brightest supernovaes') + (asciiword,"Word, all ASCII",The,{english_stem},english_stem,{}) ... + + + + + + + ts_lexize + + ts_lexize ( dict regdictionary, token text ) + text[] + + + Returns an array of replacement lexemes if the input token is known to + the dictionary, or an empty array if the token is known to the + dictionary but it is a stop word, or NULL if it is not a known word. + See for details. + + + ts_lexize('english_stem', 'stars') + {star} + + + + + + + ts_parse + + ts_parse ( parser_name text, + document text ) + setof record + ( tokid integer, + token text ) + + + Extracts tokens from the document using the + named parser. + See for details. + + + ts_parse('default', 'foo - bar') + (1,foo) ... + + + + + + ts_parse ( parser_oid oid, + document text ) + setof record + ( tokid integer, + token text ) + + + Extracts tokens from the document using a + parser specified by OID. + See for details. + + + ts_parse(3722, 'foo - bar') + (1,foo) ... + + + + + + + ts_token_type + + ts_token_type ( parser_name text ) + setof record + ( tokid integer, + alias text, + description text ) + + + Returns a table that describes each type of token the named parser can + recognize. + See for details. + + + ts_token_type('default') + (1,asciiword,"Word, all ASCII") ... + + + + + + ts_token_type ( parser_oid oid ) + setof record + ( tokid integer, + alias text, + description text ) + + + Returns a table that describes each type of token a parser specified + by OID can recognize. + See for details. + + + ts_token_type(3722) + (1,asciiword,"Word, all ASCII") ... + + + + + + + ts_stat + + ts_stat ( sqlquery text + , weights text ) + setof record + ( word text, + ndoc integer, + nentry integer ) + + + Executes the sqlquery, which must return a + single tsvector column, and returns statistics about each + distinct lexeme contained in the data. + See for details. + + + ts_stat('SELECT vector FROM apod') + (foo,10,15) ... + + + + +
+ +
diff --git a/doc/src/sgml/func/func-tid.sgml b/doc/src/sgml/func/func-tid.sgml new file mode 100644 index 0000000000000..188e66a181f69 --- /dev/null +++ b/doc/src/sgml/func/func-tid.sgml @@ -0,0 +1,70 @@ + + TID Functions + + + TID + functions + + + + tid_block + + + + tid_offset + + + + lists functions for + the tid data type (tuple identifier). + + + + <acronym>TID</acronym> Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + tid_block ( tid ) + bigint + + + Extracts the block number from a tuple identifier. + + + tid_block('(42,7)'::tid) + 42 + + + + + + tid_offset ( tid ) + integer + + + Extracts the tuple offset within the block from a tuple identifier. + + + tid_offset('(42,7)'::tid) + 7 + + + + +
+
diff --git a/doc/src/sgml/func/func-trigger.sgml b/doc/src/sgml/func/func-trigger.sgml new file mode 100644 index 0000000000000..94b40adbdb84a --- /dev/null +++ b/doc/src/sgml/func/func-trigger.sgml @@ -0,0 +1,135 @@ + + Trigger Functions + + + While many uses of triggers involve user-written trigger functions, + PostgreSQL provides a few built-in trigger + functions that can be used directly in user-defined triggers. These + are summarized in . + (Additional built-in trigger functions exist, which implement foreign + key constraints and deferred index constraints. Those are not documented + here since users need not use them directly.) + + + + For more information about creating triggers, see + . + + + + Built-In Trigger Functions + + + + + Function + + + Description + + + Example Usage + + + + + + + + + suppress_redundant_updates_trigger + + suppress_redundant_updates_trigger ( ) + trigger + + + Suppresses do-nothing update operations. See below for details. + + + CREATE TRIGGER ... suppress_redundant_updates_trigger() + + + + + + + tsvector_update_trigger + + tsvector_update_trigger ( ) + trigger + + + Automatically updates a tsvector column from associated + plain-text document column(s). The text search configuration to use + is specified by name as a trigger argument. See + for details. + + + CREATE TRIGGER ... tsvector_update_trigger(tsvcol, 'pg_catalog.swedish', title, body) + + + + + + + tsvector_update_trigger_column + + tsvector_update_trigger_column ( ) + trigger + + + Automatically updates a tsvector column from associated + plain-text document column(s). The text search configuration to use + is taken from a regconfig column of the table. See + for details. + + + CREATE TRIGGER ... tsvector_update_trigger_column(tsvcol, tsconfigcol, title, body) + + + + +
+ + + The suppress_redundant_updates_trigger function, + when applied as a row-level BEFORE UPDATE trigger, + will prevent any update that does not actually change the data in the + row from taking place. This overrides the normal behavior which always + performs a physical row update + regardless of whether or not the data has changed. (This normal behavior + makes updates run faster, since no checking is required, and is also + useful in certain cases.) + + + + Ideally, you should avoid running updates that don't actually + change the data in the record. Redundant updates can cost considerable + unnecessary time, especially if there are lots of indexes to alter, + and space in dead rows that will eventually have to be vacuumed. + However, detecting such situations in client code is not + always easy, or even possible, and writing expressions to detect + them can be error-prone. An alternative is to use + suppress_redundant_updates_trigger, which will skip + updates that don't change the data. You should use this with care, + however. The trigger takes a small but non-trivial time for each record, + so if most of the records affected by updates do actually change, + use of this trigger will make updates run slower on average. + + + + The suppress_redundant_updates_trigger function can be + added to a table like this: + +CREATE TRIGGER z_min_update +BEFORE UPDATE ON tablename +FOR EACH ROW EXECUTE FUNCTION suppress_redundant_updates_trigger(); + + In most cases, you need to fire this trigger last for each row, so that + it does not override other triggers that might wish to alter the row. + Bearing in mind that triggers fire in name order, you would therefore + choose a trigger name that comes after the name of any other trigger + you might have on the table. (Hence the z prefix in the + example.) + +
diff --git a/doc/src/sgml/func/func-uuid.sgml b/doc/src/sgml/func/func-uuid.sgml new file mode 100644 index 0000000000000..2638e2bf855f2 --- /dev/null +++ b/doc/src/sgml/func/func-uuid.sgml @@ -0,0 +1,179 @@ + + UUID Functions + + + UUID + generating + + + + gen_random_uuid + + + + uuidv4 + + + + uuidv7 + + + + uuid_extract_timestamp + + + + uuid_extract_version + + + + shows the PostgreSQL + functions that can be used to generate UUIDs. + + + + <acronym>UUID</acronym> Generation Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + gen_random_uuid ( ) + uuid + + + uuidv4 ( ) + uuid + + + Generates a version 4 (random) UUID + + + gen_random_uuid() + 5b30857f-0bfa-48b5-ac0b-5c64e28078d1 + + + uuidv4() + b42410ee-132f-42ee-9e4f-09a6485c95b8 + + + + + uuidv7 + ( shift interval ) + uuid + + + Generates a version 7 (time-ordered) UUID. The timestamp is + computed using UNIX timestamp with millisecond precision + + sub-millisecond timestamp + random. The optional + parameter shift will shift the computed + timestamp by the given interval. + + + uuidv7() + 019535d9-3df7-79fb-b466-fa907fa17f9e + + + + +
+ + + + The module provides additional functions that + implement other standard algorithms for generating UUIDs. + + + + + shows the PostgreSQL + functions that can be used to extract information from UUIDs. + + + + <acronym>UUID</acronym> Extraction Functions + + + + + Function + + + Description + + + Example(s) + + + + + + + + uuid_extract_timestamp + ( uuid ) + timestamp with time zone + + + Extracts a timestamp with time zone from a UUID of + version 1 or 7. For other versions, this function returns null. + Note that the extracted timestamp is not necessarily exactly equal + to the time the UUID was generated; this depends on the + implementation that generated the UUID. + + + uuid_extract_timestamp('019535d9-3df7-79fb-b466-&zwsp;fa907fa17f9e'::uuid) + 2025-02-23 21:46:24.503-05 + + + + + uuid_extract_version + ( uuid ) + smallint + + + Extracts the version from a UUID of one of the variants described by + RFC + 9562. For other variants, this function returns null. + For example, for a UUID generated + by gen_random_uuid(), this function will + return 4. + + + uuid_extract_version('41db1265-8bc1-4ab3-992f-&zwsp;885799a4af1d'::uuid) + 4 + + + uuid_extract_version('019535d9-3df7-79fb-b466-&zwsp;fa907fa17f9e'::uuid) + 7 + + + + +
+ + + PostgreSQL also provides the usual comparison + operators shown in for + UUIDs. + + + See for details on the data type + uuid in PostgreSQL. + +
diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml new file mode 100644 index 0000000000000..bcf755c9ebcab --- /dev/null +++ b/doc/src/sgml/func/func-window.sgml @@ -0,0 +1,292 @@ + + Window Functions + + + window function + built-in + + + + Window functions provide the ability to perform + calculations across sets of rows that are related to the current query + row. See for an introduction to this + feature, and for syntax + details. + + + + The built-in window functions are listed in + . Note that these functions + must be invoked using window function syntax, i.e., an + OVER clause is required. + + + + In addition to these functions, any built-in or user-defined + ordinary aggregate (i.e., not ordered-set or hypothetical-set aggregates) + can be used as a window function; see + for a list of the built-in aggregates. + Aggregate functions act as window functions only when an OVER + clause follows the call; otherwise they act as plain aggregates + and return a single row for the entire set. + + + + General-Purpose Window Functions + + + + + Function + + + Description + + + + + + + + + row_number + + row_number () + bigint + + + Returns the number of the current row within its partition, counting + from 1. + + + + + + + rank + + rank () + bigint + + + Returns the rank of the current row, with gaps; that is, + the row_number of the first row in its peer + group. + + + + + + + dense_rank + + dense_rank () + bigint + + + Returns the rank of the current row, without gaps; this function + effectively counts peer groups. + + + + + + + percent_rank + + percent_rank () + double precision + + + Returns the relative rank of the current row, that is + (rank - 1) / (total partition rows - 1). + The value thus ranges from 0 to 1 inclusive. + + + + + + + cume_dist + + cume_dist () + double precision + + + Returns the cumulative distribution, that is (number of partition rows + preceding or peers with current row) / (total partition rows). + The value thus ranges from 1/N to 1. + + + + + + + ntile + + ntile ( num_buckets integer ) + integer + + + Returns an integer ranging from 1 to the argument value, dividing the + partition as equally as possible. + + + + + + + lag + + lag ( value anycompatible + , offset integer + , default anycompatible ) null treatment + anycompatible + + + Returns value evaluated at + the row that is offset + rows before the current row within the partition; if there is no such + row, instead returns default + (which must be of a type compatible with + value). + Both offset and + default are evaluated + with respect to the current row. If omitted, + offset defaults to 1 and + default to NULL. + + + + + + + lead + + lead ( value anycompatible + , offset integer + , default anycompatible ) null treatment + anycompatible + + + Returns value evaluated at + the row that is offset + rows after the current row within the partition; if there is no such + row, instead returns default + (which must be of a type compatible with + value). + Both offset and + default are evaluated + with respect to the current row. If omitted, + offset defaults to 1 and + default to NULL. + + + + + + + first_value + + first_value ( value anyelement ) null treatment + anyelement + + + Returns value evaluated + at the row that is the first row of the window frame. + + + + + + + last_value + + last_value ( value anyelement ) null treatment + anyelement + + + Returns value evaluated + at the row that is the last row of the window frame. + + + + + + + nth_value + + nth_value ( value anyelement, n integer ) null treatment + anyelement + + + Returns value evaluated + at the row that is the n'th + row of the window frame (counting from 1); + returns NULL if there is no such row. + + + + +
+ + + All of the functions listed in + depend on the sort ordering + specified by the ORDER BY clause of the associated window + definition. Rows that are not distinct when considering only the + ORDER BY columns are said to be peers. + The four ranking functions (including cume_dist) are + defined so that they give the same answer for all rows of a peer group. + + + + Note that first_value, last_value, and + nth_value consider only the rows within the window + frame, which by default contains the rows from the start of the + partition through the last peer of the current row. This is + likely to give unhelpful results for last_value and + sometimes also nth_value. You can redefine the frame by + adding a suitable frame specification (RANGE, + ROWS or GROUPS) to + the OVER clause. + See for more information + about frame specifications. + + + + When an aggregate function is used as a window function, it aggregates + over the rows within the current row's window frame. + An aggregate used with ORDER BY and the default window frame + definition produces a running sum type of behavior, which may or + may not be what's wanted. To obtain + aggregation over the whole partition, omit ORDER BY or use + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING. + Other frame specifications can be used to obtain other effects. + + + + The null treatment option must be one of: + + RESPECT NULLS + IGNORE NULLS + + If unspecified, the default is RESPECT NULLS which includes NULL + values in any result calculation. IGNORE NULLS ignores NULL values. + This option is only allowed for the following functions: lag, + lead, first_value, last_value, + nth_value. + + + + + The SQL standard defines a FROM FIRST or FROM LAST + option for nth_value. This is not implemented in + PostgreSQL: only the default FROM FIRST + behavior is supported. (You can achieve the result of FROM LAST by + reversing the ORDER BY + ordering.) + + + +
diff --git a/doc/src/sgml/func/func-xml.sgml b/doc/src/sgml/func/func-xml.sgml new file mode 100644 index 0000000000000..511bc90852a58 --- /dev/null +++ b/doc/src/sgml/func/func-xml.sgml @@ -0,0 +1,1283 @@ + + + XML Functions + + + XML Functions + + + + The functions and function-like expressions described in this + section operate on values of type xml. See for information about the xml + type. The function-like expressions xmlparse + and xmlserialize for converting to and from + type xml are documented there, not in this section. + + + + Use of most of these functions + requires PostgreSQL to have been built + with configure --with-libxml. + + + + Producing XML Content + + + A set of functions and function-like expressions is available for + producing XML content from SQL data. As such, they are + particularly suitable for formatting query results into XML + documents for processing in client applications. + + + + <literal>xmltext</literal> + + + xmltext + + + +xmltext ( text ) xml + + + + The function xmltext returns an XML value with a single + text node containing the input argument as its content. Predefined entities + like ampersand (), left and right angle brackets + (]]>), and quotation marks () + are escaped. + + + + Example: +'); + xmltext +------------------------- + < foo & bar > +]]> + + + + + <literal>xmlcomment</literal> + + + xmlcomment + + + +xmlcomment ( text ) xml + + + + The function xmlcomment creates an XML value + containing an XML comment with the specified text as content. + The text cannot contain -- or end with a + -, otherwise the resulting construct + would not be a valid XML comment. + If the argument is null, the result is null. + + + + Example: + +]]> + + + + + <literal>xmlconcat</literal> + + + xmlconcat + + + +xmlconcat ( xml , ... ) xml + + + + The function xmlconcat concatenates a list + of individual XML values to create a single value containing an + XML content fragment. Null values are omitted; the result is + only null if there are no nonnull arguments. + + + + Example: +', 'foo'); + + xmlconcat +---------------------- + foo +]]> + + + + XML declarations, if present, are combined as follows. If all + argument values have the same XML version declaration, that + version is used in the result, else no version is used. If all + argument values have the standalone declaration value + yes, then that value is used in the result. If + all argument values have a standalone declaration value and at + least one is no, then that is used in the result. + Else the result will have no standalone declaration. If the + result is determined to require a standalone declaration but no + version declaration, a version declaration with version 1.0 will + be used because XML requires an XML declaration to contain a + version declaration. Encoding declarations are ignored and + removed in all cases. + + + + Example: +', ''); + + xmlconcat +----------------------------------- + +]]> + + + + + <literal>xmlelement</literal> + + + xmlelement + + + +xmlelement ( NAME name , XMLATTRIBUTES ( attvalue AS attname , ... ) , content , ... ) xml + + + + The xmlelement expression produces an XML + element with the given name, attributes, and content. + The name + and attname items shown in the syntax are + simple identifiers, not values. The attvalue + and content items are expressions, which can + yield any PostgreSQL data type. The + argument(s) within XMLATTRIBUTES generate attributes + of the XML element; the content value(s) are + concatenated to form its content. + + + + Examples: + + +SELECT xmlelement(NAME foo, xmlattributes('xyz' AS bar)); + + xmlelement +------------------ + + +SELECT xmlelement(NAME foo, xmlattributes(current_date AS bar), 'cont', 'ent'); + + xmlelement +------------------------------------- + content +]]> + + + + Element and attribute names that are not valid XML names are + escaped by replacing the offending characters by the sequence + _xHHHH_, where + HHHH is the character's Unicode + codepoint in hexadecimal notation. For example: + +]]> + + + + An explicit attribute name need not be specified if the attribute + value is a column reference, in which case the column's name will + be used as the attribute name by default. In other cases, the + attribute must be given an explicit name. So this example is + valid: + +CREATE TABLE test (a xml, b xml); +SELECT xmlelement(NAME test, xmlattributes(a, b)) FROM test; + + But these are not: + +SELECT xmlelement(NAME test, xmlattributes('constant'), a, b) FROM test; +SELECT xmlelement(NAME test, xmlattributes(func(a, b))) FROM test; + + + + + Element content, if specified, will be formatted according to + its data type. If the content is itself of type xml, + complex XML documents can be constructed. For example: + +]]> + + Content of other types will be formatted into valid XML character + data. This means in particular that the characters <, >, + and & will be converted to entities. Binary data (data type + bytea) will be represented in base64 or hex + encoding, depending on the setting of the configuration parameter + . The particular behavior for + individual data types is expected to evolve in order to align the + PostgreSQL mappings with those specified in SQL:2006 and later, + as discussed in . + + + + + <literal>xmlforest</literal> + + + xmlforest + + + +xmlforest ( content AS name , ... ) xml + + + + The xmlforest expression produces an XML + forest (sequence) of elements using the given names and content. + As for xmlelement, + each name must be a simple identifier, while + the content expressions can have any data + type. + + + + Examples: + +SELECT xmlforest('abc' AS foo, 123 AS bar); + + xmlforest +------------------------------ + <foo>abc</foo><bar>123</bar> + + +SELECT xmlforest(table_name, column_name) +FROM information_schema.columns +WHERE table_schema = 'pg_catalog'; + + xmlforest +------------------------------------&zwsp;----------------------------------- + <table_name>pg_authid</table_name>&zwsp;<column_name>rolname</column_name> + <table_name>pg_authid</table_name>&zwsp;<column_name>rolsuper</column_name> + ... + + + As seen in the second example, the element name can be omitted if + the content value is a column reference, in which case the column + name is used by default. Otherwise, a name must be specified. + + + + Element names that are not valid XML names are escaped as shown + for xmlelement above. Similarly, content + data is escaped to make valid XML content, unless it is already + of type xml. + + + + Note that XML forests are not valid XML documents if they consist + of more than one element, so it might be useful to wrap + xmlforest expressions in + xmlelement. + + + + + <literal>xmlpi</literal> + + + xmlpi + + + +xmlpi ( NAME name , content ) xml + + + + The xmlpi expression creates an XML + processing instruction. + As for xmlelement, + the name must be a simple identifier, while + the content expression can have any data type. + The content, if present, must not contain the + character sequence ?>. + + + + Example: + +]]> + + + + + <literal>xmlroot</literal> + + + xmlroot + + + +xmlroot ( xml, VERSION {text|NO VALUE} , STANDALONE {YES|NO|NO VALUE} ) xml + + + + The xmlroot expression alters the properties + of the root node of an XML value. If a version is specified, + it replaces the value in the root node's version declaration; if a + standalone setting is specified, it replaces the value in the + root node's standalone declaration. + + + +abc'), + version '1.0', standalone yes); + + xmlroot +---------------------------------------- + + abc +]]> + + + + + <literal>xmlagg</literal> + + + xmlagg + + + +xmlagg ( xml ) xml + + + + The function xmlagg is, unlike the other + functions described here, an aggregate function. It concatenates the + input values to the aggregate function call, + much like xmlconcat does, except that concatenation + occurs across rows rather than across expressions in a single row. + See for additional information + about aggregate functions. + + + + Example: +abc'); +INSERT INTO test VALUES (2, ''); +SELECT xmlagg(x) FROM test; + xmlagg +---------------------- + abc +]]> + + + + To determine the order of the concatenation, an ORDER BY + clause may be added to the aggregate call as described in + . For example: + +abc +]]> + + + + The following non-standard approach used to be recommended + in previous versions, and may still be useful in specific + cases: + +abc +]]> + + + + + + XML Predicates + + + The expressions described in this section check properties + of xml values. + + + + <literal>IS DOCUMENT</literal> + + + IS DOCUMENT + + + +xml IS DOCUMENT boolean + + + + The expression IS DOCUMENT returns true if the + argument XML value is a proper XML document, false if it is not + (that is, it is a content fragment), or null if the argument is + null. See about the difference + between documents and content fragments. + + + + + <literal>IS NOT DOCUMENT</literal> + + + IS NOT DOCUMENT + + + +xml IS NOT DOCUMENT boolean + + + + The expression IS NOT DOCUMENT returns false if the + argument XML value is a proper XML document, true if it is not (that is, + it is a content fragment), or null if the argument is null. + + + + + <literal>XMLEXISTS</literal> + + + XMLEXISTS + + + +XMLEXISTS ( text PASSING BY {REF|VALUE} xml BY {REF|VALUE} ) boolean + + + + The function xmlexists evaluates an XPath 1.0 + expression (the first argument), with the passed XML value as its context + item. The function returns false if the result of that evaluation + yields an empty node-set, true if it yields any other value. The + function returns null if any argument is null. A nonnull value + passed as the context item must be an XML document, not a content + fragment or any non-XML value. + + + + Example: + TorontoOttawa'); + + xmlexists +------------ + t +(1 row) +]]> + + + + The BY REF and BY VALUE clauses + are accepted in PostgreSQL, but are ignored, + as discussed in . + + + + In the SQL standard, the xmlexists function + evaluates an expression in the XML Query language, + but PostgreSQL allows only an XPath 1.0 + expression, as discussed in + . + + + + + <literal>xml_is_well_formed</literal> + + + xml_is_well_formed + + + + xml_is_well_formed_document + + + + xml_is_well_formed_content + + + +xml_is_well_formed ( text ) boolean +xml_is_well_formed_document ( text ) boolean +xml_is_well_formed_content ( text ) boolean + + + + These functions check whether a text string represents + well-formed XML, returning a Boolean result. + xml_is_well_formed_document checks for a well-formed + document, while xml_is_well_formed_content checks + for well-formed content. xml_is_well_formed does + the former if the configuration + parameter is set to DOCUMENT, or the latter if it is set to + CONTENT. This means that + xml_is_well_formed is useful for seeing whether + a simple cast to type xml will succeed, whereas the other two + functions are useful for seeing whether the corresponding variants of + XMLPARSE will succeed. + + + + Examples: + +'); + xml_is_well_formed +-------------------- + f +(1 row) + +SELECT xml_is_well_formed(''); + xml_is_well_formed +-------------------- + t +(1 row) + +SET xmloption TO CONTENT; +SELECT xml_is_well_formed('abc'); + xml_is_well_formed +-------------------- + t +(1 row) + +SELECT xml_is_well_formed_document('bar'); + xml_is_well_formed_document +----------------------------- + t +(1 row) + +SELECT xml_is_well_formed_document('bar'); + xml_is_well_formed_document +----------------------------- + f +(1 row) +]]> + + The last example shows that the checks include whether + namespaces are correctly matched. + + + + + + Processing XML + + + To process values of data type xml, PostgreSQL offers + the functions xpath and + xpath_exists, which evaluate XPath 1.0 + expressions, and the XMLTABLE + table function. + + + + <literal>xpath</literal> + + + XPath + + + +xpath ( xpath text, xml xml , nsarray text[] ) xml[] + + + + The function xpath evaluates the XPath 1.0 + expression xpath (given as text) + against the XML value + xml. It returns an array of XML values + corresponding to the node-set produced by the XPath expression. + If the XPath expression returns a scalar value rather than a node-set, + a single-element array is returned. + + + + The second argument must be a well formed XML document. In particular, + it must have a single root node element. + + + + The optional third argument of the function is an array of namespace + mappings. This array should be a two-dimensional text array with + the length of the second axis being equal to 2 (i.e., it should be an + array of arrays, each of which consists of exactly 2 elements). + The first element of each array entry is the namespace name (alias), the + second the namespace URI. It is not required that aliases provided in + this array be the same as those being used in the XML document itself (in + other words, both in the XML document and in the xpath + function context, aliases are local). + + + + Example: +test', + ARRAY[ARRAY['my', 'http://example.com']]); + + xpath +-------- + {test} +(1 row) +]]> + + + + To deal with default (anonymous) namespaces, do something like this: +test', + ARRAY[ARRAY['mydefns', 'http://example.com']]); + + xpath +-------- + {test} +(1 row) +]]> + + + + + <literal>xpath_exists</literal> + + + xpath_exists + + + +xpath_exists ( xpath text, xml xml , nsarray text[] ) boolean + + + + The function xpath_exists is a specialized form + of the xpath function. Instead of returning the + individual XML values that satisfy the XPath 1.0 expression, this function + returns a Boolean indicating whether the query was satisfied or not + (specifically, whether it produced any value other than an empty node-set). + This function is equivalent to the XMLEXISTS predicate, + except that it also offers support for a namespace mapping argument. + + + + Example: +test', + ARRAY[ARRAY['my', 'http://example.com']]); + + xpath_exists +-------------- + t +(1 row) +]]> + + + + + <literal>xmltable</literal> + + + xmltable + + + + table function + XMLTABLE + + + +XMLTABLE ( + XMLNAMESPACES ( namespace_uri AS namespace_name , ... ), + row_expression PASSING BY {REF|VALUE} document_expression BY {REF|VALUE} + COLUMNS name { type PATH column_expression DEFAULT default_expression NOT NULL | NULL + | FOR ORDINALITY } + , ... +) setof record + + + + The xmltable expression produces a table based + on an XML value, an XPath filter to extract rows, and a + set of column definitions. + Although it syntactically resembles a function, it can only appear + as a table in a query's FROM clause. + + + + The optional XMLNAMESPACES clause gives a + comma-separated list of namespace definitions, where + each namespace_uri is a text + expression and each namespace_name is a simple + identifier. It specifies the XML namespaces used in the document and + their aliases. A default namespace specification is not currently + supported. + + + + The required row_expression argument is an + XPath 1.0 expression (given as text) that is evaluated, + passing the XML value document_expression as + its context item, to obtain a set of XML nodes. These nodes are what + xmltable transforms into output rows. No rows + will be produced if the document_expression + is null, nor if the row_expression produces + an empty node-set or any value other than a node-set. + + + + document_expression provides the context + item for the row_expression. It must be a + well-formed XML document; fragments/forests are not accepted. + The BY REF and BY VALUE clauses + are accepted but ignored, as discussed in + . + + + + In the SQL standard, the xmltable function + evaluates expressions in the XML Query language, + but PostgreSQL allows only XPath 1.0 + expressions, as discussed in + . + + + + The required COLUMNS clause specifies the + column(s) that will be produced in the output table. + See the syntax summary above for the format. + A name is required for each column, as is a data type + (unless FOR ORDINALITY is specified, in which case + type integer is implicit). The path, default and + nullability clauses are optional. + + + + A column marked FOR ORDINALITY will be populated + with row numbers, starting with 1, in the order of nodes retrieved from + the row_expression's result node-set. + At most one column may be marked FOR ORDINALITY. + + + + + XPath 1.0 does not specify an order for nodes in a node-set, so code + that relies on a particular order of the results will be + implementation-dependent. Details can be found in + . + + + + + The column_expression for a column is an + XPath 1.0 expression that is evaluated for each row, with the current + node from the row_expression result as its + context item, to find the value of the column. If + no column_expression is given, then the + column name is used as an implicit path. + + + + If a column's XPath expression returns a non-XML value (which is limited + to string, boolean, or double in XPath 1.0) and the column has a + PostgreSQL type other than xml, the column will be set + as if by assigning the value's string representation to the PostgreSQL + type. (If the value is a boolean, its string representation is taken + to be 1 or 0 if the output + column's type category is numeric, otherwise true or + false.) + + + + If a column's XPath expression returns a non-empty set of XML nodes + and the column's PostgreSQL type is xml, the column will + be assigned the expression result exactly, if it is of document or + content form. + + + A result containing more than one element node at the top level, or + non-whitespace text outside of an element, is an example of content form. + An XPath result can be of neither form, for example if it returns an + attribute node selected from the element that contains it. Such a result + will be put into content form with each such disallowed node replaced by + its string value, as defined for the XPath 1.0 + string function. + + + + + + A non-XML result assigned to an xml output column produces + content, a single text node with the string value of the result. + An XML result assigned to a column of any other type may not have more than + one node, or an error is raised. If there is exactly one node, the column + will be set as if by assigning the node's string + value (as defined for the XPath 1.0 string function) + to the PostgreSQL type. + + + + The string value of an XML element is the concatenation, in document order, + of all text nodes contained in that element and its descendants. The string + value of an element with no descendant text nodes is an + empty string (not NULL). + Any xsi:nil attributes are ignored. + Note that the whitespace-only text() node between two non-text + elements is preserved, and that leading whitespace on a text() + node is not flattened. + The XPath 1.0 string function may be consulted for the + rules defining the string value of other XML node types and non-XML values. + + + + The conversion rules presented here are not exactly those of the SQL + standard, as discussed in . + + + + If the path expression returns an empty node-set + (typically, when it does not match) + for a given row, the column will be set to NULL, unless + a default_expression is specified; then the + value resulting from evaluating that expression is used. + + + + A default_expression, rather than being + evaluated immediately when xmltable is called, + is evaluated each time a default is needed for the column. + If the expression qualifies as stable or immutable, the repeat + evaluation may be skipped. + This means that you can usefully use volatile functions like + nextval in + default_expression. + + + + Columns may be marked NOT NULL. If the + column_expression for a NOT + NULL column does not match anything and there is + no DEFAULT or + the default_expression also evaluates to null, + an error is reported. + + + + Examples: + + + AU + Australia + + + JP + Japan + Shinzo Abe + 145935 + + + SG + Singapore + 697 + + +$$ AS data; + +SELECT xmltable.* + FROM xmldata, + XMLTABLE('//ROWS/ROW' + PASSING data + COLUMNS id int PATH '@id', + ordinality FOR ORDINALITY, + "COUNTRY_NAME" text, + country_id text PATH 'COUNTRY_ID', + size_sq_km float PATH 'SIZE[@unit = "sq_km"]', + size_other text PATH + 'concat(SIZE[@unit!="sq_km"], " ", SIZE[@unit!="sq_km"]/@unit)', + premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified'); + + id | ordinality | COUNTRY_NAME | country_id | size_sq_km | size_other | premier_name +----+------------+--------------+------------+------------+--------------+--------------- + 1 | 1 | Australia | AU | | | not specified + 5 | 2 | Japan | JP | | 145935 sq_mi | Shinzo Abe + 6 | 3 | Singapore | SG | 697 | | not specified +]]> + + The following example shows concatenation of multiple text() nodes, + usage of the column name as XPath filter, and the treatment of whitespace, + XML comments and processing instructions: + + + Hello2a2 bbbxxxCC + +$$ AS data; + +SELECT xmltable.* + FROM xmlelements, XMLTABLE('/root' PASSING data COLUMNS element text); + element +------------------------- + Hello2a2 bbbxxxCC +]]> + + + + The following example illustrates how + the XMLNAMESPACES clause can be used to specify + a list of namespaces + used in the XML document as well as in the XPath expressions: + + + + + +'::xml) +) +SELECT xmltable.* + FROM XMLTABLE(XMLNAMESPACES('http://example.com/myns' AS x, + 'http://example.com/b' AS "B"), + '/x:example/x:item' + PASSING (SELECT data FROM xmldata) + COLUMNS foo int PATH '@foo', + bar int PATH '@B:bar'); + foo | bar +-----+----- + 1 | 2 + 3 | 4 + 4 | 5 +(3 rows) +]]> + + + + + + Mapping Tables to XML + + + XML export + + + + The following functions map the contents of relational tables to + XML values. They can be thought of as XML export functionality: + +table_to_xml ( table regclass, nulls boolean, + tableforest boolean, targetns text ) xml +query_to_xml ( query text, nulls boolean, + tableforest boolean, targetns text ) xml +cursor_to_xml ( cursor refcursor, count integer, nulls boolean, + tableforest boolean, targetns text ) xml + + + + + table_to_xml maps the content of the named + table, passed as parameter table. The + regclass type accepts strings identifying tables using the + usual notation, including optional schema qualification and + double quotes (see for details). + query_to_xml executes the + query whose text is passed as parameter + query and maps the result set. + cursor_to_xml fetches the indicated number of + rows from the cursor specified by the parameter + cursor. This variant is recommended if + large tables have to be mapped, because the result value is built + up in memory by each function. + + + + If tableforest is false, then the resulting + XML document looks like this: + + + data + data + + + + ... + + + ... + +]]> + + If tableforest is true, the result is an + XML content fragment that looks like this: + + data + data + + + + ... + + +... +]]> + + If no table name is available, that is, when mapping a query or a + cursor, the string table is used in the first + format, row in the second format. + + + + The choice between these formats is up to the user. The first + format is a proper XML document, which will be important in many + applications. The second format tends to be more useful in the + cursor_to_xml function if the result values are to be + reassembled into one document later on. The functions for + producing XML content discussed above, in particular + xmlelement, can be used to alter the results + to taste. + + + + The data values are mapped in the same way as described for the + function xmlelement above. + + + + The parameter nulls determines whether null + values should be included in the output. If true, null values in + columns are represented as: + +]]> + where xsi is the XML namespace prefix for XML + Schema Instance. An appropriate namespace declaration will be + added to the result value. If false, columns containing null + values are simply omitted from the output. + + + + The parameter targetns specifies the + desired XML namespace of the result. If no particular namespace + is wanted, an empty string should be passed. + + + + The following functions return XML Schema documents describing the + mappings performed by the corresponding functions above: + +table_to_xmlschema ( table regclass, nulls boolean, + tableforest boolean, targetns text ) xml +query_to_xmlschema ( query text, nulls boolean, + tableforest boolean, targetns text ) xml +cursor_to_xmlschema ( cursor refcursor, nulls boolean, + tableforest boolean, targetns text ) xml + + It is essential that the same parameters are passed in order to + obtain matching XML data mappings and XML Schema documents. + + + + The following functions produce XML data mappings and the + corresponding XML Schema in one document (or forest), linked + together. They can be useful where self-contained and + self-describing results are wanted: + +table_to_xml_and_xmlschema ( table regclass, nulls boolean, + tableforest boolean, targetns text ) xml +query_to_xml_and_xmlschema ( query text, nulls boolean, + tableforest boolean, targetns text ) xml + + + + + In addition, the following functions are available to produce + analogous mappings of entire schemas or the entire current + database: + +schema_to_xml ( schema name, nulls boolean, + tableforest boolean, targetns text ) xml +schema_to_xmlschema ( schema name, nulls boolean, + tableforest boolean, targetns text ) xml +schema_to_xml_and_xmlschema ( schema name, nulls boolean, + tableforest boolean, targetns text ) xml + +database_to_xml ( nulls boolean, + tableforest boolean, targetns text ) xml +database_to_xmlschema ( nulls boolean, + tableforest boolean, targetns text ) xml +database_to_xml_and_xmlschema ( nulls boolean, + tableforest boolean, targetns text ) xml + + + These functions ignore tables that are not readable by the current user. + The database-wide functions additionally ignore schemas that the current + user does not have USAGE (lookup) privilege for. + + + + Note that these potentially produce a lot of data, which needs to + be built up in memory. When requesting content mappings of large + schemas or databases, it might be worthwhile to consider mapping the + tables separately instead, possibly even through a cursor. + + + + The result of a schema content mapping looks like this: + + + +table1-mapping + +table2-mapping + +... + +]]> + + where the format of a table mapping depends on the + tableforest parameter as explained above. + + + + The result of a database content mapping looks like this: + + + + + ... + + + + ... + + +... + +]]> + + where the schema mapping is as above. + + + + As an example of using the output produced by these functions, + shows an XSLT stylesheet that + converts the output of + table_to_xml_and_xmlschema to an HTML + document containing a tabular rendition of the table data. In a + similar manner, the results from these functions can be + converted into other XML-based formats. + + + + XSLT Stylesheet for Converting SQL/XML Output to HTML + + + + + + + + + + + + + <xsl:value-of select="name(current())"/> + + + + + + + + + + + + + + + + +
+ + +
+ +
+]]>
+
+
+
diff --git a/doc/src/sgml/func/func.sgml b/doc/src/sgml/func/func.sgml new file mode 100644 index 0000000000000..c9c231dd19022 --- /dev/null +++ b/doc/src/sgml/func/func.sgml @@ -0,0 +1,85 @@ + + + + Functions and Operators + + + function + + + + operator + + + + PostgreSQL provides a large number of + functions and operators for the built-in data types. This chapter + describes most of them, although additional special-purpose functions + appear in relevant sections of the manual. Users can also + define their own functions and operators, as described in + . The + psql commands \df and + \do can be used to list all + available functions and operators, respectively. + + + + The notation used throughout this chapter to describe the argument and + result data types of a function or operator is like this: + +repeat ( text, integer ) text + + which says that the function repeat takes one text and + one integer argument and returns a result of type text. The right arrow + is also used to indicate the result of an example, thus: + +repeat('Pg', 4) PgPgPgPg + + + + + If you are concerned about portability then note that most of + the functions and operators described in this chapter, with the + exception of the most trivial arithmetic and comparison operators + and some explicitly marked functions, are not specified by the + SQL standard. Some of this extended functionality + is present in other SQL database management + systems, and in many cases this functionality is compatible and + consistent between the various implementations. + + + +&func-logical; +&func-comparison; +&func-math; +&func-string; +&func-binarystring; +&func-bitstring; +&func-matching; +&func-formatting; +&func-datetime; +&func-enum; +&func-geometry; +&func-net; +&func-textsearch; +&func-tid; +&func-uuid; +&func-xml; +&func-json; +&func-sequence; +&func-conditional; +&func-array; +&func-range; +&func-aggregate; +&func-window; +&func-merge-support; +&func-subquery; +&func-comparisons; +&func-srf; +&func-info; +&func-admin; +&func-trigger; +&func-event-triggers; +&func-statistics; + + diff --git a/doc/src/sgml/generate-errcodes-table.pl b/doc/src/sgml/generate-errcodes-table.pl index d939eec3f4a8e..d35211d36e991 100644 --- a/doc/src/sgml/generate-errcodes-table.pl +++ b/doc/src/sgml/generate-errcodes-table.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate the errcodes-table.sgml file from errcodes.txt -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/doc/src/sgml/generate-keywords-table.pl b/doc/src/sgml/generate-keywords-table.pl index 76c4689872fab..36da74f3e13db 100644 --- a/doc/src/sgml/generate-keywords-table.pl +++ b/doc/src/sgml/generate-keywords-table.pl @@ -2,7 +2,7 @@ # # Generate the keywords table for the documentation's SQL Key Words appendix # -# Copyright (c) 2019-2025, PostgreSQL Global Development Group +# Copyright (c) 2019-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/doc/src/sgml/generate-targets-meson.pl b/doc/src/sgml/generate-targets-meson.pl index d127429c98181..542b2e04ff390 100644 --- a/doc/src/sgml/generate-targets-meson.pl +++ b/doc/src/sgml/generate-targets-meson.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate the targets-meson.sgml file from targets-meson.txt -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/doc/src/sgml/gin.sgml b/doc/src/sgml/gin.sgml index 46e87e01324dd..82410b1fbdfa1 100644 --- a/doc/src/sgml/gin.sgml +++ b/doc/src/sgml/gin.sgml @@ -394,7 +394,11 @@ Pointer extra_data) - Compare a partial-match query key to an index key. Returns an integer + Compare a partial-match query key to an index key. + partial_key is a query key that was returned + by extractQuery with an indication that it + requires partial match, and key is an index entry. + Returns an integer whose sign indicates the result: less than zero means the index key does not match the query, but the index scan should continue; zero means that the index key does match the query; greater than zero diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml index a373a8aa4b2fc..3f1a01f381f9d 100644 --- a/doc/src/sgml/gist.sgml +++ b/doc/src/sgml/gist.sgml @@ -273,14 +273,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops); index will depend on the penalty and picksplit methods. Two optional methods are compress and - decompress, which allow an index to have internal tree data of - a different type than the data it indexes. The leaves are to be of the - indexed data type, while the other tree nodes can be of any C struct (but - you still have to follow PostgreSQL data type rules here, - see about varlena for variable sized data). If the tree's - internal data type exists at the SQL level, the STORAGE option - of the CREATE OPERATOR CLASS command can be used. - The optional eighth method is distance, which is needed + decompress, which allow an index to store keys that + are of a different type than the data it indexes, or are a compressed + representation of that type. + The optional eighth method distance is needed if the operator class wishes to support ordered scans (nearest-neighbor searches). The optional ninth method fetch is needed if the operator class wishes to support index-only scans, except when the @@ -291,9 +287,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops); speed up building a GiST index. The optional twelfth method stratnum is used to translate compare types (from - src/include/nodes/primnodes.h) into strategy numbers + src/include/access/cmptype.h) into strategy numbers used by the operator class. This lets the core code look up operators for temporal constraint indexes. + All these methods are described in more detail below. @@ -343,7 +340,9 @@ my_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); data_type *query = PG_GETARG_DATA_TYPE_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); data_type *key = DatumGetDataType(entry->key); bool retval; @@ -482,6 +481,24 @@ my_union(PG_FUNCTION_ARGS) in the index without modification.
+ + Use the STORAGE option of the CREATE + OPERATOR CLASS command to define the data type that is + stored in the index, if it is different from the data type being + indexed. Be aware however that the STORAGE data + type is only used to define the physical properties of the index + entries (their typlen, + typbyval, + and typalign attributes). What is + actually in the index datums is under the control of the + compress and decompress + methods, so long as the stored datums match those properties. + It is allowed for compress to produce different + representations for leaf keys than for keys on higher-level index + pages, so long as both representations match + the STORAGE data type. + + The SQL declaration of the function must look like this: @@ -506,11 +523,11 @@ my_compress(PG_FUNCTION_ARGS) if (entry->leafkey) { /* replace entry->key with a compressed version */ - compressed_data_type *compressed_data = palloc(sizeof(compressed_data_type)); + compressed_data_type *compressed_data = palloc_object(compressed_data_type); /* fill *compressed_data from entry->key ... */ - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(compressed_data), entry->rel, entry->page, entry->offset, FALSE); } @@ -830,8 +847,10 @@ my_distance(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); data_type *query = PG_GETARG_DATA_TYPE_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - /* Oid subtype = PG_GETARG_OID(3); */ - /* bool *recheck = (bool *) PG_GETARG_POINTER(4); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); + bool *recheck = (bool *) PG_GETARG_POINTER(4); +#endif data_type *key = DatumGetDataType(entry->key); double retval; @@ -921,8 +940,8 @@ my_fetch(PG_FUNCTION_ARGS) fetched_data_type *fetched_data; GISTENTRY *retval; - retval = palloc(sizeof(GISTENTRY)); - fetched_data = palloc(sizeof(fetched_data_type)); + retval = palloc_object(GISTENTRY); + fetched_data = palloc_object(fetched_data_type); /* * Convert 'fetched_data' into the a Datum of the original datatype. @@ -1170,11 +1189,11 @@ my_sortsupport(PG_FUNCTION_ARGS) - stratnum + translate_cmptype Given a CompareType value from - src/include/nodes/primnodes.h, returns a strategy + src/include/access/cmptype.h, returns a strategy number used by this operator class for matching functionality. The function should return InvalidStrategy if the operator class has no matching strategy. @@ -1188,12 +1207,23 @@ my_sortsupport(PG_FUNCTION_ARGS) non-WITHOUT OVERLAPS part(s) of an index constraint. + + This support function corresponds to the index access method callback + function amtranslatecmptype (see ). The + amtranslatecmptype callback function for + GiST indexes merely calls down to the + translate_cmptype support function of the + respective operator family, since the GiST index access method has no + fixed strategy numbers itself. + + The SQL declaration of the function must look like this: -CREATE OR REPLACE FUNCTION my_stratnum(integer) +CREATE OR REPLACE FUNCTION my_translate_cmptype(integer) RETURNS smallint AS 'MODULE_PATHNAME' LANGUAGE C STRICT; @@ -1202,7 +1232,7 @@ LANGUAGE C STRICT; And the operator family registration must look like this: ALTER OPERATOR FAMILY my_opfamily USING gist ADD - FUNCTION 12 ("any", "any") my_stratnum(int); + FUNCTION 12 ("any", "any") my_translate_cmptype(int); @@ -1210,10 +1240,10 @@ ALTER OPERATOR FAMILY my_opfamily USING gist ADD The matching code in the C module could then follow this skeleton: -PG_FUNCTION_INFO_V1(my_stratnum); +PG_FUNCTION_INFO_V1(my_translate_cmptype); Datum -my_stratnum(PG_FUNCTION_ARGS) +my_translate_cmptype(PG_FUNCTION_ARGS) { CompareType cmptype = PG_GETARG_INT32(0); StrategyNumber ret = InvalidStrategy; @@ -1232,11 +1262,11 @@ my_stratnum(PG_FUNCTION_ARGS) One translation function is provided by PostgreSQL: - gist_stratnum_common is for operator classes that + gist_translate_cmptype_common is for operator classes that use the RT*StrategyNumber constants. The btree_gist extension defines a second translation function, - gist_stratnum_btree, for operator classes that use + gist_translate_cmptype_btree, for operator classes that use the BT*StrategyNumber constants. diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index b88cac598e901..b881ae71198d9 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -81,6 +81,21 @@ + + Application time + + + In a temporal table, + the dimension of time that represents when the entity described by the table + changed (as opposed to the table itself). + + + For more information, see + . + + + + Asynchronous I/O AIO @@ -184,6 +199,8 @@ (but not the autovacuum workers), the background writer, the checkpointer, + the data checksums worker, + the data checksums worker launcher, the logger, the startup process, the WAL archiver, @@ -559,6 +576,28 @@ + + Data Checksums Worker + + + A background worker + which enables data checksums in a specific database. + + + + + + Data Checksums Worker Launcher + + + A background worker + which starts data + checksum worker processes for enabling data checksums in each + database, or disables data checksums cluster-wide. + + + + Database @@ -897,6 +936,23 @@ + + GUC + + + Based on a short-hand for Grand Unified Configuration, the subsystem + which controls server configuration, it is now a commonly used term + for the individual configuration parameters themselves. Typically meant + to refer to user settable parameters, it can also refer to internal or + build-time parameters. + + + For information on working with configuration parameters, see + . + + + + Heap @@ -1419,11 +1475,15 @@ Relation - The generic term for all objects in a - database - that have a name and a list of - attributes - defined in a specific order. + Mathematically, a relation is a set of + tuples; + this is the sense meant in the term "relational database". + + + + In PostgreSQL, "relation" is commonly used to + mean an SQL object + that has a name and a list of attributes defined in a specific order. Tables, sequences, views, @@ -1431,15 +1491,14 @@ materialized views, composite types, and indexes are all relations. + A relation in this sense is a container or a descriptor for a set of tuples. + - More generically, a relation is a set of tuples; for example, - the result of a query is also a relation. - - - In PostgreSQL, - Class is an archaic synonym for - relation. + Class is an alternative but archaic term. + The system catalog + pg_class + holds an entry for each PostgreSQL relation. @@ -1844,6 +1903,22 @@ + + System time + + + In a temporal table, + the dimension of time that represents when the table itself was changed + (as opposed to the entity the table describes). + Often used for auditing, compliance, and debugging. + + + For more information, see + . + + + + Table @@ -1882,6 +1957,37 @@ + + Temporal leftovers + + + After a temporal update or delete, the portion of history that was not + updated/deleted. When using ranges to track application time, there may be + zero, one, or two stretches of history that were not updated/deleted + (before and/or after the portion that was updated/deleted). New rows are + automatically inserted into the table to preserve that history. A single + multirange can accommodate the untouched history before and after the + update/delete, so there will be only zero or one leftover. + + + + + + Temporal table + + + Tables + that track application time + or system time (or both). + Not to be confused with temporary tables. + + + For more information, see + . + + + + Temporary table diff --git a/doc/src/sgml/hash.sgml b/doc/src/sgml/hash.sgml index 9e69ef91fe834..34f3b2cb0c1b3 100644 --- a/doc/src/sgml/hash.sgml +++ b/doc/src/sgml/hash.sgml @@ -125,11 +125,10 @@ Both scanning the index and inserting tuples require locating the bucket where a given tuple ought to be located. To do this, we need the bucket count, highmask, and lowmask from the metapage; however, it's undesirable - for performance reasons to have to have to lock and pin the metapage for - every such operation. Instead, we retain a cached copy of the metapage - in each backend's relcache entry. This will produce the correct bucket - mapping as long as the target bucket hasn't been split since the last - cache refresh. + for performance reasons to have to lock and pin the metapage for every such + operation. Instead, we retain a cached copy of the metapage in each + backend's relcache entry. This will produce the correct bucket mapping as + long as the target bucket hasn't been split since the last cache refresh. diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml index b47d8b4106efb..be8d3a5bfead7 100644 --- a/doc/src/sgml/high-availability.sgml +++ b/doc/src/sgml/high-availability.sgml @@ -151,7 +151,7 @@ protocol to make nodes agree on a serializable transactional order. A standby server can be implemented using file-based log shipping - () or streaming replication (see + () or streaming physical replication (see ), or a combination of both. For information on hot standby, see . @@ -628,7 +628,7 @@ protocol to make nodes agree on a serializable transactional order. In standby mode, the server continuously applies WAL received from the primary server. The standby server can read WAL from a WAL archive (see ) or directly from the primary - over a TCP connection (streaming replication). The standby server will + over a TCP connection (streaming physical replication). The standby server will also attempt to restore any WAL found in the standby cluster's pg_wal directory. That typically happens after a server restart, when the standby replays again WAL that was streamed from the @@ -745,8 +745,8 @@ protocol to make nodes agree on a serializable transactional order. A simple example of configuration is: primary_conninfo = 'host=192.168.1.50 port=5432 user=foo password=foopass options=''-c wal_sender_timeout=5000''' -restore_command = 'cp /path/to/archive/%f %p' -archive_cleanup_command = 'pg_archivecleanup /path/to/archive %r' +restore_command = 'cp "/path/to/archive/%f" "%p"' +archive_cleanup_command = 'pg_archivecleanup /path/to/archive "%r"' @@ -772,6 +772,14 @@ archive_cleanup_command = 'pg_archivecleanup /path/to/archive %r' generated, without waiting for the WAL file to be filled. + + + This discussion of streaming replication assumes physical replication. + Although you could treat a logical replication subscriber as a warm standby, + it would require some differences to what is described here. + + + Streaming replication is asynchronous by default (see ), in which case there is @@ -857,7 +865,7 @@ archive_cleanup_command = 'pg_archivecleanup /path/to/archive %r' # as a replication standby if the user's password is correctly supplied. # # TYPE DATABASE USER ADDRESS METHOD -host replication foo 192.168.1.100/32 md5 +host replication foo 192.168.1.100/32 scram-sha-256
@@ -925,11 +933,11 @@ primary_conninfo = 'host=192.168.1.50 port=5432 user=foo password=foopass' Replication slots provide an automated way to ensure that the - primary server does - not remove WAL segments until they have been received by all standbys, - and that the primary does not remove rows which could cause a - recovery conflict even when the - standby is disconnected. + primary server does not remove WAL segments until they have been + received, physically or logically, by all standbys/subscribers, + and that the primary does not remove rows which could cause a recovery conflict on physical + replicas even when the standby is disconnected. In lieu of using replication slots, it is possible to prevent the removal @@ -943,9 +951,9 @@ primary_conninfo = 'host=192.168.1.50 port=5432 user=foo password=foopass' Similarly, on its own, without - also using a replication slot, provides protection against relevant rows + also using a physical replication slot, provides protection against relevant rows being removed by vacuum, but provides no protection during any time period - when the standby is not connected. + when the physical replication standby is disconnected. @@ -978,7 +986,7 @@ primary_conninfo = 'host=192.168.1.50 port=5432 user=foo password=foopass' Configuration Example - You can create a replication slot like this: + You can create a physical replication slot on the primary like this: postgres=# SELECT * FROM pg_create_physical_replication_slot('node_a_slot'); slot_name | lsn @@ -1182,10 +1190,12 @@ primary_slot_name = 'node_a_slot' - Users will stop waiting if a fast shutdown is requested. However, as - when using asynchronous replication, the server will not fully - shutdown until all outstanding WAL records are transferred to the currently - connected standby servers. + Users will stop waiting if a fast shutdown is requested. However, when + using replication, the server will not fully shutdown until all + outstanding WAL records are transferred to the currently connected + standby servers, or + (if set) expires, regardless of whether replication is synchronous or + asynchronous. @@ -1376,6 +1386,60 @@ synchronous_standby_names = 'ANY 2 (s1, s2, s3)' + + Read-Your-Writes Consistency + + + In asynchronous replication, there is always a short window where changes + on the primary may not yet be visible on the standby due to replication + lag. This can lead to inconsistencies when an application writes data on + the primary and then immediately issues a read query on the standby. + However, it is possible to address this without switching to synchronous + replication. + + + + To address this, PostgreSQL offers a mechanism for read-your-writes + consistency. The key idea is to ensure that a client sees its own writes + by synchronizing the WAL replay on the standby with the known point of + change on the primary. + + + + This is achieved by the following steps. After performing write + operations, the application retrieves the current WAL location using a + function call like this. + + +postgres=# SELECT pg_current_wal_insert_lsn(); +pg_current_wal_insert_lsn +-------------------- +0/306EE20 +(1 row) + + + + + The LSN obtained from the primary is then communicated + to the standby server. This can be managed at the application level or + via the connection pooler. On the standby, the application issues the + command to block further processing until + the standby's WAL replay process reaches (or exceeds) the specified + LSN. + + +postgres=# WAIT FOR LSN '0/306EE20'; + status +-------- + success +(1 row) + + Once the command returns a status of success, it guarantees that all + changes up to the provided LSN have been applied, + ensuring that subsequent read queries will reflect the latest updates. + + + Continuous Archiving in Standby diff --git a/doc/src/sgml/history.sgml b/doc/src/sgml/history.sgml index 8bfa1db670d7a..628f13ce12302 100644 --- a/doc/src/sgml/history.sgml +++ b/doc/src/sgml/history.sgml @@ -171,7 +171,7 @@ A short tutorial introducing regular SQL features as well as those of Postgres95 was distributed with the - source code + source code.
diff --git a/doc/src/sgml/hstore.sgml b/doc/src/sgml/hstore.sgml index 44325e0bba0c4..780ee224dce1f 100644 --- a/doc/src/sgml/hstore.sgml +++ b/doc/src/sgml/hstore.sgml @@ -73,9 +73,7 @@ key => NULL applies before any required quoting or escaping. If you are passing an hstore literal via a parameter, then no additional processing is needed. But if you're passing it as a quoted literal - constant, then any single-quote characters and (depending on the setting of - the standard_conforming_strings configuration parameter) - backslash characters need to be escaped correctly. See + constant, then any single-quote characters need to be escaped correctly. See for more on the handling of string constants. @@ -600,7 +598,7 @@ b Extracts an hstore's keys and values as a set of records. - select * from each('a=>1,b=>2') + SELECT * FROM each('a=>1,b=>2') key | value @@ -799,7 +797,7 @@ UPDATE tab SET h = h || hstore('c', '3'); If multiple keys are to be added or changed in one operation, the concatenation approach is more efficient than subscripting: -UPDATE tab SET h = h || hstore(array['q', 'w'], array['11', '12']); +UPDATE tab SET h = h || hstore(ARRAY['q', 'w'], ARRAY['11', '12']); diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile index 645519095d066..9362c30998e2a 100644 --- a/doc/src/sgml/images/Makefile +++ b/doc/src/sgml/images/Makefile @@ -3,9 +3,15 @@ # see README in this directory about image handling ALL_IMAGES = \ + datachecksums.svg \ genetic-algorithm.svg \ gin.svg \ - pagelayout.svg + pagelayout.svg \ + temporal-entities.svg \ + temporal-references.svg \ + temporal-update.svg \ + temporal-delete.svg \ + temporal-isolation.svg DITAA = ditaa DOT = dot diff --git a/doc/src/sgml/images/README b/doc/src/sgml/images/README index 07c4580255345..93b75485c4464 100644 --- a/doc/src/sgml/images/README +++ b/doc/src/sgml/images/README @@ -13,14 +13,14 @@ involve diffable source files. These tools are acceptable: - Graphviz (https://graphviz.org/) -- Ditaa (http://ditaa.sourceforge.net/) +- Ditaa v0.11.0 or later (https://github.com/stathissideris/ditaa) We use SVG as the format for integrating the image into the ultimate output formats of the documentation, that is, HTML, PDF, and others. Therefore, any tool used needs to be able to produce SVG. -This directory contains makefile rules to build SVG from common input -formats, using some common styling. +This directory contains makefile and meson rules to build SVG from common +input formats, using some common styling. fixup-svg.xsl applies some postprocessing to the SVG files produced by those external tools to address assorted issues. See comments in diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv new file mode 100644 index 0000000000000..dff3ff7340a3a --- /dev/null +++ b/doc/src/sgml/images/datachecksums.gv @@ -0,0 +1,14 @@ +digraph G { + A -> B [label="SELECT pg_enable_data_checksums()"]; + B -> C; + D -> A; + C -> D [label="SELECT pg_disable_data_checksums()"]; + E -> A [label=" --no-data-checksums"]; + E -> C [label=" --data-checksums"]; + + A [label="off"]; + B [label="inprogress-on"]; + C [label="on"]; + D [label="inprogress-off"]; + E [label="initdb"]; +} diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg new file mode 100644 index 0000000000000..8c58f42922e7d --- /dev/null +++ b/doc/src/sgml/images/datachecksums.svg @@ -0,0 +1,81 @@ + + + + + +G + + + +A + +off + + + +B + +inprogress-on + + + +A->B + + +SELECT pg_enable_data_checksums() + + + +C + +on + + + +B->C + + + + + +D + +inprogress-off + + + +C->D + + +SELECT pg_disable_data_checksums() + + + +D->A + + + + + +E + +initdb + + + +E->A + + + --no-data-checksums + + + +E->C + + + --data-checksums + + + diff --git a/doc/src/sgml/images/meson.build b/doc/src/sgml/images/meson.build new file mode 100644 index 0000000000000..220e3eaafb832 --- /dev/null +++ b/doc/src/sgml/images/meson.build @@ -0,0 +1,64 @@ +# doc/src/sgml/images/meson.build +# +# see README in this directory about image handling + +if not xsltproc_bin.found() or not dot.found() or not ditaa.found() + subdir_done() +endif + +image_targets = [] + +fixup_svg_xsl = files('fixup-svg.xsl') + +all_files = [ + 'genetic-algorithm.gv', + 'gin.gv', + 'pagelayout.txt', + 'temporal-entities.txt', + 'temporal-references.txt', + 'temporal-update.txt', + 'temporal-delete.txt', + 'temporal-isolation.txt', +] + +foreach file : all_files + + str_split = file.split('.') + actual_file_name = str_split[0] + extension = str_split[1] + cur_file = files(file) + tmp_name = '@0@.svg.tmp'.format(file) + output_name = '@0@.svg'.format(actual_file_name) + + command = [] + if extension == 'gv' + command = [dot, '-T', 'svg', '-o', '@OUTPUT@', '@INPUT@'] + elif extension == 'txt' + command = [ditaa, '-E', '-S', '--svg', '@INPUT@', '@OUTPUT@'] + else + error('Unknown extension: ".@0@" while generating images'.format(extension)) + endif + + svg_tmp = custom_target(tmp_name, + input: cur_file, + output: tmp_name, + command: command, + ) + + current_svg = custom_target(output_name, + input: svg_tmp, + output: output_name, + command: [xsltproc_bin, + '--nonet', + # Use --novalid to avoid loading SVG DTD if a file specifies it, since + # it might not be available locally, and we don't need it. + '--novalid', + '-o', '@OUTPUT@', + fixup_svg_xsl, + '@INPUT@'] + ) + + image_targets += current_svg +endforeach + +alias_target('images', image_targets) diff --git a/doc/src/sgml/images/temporal-delete.svg b/doc/src/sgml/images/temporal-delete.svg new file mode 100644 index 0000000000000..2d8b1d6ec7b03 --- /dev/null +++ b/doc/src/sgml/images/temporal-delete.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + products + (5, 5.00, + [1 Jan 2020,1 Aug 2021)) + 2020 + 2021 + 2022 + 2023 + products + (5, 12.00, + [1 Sep 2023,1 Mar 2025)) + 2024 + 2025 + products + (5, 8.00, + [1 Mar 2025,)) + 2026 + ... + + diff --git a/doc/src/sgml/images/temporal-delete.txt b/doc/src/sgml/images/temporal-delete.txt new file mode 100644 index 0000000000000..bf79b2207c304 --- /dev/null +++ b/doc/src/sgml/images/temporal-delete.txt @@ -0,0 +1,10 @@ ++----------------------------+ +-------------------------------+--------------------------+ +| cGRE | | cGRE | cGRE | +| products | | products | products | +| (5, 5.00, | | (5, 12.00, | (5, 8.00, | +| [1 Jan 2020,1 Aug 2021)) | | [1 Sep 2023,1 Mar 2025)) | [1 Mar 2025,)) | +| | | | | ++----------------------------+ +-------------------------------+--------------------------+ + +| | | | | | | | +2020 2021 2022 2023 2024 2025 2026 ... diff --git a/doc/src/sgml/images/temporal-entities.svg b/doc/src/sgml/images/temporal-entities.svg new file mode 100644 index 0000000000000..23958c3203c2a --- /dev/null +++ b/doc/src/sgml/images/temporal-entities.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + products + (5, 5.00, [1 Jan 2020,1 Jan 2022)) + 2020 + products + (6, 9.00, [1 Jan 2021,1 Jan 2024)) + 2021 + 2022 + products + (5, 8.00, [1 Jan 2022,)) + 2023 + 2024 + ... + + diff --git a/doc/src/sgml/images/temporal-entities.txt b/doc/src/sgml/images/temporal-entities.txt new file mode 100644 index 0000000000000..0def28e0a5922 --- /dev/null +++ b/doc/src/sgml/images/temporal-entities.txt @@ -0,0 +1,14 @@ ++-------------------------------------+-------------------------------------------------------+ +| cGRE | cGRE | +| products | products | +| (5, 5.00, [1 Jan 2020,1 Jan 2022)) | (5, 8.00, [1 Jan 2022,)) | +| | | ++------------------+------------------+-------------------------------------+-----------------+ + | cGRE | + | products | + | (6, 9.00, [1 Jan 2021,1 Jan 2024)) | + | | + +--------------------------------------------------------+ + +| | | | | | +2020 2021 2022 2023 2024 ... diff --git a/doc/src/sgml/images/temporal-isolation.svg b/doc/src/sgml/images/temporal-isolation.svg new file mode 100644 index 0000000000000..6f88c763800ff --- /dev/null +++ b/doc/src/sgml/images/temporal-isolation.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Session 1 + UPDATE; + waits... + BEGIN; + BEGIN; + Session 2 + UPDATE; + COMMIT; + COMMIT; + ...resumes + rechecks + + diff --git a/doc/src/sgml/images/temporal-isolation.txt b/doc/src/sgml/images/temporal-isolation.txt new file mode 100644 index 0000000000000..870db7b002554 --- /dev/null +++ b/doc/src/sgml/images/temporal-isolation.txt @@ -0,0 +1,44 @@ ++-----------+ +-----------+ +| cGRE | | cYEL | +|Session 1 | |Session 2 | +| | | | ++-----+-----+ +-----+-----+ + | | + v v ++-----+-----+ +-----+-----+ +| cGRE | | cYEL | +| BEGIN; | | BEGIN; | +| | | | ++-----+-----+ +-----+-----+ + | | + v | ++-----+-----+ | +| cGRE | | +| UPDATE; | | +| | | ++-----+-----+ v + | +-----+-----+ + | | cYEL | + | | UPDATE; | + | | waits... | + | | | + | +-----+-----+ + v | ++-----+-----+ | +| cGRE | | +| COMMIT; | | +| | | ++-----------+ v + +-----+-----+ + | cYEL | + | ...resumes| + | rechecks | + | | + +-----+-----+ + | + v + +-----+-----+ + | cYEL | + | COMMIT; | + | | + +-----------+ diff --git a/doc/src/sgml/images/temporal-references.svg b/doc/src/sgml/images/temporal-references.svg new file mode 100644 index 0000000000000..09230c4e4e90b --- /dev/null +++ b/doc/src/sgml/images/temporal-references.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + products + (5, 5.00, [1 Jan 2020,1 Jan 2022)) + 2021 + variants + (8, 5, 'Medium', [1 Jan 2021,1 Jun 2023)) + 2020 + variants + (9, 5, 'XXL', [1 Mar 2022,1 Jun 2024)) + products + (5, 8.00, [1 Jan 2022,)) + 2023 + 2022 + 2024 + ... + + diff --git a/doc/src/sgml/images/temporal-references.txt b/doc/src/sgml/images/temporal-references.txt new file mode 100644 index 0000000000000..57dedc32e0b34 --- /dev/null +++ b/doc/src/sgml/images/temporal-references.txt @@ -0,0 +1,19 @@ ++------------------------------------+------------------------------------------------------+ +| cGRE | cGRE | +| products | products | +| (5, 5.00, [1 Jan 2020,1 Jan 2022)) | (5, 8.00, [1 Jan 2022,)) | +| | | ++------------------+-----------------+----------------------------+-------------------------+ + | cYEL | + | variants | + | (8, 5, 'Medium', [1 Jan 2021,1 Jun 2023)) | + | | + +-----------------------+----------------------+------------------+ + | cYEL | + | variants | + | (9, 5, 'XXL', [1 Mar 2022,1 Jun 2024)) | + | | + +-----------------------------------------+ + +| | | | | | +2020 2021 2022 2023 2024 ... diff --git a/doc/src/sgml/images/temporal-update.svg b/doc/src/sgml/images/temporal-update.svg new file mode 100644 index 0000000000000..6c7c43c8d2266 --- /dev/null +++ b/doc/src/sgml/images/temporal-update.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + products + (5, 5.00, + [1 Jan 2020,1 Jan 2022)) + 2020 + 2021 + products + (5, 8.00, + [1 Jan 2022,1 Sep 2023)) + 2022 + 2023 + products + (5, 12.00, + [1 Sep 2023,1 Mar 2025)) + 2024 + 2025 + products + (5, 8.00, + [1 Mar 2025,)) + 2026 + ... + + diff --git a/doc/src/sgml/images/temporal-update.txt b/doc/src/sgml/images/temporal-update.txt new file mode 100644 index 0000000000000..87a163828103f --- /dev/null +++ b/doc/src/sgml/images/temporal-update.txt @@ -0,0 +1,10 @@ ++-----------------------------------+-----------------------------+----------------------------+------------------------------+ +| cGRE | cGRE | cGRE | cGRE | +| products | products | products | products | +| (5, 5.00, | (5, 8.00, | (5, 12.00, | (5, 8.00, | +| [1 Jan 2020,1 Jan 2022)) | [1 Jan 2022,1 Sep 2023)) | [1 Sep 2023,1 Mar 2025)) | [1 Mar 2025,)) | +| | | | | ++-----------------------------------+-----------------------------+----------------------------+------------------------------+ + +| | | | | | | | +2020 2021 2022 2023 2024 2025 2026 ... diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml index 1aa4741a8eaee..f48da3185307c 100644 --- a/doc/src/sgml/indexam.sgml +++ b/doc/src/sgml/indexam.sgml @@ -70,9 +70,15 @@ single argument of type internal and to return the pseudo-type index_am_handler. The argument is a dummy value that simply serves to prevent handler functions from being called directly from - SQL commands. The result of the function must be a palloc'd struct of - type IndexAmRoutine, which contains everything - that the core code needs to know to make use of the index access method. + SQL commands. + The result of the handler function must be a pointer to a permanently + allocated struct of type IndexAmRoutine, which + contains everything that the core code needs to know to make use of the + index access method. (Typically this struct can be pre-initialized and + marked static const; but if that is inconvenient, + build it in some long-lived context. In any case, repeat calls within + a process should return the same pointer. The core code will treat the + struct as const, and will never free it.) The IndexAmRoutine struct, also called the access method's API struct, includes fields specifying assorted fixed properties of the access method, such as whether it can support @@ -147,7 +153,7 @@ typedef struct IndexAmRoutine ambuild_function ambuild; ambuildempty_function ambuildempty; aminsert_function aminsert; - aminsertcleanup_function aminsertcleanup; + aminsertcleanup_function aminsertcleanup; /* can be NULL */ ambulkdelete_function ambulkdelete; amvacuumcleanup_function amvacuumcleanup; amcanreturn_function amcanreturn; /* can be NULL */ diff --git a/doc/src/sgml/indices.sgml b/doc/src/sgml/indices.sgml index 9c4f76abf0dcd..55f39b0df2f2e 100644 --- a/doc/src/sgml/indices.sgml +++ b/doc/src/sgml/indices.sgml @@ -593,7 +593,7 @@ CREATE INDEX test2_mm_idx ON test2 (major, minor); By default, B-tree indexes store their entries in ascending order with nulls last (table TID is treated as a tiebreaker column among otherwise equal entries). This means that a forward scan of an - index on column x produces output satisfying ORDER BY x + index on column x produces output satisfying ORDER BY x (or more verbosely, ORDER BY x ASC NULLS LAST). The index can also be scanned backward, producing output satisfying ORDER BY x DESC @@ -698,23 +698,23 @@ CREATE INDEX test3_desc_index ON test3 (id DESC NULLS LAST); indexes are best, but sometimes it's better to create separate indexes and rely on the index-combination feature. For example, if your workload includes a mix of queries that sometimes involve only column - x, sometimes only column y, and sometimes both + x, sometimes only column y, and sometimes both columns, you might choose to create two separate indexes on - x and y, relying on index combination to + x and y, relying on index combination to process the queries that use both columns. You could also create a multicolumn index on (x, y). This index would typically be more efficient than index combination for queries involving both columns, but as discussed in , it would be less useful for queries involving only y. Just how useful will depend on how effective the B-tree index skip scan - optimization is; if x has no more than several hundred + optimization is; if x has no more than several hundred distinct values, skip scan will make searches for specific - y values execute reasonably efficiently. A combination + y values execute reasonably efficiently. A combination of a multicolumn index on (x, y) and a separate index on - y might also serve reasonably well. For - queries involving only x, the multicolumn index could be + y might also serve reasonably well. For + queries involving only x, the multicolumn index could be used, though it would be larger and hence slower than an index on - x alone. The last alternative is to create all three + x alone. The last alternative is to create all three indexes, but this is probably only reasonable if the table is searched much more often than it is updated and all three types of query are common. If one of the types of query is much less common than the @@ -949,19 +949,19 @@ WHERE url = '/index.html' AND client_ip = inet '192.168.100.23'; command to create the index would look like this: CREATE INDEX orders_unbilled_index ON orders (order_nr) - WHERE billed is not true; + WHERE billed IS NOT TRUE; A possible query to use this index would be: -SELECT * FROM orders WHERE billed is not true AND order_nr < 10000; +SELECT * FROM orders WHERE billed IS NOT TRUE AND order_nr < 10000; However, the index can also be used in queries that do not involve order_nr at all, e.g.: -SELECT * FROM orders WHERE billed is not true AND amount > 5000.00; +SELECT * FROM orders WHERE billed IS NOT TRUE AND amount > 5000.00; This is not as efficient as a partial index on the amount column would be, since the system has to @@ -1179,9 +1179,9 @@ CREATE INDEX mytable_cat_data ON mytable (category, data); The query must reference only columns stored in the index. For - example, given an index on columns x - and y of a table that also has a - column z, these queries could use index-only scans: + example, given an index on columns x + and y of a table that also has a + column z, these queries could use index-only scans: SELECT x, y FROM tab WHERE x = 'key'; SELECT x FROM tab WHERE x = 'key' AND y < 42; @@ -1262,15 +1262,15 @@ CREATE INDEX tab_x_y ON tab(x) INCLUDE (y); - Because column y is not part of the index's search + Because column y is not part of the index's search key, it does not have to be of a data type that the index can handle; it's merely stored in the index and is not interpreted by the index machinery. Also, if the index is a unique index, that is CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y); - the uniqueness condition applies to just column x, - not to the combination of x and y. + the uniqueness condition applies to just column x, + not to the combination of x and y. (An INCLUDE clause can also be written in UNIQUE and PRIMARY KEY constraints, providing alternative syntax for setting up an index like @@ -1300,7 +1300,7 @@ CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y); CREATE INDEX tab_x_y ON tab(x, y); - even though they had no intention of ever using y as + even though they had no intention of ever using y as part of a WHERE clause. This works fine as long as the extra columns are trailing columns; making them be leading columns is unwise for the reasons explained in . @@ -1340,7 +1340,7 @@ SELECT f(x) FROM tab WHERE f(x) < 1; context f(x), but the planner does not notice that and concludes that an index-only scan is not possible. If an index-only scan seems sufficiently worthwhile, this can be worked around by - adding x as an included column, for example + adding x as an included column, for example CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x); diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml index 19dffe7be6aa7..4be4f1ef1eff8 100644 --- a/doc/src/sgml/information_schema.sgml +++ b/doc/src/sgml/information_schema.sgml @@ -2089,15 +2089,15 @@ Since data types can be defined in a variety of ways in SQL, and PostgreSQL contains additional ways to define data types, their representation in the information schema - can be somewhat difficult. The column data_type + can be somewhat difficult. The column data_type is supposed to identify the underlying built-in type of the column. In PostgreSQL, this means that the type is defined in the system catalog schema pg_catalog. This column might be useful if the application can handle the well-known built-in types specially (for example, format the numeric types differently or use the data in - the precision columns). The columns udt_name, - udt_schema, and udt_catalog + the precision columns). The columns udt_name, + udt_schema, and udt_catalog always identify the underlying data type of the column, even if the column is based on a domain. (Since PostgreSQL treats built-in types like @@ -2107,8 +2107,8 @@ type, because in that case it wouldn't matter if the column is really based on a domain. If the column is based on a domain, the identity of the domain is stored in the columns - domain_name, domain_schema, - and domain_catalog. If you want to pair up + domain_name, domain_schema, + and domain_catalog. If you want to pair up columns with their associated data types and treat domains as separate types, you could write coalesce(domain_name, udt_name), etc. @@ -4171,6 +4171,1098 @@ ORDER BY c.ordinal_position; + + <literal>pg_edge_table_components</literal> + + + The view pg_edge_table_components identifies which + columns are part of the source or destination vertex keys, as well as their + corresponding columns in the vertex tables being linked to, in the edge + tables of property graphs defined in the current database. Only those + property graphs are shown that the current user has access to (by way of + being the owner or having some privilege). + + + + The source and destination vertex links of edge tables are specified in + CREATE PROPERTY GRAPH and default to foreign keys in + certain cases. + + + + <structname>pg_edge_table_components</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + edge_table_alias sql_identifier + + + The element table alias of the edge table being described + + + + + + vertex_table_alias sql_identifier + + + The element table alias of the source or destination vertex table being linked to + + + + + + edge_end character_data + + + Either SOURCE or DESTINATION; + specifies which edge link is being described. + + + + + + edge_table_column_name sql_identifier + + + Name of the column that is part of the source or destination vertex key in this edge table + + + + + + vertex_table_column_name sql_identifier + + + Name of the column that is part of the key in the source or destination + vertex table being linked to + + + + + + ordinal_position cardinal_number + + + Ordinal position of the columns within the key (count starts at 1) + + + + +
+
+ + + <literal>pg_element_table_key_columns</literal> + + + The view pg_element_table_key_columns identifies which + columns are part of the keys of the element tables of property graphs defined + in the current database. Only those property graphs are shown that the + current user has access to (by way of being the owner or having some + privilege). + + + + The key of an element table uniquely identifies the rows in it. It is + either specified using the KEY clause in CREATE + PROPERTY GRAPH or defaults to the primary key. + + + + <structname>pg_element_table_key_columns</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + element_table_alias sql_identifier + + + Element table alias (unique identifier of an element table within a + property graph) + + + + + + column_name sql_identifier + + + Name of the column that is part of the key + + + + + + ordinal_position cardinal_number + + + Ordinal position of the column within the key (count starts at 1) + + + + +
+
+ + + <literal>pg_element_table_labels</literal> + + + The view pg_element_table_labels shows which labels are + defined on the element tables of property graphs defined in the current + database. Only those property graphs are shown that the current user has + access to (by way of being the owner or having some privilege). + + + + <structname>pg_element_table_labels</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + element_table_alias sql_identifier + + + Element table alias (unique identifier of an element table within a + property graph) + + + + + + label_name sql_identifier + + + Name of the label + + + + +
+
+ + + <literal>pg_element_table_properties</literal> + + + The view pg_element_table_properties shows the definitions + of the properties for the element tables of property graphs defined in the + current database. Only those property graphs are shown that the current user + has access to (by way of being the owner or having some privilege). + + + + <structname>pg_element_table_properties</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + element_table_alias sql_identifier + + + Element table alias (unique identifier of an element table within a + property graph) + + + + + + property_name sql_identifier + + + Name of the property + + + + + + property_expression character_data + + + Expression of the property definition for this element table + + + + +
+
+ + + <literal>pg_element_tables</literal> + + + The view pg_element_tables contains information about + the element tables of property graphs defined in the current database. + Only those property graphs are shown that the current user has access to + (by way of being the owner or having some privilege). + + + + <structname>pg_element_tables</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + element_table_alias sql_identifier + + + Element table alias (unique identifier of an element table within a + property graph) + + + + + + element_table_kind character_data + + + The kind of the element table: EDGE or VERTEX + + + + + + table_catalog sql_identifier + + + Name of the database that contains the referenced table (always the current database) + + + + + + table_schema sql_identifier + + + Name of the schema that contains the referenced table + + + + + + table_name sql_identifier + + + Name of the table being referenced by the element table definition + + + + + + element_table_definition character_data + + + Applies to a feature not available in PostgreSQL + + + + +
+
+ + + <literal>pg_label_properties</literal> + + + The view pg_label_properties shows which properties are + defined on labels defined in property graphs defined in the current + database. Only those property graphs are shown that the current user has + access to (by way of being the owner or having some privilege). + + + + <structname>pg_label_properties</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + label_name sql_identifier + + + Name of the label + + + + + + property_name sql_identifier + + + Name of the property + + + + +
+
+ + + <literal>pg_labels</literal> + + + The view pg_labels contains all the labels defined in + property graphs defined in the current database. Only those property + graphs are shown that the current user has access to (by way of being the + owner or having some privilege). + + + + <structname>pg_labels</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + label_name sql_identifier + + + Name of the label + + + + +
+
+ + + <literal>pg_property_data_types</literal> + + + The view pg_property_data_types shows the data types of + the properties in property graphs defined in the current database. Only + those property graphs are shown that the current user has access to (by way + of being the owner or having some privilege). + + + + <structname>pg_property_data_types</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + property_name sql_identifier + + + Name of the property + + + + + + data_type character_data + + + Data type of the property, if it is a built-in type, or + ARRAY if it is some array (in that case, see the + view element_types), else + USER-DEFINED (in that case, the type is identified + in attribute_udt_name and associated columns). + + + + + + character_maximum_length cardinal_number + + + If data_type identifies a character or bit + string type, the declared maximum length; null for all other + data types or if no maximum length was declared. + + + + + + character_octet_length cardinal_number + + + If data_type identifies a character type, + the maximum possible length in octets (bytes) of a datum; null + for all other data types. The maximum octet length depends on + the declared character maximum length (see above) and the + server encoding. + + + + + + character_set_catalog sql_identifier + + + Applies to a feature not available in PostgreSQL + + + + + + character_set_schema sql_identifier + + + Applies to a feature not available in PostgreSQL + + + + + + character_set_name sql_identifier + + + Applies to a feature not available in PostgreSQL + + + + + + collation_catalog sql_identifier + + + Name of the database containing the collation of the property (always + the current database), null if default or the data type of the + property is not collatable + + + + + + collation_schema sql_identifier + + + Name of the schema containing the collation of the property, null if + default or the data type of the property is not collatable + + + + + + collation_name sql_identifier + + + Name of the collation of the property, null if default or the data type + of the property is not collatable + + + + + + numeric_precision cardinal_number + + + If data_type identifies a numeric type, this + column contains the (declared or implicit) precision of the + type for this attribute. The precision indicates the number of + significant digits. It can be expressed in decimal (base 10) + or binary (base 2) terms, as specified in the column + numeric_precision_radix. For all other data + types, this column is null. + + + + + + numeric_precision_radix cardinal_number + + + If data_type identifies a numeric type, this + column indicates in which base the values in the columns + numeric_precision and + numeric_scale are expressed. The value is + either 2 or 10. For all other data types, this column is null. + + + + + + numeric_scale cardinal_number + + + If data_type identifies an exact numeric + type, this column contains the (declared or implicit) scale of + the type for this attribute. The scale indicates the number of + significant digits to the right of the decimal point. It can + be expressed in decimal (base 10) or binary (base 2) terms, as + specified in the column + numeric_precision_radix. For all other data + types, this column is null. + + + + + + datetime_precision cardinal_number + + + If data_type identifies a date, time, + timestamp, or interval type, this column contains the (declared + or implicit) fractional seconds precision of the type for this + attribute, that is, the number of decimal digits maintained + following the decimal point in the seconds value. For all + other data types, this column is null. + + + + + + interval_type character_data + + + If data_type identifies an interval type, + this column contains the specification which fields the + intervals include for this attribute, e.g., YEAR TO + MONTH, DAY TO SECOND, etc. If no + field restrictions were specified (that is, the interval + accepts all fields), and for all other data types, this field + is null. + + + + + + interval_precision cardinal_number + + + Applies to a feature not available + in PostgreSQL + (see datetime_precision for the fractional + seconds precision of interval type properties) + + + + + + user_defined_type_catalog sql_identifier + + + Name of the database that the property data type is defined in + (always the current database) + + + + + + user_defined_type_schema sql_identifier + + + Name of the schema that the property data type is defined in + + + + + + user_defined_type_name sql_identifier + + + Name of the property data type + + + + + + scope_catalog sql_identifier + + + Applies to a feature not available in PostgreSQL + + + + + + scope_schema sql_identifier + + + Applies to a feature not available in PostgreSQL + + + + + + scope_name sql_identifier + + + Applies to a feature not available in PostgreSQL + + + + + + maximum_cardinality cardinal_number + + + Always null, because arrays always have unlimited maximum cardinality in PostgreSQL + + + + + + dtd_identifier sql_identifier + + + An identifier of the data type descriptor of the property, unique + among the data type descriptors pertaining to the property graph. This + is mainly useful for joining with other instances of such + identifiers. (The specific format of the identifier is not + defined and not guaranteed to remain the same in future + versions.) + + + + +
+
+ + + <literal>pg_property_graph_privileges</literal> + + + The view pg_property_graph_privileges identifies all + privileges granted on property graphs to a currently enabled role or by a + currently enabled role. There is one row for each combination of property + graph, grantor, and grantee. + + + + <structname>pg_property_graph_privileges</structname> Columns + + + + + Column Type + + + Description + + + + + + + + grantor sql_identifier + + + Name of the role that granted the privilege + + + + + + grantee sql_identifier + + + Name of the role that the privilege was granted to + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + + + privilege_type character_data + + + Type of the privilege: SELECT is the only privilege + type applicable to property graphs. + + + + + + is_grantable yes_or_no + + + YES if the privilege is grantable, NO if not + + + + +
+
+ + + <literal>property_graphs</literal> + + + The view property_graphs contains all property graphs + defined in the current database. Only those property graphs are shown that + the current user has access to (by way of being the owner or having some + privilege). + + + + <structname>property_graphs</structname> Columns + + + + + Column Type + + + Description + + + + + + + + property_graph_catalog sql_identifier + + + Name of the database that contains the property graph (always the current database) + + + + + + property_graph_schema sql_identifier + + + Name of the schema that contains the property graph + + + + + + property_graph_name sql_identifier + + + Name of the property graph + + + + +
+
+ <literal>referential_constraints</literal> @@ -6376,7 +7468,7 @@ ORDER BY c.ordinal_position; the sequence data type (see above). The precision indicates the number of significant digits. It can be expressed in decimal (base 10) or binary (base 2) terms, as specified in the - column numeric_precision_radix. + column numeric_precision_radix.
@@ -6386,8 +7478,8 @@ ORDER BY c.ordinal_position;
This column indicates in which base the values in the columns - numeric_precision and - numeric_scale are expressed. The value is + numeric_precision and + numeric_scale are expressed. The value is either 2 or 10. @@ -6402,7 +7494,7 @@ ORDER BY c.ordinal_position; of significant digits to the right of the decimal point. It can be expressed in decimal (base 10) or binary (base 2) terms, as specified in the column - numeric_precision_radix. + numeric_precision_radix. @@ -6461,10 +7553,10 @@ ORDER BY c.ordinal_position; - <literal>sql_features</literal> + <structname>sql_features</structname> - The table sql_features contains information + The table sql_features contains information about which formal features defined in the SQL standard are supported by PostgreSQL. This is the same information that is presented in . @@ -6556,10 +7648,10 @@ ORDER BY c.ordinal_position; - <literal>sql_implementation_info</literal> + <structname>sql_implementation_info</structname> - The table sql_implementation_info contains + The table sql_implementation_info contains information about various aspects that are left implementation-defined by the SQL standard. This information is primarily intended for use in the context of the ODBC interface; @@ -6638,10 +7730,10 @@ ORDER BY c.ordinal_position; - <literal>sql_parts</literal> + <structname>sql_parts</structname> - The table sql_parts contains information about + The table sql_parts contains information about which of the several parts of the SQL standard are supported by PostgreSQL. @@ -6714,10 +7806,10 @@ ORDER BY c.ordinal_position; - <literal>sql_sizing</literal> + <structname>sql_sizing</structname> - The table sql_sizing contains information about + The table sql_sizing contains information about various size limits and maximum values in PostgreSQL. This information is primarily intended for use in the context of the ODBC interface; @@ -7843,7 +8935,7 @@ ORDER BY c.ordinal_position; in PostgreSQL) and distinct types (not implemented in PostgreSQL). To be future-proof, use the - column user_defined_type_category to + column user_defined_type_category to differentiate between these. Other user-defined types such as base types and enums, which are PostgreSQL extensions, are not shown here. For domains, diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml index de19f3ad92952..b345a1056740a 100644 --- a/doc/src/sgml/installation.sgml +++ b/doc/src/sgml/installation.sgml @@ -65,16 +65,15 @@ - The minimum required version of Meson is 0.54. + The minimum required version of Meson is 0.57.2. - You need an ISO/ANSI C compiler (at least - C99-compliant). Recent - versions of GCC are recommended, but - PostgreSQL is known to build using a wide variety + You need a C compiler that supports at least C11. Recent versions of + GCC are recommended, but + PostgreSQL is known to build using a variety of compilers from different vendors. @@ -1677,10 +1676,6 @@ build-postgresql: using the GCC compiler: ./configure CC='gcc -m64' --enable-dtrace DTRACEFLAGS='-64' ... - - Using Sun's compiler: - -./configure CC='/opt/SUNWspro/bin/cc -xtarget=native64' --enable-dtrace DTRACEFLAGS='-64' ... @@ -2812,6 +2807,17 @@ ninja install + + + + + This option selects whether both shared and static libraries are built + (the default), or only shared libraries. (The third variant of only + building static libraries is currently not supported.) + + + + @@ -3175,7 +3181,7 @@ ninja install Enable additional test suites, which are not run by default because they are not secure to run on a multiuser system, require special - software to run, or are resource intensive. The argument is a + software to run, or are resource-intensive. The argument is a whitespace-separated list of tests to enable. See for details. If the PG_TEST_EXTRA environment variable is set when the @@ -3444,7 +3450,7 @@ export MANPATH PostgreSQL can be expected to work on current versions of these operating systems: Linux, Windows, - FreeBSD, OpenBSD, NetBSD, DragonFlyBSD, macOS, Solaris, and illumos. + FreeBSD, OpenBSD, NetBSD, DragonFlyBSD, macOS, AIX, Solaris, and illumos. Other Unix-like systems may also work but are not currently being tested. In most cases, all CPU architectures supported by a given operating system will work. Look in @@ -3466,7 +3472,7 @@ export MANPATH Historical versions of PostgreSQL or POSTGRES also ran on CPU architectures including Alpha, Itanium, M32R, M68K, M88K, NS32K, PA-RISC, SuperH, and VAX, - and operating systems including 4.3BSD, AIX, BEOS, + and operating systems including 4.3BSD, BEOS, BSD/OS, DG/UX, Dynix, HP-UX, IRIX, NeXTSTEP, QNX, SCO, SINIX, Sprite, SunOS, Tru64 UNIX, and ULTRIX. @@ -3489,6 +3495,42 @@ export MANPATH installation issues. + + AIX + + + AIX + installation on + + + + You must use GCC + to build PostgreSQL + on AIX. + The native IBM compiler xlc is not supported. + + + + Also, only 64-bit builds are supported. While the make-based build + system will automatically create 64-bit executables and libraries, + the meson build system requires you to + set OBJECT_MODE before building: + +export OBJECT_MODE=64 +meson setup ... + + Failure to do that will usually manifest as complaints + from ar about files having the wrong + object file mode. + + + + AIX versions before 7.2 are no longer + tested nor supported by the PostgreSQL + community. + + + Cygwin @@ -3714,24 +3756,13 @@ xcrun --show-sdk-path Required Tools - You can build with either GCC or Sun's compiler suite. For - better code optimization, Sun's compiler is strongly recommended - on the SPARC architecture. If - you are using Sun's compiler, be careful not to select - /usr/ucb/cc; - use /opt/SUNWspro/bin/cc. + Only GCC is supported as the compiler. Sun's compiler suite is no longer + supported. - You can download Sun Studio - from . - Many GNU tools are integrated into Solaris 10, or they are - present on the Solaris companion CD. If you need packages for - older versions of Solaris, you can find these tools - at . - If you prefer - sources, look - at . + Many additional dependencies can be installed via the package management + system. @@ -3754,27 +3785,6 @@ configure ... LDFLAGS="-R /usr/sfw/lib:/opt/sfw/lib:/usr/local/lib" - - Compiling for Optimal Performance - - - On the SPARC architecture, Sun Studio is strongly recommended for - compilation. Try using the optimization - flag to generate significantly faster binaries. Do not use any - flags that modify behavior of floating-point operations - and errno processing (e.g., - ). - - - - If you do not have a reason to use 64-bit binaries on SPARC, - prefer the 32-bit version. The 64-bit operations are slower and - 64-bit binaries are slower than the 32-bit variants. On the - other hand, 32-bit code on the AMD64 CPU family is not native, - so 32-bit code is significantly slower on that CPU family. - - - Using DTrace for Tracing PostgreSQL @@ -3782,22 +3792,6 @@ configure ... LDFLAGS="-R /usr/sfw/lib:/opt/sfw/lib:/usr/local/lib" Yes, using DTrace is possible. See for further information. - - - If you see the linking of the postgres executable abort with an - error message like: - -Undefined first referenced - symbol in file -AbortTransaction utils/probes.o -CommitTransaction utils/probes.o -ld: fatal: Symbol referencing errors. No output written to postgres -collect2: ld returned 1 exit status -make: *** [postgres] Error 1 - - your DTrace installation is too old to handle probes in static - functions. You need Solaris 10u4 or newer to use DTrace. - @@ -3847,17 +3841,13 @@ make: *** [postgres] Error 1 Both 32-bit and 64-bit builds are possible with the Microsoft Compiler suite. 32-bit PostgreSQL builds are possible with - Visual Studio 2015 to + Visual Studio 2019 to Visual Studio 2022, as well as standalone Windows SDK releases 10 and above. 64-bit PostgreSQL builds are supported with Microsoft Windows SDK version 10 and above or - Visual Studio 2015 and above. + Visual Studio 2019 and above. + vars of several JSON processing functions + (see ), or by + using the SQL/JSON PASSING clause as described + in . diff --git a/doc/src/sgml/keywords/sql2023-16-nonreserved.txt b/doc/src/sgml/keywords/sql2023-16-nonreserved.txt new file mode 100644 index 0000000000000..39756c6067d93 --- /dev/null +++ b/doc/src/sgml/keywords/sql2023-16-nonreserved.txt @@ -0,0 +1,27 @@ +ACYCLIC +BINDINGS +BOUND +DESTINATION +DIFFERENT +DIRECTED +EDGE +EDGES +ELEMENTS +LABEL +LABELED +NODE +PATHS +PROPERTIES +PROPERTY +PROPERTY_GRAPH_CATALOG +PROPERTY_GRAPH_NAME +PROPERTY_GRAPH_SCHEMA +RELATIONSHIP +RELATIONSHIPS +SHORTEST +SINGLETONS +STEP +TABLES +TRAIL +VERTEX +WALK diff --git a/doc/src/sgml/keywords/sql2023-16-reserved.txt b/doc/src/sgml/keywords/sql2023-16-reserved.txt new file mode 100644 index 0000000000000..3bdd7e2b27277 --- /dev/null +++ b/doc/src/sgml/keywords/sql2023-16-reserved.txt @@ -0,0 +1,12 @@ +ALL_DIFFERENT +BINDING_COUNT +ELEMENT_ID +ELEMENT_NUMBER +EXPORT +GRAPH +GRAPH_TABLE +MATCHNUM +PATH_LENGTH +PATH_NAME +PROPERTY_EXISTS +SAME diff --git a/doc/src/sgml/legal.sgml b/doc/src/sgml/legal.sgml index 75c1309cf7326..742c0146f7fe0 100644 --- a/doc/src/sgml/legal.sgml +++ b/doc/src/sgml/legal.sgml @@ -1,9 +1,9 @@ -2025 +2026 - 1996–2025 + 1996–2026 The PostgreSQL Global Development Group @@ -16,7 +16,7 @@ - Portions Copyright © 1996-2025, PostgreSQL Global Development Group + Portions Copyright © 1996-2026, PostgreSQL Global Development Group Portions Copyright © 1994, The Regents of the University of California diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 695fe958c3ed3..0a19c2b553b0a 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -2168,6 +2168,24 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + ssl_max_protocol_version + + + This parameter specifies the maximum SSL/TLS protocol version to allow + for the connection. Valid values are TLSv1, + TLSv1.1, TLSv1.2 and + TLSv1.3. The supported protocols depend on the + version of OpenSSL used, older versions + not supporting the most modern protocol versions. If not set, this + parameter is ignored and the connection will use the maximum bound + defined by the backend, if set. Setting the maximum protocol version + is mainly useful for testing or if some component has issues working + with a newer protocol. + + + + min_protocol_version @@ -2193,6 +2211,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname max_protocol_version + + + During the PostgreSQL 19 beta period, libpq connections that do not + specify a max_protocol_version will "grease" the + handshake by sending unsupported startup parameters, including version + 3.9999, in order to identify software that does not + correctly negotiate the connection. This replaces the default behavior + described below. + + + If you know that a server doesn't properly implement protocol version + negotiation, you can set max_protocol_version=3.0 to + revert to the standard behavior (preferably after notifying the server's + maintainers that their software needs to be fixed). + + + Specifies the protocol version to request from the server. The default is to use version 3.0 of the @@ -2202,7 +2237,7 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname server does not support the protocol version requested by the client, the connection is automatically downgraded to a lower minor protocol version that the server supports. After the connection attempt has - completed you can use to + completed you can use to find out which exact protocol version was negotiated. @@ -2216,24 +2251,6 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname - - ssl_max_protocol_version - - - This parameter specifies the maximum SSL/TLS protocol version to allow - for the connection. Valid values are TLSv1, - TLSv1.1, TLSv1.2 and - TLSv1.3. The supported protocols depend on the - version of OpenSSL used, older versions - not supporting the most modern protocol versions. If not set, this - parameter is ignored and the connection will use the maximum bound - defined by the backend, if set. Setting the maximum protocol version - is mainly useful for testing or if some component has issues working - with a newer protocol. - - - - krbsrvname @@ -2320,6 +2337,19 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + servicefile + + + This option specifies the name of the per-user connection service file + (see ). + Defaults to ~/.pg_service.conf, or + %APPDATA%\postgresql\.pg_service.conf on + Microsoft Windows. + + + + target_session_attrs @@ -2555,6 +2585,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + oauth_ca_file + + + The name of a file containing one or more SSL certificate authority + (CA) certificates, which will be used to verify the + identity of the authorization server and its endpoints. By default, the + Curl system certificate bundle is used. + + + This parameter does not affect verification of the + PostgreSQL server certificate; see + instead. + + + + @@ -2740,26 +2787,6 @@ char *PQport(const PGconn *conn); - - PQservicePQservice - - - - Returns the service of the active connection. - - -char *PQservice(const PGconn *conn); - - - - - returns NULL if the - conn argument is NULL. - Otherwise, if there was no service provided, it returns an empty string. - - - - PQttyPQtty @@ -2896,18 +2923,13 @@ const char *PQparameterStatus(const PGconn *conn, const char *paramName); before 16; search_path was not reported by releases before 18.) Note that - server_version, - server_encoding and - integer_datetimes + server_version and + server_encoding cannot change after startup. - - - - If no value for standard_conforming_strings is reported, - applications can assume it is off, that is, backslashes - are treated as escapes in string literals. Also, the presence of - this parameter can be taken as an indication that the escape string - syntax (E'...') is accepted. + Also, integer_datetimes is + always on in releases 9.5 and later, + and standard_conforming_strings is + always on in releases 19 and later. @@ -4515,7 +4537,7 @@ Oid PQftable(const PGresult *res, InvalidOid is returned if the column number is out of range, or if the specified column is not a simple reference to a table column. - You can query the system table pg_class to determine + You can query the system table pg_class to determine exactly which table is referenced. @@ -4585,7 +4607,7 @@ Oid PQftype(const PGresult *res, - You can query the system table pg_type to + You can query the system table pg_type to obtain the names and properties of the various data types. The OIDs of the built-in data types are defined in the file catalog/pg_type_d.h @@ -9160,12 +9182,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) PGSERVICEFILE - PGSERVICEFILE specifies the name of the per-user - connection service file - (see ). - Defaults to ~/.pg_service.conf, or - %APPDATA%\postgresql\.pg_service.conf on - Microsoft Windows. + PGSERVICEFILE behaves the same as the + connection parameter. @@ -9421,6 +9439,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) linkend="libpq-connect-max-protocol-version"/> connection parameter. + + + + + PGOAUTHCAFILE + + PGOAUTHCAFILE behaves the same as the connection parameter. + + @@ -9596,7 +9624,8 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) On Microsoft Windows, it is named %APPDATA%\postgresql\.pg_service.conf (where %APPDATA% refers to the Application Data subdirectory - in the user's profile). A different file name can be specified by + in the user's profile). A different file name can be specified using the + servicefile key word in a libpq connection string or by setting the environment variable PGSERVICEFILE. The system-wide file is named pg_service.conf. By default it is sought in the etc directory @@ -10360,6 +10389,7 @@ PQauthDataHook_type PQgetAuthDataHook(void); PQAUTHDATA_PROMPT_OAUTH_DEVICE + Available in PostgreSQL 18 and later. Replaces the default user prompt during the builtin device authorization client flow. data points to @@ -10412,6 +10442,7 @@ typedef struct _PGpromptOAuthDevice PQAUTHDATA_OAUTH_BEARER_TOKEN + Available in PostgreSQL 18 and later. Adds a custom implementation of a flow, replacing the builtin flow if it is installed. @@ -10419,6 +10450,13 @@ typedef struct _PGpromptOAuthDevice user/issuer/scope combination, if one is available without blocking, or else set up an asynchronous callback to retrieve one. + + + For PostgreSQL releases 19 and later, + applications should prefer + PQAUTHDATA_OAUTH_BEARER_TOKEN_V2. + + data points to an instance of PGoauthBearerRequest, which should be filled in @@ -10432,10 +10470,14 @@ typedef struct PGoauthBearerRequest /* Hook outputs */ - /* Callback implementing a custom asynchronous OAuth flow. */ + /* + * Callback implementing a custom asynchronous OAuth flow. The signature is + * platform-dependent: PQ_SOCKTYPE is SOCKET on Windows, and int everywhere + * else. + */ PostgresPollingStatusType (*async) (PGconn *conn, struct PGoauthBearerRequest *request, - SOCKTYPE *altsock); + PQ_SOCKTYPE *altsock); /* Callback to clean up custom allocations. */ void (*cleanup) (PGconn *conn, struct PGoauthBearerRequest *request); @@ -10492,7 +10534,7 @@ typedef struct PGoauthBearerRequest hook. When the callback cannot make further progress without blocking, it should return either PGRES_POLLING_READING or PGRES_POLLING_WRITING after setting - *pgsocket to the file descriptor that will be marked + *altsock to the file descriptor that will be marked ready to read/write when progress can be made again. (This descriptor is then provided to the top-level polling loop via PQsocket().) Return PGRES_POLLING_OK @@ -10510,6 +10552,81 @@ typedef struct PGoauthBearerRequest + + + + PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 + + PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 + PQAUTHDATA_OAUTH_BEARER_TOKEN + + + + Available in PostgreSQL 19 and later. + + Provides all the functionality of + PQAUTHDATA_OAUTH_BEARER_TOKEN, + as well as the ability to set custom error messages and retrieve the + OAuth issuer ID that the client has trusted. + + + data points to an instance + of PGoauthBearerRequestV2: + +typedef struct +{ + PGoauthBearerRequest v1; /* see the PGoauthBearerRequest struct, above */ + + /* Hook inputs (constant across all calls) */ + const char *issuer; /* the issuer identifier (RFC 9207) in use */ + + /* Hook outputs */ + const char *error; /* hook-defined error message */ +} PGoauthBearerRequestV2; + + + + Applications must first use the v1 struct + member to implement the base API, as described + above. + libpq additionally guarantees that the + request pointer passed to the + v1.async and v1.cleanup + callbacks may be safely cast to (PGoauthBearerRequestV2 *), + to make use of the additional members described below. + + + + Casting to (PGoauthBearerRequestV2 *) is + only safe when the hook type is + PQAUTHDATA_OAUTH_BEARER_TOKEN_V2. Applications may + crash or misbehave if a hook implementation attempts to access v2 + members when handling a v1 (PQAUTHDATA_OAUTH_BEARER_TOKEN) + hook request. + + + + In addition to the functionality of the version 1 API, the v2 struct + provides an additional input and output for the hook: + + + issuer contains the issuer identifier, as + defined in RFC 9207, + that is in use for the current connection. This identifier is + derived from . + To avoid mix-up attacks, custom flows should ensure that any discovery + metadata provided by the authorization server matches this issuer ID. + + + error may be set to point to a custom + error message when a flow fails. The message will be included as part + of . Hooks must free any error + message allocations during the v1.cleanup + callback. + + + + @@ -10519,41 +10636,111 @@ typedef struct PGoauthBearerRequest Debugging and Developer Settings - A "dangerous debugging mode" may be enabled by setting the environment - variable PGOAUTHDEBUG=UNSAFE. This functionality is provided - for ease of local development and testing only. It does several things that - you will not want a production system to do: + While developing against a local authorization server, it may be helpful to + make use of the connection + parameter (or the equivalent PGOAUTHCAFILE environment + variable) in the client application. + - - - - permits the use of unencrypted HTTP during the OAuth provider exchange - - - - - allows the system's trusted CA list to be completely replaced using the - PGOAUTHCAFILE environment variable - - - - - prints HTTP traffic (containing several critical secrets) to standard - error during the OAuth flow - - - - - permits the use of zero-second retry intervals, which can cause the - client to busy-loop and pointlessly consume CPU - - - + + Debug features may be enabled by setting the PGOAUTHDEBUG + environment variable. This functionality is provided for ease of local + development and testing. The variable accepts a comma-separated list of + debug options: + + +PGOAUTHDEBUG=option1,option2,... for safe options only +PGOAUTHDEBUG=UNSAFE:option1,option2,... when using unsafe options +PGOAUTHDEBUG=UNSAFE legacy format; enables all options + + + + + Available debug options: + + + + http (unsafe) + + + Permits the use of unencrypted HTTP during the OAuth provider exchange. + This allows OAuth credentials to be transmitted over unencrypted + connections, which is extremely dangerous and should only be used for + local testing. + + + + + + trace (unsafe) + + + Prints HTTP traffic to standard error during the OAuth flow. This output + contains critical secrets including bearer tokens, client secrets, access + tokens, and authorization codes. Never share this output with third + parties. + + + + + + dos-endpoint (unsafe) + + + Permits the use of zero-second retry intervals instead of the normal + minimum of one second. This speeds up tests, but in normal operation it + will cause the client to busy-loop, consuming CPU and network resources. + + + + + + call-count (safe) + + + Prints the total number of calls to the flow plugin to standard error + when the OAuth flow completes. This helps developers debug the async + callback behavior. + + + + + + plugin-errors (safe) + + + Prints plugin loading errors to standard error. This helps developers + and package maintainers debug issues when the OAuth plugin fails to load. + + + + + + + Unsafe options (http, trace, + dos-endpoint) require the UNSAFE: prefix. + If unsafe options are specified without this prefix, or if an option name is + unrecognized, a warning is printed to standard error and that option is + ignored. Other valid options in the list continue to work. Safe options + (call-count, plugin-errors) can be + used without the prefix. + + + + Examples: + +PGOAUTHDEBUG=call-count safe options only +PGOAUTHDEBUG=UNSAFE:http,trace enable HTTP and traffic logging +PGOAUTHDEBUG=UNSAFE:http,call-count mix of unsafe and safe + + + - Do not share the output of the OAuth flow traffic with third parties. It - contains secrets that can be used to attack your clients and servers. + Never use unsafe debug options in production environments. They expose + secrets and behaviors that can be used to attack your clients and servers. + Do not share trace output with third parties. @@ -10629,7 +10816,11 @@ int PQisthreadsafe(); Kerberos calls because Kerberos functions are not thread-safe. See function PQregisterThreadLock in the libpq source code for a way to do cooperative - locking between libpq and your application. + locking between libpq and your application. (Note + that it is only safe to call PQregisterThreadLock when + there are no open connections.) Clients may retrieve the current locking + callback with PQgetThreadLock; if no custom callback + has been registered, a default is used. @@ -10871,7 +11062,7 @@ main(int argc, char **argv) /* * Our test case here involves using a cursor, for which we must be inside * a transaction block. We could do the whole thing with a single - * PQexec() of "select * from pg_database", but that's too trivial to make + * PQexec() of "SELECT * FROM pg_database", but that's too trivial to make * a good example. */ @@ -10888,7 +11079,7 @@ main(int argc, char **argv) /* * Fetch rows from pg_database, the system catalog of databases */ - res = PQexec(conn, "DECLARE myportal CURSOR FOR select * from pg_database"); + res = PQexec(conn, "DECLARE myportal CURSOR FOR SELECT * FROM pg_database"); if (PQresultStatus(res) != PGRES_COMMAND_OK) { fprintf(stderr, "DECLARE CURSOR failed: %s", PQerrorMessage(conn)); @@ -11114,7 +11305,6 @@ main(int argc, char **argv) * * CREATE SCHEMA testlibpq3; * SET search_path = testlibpq3; - * SET standard_conforming_strings = ON; * CREATE TABLE test1 (i int4, t text, b bytea); * INSERT INTO test1 values (1, 'joe''s place', '\000\001\002\003\004'); * INSERT INTO test1 values (2, 'ho there', '\004\003\002\001\000'); diff --git a/doc/src/sgml/lobj.sgml b/doc/src/sgml/lobj.sgml index 79731c6553f31..623c0da2c7eed 100644 --- a/doc/src/sgml/lobj.sgml +++ b/doc/src/sgml/lobj.sgml @@ -725,7 +725,7 @@ SELECT lo_export(image.raster, '/tmp/motd') FROM image * testlo.c * test using large objects with libpq * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 686dd441d0223..598e23ad4f5e6 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -6,7 +6,7 @@ Logical replication is a method of replicating data objects and their changes, based upon their replication identity (usually a primary key). We - use the term logical in contrast to physical replication, which uses exact + use the term logical replication in contrast to physical replication, which uses exact block addresses and byte-by-byte replication. PostgreSQL supports both mechanisms concurrently, see . Logical replication allows fine-grained control over both data replication and @@ -41,27 +41,27 @@ Sending incremental changes in a single database or a subset of a - database to subscribers as they occur. + database to subscribers as they occur - Firing triggers for individual changes as they arrive on the - subscriber. + Sending a subset of the database to multiple databases (i.e., + broadcast) - Consolidating multiple databases into a single one (for example for - analytical purposes). + Consolidating multiple databases into a single one (e.g., for + analytics). - Replicating between different major versions of PostgreSQL. + Replicating between different major versions of PostgreSQL @@ -80,7 +80,8 @@ - Sharing a subset of the database between multiple databases. + Firing triggers for individual changes as they arrive on the + subscriber. @@ -102,16 +103,24 @@ A publication can be defined on any physical replication primary. The node where a publication is defined is referred to as publisher. A publication is a set of changes - generated from a table or a group of tables, and might also be described as - a change set or replication set. Each publication exists in only one database. + generated from a table, a group of tables or the current state of all + sequences, and might also be described as a change set or replication set. + Each publication exists in only one database. Publications are different from schemas and do not affect how the table is accessed. Each table can be added to multiple publications if needed. - Publications may currently only contain tables and all tables in schema. - Objects must be added explicitly, except when a publication is created for - ALL TABLES. + Publications may currently only contain tables or sequences. Objects must be + added explicitly, except when a publication is created using + FOR TABLES IN SCHEMA, FOR ALL TABLES, + or FOR ALL SEQUENCES. Unlike tables, sequences can be + synchronized at any time. For more information, see + . When a publication is + created with FOR ALL TABLES, a table or set of tables can + be explicitly excluded from publication using the + EXCEPT + clause. @@ -220,8 +229,10 @@ - Each subscription will receive changes via one replication slot (see - ). Additional replication + By default a new subscription creates a logical replication slot on + the publisher and then uses this slot to track relevant transaction + activity and preserve necessary WAL (see ). Additional replication slots may be required for the initial data synchronization of pre-existing table data and those will be dropped at the end of data synchronization. @@ -283,10 +294,10 @@ - Replication Slot Management + Logical Replication Slot Management - As mentioned earlier, each (active) subscription receives changes from a + As mentioned earlier, each (active) subscription uses a logical replication slot on the remote (publishing) side. @@ -298,7 +309,7 @@ Table relid, system identifier sysid) - Normally, the remote replication slot is created automatically when the + Normally, the remote logical replication slot is created automatically when the subscription is created using CREATE SUBSCRIPTION and it is dropped automatically when the subscription is dropped using @@ -437,7 +448,7 @@ Furthermore, because the initial data copy ignores the publish operation, and because publication pub3a has no row filter, - it means the copied table t3 contains all rows even when + it means the copied table t3 contains all rows even when they do not match the row filter of publication pub3b. /* sub # */ SELECT * FROM t3; @@ -533,17 +544,17 @@ - Examples: Deferred Replication Slot Creation + Examples: Deferred Logical Replication Slot Creation There are some cases (e.g. ) where, if the - remote replication slot was not created automatically, the user must create + remote logical replication slot was not created automatically, the user must create it manually before the subscription can be activated. The steps to create the slot and activate the subscription are shown in the following examples. These examples specify the standard logical decoding output plugin - (pgoutput), which is what the built-in logical - replication uses. + (), + which is what the built-in logical replication uses. First, create a publication for the examples to use. @@ -575,8 +586,8 @@ HINT: To initiate replication, you must manually create the replication slot, e /* pub # */ SELECT * FROM pg_create_logical_replication_slot('sub1', 'pgoutput'); slot_name | lsn ------------+----------- - sub1 | 0/19404D0 +-----------+------------ + sub1 | 0/019404D0 (1 row) @@ -617,8 +628,8 @@ HINT: To initiate replication, you must manually create the replication slot, e /* pub # */ SELECT * FROM pg_create_logical_replication_slot('myslot', 'pgoutput'); slot_name | lsn ------------+----------- - myslot | 0/19059A0 +-----------+------------ + myslot | 0/019059A0 (1 row) @@ -655,8 +666,8 @@ HINT: To initiate replication, you must manually create the replication slot, e /* pub # */ SELECT * FROM pg_create_logical_replication_slot('myslot', 'pgoutput'); slot_name | lsn ------------+----------- - myslot | 0/1905930 +-----------+------------ + myslot | 0/01905930 (1 row) @@ -709,8 +720,8 @@ HINT: To initiate replication, you must manually create the replication slot, e - To confirm that the standby server is indeed ready for failover, follow these - steps to verify that all necessary logical replication slots have been + To confirm that the standby server is indeed ready for failover for a given subscriber, follow these + steps to verify that all the logical replication slots required by that subscriber have been synchronized to the standby server: @@ -764,7 +775,7 @@ HINT: To initiate replication, you must manually create the replication slot, e Check that the logical replication slots identified above exist on the standby server and are ready for failover. -/* standby # */ SELECT slot_name, (synced AND NOT temporary AND NOT conflicting) AS failover_ready +/* standby # */ SELECT slot_name, (synced AND NOT temporary AND invalidation_reason IS NULL) AS failover_ready FROM pg_replication_slots WHERE slot_name IN ('sub1','sub2','sub3', 'pg_16394_sync_16385_7394666715149055164'); @@ -782,10 +793,42 @@ HINT: To initiate replication, you must manually create the replication slot, e If all the slots are present on the standby server and the result (failover_ready) of the above SQL query is true, then - existing subscriptions can continue subscribing to publications now on the - new primary server. + existing subscriptions can continue subscribing to publications on the new + primary server. + + + + The first two steps in the above procedure are meant for a + PostgreSQL subscriber. It is recommended to run + these steps on each subscriber node, that will be served by the designated + standby after failover, to obtain the complete list of replication + slots. This list can then be verified in Step 3 to ensure failover readiness. + Non-PostgreSQL subscribers, on the other hand, may + use their own methods to identify the replication slots used by their + respective subscriptions. + + + + In some cases, such as during a planned failover, it is necessary to confirm + that all subscribers, whether PostgreSQL or + non-PostgreSQL, will be able to continue + replication after failover to a given standby server. In such cases, use the + following SQL, instead of performing the first two steps above, to identify + which replication slots on the primary need to be synced to the standby that + is intended for promotion. This query returns the relevant replication slots + associated with all the failover-enabled subscriptions. + + +/* primary # */ SELECT array_agg(quote_literal(r.slot_name)) AS slots + FROM pg_replication_slots r + WHERE r.failover AND NOT r.temporary; + slots +------- + {'sub1','sub2','sub3', 'pg_16394_sync_16385_7394666715149055164'} +(1 row) + @@ -1002,8 +1045,8 @@ HINT: To initiate replication, you must manually create the replication slot, e Create some publications. Publication p1 has one table (t1) and that table has a row filter. Publication - p2 has two tables. Table t1 has no row - filter, and table t2 has a row filter. Publication + p2 has two tables. Table t1 has no row + filter, and table t2 has a row filter. Publication p3 has two tables, and both of them have a row filter. 5 AND c = 'NSW'); @@ -1016,35 +1059,35 @@ HINT: To initiate replication, you must manually create the replication slot, e defined) for each publication. 5) AND (c = 'NSW'::text)) + "public.t1" WHERE ((a > 5) AND (c = 'NSW'::text)) - Publication p2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root -----------+------------+---------+---------+---------+-----------+---------- - postgres | f | t | t | t | t | f + Publication p2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +----------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + postgres | f | f | t | t | t | t | none | f Tables: - "public.t1" - "public.t2" WHERE (e = 99) + "public.t1" + "public.t2" WHERE (e = 99) - Publication p3 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root -----------+------------+---------+---------+---------+-----------+---------- - postgres | f | t | t | t | t | f + Publication p3 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +----------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + postgres | f | f | t | t | t | t | none | f Tables: - "public.t2" WHERE (d = 10) - "public.t3" WHERE (g = 10) + "public.t2" WHERE (d = 10) + "public.t3" WHERE (g = 10) ]]> psql can be used to show the row filter expressions (if - defined) for each table. See that table t1 is a member + defined) for each table. See that table t1 is a member of two publications, but has a row filter only in p1. - See that table t2 is a member of two publications, and + See that table t2 is a member of two publications, and has a different row filter in each of them. - On the subscriber node, create a table t1 with the same + On the subscriber node, create a table t1 with the same definition as the one on the publisher, and also create the subscription s1 that subscribes to the publication p1. @@ -1440,14 +1483,14 @@ Publications: Examples - Create a table t1 to be used in the following example. + Create a table t1 to be used in the following example. /* pub # */ CREATE TABLE t1(id int, a text, b text, c text, d text, e text, PRIMARY KEY(id)); Create a publication p1. A column list is defined for - table t1 to reduce the number of columns that will be + table t1 to reduce the number of columns that will be replicated. Notice that the order of column names in the column list does not matter. @@ -1459,10 +1502,10 @@ Publications: for each publication. /* pub # */ \dRp+ - Publication p1 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Via root -----------+------------+---------+---------+---------+-----------+---------- - postgres | f | t | t | t | t | f + Publication p1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +----------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + postgres | f | f | t | t | t | t | none | f Tables: "public.t1" (id, a, b, d) @@ -1488,9 +1531,9 @@ Publications: - On the subscriber node, create a table t1 which now + On the subscriber node, create a table t1 which now only needs a subset of the columns that were on the publisher table - t1, and also create the subscription + t1, and also create the subscription s1 that subscribes to the publication p1. @@ -1501,7 +1544,7 @@ Publications: - On the publisher node, insert some rows to table t1. + On the publisher node, insert some rows to table t1. /* pub # */ INSERT INTO t1 VALUES(1, 'a-1', 'b-1', 'c-1', 'd-1', 'e-1'); /* pub # */ INSERT INTO t1 VALUES(2, 'a-2', 'b-2', 'c-2', 'd-2', 'e-2'); @@ -1560,7 +1603,7 @@ Publications: /* sub # */ CREATE TABLE tab_gen_to_gen (a int, b int GENERATED ALWAYS AS (a * 100) STORED); /* sub # */ CREATE SUBSCRIPTION sub1 CONNECTION 'dbname=test_pub' PUBLICATION pub1; -/* sub # */ SELECT * from tab_gen_to_gen; +/* sub # */ SELECT * FROM tab_gen_to_gen; a | b ---+---- 1 | 100 @@ -1711,6 +1754,247 @@ Publications: + + Replicating Sequences + + + To synchronize sequences from a publisher to a subscriber, first publish + them using + CREATE PUBLICATION ... FOR ALL SEQUENCES and then + on the subscriber: + + + + + + + use CREATE SUBSCRIPTION + to initially synchronize the published sequences. + + + + + use + ALTER SUBSCRIPTION ... REFRESH PUBLICATION + to synchronize only newly added sequences. + + + + + use + ALTER SUBSCRIPTION ... REFRESH SEQUENCES + to re-synchronize all sequences currently known to the subscription. + + + + + + + A sequence synchronization worker will be started + after executing any of the above subscriber commands, and will exit once the + sequences are synchronized. + + + The ability to launch a sequence synchronization worker is limited by the + + max_sync_workers_per_subscription + configuration. + + + + Sequence Definition Mismatches + + The sequence synchronization worker validates that sequence definitions + match between publisher and subscriber. If mismatches exist, the worker + logs an error identifying them and exits. The apply worker continues + respawning the sequence synchronization worker until synchronization + succeeds. See also + wal_retrieve_retry_interval. + + + To resolve this, use + ALTER SEQUENCE + to align the subscriber's sequence parameters with those of the publisher. + + + + + Refreshing Out-of-Sync Sequences + + Subscriber sequence values will become out of sync as the publisher + advances them. + + + To detect this, compare the + pg_subscription_rel.srsublsn + on the subscriber with the page_lsn obtained + from the pg_get_sequence_data + function for the sequence on the publisher. Then run + + ALTER SUBSCRIPTION ... REFRESH SEQUENCES to + re-synchronize if necessary. + + + + Each sequence caches a block of values (typically 32) in memory before + generating a new WAL record, so its LSN advances only after the entire + cached batch has been consumed. As a result, sequence value drift cannot + be detected by LSN comparison when sequence increments fall within the + same cached block (typically 32 values). + + + + + + Examples + + + Create some sequences on the publisher. + +/* pub # */ CREATE SEQUENCE s1 START WITH 10 INCREMENT BY 1; +/* pub # */ CREATE SEQUENCE s2 START WITH 100 INCREMENT BY 10; + + + + Create the same sequences on the subscriber. + +/* sub # */ CREATE SEQUENCE s1 START WITH 10 INCREMENT BY 1; +/* sub # */ CREATE SEQUENCE s2 START WITH 100 INCREMENT BY 10; + + + + Advance the sequences on the publisher a few times. + +/* pub # */ SELECT nextval('s1'); + nextval +--------- + 10 +(1 row) +/* pub # */ SELECT nextval('s1'); + nextval +--------- + 11 +(1 row) +/* pub # */ SELECT nextval('s2'); + nextval +--------- + 100 +(1 row) +/* pub # */ SELECT nextval('s2'); + nextval +--------- + 110 +(1 row) + + + + Check the sequence page LSNs on the publisher. + +/* pub # */ SELECT * FROM pg_get_sequence_data('s1'); + last_value | is_called | page_lsn +------------+-----------+------------ + 11 | t | 0/0178F9E0 +(1 row) +/* pub # */ SELECT * FROM pg_get_sequence_data('s2'); + last_value | is_called | page_lsn +------------+-----------+------------ + 110 | t | 0/0178FAB0 +(1 row) + + + + Create a publication for the sequences. + +/* pub # */ CREATE PUBLICATION pub1 FOR ALL SEQUENCES; + + + + Subscribe to the publication. + +/* sub # */ CREATE SUBSCRIPTION sub1 +/* sub - */ CONNECTION 'host=localhost dbname=test_pub application_name=sub1' +/* sub - */ PUBLICATION pub1; + + + + Verify that the initial sequence values are synchronized. + +/* sub # */ SELECT last_value, is_called FROM s1; + last_value | is_called +------------+----------- + 11 | t +(1 row) + +/* sub # */ SELECT last_value, is_called FROM s2; + last_value | is_called +------------+----------- + 110 | t +(1 row) + + + + Confirm that the sequence page LSNs on the publisher have been recorded + on the subscriber. + +/* sub # */ SELECT srrelid::regclass, srsublsn FROM pg_subscription_rel; + srrelid | srsublsn +---------+------------ + s1 | 0/0178F9E0 + s2 | 0/0178FAB0 +(2 rows) + + + + Advance the sequences on the publisher 50 more times. + +/* pub # */ SELECT nextval('s1') FROM generate_series(1,50); +/* pub # */ SELECT nextval('s2') FROM generate_series(1,50); + + + + Check the sequence page LSNs on the publisher. + +/* pub # */ SELECT * FROM pg_get_sequence_data('s1'); + last_value | is_called | page_lsn +------------+-----------+------------ + 61 | t | 0/017CED28 +(1 row) + +/* pub # */ SELECT * FROM pg_get_sequence_data('s2'); + last_value | is_called | page_lsn +------------+-----------+------------ + 610 | t | 0/017CEDF8 +(1 row) + + + + The difference between the sequence page LSNs on the publisher and the + sequence page LSNs on the subscriber indicates that the sequences are out + of sync. Re-synchronize all sequences known to the subscriber using + + ALTER SUBSCRIPTION ... REFRESH SEQUENCES. + +/* sub # */ ALTER SUBSCRIPTION sub1 REFRESH SEQUENCES; + + + + Recheck the sequences on the subscriber. + +/* sub # */ SELECT last_value, is_called FROM s1; + last_value | is_called +------------+----------- + 61 | t +(1 row) + +/* sub # */ SELECT last_value, is_called FROM s2; + last_value | is_called +------------+----------- + 610 | t +(1 row) + + + + Conflicts @@ -1772,11 +2056,27 @@ Publications: + + update_deleted + + + The tuple to be updated was concurrently deleted by another origin. The + update will simply be skipped in this scenario. Note that this conflict + can only be detected when + track_commit_timestamp + and retain_dead_tuples + are enabled. Note that if a tuple cannot be found due to the table being + truncated, only a update_missing conflict will + arise. Additionally, if the tuple was deleted by the same origin, an + update_missing conflict will arise. + + + update_missing - The tuple to be updated was not found. The update will simply be + The row to be updated was not found. The update will simply be skipped in this scenario. @@ -1797,7 +2097,7 @@ Publications: delete_missing - The tuple to be deleted was not found. The delete will simply be + The row to be deleted was not found. The delete will simply be skipped in this scenario. @@ -1825,14 +2125,13 @@ Publications: The log format for logical replication conflicts is as follows: LOG: conflict detected on relation "schemaname.tablename": conflict=conflict_type -DETAIL: detailed_explanation. -{detail_values [; ... ]}. +DETAIL: detailed_explanation[: detail_values [, ... ]]. where detail_values is one of: - Key (column_name , ...)=(column_value , ...) - existing local tuple (column_name , ...)=(column_value , ...) - remote tuple (column_name , ...)=(column_value , ...) + key (column_name , ...)=(column_value , ...) + local row (column_name , ...)=(column_value , ...) + remote row (column_name , ...)=(column_value , ...) replica identity {(column_name , ...)=(column_value , ...) | full (column_name , ...)=(column_value , ...)} @@ -1866,32 +2165,32 @@ DETAIL: detailed_explanation. detailed_explanation includes the origin, transaction ID, and commit timestamp of the transaction that - modified the existing local tuple, if available. + modified the local row, if available. - The Key section includes the key values of the local - tuple that violated a unique constraint for + The key section includes the key values of the local + row that violated a unique constraint for insert_exists, update_exists or multiple_unique_conflicts conflicts. - The existing local tuple section includes the local - tuple if its origin differs from the remote tuple for + The local row section includes the local row if its + origin differs from the remote row for update_origin_differs or delete_origin_differs - conflicts, or if the key value conflicts with the remote tuple for + conflicts, or if the key value conflicts with the remote row for insert_exists, update_exists or multiple_unique_conflicts conflicts. - The remote tuple section includes the new tuple from + The remote row section includes the new row from the remote insert or update operation that caused the conflict. Note that - for an update operation, the column value of the new tuple will be null + for an update operation, the column value of the new row will be null if the value is unchanged and toasted. @@ -1899,7 +2198,7 @@ DETAIL: detailed_explanation. The replica identity section includes the replica identity key values that were used to search for the existing local - tuple to be updated or deleted. This may include the full tuple value + row to be updated or deleted. This may include the full row value if the local relation is marked with REPLICA IDENTITY FULL. @@ -1907,8 +2206,8 @@ DETAIL: detailed_explanation. column_name is the column name. - For existing local tuple, remote tuple, - and replica identity full cases, column names are + For local row, remote row, and + replica identity full cases, column names are logged only if the user lacks the privilege to access all columns of the table. If column names are present, they appear in the same order as the corresponding column values. @@ -1963,17 +2262,17 @@ DETAIL: detailed_explanation. emit the following kind of message to the subscriber's server log: ERROR: conflict detected on relation "public.test": conflict=insert_exists -DETAIL: Key already exists in unique index "t_pkey", which was modified locally in transaction 740 at 2024-06-26 10:47:04.727375+08. -Key (c)=(1); existing local tuple (1, 'local'); remote tuple (1, 'remote'). -CONTEXT: processing remote data for replication origin "pg_16395" during "INSERT" for replication target relation "public.test" in transaction 725 finished at 0/14C0378 +DETAIL: Could not apply remote change: remote row (1, 'remote'). +Key already exists in unique index "test_pkey", modified locally in transaction 800 at 2026-01-16 18:15:25.652759+09: key (c)=(1), local row (1, 'local'). +CONTEXT: processing remote data for replication origin "pg_16395" during "INSERT" for replication target relation "public.test" in transaction 725 finished at 0/014C0378 The LSN of the transaction that contains the change violating the constraint and - the replication origin name can be found from the server log (LSN 0/14C0378 and + the replication origin name can be found from the server log (LSN 0/014C0378 and replication origin pg_16395 in the above case). The transaction that produced the conflict can be skipped by using ALTER SUBSCRIPTION ... SKIP with the finish LSN - (i.e., LSN 0/14C0378). The finish LSN could be an LSN at which the transaction + (i.e., LSN 0/014C0378). The finish LSN could be an LSN at which the transaction is committed or prepared on the publisher. Alternatively, the transaction can also be skipped by calling the pg_replication_origin_advance() function. @@ -1984,7 +2283,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER disable_on_error option. Then, you can use pg_replication_origin_advance() function with the node_name (i.e., pg_16395) - and the next LSN of the finish LSN (i.e., 0/14C0379). The current position of + and the next LSN of the finish LSN (i.e., 0/014C0379). The current position of origins can be seen in the pg_replication_origin_status system view. Please note that skipping the whole transaction includes skipping changes that @@ -2040,16 +2339,19 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - Sequence data is not replicated. The data in serial or identity columns - backed by sequences will of course be replicated as part of the table, - but the sequence itself would still show the start value on the - subscriber. If the subscriber is used as a read-only database, then this - should typically not be a problem. If, however, some kind of switchover - or failover to the subscriber database is intended, then the sequences - would need to be updated to the latest values, either by copying the - current data from the publisher (perhaps - using pg_dump) or by determining a sufficiently high - value from the tables themselves. + Incremental sequence changes are not replicated. Although the data in + serial or identity columns backed by sequences will be replicated as part + of the table, the sequences themselves do not replicate ongoing changes. + On the subscriber, a sequence will retain the last value it synchronized + from the publisher. If the subscriber is used as a read-only database, + then this should typically not be a problem. If, however, some kind of + switchover or failover to the subscriber database is intended, then the + sequences would need to be updated to the latest values, either by + executing + ALTER SUBSCRIPTION ... REFRESH SEQUENCES + or by copying the current data from the publisher (perhaps using + pg_dump) or by determining a sufficiently high value + from the tables themselves. @@ -2125,8 +2427,8 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER implemented by walsender and apply processes. The walsender process starts logical decoding (described in ) of the WAL and loads the standard - logical decoding output plugin (pgoutput). The plugin - transforms the changes read + logical decoding output plugin (). + The plugin transforms the changes read from WAL to the logical replication protocol (see ) and filters the data according to the publication specification. The data is then continuously @@ -2197,8 +2499,8 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER Monitoring - Because logical replication is based on a similar architecture as - physical streaming replication, + Because streaming logical replication is based on a similar architecture as + streaming physical replication, the monitoring on a publication node is similar to monitoring of a physical replication primary (see ). @@ -2240,9 +2542,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - In order to be able to copy the initial table data, the role used for the - replication connection must have the SELECT privilege on - a published table (or be a superuser). + In order to be able to copy the initial table or sequence data, the role + used for the replication connection must have the SELECT + privilege on a published table or sequence (or be a superuser). @@ -2251,10 +2553,14 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - To add tables to a publication, the user must have ownership rights on the - table. To add all tables in schema to a publication, the user must be a - superuser. To create a publication that publishes all tables or all tables in - schema automatically, the user must be a superuser. + To create a publication using FOR TABLE, the user must + have ownership rights on all the listed tables. To create a publication + using any of FOR ALL TABLES, + FOR ALL SEQUENCES, + or FOR TABLES IN SCHEMA, the user must be a superuser. To + alter a publication using ADD TABLE, the user must have + ownership rights on all the listed tables. To alter a publication using + ADD TABLES IN SCHEMA, the user must be a superuser. @@ -2271,7 +2577,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER To create a subscription, the user must have the privileges of the pg_create_subscription role, as well as - CREATE privileges on the database. + CREATE privileges on the database. If + SERVER is specified, the user also must have + USAGE privileges on the server. @@ -2279,8 +2587,11 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER privileges of the subscription owner. However, when performing an insert, update, delete, or truncate operation on a particular table, it will switch roles to the table owner and perform the operation with the table owner's - privileges. This means that the subscription owner needs to be able to - SET ROLE to each role that owns a replicated table. + privileges. Similarly, when synchronizing sequence data, it will switch to + the sequence owner's role and perform the operation using the sequence + owner's privileges. This means that the subscription owner needs to be able + to SET ROLE to each role that owns a replicated table or + sequence. @@ -2327,7 +2638,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER wal_level must be - set to logical. + set to replica or logical. @@ -2364,11 +2675,17 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER the subscriber, plus some reserve for table synchronization. + + max_replication_slots + must be set to at least 1 when retain_dead_tuples + is enabled for any subscription. + + max_logical_replication_workers must be set to at least the number of subscriptions (for leader apply - workers), plus some reserve for the table synchronization workers and - parallel apply workers. + workers), plus some reserve for the parallel apply workers, and + table/sequence synchronization workers. @@ -2381,8 +2698,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER max_sync_workers_per_subscription - controls the amount of parallelism of the initial data copy during the - subscription initialization or when new tables are added. + controls how many tables can be synchronized in parallel during + subscription initialization or when new tables are added. One additional + worker is also needed for sequence synchronization. @@ -2413,7 +2731,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - Prepare for publisher upgrades + Prepare for Publisher Upgrades pg_upgrade attempts to migrate logical @@ -2442,7 +2760,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER The new cluster must have wal_level as - logical. + replica or logical. @@ -2467,25 +2785,24 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - All slots on the old cluster must be usable, i.e., there are no slots - whose + All slots on the old cluster must be usable, i.e., their pg_replication_slots.conflicting - is not true. + is false. - The new cluster must not have permanent logical slots, i.e., - there must be no slots where + The new cluster must not have any permanent logical slots; i.e., any + existing logical slots must have pg_replication_slots.temporary - is false. + set to true. - Prepare for subscriber upgrades + Prepare for Subscriber Upgrades Setup the @@ -2500,6 +2817,22 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER dependencies on clusters before version 17.0 will silently be ignored. + + + Commit timestamps and origin data are not preserved during the upgrade. + As a result, even if + retain_dead_tuples + is enabled, the upgraded subscriber may be unable to detect conflicts or + log relevant commit timestamps and origins when applying changes from the + publisher occurred before the upgrade. Additionally, immediately after the + upgrade, the vacuum may remove the deleted rows that are required for + conflict detection. This can affect the changes that were not replicated + before the upgrade. To ensure consistent conflict tracking, users should + ensure that all potentially conflicting changes are replicated to the + subscriber before initiating the upgrade. + + + There are some prerequisites for pg_upgrade to be able to upgrade the subscriptions. If these are not met an error @@ -2531,11 +2864,21 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER subscriptions present in the old cluster. + + + If there are subscriptions with retain_dead_tuples enabled, the reserved + replication slot pg_conflict_detection + must not exist on the new cluster. Additionally, the + wal_level on the + new cluster must be set to replica or + logical. + + - Upgrading logical replication clusters + Upgrading Logical Replication Clusters While upgrading a subscriber, write operations can be performed in the @@ -2599,7 +2942,7 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - Steps to upgrade a two-node logical replication cluster + Steps to Upgrade a Two-node Logical Replication Cluster Let's say publisher is in node1 and subscriber is in node2. The subscriber node2 has @@ -2743,7 +3086,7 @@ pg_ctl -D /opt/PostgreSQL/data2_upgraded start -l logfile - Steps to upgrade a cascaded logical replication cluster + Steps to Upgrade a Cascaded Logical Replication Cluster Let's say we have a cascaded logical replication setup node1->node2->node3. @@ -2972,7 +3315,7 @@ pg_ctl -D /opt/PostgreSQL/data3_upgraded start -l logfile - Steps to upgrade a two-node circular logical replication cluster + Steps to Upgrade a Two-node Circular Logical Replication Cluster Let's say we have a circular logical replication setup node1->node2 and @@ -3183,7 +3526,7 @@ wal_level = logical (the values here depend on your actual network configuration and user you want to use for connecting): -host all repuser 0.0.0.0/0 md5 +host all repuser 0.0.0.0/0 scram-sha-256 @@ -3203,8 +3546,8 @@ CREATE SUBSCRIPTION mysub CONNECTION 'dbname=foo host=bar user=repuser' PUBLICAT The above will start the replication process, which synchronizes the - initial table contents of the tables users and - departments and then starts replicating + initial table contents of the tables users and + departments and then starts replicating incremental changes to those tables. diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index dd9e83b08eaf1..9b1d68d0de61e 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -47,7 +47,7 @@ Before you can use logical decoding, you must set - to logical and + to replica or higher and to at least 1. Then, you should connect to the target database (in the example below, postgres) as a superuser. @@ -57,14 +57,14 @@ postgres=# -- Create a slot named 'regression_slot' using the output plugin 'test_decoding' postgres=# SELECT * FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding', false, true); slot_name | lsn ------------------+----------- - regression_slot | 0/16B1970 +-----------------+------------ + regression_slot | 0/016B1970 (1 row) postgres=# SELECT slot_name, plugin, slot_type, database, active, restart_lsn, confirmed_flush_lsn FROM pg_replication_slots; slot_name | plugin | slot_type | database | active | restart_lsn | confirmed_flush_lsn ------------------+---------------+-----------+----------+--------+-------------+----------------- - regression_slot | test_decoding | logical | postgres | f | 0/16A4408 | 0/16A4440 +-----------------+---------------+-----------+----------+--------+-------------+--------------------- + regression_slot | test_decoding | logical | postgres | f | 0/016A4408 | 0/016A4440 (1 row) postgres=# -- There are no changes to see yet @@ -73,15 +73,15 @@ postgres=# SELECT * FROM pg_logical_slot_get_changes('regression_slot', NULL, NU -----+-----+------ (0 rows) -postgres=# CREATE TABLE data(id serial primary key, data text); +postgres=# CREATE TABLE data(id serial PRIMARY KEY, data text); CREATE TABLE postgres=# -- DDL isn't replicated, so all you'll see is the transaction postgres=# SELECT * FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-------+-------------- - 0/BA2DA58 | 10297 | BEGIN 10297 - 0/BA5A5A0 | 10297 | COMMIT 10297 + lsn | xid | data +------------+-------+-------------- + 0/0BA2DA58 | 10297 | BEGIN 10297 + 0/0BA5A5A0 | 10297 | COMMIT 10297 (2 rows) postgres=# -- Once changes are read, they're consumed and not emitted @@ -97,41 +97,41 @@ postgres=*# INSERT INTO data(data) VALUES('2'); postgres=*# COMMIT; postgres=# SELECT * FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-------+--------------------------------------------------------- - 0/BA5A688 | 10298 | BEGIN 10298 - 0/BA5A6F0 | 10298 | table public.data: INSERT: id[integer]:1 data[text]:'1' - 0/BA5A7F8 | 10298 | table public.data: INSERT: id[integer]:2 data[text]:'2' - 0/BA5A8A8 | 10298 | COMMIT 10298 + lsn | xid | data +------------+-------+--------------------------------------------------------- + 0/0BA5A688 | 10298 | BEGIN 10298 + 0/0BA5A6F0 | 10298 | table public.data: INSERT: id[integer]:1 data[text]:'1' + 0/0BA5A7F8 | 10298 | table public.data: INSERT: id[integer]:2 data[text]:'2' + 0/0BA5A8A8 | 10298 | COMMIT 10298 (4 rows) postgres=# INSERT INTO data(data) VALUES('3'); postgres=# -- You can also peek ahead in the change stream without consuming changes postgres=# SELECT * FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-------+--------------------------------------------------------- - 0/BA5A8E0 | 10299 | BEGIN 10299 - 0/BA5A8E0 | 10299 | table public.data: INSERT: id[integer]:3 data[text]:'3' - 0/BA5A990 | 10299 | COMMIT 10299 + lsn | xid | data +------------+-------+--------------------------------------------------------- + 0/0BA5A8E0 | 10299 | BEGIN 10299 + 0/0BA5A8E0 | 10299 | table public.data: INSERT: id[integer]:3 data[text]:'3' + 0/0BA5A990 | 10299 | COMMIT 10299 (3 rows) postgres=# -- The next call to pg_logical_slot_peek_changes() returns the same changes again postgres=# SELECT * FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-------+--------------------------------------------------------- - 0/BA5A8E0 | 10299 | BEGIN 10299 - 0/BA5A8E0 | 10299 | table public.data: INSERT: id[integer]:3 data[text]:'3' - 0/BA5A990 | 10299 | COMMIT 10299 + lsn | xid | data +------------+-------+--------------------------------------------------------- + 0/0BA5A8E0 | 10299 | BEGIN 10299 + 0/0BA5A8E0 | 10299 | table public.data: INSERT: id[integer]:3 data[text]:'3' + 0/0BA5A990 | 10299 | COMMIT 10299 (3 rows) postgres=# -- options can be passed to output plugin, to influence the formatting postgres=# SELECT * FROM pg_logical_slot_peek_changes('regression_slot', NULL, NULL, 'include-timestamp', 'on'); - lsn | xid | data ------------+-------+--------------------------------------------------------- - 0/BA5A8E0 | 10299 | BEGIN 10299 - 0/BA5A8E0 | 10299 | table public.data: INSERT: id[integer]:3 data[text]:'3' - 0/BA5A990 | 10299 | COMMIT 10299 (at 2017-05-10 12:07:21.272494-04) + lsn | xid | data +------------+-------+--------------------------------------------------------- + 0/0BA5A8E0 | 10299 | BEGIN 10299 + 0/0BA5A8E0 | 10299 | table public.data: INSERT: id[integer]:3 data[text]:'3' + 0/0BA5A990 | 10299 | COMMIT 10299 (at 2017-05-10 12:07:21.272494-04) (3 rows) postgres=# -- Remember to destroy a slot you no longer need to stop it consuming @@ -169,7 +169,7 @@ COMMIT 693 $ pg_recvlogical -d postgres --slot=test --drop-slot Example 2: -$ pg_recvlogical -d postgres --slot=test --create-slot --two-phase +$ pg_recvlogical -d postgres --slot=test --create-slot --enable-two-phase $ pg_recvlogical -d postgres --slot=test --start -f - ControlZ $ psql -d postgres -c "BEGIN;INSERT INTO data(data) VALUES('5');PREPARE TRANSACTION 'test';" @@ -200,37 +200,37 @@ postgres=*# INSERT INTO data(data) VALUES('5'); postgres=*# PREPARE TRANSACTION 'test_prepared1'; postgres=# SELECT * FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-----+--------------------------------------------------------- - 0/1689DC0 | 529 | BEGIN 529 - 0/1689DC0 | 529 | table public.data: INSERT: id[integer]:3 data[text]:'5' - 0/1689FC0 | 529 | PREPARE TRANSACTION 'test_prepared1', txid 529 + lsn | xid | data +------------+-----+--------------------------------------------------------- + 0/01689DC0 | 529 | BEGIN 529 + 0/01689DC0 | 529 | table public.data: INSERT: id[integer]:3 data[text]:'5' + 0/01689FC0 | 529 | PREPARE TRANSACTION 'test_prepared1', txid 529 (3 rows) postgres=# COMMIT PREPARED 'test_prepared1'; -postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-----+-------------------------------------------- - 0/168A060 | 529 | COMMIT PREPARED 'test_prepared1', txid 529 +postgres=# SELECT * from pg_logical_slot_get_changes('regression_slot', NULL, NULL); + lsn | xid | data +------------+-----+-------------------------------------------- + 0/0168A060 | 529 | COMMIT PREPARED 'test_prepared1', txid 529 (4 row) postgres=#-- you can also rollback a prepared transaction postgres=# BEGIN; postgres=*# INSERT INTO data(data) VALUES('6'); postgres=*# PREPARE TRANSACTION 'test_prepared2'; -postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-----+--------------------------------------------------------- - 0/168A180 | 530 | BEGIN 530 - 0/168A1E8 | 530 | table public.data: INSERT: id[integer]:4 data[text]:'6' - 0/168A430 | 530 | PREPARE TRANSACTION 'test_prepared2', txid 530 +postgres=# SELECT * from pg_logical_slot_get_changes('regression_slot', NULL, NULL); + lsn | xid | data +------------+-----+--------------------------------------------------------- + 0/0168A180 | 530 | BEGIN 530 + 0/0168A1E8 | 530 | table public.data: INSERT: id[integer]:4 data[text]:'6' + 0/0168A430 | 530 | PREPARE TRANSACTION 'test_prepared2', txid 530 (3 rows) postgres=# ROLLBACK PREPARED 'test_prepared2'; -postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NULL); - lsn | xid | data ------------+-----+---------------------------------------------- - 0/168A4B8 | 530 | ROLLBACK PREPARED 'test_prepared2', txid 530 +postgres=# SELECT * from pg_logical_slot_get_changes('regression_slot', NULL, NULL); + lsn | xid | data +------------+-----+---------------------------------------------- + 0/0168A4B8 | 530 | ROLLBACK PREPARED 'test_prepared2', txid 530 (1 row)
@@ -257,6 +257,47 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU log, which describe changes on a storage level, into an application-specific form such as a stream of tuples or SQL statements. + + + Logical decoding becomes available in two conditions: + + + + + When is set to logical. + + + + + When is set to replica + and at least one valid logical replication slot exists on the system. + + + + + If either condition is met, the operational WAL level becomes equivalent + to logical, which can be monitored through the + parameter. + + + When wal_level is set to replica, + logical decoding is automatically activated upon creation of the first + logical replication slot. This activation process involves several steps + and requires synchronization among processes, ensuring system-wide + consistency. Conversely, if wal_level is set to + replica and the last logical replication slot is dropped + or invalidated, logical decoding is automatically disabled. Note that the + deactivation of logical decoding might take some time as it is performed + asynchronously by the checkpointer process. + + + + + When wal_level is set to replica, + dropping or invalidating the last logical slot disables logical decoding + on the primary, resulting in slots on standbys being invalidated. + + @@ -275,9 +316,9 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU - PostgreSQL also has streaming replication slots - (see ), but they are used somewhat - differently there. + PostgreSQL can also use streaming replication slots + to maintain a standby server (see ), but + typically those use physical replication, not logical. @@ -290,7 +331,7 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU A logical slot will emit each change just once in normal operation. The current position of each slot is persisted only at checkpoint, so in - the case of a crash the slot may return to an earlier LSN, which will + the case of a crash the slot might return to an earlier LSN, which will then cause recent changes to be sent again when the server restarts. Logical decoding clients are responsible for avoiding ill effects from handling the same message more than once. Clients may wish to record @@ -328,7 +369,7 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU that could be needed by the logical decoding on the standby (as it does not know about the catalog_xmin on the standby). Existing logical slots on standby also get invalidated if - wal_level on the primary is reduced to less than + effective_wal_level on the primary is reduced to less than logical. This is done as soon as the standby detects such a change in the WAL stream. It means that, for walsenders that are lagging (if any), some WAL records up @@ -370,10 +411,10 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU pg_create_logical_replication_slot, or by using the failover option of - CREATE SUBSCRIPTION during slot creation, and then calling - - pg_sync_replication_slots - on the standby. By setting + CREATE SUBSCRIPTION during slot creation. + Additionally, enabling + sync_replication_slots on the standby + is required. By enabling sync_replication_slots on the standby, the failover slots can be synchronized periodically in the slotsync worker. For the synchronization to work, it is mandatory to @@ -398,6 +439,50 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU receiving the WAL up to the latest flushed position on the primary server. + + + While enabling + sync_replication_slots allows for automatic + periodic synchronization of failover slots, they can also be manually + synchronized using the + pg_sync_replication_slots function on the standby. + However, unlike automatic synchronization, it does not perform incremental + updates. It retries cyclically until all the failover slots that existed on + primary at the start of the function call are synchronized. Any slots created + after the function begins will not be synchronized. In contrast, automatic + synchronization via sync_replication_slots provides + continuous slot updates, enabling seamless failover and supporting high + availability. Therefore, it is the recommended method for synchronizing slots. + + + + + When slot synchronization is configured as recommended, + and the initial synchronization is performed either automatically or + manually via pg_sync_replication_slots, the standby + can persist the synchronized slot only if the following condition is met: + The logical replication slot on the primary must retain WALs and system + catalog rows that are still available on the standby. This ensures data + integrity and allows logical replication to continue smoothly after + promotion. + If the required WALs or catalog rows have already been purged from the + standby, the slot will not be persisted to avoid data loss. In such + cases, the following log message may appear: + +LOG: could not synchronize replication slot "failover_slot" +DETAIL: Synchronization could lead to data loss, because the remote slot needs WAL at LSN 0/03003F28 and catalog xmin 754, but the standby has LSN 0/03003F28 and catalog xmin 756. + + If the logical replication slot is actively used by a consumer, no + manual intervention is needed; the slot will advance automatically, + and synchronization will resume in the next cycle. However, if no + consumer is configured, it is advisable to manually advance the slot + on the primary using + pg_logical_slot_get_changes or + + pg_logical_slot_get_binary_changes, + allowing synchronization to proceed. + + The ability to resume logical replication after failover depends upon the pg_replication_slots.synced @@ -528,6 +613,170 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU Logical Decoding Output Plugins + + + PostgreSQL provides two logical decoding + output plugins, and + . You can also develop custom output plugins + (see for details). + + + + pgoutput — Standard Logical Decoding Output Plugin + + + pgoutput + + + + pgoutput is the standard logical decoding output + plugin provided by PostgreSQL. + It's used for the built-in + logical replication. + + + + Options + + + + proto_version (integer) + + + Specifies the protocol version. + Currently versions 1, 2, + 3, and 4 are supported. A valid + version is required. + + + Version 2 is supported on server version 14 + and above, and is required when streaming + is set to on to stream large in-progress + transactions. + + + Version 3 is supported on server version 15 + and above, and is required when two_phase + is enabled to stream two-phase commits. + + + Version 4 is supported on server version 16 + and above, and is required when streaming + is set to parallel to stream large in-progress + transactions to be applied in parallel. + + + + + + publication_names (string) + + + A comma-separated list of publication names to subscribe to. + The individual publication names are treated + as standard objects names and can be quoted the same as needed. + At least one publication name is required. + + + + + + binary (boolean) + + + Enables binary transfer mode. Binary mode is faster + than the text mode but slightly less robust. + The default is off. + + + + + + messages (boolean) + + + Enables sending the messages that are written by + pg_logical_emit_message. + The default is off. + + + + + + streaming (enum) + + + Enables streaming of in-progress transactions. Valid values are + off (the default), on and + parallel. + + + When set to off, pgoutput + fully decodes a transaction before sending it as a whole. + This mode works with any protocol version. + + + When set to on, pgoutput + streams large in-progress transactions. + This requires protocol version 2 or higher. + + + When set to parallel, pgoutput + streams large in-progress transactions and also sends + extra information in some messages to support parallel processing. + This requires protocol version 4 or higher. + + + + + + two_phase (boolean) + + + Enables sending two-phase transactions. + Minimum protocol version 3 is required to turn it on. + The default is off. + + + + + + origin (enum) + + + Specifies whether to send changes by their origin. Possible values are + none to only send the changes that have no origin + associated, or any + to send the changes regardless of their origin. This can be used + to avoid loops (infinite replication of the same data) among + replication nodes. + The default is any. + + + + + + + + + Notes + + + pgoutput produces binary output, + so functions expecting textual data ( + pg_logical_slot_peek_changes and + pg_logical_slot_get_changes) + cannot be used with it. Use + pg_logical_slot_peek_binary_changes or + pg_logical_slot_get_binary_changes + instead. + + + + + + + Writing Logical Decoding Output Plugins An example output plugin can be found in the @@ -710,6 +959,7 @@ typedef struct OutputPluginOptions { OutputPluginOutputType output_type; bool receive_rewrites; + bool need_shared_catalogs; } OutputPluginOptions; output_type has to either be set to @@ -720,6 +970,9 @@ typedef struct OutputPluginOptions also be called for changes made by heap rewrites during certain DDL operations. These are of interest to plugins that handle DDL replication, but they require special handling. + need_shared_catalogs can be set to false if you are + certain the plugin functions do not access shared system catalogs. + Doing so can speed up creation of replication slots that use this plugin. @@ -851,7 +1104,7 @@ typedef void (*LogicalDecodeTruncateCB) (struct LogicalDecodingContext *ctx, output plugin. typedef bool (*LogicalDecodeFilterByOriginCB) (struct LogicalDecodingContext *ctx, - RepOriginId origin_id); + ReplOriginId origin_id); The ctx parameter has the same contents as for the other callbacks. No information but the origin is @@ -1366,7 +1619,7 @@ commit_prepared_cb(...); <-- commit of the prepared transaction currently used for decoded changes) is selected and streamed. However, in some cases we still have to spill to disk even if streaming is enabled because we exceed the memory threshold but still have not decoded the - complete tuple e.g., only decoded toast table insert but not the main table + complete tuple e.g., only decoded TOAST table insert but not the main table insert. diff --git a/doc/src/sgml/ltree.sgml b/doc/src/sgml/ltree.sgml index 1c3543303f0ab..ff3c227727b57 100644 --- a/doc/src/sgml/ltree.sgml +++ b/doc/src/sgml/ltree.sgml @@ -645,7 +645,7 @@ Europe & Russia*@ & !Transportation siglen determines the signature length in bytes. The default signature length is 8 bytes. The length must be a positive multiple of int alignment - (4 bytes on most machines)) up to 2024. Longer + (4 bytes on most machines) up to 2024. Longer signatures lead to a more precise search (scanning a smaller fraction of the index and fewer heap pages), at the cost of a larger index. @@ -818,7 +818,7 @@ ltreetest=> SELECT subpath(path,0,2)||'Space'||subpath(path,2) FROM test WHER at a specified position in a path: CREATE FUNCTION ins_label(ltree, int, text) RETURNS ltree - AS 'select subpath($1,0,$2) || $3 || subpath($1,$2);' + AS 'SELECT subpath($1, 0, $2) || $3 || subpath($1, $2);' LANGUAGE SQL IMMUTABLE; ltreetest=> SELECT ins_label(path,2,'Space') FROM test WHERE path <@ 'Top.Science.Astronomy'; diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 600e4b3f2f3b8..76262df0502c2 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -614,8 +614,8 @@ examine this information is to execute queries such as: -SELECT c.oid::regclass as table_name, - greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age +SELECT c.oid::regclass AS table_name, + greatest(age(c.relfrozenxid), age(t.relfrozenxid)) AS age FROM pg_class c LEFT JOIN pg_class t ON c.reltoastrelid = t.oid WHERE c.relkind IN ('r', 'm'); @@ -670,10 +670,11 @@ SELECT datname, age(datfrozenxid) FROM pg_database; If for some reason autovacuum fails to clear old XIDs from a table, the system will begin to emit warning messages like this when the database's - oldest XIDs reach forty million transactions from the wraparound point: + oldest XIDs reach one hundred million transactions from the wraparound point: -WARNING: database "mydb" must be vacuumed within 39985967 transactions +WARNING: database "mydb" must be vacuumed within 99985967 transactions +DETAIL: Approximately 4.66% of transaction IDs are available for use. HINT: To avoid XID assignment failures, execute a database-wide VACUUM in that database. @@ -779,7 +780,10 @@ HINT: Execute a database-wide VACUUM in that database. careful aging management, storage cleanup, and wraparound handling. There is a separate storage area which holds the list of members in each multixact, which also uses a 32-bit counter and which must also - be managed. + be managed. The system function + pg_get_multixact_members() described in + can be used to examine the + transaction IDs associated with a multixact ID. @@ -810,17 +814,46 @@ HINT: Execute a database-wide VACUUM in that database. As a safety device, an aggressive vacuum scan will occur for any table whose multixact-age is greater than . Also, if the - storage occupied by multixacts members exceeds about 10GB, aggressive vacuum + linkend="guc-autovacuum-multixact-freeze-max-age"/>. Also, if the number + of multixact member entries created exceeds approximately 2 billion + entries (occupying roughly 10GB in the + pg_multixact/members directory), aggressive vacuum scans will occur more often for all tables, starting with those that - have the oldest multixact-age. Both of these kinds of aggressive - scans will occur even if autovacuum is nominally disabled. The members storage - area can grow up to about 20GB before reaching wraparound. + have the oldest multixact-age. Both of these kinds of aggressive + scans will occur even if autovacuum is nominally disabled. At approximately + 4 billion entries (occupying roughly 20GB in the + pg_multixact/members directory), even more aggressive + vacuum scans are triggered to reclaim member storage space. + + + + The pg_get_multixact_stats() function described in + provides a way to monitor + multixact allocation and usage patterns in real time, for example: + +=# SELECT *, pg_size_pretty(members_size) members_size_pretty + FROM pg_catalog.pg_get_multixact_stats(); + num_mxids | num_members | members_size | oldest_multixact | members_size_pretty +-----------+-------------+--------------+------------------+--------------------- + 311740299 | 2785241176 | 13926205880 | 2 | 13 GB +(1 row) + + This output shows a system with significant multixact activity: about + 312 million multixact IDs and about 2.8 billion member entries consuming + 13 GB of storage space. + A spike in num_mxids might indicate multiple sessions + running UPDATE statements with foreign key checks, + concurrent SELECT FOR SHARE operations, or frequent + use of savepoints causing lock contention. + If oldest_multixact value remains unchanged while + num_members grows, it could indicate that long-running + transactions are preventing cleanup, or autovacuum is + not keeping up with the workload. Similar to the XID case, if autovacuum fails to clear old MXIDs from a table, the - system will begin to emit warning messages when the database's oldest MXIDs reach forty + system will begin to emit warning messages when the database's oldest MXIDs reach one hundred million transactions from the wraparound point. And, just as in the XID case, if these warnings are ignored, the system will refuse to generate new MXIDs once there are fewer than three million left until wraparound. @@ -889,7 +922,8 @@ HINT: Execute a database-wide VACUUM in that database. the next database will be processed as soon as the first worker finishes. Each worker process will check each table within its database and execute VACUUM and/or ANALYZE as needed. - can be set to monitor + and + can be set to monitor autovacuum workers' activity. @@ -930,12 +964,16 @@ vacuum threshold = Minimum(vacuum max threshold, vacuum base threshold + vacuum The table is also vacuumed if the number of tuples inserted since the last vacuum has exceeded the defined insert threshold, which is defined as: -vacuum insert threshold = vacuum base insert threshold + vacuum insert scale factor * number of tuples +vacuum insert threshold = vacuum base insert threshold + vacuum insert scale factor * number of tuples * percent of table not frozen where the vacuum insert base threshold is , - and vacuum insert scale factor is - . + the vacuum insert scale factor is + , + the number of tuples is + pg_class.reltuples, + and the percent of the table not frozen is + 1 - pg_class.relallfrozen / pg_class.relpages. Such vacuums may allow portions of the table to be marked as all visible and also allow tuples to be frozen, which can reduce the work required in subsequent vacuums. @@ -1000,6 +1038,10 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu per-table autovacuum_vacuum_cost_delay or autovacuum_vacuum_cost_limit storage parameters have been set are not considered in the balancing algorithm. + Parallel workers launched for are using + the same cost delay parameters as the leader worker. If any of these + parameters are changed in the leader worker, it will propagate the new + parameter values to all of its parallel workers. @@ -1010,8 +1052,11 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu see . However, if the autovacuum is running to prevent transaction ID wraparound (i.e., the autovacuum query name in the pg_stat_activity view ends with - (to prevent wraparound)), the autovacuum is not - automatically interrupted. + (to prevent wraparound) or the + started_by column in the + pg_stat_progress_vacuum view shows + autovacuum_wraparound value), the autovacuum is + not automatically interrupted. @@ -1021,6 +1066,145 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu effectively prevent autovacuums from ever completing. + + + Autovacuum Prioritization + + + Autovacuum decides what to process in two steps: first it chooses a + database, then it chooses the tables within that database. The autovacuum + launcher process prioritizes databases at risk of transaction ID or + multixact ID wraparound, else it chooses the database processed least + recently. As an exception, it skips databases with no connections or no + activity since the last statistics reset, unless at risk of wraparound. + + + + Within a database, the autovacuum worker process builds a list of tables + that require vacuum or analyze and sorts them using a scoring system. It + scores each table by taking the maximum value of several component scores + representing various criteria important to vacuum or analyze. Those + components are as follows: + + + + + + The transaction ID component measures the age in + transactions of the table's + pg_class.relfrozenxid + field as compared to . + Furthermore, this component increases greatly once the age surpasses + . The final value for this + component can be adjusted via + . Note that + increasing this parameter's value also lowers the age at which this + component begins scaling aggressively, i.e., the scaling age is divided + by its value if greater than 1.0. + + + + + + The multixact ID component measures the age in + multixacts of the table's + pg_class.relminmxid + field as compared to + . Furthermore, + this component increases greatly once the age surpasses + . The final value + for this component can be adjusted via + . Note + that increasing this parameter's value also lowers the age at which this + component begins scaling aggressively, i.e., the scaling age is divided + by its value if greater than 1.0. + + + + + + The vacuum component measures the number of updated + or deleted tuples as compared to the threshold calculated with + , + , and + . The final value + for this component can be adjusted via + . + + + + + + The vacuum insert component measures the number of + inserted tuples as compared to the threshold calculated with + and + . The final + value for this component can be adjusted via + . + + + + + + The analyze component measures the number of + inserted, updated, or deleted tuples as compared to the threshold + calculated with + and + . The final value + for this component can be adjusted via + . + + + + + + To revert to the prioritization strategy used before + PostgreSQL 19 (i.e., the order the tables are + listed in the pg_class system catalog), set all of the + aforementioned "weight" parameters to 0.0. Otherwise, + these "weight" parameters are multiplied to their respective component + scores. For example, raising + to + 2.0 effectively doubles the + analyze component score. + + + + The + pg_stat_autovacuum_scores + view shows the current scores of all tables in the current database. + + + + + + Parallel Vacuum + + + VACUUM can perform index vacuuming and index cleanup + phases in parallel using background workers (for the details of each + vacuum phase, please refer to ). The + degree of parallelism is determined by the number of indexes on the + relation that support parallel vacuum. For manual VACUUM, + this is limited by the PARALLEL option if specified, + which is further capped by . + For autovacuum, it is limited by the table's + if specified, + which is further capped by + parameter. Please note that it is not guaranteed that the number of parallel + workers that was calculated will be used during execution. It is possible for + a vacuum to run with fewer workers than specified, or even with no workers at + all. + + + + An index can participate in parallel vacuum if and only if the size of the + index is more than . + Only one worker can be used per index. So parallel workers are launched + only when there are at least 2 indexes in the table. + Workers for vacuum are launched before the start of each phase and exit at + the end of the phase. These behaviors might change in a future release. + diff --git a/doc/src/sgml/meson.build b/doc/src/sgml/meson.build index 6ae192eac68a4..a1ae5c54ed62f 100644 --- a/doc/src/sgml/meson.build +++ b/doc/src/sgml/meson.build @@ -1,4 +1,6 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +subdir('images') docs = [] installdocs = [] diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 4265a22d4de35..08d5b8245529f 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -338,6 +338,14 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_recoverypg_stat_recovery + Only one row, showing statistics about the state of recovery. + See + pg_stat_recovery for details. + + + pg_stat_recovery_prefetchpg_stat_recovery_prefetch Only one row, showing statistics about blocks prefetched during recovery. @@ -399,12 +407,20 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser pg_stat_progress_clusterpg_stat_progress_cluster - One row for each backend running + One row for each backend running REPACK, CLUSTER or VACUUM FULL, showing current progress. See . + + pg_stat_progress_repackpg_stat_progress_repack + One row for each backend running REPACK, + CLUSTER or VACUUM FULL, showing current progress. + . + + + pg_stat_progress_basebackuppg_stat_progress_basebackup One row for each WAL sender process streaming a base backup, @@ -493,6 +509,15 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_lockpg_stat_lock + + One row for each lock type, containing cluster-wide locks statistics. + See + pg_stat_lock for details. + + + pg_stat_replication_slotspg_stat_replication_slots One row per replication slot, showing statistics about the @@ -571,6 +596,16 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser user tables are shown. + + pg_stat_autovacuum_scorespg_stat_autovacuum_scores + + One row for each table in the current database, showing the current + autovacuum scores for that specific table. See + + pg_stat_autovacuum_scores for details. + + + pg_stat_all_indexespg_stat_all_indexes @@ -1053,11 +1088,9 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser - BufferPin - The server process is waiting for exclusive access to - a data buffer. Buffer pin waits can be protracted if - another process holds an open cursor that last read data from the - buffer in question. See . + Buffer + The server process is waiting for access to a data buffer. + See . @@ -1137,7 +1170,7 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser Here are examples of how wait events can be viewed: -SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event is NOT NULL; +SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event IS NOT NULL; pid | wait_event_type | wait_event ------+-----------------+------------ 2540 | Lock | relation @@ -1150,7 +1183,7 @@ SELECT a.pid, a.wait_event, w.description FROM pg_stat_activity a JOIN pg_wait_events w ON (a.wait_event_type = w.type AND a.wait_event = w.name) - WHERE a.wait_event is NOT NULL and a.state = 'active'; + WHERE a.wait_event IS NOT NULL AND a.state = 'active'; -[ RECORD 1 ]------------------------------------------------------&zwsp;------------ pid | 686674 wait_event | WALInitSync @@ -1287,6 +1320,10 @@ description | Waiting for a newly initialized WAL file to reach durable storage This standby's xmin horizon reported by . + This field will be null if a replication slot is used (in this case, + the standby's xmin is shown in + pg_replication_slots) + or if hot_standby_feedback is disabled. @@ -1620,6 +1657,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + mem_exceeded_countbigint + + + Number of times the memory used by logical decoding has exceeded + logical_decoding_work_mem. + + + + total_txns bigint @@ -1644,6 +1692,30 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + slotsync_skip_countbigint + + + Number of times the slot synchronization is skipped. Slot + synchronization occurs only on standby servers and thus this column has + no meaning on the primary server. + + + + + + + slotsync_last_skiptimestamp with time zone + + + Time at which last slot synchronization was skipped. Slot + synchronization occurs only on standby servers and thus this column has + no meaning on the primary server. + + + + stats_reset timestamp with time zone @@ -1700,7 +1772,44 @@ description | Waiting for a newly initialized WAL file to reach durable storage status text - Activity status of the WAL receiver process + Activity status of the WAL receiver process. Possible values are: + + + + restarting: WAL receiver has been asked to + restart streaming. + + + + + starting: WAL receiver process has been launched + but is not yet initialized. + + + + + connecting: WAL receiver is connecting to the + upstream server, replication has not yet started. + + + + + stopping: WAL receiver has been requested to + stop. + + + + + streaming: WAL receiver is streaming WAL data. + + + + + waiting: WAL receiver has stopped streaming and + is waiting for new instructions from the startup process. + + + @@ -1838,6 +1947,149 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + <structname>pg_stat_recovery</structname> + + + pg_stat_recovery + + + + The pg_stat_recovery view will contain only + one row, showing statistics about the recovery state of the startup + process. This view returns no row when the server is not in recovery. + + + + <structname>pg_stat_recovery</structname> View + + + + + Column Type + + + Description + + + + + + + + promote_triggered boolean + + + True if a promotion has been triggered. + + + + + + last_replayed_read_lsn pg_lsn + + + Start write-ahead log location of the last successfully replayed + WAL record. + + + + + + last_replayed_end_lsn pg_lsn + + + End write-ahead log location of the last successfully replayed + WAL record. + + + + + + last_replayed_tli integer + + + Timeline of the last successfully replayed WAL record. + + + + + + replay_end_lsn pg_lsn + + + Write-ahead log location of the record currently being replayed + (end position plus one). When no record is being actively replayed, + equals last_replayed_end_lsn. + + + + + + replay_end_tli integer + + + Timeline of the WAL record currently being replayed. + + + + + + recovery_last_xact_time timestamp with time zone + + + Timestamp of the last transaction commit or abort replayed during + recovery. This is the time at which the commit or abort WAL record + for that transaction was generated on the primary. + + + + + + current_chunk_start_time timestamp with time zone + + + Time when the startup process observed that replay had caught up + with the latest received WAL chunk. Used in recovery-conflict + timing and replay/apply-lag diagnostics. NULL if not yet + available. + + + + + + pause_state text + + + Recovery pause state. Possible values are: + + + + + not paused: Recovery is proceeding normally. + + + + + pause requested: A pause has been requested + but recovery has not yet paused. + + + + + paused: Recovery is paused. + + + + + + + + +
+ +
+ <structname>pg_stat_recovery_prefetch</structname> @@ -2030,8 +2282,9 @@ description | Waiting for a newly initialized WAL file to reach durable storage Type of the subscription worker process. Possible types are - apply, parallel apply, and - table synchronization. + apply, parallel apply, + table synchronization, and + sequence synchronization. @@ -2179,7 +2432,18 @@ description | Waiting for a newly initialized WAL file to reach durable storage - sync_error_count bigint + sync_seq_error_count bigint + + + Number of times an error occurred in the sequence synchronization + worker. A single worker synchronizes all sequences, so one error + increment may represent failures across multiple sequences. + + + + + + sync_table_error_count bigint Number of times an error occurred during the initial table @@ -2223,6 +2487,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + confl_update_deleted bigint + + + Number of times the tuple to be updated was concurrently deleted by + another source during the application of changes. See + for details about this conflict. + + + confl_update_missing bigint @@ -3027,70 +3302,165 @@ description | Waiting for a newly initialized WAL file to reach durable storage - - <structname>pg_stat_bgwriter</structname> + + + <structname>pg_stat_lock</structname> - pg_stat_bgwriter + pg_stat_lock - The pg_stat_bgwriter view will always have a - single row, containing data about the background writer of the cluster. + The pg_stat_lock view will contain one row for each + lock type, showing cluster-wide locks statistics. - - <structname>pg_stat_bgwriter</structname> View +
+ <structname>pg_stat_lock</structname> View - - Column Type - - - Description - + + + Column Type + + + Description + + - - - buffers_clean bigint - - - Number of buffers written by the background writer - + + + locktype text + + + Type of the lockable object. See + pg_locks for details. + + - - maxwritten_clean bigint - - - Number of times the background writer stopped a cleaning - scan because it had written too many buffers - + + + waits bigint + + + Number of times a lock of this type had to wait because of a + conflicting lock. Only incremented when the lock was successfully + acquired after waiting longer than . + + - - buffers_alloc bigint - - - Number of buffers allocated - + + + wait_time bigint + + + Total time spent waiting for locks of this type, in milliseconds. + Only incremented when the lock was successfully acquired after waiting + longer than . + + - - stats_reset timestamp with time zone - - - Time at which these statistics were last reset - - - + + + fastpath_exceeded bigint + + + Number of times a lock of this type could not be acquired via fast path + because the fast path slot limit was exceeded. Increasing + can reduce this number. + + + + + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset. + + + + + +
+
+ + + <structname>pg_stat_bgwriter</structname> + + + pg_stat_bgwriter + + + + The pg_stat_bgwriter view will always have a + single row, containing data about the background writer of the cluster. + + + + <structname>pg_stat_bgwriter</structname> View + + + + + Column Type + + + Description + + + + + + + + buffers_clean bigint + + + Number of buffers written by the background writer + + + + + + maxwritten_clean bigint + + + Number of times the background writer stopped a cleaning + scan because it had written too many buffers + + + + + + buffers_alloc bigint + + + Number of buffers allocated + + + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + +
@@ -3297,6 +3667,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + wal_fpi_bytes numeric + + + Total amount of WAL full page images in bytes + + + wal_buffers_full bigint @@ -3516,9 +3895,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage Number of data page checksum failures detected in this - database (or on a shared object), or NULL if data checksums are - disabled. - + database (or on a shared object). Detected failures are not reset if + the setting changes. Clusters + which are initialized without data checksums will show this as + 0. In PostgreSQL version + 18 and earlier, this was set to NULL for clusters + with data checksums disabled. + + @@ -3527,8 +3911,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage Time at which the last data page checksum failure was detected in - this database (or on a shared object), or NULL if data checksums are - disabled. + this database (or on a shared object). Last failure is reported + regardless of the setting. @@ -3771,6 +4155,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage on the primary + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + @@ -3980,6 +4373,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage Estimated number of rows inserted since this table was last vacuumed + (not counting VACUUM FULL) @@ -4066,7 +4460,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage total_vacuum_time double precision - Total time this table has been manually vacuumed, in milliseconds. + Total time this table has been manually vacuumed, in milliseconds + (not counting VACUUM FULL). (This includes the time spent sleeping due to cost-based delays.) @@ -4102,12 +4497,190 @@ description | Waiting for a newly initialized WAL file to reach durable storage cost-based delays.) + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + +
+ + <structname>pg_stat_autovacuum_scores</structname> + + + pg_stat_autovacuum_scores + + + + The pg_stat_autovacuum_scores view will contain one + row for each table in the current database (including TOAST tables), showing + the current autovacuum scores for that specific table. Autovacuum + prioritizes tables deemed eligible for processing based on their + score, with higher scores indicating higher + priority. See for more information. + + + + While this view generates its results the same way that autovacuum workers + do, it does so using the current source information, which might differ from + the source information that an autovacuum worker sees when it gathers its + list of tables to process. Therefore, this view is not a completely + reliable indicator of which tables autovacuum will process and what order it + will process them. + + + + <structname>pg_stat_autovacuum_scores</structname> View + + + + + Column Type + + + Description + + + + + + + + relid oid + + + Oid of the table. + + + + + + schemaname name + + + Name of the schema that the table is in. + + + + + + relname name + + + Name of the table. + + + + + + score double precision + + + Maximum value of all component scores. This is the value that + autovacuum would use to sort the list of tables to process. + + + + + + xid_score double precision + + + Transaction ID age component score. Scores greater than or equal to + indicate that + autovacuum would vacuum the table for transaction ID wraparound + prevention. + + + + + + mxid_score double precision + + + Multixact ID age component score. Scores greater than or equal to + indicate + that autovacuum would vacuum the table for multixact ID wraparound + prevention. + + + + + + vacuum_score double precision + + + Vacuum component score. Scores greater than or equal to + indicate that + autovacuum would vacuum the table (unless autovacuum is disabled). + + + + + + vacuum_insert_score double precision + + + Vacuum insert component score. Scores greater than or equal to + indicate + that autovacuum would vacuum the table (unless autovacuum is disabled). + + + + + + analyze_score double precision + + + Analyze component score. Scores greater than or equal to + indicate that + autovacuum would analyze the table (unless autovacuum is disabled). + + + + + + do_vacuum bool + + + Whether autovacuum would vacuum the table. Note that even if the + component scores indicate that autovacuum would vacuum the table, this + may be false if autovacuum is disabled. + + + + + + do_analyze bool + + + Whether autovacuum would analyze the table. Note that even if the + component scores indicate that autovacuum would analyze the table, this + may be false if autovacuum is disabled. + + + + + + for_wraparound bool + + + Whether autovacuum would vacuum the table for wraparound prevention. + + + + +
+
+ <structname>pg_stat_all_indexes</structname> @@ -4222,6 +4795,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage index + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + @@ -4419,6 +5001,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage Number of buffer hits in this table's TOAST table indexes (if any) + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + @@ -4519,14 +5110,23 @@ description | Waiting for a newly initialized WAL file to reach durable storage Number of buffer hits in this index - - - - - - - - <structname>pg_statio_all_sequences</structname> + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + + + + + + + + + <structname>pg_statio_all_sequences</structname> pg_statio_all_sequences @@ -4597,6 +5197,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage Number of buffer hits in this sequence + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + @@ -4687,6 +5296,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage other functions called by it, in milliseconds + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + @@ -5042,6 +5660,12 @@ description | Waiting for a newly initialized WAL file to reach durable storage pg_stat_io view. + + + lock: Reset all the counters shown in the + pg_stat_lock view. + + recovery_prefetch: Reset all the counters shown in @@ -5085,6 +5709,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage Resets statistics for a single table or index in the current database or shared across all databases in the cluster to zero. + It also resets statistics for a single sequence or materialized view + in the current database. This function is restricted to superusers by default, but other users @@ -5492,9 +6118,9 @@ FROM pg_stat_get_backend_idset() AS backendid; PostgreSQL has the ability to report the progress of certain commands during command execution. Currently, the only commands which support progress reporting are ANALYZE, - CLUSTER, - CREATE INDEX, VACUUM, - COPY, + COPY, CREATE INDEX, + REPACK (and its obsolete spelling CLUSTER), + VACUUM, and (i.e., replication command that issues to take a base backup). @@ -5649,11 +6275,36 @@ FROM pg_stat_get_backend_idset() AS backendid; Total time spent sleeping due to cost-based delay (see - , in milliseconds + ), in milliseconds (if is enabled, otherwise zero). + + + + started_by text + + + Shows what caused the current ANALYZE operation to be + started. Possible values are: + + + + manual: The analyze was started by an explicit + ANALYZE, or by VACUUM with + the option. + + + + + autovacuum: The analyze was started by an + autovacuum worker. + + + + + @@ -5738,8 +6389,10 @@ FROM pg_stat_get_backend_idset() AS backendid; - Whenever CLUSTER or VACUUM FULL is - running, the pg_stat_progress_cluster view will + Whenever REPACK, CLUSTER or + VACUUM FULL is running, + the backwards-compatibility pg_stat_progress_cluster + view will contain a row for each backend that is currently running either command. The tables below describe the information that will be reported and provide information about how to interpret it. @@ -5801,7 +6454,11 @@ FROM pg_stat_get_backend_idset() AS backendid; command text - The command that is running. Either CLUSTER or VACUUM FULL. + The command that is running. Either CLUSTER or + VACUUM FULL. + Because this view exists for backwards-compatibility purposes only, + it will translate any REPACK command into one of + these other two. @@ -6088,8 +6745,8 @@ FROM pg_stat_get_backend_idset() AS backendid; Number of tuples skipped because they contain malformed data. - This counter only advances when a value other than - stop is specified to the ON_ERROR + This counter only advances when + ignore is specified to the ON_ERROR option. @@ -6402,6 +7059,257 @@ FROM pg_stat_get_backend_idset() AS backendid; + + REPACK Progress Reporting + + + pg_stat_progress_repack + + + + Whenever REPACK is running, + the pg_stat_progress_repack view will contain a + row for each backend that is currently running the command. The tables + below describe the information that will be reported and provide + information about how to interpret it. + + + + <structname>pg_stat_progress_repack</structname> View + + + + + Column Type + + + Description + + + + + + + + pid integer + + + Process ID of backend. + + + + + + datid oid + + + OID of the database to which this backend is connected. + + + + + + datname name + + + Name of the database to which this backend is connected. + + + + + + relid oid + + + OID of the table being repacked. + + + + + + command text + + + The command that is running. Either REPACK or + VACUUM FULL, or CLUSTER. + + + + + + phase text + + + Current processing phase. See . + + + + + + repack_index_relid oid + + + If the table is being scanned using an index, this is the OID of the + index being used; otherwise, it is zero. + + + + + + heap_tuples_scanned bigint + + + Number of heap tuples scanned. + This counter only advances when the phase is + seq scanning heap, + index scanning heap + or writing new heap. + + + + + + heap_tuples_inserted bigint + + + Number of heap tuples inserted. + This counter only advances when the phase is + seq scanning heap, + index scanning heap, + writing new heap + or catch-up. + + + + + + heap_tuples_updated bigint + + + Number of heap tuples updated. + This counter only advances when the phase is catch-up. + + + + + + heap_tuples_deleted bigint + + + Number of heap tuples deleted. + This counter only advances when the phase is catch-up. + + + + + + heap_blks_total bigint + + + Total number of heap blocks in the table. This number is reported + as of the beginning of seq scanning heap. + + + + + + heap_blks_scanned bigint + + + Number of heap blocks scanned. This counter only advances when the + phase is seq scanning heap. + + + + + + index_rebuild_count bigint + + + Number of indexes rebuilt. This counter only advances when the phase + is rebuilding index. + + + + +
+ + + REPACK Phases + + + + + + Phase + Description + + + + + + initializing + + The command is preparing to begin scanning the heap. This phase is + expected to be very brief. + + + + seq scanning heap + + The command is currently scanning the table using a sequential scan. + + + + index scanning heap + + REPACK is currently scanning the table using an index scan. + + + + sorting tuples + + REPACK is currently sorting tuples. + + + + writing new heap + + REPACK is currently writing the new heap. + + + + catch-up + + REPACK CONCURRENTLY is currently processing the DML + commands that other transactions executed during any of the preceding + phases. + + + + swapping relation files + + The command is currently swapping newly-built files into place. + + + + rebuilding index + + The command is currently rebuilding an index. + + + + performing final cleanup + + The command is performing final cleanup. When this phase is + completed, REPACK will end. + + + + +
+
+ VACUUM Progress Reporting @@ -6594,6 +7502,81 @@ FROM pg_stat_get_backend_idset() AS backendid; stale. + + + + mode text + + + The mode in which the current VACUUM operation is + running. See for details of each + mode. Possible values are: + + + + normal: The operation is performing a standard + vacuum. It is neither required to run in aggressive mode nor operating + in failsafe mode. + + + + + aggressive: The operation is running an aggressive + vacuum, which must scan every page that is not marked all-frozen. + The parameters and + determine when a + table requires aggressive vacuuming. + + + + + failsafe: The vacuum has entered failsafe mode, + in which it performs only the minimum work necessary to avoid + transaction ID or multixact ID wraparound failure. + The parameters and + determine when the + vacuum enters failsafe mode. The vacuum may start in this mode or + switch to it while running; the value of the + mode column may transition from another + mode to failsafe during the operation. + + + + + + + + + started_by text + + + Shows what caused the current VACUUM operation to be + started. Possible values are: + + + + manual: The vacuum was started by an explicit + VACUUM command. + + + + + autovacuum: The vacuum was started by an autovacuum + worker. Vacuums run by autovacuum workers may be interrupted due to + lock conflicts. + + + + + autovacuum_wraparound: The vacuum was started by an + autovacuum worker to prevent transaction ID or multixact ID + wraparound. Vacuums run for wraparound protection are not interrupted + due to lock conflicts. + + + + + @@ -6778,6 +7761,16 @@ FROM pg_stat_get_backend_idset() AS backendid; advances when the phase is streaming database files. + + + + backup_type text + + + Backup type. Either full or + incremental. + + @@ -6854,6 +7847,219 @@ FROM pg_stat_get_backend_idset() AS backendid; + + Data Checksum Progress Reporting + + + pg_stat_progress_data_checksums + + + + When data checksums are being enabled on a running cluster, the + pg_stat_progress_data_checksums view will contain + a row for the launcher process, and one row for each worker process which + is currently calculating and writing checksums for the data pages in a database. + The launcher provides overview of the overall progress (how many databases + have been processed, how many remain), while the workers track progress for + currently processed databases. + + + + <structname>pg_stat_progress_data_checksums</structname> View + + + + + + Column Type + + + Description + + + + + + + + + + pid integer + + + Process ID of the data checksum process, launcher or worker. + + + + + + + + datid oid + + + OID of this database, or 0 for the launcher process. + + + + + + + + datname name + + + Name of this database, or NULL for the + launcher process. + + + + + + + + phase text + + + Current processing phase, see + for description of the phases. + + + + + + + + databases_total integer + + + The total number of databases which will be processed. Only the + launcher process has this value set, the worker processes have this + set to NULL. + + + + + + + + databases_done integer + + + The number of databases which have been processed. Only the launcher + process has this value set, the worker processes have this set to + NULL. + + + + + + + + relations_total integer + + + The total number of relations which will be processed, or + NULL if the worker process hasn't + calculated the number of relations yet. The launcher process has + this set to NULL since it isn't responsible for + processing relations, only launching worker processes. + + + + + + + + relations_done integer + + + The number of relations which have been processed. The launcher + process has this set to NULL. + + + + + + + + blocks_total integer + + + The number of blocks in the current relation which will be processed, + or NULL if the worker process hasn't + calculated the number of blocks yet. The launcher process has + this set to NULL. + + + + + + + + blocks_done integer + + + The number of blocks in the current relation which have been processed. + The launcher process has this set to NULL. + + + + + + +
+ + + Data Checksum Phases + + + + + + Phase + Description + + + + + enabling + + The command is currently enabling data checksums on the cluster. + + + + disabling + + The command is currently disabling data checksums on the cluster. + + + + done + + The command is done and the data checksum state in the cluster has + changed. + + + + waiting on barrier + + The command is currently waiting for the current active backends to + acknowledge the change in data checksum state. + + + + waiting on temporary tables + + The command is currently waiting for all temporary tables which existed + at the time the command was started to be removed. + + + + +
+
+ diff --git a/doc/src/sgml/mvcc.sgml b/doc/src/sgml/mvcc.sgml index 049ee75a4ba3b..241caeb3593b9 100644 --- a/doc/src/sgml/mvcc.sgml +++ b/doc/src/sgml/mvcc.sgml @@ -366,6 +366,18 @@ conventionally visible to the command. + + INSERT with an ON CONFLICT DO + SELECT clause behaves similarly to ON CONFLICT DO + UPDATE. In Read Committed mode, each row proposed for insertion + is guaranteed to either insert or return the conflicting row (unless there are + unrelated errors). If a conflict originates in another transaction whose + effects are not yet visible to the INSERT, the command + will wait for that transaction to commit or roll back, then return the + conflicting row if it was committed (even though that row was not visible + when the command started). + + INSERT with an ON CONFLICT DO NOTHING clause may have insertion not proceed for a row due to @@ -1833,15 +1845,17 @@ SELECT pg_advisory_lock(q.id) FROM Caveats - Some DDL commands, currently only TRUNCATE and the - table-rewriting forms of ALTER TABLE, are not + Some commands, currently only TRUNCATE, the + table-rewriting forms of ALTER + TABLE and REPACK with + the CONCURRENTLY option, are not MVCC-safe. This means that after the truncation or rewrite commits, the table will appear empty to concurrent transactions, if they are using a - snapshot taken before the DDL command committed. This will only be an + snapshot taken before the command committed. This will only be an issue for a transaction that did not access the table in question - before the DDL command started — any transaction that has done so + before the command started — any transaction that has done so would hold at least an ACCESS SHARE table lock, - which would block the DDL command until that transaction completes. + which would block the truncating or rewriting command until that transaction completes. So these commands will not cause any apparent inconsistency in the table contents for successive queries on the target table, but they could cause visible inconsistency between the contents of the target diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml index 704089dd7b3cb..7c9e3dd931a85 100644 --- a/doc/src/sgml/oauth-validators.sgml +++ b/doc/src/sgml/oauth-validators.sgml @@ -192,11 +192,20 @@ Logging - Modules may use the same logging + To simply log the reason for a validation failure, modules may set the + freeform error_detail field during the + validate callback. + ( has guidelines for writing good + DETAIL messages.) error_detail + is printed only to the server log, as part of the final authentication + failure message, and it is not shared with the client. + + + Modules may also use the same logging facilities as standard extensions; however, the rules for emitting log entries to the client are subtly different during the authentication phase of the connection. Generally speaking, modules should log - verification problems at the COMMERROR level and return + problems at the COMMERROR level and return normally, instead of using ERROR/FATAL to unwind the stack, to avoid leaking information to unauthenticated clients. @@ -242,6 +251,11 @@ delegate_ident_mapping=1 mode, and what additional configuration is required in order to do so. + + If an implementation provides custom + HBA options, the names and syntax of those options should be + documented as well. + @@ -334,7 +348,8 @@ typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void); Startup Callback The startup_cb callback is executed directly after - loading the module. This callback can be used to set up local state and + loading the module. This callback can be used to set up local state, + define custom HBA options, and perform additional initialization if required. If the validator module has state it can use state->private_data to store it. @@ -370,6 +385,7 @@ typedef struct ValidatorModuleResult { bool authorized; char *authn_id; + char *error_detail; } ValidatorModuleResult; @@ -387,6 +403,15 @@ typedef struct ValidatorModuleResult Otherwise the validator should return true to indicate that it has processed the token and made an authorization decision. + + In either failure case (validation error or internal error) the module may + store a user-readable reason for the failure in result->error_detail. + This will be printed to the server logs (not sent to the client) as a + DETAIL entry for the authentication failure. The memory + pointed to by error_detail may be either palloc'd + or of static duration. error_detail is ignored + on success. + The behavior after validate_cb returns depends on the specific HBA setup. Normally, the result->authn_id user @@ -413,4 +438,217 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
+ + + Custom HBA Options + + + Like other preloaded libraries, validator modules may define + custom GUC parameters for user + configuration in postgresql.conf. However, it may be + desirable to configure behavior at a more granular level (say, for a + particular issuer or a group of users) instead of globally. + + + + Beginning in PostgreSQL 19, validator + implementations may define custom options for use inside + pg_hba.conf. These options are then + made available to the user + as validator.option. The API + for registering and retrieving custom options is described below. + + + + Options API + + Modules register custom HBA option names during the startup_cb + callback, using RegisterOAuthHBAOptions(): + + +/* + * Register a list of custom option names for use in pg_hba.conf. For each name + * "foo" registered here, that option will be provided as "validator.foo" in + * the HBA. + * + * Valid option names consist of alphanumeric ASCII, underscore (_), and hyphen + * (-). Invalid option names will be ignored with a WARNING logged at + * connection time. + * + * This function may only be called during the startup_cb callback. Multiple + * calls are permitted, which will append to the existing list of registered + * options; options cannot be unregistered. + * + * Parameters: + * + * - state: the state pointer passed to the startup_cb callback + * - num: the number of options in the opts array + * - opts: an array of null-terminated option names to register + * + * The list of option names is copied internally, and the opts array is not + * required to remain valid after the call. + */ +void RegisterOAuthHBAOptions(ValidatorModuleState *state, int num, + const char *opts[]); + + + + + Each option's value, if set, may be later retrieved using + GetOAuthHBAOption(): + + +/* + * Retrieve the string value of an HBA option which was registered via + * RegisterOAuthHBAOptions(). Usable only during validate_cb or shutdown_cb. + * + * If the user has set the corresponding option in pg_hba.conf, this function + * returns that value as a null-terminated string, which must not be modified + * or freed. NULL is returned instead if the user has not set this option, if + * the option name was not registered, or if this function is incorrectly called + * during the startup_cb. + * + * Parameters: + * + * - state: the state pointer passed to the validate_cb/shutdown_cb callback + * - optname: the name of the option to retrieve + */ +const char *GetOAuthHBAOption(const ValidatorModuleState *state, + const char *optname); + + + + + See for sample usage. + + + + + Limitations + + + + + Option names are limited to ASCII alphanumeric characters, + underscores (_), and hyphens (-). + + + + + Option values are always freeform strings (in contrast to custom GUCs, + which support numerics, booleans, and enums). + + + + + Option names and values cannot be checked by the server during a reload of + the configuration. Any unregistered options in pg_hba.conf + will instead result in connection failures. It is the responsibility of + each module to document and verify the syntax of option values as needed. + + + If a module finds an invalid option value during validate_cb, + it's recommended to signal + an internal error by setting result->error_detail + to a description of the problem and returning false. + + + + + + + + + + Example Usage + + + For a hypothetical module, the options foo and + bar could be registered as follows: + + +static void +validator_startup(ValidatorModuleState *state) +{ + static const char *opts[] = { + "foo", /* description of access privileges */ + "bar", /* magic URL for additional administrator powers */ + }; + + RegisterOAuthHBAOptions(state, lengthof(opts), opts); + + /* ...other setup... */ +} + + + + + The following sample entries in pg_hba.conf can then + make use of these options: + + +# TYPE DATABASE USER ADDRESS METHOD +hostssl postgres admin 0.0.0.0/0 oauth issuer=https://admin.example.com \ + scope="pg-admin openid email" \ + map=oauth-email \ + validator.foo="admin access" \ + validator.bar=https://magic.example.com + +hostssl postgres all 0.0.0.0/0 oauth issuer=https://www.example.com \ + scope="pg-user openid email" \ + map=oauth-email \ + validator.foo="user access" + + + + + The module can retrieve the option settings from the HBA during validation: + + +static bool +validate_token(const ValidatorModuleState *state, + const char *token, const char *role, + ValidatorModuleResult *res) +{ + const char *foo = GetOAuthHBAOption(state, "foo"); /* "admin access" or "user access" */ + const char *bar = GetOAuthHBAOption(state, "bar"); /* "https://magic.example.com" or NULL */ + + if (bar && !is_valid_url(bar)) + { + res->error_detail = psprintf("validator.bar (\"%s\") is not a valid URL.", bar); + return false; + } + + /* proceed to validate token */ +} + + + + + When multiple validators are in use, their registered option lists remain + independent: + + +in postgresql.conf: +oauth_validator_libraries = 'example_org, my_validator' + +in pg_hba.conf: +# TYPE DATABASE USER ADDRESS METHOD +hostssl postgres admin 0.0.0.0/0 oauth issuer=https://admin.example.com \ + scope="pg-admin openid email" \ + map=oauth-email \ + validator=my_validator \ + validator.foo="admin access" \ + validator.bar=https://magic.example.com + +hostssl postgres all 0.0.0.0/0 oauth issuer=https://www.example.org \ + scope="pg-user openid profile" \ + validator=example_org \ + delegate_ident_mapping=1 \ + validator.magic=on \ + validator.more_magic=off + + + + diff --git a/doc/src/sgml/oid2name.sgml b/doc/src/sgml/oid2name.sgml index 54cc9be2b8278..9340d7376aae2 100644 --- a/doc/src/sgml/oid2name.sgml +++ b/doc/src/sgml/oid2name.sgml @@ -118,7 +118,7 @@ display more information about each object shown: tablespace name, - schema name, and OID. + schema name, OID and path. @@ -299,10 +299,10 @@ From database "alvherre": $ # you can mix the options, and get more details with -x $ oid2name -d alvherre -t accounts -f 1155291 -x From database "alvherre": - Filenode Table Name Oid Schema Tablespace ------------------------------------------------------- - 155173 accounts 155173 public pg_default - 1155291 accounts_pkey 1155291 public pg_default + Filenode Table Name Oid Schema Tablespace Path +-------------------------------------------------------------------------- + 155173 accounts 155173 public pg_default base/17228/155173 + 1155291 accounts_pkey 1155291 public pg_default base/17228/1155291 $ # show disk space for every db object $ du [0-9]* | diff --git a/doc/src/sgml/pageinspect.sgml b/doc/src/sgml/pageinspect.sgml index 487c5d758ffbf..3a113439e1dc4 100644 --- a/doc/src/sgml/pageinspect.sgml +++ b/doc/src/sgml/pageinspect.sgml @@ -73,9 +73,9 @@ passed as argument. For example: test=# SELECT * FROM page_header(get_raw_page('pg_class', 0)); - lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid ------------+----------+--------+-------+-------+---------+----------+---------+----------- - 0/24A1B50 | 0 | 1 | 232 | 368 | 8192 | 8192 | 4 | 0 + lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid +------------+----------+--------+-------+-------+---------+----------+---------+----------- + 0/024A1B50 | 0 | 1 | 232 | 368 | 8192 | 8192 | 4 | 0 The returned columns correspond to the fields in the PageHeaderData struct. @@ -741,9 +741,9 @@ test=# SELECT first_tid, nbytes, tids[0:5] AS some_tids For example: test=# SELECT * FROM gist_page_opaque_info(get_raw_page('test_gist_idx', 2)); - lsn | nsn | rightlink | flags ------+-----+-----------+-------- - 0/1 | 0/0 | 1 | {leaf} + lsn | nsn | rightlink | flags +------------+------------+-----------+-------- + 0/0B5FE088 | 0/00000000 | 1 | {leaf} (1 row) @@ -932,8 +932,8 @@ test=# SELECT * FROM hash_bitmap_info('con_hash_index', 2052); test=# SELECT magic, version, ntuples, ffactor, bsize, bmsize, bmshift, test-# maxbucket, highmask, lowmask, ovflpoint, firstfree, nmaps, procid, -test-# regexp_replace(spares::text, '(,0)*}', '}') as spares, -test-# regexp_replace(mapp::text, '(,0)*}', '}') as mapp +test-# regexp_replace(spares::text, '(,0)*}', '}') AS spares, +test-# regexp_replace(mapp::text, '(,0)*}', '}') AS mapp test-# FROM hash_metapage_info(get_raw_page('con_hash_index', 0)); -[ RECORD 1 ]-------------------------------------------------&zwsp;------------------------------ magic | 105121344 diff --git a/doc/src/sgml/parallel.sgml b/doc/src/sgml/parallel.sgml index 1ce9abf86f525..af43484703eb0 100644 --- a/doc/src/sgml/parallel.sgml +++ b/doc/src/sgml/parallel.sgml @@ -299,6 +299,15 @@ EXPLAIN SELECT * FROM pgbench_accounts WHERE filler LIKE '%x%'; within each worker process. + + + In a parallel tid range scan, the range of blocks + will be subdivided into smaller ranges which are shared among the + cooperating processes. Each worker process will complete the scanning + of its given range of blocks before requesting an additional range of + blocks. + + Other scan types, such as scans of non-btree indexes, may support diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml index 106583fb2965d..604e8578a8dcd 100644 --- a/doc/src/sgml/perform.sgml +++ b/doc/src/sgml/perform.sgml @@ -758,8 +758,64 @@ WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2; values shown are averages per-execution. This is done to make the numbers comparable with the way that the cost estimates are shown. Multiply by the loops value to get the total time actually spent in - the node. In the above example, we spent a total of 0.030 milliseconds - executing the index scans on tenk2. + the node and the total number of rows processed by the node across all + executions. In the above example, we spent a total of 0.030 milliseconds + executing the index scans on tenk2, and they handled a + total of 10 rows. + + + + Parallel execution will also cause nodes to be executed more than once. + This is also reported with the loops value. We can + change some planner settings to make the planner pick a parallel plan for + the above query: + + + +SET min_parallel_table_scan_size = 0; +SET parallel_tuple_cost = 0; +SET parallel_setup_cost = 0; + +EXPLAIN ANALYZE SELECT * +FROM tenk1 t1, tenk2 t2 +WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2; + QUERY PLAN +-------------------------------------------------------------------&zwsp;-------------------------------------------------------------------&zwsp;---- + Gather (cost=4.65..70.96 rows=10 width=488) (actual time=1.161..11.655 rows=10.00 loops=1) + Workers Planned: 2 + Workers Launched: 2 + Buffers: shared hit=78 read=6 + -> Nested Loop (cost=4.65..70.96 rows=4 width=488) (actual time=0.247..0.317 rows=3.33 loops=3) + Buffers: shared hit=78 read=6 + -> Parallel Bitmap Heap Scan on tenk1 t1 (cost=4.36..39.31 rows=4 width=244) (actual time=0.228..0.249 rows=3.33 loops=3) + Recheck Cond: (unique1 < 10) + Heap Blocks: exact=10 + Buffers: shared hit=54 + -> Bitmap Index Scan on tenk1_unique1 (cost=0.00..4.36 rows=10 width=0) (actual time=0.438..0.439 rows=10.00 loops=1) + Index Cond: (unique1 < 10) + Index Searches: 1 + Buffers: shared hit=2 + -> Index Scan using tenk2_unique2 on tenk2 t2 (cost=0.29..7.90 rows=1 width=244) (actual time=0.016..0.017 rows=1.00 loops=10) + Index Cond: (unique2 = t1.unique2) + Index Searches: 10 + Buffers: shared hit=24 read=6 + Planning: + Buffers: shared hit=327 read=3 + Planning Time: 4.781 ms + Execution Time: 11.858 ms +(22 rows) + + + + The parallel bitmap heap scan was split into three separate + executions: one in the leader (since + is on by default), + and one in each of the two launched workers. Similarly to sequential + repeated executions, rows and actual time are averages per-worker. + Multiply by the loops value to get the total number + of rows processed by the node across all workers. The total time + spent in all workers can be calculated similarly, but since this time + is spent concurrently, it is not equivalent to total elapsed time. @@ -1485,12 +1541,27 @@ CREATE STATISTICS stts (dependencies) ON city, zip FROM zipcodes; ANALYZE zipcodes; -SELECT stxname, stxkeys, stxddependencies - FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid) +SELECT stxkeys AS k, jsonb_pretty(stxddependencies::text::jsonb) AS dep + FROM pg_statistic_ext JOIN pg_statistic_ext_data ON (oid = stxoid) WHERE stxname = 'stts'; - stxname | stxkeys | stxddependencies ----------+---------+------------------------------------------ - stts | 1 5 | {"1 => 5": 1.000000, "5 => 1": 0.423130} +-[ RECORD 1 ]-------------------- +k | 1 5 +dep | [ + + | { + + | "degree": 1.000000,+ + | "attributes": [ + + | 1 + + | ], + + | "dependency": 5 + + | }, + + | { + + | "degree": 0.423130,+ + | "attributes": [ + + | 5 + + | ], + + | "dependency": 1 + + | } + + | ] (1 row) Here it can be seen that column 1 (zip code) fully determines column @@ -1576,12 +1647,42 @@ CREATE STATISTICS stts2 (ndistinct) ON city, state, zip FROM zipcodes; ANALYZE zipcodes; -SELECT stxkeys AS k, stxdndistinct AS nd - FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid) +SELECT stxkeys AS k, jsonb_pretty(stxdndistinct::text::jsonb) AS nd + FROM pg_statistic_ext JOIN pg_statistic_ext_data on (oid = stxoid) WHERE stxname = 'stts2'; --[ RECORD 1 ]------------------------------------------------------&zwsp;-- +-[ RECORD 1 ]------------------- k | 1 2 5 -nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178} +nd | [ + + | { + + | "ndistinct": 33178,+ + | "attributes": [ + + | 1, + + | 2 + + | ] + + | }, + + | { + + | "ndistinct": 33178,+ + | "attributes": [ + + | 1, + + | 5 + + | ] + + | }, + + | { + + | "ndistinct": 27435,+ + | "attributes": [ + + | 2, + + | 5 + + | ] + + | }, + + | { + + | "ndistinct": 33178,+ + | "attributes": [ + + | 1, + + | 2, + + | 5 + + | ] + + | } + + | ] (1 row) This indicates that there are three combinations of columns that diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml index 537d601494242..1e9aee10275f2 100644 --- a/doc/src/sgml/pgbuffercache.sgml +++ b/doc/src/sgml/pgbuffercache.sgml @@ -19,10 +19,18 @@ pg_buffercache_pages + + pg_buffercache_numa + + pg_buffercache_summary + + pg_buffercache_usage_counts + + pg_buffercache_evict @@ -35,16 +43,32 @@ pg_buffercache_evict_all + + pg_buffercache_mark_dirty + + + + pg_buffercache_mark_dirty_relation + + + + pg_buffercache_mark_dirty_all + + This module provides the pg_buffercache_pages() - function (wrapped in the pg_buffercache view), - pg_buffercache_numa_pages() function (wrapped in the - pg_buffercache_numa view), the + function (wrapped in the pg_buffercache view), the + pg_buffercache_os_pages() function (wrapped in the + pg_buffercache_os_pages and + pg_buffercache_numa views), the pg_buffercache_summary() function, the pg_buffercache_usage_counts() function, the - pg_buffercache_evict(), the - pg_buffercache_evict_relation() function and the - pg_buffercache_evict_all() function. + pg_buffercache_evict() function, the + pg_buffercache_evict_relation() function, the + pg_buffercache_evict_all() function, the + pg_buffercache_mark_dirty() function, the + pg_buffercache_mark_dirty_relation() function and the + pg_buffercache_mark_dirty_all() function. @@ -55,12 +79,16 @@ - The pg_buffercache_numa_pages() provides - NUMA node mappings for shared buffer entries. This - information is not part of pg_buffercache_pages() - itself, as it is much slower to retrieve. - The pg_buffercache_numa view wraps the function for - convenient use. + The pg_buffercache_os_pages() function provides OS + pages mappings for shared buffer entries. When its argument is + true, it also provides NUMA node + mappings for shared buffer entries (this information is not part of + pg_buffercache_pages() itself, as it is much + slower to retrieve). + The pg_buffercache_os_pages and + pg_buffercache_numa views wrap the function for + convenient use, with its argument set to false and + true respectively. @@ -99,6 +127,25 @@ function is restricted to superusers only. + + The pg_buffercache_mark_dirty() function allows a block + to be marked as dirty in the buffer pool given a buffer identifier. Use of + this function is restricted to superusers only. + + + + The pg_buffercache_mark_dirty_relation() function + allows all unpinned shared buffers in the relation to be marked as dirty in + the buffer pool given a relation identifier. Use of this function is + restricted to superusers only. + + + + The pg_buffercache_mark_dirty_all() function allows all + unpinned shared buffers to be marked as dirty in the buffer pool. Use of + this function is restricted to superusers only. + + The <structname>pg_buffercache</structname> View @@ -234,6 +281,53 @@ + + The <structname>pg_buffercache_os_pages</structname> View + + + The definitions of the columns exposed by the view are shown in + . + + + + <structname>pg_buffercache_os_pages</structname> Columns + + + + + Column Type + + + Description + + + + + + + + bufferid integer + + + ID, in the range 1..shared_buffers + + + + + + os_page_num bigint + + + Number of OS memory page for this buffer + + + + + +
+ +
+ The <structname>pg_buffercache_numa</structname> View @@ -270,7 +364,7 @@ os_page_num bigint - number of OS memory page for this buffer + Number of OS memory page for this buffer @@ -476,10 +570,12 @@ The pg_buffercache_evict() function takes a buffer identifier, as shown in the bufferid column of the pg_buffercache view. It returns information - about whether the buffer was evicted and flushed. The buffer_evicted + about whether the buffer was evicted and flushed. + The buffer_evicted column is true on success, and false if the buffer wasn't valid, if it couldn't be evicted because it was pinned, or if it became dirty again - after an attempt to write it out. The buffer_flushed column is true if the + after an attempt to write it out. + The buffer_flushed column is true if the buffer was flushed. This does not necessarily mean that buffer was flushed by us, it might be flushed by someone else. The result is immediately out of date upon return, as the buffer might become valid again at any time due @@ -489,7 +585,7 @@ - The <structname>pg_buffercache_evict_relation</structname> Function + The <function>pg_buffercache_evict_relation()</function> Function The pg_buffercache_evict_relation() function is very similar to the pg_buffercache_evict() function. The @@ -507,7 +603,7 @@ - The <structname>pg_buffercache_evict_all</structname> Function + The <function>pg_buffercache_evict_all()</function> Function The pg_buffercache_evict_all() function is very similar to the pg_buffercache_evict() function. The @@ -522,6 +618,61 @@ + + The <function>pg_buffercache_mark_dirty()</function> Function + + The pg_buffercache_mark_dirty() function takes a + buffer identifier, as shown in the bufferid + column of the pg_buffercache view. It returns + information about whether the buffer was marked as dirty. + The buffer_dirtied column is true on success, + and false if the buffer was already dirty if the buffer was not valid or + if it could not be marked as dirty because it was pinned. + The buffer_already_dirty column is true if + the buffer couldn't be marked as dirty because it was already dirty. The + result is immediately out of date upon return, as the buffer might become + valid again at any time due to concurrent activity. The function is + intended for developer testing only. + + + + + The <function>pg_buffercache_mark_dirty_relation()</function> Function + + The pg_buffercache_mark_dirty_relation() function is + very similar to the + pg_buffercache_mark_dirty() function. + The difference is that the + pg_buffercache_mark_dirty_relation() function takes a + relation identifier instead of buffer identifier. It tries to mark all + buffers dirty for all forks in that relation. + It returns the number of buffers marked as dirty, the number of buffers + already dirty and the number of buffers skipped because already pinned or + invalid. + The result is immediately out of date upon return, as the buffer might + become valid again at any time due to concurrent activity. The function is + intended for developer testing only. + + + + + The <function>pg_buffercache_mark_dirty_all()</function> Function + + The pg_buffercache_mark_dirty_all() function is + very similar to the pg_buffercache_mark_dirty() + function. + The difference is that the + pg_buffercache_mark_dirty_all() tries to mark all + buffers dirty in the buffer pool. + It returns the number of buffers marked as dirty, the number of buffers + already dirty and the number of buffers skipped because already pinned or + invalid. + The result is immediately out of date upon return, as the buffer might + become valid again at any time due to concurrent activity. The function is + intended for developer testing only. + + + Sample Output @@ -550,6 +701,46 @@ regression=# SELECT n.nspname, c.relname, count(*) AS buffers public | spgist_text_tbl | 182 (10 rows) +regression=# SELECT pages_per_buffer, COUNT(*) as buffer_count + FROM ( + SELECT bufferid, COUNT(*) as pages_per_buffer + FROM pg_buffercache_os_pages + GROUP BY bufferid + ) + GROUP BY pages_per_buffer + ORDER BY pages_per_buffer; + + pages_per_buffer | buffer_count +------------------+-------------- + 1 | 261120 + 2 | 1024 +(2 rows) + +regression=# SELECT n.nspname, c.relname, count(*) AS buffers_on_multiple_pages + FROM pg_buffercache b JOIN pg_class c + ON b.relfilenode = pg_relation_filenode(c.oid) AND + b.reldatabase IN (0, (SELECT oid FROM pg_database + WHERE datname = current_database())) + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN (SELECT bufferid FROM pg_buffercache_os_pages + GROUP BY bufferid HAVING count(*) > 1) m on m.bufferid = b.bufferid + GROUP BY n.nspname, c.relname + ORDER BY 3 DESC + LIMIT 10; + + nspname | relname | buffers_on_multiple_pages +------------+------------------------------+--------------------------- + public | delete_test_table | 3 + public | gin_test_idx | 2 + pg_catalog | pg_depend | 2 + public | quad_poly_tbl | 2 + pg_catalog | pg_depend_reference_index | 1 + pg_catalog | pg_index_indexrelid_index | 1 + pg_catalog | pg_constraint_contypid_index | 1 + pg_catalog | pg_statistic | 1 + pg_catalog | pg_depend_depender_index | 1 + pg_catalog | pg_operator | 1 +(10 rows) regression=# SELECT * FROM pg_buffercache_summary(); buffers_used | buffers_unused | buffers_dirty | buffers_pinned | usagecount_avg diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml index bc5c74ad017fa..6fc2069ad3ece 100644 --- a/doc/src/sgml/pgcrypto.sgml +++ b/doc/src/sgml/pgcrypto.sgml @@ -57,7 +57,7 @@ digest(data bytea, type text) returns bytea If you want the digest as a hexadecimal string, use encode() on the result. For example: -CREATE OR REPLACE FUNCTION sha1(bytea) returns text AS $$ +CREATE OR REPLACE FUNCTION sha1(bytea) RETURNS text AS $$ SELECT encode(digest($1, 'sha1'), 'hex') $$ LANGUAGE SQL STRICT IMMUTABLE; diff --git a/doc/src/sgml/pglogicalinspect.sgml b/doc/src/sgml/pglogicalinspect.sgml index 4b111f961133b..1c1a9d14e510a 100644 --- a/doc/src/sgml/pglogicalinspect.sgml +++ b/doc/src/sgml/pglogicalinspect.sgml @@ -95,7 +95,7 @@ two_phase_at | 0/40796AF8 initial_xmin_horizon | 0 building_full_snapshot | f in_slot_creation | f -last_serialized_snapshot | 0/0 +last_serialized_snapshot | 0/00000000 next_phase_at | 0 committed_count | 0 committed_xip | @@ -114,7 +114,7 @@ two_phase_at | 0/40796AF8 initial_xmin_horizon | 0 building_full_snapshot | f in_slot_creation | f -last_serialized_snapshot | 0/0 +last_serialized_snapshot | 0/00000000 next_phase_at | 0 committed_count | 0 committed_xip | diff --git a/doc/src/sgml/pgoverexplain.sgml b/doc/src/sgml/pgoverexplain.sgml index 21930fbd3bd76..e399c1cbad5f9 100644 --- a/doc/src/sgml/pgoverexplain.sgml +++ b/doc/src/sgml/pgoverexplain.sgml @@ -8,7 +8,7 @@ - The pg_overexplain extends EXPLAIN + The pg_overexplain module extends EXPLAIN with new options that provide additional output. It is mostly intended to assist with debugging of and development of the planner, rather than for general use. Since this module displays internal details of planner data @@ -17,6 +17,21 @@ often as) those data structures change. + + To use it, simply load it into the server. You can load it into an + individual session: + + +LOAD 'pg_overexplain'; + + + You can also preload it into some or all sessions by including + pg_overexplain in + or + in + postgresql.conf. + + EXPLAIN (DEBUG) @@ -24,8 +39,8 @@ The DEBUG option displays miscellaneous information from the plan tree that is not normally shown because it is not expected to be of general interest. For each individual plan node, it will display the - following fields. See Plan in - nodes/plannodes.h for additional documentation of these + following fields. See Plan in + nodes/plannodes.h for additional documentation of these fields. @@ -67,8 +82,8 @@ Once per query, the DEBUG option will display the - following fields. See PlannedStmt in - nodes/plannodes.h for additional detail. + following fields. See PlannedStmt in + nodes/plannodes.h for additional detail. @@ -82,7 +97,7 @@ Flags. A comma-separated list of Boolean structure - member names from the PlannedStmt that are set to + member names from the PlannedStmt that are set to true. It covers the following structure members: hasReturning, hasModifyingCTE, canSetTag, transientPlan, @@ -162,7 +177,7 @@ table entry (e.g. relation, subquery, or join), followed by the contents of various range table entry fields that are not normally part of - EXPLAIN output. Some of these fields are only displayed + EXPLAIN output. Some of these fields are only displayed for certain kinds of range table entries. For example, Eref is displayed for all types of range table entries, but CTE Name is displayed only for range table entries @@ -171,7 +186,7 @@ For more information about range table entries, see the definition of - RangeTblEntry in nodes/plannodes.h. + RangeTblEntry in nodes/parsenodes.h. diff --git a/doc/src/sgml/pgplanadvice.sgml b/doc/src/sgml/pgplanadvice.sgml new file mode 100644 index 0000000000000..73155461c0bab --- /dev/null +++ b/doc/src/sgml/pgplanadvice.sgml @@ -0,0 +1,825 @@ + + + + pg_plan_advice — help the planner get the right plan + + + pg_plan_advice + + + + The pg_plan_advice module allows key planner decisions + to be described, reproduced, and altered using a special-purpose "plan + advice" mini-language. It is intended to allow stabilization of plan choices + that the user believes to be good, as well as experimentation with plans that + the planner believes to be non-optimal. + + + + Note that, since the planner often makes good decisions, overriding its + judgment can easily backfire. For example, if the distribution of the + underlying data changes, the planner normally has the option to adjust the + plan in an attempt to preserve good performance. If the plan advice prevents + this, a very poor plan may be chosen. It is important to use plan advice + only when the risks of constraining the planner's choices are outweighed by + the benefits. + + + + Getting Started + + + First, you must arrange to load the pg_plan_advice + module. You can do this on a system-wide basis by adding + pg_plan_advice to + and restarting the + server, or by adding it to + and starting a new session, + or by loading it into an individual session using the + LOAD command. + + + + Once the pg_plan_advice module is loaded, + EXPLAIN will support + a PLAN_ADVICE option. You can use this option to see + a plan advice string for the chosen plan. For example: + + + +EXPLAIN (COSTS OFF, PLAN_ADVICE) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------ + Hash Join + Hash Cond: (f.dim_id = d.id) + -> Seq Scan on join_fact f + -> Hash + -> Seq Scan on join_dim d + Generated Plan Advice: + JOIN_ORDER(f d) + HASH_JOIN(d) + SEQ_SCAN(f d) + NO_GATHER(f d) + + + + In this example, the user has not specified any advice; instead, the + planner has been permitted to make whatever decisions it thinks best, and + those decisions are memorialized in the form of an advice string. + JOIN_ORDER(f d) means that f should + be the driving table, and the first table to which it should be joined is + d. HASH_JOIN(d) means that + d should appear on the inner side of a hash join. + SEQ_SCAN(f d) means that both f + and d should be accessed via a sequential scan. + NO_GATHER(f d) means that neither f + nor d should appear beneath a Gather + or Gather Merge node. For more details on the plan + advice mini-language, see the information on + advice targets and + advice tags, below. + + + + Once you have an advice string for a query, you can use it to control how + that query is planned. You can do this by setting + pg_plan_advice.advice to the advice string you've + chosen. This can be an advice string that was generated by the system, + or one you've written yourself. One good way of creating your own advice + string is to take the string generated by the system and pick out just + those elements that you wish to enforce. In the example above, + pg_plan_advice emits advice for the join order, the + join method, the scan method, and the use of parallelism, but you might + only want to control the join order: + + + +SET pg_plan_advice.advice = 'JOIN_ORDER(f d)'; +EXPLAIN (COSTS OFF) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +------------------------------------ + Hash Join + Hash Cond: (f.dim_id = d.id) + -> Seq Scan on join_fact f + -> Hash + -> Seq Scan on join_dim d + Supplied Plan Advice: + JOIN_ORDER(f d) /* matched */ + + + + Since the PLAN_ADVICE option to + EXPLAIN was not specified, no advice string is generated + for the plan. However, the supplied plan advice is still shown so that + anyone looking at the EXPLAIN output knows that the + chosen plan was influenced by plan advice. If information about supplied + plan advice is not desired, it can be suppressed by configuring + pg_plan_advice.always_explain_supplied_advice = false. + For each piece of supplied advice, the output shows + advice feedback indicating + whether or not the advice was successfully applied to the query. In this + case, the feedback says /* matched */, which means that + f and d were found in the query and + that the resulting query plan conforms to the specified advice. + + + + + + How It Works + + + Plan advice is written imperatively; that is, it specifies what should be + done. However, at an implementation level, + pg_plan_advice works by telling the core planner what + should not be done. In other words, it operates by constraining the + planner's choices, not by replacing it. Therefore, no matter what advice + you provide, you will only ever get a plan that the core planner would have + considered for the query in question. If you attempt to force what you + believe to be the correct plan by supplying an advice string, and the + planner still fails to produce the desired plan, this means that either + there is a bug in your advice string, or the plan in question was not + considered viable by the core planner. This commonly happens for one of two + reasons. First, it might be that the planner believes that the plan you're + trying to force would be semantically incorrect - that is, it would produce + the wrong results - and for that reason it wasn't considered. Second, it + might be that the planner rejected the plan you were hoping to generate on + some grounds other than cost. For example, given a very simple query such as + SELECT * FROM some_table, the query planner will + decide that the use of an index is worthless here before it performs any + costing calculations. You cannot force it to use an index for this query + even if you set enable_seqscan = false, and you can't + force it to use an index using plan advice, either. + + + + Specifying plan advice should never cause planner failure. However, if you + specify plan advice that asks for something impossible, you may get a plan + where some plan nodes are flagged as Disabled: true in + the EXPLAIN output. In some cases, such plans will be + basically the same plan you would have gotten with no supplied advice at + all, but in other cases, they may be much worse. For example: + + + +SET pg_plan_advice.advice = 'JOIN_ORDER(x f d)'; +EXPLAIN (COSTS OFF) + SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id; + QUERY PLAN +---------------------------------------------------- + Nested Loop + Disabled: true + -> Seq Scan on join_fact f + -> Index Scan using join_dim_pkey on join_dim d + Index Cond: (id = f.dim_id) + Supplied Plan Advice: + JOIN_ORDER(x f d) /* partially matched */ + + + + Because neither f nor d is the + first table in the JOIN_ORDER() specification, the + planner disables all direct joins between the two of them, thinking that + the join to x should happen first. Since planning isn't + allowed to fail, a disabled plan between the two relations is eventually + selected anyway, but here it's a Nested Loop rather than + the Hash Join that was chosen in the above example where + no advice was specified. There are several different ways that this kind + of thing can happen; when it does, the resulting plan is generally worse + than if no advice had been specified at all. Therefore, it is a good idea + to validate that the advice you specify applies to the query to which it + is applied and that the results are as expected. + + + + + + Advice Targets + + + An advice target uniquely identifies a particular + instance of a particular relation involved in a particular query. In simple + cases, such as the examples shown above, the advice target is simply the + relation alias. However, a more complex syntax is required when subqueries + are used, when tables are partitioned, or when the same relation alias is + mentioned more than once in the same subquery (e.g., (foo JOIN bar + ON foo.a = bar.a) x JOIN foo ON x.b = foo.b). Any combination of + these three things can occur simultaneously: a relation could be mentioned + more than once, be partitioned, and be used inside of a subquery. + + + + Because of this, the general syntax for a relation identifier is: + + + +alias_name#occurrence_number/partition_schema.partition_name@plan_name + + + + All components except for the alias_name are optional + and are included only when required. When a component is omitted, the + preceding punctuation must also be omitted. For the first occurrence of a + relation within a given subquery, generated advice will omit the occurrence + number, but it is legal to write #1, if desired. The + partition schema and partition name are included only for children of + partitioned tables. In generated advice, pg_plan_advice + always includes both, but it is legal to omit the schema. The plan name is + omitted for the top-level plan, and must be included for any subplan. + + + + It is not always easy to determine the correct advice target by examining + the query. For instance, if the planner pulls up a subquery into the parent + query level, everything inside of it becomes part of the parent query level, + and uses the parent query's subplan name (or no subplan name, if pulled up + to the top level). Furthermore, the correct subquery name is sometimes not + obvious. For example, when two queries are joined using an operation such as + UNION or INTERSECT, no name for the + subqueries is present in the SQL syntax; instead, a system-generated name is + assigned to each branch. The easiest way to discover the proper advice + targets is to use EXPLAIN (PLAN_ADVICE) and examine the + generated advice. + + + + + + Advice Tags + + + An advice tag specifies a particular behavior that + should be enforced for some portion of the query, such as a particular + join order or join method. All advice tags take + advice targets as arguments, + and many allow lists of advice targets, which in some cases can be nested + multiple levels deep. Several different classes of advice tags exist, + each controlling a different aspect of query planning. + + + + Scan Method Advice + +SEQ_SCAN(target [ ... ]) +TID_SCAN(target [ ... ]) +INDEX_SCAN(target index_name [ ... ]) +INDEX_ONLY_SCAN(target index_name [ ... ]) +FOREIGN_JOIN((target [ ... ]) [ ... ]) +BITMAP_HEAP_SCAN(target [ ... ]) +DO_NOT_SCAN(target [ ... ]) + + + SEQ_SCAN specifies that each target should be + scanned using a Seq Scan. TID_SCAN + specifies that each target should be scanned using a + TID Scan or TID Range Scan. + BITMAP_HEAP_SCAN specifies that each target + should be scanned using a Bitmap Heap Scan. + + + + INDEX_SCAN specifies that each target should + be scanned using an Index Scan on the given index + name. INDEX_ONLY_SCAN is similar, but specifies the + use of an Index Only Scan. In either case, the index + name can be, but does not have to be, schema-qualified. + + + + FOREIGN_JOIN specifies that a join between two or + more foreign tables should be pushed down to a remote server so + that it can be implemented as a single Foreign Scan. + Specifying FOREIGN_JOIN for a single foreign table is + neither necessary nor permissible: a Foreign Scan will + need to be used regardless. If you want to prevent a join from being + pushed down, consider using the JOIN_ORDER tag for + that purpose. + + + + DO_NOT_SCAN specifies that a particular target + should not appear in the final plan at all. In most cases, this is + impossible, and will simply cause the scan of the target relation to + be marked disabled. However, in certain cases, the planner considers + optimizations where a portion of the plan tree is copied and mutated, + and then considered as an alternative to the original. In those cases, + DO_NOT_SCAN can be used to exclude the non-preferred + alternative. + + + + The planner supports many types of scans other than those listed here; + however, in most of those cases, there is no meaningful decision to be + made, and hence no need for advice. For example, the output of a + set-returning function that appears in the FROM clause + can only ever be scanned using a Function Scan, so + there is no opportunity for advice to change anything. + + + + + + Join Order Advice + +JOIN_ORDER(join_order_item [ ... ]) + +where join_order_item is: + +advice_target | +( join_order_item [ ... ] ) | +{ join_order_item [ ... ] } + + + When JOIN_ORDER is used without any sublists, it + specifies an outer-deep join with the first advice target as the driving + table, joined to each subsequent advice target in turn in the order + specified. For instance, JOIN_ORDER(a b c) means that + a should be the driving table, and that it should be + joined first to b and then to c. + If there are more relations in the query than a, + b, and c, the rest can be joined + afterwards in any manner. + + + + If a JOIN_ORDER list contains a parenthesized sublist, + it specifies a non-outer-deep join. The relations in the sublist must first + be joined to each other much as if the sublist were a top-level + JOIN_ORDER list, and the resulting join product must + then appear on the inner side of a join at the appropriate point in the + join order. For example, JOIN_ORDER(a (b c) d) requires + a plan of this form: + + + +Join + -> Join + -> Scan on a + -> Join + -> Scan on b + -> Scan on c + -> Scan on d + + + + If a JOIN_ORDER list contains a sublist surrounded by + curly braces, this also specifies a non-outer-deep join. However, the join + order within the sublist is not constrained. For example, specifying + JOIN_ORDER(a {b c} d) would allow the scans of + b and c to be swapped in the + previous example, which is not allowed when parentheses are used. + + + + Parenthesized sublists can be arbitrarily nested, but sublists surrounded + by curly braces cannot themselves contain sublists. + + + + Multiple instances of JOIN_ORDER() can sometimes be + needed in order to fully constrain the join order. This occurs when there + are multiple join problems that are optimized separately by the planner. + This can happen due to the presence of subqueries, or because there is a + partitionwise join. In the latter case, each branch of the partitionwise + join can have its own join order, independent of every other branch. + + + + + + Join Method Advice + +join_method_name(join_method_item [ ... ]) + +where join_method_name is: + +{ MERGE_JOIN_MATERIALIZE | MERGE_JOIN_PLAIN | NESTED_LOOP_MATERIALIZE | NESTED_LOOP_MEMOIZE | NESTED_LOOP_PLAIN | HASH_JOIN } + +and join_method_item is: + +{ advice_target | +( advice_target [ ... ] ) } + + + Join method advice specifies the relation, or set of relations, that should + appear on the inner side of a join using the named join method. For + example, HASH_JOIN(a b) means that each of + a and b should appear on the inner + side of a hash join; a conforming plan must contain at least two hash + joins, one of which has a and nothing else on the + inner side, and the other of which has b and nothing + else on the inner side. On the other hand, + HASH_JOIN((a b)) means that the join product of + a and b should appear together + on the inner side of a single hash join. + + + + Note that join method advice implies a negative join order constraint. + Since the named relation or relations must be on the inner side of a join + using the specified method, none of them can be the driving table for the + entire join problem. Moreover, no relation inside the set should be joined + to any relation outside the set until all relations within the set have + been joined to each other. For example, if the advice specifies + HASH_JOIN((a b)) and the system begins by joining either + of those to some third relation c, the resulting + plan could never be compliant with the request to put exactly those two + relations on the inner side of a hash join. When using both join order + advice and join method advice for the same query, it is a good idea to make + sure that they do not mandate incompatible join orders. + + + + + + Partitionwise Advice + +PARTITIONWISE(partitionwise_item [ ... ]) + +where partitionwise_item is: + +{ advice_target | +( advice_target [ ... ] ) } + + + When applied to a single target, PARTITIONWISE + specifies that the specified table should not be part of any partitionwise + join. When applied to a list of targets, PARTITIONWISE + specifies that exactly that set of relations should be joined in + partitionwise fashion. Note that, regardless of what advice is specified, + no partitionwise joins will be possible if + enable_partitionwise_join = off. + + + + + + Semijoin Uniqueness Advice + +SEMIJOIN_UNIQUE(sj_unique_item [ ... ]) +SEMIJOIN_NON_UNIQUE(sj_unique_item [ ... ]) + +where sj_unique_item is: + +{ advice_target | +( advice_target [ ... ] ) } + + + The planner sometimes has a choice between implementing a semijoin + directly and implementing a semijoin by making the nullable side unique + and then performing an inner join. SEMIJOIN_UNIQUE + specifies the latter strategy, while SEMIJOIN_NON_UNIQUE + specifies the former strategy. In either case, the argument is the single + relation or list of relations that appear beneath the nullable side of the + join. + + + + + + Parallel Query Advice + +GATHER(gather_item [ ... ]) +GATHER_MERGE(gather_item [ ... ]) +NO_GATHER(advice_target [ ... ]) + +where gather_item is: + +{ advice_target | +( advice_target [ ... ] ) } + + + GATHER or GATHER_MERGE specifies + that Gather or Gather Merge, + respectively, should be placed on top of the single relation specified as + a target, or on top of the join between the list of relations specified as + a target. This means that GATHER(a b c) is a request + for three different Gather nodes, while + GATHER((a b c)) is a request for a single + Gather node on top of a 3-way join. + + + + NO_GATHER specifies that no Gather or + Gather Merge node should appear above any of the + targets, but it only constrains the planning of an individual subquery, + and outer subquery levels can still use parallel query. For example, + NO_GATHER(inner_example@any_1) precludes using a + Parallel Seq Scan to access the + inner_example table within the any_1 + subquery, but it does not prevent the planner from placing + SubPlan any_1 beneath a Gather + or Gather Merge node. The following plan is + compatible with NO_GATHER(inner_example@any_1), but + not with NO_GATHER(outer_example): + + + + Finalize Aggregate + -> Gather + -> Partial Aggregate + -> Parallel Seq Scan on outer_example + Filter: (something = (hashed SubPlan any_1).col1) + SubPlan any_1 + -> Seq Scan on inner_example + Filter: (something_else > 100) + + + + Here is the reverse case, that is, a plan compatible with + NO_GATHER(outer_example) but not with + NO_GATHER(inner_example@any_1): + + + + Aggregate + -> Seq Scan on outer_example + Filter: (something = (hashed SubPlan any_1).col1) + SubPlan any_1 + -> Gather + -> Parallel Seq Scan on inner_example + Filter: (something_else > 100) + + + + + + + Advice Feedback + + + EXPLAIN provides feedback on whether supplied advice was + successfully applied to the query in the form of a comment on each piece + of supplied advice. For example: + + + +SET pg_plan_advice.advice = 'hash_join(f g) join_order(f g) index_scan(f no_such_index)'; +EXPLAIN (COSTS OFF) + SELECT * FROM jo_fact f + LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id + LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id + WHERE val1 = 1 AND val2 = 1; + QUERY PLAN +------------------------------------------------------------------- + Hash Join + Hash Cond: ((d1.id = f.dim1_id) AND (d2.id = f.dim2_id)) + -> Nested Loop + -> Seq Scan on jo_dim2 d2 + Filter: (val2 = 1) + -> Materialize + -> Seq Scan on jo_dim1 d1 + Filter: (val1 = 1) + -> Hash + -> Seq Scan on jo_fact f + Supplied Plan Advice: + INDEX_SCAN(f no_such_index) /* matched, inapplicable, failed */ + HASH_JOIN(f) /* matched */ + HASH_JOIN(g) /* not matched */ + JOIN_ORDER(f g) /* partially matched */ + + + + For this query, f is a valid advice target, but + g is not. Therefore, the request to place + f on the inner side of a hash join is listed as + matched, but the request to place g + on the inner side of a hash join is listed as + not matched. The JOIN_ORDER advice + tag involves one valid target and one invalid target, and so is listed as + partially matched. Note that + HASH_JOIN(f g) is actually a request for two logically + separate behaviors, so in the feedback it is decomposed into + HASH_JOIN(f) and HASH_JOIN(g). + By contrast, JOIN_ORDER(f g) is a single request and + appears as-is. + + + + Advice feedback can include any of the following: + + + + + + + matched means that all of the specified advice targets + were observed together during query planning, at a time at which the + advice could be enforced. + + + + + + partially matched means that some but not all of the + specified advice targets were observed during query planning, or all + of the advice targets were observed but not together. For example, this + may happen if all the targets of JOIN_ORDER advice + individually match the query, but the proposed join order is not legal. + + + + + + not matched means that none of the + specified advice targets were observed during query planning. This may + happen if the advice simply doesn't match the query, or it may + occur if the relevant portion of the query was not planned, perhaps + because it was gated by a condition that was simplified to constant false. + + + + + + inapplicable means that the advice tag could not + be applied to the advice targets for some reason. For example, this will + happen if the use of a nonexistent index is requested, or if an attempt + is made to control semijoin uniqueness for a non-semijoin. + + + + + + conflicting means that two or more pieces of advice + request incompatible behaviors. For example, if you advise a sequential + scan and an index scan for the same table, both requests will be flagged + as conflicting. This also commonly happens if join method advice or + semijoin uniqueness advice implies a join order incompatible with the + one explicitly specified; see + . + + + + + + failed means that the query plan does not comply with + the advice. This only occurs for entries that are also shown as + matched. It frequently occurs for entries that are + also marked as conflicting or + inapplicable. However, it can also occur when the + advice is valid insofar as pg_plan_advice is able + to determine, but the planner is not able to construct a legal + plan that can comply with the advice. It is important to note that the + sanity checks performed by pg_plan_advice are fairly + superficial and focused mostly on looking for logical inconsistencies in + the advice string; only the planner knows what will actually work. + + + + + + + All advice should be marked as exactly one of matched, + partially matched, or not matched. + + + + + + Configuration Parameters + + + + + + pg_plan_advice.advice (string) + + pg_plan_advice.advice configuration parameter + + + + + + pg_plan_advice.advice is an advice string to be + used during query planning. + + + + + + + pg_plan_advice.always_explain_supplied_advice (boolean) + + pg_plan_advice.always_explain_supplied_advice configuration parameter + + + + + + pg_plan_advice.always_explain_supplied_advice causes + EXPLAIN to always show any supplied advice and the + associated + advice feedback. + The default value is true. If set to + false, this information will be displayed only when + EXPLAIN (PLAN_ADVICE) is used. + + + + + + + pg_plan_advice.always_store_advice_details (boolean) + + pg_plan_advice.always_store_advice_details configuration parameter + + + + + + pg_plan_advice.always_store_advice_details allows + EXPLAIN to show details related to plan advice even + when prepared queries are used. The default value is + false. When planning a prepared query, it is not + possible to know whether EXPLAIN will later be used, + so by default, to reduce overhead, pg_plan_advice + will not generate plan advice or feedback on supplied advice. This means + that if EXPLAIN EXECUTE is used on the prepared query, + it will not be able to show this information. Changing this setting to + true avoids this problem, but adds additional + overhead. It is probably a good idea to enable this option only in + sessions where it is needed, rather than on a system-wide basis. + + + + + + + pg_plan_advice.feedback_warnings (boolean) + + pg_plan_advice.feedback_warnings configuration parameter + + + + + + When set to true, pg_plan_advice.feedback_warnings + emits a warning whenever supplied plan advice is not successfully + enforced. The default value is false. + + + + + + + pg_plan_advice.trace_mask (boolean) + + pg_plan_advice.trace_mask configuration parameter + + + + + + When pg_plan_advice.trace_mask is + true, pg_plan_advice will print + messages during query planning each time that + pg_plan_advice alters the mask of allowable query + plan types in response to supplied plan advice. The default value is + false. The messages printed by this setting are not + expected to be useful except for purposes of debugging this module. + + + + + + + + + + Limitations + + + It is currently not possible to control any aspect of the planner's behavior + with respect to aggregation. This includes both whether aggregates are + computed by sorting or hashing, and also whether strategies such as + eager aggregation or + partitionwise + aggregation are used. + + + + It also is currently not possible to control any aspect of the planner's + behavior with respect to set operations such as UNION + or INTERSECT. + + + + As discussed above under How + It Works, the use of plan advice can only affect which plan + the planner chooses from among those it believes to be viable. It can never + force the choice of a plan which the planner refused to consider in the + first place. + + + + + Author + + + Robert Haas rhaas@postgresql.org + + + + diff --git a/doc/src/sgml/pgstashadvice.sgml b/doc/src/sgml/pgstashadvice.sgml new file mode 100644 index 0000000000000..7813d63d91e97 --- /dev/null +++ b/doc/src/sgml/pgstashadvice.sgml @@ -0,0 +1,278 @@ + + + + pg_stash_advice — store and automatically apply plan advice + + + pg_stash_advice + + + + The pg_stash_advice extension allows you to stash + plan advice strings in dynamic + shared memory where they can be automatically applied. An + advice stash is a mapping from + query identifiers to plan advice + strings. Whenever a session is asked to plan a query whose query ID appears + in the relevant advice stash, the plan advice string is automatically applied + to guide planning. Note that advice stashes are stored in dynamically + allocated shared memory. This means that it is important to be mindful + of memory consumption when deciding how much plan advice to stash. + Optionally, advice stashes and their contents can automatically be persisted + to disk and reloaded from disk; see + pg_stash_advice.persist, below. + + + + In order to use this module, you will need to execute + CREATE EXTENSION pg_stash_advice in at least + one database, so that you have access to the SQL functions to manage + advice stashes. You will also need the pg_stash_advice + module to be loaded in all sessions where you want this module to + automatically apply advice. It will usually be best to do this by adding + pg_stash_advice to + and restarting the server. + + + + Once you have met the above criteria, you can create advice stashes + using the pg_create_advice_stash function described + below and set the plan advice for a given query ID in a given stash using + the pg_set_stashed_advice function. Then, you need + only configure pg_stash_advice.stash_name to point + to the chosen advice stash name. For some use cases, rather than setting + this on a system-wide basis, you may find it helpful to use + ALTER DATABASE ... SET or + ALTER ROLE ... SET to configure values that will apply + only to a database or only to a certain role. Likewise, it may sometimes + be better to set the stash name in a particular session using + SET. + + + + Because pg_stash_advice works on the basis of query + identifiers, you will need to determine the query identifier for each query + whose plan you wish to control. You will also need to determine the advice + string that you wish to store for each query. One way to do this is to use + EXPLAIN: the VERBOSE option will + show the query ID, and the PLAN_ADVICE option will + show plan advice. Query identifiers can also be obtained through tools + such as or + , but these tools + will not provide plan advice strings. Note that + must be enabled for query + identifiers to be computed; if set to auto, loading + pg_stash_advice will enable it automatically. + + + + Generally, the fact that the planner is able to change query plans as + the underlying distribution of data changes is a feature, not a bug. + Moreover, applying plan advice can have a noticeable performance cost even + when it does not result in a change to the query plan. Therefore, it is + a good idea to use this feature only when and to the extent needed. + Plan advice strings can be trimmed down to mention only those aspects + of the plan that need to be controlled, and used only for queries where + there is believed to be a significant risk of planner error. + + + + Note that pg_stash_advice currently lacks a sophisticated + security model. Only the superuser, or a user to whom the superuser has + granted EXECUTE permission on the relevant functions, + may create advice stashes or alter their contents, but any user may set + pg_stash_advice.stash_name for their session, and this + may reveal the contents of any advice stash with that name. Users should + assume that information embedded in stashed advice strings may become visible + to non-privileged users. + + + + Functions + + + + + + pg_create_advice_stash(stash_name text) returns void + + pg_create_advice_stash + + + + + + Creates a new, empty advice stash with the given name. + + + + + + + pg_drop_advice_stash(stash_name text) returns void + + pg_drop_advice_stash + + + + + + Drops the named advice stash and all of its entries. + + + + + + + pg_set_stashed_advice(stash_name text, query_id bigint, + advice_string text) returns void + + pg_set_stashed_advice + + + + + + Stores an advice string in the named advice stash, associated with + the given query identifier. If an entry for that query identifier + already exists in the stash, it is replaced. If + advice_string is NULL, + any existing entry for that query identifier is removed. + + + + + + + pg_get_advice_stashes() returns setof (stash_name text, + num_entries bigint) + + pg_get_advice_stashes + + + + + + Returns one row for each advice stash, showing the stash name and + the number of entries it contains. + + + + + + + pg_get_advice_stash_contents(stash_name text) returns setof + (stash_name text, query_id bigint, advice_string text) + + pg_get_advice_stash_contents + + + + + + Returns one row for each entry in the named advice stash. If + stash_name is NULL, returns + entries from all stashes. + + + + + + + pg_start_stash_advice_worker() returns void + + pg_start_stash_advice_worker + + + + + + Starts the background worker, so that advice stash contents can be + automatically persisted to disk. If this module is included in + at startup time with + pg_stash_advice.persist = true, the worker will be + started automatically. When started manually, the worker will not load + anything from disk, but it will still persist data to disk. You can then + configure the server to start the worker automatically after the next + restart, preserving any stashed advice you add now. + + + + + + + + + + Configuration Parameters + + + + + + pg_stash_advice.persist (boolean) + + pg_stash_advice.persist configuration parameter + + + + + + Controls whether the advice stashes and stash entries should be + persisted to disk. This is on by default. If any stashes are persisted, + a file named pg_stash_advice.tsv will be created in + the data directory. Stashes are loaded and saved using a background + worker process. This parameter can only be set at server start. + + + + + + + pg_stash_advice.persist_interval (integer) + + pg_stash_advice.persist_interval configuration parameter + + + + + + Specifies the interval, in seconds, between checks for changes that + need to be written to pg_stash_advice.tsv. If set to + zero, changes are only written when the server shuts down. The default + value is 30. This parameter can only be set in the + postgresql.conf file or on the server command line. + + + + + + + pg_stash_advice.stash_name (string) + + pg_stash_advice.stash_name configuration parameter + + + + + + Specifies the name of the advice stash to consult during query + planning. The default value is the empty string, which disables + this module. + + + + + + + + + + Author + + + Robert Haas rhaas@postgresql.org + + + + diff --git a/doc/src/sgml/pgstatstatements.sgml b/doc/src/sgml/pgstatstatements.sgml index 7baa07dcdbf7f..d753de5836efb 100644 --- a/doc/src/sgml/pgstatstatements.sgml +++ b/doc/src/sgml/pgstatstatements.sgml @@ -554,6 +554,24 @@ + + + generic_plan_calls bigint + + + Number of times the statement has been executed using a generic plan + + + + + + custom_plan_calls bigint + + + Number of times the statement has been executed using a custom plan + + + stats_since timestamp with time zone diff --git a/doc/src/sgml/pgstattuple.sgml b/doc/src/sgml/pgstattuple.sgml index 4071da4ed941a..54d8f90245e73 100644 --- a/doc/src/sgml/pgstattuple.sgml +++ b/doc/src/sgml/pgstattuple.sgml @@ -270,6 +270,15 @@ leaf_fragmentation | 0 page than is accounted for by internal_pages + leaf_pages + empty_pages + deleted_pages, because it also includes the index's metapage. + avg_leaf_density is the fraction of the index size that + is taken up by user data. Since indexes have a default fillfactor of 90, + this should be around 90 for newly built indexes of non-negligible size, + but usually deteriorates over time. + leaf_fragmentation represents a measure of disorder. + A higher leaf_fragmentation indicates that the + physical order of the index leaf pages increasingly deviates from their + logical order. This can have a significant impact if a large part + of the index is read from disk. @@ -368,7 +377,7 @@ pending_tuples | 0 pgstathashindex returns a record showing information about a HASH index. For example: -test=> select * from pgstathashindex('con_hash_index'); +test=> SELECT * FROM pgstathashindex('con_hash_index'); -[ RECORD 1 ]--+----------------- version | 4 bucket_pages | 33081 diff --git a/doc/src/sgml/pgsurgery.sgml b/doc/src/sgml/pgsurgery.sgml index 29bccd7f36d6c..68186122a2208 100644 --- a/doc/src/sgml/pgsurgery.sgml +++ b/doc/src/sgml/pgsurgery.sgml @@ -34,17 +34,17 @@ intended use of this function is to forcibly remove tuples that are not otherwise accessible. For example: -test=> select * from t1 where ctid = '(0, 1)'; +test=> SELECT * FROM t1 WHERE ctid = '(0, 1)'; ERROR: could not access status of transaction 4007513275 DETAIL: Could not open file "pg_xact/0EED": No such file or directory. -test=# select heap_force_kill('t1'::regclass, ARRAY['(0, 1)']::tid[]); +test=# SELECT heap_force_kill('t1'::regclass, ARRAY['(0, 1)']::tid[]); heap_force_kill ----------------- (1 row) -test=# select * from t1 where ctid = '(0, 1)'; +test=# SELECT * FROM t1 WHERE ctid = '(0, 1)'; (0 rows) @@ -70,19 +70,19 @@ test=> vacuum t1; ERROR: found xmin 507 from before relfrozenxid 515 CONTEXT: while scanning block 0 of relation "public.t1" -test=# select ctid from t1 where xmin = 507; +test=# SELECT ctid FROM t1 WHERE xmin = 507; ctid ------- (0,3) (1 row) -test=# select heap_force_freeze('t1'::regclass, ARRAY['(0, 3)']::tid[]); +test=# SELECT heap_force_freeze('t1'::regclass, ARRAY['(0, 3)']::tid[]); heap_force_freeze ------------------- (1 row) -test=# select ctid from t1 where xmin = 2; +test=# SELECT ctid FROM t1 WHERE xmin = 2; ctid ------- (0,3) diff --git a/doc/src/sgml/pgwalinspect.sgml b/doc/src/sgml/pgwalinspect.sgml index 3a8121c70f1f1..79c3ead40bc71 100644 --- a/doc/src/sgml/pgwalinspect.sgml +++ b/doc/src/sgml/pgwalinspect.sgml @@ -73,9 +73,9 @@ postgres=# SELECT * FROM pg_get_wal_record_info('0/E419E28'); -[ RECORD 1 ]----+------------------------------------------------- -start_lsn | 0/E419E28 -end_lsn | 0/E419E68 -prev_lsn | 0/E419D78 +start_lsn | 0/0E419E28 +end_lsn | 0/0E419E68 +prev_lsn | 0/0E419D78 xid | 0 resource_manager | Heap2 record_type | VACUUM @@ -146,9 +146,9 @@ block_ref | postgres=# SELECT * FROM pg_get_wal_block_info('0/1230278', '0/12302B8'); -[ RECORD 1 ]-----+----------------------------------- -start_lsn | 0/1230278 -end_lsn | 0/12302B8 -prev_lsn | 0/122FD40 +start_lsn | 0/01230278 +end_lsn | 0/012302B8 +prev_lsn | 0/0122FD40 block_id | 0 reltablespace | 1663 reldatabase | 1 diff --git a/doc/src/sgml/planstats.sgml b/doc/src/sgml/planstats.sgml index 068b804a18d70..e57867ba617f3 100644 --- a/doc/src/sgml/planstats.sgml +++ b/doc/src/sgml/planstats.sgml @@ -635,7 +635,7 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a = 1 AND b = 1 pg_mcv_list_items set-returning function. -SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid), +SELECT m.* FROM pg_statistic_ext JOIN pg_statistic_ext_data ON (oid = stxoid), pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts2'; index | values | nulls | frequency | base_frequency -------+----------+-------+-----------+---------------- diff --git a/doc/src/sgml/plperl.sgml b/doc/src/sgml/plperl.sgml index 8007261d0224c..6f018645f1191 100644 --- a/doc/src/sgml/plperl.sgml +++ b/doc/src/sgml/plperl.sgml @@ -229,12 +229,12 @@ $$ LANGUAGE plperl; references to Perl arrays. Here is an example: -CREATE OR REPLACE function returns_array() +CREATE OR REPLACE FUNCTION returns_array() RETURNS text[][] AS $$ return [['a"b','c,d'],['e\\f','g']]; $$ LANGUAGE plperl; -select returns_array(); +SELECT returns_array(); @@ -468,8 +468,8 @@ optional maximum number of rows: $rv = spi_exec_query('SELECT * FROM my_table', 5); This returns up to 5 rows from the table - my_table. If my_table - has a column my_column, you can get that + my_table. If my_table + has a column my_column, you can get that value from row $i of the result like this: $foo = $rv->{rows}[$i]->{my_column}; @@ -512,7 +512,7 @@ INSERT INTO test (i, v) VALUES (3, 'third line'); INSERT INTO test (i, v) VALUES (4, 'immortal'); CREATE OR REPLACE FUNCTION test_munge() RETURNS SETOF test AS $$ - my $rv = spi_exec_query('select i, v from test;'); + my $rv = spi_exec_query('SELECT i, v FROM test;'); my $status = $rv->{status}; my $nrows = $rv->{processed}; foreach my $rn (0 .. $nrows - 1) { @@ -588,7 +588,7 @@ CREATE OR REPLACE FUNCTION lotsa_md5 (INTEGER) RETURNS SETOF foo_type AS $$ return; $$ LANGUAGE plperlu; -SELECT * from lotsa_md5(500); +SELECT * FROM lotsa_md5(500); @@ -1199,7 +1199,7 @@ $$ LANGUAGE plperl; $_TD->{new}{foo} - NEW value of column foo + NEW value of column foo @@ -1208,7 +1208,7 @@ $$ LANGUAGE plperl; $_TD->{old}{foo} - OLD value of column foo + OLD value of column foo diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index e937491e6b89e..561f6e50d6371 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -1023,7 +1023,7 @@ IF count(*) > 0 FROM my_table THEN ... tax := subtotal * 0.06; my_record.user_id := 20; my_array[j] := 20; -my_array[1:3] := array[1,2,3]; +my_array[1:3] := ARRAY[1, 2, 3]; complex_array[n].realpart = 12.3; @@ -1037,7 +1037,7 @@ complex_array[n].realpart = 12.3; within a PL/pgSQL function just by writing the command. For example, you could create and fill a table by writing -CREATE TABLE mytable (id int primary key, data text); +CREATE TABLE mytable (id int PRIMARY KEY, data text); INSERT INTO mytable VALUES (1,'one'), (2,'two'); @@ -4962,13 +4962,13 @@ $$ LANGUAGE plpgsql; Variable substitution currently works only in SELECT, INSERT, UPDATE, - DELETE, and commands containing one of - these (such as EXPLAIN and CREATE TABLE - ... AS SELECT), - because the main SQL engine allows query parameters only in these - commands. To use a non-constant name or value in other statement - types (generically called utility statements), you must construct - the utility statement as a string and EXECUTE it. + DELETE, MERGE and commands + containing one of these (such as EXPLAIN and + CREATE TABLE ... AS SELECT), because the main SQL + engine allows query parameters only in these commands. To use a + non-constant name or value in other statement types (generically called + utility statements), you must construct the utility statement as a string + and EXECUTE it. @@ -5294,24 +5294,24 @@ a_output := a_output || $$ AND name LIKE 'foobar'$$ . For example: -a_output := a_output || '' if v_'' || - referrer_keys.kind || '' like '''''''''' +a_output := a_output || '' IF v_'' || + referrer_keys.kind || '' LIKE '''''''''' || referrer_keys.key_string || '''''''''' - then return '''''' || referrer_keys.referrer_type - || ''''''; end if;''; + THEN RETURN '''''' || referrer_keys.referrer_type + || ''''''; END IF;''; The value of a_output would then be: -if v_... like ''...'' then return ''...''; end if; +IF v_... LIKE ''...'' THEN RETURN ''...''; END IF; In the dollar-quoting approach, this becomes: -a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$ +a_output := a_output || $$ IF v_$$ || referrer_keys.kind || $$ LIKE '$$ || referrer_keys.key_string || $$' - then return '$$ || referrer_keys.referrer_type - || $$'; end if;$$; + THEN RETURN '$$ || referrer_keys.referrer_type + || $$'; END IF;$$; where we assume we only need to put single quote marks into a_output, because it will be re-quoted before use. diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml index bee817ea822a2..c860a47a2e158 100644 --- a/doc/src/sgml/plpython.sgml +++ b/doc/src/sgml/plpython.sgml @@ -662,6 +662,14 @@ $$ LANGUAGE plpython3u; in PL/Python + + PL/Python can be used to define trigger + functions. + PostgreSQL requires that a function that is to + be called as a trigger must be declared as a function with no arguments and + a return type of trigger. + + When a function is used as a trigger, the dictionary TD contains trigger-related values: @@ -769,6 +777,74 @@ $$ LANGUAGE plpython3u; + + Event Trigger Functions + + + event trigger + in PL/Python + + + + PL/Python can be used to define event triggers + (see also ). + PostgreSQL requires that a function that is to + be called as an event trigger must be declared as a function with no + arguments and a return type of event_trigger. + + + + When a function is used as an event trigger, the dictionary + TD contains trigger-related values: + + + + TD["event"] + + + The event the trigger was fired for, as a string, for example + ddl_command_start. + + + + + + TD["tag"] + + + The command tag for which the trigger was fired, as a string, for + example DROP TABLE. + + + + + + + + shows an example of an + event trigger function in PL/Python. + + + + A <application>PL/Python</application> Event Trigger Function + + + This example trigger simply raises a NOTICE message + each time a supported command is executed. + + + +CREATE OR REPLACE FUNCTION pysnitch() RETURNS event_trigger +LANGUAGE plpython3u +AS $$ + plpy.notice("TD[event] => " + TD["event"] + " ; TD[tag] => " + TD["tag"]); +$$; + +CREATE EVENT TRIGGER pysnitch ON ddl_command_start EXECUTE FUNCTION pysnitch(); + + + + Database Access @@ -989,7 +1065,7 @@ $$ LANGUAGE plpython3u; CREATE FUNCTION count_odd_iterator() RETURNS integer AS $$ odd = 0 -for row in plpy.cursor("select num from largetable"): +for row in plpy.cursor("SELECT num FROM largetable"): if row['num'] % 2: odd += 1 return odd @@ -997,7 +1073,7 @@ $$ LANGUAGE plpython3u; CREATE FUNCTION count_odd_fetch(batch_size integer) RETURNS integer AS $$ odd = 0 -cursor = plpy.cursor("select num from largetable") +cursor = plpy.cursor("SELECT num FROM largetable") while True: rows = cursor.fetch(batch_size) if not rows: @@ -1010,7 +1086,7 @@ $$ LANGUAGE plpython3u; CREATE FUNCTION count_odd_prepared() RETURNS integer AS $$ odd = 0 -plan = plpy.prepare("select num from largetable where num % $1 <> 0", ["integer"]) +plan = plpy.prepare("SELECT num FROM largetable WHERE num % $1 <> 0", ["integer"]) rows = list(plpy.cursor(plan, [2])) # or: = list(plan.cursor([2])) return len(rows) diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml index 5a8e4c9d37e99..9fd008a99d7cd 100644 --- a/doc/src/sgml/pltcl.sgml +++ b/doc/src/sgml/pltcl.sgml @@ -180,7 +180,7 @@ $$ LANGUAGE pltcl; column names. Here is an example: -CREATE FUNCTION square_cube(in int, out squared int, out cubed int) AS $$ +CREATE FUNCTION square_cube(IN int, OUT squared int, OUT cubed int) AS $$ return [list squared [expr {$1 * $1}] cubed [expr {$1 * $1 * $1}]] $$ LANGUAGE pltcl; diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index 781a01067f7d6..b81f33732fb6c 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -82,7 +82,7 @@ Note that postgres_fdw currently lacks support for INSERT statements with an ON CONFLICT DO - UPDATE clause. However, the ON CONFLICT DO NOTHING + SELECT/UPDATE clause. However, the ON CONFLICT DO NOTHING clause is supported, provided a unique index inference specification is omitted. Note also that postgres_fdw supports row movement @@ -332,7 +332,7 @@ OPTIONS (ADD password_required 'false'); - The following option controls how such an ANALYZE + The following options control how such an ANALYZE operation behaves: @@ -364,6 +364,33 @@ OPTIONS (ADD password_required 'false'); + + restore_stats (boolean) + + + This option, which can be specified for a foreign table or a foreign + server, determines if ANALYZE on a foreign table + will instead attempt to fetch the existing statistics for the foreign + table on the remote server, and restore those statistics directly to + the local server. If the attempt failed, statistics are collected by + row sampling on the foreign table. + This option is only useful if the remote table is one that can have + regular statistics (tables and materialized views). + When using this option, it is the user's responsibility + to ensure that the existing statistics for the foreign + table are up-to-date. + The default is false. + + + + If the foreign table is a partition of a partitioned table, analyzing + the partitioned table will still result in row sampling on the foreign + table regardless of this setting, though direct analysis of the foreign + table would have attempted to fetch and restore remote statistics first. + + + + @@ -1049,6 +1076,32 @@ postgres=# SELECT postgres_fdw_disconnect_all(); + + Subscription Management + + + postgres_fdw supports subscription connections using + the same options described in . + + + + For example, assuming the remote server foreign-host has + a publication testpub: + +CREATE SERVER subscription_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foreign-host', dbname 'foreign_db'); +CREATE USER MAPPING FOR local_user SERVER subscription_server OPTIONS (user 'foreign_user', password 'password'); +CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testpub; + + + + + To create a subscription, the user must be a member of the role and have + USAGE privileges on the server. + + + Transaction Management @@ -1077,6 +1130,23 @@ postgres=# SELECT postgres_fdw_disconnect_all(); PostgreSQL release might modify these rules. + + The remote transaction is opened in the same read/write mode as the local + transaction: if the local transaction is READ ONLY, + the remote transaction is opened in READ ONLY mode, + otherwise it is opened in READ WRITE mode. + (This rule is also applied to remote and local subtransactions.) + Note that this does not prevent login triggers executed on the remote + server from writing. + + + + The remote transaction is also opened in the same deferrable mode as the + local transaction: if the local transaction is DEFERRABLE, + the remote transaction is opened in DEFERRABLE mode, + otherwise it is opened in NOT DEFERRABLE mode. + + Note that it is currently not supported by postgres_fdw to prepare the remote transaction for @@ -1226,7 +1296,7 @@ postgres=# SELECT postgres_fdw_disconnect_all(); PostgresFdwCleanupResult - Waiting for transaction abort on remote server. + Waiting for transaction abort on a remote server. diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml index af476c82fcc1e..2101442c90fcb 100644 --- a/doc/src/sgml/postgres.sgml +++ b/doc/src/sgml/postgres.sgml @@ -49,7 +49,7 @@ break is not needed in a wider output rendering. - After you have successfully completed this tutorial you will want to + After you have successfully completed this tutorial, you will want to read the section to gain a better understanding of the SQL language, or for information about developing applications with diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index c4d3853cbf2c2..49f81676712b4 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -189,7 +189,7 @@ - Protocol versions + Protocol Versions The current, latest version of the protocol is version 3.2. However, for @@ -198,6 +198,17 @@ by default. + + + During the PostgreSQL 19 beta period, libpq will instead default to + requesting protocol version 3.9999, to test that servers and middleware + properly implement protocol version negotiation. Servers that support + negotiation will automatically downgrade to version 3.2 or 3.0. Users can + bypass this beta-only behavior by explicitly setting + max_protocol_version=3.0 in their connection string. + + + A single server can support multiple protocol versions. The initial startup-request message tells the server which protocol version the client @@ -223,10 +234,12 @@ shows the currently supported protocol versions. + + documents protocol versions that are unsupported or otherwise reserved. - Protocol versions + Supported Protocol Versions @@ -248,6 +261,39 @@ + 3.0 + PostgreSQL 7.4 and later + + + +
+ + + Other Protocol Versions + + + + + Version + Supported by + Description + + + + + + 3.9999 + - + Reserved for protocol greasing. libpq may use this version, which + is higher than any minor version the project ever expects to use, to + test that servers and middleware properly implement protocol version + negotiation. Servers must not add special-case + logic for this version; they should simply compare it to their latest + supported version (which will always be smaller) and downgrade via a + NegotiateProtocolVersion message. + + + 3.1 - Reserved. Version 3.1 has not been used by any PostgreSQL @@ -257,15 +303,89 @@ - 3.0 - PostgreSQL 7.4 and later - - 2.0 up to PostgreSQL 13 - See previous releases of + Obsolete. See previous releases of the PostgreSQL documentation for - details + details. + + + +
+
+ + + Protocol Extensions + + + Servers and clients may additionally negotiate individual extensions to the + protocol version in use. These are offered by the client in the startup + message, as specially-named parameters with a _pq_. + prefix. Servers reject any unknown or unsupported extensions by sending a + NegotiateProtocolVersion message containing the list of rejected parameter + names, at which point the client may choose whether to continue with the + connection. and + document the supported + and reserved protocol extension parameters, respectively. + + + + Supported Protocol Extensions + + + + + + + Parameter Name + Values + Supported by + Description + + + + + + + (No supported protocol extensions are currently defined.) + + + + +
+ + + Reserved Protocol Extensions + + + + + Parameter Name + Description + + + + + + _pq_.[name] + Any other parameter names beginning with _pq_., + that are not defined above, are reserved for future protocol expansion. + Servers must reject any that are received from a + client, by sending a NegotiateProtocolVersion message during the + startup flow, and should + otherwise continue the connection. + + + + + _pq_.test_protocol_negotiation + Reserved for protocol greasing. libpq may send this extension to + test that servers and middleware properly implement protocol extension + negotiation. Servers must not add special-case + logic for this parameter; they should simply send the list of all + unsupported options (including this one) via a NegotiateProtocolVersion + message. + @@ -295,8 +415,8 @@ To begin a session, a frontend opens a connection to the server and sends a startup message. This message includes the names of the user and of the database the user wants to connect to; it also identifies the particular - protocol version to be used. (Optionally, the startup message can include - additional settings for run-time parameters.) + protocol version to be used. (Optionally, the startup message can request + protocol extensions and include additional settings for run-time parameters.) The server then uses this information and the contents of its configuration files (such as pg_hba.conf) to determine @@ -537,6 +657,11 @@ The frontend should not respond to this message, but should continue listening for a ReadyForQuery message. + + The PostgreSQL server will always send this + message, but some third party backend implementations of the protocol + that don't support query cancellation are known not to. + @@ -886,6 +1011,16 @@ SELCT 1/0; Errors detected at semantic analysis or later, such as a misspelled table or column name, do not have this effect. + + + Lastly, note that all the statements within the Query message will + observe the same value of statement_timestamp(), + since that timestamp is updated only upon receipt of the Query + message. This will result in them all observing the same + value of transaction_timestamp() as well, + except in cases where the query string ends a previously-started + transaction and begins a new one. + @@ -1621,7 +1756,7 @@ SELCT 1/0; Likewise the server expects the client to not begin the SSL negotiation until it receives the server's - single byte response to the SSL request. If the + single-byte response to the SSL request. If the client begins the SSL negotiation immediately without waiting for the server response to be received it can reduce connection latency by one round-trip. However this comes at the cost of not being @@ -2225,6 +2360,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" The name of the slot to create. Must be a valid replication slot name (see ). + The name cannot be pg_conflict_detection as it + is reserved for the conflict detection. @@ -2538,8 +2675,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" - - XLogData (B) + + WALData (B) @@ -2587,11 +2724,11 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" - A single WAL record is never split across two XLogData messages. + A single WAL record is never split across two WALData messages. When a WAL record crosses a WAL page boundary, and is therefore already split using continuation records, it can be split at the page boundary. In other words, the first main WAL record and its - continuation records can be sent in different XLogData messages. + continuation records can be sent in different WALData messages. @@ -2643,6 +2780,65 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" + + + Primary status update (B) + + + + Byte1('s') + + + Identifies the message as a primary status update. + + + + + + Int64 + + + The latest WAL write position on the server. + + + + + + Int64 + + + The oldest transaction ID that is currently in the commit phase on + the server, along with its epoch. The most significant 32 bits are + the epoch. The least significant 32 bits are the transaction ID. + If no transactions are active on the server, this number will be + the next transaction ID to be assigned. + + + + + + Int64 + + + The next transaction ID to be assigned on the server, along with + its epoch. The most significant 32 bits are the epoch. The least + significant 32 bits are the transaction ID. + + + + + + Int64 + + + The server's system clock at the time of transmission, as + microseconds since midnight on 2000-01-01. + + + + + + @@ -2787,6 +2983,33 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" + + + Request primary status update (F) + + + + Byte1('p') + + + Identifies the message as a request for a primary status update. + + + + + + Int64 + + + The client's system clock at the time of transmission, as + microseconds since midnight on 2000-01-01. + + + + + + + @@ -2852,7 +3075,7 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" The name of an option passed to the slot's logical decoding output - plugin. See for + plugin. See for options that are accepted by the standard (pgoutput) plugin. @@ -2937,8 +3160,7 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" Sets the label of the backup. If none is specified, a backup label of base backup will be used. The quoting rules - for the label are the same as a standard SQL string with - turned on. + for the label are the same as for a standard SQL string. @@ -3420,128 +3642,15 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" the physical streaming replication protocol. - - PostgreSQL logical decoding supports output - plugins. pgoutput is the standard one used for - the built-in logical replication. - - Logical Streaming Replication Parameters - Using the START_REPLICATION command, - pgoutput accepts the following options: - - - - - proto_version - - - - Protocol version. Currently versions 1, 2, - 3, and 4 are supported. A valid - version is required. - - - Version 2 is supported only for server version 14 - and above, and it allows streaming of large in-progress transactions. - - - Version 3 is supported only for server version 15 - and above, and it allows streaming of two-phase commits. - - - Version 4 is supported only for server version 16 - and above, and it allows streams of large in-progress transactions to - be applied in parallel. - - - - - - - publication_names - - - - Comma-separated list of publication names for which to subscribe - (receive changes). The individual publication names are treated - as standard objects names and can be quoted the same as needed. - At least one publication name is required. - - - - - - - binary - - - - Boolean option to use binary transfer mode. Binary mode is faster - than the text mode but slightly less robust. - - - - - - - messages - - - - Boolean option to enable sending the messages that are written - by pg_logical_emit_message. - - - - - - - streaming - - - - Boolean option to enable streaming of in-progress transactions. - It accepts an additional value "parallel" to enable sending extra - information with some messages to be used for parallelisation. - Minimum protocol version 2 is required to turn it on. Minimum protocol - version 4 is required for the "parallel" option. - - - - - - - two_phase - - - - Boolean option to enable two-phase transactions. Minimum protocol - version 3 is required to turn it on. - - - - - - - origin - - - - Option to send changes by their origin. Possible values are - none to only send the changes that have no origin - associated, or any - to send the changes regardless of their origin. This can be used - to avoid loops (infinite replication of the same data) among - replication nodes. - - - - - + The START_REPLICATION command can pass + options to the logical decoding output plugin associated + with the specified replication slot. + See for options + that are accepted by the standard (pgoutput) plugin. @@ -4146,7 +4255,7 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" message, indicated by the length field. - The maximum key length is 256 bytes. The + The minimum and maximum key length are 4 and 256 bytes, respectively. The PostgreSQL server only sends keys up to 32 bytes, but the larger maximum size allows for future server versions, as well as connection poolers and other middleware, to use @@ -4337,7 +4446,7 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" - Int32(16) + Int32 Length of message contents in bytes, including self. @@ -6081,13 +6190,14 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" - Int32(196608) + Int32 The protocol version number. The most significant 16 bits are - the major version number (3 for the protocol described here). - The least significant 16 bits are the minor version number - (0 for the protocol described here). + the major version number. The least significant 16 bits are the minor + version number. As an example protocol version 3.2 is represented as + 196610 in decimal or more clearly as + 0x00030002 in hexadecimal. @@ -6161,7 +6271,9 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" In addition to the above, other parameters may be listed. Parameter names beginning with _pq_. are - reserved for use as protocol extensions, while others are + reserved for use as + protocol extensions, + while others are treated as run-time parameters to be set at backend start time. Such settings will be applied during backend start (after parsing the command-line arguments if any) and will @@ -7292,8 +7404,8 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" Int64 (XLogRecPtr) - The LSN of the abort. This field is available since protocol version - 4. + The LSN of the abort operation, present only when streaming is set to parallel. + This field is available since protocol version 4. @@ -7302,9 +7414,9 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" Int64 (TimestampTz) - Abort timestamp of the transaction. The value is in number - of microseconds since PostgreSQL epoch (2000-01-01). This field is - available since protocol version 4. + Abort timestamp of the transaction, present only when streaming is set to + parallel. The value is in number of microseconds since PostgreSQL epoch (2000-01-01). + This field is available since protocol version 4. diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml index a326960ff4dfb..ec4ca01cd1644 100644 --- a/doc/src/sgml/queries.sgml +++ b/doc/src/sgml/queries.sgml @@ -410,7 +410,7 @@ FROM table_reference , table_r - To put this together, assume we have tables t1: + To put this together, assume we have tables t1: num | name -----+------ @@ -418,7 +418,7 @@ FROM table_reference , table_r 2 | b 3 | c - and t2: + and t2: num | value -----+------- @@ -863,6 +863,11 @@ ORDER BY p; to columns provided by preceding FROM items in any case. + + A GRAPH_TABLE FROM item can also + always contain lateral references. + + A LATERAL item can appear at the top level in the FROM list, or within a JOIN tree. In the latter @@ -1079,7 +1084,7 @@ SELECT select_list In the second query, we could not have written SELECT * FROM test1 GROUP BY x, because there is no single value - for the column y that could be associated with each + for the column y that could be associated with each group. The grouped-by columns can be referenced in the select list since they have a single value in each group. @@ -1145,10 +1150,36 @@ SELECT product_id, p.name, (sum(s.units) * p.price) AS sales In strict SQL, GROUP BY can only group by columns of - the source table but PostgreSQL extends + the source table, but PostgreSQL extends this to also allow GROUP BY to group by columns in the select list. Grouping by value expressions instead of simple - column names is also allowed. + column names is also allowed (but GROUP BY + expressions cannot contain aggregate functions or window functions). + + + + PostgreSQL also supports the syntax GROUP BY ALL, + which is equivalent to explicitly writing all select-list entries that + do not contain either an aggregate function or a window function. + This can greatly simplify ad-hoc exploration of data. + As an example, these queries are equivalent: + +=> SELECT a, b, a + b, sum(c) FROM test1 GROUP BY ALL; + a | b | ?column? | sum +---+---+----------+---- + 1 | 4 | 5 | 9 + 2 | 5 | 7 | 12 + 3 | 6 | 9 | 15 +(3 rows) + +=> SELECT a, b, a + b, sum(c) FROM test1 GROUP BY a, b, a + b; + a | b | ?column? | sum +---+---+----------+---- + 1 | 4 | 5 | 9 + 2 | 5 | 7 | 12 + 3 | 6 | 9 | 15 +(3 rows) + @@ -2206,7 +2237,7 @@ WITH RECURSIVE included_parts(sub_part, part, quantity) AS ( FROM included_parts pr, parts p WHERE p.part = pr.sub_part ) -SELECT sub_part, SUM(quantity) as total_quantity +SELECT sub_part, SUM(quantity) AS total_quantity FROM included_parts GROUP BY sub_part @@ -2577,7 +2608,7 @@ WHERE w2.key = 123; undesirable is WITH w AS ( - SELECT key, very_expensive_function(val) as f FROM some_table + SELECT key, very_expensive_function(val) AS f FROM some_table ) SELECT * FROM w AS w1 JOIN w AS w2 ON w1.f = w2.f; @@ -2745,4 +2776,161 @@ SELECT * FROM t; + + Graph Queries + + + This section describes the sublanguage for querying property graphs, + defined as described in . + + + + Overview + + + Consider this example from : + +-- get list of customers active today +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders]->(o IS orders WHERE o.ordered_when = current_date) COLUMNS (c.name AS customer_name)); + + The graph query part happens inside the GRAPH_TABLE + construct. As far as the rest of the query is concerned, this acts like a + table function in that it produces a computed table as output. Like other + FROM clause elements, table alias and column alias + names can be assigned to the result, and the result can be joined with + other tables, subsequently filtered, and so on, for example: + +SELECT ... FROM GRAPH_TABLE (mygraph MATCH ... COLUMNS (...)) AS myresult (a, b, c) JOIN othertable USING (a) WHERE b > 0 ORDER BY c; + + + + + The GRAPH_TABLE clause consists of the graph name, + followed by the keyword MATCH, followed by a graph + pattern expression (see below), followed by the keyword + COLUMNS and a column list. + + + + + Graph Patterns + + + The core of the graph querying functionality is the graph pattern, which + appears after the keyword MATCH. Formally, a graph + pattern consists of one or more path patterns. A path is a sequence of + graph elements, starting and ending with a vertex and alternating between + vertices and edges. A path pattern is a syntactic expressions that + matches paths. + + + + A path pattern thus matches a sequence of vertices and edges. The + simplest possible path pattern is + +() + + which matches a single vertex. The next simplest pattern would be + +()-[]->() + + which matches a vertex followed by an edge followed by a vertex. The + characters () are a vertex pattern and the characters + -[]-> are an edge pattern. + + + + These characters can also be separated by whitespace, for example: + +( ) - [ ] - > ( ) + + + + + + A way to remember these symbols is that in visual representations of + property graphs, vertices are usually circles (like + ()) and edges have rectangular labels (like + []). + + + + + The above patterns would match any vertex, or any two vertices connected + by any edge, which isn't very interesting. Normally, we want to search + for elements (vertices and edges) that have certain characteristics. + These characteristics are written in between the parentheses or brackets. + (This is also called an element pattern filler.) Typically, we would + search for elements with a certain label. This is written by IS + labelname. For example, this would + match all vertices with the label person: + +(IS person) + + The next + example would match a vertex with the label person + connected to a vertex with the label account connected + by an edge with the label has. + +(IS person)-[IS has]->(IS account) + + Multiple labels can also be matched, using or semantics: + +(IS person)-[IS has]->(IS account|creditcard) + + + + + Recall that edges are directed. The other direction is also possible in a + path pattern, for example: + +(IS account)<-[IS has]-(IS person) + + It is also possible to match both directions: + +(IS person)-[IS is_friend_of]-(IS person) + + This has a meaning of or: An edge in either direction would + match. + + + + In many cases, the edge patterns don't need a filler. (All the filtering + then happens on the vertices.) For these cases, an abbreviated edge + pattern syntax is available that omits the brackets, for example: + +(IS person)->(IS account) +(IS account)<-(IS person) +(IS person)-(IS person) + + As is often the case, abbreviated syntax can make expressions more compact + but also sometimes harder to understand. + + + + Furthermore, it is possible to define graph pattern variables in the path + pattern expressions. These are bound to the matched elements and can be + used to refer to the property values from those elements. The most + important use is to use them in the COLUMNS clause to + define the tabular result of the GRAPH_TABLE clause. + For example (assuming appropriate definitions of the property graph as + well as the underlying tables): + +GRAPH_TABLE (mygraph MATCH (p IS person)-[h IS has]->(a IS account) + COLUMNS (p.name AS person_name, h.since AS has_account_since, a.num AS account_number) + + WHERE clauses can be used inside element patterns to + filter matches: + +(IS person)-[IS has]->(a IS account WHERE a.type = 'savings') + + + + + + + + + + diff --git a/doc/src/sgml/query.sgml b/doc/src/sgml/query.sgml index 727a0cb185fb2..b190f28d41ea6 100644 --- a/doc/src/sgml/query.sgml +++ b/doc/src/sgml/query.sgml @@ -264,8 +264,18 @@ COPY weather FROM '/home/user/weather.txt'; where the file name for the source file must be available on the machine running the backend process, not the client, since the backend process - reads the file directly. You can read more about the - COPY command in . + reads the file directly. The data inserted above into the weather table + could also be inserted from a file containing (values are separated by a + tab character): + + +San Francisco 46 50 0.25 1994-11-27 +San Francisco 43 57 0.0 1994-11-29 +Hayward 37 54 \N 1994-11-29 + + + You can read more about the COPY command in + . diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index f5be638867abe..e1a56c362219a 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -27,6 +27,7 @@ Complete list of usable sgml source files in this directory. + @@ -79,6 +80,7 @@ Complete list of usable sgml source files in this directory. + @@ -127,6 +129,7 @@ Complete list of usable sgml source files in this directory. + @@ -167,6 +170,7 @@ Complete list of usable sgml source files in this directory. + @@ -188,6 +192,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_database.sgml b/doc/src/sgml/ref/alter_database.sgml index 9da8920e12eff..1fc051e11a311 100644 --- a/doc/src/sgml/ref/alter_database.sgml +++ b/doc/src/sgml/ref/alter_database.sgml @@ -83,7 +83,7 @@ ALTER DATABASE name RESET ALL must be empty for this database, and no one can be connected to the database. Tables and indexes in non-default tablespaces are unaffected. The method used to copy files to the new tablespace - is affected by the setting. + is affected by the setting. diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml index c819c7bb4e3c4..60218fcd01c07 100644 --- a/doc/src/sgml/ref/alter_extension.sgml +++ b/doc/src/sgml/ref/alter_extension.sgml @@ -46,6 +46,7 @@ ALTER EXTENSION name DROP object_name USING index_method | [ PROCEDURAL ] LANGUAGE object_name | PROCEDURE procedure_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | + PROPERTY GRAPH object_name | ROUTINE routine_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | SCHEMA object_name | SEQUENCE object_name | @@ -179,7 +180,7 @@ ALTER EXTENSION name DROP diff --git a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml index dc0957d965a62..640c02893cf01 100644 --- a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation ALTER FOREIGN DATA WRAPPER name [ HANDLER handler_function | NO HANDLER ] [ VALIDATOR validator_function | NO VALIDATOR ] + [ CONNECTION connection_function | NO CONNECTION ] [ OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ... ]) ] ALTER FOREIGN DATA WRAPPER name OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ALTER FOREIGN DATA WRAPPER name RENAME TO new_name @@ -112,6 +113,25 @@ ALTER FOREIGN DATA WRAPPER name REN + + CONNECTION connection_function + + + Specifies a new connection function for the foreign-data wrapper. + + + + + + NO CONNECTION + + + This is used to specify that the foreign-data wrapper should no + longer have a connection function. + + + + OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ... ] ) diff --git a/doc/src/sgml/ref/alter_foreign_table.sgml b/doc/src/sgml/ref/alter_foreign_table.sgml index e2da3cc719f90..228067f087cee 100644 --- a/doc/src/sgml/ref/alter_foreign_table.sgml +++ b/doc/src/sgml/ref/alter_foreign_table.sgml @@ -32,7 +32,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] namewhere action is one of: - ADD [ COLUMN ] column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ] + ADD [ COLUMN ] [ IF NOT EXISTS ] column_name data_type [ COLLATE collation ] [ column_constraint [ ... ] ] DROP [ COLUMN ] [ IF EXISTS ] column_name [ RESTRICT | CASCADE ] ALTER [ COLUMN ] column_name [ SET DATA ] TYPE data_type [ COLLATE collation ] ALTER [ COLUMN ] column_name SET DEFAULT expression @@ -58,7 +58,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + Description @@ -66,12 +66,14 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - - ADD COLUMN + + ADD [ COLUMN ] [ IF NOT EXISTS ] This form adds a new column to the foreign table, using the same syntax as CREATE FOREIGN TABLE. + If IF NOT EXISTS is specified and a column already + exists with this name, no error is thrown. Unlike the case when adding a column to a regular table, nothing happens to the underlying storage: this action simply declares that some new column is now accessible through the foreign table. @@ -79,8 +81,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - - DROP COLUMN [ IF EXISTS ] + + DROP [ COLUMN ] [ IF EXISTS ] This form drops a column from a foreign table. @@ -94,7 +96,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET DATA TYPE @@ -106,7 +108,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET/DROP DEFAULT @@ -118,7 +120,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET/DROP NOT NULL @@ -127,7 +129,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET STATISTICS @@ -140,7 +142,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET ( attribute_option = value [, ... ] ) RESET ( attribute_option [, ... ] ) @@ -152,7 +154,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET STORAGE @@ -167,7 +169,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + ADD table_constraint [ NOT VALID ] @@ -190,7 +192,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + VALIDATE CONSTRAINT @@ -201,7 +203,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + DROP CONSTRAINT [ IF EXISTS ] @@ -213,7 +215,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + DISABLE/ENABLE [ REPLICA | ALWAYS ] TRIGGER @@ -224,7 +226,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET WITHOUT OIDS @@ -235,7 +237,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + INHERIT parent_table @@ -247,7 +249,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + NO INHERIT parent_table @@ -257,7 +259,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + OWNER @@ -267,7 +269,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + OPTIONS ( [ ADD | SET | DROP ] option ['value'] [, ... ] ) @@ -282,7 +284,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + RENAME @@ -292,7 +294,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + SET SCHEMA @@ -332,12 +334,12 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + Parameters - + name @@ -351,7 +353,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + column_name @@ -360,7 +362,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + new_column_name @@ -369,7 +371,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + new_name @@ -378,7 +380,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + data_type @@ -388,7 +390,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + table_constraint @@ -397,7 +399,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + constraint_name @@ -406,7 +408,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + CASCADE @@ -418,7 +420,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + RESTRICT @@ -428,7 +430,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + trigger_name @@ -437,7 +439,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + ALL @@ -449,7 +451,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + USER @@ -459,7 +461,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + parent_table @@ -468,7 +470,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + new_owner @@ -477,7 +479,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + new_schema @@ -488,7 +490,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + Notes @@ -510,7 +512,7 @@ ALTER FOREIGN TABLE [ IF EXISTS ] name - + Examples @@ -528,7 +530,7 @@ ALTER FOREIGN TABLE myschema.distributors OPTIONS (ADD opt1 'value', SET opt2 'v - + Compatibility diff --git a/doc/src/sgml/ref/alter_property_graph.sgml b/doc/src/sgml/ref/alter_property_graph.sgml new file mode 100644 index 0000000000000..f517f2b2d7a7d --- /dev/null +++ b/doc/src/sgml/ref/alter_property_graph.sgml @@ -0,0 +1,299 @@ + + + + + ALTER PROPERTY GRAPH + + + + ALTER PROPERTY GRAPH + 7 + SQL - Language Statements + + + + ALTER PROPERTY GRAPH + change the definition of an SQL-property graph + + + + +ALTER PROPERTY GRAPH name ADD + [ {VERTEX|NODE} TABLES ( vertex_table_definition [, ...] ) ] + [ {EDGE|RELATIONSHIP} TABLES ( edge_table_definition [, ...] ) ] + +ALTER PROPERTY GRAPH name DROP + {VERTEX|NODE} TABLES ( vertex_table_alias [, ...] ) [ CASCADE | RESTRICT ] + +ALTER PROPERTY GRAPH name DROP + {EDGE|RELATIONSHIP} TABLES ( edge_table_alias [, ...] ) [ CASCADE | RESTRICT ] + +ALTER PROPERTY GRAPH name ALTER + {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE element_table_alias + { ADD LABEL label_name [ NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES ( { expression [ AS property_name ] } [, ...] ) ] } [ ... ] + +ALTER PROPERTY GRAPH name ALTER + {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE element_table_alias + DROP LABEL label_name [ CASCADE | RESTRICT ] + +ALTER PROPERTY GRAPH name ALTER + {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE element_table_alias + ALTER LABEL label_name ADD PROPERTIES ( { expression [ AS property_name ] } [, ...] ) + +ALTER PROPERTY GRAPH name ALTER + {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE element_table_alias + ALTER LABEL label_name DROP PROPERTIES ( property_name [, ...] ) [ CASCADE | RESTRICT ] + +ALTER PROPERTY GRAPH name OWNER TO { new_owner | CURRENT_USER | SESSION_USER } +ALTER PROPERTY GRAPH name RENAME TO new_name +ALTER PROPERTY GRAPH [ IF EXISTS ] name SET SCHEMA new_schema + + + + + Description + + + ALTER PROPERTY GRAPH changes the definition of an + existing property graph. There are several subforms: + + + + ADD {VERTEX|NODE|EDGE|RELATIONSHIP} TABLES + + + This form adds new vertex or edge tables to the property graph, using the + same syntax as CREATE + PROPERTY GRAPH. + + + + + + DROP {VERTEX|NODE|EDGE|RELATIONSHIP} TABLES + + + This form removes vertex or edge tables from the property graph. (Only + the association of the tables with the graph is removed. The tables + themselves are not dropped.) + + + + + + ALTER {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE ... ADD LABEL + + + This form adds a new label to an existing vertex or edge table, using + the same syntax as CREATE PROPERTY + GRAPH. + + + + + + ALTER {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE ... DROP LABEL + + + This form removes a label from an existing vertex or edge table. + + + + + + ALTER {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE ... ALTER LABEL ... ADD PROPERTIES + + + This form adds new properties to an existing label on an existing + vertex or edge table. + + + + + + ALTER {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE ... ALTER LABEL ... DROP PROPERTIES + + + This form removes properties from an existing label on an existing + vertex or edge table. + + + + + + OWNER + + + This form changes the owner of the property graph to the specified user. + + + + + + RENAME + + + This form changes the name of a property graph. + + + + + + SET SCHEMA + + + This form moves the property graph into another schema. + + + + + + + + You must own the property graph to use ALTER PROPERTY + GRAPH. To change a property graph's schema, you must also have + CREATE privilege on the new schema. To alter the owner, + you must be able to SET ROLE to the new owning role, and + that role must have CREATE privilege on the property + graph's schema. (These restrictions enforce that altering the owner + doesn't do anything you couldn't do by dropping and recreating the property + graph. However, a superuser can alter ownership of any property graph + anyway.) + + + + + Parameters + + + + name + + + The name (optionally schema-qualified) of a property graph to be altered. + + + + + + IF EXISTS + + + Do not throw an error if the property graph does not exist. A notice is + issued in this case. + + + + + + vertex_table_definition + edge_table_definition + + + See CREATE PROPERTY + GRAPH. + + + + + + vertex_table_alias + edge_table_alias + + + The alias of an existing vertex or edge table to operate on. (Note that + the alias is potentially different from the name of the underlying + table, if the vertex or edge table was created with AS + alias.) + + + + + + label_name + property_name + expression + + + See CREATE PROPERTY + GRAPH. + + + + + + new_owner + + + The user name of the new owner of the property graph. + + + + + + new_name + + + The new name for the property graph. + + + + + + new_schema + + + The new schema for the property graph. + + + + + + + + Notes + + + The consistency checks on a property graph described at must be maintained by + ALTER PROPERTY GRAPH operations. In some cases, it + might be necessary to make multiple alterations in a single command to + satisfy the checks. + + + + + Examples + + + +ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (v2); + +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v1 DROP LABEL foo; + +ALTER PROPERTY GRAPH g1 RENAME TO g2; + + + + + Compatibility + + + ALTER PROPERTY GRAPH conforms to ISO/IEC 9075-16 + (SQL/PGQ). + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml index d5ea383e8bc95..aa32bb169e90a 100644 --- a/doc/src/sgml/ref/alter_publication.sgml +++ b/doc/src/sgml/ref/alter_publication.sgml @@ -22,16 +22,38 @@ PostgreSQL documentation ALTER PUBLICATION name ADD publication_object [, ...] -ALTER PUBLICATION name SET publication_object [, ...] -ALTER PUBLICATION name DROP publication_object [, ...] +ALTER PUBLICATION name DROP publication_drop_object [, ...] +ALTER PUBLICATION name SET { publication_object [, ...] | publication_all_object [, ... ] } ALTER PUBLICATION name SET ( publication_parameter [= value] [, ... ] ) ALTER PUBLICATION name OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ALTER PUBLICATION name RENAME TO new_name where publication_object is one of: - TABLE [ ONLY ] table_name [ * ] [ ( column_name [, ... ] ) ] [ WHERE ( expression ) ] [, ... ] + TABLE table_and_columns [, ... ] TABLES IN SCHEMA { schema_name | CURRENT_SCHEMA } [, ... ] + +and publication_all_object is one of: + + ALL TABLES [ EXCEPT ( except_table_object [, ... ] ) ] + ALL SEQUENCES + +and publication_drop_object is one of: + + TABLE table_object [, ... ] + TABLES IN SCHEMA { schema_name | CURRENT_SCHEMA } [, ... ] + +and table_and_columns is: + + table_object [ ( column_name [, ... ] ) ] [ WHERE ( expression ) ] + +and except_table_object is: + + TABLE table_object [, ... ] + +and table_object is: + + [ ONLY ] table_name [ * ] @@ -44,21 +66,48 @@ ALTER PUBLICATION name RENAME TO - The first three variants change which tables/schemas are part of the - publication. The SET clause will replace the list of - tables/schemas in the publication with the specified list; the existing - tables/schemas that were present in the publication will be removed. The - ADD and DROP clauses will add and - remove one or more tables/schemas from the publication. Note that adding - tables/schemas to a publication that is already subscribed to will require an + The first two variants modify which tables/schemas are part of the + publication. The ADD and DROP clauses + will add and remove one or more tables/schemas from the publication. + + + + The third variant either modifies the included tables/schemas + or marks the publication as FOR ALL SEQUENCES or + FOR ALL TABLES, optionally using + EXCEPT to exclude specific tables. The + SET ALL TABLES clause can transform an empty publication, + or one defined for ALL SEQUENCES (or both + ALL TABLES and ALL SEQUENCES), into + a publication defined for ALL TABLES. Likewise, + SET ALL SEQUENCES can convert an empty publication, or + one defined for ALL TABLES (or both + ALL TABLES and ALL SEQUENCES), into a + publication defined for ALL SEQUENCES. In addition, + SET ALL TABLES can be used to update the tables specified + in the EXCEPT clause of a + FOR ALL TABLES publication. If EXCEPT + is specified with a list of tables, the existing exclusion list is replaced + with the specified tables. If EXCEPT is omitted, the + existing exclusion list is cleared. The SET clause, when + used with a publication defined with FOR TABLE or + FOR TABLES IN SCHEMA, replaces the list of tables/schemas + in the publication with the specified list; the existing tables or schemas + that were present in the publication will be removed. + + + + Note that adding tables/except tables/schemas to a publication that is + already subscribed to will require an - ALTER SUBSCRIPTION ... REFRESH PUBLICATION action on the - subscribing side in order to become effective. Note also that - DROP TABLES IN SCHEMA will not drop any schema tables - that were specified using + ALTER SUBSCRIPTION ... REFRESH PUBLICATION action + on the subscribing side in order to become effective. Likewise altering a + publication to set ALL TABLES or to set or unset + ALL SEQUENCES also requires the subscriber to refresh the + publication. Note also that DROP TABLES IN SCHEMA will + not drop any schema tables that were specified using FOR TABLE/ - ADD TABLE, and the combination of DROP - with a WHERE clause is not allowed. + ADD TABLE. @@ -75,15 +124,17 @@ ALTER PUBLICATION name RENAME TO You must own the publication to use ALTER PUBLICATION. Adding a table to a publication additionally requires owning that table. - The ADD TABLES IN SCHEMA and - SET TABLES IN SCHEMA to a publication requires the + The ADD TABLES IN SCHEMA, + SET TABLES IN SCHEMA, SET ALL TABLES, + and SET ALL SEQUENCES to a publication requires the invoking user to be a superuser. To alter the owner, you must be able to SET ROLE to the new owning role, and that role must have CREATE privilege on the database. Also, the new owner of a - FOR ALL TABLES - or FOR TABLES IN SCHEMA + FOR TABLES IN SCHEMA + or FOR ALL TABLES + or FOR ALL SEQUENCES publication must be a superuser. However, a superuser can change the ownership of a publication regardless of these restrictions. @@ -153,6 +204,7 @@ ALTER PUBLICATION name RENAME TO This clause alters publication parameters originally set by . See there for more information. + This clause is not applicable to sequences. @@ -212,6 +264,19 @@ ALTER PUBLICATION mypublication ADD TABLE users (user_id, firstname), department Change the set of columns published for a table: ALTER PUBLICATION mypublication SET TABLE users (user_id, firstname, lastname), TABLE departments; + + + + Replace the table list in the publication's EXCEPT clause: + +ALTER PUBLICATION mypublication SET ALL TABLES EXCEPT (TABLE users, departments); + + + + Reset the publication to be a FOR ALL TABLES publication with no excluded + tables: + +ALTER PUBLICATION mypublication SET ALL TABLES; diff --git a/doc/src/sgml/ref/alter_sequence.sgml b/doc/src/sgml/ref/alter_sequence.sgml index a998ccc7ead2f..db7b98fdf8bde 100644 --- a/doc/src/sgml/ref/alter_sequence.sgml +++ b/doc/src/sgml/ref/alter_sequence.sgml @@ -207,8 +207,8 @@ ALTER SEQUENCE [ IF EXISTS ] name S The optional clause RESTART [ WITH restart ] changes the current value of the sequence. This is similar to calling the - setval function with is_called = - false: the specified value will be returned by the + setval function with is_called + = false: the specified value will be returned by the next call of nextval. Writing RESTART with no restart value is equivalent to supplying diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml index fdc648d007f1c..f215fb0e5a2b8 100644 --- a/doc/src/sgml/ref/alter_subscription.sgml +++ b/doc/src/sgml/ref/alter_subscription.sgml @@ -21,11 +21,13 @@ PostgreSQL documentation +ALTER SUBSCRIPTION name SERVER servername ALTER SUBSCRIPTION name CONNECTION 'conninfo' ALTER SUBSCRIPTION name SET PUBLICATION publication_name [, ...] [ WITH ( publication_option [= value] [, ... ] ) ] ALTER SUBSCRIPTION name ADD PUBLICATION publication_name [, ...] [ WITH ( publication_option [= value] [, ... ] ) ] ALTER SUBSCRIPTION name DROP PUBLICATION publication_name [, ...] [ WITH ( publication_option [= value] [, ... ] ) ] ALTER SUBSCRIPTION name REFRESH PUBLICATION [ WITH ( refresh_option [= value] [, ... ] ) ] +ALTER SUBSCRIPTION name REFRESH SEQUENCES ALTER SUBSCRIPTION name ENABLE ALTER SUBSCRIPTION name DISABLE ALTER SUBSCRIPTION name SET ( subscription_parameter [= value] [, ... ] ) @@ -101,13 +103,24 @@ ALTER SUBSCRIPTION name RENAME TO < + + SERVER servername + + + This clause replaces the foreign server or connection string originally + set by with the foreign server + servername. + + + + CONNECTION 'conninfo' - This clause replaces the connection string originally set by - . See there for more - information. + This clause replaces the foreign server or connection string originally + set by with the connection + string conninfo. @@ -139,9 +152,10 @@ ALTER SUBSCRIPTION name RENAME TO < refresh (boolean) - When false, the command will not try to refresh table information. - REFRESH PUBLICATION should then be executed separately. - The default is true. + When false, the command will not try to refresh + table and sequence information. REFRESH PUBLICATION + should then be executed separately. The default is + true. @@ -158,13 +172,19 @@ ALTER SUBSCRIPTION name RENAME TO < REFRESH PUBLICATION - Fetch missing table information from publisher. This will start + Fetch missing table and sequence information from the publisher. This will start replication of tables that were added to the subscribed-to publications since CREATE SUBSCRIPTION or the last invocation of REFRESH PUBLICATION. + + The system catalog pg_subscription_rel + is updated to record all tables and sequences known to the subscription, + that are still part of the publication. + + refresh_option specifies additional options for the refresh operation. The supported options are: @@ -174,14 +194,25 @@ ALTER SUBSCRIPTION name RENAME TO < copy_data (boolean) - Specifies whether to copy pre-existing data in the publications - that are being subscribed to when the replication starts. - The default is true. + Specifies whether to copy pre-existing data for tables and synchronize + sequences in the publications that are being subscribed to when the replication + starts. The default is true. Previously subscribed tables are not copied, even if a table's row filter WHERE clause has since been modified. + + Previously subscribed sequences are not re-synchronized. To do that, + use + ALTER SUBSCRIPTION ... REFRESH SEQUENCES. + + + See for recommendations on how + to handle any warnings about sequence definition differences between + the publisher and the subscriber, which might occur when + copy_data = true. + See for details of how copy_data = true can interact with the @@ -200,6 +231,30 @@ ALTER SUBSCRIPTION name RENAME TO < + + REFRESH SEQUENCES + + + Re-synchronize sequence data with the publisher. Unlike + + ALTER SUBSCRIPTION ... REFRESH PUBLICATION which + only has the ability to synchronize newly added sequences, + REFRESH SEQUENCES will re-synchronize the sequence + data for all currently subscribed sequences. It does not add or remove + sequences from the subscription to match the publication. + + + See for + recommendations on how to handle any warnings about sequence definition + differences between the publisher and the subscriber. + + + See for recommendations on how to + identify and handle out-of-sync sequences. + + + + ENABLE @@ -235,8 +290,11 @@ ALTER SUBSCRIPTION name RENAME TO < password_required, run_as_owner, origin, - failover, and - two_phase. + failover, + two_phase, + retain_dead_tuples, + max_retention_duration, and + wal_receiver_timeout. Only a superuser can set password_required = false. @@ -261,8 +319,9 @@ ALTER SUBSCRIPTION name RENAME TO < - The failover - and two_phase + The failover, + two_phase, and + retain_dead_tuples parameters can only be altered when the subscription is disabled. @@ -285,6 +344,14 @@ ALTER SUBSCRIPTION name RENAME TO < option is changed from true to false, the publisher will replicate the transactions again when they are committed. + + + If the retain_dead_tuples + option is altered to false and no other subscription + has this option enabled, the replication slot named + pg_conflict_detection, created to retain + dead tuples for conflict detection, will be dropped. + diff --git a/doc/src/sgml/ref/alter_system.sgml b/doc/src/sgml/ref/alter_system.sgml index 1bde66d6ad2d3..b28919d1b262a 100644 --- a/doc/src/sgml/ref/alter_system.sgml +++ b/doc/src/sgml/ref/alter_system.sgml @@ -84,6 +84,8 @@ ALTER SYSTEM RESET ALL constants, identifiers, numbers, or comma-separated lists of these, as appropriate for the particular parameter. Values that are neither numbers nor valid identifiers must be quoted. + If the parameter accepts a list of values, NULL + can be written to specify an empty list. DEFAULT can be written to specify removing the parameter and its value from postgresql.auto.conf. @@ -136,7 +138,15 @@ ALTER SYSTEM SET wal_level = replica; in postgresql.conf: ALTER SYSTEM RESET wal_level; - + + + + + Set the list of preloaded extension modules to be empty: + +ALTER SYSTEM SET shared_preload_libraries TO NULL; + + diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index d63f3a621acc6..1f9a456fd336a 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -37,6 +37,12 @@ ALTER TABLE [ IF EXISTS ] name ATTACH PARTITION partition_name { FOR VALUES partition_bound_spec | DEFAULT } ALTER TABLE [ IF EXISTS ] name DETACH PARTITION partition_name [ CONCURRENTLY | FINALIZE ] +ALTER TABLE [ IF EXISTS ] name + MERGE PARTITIONS (partition_name1, partition_name2 [, ...]) INTO partition_name +ALTER TABLE [ IF EXISTS ] name + SPLIT PARTITION partition_name INTO + (PARTITION partition_name1 { FOR VALUES partition_bound_spec | DEFAULT }, + PARTITION partition_name2 { FOR VALUES partition_bound_spec | DEFAULT } [, ...]) where action is one of: @@ -157,7 +163,7 @@ WITH ( MODULUS numeric_literal, REM - ADD COLUMN [ IF NOT EXISTS ] + ADD [ COLUMN ] [ IF NOT EXISTS ] This form adds a new column to the table, using the same syntax as @@ -169,7 +175,7 @@ WITH ( MODULUS numeric_literal, REM - DROP COLUMN [ IF EXISTS ] + DROP [ COLUMN ] [ IF EXISTS ] This form drops a column from a table. Indexes and @@ -210,6 +216,9 @@ WITH ( MODULUS numeric_literal, REM When this form is used, the column's statistics are removed, so running ANALYZE on the table afterwards is recommended. + For a virtual generated column, ANALYZE + is not necessary unless extended statistics are defined on it, + since such columns do not have statistics by default. @@ -240,9 +249,10 @@ WITH ( MODULUS numeric_literal, REM provided none of the records in the table contain a NULL value for the column. Ordinarily this is checked during the ALTER TABLE by scanning the - entire table; however, if a valid CHECK constraint is - found which proves no NULL can exist, then the - table scan is skipped. + entire table, unless NOT VALID is specified; + however, if a valid CHECK constraint exists + (and is not dropped in the same command) which proves no + NULL can exist, then the table scan is skipped. If a column has an invalid not-null constraint, SET NOT NULL validates it. @@ -269,6 +279,19 @@ WITH ( MODULUS numeric_literal, REM This form replaces the expression of a generated column. Existing data in a stored generated column is rewritten and all the future changes will apply the new generation expression. + Replacing the expression of a virtual generated column does not require a + table rewrite, but if the column is used in a constraint, the table will + be scanned to check that existing rows meet the constraint. + + + + When this form is used on a stored generated column, its statistics + are removed, so running + ANALYZE + on the table afterwards is recommended. + For a virtual generated column, ANALYZE + is not necessary unless extended statistics are defined on it, + since such columns do not have statistics by default. @@ -347,6 +370,12 @@ WITH ( MODULUS numeric_literal, REM PostgreSQL query planner, refer to . + + This form is not supported on virtual generated columns, since such + columns do not have statistics by default. If extended statistics are + defined on such columns, the statistics-gathering target may be set on + the extended statistics object using . + SET STATISTICS acquires a SHARE UPDATE EXCLUSIVE lock. @@ -364,24 +393,22 @@ WITH ( MODULUS numeric_literal, REM n_distinct_inherited, which override the number-of-distinct-values estimates made by subsequent ANALYZE - operations. n_distinct affects the statistics for the table - itself, while n_distinct_inherited affects the statistics - gathered for the table plus its inheritance children. When set to a - positive value, ANALYZE will assume that the column contains - exactly the specified number of distinct nonnull values. When set to a - negative value, which must be greater - than or equal to -1, ANALYZE will assume that the number of - distinct nonnull values in the column is linear in the size of the - table; the exact count is to be computed by multiplying the estimated - table size by the absolute value of the given number. For example, - a value of -1 implies that all values in the column are distinct, while - a value of -0.5 implies that each value appears twice on the average. - This can be useful when the size of the table changes over time, since - the multiplication by the number of rows in the table is not performed - until query planning time. Specify a value of 0 to revert to estimating - the number of distinct values normally. For more information on the use - of statistics by the PostgreSQL query - planner, refer to . + operations. n_distinct affects the statistics for the + table itself, while n_distinct_inherited affects the + statistics gathered for the table plus its inheritance children, and for + the statistics gathered for partitioned tables. When the value + specified is a positive value, the query planner will assume that the + column contains exactly the specified number of distinct nonnull values. + Fractional values may also be specified by using values below 0 and + above or equal to -1. This instructs the query planner to estimate the + number of distinct values by multiplying the absolute value of the + specified number by the estimated number of rows in the table. For + example, a value of -1 implies that all values in the column are + distinct, while a value of -0.5 implies that each value appears twice on + average. This can be useful when the size of the table changes over + time. For more information on the use of statistics by the + PostgreSQL query planner, refer to + . Changing per-attribute options acquires a @@ -460,8 +487,8 @@ WITH ( MODULUS numeric_literal, REM This form adds a new constraint to a table using the same constraint syntax as CREATE TABLE, plus the option NOT - VALID, which is currently only allowed for foreign key, - CHECK constraints and not-null constraints. + VALID, which is currently only allowed for foreign-key, + CHECK, and not-null constraints. @@ -469,7 +496,7 @@ WITH ( MODULUS numeric_literal, REM existing rows in the table satisfy the new constraint. But if the NOT VALID option is used, this potentially-lengthy scan is skipped. The constraint will still be - enforced against subsequent inserts or updates (that is, they'll fail + applied against subsequent inserts or updates (that is, they'll fail unless there is a matching row in the referenced table, in the case of foreign keys, or they'll fail unless the new row matches the specified check condition). But the @@ -559,8 +586,8 @@ WITH ( MODULUS numeric_literal, REM This form alters the attributes of a constraint that was previously - created. Currently only foreign key constraints may be altered in - this fashion, but see below. + created. Currently FOREIGN KEY and CHECK + constraints may be altered in this fashion, but see below. @@ -591,7 +618,7 @@ WITH ( MODULUS numeric_literal, REM This form validates a foreign key, check, or not-null constraint that was previously created as NOT VALID, by scanning the table to ensure there are no rows for which the constraint is not - satisfied. If the constraint is not enforced, an error is thrown. + satisfied. If the constraint was set to NOT ENFORCED, an error is thrown. Nothing happens if the constraint is already marked valid. (See below for an explanation of the usefulness of this command.) @@ -852,7 +879,7 @@ WITH ( MODULUS numeric_literal, REM SHARE UPDATE EXCLUSIVE lock will be taken for - fillfactor, toast and autovacuum storage parameters, as well as the + fillfactor, TOAST and autovacuum storage parameters, as well as the planner parameter parallel_workers. @@ -1147,18 +1174,223 @@ WITH ( MODULUS numeric_literal, REM + + MERGE PARTITIONS (partition_name1, partition_name2 [, ...]) INTO partition_name + + + + This form merges several partitions of the target table into a new partition. + Hash-partitioned target table is not supported. + Only simple, non-partitioned partitions can be merged. + The new partition (partition_name) + can have the same name as one of the merged partitions + (partition_name1, + partition_name2 [, ...]). + + + + If the DEFAULT partition is not in the + list of merged partitions: + + + + For range-partitioned tables, the ranges of merged partitions + must be adjacent in order to be merged. + The partition bounds of merged partitions are combined to form the new partition bound for + partition_name. + + + + + For list-partitioned tables, the partition bounds of + merged partitions are combined to form the new partition bound for + partition_name. + + + + If the DEFAULT partition is in the list of merged partitions: + + + + The partition partition_name + will be the new DEFAULT partition of the target table. + + + + + The partition bound specifications for merged partitions can be arbitrary. + + + + + + All merged partitions must have the same owner. + The owner of merged partitions will be the owner of the new partition. + It is the user's responsibility to setup ACL on + the new partition. + + + + ALTER TABLE MERGE PARTITION uses the partitioned + table itself as the template to construct the new partition. + The new partition will inherit the same table access method, persistence + type, and tablespace as the partitioned table. + + Constraints, column defaults, column generation expressions, identity + columns, indexes, and triggers are copied from the partitioned table to + the new partition. But extended statistics, security policies, etc, + won't be copied from the partitioned table. + Indexes and identity columns copied from the partitioned table will be + created afterward, once the data has been moved into the new partition. + + + + When partitions are merged, any objects depending on this partition, + such as constraints, triggers, extended statistics, etc, will be + dropped. + Eventually, we will drop all the merged partitions + (using RESTRICT mode) too; therefore, if any objects + are still dependent on them, + ALTER TABLE MERGE PARTITION would fail. + (see ). + + + + Extension dependencies on partition indexes (created via + ALTER INDEX ... DEPENDS ON + EXTENSION) are preserved during merge operations. + All source partition indexes must have the same extension dependencies; + if they differ, an error is raised. This ensures that extension + dependencies are not silently lost during merge. + + + + + Merging partitions acquires an ACCESS EXCLUSIVE lock on + the parent table, in addition to the ACCESS EXCLUSIVE + locks on the tables being merged and on the default partition (if any). + + + + + ALTER TABLE MERGE PARTITIONS creates a new partition and + moves data from all merging partitions into it, which can take a long time. + So it is not recommended to use the command to merge very big partitions + with small ones. + + + + + + + + SPLIT PARTITION partition_name INTO ( + PARTITION partition_name1 { FOR VALUES partition_bound_spec | DEFAULT }, + PARTITION partition_name2 { FOR VALUES partition_bound_spec | DEFAULT } + [, ...]) + + + + + This form splits a single partition of the target table into new + partitions. Hash-partitioned target table is not supported. + Only a simple, non-partitioned partition can be split. + If the split partition is the DEFAULT partition, + one of the new partitions must be DEFAULT. + If the partitioned table does not have a DEFAULT + partition, a DEFAULT partition can be defined as one + of the new partitions. + + + + The bounds of new partitions should not overlap with those of new or + existing partitions (except partition_name). + The combined bounds of new partitions + partition_name1, + partition_name2[, ...] + should be equal to the bounds of the split partition + partition_name. + One of the new partitions can have the same name as the split partition + partition_name + (this is suitable in case of splitting the DEFAULT + partition: after the split, the DEFAULT partition + remains with the same name, but its partition bound changes). + + + + New partitions will have the same owner as the parent partition. + It is the user's responsibility to setup ACL on new + partitions. + + + + ALTER TABLE SPLIT PARTITION uses the partitioned + table itself as the template to construct new partitions. + New partitions will inherit the same table access method, persistence + type, and tablespace as the partitioned table. + + + + Constraints, column defaults, column generation expressions, + identity columns, indexes, and triggers are copied from the partitioned + table to the new partitions. But extended statistics, security + policies, etc, won't be copied from the partitioned table. + Indexes and identity columns copied from the partitioned table will be + created afterward, once the data has been moved into the new partitions. + + + + When a partition is split, any objects that depend on this partition, + such as constraints, triggers, extended statistics, etc, will be dropped. + This occurs because ALTER TABLE SPLIT PARTITION uses + the partitioned table itself as the template to reconstruct these + objects later. + Eventually, we will drop the split partition + (using RESTRICT mode) too; therefore, if any objects + are still dependent on it, ALTER TABLE SPLIT PARTITION + would fail (see ). + + + + Extension dependencies on partition indexes (created via + ALTER INDEX ... DEPENDS ON + EXTENSION) are preserved during split operations. + The new partitions' indexes will inherit the extension dependencies + from the source partition's indexes. + + + + + Split partition acquires an ACCESS EXCLUSIVE lock on + the parent table, in addition to the ACCESS EXCLUSIVE + lock on the table being split. + + + + + + ALTER TABLE SPLIT PARTITION creates new partitions and + moves data from the split partition into them, which can take a long + time. So it is not recommended to use the command for splitting a + small fraction of rows out of a very big partition. + + + + + All the forms of ALTER TABLE that act on a single table, except RENAME, SET SCHEMA, - ATTACH PARTITION, and - DETACH PARTITION can be combined into + ATTACH PARTITION, DETACH PARTITION, + MERGE PARTITIONS, and SPLIT PARTITION + can be combined into a list of multiple alterations to be applied together. For example, it is possible to add several columns and/or alter the type of several columns in a single command. This is particularly useful with large - tables, since only one pass over the table need be made. + tables, since only one pass over the table needs to be made. @@ -1397,7 +1629,19 @@ WITH ( MODULUS numeric_literal, REM partition_name - The name of the table to attach as a new partition or to detach from this table. + The name of the table to attach as a new partition or to detach from this table, + or the name of split partition, or the name of the new merged partition. + + + + + + partition_name1 + partition_name2 + + + The names of the tables being merged into the new partition or split into + new partitions. @@ -1466,11 +1710,11 @@ WITH ( MODULUS numeric_literal, REM - Adding an enforced CHECK or NOT NULL + Adding a CHECK or NOT NULL constraint requires scanning the table to verify that existing rows meet the constraint, but does not require a table rewrite. If a CHECK - constraint is added as NOT ENFORCED, the validation will - not be performed. + constraint is added as NOT ENFORCED, no verification will + be performed. @@ -1485,7 +1729,7 @@ WITH ( MODULUS numeric_literal, REM - Scanning a large table to verify a new foreign key or check constraint + Scanning a large table to verify new foreign-key, check, or not-null constraints can take a long time, and other updates to the table are locked out until the ALTER TABLE ADD CONSTRAINT command is committed. The main purpose of the NOT VALID @@ -1632,7 +1876,7 @@ ALTER TABLE measurements ALTER TABLE transactions ADD COLUMN status varchar(30) DEFAULT 'old', - ALTER COLUMN status SET default 'current'; + ALTER COLUMN status SET DEFAULT 'current'; Existing rows will be filled with old, but then the default for subsequent commands will be current. @@ -1830,6 +2074,31 @@ ALTER TABLE measurement DETACH PARTITION measurement_y2015m12; + + To split a single partition of the range-partitioned table: + +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2023 INTO + (PARTITION sales_feb2023 FOR VALUES FROM ('2023-02-01') TO ('2023-03-01'), + PARTITION sales_mar2023 FOR VALUES FROM ('2023-03-01') TO ('2023-04-01'), + PARTITION sales_apr2023 FOR VALUES FROM ('2023-04-01') TO ('2023-05-01')); + + + + To split a single partition of the list-partitioned table: + +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); + + + + To merge several partitions into one partition of the target table: + +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central) + INTO sales_all; + + diff --git a/doc/src/sgml/ref/checkpoint.sgml b/doc/src/sgml/ref/checkpoint.sgml index db011a47d0458..cd981cf2cab9f 100644 --- a/doc/src/sgml/ref/checkpoint.sgml +++ b/doc/src/sgml/ref/checkpoint.sgml @@ -21,7 +21,12 @@ PostgreSQL documentation -CHECKPOINT +CHECKPOINT [ ( option [, ...] ) ] + +where option can be one of: + + FLUSH_UNLOGGED [ boolean ] + MODE { FAST | SPREAD } @@ -37,14 +42,24 @@ CHECKPOINT - The CHECKPOINT command forces an immediate + By default, the CHECKPOINT command forces a fast checkpoint when the command is issued, without waiting for a regular checkpoint scheduled by the system (controlled by the settings in ). + To request the checkpoint be spread over a longer interval, set the + MODE option to SPREAD. CHECKPOINT is not intended for use during normal operation. + + The server may consolidate concurrently requested checkpoints. Such + consolidated requests will contain a combined set of options. For example, + if one session requests a fast checkpoint and another requests a spread + checkpoint, the server may combine those requests and perform one fast + checkpoint. + + If executed during recovery, the CHECKPOINT command will force a restartpoint (see ) @@ -58,6 +73,55 @@ CHECKPOINT + + Parameters + + + + FLUSH_UNLOGGED + + + Normally, CHECKPOINT does not flush dirty buffers of + unlogged relations. This option, which is disabled by default, enables + flushing unlogged relations to disk. + + + + + + MODE + + + When set to FAST, which is the default, the requested + checkpoint will be completed as fast as possible, which may result in a + significantly higher rate of I/O during the checkpoint. + + + MODE can also be set to SPREAD to + request the checkpoint be spread over a longer interval (controlled via + the settings in ), like a + regular checkpoint scheduled by the system. This can reduce the rate of + I/O during the checkpoint. + + + + + + boolean + + + Specifies whether the selected option should be turned on or off. + You can write TRUE, ON, or + 1 to enable the option, and FALSE, + OFF, or 0 to disable it. The + boolean value can also + be omitted, in which case TRUE is assumed. + + + + + + Compatibility diff --git a/doc/src/sgml/ref/cluster.sgml b/doc/src/sgml/ref/cluster.sgml index 8811f169ea0b1..ffb3ff898c69a 100644 --- a/doc/src/sgml/ref/cluster.sgml +++ b/doc/src/sgml/ref/cluster.sgml @@ -33,50 +33,9 @@ CLUSTER [ ( option [, ...] ) ] [ Description - CLUSTER instructs PostgreSQL - to cluster the table specified - by table_name - based on the index specified by - index_name. The index must - already have been defined on - table_name. - - - - When a table is clustered, it is physically reordered - based on the index information. Clustering is a one-time operation: - when the table is subsequently updated, the changes are - not clustered. That is, no attempt is made to store new or - updated rows according to their index order. (If one wishes, one can - periodically recluster by issuing the command again. Also, setting - the table's fillfactor storage parameter to less than - 100% can aid in preserving cluster ordering during updates, since updated - rows are kept on the same page if enough space is available there.) - - - - When a table is clustered, PostgreSQL - remembers which index it was clustered by. The form - CLUSTER table_name - reclusters the table using the same index as before. You can also - use the CLUSTER or SET WITHOUT CLUSTER - forms of ALTER TABLE to set the index to be used for - future cluster operations, or to clear any previous setting. - - - - CLUSTER without a - table_name reclusters all the - previously-clustered tables in the current database that the calling user - has privileges for. This form of CLUSTER cannot be - executed inside a transaction block. - - - - When a table is being clustered, an ACCESS - EXCLUSIVE lock is acquired on it. This prevents any other - database operations (both reads and writes) from operating on the - table until the CLUSTER is finished. + The CLUSTER command is equivalent to + with a USING INDEX + clause. See there for more details. @@ -136,63 +95,12 @@ CLUSTER [ ( option [, ...] ) ] [ - - In cases where you are accessing single rows randomly - within a table, the actual order of the data in the - table is unimportant. However, if you tend to access some - data more than others, and there is an index that groups - them together, you will benefit from using CLUSTER. - If you are requesting a range of indexed values from a table, or a - single indexed value that has multiple rows that match, - CLUSTER will help because once the index identifies the - table page for the first row that matches, all other rows - that match are probably already on the same table page, - and so you save disk accesses and speed up the query. - - - - CLUSTER can re-sort the table using either an index scan - on the specified index, or (if the index is a b-tree) a sequential - scan followed by sorting. It will attempt to choose the method that - will be faster, based on planner cost parameters and available statistical - information. - - While CLUSTER is running, the is temporarily changed to pg_catalog, pg_temp. - - When an index scan is used, a temporary copy of the table is created that - contains the table data in the index order. Temporary copies of each - index on the table are created as well. Therefore, you need free space on - disk at least equal to the sum of the table size and the index sizes. - - - - When a sequential scan and sort is used, a temporary sort file is - also created, so that the peak temporary space requirement is as much - as double the table size, plus the index sizes. This method is often - faster than the index scan method, but if the disk space requirement is - intolerable, you can disable this choice by temporarily setting to off. - - - - It is advisable to set to - a reasonably large value (but not more than the amount of RAM you can - dedicate to the CLUSTER operation) before clustering. - - - - Because the planner records statistics about the ordering of - tables, it is advisable to run ANALYZE - on the newly clustered table. - Otherwise, the planner might make poor choices of query plans. - - Because CLUSTER remembers which indexes are clustered, one can cluster the tables one wants clustered manually the first time, @@ -220,7 +128,7 @@ CLUSTER [ ( option [, ...] ) ] [ Examples - Cluster the table employees on the basis of + Cluster the table employees on the basis of its index employees_ind: CLUSTER employees USING employees_ind; @@ -228,7 +136,7 @@ CLUSTER employees USING employees_ind; - Cluster the employees table using the same + Cluster the employees table using the same index that was used before: CLUSTER employees; @@ -270,6 +178,7 @@ CLUSTER index_name ON See Also + diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index 5b43c56b13359..6d8479d6829ab 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -47,6 +47,7 @@ COMMENT ON POLICY policy_name ON table_name | [ PROCEDURAL ] LANGUAGE object_name | PROCEDURE procedure_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | + PROPERTY GRAPH object_name PUBLICATION object_name | ROLE object_name | ROUTINE routine_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | @@ -80,14 +81,16 @@ COMMENT ON Description - COMMENT stores a comment about a database object. + COMMENT stores, replaces, or removes the comment on a + database object. - Only one comment string is stored for each object, so to modify a comment, - issue a new COMMENT command for the same object. To remove a - comment, write NULL in place of the text string. - Comments are automatically dropped when their object is dropped. + Only one comment string is stored for each object. Issuing a new + COMMENT command for the same object replaces the + existing comment. Specifying NULL or an empty + string ('') removes the comment. Comments are + automatically dropped when their object is dropped. @@ -266,7 +269,8 @@ COMMENT ON string_literal - The new comment contents, written as a string literal. + The new comment contents, written as a string literal. An empty string + ('') removes the comment. @@ -275,7 +279,7 @@ COMMENT ON NULL - Write NULL to drop the comment. + Write NULL to remove the comment. @@ -301,7 +305,7 @@ COMMENT ON Examples - Attach a comment to the table mytable: + Attach a comment to the table mytable: COMMENT ON TABLE mytable IS 'This is my table.'; @@ -362,6 +366,7 @@ COMMENT ON TRANSFORM FOR hstore LANGUAGE plpython3u IS 'Transform between hstore COMMENT ON TRIGGER my_trigger ON my_table IS 'Used for RI'; COMMENT ON TYPE complex IS 'Complex number data type'; COMMENT ON VIEW my_view IS 'View of departmental costs'; +COMMENT ON VIEW my_view IS NULL; diff --git a/doc/src/sgml/ref/commit.sgml b/doc/src/sgml/ref/commit.sgml index 7e2dcac5a33a2..3234033de3fe7 100644 --- a/doc/src/sgml/ref/commit.sgml +++ b/doc/src/sgml/ref/commit.sgml @@ -33,6 +33,22 @@ COMMIT [ WORK | TRANSACTION ] [ AND [ NO ] CHAIN ] changes made by the transaction become visible to others and are guaranteed to be durable if a crash occurs. + + + If the transaction is in an aborted state then no changes will be made + and the effect of the COMMIT will be identical to that + of ROLLBACK, including the command tag output. + + + + In either case, if the AND CHAIN parameter is + specified then a new, identically configured, transaction is started. + + + + For more information regarding transactions see + . + @@ -67,6 +83,25 @@ COMMIT [ WORK | TRANSACTION ] [ AND [ NO ] CHAIN ] + + Outputs + + + On successful completion of a non-aborted transaction, + a COMMIT command returns a command tag of the form + +COMMIT + + + + However, in an aborted transaction, a COMMIT + command returns a command tag of the form + +ROLLBACK + + + + Notes @@ -96,8 +131,13 @@ COMMIT; Compatibility - The command COMMIT conforms to the SQL standard. The - form COMMIT TRANSACTION is a PostgreSQL extension. + The command COMMIT conforms to the SQL standard, except + that no exception condition is raised in the case where the transaction + was already aborted. + + + + The form COMMIT TRANSACTION is a PostgreSQL extension. @@ -107,6 +147,7 @@ COMMIT; + diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml index 8433344e5b6f5..4706c9a44100c 100644 --- a/doc/src/sgml/ref/copy.sgml +++ b/doc/src/sgml/ref/copy.sgml @@ -37,9 +37,10 @@ COPY { table_name [ ( delimiter_character' NULL 'null_string' DEFAULT 'default_string' - HEADER [ boolean | MATCH ] + HEADER [ boolean | integer | MATCH ] QUOTE 'quote_character' ESCAPE 'escape_character' + FORCE_ARRAY [ boolean ] FORCE_QUOTE { ( column_name [, ...] ) | * } FORCE_NOT_NULL { ( column_name [, ...] ) | * } FORCE_NULL { ( column_name [, ...] ) | * } @@ -50,7 +51,7 @@ COPY { table_name [ ( - + Description @@ -101,11 +102,11 @@ COPY { table_name [ ( - + Parameters - + table_name @@ -114,7 +115,7 @@ COPY { table_name [ ( - + column_name @@ -125,7 +126,7 @@ COPY { table_name [ ( - + query @@ -149,7 +150,7 @@ COPY { table_name [ ( - + filename @@ -161,7 +162,7 @@ COPY { table_name [ ( - + PROGRAM @@ -180,7 +181,7 @@ COPY { table_name [ ( - + STDIN @@ -189,7 +190,7 @@ COPY { table_name [ ( - + STDOUT @@ -198,7 +199,7 @@ COPY { table_name [ ( - + boolean @@ -212,21 +213,51 @@ COPY { table_name [ ( - + + integer + + + Specifies a non-negative integer value passed to the selected option. + + + + + FORMAT Selects the data format to be read or written: text, csv (Comma Separated Values), + json (JavaScript Object Notation), or binary. The default is text. See below for details. + + The json option is allowed only in + COPY TO. + + + + In JSON format, SQL NULL values are output as + JSON null. However, a JSON or JSONB column + whose value is the JSON literal null is also + output as null, making the two cases + indistinguishable in the COPY output. + For example: + +COPY (SELECT j FROM (VALUES ('null'::json), (NULL::json)) v(j)) + TO stdout (FORMAT JSON); +{"j":null} +{"j":null} + + + - + FREEZE @@ -249,7 +280,7 @@ COPY { table_name [ ( - + DELIMITER @@ -257,12 +288,12 @@ COPY { table_name [ ( CSV format. This must be a single one-byte character. - This option is not allowed when using binary format. + This option is not allowed when using binary or json format. - + NULL @@ -271,7 +302,7 @@ COPY { table_name [ ( CSV format. You might prefer an empty string even in text format for cases where you don't want to distinguish nulls from empty strings. - This option is not allowed when using binary format. + This option is not allowed when using binary or json format. @@ -286,7 +317,7 @@ COPY { table_name [ ( - + DEFAULT @@ -294,30 +325,39 @@ COPY { table_name [ ( COPY FROM, and only when - not using binary format. + not using binary or json format. - + HEADER - Specifies that the file contains a header line with the names of each - column in the file. On output, the first line contains the column - names from the table. On input, the first line is discarded when this - option is set to true (or equivalent Boolean value). - If this option is set to MATCH, the number and names - of the columns in the header line must match the actual column names of - the table, in order; otherwise an error is raised. - This option is not allowed when using binary format. - The MATCH option is only valid for COPY - FROM commands. + On output, if this option is set to true + (or an equivalent Boolean value), the first line of the output will + contain the column names from the table. + Integer values 0 and 1 are + accepted as Boolean values, but other integers are not allowed for + COPY TO commands. + + + On input, if this option is set to true + (or an equivalent Boolean value), the first line of the input is + discarded. If set to a non-negative integer, that number of + lines are discarded. If set to MATCH, the first line + is discarded, and it must contain column names that exactly match the + table's columns, in both number and order; otherwise, an error is raised. + The MATCH value is only valid for + COPY FROM commands. + + + This option is not allowed when using binary or json format. - + QUOTE @@ -329,7 +369,7 @@ COPY { table_name [ ( - + ESCAPE @@ -343,7 +383,20 @@ COPY { table_name [ ( - + + FORCE_ARRAY + + + Force output of square brackets as array decorations at the beginning + and end of output, and commas between the rows. It is allowed only in + COPY TO, and only when using + json format. The default is + false. + + + + + FORCE_QUOTE @@ -357,7 +410,7 @@ COPY { table_name [ ( - + FORCE_NOT_NULL @@ -372,7 +425,7 @@ COPY { table_name [ ( - + FORCE_NULL @@ -387,7 +440,7 @@ COPY { table_name [ ( - + ON_ERROR @@ -395,27 +448,38 @@ COPY { table_name [ ( error_action value of stop means fail the command, while - ignore means discard the input row and continue with the next one. + ignore means discard the input row and continue with the next one, + and set_null means replace the field containing the invalid + input value with a null value and continue to the next field. The default is stop. - The ignore option is applicable only for COPY FROM + The ignore and set_null + options are applicable only for COPY FROM when the FORMAT is text or csv. - A NOTICE message containing the ignored row count is - emitted at the end of the COPY FROM if at least one - row was discarded. When LOG_VERBOSITY option is set to - verbose, a NOTICE message + If ON_ERROR is set to ignore or + set_null, a NOTICE message is emitted at the end of the + COPY FROM command containing the count of rows that were ignored or + changed, if at least one row was affected. + + + When LOG_VERBOSITY option is set to verbose, + for ignore option, a NOTICE message containing the line of the input file and the column name whose input - conversion has failed is emitted for each discarded row. + conversion has failed is emitted for each discarded row; + for set_null option, a NOTICE + message containing the line of the input file and the column name where + value was replaced with NULL for each input conversion + failure. When it is set to silent, no message is emitted - regarding ignored rows. + regarding input conversion failed rows. - + REJECT_LIMIT @@ -433,7 +497,7 @@ COPY { table_name [ ( - + ENCODING @@ -445,7 +509,7 @@ COPY { table_name [ ( - + LOG_VERBOSITY @@ -458,12 +522,13 @@ COPY { table_name [ ( This is currently used in COPY FROM command when - ON_ERROR option is set to ignore. + ON_ERROR option is set to ignore + or set_null. - + WHERE @@ -492,7 +557,7 @@ WHERE condition - + Outputs @@ -516,18 +581,19 @@ COPY count - + Notes COPY TO can be used with plain - tables and populated materialized views. - For example, - COPY table - TO copies the same rows as + tables, populated materialized views, and partitioned tables. + For non-partitioned tables, COPY table + copies the same rows as SELECT * FROM ONLY table. + For partitioned tables, it copies the same rows as + SELECT * FROM table. However it doesn't directly support other relation types, - such as partitioned tables, inheritance child tables, or views. + such as inheritance child tables, or views. To copy all rows from such relations, use COPY (SELECT * FROM table) TO. @@ -1056,7 +1122,7 @@ versions of PostgreSQL. - + Examples @@ -1067,6 +1133,22 @@ COPY country TO STDOUT (DELIMITER '|'); + + When the FORCE_ARRAY option is enabled, + the entire output is wrapped in a single JSON array with rows separated by commas: + +COPY (SELECT * FROM (VALUES(1),(2)) val(id)) TO STDOUT (FORMAT JSON, FORCE_ARRAY); + +The output is as follows: + +[ + {"id":1} +,{"id":2} +] + + + + To copy data from a file into the country table: @@ -1122,7 +1204,7 @@ ZW ZIMBABWE - + Compatibility diff --git a/doc/src/sgml/ref/create_aggregate.sgml b/doc/src/sgml/ref/create_aggregate.sgml index 222e0aa5c9d08..0472ac2e87459 100644 --- a/doc/src/sgml/ref/create_aggregate.sgml +++ b/doc/src/sgml/ref/create_aggregate.sgml @@ -384,9 +384,13 @@ SELECT col FROM tab ORDER BY col USING sortop LIMIT 1; The approximate average size (in bytes) of the aggregate's state value. If this parameter is omitted or is zero, a default estimate is used - based on the state_data_type. + based on the state_data_type. If set to a + negative value, it indicates the state data can grow unboundedly in + size, such as when the aggregate accumulates input rows (e.g., + array_agg, string_agg). The planner uses this value to estimate the memory required for a - grouped aggregate query. + grouped aggregate query and to avoid optimizations that may cause + excessive memory usage. @@ -568,7 +572,8 @@ SELECT col FROM tab ORDER BY col USING sortop LIMIT 1; The approximate average size (in bytes) of the aggregate's state value, when using moving-aggregate mode. This works the same as - state_data_size. + state_data_size, except that negative + values are not used to indicate unbounded state size. diff --git a/doc/src/sgml/ref/create_database.sgml b/doc/src/sgml/ref/create_database.sgml index 640c0425faec5..3544b15efdafa 100644 --- a/doc/src/sgml/ref/create_database.sgml +++ b/doc/src/sgml/ref/create_database.sgml @@ -140,7 +140,7 @@ CREATE DATABASE name after the creation of the new database. In some situations, this may have a noticeable negative impact on overall system performance. The FILE_COPY strategy is affected by the setting. + linkend="guc-file-copy-method"/> setting. @@ -150,12 +150,12 @@ CREATE DATABASE name Sets the default collation order and character classification in the new database. Collation affects the sort order applied to strings, - e.g., in queries with ORDER BY, as well as the order used in indexes - on text columns. Character classification affects the categorization - of characters, e.g., lower, upper, and digit. Also sets the - associated aspects of the operating system environment, - LC_COLLATE and LC_CTYPE. The - default is the same setting as the template database. See ORDER BY, as well as the + order used in indexes on text columns. Character classification + affects the categorization of characters, e.g., lower, upper, and + digit. Also sets the LC_CTYPE aspect of the + operating system environment. The default is the same setting as the + template database. See and for details. @@ -189,17 +189,16 @@ CREATE DATABASE name lc_collate - Sets LC_COLLATE in the database server's operating - system environment. The default is the setting of if specified, otherwise the same - setting as the template database. See below for additional - restrictions. + If is + libc, sets the default collation order to use in + the new database, overriding the setting . Otherwise, this setting is + ignored. - If is - libc, also sets the default collation order to use - in the new database, overriding the setting . + The default is the setting of + if specified, otherwise the same setting as the template database. + See below for additional restrictions. @@ -208,16 +207,18 @@ CREATE DATABASE name Sets LC_CTYPE in the database server's operating - system environment. The default is the setting of if specified, otherwise the same - setting as the template database. See below for additional - restrictions. + system environment. If is - libc, also sets the default character - classification to use in the new database, overriding the setting - . + libc, sets the default character classification to + use in the new database, overriding the setting . + + + The default is the setting of + if specified, otherwise the same setting as the template database. + See below for additional restrictions. diff --git a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml index 0fcba18a3471f..7b83f500b255c 100644 --- a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation CREATE FOREIGN DATA WRAPPER name [ HANDLER handler_function | NO HANDLER ] [ VALIDATOR validator_function | NO VALIDATOR ] + [ CONNECTION connection_function | NO CONNECTION ] [ OPTIONS ( option 'value' [, ... ] ) ] @@ -99,6 +100,25 @@ CREATE FOREIGN DATA WRAPPER name + + CONNECTION connection_function + + + connection_function is the + name of a previously registered function that will be called to generate + the postgres connection string when a foreign server is used as part of + . If no connection function or + NO CONNECTION is specified, then servers using this + foreign data wrapper cannot be used for CREATE + SUBSCRIPTION. The connection function must take three + arguments: one of type oid for the user, one of type + oid for the server, and an unused third argument of type + internal (which prevents calling the function in other + contexts). + + + + OPTIONS ( option 'value' [, ... ] ) diff --git a/doc/src/sgml/ref/create_foreign_table.sgml b/doc/src/sgml/ref/create_foreign_table.sgml index d08834ac9d291..083f16772b75b 100644 --- a/doc/src/sgml/ref/create_foreign_table.sgml +++ b/doc/src/sgml/ref/create_foreign_table.sgml @@ -109,12 +109,12 @@ WITH ( MODULUS numeric_literal, REM - + Parameters - + IF NOT EXISTS @@ -126,7 +126,7 @@ WITH ( MODULUS numeric_literal, REM - + table_name @@ -135,7 +135,7 @@ WITH ( MODULUS numeric_literal, REM - + column_name @@ -144,7 +144,7 @@ WITH ( MODULUS numeric_literal, REM - + data_type @@ -156,7 +156,7 @@ WITH ( MODULUS numeric_literal, REM - + COLLATE collation @@ -167,7 +167,7 @@ WITH ( MODULUS numeric_literal, REM - + INHERITS ( parent_table [, ... ] ) @@ -180,7 +180,7 @@ WITH ( MODULUS numeric_literal, REM - + PARTITION OF parent_table { FOR VALUES partition_bound_spec | DEFAULT } @@ -196,7 +196,7 @@ WITH ( MODULUS numeric_literal, REM - + LIKE source_table [ like_option ... ] @@ -228,19 +228,19 @@ WITH ( MODULUS numeric_literal, REM available options are: - + INCLUDING COMMENTS - Comments for the copied columns, constraints, and indexes will be - copied. The default behavior is to exclude comments, resulting in - the copied columns and constraints in the new table having no + Comments for the copied columns, constraints, and extended statistics + will be copied. The default behavior is to exclude comments, + resulting in the corresponding objects in the new table having no comments. - + INCLUDING CONSTRAINTS @@ -251,7 +251,7 @@ WITH ( MODULUS numeric_literal, REM - + INCLUDING DEFAULTS @@ -265,7 +265,7 @@ WITH ( MODULUS numeric_literal, REM - + INCLUDING GENERATED @@ -275,7 +275,7 @@ WITH ( MODULUS numeric_literal, REM - + INCLUDING STATISTICS @@ -284,7 +284,7 @@ WITH ( MODULUS numeric_literal, REM - + INCLUDING ALL @@ -301,7 +301,7 @@ WITH ( MODULUS numeric_literal, REM - + CONSTRAINT constraint_name @@ -315,7 +315,7 @@ WITH ( MODULUS numeric_literal, REM - + NOT NULL [ NO INHERIT ] @@ -329,7 +329,7 @@ WITH ( MODULUS numeric_literal, REM - + NULL @@ -344,7 +344,7 @@ WITH ( MODULUS numeric_literal, REM - + CHECK ( expression ) [ NO INHERIT ] @@ -360,7 +360,7 @@ WITH ( MODULUS numeric_literal, REM Currently, CHECK expressions cannot contain subqueries nor refer to variables other than columns of the - current row. The system column tableoid + current row. The system column tableoid may be referenced, but not any other system column. @@ -371,7 +371,7 @@ WITH ( MODULUS numeric_literal, REM - + DEFAULT default_expr @@ -392,7 +392,7 @@ WITH ( MODULUS numeric_literal, REM - + GENERATED ALWAYS AS ( generation_expr ) [ STORED | VIRTUAL ]generated column @@ -419,7 +419,7 @@ WITH ( MODULUS numeric_literal, REM - + server_name @@ -430,7 +430,7 @@ WITH ( MODULUS numeric_literal, REM - + OPTIONS ( option 'value' [, ...] ) @@ -448,7 +448,7 @@ WITH ( MODULUS numeric_literal, REM - + Notes diff --git a/doc/src/sgml/ref/create_function.sgml b/doc/src/sgml/ref/create_function.sgml index 0d240484cd3f0..30bd4602f8d9a 100644 --- a/doc/src/sgml/ref/create_function.sgml +++ b/doc/src/sgml/ref/create_function.sgml @@ -649,7 +649,7 @@ END parameters. Thus for example these declarations conflict: CREATE FUNCTION foo(int) ... -CREATE FUNCTION foo(int, out text) ... +CREATE FUNCTION foo(int, OUT text) ... @@ -709,7 +709,7 @@ CREATE FUNCTION foo(int, int default 42) ... Add two integers using an SQL function: CREATE FUNCTION add(integer, integer) RETURNS integer - AS 'select $1 + $2;' + AS 'SELECT $1 + $2;' LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT; @@ -740,7 +740,7 @@ $$ LANGUAGE plpgsql; Return a record containing multiple output parameters: -CREATE FUNCTION dup(in int, out f1 int, out f2 text) +CREATE FUNCTION dup(IN int, OUT f1 int, OUT f2 text) AS $$ SELECT $1, CAST($1 AS text) || ' is text' $$ LANGUAGE SQL; @@ -817,10 +817,10 @@ $$ LANGUAGE plpgsql SET search_path = admin, pg_temp; - This function's intention is to access a table admin.pwds. + This function's intention is to access a table admin.pwds. But without the SET clause, or with a SET clause mentioning only admin, the function could be subverted by - creating a temporary table named pwds. + creating a temporary table named pwds. diff --git a/doc/src/sgml/ref/create_group.sgml b/doc/src/sgml/ref/create_group.sgml index d124c98eb516f..119d5ff3eb47c 100644 --- a/doc/src/sgml/ref/create_group.sgml +++ b/doc/src/sgml/ref/create_group.sgml @@ -36,10 +36,8 @@ CREATE GROUP name [ [ WITH ] password' | PASSWORD NULL | VALID UNTIL 'timestamp' | IN ROLE role_name [, ...] - | IN GROUP role_name [, ...] | ROLE role_name [, ...] | ADMIN role_name [, ...] - | USER role_name [, ...] | SYSID uid diff --git a/doc/src/sgml/ref/create_index.sgml b/doc/src/sgml/ref/create_index.sgml index 147a8f7587c71..bb7505d171b6d 100644 --- a/doc/src/sgml/ref/create_index.sgml +++ b/doc/src/sgml/ref/create_index.sgml @@ -814,7 +814,7 @@ Indexes: leveraging multiple CPUs in order to process the table rows faster. This feature is known as parallel index build. For index methods that support building indexes - in parallel (currently, B-tree and BRIN), + in parallel (currently, B-tree, GIN, and BRIN), maintenance_work_mem specifies the maximum amount of memory that can be used by each index build operation as a whole, regardless of how many worker processes were started. @@ -898,17 +898,17 @@ Indexes: Examples - To create a unique B-tree index on the column title in - the table films: + To create a unique B-tree index on the column title in + the table films: CREATE UNIQUE INDEX title_idx ON films (title); - To create a unique B-tree index on the column title - with included columns director - and rating in the table films: + To create a unique B-tree index on the column title + with included columns director + and rating in the table films: CREATE UNIQUE INDEX title_idx ON films (title) INCLUDE (director, rating); @@ -960,8 +960,8 @@ CREATE INDEX gin_idx ON documents_table USING GIN (locations) WITH (fastupdate = - To create an index on the column code in the table - films and have the index reside in the tablespace + To create an index on the column code in the table + films and have the index reside in the tablespace indexspace: CREATE INDEX code_idx ON films (code) TABLESPACE indexspace; diff --git a/doc/src/sgml/ref/create_operator.sgml b/doc/src/sgml/ref/create_operator.sgml index 3553d36454185..d2ffb1b2a500f 100644 --- a/doc/src/sgml/ref/create_operator.sgml +++ b/doc/src/sgml/ref/create_operator.sgml @@ -23,7 +23,7 @@ PostgreSQL documentation CREATE OPERATOR name ( {FUNCTION|PROCEDURE} = function_name - [, LEFTARG = left_type ] [, RIGHTARG = right_type ] + [, LEFTARG = left_type ] , RIGHTARG = right_type [, COMMUTATOR = com_op ] [, NEGATOR = neg_op ] [, RESTRICT = res_proc ] [, JOIN = join_proc ] [, HASHES ] [, MERGES ] @@ -88,8 +88,8 @@ CREATE OPERATOR name ( For binary operators, both LEFTARG and - RIGHTARG must be defined. For prefix operators only - RIGHTARG should be defined. + RIGHTARG must be defined. For prefix operators, only + RIGHTARG must be defined. The function_name function must have been previously defined using CREATE FUNCTION and must be defined to accept the correct number diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml index e76c342d3da67..d8a036739c0eb 100644 --- a/doc/src/sgml/ref/create_policy.sgml +++ b/doc/src/sgml/ref/create_policy.sgml @@ -49,6 +49,8 @@ CREATE POLICY name ON WITH CHECK. When a USING expression returns true for a given row then that row is visible to the user, while if false or null is returned then the row is not visible. + Typically, no error occurs when a row is not visible, but see + for exceptions. When a WITH CHECK expression returns true for a row then that row is inserted or updated, while if false or null is returned then an error occurs. @@ -194,8 +196,9 @@ CREATE POLICY name ON SELECT), and will not be available for modification (in an UPDATE - or DELETE). Such rows are silently suppressed; no error - is reported. + or DELETE). Typically, such rows are silently + suppressed; no error is reported (but see + for exceptions). @@ -251,8 +254,10 @@ CREATE POLICY name ON INSERT or UPDATE command attempts to add rows to the table that do not pass the ALL - policy's WITH CHECK expression, the entire - command will be aborted. + policy's WITH CHECK expression (or its + USING expression, if it does not have a + WITH CHECK expression), the entire command will + be aborted. @@ -268,11 +273,52 @@ CREATE POLICY name ON SELECT policy will be returned during a SELECT query, and that queries that require SELECT permissions, such as - UPDATE, will also only see those records + UPDATE, DELETE, and + MERGE, will also only see those records that are allowed by the SELECT policy. A SELECT policy cannot have a WITH CHECK expression, as it only applies in cases where - records are being retrieved from the relation. + records are being retrieved from the relation, except as described + below. + + + If a data-modifying query has a RETURNING clause, + SELECT permissions are required on the relation, + and any newly inserted or updated rows from the relation must satisfy + the relation's SELECT policies in order to be + available to the RETURNING clause. If a newly + inserted or updated row does not satisfy the relation's + SELECT policies, an error will be thrown (inserted + or updated rows to be returned are never + silently ignored). + + + If an INSERT has an ON CONFLICT DO + SELECT/UPDATE clause, or an ON CONFLICT DO + NOTHING clause with an arbiter index or constraint + specification, then SELECT + permissions are required on the relation, and the rows proposed for + insertion are checked using the relation's SELECT + policies. If a row proposed for insertion does not satisfy the + relation's SELECT policies, an error is thrown + (the INSERT is never silently + avoided). In addition, if the UPDATE path is + taken, the row to be updated and the new updated row are checked + against the relation's SELECT policies, and an + error is thrown if they are not satisfied (an auxiliary + UPDATE is never silently + avoided). + + + A MERGE command requires SELECT + permissions on both the source and target relations, and so each + relation's SELECT policies are applied before they + are joined, and the MERGE actions will only see + those records that are allowed by those policies. In addition, if + an UPDATE action is executed, the target relation's + SELECT policies are applied to the updated row, as + for a standalone UPDATE, except that an error is + thrown if they are not satisfied. @@ -292,10 +338,11 @@ CREATE POLICY name ON - Note that INSERT with ON CONFLICT DO - UPDATE checks INSERT policies' - WITH CHECK expressions only for rows appended - to the relation by the INSERT path. + Note that an INSERT with an + ON CONFLICT clause will check the + INSERT policies' WITH CHECK + expressions for all rows proposed for insertion, regardless of + whether or not they end up being inserted. @@ -305,12 +352,13 @@ CREATE POLICY name ON Using UPDATE for a policy means that it will apply - to UPDATE, SELECT FOR UPDATE - and SELECT FOR SHARE commands, as well as - auxiliary ON CONFLICT DO UPDATE clauses of - INSERT commands. - MERGE commands containing UPDATE - actions are affected as well. Since UPDATE + to UPDATE and + SELECT FOR UPDATE/SHARE commands, as well as + auxiliary ON CONFLICT DO UPDATE and + ON CONFLICT DO SELECT FOR UPDATE/SHARE clauses of + INSERT commands, and MERGE + commands containing UPDATE actions. + Since an UPDATE command involves pulling an existing record and replacing it with a new modified record, UPDATE policies accept both a USING expression and @@ -356,7 +404,8 @@ CREATE POLICY name ON USING expressions, an error will be thrown (the UPDATE path will never be silently - avoided). + avoided). The same applies to an UPDATE action + of a MERGE command. @@ -366,12 +415,18 @@ CREATE POLICY name ON Using DELETE for a policy means that it will apply - to DELETE commands. Only rows that pass this - policy will be seen by a DELETE command. There can - be rows that are visible through a SELECT that are - not available for deletion, if they do not pass the - USING expression for - the DELETE policy. + to DELETE commands and MERGE + commands containing DELETE actions. For a + DELETE command, only rows that pass this policy + will be seen by the DELETE command. There can + be rows that are visible through a SELECT policy + that are not available for deletion, if they do not pass the + USING expression for the DELETE + policy. Note, however, that a DELETE action in a + MERGE command will see rows that are visible + through SELECT policies, and if the + DELETE policy does not pass for such a row, an + error will be thrown. @@ -400,6 +455,15 @@ CREATE POLICY name ON + + summarizes how the different + types of policy apply to specific commands. In the table, + check means that the policy expression is checked and an + error is thrown if it returns false or null, whereas filter + means that the row is silently ignored if the policy expression returns + false or null. + +
Policies Applied by Command Type @@ -424,8 +488,8 @@ CREATE POLICY name ON - SELECT - Existing row + SELECT / COPY ... TO + Filter existing row @@ -433,63 +497,137 @@ CREATE POLICY name ON SELECT FOR UPDATE/SHARE - Existing row + Filter existing row + + Filter existing row + + + + + INSERT + + Check new row  + + If read access is required to either the existing or new row (for + example, a WHERE or RETURNING + clause that refers to columns from the relation). + + + + Check new row - Existing row - INSERT / MERGE ... THEN INSERT + UPDATE + + Filter existing row  & + check new row  + + + Filter existing row + Check new row - New row + + + DELETE + + Filter existing row  + + Filter existing row - INSERT ... RETURNING + INSERT ... ON CONFLICT - New row + Check new row  + + If an arbiter index or constraint is specified. + + - If read access is required to the existing or new row (for example, - a WHERE or RETURNING clause - that refers to columns from the relation). + Row proposed for insertion is checked regardless of whether or not a + conflict occurs. - New row + + Check new row  + - UPDATE / MERGE ... THEN UPDATE + ON CONFLICT DO UPDATE - Existing & new rows + Check existing & new rows  + + New row of the auxiliary UPDATE command, which + might be different from the new row of the original + INSERT command. + + - Existing row - New row + Check existing row + + Check new row  + - DELETE + ON CONFLICT DO SELECT + Check existing row + + + + + + + ON CONFLICT DO SELECT FOR UPDATE/SHARE + Check existing row + + Check existing row + + + + + MERGE + Filter source & target rows + + + + + + + MERGE ... THEN INSERT - Existing row + Check new row  + Check new row - Existing row - ON CONFLICT DO UPDATE - Existing & new rows + MERGE ... THEN UPDATE + Check new row + + Check existing row + Check new row + + + + MERGE ... THEN DELETE + + - Existing row - New row + Check existing row diff --git a/doc/src/sgml/ref/create_property_graph.sgml b/doc/src/sgml/ref/create_property_graph.sgml new file mode 100644 index 0000000000000..92f870379fd3a --- /dev/null +++ b/doc/src/sgml/ref/create_property_graph.sgml @@ -0,0 +1,318 @@ + + + + + CREATE PROPERTY GRAPH + + + + CREATE PROPERTY GRAPH + 7 + SQL - Language Statements + + + + CREATE PROPERTY GRAPH + define an SQL-property graph + + + + +CREATE [ TEMP | TEMPORARY ] PROPERTY GRAPH name + [ {VERTEX|NODE} TABLES ( vertex_table_definition [, ...] ) ] + [ {EDGE|RELATIONSHIP} TABLES ( edge_table_definition [, ...] ) ] + +where vertex_table_definition is: + + vertex_table_name [ AS alias ] [ KEY ( column_name [, ...] ) ] [ element_table_label_and_properties ] + +and edge_table_definition is: + + edge_table_name [ AS alias ] [ KEY ( column_name [, ...] ) ] + SOURCE [ KEY ( column_name [, ...] ) REFERENCES ] source_table [ ( column_name [, ...] ) ] + DESTINATION [ KEY ( column_name [, ...] ) REFERENCES ] dest_table [ ( column_name [, ...] ) ] + [ element_table_label_and_properties ] + +and element_table_label_and_properties is either: + + NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES ( { expression [ AS property_name ] } [, ...] ) + +or: + + { { LABEL label_name | DEFAULT LABEL } [ NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES ( { expression [ AS property_name ] } [, ...] ) ] } [...] + + + + + Description + + + CREATE PROPERTY GRAPH defines a property graph. A + property graph consists of vertices and edges, together called elements, + each with associated labels and properties, and can be queried using the + GRAPH_TABLE clause of with + a special path matching syntax. The data in the graph is stored in regular + tables (or views, foreign tables, etc.). Each vertex or edge corresponds + to a table. The property graph definition links these tables together into + a graph structure that can be queried using graph query techniques. + + + + CREATE PROPERTY GRAPH does not physically materialize a + graph. It is thus similar to CREATE VIEW in that it + records a structure that is used only when the defined object is queried. + + + + If a schema name is given (for example, CREATE PROPERTY GRAPH + myschema.mygraph ...) then the property graph is created in the + specified schema. Otherwise it is created in the current schema. + Temporary property graphs exist in a special schema, so a schema name + cannot be given when creating a temporary property graph. Property graphs + share a namespace with tables and other relation types, so the name of the + property graph must be distinct from the name of any other relation (table, + sequence, index, view, materialized view, or foreign table) in the same + schema. + + + + + Parameters + + + + name + + + The name (optionally schema-qualified) of the new property graph. + + + + + + VERTEX/NODE + EDGE/RELATIONSHIP + + + These keywords are synonyms, respectively. + + + + + + vertex_table_name + + + The name of a table that will contain vertices in the new property + graph. + + + + + + edge_table_name + + + The name of a table that will contain edges in the new property graph. + + + + + + alias + + + A unique identifier for the vertex or edge table. This defaults to the + name of the table. Aliases must be unique in a property graph + definition (across all vertex table and edge table definitions). + (Therefore, if a table is used more than once as a vertex or edge table, + then an explicit alias must be specified for at least one of them to + distinguish them.) + + + + + + KEY ( column_name [, ...] ) + + + A set of columns that uniquely identifies a row in the vertex or edge + table. Defaults to the primary key. + + + + + + source_table + dest_table + + + The vertex tables that the edge table is linked to. These refer to the + aliases of the source and destination vertex tables respectively. + + + + + + KEY ( column_name [, ...] ) REFERENCES ... ( column_name [, ...] ) + + + Two sets of columns that connect the edge table and the source or + destination vertex table, like in a foreign-key relationship. If a + foreign-key constraint between the two tables exists, it is used by + default. + + + + + + element_table_label_and_properties + + + Defines the labels and properties for the element (vertex or edge) + table. Each element has at least one label. By default, the label is + the same as the element table alias. This can be specified explicitly + as DEFAULT LABEL. Alternatively, one or more freely + chosen label names can be specified. (Label names do not have to be + unique across a property graph. It can be useful to assign the same + label to different elements.) Each label has a list (possibly empty) of + properties. By default, all columns of a table are automatically + exposed as properties. This can be specified explicitly as + PROPERTIES ALL COLUMNS. Alternatively, a list of + expressions, which can refer to the columns of the underlying table, can + be specified as properties. If the expressions are not a plain column + reference, then an explicit property name must also be specified. + + + + + + + + Notes + + + The following consistency checks must be satisfied by a property graph definition: + + + + + In a property graph, labels with the same name applied to different + property graph elements must have the same number of properties and + those properties must have the same names. For example, the following + would be allowed: + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 LABEL foo PROPERTIES (x, y), + v2 LABEL foo PROPERTIES (x, y) + ) ... + + but this would not: + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 LABEL foo PROPERTIES (x, y), + v2 LABEL foo PROPERTIES (z) + ) ... + + + + + + In a property graph, all properties with the same name must have the + same data type, independent of which label they are on. For example, + this would be allowed: + +CREATE TABLE v1 (a int, b int); +CREATE TABLE v2 (a int, b int); + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 LABEL foo PROPERTIES (a, b), + v2 LABEL bar PROPERTIES (a, b) + ) ... + + but this would not: + +CREATE TABLE v1 (a int, b int); +CREATE TABLE v2 (a int, b varchar); + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 LABEL foo PROPERTIES (a, b), + v2 LABEL bar PROPERTIES (a, b) + ) ... + + + + + + For each property graph element, all properties with the same name must + have the same expression for each label. For example, this would be + allowed: + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 LABEL foo PROPERTIES (a * 2 AS x) LABEL bar PROPERTIES (a * 2 AS x) + ) ... + + but this would not: + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 LABEL foo PROPERTIES (a * 2 AS x) LABEL bar PROPERTIES (a * 10 AS x) + ) ... + + + + + + + + Property graphs are queried using the GRAPH_TABLE clause + of . + + + + Access to the base relations underlying the GRAPH_TABLE + clause is determined by the permissions of the user executing the query, + rather than the property graph owner. Thus, the user of a property graph must + have the relevant permissions on the property graph and base relations + underlying the GRAPH_TABLE clause. + + + + + Examples + + + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES (v1, v2, v3) + EDGE TABLES (e1 SOURCE v1 DESTINATION v2, + e2 SOURCE v1 DESTINATION v3); + + + + + Compatibility + + + CREATE PROPERTY GRAPH conforms to ISO/IEC 9075-16 + (SQL/PGQ). + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml index 802630f2df116..0ac576d3f106a 100644 --- a/doc/src/sgml/ref/create_publication.sgml +++ b/doc/src/sgml/ref/create_publication.sgml @@ -22,14 +22,30 @@ PostgreSQL documentation CREATE PUBLICATION name - [ FOR ALL TABLES - | FOR publication_object [, ... ] ] + [ FOR { publication_object [, ... ] | publication_all_object [, ... ] } ] [ WITH ( publication_parameter [= value] [, ... ] ) ] where publication_object is one of: - TABLE [ ONLY ] table_name [ * ] [ ( column_name [, ... ] ) ] [ WHERE ( expression ) ] [, ... ] + TABLE table_and_columns [, ... ] TABLES IN SCHEMA { schema_name | CURRENT_SCHEMA } [, ... ] + +and publication_all_object is one of: + + ALL TABLES [ EXCEPT ( except_table_object [, ... ] ) ] + ALL SEQUENCES + +and table_and_columns is: + + table_object [ ( column_name [, ... ] ) ] [ WHERE ( expression ) ] + +and except_table_object is: + + TABLE table_object [, ... ] + +and table_object is: + + [ ONLY ] table_name [ * ] @@ -120,16 +136,6 @@ CREATE PUBLICATION name - - FOR ALL TABLES - - - Marks the publication as one that replicates changes for all tables in - the database, including tables created in the future. - - - - FOR TABLES IN SCHEMA @@ -161,11 +167,69 @@ CREATE PUBLICATION name + + FOR ALL TABLES + + + Marks the publication as one that replicates changes for all tables in + the database, including tables created in the future. Tables listed in + EXCEPT clause are excluded from the publication. + + + + + + FOR ALL SEQUENCES + + + Marks the publication as one that synchronizes changes for all sequences + in the database, including sequences created in the future. + + + + Only persistent sequences are included in the publication. Temporary + sequences and unlogged sequences are excluded from the publication. + + + + + + EXCEPT + + + This clause specifies a list of tables to be excluded from the + publication. + + + For inherited tables, if ONLY is specified before the + table name, only that table is excluded from the publication. If + ONLY is not specified, the table and all its descendant + tables (if any) are excluded. Optionally, * can be + specified after the table name to explicitly indicate that descendant + tables are excluded. + + + For partitioned tables, only the root partitioned table may be specified + in EXCEPT. Doing so excludes the root table and + all of its partitions from replication. The optional + ONLY and * has no effect for + partitioned tables. + + + There can be a case where a subscription includes multiple publications. + In such a case, a table or partition that is included in one publication + but excluded (explicitly or implicitly) by the EXCEPT + clause of another is considered included for replication. + + + + WITH ( publication_parameter [= value] [, ... ] ) - This clause specifies optional parameters for a publication. The + This clause specifies optional parameters for a publication when + publishing tables. This clause is not applicable to sequences. The following parameters are supported: @@ -231,13 +295,15 @@ CREATE PUBLICATION name publish_via_partition_root (boolean) - This parameter determines whether changes in a partitioned table (or - on its partitions) contained in the publication will be published - using the identity and schema of the partitioned table rather than - that of the individual partitions that are actually changed; the - latter is the default. Enabling this allows the changes to be - replicated into a non-partitioned table or a partitioned table - consisting of a different set of partitions. + This parameter controls how changes to a partitioned table (or any of + its partitions) are published. When set to true, + changes are published using the identity and schema of the + root partitioned table. When set to false (the + default), changes are published using the identity and schema of the + individual partitions where the changes actually occurred. Enabling + this option allows the changes to be replicated into a + non-partitioned table or into a partitioned table whose partition + structure differs from that of the publisher. @@ -279,10 +345,10 @@ CREATE PUBLICATION name Notes - If FOR TABLE, FOR ALL TABLES or - FOR TABLES IN SCHEMA are not specified, then the - publication starts out with an empty set of tables. That is useful if - tables or schemas are to be added later. + If FOR TABLE, FOR TABLES IN SCHEMA, + FOR ALL TABLES or FOR ALL SEQUENCES + are not specified, then the publication starts out with an empty set of + tables. That is useful if tables or schemas are to be added later. @@ -298,8 +364,9 @@ CREATE PUBLICATION name To add a table to a publication, the invoking user must have ownership - rights on the table. The FOR ALL TABLES and - FOR TABLES IN SCHEMA clauses require the invoking + rights on the table. The FOR TABLES IN SCHEMA, + FOR ALL TABLES and + FOR ALL SEQUENCES clauses require the invoking user to be a superuser. @@ -369,6 +436,12 @@ CREATE PUBLICATION name for each row inserted, updated, or deleted. + + For an UPDATE/DELETE ... FOR PORTION OF command, the + publication will publish an UPDATE or DELETE, + followed by one INSERT for each temporal leftover row inserted. + + ATTACHing a table into a partition tree whose root is published using a publication with publish_via_partition_root @@ -449,6 +522,38 @@ CREATE PUBLICATION sales_publication FOR TABLES IN SCHEMA marketing, sales; CREATE PUBLICATION users_filtered FOR TABLE users (user_id, firstname); + + + Create a publication that publishes all sequences for synchronization: + +CREATE PUBLICATION all_sequences FOR ALL SEQUENCES; + + + + + Create a publication that publishes all changes in all tables, and + all sequences for synchronization: + +CREATE PUBLICATION all_tables_sequences FOR ALL TABLES, ALL SEQUENCES; + + + + + Create a publication that publishes all changes in all tables except + users and departments: + +CREATE PUBLICATION all_tables_except FOR ALL TABLES EXCEPT (TABLE users, departments); + + + + + Create a publication that publishes all sequences for synchronization, and + all changes in all tables except users and + departments: + +CREATE PUBLICATION all_sequences_tables_except FOR ALL SEQUENCES, ALL TABLES EXCEPT (TABLE users, departments); + + diff --git a/doc/src/sgml/ref/create_schema.sgml b/doc/src/sgml/ref/create_schema.sgml index ed69298ccc6ce..4ecf82d6bcb7b 100644 --- a/doc/src/sgml/ref/create_schema.sgml +++ b/doc/src/sgml/ref/create_schema.sgml @@ -100,12 +100,27 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION role_sp An SQL statement defining an object to be created within the - schema. Currently, only CREATE - TABLE, CREATE VIEW, CREATE - INDEX, CREATE SEQUENCE, CREATE - TRIGGER and GRANT are accepted as clauses + schema. Currently, only + CREATE AGGREGATE, + CREATE COLLATION, + CREATE DOMAIN, + CREATE FUNCTION, + CREATE INDEX, + CREATE OPERATOR, + CREATE PROCEDURE, + CREATE SEQUENCE, + CREATE TABLE, + CREATE TEXT SEARCH CONFIGURATION, + CREATE TEXT SEARCH DICTIONARY, + CREATE TEXT SEARCH PARSER, + CREATE TEXT SEARCH TEMPLATE, + CREATE TRIGGER, + CREATE TYPE, + CREATE VIEW, + and GRANT are accepted as clauses within CREATE SCHEMA. Other kinds of objects may - be created in separate commands after the schema is created. + be created within the schema in separate commands after the schema + is created. @@ -131,6 +146,14 @@ CREATE SCHEMA IF NOT EXISTS AUTHORIZATION role_sp CREATE privilege for the current database. (Of course, superusers bypass this check.) + + + The schema_element + subcommands, if any, are executed in the order they are written. + An exception is that foreign key constraint clauses in CREATE + TABLE subcommands are postponed and added at the end. + This allows circular foreign key references, which are sometimes useful. + @@ -193,12 +216,12 @@ CREATE VIEW hollywood.winners AS - The SQL standard specifies that the subcommands in CREATE - SCHEMA can appear in any order. The present - PostgreSQL implementation does not - handle all cases of forward references in subcommands; it might - sometimes be necessary to reorder the subcommands in order to avoid - forward references. + Some other SQL implementations attempt to allow more kinds of forward + references to objects defined in + later schema_element + subcommands than just foreign key constraints. This is difficult or + impossible to do correctly in general, and it is not clear that the SQL + standard requires any such behavior except for foreign keys. diff --git a/doc/src/sgml/ref/create_sequence.sgml b/doc/src/sgml/ref/create_sequence.sgml index 1e283f13d15c6..0ffcd0febd1b5 100644 --- a/doc/src/sgml/ref/create_sequence.sgml +++ b/doc/src/sgml/ref/create_sequence.sgml @@ -70,7 +70,7 @@ SELECT * FROM name; to examine the parameters and current state of a sequence. In particular, - the last_value field of the sequence shows the last value + the last_value field of the sequence shows the last value allocated by any session. (Of course, this value might be obsolete by the time it's printed, if other sessions are actively doing nextval calls.) @@ -295,7 +295,7 @@ SELECT * FROM name; used for a sequence object that will be used concurrently by multiple sessions. Each session will allocate and cache successive sequence values during one access to the sequence object and - increase the sequence object's last_value accordingly. + increase the sequence object's last_value accordingly. Then, the next cache-1 uses of nextval within that session simply return the preallocated values without touching the sequence object. So, any @@ -319,7 +319,7 @@ SELECT * FROM name; class="parameter">cache setting greater than one you should only assume that the nextval values are all distinct, not that they are generated purely sequentially. Also, - last_value will reflect the latest value reserved by + last_value will reflect the latest value reserved by any session, whether or not it has yet been returned by nextval. diff --git a/doc/src/sgml/ref/create_server.sgml b/doc/src/sgml/ref/create_server.sgml index 05f4019453ba2..ce4a064eabb4b 100644 --- a/doc/src/sgml/ref/create_server.sgml +++ b/doc/src/sgml/ref/create_server.sgml @@ -42,6 +42,13 @@ CREATE SERVER [ IF NOT EXISTS ] server_name + + If the foreign data wrapper fdw_name is + specified with a CONNECTION clause, then may use this foreign server for + connection information. + + The server name must be unique within the database. diff --git a/doc/src/sgml/ref/create_statistics.sgml b/doc/src/sgml/ref/create_statistics.sgml index d6b25ed2c9b9a..5cc1d51b4b3fb 100644 --- a/doc/src/sgml/ref/create_statistics.sgml +++ b/doc/src/sgml/ref/create_statistics.sgml @@ -22,7 +22,7 @@ PostgreSQL documentation CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_name ] - ON ( expression ) + ON { column_name | ( expression ) } FROM table_name CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_name ] @@ -45,7 +45,8 @@ CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_ The CREATE STATISTICS command has two basic forms. The - first form allows univariate statistics for a single expression to be + first form allows univariate statistics for a single expression + or virtual generated column to be collected, providing benefits similar to an expression index without the overhead of index maintenance. This form does not allow the statistics kind to be specified, since the various statistics kinds refer only to @@ -53,7 +54,7 @@ CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_ multivariate statistics on multiple columns and/or expressions to be collected, optionally specifying which statistics kinds to include. This form will also automatically cause univariate statistics to be collected on - any expressions included in the list. + any expressions and virtual generated columns included in the list. @@ -109,7 +110,8 @@ CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_ If this clause is omitted, all supported statistics kinds are included in the statistics object. Univariate expression statistics are built automatically if the statistics definition includes any complex - expressions rather than just simple column references. + expressions or references to virtual generated columns + rather than just simple column references. For more information, see and . @@ -121,9 +123,16 @@ CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_ The name of a table column to be covered by the computed statistics. - This is only allowed when building multivariate statistics. At least - two column names or expressions must be specified, and their order is - not significant. + This may be used to build univariate statistics on a single virtual + generated column, or as part of a list of multiple columns (virtual or + non-virtual) and/or expressions to build multivariate statistics. In + the latter case, separate univariate statistics are built automatically + for each expression and virtual generated column in the list. + + + Defining extended statistics on a single non-virtual + column is not supported or necessary, because statistics are built + automatically on such columns without defining extended statistics. @@ -136,7 +145,8 @@ CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_ used to build univariate statistics on a single expression, or as part of a list of multiple column names and/or expressions to build multivariate statistics. In the latter case, separate univariate - statistics are built automatically for each expression in the list. + statistics are built automatically for each expression and virtual + generated column in the list. @@ -169,7 +179,9 @@ CREATE STATISTICS [ [ IF NOT EXISTS ] statistics_ Expression statistics are per-expression and are similar to creating an index on the expression, except that they avoid the overhead of index maintenance. Expression statistics are built automatically for each - expression in the statistics object definition. + expression in the statistics object definition. Extended statistics on + a virtual generated column behave the same as expression statistics on the + column's generation expression. diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml index 57dec28a5df64..07d5b1bd77c53 100644 --- a/doc/src/sgml/ref/create_subscription.sgml +++ b/doc/src/sgml/ref/create_subscription.sgml @@ -22,7 +22,7 @@ PostgreSQL documentation CREATE SUBSCRIPTION subscription_name - CONNECTION 'conninfo' + { SERVER servername | CONNECTION 'conninfo' } PUBLICATION publication_name [, ...] [ WITH ( subscription_parameter [= value] [, ... ] ) ] @@ -77,6 +77,20 @@ CREATE SUBSCRIPTION subscription_name + + SERVER servername + + + A foreign server to use for the connection. The server's foreign data + wrapper must have a connection_function + registered, and a user mapping for the subscription owner on the server + must exist. Additionally, the subscription owner must have + USAGE privileges on + servername. + + + + CONNECTION 'conninfo' @@ -127,10 +141,10 @@ CREATE SUBSCRIPTION subscription_name Since no connection is made when this option is - false, no tables are subscribed. To initiate - replication, you must manually create the replication slot, enable - the failover if required, enable the subscription, and refresh the - subscription. See + false, no tables and sequences are subscribed. To + initiate replication, you must manually create the replication slot, + enable the failover if required, enable the subscription, and refresh + the subscription. See for examples. @@ -169,7 +183,9 @@ CREATE SUBSCRIPTION subscription_name Name of the publisher's replication slot to use. The default is - to use the name of the subscription for the slot name. + to use the name of the subscription for the slot name. The name cannot + be pg_conflict_detection as it is reserved for the + conflict detection. @@ -226,7 +242,7 @@ CREATE SUBSCRIPTION subscription_name for more about send/receive - functions). + functions). This parameter has no effect for sequences. @@ -263,6 +279,12 @@ CREATE SUBSCRIPTION subscription_namecopy_data = true can interact with the origin parameter. + + See + for recommendations on how to handle any warnings about sequence + definition differences between the publisher and the subscriber, + which might occur when copy_data = true. + @@ -278,6 +300,7 @@ CREATE SUBSCRIPTION subscription_name @@ -308,7 +331,8 @@ CREATE SUBSCRIPTION subscription_name setting within this subscription's apply worker processes. The default value - is off. + is off. This parameter has no effect for + sequences. @@ -338,7 +362,8 @@ CREATE SUBSCRIPTION subscription_name Specifies whether two-phase commit is enabled for this subscription. - The default is false. + The default is false. This parameter has no effect + for sequences. @@ -396,8 +421,8 @@ CREATE SUBSCRIPTION subscription_name If true, all replication actions are performed as the subscription owner. If false, replication workers will perform actions on each - table as the owner of that table. The latter configuration is - generally much more secure; for details, see + table or sequence as the owner of that relation. The latter + configuration is generally much more secure; for details, see . The default is false. @@ -415,6 +440,7 @@ CREATE SUBSCRIPTION subscription_nameorigin to any means that the publisher sends changes regardless of their origin. The default is any. + This parameter has no effect for sequences. See for details of how @@ -435,8 +461,148 @@ CREATE SUBSCRIPTION subscription_name - + + retain_dead_tuples (boolean) + + + Specifies whether the information (e.g., dead tuples, commit + timestamps, and origins) required for conflict detection on the + subscriber is retained. The default is false. + If set to true, the detection of + is enabled, and a physical + replication slot named pg_conflict_detection + is created on the subscriber to prevent the information for detecting + conflicts from being removed. This parameter has no effect for + sequences. + + + + Note that the information useful for conflict detection is retained + only after the creation of the slot. You can verify the existence of + this slot by querying pg_replication_slots. + And even if multiple subscriptions on one node enable this option, + only one replication slot will be created. Also, + wal_level must be set to replica + or higher to allow the replication slot to be used. + + + + + Note that the information for conflict detection cannot be purged if + the subscription is disabled; thus, the information will accumulate + until the subscription is enabled. To prevent excessive accumulation, + it is recommended to disable retain_dead_tuples + if the subscription will be inactive for an extended period. + + + + Additionally when enabling retain_dead_tuples for + conflict detection in logical replication, it is important to design the + replication topology to balance data retention requirements with + overall system performance. This option provides minimal performance + overhead when applied appropriately. The following scenarios illustrate + effective usage patterns when enabling this option. + + + + a. Large Tables with Bidirectional Writes: + For large tables subject to concurrent writes on both publisher and + subscriber nodes, publishers can define row filters when creating + publications to segment data. This allows multiple subscriptions + to replicate exclusive subsets of the table in parallel, optimizing + the throughput. + + + + b. Write-Enabled Subscribers: + If a subscriber node is expected to perform write operations, replication + can be structured using multiple publications and subscriptions. By + distributing tables across these publications, the workload is spread among + several apply workers, improving concurrency and reducing contention. + + + + c. Read-Only Subscribers: + In configurations involving single or multiple publisher nodes + performing concurrent write operations, read-only subscriber nodes may + replicate changes without seeing a performance impact if it does index + scan. However, if the subscriber is impacted due to replication lag or + scan performance (say due to sequential scans), it needs to follow one + of the two previous strategies to distribute the workload on the + subscriber. + + + + + This option cannot be enabled if the publisher is a physical standby. + + + + Enabling this option ensures retention of information useful for + conflict detection solely for changes occurring locally on the + publisher. For the changes originating from different origins, + reliable conflict detection cannot be guaranteed. + + + + + + max_retention_duration (integer) + + + Maximum duration in milliseconds for which this subscription's apply worker + is allowed to retain the information useful for conflict detection when + retain_dead_tuples is enabled. The default value + is 0, indicating that the information is retained + until it is no longer needed for detection purposes. + + + The information useful for conflict detection is no longer retained if + all apply workers associated with the subscriptions, where + retain_dead_tuples is enabled, confirm that the + retention duration has exceeded the + max_retention_duration set within the corresponding + subscription. The retention will automatically resume when at least one + apply worker confirms that the retention duration is within the + specified limit, or when a new subscription is created with + retain_dead_tuples = true. Alternatively, retention + can be manually resumed by re-enabling retain_dead_tuples. + + + Note that overall retention will not stop if other subscriptions that + have a value greater than 0 for this parameter have not exceeded it, + or if they set this option to 0. + + + This option is effective only when + retain_dead_tuples is enabled and the apply + worker associated with the subscription is active. + + + + Note that setting a non-zero value for this option could lead to + information for conflict detection being removed prematurely, + potentially resulting in incorrect conflict detection. + + + + + + + wal_receiver_timeout (text) + + + The value of this parameter overrides the + setting within this + subscription's apply worker processes. The default value is + -1, which means it does not override the global setting, + i.e., the value from the server configuration, command line, role or + database settings will be used instead. + + + + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 4a41b2f553007..e342585c7f08c 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -21,8 +21,8 @@ PostgreSQL documentation -CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [ - { column_name data_type [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } ] [ COMPRESSION compression_method ] [ COLLATE collation ] [ column_constraint [ ... ] ] +CREATE [ persistence_mode ] TABLE [ IF NOT EXISTS ] table_name ( [ + { column_name data_type [ column_storage ] [ column_compression ] [ COLLATE collation ] [ column_constraint [ ... ] ] | table_constraint | LIKE source_table [ like_option ... ] } [, ... ] @@ -34,7 +34,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] [ TABLESPACE tablespace_name ] -CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name +CREATE [ persistence_mode ] TABLE [ IF NOT EXISTS ] table_name OF type_name [ ( { column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ] | table_constraint } @@ -46,7 +46,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] [ TABLESPACE tablespace_name ] -CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name +CREATE [ persistence_mode ] TABLE [ IF NOT EXISTS ] table_name PARTITION OF parent_table [ ( { column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ] | table_constraint } @@ -58,7 +58,19 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] [ TABLESPACE tablespace_name ] -where column_constraint is: +where persistence_mode is: + +{ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } } | UNLOGGED + +and column_storage is: + +STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN | DEFAULT } + +and column_compression is: + +COMPRESSION compression_method + +and column_constraint is: [ CONSTRAINT constraint_name ] { NOT NULL [ NO INHERIT ] | @@ -97,17 +109,17 @@ FROM ( { partition_bound_expr | MIN TO ( { partition_bound_expr | MINVALUE | MAXVALUE } [, ...] ) | WITH ( MODULUS numeric_literal, REMAINDER numeric_literal ) -index_parameters in UNIQUE, PRIMARY KEY, and EXCLUDE constraints are: +and index_parameters in UNIQUE, PRIMARY KEY, and EXCLUDE constraints are: [ INCLUDE ( column_name [, ... ] ) ] [ WITH ( storage_parameter [= value] [, ... ] ) ] [ USING INDEX TABLESPACE tablespace_name ] -exclude_element in an EXCLUDE constraint is: +and exclude_element in an EXCLUDE constraint is: { column_name | ( expression ) } [ COLLATE collation ] [ opclass [ ( opclass_parameter = value [, ... ] ) ] ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] -referential_action in a FOREIGN KEY/REFERENCES constraint is: +and referential_action in a FOREIGN KEY/REFERENCES constraint is: { NO ACTION | RESTRICT | CASCADE | SET NULL [ ( column_name [, ... ] ) ] | SET DEFAULT [ ( column_name [, ... ] ) ] } @@ -123,6 +135,14 @@ WITH ( MODULUS numeric_literal, REM command. + + The durability characteristics of a table are governed by its persistence + mode. By default, the data will be persistent and crash-safe. + For less stringent requirements and better performance, a table can be + specified as temporary + or unlogged. + + If a schema name is given (for example, CREATE TABLE myschema.mytable ...) then the table is created in the specified @@ -312,7 +332,7 @@ WITH ( MODULUS numeric_literal, REM - This form sets the storage mode for the column. This controls whether this + This clause sets the storage mode for the column. This controls whether this column is held inline or in a secondary TOAST table, and whether the data should be compressed or not. PLAIN must be used for fixed-length values such as integer and is @@ -447,11 +467,6 @@ WITH ( MODULUS numeric_literal, REM the values in the new row, an error will be reported. - - Partitioned tables do not support EXCLUDE constraints; - however, you can define these constraints on individual partitions. - - See for more discussion on table partitioning. @@ -677,9 +692,10 @@ WITH ( MODULUS numeric_literal, REM INCLUDING COMMENTS - Comments for the copied columns, constraints, and indexes will be + Comments for the copied columns, check constraints, + not-null constraints, indexes, and extended statistics will be copied. The default behavior is to exclude comments, resulting in - the copied columns and constraints in the new table having no + the corresponding objects in the new table having no comments. @@ -867,7 +883,7 @@ WITH ( MODULUS numeric_literal, REM Currently, CHECK expressions cannot contain subqueries nor refer to variables other than columns of the current row (see ). - The system column tableoid + The system column tableoid may be referenced, but not any other system column. @@ -929,6 +945,15 @@ WITH ( MODULUS numeric_literal, REM not other generated columns. Any functions and operators used must be immutable. References to other tables are not allowed. + + + A virtual generated column cannot have a user-defined type, and the + generation expression of a virtual generated column must not reference + user-defined functions or types, that is, it can only use built-in + functions or types. This applies also indirectly, such as for functions + or types that underlie operators or casts. (This restriction does not + exist for stored generated columns.) + @@ -1162,6 +1187,18 @@ WITH ( MODULUS numeric_literal, REM exclusion constraint on a subset of the table; internally this creates a partial index. Note that parentheses are required around the predicate. + + + When establishing an exclusion constraint for a multi-level partition + hierarchy, all the columns in the partition key of the target + partitioned table, as well as those of all its descendant partitioned + tables, must be included in the constraint definition. Additionally, + those columns must be compared using the equality operator. These + restrictions ensure that potentially-conflicting rows will exist in the + same partition. The constraint may also refer to other columns which + are not a part of any partition key, which can be compared using any + appropriate operator. + @@ -1363,8 +1400,8 @@ WITH ( MODULUS numeric_literal, REM REFERENCES (foreign key) constraints accept this clause. NOT NULL and CHECK constraints are not deferrable. Note that deferrable constraints cannot be used as - conflict arbitrators in an INSERT statement that - includes an ON CONFLICT DO UPDATE clause. + conflict arbiters in an INSERT statement that + includes an ON CONFLICT clause. @@ -1687,7 +1724,8 @@ WITH ( MODULUS numeric_literal, REM vacuum_truncate, toast.vacuum_truncate (boolean) - vacuum_truncate storage parameter + vacuum_truncate + storage parameter @@ -1700,6 +1738,22 @@ WITH ( MODULUS numeric_literal, REM + + autovacuum_parallel_workers (integer) + + autovacuum_parallel_workers storage parameter + + + + + Per-table value for + parameter. If -1 is specified, autovacuum_max_parallel_workers + value will be used. If set to 0, parallel vacuum is disabled for + this table. The default value is -1. + + + + autovacuum_vacuum_threshold, toast.autovacuum_vacuum_threshold (integer) @@ -1949,6 +2003,21 @@ WITH ( MODULUS numeric_literal, REM + + log_autoanalyze_min_duration (integer) + + log_autoanalyze_min_duration + storage parameter + + + + + Per-table value for + parameter. + + + + vacuum_max_eager_freeze_failure_rate, toast.vacuum_max_eager_freeze_failure_rate (floating point) @@ -2225,7 +2294,7 @@ CREATE TABLE employees OF employee_type ( Create a range partitioned table: CREATE TABLE measurement ( - logdate date not null, + logdate date NOT NULL, peaktemp int, unitsales int ) PARTITION BY RANGE (logdate); @@ -2235,7 +2304,7 @@ CREATE TABLE measurement ( Create a range partitioned table with multiple columns in the partition key: CREATE TABLE measurement_year_month ( - logdate date not null, + logdate date NOT NULL, peaktemp int, unitsales int ) PARTITION BY RANGE (EXTRACT(YEAR FROM logdate), EXTRACT(MONTH FROM logdate)); @@ -2245,8 +2314,8 @@ CREATE TABLE measurement_year_month ( Create a list partitioned table: CREATE TABLE cities ( - city_id bigserial not null, - name text not null, + city_id bigserial NOT NULL, + name text NOT NULL, population bigint ) PARTITION BY LIST (left(lower(name), 1)); @@ -2255,8 +2324,8 @@ CREATE TABLE cities ( Create a hash partitioned table: CREATE TABLE orders ( - order_id bigint not null, - cust_id bigint not null, + order_id bigint NOT NULL, + cust_id bigint NOT NULL, status text ) PARTITION BY HASH (order_id); diff --git a/doc/src/sgml/ref/create_table_as.sgml b/doc/src/sgml/ref/create_table_as.sgml index 8429333e3af94..0492933ff380d 100644 --- a/doc/src/sgml/ref/create_table_as.sgml +++ b/doc/src/sgml/ref/create_table_as.sgml @@ -21,14 +21,18 @@ PostgreSQL documentation -CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name - [ (column_name [, ...] ) ] +CREATE [ persistence_mode ] TABLE [ IF NOT EXISTS ] table_name + [ ( column_name [, ...] ) ] [ USING method ] [ WITH ( storage_parameter [= value] [, ... ] ) | WITHOUT OIDS ] [ ON COMMIT { PRESERVE ROWS | DELETE ROWS | DROP } ] [ TABLESPACE tablespace_name ] AS query [ WITH [ NO ] DATA ] + +where persistence_mode is: + +{ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } } | UNLOGGED @@ -266,8 +270,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI Examples - Create a new table films_recent consisting of only - recent entries from the table films: + Create a new table films_recent consisting of only + recent entries from the table films: CREATE TABLE films_recent AS @@ -286,8 +290,8 @@ CREATE TABLE films2 AS - Create a new temporary table films_recent, consisting of - only recent entries from the table films, using a + Create a new temporary table films_recent, consisting of + only recent entries from the table films, using a prepared statement. The new table will be dropped at commit: diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml index 982ab6f3ee450..bb1426f4970d5 100644 --- a/doc/src/sgml/ref/create_trigger.sgml +++ b/doc/src/sgml/ref/create_trigger.sgml @@ -29,7 +29,7 @@ PostgreSQL documentation CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } ON table_name [ FROM referenced_table_name ] - [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ] + [ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ] [ ENFORCED ] [ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ] [ FOR [ EACH ] { ROW | STATEMENT } ] [ WHEN ( condition ) ] @@ -197,9 +197,11 @@ CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name of the rows inserted, deleted, or modified by the current SQL statement. This feature lets the trigger see a global view of what the statement did, not just one row at a time. This option is only allowed for - an AFTER trigger that is not a constraint trigger; also, if - the trigger is an UPDATE trigger, it must not specify - a column_name list. + an AFTER trigger on a plain table (not a foreign table). + The trigger should not be a constraint trigger. Also, if the trigger is + an UPDATE trigger, it must not specify + a column_name list when using + this option. OLD TABLE may only be specified once, and only for a trigger that can fire on UPDATE or DELETE; it creates a transition relation containing the before-images of all rows @@ -321,6 +323,15 @@ UPDATE OF column_name1 [, column_name2 + + ENFORCED + + + This is a noise word. Constraint triggers are always enforced. + + + + REFERENCING @@ -474,7 +485,7 @@ UPDATE OF column_name1 [, column_name2BEFORE UPDATE triggers are not considered. Conversely, a command such as UPDATE ... SET x = x ... - will fire a trigger on column x, even though the column's + will fire a trigger on column x, even though the column's value did not change. @@ -587,7 +598,7 @@ UPDATE OF column_name1 [, column_name2 Execute the function check_account_update whenever - a row of the table accounts is about to be updated: + a row of the table accounts is about to be updated: CREATE TRIGGER check_update @@ -597,7 +608,7 @@ CREATE TRIGGER check_update Modify that trigger definition to only execute the function if - column balance is specified as a target in + column balance is specified as a target in the UPDATE command: @@ -607,7 +618,7 @@ CREATE OR REPLACE TRIGGER check_update EXECUTE FUNCTION check_account_update(); - This form only executes the function if column balance + This form only executes the function if column balance has in fact changed value: @@ -618,7 +629,7 @@ CREATE TRIGGER check_update EXECUTE FUNCTION check_account_update(); - Call a function to log updates of accounts, but only if + Call a function to log updates of accounts, but only if something changed: diff --git a/doc/src/sgml/ref/create_user.sgml b/doc/src/sgml/ref/create_user.sgml index 48d2089238c7f..8a138c001c22f 100644 --- a/doc/src/sgml/ref/create_user.sgml +++ b/doc/src/sgml/ref/create_user.sgml @@ -36,10 +36,8 @@ CREATE USER name [ [ WITH ] password' | PASSWORD NULL | VALID UNTIL 'timestamp' | IN ROLE role_name [, ...] - | IN GROUP role_name [, ...] | ROLE role_name [, ...] | ADMIN role_name [, ...] - | USER role_name [, ...] | SYSID uid diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml index e8d9d3c8d0f64..60215eba3b81c 100644 --- a/doc/src/sgml/ref/create_view.sgml +++ b/doc/src/sgml/ref/create_view.sgml @@ -415,7 +415,7 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello; DELETE, or MERGE statement on the view into the corresponding statement on the underlying base relation. INSERT statements that have an ON - CONFLICT UPDATE clause are fully supported. + CONFLICT clause are fully supported. @@ -430,7 +430,7 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello; an INSERT or MERGE command can potentially insert base-relation rows that do not satisfy the WHERE condition and thus are not - visible through the view (ON CONFLICT UPDATE may + visible through the view (ON CONFLICT DO SELECT/UPDATE may similarly affect an existing row not visible through the view). The CHECK OPTION may be used to prevent INSERT, UPDATE, and @@ -492,7 +492,7 @@ CREATE VIEW comedies AS WHERE kind = 'Comedy'; This will create a view containing the columns that are in the - film table at the time of view creation. Though + film table at the time of view creation. Though * was used to create the view, columns added later to the table will not be part of the view. @@ -507,12 +507,12 @@ CREATE VIEW universal_comedies AS WHERE classification = 'U' WITH LOCAL CHECK OPTION; - This will create a view based on the comedies view, showing + This will create a view based on the comedies view, showing only films with kind = 'Comedy' and classification = 'U'. Any attempt to INSERT or UPDATE a row in the view will be rejected if the new row doesn't have classification = 'U', but the film - kind will not be checked. + kind will not be checked. @@ -525,8 +525,8 @@ CREATE VIEW pg_comedies AS WHERE classification = 'PG' WITH CASCADED CHECK OPTION; - This will create a view that checks both the kind and - classification of new rows. + This will create a view that checks both the kind and + classification of new rows. @@ -543,9 +543,9 @@ CREATE VIEW comedies AS WHERE f.kind = 'Comedy'; This view will support INSERT, UPDATE and - DELETE. All the columns from the films table will - be updatable, whereas the computed columns country and - avg_rating will be read-only. + DELETE. All the columns from the films table will + be updatable, whereas the computed columns country and + avg_rating will be read-only. diff --git a/doc/src/sgml/ref/createdb.sgml b/doc/src/sgml/ref/createdb.sgml index 5c4e0465ed9da..2ccbe13f39008 100644 --- a/doc/src/sgml/ref/createdb.sgml +++ b/doc/src/sgml/ref/createdb.sgml @@ -136,7 +136,8 @@ PostgreSQL documentation - Specifies the LC_COLLATE setting to be used in this database. + Specifies the LC_COLLATE setting to be used in this database (ignored + unless the locale provider is libc). diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml index 5c34c6234233b..0c0614285141e 100644 --- a/doc/src/sgml/ref/createuser.sgml +++ b/doc/src/sgml/ref/createuser.sgml @@ -539,7 +539,7 @@ PostgreSQL documentation $ createuser -P -s -e joe Enter password for new role: xyzzy Enter it again: xyzzy -CREATE ROLE joe PASSWORD 'md5b5f5ba1a423792b526f799ae4eb3d59e' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN; +CREATE ROLE joe PASSWORD 'SCRAM-SHA-256$4096:44560wPMLfjqiAzyPDZ/eQ==$4CA054rZlSFEq8Z3FEhToBTa2X6KnWFxFkPwIbKoDe0=:L/nbSZRCjp6RhOhKK56GoR1zibCCSePKshCJ9lnl3yw=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS; In the above example, the new password isn't actually echoed when typed, but we show what was typed for clarity. As you see, the password is diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml index 29649f6afd65c..9066d7ea83df8 100644 --- a/doc/src/sgml/ref/delete.sgml +++ b/doc/src/sgml/ref/delete.sgml @@ -22,11 +22,18 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] -DELETE FROM [ ONLY ] table_name [ * ] [ [ AS ] alias ] +DELETE FROM [ ONLY ] table_name [ * ] + [ FOR PORTION OF range_column_name for_portion_of_target ] + [ [ AS ] alias ] [ USING from_item [, ...] ] [ WHERE condition | WHERE CURRENT OF cursor_name ] [ RETURNING [ WITH ( { OLD | NEW } AS output_alias [, ...] ) ] { * | output_expression [ [ AS ] output_name ] } [, ...] ] + +where for_portion_of_target is: + +{ FROM start_time TO end_time | + ( portion ) } @@ -64,12 +71,33 @@ DELETE FROM [ ONLY ] table_name [ * output list of SELECT. + + If the FOR PORTION OF clause is used, the delete will + only affect rows that overlap the given portion. Furthermore, if a row's + application time extends outside the FOR PORTION OF + bounds, then the delete will only change the application time within those + bounds. In effect, only the history targeted by FOR PORTION + OF is deleted, and no moments outside. Furthermore, after a row + is deleted, new temporal + leftovers might be inserted: rows whose range or multirange + receives the remaining application time outside the targeted bounds, with + the original values in their other columns. For range columns, there will + be zero to two inserted records, depending on whether the original + application time was completely deleted, extended before/after the change, + or both. Multiranges never require two temporal leftovers, because one + value can always contain whatever application time remains. + + You must have the DELETE privilege on the table to delete from it, as well as the SELECT privilege for any table in the USING clause or whose values are read in the condition. + When FOR PORTION OF is used, the secondary inserts do + not require INSERT privilege on the table. (This is + because conceptually no new information is being added; the inserted rows + only preserve existing data about the untargeted time period.) @@ -117,6 +145,56 @@ DELETE FROM [ ONLY ] table_name [ * + + range_column_name + + + The range or multirange column to use when performing a temporal delete. + + + + + + for_portion_of_target + + + The portion to delete. If targeting a range column, this can be in the + form FROM start_time TO + end_time. Otherwise, it + must be in the form (portion), where + portion is an expression + that yields a value of the same type as range_column_name. + + + + + + start_time + + + The earliest time (inclusive) to change in a temporal delete. This must + be a value matching the base type of the range from range_column_name. A null value here + indicates a delete whose beginning is unbounded (as with range types). + + + + + + end_time + + + The latest time (exclusive) to change in a temporal delete. This must + be a value matching the base type of the range from range_column_name. A null value here + indicates a delete whose end is unbounded (as with range types). + + + + from_item @@ -238,6 +316,10 @@ DELETE count suppressed by a BEFORE DELETE trigger. If count is 0, no rows were deleted by the query (this is not considered an error). + If FOR PORTION OF was used, the + count does not include + temporal leftovers + that were inserted. @@ -245,7 +327,13 @@ DELETE count clause, the result will be similar to that of a SELECT statement containing the columns and values defined in the RETURNING list, computed over the row(s) deleted by the - command. + command. If FOR PORTION OF was used, the + RETURNING clause gives one result for each deleted row, + but does not include inserted + temporal leftovers. + The value of the application-time column matches the old value of the deleted + row(s). Note this will represent more application time than was actually erased, + if temporal leftovers were inserted. @@ -272,6 +360,13 @@ DELETE FROM films In some cases the join style is easier to write or faster to execute than the sub-select style. + + + When FOR PORTION OF is used, this can result in users + who don't have INSERT privileges firing + INSERT triggers. This should be considered when + using SECURITY DEFINER trigger functions. + @@ -285,7 +380,7 @@ DELETE FROM films WHERE kind <> 'Musical'; - Clear the table films: + Clear the table films: DELETE FROM films; @@ -306,6 +401,15 @@ DELETE FROM tasks WHERE CURRENT OF c_tasks; + + An example of a temporal delete: + +DELETE FROM products + FOR PORTION OF valid_at FROM '2021-08-01' TO '2023-09-01' + WHERE product_no = 5; + + + While there is no LIMIT clause for DELETE, it is possible to get a similar effect @@ -323,6 +427,9 @@ DELETE FROM user_logs AS dl USING delete_batch AS del WHERE dl.ctid = del.ctid; + This use of ctid is only safe because + the query is repeatedly run, avoiding the problem of changed + ctids. diff --git a/doc/src/sgml/ref/drop_owned.sgml b/doc/src/sgml/ref/drop_owned.sgml index 46e1c229ec0fb..efda01a39e88b 100644 --- a/doc/src/sgml/ref/drop_owned.sgml +++ b/doc/src/sgml/ref/drop_owned.sgml @@ -33,7 +33,7 @@ DROP OWNED BY { name | CURRENT_ROLE database that are owned by one of the specified roles. Any privileges granted to the given roles on objects in the current database or on shared objects (databases, tablespaces, configuration - parameters, or other roles) will also be revoked. + parameters) will also be revoked. diff --git a/doc/src/sgml/ref/drop_property_graph.sgml b/doc/src/sgml/ref/drop_property_graph.sgml new file mode 100644 index 0000000000000..e16de5507b19a --- /dev/null +++ b/doc/src/sgml/ref/drop_property_graph.sgml @@ -0,0 +1,111 @@ + + + + + DROP PROPERTY GRAPH + + + + DROP PROPERTY GRAPH + 7 + SQL - Language Statements + + + + DROP PROPERTY GRAPH + remove an SQL-property graph + + + + +DROP PROPERTY GRAPH [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] + + + + + Description + + + DROP PROPERTY GRAPH drops an existing property graph. + To execute this command you must be the owner of the property graph. + + + + + Parameters + + + + IF EXISTS + + + Do not throw an error if the property graph does not exist. A notice is + issued in this case. + + + + + + name + + + The name (optionally schema-qualified) of the property graph to remove. + + + + + + CASCADE + + + Automatically drop objects that depend on the property graph, and in + turn all objects that depend on those objects (see ). + + + + + + RESTRICT + + + Refuse to drop the property graph if any objects depend on it. This is + the default. + + + + + + + + Examples + + + +DROP PROPERTY GRAPH g1; + + + + + Compatibility + + + DROP PROPERTY GRAPH conforms to ISO/IEC 9075-16 + (SQL/PGQ), except that the standard only allows one property graph to be + dropped per command, and apart from the IF EXISTS + option, which is a PostgreSQL extension. + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/drop_subscription.sgml b/doc/src/sgml/ref/drop_subscription.sgml index d4f54c7170eac..6e84bb0a25664 100644 --- a/doc/src/sgml/ref/drop_subscription.sgml +++ b/doc/src/sgml/ref/drop_subscription.sgml @@ -49,6 +49,16 @@ DROP SUBSCRIPTION [ IF EXISTS ] nameParameters + + IF EXISTS + + + Do not throw an error if the subscription does not exist. A notice is + issued in this case. + + + + name diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml index 6dda680aa0de8..e95e19081e189 100644 --- a/doc/src/sgml/ref/explain.sgml +++ b/doc/src/sgml/ref/explain.sgml @@ -46,6 +46,7 @@ EXPLAIN [ ( option [, ...] ) ] boolean ] SUMMARY [ boolean ] MEMORY [ boolean ] + IO [ boolean ] FORMAT { TEXT | XML | JSON | YAML } @@ -202,7 +203,10 @@ ROLLBACK; The number of blocks shown for an upper-level node includes those used by all its child nodes. In text format, only non-zero values are printed. Buffers information is - automatically included when ANALYZE is used. + included by default when ANALYZE is used but + otherwise is not included by default. When this parameter is + TRUE without ANALYZE, + only buffer usage during the query planning phase is reported. @@ -241,7 +245,8 @@ ROLLBACK; Include information on WAL record generation. Specifically, include the number of records, number of full page images (fpi), the amount of WAL - generated in bytes and the number of times the WAL buffers became full. + generated in bytes, the amount of full page images generated in bytes, + and the number of times the WAL buffers became full. In text format, only non-zero values are printed. This parameter may only be used when ANALYZE is also enabled. It defaults to FALSE. @@ -294,6 +299,17 @@ ROLLBACK; + + IO + + + Include information on I/O performed by scan nodes proving such information. + This parameter may only be used when ANALYZE is also + enabled. It defaults to FALSE. + + + + FORMAT diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 999f657d5c008..67426d42285d3 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -82,6 +82,11 @@ GRANT { { SET | ALTER SYSTEM } [, ... ] | ALL [ PRIVILEGES ] } TO role_specification [, ...] [ WITH GRANT OPTION ] [ GRANTED BY role_specification ] +GRANT { SELECT | ALL [ PRIVILEGES ] } + ON PROPERTY GRAPH graph_name [, ...] + TO role_specification [, ...] [ WITH GRANT OPTION ] + [ GRANTED BY role_specification ] + GRANT { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] } ON SCHEMA schema_name [, ...] TO role_specification [, ...] [ WITH GRANT OPTION ] @@ -119,7 +124,7 @@ GRANT role_name [, ...] TO @@ -153,9 +158,9 @@ GRANT role_name [, ...] TO - If GRANTED BY is specified, the specified grantor must - be the current user. This clause is currently present in this form only - for SQL compatibility. + If GRANTED BY is specified, the grant is recorded as + having been done by the specified role. A role can only attribute a grant + to another role if it inherits the privileges of that role. @@ -320,7 +325,7 @@ GRANT role_name [, ...] TO If GRANTED BY is specified, the grant is recorded as having been done by the specified role. A user can only attribute a grant - to another role if they possess the privileges of that role. The role + to another role if it inherits the privileges of that role. The role recorded as the grantor must have ADMIN OPTION on the target role, unless it is the bootstrap superuser. When a grant is recorded as having a grantor other than the bootstrap superuser, it depends on the @@ -434,7 +439,7 @@ GRANT role_name [, ...] TO Examples - Grant insert privilege to all users on table films: + Grant insert privilege to all users on table films: GRANT INSERT ON films TO PUBLIC; @@ -443,14 +448,14 @@ GRANT INSERT ON films TO PUBLIC; Grant all available privileges to user manuel on view - kinds: + kinds: GRANT ALL PRIVILEGES ON kinds TO manuel; Note that while the above will indeed grant all privileges if executed by a - superuser or the owner of kinds, when executed by someone + superuser or the owner of kinds, when executed by someone else it will only grant those permissions for which the someone else has grant options. diff --git a/doc/src/sgml/ref/initdb.sgml b/doc/src/sgml/ref/initdb.sgml index 7613174c18b56..bd0dbff8caa56 100644 --- a/doc/src/sgml/ref/initdb.sgml +++ b/doc/src/sgml/ref/initdb.sgml @@ -28,7 +28,7 @@ PostgreSQL documentation - directory + datadir @@ -190,8 +190,8 @@ PostgreSQL documentation - - + + This option specifies the directory where the database cluster diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml index 3f13991779050..121a9edcb99da 100644 --- a/doc/src/sgml/ref/insert.sgml +++ b/doc/src/sgml/ref/insert.sgml @@ -37,6 +37,7 @@ INSERT INTO table_name [ AS and conflict_action is one of: DO NOTHING + DO SELECT [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } ] [ WHERE condition ] DO UPDATE SET { column_name = { expression | DEFAULT } | ( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) | ( column_name [, ...] ) = ( sub-SELECT ) @@ -89,24 +90,27 @@ INSERT INTO table_name [ AS The optional RETURNING clause causes INSERT to compute and return value(s) based on each row actually inserted - (or updated, if an ON CONFLICT DO UPDATE clause was - used). This is primarily useful for obtaining values that were + (or selected or updated, if an ON CONFLICT DO SELECT/UPDATE + clause was used). This is primarily useful for obtaining values that were supplied by defaults, such as a serial sequence number. However, any expression using the table's columns is allowed. The syntax of the RETURNING list is identical to that of the output list of SELECT. Only rows that were successfully - inserted or updated will be returned. For example, if a row was - locked but not updated because an ON CONFLICT DO UPDATE - ... WHERE clause condition was not satisfied, the - row will not be returned. + inserted, updated, or selected will be returned. For example, if a row was + locked but not updated or selected because an ON CONFLICT ... + WHERE clause condition + was not satisfied, the row will not be returned. You must have INSERT privilege on a table in order to insert into it. If ON CONFLICT DO UPDATE is present, UPDATE privilege on the table is also - required. + required. If ON CONFLICT DO SELECT is present, + SELECT privilege on the table is required. + If ON CONFLICT DO SELECT FOR UPDATE/SHARE is used, + UPDATE privilege is required on at least one + column, in addition to SELECT privilege. @@ -114,10 +118,13 @@ INSERT INTO table_name [ AS INSERT privilege on the listed columns. Similarly, when ON CONFLICT DO UPDATE is specified, you only need UPDATE privilege on the column(s) that are - listed to be updated. However, ON CONFLICT DO UPDATE - also requires SELECT privilege on any column whose - values are read in the ON CONFLICT DO UPDATE - expressions or condition. + listed to be updated. However, all forms of ON CONFLICT + also require SELECT privilege on any column whose values + are read. This includes any column mentioned in + conflict_target (including columns referred to + by the arbiter constraint), and any column mentioned in an + ON CONFLICT DO UPDATE expression, + or a WHERE clause condition. @@ -340,8 +347,11 @@ INSERT INTO table_name [ AS For a simple INSERT, all old values will be NULL. However, for an INSERT - with an ON CONFLICT DO UPDATE clause, the old - values may be non-NULL. + with an ON CONFLICT DO SELECT/UPDATE clause, the + old values may be non-NULL (when the row proposed + for insertion conflicts with an existing row). If the + SELECT path is taken, the new values will be + identical to the old values, since no modification takes place. @@ -377,6 +387,9 @@ INSERT INTO table_name [ AS ON CONFLICT DO UPDATE updates the existing row that conflicts with the row proposed for insertion as its alternative action. + ON CONFLICT DO SELECT returns the existing row + that conflicts with the row proposed for insertion, optionally + with row-level locking. @@ -408,6 +421,15 @@ INSERT INTO table_name [ AS . + + ON CONFLICT DO SELECT similarly allows an atomic + INSERT or SELECT outcome. This + is also known as idempotent insert or + get or create. For ON CONFLICT DO + SELECT, a RETURNING clause + must be provided. + + conflict_target @@ -421,7 +443,8 @@ INSERT INTO table_name [ AS conflict_target; when omitted, conflicts with all usable constraints (and unique indexes) are handled. For ON CONFLICT DO - UPDATE, a conflict_target + UPDATE and ON CONFLICT DO SELECT, + a conflict_target must be provided. @@ -431,19 +454,23 @@ INSERT INTO table_name [ AS conflict_action - conflict_action specifies an - alternative ON CONFLICT action. It can be - either DO NOTHING, or a DO - UPDATE clause specifying the exact details of the - UPDATE action to be performed in case of a - conflict. The SET and - WHERE clauses in ON CONFLICT DO - UPDATE have access to the existing row using the - table's name (or an alias), and to the row proposed for insertion - using the special excluded table. - SELECT privilege is required on any column in the - target table where corresponding excluded - columns are read. + conflict_action specifies an alternative + ON CONFLICT action. It can be + DO NOTHING, a DO SELECT + clause that allows conflicting rows to be returned, or a + DO UPDATE clause specifying the exact details + of the UPDATE action to be performed in case + of a conflict. + + + The SET clause in DO UPDATE + and the WHERE clause in both + DO SELECT and DO UPDATE have + access to the existing row using the table's name (or an alias), + and to the row proposed for insertion using the special + excluded table. SELECT + privilege is required on any column in the target table where + corresponding excluded columns are read. Note that the effects of all per-row BEFORE @@ -542,24 +569,41 @@ INSERT INTO table_name [ AS + + FOR UPDATE + FOR NO KEY UPDATE + FOR SHARE + FOR KEY SHARE + + + When specified in an ON CONFLICT DO SELECT clause, + conflicting table rows are locked against concurrent updates. + See in the + documentation. + + + + condition An expression that returns a value of type boolean. Only rows for which this expression - returns true will be updated, although all - rows will be locked when the ON CONFLICT DO UPDATE - action is taken. Note that - condition is evaluated last, after - a conflict has been identified as a candidate to update. + returns true will be updated or selected for + return, although all conflicting rows will be locked when + ON CONFLICT DO UPDATE or + ON CONFLICT DO SELECT FOR UPDATE/SHARE is + specified. Note that condition is + evaluated last, after a conflict has been identified as a candidate + to update or select. Note that exclusion constraints are not supported as arbiters with - ON CONFLICT DO UPDATE. In all cases, only + ON CONFLICT DO SELECT/UPDATE. In all cases, only NOT DEFERRABLE constraints and unique indexes are supported as arbiters. @@ -607,7 +651,7 @@ INSERT INTO table_name [ AS oid count The count is the number of - rows inserted or updated. oid is always 0 (it + rows inserted, updated, or selected for return. oid is always 0 (it used to be the OID assigned to the inserted row if count was exactly one and the target table was declared WITH OIDS and 0 otherwise, but creating a table @@ -618,8 +662,7 @@ INSERT oid countINSERT command contains a RETURNING clause, the result will be similar to that of a SELECT statement containing the columns and values defined in the - RETURNING list, computed over the row(s) inserted or - updated by the command. + RETURNING list, computed over the row(s) affected by the command. @@ -645,7 +688,7 @@ INSERT oid countExamples - Insert a single row into table films: + Insert a single row into table films: INSERT INTO films VALUES @@ -654,7 +697,7 @@ INSERT INTO films VALUES - In this example, the len column is + In this example, the len column is omitted and therefore it will have the default value: @@ -695,8 +738,8 @@ INSERT INTO films (code, title, did, date_prod, kind) VALUES This example inserts some rows into table - films from a table tmp_films - with the same column layout as films: + films from a table tmp_films + with the same column layout as films: INSERT INTO films SELECT * FROM tmp_films WHERE date_prod < '2004-05-07'; @@ -717,7 +760,7 @@ INSERT INTO tictactoe (game, board) - Insert a single row into table distributors, returning + Insert a single row into table distributors, returning the sequence number generated by the DEFAULT clause: @@ -742,8 +785,8 @@ INSERT INTO employees_log SELECT *, current_timestamp FROM upd; Insert or update new distributors as appropriate. Assumes a unique index has been defined that constrains values appearing in the - did column. Note that the special - excluded table is used to reference values originally + did column. Note that the special + excluded table is used to reference values originally proposed for insertion: INSERT INTO distributors (did, dname) @@ -754,8 +797,8 @@ INSERT INTO distributors (did, dname) Insert or update new distributors as above, returning information about any existing values that were updated, together with the new data - inserted. Note that the returned values for old_did - and old_dname will be NULL for + inserted. Note that the returned values for old_did + and old_dname will be NULL for non-conflicting rows: INSERT INTO distributors (did, dname) @@ -770,7 +813,7 @@ INSERT INTO distributors (did, dname) when an existing, excluded row (a row with a matching constrained column or columns after before row insert triggers fire) exists. Example assumes a unique index has been defined that constrains - values appearing in the did column: + values appearing in the did column: INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING; @@ -779,7 +822,7 @@ INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') Insert or update new distributors as appropriate. Example assumes a unique index has been defined that constrains values appearing in - the did column. WHERE clause is + the did column. WHERE clause is used to limit the rows actually updated (any existing row not updated will still be locked, though): @@ -793,14 +836,43 @@ INSERT INTO distributors AS d (did, dname) VALUES (8, 'Anvil Distribution') -- index to arbitrate taking the DO NOTHING action) INSERT INTO distributors (did, dname) VALUES (9, 'Antwerp Design') ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING; + + + + Insert new distributor if possible, otherwise return the existing + distributor row. Example assumes a unique index has been defined + that constrains values appearing in the did column. + This is useful for get-or-create patterns: + +INSERT INTO distributors (did, dname) VALUES (11, 'Global Electronics') + ON CONFLICT (did) DO SELECT + RETURNING *; + + + + Insert a new distributor if the ID doesn't match, otherwise return + the existing row, if its name doesn't match: + +INSERT INTO distributors AS d (did, dname) VALUES (12, 'Micro Devices Inc') + ON CONFLICT (did) DO SELECT WHERE d.dname != EXCLUDED.dname + RETURNING *; + + + + Insert a new distributor or return and lock the existing row for update. + This is useful when you need to ensure exclusive access to the row: + +INSERT INTO distributors (did, dname) VALUES (13, 'Advanced Systems') + ON CONFLICT (did) DO SELECT FOR UPDATE + RETURNING *; Insert new distributor if possible; otherwise DO NOTHING. Example assumes a unique index has been defined that constrains values appearing in the - did column on a subset of rows where the - is_active Boolean column evaluates to + did column on a subset of rows where the + is_active Boolean column evaluates to true: -- This statement could infer a partial unique index on "did" diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml index ecbcd8345d874..765fe7a7d625f 100644 --- a/doc/src/sgml/ref/merge.sgml +++ b/doc/src/sgml/ref/merge.sgml @@ -23,37 +23,37 @@ PostgreSQL documentation [ WITH with_query [, ...] ] MERGE INTO [ ONLY ] target_table_name [ * ] [ [ AS ] target_alias ] -USING data_source ON join_condition -when_clause [...] -[ RETURNING [ WITH ( { OLD | NEW } AS output_alias [, ...] ) ] - { * | output_expression [ [ AS ] output_name ] } [, ...] ] + USING data_source ON join_condition + when_clause [...] + [ RETURNING [ WITH ( { OLD | NEW } AS output_alias [, ...] ) ] + { * | output_expression [ [ AS ] output_name ] } [, ...] ] where data_source is: -{ [ ONLY ] source_table_name [ * ] | ( source_query ) } [ [ AS ] source_alias ] + { [ ONLY ] source_table_name [ * ] | ( source_query ) } [ [ AS ] source_alias ] and when_clause is: -{ WHEN MATCHED [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING } | - WHEN NOT MATCHED BY SOURCE [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING } | - WHEN NOT MATCHED [ BY TARGET ] [ AND condition ] THEN { merge_insert | DO NOTHING } } + { WHEN MATCHED [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING } | + WHEN NOT MATCHED BY SOURCE [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING } | + WHEN NOT MATCHED [ BY TARGET ] [ AND condition ] THEN { merge_insert | DO NOTHING } } and merge_insert is: -INSERT [( column_name [, ...] )] -[ OVERRIDING { SYSTEM | USER } VALUE ] -{ VALUES ( { expression | DEFAULT } [, ...] ) | DEFAULT VALUES } + INSERT [( column_name [, ...] )] + [ OVERRIDING { SYSTEM | USER } VALUE ] + { VALUES ( { expression | DEFAULT } [, ...] ) | DEFAULT VALUES } and merge_update is: -UPDATE SET { column_name = { expression | DEFAULT } | - ( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) | - ( column_name [, ...] ) = ( sub-SELECT ) - } [, ...] + UPDATE SET { column_name = { expression | DEFAULT } | + ( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) | + ( column_name [, ...] ) = ( sub-SELECT ) + } [, ...] and merge_delete is: -DELETE + DELETE @@ -106,10 +106,11 @@ DELETE to compute and return value(s) based on each row inserted, updated, or deleted. Any expression using the source or target table's columns, or the merge_action() - function can be computed. When an INSERT or + function can be computed. By default, when an INSERT or UPDATE action is performed, the new values of the target - table's columns are used. When a DELETE is performed, - the old values of the target table's columns are used. The syntax of the + table's columns are used, and when a DELETE is performed, + the old values of the target table's columns are used, but it is also + possible to explicitly request old and new values. The syntax of the RETURNING list is identical to that of the output list of SELECT. @@ -713,7 +714,8 @@ MERGE total_count on the behavior at each isolation level. You may also wish to consider using INSERT ... ON CONFLICT as an alternative statement which offers the ability to run an - UPDATE if a concurrent INSERT + UPDATE or return the existing row (with + DO SELECT) if a concurrent INSERT occurs. There are a variety of differences and restrictions between the two statement types and they are not interchangeable. diff --git a/doc/src/sgml/ref/pg_amcheck.sgml b/doc/src/sgml/ref/pg_amcheck.sgml index 6bfe28799c4e6..ef2bdfd19ae5d 100644 --- a/doc/src/sgml/ref/pg_amcheck.sgml +++ b/doc/src/sgml/ref/pg_amcheck.sgml @@ -41,7 +41,7 @@ PostgreSQL documentation - Only ordinary and toast table relations, materialized views, sequences, and + Only ordinary and TOAST table relations, materialized views, sequences, and btree indexes are currently supported. Other relation types are silently skipped. @@ -276,7 +276,7 @@ PostgreSQL documentation - By default, if a table is checked, its toast table, if any, will also + By default, if a table is checked, its TOAST table, if any, will also be checked, even if it is not explicitly selected by an option such as --table or --relation. This option suppresses that behavior. @@ -306,9 +306,9 @@ PostgreSQL documentation - By default, whenever a toast pointer is encountered in a table, + By default, whenever a TOAST pointer is encountered in a table, a lookup is performed to ensure that it references apparently-valid - entries in the toast table. These checks can be quite slow, and this + entries in the TOAST table. These checks can be quite slow, and this option can be used to skip them. @@ -368,9 +368,9 @@ PostgreSQL documentation End checking at the specified block number. An error will occur if the table relation being checked has fewer than this number of blocks. This option does not apply to indexes, and is probably only useful when - checking a single table relation. If both a regular table and a toast + checking a single table relation. If both a regular table and a TOAST table are checked, this option will apply to both, but higher-numbered - toast blocks may still be accessed while validating toast pointers, + TOAST blocks may still be accessed while validating TOAST pointers, unless that is suppressed using . diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml index 9659f76042c5b..fecee08b0a536 100644 --- a/doc/src/sgml/ref/pg_basebackup.sgml +++ b/doc/src/sgml/ref/pg_basebackup.sgml @@ -500,8 +500,9 @@ PostgreSQL documentation - Sets checkpoint mode to fast (immediate) or spread (the default) + Sets checkpoint mode to fast or spread (see ). + The default is spread. diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml index 95043aa329c02..45890324075cb 100644 --- a/doc/src/sgml/ref/pg_checksums.sgml +++ b/doc/src/sgml/ref/pg_checksums.sgml @@ -45,6 +45,12 @@ PostgreSQL documentation exit status is nonzero if the operation failed. + + When enabling checksums, if checksums were in the process of being enabled + when the cluster was shut down, pg_checksums + will still process all relations regardless of the online processing. + + When verifying checksums, every file in the cluster is scanned. When enabling checksums, each relation file block with a changed checksum is @@ -61,8 +67,8 @@ PostgreSQL documentation - - + + Specifies the directory where the database cluster is stored. @@ -247,5 +253,9 @@ PostgreSQL documentation remains unchanged, and pg_checksums can be re-run to perform the same operation. + + The target cluster must have the same major version as + pg_checksums. + diff --git a/doc/src/sgml/ref/pg_combinebackup.sgml b/doc/src/sgml/ref/pg_combinebackup.sgml index 330a598f7013e..9a6d201e0b8e3 100644 --- a/doc/src/sgml/ref/pg_combinebackup.sgml +++ b/doc/src/sgml/ref/pg_combinebackup.sgml @@ -314,7 +314,7 @@ PostgreSQL documentation To avoid this problem, taking a new full backup after changing the checksum - state of the cluster using is + state of the cluster using is recommended. Otherwise, you can disable and then optionally reenable checksums on the directory produced by pg_combinebackup in order to correct the problem. diff --git a/doc/src/sgml/ref/pg_controldata.sgml b/doc/src/sgml/ref/pg_controldata.sgml index b47fdca9dfcb2..22dc61610083e 100644 --- a/doc/src/sgml/ref/pg_controldata.sgml +++ b/doc/src/sgml/ref/pg_controldata.sgml @@ -47,14 +47,49 @@ PostgreSQL documentation This utility can only be run by the user who initialized the cluster because it requires read access to the data directory. You can specify the data directory on the command line, or use - the environment variable PGDATA. This utility supports the options - and , which print the - pg_controldata version and exit. It also - supports options and , which output the - supported arguments. + the environment variable PGDATA. + + Options + + + + + + + + + Specifies the directory where the database cluster is stored. + + + + + + + + + + Print the pg_controldata version and exit. + + + + + + + + + + Show help about pg_controldata command line + arguments, and exit. + + + + + + + Environment diff --git a/doc/src/sgml/ref/pg_createsubscriber.sgml b/doc/src/sgml/ref/pg_createsubscriber.sgml index 4b1d08d5f16da..3b3038d18914e 100644 --- a/doc/src/sgml/ref/pg_createsubscriber.sgml +++ b/doc/src/sgml/ref/pg_createsubscriber.sgml @@ -126,8 +126,8 @@ PostgreSQL documentation - - + + The target directory that contains a cluster directory from a physical @@ -136,6 +136,40 @@ PostgreSQL documentation + + + + + + Specify the name of the log directory. A new directory is created with + this name if it does not exist. A subdirectory with a timestamp + indicating the time at which pg_createsubscriber + was run will be created. The following two log files will be created in + the subdirectory. + + + + pg_createsubscriber_server.log which captures logs + related to stopping and starting the standby server, + + + + + pg_createsubscriber_internal.log which captures + internal diagnostic output (validations, checks, etc.) + + + + By default, the umask is set to 077 so that the + log files are only readable by the user running the command. However, if + the target data directory is configured to allow group-read access, + pg_createsubscriber will adjust the log file + permissions to match. This ensures that the log file security remains + consistent with the database cluster itself. + + + + @@ -169,36 +203,6 @@ PostgreSQL documentation - - - - - - Remove all objects of the specified type from specified databases on the - target server. - - - - - - publications: - The FOR ALL TABLES publications established for this - subscriber are always removed; specifying this object type causes all - other publications replicated from the source server to be dropped as - well. - - - - - - The objects selected to be dropped are individually logged, including during - a . There is no opportunity to affect or stop the - dropping of the selected objects, so consider taking a backup of them - using pg_dump. - - - - @@ -259,6 +263,35 @@ PostgreSQL documentation + + + + + Drop all objects of the specified type from specified databases on the + target server. + + + + + + publications: + The FOR ALL TABLES publications established for this + subscriber are always dropped; specifying this object type causes all + other publications replicated from the source server to be dropped as + well. + + + + + + The objects selected to be dropped are individually logged, including during + a . There is no opportunity to affect or stop the + dropping of the selected objects, so consider taking a backup of them + using pg_dump. + + + + @@ -286,6 +319,14 @@ PostgreSQL documentation a generated name is assigned to the publication name. This option cannot be used together with . + + If a specified publication already exists on the publisher, it is reused. + It is useful to partially replicate the database if the specified + publication includes a list of tables. If the publication does not exist, + it is automatically created with FOR ALL TABLES. Use + option to preview which publications will be + reused and which will be created. + @@ -380,12 +421,12 @@ PostgreSQL documentation The source server must accept connections from the target server. The source server must not be in recovery. The source server must have as logical. The source server - must have configured to a value - greater than or equal to the number of specified databases plus existing - replication slots. The source server must have configured to a value greater than or equal - to the number of specified databases and existing WAL sender processes. + linkend="guc-wal-level"/> as replica or logical. + The source server must have + configured to a value greater than or equal to the number of specified + databases plus existing replication slots. The source server must have + configured to a value greater than or + equal to the number of specified databases and existing WAL sender processes. @@ -511,8 +552,11 @@ PostgreSQL documentation - Write recovery parameters into the target data directory and restart the - target server. It specifies an LSN (pg_createsubscriber.conf that is included from + postgresql.auto.conf using + include_if_exists in the target data directory, then + restart the target server. It specifies an LSN () of the write-ahead log location up to which recovery will proceed. It also specifies promote as the action that the server should take @@ -525,7 +569,10 @@ PostgreSQL documentation server ends standby mode and is accepting read-write transactions. If option is set, pg_createsubscriber terminates if recovery - does not end until the given number of seconds. + does not end until the given number of seconds. Upon completion, the + included configuration file is renamed to + pg_createsubscriber.conf.disabled so as it is no + longer loaded on subsequent restarts. diff --git a/doc/src/sgml/ref/pg_ctl-ref.sgml b/doc/src/sgml/ref/pg_ctl-ref.sgml index a0287bb81d623..b762b002c02bb 100644 --- a/doc/src/sgml/ref/pg_ctl-ref.sgml +++ b/doc/src/sgml/ref/pg_ctl-ref.sgml @@ -299,8 +299,9 @@ PostgreSQL documentation Append the server log output to filename. If the file does not - exist, it is created. The umask is set to 077, - so access to the log file is disallowed to other users by default. + exist, it is created. By default, only the cluster owner can + access the log file. If group access is enabled in the cluster, + users in the same group as the cluster owner can also read it. diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml index c10bca63e55c5..ae1bc14d2f26f 100644 --- a/doc/src/sgml/ref/pg_dump.sgml +++ b/doc/src/sgml/ref/pg_dump.sgml @@ -18,7 +18,7 @@ PostgreSQL documentation pg_dump - extract a PostgreSQL database into a script file or other archive file + export a PostgreSQL database as an SQL script or to other formats @@ -96,6 +96,18 @@ PostgreSQL documentation light of the limitations listed below. + + + Restoring a dump causes the destination to execute arbitrary code of the + source superusers' choice. Partial dumps and partial restores do not limit + that. If the source superusers are not trusted, the dumped SQL statements + must be inspected before restoring. Non-plain-text dumps can be inspected + by using pg_restore's + option. Note that the client running the dump and restore need not trust + the source or destination superusers. + + + @@ -251,12 +263,10 @@ PostgreSQL documentation - When is specified, - pg_dump makes no attempt to dump any other - database objects that the selected extension(s) might depend upon. - Therefore, there is no guarantee that the results of a - specific-extension dump can be successfully restored by themselves - into a clean database. + pg_dump does not dump the extension's + underlying installation files (such as shared libraries or control + files). These must be available on the destination system for the + restore to succeed. @@ -285,8 +295,8 @@ PostgreSQL documentation file based output formats, in which case the standard output is used. It must be given for the directory output format however, where it specifies the target directory instead of a file. In this case the - directory is created by pg_dump and must not exist - before. + directory is created by pg_dump unless the directory + exists and is empty. @@ -433,16 +443,6 @@ PostgreSQL documentation below. - - - When is specified, pg_dump - makes no attempt to dump any other database objects that the selected - schema(s) might depend upon. Therefore, there is no guarantee - that the results of a specific-schema dump can be successfully - restored by themselves into a clean database. - - - Non-schema objects such as large objects are not dumped when is @@ -584,16 +584,6 @@ PostgreSQL documentation be dumped. - - - When is specified, pg_dump - makes no attempt to dump any other database objects that the selected - table(s) might depend upon. Therefore, there is no guarantee - that the results of a specific-table dump can be successfully - restored by themselves into a clean database. - - - @@ -1134,7 +1124,7 @@ PostgreSQL documentation - Do not dump statistics. + Do not dump statistics. This is the default. @@ -1252,6 +1242,29 @@ PostgreSQL documentation + + + + + Use the provided string as the psql + \restrict key in the dump output. This can only be + specified for plain-text dumps, i.e., when is + set to plain or the option + is omitted. If no restrict key is specified, + pg_dump will generate a random one as + needed. Keys may contain only alphanumeric characters. + + + This option is primarily intended for testing purposes and other + scenarios that require repeatable output (e.g., comparing dump files). + It is not recommended for general use, as a malicious server with + advance knowledge of the key may be able to inject arbitrary code that + will be executed on the machine that runs + psql with the dump output. + + + + @@ -1277,11 +1290,11 @@ PostgreSQL documentation The data section contains actual table data, large-object - contents, statistics for tables and materialized views and - sequence values. + contents, sequence values, and statistics for tables, + materialized views, and foreign tables. Post-data items include definitions of indexes, triggers, rules, statistics for indexes, and constraints other than validated check - constraints. + and not-null constraints. Pre-data items include all other data definition items. @@ -1354,12 +1367,22 @@ PostgreSQL documentation + + + + + Dump optimizer statistics. + + + + Dump only the statistics, not the schema (data definitions) or data. - Statistics for tables, materialized views, and indexes are dumped. + Optimizer statistics for tables, materialized views, foreign tables, + and indexes are dumped. @@ -1439,33 +1462,6 @@ PostgreSQL documentation - - - - - Dump data. This is the default. - - - - - - - - - Dump schema (data definitions). This is the default. - - - - - - - - - Dump statistics. This is the default. - - - - @@ -1671,6 +1667,17 @@ CREATE DATABASE foo WITH TEMPLATE template0; + + When options , or + are specified, pg_dump makes no attempt to dump + any other database objects that the selected object(s) might depend upon. + Therefore, there is no guarantee that the results of a dump so generated + can be successfully restored by themselves into a clean database. + For example, if a table whose definition includes a foreign key is + specified to be restored, the table referenced by the foreign key is + not automatically restored. + + When a dump without schema is chosen and the option is used, pg_dump emits commands @@ -1681,14 +1688,27 @@ CREATE DATABASE foo WITH TEMPLATE template0; - By default, pg_dump will include most optimizer - statistics in the resulting dump file. However, some statistics may not be - included, such as those created explicitly with or custom statistics added by an - extension. Therefore, it may be useful to run ANALYZE - after restoring from a dump file to ensure optimal performance; see and for more - information. + When creating an archive (non-text) output file, it is advisable not to + restrict the set of database objects dumped, but instead plan to apply + any desired object filtering when reading the archive + with pg_restore. This will preserve + flexibility and possibly avoid problems at restore time; for details + see the documentation. + In particular, dumping table data without the corresponding table + definition (via and related options) is + not recommended. + + + + When is specified, + pg_dump will include most optimizer statistics in the + resulting dump file. This does not include all statistics, such as + those created explicitly with , + custom statistics added by an extension, or statistics collected by the + cumulative statistics system. Therefore, it may still be useful to + run ANALYZE after restoring from a dump file to ensure + optimal performance; see and for more information. @@ -1860,7 +1880,7 @@ CREATE DATABASE foo WITH TEMPLATE template0; To dump all tables whose names start with mytable, except - for table mytable2, specify a filter file + for table mytable2, specify a filter file filter.txt like: include table mytable* diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml index 8c5141d036c76..51c701980911e 100644 --- a/doc/src/sgml/ref/pg_dumpall.sgml +++ b/doc/src/sgml/ref/pg_dumpall.sgml @@ -16,7 +16,10 @@ PostgreSQL documentation pg_dumpall - extract a PostgreSQL database cluster using a specified dump format + + + export a PostgreSQL database cluster as an SQL script or to other formats + @@ -33,33 +36,41 @@ PostgreSQL documentation pg_dumpall is a utility for writing out (dumping) all PostgreSQL databases - of a cluster into an archive. The archive contains - SQL commands that can be used as input to to restore the databases. It does this by + of a cluster into an SQL script file or an archive. It does this by calling for each database in the cluster. + The output contains SQL commands that can be used + as input to or + to restore the databases. pg_dumpall also dumps global objects that are common to all databases, namely database roles, tablespaces, and privilege grants for configuration parameters. (pg_dump does not save these objects.) + The only parts of a database cluster's state that + are not included in the default output + of pg_dumpall are the configuration files + and any database parameter setting changes made with + . - Since pg_dumpall reads tables from all - databases you will most likely have to connect as a database - superuser in order to produce a complete dump. Also you will need - superuser privileges to execute the saved script in order to be - allowed to add roles and create databases. + If the output format is a + plain text SQL script, it will be written to the standard output. Use the + / option or shell operators to + redirect it into a file. - Plain text SQL scripts will be written to the standard output. Use the - / option or shell operators to - redirect it into a file. + If another output format is selected, the archive will be placed in a + directory named using the / + option, which is required in this case. - Archives in other formats will be placed in a directory named using the - /, which is required in this case. + Since pg_dumpall reads tables from all + databases you will most likely have to connect as a database + superuser in order to produce a complete dump. Also you will need + superuser privileges to execute the saved script in order to be + allowed to add roles and create databases. @@ -71,6 +82,16 @@ PostgreSQL documentation linkend="libpq-pgpass"/> for more information. + + + Restoring a dump causes the destination to execute arbitrary code of the + source superusers' choice. Partial dumps and partial restores do not limit + that. If the source superusers are not trusted, the dumped SQL statements + must be inspected before restoring. Note that the client running the dump + and restore need not trust the source or destination superusers. + + + @@ -126,7 +147,7 @@ PostgreSQL documentation Send output to the specified file. If this is omitted, the standard output is used. - Note: This option can only be omitted when is plain + This option can only be omitted when is plain. @@ -139,13 +160,13 @@ PostgreSQL documentation Specify the format of dump files. In plain format, all the dump data is sent in a single text stream. This is the default. - In all other modes, pg_dumpall first creates two files: - global.dat and map.dat, in the directory + In all other modes, pg_dumpall first creates two files, + toc.glo and map.dat, in the directory specified by . - The first file contains global data, such as roles and tablespaces. The second - contains a mapping between database oids and names. These files are used by + The first file contains global data (roles and tablespaces) in custom format. The second + contains a mapping between database OIDs and names. These files are used by pg_restore. Data for individual databases is placed in - databases subdirectory, named using the database's oid. + the databases subdirectory, named using the database's OID. @@ -198,8 +219,8 @@ PostgreSQL documentation - Note: see for details - of how the various non plain text archives work. + See for details on how the + various non-plain-text archive formats work. @@ -211,6 +232,8 @@ PostgreSQL documentation Dump only global objects (roles and tablespaces), no databases. + Note: cannot be used with + with non-text dump format. @@ -567,7 +590,7 @@ exclude database PATTERN - Do not dump statistics. + Do not dump statistics. This is the default. @@ -671,6 +694,26 @@ exclude database PATTERN + + + + + Use the provided string as the psql + \restrict key in the dump output. If no restrict + key is specified, pg_dumpall will generate a + random one as needed. Keys may contain only alphanumeric characters. + + + This option is primarily intended for testing purposes and other + scenarios that require repeatable output (e.g., comparing dump files). + It is not recommended for general use, as a malicious server with + advance knowledge of the key may be able to inject arbitrary code that + will be executed on the machine that runs + psql with the dump output. + + + + @@ -685,12 +728,22 @@ exclude database PATTERN + + + + + Dump optimizer statistics. + + + + Dump only the statistics, not the schema (data definitions) or data. - Statistics for tables, materialized views, and indexes are dumped. + Optimizer statistics for tables, materialized views, foreign tables, + and indexes are dumped. @@ -719,33 +772,6 @@ exclude database PATTERN - - - - - Dump data. This is the default. - - - - - - - - - Dump schema (data definitions). This is the default. - - - - - - - - - Dump statistics. This is the default. - - - - @@ -957,14 +983,15 @@ exclude database PATTERN - By default, pg_dumpall will include most optimizer - statistics in the resulting dump file. However, some statistics may not be - included, such as those created explicitly with or custom statistics added by an - extension. Therefore, it may be useful to run ANALYZE - on each database after restoring from a dump file to ensure optimal - performance. You can also run vacuumdb -a -z to analyze - all databases. + When is specified, + pg_dumpall will include most optimizer statistics in the + resulting dump file. This does not include all statistics, such as + those created explicitly with , + custom statistics added by an extension, or statistics collected by the + cumulative statistics system. Therefore, it may still be useful to + run ANALYZE on each database after restoring from a dump + file to ensure optimal performance. You can also run vacuumdb -a + -z to analyze all databases. @@ -1002,13 +1029,21 @@ exclude database PATTERN Examples - To dump all databases: - + To dump all databases in plain text format (the default): $ pg_dumpall > db.out + + To dump all databases using other formats: + +$ pg_dumpall --format=directory -f db.out +$ pg_dumpall --format=custom -f db.out +$ pg_dumpall --format=tar -f db.out + + + To restore database(s) from this file, you can use: @@ -1022,6 +1057,16 @@ exclude database PATTERN the script will attempt to drop other databases immediately, and that will fail for the database you are connected to. + + + If the dump was taken in a non-plain-text format, use + pg_restore to restore the databases: + +$ pg_restore db.out -d postgres -C + + This will restore all databases. To restore only some databases, use + the option to skip those not wanted. + diff --git a/doc/src/sgml/ref/pg_recvlogical.sgml b/doc/src/sgml/ref/pg_recvlogical.sgml index 63a45c7018a45..5380d776bafb1 100644 --- a/doc/src/sgml/ref/pg_recvlogical.sgml +++ b/doc/src/sgml/ref/pg_recvlogical.sgml @@ -53,6 +53,16 @@ PostgreSQL documentation (ControlC) or SIGTERM signal. + + + When pg_recvlogical receives + a SIGHUP signal, it closes the current output file + and opens a new one using the filename specified by + the option. This allows us to rotate + the output file by first renaming the current file and then sending + a SIGHUP signal to + pg_recvlogical. + @@ -74,13 +84,13 @@ PostgreSQL documentation - The and are required + The and options are required for this action. - The and options - can be specified with . + The and + options can be specified with . @@ -94,7 +104,7 @@ PostgreSQL documentation - The is required for this action. + The option is required for this action. @@ -111,8 +121,8 @@ PostgreSQL documentation - The and , - are required for this action. + The , , and + options are required for this action. @@ -166,7 +176,7 @@ PostgreSQL documentation - + Enables the slot to be synchronized to the standbys. This option may @@ -196,7 +206,7 @@ PostgreSQL documentation Specifies how often pg_recvlogical should issue fsync() calls to ensure the output file is - safely flushed to disk. + safely flushed to disk. The default value is 10 seconds. @@ -265,8 +275,10 @@ PostgreSQL documentation When creating a slot, use the specified logical decoding output - plugin. See . This option has no - effect if the slot already exists. + plugin. See for + information about the plugins PostgreSQL + provides. The default is . + This option has no effect if the slot already exists. @@ -300,7 +312,8 @@ PostgreSQL documentation - + + (deprecated) Enables decoding of prepared transactions. This option may only be specified with diff --git a/doc/src/sgml/ref/pg_resetwal.sgml b/doc/src/sgml/ref/pg_resetwal.sgml index 2c019c2aac6eb..41f2b1d480c51 100644 --- a/doc/src/sgml/ref/pg_resetwal.sgml +++ b/doc/src/sgml/ref/pg_resetwal.sgml @@ -267,14 +267,17 @@ PostgreSQL documentation A safe value for the next multitransaction ID (first part) can be determined by looking for the numerically largest file name in the directory pg_multixact/offsets under the data directory, - adding one, and then multiplying by 65536 (0x10000). Conversely, a safe + adding one, and then multiplying by 32768 (0x8000). Conversely, a safe value for the oldest multitransaction ID (second part of ) can be determined by looking for the numerically smallest - file name in the same directory and multiplying by 65536. The file - names are in hexadecimal, so the easiest way to do this is to specify - the option value in hexadecimal and append four zeroes. + file name in the same directory and multiplying by 32768 (0x8000). + Note that the file names are in hexadecimal. It is usually easiest + to specify the option value in hexadecimal too. For example, if + 000F and 0007 are the greatest and + smallest entries in pg_multixact/offsets, + -m 0x80000,0x38000 will work. - + diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml index 2295df62d03a8..6ae2cdcfc10cd 100644 --- a/doc/src/sgml/ref/pg_restore.sgml +++ b/doc/src/sgml/ref/pg_restore.sgml @@ -18,8 +18,8 @@ PostgreSQL documentation pg_restore - restore a PostgreSQL database or cluster - from an archive created by pg_dump or + restore PostgreSQL databases from archives + created by pg_dump or pg_dumpall @@ -69,6 +69,19 @@ PostgreSQL documentation pg_dump options. + + A non-plain-text archive made using pg_dumpall + is a directory containing a toc.glo file with global + objects (roles and tablespaces), a map.dat file + listing the databases, and a subdirectory for each database containing + its archive. When restoring such an archive, + pg_restore first restores global objects from + toc.glo, then processes each database listed in + map.dat. Lines in map.dat can + be commented out with # to skip restoring specific + databases. + + Obviously, pg_restore cannot restore information that is not present in the archive file. For instance, if the @@ -77,6 +90,18 @@ PostgreSQL documentation pg_restore will not be able to load the data using COPY statements. + + + + Restoring a dump causes the destination to execute arbitrary code of the + source superusers' choice. Partial dumps and partial restores do not limit + that. If the source superusers are not trusted, the dumped SQL statements + must be inspected before restoring. Non-plain-text dumps can be inspected + by using pg_restore's + option. Note that the client running the dump and restore need not trust + the source or destination superusers. + + @@ -127,6 +152,12 @@ PostgreSQL documentation ignorable error messages will be reported, unless is also specified. + + When restoring a pg_dumpall archive, + is implied by , + since global objects such as roles and tablespaces may not exist + in the target cluster. + @@ -150,7 +181,7 @@ PostgreSQL documentation Access privileges for the database itself are also restored, unless is specified. is required when restoring multiple databases - from an archive created by pg_dumpall. + from a non-plain-text archive made using pg_dumpall. @@ -159,6 +190,11 @@ PostgreSQL documentation CREATE DATABASE commands. All data is restored into the database name that appears in the archive. + + + This option cannot be used together + with . + @@ -254,7 +290,32 @@ PostgreSQL documentation Restore only global objects (roles and tablespaces), no databases. - This option is only relevant when restoring from an archive made using pg_dumpall. + This option is only relevant when restoring from a non-plain-text archive made using pg_dumpall. + Note: cannot be used with + , + , + , + , + , + , + , or + , + . + + + + + + + + + Do not restore global objects (roles and tablespaces). When + / is not specified, + databases that do not already exist on the target server are skipped. + + + This option is only relevant when restoring from a non-plain-text + archive made using pg_dumpall. @@ -271,14 +332,14 @@ PostgreSQL documentation - - + + Run the most time-consuming steps of pg_restore — those that load data, create indexes, or create constraints — concurrently, using up - to number-of-jobs + to njobs concurrent sessions. This option can dramatically reduce the time to restore a large database to a server running on a multiprocessor machine. This option is ignored when emitting a script @@ -464,16 +525,6 @@ PostgreSQL documentation specify table(s) in a particular schema. - - - When is specified, pg_restore - makes no attempt to restore any other database objects that the - selected table(s) might depend upon. Therefore, there is no - guarantee that a specific-table restore into a clean database will - succeed. - - - This flag does not behave identically to the @@ -554,6 +605,8 @@ PostgreSQL documentation ensures that either all the commands complete successfully, or no changes are applied. This option implies . + It cannot be used together with , nor with + multiple jobs (). @@ -620,7 +673,7 @@ PostgreSQL documentation quote the pattern if needed to prevent shell wildcard expansion. - This option is only relevant when restoring from an archive made using pg_dumpall. + This option is only relevant when restoring from a non-plain-text archive made using pg_dumpall. @@ -713,7 +766,9 @@ PostgreSQL documentation in mode. This suppresses does not exist errors that might otherwise be reported. This option is not valid unless is also - specified. + specified. This option is implied when restoring a + pg_dumpall archive with + . @@ -842,6 +897,28 @@ PostgreSQL documentation + + + + + Use the provided string as the psql + \restrict key in the dump output. This can only be + specified for SQL script output, i.e., when the + option is used. If no restrict key is specified, + pg_restore will generate a random one as + needed. Keys may contain only alphanumeric characters. + + + This option is primarily intended for testing purposes and other + scenarios that require repeatable output (e.g., comparing dump files). + It is not recommended for general use, as a malicious server with + advance knowledge of the key may be able to inject arbitrary code that + will be executed on the machine that runs + psql with the dump output. + + + + @@ -861,6 +938,16 @@ PostgreSQL documentation + + + + + Output commands to restore statistics, if the archive contains them. + This is the default. + + + + @@ -919,33 +1006,6 @@ PostgreSQL documentation - - - - - Dump data. This is the default. - - - - - - - - - Dump schema (data definitions). This is the default. - - - - - - - - - Dump statistics. This is the default. - - - - @@ -1118,6 +1178,16 @@ PostgreSQL documentation Notes + + When options or are specified, + pg_restore makes no attempt to restore + any other database objects that the selected table(s) or schema(s) + might depend upon. Therefore, there is no guarantee that a specific-table + restore into a clean database will succeed. For example, if a table + whose definition includes a foreign key is specified to be restored, the + table referenced by the foreign key is not automatically restored. + + If your installation has any local additions to the template1 database, be careful to load the output of @@ -1145,6 +1215,22 @@ CREATE DATABASE foo WITH TEMPLATE template0; + + + Parallel restore ( greater than 1) requires + applying dependency information from the archive file to ensure + that an object is not restored before other objects it depends on. + This information will be incomplete, leading to unexpected restore + failures, if the archive does not include object definitions + (the section). Therefore, avoid + using pg_dump options such + as , , + or without when + creating an archive you wish to restore in parallel. Instead, you + may provide such options to pg_restore. + + + pg_restore cannot restore large objects selectively; for instance, only those for a specific table. If @@ -1154,6 +1240,21 @@ CREATE DATABASE foo WITH TEMPLATE template0; + + + The following options cannot be used when restoring from a non-plain-text + archive made using pg_dumpall: + , + , + , + , + , and + . + Also, if the option is used, it must + include . + + + diff --git a/doc/src/sgml/ref/pg_rewind.sgml b/doc/src/sgml/ref/pg_rewind.sgml index 5485033ed8c7c..f704dc108e63f 100644 --- a/doc/src/sgml/ref/pg_rewind.sgml +++ b/doc/src/sgml/ref/pg_rewind.sgml @@ -28,9 +28,9 @@ PostgreSQL documentation - directory + datadir - + @@ -52,12 +52,32 @@ PostgreSQL documentation analogous to a base backup of the source data directory. Unlike taking a new base backup or using a tool like rsync, pg_rewind does not require comparing or copying - unchanged relation blocks in the cluster. Only changed blocks from existing - relation files are copied; all other files, including new relation files, - configuration files, and WAL segments, are copied in full. As such the - rewind operation is significantly faster than other approaches when the - database is large and only a small fraction of blocks differ between the - clusters. + unchanged relation blocks in the cluster: + + + + + Only changed blocks from existing relation files are copied. + + + + + WAL segments prior to the point where the source and target servers + have diverged are not copied. WAL segments generated after the source + and target servers have diverged are copied in full. + + + + + All other files, including new relation files and configuration files, + are copied in full. + + + + + As such, the rewind operation is significantly faster than other + approaches when the database is large and only a small fraction of blocks + differ between the clusters. @@ -97,9 +117,9 @@ PostgreSQL documentation pg_rewind requires that the target server either has the option enabled in postgresql.conf or data checksums enabled when - the cluster was initialized with initdb. Neither of these - are currently on by default. - must also be set to on, but is enabled by default. + the cluster was initialized with initdb (the + default). must also be set to + on, but is enabled by default. @@ -142,8 +162,8 @@ PostgreSQL documentation - - + + This option specifies the target data directory that is synchronized @@ -154,7 +174,7 @@ PostgreSQL documentation - + Specifies the file system path to the data directory of the source @@ -352,10 +372,10 @@ PostgreSQL documentation a role, named rewind_user here: CREATE USER rewind_user LOGIN; -GRANT EXECUTE ON function pg_catalog.pg_ls_dir(text, boolean, boolean) TO rewind_user; -GRANT EXECUTE ON function pg_catalog.pg_stat_file(text, boolean) TO rewind_user; -GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text) TO rewind_user; -GRANT EXECUTE ON function pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean) TO rewind_user; +GRANT EXECUTE ON FUNCTION pg_catalog.pg_ls_dir(text, boolean, boolean) TO rewind_user; +GRANT EXECUTE ON FUNCTION pg_catalog.pg_stat_file(text, boolean) TO rewind_user; +GRANT EXECUTE ON FUNCTION pg_catalog.pg_read_binary_file(text) TO rewind_user; +GRANT EXECUTE ON FUNCTION pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean) TO rewind_user; diff --git a/doc/src/sgml/ref/pg_verifybackup.sgml b/doc/src/sgml/ref/pg_verifybackup.sgml index 61c12975e4ad5..1695cfe91c8bd 100644 --- a/doc/src/sgml/ref/pg_verifybackup.sgml +++ b/doc/src/sgml/ref/pg_verifybackup.sgml @@ -36,10 +36,7 @@ PostgreSQL documentation backup_manifest generated by the server at the time of the backup. The backup may be stored either in the "plain" or the "tar" format; this includes tar-format backups compressed with any algorithm - supported by pg_basebackup. However, at present, - WAL verification is supported only for plain-format - backups. Therefore, if the backup is stored in tar-format, the - -n, --no-parse-wal option should be used. + supported by pg_basebackup. @@ -261,12 +258,13 @@ PostgreSQL documentation - + - Try to parse WAL files stored in the specified directory, rather than - in pg_wal. This may be useful if the backup is - stored in a separate location from the WAL archive. + Try to parse WAL files stored in the specified directory or tar + archive, rather than in pg_wal. This may be + useful if the backup is stored in a separate location from the WAL + archive. diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml index ce23add5577ed..9bbb4bd577270 100644 --- a/doc/src/sgml/ref/pg_waldump.sgml +++ b/doc/src/sgml/ref/pg_waldump.sgml @@ -22,8 +22,8 @@ PostgreSQL documentation pg_waldump - - + option + startsegendseg @@ -141,13 +141,21 @@ PostgreSQL documentation - Specifies a directory to search for WAL segment files or a - directory with a pg_wal subdirectory that + Specifies a tar archive or a directory to search for WAL segment files + or a directory with a pg_wal subdirectory that contains such files. The default is to search in the current directory, the pg_wal subdirectory of the current directory, and the pg_wal subdirectory of PGDATA. + + If a tar archive is provided and its WAL segment files are not in + sequential order, those files will be written to a temporary directory + named starting with waldump_tmp. This directory will be + created inside the directory specified by the TMPDIR + environment variable if it is set; otherwise, it will be created within + the same directory as the tar archive. + @@ -383,6 +391,17 @@ PostgreSQL documentation + + + TMPDIR + + + Directory in which to create temporary files when reading WAL from a + tar archive with out-of-order segment files. If not set, the temporary + directory is created within the same directory as the tar archive. + + + diff --git a/doc/src/sgml/ref/pg_walsummary.sgml b/doc/src/sgml/ref/pg_walsummary.sgml index 57b2d24650cf1..2f61df7173c25 100644 --- a/doc/src/sgml/ref/pg_walsummary.sgml +++ b/doc/src/sgml/ref/pg_walsummary.sgml @@ -23,7 +23,7 @@ PostgreSQL documentation pg_walsummary option - file + filename @@ -31,7 +31,7 @@ PostgreSQL documentation Description pg_walsummary is used to print the contents of - WAL summary files. These binary files are found with the + WAL summary files. These binary files are found in the pg_wal/summaries subdirectory of the data directory, and can be converted to text using this tool. This is not ordinarily necessary, since WAL summary files primarily exist to support @@ -56,6 +56,16 @@ PostgreSQL documentation + + filename + + + Specifies the name of a WAL summary file, found in the + pg_wal/summaries subdirectory of the data directory. + + + + diff --git a/doc/src/sgml/ref/pgarchivecleanup.sgml b/doc/src/sgml/ref/pgarchivecleanup.sgml index cd8f49b1c5bd7..79e751381ac41 100644 --- a/doc/src/sgml/ref/pgarchivecleanup.sgml +++ b/doc/src/sgml/ref/pgarchivecleanup.sgml @@ -44,7 +44,7 @@ PostgreSQL documentation server to use pg_archivecleanup, put this into its postgresql.conf configuration file: -archive_cleanup_command = 'pg_archivecleanup archivelocation %r' +archive_cleanup_command = 'pg_archivecleanup archivelocation "%r"' where archivelocation is the directory from which WAL segment files should be removed. @@ -198,7 +198,7 @@ pg_archivecleanup: removing file "archive/00000001000000370000000E" On Linux or Unix systems, you might use: -archive_cleanup_command = 'pg_archivecleanup -d /mnt/standby/archive %r 2>>cleanup.log' +archive_cleanup_command = 'pg_archivecleanup -d /mnt/standby/archive "%r" 2>>cleanup.log' where the archive directory is physically located on the standby server, so that the archive_command is accessing it across NFS, diff --git a/doc/src/sgml/ref/pgbench.sgml b/doc/src/sgml/ref/pgbench.sgml index ab252d9fc74f9..2e401d1ceb8bc 100644 --- a/doc/src/sgml/ref/pgbench.sgml +++ b/doc/src/sgml/ref/pgbench.sgml @@ -76,9 +76,8 @@ tps = 896.967014 (without initial connection time) and number of transactions per client); these will be equal unless the run failed before completion or some SQL command(s) failed. (In mode, only the actual number of transactions is printed.) - The next line reports the number of failed transactions due to - serialization or deadlock errors (see - for more information). + The next line reports the number of failed transactions (see + for more information). The last line reports the number of transactions per second. @@ -759,6 +758,26 @@ pgbench options d + + + + + Allows clients to continue running even if an SQL statement fails + due to errors other than serialization or deadlock. By default, + clients abort after such errors, but with this option enabled, + they proceed to the next transaction instead. Note that + clients still abort even with this option if an error causes + the connection to fail. + See for more information. + + + This option is useful when your custom script may raise errors + such as unique constraint violations, but you want the benchmark + to continue and measure performance including those failures. + + + + @@ -790,6 +809,9 @@ pgbench options d deadlock failures; + + other failures; + See for more information. @@ -1188,10 +1210,8 @@ pgbench options d - - \gset [prefix] - \aset [prefix] - + \gset [prefix] + \aset [prefix] @@ -1203,16 +1223,17 @@ pgbench options d When the \gset command is used, the preceding SQL query is expected to return one row, the columns of which are stored into variables named after column names, and prefixed with prefix - if provided. + if provided. If the query returns zero or multiple rows, an error is raised. When the \aset command is used, all combined SQL queries (separated by \;) have their columns stored into variables named after column names, and prefixed with prefix - if provided. If a query returns no row, no assignment is made and the variable - can be tested for existence to detect this. If a query returns more than one - row, the last value is kept. + if provided. If a query returns no rows, no assignment is made. + This can be detected by initializing the variable beforehand with + a value the query cannot return, then checking whether it changes. + If a query returns more than one row, the last value is kept. @@ -2409,8 +2430,8 @@ END; will be reported as failed. If you use the option, the time of the failed transaction will be reported as - serialization or - deadlock depending on the type of failure (see + serialization, deadlock, or + other depending on the type of failure (see for more information). @@ -2638,6 +2659,17 @@ END; + + + other_sql_failures + + + number of transactions that got an SQL error + (zero unless both and + are specified) + + + @@ -2646,8 +2678,8 @@ END; pgbench --aggregate-interval=10 --time=20 --client=10 --log --rate=1000 --latency-limit=10 --failures-detailed --max-tries=10 test -1650260552 5178 26171317 177284491527 1136 44462 2647617 7321113867 0 9866 64 7564 28340 4148 0 -1650260562 4808 25573984 220121792172 1171 62083 3037380 9666800914 0 9998 598 7392 26621 4527 0 +1650260552 5178 26171317 177284491527 1136 44462 2647617 7321113867 0 9866 64 7564 28340 4148 0 0 +1650260562 4808 25573984 220121792172 1171 62083 3037380 9666800914 0 9998 598 7392 26621 4527 0 0 @@ -2826,7 +2858,7 @@ statement latencies in milliseconds, failures and retries: start a connection to the database server / the socket for connecting the client to the database server has become invalid). In such cases all clients of this thread stop while other threads continue to work. - However, is specified, all of the + However, if is specified, all of the threads stop immediately in this case. @@ -2851,10 +2883,20 @@ statement latencies in milliseconds, failures and retries: A client's run is aborted in case of a serious error; for example, the connection with the database server was lost or the end of script was reached - without completing the last transaction. In addition, if execution of an SQL - or meta command fails for reasons other than serialization or deadlock errors, - the client is aborted. Otherwise, if an SQL command fails with serialization or - deadlock errors, the client is not aborted. In such cases, the current + without completing the last transaction. The client also aborts + if a meta command fails, or if an SQL command fails for reasons other than + serialization or deadlock errors when + is not specified. With , + the client does not abort on such SQL errors and instead proceeds to + the next transaction. These cases are reported as + other failures in the output. If the error occurs + in a meta command, however, the client still aborts even when this option + is specified. + + + If an SQL command fails due to serialization or deadlock errors, the + client does not abort, regardless of whether + is used. Instead, the current transaction is rolled back, which also includes setting the client variables as they were before the run of this transaction (it is assumed that one transaction script contains only one transaction; see diff --git a/doc/src/sgml/ref/pgtesttiming.sgml b/doc/src/sgml/ref/pgtesttiming.sgml index a5eb3aa25e02f..75fcc58d79957 100644 --- a/doc/src/sgml/ref/pgtesttiming.sgml +++ b/doc/src/sgml/ref/pgtesttiming.sgml @@ -30,11 +30,27 @@ PostgreSQL documentation Description - pg_test_timing is a tool to measure the timing overhead - on your system and confirm that the system time never moves backwards. + pg_test_timing is a tool to measure the + timing overhead on your system and confirm that the system time never + moves backwards. It reads supported clock sources over and over again + as fast as it can for a specified length of time, and then prints + statistics about the observed differences in successive clock readings, + as well as which clock source will be used. + + + Smaller (but not zero) differences are better, since they imply both + more-precise clock hardware and less overhead to collect a clock reading. Systems that are slow to collect timing data can give less accurate EXPLAIN ANALYZE results. + + This tool is also helpful to determine if + the track_io_timing configuration parameter is likely + to produce useful results, and whether the + TSC clock source (see + ) is available and if it will be + used by default. + @@ -59,6 +75,21 @@ PostgreSQL documentation + + + + + + Specifies the cutoff percentage for the list of exact observed + timing durations (that is, the changes in the system clock value + from one reading to the next). The list will end once the running + percentage total reaches or exceeds this value, except that the + largest observed duration will always be printed. The default + cutoff is 99.99. + + + + @@ -92,205 +123,173 @@ PostgreSQL documentation Interpreting Results - Good results will show most (>90%) individual timing calls take less than - one microsecond. Average per loop overhead will be even lower, below 100 - nanoseconds. This example from an Intel i7-860 system using a TSC clock - source shows excellent performance: - - + The first block of output has four columns, with rows showing a + shifted-by-one log2(ns) histogram of timing durations (that is, the + differences between successive clock readings). This is not the + classic log2(n+1) histogram as it counts zeros separately and then + switches to log2(ns) starting from value 1. - - Note that different units are used for the per loop time than the - histogram. The loop can have resolution within a few nanoseconds (ns), - while the individual timing calls can only resolve down to one microsecond - (us). + The columns are: + + + nanosecond value that is >= the durations in this + bucket + + + percentage of durations in this bucket + + + running-sum percentage of durations in this and previous + buckets + + + count of durations in this bucket + + - - - - Measuring Executor Timing Overhead - - - When the query executor is running a statement using - EXPLAIN ANALYZE, individual operations are timed as well - as showing a summary. The overhead of your system can be checked by - counting rows with the psql program: - - -CREATE TABLE t AS SELECT * FROM generate_series(1,100000); -\timing -SELECT COUNT(*) FROM t; -EXPLAIN ANALYZE SELECT COUNT(*) FROM t; - - - - The i7-860 system measured runs the count query in 9.8 ms while - the EXPLAIN ANALYZE version takes 16.6 ms, each - processing just over 100,000 rows. That 6.8 ms difference means the timing - overhead per row is 68 ns, about twice what pg_test_timing estimated it - would be. Even that relatively small amount of overhead is making the fully - timed count statement take almost 70% longer. On more substantial queries, - the timing overhead would be less problematic. + The second block of output goes into more detail, showing the exact + timing differences observed. For brevity this list is cut off when the + running-sum percentage exceeds the user-selectable cutoff value. + However, the largest observed difference is always shown. - - - - - Changing Time Sources - On some newer Linux systems, it's possible to change the clock source used - to collect timing data at any time. A second example shows the slowdown - possible from switching to the slower acpi_pm time source, on the same - system used for the fast results above: - - /sys/devices/system/clocksource/clocksource0/current_clocksource -# pg_test_timing -Per loop time including overhead: 722.92 ns -Histogram of timing durations: - < us % of total count - 1 27.84870 1155682 - 2 72.05956 2990371 - 4 0.07810 3241 - 8 0.01357 563 - 16 0.00007 3 -]]> + On platforms that support the TSC clock source, + additional output sections are shown for the RDTSCP + instruction (used for general timing needs, such as + track_io_timing) and the RDTSC + instruction (used for EXPLAIN ANALYZE). At the end + of the output, the TSC frequency, which may either be + sourced from CPU information directly, or the alternate calibration + mechanism are shown, as well as whether the TSC clock + source will be used by default. - In this configuration, the sample EXPLAIN ANALYZE above - takes 115.9 ms. That's 1061 ns of timing overhead, again a small multiple - of what's measured directly by this utility. That much timing overhead - means the actual query itself is only taking a tiny fraction of the - accounted for time, most of it is being consumed in overhead instead. In - this configuration, any EXPLAIN ANALYZE totals involving - many timed operations would be inflated significantly by timing overhead. + The example results below show system clock timing where 99.99% of loops + took between 16 and 63 nanoseconds. In the second block, we can see that + the typical loop time is 40 nanoseconds, and the readings appear to have + full nanosecond precision. Following the system clock results, the + TSC clock source results are shown, in the same fashion. + The RDTSCP instruction shows most loops completing in + 20–30 nanoseconds, while the RDTSC instruction is + the fastest with an average loop time of 20 nanoseconds. In this example + the TSC clock source will be used by default, but can be + disabled by setting timing_clock_source to + system. - FreeBSD also allows changing the time source on the fly, and it logs - information about the timer selected during boot: - - -# dmesg | grep "Timecounter" -Timecounter "ACPI-fast" frequency 3579545 Hz quality 900 -Timecounter "i8254" frequency 1193182 Hz quality 0 -Timecounters tick every 10.000 msec -Timecounter "TSC" frequency 2531787134 Hz quality 800 -# sysctl kern.timecounter.hardware=TSC -kern.timecounter.hardware: ACPI-fast -> TSC - - - - - Other systems may only allow setting the time source on boot. On older - Linux systems the "clock" kernel setting is the only way to make this sort - of change. And even on some more recent ones, the only option you'll see - for a clock source is "jiffies". Jiffies are the older Linux software clock - implementation, which can have good resolution when it's backed by fast - enough timing hardware, as in this example: - - - - - - Clock Hardware and Timing Accuracy - - - Collecting accurate timing information is normally done on computers using - hardware clocks with various levels of accuracy. With some hardware the - operating systems can pass the system clock time almost directly to - programs. A system clock can also be derived from a chip that simply - provides timing interrupts, periodic ticks at some known time interval. In - either case, operating system kernels provide a clock source that hides - these details. But the accuracy of that clock source and how quickly it can - return results varies based on the underlying hardware. - - - - Inaccurate time keeping can result in system instability. Test any change - to the clock source very carefully. Operating system defaults are sometimes - made to favor reliability over best accuracy. And if you are using a virtual - machine, look into the recommended time sources compatible with it. Virtual - hardware faces additional difficulties when emulating timers, and there are - often per operating system settings suggested by vendors. - - - - The Time Stamp Counter (TSC) clock source is the most accurate one available - on current generation CPUs. It's the preferred way to track the system time - when it's supported by the operating system and the TSC clock is - reliable. There are several ways that TSC can fail to provide an accurate - timing source, making it unreliable. Older systems can have a TSC clock that - varies based on the CPU temperature, making it unusable for timing. Trying - to use TSC on some older multicore CPUs can give a reported time that's - inconsistent among multiple cores. This can result in the time going - backwards, a problem this program checks for. And even the newest systems - can fail to provide accurate TSC timing with very aggressive power saving - configurations. - - - - Newer operating systems may check for the known TSC problems and switch to a - slower, more stable clock source when they are seen. If your system - supports TSC time but doesn't default to that, it may be disabled for a good - reason. And some operating systems may not detect all the possible problems - correctly, or will allow using TSC even in situations where it's known to be - inaccurate. - - - - The High Precision Event Timer (HPET) is the preferred timer on systems - where it's available and TSC is not accurate. The timer chip itself is - programmable to allow up to 100 nanosecond resolution, but you may not see - that much accuracy in your system clock. - - - - Advanced Configuration and Power Interface (ACPI) provides a Power - Management (PM) Timer, which Linux refers to as the acpi_pm. The clock - derived from acpi_pm will at best provide 300 nanosecond resolution. + <= ns % of total running % count + 0 0.0000 0.0000 0 + 1 0.0000 0.0000 0 + 3 0.0000 0.0000 0 + 7 0.0000 0.0000 0 + 15 0.0000 0.0000 0 + 31 24.0606 24.0606 5385707 + 63 75.8342 99.8948 16974658 + 127 0.0900 99.9848 20143 + 255 0.0069 99.9917 1542 + 511 0.0014 99.9932 322 + 1023 0.0003 99.9935 68 + 2047 0.0001 99.9936 23 + 4095 0.0036 99.9972 813 + 8191 0.0018 99.9990 402 + 16383 0.0005 99.9995 120 + 32767 0.0001 99.9997 32 + 65535 0.0001 99.9998 24 + +Observed timing durations up to 99.9900%: + ns % of total running % count + 29 3.6921 3.6921 826442 + 30 16.6755 20.3676 3732628 + 31 3.6930 24.0606 826637 + 40 75.7761 99.8368 16961658 + 41 0.0019 99.8387 431 +... + 190 0.0003 99.9901 65 +... +29657159 0.0000 100.0000 1 + +Clock source: RDTSCP +Average loop time including overhead: 37.32 ns +Histogram of timing durations: + <= ns % of total running % count + 0 0.0000 0.0000 0 + 1 0.0000 0.0000 0 + 3 0.0000 0.0000 0 + 7 0.0000 0.0000 0 + 15 0.0000 0.0000 0 + 31 99.9499 99.9499 26782299 + 63 0.0381 99.9880 10220 + 127 0.0008 99.9889 224 + 255 0.0052 99.9941 1403 + 511 0.0013 99.9954 340 + 1023 0.0001 99.9954 17 + 2047 0.0000 99.9955 7 + 4095 0.0021 99.9976 569 + 8191 0.0013 99.9989 357 + 16383 0.0005 99.9994 128 + 32767 0.0003 99.9996 70 + 65535 0.0001 99.9997 19 + +Observed timing durations up to 99.9900%: + ns % of total running % count + 20 16.9064 16.9064 4530201 + 29 41.5214 58.4279 11125972 + 30 41.5220 99.9499 11126126 + 40 0.0089 99.9587 2374 +... + 130 0.0007 99.9902 181 +... +18501572 0.0000 100.0000 1 + +Fast clock source: RDTSC +Average loop time including overhead: 27.12 ns +Histogram of timing durations: + <= ns % of total running % count + 0 0.0000 0.0000 0 + 1 0.0000 0.0000 0 + 3 0.0000 0.0000 0 + 7 0.0000 0.0000 0 + 15 1.2247 1.2247 456231 + 31 98.7566 99.9813 36789785 + 63 0.0109 99.9921 4049 + 127 0.0029 99.9951 1087 + 255 0.0008 99.9959 305 + 511 0.0007 99.9966 279 + 1023 0.0000 99.9966 7 + 2047 0.0001 99.9967 22 + 4095 0.0018 99.9985 673 + 8191 0.0010 99.9995 383 + 16383 0.0003 99.9998 94 + 32767 0.0001 99.9999 38 + 65535 0.0000 99.9999 9 + +Observed timing durations up to 99.9900%: + ns % of total running % count + 9 0.6316 0.6316 235290 + 10 0.5931 1.2247 220941 + 20 91.4328 92.6574 34061442 + 29 3.6427 96.3001 1357007 + 30 3.6811 99.9813 1371336 + 40 0.0089 99.9902 3325 +... +61594291 0.0000 100.0000 1 + +TSC frequency in use: 2449228 kHz +TSC frequency from calibration: 2448603 kHz +TSC clock source will be used by default, unless timing_clock_source is set to 'system'. +]]> - - Timers used on older PC hardware include the 8254 Programmable Interval - Timer (PIT), the real-time clock (RTC), the Advanced Programmable Interrupt - Controller (APIC) timer, and the Cyclone timer. These timers aim for - millisecond resolution. - - + @@ -298,6 +297,9 @@ Histogram of timing durations: + + Wiki + discussion about timing diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml index aeeed297437e6..38ca09b423c32 100644 --- a/doc/src/sgml/ref/pgupgrade.sgml +++ b/doc/src/sgml/ref/pgupgrade.sgml @@ -70,6 +70,14 @@ PostgreSQL documentation pg_upgrade supports upgrades from 9.2.X and later to the current major release of PostgreSQL, including snapshot and beta releases. + + + + Upgrading a cluster causes the destination to execute arbitrary code of the + source superusers' choice. Ensure that the source superusers are trusted + before upgrading. + + @@ -825,10 +833,10 @@ psql --username=postgres --file=script.sql postgres Unless the option is specified, pg_upgrade will transfer most optimizer statistics - from the old cluster to the new cluster. However, some statistics may - not be transferred, such as those created explicitly with or custom statistics added by an - extension. + from the old cluster to the new cluster. This does not transfer + all statistics, such as those created explicitly with + , custom statistics added by + an extension, or statistics collected by the cumulative statistics system. @@ -1110,7 +1118,8 @@ psql --username=postgres --file=script.sql postgres regproc regprocedure - (regclass, regrole, and regtype can be upgraded.) + (regclass, regdatabase, regrole, and + regtype can be upgraded.) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 8f7d8758ca02f..7c05afd471930 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1067,8 +1067,8 @@ INSERT INTO tbls1 VALUES ($1, $2) \parse stmt1 - - \close prepared_statement_name + + \close_prepared prepared_statement_name @@ -1081,7 +1081,7 @@ INSERT INTO tbls1 VALUES ($1, $2) \parse stmt1 Example: SELECT $1 \parse stmt1 -\close stmt1 +\close_prepared stmt1 @@ -1101,7 +1101,16 @@ SELECT $1 \parse stmt1 Outputs information about the current database connection, - including TLS-related information if TLS is in use. + including SSL-related information if SSL is in use. + + + Note that the Client User field shows + the user at the time of connection, while the + Superuser field indicates whether + the current user (in the current execution context) has + superuser privileges. These users are usually the same, but they can + differ, for example, if the current user was changed with the + SET ROLE command. @@ -1284,15 +1293,15 @@ SELECT $1 \parse stmt1 - For each relation (table, view, materialized view, index, sequence, + For each relation (table, view, materialized view, index, property graph, sequence, or foreign table) or composite type matching the pattern, show all columns, their types, the tablespace (if not the default) and any special attributes such as NOT NULL or defaults. - Associated indexes, constraints, rules, and triggers are - also shown. For foreign tables, the associated foreign - server is shown as well. + Associated objects, such as indexes, constraints, rules, triggers, + and publications, are also shown. For foreign tables, + the associated foreign server is shown as well. (Matching the pattern is defined in below.) @@ -1305,8 +1314,8 @@ SELECT $1 \parse stmt1 The command form \d+ is identical, except that - more information is displayed: any comments associated with the - columns of the table are shown, as is the presence of OIDs in the + more information is displayed: for example, any comments associated + with the columns of the table, the presence of OIDs in the table, the view definition if the relation is a view, a non-default replica identity setting and the @@ -1324,9 +1333,9 @@ SELECT $1 \parse stmt1 If \d is used without a pattern argument, it is - equivalent to \dtvmsE which will show a list of - all visible tables, views, materialized views, sequences and - foreign tables. + equivalent to \dtvmsEG which will show a list of + all visible tables, views, materialized views, sequences, + foreign tables, and property graphs. This is purely a convenience measure. @@ -1634,6 +1643,7 @@ SELECT $1 \parse stmt1 \dE[Sx+] [ pattern ] + \dG[Sx+] [ pattern ] \di[Sx+] [ pattern ] \dm[Sx+] [ pattern ] \ds[Sx+] [ pattern ] @@ -1642,10 +1652,10 @@ SELECT $1 \parse stmt1 - In this group of commands, the letters E, + In this group of commands, the letters E, G, i, m, s, t, and v - stand for foreign table, index, materialized view, + stand for foreign table, index, property graph, materialized view, sequence, table, and view, respectively. You can specify any or all of @@ -2094,8 +2104,9 @@ SELECT $1 \parse stmt1 listed. If x is appended to the command name, the results are displayed in expanded mode. - If + is appended to the command name, the tables and - schemas associated with each publication are shown as well. + If + is appended to the command name, the tables, + excluded tables, and schemas associated with each publication are shown + as well. @@ -2173,7 +2184,7 @@ SELECT $1 \parse stmt1 - \dX[x] [ pattern ] + \dX[x+] [ pattern ] Lists extended statistics. @@ -2182,6 +2193,8 @@ SELECT $1 \parse stmt1 pattern are listed. If x is appended to the command name, the results are displayed in expanded mode. + If + is appended to the command name, each object + is listed with its associated description. @@ -2522,7 +2535,7 @@ Tue Oct 26 21:40:57 CEST 1999 statement to be executed. For example, to create an index on each column of my_table: -=> SELECT format('create index on my_table(%I)', attname) +=> SELECT format('CREATE INDEX ON my_table (%I)', attname) -> FROM pg_attribute -> WHERE attrelid = 'my_table'::regclass AND attnum > 0 -> ORDER BY attnum @@ -2757,8 +2770,8 @@ hello 10 -- check for the existence of two separate records in the database and store -- the results in separate psql variables SELECT - EXISTS(SELECT 1 FROM customer WHERE customer_id = 123) as is_customer, - EXISTS(SELECT 1 FROM employee WHERE employee_id = 456) as is_employee + EXISTS (SELECT 1 FROM customer WHERE customer_id = 123) AS is_customer, + EXISTS (SELECT 1 FROM employee WHERE employee_id = 456) AS is_employee \gset \if :is_customer SELECT * FROM customer WHERE customer_id = 123; @@ -3090,6 +3103,26 @@ SELECT $1 \parse stmt1 + + display_false + + + Sets the string to be printed in place of a false value. + The default is to print f. + + + + + + display_true + + + Sets the string to be printed in place of a true value. + The default is to print t. + + + + expanded (or x) @@ -3542,6 +3575,24 @@ SELECT $1 \parse stmt1 + + \restrict restrict_key + + + Enter "restricted" mode with the provided key. In this mode, the only + allowed meta-command is \unrestrict, to exit + restricted mode. The key may contain only alphanumeric characters. + + + This command is primarily intended for use in plain-text dumps + generated by pg_dump, + pg_dumpall, and + pg_restore, but it may be useful elsewhere. + + + + + \s [ filename ] @@ -3701,7 +3752,7 @@ testdb=> \setenv LESS -imx4F All queries executed while a pipeline is ongoing use the extended query protocol. Queries are appended to the pipeline when ending with a semicolon. The meta-commands \bind, - \bind_named, \close or + \bind_named, \close_prepared or \parse can be used in an ongoing pipeline. While a pipeline is ongoing, \sendpipeline will append the current query buffer to the pipeline. Other meta-commands like @@ -3733,6 +3784,10 @@ testdb=> \setenv LESS -imx4F See for more details + + COPY is not supported while in pipeline mode. + + Example: @@ -3789,6 +3844,24 @@ SELECT 1 \bind \sendpipeline + + \unrestrict restrict_key + + + Exit "restricted" mode (i.e., where all other meta-commands are + blocked), provided the specified key matches the one given to + \restrict when restricted mode was entered. + + + This command is primarily intended for use in plain-text dumps + generated by pg_dump, + pg_dumpall, and + pg_restore, but it may be useful elsewhere. + + + + + \unset name @@ -3853,7 +3926,7 @@ SELECT 1 \bind \sendpipeline (if given) is reached, or the query no longer returns the minimum number of rows. Wait the specified number of seconds (default 2) between executions. The default wait can be changed with the variable - ). + . For backwards compatibility, seconds can be specified with or without an interval= prefix. @@ -3954,7 +4027,7 @@ SELECT 1 \bind \sendpipeline server as soon as it reaches the command-ending semicolon, even if more input remains on the current line. Thus for example entering -select 1; select 2; select 3; +SELECT 1; SELECT 2; SELECT 3; will result in the three SQL commands being individually sent to the server, with each one's results being displayed before @@ -3963,7 +4036,7 @@ select 1; select 2; select 3; command before it and the one after are effectively combined and sent to the server in one request. So for example -select 1\; select 2\; select 3; +SELECT 1\; SELECT 2\; SELECT 3; results in sending the three SQL commands to the server in a single request, when the non-backslashed semicolon is reached. @@ -4058,7 +4131,7 @@ select 1\; select 2\; select 3; Advanced users can use regular-expression notations such as character classes, for example [0-9] to match any digit. All regular expression special characters work as specified in - , except for . which + , except for . which is taken as a separator as mentioned above, * which is translated to the regular-expression notation .*, ? which is translated to ., and @@ -4610,6 +4683,15 @@ bar + + SERVICEFILE + + + The service file name, if applicable. + + + + SHELL_ERROR @@ -4752,9 +4834,10 @@ bar WATCH_INTERVAL - This variable sets the default interval which \watch - waits between executing the query. Specifying an interval in the - command overrides this variable. + This variable sets the default interval, in seconds, which + \watch waits between executing the query. The + default is 2 seconds. Specifying an interval in the command overrides + this variable. @@ -4780,7 +4863,7 @@ bar testdb=> \set foo 'my_table' testdb=> SELECT * FROM :foo; - would query the table my_table. Note that this + would query the table my_table. Note that this may be unsafe: the value of the variable is copied literally, so it can contain unbalanced quotes, or even backslash commands. You must make sure that it makes sense where you put it. @@ -4915,6 +4998,17 @@ testdb=> INSERT INTO my_table VALUES (:'content'); + + %S + + + The current value of , or + ? if connected to a server running + PostgreSQL 17 or older. + + + + %s The name of the service. @@ -4985,6 +5079,23 @@ testdb=> INSERT INTO my_table VALUES (:'content'); + + %i + + + Indicates whether the connected server is running in hot standby mode. + The value is shown as standby, if the server is + currently in hot standby and reports + as on, + and primary otherwise. This is useful when + connecting to multiple servers to quickly determine the role of + each connection. A value of ? is shown + when connected to a server running + PostgreSQL 13 or older. + + + + %x @@ -5491,7 +5602,7 @@ PSQL_EDITOR_LINENUMBER_ARG='--line ' input. Notice the changing prompt: testdb=> CREATE TABLE my_table ( -testdb(> first integer not null default 0, +testdb(> first integer NOT NULL DEFAULT 0, testdb(> second text) testdb-> ; CREATE TABLE @@ -5680,8 +5791,8 @@ testdb=> \crosstabview first second This second example shows a multiplication table with rows sorted in reverse numerical order and columns with an independent, ascending numerical order. -testdb=> SELECT t1.first as "A", t2.first+100 AS "B", t1.first*(t2.first+100) as "AxB", -testdb-> row_number() over(order by t2.first) AS ord +testdb=> SELECT t1.first AS "A", t2.first+100 AS "B", t1.first*(t2.first+100) AS "AxB", +testdb-> row_number() OVER (ORDER BY t2.first) AS ord testdb-> FROM my_table t1 CROSS JOIN my_table t2 ORDER BY 1 DESC testdb-> \crosstabview "A" "B" "AxB" ord A | 101 | 102 | 103 | 104 diff --git a/doc/src/sgml/ref/reindex.sgml b/doc/src/sgml/ref/reindex.sgml index c405539714695..185cd75ca3012 100644 --- a/doc/src/sgml/ref/reindex.sgml +++ b/doc/src/sgml/ref/reindex.sgml @@ -528,7 +528,7 @@ REINDEX INDEX my_index; - Rebuild all the indexes on the table my_table: + Rebuild all the indexes on the table my_table: REINDEX TABLE my_table; diff --git a/doc/src/sgml/ref/reindexdb.sgml b/doc/src/sgml/ref/reindexdb.sgml index abcb041179bb9..a90e48ea86ba9 100644 --- a/doc/src/sgml/ref/reindexdb.sgml +++ b/doc/src/sgml/ref/reindexdb.sgml @@ -433,7 +433,7 @@ PostgreSQL documentation - To reindex the table foo and the index + To reindex the table foo and the index bar in a database named abcd: $ reindexdb --table=foo --index=bar abcd diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml new file mode 100644 index 0000000000000..0cb72b6b28994 --- /dev/null +++ b/doc/src/sgml/ref/repack.sgml @@ -0,0 +1,452 @@ + + + + + REPACK + + + + REPACK + 7 + SQL - Language Statements + + + + REPACK + rewrite a table to reclaim disk space + + + + +REPACK [ ( option [, ...] ) ] [ table_and_columns [ USING INDEX [ index_name ] ] ] +REPACK [ ( option [, ...] ) ] USING INDEX + +where option can be one of: + + VERBOSE [ boolean ] + ANALYZE [ boolean ] + CONCURRENTLY [ boolean ] + +and table_and_columns is: + + table_name [ ( column_name [, ...] ) ] + + + + + Description + + + REPACK reclaims storage occupied by dead + tuples. Unlike VACUUM, it does so by rewriting the + entire contents of the table specified + by table_name into a new disk + file with no extra space (except for the space guaranteed by + the fillfactor storage parameter), allowing unused space + to be returned to the operating system. + + + + Without + a table_name, REPACK + processes every table and materialized view in the current database that + the current user has the MAINTAIN privilege on. This + form of REPACK cannot be executed inside a transaction + block. Also, this form is not allowed if + the CONCURRENTLY option is used. + + + + If a USING INDEX clause is specified, the rows are + physically reordered based on information from an index. Please see the + notes on clustering below. + + + + When a table is being repacked, an ACCESS EXCLUSIVE lock + is acquired on it. This prevents any other database operations (both reads + and writes) from operating on the table until the REPACK + is finished. If you want to keep the table accessible during the repacking, + consider using the CONCURRENTLY option. + + + + Notes on Clustering + + + If the USING INDEX clause is specified, the rows in + the table are stored in the order that the index specifies; + clustering, because rows are physically clustered + afterwards. + If an index name is specified in the command, the order implied by that + index is used, and that index is configured as the index to cluster on. + (This also applies to an index given to the CLUSTER + command.) + If no index name is specified, then the index that has + been configured as the index to cluster on is used; an + error is thrown if none has. + An index can be set manually using ALTER TABLE ... CLUSTER ON, + and reset with ALTER TABLE ... SET WITHOUT CLUSTER. + + + + If no table name is specified in REPACK USING INDEX, + all tables which have a clustering index defined and which the calling + user has privileges for are processed. + + + + Clustering is a one-time operation: when the table is + subsequently updated, the changes are not clustered. That is, no attempt + is made to store new or updated rows according to their index order. (If + one wishes, one can periodically recluster by issuing the command again. + Also, setting the table's fillfactor storage parameter + to less than 100% can aid in preserving cluster ordering during updates, + since updated rows are kept on the same page if enough space is available + there.) + + + + In cases where you are accessing single rows randomly within a table, the + actual order of the data in the table is unimportant. However, if you tend + to access some data more than others, and there is an index that groups + them together, you will benefit from using clustering. If + you are requesting a range of indexed values from a table, or a single + indexed value that has multiple rows that match, + clustering will help because once the index identifies the + table page for the first row that matches, all other rows that match are + probably already on the same table page, and so you save disk accesses and + speed up the query. + + + + REPACK can re-sort the table using either an index scan + on the specified index (if the index is a b-tree), or a sequential scan + followed by sorting. It will attempt to choose the method that will be + faster, based on planner cost parameters and available statistical + information. + + + + Because the planner records statistics about the ordering of tables, it is + advisable to + run ANALYZE on the + newly repacked table. Otherwise, the planner might make poor choices of + query plans. + + + + + Notes on Resources + + + When an index scan or a sequential scan without sort is used, a temporary + copy of the table is created that contains the table data in the index + order. Temporary copies of each index on the table are created as well. + Therefore, you need free space on disk at least equal to the sum of the + table size and the index sizes. + + + + When a sequential scan and sort is used, a temporary sort file is also + created, so that the peak temporary space requirement is as much as double + the table size, plus the index sizes. This method is often faster than + the index scan method, but if the disk space requirement is intolerable, + you can disable this choice by temporarily setting + to off. + + + + It is advisable to set to a + reasonably large value (but not more than the amount of RAM you can + dedicate to the REPACK operation) before repacking. + + + + + + + Parameters + + + + table_name + + + The name (possibly schema-qualified) of a table. + + + + + + column_name + + + The name of a specific column to analyze. Defaults to all columns. + If a column list is specific, ANALYZE must also + be specified. + + + + + + index_name + + + The name of an index. + + + + + + CONCURRENTLY + + + Allow other transactions to use the table while it is being repacked. + + + + Internally, REPACK copies the contents of the table + (ignoring dead tuples) into a new file, sorted by the specified index, + and also creates a new file for each index. Then it swaps the old and + new files for the table and all the indexes, and deletes the old + files. The ACCESS EXCLUSIVE lock is needed to make + sure that the old files do not change during the processing because the + changes would get lost due to the swap. + + + + With the CONCURRENTLY option, the ACCESS + EXCLUSIVE lock is only acquired to swap the table and index + files. The data changes that took place during the creation of the new + table and index files are captured using logical decoding + () and applied before + the ACCESS EXCLUSIVE lock is requested. Thus the lock + is typically held only for the time needed to swap the files, which + should be pretty short. However, the time might still be noticeable if + too many data changes have been done to the table while + REPACK was waiting for the lock: those changes must + be processed just before the files are swapped, while the + ACCESS EXCLUSIVE lock is being held. + + + + Note that REPACK with the + CONCURRENTLY option does not try to order the rows + inserted into the table after the repacking started. Also + note REPACK might fail to complete due to DDL + commands executed on the table by other transactions during the + repacking. + + + + + In addition to the temporary space requirements explained in + , + the CONCURRENTLY option can add to the usage of + temporary space a bit more. The reason is that other transactions can + perform DML operations which cannot be applied to the new file until + REPACK has copied all the existing tuples from the + old file. Thus the tuples inserted into the old file during the copying + are also stored separately in a temporary file, until they can be + processed. + + + + + The CONCURRENTLY option cannot be used in the + following cases: + + + + + The table is UNLOGGED. + + + + + + The table is partitioned. + + + + + + The table lacks a primary key and index-based replica identity. + + + + + + The table is a system catalog or a TOAST table. + + + + + + REPACK is executed inside a transaction block. + + + + + + The max_repack_replication_slots + configuration parameter does not allow for the creation of an + additional replication slot. + + + + + + + + REPACK with the CONCURRENTLY + option is not MVCC-safe, see for + details. + + + + + + + + VERBOSE + + + Prints a progress report as each table is repacked + at INFO level. + + + + + + ANALYZE + ANALYSE + + + Applies on the table after repacking. This is + currently only supported when a single (non-partitioned) table is specified. + + + + + + boolean + + + Specifies whether the selected option should be turned on or off. + You can write TRUE, ON, or + 1 to enable the option, and FALSE, + OFF, or 0 to disable it. The + boolean value can also + be omitted, in which case TRUE is assumed. + + + + + + + + Notes + + + To repack a table, one must have the MAINTAIN privilege + on the table. + + + + While REPACK is running, the is temporarily changed to pg_catalog, + pg_temp. + + + + Each backend running REPACK will report its progress + in the pg_stat_progress_repack view. See + for details. + + + + Repacking a partitioned table repacks each of its partitions. If an index + is specified, each partition is repacked using the partition of that + index. REPACK on a partitioned table cannot be executed + inside a transaction block. + + + + + + Examples + + + Repack the table employees: + +REPACK employees; + + + + + Repack the table employees on the basis of its + index employees_ind (since an index is specified, this is + effectively clustering): + +REPACK employees USING INDEX employees_ind; + + + + + Repack the employees table following the same index + as was used before, in concurrent mode: + +REPACK (CONCURRENTLY) employees USING INDEX; + + + + + Repack the table cases on physical ordering, + running an ANALYZE on the given columns once + repacking is done, showing informational messages: + +REPACK (ANALYZE, VERBOSE) cases (district, case_nr); + + + + + Repack all tables in the database on which you have + the MAINTAIN privilege: + +REPACK; + + + + + Repack all tables for which a clustering index has previously been + configured on which you have the MAINTAIN privilege, + showing informational messages: + +REPACK (VERBOSE) USING INDEX; + + + + + + + Compatibility + + + There is no REPACK statement in the SQL standard. + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 8df492281a1c5..618a204c36f65 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -104,6 +104,13 @@ REVOKE [ GRANT OPTION FOR ] [ GRANTED BY role_specification ] [ CASCADE | RESTRICT ] +REVOKE [ GRANT OPTION FOR ] + { SELECT | ALL [ PRIVILEGES ] } + ON PROPERTY GRAPH graph_name [, ...] + FROM role_specification [, ...] + [ GRANTED BY role_specification ] + [ CASCADE | RESTRICT ] + REVOKE [ GRANT OPTION FOR ] { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] } ON SCHEMA schema_name [, ...] @@ -174,6 +181,12 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] Otherwise, both the privilege and the grant option are revoked. + + If GRANTED BY is specified, only privileges granted by + the specified role are revoked. A role can only revoke grants by another + role if it inherits the privileges of that role. + + If a user holds a privilege with grant option and has granted it to other users then the privileges held by those other users are @@ -275,7 +288,7 @@ REVOKE [ { ADMIN | INHERIT | SET } OPTION FOR ] If the role executing REVOKE holds privileges indirectly via more than one role membership path, it is unspecified which containing role will be used to perform the command. In such cases - it is best practice to use SET ROLE to become the specific + it is best practice to use GRANTED BY to specify which role you want to do the REVOKE as. Failure to do so might lead to revoking privileges other than the ones you intended, or not revoking anything at all. diff --git a/doc/src/sgml/ref/security_label.sgml b/doc/src/sgml/ref/security_label.sgml index e5e5fb483e94e..c112f7a08a745 100644 --- a/doc/src/sgml/ref/security_label.sgml +++ b/doc/src/sgml/ref/security_label.sgml @@ -35,6 +35,7 @@ SECURITY LABEL [ FOR provider ] ON MATERIALIZED VIEW object_name | [ PROCEDURAL ] LANGUAGE object_name | PROCEDURE procedure_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | + PROPERTY GRAPH object_name PUBLICATION object_name | ROLE object_name | ROUTINE routine_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | @@ -84,6 +85,10 @@ SECURITY LABEL [ FOR provider ] ON based on object labels, rather than traditional discretionary access control (DAC) concepts such as users and groups. + + + You must own the database object to use SECURITY LABEL. + diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index d7089eac0bee7..09b6ce809bb16 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -37,7 +37,7 @@ SELECT [ ALL | DISTINCT [ ON ( expressionexpression [ [ AS ] output_name ] } [, ...] ] [ FROM from_item [, ...] ] [ WHERE condition ] - [ GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] ] + [ GROUP BY { ALL | [ ALL | DISTINCT ] grouping_element [, ...] } ] [ HAVING condition ] [ WINDOW window_name AS ( window_definition ) [, ...] ] [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ] @@ -59,6 +59,7 @@ SELECT [ ALL | DISTINCT [ ON ( expressionfunction_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] ) [ LATERAL ] ROWS FROM( function_name ( [ argument [, ...] ] ) [ AS ( column_definition [, ...] ) ] [, ...] ) [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ] + GRAPH_TABLE ( graph_name MATCH graph_pattern COLUMNS ( { expression [ AS name ] } [, ...] ) ) [ [ AS ] alias [ ( column_alias [, ...] ) ] ] from_itemjoin_typefrom_item { ON join_condition | USING ( join_column [, ...] ) [ AS join_using_alias ] } from_item NATURAL join_typefrom_itemfrom_item CROSS JOIN from_item @@ -587,6 +588,48 @@ TABLE [ ONLY ] table_name [ * ] + + GRAPH_TABLE ( graph_name MATCH graph_pattern COLUMNS ( { expression [ AS name ] } [, ...] ) ) + + + This clause produces output from matching the specifying graph pattern + against a property graph. See + and for more information. + + + + graph_name is the name + (optionally schema-qualified) of an existing property graph (defined + with ). + + + + graph_pattern is a graph + pattern in a special graph pattern sublanguage. See . + + + + The COLUMNS clause defines the output columns of + the GRAPH_TABLE clause. expression is a scalar expression + using the graph pattern variables defined in the graph_pattern. The name of the output + columns are specified using the AS clauses. If the + expressions are simple property references, the property names are + used as the output names, otherwise an explicit name must be + specified. + + + + Like for other FROM clause items, a table alias + name and column alias names may follow the GRAPH_TABLE + (...) clause. (A column alias list would be redundant with + the COLUMNS clause.) + + + + join_type @@ -796,7 +839,7 @@ WHERE condition The optional GROUP BY clause has the general form -GROUP BY [ ALL | DISTINCT ] grouping_element [, ...] +GROUP BY { ALL | [ ALL | DISTINCT ] grouping_element [, ...] } @@ -808,21 +851,31 @@ GROUP BY [ ALL | DISTINCT ] grouping_elementgrouping_element can be an input column name, or the name or ordinal number of an output column (SELECT list item), or an arbitrary - expression formed from input-column values. In case of ambiguity, + expression formed from input-column values; however, it cannot contain + an aggregate function or a window function. In case of ambiguity, a GROUP BY name will be interpreted as an input-column name rather than an output column name. + + The form GROUP BY ALL with no explicit + grouping_elements + provided is equivalent to writing GROUP BY with the + numbers of all SELECT output columns that do not + contain either an aggregate function or a window function. + + If any of GROUPING SETS, ROLLUP or CUBE are present as grouping elements, then the GROUP BY clause as a whole defines some number of independent grouping sets. The effect of this is equivalent to constructing a UNION ALL between - subqueries with the individual grouping sets as their + subqueries having the individual grouping sets as their GROUP BY clauses. The optional DISTINCT - clause removes duplicate sets before processing; it does not - transform the UNION ALL into a UNION DISTINCT. + key word removes duplicate grouping sets before processing; it does not + transform the implied UNION ALL into + a UNION DISTINCT. For further details on the handling of grouping sets see . @@ -1758,8 +1811,8 @@ SELECT * FROM name Examples - To join the table films with the table - distributors: + To join the table films with the table + distributors: SELECT f.title, f.did, d.name, f.date_prod, f.kind @@ -1774,8 +1827,8 @@ SELECT f.title, f.did, d.name, f.date_prod, f.kind - To sum the column len of all films and group - the results by kind: + To sum the column len of all films and group + the results by kind: SELECT kind, sum(len) AS total FROM films GROUP BY kind; @@ -1791,8 +1844,8 @@ SELECT kind, sum(len) AS total FROM films GROUP BY kind; - To sum the column len of all films, group - the results by kind and show those group totals + To sum the column len of all films, group + the results by kind and show those group totals that are less than 5 hours: @@ -1811,7 +1864,7 @@ SELECT kind, sum(len) AS total The following two examples are identical ways of sorting the individual results according to the contents of the second column - (name): + (name): SELECT * FROM distributors ORDER BY name; @@ -1837,8 +1890,8 @@ SELECT * FROM distributors ORDER BY 2; The next example shows how to obtain the union of the tables - distributors and - actors, restricting the results to those that begin + distributors and + actors, restricting the results to those that begin with the letter W in each table. Only distinct rows are wanted, so the key word ALL is omitted. @@ -1917,7 +1970,7 @@ SELECT * FROM unnest(ARRAY['a','b','c','d','e','f']) WITH ORDINALITY; WITH t AS ( - SELECT random() as x FROM generate_series(1, 3) + SELECT random() AS x FROM generate_series(1, 3) ) SELECT * FROM t UNION ALL diff --git a/doc/src/sgml/ref/select_into.sgml b/doc/src/sgml/ref/select_into.sgml index ae7e6bed24f25..cbf865ff8383c 100644 --- a/doc/src/sgml/ref/select_into.sgml +++ b/doc/src/sgml/ref/select_into.sgml @@ -27,15 +27,15 @@ SELECT [ ALL | DISTINCT [ ON ( expressionnew_table [ FROM from_item [, ...] ] [ WHERE condition ] - [ GROUP BY expression [, ...] ] + [ GROUP BY { ALL | [ ALL | DISTINCT ] grouping_element [, ...] } ] [ HAVING condition ] [ WINDOW window_name AS ( window_definition ) [, ...] ] [ { UNION | INTERSECT | EXCEPT } [ ALL | DISTINCT ] select ] [ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ] [ LIMIT { count | ALL } ] [ OFFSET start [ ROW | ROWS ] ] - [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ] - [ FOR { UPDATE | SHARE } [ OF table_name [, ...] ] [ NOWAIT ] [...] ] + [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } { ONLY | WITH TIES } ] + [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF from_reference [, ...] ] [ NOWAIT | SKIP LOCKED ] [...] ] @@ -120,8 +120,8 @@ SELECT [ ALL | DISTINCT [ ON ( expressionExamples - Create a new table films_recent consisting of only - recent entries from the table films: + Create a new table films_recent consisting of only + recent entries from the table films: SELECT * INTO films_recent FROM films WHERE date_prod >= '2002-01-01'; diff --git a/doc/src/sgml/ref/set.sgml b/doc/src/sgml/ref/set.sgml index 2218f54682ec3..16c7e9a7b2651 100644 --- a/doc/src/sgml/ref/set.sgml +++ b/doc/src/sgml/ref/set.sgml @@ -21,8 +21,8 @@ PostgreSQL documentation -SET [ SESSION | LOCAL ] configuration_parameter { TO | = } { value | 'value' | DEFAULT } -SET [ SESSION | LOCAL ] TIME ZONE { value | 'value' | LOCAL | DEFAULT } +SET [ SESSION | LOCAL ] configuration_parameter { TO | = } { value [, ...] | DEFAULT } +SET [ SESSION | LOCAL ] TIME ZONE { value | LOCAL | DEFAULT } @@ -123,7 +123,7 @@ SET [ SESSION | LOCAL ] TIME ZONE { valueconfiguration_parameter - Name of a settable run-time parameter. Available parameters are + Name of a settable configuration parameter. Available parameters are documented in and below. @@ -133,9 +133,12 @@ SET [ SESSION | LOCAL ] TIME ZONE { valuevalue - New value of parameter. Values can be specified as string + New value of the parameter. Values can be specified as string constants, identifiers, numbers, or comma-separated lists of these, as appropriate for the particular parameter. + Values that are neither numbers nor valid identifiers must be quoted. + If the parameter accepts a list of values, NULL + can be written to specify an empty list. DEFAULT can be written to specify resetting the parameter to its default value (that is, whatever value it would have had if no SET had been executed @@ -283,6 +286,19 @@ SELECT setseed(value); Set the schema search path: SET search_path TO my_schema, public; + + Note that this is not the same as + +SET search_path TO 'my_schema, public'; + + which would have the effect of setting search_path + to contain a single, probably-nonexistent schema name. + + + + Set the list of temporary tablespace names to be empty: + +SET temp_tablespaces TO NULL; diff --git a/doc/src/sgml/ref/truncate.sgml b/doc/src/sgml/ref/truncate.sgml index 9d846f88c9f60..d6efa286e993e 100644 --- a/doc/src/sgml/ref/truncate.sgml +++ b/doc/src/sgml/ref/truncate.sgml @@ -182,8 +182,8 @@ TRUNCATE [ TABLE ] [ ONLY ] name [ Examples - Truncate the tables bigtable and - fattable: + Truncate the tables bigtable and + fattable: TRUNCATE bigtable, fattable; @@ -199,8 +199,8 @@ TRUNCATE bigtable, fattable RESTART IDENTITY; - Truncate the table othertable, and cascade to any tables - that reference othertable via foreign-key + Truncate the table othertable, and cascade to any tables + that reference othertable via foreign-key constraints: diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml index 12ec5ba070939..92c12c369153c 100644 --- a/doc/src/sgml/ref/update.sgml +++ b/doc/src/sgml/ref/update.sgml @@ -22,7 +22,9 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] -UPDATE [ ONLY ] table_name [ * ] [ [ AS ] alias ] +UPDATE [ ONLY ] table_name [ * ] + [ FOR PORTION OF range_column_name for_portion_of_target ] + [ [ AS ] alias ] SET { column_name = { expression | DEFAULT } | ( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) | ( column_name [, ...] ) = ( sub-SELECT ) @@ -31,6 +33,11 @@ UPDATE [ ONLY ] table_name [ * ] [ [ WHERE condition | WHERE CURRENT OF cursor_name ] [ RETURNING [ WITH ( { OLD | NEW } AS output_alias [, ...] ) ] { * | output_expression [ [ AS ] output_name ] } [, ...] ] + +where for_portion_of_target is: + +{ FROM start_time TO end_time | + ( portion ) } @@ -57,11 +64,33 @@ UPDATE [ ONLY ] table_name [ * ] [ to compute and return value(s) based on each row actually updated. Any expression using the table's columns, and/or columns of other tables mentioned in FROM, can be computed. - The new (post-update) values of the table's columns are used. + By default, the new (post-update) values of the table's columns are used, + but it is also possible to request the old (pre-update) values. The syntax of the RETURNING list is identical to that of the output list of SELECT. + + If the FOR PORTION OF clause is used, the update will + only affect rows that overlap the given portion. Furthermore, if a row's + application time extends outside the FOR PORTION OF + bounds, then the update will only change the application time within those + bounds. In effect, only the history targeted by FOR PORTION + OF is updated, and no moments outside. Furthermore, after a row + is updated, the range or multirange is first shrunk so that its application + time no longer extends beyond the targeted FOR PORTION + OF bounds. Then, new temporal leftovers might + be inserted: rows whose range or multirange receives the remaining + application time outside the targeted + FROM/TO bounds, with the original + values in their other columns. For range columns, there will be zero to + two inserted records, depending on whether the original application time + was completely updated, extended before/after the change, or both. + Multiranges never require two temporal leftovers, because one value can + always contain whatever application time remains. + + You must have the UPDATE privilege on the table, or at least on the column(s) that are listed to be updated. @@ -69,6 +98,10 @@ UPDATE [ ONLY ] table_name [ * ] [ privilege on any column whose values are read in the expressions or condition. + When FOR PORTION OF is used, the secondary inserts do + not require INSERT privilege on the table. (This is + because conceptually no new information is being added; the inserted rows + only preserve existing data about the untargeted time period.) @@ -115,6 +148,56 @@ UPDATE [ ONLY ] table_name [ * ] [ + + range_column_name + + + The range or multirange column to use when performing a temporal update. + + + + + + for_portion_of_target + + + The portion to update. If targeting a range column, this can be in the + form FROM start_time TO + end_time. Otherwise, it + must be in the form (portion) where + portion is an expression + that yields a value of the same type as range_column_name. + + + + + + start_time + + + The earliest time (inclusive) to change in a temporal update. This must + be a value matching the base type of the range from range_column_name. A null value here + indicates an update whose beginning is unbounded (as with range types). + + + + + + end_time + + + The latest time (exclusive) to change in a temporal update. This must + be a value matching the base type of the range from range_column_name. A null value here + indicates an update whose end is unbounded (as with range types). + + + + column_name @@ -282,6 +365,10 @@ UPDATE count updates were suppressed by a BEFORE UPDATE trigger. If count is 0, no rows were updated by the query (this is not considered an error). + If FOR PORTION OF was used, the + count does not include + temporal leftovers + that were inserted. @@ -289,7 +376,12 @@ UPDATE count clause, the result will be similar to that of a SELECT statement containing the columns and values defined in the RETURNING list, computed over the row(s) updated by the - command. + command. If FOR PORTION OF was used, the + RETURNING clause gives one result for each updated row, + but does not include inserted + temporal leftovers. + The value of the application-time column matches the new value of the updated + row(s). @@ -354,6 +446,13 @@ UPDATE count partition that is not the same as the ancestor that's mentioned in the UPDATE query. + + + When FOR PORTION OF is used, this can result in users + who don't have INSERT privileges firing + INSERT triggers. This should be considered when + using SECURITY DEFINER trigger functions. + @@ -476,6 +575,16 @@ UPDATE films SET kind = 'Dramatic' WHERE CURRENT OF c_films; + + An example of a temporal update: + +UPDATE products + FOR PORTION OF valid_at FROM '2023-09-01' TO '2025-03-01' + SET price = 12.00 + WHERE product_no = 5; + + + Updates affecting many rows can have negative effects on system performance, such as table bloat, increased replica lag, and increased @@ -502,6 +611,9 @@ UPDATE work_item SET status = 'failed' WHERE work_item.ctid = emr.ctid; This command will need to be repeated until no rows remain to be updated. + (This use of ctid is only safe because + the query is repeatedly run, avoiding the problem of changed + ctids.) Use of an ORDER BY clause allows the command to prioritize which rows will be updated; it can also prevent deadlock with other update operations if they use the same ordering. diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index bd5dcaf86a5cc..38ee973ea05d6 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -25,7 +25,6 @@ VACUUM [ ( option [, ...] ) ] [ where option can be one of: - FULL [ boolean ] FREEZE [ boolean ] VERBOSE [ boolean ] ANALYZE [ boolean ] @@ -39,6 +38,7 @@ VACUUM [ ( option [, ...] ) ] [ boolean ] ONLY_DATABASE_STATS [ boolean ] BUFFER_USAGE_LIMIT size + FULL [ boolean ] and table_and_columns is: @@ -81,7 +81,7 @@ VACUUM [ ( option [, ...] ) ] [ parallel vacuum. + indexes. This feature is known as . To disable this feature, one can use PARALLEL option and specify parallel workers as zero. VACUUM FULL rewrites the entire contents of the table into a new disk file with no extra space, @@ -95,20 +95,6 @@ VACUUM [ ( option [, ...] ) ] [ Parameters - - FULL - - - Selects full vacuum, which can reclaim more - space, but takes much longer and exclusively locks the table. - This method also requires extra disk space, since it writes a - new copy of the table and doesn't release the old copy until - the operation is complete. Usually this should only be used when a - significant amount of space needs to be reclaimed from within the table. - - - - FREEZE @@ -280,24 +266,9 @@ VACUUM [ ( option [, ...] ) ] [ PARALLEL - Perform index vacuum and index cleanup phases of VACUUM - in parallel using integer - background workers (for the details of each vacuum phase, please - refer to ). The number of workers used - to perform the operation is equal to the number of indexes on the - relation that support parallel vacuum which is limited by the number of - workers specified with PARALLEL option if any which is - further limited by . - An index can participate in parallel vacuum if and only if the size of the - index is more than . - Please note that it is not guaranteed that the number of parallel workers - specified in integer will be - used during execution. It is possible for a vacuum to run with fewer - workers than specified, or even with no workers at all. Only one worker - can be used per index. So parallel workers are launched only when there - are at least 2 indexes in the table. Workers for - vacuum are launched before the start of each phase and exit at the end of - the phase. These behaviors might change in a future release. This + Specifies the maximum number of parallel workers that can be used + for , which is further limited + by . This option can't be used with the FULL option. @@ -362,6 +333,23 @@ VACUUM [ ( option [, ...] ) ] [ + + FULL + + + This option, which is deprecated, makes VACUUM + behave like REPACK without a + USING INDEX clause. + This method of compacting the table takes much longer than + VACUUM and exclusively locks the table. + This method also requires extra disk space, since it writes a + new copy of the table and doesn't release the old copy until + the operation is complete. Usually this should only be used when a + significant amount of space needs to be reclaimed from within the table. + + + + boolean @@ -512,7 +500,7 @@ VACUUM [ ( option [, ...] ) ] [ Examples - To clean a single table onek, analyze it for + To clean a single table onek, analyze it for the optimizer and print a detailed vacuum activity report: diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml index b0680a61814cc..508c8dfa14641 100644 --- a/doc/src/sgml/ref/vacuumdb.sgml +++ b/doc/src/sgml/ref/vacuumdb.sgml @@ -171,6 +171,16 @@ PostgreSQL documentation + + + + + Print, but do not execute, the vacuum and analyze commands that would + have been sent to the server. + + + + @@ -282,14 +292,24 @@ PostgreSQL documentation Only analyze relations that are missing statistics for a column, index - expression, or extended statistics object. This option prevents - vacuumdb from deleting existing statistics - so that the query optimizer's choices do not become transiently worse. + expression, or extended statistics object. When used with + , this option prevents + vacuumdb from temporarily replacing existing + statistics with ones generated with lower statistics targets, thus + avoiding transiently worse query optimizer choices. This option can only be used in conjunction with or . + + Note that requires + SELECT privileges on + pg_statistic + and + pg_statistic_ext_data, + which are restricted to superusers by default. + @@ -395,6 +415,15 @@ PostgreSQL documentation Multiple tables can be vacuumed by writing multiple switches. + + If no tables are specified with the option, + vacuumdb will clean all regular tables + and materialized views in the connected database. + If or + is also specified, + it will analyze all regular tables, partitioned tables, + and materialized views (but not foreign tables). + If you specify columns, you probably have to escape the parentheses diff --git a/doc/src/sgml/ref/wait_for.sgml b/doc/src/sgml/ref/wait_for.sgml new file mode 100644 index 0000000000000..9ba785ea3217b --- /dev/null +++ b/doc/src/sgml/ref/wait_for.sgml @@ -0,0 +1,373 @@ + + + + + WAIT FOR + + + + WAIT FOR + 7 + SQL - Language Statements + + + + WAIT FOR + wait for WAL to reach a target LSN + + + + +WAIT FOR LSN 'lsn' + [ WITH ( option [, ...] ) ] + +where option can be: + + MODE 'mode' + TIMEOUT 'timeout' + NO_THROW + +and mode can be: + + standby_replay | standby_write | standby_flush | primary_flush + + + + + Description + + + Waits until the specified lsn is reached + according to the specified mode, + which determines whether to wait for WAL to be written, flushed, or replayed. + If no timeout is specified or it is set to + zero, this command waits indefinitely for the + lsn. + + + + On timeout, an error is emitted unless NO_THROW + is specified in the WITH clause. For standby modes + (standby_replay, standby_write, + standby_flush), an error is also emitted if the + server is promoted before the lsn is reached. + If NO_THROW is specified, the command returns + a status string instead of throwing errors. + + + + The possible return values are success, + timeout, and not in recovery. + + + + + Parameters + + + + lsn + + + Specifies the target LSN to wait for. + + + + + + WITH ( option [, ...] ) + + + This clause specifies optional parameters for the wait operation. + The following parameters are supported: + + + + MODE 'mode' + + + Specifies the type of LSN processing to wait for. If not specified, + the default is standby_replay. The valid modes are: + + + + + standby_replay: Wait for the LSN to be replayed + (applied to the database) on a standby server. After successful + completion, pg_last_wal_replay_lsn() will + return a value greater than or equal to the target LSN. This mode + can only be used during recovery. + + + + + standby_write: Wait for the WAL containing the + LSN to be received from the primary and written to disk on a + standby server, but not yet flushed. This is faster than + standby_flush but provides weaker durability + guarantees since the data may still be in operating system + buffers. After successful completion, the + written_lsn column in + + pg_stat_wal_receiver will show + a value greater than or equal to the target LSN. This mode can + only be used during recovery. + + + + + standby_flush: Wait for the WAL containing the + LSN to be received from the primary and flushed to disk on a + standby server. This provides a durability guarantee without + waiting for the WAL to be applied. After successful completion, + pg_last_wal_receive_lsn() will return a + value greater than or equal to the target LSN. This value is + also available as the flushed_lsn + column in + pg_stat_wal_receiver. This mode + can only be used during recovery. + + + + + primary_flush: Wait for the WAL containing the + LSN to be flushed to disk on a primary server. After successful + completion, pg_current_wal_flush_lsn() will + return a value greater than or equal to the target LSN. This mode + can only be used on a primary server (not during recovery). + + + + + + + + TIMEOUT 'timeout' + + + When specified and timeout is greater than zero, + the command waits until lsn is reached or + the specified timeout has elapsed. + + + The timeout might be given as integer number of + milliseconds. Also it might be given as string literal with + integer number of milliseconds or a number with unit + (see ). + + + + + + NO_THROW + + + Specify to not throw an error in the case of timeout or + running on the primary. In this case the result status can be get from + the return value. + + + + + + + + + + + + Outputs + + + + success + + + This return value denotes that we have successfully reached + the target lsn. + + + + + + timeout + + + This return value denotes that the timeout happened before reaching + the target lsn. + + + + + + not in recovery + + + This return value denotes that the database server is not in a recovery + state. This might mean either the database server was not in recovery + at the moment of receiving the command (i.e., executed on a primary), + or it was promoted before reaching the target lsn. + In the promotion case, this status indicates a timeline change occurred, + and the application should re-evaluate whether the target LSN is still + relevant. + + + + + + + + Notes + + WAIT FOR must be executed as a top-level command. + It cannot be executed from a function, procedure, or + DO block. It also requires that no active or + registered snapshot be held, and therefore cannot be used in contexts + where such a snapshot must remain active, including transactions running + at isolation levels higher than READ COMMITTED. + + + + WAIT FOR waits until the specified + lsn is reached according to the specified + mode. The standby_replay mode + waits for the LSN to be replayed (applied to the database), which is + useful to achieve read-your-writes consistency while using an async + replica for reads and the primary for writes. The + standby_flush mode waits for the WAL to be flushed + to durable storage on the replica, providing a durability guarantee + without waiting for replay. The standby_write mode + waits for the WAL to be written to the operating system, which is + faster than flush but provides weaker durability guarantees. The + primary_flush mode waits for WAL to be flushed on + a primary server. In all cases, the LSN of the last + modification should be stored on the client application side or the + connection pooler side. + + + + The standby modes (standby_replay, + standby_write, standby_flush) + can only be used during recovery, and primary_flush + can only be used on a primary server. Using the wrong mode for the + current server state will result in an error. If a standby is promoted + while waiting with a standby mode, the command will return + not in recovery (or throw an error if + NO_THROW is not specified). Promotion creates a new + timeline, and the LSN being waited for may refer to WAL from the old + timeline. + + + + On a standby server, WAIT FOR sessions may be + interrupted by recovery conflicts. Some recovery conflicts are + unavoidable: for example, replaying a tablespace drop resolves + conflicts by terminating all backends, regardless of what they are + doing. Applications using WAIT FOR on a standby + should be prepared to handle such interruptions, for example by + retrying the command or falling back to an alternative mechanism. + + + + + + Examples + + + You can use WAIT FOR command to wait for + the pg_lsn value. For example, an application could update + the movie table and get the lsn after + changes just made. This example uses pg_current_wal_insert_lsn + on primary server to get the lsn given that + synchronous_commit could be set to + off. + + +postgres=# UPDATE movie SET genre = 'Dramatic' WHERE genre = 'Drama'; +UPDATE 100 +postgres=# SELECT pg_current_wal_insert_lsn(); + pg_current_wal_insert_lsn +--------------------------- + 0/306EE20 +(1 row) + + + Then an application could run WAIT FOR + with the lsn obtained from primary. After that the + changes made on primary should be guaranteed to be visible on replica. + + +postgres=# WAIT FOR LSN '0/306EE20'; + status +--------- + success +(1 row) +postgres=# SELECT * FROM movie WHERE genre = 'Drama'; + genre +------- +(0 rows) + + + + + Wait for flush (data durable on replica): + + +postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'standby_flush'); + status +--------- + success +(1 row) + + + + + Wait for write with timeout: + + +postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'standby_write', TIMEOUT '100ms', NO_THROW); + status +--------- + success +(1 row) + + + + + Wait for flush on primary: + + +postgres=# WAIT FOR LSN '0/306EE20' WITH (MODE 'primary_flush'); + status +--------- + success +(1 row) + + + + + If the target LSN is not reached before the timeout, an error is thrown: + + +postgres=# WAIT FOR LSN '0/306EE20' WITH (TIMEOUT '0.1s'); +ERROR: timed out while waiting for target LSN 0/306EE20 to be replayed; current replay LSN 0/306EA60 + + + + + The same example uses WAIT FOR with + NO_THROW option: + + +postgres=# WAIT FOR LSN '0/306EE20' WITH (TIMEOUT '100ms', NO_THROW); + status +--------- + timeout +(1 row) + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml index ff85ace83fc48..674ac17e82c86 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -55,6 +55,7 @@ &alterOperatorFamily; &alterPolicy; &alterProcedure; + &alterPropertyGraph; &alterPublication; &alterRole; &alterRoutine; @@ -107,6 +108,7 @@ &createOperatorFamily; &createPolicy; &createProcedure; + &createPropertyGraph; &createPublication; &createRole; &createRule; @@ -155,6 +157,7 @@ &dropOwned; &dropPolicy; &dropProcedure; + &dropPropertyGraph; &dropPublication; &dropRole; &dropRoutine; @@ -195,6 +198,7 @@ &refreshMaterializedView; &reindex; &releaseSavepoint; + &repack; &reset; &revoke; &rollback; @@ -216,6 +220,7 @@ &update; &vacuum; &values; + &waitFor; diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml index bf4ffb3057636..c74941bfbf20a 100644 --- a/doc/src/sgml/regress.sgml +++ b/doc/src/sgml/regress.sgml @@ -125,6 +125,18 @@ make installcheck-parallel + + Running Specific Tests + + + A subset of the regression tests can be run with the command + make check-tests TESTS="boolean char" or + make installcheck-tests TESTS="boolean char". + Note that sometimes tests have dependencies on objects created by other + tests, which can cause unexpected failures. + + + Additional Test Suites @@ -254,7 +266,7 @@ make check-world -j8 >/dev/null Some test suites are not run by default, either because they are not secure to run on a multiuser system, because they require special software or - because they are resource intensive. You can decide which test suites to + because they are resource-intensive. You can decide which test suites to run additionally by setting the make or environment variable PG_TEST_EXTRA to a whitespace-separated list, for example: @@ -263,6 +275,20 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption' The following values are currently supported: + + checksum, checksum_extended + + + Runs additional tests for enabling data checksums which inject faults + to cause re-tries in the processing, as well as tests that run pgbench + concurrently and randomly restarts the cluster. Some of these test + suites require injection points enabled in the installation. + checksum_extended is an extended version with + longer runtime, injected random delays and larger datasets. + + + + kerberos @@ -285,75 +311,98 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption' - sepgsql + libpq_encryption - Runs the test suite under contrib/sepgsql. This - requires an SELinux environment that is set up in a specific way; see - . + Runs the test src/interfaces/libpq/t/005_negotiate_encryption.pl. + This opens TCP/IP listen sockets. If PG_TEST_EXTRA + also includes kerberos, additional tests that require + an MIT Kerberos installation are enabled. - ssl + load_balance - Runs the test suite under src/test/ssl. This opens TCP/IP listen sockets. + Runs the test src/interfaces/libpq/t/004_load_balance_dns.pl. + This requires editing the system hosts file and + opens TCP/IP listen sockets. - load_balance + oauth - Runs the test src/interfaces/libpq/t/004_load_balance_dns.pl. - This requires editing the system hosts file and - opens TCP/IP listen sockets. + Runs the test suite under src/test/modules/oauth_validator. + This opens TCP/IP listen sockets for a test server running HTTPS. - libpq_encryption + regress_dump_restore - Runs the test src/interfaces/libpq/t/005_negotiate_encryption.pl. - This opens TCP/IP listen sockets. If PG_TEST_EXTRA - also includes kerberos, additional tests that require - an MIT Kerberos installation are enabled. + Runs an additional test suite in + src/bin/pg_upgrade/t/002_pg_upgrade.pl which + cycles the regression database through pg_dump/ + pg_restore. Not enabled by default because it + is resource-intensive. - wal_consistency_checking + saslprep - Uses wal_consistency_checking=all while running - certain tests under src/test/recovery. Not - enabled by default because it is resource intensive. + Runs the TAP test suite under src/test/modules/test_saslprep. + Not enabled by default because it is resource-intensive. - xid_wraparound + sepgsql - Runs the test suite under src/test/modules/xid_wraparound. - Not enabled by default because it is resource intensive. + Runs the test suite under contrib/sepgsql. This + requires an SELinux environment that is set up in a specific way; see + . - oauth + ssl - Runs the test suite under src/test/modules/oauth_validator. - This opens TCP/IP listen sockets for a test server running HTTPS. + Runs the test suite under src/test/ssl. This opens TCP/IP listen sockets. + + + + + + wal_consistency_checking + + + Uses wal_consistency_checking=all while running + certain tests under src/test/recovery. Not + enabled by default because it is resource-intensive. + + + + + + xid_wraparound + + + Runs the test suite under src/test/modules/xid_wraparound. + Not enabled by default because it is resource-intensive. @@ -419,6 +468,70 @@ make check LANG=C ENCODING=EUC_JP + + Path Substitution + + + The test suites driven by pg_regress can use the + following environment variables for path substitutions: + + + + PG_ABS_SRCDIR + + + Absolute path to the source directory. + + + + + + PG_ABS_BUILDDIR + + + Absolute path to the build directory. + + + + + + PG_DLSUFFIX + + + Name of extension for dynamically-loadable modules (e.g. + .so on Linux). + + + + + + PG_LIBDIR + + + Absolute path to dynamic libraries. + + + + + + + + These variables should be set in the tests with the meta-command + \getenv, like: + +\getenv abs_builddir PG_ABS_BUILDDIR +\getenv abs_srcdir PG_ABS_SRCDIR + + + + + These are useful when dealing with function and object loading + that require specific paths to work, like paths defined in a + CREATE FUNCTION or LOAD + command. + + + Custom Server Settings @@ -892,6 +1005,14 @@ PG_TEST_NOCLEAN=1 make -C src/bin/pg_dump check PG_TEST_TIMEOUT_DEFAULT to a higher number will change the default to avoid this. + + + For certain tests, the environment variable + PG_TEST_FILE_READ_LINES can be set to limit the number of + lines read from large output files (head and tail). This is useful when + the test output contains a lot of unnecessary content, allowing the test + framework to read only a limited number of lines for its reports. + diff --git a/doc/src/sgml/release-18.sgml b/doc/src/sgml/release-18.sgml deleted file mode 100644 index 2ae03065f9451..0000000000000 --- a/doc/src/sgml/release-18.sgml +++ /dev/null @@ -1,3534 +0,0 @@ - - - - - Release 18 - - - Release date: - 2025-??-??, CURRENT AS OF 2025-05-23 - - - - Overview - - - PostgreSQL 18 contains many new features - and enhancements, including: - - - - - - - (to be completed) - - - - - - The above items and other new features of - PostgreSQL 18 are explained in more detail - in the sections below. - - - - - - - Migration to Version 18 - - - A dump/restore using or use of - or logical replication is required for - those wishing to migrate data from any previous release. See for general information on migrating to new - major releases. - - - - Version 18 contains a number of changes that may affect compatibility - with previous releases. Observe the following incompatibilities: - - - - - - - - -Change time zone abbreviation handling (Tom Lane) -§ - - - -The system will now favor the current session's time zone abbreviations before checking the server variable timezone_abbreviations. Previously timezone_abbreviations was -checked first. - - - - - - - -Deprecate MD5 password authentication (Nathan Bossart) -§ - - - -Warnings generated by their use can be disabled by the server variable md5_password_warnings. - - - - - - - -Change VACUUM and ANALYZE to process the inheritance children of a parent (Michael Harris) -§ - - - -The previous behavior can be performed by using the new ONLY option. - - - - - - - -Prevent COPY FROM from treating \. as an end-of-file marker when reading CSV files (Daniel Vérité, Tom Lane) -§ -§ - - - -psql will still treat \. as an end-of-file marker when reading CSV files from STDIN. Older psql clients connecting to Postgres 18 servers might experience \copy problems. This -release also enforces that \. must appear alone on a line. - - - - - - - -Disallow unlogged partitioned tables (Michael Paquier) -§ - - - -Previously ALTER TABLE SET [UN]LOGGED did nothing, and the creation of an unlogged partitioned table did not cause its children to be unlogged. - - - - - - - -Remove non-functional support for RULE privileges in GRANT/REVOKE (Fujii Masao) -§ - - - -These have been non-functional since Postgres 8.2. - - - - - - - -Remove column pg_backend_memory_contexts.parent (Melih Mutlu) -§ - - - -This is now longer needed since pg_backend_memory_contexts.path was added. - - - - - - - -Change pg_backend_memory_contexts.level and pg_log_backend_memory_contexts() to be one-based (Melih Mutlu, Atsushi Torikoshi, David Rowley, Fujii Masao) -§ -§ -§ - - - -These were previously zero-based. - - - - - - - - - Changes - - - Below you will find a detailed account of the changes between - PostgreSQL 18 and the previous major - release. - - - - Server - - - Optimizer - - - - - - - -Remove some unnecessary table self-joins (Andrey Lepikhov, Alexander Kuzmenkov, Alexander Korotkov, Alena Rybakina) -§ - - - -This optimization can be disabled using server variable enable_self_join_elimination. - - - - - - - -Convert some 'IN (VALUES ...)' to 'x = ANY ...' for better optimizer statistics (Alena Rybakina, Andrei Lepikhov) -§ - - - - - - - -Allow transforming OR-clauses to arrays for faster index processing (Alexander Korotkov, Andrey Lepikhov) -§ - - - - - - - -Speed up the processing of INTERSECT, EXCEPT, window aggregates, and view column aliases (Tom Lane, David Rowley) -§ -§ -§ -§ - - - - - - - -Allow the keys of SELECT DISTINCT to be internally reordered to avoid sorting (Richard Guo) -§ - - - -This optimization can be disabled using enable_distinct_reordering. - - - - - - - -Ignore GROUP BY columns that are functionally dependent on other columns (Zhang Mingli, Jian He, David Rowley) -§ - - - -If a GROUP BY clause includes all columns of a unique index, as well as other columns of the same table, those other columns are redundant and can be dropped -from the grouping. This was already true for non-deferred primary keys. - - - - - - - -Allow some HAVING clauses on GROUPING SETS to be pushed to WHERE clauses (Richard Guo) -§ -§ -§ -§ - - - -This allows earlier row filtering. This release also fixes some GROUPING SETS queries that used to return incorrect results. - - - - - - - -Improve row estimates for generate_series() using numeric and timestamp values (David Rowley, Song Jinzhou) -§ -§ - - - - - - - -Allow the optimizer to use "Right Semi Join" plans (Richard Guo) -§ - - - -Semi-joins are used when needing to find if there is at least one match. - - - - - - - -Allow merge joins to use incremental sorts (Richard Guo) -§ - - - - - - - -Improve the efficiency of planning queries accessing many partitions (Ashutosh Bapat, Yuya Watari, David Rowley) -§ -§ - - - - - - - -Allow partitionwise joins in more cases, and reduce its memory usage (Richard Guo, Tom Lane, Ashutosh Bapat) -§ -§ - - - - - - - -Improve cost estimates of partition queries (Nikita Malakhov, Andrei Lepikhov) -§ - - - - - - - -Improve SQL-language function plan caching (Alexander Pyhalov, Tom Lane) -§ -§ - - - - - - - -Improve handling of disabled optimizer features (Robert Haas) -§ - - - - - - - - - Indexes - - - - - - - -Allow skip scans of btree indexes (Peter Geoghegan) -§ -§ - - - -This allows multi-column btree indexes to be used by queries that only -equality-reference the second or later indexed columns. - - - - - - - -Allow non-btree unique indexes to be used as partition keys and in materialized views (Mark Dilger) -§ -§ - - - -The index type must still support equality. - - - - - - - -Allow GIN indexes to be created in parallel (Tomas Vondra, Matthias van de Meent) -§ - - - - - - - -Allow values to be sorted to speed rangetype GiST and btree index builds (Bernd Helmle) -§ - - - - - - - - - General Performance - - - - - - - -Add an asynchronous I/O subsystem (Andres Freund, Thomas Munro, Nazir Bilal Yavuz, Melanie Plageman) -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ - - - -This is enabled by server variable io_method, with server variables io_combine_limit and io_max_combine_limit added to control it. This also enables -effective_io_concurrency and maintenance_io_concurrency values greater than zero for systems without fadvise() support. The new system view pg_aios shows the file handles being used -for asynchronous I/O. - - - - - - - -Improve the locking performance of queries that access many relations (Tomas Vondra) -§ - - - - - - - -Improve the performance and reduce memory usage of hash joins and GROUP BY (David Rowley, Jeff Davis) -§ -§ -§ -§ -§ - - - -This also improves hash set operations used by EXCEPT, and hash lookups of subplan values. - - - - - - - -Allow normal vacuums to freeze some pages, even though they are all-visible (Melanie Plageman) -§ -§ - - - -This reduces the overhead of later full-relation freezing. The aggressiveness of this can be controlled by server variable and per-table setting vacuum_max_eager_freeze_failure_rate. -Previously vacuum never processed all-visible pages until freezing was required. - - - - - - - -Add server variable vacuum_truncate to control file truncation during VACUUM (Nathan Bossart, Gurjeet Singh) -§ - - - -A storage-level parameter with the same name and behavior already existed. - - - - - - - -Increase server variables effective_io_concurrency's and maintenance_io_concurrency's default values to 16 (Melanie Plageman) -§ -§ - - - -This more accurately reflects modern hardware. - - - - - - - - - Monitoring - - - - - - - -Increase the logging granularity of server variable log_connections (Melanie Plageman) -§ -§ - - - -This server variable was previously only boolean; these options are still supported. - - - - - - - -Add log_line_prefix escape "%L" to output the client IP address (Greg Sabino Mullane) -§ - - - - - - - -Add server variable log_lock_failure to log lock acquisition failures (Yuki Seino) -§ - - - -Specifically it reports SELECT ... NOWAIT lock failures. - - - - - - - -Modify pg_stat_all_tables and its variants to report the time spent in vacuum, analyze, and their automatic variants (Sami Imseih) -§ - - - -The new columns are total_vacuum_time, total_autovacuum_time, total_analyze_time, and total_autoanalyze_time. - - - - - - - -Add delay time reporting to VACUUM and ANALYZE (Bertrand Drouvot, Nathan Bossart) -§ -§ - - - -This information appears in the autovacuum logs, the system views pg_stat_progress_vacuum and pg_stat_progress_analyze, and the output of VACUUM and ANALYZE when in VERBOSE -mode; tracking must be enabled with the server variable track_cost_delay_timing. - - - - - - - -Add per-backend I/O statistics reporting (Bertrand Drouvot) -§ -§ - - - -The statistics are accessed via pg_stat_get_backend_io(). Per-backend I/O statistics can be cleared via pg_stat_reset_backend_stats(). - - - - - - - -Add pg_stat_io columns to report I/O activity in bytes (Nazir Bilal Yavuz) -§ - - - -The new columns are read_bytes, write_bytes, and extend_bytes. The op_bytes column, which always equaled BLCKSZ, has been removed. - - - - - - - -Add WAL I/O activity rows to pg_stat_io (Nazir Bilal Yavuz, Bertrand Drouvot, Michael Paquier) -§ -§ -§ - - - -This includes WAL receiver activity and a wait event for such writes. - - - - - - - - -Change server variable track_wal_io_timing to control tracking WAL timing in pg_stat_io instead of pg_stat_wal (Bertrand Drouvot) -§ - - - - - - - -Remove read/sync columns from pg_stat_wal (Bertrand Drouvot) -§ -§ - - - -This removes columns wal_write, wal_sync, wal_write_time, and wal_sync_time. - - - - - - - -Add function pg_stat_get_backend_wal() to return per-backend WAL statistics (Bertrand Drouvot) -§ - - - -Per-backend WAL statistics can be cleared via pg_stat_reset_backend_stats(). - - - - - - - -Add function pg_ls_summariesdir() to specifically list the contents of PGDATA/pg_wal/summaries (Yushi Ogiwara) -§ - - - - - - - -Add column pg_stat_checkpointer.num_done to report the number of completed checkpoints (Anton A. Melnikov) -§ - - - -Columns num_timed and num_requested count both completed and skipped checkpoints. - - - - - - - -Add column pg_stat_checkpointer.slru_written to report SLRU buffers written (Nitin Jadhav) -§ - - - -Also, modify the checkpoint server log message to report separate shared buffer and SLRU buffer values. - - - - - - - -Add columns to pg_stat_database to report parallel workers activity (Benoit Lobréau) -§ - - - -The new columns are parallel_workers_to_launch and parallel_workers_launched. - - - - - - - -Have query jumbling of arrays consider only the first and last array elements (Dmitry Dolgov, Sami Imseih) -§ -§ - - - -Jumbling is used by pg_stat_statements. - - - - - - - -Adjust query jumbling to group together queries using the same relation name (Michael Paquier, Sami Imseih) -§ - - - -This is true even if the tables in different schemas have different column names. - - - - - - - -Add column pg_backend_memory_contexts.type to report the type of memory context (David Rowley) -§ - - - - - - - -Add column pg_backend_memory_contexts.path to show memory context parents (Melih Mutlu) -§ - - - - - - - - - Privileges - - - - - - - -Add function pg_get_acl() to retrieve database access control details (Joel Jacobson) -§ -§ - - - - - - - -Add function has_largeobject_privilege() to check large object privileges (Yugo Nagata) -§ - - - - - - - -Allow ALTER DEFAULT PRIVILEGES to define large object default privileges (Takatsuka Haruka, Yugo Nagata, Laurenz Albe) -§ - - - - - - - -Add predefined role pg_signal_autovacuum_worker (Kirill Reshke) -§ - - - -This allows sending signals to autovacuum workers. - - - - - - - - - Server Configuration - - - - - - - -Add support for the OAuth authentication method (Jacob Champion, Daniel Gustafsson, Thomas Munro) -§ - - - -This adds an "oauth" authentication method to pg_hba.conf, libpq OAuth options, a server variable oauth_validator_libraries to load token validation libraries, and -a configure flag --with-libcurl to add the required compile-time libraries. - - - - - - - -Add server variable ssl_tls13_ciphers to allow specification of multiple colon-separated TLSv1.3 cipher suites (Erica Zhang, Daniel Gustafsson) -§ - - - - - - - -Change server variable ssl_groups's default to include elliptic curve X25519 (Daniel Gustafsson, Jacob Champion) -§ - - - - - - - -Rename server variable ssl_ecdh_curve to ssl_groups and allow multiple colon-separated ECDH curves to be specified (Erica Zhang, Daniel Gustafsson) -§ - - -The previous name still works. - - - - - - - -Add function pg_check_fipsmode() to report the server's FIPS mode (Daniel Gustafsson) -§ - - - - - - - -Make cancel request keys 256 bits (Heikki Linnakangas, Jelte Fennema-Nio) -§ -§ - - - -This is only possible when the server and client support wire protocol version 3.2, introduced in this release. - - - - - - - -Add server variable autovacuum_worker_slots to specify the maximum number of background workers (Nathan Bossart) -§ - - - -With this variable set, autovacuum_max_workers can be adjusted at runtime up to this maximum without a server restart. - - - - - - - -Allow specification of the fixed number of dead tuples that will trigger an autovacuum (Nathan Bossart, Frédéric Yhuel) -§ - - - -The server variable is autovacuum_vacuum_max_threshold. Percentages are still used for triggering. - - - - - - - -Change server variable max_files_per_process to limit only files opened by a backend (Andres Freund) -§ - - - -Previously files opened by the postmaster were also counted toward this limit. - - - - - - - -Add server variable num_os_semaphores to report the required number of semaphores (Nathan Bossart) -§ - - - -This is useful for operating system configuration. - - - - - - - -Add server variable extension_control_path to specify the location of extension control files (Peter Eisentraut, Matheus Alcantara) -§ -§ - - - - - - - - - Streaming Replication and Recovery - - - - - - - -Allow inactive replication slots to be automatically invalided using server variable idle_replication_slot_timeout (Nisha Moond, Bharath Rupireddy) -§ - - - - - - - -Add server variable max_active_replication_origins to control the maximum active replication origins (Euler Taveira) -§ - - - -This was previously controlled by max_replication_slots, but this new setting allows a higher origin count in cases where fewer slots are required. - - - - - - - - - <link linkend="logical-replication">Logical Replication</link> - - - - - - - -Allow the values of generated columns to be logically replicated (Shubham Khanna, Vignesh C, Zhijie Hou, Shlok Kyal, Peter Smith) -§ -§ -§ -§ - - - -If the publication specifies a column list, all specified columns, generated and non-generated, are published. Without a specified column list, publication option publish_generated_columns -controls whether generated columns are published. Previously generated columns were not replicated and the subscriber had to compute the values if possible; this is particularly -useful for non-Postgres subscribers which lack such a capability. - - - - - - - -Change the default CREATE SUBSCRIPTION streaming option from "off" to "parallel" (Vignesh C) -§ - - - - - - - -Allow ALTER SUBSCRIPTION to change the replication slot's two-phase commit behavior (Hayato Kuroda, Ajin Cherian, Amit Kapila, Zhijie Hou) -§ -§ - - - - - - - -Log conflicts while applying logical replication changes (Zhijie Hou, Nisha Moond) -§ -§ -§ -§ -§ - - - -Also report in new columns of pg_stat_subscription_stats. - - - - - - - - - - - Utility Commands - - - - - - - -Allow generated columns to be virtual, and make them the default (Peter Eisentraut, Jian He, Richard Guo, Dean Rasheed) -§ -§ -§ - - - -Virtual generated columns generate their values when the columns are read, not written. The write behavior can still be specified via the STORED option. - - - - - - - -Add OLD/NEW support to RETURNING in DML queries (Dean Rasheed) -§ - - - -Previously RETURNING only returned new values for INSERT and UPDATE, and old values for DELETE; MERGE would return the appropriate value for the internal query executed. This new syntax -allows the RETURNING list of INSERT/UPDATE/DELETE/MERGE to explicitly return old and new values by using the special aliases "old" and "new". These aliases can be renamed to -avoid identifier conflicts. - - - - - - - -Allow foreign tables to be created like existing local tables (Zhang Mingli) -§ - - - -The syntax is CREATE FOREIGN TABLE ... LIKE. - - - - - - - -Allow LIKE with nondeterministic collations (Peter Eisentraut) -§ - - - - - - - -Allow text position search functions with nondeterministic collations (Peter Eisentraut) -§ - - - -These used to generate an error. - - - - - - - -Add builtin collation provider PG_UNICODE_FAST (Jeff Davis) -§ - - - -This locale supports case mapping, but sorts in code point order, not natural language order. - - - - - - - -Allow VACUUM and ANALYZE to process partitioned tables without processing their children (Michael Harris) -§ - - - -This is enabled with the new ONLY option. This is useful since autovacuum does not process partitioned tables, just its children. - - - - - - - -Add functions to modify per-relation and per-column optimizer statistics (Corey Huinker) -§ -§ -§ - - - -The functions are pg_restore_relation_stats(), pg_restore_attribute_stats(), pg_clear_relation_stats(), and pg_clear_attribute_stats. - - - - - - - - -Add server variable file_copy_method to control the file copying method (Nazir Bilal Yavuz) -§ - - - -This controls whether CREATE DATABASE ... STRATEGY=FILE_COPY and ALTER DATABASE ... SET TABLESPACE uses file copy or clone. - - - - - - - <link linkend="ddl-constraints">Constraints</link> - - - - - - - -Allow the specification of non-overlapping PRIMARY KEY and UNIQUE constraints (Paul A. Jungwirth) -§ - - - -This is specified by WITHOUT OVERLAPS on the last column. - - - - - - - -Allow CHECK and foreign key constraints to be specified as NOT ENFORCED (Amul Sul) -§ -§ - - - -This also adds column pg_constraint.conenforced. - - - - - - - -Require primary/foreign key relationships to use either deterministic collations or the the same nondeterministic collations (Peter Eisentraut) -§ - - - -The restore of a pg_dump, also used by pg_upgrade, will fail if these requirements are not met; schema changes must be made for these upgrade methods to succeed. - - - - - - - -Store column NOT NULL specifications in pg_constraint (Ãlvaro Herrera, Bernd Helmle) -§ - - - -This allows names to be specified for NOT NULL constraint. This also adds NOT NULL constraints to foreign tables and NOT NULL inheritance control to local tables. - - - - - - - -Allow ALTER TABLE to set the NOT VALID attribute of NOT NULL constraints (Rushabh Lathia, Jian He) -§ - - - - - - - -Allow modification of the inheritability of NOT NULL constraints (Suraj Kharage, Ãlvaro Herrera) -§ -§ - - - -The syntax is ALTER TABLE ... ALTER CONSTRAINT ... [NO] INHERIT. - - - - - - - -Allow NOT VALID foreign key constraints on partitioned tables (Amul Sul) -§ - - - - - - - -Allow dropping of constraints ONLY on partitioned tables (Ãlvaro Herrera) -§ - - - -This was previously erroneously prohibited. - - - - - - - - <link linkend="sql-copy"><command>COPY</command></link> - - - - - - - -Add REJECT_LIMIT to control the number of invalid rows COPY FROM can ignore (Atsushi Torikoshi) -§ - - - -This is available when ON_ERROR = 'ignore'. - - - - - - - -Allow COPY TO to copy rows from populated materialized view (Jian He) -§ - - - - - - - -Add COPY LOG_VERBOSITY level "silent" to suppress log output of ignored rows (Atsushi Torikoshi) -§ - - - -This new level suppresses output for discarded input rows when on_error = 'ignore'. - - - - - - - -Disallow COPY FREEZE on foreign tables (Nathan Bossart) -§ - - - -Previously, the COPY worked but the FREEZE was ignored, so disallow this command. - - - - - - - - <link linkend="sql-explain"><command>EXPLAIN</command></link> - - - - - - - -Automatically include BUFFERS output in EXPLAIN ANALYZE (Guillaume Lelarge, David Rowley) -§ - - - - - - - -Add WAL, CPU, and average read statistics output to EXPLAIN ANALYZE VERBOSE (Anthonin Bonnefoy) -§ -§ - - - - - - - -Add full WAL buffer count to EXPLAIN (WAL), VACUUM/ANALYZE (VERBOSE), and autovacuum log output (Bertrand Drouvot) -§ -§ - - - - - - - -In EXPLAIN ANALYZE, report the number of index lookups used per index scan node (Peter Geoghegan) -§ - - - - - - - -Modify EXPLAIN to output fractional row counts (Ibrar Ahmed, Ilia Evdokimov, Robert Haas) -§ -§ - - - - - - - -Add memory and disk usage details to Material, Window Aggregate, and common table expression nodes in EXPLAIN (David Rowley, Tatsuo Ishii) -§ -§ -§ -§ - - - - - - - - -Add details about window function arguments to EXPLAIN output (Tom Lane) -§ - - - - - - - -Add "Parallel Bitmap Heap Scan" worker cache statistics to EXPLAIN ANALYZE (David Geier, Heikki Linnakangas, Donghang Lin, Alena Rybakina, David Rowley) -§ - - - - - - - -Indicate disabled nodes in EXPLAIN ANALYZE output (Robert Haas, David Rowley, Laurenz Albe) -§ -§ -§ - - - - - - - - - - - Data Types - - - - - - - -Improve Unicode full case mapping and conversion (Jeff Davis) -§ -§ - - - -This adds the ability to do conditional and title case mapping, and case map single characters to multiple characters. - - - - - - - -Allow jsonb "null" values to be cast to scalar types as NULL (Tom Lane) -§ - - - -Previously such casts generated an error. - - - - - - - -Add optional parameter to json{b}_strip_nulls to allow removal of null array elements (Florents Tselai) -§ - - - - - - - -Add function array_sort() which sorts an array's first dimension (Junwang Zhao, Jian He) -§ - - - - - - - -Add function array_reverse() which reverses an array's first dimension (Aleksander Alekseev) -§ - - - - - - - -Add function reverse() to reverse bytea bytes (Aleksander Alekseev) -§ - - - - - - - -Allow casting between integer types and bytea (Aleksander Alekseev) -§ - - - -The integer values are stored as bytea two's complement values. - - - - - - - -Update Unicode data to Unicode 16.0.0 (Peter Eisentraut) -§ - - - - - - - -Add full text search stemming for Estonian (Tom Lane) -§ - - - - - - - -Improve the XML error codes to more closely match the SQL standard (Tom Lane) -§ - - - -These errors are reported via SQLSTATE. - - - - - - - - - Functions - - - - - - - -Add function CASEFOLD() to allow for more sophisticated case-insensitive matching (Jeff Davis) -§ - - - -Allows more accurate comparison, i.e., a character can have multiple upper or lower case equivalents, or upper or lower case conversion changes the number of characters. - - - - - - - -Allow MIN()/MAX() aggregates on arrays and composite types (Aleksander Alekseev, Marat Buharov) -§ -§ - - - - - - - -Add a WEEK option to EXTRACT() (Tom Lane) -§ - - - - - - - -Improve the output EXTRACT(QUARTER ...) for negative values (Tom Lane) -§ - - - - - - - -Add roman numeral support to to_number() (Hunaid Sohail) -§ - - - -This is accessed via the "RN" pattern. - - - - - - - -Add UUID version 7 generation function uuidv7() (Andrey Borodin) -§ - - - -This UUID value is temporally sortable. Function alias uuidv4() has been added to explicitly generate version 4 UUIDs. - - - - - - - -Add functions crc32() and crc32c() to compute CRC values (Aleksander Alekseev) -§ - - - - - - - -Add math functions gamma() and lgamma() (Dean Rasheed) -§ - - - - - - - -Allow "=>" syntax for named cursor arguments in plpgsql (Pavel Stehule) -§ - - - -We previously only accepted ":=". - - - - - - - -Allow regexp_match[es]/regexp_like/regexp_replace/regexp_count/regexp_instr/regexp_substr/regexp_split_to_table/regexp_split_to_array() to use named arguments (Jian He) -§ - - - - - - - - - <link linkend="libpq">libpq</link> - - - - - - - -Add function PQfullProtocolVersion() to report the full, including minor, protocol version number (Jacob Champion, Jelte Fennema-Nio) -§ - - - - - - - -Add libpq connection parameters and environment variables to specify the minimum and maximum acceptable protocol version for connections (Jelte Fennema-Nio) -§ -§ - - - - - - - -Add libpq function PQservice() to return the connection service name (Michael Banck) -§ - - - - - - - -Report search_path changes to the client (Alexander Kukushkin, Jelte Fennema-Nio, Tomas Vondra) -§ -§ - - - - - - - -Add PQtrace() output for all message types, including authentication (Jelte Fennema-Nio) -§ -§ -§ -§ -§ - - - - - - - -Add libpq connection parameter sslkeylogfile which dumps out SSL key material (Abhishek Chanda, Daniel Gustafsson) -§ - - - -This is useful for debugging. - - - - - - - -Modify some libpq function signatures to use int64_t (Thomas Munro) -§ - - - -These previously used pg_int64, which is now deprecated. - - - - - - - - - <xref linkend="app-psql"/> - - - - - - - -Allow psql to parse, bind, and close named prepared statements (Anthonin Bonnefoy, Michael Paquier) -§ - - - -This is accomplished with new commands \parse, \bind_named, and \close. - - - - - - - -Add psql backslash commands to allowing issuance of pipeline queries (Anthonin Bonnefoy) -§ -§ -§ - - - -The new commands are \startpipeline, \syncpipeline, \sendpipeline, \endpipeline, \flushrequest, \flush, and \getresults. - - - - - - - -Allow adding pipeline status to the psql prompt and add related state variables (Anthonin Bonnefoy) -§ - - - -The new prompt character is "%P" and the new psql variables are PIPELINE_SYNC_COUNT, PIPELINE_COMMAND_COUNT, and PIPELINE_RESULT_COUNT. - - - - - - - -Allow adding the connection service name to the psql prompt or access it via psql variable (Michael Banck) -§ - - - - - - - -Add psql option to use expanded mode on all list commands (Dean Rasheed) -§ - - - -Adding 'x' enables this. - - - - - - - -Change psql's \conninfo to use tabular format and include more information (Ãlvaro Herrera, Maiquel Grassi, Hunaid Sohail) -§ - - - - - - - -Add function's leakproof indicator to psql's \df+, \do+, \dAo+, and \dC+ outputs (Yugo Nagata) -§ - - - - - - - -Add access method details for partitioned relations in \dP+ (Justin Pryzby) -§ - - - - - - - -Add "default_version" to the psql \dx extension output (Magnus Hagander) -§ - - - - - - - -Add psql variable WATCH_INTERVAL to set the default \watch wait time (Daniel Gustafsson) -§ - - - - - - - - - Server Applications - - - - - - - -Change initdb to default to enabling checksums (Greg Sabino Mullane) -§ -§ - - - -The new initdb option --no-data-checksums disables checksums. - - - - - - - -Add initdb option --no-sync-data-files to avoid syncing heap/index files (Nathan Bossart) -§ - - - -initdb --no-sync is still available to avoid syncing any files. - - - - - - - -Add vacuumdb option --missing-stats-only to compute only missing optimizer statistics (Corey Huinker, Nathan Bossart) -§ -§ - - - -This option can only be used by --analyze-only and --analyze-in-stages. - - - - - - - -Add pg_combinebackup option -k/--link to enable hard linking (Israel Barth Rubio, Robert Haas) -§ - - - -Only some files can be hard linked. This should not be used if the backups will be used independently. - - - - - - - -Allow pg_verifybackup to verify tar-format backups (Amul Sul) -§ - - - - - - - -If pg_rewind's --source-server specifies a database name, use it in --write-recovery-conf output (Masahiko Sawada) -§ - - - - - - - -Add pg_resetwal option --char-signedness to change the default char signedness (Masahiko Sawada) -§ - - - - - - - - <link - linkend="app-pgdump"><application>pg_dump</application></link>/<link - linkend="app-pg-dumpall"><application>pg_dumpall</application></link>/<link - linkend="app-pgrestore"><application>pg_restore</application></link> - - - - - - - -Allow pg_dumpall to dump in the same output formats as pg_dump supports (Mahendra Singh Thalor, Andrew Dunstan) -§ - - - -Also modify pg_restore to handle such dumps. Previously pg_dumpall only supported text format. - - - - - - - -Add pg_dump options --with-schema, --with-data, and --with-statistics (Jeff Davis) -§ - - - - - - - -Add pg_dump and pg_dumpall option --sequence-data to dump sequence data that would normally be excluded (Nathan Bossart) -§ -§ - - - - - - - -Add pg_dump, pg_dumpall, and pg_restore options --statistics-only, --no-statistics, --no-data, and --no-schema (Corey Huinker, Jeff Davis) -§ - - - - - - - -Add option --no-policies to disable row level security policy processing in pg_dump, pg_dumpall, pg_restore (Nikolay Samokhvalov) -§ - - - -This is useful for migrating to systems with different policies. - - - - - - - - - <link linkend="pgupgrade"><application>pg_upgrade</application></link> - - - - - - - -Allow pg_upgrade to preserve optimizer statistics (Corey Huinker, Jeff Davis, Nathan Bossart) -§ -§ -§ -§ - - - -Extended statistics are not preserved. Also add pg_upgrade option --no-statistics to disable statistics preservation. - - - - - - - -Allow pg_upgrade to process database checks in parallel (Nathan Bossart) -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ -§ - - - -This is controlled by the existing --jobs option. - - - - - - - -Add pg_upgrade option --swap to swap directories rather than copy, clone, or link files (Nathan Bossart) -§ - - - -This mode is potentially the fastest. - - - - - - - -Add pg_upgrade option --set-char-signedness to set the default char signedness of new cluster (Masahiko Sawada) -§ -§ - - - -This is to handle cases where a pre-Postgres 18 cluster's default CPU signedness does not match the new cluster. - - - - - - - - - Logical Replication Applications> - - - - - - - -Add pg_createsubscriber option --all to create logical replicas for all databases (Shubham Khanna) -§ - - - - - - - -Add pg_createsubscriber option --remove to remove publications (Shubham Khanna) -§ - - - - - - - -Add pg_createsubscriber option --enable-two-phase to enable prepared transactions (Shubham Khanna) -§ - - - - - - - -Add pg_recvlogical option --failover to specify failover slots (Hayato Kuroda) -§ - - - - - - - -Allow pg_recvlogical --drop-slot to work without --dbname (Hayato Kuroda) -§ - - - - - - - - - - - Source Code - - - - - - - -Separate the loading and running of injection points (Michael Paquier, Heikki Linnakangas) -§ -§ - - - -Injection points can now be created, but not run, via INJECTION_POINT_LOAD(), and such injection points can be run via INJECTION_POINT_CACHED(). - - - - - - - -Support runtime arguments in injection points (Michael Paquier) -§ - - - - - - - -Allow inline injection point test code with IS_INJECTION_POINT_ATTACHED() (Heikki Linnakangas) -§ - - - - - - - -Improve the performance of processing long JSON strings using SIMD instructions (David Rowley) -§ - - - - - - - -Speed up CRC32C calculations using x86 AVX-512 instructions (Raghuveer Devulapalli, Paul Amonson) -§ - - - - - - - -Add ARM Neon and SVE CPU intrinsics for popcount (integer bit counting) (Chiranmoy Bhattacharya, Devanga Susmitha, Rama Malladi) -§ -§ - - - - - - - -Improve the speed of multiplication (Joel Jacobson, Dean Rasheed) -§ - - - - - - - -Add configure option --with-libnuma to enable NUMA awareness (Jakub Wartak, Bertrand Drouvot) -§ -§ -§ - - - -The function pg_numa_available() reports on NUMA awareness, and system views pg_shmem_allocations_numa and pg_buffercache_numa which report on shared memory distribution across -NUMA nodes. - - - - - - - -Add TOAST table to pg_index to allow for very large index expression indexes (Nathan Bossart) -§ - - - - - - - -Remove column pg_attribute.attcacheoff (David Rowley) -§ - - - - - - - -Add column pg_class.relallfrozen (Melanie Plageman) -§ - - - - - - - -Add amgettreeheight, amconsistentequality, and amconsistentordering to the index access method API (Mark Dilger) -§ -§ - - - - - - - -Add GiST support function stratnum (Paul A. Jungwirth) -§ - - - - - - - -Record the default CPU signedness of "char" in pg_controldata (Masahiko Sawada) -§ - - - - - - - -Add support for Python "Limited API" in PL/Python (Peter Eisentraut) -§ -§ - - - -This helps prevent problems caused by Python 3.x version mismatches. - - - - - - - -Change the minimum supported Python version to 3.6.8 (Jacob Champion) -§ - - - - - - - -Remove support for OpenSSL versions older than 1.1.1 (Daniel Gustafsson) -§ -§ - - - - - - - -If LLVM is enabled, require version 14 or later (Thomas Munro) -§ - - - - - - - -Add macro PG_MODULE_MAGIC_EXT to allow extensions to report their name and version (Andrei Lepikhov) -§ - - - -This information can be access via the new function pg_get_loaded_modules(). - - - - - - - -Document that SPI_connect/SPI_connect_ext() always returns success (SPI_OK_CONNECT) (Stepan Neretin) -§ - - - -Errors are always reported via ereport(). - - - - - - - -Remove the experimental designation of Meson builds on Windows (Aleksander Alekseev) -§ - - - - - - - -Add documentation section about API and ABI compatibility (David Wheeler, Peter Eisentraut) -§ - - - - - - - -Remove configure options --disable-spinlocks and --disable-atomics (Thomas Munro) -§ -§ - - - -Thirty-two bit atomic operations are now required. - - - - - - - -Remove support for the HPPA/PA-RISC architecture (Tom Lane) -§ - - - - - - - - - Additional Modules - - - - - - - -Add extension pg_logicalinspect to inspect logical snapshots (Bertrand Drouvot) -§ - - - - - - - -Add extension pg_overexplain which adds debug details to EXPLAIN output (Robert Haas) -§ - - - - - - - -Add output columns to postgres_fdw_get_connections() (Hayato Kuroda, Sagar Dilip Shedge) -§ -§ -§ -§ - - - -New output column "used_in_xact" indicates if the foreign data wrapper is being used by a current transaction, "closed" indicates if it is closed, "user_name" indicates the -user name, and "remote_backend_pid" indicates the remote backend process identifier. - - - - - - - -Allow SCRAM authentication from the client to be passed to postgres_fdw servers (Matheus Alcantara, Peter Eisentraut) -§ - - - -This avoids storing postgres_fdw authentication information in the database, and is enabled with the postgres_fdw "use_scram_passthrough" connection option. libpq uses new connection -parameters scram_client_key and scram_server_key. - - - - - - - -Allow SCRAM authentication from the client to be passed to dblink servers (Matheus Alcantara) -§ - - - - - - - -Add on_error and log_verbosity options to file_fdw (Atsushi Torikoshi) -§ - - - -These control how file_fdw handles and reports invalid file rows. - - - - - - - -Add "reject_limit" to control the number of invalid rows file_fdw can ignore (Atsushi Torikoshi) -§ - - - -This is active when ON_ERROR = 'ignore'. - - - - - - - -Add configurable variable min_password_length to passwordcheck (Emanuele Musella, Maurizio Boriani) -§ - - - -This controls the minimum password length. - - - - - - - -Have pgbench report the number of failed, retried, or skipped transactions in per-script reports (Yugo Nagata) -§ - - - - - - - -Add isn server variable "weak" to control invalid check digit acceptance (Viktor Holmberg) -§ - - - -This was previously only controlled by function isn_weak(). - - - - - - - -Allow values to be sorted to speed btree_gist index builds (Bernd Helmle, Andrey Borodin) -§ - - - - - - - -Add amcheck function gin_index_check() to verify GIN indexes (Grigory Kryachko, Heikki Linnakangas, Andrey Borodin) -§ - - - - - - - -Add functions pg_buffercache_evict_relation() and pg_buffercache_evict_all() to evict unpinned shared buffers (Nazir Bilal Yavuz) -§ - - - -The existing function pg_buffercache_evict() now returns the buffer flush status. - - - - - - - -Allow extensions to install custom EXPLAIN options (Robert Haas, Sami Imseih) -§ -§ -§ - - - - - - - -Allow extensions to use the server's cumulative statistics API (Michael Paquier) -§ -§ - - - - - - - <link linkend="pgstatstatements"><application>pg_stat_statements</application></link> - - - - - - - -Allow the queries of CREATE TABLE AS and DECLARE to be tracked by pg_stat_statements (Anthonin Bonnefoy) -§ - - - -They are also now assigned query ids. - - - - - - - -Allow the parameterization of SET values in pg_stat_statements (Greg Sabino Mullane, Michael Paquier) -§ - - - -This reduces the bloat caused by SET statements with differing constants. - - - - - - - -Add pg_stat_statements columns to report parallel activity (Guillaume Lelarge) -§ - - - -The new columns are parallel_workers_to_launch and parallel_workers_launched. - - - - - - - -Add pg_stat_statements.wal_buffers_full to report full WAL buffers (Bertrand Drouvot) -§ - - - - - - - - - <link linkend="pgcrypto"><application>pgcrypto</application></link> - - - - - - - -Add pgcrypto functions sha256crypt() and sha512crypt() (Bernd Helmle) -§ - - - - - - - -Add CFB mode to pgcrypto encryption and decryption (Umar Hayat) -§ - - - - - - - -Add pgcrypto server variable builtin_crypto_enabled to allow disabling builtin non-FIPS mode cryptographic functions (Daniel Gustafsson, Joe Conway) -§ - - - -This is useful for guaranteeing FIPS mode behavior. - - - - - - - - - - - - Acknowledgments - - - The following individuals (in alphabetical order) have contributed - to this release as patch authors, committers, reviewers, testers, - or reporters of issues. - - - - (to be completed) - - - - diff --git a/doc/src/sgml/release-19.sgml b/doc/src/sgml/release-19.sgml new file mode 100644 index 0000000000000..6e519a8fdf799 --- /dev/null +++ b/doc/src/sgml/release-19.sgml @@ -0,0 +1,3424 @@ + + + + + Release 19 + + + Release date: + 2026-??-??, AS OF 2026-04-13 + + + + Overview + + + PostgreSQL 19 contains many new features + and enhancements, including: + + + + + fill in later + + + + + The above items and other new features of + PostgreSQL 19 are explained in more detail + in the sections below. + + + + + + + Migration to Version 19 + + + A dump/restore using or use of + or logical replication is required for + those wishing to migrate data from any previous release. See for general information on migrating to new + major releases. + + + + Version 19 contains a number of changes that may affect compatibility + with previous releases. Observe the following incompatibilities: + + + + + + + +Add server variable password_expiration_warning_threshold to warn about password expiration (Gilles Darold, Nathan Bossart) +§ + + + +The default warning period is seven days. + + + + + + + +Issue a warning after successful MD5 password authentication (Nathan Bossart) +§ + + + +The warning can be disabled via server variable md5_password_warnings. MD5 passwords were marked marked as deprecated in Postgres 18. + + + + + + + +Remove RADIUS support (Thomas Munro) +§ + + + +Postgres only supported RADIUS over UDP, which is unfixably insecure. + + + + + + + +Force standard_conforming_strings to always be "on" in the database server (Tom Lane) +§ + + + +Server variable escape_string_warning has been removed as unnecessary. Client applications still support "standard_conforming_strings = off" for compatibility with old servers. + + + + + + + +Prevent carriage returns and line feeds in database, role, and tablespace names (Mahendra Singh Thalor) +§ + + + +This was changed to avoid security problems. pg_upgrade will also disallow upgrading from clusters that use such names. + + + + + + + +Change the default index opclasses for inet and cidr data types from btree_gist to GiST (Tom Lane) +§ +§ + + + +The btree_gist inet/cidr opclasses are broken because they can exclude rows that should be returned. Pg_upgrade will fail to upgrade if btree_gist inet/cidr indexes exist in the old +server. + + + + + + + +Stop reordering non-schema objects created by CREATE SCHEMA (Tom Lane, Jian He) +§ +§ + + + +The goal of the reordering was to avoid dependencies, but it was imperfect. Postgres now uses the specified object ordering, except for foreign keys which are created last. + + + + + + + +Disallow system columns from being used in COPY FROM ... WHERE (Tom Lane) +§ + + + +The values of such columns were not well-defined. + + + + + + + +Cause transactions to pass their READ ONLY and DEFERRABLE status to postgres_fdw sessions (Etsuro Fujita) +§ + + + +This means READ ONLY transactions can no longer modify rows processed by postgres_fdw sessions. + + + + + + + +Change default of max_locks_per_transactions from 64 to 128 (Heikki Linnakangas) +§ + + + +Lock size allocation has changed, so effectively settings must now be doubled to match their capacity in previous releases. + + + + + + + +Change JIT to be disabled by default (Jelte Fennema-Nio) +§ + + + +Previously JIT was enabled by default, and activated based on optimizer costs. Unfortunately, this costing has been determined to be unreliable, so require sites that are doing many +large analytical queries to manually enable JIT. + + + + + + + +Rename wait event type BUFFERPIN to BUFFER (Andres Freund) +§ + + + + + + + +Change index access method handlers to use a static IndexAmRoutines structure, rather than dynamically allocated ones (Matthias van de Meent) +§ + + + +This is a backwardly incompatible. + + + + + + + +Remove optimizer hook get_relation_info_hook and add better-placed hook build_simple_rel_hook (Robert Haas) +§ + + + + + + + +Remove MULE_INTERNAL encoding (Thomas Munro) +§ + + + +This encoding was complex and rarely used. Databases using it will need to be dumped and restored with a different encoding. + + + + + + + + + Changes + + + Below you will find a detailed account of the changes between + PostgreSQL 19 and the previous major + release. + + + + Server + + + Optimizer + + + + + + +Allow NOT INs to be converted to more efficient ANTI JOINs when NULLs are not present (Richard Guo) +§ + + + + + + + +Allow more LEFT JOINs to be converted to ANTI JOINs (Tender Wang, Richard Guo) +§ + + + + + + + +Allow use of Memoize for ANTI JOINS with unique inner sides (Richard Guo) +§ + + + + + + + +Improve the planning of semijoins (Richard Guo) +§ + + + + + + + +Improve hash join's handling of tuples with NULL join keys (Tom Lane) +§ + + + + + + + +Convert IS [NOT] DISTINCT FROM NULL to IS [NOT] NULL during constant folding (Richard Guo) +§ + + + +The latter form is more easily optimized. + + + + + + + +Perform earlier constant folding of "Var IS [NOT] NULL" in the optimizer (Richard Guo) +§ + + + +This allows for later optimizations. + + + + + + + +Allow Append and MergeAppend to consider explicit incremental sorts (Richard Guo) +§ + + + + + + + +Allow some aggregate processing to be performed before joins (Richard Guo, Antonin Houska) +§ +§ +§ + + + +This can reduce the number of rows needed to be processed. + + + + + + + +Allow negative values of pg_aggregate.aggtransspace to indicate unbounded memory usage (Richard Guo) +§ + + + +This information is used by the optimizer in planning memory usage. + + + + + + + +Simplify IS [NOT] TRUE/FALSE/UNKNOWN to plain boolean expressions when the input is proven non-nullable (Richard Guo) +§ + + + + + + + +Simplify COALESCE and ROW(...) IS [NOT] NULL to avoid evaluating unnecessary arguments (Richard Guo) +§ +§ + + + + + + + +Simplify IS [NOT] DISTINCT FROM to equality/inequality operators when inputs are proven non-nullable (Richard Guo) +§ + + + + + + + +Speed up join selectivity computations for large optimizer statistics targets (Ilia Evdokimov, David Geier) +§ + + + + + + + +Enable proper optimizer statistics for functions returning boolean values (Tom Lane) +§ + + + + + + + +Allow extended statistics on virtual generated columns (Yugo Nagata) +§ + + + + + + + +Allow function pg_restore_extended_stats() to restore optimizer extended statistics (Corey Huinker, Michael Paquier, Chao Li) +§ +§ +§ +§ + + + + + + + +Add function pg_clear_extended_stats() to remove extended statistics (Corey Huinker, Michael Paquier) +§ + + + + + + + +Adjust the optimizer to consider startup costs of partial paths (Robert Haas, Tomas Vondra) +§ + + + + + + + + + General Performance + + + + + + +Improve performance of foreign key constraint checks (Junwang Zhao, Amit Langote, Chao Li) +§ +§ +§ +§ + + + + + + + +Improve asynchronous I/O read-ahead scheduling for large requests (Andres Freund) +§ +§ +§ + + + + + + + +Allow io_method method "worker" to automatically control needed background workers (Thomas Munro) +§ + + + +New server variables are io_min_workers, io_max_workers, io_worker_idle_timeout, and io_worker_launch_interval. + + + + + + + +Allow query table scans to mark pages as all-visible in the visibility map (Melanie Plageman) +§ + + + +Previously only VACUUM and COPY FREEZE could do this. + + + + + + + +Allow autovacuum to use parallel vacuum workers (Daniil Davydov) +§ +§ + + + +This is enabled via server variable autovacuum_max_parallel_workers and per-table storage parameter autovacuum_parallel_workers. + + + + + + + +Allow TID Range Scans to be parallelized (Cary Huang, David Rowley) +§ + + + + + + + +Improve COPY FROM performance for text and CSV output using SIMD CPU instructions (Nazir Bilal Yavuz, Shinya Kato) +§ + + + + + + + +Improve NOTIFY to only wake up backends that are listening to specified notifications (Joel Jacobson) +§ + + + +Previously most backends were woken by NOTIFY. + + + + + + + +Allow the addition of columns based on domains containing constraints to usually avoid a table rewrite (Jian He) +§ + + + +Previously this always required a table rewrite. + + + + + + + +Change the default TOAST compression method from pglz to the more efficient lz4 (Euler Taveira) +§ + + + +This is done by changing the default for server variable default_toast_compression. + + + + + + + +Improve performance of internal row deformation (David Rowley) +§ + + + + + + + +Improve performance of hash index bulk-deletion by using streaming reads (Xuneng Zhou) +§ + + + + + + + +Improve sort performance using radix sorts (John Naylor) +§ + + + + + + + +Improve timing performance measurements (Lukas Fittl, Andres Freund, David Geier) +§ +§ + + + +This benefits EXPLAIN (ANALYZE, TIMING) and pg_test_timing, and is controlled via server variable timing_clock_source. + + + + + + + +Optimize plpgsql syntax SELECT simple-expression INTO var (Tom Lane) +§ + + + + + + + +Improve performance of numeric operations on platforms without 128-bit integer support (Dean Rasheed) +§ + + + + + + + + + System Views + + + + + + +Add system view pg_stat_lock and function pg_stat_get_lock() to report per-lock type statistics (Bertrand Drouvot) +§ + + + + + + + +Add system view pg_stat_recovery to report recovery status (Xuneng Zhou, Shinya Kato) +§ +§ + + + + + + + +Add mem_exceeded_count column to system view pg_stat_replication_slots (Bertrand Drouvot) +§ + + + +This reports the number of times that logical_decoding_work_mem was exceeded. + + + + + + + +Add stats_reset column to system views pg_stat_all_tables, pg_stat_all_indexes, and pg_statio_all_sequences (Bertrand Drouvot, Sami Imseih, Shihao Zhong) +§ + + + +It also appears in the "sys" and "user" view variants. + + + + + + + +Add stats_reset column to system views pg_stat_user_functions and pg_stat_database_conflicts (Bertrand Drouvot, Shihao Zhong) +§ +§ + + + + + + + +Add system view pg_stat_autovacuum_scores to report per-table autovacuum details (Sami Imseih) +§ + + + + + + + +Add vacuum initiation details to system view pg_stat_progress_vacuum (Shinya Kato) +§ + + + +The new "started_by" column reports the initiator of the vacuum, and "mode" indicates its aggressiveness. + + + + + + + +Add analyze initiation details to system view pg_stat_progress_analyze (Shinya Kato) +§ + + + +The new "started_by" column reports the initiator of the analyze. + + + + + + + +Add a column to system view pg_stat_progress_basebackup to report the type of backup (Shinya Kato) +§ + + + +Possible values are "full" or "incremental". + + + + + + + +Add reporting of the bytes written to WAL for full page images (Shinya Kato) +§ + + + +This is accessible via system view pg_stat_wal and function pg_stat_get_backend_wal(). + + + + + + + +Add "connecting" status to system view column pg_stat_wal_receiver.status (Xuneng Zhou) +§ + + + + + + + +Add columns to system views pg_stats, pg_stats_ext, and pg_stats_ext_exprs (Corey Huinker) +§ + + + +Adds table OID and attribute number columns to pg_stats, and table OID and statistics object OID columns to the other two. + + + + + + + +Add information about range type extended statistics to system view pg_stats_ext_exprs (Corey Huinker, Michael Paquier) +§ + + + + + + + +Add system view pg_dsm_registry_allocations to report dynamic shared memory details (Florents Tselai, Nathan Bossart) +§ +§ + + + + + + + +Add column "location" to system views pg_available_extensions and pg_available_extension_versions to report the file system directory of extensions (Matheus Alcantara) +§ + + + + + + + + Monitoring + + + + + + +Allow log_min_messages log levels to be specified by process type (Euler Taveira) +§ + + + +The new format is "type:level". A value without a colon controls unspecified process types, enabling backward compatibility. + + + + + + + +Add server variable log_autoanalyze_min_duration to log long-running autoanalyze operations (Shinya Kato) +§ + + + +Server variable log_autovacuum_min_duration now only controls logging of automatic vacuum operations. + + + + + + + +Enable server variable log_lock_waits by default (Laurenz Albe) +§ + + + + + + + +Add server variable debug_print_raw_parse to log the raw parse tree (Chao Li) +§ + + + +This is also enabled when the server is started with debug level 3 and higher. + + + + + + + +Make messages coming from remote servers appear in the server logs in the same format as local server messages (Vignesh C) +§ + + + +These include replication, postgres_fdw, and dblink servers. + + + + + + + +Add WAL full page write bytes reporting to VACUUM and ANALYZE logging (Shinya Kato) +§ + + + + + + + +Add IO wait events for COPY FROM/TO on a pipe/file/program (Nikolay Samokhvalov) +§ + + + + + + + +Add wait events for WAL write and flush LSNs (Xuneng Zhou) +§ + + + + + + + +Have pg_get_sequence_data function return the sequence page LSN (Vignesh C) +§ + + + + + + + +Add function pg_get_multixact_stats() to report multixact activity (Naga Appani) +§ + + + + + + + +Issue warnings when the wraparound of xid and multi-xids is less then 100 million (Nathan Bossart) +§ + + + +The previous warning was 40 million. Warnings are issued to clients and the server log. + + + + + + + + + Server Configuration + + + + + + +Allow online enabling and disabling of data checksums (Daniel Gustafsson, Magnus Hagander, Tomas Vondra) +§ +§ + + + +Previously the checksum status could only be set at initialization and changed only while the cluster was offline using pg_checksums. + + + + + + + +Add scoring system to control the order that tables are autovacuumed (Nathan Bossart) +§ + + + +The new server variables are autovacuum_freeze_score_weight, autovacuum_multixact_freeze_score_weight, autovacuum_vacuum_score_weight, autovacuum_vacuum_insert_score_weight, and +autovacuum_analyze_score_weight. + + + + + + + +Add server-side report for SNI (Server Name Indication) (Daniel Gustafsson, Jacob Champion) +§ + + + +New configuration file PGDATA/pg_hosts.conf specifies hostname/key pairs. + + + + + + + +Add a new OAUTH flow hook PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 (Jacob Champion) +§ +§ + + + +This is an improved version of PQAUTHDATA_OAUTH_BEARER_TOKEN by adding the issuer identifier and error message specification. + + + + + + + +Allow background workers to be configured to terminate before database-level operations (Aya Iwata) +§ + + + + + + + +Allow server variables that represent lists to be emptied by setting the value to NULL (Tom Lane) +§ + + + + + + + +Update GB18030 encoding from version 2000 to 2022 (Chao Li, Zheng Tao) +§ + + + +See the commit message for compatibility details. + + + + + + + + + Streaming Replication and Recovery + + + + + + +Allow standbys to wait for LSN values to be replayed via WAIT FOR (Kartyshov Ivan, Alexander Korotkov, Xuneng Zhou) +§ +§ + + + + + + + +Improve function pg_sync_replication_slots() to wait for the synchronization completion (Ajin Cherian, Zhijie Hou) +§ + + + +Previously, certain synchronization failures would not be reported. + + + + + + + +Add server variable wal_sender_shutdown_timeout to limit replica synchronization waits during shutdown (Andrey Silitskiy, Hayato Kuroda) +§ + + + +By default, senders still wait forever for synchronization. + + + + + + + +Allow wal_receiver_timeout to be set per subscription and user (Fujii Masao) +§ +§ + + + +This allows subscriptions to use different wal_receiver_timeout values. + + + + + + + +Add optional pid parameter to pg_replication_origin_session_setup() to allow parallelization of SQL-level replication solutions (Doruk Yilmaz, Hayato Kuroda) +§ + + + + + + + + + <link linkend="logical-replication">Logical Replication</link> + + + + + + +Allow sequence values stored in subscribers to match the publisher (Vignesh C) +§ +§ +§ + + + +This is enabled during CREATE SUBSCRIPTION, ALTER SUBSCRIPTION ... REFRESH PUBLICATION, and ALTER SUBSCRIPTION ... REFRESH SEQUENCES. The latter only updates values, not sequence +existence. Function pg_get_sequence_data() allows inspection of sequence synchronization. + + + + + + + +Allow publications to publish all sequences via the ALL SEQUENCES clause (Vignesh C, Tomas Vondra) +§ + + + + + + + +Enhance ALTER SUBSCRIPTION on publications to synchronize the existence of sequences on subscribers to match the publisher (Vignesh C) +§ + + + + + + + +Allow CREATE/ALTER PUBLICATION to exclude some tables using the EXCEPT clause (Vignesh C, Shlok Kyal) +§ +§ +§ +§ + + + +This is useful when specifying ALL TABLES. + + + + + + + +Allow CREATE SUBSCRIPTION to use postgres_fdw foreign data wrapper connection parameters (Jeff Davis) +§ + + + +The connection parameters are referenced via CREATE SUBSCRIPTION ... SERVER. + + + + + + + +When server variable wal_level is "replica", allow the automatic enablement of logical replication when needed (Masahiko Sawada) +§ + + + +New server variable effective_wal_level reports the effective WAL level. + + + + + + + +Add logical subscriber setting retain_conflict_info to retain information needed for conflict resolution (Zhijie Hou) +§ + + + + + + + +Report cases where an update is applied to a row that was already deleted on a subscriber (Zhijie Hou) +§ + + + +This requires the subscriber have retain_dead_tuples enabled. + + + + + + + +Re-enable retain_dead_tuples when the necessary transaction retention falls below max_retention_duration (Zhijie Hou) +§ + + + + + + + +Add subscription option max_retention_duration to limit retain_dead_tuples retention (Zhijie Hou) +§ + + + +When the limit is reached, dead tuple retention until manually re-enabled or a new subscription is created. + + + + + + + +Add column pg_stat_subscription_stats.sync_seq_error_count to report sequence synchronization errors (Vignesh C) +§ +§ + + + + + + + +Rename column sync_error_count to sync_table_error_count in system view pg_stat_subscription_stats (Vignesh C) +§ + + + +This is necessary since sequences errors are now also tracked. + + + + + + + +Add slot synchronization skip information to pg_stat_replication_slots (Shlok Kyal) +§ +§ +§ + + + +The new columns are slotsync_skip_count, slotsync_last_skip, and slotsync_skip_reason. + + + + + + + + + + + Query Commands + + + + + + +Add support for SQL Property Graph Queries (SQL/PGQ) (Peter Eisentraut, Ashutosh Bapat) +§ +§ +§ + + + +Internally these are processed like views so are written as standard relational queries. + + + + + + + +Add UPDATE/DELETE FOR PORTION OF (Paul A. Jungwirth) +§ +§ + + + +This allows operations on a temporal range. + + + + + + + +Add GROUP BY ALL syntax to automatically group all non-aggregate and non-window function target list parameters (David Christensen) +§ + + + + + + + +Allow GROUP BY to process target list subqueries that have expressions referencing non-subquery columns (Tom Lane) +§ + + + +Also fix a bug in how GROUPING() handles target list subquery aliases. + + + + + + + +Allow window functions to ignore NULLs with IGNORE NULLS/RESPECT NULLS option (Oliver Ford, Tatsuo Ishii) +§ + + + +Supported window functions are lead, lag, first_value, last_value and nth_value. + + + + + + + +Add support for INSERT ... ON CONFLICT DO SELECT ... RETURNING (Andreas Karlsson, Marko Tiikkaja, Viktor Holmberg) +§ + + + +This allows conflicting rows to be returned, and optionally locked with FOR UPDATE/SHARE. + + + + + + + + + Utility Commands + + + + + + +Create a REPACK command that replaces VACUUM FULL and CLUSTER (Antonin Houska) +§ + + + +The two former commands did similar things, but with confusing names, so unify them as REPACK. + + + + + + + +Allow REPACK to rebuild tables without access-exclusive locking (Antonin Houska, Mihail Nikalayeu, Ãlvaro Herrera) +§ +§ +§ + + + +This is enabled via the CONCURRENTLY option. Server variables max_repack_replication_slots was also added. + + + + + + + +Allow partitions to be merged and split using ALTER TABLE ... MERGE/SPLIT PARTITIONS (Dmitry Koval, Alexander Korotkov, Tender Wang, Richard Guo, Dagfinn Ilmari MannsÃ¥ker, Fujii Masao, Jian He) +§ +§ + + + + + + + +Allow GRANT/REVOKE to specify the effective role performing the privileges adjustment (Nathan Bossart, Tom Lane) +§ + + + +The GRANTED BY clause controls this. + + + + + + + +Allow CREATE SCHEMA to create more types of non-schema objects (Kirill Reshke, Jian He, Tom Lane) +§ + + + + + + + +Allow CHECKPOINT to accept a list of options (Christoph Berg) +§ +§ +§ + + + +Supported options are MODE and FLUSH_UNLOGGED. + + + + + + + +Add CONNECTION clause to CREATE FOREIGN DATA WRAPPER to specify a function to be called for subscription connection parameters (Jeff Davis, Noriyoshi Shinoda) +§ +§ + + + + + + + +Add memory usage and parallelism reporting to VACUUM (VERBOSE) and autovacuum logs (Tatsuya Kawata, Daniil Davydov) +§ +§ + + + + + + + <link linkend="ddl-constraints">Constraints</link> + + + + + + +Allow ALTER TABLE ALTER CONSTRAINT ... [NOT] ENFORCED for CHECK constraints (Jian He) +§ + + + +Previously enforcement changes were only supported for foreign key constraints. + + + + + + + +Allow ALTER COLUMN SET EXPRESSION to succeed on virtual columns with CHECK constraints (Jian He) +§ + + + +This was previously prohibited. + + + + + + + +Reduce lock level of ALTER DOMAIN ... VALIDATE CONSTRAINT to match ALTER TABLE ... VALIDATE CONSTRAINT (Jian He) +§ + + + + + + + + <xref linkend="sql-copy"/> + + + + + + +Allow multiple headers lines be skipped by COPY FROM (Shinya Kato, Fujii Masao) +§ + + + +Previously only a single header line could be skipped. + + + + + + + +Allow COPY FROM to set invalid input values to NULL (Jian He, Kirill Reshke) +§ + + + +This is done using the COPY option ON_ERROR SET_NULL. + + + + + + + +Allow COPY TO to output JSON format (Joe Conway, Jian He, Andrew Dunstan) +§ + + + + + + + +Allow COPY TO in JSON format to output its results as a single JSON array (Joe Conway, Jian He) +§ + + + +The COPY option is FORCE_ARRAY. + + + + + + + +Allow COPY TO to output partitioned tables (Jian He, Ajin Cherian) +§ +§ + + + +Previously COPY (SELECT ...) had to be used to output partitioned tables. This also improves logical replication table synchronization. + + + + + + + + <xref linkend="sql-explain"/> + + + + + + +Add EXPLAIN ANALYZE option IO to report asynchronous IO activity (Tomas Vondra) +§ +§ +§ + + + + + + + +Add WAL full page write bytes reporting to EXPLAIN (ANALYZE, WAL) (Shinya Kato) +§ + + + + + + + +Add Memoize cache and lookup estimates to EXPLAIN output (Ilia Evdokimov, Lukas Fittl) +§ + + + +This will help illustrate why Memoize was chosen. + + + + + + + + + + + Data Types + + + + + + +Add the 64-bit unsigned data type oid8 (Michael Paquier) +§ + + + + + + + +Add more jsonpath string methods (Florents Tselai, David E. Wheeler) +§ + + + +They are l/r/btrim(), lower(), upper(), initcap(), replace(), and split_part(). These are immutable like their non-JSON string variants. + + + + + + + +Allow casts between bytea and uuid data types (Dagfinn Ilmari MannsÃ¥ker, Aleksander Alekseev) +§ + + + + + + + +Add ability to cast between database names and oids using regdatabase (Ian Lawrence Barwick) +§ + + + + + + + +Add functions tid_block() and tid_offset() to extract block numbers and offsets from tid values (Ayush Tiwari) +§ + + + + + + + + + Functions + + + + + + +Add date, timestamp, and timestamptz versions of random(min, max) (Damien Clochard, Dean Rasheed) +§ +§ + + + + + + + +Allow encode() and decode() to process data in base64url and base32hex formats (Andrey Borodin, Aleksander Alekseev, Florents Tselai) +§ +§ +§ + + + +This format retains ordering, unlike base32. + + + + + + + +Add functions to return a set of ranges resulting from range subtraction (Paul A. Jungwirth) +§ + + + +The functions are range_minus_multi() and multirange_minus_multi(). This is useful to represent range subtractions results with gaps. + + + + + + + +Add function error_on_null() to return the supplied parameter, or error on NULL input (Joel Jacobson) +§ + + + + + + + +Allow IS JSON to work on domains defined over supported base types (Jian He) +§ + + + +The supported base domains are TEXT, JSON, JSONB, and BYTEA. + + + + + + + +Add full text stemmers for Polish and Esperanto (Tom Lane) +§ + + + +The Dutch stemmer has also been updated. The old Dutch stemmer is available via "dutch_porter". + + + + + + + +Modify pg_read_all_data() and pg_write_all_data() to read/write large objects (Nitin Motiani, Nathan Bossart) +§ + + + +These functions are designed to allow non-super users to run pg_dump. + + + + + + + +Add function pg_get_role_ddl() to output role creation commands (Mario Gonzalez, Bryan Green, Andrew Dunstan, Euler Taveira) +§ + + + + + + + +Add function pg_get_tablespace_ddl() to output tablespace creation commands (Nishant Sharma, Manni Wood, Andrew Dunstan, Euler Taveira) +§ + + + + + + + +Add function pg_get_database_ddl() to output database creation commands (Akshay Joshi, Andrew Dunstan, Euler Taveira) +§ + + + + + + + +Allow event triggers to be written using PL/Python (Euler Taveira, Dimitri Fontaine) +§ + + + + + + + + + <link linkend="libpq">Libpq</link> + + + + + + +Allow libpq connections to specify a service file via "servicefile" (Torsten Förtsch, Ryo Kanbayashi) +§ + + + + + + + +Add special libpq protocol version 3.9999 for version testing (Jelte Fennema-Nio) +§ + + + + + + + +Add libpq function PQgetThreadLock() to retrieve the current locking callback (Jacob Champion) +§ + + + + + + + +Add libpq connection setting oauth_ca_file to specify the OAUTH certificate authority file (Jonathan Gonzalez V., Jacob Champion) +§ + + + +This can also be set via the PGOAUTHCAFILE environment variable. The default is to use curl's built-in certificates. + + + + + + + +Allow custom OAUTH validators to register custom pg_hba.conf authentication options (Jacob Champion) +§ + + + + + + + +Allow OAUTH validators to supply failure details (Jacob Champion) +§ + + + +This is done by setting the ValidatorModuleResult structure member error_detail. + + + + + + + +Allow libpq environment variable PGOAUTHDEBUG to specify specific debug options (Zsolt Parragi, Jacob Champion) +§ + + + +The UNSAFE option still generates all debugging output. + + + + + + + + + <xref linkend="app-psql"/> + + + + + + +Allow the search path to appear in the psql prompt via "%S" (Florents Tselai) +§ + + + +This works when psql is connected to Postgres 18 or later. + + + + + + + +Allow the hot standby status to appear in the psql prompt via "%i" (Jim Jones) +§ + + + + + + + +Modify psql backslash commands to show comments for publications, subscriptions, and extended statistics (Fujii Masao, Jim Jones) +§ + + + +The modified commands are \dRp+, \dRs+, and \dX+. + + + + + + + +Allow control over how booleans are displayed in psql (David G. Johnston) +§ + + + +The \pset variables are display_true and display_false. + + + + + + + +Add psql variable SERVICEFILE to reference the service file location (Ryo Kanbayashi) +§ + + + + + + + +Allow psql to more accurately determine if the pager is needed (Erik Wienhold) +§ + + + + + + + +Add or improve psql tab completion (Yamaguchi Atsuo, Yugo Nagata, Haruna Miwa, Xuneng Zhou, Yugo Nagata, Dagfinn Ilmari MannsÃ¥ker, Fujii Masao, Ãlvaro Herrera, Jian He, Fujii Masao, +Tatsuya Kawata, Ian Lawrence Barwick, Vasuki M) +§ +§ +§ +§ +§ +§ +§ +§ +§ +§ +§ +§ +§ +§ + + + + + + + + + Server Applications + + + + + + +Change vacuumdb's --analyze-only option to analyze partitioned tables when no targets are specified (Laurenz Albe, Mircea Cadariu) +§ + + + +Previously it skipped partitioned tables. This now matches the behavior of ANALYZE. + + + + + + + +Allow vacuumdb to report its commands without running them using option --dry-run (Corey Huinker) +§ + + + + + + + +Allow pg_verifybackup to read WAL files stored in tar archives (Amul Sul) +§ + + + +Add option --wal-path as an alias for the existing and deprecated --wal-directory option. + + + + + + + +Allow pg_waldump to read WAL files stored in tar archives (Amul Sul) +§ + + + + + + + +Add pgbench option --continue-on-error to continue after SQL errors (Rintaro Ikeda, Yugo Nagata, Fujii Masao) +§ + + + + + + + +Improve the usability of pg_test_timing (Hannu Krosing, Tom Lane) +§ +§ + + + +Report nanoseconds instead of microseconds. In addition to histogram output, output a second table that reports exact timings, with an optional cutoff set by --cutoff. + + + + + + + <link + linkend="app-pgdump"><application>pg_dump</application></link>/<link + linkend="app-pg-dumpall"><application>pg_dumpall</application></link>/<link + linkend="app-pgrestore"><application>pg_restore</application></link> + + + + + + +Allow pg_dumpall to product output in non-text formats (Mahendra Singh Thalor, Andrew Dunstan) +§ +§ + + + +The new output formats are custom, directory, or tar. + + + + + + + +Allow pg_dump to include restorable extended statistics (Corey Huinker) +§ + + + + + + + + + <xref linkend="pgupgrade"/> + + + + + + +Have pg_upgrade copy large object metadata files rather than use COPY (Nathan Bossart) +§ +§ + + + +This is possible when upgrading from Postgres 16 and later. + + + + + + + +Allow pg_upgrade to use COPY for large object metadata (Nathan Bossart) +§ + + + +This is used when upgrading from Postgres major versions 12-15. + + + + + + + +Improve pg_upgrade performance when restoring large object metadata for origin servers version 11 and earlier (Nathan Bossart) +§ + + + + + + + +Allow pg_upgrade to process non-default tablespaces stored in the PGDATA directory (Nathan Bossart) +§ + + + +Previously such tablespaces generated an error. + + + + + + + + + Logical Replication Applications + + + + + + +Allow pg_createsubscriber to ignore specified publications that already exist (Shubham Khanna) +§ + + + +Previously this generated an error. + + + + + + + +Change the way pg_createsubscriber stores recovery parameters (Alyona Vinter) +§ + + + +Changes are stored in optionally-included pg_createsubscriber.conf rather than directly in postgresql.auto.conf. + + + + + + + +Add pg_createsubscriber option -l/--logdir to redirect output to files (Gyan Sreejith, Hayato Kuroda) +§ + + + + + + + + + + + Source Code + + + + + + +Restore support for AIX (Aditya Kamath, Srirama Kucherlapati, Peter Eisentraut) +§ +§ + + + +This uses gcc and only supports 64-bit builds. + + + + + + + +Change Solaris to use unnamed POSIX semaphores (Tom Lane) +§ + + + +Previously it used System V semaphores. + + + + + + + +Require Visual Studio 2019 or later (Peter Eisentraut) +§ + + + + + + + +Allow MSVC to create PL/Python using the Python Limited API (Bryan Green) +§ + + + + + + + +Allow building on AArch64 using MSVC (Niyas Sait, Greg Burd, Dave Cramer) +§ + + + + + + + +Allow execution stack backtraces on Windows using DbgHelp (Bryan Green) +§ + + + + + + + +Change the supported C language version to C11 (Peter Eisentraut) +§ +§ + + + +Previously C99 was used. + + + + + + + +Use standard C23 and C++ attributes if available (Peter Eisentraut) +§ + + + + + + + +Allow C++ compiler mode to be used with ICU (John Naylor) +§ + + + + + + + +Optionally use AVX2 CPU instructions for calculating page checksums (Matthew Sterrett, Andrew Kim) +§ + + + + + + + +Optionally use ARM Crypto Extension to Compute CRC32C (John Naylor) +§ + + + + + + + +Change hex_encode() and hex_decode() to use SIMD CPU instructions (Nathan Bossart, Chiranmoy Bhattacharya) +§ + + + + + + + +Require Meson version 0.57.2 or later (Peter Eisentraut) +§ + + + + + + + +Add Meson option to build both shared and static libraries, or only shared (Peter Eisentraut) +§ + + + + + + + +Update Unicode data to version 17.0.0 (Peter Eisentraut) +§ + + + + + + + +Add hooks planner_setup_hook and planner_shutdown_hook (Robert Haas) +§ + + + + + + + +Allow extensions to replace set-returning functions in the FROM clause with SQL queries (Paul A. Jungwirth) +§ + + + + + + + +Make multixid members 64-bit (Maxim Orlov) +§ + + + + + + + +Add fake LSN support to hash index AM (Peter Geoghegan) +§ + + + + + + + +Change FDW function prototypes to use uint* instead of bit* typedefs (Nathan Bossart) +§ + + + + + + + +Allow logical decoding plugins to specify if they do not access shared catalogs (Antonin Houska) +§ + + + + + + + +Add simplified and improved shared memory registration function ShmemRequestStruct (Heikki Linnakangas, Ashutosh Bapat) +§ + + + +Functions ShmemInitStruct() and ShmemInitHash() remain for backward compatibility. + + + + + + + +Add server variable debug_exec_backend to report how parameters are passed to new backends (Daniel Gustafsson) +§ + + + + + + + +Document the environment variables that control the regression tests (Michael Paquier) +§ + + + + + + + +Add documentation section about temporal tables (Paul A. Jungwirth) +§ + + + + + + + +Update documented systemd example to include a restart setting (Andrew Jackson) +§ + + + + + + + + + Additional Modules + + + + + + +Add pg_plan_advice module to stabilize and control planner decisions (Robert Haas) +§ +§ + + + + + + + +Add extension pg_stash_advice to allow per-query-id advice to be specified (Robert Haas, Lukas Fittl) +§ +§ + + + + + + + +Refactor pg_buffercache reporting of shared memory mapping (Bertrand Drouvot) +§ + + + +New function pg_buffercache_os_pages() and system view pg_buffercache_os_pages allow reporting of shared memory mapping; the function optionally includes NUMA details. Function +pg_buffercache_numa_pages() remains for backward compatibility. + + + + + + + +Add functions to pg_buffercache to mark buffers as dirty (Nazir Bilal Yavuz) +§ + + + +The functions are pg_buffercache_mark_dirty(), pg_buffercache_mark_dirty_relation(), and pg_buffercache_mark_dirty_all(). + + + + + + + +Allow pushdown of array comparisons in prepared statements to postgres_fdw foreign servers (Alexander Pyhalov) +§ + + + + + + + +Allow the retrieval of statistics from foreign data wrapper servers (Corey Huinker, Etsuro Fujita) +§ + + + +This is enabled for postgres_fdw by using the option restore_stats. The default is for ANALYZE to retrieve rows from the remote server to locally generate statistics. + + + + + + + +Allow file_fdw to read files or program output that uses multi-line headers (Shinya Kato) +§ + + + + + + + +Add server variable auto_explain.log_io to add IO reporting to auto_explain (Tomas Vondra) +§ + + + + + + + +Allow auto_explain to add extension-specific EXPLAIN options via server variable auto_explain.log_extension_options (Robert Haas) +§ + + + + + + + +Allow btree_gin to match partial qualifications (Tom Lane) +§ +§ + + + + + + + +Improve performance of bloom indexes by using streaming reads (Xuneng Zhou) +§ +§ + + + + + + + +Improve performance of pgstattuple by using streaming reads (Xuneng Zhou) +§ + + + + + + + +Allow fuzzystrmatch's dmetaphone to use single-byte encodings beyond ASCII (Peter Eisentraut) +§ + + + + + + + +Modify oid2name --extended to report the relation file path (David Bidoc) +§ + + + + + + + <xref linkend="pgstatstatements"/> + + + + + + +Show sizes of FETCH queries as constants in pg_stat_statements (Sami Imseih) +§ + + + +Fetches of different sizes will now be grouped together in pg_stat_statements output. + + + + + + + +Add generic and custom plans counts to pg_stat_statements (Sami Imseih) +§ + + + + + + + + + + + + + Acknowledgments + + + The following individuals (in alphabetical order) have contributed + to this release as patch authors, committers, reviewers, testers, + or reporters of issues. + + + + fill in later + + + + diff --git a/doc/src/sgml/release.sgml b/doc/src/sgml/release.sgml index cee577ff8d353..a659d382db95c 100644 --- a/doc/src/sgml/release.sgml +++ b/doc/src/sgml/release.sgml @@ -70,7 +70,7 @@ For new features, add links to the documentation sections. All the active branches have to be edited concurrently when doing that. --> -&release-18; +&release-19; Prior Releases diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml index bbeac84d46adf..0d2c1721ff372 100644 --- a/doc/src/sgml/rowtypes.sgml +++ b/doc/src/sgml/rowtypes.sgml @@ -502,30 +502,6 @@ SELECT c.somefunc FROM inventory_item c; embedded in field values will be doubled. - - - Remember that what you write in an SQL command will first be interpreted - as a string literal, and then as a composite. This doubles the number of - backslashes you need (assuming escape string syntax is used). - For example, to insert a text field - containing a double quote and a backslash in a composite - value, you'd need to write: - -INSERT ... VALUES ('("\"\\")'); - - The string-literal processor removes one level of backslashes, so that - what arrives at the composite-value parser looks like - ("\"\\"). In turn, the string - fed to the text data type's input routine - becomes "\. (If we were working - with a data type whose input routine also treated backslashes specially, - bytea for example, we might need as many as eight backslashes - in the command to get one backslash into the stored composite field.) - Dollar quoting (see ) can be - used to avoid the need to double backslashes. - - - The ROW constructor syntax is usually easier to work with diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml index 8467d961fd0a0..7f23962f524c4 100644 --- a/doc/src/sgml/rules.sgml +++ b/doc/src/sgml/rules.sgml @@ -60,6 +60,7 @@ SQL statement where the single parts that it is built from are stored separately. These query trees can be shown in the server log if you set the configuration parameters + debug_print_raw_parse, debug_print_parse, debug_print_rewritten, or debug_print_plan. The rule actions are also @@ -661,8 +662,8 @@ SELECT shoe_ready.shoename, shoe_ready.sh_avail, command other than a SELECT, the result relation points to the range-table entry where the result should go. Everything else is absolutely the same. So having two tables - t1 and t2 with columns a and - b, the query trees for the two statements: + t1 and t2 with columns a and + b, the query trees for the two statements: SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a; @@ -675,27 +676,27 @@ UPDATE t1 SET b = t2.b FROM t2 WHERE t1.a = t2.a; - The range tables contain entries for the tables t1 and t2. + The range tables contain entries for the tables t1 and t2. The target lists contain one variable that points to column - b of the range table entry for table t2. + b of the range table entry for table t2. - The qualification expressions compare the columns a of both + The qualification expressions compare the columns a of both range-table entries for equality. - The join trees show a simple join between t1 and t2. + The join trees show a simple join between t1 and t2. @@ -704,7 +705,7 @@ UPDATE t1 SET b = t2.b FROM t2 WHERE t1.a = t2.a; The consequence is, that both query trees result in similar execution plans: They are both joins over the two tables. For the - UPDATE the missing columns from t1 are added to + UPDATE the missing columns from t1 are added to the target list by the planner and the final query tree will read as: @@ -726,7 +727,7 @@ SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a = t2.a; one is a SELECT command and the other is an UPDATE is handled higher up in the executor, where it knows that this is an UPDATE, and it knows that - this result should go into table t1. But which of the rows + this result should go into table t1. But which of the rows that are there has to be replaced by the new row? @@ -738,7 +739,7 @@ SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a = t2.a; This is a system column containing the file block number and position in the block for the row. Knowing the table, the CTID can be used to retrieve the - original row of t1 to be updated. After adding the + original row of t1 to be updated. After adding the CTID to the target list, the query actually looks like: @@ -967,7 +968,7 @@ CREATE MATERIALIZED VIEW sales_summary AS SELECT seller_no, invoice_date, - sum(invoice_amt)::numeric(13,2) as sales_amt + sum(invoice_amt)::numeric(13,2) AS sales_amt FROM invoice WHERE invoice_date < CURRENT_DATE GROUP BY @@ -1690,7 +1691,7 @@ CREATE RULE shoelace_ok_ins AS ON INSERT TO shoelace_ok WHERE sl_name = NEW.ok_name; - Now you can fill the table shoelace_arrive with + Now you can fill the table shoelace_arrive with the data from the parts list: @@ -2354,7 +2355,7 @@ CREATE RULE computer_del AS ON DELETE TO computer DELETE FROM computer WHERE hostname = 'mypc.local.net'; - the table computer is scanned by index (fast), and the + the table computer is scanned by index (fast), and the command issued by the trigger would also use an index scan (also fast). The extra command from the rule would be: @@ -2420,16 +2421,16 @@ Nestloop This shows, that the planner does not realize that the qualification for hostname in - computer could also be used for an index scan on - software when there are multiple qualification + computer could also be used for an index scan on + software when there are multiple qualification expressions combined with AND, which is what it does in the regular-expression version of the command. The trigger will get invoked once for each of the 2000 old computers that have to be deleted, and that will result in one index scan over - computer and 2000 index scans over - software. The rule implementation will do it with two + computer and 2000 index scans over + software. The rule implementation will do it with two commands that use indexes. And it depends on the overall size of - the table software whether the rule will still be faster in the + the table software whether the rule will still be faster in the sequential scan situation. 2000 command executions from the trigger over the SPI manager take some time, even if all the index blocks will soon be in the cache. @@ -2442,7 +2443,7 @@ DELETE FROM computer WHERE manufacturer = 'bim'; Again this could result in many rows to be deleted from - computer. So the trigger will again run many commands + computer. So the trigger will again run many commands through the executor. The command generated by the rule will be: @@ -2451,7 +2452,7 @@ DELETE FROM software WHERE computer.manufacturer = 'bim' The plan for that command will again be the nested loop over two - index scans, only using a different index on computer: + index scans, only using a different index on computer: Nestloop diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 0c60bafac635d..b01e74638c429 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -490,6 +490,7 @@ ExecStart=/usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data ExecReload=/bin/kill -HUP $MAINPID KillMode=mixed KillSignal=SIGINT +Restart=on-failure TimeoutSec=infinity [Install] @@ -901,6 +902,29 @@ $ postgres -D $PGDATA -C num_os_semaphores + + AIX + AIXIPC configuration + + + + It should not be necessary to do + any special configuration for such parameters as + SHMMAX, as it appears this is configured to + allow all memory to be used as shared memory. That is the + sort of configuration commonly used for other databases such + as DB/2. + + It might, however, be necessary to modify the global + ulimit information in + /etc/security/limits, as the default hard + limits for file sizes (fsize) and numbers of + files (nofiles) might be too low. + + + + + FreeBSD FreeBSDIPC configuration @@ -1104,13 +1128,8 @@ projadd -c "PostgreSQL DB User" -K "project.max-shm-memory=(privileged,8GB,deny) - Other recommended kernel setting changes for database servers which will - have a large number of connections are: - -project.max-shm-ids=(priv,32768,deny) -project.max-sem-ids=(priv,4096,deny) -project.max-msg-ids=(priv,4096,deny) - + To run a very large server, or multiple servers concurrently, you + might also need to raise project.max-shm-ids. @@ -2445,6 +2464,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433 client certificate must not be on this list + + $PGDATA/pg_hosts.conf + SNI configuration + defines which certificates to use for which server hostname + +
@@ -2572,6 +2597,123 @@ openssl x509 -req -in server.csr -text -days 365 \
+ + SNI Configuration + + + PostgreSQL can be configured for Server Name + Indication, SNI, using the + configuration parameter. PostgreSQL inspects the + TLS hostname extension in the SSL connection handshake, and selects the + right certificate, key and CA certificate to use for the connection based + on entries in the configuration file. + + + + The configuration file contains lines of + these general forms: + +hostname SSL_certificate SSL_key SSL_CA_certificate SSL_passphrase_cmd SSL_passphrase_cmd_reload +include file +include_if_exists file +include_dir directory + + Comments, whitespace, line continuations, and inclusion directives are + handled in the same way as + in . hostname + is matched case-insensitively against the hostname TLS + extension in the SSL handshake. + SSL_certificate, + SSL_key, + SSL_CA_certificate, + SSL_passphrase_cmd, and + SSL_passphrase_cmd_reload + are treated like + , + , + , + , and + respectively. + All fields except SSL_CA_certificate, + SSL_passphrase_cmd, and + SSL_passphrase_cmd_reload are required. If + SSL_passphrase_cmd is provided but not + SSL_passphrase_cmd_reload, then the default + value for SSL_passphrase_cmd_reload is + off. + + + + hostname can be either the literal + hostname for the connection, /no_sni/, or *. + contains details on how these values are + used. + + Hostname field values + + + + Host Entry + Hostname extension + Description + + + + + + hostname + Required + + Certificate and key to use for connections to the host specified in + the connection. Multiple hostnames can be defined by using a comma + separated list. The certificate and key will be used for connections + to all hosts in the list. + + + + + /no_sni/ + Not allowed + + Certificate and key to use for connections with no + sslsni defined. + + + + + * + Not required + + Default host, matches all connections. + + + + + +
+
+ + + If is empty or missing, then the SSL + configuration in postgresql.conf will be used for all + connections. If is non-empty then it + will take precedence over certificate and key settings in + postgresql.conf. + + + + It is currently not possible to set different clientname + values for the different certificates. Any clientname + setting in will be applied during + authentication regardless of which set of certificates have been loaded + via an SNI enabled connection. + + + + The CRL configuration in postgresql.conf is applied + to all connections regardless of whether they use SNI or not. + +
diff --git a/doc/src/sgml/seg.sgml b/doc/src/sgml/seg.sgml index dc66e24f2f514..2e879c3e45202 100644 --- a/doc/src/sgml/seg.sgml +++ b/doc/src/sgml/seg.sgml @@ -46,7 +46,7 @@ when you fetch it? Watch: -test=> select 6.50 :: float8 as "pH"; +test=> SELECT 6.50::float8 AS "pH"; pH --- 6.5 @@ -72,7 +72,7 @@ test=> select 6.50 :: float8 as "pH"; Check this out: -test=> select '6.25 .. 6.50'::seg as "pH"; +test=> SELECT '6.25 .. 6.50'::seg AS "pH"; pH ------------ 6.25 .. 6.50 @@ -377,7 +377,7 @@ test=> select '6.25 .. 6.50'::seg as "pH"; boundary if the resulting interval includes a power of ten: -postgres=> select '10(+-)1'::seg as seg; +postgres=> SELECT '10(+-)1'::seg AS seg; seg --------- 9.0 .. 11 -- should be: 9 .. 11 diff --git a/doc/src/sgml/sepgsql.sgml b/doc/src/sgml/sepgsql.sgml index 03ed7d1c90d15..ddac625355737 100644 --- a/doc/src/sgml/sepgsql.sgml +++ b/doc/src/sgml/sepgsql.sgml @@ -442,7 +442,7 @@ UPDATE t1 SET x = 2, y = func1(y) WHERE z = 100; The default database privilege system allows database superusers to modify system catalogs using DML commands, and reference or modify - toast tables. These operations are prohibited when + TOAST tables. These operations are prohibited when sepgsql is enabled. @@ -613,7 +613,7 @@ postgres=# SELECT cid, cname, show_credit(cid) FROM customer; the original one. For example: -regression=# select sepgsql_getcon(); +regression=# SELECT sepgsql_getcon(); sepgsql_getcon ------------------------------------------------------- unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 diff --git a/doc/src/sgml/sourcerepo.sgml b/doc/src/sgml/sourcerepo.sgml index 6c13c5a30cde6..b9ea59ae329fd 100644 --- a/doc/src/sgml/sourcerepo.sgml +++ b/doc/src/sgml/sourcerepo.sgml @@ -3,6 +3,8 @@ The Source Code Repository + Git + The PostgreSQL source code is stored and managed using the Git version control system. A public @@ -40,7 +42,7 @@ - To begin using the Git repository, make a clone of the official mirror: + To begin using the Git repository, make a clone of the official mirror: git clone https://git.postgresql.org/git/postgresql.git @@ -51,16 +53,6 @@ git clone https://git.postgresql.org/git/postgresql.git The files will be placed in a new subdirectory postgresql of your current directory. - - - The Git mirror can also be reached via the Git protocol. Just change the URL - prefix to git, as in: - - -git clone git://git.postgresql.org/git/postgresql.git - - - diff --git a/doc/src/sgml/sources.sgml b/doc/src/sgml/sources.sgml index fa68d4d024a93..760f9b69d4778 100644 --- a/doc/src/sgml/sources.sgml +++ b/doc/src/sgml/sources.sgml @@ -153,11 +153,12 @@ ereport(ERROR, errmsg("function %s is not unique", func_signature_string(funcname, nargs, NIL, actual_arg_types)), - errhint("Unable to choose a best candidate function. " - "You might need to add explicit typecasts.")); + errdetail("Could not choose a best candidate function."), + errhint("You might need to add explicit type casts.")); This illustrates the use of format codes to embed run-time values into - a message text. Also, an optional hint message is provided. + a message text. Also, optional detail + and hint messages are provided. The auxiliary function calls can be written in any order, but conventionally errcode and errmsg appear first. @@ -907,12 +908,12 @@ BETTER: unrecognized node type: 42 C Standard Code in PostgreSQL should only rely on language - features available in the C99 standard. That means a conforming - C99 compiler has to be able to compile postgres, at least aside + features available in the C11 standard. That means a conforming + C11 compiler has to be able to compile postgres, at least aside from a few platform dependent pieces. - A few features included in the C99 standard are, at this time, not + A few features included in the C11 standard are, at this time, not permitted to be used in core PostgreSQL code. This currently includes variable length arrays, intermingled declarations and code, // comments, universal @@ -924,13 +925,11 @@ BETTER: unrecognized node type: 42 features can be used, if a fallback is provided. - For example _Static_assert() and + For example typeof() and __builtin_constant_p are currently used, even though they are from newer revisions of the C standard and a GCC extension respectively. If not available - we respectively fall back to using a C99 compatible replacement that - performs the same checks, but emits rather cryptic messages and do not - use __builtin_constant_p. + we do not use them. diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml index 7e2f2df965dba..e30d0962ae761 100644 --- a/doc/src/sgml/spi.sgml +++ b/doc/src/sgml/spi.sgml @@ -846,7 +846,7 @@ int SPI_execute_extended(const char *command, int SPI_execute_with_args(const char *command, int nargs, Oid *argtypes, - Datum *values, const char *nulls, + const Datum *values, const char *nulls, bool read_only, long count) @@ -1671,7 +1671,7 @@ bool SPI_is_cursor_plan(SPIPlanPtr plan) -int SPI_execute_plan(SPIPlanPtr plan, Datum * values, const char * nulls, +int SPI_execute_plan(SPIPlanPtr plan, const Datum * values, const char * nulls, bool read_only, long count) @@ -2317,7 +2317,7 @@ Portal SPI_cursor_open(const char * name, SPIPlanPtr name, const char *command, int nargs, Oid *argtypes, - Datum *values, const char *nulls, + const Datum *values, const char *nulls, bool read_only, int cursorOptions) diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml index 61250799ec076..6b6377503bf6a 100644 --- a/doc/src/sgml/storage.sgml +++ b/doc/src/sgml/storage.sgml @@ -39,6 +39,8 @@ these required items, the cluster configuration files Contents of <varname>PGDATA</varname> + + @@ -743,6 +745,8 @@ There are five parts to each page. Overall Page Layout Page Layout + + @@ -1064,7 +1068,7 @@ data. Empty in ordinary tables. fixed width field, then all the bytes are simply placed. If it's a variable length field (attlen = -1) then it's a bit more complicated. All variable-length data types share the common header structure - struct varlena, which includes the total length of the stored + varlena, which includes the total length of the stored value and some flag bits. Depending on the flags, the data can be either inline or in a TOAST table; it might be compressed, too (see ). diff --git a/doc/src/sgml/stylesheet-fo.xsl b/doc/src/sgml/stylesheet-fo.xsl index e7916a6a88347..aec6de7064a7b 100644 --- a/doc/src/sgml/stylesheet-fo.xsl +++ b/doc/src/sgml/stylesheet-fo.xsl @@ -14,24 +14,11 @@ 3 - - - - - - - - - - - 1.5em +0 +0 @@ -42,6 +29,8 @@ an "Unresolved ID reference found" warning during PDF builds. solid 1pt black + 0.25in + 0.25in 12pt 12pt 6pt @@ -415,5 +404,21 @@ an "Unresolved ID reference found" warning during PDF builds. + + + + + + + + + + + + diff --git a/doc/src/sgml/syntax.sgml b/doc/src/sgml/syntax.sgml index 916189a7d68ce..67482996861dc 100644 --- a/doc/src/sgml/syntax.sgml +++ b/doc/src/sgml/syntax.sgml @@ -438,30 +438,6 @@ SELECT 'foo' 'bar'; will check that the character conversion is possible. - - - If the configuration parameter - is off, - then PostgreSQL recognizes backslash escapes - in both regular and escape string constants. However, as of - PostgreSQL 9.1, the default is on, meaning - that backslash escapes are recognized only in escape string constants. - This behavior is more standards-compliant, but might break applications - which rely on the historical behavior, where backslash escapes - were always recognized. As a workaround, you can set this parameter - to off, but it is better to migrate away from using backslash - escapes. If you need to use a backslash escape to represent a special - character, write the string constant with an E. - - - - In addition to standard_conforming_strings, the configuration - parameters and - govern treatment of backslashes - in string constants. - - - The character with the code zero cannot be in a string constant. @@ -532,17 +508,6 @@ U&'d!0061t!+000061' UESCAPE '!' by one of these escape sequences is converted to the actual server encoding; an error is reported if that's not possible. - - - Also, the Unicode escape syntax for string constants only works - when the configuration - parameter is - turned on. This is because otherwise this syntax could confuse - clients that parse the SQL statements to the point that it could - lead to SQL injections and similar security issues. If the - parameter is set to off, this syntax will be rejected with an - error message. - @@ -1834,8 +1799,8 @@ FROM generate_series(1,10) AS s(i); The syntax of a window function call is one of the following: -function_name (expression , expression ... ) [ FILTER ( WHERE filter_clause ) ] OVER window_name -function_name (expression , expression ... ) [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition ) +function_name (expression , expression ... ) null treatment [ FILTER ( WHERE filter_clause ) ] OVER window_name +function_name (expression , expression ... ) null treatment [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition ) function_name ( * ) [ FILTER ( WHERE filter_clause ) ] OVER window_name function_name ( * ) [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition ) @@ -1873,7 +1838,9 @@ EXCLUDE NO OTHERS Here, expression represents any value - expression that does not itself contain window function calls. + expression that does not itself contain window function calls. Some + non-aggregate functions allow a null treatment clause, + described in . @@ -2048,7 +2015,7 @@ EXCLUDE NO OTHERS The built-in window functions are described in . Other window functions can be added by + linkend="functions-window-table"/>. Other window functions can be added by the user. Also, any built-in or user-defined general-purpose or statistical aggregate can be used as a window function. (Ordered-set and hypothetical-set aggregates cannot presently be used as window functions.) @@ -2428,8 +2395,8 @@ SELECT ROW(1,2.5,'this is a test'); which will be expanded to a list of the elements of the row value, just as occurs when the .* syntax is used at the top level of a SELECT list (see ). - For example, if table t has - columns f1 and f2, these are the same: + For example, if table t has + columns f1 and f2, these are the same: SELECT ROW(t.*, 42) FROM t; SELECT ROW(t.f1, t.f2, 42) FROM t; diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index b58c52ea50f5a..2ebec6928d5de 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -53,7 +53,7 @@ pg_aios - In-use asynchronous IO handles + in-use asynchronous IO handles @@ -81,6 +81,11 @@ open cursors + + pg_dsm_registry_allocations + shared memory allocations tracked in the DSM registry + + pg_file_settings summary of configuration file contents @@ -131,6 +136,11 @@ prepared transactions + + pg_publication_sequences + publications and information of their associated sequences + + pg_publication_tables publications and information of their associated tables @@ -589,6 +599,20 @@ + + + location text + + + The location where the extension is installed. If it is in the standard + system location, then the value will be $system, + while if it is found in the path specified by the + extension_control_path + GUC then the full path will be shown. + Only superusers can see this information. + + + comment text @@ -713,6 +737,20 @@ + + + location text + + + The location where the extension is installed. If it is in the standard + system location, then the value will be $system, + while if it is found in the path specified by the + extension_control_path + GUC then the full path will be shown. + Only superusers can see this information. + + + comment text @@ -1086,6 +1124,75 @@ AND c1.path[c2.level] = c2.path[c2.level]; + + <structname>pg_dsm_registry_allocations</structname> + + + pg_dsm_registry_allocations + + + + The pg_dsm_registry_allocations view shows shared + memory allocations tracked in the dynamic shared memory (DSM) registry. + This includes memory allocated by extensions using the mechanisms detailed + in . + + +
+ <structname>pg_dsm_registry_allocations</structname> Columns + + + + + Column Type + + + Description + + + + + + + + name text + + + The name of the allocation in the DSM registry. + + + + + + type text + + + The type of allocation. Possible values are segment, + area, and hash, which correspond + to dynamic shared memory segments, areas, and hash tables, respectively. + + + + + + size int8 + + + Size of the allocation in bytes. NULL for entries that failed + initialization. + + + + +
+ + + By default, the pg_dsm_registry_allocations view + can be read only by superusers or roles with privileges of the + pg_read_all_stats role. + +
+ <structname>pg_file_settings</structname> @@ -2475,6 +2582,67 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + <structname>pg_publication_sequences</structname> + + + pg_publication_sequences + + + + The view pg_publication_sequences provides + information about the mapping between publications and sequences. + + + + <structname>pg_publication_sequences</structname> Columns + + + + + Column Type + + + Description + + + + + + + + pubname name + (references pg_publication.pubname) + + + Name of publication + + + + + + schemaname name + (references pg_namespace.nspname) + + + Name of schema containing sequence + + + + + + sequencename name + (references pg_class.relname) + + + Name of sequence + + + + +
+
+ <structname>pg_publication_tables</structname> @@ -2819,21 +2987,18 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx unreserved means that the slot no longer retains the required WAL files and some of them are to be removed at - the next checkpoint. This state can return + the next checkpoint. This typically occurs when + is set to + a non-negative value. This state can return to reserved or extended. - lost means that some required WAL files have - been removed and this slot is no longer usable. + lost means that this slot is no longer usable. - The last two states are seen only when - is - non-negative. If restart_lsn is NULL, this - field is null. @@ -2925,14 +3090,15 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx wal_level_insufficient means that the - primary doesn't have a sufficient to - perform logical decoding. It is set only for logical slots. + primary doesn't have an + sufficient to perform logical decoding. It is set only for logical + slots. idle_timeout means that the slot has remained - idle longer than the configured + inactive longer than the configured duration. @@ -2965,6 +3131,49 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + slotsync_skip_reasontext + + + The reason for the last slot synchronization skip. Slot + synchronization occurs only on standby servers and thus this column has + no meaning on the primary server. It is relevant mainly for logical slots + on standby servers whose synced field is + true. It is NULL if slot + synchronization is successful. + Possible values are: + + + + wal_or_rows_removed means that the required WALs or + catalog rows have already been removed or are at the risk of removal + from the standby. + + + + + wal_not_flushed means that the standby had not + flushed the WAL corresponding to the position reserved on the failover + slot. + + + + + no_consistent_snapshot means that the standby could + not build a consistent snapshot to decode WALs from + restart_lsn. + + + + + slot_invalidated means that the synced slot is + invalidated. + + + + + @@ -3932,7 +4141,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx passwd text - Password (possibly encrypted); null if none. See + Encrypted password; null if none. See pg_authid for details of how encrypted passwords are stored. @@ -4045,8 +4254,8 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Anonymous allocations are allocations that have been made with ShmemAlloc() directly, rather than via - ShmemInitStruct() or - ShmemInitHash(). + ShmemRequestStruct() or + ShmemRequestHash(). @@ -4205,6 +4414,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + tableid oid + (references pg_class.oid) + + + OID of table + + + attname name @@ -4215,6 +4434,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + attnum int2 + (references pg_attribute.attnum) + + + Number of column described by this row + + + inherited bool @@ -4457,6 +4686,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + tableid oid + (references pg_class.oid) + + + OID of table + + + statistics_schemaname name @@ -4477,6 +4716,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + statistics_id oid + (references pg_statistic_ext.oid) + + + OID of extended statistics object + + + statistics_owner name @@ -4668,6 +4917,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + tableid oid + (references pg_class.oid) + + + OID of table the statistics object is defined on + + + statistics_schemaname name @@ -4688,6 +4947,16 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + statistics_id oid + (references pg_statistic_ext.oid) + + + OID of extended statistics object + + + statistics_owner name @@ -4836,6 +5105,45 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx non-null elements. (Null for scalar types.) + + + + range_length_histogram anyarray + + + A histogram of the lengths of non-empty and non-null range values of an + expression. (Null for non-range types.) + + + This histogram is calculated using the subtype_diff + range function regardless of whether range bounds are inclusive. + + + + + + range_empty_frac float4 + + + Fraction of expression entries whose values are empty ranges. + (Null for non-range types.) + + + + + + range_bounds_histogram anyarray + + + A histogram of lower and upper bounds of non-empty and non-null range + values. (Null for non-range types.) + + + These two histograms are represented as a single array of ranges, whose + lower bounds represent the histogram of lower bounds, and upper bounds + represent the histogram of upper bounds. + + diff --git a/doc/src/sgml/tablefunc.sgml b/doc/src/sgml/tablefunc.sgml index e10fe7009d163..69cafa00ad66e 100644 --- a/doc/src/sgml/tablefunc.sgml +++ b/doc/src/sgml/tablefunc.sgml @@ -293,10 +293,10 @@ INSERT INTO ct(rowid, attribute, value) VALUES('test2','att4','val8'); SELECT * FROM crosstab( - 'select rowid, attribute, value - from ct - where attribute = ''att2'' or attribute = ''att3'' - order by 1,2') + 'SELECT rowid, attribute, value + FROM ct + WHERE attribute = ''att2'' OR attribute = ''att3'' + ORDER BY 1, 2') AS ct(row_name text, category_1 text, category_2 text, category_3 text); row_name | category_1 | category_2 | category_3 @@ -371,10 +371,10 @@ CREATE TYPE tablefunc_crosstab_N AS ( SELECT * FROM crosstab3( - 'select rowid, attribute, value - from ct - where attribute = ''att2'' or attribute = ''att3'' - order by 1,2'); + 'SELECT rowid, attribute, value + FROM ct + WHERE attribute = ''att2'' OR attribute = ''att3'' + ORDER BY 1, 2'); @@ -407,7 +407,7 @@ CREATE TYPE my_crosstab_float8_5_cols AS ( ); CREATE OR REPLACE FUNCTION crosstab_float8_5_cols(text) - RETURNS setof my_crosstab_float8_5_cols + RETURNS SETOF my_crosstab_float8_5_cols AS '$libdir/tablefunc','crosstab' LANGUAGE C STABLE STRICT; @@ -426,7 +426,7 @@ CREATE OR REPLACE FUNCTION crosstab_float8_5_cols( OUT my_category_3 float8, OUT my_category_4 float8, OUT my_category_5 float8) - RETURNS setof record + RETURNS SETOF record AS '$libdir/tablefunc','crosstab' LANGUAGE C STABLE STRICT; @@ -572,18 +572,18 @@ row_name extra cat1 cat2 cat3 cat4 Here are two complete examples: -create table sales(year int, month int, qty int); -insert into sales values(2007, 1, 1000); -insert into sales values(2007, 2, 1500); -insert into sales values(2007, 7, 500); -insert into sales values(2007, 11, 1500); -insert into sales values(2007, 12, 2000); -insert into sales values(2008, 1, 1000); - -select * from crosstab( - 'select year, month, qty from sales order by 1', - 'select m from generate_series(1,12) m' -) as ( +CREATE TABLE sales (year int, month int, qty int); +INSERT INTO sales VALUES (2007, 1, 1000); +INSERT INTO sales VALUES (2007, 2, 1500); +INSERT INTO sales VALUES (2007, 7, 500); +INSERT INTO sales VALUES (2007, 11, 1500); +INSERT INTO sales VALUES (2007, 12, 2000); +INSERT INTO sales VALUES (2008, 1, 1000); + +SELECT * FROM crosstab( + 'SELECT year, month, qty FROM sales ORDER BY 1', + 'SELECT m FROM generate_series(1, 12) m' +) AS ( year int, "Jan" int, "Feb" int, diff --git a/doc/src/sgml/targets-meson.txt b/doc/src/sgml/targets-meson.txt index d0021a5eb10d2..05df077cffc6c 100644 --- a/doc/src/sgml/targets-meson.txt +++ b/doc/src/sgml/targets-meson.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # Description of important meson targets, used for the 'help' target and # installation.sgml (via generate-targets-meson.pl). Right now the parsers are @@ -16,7 +16,9 @@ Code Targets: Developer Targets: reformat-dat-files Rewrite catalog data files into standard format expand-dat-files Expand all data files to include defaults - update-unicode Update unicode data to new version + update-unicode Update Unicode data to new version + headerscheck Check that all headers compile standalone + cpluspluscheck Check that all headers compile as C++ Documentation Targets: html Build documentation in multi-page HTML format diff --git a/doc/src/sgml/tcn.sgml b/doc/src/sgml/tcn.sgml index 32a1025cc6b79..98278fbee3730 100644 --- a/doc/src/sgml/tcn.sgml +++ b/doc/src/sgml/tcn.sgml @@ -43,32 +43,32 @@ A brief example of using the extension follows. -test=# create table tcndata +test=# CREATE TABLE tcndata test-# ( -test(# a int not null, -test(# b date not null, +test(# a int NOT NULL, +test(# b date NOT NULL, test(# c text, -test(# primary key (a, b) +test(# PRIMARY KEY (a, b) test(# ); CREATE TABLE -test=# create trigger tcndata_tcn_trigger -test-# after insert or update or delete on tcndata -test-# for each row execute function triggered_change_notification(); +test=# CREATE TRIGGER tcndata_tcn_trigger +test-# AFTER INSERT OR UPDATE OR DELETE ON tcndata +test-# FOR EACH ROW EXECUTE FUNCTION triggered_change_notification(); CREATE TRIGGER -test=# listen tcn; +test=# LISTEN tcn; LISTEN -test=# insert into tcndata values (1, date '2012-12-22', 'one'), +test=# INSERT INTO tcndata VALUES (1, date '2012-12-22', 'one'), test-# (1, date '2012-12-23', 'another'), test-# (2, date '2012-12-23', 'two'); INSERT 0 3 Asynchronous notification "tcn" with payload ""tcndata",I,"a"='1',"b"='2012-12-22'" received from server process with PID 22770. Asynchronous notification "tcn" with payload ""tcndata",I,"a"='1',"b"='2012-12-23'" received from server process with PID 22770. Asynchronous notification "tcn" with payload ""tcndata",I,"a"='2',"b"='2012-12-23'" received from server process with PID 22770. -test=# update tcndata set c = 'uno' where a = 1; +test=# UPDATE tcndata SET c = 'uno' WHERE a = 1; UPDATE 2 Asynchronous notification "tcn" with payload ""tcndata",U,"a"='1',"b"='2012-12-22'" received from server process with PID 22770. Asynchronous notification "tcn" with payload ""tcndata",U,"a"='1',"b"='2012-12-23'" received from server process with PID 22770. -test=# delete from tcndata where a = 1 and b = date '2012-12-22'; +test=# DELETE FROM tcndata WHERE a = 1 AND b = date '2012-12-22'; DELETE 1 Asynchronous notification "tcn" with payload ""tcndata",D,"a"='1',"b"='2012-12-22'" received from server process with PID 22770. diff --git a/doc/src/sgml/test-decoding.sgml b/doc/src/sgml/test-decoding.sgml index 5d1ae8f4f52e2..7d3d590471a32 100644 --- a/doc/src/sgml/test-decoding.sgml +++ b/doc/src/sgml/test-decoding.sgml @@ -25,16 +25,16 @@ postgres=# SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL, 'include-xids', '0'); - lsn | xid | data ------------+-----+-------------------------------------------------- - 0/16D30F8 | 691 | BEGIN - 0/16D32A0 | 691 | table public.data: INSERT: id[int4]:2 data[text]:'arg' - 0/16D32A0 | 691 | table public.data: INSERT: id[int4]:3 data[text]:'demo' - 0/16D32A0 | 691 | COMMIT - 0/16D32D8 | 692 | BEGIN - 0/16D3398 | 692 | table public.data: DELETE: id[int4]:2 - 0/16D3398 | 692 | table public.data: DELETE: id[int4]:3 - 0/16D3398 | 692 | COMMIT + lsn | xid | data +------------+-----+-------------------------------------------------- + 0/016D30F8 | 691 | BEGIN + 0/016D32A0 | 691 | table public.data: INSERT: id[int4]:2 data[text]:'arg' + 0/016D32A0 | 691 | table public.data: INSERT: id[int4]:3 data[text]:'demo' + 0/016D32A0 | 691 | COMMIT + 0/016D32D8 | 692 | BEGIN + 0/016D3398 | 692 | table public.data: DELETE: id[int4]:2 + 0/016D3398 | 692 | table public.data: DELETE: id[int4]:3 + 0/016D3398 | 692 | COMMIT (8 rows) @@ -45,18 +45,18 @@ postgres=# SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL, 'i postgres[33712]=#* SELECT * FROM pg_logical_slot_get_changes('test_slot', NULL, NULL, 'stream-changes', '1'); - lsn | xid | data ------------+-----+-------------------------------------------------- - 0/16B21F8 | 503 | opening a streamed block for transaction TXN 503 - 0/16B21F8 | 503 | streaming change for TXN 503 - 0/16B2300 | 503 | streaming change for TXN 503 - 0/16B2408 | 503 | streaming change for TXN 503 - 0/16BEBA0 | 503 | closing a streamed block for transaction TXN 503 - 0/16B21F8 | 503 | opening a streamed block for transaction TXN 503 - 0/16BECA8 | 503 | streaming change for TXN 503 - 0/16BEDB0 | 503 | streaming change for TXN 503 - 0/16BEEB8 | 503 | streaming change for TXN 503 - 0/16BEBA0 | 503 | closing a streamed block for transaction TXN 503 + lsn | xid | data +------------+-----+-------------------------------------------------- + 0/016B21F8 | 503 | opening a streamed block for transaction TXN 503 + 0/016B21F8 | 503 | streaming change for TXN 503 + 0/016B2300 | 503 | streaming change for TXN 503 + 0/016B2408 | 503 | streaming change for TXN 503 + 0/016BEBA0 | 503 | closing a streamed block for transaction TXN 503 + 0/016B21F8 | 503 | opening a streamed block for transaction TXN 503 + 0/016BECA8 | 503 | streaming change for TXN 503 + 0/016BEDB0 | 503 | streaming change for TXN 503 + 0/016BEEB8 | 503 | streaming change for TXN 503 + 0/016BEBA0 | 503 | closing a streamed block for transaction TXN 503 (10 rows) diff --git a/doc/src/sgml/textsearch.sgml b/doc/src/sgml/textsearch.sgml index 908857a54af5f..d6d2ddeaacc74 100644 --- a/doc/src/sgml/textsearch.sgml +++ b/doc/src/sgml/textsearch.sgml @@ -1355,7 +1355,7 @@ ts_headline( config - Warning: Cross-site scripting (XSS) safety + Warning: Cross-site Scripting (XSS) Safety The output from ts_headline is not guaranteed to be safe for direct inclusion in web pages. When @@ -1974,12 +1974,12 @@ SELECT title, body FROM messages WHERE tsv @@ to_tsquery('title & body'); CREATE FUNCTION messages_trigger() RETURNS trigger AS $$ -begin +BEGIN new.tsv := setweight(to_tsvector('pg_catalog.english', coalesce(new.title,'')), 'A') || setweight(to_tsvector('pg_catalog.english', coalesce(new.body,'')), 'D'); - return new; -end + RETURN new; +END $$ LANGUAGE plpgsql; CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE @@ -3145,6 +3145,56 @@ largehearted stopword file name that gives a list of words to eliminate. (PostgreSQL's standard stopword lists are also provided by the Snowball project.) + + + + The available values of the language parameter are: + + arabic, + armenian, + basque, + catalan, + danish, + dutch, + dutch_porter, + english, + esperanto, + estonian, + finnish, + french, + german, + greek, + hindi, + hungarian, + indonesian, + irish, + italian, + lithuanian, + nepali, + norwegian, + polish, + porter, + portuguese, + romanian, + russian, + serbian, + spanish, + swedish, + tamil, + turkish, + and + yiddish. + + The porter algorithm is an old stemmer for English, + and the dutch_porter algorithm is an old stemmer + for Dutch (it was called dutch + in PostgreSQL releases before 19). + The rest are the currently-recommended stemmers for their + respective languages. + + All these algorithms except porter + and dutch_porter have built-in dictionaries + provided, most with stopword lists attached. For example, there is a built-in definition equivalent to @@ -3879,6 +3929,7 @@ Parser: "pg_catalog.default" pg_catalog | danish_stem | snowball stemmer for danish language pg_catalog | dutch_stem | snowball stemmer for dutch language pg_catalog | english_stem | snowball stemmer for english language + pg_catalog | esperanto_stem | snowball stemmer for esperanto language pg_catalog | estonian_stem | snowball stemmer for estonian language pg_catalog | finnish_stem | snowball stemmer for finnish language pg_catalog | french_stem | snowball stemmer for french language @@ -3892,6 +3943,7 @@ Parser: "pg_catalog.default" pg_catalog | lithuanian_stem | snowball stemmer for lithuanian language pg_catalog | nepali_stem | snowball stemmer for nepali language pg_catalog | norwegian_stem | snowball stemmer for norwegian language + pg_catalog | polish_stem | snowball stemmer for polish language pg_catalog | portuguese_stem | snowball stemmer for portuguese language pg_catalog | romanian_stem | snowball stemmer for romanian language pg_catalog | russian_stem | snowball stemmer for russian language @@ -3997,11 +4049,6 @@ Parser: "pg_catalog.default" The length of a tsvector (lexemes + positions) must be less than 1 megabyte - - - The number of lexemes must be less than - 264 - Position values in tsvector must be greater than 0 and no more than 16,383 diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml index e9214dcf1b1bd..8a5e72782122d 100644 --- a/doc/src/sgml/trigger.sgml +++ b/doc/src/sgml/trigger.sgml @@ -129,10 +129,9 @@ In all cases, a trigger is executed as part of the same transaction as the statement that triggered it, so if either the statement or the trigger causes an error, the effects of both will be rolled back. - Also, the trigger will always run in the security context of the role - that executed the statement that caused the trigger to fire, unless - the trigger function is defined as SECURITY DEFINER, - in which case it will run as the function owner. + Also, the trigger will always run as the role that queued the trigger + event, unless the trigger function is marked as SECURITY + DEFINER, in which case it will run as the function owner. @@ -216,6 +215,18 @@ are fired for the same kind of action. + + If an UPDATE or DELETE uses + FOR PORTION OF, causing new rows to be inserted + to preserve the leftover untargeted part of modified records, then + INSERT triggers are fired for each inserted + row. Each row is inserted separately, so they fire their own + statement triggers, and they have their own transition tables. + (The BEFORE DELETE/UPDATE triggers are fired first, + then BEFORE INSERT, then AFTER + INSERT, then AFTER DELETE/UPDATE.) + + Trigger functions invoked by per-statement triggers should always return NULL. Trigger functions invoked by per-row @@ -824,7 +835,7 @@ typedef struct Trigger attnum (1-based) is a member of this bitmap set, call bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, - trigdata->tg_updatedcols)). + trigdata->tg_updatedcols). diff --git a/doc/src/sgml/typeconv.sgml b/doc/src/sgml/typeconv.sgml index 2874874248668..96aa02e4fabe0 100644 --- a/doc/src/sgml/typeconv.sgml +++ b/doc/src/sgml/typeconv.sgml @@ -465,9 +465,9 @@ try a similar case with ~, we get: SELECT ~ '20' AS "negation"; -ERROR: operator is not unique: ~ "unknown" -HINT: Could not choose a best candidate operator. You might need to add -explicit type casts. +ERROR: operator is not unique: ~ unknown +DETAIL: Could not choose a best candidate operator. +HINT: You might need to add explicit type casts. This happens because the system cannot decide which of the several possible ~ operators should be preferred. We can help @@ -490,7 +490,7 @@ SELECT ~ CAST('20' AS int8) AS "negation"; Here is another example of resolving an operator with one known and one unknown input: -SELECT array[1,2] <@ '{1,2,3}' as "is subset"; +SELECT ARRAY[1, 2] <@ '{1,2,3}' AS "is subset"; is subset ----------- @@ -901,8 +901,8 @@ the parser will try to convert that to text: SELECT substr(1234, 3); ERROR: function substr(integer, integer) does not exist -HINT: No function matches the given name and argument types. You might need -to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. This does not work because integer does not have an implicit cast diff --git a/doc/src/sgml/unaccent.sgml b/doc/src/sgml/unaccent.sgml index 94100ed26091a..744821ca997ca 100644 --- a/doc/src/sgml/unaccent.sgml +++ b/doc/src/sgml/unaccent.sgml @@ -144,7 +144,7 @@ mydb=# ALTER TEXT SEARCH DICTIONARY unaccent (RULES='my_rules'); To test the dictionary, you can try: -mydb=# select ts_lexize('unaccent','Hôtel'); +mydb=# SELECT ts_lexize('unaccent', 'Hôtel'); ts_lexize ----------- {Hotel} @@ -160,19 +160,19 @@ mydb=# CREATE TEXT SEARCH CONFIGURATION fr ( COPY = french ); mydb=# ALTER TEXT SEARCH CONFIGURATION fr ALTER MAPPING FOR hword, hword_part, word WITH unaccent, french_stem; -mydb=# select to_tsvector('fr','Hôtels de la Mer'); +mydb=# SELECT to_tsvector('fr', 'Hôtels de la Mer'); to_tsvector ------------------- 'hotel':1 'mer':4 (1 row) -mydb=# select to_tsvector('fr','Hôtel de la Mer') @@ to_tsquery('fr','Hotels'); +mydb=# SELECT to_tsvector('fr', 'Hôtel de la Mer') @@ to_tsquery('fr', 'Hotels'); ?column? ---------- t (1 row) -mydb=# select ts_headline('fr','Hôtel de la Mer',to_tsquery('fr','Hotels')); +mydb=# SELECT ts_headline('fr', 'Hôtel de la Mer', to_tsquery('fr', 'Hotels')); ts_headline ------------------------ <b>Hôtel</b> de la Mer diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index ed18704a9c2ab..0ec32700bd4d0 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -713,7 +713,7 @@ GRANT pg_signal_backend TO admin_user; pg_read_all_data allows reading all data (tables, - views, sequences), as if having SELECT rights on + views, sequences, large objects), as if having SELECT rights on those objects and USAGE rights on all schemas. This role does not bypass row-level security (RLS) policies. If RLS is being used, an administrator may wish to set BYPASSRLS on @@ -721,7 +721,7 @@ GRANT pg_signal_backend TO admin_user; pg_write_all_data allows writing all data (tables, - views, sequences), as if having INSERT, + views, sequences, large objects), as if having INSERT, UPDATE, and DELETE rights on those objects and USAGE rights on all schemas. This role does not bypass row-level security (RLS) policies. If RLS is being diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml index f3b86b26be905..165af8a0cf277 100644 --- a/doc/src/sgml/wal.sgml +++ b/doc/src/sgml/wal.sgml @@ -246,9 +246,10 @@ Checksums can be disabled when the cluster is initialized using initdb. - They can also be enabled or disabled at a later time as an offline - operation. Data checksums are enabled or disabled at the full cluster - level, and cannot be specified individually for databases or tables. + They can also be enabled or disabled at a later time either as an offline + operation or online in a running cluster allowing concurrent access. Data + checksums are enabled or disabled at the full cluster level, and cannot be + specified individually for databases, tables or replicated cluster members. @@ -265,7 +266,7 @@ - Off-line Enabling of Checksums + Offline Enabling of Checksums The pg_checksums @@ -274,6 +275,123 @@ + + + Online Enabling of Checksums + + + Checksums can be enabled or disabled online, by calling the appropriate + functions. + + + + Both enabling and disabling data checksums happens in two phases, separated + by a checkpoint to ensure durability. The different states, and their + transitions, are illustrated in + and discussed in further detail in this section. + + + +
+ data checksums states + + + + + +
+
+ + + Enabling checksums will set the cluster checksum state to + inprogress-on. During this time, checksums will be + written but not verified. In addition to this, a background worker process + is started that enables checksums on all existing data in the cluster. Once + this worker has completed processing all databases in the cluster, the + checksum state will automatically switch to on. The + processing will consume two background worker processes, make sure that + max_worker_processes allows for at least two more + additional processes. + + + + The process will initially wait for all open transactions to finish before + it starts, so that it can be certain that there are no tables that have been + created inside a transaction that has not committed yet and thus would not + be visible to the process enabling checksums. It will also, for each database, + wait for all pre-existing temporary tables to get removed before it finishes. + If long-lived temporary tables are used in an application it may be necessary + to terminate these application connections to allow the process to complete. + + + + If the cluster is stopped while in inprogress-on state, + for any reason, or processing was interrupted, then the checksum enable + process must be restarted manually. To do this, re-execute the function + pg_enable_data_checksums() once the cluster has been + restarted. The process will start over, there is no support for resuming + work from where it was interrupted. If the cluster is stopped while in + inprogress-off, then the checksum state will be set to + off when the cluster is restarted. + + + + Disabling data checksums will set the data checksum state to + inprogress-off. During this time, checksums will be + written but not verified. After all processes acknowledge the change, + the state will automatically be set to off. + + + + Disabling data checksums while data checksums are actively being enabled + will abort the current processing. + + + + Impact on system of online operations + + Enabling data checksums can cause significant I/O to the system, as all of the + database pages will need to be rewritten, and will be written both to the + data files and the WAL. The impact may be limited by throttling using the + cost_delay and cost_limit + parameters of the pg_enable_data_checksums() function. + + + + + + I/O: all pages need to have data checksums calculated and written which + will generate a lot of dirty pages that will need to be flushed to disk, + as well as WAL logged. + + + Replication: When the standby receives the data checksum state change + in the WAL stream it will issue a + restartpoint in order to flush the current state into the + pg_control file. The restartpoint will flush the + current state to disk and will block redo until finished. This in turn + will induce replication lag, which on synchronous standbys also blocks + the primary. Reducing before the + process is started can help with reducing the time it takes for the + restartpoint to finish. + + + Shutdown/Restart: If the server is shut down or restarted when data + checksums are being enabled, the process will not resume and all pages + need to be recalculated and rewritten. Enabling data checksums should + be done when there is no need for regular maintenance or during a + service window. + + + + + + No I/O is incurred when disabling data checksums, but checkpoints are + still required. + + + +
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml index 2d81afce8cb9b..086e6231998a3 100644 --- a/doc/src/sgml/xfunc.sgml +++ b/doc/src/sgml/xfunc.sgml @@ -397,8 +397,8 @@ SELECT tf1(17, 100.0); In this example, we chose the name accountno for the first argument, but this is the same as the name of a column in the - bank table. Within the UPDATE command, - accountno refers to the column bank.accountno, + bank table. Within the UPDATE command, + accountno refers to the column bank.accountno, so tf1.accountno must be used to refer to the argument. We could of course avoid this by using a different name for the argument. @@ -1016,7 +1016,7 @@ SELECT *, upper(fooname) FROM getfoo(1) AS t1; This feature is normally used when calling the function in the FROM clause. In this case each row returned by the function becomes a row of the table seen by the query. For example, assume that - table foo has the same contents as above, and we say: + table foo has the same contents as above, and we say: CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$ @@ -1409,7 +1409,7 @@ DETAIL: A result of type anyelement requires at least one input of type anyelem For example: CREATE FUNCTION dup (f1 anyelement, OUT f2 anyelement, OUT f3 anyarray) -AS 'select $1, array[$1,$1]' LANGUAGE SQL; +AS 'SELECT $1, ARRAY[$1, $1]' LANGUAGE SQL; SELECT * FROM dup(22); f2 | f3 @@ -2051,8 +2051,7 @@ PG_MODULE_MAGIC_EXT( - By-value types can only be 1, 2, or 4 bytes in length - (also 8 bytes, if sizeof(Datum) is 8 on your machine). + By-value types can only be 1, 2, 4, or 8 bytes in length. You should be careful to define your types such that they will be the same size (in bytes) on all architectures. For example, the long type is dangerous because it is 4 bytes on some @@ -2165,7 +2164,7 @@ memcpy(destination->data, buffer, 40); it's considered good style to use the macro VARHDRSZ to refer to the size of the overhead for a variable-length type. Also, the length field must be set using the - SET_VARSIZE macro, not by simple assignment. + SET_VARSIZE function, not by simple assignment. @@ -2400,7 +2399,7 @@ PG_FUNCTION_INFO_V1(funcname); To call another version-1 function, you can use DirectFunctionCalln(func, arg1, ..., argn). This is particularly useful when you want - to call functions defined in the standard internal library, by using an + to call functions defined in the standard internal function library by using an interface similar to their SQL signature. @@ -2492,7 +2491,7 @@ makepoint(PG_FUNCTION_ARGS) /* Here, the pass-by-reference nature of Point is not hidden. */ Point *pointx = PG_GETARG_POINT_P(0); Point *pointy = PG_GETARG_POINT_P(1); - Point *new_point = (Point *) palloc(sizeof(Point)); + Point *new_point = palloc_object(Point); new_point->x = pointx->x; new_point->y = pointy->y; @@ -3629,87 +3628,147 @@ CREATE FUNCTION make_array(anyelement) RETURNS anyarray Add-ins can reserve shared memory on server startup. To do so, the add-in's shared library must be preloaded by specifying it in shared_preload_libraries. - The shared library should also register a - shmem_request_hook in its - _PG_init function. This - shmem_request_hook can reserve shared memory by - calling: - -void RequestAddinShmemSpace(Size size) - - Each backend should obtain a pointer to the reserved shared memory by - calling: + The shared library should register callbacks in + its _PG_init function, which then get called at the + right stages of the system startup to initialize the shared memory. + Here is an example: -void *ShmemInitStruct(const char *name, Size size, bool *foundPtr) - - If this function sets foundPtr to - false, the caller should proceed to initialize the - contents of the reserved shared memory. If foundPtr - is set to true, the shared memory was already - initialized by another backend, and the caller need not initialize - further. - +typedef struct MyShmemData { + LWLock lock; /* protects the fields below */ - - To avoid race conditions, each backend should use the LWLock - AddinShmemInitLock when initializing its allocation - of shared memory, as shown here: - -static mystruct *ptr = NULL; -bool found; + ... shared memory contents ... +} MyShmemData; + +static MyShmemData *MyShmem; /* pointer to the struct in shared memory */ -LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); -ptr = ShmemInitStruct("my struct name", size, &found); -if (!found) +static void my_shmem_request(void *arg); +static void my_shmem_init(void *arg); + +const ShmemCallbacks my_shmem_callbacks = { + .request_fn = my_shmem_request, + .init_fn = my_shmem_init, +}; + +/* + * Module load callback + */ +void +_PG_init(void) { - ... initialize contents of shared memory ... - ptr->locks = GetNamedLWLockTranche("my tranche name"); + /* + * In order to create our shared memory area, we have to be loaded via + * shared_preload_libraries. + */ + if (!process_shared_preload_libraries_in_progress) + return; + + /* Register our shared memory needs */ + RegisterShmemCallbacks(&my_shmem_callbacks); +} + +/* callback to request shmem space */ +static void +my_shmem_request(void *arg) +{ + ShmemRequestStruct(.name = "My shmem area", + .size = sizeof(MyShmemData), + .ptr = (void **) &MyShmem, + ); +} + +/* callback to initialize the contents of the MyShmem area at startup */ +static void +my_shmem_init(void *arg) +{ + int tranche_id; + + /* Initialize the lock */ + tranche_id = LWLockNewTrancheId("my tranche name"); + LWLockInitialize(&MyShmem->lock, tranche_id); + + ... initialize the rest of MyShmem fields ... } -LWLockRelease(AddinShmemInitLock); + - shmem_startup_hook provides a convenient place for the - initialization code, but it is not strictly required that all such code - be placed in this hook. Each backend will execute the registered - shmem_startup_hook shortly after it attaches to shared - memory. Note that add-ins should still acquire - AddinShmemInitLock within this hook, as shown in the - example above. + The request_fn callback is called during system + startup, before the shared memory has been allocated. It should call + ShmemRequestStruct() to register the add-in's + shared memory needs. Note that ShmemRequestStruct() + doesn't immediately allocate or initialize the memory, it merely + registers the space to be allocated later in the startup sequence. When + the memory is allocated, it is initialized to zero. For any more + complex initialization, set the init_fn() callback, + which will be called after the memory has been allocated and initialized + to zero, but before any other processes are running, and thus no locking + is required. - - An example of a shmem_request_hook and - shmem_startup_hook can be found in + On Windows, the attach_fn callback, if any, is + additionally called at every backend startup. It can be used to + initialize additional per-backend state related to the shared memory + area that is inherited via fork() on other systems. + + + An example of allocating shared memory can be found in contrib/pg_stat_statements/pg_stat_statements.c in the PostgreSQL source tree. - Requesting Shared Memory After Startup + Requesting Shared Memory After Startup with <function>ShmemRequestStruct</function> + + + The ShmemRequestStruct() can also be called after + system startup, which is useful to allow small allocations in add-in + libraries that are not specified in + shared_preload_libraries. + However, after startup the allocation can fail if there is not enough + shared memory available. The system reserves some memory for allocations + after startup, but that reservation is small. + + + By default, RegisterShmemCallbacks() fails with an + error if called after system startup. To use it after startup, you must + set the SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP flag in + the argument ShmemCallbacks struct to + acknowledge the risk. + + + When RegisterShmemCallbacks() is called after + startup, it will immediately call the appropriate callbacks, depending + on whether the requested memory areas were already initialized by + another backend. The callbacks will be called while holding an internal + lock, which prevents concurrent two backends from initializing the + memory area concurrently. + + + + + Allocating Dynamic Shared Memory After Startup There is another, more flexible method of reserving shared memory that - can be done after server startup and outside a - shmem_request_hook. To do so, each backend that will + can be done after server startup. To do so, each backend that will use the shared memory should obtain a pointer to it by calling: void *GetNamedDSMSegment(const char *name, size_t size, - void (*init_callback) (void *ptr), - bool *found) + void (*init_callback) (void *ptr, void *arg), + bool *found, void *arg) If a dynamic shared memory segment with the given name does not yet exist, this function will allocate it and initialize it with the provided init_callback callback function. If the segment has already been allocated and initialized by another backend, this function simply attaches the existing dynamic shared memory segment to the current - backend. + backend. In the former case, GetNamedDSMSegment + passes the void *arg argument to the + init_callback. This is particularly useful for + reusing an initialization callback function for multiple DSM segments. - Unlike shared memory reserved at server startup, there is no need to - acquire AddinShmemInitLock or otherwise take action - to avoid race conditions when reserving shared memory with - GetNamedDSMSegment. This function ensures that only + GetNamedDSMSegment ensures that only one backend allocates and initializes the segment and that all other backends receive a pointer to the fully allocated and initialized segment. @@ -3760,7 +3819,7 @@ LWLockPadded *GetNamedLWLockTranche(const char *tranche_name) shmem_request_hook. To do so, first allocate a tranche_id by calling: -int LWLockNewTrancheId(void) +int LWLockNewTrancheId(const char *name) Next, initialize each LWLock, passing the new tranche_id as an argument: @@ -3778,17 +3837,8 @@ void LWLockInitialize(LWLock *lock, int tranche_id) - Finally, each backend using the tranche_id should - associate it with a tranche_name by calling: - -void LWLockRegisterTranche(int tranche_id, const char *tranche_name) - - - - - A complete usage example of LWLockNewTrancheId, - LWLockInitialize, and - LWLockRegisterTranche can be found in + A complete usage example of LWLockNewTrancheId and + LWLockInitialize can be found in contrib/pg_prewarm/autoprewarm.c in the PostgreSQL source tree. @@ -3947,7 +3997,7 @@ extern bool InjectionPointDetach(const char *name); - Enabling injections points requires + Enabling injection points requires with configure or with Meson. @@ -4012,7 +4062,7 @@ extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind, An example describing how to register and use custom statistics can be - found in src/test/modules/injection_points. + found in src/test/modules/test_custom_stats. @@ -4033,7 +4083,7 @@ extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind, All functions accessed by the backend must present a C interface to the backend; these C functions can then call C++ functions. - For example, extern C linkage is required for + For example, extern "C" linkage is required for backend-accessed functions. This is also necessary for any functions that are passed as pointers between the backend and C++ code. @@ -4050,7 +4100,7 @@ extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind, Prevent exceptions from propagating into the C code (use a catch-all - block at the top level of all extern C functions). This + block at the top level of all extern "C" functions). This is necessary even if the C++ code does not explicitly throw any exceptions, because events like out-of-memory can still throw exceptions. Any exceptions must be caught and appropriate errors @@ -4074,7 +4124,7 @@ extern PgStat_Kind pgstat_register_kind(PgStat_Kind kind, In summary, it is best to place C++ code behind a wall of - extern C functions that interface to the backend, + extern "C" functions that interface to the backend, and avoid exception, memory, and call stack leakage. @@ -4166,6 +4216,31 @@ supportfn(internal) returns internal expression and an actual execution of the target function. + + SupportRequestSimplify is not used + for set-returning + functions. Instead, support functions can implement + the SupportRequestInlineInFrom request to expand + function calls appearing in the FROM clause of a + query. (It's also allowed to support this request for + non-set-returning functions, although + typically SupportRequestSimplify would serve as + well.) For this request type, a successful result must be + a SELECT Query tree, which will replace + the FROM item as though a sub-select had been + written instead. The Query tree must appear as it would after parse + analysis and rewrite processing. One way to ensure that that's true + is to build an SQL string then feed it + through pg_parse_query + and pg_analyze_and_rewrite, or related + functions. PARAM_EXTERN Param + nodes can appear within the Query to represent the function's + arguments; they will be replaced by the actual argument expressions. + As with SupportRequestSimplify, it is the support + function's responsibility that the replacement Query be equivalent to + normal execution of the target function. + + For target functions that return boolean, it is often useful to estimate the fraction of rows that will be selected by a WHERE clause using that diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml index 7e23a7b6e4323..3d315df2f9803 100644 --- a/doc/src/sgml/xindex.sgml +++ b/doc/src/sgml/xindex.sgml @@ -598,7 +598,7 @@ 11 - stratnum + translate_cmptype translate compare types to strategy numbers used by the operator class (optional) 12 diff --git a/doc/src/sgml/xoper.sgml b/doc/src/sgml/xoper.sgml index 954a90d77d0ed..853b07a9f1489 100644 --- a/doc/src/sgml/xoper.sgml +++ b/doc/src/sgml/xoper.sgml @@ -21,7 +21,7 @@ PostgreSQL supports prefix - and infix operators. Operators can be + and binary (or infix) operators. Operators can be overloaded;overloadingoperators that is, the same operator name can be used for different operators that have different numbers and types of operands. When a query is diff --git a/doc/src/sgml/xtypes.sgml b/doc/src/sgml/xtypes.sgml index e67e5bdf4c4ac..df56d1c3ace68 100644 --- a/doc/src/sgml/xtypes.sgml +++ b/doc/src/sgml/xtypes.sgml @@ -89,7 +89,7 @@ complex_in(PG_FUNCTION_ARGS) errmsg("invalid input syntax for type %s: \"%s\"", "complex", str))); - result = (Complex *) palloc(sizeof(Complex)); + result = palloc_object(Complex); result->x = x; result->y = y; PG_RETURN_POINTER(result); diff --git a/meson.build b/meson.build index d142e3e408b38..20b887f1a1bc1 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Entry point for building PostgreSQL with meson # @@ -8,17 +8,19 @@ project('postgresql', ['c'], - version: '18beta1', + version: '19devel', license: 'PostgreSQL', - # We want < 0.56 for python 3.5 compatibility on old platforms. EPEL for - # RHEL 7 has 0.55. < 0.54 would require replacing some uses of the fs - # module, < 0.53 all uses of fs. So far there's no need to go to >=0.56. - meson_version: '>=0.54', + # We want < 0.62 for python 3.6 compatibility on old platforms. + # RHEL 8 has 0.58. < 0.57 would require various additional + # backward-compatibility conditionals. + # Meson 0.57.0 and 0.57.1 are buggy, therefore >=0.57.2. + meson_version: '>=0.57.2', default_options: [ 'warning_level=1', #-Wall equivalent 'b_pch=false', 'buildtype=debugoptimized', # -O2 + debug + 'default_library=both', # For compatibility with the autoconf build, set a default prefix. This # works even on windows, where it's a drive-relative path (i.e. when on # d:/somepath it'll install to d:/usr/local/pgsql) @@ -40,11 +42,41 @@ build_system = build_machine.system() host_cpu = host_machine.cpu_family() cc = meson.get_compiler('c') +have_cxx = add_languages('cpp', required: false, native: false) +if have_cxx + cxx = meson.get_compiler('cpp') +endif not_found_dep = dependency('', required: false) thread_dep = dependency('threads') auto_features = get_option('auto_features') +# Declare variables to disable static or shared libraries. This +# makes the 'default_library' option work even though we don't use the +# library() function but instead shared_library() and static_library() +# separately. +# +# build_shared_lib/build_static_lib control building/installing the two +# versions of libraries that we can build both versions of (e.g., libpq). +# There are also libraries that we only build a static version of (e.g., +# libpgport). These are always built, since we need them while building, +# but they are installed only if install_internal_static_lib is true. +# +# Note: at present, -Ddefault_library=static doesn't actually work, because +# psql and other programs insist on linking to the shared version of libpq. +# This could be fixed if there was interest, but so far there is not. +default_library_opt = get_option('default_library') +build_shared_lib = true +build_static_lib = true +install_internal_static_lib = true +if default_library_opt == 'shared' + build_static_lib = false + install_internal_static_lib = false +elif default_library_opt == 'static' + build_shared_lib = false + error('-Ddefault_library=static is not yet supported') +endif + ############################################################### @@ -197,7 +229,38 @@ endif # that purpose. portname = host_system -if host_system == 'cygwin' +if host_system == 'aix' + sema_kind = 'unnamed_posix' + library_path_var = 'LIBPATH' + export_file_format = 'aix' + export_fmt = '-Wl,-bE:@0@' + mod_link_args_fmt = ['-Wl,-bI:@0@'] + mod_link_with_dir = 'libdir' + mod_link_with_name = '@0@.imp' + + # We force 64-bit builds, because AIX doesn't play very nice with dynamic + # library loading in 32-bit mode: there's not enough address space. + cppflags += '-maix64' + ldflags += '-maix64' + # Note: it's also necessary to tell programs like 'ar' to work in 64-bit + # mode. Since meson, in its infinite wisdom, doesn't allow us either + # to pass '-X64' to ar or to set the OBJECT_MODE environment variable + # from here, we have to tell the user to set OBJECT_MODE. + + # M:SRE sets a flag indicating that an object is a shared library. + ldflags_sl += '-Wl,-bM:SRE' + # -brtllib indicates binaries should use runtime-loaded shared libraries. + ldflags_be += '-Wl,-brtllib' + + # On AIX, shared libraries are wrapped in static libraries and can have + # the same extension '.a'. Therefore we must refrain from trying to build + # static libraries alongside shared ones, or meson will complain about + # duplicate targets. Note however that we leave dlsuffix with its default + # value of '.so'; this results in using '.so' for loadable modules, which + # is fine. + build_static_lib = false + +elif host_system == 'cygwin' sema_kind = 'unnamed_posix' cppflags += '-D_GNU_SOURCE' dlsuffix = '.dll' @@ -261,6 +324,7 @@ elif host_system == 'openbsd' elif host_system == 'sunos' portname = 'solaris' + sema_kind = 'unnamed_posix' export_fmt = '-Wl,-M@0@' # We need these #defines to get POSIX-conforming versions # of many interfaces (sigwait, getpwuid_r, shmdt, ...). @@ -349,6 +413,9 @@ missing = find_program('config/missing', native: true) cp = find_program('cp', required: false, native: true) xmllint_bin = find_program(get_option('XMLLINT'), native: true, required: false) xsltproc_bin = find_program(get_option('XSLTPROC'), native: true, required: false) +nm = find_program('nm', required: false, native: true) +ditaa = find_program('ditaa', native: true, required: false) +dot = find_program('dot', native: true, required: false) bison_flags = [] if bison.found() @@ -451,6 +518,14 @@ else segsize = (get_option('segsize') * 1024 * 1024 * 1024) / blocksize endif +# If we don't have largefile support, can't handle segment size >= 2GB. +if cc.sizeof('off_t', args: test_c_args) < 8 + segsize_bytes = segsize * blocksize + if segsize_bytes >= (2 * 1024 * 1024 * 1024) + error('Large file support is not enabled. Segment size cannot be larger than 1GB.') + endif +endif + cdata.set('BLCKSZ', blocksize, description: '''Size of a disk block --- this also limits the size of a tuple. You can set it bigger if you need bigger tuples (although TOAST should reduce the need @@ -545,6 +620,60 @@ dir_doc_extension = dir_doc / 'extension' # used, they need to be added to test_c_args as well. ############################################################### +# Do we need an option to enable C11? +c11_test = ''' +#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L +# error "Compiler does not advertise C11 conformance" +#endif +''' + +if not cc.compiles(c11_test, name: 'C11') + c11_ok = false + if cc.get_id() == 'msvc' + c11_test_args = ['/std:c11'] + else + c11_test_args = ['-std=gnu11', '-std=c11'] + endif + foreach arg : c11_test_args + if cc.compiles(c11_test, name: 'C11 with @0@'.format(arg), args: [arg]) + c11_ok = true + cflags += arg + break + endif + endforeach + if not c11_ok + error('C compiler does not support C11') + endif +endif + + +# Do we need an option to enable C++11? +cxx11_test = ''' +#if !defined __cplusplus || __cplusplus < 201103L +# error "Compiler does not advertise C++11 conformance" +#endif +''' + +if have_cxx and not cxx.compiles(cxx11_test, name: 'C++11') + cxx11_ok = false + if cxx.get_id() == 'msvc' + cxx11_test_args = ['/std:c++14'] # oldest version supported + else + cxx11_test_args = ['-std=gnu++11', '-std=c++11'] + endif + foreach arg : cxx11_test_args + if cxx.compiles(cxx11_test, name: 'C++11 with @0@'.format(arg), args: [arg]) + cxx11_ok = true + cxxflags += arg + break + endif + endforeach + if not cxx11_ok + error('C++ compiler does not support C++11') + endif +endif + + postgres_inc = [include_directories(postgres_inc_d)] test_lib_d = postgres_lib_d test_c_args = cppflags + cflags @@ -629,9 +758,7 @@ if not gssapiopt.disabled() gssapi = dependency('krb5-gssapi', required: false) have_gssapi = gssapi.found() - if have_gssapi - gssapi_deps = [gssapi] - elif not have_gssapi + if not have_gssapi # Hardcoded lookup for gssapi. This is necessary as gssapi on windows does # not install neither pkg-config nor cmake dependency information. if host_system == 'windows' @@ -655,18 +782,15 @@ if not gssapiopt.disabled() endforeach if have_gssapi - # Meson before 0.57.0 did not support using check_header() etc with - # declare_dependency(). Thus the tests below use the library looked up - # above. Once we require a newer meson version, we can simplify. gssapi = declare_dependency(dependencies: gssapi_deps) endif endif if not have_gssapi - elif cc.check_header('gssapi/gssapi.h', dependencies: gssapi_deps, required: false, + elif cc.check_header('gssapi/gssapi.h', dependencies: [gssapi], required: false, args: test_c_args, include_directories: postgres_inc) cdata.set('HAVE_GSSAPI_GSSAPI_H', 1) - elif cc.check_header('gssapi.h', dependencies: gssapi_deps, required: gssapiopt, + elif cc.check_header('gssapi.h', dependencies: [gssapi], required: gssapiopt, args: test_c_args, include_directories: postgres_inc) cdata.set('HAVE_GSSAPI_H', 1) else @@ -674,10 +798,10 @@ if not gssapiopt.disabled() endif if not have_gssapi - elif cc.check_header('gssapi/gssapi_ext.h', dependencies: gssapi_deps, required: false, + elif cc.check_header('gssapi/gssapi_ext.h', dependencies: [gssapi], required: false, args: test_c_args, include_directories: postgres_inc) cdata.set('HAVE_GSSAPI_GSSAPI_EXT_H', 1) - elif cc.check_header('gssapi_ext.h', dependencies: gssapi_deps, required: gssapiopt, + elif cc.check_header('gssapi_ext.h', dependencies: [gssapi], required: gssapiopt, args: test_c_args, include_directories: postgres_inc) cdata.set('HAVE_GSSAPI_EXT_H', 1) else @@ -685,7 +809,7 @@ if not gssapiopt.disabled() endif if not have_gssapi - elif cc.has_function('gss_store_cred_into', dependencies: gssapi_deps, + elif cc.has_function('gss_store_cred_into', dependencies: [gssapi], args: test_c_args, include_directories: postgres_inc) cdata.set('ENABLE_GSS', 1) @@ -799,15 +923,13 @@ endif llvmopt = get_option('llvm') llvm = not_found_dep -if add_languages('cpp', required: llvmopt, native: false) +if have_cxx llvm = dependency('llvm', version: '>=14', method: 'config-tool', required: llvmopt) if llvm.found() cdata.set('USE_LLVM', 1) - cpp = meson.get_compiler('cpp') - llvm_binpath = llvm.get_variable(configtool: 'bindir') ccache = find_program('ccache', native: true, required: false) @@ -816,8 +938,13 @@ if add_languages('cpp', required: llvmopt, native: false) # find via PATH, too. clang = find_program(llvm_binpath / 'clang', 'clang', required: true) endif -elif llvmopt.auto() - message('llvm requires a C++ compiler') +else + msg = 'llvm requires a C++ compiler' + if llvmopt.auto() + message(msg) + elif llvmopt.enabled() + error(msg) + endif endif @@ -943,10 +1070,10 @@ if not libcurlopt.disabled() # libcurl and one of either epoll or kqueue. oauth_flow_supported = ( libcurl.found() - and (cc.check_header('sys/event.h', required: false, - args: test_c_args, include_directories: postgres_inc) - or cc.check_header('sys/epoll.h', required: false, - args: test_c_args, include_directories: postgres_inc)) + and (cc.has_header('sys/event.h', + args: test_c_args, include_directories: postgres_inc) + or cc.has_header('sys/epoll.h', + args: test_c_args, include_directories: postgres_inc)) ) if oauth_flow_supported @@ -990,6 +1117,12 @@ liburingopt = get_option('liburing') liburing = dependency('liburing', required: liburingopt) if liburing.found() cdata.set('USE_LIBURING', 1) + + if cc.has_function('io_uring_queue_init_mem', + dependencies: liburing, args: test_c_args) + cdata.set('HAVE_IO_URING_QUEUE_INIT_MEM', 1) + endif + endif @@ -1136,6 +1269,7 @@ endif perlopt = get_option('plperl') perl_dep = not_found_dep +perl_includespec = '' perlversion = '' if not perlopt.disabled() perl_may_work = true @@ -1160,6 +1294,7 @@ if not perlopt.disabled() useshrplib = run_command(perl_conf_cmd, 'useshrplib', check: true).stdout() perl_inc_dir = '@0@/CORE'.format(archlibexp) + perl_includespec = '-I@0@'.format(perl_inc_dir) if perlversion.version_compare('< 5.14') perl_may_work = false @@ -1178,6 +1313,7 @@ if not perlopt.disabled() if not fs.is_file('@0@/perl.h'.format(perl_inc_dir)) and \ fs.is_file('@0@@1@/perl.h'.format(pg_sysroot, perl_inc_dir)) perl_ccflags = ['-iwithsysroot', perl_inc_dir] + perl_includespec = '-iwithsysroot @0@/CORE'.format(archlibexp) endif # check compiler finds header @@ -1205,7 +1341,7 @@ if not perlopt.disabled() if cc.get_id() == 'msvc' # prevent binary mismatch between MSVC built plperl and Strawberry or # msys ucrt perl libraries - perl_v = run_command(perl, '-V').stdout() + perl_v = run_command(perl, '-V', check: false).stdout() if not perl_v.contains('USE_THREAD_SAFE_LOCALE') perl_ccflags += ['-DNO_THREAD_SAFE_LOCALE'] endif @@ -1282,16 +1418,41 @@ endif pyopt = get_option('plpython') python3_dep = not_found_dep +python_includespec = '' if not pyopt.disabled() pm = import('python') - python3_inst = pm.find_installation(python.path(), required: pyopt) + python3_inst = pm.find_installation(python.full_path(), required: pyopt) if python3_inst.found() - python3_dep = python3_inst.dependency(embed: true, required: pyopt) + # On MSVC, link against python3.lib instead of python3XX.lib for + # Python Limited API. Right now, this is the only platform that + # needs this workaround. In the long run, Meson should handle + # this internally: + # . + if host_system == 'windows' and cc.get_id() == 'msvc' + python3_prefix = python3_inst.get_variable('prefix') + python3_libdir = python3_prefix / 'libs' + python3_incdir = python3_prefix / 'include' + python3_lib = cc.find_library('python3', dirs: python3_libdir, required: pyopt) + python3_dep = declare_dependency( + include_directories: include_directories(python3_incdir), + dependencies: python3_lib, + ) + # Explicit args needed for older Meson compatibility + python3_header_check_args = ['/I' + python3_incdir] + else + python3_dep = python3_inst.dependency(embed: true, required: pyopt) + python3_header_check_args = [] + endif # Remove this check after we depend on Meson >= 1.1.0 - if not cc.check_header('Python.h', dependencies: python3_dep, required: pyopt, include_directories: postgres_inc) + if not cc.check_header('Python.h', args: python3_header_check_args, dependencies: python3_dep, required: pyopt, include_directories: postgres_inc) python3_dep = not_found_dep endif endif + + if python3_dep.found() + command = [python, '-c', 'import sysconfig; print("-I" + sysconfig.get_config_var("INCLUDEPY"))'] + python_includespec = run_command(command, check: true).stdout().strip() + endif endif @@ -1397,17 +1558,6 @@ Use -Dreadline=disabled to disable readline support.'''.format(readline_dep)) 'rl_filename_quoting_function', ] - foreach var : check_vars - cdata.set('HAVE_' + var.to_upper(), - cc.has_header_symbol(readline_h, var, - args: test_c_args, include_directories: postgres_inc, - prefix: '#include ', - dependencies: [readline]) ? 1 : false) - endforeach - - # If found via cc.find_library() ensure headers are found when using the - # dependency. On meson < 0.57 one cannot do compiler checks using the - # dependency returned by declare_dependency(), so we can't do this above. if readline.type_name() == 'library' readline = declare_dependency(dependencies: readline, include_directories: postgres_inc) @@ -1419,6 +1569,14 @@ Use -Dreadline=disabled to disable readline support.'''.format(readline_dep)) readline = declare_dependency(dependencies: readline, link_args: '-Wl,--enable-auto-import') endif + + foreach var : check_vars + cdata.set('HAVE_' + var.to_upper(), + cc.has_header_symbol(readline_h, var, + args: test_c_args, include_directories: postgres_inc, + prefix: '#include ', + dependencies: [readline]) ? 1 : false) + endforeach endif # XXX: Figure out whether to implement mingw warning equivalent @@ -1518,6 +1676,7 @@ if sslopt in ['auto', 'openssl'] ['X509_get_signature_info'], ['SSL_CTX_set_num_tickets'], ['SSL_CTX_set_keylog_callback'], + ['SSL_CTX_set_client_hello_cb'], ] are_openssl_funcs_complete = true @@ -1573,7 +1732,10 @@ if uuidopt != 'none' elif uuidopt == 'ossp' # In upstream, the package and library is called just 'uuid', but many # distros change it to 'ossp-uuid'. - uuid = dependency('ossp-uuid', 'uuid', required: false) + uuid = dependency('ossp-uuid', required: false) + if not uuid.found() + uuid = dependency('uuid', required: false) + endif uuidfunc = 'uuid_export' uuidheader = 'uuid.h' @@ -1693,86 +1855,42 @@ endif # Compiler tests ############################################################### -# Do we need -std=c99 to compile C99 code? We don't want to add -std=c99 -# unnecessarily, because we optionally rely on newer features. -c99_test = ''' -#include -#include -#include -#include - -struct named_init_test { - int a; - int b; -}; - -extern void structfunc(struct named_init_test); - -int main(int argc, char **argv) -{ - struct named_init_test nit = { - .a = 3, - .b = 5, - }; - - for (int loop_var = 0; loop_var < 3; loop_var++) - { - nit.a += nit.b; - } - - structfunc((struct named_init_test){1, 0}); - - return nit.a != 0; -} -''' - -if not cc.compiles(c99_test, name: 'c99', args: test_c_args) - if cc.compiles(c99_test, name: 'c99 with -std=c99', - args: test_c_args + ['-std=c99']) - test_c_args += '-std=c99' - cflags += '-std=c99' - else - error('C compiler does not support C99') - endif -endif - if host_machine.endian() == 'big' cdata.set('WORDS_BIGENDIAN', 1) endif # Determine memory alignment requirements for the basic C data types. -alignof_types = ['short', 'int', 'long', 'double'] +alignof_types = ['short', 'int', 'int64_t', 'double'] foreach t : alignof_types - align = cc.alignment(t, args: test_c_args) + align = cc.alignment(t, args: test_c_args, prefix: '#include ') cdata.set('ALIGNOF_@0@'.format(t.to_upper()), align) endforeach # Compute maximum alignment of any basic type. # -# We require 'double' to have the strictest alignment among the basic types, -# because otherwise the C ABI might impose 8-byte alignment on some of the -# other C types that correspond to TYPALIGN_DOUBLE SQL types. That could -# cause a mismatch between the tuple layout and the C struct layout of a -# catalog tuple. We used to carefully order catalog columns such that any -# fixed-width, attalign=4 columns were at offsets divisible by 8 regardless -# of MAXIMUM_ALIGNOF to avoid that, but we no longer support any platforms -# where TYPALIGN_DOUBLE != MAXIMUM_ALIGNOF. -# -# We assume without checking that int64_t's alignment is at least as strong -# as long, char, short, or int. Note that we intentionally do not consider -# any types wider than 64 bits, as allowing MAXIMUM_ALIGNOF to exceed 8 -# would be too much of a penalty for disk and memory space. +# We assume without checking that the maximum alignment requirement is that +# of int64_t and/or double. (On most platforms those are the same, but not +# everywhere.) For historical reasons, both int8 and float8 datatypes have +# typalign 'd', and therefore will be aligned per ALIGNOF_DOUBLE in database +# tuples even if ALIGNOF_INT64_T is more. Note that we intentionally do not +# consider any types wider than 64 bits, as allowing MAXIMUM_ALIGNOF to exceed +# 8 would be too much of a penalty for disk and memory space. + +alignof_int64_t = cdata.get('ALIGNOF_INT64_T') alignof_double = cdata.get('ALIGNOF_DOUBLE') -if cc.alignment('int64_t', args: test_c_args, prefix: '#include ') > alignof_double - error('alignment of int64_t is greater than the alignment of double') +if alignof_int64_t > alignof_double + cdata.set('MAXIMUM_ALIGNOF', alignof_int64_t) +else + cdata.set('MAXIMUM_ALIGNOF', alignof_double) endif -cdata.set('MAXIMUM_ALIGNOF', alignof_double) cdata.set('SIZEOF_LONG', cc.sizeof('long', args: test_c_args)) cdata.set('SIZEOF_LONG_LONG', cc.sizeof('long long', args: test_c_args)) cdata.set('SIZEOF_VOID_P', cc.sizeof('void *', args: test_c_args)) cdata.set('SIZEOF_SIZE_T', cc.sizeof('size_t', args: test_c_args)) +cdata.set('SIZEOF_INTMAX_T', cc.sizeof('intmax_t', args: test_c_args, + prefix: '#include ')) # Check if __int128 is a working 128 bit integer type, and if so @@ -1818,7 +1936,7 @@ if cc.links(''' if not meson.is_cross_build() r = cc.run(''' /* This must match the corresponding code in c.h: */ - #if defined(__GNUC__) || defined(__SUNPRO_C) + #if defined(__GNUC__) #define pg_attribute_aligned(a) __attribute__((aligned(a))) #elif defined(_MSC_VER) #define pg_attribute_aligned(a) __declspec(align(a)) @@ -1875,23 +1993,8 @@ if cc.compiles(''' endif -# Check if the C compiler understands _Static_assert(), -# and define HAVE__STATIC_ASSERT if so. +# Select the format archetype to be used to check printf-type functions. # -# We actually check the syntax ({ _Static_assert(...) }), because we need -# gcc-style compound expressions to be able to wrap the thing into macros. -if cc.compiles(''' - int main(int arg, char **argv) - { - ({ _Static_assert(1, "foo"); }); - } - ''', - name: '_Static_assert', - args: test_c_args) - cdata.set('HAVE__STATIC_ASSERT', 1) -endif - - # Need to check a call with %m because netbsd supports gnu_printf but emits a # warning for each use of %m. printf_attributes = ['gnu_printf', '__syslog__', 'printf'] @@ -1906,11 +2009,24 @@ attrib_error_args = cc.get_supported_arguments('-Werror=format', '-Werror=ignore foreach a : printf_attributes if cc.compiles(testsrc.format(a), args: test_c_args + attrib_error_args, name: 'format ' + a) - cdata.set('PG_PRINTF_ATTRIBUTE', a) + cdata.set('PG_C_PRINTF_ATTRIBUTE', a) break endif endforeach +# We need to repeat the test for C++ because gcc and clang prefer different +# format archetypes. +if have_cxx + attrib_error_args = cxx.get_supported_arguments('-Werror=format', '-Werror=ignored-attributes') + foreach a : printf_attributes + if cxx.compiles(testsrc.format(a), + args: attrib_error_args, name: 'cxxformat ' + a) + cdata.set('PG_CXX_PRINTF_ATTRIBUTE', a) + break + endif + endforeach +endif + if cc.has_function_attribute('visibility:default') and \ cc.has_function_attribute('visibility:hidden') @@ -1935,7 +2051,6 @@ builtins = [ 'ctz', 'constant_p', 'frame_address', - 'popcount', 'unreachable', ] @@ -1985,10 +2100,7 @@ if cc.links(''' cdata.set('HAVE__BUILTIN_OP_OVERFLOW', 1) endif - -# XXX: The configure.ac check for __cpuid() is broken, we don't copy that -# here. To prevent problems due to two detection methods working, stop -# checking after one. +# Check for __get_cpuid() and __cpuid(). if cc.links(''' #include int main(int arg, char **argv) @@ -2012,7 +2124,7 @@ elif cc.links(''' endif -# Check for __get_cpuid_count() and __cpuidex() in a similar fashion. +# Check for __get_cpuid_count() if cc.links(''' #include int main(int arg, char **argv) @@ -2023,12 +2135,19 @@ if cc.links(''' ''', name: '__get_cpuid_count', args: test_c_args) cdata.set('HAVE__GET_CPUID_COUNT', 1) -elif cc.links(''' +endif + +# Check for __cpuidex() +if cc.links(''' + #ifdef _MSC_VER #include + #else + #include + #endif int main(int arg, char **argv) { unsigned int exx[4] = {0, 0, 0, 0}; - __cpuidex(exx, 7, 0); + __cpuidex((int *) exx, 7, 0); } ''', name: '__cpuidex', args: test_c_args) @@ -2056,6 +2175,20 @@ choke me endif +# Check whether the C++ compiler supports designated initializers. +# These are used by PG_MODULE_MAGIC, and we use the result of this +# test to decide whether to enable the test_cplusplusext test module. +# Designated initializers only got standardized in C++20. In GCC and +# Clang they also work when using earlier C++ versions, but MSVC +# really only supports them when its configured to be in C++20 mode or +# higher. +if have_cxx + have_cxx_desinit = cxx.compiles('struct S { int x; } s = { .x = 1 };', name: 'C++ designated initializers') +else + have_cxx_desinit = false +endif + + ############################################################### # Compiler flags @@ -2070,34 +2203,57 @@ common_functional_flags = [ ] cflags += cc.get_supported_arguments(common_functional_flags) -if llvm.found() - cxxflags += cpp.get_supported_arguments(common_functional_flags) +if have_cxx + cxxflags += cxx.get_supported_arguments(common_functional_flags) endif vectorize_cflags = cc.get_supported_arguments(['-ftree-vectorize']) unroll_loops_cflags = cc.get_supported_arguments(['-funroll-loops']) common_warning_flags = [ - '-Wmissing-prototypes', '-Wpointer-arith', # Really don't want VLAs to be used in our dialect of C '-Werror=vla', # On macOS, complain about usage of symbols newer than the deployment target '-Werror=unguarded-availability-new', - '-Wendif-labels', '-Wmissing-format-attribute', - '-Wimplicit-fallthrough=3', '-Wcast-function-type', '-Wshadow=compatible-local', # This was included in -Wall/-Wformat in older GCC versions '-Wformat-security', ] -cflags_warn += cc.get_supported_arguments(common_warning_flags) -if llvm.found() - cxxflags_warn += cpp.get_supported_arguments(common_warning_flags) +# C-only warnings +c_warning_flags = [ + '-Wmissing-prototypes', + '-Wold-style-declaration', + '-Wold-style-definition', + '-Wstrict-prototypes', +] + +cflags_warn += cc.get_supported_arguments(common_warning_flags, c_warning_flags) +if have_cxx + cxxflags_warn += cxx.get_supported_arguments(common_warning_flags) endif +# To require fallthrough attribute annotations, use +# -Wimplicit-fallthrough=5 with gcc and -Wimplicit-fallthrough with +# clang. The latter is also accepted on gcc but does not enforce +# attribute annotations, so test the former first. +fallthrough_warning_flags = ['-Wimplicit-fallthrough=5', '-Wimplicit-fallthrough'] +foreach w : fallthrough_warning_flags + if cc.has_argument(w) + cflags_warn += w + break + endif +endforeach +foreach w : fallthrough_warning_flags + if have_cxx and cxx.has_argument(w) + cxxflags_warn += w + break + endif +endforeach + # A few places with imported code get a pass on -Wdeclaration-after-statement, remember # the result for them cflags_no_decl_after_statement = [] @@ -2116,7 +2272,6 @@ if cc.has_argument('-Wmissing-variable-declarations') cflags_no_missing_var_decls += '-Wno-missing-variable-declarations' endif - # The following tests want to suppress various unhelpful warnings by adding # -Wno-foo switches. But gcc won't complain about unrecognized -Wno-foo # switches, so we have to test for the positive form and if that works, @@ -2148,23 +2303,59 @@ foreach w : negative_warning_flags if cc.has_argument('-W' + w) cflags_warn += '-Wno-' + w endif - if llvm.found() and cpp.has_argument('-W' + w) + if have_cxx and cxx.has_argument('-W' + w) cxxflags_warn += '-Wno-' + w endif endforeach if cc.get_id() == 'msvc' - cflags_warn += [ - '/wd4018', # signed/unsigned mismatch + msvc_common_warning_flags = [ + # Disable warnings in system headers + '/external:anglebrackets', + '/external:W0', + + # Warnings to disable: + # from /W2: '/wd4244', # conversion from 'type1' to 'type2', possible loss of data - '/wd4273', # inconsistent DLL linkage - '/wd4101', # unreferenced local variable - '/wd4102', # unreferenced label + + # Additional warnings to enable: + '/w24062', # enumerator 'identifier' in switch of enum 'enumeration' is not handled [like -Wswitch] + '/w24102', # unreferenced label [like -Wunused-label] + + # This one doesn't match a gcc warning, but it can help catch + # issues related to 4-byte long on Windows. + '/w24334', # 'operator': result of 32-bit shift implicitly converted to 64 bits (was 64-bit shift intended?) + + # Turn this into an error, to match other compilers (as of C99) + '/we4013', # 'function' undefined; assuming extern returning int' [like -Wimplicit-function-declaration] + ] + + msvc_c_warning_flags = [ + # Warnings to disable: + # from /W1: '/wd4090', # different 'modifier' qualifiers + # from /W3: + '/wd4018', # signed/unsigned mismatch + '/wd4101', # unreferenced local variable [like -Wunused-variable, but there is no "unused" attribute, so too noisy] '/wd4267', # conversion from 'size_t' to 'type', possible loss of data + + # Additional warnings to enable: + '/w24255', # 'function' : no function prototype given: converting '()' to '(void)' [like -Wstrict-prototypes] ] + msvc_cxx_warning_flags = [ + # Warnings to disable: + # from /W2: + '/wd4200', # nonstandard extension used: zero-sized array in struct/union [widely used in PostgreSQL C headers] + ] + + cflags_warn += msvc_common_warning_flags + cflags_warn += msvc_c_warning_flags + + cxxflags_warn += msvc_common_warning_flags + cxxflags_warn += msvc_cxx_warning_flags + cppflags += [ '/DWIN32', '/DWINDOWS', @@ -2209,8 +2400,8 @@ elif optimization == 's' endif cflags_builtin = cc.get_supported_arguments(common_builtin_flags) -if llvm.found() - cxxflags_builtin = cpp.get_supported_arguments(common_builtin_flags) +if have_cxx + cxxflags_builtin = cxx.get_supported_arguments(common_builtin_flags) endif @@ -2302,6 +2493,33 @@ int main(void) endif +############################################################### +# Check if the compiler supports AVX2 as a target +# There is deliberately not a guard for __has_attribute here +############################################################### + +if host_cpu == 'x86_64' + + prog = ''' +__attribute__((target("avx2"))) +static int avx2_test(void) +{ + return 0; +} + +int main(void) +{ + return avx2_test(); +} +''' + + if cc.links(prog, name: 'AVX2 support', args: test_c_args) + cdata.set('USE_AVX2_WITH_RUNTIME_CHECK', 1) + endif + +endif + + ############################################################### # Check for the availability of AVX-512 popcount intrinsics. ############################################################### @@ -2465,6 +2683,7 @@ int main(void) { __m128i z; + x = _mm512_xor_si512(_mm512_zextsi128_si512(_mm_cvtsi32_si128(0)), x); y = _mm512_clmulepi64_epi128(x, y, 0); z = _mm_ternarylogic_epi64( _mm512_castsi512_si128(y), @@ -2502,7 +2721,11 @@ int main(void) } ''' - if cc.links(prog, name: '__crc32cb, __crc32ch, __crc32cw, and __crc32cd without -march=armv8-a+crc', + # Vendor-supported versions of Windows for AArch64 require at least ARMv8.1, + # which is where CRC extension support became mandatory. Thus, use it + # unconditionally on MSVC/AArch64. + if (host_cpu == 'aarch64' and cc.get_id() == 'msvc') or \ + cc.links(prog, name: '__crc32cb, __crc32ch, __crc32cw, and __crc32cd without -march=armv8-a+crc', args: test_c_args) # Use ARM CRC Extension unconditionally cdata.set('USE_ARMV8_CRC32C', 1) @@ -2523,6 +2746,39 @@ int main(void) have_optimized_crc = true endif + # Check if the compiler supports Arm CRYPTO PMULL (carryless multiplication) + # instructions used for vectorized CRC. + prog = ''' +#include +#include +uint64x2_t a; +uint64x2_t b; +uint64x2_t c; + +#if defined(__has_attribute) && __has_attribute (target) +__attribute__((target("+crypto"))) +#endif +int main(void) +{ + uint64x2_t r1; + uint64x2_t r2; + + __asm("pmull %0.1q, %2.1d, %3.1d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r1), "+w"(c):"w"(a), "w"(b)); + __asm("pmull2 %0.1q, %2.2d, %3.2d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r2), "+w"(c):"w"(a), "w"(b)); + + r1 = veorq_u64(r1, r2); + /* return computed value, to prevent the above being optimized away */ + return (int) vgetq_lane_u64(r1, 0); +} +''' + + if cc.links(prog, + name: 'CRYPTO CRC32C', + args: test_c_args) + # Use ARM CRYPTO Extension, with runtime check + cdata.set('USE_PMULL_CRC32C_WITH_RUNTIME_CHECK', 1) + endif + elif host_cpu == 'loongarch64' prog = ''' @@ -2563,7 +2819,9 @@ endif if host_cpu == 'x86_64' - if cc.compiles(''' + if cc.get_id() == 'msvc' + cdata.set('HAVE_X86_64_POPCNTQ', 1) + elif cc.compiles(''' void main(void) { long long x = 1; long long r; @@ -2605,13 +2863,11 @@ endif # XXX: Might be worth conditioning some checks on the OS, to avoid doing # unnecessary checks over and over, particularly on windows. header_checks = [ - 'atomic.h', 'copyfile.h', 'crtdefs.h', 'execinfo.h', 'getopt.h', 'ifaddrs.h', - 'mbarrier.h', 'strings.h', 'sys/epoll.h', 'sys/event.h', @@ -2621,6 +2877,7 @@ header_checks = [ 'sys/signalfd.h', 'sys/ucred.h', 'termios.h', + 'uchar.h', 'ucred.h', 'xlocale.h', ] @@ -2642,7 +2899,6 @@ decl_checks = [ ['posix_fadvise', 'fcntl.h'], ['strlcat', 'string.h'], ['strlcpy', 'string.h'], - ['strnlen', 'string.h'], ['strsep', 'string.h'], ['timingsafe_bcmp', 'string.h'], ] @@ -2657,14 +2913,6 @@ decl_checks += [ ['memset_s', 'string.h', '#define __STDC_WANT_LIB_EXT1__ 1'], ] -# Check presence of some optional LLVM functions. -if llvm.found() - decl_checks += [ - ['LLVMCreateGDBRegistrationListener', 'llvm-c/ExecutionEngine.h'], - ['LLVMCreatePerfJITEventListener', 'llvm-c/ExecutionEngine.h'], - ] -endif - foreach c : decl_checks func = c.get(0) header = c.get(1) @@ -2736,6 +2984,10 @@ if cc.has_member('struct sockaddr', 'sa_len', cdata.set('HAVE_STRUCT_SOCKADDR_SA_LEN', 1) endif +if cc.has_header_symbol('signal.h', 'SA_SIGINFO') + cdata.set('HAVE_SA_SIGINFO', 1) +endif + if cc.has_member('struct tm', 'tm_zone', args: test_c_args, include_directories: postgres_inc, prefix: ''' @@ -2810,11 +3062,94 @@ int main(void) endif endforeach +# Check if the C++ compiler understands typeof or a variant. +if have_cxx + foreach kw : ['typeof', '__typeof__'] + if cxx.compiles(''' +int main(void) +{ + int x = 0; + @0@(x) y; + y = x; + return y; +} +'''.format(kw), + name: 'C++ ' + kw, + args: test_c_args, include_directories: postgres_inc) + + cdata.set('HAVE_CXX_TYPEOF', 1) + if kw != 'typeof' + cdata.set('pg_cxx_typeof', kw) + endif + + break + endif + endforeach +endif + +# Check if the C compiler understands typeof_unqual or a variant. Define +# HAVE_TYPEOF_UNQUAL if so, and define 'typeof_unqual' to the actual key word. +# +# Test with a void pointer, because MSVC doesn't handle that, and we +# need that for copyObject(). +foreach kw : ['typeof_unqual', '__typeof_unqual__'] + if cc.compiles(''' +int main(void) +{ + int x = 0; + @0@(x) y; + const void *a; + void *b; + y = x; + b = (@0@(*a) *) a; + return y; +} +'''.format(kw), + name: kw, + args: test_c_args, include_directories: postgres_inc) + + cdata.set('HAVE_TYPEOF_UNQUAL', 1) + if kw != 'typeof_unqual' + cdata.set('typeof_unqual', kw) + endif + + break + endif +endforeach + +# Check if the C++ compiler understands typeof_unqual or a variant. +if have_cxx + foreach kw : ['typeof_unqual', '__typeof_unqual__'] + if cxx.compiles(''' +int main(void) +{ + int x = 0; + @0@(x) y; + const void *a; + void *b; + y = x; + b = (@0@(*a) *) a; + return y; +} +'''.format(kw), + name: 'C++ ' + kw, + args: test_c_args, include_directories: postgres_inc) + + cdata.set('HAVE_CXX_TYPEOF_UNQUAL', 1) + if kw != 'typeof_unqual' + cdata.set('pg_cxx_typeof_unqual', kw) + endif + + break + endif + endforeach +endif + -# MSVC doesn't cope well with defining restrict to __restrict, the spelling it -# understands, because it conflicts with __declspec(restrict). Therefore we -# define pg_restrict to the appropriate definition, which presumably won't -# conflict. +# MSVC doesn't cope well with defining restrict to __restrict, the +# spelling it understands, because it conflicts with +# __declspec(restrict) in C++ mode. Therefore we define pg_restrict +# to the appropriate definition, which presumably won't conflict. # # We assume C99 support, so we don't need to make this conditional. cdata.set('pg_restrict', '__restrict') @@ -2841,7 +3176,7 @@ gnugetopt_dep = cc.find_library('gnugetopt', required: false) # (i.e., allow '-' as a flag character), so use our version on those platforms # - We want to use system's getopt_long() only if the system provides struct # option -always_replace_getopt = host_system in ['windows', 'cygwin', 'openbsd', 'solaris'] +always_replace_getopt = host_system in ['windows', 'cygwin', 'openbsd', 'sunos'] always_replace_getopt_long = host_system in ['windows', 'cygwin'] or not cdata.has('HAVE_STRUCT_OPTION') # Required on BSDs @@ -2872,6 +3207,7 @@ func_checks = [ ['dlsym', {'dependencies': [dl_dep], 'define': false}], ['elf_aux_info'], ['explicit_bzero'], + ['explicit_memset'], ['getauxval'], ['getifaddrs'], ['getopt', {'dependencies': [getopt_dep, gnugetopt_dep], 'skip': always_replace_getopt}], @@ -2883,6 +3219,7 @@ func_checks = [ ['kqueue'], ['localeconv_l'], ['mbstowcs_l'], + ['memset_explicit'], ['mkdtemp'], ['posix_fadvise'], ['posix_fallocate'], @@ -2899,7 +3236,6 @@ func_checks = [ ['strerror_r', {'dependencies': [thread_dep]}], ['strlcat'], ['strlcpy'], - ['strnlen'], ['strsep'], ['strsignal'], ['sync_file_range'], @@ -3033,10 +3369,12 @@ add_project_link_arguments(ldflags, language: ['c', 'cpp']) # list of targets for various alias targets backend_targets = [] bin_targets = [] +libpq_targets = [] pl_targets = [] contrib_targets = [] testprep_targets = [] nls_targets = [] +update_unicode_targets = [] # Define the tests to distribute them to the correct test styles later @@ -3124,6 +3462,8 @@ gen_export_kwargs = { 'install': false, } +# command to create stamp files on all OSs +stamp_cmd = [python, '-c', 'import sys; open(sys.argv[1], "w")', '@OUTPUT0@'] ### @@ -3145,13 +3485,13 @@ gen_kwlist_cmd = [ ### if host_system == 'windows' - pg_ico = meson.source_root() / 'src' / 'port' / 'win32.ico' + pg_ico = meson.project_source_root() / 'src' / 'port' / 'win32.ico' win32ver_rc = files('src/port/win32ver.rc') rcgen = find_program('src/tools/rcgen', native: true) rcgen_base_args = [ '--srcdir', '@SOURCE_DIR@', - '--builddir', meson.build_root(), + '--builddir', meson.project_build_root(), '--rcout', '@OUTPUT0@', '--out', '@OUTPUT1@', '--input', '@INPUT@', @@ -3160,11 +3500,11 @@ if host_system == 'windows' if cc.get_argument_syntax() == 'msvc' rc = find_program('rc', required: true) - rcgen_base_args += ['--rc', rc.path()] + rcgen_base_args += ['--rc', rc.full_path()] rcgen_outputs = ['@BASENAME@.rc', '@BASENAME@.res'] else windres = find_program('windres', required: true) - rcgen_base_args += ['--windres', windres.path()] + rcgen_base_args += ['--windres', windres.full_path()] rcgen_outputs = ['@BASENAME@.rc', '@BASENAME@.obj'] endif @@ -3241,14 +3581,14 @@ subdir('src/port') frontend_common_code = declare_dependency( compile_args: ['-DFRONTEND'], include_directories: [postgres_inc], - sources: generated_headers, + sources: generated_headers_stamp, dependencies: [os_deps, zlib, zstd, lz4], ) backend_common_code = declare_dependency( compile_args: ['-DBUILDING_DLL'], include_directories: [postgres_inc], - sources: generated_headers, + sources: generated_headers_stamp, dependencies: [os_deps, zlib, zstd], ) @@ -3263,7 +3603,7 @@ shlib_code = declare_dependency( frontend_stlib_code = declare_dependency( include_directories: [postgres_inc], link_with: [common_static, pgport_static], - sources: generated_headers, + sources: generated_headers_stamp, dependencies: [os_deps, libintl], ) @@ -3271,7 +3611,7 @@ frontend_stlib_code = declare_dependency( frontend_shlib_code = declare_dependency( include_directories: [postgres_inc], link_with: [common_shlib, pgport_shlib], - sources: generated_headers, + sources: generated_headers_stamp, dependencies: [shlib_code, os_deps, libintl], ) @@ -3281,7 +3621,7 @@ frontend_shlib_code = declare_dependency( frontend_no_fe_utils_code = declare_dependency( include_directories: [postgres_inc], link_with: [common_static, pgport_static], - sources: generated_headers, + sources: generated_headers_stamp, dependencies: [os_deps, libintl], ) @@ -3296,6 +3636,7 @@ libpq_deps += [ ] libpq_oauth_deps += [ + thread_dep, libcurl, ] @@ -3308,7 +3649,7 @@ subdir('src/interfaces/libpq-oauth') frontend_code = declare_dependency( include_directories: [postgres_inc], link_with: [fe_utils, common_static, pgport_static], - sources: generated_headers, + sources: generated_headers_stamp, dependencies: [os_deps, libintl], ) @@ -3338,7 +3679,7 @@ backend_code = declare_dependency( include_directories: [postgres_inc], link_args: ldflags_be, link_with: [], - sources: generated_headers + generated_backend_headers, + sources: generated_backend_headers_stamp, dependencies: os_deps + backend_both_deps + backend_deps, ) @@ -3397,7 +3738,7 @@ foreach t1 : configure_files potentially_conflicting_files += meson.current_build_dir() / t endforeach foreach sub, fnames : generated_sources_ac - sub = meson.build_root() / sub + sub = meson.project_build_root() / sub foreach fname : fnames potentially_conflicting_files += sub / fname endforeach @@ -3406,7 +3747,7 @@ endforeach # find and report conflicting files foreach build_path : potentially_conflicting_files build_path = host_system == 'windows' ? fs.as_posix(build_path) : build_path - # str.replace is in 0.56 + # str.replace is in meson 0.58.0. src_path = meson.current_source_dir() / build_path.split(meson.current_build_dir() / '')[1] if fs.exists(src_path) or fs.is_symlink(src_path) conflicting_files += src_path @@ -3456,7 +3797,7 @@ endif installed_targets = [ backend_targets, bin_targets, - libpq_st, + libpq_targets, pl_targets, contrib_targets, nls_mo_targets, @@ -3497,7 +3838,7 @@ run_target('install-test-files', ############################################################### # DESTDIR for the installation we'll run tests in -test_install_destdir = meson.build_root() / 'tmp_install/' +test_install_destdir = meson.project_build_root() / 'tmp_install/' # DESTDIR + prefix appropriately munged if build_system != 'windows' @@ -3540,7 +3881,7 @@ test('install_test_files', is_parallel: false, suite: ['setup']) -test_result_dir = meson.build_root() / 'testrun' +test_result_dir = meson.project_build_root() / 'testrun' # XXX: pg_regress doesn't assign unique ports on windows. To avoid the @@ -3551,12 +3892,12 @@ testport = 40000 test_env = environment() -test_initdb_template = meson.build_root() / 'tmp_install' / 'initdb-template' +test_initdb_template = meson.project_build_root() / 'tmp_install' / 'initdb-template' test_env.set('PG_REGRESS', pg_regress.full_path()) test_env.set('REGRESS_SHLIB', regress_module.full_path()) test_env.set('INITDB_TEMPLATE', test_initdb_template) # for Cluster.pm's portlock logic -test_env.set('top_builddir', meson.build_root()) +test_env.set('top_builddir', meson.project_build_root()) # Add the temporary installation to the library search path on platforms where # that works (everything but windows, basically). On windows everything @@ -3600,26 +3941,20 @@ sys.exit(sp.returncode) # Test Generation ############################################################### -# When using a meson version understanding exclude_suites, define a -# 'tmp_install' test setup (the default) that excludes tests running against a -# pre-existing install and a 'running' setup that conflicts with creation of -# the temporary installation and tap tests (which don't support running -# against a running server). +# Define a 'tmp_install' test setup (the default) that excludes tests +# running against a pre-existing install and a 'running' setup that +# conflicts with creation of the temporary installation and tap tests +# (which don't support running against a running server). running_suites = [] install_suites = [] -if meson.version().version_compare('>=0.57') - runningcheck = true -else - runningcheck = false -endif testwrap = files('src/tools/testwrap') foreach test_dir : tests testwrap_base = [ testwrap, - '--basedir', meson.build_root(), + '--basedir', meson.project_build_root(), '--srcdir', test_dir['sd'], # Some test suites are not run by default but can be run if selected by the # user via variable PG_TEST_EXTRA. Pass configuration time value of @@ -3668,11 +4003,9 @@ foreach test_dir : tests '--dbname', dbname, ] + t.get('regress_args', []) - test_selection = [] - if t.has_key('schedule') - test_selection += ['--schedule', t['schedule'],] - endif + test_schedule = t.get('schedule', []) + test_selection = [] if kind == 'isolation' test_selection += t.get('specs', []) else @@ -3696,12 +4029,13 @@ foreach test_dir : tests testwrap_base, '--testgroup', test_group, '--testname', kind, + '--schedule', test_schedule, + '--tests', test_selection, '--', test_command_base, '--outputdir', test_output, '--temp-instance', test_output / 'tmp_check', '--port', testport.to_string(), - test_selection, ], suite: test_group, kwargs: test_kwargs, @@ -3709,17 +4043,18 @@ foreach test_dir : tests install_suites += test_group # some tests can't support running against running DB - if runningcheck and t.get('runningcheck', true) + if t.get('runningcheck', true) test(test_group_running / kind, python, args: [ testwrap_base, '--testgroup', test_group_running, '--testname', kind, + '--schedule', test_schedule, + '--tests', test_selection, '--', test_command_base, '--outputdir', test_output_running, - test_selection, ], is_parallel: t.get('runningcheck-parallel', true), suite: test_group_running, @@ -3736,8 +4071,8 @@ foreach test_dir : tests endif test_command = [ - perl.path(), - '-I', meson.source_root() / 'src/test/perl', + perl.full_path(), + '-I', meson.project_source_root() / 'src/test/perl', '-I', test_dir['sd'], ] @@ -3792,13 +4127,26 @@ foreach test_dir : tests endforeach # directories with tests # repeat condition so meson realizes version dependency -if meson.version().version_compare('>=0.57') - add_test_setup('tmp_install', - is_default: true, - exclude_suites: running_suites) - add_test_setup('running', - exclude_suites: ['setup'] + install_suites) -endif +add_test_setup('tmp_install', + is_default: true, + exclude_suites: running_suites) +add_test_setup('running', + exclude_suites: ['setup'] + install_suites) + + + +############################################################### +# headerscheck +############################################################### + +headerscheck = files('src/tools/pginclude/headerscheck') +run_target('headerscheck', + command: [headerscheck, meson.project_source_root(), meson.project_build_root()] +) + +run_target('cpluspluscheck', + command: [headerscheck, '--cplusplus', meson.project_source_root(), meson.project_build_root()] +) @@ -3807,7 +4155,7 @@ endif ############################################################### alias_target('backend', backend_targets) -alias_target('bin', bin_targets + [libpq_st]) +alias_target('bin', bin_targets + libpq_targets) alias_target('pl', pl_targets) alias_target('contrib', contrib_targets) alias_target('testprep', testprep_targets) @@ -3815,6 +4163,10 @@ alias_target('testprep', testprep_targets) alias_target('world', all_built, docs) alias_target('install-world', install_quiet, installdocs) +if update_unicode_targets.length() > 0 + alias_target('update-unicode', update_unicode_targets) +endif + run_target('help', command: [ perl, '-ne', 'next if /^#/; print', @@ -3855,7 +4207,7 @@ tar_gz = custom_target('tar.gz', '--format', 'tar.gz', '-9', '--prefix', distdir + '/', - '-o', join_paths(meson.build_root(), '@OUTPUT@'), + '-o', join_paths(meson.project_build_root(), '@OUTPUT@'), pg_git_revision], output: distdir + '.tar.gz', ) @@ -3865,11 +4217,11 @@ if bzip2.found() build_always_stale: true, command: [git, '-C', '@SOURCE_ROOT@', '-c', 'core.autocrlf=false', - '-c', 'tar.tar.bz2.command="@0@" -c'.format(bzip2.path()), + '-c', 'tar.tar.bz2.command="@0@" -c'.format(bzip2.full_path()), 'archive', '--format', 'tar.bz2', '--prefix', distdir + '/', - '-o', join_paths(meson.build_root(), '@OUTPUT@'), + '-o', join_paths(meson.project_build_root(), '@OUTPUT@'), pg_git_revision], output: distdir + '.tar.bz2', ) @@ -3886,10 +4238,7 @@ alias_target('pgdist', [tar_gz, tar_bz2]) # But not if we are in a subproject, in case the parent project wants to # create a dist using the standard Meson command. if not meson.is_subproject() - # We can only pass the identifier perl here when we depend on >= 0.55 - if meson.version().version_compare('>=0.55') - meson.add_dist_script(perl, '-e', 'exit 1') - endif + meson.add_dist_script(perl, '-e', 'exit 1') endif @@ -3898,106 +4247,102 @@ endif # The End, The End, My Friend ############################################################### -if meson.version().version_compare('>=0.57') +summary( + { + 'data block size': '@0@ kB'.format(cdata.get('BLCKSZ') / 1024), + 'WAL block size': '@0@ kB'.format(cdata.get('XLOG_BLCKSZ') / 1024), + 'segment size': get_option('segsize_blocks') != 0 ? + '@0@ blocks'.format(cdata.get('RELSEG_SIZE')) : + '@0@ GB'.format(get_option('segsize')), + }, + section: 'Data layout', +) - summary( - { - 'data block size': '@0@ kB'.format(cdata.get('BLCKSZ') / 1024), - 'WAL block size': '@0@ kB'.format(cdata.get('XLOG_BLCKSZ') / 1024), - 'segment size': get_option('segsize_blocks') != 0 ? - '@0@ blocks'.format(cdata.get('RELSEG_SIZE')) : - '@0@ GB'.format(get_option('segsize')), - }, - section: 'Data layout', - ) +summary( + { + 'host system': '@0@ @1@'.format(host_system, host_cpu), + 'build system': '@0@ @1@'.format(build_machine.system(), + build_machine.cpu_family()), + }, + section: 'System', +) - summary( - { - 'host system': '@0@ @1@'.format(host_system, host_cpu), - 'build system': '@0@ @1@'.format(build_machine.system(), - build_machine.cpu_family()), - }, - section: 'System', - ) +summary( + { + 'linker': '@0@'.format(cc.get_linker_id()), + 'C compiler': '@0@ @1@'.format(cc.get_id(), cc.version()), + }, + section: 'Compiler', +) +summary( + { + 'CPP FLAGS': ' '.join(cppflags), + 'C FLAGS, functional': ' '.join(cflags), + 'C FLAGS, warnings': ' '.join(cflags_warn), + 'C FLAGS, modules': ' '.join(cflags_mod), + 'C FLAGS, user specified': ' '.join(get_option('c_args')), + 'LD FLAGS': ' '.join(ldflags + get_option('c_link_args')), + }, + section: 'Compiler Flags', +) + +if have_cxx summary( { - 'linker': '@0@'.format(cc.get_linker_id()), - 'C compiler': '@0@ @1@'.format(cc.get_id(), cc.version()), + 'C++ compiler': '@0@ @1@'.format(cxx.get_id(), cxx.version()), }, section: 'Compiler', ) summary( { - 'CPP FLAGS': ' '.join(cppflags), - 'C FLAGS, functional': ' '.join(cflags), - 'C FLAGS, warnings': ' '.join(cflags_warn), - 'C FLAGS, modules': ' '.join(cflags_mod), - 'C FLAGS, user specified': ' '.join(get_option('c_args')), - 'LD FLAGS': ' '.join(ldflags + get_option('c_link_args')), + 'C++ FLAGS, functional': ' '.join(cxxflags), + 'C++ FLAGS, warnings': ' '.join(cxxflags_warn), + 'C++ FLAGS, user specified': ' '.join(get_option('cpp_args')), }, section: 'Compiler Flags', ) +endif - if llvm.found() - summary( - { - 'C++ compiler': '@0@ @1@'.format(cpp.get_id(), cpp.version()), - }, - section: 'Compiler', - ) - - summary( - { - 'C++ FLAGS, functional': ' '.join(cxxflags), - 'C++ FLAGS, warnings': ' '.join(cxxflags_warn), - 'C++ FLAGS, user specified': ' '.join(get_option('cpp_args')), - }, - section: 'Compiler Flags', - ) - endif - - summary( - { - 'bison': '@0@ @1@'.format(bison.full_path(), bison_version), - 'dtrace': dtrace, - 'flex': '@0@ @1@'.format(flex.full_path(), flex_version), - }, - section: 'Programs', - ) - - summary( - { - 'bonjour': bonjour, - 'bsd_auth': bsd_auth, - 'docs': docs_dep, - 'docs_pdf': docs_pdf_dep, - 'gss': gssapi, - 'icu': icu, - 'ldap': ldap, - 'libcurl': libcurl, - 'libnuma': libnuma, - 'liburing': liburing, - 'libxml': libxml, - 'libxslt': libxslt, - 'llvm': llvm, - 'lz4': lz4, - 'nls': libintl, - 'openssl': ssl, - 'pam': pam, - 'plperl': [perl_dep, perlversion], - 'plpython': python3_dep, - 'pltcl': tcl_dep, - 'readline': readline, - 'selinux': selinux, - 'systemd': systemd, - 'uuid': uuid, - 'zlib': zlib, - 'zstd': zstd, - }, - section: 'External libraries', - list_sep: ' ', - ) +summary( + { + 'bison': '@0@ @1@'.format(bison.full_path(), bison_version), + 'dtrace': dtrace, + 'flex': '@0@ @1@'.format(flex.full_path(), flex_version), + }, + section: 'Programs', +) -endif +summary( + { + 'bonjour': bonjour, + 'bsd_auth': bsd_auth, + 'docs': docs_dep, + 'docs_pdf': docs_pdf_dep, + 'gss': gssapi, + 'icu': icu, + 'ldap': ldap, + 'libcurl': libcurl, + 'libnuma': libnuma, + 'liburing': liburing, + 'libxml': libxml, + 'libxslt': libxslt, + 'llvm': llvm, + 'lz4': lz4, + 'nls': libintl, + 'openssl': ssl, + 'pam': pam, + 'plperl': [perl_dep, perlversion], + 'plpython': python3_dep, + 'pltcl': tcl_dep, + 'readline': readline, + 'selinux': selinux, + 'systemd': systemd, + 'uuid': uuid, + 'zlib': zlib, + 'zstd': zstd, + }, + section: 'External libraries', + list_sep: ' ', +) diff --git a/meson_options.txt b/meson_options.txt index 06bf5627d3c03..6a793f3e47943 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Data layout influencing options diff --git a/src/Makefile b/src/Makefile index 2f31a2f20a713..a501de0f5cc57 100644 --- a/src/Makefile +++ b/src/Makefile @@ -23,6 +23,7 @@ SUBDIRS = \ interfaces \ backend/replication/libpqwalreceiver \ backend/replication/pgoutput \ + backend/replication/pgrepack \ fe_utils \ bin \ pl \ diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 04952b533ded9..cef1ad7f87d98 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -254,7 +254,7 @@ CPP = @CPP@ CPPFLAGS = @CPPFLAGS@ PG_SYSROOT = @PG_SYSROOT@ -override CPPFLAGS := $(ICU_CFLAGS) $(LIBNUMA_CFLAGS) $(LIBURING_CFLAGS) $(CPPFLAGS) +override CPPFLAGS += $(ICU_CFLAGS) $(LIBNUMA_CFLAGS) $(LIBURING_CFLAGS) ifdef PGXS override CPPFLAGS := -I$(includedir_server) -I$(includedir_internal) $(CPPFLAGS) @@ -267,7 +267,6 @@ endif # not PGXS CC = @CC@ GCC = @GCC@ -SUN_STUDIO_CC = @SUN_STUDIO_CC@ CXX = @CXX@ CFLAGS = @CFLAGS@ CFLAGS_SL = @CFLAGS_SL@ @@ -281,6 +280,8 @@ PERMIT_DECLARATION_AFTER_STATEMENT = @PERMIT_DECLARATION_AFTER_STATEMENT@ PERMIT_MISSING_VARIABLE_DECLARATIONS = @PERMIT_MISSING_VARIABLE_DECLARATIONS@ CXXFLAGS = @CXXFLAGS@ +have_cxx = @have_cxx@ + LLVM_CPPFLAGS = @LLVM_CPPFLAGS@ LLVM_CFLAGS = @LLVM_CFLAGS@ LLVM_CXXFLAGS = @LLVM_CXXFLAGS@ @@ -293,6 +294,7 @@ FLEX = @FLEX@ FLEXFLAGS = @FLEXFLAGS@ $(LFLAGS) DTRACE = @DTRACE@ DTRACEFLAGS = @DTRACEFLAGS@ +NM = @NM@ ZIC = @ZIC@ # Linking @@ -374,10 +376,10 @@ DOWNLOAD = wget -O $@ --no-use-server-timestamps # Pick a release from here: . Note # that the most recent release listed there is often a pre-release; # don't pick that one, except for testing. -UNICODE_VERSION = 16.0.0 +UNICODE_VERSION = 17.0.0 # Pick a release from here: -CLDR_VERSION = 47 +CLDR_VERSION = 48.2 # Tree-wide build support @@ -796,9 +798,6 @@ ifeq ($(PORTNAME),win32) LIBS += -lws2_32 endif -# Not really standard libc functions, used by the backend. -TAS = @TAS@ - ########################################################################## # diff --git a/src/Makefile.shlib b/src/Makefile.shlib index fa81f6ffdd6d9..cd1543550ca5b 100644 --- a/src/Makefile.shlib +++ b/src/Makefile.shlib @@ -106,13 +106,27 @@ ifdef SO_MAJOR_VERSION override CPPFLAGS += -DSO_MAJOR_VERSION=$(SO_MAJOR_VERSION) endif +ifeq ($(PORTNAME), aix) + LINK.shared = $(COMPILER) + ifdef SO_MAJOR_VERSION + shlib = lib$(NAME)$(DLSUFFIX).$(SO_MAJOR_VERSION) + endif + haslibarule = yes + # $(exports_file) is also usable as an import file + exports_file = lib$(NAME).exp + BUILD.exports = ( echo '\#! $(shlib)'; $(AWK) '/^[^\#]/ {printf "%s\n",$$1}' $< ) > $@ + ifneq (,$(SHLIB_EXPORTS)) + LINK.shared += -Wl,-bE:$(exports_file) + endif +endif + ifeq ($(PORTNAME), darwin) ifdef soname # linkable library ifneq ($(SO_MAJOR_VERSION), 0) version_link = -compatibility_version $(SO_MAJOR_VERSION) -current_version $(SO_MAJOR_VERSION).$(SO_MINOR_VERSION) endif - LINK.shared = $(COMPILER) -dynamiclib -install_name '$(libdir)/lib$(NAME).$(SO_MAJOR_VERSION)$(DLSUFFIX)' $(version_link) $(exported_symbols_list) + LINK.shared = $(COMPILER) -dynamiclib -install_name '$(libdir)/lib$(NAME).$(SO_MAJOR_VERSION)$(DLSUFFIX)' $(version_link) shlib = lib$(NAME).$(SO_MAJOR_VERSION)$(DLSUFFIX) shlib_major = lib$(NAME).$(SO_MAJOR_VERSION)$(DLSUFFIX) else @@ -122,7 +136,7 @@ ifeq ($(PORTNAME), darwin) BUILD.exports = $(AWK) '/^[^\#]/ {printf "_%s\n",$$1}' $< >$@ exports_file = $(SHLIB_EXPORTS:%.txt=%.list) ifneq (,$(exports_file)) - exported_symbols_list = -exported_symbols_list $(exports_file) + LINK.shared += -exported_symbols_list $(exports_file) endif endif @@ -254,6 +268,14 @@ $(stlib): $(OBJS) | $(SHLIB_PREREQS) touch $@ endif #haslibarule +# AIX wraps shared libraries inside a static library, which can be used both +# for static and shared linking +ifeq ($(PORTNAME), aix) +$(stlib): $(shlib) + rm -f $(stlib) + $(AR) $(AROPT) $(stlib) $(shlib) +endif # aix + ifeq (,$(filter cygwin win32,$(PORTNAME))) # Normal case @@ -267,8 +289,11 @@ ifneq ($(shlib), $(shlib_major)) endif # Make sure we have a link to a name without any version numbers ifneq ($(shlib), $(shlib_bare)) +# except on AIX, where that's not a thing +ifneq ($(PORTNAME), aix) rm -f $(shlib_bare) $(LN_S) $(shlib) $(shlib_bare) +endif # aix endif # shlib_bare endif # shlib_major @@ -376,6 +401,9 @@ install-lib-static: $(stlib) installdirs-lib install-lib-shared: $(shlib) installdirs-lib ifdef soname +# we don't install $(shlib) on AIX +# (see http://archives.postgresql.org/message-id/52EF20B2E3209443BC37736D00C3C1380A6E79FE@EXADV1.host.magwien.gv.at) +ifneq ($(PORTNAME), aix) $(INSTALL_SHLIB) $< '$(DESTDIR)$(libdir)/$(shlib)' ifneq ($(PORTNAME), cygwin) ifneq ($(PORTNAME), win32) @@ -391,6 +419,7 @@ ifneq ($(shlib), $(shlib_bare)) endif endif # not win32 endif # not cygwin +endif # not aix ifneq (,$(findstring $(PORTNAME),win32 cygwin)) $(INSTALL_SHLIB) $< '$(DESTDIR)$(bindir)/$(shlib)' endif diff --git a/src/backend/Makefile b/src/backend/Makefile index 7344c8c7f5c65..162d3f1f2a982 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -2,7 +2,7 @@ # # Makefile for the postgres backend # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/Makefile @@ -16,11 +16,33 @@ subdir = src/backend top_builddir = ../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = access archive backup bootstrap catalog parser commands executor \ - foreign lib libpq \ - main nodes optimizer partitioning port postmaster \ - regex replication rewrite \ - statistics storage tcop tsearch utils $(top_builddir)/src/timezone \ +SUBDIRS = \ + access \ + archive \ + backup \ + bootstrap \ + catalog \ + parser \ + commands \ + executor \ + foreign \ + lib \ + libpq \ + main \ + nodes \ + optimizer \ + partitioning \ + port \ + postmaster \ + regex \ + replication \ + rewrite \ + statistics \ + storage \ + tcop \ + tsearch \ + utils \ + $(top_builddir)/src/timezone \ jit include $(srcdir)/common.mk @@ -63,12 +85,14 @@ all: submake-libpgport submake-catalog-headers submake-utils-headers postgres $( ifneq ($(PORTNAME), cygwin) ifneq ($(PORTNAME), win32) +ifneq ($(PORTNAME), aix) postgres: $(OBJS) $(CC) $(CFLAGS) $(call expand_subsys,$^) $(LDFLAGS) $(LIBS) -o $@ endif endif +endif ifeq ($(PORTNAME), cygwin) @@ -95,6 +119,24 @@ libpostgres.a: postgres endif # win32 +ifeq ($(PORTNAME), aix) + +postgres: $(POSTGRES_IMP) + $(CC) $(CFLAGS) $(call expand_subsys,$(OBJS)) $(LDFLAGS) -Wl,-bE:$(top_builddir)/src/backend/$(POSTGRES_IMP) $(LIBS) -Wl,-brtllib -o $@ + +# Linking to a single .o with -r is a lot faster than building a .a or passing +# all objects to MKLDEXPORT. +# +# It looks alluring to use $(CC) -r instead of ld -r, but that doesn't +# trivially work with gcc, due to gcc specific static libraries linked in with +# -r. +$(POSTGRES_IMP): $(OBJS) + ld -b64 -r -o SUBSYS.o $(call expand_subsys,$^) + $(MKLDEXPORT) SUBSYS.o . > $@ + @rm -f SUBSYS.o + +endif # aix + $(top_builddir)/src/port/libpgport_srv.a: | submake-libpgport @@ -114,9 +156,6 @@ parser/gram.h: parser/gram.y storage/lmgr/lwlocknames.h: storage/lmgr/generate-lwlocknames.pl ../include/storage/lwlocklist.h utils/activity/wait_event_names.txt $(MAKE) -C storage/lmgr lwlocknames.h -utils/activity/wait_event_types.h: utils/activity/generate-wait_event_types.pl utils/activity/wait_event_names.txt - $(MAKE) -C utils/activity wait_event_types.h pgstat_wait_event.c wait_event_funcs_data.c - # run this unconditionally to avoid needing to know its dependencies here: submake-catalog-headers: $(MAKE) -C ../include/catalog generated-headers @@ -141,18 +180,13 @@ submake-utils-headers: .PHONY: generated-headers -generated-headers: $(top_builddir)/src/include/storage/lwlocknames.h $(top_builddir)/src/include/utils/wait_event_types.h submake-catalog-headers submake-nodes-headers submake-utils-headers parser/gram.h +generated-headers: $(top_builddir)/src/include/storage/lwlocknames.h submake-catalog-headers submake-nodes-headers submake-utils-headers parser/gram.h $(top_builddir)/src/include/storage/lwlocknames.h: storage/lmgr/lwlocknames.h prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \ cd '$(dir $@)' && rm -f $(notdir $@) && \ $(LN_S) "$$prereqdir/$(notdir $<)" . -$(top_builddir)/src/include/utils/wait_event_types.h: utils/activity/wait_event_types.h - prereqdir=`cd '$(dir $<)' >/dev/null && pwd` && \ - cd '$(dir $@)' && rm -f $(notdir $@) && \ - $(LN_S) "$$prereqdir/$(notdir $<)" . - utils/probes.o: utils/probes.d $(SUBDIROBJS) $(DTRACE) $(DTRACEFLAGS) -C -G -s $(call expand_subsys,$^) -o $@ @@ -187,6 +221,7 @@ endif $(MAKE) -C utils install-data $(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample' $(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample' + $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' $(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample' ifeq ($(with_llvm), yes) @@ -246,6 +281,7 @@ endif $(MAKE) -C utils uninstall-data rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \ '$(DESTDIR)$(datadir)/pg_ident.conf.sample' \ + '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \ '$(DESTDIR)$(datadir)/postgresql.conf.sample' ifeq ($(with_llvm), yes) $(call uninstall_llvm_module,postgres) diff --git a/src/backend/access/Makefile b/src/backend/access/Makefile index 1932d11d154e3..e88d72ea0397d 100644 --- a/src/backend/access/Makefile +++ b/src/backend/access/Makefile @@ -8,7 +8,20 @@ subdir = src/backend/access top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = brin common gin gist hash heap index nbtree rmgrdesc spgist \ - sequence table tablesample transam +SUBDIRS = \ + brin \ + common \ + gin \ + gist \ + hash \ + heap \ + index \ + nbtree \ + rmgrdesc \ + spgist \ + sequence \ + table \ + tablesample \ + transam include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c index 01e1db7f856be..bdb30752e098c 100644 --- a/src/backend/access/brin/brin.c +++ b/src/backend/access/brin/brin.c @@ -4,7 +4,7 @@ * * See src/backend/access/brin/README for details. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -28,11 +28,14 @@ #include "catalog/index.h" #include "catalog/pg_am.h" #include "commands/vacuum.h" +#include "executor/instrument.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" #include "storage/bufmgr.h" +#include "storage/condition_variable.h" #include "storage/freespace.h" +#include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/acl.h" #include "utils/datum.h" @@ -42,6 +45,7 @@ #include "utils/memutils.h" #include "utils/rel.h" #include "utils/tuplesort.h" +#include "utils/wait_event.h" /* Magic numbers for parallel state sharing */ #define PARALLEL_KEY_BRIN_SHARED UINT64CONST(0xB000000000000001) @@ -68,7 +72,7 @@ typedef struct BrinShared int scantuplesortstates; /* Query ID, for report in worker processes */ - uint64 queryid; + int64 queryid; /* * workersdonecv is used to monitor the progress of workers. All parallel @@ -249,62 +253,63 @@ static void _brin_parallel_scan_and_build(BrinBuildState *state, Datum brinhandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = 0; - amroutine->amsupport = BRIN_LAST_OPTIONAL_PROCNUM; - amroutine->amoptsprocnum = BRIN_PROCNUM_OPTIONS; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = false; - amroutine->amcanhash = false; - amroutine->amconsistentequality = false; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = false; - amroutine->amcanunique = false; - amroutine->amcanmulticol = true; - amroutine->amoptionalkey = true; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = true; - amroutine->amstorage = true; - amroutine->amclusterable = false; - amroutine->ampredlocks = false; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = true; - amroutine->amcaninclude = false; - amroutine->amusemaintenanceworkmem = false; - amroutine->amsummarizing = true; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_CLEANUP; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = brinbuild; - amroutine->ambuildempty = brinbuildempty; - amroutine->aminsert = brininsert; - amroutine->aminsertcleanup = brininsertcleanup; - amroutine->ambulkdelete = brinbulkdelete; - amroutine->amvacuumcleanup = brinvacuumcleanup; - amroutine->amcanreturn = NULL; - amroutine->amcostestimate = brincostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = brinoptions; - amroutine->amproperty = NULL; - amroutine->ambuildphasename = NULL; - amroutine->amvalidate = brinvalidate; - amroutine->amadjustmembers = NULL; - amroutine->ambeginscan = brinbeginscan; - amroutine->amrescan = brinrescan; - amroutine->amgettuple = NULL; - amroutine->amgetbitmap = bringetbitmap; - amroutine->amendscan = brinendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - amroutine->amtranslatestrategy = NULL; - amroutine->amtranslatecmptype = NULL; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = 0, + .amsupport = BRIN_LAST_OPTIONAL_PROCNUM, + .amoptsprocnum = BRIN_PROCNUM_OPTIONS, + .amcanorder = false, + .amcanorderbyop = false, + .amcanhash = false, + .amconsistentequality = false, + .amconsistentordering = false, + .amcanbackward = false, + .amcanunique = false, + .amcanmulticol = true, + .amoptionalkey = true, + .amsearcharray = false, + .amsearchnulls = true, + .amstorage = true, + .amclusterable = false, + .ampredlocks = false, + .amcanparallel = false, + .amcanbuildparallel = true, + .amcaninclude = false, + .amusemaintenanceworkmem = false, + .amsummarizing = true, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_CLEANUP, + .amkeytype = InvalidOid, + + .ambuild = brinbuild, + .ambuildempty = brinbuildempty, + .aminsert = brininsert, + .aminsertcleanup = brininsertcleanup, + .ambulkdelete = brinbulkdelete, + .amvacuumcleanup = brinvacuumcleanup, + .amcanreturn = NULL, + .amcostestimate = brincostestimate, + .amgettreeheight = NULL, + .amoptions = brinoptions, + .amproperty = NULL, + .ambuildphasename = NULL, + .amvalidate = brinvalidate, + .amadjustmembers = NULL, + .ambeginscan = brinbeginscan, + .amrescan = brinrescan, + .amgettuple = NULL, + .amgetbitmap = bringetbitmap, + .amendscan = brinendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + .amtranslatestrategy = NULL, + .amtranslatecmptype = NULL, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -318,7 +323,7 @@ initialize_brin_insertstate(Relation idxRel, IndexInfo *indexInfo) MemoryContext oldcxt; oldcxt = MemoryContextSwitchTo(indexInfo->ii_Context); - bistate = palloc0(sizeof(BrinInsertState)); + bistate = palloc0_object(BrinInsertState); bistate->bis_desc = brin_build_desc(idxRel); bistate->bis_rmAccess = brinRevmapInitialize(idxRel, &bistate->bis_pages_per_range); @@ -573,7 +578,6 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm) Relation heapRel; BrinOpaque *opaque; BlockNumber nblocks; - BlockNumber heapBlk; int64 totalpages = 0; FmgrInfo *consistentFn; MemoryContext oldcxt; @@ -735,9 +739,10 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm) /* * Now scan the revmap. We start by querying for heap page 0, * incrementing by the number of pages per range; this gives us a full - * view of the table. + * view of the table. We make use of uint64 for heapBlk as a BlockNumber + * could wrap for tables with close to 2^32 pages. */ - for (heapBlk = 0; heapBlk < nblocks; heapBlk += opaque->bo_pagesPerRange) + for (uint64 heapBlk = 0; heapBlk < nblocks; heapBlk += opaque->bo_pagesPerRange) { bool addrange; bool gottuple = false; @@ -749,7 +754,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm) MemoryContextReset(perRangeCxt); - tup = brinGetTupleForHeapBlock(opaque->bo_rmAccess, heapBlk, &buf, + tup = brinGetTupleForHeapBlock(opaque->bo_rmAccess, (BlockNumber) heapBlk, &buf, &off, &size, BUFFER_LOCK_SHARE); if (tup) { @@ -924,7 +929,7 @@ bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm) /* add the pages in the range to the output bitmap, if needed */ if (addrange) { - BlockNumber pageno; + uint64 pageno; for (pageno = heapBlk; pageno <= Min(nblocks, heapBlk + opaque->bo_pagesPerRange) - 1; @@ -1185,7 +1190,7 @@ brinbuild(Relation heap, Relation index, IndexInfo *indexInfo) { SortCoordinate coordinate; - coordinate = (SortCoordinate) palloc0(sizeof(SortCoordinateData)); + coordinate = palloc0_object(SortCoordinateData); coordinate->isWorker = false; coordinate->nParticipants = state->bs_leader->nparticipanttuplesorts; @@ -1478,8 +1483,8 @@ brin_summarize_range(PG_FUNCTION_ARGS) /* Restore userid and security context */ SetUserIdAndSecContext(save_userid, save_sec_context); - relation_close(indexRel, ShareUpdateExclusiveLock); - relation_close(heapRel, ShareUpdateExclusiveLock); + index_close(indexRel, ShareUpdateExclusiveLock); + table_close(heapRel, ShareUpdateExclusiveLock); PG_RETURN_INT32((int32) numSummarized); } @@ -1568,8 +1573,8 @@ brin_desummarize_range(PG_FUNCTION_ARGS) errmsg("index \"%s\" is not valid", RelationGetRelationName(indexRel)))); - relation_close(indexRel, ShareUpdateExclusiveLock); - relation_close(heapRel, ShareUpdateExclusiveLock); + index_close(indexRel, ShareUpdateExclusiveLock); + table_close(heapRel, ShareUpdateExclusiveLock); PG_RETURN_VOID(); } @@ -1608,7 +1613,7 @@ brin_build_desc(Relation rel) opcInfoFn = index_getprocinfo(rel, keyno + 1, BRIN_PROCNUM_OPCINFO); opcinfo[keyno] = (BrinOpcInfo *) - DatumGetPointer(FunctionCall1(opcInfoFn, attr->atttypid)); + DatumGetPointer(FunctionCall1(opcInfoFn, ObjectIdGetDatum(attr->atttypid))); totalstored += opcinfo[keyno]->oi_nstored; } @@ -1686,9 +1691,6 @@ initialize_brin_buildstate(Relation idxRel, BrinRevmap *revmap, state->bs_leader = NULL; state->bs_worker_id = 0; state->bs_sortstate = NULL; - state->bs_context = CurrentMemoryContext; - state->bs_emptyTuple = NULL; - state->bs_emptyTupleLen = 0; /* Remember the memory context to use for an empty tuple, if needed. */ state->bs_context = CurrentMemoryContext; @@ -2171,28 +2173,42 @@ union_tuples(BrinDesc *bdesc, BrinMemTuple *a, BrinTuple *b) static void brin_vacuum_scan(Relation idxrel, BufferAccessStrategy strategy) { - BlockNumber nblocks; - BlockNumber blkno; + BlockRangeReadStreamPrivate p; + ReadStream *stream; + Buffer buf; + + p.current_blocknum = 0; + p.last_exclusive = RelationGetNumberOfBlocks(idxrel); + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE | + READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + strategy, + idxrel, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); /* * Scan the index in physical order, and clean up any possible mess in * each page. */ - nblocks = RelationGetNumberOfBlocks(idxrel); - for (blkno = 0; blkno < nblocks; blkno++) + while ((buf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer) { - Buffer buf; - CHECK_FOR_INTERRUPTS(); - buf = ReadBufferExtended(idxrel, MAIN_FORKNUM, blkno, - RBM_NORMAL, strategy); - brin_page_cleanup(idxrel, buf); ReleaseBuffer(buf); } + read_stream_end(stream); + /* * Update all upper pages in the index's FSM, as well. This ensures not * only that we propagate leaf-page FSM updates made by brin_page_cleanup, @@ -2262,7 +2278,7 @@ add_values_to_range(Relation idxRel, BrinDesc *bdesc, BrinMemTuple *dtup, PointerGetDatum(bdesc), PointerGetDatum(bval), values[keyno], - nulls[keyno]); + BoolGetDatum(nulls[keyno])); /* if that returned true, we need to insert the updated tuple */ modified |= DatumGetBool(result); @@ -2370,7 +2386,7 @@ _brin_begin_parallel(BrinBuildState *buildstate, Relation heap, Relation index, Size estsort; BrinShared *brinshared; Sharedsort *sharedsort; - BrinLeader *brinleader = (BrinLeader *) palloc0(sizeof(BrinLeader)); + BrinLeader *brinleader = palloc0_object(BrinLeader); WalUsage *walusage; BufferUsage *bufferusage; bool leaderparticipates = true; @@ -2814,7 +2830,7 @@ _brin_parallel_scan_and_build(BrinBuildState *state, IndexInfo *indexInfo; /* Initialize local tuplesort coordination state */ - coordinate = palloc0(sizeof(SortCoordinateData)); + coordinate = palloc0_object(SortCoordinateData); coordinate->isWorker = true; coordinate->nParticipants = -1; coordinate->sharedsort = sharedsort; @@ -2828,7 +2844,8 @@ _brin_parallel_scan_and_build(BrinBuildState *state, indexInfo->ii_Concurrent = brinshared->isconcurrent; scan = table_beginscan_parallel(heap, - ParallelTableScanFromBrinShared(brinshared)); + ParallelTableScanFromBrinShared(brinshared), + SO_NONE); reltuples = table_index_build_scan(heap, index, indexInfo, true, true, brinbuildCallbackParallel, state, scan); diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c index 82b425ce37daa..5558a2edea5d4 100644 --- a/src/backend/access/brin/brin_bloom.c +++ b/src/backend/access/brin/brin_bloom.c @@ -2,7 +2,7 @@ * brin_bloom.c * Implementation of Bloom opclass for BRIN * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -114,6 +114,7 @@ */ #include "postgres.h" +#include #include #include "access/brin.h" @@ -126,6 +127,7 @@ #include "catalog/pg_am.h" #include "catalog/pg_type.h" #include "common/hashfn.h" +#include "port/pg_bitutils.h" #include "utils/fmgrprotos.h" #include "utils/rel.h" @@ -540,7 +542,7 @@ brin_bloom_add_value(PG_FUNCTION_ARGS) BrinDesc *bdesc = (BrinDesc *) PG_GETARG_POINTER(0); BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1); Datum newval = PG_GETARG_DATUM(2); - bool isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3); + bool isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3); BloomOptions *opts = (BloomOptions *) PG_GET_OPCLASS_OPTIONS(); Oid colloid = PG_GET_COLLATION(); FmgrInfo *hashFn; diff --git a/src/backend/access/brin/brin_inclusion.c b/src/backend/access/brin/brin_inclusion.c index b86ca5744a348..5a2058d9aad7a 100644 --- a/src/backend/access/brin/brin_inclusion.c +++ b/src/backend/access/brin/brin_inclusion.c @@ -16,7 +16,7 @@ * writing is the INET type, where IPv6 values cannot be merged with IPv4 * values. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -641,7 +641,7 @@ inclusion_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily), ObjectIdGetDatum(attr->atttypid), ObjectIdGetDatum(subtype), - Int16GetDatum(strategynum)); + UInt16GetDatum(strategynum)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", diff --git a/src/backend/access/brin/brin_minmax.c b/src/backend/access/brin/brin_minmax.c index d21ab3a668cce..732010293718a 100644 --- a/src/backend/access/brin/brin_minmax.c +++ b/src/backend/access/brin/brin_minmax.c @@ -2,7 +2,7 @@ * brin_minmax.c * Implementation of Min/Max opclass for BRIN * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -66,7 +66,7 @@ brin_minmax_add_value(PG_FUNCTION_ARGS) BrinDesc *bdesc = (BrinDesc *) PG_GETARG_POINTER(0); BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1); Datum newval = PG_GETARG_DATUM(2); - bool isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3); + bool isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3); Oid colloid = PG_GET_COLLATION(); FmgrInfo *cmpFn; Datum compar; @@ -225,8 +225,8 @@ brin_minmax_union(PG_FUNCTION_ARGS) /* Adjust minimum, if B's min is less than A's min */ finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTLessStrategyNumber); - needsadj = FunctionCall2Coll(finfo, colloid, col_b->bv_values[0], - col_a->bv_values[0]); + needsadj = DatumGetBool(FunctionCall2Coll(finfo, colloid, col_b->bv_values[0], + col_a->bv_values[0])); if (needsadj) { if (!attr->attbyval) @@ -238,8 +238,8 @@ brin_minmax_union(PG_FUNCTION_ARGS) /* Adjust maximum, if B's max is greater than A's max */ finfo = minmax_get_strategy_procinfo(bdesc, attno, attr->atttypid, BTGreaterStrategyNumber); - needsadj = FunctionCall2Coll(finfo, colloid, col_b->bv_values[1], - col_a->bv_values[1]); + needsadj = DatumGetBool(FunctionCall2Coll(finfo, colloid, col_b->bv_values[1], + col_a->bv_values[1])); if (needsadj) { if (!attr->attbyval) @@ -294,7 +294,7 @@ minmax_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily), ObjectIdGetDatum(attr->atttypid), ObjectIdGetDatum(subtype), - Int16GetDatum(strategynum)); + UInt16GetDatum(strategynum)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index 0d1507a2a3624..207ae336091b3 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -2,7 +2,7 @@ * brin_minmax_multi.c * Implementation of Multi Min/Max opclass for BRIN * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -131,8 +131,6 @@ typedef struct MinMaxMultiOptions ((MinMaxMultiOptions *) (opts))->valuesPerRange : \ MINMAX_MULTI_DEFAULT_VALUES_PER_PAGE) -#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0)) - /* * The summary of minmax-multi indexes has two representations - Ranges for * convenient processing, and SerializedRanges for storage in bytea value. @@ -276,7 +274,7 @@ static int compare_values(const void *a, const void *b, void *arg); * function (which should be BTLessStrategyNumber). */ static void -AssertArrayOrder(FmgrInfo *cmp, Oid colloid, Datum *values, int nvalues) +AssertArrayOrder(FmgrInfo *cmp, Oid colloid, const Datum *values, int nvalues) { int i; Datum lt; @@ -624,7 +622,7 @@ brin_range_serialize(Ranges *range) for (i = 0; i < nvalues; i++) { - len += VARSIZE_ANY(range->values[i]); + len += VARSIZE_ANY(DatumGetPointer(range->values[i])); } } else if (typlen == -2) /* cstring */ @@ -712,7 +710,7 @@ brin_range_serialize(Ranges *range) /* * brin_range_deserialize - * Serialize the in-memory representation into a compact varlena value. + * Deserialize a compact varlena value into the in-memory representation. * * Simply copy the header and then also the individual values, as stored * in the in-memory value array. @@ -857,8 +855,8 @@ brin_range_deserialize(int maxvalues, SerializedRanges *serialized) static int compare_expanded_ranges(const void *a, const void *b, void *arg) { - ExpandedRange *ra = (ExpandedRange *) a; - ExpandedRange *rb = (ExpandedRange *) b; + const ExpandedRange *ra = a; + const ExpandedRange *rb = b; Datum r; compare_context *cxt = (compare_context *) arg; @@ -895,8 +893,8 @@ compare_expanded_ranges(const void *a, const void *b, void *arg) static int compare_values(const void *a, const void *b, void *arg) { - Datum *da = (Datum *) a; - Datum *db = (Datum *) b; + const Datum *da = a; + const Datum *db = b; Datum r; compare_context *cxt = (compare_context *) arg; @@ -1304,8 +1302,8 @@ merge_overlapping_ranges(FmgrInfo *cmp, Oid colloid, static int compare_distances(const void *a, const void *b) { - DistanceValue *da = (DistanceValue *) a; - DistanceValue *db = (DistanceValue *) b; + const DistanceValue *da = a; + const DistanceValue *db = b; if (da->value < db->value) return 1; @@ -1340,7 +1338,7 @@ build_distances(FmgrInfo *distanceFn, Oid colloid, return NULL; ndistances = (neranges - 1); - distances = (DistanceValue *) palloc0(sizeof(DistanceValue) * ndistances); + distances = palloc0_array(DistanceValue, ndistances); /* * Walk through the ranges once and compute the distance between the @@ -1504,7 +1502,7 @@ reduce_expanded_ranges(ExpandedRange *eranges, int neranges, /* allocate space for the boundary values */ nvalues = 0; - values = (Datum *) palloc(sizeof(Datum) * max_values); + values = palloc_array(Datum, max_values); /* add the global min/max values, from the first/last range */ values[nvalues++] = eranges[0].minval; @@ -1992,8 +1990,8 @@ brin_minmax_multi_distance_tid(PG_FUNCTION_ARGS) double da1, da2; - ItemPointer pa1 = (ItemPointer) PG_GETARG_DATUM(0); - ItemPointer pa2 = (ItemPointer) PG_GETARG_DATUM(1); + ItemPointer pa1 = (ItemPointer) PG_GETARG_POINTER(0); + ItemPointer pa2 = (ItemPointer) PG_GETARG_POINTER(1); /* * We know the values are range boundaries, but the range may be collapsed @@ -2032,7 +2030,7 @@ brin_minmax_multi_distance_numeric(PG_FUNCTION_ARGS) d = DirectFunctionCall2(numeric_sub, a2, a1); /* a2 - a1 */ - PG_RETURN_FLOAT8(DirectFunctionCall1(numeric_float8, d)); + PG_RETURN_DATUM(DirectFunctionCall1(numeric_float8, d)); } /* @@ -2414,7 +2412,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) BrinDesc *bdesc = (BrinDesc *) PG_GETARG_POINTER(0); BrinValues *column = (BrinValues *) PG_GETARG_POINTER(1); Datum newval = PG_GETARG_DATUM(2); - bool isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_DATUM(3); + bool isnull PG_USED_FOR_ASSERTS_ONLY = PG_GETARG_BOOL(3); MinMaxMultiOptions *opts = (MinMaxMultiOptions *) PG_GET_OPCLASS_OPTIONS(); Oid colloid = PG_GET_COLLATION(); bool modified = false; @@ -2932,7 +2930,7 @@ minmax_multi_get_strategy_procinfo(BrinDesc *bdesc, uint16 attno, Oid subtype, tuple = SearchSysCache4(AMOPSTRATEGY, ObjectIdGetDatum(opfamily), ObjectIdGetDatum(attr->atttypid), ObjectIdGetDatum(subtype), - Int16GetDatum(strategynum)); + UInt16GetDatum(strategynum)); if (!HeapTupleIsValid(tuple)) elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", strategynum, attr->atttypid, subtype, opfamily); diff --git a/src/backend/access/brin/brin_pageops.c b/src/backend/access/brin/brin_pageops.c index 6d8dd1512d6a7..7da97bec43b55 100644 --- a/src/backend/access/brin/brin_pageops.c +++ b/src/backend/access/brin/brin_pageops.c @@ -2,7 +2,7 @@ * brin_pageops.c * Page-handling routines for BRIN indexes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -176,7 +176,7 @@ brin_doupdate(Relation idxrel, BlockNumber pagesPerRange, brin_can_do_samepage_update(oldbuf, origsz, newsz)) { START_CRIT_SECTION(); - if (!PageIndexTupleOverwrite(oldpage, oldoff, (Item) unconstify(BrinTuple *, newtup), newsz)) + if (!PageIndexTupleOverwrite(oldpage, oldoff, newtup, newsz)) elog(ERROR, "failed to replace BRIN tuple"); MarkBufferDirty(oldbuf); @@ -250,8 +250,7 @@ brin_doupdate(Relation idxrel, BlockNumber pagesPerRange, brin_page_init(newpage, BRIN_PAGETYPE_REGULAR); PageIndexTupleDeleteNoCompact(oldpage, oldoff); - newoff = PageAddItem(newpage, (Item) unconstify(BrinTuple *, newtup), newsz, - InvalidOffsetNumber, false, false); + newoff = PageAddItem(newpage, newtup, newsz, InvalidOffsetNumber, false, false); if (newoff == InvalidOffsetNumber) elog(ERROR, "failed to add BRIN tuple to new page"); MarkBufferDirty(oldbuf); @@ -341,7 +340,7 @@ brin_can_do_samepage_update(Buffer buffer, Size origsz, Size newsz) OffsetNumber brin_doinsert(Relation idxrel, BlockNumber pagesPerRange, BrinRevmap *revmap, Buffer *buffer, BlockNumber heapBlk, - BrinTuple *tup, Size itemsz) + const BrinTuple *tup, Size itemsz) { Page page; BlockNumber blk; @@ -408,8 +407,7 @@ brin_doinsert(Relation idxrel, BlockNumber pagesPerRange, START_CRIT_SECTION(); if (extended) brin_page_init(page, BRIN_PAGETYPE_REGULAR); - off = PageAddItem(page, (Item) tup, itemsz, InvalidOffsetNumber, - false, false); + off = PageAddItem(page, tup, itemsz, InvalidOffsetNumber, false, false); if (off == InvalidOffsetNumber) elog(ERROR, "failed to add BRIN tuple to new page"); MarkBufferDirty(*buffer); @@ -893,7 +891,11 @@ brin_initialize_empty_new_buffer(Relation idxrel, Buffer buffer) page = BufferGetPage(buffer); brin_page_init(page, BRIN_PAGETYPE_REGULAR); MarkBufferDirty(buffer); - log_newpage_buffer(buffer, true); + + /* XLOG stuff */ + if (RelationNeedsWAL(idxrel)) + log_newpage_buffer(buffer, true); + END_CRIT_SECTION(); /* diff --git a/src/backend/access/brin/brin_revmap.c b/src/backend/access/brin/brin_revmap.c index 4e380ecc71097..233355cb2d5d1 100644 --- a/src/backend/access/brin/brin_revmap.c +++ b/src/backend/access/brin/brin_revmap.c @@ -12,7 +12,7 @@ * the metapage. When the revmap needs to be expanded, all tuples on the * regular BRIN page at that block (if any) are moved out of the way. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -79,7 +79,7 @@ brinRevmapInitialize(Relation idxrel, BlockNumber *pagesPerRange) page = BufferGetPage(meta); metadata = (BrinMetaPageData *) PageGetContents(page); - revmap = palloc(sizeof(BrinRevmap)); + revmap = palloc_object(BrinRevmap); revmap->rm_irel = idxrel; revmap->rm_pagesPerRange = metadata->pagesPerRange; revmap->rm_lastRevmapPage = metadata->lastRevmapPage; diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c index 861f397e6db58..af39d4489622c 100644 --- a/src/backend/access/brin/brin_tuple.c +++ b/src/backend/access/brin/brin_tuple.c @@ -23,7 +23,7 @@ * Note the size of the null bitmask may not be the same as that of the * datum array. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -50,7 +50,7 @@ static inline void brin_deconstruct_tuple(BrinDesc *brdesc, - char *tp, bits8 *nullbits, bool nulls, + char *tp, uint8 *nullbits, bool nulls, Datum *values, bool *allnulls, bool *hasnulls); @@ -84,6 +84,7 @@ brtuple_disk_tupdesc(BrinDesc *brdesc) MemoryContextSwitchTo(oldcxt); + TupleDescFinalize(tupdesc); brdesc->bd_disktdesc = tupdesc; } @@ -106,7 +107,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, int keyno; int idxattno; uint16 phony_infomask = 0; - bits8 *phony_nullbitmap; + uint8 *phony_nullbitmap; Size len, hoff, data_len; @@ -119,13 +120,12 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, Assert(brdesc->bd_totalstored > 0); - values = (Datum *) palloc(sizeof(Datum) * brdesc->bd_totalstored); - nulls = (bool *) palloc0(sizeof(bool) * brdesc->bd_totalstored); - phony_nullbitmap = (bits8 *) - palloc(sizeof(bits8) * BITMAPLEN(brdesc->bd_totalstored)); + values = palloc_array(Datum, brdesc->bd_totalstored); + nulls = palloc0_array(bool, brdesc->bd_totalstored); + phony_nullbitmap = palloc_array(uint8, BITMAPLEN(brdesc->bd_totalstored)); #ifdef TOAST_INDEX_HACK - untoasted_values = (Datum *) palloc(sizeof(Datum) * brdesc->bd_totalstored); + untoasted_values = palloc_array(Datum, brdesc->bd_totalstored); #endif /* @@ -207,7 +207,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, */ if (VARATT_IS_EXTERNAL(DatumGetPointer(value))) { - value = PointerGetDatum(detoast_external_attr((struct varlena *) + value = PointerGetDatum(detoast_external_attr((varlena *) DatumGetPointer(value))); free_value = true; } @@ -322,7 +322,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, */ if (anynulls) { - bits8 *bitP; + uint8 *bitP; int bitmask; rettuple->bt_info |= BRIN_NULLS_MASK; @@ -332,7 +332,7 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple, * store a 1 for a null attribute rather than a 0. So we must reverse * the sense of the att_isnull test in brin_deconstruct_tuple as well. */ - bitP = ((bits8 *) ((char *) rettuple + SizeOfBrinTuple)) - 1; + bitP = ((uint8 *) ((char *) rettuple + SizeOfBrinTuple)) - 1; bitmask = HIGHBIT; for (keyno = 0; keyno < brdesc->bd_tupdesc->natts; keyno++) { @@ -391,7 +391,7 @@ brin_form_placeholder_tuple(BrinDesc *brdesc, BlockNumber blkno, Size *size) Size hoff; BrinTuple *rettuple; int keyno; - bits8 *bitP; + uint8 *bitP; int bitmask; /* compute total space needed: always add nulls */ @@ -404,7 +404,7 @@ brin_form_placeholder_tuple(BrinDesc *brdesc, BlockNumber blkno, Size *size) rettuple->bt_info = hoff; rettuple->bt_info |= BRIN_NULLS_MASK | BRIN_PLACEHOLDER_MASK | BRIN_EMPTY_RANGE_MASK; - bitP = ((bits8 *) ((char *) rettuple + SizeOfBrinTuple)) - 1; + bitP = ((uint8 *) ((char *) rettuple + SizeOfBrinTuple)) - 1; bitmask = HIGHBIT; /* set allnulls true for all attributes */ for (keyno = 0; keyno < brdesc->bd_tupdesc->natts; keyno++) @@ -488,9 +488,9 @@ brin_new_memtuple(BrinDesc *brdesc) sizeof(BrinValues) * brdesc->bd_tupdesc->natts); dtup = palloc0(basesize + sizeof(Datum) * brdesc->bd_totalstored); - dtup->bt_values = palloc(sizeof(Datum) * brdesc->bd_totalstored); - dtup->bt_allnulls = palloc(sizeof(bool) * brdesc->bd_tupdesc->natts); - dtup->bt_hasnulls = palloc(sizeof(bool) * brdesc->bd_tupdesc->natts); + dtup->bt_values = palloc_array(Datum, brdesc->bd_totalstored); + dtup->bt_allnulls = palloc_array(bool, brdesc->bd_tupdesc->natts); + dtup->bt_hasnulls = palloc_array(bool, brdesc->bd_tupdesc->natts); dtup->bt_empty_range = true; @@ -557,7 +557,7 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple) bool *allnulls; bool *hasnulls; char *tp; - bits8 *nullbits; + uint8 *nullbits; int keyno; int valueno; MemoryContext oldcxt; @@ -581,7 +581,7 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple) tp = (char *) tuple + BrinTupleDataOffset(tuple); if (BrinTupleHasNulls(tuple)) - nullbits = (bits8 *) ((char *) tuple + SizeOfBrinTuple); + nullbits = (uint8 *) ((char *) tuple + SizeOfBrinTuple); else nullbits = NULL; brin_deconstruct_tuple(brdesc, @@ -643,7 +643,7 @@ brin_deform_tuple(BrinDesc *brdesc, BrinTuple *tuple, BrinMemTuple *dMemtuple) */ static inline void brin_deconstruct_tuple(BrinDesc *brdesc, - char *tp, bits8 *nullbits, bool nulls, + char *tp, uint8 *nullbits, bool nulls, Datum *values, bool *allnulls, bool *hasnulls) { int attnum; diff --git a/src/backend/access/brin/brin_validate.c b/src/backend/access/brin/brin_validate.c index 915b8628b460e..3f5be426b7801 100644 --- a/src/backend/access/brin/brin_validate.c +++ b/src/backend/access/brin/brin_validate.c @@ -3,7 +3,7 @@ * brin_validate.c * Opclass validator for BRIN. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/brin/brin_xlog.c b/src/backend/access/brin/brin_xlog.c index 85db2f0fd5ace..a07f1d11ddeeb 100644 --- a/src/backend/access/brin/brin_xlog.c +++ b/src/backend/access/brin/brin_xlog.c @@ -2,7 +2,7 @@ * brin_xlog.c * XLog replay routines for BRIN indexes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -31,7 +31,7 @@ brin_xlog_createidx(XLogReaderState *record) /* create the index' metapage */ buf = XLogInitBufferForRedo(record, 0); Assert(BufferIsValid(buf)); - page = (Page) BufferGetPage(buf); + page = BufferGetPage(buf); brin_metapage_init(page, xlrec->pagesPerRange, xlrec->version); PageSetLSN(page, lsn); MarkBufferDirty(buf); @@ -82,12 +82,12 @@ brin_xlog_insert_update(XLogReaderState *record, Assert(tuple->bt_blkno == xlrec->heapBlk); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); offnum = xlrec->offnum; if (PageGetMaxOffsetNumber(page) + 1 < offnum) elog(PANIC, "brin_xlog_insert_update: invalid max offset number"); - offnum = PageAddItem(page, (Item) tuple, tuplen, offnum, true, false); + offnum = PageAddItem(page, tuple, tuplen, offnum, true, false); if (offnum == InvalidOffsetNumber) elog(PANIC, "brin_xlog_insert_update: failed to add tuple"); @@ -104,7 +104,7 @@ brin_xlog_insert_update(XLogReaderState *record, ItemPointerData tid; ItemPointerSet(&tid, regpgno, xlrec->offnum); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); brinSetHeapBlockItemptr(buffer, xlrec->pagesPerRange, xlrec->heapBlk, tid); @@ -146,7 +146,7 @@ brin_xlog_update(XLogReaderState *record) Page page; OffsetNumber offnum; - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); offnum = xlrec->oldOffnum; @@ -185,11 +185,11 @@ brin_xlog_samepage_update(XLogReaderState *record) brintuple = (BrinTuple *) XLogRecGetBlockData(record, 0, &tuplen); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); offnum = xlrec->offnum; - if (!PageIndexTupleOverwrite(page, offnum, (Item) brintuple, tuplen)) + if (!PageIndexTupleOverwrite(page, offnum, brintuple, tuplen)) elog(PANIC, "brin_xlog_samepage_update: failed to replace tuple"); PageSetLSN(page, lsn); @@ -254,7 +254,7 @@ brin_xlog_revmap_extend(XLogReaderState *record) */ buf = XLogInitBufferForRedo(record, 1); - page = (Page) BufferGetPage(buf); + page = BufferGetPage(buf); brin_page_init(page, BRIN_PAGETYPE_REVMAP); PageSetLSN(page, lsn); diff --git a/src/backend/access/brin/meson.build b/src/backend/access/brin/meson.build index 4aa1439721d11..d533cfc14c250 100644 --- a/src/backend/access/brin/meson.build +++ b/src/backend/access/brin/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'brin.c', diff --git a/src/backend/access/common/attmap.c b/src/backend/access/common/attmap.c index 4901ebecef73f..ccd03222818c3 100644 --- a/src/backend/access/common/attmap.c +++ b/src/backend/access/common/attmap.c @@ -10,7 +10,7 @@ * columns in a different order, taking into account dropped columns. * They are also used by the tuple conversion routines in tupconvert.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -41,9 +41,9 @@ make_attrmap(int maplen) { AttrMap *res; - res = (AttrMap *) palloc0(sizeof(AttrMap)); + res = palloc0_object(AttrMap); res->maplen = maplen; - res->attnums = (AttrNumber *) palloc0(sizeof(AttrNumber) * maplen); + res->attnums = palloc0_array(AttrNumber, maplen); return res; } diff --git a/src/backend/access/common/bufmask.c b/src/backend/access/common/bufmask.c index bb260cffa6828..5f63d04c9cbf6 100644 --- a/src/backend/access/common/bufmask.c +++ b/src/backend/access/common/bufmask.c @@ -5,7 +5,7 @@ * in a page which can be different when the WAL is generated * and when the WAL is applied. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * Contains common routines required for masking a page. * @@ -32,7 +32,7 @@ mask_page_lsn_and_checksum(Page page) { PageHeader phdr = (PageHeader) page; - PageXLogRecPtrSet(phdr->pd_lsn, (uint64) MASK_MARKER); + PageXLogRecPtrSet(&phdr->pd_lsn, (uint64) MASK_MARKER); phdr->pd_checksum = MASK_MARKER; } @@ -55,9 +55,8 @@ mask_page_hint_bits(Page page) PageClearHasFreeLinePointers(page); /* - * During replay, if the page LSN has advanced past our XLOG record's LSN, - * we don't mark the page all-visible. See heap_xlog_visible() for - * details. + * PD_ALL_VISIBLE is masked during WAL consistency checking. XXX: It is + * worth investigating if we could stop doing this. */ PageClearAllVisible(page); } diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c index 6265178774221..a6c1f3a734b2a 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -3,7 +3,7 @@ * detoast.c * Retrieve compressed or external variable size attributes. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/access/common/detoast.c @@ -22,12 +22,12 @@ #include "utils/expandeddatum.h" #include "utils/rel.h" -static struct varlena *toast_fetch_datum(struct varlena *attr); -static struct varlena *toast_fetch_datum_slice(struct varlena *attr, - int32 sliceoffset, - int32 slicelength); -static struct varlena *toast_decompress_datum(struct varlena *attr); -static struct varlena *toast_decompress_datum_slice(struct varlena *attr, int32 slicelength); +static varlena *toast_fetch_datum(varlena *attr); +static varlena *toast_fetch_datum_slice(varlena *attr, + int32 sliceoffset, + int32 slicelength); +static varlena *toast_decompress_datum(varlena *attr); +static varlena *toast_decompress_datum_slice(varlena *attr, int32 slicelength); /* ---------- * detoast_external_attr - @@ -41,10 +41,10 @@ static struct varlena *toast_decompress_datum_slice(struct varlena *attr, int32 * EXTERNAL datum, the result will be a pfree'able chunk. * ---------- */ -struct varlena * -detoast_external_attr(struct varlena *attr) +varlena * +detoast_external_attr(varlena *attr) { - struct varlena *result; + varlena *result; if (VARATT_IS_EXTERNAL_ONDISK(attr)) { @@ -58,10 +58,10 @@ detoast_external_attr(struct varlena *attr) /* * This is an indirect pointer --- dereference it */ - struct varatt_indirect redirect; + varatt_indirect redirect; VARATT_EXTERNAL_GET_POINTER(redirect, attr); - attr = (struct varlena *) redirect.pointer; + attr = (varlena *) redirect.pointer; /* nested indirect Datums aren't allowed */ Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr)); @@ -74,7 +74,7 @@ detoast_external_attr(struct varlena *attr) * Copy into the caller's memory context, in case caller tries to * pfree the result. */ - result = (struct varlena *) palloc(VARSIZE_ANY(attr)); + result = (varlena *) palloc(VARSIZE_ANY(attr)); memcpy(result, attr, VARSIZE_ANY(attr)); } else if (VARATT_IS_EXTERNAL_EXPANDED(attr)) @@ -87,7 +87,7 @@ detoast_external_attr(struct varlena *attr) eoh = DatumGetEOHP(PointerGetDatum(attr)); resultsize = EOH_get_flat_size(eoh); - result = (struct varlena *) palloc(resultsize); + result = (varlena *) palloc(resultsize); EOH_flatten_into(eoh, result, resultsize); } else @@ -112,8 +112,8 @@ detoast_external_attr(struct varlena *attr) * datum, the result will be a pfree'able chunk. * ---------- */ -struct varlena * -detoast_attr(struct varlena *attr) +varlena * +detoast_attr(varlena *attr) { if (VARATT_IS_EXTERNAL_ONDISK(attr)) { @@ -124,7 +124,7 @@ detoast_attr(struct varlena *attr) /* If it's compressed, decompress it */ if (VARATT_IS_COMPRESSED(attr)) { - struct varlena *tmp = attr; + varlena *tmp = attr; attr = toast_decompress_datum(tmp); pfree(tmp); @@ -135,10 +135,10 @@ detoast_attr(struct varlena *attr) /* * This is an indirect pointer --- dereference it */ - struct varatt_indirect redirect; + varatt_indirect redirect; VARATT_EXTERNAL_GET_POINTER(redirect, attr); - attr = (struct varlena *) redirect.pointer; + attr = (varlena *) redirect.pointer; /* nested indirect Datums aren't allowed */ Assert(!VARATT_IS_EXTERNAL_INDIRECT(attr)); @@ -147,11 +147,11 @@ detoast_attr(struct varlena *attr) attr = detoast_attr(attr); /* if it isn't, we'd better copy it */ - if (attr == (struct varlena *) redirect.pointer) + if (attr == (varlena *) redirect.pointer) { - struct varlena *result; + varlena *result; - result = (struct varlena *) palloc(VARSIZE_ANY(attr)); + result = (varlena *) palloc(VARSIZE_ANY(attr)); memcpy(result, attr, VARSIZE_ANY(attr)); attr = result; } @@ -179,9 +179,9 @@ detoast_attr(struct varlena *attr) */ Size data_size = VARSIZE_SHORT(attr) - VARHDRSZ_SHORT; Size new_size = data_size + VARHDRSZ; - struct varlena *new_attr; + varlena *new_attr; - new_attr = (struct varlena *) palloc(new_size); + new_attr = (varlena *) palloc(new_size); SET_VARSIZE(new_attr, new_size); memcpy(VARDATA(new_attr), VARDATA_SHORT(attr), data_size); attr = new_attr; @@ -201,12 +201,12 @@ detoast_attr(struct varlena *attr) * If slicelength < 0, return everything beyond sliceoffset * ---------- */ -struct varlena * -detoast_attr_slice(struct varlena *attr, +varlena * +detoast_attr_slice(varlena *attr, int32 sliceoffset, int32 slicelength) { - struct varlena *preslice; - struct varlena *result; + varlena *preslice; + varlena *result; char *attrdata; int32 slicelimit; int32 attrsize; @@ -225,7 +225,7 @@ detoast_attr_slice(struct varlena *attr, if (VARATT_IS_EXTERNAL_ONDISK(attr)) { - struct varatt_external toast_pointer; + varatt_external toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); @@ -266,7 +266,7 @@ detoast_attr_slice(struct varlena *attr, } else if (VARATT_IS_EXTERNAL_INDIRECT(attr)) { - struct varatt_indirect redirect; + varatt_indirect redirect; VARATT_EXTERNAL_GET_POINTER(redirect, attr); @@ -288,7 +288,7 @@ detoast_attr_slice(struct varlena *attr, if (VARATT_IS_COMPRESSED(preslice)) { - struct varlena *tmp = preslice; + varlena *tmp = preslice; /* Decompress enough to encompass the slice and the offset */ if (slicelimit >= 0) @@ -321,7 +321,7 @@ detoast_attr_slice(struct varlena *attr, else if (slicelength < 0 || slicelimit > attrsize) slicelength = attrsize - sliceoffset; - result = (struct varlena *) palloc(slicelength + VARHDRSZ); + result = (varlena *) palloc(slicelength + VARHDRSZ); SET_VARSIZE(result, slicelength + VARHDRSZ); memcpy(VARDATA(result), attrdata + sliceoffset, slicelength); @@ -339,12 +339,12 @@ detoast_attr_slice(struct varlena *attr, * in the toast relation * ---------- */ -static struct varlena * -toast_fetch_datum(struct varlena *attr) +static varlena * +toast_fetch_datum(varlena *attr) { Relation toastrel; - struct varlena *result; - struct varatt_external toast_pointer; + varlena *result; + varatt_external toast_pointer; int32 attrsize; if (!VARATT_IS_EXTERNAL_ONDISK(attr)) @@ -355,7 +355,7 @@ toast_fetch_datum(struct varlena *attr) attrsize = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer); - result = (struct varlena *) palloc(attrsize + VARHDRSZ); + result = (varlena *) palloc(attrsize + VARHDRSZ); if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) SET_VARSIZE_COMPRESSED(result, attrsize + VARHDRSZ); @@ -392,13 +392,13 @@ toast_fetch_datum(struct varlena *attr) * has to be a prefix, i.e. sliceoffset has to be 0). * ---------- */ -static struct varlena * -toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, +static varlena * +toast_fetch_datum_slice(varlena *attr, int32 sliceoffset, int32 slicelength) { Relation toastrel; - struct varlena *result; - struct varatt_external toast_pointer; + varlena *result; + varatt_external toast_pointer; int32 attrsize; if (!VARATT_IS_EXTERNAL_ONDISK(attr)) @@ -438,7 +438,7 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, if (((sliceoffset + slicelength) > attrsize) || slicelength < 0) slicelength = attrsize - sliceoffset; - result = (struct varlena *) palloc(slicelength + VARHDRSZ); + result = (varlena *) palloc(slicelength + VARHDRSZ); if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) SET_VARSIZE_COMPRESSED(result, slicelength + VARHDRSZ); @@ -467,8 +467,8 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, * * Decompress a compressed version of a varlena datum */ -static struct varlena * -toast_decompress_datum(struct varlena *attr) +static varlena * +toast_decompress_datum(varlena *attr) { ToastCompressionId cmid; @@ -499,8 +499,8 @@ toast_decompress_datum(struct varlena *attr) * offset handling happens in detoast_attr_slice. * Here we just decompress a slice from the front. */ -static struct varlena * -toast_decompress_datum_slice(struct varlena *attr, int32 slicelength) +static varlena * +toast_decompress_datum_slice(varlena *attr, int32 slicelength) { ToastCompressionId cmid; @@ -544,20 +544,20 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength) Size toast_raw_datum_size(Datum value) { - struct varlena *attr = (struct varlena *) DatumGetPointer(value); + varlena *attr = (varlena *) DatumGetPointer(value); Size result; if (VARATT_IS_EXTERNAL_ONDISK(attr)) { /* va_rawsize is the size of the original datum -- including header */ - struct varatt_external toast_pointer; + varatt_external toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); result = toast_pointer.va_rawsize; } else if (VARATT_IS_EXTERNAL_INDIRECT(attr)) { - struct varatt_indirect toast_pointer; + varatt_indirect toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); @@ -600,7 +600,7 @@ toast_raw_datum_size(Datum value) Size toast_datum_size(Datum value) { - struct varlena *attr = (struct varlena *) DatumGetPointer(value); + varlena *attr = (varlena *) DatumGetPointer(value); Size result; if (VARATT_IS_EXTERNAL_ONDISK(attr)) @@ -610,14 +610,14 @@ toast_datum_size(Datum value) * compressed or not. We do not count the size of the toast pointer * ... should we? */ - struct varatt_external toast_pointer; + varatt_external toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); result = VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer); } else if (VARATT_IS_EXTERNAL_INDIRECT(attr)) { - struct varatt_indirect toast_pointer; + varatt_indirect toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c index 969d1028cae89..f30346469ed03 100644 --- a/src/backend/access/common/heaptuple.c +++ b/src/backend/access/common/heaptuple.c @@ -45,7 +45,7 @@ * and we'd like to still refer to them via C struct offsets. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -103,16 +103,16 @@ static HTAB *missing_cache = NULL; static uint32 missing_hash(const void *key, Size keysize) { - const missing_cache_key *entry = (missing_cache_key *) key; + const missing_cache_key *entry = key; - return hash_bytes((const unsigned char *) entry->value, entry->len); + return hash_bytes((const unsigned char *) DatumGetPointer(entry->value), entry->len); } static int missing_match(const void *key1, const void *key2, Size keysize) { - const missing_cache_key *entry1 = (missing_cache_key *) key1; - const missing_cache_key *entry2 = (missing_cache_key *) key2; + const missing_cache_key *entry1 = key1; + const missing_cache_key *entry2 = key2; if (entry1->len != entry2->len) return entry1->len > entry2->len ? 1 : -1; @@ -123,7 +123,7 @@ missing_match(const void *key1, const void *key2, Size keysize) } static void -init_missing_cache() +init_missing_cache(void) { HASHCTL hash_ctl; @@ -189,7 +189,7 @@ getmissingattr(TupleDesc tupleDesc, if (att->attlen > 0) key.len = att->attlen; else - key.len = VARSIZE_ANY(attrmiss->am_value); + key.len = VARSIZE_ANY(DatumGetPointer(attrmiss->am_value)); key.value = attrmiss->am_value; entry = hash_search(missing_cache, &key, HASH_ENTER, &found); @@ -273,7 +273,7 @@ heap_compute_data_size(TupleDesc tupleDesc, */ static inline void fill_val(CompactAttribute *att, - bits8 **bit, + uint8 **bit, int *bitmask, char **dataP, uint16 *infomask, @@ -401,9 +401,9 @@ void heap_fill_tuple(TupleDesc tupleDesc, const Datum *values, const bool *isnull, char *data, Size data_size, - uint16 *infomask, bits8 *bit) + uint16 *infomask, uint8 *bit) { - bits8 *bitP; + uint8 *bitP; int bitmask; int i; int numberOfAttributes = tupleDesc->natts; @@ -498,19 +498,7 @@ heap_attisnull(HeapTuple tup, int attnum, TupleDesc tupleDesc) * nocachegetattr * * This only gets called from fastgetattr(), in cases where we - * can't use a cacheoffset and the value is not null. - * - * This caches attribute offsets in the attribute descriptor. - * - * An alternative way to speed things up would be to cache offsets - * with the tuple, but that seems more difficult unless you take - * the storage hit of actually putting those offsets into the - * tuple you send to disk. Yuck. - * - * This scheme will be slightly slower than that, but should - * perform well for queries which hit large #'s of tuples. After - * you cache the offsets once, examining all the other tuples using - * the same attribute descriptor will go much quicker. -cim 5/4/91 + * can't use the attcacheoff and the value is not null. * * NOTE: if you need to change this code, see also heap_deform_tuple. * Also see nocache_index_getattr, which is the same code for index @@ -522,194 +510,114 @@ nocachegetattr(HeapTuple tup, int attnum, TupleDesc tupleDesc) { + CompactAttribute *cattr; HeapTupleHeader td = tup->t_data; char *tp; /* ptr to data part of tuple */ - bits8 *bp = td->t_bits; /* ptr to null bitmap in tuple */ - bool slow = false; /* do we have to walk attrs? */ + uint8 *bp = td->t_bits; /* ptr to null bitmap in tuple */ int off; /* current offset within data */ + int startAttr; + int firstNullAttr; + int i; + bool hasnulls = HeapTupleHasNulls(tup); - /* ---------------- - * Three cases: - * - * 1: No nulls and no variable-width attributes. - * 2: Has a null or a var-width AFTER att. - * 3: Has nulls or var-widths BEFORE att. - * ---------------- - */ + /* Did someone forget to call TupleDescFinalize()? */ + Assert(tupleDesc->firstNonCachedOffsetAttr >= 0); attnum--; - if (!HeapTupleNoNulls(tup)) + /* + * To minimize the number of attributes we need to look at, start walking + * the tuple at the attribute with the highest attcacheoff prior to attnum + * or the first NULL attribute prior to attnum, whichever comes first. + */ + if (hasnulls) + firstNullAttr = first_null_attr(bp, attnum); + else + firstNullAttr = attnum; + + if (tupleDesc->firstNonCachedOffsetAttr > 0 && firstNullAttr > 0) { /* - * there's a null somewhere in the tuple - * - * check to see if any preceding bits are null... + * Try to start with the highest attribute with an attcacheoff that's + * prior to the one we're looking for, or with the attribute prior to + * the first NULL attribute, if there is one. */ - int byte = attnum >> 3; - int finalbit = attnum & 0x07; - - /* check for nulls "before" final bit of last byte */ - if ((~bp[byte]) & ((1 << finalbit) - 1)) - slow = true; - else - { - /* check for nulls in any "earlier" bytes */ - int i; - - for (i = 0; i < byte; i++) - { - if (bp[i] != 0xFF) - { - slow = true; - break; - } - } - } + startAttr = Min(tupleDesc->firstNonCachedOffsetAttr - 1, firstNullAttr - 1); + off = TupleDescCompactAttr(tupleDesc, startAttr)->attcacheoff; + } + else + { + /* Otherwise, start at the beginning... */ + startAttr = 0; + off = 0; } tp = (char *) td + td->t_hoff; - if (!slow) + /* + * Calculate 'off' up to the first NULL attr. We use two cheaper loops + * when the tuple has no variable-width columns. When variable-width + * columns exists, we use att_addlength_pointer() to move the offset + * beyond the current attribute. + */ + if (!HeapTupleHasVarWidth(tup)) { - CompactAttribute *att; - - /* - * If we get here, there are no nulls up to and including the target - * attribute. If we have a cached offset, we can use it. - */ - att = TupleDescCompactAttr(tupleDesc, attnum); - if (att->attcacheoff >= 0) - return fetchatt(att, tp + att->attcacheoff); - - /* - * Otherwise, check for non-fixed-length attrs up to and including - * target. If there aren't any, it's safe to cheaply initialize the - * cached offsets for these attrs. - */ - if (HeapTupleHasVarWidth(tup)) + for (i = startAttr; i < firstNullAttr; i++) { - int j; + cattr = TupleDescCompactAttr(tupleDesc, i); - for (j = 0; j <= attnum; j++) - { - if (TupleDescCompactAttr(tupleDesc, j)->attlen <= 0) - { - slow = true; - break; - } - } + off = att_nominal_alignby(off, cattr->attalignby); + off += cattr->attlen; } - } - - if (!slow) - { - int natts = tupleDesc->natts; - int j = 1; - - /* - * If we get here, we have a tuple with no nulls or var-widths up to - * and including the target attribute, so we can use the cached offset - * ... only we don't have it yet, or we'd not have got here. Since - * it's cheap to compute offsets for fixed-width columns, we take the - * opportunity to initialize the cached offsets for *all* the leading - * fixed-width columns, in hope of avoiding future visits to this - * routine. - */ - TupleDescCompactAttr(tupleDesc, 0)->attcacheoff = 0; - - /* we might have set some offsets in the slow path previously */ - while (j < natts && TupleDescCompactAttr(tupleDesc, j)->attcacheoff > 0) - j++; - - off = TupleDescCompactAttr(tupleDesc, j - 1)->attcacheoff + - TupleDescCompactAttr(tupleDesc, j - 1)->attlen; - for (; j < natts; j++) + for (; i < attnum; i++) { - CompactAttribute *att = TupleDescCompactAttr(tupleDesc, j); + if (att_isnull(i, bp)) + continue; - if (att->attlen <= 0) - break; - - off = att_nominal_alignby(off, att->attalignby); + cattr = TupleDescCompactAttr(tupleDesc, i); - att->attcacheoff = off; - - off += att->attlen; + off = att_nominal_alignby(off, cattr->attalignby); + off += cattr->attlen; } - - Assert(j > attnum); - - off = TupleDescCompactAttr(tupleDesc, attnum)->attcacheoff; } else { - bool usecache = true; - int i; - - /* - * Now we know that we have to walk the tuple CAREFULLY. But we still - * might be able to cache some offsets for next time. - * - * Note - This loop is a little tricky. For each non-null attribute, - * we have to first account for alignment padding before the attr, - * then advance over the attr based on its length. Nulls have no - * storage and no alignment padding either. We can use/set - * attcacheoff until we reach either a null or a var-width attribute. - */ - off = 0; - for (i = 0;; i++) /* loop exit is at "break" */ + for (i = startAttr; i < firstNullAttr; i++) { - CompactAttribute *att = TupleDescCompactAttr(tupleDesc, i); - - if (HeapTupleHasNulls(tup) && att_isnull(i, bp)) - { - usecache = false; - continue; /* this cannot be the target att */ - } - - /* If we know the next offset, we can skip the rest */ - if (usecache && att->attcacheoff >= 0) - off = att->attcacheoff; - else if (att->attlen == -1) - { - /* - * We can only cache the offset for a varlena attribute if the - * offset is already suitably aligned, so that there would be - * no pad bytes in any case: then the offset will be valid for - * either an aligned or unaligned value. - */ - if (usecache && - off == att_nominal_alignby(off, att->attalignby)) - att->attcacheoff = off; - else - { - off = att_pointer_alignby(off, att->attalignby, -1, - tp + off); - usecache = false; - } - } - else - { - /* not varlena, so safe to use att_nominal_alignby */ - off = att_nominal_alignby(off, att->attalignby); - - if (usecache) - att->attcacheoff = off; - } + int attlen; + + cattr = TupleDescCompactAttr(tupleDesc, i); + attlen = cattr->attlen; + off = att_pointer_alignby(off, + cattr->attalignby, + attlen, + tp + off); + off = att_addlength_pointer(off, attlen, tp + off); + } - if (i == attnum) - break; + for (; i < attnum; i++) + { + int attlen; - off = att_addlength_pointer(off, att->attlen, tp + off); + if (att_isnull(i, bp)) + continue; - if (usecache && att->attlen <= 0) - usecache = false; + cattr = TupleDescCompactAttr(tupleDesc, i); + attlen = cattr->attlen; + off = att_pointer_alignby(off, cattr->attalignby, attlen, + tp + off); + off = att_addlength_pointer(off, attlen, tp + off); } } - return fetchatt(TupleDescCompactAttr(tupleDesc, attnum), tp + off); + cattr = TupleDescCompactAttr(tupleDesc, attnum); + off = att_pointer_alignby(off, + cattr->attalignby, + cattr->attlen, + tp + off); + + return fetchatt(cattr, tp + off); } /* ---------------- @@ -846,7 +754,7 @@ expand_tuple(HeapTuple *targetHeapTuple, Size targetDataLen; Size len; int hoff; - bits8 *nullBits = NULL; + uint8 *nullBits = NULL; int bitMask = 0; char *targetData; uint16 *infoMask; @@ -901,9 +809,9 @@ expand_tuple(HeapTuple *targetHeapTuple, att->attlen, attrmiss[attnum].am_value); - targetDataLen = att_addlength_pointer(targetDataLen, - att->attlen, - attrmiss[attnum].am_value); + targetDataLen = att_addlength_datum(targetDataLen, + att->attlen, + attrmiss[attnum].am_value); } else { @@ -958,7 +866,7 @@ expand_tuple(HeapTuple *targetHeapTuple, /* We also make sure that t_ctid is invalid unless explicitly set */ ItemPointerSetInvalid(&(targetTHeader->t_ctid)); if (targetNullLen > 0) - nullBits = (bits8 *) ((char *) (*targetHeapTuple)->t_data + nullBits = (uint8 *) ((char *) (*targetHeapTuple)->t_data + offsetof(HeapTupleHeaderData, t_bits)); targetData = (char *) (*targetHeapTuple)->t_data + hoff; infoMask = &(targetTHeader->t_infomask); @@ -976,7 +884,7 @@ expand_tuple(HeapTuple *targetHeapTuple, /* Same macro works for MinimalTuples */ HeapTupleHeaderSetNatts(*targetMinimalTuple, natts); if (targetNullLen > 0) - nullBits = (bits8 *) ((char *) *targetMinimalTuple + nullBits = (uint8 *) ((char *) *targetMinimalTuple + offsetof(MinimalTupleData, t_bits)); targetData = (char *) *targetMinimalTuple + hoff; infoMask = &((*targetMinimalTuple)->t_infomask); @@ -1230,8 +1138,8 @@ heap_modify_tuple(HeapTuple tuple, * O(N^2) if there are many non-replaced columns, so it seems better to * err on the side of linear cost. */ - values = (Datum *) palloc(numberOfAttributes * sizeof(Datum)); - isnull = (bool *) palloc(numberOfAttributes * sizeof(bool)); + values = palloc_array(Datum, numberOfAttributes); + isnull = palloc_array(bool, numberOfAttributes); heap_deform_tuple(tuple, tupleDesc, values, isnull); @@ -1292,8 +1200,8 @@ heap_modify_tuple_by_cols(HeapTuple tuple, * allocate and fill values and isnull arrays from the tuple, then replace * selected columns from the input arrays. */ - values = (Datum *) palloc(numberOfAttributes * sizeof(Datum)); - isnull = (bool *) palloc(numberOfAttributes * sizeof(bool)); + values = palloc_array(Datum, numberOfAttributes); + isnull = palloc_array(bool, numberOfAttributes); heap_deform_tuple(tuple, tupleDesc, values, isnull); @@ -1347,77 +1255,106 @@ heap_deform_tuple(HeapTuple tuple, TupleDesc tupleDesc, Datum *values, bool *isnull) { HeapTupleHeader tup = tuple->t_data; + CompactAttribute *cattr; bool hasnulls = HeapTupleHasNulls(tuple); int tdesc_natts = tupleDesc->natts; int natts; /* number of atts to extract */ int attnum; char *tp; /* ptr to tuple data */ uint32 off; /* offset in tuple data */ - bits8 *bp = tup->t_bits; /* ptr to null bitmap in tuple */ - bool slow = false; /* can we use/set attcacheoff? */ + uint8 *bp = tup->t_bits; /* ptr to null bitmap in tuple */ + int firstNonCacheOffsetAttr; + int firstNullAttr; natts = HeapTupleHeaderGetNatts(tup); + /* Did someone forget to call TupleDescFinalize()? */ + Assert(tupleDesc->firstNonCachedOffsetAttr >= 0); + /* * In inheritance situations, it is possible that the given tuple actually * has more fields than the caller is expecting. Don't run off the end of * the caller's arrays. */ natts = Min(natts, tdesc_natts); + firstNonCacheOffsetAttr = Min(tupleDesc->firstNonCachedOffsetAttr, natts); + + if (hasnulls) + { + firstNullAttr = first_null_attr(bp, natts); + + /* + * XXX: it'd be nice to use populate_isnull_array() here, but that + * requires that the isnull array's size is rounded up to the next + * multiple of 8. Doing that would require adjusting many locations + * that allocate the array. + */ + firstNonCacheOffsetAttr = Min(firstNonCacheOffsetAttr, firstNullAttr); + } + else + firstNullAttr = natts; tp = (char *) tup + tup->t_hoff; + attnum = 0; - off = 0; + if (firstNonCacheOffsetAttr > 0) + { +#ifdef USE_ASSERT_CHECKING + /* In Assert enabled builds, verify attcacheoff is correct */ + int offcheck = 0; +#endif + do + { + isnull[attnum] = false; + cattr = TupleDescCompactAttr(tupleDesc, attnum); + off = cattr->attcacheoff; - for (attnum = 0; attnum < natts; attnum++) +#ifdef USE_ASSERT_CHECKING + offcheck = att_nominal_alignby(offcheck, cattr->attalignby); + Assert(offcheck == cattr->attcacheoff); + offcheck += cattr->attlen; +#endif + + values[attnum] = fetch_att_noerr(tp + off, + cattr->attbyval, + cattr->attlen); + } while (++attnum < firstNonCacheOffsetAttr); + off += cattr->attlen; + } + else + off = 0; + + for (; attnum < firstNullAttr; attnum++) { - CompactAttribute *thisatt = TupleDescCompactAttr(tupleDesc, attnum); + isnull[attnum] = false; + cattr = TupleDescCompactAttr(tupleDesc, attnum); + values[attnum] = align_fetch_then_add(tp, + &off, + cattr->attbyval, + cattr->attlen, + cattr->attalignby); + } - if (hasnulls && att_isnull(attnum, bp)) + for (; attnum < natts; attnum++) + { + Assert(hasnulls); + + if (att_isnull(attnum, bp)) { values[attnum] = (Datum) 0; isnull[attnum] = true; - slow = true; /* can't use attcacheoff anymore */ continue; } isnull[attnum] = false; - - if (!slow && thisatt->attcacheoff >= 0) - off = thisatt->attcacheoff; - else if (thisatt->attlen == -1) - { - /* - * We can only cache the offset for a varlena attribute if the - * offset is already suitably aligned, so that there would be no - * pad bytes in any case: then the offset will be valid for either - * an aligned or unaligned value. - */ - if (!slow && - off == att_nominal_alignby(off, thisatt->attalignby)) - thisatt->attcacheoff = off; - else - { - off = att_pointer_alignby(off, thisatt->attalignby, -1, - tp + off); - slow = true; - } - } - else - { - /* not varlena, so safe to use att_nominal_alignby */ - off = att_nominal_alignby(off, thisatt->attalignby); - - if (!slow) - thisatt->attcacheoff = off; - } - - values[attnum] = fetchatt(thisatt, tp + off); - - off = att_addlength_pointer(off, thisatt->attlen, tp + off); - - if (thisatt->attlen <= 0) - slow = true; /* can't use attcacheoff anymore */ + cattr = TupleDescCompactAttr(tupleDesc, attnum); + + /* align 'off', fetch the attr's value, and increment off beyond it */ + values[attnum] = align_fetch_then_add(tp, + &off, + cattr->attbyval, + cattr->attlen, + cattr->attalignby); } /* @@ -1502,7 +1439,6 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor, * Allocate and zero the space needed. */ mem = palloc0(len + extra); - memset(mem, 0, extra); tuple = (MinimalTuple) (mem + extra); /* diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c index 1986b943a28be..60bba0a21459a 100644 --- a/src/backend/access/common/indextuple.c +++ b/src/backend/access/common/indextuple.c @@ -4,7 +4,7 @@ * This file contains index tuple accessor and mutator routines, * as well as various tuple utilities. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -108,7 +108,7 @@ index_form_tuple_context(TupleDesc tupleDescriptor, if (VARATT_IS_EXTERNAL(DatumGetPointer(values[i]))) { untoasted_values[i] = - PointerGetDatum(detoast_external_attr((struct varlena *) + PointerGetDatum(detoast_external_attr((varlena *) DatumGetPointer(values[i]))); untoasted_free[i] = true; } @@ -172,10 +172,10 @@ index_form_tuple_context(TupleDesc tupleDescriptor, values, #endif isnull, - (char *) tp + hoff, + tp + hoff, data_size, &tupmask, - (hasnull ? (bits8 *) tp + sizeof(IndexTupleData) : NULL)); + (hasnull ? (uint8 *) tp + sizeof(IndexTupleData) : NULL)); #ifdef TOAST_INDEX_HACK for (i = 0; i < numberOfAttributes; i++) @@ -222,19 +222,7 @@ index_form_tuple_context(TupleDesc tupleDescriptor, * nocache_index_getattr * * This gets called from index_getattr() macro, and only in cases - * where we can't use cacheoffset and the value is not null. - * - * This caches attribute offsets in the attribute descriptor. - * - * An alternative way to speed things up would be to cache offsets - * with the tuple, but that seems more difficult unless you take - * the storage hit of actually putting those offsets into the - * tuple you send to disk. Yuck. - * - * This scheme will be slightly slower than that, but should - * perform well for queries which hit large #'s of tuples. After - * you cache the offsets once, examining all the other tuples using - * the same attribute descriptor will go much quicker. -cim 5/4/91 + * where we can't use attcacheoff and the value is not null. * ---------------- */ Datum @@ -242,205 +230,125 @@ nocache_index_getattr(IndexTuple tup, int attnum, TupleDesc tupleDesc) { + CompactAttribute *cattr; char *tp; /* ptr to data part of tuple */ - bits8 *bp = NULL; /* ptr to null bitmap in tuple */ - bool slow = false; /* do we have to walk attrs? */ + uint8 *bp = NULL; /* ptr to null bitmap in tuple */ int data_off; /* tuple data offset */ int off; /* current offset within data */ + int startAttr; + int firstNullAttr; + bool hasnulls = IndexTupleHasNulls(tup); + int i; - /* ---------------- - * Three cases: - * - * 1: No nulls and no variable-width attributes. - * 2: Has a null or a var-width AFTER att. - * 3: Has nulls or var-widths BEFORE att. - * ---------------- - */ - - data_off = IndexInfoFindDataOffset(tup->t_info); + /* Did someone forget to call TupleDescFinalize()? */ + Assert(tupleDesc->firstNonCachedOffsetAttr >= 0); attnum--; - if (IndexTupleHasNulls(tup)) - { - /* - * there's a null somewhere in the tuple - * - * check to see if desired att is null - */ - - /* XXX "knows" t_bits are just after fixed tuple header! */ - bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData)); - - /* - * Now check to see if any preceding bits are null... - */ - { - int byte = attnum >> 3; - int finalbit = attnum & 0x07; - - /* check for nulls "before" final bit of last byte */ - if ((~bp[byte]) & ((1 << finalbit) - 1)) - slow = true; - else - { - /* check for nulls in any "earlier" bytes */ - int i; - - for (i = 0; i < byte; i++) - { - if (bp[i] != 0xFF) - { - slow = true; - break; - } - } - } - } - } - + data_off = IndexInfoFindDataOffset(tup->t_info); tp = (char *) tup + data_off; - if (!slow) + /* + * To minimize the number of attributes we need to look at, start walking + * the tuple at the attribute with the highest attcacheoff prior to attnum + * or the first NULL attribute prior to attnum, whichever comes first. + */ + if (hasnulls) { - CompactAttribute *att; - - /* - * If we get here, there are no nulls up to and including the target - * attribute. If we have a cached offset, we can use it. - */ - att = TupleDescCompactAttr(tupleDesc, attnum); - if (att->attcacheoff >= 0) - return fetchatt(att, tp + att->attcacheoff); - - /* - * Otherwise, check for non-fixed-length attrs up to and including - * target. If there aren't any, it's safe to cheaply initialize the - * cached offsets for these attrs. - */ - if (IndexTupleHasVarwidths(tup)) - { - int j; - - for (j = 0; j <= attnum; j++) - { - if (TupleDescCompactAttr(tupleDesc, j)->attlen <= 0) - { - slow = true; - break; - } - } - } + bp = (uint8 *) ((char *) tup + sizeof(IndexTupleData)); + firstNullAttr = first_null_attr(bp, attnum); } + else + firstNullAttr = attnum; - if (!slow) + if (tupleDesc->firstNonCachedOffsetAttr > 0 && firstNullAttr > 0) { - int natts = tupleDesc->natts; - int j = 1; - /* - * If we get here, we have a tuple with no nulls or var-widths up to - * and including the target attribute, so we can use the cached offset - * ... only we don't have it yet, or we'd not have got here. Since - * it's cheap to compute offsets for fixed-width columns, we take the - * opportunity to initialize the cached offsets for *all* the leading - * fixed-width columns, in hope of avoiding future visits to this - * routine. + * Try to start with the highest attribute with an attcacheoff that's + * prior to the one we're looking for, or with the attribute prior to + * the first NULL attribute, if there is one. */ - TupleDescCompactAttr(tupleDesc, 0)->attcacheoff = 0; + startAttr = Min(tupleDesc->firstNonCachedOffsetAttr - 1, firstNullAttr - 1); + off = TupleDescCompactAttr(tupleDesc, startAttr)->attcacheoff; + } + else + { + /* Otherwise, start at the beginning... */ + startAttr = 0; + off = 0; + } - /* we might have set some offsets in the slow path previously */ - while (j < natts && TupleDescCompactAttr(tupleDesc, j)->attcacheoff > 0) - j++; + /* + * Calculate 'off' up to the first NULL attr. We use two cheaper loops + * when the tuple has no variable-width columns. When variable-width + * columns exists, we use att_addlength_pointer() to move the offset + * beyond the current attribute. + */ + if (IndexTupleHasVarwidths(tup)) + { + /* Calculate the offset up until the first NULL */ + for (i = startAttr; i < firstNullAttr; i++) + { + cattr = TupleDescCompactAttr(tupleDesc, i); - off = TupleDescCompactAttr(tupleDesc, j - 1)->attcacheoff + - TupleDescCompactAttr(tupleDesc, j - 1)->attlen; + off = att_pointer_alignby(off, + cattr->attalignby, + cattr->attlen, + tp + off); + off = att_addlength_pointer(off, cattr->attlen, tp + off); + } - for (; j < natts; j++) + /* Calculate the offset for any remaining columns. */ + for (; i < attnum; i++) { - CompactAttribute *att = TupleDescCompactAttr(tupleDesc, j); + Assert(hasnulls); - if (att->attlen <= 0) - break; + if (att_isnull(i, bp)) + continue; - off = att_nominal_alignby(off, att->attalignby); + cattr = TupleDescCompactAttr(tupleDesc, i); - att->attcacheoff = off; - - off += att->attlen; + off = att_pointer_alignby(off, + cattr->attalignby, + cattr->attlen, + tp + off); + off = att_addlength_pointer(off, cattr->attlen, tp + off); } - - Assert(j > attnum); - - off = TupleDescCompactAttr(tupleDesc, attnum)->attcacheoff; } else { - bool usecache = true; - int i; + /* Handle tuples with only fixed-width attributes */ - /* - * Now we know that we have to walk the tuple CAREFULLY. But we still - * might be able to cache some offsets for next time. - * - * Note - This loop is a little tricky. For each non-null attribute, - * we have to first account for alignment padding before the attr, - * then advance over the attr based on its length. Nulls have no - * storage and no alignment padding either. We can use/set - * attcacheoff until we reach either a null or a var-width attribute. - */ - off = 0; - for (i = 0;; i++) /* loop exit is at "break" */ + /* Calculate the offset up until the first NULL */ + for (i = startAttr; i < firstNullAttr; i++) { - CompactAttribute *att = TupleDescCompactAttr(tupleDesc, i); + cattr = TupleDescCompactAttr(tupleDesc, i); - if (IndexTupleHasNulls(tup) && att_isnull(i, bp)) - { - usecache = false; - continue; /* this cannot be the target att */ - } - - /* If we know the next offset, we can skip the rest */ - if (usecache && att->attcacheoff >= 0) - off = att->attcacheoff; - else if (att->attlen == -1) - { - /* - * We can only cache the offset for a varlena attribute if the - * offset is already suitably aligned, so that there would be - * no pad bytes in any case: then the offset will be valid for - * either an aligned or unaligned value. - */ - if (usecache && - off == att_nominal_alignby(off, att->attalignby)) - att->attcacheoff = off; - else - { - off = att_pointer_alignby(off, att->attalignby, -1, - tp + off); - usecache = false; - } - } - else - { - /* not varlena, so safe to use att_nominal_alignby */ - off = att_nominal_alignby(off, att->attalignby); + Assert(cattr->attlen > 0); + off = att_nominal_alignby(off, cattr->attalignby); + off += cattr->attlen; + } - if (usecache) - att->attcacheoff = off; - } + /* Calculate the offset for any remaining columns. */ + for (; i < attnum; i++) + { + Assert(hasnulls); - if (i == attnum) - break; + if (att_isnull(i, bp)) + continue; - off = att_addlength_pointer(off, att->attlen, tp + off); + cattr = TupleDescCompactAttr(tupleDesc, i); - if (usecache && att->attlen <= 0) - usecache = false; + Assert(cattr->attlen > 0); + off = att_nominal_alignby(off, cattr->attalignby); + off += cattr->attlen; } } - return fetchatt(TupleDescCompactAttr(tupleDesc, attnum), tp + off); + cattr = TupleDescCompactAttr(tupleDesc, attnum); + off = att_pointer_alignby(off, cattr->attalignby, + cattr->attlen, tp + off); + return fetchatt(cattr, tp + off); } /* @@ -457,10 +365,10 @@ index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor, Datum *values, bool *isnull) { char *tp; /* ptr to tuple data */ - bits8 *bp; /* ptr to null bitmap in tuple */ + uint8 *bp; /* ptr to null bitmap in tuple */ /* XXX "knows" t_bits are just after fixed tuple header! */ - bp = (bits8 *) ((char *) tup + sizeof(IndexTupleData)); + bp = (uint8 *) ((char *) tup + sizeof(IndexTupleData)); tp = (char *) tup + IndexInfoFindDataOffset(tup->t_info); @@ -478,65 +386,89 @@ index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor, void index_deform_tuple_internal(TupleDesc tupleDescriptor, Datum *values, bool *isnull, - char *tp, bits8 *bp, int hasnulls) + char *tp, uint8 *bp, int hasnulls) { + CompactAttribute *cattr; int natts = tupleDescriptor->natts; /* number of atts to extract */ - int attnum; - int off = 0; /* offset in tuple data */ - bool slow = false; /* can we use/set attcacheoff? */ + int attnum = 0; + uint32 off = 0; /* offset in tuple data */ + int firstNonCacheOffsetAttr; + int firstNullAttr; /* Assert to protect callers who allocate fixed-size arrays */ Assert(natts <= INDEX_MAX_KEYS); - for (attnum = 0; attnum < natts; attnum++) + /* Did someone forget to call TupleDescFinalize()? */ + Assert(tupleDescriptor->firstNonCachedOffsetAttr >= 0); + + firstNonCacheOffsetAttr = Min(tupleDescriptor->firstNonCachedOffsetAttr, natts); + + if (hasnulls) + { + firstNullAttr = first_null_attr(bp, natts); + firstNonCacheOffsetAttr = Min(firstNonCacheOffsetAttr, firstNullAttr); + } + else + firstNullAttr = natts; + + if (firstNonCacheOffsetAttr > 0) { - CompactAttribute *thisatt = TupleDescCompactAttr(tupleDescriptor, attnum); +#ifdef USE_ASSERT_CHECKING + /* In Assert enabled builds, verify attcacheoff is correct */ + off = 0; +#endif - if (hasnulls && att_isnull(attnum, bp)) + do { - values[attnum] = (Datum) 0; - isnull[attnum] = true; - slow = true; /* can't use attcacheoff anymore */ - continue; - } + isnull[attnum] = false; + cattr = TupleDescCompactAttr(tupleDescriptor, attnum); - isnull[attnum] = false; +#ifdef USE_ASSERT_CHECKING + off = att_nominal_alignby(off, cattr->attalignby); + Assert(off == cattr->attcacheoff); + off += cattr->attlen; +#endif - if (!slow && thisatt->attcacheoff >= 0) - off = thisatt->attcacheoff; - else if (thisatt->attlen == -1) - { - /* - * We can only cache the offset for a varlena attribute if the - * offset is already suitably aligned, so that there would be no - * pad bytes in any case: then the offset will be valid for either - * an aligned or unaligned value. - */ - if (!slow && - off == att_nominal_alignby(off, thisatt->attalignby)) - thisatt->attcacheoff = off; - else - { - off = att_pointer_alignby(off, thisatt->attalignby, -1, - tp + off); - slow = true; - } - } - else - { - /* not varlena, so safe to use att_nominal_alignby */ - off = att_nominal_alignby(off, thisatt->attalignby); + values[attnum] = fetch_att_noerr(tp + cattr->attcacheoff, cattr->attbyval, + cattr->attlen); + } while (++attnum < firstNonCacheOffsetAttr); - if (!slow) - thisatt->attcacheoff = off; - } + off = cattr->attcacheoff + cattr->attlen; + } - values[attnum] = fetchatt(thisatt, tp + off); + for (; attnum < firstNullAttr; attnum++) + { + isnull[attnum] = false; + cattr = TupleDescCompactAttr(tupleDescriptor, attnum); + + /* align 'off', fetch the datum, and increment off beyond the datum */ + values[attnum] = align_fetch_then_add(tp, + &off, + cattr->attbyval, + cattr->attlen, + cattr->attalignby); + } - off = att_addlength_pointer(off, thisatt->attlen, tp + off); + for (; attnum < natts; attnum++) + { + Assert(hasnulls); - if (thisatt->attlen <= 0) - slow = true; /* can't use attcacheoff anymore */ + if (att_isnull(attnum, bp)) + { + values[attnum] = (Datum) 0; + isnull[attnum] = true; + continue; + } + + isnull[attnum] = false; + cattr = TupleDescCompactAttr(tupleDescriptor, attnum); + + /* align 'off', fetch the attr's value, and increment off beyond it */ + values[attnum] = align_fetch_then_add(tp, + &off, + cattr->attbyval, + cattr->attlen, + cattr->attalignby); } } diff --git a/src/backend/access/common/meson.build b/src/backend/access/common/meson.build index e3cdbe7a22e1a..35e89b5ea67d5 100644 --- a/src/backend/access/common/meson.build +++ b/src/backend/access/common/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'attmap.c', diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c index f346ab3e8125b..af0c707db01fc 100644 --- a/src/backend/access/common/printsimple.c +++ b/src/backend/access/common/printsimple.c @@ -8,7 +8,7 @@ * doesn't handle standalone backends or protocol versions other than * 3.0, because we don't need such handling for current applications. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -23,6 +23,7 @@ #include "libpq/pqformat.h" #include "libpq/protocol.h" #include "utils/builtins.h" +#include "varatt.h" /* * At startup time, send a RowDescription message. @@ -123,7 +124,7 @@ printsimple(TupleTableSlot *slot, DestReceiver *self) case OIDOID: { - Oid num = ObjectIdGetDatum(value); + Oid num = DatumGetObjectId(value); char str[10]; /* 10 digits */ int len; diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c index 830a3d883aa2e..616bdafd3951d 100644 --- a/src/backend/access/common/printtup.c +++ b/src/backend/access/common/printtup.c @@ -5,7 +5,7 @@ * clients and standalone backends are supported here). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,6 +22,7 @@ #include "utils/lsyscache.h" #include "utils/memdebug.h" #include "utils/memutils.h" +#include "varatt.h" static void printtup_startup(DestReceiver *self, int operation, @@ -70,7 +71,7 @@ typedef struct DestReceiver * printtup_create_DR(CommandDest dest) { - DR_printtup *self = (DR_printtup *) palloc0(sizeof(DR_printtup)); + DR_printtup *self = palloc0_object(DR_printtup); self->pub.receiveSlot = printtup; /* might get changed later */ self->pub.rStartup = printtup_startup; @@ -350,7 +351,7 @@ printtup(TupleTableSlot *slot, DestReceiver *self) */ if (thisState->typisvarlena) VALGRIND_CHECK_MEM_IS_DEFINED(DatumGetPointer(attr), - VARSIZE_ANY(attr)); + VARSIZE_ANY(DatumGetPointer(attr))); if (thisState->format == 0) { @@ -430,7 +431,7 @@ printatt(unsigned attributeId, value != NULL ? " = \"" : "", value != NULL ? value : "", value != NULL ? "\"" : "", - (unsigned int) (attributeP->atttypid), + attributeP->atttypid, attributeP->attlen, attributeP->atttypmod, attributeP->attbyval ? 't' : 'f'); diff --git a/src/backend/access/common/relation.c b/src/backend/access/common/relation.c index 22c4cd5a25658..38b356b8239b3 100644 --- a/src/backend/access/common/relation.c +++ b/src/backend/access/common/relation.c @@ -3,7 +3,7 @@ * relation.c * Generic relation related routines. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "catalog/namespace.h" #include "pgstat.h" #include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/inval.h" #include "utils/syscache.h" diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 46c1dce222d10..3e832c3797e89 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -3,7 +3,7 @@ * reloptions.c * Core support for relation options (pg_class.reloptions) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -28,6 +28,7 @@ #include "commands/defrem.h" #include "commands/tablespace.h" #include "nodes/makefuncs.h" +#include "storage/lock.h" #include "utils/array.h" #include "utils/attoptcache.h" #include "utils/builtins.h" @@ -40,9 +41,9 @@ * * To add an option: * - * (i) decide on a type (bool, integer, real, enum, string), name, default - * value, upper and lower bounds (if applicable); for strings, consider a - * validation routine. + * (i) decide on a type (bool, ternary, integer, real, enum, string), name, + * default value, upper and lower bounds (if applicable); for strings, + * consider a validation routine. * (ii) add a record below (or use add__reloption). * (iii) add it to the appropriate options struct (perhaps StdRdOptions) * (iv) add it to the appropriate handling routine (perhaps @@ -50,6 +51,10 @@ * (v) make sure the lock level is set correctly for that operation * (vi) don't forget to document the option * + * From the user's point of view, a 'ternary' is exactly like a Boolean, + * so we don't document it separately. On the implementation side, the + * handling code can detect the case where the option has not been set. + * * The default choice for any new option should be AccessExclusiveLock. * In some cases the lock level can be reduced from there, but the lock * level chosen should always conflict with itself to ensure that multiple @@ -147,15 +152,6 @@ static relopt_bool boolRelOpts[] = }, false }, - { - { - "vacuum_truncate", - "Enables vacuum to truncate empty pages at the end of this table", - RELOPT_KIND_HEAP | RELOPT_KIND_TOAST, - ShareUpdateExclusiveLock - }, - true - }, { { "deduplicate_items", @@ -170,6 +166,24 @@ static relopt_bool boolRelOpts[] = {{NULL}} }; +static relopt_ternary ternaryRelOpts[] = +{ + { + { + "vacuum_truncate", + "Enables vacuum to truncate empty pages at the end of this table", + RELOPT_KIND_HEAP | RELOPT_KIND_TOAST, + ShareUpdateExclusiveLock + } + }, + /* list terminator */ + { + { + NULL + } + } +}; + static relopt_int intRelOpts[] = { { @@ -222,6 +236,15 @@ static relopt_int intRelOpts[] = }, SPGIST_DEFAULT_FILLFACTOR, SPGIST_MIN_FILLFACTOR, 100 }, + { + { + "autovacuum_parallel_workers", + "Maximum number of parallel autovacuum workers that can be used for processing this table.", + RELOPT_KIND_HEAP, + ShareUpdateExclusiveLock + }, + -1, -1, 1024 + }, { { "autovacuum_vacuum_threshold", @@ -322,12 +345,21 @@ static relopt_int intRelOpts[] = { { "log_autovacuum_min_duration", - "Sets the minimum execution time above which autovacuum actions will be logged", + "Sets the minimum execution time above which vacuum actions by autovacuum will be logged", RELOPT_KIND_HEAP | RELOPT_KIND_TOAST, ShareUpdateExclusiveLock }, -1, -1, INT_MAX }, + { + { + "log_autoanalyze_min_duration", + "Sets the minimum execution time above which analyze actions by autovacuum will be logged", + RELOPT_KIND_HEAP, + ShareUpdateExclusiveLock + }, + -1, -1, INT_MAX + }, { { "toast_tuple_target", @@ -562,7 +594,7 @@ static relopt_string stringRelOpts[] = }; static relopt_gen **relOpts = NULL; -static bits32 last_assigned_kind = RELOPT_KIND_LAST_DEFAULT; +static uint32 last_assigned_kind = RELOPT_KIND_LAST_DEFAULT; static int num_custom_options = 0; static relopt_gen **custom_options = NULL; @@ -578,7 +610,7 @@ static void parse_one_reloption(relopt_value *option, char *text_str, * relation options. */ #define GET_STRING_RELOPTION_LEN(option) \ - ((option).isset ? strlen((option).values.string_val) : \ + ((option).isset ? strlen((option).string_val) : \ ((relopt_string *) (option).gen)->default_len) /* @@ -600,6 +632,13 @@ initialize_reloptions(void) boolRelOpts[i].gen.lockmode)); j++; } + for (i = 0; ternaryRelOpts[i].gen.name; i++) + { + Assert(DoLockModesConflict(ternaryRelOpts[i].gen.lockmode, + ternaryRelOpts[i].gen.lockmode)); + j++; + } + for (i = 0; intRelOpts[i].gen.name; i++) { Assert(DoLockModesConflict(intRelOpts[i].gen.lockmode, @@ -640,6 +679,14 @@ initialize_reloptions(void) j++; } + for (i = 0; ternaryRelOpts[i].gen.name; i++) + { + relOpts[j] = &ternaryRelOpts[i].gen; + relOpts[j]->type = RELOPT_TYPE_TERNARY; + relOpts[j]->namelen = strlen(relOpts[j]->name); + j++; + } + for (i = 0; intRelOpts[i].gen.name; i++) { relOpts[j] = &intRelOpts[i].gen; @@ -767,7 +814,7 @@ register_reloptions_validator(local_relopts *relopts, relopts_validator validato static void add_local_reloption(local_relopts *relopts, relopt_gen *newoption, int offset) { - local_relopt *opt = palloc(sizeof(*opt)); + local_relopt *opt = palloc_object(local_relopt); Assert(offset < relopts->relopt_struct_size); @@ -783,7 +830,7 @@ add_local_reloption(local_relopts *relopts, relopt_gen *newoption, int offset) * (for types other than string) */ static relopt_gen * -allocate_reloption(bits32 kinds, int type, const char *name, const char *desc, +allocate_reloption(uint32 kinds, int type, const char *name, const char *desc, LOCKMODE lockmode) { MemoryContext oldcxt; @@ -800,6 +847,9 @@ allocate_reloption(bits32 kinds, int type, const char *name, const char *desc, case RELOPT_TYPE_BOOL: size = sizeof(relopt_bool); break; + case RELOPT_TYPE_TERNARY: + size = sizeof(relopt_ternary); + break; case RELOPT_TYPE_INT: size = sizeof(relopt_int); break; @@ -840,7 +890,7 @@ allocate_reloption(bits32 kinds, int type, const char *name, const char *desc, * Allocate and initialize a new boolean reloption */ static relopt_bool * -init_bool_reloption(bits32 kinds, const char *name, const char *desc, +init_bool_reloption(uint32 kinds, const char *name, const char *desc, bool default_val, LOCKMODE lockmode) { relopt_bool *newoption; @@ -857,7 +907,7 @@ init_bool_reloption(bits32 kinds, const char *name, const char *desc, * Add a new boolean reloption */ void -add_bool_reloption(bits32 kinds, const char *name, const char *desc, +add_bool_reloption(uint32 kinds, const char *name, const char *desc, bool default_val, LOCKMODE lockmode) { relopt_bool *newoption = init_bool_reloption(kinds, name, desc, @@ -883,13 +933,62 @@ add_local_bool_reloption(local_relopts *relopts, const char *name, add_local_reloption(relopts, (relopt_gen *) newoption, offset); } +/* + * init_ternary_reloption + * Allocate and initialize a new ternary reloption + */ +static relopt_ternary * +init_ternary_reloption(uint32 kinds, const char *name, const char *desc, + LOCKMODE lockmode) +{ + relopt_ternary *newoption; + + newoption = (relopt_ternary *) + allocate_reloption(kinds, RELOPT_TYPE_TERNARY, name, desc, lockmode); + + return newoption; +} + +/* + * add_ternary_reloption + * Add a new ternary reloption + */ +void +add_ternary_reloption(uint32 kinds, const char *name, const char *desc, + LOCKMODE lockmode) +{ + relopt_ternary *newoption; + + newoption = + init_ternary_reloption(kinds, name, desc, lockmode); + + add_reloption((relopt_gen *) newoption); +} + +/* + * add_local_ternary_reloption + * Add a new ternary local reloption + * + * 'offset' is offset of ternary-typed field. + */ +void +add_local_ternary_reloption(local_relopts *relopts, const char *name, + const char *desc, int offset) +{ + relopt_ternary *newoption; + + newoption = + init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc, 0); + + add_local_reloption(relopts, (relopt_gen *) newoption, offset); +} /* * init_real_reloption * Allocate and initialize a new integer reloption */ static relopt_int * -init_int_reloption(bits32 kinds, const char *name, const char *desc, +init_int_reloption(uint32 kinds, const char *name, const char *desc, int default_val, int min_val, int max_val, LOCKMODE lockmode) { @@ -909,7 +1008,7 @@ init_int_reloption(bits32 kinds, const char *name, const char *desc, * Add a new integer reloption */ void -add_int_reloption(bits32 kinds, const char *name, const char *desc, int default_val, +add_int_reloption(uint32 kinds, const char *name, const char *desc, int default_val, int min_val, int max_val, LOCKMODE lockmode) { relopt_int *newoption = init_int_reloption(kinds, name, desc, @@ -942,7 +1041,7 @@ add_local_int_reloption(local_relopts *relopts, const char *name, * Allocate and initialize a new real reloption */ static relopt_real * -init_real_reloption(bits32 kinds, const char *name, const char *desc, +init_real_reloption(uint32 kinds, const char *name, const char *desc, double default_val, double min_val, double max_val, LOCKMODE lockmode) { @@ -962,7 +1061,7 @@ init_real_reloption(bits32 kinds, const char *name, const char *desc, * Add a new float reloption */ void -add_real_reloption(bits32 kinds, const char *name, const char *desc, +add_real_reloption(uint32 kinds, const char *name, const char *desc, double default_val, double min_val, double max_val, LOCKMODE lockmode) { @@ -997,7 +1096,7 @@ add_local_real_reloption(local_relopts *relopts, const char *name, * Allocate and initialize a new enum reloption */ static relopt_enum * -init_enum_reloption(bits32 kinds, const char *name, const char *desc, +init_enum_reloption(uint32 kinds, const char *name, const char *desc, relopt_enum_elt_def *members, int default_val, const char *detailmsg, LOCKMODE lockmode) { @@ -1026,7 +1125,7 @@ init_enum_reloption(bits32 kinds, const char *name, const char *desc, * they are valid throughout the life of the process. */ void -add_enum_reloption(bits32 kinds, const char *name, const char *desc, +add_enum_reloption(uint32 kinds, const char *name, const char *desc, relopt_enum_elt_def *members, int default_val, const char *detailmsg, LOCKMODE lockmode) { @@ -1061,7 +1160,7 @@ add_local_enum_reloption(local_relopts *relopts, const char *name, * Allocate and initialize a new string reloption */ static relopt_string * -init_string_reloption(bits32 kinds, const char *name, const char *desc, +init_string_reloption(uint32 kinds, const char *name, const char *desc, const char *default_val, validate_string_relopt validator, fill_string_relopt filler, @@ -1106,7 +1205,7 @@ init_string_reloption(bits32 kinds, const char *name, const char *desc, * the validation. */ void -add_string_reloption(bits32 kinds, const char *name, const char *desc, +add_string_reloption(uint32 kinds, const char *name, const char *desc, const char *default_val, validate_string_relopt validator, LOCKMODE lockmode) { @@ -1164,7 +1263,7 @@ add_local_string_reloption(local_relopts *relopts, const char *name, * but we declare them as Datums to avoid including array.h in reloptions.h. */ Datum -transformRelOptions(Datum oldOptions, List *defList, const char *namspace, +transformRelOptions(Datum oldOptions, List *defList, const char *nameSpace, const char *const validnsps[], bool acceptOidsOff, bool isReset) { Datum result; @@ -1179,7 +1278,7 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, astate = NULL; /* Copy any oldOptions that aren't to be replaced */ - if (PointerIsValid(DatumGetPointer(oldOptions))) + if (DatumGetPointer(oldOptions) != NULL) { ArrayType *array = DatumGetArrayTypeP(oldOptions); Datum *oldoptions; @@ -1190,8 +1289,8 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, for (i = 0; i < noldoptions; i++) { - char *text_str = VARDATA(oldoptions[i]); - int text_len = VARSIZE(oldoptions[i]) - VARHDRSZ; + char *text_str = VARDATA(DatumGetPointer(oldoptions[i])); + int text_len = VARSIZE(DatumGetPointer(oldoptions[i])) - VARHDRSZ; /* Search for a match in defList */ foreach(cell, defList) @@ -1200,14 +1299,14 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, int kw_len; /* ignore if not in the same namespace */ - if (namspace == NULL) + if (nameSpace == NULL) { if (def->defnamespace != NULL) continue; } else if (def->defnamespace == NULL) continue; - else if (strcmp(def->defnamespace, namspace) != 0) + else if (strcmp(def->defnamespace, nameSpace) != 0) continue; kw_len = strlen(def->defname); @@ -1243,8 +1342,9 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, } else { - text *t; + const char *name; const char *value; + text *t; Size len; /* @@ -1276,14 +1376,14 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, } /* ignore if not in the same namespace */ - if (namspace == NULL) + if (nameSpace == NULL) { if (def->defnamespace != NULL) continue; } else if (def->defnamespace == NULL) continue; - else if (strcmp(def->defnamespace, namspace) != 0) + else if (strcmp(def->defnamespace, nameSpace) != 0) continue; /* @@ -1291,11 +1391,19 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, * have just "name", assume "name=true" is meant. Note: the * namespace is not output. */ + name = def->defname; if (def->arg != NULL) value = defGetString(def); else value = "true"; + /* Insist that name not contain "=", else "a=b=c" is ambiguous */ + if (strchr(name, '=') != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid option name \"%s\": must not contain \"=\"", + name))); + /* * This is not a great place for this test, but there's no other * convenient place to filter the option out. As WITH (oids = @@ -1303,7 +1411,7 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, * amount of ugly. */ if (acceptOidsOff && def->defnamespace == NULL && - strcmp(def->defname, "oids") == 0) + strcmp(name, "oids") == 0) { if (defGetBoolean(def)) ereport(ERROR, @@ -1313,11 +1421,11 @@ transformRelOptions(Datum oldOptions, List *defList, const char *namspace, continue; } - len = VARHDRSZ + strlen(def->defname) + 1 + strlen(value); + len = VARHDRSZ + strlen(name) + 1 + strlen(value); /* +1 leaves room for sprintf's trailing null */ t = (text *) palloc(len + 1); SET_VARSIZE(t, len); - sprintf(VARDATA(t), "%s=%s", def->defname, value); + sprintf(VARDATA(t), "%s=%s", name, value); astate = accumArrayResult(astate, PointerGetDatum(t), false, TEXTOID, @@ -1348,7 +1456,7 @@ untransformRelOptions(Datum options) int i; /* Nothing to do if no options */ - if (!PointerIsValid(DatumGetPointer(options))) + if (DatumGetPointer(options) == NULL) return result; array = DatumGetArrayTypeP(options); @@ -1447,8 +1555,8 @@ parseRelOptionsInternal(Datum options, bool validate, for (i = 0; i < noptions; i++) { - char *text_str = VARDATA(optiondatums[i]); - int text_len = VARSIZE(optiondatums[i]) - VARHDRSZ; + char *text_str = VARDATA(DatumGetPointer(optiondatums[i])); + int text_len = VARSIZE(DatumGetPointer(optiondatums[i])) - VARHDRSZ; int j; /* Search for a match in reloptions */ @@ -1540,7 +1648,7 @@ parseRelOptions(Datum options, bool validate, relopt_kind kind, } /* Done if no options */ - if (PointerIsValid(DatumGetPointer(options))) + if (DatumGetPointer(options) != NULL) parseRelOptionsInternal(options, validate, reloptions, numoptions); *numrelopts = numoptions; @@ -1552,7 +1660,7 @@ static relopt_value * parseLocalRelOptions(local_relopts *relopts, Datum options, bool validate) { int nopts = list_length(relopts->options); - relopt_value *values = palloc(sizeof(*values) * nopts); + relopt_value *values = palloc_array(relopt_value, nopts); ListCell *lc; int i = 0; @@ -1600,7 +1708,7 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len, { case RELOPT_TYPE_BOOL: { - parsed = parse_bool(value, &option->values.bool_val); + parsed = parse_bool(value, &option->bool_val); if (validate && !parsed) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -1608,18 +1716,32 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len, option->gen->name, value))); } break; + case RELOPT_TYPE_TERNARY: + { + bool b; + + parsed = parse_bool(value, &b); + option->ternary_val = b ? PG_TERNARY_TRUE : + PG_TERNARY_FALSE; + if (validate && !parsed) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for boolean option \"%s\": %s", + option->gen->name, value)); + } + break; case RELOPT_TYPE_INT: { relopt_int *optint = (relopt_int *) option->gen; - parsed = parse_int(value, &option->values.int_val, 0, NULL); + parsed = parse_int(value, &option->int_val, 0, NULL); if (validate && !parsed) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid value for integer option \"%s\": %s", option->gen->name, value))); - if (validate && (option->values.int_val < optint->min || - option->values.int_val > optint->max)) + if (validate && (option->int_val < optint->min || + option->int_val > optint->max)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("value %s out of bounds for option \"%s\"", @@ -1632,14 +1754,14 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len, { relopt_real *optreal = (relopt_real *) option->gen; - parsed = parse_real(value, &option->values.real_val, 0, NULL); + parsed = parse_real(value, &option->real_val, 0, NULL); if (validate && !parsed) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid value for floating point option \"%s\": %s", option->gen->name, value))); - if (validate && (option->values.real_val < optreal->min || - option->values.real_val > optreal->max)) + if (validate && (option->real_val < optreal->min || + option->real_val > optreal->max)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("value %s out of bounds for option \"%s\"", @@ -1658,7 +1780,7 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len, { if (pg_strcasecmp(value, elt->string_val) == 0) { - option->values.enum_val = elt->symbol_val; + option->enum_val = elt->symbol_val; parsed = true; break; } @@ -1676,14 +1798,14 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len, * not asked to validate, just use the default numeric value. */ if (!parsed) - option->values.enum_val = optenum->default_val; + option->enum_val = optenum->default_val; } break; case RELOPT_TYPE_STRING: { relopt_string *optstring = (relopt_string *) option->gen; - option->values.string_val = value; + option->string_val = value; nofree = true; if (validate && optstring->validate_cb) (optstring->validate_cb) (value); @@ -1725,7 +1847,7 @@ allocateReloptStruct(Size base, relopt_value *options, int numoptions) if (optstr->fill_cb) { - const char *val = optval->isset ? optval->values.string_val : + const char *val = optval->isset ? optval->string_val : optstr->default_isnull ? NULL : optstr->default_val; size += optstr->fill_cb(val, NULL); @@ -1771,43 +1893,36 @@ fillRelOptions(void *rdopts, Size basesize, char *itempos = ((char *) rdopts) + elems[j].offset; char *string_val; - /* - * If isset_offset is provided, store whether the reloption is - * set there. - */ - if (elems[j].isset_offset > 0) - { - char *setpos = ((char *) rdopts) + elems[j].isset_offset; - - *(bool *) setpos = options[i].isset; - } - switch (options[i].gen->type) { case RELOPT_TYPE_BOOL: *(bool *) itempos = options[i].isset ? - options[i].values.bool_val : + options[i].bool_val : ((relopt_bool *) options[i].gen)->default_val; break; + case RELOPT_TYPE_TERNARY: + *(pg_ternary *) itempos = options[i].isset ? + options[i].ternary_val : PG_TERNARY_UNSET; + break; case RELOPT_TYPE_INT: *(int *) itempos = options[i].isset ? - options[i].values.int_val : + options[i].int_val : ((relopt_int *) options[i].gen)->default_val; break; case RELOPT_TYPE_REAL: *(double *) itempos = options[i].isset ? - options[i].values.real_val : + options[i].real_val : ((relopt_real *) options[i].gen)->default_val; break; case RELOPT_TYPE_ENUM: *(int *) itempos = options[i].isset ? - options[i].values.enum_val : + options[i].enum_val : ((relopt_enum *) options[i].gen)->default_val; break; case RELOPT_TYPE_STRING: optstring = (relopt_string *) options[i].gen; if (options[i].isset) - string_val = options[i].values.string_val; + string_val = options[i].string_val; else if (!optstring->default_isnull) string_val = optstring->default_val; else @@ -1863,6 +1978,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)}, {"autovacuum_enabled", RELOPT_TYPE_BOOL, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)}, + {"autovacuum_parallel_workers", RELOPT_TYPE_INT, + offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)}, {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)}, {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT, @@ -1886,7 +2003,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)}, {"log_autovacuum_min_duration", RELOPT_TYPE_INT, - offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_min_duration)}, + offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)}, + {"log_autoanalyze_min_duration", RELOPT_TYPE_INT, + offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)}, {"toast_tuple_target", RELOPT_TYPE_INT, offsetof(StdRdOptions, toast_tuple_target)}, {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL, @@ -1903,8 +2022,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) offsetof(StdRdOptions, parallel_workers)}, {"vacuum_index_cleanup", RELOPT_TYPE_ENUM, offsetof(StdRdOptions, vacuum_index_cleanup)}, - {"vacuum_truncate", RELOPT_TYPE_BOOL, - offsetof(StdRdOptions, vacuum_truncate), offsetof(StdRdOptions, vacuum_truncate_set)}, + {"vacuum_truncate", RELOPT_TYPE_TERNARY, + offsetof(StdRdOptions, vacuum_truncate)}, {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL, offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)} }; @@ -1971,7 +2090,7 @@ void * build_local_reloptions(local_relopts *relopts, Datum options, bool validate) { int noptions = list_length(relopts->options); - relopt_parse_elt *elems = palloc(sizeof(*elems) * noptions); + relopt_parse_elt *elems = palloc_array(relopt_parse_elt, noptions); relopt_value *vals; void *opts; int i = 0; @@ -1984,7 +2103,6 @@ build_local_reloptions(local_relopts *relopts, Datum options, bool validate) elems[i].optname = opt->option->name; elems[i].opttype = opt->option->type; elems[i].offset = opt->offset; - elems[i].isset_offset = 0; /* not supported for local relopts yet */ i++; } @@ -2083,7 +2201,7 @@ index_reloptions(amoptions_function amoptions, Datum reloptions, bool validate) Assert(amoptions != NULL); /* Assume function is strict */ - if (!PointerIsValid(DatumGetPointer(reloptions))) + if (DatumGetPointer(reloptions) == NULL) return NULL; return amoptions(reloptions, validate); diff --git a/src/backend/access/common/scankey.c b/src/backend/access/common/scankey.c index 2d65ab02dd38e..ae2b19648d4f3 100644 --- a/src/backend/access/common/scankey.c +++ b/src/backend/access/common/scankey.c @@ -3,7 +3,7 @@ * scankey.c * scan key support code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/common/session.c b/src/backend/access/common/session.c index 8836f73c90bf1..bd1b7d85b8546 100644 --- a/src/backend/access/common/session.c +++ b/src/backend/access/common/session.c @@ -12,7 +12,7 @@ * Currently this infrastructure is used to share: * - typemod registry for ephemeral row-types, i.e. BlessTupleDesc etc. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/backend/access/common/session.c * diff --git a/src/backend/access/common/syncscan.c b/src/backend/access/common/syncscan.c index 4e2cfc1f7c91c..0f9eb167bed50 100644 --- a/src/backend/access/common/syncscan.c +++ b/src/backend/access/common/syncscan.c @@ -36,7 +36,7 @@ * ss_report_location - update current scan location * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -50,6 +50,7 @@ #include "miscadmin.h" #include "storage/lwlock.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/rel.h" @@ -111,6 +112,14 @@ typedef struct ss_scan_locations_t #define SizeOfScanLocations(N) \ (offsetof(ss_scan_locations_t, items) + (N) * sizeof(ss_lru_item_t)) +static void SyncScanShmemRequest(void *arg); +static void SyncScanShmemInit(void *arg); + +const ShmemCallbacks SyncScanShmemCallbacks = { + .request_fn = SyncScanShmemRequest, + .init_fn = SyncScanShmemInit, +}; + /* Pointer to struct in shared memory */ static ss_scan_locations_t *scan_locations; @@ -120,58 +129,47 @@ static BlockNumber ss_search(RelFileLocator relfilelocator, /* - * SyncScanShmemSize --- report amount of shared memory space needed + * SyncScanShmemRequest --- register this module's shared memory */ -Size -SyncScanShmemSize(void) +static void +SyncScanShmemRequest(void *arg) { - return SizeOfScanLocations(SYNC_SCAN_NELEM); + ShmemRequestStruct(.name = "Sync Scan Locations List", + .size = SizeOfScanLocations(SYNC_SCAN_NELEM), + .ptr = (void **) &scan_locations, + ); } /* * SyncScanShmemInit --- initialize this module's shared memory */ -void -SyncScanShmemInit(void) +static void +SyncScanShmemInit(void *arg) { int i; - bool found; - scan_locations = (ss_scan_locations_t *) - ShmemInitStruct("Sync Scan Locations List", - SizeOfScanLocations(SYNC_SCAN_NELEM), - &found); + scan_locations->head = &scan_locations->items[0]; + scan_locations->tail = &scan_locations->items[SYNC_SCAN_NELEM - 1]; - if (!IsUnderPostmaster) + for (i = 0; i < SYNC_SCAN_NELEM; i++) { - /* Initialize shared memory area */ - Assert(!found); - - scan_locations->head = &scan_locations->items[0]; - scan_locations->tail = &scan_locations->items[SYNC_SCAN_NELEM - 1]; - - for (i = 0; i < SYNC_SCAN_NELEM; i++) - { - ss_lru_item_t *item = &scan_locations->items[i]; - - /* - * Initialize all slots with invalid values. As scans are started, - * these invalid entries will fall off the LRU list and get - * replaced with real entries. - */ - item->location.relfilelocator.spcOid = InvalidOid; - item->location.relfilelocator.dbOid = InvalidOid; - item->location.relfilelocator.relNumber = InvalidRelFileNumber; - item->location.location = InvalidBlockNumber; - - item->prev = (i > 0) ? - (&scan_locations->items[i - 1]) : NULL; - item->next = (i < SYNC_SCAN_NELEM - 1) ? - (&scan_locations->items[i + 1]) : NULL; - } + ss_lru_item_t *item = &scan_locations->items[i]; + + /* + * Initialize all slots with invalid values. As scans are started, + * these invalid entries will fall off the LRU list and get replaced + * with real entries. + */ + item->location.relfilelocator.spcOid = InvalidOid; + item->location.relfilelocator.dbOid = InvalidOid; + item->location.relfilelocator.relNumber = InvalidRelFileNumber; + item->location.location = InvalidBlockNumber; + + item->prev = (i > 0) ? + (&scan_locations->items[i - 1]) : NULL; + item->next = (i < SYNC_SCAN_NELEM - 1) ? + (&scan_locations->items[i + 1]) : NULL; } - else - Assert(found); } /* diff --git a/src/backend/access/common/tidstore.c b/src/backend/access/common/tidstore.c index 5bd75fb499cef..de3e44d0f5d66 100644 --- a/src/backend/access/common/tidstore.c +++ b/src/backend/access/common/tidstore.c @@ -11,7 +11,7 @@ * TidStoreCreateShared(). Other backends can attach to the shared TidStore * by TidStoreAttach(). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -166,7 +166,7 @@ TidStoreCreateLocal(size_t max_bytes, bool insert_only) size_t minContextSize = ALLOCSET_DEFAULT_MINSIZE; size_t maxBlockSize = ALLOCSET_DEFAULT_MAXSIZE; - ts = palloc0(sizeof(TidStore)); + ts = palloc0_object(TidStore); /* choose the maxBlockSize to be no larger than 1/16 of max_bytes */ while (16 * maxBlockSize > max_bytes) @@ -212,7 +212,7 @@ TidStoreCreateShared(size_t max_bytes, int tranche_id) size_t dsa_init_size = DSA_DEFAULT_INIT_SEGMENT_SIZE; size_t dsa_max_size = DSA_MAX_SEGMENT_SIZE; - ts = palloc0(sizeof(TidStore)); + ts = palloc0_object(TidStore); /* * Choose the initial and maximum DSA segment sizes to be no longer than @@ -250,11 +250,11 @@ TidStoreAttach(dsa_handle area_handle, dsa_pointer handle) Assert(DsaPointerIsValid(handle)); /* create per-backend state */ - ts = palloc0(sizeof(TidStore)); + ts = palloc0_object(TidStore); area = dsa_attach(area_handle); - /* Find the shared the shared radix tree */ + /* Find the shared radix tree */ ts->tree.shared = shared_ts_attach(area, handle); ts->area = area; @@ -418,7 +418,7 @@ TidStoreSetBlockOffsets(TidStore *ts, BlockNumber blkno, OffsetNumber *offsets, /* Return true if the given TID is present in the TidStore */ bool -TidStoreIsMember(TidStore *ts, ItemPointer tid) +TidStoreIsMember(TidStore *ts, const ItemPointerData *tid) { int wordnum; int bitnum; @@ -472,7 +472,7 @@ TidStoreBeginIterate(TidStore *ts) { TidStoreIter *iter; - iter = palloc0(sizeof(TidStoreIter)); + iter = palloc0_object(TidStoreIter); iter->ts = ts; if (TidStoreIsShared(ts)) diff --git a/src/backend/access/common/toast_compression.c b/src/backend/access/common/toast_compression.c index 21f2f4af97e3f..5a5d579494a23 100644 --- a/src/backend/access/common/toast_compression.c +++ b/src/backend/access/common/toast_compression.c @@ -3,7 +3,7 @@ * toast_compression.c * Functions for toast compression. * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -23,25 +23,25 @@ #include "varatt.h" /* GUC */ -int default_toast_compression = TOAST_PGLZ_COMPRESSION; +int default_toast_compression = DEFAULT_TOAST_COMPRESSION; -#define NO_LZ4_SUPPORT() \ +#define NO_COMPRESSION_SUPPORT(method) \ ereport(ERROR, \ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \ - errmsg("compression method lz4 not supported"), \ - errdetail("This functionality requires the server to be built with lz4 support."))) + errmsg("compression method %s not supported", method), \ + errdetail("This functionality requires the server to be built with %s support.", method))) /* * Compress a varlena using PGLZ. * * Returns the compressed varlena, or NULL if compression fails. */ -struct varlena * -pglz_compress_datum(const struct varlena *value) +varlena * +pglz_compress_datum(const varlena *value) { int32 valsize, len; - struct varlena *tmp = NULL; + varlena *tmp = NULL; valsize = VARSIZE_ANY_EXHDR(value); @@ -57,8 +57,8 @@ pglz_compress_datum(const struct varlena *value) * Figure out the maximum possible size of the pglz output, add the bytes * that will be needed for varlena overhead, and allocate that amount. */ - tmp = (struct varlena *) palloc(PGLZ_MAX_OUTPUT(valsize) + - VARHDRSZ_COMPRESSED); + tmp = (varlena *) palloc(PGLZ_MAX_OUTPUT(valsize) + + VARHDRSZ_COMPRESSED); len = pglz_compress(VARDATA_ANY(value), valsize, @@ -78,17 +78,17 @@ pglz_compress_datum(const struct varlena *value) /* * Decompress a varlena that was compressed using PGLZ. */ -struct varlena * -pglz_decompress_datum(const struct varlena *value) +varlena * +pglz_decompress_datum(const varlena *value) { - struct varlena *result; + varlena *result; int32 rawsize; /* allocate memory for the uncompressed data */ - result = (struct varlena *) palloc(VARDATA_COMPRESSED_GET_EXTSIZE(value) + VARHDRSZ); + result = (varlena *) palloc(VARDATA_COMPRESSED_GET_EXTSIZE(value) + VARHDRSZ); /* decompress the data */ - rawsize = pglz_decompress((char *) value + VARHDRSZ_COMPRESSED, + rawsize = pglz_decompress((const char *) value + VARHDRSZ_COMPRESSED, VARSIZE(value) - VARHDRSZ_COMPRESSED, VARDATA(result), VARDATA_COMPRESSED_GET_EXTSIZE(value), true); @@ -105,18 +105,18 @@ pglz_decompress_datum(const struct varlena *value) /* * Decompress part of a varlena that was compressed using PGLZ. */ -struct varlena * -pglz_decompress_datum_slice(const struct varlena *value, +varlena * +pglz_decompress_datum_slice(const varlena *value, int32 slicelength) { - struct varlena *result; + varlena *result; int32 rawsize; /* allocate memory for the uncompressed data */ - result = (struct varlena *) palloc(slicelength + VARHDRSZ); + result = (varlena *) palloc(slicelength + VARHDRSZ); /* decompress the data */ - rawsize = pglz_decompress((char *) value + VARHDRSZ_COMPRESSED, + rawsize = pglz_decompress((const char *) value + VARHDRSZ_COMPRESSED, VARSIZE(value) - VARHDRSZ_COMPRESSED, VARDATA(result), slicelength, false); @@ -135,17 +135,17 @@ pglz_decompress_datum_slice(const struct varlena *value, * * Returns the compressed varlena, or NULL if compression fails. */ -struct varlena * -lz4_compress_datum(const struct varlena *value) +varlena * +lz4_compress_datum(const varlena *value) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + NO_COMPRESSION_SUPPORT("lz4"); return NULL; /* keep compiler quiet */ #else int32 valsize; int32 len; int32 max_size; - struct varlena *tmp = NULL; + varlena *tmp = NULL; valsize = VARSIZE_ANY_EXHDR(value); @@ -154,7 +154,7 @@ lz4_compress_datum(const struct varlena *value) * that will be needed for varlena overhead, and allocate that amount. */ max_size = LZ4_compressBound(valsize); - tmp = (struct varlena *) palloc(max_size + VARHDRSZ_COMPRESSED); + tmp = (varlena *) palloc(max_size + VARHDRSZ_COMPRESSED); len = LZ4_compress_default(VARDATA_ANY(value), (char *) tmp + VARHDRSZ_COMPRESSED, @@ -178,21 +178,21 @@ lz4_compress_datum(const struct varlena *value) /* * Decompress a varlena that was compressed using LZ4. */ -struct varlena * -lz4_decompress_datum(const struct varlena *value) +varlena * +lz4_decompress_datum(const varlena *value) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + NO_COMPRESSION_SUPPORT("lz4"); return NULL; /* keep compiler quiet */ #else int32 rawsize; - struct varlena *result; + varlena *result; /* allocate memory for the uncompressed data */ - result = (struct varlena *) palloc(VARDATA_COMPRESSED_GET_EXTSIZE(value) + VARHDRSZ); + result = (varlena *) palloc(VARDATA_COMPRESSED_GET_EXTSIZE(value) + VARHDRSZ); /* decompress the data */ - rawsize = LZ4_decompress_safe((char *) value + VARHDRSZ_COMPRESSED, + rawsize = LZ4_decompress_safe((const char *) value + VARHDRSZ_COMPRESSED, VARDATA(result), VARSIZE(value) - VARHDRSZ_COMPRESSED, VARDATA_COMPRESSED_GET_EXTSIZE(value)); @@ -211,25 +211,25 @@ lz4_decompress_datum(const struct varlena *value) /* * Decompress part of a varlena that was compressed using LZ4. */ -struct varlena * -lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength) +varlena * +lz4_decompress_datum_slice(const varlena *value, int32 slicelength) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + NO_COMPRESSION_SUPPORT("lz4"); return NULL; /* keep compiler quiet */ #else int32 rawsize; - struct varlena *result; + varlena *result; /* slice decompression not supported prior to 1.8.3 */ if (LZ4_versionNumber() < 10803) return lz4_decompress_datum(value); /* allocate memory for the uncompressed data */ - result = (struct varlena *) palloc(slicelength + VARHDRSZ); + result = (varlena *) palloc(slicelength + VARHDRSZ); /* decompress the data */ - rawsize = LZ4_decompress_safe_partial((char *) value + VARHDRSZ_COMPRESSED, + rawsize = LZ4_decompress_safe_partial((const char *) value + VARHDRSZ_COMPRESSED, VARDATA(result), VARSIZE(value) - VARHDRSZ_COMPRESSED, slicelength, @@ -251,7 +251,7 @@ lz4_decompress_datum_slice(const struct varlena *value, int32 slicelength) * Returns TOAST_INVALID_COMPRESSION_ID if the varlena is not compressed. */ ToastCompressionId -toast_get_compression_id(struct varlena *attr) +toast_get_compression_id(varlena *attr) { ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; @@ -262,7 +262,7 @@ toast_get_compression_id(struct varlena *attr) */ if (VARATT_IS_EXTERNAL_ONDISK(attr)) { - struct varatt_external toast_pointer; + varatt_external toast_pointer; VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr); @@ -289,7 +289,7 @@ CompressionNameToMethod(const char *compression) else if (strcmp(compression, "lz4") == 0) { #ifndef USE_LZ4 - NO_LZ4_SUPPORT(); + NO_COMPRESSION_SUPPORT("lz4"); #endif return TOAST_LZ4_COMPRESSION; } diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 7d8be8346ce52..77d42e7ed65af 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -3,7 +3,7 @@ * toast_internals.c * Functions for internal use by the TOAST system. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/access/common/toast_internals.c @@ -45,7 +45,7 @@ static bool toastid_valueid_exists(Oid toastrelid, Oid valueid); Datum toast_compress_datum(Datum value, char cmethod) { - struct varlena *tmp = NULL; + varlena *tmp = NULL; int32 valsize; ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID; @@ -64,11 +64,11 @@ toast_compress_datum(Datum value, char cmethod) switch (cmethod) { case TOAST_PGLZ_COMPRESSION: - tmp = pglz_compress_datum((const struct varlena *) value); + tmp = pglz_compress_datum((const varlena *) DatumGetPointer(value)); cmid = TOAST_PGLZ_COMPRESSION_ID; break; case TOAST_LZ4_COMPRESSION: - tmp = lz4_compress_datum((const struct varlena *) value); + tmp = lz4_compress_datum((const varlena *) DatumGetPointer(value)); cmid = TOAST_LZ4_COMPRESSION_ID; break; default: @@ -117,26 +117,14 @@ toast_compress_datum(Datum value, char cmethod) */ Datum toast_save_datum(Relation rel, Datum value, - struct varlena *oldexternal, int options) + varlena *oldexternal, uint32 options) { Relation toastrel; Relation *toastidxs; - HeapTuple toasttup; TupleDesc toasttupDesc; - Datum t_values[3]; - bool t_isnull[3]; CommandId mycid = GetCurrentCommandId(true); - struct varlena *result; - struct varatt_external toast_pointer; - union - { - struct varlena hdr; - /* this is to make the union big enough for a chunk: */ - char data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ]; - /* ensure union is aligned well enough: */ - int32 align_it; - } chunk_data; - int32 chunk_size; + varlena *result; + varatt_external toast_pointer; int32 chunk_seq = 0; char *data_p; int32 data_todo; @@ -144,7 +132,7 @@ toast_save_datum(Relation rel, Datum value, int num_indexes; int validIndex; - Assert(!VARATT_IS_EXTERNAL(value)); + Assert(!VARATT_IS_EXTERNAL(dval)); /* * Open the toast relation and its indexes. We can use the index to check @@ -237,7 +225,7 @@ toast_save_datum(Relation rel, Datum value, toast_pointer.va_valueid = InvalidOid; if (oldexternal != NULL) { - struct varatt_external old_toast_pointer; + varatt_external old_toast_pointer; Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal)); /* Must copy to access aligned fields */ @@ -289,21 +277,21 @@ toast_save_datum(Relation rel, Datum value, } } - /* - * Initialize constant parts of the tuple data - */ - t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid); - t_values[2] = PointerGetDatum(&chunk_data); - t_isnull[0] = false; - t_isnull[1] = false; - t_isnull[2] = false; - /* * Split up the item into chunks */ while (data_todo > 0) { - int i; + HeapTuple toasttup; + Datum t_values[3]; + bool t_isnull[3] = {0}; + union + { + alignas(int32) varlena hdr; + /* this is to make the union big enough for a chunk: */ + char data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ]; + } chunk_data; + int32 chunk_size; CHECK_FOR_INTERRUPTS(); @@ -315,9 +303,12 @@ toast_save_datum(Relation rel, Datum value, /* * Build a tuple and store it */ + t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid); t_values[1] = Int32GetDatum(chunk_seq++); SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ); memcpy(VARDATA(&chunk_data), data_p, chunk_size); + t_values[2] = PointerGetDatum(&chunk_data); + toasttup = heap_form_tuple(toasttupDesc, t_values, t_isnull); heap_insert(toastrel, toasttup, mycid, options, NULL); @@ -333,7 +324,7 @@ toast_save_datum(Relation rel, Datum value, * Note also that there had better not be any user-created index on * the TOAST table, since we don't bother to update anything else. */ - for (i = 0; i < num_indexes; i++) + for (int i = 0; i < num_indexes; i++) { /* Only index relations marked as ready can be updated */ if (toastidxs[i]->rd_index->indisready) @@ -368,7 +359,7 @@ toast_save_datum(Relation rel, Datum value, /* * Create the TOAST pointer value that we'll return */ - result = (struct varlena *) palloc(TOAST_POINTER_SIZE); + result = (varlena *) palloc(TOAST_POINTER_SIZE); SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK); memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer)); @@ -384,8 +375,8 @@ toast_save_datum(Relation rel, Datum value, void toast_delete_datum(Relation rel, Datum value, bool is_speculative) { - struct varlena *attr = (struct varlena *) DatumGetPointer(value); - struct varatt_external toast_pointer; + varlena *attr = (varlena *) DatumGetPointer(value); + varatt_external toast_pointer; Relation toastrel; Relation *toastidxs; ScanKeyData toastkey; @@ -577,7 +568,7 @@ toast_open_indexes(Relation toastrel, *num_indexes = list_length(indexlist); /* Open all the index relations */ - *toastidxs = (Relation *) palloc(*num_indexes * sizeof(Relation)); + *toastidxs = palloc_array(Relation, *num_indexes); foreach(lc, indexlist) (*toastidxs)[i++] = index_open(lfirst_oid(lc), lock); diff --git a/src/backend/access/common/tupconvert.c b/src/backend/access/common/tupconvert.c index 54dc2f4ab80ab..6e495fda5b16c 100644 --- a/src/backend/access/common/tupconvert.c +++ b/src/backend/access/common/tupconvert.c @@ -7,7 +7,7 @@ * equivalent but might have columns in a different order or different sets of * dropped columns. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,6 +18,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "access/tupconvert.h" #include "executor/tuptable.h" @@ -74,17 +75,17 @@ convert_tuples_by_position(TupleDesc indesc, } /* Prepare the map structure */ - map = (TupleConversionMap *) palloc(sizeof(TupleConversionMap)); + map = palloc_object(TupleConversionMap); map->indesc = indesc; map->outdesc = outdesc; map->attrMap = attrMap; /* preallocate workspace for Datum arrays */ n = outdesc->natts + 1; /* +1 for NULL */ - map->outvalues = (Datum *) palloc(n * sizeof(Datum)); - map->outisnull = (bool *) palloc(n * sizeof(bool)); + map->outvalues = palloc_array(Datum, n); + map->outisnull = palloc_array(bool, n); n = indesc->natts + 1; /* +1 for NULL */ - map->invalues = (Datum *) palloc(n * sizeof(Datum)); - map->inisnull = (bool *) palloc(n * sizeof(bool)); + map->invalues = palloc_array(Datum, n); + map->inisnull = palloc_array(bool, n); map->invalues[0] = (Datum) 0; /* set up the NULL entry */ map->inisnull[0] = true; @@ -131,16 +132,16 @@ convert_tuples_by_name_attrmap(TupleDesc indesc, Assert(attrMap != NULL); /* Prepare the map structure */ - map = (TupleConversionMap *) palloc(sizeof(TupleConversionMap)); + map = palloc_object(TupleConversionMap); map->indesc = indesc; map->outdesc = outdesc; map->attrMap = attrMap; /* preallocate workspace for Datum arrays */ - map->outvalues = (Datum *) palloc(n * sizeof(Datum)); - map->outisnull = (bool *) palloc(n * sizeof(bool)); + map->outvalues = palloc_array(Datum, n); + map->outisnull = palloc_array(bool, n); n = indesc->natts + 1; /* +1 for NULL */ - map->invalues = (Datum *) palloc(n * sizeof(Datum)); - map->inisnull = (bool *) palloc(n * sizeof(bool)); + map->invalues = palloc_array(Datum, n); + map->inisnull = palloc_array(bool, n); map->invalues[0] = (Datum) 0; /* set up the NULL entry */ map->inisnull[0] = true; diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c index ffd0c78f905a5..196472c05d066 100644 --- a/src/backend/access/common/tupdesc.c +++ b/src/backend/access/common/tupdesc.c @@ -3,7 +3,7 @@ * tupdesc.c * POSTGRES tuple descriptor support code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -86,25 +86,8 @@ populate_compact_attribute_internal(Form_pg_attribute src, IsCatalogRelationOid(src->attrelid) ? ATTNULLABLE_VALID : ATTNULLABLE_UNKNOWN; - switch (src->attalign) - { - case TYPALIGN_INT: - dst->attalignby = ALIGNOF_INT; - break; - case TYPALIGN_CHAR: - dst->attalignby = sizeof(char); - break; - case TYPALIGN_DOUBLE: - dst->attalignby = ALIGNOF_DOUBLE; - break; - case TYPALIGN_SHORT: - dst->attalignby = ALIGNOF_SHORT; - break; - default: - dst->attalignby = 0; - elog(ERROR, "invalid attalign value: %c", src->attalign); - break; - } + /* Compute numeric alignment requirement, too */ + dst->attalignby = typalign_to_alignby(src->attalign); } /* @@ -142,10 +125,17 @@ void verify_compact_attribute(TupleDesc tupdesc, int attnum) { #ifdef USE_ASSERT_CHECKING - CompactAttribute *cattr = &tupdesc->compact_attrs[attnum]; + CompactAttribute cattr; Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum); CompactAttribute tmp; + /* + * Make a temp copy of the TupleDesc's CompactAttribute. This may be a + * shared TupleDesc and the attcacheoff might get changed by another + * backend. + */ + memcpy(&cattr, &tupdesc->compact_attrs[attnum], sizeof(CompactAttribute)); + /* * Populate the temporary CompactAttribute from the corresponding * Form_pg_attribute @@ -156,11 +146,11 @@ verify_compact_attribute(TupleDesc tupdesc, int attnum) * Make the attcacheoff match since it's been reset to -1 by * populate_compact_attribute_internal. Same with attnullability. */ - tmp.attcacheoff = cattr->attcacheoff; - tmp.attnullability = cattr->attnullability; + tmp.attcacheoff = cattr.attcacheoff; + tmp.attnullability = cattr.attnullability; /* Check the freshly populated CompactAttribute matches the TupleDesc's */ - Assert(memcmp(&tmp, cattr, sizeof(CompactAttribute)) == 0); + Assert(memcmp(&tmp, &cattr, sizeof(CompactAttribute)) == 0); #endif } @@ -207,6 +197,10 @@ CreateTemplateTupleDesc(int natts) desc->tdtypmod = -1; desc->tdrefcount = -1; /* assume not reference-counted */ + /* This will be set to the correct value by TupleDescFinalize() */ + desc->firstNonCachedOffsetAttr = -1; + desc->firstNonGuaranteedAttr = -1; + return desc; } @@ -231,6 +225,9 @@ CreateTupleDesc(int natts, Form_pg_attribute *attrs) memcpy(TupleDescAttr(desc, i), attrs[i], ATTRIBUTE_FIXED_PART_SIZE); populate_compact_attribute(desc, i); } + + TupleDescFinalize(desc); + return desc; } @@ -249,10 +246,11 @@ CreateTupleDescCopy(TupleDesc tupdesc) desc = CreateTemplateTupleDesc(tupdesc->natts); - /* Flat-copy the attribute array */ - memcpy(TupleDescAttr(desc, 0), - TupleDescAttr(tupdesc, 0), - desc->natts * sizeof(FormData_pg_attribute)); + /* Flat-copy the attribute array (unless there are no attributes) */ + if (desc->natts > 0) + memcpy(TupleDescAttr(desc, 0), + TupleDescAttr(tupdesc, 0), + desc->natts * sizeof(FormData_pg_attribute)); /* * Since we're not copying constraints and defaults, clear fields @@ -275,6 +273,8 @@ CreateTupleDescCopy(TupleDesc tupdesc) desc->tdtypeid = tupdesc->tdtypeid; desc->tdtypmod = tupdesc->tdtypmod; + TupleDescFinalize(desc); + return desc; } @@ -295,10 +295,11 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts) desc = CreateTemplateTupleDesc(natts); - /* Flat-copy the attribute array */ - memcpy(TupleDescAttr(desc, 0), - TupleDescAttr(tupdesc, 0), - desc->natts * sizeof(FormData_pg_attribute)); + /* Flat-copy the attribute array (unless there are no attributes) */ + if (desc->natts > 0) + memcpy(TupleDescAttr(desc, 0), + TupleDescAttr(tupdesc, 0), + desc->natts * sizeof(FormData_pg_attribute)); /* * Since we're not copying constraints and defaults, clear fields @@ -321,6 +322,8 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts) desc->tdtypeid = tupdesc->tdtypeid; desc->tdtypmod = tupdesc->tdtypmod; + TupleDescFinalize(desc); + return desc; } @@ -338,10 +341,11 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc) desc = CreateTemplateTupleDesc(tupdesc->natts); - /* Flat-copy the attribute array */ - memcpy(TupleDescAttr(desc, 0), - TupleDescAttr(tupdesc, 0), - desc->natts * sizeof(FormData_pg_attribute)); + /* Flat-copy the attribute array (unless there are no attributes) */ + if (desc->natts > 0) + memcpy(TupleDescAttr(desc, 0), + TupleDescAttr(tupdesc, 0), + desc->natts * sizeof(FormData_pg_attribute)); for (i = 0; i < desc->natts; i++) { @@ -354,7 +358,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc) /* Copy the TupleConstr data structure, if any */ if (constr) { - TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr)); + TupleConstr *cpy = palloc0_object(TupleConstr); cpy->has_not_null = constr->has_not_null; cpy->has_generated_stored = constr->has_generated_stored; @@ -406,6 +410,8 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc) desc->tdtypeid = tupdesc->tdtypeid; desc->tdtypmod = tupdesc->tdtypmod; + TupleDescFinalize(desc); + return desc; } @@ -448,6 +454,8 @@ TupleDescCopy(TupleDesc dst, TupleDesc src) * source's refcount would be wrong in any case.) */ dst->tdrefcount = -1; + + TupleDescFinalize(dst); } /* @@ -456,6 +464,9 @@ TupleDescCopy(TupleDesc dst, TupleDesc src) * descriptor to another. * * !!! Constraints and defaults are not copied !!! + * + * The caller must take care of calling TupleDescFinalize() on 'dst' once all + * TupleDesc changes have been made. */ void TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno, @@ -467,8 +478,8 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno, /* * sanity checks */ - Assert(PointerIsValid(src)); - Assert(PointerIsValid(dst)); + Assert(src); + Assert(dst); Assert(srcAttno >= 1); Assert(srcAttno <= src->natts); Assert(dstAttno >= 1); @@ -488,6 +499,60 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno, populate_compact_attribute(dst, dstAttno - 1); } +/* + * TupleDescFinalize + * Finalize the given TupleDesc. This must be called after the + * attributes arrays have been populated or adjusted by any code. + * + * Must be called after populate_compact_attribute() and before + * BlessTupleDesc(). + */ +void +TupleDescFinalize(TupleDesc tupdesc) +{ + int firstNonCachedOffsetAttr = 0; + int firstNonGuaranteedAttr = tupdesc->natts; + int off = 0; + + for (int i = 0; i < tupdesc->natts; i++) + { + CompactAttribute *cattr = TupleDescCompactAttr(tupdesc, i); + + /* + * Find the highest attnum which is guaranteed to exist in all tuples + * in the table. We currently only pay attention to byval attributes + * to allow additional optimizations during tuple deformation. + */ + if (firstNonGuaranteedAttr == tupdesc->natts && + (cattr->attnullability != ATTNULLABLE_VALID || !cattr->attbyval || + cattr->atthasmissing || cattr->attisdropped || cattr->attlen <= 0)) + firstNonGuaranteedAttr = i; + + if (cattr->attlen <= 0) + break; + + off = att_nominal_alignby(off, cattr->attalignby); + + /* + * attcacheoff is an int16, so don't try to cache any offsets larger + * than will fit in that type. Any attributes which are offset more + * than 2^15 are likely due to variable-length attributes. Since we + * don't cache offsets for or beyond variable-length attributes, using + * an int16 rather than an int32 here is unlikely to cost us anything. + */ + if (off > PG_INT16_MAX) + break; + + cattr->attcacheoff = (int16) off; + + off += cattr->attlen; + firstNonCachedOffsetAttr = i + 1; + } + + tupdesc->firstNonCachedOffsetAttr = firstNonCachedOffsetAttr; + tupdesc->firstNonGuaranteedAttr = firstNonGuaranteedAttr; +} + /* * Free a TupleDesc including all substructure */ @@ -808,10 +873,10 @@ hashRowType(TupleDesc desc) uint32 s; int i; - s = hash_combine(0, hash_uint32(desc->natts)); - s = hash_combine(s, hash_uint32(desc->tdtypeid)); + s = hash_combine(0, hash_bytes_uint32(desc->natts)); + s = hash_combine(s, hash_bytes_uint32(desc->tdtypeid)); for (i = 0; i < desc->natts; ++i) - s = hash_combine(s, hash_uint32(TupleDescAttr(desc, i)->atttypid)); + s = hash_combine(s, hash_bytes_uint32(TupleDescAttr(desc, i)->atttypid)); return s; } @@ -846,7 +911,7 @@ TupleDescInitEntry(TupleDesc desc, /* * sanity checks */ - Assert(PointerIsValid(desc)); + Assert(desc); Assert(attributeNumber >= 1); Assert(attributeNumber <= desc->natts); Assert(attdim >= 0); @@ -918,7 +983,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc, Form_pg_attribute att; /* sanity checks */ - Assert(PointerIsValid(desc)); + Assert(desc); Assert(attributeNumber >= 1); Assert(attributeNumber <= desc->natts); Assert(attdim >= 0); @@ -986,7 +1051,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc, case INT8OID: att->attlen = 8; - att->attbyval = FLOAT8PASSBYVAL; + att->attbyval = true; att->attalign = TYPALIGN_DOUBLE; att->attstorage = TYPSTORAGE_PLAIN; att->attcompression = InvalidCompressionMethod; @@ -1023,7 +1088,7 @@ TupleDescInitEntryCollation(TupleDesc desc, /* * sanity checks */ - Assert(PointerIsValid(desc)); + Assert(desc); Assert(attributeNumber >= 1); Assert(attributeNumber <= desc->natts); @@ -1075,6 +1140,8 @@ BuildDescFromLists(const List *names, const List *types, const List *typmods, co TupleDescInitEntryCollation(desc, attnum, attcollation); } + TupleDescFinalize(desc); + return desc; } diff --git a/src/backend/access/gin/README b/src/backend/access/gin/README index 742bcbad499fa..4c16dc4d4e0ec 100644 --- a/src/backend/access/gin/README +++ b/src/backend/access/gin/README @@ -393,11 +393,11 @@ tree leafs in logical order by rightlinks and removes deletable TIDs from posting lists. Posting trees are processed by links from entry tree leafs. They are vacuumed in two stages. At first stage, deletable TIDs are removed from leafs. If first stage detects at least one empty page, then at the second stage -ginScanToDelete() deletes empty pages. +ginScanPostingTreeToDelete() deletes empty pages. -ginScanToDelete() traverses the whole tree in depth-first manner. It starts -from the full cleanup lock on the tree root. This lock prevents all the -concurrent insertions into this tree while we're deleting pages. However, +ginScanPostingTreeToDelete() traverses the whole tree in depth-first manner. +It starts from the full cleanup lock on the tree root. This lock prevents all +the concurrent insertions into this tree while we're deleting pages. However, there are still might be some in-progress readers, who traversed root before we locked it. diff --git a/src/backend/access/gin/ginarrayproc.c b/src/backend/access/gin/ginarrayproc.c index 1f821323eb0a0..12f1731c5bd1c 100644 --- a/src/backend/access/gin/ginarrayproc.c +++ b/src/backend/access/gin/ginarrayproc.c @@ -4,7 +4,7 @@ * support functions for GIN's indexing of any array * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -82,9 +82,10 @@ ginqueryarrayextract(PG_FUNCTION_ARGS) ArrayType *array = PG_GETARG_ARRAYTYPE_P_COPY(0); int32 *nkeys = (int32 *) PG_GETARG_POINTER(1); StrategyNumber strategy = PG_GETARG_UINT16(2); - - /* bool **pmatch = (bool **) PG_GETARG_POINTER(3); */ - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ +#ifdef NOT_USED + bool **pmatch = (bool **) PG_GETARG_POINTER(3); + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); +#endif bool **nullFlags = (bool **) PG_GETARG_POINTER(5); int32 *searchMode = (int32 *) PG_GETARG_POINTER(6); int16 elmlen; @@ -143,14 +144,17 @@ ginarrayconsistent(PG_FUNCTION_ARGS) { bool *check = (bool *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* ArrayType *query = PG_GETARG_ARRAYTYPE_P(2); */ +#ifdef NOT_USED + ArrayType *query = PG_GETARG_ARRAYTYPE_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ +#ifdef NOT_USED + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(5); - - /* Datum *queryKeys = (Datum *) PG_GETARG_POINTER(6); */ +#ifdef NOT_USED + Datum *queryKeys = (Datum *) PG_GETARG_POINTER(6); +#endif bool *nullFlags = (bool *) PG_GETARG_POINTER(7); bool res; int32 i; @@ -227,12 +231,14 @@ ginarraytriconsistent(PG_FUNCTION_ARGS) { GinTernaryValue *check = (GinTernaryValue *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* ArrayType *query = PG_GETARG_ARRAYTYPE_P(2); */ +#ifdef NOT_USED + ArrayType *query = PG_GETARG_ARRAYTYPE_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ - /* Datum *queryKeys = (Datum *) PG_GETARG_POINTER(5); */ +#ifdef NOT_USED + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); + Datum *queryKeys = (Datum *) PG_GETARG_POINTER(5); +#endif bool *nullFlags = (bool *) PG_GETARG_POINTER(6); GinTernaryValue res; int32 i; diff --git a/src/backend/access/gin/ginbtree.c b/src/backend/access/gin/ginbtree.c index 644d484ea53c6..3d3a9da56b18d 100644 --- a/src/backend/access/gin/ginbtree.c +++ b/src/backend/access/gin/ginbtree.c @@ -4,7 +4,7 @@ * page utilities routines for the postgres inverted index access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -85,7 +85,7 @@ ginFindLeafPage(GinBtree btree, bool searchMode, { GinBtreeStack *stack; - stack = (GinBtreeStack *) palloc(sizeof(GinBtreeStack)); + stack = palloc_object(GinBtreeStack); stack->blkno = btree->rootBlkno; stack->buffer = ReadBuffer(btree->index, btree->rootBlkno); stack->parent = NULL; @@ -152,7 +152,7 @@ ginFindLeafPage(GinBtree btree, bool searchMode, } else { - GinBtreeStack *ptr = (GinBtreeStack *) palloc(sizeof(GinBtreeStack)); + GinBtreeStack *ptr = palloc_object(GinBtreeStack); ptr->parent = stack; stack = ptr; @@ -246,7 +246,7 @@ ginFindParents(GinBtree btree, GinBtreeStack *stack) blkno = root->blkno; buffer = root->buffer; - ptr = (GinBtreeStack *) palloc(sizeof(GinBtreeStack)); + ptr = palloc_object(GinBtreeStack); for (;;) { diff --git a/src/backend/access/gin/ginbulk.c b/src/backend/access/gin/ginbulk.c index 302cb2092a9a8..85865b391053c 100644 --- a/src/backend/access/gin/ginbulk.c +++ b/src/backend/access/gin/ginbulk.c @@ -4,7 +4,7 @@ * routines for fast build of inverted index * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -93,7 +93,7 @@ ginAllocEntryAccumulator(void *arg) */ if (accum->entryallocator == NULL || accum->eas_used >= DEF_NENTRY) { - accum->entryallocator = palloc(sizeof(GinEntryAccumulator) * DEF_NENTRY); + accum->entryallocator = palloc_array(GinEntryAccumulator, DEF_NENTRY); accum->allocatedMemory += GetMemoryChunkSpace(accum->entryallocator); accum->eas_used = 0; } @@ -177,8 +177,7 @@ ginInsertBAEntry(BuildAccumulator *accum, ea->maxcount = DEF_NPTR; ea->count = 1; ea->shouldSort = false; - ea->list = - (ItemPointerData *) palloc(sizeof(ItemPointerData) * DEF_NPTR); + ea->list = palloc_array(ItemPointerData, DEF_NPTR); ea->list[0] = *heapptr; accum->allocatedMemory += GetMemoryChunkSpace(ea->list); } @@ -245,7 +244,7 @@ ginInsertBAEntries(BuildAccumulator *accum, static int qsortCompareItemPointers(const void *a, const void *b) { - int res = ginCompareItemPointers((ItemPointer) a, (ItemPointer) b); + int res = ginCompareItemPointers((const ItemPointerData *) a, (const ItemPointerData *) b); /* Assert that there are no equal item pointers being sorted */ Assert(res != 0); diff --git a/src/backend/access/gin/gindatapage.c b/src/backend/access/gin/gindatapage.c index 6c2c61947204a..c5d7db28077da 100644 --- a/src/backend/access/gin/gindatapage.c +++ b/src/backend/access/gin/gindatapage.c @@ -4,7 +4,7 @@ * routines for handling GIN posting tree pages. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -140,20 +140,20 @@ GinDataLeafPageGetItems(Page page, int *nitems, ItemPointerData advancePast) { GinPostingList *seg = GinDataLeafPageGetPostingList(page); Size len = GinDataLeafPageGetPostingListSize(page); - Pointer endptr = ((Pointer) seg) + len; + char *endptr = (char *) seg + len; GinPostingList *next; /* Skip to the segment containing advancePast+1 */ if (ItemPointerIsValid(&advancePast)) { next = GinNextPostingListSegment(seg); - while ((Pointer) next < endptr && + while ((char *) next < endptr && ginCompareItemPointers(&next->first, &advancePast) <= 0) { seg = next; next = GinNextPostingListSegment(seg); } - len = endptr - (Pointer) seg; + len = endptr - (char *) seg; } if (len > 0) @@ -607,11 +607,11 @@ dataBeginPlaceToPageLeaf(GinBtree btree, Buffer buf, GinBtreeStack *stack, if (append) elog(DEBUG2, "appended %d new items to block %u; %d bytes (%d to go)", - maxitems, BufferGetBlockNumber(buf), (int) leaf->lsize, + maxitems, BufferGetBlockNumber(buf), leaf->lsize, items->nitem - items->curitem - maxitems); else elog(DEBUG2, "inserted %d new items to block %u; %d bytes (%d to go)", - maxitems, BufferGetBlockNumber(buf), (int) leaf->lsize, + maxitems, BufferGetBlockNumber(buf), leaf->lsize, items->nitem - items->curitem - maxitems); } else @@ -693,11 +693,11 @@ dataBeginPlaceToPageLeaf(GinBtree btree, Buffer buf, GinBtreeStack *stack, if (append) elog(DEBUG2, "appended %d items to block %u; split %d/%d (%d to go)", - maxitems, BufferGetBlockNumber(buf), (int) leaf->lsize, (int) leaf->rsize, + maxitems, BufferGetBlockNumber(buf), leaf->lsize, leaf->rsize, items->nitem - items->curitem - maxitems); else elog(DEBUG2, "inserted %d items to block %u; split %d/%d (%d to go)", - maxitems, BufferGetBlockNumber(buf), (int) leaf->lsize, (int) leaf->rsize, + maxitems, BufferGetBlockNumber(buf), leaf->lsize, leaf->rsize, items->nitem - items->curitem - maxitems); } @@ -1332,7 +1332,7 @@ dataSplitPageInternal(GinBtree btree, Buffer origbuf, static void * dataPrepareDownlink(GinBtree btree, Buffer lbuf) { - PostingItem *pitem = palloc(sizeof(PostingItem)); + PostingItem *pitem = palloc_object(PostingItem); Page lpage = BufferGetPage(lbuf); PostingItemSetBlockNumber(pitem, BufferGetBlockNumber(lbuf)); @@ -1371,10 +1371,10 @@ disassembleLeaf(Page page) { disassembledLeaf *leaf; GinPostingList *seg; - Pointer segbegin; - Pointer segend; + char *segbegin; + char *segend; - leaf = palloc0(sizeof(disassembledLeaf)); + leaf = palloc0_object(disassembledLeaf); dlist_init(&leaf->segments); if (GinPageIsCompressed(page)) @@ -1383,11 +1383,11 @@ disassembleLeaf(Page page) * Create a leafSegmentInfo entry for each segment. */ seg = GinDataLeafPageGetPostingList(page); - segbegin = (Pointer) seg; + segbegin = (char *) seg; segend = segbegin + GinDataLeafPageGetPostingListSize(page); - while ((Pointer) seg < segend) + while ((char *) seg < segend) { - leafSegmentInfo *seginfo = palloc(sizeof(leafSegmentInfo)); + leafSegmentInfo *seginfo = palloc_object(leafSegmentInfo); seginfo->action = GIN_SEGMENT_UNMODIFIED; seginfo->seg = seg; @@ -1414,7 +1414,7 @@ disassembleLeaf(Page page) if (nuncompressed > 0) { - seginfo = palloc(sizeof(leafSegmentInfo)); + seginfo = palloc_object(leafSegmentInfo); seginfo->action = GIN_SEGMENT_REPLACE; seginfo->seg = NULL; @@ -1455,7 +1455,7 @@ addItemsToLeaf(disassembledLeaf *leaf, ItemPointer newItems, int nNewItems) */ if (dlist_is_empty(&leaf->segments)) { - newseg = palloc(sizeof(leafSegmentInfo)); + newseg = palloc_object(leafSegmentInfo); newseg->seg = NULL; newseg->items = newItems; newseg->nitems = nNewItems; @@ -1512,7 +1512,7 @@ addItemsToLeaf(disassembledLeaf *leaf, ItemPointer newItems, int nNewItems) cur->seg != NULL && SizeOfGinPostingList(cur->seg) >= GinPostingListSegmentTargetSize) { - newseg = palloc(sizeof(leafSegmentInfo)); + newseg = palloc_object(leafSegmentInfo); newseg->seg = NULL; newseg->items = nextnew; newseg->nitems = nthis; @@ -1629,7 +1629,7 @@ leafRepackItems(disassembledLeaf *leaf, ItemPointer remaining) if (seginfo->action != GIN_SEGMENT_INSERT) seginfo->action = GIN_SEGMENT_REPLACE; - nextseg = palloc(sizeof(leafSegmentInfo)); + nextseg = palloc_object(leafSegmentInfo); nextseg->action = GIN_SEGMENT_INSERT; nextseg->seg = NULL; nextseg->items = &seginfo->items[npacked]; @@ -1779,7 +1779,7 @@ createPostingTree(Relation index, ItemPointerData *items, uint32 nitems, Buffer buffer; Page tmppage; Page page; - Pointer ptr; + char *ptr; int nrootitems; int rootsize; bool is_build = (buildStats != NULL); @@ -1795,7 +1795,7 @@ createPostingTree(Relation index, ItemPointerData *items, uint32 nitems, */ nrootitems = 0; rootsize = 0; - ptr = (Pointer) GinDataLeafPageGetPostingList(tmppage); + ptr = (char *) GinDataLeafPageGetPostingList(tmppage); while (nrootitems < nitems) { GinPostingList *segment; @@ -1854,10 +1854,10 @@ createPostingTree(Relation index, ItemPointerData *items, uint32 nitems, PageSetLSN(page, recptr); } - UnlockReleaseBuffer(buffer); - END_CRIT_SECTION(); + UnlockReleaseBuffer(buffer); + /* During index build, count the newly-added data page */ if (buildStats) buildStats->nDataPages++; diff --git a/src/backend/access/gin/ginentrypage.c b/src/backend/access/gin/ginentrypage.c index ba6bbd562df0c..f818132eceba2 100644 --- a/src/backend/access/gin/ginentrypage.c +++ b/src/backend/access/gin/ginentrypage.c @@ -4,7 +4,7 @@ * routines for handling GIN entry tree pages. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -171,7 +171,7 @@ ginReadTuple(GinState *ginstate, OffsetNumber attnum, IndexTuple itup, { if (nipd > 0) { - ipd = ginPostingListDecode((GinPostingList *) ptr, &ndecoded); + ipd = ginPostingListDecode(ptr, &ndecoded); if (nipd != ndecoded) elog(ERROR, "number of items mismatch in GIN entry tuple, %d in tuple header, %d decoded", nipd, ndecoded); @@ -183,7 +183,7 @@ ginReadTuple(GinState *ginstate, OffsetNumber attnum, IndexTuple itup, } else { - ipd = (ItemPointer) palloc(sizeof(ItemPointerData) * nipd); + ipd = palloc_array(ItemPointerData, nipd); memcpy(ipd, ptr, sizeof(ItemPointerData) * nipd); } *nitems = nipd; @@ -563,7 +563,7 @@ entryExecPlaceToPage(GinBtree btree, Buffer buf, GinBtreeStack *stack, entryPreparePage(btree, page, off, insertData, updateblkno); placed = PageAddItem(page, - (Item) insertData->entry, + insertData->entry, IndexTupleSize(insertData->entry), off, false, false); if (placed != off) @@ -684,7 +684,7 @@ entrySplitPage(GinBtree btree, Buffer origbuf, lsize += MAXALIGN(IndexTupleSize(itup)) + sizeof(ItemIdData); } - if (PageAddItem(page, (Item) itup, IndexTupleSize(itup), InvalidOffsetNumber, false, false) == InvalidOffsetNumber) + if (PageAddItem(page, itup, IndexTupleSize(itup), InvalidOffsetNumber, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page in \"%s\"", RelationGetRelationName(btree->index)); ptr += MAXALIGN(IndexTupleSize(itup)); @@ -708,7 +708,7 @@ entryPrepareDownlink(GinBtree btree, Buffer lbuf) itup = getRightMostTuple(lpage); - insertData = palloc(sizeof(GinBtreeEntryInsertData)); + insertData = palloc_object(GinBtreeEntryInsertData); insertData->entry = GinFormInteriorTuple(itup, lpage, lblkno); insertData->isDelete = false; @@ -727,12 +727,12 @@ ginEntryFillRoot(GinBtree btree, Page root, IndexTuple itup; itup = GinFormInteriorTuple(getRightMostTuple(lpage), lpage, lblkno); - if (PageAddItem(root, (Item) itup, IndexTupleSize(itup), InvalidOffsetNumber, false, false) == InvalidOffsetNumber) + if (PageAddItem(root, itup, IndexTupleSize(itup), InvalidOffsetNumber, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to index root page"); pfree(itup); itup = GinFormInteriorTuple(getRightMostTuple(rpage), rpage, rblkno); - if (PageAddItem(root, (Item) itup, IndexTupleSize(itup), InvalidOffsetNumber, false, false) == InvalidOffsetNumber) + if (PageAddItem(root, itup, IndexTupleSize(itup), InvalidOffsetNumber, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to index root page"); pfree(itup); } diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c index a6d88572cc29b..f50848eb65a81 100644 --- a/src/backend/access/gin/ginfast.c +++ b/src/backend/access/gin/ginfast.c @@ -7,7 +7,7 @@ * transfer pending entries into the regular index structure. This * wins because bulk insertion is much more efficient than retail. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -57,7 +57,7 @@ typedef struct KeyArray */ static int32 writeListPage(Relation index, Buffer buffer, - IndexTuple *tuples, int32 ntuples, BlockNumber rightlink) + const IndexTuple *tuples, int32 ntuples, BlockNumber rightlink) { Page page = BufferGetPage(buffer); int32 i, @@ -83,7 +83,7 @@ writeListPage(Relation index, Buffer buffer, ptr += this_size; size += this_size; - l = PageAddItem(page, (Item) tuples[i], this_size, off, false, false); + l = PageAddItem(page, tuples[i], this_size, off, false, false); if (l == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page in \"%s\"", @@ -134,10 +134,10 @@ writeListPage(Relation index, Buffer buffer, /* get free space before releasing buffer */ freesize = PageGetExactFreeSpace(page); - UnlockReleaseBuffer(buffer); - END_CRIT_SECTION(); + UnlockReleaseBuffer(buffer); + return freesize; } @@ -384,7 +384,7 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector) for (i = 0; i < collector->ntuples; i++) { tupsize = IndexTupleSize(collector->tuples[i]); - l = PageAddItem(page, (Item) collector->tuples[i], tupsize, off, false, false); + l = PageAddItem(page, collector->tuples[i], tupsize, off, false, false); if (l == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page in \"%s\"", @@ -459,10 +459,10 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector) if (metadata->nPendingPages * GIN_PAGE_FREESIZE > cleanupSize * (Size) 1024) needCleanup = true; - UnlockReleaseBuffer(metabuffer); - END_CRIT_SECTION(); + UnlockReleaseBuffer(metabuffer); + /* * Since it could contend with concurrent cleanup process we cleanup * pending list not forcibly. @@ -659,11 +659,11 @@ shiftList(Relation index, Buffer metabuffer, BlockNumber newHead, } } + END_CRIT_SECTION(); + for (i = 0; i < data.ndeleted; i++) UnlockReleaseBuffer(buffers[i]); - END_CRIT_SECTION(); - for (i = 0; fill_fsm && i < data.ndeleted; i++) RecordFreeIndexPage(index, freespace[i]); diff --git a/src/backend/access/gin/ginget.c b/src/backend/access/gin/ginget.c index f29ccd3c2d1ff..6b148e69a8e14 100644 --- a/src/backend/access/gin/ginget.c +++ b/src/backend/access/gin/ginget.c @@ -4,7 +4,7 @@ * fetch tuples from a GIN scan. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -489,7 +489,7 @@ startScanEntry(GinState *ginstate, GinScanEntry entry, Snapshot snapshot) static int entryIndexByFrequencyCmp(const void *a1, const void *a2, void *arg) { - const GinScanKey key = (const GinScanKey) arg; + const GinScanKeyData *key = arg; int i1 = *(const int *) a1; int i2 = *(const int *) a2; uint32 n1 = key->scanEntry[i1]->predictNumberResult; @@ -552,7 +552,7 @@ startScanKey(GinState *ginstate, GinScanOpaque so, GinScanKey key) { MemoryContextSwitchTo(so->tempCtx); - entryIndexes = (int *) palloc(sizeof(int) * key->nentries); + entryIndexes = palloc_array(int, key->nentries); for (i = 0; i < key->nentries; i++) entryIndexes[i] = i; qsort_arg(entryIndexes, key->nentries, sizeof(int), @@ -1327,6 +1327,8 @@ scanGetItem(IndexScanDesc scan, ItemPointerData advancePast, */ do { + CHECK_FOR_INTERRUPTS(); + ItemPointerSetMin(item); match = true; for (i = 0; i < so->nkeys && match; i++) @@ -1871,7 +1873,7 @@ scanPendingInsert(IndexScanDesc scan, TIDBitmap *tbm, int64 *ntids) LockBuffer(pos.pendingBuffer, GIN_SHARE); pos.firstOffset = FirstOffsetNumber; UnlockReleaseBuffer(metabuffer); - pos.hasMatchKey = palloc(sizeof(bool) * so->nkeys); + pos.hasMatchKey = palloc_array(bool, so->nkeys); /* * loop for each heap row. scanGetCandidate returns full row or row's @@ -1966,8 +1968,6 @@ gingetbitmap(IndexScanDesc scan, TIDBitmap *tbm) for (;;) { - CHECK_FOR_INTERRUPTS(); - if (!scanGetItem(scan, iptr, &iptr, &recheck)) break; diff --git a/src/backend/access/gin/gininsert.c b/src/backend/access/gin/gininsert.c index a65acd8910493..9d83a4957757b 100644 --- a/src/backend/access/gin/gininsert.c +++ b/src/backend/access/gin/gininsert.c @@ -4,7 +4,7 @@ * insert routines for the postgres inverted index access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -23,16 +23,22 @@ #include "catalog/index.h" #include "catalog/pg_collation.h" #include "commands/progress.h" +#include "executor/instrument.h" #include "miscadmin.h" #include "nodes/execnodes.h" #include "pgstat.h" #include "storage/bufmgr.h" +#include "storage/condition_variable.h" +#include "storage/proc.h" #include "storage/predicate.h" #include "tcop/tcopprot.h" #include "utils/datum.h" #include "utils/memutils.h" -#include "utils/rel.h" #include "utils/builtins.h" +#include "utils/rel.h" +#include "utils/tuplesort.h" +#include "utils/typcache.h" +#include "utils/wait_event.h" /* Magic numbers for parallel state sharing */ @@ -152,7 +158,9 @@ typedef struct * only in the leader process. */ GinLeader *bs_leader; - int bs_worker_id; + + /* number of participating workers (including leader) */ + int bs_num_workers; /* used to pass information from workers to leader */ double bs_numtuples; @@ -218,7 +226,8 @@ addItemPointersToLeafTuple(GinState *ginstate, ItemPointerData *newItems, *oldItems; int oldNPosting, - newNPosting; + newNPosting, + nwritten; GinPostingList *compressedList; Assert(!GinIsPostingTree(old)); @@ -235,18 +244,19 @@ addItemPointersToLeafTuple(GinState *ginstate, /* Compress the posting list, and try to a build tuple with room for it */ res = NULL; - compressedList = ginCompressPostingList(newItems, newNPosting, GinMaxItemSize, - NULL); - pfree(newItems); - if (compressedList) + compressedList = ginCompressPostingList(newItems, newNPosting, GinMaxItemSize, &nwritten); + if (nwritten == newNPosting) { res = GinFormTuple(ginstate, attnum, key, category, (char *) compressedList, SizeOfGinPostingList(compressedList), newNPosting, false); - pfree(compressedList); } + + pfree(newItems); + pfree(compressedList); + if (!res) { /* posting list would be too big, convert to posting tree */ @@ -293,17 +303,19 @@ buildFreshLeafTuple(GinState *ginstate, { IndexTuple res = NULL; GinPostingList *compressedList; + int nwritten; /* try to build a posting list tuple with all the items */ - compressedList = ginCompressPostingList(items, nitem, GinMaxItemSize, NULL); - if (compressedList) + compressedList = ginCompressPostingList(items, nitem, GinMaxItemSize, &nwritten); + if (nwritten == nitem) { res = GinFormTuple(ginstate, attnum, key, category, (char *) compressedList, SizeOfGinPostingList(compressedList), nitem, false); - pfree(compressedList); } + pfree(compressedList); + if (!res) { /* posting list would be too big, build posting tree */ @@ -479,6 +491,15 @@ ginBuildCallback(Relation index, ItemPointer tid, Datum *values, /* * ginFlushBuildState * Write all data from BuildAccumulator into the tuplesort. + * + * The number of TIDs written to the tuplesort at once is limited, to reduce + * the amount of memory needed when merging the intermediate results later. + * The leader will see up to two chunks per worker, so calculate the limit to + * not need more than MaxAllocSize overall. + * + * We don't need to worry about overflowing maintenance_work_mem. We can't + * build chunks larger than work_mem, and that limit was set so that workers + * produce sufficiently small chunks. */ static void ginFlushBuildState(GinBuildState *buildstate, Relation index) @@ -489,28 +510,44 @@ ginFlushBuildState(GinBuildState *buildstate, Relation index) uint32 nlist; OffsetNumber attnum; TupleDesc tdesc = RelationGetDescr(index); + uint32 maxlen; + + /* maximum number of TIDs per chunk (two chunks per worker) */ + maxlen = MaxAllocSize / sizeof(ItemPointerData); + maxlen /= (2 * buildstate->bs_num_workers); ginBeginBAScan(&buildstate->accum); while ((list = ginGetBAEntry(&buildstate->accum, &attnum, &key, &category, &nlist)) != NULL) { /* information about the key */ - Form_pg_attribute attr = TupleDescAttr(tdesc, (attnum - 1)); + CompactAttribute *attr = TupleDescCompactAttr(tdesc, (attnum - 1)); - /* GIN tuple and tuple length */ - GinTuple *tup; - Size tuplen; + /* start of the chunk */ + uint32 offset = 0; - /* there could be many entries, so be willing to abort here */ - CHECK_FOR_INTERRUPTS(); + /* split the entry into smaller chunk with up to maxlen items */ + while (offset < nlist) + { + /* GIN tuple and tuple length */ + GinTuple *tup; + Size tuplen; + uint32 len = Min(maxlen, nlist - offset); + + /* there could be many entries, so be willing to abort here */ + CHECK_FOR_INTERRUPTS(); + + tup = _gin_build_tuple(attnum, category, + key, attr->attlen, attr->attbyval, + &list[offset], len, + &tuplen); - tup = _gin_build_tuple(attnum, category, - key, attr->attlen, attr->attbyval, - list, nlist, &tuplen); + offset += len; - tuplesort_putgintuple(buildstate->bs_worker_sort, tup, tuplen); + tuplesort_putgintuple(buildstate->bs_worker_sort, tup, tuplen); - pfree(tup); + pfree(tup); + } } MemoryContextReset(buildstate->tmpCtx); @@ -676,7 +713,7 @@ ginbuild(Relation heap, Relation index, IndexInfo *indexInfo) { SortCoordinate coordinate; - coordinate = (SortCoordinate) palloc0(sizeof(SortCoordinateData)); + coordinate = palloc0_object(SortCoordinateData); coordinate->isWorker = false; coordinate->nParticipants = state->bs_leader->nparticipanttuplesorts; @@ -760,7 +797,7 @@ ginbuild(Relation heap, Relation index, IndexInfo *indexInfo) /* * Return statistics */ - result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + result = palloc_object(IndexBuildResult); result->heap_tuples = reltuples; result->index_tuples = buildstate.indtuples; @@ -816,8 +853,12 @@ ginHeapTupleInsert(GinState *ginstate, OffsetNumber attnum, &nentries, &categories); for (i = 0; i < nentries; i++) + { + /* there could be many entries, so be willing to abort here */ + CHECK_FOR_INTERRUPTS(); ginEntryInsert(ginstate, attnum, entries[i], categories[i], item, 1, NULL); + } } bool @@ -836,7 +877,7 @@ gininsert(Relation index, Datum *values, bool *isnull, if (ginstate == NULL) { oldCtx = MemoryContextSwitchTo(indexInfo->ii_Context); - ginstate = (GinState *) palloc(sizeof(GinState)); + ginstate = palloc_object(GinState); initGinState(ginstate, index); indexInfo->ii_AmCache = ginstate; MemoryContextSwitchTo(oldCtx); @@ -903,7 +944,7 @@ _gin_begin_parallel(GinBuildState *buildstate, Relation heap, Relation index, Size estsort; GinBuildShared *ginshared; Sharedsort *sharedsort; - GinLeader *ginleader = (GinLeader *) palloc0(sizeof(GinLeader)); + GinLeader *ginleader = palloc0_object(GinLeader); WalUsage *walusage; BufferUsage *bufferusage; bool leaderparticipates = true; @@ -1228,7 +1269,7 @@ AssertCheckGinBuffer(GinBuffer *buffer) static GinBuffer * GinBufferInit(Relation index) { - GinBuffer *buffer = palloc0(sizeof(GinBuffer)); + GinBuffer *buffer = palloc0_object(GinBuffer); int i, nKeys; TupleDesc desc = RelationGetDescr(index); @@ -1242,7 +1283,7 @@ GinBufferInit(Relation index) nKeys = IndexRelationGetNumberOfKeyAttributes(index); - buffer->ssup = palloc0(sizeof(SortSupportData) * nKeys); + buffer->ssup = palloc0_array(SortSupportData, nKeys); /* * Lookup ordering operator for the index key data type, and initialize @@ -1370,10 +1411,46 @@ GinBufferKeyEquals(GinBuffer *buffer, GinTuple *tup) * enough TIDs to trim (with values less than "first" TID from the new tuple), * we do the trim. By enough we mean at least 128 TIDs (mostly an arbitrary * number). + * + * We try freezing TIDs at the beginning of the list first, before attempting + * to trim the buffer. This may allow trimming the data earlier, reducing the + * memory usage and excluding it from the mergesort. */ static bool GinBufferShouldTrim(GinBuffer *buffer, GinTuple *tup) { + /* + * Check if the last TID in the current list is frozen. This is the case + * when merging non-overlapping lists, e.g. in each parallel worker. + */ + if ((buffer->nitems > 0) && + (ItemPointerCompare(&buffer->items[buffer->nitems - 1], + GinTupleGetFirst(tup)) == 0)) + buffer->nfrozen = buffer->nitems; + + /* + * Now find the last TID we know to be frozen, i.e. the last TID right + * before the new GIN tuple. + * + * Start with the first not-yet-frozen tuple, and walk until we find the + * first TID that's higher. If we already know the whole list is frozen + * (i.e. nfrozen == nitems), this does nothing. + * + * XXX This might do a binary search for sufficiently long lists, but it + * does not seem worth the complexity. Overlapping lists should be rare + * common, TID comparisons are cheap, and we should quickly freeze most of + * the list. + */ + for (int i = buffer->nfrozen; i < buffer->nitems; i++) + { + /* Is the TID after the first TID of the new tuple? Can't freeze. */ + if (ItemPointerCompare(&buffer->items[i], + GinTupleGetFirst(tup)) > 0) + break; + + buffer->nfrozen++; + } + /* not enough TIDs to trim (1024 is somewhat arbitrary number) */ if (buffer->nfrozen < 1024) return false; @@ -1439,48 +1516,6 @@ GinBufferStoreTuple(GinBuffer *buffer, GinTuple *tup) buffer->key = (Datum) 0; } - /* - * Try freeze TIDs at the beginning of the list, i.e. exclude them from - * the mergesort. We can do that with TIDs before the first TID in the new - * tuple we're about to add into the buffer. - * - * We do this incrementally when adding data into the in-memory buffer, - * and not later (e.g. when hitting a memory limit), because it allows us - * to skip the frozen data during the mergesort, making it cheaper. - */ - - /* - * Check if the last TID in the current list is frozen. This is the case - * when merging non-overlapping lists, e.g. in each parallel worker. - */ - if ((buffer->nitems > 0) && - (ItemPointerCompare(&buffer->items[buffer->nitems - 1], - GinTupleGetFirst(tup)) == 0)) - buffer->nfrozen = buffer->nitems; - - /* - * Now find the last TID we know to be frozen, i.e. the last TID right - * before the new GIN tuple. - * - * Start with the first not-yet-frozen tuple, and walk until we find the - * first TID that's higher. If we already know the whole list is frozen - * (i.e. nfrozen == nitems), this does nothing. - * - * XXX This might do a binary search for sufficiently long lists, but it - * does not seem worth the complexity. Overlapping lists should be rare - * common, TID comparisons are cheap, and we should quickly freeze most of - * the list. - */ - for (int i = buffer->nfrozen; i < buffer->nitems; i++) - { - /* Is the TID after the first TID of the new tuple? Can't freeze. */ - if (ItemPointerCompare(&buffer->items[i], - GinTupleGetFirst(tup)) > 0) - break; - - buffer->nfrozen++; - } - /* add the new TIDs into the buffer, combine using merge-sort */ { int nnew; @@ -1759,7 +1794,7 @@ _gin_parallel_merge(GinBuildState *state) ++numtuples); } - /* relase all the memory */ + /* release all the memory */ GinBufferFree(buffer); tuplesort_end(state->bs_sortstate); @@ -1947,7 +1982,7 @@ _gin_process_worker_data(GinBuildState *state, Tuplesortstate *worker_sort, GinBufferReset(buffer); } - /* relase all the memory */ + /* release all the memory */ GinBufferFree(buffer); tuplesort_end(worker_sort); @@ -2005,7 +2040,7 @@ _gin_parallel_scan_and_build(GinBuildState *state, IndexInfo *indexInfo; /* Initialize local tuplesort coordination state */ - coordinate = palloc0(sizeof(SortCoordinateData)); + coordinate = palloc0_object(SortCoordinateData); coordinate->isWorker = true; coordinate->nParticipants = -1; coordinate->sharedsort = sharedsort; @@ -2013,6 +2048,9 @@ _gin_parallel_scan_and_build(GinBuildState *state, /* remember how much space is allowed for the accumulated entries */ state->work_mem = (sortmem / 2); + /* remember how many workers participate in the build */ + state->bs_num_workers = ginshared->scantuplesortstates; + /* Begin "partial" tuplesort */ state->bs_sortstate = tuplesort_begin_index_gin(heap, index, state->work_mem, @@ -2030,7 +2068,8 @@ _gin_parallel_scan_and_build(GinBuildState *state, indexInfo->ii_Concurrent = ginshared->isconcurrent; scan = table_beginscan_parallel(heap, - ParallelTableScanFromGinBuildShared(ginshared)); + ParallelTableScanFromGinBuildShared(ginshared), + SO_NONE); reltuples = table_index_build_scan(heap, index, indexInfo, true, progress, ginBuildCallbackParallel, state, scan); @@ -2187,9 +2226,12 @@ typedef struct * * For by-reference data types, we store the actual data. For by-val types * we simply copy the whole Datum, so that we don't have to care about stuff - * like endianess etc. We could make it a little bit smaller, but it's not + * like endianness etc. We could make it a little bit smaller, but it's not * worth it - it's a tiny fraction of the data, and we need to MAXALIGN the - * start of the TID list anyway. So we wouldn't save anything. + * start of the TID list anyway. So we wouldn't save anything. (This would + * not be a good idea for the permanent in-index data, since we'd prefer + * that that not depend on sizeof(Datum). But this is just a transient + * representation to use while sorting the data.) * * The TID list is serialized as compressed - it's highly compressible, and * we already have ginCompressPostingList for this purpose. The list may be @@ -2233,7 +2275,7 @@ _gin_build_tuple(OffsetNumber attrnum, unsigned char category, else if (typlen > 0) keylen = typlen; else if (typlen == -1) - keylen = VARSIZE_ANY(key); + keylen = VARSIZE_ANY(DatumGetPointer(key)); else if (typlen == -2) keylen = strlen(DatumGetPointer(key)) + 1; else @@ -2248,7 +2290,7 @@ _gin_build_tuple(OffsetNumber attrnum, unsigned char category, while (ncompressed < nitems) { int cnt; - GinSegmentInfo *seginfo = palloc(sizeof(GinSegmentInfo)); + GinSegmentInfo *seginfo = palloc_object(GinSegmentInfo); seginfo->seg = ginCompressPostingList(&items[ncompressed], (nitems - ncompressed), @@ -2381,7 +2423,7 @@ _gin_parse_tuple_items(GinTuple *a) Assert(ndecoded == a->nitems); - return (ItemPointer) items; + return items; } /* diff --git a/src/backend/access/gin/ginlogic.c b/src/backend/access/gin/ginlogic.c index ff456247cefac..e851d49f74deb 100644 --- a/src/backend/access/gin/ginlogic.c +++ b/src/backend/access/gin/ginlogic.c @@ -24,7 +24,7 @@ * is used for.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/gin/ginpostinglist.c b/src/backend/access/gin/ginpostinglist.c index 48eadec87b0b1..d8dfcee4bde4c 100644 --- a/src/backend/access/gin/ginpostinglist.c +++ b/src/backend/access/gin/ginpostinglist.c @@ -4,7 +4,7 @@ * routines for dealing with posting lists. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -84,7 +84,7 @@ #define MaxBytesPerInteger 7 static inline uint64 -itemptr_to_uint64(const ItemPointer iptr) +itemptr_to_uint64(const ItemPointerData *iptr) { uint64 val; @@ -194,7 +194,7 @@ decode_varbyte(unsigned char **ptr) * byte at the end, if any, is zero. */ GinPostingList * -ginCompressPostingList(const ItemPointer ipd, int nipd, int maxsize, +ginCompressPostingList(const ItemPointerData *ipd, int nipd, int maxsize, int *nwritten) { uint64 prev; diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c index c2d1771bd77b5..fb929761ab771 100644 --- a/src/backend/access/gin/ginscan.c +++ b/src/backend/access/gin/ginscan.c @@ -4,7 +4,7 @@ * routines to manage scans of inverted index relations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -16,6 +16,7 @@ #include "access/gin_private.h" #include "access/relscan.h" +#include "executor/instrument_node.h" #include "pgstat.h" #include "utils/memutils.h" #include "utils/rel.h" @@ -33,7 +34,7 @@ ginbeginscan(Relation rel, int nkeys, int norderbys) scan = RelationGetIndexScan(rel, nkeys, norderbys); /* allocate private workspace */ - so = (GinScanOpaque) palloc(sizeof(GinScanOpaqueData)); + so = (GinScanOpaque) palloc_object(GinScanOpaqueData); so->keys = NULL; so->nkeys = 0; so->tempCtx = AllocSetContextCreate(CurrentMemoryContext, @@ -98,7 +99,7 @@ ginFillScanEntry(GinScanOpaque so, OffsetNumber attnum, } /* Nope, create a new entry */ - scanEntry = (GinScanEntry) palloc(sizeof(GinScanEntryData)); + scanEntry = palloc_object(GinScanEntryData); scanEntry->queryKey = queryKey; scanEntry->queryCategory = queryCategory; scanEntry->isPartialMatch = isPartialMatch; @@ -123,8 +124,7 @@ ginFillScanEntry(GinScanOpaque so, OffsetNumber attnum, if (so->totalentries >= so->allocentries) { so->allocentries *= 2; - so->entries = (GinScanEntry *) - repalloc(so->entries, so->allocentries * sizeof(GinScanEntry)); + so->entries = repalloc_array(so->entries, GinScanEntry, so->allocentries); } so->entries[so->totalentries++] = scanEntry; @@ -170,10 +170,8 @@ ginFillScanKey(GinScanOpaque so, OffsetNumber attnum, key->nuserentries = nQueryValues; /* Allocate one extra array slot for possible "hidden" entry */ - key->scanEntry = (GinScanEntry *) palloc(sizeof(GinScanEntry) * - (nQueryValues + 1)); - key->entryRes = (GinTernaryValue *) palloc0(sizeof(GinTernaryValue) * - (nQueryValues + 1)); + key->scanEntry = palloc_array(GinScanEntry, nQueryValues + 1); + key->entryRes = palloc0_array(GinTernaryValue, nQueryValues + 1); key->query = query; key->queryValues = queryValues; @@ -271,6 +269,7 @@ ginNewScanKey(IndexScanDesc scan) ScanKey scankey = scan->keyData; GinScanOpaque so = (GinScanOpaque) scan->opaque; int i; + int numExcludeOnly; bool hasNullQuery = false; bool attrHasNormalScan[INDEX_MAX_KEYS] = {false}; MemoryContext oldCtx; @@ -393,6 +392,7 @@ ginNewScanKey(IndexScanDesc scan) * excludeOnly scan key must receive a GIN_CAT_EMPTY_QUERY hidden entry * and be set to normal (excludeOnly = false). */ + numExcludeOnly = 0; for (i = 0; i < so->nkeys; i++) { GinScanKey key = &so->keys[i]; @@ -406,6 +406,47 @@ ginNewScanKey(IndexScanDesc scan) ginScanKeyAddHiddenEntry(so, key, GIN_CAT_EMPTY_QUERY); attrHasNormalScan[key->attnum - 1] = true; } + else + numExcludeOnly++; + } + + /* + * If we left any excludeOnly scan keys as-is, move them to the end of the + * scan key array: they must appear after normal key(s). + */ + if (numExcludeOnly > 0) + { + GinScanKey tmpkeys; + int iNormalKey; + int iExcludeOnly; + + /* We'd better have made at least one normal key */ + Assert(numExcludeOnly < so->nkeys); + /* Make a temporary array to hold the re-ordered scan keys */ + tmpkeys = (GinScanKey) palloc(so->nkeys * sizeof(GinScanKeyData)); + /* Re-order the keys ... */ + iNormalKey = 0; + iExcludeOnly = so->nkeys - numExcludeOnly; + for (i = 0; i < so->nkeys; i++) + { + GinScanKey key = &so->keys[i]; + + if (key->excludeOnly) + { + memcpy(tmpkeys + iExcludeOnly, key, sizeof(GinScanKeyData)); + iExcludeOnly++; + } + else + { + memcpy(tmpkeys + iNormalKey, key, sizeof(GinScanKeyData)); + iNormalKey++; + } + } + Assert(iNormalKey == so->nkeys - numExcludeOnly); + Assert(iExcludeOnly == so->nkeys); + /* ... and copy them back to so->keys[] */ + memcpy(so->keys, tmpkeys, so->nkeys * sizeof(GinScanKeyData)); + pfree(tmpkeys); } /* diff --git a/src/backend/access/gin/ginutil.c b/src/backend/access/gin/ginutil.c index 78f7b7a2495cf..e7cba81d47709 100644 --- a/src/backend/access/gin/ginutil.c +++ b/src/backend/access/gin/ginutil.c @@ -4,7 +4,7 @@ * Utility routines for the Postgres inverted index access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -28,6 +28,7 @@ #include "utils/index_selfuncs.h" #include "utils/rel.h" #include "utils/typcache.h" +#include "lib/qunique.h" /* @@ -37,60 +38,61 @@ Datum ginhandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = 0; - amroutine->amsupport = GINNProcs; - amroutine->amoptsprocnum = GIN_OPTIONS_PROC; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = false; - amroutine->amcanhash = false; - amroutine->amconsistentequality = false; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = false; - amroutine->amcanunique = false; - amroutine->amcanmulticol = true; - amroutine->amoptionalkey = true; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = false; - amroutine->amstorage = true; - amroutine->amclusterable = false; - amroutine->ampredlocks = true; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = true; - amroutine->amcaninclude = false; - amroutine->amusemaintenanceworkmem = true; - amroutine->amsummarizing = false; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = ginbuild; - amroutine->ambuildempty = ginbuildempty; - amroutine->aminsert = gininsert; - amroutine->aminsertcleanup = NULL; - amroutine->ambulkdelete = ginbulkdelete; - amroutine->amvacuumcleanup = ginvacuumcleanup; - amroutine->amcanreturn = NULL; - amroutine->amcostestimate = gincostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = ginoptions; - amroutine->amproperty = NULL; - amroutine->ambuildphasename = ginbuildphasename; - amroutine->amvalidate = ginvalidate; - amroutine->amadjustmembers = ginadjustmembers; - amroutine->ambeginscan = ginbeginscan; - amroutine->amrescan = ginrescan; - amroutine->amgettuple = NULL; - amroutine->amgetbitmap = gingetbitmap; - amroutine->amendscan = ginendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = 0, + .amsupport = GINNProcs, + .amoptsprocnum = GIN_OPTIONS_PROC, + .amcanorder = false, + .amcanorderbyop = false, + .amcanhash = false, + .amconsistentequality = false, + .amconsistentordering = false, + .amcanbackward = false, + .amcanunique = false, + .amcanmulticol = true, + .amoptionalkey = true, + .amsearcharray = false, + .amsearchnulls = false, + .amstorage = true, + .amclusterable = false, + .ampredlocks = true, + .amcanparallel = false, + .amcanbuildparallel = true, + .amcaninclude = false, + .amusemaintenanceworkmem = true, + .amsummarizing = false, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_CLEANUP, + .amkeytype = InvalidOid, + + .ambuild = ginbuild, + .ambuildempty = ginbuildempty, + .aminsert = gininsert, + .aminsertcleanup = NULL, + .ambulkdelete = ginbulkdelete, + .amvacuumcleanup = ginvacuumcleanup, + .amcanreturn = NULL, + .amcostestimate = gincostestimate, + .amgettreeheight = NULL, + .amoptions = ginoptions, + .amproperty = NULL, + .ambuildphasename = ginbuildphasename, + .amvalidate = ginvalidate, + .amadjustmembers = ginadjustmembers, + .ambeginscan = ginbeginscan, + .amrescan = ginrescan, + .amgettuple = NULL, + .amgetbitmap = gingetbitmap, + .amendscan = ginendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -128,6 +130,7 @@ initGinState(GinState *state, Relation index) attr->attndims); TupleDescInitEntryCollation(state->tupdesc[i], (AttrNumber) 2, attr->attcollation); + TupleDescFinalize(state->tupdesc[i]); } /* @@ -387,56 +390,9 @@ GinInitMetabuffer(Buffer b) } /* - * Compare two keys of the same index column - */ -int -ginCompareEntries(GinState *ginstate, OffsetNumber attnum, - Datum a, GinNullCategory categorya, - Datum b, GinNullCategory categoryb) -{ - /* if not of same null category, sort by that first */ - if (categorya != categoryb) - return (categorya < categoryb) ? -1 : 1; - - /* all null items in same category are equal */ - if (categorya != GIN_CAT_NORM_KEY) - return 0; - - /* both not null, so safe to call the compareFn */ - return DatumGetInt32(FunctionCall2Coll(&ginstate->compareFn[attnum - 1], - ginstate->supportCollation[attnum - 1], - a, b)); -} - -/* - * Compare two keys of possibly different index columns - */ -int -ginCompareAttEntries(GinState *ginstate, - OffsetNumber attnuma, Datum a, GinNullCategory categorya, - OffsetNumber attnumb, Datum b, GinNullCategory categoryb) -{ - /* attribute number is the first sort key */ - if (attnuma != attnumb) - return (attnuma < attnumb) ? -1 : 1; - - return ginCompareEntries(ginstate, attnuma, a, categorya, b, categoryb); -} - - -/* - * Support for sorting key datums in ginExtractEntries - * - * Note: we only have to worry about null and not-null keys here; - * ginExtractEntries never generates more than one placeholder null, - * so it doesn't have to sort those. + * Support for sorting key datums and detecting duplicates in + * ginExtractEntries */ -typedef struct -{ - Datum datum; - bool isnull; -} keyEntryData; - typedef struct { FmgrInfo *cmpDatumFunc; @@ -447,24 +403,14 @@ typedef struct static int cmpEntries(const void *a, const void *b, void *arg) { - const keyEntryData *aa = (const keyEntryData *) a; - const keyEntryData *bb = (const keyEntryData *) b; + const Datum *aa = (const Datum *) a; + const Datum *bb = (const Datum *) b; cmpEntriesArg *data = (cmpEntriesArg *) arg; int res; - if (aa->isnull) - { - if (bb->isnull) - res = 0; /* NULL "=" NULL */ - else - res = 1; /* NULL ">" not-NULL */ - } - else if (bb->isnull) - res = -1; /* not-NULL "<" NULL */ - else - res = DatumGetInt32(FunctionCall2Coll(data->cmpDatumFunc, - data->collation, - aa->datum, bb->datum)); + res = DatumGetInt32(FunctionCall2Coll(data->cmpDatumFunc, + data->collation, + *aa, *bb)); /* * Detect if we have any duplicates. If there are equal keys, qsort must @@ -477,6 +423,14 @@ cmpEntries(const void *a, const void *b, void *arg) return res; } +#define ST_SORT qsort_arg_entries +#define ST_ELEMENT_TYPE Datum +#define ST_COMPARE_ARG_TYPE cmpEntriesArg +#define ST_COMPARE(a, b, arg) cmpEntries(a, b, arg) +#define ST_SCOPE static +#define ST_DEFINE +#define ST_DECLARE +#include "lib/sort_template.h" /* * Extract the index key values from an indexable item @@ -487,11 +441,13 @@ cmpEntries(const void *a, const void *b, void *arg) Datum * ginExtractEntries(GinState *ginstate, OffsetNumber attnum, Datum value, bool isNull, - int32 *nentries, GinNullCategory **categories) + int32 *nentries_p, GinNullCategory **categories_p) { Datum *entries; bool *nullFlags; - int32 i; + GinNullCategory *categories; + bool hasNull; + int32 nentries; /* * We don't call the extractValueFn on a null item. Instead generate a @@ -499,42 +455,60 @@ ginExtractEntries(GinState *ginstate, OffsetNumber attnum, */ if (isNull) { - *nentries = 1; - entries = (Datum *) palloc(sizeof(Datum)); + *nentries_p = 1; + entries = palloc_object(Datum); entries[0] = (Datum) 0; - *categories = (GinNullCategory *) palloc(sizeof(GinNullCategory)); - (*categories)[0] = GIN_CAT_NULL_ITEM; + *categories_p = palloc_object(GinNullCategory); + (*categories_p)[0] = GIN_CAT_NULL_ITEM; return entries; } /* OK, call the opclass's extractValueFn */ nullFlags = NULL; /* in case extractValue doesn't set it */ + nentries = 0; entries = (Datum *) DatumGetPointer(FunctionCall3Coll(&ginstate->extractValueFn[attnum - 1], ginstate->supportCollation[attnum - 1], value, - PointerGetDatum(nentries), + PointerGetDatum(&nentries), PointerGetDatum(&nullFlags))); /* * Generate a placeholder if the item contained no keys. */ - if (entries == NULL || *nentries <= 0) + if (entries == NULL || nentries <= 0) { - *nentries = 1; - entries = (Datum *) palloc(sizeof(Datum)); + *nentries_p = 1; + entries = palloc_object(Datum); entries[0] = (Datum) 0; - *categories = (GinNullCategory *) palloc(sizeof(GinNullCategory)); - (*categories)[0] = GIN_CAT_EMPTY_ITEM; + *categories_p = palloc_object(GinNullCategory); + (*categories_p)[0] = GIN_CAT_EMPTY_ITEM; return entries; } /* - * If the extractValueFn didn't create a nullFlags array, create one, - * assuming that everything's non-null. + * Scan the items for any NULLs. All NULLs are considered equal, so we + * just need to check and remember if there are any. We remove them from + * the array here, and after deduplication, put back one NULL entry to + * represent them all. */ - if (nullFlags == NULL) - nullFlags = (bool *) palloc0(*nentries * sizeof(bool)); + hasNull = false; + if (nullFlags) + { + int32 numNonNulls = 0; + + for (int32 i = 0; i < nentries; i++) + { + if (nullFlags[i]) + hasNull = true; + else + { + entries[numNonNulls] = entries[i]; + numNonNulls++; + } + } + nentries = numNonNulls; + } /* * If there's more than one key, sort and unique-ify. @@ -543,63 +517,39 @@ ginExtractEntries(GinState *ginstate, OffsetNumber attnum, * pretty bad too. For small numbers of keys it'd likely be better to use * a simple insertion sort. */ - if (*nentries > 1) + if (nentries > 1) { - keyEntryData *keydata; cmpEntriesArg arg; - keydata = (keyEntryData *) palloc(*nentries * sizeof(keyEntryData)); - for (i = 0; i < *nentries; i++) - { - keydata[i].datum = entries[i]; - keydata[i].isnull = nullFlags[i]; - } - arg.cmpDatumFunc = &ginstate->compareFn[attnum - 1]; arg.collation = ginstate->supportCollation[attnum - 1]; arg.haveDups = false; - qsort_arg(keydata, *nentries, sizeof(keyEntryData), - cmpEntries, &arg); - if (arg.haveDups) - { - /* there are duplicates, must get rid of 'em */ - int32 j; + qsort_arg_entries(entries, nentries, &arg); - entries[0] = keydata[0].datum; - nullFlags[0] = keydata[0].isnull; - j = 1; - for (i = 1; i < *nentries; i++) - { - if (cmpEntries(&keydata[i - 1], &keydata[i], &arg) != 0) - { - entries[j] = keydata[i].datum; - nullFlags[j] = keydata[i].isnull; - j++; - } - } - *nentries = j; - } - else - { - /* easy, no duplicates */ - for (i = 0; i < *nentries; i++) - { - entries[i] = keydata[i].datum; - nullFlags[i] = keydata[i].isnull; - } - } - - pfree(keydata); + if (arg.haveDups) + nentries = qunique_arg(entries, nentries, sizeof(Datum), cmpEntries, &arg); } /* - * Create GinNullCategory representation from nullFlags. + * Create GinNullCategory representation. */ - *categories = (GinNullCategory *) palloc0(*nentries * sizeof(GinNullCategory)); - for (i = 0; i < *nentries; i++) - (*categories)[i] = (nullFlags[i] ? GIN_CAT_NULL_KEY : GIN_CAT_NORM_KEY); + { + /* palloc0 sets all entries to GIN_CAT_NORM_KEY */ + StaticAssertDecl(GIN_CAT_NORM_KEY == 0, "Assuming GIN_CAT_NORM_KEY == 0"); + categories = palloc0_array(GinNullCategory, nentries + (hasNull ? 1 : 0)); + } + /* Put back a NULL entry, if there were any */ + if (hasNull) + { + entries[nentries] = (Datum) 0; + categories[nentries] = GIN_CAT_NULL_KEY; + nentries++; + } + + *nentries_p = nentries; + *categories_p = categories; return entries; } @@ -700,9 +650,9 @@ ginUpdateStats(Relation index, const GinStatsData *stats, bool is_build) PageSetLSN(metapage, recptr); } - UnlockReleaseBuffer(metabuffer); - END_CRIT_SECTION(); + + UnlockReleaseBuffer(metabuffer); } /* diff --git a/src/backend/access/gin/ginvacuum.c b/src/backend/access/gin/ginvacuum.c index fbbe3a6dd7046..840543eb6642b 100644 --- a/src/backend/access/gin/ginvacuum.c +++ b/src/backend/access/gin/ginvacuum.c @@ -4,7 +4,7 @@ * delete & vacuum routines for the postgres GIN * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,6 +22,7 @@ #include "storage/indexfsm.h" #include "storage/lmgr.h" #include "storage/predicate.h" +#include "storage/read_stream.h" #include "utils/memutils.h" struct GinVacuumState @@ -65,7 +66,7 @@ ginVacuumItemPointers(GinVacuumState *gvs, ItemPointerData *items, * First TID to be deleted: allocate memory to hold the * remaining items. */ - tmpitems = palloc(sizeof(ItemPointerData) * nitem); + tmpitems = palloc_array(ItemPointerData, nitem); memcpy(tmpitems, items, sizeof(ItemPointerData) * i); } } @@ -110,31 +111,51 @@ xlogVacuumPage(Relation index, Buffer buffer) } +/* + * Stack entry used during posting tree empty-page deletion scan. + * + * One DataPageDeleteStack entry is allocated per tree level. As + * ginScanPostingTreeToDelete() recurses down the tree, each entry tracks + * the buffer of the page currently being visited at that level ('buffer'), + * and the buffer of its left sibling ('leftBuffer'). The left page is kept + * pinned and exclusively locked because ginDeletePostingPage() needs it to + * update the sibling chain; acquiring it later could deadlock with + * ginStepRight(), which locks pages left-to-right. + */ typedef struct DataPageDeleteStack { struct DataPageDeleteStack *child; struct DataPageDeleteStack *parent; - BlockNumber blkno; /* current block number */ - Buffer leftBuffer; /* pinned and locked rightest non-deleted page - * on left */ + Buffer buffer; /* buffer for the page being visited at this + * tree level */ + Buffer leftBuffer; /* pinned and locked rightmost non-deleted + * sibling to the left of the current page */ + OffsetNumber myoff; /* offset of this page's downlink in the + * parent */ bool isRoot; } DataPageDeleteStack; /* * Delete a posting tree page. + * + * Removes the page identified by dBuffer from the posting tree by updating + * the left sibling's rightlink (in lBuffer) to skip over the deleted page, + * and removing the downlink from the parent page (in pBuffer). All three + * buffers must already have been pinned and exclusively locked by the caller. + * + * The buffers are NOT released nor unlocked here; the caller is responsible + * for this. */ static void -ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkno, - BlockNumber parentBlkno, OffsetNumber myoff, bool isParentRoot) +ginDeletePostingPage(GinVacuumState *gvs, Buffer dBuffer, Buffer lBuffer, + Buffer pBuffer, OffsetNumber myoff, bool isParentRoot) { - Buffer dBuffer; - Buffer lBuffer; - Buffer pBuffer; Page page, parentPage; BlockNumber rightlink; + BlockNumber deleteBlkno = BufferGetBlockNumber(dBuffer); /* * This function MUST be called only if someone of parent pages hold @@ -142,12 +163,6 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn * happen in this subtree. Caller also acquires Exclusive locks on * deletable, parent and left pages. */ - lBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, leftBlkno, - RBM_NORMAL, gvs->strategy); - dBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, deleteBlkno, - RBM_NORMAL, gvs->strategy); - pBuffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, parentBlkno, - RBM_NORMAL, gvs->strategy); page = BufferGetPage(dBuffer); rightlink = GinPageGetOpaque(page)->rightlink; @@ -224,10 +239,6 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn PageSetLSN(BufferGetPage(lBuffer), recptr); } - ReleaseBuffer(pBuffer); - ReleaseBuffer(lBuffer); - ReleaseBuffer(dBuffer); - END_CRIT_SECTION(); gvs->result->pages_newly_deleted++; @@ -236,45 +247,32 @@ ginDeletePage(GinVacuumState *gvs, BlockNumber deleteBlkno, BlockNumber leftBlkn /* - * Scans posting tree and deletes empty pages. Caller must lock root page for - * cleanup. During scan path from root to current page is kept exclusively - * locked. Also keep left page exclusively locked, because ginDeletePage() - * needs it. If we try to relock left page later, it could deadlock with - * ginStepRight(). + * Scans a posting tree and deletes empty pages. + * + * The caller must hold a cleanup lock on the root page to prevent concurrent + * inserts. The entire path from the root down to the current page is kept + * exclusively locked throughout the scan. The left sibling at each level is + * also kept locked, because ginDeletePostingPage() needs it to update the + * rightlink of the left sibling; re-acquiring the left sibling lock later + * could deadlock with ginStepRight(), which acquires page locks + * left-to-right. + * + * All per-level state is carried in 'myStackItem': the buffer to process + * (must already be pinned and exclusively locked), the left sibling buffer, + * and this page's offset in the parent's downlink array. The root entry is + * set up by ginVacuumPostingTree(); child entries are populated here before + * recursing. + * + * Returns true if the page was deleted, false otherwise. */ static bool -ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot, - DataPageDeleteStack *parent, OffsetNumber myoff) +ginScanPostingTreeToDelete(GinVacuumState *gvs, DataPageDeleteStack *myStackItem) { - DataPageDeleteStack *me; - Buffer buffer; + Buffer buffer = myStackItem->buffer; Page page; - bool meDelete = false; + bool pageWasDeleted = false; bool isempty; - if (isRoot) - { - me = parent; - } - else - { - if (!parent->child) - { - me = (DataPageDeleteStack *) palloc0(sizeof(DataPageDeleteStack)); - me->parent = parent; - parent->child = me; - me->leftBuffer = InvalidBuffer; - } - else - me = parent->child; - } - - buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, blkno, - RBM_NORMAL, gvs->strategy); - - if (!isRoot) - LockBuffer(buffer, GIN_EXCLUSIVE); - page = BufferGetPage(buffer); Assert(GinPageIsData(page)); @@ -283,19 +281,48 @@ ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot, { OffsetNumber i; - me->blkno = blkno; - for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff; i++) + for (i = FirstOffsetNumber; i <= GinPageGetOpaque(page)->maxoff;) { PostingItem *pitem = GinDataPageGetPostingItem(page, i); + Buffer childBuffer; + + childBuffer = ReadBufferExtended(gvs->index, + MAIN_FORKNUM, + PostingItemGetBlockNumber(pitem), + RBM_NORMAL, gvs->strategy); + LockBuffer(childBuffer, GIN_EXCLUSIVE); + + /* Allocate a child stack entry on first use; reuse thereafter */ + if (!myStackItem->child) + { + myStackItem->child = palloc0_object(DataPageDeleteStack); + myStackItem->child->parent = myStackItem; + myStackItem->child->leftBuffer = InvalidBuffer; + } - if (ginScanToDelete(gvs, PostingItemGetBlockNumber(pitem), false, me, i)) - i--; + myStackItem->child->buffer = childBuffer; + myStackItem->child->isRoot = false; + myStackItem->child->myoff = i; + + /* + * Recurse into child. If the child page was deleted, its + * downlink was removed from our page, so re-examine the same + * offset; otherwise advance to the next downlink. + */ + if (!ginScanPostingTreeToDelete(gvs, myStackItem->child)) + i++; } + myStackItem->buffer = InvalidBuffer; - if (GinPageRightMost(page) && BufferIsValid(me->child->leftBuffer)) + /* + * After processing all children at this level, release the child + * level's leftBuffer if we're at the rightmost page. There is no + * right sibling that could need it for deletion. + */ + if (GinPageRightMost(page) && BufferIsValid(myStackItem->child->leftBuffer)) { - UnlockReleaseBuffer(me->child->leftBuffer); - me->child->leftBuffer = InvalidBuffer; + UnlockReleaseBuffer(myStackItem->child->leftBuffer); + myStackItem->child->leftBuffer = InvalidBuffer; } } @@ -306,34 +333,41 @@ ginScanToDelete(GinVacuumState *gvs, BlockNumber blkno, bool isRoot, if (isempty) { - /* we never delete the left- or rightmost branch */ - if (BufferIsValid(me->leftBuffer) && !GinPageRightMost(page)) + /* + * Proceed to the ginDeletePostingPage() if that's not the leftmost or + * the rightmost page. + */ + if (BufferIsValid(myStackItem->leftBuffer) && !GinPageRightMost(page)) { - Assert(!isRoot); - ginDeletePage(gvs, blkno, BufferGetBlockNumber(me->leftBuffer), - me->parent->blkno, myoff, me->parent->isRoot); - meDelete = true; + Assert(!myStackItem->isRoot); + ginDeletePostingPage(gvs, buffer, myStackItem->leftBuffer, + myStackItem->parent->buffer, + myStackItem->myoff, + myStackItem->parent->isRoot); + pageWasDeleted = true; } } - if (!meDelete) + if (!pageWasDeleted) { - if (BufferIsValid(me->leftBuffer)) - UnlockReleaseBuffer(me->leftBuffer); - me->leftBuffer = buffer; + /* + * Keep this page as the new leftBuffer for this level: the next + * sibling to the right might need it for deletion. Release any + * previously held left page first. + */ + if (BufferIsValid(myStackItem->leftBuffer)) + UnlockReleaseBuffer(myStackItem->leftBuffer); + myStackItem->leftBuffer = buffer; } else { - if (!isRoot) - LockBuffer(buffer, GIN_UNLOCK); - - ReleaseBuffer(buffer); + /* + * Page was deleted; release the buffer. leftBuffer remains the same. + */ + UnlockReleaseBuffer(buffer); } - if (isRoot) - ReleaseBuffer(buffer); - - return meDelete; + return pageWasDeleted; } @@ -417,6 +451,7 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno) DataPageDeleteStack root, *ptr, *tmp; + bool deleted PG_USED_FOR_ASSERTS_ONLY; buffer = ReadBufferExtended(gvs->index, MAIN_FORKNUM, rootBlkno, RBM_NORMAL, gvs->strategy); @@ -428,10 +463,13 @@ ginVacuumPostingTree(GinVacuumState *gvs, BlockNumber rootBlkno) LockBufferForCleanup(buffer); memset(&root, 0, sizeof(DataPageDeleteStack)); + root.buffer = buffer; root.leftBuffer = InvalidBuffer; + root.myoff = InvalidOffsetNumber; root.isRoot = true; - ginScanToDelete(gvs, rootBlkno, true, &root, InvalidOffsetNumber); + deleted = ginScanPostingTreeToDelete(gvs, &root); + Assert(!deleted); ptr = root.child; @@ -547,7 +585,7 @@ ginVacuumEntryPage(GinVacuumState *gvs, Buffer buffer, BlockNumber *roots, uint3 pfree(plist); PageIndexTupleDelete(tmppage, i); - if (PageAddItem(tmppage, (Item) itup, IndexTupleSize(itup), i, false, false) != i) + if (PageAddItem(tmppage, itup, IndexTupleSize(itup), i, false, false) != i) elog(ERROR, "failed to add item to index page in \"%s\"", RelationGetRelationName(gvs->index)); @@ -584,7 +622,7 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, if (stats == NULL) { /* Yes, so initialize stats to zeroes */ - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); /* * and cleanup any pending inserts @@ -654,8 +692,8 @@ ginbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, PageRestoreTempPage(resPage, page); MarkBufferDirty(buffer); xlogVacuumPage(gvs.index, buffer); - UnlockReleaseBuffer(buffer); END_CRIT_SECTION(); + UnlockReleaseBuffer(buffer); } else { @@ -693,6 +731,8 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) BlockNumber totFreePages; GinState ginstate; GinStatsData idxStat; + BlockRangeReadStreamPrivate p; + ReadStream *stream; /* * In an autovacuum analyze, we want to clean up pending insertions. @@ -714,7 +754,7 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) */ if (stats == NULL) { - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); initGinState(&ginstate, index); ginInsertCleanup(&ginstate, !AmAutoVacuumWorkerProcess(), false, true, stats); @@ -743,6 +783,24 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) totFreePages = 0; + /* Scan all blocks starting from the root using streaming reads */ + p.current_blocknum = GIN_ROOT_BLKNO; + p.last_exclusive = npages; + + /* + * It is safe to use batchmode as block_range_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE | + READ_STREAM_FULL | + READ_STREAM_USE_BATCHING, + info->strategy, + index, + MAIN_FORKNUM, + block_range_read_stream_cb, + &p, + 0); + for (blkno = GIN_ROOT_BLKNO; blkno < npages; blkno++) { Buffer buffer; @@ -750,10 +808,10 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) vacuum_delay_point(false); - buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, - RBM_NORMAL, info->strategy); + buffer = read_stream_next_buffer(stream, NULL); + LockBuffer(buffer, GIN_SHARE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (GinPageIsRecyclable(page)) { @@ -776,6 +834,9 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) UnlockReleaseBuffer(buffer); } + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); + /* Update the metapage with accurate page and entry counts */ idxStat.nTotalPages = npages; ginUpdateStats(info->index, &idxStat, false); @@ -815,7 +876,7 @@ GinPageIsRecyclable(Page page) /* * If no backend still could view delete_xid as in running, all scans - * concurrent with ginDeletePage() must have finished. + * concurrent with ginDeletePostingPage() must have finished. */ return GlobalVisCheckRemovableXid(NULL, delete_xid); } diff --git a/src/backend/access/gin/ginvalidate.c b/src/backend/access/gin/ginvalidate.c index 5b0bfe8cc1dbc..ac82ce922d22d 100644 --- a/src/backend/access/gin/ginvalidate.c +++ b/src/backend/access/gin/ginvalidate.c @@ -3,7 +3,7 @@ * ginvalidate.c * Opclass validator for GIN. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/gin/ginxlog.c b/src/backend/access/gin/ginxlog.c index 55a1ec09776ba..b1fee3c281f33 100644 --- a/src/backend/access/gin/ginxlog.c +++ b/src/backend/access/gin/ginxlog.c @@ -4,7 +4,7 @@ * WAL replay logic for inverted index. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -30,7 +30,7 @@ ginRedoClearIncompleteSplit(XLogReaderState *record, uint8 block_id) if (XLogReadBufferForRedo(record, block_id, &buffer) == BLK_NEEDS_REDO) { - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); GinPageGetOpaque(page)->flags &= ~GIN_INCOMPLETE_SPLIT; PageSetLSN(page, lsn); @@ -50,7 +50,7 @@ ginRedoCreatePTree(XLogReaderState *record) Page page; buffer = XLogInitBufferForRedo(record, 0); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); GinInitBuffer(buffer, GIN_DATA | GIN_LEAF | GIN_COMPRESSED); @@ -93,7 +93,7 @@ ginRedoInsertEntry(Buffer buffer, bool isLeaf, BlockNumber rightblkno, void *rda itup = &data->tuple; - if (PageAddItem(page, (Item) itup, IndexTupleSize(itup), offset, false, false) == InvalidOffsetNumber) + if (PageAddItem(page, itup, IndexTupleSize(itup), offset, false, false) == InvalidOffsetNumber) { RelFileLocator locator; ForkNumber forknum; @@ -119,12 +119,12 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) int actionno; int segno; GinPostingList *oldseg; - Pointer segmentend; + char *segmentend; char *walbuf; int totalsize; - Pointer tailCopy = NULL; - Pointer writePtr; - Pointer segptr; + void *tailCopy = NULL; + char *writePtr; + char *segptr; /* * If the page is in pre-9.4 format, convert to new format first. @@ -164,8 +164,8 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) } oldseg = GinDataLeafPageGetPostingList(page); - writePtr = (Pointer) oldseg; - segmentend = (Pointer) oldseg + GinDataLeafPageGetPostingListSize(page); + writePtr = (char *) oldseg; + segmentend = (char *) oldseg + GinDataLeafPageGetPostingListSize(page); segno = 0; walbuf = ((char *) data) + sizeof(ginxlogRecompressDataLeaf); @@ -212,7 +212,7 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) if (tailCopy) { Assert(writePtr + segsize < PageGetSpecialPointer(page)); - memcpy(writePtr, (Pointer) oldseg, segsize); + memcpy(writePtr, oldseg, segsize); } writePtr += segsize; oldseg = GinNextPostingListSegment(oldseg); @@ -243,7 +243,7 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) a_action = GIN_SEGMENT_REPLACE; } - segptr = (Pointer) oldseg; + segptr = (char *) oldseg; if (segptr != segmentend) segsize = SizeOfGinPostingList(oldseg); else @@ -264,7 +264,7 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) { int tailSize = segmentend - segptr; - tailCopy = (Pointer) palloc(tailSize); + tailCopy = palloc(tailSize); memcpy(tailCopy, segptr, tailSize); segptr = tailCopy; oldseg = (GinPostingList *) segptr; @@ -301,7 +301,7 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) } /* Copy the rest of unmodified segments if any. */ - segptr = (Pointer) oldseg; + segptr = (char *) oldseg; if (segptr != segmentend && tailCopy) { int restSize = segmentend - segptr; @@ -311,7 +311,7 @@ ginRedoRecompress(Page page, ginxlogRecompressDataLeaf *data) writePtr += restSize; } - totalsize = writePtr - (Pointer) GinDataLeafPageGetPostingList(page); + totalsize = writePtr - (char *) GinDataLeafPageGetPostingList(page); GinDataPageSetDataSize(page, totalsize); } @@ -368,7 +368,6 @@ ginRedoInsert(XLogReaderState *record) #endif payload += sizeof(BlockIdData); rightChildBlkno = BlockIdGetBlockNumber((BlockId) payload); - payload += sizeof(BlockIdData); ginRedoClearIncompleteSplit(record, 1); } @@ -574,8 +573,7 @@ ginRedoUpdateMetapage(XLogReaderState *record) { tupsize = IndexTupleSize(tuples); - if (PageAddItem(page, (Item) tuples, tupsize, off, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, tuples, tupsize, off, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page"); tuples = (IndexTuple) (((char *) tuples) + tupsize); @@ -655,7 +653,7 @@ ginRedoInsertListPage(XLogReaderState *record) { tupsize = IndexTupleSize(tuples); - l = PageAddItem(page, (Item) tuples, tupsize, off, false, false); + l = PageAddItem(page, tuples, tupsize, off, false, false); if (l == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page"); diff --git a/src/backend/access/gin/meson.build b/src/backend/access/gin/meson.build index 6472831476d64..278bf3814e530 100644 --- a/src/backend/access/gin/meson.build +++ b/src/backend/access/gin/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'ginarrayproc.c', diff --git a/src/backend/access/gist/README b/src/backend/access/gist/README index 8015ff19f05bc..75445b0745553 100644 --- a/src/backend/access/gist/README +++ b/src/backend/access/gist/README @@ -10,9 +10,13 @@ GiST stands for Generalized Search Tree. It was introduced in the seminal paper Jeffrey F. Naughton, Avi Pfeffer: http://www.sai.msu.su/~megera/postgres/gist/papers/gist.ps + +Concurrency support was described in "Concurrency and Recovery in Generalized +Search Trees", 1997, Marcel Kornacker, C. Mohan, Joseph M. Hellerstein: + https://dsf.berkeley.edu/papers/sigmod97-gist.pdf -and implemented by J. Hellerstein and P. Aoki in an early version of +GiST was implemented by J. Hellerstein and P. Aoki in an early version of PostgreSQL (more details are available from The GiST Indexing Project at Berkeley at http://gist.cs.berkeley.edu/). As a "university" project it had a limited number of features and was in rare use. @@ -55,6 +59,9 @@ The original algorithms were modified in several ways: it is now a single-pass algorithm. * Since the papers were theoretical, some details were omitted and we had to find out ourself how to solve some specific problems. +* The 1997 paper above (but not the 1995 one) states that leaf pages should + store the original key. While that can be done in PostgreSQL, it is + also possible to use a compressed representation in leaf pages. Because of the above reasons, we have revised the interaction of GiST core and PostgreSQL WAL system. Moreover, we encountered (and solved) @@ -172,7 +179,7 @@ it splits the page, and constructs the new downlink tuples for the split pages. The caller must then call gistplacetopage() on the parent page to insert the downlink tuples. The parent page that holds the downlink to the child might have migrated as a result of concurrent splits of the -parent, gistFindCorrectParent() is used to find the parent page. +parent, so gistFindCorrectParent() is used to find the parent page. Splitting the root page works slightly differently. At root split, gistplacetopage() allocates the new child pages and replaces the old root @@ -291,7 +298,7 @@ Buffering build algorithm ------------------------- In the buffering index build algorithm, some or all internal nodes have a -buffer attached to them. When a tuple is inserted at the top, the descend down +buffer attached to them. When a tuple is inserted at the top, the descent down the tree is stopped as soon as a buffer is reached, and the tuple is pushed to the buffer. When a buffer gets too full, all the tuples in it are flushed to the lower level, where they again hit lower level buffers or leaf pages. This @@ -455,8 +462,8 @@ be reused. In order to delete an empty page, its downlink must be removed from the parent. We scan all the internal pages, whose block numbers we memorized in the first stage, and look for downlinks to pages that we have memorized as being empty. Whenever we find one, we acquire a lock on the parent and child -page, re-check that the child page is still empty. Then, we remove the -downlink and mark the child as deleted, and release the locks. +page and re-check that the child page is still empty. Then we remove the +downlink, mark the child as deleted, and release the locks. The insertion algorithm would get confused, if an internal page was completely empty. So we never delete the last child of an internal page, even if it's diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c index 7b24380c97801..8565e225be7fd 100644 --- a/src/backend/access/gist/gist.c +++ b/src/backend/access/gist/gist.c @@ -4,7 +4,7 @@ * interface routines for the postgres GiST index access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -43,7 +43,7 @@ static void gistprunepage(Relation rel, Page page, Buffer buffer, #define ROTATEDIST(d) do { \ - SplitPageLayout *tmp = (SplitPageLayout *) palloc0(sizeof(SplitPageLayout)); \ + SplitPageLayout *tmp = palloc0_object(SplitPageLayout); \ tmp->block.blkno = InvalidBlockNumber; \ tmp->buffer = InvalidBuffer; \ tmp->next = (d); \ @@ -58,62 +58,63 @@ static void gistprunepage(Relation rel, Page page, Buffer buffer, Datum gisthandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = 0; - amroutine->amsupport = GISTNProcs; - amroutine->amoptsprocnum = GIST_OPTIONS_PROC; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = true; - amroutine->amcanhash = false; - amroutine->amconsistentequality = false; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = false; - amroutine->amcanunique = false; - amroutine->amcanmulticol = true; - amroutine->amoptionalkey = true; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = true; - amroutine->amstorage = true; - amroutine->amclusterable = true; - amroutine->ampredlocks = true; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = false; - amroutine->amcaninclude = true; - amroutine->amusemaintenanceworkmem = false; - amroutine->amsummarizing = false; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = gistbuild; - amroutine->ambuildempty = gistbuildempty; - amroutine->aminsert = gistinsert; - amroutine->aminsertcleanup = NULL; - amroutine->ambulkdelete = gistbulkdelete; - amroutine->amvacuumcleanup = gistvacuumcleanup; - amroutine->amcanreturn = gistcanreturn; - amroutine->amcostestimate = gistcostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = gistoptions; - amroutine->amproperty = gistproperty; - amroutine->ambuildphasename = NULL; - amroutine->amvalidate = gistvalidate; - amroutine->amadjustmembers = gistadjustmembers; - amroutine->ambeginscan = gistbeginscan; - amroutine->amrescan = gistrescan; - amroutine->amgettuple = gistgettuple; - amroutine->amgetbitmap = gistgetbitmap; - amroutine->amendscan = gistendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - amroutine->amtranslatestrategy = NULL; - amroutine->amtranslatecmptype = gisttranslatecmptype; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = 0, + .amsupport = GISTNProcs, + .amoptsprocnum = GIST_OPTIONS_PROC, + .amcanorder = false, + .amcanorderbyop = true, + .amcanhash = false, + .amconsistentequality = false, + .amconsistentordering = false, + .amcanbackward = false, + .amcanunique = false, + .amcanmulticol = true, + .amoptionalkey = true, + .amsearcharray = false, + .amsearchnulls = true, + .amstorage = true, + .amclusterable = true, + .ampredlocks = true, + .amcanparallel = false, + .amcanbuildparallel = false, + .amcaninclude = true, + .amusemaintenanceworkmem = false, + .amsummarizing = false, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP, + .amkeytype = InvalidOid, + + .ambuild = gistbuild, + .ambuildempty = gistbuildempty, + .aminsert = gistinsert, + .aminsertcleanup = NULL, + .ambulkdelete = gistbulkdelete, + .amvacuumcleanup = gistvacuumcleanup, + .amcanreturn = gistcanreturn, + .amcostestimate = gistcostestimate, + .amgettreeheight = NULL, + .amoptions = gistoptions, + .amproperty = gistproperty, + .ambuildphasename = NULL, + .amvalidate = gistvalidate, + .amadjustmembers = gistadjustmembers, + .ambeginscan = gistbeginscan, + .amrescan = gistrescan, + .amgettuple = gistgettuple, + .amgetbitmap = gistgetbitmap, + .amendscan = gistendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + .amtranslatestrategy = NULL, + .amtranslatecmptype = gisttranslatecmptype, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -290,7 +291,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, SplitPageLayout *dist = NULL, *ptr; BlockNumber oldrlink = InvalidBlockNumber; - GistNSN oldnsn = 0; + GistNSN oldnsn = InvalidXLogRecPtr; SplitPageLayout rootpg; bool is_rootsplit; int npage; @@ -392,7 +393,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, /* Prepare a vector of all the downlinks */ for (ptr = dist; ptr; ptr = ptr->next) ndownlinks++; - downlinks = palloc(sizeof(IndexTuple) * ndownlinks); + downlinks = palloc_array(IndexTuple, ndownlinks); for (i = 0, ptr = dist; ptr; ptr = ptr->next) downlinks[i++] = ptr->itup; @@ -410,7 +411,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, /* Prepare split-info to be returned to caller */ for (ptr = dist; ptr; ptr = ptr->next) { - GISTPageSplitInfo *si = palloc(sizeof(GISTPageSplitInfo)); + GISTPageSplitInfo *si = palloc_object(GISTPageSplitInfo); si->buf = ptr->buffer; si->downlink = ptr->itup; @@ -430,7 +431,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, { IndexTuple thistup = (IndexTuple) data; - if (PageAddItem(ptr->page, (Item) data, IndexTupleSize(thistup), i + FirstOffsetNumber, false, false) == InvalidOffsetNumber) + if (PageAddItem(ptr->page, data, IndexTupleSize(thistup), i + FirstOffsetNumber, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page in \"%s\"", RelationGetRelationName(rel)); /* @@ -516,7 +517,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, dist, oldrlink, oldnsn, leftchildbuf, markfollowright); else - recptr = gistGetFakeLSN(rel); + recptr = XLogGetFakeLSN(rel); } for (ptr = dist; ptr; ptr = ptr->next) @@ -551,8 +552,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, if (ntup == 1) { /* One-for-one replacement, so use PageIndexTupleOverwrite */ - if (!PageIndexTupleOverwrite(page, oldoffnum, (Item) *itup, - IndexTupleSize(*itup))) + if (!PageIndexTupleOverwrite(page, oldoffnum, *itup, IndexTupleSize(*itup))) elog(ERROR, "failed to add item to index page in \"%s\"", RelationGetRelationName(rel)); } @@ -594,7 +594,7 @@ gistplacetopage(Relation rel, Size freespace, GISTSTATE *giststate, leftchildbuf); } else - recptr = gistGetFakeLSN(rel); + recptr = XLogGetFakeLSN(rel); } PageSetLSN(page, recptr); @@ -654,7 +654,7 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, /* Start from the root */ firststack.blkno = GIST_ROOT_BLKNO; - firststack.lsn = 0; + firststack.lsn = InvalidXLogRecPtr; firststack.retry_from_parent = false; firststack.parent = NULL; firststack.downlinkoffnum = InvalidOffsetNumber; @@ -683,7 +683,7 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, state.stack = stack = stack->parent; } - if (XLogRecPtrIsInvalid(stack->lsn)) + if (!XLogRecPtrIsValid(stack->lsn)) stack->buffer = ReadBuffer(state.r, stack->blkno); /* @@ -696,10 +696,10 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, gistcheckpage(state.r, stack->buffer); } - stack->page = (Page) BufferGetPage(stack->buffer); + stack->page = BufferGetPage(stack->buffer); stack->lsn = xlocked ? PageGetLSN(stack->page) : BufferGetLSNAtomic(stack->buffer); - Assert(!RelationNeedsWAL(state.r) || !XLogRecPtrIsInvalid(stack->lsn)); + Assert(!RelationNeedsWAL(state.r) || XLogRecPtrIsValid(stack->lsn)); /* * If this page was split but the downlink was never inserted to the @@ -783,7 +783,7 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, LockBuffer(stack->buffer, GIST_UNLOCK); LockBuffer(stack->buffer, GIST_EXCLUSIVE); xlocked = true; - stack->page = (Page) BufferGetPage(stack->buffer); + stack->page = BufferGetPage(stack->buffer); if (PageGetLSN(stack->page) != stack->lsn) { @@ -824,7 +824,7 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, xlocked = false; /* descend to the chosen child */ - item = (GISTInsertStack *) palloc0(sizeof(GISTInsertStack)); + item = palloc0_object(GISTInsertStack); item->blkno = childblkno; item->parent = stack; item->downlinkoffnum = downlinkoffnum; @@ -847,7 +847,7 @@ gistdoinsert(Relation r, IndexTuple itup, Size freespace, LockBuffer(stack->buffer, GIST_UNLOCK); LockBuffer(stack->buffer, GIST_EXCLUSIVE); xlocked = true; - stack->page = (Page) BufferGetPage(stack->buffer); + stack->page = BufferGetPage(stack->buffer); stack->lsn = PageGetLSN(stack->page); if (stack->blkno == GIST_ROOT_BLKNO) @@ -924,7 +924,7 @@ gistFindPath(Relation r, BlockNumber child, OffsetNumber *downlinkoffnum) *ptr; BlockNumber blkno; - top = (GISTInsertStack *) palloc0(sizeof(GISTInsertStack)); + top = palloc0_object(GISTInsertStack); top->blkno = GIST_ROOT_BLKNO; top->downlinkoffnum = InvalidOffsetNumber; @@ -938,7 +938,7 @@ gistFindPath(Relation r, BlockNumber child, OffsetNumber *downlinkoffnum) buffer = ReadBuffer(r, top->blkno); LockBuffer(buffer, GIST_SHARE); gistcheckpage(r, buffer); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (GistPageIsLeaf(page)) { @@ -976,7 +976,7 @@ gistFindPath(Relation r, BlockNumber child, OffsetNumber *downlinkoffnum) * leaf pages, and we assume that there can't be any non-leaf * pages behind leaf pages. */ - ptr = (GISTInsertStack *) palloc0(sizeof(GISTInsertStack)); + ptr = palloc0_object(GISTInsertStack); ptr->blkno = GistPageGetOpaque(page)->rightlink; ptr->downlinkoffnum = InvalidOffsetNumber; ptr->parent = top->parent; @@ -1001,7 +1001,7 @@ gistFindPath(Relation r, BlockNumber child, OffsetNumber *downlinkoffnum) else { /* Append this child to the list of pages to visit later */ - ptr = (GISTInsertStack *) palloc0(sizeof(GISTInsertStack)); + ptr = palloc0_object(GISTInsertStack); ptr->blkno = blkno; ptr->downlinkoffnum = i; ptr->parent = top; @@ -1033,7 +1033,7 @@ gistFindCorrectParent(Relation r, GISTInsertStack *child, bool is_build) GISTInsertStack *ptr; gistcheckpage(r, parent->buffer); - parent->page = (Page) BufferGetPage(parent->buffer); + parent->page = BufferGetPage(parent->buffer); maxoff = PageGetMaxOffsetNumber(parent->page); /* Check if the downlink is still where it was before */ @@ -1098,7 +1098,7 @@ gistFindCorrectParent(Relation r, GISTInsertStack *child, bool is_build) parent->buffer = ReadBuffer(r, parent->blkno); LockBuffer(parent->buffer, GIST_EXCLUSIVE); gistcheckpage(r, parent->buffer); - parent->page = (Page) BufferGetPage(parent->buffer); + parent->page = BufferGetPage(parent->buffer); } /* @@ -1121,7 +1121,7 @@ gistFindCorrectParent(Relation r, GISTInsertStack *child, bool is_build) while (ptr) { ptr->buffer = ReadBuffer(r, ptr->blkno); - ptr->page = (Page) BufferGetPage(ptr->buffer); + ptr->page = BufferGetPage(ptr->buffer); ptr = ptr->parent; } @@ -1219,7 +1219,7 @@ gistfixsplit(GISTInsertState *state, GISTSTATE *giststate) */ for (;;) { - GISTPageSplitInfo *si = palloc(sizeof(GISTPageSplitInfo)); + GISTPageSplitInfo *si = palloc_object(GISTPageSplitInfo); IndexTuple downlink; page = BufferGetPage(buf); @@ -1483,8 +1483,8 @@ gistSplit(Relation r, gistSplitByKey(r, page, itup, len, giststate, &v, 0); /* form left and right vector */ - lvectup = (IndexTuple *) palloc(sizeof(IndexTuple) * (len + 1)); - rvectup = (IndexTuple *) palloc(sizeof(IndexTuple) * (len + 1)); + lvectup = palloc_array(IndexTuple, len + 1); + rvectup = palloc_array(IndexTuple, len + 1); for (i = 0; i < v.splitVector.spl_nleft; i++) lvectup[i] = itup[v.splitVector.spl_left[i] - 1]; @@ -1553,7 +1553,7 @@ initGISTstate(Relation index) oldCxt = MemoryContextSwitchTo(scanCxt); /* Create and fill in the GISTSTATE */ - giststate = (GISTSTATE *) palloc(sizeof(GISTSTATE)); + giststate = palloc_object(GISTSTATE); giststate->scanCxt = scanCxt; giststate->tempCxt = scanCxt; /* caller must change this if needed */ @@ -1733,7 +1733,7 @@ gistprunepage(Relation rel, Page page, Buffer buffer, Relation heapRel) PageSetLSN(page, recptr); } else - PageSetLSN(page, gistGetFakeLSN(rel)); + PageSetLSN(page, XLogGetFakeLSN(rel)); END_CRIT_SECTION(); } diff --git a/src/backend/access/gist/gistbuild.c b/src/backend/access/gist/gistbuild.c index 9e707167d984b..7f57c787f4cbf 100644 --- a/src/backend/access/gist/gistbuild.c +++ b/src/backend/access/gist/gistbuild.c @@ -22,7 +22,7 @@ * tuples (unless buffering mode is disabled). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -346,7 +346,7 @@ gistbuild(Relation heap, Relation index, IndexInfo *indexInfo) /* * Return statistics */ - result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + result = palloc_object(IndexBuildResult); result->heap_tuples = reltuples; result->index_tuples = (double) buildstate.indtuples; @@ -409,7 +409,7 @@ gist_indexsortbuild(GISTBuildState *state) state->bulkstate = smgr_bulk_start_rel(state->indexrel, MAIN_FORKNUM); /* Allocate a temporary buffer for the first leaf page batch. */ - levelstate = palloc0(sizeof(GistSortedBuildLevelState)); + levelstate = palloc0_object(GistSortedBuildLevelState); levelstate->pages[0] = palloc(BLCKSZ); levelstate->parent = NULL; gistinitpage(levelstate->pages[0], F_LEAF); @@ -526,7 +526,7 @@ gist_indexsortbuild_levelstate_flush(GISTBuildState *state, else { /* Create split layout from single page */ - dist = (SplitPageLayout *) palloc0(sizeof(SplitPageLayout)); + dist = palloc0_object(SplitPageLayout); union_tuple = gistunion(state->indexrel, itvec, vect_len, state->giststate); dist->itup = union_tuple; @@ -558,7 +558,7 @@ gist_indexsortbuild_levelstate_flush(GISTBuildState *state, { IndexTuple thistup = (IndexTuple) data; - if (PageAddItem(target, (Item) data, IndexTupleSize(thistup), i + FirstOffsetNumber, false, false) == InvalidOffsetNumber) + if (PageAddItem(target, data, IndexTupleSize(thistup), i + FirstOffsetNumber, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to index page in \"%s\"", RelationGetRelationName(state->indexrel)); data += IndexTupleSize(thistup); @@ -597,7 +597,7 @@ gist_indexsortbuild_levelstate_flush(GISTBuildState *state, parent = levelstate->parent; if (parent == NULL) { - parent = palloc0(sizeof(GistSortedBuildLevelState)); + parent = palloc0_object(GistSortedBuildLevelState); parent->pages[0] = palloc(BLCKSZ); parent->parent = NULL; gistinitpage(parent->pages[0], 0); @@ -969,7 +969,7 @@ gistProcessItup(GISTBuildState *buildstate, IndexTuple itup, buffer = ReadBuffer(indexrel, blkno); LockBuffer(buffer, GIST_EXCLUSIVE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); childoffnum = gistchoose(indexrel, page, itup, giststate); iid = PageGetItemId(page, childoffnum); idxtuple = (IndexTuple) PageGetItem(page, iid); @@ -1154,7 +1154,7 @@ gistbufferinginserttuples(GISTBuildState *buildstate, Buffer buffer, int level, /* Create an array of all the downlink tuples */ ndownlinks = list_length(splitinfo); - downlinks = (IndexTuple *) palloc(sizeof(IndexTuple) * ndownlinks); + downlinks = palloc_array(IndexTuple, ndownlinks); i = 0; foreach(lc, splitinfo) { @@ -1448,7 +1448,7 @@ gistGetMaxLevel(Relation index) * pro forma. */ LockBuffer(buffer, GIST_SHARE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (GistPageIsLeaf(page)) { diff --git a/src/backend/access/gist/gistbuildbuffers.c b/src/backend/access/gist/gistbuildbuffers.c index 0707254d18ea1..3213cf45aa690 100644 --- a/src/backend/access/gist/gistbuildbuffers.c +++ b/src/backend/access/gist/gistbuildbuffers.c @@ -4,7 +4,7 @@ * node buffer management functions for GiST buffering build algorithm. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -46,7 +46,7 @@ gistInitBuildBuffers(int pagesPerBuffer, int levelStep, int maxLevel) GISTBuildBuffers *gfbb; HASHCTL hashCtl; - gfbb = palloc(sizeof(GISTBuildBuffers)); + gfbb = palloc_object(GISTBuildBuffers); gfbb->pagesPerBuffer = pagesPerBuffer; gfbb->levelStep = levelStep; @@ -60,7 +60,7 @@ gistInitBuildBuffers(int pagesPerBuffer, int levelStep, int maxLevel) /* Initialize free page management. */ gfbb->nFreeBlocks = 0; gfbb->freeBlocksLen = 32; - gfbb->freeBlocks = (long *) palloc(gfbb->freeBlocksLen * sizeof(long)); + gfbb->freeBlocks = palloc_array(long, gfbb->freeBlocksLen); /* * Current memory context will be used for all in-memory data structures @@ -87,8 +87,7 @@ gistInitBuildBuffers(int pagesPerBuffer, int levelStep, int maxLevel) * buffers are inserted here when they are created. */ gfbb->buffersOnLevelsLen = 1; - gfbb->buffersOnLevels = (List **) palloc(sizeof(List *) * - gfbb->buffersOnLevelsLen); + gfbb->buffersOnLevels = palloc_array(List *, gfbb->buffersOnLevelsLen); gfbb->buffersOnLevels[0] = NIL; /* @@ -96,8 +95,7 @@ gistInitBuildBuffers(int pagesPerBuffer, int levelStep, int maxLevel) * into main memory. */ gfbb->loadedBuffersLen = 32; - gfbb->loadedBuffers = (GISTNodeBuffer **) palloc(gfbb->loadedBuffersLen * - sizeof(GISTNodeBuffer *)); + gfbb->loadedBuffers = palloc_array(GISTNodeBuffer *, gfbb->loadedBuffersLen); gfbb->loadedBuffersCount = 0; gfbb->rootlevel = maxLevel; @@ -582,9 +580,7 @@ gistRelocateBuildBuffersOnSplit(GISTBuildBuffers *gfbb, GISTSTATE *giststate, * Allocate memory for information about relocation buffers. */ splitPagesCount = list_length(splitinfo); - relocationBuffersInfos = - (RelocationBufferInfo *) palloc(sizeof(RelocationBufferInfo) * - splitPagesCount); + relocationBuffersInfos = palloc_array(RelocationBufferInfo, splitPagesCount); /* * Fill relocation buffers information for node buffers of pages produced diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c index 387d997234537..4d7c100d73781 100644 --- a/src/backend/access/gist/gistget.c +++ b/src/backend/access/gist/gistget.c @@ -4,7 +4,7 @@ * fetch tuples from a GiST scan. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -17,6 +17,7 @@ #include "access/genam.h" #include "access/gist_private.h" #include "access/relscan.h" +#include "executor/instrument_node.h" #include "lib/pairingheap.h" #include "miscadmin.h" #include "pgstat.h" @@ -46,7 +47,7 @@ gistkillitems(IndexScanDesc scan) bool killedsomething = false; Assert(so->curBlkno != InvalidBlockNumber); - Assert(!XLogRecPtrIsInvalid(so->curPageLSN)); + Assert(XLogRecPtrIsValid(so->curPageLSN)); Assert(so->killedItems != NULL); buffer = ReadBuffer(scan->indexRelation, so->curBlkno); @@ -63,11 +64,7 @@ gistkillitems(IndexScanDesc scan) * safe. */ if (BufferGetLSNAtomic(buffer) != so->curPageLSN) - { - UnlockReleaseBuffer(buffer); - so->numKilled = 0; /* reset counter */ - return; - } + goto unlock; Assert(GistPageIsLeaf(page)); @@ -77,6 +74,17 @@ gistkillitems(IndexScanDesc scan) */ for (i = 0; i < so->numKilled; i++) { + if (!killedsomething) + { + /* + * Use the hint bit infrastructure to check if we can update the + * page while just holding a share lock. If we are not allowed, + * there's no point continuing. + */ + if (!BufferBeginSetHintBits(buffer)) + goto unlock; + } + offnum = so->killedItems[i]; iid = PageGetItemId(page, offnum); ItemIdMarkDead(iid); @@ -86,9 +94,10 @@ gistkillitems(IndexScanDesc scan) if (killedsomething) { GistMarkPageHasGarbage(page); - MarkBufferDirtyHint(buffer, true); + BufferFinishSetHintBits(buffer, true, true); } +unlock: UnlockReleaseBuffer(buffer); /* @@ -222,7 +231,7 @@ gistindex_keytest(IndexScanDesc scan, key->sk_collation, PointerGetDatum(&de), key->sk_argument, - Int16GetDatum(key->sk_strategy), + UInt16GetDatum(key->sk_strategy), ObjectIdGetDatum(key->sk_subtype), PointerGetDatum(&recheck)); @@ -286,7 +295,7 @@ gistindex_keytest(IndexScanDesc scan, key->sk_collation, PointerGetDatum(&de), key->sk_argument, - Int16GetDatum(key->sk_strategy), + UInt16GetDatum(key->sk_strategy), ObjectIdGetDatum(key->sk_subtype), PointerGetDatum(&recheck)); *recheck_distances_p |= recheck; @@ -353,7 +362,7 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem, * parentlsn < nsn), or if the system crashed after a page split but * before the downlink was inserted into the parent. */ - if (!XLogRecPtrIsInvalid(pageItem->data.parentlsn) && + if (XLogRecPtrIsValid(pageItem->data.parentlsn) && (GistFollowRight(page) || pageItem->data.parentlsn < GistPageGetNSN(page)) && opaque->rightlink != InvalidBlockNumber /* sanity check */ ) @@ -401,8 +410,8 @@ gistScanPage(IndexScanDesc scan, GISTSearchItem *pageItem, /* * We save the LSN of the page as we read it, so that we know whether it - * safe to apply LP_DEAD hints to the page later. This allows us to drop - * the pin for MVCC scans, which allows vacuum to avoid blocking. + * is safe to apply LP_DEAD hints to the page later. This allows us to + * drop the pin for MVCC scans, which allows vacuum to avoid blocking. */ so->curPageLSN = BufferGetLSNAtomic(buffer); diff --git a/src/backend/access/gist/gistproc.c b/src/backend/access/gist/gistproc.c index 392163cb22900..bb84030b23d00 100644 --- a/src/backend/access/gist/gistproc.c +++ b/src/backend/access/gist/gistproc.c @@ -7,7 +7,7 @@ * This gives R-tree behavior, with Guttman's poly-time split algorithm. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -115,8 +115,9 @@ gist_box_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); BOX *query = PG_GETARG_BOX_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); /* All cases served by this function are exact */ @@ -171,7 +172,7 @@ gist_box_union(PG_FUNCTION_ARGS) *pageunion; numranges = entryvec->n; - pageunion = (BOX *) palloc(sizeof(BOX)); + pageunion = palloc_object(BOX); cur = DatumGetBoxP(entryvec->vector[0].key); memcpy(pageunion, cur, sizeof(BOX)); @@ -237,7 +238,7 @@ fallbackSplit(GistEntryVector *entryvec, GIST_SPLITVEC *v) v->spl_left[v->spl_nleft] = i; if (unionL == NULL) { - unionL = (BOX *) palloc(sizeof(BOX)); + unionL = palloc_object(BOX); *unionL = *cur; } else @@ -250,7 +251,7 @@ fallbackSplit(GistEntryVector *entryvec, GIST_SPLITVEC *v) v->spl_right[v->spl_nright] = i; if (unionR == NULL) { - unionR = (BOX *) palloc(sizeof(BOX)); + unionR = palloc_object(BOX); *unionR = *cur; } else @@ -698,8 +699,8 @@ gist_box_picksplit(PG_FUNCTION_ARGS) v->spl_nright = 0; /* Allocate bounding boxes of left and right groups */ - leftBox = palloc0(sizeof(BOX)); - rightBox = palloc0(sizeof(BOX)); + leftBox = palloc0_object(BOX); + rightBox = palloc0_object(BOX); /* * Allocate an array for "common entries" - entries which can be placed to @@ -1042,10 +1043,10 @@ gist_poly_compress(PG_FUNCTION_ARGS) POLYGON *in = DatumGetPolygonP(entry->key); BOX *r; - r = (BOX *) palloc(sizeof(BOX)); + r = palloc_object(BOX); memcpy(r, &(in->boundbox), sizeof(BOX)); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, false); @@ -1064,8 +1065,9 @@ gist_poly_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); POLYGON *query = PG_GETARG_POLYGON_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); bool result; @@ -1107,13 +1109,13 @@ gist_circle_compress(PG_FUNCTION_ARGS) CIRCLE *in = DatumGetCircleP(entry->key); BOX *r; - r = (BOX *) palloc(sizeof(BOX)); + r = palloc_object(BOX); r->high.x = float8_pl(in->center.x, in->radius); r->low.x = float8_mi(in->center.x, in->radius); r->high.y = float8_pl(in->center.y, in->radius); r->low.y = float8_mi(in->center.y, in->radius); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(r), entry->rel, entry->page, entry->offset, false); @@ -1132,8 +1134,9 @@ gist_circle_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); CIRCLE *query = PG_GETARG_CIRCLE_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); BOX bbox; bool result; @@ -1171,9 +1174,9 @@ gist_point_compress(PG_FUNCTION_ARGS) if (entry->leafkey) /* Point, actually */ { - BOX *box = palloc(sizeof(BOX)); + BOX *box = palloc_object(BOX); Point *point = DatumGetPointP(entry->key); - GISTENTRY *retval = palloc(sizeof(GISTENTRY)); + GISTENTRY *retval = palloc_object(GISTENTRY); box->high = box->low = *point; @@ -1200,9 +1203,9 @@ gist_point_fetch(PG_FUNCTION_ARGS) Point *r; GISTENTRY *retval; - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); - r = (Point *) palloc(sizeof(Point)); + r = palloc_object(Point); r->x = in->high.x; r->y = in->high.y; gistentryinit(*retval, PointerGetDatum(r), @@ -1502,9 +1505,10 @@ gist_box_distance(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Datum query = PG_GETARG_DATUM(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ - /* bool *recheck = (bool *) PG_GETARG_POINTER(4); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); + bool *recheck = (bool *) PG_GETARG_POINTER(4); +#endif float8 distance; distance = gist_bbox_distance(entry, query, strategy); @@ -1528,8 +1532,9 @@ gist_circle_distance(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Datum query = PG_GETARG_DATUM(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); float8 distance; @@ -1545,8 +1550,9 @@ gist_poly_distance(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); Datum query = PG_GETARG_DATUM(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); float8 distance; @@ -1707,8 +1713,8 @@ gist_bbox_zorder_cmp(Datum a, Datum b, SortSupport ssup) * Abbreviated version of Z-order comparison * * The abbreviated format is a Z-order value computed from the two 32-bit - * floats. If SIZEOF_DATUM == 8, the 64-bit Z-order value fits fully in the - * abbreviated Datum, otherwise use its most significant bits. + * floats. Now that sizeof(Datum) is always 8, the 64-bit Z-order value + * always fits fully in the abbreviated Datum. */ static Datum gist_bbox_zorder_abbrev_convert(Datum original, SortSupport ssup) @@ -1718,11 +1724,7 @@ gist_bbox_zorder_abbrev_convert(Datum original, SortSupport ssup) z = point_zorder_internal(p->x, p->y); -#if SIZEOF_DATUM == 8 - return (Datum) z; -#else - return (Datum) (z >> 32); -#endif + return UInt64GetDatum(z); } /* diff --git a/src/backend/access/gist/gistscan.c b/src/backend/access/gist/gistscan.c index 700fa959d03d8..c65f93abdaeb8 100644 --- a/src/backend/access/gist/gistscan.c +++ b/src/backend/access/gist/gistscan.c @@ -4,7 +4,7 @@ * routines to manage scans on GiST index relations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -90,7 +90,7 @@ gistbeginscan(Relation r, int nkeys, int norderbys) oldCxt = MemoryContextSwitchTo(giststate->scanCxt); /* initialize opaque data */ - so = (GISTScanOpaque) palloc0(sizeof(GISTScanOpaqueData)); + so = palloc0_object(GISTScanOpaqueData); so->giststate = giststate; giststate->tempCxt = createTempGistContext(); so->queue = NULL; @@ -101,8 +101,8 @@ gistbeginscan(Relation r, int nkeys, int norderbys) so->qual_ok = true; /* in case there are zero keys */ if (scan->numberOfOrderBys > 0) { - scan->xs_orderbyvals = palloc0(sizeof(Datum) * scan->numberOfOrderBys); - scan->xs_orderbynulls = palloc(sizeof(bool) * scan->numberOfOrderBys); + scan->xs_orderbyvals = palloc0_array(Datum, scan->numberOfOrderBys); + scan->xs_orderbynulls = palloc_array(bool, scan->numberOfOrderBys); memset(scan->xs_orderbynulls, true, sizeof(bool) * scan->numberOfOrderBys); } @@ -201,6 +201,7 @@ gistrescan(IndexScanDesc scan, ScanKey key, int nkeys, attno - 1)->atttypid, -1, 0); } + TupleDescFinalize(so->giststate->fetchTupdesc); scan->xs_hitupdesc = so->giststate->fetchTupdesc; /* Also create a memory context that will hold the returned tuples */ diff --git a/src/backend/access/gist/gistsplit.c b/src/backend/access/gist/gistsplit.c index 49838ceb07b19..dc899565320ac 100644 --- a/src/backend/access/gist/gistsplit.c +++ b/src/backend/access/gist/gistsplit.c @@ -15,7 +15,7 @@ * gistSplitByKey() is the entry point to this file. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -51,7 +51,7 @@ gistunionsubkeyvec(GISTSTATE *giststate, IndexTuple *itvec, int i, cleanedLen = 0; - cleanedItVec = (IndexTuple *) palloc(sizeof(IndexTuple) * gsvp->len); + cleanedItVec = palloc_array(IndexTuple, gsvp->len); for (i = 0; i < gsvp->len; i++) { @@ -501,7 +501,7 @@ gistUserPicksplit(Relation r, GistEntryVector *entryvec, int attno, GistSplitVec * Locate don't-care tuples, if any. If there are none, the split is * optimal, so just fall out and return false. */ - v->spl_dontcare = (bool *) palloc0(sizeof(bool) * (entryvec->n + 1)); + v->spl_dontcare = palloc0_array(bool, entryvec->n + 1); NumDontCare = findDontCares(r, giststate, entryvec->vector, v, attno); @@ -738,9 +738,9 @@ gistSplitByKey(Relation r, Page page, IndexTuple *itup, int len, * call will overwrite that with its own result. */ backupSplit = v->splitVector; - backupSplit.spl_left = (OffsetNumber *) palloc(sizeof(OffsetNumber) * len); + backupSplit.spl_left = palloc_array(OffsetNumber, len); memcpy(backupSplit.spl_left, v->splitVector.spl_left, sizeof(OffsetNumber) * v->splitVector.spl_nleft); - backupSplit.spl_right = (OffsetNumber *) palloc(sizeof(OffsetNumber) * len); + backupSplit.spl_right = palloc_array(OffsetNumber, len); memcpy(backupSplit.spl_right, v->splitVector.spl_right, sizeof(OffsetNumber) * v->splitVector.spl_nright); /* Recursively decide how to split the don't-care tuples */ diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c index a6b701943d3de..0f58f61879fb0 100644 --- a/src/backend/access/gist/gistutil.c +++ b/src/backend/access/gist/gistutil.c @@ -4,7 +4,7 @@ * utilities routines for the postgres GiST index access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -44,10 +44,10 @@ gistfillbuffer(Page page, IndexTuple *itup, int len, OffsetNumber off) Size sz = IndexTupleSize(itup[i]); OffsetNumber l; - l = PageAddItem(page, (Item) itup[i], sz, off, false, false); + l = PageAddItem(page, itup[i], sz, off, false, false); if (l == InvalidOffsetNumber) - elog(ERROR, "failed to add item to GiST index page, item %d out of %d, size %d bytes", - i, len, (int) sz); + elog(ERROR, "failed to add item to GiST index page, item %d out of %d, size %zu bytes", + i, len, sz); off++; } } @@ -100,7 +100,7 @@ gistextractpage(Page page, int *len /* out */ ) maxoff = PageGetMaxOffsetNumber(page); *len = maxoff; - itvec = palloc(sizeof(IndexTuple) * maxoff); + itvec = palloc_array(IndexTuple, maxoff); for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) itvec[i - FirstOffsetNumber] = (IndexTuple) PageGetItem(page, PageGetItemId(page, i)); @@ -113,7 +113,7 @@ gistextractpage(Page page, int *len /* out */ ) IndexTuple * gistjoinvector(IndexTuple *itvec, int *len, IndexTuple *additvec, int addlen) { - itvec = (IndexTuple *) repalloc(itvec, sizeof(IndexTuple) * ((*len) + addlen)); + itvec = repalloc_array(itvec, IndexTuple, (*len) + addlen); memmove(&itvec[*len], additvec, sizeof(IndexTuple) * addlen); *len += addlen; return itvec; @@ -157,7 +157,7 @@ gistMakeUnionItVec(GISTSTATE *giststate, IndexTuple *itvec, int len, { int i; GistEntryVector *evec; - int attrsize; + int attrsize = 0; /* silence compiler warning */ evec = (GistEntryVector *) palloc((len + 2) * sizeof(GISTENTRY) + GEVHDRSZ); @@ -242,7 +242,7 @@ gistMakeUnionKey(GISTSTATE *giststate, int attno, char padding[2 * sizeof(GISTENTRY) + GEVHDRSZ]; } storage; GistEntryVector *evec = &storage.gev; - int dstsize; + int dstsize = 0; /* silence compiler warning */ evec->n = 2; @@ -1008,61 +1008,11 @@ gistproperty(Oid index_oid, int attno, } /* - * Some indexes are not WAL-logged, but we need LSNs to detect concurrent page - * splits anyway. This function provides a fake sequence of LSNs for that - * purpose. - */ -XLogRecPtr -gistGetFakeLSN(Relation rel) -{ - if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) - { - /* - * Temporary relations are only accessible in our session, so a simple - * backend-local counter will do. - */ - static XLogRecPtr counter = FirstNormalUnloggedLSN; - - return counter++; - } - else if (RelationIsPermanent(rel)) - { - /* - * WAL-logging on this relation will start after commit, so its LSNs - * must be distinct numbers smaller than the LSN at the next commit. - * Emit a dummy WAL record if insert-LSN hasn't advanced after the - * last call. - */ - static XLogRecPtr lastlsn = InvalidXLogRecPtr; - XLogRecPtr currlsn = GetXLogInsertRecPtr(); - - /* Shouldn't be called for WAL-logging relations */ - Assert(!RelationNeedsWAL(rel)); - - /* No need for an actual record if we already have a distinct LSN */ - if (!XLogRecPtrIsInvalid(lastlsn) && lastlsn == currlsn) - currlsn = gistXLogAssignLSN(); - - lastlsn = currlsn; - return currlsn; - } - else - { - /* - * Unlogged relations are accessible from other backends, and survive - * (clean) restarts. GetFakeLSNForUnloggedRel() handles that for us. - */ - Assert(rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED); - return GetFakeLSNForUnloggedRel(); - } -} - -/* - * This is a stratnum support function for GiST opclasses that use the - * RT*StrategyNumber constants. + * This is a stratnum translation support function for GiST opclasses that use + * the RT*StrategyNumber constants. */ Datum -gist_stratnum_common(PG_FUNCTION_ARGS) +gist_translate_cmptype_common(PG_FUNCTION_ARGS) { CompareType cmptype = PG_GETARG_INT32(0); @@ -1090,9 +1040,9 @@ gist_stratnum_common(PG_FUNCTION_ARGS) /* * Returns the opclass's private stratnum used for the given compare type. * - * Calls the opclass's GIST_STRATNUM_PROC support function, if any, - * and returns the result. - * Returns InvalidStrategy if the function is not defined. + * Calls the opclass's GIST_TRANSLATE_CMPTYPE_PROC support function, if any, + * and returns the result. Returns InvalidStrategy if the function is not + * defined. */ StrategyNumber gisttranslatecmptype(CompareType cmptype, Oid opfamily) @@ -1101,7 +1051,7 @@ gisttranslatecmptype(CompareType cmptype, Oid opfamily) Datum result; /* Check whether the function is provided. */ - funcid = get_opfamily_proc(opfamily, ANYOID, ANYOID, GIST_STRATNUM_PROC); + funcid = get_opfamily_proc(opfamily, ANYOID, ANYOID, GIST_TRANSLATE_CMPTYPE_PROC); if (!OidIsValid(funcid)) return InvalidStrategy; diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c index dca236b6e5735..686a04180546b 100644 --- a/src/backend/access/gist/gistvacuum.c +++ b/src/backend/access/gist/gistvacuum.c @@ -4,7 +4,7 @@ * vacuuming routines for the postgres GiST index access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -16,7 +16,7 @@ #include "access/genam.h" #include "access/gist_private.h" -#include "access/transam.h" +#include "access/xloginsert.h" #include "commands/vacuum.h" #include "lib/integerset.h" #include "miscadmin.h" @@ -61,7 +61,7 @@ gistbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, { /* allocate stats if first time through, else re-use existing struct */ if (stats == NULL) - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); gistvacuumscan(info, stats, callback, callback_state); @@ -85,7 +85,7 @@ gistvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) */ if (stats == NULL) { - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); gistvacuumscan(info, stats, NULL, NULL); } @@ -182,7 +182,7 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, if (RelationNeedsWAL(rel)) vstate.startNSN = GetInsertRecPtr(); else - vstate.startNSN = gistGetFakeLSN(rel); + vstate.startNSN = XLogGetFakeLSN(rel); /* * The outer loop iterates over all index pages, in physical order (we @@ -330,7 +330,7 @@ gistvacuumpage(GistVacState *vstate, Buffer buffer) * exclusive lock. */ LockBuffer(buffer, GIST_EXCLUSIVE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (gistPageRecyclable(page)) { @@ -413,7 +413,7 @@ gistvacuumpage(GistVacState *vstate, Buffer buffer) PageSetLSN(page, recptr); } else - PageSetLSN(page, gistGetFakeLSN(rel)); + PageSetLSN(page, XLogGetFakeLSN(rel)); END_CRIT_SECTION(); @@ -528,7 +528,7 @@ gistvacuum_delete_empty_pages(IndexVacuumInfo *info, GistVacState *vstate) RBM_NORMAL, info->strategy); LockBuffer(buffer, GIST_SHARE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (PageIsNew(page) || GistPageIsDeleted(page) || GistPageIsLeaf(page)) { @@ -707,7 +707,7 @@ gistdeletepage(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, if (RelationNeedsWAL(info->index)) recptr = gistXLogPageDelete(leafBuffer, txid, parentBuffer, downlink); else - recptr = gistGetFakeLSN(info->index); + recptr = XLogGetFakeLSN(info->index); PageSetLSN(parentPage, recptr); PageSetLSN(leafPage, recptr); diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c index 2a49e6d20f049..56feb8d840012 100644 --- a/src/backend/access/gist/gistvalidate.c +++ b/src/backend/access/gist/gistvalidate.c @@ -3,7 +3,7 @@ * gistvalidate.c * Opclass validator for GiST. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -138,7 +138,7 @@ gistvalidate(Oid opclassoid) ok = check_amproc_signature(procform->amproc, VOIDOID, true, 1, 1, INTERNALOID); break; - case GIST_STRATNUM_PROC: + case GIST_TRANSLATE_CMPTYPE_PROC: ok = check_amproc_signature(procform->amproc, INT2OID, true, 1, 1, INT4OID) && procform->amproclefttype == ANYOID && @@ -265,7 +265,7 @@ gistvalidate(Oid opclassoid) if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC || i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC || i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC || - i == GIST_STRATNUM_PROC) + i == GIST_TRANSLATE_CMPTYPE_PROC) continue; /* optional methods */ ereport(INFO, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), @@ -336,7 +336,7 @@ gistadjustmembers(Oid opfamilyoid, case GIST_FETCH_PROC: case GIST_OPTIONS_PROC: case GIST_SORTSUPPORT_PROC: - case GIST_STRATNUM_PROC: + case GIST_TRANSLATE_CMPTYPE_PROC: /* Optional, so force it to be a soft family dependency */ op->ref_is_hard = false; op->ref_is_family = true; diff --git a/src/backend/access/gist/gistxlog.c b/src/backend/access/gist/gistxlog.c index b354e4ba5d1b7..ae538dc81ca1e 100644 --- a/src/backend/access/gist/gistxlog.c +++ b/src/backend/access/gist/gistxlog.c @@ -4,7 +4,7 @@ * WAL replay logic for GiST. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -83,7 +83,7 @@ gistRedoPageUpdateRecord(XLogReaderState *record) data = begin = XLogRecGetBlockData(record, 0, &datalen); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (xldata->ntodelete == 1 && xldata->ntoinsert == 1) { @@ -98,9 +98,8 @@ gistRedoPageUpdateRecord(XLogReaderState *record) data += sizeof(OffsetNumber); itup = (IndexTuple) data; itupsize = IndexTupleSize(itup); - if (!PageIndexTupleOverwrite(page, offnum, (Item) itup, itupsize)) - elog(ERROR, "failed to add item to GiST index page, size %d bytes", - (int) itupsize); + if (!PageIndexTupleOverwrite(page, offnum, itup, itupsize)) + elog(ERROR, "failed to add item to GiST index page, size %zu bytes", itupsize); data += itupsize; /* should be nothing left after consuming 1 tuple */ Assert(data - begin == datalen); @@ -133,10 +132,9 @@ gistRedoPageUpdateRecord(XLogReaderState *record) data += sz; - l = PageAddItem(page, (Item) itup, sz, off, false, false); + l = PageAddItem(page, itup, sz, off, false, false); if (l == InvalidOffsetNumber) - elog(ERROR, "failed to add item to GiST index page, size %d bytes", - (int) sz); + elog(ERROR, "failed to add item to GiST index page, size %zu bytes", sz); off++; ninserted++; } @@ -201,7 +199,7 @@ gistRedoDeleteRecord(XLogReaderState *record) if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) { - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); PageIndexMultiDelete(page, toDelete, xldata->ntodelete); @@ -280,7 +278,7 @@ gistRedoPageSplitRecord(XLogReaderState *record) } buffer = XLogInitBufferForRedo(record, i + 1); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); data = XLogRecGetBlockData(record, i + 1, &datalen); tuples = decodePageSplitRecord(data, datalen, &num); @@ -348,7 +346,7 @@ gistRedoPageDelete(XLogReaderState *record) if (XLogReadBufferForRedo(record, 0, &leafBuffer) == BLK_NEEDS_REDO) { - Page page = (Page) BufferGetPage(leafBuffer); + Page page = BufferGetPage(leafBuffer); GistPageSetDeleted(page, xldata->deleteXid); @@ -358,7 +356,7 @@ gistRedoPageDelete(XLogReaderState *record) if (XLogReadBufferForRedo(record, 1, &parentBuffer) == BLK_NEEDS_REDO) { - Page page = (Page) BufferGetPage(parentBuffer); + Page page = BufferGetPage(parentBuffer); PageIndexTupleDelete(page, xldata->downlinkOffset); @@ -423,9 +421,6 @@ gist_redo(XLogReaderState *record) case XLOG_GIST_PAGE_DELETE: gistRedoPageDelete(record); break; - case XLOG_GIST_ASSIGN_LSN: - /* nop. See gistGetFakeLSN(). */ - break; default: elog(PANIC, "gist_redo: unknown op code %u", info); } @@ -569,24 +564,6 @@ gistXLogPageDelete(Buffer buffer, FullTransactionId xid, return recptr; } -/* - * Write an empty XLOG record to assign a distinct LSN. - */ -XLogRecPtr -gistXLogAssignLSN(void) -{ - int dummy = 0; - - /* - * Records other than XLOG_SWITCH must have content. We use an integer 0 - * to follow the restriction. - */ - XLogBeginInsert(); - XLogSetRecordFlags(XLOG_MARK_UNIMPORTANT); - XLogRegisterData(&dummy, sizeof(dummy)); - return XLogInsert(RM_GIST_ID, XLOG_GIST_ASSIGN_LSN); -} - /* * Write XLOG record about reuse of a deleted page. */ diff --git a/src/backend/access/gist/meson.build b/src/backend/access/gist/meson.build index 14c37c5eb1b9b..d4eb58e6f73dd 100644 --- a/src/backend/access/gist/meson.build +++ b/src/backend/access/gist/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'gist.c', diff --git a/src/backend/access/hash/README b/src/backend/access/hash/README index 13dc59c124a75..fc9031117c98b 100644 --- a/src/backend/access/hash/README +++ b/src/backend/access/hash/README @@ -171,11 +171,10 @@ Metapage Caching Both scanning the index and inserting tuples require locating the bucket where a given tuple ought to be located. To do this, we need the bucket count, highmask, and lowmask from the metapage; however, it's undesirable -for performance reasons to have to have to lock and pin the metapage for -every such operation. Instead, we retain a cached copy of the metapage -in each backend's relcache entry. This will produce the correct -bucket mapping as long as the target bucket hasn't been split since the -last cache refresh. +for performance reasons to have to lock and pin the metapage for every such +operation. Instead, we retain a cached copy of the metapage in each backend's +relcache entry. This will produce the correct bucket mapping as long as the +target bucket hasn't been split since the last cache refresh. To guard against the possibility that such a split has occurred, the primary page of each bucket chain stores the number of buckets that diff --git a/src/backend/access/hash/hash.c b/src/backend/access/hash/hash.c index 53061c819fbf0..8d8cd30dc386b 100644 --- a/src/backend/access/hash/hash.c +++ b/src/backend/access/hash/hash.c @@ -3,7 +3,7 @@ * hash.c * Implementation of Margo Seltzer's Hashing package for postgres. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -30,6 +30,7 @@ #include "nodes/execnodes.h" #include "optimizer/plancat.h" #include "pgstat.h" +#include "storage/read_stream.h" #include "utils/fmgrprotos.h" #include "utils/index_selfuncs.h" #include "utils/rel.h" @@ -42,12 +43,23 @@ typedef struct Relation heapRel; /* heap relation descriptor */ } HashBuildState; +/* Working state for streaming reads in hashbulkdelete */ +typedef struct +{ + HashMetaPage metap; /* cached metapage for BUCKET_TO_BLKNO */ + Bucket next_bucket; /* next bucket to prefetch */ + Bucket max_bucket; /* stop when next_bucket > max_bucket */ +} HashBulkDeleteStreamPrivate; + static void hashbuildCallback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *state); +static BlockNumber hash_bulkdelete_read_stream_cb(ReadStream *stream, + void *callback_private_data, + void *per_buffer_data); /* @@ -57,62 +69,63 @@ static void hashbuildCallback(Relation index, Datum hashhandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = HTMaxStrategyNumber; - amroutine->amsupport = HASHNProcs; - amroutine->amoptsprocnum = HASHOPTIONS_PROC; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = false; - amroutine->amcanhash = true; - amroutine->amconsistentequality = true; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = true; - amroutine->amcanunique = false; - amroutine->amcanmulticol = false; - amroutine->amoptionalkey = false; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = false; - amroutine->amstorage = false; - amroutine->amclusterable = false; - amroutine->ampredlocks = true; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = false; - amroutine->amcaninclude = false; - amroutine->amusemaintenanceworkmem = false; - amroutine->amsummarizing = false; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_BULKDEL; - amroutine->amkeytype = INT4OID; - - amroutine->ambuild = hashbuild; - amroutine->ambuildempty = hashbuildempty; - amroutine->aminsert = hashinsert; - amroutine->aminsertcleanup = NULL; - amroutine->ambulkdelete = hashbulkdelete; - amroutine->amvacuumcleanup = hashvacuumcleanup; - amroutine->amcanreturn = NULL; - amroutine->amcostestimate = hashcostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = hashoptions; - amroutine->amproperty = NULL; - amroutine->ambuildphasename = NULL; - amroutine->amvalidate = hashvalidate; - amroutine->amadjustmembers = hashadjustmembers; - amroutine->ambeginscan = hashbeginscan; - amroutine->amrescan = hashrescan; - amroutine->amgettuple = hashgettuple; - amroutine->amgetbitmap = hashgetbitmap; - amroutine->amendscan = hashendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - amroutine->amtranslatestrategy = hashtranslatestrategy; - amroutine->amtranslatecmptype = hashtranslatecmptype; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = HTMaxStrategyNumber, + .amsupport = HASHNProcs, + .amoptsprocnum = HASHOPTIONS_PROC, + .amcanorder = false, + .amcanorderbyop = false, + .amcanhash = true, + .amconsistentequality = true, + .amconsistentordering = false, + .amcanbackward = true, + .amcanunique = false, + .amcanmulticol = false, + .amoptionalkey = false, + .amsearcharray = false, + .amsearchnulls = false, + .amstorage = false, + .amclusterable = false, + .ampredlocks = true, + .amcanparallel = false, + .amcanbuildparallel = false, + .amcaninclude = false, + .amusemaintenanceworkmem = false, + .amsummarizing = false, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_BULKDEL, + .amkeytype = INT4OID, + + .ambuild = hashbuild, + .ambuildempty = hashbuildempty, + .aminsert = hashinsert, + .aminsertcleanup = NULL, + .ambulkdelete = hashbulkdelete, + .amvacuumcleanup = hashvacuumcleanup, + .amcanreturn = NULL, + .amcostestimate = hashcostestimate, + .amgettreeheight = NULL, + .amoptions = hashoptions, + .amproperty = NULL, + .ambuildphasename = NULL, + .amvalidate = hashvalidate, + .amadjustmembers = hashadjustmembers, + .ambeginscan = hashbeginscan, + .amrescan = hashrescan, + .amgettuple = hashgettuple, + .amgetbitmap = hashgetbitmap, + .amendscan = hashendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + .amtranslatestrategy = hashtranslatestrategy, + .amtranslatecmptype = hashtranslatecmptype, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -193,7 +206,7 @@ hashbuild(Relation heap, Relation index, IndexInfo *indexInfo) /* * Return statistics */ - result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + result = palloc_object(IndexBuildResult); result->heap_tuples = reltuples; result->index_tuples = buildstate.indtuples; @@ -318,8 +331,7 @@ hashgettuple(IndexScanDesc scan, ScanDirection dir) * entries. */ if (so->killedItems == NULL) - so->killedItems = (int *) - palloc(MaxIndexTuplesPerPage * sizeof(int)); + so->killedItems = palloc_array(int, MaxIndexTuplesPerPage); if (so->numKilled < MaxIndexTuplesPerPage) so->killedItems[so->numKilled++] = so->currPos.itemIndex; @@ -381,7 +393,7 @@ hashbeginscan(Relation rel, int nkeys, int norderbys) scan = RelationGetIndexScan(rel, nkeys, norderbys); - so = (HashScanOpaque) palloc(sizeof(HashScanOpaqueData)); + so = (HashScanOpaque) palloc_object(HashScanOpaqueData); HashScanPosInvalidate(so->currPos); so->hashso_bucket_buf = InvalidBuffer; so->hashso_split_bucket_buf = InvalidBuffer; @@ -451,6 +463,27 @@ hashendscan(IndexScanDesc scan) scan->opaque = NULL; } +/* + * Read stream callback for hashbulkdelete. + * + * Returns the block number of the primary page for the next bucket to + * vacuum, using the BUCKET_TO_BLKNO mapping from the cached metapage. + */ +static BlockNumber +hash_bulkdelete_read_stream_cb(ReadStream *stream, + void *callback_private_data, + void *per_buffer_data) +{ + HashBulkDeleteStreamPrivate *p = callback_private_data; + Bucket bucket; + + if (p->next_bucket > p->max_bucket) + return InvalidBlockNumber; + + bucket = p->next_bucket++; + return BUCKET_TO_BLKNO(p->metap, bucket); +} + /* * Bulk deletion of all index entries pointing to a set of heap tuples. * The set of target tuples is specified via a callback routine that tells @@ -475,6 +508,9 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, Buffer metabuf = InvalidBuffer; HashMetaPage metap; HashMetaPage cachedmetap; + HashBulkDeleteStreamPrivate stream_private; + ReadStream *stream = NULL; + XLogRecPtr recptr; tuples_removed = 0; num_index_tuples = 0; @@ -495,7 +531,25 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, cur_bucket = 0; cur_maxbucket = orig_maxbucket; -loop_top: + /* Set up streaming read for primary bucket pages */ + stream_private.metap = cachedmetap; + stream_private.next_bucket = cur_bucket; + stream_private.max_bucket = cur_maxbucket; + + /* + * It is safe to use batchmode as hash_bulkdelete_read_stream_cb takes no + * locks. + */ + stream = read_stream_begin_relation(READ_STREAM_MAINTENANCE | + READ_STREAM_USE_BATCHING, + info->strategy, + rel, + MAIN_FORKNUM, + hash_bulkdelete_read_stream_cb, + &stream_private, + 0); + +bucket_loop: while (cur_bucket <= cur_maxbucket) { BlockNumber bucket_blkno; @@ -515,7 +569,8 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, * We need to acquire a cleanup lock on the primary bucket page to out * wait concurrent scans before deleting the dead tuples. */ - buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, info->strategy); + buf = read_stream_next_buffer(stream, NULL); + Assert(BufferIsValid(buf)); LockBufferForCleanup(buf); _hash_checkpage(rel, buf, LH_BUCKET_PAGE); @@ -546,6 +601,16 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, { cachedmetap = _hash_getcachedmetap(rel, &metabuf, true); Assert(cachedmetap != NULL); + + /* + * Reset stream with updated metadata for remaining buckets. + * The BUCKET_TO_BLKNO mapping depends on hashm_spares[], + * which may have changed. + */ + stream_private.metap = cachedmetap; + stream_private.next_bucket = cur_bucket + 1; + stream_private.max_bucket = cur_maxbucket; + read_stream_reset(stream); } } @@ -578,9 +643,19 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, cachedmetap = _hash_getcachedmetap(rel, &metabuf, true); Assert(cachedmetap != NULL); cur_maxbucket = cachedmetap->hashm_maxbucket; - goto loop_top; + + /* Reset stream to process additional buckets from split */ + stream_private.metap = cachedmetap; + stream_private.next_bucket = cur_bucket; + stream_private.max_bucket = cur_maxbucket; + read_stream_reset(stream); + goto bucket_loop; } + /* Stream should be exhausted since we processed all buckets */ + Assert(read_stream_next_buffer(stream, NULL) == InvalidBuffer); + read_stream_end(stream); + /* Okay, we're really done. Update tuple count in metapage. */ START_CRIT_SECTION(); @@ -614,7 +689,6 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, if (RelationNeedsWAL(rel)) { xl_hash_update_meta_page xlrec; - XLogRecPtr recptr; xlrec.ntuples = metap->hashm_ntuples; @@ -624,8 +698,11 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, XLogRegisterBuffer(0, metabuf, REGBUF_STANDARD); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_UPDATE_META_PAGE); - PageSetLSN(BufferGetPage(metabuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(metabuf), recptr); END_CRIT_SECTION(); @@ -633,7 +710,7 @@ hashbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, /* return statistics */ if (stats == NULL) - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); stats->estimated_count = false; stats->num_index_tuples = num_index_tuples; stats->tuples_removed += tuples_removed; @@ -698,6 +775,7 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf, Buffer buf; Bucket new_bucket PG_USED_FOR_ASSERTS_ONLY = InvalidBucket; bool bucket_dirty = false; + XLogRecPtr recptr; blkno = bucket_blkno; buf = bucket_buf; @@ -820,7 +898,6 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf, if (RelationNeedsWAL(rel)) { xl_hash_delete xlrec; - XLogRecPtr recptr; xlrec.clear_dead_marking = clear_dead_marking; xlrec.is_primary_bucket_page = (buf == bucket_buf); @@ -845,8 +922,11 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf, ndeletable * sizeof(OffsetNumber)); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_DELETE); - PageSetLSN(BufferGetPage(buf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(buf), recptr); END_CRIT_SECTION(); } @@ -905,14 +985,15 @@ hashbucketcleanup(Relation rel, Bucket cur_bucket, Buffer bucket_buf, /* XLOG stuff */ if (RelationNeedsWAL(rel)) { - XLogRecPtr recptr; - XLogBeginInsert(); XLogRegisterBuffer(0, bucket_buf, REGBUF_STANDARD); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SPLIT_CLEANUP); - PageSetLSN(page, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(page, recptr); END_CRIT_SECTION(); } diff --git a/src/backend/access/hash/hash_xlog.c b/src/backend/access/hash/hash_xlog.c index 8d97067fe5403..2060620c7dec9 100644 --- a/src/backend/access/hash/hash_xlog.c +++ b/src/backend/access/hash/hash_xlog.c @@ -4,7 +4,7 @@ * WAL replay logic for hash index. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -38,7 +38,7 @@ hash_xlog_init_meta_page(XLogReaderState *record) Assert(BufferIsValid(metabuf)); _hash_init_metabuffer(metabuf, xlrec->num_tuples, xlrec->procid, xlrec->ffactor, true); - page = (Page) BufferGetPage(metabuf); + page = BufferGetPage(metabuf); PageSetLSN(page, lsn); MarkBufferDirty(metabuf); @@ -137,8 +137,7 @@ hash_xlog_insert(XLogReaderState *record) page = BufferGetPage(buffer); - if (PageAddItem(page, (Item) datapos, datalen, xlrec->offnum, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, datapos, datalen, xlrec->offnum, false, false) == InvalidOffsetNumber) elog(PANIC, "hash_xlog_insert: failed to add item"); PageSetLSN(page, lsn); @@ -235,7 +234,7 @@ hash_xlog_add_ovfl_page(XLogReaderState *record) if (XLogReadBufferForRedo(record, 2, &mapbuffer) == BLK_NEEDS_REDO) { - Page mappage = (Page) BufferGetPage(mapbuffer); + Page mappage = BufferGetPage(mapbuffer); uint32 *freep = NULL; uint32 *bitmap_page_bit; @@ -315,8 +314,6 @@ hash_xlog_split_allocate_page(XLogReaderState *record) Buffer oldbuf; Buffer newbuf; Buffer metabuf; - Size datalen PG_USED_FOR_ASSERTS_ONLY; - char *data; XLogRedoAction action; /* @@ -376,6 +373,10 @@ hash_xlog_split_allocate_page(XLogReaderState *record) { Page page; HashMetaPage metap; + Size datalen; + char *data; + uint32 *uidata; + int uidatacount; page = BufferGetPage(metabuf); metap = HashPageGetMeta(page); @@ -383,34 +384,31 @@ hash_xlog_split_allocate_page(XLogReaderState *record) data = XLogRecGetBlockData(record, 2, &datalen); + /* + * This cast is ok because XLogRecGetBlockData() returns a MAXALIGNed + * buffer. + */ + uidata = (uint32 *) data; + uidatacount = 0; + if (xlrec->flags & XLH_SPLIT_META_UPDATE_MASKS) { - uint32 lowmask; - uint32 *highmask; - - /* extract low and high masks. */ - memcpy(&lowmask, data, sizeof(uint32)); - highmask = (uint32 *) ((char *) data + sizeof(uint32)); + uint32 lowmask = uidata[uidatacount++]; + uint32 highmask = uidata[uidatacount++]; /* update metapage */ metap->hashm_lowmask = lowmask; - metap->hashm_highmask = *highmask; - - data += sizeof(uint32) * 2; + metap->hashm_highmask = highmask; } if (xlrec->flags & XLH_SPLIT_META_UPDATE_SPLITPOINT) { - uint32 ovflpoint; - uint32 *ovflpages; - - /* extract information of overflow pages. */ - memcpy(&ovflpoint, data, sizeof(uint32)); - ovflpages = (uint32 *) ((char *) data + sizeof(uint32)); + uint32 ovflpoint = uidata[uidatacount++]; + uint32 ovflpages = uidata[uidatacount++]; /* update metapage */ - metap->hashm_spares[ovflpoint] = *ovflpages; metap->hashm_ovflpoint = ovflpoint; + metap->hashm_spares[ovflpoint] = ovflpages; } MarkBufferDirty(metabuf); @@ -538,7 +536,7 @@ hash_xlog_move_page_contents(XLogReaderState *record) data = begin = XLogRecGetBlockData(record, 1, &datalen); - writepage = (Page) BufferGetPage(writebuf); + writepage = BufferGetPage(writebuf); if (xldata->ntups > 0) { @@ -557,10 +555,9 @@ hash_xlog_move_page_contents(XLogReaderState *record) data += itemsz; - l = PageAddItem(writepage, (Item) itup, itemsz, towrite[ninserted], false, false); + l = PageAddItem(writepage, itup, itemsz, towrite[ninserted], false, false); if (l == InvalidOffsetNumber) - elog(ERROR, "hash_xlog_move_page_contents: failed to add item to hash index page, size %d bytes", - (int) itemsz); + elog(ERROR, "hash_xlog_move_page_contents: failed to add item to hash index page, size %zu bytes", itemsz); ninserted++; } @@ -584,7 +581,7 @@ hash_xlog_move_page_contents(XLogReaderState *record) ptr = XLogRecGetBlockData(record, 2, &len); - page = (Page) BufferGetPage(deletebuf); + page = BufferGetPage(deletebuf); if (len > 0) { @@ -592,7 +589,7 @@ hash_xlog_move_page_contents(XLogReaderState *record) OffsetNumber *unend; unused = (OffsetNumber *) ptr; - unend = (OffsetNumber *) ((char *) ptr + len); + unend = (OffsetNumber *) (ptr + len); if ((unend - unused) > 0) PageIndexMultiDelete(page, unused, unend - unused); @@ -670,7 +667,7 @@ hash_xlog_squeeze_page(XLogReaderState *record) data = begin = XLogRecGetBlockData(record, 1, &datalen); - writepage = (Page) BufferGetPage(writebuf); + writepage = BufferGetPage(writebuf); if (xldata->ntups > 0) { @@ -689,10 +686,9 @@ hash_xlog_squeeze_page(XLogReaderState *record) data += itemsz; - l = PageAddItem(writepage, (Item) itup, itemsz, towrite[ninserted], false, false); + l = PageAddItem(writepage, itup, itemsz, towrite[ninserted], false, false); if (l == InvalidOffsetNumber) - elog(ERROR, "hash_xlog_squeeze_page: failed to add item to hash index page, size %d bytes", - (int) itemsz); + elog(ERROR, "hash_xlog_squeeze_page: failed to add item to hash index page, size %zu bytes", itemsz); ninserted++; } @@ -807,7 +803,7 @@ hash_xlog_squeeze_page(XLogReaderState *record) /* replay the record for bitmap page */ if (XLogReadBufferForRedo(record, 5, &mapbuf) == BLK_NEEDS_REDO) { - Page mappage = (Page) BufferGetPage(mapbuf); + Page mappage = BufferGetPage(mapbuf); uint32 *freep = NULL; char *data; uint32 *bitmap_page_bit; @@ -895,7 +891,7 @@ hash_xlog_delete(XLogReaderState *record) ptr = XLogRecGetBlockData(record, 1, &len); - page = (Page) BufferGetPage(deletebuf); + page = BufferGetPage(deletebuf); if (len > 0) { @@ -903,7 +899,7 @@ hash_xlog_delete(XLogReaderState *record) OffsetNumber *unend; unused = (OffsetNumber *) ptr; - unend = (OffsetNumber *) ((char *) ptr + len); + unend = (OffsetNumber *) (ptr + len); if ((unend - unused) > 0) PageIndexMultiDelete(page, unused, unend - unused); @@ -946,7 +942,7 @@ hash_xlog_split_cleanup(XLogReaderState *record) { HashPageOpaque bucket_opaque; - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); bucket_opaque = HashPageGetOpaque(page); bucket_opaque->hasho_flag &= ~LH_BUCKET_NEEDS_SPLIT_CLEANUP; @@ -1029,7 +1025,7 @@ hash_xlog_vacuum_one_page(XLogReaderState *record) if (action == BLK_NEEDS_REDO) { - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); PageIndexMultiDelete(page, toDelete, xldata->ntuples); diff --git a/src/backend/access/hash/hashfunc.c b/src/backend/access/hash/hashfunc.c index ec96348942ee7..575342a21b6b0 100644 --- a/src/backend/access/hash/hashfunc.c +++ b/src/backend/access/hash/hashfunc.c @@ -3,7 +3,7 @@ * hashfunc.c * Support functions for hash access method. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -27,6 +27,7 @@ #include "postgres.h" #include "common/hashfn.h" +#include "utils/builtins.h" #include "utils/float.h" #include "utils/fmgrprotos.h" #include "utils/pg_locale.h" @@ -233,6 +234,7 @@ hashoidvector(PG_FUNCTION_ARGS) { oidvector *key = (oidvector *) PG_GETARG_POINTER(0); + check_valid_oidvector(key); return hash_any((unsigned char *) key->values, key->dim1 * sizeof(Oid)); } @@ -241,6 +243,7 @@ hashoidvectorextended(PG_FUNCTION_ARGS) { oidvector *key = (oidvector *) PG_GETARG_POINTER(0); + check_valid_oidvector(key); return hash_any_extended((unsigned char *) key->values, key->dim1 * sizeof(Oid), PG_GETARG_INT64(1)); @@ -385,7 +388,7 @@ hashtextextended(PG_FUNCTION_ARGS) Datum hashvarlena(PG_FUNCTION_ARGS) { - struct varlena *key = PG_GETARG_VARLENA_PP(0); + varlena *key = PG_GETARG_VARLENA_PP(0); Datum result; result = hash_any((unsigned char *) VARDATA_ANY(key), @@ -400,7 +403,7 @@ hashvarlena(PG_FUNCTION_ARGS) Datum hashvarlenaextended(PG_FUNCTION_ARGS) { - struct varlena *key = PG_GETARG_VARLENA_PP(0); + varlena *key = PG_GETARG_VARLENA_PP(0); Datum result; result = hash_any_extended((unsigned char *) VARDATA_ANY(key), diff --git a/src/backend/access/hash/hashinsert.c b/src/backend/access/hash/hashinsert.c index 10de1580dc211..3395bbc13f825 100644 --- a/src/backend/access/hash/hashinsert.c +++ b/src/backend/access/hash/hashinsert.c @@ -3,7 +3,7 @@ * hashinsert.c * Item insertion in hash tables for Postgres. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -50,6 +50,7 @@ _hash_doinsert(Relation rel, IndexTuple itup, Relation heapRel, bool sorted) uint32 hashkey; Bucket bucket; OffsetNumber itup_off; + XLogRecPtr recptr; /* * Get the hash key for the item (it's stored in the index tuple itself). @@ -216,7 +217,6 @@ _hash_doinsert(Relation rel, IndexTuple itup, Relation heapRel, bool sorted) if (RelationNeedsWAL(rel)) { xl_hash_insert xlrec; - XLogRecPtr recptr; xlrec.offnum = itup_off; @@ -229,10 +229,12 @@ _hash_doinsert(Relation rel, IndexTuple itup, Relation heapRel, bool sorted) XLogRegisterBufData(0, itup, IndexTupleSize(itup)); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_INSERT); - - PageSetLSN(BufferGetPage(buf), recptr); - PageSetLSN(BufferGetPage(metabuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(buf), recptr); + PageSetLSN(BufferGetPage(metabuf), recptr); END_CRIT_SECTION(); @@ -310,10 +312,8 @@ _hash_pgaddtup(Relation rel, Buffer buf, Size itemsize, IndexTuple itup, itup_off = _hash_binsearch(page, hashkey); } - if (PageAddItem(page, (Item) itup, itemsize, itup_off, false, false) - == InvalidOffsetNumber) - elog(ERROR, "failed to add index item to \"%s\"", - RelationGetRelationName(rel)); + if (PageAddItem(page, itup, itemsize, itup_off, false, false) == InvalidOffsetNumber) + elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(rel)); return itup_off; } @@ -352,10 +352,8 @@ _hash_pgaddmultitup(Relation rel, Buffer buf, IndexTuple *itups, itup_offsets[i] = itup_off; - if (PageAddItem(page, (Item) itups[i], itemsize, itup_off, false, false) - == InvalidOffsetNumber) - elog(ERROR, "failed to add index item to \"%s\"", - RelationGetRelationName(rel)); + if (PageAddItem(page, itups[i], itemsize, itup_off, false, false) == InvalidOffsetNumber) + elog(ERROR, "failed to add index item to \"%s\"", RelationGetRelationName(rel)); } } @@ -376,6 +374,7 @@ _hash_vacuum_one_page(Relation rel, Relation hrel, Buffer metabuf, Buffer buf) Page page = BufferGetPage(buf); HashPageOpaque pageopaque; HashMetaPage metap; + XLogRecPtr recptr; /* Scan each tuple in page to see if it is marked as LP_DEAD */ maxoff = PageGetMaxOffsetNumber(page); @@ -428,7 +427,6 @@ _hash_vacuum_one_page(Relation rel, Relation hrel, Buffer metabuf, Buffer buf) if (RelationNeedsWAL(rel)) { xl_hash_vacuum_one_page xlrec; - XLogRecPtr recptr; xlrec.isCatalogRel = RelationIsAccessibleInLogicalDecoding(hrel); xlrec.snapshotConflictHorizon = snapshotConflictHorizon; @@ -449,10 +447,12 @@ _hash_vacuum_one_page(Relation rel, Relation hrel, Buffer metabuf, Buffer buf) XLogRegisterBuffer(1, metabuf, REGBUF_STANDARD); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_VACUUM_ONE_PAGE); - - PageSetLSN(BufferGetPage(buf), recptr); - PageSetLSN(BufferGetPage(metabuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(buf), recptr); + PageSetLSN(BufferGetPage(metabuf), recptr); END_CRIT_SECTION(); diff --git a/src/backend/access/hash/hashovfl.c b/src/backend/access/hash/hashovfl.c index 4f5fd3b28372a..dbc57ef958c0d 100644 --- a/src/backend/access/hash/hashovfl.c +++ b/src/backend/access/hash/hashovfl.c @@ -3,7 +3,7 @@ * hashovfl.c * Overflow page management code for the Postgres hash access method * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -132,6 +132,7 @@ _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin) uint32 i, j; bool page_found = false; + XLogRecPtr recptr; /* * Write-lock the tail page. Here, we need to maintain locking order such @@ -381,7 +382,6 @@ _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin) /* XLOG stuff */ if (RelationNeedsWAL(rel)) { - XLogRecPtr recptr; xl_hash_add_ovfl_page xlrec; xlrec.bmpage_found = page_found; @@ -408,18 +408,20 @@ _hash_addovflpage(Relation rel, Buffer metabuf, Buffer buf, bool retain_pin) XLogRegisterBufData(4, &metap->hashm_firstfree, sizeof(uint32)); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_ADD_OVFL_PAGE); + } + else + recptr = XLogGetFakeLSN(rel); - PageSetLSN(BufferGetPage(ovflbuf), recptr); - PageSetLSN(BufferGetPage(buf), recptr); + PageSetLSN(BufferGetPage(ovflbuf), recptr); + PageSetLSN(BufferGetPage(buf), recptr); - if (BufferIsValid(mapbuf)) - PageSetLSN(BufferGetPage(mapbuf), recptr); + if (BufferIsValid(mapbuf)) + PageSetLSN(BufferGetPage(mapbuf), recptr); - if (BufferIsValid(newmapbuf)) - PageSetLSN(BufferGetPage(newmapbuf), recptr); + if (BufferIsValid(newmapbuf)) + PageSetLSN(BufferGetPage(newmapbuf), recptr); - PageSetLSN(BufferGetPage(metabuf), recptr); - } + PageSetLSN(BufferGetPage(metabuf), recptr); END_CRIT_SECTION(); @@ -510,7 +512,11 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, Bucket bucket PG_USED_FOR_ASSERTS_ONLY; Buffer prevbuf = InvalidBuffer; Buffer nextbuf = InvalidBuffer; - bool update_metap = false; + bool update_metap = false, + mod_wbuf, + is_prim_bucket_same_wrt, + is_prev_bucket_same_wrt; + XLogRecPtr recptr; /* Get information from the doomed page */ _hash_checkpage(rel, ovflbuf, LH_OVERFLOW_PAGE); @@ -641,19 +647,21 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, MarkBufferDirty(metabuf); } + /* Determine which pages are modified */ + is_prim_bucket_same_wrt = (wbuf == bucketbuf); + is_prev_bucket_same_wrt = (wbuf == prevbuf); + mod_wbuf = (nitups > 0 || is_prev_bucket_same_wrt); + /* XLOG stuff */ if (RelationNeedsWAL(rel)) { xl_hash_squeeze_page xlrec; - XLogRecPtr recptr; - int i; - bool mod_wbuf = false; xlrec.prevblkno = prevblkno; xlrec.nextblkno = nextblkno; xlrec.ntups = nitups; - xlrec.is_prim_bucket_same_wrt = (wbuf == bucketbuf); - xlrec.is_prev_bucket_same_wrt = (wbuf == prevbuf); + xlrec.is_prim_bucket_same_wrt = is_prim_bucket_same_wrt; + xlrec.is_prev_bucket_same_wrt = is_prev_bucket_same_wrt; XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHashSqueezePage); @@ -662,26 +670,22 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, * bucket buffer was not changed, but still needs to be registered to * ensure that we can acquire a cleanup lock on it during replay. */ - if (!xlrec.is_prim_bucket_same_wrt) + if (!is_prim_bucket_same_wrt) { uint8 flags = REGBUF_STANDARD | REGBUF_NO_IMAGE | REGBUF_NO_CHANGE; XLogRegisterBuffer(0, bucketbuf, flags); } - if (xlrec.ntups > 0) + if (nitups > 0) { XLogRegisterBuffer(1, wbuf, REGBUF_STANDARD); - - /* Remember that wbuf is modified. */ - mod_wbuf = true; - XLogRegisterBufData(1, itup_offsets, nitups * sizeof(OffsetNumber)); - for (i = 0; i < nitups; i++) + for (int i = 0; i < nitups; i++) XLogRegisterBufData(1, itups[i], tups_size[i]); } - else if (xlrec.is_prim_bucket_same_wrt || xlrec.is_prev_bucket_same_wrt) + else if (is_prim_bucket_same_wrt || is_prev_bucket_same_wrt) { uint8 wbuf_flags; @@ -691,18 +695,13 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, * if it is the same as primary bucket buffer or update the * nextblkno if it is same as the previous bucket buffer. */ - Assert(xlrec.ntups == 0); + Assert(nitups == 0); wbuf_flags = REGBUF_STANDARD; - if (!xlrec.is_prev_bucket_same_wrt) - { + if (!is_prev_bucket_same_wrt) wbuf_flags |= REGBUF_NO_CHANGE; - } else - { - /* Remember that wbuf is modified. */ - mod_wbuf = true; - } + Assert(mod_wbuf); XLogRegisterBuffer(1, wbuf, wbuf_flags); } @@ -714,7 +713,7 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, * prevpage. During replay, we can directly update the nextblock in * writepage. */ - if (BufferIsValid(prevbuf) && !xlrec.is_prev_bucket_same_wrt) + if (BufferIsValid(prevbuf) && !is_prev_bucket_same_wrt) XLogRegisterBuffer(3, prevbuf, REGBUF_STANDARD); if (BufferIsValid(nextbuf)) @@ -730,23 +729,25 @@ _hash_freeovflpage(Relation rel, Buffer bucketbuf, Buffer ovflbuf, } recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SQUEEZE_PAGE); + } + else /* !RelationNeedsWAL(rel) */ + recptr = XLogGetFakeLSN(rel); - /* Set LSN iff wbuf is modified. */ - if (mod_wbuf) - PageSetLSN(BufferGetPage(wbuf), recptr); + /* Set LSN iff wbuf is modified. */ + if (mod_wbuf) + PageSetLSN(BufferGetPage(wbuf), recptr); - PageSetLSN(BufferGetPage(ovflbuf), recptr); + PageSetLSN(BufferGetPage(ovflbuf), recptr); - if (BufferIsValid(prevbuf) && !xlrec.is_prev_bucket_same_wrt) - PageSetLSN(BufferGetPage(prevbuf), recptr); - if (BufferIsValid(nextbuf)) - PageSetLSN(BufferGetPage(nextbuf), recptr); + if (BufferIsValid(prevbuf) && !is_prev_bucket_same_wrt) + PageSetLSN(BufferGetPage(prevbuf), recptr); + if (BufferIsValid(nextbuf)) + PageSetLSN(BufferGetPage(nextbuf), recptr); - PageSetLSN(BufferGetPage(mapbuf), recptr); + PageSetLSN(BufferGetPage(mapbuf), recptr); - if (update_metap) - PageSetLSN(BufferGetPage(metabuf), recptr); - } + if (update_metap) + PageSetLSN(BufferGetPage(metabuf), recptr); END_CRIT_SECTION(); @@ -959,6 +960,8 @@ _hash_squeezebucket(Relation rel, if (nitups > 0) { + XLogRecPtr recptr; + Assert(nitups == ndeletable); /* @@ -986,7 +989,6 @@ _hash_squeezebucket(Relation rel, /* XLOG stuff */ if (RelationNeedsWAL(rel)) { - XLogRecPtr recptr; xl_hash_move_page_contents xlrec; xlrec.ntups = nitups; @@ -1018,10 +1020,12 @@ _hash_squeezebucket(Relation rel, ndeletable * sizeof(OffsetNumber)); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_MOVE_PAGE_CONTENTS); - - PageSetLSN(BufferGetPage(wbuf), recptr); - PageSetLSN(BufferGetPage(rbuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(wbuf), recptr); + PageSetLSN(BufferGetPage(rbuf), recptr); END_CRIT_SECTION(); diff --git a/src/backend/access/hash/hashpage.c b/src/backend/access/hash/hashpage.c index b8e5bd005e59a..8099b0d021f05 100644 --- a/src/backend/access/hash/hashpage.c +++ b/src/backend/access/hash/hashpage.c @@ -3,7 +3,7 @@ * hashpage.c * Hash table page management code for the Postgres hash access method * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -630,6 +630,7 @@ _hash_expandtable(Relation rel, Buffer metabuf) uint32 lowmask; bool metap_update_masks = false; bool metap_update_splitpoint = false; + XLogRecPtr recptr; restart_expand: @@ -900,7 +901,6 @@ _hash_expandtable(Relation rel, Buffer metabuf) if (RelationNeedsWAL(rel)) { xl_hash_split_allocate_page xlrec; - XLogRecPtr recptr; xlrec.new_bucket = maxbucket; xlrec.old_bucket_flag = oopaque->hasho_flag; @@ -933,11 +933,13 @@ _hash_expandtable(Relation rel, Buffer metabuf) XLogRegisterData(&xlrec, SizeOfHashSplitAllocPage); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SPLIT_ALLOCATE_PAGE); - - PageSetLSN(BufferGetPage(buf_oblkno), recptr); - PageSetLSN(BufferGetPage(buf_nblkno), recptr); - PageSetLSN(BufferGetPage(metabuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(buf_oblkno), recptr); + PageSetLSN(BufferGetPage(buf_nblkno), recptr); + PageSetLSN(BufferGetPage(metabuf), recptr); END_CRIT_SECTION(); @@ -1029,7 +1031,7 @@ _hash_alloc_buckets(Relation rel, BlockNumber firstblock, uint32 nblocks) zerobuf.data, true); - PageSetChecksumInplace(page, lastblock); + PageSetChecksum(page, lastblock); smgrextend(RelationGetSmgr(rel), MAIN_FORKNUM, lastblock, zerobuf.data, false); @@ -1092,6 +1094,7 @@ _hash_splitbucket(Relation rel, Size all_tups_size = 0; int i; uint16 nitups = 0; + XLogRecPtr recptr; bucket_obuf = obuf; opage = BufferGetPage(obuf); @@ -1296,7 +1299,6 @@ _hash_splitbucket(Relation rel, if (RelationNeedsWAL(rel)) { - XLogRecPtr recptr; xl_hash_split_complete xlrec; xlrec.old_bucket_flag = oopaque->hasho_flag; @@ -1310,10 +1312,12 @@ _hash_splitbucket(Relation rel, XLogRegisterBuffer(1, bucket_nbuf, REGBUF_STANDARD); recptr = XLogInsert(RM_HASH_ID, XLOG_HASH_SPLIT_COMPLETE); - - PageSetLSN(BufferGetPage(bucket_obuf), recptr); - PageSetLSN(BufferGetPage(bucket_nbuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(BufferGetPage(bucket_obuf), recptr); + PageSetLSN(BufferGetPage(bucket_nbuf), recptr); END_CRIT_SECTION(); diff --git a/src/backend/access/hash/hashsearch.c b/src/backend/access/hash/hashsearch.c index 92c15a65be29a..89d1c5bc6d7b2 100644 --- a/src/backend/access/hash/hashsearch.c +++ b/src/backend/access/hash/hashsearch.c @@ -3,7 +3,7 @@ * hashsearch.c * search code for postgres hash tables * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include "access/hash.h" #include "access/relscan.h" #include "miscadmin.h" +#include "executor/instrument_node.h" #include "pgstat.h" #include "storage/predicate.h" #include "utils/rel.h" diff --git a/src/backend/access/hash/hashsort.c b/src/backend/access/hash/hashsort.c index 6e8c0e68a92c8..77bbfaa461b14 100644 --- a/src/backend/access/hash/hashsort.c +++ b/src/backend/access/hash/hashsort.c @@ -14,7 +14,7 @@ * plenty of locality of access. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -59,7 +59,7 @@ struct HSpool HSpool * _h_spoolinit(Relation heap, Relation index, uint32 num_buckets) { - HSpool *hspool = (HSpool *) palloc0(sizeof(HSpool)); + HSpool *hspool = palloc0_object(HSpool); hspool->index = index; @@ -106,7 +106,7 @@ _h_spooldestroy(HSpool *hspool) * spool an index entry into the sort file. */ void -_h_spool(HSpool *hspool, ItemPointer self, const Datum *values, const bool *isnull) +_h_spool(HSpool *hspool, const ItemPointerData *self, const Datum *values, const bool *isnull) { tuplesort_putindextuplevalues(hspool->sortstate, hspool->index, self, values, isnull); diff --git a/src/backend/access/hash/hashutil.c b/src/backend/access/hash/hashutil.c index 66c39f606540b..1d1b05f876ad3 100644 --- a/src/backend/access/hash/hashutil.c +++ b/src/backend/access/hash/hashutil.c @@ -3,7 +3,7 @@ * hashutil.c * Utility code for Postgres hash implementation. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -316,7 +316,7 @@ _hash_get_indextuple_hashkey(IndexTuple itup) */ bool _hash_convert_tuple(Relation index, - Datum *user_values, bool *user_isnull, + const Datum *user_values, const bool *user_isnull, Datum *index_values, bool *index_isnull) { uint32 hashkey; @@ -593,6 +593,17 @@ _hash_kill_items(IndexScanDesc scan) if (ItemPointerEquals(&ituple->t_tid, &currItem->heapTid)) { + if (!killedsomething) + { + /* + * Use the hint bit infrastructure to check if we can + * update the page while just holding a share lock. If we + * are not allowed, there's no point continuing. + */ + if (!BufferBeginSetHintBits(buf)) + goto unlock_page; + } + /* found the item */ ItemIdMarkDead(iid); killedsomething = true; @@ -610,11 +621,11 @@ _hash_kill_items(IndexScanDesc scan) if (killedsomething) { opaque->hasho_flag |= LH_PAGE_HAS_DEAD_TUPLES; - MarkBufferDirtyHint(buf, true); + BufferFinishSetHintBits(buf, true, true); } - if (so->hashso_bucket_buf == so->currPos.buf || - havePin) +unlock_page: + if (havePin) LockBuffer(so->currPos.buf, BUFFER_LOCK_UNLOCK); else _hash_relbuf(rel, buf); diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c index 06ac832ba10f8..87b4015827759 100644 --- a/src/backend/access/hash/hashvalidate.c +++ b/src/backend/access/hash/hashvalidate.c @@ -3,7 +3,7 @@ * hashvalidate.c * Opclass validator for hash. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/hash/meson.build b/src/backend/access/hash/meson.build index 5f933faa6c9fe..ad011b8f99ab6 100644 --- a/src/backend/access/hash/meson.build +++ b/src/backend/access/hash/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'hash.c', diff --git a/src/backend/access/heap/Makefile b/src/backend/access/heap/Makefile index 394534172fa1a..1d27ccb916e09 100644 --- a/src/backend/access/heap/Makefile +++ b/src/backend/access/heap/Makefile @@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global OBJS = \ heapam.o \ heapam_handler.o \ + heapam_indexscan.o \ heapam_visibility.o \ heapam_xlog.o \ heaptoast.o \ diff --git a/src/backend/access/heap/README.tuplock b/src/backend/access/heap/README.tuplock index 843c2e58f929d..16f7d78b7d232 100644 --- a/src/backend/access/heap/README.tuplock +++ b/src/backend/access/heap/README.tuplock @@ -199,3 +199,35 @@ under a reader holding a pin. A reader of a heap_fetch() result tuple may witness a torn read. Current inplace-updated fields are aligned and are no wider than four bytes, and current readers don't need consistency across fields. Hence, they get by with just fetching each field once. + +During logical decoding, caches reflect an inplace update no later than the +next XLOG_XACT_INVALIDATIONS. That record witnesses the end of a command. +Tuples of its cmin are then visible to decoding, as are inplace updates of any +lower LSN. Inplace updates of a higher LSN may also be visible, even if those +updates would have been invisible to a non-historic snapshot matching +decoding's historic snapshot. (In other words, decoding may see inplace +updates that were not visible to a similar snapshot taken during original +transaction processing.) That's a consequence of inplace update violating +MVCC: there are no snapshot-specific versions of inplace-updated values. This +all makes it hard to reason about inplace-updated column reads during logical +decoding, but the behavior does suffice for relhasindex. A relhasindex=t in +CREATE INDEX becomes visible no later than the new pg_index row. While it may +be visible earlier, that's harmless. Finding zero indexes despite +relhasindex=t is normal in more cases than this, e.g. after DROP INDEX. +Example of a case that meaningfully reacts to the inplace inval: + +CREATE TABLE cat (c int) WITH (user_catalog_table = true); +CREATE TABLE normal (d int); +... +CREATE INDEX ON cat (c)\; INSERT INTO normal VALUES (1); + +If the output plugin reads "cat" during decoding of the INSERT, it's fair to +want that read to see relhasindex=t and use the new index. + +An alternative would be to have decoding of XLOG_HEAP_INPLACE immediately +execute its invals. That would behave more like invals during original +transaction processing. It would remove the decoding-specific delay in e.g. a +decoding plugin witnessing a relfrozenxid change. However, a good use case +for that is unlikely, since the plugin would still witness relfrozenxid +changes prematurely. Hence, inplace update takes the trivial approach of +delegating to XLOG_XACT_INVALIDATIONS. diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 9ec8cda1c6801..abfd8e8970a60 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -3,7 +3,7 @@ * heapam.c * heap access method code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -43,10 +43,12 @@ #include "catalog/pg_database.h" #include "catalog/pg_database_d.h" #include "commands/vacuum.h" +#include "executor/instrument_node.h" #include "pgstat.h" #include "port/pg_bitutils.h" #include "storage/lmgr.h" #include "storage/predicate.h" +#include "storage/proc.h" #include "storage/procarray.h" #include "utils/datum.h" #include "utils/injection_point.h" @@ -56,14 +58,15 @@ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup, - TransactionId xid, CommandId cid, int options); + TransactionId xid, CommandId cid, uint32 options); static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, - bool all_visible_cleared, bool new_all_visible_cleared); + bool all_visible_cleared, bool new_all_visible_cleared, + bool walLogical); #ifdef USE_ASSERT_CHECKING static void check_lock_if_inplace_updateable_rel(Relation relation, - ItemPointer otid, + const ItemPointerData *otid, HeapTuple newtup); static void check_inplace_rel_lock(HeapTuple oldtup); #endif @@ -72,7 +75,7 @@ static Bitmapset *HeapDetermineColumnsInfo(Relation relation, Bitmapset *external_cols, HeapTuple oldtup, HeapTuple newtup, bool *has_external); -static bool heap_acquire_tuplock(Relation relation, ItemPointer tid, +static bool heap_acquire_tuplock(Relation relation, const ItemPointerData *tid, LockTupleMode mode, LockWaitPolicy wait_policy, bool *have_tuple_lock); static inline BlockNumber heapgettup_advance_block(HeapScanDesc scan, @@ -85,8 +88,11 @@ static void compute_new_xmax_infomask(TransactionId xmax, uint16 old_infomask, LockTupleMode mode, bool is_update, TransactionId *result_xmax, uint16 *result_infomask, uint16 *result_infomask2); -static TM_Result heap_lock_updated_tuple(Relation rel, HeapTuple tuple, - ItemPointer ctid, TransactionId xid, +static TM_Result heap_lock_updated_tuple(Relation rel, + uint16 prior_infomask, + TransactionId prior_raw_xmax, + const ItemPointerData *prior_ctid, + TransactionId xid, LockTupleMode mode); static void GetMultiXactIdHintBits(MultiXactId multi, uint16 *new_infomask, uint16 *new_infomask2); @@ -95,7 +101,7 @@ static TransactionId MultiXactIdGetUpdateXid(TransactionId xmax, static bool DoesMultiXactIdConflict(MultiXactId multi, uint16 infomask, LockTupleMode lockmode, bool *current_is_member); static void MultiXactIdWait(MultiXactId multi, MultiXactStatus status, uint16 infomask, - Relation rel, ItemPointer ctid, XLTW_Oper oper, + Relation rel, const ItemPointerData *ctid, XLTW_Oper oper, int *remaining); static bool ConditionalMultiXactIdWait(MultiXactId multi, MultiXactStatus status, uint16 infomask, Relation rel, int *remaining, @@ -108,11 +114,11 @@ static HeapTuple ExtractReplicaIdentity(Relation relation, HeapTuple tp, bool ke /* - * Each tuple lock mode has a corresponding heavyweight lock, and one or two - * corresponding MultiXactStatuses (one to merely lock tuples, another one to - * update them). This table (and the macros below) helps us determine the - * heavyweight lock mode and MultiXactStatus values to use for any particular - * tuple lock strength. + * This table lists the heavyweight lock mode that corresponds to each tuple + * lock mode, as well as one or two corresponding MultiXactStatus values: + * .lockstatus to merely lock tuples, and .updstatus to update them. The + * latter is set to -1 if the corresponding tuple lock mode does not allow + * updating tuples -- see get_mxact_status_for_lock(). * * These interact with InplaceUpdateTupleLock, an alias for ExclusiveLock. * @@ -124,29 +130,30 @@ static const struct LOCKMODE hwlock; int lockstatus; int updstatus; -} +} tupleLockExtraInfo[] = - tupleLockExtraInfo[MaxLockTupleMode + 1] = { - { /* LockTupleKeyShare */ - AccessShareLock, - MultiXactStatusForKeyShare, - -1 /* KeyShare does not allow updating tuples */ + [LockTupleKeyShare] = { + .hwlock = AccessShareLock, + .lockstatus = MultiXactStatusForKeyShare, + /* KeyShare does not allow updating tuples */ + .updstatus = -1 }, - { /* LockTupleShare */ - RowShareLock, - MultiXactStatusForShare, - -1 /* Share does not allow updating tuples */ + [LockTupleShare] = { + .hwlock = RowShareLock, + .lockstatus = MultiXactStatusForShare, + /* Share does not allow updating tuples */ + .updstatus = -1 }, - { /* LockTupleNoKeyExclusive */ - ExclusiveLock, - MultiXactStatusForNoKeyUpdate, - MultiXactStatusNoKeyUpdate + [LockTupleNoKeyExclusive] = { + .hwlock = ExclusiveLock, + .lockstatus = MultiXactStatusForNoKeyUpdate, + .updstatus = MultiXactStatusNoKeyUpdate }, - { /* LockTupleExclusive */ - AccessExclusiveLock, - MultiXactStatusForUpdate, - MultiXactStatusUpdate + [LockTupleExclusive] = { + .hwlock = AccessExclusiveLock, + .lockstatus = MultiXactStatusForUpdate, + .updstatus = MultiXactStatusUpdate } }; @@ -213,6 +220,27 @@ static const int MultiXactStatusLock[MaxMultiXactStatus + 1] = #define TUPLOCK_from_mxstatus(status) \ (MultiXactStatusLock[(status)]) +/* + * Check that we have a valid snapshot if we might need TOAST access. + */ +static inline void +AssertHasSnapshotForToast(Relation rel) +{ +#ifdef USE_ASSERT_CHECKING + + /* bootstrap mode in particular breaks this rule */ + if (!IsNormalProcessingMode()) + return; + + /* if the relation doesn't have a TOAST table, we are good */ + if (!OidIsValid(rel->rd_rel->reltoastrelid)) + return; + + Assert(HaveRegisteredOrActiveSnapshot()); + +#endif /* USE_ASSERT_CHECKING */ +} + /* ---------------------------------------------------------------- * heap support routines * ---------------------------------------------------------------- @@ -237,7 +265,9 @@ heap_scan_stream_read_next_parallel(ReadStream *stream, /* parallel scan */ table_block_parallelscan_startblock_init(scan->rs_base.rs_rd, scan->rs_parallelworkerdata, - (ParallelBlockTableScanDesc) scan->rs_base.rs_parallel); + (ParallelBlockTableScanDesc) scan->rs_base.rs_parallel, + scan->rs_startblock, + scan->rs_numblocks); /* may return InvalidBlockNumber if there are no more blocks */ scan->rs_prefetch_block = table_block_parallelscan_nextpage(scan->rs_base.rs_rd, @@ -392,28 +422,41 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock) scan->rs_base.rs_flags |= SO_ALLOW_SYNC; else scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC; - } - else if (keep_startblock) - { + /* - * When rescanning, we want to keep the previous startblock setting, - * so that rewinding a cursor doesn't generate surprising results. - * Reset the active syncscan setting, though. + * If not rescanning, initialize the startblock. Finding the actual + * start location is done in table_block_parallelscan_startblock_init, + * based on whether an alternative start location has been set with + * heap_setscanlimits, or using the syncscan location, when syncscan + * is enabled. */ - if (allow_sync && synchronize_seqscans) - scan->rs_base.rs_flags |= SO_ALLOW_SYNC; - else - scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC; - } - else if (allow_sync && synchronize_seqscans) - { - scan->rs_base.rs_flags |= SO_ALLOW_SYNC; - scan->rs_startblock = ss_get_location(scan->rs_base.rs_rd, scan->rs_nblocks); + if (!keep_startblock) + scan->rs_startblock = InvalidBlockNumber; } else { - scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC; - scan->rs_startblock = 0; + if (keep_startblock) + { + /* + * When rescanning, we want to keep the previous startblock + * setting, so that rewinding a cursor doesn't generate surprising + * results. Reset the active syncscan setting, though. + */ + if (allow_sync && synchronize_seqscans) + scan->rs_base.rs_flags |= SO_ALLOW_SYNC; + else + scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC; + } + else if (allow_sync && synchronize_seqscans) + { + scan->rs_base.rs_flags |= SO_ALLOW_SYNC; + scan->rs_startblock = ss_get_location(scan->rs_base.rs_rd, scan->rs_nblocks); + } + else + { + scan->rs_base.rs_flags &= ~SO_ALLOW_SYNC; + scan->rs_startblock = 0; + } } scan->rs_numblocks = InvalidBlockNumber; @@ -483,42 +526,86 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot, BlockNumber block, int lines, bool all_visible, bool check_serializable) { + Oid relid = RelationGetRelid(scan->rs_base.rs_rd); int ntup = 0; - OffsetNumber lineoff; + int nvis = 0; + BatchMVCCState batchmvcc; + + /* page at a time should have been disabled otherwise */ + Assert(IsMVCCSnapshot(snapshot)); - for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++) + /* first find all tuples on the page */ + for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++) { ItemId lpp = PageGetItemId(page, lineoff); - HeapTupleData loctup; - bool valid; + HeapTuple tup; - if (!ItemIdIsNormal(lpp)) + if (unlikely(!ItemIdIsNormal(lpp))) continue; - loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp); - loctup.t_len = ItemIdGetLength(lpp); - loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd); - ItemPointerSet(&(loctup.t_self), block, lineoff); - - if (all_visible) - valid = true; - else - valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer); + /* + * If the page is not all-visible or we need to check serializability, + * maintain enough state to be able to refind the tuple efficiently, + * without again first needing to fetch the item and then via that the + * tuple. + */ + if (!all_visible || check_serializable) + { + tup = &batchmvcc.tuples[ntup]; - if (check_serializable) - HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd, - &loctup, buffer, snapshot); + tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp); + tup->t_len = ItemIdGetLength(lpp); + tup->t_tableOid = relid; + ItemPointerSet(&(tup->t_self), block, lineoff); + } - if (valid) + /* + * If the page is all visible, these fields otherwise won't be + * populated in loop below. + */ + if (all_visible) { + if (check_serializable) + { + batchmvcc.visible[ntup] = true; + } scan->rs_vistuples[ntup] = lineoff; - ntup++; } + + ntup++; } Assert(ntup <= MaxHeapTuplesPerPage); - return ntup; + /* + * Unless the page is all visible, test visibility for all tuples one go. + * That is considerably more efficient than calling + * HeapTupleSatisfiesMVCC() one-by-one. + */ + if (all_visible) + nvis = ntup; + else + nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer, + ntup, + &batchmvcc, + scan->rs_vistuples); + + /* + * So far we don't have batch API for testing serializabilty, so do so + * one-by-one. + */ + if (check_serializable) + { + for (int i = 0; i < ntup; i++) + { + HeapCheckForSerializableConflictOut(batchmvcc.visible[i], + scan->rs_base.rs_rd, + &batchmvcc.tuples[i], + buffer, snapshot); + } + } + + return nvis; } /* @@ -548,7 +635,8 @@ heap_prepare_pagescan(TableScanDesc sscan) /* * Prune and repair fragmentation for the whole page, if possible. */ - heap_page_prune_opt(scan->rs_base.rs_rd, buffer); + heap_page_prune_opt(scan->rs_base.rs_rd, buffer, &scan->rs_vmbuffer, + sscan->rs_flags & SO_HINT_REL_READ_ONLY); /* * We must hold share lock on the buffer content while examining tuple @@ -1038,7 +1126,7 @@ heapgettup_pagemode(HeapScanDesc scan, ItemId lpp; OffsetNumber lineoff; - Assert(lineindex <= scan->rs_ntuples); + Assert(lineindex < scan->rs_ntuples); lineoff = scan->rs_vistuples[lineindex]; lpp = PageGetItemId(page, lineoff); Assert(ItemIdIsNormal(lpp)); @@ -1097,7 +1185,7 @@ heap_beginscan(Relation relation, Snapshot snapshot, */ if (flags & SO_TYPE_BITMAPSCAN) { - BitmapHeapScanDesc bscan = palloc(sizeof(BitmapHeapScanDescData)); + BitmapHeapScanDesc bscan = palloc_object(BitmapHeapScanDescData); /* * Bitmap Heap scans do not have any fields that a normal Heap Scan @@ -1106,13 +1194,14 @@ heap_beginscan(Relation relation, Snapshot snapshot, scan = (HeapScanDesc) bscan; } else - scan = (HeapScanDesc) palloc(sizeof(HeapScanDescData)); + scan = (HeapScanDesc) palloc_object(HeapScanDescData); scan->rs_base.rs_rd = relation; scan->rs_base.rs_snapshot = snapshot; scan->rs_base.rs_nkeys = nkeys; scan->rs_base.rs_flags = flags; scan->rs_base.rs_parallel = parallel_scan; + scan->rs_base.rs_instrument = NULL; scan->rs_strategy = NULL; /* set in initscan */ scan->rs_cbuf = InvalidBuffer; @@ -1122,6 +1211,17 @@ heap_beginscan(Relation relation, Snapshot snapshot, if (!(snapshot && IsMVCCSnapshot(snapshot))) scan->rs_base.rs_flags &= ~SO_ALLOW_PAGEMODE; + /* Check that a historic snapshot is not used for non-catalog tables */ + if (snapshot && + IsHistoricMVCCSnapshot(snapshot) && + !RelationIsAccessibleInLogicalDecoding(relation)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_TRANSACTION_STATE), + errmsg("cannot query non-catalog table \"%s\" during logical decoding", + RelationGetRelationName(relation)))); + } + /* * For seqscan and sample scans in a serializable transaction, acquire a * predicate lock on the entire relation. This is required not only to @@ -1154,7 +1254,7 @@ heap_beginscan(Relation relation, Snapshot snapshot, * when doing a parallel scan. */ if (parallel_scan != NULL) - scan->rs_parallelworkerdata = palloc(sizeof(ParallelBlockTableScanWorkerData)); + scan->rs_parallelworkerdata = palloc_object(ParallelBlockTableScanWorkerData); else scan->rs_parallelworkerdata = NULL; @@ -1163,7 +1263,7 @@ heap_beginscan(Relation relation, Snapshot snapshot, * initscan() and we don't want to allocate memory again */ if (nkeys > 0) - scan->rs_base.rs_key = (ScanKey) palloc(sizeof(ScanKeyData) * nkeys); + scan->rs_base.rs_key = palloc_array(ScanKeyData, nkeys); else scan->rs_base.rs_key = NULL; @@ -1214,6 +1314,15 @@ heap_beginscan(Relation relation, Snapshot snapshot, sizeof(TBMIterateResult)); } + /* enable read stream instrumentation */ + if ((flags & SO_SCAN_INSTRUMENT) && (scan->rs_read_stream != NULL)) + { + scan->rs_base.rs_instrument = palloc0_object(TableScanInstrumentation); + read_stream_enable_stats(scan->rs_read_stream, + &scan->rs_base.rs_instrument->io); + } + + scan->rs_vmbuffer = InvalidBuffer; return (TableScanDesc) scan; } @@ -1252,6 +1361,12 @@ heap_rescan(TableScanDesc sscan, ScanKey key, bool set_params, scan->rs_cbuf = InvalidBuffer; } + if (BufferIsValid(scan->rs_vmbuffer)) + { + ReleaseBuffer(scan->rs_vmbuffer); + scan->rs_vmbuffer = InvalidBuffer; + } + /* * SO_TYPE_BITMAPSCAN would be cleaned up here, but it does not hold any * additional data vs a normal HeapScan @@ -1284,6 +1399,9 @@ heap_endscan(TableScanDesc sscan) if (BufferIsValid(scan->rs_cbuf)) ReleaseBuffer(scan->rs_cbuf); + if (BufferIsValid(scan->rs_vmbuffer)) + ReleaseBuffer(scan->rs_vmbuffer); + /* * Must free the read stream before freeing the BufferAccessStrategy. */ @@ -1307,6 +1425,9 @@ heap_endscan(TableScanDesc sscan) if (scan->rs_base.rs_flags & SO_TEMP_SNAPSHOT) UnregisterSnapshot(scan->rs_base.rs_snapshot); + if (scan->rs_base.rs_instrument) + pfree(scan->rs_base.rs_instrument); + pfree(scan); } @@ -1327,16 +1448,6 @@ heap_getnext(TableScanDesc sscan, ScanDirection direction) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg_internal("only heap AM is supported"))); - /* - * We don't expect direct calls to heap_getnext with valid CheckXidAlive - * for catalog or regular tables. See detailed comments in xact.c where - * these variables are declared. Normally we have such a check at tableam - * level API but this is called from many places so we need to ensure it - * here. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected heap_getnext call during logical decoding"); - /* Note: no locking manipulations needed */ if (scan->rs_base.rs_flags & SO_ALLOW_PAGEMODE) @@ -1601,8 +1712,7 @@ heap_fetch(Relation relation, offnum = ItemPointerGetOffsetNumber(tid); if (offnum < FirstOffsetNumber || offnum > PageGetMaxOffsetNumber(page)) { - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buffer); + UnlockReleaseBuffer(buffer); *userbuf = InvalidBuffer; tuple->t_data = NULL; return false; @@ -1618,8 +1728,7 @@ heap_fetch(Relation relation, */ if (!ItemIdIsNormal(lp)) { - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buffer); + UnlockReleaseBuffer(buffer); *userbuf = InvalidBuffer; tuple->t_data = NULL; return false; @@ -1669,167 +1778,6 @@ heap_fetch(Relation relation, return false; } -/* - * heap_hot_search_buffer - search HOT chain for tuple satisfying snapshot - * - * On entry, *tid is the TID of a tuple (either a simple tuple, or the root - * of a HOT chain), and buffer is the buffer holding this tuple. We search - * for the first chain member satisfying the given snapshot. If one is - * found, we update *tid to reference that tuple's offset number, and - * return true. If no match, return false without modifying *tid. - * - * heapTuple is a caller-supplied buffer. When a match is found, we return - * the tuple here, in addition to updating *tid. If no match is found, the - * contents of this buffer on return are undefined. - * - * If all_dead is not NULL, we check non-visible tuples to see if they are - * globally dead; *all_dead is set true if all members of the HOT chain - * are vacuumable, false if not. - * - * Unlike heap_fetch, the caller must already have pin and (at least) share - * lock on the buffer; it is still pinned/locked at exit. - */ -bool -heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer, - Snapshot snapshot, HeapTuple heapTuple, - bool *all_dead, bool first_call) -{ - Page page = BufferGetPage(buffer); - TransactionId prev_xmax = InvalidTransactionId; - BlockNumber blkno; - OffsetNumber offnum; - bool at_chain_start; - bool valid; - bool skip; - GlobalVisState *vistest = NULL; - - /* If this is not the first call, previous call returned a (live!) tuple */ - if (all_dead) - *all_dead = first_call; - - blkno = ItemPointerGetBlockNumber(tid); - offnum = ItemPointerGetOffsetNumber(tid); - at_chain_start = first_call; - skip = !first_call; - - /* XXX: we should assert that a snapshot is pushed or registered */ - Assert(TransactionIdIsValid(RecentXmin)); - Assert(BufferGetBlockNumber(buffer) == blkno); - - /* Scan through possible multiple members of HOT-chain */ - for (;;) - { - ItemId lp; - - /* check for bogus TID */ - if (offnum < FirstOffsetNumber || offnum > PageGetMaxOffsetNumber(page)) - break; - - lp = PageGetItemId(page, offnum); - - /* check for unused, dead, or redirected items */ - if (!ItemIdIsNormal(lp)) - { - /* We should only see a redirect at start of chain */ - if (ItemIdIsRedirected(lp) && at_chain_start) - { - /* Follow the redirect */ - offnum = ItemIdGetRedirect(lp); - at_chain_start = false; - continue; - } - /* else must be end of chain */ - break; - } - - /* - * Update heapTuple to point to the element of the HOT chain we're - * currently investigating. Having t_self set correctly is important - * because the SSI checks and the *Satisfies routine for historical - * MVCC snapshots need the correct tid to decide about the visibility. - */ - heapTuple->t_data = (HeapTupleHeader) PageGetItem(page, lp); - heapTuple->t_len = ItemIdGetLength(lp); - heapTuple->t_tableOid = RelationGetRelid(relation); - ItemPointerSet(&heapTuple->t_self, blkno, offnum); - - /* - * Shouldn't see a HEAP_ONLY tuple at chain start. - */ - if (at_chain_start && HeapTupleIsHeapOnly(heapTuple)) - break; - - /* - * The xmin should match the previous xmax value, else chain is - * broken. - */ - if (TransactionIdIsValid(prev_xmax) && - !TransactionIdEquals(prev_xmax, - HeapTupleHeaderGetXmin(heapTuple->t_data))) - break; - - /* - * When first_call is true (and thus, skip is initially false) we'll - * return the first tuple we find. But on later passes, heapTuple - * will initially be pointing to the tuple we returned last time. - * Returning it again would be incorrect (and would loop forever), so - * we skip it and return the next match we find. - */ - if (!skip) - { - /* If it's visible per the snapshot, we must return it */ - valid = HeapTupleSatisfiesVisibility(heapTuple, snapshot, buffer); - HeapCheckForSerializableConflictOut(valid, relation, heapTuple, - buffer, snapshot); - - if (valid) - { - ItemPointerSetOffsetNumber(tid, offnum); - PredicateLockTID(relation, &heapTuple->t_self, snapshot, - HeapTupleHeaderGetXmin(heapTuple->t_data)); - if (all_dead) - *all_dead = false; - return true; - } - } - skip = false; - - /* - * If we can't see it, maybe no one else can either. At caller - * request, check whether all chain members are dead to all - * transactions. - * - * Note: if you change the criterion here for what is "dead", fix the - * planner's get_actual_variable_range() function to match. - */ - if (all_dead && *all_dead) - { - if (!vistest) - vistest = GlobalVisTestFor(relation); - - if (!HeapTupleIsSurelyDead(heapTuple, vistest)) - *all_dead = false; - } - - /* - * Check to see if HOT chain continues past this tuple; if so fetch - * the next offnum and loop around. - */ - if (HeapTupleIsHotUpdated(heapTuple)) - { - Assert(ItemPointerGetBlockNumber(&heapTuple->t_data->t_ctid) == - blkno); - offnum = ItemPointerGetOffsetNumber(&heapTuple->t_data->t_ctid); - at_chain_start = false; - prev_xmax = HeapTupleHeaderGetUpdateXid(heapTuple->t_data); - } - else - break; /* end of chain */ - } - - return false; -} - /* * heap_get_latest_tid - get the latest tid of a specified tuple * @@ -1990,7 +1938,7 @@ GetBulkInsertState(void) { BulkInsertState bistate; - bistate = (BulkInsertState) palloc(sizeof(BulkInsertStateData)); + bistate = (BulkInsertState) palloc_object(BulkInsertStateData); bistate->strategy = GetAccessStrategy(BAS_BULKWRITE); bistate->current_buf = InvalidBuffer; bistate->next_free = InvalidBlockNumber; @@ -2054,11 +2002,12 @@ ReleaseBulkInsertStatePin(BulkInsertState bistate) */ void heap_insert(Relation relation, HeapTuple tup, CommandId cid, - int options, BulkInsertState bistate) + uint32 options, BulkInsertState bistate) { TransactionId xid = GetCurrentTransactionId(); HeapTuple heaptup; Buffer buffer; + Page page; Buffer vmbuffer = InvalidBuffer; bool all_visible_cleared = false; @@ -2066,6 +2015,8 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, Assert(HeapTupleHeaderGetNatts(tup->t_data) <= RelationGetNumberOfAttributes(relation)); + AssertHasSnapshotForToast(relation); + /* * Fill in tuple header fields and toast the tuple if necessary. * @@ -2083,6 +2034,8 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, &vmbuffer, NULL, 0); + page = BufferGetPage(buffer); + /* * We're about to do the actual insert -- but check for conflict first, to * avoid possibly having to roll back work we've just done. @@ -2106,25 +2059,30 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, RelationPutHeapTuple(relation, buffer, heaptup, (options & HEAP_INSERT_SPECULATIVE) != 0); - if (PageIsAllVisible(BufferGetPage(buffer))) + if (PageIsAllVisible(page)) { all_visible_cleared = true; - PageClearAllVisible(BufferGetPage(buffer)); + PageClearAllVisible(page); visibilitymap_clear(relation, ItemPointerGetBlockNumber(&(heaptup->t_self)), vmbuffer, VISIBILITYMAP_VALID_BITS); } /* - * XXX Should we set PageSetPrunable on this page ? + * Set pd_prune_xid to trigger heap_page_prune_and_freeze() once the page + * is full so that we can set the page all-visible in the VM on the next + * page access. * - * The inserting transaction may eventually abort thus making this tuple - * DEAD and hence available for pruning. Though we don't want to optimize - * for aborts, if no other tuple in this page is UPDATEd/DELETEd, the - * aborted tuple will never be pruned until next vacuum is triggered. + * Setting pd_prune_xid is also handy if the inserting transaction + * eventually aborts making this tuple DEAD and hence available for + * pruning. If no other tuple in this page is UPDATEd/DELETEd, the aborted + * tuple would never otherwise be pruned until next vacuum is triggered. * - * If you do add PageSetPrunable here, add it in heap_xlog_insert too. + * Don't set it if we are in bootstrap mode or we are inserting a frozen + * tuple, as there is no further pruning/freezing needed in those cases. */ + if (TransactionIdIsNormal(xid) && !(options & HEAP_INSERT_FROZEN)) + PageSetPrunable(page, xid); MarkBufferDirty(buffer); @@ -2134,7 +2092,6 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, xl_heap_insert xlrec; xl_heap_header xlhdr; XLogRecPtr recptr; - Page page = BufferGetPage(buffer); uint8 info = XLOG_HEAP_INSERT; int bufflags = 0; @@ -2214,7 +2171,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, ReleaseBuffer(vmbuffer); /* - * If tuple is cachable, mark it for invalidation from the caches in case + * If tuple is cacheable, mark it for invalidation from the caches in case * we abort. Note it is OK to do this after releasing the buffer, because * the heaptup data structure is all in local memory, not in the shared * buffer. @@ -2243,7 +2200,7 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid, */ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid, - CommandId cid, int options) + CommandId cid, uint32 options) { /* * To allow parallel inserts, we need to ensure that they are safe to be @@ -2323,7 +2280,7 @@ heap_multi_insert_pages(HeapTuple *heaptuples, int done, int ntuples, Size saveF */ void heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, - CommandId cid, int options, BulkInsertState bistate) + CommandId cid, uint32 options, BulkInsertState bistate) { TransactionId xid = GetCurrentTransactionId(); HeapTuple *heaptuples; @@ -2343,6 +2300,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, /* currently not needed (thus unsupported) for heap_multi_insert() */ Assert(!(options & HEAP_INSERT_NO_LOGICAL)); + AssertHasSnapshotForToast(relation); + needwal = RelationNeedsWAL(relation); saveFreeSpace = RelationGetTargetPageFreeSpace(relation, HEAP_DEFAULT_FILLFACTOR); @@ -2430,7 +2389,11 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, starting_with_empty_page = PageGetMaxOffsetNumber(page) == 0; if (starting_with_empty_page && (options & HEAP_INSERT_FROZEN)) + { all_frozen_set = true; + /* Lock the vmbuffer before entering the critical section */ + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + } /* NO EREPORT(ERROR) from here till changes are logged */ START_CRIT_SECTION(); @@ -2470,7 +2433,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, * going to add further frozen rows to it. * * If we're only adding already frozen rows to a previously empty - * page, mark it as all-visible. + * page, mark it as all-frozen and update the visibility map. We're + * already holding a pin on the vmbuffer. */ if (PageIsAllVisible(page) && !(options & HEAP_INSERT_FROZEN)) { @@ -2481,11 +2445,23 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, vmbuffer, VISIBILITYMAP_VALID_BITS); } else if (all_frozen_set) + { PageSetAllVisible(page); + PageClearPrunable(page); + visibilitymap_set(BufferGetBlockNumber(buffer), + vmbuffer, + VISIBILITYMAP_ALL_VISIBLE | + VISIBILITYMAP_ALL_FROZEN, + relation->rd_locator); + } /* - * XXX Should we set PageSetPrunable on this page ? See heap_insert() + * Set pd_prune_xid. See heap_insert() for more on why we do this when + * inserting tuples. This only makes sense if we aren't already + * setting the page frozen in the VM and we're not in bootstrap mode. */ + if (!all_frozen_set && TransactionIdIsNormal(xid)) + PageSetPrunable(page, xid); MarkBufferDirty(buffer); @@ -2529,6 +2505,12 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, xlrec->flags = 0; if (all_visible_cleared) xlrec->flags = XLH_INSERT_ALL_VISIBLE_CLEARED; + + /* + * We don't have to worry about including a conflict xid in the + * WAL record, as HEAP_INSERT_FROZEN intentionally violates + * visibility rules. + */ if (all_frozen_set) xlrec->flags = XLH_INSERT_ALL_FROZEN_SET; @@ -2592,6 +2574,8 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, XLogBeginInsert(); XLogRegisterData(xlrec, tupledata - scratch.data); XLogRegisterBuffer(0, buffer, REGBUF_STANDARD | bufflags); + if (all_frozen_set) + XLogRegisterBuffer(1, vmbuffer, 0); XLogRegisterBufData(0, tupledata, totaldatalen); @@ -2601,29 +2585,17 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, recptr = XLogInsert(RM_HEAP2_ID, info); PageSetLSN(page, recptr); + if (all_frozen_set) + { + Assert(BufferIsDirty(vmbuffer)); + PageSetLSN(BufferGetPage(vmbuffer), recptr); + } } END_CRIT_SECTION(); - /* - * If we've frozen everything on the page, update the visibilitymap. - * We're already holding pin on the vmbuffer. - */ if (all_frozen_set) - { - Assert(PageIsAllVisible(page)); - Assert(visibilitymap_pin_ok(BufferGetBlockNumber(buffer), vmbuffer)); - - /* - * It's fine to use InvalidTransactionId here - this is only used - * when HEAP_INSERT_FROZEN is specified, which intentionally - * violates visibility rules. - */ - visibilitymap_set(relation, BufferGetBlockNumber(buffer), buffer, - InvalidXLogRecPtr, vmbuffer, - InvalidTransactionId, - VISIBILITYMAP_ALL_VISIBLE | VISIBILITYMAP_ALL_FROZEN); - } + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); UnlockReleaseBuffer(buffer); ndone += nthispage; @@ -2656,7 +2628,7 @@ heap_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples, CheckForSerializableConflictIn(relation, NULL, InvalidBlockNumber); /* - * If tuples are cachable, mark them for invalidation from the caches in + * If tuples are cacheable, mark them for invalidation from the caches in * case we abort. Note it is OK to do this after releasing the buffer, * because the heaptuples data structure is all in local memory, not in * the shared buffer. @@ -2742,9 +2714,9 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask) * generated by another transaction). */ TM_Result -heap_delete(Relation relation, ItemPointer tid, - CommandId cid, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, bool changingPart) +heap_delete(Relation relation, const ItemPointerData *tid, + CommandId cid, uint32 options, Snapshot crosscheck, + bool wait, TM_FailureData *tmfd) { TM_Result result; TransactionId xid = GetCurrentTransactionId(); @@ -2757,6 +2729,8 @@ heap_delete(Relation relation, ItemPointer tid, TransactionId new_xmax; uint16 new_infomask, new_infomask2; + bool changingPart = (options & TABLE_DELETE_CHANGING_PARTITION) != 0; + bool walLogical = (options & TABLE_DELETE_NO_LOGICAL) == 0; bool have_tuple_lock = false; bool iscombo; bool all_visible_cleared = false; @@ -2765,6 +2739,8 @@ heap_delete(Relation relation, ItemPointer tid, Assert(ItemPointerIsValid(tid)); + AssertHasSnapshotForToast(relation); + /* * Forbid this during a parallel operation, lest it allocate a combo CID. * Other workers might need that combo CID for visibility checks, and we @@ -2989,7 +2965,8 @@ heap_delete(Relation relation, ItemPointer tid, * Compute replica identity tuple before entering the critical section so * we don't PANIC upon a memory allocation failure. */ - old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied); + old_key_tuple = walLogical ? + ExtractReplicaIdentity(relation, &tp, true, &old_key_copied) : NULL; /* * If this is the first possibly-multixact-able operation in the current @@ -3079,6 +3056,16 @@ heap_delete(Relation relation, ItemPointer tid, xlrec.flags |= XLH_DELETE_CONTAINS_OLD_KEY; } + /* + * Mark the change as not-for-logical-decoding if caller requested so. + * + * (This is used for changes that affect relations not visible to + * other transactions, such as the transient table during concurrent + * repack.) + */ + if (!walLogical) + xlrec.flags |= XLH_DELETE_NO_LOGICAL; + XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfHeapDelete); @@ -3163,15 +3150,17 @@ heap_delete(Relation relation, ItemPointer tid, * via ereport(). */ void -simple_heap_delete(Relation relation, ItemPointer tid) +simple_heap_delete(Relation relation, const ItemPointerData *tid) { TM_Result result; TM_FailureData tmfd; result = heap_delete(relation, tid, - GetCurrentCommandId(true), InvalidSnapshot, + GetCurrentCommandId(true), + 0, + InvalidSnapshot, true /* wait for commit */ , - &tmfd, false /* changingPart */ ); + &tmfd); switch (result) { case TM_SelfModified: @@ -3209,8 +3198,8 @@ simple_heap_delete(Relation relation, ItemPointer tid) * generated by another transaction). */ TM_Result -heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, - CommandId cid, Snapshot crosscheck, bool wait, +heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, + CommandId cid, uint32 options pg_attribute_unused(), Snapshot crosscheck, bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes) { @@ -3227,7 +3216,9 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, HeapTuple heaptup; HeapTuple old_key_tuple = NULL; bool old_key_copied = false; - Page page; + bool walLogical = (options & TABLE_UPDATE_NO_LOGICAL) == 0; + Page page, + newpage; BlockNumber block; MultiXactStatus mxact_status; Buffer buffer, @@ -3260,6 +3251,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, Assert(HeapTupleHeaderGetNatts(newtup->t_data) <= RelationGetNumberOfAttributes(relation)); + AssertHasSnapshotForToast(relation); + /* * Forbid this during a parallel operation, lest it allocate a combo CID. * Other workers might need that combo CID for visibility checks, and we @@ -3951,6 +3944,8 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, heaptup = newtup; } + newpage = BufferGetPage(newbuf); + /* * We're about to do the actual update -- check for conflict first, to * avoid possibly having to roll back work we've just done. @@ -4024,12 +4019,12 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, * the subsequent page pruning will be a no-op and the hint will be * cleared. * - * XXX Should we set hint on newbuf as well? If the transaction aborts, - * there would be a prunable tuple in the newbuf; but for now we choose - * not to optimize for aborts. Note that heap_xlog_update must be kept in - * sync if this decision changes. + * We set the new page prunable as well. See heap_insert() for more on why + * we do this when inserting tuples. */ PageSetPrunable(page, xid); + if (newbuf != buffer) + PageSetPrunable(newpage, xid); if (use_hot_update) { @@ -4065,17 +4060,17 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, oldtup.t_data->t_ctid = heaptup->t_self; /* clear PD_ALL_VISIBLE flags, reset all visibilitymap bits */ - if (PageIsAllVisible(BufferGetPage(buffer))) + if (PageIsAllVisible(page)) { all_visible_cleared = true; - PageClearAllVisible(BufferGetPage(buffer)); + PageClearAllVisible(page); visibilitymap_clear(relation, BufferGetBlockNumber(buffer), vmbuffer, VISIBILITYMAP_VALID_BITS); } - if (newbuf != buffer && PageIsAllVisible(BufferGetPage(newbuf))) + if (newbuf != buffer && PageIsAllVisible(newpage)) { all_visible_cleared_new = true; - PageClearAllVisible(BufferGetPage(newbuf)); + PageClearAllVisible(newpage); visibilitymap_clear(relation, BufferGetBlockNumber(newbuf), vmbuffer_new, VISIBILITYMAP_VALID_BITS); } @@ -4103,12 +4098,13 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, newbuf, &oldtup, heaptup, old_key_tuple, all_visible_cleared, - all_visible_cleared_new); + all_visible_cleared_new, + walLogical); if (newbuf != buffer) { - PageSetLSN(BufferGetPage(newbuf), recptr); + PageSetLSN(newpage, recptr); } - PageSetLSN(BufferGetPage(buffer), recptr); + PageSetLSN(page, recptr); } END_CRIT_SECTION(); @@ -4190,7 +4186,7 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup, */ static void check_lock_if_inplace_updateable_rel(Relation relation, - ItemPointer otid, + const ItemPointerData *otid, HeapTuple newtup) { /* LOCKTAG_TUPLE acceptable for any catalog */ @@ -4434,7 +4430,7 @@ HeapDetermineColumnsInfo(Relation relation, * Check if the old tuple's attribute is stored externally and is a * member of external_cols. */ - if (VARATT_IS_EXTERNAL((struct varlena *) DatumGetPointer(value1)) && + if (VARATT_IS_EXTERNAL((varlena *) DatumGetPointer(value1)) && bms_is_member(attidx, external_cols)) *has_external = true; } @@ -4451,7 +4447,7 @@ HeapDetermineColumnsInfo(Relation relation, * via ereport(). */ void -simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup, +simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup, TU_UpdateIndexes *update_indexes) { TM_Result result; @@ -4459,7 +4455,8 @@ simple_heap_update(Relation relation, ItemPointer otid, HeapTuple tup, LockTupleMode lockmode; result = heap_update(relation, otid, tup, - GetCurrentCommandId(true), InvalidSnapshot, + GetCurrentCommandId(true), 0, + InvalidSnapshot, true /* wait for commit */ , &tmfd, &lockmode, update_indexes); switch (result) @@ -4515,7 +4512,6 @@ get_mxact_status_for_lock(LockTupleMode mode, bool is_update) * * Input parameters: * relation: relation containing tuple (caller must hold suitable lock) - * tid: TID of tuple to lock * cid: current command ID (used for visibility test, and stored into * tuple's cmax if lock is successful) * mode: indicates if shared or exclusive tuple lock is desired @@ -4589,10 +4585,10 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, if (result == TM_Invisible) { /* - * This is possible, but only when locking a tuple for ON CONFLICT - * UPDATE. We return this value here rather than throwing an error in - * order to give that case the opportunity to throw a more specific - * error. + * This is possible, but only when locking a tuple for ON CONFLICT DO + * SELECT/UPDATE. We return this value here rather than throwing an + * error in order to give that case the opportunity to throw a more + * specific error. */ result = TM_Invisible; goto out_locked; @@ -4754,11 +4750,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, * If there are updates, follow the update chain; bail out if * that cannot be done. */ - if (follow_updates && updated) + if (follow_updates && updated && + !ItemPointerEquals(&tuple->t_self, &t_ctid)) { TM_Result res; - res = heap_lock_updated_tuple(relation, tuple, &t_ctid, + res = heap_lock_updated_tuple(relation, + infomask, xwait, &t_ctid, GetCurrentTransactionId(), mode); if (res != TM_Ok) @@ -4953,7 +4951,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, case LockWaitError: if (!ConditionalMultiXactIdWait((MultiXactId) xwait, status, infomask, relation, - NULL, log_lock_failure)) + NULL, log_lock_failures)) ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("could not obtain lock on row in relation \"%s\"", @@ -4991,7 +4989,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, } break; case LockWaitError: - if (!ConditionalXactLockTableWait(xwait, log_lock_failure)) + if (!ConditionalXactLockTableWait(xwait, log_lock_failures)) ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("could not obtain lock on row in relation \"%s\"", @@ -5001,11 +4999,13 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, } /* if there are updates, follow the update chain */ - if (follow_updates && !HEAP_XMAX_IS_LOCKED_ONLY(infomask)) + if (follow_updates && !HEAP_XMAX_IS_LOCKED_ONLY(infomask) && + !ItemPointerEquals(&tuple->t_self, &t_ctid)) { TM_Result res; - res = heap_lock_updated_tuple(relation, tuple, &t_ctid, + res = heap_lock_updated_tuple(relation, + infomask, xwait, &t_ctid, GetCurrentTransactionId(), mode); if (res != TM_Ok) @@ -5238,7 +5238,7 @@ heap_lock_tuple(Relation relation, HeapTuple tuple, * wait_policy is Skip. */ static bool -heap_acquire_tuplock(Relation relation, ItemPointer tid, LockTupleMode mode, +heap_acquire_tuplock(Relation relation, const ItemPointerData *tid, LockTupleMode mode, LockWaitPolicy wait_policy, bool *have_tuple_lock) { if (*have_tuple_lock) @@ -5256,7 +5256,7 @@ heap_acquire_tuplock(Relation relation, ItemPointer tid, LockTupleMode mode, break; case LockWaitError: - if (!ConditionalLockTupleTuplock(relation, tid, mode, log_lock_failure)) + if (!ConditionalLockTupleTuplock(relation, tid, mode, log_lock_failures)) ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("could not obtain lock on row in relation \"%s\"", @@ -5659,7 +5659,8 @@ test_lockmode_for_conflict(MultiXactStatus status, TransactionId xid, * version as well. */ static TM_Result -heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid, +heap_lock_updated_tuple_rec(Relation rel, TransactionId priorXmax, + const ItemPointerData *tid, TransactionId xid, LockTupleMode mode) { TM_Result result; @@ -5672,7 +5673,6 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid, old_infomask2; TransactionId xmax, new_xmax; - TransactionId priorXmax = InvalidTransactionId; bool cleared_all_frozen = false; bool pinned_desired_page; Buffer vmbuffer = InvalidBuffer; @@ -5986,7 +5986,10 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid, * Follow update chain when locking an updated tuple, acquiring locks (row * marks) on the updated versions. * - * The initial tuple is assumed to be already locked. + * 'prior_infomask', 'prior_raw_xmax' and 'prior_ctid' are the corresponding + * fields from the initial tuple. We will lock the tuples starting from the + * one that 'prior_ctid' points to. Note: This function does not lock the + * initial tuple itself. * * This function doesn't check visibility, it just unconditionally marks the * tuple(s) as locked. If any tuple in the updated chain is being deleted @@ -6004,16 +6007,22 @@ heap_lock_updated_tuple_rec(Relation rel, ItemPointer tid, TransactionId xid, * levels, because that would lead to a serializability failure. */ static TM_Result -heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid, +heap_lock_updated_tuple(Relation rel, + uint16 prior_infomask, + TransactionId prior_raw_xmax, + const ItemPointerData *prior_ctid, TransactionId xid, LockTupleMode mode) { + INJECTION_POINT("heap_lock_updated_tuple", NULL); + /* - * If the tuple has not been updated, or has moved into another partition - * (effectively a delete) stop here. + * If the tuple has moved into another partition (effectively a delete) + * stop here. */ - if (!HeapTupleHeaderIndicatesMovedPartitions(tuple->t_data) && - !ItemPointerEquals(&tuple->t_self, ctid)) + if (!ItemPointerIndicatesMovedPartitions(prior_ctid)) { + TransactionId prior_xmax; + /* * If this is the first possibly-multixact-able operation in the * current transaction, set my per-backend OldestMemberMXactId @@ -6025,7 +6034,9 @@ heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid, */ MultiXactIdSetOldestMember(); - return heap_lock_updated_tuple_rec(rel, ctid, xid, mode); + prior_xmax = (prior_infomask & HEAP_XMAX_IS_MULTI) ? + MultiXactIdGetUpdateXid(prior_raw_xmax, prior_infomask) : prior_raw_xmax; + return heap_lock_updated_tuple_rec(rel, prior_xmax, prior_ctid, xid, mode); } /* nothing to lock */ @@ -6049,23 +6060,23 @@ heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid, * An explicit confirmation WAL record also makes logical decoding simpler. */ void -heap_finish_speculative(Relation relation, ItemPointer tid) +heap_finish_speculative(Relation relation, const ItemPointerData *tid) { Buffer buffer; Page page; OffsetNumber offnum; - ItemId lp = NULL; + ItemId lp; HeapTupleHeader htup; buffer = ReadBuffer(relation, ItemPointerGetBlockNumber(tid)); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); offnum = ItemPointerGetOffsetNumber(tid); - if (PageGetMaxOffsetNumber(page) >= offnum) - lp = PageGetItemId(page, offnum); - - if (PageGetMaxOffsetNumber(page) < offnum || !ItemIdIsNormal(lp)) + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(page)) + elog(ERROR, "offnum out of range"); + lp = PageGetItemId(page, offnum); + if (!ItemIdIsNormal(lp)) elog(ERROR, "invalid lp"); htup = (HeapTupleHeader) PageGetItem(page, lp); @@ -6136,7 +6147,7 @@ heap_finish_speculative(Relation relation, ItemPointer tid) * confirmation records. */ void -heap_abort_speculative(Relation relation, ItemPointer tid) +heap_abort_speculative(Relation relation, const ItemPointerData *tid) { TransactionId xid = GetCurrentTransactionId(); ItemId lp; @@ -6287,10 +6298,13 @@ heap_abort_speculative(Relation relation, ItemPointer tid) * Since this is intended for system catalogs and SERIALIZABLE doesn't cover * DDL, this doesn't guarantee any particular predicate locking. * - * One could modify this to return true for tuples with delete in progress, - * All inplace updaters take a lock that conflicts with DROP. If explicit - * "DELETE FROM pg_class" is in progress, we'll wait for it like we would an - * update. + * heap_delete() is a rarer source of blocking transactions (xwait). We'll + * wait for such a transaction just like for the normal heap_update() case. + * Normal concurrent DROP commands won't cause that, because all inplace + * updaters take some lock that conflicts with DROP. An explicit SQL "DELETE + * FROM pg_class" can cause it. By waiting, if the concurrent transaction + * executed both "DELETE FROM pg_class" and "INSERT INTO pg_class", our caller + * can find the successor tuple. * * Readers of inplace-updated fields expect changes to those fields are * durable. For example, vac_truncate_clog() reads datfrozenxid from @@ -6331,15 +6345,17 @@ heap_inplace_lock(Relation relation, Assert(BufferIsValid(buffer)); /* - * Construct shared cache inval if necessary. Because we pass a tuple - * version without our own inplace changes or inplace changes other - * sessions complete while we wait for locks, inplace update mustn't - * change catcache lookup keys. But we aren't bothering with index - * updates either, so that's true a fortiori. After LockBuffer(), it - * would be too late, because this might reach a - * CatalogCacheInitializeCache() that locks "buffer". + * Register shared cache invals if necessary. Other sessions may finish + * inplace updates of this tuple between this step and LockTuple(). Since + * inplace updates don't change cache keys, that's harmless. + * + * While it's tempting to register invals only after confirming we can + * return true, the following obstacle precludes reordering steps that + * way. Registering invals might reach a CatalogCacheInitializeCache() + * that locks "buffer". That would hang indefinitely if running after our + * own LockBuffer(). Hence, we must register invals before LockBuffer(). */ - CacheInvalidateHeapTupleInplace(relation, oldtup_ptr, NULL); + CacheInvalidateHeapTupleInplace(relation, oldtup_ptr); LockTuple(relation, &oldtup.t_self, InplaceUpdateTupleLock); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); @@ -6491,11 +6507,11 @@ heap_inplace_update_and_unlock(Relation relation, /*---------- * NO EREPORT(ERROR) from here till changes are complete * - * Our buffer lock won't stop a reader having already pinned and checked - * visibility for this tuple. Hence, we write WAL first, then mutate the - * buffer. Like in MarkBufferDirtyHint() or RecordTransactionCommit(), - * checkpoint delay makes that acceptable. With the usual order of - * changes, a crash after memcpy() and before XLogInsert() could allow + * Our exclusive buffer lock won't stop a reader having already pinned and + * checked visibility for this tuple. With the usual order of changes + * (i.e. updating the buffer contents before WAL logging), a reader could + * observe our not-yet-persistent update to relfrozenxid and update + * datfrozenxid based on that. A crash in that moment could allow * datfrozenxid to overtake relfrozenxid: * * ["D" is a VACUUM (ONLY_DATABASE_STATS)] @@ -6507,21 +6523,15 @@ heap_inplace_update_and_unlock(Relation relation, * [crash] * [recovery restores datfrozenxid w/o relfrozenxid] * - * Mimic MarkBufferDirtyHint() subroutine XLogSaveBufferForHint(). - * Specifically, use DELAY_CHKPT_START, and copy the buffer to the stack. - * The stack copy facilitates a FPI of the post-mutation block before we - * accept other sessions seeing it. DELAY_CHKPT_START allows us to - * XLogInsert() before MarkBufferDirty(). Since XLogSaveBufferForHint() - * can operate under BUFFER_LOCK_SHARED, it can't avoid DELAY_CHKPT_START. - * This function, however, likely could avoid it with the following order - * of operations: MarkBufferDirty(), XLogInsert(), memcpy(). Opt to use - * DELAY_CHKPT_START here, too, as a way to have fewer distinct code - * patterns to analyze. Inplace update isn't so frequent that it should - * pursue the small optimization of skipping DELAY_CHKPT_START. - */ - Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) == 0); + * We avoid that by using a temporary copy of the buffer to hide our + * change from other backends until the change has been WAL-logged. We + * apply our change to the temporary copy and WAL-log it, before modifying + * the real page. That way any action a reader of the in-place-updated + * value takes will be WAL logged after this change. + */ START_CRIT_SECTION(); - MyProc->delayChkptFlags |= DELAY_CHKPT_START; + + MarkBufferDirty(buffer); /* XLOG stuff */ if (RelationNeedsWAL(relation)) @@ -6570,31 +6580,24 @@ heap_inplace_update_and_unlock(Relation relation, memcpy(dst, src, newlen); - MarkBufferDirty(buffer); - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* * Send invalidations to shared queue. SearchSysCacheLocked1() assumes we * do this before UnlockTuple(). - * - * If we're mutating a tuple visible only to this transaction, there's an - * equivalent transactional inval from the action that created the tuple, - * and this inval is superfluous. */ AtInplace_Inval(); - MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; END_CRIT_SECTION(); UnlockTuple(relation, &tuple->t_self, InplaceUpdateTupleLock); AcceptInvalidationMessages(); /* local processing of just-sent inval */ /* - * Queue a transactional inval. The immediate invalidation we just sent - * is the only one known to be necessary. To reduce risk from the - * transition to immediate invalidation, continue sending a transactional - * invalidation like we've long done. Third-party code might rely on it. + * Queue a transactional inval, for logical decoding and for third-party + * code that might have been relying on it since long before inplace + * update adopted immediate invalidation. See README.tuplock section + * "Reading inplace-updated columns" for logical decoding details. */ if (!IsBootstrapProcessingMode()) CacheInvalidateHeapTuple(relation, tuple, NULL); @@ -6833,7 +6836,7 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask, * even member XIDs >= OldestXmin often won't be kept by second pass. */ nnewmembers = 0; - newmembers = palloc(sizeof(MultiXactMember) * nmembers); + newmembers = palloc_array(MultiXactMember, nmembers); has_lockers = false; update_xid = InvalidTransactionId; update_committed = false; @@ -6980,6 +6983,12 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask, * process this tuple as part of freezing its page, and return true. Return * false if nothing can be changed about the tuple right now. * + * FreezePageConflictXid is advanced only for xmin/xvac freezing, not for xmax + * changes. We only remove xmax state here when it is lock-only, or when the + * updater XID (including an updater member of a MultiXact) must be aborted; + * otherwise, the tuple would already be removable. Neither case affects + * visibility on a standby. + * * Also sets *totally_frozen to true if the tuple will be totally frozen once * caller executes returned freeze plan (or if the tuple was already totally * frozen by an earlier VACUUM). This indicates that there are no remaining @@ -7055,7 +7064,11 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, /* Verify that xmin committed if and when freeze plan is executed */ if (freeze_xmin) + { frz->checkflags |= HEAP_FREEZE_CHECK_XMIN_COMMITTED; + if (TransactionIdFollows(xid, pagefrz->FreezePageConflictXid)) + pagefrz->FreezePageConflictXid = xid; + } } /* @@ -7074,6 +7087,9 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple, */ replace_xvac = pagefrz->freeze_required = true; + if (TransactionIdFollows(xid, pagefrz->FreezePageConflictXid)) + pagefrz->FreezePageConflictXid = xid; + /* Will set replace_xvac flags in freeze plan below */ } @@ -7383,6 +7399,7 @@ heap_freeze_tuple(HeapTupleHeader tuple, pagefrz.freeze_required = true; pagefrz.FreezePageRelfrozenXid = FreezeLimit; pagefrz.FreezePageRelminMxid = MultiXactCutoff; + pagefrz.FreezePageConflictXid = InvalidTransactionId; pagefrz.NoFreezePageRelfrozenXid = FreezeLimit; pagefrz.NoFreezePageRelminMxid = MultiXactCutoff; @@ -7658,7 +7675,7 @@ DoesMultiXactIdConflict(MultiXactId multi, uint16 infomask, static bool Do_MultiXactIdWait(MultiXactId multi, MultiXactStatus status, uint16 infomask, bool nowait, - Relation rel, ItemPointer ctid, XLTW_Oper oper, + Relation rel, const ItemPointerData *ctid, XLTW_Oper oper, int *remaining, bool logLockFailure) { bool result = true; @@ -7735,7 +7752,7 @@ Do_MultiXactIdWait(MultiXactId multi, MultiXactStatus status, */ static void MultiXactIdWait(MultiXactId multi, MultiXactStatus status, uint16 infomask, - Relation rel, ItemPointer ctid, XLTW_Oper oper, + Relation rel, const ItemPointerData *ctid, XLTW_Oper oper, int *remaining) { (void) Do_MultiXactIdWait(multi, status, infomask, false, @@ -8021,7 +8038,7 @@ index_delete_prefetch_buffer(Relation rel, static inline void index_delete_check_htid(TM_IndexDeleteOp *delstate, Page page, OffsetNumber maxoff, - ItemPointer htid, TM_IndexStatus *istatus) + const ItemPointerData *htid, TM_IndexStatus *istatus) { OffsetNumber indexpagehoffnum = ItemPointerGetOffsetNumber(htid); ItemId iid; @@ -8649,7 +8666,7 @@ bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate) Assert(delstate->ndeltids > 0); /* Calculate per-heap-block count of TIDs */ - blockgroups = palloc(sizeof(IndexDeleteCounts) * delstate->ndeltids); + blockgroups = palloc_array(IndexDeleteCounts, delstate->ndeltids); for (int i = 0; i < delstate->ndeltids; i++) { TM_IndexDelete *ideltid = &delstate->deltids[i]; @@ -8750,50 +8767,6 @@ bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate) return nblocksfavorable; } -/* - * Perform XLogInsert for a heap-visible operation. 'block' is the block - * being marked all-visible, and vm_buffer is the buffer containing the - * corresponding visibility map block. Both should have already been modified - * and dirtied. - * - * snapshotConflictHorizon comes from the largest xmin on the page being - * marked all-visible. REDO routine uses it to generate recovery conflicts. - * - * If checksums or wal_log_hints are enabled, we may also generate a full-page - * image of heap_buffer. Otherwise, we optimize away the FPI (by specifying - * REGBUF_NO_IMAGE for the heap buffer), in which case the caller should *not* - * update the heap page's LSN. - */ -XLogRecPtr -log_heap_visible(Relation rel, Buffer heap_buffer, Buffer vm_buffer, - TransactionId snapshotConflictHorizon, uint8 vmflags) -{ - xl_heap_visible xlrec; - XLogRecPtr recptr; - uint8 flags; - - Assert(BufferIsValid(heap_buffer)); - Assert(BufferIsValid(vm_buffer)); - - xlrec.snapshotConflictHorizon = snapshotConflictHorizon; - xlrec.flags = vmflags; - if (RelationIsAccessibleInLogicalDecoding(rel)) - xlrec.flags |= VISIBILITYMAP_XLOG_CATALOG_REL; - XLogBeginInsert(); - XLogRegisterData(&xlrec, SizeOfHeapVisible); - - XLogRegisterBuffer(0, vm_buffer, 0); - - flags = REGBUF_STANDARD; - if (!XLogHintBitIsNeeded()) - flags |= REGBUF_NO_IMAGE; - XLogRegisterBuffer(1, heap_buffer, flags); - - recptr = XLogInsert(RM_HEAP2_ID, XLOG_HEAP2_VISIBLE); - - return recptr; -} - /* * Perform XLogInsert for a heap-update operation. Caller must already * have modified the buffer(s) and marked them dirty. @@ -8802,7 +8775,8 @@ static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf, Buffer newbuf, HeapTuple oldtup, HeapTuple newtup, HeapTuple old_key_tuple, - bool all_visible_cleared, bool new_all_visible_cleared) + bool all_visible_cleared, bool new_all_visible_cleared, + bool walLogical) { xl_heap_update xlrec; xl_heap_header xlhdr; @@ -8813,7 +8787,7 @@ log_heap_update(Relation reln, Buffer oldbuf, suffixlen = 0; XLogRecPtr recptr; Page page = BufferGetPage(newbuf); - bool need_tuple_data = RelationIsLogicallyLogged(reln); + bool need_tuple_data = walLogical && RelationIsLogicallyLogged(reln); bool init; int bufflags; @@ -8844,8 +8818,8 @@ log_heap_update(Relation reln, Buffer oldbuf, * * Skip this if we're taking a full-page image of the new page, as we * don't include the new tuple in the WAL record in that case. Also - * disable if wal_level='logical', as logical decoding needs to be able to - * read the new tuple in whole from the WAL record alone. + * disable if effective_wal_level='logical', as logical decoding needs to + * be able to read the new tuple in whole from the WAL record alone. */ if (oldbuf == newbuf && !need_tuple_data && !XLogCheckBufferNeedsBackup(newbuf)) @@ -9017,8 +8991,8 @@ log_heap_update(Relation reln, Buffer oldbuf, /* * Perform XLogInsert of an XLOG_HEAP2_NEW_CID record * - * This is only used in wal_level >= WAL_LEVEL_LOGICAL, and only for catalog - * tuples. + * This is only used when effective_wal_level is logical, and only for + * catalog tuples. */ static XLogRecPtr log_heap_new_cid(Relation relation, HeapTuple tup) diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c index ac082fefa77a7..20d3b46e06207 100644 --- a/src/backend/access/heap/heapam_handler.c +++ b/src/backend/access/heap/heapam_handler.c @@ -3,7 +3,7 @@ * heapam_handler.c * heap table access method code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -40,15 +40,22 @@ #include "storage/bufmgr.h" #include "storage/bufpage.h" #include "storage/lmgr.h" +#include "storage/lock.h" #include "storage/predicate.h" #include "storage/procarray.h" #include "storage/smgr.h" #include "utils/builtins.h" #include "utils/rel.h" +#include "utils/tuplesort.h" static void reform_and_rewrite_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, Datum *values, bool *isnull, RewriteState rwstate); +static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, + Relation NewHeap, Datum *values, bool *isnull, + BulkInsertState bistate); +static HeapTuple reform_tuple(HeapTuple tuple, Relation OldHeap, + Relation NewHeap, Datum *values, bool *isnull); static bool SampleHeapTupleVisible(TableScanDesc scan, Buffer buffer, HeapTuple tuple, @@ -73,107 +80,6 @@ heapam_slot_callbacks(Relation relation) } -/* ------------------------------------------------------------------------ - * Index Scan Callbacks for heap AM - * ------------------------------------------------------------------------ - */ - -static IndexFetchTableData * -heapam_index_fetch_begin(Relation rel) -{ - IndexFetchHeapData *hscan = palloc0(sizeof(IndexFetchHeapData)); - - hscan->xs_base.rel = rel; - hscan->xs_cbuf = InvalidBuffer; - - return &hscan->xs_base; -} - -static void -heapam_index_fetch_reset(IndexFetchTableData *scan) -{ - IndexFetchHeapData *hscan = (IndexFetchHeapData *) scan; - - if (BufferIsValid(hscan->xs_cbuf)) - { - ReleaseBuffer(hscan->xs_cbuf); - hscan->xs_cbuf = InvalidBuffer; - } -} - -static void -heapam_index_fetch_end(IndexFetchTableData *scan) -{ - IndexFetchHeapData *hscan = (IndexFetchHeapData *) scan; - - heapam_index_fetch_reset(scan); - - pfree(hscan); -} - -static bool -heapam_index_fetch_tuple(struct IndexFetchTableData *scan, - ItemPointer tid, - Snapshot snapshot, - TupleTableSlot *slot, - bool *call_again, bool *all_dead) -{ - IndexFetchHeapData *hscan = (IndexFetchHeapData *) scan; - BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; - bool got_heap_tuple; - - Assert(TTS_IS_BUFFERTUPLE(slot)); - - /* We can skip the buffer-switching logic if we're in mid-HOT chain. */ - if (!*call_again) - { - /* Switch to correct buffer if we don't have it already */ - Buffer prev_buf = hscan->xs_cbuf; - - hscan->xs_cbuf = ReleaseAndReadBuffer(hscan->xs_cbuf, - hscan->xs_base.rel, - ItemPointerGetBlockNumber(tid)); - - /* - * Prune page, but only if we weren't already on this page - */ - if (prev_buf != hscan->xs_cbuf) - heap_page_prune_opt(hscan->xs_base.rel, hscan->xs_cbuf); - } - - /* Obtain share-lock on the buffer so we can examine visibility */ - LockBuffer(hscan->xs_cbuf, BUFFER_LOCK_SHARE); - got_heap_tuple = heap_hot_search_buffer(tid, - hscan->xs_base.rel, - hscan->xs_cbuf, - snapshot, - &bslot->base.tupdata, - all_dead, - !*call_again); - bslot->base.tupdata.t_self = *tid; - LockBuffer(hscan->xs_cbuf, BUFFER_LOCK_UNLOCK); - - if (got_heap_tuple) - { - /* - * Only in a non-MVCC snapshot can more than one member of the HOT - * chain be visible. - */ - *call_again = !IsMVCCSnapshot(snapshot); - - slot->tts_tableOid = RelationGetRelid(scan->rel); - ExecStoreBufferHeapTuple(&bslot->base.tupdata, slot, hscan->xs_cbuf); - } - else - { - /* We've reached the end of the HOT chain. */ - *call_again = false; - } - - return got_heap_tuple; -} - - /* ------------------------------------------------------------------------ * Callbacks for non-modifying operations on individual tuples for heap AM * ------------------------------------------------------------------------ @@ -242,7 +148,7 @@ heapam_tuple_satisfies_snapshot(Relation rel, TupleTableSlot *slot, static void heapam_tuple_insert(Relation relation, TupleTableSlot *slot, CommandId cid, - int options, BulkInsertState bistate) + uint32 options, BulkInsertState bistate) { bool shouldFree = true; HeapTuple tuple = ExecFetchSlotHeapTuple(slot, true, &shouldFree); @@ -261,7 +167,7 @@ heapam_tuple_insert(Relation relation, TupleTableSlot *slot, CommandId cid, static void heapam_tuple_insert_speculative(Relation relation, TupleTableSlot *slot, - CommandId cid, int options, + CommandId cid, uint32 options, BulkInsertState bistate, uint32 specToken) { bool shouldFree = true; @@ -301,21 +207,23 @@ heapam_tuple_complete_speculative(Relation relation, TupleTableSlot *slot, static TM_Result heapam_tuple_delete(Relation relation, ItemPointer tid, CommandId cid, - Snapshot snapshot, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, bool changingPart) + uint32 options, Snapshot snapshot, Snapshot crosscheck, + bool wait, TM_FailureData *tmfd) { /* * Currently Deleting of index tuples are handled at vacuum, in case if * the storage itself is cleaning the dead tuples by itself, it is the * time to call the index tuple deletion also. */ - return heap_delete(relation, tid, cid, crosscheck, wait, tmfd, changingPart); + return heap_delete(relation, tid, cid, options, crosscheck, wait, + tmfd); } static TM_Result heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot, - CommandId cid, Snapshot snapshot, Snapshot crosscheck, + CommandId cid, uint32 options, + Snapshot snapshot, Snapshot crosscheck, bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes) { @@ -327,7 +235,8 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot, slot->tts_tableOid = RelationGetRelid(relation); tuple->t_tableOid = slot->tts_tableOid; - result = heap_update(relation, otid, tuple, cid, crosscheck, wait, + result = heap_update(relation, otid, tuple, cid, options, + crosscheck, wait, tmfd, lockmode, update_indexes); ItemPointerCopy(&tuple->t_self, &slot->tts_tid); @@ -464,7 +373,7 @@ heapam_tuple_lock(Relation relation, ItemPointer tid, Snapshot snapshot, return TM_WouldBlock; break; case LockWaitError: - if (!ConditionalXactLockTableWait(SnapshotDirty.xmax, log_lock_failure)) + if (!ConditionalXactLockTableWait(SnapshotDirty.xmax, log_lock_failures)) ereport(ERROR, (errcode(ERRCODE_LOCK_NOT_AVAILABLE), errmsg("could not obtain lock on row in relation \"%s\"", @@ -685,6 +594,7 @@ static void heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -692,6 +602,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, double *tups_recently_dead) { RewriteState rwstate; + BulkInsertState bistate; IndexScanDesc indexScan; TableScanDesc tableScan; HeapScanDesc heapScan; @@ -705,6 +616,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, bool *isnull; BufferHeapTupleTableSlot *hslot; BlockNumber prev_cblock = InvalidBlockNumber; + bool concurrent = snapshot != NULL; /* Remember if it's a system catalog */ is_system_catalog = IsSystemRelation(OldHeap); @@ -717,13 +629,24 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, /* Preallocate values/isnull arrays */ natts = newTupDesc->natts; - values = (Datum *) palloc(natts * sizeof(Datum)); - isnull = (bool *) palloc(natts * sizeof(bool)); + values = palloc_array(Datum, natts); + isnull = palloc_array(bool, natts); - /* Initialize the rewrite operation */ - rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff, - *multi_cutoff); + /* + * In non-concurrent mode, initialize the rewrite operation. This is not + * needed in concurrent mode. + */ + if (!concurrent) + rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, + *xid_cutoff, *multi_cutoff); + else + rwstate = NULL; + /* In concurrent mode, prepare for bulk-insert operation. */ + if (concurrent) + bistate = GetBulkInsertState(); + else + bistate = NULL; /* Set up sorting if wanted */ if (use_sort) @@ -737,37 +660,46 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * Prepare to scan the OldHeap. To ensure we see recently-dead tuples * that still need to be copied, we scan with SnapshotAny and use * HeapTupleSatisfiesVacuum for the visibility test. + * + * In the CONCURRENTLY case, we do regular MVCC visibility tests, using + * the snapshot passed by the caller. */ if (OldIndex != NULL && !use_sort) { const int ci_index[] = { - PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_INDEX_RELID + PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_INDEX_RELID }; int64 ci_val[2]; /* Set phase and OIDOldIndex to columns */ - ci_val[0] = PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP; + ci_val[0] = PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP; ci_val[1] = RelationGetRelid(OldIndex); pgstat_progress_update_multi_param(2, ci_index, ci_val); tableScan = NULL; heapScan = NULL; - indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, NULL, 0, 0); + indexScan = index_beginscan(OldHeap, OldIndex, + snapshot ? snapshot : SnapshotAny, + NULL, 0, 0, + SO_NONE); index_rescan(indexScan, NULL, 0, NULL, 0); } else { /* In scan-and-sort mode and also VACUUM FULL, set phase */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP); - tableScan = table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL); + tableScan = table_beginscan(OldHeap, + snapshot ? snapshot : SnapshotAny, + 0, (ScanKey) NULL, + SO_NONE); heapScan = (HeapScanDesc) tableScan; indexScan = NULL; /* Set total heap blocks */ - pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS, + pgstat_progress_update_param(PROGRESS_REPACK_TOTAL_HEAP_BLKS, heapScan->rs_nblocks); } @@ -809,7 +741,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * is manually updated to the correct value when the table * scan finishes. */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED, heapScan->rs_nblocks); break; } @@ -825,7 +757,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, */ if (prev_cblock != heapScan->rs_cblock) { - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_BLKS_SCANNED, (heapScan->rs_cblock + heapScan->rs_nblocks - heapScan->rs_startblock @@ -837,70 +769,95 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, tuple = ExecFetchSlotHeapTuple(slot, false, NULL); buf = hslot->buffer; - LockBuffer(buf, BUFFER_LOCK_SHARE); - - switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) + /* + * In concurrent mode, our table or index scan has used regular MVCC + * visibility test against a snapshot passed by caller; therefore we + * don't need another visibility test. In non-concurrent mode + * however, we must test the visibility of each tuple we read. + */ + if (!concurrent) { - case HEAPTUPLE_DEAD: - /* Definitely dead */ - isdead = true; - break; - case HEAPTUPLE_RECENTLY_DEAD: - *tups_recently_dead += 1; - /* fall through */ - case HEAPTUPLE_LIVE: - /* Live or recently dead, must copy it */ - isdead = false; - break; - case HEAPTUPLE_INSERT_IN_PROGRESS: + /* + * To be able to guarantee that we can set the hint bit, acquire + * an exclusive lock on the old buffer. We need the hint bits, set + * in heapam_relation_copy_for_cluster() -> + * HeapTupleSatisfiesVacuum(), to be set, as otherwise + * reform_and_rewrite_tuple() -> rewrite_heap_tuple() will get + * confused. Specifically, rewrite_heap_tuple() checks for + * HEAP_XMAX_INVALID in the old tuple to determine whether to + * check the old-to-new mapping hash table. + * + * It'd be better if we somehow could avoid setting hint bits on + * the old page. One reason to use VACUUM FULL are very bloated + * tables - rewriting most of the old table during VACUUM FULL + * doesn't exactly help... + */ + LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); - /* - * Since we hold exclusive lock on the relation, normally the - * only way to see this is if it was inserted earlier in our - * own transaction. However, it can happen in system - * catalogs, since we tend to release write lock before commit - * there. Give a warning if neither case applies; but in any - * case we had better copy it. - */ - if (!is_system_catalog && - !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) - elog(WARNING, "concurrent insert in progress within table \"%s\"", - RelationGetRelationName(OldHeap)); - /* treat as live */ - isdead = false; - break; - case HEAPTUPLE_DELETE_IN_PROGRESS: + switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf)) + { + case HEAPTUPLE_DEAD: + /* Definitely dead */ + isdead = true; + break; + case HEAPTUPLE_RECENTLY_DEAD: + *tups_recently_dead += 1; + pg_fallthrough; + case HEAPTUPLE_LIVE: + /* Live or recently dead, must copy it */ + isdead = false; + break; + case HEAPTUPLE_INSERT_IN_PROGRESS: - /* - * Similar situation to INSERT_IN_PROGRESS case. - */ - if (!is_system_catalog && - !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) - elog(WARNING, "concurrent delete in progress within table \"%s\"", - RelationGetRelationName(OldHeap)); - /* treat as recently dead */ - *tups_recently_dead += 1; - isdead = false; - break; - default: - elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); - isdead = false; /* keep compiler quiet */ - break; - } + /* + * As long as we hold exclusive lock on the relation, + * normally the only way to see this is if it was inserted + * earlier in our own transaction. However, it can happen + * in system catalogs, since we tend to release write lock + * before commit there. Give a warning if neither case + * applies; but in any case we had better copy it. + */ + if (!is_system_catalog && + !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data))) + elog(WARNING, "concurrent insert in progress within table \"%s\"", + RelationGetRelationName(OldHeap)); + /* treat as live */ + isdead = false; + break; + case HEAPTUPLE_DELETE_IN_PROGRESS: - LockBuffer(buf, BUFFER_LOCK_UNLOCK); + /* + * Similar situation to INSERT_IN_PROGRESS case. + */ + if (!is_system_catalog && + !TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data))) + elog(WARNING, "concurrent delete in progress within table \"%s\"", + RelationGetRelationName(OldHeap)); + /* treat as recently dead */ + *tups_recently_dead += 1; + isdead = false; + break; + default: + elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result"); + isdead = false; /* keep compiler quiet */ + break; + } - if (isdead) - { - *tups_vacuumed += 1; - /* heap rewrite module still needs to see it... */ - if (rewrite_heap_dead_tuple(rwstate, tuple)) + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + + if (isdead) { - /* A previous recently-dead tuple is now known dead */ *tups_vacuumed += 1; - *tups_recently_dead -= 1; + /* heap rewrite module still needs to see it... */ + if (rewrite_heap_dead_tuple(rwstate, tuple)) + { + /* A previous recently-dead tuple is now known dead */ + *tups_vacuumed += 1; + *tups_recently_dead -= 1; + } + + continue; } - continue; } *num_tuples += 1; @@ -912,19 +869,23 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, * In scan-and-sort mode, report increase in number of tuples * scanned */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_SCANNED, *num_tuples); } else { const int ct_index[] = { - PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED, - PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN + PROGRESS_REPACK_HEAP_TUPLES_SCANNED, + PROGRESS_REPACK_HEAP_TUPLES_INSERTED }; int64 ct_val[2]; - reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, - values, isnull, rwstate); + if (!concurrent) + reform_and_rewrite_tuple(tuple, OldHeap, NewHeap, + values, isnull, rwstate); + else + heap_insert_for_repack(tuple, OldHeap, NewHeap, + values, isnull, bistate); /* * In indexscan mode and also VACUUM FULL, report increase in @@ -952,14 +913,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, double n_tuples = 0; /* Report that we are now sorting tuples */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SORT_TUPLES); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SORT_TUPLES); tuplesort_performsort(tuplesort); /* Report that we are now writing new heap */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP); + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP); for (;;) { @@ -972,12 +933,17 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, break; n_tuples += 1; - reform_and_rewrite_tuple(tuple, - OldHeap, NewHeap, - values, isnull, - rwstate); + if (!concurrent) + reform_and_rewrite_tuple(tuple, + OldHeap, NewHeap, + values, isnull, + rwstate); + else + heap_insert_for_repack(tuple, OldHeap, NewHeap, + values, isnull, bistate); + /* Report n_tuples */ - pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN, + pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, n_tuples); } @@ -985,7 +951,10 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap, } /* Write out any remaining tuples, and fsync if needed */ - end_heap_rewrite(rwstate); + if (rwstate) + end_heap_rewrite(rwstate); + if (bistate) + FreeBulkInsertState(bistate); /* Clean up */ pfree(values); @@ -1026,7 +995,7 @@ heapam_scan_analyze_next_block(TableScanDesc scan, ReadStream *stream) } static bool -heapam_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin, +heapam_scan_analyze_next_tuple(TableScanDesc scan, double *liverows, double *deadrows, TupleTableSlot *slot) { @@ -1047,6 +1016,7 @@ heapam_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin, ItemId itemid; HeapTuple targtuple = &hslot->base.tupdata; bool sample_it = false; + TransactionId dead_after; itemid = PageGetItemId(targpage, hscan->rs_cindex); @@ -1069,8 +1039,9 @@ heapam_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin, targtuple->t_data = (HeapTupleHeader) PageGetItem(targpage, itemid); targtuple->t_len = ItemIdGetLength(itemid); - switch (HeapTupleSatisfiesVacuum(targtuple, OldestXmin, - hscan->rs_cbuf)) + switch (HeapTupleSatisfiesVacuumHorizon(targtuple, + hscan->rs_cbuf, + &dead_after)) { case HEAPTUPLE_LIVE: sample_it = true; @@ -2280,7 +2251,7 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate, if (!pagemode) LockBuffer(hscan->rs_cbuf, BUFFER_LOCK_SHARE); - page = (Page) BufferGetPage(hscan->rs_cbuf); + page = BufferGetPage(hscan->rs_cbuf); all_visible = PageIsAllVisible(page) && !scan->rs_snapshot->takenDuringRecovery; maxoffset = PageGetMaxOffsetNumber(page); @@ -2381,27 +2352,84 @@ static void reform_and_rewrite_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, Datum *values, bool *isnull, RewriteState rwstate) +{ + HeapTuple newtuple; + + newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + + /* The heap rewrite module does the rest */ + rewrite_heap_tuple(rwstate, tuple, newtuple); + + heap_freetuple(newtuple); +} + +/* + * Insert tuple when processing REPACK CONCURRENTLY. + * + * rewriteheap.c is not used in the CONCURRENTLY case because it'd be + * difficult to do the same in the catch-up phase (as the logical + * decoding does not provide us with sufficient visibility + * information). Thus we must use heap_insert() both during the + * catch-up and here. + * + * We pass the NO_LOGICAL flag to heap_insert() in order to skip logical + * decoding: as soon as REPACK CONCURRENTLY swaps the relation files, it drops + * this relation, so no logical replication subscription should need the data. + * + * BulkInsertState is used because many tuples are inserted in the typical + * case. + */ +static void +heap_insert_for_repack(HeapTuple tuple, Relation OldHeap, Relation NewHeap, + Datum *values, bool *isnull, BulkInsertState bistate) +{ + HeapTuple newtuple; + + newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull); + + heap_insert(NewHeap, newtuple, GetCurrentCommandId(true), + HEAP_INSERT_NO_LOGICAL, bistate); + + heap_freetuple(newtuple); +} + +/* + * Subroutine for reform_and_rewrite_tuple and heap_insert_for_repack. + * + * Deform the given tuple, set values of dropped columns to NULL, form a new + * tuple and return it. If no attributes need to be changed in this way, a + * copy of the original tuple is returned. Caller is responsible for freeing + * the returned tuple. + * + * XXX this coding assumes that both relations have the same tupledesc. + */ +static HeapTuple +reform_tuple(HeapTuple tuple, Relation OldHeap, Relation NewHeap, + Datum *values, bool *isnull) { TupleDesc oldTupDesc = RelationGetDescr(OldHeap); TupleDesc newTupDesc = RelationGetDescr(NewHeap); - HeapTuple copiedTuple; - int i; + bool needs_reform = false; + + /* Skip work if the tuple doesn't need any attributes changed */ + for (int i = 0; i < newTupDesc->natts; i++) + { + if (TupleDescCompactAttr(newTupDesc, i)->attisdropped && + !heap_attisnull(tuple, i + 1, newTupDesc)) + needs_reform = true; + } + if (!needs_reform) + return heap_copytuple(tuple); heap_deform_tuple(tuple, oldTupDesc, values, isnull); - /* Be sure to null out any dropped columns */ - for (i = 0; i < newTupDesc->natts; i++) + for (int i = 0; i < newTupDesc->natts; i++) { if (TupleDescCompactAttr(newTupDesc, i)->attisdropped) isnull[i] = true; } - copiedTuple = heap_form_tuple(newTupDesc, values, isnull); - - /* The heap rewrite module does the rest */ - rewrite_heap_tuple(rwstate, tuple, copiedTuple); - - heap_freetuple(copiedTuple); + return heap_form_tuple(newTupDesc, values, isnull); } /* @@ -2517,7 +2545,8 @@ BitmapHeapScanNextBlock(TableScanDesc scan, /* * Prune and repair fragmentation for the whole page, if possible. */ - heap_page_prune_opt(scan->rs_rd, buffer); + heap_page_prune_opt(scan->rs_rd, buffer, &hscan->rs_vmbuffer, + scan->rs_flags & SO_HINT_REL_READ_ONLY); /* * We must hold share lock on the buffer content while examining tuple diff --git a/src/backend/access/heap/heapam_indexscan.c b/src/backend/access/heap/heapam_indexscan.c new file mode 100644 index 0000000000000..33d14f1de7d52 --- /dev/null +++ b/src/backend/access/heap/heapam_indexscan.c @@ -0,0 +1,298 @@ +/*------------------------------------------------------------------------- + * + * heapam_indexscan.c + * heap table plain index scan and index-only scan code + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/access/heap/heapam_indexscan.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "access/relscan.h" +#include "storage/predicate.h" + + +/* ------------------------------------------------------------------------ + * Index Scan Callbacks for heap AM + * ------------------------------------------------------------------------ + */ + +IndexFetchTableData * +heapam_index_fetch_begin(Relation rel, uint32 flags) +{ + IndexFetchHeapData *hscan = palloc0_object(IndexFetchHeapData); + + hscan->xs_base.rel = rel; + hscan->xs_base.flags = flags; + hscan->xs_cbuf = InvalidBuffer; + hscan->xs_blk = InvalidBlockNumber; + hscan->xs_vmbuffer = InvalidBuffer; + + return &hscan->xs_base; +} + +void +heapam_index_fetch_reset(IndexFetchTableData *scan) +{ + /* + * Resets are a no-op. + * + * Deliberately avoid dropping pins now held in xs_cbuf and xs_vmbuffer. + * This saves cycles during certain tight nested loop joins (it can avoid + * repeated pinning and unpinning of the same buffer across rescans). + */ +} + +void +heapam_index_fetch_end(IndexFetchTableData *scan) +{ + IndexFetchHeapData *hscan = (IndexFetchHeapData *) scan; + + /* drop pin if there's a pinned heap page */ + if (BufferIsValid(hscan->xs_cbuf)) + ReleaseBuffer(hscan->xs_cbuf); + + /* drop pin if there's a pinned visibility map page */ + if (BufferIsValid(hscan->xs_vmbuffer)) + ReleaseBuffer(hscan->xs_vmbuffer); + + pfree(hscan); +} + +/* + * heap_hot_search_buffer - search HOT chain for tuple satisfying snapshot + * + * On entry, *tid is the TID of a tuple (either a simple tuple, or the root + * of a HOT chain), and buffer is the buffer holding this tuple. We search + * for the first chain member satisfying the given snapshot. If one is + * found, we update *tid to reference that tuple's offset number, and + * return true. If no match, return false without modifying *tid. + * + * heapTuple is a caller-supplied buffer. When a match is found, we return + * the tuple here, in addition to updating *tid. If no match is found, the + * contents of this buffer on return are undefined. + * + * If all_dead is not NULL, we check non-visible tuples to see if they are + * globally dead; *all_dead is set true if all members of the HOT chain + * are vacuumable, false if not. + * + * Unlike heap_fetch, the caller must already have pin and (at least) share + * lock on the buffer; it is still pinned/locked at exit. + */ +bool +heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer, + Snapshot snapshot, HeapTuple heapTuple, + bool *all_dead, bool first_call) +{ + Page page = BufferGetPage(buffer); + TransactionId prev_xmax = InvalidTransactionId; + BlockNumber blkno; + OffsetNumber offnum; + bool at_chain_start; + bool valid; + bool skip; + GlobalVisState *vistest = NULL; + + /* If this is not the first call, previous call returned a (live!) tuple */ + if (all_dead) + *all_dead = first_call; + + blkno = ItemPointerGetBlockNumber(tid); + offnum = ItemPointerGetOffsetNumber(tid); + at_chain_start = first_call; + skip = !first_call; + + /* XXX: we should assert that a snapshot is pushed or registered */ + Assert(TransactionIdIsValid(RecentXmin)); + Assert(BufferGetBlockNumber(buffer) == blkno); + + /* Scan through possible multiple members of HOT-chain */ + for (;;) + { + ItemId lp; + + /* check for bogus TID */ + if (offnum < FirstOffsetNumber || offnum > PageGetMaxOffsetNumber(page)) + break; + + lp = PageGetItemId(page, offnum); + + /* check for unused, dead, or redirected items */ + if (!ItemIdIsNormal(lp)) + { + /* We should only see a redirect at start of chain */ + if (ItemIdIsRedirected(lp) && at_chain_start) + { + /* Follow the redirect */ + offnum = ItemIdGetRedirect(lp); + at_chain_start = false; + continue; + } + /* else must be end of chain */ + break; + } + + /* + * Update heapTuple to point to the element of the HOT chain we're + * currently investigating. Having t_self set correctly is important + * because the SSI checks and the *Satisfies routine for historical + * MVCC snapshots need the correct tid to decide about the visibility. + */ + heapTuple->t_data = (HeapTupleHeader) PageGetItem(page, lp); + heapTuple->t_len = ItemIdGetLength(lp); + heapTuple->t_tableOid = RelationGetRelid(relation); + ItemPointerSet(&heapTuple->t_self, blkno, offnum); + + /* + * Shouldn't see a HEAP_ONLY tuple at chain start. + */ + if (at_chain_start && HeapTupleIsHeapOnly(heapTuple)) + break; + + /* + * The xmin should match the previous xmax value, else chain is + * broken. + */ + if (TransactionIdIsValid(prev_xmax) && + !TransactionIdEquals(prev_xmax, + HeapTupleHeaderGetXmin(heapTuple->t_data))) + break; + + /* + * When first_call is true (and thus, skip is initially false) we'll + * return the first tuple we find. But on later passes, heapTuple + * will initially be pointing to the tuple we returned last time. + * Returning it again would be incorrect (and would loop forever), so + * we skip it and return the next match we find. + */ + if (!skip) + { + /* If it's visible per the snapshot, we must return it */ + valid = HeapTupleSatisfiesVisibility(heapTuple, snapshot, buffer); + HeapCheckForSerializableConflictOut(valid, relation, heapTuple, + buffer, snapshot); + + if (valid) + { + ItemPointerSetOffsetNumber(tid, offnum); + PredicateLockTID(relation, &heapTuple->t_self, snapshot, + HeapTupleHeaderGetXmin(heapTuple->t_data)); + if (all_dead) + *all_dead = false; + return true; + } + } + skip = false; + + /* + * If we can't see it, maybe no one else can either. At caller + * request, check whether all chain members are dead to all + * transactions. + * + * Note: if you change the criterion here for what is "dead", fix the + * planner's get_actual_variable_range() function to match. + */ + if (all_dead && *all_dead) + { + if (!vistest) + vistest = GlobalVisTestFor(relation); + + if (!HeapTupleIsSurelyDead(heapTuple, vistest)) + *all_dead = false; + } + + /* + * Check to see if HOT chain continues past this tuple; if so fetch + * the next offnum and loop around. + */ + if (HeapTupleIsHotUpdated(heapTuple)) + { + Assert(ItemPointerGetBlockNumber(&heapTuple->t_data->t_ctid) == + blkno); + offnum = ItemPointerGetOffsetNumber(&heapTuple->t_data->t_ctid); + at_chain_start = false; + prev_xmax = HeapTupleHeaderGetUpdateXid(heapTuple->t_data); + } + else + break; /* end of chain */ + + } + + return false; +} + +bool +heapam_index_fetch_tuple(struct IndexFetchTableData *scan, + ItemPointer tid, + Snapshot snapshot, + TupleTableSlot *slot, + bool *heap_continue, bool *all_dead) +{ + IndexFetchHeapData *hscan = (IndexFetchHeapData *) scan; + BufferHeapTupleTableSlot *bslot = (BufferHeapTupleTableSlot *) slot; + bool got_heap_tuple; + + Assert(TTS_IS_BUFFERTUPLE(slot)); + + /* We can skip the buffer-switching logic if we're on the same page. */ + if (hscan->xs_blk != ItemPointerGetBlockNumber(tid)) + { + Assert(!*heap_continue); + + /* Remember this buffer's block number for next time */ + hscan->xs_blk = ItemPointerGetBlockNumber(tid); + + if (BufferIsValid(hscan->xs_cbuf)) + ReleaseBuffer(hscan->xs_cbuf); + + hscan->xs_cbuf = ReadBuffer(hscan->xs_base.rel, hscan->xs_blk); + + /* + * Prune page when it is pinned for the first time + */ + heap_page_prune_opt(hscan->xs_base.rel, hscan->xs_cbuf, + &hscan->xs_vmbuffer, + hscan->xs_base.flags & SO_HINT_REL_READ_ONLY); + } + + Assert(BufferGetBlockNumber(hscan->xs_cbuf) == hscan->xs_blk); + Assert(hscan->xs_blk == ItemPointerGetBlockNumber(tid)); + + /* Obtain share-lock on the buffer so we can examine visibility */ + LockBuffer(hscan->xs_cbuf, BUFFER_LOCK_SHARE); + got_heap_tuple = heap_hot_search_buffer(tid, + hscan->xs_base.rel, + hscan->xs_cbuf, + snapshot, + &bslot->base.tupdata, + all_dead, + !*heap_continue); + bslot->base.tupdata.t_self = *tid; + LockBuffer(hscan->xs_cbuf, BUFFER_LOCK_UNLOCK); + + if (got_heap_tuple) + { + /* + * Only in a non-MVCC snapshot can more than one member of the HOT + * chain be visible. + */ + *heap_continue = !IsMVCCLikeSnapshot(snapshot); + + slot->tts_tableOid = RelationGetRelid(scan->rel); + ExecStoreBufferHeapTuple(&bslot->base.tupdata, slot, hscan->xs_cbuf); + } + else + { + /* We've reached the end of the HOT chain. */ + *heap_continue = false; + } + + return got_heap_tuple; +} diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 05f6946fe60d2..361b76e506528 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -55,7 +55,7 @@ * HeapTupleSatisfiesAny() * all tuples are visible * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -80,10 +80,38 @@ /* - * SetHintBits() + * To be allowed to set hint bits, SetHintBits() needs to call + * BufferBeginSetHintBits(). However, that's not free, and some callsites call + * SetHintBits() on many tuples in a row. For those it makes sense to amortize + * the cost of BufferBeginSetHintBits(). Additionally it's desirable to defer + * the cost of BufferBeginSetHintBits() until a hint bit needs to actually be + * set. This enum serves as the necessary state space passed to + * SetHintBitsExt(). + */ +typedef enum SetHintBitsState +{ + /* not yet checked if hint bits may be set */ + SHB_INITIAL, + /* failed to get permission to set hint bits, don't check again */ + SHB_DISABLED, + /* allowed to set hint bits */ + SHB_ENABLED, +} SetHintBitsState; + +/* + * SetHintBitsExt() * * Set commit/abort hint bits on a tuple, if appropriate at this time. * + * To be allowed to set a hint bit on a tuple, the page must not be undergoing + * IO at this time (otherwise we e.g. could corrupt PG's page checksum or even + * the filesystem's, as is known to happen with btrfs). + * + * The right to set a hint bit can be acquired on a page level with + * BufferBeginSetHintBits(). Only a single backend gets the right to set hint + * bits at a time. Alternatively, if called with a NULL SetHintBitsState*, + * hint bits are set with BufferSetHintBits16(). + * * It is only safe to set a transaction-committed hint bit if we know the * transaction's commit record is guaranteed to be flushed to disk before the * buffer, or if the table is temporary or unlogged and will be obliterated by @@ -111,24 +139,67 @@ * InvalidTransactionId if no check is needed. */ static inline void -SetHintBits(HeapTupleHeader tuple, Buffer buffer, - uint16 infomask, TransactionId xid) +SetHintBitsExt(HeapTupleHeader tuple, Buffer buffer, + uint16 infomask, TransactionId xid, SetHintBitsState *state) { + /* + * In batched mode, if we previously did not get permission to set hint + * bits, don't try again - in all likelihood IO is still going on. + */ + if (state && *state == SHB_DISABLED) + return; + if (TransactionIdIsValid(xid)) { - /* NB: xid must be known committed here! */ - XLogRecPtr commitLSN = TransactionIdGetCommitLSN(xid); + if (BufferIsPermanent(buffer)) + { + /* NB: xid must be known committed here! */ + XLogRecPtr commitLSN = TransactionIdGetCommitLSN(xid); - if (BufferIsPermanent(buffer) && XLogNeedsFlush(commitLSN) && - BufferGetLSNAtomic(buffer) < commitLSN) + if (XLogNeedsFlush(commitLSN) && + BufferGetLSNAtomic(buffer) < commitLSN) + { + /* not flushed and no LSN interlock, so don't set hint */ + return; + } + } + } + + /* + * If we're not operating in batch mode, use BufferSetHintBits16() to mark + * the page dirty, that's cheaper than + * BufferBeginSetHintBits()/BufferFinishSetHintBits(). That's important + * for cases where we set a lot of hint bits on a page individually. + */ + if (!state) + { + BufferSetHintBits16(&tuple->t_infomask, + tuple->t_infomask | infomask, buffer); + return; + } + + if (*state == SHB_INITIAL) + { + if (!BufferBeginSetHintBits(buffer)) { - /* not flushed and no LSN interlock, so don't set hint */ + *state = SHB_DISABLED; return; } - } + *state = SHB_ENABLED; + } tuple->t_infomask |= infomask; - MarkBufferDirtyHint(buffer, true); +} + +/* + * Simple wrapper around SetHintBitsExt(), use when operating on a single + * tuple. + */ +static inline void +SetHintBits(HeapTupleHeader tuple, Buffer buffer, + uint16 infomask, TransactionId xid) +{ + SetHintBitsExt(tuple, buffer, infomask, xid, NULL); } /* @@ -141,9 +212,65 @@ void HeapTupleSetHintBits(HeapTupleHeader tuple, Buffer buffer, uint16 infomask, TransactionId xid) { + /* + * The uses from heapam.c rely on being able to perform the hint bit + * updates, which can only be guaranteed if we are holding an exclusive + * lock on the buffer - which all callers are doing. + */ + Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE)); + SetHintBits(tuple, buffer, infomask, xid); } +/* + * If HEAP_MOVED_OFF or HEAP_MOVED_IN are set on the tuple, remove them and + * adjust hint bits. See the comment for SetHintBits() for more background. + * + * This helper returns false if the row ought to be invisible, true otherwise. + */ +static inline bool +HeapTupleCleanMoved(HeapTupleHeader tuple, Buffer buffer) +{ + TransactionId xvac; + + /* only used by pre-9.0 binary upgrades */ + if (likely(!(tuple->t_infomask & (HEAP_MOVED_OFF | HEAP_MOVED_IN)))) + return true; + + xvac = HeapTupleHeaderGetXvac(tuple); + + if (TransactionIdIsCurrentTransactionId(xvac)) + elog(ERROR, "encountered tuple with HEAP_MOVED considered current"); + + if (TransactionIdIsInProgress(xvac)) + elog(ERROR, "encountered tuple with HEAP_MOVED considered in-progress"); + + if (tuple->t_infomask & HEAP_MOVED_OFF) + { + if (TransactionIdDidCommit(xvac)) + { + SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, + InvalidTransactionId); + return false; + } + SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, + InvalidTransactionId); + } + else if (tuple->t_infomask & HEAP_MOVED_IN) + { + if (TransactionIdDidCommit(xvac)) + SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, + InvalidTransactionId); + else + { + SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, + InvalidTransactionId); + return false; + } + } + + return true; +} /* * HeapTupleSatisfiesSelf @@ -179,45 +306,8 @@ HeapTupleSatisfiesSelf(HeapTuple htup, Snapshot snapshot, Buffer buffer) if (HeapTupleHeaderXminInvalid(tuple)) return false; - /* Used by pre-9.0 binary upgrades */ - if (tuple->t_infomask & HEAP_MOVED_OFF) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return false; - if (!TransactionIdIsInProgress(xvac)) - { - if (TransactionIdDidCommit(xvac)) - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - } - } - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_IN) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (!TransactionIdIsCurrentTransactionId(xvac)) - { - if (TransactionIdIsInProgress(xvac)) - return false; - if (TransactionIdDidCommit(xvac)) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - else - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - } - } + if (!HeapTupleCleanMoved(tuple, buffer)) + return false; else if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) { if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid invalid */ @@ -372,45 +462,8 @@ HeapTupleSatisfiesToast(HeapTuple htup, Snapshot snapshot, if (HeapTupleHeaderXminInvalid(tuple)) return false; - /* Used by pre-9.0 binary upgrades */ - if (tuple->t_infomask & HEAP_MOVED_OFF) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return false; - if (!TransactionIdIsInProgress(xvac)) - { - if (TransactionIdDidCommit(xvac)) - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - } - } - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_IN) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (!TransactionIdIsCurrentTransactionId(xvac)) - { - if (TransactionIdIsInProgress(xvac)) - return false; - if (TransactionIdDidCommit(xvac)) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - else - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - } - } + if (!HeapTupleCleanMoved(tuple, buffer)) + return false; /* * An invalid Xmin can be left behind by a speculative insertion that @@ -468,45 +521,8 @@ HeapTupleSatisfiesUpdate(HeapTuple htup, CommandId curcid, if (HeapTupleHeaderXminInvalid(tuple)) return TM_Invisible; - /* Used by pre-9.0 binary upgrades */ - if (tuple->t_infomask & HEAP_MOVED_OFF) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return TM_Invisible; - if (!TransactionIdIsInProgress(xvac)) - { - if (TransactionIdDidCommit(xvac)) - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return TM_Invisible; - } - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - } - } - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_IN) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (!TransactionIdIsCurrentTransactionId(xvac)) - { - if (TransactionIdIsInProgress(xvac)) - return TM_Invisible; - if (TransactionIdDidCommit(xvac)) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - else - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return TM_Invisible; - } - } - } + else if (!HeapTupleCleanMoved(tuple, buffer)) + return TM_Invisible; else if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) { if (HeapTupleHeaderGetCmin(tuple) >= curcid) @@ -756,45 +772,8 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot, if (HeapTupleHeaderXminInvalid(tuple)) return false; - /* Used by pre-9.0 binary upgrades */ - if (tuple->t_infomask & HEAP_MOVED_OFF) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return false; - if (!TransactionIdIsInProgress(xvac)) - { - if (TransactionIdDidCommit(xvac)) - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - } - } - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_IN) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (!TransactionIdIsCurrentTransactionId(xvac)) - { - if (TransactionIdIsInProgress(xvac)) - return false; - if (TransactionIdDidCommit(xvac)) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - else - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - } - } + if (!HeapTupleCleanMoved(tuple, buffer)) + return false; else if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) { if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid invalid */ @@ -956,9 +935,9 @@ HeapTupleSatisfiesDirty(HeapTuple htup, Snapshot snapshot, * inserting/deleting transaction was still running --- which was more cycles * and more contention on ProcArrayLock. */ -static bool +static inline bool HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, - Buffer buffer) + Buffer buffer, SetHintBitsState *state) { HeapTupleHeader tuple = htup->t_data; @@ -979,45 +958,8 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, if (HeapTupleHeaderXminInvalid(tuple)) return false; - /* Used by pre-9.0 binary upgrades */ - if (tuple->t_infomask & HEAP_MOVED_OFF) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return false; - if (!XidInMVCCSnapshot(xvac, snapshot)) - { - if (TransactionIdDidCommit(xvac)) - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - } - } - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_IN) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (!TransactionIdIsCurrentTransactionId(xvac)) - { - if (XidInMVCCSnapshot(xvac, snapshot)) - return false; - if (TransactionIdDidCommit(xvac)) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - else - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return false; - } - } - } + if (!HeapTupleCleanMoved(tuple, buffer)) + return false; else if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) { if (HeapTupleHeaderGetCmin(tuple) >= snapshot->curcid) @@ -1050,8 +992,8 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, if (!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmax(tuple))) { /* deleting subtransaction must have aborted */ - SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, - InvalidTransactionId); + SetHintBitsExt(tuple, buffer, HEAP_XMAX_INVALID, + InvalidTransactionId, state); return true; } @@ -1063,13 +1005,13 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, else if (XidInMVCCSnapshot(HeapTupleHeaderGetRawXmin(tuple), snapshot)) return false; else if (TransactionIdDidCommit(HeapTupleHeaderGetRawXmin(tuple))) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - HeapTupleHeaderGetRawXmin(tuple)); + SetHintBitsExt(tuple, buffer, HEAP_XMIN_COMMITTED, + HeapTupleHeaderGetRawXmin(tuple), state); else { /* it must have aborted or crashed */ - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); + SetHintBitsExt(tuple, buffer, HEAP_XMIN_INVALID, + InvalidTransactionId, state); return false; } } @@ -1132,14 +1074,14 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot, if (!TransactionIdDidCommit(HeapTupleHeaderGetRawXmax(tuple))) { /* it must have aborted or crashed */ - SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, - InvalidTransactionId); + SetHintBitsExt(tuple, buffer, HEAP_XMAX_INVALID, + InvalidTransactionId, state); return true; } /* xmax transaction committed */ - SetHintBits(tuple, buffer, HEAP_XMAX_COMMITTED, - HeapTupleHeaderGetRawXmax(tuple)); + SetHintBitsExt(tuple, buffer, HEAP_XMAX_COMMITTED, + HeapTupleHeaderGetRawXmax(tuple), state); } else { @@ -1222,43 +1164,8 @@ HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *de { if (HeapTupleHeaderXminInvalid(tuple)) return HEAPTUPLE_DEAD; - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_OFF) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return HEAPTUPLE_DELETE_IN_PROGRESS; - if (TransactionIdIsInProgress(xvac)) - return HEAPTUPLE_DELETE_IN_PROGRESS; - if (TransactionIdDidCommit(xvac)) - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return HEAPTUPLE_DEAD; - } - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - } - /* Used by pre-9.0 binary upgrades */ - else if (tuple->t_infomask & HEAP_MOVED_IN) - { - TransactionId xvac = HeapTupleHeaderGetXvac(tuple); - - if (TransactionIdIsCurrentTransactionId(xvac)) - return HEAPTUPLE_INSERT_IN_PROGRESS; - if (TransactionIdIsInProgress(xvac)) - return HEAPTUPLE_INSERT_IN_PROGRESS; - if (TransactionIdDidCommit(xvac)) - SetHintBits(tuple, buffer, HEAP_XMIN_COMMITTED, - InvalidTransactionId); - else - { - SetHintBits(tuple, buffer, HEAP_XMIN_INVALID, - InvalidTransactionId); - return HEAPTUPLE_DEAD; - } - } + else if (!HeapTupleCleanMoved(tuple, buffer)) + return HEAPTUPLE_DEAD; else if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetRawXmin(tuple))) { if (tuple->t_infomask & HEAP_XMAX_INVALID) /* xid invalid */ @@ -1447,7 +1354,7 @@ HeapTupleSatisfiesNonVacuumable(HeapTuple htup, Snapshot snapshot, { Assert(TransactionIdIsValid(dead_after)); - if (GlobalVisTestIsRemovableXid(snapshot->vistest, dead_after)) + if (GlobalVisTestIsRemovableXid(snapshot->vistest, dead_after, true)) res = HEAPTUPLE_DEAD; } else @@ -1513,7 +1420,8 @@ HeapTupleIsSurelyDead(HeapTuple htup, GlobalVisState *vistest) /* Deleter committed, so tuple is dead if the XID is old enough. */ return GlobalVisTestIsRemovableXid(vistest, - HeapTupleHeaderGetRawXmax(tuple)); + HeapTupleHeaderGetRawXmax(tuple), + true); } /* @@ -1762,6 +1670,54 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot, return true; } +/* + * Perform HeapTupleSatisfiesMVCC() on each passed in tuple. This is more + * efficient than doing HeapTupleSatisfiesMVCC() one-by-one. + * + * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's + * visibility is stored in batchmvcc->visible[]. In addition, + * ->vistuples_dense is set to contain the offsets of visible tuples. + * + * The reason this is more efficient than HeapTupleSatisfiesMVCC() is that it + * avoids a cross-translation-unit function call for each tuple, allows the + * compiler to optimize across calls to HeapTupleSatisfiesMVCC and allows + * setting hint bits more efficiently (see the one BufferFinishSetHintBits() + * call below). + * + * Returns the number of visible tuples. + */ +int +HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer, + int ntups, + BatchMVCCState *batchmvcc, + OffsetNumber *vistuples_dense) +{ + int nvis = 0; + SetHintBitsState state = SHB_INITIAL; + + Assert(IsMVCCSnapshot(snapshot)); + + for (int i = 0; i < ntups; i++) + { + bool valid; + HeapTuple tup = &batchmvcc->tuples[i]; + + valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer, &state); + batchmvcc->visible[i] = valid; + + if (likely(valid)) + { + vistuples_dense[nvis] = tup->t_self.ip_posid; + nvis++; + } + } + + if (state == SHB_ENABLED) + BufferFinishSetHintBits(buffer, true, true); + + return nvis; +} + /* * HeapTupleSatisfiesVisibility * True iff heap tuple satisfies a time qual. @@ -1778,7 +1734,7 @@ HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, Buffer buffer) switch (snapshot->snapshot_type) { case SNAPSHOT_MVCC: - return HeapTupleSatisfiesMVCC(htup, snapshot, buffer); + return HeapTupleSatisfiesMVCC(htup, snapshot, buffer, NULL); case SNAPSHOT_SELF: return HeapTupleSatisfiesSelf(htup, snapshot, buffer); case SNAPSHOT_ANY: diff --git a/src/backend/access/heap/heapam_xlog.c b/src/backend/access/heap/heapam_xlog.c index 30f4c2d3c6719..9ed7024e81474 100644 --- a/src/backend/access/heap/heapam_xlog.c +++ b/src/backend/access/heap/heapam_xlog.c @@ -3,7 +3,7 @@ * heapam_xlog.c * WAL replay logic for heap access method. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -35,7 +35,10 @@ heap_xlog_prune_freeze(XLogReaderState *record) Buffer buffer; RelFileLocator rlocator; BlockNumber blkno; - XLogRedoAction action; + Buffer vmbuffer = InvalidBuffer; + uint8 vmflags = 0; + Size freespace = 0; + bool do_update_fsm = false; XLogRecGetBlockTag(record, 0, &rlocator, NULL, &blkno); memcpy(&xlrec, maindataptr, SizeOfHeapPrune); @@ -50,11 +53,22 @@ heap_xlog_prune_freeze(XLogReaderState *record) Assert((xlrec.flags & XLHP_CLEANUP_LOCK) != 0 || (xlrec.flags & (XLHP_HAS_REDIRECTIONS | XLHP_HAS_DEAD_ITEMS)) == 0); + if (xlrec.flags & XLHP_VM_ALL_VISIBLE) + { + vmflags = VISIBILITYMAP_ALL_VISIBLE; + if (xlrec.flags & XLHP_VM_ALL_FROZEN) + vmflags |= VISIBILITYMAP_ALL_FROZEN; + } + /* - * We are about to remove and/or freeze tuples. In Hot Standby mode, - * ensure that there are no queries running for which the removed tuples - * are still visible or which still consider the frozen xids as running. - * The conflict horizon XID comes after xl_heap_prune. + * After xl_heap_prune is the optional snapshot conflict horizon. + * + * In Hot Standby mode, we must ensure that there are no running queries + * which would conflict with the changes in this record. That means we + * can't replay this record if it removes tuples that are still visible to + * transactions on the standby, freeze tuples with xids that are still + * considered running on the standby, or set a page as all-visible in the + * VM if it isn't all-visible to all transactions on the standby. */ if ((xlrec.flags & XLHP_HAS_CONFLICT_HORIZON) != 0) { @@ -71,14 +85,14 @@ heap_xlog_prune_freeze(XLogReaderState *record) } /* - * If we have a full-page image, restore it and we're done. + * If we have a full-page image of the heap block, restore it and we're + * done with the heap block. */ - action = XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, - (xlrec.flags & XLHP_CLEANUP_LOCK) != 0, - &buffer); - if (action == BLK_NEEDS_REDO) + if (XLogReadBufferForRedoExtended(record, 0, RBM_NORMAL, + (xlrec.flags & XLHP_CLEANUP_LOCK) != 0, + &buffer) == BLK_NEEDS_REDO) { - Page page = (Page) BufferGetPage(buffer); + Page page = BufferGetPage(buffer); OffsetNumber *redirected; OffsetNumber *nowdead; OffsetNumber *nowunused; @@ -90,6 +104,7 @@ heap_xlog_prune_freeze(XLogReaderState *record) xlhp_freeze_plan *plans; OffsetNumber *frz_offsets; char *dataptr = XLogRecGetBlockData(record, 0, &datalen); + bool do_prune; heap_xlog_deserialize_prune_and_freeze(dataptr, xlrec.flags, &nplans, &plans, &frz_offsets, @@ -97,11 +112,16 @@ heap_xlog_prune_freeze(XLogReaderState *record) &ndead, &nowdead, &nunused, &nowunused); + do_prune = nredirected > 0 || ndead > 0 || nunused > 0; + + /* Ensure the record does something */ + Assert(do_prune || nplans > 0 || vmflags & VISIBILITYMAP_VALID_BITS); + /* * Update all line pointers per the record, and repair fragmentation * if needed. */ - if (nredirected > 0 || ndead > 0 || nunused > 0) + if (do_prune) heap_page_prune_execute(buffer, (xlrec.flags & XLHP_CLEANUP_LOCK) == 0, redirected, nredirected, @@ -139,172 +159,101 @@ heap_xlog_prune_freeze(XLogReaderState *record) Assert((char *) frz_offsets == dataptr + datalen); /* - * Note: we don't worry about updating the page's prunability hints. - * At worst this will cause an extra prune cycle to occur soon. + * The critical integrity requirement here is that we must never end + * up with the visibility map bit set and the page-level + * PD_ALL_VISIBLE bit unset. If that were to occur, a subsequent page + * modification would fail to clear the visibility map bit. */ - - PageSetLSN(page, lsn); - MarkBufferDirty(buffer); - } - - /* - * If we released any space or line pointers, update the free space map. - * - * Do this regardless of a full-page image being applied, since the FSM - * data is not in the page anyway. - */ - if (BufferIsValid(buffer)) - { - if (xlrec.flags & (XLHP_HAS_REDIRECTIONS | - XLHP_HAS_DEAD_ITEMS | - XLHP_HAS_NOW_UNUSED_ITEMS)) + if (vmflags & VISIBILITYMAP_VALID_BITS) { - Size freespace = PageGetHeapFreeSpace(BufferGetPage(buffer)); - - UnlockReleaseBuffer(buffer); - - XLogRecordPageWithFreeSpace(rlocator, blkno, freespace); + PageSetAllVisible(page); + PageClearPrunable(page); } - else - UnlockReleaseBuffer(buffer); - } -} -/* - * Replay XLOG_HEAP2_VISIBLE records. - * - * The critical integrity requirement here is that we must never end up with - * a situation where the visibility map bit is set, and the page-level - * PD_ALL_VISIBLE bit is clear. If that were to occur, then a subsequent - * page modification would fail to clear the visibility map bit. - */ -static void -heap_xlog_visible(XLogReaderState *record) -{ - XLogRecPtr lsn = record->EndRecPtr; - xl_heap_visible *xlrec = (xl_heap_visible *) XLogRecGetData(record); - Buffer vmbuffer = InvalidBuffer; - Buffer buffer; - Page page; - RelFileLocator rlocator; - BlockNumber blkno; - XLogRedoAction action; - - Assert((xlrec->flags & VISIBILITYMAP_XLOG_VALID_BITS) == xlrec->flags); - - XLogRecGetBlockTag(record, 1, &rlocator, NULL, &blkno); - - /* - * If there are any Hot Standby transactions running that have an xmin - * horizon old enough that this page isn't all-visible for them, they - * might incorrectly decide that an index-only scan can skip a heap fetch. - * - * NB: It might be better to throw some kind of "soft" conflict here that - * forces any index-only scan that is in flight to perform heap fetches, - * rather than killing the transaction outright. - */ - if (InHotStandby) - ResolveRecoveryConflictWithSnapshot(xlrec->snapshotConflictHorizon, - xlrec->flags & VISIBILITYMAP_XLOG_CATALOG_REL, - rlocator); + MarkBufferDirty(buffer); - /* - * Read the heap page, if it still exists. If the heap file has dropped or - * truncated later in recovery, we don't need to update the page, but we'd - * better still update the visibility map. - */ - action = XLogReadBufferForRedo(record, 1, &buffer); - if (action == BLK_NEEDS_REDO) - { /* - * We don't bump the LSN of the heap page when setting the visibility - * map bit (unless checksums or wal_hint_bits is enabled, in which - * case we must). This exposes us to torn page hazards, but since - * we're not inspecting the existing page contents in any way, we - * don't care. + * See log_heap_prune_and_freeze() for commentary on when we set the + * heap page LSN. */ - page = BufferGetPage(buffer); - - PageSetAllVisible(page); - - if (XLogHintBitIsNeeded()) + if (do_prune || nplans > 0 || + ((vmflags & VISIBILITYMAP_VALID_BITS) && XLogHintBitIsNeeded())) PageSetLSN(page, lsn); - MarkBufferDirty(buffer); - } - else if (action == BLK_RESTORED) - { /* - * If heap block was backed up, we already restored it and there's - * nothing more to do. (This can only happen with checksums or - * wal_log_hints enabled.) + * Note: we don't worry about updating the page's prunability hints. + * At worst this will cause an extra prune cycle to occur soon. */ } + /* + * If we 1) released any space or line pointers or 2) set PD_ALL_VISIBLE + * or the VM, update the freespace map. + * + * Even when no actual space is freed (when only marking the page + * all-visible or frozen), we still update the FSM. Because the FSM is + * unlogged and maintained heuristically, it often becomes stale on + * standbys. If such a standby is later promoted and runs VACUUM, it will + * skip recalculating free space for pages that were marked + * all-visible/all-frozen. FreeSpaceMapVacuum() can then propagate overly + * optimistic free space values upward, causing future insertions to + * select pages that turn out to be unusable. In bulk, this can lead to + * long stalls. + * + * To prevent this, always update the FSM even when only marking a page + * all-visible/all-frozen. + * + * Do this regardless of whether a full-page image is logged, since FSM + * data is not part of the page itself. + */ if (BufferIsValid(buffer)) { - Size space = PageGetFreeSpace(BufferGetPage(buffer)); - - UnlockReleaseBuffer(buffer); + if ((xlrec.flags & (XLHP_HAS_REDIRECTIONS | + XLHP_HAS_DEAD_ITEMS | + XLHP_HAS_NOW_UNUSED_ITEMS)) || + (vmflags & VISIBILITYMAP_VALID_BITS)) + { + freespace = PageGetHeapFreeSpace(BufferGetPage(buffer)); + do_update_fsm = true; + } /* - * Since FSM is not WAL-logged and only updated heuristically, it - * easily becomes stale in standbys. If the standby is later promoted - * and runs VACUUM, it will skip updating individual free space - * figures for pages that became all-visible (or all-frozen, depending - * on the vacuum mode,) which is troublesome when FreeSpaceMapVacuum - * propagates too optimistic free space values to upper FSM layers; - * later inserters try to use such pages only to find out that they - * are unusable. This can cause long stalls when there are many such - * pages. - * - * Forestall those problems by updating FSM's idea about a page that - * is becoming all-visible or all-frozen. - * - * Do this regardless of a full-page image being applied, since the - * FSM data is not in the page anyway. + * We want to avoid holding an exclusive lock on the heap buffer while + * doing IO (either of the FSM or the VM), so we'll release it now. */ - if (xlrec->flags & VISIBILITYMAP_VALID_BITS) - XLogRecordPageWithFreeSpace(rlocator, blkno, space); + UnlockReleaseBuffer(buffer); } /* - * Even if we skipped the heap page update due to the LSN interlock, it's - * still safe to update the visibility map. Any WAL record that clears - * the visibility map bit does so before checking the page LSN, so any - * bits that need to be cleared will still be cleared. + * Now read and update the VM block. + * + * We must redo changes to the VM even if the heap page was skipped due to + * LSN interlock. See comment in heap_xlog_multi_insert() for more details + * on replaying changes to the VM. */ - if (XLogReadBufferForRedoExtended(record, 0, RBM_ZERO_ON_ERROR, false, + if ((vmflags & VISIBILITYMAP_VALID_BITS) && + XLogReadBufferForRedoExtended(record, 1, + RBM_ZERO_ON_ERROR, + false, &vmbuffer) == BLK_NEEDS_REDO) { Page vmpage = BufferGetPage(vmbuffer); - Relation reln; - uint8 vmbits; /* initialize the page if it was read as zeros */ if (PageIsNew(vmpage)) PageInit(vmpage, BLCKSZ, 0); - /* remove VISIBILITYMAP_XLOG_* */ - vmbits = xlrec->flags & VISIBILITYMAP_VALID_BITS; + visibilitymap_set(blkno, vmbuffer, vmflags, rlocator); - /* - * XLogReadBufferForRedoExtended locked the buffer. But - * visibilitymap_set will handle locking itself. - */ - LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); - - reln = CreateFakeRelcacheEntry(rlocator); - visibilitymap_pin(reln, blkno, &vmbuffer); - - visibilitymap_set(reln, blkno, InvalidBuffer, lsn, vmbuffer, - xlrec->snapshotConflictHorizon, vmbits); - - ReleaseBuffer(vmbuffer); - FreeFakeRelcacheEntry(reln); + Assert(BufferIsDirty(vmbuffer)); + PageSetLSN(vmpage, lsn); } - else if (BufferIsValid(vmbuffer)) + + if (BufferIsValid(vmbuffer)) UnlockReleaseBuffer(vmbuffer); + + if (do_update_fsm) + XLogRecordPageWithFreeSpace(rlocator, blkno, freespace); } /* @@ -344,7 +293,7 @@ heap_xlog_delete(XLogReaderState *record) xl_heap_delete *xlrec = (xl_heap_delete *) XLogRecGetData(record); Buffer buffer; Page page; - ItemId lp = NULL; + ItemId lp; HeapTupleHeader htup; BlockNumber blkno; RelFileLocator target_locator; @@ -373,10 +322,10 @@ heap_xlog_delete(XLogReaderState *record) { page = BufferGetPage(buffer); - if (PageGetMaxOffsetNumber(page) >= xlrec->offnum) - lp = PageGetItemId(page, xlrec->offnum); - - if (PageGetMaxOffsetNumber(page) < xlrec->offnum || !ItemIdIsNormal(lp)) + if (xlrec->offnum < 1 || xlrec->offnum > PageGetMaxOffsetNumber(page)) + elog(PANIC, "offnum out of range"); + lp = PageGetItemId(page, xlrec->offnum); + if (!ItemIdIsNormal(lp)) elog(PANIC, "invalid lp"); htup = (HeapTupleHeader) PageGetItem(page, lp); @@ -438,6 +387,9 @@ heap_xlog_insert(XLogReaderState *record) ItemPointerSetBlockNumber(&target_tid, blkno); ItemPointerSetOffsetNumber(&target_tid, xlrec->offnum); + /* No freezing in the heap_insert() code path */ + Assert(!(xlrec->flags & XLH_INSERT_ALL_FROZEN_SET)); + /* * The visibility map may need to be fixed even if the heap page is * already up-to-date. @@ -497,21 +449,24 @@ heap_xlog_insert(XLogReaderState *record) HeapTupleHeaderSetCmin(htup, FirstCommandId); htup->t_ctid = target_tid; - if (PageAddItem(page, (Item) htup, newlen, xlrec->offnum, - true, true) == InvalidOffsetNumber) + if (PageAddItem(page, htup, newlen, xlrec->offnum, true, true) == InvalidOffsetNumber) elog(PANIC, "failed to add tuple"); freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */ + /* + * Set the page prunable to trigger on-access pruning later, which may + * set the page all-visible in the VM. See comments in heap_insert(). + */ + if (TransactionIdIsNormal(XLogRecGetXid(record)) && + !HeapTupleHeaderXminFrozen(htup)) + PageSetPrunable(page, XLogRecGetXid(record)); + PageSetLSN(page, lsn); if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) PageClearAllVisible(page); - /* XLH_INSERT_ALL_FROZEN_SET implies that all tuples are visible */ - if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET) - PageSetAllVisible(page); - MarkBufferDirty(buffer); } if (BufferIsValid(buffer)) @@ -553,6 +508,7 @@ heap_xlog_multi_insert(XLogReaderState *record) int i; bool isinit = (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) != 0; XLogRedoAction action; + Buffer vmbuffer = InvalidBuffer; /* * Insertion doesn't overwrite MVCC data, so no conflict processing is @@ -573,11 +529,11 @@ heap_xlog_multi_insert(XLogReaderState *record) if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) { Relation reln = CreateFakeRelcacheEntry(rlocator); - Buffer vmbuffer = InvalidBuffer; visibilitymap_pin(reln, blkno, &vmbuffer); visibilitymap_clear(reln, blkno, vmbuffer, VISIBILITYMAP_VALID_BITS); ReleaseBuffer(vmbuffer); + vmbuffer = InvalidBuffer; FreeFakeRelcacheEntry(reln); } @@ -600,7 +556,7 @@ heap_xlog_multi_insert(XLogReaderState *record) tupdata = XLogRecGetBlockData(record, 0, &len); endptr = tupdata + len; - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); for (i = 0; i < xlrec->ntuples; i++) { @@ -641,7 +597,7 @@ heap_xlog_multi_insert(XLogReaderState *record) ItemPointerSetBlockNumber(&htup->t_ctid, blkno); ItemPointerSetOffsetNumber(&htup->t_ctid, offnum); - offnum = PageAddItem(page, (Item) htup, newlen, offnum, true, true); + offnum = PageAddItem(page, htup, newlen, offnum, true, true); if (offnum == InvalidOffsetNumber) elog(PANIC, "failed to add tuple"); } @@ -655,15 +611,72 @@ heap_xlog_multi_insert(XLogReaderState *record) if (xlrec->flags & XLH_INSERT_ALL_VISIBLE_CLEARED) PageClearAllVisible(page); - /* XLH_INSERT_ALL_FROZEN_SET implies that all tuples are visible */ + /* + * XLH_INSERT_ALL_FROZEN_SET implies that all tuples are visible. If + * we are not setting the page frozen, then set the page's prunable + * hint so that we trigger on-access pruning later which may set the + * page all-visible in the VM. + */ if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET) + { PageSetAllVisible(page); + PageClearPrunable(page); + } + else + PageSetPrunable(page, XLogRecGetXid(record)); MarkBufferDirty(buffer); } if (BufferIsValid(buffer)) UnlockReleaseBuffer(buffer); + buffer = InvalidBuffer; + + /* + * Read and update the visibility map (VM) block. + * + * We must always redo VM changes, even if the corresponding heap page + * update was skipped due to the LSN interlock. Each VM block covers + * multiple heap pages, so later WAL records may update other bits in the + * same block. If this record includes an FPI (full-page image), + * subsequent WAL records may depend on it to guard against torn pages. + * + * Heap page changes are replayed first to preserve the invariant: + * PD_ALL_VISIBLE must be set on the heap page if the VM bit is set. + * + * Note that we released the heap page lock above. During normal + * operation, this would be unsafe — a concurrent modification could + * clear PD_ALL_VISIBLE while the VM bit remained set, violating the + * invariant. + * + * During recovery, however, no concurrent writers exist. Therefore, + * updating the VM without holding the heap page lock is safe enough. This + * same approach is taken when replaying XLOG_HEAP2_PRUNE* records (see + * heap_xlog_prune_freeze()). + */ + if ((xlrec->flags & XLH_INSERT_ALL_FROZEN_SET) && + XLogReadBufferForRedoExtended(record, 1, RBM_ZERO_ON_ERROR, false, + &vmbuffer) == BLK_NEEDS_REDO) + { + Page vmpage = BufferGetPage(vmbuffer); + + /* initialize the page if it was read as zeros */ + if (PageIsNew(vmpage)) + PageInit(vmpage, BLCKSZ, 0); + + visibilitymap_set(blkno, + vmbuffer, + VISIBILITYMAP_ALL_VISIBLE | + VISIBILITYMAP_ALL_FROZEN, + rlocator); + + Assert(BufferIsDirty(vmbuffer)); + PageSetLSN(vmpage, lsn); + } + + if (BufferIsValid(vmbuffer)) + UnlockReleaseBuffer(vmbuffer); + /* * If the page is running low on free space, update the FSM as well. * Arbitrarily, our definition of "low" is less than 20%. We can't do much @@ -691,9 +704,10 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) ItemPointerData newtid; Buffer obuffer, nbuffer; - Page page; + Page opage, + npage; OffsetNumber offnum; - ItemId lp = NULL; + ItemId lp; HeapTupleData oldtup; HeapTupleHeader htup; uint16 prefixlen = 0, @@ -755,15 +769,15 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) &obuffer); if (oldaction == BLK_NEEDS_REDO) { - page = BufferGetPage(obuffer); + opage = BufferGetPage(obuffer); offnum = xlrec->old_offnum; - if (PageGetMaxOffsetNumber(page) >= offnum) - lp = PageGetItemId(page, offnum); - - if (PageGetMaxOffsetNumber(page) < offnum || !ItemIdIsNormal(lp)) + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(opage)) + elog(PANIC, "offnum out of range"); + lp = PageGetItemId(opage, offnum); + if (!ItemIdIsNormal(lp)) elog(PANIC, "invalid lp"); - htup = (HeapTupleHeader) PageGetItem(page, lp); + htup = (HeapTupleHeader) PageGetItem(opage, lp); oldtup.t_data = htup; oldtup.t_len = ItemIdGetLength(lp); @@ -782,12 +796,12 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) htup->t_ctid = newtid; /* Mark the page as a candidate for pruning */ - PageSetPrunable(page, XLogRecGetXid(record)); + PageSetPrunable(opage, XLogRecGetXid(record)); if (xlrec->flags & XLH_UPDATE_OLD_ALL_VISIBLE_CLEARED) - PageClearAllVisible(page); + PageClearAllVisible(opage); - PageSetLSN(page, lsn); + PageSetLSN(opage, lsn); MarkBufferDirty(obuffer); } @@ -802,8 +816,8 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) else if (XLogRecGetInfo(record) & XLOG_HEAP_INIT_PAGE) { nbuffer = XLogInitBufferForRedo(record, 0); - page = (Page) BufferGetPage(nbuffer); - PageInit(page, BufferGetPageSize(nbuffer), 0); + npage = BufferGetPage(nbuffer); + PageInit(npage, BufferGetPageSize(nbuffer), 0); newaction = BLK_NEEDS_REDO; } else @@ -835,10 +849,10 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) recdata = XLogRecGetBlockData(record, 0, &datalen); recdata_end = recdata + datalen; - page = BufferGetPage(nbuffer); + npage = BufferGetPage(nbuffer); offnum = xlrec->new_offnum; - if (PageGetMaxOffsetNumber(page) + 1 < offnum) + if (PageGetMaxOffsetNumber(npage) + 1 < offnum) elog(PANIC, "invalid max offset number"); if (xlrec->flags & XLH_UPDATE_PREFIX_FROM_OLD) @@ -915,16 +929,19 @@ heap_xlog_update(XLogReaderState *record, bool hot_update) /* Make sure there is no forward chain link in t_ctid */ htup->t_ctid = newtid; - offnum = PageAddItem(page, (Item) htup, newlen, offnum, true, true); + offnum = PageAddItem(npage, htup, newlen, offnum, true, true); if (offnum == InvalidOffsetNumber) elog(PANIC, "failed to add tuple"); if (xlrec->flags & XLH_UPDATE_NEW_ALL_VISIBLE_CLEARED) - PageClearAllVisible(page); + PageClearAllVisible(npage); - freespace = PageGetHeapFreeSpace(page); /* needed to update FSM below */ + /* needed to update FSM below */ + freespace = PageGetHeapFreeSpace(npage); - PageSetLSN(page, lsn); + PageSetLSN(npage, lsn); + /* See heap_insert() for why we set pd_prune_xid on insert */ + PageSetPrunable(npage, XLogRecGetXid(record)); MarkBufferDirty(nbuffer); } @@ -963,7 +980,7 @@ heap_xlog_confirm(XLogReaderState *record) Buffer buffer; Page page; OffsetNumber offnum; - ItemId lp = NULL; + ItemId lp; HeapTupleHeader htup; if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) @@ -971,10 +988,10 @@ heap_xlog_confirm(XLogReaderState *record) page = BufferGetPage(buffer); offnum = xlrec->offnum; - if (PageGetMaxOffsetNumber(page) >= offnum) - lp = PageGetItemId(page, offnum); - - if (PageGetMaxOffsetNumber(page) < offnum || !ItemIdIsNormal(lp)) + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(page)) + elog(PANIC, "offnum out of range"); + lp = PageGetItemId(page, offnum); + if (!ItemIdIsNormal(lp)) elog(PANIC, "invalid lp"); htup = (HeapTupleHeader) PageGetItem(page, lp); @@ -1002,7 +1019,7 @@ heap_xlog_lock(XLogReaderState *record) Buffer buffer; Page page; OffsetNumber offnum; - ItemId lp = NULL; + ItemId lp; HeapTupleHeader htup; /* @@ -1028,13 +1045,13 @@ heap_xlog_lock(XLogReaderState *record) if (XLogReadBufferForRedo(record, 0, &buffer) == BLK_NEEDS_REDO) { - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); offnum = xlrec->offnum; - if (PageGetMaxOffsetNumber(page) >= offnum) - lp = PageGetItemId(page, offnum); - - if (PageGetMaxOffsetNumber(page) < offnum || !ItemIdIsNormal(lp)) + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(page)) + elog(PANIC, "offnum out of range"); + lp = PageGetItemId(page, offnum); + if (!ItemIdIsNormal(lp)) elog(PANIC, "invalid lp"); htup = (HeapTupleHeader) PageGetItem(page, lp); @@ -1076,7 +1093,7 @@ heap_xlog_lock_updated(XLogReaderState *record) Buffer buffer; Page page; OffsetNumber offnum; - ItemId lp = NULL; + ItemId lp; HeapTupleHeader htup; xlrec = (xl_heap_lock_updated *) XLogRecGetData(record); @@ -1107,10 +1124,10 @@ heap_xlog_lock_updated(XLogReaderState *record) page = BufferGetPage(buffer); offnum = xlrec->offnum; - if (PageGetMaxOffsetNumber(page) >= offnum) - lp = PageGetItemId(page, offnum); - - if (PageGetMaxOffsetNumber(page) < offnum || !ItemIdIsNormal(lp)) + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(page)) + elog(PANIC, "offnum out of range"); + lp = PageGetItemId(page, offnum); + if (!ItemIdIsNormal(lp)) elog(PANIC, "invalid lp"); htup = (HeapTupleHeader) PageGetItem(page, lp); @@ -1139,7 +1156,7 @@ heap_xlog_inplace(XLogReaderState *record) Buffer buffer; Page page; OffsetNumber offnum; - ItemId lp = NULL; + ItemId lp; HeapTupleHeader htup; uint32 oldlen; Size newlen; @@ -1151,10 +1168,10 @@ heap_xlog_inplace(XLogReaderState *record) page = BufferGetPage(buffer); offnum = xlrec->offnum; - if (PageGetMaxOffsetNumber(page) >= offnum) - lp = PageGetItemId(page, offnum); - - if (PageGetMaxOffsetNumber(page) < offnum || !ItemIdIsNormal(lp)) + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(page)) + elog(PANIC, "offnum out of range"); + lp = PageGetItemId(page, offnum); + if (!ItemIdIsNormal(lp)) elog(PANIC, "invalid lp"); htup = (HeapTupleHeader) PageGetItem(page, lp); @@ -1236,9 +1253,6 @@ heap2_redo(XLogReaderState *record) case XLOG_HEAP2_PRUNE_VACUUM_CLEANUP: heap_xlog_prune_freeze(record); break; - case XLOG_HEAP2_VISIBLE: - heap_xlog_visible(record); - break; case XLOG_HEAP2_MULTI_INSERT: heap_xlog_multi_insert(record); break; diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c index cb1e57030f64c..03f885a25b075 100644 --- a/src/backend/access/heap/heaptoast.c +++ b/src/backend/access/heap/heaptoast.c @@ -4,7 +4,7 @@ * Heap-specific definitions for external and compressed storage * of variable size attributes. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -94,7 +94,7 @@ heap_toast_delete(Relation rel, HeapTuple oldtup, bool is_speculative) */ HeapTuple heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup, - int options) + uint32 options) { HeapTuple result_tuple; TupleDesc tupleDesc; @@ -371,9 +371,9 @@ toast_flatten_tuple(HeapTuple tup, TupleDesc tupleDesc) */ if (!toast_isnull[i] && TupleDescCompactAttr(tupleDesc, i)->attlen == -1) { - struct varlena *new_value; + varlena *new_value; - new_value = (struct varlena *) DatumGetPointer(toast_values[i]); + new_value = (varlena *) DatumGetPointer(toast_values[i]); if (VARATT_IS_EXTERNAL(new_value)) { new_value = detoast_external_attr(new_value); @@ -485,9 +485,9 @@ toast_flatten_tuple_to_datum(HeapTupleHeader tup, has_nulls = true; else if (TupleDescCompactAttr(tupleDesc, i)->attlen == -1) { - struct varlena *new_value; + varlena *new_value; - new_value = (struct varlena *) DatumGetPointer(toast_values[i]); + new_value = (varlena *) DatumGetPointer(toast_values[i]); if (VARATT_IS_EXTERNAL(new_value) || VARATT_IS_COMPRESSED(new_value)) { @@ -561,15 +561,15 @@ toast_flatten_tuple_to_datum(HeapTupleHeader tup, */ HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc, - Datum *values, - bool *isnull) + const Datum *values, + const bool *isnull) { HeapTuple new_tuple; int numAttrs = tupleDesc->natts; int num_to_free; int i; Datum new_values[MaxTupleAttributeNumber]; - Pointer freeable_values[MaxTupleAttributeNumber]; + void *freeable_values[MaxTupleAttributeNumber]; /* * We can pass the caller's isnull array directly to heap_form_tuple, but @@ -586,14 +586,14 @@ toast_build_flattened_tuple(TupleDesc tupleDesc, */ if (!isnull[i] && TupleDescCompactAttr(tupleDesc, i)->attlen == -1) { - struct varlena *new_value; + varlena *new_value; - new_value = (struct varlena *) DatumGetPointer(new_values[i]); + new_value = (varlena *) DatumGetPointer(new_values[i]); if (VARATT_IS_EXTERNAL(new_value)) { new_value = detoast_external_attr(new_value); new_values[i] = PointerGetDatum(new_value); - freeable_values[num_to_free++] = (Pointer) new_value; + freeable_values[num_to_free++] = new_value; } } } @@ -625,7 +625,7 @@ toast_build_flattened_tuple(TupleDesc tupleDesc, void heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize, int32 sliceoffset, int32 slicelength, - struct varlena *result) + varlena *result) { Relation *toastidxs; ScanKeyData toastkey[3]; @@ -768,7 +768,7 @@ heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize, chcpyend = (sliceoffset + slicelength - 1) % TOAST_MAX_CHUNK_SIZE; memcpy(VARDATA(result) + - (curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset) + chcpystrt, + curchunk * TOAST_MAX_CHUNK_SIZE - sliceoffset + chcpystrt, chunkdata + chcpystrt, (chcpyend - chcpystrt) + 1); diff --git a/src/backend/access/heap/hio.c b/src/backend/access/heap/hio.c index c482c9d61b265..e96e0f77d9264 100644 --- a/src/backend/access/heap/hio.c +++ b/src/backend/access/heap/hio.c @@ -3,7 +3,7 @@ * hio.c * POSTGRES heap access method input/output code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -58,9 +58,7 @@ RelationPutHeapTuple(Relation relation, /* Add the tuple to the page */ pageHeader = BufferGetPage(buffer); - offnum = PageAddItem(pageHeader, (Item) tuple->t_data, - tuple->t_len, InvalidOffsetNumber, false, true); - + offnum = PageAddItem(pageHeader, tuple->t_data, tuple->t_len, InvalidOffsetNumber, false, true); if (offnum == InvalidOffsetNumber) elog(PANIC, "failed to add tuple to page"); @@ -500,7 +498,7 @@ RelationAddBlocks(Relation relation, BulkInsertState bistate, */ Buffer RelationGetBufferForTuple(Relation relation, Size len, - Buffer otherBuffer, int options, + Buffer otherBuffer, uint32 options, BulkInsertState bistate, Buffer *vmbuffer, Buffer *vmbuffer_other, int num_pages) @@ -713,14 +711,15 @@ RelationGetBufferForTuple(Relation relation, Size len, * unlock the two buffers in, so this can be slightly simpler than the * code above. */ - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); if (otherBuffer == InvalidBuffer) - ReleaseBuffer(buffer); + UnlockReleaseBuffer(buffer); else if (otherBlock != targetBlock) { + UnlockReleaseBuffer(buffer); LockBuffer(otherBuffer, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buffer); } + else + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* Is there an ongoing bulk extension? */ if (bistate && bistate->next_free != InvalidBlockNumber) diff --git a/src/backend/access/heap/meson.build b/src/backend/access/heap/meson.build index 2637b24112fba..00ec07d7f30d1 100644 --- a/src/backend/access/heap/meson.build +++ b/src/backend/access/heap/meson.build @@ -1,8 +1,9 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'heapam.c', 'heapam_handler.c', + 'heapam_indexscan.c', 'heapam_visibility.c', 'heapam_xlog.c', 'heaptoast.c', diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index a8025889be088..fdddd23035b54 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -3,7 +3,7 @@ * pruneheap.c * heap page pruning and HOT-chain management code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,6 +19,7 @@ #include "access/htup_details.h" #include "access/multixact.h" #include "access/transam.h" +#include "access/visibilitymap.h" #include "access/xlog.h" #include "access/xloginsert.h" #include "commands/vacuum.h" @@ -42,8 +43,20 @@ typedef struct /* whether or not dead items can be set LP_UNUSED during pruning */ bool mark_unused_now; /* whether to attempt freezing tuples */ - bool freeze; + bool attempt_freeze; + /* whether to attempt setting the VM */ + bool attempt_set_vm; struct VacuumCutoffs *cutoffs; + Relation relation; + + /* + * Keep the buffer, block, and page handy so that helpers needing to + * access them don't need to make repeated calls to BufferGetBlockNumber() + * and BufferGetPage(). + */ + BlockNumber block; + Buffer buffer; + Page page; /*------------------------------------------------------- * Fields describing what to do to the page @@ -61,6 +74,22 @@ typedef struct OffsetNumber nowunused[MaxHeapTuplesPerPage]; HeapTupleFreeze frozen[MaxHeapTuplesPerPage]; + /* + * set_all_visible and set_all_frozen indicate if the all-visible and + * all-frozen bits in the visibility map can be set for this page after + * pruning. They are only tracked when the caller requests VM updates + * (attempt_set_vm); otherwise they remain false throughout. + * + * NOTE: set_all_visible and set_all_frozen initially don't include + * LP_DEAD items. That's convenient for heap_page_prune_and_freeze() to + * use them to decide whether to opportunistically freeze the page or not. + * The set_all_visible and set_all_frozen values ultimately used to set + * the VM are adjusted to include LP_DEAD items after we determine whether + * or not to opportunistically freeze. + */ + bool set_all_visible; + bool set_all_frozen; + /*------------------------------------------------------- * Working state for HOT chain processing *------------------------------------------------------- @@ -97,11 +126,34 @@ typedef struct */ int8 htsv[MaxHeapTuplesPerPage + 1]; - /* - * Freezing-related state. + /*------------------------------------------------------- + * Working state for freezing + *------------------------------------------------------- */ HeapPageFreeze pagefrz; + /*------------------------------------------------------- + * Working state for visibility map processing + *------------------------------------------------------- + */ + + /* + * Caller must provide a pinned vmbuffer corresponding to the heap block + * passed to heap_page_prune_and_freeze(). We will fix any corruption + * found in the VM and set the VM if the page is all-visible/all-frozen. + */ + Buffer vmbuffer; + + /* + * The state of the VM bits at the beginning of pruning and the state they + * will be in at the end. + */ + uint8 old_vmbits; + uint8 new_vmbits; + + /* The newest xmin of live tuples on the page */ + TransactionId newest_live_xid; + /*------------------------------------------------------- * Information about what was done * @@ -127,39 +179,44 @@ typedef struct */ int lpdead_items; /* number of items in the array */ OffsetNumber *deadoffsets; /* points directly to presult->deadoffsets */ - - /* - * all_visible and all_frozen indicate if the all-visible and all-frozen - * bits in the visibility map can be set for this page after pruning. - * - * visibility_cutoff_xid is the newest xmin of live tuples on the page. - * The caller can use it as the conflict horizon, when setting the VM - * bits. It is only valid if we froze some tuples, and all_frozen is - * true. - * - * NOTE: all_visible and all_frozen don't include LP_DEAD items. That's - * convenient for heap_page_prune_and_freeze(), to use them to decide - * whether to freeze the page or not. The all_visible and all_frozen - * values returned to the caller are adjusted to include LP_DEAD items at - * the end. - * - * all_frozen should only be considered valid if all_visible is also set; - * we don't bother to clear the all_frozen flag every time we clear the - * all_visible flag. - */ - bool all_visible; - bool all_frozen; - TransactionId visibility_cutoff_xid; } PruneState; +/* + * Type of visibility map corruption detected on a heap page and its + * associated VM page. Passed to heap_page_fix_vm_corruption() so the caller + * can specify what it found rather than having the function rederive the + * corruption from page state. + */ +typedef enum VMCorruptionType +{ + /* VM bits are set but the heap page-level PD_ALL_VISIBLE flag is not */ + VM_CORRUPT_MISSING_PAGE_HINT, + /* LP_DEAD line pointers found on a page marked all-visible */ + VM_CORRUPT_LPDEAD, + /* Tuple not visible to all transactions on a page marked all-visible */ + VM_CORRUPT_TUPLE_VISIBILITY, +} VMCorruptionType; + /* Local functions */ +static void prune_freeze_setup(PruneFreezeParams *params, + TransactionId *new_relfrozen_xid, + MultiXactId *new_relmin_mxid, + PruneFreezeResult *presult, + PruneState *prstate); +static void heap_page_fix_vm_corruption(PruneState *prstate, + OffsetNumber offnum, + VMCorruptionType corruption_type); +static void prune_freeze_fast_path(PruneState *prstate, + PruneFreezeResult *presult); +static void prune_freeze_plan(PruneState *prstate, + OffsetNumber *off_loc); static HTSV_Result heap_prune_satisfies_vacuum(PruneState *prstate, - HeapTuple tup, - Buffer buffer); + HeapTuple tup); static inline HTSV_Result htsv_get_valid_status(int status); -static void heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff, +static void heap_prune_chain(OffsetNumber maxoff, OffsetNumber rootoffnum, PruneState *prstate); -static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid); +static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid, + OffsetNumber offnum); static void heap_prune_record_redirect(PruneState *prstate, OffsetNumber offnum, OffsetNumber rdoffnum, bool was_normal); @@ -169,13 +226,18 @@ static void heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber o bool was_normal); static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum, bool was_normal); -static void heap_prune_record_unchanged_lp_unused(Page page, PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumber offnum); -static void heap_prune_record_unchanged_lp_dead(Page page, PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_unchanged_lp_unused(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_unchanged_lp_normal(PruneState *prstate, OffsetNumber offnum); +static void heap_prune_record_unchanged_lp_dead(PruneState *prstate, OffsetNumber offnum); static void heap_prune_record_unchanged_lp_redirect(PruneState *prstate, OffsetNumber offnum); static void page_verify_redirects(Page page); +static bool heap_page_will_freeze(bool did_tuple_hint_fpi, bool do_prune, bool do_hint_prune, + PruneState *prstate); +static bool heap_page_will_set_vm(PruneState *prstate, PruneReason reason, + bool do_prune, bool do_freeze); + /* * Optionally prune and repair fragmentation in the specified page. @@ -188,9 +250,26 @@ static void page_verify_redirects(Page page); * if there's not any use in pruning. * * Caller must have pin on the buffer, and must *not* have a lock on it. + * + * This function may pin *vmbuffer. It's passed by reference so the caller can + * reuse the pin across calls, avoiding repeated pin/unpin cycles. If we find + * VM corruption during pruning, we will fix it. Caller is responsible for + * unpinning *vmbuffer. + * + * rel_read_only is true if we determined at plan time that the query does not + * modify the relation. It is counterproductive to set the VM if the query + * will immediately clear it. + * + * As noted in ScanRelIsReadOnly(), INSERT ... SELECT from the same table will + * report the scan relation as read-only. This is usually harmless in + * practice. It is useful to set scanned pages all-visible that won't be + * inserted into. Pages it does insert to will rarely meet the criteria for + * pruning, and those that do are likely to contain in-progress inserts which + * make the page not fully all-visible. */ void -heap_page_prune_opt(Relation relation, Buffer buffer) +heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer, + bool rel_read_only) { Page page = BufferGetPage(buffer); TransactionId prune_xid; @@ -208,9 +287,10 @@ heap_page_prune_opt(Relation relation, Buffer buffer) /* * First check whether there's any chance there's something to prune, * determining the appropriate horizon is a waste if there's no prune_xid - * (i.e. no updates/deletes left potentially dead tuples around). + * (i.e. no updates/deletes left potentially dead tuples around and no + * inserts inserted new tuples that may be visible to all). */ - prune_xid = ((PageHeader) page)->pd_prune_xid; + prune_xid = PageGetPruneXid(page); if (!TransactionIdIsValid(prune_xid)) return; @@ -220,7 +300,7 @@ heap_page_prune_opt(Relation relation, Buffer buffer) */ vistest = GlobalVisTestFor(relation); - if (!GlobalVisTestIsRemovableXid(vistest, prune_xid)) + if (!GlobalVisTestIsRemovableXid(vistest, prune_xid, true)) return; /* @@ -254,14 +334,30 @@ heap_page_prune_opt(Relation relation, Buffer buffer) { OffsetNumber dummy_off_loc; PruneFreezeResult presult; + PruneFreezeParams params; + + visibilitymap_pin(relation, BufferGetBlockNumber(buffer), + vmbuffer); + + params.relation = relation; + params.buffer = buffer; + params.vmbuffer = *vmbuffer; + params.reason = PRUNE_ON_ACCESS; + params.vistest = vistest; + params.cutoffs = NULL; /* - * For now, pass mark_unused_now as false regardless of whether or - * not the relation has indexes, since we cannot safely determine - * that during on-access pruning with the current implementation. + * We don't pass the HEAP_PAGE_PRUNE_MARK_UNUSED_NOW option + * regardless of whether or not the relation has indexes, since we + * cannot safely determine that during on-access pruning with the + * current implementation. */ - heap_page_prune_and_freeze(relation, buffer, vistest, 0, - NULL, &presult, PRUNE_ON_ACCESS, &dummy_off_loc, NULL, NULL); + params.options = HEAP_PAGE_PRUNE_ALLOW_FAST_PATH; + if (rel_read_only) + params.options |= HEAP_PAGE_PRUNE_SET_VM; + + heap_page_prune_and_freeze(¶ms, &presult, &dummy_off_loc, + NULL, NULL); /* * Report the number of tuples reclaimed to pgstats. This is @@ -293,87 +389,41 @@ heap_page_prune_opt(Relation relation, Buffer buffer) } } - /* - * Prune and repair fragmentation and potentially freeze tuples on the - * specified page. - * - * Caller must have pin and buffer cleanup lock on the page. Note that we - * don't update the FSM information for page on caller's behalf. Caller might - * also need to account for a reduction in the length of the line pointer - * array following array truncation by us. - * - * If the HEAP_PRUNE_FREEZE option is set, we will also freeze tuples if it's - * required in order to advance relfrozenxid / relminmxid, or if it's - * considered advantageous for overall system performance to do so now. The - * 'cutoffs', 'presult', 'new_relfrozen_xid' and 'new_relmin_mxid' arguments - * are required when freezing. When HEAP_PRUNE_FREEZE option is set, we also - * set presult->all_visible and presult->all_frozen on exit, to indicate if - * the VM bits can be set. They are always set to false when the - * HEAP_PRUNE_FREEZE option is not set, because at the moment only callers - * that also freeze need that information. - * - * vistest is used to distinguish whether tuples are DEAD or RECENTLY_DEAD - * (see heap_prune_satisfies_vacuum). - * - * options: - * MARK_UNUSED_NOW indicates that dead items can be set LP_UNUSED during - * pruning. - * - * FREEZE indicates that we will also freeze tuples, and will return - * 'all_visible', 'all_frozen' flags to the caller. - * - * cutoffs contains the freeze cutoffs, established by VACUUM at the beginning - * of vacuuming the relation. Required if HEAP_PRUNE_FREEZE option is set. - * cutoffs->OldestXmin is also used to determine if dead tuples are - * HEAPTUPLE_RECENTLY_DEAD or HEAPTUPLE_DEAD. + * Helper for heap_page_prune_and_freeze() to initialize the PruneState using + * the provided parameters. * - * presult contains output parameters needed by callers, such as the number of - * tuples removed and the offsets of dead items on the page after pruning. - * heap_page_prune_and_freeze() is responsible for initializing it. Required - * by all callers. - * - * reason indicates why the pruning is performed. It is included in the WAL - * record for debugging and analysis purposes, but otherwise has no effect. - * - * off_loc is the offset location required by the caller to use in error - * callback. - * - * new_relfrozen_xid and new_relmin_mxid must provided by the caller if the - * HEAP_PRUNE_FREEZE option is set. On entry, they contain the oldest XID and - * multi-XID seen on the relation so far. They will be updated with oldest - * values present on the page after pruning. After processing the whole - * relation, VACUUM can use these values as the new relfrozenxid/relminmxid - * for the relation. + * params, new_relfrozen_xid, new_relmin_mxid, and presult are input + * parameters and are not modified by this function. Only prstate is modified. */ -void -heap_page_prune_and_freeze(Relation relation, Buffer buffer, - GlobalVisState *vistest, - int options, - struct VacuumCutoffs *cutoffs, - PruneFreezeResult *presult, - PruneReason reason, - OffsetNumber *off_loc, - TransactionId *new_relfrozen_xid, - MultiXactId *new_relmin_mxid) +static void +prune_freeze_setup(PruneFreezeParams *params, + TransactionId *new_relfrozen_xid, + MultiXactId *new_relmin_mxid, + PruneFreezeResult *presult, + PruneState *prstate) { - Page page = BufferGetPage(buffer); - BlockNumber blockno = BufferGetBlockNumber(buffer); - OffsetNumber offnum, - maxoff; - PruneState prstate; - HeapTupleData tup; - bool do_freeze; - bool do_prune; - bool do_hint; - bool hint_bit_fpi; - int64 fpi_before = pgWalUsage.wal_fpi; - /* Copy parameters to prstate */ - prstate.vistest = vistest; - prstate.mark_unused_now = (options & HEAP_PAGE_PRUNE_MARK_UNUSED_NOW) != 0; - prstate.freeze = (options & HEAP_PAGE_PRUNE_FREEZE) != 0; - prstate.cutoffs = cutoffs; + prstate->vistest = params->vistest; + prstate->mark_unused_now = + (params->options & HEAP_PAGE_PRUNE_MARK_UNUSED_NOW) != 0; + + /* cutoffs must be provided if we will attempt freezing */ + Assert(!(params->options & HEAP_PAGE_PRUNE_FREEZE) || params->cutoffs); + prstate->attempt_freeze = (params->options & HEAP_PAGE_PRUNE_FREEZE) != 0; + prstate->attempt_set_vm = (params->options & HEAP_PAGE_PRUNE_SET_VM) != 0; + prstate->cutoffs = params->cutoffs; + prstate->relation = params->relation; + prstate->block = BufferGetBlockNumber(params->buffer); + prstate->buffer = params->buffer; + prstate->page = BufferGetPage(params->buffer); + + Assert(BufferIsValid(params->vmbuffer)); + prstate->vmbuffer = params->vmbuffer; + prstate->new_vmbits = 0; + prstate->old_vmbits = visibilitymap_get_status(prstate->relation, + prstate->block, + &prstate->vmbuffer); /* * Our strategy is to scan the page and make lists of items to change, @@ -386,88 +436,107 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * prunable, we will save the lowest relevant XID in new_prune_xid. Also * initialize the rest of our working state. */ - prstate.new_prune_xid = InvalidTransactionId; - prstate.latest_xid_removed = InvalidTransactionId; - prstate.nredirected = prstate.ndead = prstate.nunused = prstate.nfrozen = 0; - prstate.nroot_items = 0; - prstate.nheaponly_items = 0; + prstate->new_prune_xid = InvalidTransactionId; + prstate->latest_xid_removed = InvalidTransactionId; + prstate->nredirected = prstate->ndead = prstate->nunused = 0; + prstate->nfrozen = 0; + prstate->nroot_items = 0; + prstate->nheaponly_items = 0; /* initialize page freezing working state */ - prstate.pagefrz.freeze_required = false; - if (prstate.freeze) + prstate->pagefrz.freeze_required = false; + prstate->pagefrz.FreezePageConflictXid = InvalidTransactionId; + if (prstate->attempt_freeze) { Assert(new_relfrozen_xid && new_relmin_mxid); - prstate.pagefrz.FreezePageRelfrozenXid = *new_relfrozen_xid; - prstate.pagefrz.NoFreezePageRelfrozenXid = *new_relfrozen_xid; - prstate.pagefrz.FreezePageRelminMxid = *new_relmin_mxid; - prstate.pagefrz.NoFreezePageRelminMxid = *new_relmin_mxid; + prstate->pagefrz.FreezePageRelfrozenXid = *new_relfrozen_xid; + prstate->pagefrz.NoFreezePageRelfrozenXid = *new_relfrozen_xid; + prstate->pagefrz.FreezePageRelminMxid = *new_relmin_mxid; + prstate->pagefrz.NoFreezePageRelminMxid = *new_relmin_mxid; } else { - Assert(new_relfrozen_xid == NULL && new_relmin_mxid == NULL); - prstate.pagefrz.FreezePageRelminMxid = InvalidMultiXactId; - prstate.pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId; - prstate.pagefrz.FreezePageRelfrozenXid = InvalidTransactionId; - prstate.pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId; + Assert(!new_relfrozen_xid && !new_relmin_mxid); + prstate->pagefrz.FreezePageRelminMxid = InvalidMultiXactId; + prstate->pagefrz.NoFreezePageRelminMxid = InvalidMultiXactId; + prstate->pagefrz.FreezePageRelfrozenXid = InvalidTransactionId; + prstate->pagefrz.NoFreezePageRelfrozenXid = InvalidTransactionId; } - prstate.ndeleted = 0; - prstate.live_tuples = 0; - prstate.recently_dead_tuples = 0; - prstate.hastup = false; - prstate.lpdead_items = 0; - prstate.deadoffsets = presult->deadoffsets; + prstate->ndeleted = 0; + prstate->live_tuples = 0; + prstate->recently_dead_tuples = 0; + prstate->hastup = false; + prstate->lpdead_items = 0; /* - * Caller may update the VM after we're done. We can keep track of - * whether the page will be all-visible and all-frozen after pruning and - * freezing to help the caller to do that. - * - * Currently, only VACUUM sets the VM bits. To save the effort, only do - * the bookkeeping if the caller needs it. Currently, that's tied to - * HEAP_PAGE_PRUNE_FREEZE, but it could be a separate flag if you wanted - * to update the VM bits without also freezing or freeze without also - * setting the VM bits. + * deadoffsets are filled in during pruning but are only used to populate + * PruneFreezeResult->deadoffsets. To avoid needing two copies of the + * array, just save a pointer to the result offsets array in the + * PruneState. + */ + prstate->deadoffsets = presult->deadoffsets; + + /* + * We track whether the page will be all-visible/all-frozen at the end of + * pruning and freezing. While examining tuple visibility, we'll set + * set_all_visible to false if there are tuples on the page not visible to + * all running and future transactions. If setting the VM is enabled for + * this scan, we will do so if the page ends up being all-visible. * - * In addition to telling the caller whether it can set the VM bit, we - * also use 'all_visible' and 'all_frozen' for our own decision-making. If - * the whole page would become frozen, we consider opportunistically - * freezing tuples. We will not be able to freeze the whole page if there - * are tuples present that are not visible to everyone or if there are - * dead tuples which are not yet removable. However, dead tuples which - * will be removed by the end of vacuuming should not preclude us from - * opportunistically freezing. Because of that, we do not clear - * all_visible when we see LP_DEAD items. We fix that at the end of the - * function, when we return the value to the caller, so that the caller - * doesn't set the VM bit incorrectly. + * We also keep track of the newest live XID, which is used to calculate + * the snapshot conflict horizon for a WAL record setting the VM. */ - if (prstate.freeze) - { - prstate.all_visible = true; - prstate.all_frozen = true; - } - else - { - /* - * Initializing to false allows skipping the work to update them in - * heap_prune_record_unchanged_lp_normal(). - */ - prstate.all_visible = false; - prstate.all_frozen = false; - } + prstate->set_all_visible = prstate->attempt_set_vm; + prstate->newest_live_xid = InvalidTransactionId; /* - * The visibility cutoff xid is the newest xmin of live tuples on the - * page. In the common case, this will be set as the conflict horizon the - * caller can use for updating the VM. If, at the end of freezing and - * pruning, the page is all-frozen, there is no possibility that any - * running transaction on the standby does not see tuples on the page as - * all-visible, so the conflict horizon remains InvalidTransactionId. + * Currently, only VACUUM performs freezing, but other callers may in the + * future. We must initialize set_all_frozen based on whether or not the + * caller passed HEAP_PAGE_PRUNE_FREEZE, because if they did not, we won't + * call heap_prepare_freeze_tuple() for each tuple, and set_all_frozen + * will never be cleared for tuples that need freezing. This would lead to + * incorrectly setting the visibility map all-frozen for this page. We + * can't set the page all-frozen in the VM if the caller didn't pass + * HEAP_PAGE_PRUNE_SET_VM. + * + * When freezing is not required (no XIDs/MXIDs older than the freeze + * cutoff), we may still choose to "opportunistically" freeze if doing so + * would make the page all-frozen. + * + * We will not be able to freeze the whole page at the end of vacuum if + * there are tuples present that are not visible to everyone or if there + * are dead tuples which will not be removable. However, dead tuples that + * will be removed by the end of vacuum should not prevent this + * opportunistic freezing. + * + * Therefore, we do not clear set_all_visible and set_all_frozen when we + * encounter LP_DEAD items. Instead, we correct them after deciding + * whether to freeze, but before updating the VM, to avoid setting the VM + * bits incorrectly. */ - prstate.visibility_cutoff_xid = InvalidTransactionId; + prstate->set_all_frozen = prstate->attempt_freeze && prstate->attempt_set_vm; +} - maxoff = PageGetMaxOffsetNumber(page); - tup.t_tableOid = RelationGetRelid(relation); +/* + * Helper for heap_page_prune_and_freeze(). Iterates over every tuple on the + * page, examines its visibility information, and determines the appropriate + * action for each tuple. All tuples are processed and classified during this + * phase, but no modifications are made to the page until the later execution + * stage. + * + * *off_loc is used for error callback and cleared before returning. + */ +static void +prune_freeze_plan(PruneState *prstate, OffsetNumber *off_loc) +{ + Page page = prstate->page; + BlockNumber blockno = prstate->block; + OffsetNumber maxoff = PageGetMaxOffsetNumber(prstate->page); + OffsetNumber offnum; + HeapTupleData tup; + + tup.t_tableOid = RelationGetRelid(prstate->relation); /* * Determine HTSV for all tuples, and queue them up for processing as HOT @@ -502,13 +571,13 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, */ *off_loc = offnum; - prstate.processed[offnum] = false; - prstate.htsv[offnum] = -1; + prstate->processed[offnum] = false; + prstate->htsv[offnum] = -1; /* Nothing to do if slot doesn't contain a tuple */ if (!ItemIdIsUsed(itemid)) { - heap_prune_record_unchanged_lp_unused(page, &prstate, offnum); + heap_prune_record_unchanged_lp_unused(prstate, offnum); continue; } @@ -518,17 +587,17 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * If the caller set mark_unused_now true, we can set dead line * pointers LP_UNUSED now. */ - if (unlikely(prstate.mark_unused_now)) - heap_prune_record_unused(&prstate, offnum, false); + if (unlikely(prstate->mark_unused_now)) + heap_prune_record_unused(prstate, offnum, false); else - heap_prune_record_unchanged_lp_dead(page, &prstate, offnum); + heap_prune_record_unchanged_lp_dead(prstate, offnum); continue; } if (ItemIdIsRedirected(itemid)) { /* This is the start of a HOT chain */ - prstate.root_items[prstate.nroot_items++] = offnum; + prstate->root_items[prstate->nroot_items++] = offnum; continue; } @@ -542,21 +611,14 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, tup.t_len = ItemIdGetLength(itemid); ItemPointerSet(&tup.t_self, blockno, offnum); - prstate.htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup, - buffer); + prstate->htsv[offnum] = heap_prune_satisfies_vacuum(prstate, &tup); if (!HeapTupleHeaderIsHeapOnly(htup)) - prstate.root_items[prstate.nroot_items++] = offnum; + prstate->root_items[prstate->nroot_items++] = offnum; else - prstate.heaponly_items[prstate.nheaponly_items++] = offnum; + prstate->heaponly_items[prstate->nheaponly_items++] = offnum; } - /* - * If checksums are enabled, heap_prune_satisfies_vacuum() may have caused - * an FPI to be emitted. - */ - hint_bit_fpi = fpi_before != pgWalUsage.wal_fpi; - /* * Process HOT chains. * @@ -568,30 +630,30 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * the page instead of using the root_items array, also did it in * ascending offset number order.) */ - for (int i = prstate.nroot_items - 1; i >= 0; i--) + for (int i = prstate->nroot_items - 1; i >= 0; i--) { - offnum = prstate.root_items[i]; + offnum = prstate->root_items[i]; /* Ignore items already processed as part of an earlier chain */ - if (prstate.processed[offnum]) + if (prstate->processed[offnum]) continue; /* see preceding loop */ *off_loc = offnum; /* Process this item or chain of items */ - heap_prune_chain(page, blockno, maxoff, offnum, &prstate); + heap_prune_chain(maxoff, offnum, prstate); } /* * Process any heap-only tuples that were not already processed as part of * a HOT chain. */ - for (int i = prstate.nheaponly_items - 1; i >= 0; i--) + for (int i = prstate->nheaponly_items - 1; i >= 0; i--) { - offnum = prstate.heaponly_items[i]; + offnum = prstate->heaponly_items[i]; - if (prstate.processed[offnum]) + if (prstate->processed[offnum]) continue; /* see preceding loop */ @@ -610,7 +672,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * return true for an XMIN_INVALID tuple, so this code will work even * when there were sequential updates within the aborted transaction.) */ - if (prstate.htsv[offnum] == HEAPTUPLE_DEAD) + if (prstate->htsv[offnum] == HEAPTUPLE_DEAD) { ItemId itemid = PageGetItemId(page, offnum); HeapTupleHeader htup = (HeapTupleHeader) PageGetItem(page, itemid); @@ -618,8 +680,8 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, if (likely(!HeapTupleHeaderIsHotUpdated(htup))) { HeapTupleHeaderAdvanceConflictHorizon(htup, - &prstate.latest_xid_removed); - heap_prune_record_unused(&prstate, offnum, true); + &prstate->latest_xid_removed); + heap_prune_record_unused(prstate, offnum, true); } else { @@ -636,7 +698,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, } } else - heap_prune_record_unchanged_lp_normal(page, &prstate, offnum); + heap_prune_record_unchanged_lp_normal(prstate, offnum); } /* We should now have processed every tuple exactly once */ @@ -647,75 +709,89 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, { *off_loc = offnum; - Assert(prstate.processed[offnum]); + Assert(prstate->processed[offnum]); } #endif /* Clear the offset information once we have processed the given page. */ *off_loc = InvalidOffsetNumber; +} - do_prune = prstate.nredirected > 0 || - prstate.ndead > 0 || - prstate.nunused > 0; +/* + * Decide whether to proceed with freezing according to the freeze plans + * prepared for the current heap buffer. If freezing is chosen, this function + * performs several pre-freeze checks. + * + * The values of do_prune, do_hint_prune, and did_tuple_hint_fpi must be + * determined before calling this function. + * + * prstate is both an input and output parameter. + * + * Returns true if we should apply the freeze plans and freeze tuples on the + * page, and false otherwise. + */ +static bool +heap_page_will_freeze(bool did_tuple_hint_fpi, + bool do_prune, + bool do_hint_prune, + PruneState *prstate) +{ + bool do_freeze = false; /* - * Even if we don't prune anything, if we found a new value for the - * pd_prune_xid field or the page was marked full, we will update the hint - * bit. + * If the caller specified we should not attempt to freeze any tuples, + * validate that everything is in the right state and return. */ - do_hint = ((PageHeader) page)->pd_prune_xid != prstate.new_prune_xid || - PageIsFull(page); + if (!prstate->attempt_freeze) + { + Assert(!prstate->set_all_frozen && prstate->nfrozen == 0); + return false; + } - /* - * Decide if we want to go ahead with freezing according to the freeze - * plans we prepared, or not. - */ - do_freeze = false; - if (prstate.freeze) + if (prstate->pagefrz.freeze_required) { - if (prstate.pagefrz.freeze_required) - { - /* - * heap_prepare_freeze_tuple indicated that at least one XID/MXID - * from before FreezeLimit/MultiXactCutoff is present. Must - * freeze to advance relfrozenxid/relminmxid. - */ - do_freeze = true; - } - else + /* + * heap_prepare_freeze_tuple indicated that at least one XID/MXID from + * before FreezeLimit/MultiXactCutoff is present. Must freeze to + * advance relfrozenxid/relminmxid. + */ + do_freeze = true; + } + else + { + /* + * Opportunistically freeze the page if we are generating an FPI + * anyway and if doing so means that we can set the page all-frozen + * afterwards (might not happen until VACUUM's final heap pass). + * + * XXX: Previously, we knew if pruning emitted an FPI by checking + * pgWalUsage.wal_fpi before and after pruning. Once the freeze and + * prune records were combined, this heuristic couldn't be used + * anymore. The opportunistic freeze heuristic must be improved; + * however, for now, try to approximate the old logic. + */ + if (prstate->set_all_frozen && prstate->nfrozen > 0) { + Assert(prstate->set_all_visible); + /* - * Opportunistically freeze the page if we are generating an FPI - * anyway and if doing so means that we can set the page - * all-frozen afterwards (might not happen until VACUUM's final - * heap pass). - * - * XXX: Previously, we knew if pruning emitted an FPI by checking - * pgWalUsage.wal_fpi before and after pruning. Once the freeze - * and prune records were combined, this heuristic couldn't be - * used anymore. The opportunistic freeze heuristic must be - * improved; however, for now, try to approximate the old logic. + * Freezing would make the page all-frozen. Have already emitted + * an FPI or will do so anyway? */ - if (prstate.all_visible && prstate.all_frozen && prstate.nfrozen > 0) + if (RelationNeedsWAL(prstate->relation)) { - /* - * Freezing would make the page all-frozen. Have already - * emitted an FPI or will do so anyway? - */ - if (RelationNeedsWAL(relation)) + if (did_tuple_hint_fpi) + do_freeze = true; + else if (do_prune) + { + if (XLogCheckBufferNeedsBackup(prstate->buffer)) + do_freeze = true; + } + else if (do_hint_prune) { - if (hint_bit_fpi) + if (XLogHintBitIsNeeded() && + XLogCheckBufferNeedsBackup(prstate->buffer)) do_freeze = true; - else if (do_prune) - { - if (XLogCheckBufferNeedsBackup(buffer)) - do_freeze = true; - } - else if (do_hint) - { - if (XLogHintBitIsNeeded() && XLogCheckBufferNeedsBackup(buffer)) - do_freeze = true; - } } } } @@ -727,18 +803,20 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * Validate the tuples we will be freezing before entering the * critical section. */ - heap_pre_freeze_checks(buffer, prstate.frozen, prstate.nfrozen); + heap_pre_freeze_checks(prstate->buffer, prstate->frozen, prstate->nfrozen); + Assert(TransactionIdPrecedes(prstate->pagefrz.FreezePageConflictXid, + prstate->cutoffs->OldestXmin)); } - else if (prstate.nfrozen > 0) + else if (prstate->nfrozen > 0) { /* * The page contained some tuples that were not already frozen, and we * chose not to freeze them now. The page won't be all-frozen then. */ - Assert(!prstate.pagefrz.freeze_required); + Assert(!prstate->pagefrz.freeze_required); - prstate.all_frozen = false; - prstate.nfrozen = 0; /* avoid miscounts in instrumentation */ + prstate->set_all_frozen = false; + prstate->nfrozen = 0; /* avoid miscounts in instrumentation */ } else { @@ -750,93 +828,482 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, */ } + return do_freeze; +} + +/* + * Emit a warning about and fix visibility map corruption on the given page. + * + * The caller specifies the type of corruption it has already detected via + * corruption_type, so that we can emit the appropriate warning. All cases + * result in the VM bits being cleared; corruption types where PD_ALL_VISIBLE + * is incorrectly set also clear PD_ALL_VISIBLE. + * + * Must be called while holding an exclusive lock on the heap buffer. Dead + * items and not all-visible tuples must have been discovered under that same + * lock. Although we do not hold a lock on the VM buffer, it is pinned, and + * the heap buffer is exclusively locked, ensuring that no other backend can + * update the VM bits corresponding to this heap page. + * + * This function makes changes to the VM and, potentially, the heap page, but + * it does not need to be done in a critical section. + */ +static void +heap_page_fix_vm_corruption(PruneState *prstate, OffsetNumber offnum, + VMCorruptionType corruption_type) +{ + const char *relname = RelationGetRelationName(prstate->relation); + bool do_clear_vm = false; + bool do_clear_heap = false; + + Assert(BufferIsLockedByMeInMode(prstate->buffer, BUFFER_LOCK_EXCLUSIVE)); + + switch (corruption_type) + { + case VM_CORRUPT_LPDEAD: + ereport(WARNING, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("dead line pointer found on page marked all-visible"), + errcontext("relation \"%s\", page %u, tuple %u", + relname, prstate->block, offnum))); + do_clear_vm = true; + do_clear_heap = true; + break; + + case VM_CORRUPT_TUPLE_VISIBILITY: + + /* + * A HEAPTUPLE_LIVE tuple on an all-visible page can appear to not + * be visible to everyone when + * GetOldestNonRemovableTransactionId() returns a conservative + * value that's older than the real safe xmin. That is not + * corruption -- the PD_ALL_VISIBLE flag is still correct. + * + * However, dead tuple versions, in-progress inserts, and + * in-progress deletes should never appear on a page marked + * all-visible. That indicates real corruption. PD_ALL_VISIBLE + * should have been cleared by the DML operation that deleted or + * inserted the tuple. + */ + ereport(WARNING, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("tuple not visible to all transactions found on page marked all-visible"), + errcontext("relation \"%s\", page %u, tuple %u", + relname, prstate->block, offnum))); + do_clear_vm = true; + do_clear_heap = true; + break; + + case VM_CORRUPT_MISSING_PAGE_HINT: + + /* + * As of PostgreSQL 9.2, the visibility map bit should never be + * set if the page-level bit is clear. However, for vacuum, it's + * possible that the bit got cleared after + * heap_vac_scan_next_block() was called, so we must recheck now + * that we have the buffer lock before concluding that the VM is + * corrupt. + */ + Assert(!PageIsAllVisible(prstate->page)); + Assert(prstate->old_vmbits & VISIBILITYMAP_VALID_BITS); + ereport(WARNING, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("page is not marked all-visible but visibility map bit is set"), + errcontext("relation \"%s\", page %u", + relname, prstate->block))); + do_clear_vm = true; + break; + } + + Assert(do_clear_heap || do_clear_vm); + + /* Avoid marking the buffer dirty if PD_ALL_VISIBLE is already clear */ + if (do_clear_heap) + { + Assert(PageIsAllVisible(prstate->page)); + PageClearAllVisible(prstate->page); + MarkBufferDirtyHint(prstate->buffer, true); + } + + if (do_clear_vm) + { + visibilitymap_clear(prstate->relation, prstate->block, + prstate->vmbuffer, + VISIBILITYMAP_VALID_BITS); + prstate->old_vmbits = 0; + } +} + +/* + * Decide whether to set the visibility map bits (all-visible and all-frozen) + * for the current page using information from the PruneState and VM. + * + * This function does not actually set the VM bits or page-level visibility + * hint, PD_ALL_VISIBLE. + * + * This should be called only after do_freeze has been decided (and do_prune + * has been set), as these factor into our heuristic-based decision. + * + * Returns true if one or both VM bits should be set and false otherwise. + */ +static bool +heap_page_will_set_vm(PruneState *prstate, PruneReason reason, + bool do_prune, bool do_freeze) +{ + if (!prstate->attempt_set_vm) + return false; + + if (!prstate->set_all_visible) + return false; + + /* + * If this is an on-access call and we're not actually pruning, avoid + * setting the visibility map if it would newly dirty the heap page or, if + * the page is already dirty, if doing so would require including a + * full-page image (FPI) of the heap page in the WAL. + */ + if (reason == PRUNE_ON_ACCESS && !do_prune && !do_freeze && + (!BufferIsDirty(prstate->buffer) || XLogCheckBufferNeedsBackup(prstate->buffer))) + { + prstate->set_all_visible = prstate->set_all_frozen = false; + return false; + } + + prstate->new_vmbits = VISIBILITYMAP_ALL_VISIBLE; + + if (prstate->set_all_frozen) + prstate->new_vmbits |= VISIBILITYMAP_ALL_FROZEN; + + if (prstate->new_vmbits == prstate->old_vmbits) + { + prstate->new_vmbits = 0; + return false; + } + + return true; +} + +/* + * If the page is already all-frozen, or already all-visible and freezing + * won't be attempted, there is no remaining work and we can use the fast path + * to avoid the expensive overhead of heap_page_prune_and_freeze(). + * + * This can happen when the page has a stale prune hint, or if VACUUM is + * scanning an already all-frozen page due to SKIP_PAGES_THRESHOLD. + * + * The caller must already have examined the visibility map and saved the + * status of the page's VM bits in prstate->old_vmbits. Caller must hold a + * content lock on the heap page since it will examine line pointers. + * + * Before calling prune_freeze_fast_path(), the caller should first + * check for and fix any discrepancy between the page-level visibility hint + * and the visibility map. Otherwise, the fast path will always prevent us + * from getting them in sync. Note that if there are tuples on the page that + * are not visible to all but the VM is incorrectly marked + * all-visible/all-frozen, we will not get the chance to fix that corruption + * when using the fast path. + */ +static void +prune_freeze_fast_path(PruneState *prstate, PruneFreezeResult *presult) +{ + OffsetNumber maxoff = PageGetMaxOffsetNumber(prstate->page); + Page page = prstate->page; + + Assert((prstate->old_vmbits & VISIBILITYMAP_ALL_FROZEN) || + ((prstate->old_vmbits & VISIBILITYMAP_ALL_VISIBLE) && + !prstate->attempt_freeze)); + + /* We'll fill in presult for the caller */ + memset(presult, 0, sizeof(PruneFreezeResult)); + + /* Clear any stale prune hint */ + if (TransactionIdIsValid(PageGetPruneXid(page))) + { + PageClearPrunable(page); + MarkBufferDirtyHint(prstate->buffer, true); + } + + if (PageIsEmpty(page)) + return; + + /* + * Since the page is all-visible, a count of the normal ItemIds on the + * page should be sufficient for vacuum's live tuple count. + */ + for (OffsetNumber off = FirstOffsetNumber; + off <= maxoff; + off = OffsetNumberNext(off)) + { + ItemId lp = PageGetItemId(page, off); + + if (!ItemIdIsUsed(lp)) + continue; + + presult->hastup = true; + + if (ItemIdIsNormal(lp)) + prstate->live_tuples++; + } + + presult->live_tuples = prstate->live_tuples; +} + +/* + * Prune and repair fragmentation and potentially freeze tuples on the + * specified page. If the page's visibility status has changed, update it in + * the VM. + * + * Caller must have pin and buffer cleanup lock on the page. Note that we + * don't update the FSM information for page on caller's behalf. Caller might + * also need to account for a reduction in the length of the line pointer + * array following array truncation by us. + * + * params contains the input parameters used to control freezing and pruning + * behavior. See the definition of PruneFreezeParams for more on what each + * parameter does. + * + * If the HEAP_PAGE_PRUNE_FREEZE option is set in params, we will freeze + * tuples if it's required in order to advance relfrozenxid / relminmxid, or + * if it's considered advantageous for overall system performance to do so + * now. The 'params.cutoffs', 'presult', 'new_relfrozen_xid' and + * 'new_relmin_mxid' arguments are required when freezing. + * + * A vmbuffer corresponding to the heap page is also passed and if the page is + * found to be all-visible/all-frozen, we will set it in the VM. + * + * presult contains output parameters needed by callers, such as the number of + * tuples removed and the offsets of dead items on the page after pruning. + * heap_page_prune_and_freeze() is responsible for initializing it. Required + * by all callers. + * + * off_loc is the offset location required by the caller to use in error + * callback. + * + * new_relfrozen_xid and new_relmin_mxid must be provided by the caller if the + * HEAP_PAGE_PRUNE_FREEZE option is set in params. On entry, they contain the + * oldest XID and multi-XID seen on the relation so far. They will be updated + * with the oldest values present on the page after pruning. After processing + * the whole relation, VACUUM can use these values as the new + * relfrozenxid/relminmxid for the relation. + */ +void +heap_page_prune_and_freeze(PruneFreezeParams *params, + PruneFreezeResult *presult, + OffsetNumber *off_loc, + TransactionId *new_relfrozen_xid, + MultiXactId *new_relmin_mxid) +{ + PruneState prstate; + bool do_freeze; + bool do_prune; + bool do_hint_prune; + bool do_set_vm; + bool did_tuple_hint_fpi; + int64 fpi_before = pgWalUsage.wal_fpi; + TransactionId conflict_xid; + + /* Initialize prstate */ + prune_freeze_setup(params, + new_relfrozen_xid, new_relmin_mxid, + presult, &prstate); + + /* + * If the VM is set but PD_ALL_VISIBLE is clear, fix that corruption + * before pruning and freezing so that the page and VM start out in a + * consistent state. + */ + if ((prstate.old_vmbits & VISIBILITYMAP_VALID_BITS) && + !PageIsAllVisible(prstate.page)) + heap_page_fix_vm_corruption(&prstate, InvalidOffsetNumber, + VM_CORRUPT_MISSING_PAGE_HINT); + + /* + * If the page is already all-frozen, or already all-visible when freezing + * is not being attempted, take the fast path, skipping pruning and + * freezing code entirely. This must be done after fixing any discrepancy + * between the page-level visibility hint and the VM, since that may have + * cleared old_vmbits. + */ + if ((params->options & HEAP_PAGE_PRUNE_ALLOW_FAST_PATH) != 0 && + ((prstate.old_vmbits & VISIBILITYMAP_ALL_FROZEN) || + ((prstate.old_vmbits & VISIBILITYMAP_ALL_VISIBLE) && + !prstate.attempt_freeze))) + { + prune_freeze_fast_path(&prstate, presult); + return; + } + + /* + * Examine all line pointers and tuple visibility information to determine + * which line pointers should change state and which tuples may be frozen. + * Prepare queue of state changes to later be executed in a critical + * section. + */ + prune_freeze_plan(&prstate, off_loc); + + /* + * After processing all the live tuples on the page, if the newest xmin + * amongst them may be considered running by any snapshot, the page cannot + * be all-visible. This should be done before determining whether or not + * to opportunistically freeze. + */ + if (prstate.set_all_visible && + TransactionIdIsNormal(prstate.newest_live_xid) && + GlobalVisTestXidConsideredRunning(prstate.vistest, + prstate.newest_live_xid, + true)) + prstate.set_all_visible = prstate.set_all_frozen = false; + + /* + * If checksums are enabled, calling heap_prune_satisfies_vacuum() while + * checking tuple visibility information in prune_freeze_plan() may have + * caused an FPI to be emitted. + */ + did_tuple_hint_fpi = fpi_before != pgWalUsage.wal_fpi; + + do_prune = prstate.nredirected > 0 || + prstate.ndead > 0 || + prstate.nunused > 0; + + /* + * Even if we don't prune anything, if we found a new value for the + * pd_prune_xid field or the page was marked full, we will update the hint + * bit. + */ + do_hint_prune = PageGetPruneXid(prstate.page) != prstate.new_prune_xid || + PageIsFull(prstate.page); + + /* + * Decide if we want to go ahead with freezing according to the freeze + * plans we prepared, or not. + */ + do_freeze = heap_page_will_freeze(did_tuple_hint_fpi, + do_prune, + do_hint_prune, + &prstate); + + /* + * While scanning the line pointers, we did not clear + * set_all_visible/set_all_frozen when encountering LP_DEAD items because + * we wanted the decision whether or not to freeze the page to be + * unaffected by the short-term presence of LP_DEAD items. These LP_DEAD + * items are effectively assumed to be LP_UNUSED items in the making. It + * doesn't matter which vacuum heap pass (initial pass or final pass) ends + * up setting the page all-frozen, as long as the ongoing VACUUM does it. + * + * Now that we finished determining whether or not to freeze the page, + * update set_all_visible and set_all_frozen so that they reflect the true + * state of the page for setting PD_ALL_VISIBLE and VM bits. + */ + if (prstate.lpdead_items > 0) + prstate.set_all_visible = prstate.set_all_frozen = false; + + Assert(!prstate.set_all_frozen || prstate.set_all_visible); + Assert(!prstate.set_all_visible || prstate.attempt_set_vm); + Assert(!prstate.set_all_visible || (prstate.lpdead_items == 0)); + + do_set_vm = heap_page_will_set_vm(&prstate, params->reason, do_prune, do_freeze); + + /* + * new_vmbits should be 0 regardless of whether or not the page is + * all-visible if we do not intend to set the VM. + */ + Assert(do_set_vm || prstate.new_vmbits == 0); + + /* + * The snapshot conflict horizon for the whole record is the most + * conservative (newest) horizon required by any change in the record. + */ + conflict_xid = InvalidTransactionId; + if (do_set_vm) + conflict_xid = prstate.newest_live_xid; + if (do_freeze && TransactionIdFollows(prstate.pagefrz.FreezePageConflictXid, conflict_xid)) + conflict_xid = prstate.pagefrz.FreezePageConflictXid; + if (do_prune && TransactionIdFollows(prstate.latest_xid_removed, conflict_xid)) + conflict_xid = prstate.latest_xid_removed; + + /* Lock vmbuffer before entering a critical section */ + if (do_set_vm) + LockBuffer(prstate.vmbuffer, BUFFER_LOCK_EXCLUSIVE); + /* Any error while applying the changes is critical */ START_CRIT_SECTION(); - if (do_hint) + if (do_hint_prune) { /* * Update the page's pd_prune_xid field to either zero, or the lowest * XID of any soon-prunable tuple. */ - ((PageHeader) page)->pd_prune_xid = prstate.new_prune_xid; + ((PageHeader) prstate.page)->pd_prune_xid = prstate.new_prune_xid; /* * Also clear the "page is full" flag, since there's no point in * repeating the prune/defrag process until something else happens to * the page. */ - PageClearFull(page); + PageClearFull(prstate.page); /* * If that's all we had to do to the page, this is a non-WAL-logged - * hint. If we are going to freeze or prune the page, we will mark - * the buffer dirty below. + * hint. If we are going to freeze or prune the page or set + * PD_ALL_VISIBLE, we will mark the buffer dirty below. + * + * Setting PD_ALL_VISIBLE is fully WAL-logged because it is forbidden + * for the VM to be set and PD_ALL_VISIBLE to be clear. */ - if (!do_freeze && !do_prune) - MarkBufferDirtyHint(buffer, true); + if (!do_freeze && !do_prune && !do_set_vm) + MarkBufferDirtyHint(prstate.buffer, true); } - if (do_prune || do_freeze) + if (do_prune || do_freeze || do_set_vm) { /* Apply the planned item changes and repair page fragmentation. */ if (do_prune) { - heap_page_prune_execute(buffer, false, + heap_page_prune_execute(prstate.buffer, false, prstate.redirected, prstate.nredirected, prstate.nowdead, prstate.ndead, prstate.nowunused, prstate.nunused); } if (do_freeze) - heap_freeze_prepared_tuples(buffer, prstate.frozen, prstate.nfrozen); + heap_freeze_prepared_tuples(prstate.buffer, prstate.frozen, prstate.nfrozen); - MarkBufferDirty(buffer); - - /* - * Emit a WAL XLOG_HEAP2_PRUNE_FREEZE record showing what we did - */ - if (RelationNeedsWAL(relation)) + /* Set the visibility map and page visibility hint */ + if (do_set_vm) { /* - * The snapshotConflictHorizon for the whole record should be the - * most conservative of all the horizons calculated for any of the - * possible modifications. If this record will prune tuples, any - * transactions on the standby older than the youngest xmax of the - * most recently removed tuple this record will prune will - * conflict. If this record will freeze tuples, any transactions - * on the standby with xids older than the youngest tuple this - * record will freeze will conflict. - */ - TransactionId frz_conflict_horizon = InvalidTransactionId; - TransactionId conflict_xid; - - /* - * We can use the visibility_cutoff_xid as our cutoff for - * conflicts when the whole page is eligible to become all-frozen - * in the VM once we're done with it. Otherwise we generate a - * conservative cutoff by stepping back from OldestXmin. + * While it is valid for PD_ALL_VISIBLE to be set when the + * corresponding VM bit is clear, we strongly prefer to keep them + * in sync. + * + * The heap buffer must be marked dirty before adding it to the + * WAL chain when setting the VM. We don't worry about + * unnecessarily dirtying the heap buffer if PD_ALL_VISIBLE is + * already set, though. It is extremely rare to have a clean heap + * buffer with PD_ALL_VISIBLE already set and the VM bits clear, + * so there is no point in optimizing it. */ - if (do_freeze) - { - if (prstate.all_visible && prstate.all_frozen) - frz_conflict_horizon = prstate.visibility_cutoff_xid; - else - { - /* Avoids false conflicts when hot_standby_feedback in use */ - frz_conflict_horizon = prstate.cutoffs->OldestXmin; - TransactionIdRetreat(frz_conflict_horizon); - } - } + PageSetAllVisible(prstate.page); + PageClearPrunable(prstate.page); + visibilitymap_set(prstate.block, prstate.vmbuffer, prstate.new_vmbits, + prstate.relation->rd_locator); + } - if (TransactionIdFollows(frz_conflict_horizon, prstate.latest_xid_removed)) - conflict_xid = frz_conflict_horizon; - else - conflict_xid = prstate.latest_xid_removed; + MarkBufferDirty(prstate.buffer); - log_heap_prune_and_freeze(relation, buffer, + /* + * Emit a WAL XLOG_HEAP2_PRUNE* record showing what we did + */ + if (RelationNeedsWAL(prstate.relation)) + { + log_heap_prune_and_freeze(prstate.relation, prstate.buffer, + do_set_vm ? prstate.vmbuffer : InvalidBuffer, + do_set_vm ? prstate.new_vmbits : 0, conflict_xid, - true, reason, + true, /* cleanup lock */ + params->reason, prstate.frozen, prstate.nfrozen, prstate.redirected, prstate.nredirected, prstate.nowdead, prstate.ndead, @@ -846,55 +1313,72 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, END_CRIT_SECTION(); - /* Copy information back for caller */ - presult->ndeleted = prstate.ndeleted; - presult->nnewlpdead = prstate.ndead; - presult->nfrozen = prstate.nfrozen; - presult->live_tuples = prstate.live_tuples; - presult->recently_dead_tuples = prstate.recently_dead_tuples; + if (do_set_vm) + LockBuffer(prstate.vmbuffer, BUFFER_LOCK_UNLOCK); /* - * It was convenient to ignore LP_DEAD items in all_visible earlier on to - * make the choice of whether or not to freeze the page unaffected by the - * short-term presence of LP_DEAD items. These LP_DEAD items were - * effectively assumed to be LP_UNUSED items in the making. It doesn't - * matter which vacuum heap pass (initial pass or final pass) ends up - * setting the page all-frozen, as long as the ongoing VACUUM does it. - * - * Now that freezing has been finalized, unset all_visible if there are - * any LP_DEAD items on the page. It needs to reflect the present state - * of the page, as expected by our caller. + * During its second pass over the heap, VACUUM calls + * heap_page_would_be_all_visible() to determine whether a page is + * all-visible and all-frozen. The logic here is similar. After completing + * pruning and freezing, use an assertion to verify that our results + * remain consistent with heap_page_would_be_all_visible(). It's also a + * valuable cross-check of the page state after pruning and freezing. */ - if (prstate.all_visible && prstate.lpdead_items == 0) - { - presult->all_visible = prstate.all_visible; - presult->all_frozen = prstate.all_frozen; - } - else +#ifdef USE_ASSERT_CHECKING + if (prstate.set_all_visible) { - presult->all_visible = false; - presult->all_frozen = false; + TransactionId debug_cutoff; + bool debug_all_frozen; + + Assert(prstate.lpdead_items == 0); + + Assert(heap_page_is_all_visible(prstate.relation, prstate.buffer, + prstate.vistest, + &debug_all_frozen, + &debug_cutoff, off_loc)); + + Assert(!TransactionIdIsValid(debug_cutoff) || + debug_cutoff == prstate.newest_live_xid); + + /* + * It's possible the page is composed entirely of frozen tuples but is + * not set all-frozen in the VM and did not pass + * HEAP_PAGE_PRUNE_FREEZE. In this case, it's possible + * heap_page_is_all_visible() finds the page completely frozen, even + * though prstate.set_all_frozen is false. + */ + Assert(!prstate.set_all_frozen || debug_all_frozen); } +#endif + /* Copy information back for caller */ + presult->ndeleted = prstate.ndeleted; + presult->nnewlpdead = prstate.ndead; + presult->nfrozen = prstate.nfrozen; + presult->live_tuples = prstate.live_tuples; + presult->recently_dead_tuples = prstate.recently_dead_tuples; presult->hastup = prstate.hastup; - /* - * For callers planning to update the visibility map, the conflict horizon - * for that record must be the newest xmin on the page. However, if the - * page is completely frozen, there can be no conflict and the - * vm_conflict_horizon should remain InvalidTransactionId. This includes - * the case that we just froze all the tuples; the prune-freeze record - * included the conflict XID already so the caller doesn't need it. - */ - if (presult->all_frozen) - presult->vm_conflict_horizon = InvalidTransactionId; - else - presult->vm_conflict_horizon = prstate.visibility_cutoff_xid; - presult->lpdead_items = prstate.lpdead_items; /* the presult->deadoffsets array was already filled in */ - if (prstate.freeze) + presult->newly_all_visible = false; + presult->newly_all_frozen = false; + presult->newly_all_visible_frozen = false; + if (do_set_vm) + { + if ((prstate.old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0) + { + presult->newly_all_visible = true; + if (prstate.set_all_frozen) + presult->newly_all_visible_frozen = true; + } + else if ((prstate.old_vmbits & VISIBILITYMAP_ALL_FROZEN) == 0 && + prstate.set_all_frozen) + presult->newly_all_frozen = true; + } + + if (prstate.attempt_freeze) { if (presult->nfrozen > 0) { @@ -914,12 +1398,12 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * Perform visibility checks for heap pruning. */ static HTSV_Result -heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup, Buffer buffer) +heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup) { HTSV_Result res; TransactionId dead_after; - res = HeapTupleSatisfiesVacuumHorizon(tup, buffer, &dead_after); + res = HeapTupleSatisfiesVacuumHorizon(tup, prstate->buffer, &dead_after); if (res != HEAPTUPLE_RECENTLY_DEAD) return res; @@ -943,7 +1427,7 @@ heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup, Buffer buffer) * if the GlobalVisState has been updated since the beginning of vacuuming * the relation. */ - if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after)) + if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after, true)) return HEAPTUPLE_DEAD; return res; @@ -996,13 +1480,14 @@ htsv_get_valid_status(int status) * based on that outcome. */ static void -heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff, - OffsetNumber rootoffnum, PruneState *prstate) +heap_prune_chain(OffsetNumber maxoff, OffsetNumber rootoffnum, + PruneState *prstate) { TransactionId priorXmax = InvalidTransactionId; ItemId rootlp; OffsetNumber offnum; OffsetNumber chainitems[MaxHeapTuplesPerPage]; + Page page = prstate->page; /* * After traversing the HOT chain, ndeadchain is the index in chainitems @@ -1131,7 +1616,7 @@ heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff, /* * Advance to next chain member. */ - Assert(ItemPointerGetBlockNumber(&htup->t_ctid) == blockno); + Assert(ItemPointerGetBlockNumber(&htup->t_ctid) == prstate->block); offnum = ItemPointerGetOffsetNumber(&htup->t_ctid); priorXmax = HeapTupleHeaderGetUpdateXid(htup); } @@ -1166,7 +1651,7 @@ heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff, i++; } for (; i < nchain; i++) - heap_prune_record_unchanged_lp_normal(page, prstate, chainitems[i]); + heap_prune_record_unchanged_lp_normal(prstate, chainitems[i]); } else if (ndeadchain == nchain) { @@ -1192,13 +1677,14 @@ heap_prune_chain(Page page, BlockNumber blockno, OffsetNumber maxoff, /* the rest of tuples in the chain are normal, unchanged tuples */ for (int i = ndeadchain; i < nchain; i++) - heap_prune_record_unchanged_lp_normal(page, prstate, chainitems[i]); + heap_prune_record_unchanged_lp_normal(prstate, chainitems[i]); } } /* Record lowest soon-prunable XID */ static void -heap_prune_record_prunable(PruneState *prstate, TransactionId xid) +heap_prune_record_prunable(PruneState *prstate, TransactionId xid, + OffsetNumber offnum) { /* * This should exactly match the PageSetPrunable macro. We can't store @@ -1208,6 +1694,14 @@ heap_prune_record_prunable(PruneState *prstate, TransactionId xid) if (!TransactionIdIsValid(prstate->new_prune_xid) || TransactionIdPrecedes(xid, prstate->new_prune_xid)) prstate->new_prune_xid = xid; + + /* + * It's incorrect for a page to be marked all-visible if it contains + * prunable items. + */ + if (PageIsAllVisible(prstate->page)) + heap_page_fix_vm_corruption(prstate, offnum, + VM_CORRUPT_TUPLE_VISIBILITY); } /* Record line pointer to be redirected */ @@ -1254,8 +1748,9 @@ heap_prune_record_dead(PruneState *prstate, OffsetNumber offnum, prstate->ndead++; /* - * Deliberately delay unsetting all_visible until later during pruning. - * Removable dead tuples shouldn't preclude freezing the page. + * Deliberately delay unsetting set_all_visible and set_all_frozen until + * later during pruning. Removable dead tuples shouldn't preclude freezing + * the page. */ /* Record the dead offset for vacuum */ @@ -1290,6 +1785,15 @@ heap_prune_record_dead_or_unused(PruneState *prstate, OffsetNumber offnum, heap_prune_record_unused(prstate, offnum, was_normal); else heap_prune_record_dead(prstate, offnum, was_normal); + + /* + * It's incorrect for the page to be set all-visible if it contains dead + * items. Fix that on the heap page and check the VM for corruption as + * well. Do that here rather than in heap_prune_record_dead() so we also + * cover tuples that are directly marked LP_UNUSED via mark_unused_now. + */ + if (PageIsAllVisible(prstate->page)) + heap_page_fix_vm_corruption(prstate, offnum, VM_CORRUPT_LPDEAD); } /* Record line pointer to be marked unused */ @@ -1316,7 +1820,7 @@ heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum, bool was_norm * Record an unused line pointer that is left unchanged. */ static void -heap_prune_record_unchanged_lp_unused(Page page, PruneState *prstate, OffsetNumber offnum) +heap_prune_record_unchanged_lp_unused(PruneState *prstate, OffsetNumber offnum) { Assert(!prstate->processed[offnum]); prstate->processed[offnum] = true; @@ -1327,9 +1831,11 @@ heap_prune_record_unchanged_lp_unused(Page page, PruneState *prstate, OffsetNumb * update bookkeeping of tuple counts and page visibility. */ static void -heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumber offnum) +heap_prune_record_unchanged_lp_normal(PruneState *prstate, OffsetNumber offnum) { HeapTupleHeader htup; + TransactionId xmin; + Page page = prstate->page; Assert(!prstate->processed[offnum]); prstate->processed[offnum] = true; @@ -1376,55 +1882,41 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb * See SetHintBits for more info. Check that the tuple is hinted * xmin-committed because of that. */ - if (prstate->all_visible) + if (!HeapTupleHeaderXminCommitted(htup)) { - TransactionId xmin; - - if (!HeapTupleHeaderXminCommitted(htup)) - { - prstate->all_visible = false; - break; - } + prstate->set_all_visible = false; + prstate->set_all_frozen = false; + break; + } - /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? A FrozenTransactionId - * is seen as committed to everyone. Otherwise, we check if - * there is a snapshot that considers this xid to still be - * running, and if so, we don't consider the page all-visible. - */ - xmin = HeapTupleHeaderGetXmin(htup); + /* + * The inserter definitely committed. But we don't know if it is + * old enough that everyone sees it as committed. Later, after + * processing all the tuples on the page, we'll check if there is + * any snapshot that still considers the newest xid on the page to + * be running. If so, we don't consider the page all-visible. + */ + xmin = HeapTupleHeaderGetXmin(htup); - /* - * For now always use prstate->cutoffs for this test, because - * we only update 'all_visible' when freezing is requested. We - * could use GlobalVisTestIsRemovableXid instead, if a - * non-freezing caller wanted to set the VM bit. - */ - Assert(prstate->cutoffs); - if (!TransactionIdPrecedes(xmin, prstate->cutoffs->OldestXmin)) - { - prstate->all_visible = false; - break; - } + /* Track newest xmin on page. */ + if (TransactionIdFollows(xmin, prstate->newest_live_xid) && + TransactionIdIsNormal(xmin)) + prstate->newest_live_xid = xmin; - /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, prstate->visibility_cutoff_xid) && - TransactionIdIsNormal(xmin)) - prstate->visibility_cutoff_xid = xmin; - } break; case HEAPTUPLE_RECENTLY_DEAD: prstate->recently_dead_tuples++; - prstate->all_visible = false; + prstate->set_all_visible = false; + prstate->set_all_frozen = false; /* * This tuple will soon become DEAD. Update the hint field so * that the page is reconsidered for pruning in future. */ heap_prune_record_prunable(prstate, - HeapTupleHeaderGetUpdateXid(htup)); + HeapTupleHeaderGetUpdateXid(htup), + offnum); break; case HEAPTUPLE_INSERT_IN_PROGRESS: @@ -1436,14 +1928,17 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb * assumption is a bit shaky, but it is what acquire_sample_rows() * does, so be consistent. */ - prstate->all_visible = false; + prstate->set_all_visible = false; + prstate->set_all_frozen = false; /* - * If we wanted to optimize for aborts, we might consider marking - * the page prunable when we see INSERT_IN_PROGRESS. But we - * don't. See related decisions about when to mark the page - * prunable in heapam.c. + * Though there is nothing "prunable" on the page, we maintain + * pd_prune_xid for inserts so that we have the opportunity to + * mark them all-visible during the next round of pruning. */ + heap_prune_record_prunable(prstate, + HeapTupleHeaderGetXmin(htup), + offnum); break; case HEAPTUPLE_DELETE_IN_PROGRESS: @@ -1454,14 +1949,16 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb * will commit and update the counters after we report. */ prstate->live_tuples++; - prstate->all_visible = false; + prstate->set_all_visible = false; + prstate->set_all_frozen = false; /* * This tuple may soon become DEAD. Update the hint field so that * the page is reconsidered for pruning in future. */ heap_prune_record_prunable(prstate, - HeapTupleHeaderGetUpdateXid(htup)); + HeapTupleHeaderGetUpdateXid(htup), + offnum); break; default: @@ -1476,7 +1973,7 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb } /* Consider freezing any normal tuples which will not be removed */ - if (prstate->freeze) + if (prstate->attempt_freeze) { bool totally_frozen; @@ -1496,7 +1993,7 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb * definitely cannot be set all-frozen in the visibility map later on. */ if (!totally_frozen) - prstate->all_frozen = false; + prstate->set_all_frozen = false; } } @@ -1505,7 +2002,7 @@ heap_prune_record_unchanged_lp_normal(Page page, PruneState *prstate, OffsetNumb * Record line pointer that was already LP_DEAD and is left unchanged. */ static void -heap_prune_record_unchanged_lp_dead(Page page, PruneState *prstate, OffsetNumber offnum) +heap_prune_record_unchanged_lp_dead(PruneState *prstate, OffsetNumber offnum) { Assert(!prstate->processed[offnum]); prstate->processed[offnum] = true; @@ -1519,14 +2016,21 @@ heap_prune_record_unchanged_lp_dead(Page page, PruneState *prstate, OffsetNumber * hastup/nonempty_pages as provisional no matter how LP_DEAD items are * handled (handled here, or handled later on). * - * Similarly, don't unset all_visible until later, at the end of - * heap_page_prune_and_freeze(). This will allow us to attempt to freeze - * the page after pruning. As long as we unset it before updating the - * visibility map, this will be correct. + * Similarly, don't unset set_all_visible and set_all_frozen until later, + * at the end of heap_page_prune_and_freeze(). This will allow us to + * attempt to freeze the page after pruning. As long as we unset it + * before updating the visibility map, this will be correct. */ /* Record the dead offset for vacuum */ prstate->deadoffsets[prstate->lpdead_items++] = offnum; + + /* + * It's incorrect for a page to be marked all-visible if it contains dead + * items. + */ + if (PageIsAllVisible(prstate->page)) + heap_page_fix_vm_corruption(prstate, offnum, VM_CORRUPT_LPDEAD); } /* @@ -1563,7 +2067,7 @@ heap_page_prune_execute(Buffer buffer, bool lp_truncate_only, OffsetNumber *nowdead, int ndead, OffsetNumber *nowunused, int nunused) { - Page page = (Page) BufferGetPage(buffer); + Page page = BufferGetPage(buffer); OffsetNumber *offnum; HeapTupleHeader htup PG_USED_FOR_ASSERTS_ONLY; @@ -1911,8 +2415,8 @@ heap_log_freeze_eq(xlhp_freeze_plan *plan, HeapTupleFreeze *frz) static int heap_log_freeze_cmp(const void *arg1, const void *arg2) { - HeapTupleFreeze *frz1 = (HeapTupleFreeze *) arg1; - HeapTupleFreeze *frz2 = (HeapTupleFreeze *) arg2; + const HeapTupleFreeze *frz1 = arg1; + const HeapTupleFreeze *frz2 = arg2; if (frz1->xmax < frz2->xmax) return -1; @@ -2026,7 +2530,7 @@ heap_log_freeze_plan(HeapTupleFreeze *tuples, int ntuples, } /* - * Write an XLOG_HEAP2_PRUNE_FREEZE WAL record + * Write an XLOG_HEAP2_PRUNE* WAL record * * This is used for several different page maintenance operations: * @@ -2045,12 +2549,17 @@ heap_log_freeze_plan(HeapTupleFreeze *tuples, int ntuples, * replaying 'unused' items depends on whether they were all previously marked * as dead. * + * If the VM is being updated, vmflags will contain the bits to set. In this + * case, vmbuffer should already have been updated and marked dirty and should + * still be pinned and locked. + * * Note: This function scribbles on the 'frozen' array. * * Note: This is called in a critical section, so careful what you do here. */ void log_heap_prune_and_freeze(Relation relation, Buffer buffer, + Buffer vmbuffer, uint8 vmflags, TransactionId conflict_xid, bool cleanup_lock, PruneReason reason, @@ -2062,6 +2571,9 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer, xl_heap_prune xlrec; XLogRecPtr recptr; uint8 info; + uint8 regbuf_flags_heap; + + Page heap_page = BufferGetPage(buffer); /* The following local variables hold data registered in the WAL record: */ xlhp_freeze_plan plans[MaxHeapTuplesPerPage]; @@ -2070,8 +2582,34 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer, xlhp_prune_items dead_items; xlhp_prune_items unused_items; OffsetNumber frz_offsets[MaxHeapTuplesPerPage]; + bool do_prune = nredirected > 0 || ndead > 0 || nunused > 0; + bool do_set_vm = vmflags & VISIBILITYMAP_VALID_BITS; + bool heap_fpi_allowed = true; + + Assert((vmflags & VISIBILITYMAP_VALID_BITS) == vmflags); xlrec.flags = 0; + regbuf_flags_heap = REGBUF_STANDARD; + + /* + * We can avoid an FPI of the heap page if the only modification we are + * making to it is to set PD_ALL_VISIBLE and checksums/wal_log_hints are + * disabled. + * + * However, if the page has never been WAL-logged (LSN is invalid), we + * must force an FPI regardless. This can happen when another backend + * extends the heap, initializes the page, and then fails before WAL- + * logging it. Since heap extension is not WAL-logged, recovery might try + * to replay our record and find that the page isn't initialized, which + * would cause a PANIC. + */ + if (!XLogRecPtrIsValid(PageGetLSN(heap_page))) + regbuf_flags_heap |= REGBUF_FORCE_IMAGE; + else if (!do_prune && nfrozen == 0 && (!do_set_vm || !XLogHintBitIsNeeded())) + { + regbuf_flags_heap |= REGBUF_NO_IMAGE; + heap_fpi_allowed = false; + } /* * Prepare data for the buffer. The arrays are not actually in the @@ -2079,7 +2617,11 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer, * page image, the arrays can be omitted. */ XLogBeginInsert(); - XLogRegisterBuffer(0, buffer, REGBUF_STANDARD); + XLogRegisterBuffer(0, buffer, regbuf_flags_heap); + + if (do_set_vm) + XLogRegisterBuffer(1, vmbuffer, 0); + if (nfrozen > 0) { int nplans; @@ -2136,6 +2678,12 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer, * Prepare the main xl_heap_prune record. We already set the XLHP_HAS_* * flag above. */ + if (vmflags & VISIBILITYMAP_ALL_VISIBLE) + { + xlrec.flags |= XLHP_VM_ALL_VISIBLE; + if (vmflags & VISIBILITYMAP_ALL_FROZEN) + xlrec.flags |= XLHP_VM_ALL_FROZEN; + } if (RelationIsAccessibleInLogicalDecoding(relation)) xlrec.flags |= XLHP_IS_CATALOG_REL; if (TransactionIdIsValid(conflict_xid)) @@ -2168,5 +2716,20 @@ log_heap_prune_and_freeze(Relation relation, Buffer buffer, } recptr = XLogInsert(RM_HEAP2_ID, info); - PageSetLSN(BufferGetPage(buffer), recptr); + if (do_set_vm) + { + Assert(BufferIsDirty(vmbuffer)); + PageSetLSN(BufferGetPage(vmbuffer), recptr); + } + + /* + * If we explicitly skip an FPI, we must not stamp the heap page with this + * record's LSN. Recovery skips records <= the stamped LSN, so this could + * lead to skipping an earlier FPI needed to repair a torn page. + */ + if (heap_fpi_allowed) + { + Assert(BufferIsDirty(buffer)); + PageSetLSN(heap_page, recptr); + } } diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index e6d2b5fced198..5a5398a76ae7d 100644 --- a/src/backend/access/heap/rewriteheap.c +++ b/src/backend/access/heap/rewriteheap.c @@ -92,7 +92,7 @@ * heap's TOAST table will go through the normal bufmgr. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * IDENTIFICATION @@ -122,6 +122,7 @@ #include "storage/procarray.h" #include "utils/memutils.h" #include "utils/rel.h" +#include "utils/wait_event.h" /* * State associated with a rewrite operation. This is opaque to the user @@ -150,7 +151,7 @@ typedef struct RewriteStateData HTAB *rs_old_new_tid_map; /* unmatched B tuples */ HTAB *rs_logical_mappings; /* logical remapping files */ uint32 rs_num_rewrite_mappings; /* # in memory mappings */ -} RewriteStateData; +} RewriteStateData; /* * The lookup keys for the hash tables are tuple TID and xmin (we must check @@ -249,7 +250,7 @@ begin_heap_rewrite(Relation old_heap, Relation new_heap, TransactionId oldest_xm old_cxt = MemoryContextSwitchTo(rw_cxt); /* Create and fill in the state struct */ - state = palloc0(sizeof(RewriteStateData)); + state = palloc0_object(RewriteStateData); state->rs_old_rel = old_heap; state->rs_new_rel = new_heap; @@ -382,6 +383,9 @@ rewrite_heap_tuple(RewriteState state, /* * If the tuple has been updated, check the old-to-new mapping hash table. + * + * Note that this check relies on the HeapTupleSatisfiesVacuum() in + * heapam_relation_copy_for_cluster() to have set hint bits. */ if (!((old_tuple->t_data->t_infomask & HEAP_XMAX_INVALID) || HeapTupleHeaderIsOnlyLocked(old_tuple->t_data)) && @@ -614,12 +618,12 @@ raw_heap_insert(RewriteState state, HeapTuple tup) } else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD) { - int options = HEAP_INSERT_SKIP_FSM; + uint32 options = HEAP_INSERT_SKIP_FSM; /* - * While rewriting the heap for VACUUM FULL / CLUSTER, make sure data - * for the TOAST table are not logically decoded. The main heap is - * WAL-logged as XLOG FPI records, which are not logically decoded. + * While rewriting the heap for REPACK, make sure data for the TOAST + * table are not logically decoded. The main heap is WAL-logged as + * XLOG FPI records, which are not logically decoded. */ options |= HEAP_INSERT_NO_LOGICAL; @@ -673,8 +677,7 @@ raw_heap_insert(RewriteState state, HeapTuple tup) } /* And now we can insert the tuple into the page */ - newoff = PageAddItem(page, (Item) heaptup->t_data, heaptup->t_len, - InvalidOffsetNumber, false, true); + newoff = PageAddItem(page, heaptup->t_data, heaptup->t_len, InvalidOffsetNumber, false, true); if (newoff == InvalidOffsetNumber) elog(ERROR, "failed to add tuple"); @@ -1170,7 +1173,7 @@ CheckPointLogicalRewriteHeap(void) cutoff = ReplicationSlotsComputeLogicalRestartLSN(); /* don't start earlier than the restart lsn */ - if (cutoff != InvalidXLogRecPtr && redo < cutoff) + if (XLogRecPtrIsValid(cutoff) && redo < cutoff) cutoff = redo; mappings_dir = AllocateDir(PG_LOGICAL_MAPPINGS_DIR); @@ -1205,7 +1208,7 @@ CheckPointLogicalRewriteHeap(void) lsn = ((uint64) hi) << 32 | lo; - if (lsn < cutoff || cutoff == InvalidXLogRecPtr) + if (lsn < cutoff || !XLogRecPtrIsValid(cutoff)) { elog(DEBUG1, "removing logical rewrite file \"%s\"", path); if (unlink(path) < 0) diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index f28326bad0951..39395aed0d592 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -118,7 +118,7 @@ * that there only needs to be one call to lazy_vacuum, after the initial pass * completes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -129,8 +129,6 @@ */ #include "postgres.h" -#include - #include "access/genam.h" #include "access/heapam.h" #include "access/htup_details.h" @@ -140,7 +138,6 @@ #include "access/visibilitymap.h" #include "access/xloginsert.h" #include "catalog/storage.h" -#include "commands/dbcommands.h" #include "commands/progress.h" #include "commands/vacuum.h" #include "common/int.h" @@ -152,11 +149,14 @@ #include "postmaster/autovacuum.h" #include "storage/bufmgr.h" #include "storage/freespace.h" +#include "storage/latch.h" #include "storage/lmgr.h" #include "storage/read_stream.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/pg_rusage.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* @@ -249,13 +249,6 @@ typedef enum */ #define EAGER_SCAN_REGION_SIZE 4096 -/* - * heap_vac_scan_next_block() sets these flags to communicate information - * about the block it read to the caller. - */ -#define VAC_BLK_WAS_EAGER_SCANNED (1 << 0) -#define VAC_BLK_ALL_VISIBLE_ACCORDING_TO_VM (1 << 1) - typedef struct LVRelState { /* Target heap relation and its indexes */ @@ -323,18 +316,19 @@ typedef struct LVRelState BlockNumber new_frozen_tuple_pages; /* # pages with newly frozen tuples */ /* # pages newly set all-visible in the VM */ - BlockNumber vm_new_visible_pages; + BlockNumber new_all_visible_pages; /* * # pages newly set all-visible and all-frozen in the VM. This is a - * subset of vm_new_visible_pages. That is, vm_new_visible_pages includes - * all pages set all-visible, but vm_new_visible_frozen_pages includes - * only those which were also set all-frozen. + * subset of new_all_visible_pages. That is, new_all_visible_pages + * includes all pages set all-visible, but + * new_all_visible_all_frozen_pages includes only those which were also + * set all-frozen. */ - BlockNumber vm_new_visible_frozen_pages; + BlockNumber new_all_visible_all_frozen_pages; /* # all-visible pages newly set all-frozen in the VM */ - BlockNumber vm_new_frozen_pages; + BlockNumber new_all_frozen_pages; BlockNumber lpdead_item_pages; /* # pages with LP_DEAD items */ BlockNumber missed_dead_pages; /* # pages with missed dead tuples */ @@ -348,6 +342,15 @@ typedef struct LVRelState /* Instrumentation counters */ int num_index_scans; + int num_dead_items_resets; + Size total_dead_items_bytes; + + /* + * Total number of planned and actually launched parallel workers for + * index vacuuming and index cleanup. + */ + PVWorkerUsage worker_usage; + /* Counters that follow are only for scanned_pages */ int64 tuples_deleted; /* # deleted from table */ int64 tuples_frozen; /* # newly frozen */ @@ -359,7 +362,6 @@ typedef struct LVRelState /* State maintained by heap_vac_scan_next_block() */ BlockNumber current_block; /* last block returned */ BlockNumber next_unskippable_block; /* next unskippable block */ - bool next_unskippable_allvis; /* its visibility status */ bool next_unskippable_eager_scanned; /* if it was eagerly scanned */ Buffer next_unskippable_vmbuffer; /* buffer containing its VM bit */ @@ -423,7 +425,7 @@ typedef struct LVSavedErrInfo /* non-export function prototypes */ static void lazy_scan_heap(LVRelState *vacrel); static void heap_vacuum_eager_scan_setup(LVRelState *vacrel, - VacuumParams *params); + const VacuumParams *params); static BlockNumber heap_vac_scan_next_block(ReadStream *stream, void *callback_private_data, void *per_buffer_data); @@ -431,9 +433,9 @@ static void find_next_unskippable_block(LVRelState *vacrel, bool *skipsallvis); static bool lazy_scan_new_or_empty(LVRelState *vacrel, Buffer buf, BlockNumber blkno, Page page, bool sharelock, Buffer vmbuffer); -static void lazy_scan_prune(LVRelState *vacrel, Buffer buf, +static int lazy_scan_prune(LVRelState *vacrel, Buffer buf, BlockNumber blkno, Page page, - Buffer vmbuffer, bool all_visible_according_to_vm, + Buffer vmbuffer, bool *has_lpdead_items, bool *vm_page_frozen); static bool lazy_scan_noprune(LVRelState *vacrel, Buffer buf, BlockNumber blkno, Page page, @@ -464,8 +466,15 @@ static void dead_items_add(LVRelState *vacrel, BlockNumber blkno, OffsetNumber * int num_offsets); static void dead_items_reset(LVRelState *vacrel); static void dead_items_cleanup(LVRelState *vacrel); -static bool heap_page_is_all_visible(LVRelState *vacrel, Buffer buf, - TransactionId *visibility_cutoff_xid, bool *all_frozen); + +static bool heap_page_would_be_all_visible(Relation rel, Buffer buf, + GlobalVisState *vistest, + bool allow_update_vistest, + OffsetNumber *deadoffsets, + int ndeadoffsets, + bool *all_frozen, + TransactionId *newest_live_xid, + OffsetNumber *logging_offnum); static void update_relstats_all_indexes(LVRelState *vacrel); static void vacuum_error_callback(void *arg); static void update_vacuum_error_info(LVRelState *vacrel, @@ -485,7 +494,7 @@ static void restore_vacuum_error_info(LVRelState *vacrel, * vacuum options or for relfrozenxid/relminmxid advancement. */ static void -heap_vacuum_eager_scan_setup(LVRelState *vacrel, VacuumParams *params) +heap_vacuum_eager_scan_setup(LVRelState *vacrel, const VacuumParams *params) { uint32 randseed; BlockNumber allvisible; @@ -612,7 +621,7 @@ heap_vacuum_eager_scan_setup(LVRelState *vacrel, VacuumParams *params) * and locked the relation. */ void -heap_vacuum_rel(Relation rel, VacuumParams *params, +heap_vacuum_rel(Relation rel, const VacuumParams *params, BufferAccessStrategy bstrategy) { LVRelState *vacrel; @@ -633,10 +642,11 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, BufferUsage startbufferusage = pgBufferUsage; ErrorContextCallback errcallback; char **indnames = NULL; + Size dead_items_max_bytes = 0; verbose = (params->options & VACOPT_VERBOSE) != 0; instrument = (verbose || (AmAutoVacuumWorkerProcess() && - params->log_min_duration >= 0)); + params->log_vacuum_min_duration >= 0)); if (instrument) { pg_rusage_init(&ru0); @@ -652,6 +662,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, RelationGetRelid(rel)); + if (AmAutoVacuumWorkerProcess()) + pgstat_progress_update_param(PROGRESS_VACUUM_STARTED_BY, + params->is_wraparound + ? PROGRESS_VACUUM_STARTED_BY_AUTOVACUUM_WRAPAROUND + : PROGRESS_VACUUM_STARTED_BY_AUTOVACUUM); + else + pgstat_progress_update_param(PROGRESS_VACUUM_STARTED_BY, + PROGRESS_VACUUM_STARTED_BY_MANUAL); /* * Setup error traceback support for ereport() first. The idea is to set @@ -665,7 +683,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * of each rel. It's convenient for code in lazy_scan_heap to always use * these temp copies. */ - vacrel = (LVRelState *) palloc0(sizeof(LVRelState)); + vacrel = palloc0_object(LVRelState); vacrel->dbname = get_database_name(MyDatabaseId); vacrel->relnamespace = get_namespace_name(RelationGetNamespace(rel)); vacrel->relname = pstrdup(RelationGetRelationName(rel)); @@ -685,7 +703,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, if (instrument && vacrel->nindexes > 0) { /* Copy index names used by instrumentation (not error reporting) */ - indnames = palloc(sizeof(char *) * vacrel->nindexes); + indnames = palloc_array(char *, vacrel->nindexes); for (int i = 0; i < vacrel->nindexes; i++) indnames[i] = pstrdup(RelationGetRelationName(vacrel->indrels[i])); } @@ -747,6 +765,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, /* Initialize remaining counters (be tidy) */ vacrel->num_index_scans = 0; + vacrel->num_dead_items_resets = 0; + vacrel->total_dead_items_bytes = 0; vacrel->tuples_deleted = 0; vacrel->tuples_frozen = 0; vacrel->lpdead_items = 0; @@ -754,10 +774,14 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, vacrel->recently_dead_tuples = 0; vacrel->missed_dead_tuples = 0; - vacrel->vm_new_visible_pages = 0; - vacrel->vm_new_visible_frozen_pages = 0; - vacrel->vm_new_frozen_pages = 0; - vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel); + vacrel->new_all_visible_pages = 0; + vacrel->new_all_visible_all_frozen_pages = 0; + vacrel->new_all_frozen_pages = 0; + + vacrel->worker_usage.vacuum.nlaunched = 0; + vacrel->worker_usage.vacuum.nplanned = 0; + vacrel->worker_usage.cleanup.nlaunched = 0; + vacrel->worker_usage.cleanup.nplanned = 0; /* * Get cutoffs that determine which deleted tuples are considered DEAD, @@ -776,7 +800,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * to increase the number of dead tuples it can prune away.) */ vacrel->aggressive = vacuum_get_cutoffs(rel, params, &vacrel->cutoffs); + vacrel->rel_pages = orig_rel_pages = RelationGetNumberOfBlocks(rel); vacrel->vistest = GlobalVisTestFor(rel); + /* Initialize state used to track oldest extant XID/MXID */ vacrel->NewRelfrozenXid = vacrel->cutoffs.OldestXmin; vacrel->NewRelminMxid = vacrel->cutoffs.OldestMxact; @@ -807,6 +833,12 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, */ heap_vacuum_eager_scan_setup(vacrel, params); + /* Report the vacuum mode: 'normal' or 'aggressive' */ + pgstat_progress_update_param(PROGRESS_VACUUM_MODE, + vacrel->aggressive + ? PROGRESS_VACUUM_MODE_AGGRESSIVE + : PROGRESS_VACUUM_MODE_NORMAL); + if (verbose) { if (vacrel->aggressive) @@ -831,12 +863,31 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, lazy_check_wraparound_failsafe(vacrel); dead_items_alloc(vacrel, params->nworkers); +#ifdef USE_INJECTION_POINTS + + /* + * Used by tests to pause before parallel vacuum is launched, allowing + * test code to modify configuration that the leader then propagates to + * workers. + */ + if (AmAutoVacuumWorkerProcess() && ParallelVacuumIsActive(vacrel)) + INJECTION_POINT("autovacuum-start-parallel-vacuum", NULL); +#endif + /* * Call lazy_scan_heap to perform all required heap pruning, index * vacuuming, and heap vacuuming (plus related processing) */ lazy_scan_heap(vacrel); + /* + * Save dead items max_bytes and update the memory usage statistics before + * cleanup, they are freed in parallel vacuum cases during + * dead_items_cleanup(). + */ + dead_items_max_bytes = vacrel->dead_items_info->max_bytes; + vacrel->total_dead_items_bytes += TidStoreMemoryUsage(vacrel->dead_items); + /* * Free resources managed by dead_items_alloc. This ends parallel mode in * passing when necessary. @@ -934,8 +985,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * soon in cases where the failsafe prevented significant amounts of heap * vacuuming. */ - pgstat_report_vacuum(RelationGetRelid(rel), - rel->rd_rel->relisshared, + pgstat_report_vacuum(rel, Max(vacrel->new_live_tuples, 0), vacrel->recently_dead_tuples + vacrel->missed_dead_tuples, @@ -946,9 +996,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, { TimestampTz endtime = GetCurrentTimestamp(); - if (verbose || params->log_min_duration == 0 || + if (verbose || params->log_vacuum_min_duration == 0 || TimestampDifferenceExceeds(starttime, endtime, - params->log_min_duration)) + params->log_vacuum_min_duration)) { long secs_dur; int usecs_dur; @@ -1059,10 +1109,10 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, appendStringInfo(&buf, _("visibility map: %u pages set all-visible, %u pages set all-frozen (%u were all-visible)\n"), - vacrel->vm_new_visible_pages, - vacrel->vm_new_visible_frozen_pages + - vacrel->vm_new_frozen_pages, - vacrel->vm_new_frozen_pages); + vacrel->new_all_visible_pages, + vacrel->new_all_visible_all_frozen_pages + + vacrel->new_all_frozen_pages, + vacrel->new_all_frozen_pages); if (vacrel->do_index_vacuuming) { if (vacrel->nindexes == 0 || vacrel->num_index_scans == 0) @@ -1086,6 +1136,19 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, orig_rel_pages == 0 ? 100.0 : 100.0 * vacrel->lpdead_item_pages / orig_rel_pages, vacrel->lpdead_items); + + if (vacrel->worker_usage.vacuum.nplanned > 0) + appendStringInfo(&buf, + _("parallel workers: index vacuum: %d planned, %d launched in total\n"), + vacrel->worker_usage.vacuum.nplanned, + vacrel->worker_usage.vacuum.nlaunched); + + if (vacrel->worker_usage.cleanup.nplanned > 0) + appendStringInfo(&buf, + _("parallel workers: index cleanup: %d planned, %d launched\n"), + vacrel->worker_usage.cleanup.nplanned, + vacrel->worker_usage.cleanup.nlaunched); + for (int i = 0; i < vacrel->nindexes; i++) { IndexBulkDeleteResult *istat = vacrel->indstats[i]; @@ -1135,11 +1198,28 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, total_blks_read, total_blks_dirtied); appendStringInfo(&buf, - _("WAL usage: %" PRId64 " records, %" PRId64 " full page images, %" PRIu64 " bytes, %" PRId64 " buffers full\n"), + _("WAL usage: %" PRId64 " records, %" PRId64 " full page images, %" PRIu64 " bytes, %" PRIu64 " full page image bytes, %" PRId64 " buffers full\n"), walusage.wal_records, walusage.wal_fpi, walusage.wal_bytes, + walusage.wal_fpi_bytes, walusage.wal_buffers_full); + + /* + * Report the dead items memory usage. + * + * The num_dead_items_resets counter increases when we reset the + * collected dead items, so the counter is non-zero if at least + * one dead items are collected, even if index vacuuming is + * disabled. + */ + appendStringInfo(&buf, + ngettext("memory usage: dead item storage %.2f MB accumulated across %d reset (limit %.2f MB each)\n", + "memory usage: dead item storage %.2f MB accumulated across %d resets (limit %.2f MB each)\n", + vacrel->num_dead_items_resets), + (double) vacrel->total_dead_items_bytes / (1024 * 1024), + vacrel->num_dead_items_resets, + (double) dead_items_max_bytes / (1024 * 1024)); appendStringInfo(&buf, _("system usage: %s"), pg_rusage_show(&ru0)); ereport(verbose ? INFO : LOG, @@ -1221,7 +1301,6 @@ lazy_scan_heap(LVRelState *vacrel) /* Initialize for the first heap_vac_scan_next_block() call */ vacrel->current_block = InvalidBlockNumber; vacrel->next_unskippable_block = InvalidBlockNumber; - vacrel->next_unskippable_allvis = false; vacrel->next_unskippable_eager_scanned = false; vacrel->next_unskippable_vmbuffer = InvalidBuffer; @@ -1237,13 +1316,14 @@ lazy_scan_heap(LVRelState *vacrel) MAIN_FORKNUM, heap_vac_scan_next_block, vacrel, - sizeof(uint8)); + sizeof(bool)); while (true) { Buffer buf; Page page; - uint8 blk_info = 0; + bool was_eager_scanned = false; + int ndeleted = 0; bool has_lpdead_items; void *per_buffer_data = NULL; bool vm_page_frozen = false; @@ -1311,13 +1391,13 @@ lazy_scan_heap(LVRelState *vacrel) if (!BufferIsValid(buf)) break; - blk_info = *((uint8 *) per_buffer_data); + was_eager_scanned = *((bool *) per_buffer_data); CheckBufferIsPinnedOnce(buf); page = BufferGetPage(buf); blkno = BufferGetBlockNumber(buf); vacrel->scanned_pages++; - if (blk_info & VAC_BLK_WAS_EAGER_SCANNED) + if (was_eager_scanned) vacrel->eager_scanned_pages++; /* Report as block scanned, update error traceback information */ @@ -1386,10 +1466,9 @@ lazy_scan_heap(LVRelState *vacrel) * line pointers previously marked LP_DEAD. */ if (got_cleanup_lock) - lazy_scan_prune(vacrel, buf, blkno, page, - vmbuffer, - blk_info & VAC_BLK_ALL_VISIBLE_ACCORDING_TO_VM, - &has_lpdead_items, &vm_page_frozen); + ndeleted = lazy_scan_prune(vacrel, buf, blkno, page, + vmbuffer, + &has_lpdead_items, &vm_page_frozen); /* * Count an eagerly scanned page as a failure or a success. @@ -1405,19 +1484,32 @@ lazy_scan_heap(LVRelState *vacrel) * exclude pages skipped due to cleanup lock contention from eager * freeze algorithm caps. */ - if (got_cleanup_lock && - (blk_info & VAC_BLK_WAS_EAGER_SCANNED)) + if (got_cleanup_lock && was_eager_scanned) { /* Aggressive vacuums do not eager scan. */ Assert(!vacrel->aggressive); if (vm_page_frozen) { - Assert(vacrel->eager_scan_remaining_successes > 0); - vacrel->eager_scan_remaining_successes--; + if (vacrel->eager_scan_remaining_successes > 0) + vacrel->eager_scan_remaining_successes--; if (vacrel->eager_scan_remaining_successes == 0) { + /* + * Report only once that we disabled eager scanning. We + * may eagerly read ahead blocks in excess of the success + * or failure caps before attempting to freeze them, so we + * could reach here even after disabling additional eager + * scanning. + */ + if (vacrel->eager_scan_max_fails_per_region > 0) + ereport(vacrel->verbose ? INFO : DEBUG2, + (errmsg("disabling eager scanning after freezing %u eagerly scanned blocks of relation \"%s.%s.%s\"", + orig_eager_scan_success_limit, + vacrel->dbname, vacrel->relnamespace, + vacrel->relname))); + /* * If we hit our success cap, permanently disable eager * scanning by setting the other eager scan management @@ -1426,19 +1518,10 @@ lazy_scan_heap(LVRelState *vacrel) vacrel->eager_scan_remaining_fails = 0; vacrel->next_eager_scan_region_start = InvalidBlockNumber; vacrel->eager_scan_max_fails_per_region = 0; - - ereport(vacrel->verbose ? INFO : DEBUG2, - (errmsg("disabling eager scanning after freezing %u eagerly scanned blocks of \"%s.%s.%s\"", - orig_eager_scan_success_limit, - vacrel->dbname, vacrel->relnamespace, - vacrel->relname))); } } - else - { - Assert(vacrel->eager_scan_remaining_fails > 0); + else if (vacrel->eager_scan_remaining_fails > 0) vacrel->eager_scan_remaining_fails--; - } } /* @@ -1475,7 +1558,7 @@ lazy_scan_heap(LVRelState *vacrel) * table has indexes. There will only be newly-freed space if we * held the cleanup lock and lazy_scan_prune() was called. */ - if (got_cleanup_lock && vacrel->nindexes == 0 && has_lpdead_items && + if (got_cleanup_lock && vacrel->nindexes == 0 && ndeleted > 0 && blkno - next_fsm_block_to_vacuum >= VACUUM_FSM_EVERY_PAGES) { FreeSpaceMapVacuumRange(vacrel->rel, next_fsm_block_to_vacuum, @@ -1568,7 +1651,6 @@ heap_vac_scan_next_block(ReadStream *stream, { BlockNumber next_block; LVRelState *vacrel = callback_private_data; - uint8 blk_info = 0; /* relies on InvalidBlockNumber + 1 overflowing to 0 on first call */ next_block = vacrel->current_block + 1; @@ -1631,8 +1713,8 @@ heap_vac_scan_next_block(ReadStream *stream, * otherwise they would've been unskippable. */ vacrel->current_block = next_block; - blk_info |= VAC_BLK_ALL_VISIBLE_ACCORDING_TO_VM; - *((uint8 *) per_buffer_data) = blk_info; + /* Block was not eager scanned */ + *((bool *) per_buffer_data) = false; return vacrel->current_block; } else @@ -1644,11 +1726,7 @@ heap_vac_scan_next_block(ReadStream *stream, Assert(next_block == vacrel->next_unskippable_block); vacrel->current_block = next_block; - if (vacrel->next_unskippable_allvis) - blk_info |= VAC_BLK_ALL_VISIBLE_ACCORDING_TO_VM; - if (vacrel->next_unskippable_eager_scanned) - blk_info |= VAC_BLK_WAS_EAGER_SCANNED; - *((uint8 *) per_buffer_data) = blk_info; + *((bool *) per_buffer_data) = vacrel->next_unskippable_eager_scanned; return vacrel->current_block; } } @@ -1673,7 +1751,6 @@ find_next_unskippable_block(LVRelState *vacrel, bool *skipsallvis) BlockNumber next_unskippable_block = vacrel->next_unskippable_block + 1; Buffer next_unskippable_vmbuffer = vacrel->next_unskippable_vmbuffer; bool next_unskippable_eager_scanned = false; - bool next_unskippable_allvis; *skipsallvis = false; @@ -1683,7 +1760,6 @@ find_next_unskippable_block(LVRelState *vacrel, bool *skipsallvis) next_unskippable_block, &next_unskippable_vmbuffer); - next_unskippable_allvis = (mapbits & VISIBILITYMAP_ALL_VISIBLE) != 0; /* * At the start of each eager scan region, normal vacuums with eager @@ -1702,7 +1778,7 @@ find_next_unskippable_block(LVRelState *vacrel, bool *skipsallvis) * A block is unskippable if it is not all visible according to the * visibility map. */ - if (!next_unskippable_allvis) + if ((mapbits & VISIBILITYMAP_ALL_VISIBLE) == 0) { Assert((mapbits & VISIBILITYMAP_ALL_FROZEN) == 0); break; @@ -1759,7 +1835,6 @@ find_next_unskippable_block(LVRelState *vacrel, bool *skipsallvis) /* write the local variables back to vacrel */ vacrel->next_unskippable_block = next_unskippable_block; - vacrel->next_unskippable_allvis = next_unskippable_allvis; vacrel->next_unskippable_eager_scanned = next_unskippable_eager_scanned; vacrel->next_unskippable_vmbuffer = next_unskippable_vmbuffer; } @@ -1866,45 +1941,46 @@ lazy_scan_new_or_empty(LVRelState *vacrel, Buffer buf, BlockNumber blkno, */ if (!PageIsAllVisible(page)) { - uint8 old_vmbits; + /* Lock vmbuffer before entering critical section */ + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); START_CRIT_SECTION(); /* mark buffer dirty before writing a WAL record */ MarkBufferDirty(buf); + PageSetAllVisible(page); + PageClearPrunable(page); + visibilitymap_set(blkno, + vmbuffer, + VISIBILITYMAP_ALL_VISIBLE | + VISIBILITYMAP_ALL_FROZEN, + vacrel->rel->rd_locator); + /* - * It's possible that another backend has extended the heap, - * initialized the page, and then failed to WAL-log the page due - * to an ERROR. Since heap extension is not WAL-logged, recovery - * might try to replay our record setting the page all-visible and - * find that the page isn't initialized, which will cause a PANIC. - * To prevent that, check whether the page has been previously - * WAL-logged, and if not, do that now. + * Emit WAL for setting PD_ALL_VISIBLE on the heap page and + * setting the VM. */ - if (RelationNeedsWAL(vacrel->rel) && - PageGetLSN(page) == InvalidXLogRecPtr) - log_newpage_buffer(buf, true); + if (RelationNeedsWAL(vacrel->rel)) + log_heap_prune_and_freeze(vacrel->rel, buf, + vmbuffer, + VISIBILITYMAP_ALL_VISIBLE | + VISIBILITYMAP_ALL_FROZEN, + InvalidTransactionId, /* conflict xid */ + false, /* cleanup lock */ + PRUNE_VACUUM_SCAN, /* reason */ + NULL, 0, + NULL, 0, + NULL, 0, + NULL, 0); - PageSetAllVisible(page); - old_vmbits = visibilitymap_set(vacrel->rel, blkno, buf, - InvalidXLogRecPtr, - vmbuffer, InvalidTransactionId, - VISIBILITYMAP_ALL_VISIBLE | - VISIBILITYMAP_ALL_FROZEN); END_CRIT_SECTION(); - /* - * If the page wasn't already set all-visible and/or all-frozen in - * the VM, count it as newly set for logging. - */ - if ((old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0) - { - vacrel->vm_new_visible_pages++; - vacrel->vm_new_visible_frozen_pages++; - } - else if ((old_vmbits & VISIBILITYMAP_ALL_FROZEN) == 0) - vacrel->vm_new_frozen_pages++; + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + + /* Count the newly all-frozen pages for logging */ + vacrel->new_all_visible_pages++; + vacrel->new_all_visible_all_frozen_pages++; } freespace = PageGetHeapFreeSpace(page); @@ -1930,9 +2006,7 @@ cmpOffsetNumbers(const void *a, const void *b) * Caller must hold pin and buffer cleanup lock on the buffer. * * vmbuffer is the buffer containing the VM block with visibility information - * for the heap block, blkno. all_visible_according_to_vm is the saved - * visibility status of the heap block looked up earlier by the caller. We - * won't rely entirely on this status, as it may be out of date. + * for the heap block, blkno. * * *has_lpdead_items is set to true or false depending on whether, upon return * from this function, any LP_DEAD items are still present on the page. @@ -1940,20 +2014,29 @@ cmpOffsetNumbers(const void *a, const void *b) * *vm_page_frozen is set to true if the page is newly set all-frozen in the * VM. The caller currently only uses this for determining whether an eagerly * scanned page was successfully set all-frozen. + * + * Returns the number of tuples deleted from the page during HOT pruning. */ -static void +static int lazy_scan_prune(LVRelState *vacrel, Buffer buf, BlockNumber blkno, Page page, Buffer vmbuffer, - bool all_visible_according_to_vm, bool *has_lpdead_items, bool *vm_page_frozen) { Relation rel = vacrel->rel; PruneFreezeResult presult; - int prune_options = 0; + PruneFreezeParams params = { + .relation = rel, + .buffer = buf, + .vmbuffer = vmbuffer, + .reason = PRUNE_VACUUM_SCAN, + .options = HEAP_PAGE_PRUNE_FREEZE | HEAP_PAGE_PRUNE_SET_VM, + .vistest = vacrel->vistest, + .cutoffs = &vacrel->cutoffs, + }; Assert(BufferGetBlockNumber(buf) == blkno); @@ -1972,12 +2055,21 @@ lazy_scan_prune(LVRelState *vacrel, * tuples. Pruning will have determined whether or not the page is * all-visible. */ - prune_options = HEAP_PAGE_PRUNE_FREEZE; if (vacrel->nindexes == 0) - prune_options |= HEAP_PAGE_PRUNE_MARK_UNUSED_NOW; + params.options |= HEAP_PAGE_PRUNE_MARK_UNUSED_NOW; + + /* + * Allow skipping full inspection of pages that the VM indicates are + * already all-frozen (which may be scanned due to SKIP_PAGES_THRESHOLD). + * However, if DISABLE_PAGE_SKIPPING was specified, we can't trust the VM, + * so we must examine the page to make sure it is truly all-frozen and fix + * it otherwise. + */ + if (vacrel->skipwithvm) + params.options |= HEAP_PAGE_PRUNE_ALLOW_FAST_PATH; - heap_page_prune_and_freeze(rel, buf, vacrel->vistest, prune_options, - &vacrel->cutoffs, &presult, PRUNE_VACUUM_SCAN, + heap_page_prune_and_freeze(¶ms, + &presult, &vacrel->offnum, &vacrel->NewRelfrozenXid, &vacrel->NewRelminMxid); @@ -1995,33 +2087,6 @@ lazy_scan_prune(LVRelState *vacrel, vacrel->new_frozen_tuple_pages++; } - /* - * VACUUM will call heap_page_is_all_visible() during the second pass over - * the heap to determine all_visible and all_frozen for the page -- this - * is a specialized version of the logic from this function. Now that - * we've finished pruning and freezing, make sure that we're in total - * agreement with heap_page_is_all_visible() using an assertion. - */ -#ifdef USE_ASSERT_CHECKING - /* Note that all_frozen value does not matter when !all_visible */ - if (presult.all_visible) - { - TransactionId debug_cutoff; - bool debug_all_frozen; - - Assert(presult.lpdead_items == 0); - - if (!heap_page_is_all_visible(vacrel, buf, - &debug_cutoff, &debug_all_frozen)) - Assert(false); - - Assert(presult.all_frozen == debug_all_frozen); - - Assert(!TransactionIdIsValid(debug_cutoff) || - debug_cutoff == presult.vm_conflict_horizon); - } -#endif - /* * Now save details of the LP_DEAD items from the page in vacrel */ @@ -2042,6 +2107,17 @@ lazy_scan_prune(LVRelState *vacrel, } /* Finally, add page-local counts to whole-VACUUM counts */ + if (presult.newly_all_visible) + vacrel->new_all_visible_pages++; + if (presult.newly_all_visible_frozen) + vacrel->new_all_visible_all_frozen_pages++; + if (presult.newly_all_frozen) + vacrel->new_all_frozen_pages++; + + /* Capture if the page was newly set frozen */ + *vm_page_frozen = presult.newly_all_visible_frozen || + presult.newly_all_frozen; + vacrel->tuples_deleted += presult.ndeleted; vacrel->tuples_frozen += presult.nfrozen; vacrel->lpdead_items += presult.lpdead_items; @@ -2055,163 +2131,7 @@ lazy_scan_prune(LVRelState *vacrel, /* Did we find LP_DEAD items? */ *has_lpdead_items = (presult.lpdead_items > 0); - Assert(!presult.all_visible || !(*has_lpdead_items)); - - /* - * Handle setting visibility map bit based on information from the VM (as - * of last heap_vac_scan_next_block() call), and from all_visible and - * all_frozen variables - */ - if (!all_visible_according_to_vm && presult.all_visible) - { - uint8 old_vmbits; - uint8 flags = VISIBILITYMAP_ALL_VISIBLE; - - if (presult.all_frozen) - { - Assert(!TransactionIdIsValid(presult.vm_conflict_horizon)); - flags |= VISIBILITYMAP_ALL_FROZEN; - } - - /* - * It should never be the case that the visibility map page is set - * while the page-level bit is clear, but the reverse is allowed (if - * checksums are not enabled). Regardless, set both bits so that we - * get back in sync. - * - * NB: If the heap page is all-visible but the VM bit is not set, we - * don't need to dirty the heap page. However, if checksums are - * enabled, we do need to make sure that the heap page is dirtied - * before passing it to visibilitymap_set(), because it may be logged. - * Given that this situation should only happen in rare cases after a - * crash, it is not worth optimizing. - */ - PageSetAllVisible(page); - MarkBufferDirty(buf); - old_vmbits = visibilitymap_set(vacrel->rel, blkno, buf, - InvalidXLogRecPtr, - vmbuffer, presult.vm_conflict_horizon, - flags); - - /* - * If the page wasn't already set all-visible and/or all-frozen in the - * VM, count it as newly set for logging. - */ - if ((old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0) - { - vacrel->vm_new_visible_pages++; - if (presult.all_frozen) - { - vacrel->vm_new_visible_frozen_pages++; - *vm_page_frozen = true; - } - } - else if ((old_vmbits & VISIBILITYMAP_ALL_FROZEN) == 0 && - presult.all_frozen) - { - vacrel->vm_new_frozen_pages++; - *vm_page_frozen = true; - } - } - - /* - * As of PostgreSQL 9.2, the visibility map bit should never be set if the - * page-level bit is clear. However, it's possible that the bit got - * cleared after heap_vac_scan_next_block() was called, so we must recheck - * with buffer lock before concluding that the VM is corrupt. - */ - else if (all_visible_according_to_vm && !PageIsAllVisible(page) && - visibilitymap_get_status(vacrel->rel, blkno, &vmbuffer) != 0) - { - elog(WARNING, "page is not marked all-visible but visibility map bit is set in relation \"%s\" page %u", - vacrel->relname, blkno); - visibilitymap_clear(vacrel->rel, blkno, vmbuffer, - VISIBILITYMAP_VALID_BITS); - } - - /* - * It's possible for the value returned by - * GetOldestNonRemovableTransactionId() to move backwards, so it's not - * wrong for us to see tuples that appear to not be visible to everyone - * yet, while PD_ALL_VISIBLE is already set. The real safe xmin value - * never moves backwards, but GetOldestNonRemovableTransactionId() is - * conservative and sometimes returns a value that's unnecessarily small, - * so if we see that contradiction it just means that the tuples that we - * think are not visible to everyone yet actually are, and the - * PD_ALL_VISIBLE flag is correct. - * - * There should never be LP_DEAD items on a page with PD_ALL_VISIBLE set, - * however. - */ - else if (presult.lpdead_items > 0 && PageIsAllVisible(page)) - { - elog(WARNING, "page containing LP_DEAD items is marked as all-visible in relation \"%s\" page %u", - vacrel->relname, blkno); - PageClearAllVisible(page); - MarkBufferDirty(buf); - visibilitymap_clear(vacrel->rel, blkno, vmbuffer, - VISIBILITYMAP_VALID_BITS); - } - - /* - * If the all-visible page is all-frozen but not marked as such yet, mark - * it as all-frozen. Note that all_frozen is only valid if all_visible is - * true, so we must check both all_visible and all_frozen. - */ - else if (all_visible_according_to_vm && presult.all_visible && - presult.all_frozen && !VM_ALL_FROZEN(vacrel->rel, blkno, &vmbuffer)) - { - uint8 old_vmbits; - - /* - * Avoid relying on all_visible_according_to_vm as a proxy for the - * page-level PD_ALL_VISIBLE bit being set, since it might have become - * stale -- even when all_visible is set - */ - if (!PageIsAllVisible(page)) - { - PageSetAllVisible(page); - MarkBufferDirty(buf); - } - - /* - * Set the page all-frozen (and all-visible) in the VM. - * - * We can pass InvalidTransactionId as our cutoff_xid, since a - * snapshotConflictHorizon sufficient to make everything safe for REDO - * was logged when the page's tuples were frozen. - */ - Assert(!TransactionIdIsValid(presult.vm_conflict_horizon)); - old_vmbits = visibilitymap_set(vacrel->rel, blkno, buf, - InvalidXLogRecPtr, - vmbuffer, InvalidTransactionId, - VISIBILITYMAP_ALL_VISIBLE | - VISIBILITYMAP_ALL_FROZEN); - - /* - * The page was likely already set all-visible in the VM. However, - * there is a small chance that it was modified sometime between - * setting all_visible_according_to_vm and checking the visibility - * during pruning. Check the return value of old_vmbits anyway to - * ensure the visibility map counters used for logging are accurate. - */ - if ((old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0) - { - vacrel->vm_new_visible_pages++; - vacrel->vm_new_visible_frozen_pages++; - *vm_page_frozen = true; - } - - /* - * We already checked that the page was not set all-frozen in the VM - * above, so we don't need to test the value of old_vmbits. - */ - else - { - vacrel->vm_new_frozen_pages++; - *vm_page_frozen = true; - } - } + return presult.ndeleted; } /* @@ -2633,7 +2553,8 @@ lazy_vacuum_all_indexes(LVRelState *vacrel) { /* Outsource everything to parallel variant */ parallel_vacuum_bulkdel_all_indexes(vacrel->pvs, old_live_tuples, - vacrel->num_index_scans); + vacrel->num_index_scans, + &(vacrel->worker_usage.vacuum)); /* * Do a postcheck to consider applying wraparound failsafe now. Note @@ -2841,9 +2762,11 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, Page page = BufferGetPage(buffer); OffsetNumber unused[MaxHeapTuplesPerPage]; int nunused = 0; - TransactionId visibility_cutoff_xid; + TransactionId newest_live_xid; + TransactionId conflict_xid = InvalidTransactionId; bool all_frozen; LVSavedErrInfo saved_err_info; + uint8 vmflags = 0; Assert(vacrel->do_index_vacuuming); @@ -2854,6 +2777,35 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, VACUUM_ERRCB_PHASE_VACUUM_HEAP, blkno, InvalidOffsetNumber); + /* + * Before marking dead items unused, check whether the page will become + * all-visible once that change is applied. This lets us reap the tuples + * and mark the page all-visible within the same critical section, + * enabling both changes to be emitted in a single WAL record. Since the + * visibility checks may perform I/O and allocate memory, they must be + * done outside the critical section. + */ + if (heap_page_would_be_all_visible(vacrel->rel, buffer, + vacrel->vistest, true, + deadoffsets, num_offsets, + &all_frozen, &newest_live_xid, + &vacrel->offnum)) + { + vmflags |= VISIBILITYMAP_ALL_VISIBLE; + if (all_frozen) + { + vmflags |= VISIBILITYMAP_ALL_FROZEN; + Assert(!TransactionIdIsValid(newest_live_xid)); + } + + /* + * Take the lock on the vmbuffer before entering a critical section. + * The heap page lock must also be held while updating the VM to + * ensure consistency. + */ + LockBuffer(vmbuffer, BUFFER_LOCK_EXCLUSIVE); + } + START_CRIT_SECTION(); for (int i = 0; i < num_offsets; i++) @@ -2873,6 +2825,20 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, /* Attempt to truncate line pointer array now */ PageTruncateLinePointerArray(page); + if ((vmflags & VISIBILITYMAP_VALID_BITS) != 0) + { + /* + * The page is guaranteed to have had dead line pointers, so we always + * set PD_ALL_VISIBLE. + */ + PageSetAllVisible(page); + PageClearPrunable(page); + visibilitymap_set(blkno, + vmbuffer, vmflags, + vacrel->rel->rd_locator); + conflict_xid = newest_live_xid; + } + /* * Mark buffer dirty before we write WAL. */ @@ -2882,7 +2848,9 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, if (RelationNeedsWAL(vacrel->rel)) { log_heap_prune_and_freeze(vacrel->rel, buffer, - InvalidTransactionId, + vmflags != 0 ? vmbuffer : InvalidBuffer, + vmflags, + conflict_xid, false, /* no cleanup lock required */ PRUNE_VACUUM_CLEANUP, NULL, 0, /* frozen */ @@ -2891,53 +2859,15 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer, unused, nunused); } - /* - * End critical section, so we safely can do visibility tests (which - * possibly need to perform IO and allocate memory!). If we crash now the - * page (including the corresponding vm bit) might not be marked all - * visible, but that's fine. A later vacuum will fix that. - */ END_CRIT_SECTION(); - /* - * Now that we have removed the LP_DEAD items from the page, once again - * check if the page has become all-visible. The page is already marked - * dirty, exclusively locked, and, if needed, a full page image has been - * emitted. - */ - Assert(!PageIsAllVisible(page)); - if (heap_page_is_all_visible(vacrel, buffer, &visibility_cutoff_xid, - &all_frozen)) + if ((vmflags & VISIBILITYMAP_ALL_VISIBLE) != 0) { - uint8 old_vmbits; - uint8 flags = VISIBILITYMAP_ALL_VISIBLE; - + /* Count the newly set VM page for logging */ + LockBuffer(vmbuffer, BUFFER_LOCK_UNLOCK); + vacrel->new_all_visible_pages++; if (all_frozen) - { - Assert(!TransactionIdIsValid(visibility_cutoff_xid)); - flags |= VISIBILITYMAP_ALL_FROZEN; - } - - PageSetAllVisible(page); - old_vmbits = visibilitymap_set(vacrel->rel, blkno, buffer, - InvalidXLogRecPtr, - vmbuffer, visibility_cutoff_xid, - flags); - - /* - * If the page wasn't already set all-visible and/or all-frozen in the - * VM, count it as newly set for logging. - */ - if ((old_vmbits & VISIBILITYMAP_ALL_VISIBLE) == 0) - { - vacrel->vm_new_visible_pages++; - if (all_frozen) - vacrel->vm_new_visible_frozen_pages++; - } - - else if ((old_vmbits & VISIBILITYMAP_ALL_FROZEN) == 0 && - all_frozen) - vacrel->vm_new_frozen_pages++; + vacrel->new_all_visible_all_frozen_pages++; } /* Revert to the previous phase information for error traceback */ @@ -2967,9 +2897,10 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel) { const int progress_index[] = { PROGRESS_VACUUM_INDEXES_TOTAL, - PROGRESS_VACUUM_INDEXES_PROCESSED + PROGRESS_VACUUM_INDEXES_PROCESSED, + PROGRESS_VACUUM_MODE }; - int64 progress_val[2] = {0, 0}; + int64 progress_val[3] = {0, 0, PROGRESS_VACUUM_MODE_FAILSAFE}; VacuumFailsafeActive = true; @@ -2985,8 +2916,8 @@ lazy_check_wraparound_failsafe(LVRelState *vacrel) vacrel->do_index_cleanup = false; vacrel->do_rel_truncate = false; - /* Reset the progress counters */ - pgstat_progress_update_multi_param(2, progress_index, progress_val); + /* Reset the progress counters and set the failsafe mode */ + pgstat_progress_update_multi_param(3, progress_index, progress_val); ereport(WARNING, (errmsg("bypassing nonessential maintenance of table \"%s.%s.%s\" as a failsafe after %d index scans", @@ -3057,7 +2988,8 @@ lazy_cleanup_all_indexes(LVRelState *vacrel) /* Outsource everything to parallel variant */ parallel_vacuum_cleanup_all_indexes(vacrel->pvs, reltuples, vacrel->num_index_scans, - estimated_count); + estimated_count, + &(vacrel->worker_usage.cleanup)); } /* Reset the progress counters */ @@ -3340,6 +3272,9 @@ lazy_truncate_heap(LVRelState *vacrel) static BlockNumber count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected) { + StaticAssertDecl((PREFETCH_SIZE & (PREFETCH_SIZE - 1)) == 0, + "prefetch size must be power of 2"); + BlockNumber blkno; BlockNumber prefetchedUntil; instr_time starttime; @@ -3354,8 +3289,6 @@ count_nondeletable_pages(LVRelState *vacrel, bool *lock_waiter_detected) * in forward direction, so that OS-level readahead can kick in. */ blkno = vacrel->rel_pages; - StaticAssertStmt((PREFETCH_SIZE & (PREFETCH_SIZE - 1)) == 0, - "prefetch size must be power of 2"); prefetchedUntil = InvalidBlockNumber; while (blkno > vacrel->nonempty_pages) { @@ -3533,7 +3466,7 @@ dead_items_alloc(LVRelState *vacrel, int nworkers) * locally. */ - dead_items_info = (VacDeadItemsInfo *) palloc(sizeof(VacDeadItemsInfo)); + dead_items_info = palloc_object(VacDeadItemsInfo); dead_items_info->max_bytes = vac_work_mem * (Size) 1024; dead_items_info->num_items = 0; vacrel->dead_items_info = dead_items_info; @@ -3569,9 +3502,15 @@ dead_items_add(LVRelState *vacrel, BlockNumber blkno, OffsetNumber *offsets, static void dead_items_reset(LVRelState *vacrel) { + /* Update statistics for dead items */ + vacrel->num_dead_items_resets++; + vacrel->total_dead_items_bytes += TidStoreMemoryUsage(vacrel->dead_items); + if (ParallelVacuumIsActive(vacrel)) { parallel_vacuum_reset_dead_items(vacrel->pvs); + vacrel->dead_items = parallel_vacuum_get_dead_items(vacrel->pvs, + &vacrel->dead_items_info); return; } @@ -3600,31 +3539,98 @@ dead_items_cleanup(LVRelState *vacrel) vacrel->pvs = NULL; } +#ifdef USE_ASSERT_CHECKING + /* - * Check if every tuple in the given page is visible to all current and future - * transactions. Also return the visibility_cutoff_xid which is the highest - * xmin amongst the visible tuples. Set *all_frozen to true if every tuple - * on this page is frozen. - * - * This is a stripped down version of lazy_scan_prune(). If you change - * anything here, make sure that everything stays in sync. Note that an - * assertion calls us to verify that everybody still agrees. Be sure to avoid - * introducing new side-effects here. + * Wrapper for heap_page_would_be_all_visible() which can be used for callers + * that expect no LP_DEAD on the page. Currently assert-only, but there is no + * reason not to use it outside of asserts. + */ +bool +heap_page_is_all_visible(Relation rel, Buffer buf, + GlobalVisState *vistest, + bool *all_frozen, + TransactionId *newest_live_xid, + OffsetNumber *logging_offnum) +{ + /* + * Pass allow_update_vistest as false so that the GlobalVisState + * boundaries used here match those used by the pruning code we are + * cross-checking. Allowing an update could move the boundaries between + * the two calls, causing a spurious assertion failure. + */ + return heap_page_would_be_all_visible(rel, buf, + vistest, false, + NULL, 0, + all_frozen, + newest_live_xid, + logging_offnum); +} +#endif + +/* + * Check whether the heap page in buf is all-visible except for the dead + * tuples referenced in the deadoffsets array. + * + * Vacuum uses this to check if a page would become all-visible after reaping + * known dead tuples. This function does not remove the dead items. + * + * This cannot be called in a critical section, as the visibility checks may + * perform IO and allocate memory. + * + * Returns true if the page is all-visible other than the provided + * deadoffsets and false otherwise. + * + * vistest is used to determine visibility. If allow_update_vistest is true, + * the boundaries of the GlobalVisState may be updated when checking the + * visibility of the newest live XID on the page. + * + * Output parameters: + * + * - *all_frozen: true if every tuple on the page is frozen + * - *newest_live_xid: newest xmin of live tuples on the page + * - *logging_offnum: OffsetNumber of current tuple being processed; + * used by vacuum's error callback system. + * + * Callers looking to verify that the page is already all-visible can call + * heap_page_is_all_visible(). + * + * This logic is closely related to heap_prune_record_unchanged_lp_normal(). + * If you modify this function, ensure consistency with that code. An + * assertion cross-checks that both remain in agreement. Do not introduce new + * side-effects. */ static bool -heap_page_is_all_visible(LVRelState *vacrel, Buffer buf, - TransactionId *visibility_cutoff_xid, - bool *all_frozen) +heap_page_would_be_all_visible(Relation rel, Buffer buf, + GlobalVisState *vistest, + bool allow_update_vistest, + OffsetNumber *deadoffsets, + int ndeadoffsets, + bool *all_frozen, + TransactionId *newest_live_xid, + OffsetNumber *logging_offnum) { Page page = BufferGetPage(buf); BlockNumber blockno = BufferGetBlockNumber(buf); OffsetNumber offnum, maxoff; bool all_visible = true; + int matched_dead_count = 0; - *visibility_cutoff_xid = InvalidTransactionId; + *newest_live_xid = InvalidTransactionId; *all_frozen = true; + Assert(ndeadoffsets == 0 || deadoffsets); + +#ifdef USE_ASSERT_CHECKING + /* Confirm input deadoffsets[] is strictly sorted */ + if (ndeadoffsets > 1) + { + for (int i = 1; i < ndeadoffsets; i++) + Assert(deadoffsets[i - 1] < deadoffsets[i]); + } +#endif + maxoff = PageGetMaxOffsetNumber(page); for (offnum = FirstOffsetNumber; offnum <= maxoff && all_visible; @@ -3632,12 +3638,13 @@ heap_page_is_all_visible(LVRelState *vacrel, Buffer buf, { ItemId itemid; HeapTupleData tuple; + TransactionId dead_after; /* * Set the offset number so that we can display it along with any * error that occurred while processing this tuple. */ - vacrel->offnum = offnum; + *logging_offnum = offnum; itemid = PageGetItemId(page, offnum); /* Unused or redirect line pointers are of no interest */ @@ -3652,25 +3659,32 @@ heap_page_is_all_visible(LVRelState *vacrel, Buffer buf, */ if (ItemIdIsDead(itemid)) { - all_visible = false; - *all_frozen = false; - break; + if (!deadoffsets || + matched_dead_count >= ndeadoffsets || + deadoffsets[matched_dead_count] != offnum) + { + *all_frozen = all_visible = false; + break; + } + matched_dead_count++; + continue; } Assert(ItemIdIsNormal(itemid)); tuple.t_data = (HeapTupleHeader) PageGetItem(page, itemid); tuple.t_len = ItemIdGetLength(itemid); - tuple.t_tableOid = RelationGetRelid(vacrel->rel); + tuple.t_tableOid = RelationGetRelid(rel); - switch (HeapTupleSatisfiesVacuum(&tuple, vacrel->cutoffs.OldestXmin, - buf)) + /* Visibility checks may do IO or allocate memory */ + Assert(CritSectionCount == 0); + switch (HeapTupleSatisfiesVacuumHorizon(&tuple, buf, &dead_after)) { case HEAPTUPLE_LIVE: { TransactionId xmin; - /* Check comments in lazy_scan_prune. */ + /* Check heap_prune_record_unchanged_lp_normal comments */ if (!HeapTupleHeaderXminCommitted(tuple.t_data)) { all_visible = false; @@ -3679,22 +3693,22 @@ heap_page_is_all_visible(LVRelState *vacrel, Buffer buf, } /* - * The inserter definitely committed. But is it old enough - * that everyone sees it as committed? + * The inserter definitely committed. But we don't know if + * it is old enough that everyone sees it as committed. + * Don't check that now. + * + * If we scan all tuples without finding one that prevents + * the page from being all-visible, we then check whether + * any snapshot still considers the newest XID on the page + * to be running. In that case, the page is not considered + * all-visible. */ xmin = HeapTupleHeaderGetXmin(tuple.t_data); - if (!TransactionIdPrecedes(xmin, - vacrel->cutoffs.OldestXmin)) - { - all_visible = false; - *all_frozen = false; - break; - } /* Track newest xmin on page. */ - if (TransactionIdFollows(xmin, *visibility_cutoff_xid) && + if (TransactionIdFollows(xmin, *newest_live_xid) && TransactionIdIsNormal(xmin)) - *visibility_cutoff_xid = xmin; + *newest_live_xid = xmin; /* Check whether this tuple is already frozen or not */ if (all_visible && *all_frozen && @@ -3718,8 +3732,22 @@ heap_page_is_all_visible(LVRelState *vacrel, Buffer buf, } } /* scan along page */ + /* + * After processing all the live tuples on the page, if the newest xmin + * among them may still be considered running by any snapshot, the page + * cannot be all-visible. + */ + if (all_visible && + TransactionIdIsNormal(*newest_live_xid) && + GlobalVisTestXidConsideredRunning(vistest, *newest_live_xid, + allow_update_vistest)) + { + all_visible = false; + *all_frozen = false; + } + /* Clear the offset information once we have processed the given page. */ - vacrel->offnum = InvalidOffsetNumber; + *logging_offnum = InvalidOffsetNumber; return all_visible; } diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c index 745a04ef26e29..4fd470702aae7 100644 --- a/src/backend/access/heap/visibilitymap.c +++ b/src/backend/access/heap/visibilitymap.c @@ -3,7 +3,7 @@ * visibilitymap.c * bitmap for tracking visibility of heap tuples * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,7 +14,7 @@ * visibilitymap_clear - clear bits for one page in the visibility map * visibilitymap_pin - pin a map page for setting a bit * visibilitymap_pin_ok - check whether correct map page is already pinned - * visibilitymap_set - set a bit in a previously pinned page + * visibilitymap_set - set bit(s) in a previously pinned page * visibilitymap_get_status - get status of bits * visibilitymap_count - count number of bits set in visibility map * visibilitymap_prepare_truncate - @@ -34,21 +34,32 @@ * is set, we know the condition is true, but if a bit is not set, it might or * might not be true. * - * Clearing visibility map bits is not separately WAL-logged. The callers - * must make sure that whenever a bit is cleared, the bit is cleared on WAL - * replay of the updating operation as well. - * - * When we *set* a visibility map during VACUUM, we must write WAL. This may - * seem counterintuitive, since the bit is basically a hint: if it is clear, - * it may still be the case that every tuple on the page is visible to all - * transactions; we just don't know that for certain. The difficulty is that - * there are two bits which are typically set together: the PD_ALL_VISIBLE bit - * on the page itself, and the visibility map bit. If a crash occurs after the - * visibility map page makes it to disk and before the updated heap page makes - * it to disk, redo must set the bit on the heap page. Otherwise, the next - * insert, update, or delete on the heap page will fail to realize that the - * visibility map bit must be cleared, possibly causing index-only scans to - * return wrong answers. + * Changes to the visibility map bits are not separately WAL-logged. Callers + * must make sure that whenever a visibility map bit is cleared, the bit is + * cleared on WAL replay of the updating operation. And whenever a visibility + * map bit is set, the bit is set on WAL replay of the operation that rendered + * the page all-visible/all-frozen. + * + * The visibility map bits operate as a hint in one direction: if they are + * clear, it may still be the case that every tuple on the page is visible to + * all transactions (we just don't know that for certain). However, if they + * are set, we may skip vacuuming pages and advance relfrozenxid or skip + * reading heap pages for an index-only scan. If they are incorrectly set, + * this can lead to data corruption and wrong results. + * + * Additionally, it is critical that the heap-page level PD_ALL_VISIBLE bit be + * correctly set and cleared along with the VM bits. + * + * When clearing the VM, if a crash occurs after the heap page makes it to + * disk but before the VM page makes it to disk, replay must clear the VM or + * the next index-only scan can return wrong results or vacuum may incorrectly + * advance relfrozenxid. + * + * When setting the VM, if a crash occurs after the visibility map page makes + * it to disk and before the updated heap page makes it to disk, redo must set + * the bit on the heap page. Otherwise, the next insert, update, or delete on + * the heap page will fail to realize that the visibility map bit must be + * cleared, possibly causing index-only scans to return wrong answers. * * VACUUM will normally skip pages for which the visibility map bit is set; * such pages can't contain any dead tuples and therefore don't need vacuuming. @@ -115,6 +126,8 @@ /* Mapping from heap block number to the right bit in the visibility map */ #define HEAPBLK_TO_MAPBLOCK(x) ((x) / HEAPBLOCKS_PER_PAGE) +#define HEAPBLK_TO_MAPBLOCK_LIMIT(x) \ + (((x) + HEAPBLOCKS_PER_PAGE - 1) / HEAPBLOCKS_PER_PAGE) #define HEAPBLK_TO_MAPBYTE(x) (((x) % HEAPBLOCKS_PER_PAGE) / HEAPBLOCKS_PER_BYTE) #define HEAPBLK_TO_OFFSET(x) (((x) % HEAPBLOCKS_PER_BYTE) * BITS_PER_HEAPBLOCK) @@ -220,32 +233,28 @@ visibilitymap_pin_ok(BlockNumber heapBlk, Buffer vmbuf) } /* - * visibilitymap_set - set bit(s) on a previously pinned page - * - * recptr is the LSN of the XLOG record we're replaying, if we're in recovery, - * or InvalidXLogRecPtr in normal running. The VM page LSN is advanced to the - * one provided; in normal running, we generate a new XLOG record and set the - * page LSN to that value (though the heap page's LSN may *not* be updated; - * see below). cutoff_xid is the largest xmin on the page being marked - * all-visible; it is needed for Hot Standby, and can be InvalidTransactionId - * if the page contains no tuples. It can also be set to InvalidTransactionId - * when a page that is already all-visible is being marked all-frozen. - * - * Caller is expected to set the heap page's PD_ALL_VISIBLE bit before calling - * this function. Except in recovery, caller should also pass the heap - * buffer. When checksums are enabled and we're not in recovery, we must add - * the heap buffer to the WAL chain to protect it from being torn. + * Set VM (visibility map) flags in the VM block in vmBuf. * - * You must pass a buffer containing the correct map page to this function. - * Call visibilitymap_pin first to pin the right one. This function doesn't do - * any I/O. + * This function is intended for callers that log VM changes together + * with the heap page modifications that rendered the page all-visible. + * + * vmBuf must be pinned and exclusively locked, and it must cover the VM bits + * corresponding to heapBlk. + * + * In normal operation (not recovery), this must be called inside a critical + * section that also applies the necessary heap page changes and, if + * applicable, emits WAL. * - * Returns the state of the page's VM bits before setting flags. + * The caller is responsible for ensuring consistency between the heap page + * and the VM page by holding a pin and exclusive lock on the buffer + * containing heapBlk. + * + * rlocator is used only for debugging messages. */ -uint8 -visibilitymap_set(Relation rel, BlockNumber heapBlk, Buffer heapBuf, - XLogRecPtr recptr, Buffer vmBuf, TransactionId cutoff_xid, - uint8 flags) +void +visibilitymap_set(BlockNumber heapBlk, + Buffer vmBuf, uint8 flags, + const RelFileLocator rlocator) { BlockNumber mapBlock = HEAPBLK_TO_MAPBLOCK(heapBlk); uint32 mapByte = HEAPBLK_TO_MAPBYTE(heapBlk); @@ -255,67 +264,36 @@ visibilitymap_set(Relation rel, BlockNumber heapBlk, Buffer heapBuf, uint8 status; #ifdef TRACE_VISIBILITYMAP - elog(DEBUG1, "vm_set %s %d", RelationGetRelationName(rel), heapBlk); + elog(DEBUG1, "vm_set flags 0x%02X for %s %d", + flags, + relpathbackend(rlocator, MyProcNumber, MAIN_FORKNUM).str, + heapBlk); #endif - Assert(InRecovery || XLogRecPtrIsInvalid(recptr)); - Assert(InRecovery || PageIsAllVisible((Page) BufferGetPage(heapBuf))); + /* Call in same critical section where WAL is emitted. */ + Assert(InRecovery || CritSectionCount > 0); + + /* Flags should be valid. Also never clear bits with this function */ Assert((flags & VISIBILITYMAP_VALID_BITS) == flags); /* Must never set all_frozen bit without also setting all_visible bit */ Assert(flags != VISIBILITYMAP_ALL_FROZEN); - /* Check that we have the right heap page pinned, if present */ - if (BufferIsValid(heapBuf) && BufferGetBlockNumber(heapBuf) != heapBlk) - elog(ERROR, "wrong heap buffer passed to visibilitymap_set"); - /* Check that we have the right VM page pinned */ if (!BufferIsValid(vmBuf) || BufferGetBlockNumber(vmBuf) != mapBlock) elog(ERROR, "wrong VM buffer passed to visibilitymap_set"); + Assert(BufferIsLockedByMeInMode(vmBuf, BUFFER_LOCK_EXCLUSIVE)); + page = BufferGetPage(vmBuf); map = (uint8 *) PageGetContents(page); - LockBuffer(vmBuf, BUFFER_LOCK_EXCLUSIVE); status = (map[mapByte] >> mapOffset) & VISIBILITYMAP_VALID_BITS; if (flags != status) { - START_CRIT_SECTION(); - map[mapByte] |= (flags << mapOffset); MarkBufferDirty(vmBuf); - - if (RelationNeedsWAL(rel)) - { - if (XLogRecPtrIsInvalid(recptr)) - { - Assert(!InRecovery); - recptr = log_heap_visible(rel, heapBuf, vmBuf, cutoff_xid, flags); - - /* - * If data checksums are enabled (or wal_log_hints=on), we - * need to protect the heap page from being torn. - * - * If not, then we must *not* update the heap page's LSN. In - * this case, the FPI for the heap page was omitted from the - * WAL record inserted above, so it would be incorrect to - * update the heap page's LSN. - */ - if (XLogHintBitIsNeeded()) - { - Page heapPage = BufferGetPage(heapBuf); - - PageSetLSN(heapPage, recptr); - } - } - PageSetLSN(page, recptr); - } - - END_CRIT_SECTION(); } - - LockBuffer(vmBuf, BUFFER_LOCK_UNLOCK); - return status; } /* @@ -364,7 +342,7 @@ visibilitymap_get_status(Relation rel, BlockNumber heapBlk, Buffer *vmbuf) { *vmbuf = vm_readbuf(rel, mapBlock, false); if (!BufferIsValid(*vmbuf)) - return false; + return (uint8) 0; } map = PageGetContents(BufferGetPage(*vmbuf)); @@ -533,6 +511,21 @@ visibilitymap_prepare_truncate(Relation rel, BlockNumber nheapblocks) return newnblocks; } +/* + * visibilitymap_truncation_length - + * compute truncation length for visibility map + * + * Given a proposed truncation length for the main fork, compute the + * correct truncation length for the visibility map. Should return the + * same answer as visibilitymap_prepare_truncate(), but without modifying + * anything. + */ +BlockNumber +visibilitymap_truncation_length(BlockNumber nheapblocks) +{ + return HEAPBLK_TO_MAPBLOCK_LIMIT(nheapblocks); +} + /* * Read a visibility map page. * diff --git a/src/backend/access/index/amapi.c b/src/backend/access/index/amapi.c index f0f4f974bcedb..efa007030ae9e 100644 --- a/src/backend/access/index/amapi.c +++ b/src/backend/access/index/amapi.c @@ -3,7 +3,7 @@ * amapi.c * Support routines for API for Postgres index access methods. * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -23,25 +23,38 @@ /* * GetIndexAmRoutine - call the specified access method handler routine to get - * its IndexAmRoutine struct, which will be palloc'd in the caller's context. + * its IndexAmRoutine struct, which we expect to be statically allocated. * * Note that if the amhandler function is built-in, this will not involve * any catalog access. It's therefore safe to use this while bootstrapping * indexes for the system catalogs. relcache.c relies on that. */ -IndexAmRoutine * +const IndexAmRoutine * GetIndexAmRoutine(Oid amhandler) { Datum datum; - IndexAmRoutine *routine; + const IndexAmRoutine *routine; datum = OidFunctionCall0(amhandler); - routine = (IndexAmRoutine *) DatumGetPointer(datum); + routine = (const IndexAmRoutine *) DatumGetPointer(datum); if (routine == NULL || !IsA(routine, IndexAmRoutine)) elog(ERROR, "index access method handler function %u did not return an IndexAmRoutine struct", amhandler); + /* Assert that all required callbacks are present. */ + Assert(routine->ambuild != NULL); + Assert(routine->ambuildempty != NULL); + Assert(routine->aminsert != NULL); + Assert(routine->ambulkdelete != NULL); + Assert(routine->amvacuumcleanup != NULL); + Assert(routine->amcostestimate != NULL); + Assert(routine->amoptions != NULL); + Assert(routine->amvalidate != NULL); + Assert(routine->ambeginscan != NULL); + Assert(routine->amrescan != NULL); + Assert(routine->amendscan != NULL); + return routine; } @@ -52,7 +65,7 @@ GetIndexAmRoutine(Oid amhandler) * If the given OID isn't a valid index access method, returns NULL if * noerror is true, else throws error. */ -IndexAmRoutine * +const IndexAmRoutine * GetIndexAmRoutineByAmId(Oid amoid, bool noerror) { HeapTuple tuple; @@ -118,7 +131,7 @@ CompareType IndexAmTranslateStrategy(StrategyNumber strategy, Oid amoid, Oid opfamily, bool missing_ok) { CompareType result; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; /* shortcut for common case */ if (amoid == BTREE_AM_OID && @@ -148,7 +161,7 @@ StrategyNumber IndexAmTranslateCompareType(CompareType cmptype, Oid amoid, Oid opfamily, bool missing_ok) { StrategyNumber result; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; /* shortcut for common case */ if (amoid == BTREE_AM_OID && @@ -178,7 +191,7 @@ amvalidate(PG_FUNCTION_ARGS) HeapTuple classtup; Form_pg_opclass classform; Oid amoid; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; classtup = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclassoid)); if (!HeapTupleIsValid(classtup)) @@ -197,7 +210,5 @@ amvalidate(PG_FUNCTION_ARGS) result = amroutine->amvalidate(opclassoid); - pfree(amroutine); - PG_RETURN_BOOL(result); } diff --git a/src/backend/access/index/amvalidate.c b/src/backend/access/index/amvalidate.c index 4cf237019adaf..f86ba7d76d297 100644 --- a/src/backend/access/index/amvalidate.c +++ b/src/backend/access/index/amvalidate.c @@ -4,7 +4,7 @@ * Support routines for index access methods' amvalidate and * amadjustmembers functions. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -118,7 +118,7 @@ identify_opfamily_groups(CatCList *oprlist, CatCList *proclist) } /* Time for a new group */ - thisgroup = (OpFamilyOpFuncGroup *) palloc(sizeof(OpFamilyOpFuncGroup)); + thisgroup = palloc_object(OpFamilyOpFuncGroup); if (oprform && (!procform || (oprform->amoplefttype < procform->amproclefttype || diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c index 0cb27af131095..97d44b8462296 100644 --- a/src/backend/access/index/genam.c +++ b/src/backend/access/index/genam.c @@ -3,7 +3,7 @@ * genam.c * general index access method routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -81,7 +81,7 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys) { IndexScanDesc scan; - scan = (IndexScanDesc) palloc(sizeof(IndexScanDescData)); + scan = palloc_object(IndexScanDescData); scan->heapRelation = NULL; /* may be set later */ scan->xs_heapfetch = NULL; @@ -94,11 +94,11 @@ RelationGetIndexScan(Relation indexRelation, int nkeys, int norderbys) * We allocate key workspace here, but it won't get filled until amrescan. */ if (nkeys > 0) - scan->keyData = (ScanKey) palloc(sizeof(ScanKeyData) * nkeys); + scan->keyData = palloc_array(ScanKeyData, nkeys); else scan->keyData = NULL; if (norderbys > 0) - scan->orderByData = (ScanKey) palloc(sizeof(ScanKeyData) * norderbys); + scan->orderByData = palloc_array(ScanKeyData, norderbys); else scan->orderByData = NULL; @@ -310,8 +310,8 @@ index_compute_xid_horizon_for_tuples(Relation irel, delstate.bottomup = false; delstate.bottomupfreespace = 0; delstate.ndeltids = 0; - delstate.deltids = palloc(nitems * sizeof(TM_IndexDelete)); - delstate.status = palloc(nitems * sizeof(TM_IndexStatus)); + delstate.deltids = palloc_array(TM_IndexDelete, nitems); + delstate.status = palloc_array(TM_IndexStatus, nitems); /* identify what the index tuples about to be deleted point to */ for (int i = 0; i < nitems; i++) @@ -394,6 +394,14 @@ systable_beginscan(Relation heapRelation, SysScanDesc sysscan; Relation irel; + /* + * If this backend promised that it won't access shared catalogs during + * logical decoding, this it the right place to verify. + */ + Assert(!HistoricSnapshotActive() || + accessSharedCatalogsInDecoding || + !heapRelation->rd_rel->relisshared); + if (indexOK && !IgnoreSystemIndexes && !ReindexIsProcessingIndex(indexId)) @@ -401,7 +409,7 @@ systable_beginscan(Relation heapRelation, else irel = NULL; - sysscan = (SysScanDesc) palloc(sizeof(SysScanDescData)); + sysscan = palloc_object(SysScanDescData); sysscan->heap_rel = heapRelation; sysscan->irel = irel; @@ -420,6 +428,14 @@ systable_beginscan(Relation heapRelation, sysscan->snapshot = NULL; } + /* + * If CheckXidAlive is set then set a flag to indicate that system table + * scan is in-progress. See detailed comments in xact.c where these + * variables are declared. + */ + if (TransactionIdIsValid(CheckXidAlive)) + bsysscan = true; + if (irel) { int i; @@ -447,7 +463,8 @@ systable_beginscan(Relation heapRelation, } sysscan->iscan = index_beginscan(heapRelation, irel, - snapshot, NULL, nkeys, 0); + snapshot, NULL, nkeys, 0, + SO_NONE); index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0); sysscan->scan = NULL; @@ -468,14 +485,6 @@ systable_beginscan(Relation heapRelation, sysscan->iscan = NULL; } - /* - * If CheckXidAlive is set then set a flag to indicate that system table - * scan is in-progress. See detailed comments in xact.c where these - * variables are declared. - */ - if (TransactionIdIsValid(CheckXidAlive)) - bsysscan = true; - return sysscan; } @@ -488,7 +497,7 @@ systable_beginscan(Relation heapRelation, * is declared. */ static inline void -HandleConcurrentAbort() +HandleConcurrentAbort(void) { if (TransactionIdIsValid(CheckXidAlive) && !TransactionIdIsInProgress(CheckXidAlive) && @@ -667,7 +676,7 @@ systable_beginscan_ordered(Relation heapRelation, elog(WARNING, "using index \"%s\" despite IgnoreSystemIndexes", RelationGetRelationName(indexRelation)); - sysscan = (SysScanDesc) palloc(sizeof(SysScanDescData)); + sysscan = palloc_object(SysScanDescData); sysscan->heap_rel = heapRelation; sysscan->irel = indexRelation; @@ -707,13 +716,6 @@ systable_beginscan_ordered(Relation heapRelation, elog(ERROR, "column is not in index"); } - sysscan->iscan = index_beginscan(heapRelation, indexRelation, - snapshot, NULL, nkeys, 0); - index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0); - sysscan->scan = NULL; - - pfree(idxkey); - /* * If CheckXidAlive is set then set a flag to indicate that system table * scan is in-progress. See detailed comments in xact.c where these @@ -722,6 +724,14 @@ systable_beginscan_ordered(Relation heapRelation, if (TransactionIdIsValid(CheckXidAlive)) bsysscan = true; + sysscan->iscan = index_beginscan(heapRelation, indexRelation, + snapshot, NULL, nkeys, 0, + SO_NONE); + index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0); + sysscan->scan = NULL; + + pfree(idxkey); + return sysscan; } @@ -781,10 +791,11 @@ systable_endscan_ordered(SysScanDesc sysscan) * systable_inplace_update_begin --- update a row "in place" (overwrite it) * * Overwriting violates both MVCC and transactional safety, so the uses of - * this function in Postgres are extremely limited. Nonetheless we find some - * places to use it. See README.tuplock section "Locking to write - * inplace-updated tables" and later sections for expectations of readers and - * writers of a table that gets inplace updates. Standard flow: + * this function in Postgres are extremely limited. This makes no effort to + * support updating cache key columns or other indexed columns. Nonetheless + * we find some places to use it. See README.tuplock section "Locking to + * write inplace-updated tables" and later sections for expectations of + * readers and writers of a table that gets inplace updates. Standard flow: * * ... [any slow preparation not requiring oldtup] ... * systable_inplace_update_begin([...], &tup, &inplace_state); diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c index 219df1971da66..7967e93984786 100644 --- a/src/backend/access/index/indexam.c +++ b/src/backend/access/index/indexam.c @@ -3,7 +3,7 @@ * indexam.c * general index access method routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -53,6 +53,7 @@ #include "nodes/execnodes.h" #include "pgstat.h" #include "storage/lmgr.h" +#include "storage/lock.h" #include "storage/predicate.h" #include "utils/ruleutils.h" #include "utils/snapmgr.h" @@ -75,7 +76,7 @@ #define RELATION_CHECKS \ do { \ Assert(RelationIsValid(indexRelation)); \ - Assert(PointerIsValid(indexRelation->rd_indam)); \ + Assert(indexRelation->rd_indam); \ if (unlikely(ReindexIsProcessingIndex(RelationGetRelid(indexRelation)))) \ ereport(ERROR, \ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), \ @@ -85,9 +86,9 @@ do { \ #define SCAN_CHECKS \ ( \ - AssertMacro(IndexScanIsValid(scan)), \ + AssertMacro(scan), \ AssertMacro(RelationIsValid(scan->indexRelation)), \ - AssertMacro(PointerIsValid(scan->indexRelation->rd_indam)) \ + AssertMacro(scan->indexRelation->rd_indam) \ ) #define CHECK_REL_PROCEDURE(pname) \ @@ -107,7 +108,7 @@ do { \ static IndexScanDesc index_beginscan_internal(Relation indexRelation, int nkeys, int norderbys, Snapshot snapshot, ParallelIndexScanDesc pscan, bool temp_snap); -static inline void validate_relation_kind(Relation r); +static inline void validate_relation_as_index(Relation r); /* ---------------------------------------------------------------- @@ -136,7 +137,7 @@ index_open(Oid relationId, LOCKMODE lockmode) r = relation_open(relationId, lockmode); - validate_relation_kind(r); + validate_relation_as_index(r); return r; } @@ -159,7 +160,7 @@ try_index_open(Oid relationId, LOCKMODE lockmode) if (!r) return NULL; - validate_relation_kind(r); + validate_relation_as_index(r); return r; } @@ -188,13 +189,13 @@ index_close(Relation relation, LOCKMODE lockmode) } /* ---------------- - * validate_relation_kind - check the relation's kind + * validate_relation_as_index * * Make sure relkind is an index or a partitioned index. * ---------------- */ static inline void -validate_relation_kind(Relation r) +validate_relation_as_index(Relation r) { if (r->rd_rel->relkind != RELKIND_INDEX && r->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) @@ -257,12 +258,23 @@ index_beginscan(Relation heapRelation, Relation indexRelation, Snapshot snapshot, IndexScanInstrumentation *instrument, - int nkeys, int norderbys) + int nkeys, int norderbys, + uint32 flags) { IndexScanDesc scan; Assert(snapshot != InvalidSnapshot); + /* Check that a historic snapshot is not used for non-catalog tables */ + if (IsHistoricMVCCSnapshot(snapshot) && + !RelationIsAccessibleInLogicalDecoding(heapRelation)) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_TRANSACTION_STATE), + errmsg("cannot query non-catalog table \"%s\" during logical decoding", + RelationGetRelationName(heapRelation)))); + } + scan = index_beginscan_internal(indexRelation, nkeys, norderbys, snapshot, NULL, false); /* @@ -274,7 +286,7 @@ index_beginscan(Relation heapRelation, scan->instrument = instrument; /* prepare to fetch index matches from table */ - scan->xs_heapfetch = table_index_fetch_begin(heapRelation); + scan->xs_heapfetch = table_index_fetch_begin(heapRelation, flags); return scan; } @@ -363,7 +375,7 @@ index_rescan(IndexScanDesc scan, Assert(nkeys == scan->numberOfKeys); Assert(norderbys == scan->numberOfOrderBys); - /* Release resources (like buffer pins) from table accesses */ + /* reset table AM state for rescan */ if (scan->xs_heapfetch) table_index_fetch_reset(scan->xs_heapfetch); @@ -435,12 +447,12 @@ index_markpos(IndexScanDesc scan) void index_restrpos(IndexScanDesc scan) { - Assert(IsMVCCSnapshot(scan->xs_snapshot)); + Assert(IsMVCCLikeSnapshot(scan->xs_snapshot)); SCAN_CHECKS; CHECK_SCAN_PROCEDURE(amrestrpos); - /* release resources (like buffer pins) from table accesses */ + /* reset table AM state for restoring the marked position */ if (scan->xs_heapfetch) table_index_fetch_reset(scan->xs_heapfetch); @@ -451,43 +463,26 @@ index_restrpos(IndexScanDesc scan) } /* - * index_parallelscan_estimate - estimate shared memory for parallel scan - * - * When instrument=true, estimate includes SharedIndexScanInstrumentation - * space. When parallel_aware=true, estimate includes whatever space the - * index AM's amestimateparallelscan routine requested when called. + * Estimates the shared memory needed for parallel scan, including any + * AM-specific parallel scan state. */ Size index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys, - Snapshot snapshot, bool instrument, - bool parallel_aware, int nworkers) + Snapshot snapshot) { Size nbytes; - Assert(instrument || parallel_aware); - RELATION_CHECKS; nbytes = offsetof(ParallelIndexScanDescData, ps_snapshot_data); nbytes = add_size(nbytes, EstimateSnapshotSpace(snapshot)); nbytes = MAXALIGN(nbytes); - if (instrument) - { - Size sharedinfosz; - - sharedinfosz = offsetof(SharedIndexScanInstrumentation, winstrument) + - nworkers * sizeof(IndexScanInstrumentation); - nbytes = add_size(nbytes, sharedinfosz); - nbytes = MAXALIGN(nbytes); - } - /* * If parallel scan index AM interface can't be used (or index AM provides * no such interface), assume there is no AM-specific data needed */ - if (parallel_aware && - indexRelation->rd_indam->amestimateparallelscan != NULL) + if (indexRelation->rd_indam->amestimateparallelscan != NULL) nbytes = add_size(nbytes, indexRelation->rd_indam->amestimateparallelscan(indexRelation, nkeys, @@ -508,15 +503,11 @@ index_parallelscan_estimate(Relation indexRelation, int nkeys, int norderbys, */ void index_parallelscan_initialize(Relation heapRelation, Relation indexRelation, - Snapshot snapshot, bool instrument, - bool parallel_aware, int nworkers, - SharedIndexScanInstrumentation **sharedinfo, + Snapshot snapshot, ParallelIndexScanDesc target) { Size offset; - Assert(instrument || parallel_aware); - RELATION_CHECKS; offset = add_size(offsetof(ParallelIndexScanDescData, ps_snapshot_data), @@ -525,29 +516,11 @@ index_parallelscan_initialize(Relation heapRelation, Relation indexRelation, target->ps_locator = heapRelation->rd_locator; target->ps_indexlocator = indexRelation->rd_locator; - target->ps_offset_ins = 0; target->ps_offset_am = 0; SerializeSnapshot(snapshot, target->ps_snapshot_data); - if (instrument) - { - Size sharedinfosz; - - target->ps_offset_ins = offset; - sharedinfosz = offsetof(SharedIndexScanInstrumentation, winstrument) + - nworkers * sizeof(IndexScanInstrumentation); - offset = add_size(offset, sharedinfosz); - offset = MAXALIGN(offset); - - /* Set leader's *sharedinfo pointer, and initialize stats */ - *sharedinfo = (SharedIndexScanInstrumentation *) - OffsetToPointer(target, target->ps_offset_ins); - memset(*sharedinfo, 0, sharedinfosz); - (*sharedinfo)->num_workers = nworkers; - } - /* aminitparallelscan is optional; assume no-op if not provided by AM */ - if (parallel_aware && indexRelation->rd_indam->aminitparallelscan != NULL) + if (indexRelation->rd_indam->aminitparallelscan != NULL) { void *amtarget; @@ -566,6 +539,7 @@ index_parallelrescan(IndexScanDesc scan) { SCAN_CHECKS; + /* reset table AM state for rescan */ if (scan->xs_heapfetch) table_index_fetch_reset(scan->xs_heapfetch); @@ -577,13 +551,17 @@ index_parallelrescan(IndexScanDesc scan) /* * index_beginscan_parallel - join parallel index scan * + * flags is a bitmask of ScanOptions affecting the underlying table scan. No + * SO_INTERNAL_FLAGS are permitted. + * * Caller must be holding suitable locks on the heap and the index. */ IndexScanDesc index_beginscan_parallel(Relation heaprel, Relation indexrel, IndexScanInstrumentation *instrument, int nkeys, int norderbys, - ParallelIndexScanDesc pscan) + ParallelIndexScanDesc pscan, + uint32 flags) { Snapshot snapshot; IndexScanDesc scan; @@ -605,7 +583,7 @@ index_beginscan_parallel(Relation heaprel, Relation indexrel, scan->instrument = instrument; /* prepare to fetch index matches from table */ - scan->xs_heapfetch = table_index_fetch_begin(heaprel); + scan->xs_heapfetch = table_index_fetch_begin(heaprel, flags); return scan; } @@ -643,7 +621,7 @@ index_getnext_tid(IndexScanDesc scan, ScanDirection direction) /* If we're out of index entries, we're done */ if (!found) { - /* release resources (like buffer pins) from table accesses */ + /* reset table AM state */ if (scan->xs_heapfetch) table_index_fetch_reset(scan->xs_heapfetch); @@ -986,11 +964,6 @@ index_store_float8_orderby_distances(IndexScanDesc scan, Oid *orderByTypes, { if (orderByTypes[i] == FLOAT8OID) { -#ifndef USE_FLOAT8_BYVAL - /* must free any old value to avoid memory leakage */ - if (!scan->xs_orderbynulls[i]) - pfree(DatumGetPointer(scan->xs_orderbyvals[i])); -#endif if (distances && !distances[i].isnull) { scan->xs_orderbyvals[i] = Float8GetDatum(distances[i].value); diff --git a/src/backend/access/index/meson.build b/src/backend/access/index/meson.build index e29c03089fd6d..da64cb595a49f 100644 --- a/src/backend/access/index/meson.build +++ b/src/backend/access/index/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'amapi.c', diff --git a/src/backend/access/meson.build b/src/backend/access/meson.build index 7a2d0ddb68942..5fd18de74f92b 100644 --- a/src/backend/access/meson.build +++ b/src/backend/access/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('brin') subdir('common') diff --git a/src/backend/access/nbtree/Makefile b/src/backend/access/nbtree/Makefile index c5cd4e0177fa5..0daf640af96c7 100644 --- a/src/backend/access/nbtree/Makefile +++ b/src/backend/access/nbtree/Makefile @@ -18,6 +18,7 @@ OBJS = \ nbtinsert.o \ nbtpage.o \ nbtpreprocesskeys.o \ + nbtreadpage.o \ nbtree.o \ nbtsearch.o \ nbtsort.o \ diff --git a/src/backend/access/nbtree/README b/src/backend/access/nbtree/README index 53d4a61dc3f19..cb921ca2ef6fb 100644 --- a/src/backend/access/nbtree/README +++ b/src/backend/access/nbtree/README @@ -485,9 +485,8 @@ We handle this kill_prior_tuple race condition by having affected index scans conservatively assume that any change to the leaf page at all implies that it was reached by btbulkdelete in the interim period when no buffer pin was held. This is implemented by not setting any LP_DEAD bits -on the leaf page at all when the page's LSN has changed. (That won't work -with an unlogged index, so for now we don't ever apply the "don't hold -onto pin" optimization there.) +on the leaf page at all when the page's LSN has changed. (This is why we +implement "fake" LSNs for unlogged index relations.) Fastpath For Index Insertion ---------------------------- diff --git a/src/backend/access/nbtree/meson.build b/src/backend/access/nbtree/meson.build index 80962de6e6ed9..812f067e7101c 100644 --- a/src/backend/access/nbtree/meson.build +++ b/src/backend/access/nbtree/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'nbtcompare.c', @@ -6,6 +6,7 @@ backend_sources += files( 'nbtinsert.c', 'nbtpage.c', 'nbtpreprocesskeys.c', + 'nbtreadpage.c', 'nbtree.c', 'nbtsearch.c', 'nbtsort.c', diff --git a/src/backend/access/nbtree/nbtcompare.c b/src/backend/access/nbtree/nbtcompare.c index 4da5a3c1d161d..1d343377e9801 100644 --- a/src/backend/access/nbtree/nbtcompare.c +++ b/src/backend/access/nbtree/nbtcompare.c @@ -3,7 +3,7 @@ * nbtcompare.c * Comparison functions for btree access method. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -57,6 +57,7 @@ #include +#include "utils/builtins.h" #include "utils/fmgrprotos.h" #include "utils/skipsupport.h" #include "utils/sortsupport.h" @@ -278,32 +279,12 @@ btint8cmp(PG_FUNCTION_ARGS) PG_RETURN_INT32(A_LESS_THAN_B); } -#if SIZEOF_DATUM < 8 -static int -btint8fastcmp(Datum x, Datum y, SortSupport ssup) -{ - int64 a = DatumGetInt64(x); - int64 b = DatumGetInt64(y); - - if (a > b) - return A_GREATER_THAN_B; - else if (a == b) - return 0; - else - return A_LESS_THAN_B; -} -#endif - Datum btint8sortsupport(PG_FUNCTION_ARGS) { SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0); -#if SIZEOF_DATUM >= 8 ssup->comparator = ssup_datum_signed_cmp; -#else - ssup->comparator = btint8fastcmp; -#endif PG_RETURN_VOID(); } @@ -518,6 +499,88 @@ btoidskipsupport(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +Datum +btoid8cmp(PG_FUNCTION_ARGS) +{ + Oid8 a = PG_GETARG_OID8(0); + Oid8 b = PG_GETARG_OID8(1); + + if (a > b) + PG_RETURN_INT32(A_GREATER_THAN_B); + else if (a == b) + PG_RETURN_INT32(0); + else + PG_RETURN_INT32(A_LESS_THAN_B); +} + +static int +btoid8fastcmp(Datum x, Datum y, SortSupport ssup) +{ + Oid8 a = DatumGetObjectId8(x); + Oid8 b = DatumGetObjectId8(y); + + if (a > b) + return A_GREATER_THAN_B; + else if (a == b) + return 0; + else + return A_LESS_THAN_B; +} + +Datum +btoid8sortsupport(PG_FUNCTION_ARGS) +{ + SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0); + + ssup->comparator = btoid8fastcmp; + PG_RETURN_VOID(); +} + +static Datum +oid8_decrement(Relation rel, Datum existing, bool *underflow) +{ + Oid8 oexisting = DatumGetObjectId8(existing); + + if (oexisting == InvalidOid8) + { + /* return value is undefined */ + *underflow = true; + return (Datum) 0; + } + + *underflow = false; + return ObjectId8GetDatum(oexisting - 1); +} + +static Datum +oid8_increment(Relation rel, Datum existing, bool *overflow) +{ + Oid8 oexisting = DatumGetObjectId8(existing); + + if (oexisting == OID8_MAX) + { + /* return value is undefined */ + *overflow = true; + return (Datum) 0; + } + + *overflow = false; + return ObjectId8GetDatum(oexisting + 1); +} + +Datum +btoid8skipsupport(PG_FUNCTION_ARGS) +{ + SkipSupport sksup = (SkipSupport) PG_GETARG_POINTER(0); + + sksup->decrement = oid8_decrement; + sksup->increment = oid8_increment; + sksup->low_elem = ObjectId8GetDatum(InvalidOid8); + sksup->high_elem = ObjectId8GetDatum(OID8_MAX); + + PG_RETURN_VOID(); +} + Datum btoidvectorcmp(PG_FUNCTION_ARGS) { @@ -525,6 +588,9 @@ btoidvectorcmp(PG_FUNCTION_ARGS) oidvector *b = (oidvector *) PG_GETARG_POINTER(1); int i; + check_valid_oidvector(a); + check_valid_oidvector(b); + /* We arbitrarily choose to sort first by vector length */ if (a->dim1 != b->dim1) PG_RETURN_INT32(a->dim1 - b->dim1); @@ -555,7 +621,7 @@ btcharcmp(PG_FUNCTION_ARGS) static Datum char_decrement(Relation rel, Datum existing, bool *underflow) { - uint8 cexisting = UInt8GetDatum(existing); + uint8 cexisting = DatumGetUInt8(existing); if (cexisting == 0) { @@ -571,7 +637,7 @@ char_decrement(Relation rel, Datum existing, bool *underflow) static Datum char_increment(Relation rel, Datum existing, bool *overflow) { - uint8 cexisting = UInt8GetDatum(existing); + uint8 cexisting = DatumGetUInt8(existing); if (cexisting == UCHAR_MAX) { diff --git a/src/backend/access/nbtree/nbtdedup.c b/src/backend/access/nbtree/nbtdedup.c index 08884116aecbe..af7affdf409d3 100644 --- a/src/backend/access/nbtree/nbtdedup.c +++ b/src/backend/access/nbtree/nbtdedup.c @@ -3,7 +3,7 @@ * nbtdedup.c * Deduplicate or bottom-up delete items in Postgres btrees. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,7 @@ #include "access/nbtree.h" #include "access/nbtxlog.h" +#include "access/tableam.h" #include "access/xloginsert.h" #include "miscadmin.h" #include "utils/rel.h" @@ -68,6 +69,7 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, Size pagesaving PG_USED_FOR_ASSERTS_ONLY = 0; bool singlevalstrat = false; int nkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); + XLogRecPtr recptr; /* Passed-in newitemsz is MAXALIGNED but does not include line pointer */ newitemsz += sizeof(ItemIdData); @@ -81,7 +83,7 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, * That ought to leave us with a good split point when pages full of * duplicates can be split several times. */ - state = (BTDedupState) palloc(sizeof(BTDedupStateData)); + state = palloc_object(BTDedupStateData); state->deduplicate = true; state->nmaxitems = 0; state->maxpostingsize = Min(BTMaxItemSize / 2, INDEX_SIZE_MASK); @@ -125,8 +127,7 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, Size hitemsz = ItemIdGetLength(hitemid); IndexTuple hitem = (IndexTuple) PageGetItem(page, hitemid); - if (PageAddItem(newpage, (Item) hitem, hitemsz, P_HIKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(newpage, hitem, hitemsz, P_HIKEY, false, false) == InvalidOffsetNumber) elog(ERROR, "deduplication failed to add highkey"); } @@ -245,7 +246,6 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, /* XLOG stuff */ if (RelationNeedsWAL(rel)) { - XLogRecPtr recptr; xl_btree_dedup xlrec_dedup; xlrec_dedup.nintervals = state->nintervals; @@ -263,9 +263,11 @@ _bt_dedup_pass(Relation rel, Buffer buf, IndexTuple newitem, Size newitemsz, state->nintervals * sizeof(BTDedupInterval)); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_DEDUP); - - PageSetLSN(page, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(page, recptr); END_CRIT_SECTION(); @@ -321,7 +323,7 @@ _bt_bottomupdel_pass(Relation rel, Buffer buf, Relation heapRel, newitemsz += sizeof(ItemIdData); /* Initialize deduplication state */ - state = (BTDedupState) palloc(sizeof(BTDedupStateData)); + state = palloc_object(BTDedupStateData); state->deduplicate = true; state->nmaxitems = 0; state->maxpostingsize = BLCKSZ; /* We're not really deduplicating */ @@ -355,8 +357,8 @@ _bt_bottomupdel_pass(Relation rel, Buffer buf, Relation heapRel, delstate.bottomup = true; delstate.bottomupfreespace = Max(BLCKSZ / 16, newitemsz); delstate.ndeltids = 0; - delstate.deltids = palloc(MaxTIDsPerBTreePage * sizeof(TM_IndexDelete)); - delstate.status = palloc(MaxTIDsPerBTreePage * sizeof(TM_IndexStatus)); + delstate.deltids = palloc_array(TM_IndexDelete, MaxTIDsPerBTreePage); + delstate.status = palloc_array(TM_IndexStatus, MaxTIDsPerBTreePage); minoff = P_FIRSTDATAKEY(opaque); maxoff = PageGetMaxOffsetNumber(page); @@ -569,8 +571,7 @@ _bt_dedup_finish_pending(Page newpage, BTDedupState state) tuplesz = IndexTupleSize(state->base); Assert(tuplesz == MAXALIGN(IndexTupleSize(state->base))); Assert(tuplesz <= BTMaxItemSize); - if (PageAddItem(newpage, (Item) state->base, tuplesz, tupoff, - false, false) == InvalidOffsetNumber) + if (PageAddItem(newpage, state->base, tuplesz, tupoff, false, false) == InvalidOffsetNumber) elog(ERROR, "deduplication failed to add tuple to page"); spacesaving = 0; @@ -589,8 +590,7 @@ _bt_dedup_finish_pending(Page newpage, BTDedupState state) Assert(tuplesz == MAXALIGN(IndexTupleSize(final))); Assert(tuplesz <= BTMaxItemSize); - if (PageAddItem(newpage, (Item) final, tuplesz, tupoff, false, - false) == InvalidOffsetNumber) + if (PageAddItem(newpage, final, tuplesz, tupoff, false, false) == InvalidOffsetNumber) elog(ERROR, "deduplication failed to add tuple to page"); pfree(final); @@ -861,7 +861,7 @@ _bt_singleval_fillfactor(Page page, BTDedupState state, Size newitemsz) * returned posting list tuple (they must be included in htids array.) */ IndexTuple -_bt_form_posting(IndexTuple base, ItemPointer htids, int nhtids) +_bt_form_posting(IndexTuple base, const ItemPointerData *htids, int nhtids) { uint32 keysize, newsize; diff --git a/src/backend/access/nbtree/nbtinsert.c b/src/backend/access/nbtree/nbtinsert.c index aa82cede30aa4..c8af97dd23dfb 100644 --- a/src/backend/access/nbtree/nbtinsert.c +++ b/src/backend/access/nbtree/nbtinsert.c @@ -3,7 +3,7 @@ * nbtinsert.c * Item insertion in Lehman and Yao btrees for Postgres. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include "access/nbtree.h" #include "access/nbtxlog.h" +#include "access/tableam.h" #include "access/transam.h" #include "access/xloginsert.h" #include "common/int.h" @@ -25,6 +26,7 @@ #include "miscadmin.h" #include "storage/lmgr.h" #include "storage/predicate.h" +#include "utils/injection_point.h" /* Minimum tree height for application of fastpath optimization */ #define BTREE_FASTPATH_MIN_LEVEL 2 @@ -59,8 +61,9 @@ static Buffer _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, IndexTuple nposting, uint16 postingoff); static void _bt_insert_parent(Relation rel, Relation heaprel, Buffer buf, Buffer rbuf, BTStack stack, bool isroot, bool isonly); +static void _bt_freestack(BTStack stack); static Buffer _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf); -static inline bool _bt_pgaddtup(Page page, Size itemsize, IndexTuple itup, +static inline bool _bt_pgaddtup(Page page, Size itemsize, const IndexTupleData *itup, OffsetNumber itup_off, bool newfirstdataitem); static void _bt_delete_or_dedup_one_page(Relation rel, Relation heapRel, BTInsertState insertstate, @@ -378,7 +381,7 @@ _bt_search_insert(Relation rel, Relation heaprel, BTInsertState insertstate) /* Cannot use optimization -- descend tree, return proper descent stack */ return _bt_search(rel, heaprel, insertstate->itup_key, &insertstate->buf, - BT_WRITE); + BT_WRITE, true); } /* @@ -679,20 +682,31 @@ _bt_check_unique(Relation rel, BTInsertState insertstate, Relation heapRel, { /* * The conflicting tuple (or all HOT chains pointed to by - * all posting list TIDs) is dead to everyone, so mark the - * index entry killed. + * all posting list TIDs) is dead to everyone, so try to + * mark the index entry killed. It's ok if we're not + * allowed to, this isn't required for correctness. */ - ItemIdMarkDead(curitemid); - opaque->btpo_flags |= BTP_HAS_GARBAGE; + Buffer buf; - /* - * Mark buffer with a dirty hint, since state is not - * crucial. Be sure to mark the proper buffer dirty. - */ + /* Be sure to operate on the proper buffer */ if (nbuf != InvalidBuffer) - MarkBufferDirtyHint(nbuf, true); + buf = nbuf; else - MarkBufferDirtyHint(insertstate->buf, true); + buf = insertstate->buf; + + /* + * Use the hint bit infrastructure to check if we can + * update the page while just holding a share lock. + * + * Can't use BufferSetHintBits16() here as we update two + * different locations. + */ + if (BufferBeginSetHintBits(buf)) + { + ItemIdMarkDead(curitemid); + opaque->btpo_flags |= BTP_HAS_GARBAGE; + BufferFinishSetHintBits(buf, true, true); + } } /* @@ -1123,6 +1137,7 @@ _bt_insertonpg(Relation rel, IndexTuple oposting = NULL; IndexTuple origitup = NULL; IndexTuple nposting = NULL; + XLogRecPtr recptr; page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); @@ -1238,6 +1253,13 @@ _bt_insertonpg(Relation rel, * page. *---------- */ +#ifdef USE_INJECTION_POINTS + if (P_ISLEAF(opaque)) + INJECTION_POINT("nbtree-leave-leaf-split-incomplete", NULL); + else + INJECTION_POINT("nbtree-leave-internal-split-incomplete", NULL); +#endif + _bt_insert_parent(rel, heaprel, buf, rbuf, stack, isroot, isonly); } else @@ -1277,8 +1299,7 @@ _bt_insertonpg(Relation rel, if (postingoff != 0) memcpy(oposting, nposting, MAXALIGN(IndexTupleSize(nposting))); - if (PageAddItem(page, (Item) itup, itemsz, newitemoff, false, - false) == InvalidOffsetNumber) + if (PageAddItem(page, itup, itemsz, newitemoff, false, false) == InvalidOffsetNumber) elog(PANIC, "failed to add new item to block %u in index \"%s\"", BufferGetBlockNumber(buf), RelationGetRelationName(rel)); @@ -1314,7 +1335,6 @@ _bt_insertonpg(Relation rel, xl_btree_insert xlrec; xl_btree_metadata xlmeta; uint8 xlinfo; - XLogRecPtr recptr; uint16 upostingoff; xlrec.offnum = newitemoff; @@ -1387,14 +1407,16 @@ _bt_insertonpg(Relation rel, } recptr = XLogInsert(RM_BTREE_ID, xlinfo); + } + else + recptr = XLogGetFakeLSN(rel); - if (BufferIsValid(metabuf)) - PageSetLSN(metapg, recptr); - if (!isleaf) - PageSetLSN(BufferGetPage(cbuf), recptr); + if (BufferIsValid(metabuf)) + PageSetLSN(metapg, recptr); + if (!isleaf) + PageSetLSN(BufferGetPage(cbuf), recptr); - PageSetLSN(page, recptr); - } + PageSetLSN(page, recptr); END_CRIT_SECTION(); @@ -1472,6 +1494,8 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, Page origpage; Page leftpage, rightpage; + PGAlignedBlock leftpage_buf, + rightpage_buf; BlockNumber origpagenumber, rightpagenumber; BTPageOpaque ropaque, @@ -1494,6 +1518,7 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, bool newitemonleft, isleaf, isrightmost; + XLogRecPtr recptr; /* * origpage is the original page to be split. leftpage is a temporary @@ -1542,8 +1567,8 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, firstrightoff = _bt_findsplitloc(rel, origpage, newitemoff, newitemsz, newitem, &newitemonleft); - /* Allocate temp buffer for leftpage */ - leftpage = PageGetTempPage(origpage); + /* Use temporary buffer for leftpage */ + leftpage = leftpage_buf.data; _bt_pageinit(leftpage, BufferGetPageSize(buf)); lopaque = BTPageGetOpaque(leftpage); @@ -1697,8 +1722,7 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, Assert(BTreeTupleGetNAtts(lefthighkey, rel) <= IndexRelationGetNumberOfKeyAttributes(rel)); Assert(itemsz == MAXALIGN(IndexTupleSize(lefthighkey))); - if (PageAddItem(leftpage, (Item) lefthighkey, itemsz, afterleftoff, false, - false) == InvalidOffsetNumber) + if (PageAddItem(leftpage, lefthighkey, itemsz, afterleftoff, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add high key to the left sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1706,19 +1730,23 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, /* * Acquire a new right page to split into, now that left page has a new - * high key. From here on, it's not okay to throw an error without - * zeroing rightpage first. This coding rule ensures that we won't - * confuse future VACUUM operations, which might otherwise try to re-find - * a downlink to a leftover junk page as the page undergoes deletion. + * high key. * - * It would be reasonable to start the critical section just after the new - * rightpage buffer is acquired instead; that would allow us to avoid - * leftover junk pages without bothering to zero rightpage. We do it this - * way because it avoids an unnecessary PANIC when either origpage or its - * existing sibling page are corrupt. + * To not confuse future VACUUM operations, we zero the right page and + * work on an in-memory copy of it before writing WAL, then copy its + * contents back to the actual page once we start the critical section + * work. This simplifies the split work, so as there is no need to zero + * the right page before throwing an error. */ rbuf = _bt_allocbuf(rel, heaprel); - rightpage = BufferGetPage(rbuf); + rightpage = rightpage_buf.data; + + /* + * Copy the contents of the right page into its temporary location, and + * zero the original space. + */ + memcpy(rightpage, BufferGetPage(rbuf), BLCKSZ); + memset(BufferGetPage(rbuf), 0, BLCKSZ); rightpagenumber = BufferGetBlockNumber(rbuf); /* rightpage was initialized by _bt_allocbuf */ ropaque = BTPageGetOpaque(rightpage); @@ -1764,10 +1792,8 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, Assert(BTreeTupleGetNAtts(righthighkey, rel) > 0); Assert(BTreeTupleGetNAtts(righthighkey, rel) <= IndexRelationGetNumberOfKeyAttributes(rel)); - if (PageAddItem(rightpage, (Item) righthighkey, itemsz, afterrightoff, - false, false) == InvalidOffsetNumber) + if (PageAddItem(rightpage, righthighkey, itemsz, afterrightoff, false, false) == InvalidOffsetNumber) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); elog(ERROR, "failed to add high key to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1815,7 +1841,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (!_bt_pgaddtup(leftpage, newitemsz, newitem, afterleftoff, false)) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); elog(ERROR, "failed to add new item to the left sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1828,7 +1853,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (!_bt_pgaddtup(rightpage, newitemsz, newitem, afterrightoff, afterrightoff == minusinfoff)) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); elog(ERROR, "failed to add new item to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1842,7 +1866,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, { if (!_bt_pgaddtup(leftpage, itemsz, dataitem, afterleftoff, false)) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); elog(ERROR, "failed to add old item to the left sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1854,7 +1877,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (!_bt_pgaddtup(rightpage, itemsz, dataitem, afterrightoff, afterrightoff == minusinfoff)) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); elog(ERROR, "failed to add old item to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1875,7 +1897,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, if (!_bt_pgaddtup(rightpage, newitemsz, newitem, afterrightoff, afterrightoff == minusinfoff)) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); elog(ERROR, "failed to add new item to the right sibling" " while splitting block %u of index \"%s\"", origpagenumber, RelationGetRelationName(rel)); @@ -1895,7 +1916,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, sopaque = BTPageGetOpaque(spage); if (sopaque->btpo_prev != origpagenumber) { - memset(rightpage, 0, BufferGetPageSize(rbuf)); ereport(ERROR, (errcode(ERRCODE_INDEX_CORRUPTED), errmsg_internal("right sibling's left-link doesn't match: " @@ -1938,9 +1958,19 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, * original. We need to do this before writing the WAL record, so that * XLogInsert can WAL log an image of the page if necessary. */ - PageRestoreTempPage(leftpage, origpage); + memcpy(origpage, leftpage, BLCKSZ); /* leftpage, lopaque must not be used below here */ + /* + * Move the contents of the right page from its temporary location to the + * destination buffer, before writing the WAL record. Unlike the left + * page, the right page and its opaque area are still needed to complete + * the update of the page, so reinitialize them. + */ + rightpage = BufferGetPage(rbuf); + memcpy(rightpage, rightpage_buf.data, BLCKSZ); + ropaque = BTPageGetOpaque(rightpage); + MarkBufferDirty(buf); MarkBufferDirty(rbuf); @@ -1968,7 +1998,6 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, { xl_btree_split xlrec; uint8 xlinfo; - XLogRecPtr recptr; xlrec.level = ropaque->btpo_level; /* See comments below on newitem, orignewitem, and posting lists */ @@ -2052,14 +2081,16 @@ _bt_split(Relation rel, Relation heaprel, BTScanInsert itup_key, Buffer buf, xlinfo = newitemonleft ? XLOG_BTREE_SPLIT_L : XLOG_BTREE_SPLIT_R; recptr = XLogInsert(RM_BTREE_ID, xlinfo); - - PageSetLSN(origpage, recptr); - PageSetLSN(rightpage, recptr); - if (!isrightmost) - PageSetLSN(spage, recptr); - if (!isleaf) - PageSetLSN(BufferGetPage(cbuf), recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(origpage, recptr); + PageSetLSN(rightpage, recptr); + if (!isrightmost) + PageSetLSN(spage, recptr); + if (!isleaf) + PageSetLSN(BufferGetPage(cbuf), recptr); END_CRIT_SECTION(); @@ -2278,6 +2309,7 @@ _bt_finish_split(Relation rel, Relation heaprel, Buffer lbuf, BTStack stack) /* Was this the only page on the level before split? */ wasonly = (P_LEFTMOST(lpageop) && P_RIGHTMOST(rpageop)); + INJECTION_POINT("nbtree-finish-incomplete-split", NULL); elog(DEBUG1, "finishing incomplete split of %u/%u", BufferGetBlockNumber(lbuf), BufferGetBlockNumber(rbuf)); @@ -2422,6 +2454,22 @@ _bt_getstackbuf(Relation rel, Relation heaprel, BTStack stack, BlockNumber child } } +/* + * _bt_freestack() -- free a retracement stack made by _bt_search_insert. + */ +static void +_bt_freestack(BTStack stack) +{ + BTStack ostack; + + while (stack != NULL) + { + ostack = stack; + stack = stack->bts_parent; + pfree(ostack); + } +} + /* * _bt_newlevel() -- Create a new level above root page. * @@ -2460,6 +2508,7 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf) Buffer metabuf; Page metapg; BTMetaPageData *metad; + XLogRecPtr recptr; lbkno = BufferGetBlockNumber(lbuf); rbkno = BufferGetBlockNumber(rbuf); @@ -2527,8 +2576,7 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf) * benefit of _bt_restore_page(). */ Assert(BTreeTupleGetNAtts(left_item, rel) == 0); - if (PageAddItem(rootpage, (Item) left_item, left_item_sz, P_HIKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(rootpage, left_item, left_item_sz, P_HIKEY, false, false) == InvalidOffsetNumber) elog(PANIC, "failed to add leftkey to new root page" " while splitting block %u of index \"%s\"", BufferGetBlockNumber(lbuf), RelationGetRelationName(rel)); @@ -2539,8 +2587,7 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf) Assert(BTreeTupleGetNAtts(right_item, rel) > 0); Assert(BTreeTupleGetNAtts(right_item, rel) <= IndexRelationGetNumberOfKeyAttributes(rel)); - if (PageAddItem(rootpage, (Item) right_item, right_item_sz, P_FIRSTKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(rootpage, right_item, right_item_sz, P_FIRSTKEY, false, false) == InvalidOffsetNumber) elog(PANIC, "failed to add rightkey to new root page" " while splitting block %u of index \"%s\"", BufferGetBlockNumber(lbuf), RelationGetRelationName(rel)); @@ -2557,7 +2604,6 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf) if (RelationNeedsWAL(rel)) { xl_btree_newroot xlrec; - XLogRecPtr recptr; xl_btree_metadata md; xlrec.rootblk = rootblknum; @@ -2591,11 +2637,13 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf) ((PageHeader) rootpage)->pd_upper); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_NEWROOT); - - PageSetLSN(lpage, recptr); - PageSetLSN(rootpage, recptr); - PageSetLSN(metapg, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(lpage, recptr); + PageSetLSN(rootpage, recptr); + PageSetLSN(metapg, recptr); END_CRIT_SECTION(); @@ -2629,7 +2677,7 @@ _bt_newlevel(Relation rel, Relation heaprel, Buffer lbuf, Buffer rbuf) static inline bool _bt_pgaddtup(Page page, Size itemsize, - IndexTuple itup, + const IndexTupleData *itup, OffsetNumber itup_off, bool newfirstdataitem) { @@ -2644,8 +2692,7 @@ _bt_pgaddtup(Page page, itemsize = sizeof(IndexTupleData); } - if (unlikely(PageAddItem(page, (Item) itup, itemsize, itup_off, false, - false) == InvalidOffsetNumber)) + if (unlikely(PageAddItem(page, itup, itemsize, itup_off, false, false) == InvalidOffsetNumber)) return false; return true; @@ -2950,7 +2997,7 @@ _bt_deadblocks(Page page, OffsetNumber *deletable, int ndeletable, */ spacentids = ndeletable + 1; ntids = 0; - tidblocks = (BlockNumber *) palloc(sizeof(BlockNumber) * spacentids); + tidblocks = palloc_array(BlockNumber, spacentids); /* * First add the table block for the incoming newitem. This is the one @@ -3010,8 +3057,8 @@ _bt_deadblocks(Page page, OffsetNumber *deletable, int ndeletable, static inline int _bt_blk_cmp(const void *arg1, const void *arg2) { - BlockNumber b1 = *((BlockNumber *) arg1); - BlockNumber b2 = *((BlockNumber *) arg2); + BlockNumber b1 = *((const BlockNumber *) arg1); + BlockNumber b2 = *((const BlockNumber *) arg2); return pg_cmp_u32(b1, b2); } diff --git a/src/backend/access/nbtree/nbtpage.c b/src/backend/access/nbtree/nbtpage.c index c79dd38ee18f3..0547038616e04 100644 --- a/src/backend/access/nbtree/nbtpage.c +++ b/src/backend/access/nbtree/nbtpage.c @@ -4,7 +4,7 @@ * BTree-specific page management code for the Postgres btree access * method. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -33,6 +33,7 @@ #include "storage/indexfsm.h" #include "storage/predicate.h" #include "storage/procarray.h" +#include "utils/injection_point.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/snapmgr.h" @@ -234,6 +235,7 @@ _bt_set_cleanup_info(Relation rel, BlockNumber num_delpages) Buffer metabuf; Page metapg; BTMetaPageData *metad; + XLogRecPtr recptr; /* * On-disk compatibility note: The btm_last_cleanup_num_delpages metapage @@ -285,7 +287,6 @@ _bt_set_cleanup_info(Relation rel, BlockNumber num_delpages) if (RelationNeedsWAL(rel)) { xl_btree_metadata md; - XLogRecPtr recptr; XLogBeginInsert(); XLogRegisterBuffer(0, metabuf, REGBUF_WILL_INIT | REGBUF_STANDARD); @@ -302,9 +303,11 @@ _bt_set_cleanup_info(Relation rel, BlockNumber num_delpages) XLogRegisterBufData(0, &md, sizeof(xl_btree_metadata)); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_META_CLEANUP); - - PageSetLSN(metapg, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(metapg, recptr); END_CRIT_SECTION(); @@ -350,6 +353,7 @@ _bt_getroot(Relation rel, Relation heaprel, int access) BlockNumber rootblkno; uint32 rootlevel; BTMetaPageData *metad; + XLogRecPtr recptr; Assert(access == BT_READ || heaprel != NULL); @@ -472,7 +476,6 @@ _bt_getroot(Relation rel, Relation heaprel, int access) if (RelationNeedsWAL(rel)) { xl_btree_newroot xlrec; - XLogRecPtr recptr; xl_btree_metadata md; XLogBeginInsert(); @@ -496,10 +499,12 @@ _bt_getroot(Relation rel, Relation heaprel, int access) XLogRegisterData(&xlrec, SizeOfBtreeNewroot); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_NEWROOT); - - PageSetLSN(rootpage, recptr); - PageSetLSN(metapg, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(rootpage, recptr); + PageSetLSN(metapg, recptr); END_CRIT_SECTION(); @@ -1006,24 +1011,47 @@ _bt_relandgetbuf(Relation rel, Buffer obuf, BlockNumber blkno, int access) Assert(BlockNumberIsValid(blkno)); if (BufferIsValid(obuf)) - _bt_unlockbuf(rel, obuf); - buf = ReleaseAndReadBuffer(obuf, rel, blkno); - _bt_lockbuf(rel, buf, access); + { + if (BufferGetBlockNumber(obuf) == blkno) + { + /* trade in old lock mode for new lock */ + _bt_unlockbuf(rel, obuf); + buf = obuf; + } + else + { + /* release lock and pin at once, that's a bit more efficient */ + _bt_relbuf(rel, obuf); + buf = ReadBuffer(rel, blkno); + } + } + else + buf = ReadBuffer(rel, blkno); + _bt_lockbuf(rel, buf, access); _bt_checkpage(rel, buf); + return buf; } /* * _bt_relbuf() -- release a locked buffer. * - * Lock and pin (refcount) are both dropped. + * Lock and pin (refcount) are both dropped. This is a bit more efficient than + * doing the two operations separately. */ void _bt_relbuf(Relation rel, Buffer buf) { - _bt_unlockbuf(rel, buf); - ReleaseBuffer(buf); + /* + * Buffer is pinned and locked, which means that it is expected to be + * defined and addressable. Check that proactively. + */ + VALGRIND_CHECK_MEM_IS_DEFINED(BufferGetPage(buf), BLCKSZ); + if (!RelationUsesLocalBuffers(rel)) + VALGRIND_MAKE_MEM_NOACCESS(BufferGetPage(buf), BLCKSZ); + + UnlockReleaseBuffer(buf); } /* @@ -1161,6 +1189,7 @@ _bt_delitems_vacuum(Relation rel, Buffer buf, char *updatedbuf = NULL; Size updatedbuflen = 0; OffsetNumber updatedoffsets[MaxIndexTuplesPerPage]; + XLogRecPtr recptr; /* Shouldn't be called unless there's something to do */ Assert(ndeletable > 0 || nupdatable > 0); @@ -1194,8 +1223,7 @@ _bt_delitems_vacuum(Relation rel, Buffer buf, itup = updatable[i]->itup; itemsz = MAXALIGN(IndexTupleSize(itup)); - if (!PageIndexTupleOverwrite(page, updatedoffset, (Item) itup, - itemsz)) + if (!PageIndexTupleOverwrite(page, updatedoffset, itup, itemsz)) elog(PANIC, "failed to update partially dead item in block %u of index \"%s\"", BufferGetBlockNumber(buf), RelationGetRelationName(rel)); } @@ -1226,7 +1254,6 @@ _bt_delitems_vacuum(Relation rel, Buffer buf, /* XLOG stuff */ if (needswal) { - XLogRecPtr recptr; xl_btree_vacuum xlrec_vacuum; xlrec_vacuum.ndeleted = ndeletable; @@ -1248,9 +1275,11 @@ _bt_delitems_vacuum(Relation rel, Buffer buf, } recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_VACUUM); - - PageSetLSN(page, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(page, recptr); END_CRIT_SECTION(); @@ -1292,6 +1321,7 @@ _bt_delitems_delete(Relation rel, Buffer buf, char *updatedbuf = NULL; Size updatedbuflen = 0; OffsetNumber updatedoffsets[MaxIndexTuplesPerPage]; + XLogRecPtr recptr; /* Shouldn't be called unless there's something to do */ Assert(ndeletable > 0 || nupdatable > 0); @@ -1314,8 +1344,7 @@ _bt_delitems_delete(Relation rel, Buffer buf, itup = updatable[i]->itup; itemsz = MAXALIGN(IndexTupleSize(itup)); - if (!PageIndexTupleOverwrite(page, updatedoffset, (Item) itup, - itemsz)) + if (!PageIndexTupleOverwrite(page, updatedoffset, itup, itemsz)) elog(PANIC, "failed to update partially dead item in block %u of index \"%s\"", BufferGetBlockNumber(buf), RelationGetRelationName(rel)); } @@ -1343,7 +1372,6 @@ _bt_delitems_delete(Relation rel, Buffer buf, /* XLOG stuff */ if (needswal) { - XLogRecPtr recptr; xl_btree_delete xlrec_delete; xlrec_delete.snapshotConflictHorizon = snapshotConflictHorizon; @@ -1367,9 +1395,11 @@ _bt_delitems_delete(Relation rel, Buffer buf, } recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_DELETE); - - PageSetLSN(page, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + PageSetLSN(page, recptr); END_CRIT_SECTION(); @@ -1463,8 +1493,8 @@ _bt_delitems_update(BTVacuumPosting *updatable, int nupdatable, static int _bt_delitems_cmp(const void *a, const void *b) { - TM_IndexDelete *indexdelete1 = (TM_IndexDelete *) a; - TM_IndexDelete *indexdelete2 = (TM_IndexDelete *) b; + const TM_IndexDelete *indexdelete1 = a; + const TM_IndexDelete *indexdelete2 = b; Assert(indexdelete1->id != indexdelete2->id); @@ -1967,7 +1997,7 @@ _bt_pagedel(Relation rel, Buffer leafbuf, BTVacState *vstate) /* Set up a BTLessStrategyNumber-like insertion scan key */ itup_key->nextkey = false; itup_key->backward = true; - stack = _bt_search(rel, NULL, itup_key, &sleafbuf, BT_READ); + stack = _bt_search(rel, NULL, itup_key, &sleafbuf, BT_READ, true); /* won't need a second lock or pin on leafbuf */ _bt_relbuf(rel, sleafbuf); @@ -2005,6 +2035,10 @@ _bt_pagedel(Relation rel, Buffer leafbuf, BTVacState *vstate) return; } } + else + { + INJECTION_POINT("nbtree-finish-half-dead-page-vacuum", NULL); + } /* * Then unlink it from its siblings. Each call to @@ -2100,6 +2134,7 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, OffsetNumber nextoffset; IndexTuple itup; IndexTupleData trunctuple; + XLogRecPtr recptr; page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); @@ -2239,8 +2274,7 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, else BTreeTupleSetTopParent(&trunctuple, InvalidBlockNumber); - if (!PageIndexTupleOverwrite(page, P_HIKEY, (Item) &trunctuple, - IndexTupleSize(&trunctuple))) + if (!PageIndexTupleOverwrite(page, P_HIKEY, &trunctuple, IndexTupleSize(&trunctuple))) elog(ERROR, "could not overwrite high key in half-dead page"); /* Must mark buffers dirty before XLogInsert */ @@ -2251,7 +2285,6 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, if (RelationNeedsWAL(rel)) { xl_btree_mark_page_halfdead xlrec; - XLogRecPtr recptr; xlrec.poffset = poffset; xlrec.leafblk = leafblkno; @@ -2272,12 +2305,14 @@ _bt_mark_page_halfdead(Relation rel, Relation heaprel, Buffer leafbuf, XLogRegisterData(&xlrec, SizeOfBtreeMarkPageHalfDead); recptr = XLogInsert(RM_BTREE_ID, XLOG_BTREE_MARK_PAGE_HALFDEAD); - - page = BufferGetPage(subtreeparent); - PageSetLSN(page, recptr); - page = BufferGetPage(leafbuf); - PageSetLSN(page, recptr); } + else + recptr = XLogGetFakeLSN(rel); + + page = BufferGetPage(subtreeparent); + PageSetLSN(page, recptr); + page = BufferGetPage(leafbuf); + PageSetLSN(page, recptr); END_CRIT_SECTION(); @@ -2335,6 +2370,7 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno, uint32 targetlevel; IndexTuple leafhikey; BlockNumber leaftopparent; + XLogRecPtr recptr; page = BufferGetPage(leafbuf); opaque = BTPageGetOpaque(page); @@ -2352,6 +2388,8 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno, _bt_unlockbuf(rel, leafbuf); + INJECTION_POINT("nbtree-leave-page-half-dead", NULL); + /* * Check here, as calling loops will have locks held, preventing * interrupts from being processed. @@ -2672,7 +2710,6 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno, xl_btree_unlink_page xlrec; xl_btree_metadata xlmeta; uint8 xlinfo; - XLogRecPtr recptr; XLogBeginInsert(); @@ -2716,25 +2753,25 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno, xlinfo = XLOG_BTREE_UNLINK_PAGE; recptr = XLogInsert(RM_BTREE_ID, xlinfo); + } + else + recptr = XLogGetFakeLSN(rel); - if (BufferIsValid(metabuf)) - { - PageSetLSN(metapg, recptr); - } - page = BufferGetPage(rbuf); + if (BufferIsValid(metabuf)) + PageSetLSN(metapg, recptr); + page = BufferGetPage(rbuf); + PageSetLSN(page, recptr); + page = BufferGetPage(buf); + PageSetLSN(page, recptr); + if (BufferIsValid(lbuf)) + { + page = BufferGetPage(lbuf); PageSetLSN(page, recptr); - page = BufferGetPage(buf); + } + if (target != leafblkno) + { + page = BufferGetPage(leafbuf); PageSetLSN(page, recptr); - if (BufferIsValid(lbuf)) - { - page = BufferGetPage(lbuf); - PageSetLSN(page, recptr); - } - if (target != leafblkno) - { - page = BufferGetPage(leafbuf); - PageSetLSN(page, recptr); - } } END_CRIT_SECTION(); @@ -2978,7 +3015,7 @@ _bt_pendingfsm_init(Relation rel, BTVacState *vstate, bool cleanuponly) vstate->maxbufsize = (int) maxbufsize; /* Allocate buffer, indicate that there are currently 0 pending pages */ - vstate->pendingpages = palloc(sizeof(BTPendingFSM) * vstate->bufsize); + vstate->pendingpages = palloc_array(BTPendingFSM, vstate->bufsize); vstate->npendingpages = 0; } diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c index a136e4bbfdfb5..39c0a5d610f9f 100644 --- a/src/backend/access/nbtree/nbtpreprocesskeys.c +++ b/src/backend/access/nbtree/nbtpreprocesskeys.c @@ -3,7 +3,7 @@ * nbtpreprocesskeys.c * Preprocessing for Postgres btree scan keys. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,10 +16,13 @@ #include "postgres.h" #include "access/nbtree.h" +#include "access/relscan.h" +#include "common/int.h" #include "lib/qunique.h" #include "utils/array.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/rel.h" typedef struct BTScanKeyPreproc { @@ -56,13 +59,15 @@ static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk, BTArrayKeyInfo *array); static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk, BTArrayKeyInfo *array); +static void _bt_unmark_keys(IndexScanDesc scan, int *keyDataMap); +static int _bt_reorder_array_cmp(const void *a, const void *b); static ScanKey _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys); static void _bt_preprocess_array_keys_final(IndexScanDesc scan, int *keyDataMap); static int _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops_out, int *numSkipArrayKeys_out); static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey, Oid elemtype, StrategyNumber strat, - Datum *elems, int nelems); + const Datum *elems, int nelems); static void _bt_setup_array_cmp(IndexScanDesc scan, ScanKey skey, Oid elemtype, FmgrInfo *orderproc, FmgrInfo **sortprocp); static int _bt_sort_array_elements(ScanKey skey, FmgrInfo *sortproc, @@ -96,7 +101,7 @@ static int _bt_compare_array_elements(const void *a, const void *b, void *arg); * incomplete sets of cross-type operators, we may fail to detect redundant * or contradictory keys, but we can survive that.) * - * The output keys must be sorted by index attribute. Presently we expect + * Required output keys are sorted by index attribute. Presently we expect * (but verify) that the input keys are already so sorted --- this is done * by match_clauses_to_index() in indxpath.c. Some reordering of the keys * within each attribute may be done as a byproduct of the processing here. @@ -117,6 +122,10 @@ static int _bt_compare_array_elements(const void *a, const void *b, void *arg); * For the first attribute without an "=" key, any "<" and "<=" keys are * marked SK_BT_REQFWD while any ">" and ">=" keys are marked SK_BT_REQBKWD. * This can be seen to be correct by considering the above example. + * (Actually, the z key _will_ be marked SK_BT_REQFWD, since preprocessing + * will generate a skip array on y -- except when DEBUG_DISABLE_SKIP_SCAN. + * See below description of how and why we generate skip array = keys in the + * presence of a "contradictory" condition such as "y < 4".) * * If we never generated skip array scan keys, it would be possible for "gaps" * to appear that make it unsafe to mark any subsequent input scan keys @@ -127,29 +136,36 @@ static int _bt_compare_array_elements(const void *a, const void *b, void *arg); * This has the potential to be much more efficient than a full index scan * (though it behaves like a full scan when there's many distinct "x" values). * - * If possible, redundant keys are eliminated: we keep only the tightest + * Typically, redundant keys are eliminated: we keep only the tightest * >/>= bound and the tightest />= or both - * 4::int AND x > 10::bigint", and we are unable to determine - * which key is more restrictive for lack of a suitable cross-type operator. - * _bt_first will arbitrarily pick one of the keys to do the initial - * positioning with. If it picks x > 4, then the x > 10 condition will fail - * until we reach index entries > 10; but we can't stop the scan just because - * x > 10 is failing. On the other hand, if we are scanning backwards, then - * failure of either key is indeed enough to stop the scan. (In general, when - * inequality keys are present, the initial-positioning code only promises to - * position before the first possible match, not exactly at the first match, - * for a forward scan; or after the last match for a backward scan.) + * we cannot eliminate either key. + * + * When all redundant keys could not be eliminated, we'll output a key array + * that can more or less be treated as if it had no redundant keys. Suppose + * we have "x > 4::int AND x > 10::bigint AND x < 70", and we are unable to + * determine which > key is more restrictive for lack of a suitable cross-type + * operator. We'll arbitrarily pick one of the > keys; the other > key won't + * be marked required. Obviously, the scan will be less efficient if we + * choose x > 4 over x > 10 -- but it can still largely proceed as if there + * was only a single > condition. "x > 10" will be placed at the end of the + * so->keyData[] output array. It'll always be evaluated last, after the keys + * that could be marked required in the usual way (after "x > 4 AND x < 70"). + * This can sometimes result in so->keyData[] keys that aren't even in index + * attribute order (if the qual involves multiple attributes). The scan's + * required keys will still be in attribute order, though, so it can't matter. + * + * This scheme ensures that _bt_first always uses the same set of keys at the + * start of a forwards scan as those _bt_checkkeys uses to determine when to + * end a similar backwards scan (and vice-versa). _bt_advance_array_keys + * depends on this: it expects to be able to reliably predict what the next + * _bt_first call will do by testing whether _bt_checkkeys' routines report + * that the final tuple on the page is past the end of matches for the scan's + * keys with the scan direction flipped. If it is (if continuescan=false), + * then it follows that calling _bt_first will, at a minimum, relocate the + * scan to the very next leaf page (in the current scan direction). * * As a byproduct of this work, we can detect contradictory quals such * as "x = 1 AND x > 2". If we see that, we return so->qual_ok = false, @@ -165,13 +181,18 @@ static int _bt_compare_array_elements(const void *a, const void *b, void *arg); * array will generate its array elements from a range that's constrained by * any merged input inequalities (which won't get output in so->keyData[]). * - * Row comparison keys currently have a couple of notable limitations. - * Right now we just transfer them into the preprocessed array without any - * editorialization. We can treat them the same as an ordinary inequality - * comparison on the row's first index column, for the purposes of the logic - * about required keys. Also, we are unable to merge a row comparison key - * into a skip array (only ordinary inequalities are merged). A key that - * comes after a Row comparison key is therefore never marked as required. + * Row compares are treated as ordinary inequality comparisons on the row's + * first index column whenever possible. We treat their first subkey as if it + * was a simple scalar inequality for the purposes of the logic about required + * keys. This also gives us limited ability to detect contradictory/redundant + * conditions involving a row compare: we can do so whenever it involves an + * SK_ISNULL condition on a row compare's first column (the same rules used + * with simple inequalities work just as well here). We have no ability to + * detect redundant/contradictory conditions in any other row compare case. + * Note in particular that we are unable to merge a row comparison key into a + * skip array (only ordinary inequalities are merged). Any so->keyData[] key + * on a column that comes after a row comparison's first column can therefore + * never be marked as required at present. * * Note: the reason we have to copy the preprocessed scan keys into private * storage is that we are modifying the array based on comparisons of the @@ -188,7 +209,8 @@ _bt_preprocess_keys(IndexScanDesc scan) int numberOfEqualCols; ScanKey inkeys; BTScanKeyPreproc xform[BTMaxStrategyNumber]; - bool test_result; + bool test_result, + redundant_key_kept = false; AttrNumber attno; ScanKey arrayKeyData; int *keyDataMap = NULL; @@ -388,7 +410,8 @@ _bt_preprocess_keys(IndexScanDesc scan) xform[j].inkey = NULL; xform[j].inkeyi = -1; } - /* else, cannot determine redundancy, keep both keys */ + else + redundant_key_kept = true; } /* track number of attrs for which we have "=" keys */ numberOfEqualCols++; @@ -409,6 +432,8 @@ _bt_preprocess_keys(IndexScanDesc scan) else xform[BTLessStrategyNumber - 1].inkey = NULL; } + else + redundant_key_kept = true; } /* try to keep only one of >, >= */ @@ -426,6 +451,8 @@ _bt_preprocess_keys(IndexScanDesc scan) else xform[BTGreaterStrategyNumber - 1].inkey = NULL; } + else + redundant_key_kept = true; } /* @@ -466,25 +493,6 @@ _bt_preprocess_keys(IndexScanDesc scan) /* check strategy this key's operator corresponds to */ j = inkey->sk_strategy - 1; - /* if row comparison, push it directly to the output array */ - if (inkey->sk_flags & SK_ROW_HEADER) - { - ScanKey outkey = &so->keyData[new_numberOfKeys++]; - - memcpy(outkey, inkey, sizeof(ScanKeyData)); - if (arrayKeyData) - keyDataMap[new_numberOfKeys - 1] = i; - if (numberOfEqualCols == attno - 1) - _bt_mark_scankey_required(outkey); - - /* - * We don't support RowCompare using equality; such a qual would - * mess up the numberOfEqualCols tracking. - */ - Assert(j != (BTEqualStrategyNumber - 1)); - continue; - } - if (inkey->sk_strategy == BTEqualStrategyNumber && (inkey->sk_flags & SK_SEARCHARRAY)) { @@ -593,9 +601,8 @@ _bt_preprocess_keys(IndexScanDesc scan) * the new scan key. * * Note: We do things this way around so that our arrays are - * always in the same order as their corresponding scan keys, - * even with incomplete opfamilies. _bt_advance_array_keys - * depends on this. + * always in the same order as their corresponding scan keys. + * _bt_preprocess_array_keys_final expects this. */ ScanKey outkey = &so->keyData[new_numberOfKeys++]; @@ -607,6 +614,7 @@ _bt_preprocess_keys(IndexScanDesc scan) xform[j].inkey = inkey; xform[j].inkeyi = i; xform[j].arrayidx = arrayidx; + redundant_key_kept = true; } } } @@ -622,6 +630,15 @@ _bt_preprocess_keys(IndexScanDesc scan) if (arrayKeyData) _bt_preprocess_array_keys_final(scan, keyDataMap); + /* + * If there are remaining redundant inequality keys, we must make sure + * that each index attribute has no more than one required >/>= key, and + * no more than one required qual_ok) + _bt_unmark_keys(scan, keyDataMap); + /* Could pfree arrayKeyData/keyDataMap now, but not worth the cycles */ } @@ -746,9 +763,12 @@ _bt_fix_scankey_strategy(ScanKey skey, int16 *indoption) * * Depending on the operator type, the key may be required for both scan * directions or just one. Also, if the key is a row comparison header, - * we have to mark its first subsidiary ScanKey as required. (Subsequent - * subsidiary ScanKeys are normally for lower-order columns, and thus - * cannot be required, since they're after the first non-equality scankey.) + * we have to mark the appropriate subsidiary ScanKeys as required. In such + * cases, the first subsidiary key is required, but subsequent ones are + * required only as long as they correspond to successive index columns and + * match the leading column as to sort direction. Otherwise the row + * comparison ordering is different from the index ordering and so we can't + * stop the scan on the basis of those lower-order columns. * * Note: when we set required-key flag bits in a subsidiary scankey, we are * scribbling on a data structure belonging to the index AM's caller, not on @@ -786,12 +806,25 @@ _bt_mark_scankey_required(ScanKey skey) if (skey->sk_flags & SK_ROW_HEADER) { ScanKey subkey = (ScanKey) DatumGetPointer(skey->sk_argument); + AttrNumber attno = skey->sk_attno; /* First subkey should be same column/operator as the header */ - Assert(subkey->sk_flags & SK_ROW_MEMBER); - Assert(subkey->sk_attno == skey->sk_attno); + Assert(subkey->sk_attno == attno); Assert(subkey->sk_strategy == skey->sk_strategy); - subkey->sk_flags |= addflags; + + for (;;) + { + Assert(subkey->sk_flags & SK_ROW_MEMBER); + if (subkey->sk_attno != attno) + break; /* non-adjacent key, so not required */ + if (subkey->sk_strategy != skey->sk_strategy) + break; /* wrong direction, so not required */ + subkey->sk_flags |= addflags; + if (subkey->sk_flags & SK_ROW_END) + break; + subkey++; + attno++; + } } } @@ -847,8 +880,7 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op, cmp_op; StrategyNumber strat; - Assert(!((leftarg->sk_flags | rightarg->sk_flags) & - (SK_ROW_HEADER | SK_ROW_MEMBER))); + Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_ROW_MEMBER)); /* * First, deal with cases where one or both args are NULL. This should @@ -924,6 +956,16 @@ _bt_compare_scankey_args(IndexScanDesc scan, ScanKey op, return true; } + /* + * We don't yet know how to determine redundancy when it involves a row + * compare key (barring simple cases involving IS NULL/IS NOT NULL) + */ + if ((leftarg->sk_flags | rightarg->sk_flags) & SK_ROW_HEADER) + { + Assert(!((leftarg->sk_flags | rightarg->sk_flags) & SK_BT_SKIP)); + return false; + } + /* * If either leftarg or rightarg are equality-type array scankeys, we need * specialized handling (since by now we know that IS NULL wasn't used) @@ -1156,7 +1198,7 @@ _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey, { case BTLessStrategyNumber: cmpexact = 1; /* exclude exact match, if any */ - /* FALL THRU */ + pg_fallthrough; case BTLessEqualStrategyNumber: if (cmpresult >= cmpexact) matchelem++; @@ -1178,7 +1220,7 @@ _bt_saoparray_shrink(IndexScanDesc scan, ScanKey arraysk, ScanKey skey, break; case BTGreaterEqualStrategyNumber: cmpexact = 1; /* include exact match, if any */ - /* FALL THRU */ + pg_fallthrough; case BTGreaterStrategyNumber: if (cmpresult >= cmpexact) matchelem++; @@ -1364,7 +1406,7 @@ _bt_skiparray_strat_adjust(IndexScanDesc scan, ScanKey arraysk, } /* - * Convert skip array's > low_compare key into a >= key + * Convert skip array's < high_compare key into a <= key */ static void _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk, @@ -1379,6 +1421,7 @@ _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk, Datum orig_sk_argument = high_compare->sk_argument, new_sk_argument; bool uflow; + int16 lookupstrat; Assert(high_compare->sk_strategy == BTLessStrategyNumber); @@ -1400,9 +1443,14 @@ _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk, return; } - /* Look up <= operator (might fail) */ - leop = get_opfamily_member(opfamily, opcintype, opcintype, - BTLessEqualStrategyNumber); + /* + * Look up <= operator (might fail), accounting for the fact that a + * high_compare on a DESC column already had its strategy commuted + */ + lookupstrat = BTLessEqualStrategyNumber; + if (high_compare->sk_flags & SK_BT_DESC) + lookupstrat = BTGreaterEqualStrategyNumber; /* commute this too */ + leop = get_opfamily_member(opfamily, opcintype, opcintype, lookupstrat); if (!OidIsValid(leop)) return; cmp_proc = get_opcode(leop); @@ -1416,7 +1464,7 @@ _bt_skiparray_strat_decrement(IndexScanDesc scan, ScanKey arraysk, } /* - * Convert skip array's < low_compare key into a <= key + * Convert skip array's > low_compare key into a >= key */ static void _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk, @@ -1431,6 +1479,7 @@ _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk, Datum orig_sk_argument = low_compare->sk_argument, new_sk_argument; bool oflow; + int16 lookupstrat; Assert(low_compare->sk_strategy == BTGreaterStrategyNumber); @@ -1452,9 +1501,14 @@ _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk, return; } - /* Look up >= operator (might fail) */ - geop = get_opfamily_member(opfamily, opcintype, opcintype, - BTGreaterEqualStrategyNumber); + /* + * Look up >= operator (might fail), accounting for the fact that a + * low_compare on a DESC column already had its strategy commuted + */ + lookupstrat = BTGreaterEqualStrategyNumber; + if (low_compare->sk_flags & SK_BT_DESC) + lookupstrat = BTLessEqualStrategyNumber; /* commute this too */ + geop = get_opfamily_member(opfamily, opcintype, opcintype, lookupstrat); if (!OidIsValid(geop)) return; cmp_proc = get_opcode(geop); @@ -1467,6 +1521,283 @@ _bt_skiparray_strat_increment(IndexScanDesc scan, ScanKey arraysk, } } +/* + * _bt_unmark_keys() -- make superfluous required keys nonrequired after all + * + * When _bt_preprocess_keys fails to eliminate one or more redundant keys, it + * calls here to make sure that no index attribute has more than one > or >= + * key marked required, and no more than one required < or <= key. Attributes + * with = keys will always get one = key as their required key. All other + * keys that were initially marked required get "unmarked" here. That way, + * _bt_first and _bt_checkkeys will reliably agree on which keys to use to + * start and/or to end the scan. + * + * We also relocate keys that become/started out nonrequired to the end of + * so->keyData[]. That way, _bt_first and _bt_checkkeys cannot fail to reach + * a required key due to some earlier nonrequired key getting in the way. + * + * Only call here when _bt_compare_scankey_args returned false at least once + * (otherwise, calling here will just waste cycles). + */ +static void +_bt_unmark_keys(IndexScanDesc scan, int *keyDataMap) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + AttrNumber attno; + bool *unmarkikey; + int nunmark, + nunmarked, + nkept, + firsti; + ScanKey keepKeys, + unmarkKeys; + FmgrInfo *keepOrderProcs = NULL, + *unmarkOrderProcs = NULL; + bool haveReqEquals, + haveReqForward, + haveReqBackward; + + /* + * Do an initial pass over so->keyData[] that determines which keys to + * keep as required. We expect so->keyData[] to still be in attribute + * order when we're called (though we don't expect any particular order + * among each attribute's keys). + * + * When both equality and inequality keys remain on a single attribute, we + * *must* make sure that exactly one of the equalities remains required. + * Any requiredness markings that we might leave on later keys/attributes + * are predicated on there being required = keys on all prior columns. + */ + unmarkikey = palloc0(so->numberOfKeys * sizeof(bool)); + nunmark = 0; + + /* Set things up for first key's attribute */ + attno = so->keyData[0].sk_attno; + firsti = 0; + haveReqEquals = false; + haveReqForward = false; + haveReqBackward = false; + for (int i = 0; i < so->numberOfKeys; i++) + { + ScanKey origkey = &so->keyData[i]; + + if (origkey->sk_attno != attno) + { + /* Reset for next attribute */ + attno = origkey->sk_attno; + firsti = i; + + haveReqEquals = false; + haveReqForward = false; + haveReqBackward = false; + } + + /* Equalities get priority over inequalities */ + if (haveReqEquals) + { + /* + * We already found the first "=" key for this attribute. We've + * already decided that all its other keys will be unmarked. + */ + Assert(!(origkey->sk_flags & SK_SEARCHNULL)); + unmarkikey[i] = true; + nunmark++; + continue; + } + else if ((origkey->sk_flags & SK_BT_REQFWD) && + (origkey->sk_flags & SK_BT_REQBKWD)) + { + /* + * Found the first "=" key for attno. All other attno keys will + * be unmarked. + */ + Assert(origkey->sk_strategy == BTEqualStrategyNumber); + + haveReqEquals = true; + for (int j = firsti; j < i; j++) + { + /* Unmark any prior inequality keys on attno after all */ + if (!unmarkikey[j]) + { + unmarkikey[j] = true; + nunmark++; + } + } + continue; + } + + /* Deal with inequalities next */ + if ((origkey->sk_flags & SK_BT_REQFWD) && !haveReqForward) + { + haveReqForward = true; + continue; + } + else if ((origkey->sk_flags & SK_BT_REQBKWD) && !haveReqBackward) + { + haveReqBackward = true; + continue; + } + + /* + * We have either a redundant inequality key that will be unmarked, or + * we have a key that wasn't marked required in the first place + */ + unmarkikey[i] = true; + nunmark++; + } + + /* Should only be called when _bt_compare_scankey_args reported failure */ + Assert(nunmark > 0); + + /* + * Next, allocate temp arrays: one for required keys that'll remain + * required, the other for all remaining keys + */ + unmarkKeys = palloc(nunmark * sizeof(ScanKeyData)); + keepKeys = palloc((so->numberOfKeys - nunmark) * sizeof(ScanKeyData)); + nunmarked = 0; + nkept = 0; + if (so->numArrayKeys) + { + unmarkOrderProcs = palloc(nunmark * sizeof(FmgrInfo)); + keepOrderProcs = palloc((so->numberOfKeys - nunmark) * sizeof(FmgrInfo)); + } + + /* + * Next, copy the contents of so->keyData[] into the appropriate temp + * array. + * + * Scans with = array keys need us to maintain invariants around the order + * of so->orderProcs[] and so->arrayKeys[] relative to so->keyData[]. See + * _bt_preprocess_array_keys_final for a full explanation. + */ + for (int i = 0; i < so->numberOfKeys; i++) + { + ScanKey origkey = &so->keyData[i]; + ScanKey unmark; + + if (!unmarkikey[i]) + { + /* + * Key gets to keep its original requiredness markings. + * + * Key will stay in its original position, unless we're going to + * unmark an earlier key (in which case this key gets moved back). + */ + memcpy(keepKeys + nkept, origkey, sizeof(ScanKeyData)); + + if (so->numArrayKeys) + { + keyDataMap[i] = nkept; + memcpy(keepOrderProcs + nkept, &so->orderProcs[i], + sizeof(FmgrInfo)); + } + + nkept++; + continue; + } + + /* + * Key will be unmarked as needed, and moved to the end of the array, + * next to other keys that will become (or always were) nonrequired + */ + unmark = unmarkKeys + nunmarked; + memcpy(unmark, origkey, sizeof(ScanKeyData)); + + if (so->numArrayKeys) + { + keyDataMap[i] = (so->numberOfKeys - nunmark) + nunmarked; + memcpy(&unmarkOrderProcs[nunmarked], &so->orderProcs[i], + sizeof(FmgrInfo)); + } + + /* + * Preprocessing only generates skip arrays when it knows that they'll + * be the only required = key on the attr. We'll never unmark them. + */ + Assert(!(unmark->sk_flags & SK_BT_SKIP)); + + /* + * Also shouldn't have to unmark an IS NULL or an IS NOT NULL key. + * They aren't cross-type, so an incomplete opfamily can't matter. + */ + Assert(!(unmark->sk_flags & SK_ISNULL) || + !(unmark->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))); + + /* Clear requiredness flags on redundant key (and on any subkeys) */ + unmark->sk_flags &= ~(SK_BT_REQFWD | SK_BT_REQBKWD); + if (unmark->sk_flags & SK_ROW_HEADER) + { + ScanKey subkey = (ScanKey) DatumGetPointer(unmark->sk_argument); + + Assert(subkey->sk_strategy == unmark->sk_strategy); + for (;;) + { + Assert(subkey->sk_flags & SK_ROW_MEMBER); + subkey->sk_flags &= ~(SK_BT_REQFWD | SK_BT_REQBKWD); + if (subkey->sk_flags & SK_ROW_END) + break; + subkey++; + } + } + + nunmarked++; + } + + /* Copy both temp arrays back into so->keyData[] to reorder */ + Assert(nkept == so->numberOfKeys - nunmark); + Assert(nunmarked == nunmark); + memcpy(so->keyData, keepKeys, sizeof(ScanKeyData) * nkept); + memcpy(so->keyData + nkept, unmarkKeys, sizeof(ScanKeyData) * nunmarked); + + /* Done with temp arrays */ + pfree(unmarkikey); + pfree(keepKeys); + pfree(unmarkKeys); + + /* + * Now copy so->orderProcs[] temp entries needed by scans with = array + * keys back (just like with the so->keyData[] temp arrays) + */ + if (so->numArrayKeys) + { + memcpy(so->orderProcs, keepOrderProcs, sizeof(FmgrInfo) * nkept); + memcpy(so->orderProcs + nkept, unmarkOrderProcs, + sizeof(FmgrInfo) * nunmarked); + + /* Also fix-up array->scan_key references */ + for (int arridx = 0; arridx < so->numArrayKeys; arridx++) + { + BTArrayKeyInfo *array = &so->arrayKeys[arridx]; + + array->scan_key = keyDataMap[array->scan_key]; + } + + /* + * Sort so->arrayKeys[] based on its new BTArrayKeyInfo.scan_key + * offsets, so that its order matches so->keyData[] order as expected + */ + qsort(so->arrayKeys, so->numArrayKeys, sizeof(BTArrayKeyInfo), + _bt_reorder_array_cmp); + + /* Done with temp arrays */ + pfree(unmarkOrderProcs); + pfree(keepOrderProcs); + } +} + +/* + * qsort comparator for reordering so->arrayKeys[] BTArrayKeyInfo entries + */ +static int +_bt_reorder_array_cmp(const void *a, const void *b) +{ + const BTArrayKeyInfo *arraya = a; + const BTArrayKeyInfo *arrayb = b; + + return pg_cmp_s32(arraya->scan_key, arrayb->scan_key); +} + /* * _bt_preprocess_array_keys() -- Preprocess SK_SEARCHARRAY scan keys * @@ -1532,6 +1863,7 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys) * (also checks if we should add extra skip arrays based on input keys) */ numArrayKeys = _bt_num_array_keys(scan, skip_eq_ops, &numSkipArrayKeys); + so->skipScan = (numSkipArrayKeys > 0); /* Quit if nothing to do. */ if (numArrayKeys == 0) @@ -1561,7 +1893,6 @@ _bt_preprocess_array_keys(IndexScanDesc scan, int *new_numberOfKeys) arrayKeyData = (ScanKey) palloc(numArrayKeyData * sizeof(ScanKeyData)); /* Allocate space for per-array data in the workspace context */ - so->skipScan = (numSkipArrayKeys > 0); so->arrayKeys = (BTArrayKeyInfo *) palloc(numArrayKeys * sizeof(BTArrayKeyInfo)); /* Allocate space for ORDER procs used to help _bt_checkkeys */ @@ -2247,7 +2578,7 @@ _bt_num_array_keys(IndexScanDesc scan, Oid *skip_eq_ops_out, static Datum _bt_find_extreme_element(IndexScanDesc scan, ScanKey skey, Oid elemtype, StrategyNumber strat, - Datum *elems, int nelems) + const Datum *elems, int nelems) { Relation rel = scan->indexRelation; Oid cmp_op; diff --git a/src/backend/access/nbtree/nbtreadpage.c b/src/backend/access/nbtree/nbtreadpage.c new file mode 100644 index 0000000000000..2ba1ca6602334 --- /dev/null +++ b/src/backend/access/nbtree/nbtreadpage.c @@ -0,0 +1,3718 @@ +/*------------------------------------------------------------------------- + * + * nbtreadpage.c + * Leaf page reading for btree index scans. + * + * NOTES + * This file contains code to return items that satisfy the scan's + * search-type scan keys within caller-supplied btree leaf page. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/access/nbtree/nbtreadpage.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/nbtree.h" +#include "access/relscan.h" +#include "storage/predicate.h" +#include "utils/datum.h" +#include "utils/rel.h" + + +/* + * _bt_readpage state used across _bt_checkkeys calls for a page + */ +typedef struct BTReadPageState +{ + /* Input parameters, set by _bt_readpage for _bt_checkkeys */ + ScanDirection dir; /* current scan direction */ + OffsetNumber minoff; /* Lowest non-pivot tuple's offset */ + OffsetNumber maxoff; /* Highest non-pivot tuple's offset */ + IndexTuple finaltup; /* Needed by scans with array keys */ + Page page; /* Page being read */ + bool firstpage; /* page is first for primitive scan? */ + bool forcenonrequired; /* treat all keys as nonrequired? */ + int startikey; /* start comparisons from this scan key */ + + /* Per-tuple input parameters, set by _bt_readpage for _bt_checkkeys */ + OffsetNumber offnum; /* current tuple's page offset number */ + + /* Output parameters, set by _bt_checkkeys for _bt_readpage */ + OffsetNumber skip; /* Array keys "look ahead" skip offnum */ + bool continuescan; /* Terminate ongoing (primitive) index scan? */ + + /* + * Private _bt_checkkeys state used to manage "look ahead" optimization + * and primscan scheduling (only used during scans with array keys) + */ + int16 rechecks; + int16 targetdistance; + int16 nskipadvances; + +} BTReadPageState; + + +static void _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate); +static bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir, + IndexTuple finaltup); +static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir, + IndexTuple finaltup); +static void _bt_saveitem(BTScanOpaque so, int itemIndex, + OffsetNumber offnum, IndexTuple itup); +static int _bt_setuppostingitems(BTScanOpaque so, int itemIndex, + OffsetNumber offnum, const ItemPointerData *heapTid, + IndexTuple itup); +static inline void _bt_savepostingitem(BTScanOpaque so, int itemIndex, + OffsetNumber offnum, + ItemPointer heapTid, int tupleOffset); +static bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys, + IndexTuple tuple, int tupnatts); +static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir, + IndexTuple tuple, int tupnatts, TupleDesc tupdesc, + bool advancenonrequired, bool forcenonrequired, + bool *continuescan, int *ikey); +static bool _bt_check_rowcompare(ScanKey header, + IndexTuple tuple, int tupnatts, TupleDesc tupdesc, + ScanDirection dir, bool forcenonrequired, bool *continuescan); +static bool _bt_rowcompare_cmpresult(ScanKey subkey, int cmpresult); +static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir, + IndexTuple tuple, TupleDesc tupdesc, int tupnatts, + bool readpagetup, int sktrig, bool *scanBehind); +static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, + int tupnatts, TupleDesc tupdesc); +static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate, + IndexTuple tuple, int tupnatts, TupleDesc tupdesc, + int sktrig, bool sktrig_required); +static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir, + bool *skip_array_set); +static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array); +static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array); +static void _bt_array_set_low_or_high(Relation rel, ScanKey skey, + BTArrayKeyInfo *array, bool low_not_high); +static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array, + int32 set_elem_result, Datum tupdatum, bool tupnull); +static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array); +static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc, + Datum tupdatum, bool tupnull, + Datum arrdatum, ScanKey cur); +static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir, + Datum tupdatum, bool tupnull, + BTArrayKeyInfo *array, ScanKey cur, + int32 *set_elem_result); +#ifdef USE_ASSERT_CHECKING +static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan); +#endif + + +/* + * _bt_readpage() -- Load data from current index page into so->currPos + * + * Caller must have pinned and read-locked so->currPos.buf; the buffer's state + * is not changed here. Also, currPos.moreLeft and moreRight must be valid; + * they are updated as appropriate. All other fields of so->currPos are + * initialized from scratch here. + * + * We scan the current page starting at offnum and moving in the indicated + * direction. All items matching the scan keys are loaded into currPos.items. + * moreLeft or moreRight (as appropriate) is cleared if _bt_checkkeys reports + * that there can be no more matching tuples in the current scan direction + * (could just be for the current primitive index scan when scan has arrays). + * + * In the case of a parallel scan, caller must have called _bt_parallel_seize + * prior to calling this function; this function will invoke + * _bt_parallel_release before returning. + * + * Returns true if any matching items found on the page, false if none. + */ +bool +_bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum, + bool firstpage) +{ + Relation rel = scan->indexRelation; + BTScanOpaque so = (BTScanOpaque) scan->opaque; + Page page; + BTPageOpaque opaque; + OffsetNumber minoff; + OffsetNumber maxoff; + BTReadPageState pstate; + bool arrayKeys, + ignore_killed_tuples = scan->ignore_killed_tuples; + int itemIndex, + indnatts; + + /* save the page/buffer block number, along with its sibling links */ + page = BufferGetPage(so->currPos.buf); + opaque = BTPageGetOpaque(page); + so->currPos.currPage = BufferGetBlockNumber(so->currPos.buf); + so->currPos.prevPage = opaque->btpo_prev; + so->currPos.nextPage = opaque->btpo_next; + /* delay setting so->currPos.lsn until _bt_drop_lock_and_maybe_pin */ + pstate.dir = so->currPos.dir = dir; + so->currPos.nextTupleOffset = 0; + + /* either moreRight or moreLeft should be set now (may be unset later) */ + Assert(ScanDirectionIsForward(dir) ? so->currPos.moreRight : + so->currPos.moreLeft); + Assert(!P_IGNORE(opaque)); + Assert(BTScanPosIsPinned(so->currPos)); + Assert(!so->needPrimScan); + + /* initialize local variables */ + indnatts = IndexRelationGetNumberOfAttributes(rel); + arrayKeys = so->numArrayKeys != 0; + minoff = P_FIRSTDATAKEY(opaque); + maxoff = PageGetMaxOffsetNumber(page); + + /* initialize page-level state that we'll pass to _bt_checkkeys */ + pstate.minoff = minoff; + pstate.maxoff = maxoff; + pstate.finaltup = NULL; + pstate.page = page; + pstate.firstpage = firstpage; + pstate.forcenonrequired = false; + pstate.startikey = 0; + pstate.offnum = InvalidOffsetNumber; + pstate.skip = InvalidOffsetNumber; + pstate.continuescan = true; /* default assumption */ + pstate.rechecks = 0; + pstate.targetdistance = 0; + pstate.nskipadvances = 0; + + if (scan->parallel_scan) + { + /* allow next/prev page to be read by other worker without delay */ + if (ScanDirectionIsForward(dir)) + _bt_parallel_release(scan, so->currPos.nextPage, + so->currPos.currPage); + else + _bt_parallel_release(scan, so->currPos.prevPage, + so->currPos.currPage); + } + + PredicateLockPage(rel, so->currPos.currPage, scan->xs_snapshot); + + if (ScanDirectionIsForward(dir)) + { + /* SK_SEARCHARRAY forward scans must provide high key up front */ + if (arrayKeys) + { + if (!P_RIGHTMOST(opaque)) + { + ItemId iid = PageGetItemId(page, P_HIKEY); + + pstate.finaltup = (IndexTuple) PageGetItem(page, iid); + + if (unlikely(so->scanBehind) && + !_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup)) + { + /* Schedule another primitive index scan after all */ + so->currPos.moreRight = false; + so->needPrimScan = true; + if (scan->parallel_scan) + _bt_parallel_primscan_schedule(scan, + so->currPos.currPage); + return false; + } + } + + so->scanBehind = so->oppositeDirCheck = false; /* reset */ + } + + /* + * Consider pstate.startikey optimization once the ongoing primitive + * index scan has already read at least one page + */ + if (!pstate.firstpage && minoff < maxoff) + _bt_set_startikey(scan, &pstate); + + /* load items[] in ascending order */ + itemIndex = 0; + + offnum = Max(offnum, minoff); + + while (offnum <= maxoff) + { + ItemId iid = PageGetItemId(page, offnum); + IndexTuple itup; + bool passes_quals; + + /* + * If the scan specifies not to return killed tuples, then we + * treat a killed tuple as not passing the qual + */ + if (ignore_killed_tuples && ItemIdIsDead(iid)) + { + offnum = OffsetNumberNext(offnum); + continue; + } + + itup = (IndexTuple) PageGetItem(page, iid); + Assert(!BTreeTupleIsPivot(itup)); + + pstate.offnum = offnum; + passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys, + itup, indnatts); + + /* + * Check if we need to skip ahead to a later tuple (only possible + * when the scan uses array keys) + */ + if (arrayKeys && OffsetNumberIsValid(pstate.skip)) + { + Assert(!passes_quals && pstate.continuescan); + Assert(offnum < pstate.skip); + Assert(!pstate.forcenonrequired); + + offnum = pstate.skip; + pstate.skip = InvalidOffsetNumber; + continue; + } + + if (passes_quals) + { + /* tuple passes all scan key conditions */ + if (!BTreeTupleIsPosting(itup)) + { + /* Remember it */ + _bt_saveitem(so, itemIndex, offnum, itup); + itemIndex++; + } + else + { + int tupleOffset; + + /* Set up posting list state (and remember first TID) */ + tupleOffset = + _bt_setuppostingitems(so, itemIndex, offnum, + BTreeTupleGetPostingN(itup, 0), + itup); + itemIndex++; + + /* Remember all later TIDs (must be at least one) */ + for (int i = 1; i < BTreeTupleGetNPosting(itup); i++) + { + _bt_savepostingitem(so, itemIndex, offnum, + BTreeTupleGetPostingN(itup, i), + tupleOffset); + itemIndex++; + } + } + } + /* When !continuescan, there can't be any more matches, so stop */ + if (!pstate.continuescan) + break; + + offnum = OffsetNumberNext(offnum); + } + + /* + * We don't need to visit page to the right when the high key + * indicates that no more matches will be found there. + * + * Checking the high key like this works out more often than you might + * think. Leaf page splits pick a split point between the two most + * dissimilar tuples (this is weighed against the need to evenly share + * free space). Leaf pages with high key attribute values that can + * only appear on non-pivot tuples on the right sibling page are + * common. + */ + if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque)) + { + ItemId iid = PageGetItemId(page, P_HIKEY); + IndexTuple itup = (IndexTuple) PageGetItem(page, iid); + int truncatt; + + /* Reset arrays, per _bt_set_startikey contract */ + if (pstate.forcenonrequired) + _bt_start_array_keys(scan, dir); + pstate.forcenonrequired = false; + pstate.startikey = 0; /* _bt_set_startikey ignores P_HIKEY */ + + truncatt = BTreeTupleGetNAtts(itup, rel); + _bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt); + } + + if (!pstate.continuescan) + so->currPos.moreRight = false; + + Assert(itemIndex <= MaxTIDsPerBTreePage); + so->currPos.firstItem = 0; + so->currPos.lastItem = itemIndex - 1; + so->currPos.itemIndex = 0; + } + else + { + /* SK_SEARCHARRAY backward scans must provide final tuple up front */ + if (arrayKeys) + { + if (minoff <= maxoff && !P_LEFTMOST(opaque)) + { + ItemId iid = PageGetItemId(page, minoff); + + pstate.finaltup = (IndexTuple) PageGetItem(page, iid); + + if (unlikely(so->scanBehind) && + !_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup)) + { + /* Schedule another primitive index scan after all */ + so->currPos.moreLeft = false; + so->needPrimScan = true; + if (scan->parallel_scan) + _bt_parallel_primscan_schedule(scan, + so->currPos.currPage); + return false; + } + } + + so->scanBehind = so->oppositeDirCheck = false; /* reset */ + } + + /* + * Consider pstate.startikey optimization once the ongoing primitive + * index scan has already read at least one page + */ + if (!pstate.firstpage && minoff < maxoff) + _bt_set_startikey(scan, &pstate); + + /* load items[] in descending order */ + itemIndex = MaxTIDsPerBTreePage; + + offnum = Min(offnum, maxoff); + + while (offnum >= minoff) + { + ItemId iid = PageGetItemId(page, offnum); + IndexTuple itup; + bool tuple_alive; + bool passes_quals; + + /* + * If the scan specifies not to return killed tuples, then we + * treat a killed tuple as not passing the qual. Most of the + * time, it's a win to not bother examining the tuple's index + * keys, but just skip to the next tuple (previous, actually, + * since we're scanning backwards). However, if this is the first + * tuple on the page, we do check the index keys, to prevent + * uselessly advancing to the page to the left. This is similar + * to the high key optimization used by forward scans. + */ + if (ignore_killed_tuples && ItemIdIsDead(iid)) + { + if (offnum > minoff) + { + offnum = OffsetNumberPrev(offnum); + continue; + } + + tuple_alive = false; + } + else + tuple_alive = true; + + itup = (IndexTuple) PageGetItem(page, iid); + Assert(!BTreeTupleIsPivot(itup)); + + pstate.offnum = offnum; + if (arrayKeys && offnum == minoff && pstate.forcenonrequired) + { + /* Reset arrays, per _bt_set_startikey contract */ + pstate.forcenonrequired = false; + pstate.startikey = 0; + _bt_start_array_keys(scan, dir); + } + passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys, + itup, indnatts); + + if (arrayKeys && so->scanBehind) + { + /* + * Done scanning this page, but not done with the current + * primscan. + * + * Note: Forward scans don't check this explicitly, since they + * prefer to reuse pstate.skip for this instead. + */ + Assert(!passes_quals && pstate.continuescan); + Assert(!pstate.forcenonrequired); + + break; + } + + /* + * Check if we need to skip ahead to a later tuple (only possible + * when the scan uses array keys) + */ + if (arrayKeys && OffsetNumberIsValid(pstate.skip)) + { + Assert(!passes_quals && pstate.continuescan); + Assert(offnum > pstate.skip); + Assert(!pstate.forcenonrequired); + + offnum = pstate.skip; + pstate.skip = InvalidOffsetNumber; + continue; + } + + if (passes_quals && tuple_alive) + { + /* tuple passes all scan key conditions */ + if (!BTreeTupleIsPosting(itup)) + { + /* Remember it */ + itemIndex--; + _bt_saveitem(so, itemIndex, offnum, itup); + } + else + { + uint16 nitems = BTreeTupleGetNPosting(itup); + int tupleOffset; + + /* Set up posting list state (and remember last TID) */ + itemIndex--; + tupleOffset = + _bt_setuppostingitems(so, itemIndex, offnum, + BTreeTupleGetPostingN(itup, nitems - 1), + itup); + + /* Remember all prior TIDs (must be at least one) */ + for (int i = nitems - 2; i >= 0; i--) + { + itemIndex--; + _bt_savepostingitem(so, itemIndex, offnum, + BTreeTupleGetPostingN(itup, i), + tupleOffset); + } + } + } + /* When !continuescan, there can't be any more matches, so stop */ + if (!pstate.continuescan) + break; + + offnum = OffsetNumberPrev(offnum); + } + + /* + * We don't need to visit page to the left when no more matches will + * be found there + */ + if (!pstate.continuescan) + so->currPos.moreLeft = false; + + Assert(itemIndex >= 0); + so->currPos.firstItem = itemIndex; + so->currPos.lastItem = MaxTIDsPerBTreePage - 1; + so->currPos.itemIndex = MaxTIDsPerBTreePage - 1; + } + + /* + * If _bt_set_startikey told us to temporarily treat the scan's keys as + * nonrequired (possible only during scans with array keys), there must be + * no lasting consequences for the scan's array keys. The scan's arrays + * should now have exactly the same elements as they would have had if the + * nonrequired behavior had never been used. (In general, a scan's arrays + * are expected to track its progress through the index's key space.) + * + * We are required (by _bt_set_startikey) to call _bt_checkkeys against + * pstate.finaltup with pstate.forcenonrequired=false to allow the scan's + * arrays to recover. Assert that that step hasn't been missed. + */ + Assert(!pstate.forcenonrequired); + + return (so->currPos.firstItem <= so->currPos.lastItem); +} + +/* + * _bt_start_array_keys() -- Initialize array keys at start of a scan + * + * Set up the cur_elem counters and fill in the first sk_argument value for + * each array scankey. + */ +void +_bt_start_array_keys(IndexScanDesc scan, ScanDirection dir) +{ + Relation rel = scan->indexRelation; + BTScanOpaque so = (BTScanOpaque) scan->opaque; + + Assert(so->numArrayKeys); + Assert(so->qual_ok); + + for (int i = 0; i < so->numArrayKeys; i++) + { + BTArrayKeyInfo *array = &so->arrayKeys[i]; + ScanKey skey = &so->keyData[array->scan_key]; + + Assert(skey->sk_flags & SK_SEARCHARRAY); + + _bt_array_set_low_or_high(rel, skey, array, + ScanDirectionIsForward(dir)); + } + so->scanBehind = so->oppositeDirCheck = false; /* reset */ +} + +/* + * Determines an offset to the first scan key (an so->keyData[]-wise offset) + * that is _not_ guaranteed to be satisfied by every tuple from pstate.page, + * which is set in pstate.startikey for _bt_checkkeys calls for the page. + * This allows caller to save cycles on comparisons of a prefix of keys while + * reading pstate.page. + * + * Also determines if later calls to _bt_checkkeys (for pstate.page) should be + * forced to treat all required scan keys >= pstate.startikey as nonrequired + * (that is, if they're to be treated as if any SK_BT_REQFWD/SK_BT_REQBKWD + * markings that were set by preprocessing were not set at all, for the + * duration of _bt_checkkeys calls prior to the call for pstate.finaltup). + * This is indicated to caller by setting pstate.forcenonrequired. + * + * Call here at the start of reading a leaf page beyond the first one for the + * primitive index scan. We consider all non-pivot tuples, so it doesn't make + * sense to call here when only a subset of those tuples can ever be read. + * This is also a good idea on performance grounds; not calling here when on + * the first page (first for the current primitive scan) avoids wasting cycles + * during selective point queries. They typically don't stand to gain as much + * when we can set pstate.startikey, and are likely to notice the overhead of + * calling here. (Also, allowing pstate.forcenonrequired to be set on a + * primscan's first page would mislead _bt_advance_array_keys, which expects + * pstate.nskipadvances to be representative of every first page's key space.) + * + * Caller must call _bt_start_array_keys and reset startikey/forcenonrequired + * ahead of the finaltup _bt_checkkeys call when we set forcenonrequired=true. + * This will give _bt_checkkeys the opportunity to call _bt_advance_array_keys + * with sktrig_required=true, restoring the invariant that the scan's required + * arrays always track the scan's progress through the index's key space. + * Caller won't need to do this on the rightmost/leftmost page in the index + * (where pstate.finaltup isn't ever set), since forcenonrequired will never + * be set here in the first place. + */ +static void +_bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + Relation rel = scan->indexRelation; + TupleDesc tupdesc = RelationGetDescr(rel); + ItemId iid; + IndexTuple firsttup, + lasttup; + int startikey = 0, + arrayidx = 0, + firstchangingattnum; + bool start_past_saop_eq = false; + + Assert(!so->scanBehind); + Assert(pstate->minoff < pstate->maxoff); + Assert(!pstate->firstpage); + Assert(pstate->startikey == 0); + Assert(!so->numArrayKeys || pstate->finaltup || + P_RIGHTMOST(BTPageGetOpaque(pstate->page)) || + P_LEFTMOST(BTPageGetOpaque(pstate->page))); + + if (so->numberOfKeys == 0) + return; + + /* minoff is an offset to the lowest non-pivot tuple on the page */ + iid = PageGetItemId(pstate->page, pstate->minoff); + firsttup = (IndexTuple) PageGetItem(pstate->page, iid); + + /* maxoff is an offset to the highest non-pivot tuple on the page */ + iid = PageGetItemId(pstate->page, pstate->maxoff); + lasttup = (IndexTuple) PageGetItem(pstate->page, iid); + + /* Determine the first attribute whose values change on caller's page */ + firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup); + + for (; startikey < so->numberOfKeys; startikey++) + { + ScanKey key = so->keyData + startikey; + BTArrayKeyInfo *array; + Datum firstdatum, + lastdatum; + bool firstnull, + lastnull; + int32 result; + + /* + * Determine if it's safe to set pstate.startikey to an offset to a + * key that comes after this key, by examining this key + */ + if (key->sk_flags & SK_ROW_HEADER) + { + /* RowCompare inequality (header key) */ + ScanKey subkey = (ScanKey) DatumGetPointer(key->sk_argument); + bool satisfied = false; + + for (;;) + { + int cmpresult; + bool firstsatisfies = false; + + if (subkey->sk_attno > firstchangingattnum) /* >, not >= */ + break; /* unsafe, preceding attr has multiple + * distinct values */ + + if (subkey->sk_flags & SK_ISNULL) + break; /* unsafe, unsatisfiable NULL subkey arg */ + + firstdatum = index_getattr(firsttup, subkey->sk_attno, + tupdesc, &firstnull); + lastdatum = index_getattr(lasttup, subkey->sk_attno, + tupdesc, &lastnull); + + if (firstnull || lastnull) + break; /* unsafe, NULL value won't satisfy subkey */ + + /* + * Compare the first tuple's datum for this row compare member + */ + cmpresult = DatumGetInt32(FunctionCall2Coll(&subkey->sk_func, + subkey->sk_collation, + firstdatum, + subkey->sk_argument)); + if (subkey->sk_flags & SK_BT_DESC) + INVERT_COMPARE_RESULT(cmpresult); + + if (cmpresult != 0 || (subkey->sk_flags & SK_ROW_END)) + { + firstsatisfies = _bt_rowcompare_cmpresult(subkey, + cmpresult); + if (!firstsatisfies) + { + /* Unsafe, firstdatum does not satisfy subkey */ + break; + } + } + + /* + * Compare the last tuple's datum for this row compare member + */ + cmpresult = DatumGetInt32(FunctionCall2Coll(&subkey->sk_func, + subkey->sk_collation, + lastdatum, + subkey->sk_argument)); + if (subkey->sk_flags & SK_BT_DESC) + INVERT_COMPARE_RESULT(cmpresult); + + if (cmpresult != 0 || (subkey->sk_flags & SK_ROW_END)) + { + if (!firstsatisfies) + { + /* + * It's only safe to set startikey beyond the row + * compare header key when both firsttup and lasttup + * satisfy the key as a whole based on the same + * deciding subkey/attribute. That can't happen now. + */ + break; /* unsafe */ + } + + satisfied = _bt_rowcompare_cmpresult(subkey, cmpresult); + break; /* safe iff 'satisfied' is true */ + } + + /* Move on to next row member/subkey */ + if (subkey->sk_flags & SK_ROW_END) + break; /* defensive */ + subkey++; + + /* + * We deliberately don't check if the next subkey has the same + * strategy as this iteration's subkey (which happens when + * subkeys for both ASC and DESC columns are used together), + * nor if any subkey is marked required. This is safe because + * in general all prior index attributes must have only one + * distinct value (across all of the tuples on the page) in + * order for us to even consider any subkey's attribute. + */ + } + + if (satisfied) + { + /* Safe, row compare satisfied by every tuple on page */ + continue; + } + + break; /* unsafe */ + } + if (key->sk_strategy != BTEqualStrategyNumber) + { + /* + * Scalar inequality key. + * + * It's definitely safe for _bt_checkkeys to avoid assessing this + * inequality when the page's first and last non-pivot tuples both + * satisfy the inequality (since the same must also be true of all + * the tuples in between these two). + * + * Unlike the "=" case, it doesn't matter if this attribute has + * more than one distinct value (though it _is_ necessary for any + * and all _prior_ attributes to contain no more than one distinct + * value amongst all of the tuples from pstate.page). + */ + if (key->sk_attno > firstchangingattnum) /* >, not >= */ + break; /* unsafe, preceding attr has multiple + * distinct values */ + + firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull); + lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull); + + if (key->sk_flags & SK_ISNULL) + { + /* IS NOT NULL key */ + Assert(key->sk_flags & SK_SEARCHNOTNULL); + + if (firstnull || lastnull) + break; /* unsafe */ + + /* Safe, IS NOT NULL key satisfied by every tuple */ + continue; + } + + /* Test firsttup */ + if (firstnull || + !DatumGetBool(FunctionCall2Coll(&key->sk_func, + key->sk_collation, firstdatum, + key->sk_argument))) + break; /* unsafe */ + + /* Test lasttup */ + if (lastnull || + !DatumGetBool(FunctionCall2Coll(&key->sk_func, + key->sk_collation, lastdatum, + key->sk_argument))) + break; /* unsafe */ + + /* Safe, scalar inequality satisfied by every tuple */ + continue; + } + + /* Some = key (could be a scalar = key, could be an array = key) */ + Assert(key->sk_strategy == BTEqualStrategyNumber); + + if (!(key->sk_flags & SK_SEARCHARRAY)) + { + /* + * Scalar = key (possibly an IS NULL key). + * + * It is unsafe to set pstate.startikey to an ikey beyond this + * key, unless the = key is satisfied by every possible tuple on + * the page (possible only when attribute has just one distinct + * value among all tuples on the page). + */ + if (key->sk_attno >= firstchangingattnum) + break; /* unsafe, multiple distinct attr values */ + + firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, + &firstnull); + if (key->sk_flags & SK_ISNULL) + { + /* IS NULL key */ + Assert(key->sk_flags & SK_SEARCHNULL); + + if (!firstnull) + break; /* unsafe */ + + /* Safe, IS NULL key satisfied by every tuple */ + continue; + } + if (firstnull || + !DatumGetBool(FunctionCall2Coll(&key->sk_func, + key->sk_collation, firstdatum, + key->sk_argument))) + break; /* unsafe */ + + /* Safe, scalar = key satisfied by every tuple */ + continue; + } + + /* = array key (could be a SAOP array, could be a skip array) */ + array = &so->arrayKeys[arrayidx++]; + Assert(array->scan_key == startikey); + if (array->num_elems != -1) + { + /* + * SAOP array = key. + * + * Handle this like we handle scalar = keys (though binary search + * for a matching element, to avoid relying on key's sk_argument). + */ + if (key->sk_attno >= firstchangingattnum) + break; /* unsafe, multiple distinct attr values */ + + firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, + &firstnull); + _bt_binsrch_array_skey(&so->orderProcs[startikey], + false, NoMovementScanDirection, + firstdatum, firstnull, array, key, + &result); + if (result != 0) + break; /* unsafe */ + + /* Safe, SAOP = key satisfied by every tuple */ + start_past_saop_eq = true; + continue; + } + + /* + * Skip array = key + */ + Assert(key->sk_flags & SK_BT_SKIP); + if (array->null_elem) + { + /* + * Non-range skip array = key. + * + * Safe, non-range skip array "satisfied" by every tuple on page + * (safe even when "key->sk_attno > firstchangingattnum"). + */ + continue; + } + + /* + * Range skip array = key. + * + * Handle this like we handle scalar inequality keys (but avoid using + * key's sk_argument directly, as in the SAOP array case). + */ + if (key->sk_attno > firstchangingattnum) /* >, not >= */ + break; /* unsafe, preceding attr has multiple + * distinct values */ + + firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull); + lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull); + + /* Test firsttup */ + _bt_binsrch_skiparray_skey(false, ForwardScanDirection, + firstdatum, firstnull, array, key, + &result); + if (result != 0) + break; /* unsafe */ + + /* Test lasttup */ + _bt_binsrch_skiparray_skey(false, ForwardScanDirection, + lastdatum, lastnull, array, key, + &result); + if (result != 0) + break; /* unsafe */ + + /* Safe, range skip array satisfied by every tuple on page */ + } + + /* + * Use of forcenonrequired is typically undesirable, since it'll force + * _bt_readpage caller to read every tuple on the page -- even though, in + * general, it might well be possible to end the scan on an earlier tuple. + * However, caller must use forcenonrequired when start_past_saop_eq=true, + * since the usual required array behavior might fail to roll over to the + * SAOP array. + * + * We always prefer forcenonrequired=true during scans with skip arrays + * (except on the first page of each primitive index scan), though -- even + * when "startikey == 0". That way, _bt_advance_array_keys's low-order + * key precheck optimization can always be used (unless on the first page + * of the scan). It seems slightly preferable to check more tuples when + * that allows us to do significantly less skip array maintenance. + */ + pstate->forcenonrequired = (start_past_saop_eq || so->skipScan); + pstate->startikey = startikey; + + /* + * _bt_readpage caller is required to call _bt_checkkeys against page's + * finaltup with forcenonrequired=false whenever we initially set + * forcenonrequired=true. That way the scan's arrays will reliably track + * its progress through the index's key space. + * + * We don't expect this when _bt_readpage caller has no finaltup due to + * its page being the rightmost (or the leftmost, during backwards scans). + * When we see that _bt_readpage has no finaltup, back out of everything. + */ + Assert(!pstate->forcenonrequired || so->numArrayKeys); + if (pstate->forcenonrequired && !pstate->finaltup) + { + pstate->forcenonrequired = false; + pstate->startikey = 0; + } +} + +/* + * Test whether caller's finaltup tuple is still before the start of matches + * for the current array keys. + * + * Called at the start of reading a page during a scan with array keys, though + * only when the so->scanBehind flag was set on the scan's prior page. + * + * Returns false if the tuple is still before the start of matches. When that + * happens, caller should cut its losses and start a new primitive index scan. + * Otherwise returns true. + */ +static bool +_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir, + IndexTuple finaltup) +{ + Relation rel = scan->indexRelation; + TupleDesc tupdesc = RelationGetDescr(rel); + BTScanOpaque so = (BTScanOpaque) scan->opaque; + int nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel); + bool scanBehind; + + Assert(so->numArrayKeys); + + if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc, + nfinaltupatts, false, 0, &scanBehind)) + return false; + + /* + * If scanBehind was set, all of the untruncated attribute values from + * finaltup that correspond to an array match the array's current element, + * but there are other keys associated with truncated suffix attributes. + * Array advancement must have incremented the scan's arrays on the + * previous page, resulting in a set of array keys that happen to be an + * exact match for the current page high key's untruncated prefix values. + * + * This page definitely doesn't contain tuples that the scan will need to + * return. The next page may or may not contain relevant tuples. Handle + * this by cutting our losses and starting a new primscan. + */ + if (scanBehind) + return false; + + if (!so->oppositeDirCheck) + return true; + + return _bt_oppodir_checkkeys(scan, dir, finaltup); +} + +/* + * Test whether an indextuple fails to satisfy an inequality required in the + * opposite direction only. + * + * Caller's finaltup tuple is the page high key (for forwards scans), or the + * first non-pivot tuple (for backwards scans). Called during scans with + * required array keys and required opposite-direction inequalities. + * + * Returns false if an inequality scan key required in the opposite direction + * only isn't satisfied (and any earlier required scan keys are satisfied). + * Otherwise returns true. + * + * An unsatisfied inequality required in the opposite direction only might + * well enable skipping over many leaf pages, provided another _bt_first call + * takes place. This type of unsatisfied inequality won't usually cause + * _bt_checkkeys to stop the scan to consider array advancement/starting a new + * primitive index scan. + */ +static bool +_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir, + IndexTuple finaltup) +{ + Relation rel = scan->indexRelation; + TupleDesc tupdesc = RelationGetDescr(rel); + BTScanOpaque so = (BTScanOpaque) scan->opaque; + int nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel); + bool continuescan; + ScanDirection flipped = -dir; + int ikey = 0; + + Assert(so->numArrayKeys); + + _bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc, false, + false, &continuescan, + &ikey); + + if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber) + return false; + + return true; +} + +/* Save an index item into so->currPos.items[itemIndex] */ +static void +_bt_saveitem(BTScanOpaque so, int itemIndex, + OffsetNumber offnum, IndexTuple itup) +{ + BTScanPosItem *currItem = &so->currPos.items[itemIndex]; + + Assert(!BTreeTupleIsPivot(itup) && !BTreeTupleIsPosting(itup)); + + currItem->heapTid = itup->t_tid; + currItem->indexOffset = offnum; + if (so->currTuples) + { + Size itupsz = IndexTupleSize(itup); + + currItem->tupleOffset = so->currPos.nextTupleOffset; + memcpy(so->currTuples + so->currPos.nextTupleOffset, itup, itupsz); + so->currPos.nextTupleOffset += MAXALIGN(itupsz); + } +} + +/* + * Setup state to save TIDs/items from a single posting list tuple. + * + * Saves an index item into so->currPos.items[itemIndex] for TID that is + * returned to scan first. Second or subsequent TIDs for posting list should + * be saved by calling _bt_savepostingitem(). + * + * Returns an offset into tuple storage space that main tuple is stored at if + * needed. + */ +static int +_bt_setuppostingitems(BTScanOpaque so, int itemIndex, OffsetNumber offnum, + const ItemPointerData *heapTid, IndexTuple itup) +{ + BTScanPosItem *currItem = &so->currPos.items[itemIndex]; + + Assert(BTreeTupleIsPosting(itup)); + + currItem->heapTid = *heapTid; + currItem->indexOffset = offnum; + if (so->currTuples) + { + /* Save base IndexTuple (truncate posting list) */ + IndexTuple base; + Size itupsz = BTreeTupleGetPostingOffset(itup); + + itupsz = MAXALIGN(itupsz); + currItem->tupleOffset = so->currPos.nextTupleOffset; + base = (IndexTuple) (so->currTuples + so->currPos.nextTupleOffset); + memcpy(base, itup, itupsz); + /* Defensively reduce work area index tuple header size */ + base->t_info &= ~INDEX_SIZE_MASK; + base->t_info |= itupsz; + so->currPos.nextTupleOffset += itupsz; + + return currItem->tupleOffset; + } + + return 0; +} + +/* + * Save an index item into so->currPos.items[itemIndex] for current posting + * tuple. + * + * Assumes that _bt_setuppostingitems() has already been called for current + * posting list tuple. Caller passes its return value as tupleOffset. + */ +static inline void +_bt_savepostingitem(BTScanOpaque so, int itemIndex, OffsetNumber offnum, + ItemPointer heapTid, int tupleOffset) +{ + BTScanPosItem *currItem = &so->currPos.items[itemIndex]; + + currItem->heapTid = *heapTid; + currItem->indexOffset = offnum; + + /* + * Have index-only scans return the same base IndexTuple for every TID + * that originates from the same posting list + */ + if (so->currTuples) + currItem->tupleOffset = tupleOffset; +} + +#define LOOK_AHEAD_REQUIRED_RECHECKS 3 +#define LOOK_AHEAD_DEFAULT_DISTANCE 5 +#define NSKIPADVANCES_THRESHOLD 3 + +/* + * Test whether an indextuple satisfies all the scankey conditions. + * + * Returns true if so, false if not. If not, + * we also determine whether there's any need to continue the scan beyond + * this tuple, and set pstate.continuescan accordingly. See comments for + * _bt_preprocess_keys() about how this is done. + * + * Forward scan callers can pass a high key tuple in the hopes of having + * us set pstate.continuescan to false, avoiding an unnecessary visit to + * the page to the right. + * + * Advances the scan's array keys when necessary for arrayKeys=true callers. + * Scans without any array keys must always pass arrayKeys=false. + * + * Also stops and starts primitive index scans for arrayKeys=true callers. + * Scans with array keys are required to set up page state that helps us with + * this. The page's finaltup tuple (the page high key for a forward scan, or + * the page's first non-pivot tuple for a backward scan) must be set in + * pstate.finaltup ahead of the first call here for the page. Set it to + * NULL for rightmost page (or the leftmost page for backwards scans). + * + * scan: index scan descriptor (containing a search-type scankey) + * pstate: page level input and output parameters + * arrayKeys: should we advance the scan's array keys if necessary? + * tuple: index tuple to test + * tupnatts: number of attributes in tupnatts (high key may be truncated) + */ +static bool +_bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys, + IndexTuple tuple, int tupnatts) +{ + TupleDesc tupdesc = RelationGetDescr(scan->indexRelation); + BTScanOpaque so PG_USED_FOR_ASSERTS_ONLY = (BTScanOpaque) scan->opaque; + ScanDirection dir = pstate->dir; + int ikey = pstate->startikey; + bool res; + + Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts); + Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck); + Assert(arrayKeys || so->numArrayKeys == 0); + + res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys, + pstate->forcenonrequired, &pstate->continuescan, + &ikey); + + /* + * If _bt_check_compare relied on the pstate.startikey optimization, call + * again (in assert-enabled builds) to verify it didn't affect our answer. + * + * Note: we can't do this when !pstate.forcenonrequired, since any arrays + * before pstate.startikey won't have advanced on this page at all. + */ + Assert(!pstate->forcenonrequired || arrayKeys); +#ifdef USE_ASSERT_CHECKING + if (pstate->startikey > 0 && !pstate->forcenonrequired) + { + bool dres, + dcontinuescan; + int dikey = 0; + + /* Pass advancenonrequired=false to avoid array side-effects */ + dres = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false, + pstate->forcenonrequired, &dcontinuescan, + &dikey); + Assert(res == dres); + Assert(pstate->continuescan == dcontinuescan); + + /* + * Should also get the same ikey result. We need a slightly weaker + * assertion during arrayKeys calls, since they might be using an + * array that couldn't be marked required during preprocessing. + */ + Assert(arrayKeys || ikey == dikey); + Assert(ikey <= dikey); + } +#endif + + /* + * Only one _bt_check_compare call is required in the common case where + * there are no equality strategy array scan keys. With array keys, we + * can only accept _bt_check_compare's answer unreservedly when it set + * pstate.continuescan=true. + */ + if (!arrayKeys || pstate->continuescan) + return res; + + /* + * _bt_check_compare call set continuescan=false in the presence of + * equality type array keys. This could mean that the tuple is just past + * the end of matches for the current array keys. + * + * It's also possible that the scan is still _before_ the _start_ of + * tuples matching the current set of array keys. Check for that first. + */ + Assert(!pstate->forcenonrequired); + if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true, + ikey, NULL)) + { + /* Override _bt_check_compare, continue primitive scan */ + pstate->continuescan = true; + + /* + * We will end up here repeatedly given a group of tuples > the + * previous array keys and < the now-current keys (for a backwards + * scan it's just the same, though the operators swap positions). + * + * We must avoid allowing this linear search process to scan very many + * tuples from well before the start of tuples matching the current + * array keys (or from well before the point where we'll once again + * have to advance the scan's array keys). + * + * We keep the overhead under control by speculatively "looking ahead" + * to later still-unscanned items from this same leaf page. We'll + * only attempt this once the number of tuples that the linear search + * process has examined starts to get out of hand. + */ + pstate->rechecks++; + if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS) + { + /* See if we should skip ahead within the current leaf page */ + _bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc); + + /* + * Might have set pstate.skip to a later page offset. When that + * happens then _bt_readpage caller will inexpensively skip ahead + * to a later tuple from the same page (the one just after the + * tuple we successfully "looked ahead" to). + */ + } + + /* This indextuple doesn't match the current qual, in any case */ + return false; + } + + /* + * Caller's tuple is >= the current set of array keys and other equality + * constraint scan keys (or <= if this is a backwards scan). It's now + * clear that we _must_ advance any required array keys in lockstep with + * the scan. + */ + return _bt_advance_array_keys(scan, pstate, tuple, tupnatts, tupdesc, + ikey, true); +} + +/* + * Test whether an indextuple satisfies current scan condition. + * + * Return true if so, false if not. If not, also sets *continuescan to false + * when it's also not possible for any later tuples to pass the current qual + * (with the scan's current set of array keys, in the current scan direction), + * in addition to setting *ikey to the so->keyData[] subscript/offset for the + * unsatisfied scan key (needed when caller must consider advancing the scan's + * array keys). + * + * This is a subroutine for _bt_checkkeys. We provisionally assume that + * reaching the end of the current set of required keys (in particular the + * current required array keys) ends the ongoing (primitive) index scan. + * Callers without array keys should just end the scan right away when they + * find that continuescan has been set to false here by us. Things are more + * complicated for callers with array keys. + * + * Callers with array keys must first consider advancing the arrays when + * continuescan has been set to false here by us. They must then consider if + * it really does make sense to end the current (primitive) index scan, in + * light of everything that is known at that point. (In general when we set + * continuescan=false for these callers it must be treated as provisional.) + * + * We deal with advancing unsatisfied non-required arrays directly, though. + * This is safe, since by definition non-required keys can't end the scan. + * This is just how we determine if non-required arrays are just unsatisfied + * by the current array key, or if they're truly unsatisfied (that is, if + * they're unsatisfied by every possible array key). + * + * Pass advancenonrequired=false to avoid all array related side effects. + * This allows _bt_advance_array_keys caller to avoid infinite recursion. + * + * Pass forcenonrequired=true to instruct us to treat all keys as nonrequired. + * This is used to make it safe to temporarily stop properly maintaining the + * scan's required arrays. _bt_checkkeys caller (_bt_readpage, actually) + * determines a prefix of keys that must satisfy every possible corresponding + * index attribute value from its page, which is passed to us via *ikey arg + * (this is the first key that might be unsatisfied by tuples on the page). + * Obviously, we won't maintain any array keys from before *ikey, so it's + * quite possible for such arrays to "fall behind" the index's keyspace. + * Caller will need to "catch up" by passing forcenonrequired=true (alongside + * an *ikey=0) once the page's finaltup is reached. + * + * Note: it's safe to pass an *ikey > 0 with forcenonrequired=false, but only + * when caller determines that it won't affect array maintenance. + */ +static bool +_bt_check_compare(IndexScanDesc scan, ScanDirection dir, + IndexTuple tuple, int tupnatts, TupleDesc tupdesc, + bool advancenonrequired, bool forcenonrequired, + bool *continuescan, int *ikey) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + + *continuescan = true; /* default assumption */ + + for (; *ikey < so->numberOfKeys; (*ikey)++) + { + ScanKey key = so->keyData + *ikey; + Datum datum; + bool isNull; + bool requiredSameDir = false, + requiredOppositeDirOnly = false; + + /* + * Check if the key is required in the current scan direction, in the + * opposite scan direction _only_, or in neither direction (except + * when we're forced to treat all scan keys as nonrequired) + */ + if (forcenonrequired) + { + /* treating scan's keys as non-required */ + } + else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) || + ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir))) + requiredSameDir = true; + else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) || + ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir))) + requiredOppositeDirOnly = true; + + if (key->sk_attno > tupnatts) + { + /* + * This attribute is truncated (must be high key). The value for + * this attribute in the first non-pivot tuple on the page to the + * right could be any possible value. Assume that truncated + * attribute passes the qual. + */ + Assert(BTreeTupleIsPivot(tuple)); + continue; + } + + /* + * A skip array scan key uses one of several sentinel values. We just + * fall back on _bt_tuple_before_array_skeys when we see such a value. + */ + if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL | + SK_BT_NEXT | SK_BT_PRIOR)) + { + Assert(key->sk_flags & SK_SEARCHARRAY); + Assert(key->sk_flags & SK_BT_SKIP); + Assert(requiredSameDir || forcenonrequired); + + /* + * Cannot fall back on _bt_tuple_before_array_skeys when we're + * treating the scan's keys as nonrequired, though. Just handle + * this like any other non-required equality-type array key. + */ + if (forcenonrequired) + return _bt_advance_array_keys(scan, NULL, tuple, tupnatts, + tupdesc, *ikey, false); + + *continuescan = false; + return false; + } + + /* row-comparison keys need special processing */ + if (key->sk_flags & SK_ROW_HEADER) + { + if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir, + forcenonrequired, continuescan)) + continue; + return false; + } + + datum = index_getattr(tuple, + key->sk_attno, + tupdesc, + &isNull); + + if (key->sk_flags & SK_ISNULL) + { + /* Handle IS NULL/NOT NULL tests */ + if (key->sk_flags & SK_SEARCHNULL) + { + if (isNull) + continue; /* tuple satisfies this qual */ + } + else + { + Assert(key->sk_flags & SK_SEARCHNOTNULL); + Assert(!(key->sk_flags & SK_BT_SKIP)); + if (!isNull) + continue; /* tuple satisfies this qual */ + } + + /* + * Tuple fails this qual. If it's a required qual for the current + * scan direction, then we can conclude no further tuples will + * pass, either. + */ + if (requiredSameDir) + *continuescan = false; + else if (unlikely(key->sk_flags & SK_BT_SKIP)) + { + /* + * If we're treating scan keys as nonrequired, and encounter a + * skip array scan key whose current element is NULL, then it + * must be a non-range skip array. It must be satisfied, so + * there's no need to call _bt_advance_array_keys to check. + */ + Assert(forcenonrequired && *ikey > 0); + continue; + } + + /* + * This indextuple doesn't match the qual. + */ + return false; + } + + if (isNull) + { + /* + * Scalar scan key isn't satisfied by NULL tuple value. + * + * If we're treating scan keys as nonrequired, and key is for a + * skip array, then we must attempt to advance the array to NULL + * (if we're successful then the tuple might match the qual). + */ + if (unlikely(forcenonrequired && key->sk_flags & SK_BT_SKIP)) + return _bt_advance_array_keys(scan, NULL, tuple, tupnatts, + tupdesc, *ikey, false); + + if (key->sk_flags & SK_BT_NULLS_FIRST) + { + /* + * Since NULLs are sorted before non-NULLs, we know we have + * reached the lower limit of the range of values for this + * index attr. On a backward scan, we can stop if this qual + * is one of the "must match" subset. We can stop regardless + * of whether the qual is > or <, so long as it's required, + * because it's not possible for any future tuples to pass. On + * a forward scan, however, we must keep going, because we may + * have initially positioned to the start of the index. + * (_bt_advance_array_keys also relies on this behavior during + * forward scans.) + */ + if ((requiredSameDir || requiredOppositeDirOnly) && + ScanDirectionIsBackward(dir)) + *continuescan = false; + } + else + { + /* + * Since NULLs are sorted after non-NULLs, we know we have + * reached the upper limit of the range of values for this + * index attr. On a forward scan, we can stop if this qual is + * one of the "must match" subset. We can stop regardless of + * whether the qual is > or <, so long as it's required, + * because it's not possible for any future tuples to pass. On + * a backward scan, however, we must keep going, because we + * may have initially positioned to the end of the index. + * (_bt_advance_array_keys also relies on this behavior during + * backward scans.) + */ + if ((requiredSameDir || requiredOppositeDirOnly) && + ScanDirectionIsForward(dir)) + *continuescan = false; + } + + /* + * This indextuple doesn't match the qual. + */ + return false; + } + + if (!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation, + datum, key->sk_argument))) + { + /* + * Tuple fails this qual. If it's a required qual for the current + * scan direction, then we can conclude no further tuples will + * pass, either. + */ + if (requiredSameDir) + *continuescan = false; + + /* + * If this is a non-required equality-type array key, the tuple + * needs to be checked against every possible array key. Handle + * this by "advancing" the scan key's array to a matching value + * (if we're successful then the tuple might match the qual). + */ + else if (advancenonrequired && + key->sk_strategy == BTEqualStrategyNumber && + (key->sk_flags & SK_SEARCHARRAY)) + return _bt_advance_array_keys(scan, NULL, tuple, tupnatts, + tupdesc, *ikey, false); + + /* + * This indextuple doesn't match the qual. + */ + return false; + } + } + + /* If we get here, the tuple passes all index quals. */ + return true; +} + +/* + * Test whether an indextuple satisfies a row-comparison scan condition. + * + * Return true if so, false if not. If not, also clear *continuescan if + * it's not possible for any future tuples in the current scan direction + * to pass the qual. + * + * This is a subroutine for _bt_checkkeys/_bt_check_compare. Caller passes us + * a row compare header key taken from so->keyData[]. + * + * Row value comparisons can be described in terms of logical expansions that + * use only scalar operators. Consider the following example row comparison: + * + * "(a, b, c) > (7, 'bar', 62)" + * + * This can be evaluated as: + * + * "(a = 7 AND b = 'bar' AND c > 62) OR (a = 7 AND b > 'bar') OR (a > 7)". + * + * Notice that this condition is satisfied by _all_ rows that satisfy "a > 7", + * and by a subset of all rows that satisfy "a >= 7" (possibly all such rows). + * It _can't_ be satisfied by other rows (where "a < 7" or where "a IS NULL"). + * A row comparison header key can therefore often be treated as if it was a + * simple scalar inequality on the row compare's most significant column. + * (For example, _bt_advance_array_keys and most preprocessing routines treat + * row compares like any other same-strategy inequality on the same column.) + * + * Things get more complicated for our row compare given a row where "a = 7". + * Note that a row compare isn't necessarily satisfied by _every_ tuple that + * appears between the first and last satisfied tuple returned by the scan, + * due to the way that its lower-order subkeys are only conditionally applied. + * A forwards scan that uses our example qual might initially return a tuple + * "(a, b, c) = (7, 'zebra', 54)". But it won't subsequently return a tuple + * "(a, b, c) = (7, NULL, 1)" located to the right of the first matching tuple + * (assume that "b" was declared NULLS LAST here). The scan will only return + * additional matches upon reaching tuples where "a > 7". If you rereview our + * example row comparison's logical expansion, you'll understand why this is. + * (Here we assume that all subkeys could be marked required, guaranteeing + * that row comparison order matches index order. This is the common case.) + * + * Note that a row comparison header key behaves _exactly_ the same as a + * similar scalar inequality key on the row's most significant column once the + * scan reaches the point where it no longer needs to evaluate lower-order + * subkeys (or before the point where it starts needing to evaluate them). + * For example, once a forwards scan that uses our example qual reaches the + * first tuple "a > 7", we'll behave in just the same way as our caller would + * behave with a similar scalar inequality "a > 7" for the remainder of the + * scan (assuming that the scan never changes direction/never goes backwards). + * We'll even set continuescan=false according to exactly the same rules as + * the ones our caller applies with simple scalar inequalities, including the + * rules it applies when NULL tuple values don't satisfy an inequality qual. + */ +static bool +_bt_check_rowcompare(ScanKey header, IndexTuple tuple, int tupnatts, + TupleDesc tupdesc, ScanDirection dir, + bool forcenonrequired, bool *continuescan) +{ + ScanKey subkey = (ScanKey) DatumGetPointer(header->sk_argument); + int32 cmpresult = 0; + bool result; + + /* First subkey should be same as the header says */ + Assert(header->sk_flags & SK_ROW_HEADER); + Assert(subkey->sk_attno == header->sk_attno); + Assert(subkey->sk_strategy == header->sk_strategy); + + /* Loop over columns of the row condition */ + for (;;) + { + Datum datum; + bool isNull; + + Assert(subkey->sk_flags & SK_ROW_MEMBER); + + /* When a NULL row member is compared, the row never matches */ + if (subkey->sk_flags & SK_ISNULL) + { + /* + * Unlike the simple-scankey case, this isn't a disallowed case + * (except when it's the first row element that has the NULL arg). + * But it can never match. If all the earlier row comparison + * columns are required for the scan direction, we can stop the + * scan, because there can't be another tuple that will succeed. + */ + Assert(subkey != (ScanKey) DatumGetPointer(header->sk_argument)); + subkey--; + if (forcenonrequired) + { + /* treating scan's keys as non-required */ + } + else if ((subkey->sk_flags & SK_BT_REQFWD) && + ScanDirectionIsForward(dir)) + *continuescan = false; + else if ((subkey->sk_flags & SK_BT_REQBKWD) && + ScanDirectionIsBackward(dir)) + *continuescan = false; + return false; + } + + if (subkey->sk_attno > tupnatts) + { + /* + * This attribute is truncated (must be high key). The value for + * this attribute in the first non-pivot tuple on the page to the + * right could be any possible value. Assume that truncated + * attribute passes the qual. + */ + Assert(BTreeTupleIsPivot(tuple)); + return true; + } + + datum = index_getattr(tuple, + subkey->sk_attno, + tupdesc, + &isNull); + + if (isNull) + { + int reqflags; + + if (forcenonrequired) + { + /* treating scan's keys as non-required */ + } + else if (subkey->sk_flags & SK_BT_NULLS_FIRST) + { + /* + * Since NULLs are sorted before non-NULLs, we know we have + * reached the lower limit of the range of values for this + * index attr. On a backward scan, we can stop if this qual + * is one of the "must match" subset. However, on a forwards + * scan, we must keep going, because we may have initially + * positioned to the start of the index. + * + * All required NULLS FIRST > row members can use NULL tuple + * values to end backwards scans, just like with other values. + * A qual "WHERE (a, b, c) > (9, 42, 'foo')" can terminate a + * backwards scan upon reaching the index's rightmost "a = 9" + * tuple whose "b" column contains a NULL (if not sooner). + * Since "b" is NULLS FIRST, we can treat its NULLs as "<" 42. + */ + reqflags = SK_BT_REQBKWD; + + /* + * When a most significant required NULLS FIRST < row compare + * member sees NULL tuple values during a backwards scan, it + * signals the end of matches for the whole row compare/scan. + * A qual "WHERE (a, b, c) < (9, 42, 'foo')" will terminate a + * backwards scan upon reaching the rightmost tuple whose "a" + * column has a NULL. The "a" NULL value is "<" 9, and yet + * our < row compare will still end the scan. (This isn't + * safe with later/lower-order row members. Notice that it + * can only happen with an "a" NULL some time after the scan + * completely stops needing to use its "b" and "c" members.) + */ + if (subkey == (ScanKey) DatumGetPointer(header->sk_argument)) + reqflags |= SK_BT_REQFWD; /* safe, first row member */ + + if ((subkey->sk_flags & reqflags) && + ScanDirectionIsBackward(dir)) + *continuescan = false; + } + else + { + /* + * Since NULLs are sorted after non-NULLs, we know we have + * reached the upper limit of the range of values for this + * index attr. On a forward scan, we can stop if this qual is + * one of the "must match" subset. However, on a backward + * scan, we must keep going, because we may have initially + * positioned to the end of the index. + * + * All required NULLS LAST < row members can use NULL tuple + * values to end forwards scans, just like with other values. + * A qual "WHERE (a, b, c) < (9, 42, 'foo')" can terminate a + * forwards scan upon reaching the index's leftmost "a = 9" + * tuple whose "b" column contains a NULL (if not sooner). + * Since "b" is NULLS LAST, we can treat its NULLs as ">" 42. + */ + reqflags = SK_BT_REQFWD; + + /* + * When a most significant required NULLS LAST > row compare + * member sees NULL tuple values during a forwards scan, it + * signals the end of matches for the whole row compare/scan. + * A qual "WHERE (a, b, c) > (9, 42, 'foo')" will terminate a + * forwards scan upon reaching the leftmost tuple whose "a" + * column has a NULL. The "a" NULL value is ">" 9, and yet + * our > row compare will end the scan. (This isn't safe with + * later/lower-order row members. Notice that it can only + * happen with an "a" NULL some time after the scan completely + * stops needing to use its "b" and "c" members.) + */ + if (subkey == (ScanKey) DatumGetPointer(header->sk_argument)) + reqflags |= SK_BT_REQBKWD; /* safe, first row member */ + + if ((subkey->sk_flags & reqflags) && + ScanDirectionIsForward(dir)) + *continuescan = false; + } + + /* + * In any case, this indextuple doesn't match the qual. + */ + return false; + } + + /* Perform the test --- three-way comparison not bool operator */ + cmpresult = DatumGetInt32(FunctionCall2Coll(&subkey->sk_func, + subkey->sk_collation, + datum, + subkey->sk_argument)); + + if (subkey->sk_flags & SK_BT_DESC) + INVERT_COMPARE_RESULT(cmpresult); + + /* Done comparing if unequal, else advance to next column */ + if (cmpresult != 0) + break; + + if (subkey->sk_flags & SK_ROW_END) + break; + subkey++; + } + + /* Final subkey/column determines if row compare is satisfied */ + result = _bt_rowcompare_cmpresult(subkey, cmpresult); + + if (!result && !forcenonrequired) + { + /* + * Tuple fails this qual. If it's a required qual for the current + * scan direction, then we can conclude no further tuples will pass, + * either. Note we have to look at the deciding column, not + * necessarily the first or last column of the row condition. + */ + if ((subkey->sk_flags & SK_BT_REQFWD) && + ScanDirectionIsForward(dir)) + *continuescan = false; + else if ((subkey->sk_flags & SK_BT_REQBKWD) && + ScanDirectionIsBackward(dir)) + *continuescan = false; + } + + return result; +} + +/* + * Call here when a row compare member returns a non-zero result, or with the + * result for the final ROW_END row compare member (no matter the cmpresult). + * + * cmpresult indicates the overall result of the row comparison (must already + * be commuted for DESC subkeys), and subkey is the deciding row member. + */ +static bool +_bt_rowcompare_cmpresult(ScanKey subkey, int cmpresult) +{ + bool satisfied; + + Assert(subkey->sk_flags & SK_ROW_MEMBER); + + switch (subkey->sk_strategy) + { + case BTLessStrategyNumber: + satisfied = (cmpresult < 0); + break; + case BTLessEqualStrategyNumber: + satisfied = (cmpresult <= 0); + break; + case BTGreaterEqualStrategyNumber: + satisfied = (cmpresult >= 0); + break; + case BTGreaterStrategyNumber: + satisfied = (cmpresult > 0); + break; + default: + /* EQ and NE cases aren't allowed here */ + elog(ERROR, "unexpected strategy number %d", subkey->sk_strategy); + satisfied = false; /* keep compiler quiet */ + break; + } + + return satisfied; +} + +/* + * _bt_tuple_before_array_skeys() -- too early to advance required arrays? + * + * We always compare the tuple using the current array keys (which we assume + * are already set in so->keyData[]). readpagetup indicates if tuple is the + * scan's current _bt_readpage-wise tuple. + * + * readpagetup callers must only call here when _bt_check_compare already set + * continuescan=false. We help these callers deal with _bt_check_compare's + * inability to distinguish between the < and > cases (it uses equality + * operator scan keys, whereas we use 3-way ORDER procs). These callers pass + * a _bt_check_compare-set sktrig value that indicates which scan key + * triggered the call (!readpagetup callers just pass us sktrig=0 instead). + * This information allows us to avoid wastefully checking earlier scan keys + * that were already deemed to have been satisfied inside _bt_check_compare. + * + * Returns false when caller's tuple is >= the current required equality scan + * keys (or <=, in the case of backwards scans). This happens to readpagetup + * callers when the scan has reached the point of needing its array keys + * advanced; caller will need to advance required and non-required arrays at + * scan key offsets >= sktrig, plus scan keys < sktrig iff sktrig rolls over. + * (When we return false to readpagetup callers, tuple can only be == current + * required equality scan keys when caller's sktrig indicates that the arrays + * need to be advanced due to an unsatisfied required inequality key trigger.) + * + * Returns true when caller passes a tuple that is < the current set of + * equality keys for the most significant non-equal required scan key/column + * (or > the keys, during backwards scans). This happens to readpagetup + * callers when tuple is still before the start of matches for the scan's + * required equality strategy scan keys. (sktrig can't have indicated that an + * inequality strategy scan key wasn't satisfied in _bt_check_compare when we + * return true. In fact, we automatically return false when passed such an + * inequality sktrig by readpagetup callers -- _bt_check_compare's initial + * continuescan=false doesn't really need to be confirmed here by us.) + * + * !readpagetup callers optionally pass us *scanBehind, which tracks whether + * any missing truncated attributes might have affected array advancement + * (compared to what would happen if it was shown the first non-pivot tuple on + * the page to the right of caller's finaltup/high key tuple instead). It's + * only possible that we'll set *scanBehind to true when caller passes us a + * pivot tuple (with truncated -inf attributes) that we return false for. + */ +static bool +_bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir, + IndexTuple tuple, TupleDesc tupdesc, int tupnatts, + bool readpagetup, int sktrig, bool *scanBehind) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + + Assert(so->numArrayKeys); + Assert(so->numberOfKeys); + Assert(sktrig == 0 || readpagetup); + Assert(!readpagetup || scanBehind == NULL); + + if (scanBehind) + *scanBehind = false; + + for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++) + { + ScanKey cur = so->keyData + ikey; + Datum tupdatum; + bool tupnull; + int32 result; + + /* readpagetup calls require one ORDER proc comparison (at most) */ + Assert(!readpagetup || ikey == sktrig); + + /* + * Once we reach a non-required scan key, we're completely done. + * + * Note: we deliberately don't consider the scan direction here. + * _bt_advance_array_keys caller requires that we track *scanBehind + * without concern for scan direction. + */ + if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0) + { + Assert(!readpagetup); + Assert(ikey > sktrig || ikey == 0); + return false; + } + + if (cur->sk_attno > tupnatts) + { + Assert(!readpagetup); + + /* + * When we reach a high key's truncated attribute, assume that the + * tuple attribute's value is >= the scan's equality constraint + * scan keys (but set *scanBehind to let interested callers know + * that a truncated attribute might have affected our answer). + */ + if (scanBehind) + *scanBehind = true; + + return false; + } + + /* + * Deal with inequality strategy scan keys that _bt_check_compare set + * continuescan=false for + */ + if (cur->sk_strategy != BTEqualStrategyNumber) + { + /* + * When _bt_check_compare indicated that a required inequality + * scan key wasn't satisfied, there's no need to verify anything; + * caller always calls _bt_advance_array_keys with this sktrig. + */ + if (readpagetup) + return false; + + /* + * Otherwise we can't give up, since we must check all required + * scan keys (required in either direction) in order to correctly + * track *scanBehind for caller + */ + continue; + } + + tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull); + + if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))) + { + /* Scankey has a valid/comparable sk_argument value */ + result = _bt_compare_array_skey(&so->orderProcs[ikey], + tupdatum, tupnull, + cur->sk_argument, cur); + + if (result == 0) + { + /* + * Interpret result in a way that takes NEXT/PRIOR into + * account + */ + if (cur->sk_flags & SK_BT_NEXT) + result = -1; + else if (cur->sk_flags & SK_BT_PRIOR) + result = 1; + + Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP)); + } + } + else + { + BTArrayKeyInfo *array = NULL; + + /* + * Current array element/array = scan key value is a sentinel + * value that represents the lowest (or highest) possible value + * that's still within the range of the array. + * + * Like _bt_first, we only see MINVAL keys during forwards scans + * (and similarly only see MAXVAL keys during backwards scans). + * Even if the scan's direction changes, we'll stop at some higher + * order key before we can ever reach any MAXVAL (or MINVAL) keys. + * (However, unlike _bt_first we _can_ get to keys marked either + * NEXT or PRIOR, regardless of the scan's current direction.) + */ + Assert(ScanDirectionIsForward(dir) ? + !(cur->sk_flags & SK_BT_MAXVAL) : + !(cur->sk_flags & SK_BT_MINVAL)); + + /* + * There are no valid sk_argument values in MINVAL/MAXVAL keys. + * Check if tupdatum is within the range of skip array instead. + */ + for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++) + { + array = &so->arrayKeys[arrayidx]; + if (array->scan_key == ikey) + break; + } + + _bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull, + array, cur, &result); + + if (result == 0) + { + /* + * tupdatum satisfies both low_compare and high_compare, so + * it's time to advance the array keys. + * + * Note: It's possible that the skip array will "advance" from + * its MINVAL (or MAXVAL) representation to an alternative, + * logically equivalent representation of the same value: a + * representation where the = key gets a valid datum in its + * sk_argument. This is only possible when low_compare uses + * the >= strategy (or high_compare uses the <= strategy). + */ + return false; + } + } + + /* + * Does this comparison indicate that caller must _not_ advance the + * scan's arrays just yet? + */ + if ((ScanDirectionIsForward(dir) && result < 0) || + (ScanDirectionIsBackward(dir) && result > 0)) + return true; + + /* + * Does this comparison indicate that caller should now advance the + * scan's arrays? (Must be if we get here during a readpagetup call.) + */ + if (readpagetup || result != 0) + { + Assert(result != 0); + return false; + } + + /* + * Inconclusive -- need to check later scan keys, too. + * + * This must be a finaltup precheck, or a call made from an assertion. + */ + Assert(result == 0); + } + + Assert(!readpagetup); + + return false; +} + +/* + * Determine if a scan with array keys should skip over uninteresting tuples. + * + * This is a subroutine for _bt_checkkeys, called when _bt_readpage's linear + * search process has scanned an excessive number of tuples whose key space is + * "between arrays". (The linear search process is started after _bt_readpage + * finishes reading an initial group of matching tuples. It locates the start + * of the first group of tuples matching the next set of required array keys.) + * + * When look ahead is successful, we set pstate.skip which + * instructs _bt_readpage to skip ahead to that tuple next (could be past the + * end of the scan's leaf page). Pages where the optimization is effective + * will generally still need to skip several times. Each call here performs + * only a single "look ahead" comparison of a later tuple, whose distance from + * the current tuple is determined by heuristics. + */ +static void +_bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, + int tupnatts, TupleDesc tupdesc) +{ + ScanDirection dir = pstate->dir; + OffsetNumber aheadoffnum; + IndexTuple ahead; + + Assert(!pstate->forcenonrequired); + + /* Avoid looking ahead when comparing the page high key */ + if (pstate->offnum < pstate->minoff) + return; + + /* + * Don't look ahead when there aren't enough tuples remaining on the page + * (in the current scan direction) for it to be worth our while + */ + if (ScanDirectionIsForward(dir) && + pstate->offnum >= pstate->maxoff - LOOK_AHEAD_DEFAULT_DISTANCE) + return; + else if (ScanDirectionIsBackward(dir) && + pstate->offnum <= pstate->minoff + LOOK_AHEAD_DEFAULT_DISTANCE) + return; + + /* + * The look ahead distance starts small, and ramps up as each call here + * allows _bt_readpage to skip over more tuples + */ + if (!pstate->targetdistance) + pstate->targetdistance = LOOK_AHEAD_DEFAULT_DISTANCE; + else if (pstate->targetdistance < MaxIndexTuplesPerPage / 2) + pstate->targetdistance *= 2; + + /* Don't read past the end (or before the start) of the page, though */ + if (ScanDirectionIsForward(dir)) + aheadoffnum = Min((int) pstate->maxoff, + (int) pstate->offnum + pstate->targetdistance); + else + aheadoffnum = Max((int) pstate->minoff, + (int) pstate->offnum - pstate->targetdistance); + + ahead = (IndexTuple) PageGetItem(pstate->page, + PageGetItemId(pstate->page, aheadoffnum)); + if (_bt_tuple_before_array_skeys(scan, dir, ahead, tupdesc, tupnatts, + false, 0, NULL)) + { + /* + * Success -- instruct _bt_readpage to skip ahead to very next tuple + * after the one we determined was still before the current array keys + */ + if (ScanDirectionIsForward(dir)) + pstate->skip = aheadoffnum + 1; + else + pstate->skip = aheadoffnum - 1; + } + else + { + /* + * Failure -- "ahead" tuple is too far ahead (we were too aggressive). + * + * Reset the number of rechecks, and aggressively reduce the target + * distance (we're much more aggressive here than we were when the + * distance was initially ramped up). + */ + pstate->rechecks = 0; + pstate->targetdistance = Max(pstate->targetdistance / 8, 1); + } +} + +/* + * _bt_advance_array_keys() -- Advance array elements using a tuple + * + * The scan always gets a new qual as a consequence of calling here (except + * when we determine that the top-level scan has run out of matching tuples). + * All later _bt_check_compare calls also use the same new qual that was first + * used here (at least until the next call here advances the keys once again). + * It's convenient to structure _bt_check_compare rechecks of caller's tuple + * (using the new qual) as one the steps of advancing the scan's array keys, + * so this function works as a wrapper around _bt_check_compare. + * + * Like _bt_check_compare, we'll set pstate.continuescan on behalf of the + * caller, and return a boolean indicating if caller's tuple satisfies the + * scan's new qual. But unlike _bt_check_compare, we set so->needPrimScan + * when we set continuescan=false, indicating if a new primitive index scan + * has been scheduled (otherwise, the top-level scan has run out of tuples in + * the current scan direction). + * + * Caller must use _bt_tuple_before_array_skeys to determine if the current + * place in the scan is >= the current array keys _before_ calling here. + * We're responsible for ensuring that caller's tuple is <= the newly advanced + * required array keys once we return. We try to find an exact match, but + * failing that we'll advance the array keys to whatever set of array elements + * comes next in the key space for the current scan direction. Required array + * keys "ratchet forwards" (or backwards). They can only advance as the scan + * itself advances through the index/key space. + * + * (The rules are the same for backwards scans, except that the operators are + * flipped: just replace the precondition's >= operator with a <=, and the + * postcondition's <= operator with a >=. In other words, just swap the + * precondition with the postcondition.) + * + * We also deal with "advancing" non-required arrays here (or arrays that are + * treated as non-required for the duration of a _bt_readpage call). Callers + * whose sktrig scan key is non-required specify sktrig_required=false. These + * calls are the only exception to the general rule about always advancing the + * required array keys (the scan may not even have a required array). These + * callers should just pass a NULL pstate (since there is never any question + * of stopping the scan). No call to _bt_tuple_before_array_skeys is required + * ahead of these calls (it's already clear that any required scan keys must + * be satisfied by caller's tuple). + * + * Note that we deal with non-array required equality strategy scan keys as + * degenerate single element arrays here. Obviously, they can never really + * advance in the way that real arrays can, but they must still affect how we + * advance real array scan keys (exactly like true array equality scan keys). + * We have to keep around a 3-way ORDER proc for these (using the "=" operator + * won't do), since in general whether the tuple is < or > _any_ unsatisfied + * required equality key influences how the scan's real arrays must advance. + * + * Note also that we may sometimes need to advance the array keys when the + * existing required array keys (and other required equality keys) are already + * an exact match for every corresponding value from caller's tuple. We must + * do this for inequalities that _bt_check_compare set continuescan=false for. + * They'll advance the array keys here, just like any other scan key that + * _bt_check_compare stops on. (This can even happen _after_ we advance the + * array keys, in which case we'll advance the array keys a second time. That + * way _bt_checkkeys caller always has its required arrays advance to the + * maximum possible extent that its tuple will allow.) + */ +static bool +_bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate, + IndexTuple tuple, int tupnatts, TupleDesc tupdesc, + int sktrig, bool sktrig_required) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + Relation rel = scan->indexRelation; + ScanDirection dir = pstate ? pstate->dir : ForwardScanDirection; + int arrayidx = 0; + bool beyond_end_advance = false, + skip_array_advanced = false, + has_required_opposite_direction_only = false, + all_required_satisfied = true, + all_satisfied = true; + + Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck); + Assert(_bt_verify_keys_with_arraykeys(scan)); + + if (sktrig_required) + { + /* + * Precondition array state assertion + */ + Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, + tupnatts, false, 0, NULL)); + + /* + * Once we return we'll have a new set of required array keys, so + * reset state used by "look ahead" optimization + */ + pstate->rechecks = 0; + pstate->targetdistance = 0; + } + else if (sktrig < so->numberOfKeys - 1 && + !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY)) + { + int least_sign_ikey = so->numberOfKeys - 1; + bool continuescan; + + /* + * Optimization: perform a precheck of the least significant key + * during !sktrig_required calls when it isn't already our sktrig + * (provided the precheck key is not itself an array). + * + * When the precheck works out we'll avoid an expensive binary search + * of sktrig's array (plus any other arrays before least_sign_ikey). + */ + Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY); + if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false, + false, &continuescan, + &least_sign_ikey)) + return false; + } + + for (int ikey = 0; ikey < so->numberOfKeys; ikey++) + { + ScanKey cur = so->keyData + ikey; + BTArrayKeyInfo *array = NULL; + Datum tupdatum; + bool required = false, + tupnull; + int32 result; + int set_elem = 0; + + if (cur->sk_strategy == BTEqualStrategyNumber) + { + /* Manage array state */ + if (cur->sk_flags & SK_SEARCHARRAY) + { + array = &so->arrayKeys[arrayidx++]; + Assert(array->scan_key == ikey); + } + } + else + { + /* + * Are any inequalities required in the opposite direction only + * present here? + */ + if (((ScanDirectionIsForward(dir) && + (cur->sk_flags & (SK_BT_REQBKWD))) || + (ScanDirectionIsBackward(dir) && + (cur->sk_flags & (SK_BT_REQFWD))))) + has_required_opposite_direction_only = true; + } + + /* Optimization: skip over known-satisfied scan keys */ + if (ikey < sktrig) + continue; + + if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) + { + required = true; + + if (cur->sk_attno > tupnatts) + { + /* Set this just like _bt_tuple_before_array_skeys */ + Assert(sktrig < ikey); + so->scanBehind = true; + } + } + + /* + * Handle a required non-array scan key that the initial call to + * _bt_check_compare indicated triggered array advancement, if any. + * + * The non-array scan key's strategy will be <, <=, or = during a + * forwards scan (or any one of =, >=, or > during a backwards scan). + * It follows that the corresponding tuple attribute's value must now + * be either > or >= the scan key value (for backwards scans it must + * be either < or <= that value). + * + * If this is a required equality strategy scan key, this is just an + * optimization; _bt_tuple_before_array_skeys already confirmed that + * this scan key places us ahead of caller's tuple. There's no need + * to repeat that work now. (The same underlying principle also gets + * applied by the cur_elem_trig optimization used to speed up searches + * for the next array element.) + * + * If this is a required inequality strategy scan key, we _must_ rely + * on _bt_check_compare like this; we aren't capable of directly + * evaluating required inequality strategy scan keys here, on our own. + */ + if (ikey == sktrig && !array) + { + Assert(sktrig_required && required && all_required_satisfied); + + /* Use "beyond end" advancement. See below for an explanation. */ + beyond_end_advance = true; + all_satisfied = all_required_satisfied = false; + + continue; + } + + /* + * Nothing more for us to do with an inequality strategy scan key that + * wasn't the one that _bt_check_compare stopped on, though. + * + * Note: if our later call to _bt_check_compare (to recheck caller's + * tuple) sets continuescan=false due to finding this same inequality + * unsatisfied (possible when it's required in the scan direction), + * we'll deal with it via a recursive "second pass" call. + */ + else if (cur->sk_strategy != BTEqualStrategyNumber) + continue; + + /* + * Nothing for us to do with an equality strategy scan key that isn't + * marked required, either -- unless it's a non-required array + */ + else if (!required && !array) + continue; + + /* + * Here we perform steps for all array scan keys after a required + * array scan key whose binary search triggered "beyond end of array + * element" array advancement due to encountering a tuple attribute + * value > the closest matching array key (or < for backwards scans). + */ + if (beyond_end_advance) + { + if (array) + _bt_array_set_low_or_high(rel, cur, array, + ScanDirectionIsBackward(dir)); + + continue; + } + + /* + * Here we perform steps for all array scan keys after a required + * array scan key whose tuple attribute was < the closest matching + * array key when we dealt with it (or > for backwards scans). + * + * This earlier required array key already puts us ahead of caller's + * tuple in the key space (for the current scan direction). We must + * make sure that subsequent lower-order array keys do not put us too + * far ahead (ahead of tuples that have yet to be seen by our caller). + * For example, when a tuple "(a, b) = (42, 5)" advances the array + * keys on "a" from 40 to 45, we must also set "b" to whatever the + * first array element for "b" is. It would be wrong to allow "b" to + * be set based on the tuple value. + * + * Perform the same steps with truncated high key attributes. You can + * think of this as a "binary search" for the element closest to the + * value -inf. Again, the arrays must never get ahead of the scan. + */ + if (!all_required_satisfied || cur->sk_attno > tupnatts) + { + if (array) + _bt_array_set_low_or_high(rel, cur, array, + ScanDirectionIsForward(dir)); + + continue; + } + + /* + * Search in scankey's array for the corresponding tuple attribute + * value from caller's tuple + */ + tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull); + + if (array) + { + bool cur_elem_trig = (sktrig_required && ikey == sktrig); + + /* + * "Binary search" by checking if tupdatum/tupnull are within the + * range of the skip array + */ + if (array->num_elems == -1) + _bt_binsrch_skiparray_skey(cur_elem_trig, dir, + tupdatum, tupnull, array, cur, + &result); + + /* + * Binary search for the closest match from the SAOP array + */ + else + set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey], + cur_elem_trig, dir, + tupdatum, tupnull, array, cur, + &result); + } + else + { + Assert(required); + + /* + * This is a required non-array equality strategy scan key, which + * we'll treat as a degenerate single element array. + * + * This scan key's imaginary "array" can't really advance, but it + * can still roll over like any other array. (Actually, this is + * no different to real single value arrays, which never advance + * without rolling over -- they can never truly advance, either.) + */ + result = _bt_compare_array_skey(&so->orderProcs[ikey], + tupdatum, tupnull, + cur->sk_argument, cur); + } + + /* + * Consider "beyond end of array element" array advancement. + * + * When the tuple attribute value is > the closest matching array key + * (or < in the backwards scan case), we need to ratchet this array + * forward (backward) by one increment, so that caller's tuple ends up + * being < final array value instead (or > final array value instead). + * This process has to work for all of the arrays, not just this one: + * it must "carry" to higher-order arrays when the set_elem that we + * just found happens to be the final one for the scan's direction. + * Incrementing (decrementing) set_elem itself isn't good enough. + * + * Our approach is to provisionally use set_elem as if it was an exact + * match now, then set each later/less significant array to whatever + * its final element is. Once outside the loop we'll then "increment + * this array's set_elem" by calling _bt_advance_array_keys_increment. + * That way the process rolls over to higher order arrays as needed. + * + * Under this scheme any required arrays only ever ratchet forwards + * (or backwards), and always do so to the maximum possible extent + * that we can know will be safe without seeing the scan's next tuple. + * We don't need any special handling for required scan keys that lack + * a real array to advance, nor for redundant scan keys that couldn't + * be eliminated by _bt_preprocess_keys. It won't matter if some of + * our "true" array scan keys (or even all of them) are non-required. + */ + if (sktrig_required && required && + ((ScanDirectionIsForward(dir) && result > 0) || + (ScanDirectionIsBackward(dir) && result < 0))) + beyond_end_advance = true; + + Assert(all_required_satisfied && all_satisfied); + if (result != 0) + { + /* + * Track whether caller's tuple satisfies our new post-advancement + * qual, for required scan keys, as well as for the entire set of + * interesting scan keys (all required scan keys plus non-required + * array scan keys are considered interesting.) + */ + all_satisfied = false; + if (sktrig_required && required) + all_required_satisfied = false; + else + { + /* + * There's no need to advance the arrays using the best + * available match for a non-required array. Give up now. + * (Though note that sktrig_required calls still have to do + * all the usual post-advancement steps, including the recheck + * call to _bt_check_compare.) + */ + break; + } + } + + /* Advance array keys, even when we don't have an exact match */ + if (array) + { + if (array->num_elems == -1) + { + /* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */ + _bt_skiparray_set_element(rel, cur, array, result, + tupdatum, tupnull); + skip_array_advanced = true; + } + else if (array->cur_elem != set_elem) + { + /* SAOP array's new element is set_elem datum */ + array->cur_elem = set_elem; + cur->sk_argument = array->elem_values[set_elem]; + } + } + } + + /* + * Advance the array keys incrementally whenever "beyond end of array + * element" array advancement happens, so that advancement will carry to + * higher-order arrays (might exhaust all the scan's arrays instead, which + * ends the top-level scan). + */ + if (beyond_end_advance && + !_bt_advance_array_keys_increment(scan, dir, &skip_array_advanced)) + goto end_toplevel_scan; + + Assert(_bt_verify_keys_with_arraykeys(scan)); + + /* + * Maintain a page-level count of the number of times the scan's array + * keys advanced in a way that affected at least one skip array + */ + if (sktrig_required && skip_array_advanced) + pstate->nskipadvances++; + + /* + * Does tuple now satisfy our new qual? Recheck with _bt_check_compare. + * + * Calls triggered by an unsatisfied required scan key, whose tuple now + * satisfies all required scan keys, but not all nonrequired array keys, + * will still require a recheck call to _bt_check_compare. They'll still + * need its "second pass" handling of required inequality scan keys. + * (Might have missed a still-unsatisfied required inequality scan key + * that caller didn't detect as the sktrig scan key during its initial + * _bt_check_compare call that used the old/original qual.) + * + * Calls triggered by an unsatisfied nonrequired array scan key never need + * "second pass" handling of required inequalities (nor any other handling + * of any required scan key). All that matters is whether caller's tuple + * satisfies the new qual, so it's safe to just skip the _bt_check_compare + * recheck when we've already determined that it can only return 'false'. + * + * Note: In practice most scan keys are marked required by preprocessing, + * if necessary by generating a preceding skip array. We nevertheless + * often handle array keys marked required as if they were nonrequired. + * This behavior is requested by our _bt_check_compare caller, though only + * when it is passed "forcenonrequired=true" by _bt_checkkeys. + */ + if ((sktrig_required && all_required_satisfied) || + (!sktrig_required && all_satisfied)) + { + int nsktrig = sktrig + 1; + bool continuescan; + + Assert(all_required_satisfied); + + /* Recheck _bt_check_compare on behalf of caller */ + if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false, + !sktrig_required, &continuescan, + &nsktrig) && + !so->scanBehind) + { + /* This tuple satisfies the new qual */ + Assert(all_satisfied && continuescan); + + if (pstate) + pstate->continuescan = true; + + return true; + } + + /* + * Consider "second pass" handling of required inequalities. + * + * It's possible that our _bt_check_compare call indicated that the + * scan should end due to some unsatisfied inequality that wasn't + * initially recognized as such by us. Handle this by calling + * ourselves recursively, this time indicating that the trigger is the + * inequality that we missed first time around (and using a set of + * required array/equality keys that are now exact matches for tuple). + * + * We make a strong, general guarantee that every _bt_checkkeys call + * here will advance the array keys to the maximum possible extent + * that we can know to be safe based on caller's tuple alone. If we + * didn't perform this step, then that guarantee wouldn't quite hold. + */ + if (unlikely(!continuescan)) + { + bool satisfied PG_USED_FOR_ASSERTS_ONLY; + + Assert(sktrig_required); + Assert(so->keyData[nsktrig].sk_strategy != BTEqualStrategyNumber); + + /* + * The tuple must use "beyond end" advancement during the + * recursive call, so we cannot possibly end up back here when + * recursing. We'll consume a small, fixed amount of stack space. + */ + Assert(!beyond_end_advance); + + /* Advance the array keys a second time using same tuple */ + satisfied = _bt_advance_array_keys(scan, pstate, tuple, tupnatts, + tupdesc, nsktrig, true); + + /* This tuple doesn't satisfy the inequality */ + Assert(!satisfied); + return false; + } + + /* + * Some non-required scan key (from new qual) still not satisfied. + * + * All scan keys required in the current scan direction must still be + * satisfied, though, so we can trust all_required_satisfied below. + */ + } + + /* + * When we were called just to deal with "advancing" non-required arrays, + * this is as far as we can go (cannot stop the scan for these callers) + */ + if (!sktrig_required) + { + /* Caller's tuple doesn't match any qual */ + return false; + } + + /* + * Postcondition array state assertion (for still-unsatisfied tuples). + * + * By here we have established that the scan's required arrays (scan must + * have at least one required array) advanced, without becoming exhausted. + * + * Caller's tuple is now < the newly advanced array keys (or > when this + * is a backwards scan), except in the case where we only got this far due + * to an unsatisfied non-required scan key. Verify that with an assert. + * + * Note: we don't just quit at this point when all required scan keys were + * found to be satisfied because we need to consider edge-cases involving + * scan keys required in the opposite direction only; those aren't tracked + * by all_required_satisfied. + */ + Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, + false, 0, NULL) == + !all_required_satisfied); + + /* + * We generally permit primitive index scans to continue onto the next + * sibling page when the page's finaltup satisfies all required scan keys + * at the point where we're between pages. + * + * If caller's tuple is also the page's finaltup, and we see that required + * scan keys still aren't satisfied, start a new primitive index scan. + */ + if (!all_required_satisfied && pstate->finaltup == tuple) + goto new_prim_scan; + + /* + * Proactively check finaltup (don't wait until finaltup is reached by the + * scan) when it might well turn out to not be satisfied later on. + * + * Note: if so->scanBehind hasn't already been set for finaltup by us, + * it'll be set during this call to _bt_tuple_before_array_skeys. Either + * way, it'll be set correctly (for the whole page) after this point. + */ + if (!all_required_satisfied && pstate->finaltup && + _bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc, + BTreeTupleGetNAtts(pstate->finaltup, rel), + false, 0, &so->scanBehind)) + goto new_prim_scan; + + /* + * When we encounter a truncated finaltup high key attribute, we're + * optimistic about the chances of its corresponding required scan key + * being satisfied when we go on to recheck it against tuples from this + * page's right sibling leaf page. We consider truncated attributes to be + * satisfied by required scan keys, which allows the primitive index scan + * to continue to the next leaf page. We must set so->scanBehind to true + * to remember that the last page's finaltup had "satisfied" required scan + * keys for one or more truncated attribute values (scan keys required in + * _either_ scan direction). + * + * There is a chance that _bt_readpage (which checks so->scanBehind) will + * find that even the sibling leaf page's finaltup is < the new array + * keys. When that happens, our optimistic policy will have incurred a + * single extra leaf page access that could have been avoided. + * + * A pessimistic policy would give backward scans a gratuitous advantage + * over forward scans. We'd punish forward scans for applying more + * accurate information from the high key, rather than just using the + * final non-pivot tuple as finaltup, in the style of backward scans. + * Being pessimistic would also give some scans with non-required arrays a + * perverse advantage over similar scans that use required arrays instead. + * + * This is similar to our scan-level heuristics, below. They also set + * scanBehind to speculatively continue the primscan onto the next page. + */ + if (so->scanBehind) + { + /* Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled */ + } + + /* + * Handle inequalities marked required in the opposite scan direction. + * They can also signal that we should start a new primitive index scan. + * + * It's possible that the scan is now positioned where "matching" tuples + * begin, and that caller's tuple satisfies all scan keys required in the + * current scan direction. But if caller's tuple still doesn't satisfy + * other scan keys that are required in the opposite scan direction only + * (e.g., a required >= strategy scan key when scan direction is forward), + * it's still possible that there are many leaf pages before the page that + * _bt_first could skip straight to. Groveling through all those pages + * will always give correct answers, but it can be very inefficient. We + * must avoid needlessly scanning extra pages. + * + * Separately, it's possible that _bt_check_compare set continuescan=false + * for a scan key that's required in the opposite direction only. This is + * a special case, that happens only when _bt_check_compare sees that the + * inequality encountered a NULL value. This signals the end of non-NULL + * values in the current scan direction, which is reason enough to end the + * (primitive) scan. If this happens at the start of a large group of + * NULL values, then we shouldn't expect to be called again until after + * the scan has already read indefinitely-many leaf pages full of tuples + * with NULL suffix values. (_bt_first is expected to skip over the group + * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which + * involves consing up an explicit SK_SEARCHNOTNULL key.) + * + * Apply a test against finaltup to detect and recover from the problem: + * if even finaltup doesn't satisfy such an inequality, we just skip by + * starting a new primitive index scan. When we skip, we know for sure + * that all of the tuples on the current page following caller's tuple are + * also before the _bt_first-wise start of tuples for our new qual. That + * at least suggests many more skippable pages beyond the current page. + * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen + * when we test the next page's finaltup/high key instead.) + */ + else if (has_required_opposite_direction_only && pstate->finaltup && + unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup))) + goto new_prim_scan; + +continue_scan: + + /* + * Stick with the ongoing primitive index scan for now. + * + * It's possible that later tuples will also turn out to have values that + * are still < the now-current array keys (or > the current array keys). + * Our caller will handle this by performing what amounts to a linear + * search of the page, implemented by calling _bt_check_compare and then + * _bt_tuple_before_array_skeys for each tuple. + * + * This approach has various advantages over a binary search of the page. + * Repeated binary searches of the page (one binary search for every array + * advancement) won't outperform a continuous linear search. While there + * are workloads that a naive linear search won't handle well, our caller + * has a "look ahead" fallback mechanism to deal with that problem. + */ + pstate->continuescan = true; /* Override _bt_check_compare */ + so->needPrimScan = false; /* _bt_readpage has more tuples to check */ + + if (so->scanBehind) + { + /* + * Remember if recheck needs to call _bt_oppodir_checkkeys for next + * page's finaltup (see above comments about "Handle inequalities + * marked required in the opposite scan direction" for why). + */ + so->oppositeDirCheck = has_required_opposite_direction_only; + + /* + * skip by setting "look ahead" mechanism's offnum for forwards scans + * (backwards scans check scanBehind flag directly instead) + */ + if (ScanDirectionIsForward(dir)) + pstate->skip = pstate->maxoff + 1; + } + + /* Caller's tuple doesn't match the new qual */ + return false; + +new_prim_scan: + + Assert(pstate->finaltup); /* not on rightmost/leftmost page */ + + /* + * Looks like another primitive index scan is required. But consider + * continuing the current primscan based on scan-level heuristics. + * + * Continue the ongoing primitive scan (and schedule a recheck for when + * the scan arrives on the next sibling leaf page) when it has already + * read at least one leaf page before the one we're reading now. This + * makes primscan scheduling more efficient when scanning subsets of an + * index with many distinct attribute values matching many array elements. + * It encourages fewer, larger primitive scans where that makes sense. + * This will in turn encourage _bt_readpage to apply the pstate.startikey + * optimization more often. + * + * Also continue the ongoing primitive index scan when it is still on the + * first page if there have been more than NSKIPADVANCES_THRESHOLD calls + * here that each advanced at least one of the scan's skip arrays + * (deliberately ignore advancements that only affected SAOP arrays here). + * A page that cycles through this many skip array elements is quite + * likely to neighbor similar pages, that we'll also need to read. + * + * Note: These heuristics aren't as aggressive as you might think. We're + * conservative about allowing a primitive scan to step from the first + * leaf page it reads to the page's sibling page (we only allow it on + * first pages whose finaltup strongly suggests that it'll work out, as + * well as first pages that have a large number of skip array advances). + * Clearing this first page finaltup hurdle is a strong signal in itself. + * + * Note: The NSKIPADVANCES_THRESHOLD heuristic exists only to avoid + * pathological cases. Specifically, cases where a skip scan should just + * behave like a traditional full index scan, but ends up "skipping" again + * and again, descending to the prior leaf page's direct sibling leaf page + * each time. This misbehavior would otherwise be possible during scans + * that never quite manage to "clear the first page finaltup hurdle". + */ + if (!pstate->firstpage || pstate->nskipadvances > NSKIPADVANCES_THRESHOLD) + { + /* Schedule a recheck once on the next (or previous) page */ + so->scanBehind = true; + + /* Continue the current primitive scan after all */ + goto continue_scan; + } + + /* + * End this primitive index scan, but schedule another. + * + * Note: We make a soft assumption that the current scan direction will + * also be used within _bt_next, when it is asked to step off this page. + * It is up to _bt_next to cancel this scheduled primitive index scan + * whenever it steps to a page in the direction opposite currPos.dir. + */ + pstate->continuescan = false; /* Tell _bt_readpage we're done... */ + so->needPrimScan = true; /* ...but call _bt_first again */ + + if (scan->parallel_scan) + _bt_parallel_primscan_schedule(scan, so->currPos.currPage); + + /* Caller's tuple doesn't match the new qual */ + return false; + +end_toplevel_scan: + + /* + * End the current primitive index scan, but don't schedule another. + * + * This ends the entire top-level scan in the current scan direction. + * + * Note: The scan's arrays (including any non-required arrays) are now in + * their final positions for the current scan direction. If the scan + * direction happens to change, then the arrays will already be in their + * first positions for what will then be the current scan direction. + */ + pstate->continuescan = false; /* Tell _bt_readpage we're done... */ + so->needPrimScan = false; /* ...and don't call _bt_first again */ + + /* Caller's tuple doesn't match any qual */ + return false; +} + +/* + * _bt_advance_array_keys_increment() -- Advance to next set of array elements + * + * Advances the array keys by a single increment in the current scan + * direction. When there are multiple array keys this can roll over from the + * lowest order array to higher order arrays. + * + * Returns true if there is another set of values to consider, false if not. + * On true result, the scankeys are initialized with the next set of values. + * On false result, the scankeys stay the same, and the array keys are not + * advanced (every array remains at its final element for scan direction). + */ +static bool +_bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir, + bool *skip_array_set) +{ + Relation rel = scan->indexRelation; + BTScanOpaque so = (BTScanOpaque) scan->opaque; + + /* + * We must advance the last array key most quickly, since it will + * correspond to the lowest-order index column among the available + * qualifications + */ + for (int i = so->numArrayKeys - 1; i >= 0; i--) + { + BTArrayKeyInfo *array = &so->arrayKeys[i]; + ScanKey skey = &so->keyData[array->scan_key]; + + if (array->num_elems == -1) + *skip_array_set = true; + + if (ScanDirectionIsForward(dir)) + { + if (_bt_array_increment(rel, skey, array)) + return true; + } + else + { + if (_bt_array_decrement(rel, skey, array)) + return true; + } + + /* + * Couldn't increment (or decrement) array. Handle array roll over. + * + * Start over at the array's lowest sorting value (or its highest + * value, for backward scans)... + */ + _bt_array_set_low_or_high(rel, skey, array, + ScanDirectionIsForward(dir)); + + /* ...then increment (or decrement) next most significant array */ + } + + /* + * The array keys are now exhausted. + * + * Restore the array keys to the state they were in immediately before we + * were called. This ensures that the arrays only ever ratchet in the + * current scan direction. + * + * Without this, scans could overlook matching tuples when the scan + * direction gets reversed just before btgettuple runs out of items to + * return, but just after _bt_readpage prepares all the items from the + * scan's final page in so->currPos. When we're on the final page it is + * typical for so->currPos to get invalidated once btgettuple finally + * returns false, which'll effectively invalidate the scan's array keys. + * That hasn't happened yet, though -- and in general it may never happen. + */ + _bt_start_array_keys(scan, -dir); + + return false; +} + +/* + * _bt_array_increment() -- increment array scan key's sk_argument + * + * Return value indicates whether caller's array was successfully incremented. + * Cannot increment an array whose current element is already the final one. + */ +static bool +_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array) +{ + bool oflow = false; + Datum inc_sk_argument; + + Assert(skey->sk_flags & SK_SEARCHARRAY); + Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR))); + + /* SAOP array? */ + if (array->num_elems != -1) + { + Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL))); + if (array->cur_elem < array->num_elems - 1) + { + /* + * Just increment current element, and assign its datum to skey + * (only skip arrays need us to free existing sk_argument memory) + */ + array->cur_elem++; + skey->sk_argument = array->elem_values[array->cur_elem]; + + /* Successfully incremented array */ + return true; + } + + /* Cannot increment past final array element */ + return false; + } + + /* Nope, this is a skip array */ + Assert(skey->sk_flags & SK_BT_SKIP); + + /* + * The sentinel value that represents the maximum value within the range + * of a skip array (often just +inf) is never incrementable + */ + if (skey->sk_flags & SK_BT_MAXVAL) + return false; + + /* + * When the current array element is NULL, and the highest sorting value + * in the index is also NULL, we cannot increment past the final element + */ + if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST)) + return false; + + /* + * Opclasses without skip support "increment" the scan key's current + * element by setting the NEXT flag. The true next value is determined by + * repositioning to the first index tuple > existing sk_argument/current + * array element. Note that this works in the usual way when the scan key + * is already marked ISNULL (i.e. when the current element is NULL). + */ + if (!array->sksup) + { + /* Successfully "incremented" array */ + skey->sk_flags |= SK_BT_NEXT; + return true; + } + + /* + * Opclasses with skip support directly increment sk_argument + */ + if (skey->sk_flags & SK_ISNULL) + { + Assert(skey->sk_flags & SK_BT_NULLS_FIRST); + + /* + * Existing sk_argument/array element is NULL (for an IS NULL qual). + * + * "Increment" from NULL to the low_elem value provided by opclass + * skip support routine. + */ + skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL); + skey->sk_argument = datumCopy(array->sksup->low_elem, + array->attbyval, array->attlen); + return true; + } + + /* + * Ask opclass support routine to provide incremented copy of existing + * non-NULL sk_argument + */ + inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow); + if (unlikely(oflow)) + { + /* inc_sk_argument has undefined value (so no pfree) */ + if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST)) + { + _bt_skiparray_set_isnull(rel, skey, array); + + /* Successfully "incremented" array to NULL */ + return true; + } + + /* Cannot increment past final array element */ + return false; + } + + /* + * Successfully incremented sk_argument to a non-NULL value. Make sure + * that the incremented value is still within the range of the array. + */ + if (array->high_compare && + !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, + array->high_compare->sk_collation, + inc_sk_argument, + array->high_compare->sk_argument))) + { + /* Keep existing sk_argument after all */ + if (!array->attbyval) + pfree(DatumGetPointer(inc_sk_argument)); + + /* Cannot increment past final array element */ + return false; + } + + /* Accept value returned by opclass increment callback */ + if (!array->attbyval && skey->sk_argument) + pfree(DatumGetPointer(skey->sk_argument)); + skey->sk_argument = inc_sk_argument; + + /* Successfully incremented array */ + return true; +} + +/* + * _bt_array_decrement() -- decrement array scan key's sk_argument + * + * Return value indicates whether caller's array was successfully decremented. + * Cannot decrement an array whose current element is already the first one. + */ +static bool +_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array) +{ + bool uflow = false; + Datum dec_sk_argument; + + Assert(skey->sk_flags & SK_SEARCHARRAY); + Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR))); + + /* SAOP array? */ + if (array->num_elems != -1) + { + Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL))); + if (array->cur_elem > 0) + { + /* + * Just decrement current element, and assign its datum to skey + * (only skip arrays need us to free existing sk_argument memory) + */ + array->cur_elem--; + skey->sk_argument = array->elem_values[array->cur_elem]; + + /* Successfully decremented array */ + return true; + } + + /* Cannot decrement to before first array element */ + return false; + } + + /* Nope, this is a skip array */ + Assert(skey->sk_flags & SK_BT_SKIP); + + /* + * The sentinel value that represents the minimum value within the range + * of a skip array (often just -inf) is never decrementable + */ + if (skey->sk_flags & SK_BT_MINVAL) + return false; + + /* + * When the current array element is NULL, and the lowest sorting value in + * the index is also NULL, we cannot decrement before first array element + */ + if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST)) + return false; + + /* + * Opclasses without skip support "decrement" the scan key's current + * element by setting the PRIOR flag. The true prior value is determined + * by repositioning to the last index tuple < existing sk_argument/current + * array element. Note that this works in the usual way when the scan key + * is already marked ISNULL (i.e. when the current element is NULL). + */ + if (!array->sksup) + { + /* Successfully "decremented" array */ + skey->sk_flags |= SK_BT_PRIOR; + return true; + } + + /* + * Opclasses with skip support directly decrement sk_argument + */ + if (skey->sk_flags & SK_ISNULL) + { + Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST)); + + /* + * Existing sk_argument/array element is NULL (for an IS NULL qual). + * + * "Decrement" from NULL to the high_elem value provided by opclass + * skip support routine. + */ + skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL); + skey->sk_argument = datumCopy(array->sksup->high_elem, + array->attbyval, array->attlen); + return true; + } + + /* + * Ask opclass support routine to provide decremented copy of existing + * non-NULL sk_argument + */ + dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow); + if (unlikely(uflow)) + { + /* dec_sk_argument has undefined value (so no pfree) */ + if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST)) + { + _bt_skiparray_set_isnull(rel, skey, array); + + /* Successfully "decremented" array to NULL */ + return true; + } + + /* Cannot decrement to before first array element */ + return false; + } + + /* + * Successfully decremented sk_argument to a non-NULL value. Make sure + * that the decremented value is still within the range of the array. + */ + if (array->low_compare && + !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, + array->low_compare->sk_collation, + dec_sk_argument, + array->low_compare->sk_argument))) + { + /* Keep existing sk_argument after all */ + if (!array->attbyval) + pfree(DatumGetPointer(dec_sk_argument)); + + /* Cannot decrement to before first array element */ + return false; + } + + /* Accept value returned by opclass decrement callback */ + if (!array->attbyval && skey->sk_argument) + pfree(DatumGetPointer(skey->sk_argument)); + skey->sk_argument = dec_sk_argument; + + /* Successfully decremented array */ + return true; +} + +/* + * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element + * + * Caller also passes associated scan key, which will have its argument set to + * the lowest/highest array value in passing. + */ +static void +_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array, + bool low_not_high) +{ + Assert(skey->sk_flags & SK_SEARCHARRAY); + + if (array->num_elems != -1) + { + /* set low or high element for SAOP array */ + int set_elem = 0; + + Assert(!(skey->sk_flags & SK_BT_SKIP)); + + if (!low_not_high) + set_elem = array->num_elems - 1; + + /* + * Just copy over array datum (only skip arrays require freeing and + * allocating memory for sk_argument) + */ + array->cur_elem = set_elem; + skey->sk_argument = array->elem_values[set_elem]; + + return; + } + + /* set low or high element for skip array */ + Assert(skey->sk_flags & SK_BT_SKIP); + Assert(array->num_elems == -1); + + /* Free memory previously allocated for sk_argument if needed */ + if (!array->attbyval && skey->sk_argument) + pfree(DatumGetPointer(skey->sk_argument)); + + /* Reset flags */ + skey->sk_argument = (Datum) 0; + skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL | + SK_BT_MINVAL | SK_BT_MAXVAL | + SK_BT_NEXT | SK_BT_PRIOR); + + if (array->null_elem && + (low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0))) + { + /* Requested element (either lowest or highest) has the value NULL */ + skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL); + } + else if (low_not_high) + { + /* Setting array to lowest element (according to low_compare) */ + skey->sk_flags |= SK_BT_MINVAL; + } + else + { + /* Setting array to highest element (according to high_compare) */ + skey->sk_flags |= SK_BT_MAXVAL; + } +} + +/* + * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument + * + * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for + * caller's tupdatum/tupnull. + * + * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0. + * Otherwise, we set skey to either the lowest or highest value that's within + * the range of caller's skip array (whichever is the best available match to + * tupdatum/tupnull that is still within the range of the skip array according + * to _bt_binsrch_skiparray_skey/set_elem_result). + */ +static void +_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array, + int32 set_elem_result, Datum tupdatum, bool tupnull) +{ + Assert(skey->sk_flags & SK_BT_SKIP); + Assert(skey->sk_flags & SK_SEARCHARRAY); + + if (set_elem_result) + { + /* tupdatum/tupnull is out of the range of the skip array */ + Assert(!array->null_elem); + + _bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0); + return; + } + + /* Advance skip array to tupdatum (or tupnull) value */ + if (unlikely(tupnull)) + { + _bt_skiparray_set_isnull(rel, skey, array); + return; + } + + /* Free memory previously allocated for sk_argument if needed */ + if (!array->attbyval && skey->sk_argument) + pfree(DatumGetPointer(skey->sk_argument)); + + /* tupdatum becomes new sk_argument/new current element */ + skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL | + SK_BT_MINVAL | SK_BT_MAXVAL | + SK_BT_NEXT | SK_BT_PRIOR); + skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen); +} + +/* + * _bt_skiparray_set_isnull() -- set skip array scan key to NULL + */ +static void +_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array) +{ + Assert(skey->sk_flags & SK_BT_SKIP); + Assert(skey->sk_flags & SK_SEARCHARRAY); + Assert(array->null_elem && !array->low_compare && !array->high_compare); + + /* Free memory previously allocated for sk_argument if needed */ + if (!array->attbyval && skey->sk_argument) + pfree(DatumGetPointer(skey->sk_argument)); + + /* NULL becomes new sk_argument/new current element */ + skey->sk_argument = (Datum) 0; + skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL | + SK_BT_NEXT | SK_BT_PRIOR); + skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL); +} + +/* + * _bt_compare_array_skey() -- apply array comparison function + * + * Compares caller's tuple attribute value to a scan key/array element. + * Helper function used during binary searches of SK_SEARCHARRAY arrays. + * + * This routine returns: + * <0 if tupdatum < arrdatum; + * 0 if tupdatum == arrdatum; + * >0 if tupdatum > arrdatum. + * + * This is essentially the same interface as _bt_compare: both functions + * compare the value that they're searching for to a binary search pivot. + * However, unlike _bt_compare, this function's "tuple argument" comes first, + * while its "array/scankey argument" comes second. +*/ +static inline int32 +_bt_compare_array_skey(FmgrInfo *orderproc, + Datum tupdatum, bool tupnull, + Datum arrdatum, ScanKey cur) +{ + int32 result = 0; + + Assert(cur->sk_strategy == BTEqualStrategyNumber); + Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))); + + if (tupnull) /* NULL tupdatum */ + { + if (cur->sk_flags & SK_ISNULL) + result = 0; /* NULL "=" NULL */ + else if (cur->sk_flags & SK_BT_NULLS_FIRST) + result = -1; /* NULL "<" NOT_NULL */ + else + result = 1; /* NULL ">" NOT_NULL */ + } + else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */ + { + if (cur->sk_flags & SK_BT_NULLS_FIRST) + result = 1; /* NOT_NULL ">" NULL */ + else + result = -1; /* NOT_NULL "<" NULL */ + } + else + { + /* + * Like _bt_compare, we need to be careful of cross-type comparisons, + * so the left value has to be the value that came from an index tuple + */ + result = DatumGetInt32(FunctionCall2Coll(orderproc, cur->sk_collation, + tupdatum, arrdatum)); + + /* + * We flip the sign by following the obvious rule: flip whenever the + * column is a DESC column. + * + * _bt_compare does it the wrong way around (flip when *ASC*) in order + * to compensate for passing its orderproc arguments backwards. We + * don't need to play these games because we find it natural to pass + * tupdatum as the left value (and arrdatum as the right value). + */ + if (cur->sk_flags & SK_BT_DESC) + INVERT_COMPARE_RESULT(result); + } + + return result; +} + +/* + * _bt_binsrch_array_skey() -- Binary search for next matching array key + * + * Returns an index to the first array element >= caller's tupdatum argument. + * This convention is more natural for forwards scan callers, but that can't + * really matter to backwards scan callers. Both callers require handling for + * the case where the match we return is < tupdatum, and symmetric handling + * for the case where our best match is > tupdatum. + * + * Also sets *set_elem_result to the result _bt_compare_array_skey returned + * when we used it to compare the matching array element to tupdatum/tupnull. + * + * cur_elem_trig indicates if array advancement was triggered by this array's + * scan key, and that the array is for a required scan key. We can apply this + * information to find the next matching array element in the current scan + * direction using far fewer comparisons (fewer on average, compared to naive + * binary search). This scheme takes advantage of an important property of + * required arrays: required arrays always advance in lockstep with the index + * scan's progress through the index's key space. + */ +int +_bt_binsrch_array_skey(FmgrInfo *orderproc, + bool cur_elem_trig, ScanDirection dir, + Datum tupdatum, bool tupnull, + BTArrayKeyInfo *array, ScanKey cur, + int32 *set_elem_result) +{ + int low_elem = 0, + mid_elem = -1, + high_elem = array->num_elems - 1, + result = 0; + Datum arrdatum; + + Assert(cur->sk_flags & SK_SEARCHARRAY); + Assert(!(cur->sk_flags & SK_BT_SKIP)); + Assert(!(cur->sk_flags & SK_ISNULL)); /* SAOP arrays never have NULLs */ + Assert(cur->sk_strategy == BTEqualStrategyNumber); + + if (cur_elem_trig) + { + Assert(!ScanDirectionIsNoMovement(dir)); + Assert(cur->sk_flags & SK_BT_REQFWD); + + /* + * When the scan key that triggered array advancement is a required + * array scan key, it is now certain that the current array element + * (plus all prior elements relative to the current scan direction) + * cannot possibly be at or ahead of the corresponding tuple value. + * (_bt_checkkeys must have called _bt_tuple_before_array_skeys, which + * makes sure this is true as a condition of advancing the arrays.) + * + * This makes it safe to exclude array elements up to and including + * the former-current array element from our search. + * + * Separately, when array advancement was triggered by a required scan + * key, the array element immediately after the former-current element + * is often either an exact tupdatum match, or a "close by" near-match + * (a near-match tupdatum is one whose key space falls _between_ the + * former-current and new-current array elements). We'll detect both + * cases via an optimistic comparison of the new search lower bound + * (or new search upper bound in the case of backwards scans). + */ + if (ScanDirectionIsForward(dir)) + { + low_elem = array->cur_elem + 1; /* old cur_elem exhausted */ + + /* Compare prospective new cur_elem (also the new lower bound) */ + if (high_elem >= low_elem) + { + arrdatum = array->elem_values[low_elem]; + result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, + arrdatum, cur); + + if (result <= 0) + { + /* Optimistic comparison optimization worked out */ + *set_elem_result = result; + return low_elem; + } + mid_elem = low_elem; + low_elem++; /* this cur_elem exhausted, too */ + } + + if (high_elem < low_elem) + { + /* Caller needs to perform "beyond end" array advancement */ + *set_elem_result = 1; + return high_elem; + } + } + else + { + high_elem = array->cur_elem - 1; /* old cur_elem exhausted */ + + /* Compare prospective new cur_elem (also the new upper bound) */ + if (high_elem >= low_elem) + { + arrdatum = array->elem_values[high_elem]; + result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, + arrdatum, cur); + + if (result >= 0) + { + /* Optimistic comparison optimization worked out */ + *set_elem_result = result; + return high_elem; + } + mid_elem = high_elem; + high_elem--; /* this cur_elem exhausted, too */ + } + + if (high_elem < low_elem) + { + /* Caller needs to perform "beyond end" array advancement */ + *set_elem_result = -1; + return low_elem; + } + } + } + + while (high_elem > low_elem) + { + mid_elem = low_elem + ((high_elem - low_elem) / 2); + arrdatum = array->elem_values[mid_elem]; + + result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, + arrdatum, cur); + + if (result == 0) + { + /* + * It's safe to quit as soon as we see an equal array element. + * This often saves an extra comparison or two... + */ + low_elem = mid_elem; + break; + } + + if (result > 0) + low_elem = mid_elem + 1; + else + high_elem = mid_elem; + } + + /* + * ...but our caller also cares about how its searched-for tuple datum + * compares to the low_elem datum. Must always set *set_elem_result with + * the result of that comparison specifically. + */ + if (low_elem != mid_elem) + result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, + array->elem_values[low_elem], cur); + + *set_elem_result = result; + + return low_elem; +} + +/* + * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array + * + * Does not return an index into the array, since skip arrays don't really + * contain elements (they generate their array elements procedurally instead). + * Our interface matches that of _bt_binsrch_array_skey in every other way. + * + * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true + * array. The value 0 indicates that tupdatum/tupnull is within the range of + * the skip array. We return -1 when tupdatum/tupnull is lower that any value + * within the range of the array, and 1 when it is higher than every value. + * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance + * the array. + * + * cur_elem_trig indicates if array advancement was triggered by this array's + * scan key. We use this to optimize-away comparisons that are known by our + * caller to be unnecessary from context, just like _bt_binsrch_array_skey. + */ +static void +_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir, + Datum tupdatum, bool tupnull, + BTArrayKeyInfo *array, ScanKey cur, + int32 *set_elem_result) +{ + Assert(cur->sk_flags & SK_BT_SKIP); + Assert(cur->sk_flags & SK_SEARCHARRAY); + Assert(cur->sk_flags & SK_BT_REQFWD); + Assert(array->num_elems == -1); + Assert(!ScanDirectionIsNoMovement(dir)); + + if (array->null_elem) + { + Assert(!array->low_compare && !array->high_compare); + + *set_elem_result = 0; + return; + } + + if (tupnull) /* NULL tupdatum */ + { + if (cur->sk_flags & SK_BT_NULLS_FIRST) + *set_elem_result = -1; /* NULL "<" NOT_NULL */ + else + *set_elem_result = 1; /* NULL ">" NOT_NULL */ + return; + } + + /* + * Array inequalities determine whether tupdatum is within the range of + * caller's skip array + */ + *set_elem_result = 0; + if (ScanDirectionIsForward(dir)) + { + /* + * Evaluate low_compare first (unless cur_elem_trig tells us that it + * cannot possibly fail to be satisfied), then evaluate high_compare + */ + if (!cur_elem_trig && array->low_compare && + !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, + array->low_compare->sk_collation, + tupdatum, + array->low_compare->sk_argument))) + *set_elem_result = -1; + else if (array->high_compare && + !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, + array->high_compare->sk_collation, + tupdatum, + array->high_compare->sk_argument))) + *set_elem_result = 1; + } + else + { + /* + * Evaluate high_compare first (unless cur_elem_trig tells us that it + * cannot possibly fail to be satisfied), then evaluate low_compare + */ + if (!cur_elem_trig && array->high_compare && + !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, + array->high_compare->sk_collation, + tupdatum, + array->high_compare->sk_argument))) + *set_elem_result = 1; + else if (array->low_compare && + !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, + array->low_compare->sk_collation, + tupdatum, + array->low_compare->sk_argument))) + *set_elem_result = -1; + } + + /* + * Assert that any keys that were assumed to be satisfied already (due to + * caller passing cur_elem_trig=true) really are satisfied as expected + */ +#ifdef USE_ASSERT_CHECKING + if (cur_elem_trig) + { + if (ScanDirectionIsForward(dir) && array->low_compare) + Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, + array->low_compare->sk_collation, + tupdatum, + array->low_compare->sk_argument))); + + if (ScanDirectionIsBackward(dir) && array->high_compare) + Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, + array->high_compare->sk_collation, + tupdatum, + array->high_compare->sk_argument))); + } +#endif +} + +#ifdef USE_ASSERT_CHECKING +/* + * Verify that the scan's "so->keyData[]" scan keys are in agreement with + * its array key state + */ +static bool +_bt_verify_keys_with_arraykeys(IndexScanDesc scan) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + int last_sk_attno = InvalidAttrNumber, + arrayidx = 0; + bool nonrequiredseen = false; + + if (!so->qual_ok) + return false; + + for (int ikey = 0; ikey < so->numberOfKeys; ikey++) + { + ScanKey cur = so->keyData + ikey; + BTArrayKeyInfo *array; + + if (cur->sk_strategy != BTEqualStrategyNumber || + !(cur->sk_flags & SK_SEARCHARRAY)) + continue; + + array = &so->arrayKeys[arrayidx++]; + if (array->scan_key != ikey) + return false; + + if (array->num_elems == 0 || array->num_elems < -1) + return false; + + if (array->num_elems != -1 && + cur->sk_argument != array->elem_values[array->cur_elem]) + return false; + if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) + { + if (last_sk_attno > cur->sk_attno) + return false; + if (nonrequiredseen) + return false; + } + else + nonrequiredseen = true; + + last_sk_attno = cur->sk_attno; + } + + if (arrayidx != so->numArrayKeys) + return false; + + return true; +} +#endif diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c index 765659887af73..6d870e4ebe7fc 100644 --- a/src/backend/access/nbtree/nbtree.c +++ b/src/backend/access/nbtree/nbtree.c @@ -8,7 +8,7 @@ * This file contains only the public interface routines. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -30,11 +30,13 @@ #include "storage/indexfsm.h" #include "storage/ipc.h" #include "storage/lmgr.h" +#include "storage/lwlock.h" #include "storage/read_stream.h" #include "utils/datum.h" #include "utils/fmgrprotos.h" #include "utils/index_selfuncs.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* @@ -93,6 +95,7 @@ typedef struct BTParallelScanDescData typedef struct BTParallelScanDescData *BTParallelScanDesc; +static bool _bt_start_prim_scan(IndexScanDesc scan); static void _bt_parallel_serialize_arrays(Relation rel, BTParallelScanDesc btscan, BTScanOpaque so); static void _bt_parallel_restore_arrays(Relation rel, BTParallelScanDesc btscan, @@ -114,62 +117,63 @@ static BTVacuumPosting btreevacuumposting(BTVacState *vstate, Datum bthandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = BTMaxStrategyNumber; - amroutine->amsupport = BTNProcs; - amroutine->amoptsprocnum = BTOPTIONS_PROC; - amroutine->amcanorder = true; - amroutine->amcanorderbyop = false; - amroutine->amcanhash = false; - amroutine->amconsistentequality = true; - amroutine->amconsistentordering = true; - amroutine->amcanbackward = true; - amroutine->amcanunique = true; - amroutine->amcanmulticol = true; - amroutine->amoptionalkey = true; - amroutine->amsearcharray = true; - amroutine->amsearchnulls = true; - amroutine->amstorage = false; - amroutine->amclusterable = true; - amroutine->ampredlocks = true; - amroutine->amcanparallel = true; - amroutine->amcanbuildparallel = true; - amroutine->amcaninclude = true; - amroutine->amusemaintenanceworkmem = false; - amroutine->amsummarizing = false; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = btbuild; - amroutine->ambuildempty = btbuildempty; - amroutine->aminsert = btinsert; - amroutine->aminsertcleanup = NULL; - amroutine->ambulkdelete = btbulkdelete; - amroutine->amvacuumcleanup = btvacuumcleanup; - amroutine->amcanreturn = btcanreturn; - amroutine->amcostestimate = btcostestimate; - amroutine->amgettreeheight = btgettreeheight; - amroutine->amoptions = btoptions; - amroutine->amproperty = btproperty; - amroutine->ambuildphasename = btbuildphasename; - amroutine->amvalidate = btvalidate; - amroutine->amadjustmembers = btadjustmembers; - amroutine->ambeginscan = btbeginscan; - amroutine->amrescan = btrescan; - amroutine->amgettuple = btgettuple; - amroutine->amgetbitmap = btgetbitmap; - amroutine->amendscan = btendscan; - amroutine->ammarkpos = btmarkpos; - amroutine->amrestrpos = btrestrpos; - amroutine->amestimateparallelscan = btestimateparallelscan; - amroutine->aminitparallelscan = btinitparallelscan; - amroutine->amparallelrescan = btparallelrescan; - amroutine->amtranslatestrategy = bttranslatestrategy; - amroutine->amtranslatecmptype = bttranslatecmptype; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = BTMaxStrategyNumber, + .amsupport = BTNProcs, + .amoptsprocnum = BTOPTIONS_PROC, + .amcanorder = true, + .amcanorderbyop = false, + .amcanhash = false, + .amconsistentequality = true, + .amconsistentordering = true, + .amcanbackward = true, + .amcanunique = true, + .amcanmulticol = true, + .amoptionalkey = true, + .amsearcharray = true, + .amsearchnulls = true, + .amstorage = false, + .amclusterable = true, + .ampredlocks = true, + .amcanparallel = true, + .amcanbuildparallel = true, + .amcaninclude = true, + .amusemaintenanceworkmem = false, + .amsummarizing = false, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP, + .amkeytype = InvalidOid, + + .ambuild = btbuild, + .ambuildempty = btbuildempty, + .aminsert = btinsert, + .aminsertcleanup = NULL, + .ambulkdelete = btbulkdelete, + .amvacuumcleanup = btvacuumcleanup, + .amcanreturn = btcanreturn, + .amcostestimate = btcostestimate, + .amgettreeheight = btgettreeheight, + .amoptions = btoptions, + .amproperty = btproperty, + .ambuildphasename = btbuildphasename, + .amvalidate = btvalidate, + .amadjustmembers = btadjustmembers, + .ambeginscan = btbeginscan, + .amrescan = btrescan, + .amgettuple = btgettuple, + .amgetbitmap = btgetbitmap, + .amendscan = btendscan, + .ammarkpos = btmarkpos, + .amrestrpos = btrestrpos, + .amestimateparallelscan = btestimateparallelscan, + .aminitparallelscan = btinitparallelscan, + .amparallelrescan = btparallelrescan, + .amtranslatestrategy = bttranslatestrategy, + .amtranslatecmptype = bttranslatecmptype, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -228,6 +232,8 @@ btgettuple(IndexScanDesc scan, ScanDirection dir) BTScanOpaque so = (BTScanOpaque) scan->opaque; bool res; + Assert(scan->heapRelation != NULL); + /* btree indexes are never lossy */ scan->xs_recheck = false; @@ -258,8 +264,7 @@ btgettuple(IndexScanDesc scan, ScanDirection dir) * just forget any excess entries. */ if (so->killedItems == NULL) - so->killedItems = (int *) - palloc(MaxTIDsPerBTreePage * sizeof(int)); + so->killedItems = palloc_array(int, MaxTIDsPerBTreePage); if (so->numKilled < MaxTIDsPerBTreePage) so->killedItems[so->numKilled++] = so->currPos.itemIndex; } @@ -274,7 +279,7 @@ btgettuple(IndexScanDesc scan, ScanDirection dir) if (res) break; /* ... otherwise see if we need another primitive index scan */ - } while (so->numArrayKeys && _bt_start_prim_scan(scan, dir)); + } while (so->numArrayKeys && _bt_start_prim_scan(scan)); return res; } @@ -289,6 +294,8 @@ btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) int64 ntids = 0; ItemPointer heapTid; + Assert(scan->heapRelation == NULL); + /* Each loop iteration performs another primitive index scan */ do { @@ -320,7 +327,7 @@ btgetbitmap(IndexScanDesc scan, TIDBitmap *tbm) } } /* Now see if we need another primitive index scan */ - } while (so->numArrayKeys && _bt_start_prim_scan(scan, ForwardScanDirection)); + } while (so->numArrayKeys && _bt_start_prim_scan(scan)); return ntids; } @@ -341,7 +348,7 @@ btbeginscan(Relation rel, int nkeys, int norderbys) scan = RelationGetIndexScan(rel, nkeys, norderbys); /* allocate private workspace */ - so = (BTScanOpaque) palloc(sizeof(BTScanOpaqueData)); + so = palloc_object(BTScanOpaqueData); BTScanPosInvalidate(so->currPos); BTScanPosInvalidate(so->markPos); if (scan->numberOfKeys > 0) @@ -393,6 +400,26 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys, BTScanPosInvalidate(so->currPos); } + /* + * We prefer to eagerly drop leaf page pins before btgettuple returns. + * This avoids making VACUUM wait to acquire a cleanup lock on the page. + * + * We cannot safely drop leaf page pins during index-only scans due to a + * race condition involving VACUUM setting pages all-visible in the VM. + * It's also unsafe for plain index scans that use a non-MVCC snapshot. + * + * Also opt out of dropping leaf page pins eagerly during bitmap scans. + * Pins cannot be held for more than an instant during bitmap scans either + * way, so we might as well avoid wasting cycles on acquiring page LSNs. + * + * See nbtree/README section on making concurrent TID recycling safe. + * + * Note: so->dropPin should never change across rescans. + */ + so->dropPin = (!scan->xs_want_itup && + IsMVCCLikeSnapshot(scan->xs_snapshot) && + scan->heapRelation != NULL); + so->markItemIndex = -1; so->needPrimScan = false; so->scanBehind = false; @@ -405,16 +432,6 @@ btrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys, * not already done in a previous rescan call. To save on palloc * overhead, both workspaces are allocated as one palloc block; only this * function and btendscan know that. - * - * NOTE: this data structure also makes it safe to return data from a - * "name" column, even though btree name_ops uses an underlying storage - * datatype of cstring. The risk there is that "name" is supposed to be - * padded to NAMEDATALEN, but the actual index tuple is probably shorter. - * However, since we only return data out of tuples sitting in the - * currTuples array, a fetch of NAMEDATALEN bytes can at worst pull some - * data out of the markTuples array --- running off the end of memory for - * a SIGSEGV is not possible. Yeah, this is ugly as sin, but it beats - * adding special-case treatment for name_ops elsewhere. */ if (scan->xs_want_itup && so->currTuples == NULL) { @@ -622,6 +639,75 @@ btestimateparallelscan(Relation rel, int nkeys, int norderbys) return estnbtreeshared; } +/* + * _bt_start_prim_scan() -- start scheduled primitive index scan? + * + * Returns true if _bt_checkkeys scheduled another primitive index scan, just + * as the last one ended. Otherwise returns false, indicating that the array + * keys are now fully exhausted. + * + * Only call here during scans with one or more equality type array scan keys, + * after _bt_first or _bt_next return false. + */ +static bool +_bt_start_prim_scan(IndexScanDesc scan) +{ + BTScanOpaque so = (BTScanOpaque) scan->opaque; + + Assert(so->numArrayKeys); + + so->scanBehind = so->oppositeDirCheck = false; /* reset */ + + /* + * Array keys are advanced within _bt_checkkeys when the scan reaches the + * leaf level (more precisely, they're advanced when the scan reaches the + * end of each distinct set of array elements). This process avoids + * repeat access to leaf pages (across multiple primitive index scans) by + * advancing the scan's array keys when it allows the primitive index scan + * to find nearby matching tuples (or when it eliminates ranges of array + * key space that can't possibly be satisfied by any index tuple). + * + * _bt_checkkeys sets a simple flag variable to schedule another primitive + * index scan. The flag tells us what to do. + * + * We cannot rely on _bt_first always reaching _bt_checkkeys. There are + * various cases where that won't happen. For example, if the index is + * completely empty, then _bt_first won't call _bt_readpage/_bt_checkkeys. + * We also don't expect a call to _bt_checkkeys during searches for a + * non-existent value that happens to be lower/higher than any existing + * value in the index. + * + * We don't require special handling for these cases -- we don't need to + * be explicitly instructed to _not_ perform another primitive index scan. + * It's up to code under the control of _bt_first to always set the flag + * when another primitive index scan will be required. + * + * This works correctly, even with the tricky cases listed above, which + * all involve access to leaf pages "near the boundaries of the key space" + * (whether it's from a leftmost/rightmost page, or an imaginary empty + * leaf root page). If _bt_checkkeys cannot be reached by a primitive + * index scan for one set of array keys, then it also won't be reached for + * any later set ("later" in terms of the direction that we scan the index + * and advance the arrays). The array keys won't have advanced in these + * cases, but that's the correct behavior (even _bt_advance_array_keys + * won't always advance the arrays at the point they become "exhausted"). + */ + if (so->needPrimScan) + { + /* + * Flag was set -- must call _bt_first again, which will reset the + * scan's needPrimScan flag + */ + return true; + } + + /* The top-level index scan ran out of tuples in this scan direction */ + if (scan->parallel_scan != NULL) + _bt_parallel_done(scan); + + return false; +} + /* * _bt_parallel_serialize_arrays() -- Serialize parallel array state. * @@ -1038,7 +1124,7 @@ btbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, /* allocate stats if first time through, else re-use existing struct */ if (stats == NULL) - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); /* Establish the vacuum cycle ID to use for this scan */ /* The ENSURE stuff ensures we clean up shared memory on failure */ @@ -1099,7 +1185,7 @@ btvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) * We handle the problem by making num_index_tuples an estimate in * cleanup-only case. */ - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); btvacuumscan(info, stats, NULL, NULL, 0); stats->estimated_count = true; } diff --git a/src/backend/access/nbtree/nbtsearch.c b/src/backend/access/nbtree/nbtsearch.c index fe9a3886913d8..aae6acb7f57dd 100644 --- a/src/backend/access/nbtree/nbtsearch.c +++ b/src/backend/access/nbtree/nbtsearch.c @@ -4,7 +4,7 @@ * Search code for postgres btrees. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -18,6 +18,7 @@ #include "access/nbtree.h" #include "access/relscan.h" #include "access/xact.h" +#include "executor/instrument_node.h" #include "miscadmin.h" #include "pgstat.h" #include "storage/predicate.h" @@ -25,23 +26,13 @@ #include "utils/rel.h" -static void _bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp); +static inline void _bt_drop_lock_and_maybe_pin(Relation rel, BTScanOpaque so); static Buffer _bt_moveright(Relation rel, Relation heaprel, BTScanInsert key, Buffer buf, bool forupdate, BTStack stack, int access); static OffsetNumber _bt_binsrch(Relation rel, BTScanInsert key, Buffer buf); static int _bt_binsrch_posting(BTScanInsert key, Page page, OffsetNumber offnum); -static bool _bt_readpage(IndexScanDesc scan, ScanDirection dir, - OffsetNumber offnum, bool firstpage); -static void _bt_saveitem(BTScanOpaque so, int itemIndex, - OffsetNumber offnum, IndexTuple itup); -static int _bt_setuppostingitems(BTScanOpaque so, int itemIndex, - OffsetNumber offnum, ItemPointer heapTid, - IndexTuple itup); -static inline void _bt_savepostingitem(BTScanOpaque so, int itemIndex, - OffsetNumber offnum, - ItemPointer heapTid, int tupleOffset); static inline void _bt_returnitem(IndexScanDesc scan, BTScanOpaque so); static bool _bt_steppage(IndexScanDesc scan, ScanDirection dir); static bool _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, @@ -57,24 +48,28 @@ static bool _bt_endpoint(IndexScanDesc scan, ScanDirection dir); /* * _bt_drop_lock_and_maybe_pin() * - * Unlock the buffer; and if it is safe to release the pin, do that, too. - * This will prevent vacuum from stalling in a blocked state trying to read a - * page when a cursor is sitting on it. - * - * See nbtree/README section on making concurrent TID recycling safe. + * Unlock so->currPos.buf. If scan is so->dropPin, drop the pin, too. + * Dropping the pin prevents VACUUM from blocking on acquiring a cleanup lock. */ -static void -_bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp) +static inline void +_bt_drop_lock_and_maybe_pin(Relation rel, BTScanOpaque so) { - _bt_unlockbuf(scan->indexRelation, sp->buf); - - if (IsMVCCSnapshot(scan->xs_snapshot) && - RelationNeedsWAL(scan->indexRelation) && - !scan->xs_want_itup) + if (!so->dropPin) { - ReleaseBuffer(sp->buf); - sp->buf = InvalidBuffer; + /* Just drop the lock (not the pin) */ + _bt_unlockbuf(rel, so->currPos.buf); + return; } + + /* + * Drop both the lock and the pin. + * + * Have to set so->currPos.lsn so that _bt_killitems has a way to detect + * when concurrent heap TID recycling by VACUUM might have taken place. + */ + so->currPos.lsn = BufferGetLSNAtomic(so->currPos.buf); + _bt_relbuf(rel, so->currPos.buf); + so->currPos.buf = InvalidBuffer; } /* @@ -84,10 +79,13 @@ _bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp) * The passed scankey is an insertion-type scankey (see nbtree/README), * but it can omit the rightmost column(s) of the index. * - * Return value is a stack of parent-page pointers (i.e. there is no entry for - * the leaf level/page). *bufP is set to the address of the leaf-page buffer, - * which is locked and pinned. No locks are held on the parent pages, - * however! + * If returnstack is true, return value is a stack of parent-page pointers + * (i.e. there is no entry for the leaf level/page). If returnstack is false, + * we just return NULL. This scheme allows callers that don't need a descent + * stack to avoid palloc churn. + * + * When we return, *bufP is set to the address of the leaf-page buffer, which + * is locked and pinned. No locks are held on the parent pages, however! * * The returned buffer is locked according to access parameter. Additionally, * access = BT_WRITE will allow an empty root page to be created and returned. @@ -100,7 +98,7 @@ _bt_drop_lock_and_maybe_pin(IndexScanDesc scan, BTScanPos sp) */ BTStack _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP, - int access) + int access, bool returnstack) { BTStack stack_in = NULL; int page_access = BT_READ; @@ -164,10 +162,14 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP, * page one level down, it usually ends up inserting a new pivot * tuple/downlink immediately after the location recorded here. */ - new_stack = (BTStack) palloc(sizeof(BTStackData)); - new_stack->bts_blkno = BufferGetBlockNumber(*bufP); - new_stack->bts_offset = offnum; - new_stack->bts_parent = stack_in; + if (returnstack) + { + new_stack = (BTStack) palloc_object(BTStackData); + new_stack->bts_blkno = BufferGetBlockNumber(*bufP); + new_stack->bts_offset = offnum; + new_stack->bts_parent = stack_in; + stack_in = new_stack; + } /* * Page level 1 is lowest non-leaf page level prior to leaves. So, if @@ -181,7 +183,6 @@ _bt_search(Relation rel, Relation heaprel, BTScanInsert key, Buffer *bufP, *bufP = _bt_relandgetbuf(rel, *bufP, child, page_access); /* okay, all set to move down a level */ - stack_in = new_stack; } /* @@ -866,8 +867,8 @@ _bt_compare(Relation rel, * if backwards scan, the last item) in the tree that satisfies the * qualifications in the scan key. On success exit, data about the * matching tuple(s) on the page has been loaded into so->currPos. We'll - * drop all locks and hold onto a pin on page's buffer, except when - * _bt_drop_lock_and_maybe_pin dropped the pin to avoid blocking VACUUM. + * drop all locks and hold onto a pin on page's buffer, except during + * so->dropPin scans, when we drop both the lock and the pin. * _bt_returnitem sets the next item to return to scan on success exit. * * If there are no matching items in the index, we return false, with no @@ -883,13 +884,12 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) { Relation rel = scan->indexRelation; BTScanOpaque so = (BTScanOpaque) scan->opaque; - BTStack stack; OffsetNumber offnum; BTScanInsertData inskey; ScanKey startKeys[INDEX_MAX_KEYS]; - ScanKeyData notnullkeys[INDEX_MAX_KEYS]; + ScanKeyData notnullkey; int keysz = 0; - StrategyNumber strat_total; + StrategyNumber strat_total = InvalidStrategy; BlockNumber blkno = InvalidBlockNumber, lastcurrblkno; @@ -955,46 +955,51 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) /*---------- * Examine the scan keys to discover where we need to start the scan. + * The selected scan keys (at most one per index column) are remembered by + * storing their addresses into the local startKeys[] array. The final + * startKeys[] entry's strategy is set in strat_total. (Actually, there + * are a couple of cases where we force a less/more restrictive strategy.) * - * We want to identify the keys that can be used as starting boundaries; - * these are =, >, or >= keys for a forward scan or =, <, <= keys for - * a backwards scan. We can use keys for multiple attributes so long as - * the prior attributes had only =, >= (resp. =, <=) keys. Once we accept - * a > or < boundary or find an attribute with no boundary (which can be - * thought of as the same as "> -infinity"), we can't use keys for any - * attributes to its right, because it would break our simplistic notion - * of what initial positioning strategy to use. + * We must use the key that was marked required (in the direction opposite + * our own scan's) during preprocessing. Each index attribute can only + * have one such required key. In general, the keys that we use to find + * an initial position when scanning forwards are the same keys that end + * the scan on the leaf level when scanning backwards (and vice-versa). * * When the scan keys include cross-type operators, _bt_preprocess_keys - * may not be able to eliminate redundant keys; in such cases we will - * arbitrarily pick a usable one for each attribute. This is correct - * but possibly not optimal behavior. (For example, with keys like - * "x >= 4 AND x >= 5" we would elect to scan starting at x=4 when - * x=5 would be more efficient.) Since the situation only arises given - * a poorly-worded query plus an incomplete opfamily, live with it. + * may not be able to eliminate redundant keys; in such cases it will + * arbitrarily pick a usable key for each attribute (and scan direction), + * ensuring that there is no more than one key required in each direction. + * We stop considering further keys once we reach the first nonrequired + * key (which must come after all required keys), so this can't affect us. * - * When both equality and inequality keys appear for a single attribute - * (again, only possible when cross-type operators appear), we *must* - * select one of the equality keys for the starting point, because - * _bt_checkkeys() will stop the scan as soon as an equality qual fails. - * For example, if we have keys like "x >= 4 AND x = 10" and we elect to - * start at x=4, we will fail and stop before reaching x=10. If multiple - * equality quals survive preprocessing, however, it doesn't matter which - * one we use --- by definition, they are either redundant or - * contradictory. + * The required keys that we use as starting boundaries have to be =, >, + * or >= keys for a forward scan or =, <, <= keys for a backwards scan. + * We can use keys for multiple attributes so long as the prior attributes + * had only =, >= (resp. =, <=) keys. These rules are very similar to the + * rules that preprocessing used to determine which keys to mark required. + * We cannot always use every required key as a positioning key, though. + * Skip arrays necessitate independently applying our own rules here. + * Skip arrays are always generally considered = array keys, but we'll + * nevertheless treat them as inequalities at certain points of the scan. + * When that happens, it _might_ have implications for the number of + * required keys that we can safely use for initial positioning purposes. * - * In practice we rarely see any "attribute boundary key gaps" here. - * Preprocessing can usually backfill skip array keys for any attributes - * that were omitted from the original scan->keyData[] input keys. All - * array keys are always considered = keys, but we'll sometimes need to - * treat the current key value as if we were using an inequality strategy. - * This happens with range skip arrays, which store inequality keys in the - * array's low_compare/high_compare fields (used to find the first/last - * set of matches, when = key will lack a usable sk_argument value). - * These are always preferred over any redundant "standard" inequality - * keys on the same column (per the usual rule about preferring = keys). - * Note also that any column with an = skip array key can never have an - * additional, contradictory = key. + * For example, a forward scan with a skip array on its leading attribute + * (with no low_compare/high_compare) will have at least two required scan + * keys, but we won't use any of them as boundary keys during the scan's + * initial call here. Our positioning key during the first call here can + * be thought of as representing "> -infinity". Similarly, if such a skip + * array's low_compare is "a > 'foo'", then we position using "a > 'foo'" + * during the scan's initial call here; a lower-order key such as "b = 42" + * can't be used until the "a" array advances beyond MINVAL/low_compare. + * + * On the other hand, if such a skip array's low_compare was "a >= 'foo'", + * then we _can_ use "a >= 'foo' AND b = 42" during the initial call here. + * A subsequent call here might have us use "a = 'fop' AND b = 42". Note + * that we treat = and >= as equivalent when scanning forwards (just as we + * treat = and <= as equivalent when scanning backwards). We effectively + * do the same thing (though with a distinct "a" element/value) each time. * * All keys (with the exception of SK_SEARCHNULL keys and SK_BT_SKIP * array keys whose array is "null_elem=true") imply a NOT NULL qualifier. @@ -1006,41 +1011,38 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) * traversing a lot of null entries at the start of the scan. * * In this loop, row-comparison keys are treated the same as keys on their - * first (leftmost) columns. We'll add on lower-order columns of the row - * comparison below, if possible. + * first (leftmost) columns. We'll add all lower-order columns of the row + * comparison that were marked required during preprocessing below. * - * The selected scan keys (at most one per index column) are remembered by - * storing their addresses into the local startKeys[] array. - * - * _bt_checkkeys/_bt_advance_array_keys decide whether and when to start - * the next primitive index scan (for scans with array keys) based in part - * on an understanding of how it'll enable us to reposition the scan. - * They're directly aware of how we'll sometimes cons up an explicit - * SK_SEARCHNOTNULL key. They'll even end primitive scans by applying a - * symmetric "deduce NOT NULL" rule of their own. This allows top-level - * scans to skip large groups of NULLs through repeated deductions about - * key strictness (for a required inequality key) and whether NULLs in the - * key's index column are stored last or first (relative to non-NULLs). + * _bt_advance_array_keys needs to know exactly how we'll reposition the + * scan (should it opt to schedule another primitive index scan). It is + * critical that primscans only be scheduled when they'll definitely make + * some useful progress. _bt_advance_array_keys does this by calling + * _bt_checkkeys routines that report whether a tuple is past the end of + * matches for the scan's keys (given the scan's current array elements). + * If the page's final tuple is "after the end of matches" for a scan that + * uses the *opposite* scan direction, then it must follow that it's also + * "before the start of matches" for the actual current scan direction. + * It is therefore essential that all of our initial positioning rules are + * symmetric with _bt_checkkeys's corresponding continuescan=false rule. * If you update anything here, _bt_checkkeys/_bt_advance_array_keys might * need to be kept in sync. *---------- */ - strat_total = BTEqualStrategyNumber; if (so->numberOfKeys > 0) { AttrNumber curattr; - ScanKey chosen; + ScanKey bkey; ScanKey impliesNN; ScanKey cur; /* - * chosen is the so-far-chosen key for the current attribute, if any. - * We don't cast the decision in stone until we reach keys for the - * next attribute. + * bkey will be set to the key that preprocessing left behind as the + * boundary key for this attribute, in this scan direction (if any) */ cur = so->keyData; curattr = 1; - chosen = NULL; + bkey = NULL; /* Also remember any scankey that implies a NOT NULL constraint */ impliesNN = NULL; @@ -1053,23 +1055,29 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) { if (i >= so->numberOfKeys || cur->sk_attno != curattr) { + /* Done looking for the curattr boundary key */ + Assert(bkey == NULL || + (bkey->sk_attno == curattr && + (bkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))); + Assert(impliesNN == NULL || + (impliesNN->sk_attno == curattr && + (impliesNN->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)))); + /* - * Done looking at keys for curattr. - * * If this is a scan key for a skip array whose current * element is MINVAL, choose low_compare (when scanning * backwards it'll be MAXVAL, and we'll choose high_compare). * - * Note: if the array's low_compare key makes 'chosen' NULL, + * Note: if the array's low_compare key makes 'bkey' NULL, * then we behave as if the array's first element is -inf, * except when !array->null_elem implies a usable NOT NULL * constraint. */ - if (chosen != NULL && - (chosen->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))) + if (bkey != NULL && + (bkey->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))) { - int ikey = chosen - so->keyData; - ScanKey skipequalitykey = chosen; + int ikey = bkey - so->keyData; + ScanKey skipequalitykey = bkey; BTArrayKeyInfo *array = NULL; for (int arridx = 0; arridx < so->numArrayKeys; arridx++) @@ -1082,42 +1090,41 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) if (ScanDirectionIsForward(dir)) { Assert(!(skipequalitykey->sk_flags & SK_BT_MAXVAL)); - chosen = array->low_compare; + bkey = array->low_compare; } else { Assert(!(skipequalitykey->sk_flags & SK_BT_MINVAL)); - chosen = array->high_compare; + bkey = array->high_compare; } - Assert(chosen == NULL || - chosen->sk_attno == skipequalitykey->sk_attno); + Assert(bkey == NULL || + bkey->sk_attno == skipequalitykey->sk_attno); if (!array->null_elem) impliesNN = skipequalitykey; else - Assert(chosen == NULL && impliesNN == NULL); + Assert(bkey == NULL && impliesNN == NULL); } /* * If we didn't find a usable boundary key, see if we can * deduce a NOT NULL key */ - if (chosen == NULL && impliesNN != NULL && + if (bkey == NULL && impliesNN != NULL && ((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ? ScanDirectionIsForward(dir) : ScanDirectionIsBackward(dir))) { - /* Yes, so build the key in notnullkeys[keysz] */ - chosen = ¬nullkeys[keysz]; - ScanKeyEntryInitialize(chosen, + /* Final startKeys[] entry will be deduced NOT NULL key */ + bkey = ¬nullkey; + ScanKeyEntryInitialize(bkey, (SK_SEARCHNOTNULL | SK_ISNULL | (impliesNN->sk_flags & (SK_BT_DESC | SK_BT_NULLS_FIRST))), curattr, - ((impliesNN->sk_flags & SK_BT_NULLS_FIRST) ? - BTGreaterStrategyNumber : - BTLessStrategyNumber), + ScanDirectionIsForward(dir) ? + BTGreaterStrategyNumber : BTLessStrategyNumber, InvalidOid, InvalidOid, InvalidOid, @@ -1125,12 +1132,12 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) } /* - * If we still didn't find a usable boundary key, quit; else - * save the boundary key pointer in startKeys. + * If preprocessing didn't leave a usable boundary key, quit; + * else save the boundary key pointer in startKeys[] */ - if (chosen == NULL) + if (bkey == NULL) break; - startKeys[keysz++] = chosen; + startKeys[keysz++] = bkey; /* * We can only consider adding more boundary keys when the one @@ -1138,7 +1145,7 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) * (during backwards scans we can only do so when the key that * we just added to startKeys[] uses the = or <= strategy) */ - strat_total = chosen->sk_strategy; + strat_total = bkey->sk_strategy; if (strat_total == BTGreaterStrategyNumber || strat_total == BTLessStrategyNumber) break; @@ -1149,19 +1156,19 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) * make strat_total > or < (and stop adding boundary keys). * This can only happen with opclasses that lack skip support. */ - if (chosen->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)) + if (bkey->sk_flags & (SK_BT_NEXT | SK_BT_PRIOR)) { - Assert(chosen->sk_flags & SK_BT_SKIP); + Assert(bkey->sk_flags & SK_BT_SKIP); Assert(strat_total == BTEqualStrategyNumber); if (ScanDirectionIsForward(dir)) { - Assert(!(chosen->sk_flags & SK_BT_PRIOR)); + Assert(!(bkey->sk_flags & SK_BT_PRIOR)); strat_total = BTGreaterStrategyNumber; } else { - Assert(!(chosen->sk_flags & SK_BT_NEXT)); + Assert(!(bkey->sk_flags & SK_BT_NEXT)); strat_total = BTLessStrategyNumber; } @@ -1175,24 +1182,30 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) /* * Done if that was the last scan key output by preprocessing. - * Also done if there is a gap index attribute that lacks a - * usable key (only possible when preprocessing was unable to - * generate a skip array key to "fill in the gap"). + * Also done if we've now examined all keys marked required. */ if (i >= so->numberOfKeys || - cur->sk_attno != curattr + 1) + !(cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))) break; /* * Reset for next attr. */ + Assert(cur->sk_attno == curattr + 1); curattr = cur->sk_attno; - chosen = NULL; + bkey = NULL; impliesNN = NULL; } /* - * Can we use this key as a starting boundary for this attr? + * If we've located the starting boundary key for curattr, we have + * no interest in curattr's other required key + */ + if (bkey != NULL) + continue; + + /* + * Is this key the starting boundary key for curattr? * * If not, does it imply a NOT NULL constraint? (Because * SK_SEARCHNULL keys are always assigned BTEqualStrategyNumber, @@ -1202,27 +1215,20 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) { case BTLessStrategyNumber: case BTLessEqualStrategyNumber: - if (chosen == NULL) - { - if (ScanDirectionIsBackward(dir)) - chosen = cur; - else - impliesNN = cur; - } + if (ScanDirectionIsBackward(dir)) + bkey = cur; + else if (impliesNN == NULL) + impliesNN = cur; break; case BTEqualStrategyNumber: - /* override any non-equality choice */ - chosen = cur; + bkey = cur; break; case BTGreaterEqualStrategyNumber: case BTGreaterStrategyNumber: - if (chosen == NULL) - { - if (ScanDirectionIsForward(dir)) - chosen = cur; - else - impliesNN = cur; - } + if (ScanDirectionIsForward(dir)) + bkey = cur; + else if (impliesNN == NULL) + impliesNN = cur; break; } } @@ -1248,16 +1254,18 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) Assert(keysz <= INDEX_MAX_KEYS); for (int i = 0; i < keysz; i++) { - ScanKey cur = startKeys[i]; + ScanKey bkey = startKeys[i]; - Assert(cur->sk_attno == i + 1); + Assert(bkey->sk_attno == i + 1); - if (cur->sk_flags & SK_ROW_HEADER) + if (bkey->sk_flags & SK_ROW_HEADER) { /* * Row comparison header: look to the first row member instead */ - ScanKey subkey = (ScanKey) DatumGetPointer(cur->sk_argument); + ScanKey subkey = (ScanKey) DatumGetPointer(bkey->sk_argument); + bool loosen_strat = false, + tighten_strat = false; /* * Cannot be a NULL in the first row member: _bt_preprocess_keys @@ -1265,9 +1273,20 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) * ever getting this far */ Assert(subkey->sk_flags & SK_ROW_MEMBER); - Assert(subkey->sk_attno == cur->sk_attno); + Assert(subkey->sk_attno == bkey->sk_attno); Assert(!(subkey->sk_flags & SK_ISNULL)); + /* + * This is either a > or >= key (during backwards scans it is + * either < or <=) that was marked required during preprocessing. + * Later so->keyData[] keys can't have been marked required, so + * our row compare header key must be the final startKeys[] entry. + */ + Assert(subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)); + Assert(subkey->sk_strategy == bkey->sk_strategy); + Assert(subkey->sk_strategy == strat_total); + Assert(i == keysz - 1); + /* * The member scankeys are already in insertion format (ie, they * have sk_func = 3-way-comparison function) @@ -1275,112 +1294,141 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) memcpy(inskey.scankeys + i, subkey, sizeof(ScanKeyData)); /* - * If the row comparison is the last positioning key we accepted, - * try to add additional keys from the lower-order row members. - * (If we accepted independent conditions on additional index - * columns, we use those instead --- doesn't seem worth trying to - * determine which is more restrictive.) Note that this is OK - * even if the row comparison is of ">" or "<" type, because the - * condition applied to all but the last row member is effectively - * ">=" or "<=", and so the extra keys don't break the positioning - * scheme. But, by the same token, if we aren't able to use all - * the row members, then the part of the row comparison that we - * did use has to be treated as just a ">=" or "<=" condition, and - * so we'd better adjust strat_total accordingly. + * Now look to later row compare members. + * + * If there's an "index attribute gap" between two row compare + * members, the second member won't have been marked required, and + * so can't be used as a starting boundary key here. The part of + * the row comparison that we do still use has to be treated as a + * ">=" or "<=" condition. For example, a qual "(a, c) > (1, 42)" + * with an omitted intervening index attribute "b" will use an + * insertion scan key "a >= 1". Even the first "a = 1" tuple on + * the leaf level might satisfy the row compare qual. + * + * We're able to use a _more_ restrictive strategy when we reach a + * NULL row compare member, since they're always unsatisfiable. + * For example, a qual "(a, b, c) >= (1, NULL, 77)" will use an + * insertion scan key "a > 1". All tuples where "a = 1" cannot + * possibly satisfy the row compare qual, so this is safe. */ - if (i == keysz - 1) + Assert(!(subkey->sk_flags & SK_ROW_END)); + for (;;) { - bool used_all_subkeys = false; + subkey++; + Assert(subkey->sk_flags & SK_ROW_MEMBER); - Assert(!(subkey->sk_flags & SK_ROW_END)); - for (;;) + if (subkey->sk_flags & SK_ISNULL) { - subkey++; - Assert(subkey->sk_flags & SK_ROW_MEMBER); - if (subkey->sk_attno != keysz + 1) - break; /* out-of-sequence, can't use it */ - if (subkey->sk_strategy != cur->sk_strategy) - break; /* wrong direction, can't use it */ - if (subkey->sk_flags & SK_ISNULL) - break; /* can't use null keys */ - Assert(keysz < INDEX_MAX_KEYS); - memcpy(inskey.scankeys + keysz, subkey, - sizeof(ScanKeyData)); - keysz++; - if (subkey->sk_flags & SK_ROW_END) - { - used_all_subkeys = true; - break; - } + /* + * NULL member key, can only use earlier keys. + * + * We deliberately avoid checking if this key is marked + * required. All earlier keys are required, and this key + * is unsatisfiable either way, so we can't miss anything. + */ + tighten_strat = true; + break; } - if (!used_all_subkeys) + + if (!(subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))) { - switch (strat_total) - { - case BTLessStrategyNumber: - strat_total = BTLessEqualStrategyNumber; - break; - case BTGreaterStrategyNumber: - strat_total = BTGreaterEqualStrategyNumber; - break; - } + /* nonrequired member key, can only use earlier keys */ + loosen_strat = true; + break; } - break; /* done with outer loop */ + + Assert(subkey->sk_attno == keysz + 1); + Assert(subkey->sk_strategy == bkey->sk_strategy); + Assert(keysz < INDEX_MAX_KEYS); + + memcpy(inskey.scankeys + keysz, subkey, sizeof(ScanKeyData)); + keysz++; + + if (subkey->sk_flags & SK_ROW_END) + break; } - } - else - { - /* - * Ordinary comparison key. Transform the search-style scan key - * to an insertion scan key by replacing the sk_func with the - * appropriate btree comparison function. - * - * If scankey operator is not a cross-type comparison, we can use - * the cached comparison function; otherwise gotta look it up in - * the catalogs. (That can't lead to infinite recursion, since no - * indexscan initiated by syscache lookup will use cross-data-type - * operators.) - * - * We support the convention that sk_subtype == InvalidOid means - * the opclass input type; this is a hack to simplify life for - * ScanKeyInit(). - */ - if (cur->sk_subtype == rel->rd_opcintype[i] || - cur->sk_subtype == InvalidOid) + Assert(!(loosen_strat && tighten_strat)); + if (loosen_strat) { - FmgrInfo *procinfo; - - procinfo = index_getprocinfo(rel, cur->sk_attno, BTORDER_PROC); - ScanKeyEntryInitializeWithInfo(inskey.scankeys + i, - cur->sk_flags, - cur->sk_attno, - InvalidStrategy, - cur->sk_subtype, - cur->sk_collation, - procinfo, - cur->sk_argument); + /* Use less restrictive strategy (and fewer member keys) */ + switch (strat_total) + { + case BTLessStrategyNumber: + strat_total = BTLessEqualStrategyNumber; + break; + case BTGreaterStrategyNumber: + strat_total = BTGreaterEqualStrategyNumber; + break; + } } - else + if (tighten_strat) { - RegProcedure cmp_proc; - - cmp_proc = get_opfamily_proc(rel->rd_opfamily[i], - rel->rd_opcintype[i], - cur->sk_subtype, - BTORDER_PROC); - if (!RegProcedureIsValid(cmp_proc)) - elog(ERROR, "missing support function %d(%u,%u) for attribute %d of index \"%s\"", - BTORDER_PROC, rel->rd_opcintype[i], cur->sk_subtype, - cur->sk_attno, RelationGetRelationName(rel)); - ScanKeyEntryInitialize(inskey.scankeys + i, - cur->sk_flags, - cur->sk_attno, - InvalidStrategy, - cur->sk_subtype, - cur->sk_collation, - cmp_proc, - cur->sk_argument); + /* Use more restrictive strategy (and fewer member keys) */ + switch (strat_total) + { + case BTLessEqualStrategyNumber: + strat_total = BTLessStrategyNumber; + break; + case BTGreaterEqualStrategyNumber: + strat_total = BTGreaterStrategyNumber; + break; + } } + + /* Done (row compare header key is always last startKeys[] key) */ + break; + } + + /* + * Ordinary comparison key/search-style key. + * + * Transform the search-style scan key to an insertion scan key by + * replacing the sk_func with the appropriate btree 3-way-comparison + * function. + * + * If scankey operator is not a cross-type comparison, we can use the + * cached comparison function; otherwise gotta look it up in the + * catalogs. (That can't lead to infinite recursion, since no + * indexscan initiated by syscache lookup will use cross-data-type + * operators.) + * + * We support the convention that sk_subtype == InvalidOid means the + * opclass input type; this hack simplifies life for ScanKeyInit(). + */ + if (bkey->sk_subtype == rel->rd_opcintype[i] || + bkey->sk_subtype == InvalidOid) + { + FmgrInfo *procinfo; + + procinfo = index_getprocinfo(rel, bkey->sk_attno, BTORDER_PROC); + ScanKeyEntryInitializeWithInfo(inskey.scankeys + i, + bkey->sk_flags, + bkey->sk_attno, + InvalidStrategy, + bkey->sk_subtype, + bkey->sk_collation, + procinfo, + bkey->sk_argument); + } + else + { + RegProcedure cmp_proc; + + cmp_proc = get_opfamily_proc(rel->rd_opfamily[i], + rel->rd_opcintype[i], + bkey->sk_subtype, BTORDER_PROC); + if (!RegProcedureIsValid(cmp_proc)) + elog(ERROR, "missing support function %d(%u,%u) for attribute %d of index \"%s\"", + BTORDER_PROC, rel->rd_opcintype[i], bkey->sk_subtype, + bkey->sk_attno, RelationGetRelationName(rel)); + ScanKeyEntryInitialize(inskey.scankeys + i, + bkey->sk_flags, + bkey->sk_attno, + InvalidStrategy, + bkey->sk_subtype, + bkey->sk_collation, + cmp_proc, + bkey->sk_argument); } } @@ -1462,13 +1510,12 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) * position ourselves on the target leaf page. */ Assert(ScanDirectionIsBackward(dir) == inskey.backward); - stack = _bt_search(rel, NULL, &inskey, &so->currPos.buf, BT_READ); - - /* don't need to keep the stack around... */ - _bt_freestack(stack); + _bt_search(rel, NULL, &inskey, &so->currPos.buf, BT_READ, false); if (!BufferIsValid(so->currPos.buf)) { + Assert(!so->needPrimScan); + /* * We only get here if the index is completely empty. Lock relation * because nothing finer to lock exists. Without a buffer lock, it's @@ -1481,13 +1528,11 @@ _bt_first(IndexScanDesc scan, ScanDirection dir) if (IsolationIsSerializable()) { PredicateLockRelation(rel, scan->xs_snapshot); - stack = _bt_search(rel, NULL, &inskey, &so->currPos.buf, BT_READ); - _bt_freestack(stack); + _bt_search(rel, NULL, &inskey, &so->currPos.buf, BT_READ, false); } if (!BufferIsValid(so->currPos.buf)) { - Assert(!so->needPrimScan); _bt_parallel_done(scan); return false; } @@ -1569,519 +1614,6 @@ _bt_next(IndexScanDesc scan, ScanDirection dir) return true; } -/* - * _bt_readpage() -- Load data from current index page into so->currPos - * - * Caller must have pinned and read-locked so->currPos.buf; the buffer's state - * is not changed here. Also, currPos.moreLeft and moreRight must be valid; - * they are updated as appropriate. All other fields of so->currPos are - * initialized from scratch here. - * - * We scan the current page starting at offnum and moving in the indicated - * direction. All items matching the scan keys are loaded into currPos.items. - * moreLeft or moreRight (as appropriate) is cleared if _bt_checkkeys reports - * that there can be no more matching tuples in the current scan direction - * (could just be for the current primitive index scan when scan has arrays). - * - * In the case of a parallel scan, caller must have called _bt_parallel_seize - * prior to calling this function; this function will invoke - * _bt_parallel_release before returning. - * - * Returns true if any matching items found on the page, false if none. - */ -static bool -_bt_readpage(IndexScanDesc scan, ScanDirection dir, OffsetNumber offnum, - bool firstpage) -{ - Relation rel = scan->indexRelation; - BTScanOpaque so = (BTScanOpaque) scan->opaque; - Page page; - BTPageOpaque opaque; - OffsetNumber minoff; - OffsetNumber maxoff; - BTReadPageState pstate; - bool arrayKeys; - int itemIndex, - indnatts; - - /* save the page/buffer block number, along with its sibling links */ - page = BufferGetPage(so->currPos.buf); - opaque = BTPageGetOpaque(page); - so->currPos.currPage = BufferGetBlockNumber(so->currPos.buf); - so->currPos.prevPage = opaque->btpo_prev; - so->currPos.nextPage = opaque->btpo_next; - - Assert(!P_IGNORE(opaque)); - Assert(BTScanPosIsPinned(so->currPos)); - Assert(!so->needPrimScan); - - if (scan->parallel_scan) - { - /* allow next/prev page to be read by other worker without delay */ - if (ScanDirectionIsForward(dir)) - _bt_parallel_release(scan, so->currPos.nextPage, - so->currPos.currPage); - else - _bt_parallel_release(scan, so->currPos.prevPage, - so->currPos.currPage); - } - - /* initialize remaining currPos fields related to current page */ - so->currPos.lsn = BufferGetLSNAtomic(so->currPos.buf); - so->currPos.dir = dir; - so->currPos.nextTupleOffset = 0; - /* either moreLeft or moreRight should be set now (may be unset later) */ - Assert(ScanDirectionIsForward(dir) ? so->currPos.moreRight : - so->currPos.moreLeft); - - PredicateLockPage(rel, so->currPos.currPage, scan->xs_snapshot); - - /* initialize local variables */ - indnatts = IndexRelationGetNumberOfAttributes(rel); - arrayKeys = so->numArrayKeys != 0; - minoff = P_FIRSTDATAKEY(opaque); - maxoff = PageGetMaxOffsetNumber(page); - - /* initialize page-level state that we'll pass to _bt_checkkeys */ - pstate.minoff = minoff; - pstate.maxoff = maxoff; - pstate.finaltup = NULL; - pstate.page = page; - pstate.firstpage = firstpage; - pstate.forcenonrequired = false; - pstate.startikey = 0; - pstate.offnum = InvalidOffsetNumber; - pstate.skip = InvalidOffsetNumber; - pstate.continuescan = true; /* default assumption */ - pstate.rechecks = 0; - pstate.targetdistance = 0; - pstate.nskipadvances = 0; - - if (ScanDirectionIsForward(dir)) - { - /* SK_SEARCHARRAY forward scans must provide high key up front */ - if (arrayKeys) - { - if (!P_RIGHTMOST(opaque)) - { - ItemId iid = PageGetItemId(page, P_HIKEY); - - pstate.finaltup = (IndexTuple) PageGetItem(page, iid); - - if (so->scanBehind && - !_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup)) - { - /* Schedule another primitive index scan after all */ - so->currPos.moreRight = false; - so->needPrimScan = true; - if (scan->parallel_scan) - _bt_parallel_primscan_schedule(scan, - so->currPos.currPage); - return false; - } - } - - so->scanBehind = so->oppositeDirCheck = false; /* reset */ - } - - /* - * Consider pstate.startikey optimization once the ongoing primitive - * index scan has already read at least one page - */ - if (!pstate.firstpage && minoff < maxoff) - _bt_set_startikey(scan, &pstate); - - /* load items[] in ascending order */ - itemIndex = 0; - - offnum = Max(offnum, minoff); - - while (offnum <= maxoff) - { - ItemId iid = PageGetItemId(page, offnum); - IndexTuple itup; - bool passes_quals; - - /* - * If the scan specifies not to return killed tuples, then we - * treat a killed tuple as not passing the qual - */ - if (scan->ignore_killed_tuples && ItemIdIsDead(iid)) - { - offnum = OffsetNumberNext(offnum); - continue; - } - - itup = (IndexTuple) PageGetItem(page, iid); - Assert(!BTreeTupleIsPivot(itup)); - - pstate.offnum = offnum; - passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys, - itup, indnatts); - - /* - * Check if we need to skip ahead to a later tuple (only possible - * when the scan uses array keys) - */ - if (arrayKeys && OffsetNumberIsValid(pstate.skip)) - { - Assert(!passes_quals && pstate.continuescan); - Assert(offnum < pstate.skip); - Assert(!pstate.forcenonrequired); - - offnum = pstate.skip; - pstate.skip = InvalidOffsetNumber; - continue; - } - - if (passes_quals) - { - /* tuple passes all scan key conditions */ - if (!BTreeTupleIsPosting(itup)) - { - /* Remember it */ - _bt_saveitem(so, itemIndex, offnum, itup); - itemIndex++; - } - else - { - int tupleOffset; - - /* - * Set up state to return posting list, and remember first - * TID - */ - tupleOffset = - _bt_setuppostingitems(so, itemIndex, offnum, - BTreeTupleGetPostingN(itup, 0), - itup); - itemIndex++; - /* Remember additional TIDs */ - for (int i = 1; i < BTreeTupleGetNPosting(itup); i++) - { - _bt_savepostingitem(so, itemIndex, offnum, - BTreeTupleGetPostingN(itup, i), - tupleOffset); - itemIndex++; - } - } - } - /* When !continuescan, there can't be any more matches, so stop */ - if (!pstate.continuescan) - break; - - offnum = OffsetNumberNext(offnum); - } - - /* - * We don't need to visit page to the right when the high key - * indicates that no more matches will be found there. - * - * Checking the high key like this works out more often than you might - * think. Leaf page splits pick a split point between the two most - * dissimilar tuples (this is weighed against the need to evenly share - * free space). Leaf pages with high key attribute values that can - * only appear on non-pivot tuples on the right sibling page are - * common. - */ - if (pstate.continuescan && !so->scanBehind && !P_RIGHTMOST(opaque)) - { - ItemId iid = PageGetItemId(page, P_HIKEY); - IndexTuple itup = (IndexTuple) PageGetItem(page, iid); - int truncatt; - - /* Reset arrays, per _bt_set_startikey contract */ - if (pstate.forcenonrequired) - _bt_start_array_keys(scan, dir); - pstate.forcenonrequired = false; - pstate.startikey = 0; /* _bt_set_startikey ignores P_HIKEY */ - - truncatt = BTreeTupleGetNAtts(itup, rel); - _bt_checkkeys(scan, &pstate, arrayKeys, itup, truncatt); - } - - if (!pstate.continuescan) - so->currPos.moreRight = false; - - Assert(itemIndex <= MaxTIDsPerBTreePage); - so->currPos.firstItem = 0; - so->currPos.lastItem = itemIndex - 1; - so->currPos.itemIndex = 0; - } - else - { - /* SK_SEARCHARRAY backward scans must provide final tuple up front */ - if (arrayKeys) - { - if (minoff <= maxoff && !P_LEFTMOST(opaque)) - { - ItemId iid = PageGetItemId(page, minoff); - - pstate.finaltup = (IndexTuple) PageGetItem(page, iid); - - if (so->scanBehind && - !_bt_scanbehind_checkkeys(scan, dir, pstate.finaltup)) - { - /* Schedule another primitive index scan after all */ - so->currPos.moreLeft = false; - so->needPrimScan = true; - if (scan->parallel_scan) - _bt_parallel_primscan_schedule(scan, - so->currPos.currPage); - return false; - } - } - - so->scanBehind = so->oppositeDirCheck = false; /* reset */ - } - - /* - * Consider pstate.startikey optimization once the ongoing primitive - * index scan has already read at least one page - */ - if (!pstate.firstpage && minoff < maxoff) - _bt_set_startikey(scan, &pstate); - - /* load items[] in descending order */ - itemIndex = MaxTIDsPerBTreePage; - - offnum = Min(offnum, maxoff); - - while (offnum >= minoff) - { - ItemId iid = PageGetItemId(page, offnum); - IndexTuple itup; - bool tuple_alive; - bool passes_quals; - - /* - * If the scan specifies not to return killed tuples, then we - * treat a killed tuple as not passing the qual. Most of the - * time, it's a win to not bother examining the tuple's index - * keys, but just skip to the next tuple (previous, actually, - * since we're scanning backwards). However, if this is the first - * tuple on the page, we do check the index keys, to prevent - * uselessly advancing to the page to the left. This is similar - * to the high key optimization used by forward scans. - */ - if (scan->ignore_killed_tuples && ItemIdIsDead(iid)) - { - if (offnum > minoff) - { - offnum = OffsetNumberPrev(offnum); - continue; - } - - tuple_alive = false; - } - else - tuple_alive = true; - - itup = (IndexTuple) PageGetItem(page, iid); - Assert(!BTreeTupleIsPivot(itup)); - - pstate.offnum = offnum; - if (arrayKeys && offnum == minoff && pstate.forcenonrequired) - { - /* Reset arrays, per _bt_set_startikey contract */ - pstate.forcenonrequired = false; - pstate.startikey = 0; - _bt_start_array_keys(scan, dir); - } - passes_quals = _bt_checkkeys(scan, &pstate, arrayKeys, - itup, indnatts); - - if (arrayKeys && so->scanBehind) - { - /* - * Done scanning this page, but not done with the current - * primscan. - * - * Note: Forward scans don't check this explicitly, since they - * prefer to reuse pstate.skip for this instead. - */ - Assert(!passes_quals && pstate.continuescan); - Assert(!pstate.forcenonrequired); - - break; - } - - /* - * Check if we need to skip ahead to a later tuple (only possible - * when the scan uses array keys) - */ - if (arrayKeys && OffsetNumberIsValid(pstate.skip)) - { - Assert(!passes_quals && pstate.continuescan); - Assert(offnum > pstate.skip); - Assert(!pstate.forcenonrequired); - - offnum = pstate.skip; - pstate.skip = InvalidOffsetNumber; - continue; - } - - if (passes_quals && tuple_alive) - { - /* tuple passes all scan key conditions */ - if (!BTreeTupleIsPosting(itup)) - { - /* Remember it */ - itemIndex--; - _bt_saveitem(so, itemIndex, offnum, itup); - } - else - { - int tupleOffset; - - /* - * Set up state to return posting list, and remember first - * TID. - * - * Note that we deliberately save/return items from - * posting lists in ascending heap TID order for backwards - * scans. This allows _bt_killitems() to make a - * consistent assumption about the order of items - * associated with the same posting list tuple. - */ - itemIndex--; - tupleOffset = - _bt_setuppostingitems(so, itemIndex, offnum, - BTreeTupleGetPostingN(itup, 0), - itup); - /* Remember additional TIDs */ - for (int i = 1; i < BTreeTupleGetNPosting(itup); i++) - { - itemIndex--; - _bt_savepostingitem(so, itemIndex, offnum, - BTreeTupleGetPostingN(itup, i), - tupleOffset); - } - } - } - /* When !continuescan, there can't be any more matches, so stop */ - if (!pstate.continuescan) - break; - - offnum = OffsetNumberPrev(offnum); - } - - /* - * We don't need to visit page to the left when no more matches will - * be found there - */ - if (!pstate.continuescan) - so->currPos.moreLeft = false; - - Assert(itemIndex >= 0); - so->currPos.firstItem = itemIndex; - so->currPos.lastItem = MaxTIDsPerBTreePage - 1; - so->currPos.itemIndex = MaxTIDsPerBTreePage - 1; - } - - /* - * If _bt_set_startikey told us to temporarily treat the scan's keys as - * nonrequired (possible only during scans with array keys), there must be - * no lasting consequences for the scan's array keys. The scan's arrays - * should now have exactly the same elements as they would have had if the - * nonrequired behavior had never been used. (In general, a scan's arrays - * are expected to track its progress through the index's key space.) - * - * We are required (by _bt_set_startikey) to call _bt_checkkeys against - * pstate.finaltup with pstate.forcenonrequired=false to allow the scan's - * arrays to recover. Assert that that step hasn't been missed. - */ - Assert(!pstate.forcenonrequired); - - return (so->currPos.firstItem <= so->currPos.lastItem); -} - -/* Save an index item into so->currPos.items[itemIndex] */ -static void -_bt_saveitem(BTScanOpaque so, int itemIndex, - OffsetNumber offnum, IndexTuple itup) -{ - BTScanPosItem *currItem = &so->currPos.items[itemIndex]; - - Assert(!BTreeTupleIsPivot(itup) && !BTreeTupleIsPosting(itup)); - - currItem->heapTid = itup->t_tid; - currItem->indexOffset = offnum; - if (so->currTuples) - { - Size itupsz = IndexTupleSize(itup); - - currItem->tupleOffset = so->currPos.nextTupleOffset; - memcpy(so->currTuples + so->currPos.nextTupleOffset, itup, itupsz); - so->currPos.nextTupleOffset += MAXALIGN(itupsz); - } -} - -/* - * Setup state to save TIDs/items from a single posting list tuple. - * - * Saves an index item into so->currPos.items[itemIndex] for TID that is - * returned to scan first. Second or subsequent TIDs for posting list should - * be saved by calling _bt_savepostingitem(). - * - * Returns an offset into tuple storage space that main tuple is stored at if - * needed. - */ -static int -_bt_setuppostingitems(BTScanOpaque so, int itemIndex, OffsetNumber offnum, - ItemPointer heapTid, IndexTuple itup) -{ - BTScanPosItem *currItem = &so->currPos.items[itemIndex]; - - Assert(BTreeTupleIsPosting(itup)); - - currItem->heapTid = *heapTid; - currItem->indexOffset = offnum; - if (so->currTuples) - { - /* Save base IndexTuple (truncate posting list) */ - IndexTuple base; - Size itupsz = BTreeTupleGetPostingOffset(itup); - - itupsz = MAXALIGN(itupsz); - currItem->tupleOffset = so->currPos.nextTupleOffset; - base = (IndexTuple) (so->currTuples + so->currPos.nextTupleOffset); - memcpy(base, itup, itupsz); - /* Defensively reduce work area index tuple header size */ - base->t_info &= ~INDEX_SIZE_MASK; - base->t_info |= itupsz; - so->currPos.nextTupleOffset += itupsz; - - return currItem->tupleOffset; - } - - return 0; -} - -/* - * Save an index item into so->currPos.items[itemIndex] for current posting - * tuple. - * - * Assumes that _bt_setuppostingitems() has already been called for current - * posting list tuple. Caller passes its return value as tupleOffset. - */ -static inline void -_bt_savepostingitem(BTScanOpaque so, int itemIndex, OffsetNumber offnum, - ItemPointer heapTid, int tupleOffset) -{ - BTScanPosItem *currItem = &so->currPos.items[itemIndex]; - - currItem->heapTid = *heapTid; - currItem->indexOffset = offnum; - - /* - * Have index-only scans return the same base IndexTuple for every TID - * that originates from the same posting list - */ - if (so->currTuples) - currItem->tupleOffset = tupleOffset; -} - /* * Return the index item from so->currPos.items[so->currPos.itemIndex] to the * index scan by setting the relevant fields in caller's index scan descriptor @@ -2107,10 +1639,9 @@ _bt_returnitem(IndexScanDesc scan, BTScanOpaque so) * * Wrapper on _bt_readnextpage that performs final steps for the current page. * - * On entry, if so->currPos.buf is valid the buffer is pinned but not locked. - * If there's no pin held, it's because _bt_drop_lock_and_maybe_pin dropped - * the pin eagerly earlier on. The scan must have so->currPos.currPage set to - * a valid block, in any case. + * On entry, so->currPos must be valid. Its buffer will be pinned, though + * never locked. (Actually, when so->dropPin there won't even be a pin held, + * though so->currPos.currPage must still be set to a valid block number.) */ static bool _bt_steppage(IndexScanDesc scan, ScanDirection dir) @@ -2197,12 +1728,9 @@ _bt_steppage(IndexScanDesc scan, ScanDirection dir) * * _bt_first caller passes us an offnum returned by _bt_binsrch, which might * be an out of bounds offnum such as "maxoff + 1" in certain corner cases. - * _bt_checkkeys will stop the scan as soon as an equality qual fails (when - * its scan key was marked required), so _bt_first _must_ pass us an offnum - * exactly at the beginning of where equal tuples are to be found. When we're - * passed an offnum past the end of the page, we might still manage to stop - * the scan on this page by calling _bt_checkkeys against the high key. See - * _bt_readpage for full details. + * When we're passed an offnum past the end of the page, we might still manage + * to stop the scan on this page by calling _bt_checkkeys against the high + * key. See _bt_readpage for full details. * * On entry, so->currPos must be pinned and locked (so offnum stays valid). * Parallel scan callers must have seized the scan before calling here. @@ -2251,12 +1779,14 @@ _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, ScanDirection dir) */ if (_bt_readpage(scan, dir, offnum, true)) { + Relation rel = scan->indexRelation; + /* * _bt_readpage succeeded. Drop the lock (and maybe the pin) on * so->currPos.buf in preparation for btgettuple returning tuples. */ Assert(BTScanPosIsPinned(so->currPos)); - _bt_drop_lock_and_maybe_pin(scan, &so->currPos); + _bt_drop_lock_and_maybe_pin(rel, so); return true; } @@ -2278,9 +1808,12 @@ _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, ScanDirection dir) * previously-saved right link or left link. lastcurrblkno is the page that * was current at the point where the blkno link was saved, which we use to * reason about concurrent page splits/page deletions during backwards scans. + * In the common case where seized=false, blkno is either so->currPos.nextPage + * or so->currPos.prevPage, and lastcurrblkno is so->currPos.currPage. * - * On entry, caller shouldn't hold any locks or pins on any page (we work - * directly off of blkno and lastcurrblkno instead). Parallel scan callers + * On entry, so->currPos shouldn't be locked by caller. so->currPos.buf must + * be InvalidBuffer/unpinned as needed by caller (note that lastcurrblkno + * won't need to be read again in almost all cases). Parallel scan callers * that seized the scan before calling here should pass seized=true; such a * caller's blkno and lastcurrblkno arguments come from the seized scan. * seized=false callers just pass us the blkno/lastcurrblkno taken from their @@ -2294,11 +1827,11 @@ _bt_readfirstpage(IndexScanDesc scan, OffsetNumber offnum, ScanDirection dir) * * On success exit, so->currPos is updated to contain data from the next * interesting page, and we return true. We hold a pin on the buffer on - * success exit, except when _bt_drop_lock_and_maybe_pin decided it was safe - * to eagerly drop the pin (to avoid blocking VACUUM). + * success exit (except during so->dropPin index scans, when we drop the pin + * eagerly to avoid blocking VACUUM). * - * If there are no more matching records in the given direction, we drop all - * locks and pins, invalidate so->currPos, and return false. + * If there are no more matching records in the given direction, we invalidate + * so->currPos (while ensuring it retains no locks or pins), and return false. * * We always release the scan for a parallel scan caller, regardless of * success or failure; we'll call _bt_parallel_release as soon as possible. @@ -2413,7 +1946,7 @@ _bt_readnextpage(IndexScanDesc scan, BlockNumber blkno, */ Assert(so->currPos.currPage == blkno); Assert(BTScanPosIsPinned(so->currPos)); - _bt_drop_lock_and_maybe_pin(scan, &so->currPos); + _bt_drop_lock_and_maybe_pin(rel, so); return true; } @@ -2616,6 +2149,9 @@ _bt_get_endpoint(Relation rel, uint32 level, bool rightmost) else offnum = P_FIRSTDATAKEY(opaque); + if (offnum < 1 || offnum > PageGetMaxOffsetNumber(page)) + elog(PANIC, "offnum out of range"); + itup = (IndexTuple) PageGetItem(page, PageGetItemId(page, offnum)); blkno = BTreeTupleGetDownLink(itup); diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c index 3794cc924ad46..756dfa3dcf47e 100644 --- a/src/backend/access/nbtree/nbtsort.c +++ b/src/backend/access/nbtree/nbtsort.c @@ -29,7 +29,7 @@ * This code isn't concerned about the FSM at all. The caller is responsible * for initializing that. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -44,6 +44,7 @@ #include "access/parallel.h" #include "access/relscan.h" #include "access/table.h" +#include "access/tableam.h" #include "access/xact.h" #include "catalog/index.h" #include "commands/progress.h" @@ -51,10 +52,13 @@ #include "miscadmin.h" #include "pgstat.h" #include "storage/bulk_write.h" +#include "storage/condition_variable.h" +#include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/rel.h" #include "utils/sortsupport.h" #include "utils/tuplesort.h" +#include "utils/wait_event.h" /* Magic numbers for parallel state sharing */ @@ -68,8 +72,8 @@ /* * DISABLE_LEADER_PARTICIPATION disables the leader's participation in * parallel index builds. This may be useful as a debugging aid. -#undef DISABLE_LEADER_PARTICIPATION */ +/* #define DISABLE_LEADER_PARTICIPATION */ /* * Status record for spooling/sorting phase. (Note we may have two of @@ -105,7 +109,7 @@ typedef struct BTShared int scantuplesortstates; /* Query ID, for report in worker processes */ - uint64 queryid; + int64 queryid; /* * workersdonecv is used to monitor the progress of workers. All parallel @@ -256,8 +260,8 @@ typedef struct BTWriteState static double _bt_spools_heapscan(Relation heap, Relation index, BTBuildState *buildstate, IndexInfo *indexInfo); static void _bt_spooldestroy(BTSpool *btspool); -static void _bt_spool(BTSpool *btspool, ItemPointer self, - Datum *values, bool *isnull); +static void _bt_spool(BTSpool *btspool, const ItemPointerData *self, + const Datum *values, const bool *isnull); static void _bt_leafbuild(BTSpool *btspool, BTSpool *btspool2); static void _bt_build_callback(Relation index, ItemPointer tid, Datum *values, bool *isnull, bool tupleIsAlive, void *state); @@ -265,7 +269,7 @@ static BulkWriteBuffer _bt_blnewpage(BTWriteState *wstate, uint32 level); static BTPageState *_bt_pagestate(BTWriteState *wstate, uint32 level); static void _bt_slideleft(Page rightmostpage); static void _bt_sortaddtup(Page page, Size itemsize, - IndexTuple itup, OffsetNumber itup_off, + const IndexTupleData *itup, OffsetNumber itup_off, bool newfirstdataitem); static void _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, Size truncextra); @@ -334,7 +338,7 @@ btbuild(Relation heap, Relation index, IndexInfo *indexInfo) if (buildstate.btleader) _bt_end_parallel(buildstate.btleader); - result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + result = palloc_object(IndexBuildResult); result->heap_tuples = reltuples; result->index_tuples = buildstate.indtuples; @@ -365,7 +369,7 @@ static double _bt_spools_heapscan(Relation heap, Relation index, BTBuildState *buildstate, IndexInfo *indexInfo) { - BTSpool *btspool = (BTSpool *) palloc0(sizeof(BTSpool)); + BTSpool *btspool = palloc0_object(BTSpool); SortCoordinate coordinate = NULL; double reltuples = 0; @@ -398,7 +402,7 @@ _bt_spools_heapscan(Relation heap, Relation index, BTBuildState *buildstate, */ if (buildstate->btleader) { - coordinate = (SortCoordinate) palloc0(sizeof(SortCoordinateData)); + coordinate = palloc0_object(SortCoordinateData); coordinate->isWorker = false; coordinate->nParticipants = buildstate->btleader->nparticipanttuplesorts; @@ -439,7 +443,7 @@ _bt_spools_heapscan(Relation heap, Relation index, BTBuildState *buildstate, */ if (indexInfo->ii_Unique) { - BTSpool *btspool2 = (BTSpool *) palloc0(sizeof(BTSpool)); + BTSpool *btspool2 = palloc0_object(BTSpool); SortCoordinate coordinate2 = NULL; /* Initialize secondary spool */ @@ -456,7 +460,7 @@ _bt_spools_heapscan(Relation heap, Relation index, BTBuildState *buildstate, * tuplesort_begin_index_btree() about the basic high level * coordination of a parallel sort. */ - coordinate2 = (SortCoordinate) palloc0(sizeof(SortCoordinateData)); + coordinate2 = palloc0_object(SortCoordinateData); coordinate2->isWorker = false; coordinate2->nParticipants = buildstate->btleader->nparticipanttuplesorts; @@ -524,7 +528,7 @@ _bt_spooldestroy(BTSpool *btspool) * spool an index entry into the sort file. */ static void -_bt_spool(BTSpool *btspool, ItemPointer self, Datum *values, bool *isnull) +_bt_spool(BTSpool *btspool, const ItemPointerData *self, const Datum *values, const bool *isnull) { tuplesort_putindextuplevalues(btspool->sortstate, btspool->index, self, values, isnull); @@ -647,7 +651,7 @@ _bt_blwritepage(BTWriteState *wstate, BulkWriteBuffer buf, BlockNumber blkno) static BTPageState * _bt_pagestate(BTWriteState *wstate, uint32 level) { - BTPageState *state = (BTPageState *) palloc0(sizeof(BTPageState)); + BTPageState *state = palloc0_object(BTPageState); /* create initial page for level */ state->btps_buf = _bt_blnewpage(wstate, level); @@ -715,7 +719,7 @@ _bt_slideleft(Page rightmostpage) static void _bt_sortaddtup(Page page, Size itemsize, - IndexTuple itup, + const IndexTupleData *itup, OffsetNumber itup_off, bool newfirstdataitem) { @@ -730,8 +734,7 @@ _bt_sortaddtup(Page page, itemsize = sizeof(IndexTupleData); } - if (PageAddItem(page, (Item) itup, itemsize, itup_off, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, itup, itemsize, itup_off, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add item to the index page"); } @@ -933,8 +936,7 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, Assert(IndexTupleSize(oitup) > last_truncextra); truncated = _bt_truncate(wstate->index, lastleft, oitup, wstate->inskey); - if (!PageIndexTupleOverwrite(opage, P_HIKEY, (Item) truncated, - IndexTupleSize(truncated))) + if (!PageIndexTupleOverwrite(opage, P_HIKEY, truncated, IndexTupleSize(truncated))) elog(ERROR, "failed to add high key to the index page"); pfree(truncated); @@ -1003,7 +1005,7 @@ _bt_buildadd(BTWriteState *wstate, BTPageState *state, IndexTuple itup, if (last_off == P_HIKEY) { Assert(state->btps_lowkey == NULL); - state->btps_lowkey = palloc0(sizeof(IndexTupleData)); + state->btps_lowkey = palloc0_object(IndexTupleData); state->btps_lowkey->t_info = sizeof(IndexTupleData); BTreeTupleSetNAtts(state->btps_lowkey, 0, false); } @@ -1165,7 +1167,7 @@ _bt_load(BTWriteState *wstate, BTSpool *btspool, BTSpool *btspool2) itup2 = tuplesort_getindextuple(btspool2->sortstate, true); /* Prepare SortSupport data for each column */ - sortKeys = (SortSupport) palloc0(keysz * sizeof(SortSupportData)); + sortKeys = palloc0_array(SortSupportData, keysz); for (i = 0; i < keysz; i++) { @@ -1267,7 +1269,7 @@ _bt_load(BTWriteState *wstate, BTSpool *btspool, BTSpool *btspool2) /* merge is unnecessary, deduplicate into posting lists */ BTDedupState dstate; - dstate = (BTDedupState) palloc(sizeof(BTDedupStateData)); + dstate = palloc_object(BTDedupStateData); dstate->deduplicate = true; /* unused */ dstate->nmaxitems = 0; /* unused */ dstate->maxpostingsize = 0; /* set later */ @@ -1405,7 +1407,7 @@ _bt_begin_parallel(BTBuildState *buildstate, bool isconcurrent, int request) Sharedsort *sharedsort; Sharedsort *sharedsort2; BTSpool *btspool = buildstate->spool; - BTLeader *btleader = (BTLeader *) palloc0(sizeof(BTLeader)); + BTLeader *btleader = palloc0_object(BTLeader); WalUsage *walusage; BufferUsage *bufferusage; bool leaderparticipates = true; @@ -1694,7 +1696,7 @@ _bt_leader_participate_as_worker(BTBuildState *buildstate) int sortmem; /* Allocate memory and initialize private spool */ - leaderworker = (BTSpool *) palloc0(sizeof(BTSpool)); + leaderworker = palloc0_object(BTSpool); leaderworker->heap = buildstate->spool->heap; leaderworker->index = buildstate->spool->index; leaderworker->isunique = buildstate->spool->isunique; @@ -1706,7 +1708,7 @@ _bt_leader_participate_as_worker(BTBuildState *buildstate) else { /* Allocate memory for worker's own private secondary spool */ - leaderworker2 = (BTSpool *) palloc0(sizeof(BTSpool)); + leaderworker2 = palloc0_object(BTSpool); /* Initialize worker's own secondary spool */ leaderworker2->heap = leaderworker->heap; @@ -1797,7 +1799,7 @@ _bt_parallel_build_main(dsm_segment *seg, shm_toc *toc) indexRel = index_open(btshared->indexrelid, indexLockmode); /* Initialize worker's own spool */ - btspool = (BTSpool *) palloc0(sizeof(BTSpool)); + btspool = palloc0_object(BTSpool); btspool->heap = heapRel; btspool->index = indexRel; btspool->isunique = btshared->isunique; @@ -1814,7 +1816,7 @@ _bt_parallel_build_main(dsm_segment *seg, shm_toc *toc) else { /* Allocate memory for worker's own private secondary spool */ - btspool2 = (BTSpool *) palloc0(sizeof(BTSpool)); + btspool2 = palloc0_object(BTSpool); /* Initialize worker's own secondary spool */ btspool2->heap = btspool->heap; @@ -1875,7 +1877,7 @@ _bt_parallel_scan_and_sort(BTSpool *btspool, BTSpool *btspool2, IndexInfo *indexInfo; /* Initialize local tuplesort coordination state */ - coordinate = palloc0(sizeof(SortCoordinateData)); + coordinate = palloc0_object(SortCoordinateData); coordinate->isWorker = true; coordinate->nParticipants = -1; coordinate->sharedsort = sharedsort; @@ -1902,7 +1904,7 @@ _bt_parallel_scan_and_sort(BTSpool *btspool, BTSpool *btspool2, * worker). Worker processes are generally permitted to allocate * work_mem independently. */ - coordinate2 = palloc0(sizeof(SortCoordinateData)); + coordinate2 = palloc0_object(SortCoordinateData); coordinate2->isWorker = true; coordinate2->nParticipants = -1; coordinate2->sharedsort = sharedsort2; @@ -1926,7 +1928,8 @@ _bt_parallel_scan_and_sort(BTSpool *btspool, BTSpool *btspool2, indexInfo = BuildIndexInfo(btspool->index); indexInfo->ii_Concurrent = btshared->isconcurrent; scan = table_beginscan_parallel(btspool->heap, - ParallelTableScanFromBTShared(btshared)); + ParallelTableScanFromBTShared(btshared), + SO_NONE); reltuples = table_index_build_scan(btspool->heap, btspool->index, indexInfo, true, progress, _bt_build_callback, &buildstate, scan); diff --git a/src/backend/access/nbtree/nbtsplitloc.c b/src/backend/access/nbtree/nbtsplitloc.c index e6c9aaa0454dd..de9eca3c8b2ea 100644 --- a/src/backend/access/nbtree/nbtsplitloc.c +++ b/src/backend/access/nbtree/nbtsplitloc.c @@ -3,7 +3,7 @@ * nbtsplitloc.c * Choose split point code for Postgres btree implementation. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -15,6 +15,7 @@ #include "postgres.h" #include "access/nbtree.h" +#include "access/tableam.h" #include "common/int.h" typedef enum @@ -68,7 +69,7 @@ static void _bt_deltasortsplits(FindSplitData *state, double fillfactormult, static int _bt_splitcmp(const void *arg1, const void *arg2); static bool _bt_afternewitemoff(FindSplitData *state, OffsetNumber maxoff, int leaffillfactor, bool *usemult); -static bool _bt_adjacenthtid(ItemPointer lowhtid, ItemPointer highhtid); +static bool _bt_adjacenthtid(const ItemPointerData *lowhtid, const ItemPointerData *highhtid); static OffsetNumber _bt_bestsplitloc(FindSplitData *state, int perfectpenalty, bool *newitemonleft, FindSplitStrat strategy); static int _bt_defaultinterval(FindSplitData *state); @@ -196,7 +197,7 @@ _bt_findsplitloc(Relation rel, * between tuples will be legal). */ state.maxsplits = maxoff; - state.splits = palloc(sizeof(SplitPoint) * state.maxsplits); + state.splits = palloc_array(SplitPoint, state.maxsplits); state.nsplits = 0; /* @@ -593,8 +594,8 @@ _bt_deltasortsplits(FindSplitData *state, double fillfactormult, static int _bt_splitcmp(const void *arg1, const void *arg2) { - SplitPoint *split1 = (SplitPoint *) arg1; - SplitPoint *split2 = (SplitPoint *) arg2; + const SplitPoint *split1 = arg1; + const SplitPoint *split2 = arg2; return pg_cmp_s16(split1->curdelta, split2->curdelta); } @@ -746,7 +747,7 @@ _bt_afternewitemoff(FindSplitData *state, OffsetNumber maxoff, * transaction. */ static bool -_bt_adjacenthtid(ItemPointer lowhtid, ItemPointer highhtid) +_bt_adjacenthtid(const ItemPointerData *lowhtid, const ItemPointerData *highhtid) { BlockNumber lowblk, highblk; diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c index 1a15dfcb7d357..014faa1622fb5 100644 --- a/src/backend/access/nbtree/nbtutils.c +++ b/src/backend/access/nbtree/nbtutils.c @@ -3,7 +3,7 @@ * nbtutils.c * Utility code for Postgres btree implementation. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,3307 +19,143 @@ #include "access/nbtree.h" #include "access/reloptions.h" +#include "access/relscan.h" #include "commands/progress.h" +#include "common/int.h" +#include "lib/qunique.h" #include "miscadmin.h" +#include "storage/lwlock.h" +#include "storage/subsystems.h" #include "utils/datum.h" #include "utils/lsyscache.h" +#include "utils/rel.h" -#define LOOK_AHEAD_REQUIRED_RECHECKS 3 -#define LOOK_AHEAD_DEFAULT_DISTANCE 5 -#define NSKIPADVANCES_THRESHOLD 3 - -static inline int32 _bt_compare_array_skey(FmgrInfo *orderproc, - Datum tupdatum, bool tupnull, - Datum arrdatum, ScanKey cur); -static void _bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir, - Datum tupdatum, bool tupnull, - BTArrayKeyInfo *array, ScanKey cur, - int32 *set_elem_result); -static void _bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array, - int32 set_elem_result, Datum tupdatum, bool tupnull); -static void _bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array); -static void _bt_array_set_low_or_high(Relation rel, ScanKey skey, - BTArrayKeyInfo *array, bool low_not_high); -static bool _bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array); -static bool _bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array); -static bool _bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir, - bool *skip_array_set); -static void _bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir); -static bool _bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir, - IndexTuple tuple, TupleDesc tupdesc, int tupnatts, - bool readpagetup, int sktrig, bool *scanBehind); -static bool _bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate, - IndexTuple tuple, int tupnatts, TupleDesc tupdesc, - int sktrig, bool sktrig_required); -#ifdef USE_ASSERT_CHECKING -static bool _bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir); -static bool _bt_verify_keys_with_arraykeys(IndexScanDesc scan); -#endif -static bool _bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir, - IndexTuple finaltup); -static bool _bt_check_compare(IndexScanDesc scan, ScanDirection dir, - IndexTuple tuple, int tupnatts, TupleDesc tupdesc, - bool advancenonrequired, bool forcenonrequired, - bool *continuescan, int *ikey); -static bool _bt_check_rowcompare(ScanKey skey, - IndexTuple tuple, int tupnatts, TupleDesc tupdesc, - ScanDirection dir, bool forcenonrequired, bool *continuescan); -static void _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, - int tupnatts, TupleDesc tupdesc); -static int _bt_keep_natts(Relation rel, IndexTuple lastleft, - IndexTuple firstright, BTScanInsert itup_key); - - -/* - * _bt_mkscankey - * Build an insertion scan key that contains comparison data from itup - * as well as comparator routines appropriate to the key datatypes. - * - * The result is intended for use with _bt_compare() and _bt_truncate(). - * Callers that don't need to fill out the insertion scankey arguments - * (e.g. they use an ad-hoc comparison routine, or only need a scankey - * for _bt_truncate()) can pass a NULL index tuple. The scankey will - * be initialized as if an "all truncated" pivot tuple was passed - * instead. - * - * Note that we may occasionally have to share lock the metapage to - * determine whether or not the keys in the index are expected to be - * unique (i.e. if this is a "heapkeyspace" index). We assume a - * heapkeyspace index when caller passes a NULL tuple, allowing index - * build callers to avoid accessing the non-existent metapage. We - * also assume that the index is _not_ allequalimage when a NULL tuple - * is passed; CREATE INDEX callers call _bt_allequalimage() to set the - * field themselves. - */ -BTScanInsert -_bt_mkscankey(Relation rel, IndexTuple itup) -{ - BTScanInsert key; - ScanKey skey; - TupleDesc itupdesc; - int indnkeyatts; - int16 *indoption; - int tupnatts; - int i; - - itupdesc = RelationGetDescr(rel); - indnkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); - indoption = rel->rd_indoption; - tupnatts = itup ? BTreeTupleGetNAtts(itup, rel) : 0; - - Assert(tupnatts <= IndexRelationGetNumberOfAttributes(rel)); - - /* - * We'll execute search using scan key constructed on key columns. - * Truncated attributes and non-key attributes are omitted from the final - * scan key. - */ - key = palloc(offsetof(BTScanInsertData, scankeys) + - sizeof(ScanKeyData) * indnkeyatts); - if (itup) - _bt_metaversion(rel, &key->heapkeyspace, &key->allequalimage); - else - { - /* Utility statement callers can set these fields themselves */ - key->heapkeyspace = true; - key->allequalimage = false; - } - key->anynullkeys = false; /* initial assumption */ - key->nextkey = false; /* usual case, required by btinsert */ - key->backward = false; /* usual case, required by btinsert */ - key->keysz = Min(indnkeyatts, tupnatts); - key->scantid = key->heapkeyspace && itup ? - BTreeTupleGetHeapTID(itup) : NULL; - skey = key->scankeys; - for (i = 0; i < indnkeyatts; i++) - { - FmgrInfo *procinfo; - Datum arg; - bool null; - int flags; - - /* - * We can use the cached (default) support procs since no cross-type - * comparison can be needed. - */ - procinfo = index_getprocinfo(rel, i + 1, BTORDER_PROC); - - /* - * Key arguments built from truncated attributes (or when caller - * provides no tuple) are defensively represented as NULL values. They - * should never be used. - */ - if (i < tupnatts) - arg = index_getattr(itup, i + 1, itupdesc, &null); - else - { - arg = (Datum) 0; - null = true; - } - flags = (null ? SK_ISNULL : 0) | (indoption[i] << SK_BT_INDOPTION_SHIFT); - ScanKeyEntryInitializeWithInfo(&skey[i], - flags, - (AttrNumber) (i + 1), - InvalidStrategy, - InvalidOid, - rel->rd_indcollation[i], - procinfo, - arg); - /* Record if any key attribute is NULL (or truncated) */ - if (null) - key->anynullkeys = true; - } - - /* - * In NULLS NOT DISTINCT mode, we pretend that there are no null keys, so - * that full uniqueness check is done. - */ - if (rel->rd_index->indnullsnotdistinct) - key->anynullkeys = false; - - return key; -} - -/* - * free a retracement stack made by _bt_search. - */ -void -_bt_freestack(BTStack stack) -{ - BTStack ostack; - - while (stack != NULL) - { - ostack = stack; - stack = stack->bts_parent; - pfree(ostack); - } -} - -/* - * _bt_compare_array_skey() -- apply array comparison function - * - * Compares caller's tuple attribute value to a scan key/array element. - * Helper function used during binary searches of SK_SEARCHARRAY arrays. - * - * This routine returns: - * <0 if tupdatum < arrdatum; - * 0 if tupdatum == arrdatum; - * >0 if tupdatum > arrdatum. - * - * This is essentially the same interface as _bt_compare: both functions - * compare the value that they're searching for to a binary search pivot. - * However, unlike _bt_compare, this function's "tuple argument" comes first, - * while its "array/scankey argument" comes second. -*/ -static inline int32 -_bt_compare_array_skey(FmgrInfo *orderproc, - Datum tupdatum, bool tupnull, - Datum arrdatum, ScanKey cur) -{ - int32 result = 0; - - Assert(cur->sk_strategy == BTEqualStrategyNumber); - Assert(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL))); - - if (tupnull) /* NULL tupdatum */ - { - if (cur->sk_flags & SK_ISNULL) - result = 0; /* NULL "=" NULL */ - else if (cur->sk_flags & SK_BT_NULLS_FIRST) - result = -1; /* NULL "<" NOT_NULL */ - else - result = 1; /* NULL ">" NOT_NULL */ - } - else if (cur->sk_flags & SK_ISNULL) /* NOT_NULL tupdatum, NULL arrdatum */ - { - if (cur->sk_flags & SK_BT_NULLS_FIRST) - result = 1; /* NOT_NULL ">" NULL */ - else - result = -1; /* NOT_NULL "<" NULL */ - } - else - { - /* - * Like _bt_compare, we need to be careful of cross-type comparisons, - * so the left value has to be the value that came from an index tuple - */ - result = DatumGetInt32(FunctionCall2Coll(orderproc, cur->sk_collation, - tupdatum, arrdatum)); - - /* - * We flip the sign by following the obvious rule: flip whenever the - * column is a DESC column. - * - * _bt_compare does it the wrong way around (flip when *ASC*) in order - * to compensate for passing its orderproc arguments backwards. We - * don't need to play these games because we find it natural to pass - * tupdatum as the left value (and arrdatum as the right value). - */ - if (cur->sk_flags & SK_BT_DESC) - INVERT_COMPARE_RESULT(result); - } - - return result; -} - -/* - * _bt_binsrch_array_skey() -- Binary search for next matching array key - * - * Returns an index to the first array element >= caller's tupdatum argument. - * This convention is more natural for forwards scan callers, but that can't - * really matter to backwards scan callers. Both callers require handling for - * the case where the match we return is < tupdatum, and symmetric handling - * for the case where our best match is > tupdatum. - * - * Also sets *set_elem_result to the result _bt_compare_array_skey returned - * when we used it to compare the matching array element to tupdatum/tupnull. - * - * cur_elem_trig indicates if array advancement was triggered by this array's - * scan key, and that the array is for a required scan key. We can apply this - * information to find the next matching array element in the current scan - * direction using far fewer comparisons (fewer on average, compared to naive - * binary search). This scheme takes advantage of an important property of - * required arrays: required arrays always advance in lockstep with the index - * scan's progress through the index's key space. - */ -int -_bt_binsrch_array_skey(FmgrInfo *orderproc, - bool cur_elem_trig, ScanDirection dir, - Datum tupdatum, bool tupnull, - BTArrayKeyInfo *array, ScanKey cur, - int32 *set_elem_result) -{ - int low_elem = 0, - mid_elem = -1, - high_elem = array->num_elems - 1, - result = 0; - Datum arrdatum; - - Assert(cur->sk_flags & SK_SEARCHARRAY); - Assert(!(cur->sk_flags & SK_BT_SKIP)); - Assert(!(cur->sk_flags & SK_ISNULL)); /* SAOP arrays never have NULLs */ - Assert(cur->sk_strategy == BTEqualStrategyNumber); - - if (cur_elem_trig) - { - Assert(!ScanDirectionIsNoMovement(dir)); - Assert(cur->sk_flags & SK_BT_REQFWD); - - /* - * When the scan key that triggered array advancement is a required - * array scan key, it is now certain that the current array element - * (plus all prior elements relative to the current scan direction) - * cannot possibly be at or ahead of the corresponding tuple value. - * (_bt_checkkeys must have called _bt_tuple_before_array_skeys, which - * makes sure this is true as a condition of advancing the arrays.) - * - * This makes it safe to exclude array elements up to and including - * the former-current array element from our search. - * - * Separately, when array advancement was triggered by a required scan - * key, the array element immediately after the former-current element - * is often either an exact tupdatum match, or a "close by" near-match - * (a near-match tupdatum is one whose key space falls _between_ the - * former-current and new-current array elements). We'll detect both - * cases via an optimistic comparison of the new search lower bound - * (or new search upper bound in the case of backwards scans). - */ - if (ScanDirectionIsForward(dir)) - { - low_elem = array->cur_elem + 1; /* old cur_elem exhausted */ - - /* Compare prospective new cur_elem (also the new lower bound) */ - if (high_elem >= low_elem) - { - arrdatum = array->elem_values[low_elem]; - result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, - arrdatum, cur); - - if (result <= 0) - { - /* Optimistic comparison optimization worked out */ - *set_elem_result = result; - return low_elem; - } - mid_elem = low_elem; - low_elem++; /* this cur_elem exhausted, too */ - } - - if (high_elem < low_elem) - { - /* Caller needs to perform "beyond end" array advancement */ - *set_elem_result = 1; - return high_elem; - } - } - else - { - high_elem = array->cur_elem - 1; /* old cur_elem exhausted */ - - /* Compare prospective new cur_elem (also the new upper bound) */ - if (high_elem >= low_elem) - { - arrdatum = array->elem_values[high_elem]; - result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, - arrdatum, cur); - - if (result >= 0) - { - /* Optimistic comparison optimization worked out */ - *set_elem_result = result; - return high_elem; - } - mid_elem = high_elem; - high_elem--; /* this cur_elem exhausted, too */ - } - - if (high_elem < low_elem) - { - /* Caller needs to perform "beyond end" array advancement */ - *set_elem_result = -1; - return low_elem; - } - } - } - - while (high_elem > low_elem) - { - mid_elem = low_elem + ((high_elem - low_elem) / 2); - arrdatum = array->elem_values[mid_elem]; - - result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, - arrdatum, cur); - - if (result == 0) - { - /* - * It's safe to quit as soon as we see an equal array element. - * This often saves an extra comparison or two... - */ - low_elem = mid_elem; - break; - } - - if (result > 0) - low_elem = mid_elem + 1; - else - high_elem = mid_elem; - } - - /* - * ...but our caller also cares about how its searched-for tuple datum - * compares to the low_elem datum. Must always set *set_elem_result with - * the result of that comparison specifically. - */ - if (low_elem != mid_elem) - result = _bt_compare_array_skey(orderproc, tupdatum, tupnull, - array->elem_values[low_elem], cur); - - *set_elem_result = result; - - return low_elem; -} - -/* - * _bt_binsrch_skiparray_skey() -- "Binary search" within a skip array - * - * Does not return an index into the array, since skip arrays don't really - * contain elements (they generate their array elements procedurally instead). - * Our interface matches that of _bt_binsrch_array_skey in every other way. - * - * Sets *set_elem_result just like _bt_binsrch_array_skey would with a true - * array. The value 0 indicates that tupdatum/tupnull is within the range of - * the skip array. We return -1 when tupdatum/tupnull is lower that any value - * within the range of the array, and 1 when it is higher than every value. - * Caller should pass *set_elem_result to _bt_skiparray_set_element to advance - * the array. - * - * cur_elem_trig indicates if array advancement was triggered by this array's - * scan key. We use this to optimize-away comparisons that are known by our - * caller to be unnecessary from context, just like _bt_binsrch_array_skey. - */ -static void -_bt_binsrch_skiparray_skey(bool cur_elem_trig, ScanDirection dir, - Datum tupdatum, bool tupnull, - BTArrayKeyInfo *array, ScanKey cur, - int32 *set_elem_result) -{ - Assert(cur->sk_flags & SK_BT_SKIP); - Assert(cur->sk_flags & SK_SEARCHARRAY); - Assert(cur->sk_flags & SK_BT_REQFWD); - Assert(array->num_elems == -1); - Assert(!ScanDirectionIsNoMovement(dir)); - - if (array->null_elem) - { - Assert(!array->low_compare && !array->high_compare); - - *set_elem_result = 0; - return; - } - - if (tupnull) /* NULL tupdatum */ - { - if (cur->sk_flags & SK_BT_NULLS_FIRST) - *set_elem_result = -1; /* NULL "<" NOT_NULL */ - else - *set_elem_result = 1; /* NULL ">" NOT_NULL */ - return; - } - - /* - * Array inequalities determine whether tupdatum is within the range of - * caller's skip array - */ - *set_elem_result = 0; - if (ScanDirectionIsForward(dir)) - { - /* - * Evaluate low_compare first (unless cur_elem_trig tells us that it - * cannot possibly fail to be satisfied), then evaluate high_compare - */ - if (!cur_elem_trig && array->low_compare && - !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, - array->low_compare->sk_collation, - tupdatum, - array->low_compare->sk_argument))) - *set_elem_result = -1; - else if (array->high_compare && - !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, - array->high_compare->sk_collation, - tupdatum, - array->high_compare->sk_argument))) - *set_elem_result = 1; - } - else - { - /* - * Evaluate high_compare first (unless cur_elem_trig tells us that it - * cannot possibly fail to be satisfied), then evaluate low_compare - */ - if (!cur_elem_trig && array->high_compare && - !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, - array->high_compare->sk_collation, - tupdatum, - array->high_compare->sk_argument))) - *set_elem_result = 1; - else if (array->low_compare && - !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, - array->low_compare->sk_collation, - tupdatum, - array->low_compare->sk_argument))) - *set_elem_result = -1; - } - - /* - * Assert that any keys that were assumed to be satisfied already (due to - * caller passing cur_elem_trig=true) really are satisfied as expected - */ -#ifdef USE_ASSERT_CHECKING - if (cur_elem_trig) - { - if (ScanDirectionIsForward(dir) && array->low_compare) - Assert(DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, - array->low_compare->sk_collation, - tupdatum, - array->low_compare->sk_argument))); - - if (ScanDirectionIsBackward(dir) && array->high_compare) - Assert(DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, - array->high_compare->sk_collation, - tupdatum, - array->high_compare->sk_argument))); - } -#endif -} - -/* - * _bt_skiparray_set_element() -- Set skip array scan key's sk_argument - * - * Caller passes set_elem_result returned by _bt_binsrch_skiparray_skey for - * caller's tupdatum/tupnull. - * - * We copy tupdatum/tupnull into skey's sk_argument iff set_elem_result == 0. - * Otherwise, we set skey to either the lowest or highest value that's within - * the range of caller's skip array (whichever is the best available match to - * tupdatum/tupnull that is still within the range of the skip array according - * to _bt_binsrch_skiparray_skey/set_elem_result). - */ -static void -_bt_skiparray_set_element(Relation rel, ScanKey skey, BTArrayKeyInfo *array, - int32 set_elem_result, Datum tupdatum, bool tupnull) -{ - Assert(skey->sk_flags & SK_BT_SKIP); - Assert(skey->sk_flags & SK_SEARCHARRAY); - - if (set_elem_result) - { - /* tupdatum/tupnull is out of the range of the skip array */ - Assert(!array->null_elem); - - _bt_array_set_low_or_high(rel, skey, array, set_elem_result < 0); - return; - } - - /* Advance skip array to tupdatum (or tupnull) value */ - if (unlikely(tupnull)) - { - _bt_skiparray_set_isnull(rel, skey, array); - return; - } - - /* Free memory previously allocated for sk_argument if needed */ - if (!array->attbyval && skey->sk_argument) - pfree(DatumGetPointer(skey->sk_argument)); - - /* tupdatum becomes new sk_argument/new current element */ - skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL | - SK_BT_MINVAL | SK_BT_MAXVAL | - SK_BT_NEXT | SK_BT_PRIOR); - skey->sk_argument = datumCopy(tupdatum, array->attbyval, array->attlen); -} - -/* - * _bt_skiparray_set_isnull() -- set skip array scan key to NULL - */ -static void -_bt_skiparray_set_isnull(Relation rel, ScanKey skey, BTArrayKeyInfo *array) -{ - Assert(skey->sk_flags & SK_BT_SKIP); - Assert(skey->sk_flags & SK_SEARCHARRAY); - Assert(array->null_elem && !array->low_compare && !array->high_compare); - - /* Free memory previously allocated for sk_argument if needed */ - if (!array->attbyval && skey->sk_argument) - pfree(DatumGetPointer(skey->sk_argument)); - - /* NULL becomes new sk_argument/new current element */ - skey->sk_argument = (Datum) 0; - skey->sk_flags &= ~(SK_BT_MINVAL | SK_BT_MAXVAL | - SK_BT_NEXT | SK_BT_PRIOR); - skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL); -} - -/* - * _bt_start_array_keys() -- Initialize array keys at start of a scan - * - * Set up the cur_elem counters and fill in the first sk_argument value for - * each array scankey. - */ -void -_bt_start_array_keys(IndexScanDesc scan, ScanDirection dir) -{ - Relation rel = scan->indexRelation; - BTScanOpaque so = (BTScanOpaque) scan->opaque; - - Assert(so->numArrayKeys); - Assert(so->qual_ok); - - for (int i = 0; i < so->numArrayKeys; i++) - { - BTArrayKeyInfo *array = &so->arrayKeys[i]; - ScanKey skey = &so->keyData[array->scan_key]; - - Assert(skey->sk_flags & SK_SEARCHARRAY); - - _bt_array_set_low_or_high(rel, skey, array, - ScanDirectionIsForward(dir)); - } - so->scanBehind = so->oppositeDirCheck = false; /* reset */ -} - -/* - * _bt_array_set_low_or_high() -- Set array scan key to lowest/highest element - * - * Caller also passes associated scan key, which will have its argument set to - * the lowest/highest array value in passing. - */ -static void -_bt_array_set_low_or_high(Relation rel, ScanKey skey, BTArrayKeyInfo *array, - bool low_not_high) -{ - Assert(skey->sk_flags & SK_SEARCHARRAY); - - if (array->num_elems != -1) - { - /* set low or high element for SAOP array */ - int set_elem = 0; - - Assert(!(skey->sk_flags & SK_BT_SKIP)); - - if (!low_not_high) - set_elem = array->num_elems - 1; - - /* - * Just copy over array datum (only skip arrays require freeing and - * allocating memory for sk_argument) - */ - array->cur_elem = set_elem; - skey->sk_argument = array->elem_values[set_elem]; - - return; - } - - /* set low or high element for skip array */ - Assert(skey->sk_flags & SK_BT_SKIP); - Assert(array->num_elems == -1); - - /* Free memory previously allocated for sk_argument if needed */ - if (!array->attbyval && skey->sk_argument) - pfree(DatumGetPointer(skey->sk_argument)); - - /* Reset flags */ - skey->sk_argument = (Datum) 0; - skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL | - SK_BT_MINVAL | SK_BT_MAXVAL | - SK_BT_NEXT | SK_BT_PRIOR); - - if (array->null_elem && - (low_not_high == ((skey->sk_flags & SK_BT_NULLS_FIRST) != 0))) - { - /* Requested element (either lowest or highest) has the value NULL */ - skey->sk_flags |= (SK_SEARCHNULL | SK_ISNULL); - } - else if (low_not_high) - { - /* Setting array to lowest element (according to low_compare) */ - skey->sk_flags |= SK_BT_MINVAL; - } - else - { - /* Setting array to highest element (according to high_compare) */ - skey->sk_flags |= SK_BT_MAXVAL; - } -} - -/* - * _bt_array_decrement() -- decrement array scan key's sk_argument - * - * Return value indicates whether caller's array was successfully decremented. - * Cannot decrement an array whose current element is already the first one. - */ -static bool -_bt_array_decrement(Relation rel, ScanKey skey, BTArrayKeyInfo *array) -{ - bool uflow = false; - Datum dec_sk_argument; - - Assert(skey->sk_flags & SK_SEARCHARRAY); - Assert(!(skey->sk_flags & (SK_BT_MAXVAL | SK_BT_NEXT | SK_BT_PRIOR))); - - /* SAOP array? */ - if (array->num_elems != -1) - { - Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL))); - if (array->cur_elem > 0) - { - /* - * Just decrement current element, and assign its datum to skey - * (only skip arrays need us to free existing sk_argument memory) - */ - array->cur_elem--; - skey->sk_argument = array->elem_values[array->cur_elem]; - - /* Successfully decremented array */ - return true; - } - - /* Cannot decrement to before first array element */ - return false; - } - - /* Nope, this is a skip array */ - Assert(skey->sk_flags & SK_BT_SKIP); - - /* - * The sentinel value that represents the minimum value within the range - * of a skip array (often just -inf) is never decrementable - */ - if (skey->sk_flags & SK_BT_MINVAL) - return false; - - /* - * When the current array element is NULL, and the lowest sorting value in - * the index is also NULL, we cannot decrement before first array element - */ - if ((skey->sk_flags & SK_ISNULL) && (skey->sk_flags & SK_BT_NULLS_FIRST)) - return false; - - /* - * Opclasses without skip support "decrement" the scan key's current - * element by setting the PRIOR flag. The true prior value is determined - * by repositioning to the last index tuple < existing sk_argument/current - * array element. Note that this works in the usual way when the scan key - * is already marked ISNULL (i.e. when the current element is NULL). - */ - if (!array->sksup) - { - /* Successfully "decremented" array */ - skey->sk_flags |= SK_BT_PRIOR; - return true; - } - - /* - * Opclasses with skip support directly decrement sk_argument - */ - if (skey->sk_flags & SK_ISNULL) - { - Assert(!(skey->sk_flags & SK_BT_NULLS_FIRST)); - - /* - * Existing sk_argument/array element is NULL (for an IS NULL qual). - * - * "Decrement" from NULL to the high_elem value provided by opclass - * skip support routine. - */ - skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL); - skey->sk_argument = datumCopy(array->sksup->high_elem, - array->attbyval, array->attlen); - return true; - } - - /* - * Ask opclass support routine to provide decremented copy of existing - * non-NULL sk_argument - */ - dec_sk_argument = array->sksup->decrement(rel, skey->sk_argument, &uflow); - if (unlikely(uflow)) - { - /* dec_sk_argument has undefined value (so no pfree) */ - if (array->null_elem && (skey->sk_flags & SK_BT_NULLS_FIRST)) - { - _bt_skiparray_set_isnull(rel, skey, array); - - /* Successfully "decremented" array to NULL */ - return true; - } - - /* Cannot decrement to before first array element */ - return false; - } - - /* - * Successfully decremented sk_argument to a non-NULL value. Make sure - * that the decremented value is still within the range of the array. - */ - if (array->low_compare && - !DatumGetBool(FunctionCall2Coll(&array->low_compare->sk_func, - array->low_compare->sk_collation, - dec_sk_argument, - array->low_compare->sk_argument))) - { - /* Keep existing sk_argument after all */ - if (!array->attbyval) - pfree(DatumGetPointer(dec_sk_argument)); - - /* Cannot decrement to before first array element */ - return false; - } - - /* Accept value returned by opclass decrement callback */ - if (!array->attbyval && skey->sk_argument) - pfree(DatumGetPointer(skey->sk_argument)); - skey->sk_argument = dec_sk_argument; - - /* Successfully decremented array */ - return true; -} - -/* - * _bt_array_increment() -- increment array scan key's sk_argument - * - * Return value indicates whether caller's array was successfully incremented. - * Cannot increment an array whose current element is already the final one. - */ -static bool -_bt_array_increment(Relation rel, ScanKey skey, BTArrayKeyInfo *array) -{ - bool oflow = false; - Datum inc_sk_argument; - - Assert(skey->sk_flags & SK_SEARCHARRAY); - Assert(!(skey->sk_flags & (SK_BT_MINVAL | SK_BT_NEXT | SK_BT_PRIOR))); - - /* SAOP array? */ - if (array->num_elems != -1) - { - Assert(!(skey->sk_flags & (SK_BT_SKIP | SK_BT_MINVAL | SK_BT_MAXVAL))); - if (array->cur_elem < array->num_elems - 1) - { - /* - * Just increment current element, and assign its datum to skey - * (only skip arrays need us to free existing sk_argument memory) - */ - array->cur_elem++; - skey->sk_argument = array->elem_values[array->cur_elem]; - - /* Successfully incremented array */ - return true; - } - - /* Cannot increment past final array element */ - return false; - } - - /* Nope, this is a skip array */ - Assert(skey->sk_flags & SK_BT_SKIP); - - /* - * The sentinel value that represents the maximum value within the range - * of a skip array (often just +inf) is never incrementable - */ - if (skey->sk_flags & SK_BT_MAXVAL) - return false; - - /* - * When the current array element is NULL, and the highest sorting value - * in the index is also NULL, we cannot increment past the final element - */ - if ((skey->sk_flags & SK_ISNULL) && !(skey->sk_flags & SK_BT_NULLS_FIRST)) - return false; - - /* - * Opclasses without skip support "increment" the scan key's current - * element by setting the NEXT flag. The true next value is determined by - * repositioning to the first index tuple > existing sk_argument/current - * array element. Note that this works in the usual way when the scan key - * is already marked ISNULL (i.e. when the current element is NULL). - */ - if (!array->sksup) - { - /* Successfully "incremented" array */ - skey->sk_flags |= SK_BT_NEXT; - return true; - } - - /* - * Opclasses with skip support directly increment sk_argument - */ - if (skey->sk_flags & SK_ISNULL) - { - Assert(skey->sk_flags & SK_BT_NULLS_FIRST); - - /* - * Existing sk_argument/array element is NULL (for an IS NULL qual). - * - * "Increment" from NULL to the low_elem value provided by opclass - * skip support routine. - */ - skey->sk_flags &= ~(SK_SEARCHNULL | SK_ISNULL); - skey->sk_argument = datumCopy(array->sksup->low_elem, - array->attbyval, array->attlen); - return true; - } - - /* - * Ask opclass support routine to provide incremented copy of existing - * non-NULL sk_argument - */ - inc_sk_argument = array->sksup->increment(rel, skey->sk_argument, &oflow); - if (unlikely(oflow)) - { - /* inc_sk_argument has undefined value (so no pfree) */ - if (array->null_elem && !(skey->sk_flags & SK_BT_NULLS_FIRST)) - { - _bt_skiparray_set_isnull(rel, skey, array); - - /* Successfully "incremented" array to NULL */ - return true; - } - - /* Cannot increment past final array element */ - return false; - } - - /* - * Successfully incremented sk_argument to a non-NULL value. Make sure - * that the incremented value is still within the range of the array. - */ - if (array->high_compare && - !DatumGetBool(FunctionCall2Coll(&array->high_compare->sk_func, - array->high_compare->sk_collation, - inc_sk_argument, - array->high_compare->sk_argument))) - { - /* Keep existing sk_argument after all */ - if (!array->attbyval) - pfree(DatumGetPointer(inc_sk_argument)); - - /* Cannot increment past final array element */ - return false; - } - - /* Accept value returned by opclass increment callback */ - if (!array->attbyval && skey->sk_argument) - pfree(DatumGetPointer(skey->sk_argument)); - skey->sk_argument = inc_sk_argument; - - /* Successfully incremented array */ - return true; -} - -/* - * _bt_advance_array_keys_increment() -- Advance to next set of array elements - * - * Advances the array keys by a single increment in the current scan - * direction. When there are multiple array keys this can roll over from the - * lowest order array to higher order arrays. - * - * Returns true if there is another set of values to consider, false if not. - * On true result, the scankeys are initialized with the next set of values. - * On false result, the scankeys stay the same, and the array keys are not - * advanced (every array remains at its final element for scan direction). - */ -static bool -_bt_advance_array_keys_increment(IndexScanDesc scan, ScanDirection dir, - bool *skip_array_set) -{ - Relation rel = scan->indexRelation; - BTScanOpaque so = (BTScanOpaque) scan->opaque; - - /* - * We must advance the last array key most quickly, since it will - * correspond to the lowest-order index column among the available - * qualifications - */ - for (int i = so->numArrayKeys - 1; i >= 0; i--) - { - BTArrayKeyInfo *array = &so->arrayKeys[i]; - ScanKey skey = &so->keyData[array->scan_key]; - - if (array->num_elems == -1) - *skip_array_set = true; - - if (ScanDirectionIsForward(dir)) - { - if (_bt_array_increment(rel, skey, array)) - return true; - } - else - { - if (_bt_array_decrement(rel, skey, array)) - return true; - } - - /* - * Couldn't increment (or decrement) array. Handle array roll over. - * - * Start over at the array's lowest sorting value (or its highest - * value, for backward scans)... - */ - _bt_array_set_low_or_high(rel, skey, array, - ScanDirectionIsForward(dir)); - - /* ...then increment (or decrement) next most significant array */ - } - - /* - * The array keys are now exhausted. - * - * Restore the array keys to the state they were in immediately before we - * were called. This ensures that the arrays only ever ratchet in the - * current scan direction. - * - * Without this, scans could overlook matching tuples when the scan - * direction gets reversed just before btgettuple runs out of items to - * return, but just after _bt_readpage prepares all the items from the - * scan's final page in so->currPos. When we're on the final page it is - * typical for so->currPos to get invalidated once btgettuple finally - * returns false, which'll effectively invalidate the scan's array keys. - * That hasn't happened yet, though -- and in general it may never happen. - */ - _bt_start_array_keys(scan, -dir); - - return false; -} - -/* - * _bt_rewind_nonrequired_arrays() -- Rewind SAOP arrays not marked required - * - * Called when _bt_advance_array_keys decides to start a new primitive index - * scan on the basis of the current scan position being before the position - * that _bt_first is capable of repositioning the scan to by applying an - * inequality operator required in the opposite-to-scan direction only. - * - * Although equality strategy scan keys (for both arrays and non-arrays alike) - * are either marked required in both directions or in neither direction, - * there is a sense in which non-required arrays behave like required arrays. - * With a qual such as "WHERE a IN (100, 200) AND b >= 3 AND c IN (5, 6, 7)", - * the scan key on "c" is non-required, but nevertheless enables positioning - * the scan at the first tuple >= "(100, 3, 5)" on the leaf level during the - * first descent of the tree by _bt_first. Later on, there could also be a - * second descent, that places the scan right before tuples >= "(200, 3, 5)". - * _bt_first must never be allowed to build an insertion scan key whose "c" - * entry is set to a value other than 5, the "c" array's first element/value. - * (Actually, it's the first in the current scan direction. This example uses - * a forward scan.) - * - * Calling here resets the array scan key elements for the scan's non-required - * arrays. This is strictly necessary for correctness in a subset of cases - * involving "required in opposite direction"-triggered primitive index scans. - * Not all callers are at risk of _bt_first using a non-required array like - * this, but advancement always resets the arrays when another primitive scan - * is scheduled, just to keep things simple. Array advancement even makes - * sure to reset non-required arrays during scans that have no inequalities. - * (Advancement still won't call here when there are no inequalities, though - * that's just because it's all handled indirectly instead.) - * - * Note: _bt_verify_arrays_bt_first is called by an assertion to enforce that - * everybody got this right. - * - * Note: In practice almost all SAOP arrays are marked required during - * preprocessing (if necessary by generating skip arrays). It is hardly ever - * truly necessary to call here, but consistently doing so is simpler. - */ -static void -_bt_rewind_nonrequired_arrays(IndexScanDesc scan, ScanDirection dir) -{ - Relation rel = scan->indexRelation; - BTScanOpaque so = (BTScanOpaque) scan->opaque; - int arrayidx = 0; - - for (int ikey = 0; ikey < so->numberOfKeys; ikey++) - { - ScanKey cur = so->keyData + ikey; - BTArrayKeyInfo *array = NULL; - - if (!(cur->sk_flags & SK_SEARCHARRAY) || - cur->sk_strategy != BTEqualStrategyNumber) - continue; - - array = &so->arrayKeys[arrayidx++]; - Assert(array->scan_key == ikey); - - if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))) - continue; - - Assert(array->num_elems != -1); /* No non-required skip arrays */ - - _bt_array_set_low_or_high(rel, cur, array, - ScanDirectionIsForward(dir)); - } -} - -/* - * _bt_tuple_before_array_skeys() -- too early to advance required arrays? - * - * We always compare the tuple using the current array keys (which we assume - * are already set in so->keyData[]). readpagetup indicates if tuple is the - * scan's current _bt_readpage-wise tuple. - * - * readpagetup callers must only call here when _bt_check_compare already set - * continuescan=false. We help these callers deal with _bt_check_compare's - * inability to distinguish between the < and > cases (it uses equality - * operator scan keys, whereas we use 3-way ORDER procs). These callers pass - * a _bt_check_compare-set sktrig value that indicates which scan key - * triggered the call (!readpagetup callers just pass us sktrig=0 instead). - * This information allows us to avoid wastefully checking earlier scan keys - * that were already deemed to have been satisfied inside _bt_check_compare. - * - * Returns false when caller's tuple is >= the current required equality scan - * keys (or <=, in the case of backwards scans). This happens to readpagetup - * callers when the scan has reached the point of needing its array keys - * advanced; caller will need to advance required and non-required arrays at - * scan key offsets >= sktrig, plus scan keys < sktrig iff sktrig rolls over. - * (When we return false to readpagetup callers, tuple can only be == current - * required equality scan keys when caller's sktrig indicates that the arrays - * need to be advanced due to an unsatisfied required inequality key trigger.) - * - * Returns true when caller passes a tuple that is < the current set of - * equality keys for the most significant non-equal required scan key/column - * (or > the keys, during backwards scans). This happens to readpagetup - * callers when tuple is still before the start of matches for the scan's - * required equality strategy scan keys. (sktrig can't have indicated that an - * inequality strategy scan key wasn't satisfied in _bt_check_compare when we - * return true. In fact, we automatically return false when passed such an - * inequality sktrig by readpagetup callers -- _bt_check_compare's initial - * continuescan=false doesn't really need to be confirmed here by us.) - * - * !readpagetup callers optionally pass us *scanBehind, which tracks whether - * any missing truncated attributes might have affected array advancement - * (compared to what would happen if it was shown the first non-pivot tuple on - * the page to the right of caller's finaltup/high key tuple instead). It's - * only possible that we'll set *scanBehind to true when caller passes us a - * pivot tuple (with truncated -inf attributes) that we return false for. - */ -static bool -_bt_tuple_before_array_skeys(IndexScanDesc scan, ScanDirection dir, - IndexTuple tuple, TupleDesc tupdesc, int tupnatts, - bool readpagetup, int sktrig, bool *scanBehind) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - - Assert(so->numArrayKeys); - Assert(so->numberOfKeys); - Assert(sktrig == 0 || readpagetup); - Assert(!readpagetup || scanBehind == NULL); - - if (scanBehind) - *scanBehind = false; - - for (int ikey = sktrig; ikey < so->numberOfKeys; ikey++) - { - ScanKey cur = so->keyData + ikey; - Datum tupdatum; - bool tupnull; - int32 result; - - /* readpagetup calls require one ORDER proc comparison (at most) */ - Assert(!readpagetup || ikey == sktrig); - - /* - * Once we reach a non-required scan key, we're completely done. - * - * Note: we deliberately don't consider the scan direction here. - * _bt_advance_array_keys caller requires that we track *scanBehind - * without concern for scan direction. - */ - if ((cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) == 0) - { - Assert(!readpagetup); - Assert(ikey > sktrig || ikey == 0); - return false; - } - - if (cur->sk_attno > tupnatts) - { - Assert(!readpagetup); - - /* - * When we reach a high key's truncated attribute, assume that the - * tuple attribute's value is >= the scan's equality constraint - * scan keys (but set *scanBehind to let interested callers know - * that a truncated attribute might have affected our answer). - */ - if (scanBehind) - *scanBehind = true; - - return false; - } - - /* - * Deal with inequality strategy scan keys that _bt_check_compare set - * continuescan=false for - */ - if (cur->sk_strategy != BTEqualStrategyNumber) - { - /* - * When _bt_check_compare indicated that a required inequality - * scan key wasn't satisfied, there's no need to verify anything; - * caller always calls _bt_advance_array_keys with this sktrig. - */ - if (readpagetup) - return false; - - /* - * Otherwise we can't give up, since we must check all required - * scan keys (required in either direction) in order to correctly - * track *scanBehind for caller - */ - continue; - } - - tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull); - - if (likely(!(cur->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL)))) - { - /* Scankey has a valid/comparable sk_argument value */ - result = _bt_compare_array_skey(&so->orderProcs[ikey], - tupdatum, tupnull, - cur->sk_argument, cur); - - if (result == 0) - { - /* - * Interpret result in a way that takes NEXT/PRIOR into - * account - */ - if (cur->sk_flags & SK_BT_NEXT) - result = -1; - else if (cur->sk_flags & SK_BT_PRIOR) - result = 1; - - Assert(result == 0 || (cur->sk_flags & SK_BT_SKIP)); - } - } - else - { - BTArrayKeyInfo *array = NULL; - - /* - * Current array element/array = scan key value is a sentinel - * value that represents the lowest (or highest) possible value - * that's still within the range of the array. - * - * Like _bt_first, we only see MINVAL keys during forwards scans - * (and similarly only see MAXVAL keys during backwards scans). - * Even if the scan's direction changes, we'll stop at some higher - * order key before we can ever reach any MAXVAL (or MINVAL) keys. - * (However, unlike _bt_first we _can_ get to keys marked either - * NEXT or PRIOR, regardless of the scan's current direction.) - */ - Assert(ScanDirectionIsForward(dir) ? - !(cur->sk_flags & SK_BT_MAXVAL) : - !(cur->sk_flags & SK_BT_MINVAL)); - - /* - * There are no valid sk_argument values in MINVAL/MAXVAL keys. - * Check if tupdatum is within the range of skip array instead. - */ - for (int arrayidx = 0; arrayidx < so->numArrayKeys; arrayidx++) - { - array = &so->arrayKeys[arrayidx]; - if (array->scan_key == ikey) - break; - } - - _bt_binsrch_skiparray_skey(false, dir, tupdatum, tupnull, - array, cur, &result); - - if (result == 0) - { - /* - * tupdatum satisfies both low_compare and high_compare, so - * it's time to advance the array keys. - * - * Note: It's possible that the skip array will "advance" from - * its MINVAL (or MAXVAL) representation to an alternative, - * logically equivalent representation of the same value: a - * representation where the = key gets a valid datum in its - * sk_argument. This is only possible when low_compare uses - * the >= strategy (or high_compare uses the <= strategy). - */ - return false; - } - } - - /* - * Does this comparison indicate that caller must _not_ advance the - * scan's arrays just yet? - */ - if ((ScanDirectionIsForward(dir) && result < 0) || - (ScanDirectionIsBackward(dir) && result > 0)) - return true; - - /* - * Does this comparison indicate that caller should now advance the - * scan's arrays? (Must be if we get here during a readpagetup call.) - */ - if (readpagetup || result != 0) - { - Assert(result != 0); - return false; - } - - /* - * Inconclusive -- need to check later scan keys, too. - * - * This must be a finaltup precheck, or a call made from an assertion. - */ - Assert(result == 0); - } - - Assert(!readpagetup); - - return false; -} - -/* - * _bt_start_prim_scan() -- start scheduled primitive index scan? - * - * Returns true if _bt_checkkeys scheduled another primitive index scan, just - * as the last one ended. Otherwise returns false, indicating that the array - * keys are now fully exhausted. - * - * Only call here during scans with one or more equality type array scan keys, - * after _bt_first or _bt_next return false. - */ -bool -_bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - - Assert(so->numArrayKeys); - - so->scanBehind = so->oppositeDirCheck = false; /* reset */ - - /* - * Array keys are advanced within _bt_checkkeys when the scan reaches the - * leaf level (more precisely, they're advanced when the scan reaches the - * end of each distinct set of array elements). This process avoids - * repeat access to leaf pages (across multiple primitive index scans) by - * advancing the scan's array keys when it allows the primitive index scan - * to find nearby matching tuples (or when it eliminates ranges of array - * key space that can't possibly be satisfied by any index tuple). - * - * _bt_checkkeys sets a simple flag variable to schedule another primitive - * index scan. The flag tells us what to do. - * - * We cannot rely on _bt_first always reaching _bt_checkkeys. There are - * various cases where that won't happen. For example, if the index is - * completely empty, then _bt_first won't call _bt_readpage/_bt_checkkeys. - * We also don't expect a call to _bt_checkkeys during searches for a - * non-existent value that happens to be lower/higher than any existing - * value in the index. - * - * We don't require special handling for these cases -- we don't need to - * be explicitly instructed to _not_ perform another primitive index scan. - * It's up to code under the control of _bt_first to always set the flag - * when another primitive index scan will be required. - * - * This works correctly, even with the tricky cases listed above, which - * all involve access to leaf pages "near the boundaries of the key space" - * (whether it's from a leftmost/rightmost page, or an imaginary empty - * leaf root page). If _bt_checkkeys cannot be reached by a primitive - * index scan for one set of array keys, then it also won't be reached for - * any later set ("later" in terms of the direction that we scan the index - * and advance the arrays). The array keys won't have advanced in these - * cases, but that's the correct behavior (even _bt_advance_array_keys - * won't always advance the arrays at the point they become "exhausted"). - */ - if (so->needPrimScan) - { - Assert(_bt_verify_arrays_bt_first(scan, dir)); - - /* - * Flag was set -- must call _bt_first again, which will reset the - * scan's needPrimScan flag - */ - return true; - } - - /* The top-level index scan ran out of tuples in this scan direction */ - if (scan->parallel_scan != NULL) - _bt_parallel_done(scan); - - return false; -} - -/* - * _bt_advance_array_keys() -- Advance array elements using a tuple - * - * The scan always gets a new qual as a consequence of calling here (except - * when we determine that the top-level scan has run out of matching tuples). - * All later _bt_check_compare calls also use the same new qual that was first - * used here (at least until the next call here advances the keys once again). - * It's convenient to structure _bt_check_compare rechecks of caller's tuple - * (using the new qual) as one the steps of advancing the scan's array keys, - * so this function works as a wrapper around _bt_check_compare. - * - * Like _bt_check_compare, we'll set pstate.continuescan on behalf of the - * caller, and return a boolean indicating if caller's tuple satisfies the - * scan's new qual. But unlike _bt_check_compare, we set so->needPrimScan - * when we set continuescan=false, indicating if a new primitive index scan - * has been scheduled (otherwise, the top-level scan has run out of tuples in - * the current scan direction). - * - * Caller must use _bt_tuple_before_array_skeys to determine if the current - * place in the scan is >= the current array keys _before_ calling here. - * We're responsible for ensuring that caller's tuple is <= the newly advanced - * required array keys once we return. We try to find an exact match, but - * failing that we'll advance the array keys to whatever set of array elements - * comes next in the key space for the current scan direction. Required array - * keys "ratchet forwards" (or backwards). They can only advance as the scan - * itself advances through the index/key space. - * - * (The rules are the same for backwards scans, except that the operators are - * flipped: just replace the precondition's >= operator with a <=, and the - * postcondition's <= operator with a >=. In other words, just swap the - * precondition with the postcondition.) - * - * We also deal with "advancing" non-required arrays here (or arrays that are - * treated as non-required for the duration of a _bt_readpage call). Callers - * whose sktrig scan key is non-required specify sktrig_required=false. These - * calls are the only exception to the general rule about always advancing the - * required array keys (the scan may not even have a required array). These - * callers should just pass a NULL pstate (since there is never any question - * of stopping the scan). No call to _bt_tuple_before_array_skeys is required - * ahead of these calls (it's already clear that any required scan keys must - * be satisfied by caller's tuple). - * - * Note that we deal with non-array required equality strategy scan keys as - * degenerate single element arrays here. Obviously, they can never really - * advance in the way that real arrays can, but they must still affect how we - * advance real array scan keys (exactly like true array equality scan keys). - * We have to keep around a 3-way ORDER proc for these (using the "=" operator - * won't do), since in general whether the tuple is < or > _any_ unsatisfied - * required equality key influences how the scan's real arrays must advance. - * - * Note also that we may sometimes need to advance the array keys when the - * existing required array keys (and other required equality keys) are already - * an exact match for every corresponding value from caller's tuple. We must - * do this for inequalities that _bt_check_compare set continuescan=false for. - * They'll advance the array keys here, just like any other scan key that - * _bt_check_compare stops on. (This can even happen _after_ we advance the - * array keys, in which case we'll advance the array keys a second time. That - * way _bt_checkkeys caller always has its required arrays advance to the - * maximum possible extent that its tuple will allow.) - */ -static bool -_bt_advance_array_keys(IndexScanDesc scan, BTReadPageState *pstate, - IndexTuple tuple, int tupnatts, TupleDesc tupdesc, - int sktrig, bool sktrig_required) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - Relation rel = scan->indexRelation; - ScanDirection dir = so->currPos.dir; - int arrayidx = 0; - bool beyond_end_advance = false, - skip_array_advanced = false, - has_required_opposite_direction_only = false, - all_required_satisfied = true, - all_satisfied = true; - - Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck); - Assert(_bt_verify_keys_with_arraykeys(scan)); - - if (sktrig_required) - { - /* - * Precondition array state assertion - */ - Assert(!_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, - tupnatts, false, 0, NULL)); - - /* - * Once we return we'll have a new set of required array keys, so - * reset state used by "look ahead" optimization - */ - pstate->rechecks = 0; - pstate->targetdistance = 0; - } - else if (sktrig < so->numberOfKeys - 1 && - !(so->keyData[so->numberOfKeys - 1].sk_flags & SK_SEARCHARRAY)) - { - int least_sign_ikey = so->numberOfKeys - 1; - bool continuescan; - - /* - * Optimization: perform a precheck of the least significant key - * during !sktrig_required calls when it isn't already our sktrig - * (provided the precheck key is not itself an array). - * - * When the precheck works out we'll avoid an expensive binary search - * of sktrig's array (plus any other arrays before least_sign_ikey). - */ - Assert(so->keyData[sktrig].sk_flags & SK_SEARCHARRAY); - if (!_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false, - false, &continuescan, - &least_sign_ikey)) - return false; - } - - for (int ikey = 0; ikey < so->numberOfKeys; ikey++) - { - ScanKey cur = so->keyData + ikey; - BTArrayKeyInfo *array = NULL; - Datum tupdatum; - bool required = false, - required_opposite_direction_only = false, - tupnull; - int32 result; - int set_elem = 0; - - if (cur->sk_strategy == BTEqualStrategyNumber) - { - /* Manage array state */ - if (cur->sk_flags & SK_SEARCHARRAY) - { - array = &so->arrayKeys[arrayidx++]; - Assert(array->scan_key == ikey); - } - } - else - { - /* - * Are any inequalities required in the opposite direction only - * present here? - */ - if (((ScanDirectionIsForward(dir) && - (cur->sk_flags & (SK_BT_REQBKWD))) || - (ScanDirectionIsBackward(dir) && - (cur->sk_flags & (SK_BT_REQFWD))))) - has_required_opposite_direction_only = - required_opposite_direction_only = true; - } - - /* Optimization: skip over known-satisfied scan keys */ - if (ikey < sktrig) - continue; - - if (cur->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) - { - required = true; - - if (cur->sk_attno > tupnatts) - { - /* Set this just like _bt_tuple_before_array_skeys */ - Assert(sktrig < ikey); - so->scanBehind = true; - } - } - - /* - * Handle a required non-array scan key that the initial call to - * _bt_check_compare indicated triggered array advancement, if any. - * - * The non-array scan key's strategy will be <, <=, or = during a - * forwards scan (or any one of =, >=, or > during a backwards scan). - * It follows that the corresponding tuple attribute's value must now - * be either > or >= the scan key value (for backwards scans it must - * be either < or <= that value). - * - * If this is a required equality strategy scan key, this is just an - * optimization; _bt_tuple_before_array_skeys already confirmed that - * this scan key places us ahead of caller's tuple. There's no need - * to repeat that work now. (The same underlying principle also gets - * applied by the cur_elem_trig optimization used to speed up searches - * for the next array element.) - * - * If this is a required inequality strategy scan key, we _must_ rely - * on _bt_check_compare like this; we aren't capable of directly - * evaluating required inequality strategy scan keys here, on our own. - */ - if (ikey == sktrig && !array) - { - Assert(sktrig_required && required && all_required_satisfied); - - /* Use "beyond end" advancement. See below for an explanation. */ - beyond_end_advance = true; - all_satisfied = all_required_satisfied = false; - - continue; - } - - /* - * Nothing more for us to do with an inequality strategy scan key that - * wasn't the one that _bt_check_compare stopped on, though. - * - * Note: if our later call to _bt_check_compare (to recheck caller's - * tuple) sets continuescan=false due to finding this same inequality - * unsatisfied (possible when it's required in the scan direction), - * we'll deal with it via a recursive "second pass" call. - */ - else if (cur->sk_strategy != BTEqualStrategyNumber) - continue; - - /* - * Nothing for us to do with an equality strategy scan key that isn't - * marked required, either -- unless it's a non-required array - */ - else if (!required && !array) - continue; - - /* - * Here we perform steps for all array scan keys after a required - * array scan key whose binary search triggered "beyond end of array - * element" array advancement due to encountering a tuple attribute - * value > the closest matching array key (or < for backwards scans). - */ - if (beyond_end_advance) - { - if (array) - _bt_array_set_low_or_high(rel, cur, array, - ScanDirectionIsBackward(dir)); - - continue; - } - - /* - * Here we perform steps for all array scan keys after a required - * array scan key whose tuple attribute was < the closest matching - * array key when we dealt with it (or > for backwards scans). - * - * This earlier required array key already puts us ahead of caller's - * tuple in the key space (for the current scan direction). We must - * make sure that subsequent lower-order array keys do not put us too - * far ahead (ahead of tuples that have yet to be seen by our caller). - * For example, when a tuple "(a, b) = (42, 5)" advances the array - * keys on "a" from 40 to 45, we must also set "b" to whatever the - * first array element for "b" is. It would be wrong to allow "b" to - * be set based on the tuple value. - * - * Perform the same steps with truncated high key attributes. You can - * think of this as a "binary search" for the element closest to the - * value -inf. Again, the arrays must never get ahead of the scan. - */ - if (!all_required_satisfied || cur->sk_attno > tupnatts) - { - if (array) - _bt_array_set_low_or_high(rel, cur, array, - ScanDirectionIsForward(dir)); - - continue; - } - - /* - * Search in scankey's array for the corresponding tuple attribute - * value from caller's tuple - */ - tupdatum = index_getattr(tuple, cur->sk_attno, tupdesc, &tupnull); - - if (array) - { - bool cur_elem_trig = (sktrig_required && ikey == sktrig); - - /* - * "Binary search" by checking if tupdatum/tupnull are within the - * range of the skip array - */ - if (array->num_elems == -1) - _bt_binsrch_skiparray_skey(cur_elem_trig, dir, - tupdatum, tupnull, array, cur, - &result); - - /* - * Binary search for the closest match from the SAOP array - */ - else - set_elem = _bt_binsrch_array_skey(&so->orderProcs[ikey], - cur_elem_trig, dir, - tupdatum, tupnull, array, cur, - &result); - } - else - { - Assert(required); - - /* - * This is a required non-array equality strategy scan key, which - * we'll treat as a degenerate single element array. - * - * This scan key's imaginary "array" can't really advance, but it - * can still roll over like any other array. (Actually, this is - * no different to real single value arrays, which never advance - * without rolling over -- they can never truly advance, either.) - */ - result = _bt_compare_array_skey(&so->orderProcs[ikey], - tupdatum, tupnull, - cur->sk_argument, cur); - } - - /* - * Consider "beyond end of array element" array advancement. - * - * When the tuple attribute value is > the closest matching array key - * (or < in the backwards scan case), we need to ratchet this array - * forward (backward) by one increment, so that caller's tuple ends up - * being < final array value instead (or > final array value instead). - * This process has to work for all of the arrays, not just this one: - * it must "carry" to higher-order arrays when the set_elem that we - * just found happens to be the final one for the scan's direction. - * Incrementing (decrementing) set_elem itself isn't good enough. - * - * Our approach is to provisionally use set_elem as if it was an exact - * match now, then set each later/less significant array to whatever - * its final element is. Once outside the loop we'll then "increment - * this array's set_elem" by calling _bt_advance_array_keys_increment. - * That way the process rolls over to higher order arrays as needed. - * - * Under this scheme any required arrays only ever ratchet forwards - * (or backwards), and always do so to the maximum possible extent - * that we can know will be safe without seeing the scan's next tuple. - * We don't need any special handling for required scan keys that lack - * a real array to advance, nor for redundant scan keys that couldn't - * be eliminated by _bt_preprocess_keys. It won't matter if some of - * our "true" array scan keys (or even all of them) are non-required. - */ - if (sktrig_required && required && - ((ScanDirectionIsForward(dir) && result > 0) || - (ScanDirectionIsBackward(dir) && result < 0))) - beyond_end_advance = true; - - Assert(all_required_satisfied && all_satisfied); - if (result != 0) - { - /* - * Track whether caller's tuple satisfies our new post-advancement - * qual, for required scan keys, as well as for the entire set of - * interesting scan keys (all required scan keys plus non-required - * array scan keys are considered interesting.) - */ - all_satisfied = false; - if (sktrig_required && required) - all_required_satisfied = false; - else - { - /* - * There's no need to advance the arrays using the best - * available match for a non-required array. Give up now. - * (Though note that sktrig_required calls still have to do - * all the usual post-advancement steps, including the recheck - * call to _bt_check_compare.) - */ - break; - } - } - - /* Advance array keys, even when we don't have an exact match */ - if (array) - { - if (array->num_elems == -1) - { - /* Skip array's new element is tupdatum (or MINVAL/MAXVAL) */ - _bt_skiparray_set_element(rel, cur, array, result, - tupdatum, tupnull); - skip_array_advanced = true; - } - else if (array->cur_elem != set_elem) - { - /* SAOP array's new element is set_elem datum */ - array->cur_elem = set_elem; - cur->sk_argument = array->elem_values[set_elem]; - } - } - } - - /* - * Advance the array keys incrementally whenever "beyond end of array - * element" array advancement happens, so that advancement will carry to - * higher-order arrays (might exhaust all the scan's arrays instead, which - * ends the top-level scan). - */ - if (beyond_end_advance && - !_bt_advance_array_keys_increment(scan, dir, &skip_array_advanced)) - goto end_toplevel_scan; - - Assert(_bt_verify_keys_with_arraykeys(scan)); - - /* - * Maintain a page-level count of the number of times the scan's array - * keys advanced in a way that affected at least one skip array - */ - if (sktrig_required && skip_array_advanced) - pstate->nskipadvances++; - - /* - * Does tuple now satisfy our new qual? Recheck with _bt_check_compare. - * - * Calls triggered by an unsatisfied required scan key, whose tuple now - * satisfies all required scan keys, but not all nonrequired array keys, - * will still require a recheck call to _bt_check_compare. They'll still - * need its "second pass" handling of required inequality scan keys. - * (Might have missed a still-unsatisfied required inequality scan key - * that caller didn't detect as the sktrig scan key during its initial - * _bt_check_compare call that used the old/original qual.) - * - * Calls triggered by an unsatisfied nonrequired array scan key never need - * "second pass" handling of required inequalities (nor any other handling - * of any required scan key). All that matters is whether caller's tuple - * satisfies the new qual, so it's safe to just skip the _bt_check_compare - * recheck when we've already determined that it can only return 'false'. - * - * Note: In practice most scan keys are marked required by preprocessing, - * if necessary by generating a preceding skip array. We nevertheless - * often handle array keys marked required as if they were nonrequired. - * This behavior is requested by our _bt_check_compare caller, though only - * when it is passed "forcenonrequired=true" by _bt_checkkeys. - */ - if ((sktrig_required && all_required_satisfied) || - (!sktrig_required && all_satisfied)) - { - int nsktrig = sktrig + 1; - bool continuescan; - - Assert(all_required_satisfied); - - /* Recheck _bt_check_compare on behalf of caller */ - if (_bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false, - !sktrig_required, &continuescan, - &nsktrig) && - !so->scanBehind) - { - /* This tuple satisfies the new qual */ - Assert(all_satisfied && continuescan); - - if (pstate) - pstate->continuescan = true; - - return true; - } - - /* - * Consider "second pass" handling of required inequalities. - * - * It's possible that our _bt_check_compare call indicated that the - * scan should end due to some unsatisfied inequality that wasn't - * initially recognized as such by us. Handle this by calling - * ourselves recursively, this time indicating that the trigger is the - * inequality that we missed first time around (and using a set of - * required array/equality keys that are now exact matches for tuple). - * - * We make a strong, general guarantee that every _bt_checkkeys call - * here will advance the array keys to the maximum possible extent - * that we can know to be safe based on caller's tuple alone. If we - * didn't perform this step, then that guarantee wouldn't quite hold. - */ - if (unlikely(!continuescan)) - { - bool satisfied PG_USED_FOR_ASSERTS_ONLY; - - Assert(sktrig_required); - Assert(so->keyData[nsktrig].sk_strategy != BTEqualStrategyNumber); - - /* - * The tuple must use "beyond end" advancement during the - * recursive call, so we cannot possibly end up back here when - * recursing. We'll consume a small, fixed amount of stack space. - */ - Assert(!beyond_end_advance); - - /* Advance the array keys a second time using same tuple */ - satisfied = _bt_advance_array_keys(scan, pstate, tuple, tupnatts, - tupdesc, nsktrig, true); - - /* This tuple doesn't satisfy the inequality */ - Assert(!satisfied); - return false; - } - - /* - * Some non-required scan key (from new qual) still not satisfied. - * - * All scan keys required in the current scan direction must still be - * satisfied, though, so we can trust all_required_satisfied below. - */ - } - - /* - * When we were called just to deal with "advancing" non-required arrays, - * this is as far as we can go (cannot stop the scan for these callers) - */ - if (!sktrig_required) - { - /* Caller's tuple doesn't match any qual */ - return false; - } - - /* - * Postcondition array state assertion (for still-unsatisfied tuples). - * - * By here we have established that the scan's required arrays (scan must - * have at least one required array) advanced, without becoming exhausted. - * - * Caller's tuple is now < the newly advanced array keys (or > when this - * is a backwards scan), except in the case where we only got this far due - * to an unsatisfied non-required scan key. Verify that with an assert. - * - * Note: we don't just quit at this point when all required scan keys were - * found to be satisfied because we need to consider edge-cases involving - * scan keys required in the opposite direction only; those aren't tracked - * by all_required_satisfied. - */ - Assert(_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, - false, 0, NULL) == - !all_required_satisfied); - - /* - * We generally permit primitive index scans to continue onto the next - * sibling page when the page's finaltup satisfies all required scan keys - * at the point where we're between pages. - * - * If caller's tuple is also the page's finaltup, and we see that required - * scan keys still aren't satisfied, start a new primitive index scan. - */ - if (!all_required_satisfied && pstate->finaltup == tuple) - goto new_prim_scan; - - /* - * Proactively check finaltup (don't wait until finaltup is reached by the - * scan) when it might well turn out to not be satisfied later on. - * - * Note: if so->scanBehind hasn't already been set for finaltup by us, - * it'll be set during this call to _bt_tuple_before_array_skeys. Either - * way, it'll be set correctly (for the whole page) after this point. - */ - if (!all_required_satisfied && pstate->finaltup && - _bt_tuple_before_array_skeys(scan, dir, pstate->finaltup, tupdesc, - BTreeTupleGetNAtts(pstate->finaltup, rel), - false, 0, &so->scanBehind)) - goto new_prim_scan; - - /* - * When we encounter a truncated finaltup high key attribute, we're - * optimistic about the chances of its corresponding required scan key - * being satisfied when we go on to recheck it against tuples from this - * page's right sibling leaf page. We consider truncated attributes to be - * satisfied by required scan keys, which allows the primitive index scan - * to continue to the next leaf page. We must set so->scanBehind to true - * to remember that the last page's finaltup had "satisfied" required scan - * keys for one or more truncated attribute values (scan keys required in - * _either_ scan direction). - * - * There is a chance that _bt_readpage (which checks so->scanBehind) will - * find that even the sibling leaf page's finaltup is < the new array - * keys. When that happens, our optimistic policy will have incurred a - * single extra leaf page access that could have been avoided. - * - * A pessimistic policy would give backward scans a gratuitous advantage - * over forward scans. We'd punish forward scans for applying more - * accurate information from the high key, rather than just using the - * final non-pivot tuple as finaltup, in the style of backward scans. - * Being pessimistic would also give some scans with non-required arrays a - * perverse advantage over similar scans that use required arrays instead. - * - * This is similar to our scan-level heuristics, below. They also set - * scanBehind to speculatively continue the primscan onto the next page. - */ - if (so->scanBehind) - { - /* Truncated high key -- _bt_scanbehind_checkkeys recheck scheduled */ - } - - /* - * Handle inequalities marked required in the opposite scan direction. - * They can also signal that we should start a new primitive index scan. - * - * It's possible that the scan is now positioned where "matching" tuples - * begin, and that caller's tuple satisfies all scan keys required in the - * current scan direction. But if caller's tuple still doesn't satisfy - * other scan keys that are required in the opposite scan direction only - * (e.g., a required >= strategy scan key when scan direction is forward), - * it's still possible that there are many leaf pages before the page that - * _bt_first could skip straight to. Groveling through all those pages - * will always give correct answers, but it can be very inefficient. We - * must avoid needlessly scanning extra pages. - * - * Separately, it's possible that _bt_check_compare set continuescan=false - * for a scan key that's required in the opposite direction only. This is - * a special case, that happens only when _bt_check_compare sees that the - * inequality encountered a NULL value. This signals the end of non-NULL - * values in the current scan direction, which is reason enough to end the - * (primitive) scan. If this happens at the start of a large group of - * NULL values, then we shouldn't expect to be called again until after - * the scan has already read indefinitely-many leaf pages full of tuples - * with NULL suffix values. (_bt_first is expected to skip over the group - * of NULLs by applying a similar "deduce NOT NULL" rule of its own, which - * involves consing up an explicit SK_SEARCHNOTNULL key.) - * - * Apply a test against finaltup to detect and recover from the problem: - * if even finaltup doesn't satisfy such an inequality, we just skip by - * starting a new primitive index scan. When we skip, we know for sure - * that all of the tuples on the current page following caller's tuple are - * also before the _bt_first-wise start of tuples for our new qual. That - * at least suggests many more skippable pages beyond the current page. - * (when so->scanBehind and so->oppositeDirCheck are set, this'll happen - * when we test the next page's finaltup/high key instead.) - */ - else if (has_required_opposite_direction_only && pstate->finaltup && - unlikely(!_bt_oppodir_checkkeys(scan, dir, pstate->finaltup))) - { - /* - * Make sure that any SAOP arrays that were not marked required by - * preprocessing are reset to their first element for this direction - */ - _bt_rewind_nonrequired_arrays(scan, dir); - goto new_prim_scan; - } - -continue_scan: - - /* - * Stick with the ongoing primitive index scan for now. - * - * It's possible that later tuples will also turn out to have values that - * are still < the now-current array keys (or > the current array keys). - * Our caller will handle this by performing what amounts to a linear - * search of the page, implemented by calling _bt_check_compare and then - * _bt_tuple_before_array_skeys for each tuple. - * - * This approach has various advantages over a binary search of the page. - * Repeated binary searches of the page (one binary search for every array - * advancement) won't outperform a continuous linear search. While there - * are workloads that a naive linear search won't handle well, our caller - * has a "look ahead" fallback mechanism to deal with that problem. - */ - pstate->continuescan = true; /* Override _bt_check_compare */ - so->needPrimScan = false; /* _bt_readpage has more tuples to check */ - - if (so->scanBehind) - { - /* - * Remember if recheck needs to call _bt_oppodir_checkkeys for next - * page's finaltup (see above comments about "Handle inequalities - * marked required in the opposite scan direction" for why). - */ - so->oppositeDirCheck = has_required_opposite_direction_only; - - _bt_rewind_nonrequired_arrays(scan, dir); - - /* - * skip by setting "look ahead" mechanism's offnum for forwards scans - * (backwards scans check scanBehind flag directly instead) - */ - if (ScanDirectionIsForward(dir)) - pstate->skip = pstate->maxoff + 1; - } - - /* Caller's tuple doesn't match the new qual */ - return false; - -new_prim_scan: - - Assert(pstate->finaltup); /* not on rightmost/leftmost page */ - - /* - * Looks like another primitive index scan is required. But consider - * continuing the current primscan based on scan-level heuristics. - * - * Continue the ongoing primitive scan (and schedule a recheck for when - * the scan arrives on the next sibling leaf page) when it has already - * read at least one leaf page before the one we're reading now. This - * makes primscan scheduling more efficient when scanning subsets of an - * index with many distinct attribute values matching many array elements. - * It encourages fewer, larger primitive scans where that makes sense. - * This will in turn encourage _bt_readpage to apply the pstate.startikey - * optimization more often. - * - * Also continue the ongoing primitive index scan when it is still on the - * first page if there have been more than NSKIPADVANCES_THRESHOLD calls - * here that each advanced at least one of the scan's skip arrays - * (deliberately ignore advancements that only affected SAOP arrays here). - * A page that cycles through this many skip array elements is quite - * likely to neighbor similar pages, that we'll also need to read. - * - * Note: These heuristics aren't as aggressive as you might think. We're - * conservative about allowing a primitive scan to step from the first - * leaf page it reads to the page's sibling page (we only allow it on - * first pages whose finaltup strongly suggests that it'll work out, as - * well as first pages that have a large number of skip array advances). - * Clearing this first page finaltup hurdle is a strong signal in itself. - * - * Note: The NSKIPADVANCES_THRESHOLD heuristic exists only to avoid - * pathological cases. Specifically, cases where a skip scan should just - * behave like a traditional full index scan, but ends up "skipping" again - * and again, descending to the prior leaf page's direct sibling leaf page - * each time. This misbehavior would otherwise be possible during scans - * that never quite manage to "clear the first page finaltup hurdle". - */ - if (!pstate->firstpage || pstate->nskipadvances > NSKIPADVANCES_THRESHOLD) - { - /* Schedule a recheck once on the next (or previous) page */ - so->scanBehind = true; - - /* Continue the current primitive scan after all */ - goto continue_scan; - } - - /* - * End this primitive index scan, but schedule another. - * - * Note: We make a soft assumption that the current scan direction will - * also be used within _bt_next, when it is asked to step off this page. - * It is up to _bt_next to cancel this scheduled primitive index scan - * whenever it steps to a page in the direction opposite currPos.dir. - */ - pstate->continuescan = false; /* Tell _bt_readpage we're done... */ - so->needPrimScan = true; /* ...but call _bt_first again */ - - if (scan->parallel_scan) - _bt_parallel_primscan_schedule(scan, so->currPos.currPage); - - /* Caller's tuple doesn't match the new qual */ - return false; - -end_toplevel_scan: - - /* - * End the current primitive index scan, but don't schedule another. - * - * This ends the entire top-level scan in the current scan direction. - * - * Note: The scan's arrays (including any non-required arrays) are now in - * their final positions for the current scan direction. If the scan - * direction happens to change, then the arrays will already be in their - * first positions for what will then be the current scan direction. - */ - pstate->continuescan = false; /* Tell _bt_readpage we're done... */ - so->needPrimScan = false; /* ...and don't call _bt_first again */ - - /* Caller's tuple doesn't match any qual */ - return false; -} - -#ifdef USE_ASSERT_CHECKING -/* - * Verify that the scan's qual state matches what we expect at the point that - * _bt_start_prim_scan is about to start a just-scheduled new primitive scan. - * - * We enforce a rule against non-required array scan keys: they must start out - * with whatever element is the first for the scan's current scan direction. - * See _bt_rewind_nonrequired_arrays comments for an explanation. - */ -static bool -_bt_verify_arrays_bt_first(IndexScanDesc scan, ScanDirection dir) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - int arrayidx = 0; - - for (int ikey = 0; ikey < so->numberOfKeys; ikey++) - { - ScanKey cur = so->keyData + ikey; - BTArrayKeyInfo *array = NULL; - int first_elem_dir; - - if (!(cur->sk_flags & SK_SEARCHARRAY) || - cur->sk_strategy != BTEqualStrategyNumber) - continue; - - array = &so->arrayKeys[arrayidx++]; - - if (((cur->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) || - ((cur->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir))) - continue; - - if (ScanDirectionIsForward(dir)) - first_elem_dir = 0; - else - first_elem_dir = array->num_elems - 1; - - if (array->cur_elem != first_elem_dir) - return false; - } - - return _bt_verify_keys_with_arraykeys(scan); -} - -/* - * Verify that the scan's "so->keyData[]" scan keys are in agreement with - * its array key state - */ -static bool -_bt_verify_keys_with_arraykeys(IndexScanDesc scan) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - int last_sk_attno = InvalidAttrNumber, - arrayidx = 0; - - if (!so->qual_ok) - return false; - - for (int ikey = 0; ikey < so->numberOfKeys; ikey++) - { - ScanKey cur = so->keyData + ikey; - BTArrayKeyInfo *array; - - if (cur->sk_strategy != BTEqualStrategyNumber || - !(cur->sk_flags & SK_SEARCHARRAY)) - continue; - - array = &so->arrayKeys[arrayidx++]; - if (array->scan_key != ikey) - return false; - - if (array->num_elems == 0 || array->num_elems < -1) - return false; - - if (array->num_elems != -1 && - cur->sk_argument != array->elem_values[array->cur_elem]) - return false; - if (last_sk_attno > cur->sk_attno) - return false; - last_sk_attno = cur->sk_attno; - } - - if (arrayidx != so->numArrayKeys) - return false; - - return true; -} -#endif - -/* - * Test whether an indextuple satisfies all the scankey conditions. - * - * Return true if so, false if not. If the tuple fails to pass the qual, - * we also determine whether there's any need to continue the scan beyond - * this tuple, and set pstate.continuescan accordingly. See comments for - * _bt_preprocess_keys() about how this is done. - * - * Forward scan callers can pass a high key tuple in the hopes of having - * us set *continuescan to false, and avoiding an unnecessary visit to - * the page to the right. - * - * Advances the scan's array keys when necessary for arrayKeys=true callers. - * Scans without any array keys must always pass arrayKeys=false. - * - * Also stops and starts primitive index scans for arrayKeys=true callers. - * Scans with array keys are required to set up page state that helps us with - * this. The page's finaltup tuple (the page high key for a forward scan, or - * the page's first non-pivot tuple for a backward scan) must be set in - * pstate.finaltup ahead of the first call here for the page. Set this to - * NULL for rightmost page (or the leftmost page for backwards scans). - * - * scan: index scan descriptor (containing a search-type scankey) - * pstate: page level input and output parameters - * arrayKeys: should we advance the scan's array keys if necessary? - * tuple: index tuple to test - * tupnatts: number of attributes in tupnatts (high key may be truncated) - */ -bool -_bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys, - IndexTuple tuple, int tupnatts) -{ - TupleDesc tupdesc = RelationGetDescr(scan->indexRelation); - BTScanOpaque so = (BTScanOpaque) scan->opaque; - ScanDirection dir = so->currPos.dir; - int ikey = pstate->startikey; - bool res; - - Assert(BTreeTupleGetNAtts(tuple, scan->indexRelation) == tupnatts); - Assert(!so->needPrimScan && !so->scanBehind && !so->oppositeDirCheck); - Assert(arrayKeys || so->numArrayKeys == 0); - - res = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, arrayKeys, - pstate->forcenonrequired, &pstate->continuescan, - &ikey); - - /* - * If _bt_check_compare relied on the pstate.startikey optimization, call - * again (in assert-enabled builds) to verify it didn't affect our answer. - * - * Note: we can't do this when !pstate.forcenonrequired, since any arrays - * before pstate.startikey won't have advanced on this page at all. - */ - Assert(!pstate->forcenonrequired || arrayKeys); -#ifdef USE_ASSERT_CHECKING - if (pstate->startikey > 0 && !pstate->forcenonrequired) - { - bool dres, - dcontinuescan; - int dikey = 0; - - /* Pass arrayKeys=false to avoid array side-effects */ - dres = _bt_check_compare(scan, dir, tuple, tupnatts, tupdesc, false, - pstate->forcenonrequired, &dcontinuescan, - &dikey); - Assert(res == dres); - Assert(pstate->continuescan == dcontinuescan); - - /* - * Should also get the same ikey result. We need a slightly weaker - * assertion during arrayKeys calls, since they might be using an - * array that couldn't be marked required during preprocessing. - */ - Assert(arrayKeys || ikey == dikey); - Assert(ikey <= dikey); - } -#endif - - /* - * Only one _bt_check_compare call is required in the common case where - * there are no equality strategy array scan keys. Otherwise we can only - * accept _bt_check_compare's answer unreservedly when it didn't set - * pstate.continuescan=false. - */ - if (!arrayKeys || pstate->continuescan) - return res; - - /* - * _bt_check_compare call set continuescan=false in the presence of - * equality type array keys. This could mean that the tuple is just past - * the end of matches for the current array keys. - * - * It's also possible that the scan is still _before_ the _start_ of - * tuples matching the current set of array keys. Check for that first. - */ - Assert(!pstate->forcenonrequired); - if (_bt_tuple_before_array_skeys(scan, dir, tuple, tupdesc, tupnatts, true, - ikey, NULL)) - { - /* Override _bt_check_compare, continue primitive scan */ - pstate->continuescan = true; - - /* - * We will end up here repeatedly given a group of tuples > the - * previous array keys and < the now-current keys (for a backwards - * scan it's just the same, though the operators swap positions). - * - * We must avoid allowing this linear search process to scan very many - * tuples from well before the start of tuples matching the current - * array keys (or from well before the point where we'll once again - * have to advance the scan's array keys). - * - * We keep the overhead under control by speculatively "looking ahead" - * to later still-unscanned items from this same leaf page. We'll - * only attempt this once the number of tuples that the linear search - * process has examined starts to get out of hand. - */ - pstate->rechecks++; - if (pstate->rechecks >= LOOK_AHEAD_REQUIRED_RECHECKS) - { - /* See if we should skip ahead within the current leaf page */ - _bt_checkkeys_look_ahead(scan, pstate, tupnatts, tupdesc); - - /* - * Might have set pstate.skip to a later page offset. When that - * happens then _bt_readpage caller will inexpensively skip ahead - * to a later tuple from the same page (the one just after the - * tuple we successfully "looked ahead" to). - */ - } - - /* This indextuple doesn't match the current qual, in any case */ - return false; - } - - /* - * Caller's tuple is >= the current set of array keys and other equality - * constraint scan keys (or <= if this is a backwards scan). It's now - * clear that we _must_ advance any required array keys in lockstep with - * the scan. - */ - return _bt_advance_array_keys(scan, pstate, tuple, tupnatts, tupdesc, - ikey, true); -} - -/* - * Test whether caller's finaltup tuple is still before the start of matches - * for the current array keys. - * - * Called at the start of reading a page during a scan with array keys, though - * only when the so->scanBehind flag was set on the scan's prior page. - * - * Returns false if the tuple is still before the start of matches. When that - * happens, caller should cut its losses and start a new primitive index scan. - * Otherwise returns true. - */ -bool -_bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir, - IndexTuple finaltup) -{ - Relation rel = scan->indexRelation; - TupleDesc tupdesc = RelationGetDescr(rel); - BTScanOpaque so = (BTScanOpaque) scan->opaque; - int nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel); - bool scanBehind; - - Assert(so->numArrayKeys); - - if (_bt_tuple_before_array_skeys(scan, dir, finaltup, tupdesc, - nfinaltupatts, false, 0, &scanBehind)) - return false; - - /* - * If scanBehind was set, all of the untruncated attribute values from - * finaltup that correspond to an array match the array's current element, - * but there are other keys associated with truncated suffix attributes. - * Array advancement must have incremented the scan's arrays on the - * previous page, resulting in a set of array keys that happen to be an - * exact match for the current page high key's untruncated prefix values. - * - * This page definitely doesn't contain tuples that the scan will need to - * return. The next page may or may not contain relevant tuples. Handle - * this by cutting our losses and starting a new primscan. - */ - if (scanBehind) - return false; - - if (!so->oppositeDirCheck) - return true; - - return _bt_oppodir_checkkeys(scan, dir, finaltup); -} - -/* - * Test whether an indextuple fails to satisfy an inequality required in the - * opposite direction only. - * - * Caller's finaltup tuple is the page high key (for forwards scans), or the - * first non-pivot tuple (for backwards scans). Called during scans with - * required array keys and required opposite-direction inequalities. - * - * Returns false if an inequality scan key required in the opposite direction - * only isn't satisfied (and any earlier required scan keys are satisfied). - * Otherwise returns true. - * - * An unsatisfied inequality required in the opposite direction only might - * well enable skipping over many leaf pages, provided another _bt_first call - * takes place. This type of unsatisfied inequality won't usually cause - * _bt_checkkeys to stop the scan to consider array advancement/starting a new - * primitive index scan. - */ -static bool -_bt_oppodir_checkkeys(IndexScanDesc scan, ScanDirection dir, - IndexTuple finaltup) -{ - Relation rel = scan->indexRelation; - TupleDesc tupdesc = RelationGetDescr(rel); - BTScanOpaque so = (BTScanOpaque) scan->opaque; - int nfinaltupatts = BTreeTupleGetNAtts(finaltup, rel); - bool continuescan; - ScanDirection flipped = -dir; - int ikey = 0; - - Assert(so->numArrayKeys); - - _bt_check_compare(scan, flipped, finaltup, nfinaltupatts, tupdesc, false, - false, &continuescan, - &ikey); - - if (!continuescan && so->keyData[ikey].sk_strategy != BTEqualStrategyNumber) - return false; - - return true; -} - -/* - * Determines an offset to the first scan key (an so->keyData[]-wise offset) - * that is _not_ guaranteed to be satisfied by every tuple from pstate.page, - * which is set in pstate.startikey for _bt_checkkeys calls for the page. - * This allows caller to save cycles on comparisons of a prefix of keys while - * reading pstate.page. - * - * Also determines if later calls to _bt_checkkeys (for pstate.page) should be - * forced to treat all required scan keys >= pstate.startikey as nonrequired - * (that is, if they're to be treated as if any SK_BT_REQFWD/SK_BT_REQBKWD - * markings that were set by preprocessing were not set at all, for the - * duration of _bt_checkkeys calls prior to the call for pstate.finaltup). - * This is indicated to caller by setting pstate.forcenonrequired. - * - * Call here at the start of reading a leaf page beyond the first one for the - * primitive index scan. We consider all non-pivot tuples, so it doesn't make - * sense to call here when only a subset of those tuples can ever be read. - * This is also a good idea on performance grounds; not calling here when on - * the first page (first for the current primitive scan) avoids wasting cycles - * during selective point queries. They typically don't stand to gain as much - * when we can set pstate.startikey, and are likely to notice the overhead of - * calling here. (Also, allowing pstate.forcenonrequired to be set on a - * primscan's first page would mislead _bt_advance_array_keys, which expects - * pstate.nskipadvances to be representative of every first page's key space.) - * - * Caller must call _bt_start_array_keys and reset startikey/forcenonrequired - * ahead of the finaltup _bt_checkkeys call when we set forcenonrequired=true. - * This will give _bt_checkkeys the opportunity to call _bt_advance_array_keys - * with sktrig_required=true, restoring the invariant that the scan's required - * arrays always track the scan's progress through the index's key space. - * Caller won't need to do this on the rightmost/leftmost page in the index - * (where pstate.finaltup isn't ever set), since forcenonrequired will never - * be set here in the first place. - */ -void -_bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - Relation rel = scan->indexRelation; - TupleDesc tupdesc = RelationGetDescr(rel); - ItemId iid; - IndexTuple firsttup, - lasttup; - int startikey = 0, - arrayidx = 0, - firstchangingattnum; - bool start_past_saop_eq = false; - - Assert(!so->scanBehind); - Assert(pstate->minoff < pstate->maxoff); - Assert(!pstate->firstpage); - Assert(pstate->startikey == 0); - Assert(!so->numArrayKeys || pstate->finaltup || - P_RIGHTMOST(BTPageGetOpaque(pstate->page)) || - P_LEFTMOST(BTPageGetOpaque(pstate->page))); - - if (so->numberOfKeys == 0) - return; - - /* minoff is an offset to the lowest non-pivot tuple on the page */ - iid = PageGetItemId(pstate->page, pstate->minoff); - firsttup = (IndexTuple) PageGetItem(pstate->page, iid); - - /* maxoff is an offset to the highest non-pivot tuple on the page */ - iid = PageGetItemId(pstate->page, pstate->maxoff); - lasttup = (IndexTuple) PageGetItem(pstate->page, iid); - - /* Determine the first attribute whose values change on caller's page */ - firstchangingattnum = _bt_keep_natts_fast(rel, firsttup, lasttup); - - for (; startikey < so->numberOfKeys; startikey++) - { - ScanKey key = so->keyData + startikey; - BTArrayKeyInfo *array; - Datum firstdatum, - lastdatum; - bool firstnull, - lastnull; - int32 result; - - /* - * Determine if it's safe to set pstate.startikey to an offset to a - * key that comes after this key, by examining this key - */ - if (!(key->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD))) - { - /* Scan key isn't marked required (corner case) */ - Assert(!(key->sk_flags & SK_ROW_HEADER)); - break; /* unsafe */ - } - if (key->sk_flags & SK_ROW_HEADER) - { - /* - * RowCompare inequality. - * - * Only the first subkey from a RowCompare can ever be marked - * required (that happens when the row header is marked required). - * There is no simple, general way for us to transitively deduce - * whether or not every tuple on the page satisfies a RowCompare - * key based only on firsttup and lasttup -- so we just give up. - */ - if (!start_past_saop_eq && !so->skipScan) - break; /* unsafe to go further */ - - /* - * We have to be even more careful with RowCompares that come - * after an array: we assume it's unsafe to even bypass the array. - * Calling _bt_start_array_keys to recover the scan's arrays - * following use of forcenonrequired mode isn't compatible with - * _bt_check_rowcompare's continuescan=false behavior with NULL - * row compare members. _bt_advance_array_keys must not make a - * decision on the basis of a key not being satisfied in the - * opposite-to-scan direction until the scan reaches a leaf page - * where the same key begins to be satisfied in scan direction. - * The _bt_first !used_all_subkeys behavior makes this limitation - * hard to work around some other way. - */ - return; /* completely unsafe to set pstate.startikey */ - } - if (key->sk_strategy != BTEqualStrategyNumber) - { - /* - * Scalar inequality key. - * - * It's definitely safe for _bt_checkkeys to avoid assessing this - * inequality when the page's first and last non-pivot tuples both - * satisfy the inequality (since the same must also be true of all - * the tuples in between these two). - * - * Unlike the "=" case, it doesn't matter if this attribute has - * more than one distinct value (though it _is_ necessary for any - * and all _prior_ attributes to contain no more than one distinct - * value amongst all of the tuples from pstate.page). - */ - if (key->sk_attno > firstchangingattnum) /* >, not >= */ - break; /* unsafe, preceding attr has multiple - * distinct values */ - - firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull); - lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull); - - if (key->sk_flags & SK_ISNULL) - { - /* IS NOT NULL key */ - Assert(key->sk_flags & SK_SEARCHNOTNULL); - - if (firstnull || lastnull) - break; /* unsafe */ - - /* Safe, IS NOT NULL key satisfied by every tuple */ - continue; - } - - /* Test firsttup */ - if (firstnull || - !DatumGetBool(FunctionCall2Coll(&key->sk_func, - key->sk_collation, firstdatum, - key->sk_argument))) - break; /* unsafe */ - - /* Test lasttup */ - if (lastnull || - !DatumGetBool(FunctionCall2Coll(&key->sk_func, - key->sk_collation, lastdatum, - key->sk_argument))) - break; /* unsafe */ - - /* Safe, scalar inequality satisfied by every tuple */ - continue; - } - - /* Some = key (could be a scalar = key, could be an array = key) */ - Assert(key->sk_strategy == BTEqualStrategyNumber); - - if (!(key->sk_flags & SK_SEARCHARRAY)) - { - /* - * Scalar = key (possibly an IS NULL key). - * - * It is unsafe to set pstate.startikey to an ikey beyond this - * key, unless the = key is satisfied by every possible tuple on - * the page (possible only when attribute has just one distinct - * value among all tuples on the page). - */ - if (key->sk_attno >= firstchangingattnum) - break; /* unsafe, multiple distinct attr values */ - - firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, - &firstnull); - if (key->sk_flags & SK_ISNULL) - { - /* IS NULL key */ - Assert(key->sk_flags & SK_SEARCHNULL); - - if (!firstnull) - break; /* unsafe */ - - /* Safe, IS NULL key satisfied by every tuple */ - continue; - } - if (firstnull || - !DatumGetBool(FunctionCall2Coll(&key->sk_func, - key->sk_collation, firstdatum, - key->sk_argument))) - break; /* unsafe */ - - /* Safe, scalar = key satisfied by every tuple */ - continue; - } - - /* = array key (could be a SAOP array, could be a skip array) */ - array = &so->arrayKeys[arrayidx++]; - Assert(array->scan_key == startikey); - if (array->num_elems != -1) - { - /* - * SAOP array = key. - * - * Handle this like we handle scalar = keys (though binary search - * for a matching element, to avoid relying on key's sk_argument). - */ - if (key->sk_attno >= firstchangingattnum) - break; /* unsafe, multiple distinct attr values */ - - firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, - &firstnull); - _bt_binsrch_array_skey(&so->orderProcs[startikey], - false, NoMovementScanDirection, - firstdatum, firstnull, array, key, - &result); - if (result != 0) - break; /* unsafe */ - - /* Safe, SAOP = key satisfied by every tuple */ - start_past_saop_eq = true; - continue; - } - - /* - * Skip array = key - */ - Assert(key->sk_flags & SK_BT_SKIP); - if (array->null_elem) - { - /* - * Non-range skip array = key. - * - * Safe, non-range skip array "satisfied" by every tuple on page - * (safe even when "key->sk_attno > firstchangingattnum"). - */ - continue; - } - - /* - * Range skip array = key. - * - * Handle this like we handle scalar inequality keys (but avoid using - * key's sk_argument directly, as in the SAOP array case). - */ - if (key->sk_attno > firstchangingattnum) /* >, not >= */ - break; /* unsafe, preceding attr has multiple - * distinct values */ - - firstdatum = index_getattr(firsttup, key->sk_attno, tupdesc, &firstnull); - lastdatum = index_getattr(lasttup, key->sk_attno, tupdesc, &lastnull); - - /* Test firsttup */ - _bt_binsrch_skiparray_skey(false, ForwardScanDirection, - firstdatum, firstnull, array, key, - &result); - if (result != 0) - break; /* unsafe */ - - /* Test lasttup */ - _bt_binsrch_skiparray_skey(false, ForwardScanDirection, - lastdatum, lastnull, array, key, - &result); - if (result != 0) - break; /* unsafe */ - - /* Safe, range skip array satisfied by every tuple on page */ - } - - /* - * Use of forcenonrequired is typically undesirable, since it'll force - * _bt_readpage caller to read every tuple on the page -- even though, in - * general, it might well be possible to end the scan on an earlier tuple. - * However, caller must use forcenonrequired when start_past_saop_eq=true, - * since the usual required array behavior might fail to roll over to the - * SAOP array. - * - * We always prefer forcenonrequired=true during scans with skip arrays - * (except on the first page of each primitive index scan), though -- even - * when "startikey == 0". That way, _bt_advance_array_keys's low-order - * key precheck optimization can always be used (unless on the first page - * of the scan). It seems slightly preferable to check more tuples when - * that allows us to do significantly less skip array maintenance. - */ - pstate->forcenonrequired = (start_past_saop_eq || so->skipScan); - pstate->startikey = startikey; - - /* - * _bt_readpage caller is required to call _bt_checkkeys against page's - * finaltup with forcenonrequired=false whenever we initially set - * forcenonrequired=true. That way the scan's arrays will reliably track - * its progress through the index's key space. - * - * We don't expect this when _bt_readpage caller has no finaltup due to - * its page being the rightmost (or the leftmost, during backwards scans). - * When we see that _bt_readpage has no finaltup, back out of everything. - */ - Assert(!pstate->forcenonrequired || so->numArrayKeys); - if (pstate->forcenonrequired && !pstate->finaltup) - { - pstate->forcenonrequired = false; - pstate->startikey = 0; - } -} - -/* - * Test whether an indextuple satisfies current scan condition. - * - * Return true if so, false if not. If not, also sets *continuescan to false - * when it's also not possible for any later tuples to pass the current qual - * (with the scan's current set of array keys, in the current scan direction), - * in addition to setting *ikey to the so->keyData[] subscript/offset for the - * unsatisfied scan key (needed when caller must consider advancing the scan's - * array keys). - * - * This is a subroutine for _bt_checkkeys. We provisionally assume that - * reaching the end of the current set of required keys (in particular the - * current required array keys) ends the ongoing (primitive) index scan. - * Callers without array keys should just end the scan right away when they - * find that continuescan has been set to false here by us. Things are more - * complicated for callers with array keys. - * - * Callers with array keys must first consider advancing the arrays when - * continuescan has been set to false here by us. They must then consider if - * it really does make sense to end the current (primitive) index scan, in - * light of everything that is known at that point. (In general when we set - * continuescan=false for these callers it must be treated as provisional.) - * - * We deal with advancing unsatisfied non-required arrays directly, though. - * This is safe, since by definition non-required keys can't end the scan. - * This is just how we determine if non-required arrays are just unsatisfied - * by the current array key, or if they're truly unsatisfied (that is, if - * they're unsatisfied by every possible array key). - * - * Pass advancenonrequired=false to avoid all array related side effects. - * This allows _bt_advance_array_keys caller to avoid infinite recursion. - * - * Pass forcenonrequired=true to instruct us to treat all keys as nonrequired. - * This is used to make it safe to temporarily stop properly maintaining the - * scan's required arrays. _bt_checkkeys caller (_bt_readpage, actually) - * determines a prefix of keys that must satisfy every possible corresponding - * index attribute value from its page, which is passed to us via *ikey arg - * (this is the first key that might be unsatisfied by tuples on the page). - * Obviously, we won't maintain any array keys from before *ikey, so it's - * quite possible for such arrays to "fall behind" the index's keyspace. - * Caller will need to "catch up" by passing forcenonrequired=true (alongside - * an *ikey=0) once the page's finaltup is reached. - * - * Note: it's safe to pass an *ikey > 0 with forcenonrequired=false, but only - * when caller determines that it won't affect array maintenance. - */ -static bool -_bt_check_compare(IndexScanDesc scan, ScanDirection dir, - IndexTuple tuple, int tupnatts, TupleDesc tupdesc, - bool advancenonrequired, bool forcenonrequired, - bool *continuescan, int *ikey) -{ - BTScanOpaque so = (BTScanOpaque) scan->opaque; - - *continuescan = true; /* default assumption */ - - for (; *ikey < so->numberOfKeys; (*ikey)++) - { - ScanKey key = so->keyData + *ikey; - Datum datum; - bool isNull; - bool requiredSameDir = false, - requiredOppositeDirOnly = false; - - /* - * Check if the key is required in the current scan direction, in the - * opposite scan direction _only_, or in neither direction (except - * when we're forced to treat all scan keys as nonrequired) - */ - if (forcenonrequired) - { - /* treating scan's keys as non-required */ - } - else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsForward(dir)) || - ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsBackward(dir))) - requiredSameDir = true; - else if (((key->sk_flags & SK_BT_REQFWD) && ScanDirectionIsBackward(dir)) || - ((key->sk_flags & SK_BT_REQBKWD) && ScanDirectionIsForward(dir))) - requiredOppositeDirOnly = true; - - if (key->sk_attno > tupnatts) - { - /* - * This attribute is truncated (must be high key). The value for - * this attribute in the first non-pivot tuple on the page to the - * right could be any possible value. Assume that truncated - * attribute passes the qual. - */ - Assert(BTreeTupleIsPivot(tuple)); - continue; - } - - /* - * A skip array scan key uses one of several sentinel values. We just - * fall back on _bt_tuple_before_array_skeys when we see such a value. - */ - if (key->sk_flags & (SK_BT_MINVAL | SK_BT_MAXVAL | - SK_BT_NEXT | SK_BT_PRIOR)) - { - Assert(key->sk_flags & SK_SEARCHARRAY); - Assert(key->sk_flags & SK_BT_SKIP); - Assert(requiredSameDir || forcenonrequired); - - /* - * Cannot fall back on _bt_tuple_before_array_skeys when we're - * treating the scan's keys as nonrequired, though. Just handle - * this like any other non-required equality-type array key. - */ - if (forcenonrequired) - return _bt_advance_array_keys(scan, NULL, tuple, tupnatts, - tupdesc, *ikey, false); - *continuescan = false; - return false; - } - - /* row-comparison keys need special processing */ - if (key->sk_flags & SK_ROW_HEADER) - { - if (_bt_check_rowcompare(key, tuple, tupnatts, tupdesc, dir, - forcenonrequired, continuescan)) - continue; - return false; - } - - datum = index_getattr(tuple, - key->sk_attno, - tupdesc, - &isNull); - - if (key->sk_flags & SK_ISNULL) - { - /* Handle IS NULL/NOT NULL tests */ - if (key->sk_flags & SK_SEARCHNULL) - { - if (isNull) - continue; /* tuple satisfies this qual */ - } - else - { - Assert(key->sk_flags & SK_SEARCHNOTNULL); - Assert(!(key->sk_flags & SK_BT_SKIP)); - if (!isNull) - continue; /* tuple satisfies this qual */ - } - - /* - * Tuple fails this qual. If it's a required qual for the current - * scan direction, then we can conclude no further tuples will - * pass, either. - */ - if (requiredSameDir) - *continuescan = false; - else if (unlikely(key->sk_flags & SK_BT_SKIP)) - { - /* - * If we're treating scan keys as nonrequired, and encounter a - * skip array scan key whose current element is NULL, then it - * must be a non-range skip array. It must be satisfied, so - * there's no need to call _bt_advance_array_keys to check. - */ - Assert(forcenonrequired && *ikey > 0); - continue; - } - - /* - * This indextuple doesn't match the qual. - */ - return false; - } - - if (isNull) - { - /* - * Scalar scan key isn't satisfied by NULL tuple value. - * - * If we're treating scan keys as nonrequired, and key is for a - * skip array, then we must attempt to advance the array to NULL - * (if we're successful then the tuple might match the qual). - */ - if (unlikely(forcenonrequired && key->sk_flags & SK_BT_SKIP)) - return _bt_advance_array_keys(scan, NULL, tuple, tupnatts, - tupdesc, *ikey, false); - - if (key->sk_flags & SK_BT_NULLS_FIRST) - { - /* - * Since NULLs are sorted before non-NULLs, we know we have - * reached the lower limit of the range of values for this - * index attr. On a backward scan, we can stop if this qual - * is one of the "must match" subset. We can stop regardless - * of whether the qual is > or <, so long as it's required, - * because it's not possible for any future tuples to pass. On - * a forward scan, however, we must keep going, because we may - * have initially positioned to the start of the index. - * (_bt_advance_array_keys also relies on this behavior during - * forward scans.) - */ - if ((requiredSameDir || requiredOppositeDirOnly) && - ScanDirectionIsBackward(dir)) - *continuescan = false; - } - else - { - /* - * Since NULLs are sorted after non-NULLs, we know we have - * reached the upper limit of the range of values for this - * index attr. On a forward scan, we can stop if this qual is - * one of the "must match" subset. We can stop regardless of - * whether the qual is > or <, so long as it's required, - * because it's not possible for any future tuples to pass. On - * a backward scan, however, we must keep going, because we - * may have initially positioned to the end of the index. - * (_bt_advance_array_keys also relies on this behavior during - * backward scans.) - */ - if ((requiredSameDir || requiredOppositeDirOnly) && - ScanDirectionIsForward(dir)) - *continuescan = false; - } - - /* - * This indextuple doesn't match the qual. - */ - return false; - } - - if (!DatumGetBool(FunctionCall2Coll(&key->sk_func, key->sk_collation, - datum, key->sk_argument))) - { - /* - * Tuple fails this qual. If it's a required qual for the current - * scan direction, then we can conclude no further tuples will - * pass, either. - * - * Note: because we stop the scan as soon as any required equality - * qual fails, it is critical that equality quals be used for the - * initial positioning in _bt_first() when they are available. See - * comments in _bt_first(). - */ - if (requiredSameDir) - *continuescan = false; - - /* - * If this is a non-required equality-type array key, the tuple - * needs to be checked against every possible array key. Handle - * this by "advancing" the scan key's array to a matching value - * (if we're successful then the tuple might match the qual). - */ - else if (advancenonrequired && - key->sk_strategy == BTEqualStrategyNumber && - (key->sk_flags & SK_SEARCHARRAY)) - return _bt_advance_array_keys(scan, NULL, tuple, tupnatts, - tupdesc, *ikey, false); - - /* - * This indextuple doesn't match the qual. - */ - return false; - } - } +static int _bt_compare_int(const void *va, const void *vb); +static int _bt_keep_natts(Relation rel, IndexTuple lastleft, + IndexTuple firstright, BTScanInsert itup_key); - /* If we get here, the tuple passes all index quals. */ - return true; -} /* - * Test whether an indextuple satisfies a row-comparison scan condition. + * _bt_mkscankey + * Build an insertion scan key that contains comparison data from itup + * as well as comparator routines appropriate to the key datatypes. * - * Return true if so, false if not. If not, also clear *continuescan if - * it's not possible for any future tuples in the current scan direction - * to pass the qual. + * The result is intended for use with _bt_compare() and _bt_truncate(). + * Callers that don't need to fill out the insertion scankey arguments + * (e.g. they use an ad-hoc comparison routine, or only need a scankey + * for _bt_truncate()) can pass a NULL index tuple. The scankey will + * be initialized as if an "all truncated" pivot tuple was passed + * instead. * - * This is a subroutine for _bt_checkkeys/_bt_check_compare. + * Note that we may occasionally have to share lock the metapage to + * determine whether or not the keys in the index are expected to be + * unique (i.e. if this is a "heapkeyspace" index). We assume a + * heapkeyspace index when caller passes a NULL tuple, allowing index + * build callers to avoid accessing the non-existent metapage. We + * also assume that the index is _not_ allequalimage when a NULL tuple + * is passed; CREATE INDEX callers call _bt_allequalimage() to set the + * field themselves. */ -static bool -_bt_check_rowcompare(ScanKey skey, IndexTuple tuple, int tupnatts, - TupleDesc tupdesc, ScanDirection dir, - bool forcenonrequired, bool *continuescan) +BTScanInsert +_bt_mkscankey(Relation rel, IndexTuple itup) { - ScanKey subkey = (ScanKey) DatumGetPointer(skey->sk_argument); - int32 cmpresult = 0; - bool result; - - /* First subkey should be same as the header says */ - Assert(subkey->sk_attno == skey->sk_attno); - - /* Loop over columns of the row condition */ - for (;;) - { - Datum datum; - bool isNull; - - Assert(subkey->sk_flags & SK_ROW_MEMBER); - - if (subkey->sk_attno > tupnatts) - { - /* - * This attribute is truncated (must be high key). The value for - * this attribute in the first non-pivot tuple on the page to the - * right could be any possible value. Assume that truncated - * attribute passes the qual. - */ - Assert(BTreeTupleIsPivot(tuple)); - cmpresult = 0; - if (subkey->sk_flags & SK_ROW_END) - break; - subkey++; - continue; - } - - datum = index_getattr(tuple, - subkey->sk_attno, - tupdesc, - &isNull); - - if (isNull) - { - if (forcenonrequired) - { - /* treating scan's keys as non-required */ - } - else if (subkey->sk_flags & SK_BT_NULLS_FIRST) - { - /* - * Since NULLs are sorted before non-NULLs, we know we have - * reached the lower limit of the range of values for this - * index attr. On a backward scan, we can stop if this qual - * is one of the "must match" subset. We can stop regardless - * of whether the qual is > or <, so long as it's required, - * because it's not possible for any future tuples to pass. On - * a forward scan, however, we must keep going, because we may - * have initially positioned to the start of the index. - * (_bt_advance_array_keys also relies on this behavior during - * forward scans.) - */ - if ((subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) && - ScanDirectionIsBackward(dir)) - *continuescan = false; - } - else - { - /* - * Since NULLs are sorted after non-NULLs, we know we have - * reached the upper limit of the range of values for this - * index attr. On a forward scan, we can stop if this qual is - * one of the "must match" subset. We can stop regardless of - * whether the qual is > or <, so long as it's required, - * because it's not possible for any future tuples to pass. On - * a backward scan, however, we must keep going, because we - * may have initially positioned to the end of the index. - * (_bt_advance_array_keys also relies on this behavior during - * backward scans.) - */ - if ((subkey->sk_flags & (SK_BT_REQFWD | SK_BT_REQBKWD)) && - ScanDirectionIsForward(dir)) - *continuescan = false; - } - - /* - * In any case, this indextuple doesn't match the qual. - */ - return false; - } - - if (subkey->sk_flags & SK_ISNULL) - { - /* - * Unlike the simple-scankey case, this isn't a disallowed case - * (except when it's the first row element that has the NULL arg). - * But it can never match. If all the earlier row comparison - * columns are required for the scan direction, we can stop the - * scan, because there can't be another tuple that will succeed. - */ - Assert(subkey != (ScanKey) DatumGetPointer(skey->sk_argument)); - subkey--; - if (forcenonrequired) - { - /* treating scan's keys as non-required */ - } - else if ((subkey->sk_flags & SK_BT_REQFWD) && - ScanDirectionIsForward(dir)) - *continuescan = false; - else if ((subkey->sk_flags & SK_BT_REQBKWD) && - ScanDirectionIsBackward(dir)) - *continuescan = false; - return false; - } - - /* Perform the test --- three-way comparison not bool operator */ - cmpresult = DatumGetInt32(FunctionCall2Coll(&subkey->sk_func, - subkey->sk_collation, - datum, - subkey->sk_argument)); - - if (subkey->sk_flags & SK_BT_DESC) - INVERT_COMPARE_RESULT(cmpresult); + BTScanInsert key; + ScanKey skey; + TupleDesc itupdesc; + int indnkeyatts; + int16 *indoption; + int tupnatts; + int i; - /* Done comparing if unequal, else advance to next column */ - if (cmpresult != 0) - break; + itupdesc = RelationGetDescr(rel); + indnkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); + indoption = rel->rd_indoption; + tupnatts = itup ? BTreeTupleGetNAtts(itup, rel) : 0; - if (subkey->sk_flags & SK_ROW_END) - break; - subkey++; - } + Assert(tupnatts <= IndexRelationGetNumberOfAttributes(rel)); /* - * At this point cmpresult indicates the overall result of the row - * comparison, and subkey points to the deciding column (or the last - * column if the result is "="). + * We'll execute search using scan key constructed on key columns. + * Truncated attributes and non-key attributes are omitted from the final + * scan key. */ - switch (subkey->sk_strategy) + key = palloc(offsetof(BTScanInsertData, scankeys) + + sizeof(ScanKeyData) * indnkeyatts); + if (itup) + _bt_metaversion(rel, &key->heapkeyspace, &key->allequalimage); + else { - /* EQ and NE cases aren't allowed here */ - case BTLessStrategyNumber: - result = (cmpresult < 0); - break; - case BTLessEqualStrategyNumber: - result = (cmpresult <= 0); - break; - case BTGreaterEqualStrategyNumber: - result = (cmpresult >= 0); - break; - case BTGreaterStrategyNumber: - result = (cmpresult > 0); - break; - default: - elog(ERROR, "unexpected strategy number %d", subkey->sk_strategy); - result = 0; /* keep compiler quiet */ - break; + /* Utility statement callers can set these fields themselves */ + key->heapkeyspace = true; + key->allequalimage = false; } - - if (!result && !forcenonrequired) + key->anynullkeys = false; /* initial assumption */ + key->nextkey = false; /* usual case, required by btinsert */ + key->backward = false; /* usual case, required by btinsert */ + key->keysz = Min(indnkeyatts, tupnatts); + key->scantid = key->heapkeyspace && itup ? + BTreeTupleGetHeapTID(itup) : NULL; + skey = key->scankeys; + for (i = 0; i < indnkeyatts; i++) { + FmgrInfo *procinfo; + Datum arg; + bool null; + int flags; + + /* + * We can use the cached (default) support procs since no cross-type + * comparison can be needed. + */ + procinfo = index_getprocinfo(rel, i + 1, BTORDER_PROC); + /* - * Tuple fails this qual. If it's a required qual for the current - * scan direction, then we can conclude no further tuples will pass, - * either. Note we have to look at the deciding column, not - * necessarily the first or last column of the row condition. + * Key arguments built from truncated attributes (or when caller + * provides no tuple) are defensively represented as NULL values. They + * should never be used. */ - if ((subkey->sk_flags & SK_BT_REQFWD) && - ScanDirectionIsForward(dir)) - *continuescan = false; - else if ((subkey->sk_flags & SK_BT_REQBKWD) && - ScanDirectionIsBackward(dir)) - *continuescan = false; + if (i < tupnatts) + arg = index_getattr(itup, i + 1, itupdesc, &null); + else + { + arg = (Datum) 0; + null = true; + } + flags = (null ? SK_ISNULL : 0) | (indoption[i] << SK_BT_INDOPTION_SHIFT); + ScanKeyEntryInitializeWithInfo(&skey[i], + flags, + (AttrNumber) (i + 1), + InvalidStrategy, + InvalidOid, + rel->rd_indcollation[i], + procinfo, + arg); + /* Record if any key attribute is NULL (or truncated) */ + if (null) + key->anynullkeys = true; } - return result; + /* + * In NULLS NOT DISTINCT mode, we pretend that there are no null keys, so + * that full uniqueness check is done. + */ + if (rel->rd_index->indnullsnotdistinct) + key->anynullkeys = false; + + return key; } /* - * Determine if a scan with array keys should skip over uninteresting tuples. - * - * This is a subroutine for _bt_checkkeys. Called when _bt_readpage's linear - * search process (started after it finishes reading an initial group of - * matching tuples, used to locate the start of the next group of tuples - * matching the next set of required array keys) has already scanned an - * excessive number of tuples whose key space is "between arrays". - * - * When we perform look ahead successfully, we'll sets pstate.skip, which - * instructs _bt_readpage to skip ahead to that tuple next (could be past the - * end of the scan's leaf page). Pages where the optimization is effective - * will generally still need to skip several times. Each call here performs - * only a single "look ahead" comparison of a later tuple, whose distance from - * the current tuple's offset number is determined by applying heuristics. + * qsort comparison function for int arrays */ -static void -_bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, - int tupnatts, TupleDesc tupdesc) +static int +_bt_compare_int(const void *va, const void *vb) { - BTScanOpaque so = (BTScanOpaque) scan->opaque; - ScanDirection dir = so->currPos.dir; - OffsetNumber aheadoffnum; - IndexTuple ahead; - - Assert(!pstate->forcenonrequired); - - /* Avoid looking ahead when comparing the page high key */ - if (pstate->offnum < pstate->minoff) - return; - - /* - * Don't look ahead when there aren't enough tuples remaining on the page - * (in the current scan direction) for it to be worth our while - */ - if (ScanDirectionIsForward(dir) && - pstate->offnum >= pstate->maxoff - LOOK_AHEAD_DEFAULT_DISTANCE) - return; - else if (ScanDirectionIsBackward(dir) && - pstate->offnum <= pstate->minoff + LOOK_AHEAD_DEFAULT_DISTANCE) - return; - - /* - * The look ahead distance starts small, and ramps up as each call here - * allows _bt_readpage to skip over more tuples - */ - if (!pstate->targetdistance) - pstate->targetdistance = LOOK_AHEAD_DEFAULT_DISTANCE; - else if (pstate->targetdistance < MaxIndexTuplesPerPage / 2) - pstate->targetdistance *= 2; - - /* Don't read past the end (or before the start) of the page, though */ - if (ScanDirectionIsForward(dir)) - aheadoffnum = Min((int) pstate->maxoff, - (int) pstate->offnum + pstate->targetdistance); - else - aheadoffnum = Max((int) pstate->minoff, - (int) pstate->offnum - pstate->targetdistance); + int a = *((const int *) va); + int b = *((const int *) vb); - ahead = (IndexTuple) PageGetItem(pstate->page, - PageGetItemId(pstate->page, aheadoffnum)); - if (_bt_tuple_before_array_skeys(scan, dir, ahead, tupdesc, tupnatts, - false, 0, NULL)) - { - /* - * Success -- instruct _bt_readpage to skip ahead to very next tuple - * after the one we determined was still before the current array keys - */ - if (ScanDirectionIsForward(dir)) - pstate->skip = aheadoffnum + 1; - else - pstate->skip = aheadoffnum - 1; - } - else - { - /* - * Failure -- "ahead" tuple is too far ahead (we were too aggressive). - * - * Reset the number of rechecks, and aggressively reduce the target - * distance (we're much more aggressive here than we were when the - * distance was initially ramped up). - */ - pstate->rechecks = 0; - pstate->targetdistance = Max(pstate->targetdistance / 8, 1); - } + return pg_cmp_s32(a, b); } /* @@ -3330,87 +166,99 @@ _bt_checkkeys_look_ahead(IndexScanDesc scan, BTReadPageState *pstate, * current page and killed tuples thereon (generally, this should only be * called if so->numKilled > 0). * - * The caller does not have a lock on the page and may or may not have the - * page pinned in a buffer. Note that read-lock is sufficient for setting - * LP_DEAD status (which is only a hint). - * - * We match items by heap TID before assuming they are the right ones to - * delete. We cope with cases where items have moved right due to insertions. - * If an item has moved off the current page due to a split, we'll fail to - * find it and do nothing (this is not an error case --- we assume the item - * will eventually get marked in a future indexscan). - * - * Note that if we hold a pin on the target page continuously from initially - * reading the items until applying this function, VACUUM cannot have deleted - * any items from the page, and so there is no need to search left from the - * recorded offset. (This observation also guarantees that the item is still - * the right one to delete, which might otherwise be questionable since heap - * TIDs can get recycled.) This holds true even if the page has been modified - * by inserts and page splits, so there is no need to consult the LSN. - * - * If the pin was released after reading the page, then we re-read it. If it - * has been modified since we read it (as determined by the LSN), we dare not - * flag any entries because it is possible that the old entry was vacuumed - * away and the TID was re-used by a completely different heap tuple. + * Caller should not have a lock on the so->currPos page, but must hold a + * buffer pin when !so->dropPin. When we return, it still won't be locked. + * It'll continue to hold whatever pins were held before calling here. + * + * We match items by heap TID before assuming they are the right ones to set + * LP_DEAD. If the scan is one that holds a buffer pin on the target page + * continuously from initially reading the items until applying this function + * (if it is a !so->dropPin scan), VACUUM cannot have deleted any items on the + * page, so the page's TIDs can't have been recycled by now. There's no risk + * that we'll confuse a new index tuple that happens to use a recycled TID + * with a now-removed tuple with the same TID (that used to be on this same + * page). We can't rely on that during scans that drop buffer pins eagerly + * (so->dropPin scans), though, so we must condition setting LP_DEAD bits on + * the page LSN having not changed since back when _bt_readpage saw the page. + * We totally give up on setting LP_DEAD bits when the page LSN changed. + * + * We give up much less often during !so->dropPin scans, but it still happens. + * We cope with cases where items have moved right due to insertions. If an + * item has moved off the current page due to a split, we'll fail to find it + * and just give up on it. */ void _bt_killitems(IndexScanDesc scan) { + Relation rel = scan->indexRelation; BTScanOpaque so = (BTScanOpaque) scan->opaque; Page page; BTPageOpaque opaque; OffsetNumber minoff; OffsetNumber maxoff; - int i; int numKilled = so->numKilled; bool killedsomething = false; - bool droppedpin PG_USED_FOR_ASSERTS_ONLY; + Buffer buf; + Assert(numKilled > 0); Assert(BTScanPosIsValid(so->currPos)); + Assert(scan->heapRelation != NULL); /* can't be a bitmap index scan */ + + /* Always invalidate so->killedItems[] before leaving so->currPos */ + so->numKilled = 0; /* - * Always reset the scan state, so we don't look for same items on other - * pages. + * We need to iterate through so->killedItems[] in leaf page order; the + * loop below expects this (when marking posting list tuples, at least). + * so->killedItems[] is now in whatever order the scan returned items in. + * Scrollable cursor scans might have even saved the same item/TID twice. + * + * Sort and unique-ify so->killedItems[] to deal with all this. */ - so->numKilled = 0; + if (numKilled > 1) + { + qsort(so->killedItems, numKilled, sizeof(int), _bt_compare_int); + numKilled = qunique(so->killedItems, numKilled, sizeof(int), + _bt_compare_int); + } - if (BTScanPosIsPinned(so->currPos)) + if (!so->dropPin) { /* * We have held the pin on this page since we read the index tuples, * so all we need to do is lock it. The pin will have prevented - * re-use of any TID on the page, so there is no need to check the - * LSN. + * concurrent VACUUMs from recycling any of the TIDs on the page. */ - droppedpin = false; - _bt_lockbuf(scan->indexRelation, so->currPos.buf, BT_READ); - - page = BufferGetPage(so->currPos.buf); + Assert(BTScanPosIsPinned(so->currPos)); + buf = so->currPos.buf; + _bt_lockbuf(rel, buf, BT_READ); } else { - Buffer buf; + XLogRecPtr latestlsn; - droppedpin = true; - /* Attempt to re-read the buffer, getting pin and lock. */ - buf = _bt_getbuf(scan->indexRelation, so->currPos.currPage, BT_READ); + Assert(!BTScanPosIsPinned(so->currPos)); + buf = _bt_getbuf(rel, so->currPos.currPage, BT_READ); - page = BufferGetPage(buf); - if (BufferGetLSNAtomic(buf) == so->currPos.lsn) - so->currPos.buf = buf; - else + latestlsn = BufferGetLSNAtomic(buf); + Assert(so->currPos.lsn <= latestlsn); + if (so->currPos.lsn != latestlsn) { - /* Modified while not pinned means hinting is not safe. */ - _bt_relbuf(scan->indexRelation, buf); + /* Modified, give up on hinting */ + _bt_relbuf(rel, buf); return; } + + /* Unmodified, hinting is safe */ } + page = BufferGetPage(buf); opaque = BTPageGetOpaque(page); minoff = P_FIRSTDATAKEY(opaque); maxoff = PageGetMaxOffsetNumber(page); - for (i = 0; i < numKilled; i++) + /* Iterate through so->killedItems[] in leaf page order */ + for (int i = 0; i < numKilled; i++) { int itemIndex = so->killedItems[i]; BTScanPosItem *kitem = &so->currPos.items[itemIndex]; @@ -3418,6 +266,9 @@ _bt_killitems(IndexScanDesc scan) Assert(itemIndex >= so->currPos.firstItem && itemIndex <= so->currPos.lastItem); + Assert(i == 0 || + offnum >= so->currPos.items[so->killedItems[i - 1]].indexOffset); + if (offnum < minoff) continue; /* pure paranoia */ while (offnum <= maxoff) @@ -3433,16 +284,8 @@ _bt_killitems(IndexScanDesc scan) int j; /* - * We rely on the convention that heap TIDs in the scanpos - * items array are stored in ascending heap TID order for a - * group of TIDs that originally came from a posting list - * tuple. This convention even applies during backwards - * scans, where returning the TIDs in descending order might - * seem more natural. This is about effectiveness, not - * correctness. - * * Note that the page may have been modified in almost any way - * since we first read it (in the !droppedpin case), so it's + * since we first read it (in the !so->dropPin case), so it's * possible that this posting list tuple wasn't a posting list * tuple when we first encountered its heap TIDs. */ @@ -3458,7 +301,7 @@ _bt_killitems(IndexScanDesc scan) * though only in the common case where the page can't * have been concurrently modified */ - Assert(kitem->indexOffset == offnum || !droppedpin); + Assert(kitem->indexOffset == offnum || !so->dropPin); /* * Read-ahead to later kitems here. @@ -3503,6 +346,17 @@ _bt_killitems(IndexScanDesc scan) */ if (killtuple && !ItemIdIsDead(iid)) { + if (!killedsomething) + { + /* + * Use the hint bit infrastructure to check if we can + * update the page while just holding a share lock. If we + * are not allowed, there's no point continuing. + */ + if (!BufferBeginSetHintBits(buf)) + goto unlock_page; + } + /* found the item/all posting list items */ ItemIdMarkDead(iid); killedsomething = true; @@ -3522,10 +376,14 @@ _bt_killitems(IndexScanDesc scan) if (killedsomething) { opaque->btpo_flags |= BTP_HAS_GARBAGE; - MarkBufferDirtyHint(so->currPos.buf, true); + BufferFinishSetHintBits(buf, true, true); } - _bt_unlockbuf(scan->indexRelation, so->currPos.buf); +unlock_page: + if (!so->dropPin) + _bt_unlockbuf(rel, buf); + else + _bt_relbuf(rel, buf); } @@ -3560,6 +418,13 @@ typedef struct BTVacInfo static BTVacInfo *btvacinfo; +static void BTreeShmemRequest(void *arg); +static void BTreeShmemInit(void *arg); + +const ShmemCallbacks BTreeShmemCallbacks = { + .request_fn = BTreeShmemRequest, + .init_fn = BTreeShmemInit, +}; /* * _bt_vacuum_cycleid --- get the active vacuum cycle ID for an index, @@ -3696,47 +561,37 @@ _bt_end_vacuum_callback(int code, Datum arg) } /* - * BTreeShmemSize --- report amount of shared memory space needed + * BTreeShmemRequest --- register this module's shared memory */ -Size -BTreeShmemSize(void) +static void +BTreeShmemRequest(void *arg) { Size size; size = offsetof(BTVacInfo, vacuums); size = add_size(size, mul_size(MaxBackends, sizeof(BTOneVacInfo))); - return size; + + ShmemRequestStruct(.name = "BTree Vacuum State", + .size = size, + .ptr = (void **) &btvacinfo, + ); } /* * BTreeShmemInit --- initialize this module's shared memory */ -void -BTreeShmemInit(void) +static void +BTreeShmemInit(void *arg) { - bool found; - - btvacinfo = (BTVacInfo *) ShmemInitStruct("BTree Vacuum State", - BTreeShmemSize(), - &found); - - if (!IsUnderPostmaster) - { - /* Initialize shared memory area */ - Assert(!found); - - /* - * It doesn't really matter what the cycle counter starts at, but - * having it always start the same doesn't seem good. Seed with - * low-order bits of time() instead. - */ - btvacinfo->cycle_ctr = (BTCycleId) time(NULL); + /* + * It doesn't really matter what the cycle counter starts at, but having + * it always start the same doesn't seem good. Seed with low-order bits + * of time() instead. + */ + btvacinfo->cycle_ctr = (BTCycleId) time(NULL); - btvacinfo->num_vacuums = 0; - btvacinfo->max_vacuums = MaxBackends; - } - else - Assert(found); + btvacinfo->num_vacuums = 0; + btvacinfo->max_vacuums = MaxBackends; } bytea * diff --git a/src/backend/access/nbtree/nbtvalidate.c b/src/backend/access/nbtree/nbtvalidate.c index 817ad358f0caa..b67c015a4e758 100644 --- a/src/backend/access/nbtree/nbtvalidate.c +++ b/src/backend/access/nbtree/nbtvalidate.c @@ -3,7 +3,7 @@ * nbtvalidate.c * Opclass validator for btree. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/nbtree/nbtxlog.c b/src/backend/access/nbtree/nbtxlog.c index d31dd56732d2f..dff7d286fc834 100644 --- a/src/backend/access/nbtree/nbtxlog.c +++ b/src/backend/access/nbtree/nbtxlog.c @@ -4,7 +4,7 @@ * WAL replay logic for btrees. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -38,7 +38,7 @@ _bt_restore_page(Page page, char *from, int len) IndexTupleData itupdata; Size itemsz; char *end = from + len; - Item items[MaxIndexTuplesPerPage]; + void *items[MaxIndexTuplesPerPage]; uint16 itemsizes[MaxIndexTuplesPerPage]; int i; int nitems; @@ -53,16 +53,15 @@ _bt_restore_page(Page page, char *from, int len) { /* * As we step through the items, 'from' won't always be properly - * aligned, so we need to use memcpy(). Further, we use Item (which - * is just a char*) here for our items array for the same reason; - * wouldn't want the compiler or anyone thinking that an item is - * aligned when it isn't. + * aligned, so we need to use memcpy(). Further, we use void * here + * for our items array for the same reason; wouldn't want the compiler + * or anyone thinking that an item is aligned when it isn't. */ memcpy(&itupdata, from, sizeof(IndexTupleData)); itemsz = IndexTupleSize(&itupdata); itemsz = MAXALIGN(itemsz); - items[i] = (Item) from; + items[i] = from; itemsizes[i] = itemsz; i++; @@ -72,8 +71,7 @@ _bt_restore_page(Page page, char *from, int len) for (i = nitems - 1; i >= 0; i--) { - if (PageAddItem(page, items[i], itemsizes[i], nitems - i, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, items[i], itemsizes[i], nitems - i, false, false) == InvalidOffsetNumber) elog(PANIC, "_bt_restore_page: cannot add item to page"); } } @@ -143,7 +141,7 @@ _bt_clear_incomplete_split(XLogReaderState *record, uint8 block_id) if (XLogReadBufferForRedo(record, block_id, &buf) == BLK_NEEDS_REDO) { - Page page = (Page) BufferGetPage(buf); + Page page = BufferGetPage(buf); BTPageOpaque pageop = BTPageGetOpaque(page); Assert(P_INCOMPLETE_SPLIT(pageop)); @@ -186,8 +184,7 @@ btree_xlog_insert(bool isleaf, bool ismeta, bool posting, if (!posting) { /* Simple retail insertion */ - if (PageAddItem(page, (Item) datapos, datalen, xlrec->offnum, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, datapos, datalen, xlrec->offnum, false, false) == InvalidOffsetNumber) elog(PANIC, "failed to add new item"); } else @@ -225,8 +222,7 @@ btree_xlog_insert(bool isleaf, bool ismeta, bool posting, /* Insert "final" new item (not orignewitem from WAL stream) */ Assert(IndexTupleSize(newitem) == datalen); - if (PageAddItem(page, (Item) newitem, datalen, xlrec->offnum, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, newitem, datalen, xlrec->offnum, false, false) == InvalidOffsetNumber) elog(PANIC, "failed to add posting split new item"); } @@ -287,7 +283,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) /* Reconstruct right (new) sibling page from scratch */ rbuf = XLogInitBufferForRedo(record, 1); datapos = XLogRecGetBlockData(record, 1, &datalen); - rpage = (Page) BufferGetPage(rbuf); + rpage = BufferGetPage(rbuf); _bt_pageinit(rpage, BufferGetPageSize(rbuf)); ropaque = BTPageGetOpaque(rpage); @@ -314,7 +310,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) * checking possible. See also _bt_restore_page(), which does the * same for the right page. */ - Page origpage = (Page) BufferGetPage(buf); + Page origpage = BufferGetPage(buf); BTPageOpaque oopaque = BTPageGetOpaque(origpage); OffsetNumber off; IndexTuple newitem = NULL, @@ -368,8 +364,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) /* Add high key tuple from WAL record to temp page */ leftoff = P_HIKEY; - if (PageAddItem(leftpage, (Item) left_hikey, left_hikeysz, P_HIKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(leftpage, left_hikey, left_hikeysz, P_HIKEY, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add high key to left page after split"); leftoff = OffsetNumberNext(leftoff); @@ -384,9 +379,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) { Assert(newitemonleft || xlrec->firstrightoff == xlrec->newitemoff); - if (PageAddItem(leftpage, (Item) nposting, - MAXALIGN(IndexTupleSize(nposting)), leftoff, - false, false) == InvalidOffsetNumber) + if (PageAddItem(leftpage, nposting, MAXALIGN(IndexTupleSize(nposting)), leftoff, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add new posting list item to left page after split"); leftoff = OffsetNumberNext(leftoff); continue; /* don't insert oposting */ @@ -395,8 +388,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) /* add the new item if it was inserted on left page */ else if (newitemonleft && off == xlrec->newitemoff) { - if (PageAddItem(leftpage, (Item) newitem, newitemsz, leftoff, - false, false) == InvalidOffsetNumber) + if (PageAddItem(leftpage, newitem, newitemsz, leftoff, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add new item to left page after split"); leftoff = OffsetNumberNext(leftoff); } @@ -404,8 +396,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) itemid = PageGetItemId(origpage, off); itemsz = ItemIdGetLength(itemid); item = (IndexTuple) PageGetItem(origpage, itemid); - if (PageAddItem(leftpage, (Item) item, itemsz, leftoff, - false, false) == InvalidOffsetNumber) + if (PageAddItem(leftpage, item, itemsz, leftoff, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add old item to left page after split"); leftoff = OffsetNumberNext(leftoff); } @@ -413,8 +404,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) /* cope with possibility that newitem goes at the end */ if (newitemonleft && off == xlrec->newitemoff) { - if (PageAddItem(leftpage, (Item) newitem, newitemsz, leftoff, - false, false) == InvalidOffsetNumber) + if (PageAddItem(leftpage, newitem, newitemsz, leftoff, false, false) == InvalidOffsetNumber) elog(ERROR, "failed to add new item to left page after split"); leftoff = OffsetNumberNext(leftoff); } @@ -439,7 +429,7 @@ btree_xlog_split(bool newitemonleft, XLogReaderState *record) if (XLogReadBufferForRedo(record, 2, &sbuf) == BLK_NEEDS_REDO) { - Page spage = (Page) BufferGetPage(sbuf); + Page spage = BufferGetPage(sbuf); BTPageOpaque spageop = BTPageGetOpaque(spage); spageop->btpo_prev = rightpagenumber; @@ -470,7 +460,7 @@ btree_xlog_dedup(XLogReaderState *record) if (XLogReadBufferForRedo(record, 0, &buf) == BLK_NEEDS_REDO) { char *ptr = XLogRecGetBlockData(record, 0, NULL); - Page page = (Page) BufferGetPage(buf); + Page page = BufferGetPage(buf); BTPageOpaque opaque = BTPageGetOpaque(page); OffsetNumber offnum, minoff, @@ -479,7 +469,7 @@ btree_xlog_dedup(XLogReaderState *record) BTDedupInterval *intervals; Page newpage; - state = (BTDedupState) palloc(sizeof(BTDedupStateData)); + state = palloc_object(BTDedupStateData); state->deduplicate = true; /* unused */ state->nmaxitems = 0; /* unused */ /* Conservatively use larger maxpostingsize than primary */ @@ -503,8 +493,7 @@ btree_xlog_dedup(XLogReaderState *record) Size itemsz = ItemIdGetLength(itemid); IndexTuple item = (IndexTuple) PageGetItem(page, itemid); - if (PageAddItem(newpage, (Item) item, itemsz, P_HIKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(newpage, item, itemsz, P_HIKEY, false, false) == InvalidOffsetNumber) elog(ERROR, "deduplication failed to add highkey"); } @@ -580,8 +569,7 @@ btree_xlog_updates(Page page, OffsetNumber *updatedoffsets, /* Overwrite updated version of tuple */ itemsz = MAXALIGN(IndexTupleSize(vacposting->itup)); - if (!PageIndexTupleOverwrite(page, updatedoffsets[i], - (Item) vacposting->itup, itemsz)) + if (!PageIndexTupleOverwrite(page, updatedoffsets[i], vacposting->itup, itemsz)) elog(PANIC, "failed to update partially dead item"); pfree(vacposting->itup); @@ -614,7 +602,7 @@ btree_xlog_vacuum(XLogReaderState *record) { char *ptr = XLogRecGetBlockData(record, 0, NULL); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (xlrec->nupdated > 0) { @@ -680,7 +668,7 @@ btree_xlog_delete(XLogReaderState *record) { char *ptr = XLogRecGetBlockData(record, 0, NULL); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (xlrec->nupdated > 0) { @@ -740,7 +728,7 @@ btree_xlog_mark_page_halfdead(uint8 info, XLogReaderState *record) OffsetNumber nextoffset; BlockNumber rightsib; - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); pageop = BTPageGetOpaque(page); poffset = xlrec->poffset; @@ -769,7 +757,7 @@ btree_xlog_mark_page_halfdead(uint8 info, XLogReaderState *record) /* Rewrite the leaf page as a halfdead page */ buffer = XLogInitBufferForRedo(record, 0); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); _bt_pageinit(page, BufferGetPageSize(buffer)); pageop = BTPageGetOpaque(page); @@ -788,8 +776,7 @@ btree_xlog_mark_page_halfdead(uint8 info, XLogReaderState *record) trunctuple.t_info = sizeof(IndexTupleData); BTreeTupleSetTopParent(&trunctuple, xlrec->topparent); - if (PageAddItem(page, (Item) &trunctuple, sizeof(IndexTupleData), P_HIKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, &trunctuple, sizeof(IndexTupleData), P_HIKEY, false, false) == InvalidOffsetNumber) elog(ERROR, "could not add dummy high key to half-dead page"); PageSetLSN(page, lsn); @@ -836,7 +823,7 @@ btree_xlog_unlink_page(uint8 info, XLogReaderState *record) { if (XLogReadBufferForRedo(record, 1, &leftbuf) == BLK_NEEDS_REDO) { - page = (Page) BufferGetPage(leftbuf); + page = BufferGetPage(leftbuf); pageop = BTPageGetOpaque(page); pageop->btpo_next = rightsib; @@ -849,7 +836,7 @@ btree_xlog_unlink_page(uint8 info, XLogReaderState *record) /* Rewrite target page as empty deleted page */ target = XLogInitBufferForRedo(record, 0); - page = (Page) BufferGetPage(target); + page = BufferGetPage(target); _bt_pageinit(page, BufferGetPageSize(target)); pageop = BTPageGetOpaque(page); @@ -868,7 +855,7 @@ btree_xlog_unlink_page(uint8 info, XLogReaderState *record) /* Fix left-link of right sibling */ if (XLogReadBufferForRedo(record, 2, &rightbuf) == BLK_NEEDS_REDO) { - page = (Page) BufferGetPage(rightbuf); + page = BufferGetPage(rightbuf); pageop = BTPageGetOpaque(page); pageop->btpo_prev = leftsib; @@ -907,7 +894,7 @@ btree_xlog_unlink_page(uint8 info, XLogReaderState *record) Assert(!isleaf); leafbuf = XLogInitBufferForRedo(record, 3); - page = (Page) BufferGetPage(leafbuf); + page = BufferGetPage(leafbuf); _bt_pageinit(page, BufferGetPageSize(leafbuf)); pageop = BTPageGetOpaque(page); @@ -923,8 +910,7 @@ btree_xlog_unlink_page(uint8 info, XLogReaderState *record) trunctuple.t_info = sizeof(IndexTupleData); BTreeTupleSetTopParent(&trunctuple, xlrec->leaftopparent); - if (PageAddItem(page, (Item) &trunctuple, sizeof(IndexTupleData), P_HIKEY, - false, false) == InvalidOffsetNumber) + if (PageAddItem(page, &trunctuple, sizeof(IndexTupleData), P_HIKEY, false, false) == InvalidOffsetNumber) elog(ERROR, "could not add dummy high key to half-dead page"); PageSetLSN(page, lsn); @@ -949,7 +935,7 @@ btree_xlog_newroot(XLogReaderState *record) Size len; buffer = XLogInitBufferForRedo(record, 0); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); _bt_pageinit(page, BufferGetPageSize(buffer)); pageop = BTPageGetOpaque(page); diff --git a/src/backend/access/rmgrdesc/brindesc.c b/src/backend/access/rmgrdesc/brindesc.c index 9fc0bfe2a523a..618d8a417f6b6 100644 --- a/src/backend/access/rmgrdesc/brindesc.c +++ b/src/backend/access/rmgrdesc/brindesc.c @@ -3,7 +3,7 @@ * brindesc.c * rmgr descriptor routines for BRIN indexes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/clogdesc.c b/src/backend/access/rmgrdesc/clogdesc.c index 41bf28dcfd05c..022376045d798 100644 --- a/src/backend/access/rmgrdesc/clogdesc.c +++ b/src/backend/access/rmgrdesc/clogdesc.c @@ -3,7 +3,7 @@ * clogdesc.c * rmgr descriptor routines for access/transam/clog.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/committsdesc.c b/src/backend/access/rmgrdesc/committsdesc.c index a6ab9dd78de17..958db3946e0c4 100644 --- a/src/backend/access/rmgrdesc/committsdesc.c +++ b/src/backend/access/rmgrdesc/committsdesc.c @@ -3,7 +3,7 @@ * committsdesc.c * rmgr descriptor routines for access/transam/commit_ts.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/dbasedesc.c b/src/backend/access/rmgrdesc/dbasedesc.c index 4224c5673ff87..3061b01726a83 100644 --- a/src/backend/access/rmgrdesc/dbasedesc.c +++ b/src/backend/access/rmgrdesc/dbasedesc.c @@ -3,7 +3,7 @@ * dbasedesc.c * rmgr descriptor routines for commands/dbcommands.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/genericdesc.c b/src/backend/access/rmgrdesc/genericdesc.c index 75dc4108b9aa2..abecc9ba32b88 100644 --- a/src/backend/access/rmgrdesc/genericdesc.c +++ b/src/backend/access/rmgrdesc/genericdesc.c @@ -4,7 +4,7 @@ * rmgr descriptor routines for access/transam/generic_xlog.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/rmgrdesc/genericdesc.c @@ -23,8 +23,8 @@ void generic_desc(StringInfo buf, XLogReaderState *record) { - Pointer ptr = XLogRecGetData(record), - end = ptr + XLogRecGetDataLen(record); + const char *ptr = XLogRecGetData(record); + const char *end = ptr + XLogRecGetDataLen(record); while (ptr < end) { diff --git a/src/backend/access/rmgrdesc/gindesc.c b/src/backend/access/rmgrdesc/gindesc.c index 723ff9499cf46..4e24a1cdfe725 100644 --- a/src/backend/access/rmgrdesc/gindesc.c +++ b/src/backend/access/rmgrdesc/gindesc.c @@ -3,7 +3,7 @@ * gindesc.c * rmgr descriptor routines for access/transam/gin/ginxlog.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -23,7 +23,7 @@ desc_recompress_leaf(StringInfo buf, ginxlogRecompressDataLeaf *insertData) int i; char *walbuf = ((char *) insertData) + sizeof(ginxlogRecompressDataLeaf); - appendStringInfo(buf, " %d segments:", (int) insertData->nactions); + appendStringInfo(buf, " %d segments:", insertData->nactions); for (i = 0; i < insertData->nactions; i++) { @@ -99,14 +99,7 @@ gin_desc(StringInfo buf, XLogReaderState *record) appendStringInfo(buf, " children: %u/%u", leftChildBlkno, rightChildBlkno); } - if (XLogRecHasBlockImage(record, 0)) - { - if (XLogRecBlockImageApply(record, 0)) - appendStringInfoString(buf, " (full page image)"); - else - appendStringInfoString(buf, " (full page image, for WAL verification)"); - } - else + if (!XLogRecHasBlockImage(record, 0)) { char *payload = XLogRecGetBlockData(record, 0, NULL); @@ -133,10 +126,13 @@ gin_desc(StringInfo buf, XLogReaderState *record) ginxlogSplit *xlrec = (ginxlogSplit *) rec; appendStringInfo(buf, "isrootsplit: %c", - (((ginxlogSplit *) rec)->flags & GIN_SPLIT_ROOT) ? 'T' : 'F'); + (xlrec->flags & GIN_SPLIT_ROOT) ? 'T' : 'F'); appendStringInfo(buf, " isdata: %c isleaf: %c", (xlrec->flags & GIN_INSERT_ISDATA) ? 'T' : 'F', (xlrec->flags & GIN_INSERT_ISLEAF) ? 'T' : 'F'); + if (xlrec->leftChildBlkno != InvalidBlockNumber) + appendStringInfo(buf, " children: %u/%u", + xlrec->leftChildBlkno, xlrec->rightChildBlkno); } break; case XLOG_GIN_VACUUM_PAGE: @@ -144,14 +140,7 @@ gin_desc(StringInfo buf, XLogReaderState *record) break; case XLOG_GIN_VACUUM_DATA_LEAF_PAGE: { - if (XLogRecHasBlockImage(record, 0)) - { - if (XLogRecBlockImageApply(record, 0)) - appendStringInfoString(buf, " (full page image)"); - else - appendStringInfoString(buf, " (full page image, for WAL verification)"); - } - else + if (!XLogRecHasBlockImage(record, 0)) { ginxlogVacuumDataLeafPage *xlrec = (ginxlogVacuumDataLeafPage *) XLogRecGetBlockData(record, 0, NULL); @@ -164,10 +153,27 @@ gin_desc(StringInfo buf, XLogReaderState *record) /* no further information */ break; case XLOG_GIN_UPDATE_META_PAGE: - /* no further information */ + { + ginxlogUpdateMeta *xlrec = (ginxlogUpdateMeta *) rec; + + appendStringInfo(buf, "ntuples: %d", xlrec->ntuples); + if (xlrec->prevTail != InvalidBlockNumber) + appendStringInfo(buf, " prevTail: %u", + xlrec->prevTail); + if (xlrec->newRightlink != InvalidBlockNumber) + appendStringInfo(buf, " newRightlink: %u", + xlrec->newRightlink); + } break; case XLOG_GIN_INSERT_LISTPAGE: - /* no further information */ + { + ginxlogInsertListPage *xlrec = (ginxlogInsertListPage *) rec; + + appendStringInfo(buf, "ntuples: %d", xlrec->ntuples); + if (xlrec->rightlink != InvalidBlockNumber) + appendStringInfo(buf, " rightlink: %u", + xlrec->rightlink); + } break; case XLOG_GIN_DELETE_LISTPAGE: appendStringInfo(buf, "ndeleted: %d", diff --git a/src/backend/access/rmgrdesc/gistdesc.c b/src/backend/access/rmgrdesc/gistdesc.c index a2b84e898f959..67789e0253bf7 100644 --- a/src/backend/access/rmgrdesc/gistdesc.c +++ b/src/backend/access/rmgrdesc/gistdesc.c @@ -3,7 +3,7 @@ * gistdesc.c * rmgr descriptor routines for access/gist/gistxlog.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -80,9 +80,6 @@ gist_desc(StringInfo buf, XLogReaderState *record) case XLOG_GIST_PAGE_DELETE: out_gistxlogPageDelete(buf, (gistxlogPageDelete *) rec); break; - case XLOG_GIST_ASSIGN_LSN: - /* No details to write out */ - break; } } @@ -108,9 +105,6 @@ gist_identify(uint8 info) case XLOG_GIST_PAGE_DELETE: id = "PAGE_DELETE"; break; - case XLOG_GIST_ASSIGN_LSN: - id = "ASSIGN_LSN"; - break; } return id; diff --git a/src/backend/access/rmgrdesc/hashdesc.c b/src/backend/access/rmgrdesc/hashdesc.c index 75f43a9152071..593013afe9c82 100644 --- a/src/backend/access/rmgrdesc/hashdesc.c +++ b/src/backend/access/rmgrdesc/hashdesc.c @@ -3,7 +3,7 @@ * hashdesc.c * rmgr descriptor routines for access/hash/hash.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -28,8 +28,10 @@ hash_desc(StringInfo buf, XLogReaderState *record) { xl_hash_init_meta_page *xlrec = (xl_hash_init_meta_page *) rec; - appendStringInfo(buf, "num_tuples %g, fillfactor %d", - xlrec->num_tuples, xlrec->ffactor); + appendStringInfo(buf, "num_tuples %g, procid %u, fillfactor %d", + xlrec->num_tuples, + xlrec->procid, + xlrec->ffactor); break; } case XLOG_HASH_INIT_BITMAP_PAGE: @@ -58,8 +60,10 @@ hash_desc(StringInfo buf, XLogReaderState *record) { xl_hash_split_allocate_page *xlrec = (xl_hash_split_allocate_page *) rec; - appendStringInfo(buf, "new_bucket %u, meta_page_masks_updated %c, issplitpoint_changed %c", + appendStringInfo(buf, "new_bucket %u, old_bucket_flag %u, new_bucket_flag %u, meta_page_masks_updated %c, issplitpoint_changed %c", xlrec->new_bucket, + xlrec->old_bucket_flag, + xlrec->new_bucket_flag, (xlrec->flags & XLH_SPLIT_META_UPDATE_MASKS) ? 'T' : 'F', (xlrec->flags & XLH_SPLIT_META_UPDATE_SPLITPOINT) ? 'T' : 'F'); break; @@ -85,11 +89,12 @@ hash_desc(StringInfo buf, XLogReaderState *record) { xl_hash_squeeze_page *xlrec = (xl_hash_squeeze_page *) rec; - appendStringInfo(buf, "prevblkno %u, nextblkno %u, ntups %d, is_primary %c", + appendStringInfo(buf, "prevblkno %u, nextblkno %u, ntups %d, is_primary %c, is_previous %c", xlrec->prevblkno, xlrec->nextblkno, xlrec->ntups, - xlrec->is_prim_bucket_same_wrt ? 'T' : 'F'); + xlrec->is_prim_bucket_same_wrt ? 'T' : 'F', + xlrec->is_prev_bucket_same_wrt ? 'T' : 'F'); break; } case XLOG_HASH_DELETE: diff --git a/src/backend/access/rmgrdesc/heapdesc.c b/src/backend/access/rmgrdesc/heapdesc.c index 82b62c95de574..75ae6f9d375cd 100644 --- a/src/backend/access/rmgrdesc/heapdesc.c +++ b/src/backend/access/rmgrdesc/heapdesc.c @@ -3,7 +3,7 @@ * heapdesc.c * rmgr descriptor routines for access/heap/heapam.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,7 @@ #include "access/heapam_xlog.h" #include "access/rmgrdesc_utils.h" +#include "access/visibilitymapdefs.h" #include "storage/standbydefs.h" /* @@ -102,7 +103,7 @@ plan_elem_desc(StringInfo buf, void *plan, void *data) * code, the latter of which is used in frontend (pg_waldump) code. */ void -heap_xlog_deserialize_prune_and_freeze(char *cursor, uint8 flags, +heap_xlog_deserialize_prune_and_freeze(char *cursor, uint16 flags, int *nplans, xlhp_freeze_plan **plans, OffsetNumber **frz_offsets, int *nredirected, OffsetNumber **redirected, @@ -286,6 +287,15 @@ heap2_desc(StringInfo buf, XLogReaderState *record) appendStringInfo(buf, ", isCatalogRel: %c", xlrec->flags & XLHP_IS_CATALOG_REL ? 'T' : 'F'); + if (xlrec->flags & XLHP_VM_ALL_VISIBLE) + { + uint8 vmflags = VISIBILITYMAP_ALL_VISIBLE; + + if (xlrec->flags & XLHP_VM_ALL_FROZEN) + vmflags |= VISIBILITYMAP_ALL_FROZEN; + appendStringInfo(buf, ", vm_flags: 0x%02X", vmflags); + } + if (XLogRecHasBlockData(record, 0)) { Size datalen; @@ -339,13 +349,6 @@ heap2_desc(StringInfo buf, XLogReaderState *record) } } } - else if (info == XLOG_HEAP2_VISIBLE) - { - xl_heap_visible *xlrec = (xl_heap_visible *) rec; - - appendStringInfo(buf, "snapshotConflictHorizon: %u, flags: 0x%02X", - xlrec->snapshotConflictHorizon, xlrec->flags); - } else if (info == XLOG_HEAP2_MULTI_INSERT) { xl_heap_multi_insert *xlrec = (xl_heap_multi_insert *) rec; @@ -354,6 +357,11 @@ heap2_desc(StringInfo buf, XLogReaderState *record) appendStringInfo(buf, "ntuples: %d, flags: 0x%02X", xlrec->ntuples, xlrec->flags); + if (xlrec->flags & XLH_INSERT_ALL_FROZEN_SET) + appendStringInfo(buf, ", vm_flags: 0x%02X", + VISIBILITYMAP_ALL_VISIBLE | + VISIBILITYMAP_ALL_FROZEN); + if (XLogRecHasBlockData(record, 0) && !isinit) { appendStringInfoString(buf, ", offsets:"); @@ -446,9 +454,6 @@ heap2_identify(uint8 info) case XLOG_HEAP2_PRUNE_VACUUM_CLEANUP: id = "PRUNE_VACUUM_CLEANUP"; break; - case XLOG_HEAP2_VISIBLE: - id = "VISIBLE"; - break; case XLOG_HEAP2_MULTI_INSERT: id = "MULTI_INSERT"; break; diff --git a/src/backend/access/rmgrdesc/logicalmsgdesc.c b/src/backend/access/rmgrdesc/logicalmsgdesc.c index 1c8c99f19f836..8824f06b3e002 100644 --- a/src/backend/access/rmgrdesc/logicalmsgdesc.c +++ b/src/backend/access/rmgrdesc/logicalmsgdesc.c @@ -3,7 +3,7 @@ * logicalmsgdesc.c * rmgr descriptor routines for replication/logical/message.c * - * Portions Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2015-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/access/rmgrdesc/meson.build b/src/backend/access/rmgrdesc/meson.build index 96c98e800c22d..d9000ccd9fd10 100644 --- a/src/backend/access/rmgrdesc/meson.build +++ b/src/backend/access/rmgrdesc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # used by frontend programs like pg_waldump rmgr_desc_sources = files( diff --git a/src/backend/access/rmgrdesc/mxactdesc.c b/src/backend/access/rmgrdesc/mxactdesc.c index 3ca0582db3647..6919c7ddd2ea3 100644 --- a/src/backend/access/rmgrdesc/mxactdesc.c +++ b/src/backend/access/rmgrdesc/mxactdesc.c @@ -3,7 +3,7 @@ * mxactdesc.c * rmgr descriptor routines for access/transam/multixact.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -65,7 +65,7 @@ multixact_desc(StringInfo buf, XLogReaderState *record) xl_multixact_create *xlrec = (xl_multixact_create *) rec; int i; - appendStringInfo(buf, "%u offset %u nmembers %d: ", xlrec->mid, + appendStringInfo(buf, "%u offset %" PRIu64 " nmembers %d: ", xlrec->mid, xlrec->moff, xlrec->nmembers); for (i = 0; i < xlrec->nmembers; i++) out_member(buf, &xlrec->members[i]); @@ -74,9 +74,8 @@ multixact_desc(StringInfo buf, XLogReaderState *record) { xl_multixact_truncate *xlrec = (xl_multixact_truncate *) rec; - appendStringInfo(buf, "offsets [%u, %u), members [%u, %u)", - xlrec->startTruncOff, xlrec->endTruncOff, - xlrec->startTruncMemb, xlrec->endTruncMemb); + appendStringInfo(buf, "oldestMulti %u, oldestOffset %" PRIu64, + xlrec->oldestMulti, xlrec->oldestOffset); } } diff --git a/src/backend/access/rmgrdesc/nbtdesc.c b/src/backend/access/rmgrdesc/nbtdesc.c index c05d19ab00753..1d08f9957bdca 100644 --- a/src/backend/access/rmgrdesc/nbtdesc.c +++ b/src/backend/access/rmgrdesc/nbtdesc.c @@ -3,7 +3,7 @@ * nbtdesc.c * rmgr descriptor routines for access/nbtree/nbtxlog.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/relmapdesc.c b/src/backend/access/rmgrdesc/relmapdesc.c index caf1846032167..e19f304609808 100644 --- a/src/backend/access/rmgrdesc/relmapdesc.c +++ b/src/backend/access/rmgrdesc/relmapdesc.c @@ -3,7 +3,7 @@ * relmapdesc.c * rmgr descriptor routines for utils/cache/relmapper.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/replorigindesc.c b/src/backend/access/rmgrdesc/replorigindesc.c index 5dd742339969a..248dbed2c2c2d 100644 --- a/src/backend/access/rmgrdesc/replorigindesc.c +++ b/src/backend/access/rmgrdesc/replorigindesc.c @@ -3,7 +3,7 @@ * replorigindesc.c * rmgr descriptor routines for replication/logical/origin.c * - * Portions Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2015-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -29,7 +29,7 @@ replorigin_desc(StringInfo buf, XLogReaderState *record) xlrec = (xl_replorigin_set *) rec; - appendStringInfo(buf, "set %u; lsn %X/%X; force: %d", + appendStringInfo(buf, "set %u; lsn %X/%08X; force: %d", xlrec->node_id, LSN_FORMAT_ARGS(xlrec->remote_lsn), xlrec->force); diff --git a/src/backend/access/rmgrdesc/rmgrdesc_utils.c b/src/backend/access/rmgrdesc/rmgrdesc_utils.c index 25cf167f17524..b157b0fdc44f1 100644 --- a/src/backend/access/rmgrdesc/rmgrdesc_utils.c +++ b/src/backend/access/rmgrdesc/rmgrdesc_utils.c @@ -3,7 +3,7 @@ * rmgrdesc_utils.c * Support functions for rmgrdesc routines * - * Copyright (c) 2023-2025, PostgreSQL Global Development Group + * Copyright (c) 2023-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/access/rmgrdesc/seqdesc.c b/src/backend/access/rmgrdesc/seqdesc.c index 0d289d77fcf7a..c9fc6dc18503d 100644 --- a/src/backend/access/rmgrdesc/seqdesc.c +++ b/src/backend/access/rmgrdesc/seqdesc.c @@ -3,7 +3,7 @@ * seqdesc.c * rmgr descriptor routines for commands/sequence.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,7 +14,7 @@ */ #include "postgres.h" -#include "commands/sequence.h" +#include "commands/sequence_xlog.h" void diff --git a/src/backend/access/rmgrdesc/smgrdesc.c b/src/backend/access/rmgrdesc/smgrdesc.c index 4bb7fc79bced6..aaf1b07999dbc 100644 --- a/src/backend/access/rmgrdesc/smgrdesc.c +++ b/src/backend/access/rmgrdesc/smgrdesc.c @@ -3,7 +3,7 @@ * smgrdesc.c * rmgr descriptor routines for catalog/storage.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/spgdesc.c b/src/backend/access/rmgrdesc/spgdesc.c index 72efedc5b40d3..548de1374044f 100644 --- a/src/backend/access/rmgrdesc/spgdesc.c +++ b/src/backend/access/rmgrdesc/spgdesc.c @@ -3,7 +3,7 @@ * spgdesc.c * rmgr descriptor routines for access/spgist/spgxlog.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/standbydesc.c b/src/backend/access/rmgrdesc/standbydesc.c index 81eff5f31c4f6..685d1bdb02413 100644 --- a/src/backend/access/rmgrdesc/standbydesc.c +++ b/src/backend/access/rmgrdesc/standbydesc.c @@ -3,7 +3,7 @@ * standbydesc.c * rmgr descriptor routines for storage/ipc/standby.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -41,6 +41,8 @@ standby_desc_running_xacts(StringInfo buf, xl_running_xacts *xlrec) for (i = 0; i < xlrec->subxcnt; i++) appendStringInfo(buf, " %u", xlrec->xids[xlrec->xcnt + i]); } + + appendStringInfo(buf, "; dbid: %u", xlrec->dbid); } void diff --git a/src/backend/access/rmgrdesc/tblspcdesc.c b/src/backend/access/rmgrdesc/tblspcdesc.c index 5d612b4232e26..c9255c48a4722 100644 --- a/src/backend/access/rmgrdesc/tblspcdesc.c +++ b/src/backend/access/rmgrdesc/tblspcdesc.c @@ -3,7 +3,7 @@ * tblspcdesc.c * rmgr descriptor routines for commands/tablespace.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/rmgrdesc/xactdesc.c b/src/backend/access/rmgrdesc/xactdesc.c index 305598e2865c8..4f53d3035cc26 100644 --- a/src/backend/access/rmgrdesc/xactdesc.c +++ b/src/backend/access/rmgrdesc/xactdesc.c @@ -3,7 +3,7 @@ * xactdesc.c * rmgr descriptor routines for access/transam/xact.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -331,7 +331,7 @@ xact_desc_stats(StringInfo buf, const char *label, } static void -xact_desc_commit(StringInfo buf, uint8 info, xl_xact_commit *xlrec, RepOriginId origin_id) +xact_desc_commit(StringInfo buf, uint8 info, xl_xact_commit *xlrec, ReplOriginId origin_id) { xl_xact_parsed_commit parsed; @@ -359,7 +359,7 @@ xact_desc_commit(StringInfo buf, uint8 info, xl_xact_commit *xlrec, RepOriginId if (parsed.xinfo & XACT_XINFO_HAS_ORIGIN) { - appendStringInfo(buf, "; origin: node %u, lsn %X/%X, at %s", + appendStringInfo(buf, "; origin: node %u, lsn %X/%08X, at %s", origin_id, LSN_FORMAT_ARGS(parsed.origin_lsn), timestamptz_to_str(parsed.origin_timestamp)); @@ -367,7 +367,7 @@ xact_desc_commit(StringInfo buf, uint8 info, xl_xact_commit *xlrec, RepOriginId } static void -xact_desc_abort(StringInfo buf, uint8 info, xl_xact_abort *xlrec, RepOriginId origin_id) +xact_desc_abort(StringInfo buf, uint8 info, xl_xact_abort *xlrec, ReplOriginId origin_id) { xl_xact_parsed_abort parsed; @@ -384,7 +384,7 @@ xact_desc_abort(StringInfo buf, uint8 info, xl_xact_abort *xlrec, RepOriginId or if (parsed.xinfo & XACT_XINFO_HAS_ORIGIN) { - appendStringInfo(buf, "; origin: node %u, lsn %X/%X, at %s", + appendStringInfo(buf, "; origin: node %u, lsn %X/%08X, at %s", origin_id, LSN_FORMAT_ARGS(parsed.origin_lsn), timestamptz_to_str(parsed.origin_timestamp)); @@ -394,7 +394,7 @@ xact_desc_abort(StringInfo buf, uint8 info, xl_xact_abort *xlrec, RepOriginId or } static void -xact_desc_prepare(StringInfo buf, uint8 info, xl_xact_prepare *xlrec, RepOriginId origin_id) +xact_desc_prepare(StringInfo buf, uint8 info, xl_xact_prepare *xlrec, ReplOriginId origin_id) { xl_xact_parsed_prepare parsed; @@ -417,8 +417,8 @@ xact_desc_prepare(StringInfo buf, uint8 info, xl_xact_prepare *xlrec, RepOriginI * Check if the replication origin has been set in this record in the same * way as PrepareRedoAdd(). */ - if (origin_id != InvalidRepOriginId) - appendStringInfo(buf, "; origin: node %u, lsn %X/%X, at %s", + if (origin_id != InvalidReplOriginId) + appendStringInfo(buf, "; origin: node %u, lsn %X/%08X, at %s", origin_id, LSN_FORMAT_ARGS(parsed.origin_lsn), timestamptz_to_str(parsed.origin_timestamp)); diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c index 58040f28656fc..2468a7d257855 100644 --- a/src/backend/access/rmgrdesc/xlogdesc.c +++ b/src/backend/access/rmgrdesc/xlogdesc.c @@ -3,7 +3,7 @@ * xlogdesc.c * rmgr descriptor routines for access/transam/xlog.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,6 +18,7 @@ #include "access/xlog.h" #include "access/xlog_internal.h" #include "catalog/pg_control.h" +#include "storage/checksum.h" #include "utils/guc.h" #include "utils/timestamp.h" @@ -54,6 +55,40 @@ get_wal_level_string(int wal_level) return wal_level_str; } +const char * +get_checksum_state_string(uint32 state) +{ + switch (state) + { + case PG_DATA_CHECKSUM_VERSION: + return "on"; + case PG_DATA_CHECKSUM_INPROGRESS_OFF: + return "inprogress-off"; + case PG_DATA_CHECKSUM_INPROGRESS_ON: + return "inprogress-on"; + case PG_DATA_CHECKSUM_OFF: + return "off"; + } + + Assert(false); + return "?"; +} + +void +xlog2_desc(StringInfo buf, XLogReaderState *record) +{ + char *rec = XLogRecGetData(record); + uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; + + if (info == XLOG2_CHECKSUMS) + { + xl_checksum_state xlrec; + + memcpy(&xlrec, rec, sizeof(xl_checksum_state)); + appendStringInfoString(buf, get_checksum_state_string(xlrec.new_checksum_state)); + } +} + void xlog_desc(StringInfo buf, XLogReaderState *record) { @@ -65,16 +100,18 @@ xlog_desc(StringInfo buf, XLogReaderState *record) { CheckPoint *checkpoint = (CheckPoint *) rec; - appendStringInfo(buf, "redo %X/%X; " - "tli %u; prev tli %u; fpw %s; wal_level %s; xid %u:%u; oid %u; multi %u; offset %u; " + appendStringInfo(buf, "redo %X/%08X; " + "tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; " "oldest xid %u in DB %u; oldest multi %u in DB %u; " "oldest/newest commit timestamp xid: %u/%u; " - "oldest running xid %u; %s", + "oldest running xid %u; " + "checksums %s; %s", LSN_FORMAT_ARGS(checkpoint->redo), checkpoint->ThisTimeLineID, checkpoint->PrevTimeLineID, checkpoint->fullPageWrites ? "true" : "false", get_wal_level_string(checkpoint->wal_level), + checkpoint->logicalDecodingEnabled ? "true" : "false", EpochFromFullTransactionId(checkpoint->nextXid), XidFromFullTransactionId(checkpoint->nextXid), checkpoint->nextOid, @@ -87,6 +124,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record) checkpoint->oldestCommitTsXid, checkpoint->newestCommitTsXid, checkpoint->oldestActiveXid, + get_checksum_state_string(checkpoint->dataChecksumState), (info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online"); } else if (info == XLOG_NEXTOID) @@ -111,7 +149,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record) XLogRecPtr startpoint; memcpy(&startpoint, rec, sizeof(XLogRecPtr)); - appendStringInfo(buf, "%X/%X", LSN_FORMAT_ARGS(startpoint)); + appendStringInfo(buf, "%X/%08X", LSN_FORMAT_ARGS(startpoint)); } else if (info == XLOG_PARAMETER_CHANGE) { @@ -156,16 +194,29 @@ xlog_desc(StringInfo buf, XLogReaderState *record) xl_overwrite_contrecord xlrec; memcpy(&xlrec, rec, sizeof(xl_overwrite_contrecord)); - appendStringInfo(buf, "lsn %X/%X; time %s", + appendStringInfo(buf, "lsn %X/%08X; time %s", LSN_FORMAT_ARGS(xlrec.overwritten_lsn), timestamptz_to_str(xlrec.overwrite_time)); } else if (info == XLOG_CHECKPOINT_REDO) { - int wal_level; + xl_checkpoint_redo xlrec; - memcpy(&wal_level, rec, sizeof(int)); - appendStringInfo(buf, "wal_level %s", get_wal_level_string(wal_level)); + memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo)); + appendStringInfo(buf, "wal_level %s; checksums %s", + get_wal_level_string(xlrec.wal_level), + get_checksum_state_string(xlrec.data_checksum_version)); + } + else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE) + { + bool enabled; + + memcpy(&enabled, rec, sizeof(bool)); + appendStringInfoString(buf, enabled ? "true" : "false"); + } + else if (info == XLOG_ASSIGN_LSN) + { + /* no further information to print */ } } @@ -218,6 +269,27 @@ xlog_identify(uint8 info) case XLOG_CHECKPOINT_REDO: id = "CHECKPOINT_REDO"; break; + case XLOG_LOGICAL_DECODING_STATUS_CHANGE: + id = "LOGICAL_DECODING_STATUS_CHANGE"; + break; + case XLOG_ASSIGN_LSN: + id = "ASSIGN_LSN"; + break; + } + + return id; +} + +const char * +xlog2_identify(uint8 info) +{ + const char *id = NULL; + + switch (info & ~XLR_INFO_MASK) + { + case XLOG2_CHECKSUMS: + id = "CHECKSUMS"; + break; } return id; diff --git a/src/backend/access/sequence/meson.build b/src/backend/access/sequence/meson.build index ec9ab9b7e9dbd..40cc02c770c06 100644 --- a/src/backend/access/sequence/meson.build +++ b/src/backend/access/sequence/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'sequence.c', diff --git a/src/backend/access/sequence/sequence.c b/src/backend/access/sequence/sequence.c index 8b53035537022..81f46163749d9 100644 --- a/src/backend/access/sequence/sequence.c +++ b/src/backend/access/sequence/sequence.c @@ -3,7 +3,7 @@ * sequence.c * Generic routines for sequence-related code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -24,7 +24,7 @@ #include "access/sequence.h" #include "utils/rel.h" -static inline void validate_relation_kind(Relation r); +static inline void validate_relation_as_sequence(Relation r); /* ---------------- * sequence_open - open a sequence relation by relation OID @@ -40,7 +40,7 @@ sequence_open(Oid relationId, LOCKMODE lockmode) r = relation_open(relationId, lockmode); - validate_relation_kind(r); + validate_relation_as_sequence(r); return r; } @@ -61,18 +61,17 @@ sequence_close(Relation relation, LOCKMODE lockmode) } /* ---------------- - * validate_relation_kind - check the relation's kind + * validate_relation_as_sequence * - * Make sure relkind is from a sequence. + * Make sure relkind is a sequence. * ---------------- */ static inline void -validate_relation_kind(Relation r) +validate_relation_as_sequence(Relation r) { if (r->rd_rel->relkind != RELKIND_SEQUENCE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot open relation \"%s\"", - RelationGetRelationName(r)), - errdetail_relkind_not_supported(r->rd_rel->relkind))); + errmsg("\"%s\" is not a sequence", + RelationGetRelationName(r)))); } diff --git a/src/backend/access/spgist/meson.build b/src/backend/access/spgist/meson.build index e763f05304f73..c29e1f1d32bde 100644 --- a/src/backend/access/spgist/meson.build +++ b/src/backend/access/spgist/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'spgdoinsert.c', diff --git a/src/backend/access/spgist/spgdoinsert.c b/src/backend/access/spgist/spgdoinsert.c index af6b27b2135ac..7c7371c69e80e 100644 --- a/src/backend/access/spgist/spgdoinsert.c +++ b/src/backend/access/spgist/spgdoinsert.c @@ -4,7 +4,7 @@ * implementation of insert algorithm * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -89,7 +89,7 @@ addNode(SpGistState *state, SpGistInnerTuple tuple, Datum label, int offset) else if (offset > tuple->nNodes) elog(ERROR, "invalid offset for adding node to SPGiST inner tuple"); - nodes = palloc(sizeof(SpGistNodeTuple) * (tuple->nNodes + 1)); + nodes = palloc_array(SpGistNodeTuple, tuple->nNodes + 1); SGITITERATE(tuple, i, node) { if (i < offset) @@ -165,8 +165,7 @@ spgPageIndexMultiDelete(SpGistState *state, Page page, if (tuple == NULL || tuple->tupstate != tupstate) tuple = spgFormDeadTuple(state, tupstate, blkno, offnum); - if (PageAddItem(page, (Item) tuple, tuple->size, - itemno, false, false) != itemno) + if (PageAddItem(page, tuple, tuple->size, itemno, false, false) != itemno) elog(ERROR, "failed to add item of size %u to SPGiST index page", tuple->size); @@ -222,7 +221,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple, /* Tuple is not part of a chain */ SGLT_SET_NEXTOFFSET(leafTuple, InvalidOffsetNumber); current->offnum = SpGistPageAddNewItem(state, current->page, - (Item) leafTuple, leafTuple->size, + leafTuple, leafTuple->size, NULL, false); xlrec.offnumLeaf = current->offnum; @@ -255,7 +254,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple, { SGLT_SET_NEXTOFFSET(leafTuple, SGLT_GET_NEXTOFFSET(head)); offnum = SpGistPageAddNewItem(state, current->page, - (Item) leafTuple, leafTuple->size, + leafTuple, leafTuple->size, NULL, false); /* @@ -274,7 +273,7 @@ addLeafTuple(Relation index, SpGistState *state, SpGistLeafTuple leafTuple, SGLT_SET_NEXTOFFSET(leafTuple, InvalidOffsetNumber); PageIndexTupleDelete(current->page, current->offnum); if (PageAddItem(current->page, - (Item) leafTuple, leafTuple->size, + leafTuple, leafTuple->size, current->offnum, false, false) != current->offnum) elog(ERROR, "failed to add item of size %u to SPGiST index page", leafTuple->size); @@ -410,8 +409,8 @@ moveLeafs(Relation index, SpGistState *state, /* Locate the tuples to be moved, and count up the space needed */ i = PageGetMaxOffsetNumber(current->page); - toDelete = (OffsetNumber *) palloc(sizeof(OffsetNumber) * i); - toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * (i + 1)); + toDelete = palloc_array(OffsetNumber, i); + toInsert = palloc_array(OffsetNumber, i + 1); size = newLeafTuple->size + sizeof(ItemIdData); @@ -478,8 +477,7 @@ moveLeafs(Relation index, SpGistState *state, */ SGLT_SET_NEXTOFFSET(it, r); - r = SpGistPageAddNewItem(state, npage, (Item) it, it->size, - &startOffset, false); + r = SpGistPageAddNewItem(state, npage, it, it->size, &startOffset, false); toInsert[nInsert] = r; nInsert++; @@ -492,9 +490,7 @@ moveLeafs(Relation index, SpGistState *state, /* add the new tuple as well */ SGLT_SET_NEXTOFFSET(newLeafTuple, r); - r = SpGistPageAddNewItem(state, npage, - (Item) newLeafTuple, newLeafTuple->size, - &startOffset, false); + r = SpGistPageAddNewItem(state, npage, newLeafTuple, newLeafTuple->size, &startOffset, false); toInsert[nInsert] = r; nInsert++; memcpy(leafptr, newLeafTuple, newLeafTuple->size); @@ -638,7 +634,7 @@ checkAllTheSame(spgPickSplitIn *in, spgPickSplitOut *out, bool tooBig, { Datum theLabel = out->nodeLabels[theNode]; - out->nodeLabels = (Datum *) palloc(sizeof(Datum) * out->nNodes); + out->nodeLabels = palloc_array(Datum, out->nNodes); for (i = 0; i < out->nNodes; i++) out->nodeLabels[i] = theLabel; } @@ -721,12 +717,12 @@ doPickSplit(Relation index, SpGistState *state, */ max = PageGetMaxOffsetNumber(current->page); n = max + 1; - in.datums = (Datum *) palloc(sizeof(Datum) * n); - toDelete = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n); - toInsert = (OffsetNumber *) palloc(sizeof(OffsetNumber) * n); - oldLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n); - newLeafs = (SpGistLeafTuple *) palloc(sizeof(SpGistLeafTuple) * n); - leafPageSelect = (uint8 *) palloc(sizeof(uint8) * n); + in.datums = palloc_array(Datum, n); + toDelete = palloc_array(OffsetNumber, n); + toInsert = palloc_array(OffsetNumber, n); + oldLeafs = palloc_array(SpGistLeafTuple, n); + newLeafs = palloc_array(SpGistLeafTuple, n); + leafPageSelect = palloc_array(uint8, n); STORE_STATE(state, xlrec.stateSrc); @@ -862,7 +858,7 @@ doPickSplit(Relation index, SpGistState *state, out.hasPrefix = false; out.nNodes = 1; out.nodeLabels = NULL; - out.mapTuplesToNodes = palloc0(sizeof(int) * in.nTuples); + out.mapTuplesToNodes = palloc0_array(int, in.nTuples); /* * Form new leaf tuples and count up the total space needed. @@ -918,8 +914,8 @@ doPickSplit(Relation index, SpGistState *state, * out.nNodes with a value larger than the number of tuples on the input * page, we can't allocate these arrays before here. */ - nodes = (SpGistNodeTuple *) palloc(sizeof(SpGistNodeTuple) * out.nNodes); - leafSizes = (int *) palloc0(sizeof(int) * out.nNodes); + nodes = palloc_array(SpGistNodeTuple, out.nNodes); + leafSizes = palloc0_array(int, out.nNodes); /* * Form nodes of inner tuple and inner tuple itself @@ -1058,7 +1054,7 @@ doPickSplit(Relation index, SpGistState *state, * do so, even if totalLeafSizes is less than the available space, * because we can't split a group across pages. */ - nodePageSelect = (uint8 *) palloc(sizeof(uint8) * out.nNodes); + nodePageSelect = palloc_array(uint8, out.nNodes); curspace = currentFreeSpace; newspace = PageGetExactFreeSpace(BufferGetPage(newLeafBuffer)); @@ -1226,7 +1222,7 @@ doPickSplit(Relation index, SpGistState *state, /* Insert it on page */ newoffset = SpGistPageAddNewItem(state, BufferGetPage(leafBuffer), - (Item) it, it->size, + it, it->size, &startOffsets[leafPageSelect[i]], false); toInsert[i] = newoffset; @@ -1268,7 +1264,7 @@ doPickSplit(Relation index, SpGistState *state, current->page = parent->page; xlrec.offnumInner = current->offnum = SpGistPageAddNewItem(state, current->page, - (Item) innerTuple, innerTuple->size, + innerTuple, innerTuple->size, NULL, false); /* @@ -1302,7 +1298,7 @@ doPickSplit(Relation index, SpGistState *state, current->page = BufferGetPage(current->buffer); xlrec.offnumInner = current->offnum = SpGistPageAddNewItem(state, current->page, - (Item) innerTuple, innerTuple->size, + innerTuple, innerTuple->size, NULL, false); /* Done modifying new current buffer, mark it dirty */ @@ -1340,7 +1336,7 @@ doPickSplit(Relation index, SpGistState *state, xlrec.innerIsParent = false; xlrec.offnumInner = current->offnum = - PageAddItem(current->page, (Item) innerTuple, innerTuple->size, + PageAddItem(current->page, innerTuple, innerTuple->size, InvalidOffsetNumber, false, false); if (current->offnum != FirstOffsetNumber) elog(ERROR, "failed to add item of size %u to SPGiST index page", @@ -1547,7 +1543,7 @@ spgAddNodeAction(Relation index, SpGistState *state, PageIndexTupleDelete(current->page, current->offnum); if (PageAddItem(current->page, - (Item) newInnerTuple, newInnerTuple->size, + newInnerTuple, newInnerTuple->size, current->offnum, false, false) != current->offnum) elog(ERROR, "failed to add item of size %u to SPGiST index page", newInnerTuple->size); @@ -1631,7 +1627,7 @@ spgAddNodeAction(Relation index, SpGistState *state, /* insert new ... */ xlrec.offnumNew = current->offnum = SpGistPageAddNewItem(state, current->page, - (Item) newInnerTuple, newInnerTuple->size, + newInnerTuple, newInnerTuple->size, NULL, false); MarkBufferDirty(current->buffer); @@ -1654,7 +1650,7 @@ spgAddNodeAction(Relation index, SpGistState *state, current->blkno, current->offnum); PageIndexTupleDelete(saveCurrent.page, saveCurrent.offnum); - if (PageAddItem(saveCurrent.page, (Item) dt, dt->size, + if (PageAddItem(saveCurrent.page, dt, dt->size, saveCurrent.offnum, false, false) != saveCurrent.offnum) elog(ERROR, "failed to add item of size %u to SPGiST index page", @@ -1744,8 +1740,7 @@ spgSplitNodeAction(Relation index, SpGistState *state, * Construct new prefix tuple with requested number of nodes. We'll fill * in the childNodeN'th node's downlink below. */ - nodes = (SpGistNodeTuple *) palloc(sizeof(SpGistNodeTuple) * - out->result.splitTuple.prefixNNodes); + nodes = palloc_array(SpGistNodeTuple, out->result.splitTuple.prefixNNodes); for (i = 0; i < out->result.splitTuple.prefixNNodes; i++) { @@ -1773,7 +1768,7 @@ spgSplitNodeAction(Relation index, SpGistState *state, * same node datums, but with the prefix specified by the picksplit * function. */ - nodes = palloc(sizeof(SpGistNodeTuple) * innerTuple->nNodes); + nodes = palloc_array(SpGistNodeTuple, innerTuple->nNodes); SGITITERATE(innerTuple, i, node) { nodes[i] = node; @@ -1818,7 +1813,7 @@ spgSplitNodeAction(Relation index, SpGistState *state, */ PageIndexTupleDelete(current->page, current->offnum); xlrec.offnumPrefix = PageAddItem(current->page, - (Item) prefixTuple, prefixTuple->size, + prefixTuple, prefixTuple->size, current->offnum, false, false); if (xlrec.offnumPrefix != current->offnum) elog(ERROR, "failed to add item of size %u to SPGiST index page", @@ -1832,7 +1827,7 @@ spgSplitNodeAction(Relation index, SpGistState *state, postfixBlkno = current->blkno; xlrec.offnumPostfix = postfixOffset = SpGistPageAddNewItem(state, current->page, - (Item) postfixTuple, postfixTuple->size, + postfixTuple, postfixTuple->size, NULL, false); xlrec.postfixBlkSame = true; } @@ -1841,7 +1836,7 @@ spgSplitNodeAction(Relation index, SpGistState *state, postfixBlkno = BufferGetBlockNumber(newBuffer); xlrec.offnumPostfix = postfixOffset = SpGistPageAddNewItem(state, BufferGetPage(newBuffer), - (Item) postfixTuple, postfixTuple->size, + postfixTuple, postfixTuple->size, NULL, false); MarkBufferDirty(newBuffer); xlrec.postfixBlkSame = false; @@ -1912,7 +1907,7 @@ spgSplitNodeAction(Relation index, SpGistState *state, */ bool spgdoinsert(Relation index, SpGistState *state, - ItemPointer heapPtr, Datum *datums, bool *isnulls) + const ItemPointerData *heapPtr, const Datum *datums, const bool *isnulls) { bool result = true; TupleDesc leafDescriptor = state->leafTupDesc; diff --git a/src/backend/access/spgist/spginsert.c b/src/backend/access/spgist/spginsert.c index 6a61e093fa05a..780ef646a5491 100644 --- a/src/backend/access/spgist/spginsert.c +++ b/src/backend/access/spgist/spginsert.c @@ -5,7 +5,7 @@ * * All the actual insertion logic is in spgdoinsert.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -140,7 +140,7 @@ spgbuild(Relation heap, Relation index, IndexInfo *indexInfo) true); } - result = (IndexBuildResult *) palloc0(sizeof(IndexBuildResult)); + result = palloc0_object(IndexBuildResult); result->heap_tuples = reltuples; result->index_tuples = buildstate.indtuples; diff --git a/src/backend/access/spgist/spgkdtreeproc.c b/src/backend/access/spgist/spgkdtreeproc.c index d6989759e5fd7..83fca0e7cb96b 100644 --- a/src/backend/access/spgist/spgkdtreeproc.c +++ b/src/backend/access/spgist/spgkdtreeproc.c @@ -4,7 +4,7 @@ * implementation of k-d tree over points for SP-GiST * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -27,7 +27,9 @@ Datum spg_kd_config(PG_FUNCTION_ARGS) { - /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ +#ifdef NOT_USED + spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); +#endif spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); cfg->prefixType = FLOAT8OID; @@ -84,8 +86,8 @@ typedef struct SortedPoint static int x_cmp(const void *a, const void *b) { - SortedPoint *pa = (SortedPoint *) a; - SortedPoint *pb = (SortedPoint *) b; + const SortedPoint *pa = a; + const SortedPoint *pb = b; if (pa->p->x == pb->p->x) return 0; @@ -95,8 +97,8 @@ x_cmp(const void *a, const void *b) static int y_cmp(const void *a, const void *b) { - SortedPoint *pa = (SortedPoint *) a; - SortedPoint *pb = (SortedPoint *) b; + const SortedPoint *pa = a; + const SortedPoint *pb = b; if (pa->p->y == pb->p->y) return 0; @@ -114,7 +116,7 @@ spg_kd_picksplit(PG_FUNCTION_ARGS) SortedPoint *sorted; double coord; - sorted = palloc(sizeof(*sorted) * in->nTuples); + sorted = palloc_array(SortedPoint, in->nTuples); for (i = 0; i < in->nTuples; i++) { sorted[i].p = DatumGetPointP(in->datums[i]); @@ -132,8 +134,8 @@ spg_kd_picksplit(PG_FUNCTION_ARGS) out->nNodes = 2; out->nodeLabels = NULL; /* we don't need node labels */ - out->mapTuplesToNodes = palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = palloc(sizeof(Datum) * in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); /* * Note: points that have coordinates exactly equal to coord may get @@ -259,7 +261,7 @@ spg_kd_inner_consistent(PG_FUNCTION_ARGS) if (!which) PG_RETURN_VOID(); - out->nodeNumbers = (int *) palloc(sizeof(int) * 2); + out->nodeNumbers = palloc_array(int, 2); /* * When ordering scan keys are specified, we've to calculate distance for @@ -273,8 +275,8 @@ spg_kd_inner_consistent(PG_FUNCTION_ARGS) BOX infArea; BOX *area; - out->distances = (double **) palloc(sizeof(double *) * in->nNodes); - out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes); + out->distances = palloc_array(double *, in->nNodes); + out->traversalValues = palloc_array(void *, in->nNodes); if (in->level == 0) { @@ -335,7 +337,7 @@ spg_kd_inner_consistent(PG_FUNCTION_ARGS) } /* Set up level increments, too */ - out->levelAdds = (int *) palloc(sizeof(int) * 2); + out->levelAdds = palloc_array(int, 2); out->levelAdds[0] = 1; out->levelAdds[1] = 1; diff --git a/src/backend/access/spgist/spgproc.c b/src/backend/access/spgist/spgproc.c index 660009291da4d..78873f23855bf 100644 --- a/src/backend/access/spgist/spgproc.c +++ b/src/backend/access/spgist/spgproc.c @@ -4,7 +4,7 @@ * Common supporting procedures for SP-GiST opclasses. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -51,7 +51,7 @@ point_box_distance(Point *point, BOX *box) else dy = 0.0; - return HYPOT(dx, dy); + return hypot(dx, dy); } /* @@ -64,7 +64,7 @@ spg_key_orderbys_distances(Datum key, bool isLeaf, ScanKey orderbys, int norderbys) { int sk_num; - double *distances = (double *) palloc(norderbys * sizeof(double)), + double *distances = palloc_array(double, norderbys), *distance = distances; for (sk_num = 0; sk_num < norderbys; ++sk_num, ++orderbys, ++distance) @@ -81,7 +81,7 @@ spg_key_orderbys_distances(Datum key, bool isLeaf, BOX * box_copy(BOX *orig) { - BOX *result = palloc(sizeof(BOX)); + BOX *result = palloc_object(BOX); *result = *orig; return result; diff --git a/src/backend/access/spgist/spgquadtreeproc.c b/src/backend/access/spgist/spgquadtreeproc.c index 3e8cfa1709af3..946dabc4527a7 100644 --- a/src/backend/access/spgist/spgquadtreeproc.c +++ b/src/backend/access/spgist/spgquadtreeproc.c @@ -4,7 +4,7 @@ * implementation of quad tree over points for SP-GiST * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -26,7 +26,9 @@ Datum spg_quad_config(PG_FUNCTION_ARGS) { - /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ +#ifdef NOT_USED + spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); +#endif spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); cfg->prefixType = POINTOID; @@ -82,7 +84,7 @@ getQuadrant(Point *centroid, Point *tst) static BOX * getQuadrantArea(BOX *bbox, Point *centroid, int quadrant) { - BOX *result = (BOX *) palloc(sizeof(BOX)); + BOX *result = palloc_object(BOX); switch (quadrant) { @@ -145,8 +147,8 @@ spg_quad_choose(PG_FUNCTION_ARGS) static int x_cmp(const void *a, const void *b, void *arg) { - Point *pa = *(Point **) a; - Point *pb = *(Point **) b; + Point *pa = *(Point *const *) a; + Point *pb = *(Point *const *) b; if (pa->x == pb->x) return 0; @@ -156,8 +158,8 @@ x_cmp(const void *a, const void *b, void *arg) static int y_cmp(const void *a, const void *b, void *arg) { - Point *pa = *(Point **) a; - Point *pb = *(Point **) b; + Point *pa = *(Point *const *) a; + Point *pb = *(Point *const *) b; if (pa->y == pb->y) return 0; @@ -177,11 +179,11 @@ spg_quad_picksplit(PG_FUNCTION_ARGS) /* Use the median values of x and y as the centroid point */ Point **sorted; - sorted = palloc(sizeof(*sorted) * in->nTuples); + sorted = palloc_array(Point *, in->nTuples); for (i = 0; i < in->nTuples; i++) sorted[i] = DatumGetPointP(in->datums[i]); - centroid = palloc(sizeof(*centroid)); + centroid = palloc_object(Point); qsort(sorted, in->nTuples, sizeof(*sorted), x_cmp); centroid->x = sorted[in->nTuples >> 1]->x; @@ -189,7 +191,7 @@ spg_quad_picksplit(PG_FUNCTION_ARGS) centroid->y = sorted[in->nTuples >> 1]->y; #else /* Use the average values of x and y as the centroid point */ - centroid = palloc0(sizeof(*centroid)); + centroid = palloc0_object(Point); for (i = 0; i < in->nTuples; i++) { @@ -207,8 +209,8 @@ spg_quad_picksplit(PG_FUNCTION_ARGS) out->nNodes = 4; out->nodeLabels = NULL; /* we don't need node labels */ - out->mapTuplesToNodes = palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = palloc(sizeof(Datum) * in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); for (i = 0; i < in->nTuples; i++) { @@ -246,8 +248,8 @@ spg_quad_inner_consistent(PG_FUNCTION_ARGS) */ if (in->norderbys > 0) { - out->distances = (double **) palloc(sizeof(double *) * in->nNodes); - out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes); + out->distances = palloc_array(double *, in->nNodes); + out->traversalValues = palloc_array(void *, in->nNodes); if (in->level == 0) { @@ -270,7 +272,7 @@ spg_quad_inner_consistent(PG_FUNCTION_ARGS) { /* Report that all nodes should be visited */ out->nNodes = in->nNodes; - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); for (i = 0; i < in->nNodes; i++) { out->nodeNumbers[i] = i; @@ -368,12 +370,12 @@ spg_quad_inner_consistent(PG_FUNCTION_ARGS) break; /* no need to consider remaining conditions */ } - out->levelAdds = palloc(sizeof(int) * 4); + out->levelAdds = palloc_array(int, 4); for (i = 0; i < 4; ++i) out->levelAdds[i] = 1; /* We must descend into the quadrant(s) identified by which */ - out->nodeNumbers = (int *) palloc(sizeof(int) * 4); + out->nodeNumbers = palloc_array(int, 4); out->nNodes = 0; for (i = 1; i <= 4; i++) diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c index 25893050c5876..2cc5f06f5d779 100644 --- a/src/backend/access/spgist/spgscan.c +++ b/src/backend/access/spgist/spgscan.c @@ -4,7 +4,7 @@ * routines for scanning SP-GiST indexes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -18,6 +18,7 @@ #include "access/genam.h" #include "access/relscan.h" #include "access/spgist_private.h" +#include "executor/instrument_node.h" #include "miscadmin.h" #include "pgstat.h" #include "storage/bufmgr.h" @@ -309,9 +310,9 @@ spgbeginscan(Relation rel, int keysz, int orderbysz) scan = RelationGetIndexScan(rel, keysz, orderbysz); - so = (SpGistScanOpaque) palloc0(sizeof(SpGistScanOpaqueData)); + so = palloc0_object(SpGistScanOpaqueData); if (keysz > 0) - so->keyData = (ScanKey) palloc(sizeof(ScanKeyData) * keysz); + so->keyData = palloc_array(ScanKeyData, keysz); else so->keyData = NULL; initSpGistState(&so->state, scan->indexRelation); @@ -336,16 +337,12 @@ spgbeginscan(Relation rel, int keysz, int orderbysz) if (scan->numberOfOrderBys > 0) { /* This will be filled in spgrescan, but allocate the space here */ - so->orderByTypes = (Oid *) - palloc(sizeof(Oid) * scan->numberOfOrderBys); - so->nonNullOrderByOffsets = (int *) - palloc(sizeof(int) * scan->numberOfOrderBys); + so->orderByTypes = palloc_array(Oid, scan->numberOfOrderBys); + so->nonNullOrderByOffsets = palloc_array(int, scan->numberOfOrderBys); /* These arrays have constant contents, so we can fill them now */ - so->zeroDistances = (double *) - palloc(sizeof(double) * scan->numberOfOrderBys); - so->infDistances = (double *) - palloc(sizeof(double) * scan->numberOfOrderBys); + so->zeroDistances = palloc_array(double, scan->numberOfOrderBys); + so->infDistances = palloc_array(double, scan->numberOfOrderBys); for (i = 0; i < scan->numberOfOrderBys; i++) { @@ -353,10 +350,8 @@ spgbeginscan(Relation rel, int keysz, int orderbysz) so->infDistances[i] = get_float8_infinity(); } - scan->xs_orderbyvals = (Datum *) - palloc0(sizeof(Datum) * scan->numberOfOrderBys); - scan->xs_orderbynulls = (bool *) - palloc(sizeof(bool) * scan->numberOfOrderBys); + scan->xs_orderbyvals = palloc0_array(Datum, scan->numberOfOrderBys); + scan->xs_orderbynulls = palloc_array(bool, scan->numberOfOrderBys); memset(scan->xs_orderbynulls, true, sizeof(bool) * scan->numberOfOrderBys); } @@ -690,7 +685,7 @@ spgInnerTest(SpGistScanOpaque so, SpGistSearchItem *item, { /* force all children to be visited */ out.nNodes = nNodes; - out.nodeNumbers = (int *) palloc(sizeof(int) * nNodes); + out.nodeNumbers = palloc_array(int, nNodes); for (i = 0; i < nNodes; i++) out.nodeNumbers[i] = i; } @@ -703,7 +698,7 @@ spgInnerTest(SpGistScanOpaque so, SpGistSearchItem *item, { /* collect node pointers */ SpGistNodeTuple node; - SpGistNodeTuple *nodes = (SpGistNodeTuple *) palloc(sizeof(SpGistNodeTuple) * nNodes); + SpGistNodeTuple *nodes = palloc_array(SpGistNodeTuple, nNodes); SGITITERATE(innerTuple, i, node) { @@ -972,8 +967,8 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr, so->distances[so->nPtrs] = NULL; else { - IndexOrderByDistance *distances = - palloc(sizeof(distances[0]) * so->numberOfOrderBys); + IndexOrderByDistance *distances = palloc_array(IndexOrderByDistance, + so->numberOfOrderBys); int i; for (i = 0; i < so->numberOfOrderBys; i++) diff --git a/src/backend/access/spgist/spgtextproc.c b/src/backend/access/spgist/spgtextproc.c index 73842655f086c..e00feff0bb36d 100644 --- a/src/backend/access/spgist/spgtextproc.c +++ b/src/backend/access/spgist/spgtextproc.c @@ -29,7 +29,7 @@ * No new entries ever get pushed into a -2-labeled child, either. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -95,7 +95,9 @@ typedef struct spgNodePtr Datum spg_text_config(PG_FUNCTION_ARGS) { - /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ +#ifdef NOT_USED + spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); +#endif spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); cfg->prefixType = TEXTOID; @@ -155,7 +157,7 @@ commonPrefix(const char *a, const char *b, int lena, int lenb) * On success, *i gets the match location; on failure, it gets where to insert */ static bool -searchChar(Datum *nodeLabels, int nNodes, int16 c, int *i) +searchChar(const Datum *nodeLabels, int nNodes, int16 c, int *i) { int StopLow = 0, StopHigh = nNodes; @@ -230,8 +232,7 @@ spg_text_choose(PG_FUNCTION_ARGS) formTextDatum(prefixStr, commonLen); } out->result.splitTuple.prefixNNodes = 1; - out->result.splitTuple.prefixNodeLabels = - (Datum *) palloc(sizeof(Datum)); + out->result.splitTuple.prefixNodeLabels = palloc_object(Datum); out->result.splitTuple.prefixNodeLabels[0] = Int16GetDatum(*(unsigned char *) (prefixStr + commonLen)); @@ -303,7 +304,7 @@ spg_text_choose(PG_FUNCTION_ARGS) out->result.splitTuple.prefixHasPrefix = in->hasPrefix; out->result.splitTuple.prefixPrefixDatum = in->prefixDatum; out->result.splitTuple.prefixNNodes = 1; - out->result.splitTuple.prefixNodeLabels = (Datum *) palloc(sizeof(Datum)); + out->result.splitTuple.prefixNodeLabels = palloc_object(Datum); out->result.splitTuple.prefixNodeLabels[0] = Int16GetDatum(-2); out->result.splitTuple.childNodeN = 0; out->result.splitTuple.postfixHasPrefix = false; @@ -371,7 +372,7 @@ spg_text_picksplit(PG_FUNCTION_ARGS) } /* Extract the node label (first non-common byte) from each value */ - nodes = (spgNodePtr *) palloc(sizeof(spgNodePtr) * in->nTuples); + nodes = palloc_array(spgNodePtr, in->nTuples); for (i = 0; i < in->nTuples; i++) { @@ -394,9 +395,9 @@ spg_text_picksplit(PG_FUNCTION_ARGS) /* And emit results */ out->nNodes = 0; - out->nodeLabels = (Datum *) palloc(sizeof(Datum) * in->nTuples); - out->mapTuplesToNodes = (int *) palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = (Datum *) palloc(sizeof(Datum) * in->nTuples); + out->nodeLabels = palloc_array(Datum, in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); for (i = 0; i < in->nTuples; i++) { @@ -476,9 +477,9 @@ spg_text_inner_consistent(PG_FUNCTION_ARGS) * and see if it's consistent with the query. If so, emit an entry into * the output arrays. */ - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); - out->levelAdds = (int *) palloc(sizeof(int) * in->nNodes); - out->reconstructedValues = (Datum *) palloc(sizeof(Datum) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); + out->levelAdds = palloc_array(int, in->nNodes); + out->reconstructedValues = palloc_array(Datum, in->nNodes); out->nNodes = 0; for (i = 0; i < in->nNodes; i++) diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c index 95fea74e296f8..f2ee333f60d84 100644 --- a/src/backend/access/spgist/spgutils.c +++ b/src/backend/access/spgist/spgutils.c @@ -4,7 +4,7 @@ * various support functions for SP-GiST * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -43,62 +43,63 @@ Datum spghandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = 0; - amroutine->amsupport = SPGISTNProc; - amroutine->amoptsprocnum = SPGIST_OPTIONS_PROC; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = true; - amroutine->amcanhash = false; - amroutine->amconsistentequality = false; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = false; - amroutine->amcanunique = false; - amroutine->amcanmulticol = false; - amroutine->amoptionalkey = true; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = true; - amroutine->amstorage = true; - amroutine->amclusterable = false; - amroutine->ampredlocks = false; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = false; - amroutine->amcaninclude = true; - amroutine->amusemaintenanceworkmem = false; - amroutine->amsummarizing = false; - amroutine->amparallelvacuumoptions = - VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = spgbuild; - amroutine->ambuildempty = spgbuildempty; - amroutine->aminsert = spginsert; - amroutine->aminsertcleanup = NULL; - amroutine->ambulkdelete = spgbulkdelete; - amroutine->amvacuumcleanup = spgvacuumcleanup; - amroutine->amcanreturn = spgcanreturn; - amroutine->amcostestimate = spgcostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = spgoptions; - amroutine->amproperty = spgproperty; - amroutine->ambuildphasename = NULL; - amroutine->amvalidate = spgvalidate; - amroutine->amadjustmembers = spgadjustmembers; - amroutine->ambeginscan = spgbeginscan; - amroutine->amrescan = spgrescan; - amroutine->amgettuple = spggettuple; - amroutine->amgetbitmap = spggetbitmap; - amroutine->amendscan = spgendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - amroutine->amtranslatestrategy = NULL; - amroutine->amtranslatecmptype = NULL; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = 0, + .amsupport = SPGISTNProc, + .amoptsprocnum = SPGIST_OPTIONS_PROC, + .amcanorder = false, + .amcanorderbyop = true, + .amcanhash = false, + .amconsistentequality = false, + .amconsistentordering = false, + .amcanbackward = false, + .amcanunique = false, + .amcanmulticol = false, + .amoptionalkey = true, + .amsearcharray = false, + .amsearchnulls = true, + .amstorage = true, + .amclusterable = false, + .ampredlocks = false, + .amcanparallel = false, + .amcanbuildparallel = false, + .amcaninclude = true, + .amusemaintenanceworkmem = false, + .amsummarizing = false, + .amparallelvacuumoptions = + VACUUM_OPTION_PARALLEL_BULKDEL | VACUUM_OPTION_PARALLEL_COND_CLEANUP, + .amkeytype = InvalidOid, + + .ambuild = spgbuild, + .ambuildempty = spgbuildempty, + .aminsert = spginsert, + .aminsertcleanup = NULL, + .ambulkdelete = spgbulkdelete, + .amvacuumcleanup = spgvacuumcleanup, + .amcanreturn = spgcanreturn, + .amcostestimate = spgcostestimate, + .amgettreeheight = NULL, + .amoptions = spgoptions, + .amproperty = spgproperty, + .ambuildphasename = NULL, + .amvalidate = spgvalidate, + .amadjustmembers = spgadjustmembers, + .ambeginscan = spgbeginscan, + .amrescan = spgrescan, + .amgettuple = spggettuple, + .amgetbitmap = spggetbitmap, + .amendscan = spgendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + .amtranslatestrategy = NULL, + .amtranslatecmptype = NULL, + }; + + PG_RETURN_POINTER(&amroutine); } /* @@ -334,11 +335,9 @@ getSpGistTupleDesc(Relation index, SpGistTypeDesc *keyType) /* We shouldn't need to bother with making these valid: */ att->attcompression = InvalidCompressionMethod; att->attcollation = InvalidOid; - /* In case we changed typlen, we'd better reset following offsets */ - for (int i = spgFirstIncludeColumn; i < outTupDesc->natts; i++) - TupleDescCompactAttr(outTupDesc, i)->attcacheoff = -1; populate_compact_attribute(outTupDesc, spgKeyColumn); + TupleDescFinalize(outTupDesc); } return outTupDesc; } @@ -785,7 +784,7 @@ SpGistGetInnerTypeSize(SpGistTypeDesc *att, Datum datum) else if (att->attlen > 0) size = att->attlen; else - size = VARSIZE_ANY(datum); + size = VARSIZE_ANY(DatumGetPointer(datum)); return MAXALIGN(size); } @@ -804,7 +803,7 @@ memcpyInnerDatum(void *target, SpGistTypeDesc *att, Datum datum) } else { - size = (att->attlen > 0) ? att->attlen : VARSIZE_ANY(datum); + size = (att->attlen > 0) ? att->attlen : VARSIZE_ANY(DatumGetPointer(datum)); memcpy(target, DatumGetPointer(datum), size); } } @@ -868,7 +867,7 @@ SpGistGetLeafTupleSize(TupleDesc tupleDescriptor, * Construct a leaf tuple containing the given heap TID and datum values */ SpGistLeafTuple -spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr, +spgFormLeafTuple(SpGistState *state, const ItemPointerData *heapPtr, const Datum *datums, const bool *isnulls) { SpGistLeafTuple tup; @@ -930,12 +929,12 @@ spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr, if (needs_null_mask) { - bits8 *bp; /* ptr to null bitmap in tuple */ + uint8 *bp; /* ptr to null bitmap in tuple */ /* Set nullmask presence bit in SpGistLeafTuple header */ SGLT_SET_HASNULLMASK(tup, true); /* Fill the data area and null mask */ - bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData)); + bp = (uint8 *) ((char *) tup + sizeof(SpGistLeafTupleData)); heap_fill_tuple(tupleDescriptor, datums, isnulls, tp, data_size, &tupmask, bp); } @@ -943,7 +942,7 @@ spgFormLeafTuple(SpGistState *state, ItemPointer heapPtr, { /* Fill data area only */ heap_fill_tuple(tupleDescriptor, datums, isnulls, tp, data_size, - &tupmask, (bits8 *) NULL); + &tupmask, (uint8 *) NULL); } /* otherwise we have no data, nor a bitmap, to fill */ @@ -1117,7 +1116,7 @@ spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor, { bool hasNullsMask = SGLT_GET_HASNULLMASK(tup); char *tp; /* ptr to tuple data */ - bits8 *bp; /* ptr to null bitmap in tuple */ + uint8 *bp; /* ptr to null bitmap in tuple */ if (keyColumnIsNull && tupleDescriptor->natts == 1) { @@ -1138,7 +1137,7 @@ spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor, } tp = (char *) tup + SGLTHDRSZ(hasNullsMask); - bp = (bits8 *) ((char *) tup + sizeof(SpGistLeafTupleData)); + bp = (uint8 *) ((char *) tup + sizeof(SpGistLeafTupleData)); index_deform_tuple_internal(tupleDescriptor, datums, isnulls, @@ -1177,7 +1176,7 @@ spgExtractNodeLabels(SpGistState *state, SpGistInnerTuple innerTuple) } else { - nodeLabels = (Datum *) palloc(sizeof(Datum) * innerTuple->nNodes); + nodeLabels = palloc_array(Datum, innerTuple->nNodes); SGITITERATE(innerTuple, i, node) { if (IndexTupleHasNulls(node)) @@ -1200,7 +1199,7 @@ spgExtractNodeLabels(SpGistState *state, SpGistInnerTuple innerTuple) * rather than returning InvalidOffsetNumber. */ OffsetNumber -SpGistPageAddNewItem(SpGistState *state, Page page, Item item, Size size, +SpGistPageAddNewItem(SpGistState *state, Page page, const void *item, Size size, OffsetNumber *startOffset, bool errorOK) { SpGistPageOpaque opaque = SpGistPageGetOpaque(page); diff --git a/src/backend/access/spgist/spgvacuum.c b/src/backend/access/spgist/spgvacuum.c index 2678f7ab7829a..c461f8dc02d1b 100644 --- a/src/backend/access/spgist/spgvacuum.c +++ b/src/backend/access/spgist/spgvacuum.c @@ -4,7 +4,7 @@ * vacuum for SP-GiST * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -61,7 +61,7 @@ typedef struct spgBulkDeleteState * ensures that scans of the list don't miss items added during the scan. */ static void -spgAddPendingTID(spgBulkDeleteState *bds, ItemPointer tid) +spgAddPendingTID(spgBulkDeleteState *bds, const ItemPointerData *tid) { spgVacPendingItem *pitem; spgVacPendingItem **listLink; @@ -76,7 +76,7 @@ spgAddPendingTID(spgBulkDeleteState *bds, ItemPointer tid) listLink = &pitem->next; } /* not there, so append new entry */ - pitem = (spgVacPendingItem *) palloc(sizeof(spgVacPendingItem)); + pitem = palloc_object(spgVacPendingItem); pitem->tid = *tid; pitem->done = false; pitem->next = NULL; @@ -536,7 +536,7 @@ vacuumRedirectAndPlaceholder(Relation index, Relation heaprel, Buffer buffer) */ if (dt->tupstate == SPGIST_REDIRECT && (!TransactionIdIsValid(dt->xid) || - GlobalVisTestIsRemovableXid(vistest, dt->xid))) + GlobalVisTestIsRemovableXid(vistest, dt->xid, true))) { dt->tupstate = SPGIST_PLACEHOLDER; Assert(opaque->nRedirection > 0); @@ -626,7 +626,7 @@ spgvacuumpage(spgBulkDeleteState *bds, Buffer buffer) Page page; LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (PageIsNew(page)) { @@ -707,7 +707,7 @@ spgprocesspending(spgBulkDeleteState *bds) buffer = ReadBufferExtended(index, MAIN_FORKNUM, blkno, RBM_NORMAL, bds->info->strategy); LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); - page = (Page) BufferGetPage(buffer); + page = BufferGetPage(buffer); if (PageIsNew(page) || SpGistPageIsDeleted(page)) { @@ -954,7 +954,7 @@ spgbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, /* allocate stats if first time through, else re-use existing struct */ if (stats == NULL) - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); bds.info = info; bds.stats = stats; bds.callback = callback; @@ -994,7 +994,7 @@ spgvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats) */ if (stats == NULL) { - stats = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + stats = palloc0_object(IndexBulkDeleteResult); bds.info = info; bds.stats = stats; bds.callback = dummy_callback; diff --git a/src/backend/access/spgist/spgvalidate.c b/src/backend/access/spgist/spgvalidate.c index e9964fab4f41a..27c855921e67f 100644 --- a/src/backend/access/spgist/spgvalidate.c +++ b/src/backend/access/spgist/spgvalidate.c @@ -3,7 +3,7 @@ * spgvalidate.c * Opclass validator for SP-GiST. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/spgist/spgxlog.c b/src/backend/access/spgist/spgxlog.c index b7986e6f7131e..55e8066a77b35 100644 --- a/src/backend/access/spgist/spgxlog.c +++ b/src/backend/access/spgist/spgxlog.c @@ -4,7 +4,7 @@ * WAL replay logic for SP-GiST * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -47,7 +47,7 @@ fillFakeState(SpGistState *state, spgxlogState stateSrc) * existing tuple, it had better be a placeholder tuple. */ static void -addOrReplaceTuple(Page page, Item tuple, int size, OffsetNumber offset) +addOrReplaceTuple(Page page, const void *tuple, int size, OffsetNumber offset) { if (offset <= PageGetMaxOffsetNumber(page)) { @@ -110,8 +110,7 @@ spgRedoAddLeaf(XLogReaderState *record) if (xldata->offnumLeaf != xldata->offnumHeadLeaf) { /* normal cases, tuple was added by SpGistPageAddNewItem */ - addOrReplaceTuple(page, (Item) leafTuple, leafTupleHdr.size, - xldata->offnumLeaf); + addOrReplaceTuple(page, leafTuple, leafTupleHdr.size, xldata->offnumLeaf); /* update head tuple's chain link if needed */ if (xldata->offnumHeadLeaf != InvalidOffsetNumber) @@ -129,7 +128,7 @@ spgRedoAddLeaf(XLogReaderState *record) /* replacing a DEAD tuple */ PageIndexTupleDelete(page, xldata->offnumLeaf); if (PageAddItem(page, - (Item) leafTuple, leafTupleHdr.size, + leafTuple, leafTupleHdr.size, xldata->offnumLeaf, false, false) != xldata->offnumLeaf) elog(ERROR, "failed to add item of size %u to SPGiST index page", leafTupleHdr.size); @@ -232,8 +231,7 @@ spgRedoMoveLeafs(XLogReaderState *record) memcpy(&leafTupleHdr, leafTuple, sizeof(SpGistLeafTupleData)); - addOrReplaceTuple(page, (Item) leafTuple, - leafTupleHdr.size, toInsert[i]); + addOrReplaceTuple(page, leafTuple, leafTupleHdr.size, toInsert[i]); ptr += leafTupleHdr.size; } @@ -309,7 +307,7 @@ spgRedoAddNode(XLogReaderState *record) page = BufferGetPage(buffer); PageIndexTupleDelete(page, xldata->offnum); - if (PageAddItem(page, (Item) innerTuple, innerTupleHdr.size, + if (PageAddItem(page, innerTuple, innerTupleHdr.size, xldata->offnum, false, false) != xldata->offnum) elog(ERROR, "failed to add item of size %u to SPGiST index page", @@ -351,8 +349,7 @@ spgRedoAddNode(XLogReaderState *record) { page = BufferGetPage(buffer); - addOrReplaceTuple(page, (Item) innerTuple, - innerTupleHdr.size, xldata->offnumNew); + addOrReplaceTuple(page, innerTuple, innerTupleHdr.size, xldata->offnumNew); /* * If parent is in this same page, update it now. @@ -390,7 +387,7 @@ spgRedoAddNode(XLogReaderState *record) xldata->offnumNew); PageIndexTupleDelete(page, xldata->offnum); - if (PageAddItem(page, (Item) dt, dt->size, + if (PageAddItem(page, dt, dt->size, xldata->offnum, false, false) != xldata->offnum) elog(ERROR, "failed to add item of size %u to SPGiST index page", @@ -492,8 +489,7 @@ spgRedoSplitTuple(XLogReaderState *record) { page = BufferGetPage(buffer); - addOrReplaceTuple(page, (Item) postfixTuple, - postfixTupleHdr.size, xldata->offnumPostfix); + addOrReplaceTuple(page, postfixTuple, postfixTupleHdr.size, xldata->offnumPostfix); PageSetLSN(page, lsn); MarkBufferDirty(buffer); @@ -508,15 +504,13 @@ spgRedoSplitTuple(XLogReaderState *record) page = BufferGetPage(buffer); PageIndexTupleDelete(page, xldata->offnumPrefix); - if (PageAddItem(page, (Item) prefixTuple, prefixTupleHdr.size, + if (PageAddItem(page, prefixTuple, prefixTupleHdr.size, xldata->offnumPrefix, false, false) != xldata->offnumPrefix) elog(ERROR, "failed to add item of size %u to SPGiST index page", prefixTupleHdr.size); if (xldata->postfixBlkSame) - addOrReplaceTuple(page, (Item) postfixTuple, - postfixTupleHdr.size, - xldata->offnumPostfix); + addOrReplaceTuple(page, postfixTuple, postfixTupleHdr.size, xldata->offnumPostfix); PageSetLSN(page, lsn); MarkBufferDirty(buffer); @@ -576,7 +570,7 @@ spgRedoPickSplit(XLogReaderState *record) { /* just re-init the source page */ srcBuffer = XLogInitBufferForRedo(record, 0); - srcPage = (Page) BufferGetPage(srcBuffer); + srcPage = BufferGetPage(srcBuffer); SpGistInitBuffer(srcBuffer, SPGIST_LEAF | (xldata->storesNulls ? SPGIST_NULLS : 0)); @@ -629,7 +623,7 @@ spgRedoPickSplit(XLogReaderState *record) { /* just re-init the dest page */ destBuffer = XLogInitBufferForRedo(record, 1); - destPage = (Page) BufferGetPage(destBuffer); + destPage = BufferGetPage(destBuffer); SpGistInitBuffer(destBuffer, SPGIST_LEAF | (xldata->storesNulls ? SPGIST_NULLS : 0)); @@ -642,7 +636,7 @@ spgRedoPickSplit(XLogReaderState *record) * full-page-image case, but for safety let's hold it till later. */ if (XLogReadBufferForRedo(record, 1, &destBuffer) == BLK_NEEDS_REDO) - destPage = (Page) BufferGetPage(destBuffer); + destPage = BufferGetPage(destBuffer); else destPage = NULL; /* don't do any page updates */ } @@ -662,8 +656,7 @@ spgRedoPickSplit(XLogReaderState *record) if (page == NULL) continue; /* no need to touch this page */ - addOrReplaceTuple(page, (Item) leafTuple, leafTupleHdr.size, - toInsert[i]); + addOrReplaceTuple(page, leafTuple, leafTupleHdr.size, toInsert[i]); } /* Now update src and dest page LSNs if needed */ @@ -692,8 +685,7 @@ spgRedoPickSplit(XLogReaderState *record) { page = BufferGetPage(innerBuffer); - addOrReplaceTuple(page, (Item) innerTuple, innerTupleHdr.size, - xldata->offnumInner); + addOrReplaceTuple(page, innerTuple, innerTupleHdr.size, xldata->offnumInner); /* if inner is also parent, update link while we're here */ if (xldata->innerIsParent) @@ -909,7 +901,7 @@ spgRedoVacuumRedirect(XLogReaderState *record) int max = PageGetMaxOffsetNumber(page); OffsetNumber *toDelete; - toDelete = palloc(sizeof(OffsetNumber) * max); + toDelete = palloc_array(OffsetNumber, max); for (i = xldata->firstPlaceholder; i <= max; i++) toDelete[i - xldata->firstPlaceholder] = i; diff --git a/src/backend/access/table/meson.build b/src/backend/access/table/meson.build index 36ad5fb835ad1..7c9e96e98746a 100644 --- a/src/backend/access/table/meson.build +++ b/src/backend/access/table/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'table.c', diff --git a/src/backend/access/table/table.c b/src/backend/access/table/table.c index be698bba0ed1b..3479b4633effd 100644 --- a/src/backend/access/table/table.c +++ b/src/backend/access/table/table.c @@ -3,7 +3,7 @@ * table.c * Generic routines for table related code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,7 +25,7 @@ #include "access/table.h" #include "utils/rel.h" -static inline void validate_relation_kind(Relation r); +static inline void validate_relation_as_table(Relation r); /* ---------------- * table_open - open a table relation by relation OID @@ -43,7 +43,7 @@ table_open(Oid relationId, LOCKMODE lockmode) r = relation_open(relationId, lockmode); - validate_relation_kind(r); + validate_relation_as_table(r); return r; } @@ -67,7 +67,7 @@ try_table_open(Oid relationId, LOCKMODE lockmode) if (!r) return NULL; - validate_relation_kind(r); + validate_relation_as_table(r); return r; } @@ -86,7 +86,7 @@ table_openrv(const RangeVar *relation, LOCKMODE lockmode) r = relation_openrv(relation, lockmode); - validate_relation_kind(r); + validate_relation_as_table(r); return r; } @@ -108,7 +108,7 @@ table_openrv_extended(const RangeVar *relation, LOCKMODE lockmode, r = relation_openrv_extended(relation, lockmode, missing_ok); if (r) - validate_relation_kind(r); + validate_relation_as_table(r); return r; } @@ -129,17 +129,22 @@ table_close(Relation relation, LOCKMODE lockmode) } /* ---------------- - * validate_relation_kind - check the relation's kind + * validate_relation_as_table * - * Make sure relkind is not index or composite type + * Make sure relkind is table-like, that is, something that could be read + * from or written to directly in a query. * ---------------- */ static inline void -validate_relation_kind(Relation r) +validate_relation_as_table(Relation r) { - if (r->rd_rel->relkind == RELKIND_INDEX || - r->rd_rel->relkind == RELKIND_PARTITIONED_INDEX || - r->rd_rel->relkind == RELKIND_COMPOSITE_TYPE) + if (r->rd_rel->relkind != RELKIND_RELATION && + r->rd_rel->relkind != RELKIND_SEQUENCE && + r->rd_rel->relkind != RELKIND_TOASTVALUE && + r->rd_rel->relkind != RELKIND_VIEW && + r->rd_rel->relkind != RELKIND_MATVIEW && + r->rd_rel->relkind != RELKIND_FOREIGN_TABLE && + r->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot open relation \"%s\"", diff --git a/src/backend/access/table/tableam.c b/src/backend/access/table/tableam.c index a56c5eceb14ad..68ff0966f1c57 100644 --- a/src/backend/access/table/tableam.c +++ b/src/backend/access/table/tableam.c @@ -3,7 +3,7 @@ * tableam.c * Table access method routines too big to be inline functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -11,7 +11,7 @@ * src/backend/access/table/tableam.c * * NOTES - * Note that most function in here are documented in tableam.h, rather than + * Note that most functions in here are documented in tableam.h, rather than * here. That's because there's a lot of inline functions in tableam.h and * it'd be harder to understand if one constantly had to switch between files. * @@ -110,15 +110,15 @@ table_slot_create(Relation relation, List **reglist) */ TableScanDesc -table_beginscan_catalog(Relation relation, int nkeys, struct ScanKeyData *key) +table_beginscan_catalog(Relation relation, int nkeys, ScanKeyData *key) { uint32 flags = SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE | SO_TEMP_SNAPSHOT; Oid relid = RelationGetRelid(relation); Snapshot snapshot = RegisterSnapshot(GetCatalogSnapshot(relid)); - return relation->rd_tableam->scan_begin(relation, snapshot, nkeys, key, - NULL, flags); + return table_beginscan_common(relation, snapshot, nkeys, key, + NULL, flags, SO_NONE); } @@ -163,10 +163,11 @@ table_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan, } TableScanDesc -table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan) +table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan, + uint32 flags) { Snapshot snapshot; - uint32 flags = SO_TYPE_SEQSCAN | + uint32 internal_flags = SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE; Assert(RelFileLocatorEquals(relation->rd_locator, pscan->phs_locator)); @@ -176,7 +177,38 @@ table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan) /* Snapshot was serialized -- restore it */ snapshot = RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off); RegisterSnapshot(snapshot); - flags |= SO_TEMP_SNAPSHOT; + internal_flags |= SO_TEMP_SNAPSHOT; + } + else + { + /* SnapshotAny passed by caller (not serialized) */ + snapshot = SnapshotAny; + } + + return table_beginscan_common(relation, snapshot, 0, NULL, + pscan, internal_flags, flags); +} + +TableScanDesc +table_beginscan_parallel_tidrange(Relation relation, + ParallelTableScanDesc pscan, + uint32 flags) +{ + Snapshot snapshot; + TableScanDesc sscan; + uint32 internal_flags = SO_TYPE_TIDRANGESCAN | SO_ALLOW_PAGEMODE; + + Assert(RelFileLocatorEquals(relation->rd_locator, pscan->phs_locator)); + + /* disable syncscan in parallel tid range scan. */ + pscan->phs_syncscan = false; + + if (!pscan->phs_snapshot_any) + { + /* Snapshot was serialized -- restore it */ + snapshot = RestoreSnapshot((char *) pscan + pscan->phs_snapshot_off); + RegisterSnapshot(snapshot); + internal_flags |= SO_TEMP_SNAPSHOT; } else { @@ -184,8 +216,9 @@ table_beginscan_parallel(Relation relation, ParallelTableScanDesc pscan) snapshot = SnapshotAny; } - return relation->rd_tableam->scan_begin(relation, snapshot, 0, NULL, - pscan, flags); + sscan = table_beginscan_common(relation, snapshot, 0, NULL, + pscan, internal_flags, flags); + return sscan; } @@ -217,7 +250,7 @@ table_index_fetch_tuple_check(Relation rel, bool found; slot = table_slot_create(rel, NULL); - scan = table_index_fetch_begin(rel); + scan = table_index_fetch_begin(rel, SO_NONE); found = table_index_fetch_tuple(scan, tid, snapshot, slot, &call_again, all_dead); table_index_fetch_end(scan); @@ -238,14 +271,6 @@ table_tuple_get_latest_tid(TableScanDesc scan, ItemPointer tid) Relation rel = scan->rs_rd; const TableAmRoutine *tableam = rel->rd_tableam; - /* - * We don't expect direct calls to table_tuple_get_latest_tid with valid - * CheckXidAlive for catalog or regular tables. See detailed comments in - * xact.c where these variables are declared. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected table_tuple_get_latest_tid call during logical decoding"); - /* * Since this can be called with user-supplied TID, don't trust the input * too much. @@ -295,9 +320,9 @@ simple_table_tuple_delete(Relation rel, ItemPointer tid, Snapshot snapshot) result = table_tuple_delete(rel, tid, GetCurrentCommandId(true), - snapshot, InvalidSnapshot, + 0, snapshot, InvalidSnapshot, true /* wait for commit */ , - &tmfd, false /* changingPart */ ); + &tmfd); switch (result) { @@ -344,7 +369,7 @@ simple_table_tuple_update(Relation rel, ItemPointer otid, result = table_tuple_update(rel, otid, slot, GetCurrentCommandId(true), - snapshot, InvalidSnapshot, + 0, snapshot, InvalidSnapshot, true /* wait for commit */ , &tmfd, &lockmode, update_indexes); @@ -398,6 +423,7 @@ table_block_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan) bpscan->phs_nblocks > NBuffers / 4; SpinLockInit(&bpscan->phs_mutex); bpscan->phs_startblock = InvalidBlockNumber; + bpscan->phs_numblock = InvalidBlockNumber; pg_atomic_init_u64(&bpscan->phs_nallocated, 0); return sizeof(ParallelBlockTableScanDescData); @@ -416,57 +442,59 @@ table_block_parallelscan_reinitialize(Relation rel, ParallelTableScanDesc pscan) * * Determine where the parallel seq scan should start. This function may be * called many times, once by each parallel worker. We must be careful only - * to set the startblock once. + * to set the phs_startblock and phs_numblock fields once. + * + * Callers may optionally specify a non-InvalidBlockNumber value for + * 'startblock' to force the scan to start at the given page. Likewise, + * 'numblocks' can be specified as a non-InvalidBlockNumber to limit the + * number of blocks to scan to that many blocks. */ void table_block_parallelscan_startblock_init(Relation rel, ParallelBlockTableScanWorker pbscanwork, - ParallelBlockTableScanDesc pbscan) + ParallelBlockTableScanDesc pbscan, + BlockNumber startblock, + BlockNumber numblocks) { + StaticAssertDecl(MaxBlockNumber <= 0xFFFFFFFE, + "pg_nextpower2_32 may be too small for non-standard BlockNumber width"); + BlockNumber sync_startpage = InvalidBlockNumber; + BlockNumber scan_nblocks; /* Reset the state we use for controlling allocation size. */ memset(pbscanwork, 0, sizeof(*pbscanwork)); - StaticAssertStmt(MaxBlockNumber <= 0xFFFFFFFE, - "pg_nextpower2_32 may be too small for non-standard BlockNumber width"); - - /* - * We determine the chunk size based on the size of the relation. First we - * split the relation into PARALLEL_SEQSCAN_NCHUNKS chunks but we then - * take the next highest power of 2 number of the chunk size. This means - * we split the relation into somewhere between PARALLEL_SEQSCAN_NCHUNKS - * and PARALLEL_SEQSCAN_NCHUNKS / 2 chunks. - */ - pbscanwork->phsw_chunk_size = pg_nextpower2_32(Max(pbscan->phs_nblocks / - PARALLEL_SEQSCAN_NCHUNKS, 1)); - - /* - * Ensure we don't go over the maximum chunk size with larger tables. This - * means we may get much more than PARALLEL_SEQSCAN_NCHUNKS for larger - * tables. Too large a chunk size has been shown to be detrimental to - * synchronous scan performance. - */ - pbscanwork->phsw_chunk_size = Min(pbscanwork->phsw_chunk_size, - PARALLEL_SEQSCAN_MAX_CHUNK_SIZE); - retry: /* Grab the spinlock. */ SpinLockAcquire(&pbscan->phs_mutex); /* - * If the scan's startblock has not yet been initialized, we must do so - * now. If this is not a synchronized scan, we just start at block 0, but - * if it is a synchronized scan, we must get the starting position from - * the synchronized scan machinery. We can't hold the spinlock while - * doing that, though, so release the spinlock, get the information we - * need, and retry. If nobody else has initialized the scan in the - * meantime, we'll fill in the value we fetched on the second time - * through. + * When the caller specified a limit on the number of blocks to scan, set + * that in the ParallelBlockTableScanDesc, if it's not been done by + * another worker already. + */ + if (numblocks != InvalidBlockNumber && + pbscan->phs_numblock == InvalidBlockNumber) + { + pbscan->phs_numblock = numblocks; + } + + /* + * If the scan's phs_startblock has not yet been initialized, we must do + * so now. If a startblock was specified, start there, otherwise if this + * is not a synchronized scan, we just start at block 0, but if it is a + * synchronized scan, we must get the starting position from the + * synchronized scan machinery. We can't hold the spinlock while doing + * that, though, so release the spinlock, get the information we need, and + * retry. If nobody else has initialized the scan in the meantime, we'll + * fill in the value we fetched on the second time through. */ if (pbscan->phs_startblock == InvalidBlockNumber) { - if (!pbscan->base.phs_syncscan) + if (startblock != InvalidBlockNumber) + pbscan->phs_startblock = startblock; + else if (!pbscan->base.phs_syncscan) pbscan->phs_startblock = 0; else if (sync_startpage != InvalidBlockNumber) pbscan->phs_startblock = sync_startpage; @@ -478,6 +506,34 @@ table_block_parallelscan_startblock_init(Relation rel, } } SpinLockRelease(&pbscan->phs_mutex); + + /* + * Figure out how many blocks we're going to scan; either all of them, or + * just phs_numblock's worth, if a limit has been imposed. + */ + if (pbscan->phs_numblock == InvalidBlockNumber) + scan_nblocks = pbscan->phs_nblocks; + else + scan_nblocks = pbscan->phs_numblock; + + /* + * We determine the chunk size based on scan_nblocks. First we split + * scan_nblocks into PARALLEL_SEQSCAN_NCHUNKS chunks then we calculate the + * next highest power of 2 number of the result. This means we split the + * blocks we're scanning into somewhere between PARALLEL_SEQSCAN_NCHUNKS + * and PARALLEL_SEQSCAN_NCHUNKS / 2 chunks. + */ + pbscanwork->phsw_chunk_size = pg_nextpower2_32(Max(scan_nblocks / + PARALLEL_SEQSCAN_NCHUNKS, 1)); + + /* + * Ensure we don't go over the maximum chunk size with larger tables. This + * means we may get much more than PARALLEL_SEQSCAN_NCHUNKS for larger + * tables. Too large a chunk size has been shown to be detrimental to + * sequential scan performance. + */ + pbscanwork->phsw_chunk_size = Min(pbscanwork->phsw_chunk_size, + PARALLEL_SEQSCAN_MAX_CHUNK_SIZE); } /* @@ -493,6 +549,7 @@ table_block_parallelscan_nextpage(Relation rel, ParallelBlockTableScanWorker pbscanwork, ParallelBlockTableScanDesc pbscan) { + BlockNumber scan_nblocks; BlockNumber page; uint64 nallocated; @@ -513,7 +570,7 @@ table_block_parallelscan_nextpage(Relation rel, * * Here we name these ranges of blocks "chunks". The initial size of * these chunks is determined in table_block_parallelscan_startblock_init - * based on the size of the relation. Towards the end of the scan, we + * based on the number of blocks to scan. Towards the end of the scan, we * start making reductions in the size of the chunks in order to attempt * to divide the remaining work over all the workers as evenly as * possible. @@ -530,17 +587,23 @@ table_block_parallelscan_nextpage(Relation rel, * phs_nallocated counter will exceed rs_nblocks, because workers will * still increment the value, when they try to allocate the next block but * all blocks have been allocated already. The counter must be 64 bits - * wide because of that, to avoid wrapping around when rs_nblocks is close - * to 2^32. + * wide because of that, to avoid wrapping around when scan_nblocks is + * close to 2^32. * * The actual block to return is calculated by adding the counter to the - * starting block number, modulo nblocks. + * starting block number, modulo phs_nblocks. */ + /* First, figure out how many blocks we're planning on scanning */ + if (pbscan->phs_numblock == InvalidBlockNumber) + scan_nblocks = pbscan->phs_nblocks; + else + scan_nblocks = pbscan->phs_numblock; + /* - * First check if we have any remaining blocks in a previous chunk for - * this worker. We must consume all of the blocks from that before we - * allocate a new chunk to the worker. + * Now check if we have any remaining blocks in a previous chunk for this + * worker. We must consume all of the blocks from that before we allocate + * a new chunk to the worker. */ if (pbscanwork->phsw_chunk_remaining > 0) { @@ -562,7 +625,7 @@ table_block_parallelscan_nextpage(Relation rel, * chunk size set to 1. */ if (pbscanwork->phsw_chunk_size > 1 && - pbscanwork->phsw_nallocated > pbscan->phs_nblocks - + pbscanwork->phsw_nallocated > scan_nblocks - (pbscanwork->phsw_chunk_size * PARALLEL_SEQSCAN_RAMPDOWN_CHUNKS)) pbscanwork->phsw_chunk_size >>= 1; @@ -577,7 +640,8 @@ table_block_parallelscan_nextpage(Relation rel, pbscanwork->phsw_chunk_remaining = pbscanwork->phsw_chunk_size - 1; } - if (nallocated >= pbscan->phs_nblocks) + /* Check if we've run out of blocks to scan */ + if (nallocated >= scan_nblocks) page = InvalidBlockNumber; /* all blocks have been allocated */ else page = (nallocated + pbscan->phs_startblock) % pbscan->phs_nblocks; diff --git a/src/backend/access/table/tableamapi.c b/src/backend/access/table/tableamapi.c index 476663b66aada..5450a27faebad 100644 --- a/src/backend/access/table/tableamapi.c +++ b/src/backend/access/table/tableamapi.c @@ -3,7 +3,7 @@ * tableamapi.c * Support routines for API for Postgres table access methods * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/table/tableamapi.c @@ -21,8 +21,7 @@ /* * GetTableAmRoutine * Call the specified access method handler routine to get its - * TableAmRoutine struct, which will be palloc'd in the caller's - * memory context. + * TableAmRoutine struct, which we expect to be statically allocated. */ const TableAmRoutine * GetTableAmRoutine(Oid amhandler) @@ -31,7 +30,7 @@ GetTableAmRoutine(Oid amhandler) const TableAmRoutine *routine; datum = OidFunctionCall0(amhandler); - routine = (TableAmRoutine *) DatumGetPointer(datum); + routine = (const TableAmRoutine *) DatumGetPointer(datum); if (routine == NULL || !IsA(routine, TableAmRoutine)) elog(ERROR, "table access method handler %u did not return a TableAmRoutine struct", diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c index b60fab0a4d294..2f2022d99510a 100644 --- a/src/backend/access/table/toast_helper.c +++ b/src/backend/access/table/toast_helper.c @@ -4,7 +4,7 @@ * Helper functions for table AMs implementing compressed or * out-of-line storage of varlena attributes. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/access/table/toast_helper.c @@ -49,8 +49,8 @@ toast_tuple_init(ToastTupleContext *ttc) for (i = 0; i < numAttrs; i++) { Form_pg_attribute att = TupleDescAttr(tupleDesc, i); - struct varlena *old_value; - struct varlena *new_value; + varlena *old_value; + varlena *new_value; ttc->ttc_attr[i].tai_colflags = 0; ttc->ttc_attr[i].tai_oldexternal = NULL; @@ -62,9 +62,9 @@ toast_tuple_init(ToastTupleContext *ttc) * For UPDATE get the old and new values of this attribute */ old_value = - (struct varlena *) DatumGetPointer(ttc->ttc_oldvalues[i]); + (varlena *) DatumGetPointer(ttc->ttc_oldvalues[i]); new_value = - (struct varlena *) DatumGetPointer(ttc->ttc_values[i]); + (varlena *) DatumGetPointer(ttc->ttc_values[i]); /* * If the old value is stored on disk, check if it has changed so @@ -102,7 +102,7 @@ toast_tuple_init(ToastTupleContext *ttc) /* * For INSERT simply get the new value */ - new_value = (struct varlena *) DatumGetPointer(ttc->ttc_values[i]); + new_value = (varlena *) DatumGetPointer(ttc->ttc_values[i]); } /* @@ -253,7 +253,7 @@ toast_tuple_try_compression(ToastTupleContext *ttc, int attribute) * Move an attribute to external storage. */ void -toast_tuple_externalize(ToastTupleContext *ttc, int attribute, int options) +toast_tuple_externalize(ToastTupleContext *ttc, int attribute, uint32 options) { Datum *value = &ttc->ttc_values[attribute]; Datum old_value = *value; @@ -330,7 +330,7 @@ toast_delete_external(Relation rel, const Datum *values, const bool *isnull, if (isnull[i]) continue; - else if (VARATT_IS_EXTERNAL_ONDISK(value)) + else if (VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(value))) toast_delete_datum(rel, value, is_speculative); } } diff --git a/src/backend/access/tablesample/bernoulli.c b/src/backend/access/tablesample/bernoulli.c index 5e1c5d2b7231a..7d8560464c807 100644 --- a/src/backend/access/tablesample/bernoulli.c +++ b/src/backend/access/tablesample/bernoulli.c @@ -13,7 +13,7 @@ * cutoff value computed from the selection probability by BeginSampleScan. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/access/tablesample/meson.build b/src/backend/access/tablesample/meson.build index b0124cde63a01..21bab6ac80851 100644 --- a/src/backend/access/tablesample/meson.build +++ b/src/backend/access/tablesample/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'bernoulli.c', diff --git a/src/backend/access/tablesample/system.c b/src/backend/access/tablesample/system.c index 8db813b89fc64..a2b9ba8eea9d2 100644 --- a/src/backend/access/tablesample/system.c +++ b/src/backend/access/tablesample/system.c @@ -13,7 +13,7 @@ * cutoff value computed from the selection probability by BeginSampleScan. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -129,7 +129,7 @@ system_samplescangetsamplesize(PlannerInfo *root, static void system_initsamplescan(SampleScanState *node, int eflags) { - node->tsm_state = palloc0(sizeof(SystemSamplerData)); + node->tsm_state = palloc0_object(SystemSamplerData); } /* diff --git a/src/backend/access/tablesample/tablesample.c b/src/backend/access/tablesample/tablesample.c index 5ad424cf8491f..b3c9cc69396aa 100644 --- a/src/backend/access/tablesample/tablesample.c +++ b/src/backend/access/tablesample/tablesample.c @@ -3,7 +3,7 @@ * tablesample.c * Support functions for TABLESAMPLE feature * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/transam/Makefile b/src/backend/access/transam/Makefile index 661c55a9db789..a32f473e0a22b 100644 --- a/src/backend/access/transam/Makefile +++ b/src/backend/access/transam/Makefile @@ -36,7 +36,8 @@ OBJS = \ xlogreader.o \ xlogrecovery.o \ xlogstats.o \ - xlogutils.o + xlogutils.o \ + xlogwait.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c index 48f10bec91e12..75012d4b8f01c 100644 --- a/src/backend/access/transam/clog.c +++ b/src/backend/access/transam/clog.c @@ -24,7 +24,7 @@ * for aborts (whether sync or async), since the post-crash assumption would * be that such transactions failed anyway. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/clog.c @@ -43,8 +43,10 @@ #include "pg_trace.h" #include "pgstat.h" #include "storage/proc.h" +#include "storage/subsystems.h" #include "storage/sync.h" #include "utils/guc_hooks.h" +#include "utils/wait_event.h" /* * Defines for CLOG page sizes. A page is the same BLCKSZ as is used @@ -105,14 +107,21 @@ TransactionIdToPage(TransactionId xid) /* * Link to shared-memory data structures for CLOG control */ -static SlruCtlData XactCtlData; +static void CLOGShmemRequest(void *arg); +static void CLOGShmemInit(void *arg); +static bool CLOGPagePrecedes(int64 page1, int64 page2); +static int clog_errdetail_for_io_error(const void *opaque_data); -#define XactCtl (&XactCtlData) +const ShmemCallbacks CLOGShmemCallbacks = { + .request_fn = CLOGShmemRequest, + .init_fn = CLOGShmemInit, +}; + +static SlruDesc XactSlruDesc; + +#define XactCtl (&XactSlruDesc) -static int ZeroCLOGPage(int64 pageno, bool writeXlog); -static bool CLOGPagePrecedes(int64 page1, int64 page2); -static void WriteZeroPageXlogRec(int64 pageno); static void WriteTruncateXlogRec(int64 pageno, TransactionId oldestXact, Oid oldestXactDb); static void TransactionIdSetPageStatus(TransactionId xid, int nsubxids, @@ -383,7 +392,7 @@ TransactionIdSetPageStatusInternal(TransactionId xid, int nsubxids, * write-busy, since we don't care if the update reaches disk sooner than * we think. */ - slotno = SimpleLruReadPage(XactCtl, pageno, XLogRecPtrIsInvalid(lsn), xid); + slotno = SimpleLruReadPage(XactCtl, pageno, !XLogRecPtrIsValid(lsn), &xid); /* * Set the main transaction id, if any. @@ -576,7 +585,7 @@ TransactionGroupUpdateXidStatus(TransactionId xid, XidStatus status, /* Walk the list and update the status of all XIDs. */ while (nextidx != INVALID_PROC_NUMBER) { - PGPROC *nextproc = &ProcGlobal->allProcs[nextidx]; + PGPROC *nextproc = GetPGProcByNumber(nextidx); int64 thispageno = nextproc->clogGroupMemberPage; /* @@ -635,7 +644,7 @@ TransactionGroupUpdateXidStatus(TransactionId xid, XidStatus status, */ while (wakeidx != INVALID_PROC_NUMBER) { - PGPROC *wakeproc = &ProcGlobal->allProcs[wakeidx]; + PGPROC *wakeproc = GetPGProcByNumber(wakeidx); wakeidx = pg_atomic_read_u32(&wakeproc->clogGroupNext); pg_atomic_write_u32(&wakeproc->clogGroupNext, INVALID_PROC_NUMBER); @@ -707,7 +716,7 @@ TransactionIdSetStatusBit(TransactionId xid, XidStatus status, XLogRecPtr lsn, i * recovery. After recovery completes the next clog change will set the * LSN correctly. */ - if (!XLogRecPtrIsInvalid(lsn)) + if (XLogRecPtrIsValid(lsn)) { int lsnindex = GetLSNIndex(slotno, xid); @@ -744,7 +753,7 @@ TransactionIdGetStatus(TransactionId xid, XLogRecPtr *lsn) /* lock is acquired by SimpleLruReadPage_ReadOnly */ - slotno = SimpleLruReadPage_ReadOnly(XactCtl, pageno, xid); + slotno = SimpleLruReadPage_ReadOnly(XactCtl, pageno, &xid); byteptr = XactCtl->shared->page_buffer[slotno] + byteno; status = (*byteptr >> bshift) & CLOG_XACT_BITMASK; @@ -775,16 +784,10 @@ CLOGShmemBuffers(void) } /* - * Initialization of shared memory for CLOG + * Register shared memory for CLOG */ -Size -CLOGShmemSize(void) -{ - return SimpleLruShmemSize(CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE); -} - -void -CLOGShmemInit(void) +static void +CLOGShmemRequest(void *arg) { /* If auto-tuning is requested, now is the time to do it */ if (transaction_buffers == 0) @@ -806,11 +809,26 @@ CLOGShmemInit(void) PGC_S_OVERRIDE); } Assert(transaction_buffers != 0); + SimpleLruRequest(.desc = &XactSlruDesc, + .name = "transaction", + .Dir = "pg_xact", + .long_segment_names = false, - XactCtl->PagePrecedes = CLOGPagePrecedes; - SimpleLruInit(XactCtl, "transaction", CLOGShmemBuffers(), CLOG_LSNS_PER_PAGE, - "pg_xact", LWTRANCHE_XACT_BUFFER, - LWTRANCHE_XACT_SLRU, SYNC_HANDLER_CLOG, false); + .nslots = CLOGShmemBuffers(), + .nlsns = CLOG_LSNS_PER_PAGE, + + .sync_handler = SYNC_HANDLER_CLOG, + .PagePrecedes = CLOGPagePrecedes, + .errdetail_for_io_error = clog_errdetail_for_io_error, + + .buffer_tranche_id = LWTRANCHE_XACT_BUFFER, + .bank_tranche_id = LWTRANCHE_XACT_SLRU, + ); +} + +static void +CLOGShmemInit(void *arg) +{ SlruPagePrecedesUnitTests(XactCtl, CLOG_XACTS_PER_PAGE); } @@ -832,41 +850,8 @@ check_transaction_buffers(int *newval, void **extra, GucSource source) void BootStrapCLOG(void) { - int slotno; - LWLock *lock = SimpleLruGetBankLock(XactCtl, 0); - - LWLockAcquire(lock, LW_EXCLUSIVE); - - /* Create and zero the first page of the commit log */ - slotno = ZeroCLOGPage(0, false); - - /* Make sure it's written out */ - SimpleLruWritePage(XactCtl, slotno); - Assert(!XactCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); -} - -/* - * Initialize (or reinitialize) a page of CLOG to zeroes. - * If writeXlog is true, also emit an XLOG record saying we did this. - * - * The page is not actually written, just set up in shared memory. - * The slot number of the new page is returned. - * - * Control lock must be held at entry, and will be held at exit. - */ -static int -ZeroCLOGPage(int64 pageno, bool writeXlog) -{ - int slotno; - - slotno = SimpleLruZeroPage(XactCtl, pageno); - - if (writeXlog) - WriteZeroPageXlogRec(pageno); - - return slotno; + /* Zero the initial page and flush it to disk */ + SimpleLruZeroAndWritePage(XactCtl, 0); } /* @@ -916,7 +901,7 @@ TrimCLOG(void) int slotno; char *byteptr; - slotno = SimpleLruReadPage(XactCtl, pageno, false, xid); + slotno = SimpleLruReadPage(XactCtl, pageno, false, &xid); byteptr = XactCtl->shared->page_buffer[slotno] + byteno; /* Zero so-far-unused positions in the current byte */ @@ -974,8 +959,9 @@ ExtendCLOG(TransactionId newestXact) LWLockAcquire(lock, LW_EXCLUSIVE); - /* Zero the page and make an XLOG entry about it */ - ZeroCLOGPage(pageno, true); + /* Zero the page and make a WAL entry about it */ + SimpleLruZeroPage(XactCtl, pageno); + XLogSimpleInsertInt64(RM_CLOG_ID, CLOG_ZEROPAGE, pageno); LWLockRelease(lock); } @@ -1066,18 +1052,15 @@ CLOGPagePrecedes(int64 page1, int64 page2) TransactionIdPrecedes(xid1, xid2 + CLOG_XACTS_PER_PAGE - 1)); } - -/* - * Write a ZEROPAGE xlog record - */ -static void -WriteZeroPageXlogRec(int64 pageno) +static int +clog_errdetail_for_io_error(const void *opaque_data) { - XLogBeginInsert(); - XLogRegisterData(&pageno, sizeof(pageno)); - (void) XLogInsert(RM_CLOG_ID, CLOG_ZEROPAGE); + TransactionId xid = *(const TransactionId *) opaque_data; + + return errdetail("Could not access commit status of transaction %u.", xid); } + /* * Write a TRUNCATE xlog record * @@ -1114,19 +1097,9 @@ clog_redo(XLogReaderState *record) if (info == CLOG_ZEROPAGE) { int64 pageno; - int slotno; - LWLock *lock; memcpy(&pageno, XLogRecGetData(record), sizeof(pageno)); - - lock = SimpleLruGetBankLock(XactCtl, pageno); - LWLockAcquire(lock, LW_EXCLUSIVE); - - slotno = ZeroCLOGPage(pageno, false); - SimpleLruWritePage(XactCtl, slotno); - Assert(!XactCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); + SimpleLruZeroAndWritePage(XactCtl, pageno); } else if (info == CLOG_TRUNCATE) { diff --git a/src/backend/access/transam/commit_ts.c b/src/backend/access/transam/commit_ts.c index 113fae1437ad8..9e6fd5d465722 100644 --- a/src/backend/access/transam/commit_ts.c +++ b/src/backend/access/transam/commit_ts.c @@ -12,7 +12,7 @@ * XLOG records for these events and will re-perform the status update on * redo; so we need make no additional XLOG entry here. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/commit_ts.c @@ -30,6 +30,7 @@ #include "funcapi.h" #include "miscadmin.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/fmgrprotos.h" #include "utils/guc_hooks.h" #include "utils/timestamp.h" @@ -54,11 +55,11 @@ typedef struct CommitTimestampEntry { TimestampTz time; - RepOriginId nodeid; + ReplOriginId nodeid; } CommitTimestampEntry; #define SizeOfCommitTimestampEntry (offsetof(CommitTimestampEntry, nodeid) + \ - sizeof(RepOriginId)) + sizeof(ReplOriginId)) #define COMMIT_TS_XACTS_PER_PAGE \ (BLCKSZ / SizeOfCommitTimestampEntry) @@ -80,9 +81,19 @@ TransactionIdToCTsPage(TransactionId xid) /* * Link to shared-memory data structures for CommitTs control */ -static SlruCtlData CommitTsCtlData; +static void CommitTsShmemRequest(void *arg); +static void CommitTsShmemInit(void *arg); +static bool CommitTsPagePrecedes(int64 page1, int64 page2); +static int commit_ts_errdetail_for_io_error(const void *opaque_data); + +const ShmemCallbacks CommitTsShmemCallbacks = { + .request_fn = CommitTsShmemRequest, + .init_fn = CommitTsShmemInit, +}; -#define CommitTsCtl (&CommitTsCtlData) +static SlruDesc CommitTsSlruDesc; + +#define CommitTsCtl (&CommitTsSlruDesc) /* * We keep a cache of the last value set in shared memory. @@ -104,21 +115,19 @@ typedef struct CommitTimestampShared static CommitTimestampShared *commitTsShared; +static void CommitTsShmemInit(void *arg); /* GUC variable */ bool track_commit_timestamp; static void SetXidCommitTsInPage(TransactionId xid, int nsubxids, TransactionId *subxids, TimestampTz ts, - RepOriginId nodeid, int64 pageno); + ReplOriginId nodeid, int64 pageno); static void TransactionIdSetCommitTs(TransactionId xid, TimestampTz ts, - RepOriginId nodeid, int slotno); + ReplOriginId nodeid, int slotno); static void error_commit_ts_disabled(void); -static int ZeroCommitTsPage(int64 pageno, bool writeXlog); -static bool CommitTsPagePrecedes(int64 page1, int64 page2); static void ActivateCommitTs(void); static void DeactivateCommitTs(void); -static void WriteZeroPageXlogRec(int64 pageno); static void WriteTruncateXlogRec(int64 pageno, TransactionId oldestXid); /* @@ -140,7 +149,7 @@ static void WriteTruncateXlogRec(int64 pageno, TransactionId oldestXid); void TransactionTreeSetCommitTsData(TransactionId xid, int nsubxids, TransactionId *subxids, TimestampTz timestamp, - RepOriginId nodeid) + ReplOriginId nodeid) { int i; TransactionId headxid; @@ -221,7 +230,7 @@ TransactionTreeSetCommitTsData(TransactionId xid, int nsubxids, static void SetXidCommitTsInPage(TransactionId xid, int nsubxids, TransactionId *subxids, TimestampTz ts, - RepOriginId nodeid, int64 pageno) + ReplOriginId nodeid, int64 pageno) { LWLock *lock = SimpleLruGetBankLock(CommitTsCtl, pageno); int slotno; @@ -229,7 +238,7 @@ SetXidCommitTsInPage(TransactionId xid, int nsubxids, LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = SimpleLruReadPage(CommitTsCtl, pageno, true, xid); + slotno = SimpleLruReadPage(CommitTsCtl, pageno, true, &xid); TransactionIdSetCommitTs(xid, ts, nodeid, slotno); for (i = 0; i < nsubxids; i++) @@ -247,7 +256,7 @@ SetXidCommitTsInPage(TransactionId xid, int nsubxids, */ static void TransactionIdSetCommitTs(TransactionId xid, TimestampTz ts, - RepOriginId nodeid, int slotno) + ReplOriginId nodeid, int slotno) { int entryno = TransactionIdToCTsEntry(xid); CommitTimestampEntry entry; @@ -272,7 +281,7 @@ TransactionIdSetCommitTs(TransactionId xid, TimestampTz ts, */ bool TransactionIdGetCommitTsData(TransactionId xid, TimestampTz *ts, - RepOriginId *nodeid) + ReplOriginId *nodeid) { int64 pageno = TransactionIdToCTsPage(xid); int entryno = TransactionIdToCTsEntry(xid); @@ -290,7 +299,7 @@ TransactionIdGetCommitTsData(TransactionId xid, TimestampTz *ts, /* frozen and bootstrap xids are always committed far in the past */ *ts = 0; if (nodeid) - *nodeid = 0; + *nodeid = InvalidReplOriginId; return false; } @@ -329,12 +338,12 @@ TransactionIdGetCommitTsData(TransactionId xid, TimestampTz *ts, { *ts = 0; if (nodeid) - *nodeid = InvalidRepOriginId; + *nodeid = InvalidReplOriginId; return false; } /* lock is acquired by SimpleLruReadPage_ReadOnly */ - slotno = SimpleLruReadPage_ReadOnly(CommitTsCtl, pageno, xid); + slotno = SimpleLruReadPage_ReadOnly(CommitTsCtl, pageno, &xid); memcpy(&entry, CommitTsCtl->shared->page_buffer[slotno] + SizeOfCommitTimestampEntry * entryno, @@ -357,7 +366,7 @@ TransactionIdGetCommitTsData(TransactionId xid, TimestampTz *ts, * as NULL if not wanted. */ TransactionId -GetLatestCommitTsData(TimestampTz *ts, RepOriginId *nodeid) +GetLatestCommitTsData(TimestampTz *ts, ReplOriginId *nodeid) { TransactionId xid; @@ -420,7 +429,7 @@ Datum pg_last_committed_xact(PG_FUNCTION_ARGS) { TransactionId xid; - RepOriginId nodeid; + ReplOriginId nodeid; TimestampTz ts; Datum values[3]; bool nulls[3]; @@ -464,7 +473,7 @@ Datum pg_xact_commit_timestamp_origin(PG_FUNCTION_ARGS) { TransactionId xid = PG_GETARG_TRANSACTIONID(0); - RepOriginId nodeid; + ReplOriginId nodeid; TimestampTz ts; Datum values[2]; bool nulls[2]; @@ -513,24 +522,12 @@ CommitTsShmemBuffers(void) } /* - * Shared memory sizing for CommitTs - */ -Size -CommitTsShmemSize(void) -{ - return SimpleLruShmemSize(CommitTsShmemBuffers(), 0) + - sizeof(CommitTimestampShared); -} - -/* - * Initialize CommitTs at system startup (postmaster start or standalone - * backend) + * Register CommitTs shared memory needs at system startup (postmaster start + * or standalone backend) */ -void -CommitTsShmemInit(void) +static void +CommitTsShmemRequest(void *arg) { - bool found; - /* If auto-tuning is requested, now is the time to do it */ if (commit_timestamp_buffers == 0) { @@ -551,30 +548,36 @@ CommitTsShmemInit(void) PGC_S_OVERRIDE); } Assert(commit_timestamp_buffers != 0); + SimpleLruRequest(.desc = &CommitTsSlruDesc, + .name = "commit_timestamp", + .Dir = "pg_commit_ts", + .long_segment_names = false, - CommitTsCtl->PagePrecedes = CommitTsPagePrecedes; - SimpleLruInit(CommitTsCtl, "commit_timestamp", CommitTsShmemBuffers(), 0, - "pg_commit_ts", LWTRANCHE_COMMITTS_BUFFER, - LWTRANCHE_COMMITTS_SLRU, - SYNC_HANDLER_COMMIT_TS, - false); - SlruPagePrecedesUnitTests(CommitTsCtl, COMMIT_TS_XACTS_PER_PAGE); + .nslots = CommitTsShmemBuffers(), - commitTsShared = ShmemInitStruct("CommitTs shared", - sizeof(CommitTimestampShared), - &found); + .PagePrecedes = CommitTsPagePrecedes, + .errdetail_for_io_error = commit_ts_errdetail_for_io_error, - if (!IsUnderPostmaster) - { - Assert(!found); + .sync_handler = SYNC_HANDLER_COMMIT_TS, + .buffer_tranche_id = LWTRANCHE_COMMITTS_BUFFER, + .bank_tranche_id = LWTRANCHE_COMMITTS_SLRU, + ); - commitTsShared->xidLastCommit = InvalidTransactionId; - TIMESTAMP_NOBEGIN(commitTsShared->dataLastCommit.time); - commitTsShared->dataLastCommit.nodeid = InvalidRepOriginId; - commitTsShared->commitTsActive = false; - } - else - Assert(found); + ShmemRequestStruct(.name = "CommitTs shared", + .size = sizeof(CommitTimestampShared), + .ptr = (void **) &commitTsShared, + ); +} + +static void +CommitTsShmemInit(void *arg) +{ + commitTsShared->xidLastCommit = InvalidTransactionId; + TIMESTAMP_NOBEGIN(commitTsShared->dataLastCommit.time); + commitTsShared->dataLastCommit.nodeid = InvalidReplOriginId; + commitTsShared->commitTsActive = false; + + SlruPagePrecedesUnitTests(CommitTsCtl, COMMIT_TS_XACTS_PER_PAGE); } /* @@ -602,28 +605,6 @@ BootStrapCommitTs(void) */ } -/* - * Initialize (or reinitialize) a page of CommitTs to zeroes. - * If writeXlog is true, also emit an XLOG record saying we did this. - * - * The page is not actually written, just set up in shared memory. - * The slot number of the new page is returned. - * - * Control lock must be held at entry, and will be held at exit. - */ -static int -ZeroCommitTsPage(int64 pageno, bool writeXlog) -{ - int slotno; - - slotno = SimpleLruZeroPage(CommitTsCtl, pageno); - - if (writeXlog) - WriteZeroPageXlogRec(pageno); - - return slotno; -} - /* * This must be called ONCE during postmaster or standalone-backend startup, * after StartupXLOG has initialized TransamVariables->nextXid. @@ -707,6 +688,13 @@ ActivateCommitTs(void) TransactionId xid; int64 pageno; + /* + * During bootstrap, we should not register commit timestamps so skip the + * activation in this case. + */ + if (IsBootstrapProcessingMode()) + return; + /* If we've done this already, there's nothing to do */ LWLockAcquire(CommitTsLock, LW_EXCLUSIVE); if (commitTsShared->commitTsActive) @@ -747,16 +735,7 @@ ActivateCommitTs(void) /* Create the current segment file, if necessary */ if (!SimpleLruDoesPhysicalPageExist(CommitTsCtl, pageno)) - { - LWLock *lock = SimpleLruGetBankLock(CommitTsCtl, pageno); - int slotno; - - LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = ZeroCommitTsPage(pageno, false); - SimpleLruWritePage(CommitTsCtl, slotno); - Assert(!CommitTsCtl->shared->page_dirty[slotno]); - LWLockRelease(lock); - } + SimpleLruZeroAndWritePage(CommitTsCtl, pageno); /* Change the activation status in shared memory. */ LWLockAcquire(CommitTsLock, LW_EXCLUSIVE); @@ -789,7 +768,7 @@ DeactivateCommitTs(void) commitTsShared->commitTsActive = false; commitTsShared->xidLastCommit = InvalidTransactionId; TIMESTAMP_NOBEGIN(commitTsShared->dataLastCommit.time); - commitTsShared->dataLastCommit.nodeid = InvalidRepOriginId; + commitTsShared->dataLastCommit.nodeid = InvalidReplOriginId; TransamVariables->oldestCommitTsXid = InvalidTransactionId; TransamVariables->newestCommitTsXid = InvalidTransactionId; @@ -867,8 +846,12 @@ ExtendCommitTs(TransactionId newestXact) LWLockAcquire(lock, LW_EXCLUSIVE); - /* Zero the page and make an XLOG entry about it */ - ZeroCommitTsPage(pageno, !InRecovery); + /* Zero the page ... */ + SimpleLruZeroPage(CommitTsCtl, pageno); + + /* and make a WAL entry about that, unless we're in REDO */ + if (!InRecovery) + XLogSimpleInsertInt64(RM_COMMIT_TS_ID, COMMIT_TS_ZEROPAGE, pageno); LWLockRelease(lock); } @@ -981,16 +964,12 @@ CommitTsPagePrecedes(int64 page1, int64 page2) TransactionIdPrecedes(xid1, xid2 + COMMIT_TS_XACTS_PER_PAGE - 1)); } - -/* - * Write a ZEROPAGE xlog record - */ -static void -WriteZeroPageXlogRec(int64 pageno) +static int +commit_ts_errdetail_for_io_error(const void *opaque_data) { - XLogBeginInsert(); - XLogRegisterData(&pageno, sizeof(pageno)); - (void) XLogInsert(RM_COMMIT_TS_ID, COMMIT_TS_ZEROPAGE); + TransactionId xid = *(const TransactionId *) opaque_data; + + return errdetail("Could not access commit timestamp of transaction %u.", xid); } /* @@ -1023,19 +1002,9 @@ commit_ts_redo(XLogReaderState *record) if (info == COMMIT_TS_ZEROPAGE) { int64 pageno; - int slotno; - LWLock *lock; memcpy(&pageno, XLogRecGetData(record), sizeof(pageno)); - - lock = SimpleLruGetBankLock(CommitTsCtl, pageno); - LWLockAcquire(lock, LW_EXCLUSIVE); - - slotno = ZeroCommitTsPage(pageno, false); - SimpleLruWritePage(CommitTsCtl, slotno); - Assert(!CommitTsCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); + SimpleLruZeroAndWritePage(CommitTsCtl, pageno); } else if (info == COMMIT_TS_TRUNCATE) { diff --git a/src/backend/access/transam/generic_xlog.c b/src/backend/access/transam/generic_xlog.c index 2ce11e96af0dd..7f82186d0d6ee 100644 --- a/src/backend/access/transam/generic_xlog.c +++ b/src/backend/access/transam/generic_xlog.c @@ -4,7 +4,7 @@ * Implementation of generic xlog records. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/generic_xlog.c diff --git a/src/backend/access/transam/meson.build b/src/backend/access/transam/meson.build index e8ae9b13c8e49..06aadc7f315fb 100644 --- a/src/backend/access/transam/meson.build +++ b/src/backend/access/transam/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'clog.c', @@ -24,6 +24,7 @@ backend_sources += files( 'xlogrecovery.c', 'xlogstats.c', 'xlogutils.c', + 'xlogwait.c', ) # used by frontend programs to build a frontend xlogreader diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index 3c06ac45532f8..10cbc0d76bd7a 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -59,7 +59,7 @@ * counter does not fall within the wraparound horizon considering the global * minimum value. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/multixact.c @@ -69,17 +69,13 @@ #include "postgres.h" #include "access/multixact.h" +#include "access/multixact_internal.h" #include "access/slru.h" -#include "access/transam.h" #include "access/twophase.h" #include "access/twophase_rmgr.h" -#include "access/xact.h" #include "access/xlog.h" #include "access/xloginsert.h" #include "access/xlogutils.h" -#include "commands/dbcommands.h" -#include "funcapi.h" -#include "lib/ilist.h" #include "miscadmin.h" #include "pg_trace.h" #include "pgstat.h" @@ -87,136 +83,28 @@ #include "storage/pmsignal.h" #include "storage/proc.h" #include "storage/procarray.h" -#include "utils/fmgrprotos.h" +#include "storage/subsystems.h" #include "utils/guc_hooks.h" #include "utils/injection_point.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" /* - * Defines for MultiXactOffset page sizes. A page is the same BLCKSZ as is - * used everywhere else in Postgres. - * - * Note: because MultiXactOffsets are 32 bits and wrap around at 0xFFFFFFFF, - * MultiXact page numbering also wraps around at - * 0xFFFFFFFF/MULTIXACT_OFFSETS_PER_PAGE, and segment numbering at - * 0xFFFFFFFF/MULTIXACT_OFFSETS_PER_PAGE/SLRU_PAGES_PER_SEGMENT. We need - * take no explicit notice of that fact in this module, except when comparing - * segment and page numbers in TruncateMultiXact (see - * MultiXactOffsetPagePrecedes). - */ - -/* We need four bytes per offset */ -#define MULTIXACT_OFFSETS_PER_PAGE (BLCKSZ / sizeof(MultiXactOffset)) - -static inline int64 -MultiXactIdToOffsetPage(MultiXactId multi) -{ - return multi / MULTIXACT_OFFSETS_PER_PAGE; -} - -static inline int -MultiXactIdToOffsetEntry(MultiXactId multi) -{ - return multi % MULTIXACT_OFFSETS_PER_PAGE; -} - -static inline int64 -MultiXactIdToOffsetSegment(MultiXactId multi) -{ - return MultiXactIdToOffsetPage(multi) / SLRU_PAGES_PER_SEGMENT; -} - -/* - * The situation for members is a bit more complex: we store one byte of - * additional flag bits for each TransactionId. To do this without getting - * into alignment issues, we store four bytes of flags, and then the - * corresponding 4 Xids. Each such 5-word (20-byte) set we call a "group", and - * are stored as a whole in pages. Thus, with 8kB BLCKSZ, we keep 409 groups - * per page. This wastes 12 bytes per page, but that's OK -- simplicity (and - * performance) trumps space efficiency here. - * - * Note that the "offset" macros work with byte offset, not array indexes, so - * arithmetic must be done using "char *" pointers. + * Thresholds used to keep members disk usage in check when multixids have a + * lot of members. When MULTIXACT_MEMBER_LOW_THRESHOLD is reached, vacuum + * starts freezing multixids more aggressively, even if the normal multixid + * age limits haven't been reached yet. */ -/* We need eight bits per xact, so one xact fits in a byte */ -#define MXACT_MEMBER_BITS_PER_XACT 8 -#define MXACT_MEMBER_FLAGS_PER_BYTE 1 -#define MXACT_MEMBER_XACT_BITMASK ((1 << MXACT_MEMBER_BITS_PER_XACT) - 1) - -/* how many full bytes of flags are there in a group? */ -#define MULTIXACT_FLAGBYTES_PER_GROUP 4 -#define MULTIXACT_MEMBERS_PER_MEMBERGROUP \ - (MULTIXACT_FLAGBYTES_PER_GROUP * MXACT_MEMBER_FLAGS_PER_BYTE) -/* size in bytes of a complete group */ -#define MULTIXACT_MEMBERGROUP_SIZE \ - (sizeof(TransactionId) * MULTIXACT_MEMBERS_PER_MEMBERGROUP + MULTIXACT_FLAGBYTES_PER_GROUP) -#define MULTIXACT_MEMBERGROUPS_PER_PAGE (BLCKSZ / MULTIXACT_MEMBERGROUP_SIZE) -#define MULTIXACT_MEMBERS_PER_PAGE \ - (MULTIXACT_MEMBERGROUPS_PER_PAGE * MULTIXACT_MEMBERS_PER_MEMBERGROUP) - -/* - * Because the number of items per page is not a divisor of the last item - * number (member 0xFFFFFFFF), the last segment does not use the maximum number - * of pages, and moreover the last used page therein does not use the same - * number of items as previous pages. (Another way to say it is that the - * 0xFFFFFFFF member is somewhere in the middle of the last page, so the page - * has some empty space after that item.) - * - * This constant is the number of members in the last page of the last segment. - */ -#define MAX_MEMBERS_IN_LAST_MEMBERS_PAGE \ - ((uint32) ((0xFFFFFFFF % MULTIXACT_MEMBERS_PER_PAGE) + 1)) - -/* page in which a member is to be found */ -static inline int64 -MXOffsetToMemberPage(MultiXactOffset offset) -{ - return offset / MULTIXACT_MEMBERS_PER_PAGE; -} - -static inline int64 -MXOffsetToMemberSegment(MultiXactOffset offset) -{ - return MXOffsetToMemberPage(offset) / SLRU_PAGES_PER_SEGMENT; -} - -/* Location (byte offset within page) of flag word for a given member */ -static inline int -MXOffsetToFlagsOffset(MultiXactOffset offset) -{ - MultiXactOffset group = offset / MULTIXACT_MEMBERS_PER_MEMBERGROUP; - int grouponpg = group % MULTIXACT_MEMBERGROUPS_PER_PAGE; - int byteoff = grouponpg * MULTIXACT_MEMBERGROUP_SIZE; - - return byteoff; -} - -static inline int -MXOffsetToFlagsBitShift(MultiXactOffset offset) -{ - int member_in_group = offset % MULTIXACT_MEMBERS_PER_MEMBERGROUP; - int bshift = member_in_group * MXACT_MEMBER_BITS_PER_XACT; +#define MULTIXACT_MEMBER_LOW_THRESHOLD UINT64CONST(2000000000) +#define MULTIXACT_MEMBER_HIGH_THRESHOLD UINT64CONST(4000000000) - return bshift; -} - -/* Location (byte offset within page) of TransactionId of given member */ -static inline int -MXOffsetToMemberOffset(MultiXactOffset offset) +static inline MultiXactId +NextMultiXactId(MultiXactId multi) { - int member_in_group = offset % MULTIXACT_MEMBERS_PER_MEMBERGROUP; - - return MXOffsetToFlagsOffset(offset) + - MULTIXACT_FLAGBYTES_PER_GROUP + - member_in_group * sizeof(TransactionId); + return multi == MaxMultiXactId ? FirstMultiXactId : multi + 1; } -/* Multixact members wraparound thresholds. */ -#define MULTIXACT_MEMBER_SAFE_THRESHOLD (MaxMultiXactOffset / 2) -#define MULTIXACT_MEMBER_DANGER_THRESHOLD \ - (MaxMultiXactOffset - MaxMultiXactOffset / 4) - static inline MultiXactId PreviousMultiXactId(MultiXactId multi) { @@ -226,11 +114,16 @@ PreviousMultiXactId(MultiXactId multi) /* * Links to shared-memory data structures for MultiXact control */ -static SlruCtlData MultiXactOffsetCtlData; -static SlruCtlData MultiXactMemberCtlData; +static bool MultiXactOffsetPagePrecedes(int64 page1, int64 page2); +static int MultiXactOffsetIoErrorDetail(const void *opaque_data); +static bool MultiXactMemberPagePrecedes(int64 page1, int64 page2); +static int MultiXactMemberIoErrorDetail(const void *opaque_data); + +static SlruDesc MultiXactOffsetSlruDesc; +static SlruDesc MultiXactMemberSlruDesc; -#define MultiXactOffsetCtl (&MultiXactOffsetCtlData) -#define MultiXactMemberCtl (&MultiXactMemberCtlData) +#define MultiXactOffsetCtl (&MultiXactOffsetSlruDesc) +#define MultiXactMemberCtl (&MultiXactMemberSlruDesc) /* * MultiXact state shared across all backends. All this state is protected @@ -260,11 +153,9 @@ typedef struct MultiXactStateData /* * Oldest multixact offset that is potentially referenced by a multixact - * referenced by a relation. We don't always know this value, so there's - * a flag here to indicate whether or not we currently do. + * referenced by a relation. */ MultiXactOffset oldestOffset; - bool oldestOffsetKnown; /* support for anti-wraparound measures */ MultiXactId multiVacLimit; @@ -272,23 +163,9 @@ typedef struct MultiXactStateData MultiXactId multiStopLimit; MultiXactId multiWrapLimit; - /* support for members anti-wraparound measures */ - MultiXactOffset offsetStopLimit; /* known if oldestOffsetKnown */ - - /* - * This is used to sleep until a multixact offset is written when we want - * to create the next one. - */ - ConditionVariable nextoff_cv; - /* * Per-backend data starts here. We have two arrays stored in the area - * immediately following the MultiXactStateData struct. Each is indexed by - * ProcNumber. - * - * In both arrays, there's a slot for all normal backends - * (0..MaxBackends-1) followed by a slot for max_prepared_xacts prepared - * transactions. + * immediately following the MultiXactStateData struct: * * OldestMemberMXactId[k] is the oldest MultiXactId each backend's current * transaction(s) could possibly be a member of, or InvalidMultiXactId @@ -300,6 +177,10 @@ typedef struct MultiXactStateData * member of a MultiXact, and that MultiXact would have to be created * during or after the lock acquisition.) * + * In the OldestMemberMXactId array, there's a slot for all normal + * backends (0..MaxBackends-1) followed by a slot for max_prepared_xacts + * prepared transactions. + * * OldestVisibleMXactId[k] is the oldest MultiXactId each backend's * current transaction(s) think is potentially live, or InvalidMultiXactId * when not in a transaction or not in a transaction that's paid any @@ -311,6 +192,9 @@ typedef struct MultiXactStateData * than its own OldestVisibleMXactId[] setting; this is necessary because * the relevant SLRU data can be concurrently truncated away. * + * In the OldestVisibleMXactId array, there's a slot for all normal + * backends (0..MaxBackends-1) only. No slots for prepared transactions. + * * The oldest valid value among all of the OldestMemberMXactId[] and * OldestVisibleMXactId[] entries is considered by vacuum as the earliest * possible value still having any live member transaction -- OldestMxact. @@ -332,15 +216,60 @@ typedef struct MultiXactStateData } MultiXactStateData; /* - * Size of OldestMemberMXactId and OldestVisibleMXactId arrays. + * Sizes of OldestMemberMXactId and OldestVisibleMXactId arrays. */ -#define MaxOldestSlot (MaxBackends + max_prepared_xacts) +#define NumMemberSlots (MaxBackends + max_prepared_xacts) +#define NumVisibleSlots MaxBackends /* Pointers to the state data in shared memory */ static MultiXactStateData *MultiXactState; static MultiXactId *OldestMemberMXactId; static MultiXactId *OldestVisibleMXactId; +static void MultiXactShmemRequest(void *arg); +static void MultiXactShmemInit(void *arg); +static void MultiXactShmemAttach(void *arg); + +const ShmemCallbacks MultiXactShmemCallbacks = { + .request_fn = MultiXactShmemRequest, + .init_fn = MultiXactShmemInit, + .attach_fn = MultiXactShmemAttach, +}; + +static inline MultiXactId * +MyOldestMemberMXactIdSlot(void) +{ + /* + * The first MaxBackends entries in the OldestMemberMXactId array are + * reserved for regular backends. MyProcNumber should index into one of + * them. + */ + Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends); + return &OldestMemberMXactId[MyProcNumber]; +} + +static inline MultiXactId * +PreparedXactOldestMemberMXactIdSlot(ProcNumber procno) +{ + int prepared_xact_idx; + + Assert(procno >= FIRST_PREPARED_XACT_PROC_NUMBER); + prepared_xact_idx = procno - FIRST_PREPARED_XACT_PROC_NUMBER; + + /* + * The first MaxBackends entries in the OldestMemberMXactId array are + * reserved for regular backends. Prepared xacts come after them. + */ + Assert(MaxBackends + prepared_xact_idx < NumMemberSlots); + return &OldestMemberMXactId[MaxBackends + prepared_xact_idx]; +} + +static inline MultiXactId * +MyOldestVisibleMXactIdSlot(void) +{ + Assert(MyProcNumber >= 0 && MyProcNumber < NumVisibleSlots); + return &OldestVisibleMXactId[MyProcNumber]; +} /* * Definitions for the backend-local MultiXactId cache. @@ -398,27 +327,22 @@ static int mXactCacheGetById(MultiXactId multi, MultiXactMember **members); static void mXactCachePut(MultiXactId multi, int nmembers, MultiXactMember *members); -static char *mxstatus_to_string(MultiXactStatus status); - /* management of SLRU infrastructure */ -static int ZeroMultiXactOffsetPage(int64 pageno, bool writeXlog); -static int ZeroMultiXactMemberPage(int64 pageno, bool writeXlog); -static bool MultiXactOffsetPagePrecedes(int64 page1, int64 page2); -static bool MultiXactMemberPagePrecedes(int64 page1, int64 page2); -static bool MultiXactOffsetPrecedes(MultiXactOffset offset1, - MultiXactOffset offset2); + +/* opaque_data type for MultiXactMemberIoErrorDetail */ +typedef struct MultiXactMemberSlruReadContext +{ + MultiXactId multi; + MultiXactOffset offset; +} MultiXactMemberSlruReadContext; + static void ExtendMultiXactOffset(MultiXactId multi); static void ExtendMultiXactMember(MultiXactOffset offset, int nmembers); -static bool MultiXactOffsetWouldWrap(MultiXactOffset boundary, - MultiXactOffset start, uint32 distance); -static bool SetOffsetVacuumLimit(bool is_startup); +static void SetOldestOffset(void); static bool find_multixact_start(MultiXactId multi, MultiXactOffset *result); -static void WriteMZeroPageXlogRec(int64 pageno, uint8 info); static void WriteMTruncateXlogRec(Oid oldestMultiDB, - MultiXactId startTruncOff, - MultiXactId endTruncOff, - MultiXactOffset startTruncMemb, - MultiXactOffset endTruncMemb); + MultiXactId oldestMulti, + MultiXactOffset oldestOffset); /* @@ -443,7 +367,7 @@ MultiXactIdCreate(TransactionId xid1, MultiXactStatus status1, Assert(!TransactionIdEquals(xid1, xid2) || (status1 != status2)); /* MultiXactIdSetOldestMember() must have been called already. */ - Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber])); + Assert(MultiXactIdIsValid(*MyOldestMemberMXactIdSlot())); /* * Note: unlike MultiXactIdExpand, we don't bother to check that both XIDs @@ -497,7 +421,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status) Assert(TransactionIdIsValid(xid)); /* MultiXactIdSetOldestMember() must have been called already. */ - Assert(MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber])); + Assert(MultiXactIdIsValid(*MyOldestMemberMXactIdSlot())); debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s", multi, xid, mxstatus_to_string(status)); @@ -558,8 +482,7 @@ MultiXactIdExpand(MultiXactId multi, TransactionId xid, MultiXactStatus status) * Note we have the same race condition here as above: j could be 0 at the * end of the loop. */ - newMembers = (MultiXactMember *) - palloc(sizeof(MultiXactMember) * (nmembers + 1)); + newMembers = palloc_array(MultiXactMember, nmembers + 1); for (i = 0, j = 0; i < nmembers; i++) { @@ -672,7 +595,7 @@ MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly) void MultiXactIdSetOldestMember(void) { - if (!MultiXactIdIsValid(OldestMemberMXactId[MyProcNumber])) + if (!MultiXactIdIsValid(*MyOldestMemberMXactIdSlot())) { MultiXactId nextMXact; @@ -692,16 +615,9 @@ MultiXactIdSetOldestMember(void) */ LWLockAcquire(MultiXactGenLock, LW_SHARED); - /* - * We have to beware of the possibility that nextMXact is in the - * wrapped-around state. We don't fix the counter itself here, but we - * must be sure to store a valid value in our array entry. - */ nextMXact = MultiXactState->nextMXact; - if (nextMXact < FirstMultiXactId) - nextMXact = FirstMultiXactId; - OldestMemberMXactId[MyProcNumber] = nextMXact; + *MyOldestMemberMXactIdSlot() = nextMXact; LWLockRelease(MultiXactGenLock); @@ -729,23 +645,15 @@ MultiXactIdSetOldestMember(void) static void MultiXactIdSetOldestVisible(void) { - if (!MultiXactIdIsValid(OldestVisibleMXactId[MyProcNumber])) + if (!MultiXactIdIsValid(*MyOldestVisibleMXactIdSlot())) { MultiXactId oldestMXact; int i; LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); - /* - * We have to beware of the possibility that nextMXact is in the - * wrapped-around state. We don't fix the counter itself here, but we - * must be sure to store a valid value in our array entry. - */ oldestMXact = MultiXactState->nextMXact; - if (oldestMXact < FirstMultiXactId) - oldestMXact = FirstMultiXactId; - - for (i = 0; i < MaxOldestSlot; i++) + for (i = 0; i < NumMemberSlots; i++) { MultiXactId thisoldest = OldestMemberMXactId[i]; @@ -754,7 +662,7 @@ MultiXactIdSetOldestVisible(void) oldestMXact = thisoldest; } - OldestVisibleMXactId[MyProcNumber] = oldestMXact; + *MyOldestVisibleMXactIdSlot() = oldestMXact; LWLockRelease(MultiXactGenLock); @@ -777,9 +685,6 @@ ReadNextMultiXactId(void) mxid = MultiXactState->nextMXact; LWLockRelease(MultiXactGenLock); - if (mxid < FirstMultiXactId) - mxid = FirstMultiXactId; - return mxid; } @@ -794,11 +699,6 @@ ReadMultiXactIdRange(MultiXactId *oldest, MultiXactId *next) *oldest = MultiXactState->oldestMultiXactId; *next = MultiXactState->nextMXact; LWLockRelease(MultiXactGenLock); - - if (*oldest < FirstMultiXactId) - *oldest = FirstMultiXactId; - if (*next < FirstMultiXactId) - *next = FirstMultiXactId; } @@ -921,43 +821,87 @@ RecordNewMultiXact(MultiXactId multi, MultiXactOffset offset, int entryno; int slotno; MultiXactOffset *offptr; - int i; + MultiXactId next; + int64 next_pageno; + int next_entryno; + MultiXactOffset *next_offptr; + MultiXactOffset next_offset; LWLock *lock; LWLock *prevlock = NULL; + /* position of this multixid in the offsets SLRU area */ pageno = MultiXactIdToOffsetPage(multi); entryno = MultiXactIdToOffsetEntry(multi); - lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno); - LWLockAcquire(lock, LW_EXCLUSIVE); + /* position of the next multixid */ + next = NextMultiXactId(multi); + next_pageno = MultiXactIdToOffsetPage(next); + next_entryno = MultiXactIdToOffsetEntry(next); /* - * Note: we pass the MultiXactId to SimpleLruReadPage as the "transaction" - * to complain about if there's any I/O error. This is kinda bogus, but - * since the errors will always give the full pathname, it should be clear - * enough that a MultiXactId is really involved. Perhaps someday we'll - * take the trouble to generalize the slru.c error reporting code. + * Set the starting offset of this multixid's members. + * + * In the common case, it was already set by the previous + * RecordNewMultiXact call, as this was the next multixid of the previous + * multixid. But if multiple backends are generating multixids + * concurrently, we might race ahead and get called before the previous + * multixid. */ - slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, multi); + lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno); + LWLockAcquire(lock, LW_EXCLUSIVE); + + slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, &multi); offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[slotno]; offptr += entryno; - *offptr = offset; + if (*offptr != offset) + { + /* should already be set to the correct value, or not at all */ + Assert(*offptr == 0); + *offptr = offset; + MultiXactOffsetCtl->shared->page_dirty[slotno] = true; + } + + /* + * Set the next multixid's offset to the end of this multixid's members. + */ + if (next_pageno == pageno) + { + next_offptr = offptr + 1; + } + else + { + /* must be the first entry on the page */ + Assert(next_entryno == 0 || next == FirstMultiXactId); + + /* Swap the lock for a lock on the next page */ + LWLockRelease(lock); + lock = SimpleLruGetBankLock(MultiXactOffsetCtl, next_pageno); + LWLockAcquire(lock, LW_EXCLUSIVE); + + slotno = SimpleLruReadPage(MultiXactOffsetCtl, next_pageno, true, &next); + next_offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[slotno]; + next_offptr += next_entryno; + } - MultiXactOffsetCtl->shared->page_dirty[slotno] = true; + /* Like in GetNewMultiXactId(), skip over offset 0 */ + next_offset = offset + nmembers; + if (next_offset == 0) + next_offset = 1; + if (*next_offptr != next_offset) + { + /* should already be set to the correct value, or not at all */ + Assert(*next_offptr == 0); + *next_offptr = next_offset; + MultiXactOffsetCtl->shared->page_dirty[slotno] = true; + } /* Release MultiXactOffset SLRU lock. */ LWLockRelease(lock); - /* - * If anybody was waiting to know the offset of this multixact ID we just - * wrote, they can read it now, so wake them up. - */ - ConditionVariableBroadcast(&MultiXactState->nextoff_cv); - prev_pageno = -1; - for (i = 0; i < nmembers; i++, offset++) + for (int i = 0; i < nmembers; i++, offset++) { TransactionId *memberptr; uint32 *flagsptr; @@ -975,6 +919,8 @@ RecordNewMultiXact(MultiXactId multi, MultiXactOffset offset, if (pageno != prev_pageno) { + MultiXactMemberSlruReadContext slru_read_context = {multi, offset}; + /* * MultiXactMember SLRU page is changed so check if this new page * fall into the different SLRU bank then release the old bank's @@ -989,7 +935,8 @@ RecordNewMultiXact(MultiXactId multi, MultiXactOffset offset, LWLockAcquire(lock, LW_EXCLUSIVE); prevlock = lock; } - slotno = SimpleLruReadPage(MultiXactMemberCtl, pageno, true, multi); + slotno = SimpleLruReadPage(MultiXactMemberCtl, pageno, true, + &slru_read_context); prev_pageno = pageno; } @@ -1042,10 +989,6 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); - /* Handle wraparound of the nextMXact counter */ - if (MultiXactState->nextMXact < FirstMultiXactId) - MultiXactState->nextMXact = FirstMultiXactId; - /* Assign the MXID */ result = MultiXactState->nextMXact; @@ -1112,7 +1055,7 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) * request only once per 64K multis generated. This still gives * plenty of chances before we get into real trouble. */ - if (IsUnderPostmaster && (result % 65536) == 0) + if (IsUnderPostmaster && ((result % 65536) == 0 || result == FirstMultiXactId)) SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER); if (!MultiXactIdPrecedes(result, multiWarnLimit)) @@ -1127,6 +1070,8 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) multiWrapLimit - result, oldest_datname, multiWrapLimit - result), + errdetail("Approximately %.2f%% of MultiXactIds are available for use.", + (double) (multiWrapLimit - result) / (MaxMultiXactId / 2) * 100), errhint("Execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); else @@ -1136,6 +1081,8 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) multiWrapLimit - result, oldest_datoid, multiWrapLimit - result), + errdetail("Approximately %.2f%% of MultiXactIds are available for use.", + (double) (multiWrapLimit - result) / (MaxMultiXactId / 2) * 100), errhint("Execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); } @@ -1143,98 +1090,31 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) /* Re-acquire lock and start over */ LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); result = MultiXactState->nextMXact; - if (result < FirstMultiXactId) - result = FirstMultiXactId; } - /* Make sure there is room for the MXID in the file. */ - ExtendMultiXactOffset(result); - /* - * Reserve the members space, similarly to above. Also, be careful not to - * return zero as the starting offset for any multixact. See - * GetMultiXactIdMembers() for motivation. + * Make sure there is room for the next MXID in the file. Assigning this + * MXID sets the next MXID's offset already. */ - nextOffset = MultiXactState->nextOffset; - if (nextOffset == 0) - { - *offset = 1; - nmembers++; /* allocate member slot 0 too */ - } - else - *offset = nextOffset; + ExtendMultiXactOffset(NextMultiXactId(result)); - /*---------- - * Protect against overrun of the members space as well, with the - * following rules: - * - * If we're past offsetStopLimit, refuse to generate more multis. - * If we're close to offsetStopLimit, emit a warning. - * - * Arbitrarily, we start emitting warnings when we're 20 segments or less - * from offsetStopLimit. - * - * Note we haven't updated the shared state yet, so if we fail at this - * point, the multixact ID we grabbed can still be used by the next guy. - * - * Note that there is no point in forcing autovacuum runs here: the - * multixact freeze settings would have to be reduced for that to have any - * effect. - *---------- + /* + * Reserve the members space, similarly to above. */ -#define OFFSET_WARN_SEGMENTS 20 - if (MultiXactState->oldestOffsetKnown && - MultiXactOffsetWouldWrap(MultiXactState->offsetStopLimit, nextOffset, - nmembers)) - { - /* see comment in the corresponding offsets wraparound case */ - SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER); - - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("multixact \"members\" limit exceeded"), - errdetail_plural("This command would create a multixact with %u members, but the remaining space is only enough for %u member.", - "This command would create a multixact with %u members, but the remaining space is only enough for %u members.", - MultiXactState->offsetStopLimit - nextOffset - 1, - nmembers, - MultiXactState->offsetStopLimit - nextOffset - 1), - errhint("Execute a database-wide VACUUM in database with OID %u with reduced \"vacuum_multixact_freeze_min_age\" and \"vacuum_multixact_freeze_table_age\" settings.", - MultiXactState->oldestMultiXactDB))); - } + nextOffset = MultiXactState->nextOffset; /* - * Check whether we should kick autovacuum into action, to prevent members - * wraparound. NB we use a much larger window to trigger autovacuum than - * just the warning limit. The warning is just a measure of last resort - - * this is in line with GetNewTransactionId's behaviour. + * Offsets are 64-bit integers and will never wrap around. Firstly, it + * would take an unrealistic amount of time and resources to consume 2^64 + * offsets. Secondly, multixid creation is WAL-logged, so you would run + * out of LSNs before reaching offset wraparound. Nevertheless, check for + * wraparound as a sanity check. */ - if (!MultiXactState->oldestOffsetKnown || - (MultiXactState->nextOffset - MultiXactState->oldestOffset - > MULTIXACT_MEMBER_SAFE_THRESHOLD)) - { - /* - * To avoid swamping the postmaster with signals, we issue the autovac - * request only when crossing a segment boundary. With default - * compilation settings that's roughly after 50k members. This still - * gives plenty of chances before we get into real trouble. - */ - if ((MXOffsetToMemberPage(nextOffset) / SLRU_PAGES_PER_SEGMENT) != - (MXOffsetToMemberPage(nextOffset + nmembers) / SLRU_PAGES_PER_SEGMENT)) - SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER); - } - - if (MultiXactState->oldestOffsetKnown && - MultiXactOffsetWouldWrap(MultiXactState->offsetStopLimit, - nextOffset, - nmembers + MULTIXACT_MEMBERS_PER_PAGE * SLRU_PAGES_PER_SEGMENT * OFFSET_WARN_SEGMENTS)) - ereport(WARNING, + if (nextOffset + nmembers < nextOffset) + ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg_plural("database with OID %u must be vacuumed before %d more multixact member is used", - "database with OID %u must be vacuumed before %d more multixact members are used", - MultiXactState->offsetStopLimit - nextOffset + nmembers, - MultiXactState->oldestMultiXactDB, - MultiXactState->offsetStopLimit - nextOffset + nmembers), - errhint("Execute a database-wide VACUUM in that database with reduced \"vacuum_multixact_freeze_min_age\" and \"vacuum_multixact_freeze_table_age\" settings."))); + errmsg("MultiXact members would wrap around"))); + *offset = nextOffset; ExtendMultiXactMember(nextOffset, nmembers); @@ -1250,21 +1130,14 @@ GetNewMultiXactId(int nmembers, MultiXactOffset *offset) /* * Advance counters. As in GetNewTransactionId(), this must not happen * until after file extension has succeeded! - * - * We don't care about MultiXactId wraparound here; it will be handled by - * the next iteration. But note that nextMXact may be InvalidMultiXactId - * or the first value on a segment-beginning page after this routine - * exits, so anyone else looking at the variable must be prepared to deal - * with either case. Similarly, nextOffset may be zero, but we won't use - * that as the actual start offset of the next multixact. */ - (MultiXactState->nextMXact)++; - + MultiXactState->nextMXact = NextMultiXactId(result); MultiXactState->nextOffset += nmembers; LWLockRelease(MultiXactGenLock); - debug_elog4(DEBUG2, "GetNew: returning %u offset %u", result, *offset); + debug_elog4(DEBUG2, "GetNew: returning %u offset %" PRIu64, + result, *offset); return result; } @@ -1305,15 +1178,12 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, int slotno; MultiXactOffset *offptr; MultiXactOffset offset; + MultiXactOffset nextMXOffset; int length; - int truelength; MultiXactId oldestMXact; MultiXactId nextMXact; - MultiXactId tmpMXact; - MultiXactOffset nextOffset; MultiXactMember *ptr; LWLock *lock; - bool slept = false; debug_elog3(DEBUG2, "GetMembers: asked for %u", multi); @@ -1341,7 +1211,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, * multi. It cannot possibly still be running. */ if (isLockOnly && - MultiXactIdPrecedes(multi, OldestVisibleMXactId[MyProcNumber])) + MultiXactIdPrecedes(multi, *MyOldestVisibleMXactIdSlot())) { debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old"); *members = NULL; @@ -1360,14 +1230,12 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, * error. * * Shared lock is enough here since we aren't modifying any global state. - * Acquire it just long enough to grab the current counter values. We may - * need both nextMXact and nextOffset; see below. + * Acquire it just long enough to grab the current counter values. */ LWLockAcquire(MultiXactGenLock, LW_SHARED); oldestMXact = MultiXactState->oldestMultiXactId; nextMXact = MultiXactState->nextMXact; - nextOffset = MultiXactState->nextOffset; LWLockRelease(MultiXactGenLock); @@ -1387,38 +1255,8 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, * Find out the offset at which we need to start reading MultiXactMembers * and the number of members in the multixact. We determine the latter as * the difference between this multixact's starting offset and the next - * one's. However, there are some corner cases to worry about: - * - * 1. This multixact may be the latest one created, in which case there is - * no next one to look at. In this case the nextOffset value we just - * saved is the correct endpoint. - * - * 2. The next multixact may still be in process of being filled in: that - * is, another process may have done GetNewMultiXactId but not yet written - * the offset entry for that ID. In that scenario, it is guaranteed that - * the offset entry for that multixact exists (because GetNewMultiXactId - * won't release MultiXactGenLock until it does) but contains zero - * (because we are careful to pre-zero offset pages). Because - * GetNewMultiXactId will never return zero as the starting offset for a - * multixact, when we read zero as the next multixact's offset, we know we - * have this case. We handle this by sleeping on the condition variable - * we have just for this; the process in charge will signal the CV as soon - * as it has finished writing the multixact offset. - * - * 3. Because GetNewMultiXactId increments offset zero to offset one to - * handle case #2, there is an ambiguity near the point of offset - * wraparound. If we see next multixact's offset is one, is that our - * multixact's actual endpoint, or did it end at zero with a subsequent - * increment? We handle this using the knowledge that if the zero'th - * member slot wasn't filled, it'll contain zero, and zero isn't a valid - * transaction ID so it can't be a multixact member. Therefore, if we - * read a zero from the members array, just ignore it. - * - * This is all pretty messy, but the mess occurs only in infrequent corner - * cases, so it seems better than holding the MultiXactGenLock for a long - * time on every multixact creation. + * one's. */ -retry: pageno = MultiXactIdToOffsetPage(multi); entryno = MultiXactIdToOffsetEntry(multi); @@ -1426,31 +1264,23 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno); LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, multi); + /* read this multi's offset */ + slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, &multi); offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[slotno]; offptr += entryno; offset = *offptr; - Assert(offset != 0); - - /* - * Use the same increment rule as GetNewMultiXactId(), that is, don't - * handle wraparound explicitly until needed. - */ - tmpMXact = multi + 1; + if (offset == 0) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("MultiXact %u has invalid offset", multi))); - if (nextMXact == tmpMXact) - { - /* Corner case 1: there is no next multixact */ - length = nextOffset - offset; - } - else + /* read next multi's offset */ { - MultiXactOffset nextMXOffset; + MultiXactId tmpMXact; /* handle wraparound if needed */ - if (tmpMXact < FirstMultiXactId) - tmpMXact = FirstMultiXactId; + tmpMXact = NextMultiXactId(multi); prev_pageno = pageno; @@ -1473,42 +1303,41 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, LWLockAcquire(newlock, LW_EXCLUSIVE); lock = newlock; } - slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, tmpMXact); + slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, &tmpMXact); } offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[slotno]; offptr += entryno; nextMXOffset = *offptr; - - if (nextMXOffset == 0) - { - /* Corner case 2: next multixact is still being filled in */ - LWLockRelease(lock); - CHECK_FOR_INTERRUPTS(); - - INJECTION_POINT("multixact-get-members-cv-sleep", NULL); - - ConditionVariableSleep(&MultiXactState->nextoff_cv, - WAIT_EVENT_MULTIXACT_CREATION); - slept = true; - goto retry; - } - - length = nextMXOffset - offset; } LWLockRelease(lock); lock = NULL; - /* - * If we slept above, clean up state; it's no longer needed. - */ - if (slept) - ConditionVariableCancelSleep(); + /* Sanity check the next offset */ + if (nextMXOffset == 0) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("MultiXact %u has invalid next offset", multi))); + if (nextMXOffset == offset) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("MultiXact %u with offset (%" PRIu64 ") has zero members", + multi, offset))); + if (nextMXOffset < offset) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("MultiXact %u has offset (%" PRIu64 ") greater than its next offset (%" PRIu64 ")", + multi, offset, nextMXOffset))); + if (nextMXOffset - offset > INT32_MAX) + ereport(ERROR, + (errcode(ERRCODE_DATA_CORRUPTED), + errmsg("MultiXact %u has too many members (%" PRIu64 ")", + multi, nextMXOffset - offset))); + length = nextMXOffset - offset; + /* read the members */ ptr = (MultiXactMember *) palloc(length * sizeof(MultiXactMember)); - - truelength = 0; prev_pageno = -1; for (int i = 0; i < length; i++, offset++) { @@ -1523,6 +1352,7 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, if (pageno != prev_pageno) { + MultiXactMemberSlruReadContext slru_read_context = {multi, offset}; LWLock *newlock; /* @@ -1538,44 +1368,34 @@ GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, LWLockAcquire(newlock, LW_EXCLUSIVE); lock = newlock; } - - slotno = SimpleLruReadPage(MultiXactMemberCtl, pageno, true, multi); + slotno = SimpleLruReadPage(MultiXactMemberCtl, pageno, true, + &slru_read_context); prev_pageno = pageno; } xactptr = (TransactionId *) (MultiXactMemberCtl->shared->page_buffer[slotno] + memberoff); - - if (!TransactionIdIsValid(*xactptr)) - { - /* Corner case 3: we must be looking at unused slot zero */ - Assert(offset == 0); - continue; - } + Assert(TransactionIdIsValid(*xactptr)); flagsoff = MXOffsetToFlagsOffset(offset); bshift = MXOffsetToFlagsBitShift(offset); flagsptr = (uint32 *) (MultiXactMemberCtl->shared->page_buffer[slotno] + flagsoff); - ptr[truelength].xid = *xactptr; - ptr[truelength].status = (*flagsptr >> bshift) & MXACT_MEMBER_XACT_BITMASK; - truelength++; + ptr[i].xid = *xactptr; + ptr[i].status = (*flagsptr >> bshift) & MXACT_MEMBER_XACT_BITMASK; } LWLockRelease(lock); - /* A multixid with zero members should not happen */ - Assert(truelength > 0); - /* * Copy the result into the local cache. */ - mXactCachePut(multi, truelength, ptr); + mXactCachePut(multi, length, ptr); debug_elog3(DEBUG2, "GetMembers: no cache for %s", - mxid_to_string(multi, truelength, ptr)); + mxid_to_string(multi, length, ptr)); *members = ptr; - return truelength; + return length; } /* @@ -1750,7 +1570,7 @@ mXactCachePut(MultiXactId multi, int nmembers, MultiXactMember *members) } } -static char * +char * mxstatus_to_string(MultiXactStatus status) { switch (status) @@ -1814,8 +1634,8 @@ AtEOXact_MultiXact(void) * We assume that storing a MultiXactId is atomic and so we need not take * MultiXactGenLock to do this. */ - OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId; - OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId; + *MyOldestMemberMXactIdSlot() = InvalidMultiXactId; + *MyOldestVisibleMXactIdSlot() = InvalidMultiXactId; /* * Discard the local MultiXactId cache. Since MXactContext was created as @@ -1835,7 +1655,7 @@ AtEOXact_MultiXact(void) void AtPrepare_MultiXact(void) { - MultiXactId myOldestMember = OldestMemberMXactId[MyProcNumber]; + MultiXactId myOldestMember = *MyOldestMemberMXactIdSlot(); if (MultiXactIdIsValid(myOldestMember)) RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0, @@ -1847,7 +1667,7 @@ AtPrepare_MultiXact(void) * Clean up after successful PREPARE TRANSACTION */ void -PostPrepare_MultiXact(TransactionId xid) +PostPrepare_MultiXact(FullTransactionId fxid) { MultiXactId myOldestMember; @@ -1855,10 +1675,10 @@ PostPrepare_MultiXact(TransactionId xid) * Transfer our OldestMemberMXactId value to the slot reserved for the * prepared transaction. */ - myOldestMember = OldestMemberMXactId[MyProcNumber]; + myOldestMember = *MyOldestMemberMXactIdSlot(); if (MultiXactIdIsValid(myOldestMember)) { - ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(xid, false); + ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false); /* * Even though storing MultiXactId is atomic, acquire lock to make @@ -1868,8 +1688,8 @@ PostPrepare_MultiXact(TransactionId xid) */ LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); - OldestMemberMXactId[dummyProcNumber] = myOldestMember; - OldestMemberMXactId[MyProcNumber] = InvalidMultiXactId; + *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = myOldestMember; + *MyOldestMemberMXactIdSlot() = InvalidMultiXactId; LWLockRelease(MultiXactGenLock); } @@ -1882,7 +1702,7 @@ PostPrepare_MultiXact(TransactionId xid) * We assume that storing a MultiXactId is atomic and so we need not take * MultiXactGenLock to do this. */ - OldestVisibleMXactId[MyProcNumber] = InvalidMultiXactId; + *MyOldestVisibleMXactIdSlot() = InvalidMultiXactId; /* * Discard the local MultiXactId cache like in AtEOXact_MultiXact. @@ -1896,10 +1716,10 @@ PostPrepare_MultiXact(TransactionId xid) * Recover the state of a prepared transaction at startup */ void -multixact_twophase_recover(TransactionId xid, uint16 info, +multixact_twophase_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { - ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(xid, false); + ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, false); MultiXactId oldestMember; /* @@ -1909,7 +1729,7 @@ multixact_twophase_recover(TransactionId xid, uint16 info, Assert(len == sizeof(MultiXactId)); oldestMember = *((MultiXactId *) recdata); - OldestMemberMXactId[dummyProcNumber] = oldestMember; + *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = oldestMember; } /* @@ -1917,14 +1737,14 @@ multixact_twophase_recover(TransactionId xid, uint16 info, * Similar to AtEOXact_MultiXact but for COMMIT PREPARED */ void -multixact_twophase_postcommit(TransactionId xid, uint16 info, +multixact_twophase_postcommit(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { - ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(xid, true); + ProcNumber dummyProcNumber = TwoPhaseGetDummyProcNumber(fxid, true); Assert(len == sizeof(MultiXactId)); - OldestMemberMXactId[dummyProcNumber] = InvalidMultiXactId; + *PreparedXactOldestMemberMXactIdSlot(dummyProcNumber) = InvalidMultiXactId; } /* @@ -1932,79 +1752,88 @@ multixact_twophase_postcommit(TransactionId xid, uint16 info, * This is actually just the same as the COMMIT case. */ void -multixact_twophase_postabort(TransactionId xid, uint16 info, +multixact_twophase_postabort(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { - multixact_twophase_postcommit(xid, info, recdata, len); + multixact_twophase_postcommit(fxid, info, recdata, len); } + /* - * Initialization of shared memory for MultiXact. We use two SLRU areas, - * thus double memory. Also, reserve space for the shared MultiXactState - * struct and the per-backend MultiXactId arrays (two of those, too). + * Register shared memory needs for MultiXact. */ -Size -MultiXactShmemSize(void) +static void +MultiXactShmemRequest(void *arg) { Size size; - /* We need 2*MaxOldestSlot perBackendXactIds[] entries */ -#define SHARED_MULTIXACT_STATE_SIZE \ - add_size(offsetof(MultiXactStateData, perBackendXactIds), \ - mul_size(sizeof(MultiXactId) * 2, MaxOldestSlot)) + /* + * Calculate the size of the MultiXactState struct, and the two + * per-backend MultiXactId arrays. They are carved out of the same + * allocation. + */ + size = offsetof(MultiXactStateData, perBackendXactIds); + size = add_size(size, + mul_size(sizeof(MultiXactId), NumMemberSlots)); + size = add_size(size, + mul_size(sizeof(MultiXactId), NumVisibleSlots)); + ShmemRequestStruct(.name = "Shared MultiXact State", + .size = size, + .ptr = (void **) &MultiXactState, + ); - size = SHARED_MULTIXACT_STATE_SIZE; - size = add_size(size, SimpleLruShmemSize(multixact_offset_buffers, 0)); - size = add_size(size, SimpleLruShmemSize(multixact_member_buffers, 0)); + SimpleLruRequest(.desc = &MultiXactOffsetSlruDesc, + .name = "multixact_offset", + .Dir = "pg_multixact/offsets", + .long_segment_names = false, - return size; -} + .nslots = multixact_offset_buffers, -void -MultiXactShmemInit(void) -{ - bool found; + .sync_handler = SYNC_HANDLER_MULTIXACT_OFFSET, + .PagePrecedes = MultiXactOffsetPagePrecedes, + .errdetail_for_io_error = MultiXactOffsetIoErrorDetail, - debug_elog2(DEBUG2, "Shared Memory Init for MultiXact"); + .buffer_tranche_id = LWTRANCHE_MULTIXACTOFFSET_BUFFER, + .bank_tranche_id = LWTRANCHE_MULTIXACTOFFSET_SLRU, + ); - MultiXactOffsetCtl->PagePrecedes = MultiXactOffsetPagePrecedes; - MultiXactMemberCtl->PagePrecedes = MultiXactMemberPagePrecedes; + SimpleLruRequest(.desc = &MultiXactMemberSlruDesc, + .name = "multixact_member", + .Dir = "pg_multixact/members", + .long_segment_names = true, - SimpleLruInit(MultiXactOffsetCtl, - "multixact_offset", multixact_offset_buffers, 0, - "pg_multixact/offsets", LWTRANCHE_MULTIXACTOFFSET_BUFFER, - LWTRANCHE_MULTIXACTOFFSET_SLRU, - SYNC_HANDLER_MULTIXACT_OFFSET, - false); - SlruPagePrecedesUnitTests(MultiXactOffsetCtl, MULTIXACT_OFFSETS_PER_PAGE); - SimpleLruInit(MultiXactMemberCtl, - "multixact_member", multixact_member_buffers, 0, - "pg_multixact/members", LWTRANCHE_MULTIXACTMEMBER_BUFFER, - LWTRANCHE_MULTIXACTMEMBER_SLRU, - SYNC_HANDLER_MULTIXACT_MEMBER, - false); - /* doesn't call SimpleLruTruncate() or meet criteria for unit tests */ - - /* Initialize our shared state struct */ - MultiXactState = ShmemInitStruct("Shared MultiXact State", - SHARED_MULTIXACT_STATE_SIZE, - &found); - if (!IsUnderPostmaster) - { - Assert(!found); + .nslots = multixact_member_buffers, - /* Make sure we zero out the per-backend state */ - MemSet(MultiXactState, 0, SHARED_MULTIXACT_STATE_SIZE); - ConditionVariableInit(&MultiXactState->nextoff_cv); - } - else - Assert(found); + .sync_handler = SYNC_HANDLER_MULTIXACT_MEMBER, + .PagePrecedes = MultiXactMemberPagePrecedes, + .errdetail_for_io_error = MultiXactMemberIoErrorDetail, + + .buffer_tranche_id = LWTRANCHE_MULTIXACTMEMBER_BUFFER, + .bank_tranche_id = LWTRANCHE_MULTIXACTMEMBER_SLRU, + ); +} + +static void +MultiXactShmemInit(void *arg) +{ + SlruPagePrecedesUnitTests(MultiXactOffsetCtl, MULTIXACT_OFFSETS_PER_PAGE); /* - * Set up array pointers. + * members SLRU doesn't call SimpleLruTruncate() or meet criteria for unit + * tests */ + + /* Set up array pointers */ + OldestMemberMXactId = MultiXactState->perBackendXactIds; + OldestVisibleMXactId = OldestMemberMXactId + NumMemberSlots; +} + +static void +MultiXactShmemAttach(void *arg) +{ + /* Set up array pointers */ OldestMemberMXactId = MultiXactState->perBackendXactIds; - OldestVisibleMXactId = OldestMemberMXactId + MaxOldestSlot; + OldestVisibleMXactId = OldestMemberMXactId + NumMemberSlots; } /* @@ -2033,112 +1862,9 @@ check_multixact_member_buffers(int *newval, void **extra, GucSource source) void BootStrapMultiXact(void) { - int slotno; - LWLock *lock; - - lock = SimpleLruGetBankLock(MultiXactOffsetCtl, 0); - LWLockAcquire(lock, LW_EXCLUSIVE); - - /* Create and zero the first page of the offsets log */ - slotno = ZeroMultiXactOffsetPage(0, false); - - /* Make sure it's written out */ - SimpleLruWritePage(MultiXactOffsetCtl, slotno); - Assert(!MultiXactOffsetCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); - - lock = SimpleLruGetBankLock(MultiXactMemberCtl, 0); - LWLockAcquire(lock, LW_EXCLUSIVE); - - /* Create and zero the first page of the members log */ - slotno = ZeroMultiXactMemberPage(0, false); - - /* Make sure it's written out */ - SimpleLruWritePage(MultiXactMemberCtl, slotno); - Assert(!MultiXactMemberCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); -} - -/* - * Initialize (or reinitialize) a page of MultiXactOffset to zeroes. - * If writeXlog is true, also emit an XLOG record saying we did this. - * - * The page is not actually written, just set up in shared memory. - * The slot number of the new page is returned. - * - * Control lock must be held at entry, and will be held at exit. - */ -static int -ZeroMultiXactOffsetPage(int64 pageno, bool writeXlog) -{ - int slotno; - - slotno = SimpleLruZeroPage(MultiXactOffsetCtl, pageno); - - if (writeXlog) - WriteMZeroPageXlogRec(pageno, XLOG_MULTIXACT_ZERO_OFF_PAGE); - - return slotno; -} - -/* - * Ditto, for MultiXactMember - */ -static int -ZeroMultiXactMemberPage(int64 pageno, bool writeXlog) -{ - int slotno; - - slotno = SimpleLruZeroPage(MultiXactMemberCtl, pageno); - - if (writeXlog) - WriteMZeroPageXlogRec(pageno, XLOG_MULTIXACT_ZERO_MEM_PAGE); - - return slotno; -} - -/* - * MaybeExtendOffsetSlru - * Extend the offsets SLRU area, if necessary - * - * After a binary upgrade from <= 9.2, the pg_multixact/offsets SLRU area might - * contain files that are shorter than necessary; this would occur if the old - * installation had used multixacts beyond the first page (files cannot be - * copied, because the on-disk representation is different). pg_upgrade would - * update pg_control to set the next offset value to be at that position, so - * that tuples marked as locked by such MultiXacts would be seen as visible - * without having to consult multixact. However, trying to create and use a - * new MultiXactId would result in an error because the page on which the new - * value would reside does not exist. This routine is in charge of creating - * such pages. - */ -static void -MaybeExtendOffsetSlru(void) -{ - int64 pageno; - LWLock *lock; - - pageno = MultiXactIdToOffsetPage(MultiXactState->nextMXact); - lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno); - - LWLockAcquire(lock, LW_EXCLUSIVE); - - if (!SimpleLruDoesPhysicalPageExist(MultiXactOffsetCtl, pageno)) - { - int slotno; - - /* - * Fortunately for us, SimpleLruWritePage is already prepared to deal - * with creating a new segment file even if the page we're writing is - * not the first in it, so this is enough. - */ - slotno = ZeroMultiXactOffsetPage(pageno, false); - SimpleLruWritePage(MultiXactOffsetCtl, slotno); - } - - LWLockRelease(lock); + /* Zero the initial pages and flush them to disk */ + SimpleLruZeroAndWritePage(MultiXactOffsetCtl, 0); + SimpleLruZeroAndWritePage(MultiXactMemberCtl, 0); } /* @@ -2202,26 +1928,34 @@ TrimMultiXact(void) pageno); /* - * Zero out the remainder of the current offsets page. See notes in - * TrimCLOG() for background. Unlike CLOG, some WAL record covers every - * pg_multixact SLRU mutation. Since, also unlike CLOG, we ignore the WAL - * rule "write xlog before data," nextMXact successors may carry obsolete, - * nonzero offset values. Zero those so case 2 of GetMultiXactIdMembers() - * operates normally. + * Set the offset of nextMXact on the offsets page. This is normally done + * in RecordNewMultiXact() of the previous multixact, but let's be sure + * the next page exists, if the nextMXact was reset with pg_resetwal for + * example. + * + * Zero out the remainder of the page. See notes in TrimCLOG() for + * background. Unlike CLOG, some WAL record covers every pg_multixact + * SLRU mutation. Since, also unlike CLOG, we ignore the WAL rule "write + * xlog before data," nextMXact successors may carry obsolete, nonzero + * offset values. */ entryno = MultiXactIdToOffsetEntry(nextMXact); - if (entryno != 0) { int slotno; MultiXactOffset *offptr; LWLock *lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno); LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, nextMXact); + if (entryno == 0 || nextMXact == FirstMultiXactId) + slotno = SimpleLruZeroPage(MultiXactOffsetCtl, pageno); + else + slotno = SimpleLruReadPage(MultiXactOffsetCtl, pageno, true, &nextMXact); offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[slotno]; offptr += entryno; - MemSet(offptr, 0, BLCKSZ - (entryno * sizeof(MultiXactOffset))); + *offptr = offset; + if (entryno != 0 && (entryno + 1) * sizeof(MultiXactOffset) != BLCKSZ) + MemSet(offptr + 1, 0, BLCKSZ - (entryno + 1) * sizeof(MultiXactOffset)); MultiXactOffsetCtl->shared->page_dirty[slotno] = true; LWLockRelease(lock); @@ -2243,6 +1977,7 @@ TrimMultiXact(void) flagsoff = MXOffsetToFlagsOffset(offset); if (flagsoff != 0) { + MultiXactMemberSlruReadContext slru_read_context = {InvalidMultiXactId, offset}; int slotno; TransactionId *xidptr; int memberoff; @@ -2250,7 +1985,7 @@ TrimMultiXact(void) LWLockAcquire(lock, LW_EXCLUSIVE); memberoff = MXOffsetToMemberOffset(offset); - slotno = SimpleLruReadPage(MultiXactMemberCtl, pageno, true, offset); + slotno = SimpleLruReadPage(MultiXactMemberCtl, pageno, true, &slru_read_context); xidptr = (TransactionId *) (MultiXactMemberCtl->shared->page_buffer[slotno] + memberoff); @@ -2271,8 +2006,8 @@ TrimMultiXact(void) MultiXactState->finishedStartup = true; LWLockRelease(MultiXactGenLock); - /* Now compute how far away the next members wraparound is. */ - SetMultiXactIdLimit(oldestMXact, oldestMXactDB, true); + /* Now compute how far away the next multixid wraparound is. */ + SetMultiXactIdLimit(oldestMXact, oldestMXactDB); } /* @@ -2293,7 +2028,7 @@ MultiXactGetCheckptMulti(bool is_shutdown, LWLockRelease(MultiXactGenLock); debug_elog6(DEBUG2, - "MultiXact: checkpoint is nextMulti %u, nextOffset %u, oldestMulti %u in DB %u", + "MultiXact: checkpoint is nextMulti %u, nextOffset %" PRIu64 ", oldestMulti %u in DB %u", *nextMulti, *nextMultiOffset, *oldestMulti, *oldestMultiDB); } @@ -2328,26 +2063,14 @@ void MultiXactSetNextMXact(MultiXactId nextMulti, MultiXactOffset nextMultiOffset) { - debug_elog4(DEBUG2, "MultiXact: setting next multi to %u offset %u", + Assert(MultiXactIdIsValid(nextMulti)); + debug_elog4(DEBUG2, "MultiXact: setting next multi to %u offset %" PRIu64, nextMulti, nextMultiOffset); + LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); MultiXactState->nextMXact = nextMulti; MultiXactState->nextOffset = nextMultiOffset; LWLockRelease(MultiXactGenLock); - - /* - * During a binary upgrade, make sure that the offsets SLRU is large - * enough to contain the next value that would be created. - * - * We need to do this pretty early during the first startup in binary - * upgrade mode: before StartupMultiXact() in fact, because this routine - * is called even before that by StartupXLOG(). And we can't do it - * earlier than at this point, because during that first call of this - * routine we determine the MultiXactState->nextMXact value that - * MaybeExtendOffsetSlru needs. - */ - if (IsBinaryUpgrade) - MaybeExtendOffsetSlru(); } /* @@ -2355,28 +2078,24 @@ MultiXactSetNextMXact(MultiXactId nextMulti, * datminmxid (ie, the oldest MultiXactId that might exist in any database * of our cluster), and the OID of the (or a) database with that value. * - * is_startup is true when we are just starting the cluster, false when we - * are updating state in a running cluster. This only affects log messages. + * This also updates MultiXactState->oldestOffset, by looking up the offset of + * MultiXactState->oldestMultiXactId. */ void -SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, - bool is_startup) +SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid) { MultiXactId multiVacLimit; MultiXactId multiWarnLimit; MultiXactId multiStopLimit; MultiXactId multiWrapLimit; MultiXactId curMulti; - bool needs_offset_vacuum; Assert(MultiXactIdIsValid(oldest_datminmxid)); /* * We pretend that a wrap will happen halfway through the multixact ID * space, but that's not really true, because multixacts wrap differently - * from transaction IDs. Note that, separately from any concern about - * multixact IDs wrapping, we must ensure that multixact members do not - * wrap. Limits for that are set in SetOffsetVacuumLimit, not here. + * from transaction IDs. */ multiWrapLimit = oldest_datminmxid + (MaxMultiXactId >> 1); if (multiWrapLimit < FirstMultiXactId) @@ -2391,16 +2110,16 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, multiStopLimit -= FirstMultiXactId; /* - * We'll start complaining loudly when we get within 40M multis of data + * We'll start complaining loudly when we get within 100M multis of data * loss. This is kind of arbitrary, but if you let your gas gauge get - * down to 2% of full, would you be looking for the next gas station? We + * down to 5% of full, would you be looking for the next gas station? We * need to be fairly liberal about this number because there are lots of * scenarios where most transactions are done by automatic clients that * won't pay attention to warnings. (No, we're not gonna make this * configurable. If you know enough to configure it, you know enough to * not get in this kind of trouble in the first place.) */ - multiWarnLimit = multiWrapLimit - 40000000; + multiWarnLimit = multiWrapLimit - 100000000; if (multiWarnLimit < FirstMultiXactId) multiWarnLimit -= FirstMultiXactId; @@ -2444,8 +2163,14 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, Assert(!InRecovery); - /* Set limits for offset vacuum. */ - needs_offset_vacuum = SetOffsetVacuumLimit(is_startup); + /* + * Offsets are 64-bits wide and never wrap around, so we don't need to + * consider them for emergency autovacuum purposes. But now that we're in + * a consistent state, determine MultiXactState->oldestOffset. It will be + * used to adjust the freezing cutoff, to keep the offsets disk usage in + * check. + */ + SetOldestOffset(); /* * If past the autovacuum force point, immediately signal an autovac @@ -2454,8 +2179,7 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, * database, it'll call here, and we'll signal the postmaster to start * another iteration immediately if there are still any old databases. */ - if ((MultiXactIdPrecedes(multiVacLimit, curMulti) || - needs_offset_vacuum) && IsUnderPostmaster) + if (MultiXactIdPrecedes(multiVacLimit, curMulti) && IsUnderPostmaster) SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER); /* Give an immediate warning if past the wrap warn point */ @@ -2484,6 +2208,8 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, multiWrapLimit - curMulti, oldest_datname, multiWrapLimit - curMulti), + errdetail("Approximately %.2f%% of MultiXactIds are available for use.", + (double) (multiWrapLimit - curMulti) / (MaxMultiXactId / 2) * 100), errhint("To avoid MultiXactId assignment failures, execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); else @@ -2493,6 +2219,8 @@ SetMultiXactIdLimit(MultiXactId oldest_datminmxid, Oid oldest_datoid, multiWrapLimit - curMulti, oldest_datoid, multiWrapLimit - curMulti), + errdetail("Approximately %.2f%% of MultiXactIds are available for use.", + (double) (multiWrapLimit - curMulti) / (MaxMultiXactId / 2) * 100), errhint("To avoid MultiXactId assignment failures, execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); } @@ -2511,15 +2239,17 @@ void MultiXactAdvanceNextMXact(MultiXactId minMulti, MultiXactOffset minMultiOffset) { + Assert(MultiXactIdIsValid(minMulti)); + LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); if (MultiXactIdPrecedes(MultiXactState->nextMXact, minMulti)) { debug_elog3(DEBUG2, "MultiXact: setting next multi to %u", minMulti); MultiXactState->nextMXact = minMulti; } - if (MultiXactOffsetPrecedes(MultiXactState->nextOffset, minMultiOffset)) + if (MultiXactState->nextOffset < minMultiOffset) { - debug_elog3(DEBUG2, "MultiXact: setting next offset to %u", + debug_elog3(DEBUG2, "MultiXact: setting next offset to %" PRIu64, minMultiOffset); MultiXactState->nextOffset = minMultiOffset; } @@ -2538,7 +2268,7 @@ MultiXactAdvanceOldest(MultiXactId oldestMulti, Oid oldestMultiDB) Assert(InRecovery); if (MultiXactIdPrecedes(MultiXactState->oldestMultiXactId, oldestMulti)) - SetMultiXactIdLimit(oldestMulti, oldestMultiDB, false); + SetMultiXactIdLimit(oldestMulti, oldestMultiDB); } /* @@ -2568,8 +2298,10 @@ ExtendMultiXactOffset(MultiXactId multi) LWLockAcquire(lock, LW_EXCLUSIVE); - /* Zero the page and make an XLOG entry about it */ - ZeroMultiXactOffsetPage(pageno, true); + /* Zero the page and make a WAL entry about it */ + SimpleLruZeroPage(MultiXactOffsetCtl, pageno); + XLogSimpleInsertInt64(RM_MULTIXACT_ID, XLOG_MULTIXACT_ZERO_OFF_PAGE, + pageno); LWLockRelease(lock); } @@ -2611,33 +2343,19 @@ ExtendMultiXactMember(MultiXactOffset offset, int nmembers) LWLockAcquire(lock, LW_EXCLUSIVE); - /* Zero the page and make an XLOG entry about it */ - ZeroMultiXactMemberPage(pageno, true); + /* Zero the page and make a WAL entry about it */ + SimpleLruZeroPage(MultiXactMemberCtl, pageno); + XLogSimpleInsertInt64(RM_MULTIXACT_ID, + XLOG_MULTIXACT_ZERO_MEM_PAGE, pageno); LWLockRelease(lock); } - /* - * Compute the number of items till end of current page. Careful: if - * addition of unsigned ints wraps around, we're at the last page of - * the last segment; since that page holds a different number of items - * than other pages, we need to do it differently. - */ - if (offset + MAX_MEMBERS_IN_LAST_MEMBERS_PAGE < offset) - { - /* - * This is the last page of the last segment; we can compute the - * number of items left to allocate in it without modulo - * arithmetic. - */ - difference = MaxMultiXactOffset - offset + 1; - } - else - difference = MULTIXACT_MEMBERS_PER_PAGE - offset % MULTIXACT_MEMBERS_PER_PAGE; + /* Compute the number of items till end of current page. */ + difference = MULTIXACT_MEMBERS_PER_PAGE - offset % MULTIXACT_MEMBERS_PER_PAGE; /* - * Advance to next page, taking care to properly handle the wraparound - * case. OK if nmembers goes negative. + * Advance to next page. OK if nmembers goes negative. */ nmembers -= difference; offset += difference; @@ -2660,26 +2378,14 @@ MultiXactId GetOldestMultiXactId(void) { MultiXactId oldestMXact; - MultiXactId nextMXact; - int i; /* * This is the oldest valid value among all the OldestMemberMXactId[] and * OldestVisibleMXactId[] entries, or nextMXact if none are valid. */ LWLockAcquire(MultiXactGenLock, LW_SHARED); - - /* - * We have to beware of the possibility that nextMXact is in the - * wrapped-around state. We don't fix the counter itself here, but we - * must be sure to use a valid value in our calculation. - */ - nextMXact = MultiXactState->nextMXact; - if (nextMXact < FirstMultiXactId) - nextMXact = FirstMultiXactId; - - oldestMXact = nextMXact; - for (i = 0; i < MaxOldestSlot; i++) + oldestMXact = MultiXactState->nextMXact; + for (int i = 0; i < NumMemberSlots; i++) { MultiXactId thisoldest; @@ -2687,6 +2393,11 @@ GetOldestMultiXactId(void) if (MultiXactIdIsValid(thisoldest) && MultiXactIdPrecedes(thisoldest, oldestMXact)) oldestMXact = thisoldest; + } + for (int i = 0; i < NumVisibleSlots; i++) + { + MultiXactId thisoldest; + thisoldest = OldestVisibleMXactId[i]; if (MultiXactIdIsValid(thisoldest) && MultiXactIdPrecedes(thisoldest, oldestMXact)) @@ -2699,28 +2410,17 @@ GetOldestMultiXactId(void) } /* - * Determine how aggressively we need to vacuum in order to prevent member - * wraparound. - * - * To do so determine what's the oldest member offset and install the limit - * info in MultiXactState, where it can be used to prevent overrun of old data - * in the members SLRU area. - * - * The return value is true if emergency autovacuum is required and false - * otherwise. + * Calculate the oldest member offset and install it in MultiXactState, where + * it can be used to adjust multixid freezing cutoffs. */ -static bool -SetOffsetVacuumLimit(bool is_startup) +static void +SetOldestOffset(void) { MultiXactId oldestMultiXactId; MultiXactId nextMXact; MultiXactOffset oldestOffset = 0; /* placate compiler */ - MultiXactOffset prevOldestOffset; MultiXactOffset nextOffset; bool oldestOffsetKnown = false; - bool prevOldestOffsetKnown; - MultiXactOffset offsetStopLimit = 0; - MultiXactOffset prevOffsetStopLimit; /* * NB: Have to prevent concurrent truncation, we might otherwise try to @@ -2733,9 +2433,6 @@ SetOffsetVacuumLimit(bool is_startup) oldestMultiXactId = MultiXactState->oldestMultiXactId; nextMXact = MultiXactState->nextMXact; nextOffset = MultiXactState->nextOffset; - prevOldestOffsetKnown = MultiXactState->oldestOffsetKnown; - prevOldestOffset = MultiXactState->oldestOffset; - prevOffsetStopLimit = MultiXactState->offsetStopLimit; Assert(MultiXactState->finishedStartup); LWLockRelease(MultiXactGenLock); @@ -2758,121 +2455,39 @@ SetOffsetVacuumLimit(bool is_startup) else { /* - * Figure out where the oldest existing multixact's offsets are - * stored. Due to bugs in early release of PostgreSQL 9.3.X and 9.4.X, - * the supposedly-earliest multixact might not really exist. We are - * careful not to fail in that case. + * Look up the offset at which the oldest existing multixact's members + * are stored. If we cannot find it, be careful not to fail, and + * leave oldestOffset unchanged. oldestOffset is initialized to zero + * at system startup, which prevents truncating members until a proper + * value is calculated. + * + * (We had bugs in early releases of PostgreSQL 9.3.X and 9.4.X where + * the supposedly-earliest multixact might not really exist. Those + * should be long gone by now, so this should not fail, but let's + * still be defensive.) */ oldestOffsetKnown = find_multixact_start(oldestMultiXactId, &oldestOffset); if (oldestOffsetKnown) ereport(DEBUG1, - (errmsg_internal("oldest MultiXactId member is at offset %u", + (errmsg_internal("oldest MultiXactId member is at offset %" PRIu64, oldestOffset))); else ereport(LOG, - (errmsg("MultiXact member wraparound protections are disabled because oldest checkpointed MultiXact %u does not exist on disk", + (errmsg("MultiXact member truncation is disabled because oldest checkpointed MultiXact %u does not exist on disk", oldestMultiXactId))); } LWLockRelease(MultiXactTruncationLock); - /* - * If we can, compute limits (and install them MultiXactState) to prevent - * overrun of old data in the members SLRU area. We can only do so if the - * oldest offset is known though. - */ + /* Install the computed value */ if (oldestOffsetKnown) { - /* move back to start of the corresponding segment */ - offsetStopLimit = oldestOffset - (oldestOffset % - (MULTIXACT_MEMBERS_PER_PAGE * SLRU_PAGES_PER_SEGMENT)); - - /* always leave one segment before the wraparound point */ - offsetStopLimit -= (MULTIXACT_MEMBERS_PER_PAGE * SLRU_PAGES_PER_SEGMENT); - - if (!prevOldestOffsetKnown && !is_startup) - ereport(LOG, - (errmsg("MultiXact member wraparound protections are now enabled"))); - - ereport(DEBUG1, - (errmsg_internal("MultiXact member stop limit is now %u based on MultiXact %u", - offsetStopLimit, oldestMultiXactId))); - } - else if (prevOldestOffsetKnown) - { - /* - * If we failed to get the oldest offset this time, but we have a - * value from a previous pass through this function, use the old - * values rather than automatically forcing an emergency autovacuum - * cycle again. - */ - oldestOffset = prevOldestOffset; - oldestOffsetKnown = true; - offsetStopLimit = prevOffsetStopLimit; + LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); + MultiXactState->oldestOffset = oldestOffset; + LWLockRelease(MultiXactGenLock); } - - /* Install the computed values */ - LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); - MultiXactState->oldestOffset = oldestOffset; - MultiXactState->oldestOffsetKnown = oldestOffsetKnown; - MultiXactState->offsetStopLimit = offsetStopLimit; - LWLockRelease(MultiXactGenLock); - - /* - * Do we need an emergency autovacuum? If we're not sure, assume yes. - */ - return !oldestOffsetKnown || - (nextOffset - oldestOffset > MULTIXACT_MEMBER_SAFE_THRESHOLD); -} - -/* - * Return whether adding "distance" to "start" would move past "boundary". - * - * We use this to determine whether the addition is "wrapping around" the - * boundary point, hence the name. The reason we don't want to use the regular - * 2^31-modulo arithmetic here is that we want to be able to use the whole of - * the 2^32-1 space here, allowing for more multixacts than would fit - * otherwise. - */ -static bool -MultiXactOffsetWouldWrap(MultiXactOffset boundary, MultiXactOffset start, - uint32 distance) -{ - MultiXactOffset finish; - - /* - * Note that offset number 0 is not used (see GetMultiXactIdMembers), so - * if the addition wraps around the UINT_MAX boundary, skip that value. - */ - finish = start + distance; - if (finish < start) - finish++; - - /*----------------------------------------------------------------------- - * When the boundary is numerically greater than the starting point, any - * value numerically between the two is not wrapped: - * - * <----S----B----> - * [---) = F wrapped past B (and UINT_MAX) - * [---) = F not wrapped - * [----] = F wrapped past B - * - * When the boundary is numerically less than the starting point (i.e. the - * UINT_MAX wraparound occurs somewhere in between) then all values in - * between are wrapped: - * - * <----B----S----> - * [---) = F not wrapped past B (but wrapped past UINT_MAX) - * [---) = F wrapped past B (and UINT_MAX) - * [----] = F not wrapped - *----------------------------------------------------------------------- - */ - if (start < boundary) - return finish >= boundary || finish < start; - else - return finish >= boundary && finish < start; } /* @@ -2908,7 +2523,7 @@ find_multixact_start(MultiXactId multi, MultiXactOffset *result) return false; /* lock is acquired by SimpleLruReadPage_ReadOnly */ - slotno = SimpleLruReadPage_ReadOnly(MultiXactOffsetCtl, pageno, multi); + slotno = SimpleLruReadPage_ReadOnly(MultiXactOffsetCtl, pageno, &multi); offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[slotno]; offptr += entryno; offset = *offptr; @@ -2919,32 +2534,28 @@ find_multixact_start(MultiXactId multi, MultiXactOffset *result) } /* - * Determine how many multixacts, and how many multixact members, currently - * exist. Return false if unable to determine. + * GetMultiXactInfo + * + * Returns information about the current MultiXact state, as of: + * multixacts: Number of MultiXacts (nextMultiXactId - oldestMultiXactId) + * nextOffset: Next-to-be-assigned offset + * oldestMultiXactId: Oldest MultiXact ID still in use + * oldestOffset: Oldest offset still in use */ -static bool -ReadMultiXactCounts(uint32 *multixacts, MultiXactOffset *members) +void +GetMultiXactInfo(uint32 *multixacts, MultiXactOffset *nextOffset, + MultiXactId *oldestMultiXactId, MultiXactOffset *oldestOffset) { - MultiXactOffset nextOffset; - MultiXactOffset oldestOffset; - MultiXactId oldestMultiXactId; MultiXactId nextMultiXactId; - bool oldestOffsetKnown; LWLockAcquire(MultiXactGenLock, LW_SHARED); - nextOffset = MultiXactState->nextOffset; - oldestMultiXactId = MultiXactState->oldestMultiXactId; + *nextOffset = MultiXactState->nextOffset; + *oldestMultiXactId = MultiXactState->oldestMultiXactId; nextMultiXactId = MultiXactState->nextMXact; - oldestOffset = MultiXactState->oldestOffset; - oldestOffsetKnown = MultiXactState->oldestOffsetKnown; + *oldestOffset = MultiXactState->oldestOffset; LWLockRelease(MultiXactGenLock); - if (!oldestOffsetKnown) - return false; - - *members = nextOffset - oldestOffset; - *multixacts = nextMultiXactId - oldestMultiXactId; - return true; + *multixacts = nextMultiXactId - *oldestMultiXactId; } /* @@ -2953,56 +2564,68 @@ ReadMultiXactCounts(uint32 *multixacts, MultiXactOffset *members) * vacuum_multixact_freeze_table_age work together to make sure we never have * too many multixacts; we hope that, at least under normal circumstances, * this will also be sufficient to keep us from using too many offsets. - * However, if the average multixact has many members, we might exhaust the - * members space while still using few enough members that these limits fail - * to trigger relminmxid advancement by VACUUM. At that point, we'd have no - * choice but to start failing multixact-creating operations with an error. - * - * To prevent that, if more than a threshold portion of the members space is - * used, we effectively reduce autovacuum_multixact_freeze_max_age and - * to a value just less than the number of multixacts in use. We hope that - * this will quickly trigger autovacuuming on the table or tables with the - * oldest relminmxid, thus allowing datminmxid values to advance and removing - * some members. - * - * As the fraction of the member space currently in use grows, we become - * more aggressive in clamping this value. That not only causes autovacuum - * to ramp up, but also makes any manual vacuums the user issues more - * aggressive. This happens because vacuum_get_cutoffs() will clamp the - * freeze table and the minimum freeze age cutoffs based on the effective - * autovacuum_multixact_freeze_max_age this function returns. In the worst - * case, we'll claim the freeze_max_age to zero, and every vacuum of any - * table will freeze every multixact. + * However, if the average multixact has many members, we might accumulate a + * large amount of members, consuming disk space, while still using few enough + * multixids that the multixid limits fail to trigger relminmxid advancement + * by VACUUM. + * + * To prevent that, if the members space usage exceeds a threshold + * (MULTIXACT_MEMBER_LOW_THRESHOLD), we effectively reduce + * autovacuum_multixact_freeze_max_age to a value just less than the number of + * multixacts in use. We hope that this will quickly trigger autovacuuming on + * the table or tables with the oldest relminmxid, thus allowing datminmxid + * values to advance and removing some members. + * + * As the amount of the member space in use grows, we become more aggressive + * in clamping this value. That not only causes autovacuum to ramp up, but + * also makes any manual vacuums the user issues more aggressive. This + * happens because vacuum_get_cutoffs() will clamp the freeze table and the + * minimum freeze age cutoffs based on the effective + * autovacuum_multixact_freeze_max_age this function returns. At the extreme, + * when the members usage reaches MULTIXACT_MEMBER_HIGH_THRESHOLD, we clamp + * freeze_max_age to zero, and every vacuum of any table will freeze every + * multixact. */ int MultiXactMemberFreezeThreshold(void) { - MultiXactOffset members; uint32 multixacts; uint32 victim_multixacts; double fraction; int result; + MultiXactId oldestMultiXactId; + MultiXactOffset oldestOffset; + MultiXactOffset nextOffset; + uint64 members; - /* If we can't determine member space utilization, assume the worst. */ - if (!ReadMultiXactCounts(&multixacts, &members)) - return 0; + /* Read the current offsets and multixact usage. */ + GetMultiXactInfo(&multixacts, &nextOffset, &oldestMultiXactId, &oldestOffset); + members = nextOffset - oldestOffset; /* If member space utilization is low, no special action is required. */ - if (members <= MULTIXACT_MEMBER_SAFE_THRESHOLD) + if (members <= MULTIXACT_MEMBER_LOW_THRESHOLD) return autovacuum_multixact_freeze_max_age; /* * Compute a target for relminmxid advancement. The number of multixacts * we try to eliminate from the system is based on how far we are past - * MULTIXACT_MEMBER_SAFE_THRESHOLD. + * MULTIXACT_MEMBER_LOW_THRESHOLD. + * + * The way this formula works is that when members is exactly at the low + * threshold, fraction = 0.0, and we set freeze_max_age equal to + * mxid_age(oldestMultiXactId). As members grows further, towards the + * high threshold, fraction grows linearly from 0.0 to 1.0, and the result + * shrinks from mxid_age(oldestMultiXactId) to 0. Beyond the high + * threshold, fraction > 1.0 and the result is clamped to 0. */ - fraction = (double) (members - MULTIXACT_MEMBER_SAFE_THRESHOLD) / - (MULTIXACT_MEMBER_DANGER_THRESHOLD - MULTIXACT_MEMBER_SAFE_THRESHOLD); - victim_multixacts = multixacts * fraction; + fraction = (double) (members - MULTIXACT_MEMBER_LOW_THRESHOLD) / + (MULTIXACT_MEMBER_HIGH_THRESHOLD - MULTIXACT_MEMBER_LOW_THRESHOLD); /* fraction could be > 1.0, but lowest possible freeze age is zero */ - if (victim_multixacts > multixacts) + if (fraction >= 1.0) return 0; + + victim_multixacts = multixacts * fraction; result = multixacts - victim_multixacts; /* @@ -3012,69 +2635,22 @@ MultiXactMemberFreezeThreshold(void) return Min(result, autovacuum_multixact_freeze_max_age); } -typedef struct mxtruncinfo -{ - int64 earliestExistingPage; -} mxtruncinfo; - -/* - * SlruScanDirectory callback - * This callback determines the earliest existing page number. - */ -static bool -SlruScanDirCbFindEarliest(SlruCtl ctl, char *filename, int64 segpage, void *data) -{ - mxtruncinfo *trunc = (mxtruncinfo *) data; - - if (trunc->earliestExistingPage == -1 || - ctl->PagePrecedes(segpage, trunc->earliestExistingPage)) - { - trunc->earliestExistingPage = segpage; - } - - return false; /* keep going */ -} - /* - * Delete members segments [oldest, newOldest) - * - * The members SLRU can, in contrast to the offsets one, be filled to almost - * the full range at once. This means SimpleLruTruncate() can't trivially be - * used - instead the to-be-deleted range is computed using the offsets - * SLRU. C.f. TruncateMultiXact(). + * Delete members segments older than newOldestOffset */ static void -PerformMembersTruncation(MultiXactOffset oldestOffset, MultiXactOffset newOldestOffset) +PerformMembersTruncation(MultiXactOffset newOldestOffset) { - const int64 maxsegment = MXOffsetToMemberSegment(MaxMultiXactOffset); - int64 startsegment = MXOffsetToMemberSegment(oldestOffset); - int64 endsegment = MXOffsetToMemberSegment(newOldestOffset); - int64 segment = startsegment; - - /* - * Delete all the segments but the last one. The last segment can still - * contain, possibly partially, valid data. - */ - while (segment != endsegment) - { - elog(DEBUG2, "truncating multixact members segment %" PRIx64, - segment); - SlruDeleteSegment(MultiXactMemberCtl, segment); - - /* move to next segment, handling wraparound correctly */ - if (segment == maxsegment) - segment = 0; - else - segment += 1; - } + SimpleLruTruncate(MultiXactMemberCtl, + MXOffsetToMemberPage(newOldestOffset)); } /* - * Delete offsets segments [oldest, newOldest) + * Delete offsets segments older than newOldestMulti */ static void -PerformOffsetsTruncation(MultiXactId oldestMulti, MultiXactId newOldestMulti) +PerformOffsetsTruncation(MultiXactId newOldestMulti) { /* * We step back one multixact to avoid passing a cutoff page that hasn't @@ -3104,13 +2680,11 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB) MultiXactId oldestMulti; MultiXactId nextMulti; MultiXactOffset newOldestOffset; - MultiXactOffset oldestOffset; MultiXactOffset nextOffset; - mxtruncinfo trunc; - MultiXactId earliest; Assert(!RecoveryInProgress()); Assert(MultiXactState->finishedStartup); + Assert(MultiXactIdIsValid(newOldestMulti)); /* * We can only allow one truncation to happen at once. Otherwise parts of @@ -3125,7 +2699,6 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB) nextOffset = MultiXactState->nextOffset; oldestMulti = MultiXactState->oldestMultiXactId; LWLockRelease(MultiXactGenLock); - Assert(MultiXactIdIsValid(oldestMulti)); /* * Make sure to only attempt truncation if there's values to truncate @@ -3139,84 +2712,46 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB) } /* - * Note we can't just plow ahead with the truncation; it's possible that - * there are no segments to truncate, which is a problem because we are - * going to attempt to read the offsets page to determine where to - * truncate the members SLRU. So we first scan the directory to determine - * the earliest offsets page number that we can read without error. - * - * When nextMXact is less than one segment away from multiWrapLimit, - * SlruScanDirCbFindEarliest can find some early segment other than the - * actual earliest. (MultiXactOffsetPagePrecedes(EARLIEST, LATEST) - * returns false, because not all pairs of entries have the same answer.) - * That can also arise when an earlier truncation attempt failed unlink() - * or returned early from this function. The only consequence is - * returning early, which wastes space that we could have liberated. - * - * NB: It's also possible that the page that oldestMulti is on has already - * been truncated away, and we crashed before updating oldestMulti. - */ - trunc.earliestExistingPage = -1; - SlruScanDirectory(MultiXactOffsetCtl, SlruScanDirCbFindEarliest, &trunc); - earliest = trunc.earliestExistingPage * MULTIXACT_OFFSETS_PER_PAGE; - if (earliest < FirstMultiXactId) - earliest = FirstMultiXactId; - - /* If there's nothing to remove, we can bail out early. */ - if (MultiXactIdPrecedes(oldestMulti, earliest)) - { - LWLockRelease(MultiXactTruncationLock); - return; - } - - /* - * First, compute the safe truncation point for MultiXactMember. This is - * the starting offset of the oldest multixact. - * - * Hopefully, find_multixact_start will always work here, because we've - * already checked that it doesn't precede the earliest MultiXact on disk. - * But if it fails, don't truncate anything, and log a message. + * Compute up to where to truncate MultiXactMember. Lookup the + * corresponding member offset for newOldestMulti for that. */ - if (oldestMulti == nextMulti) + if (newOldestMulti == nextMulti) { /* there are NO MultiXacts */ - oldestOffset = nextOffset; + newOldestOffset = nextOffset; } - else if (!find_multixact_start(oldestMulti, &oldestOffset)) + else if (!find_multixact_start(newOldestMulti, &newOldestOffset)) { ereport(LOG, - (errmsg("oldest MultiXact %u not found, earliest MultiXact %u, skipping truncation", - oldestMulti, earliest))); + (errmsg("cannot truncate up to MultiXact %u because it does not exist on disk, skipping truncation", + newOldestMulti))); LWLockRelease(MultiXactTruncationLock); return; } /* - * Secondly compute up to where to truncate. Lookup the corresponding - * member offset for newOldestMulti for that. + * On crash, MultiXactIdCreateFromMembers() can leave behind multixids + * that were not yet written out and hence have zero offset on disk. If + * such a multixid becomes oldestMulti, we won't be able to look up its + * offset. That should be rare, so we don't try to do anything smart about + * it. Just skip the truncation, and hope that by the next truncation + * attempt, oldestMulti has advanced to a valid multixid. */ - if (newOldestMulti == nextMulti) - { - /* there are NO MultiXacts */ - newOldestOffset = nextOffset; - } - else if (!find_multixact_start(newOldestMulti, &newOldestOffset)) + if (newOldestOffset == 0) { ereport(LOG, - (errmsg("cannot truncate up to MultiXact %u because it does not exist on disk, skipping truncation", + (errmsg("cannot truncate up to MultiXact %u because it has invalid offset, skipping truncation", newOldestMulti))); LWLockRelease(MultiXactTruncationLock); return; } elog(DEBUG1, "performing multixact truncation: " - "offsets [%u, %u), offsets segments [%" PRIx64 ", %" PRIx64 "), " - "members [%u, %u), members segments [%" PRIx64 ", %" PRIx64 ")", - oldestMulti, newOldestMulti, - MultiXactIdToOffsetSegment(oldestMulti), + "oldestMulti %u (offsets segment %" PRIx64 "), " + "oldestOffset %" PRIu64 " (members segment %" PRIx64 ")", + newOldestMulti, MultiXactIdToOffsetSegment(newOldestMulti), - oldestOffset, newOldestOffset, - MXOffsetToMemberSegment(oldestOffset), + newOldestOffset, MXOffsetToMemberSegment(newOldestOffset)); /* @@ -3237,9 +2772,7 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB) MyProc->delayChkptFlags |= DELAY_CHKPT_START; /* WAL log truncation */ - WriteMTruncateXlogRec(newOldestMultiDB, - oldestMulti, newOldestMulti, - oldestOffset, newOldestOffset); + WriteMTruncateXlogRec(newOldestMultiDB, newOldestMulti, newOldestOffset); /* * Update in-memory limits before performing the truncation, while inside @@ -3252,13 +2785,14 @@ TruncateMultiXact(MultiXactId newOldestMulti, Oid newOldestMultiDB) LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE); MultiXactState->oldestMultiXactId = newOldestMulti; MultiXactState->oldestMultiXactDB = newOldestMultiDB; + MultiXactState->oldestOffset = newOldestOffset; LWLockRelease(MultiXactGenLock); /* First truncate members */ - PerformMembersTruncation(oldestOffset, newOldestOffset); + PerformMembersTruncation(newOldestOffset); /* Then offsets */ - PerformOffsetsTruncation(oldestMulti, newOldestMulti); + PerformOffsetsTruncation(newOldestMulti); MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; @@ -3291,20 +2825,34 @@ MultiXactOffsetPagePrecedes(int64 page1, int64 page2) /* * Decide whether a MultiXactMember page number is "older" for truncation - * purposes. There is no "invalid offset number" so use the numbers verbatim. + * purposes. There is no "invalid offset number" and members never wrap + * around, so use the numbers verbatim. */ static bool MultiXactMemberPagePrecedes(int64 page1, int64 page2) { - MultiXactOffset offset1; - MultiXactOffset offset2; + return page1 < page2; +} - offset1 = ((MultiXactOffset) page1) * MULTIXACT_MEMBERS_PER_PAGE; - offset2 = ((MultiXactOffset) page2) * MULTIXACT_MEMBERS_PER_PAGE; +static int +MultiXactOffsetIoErrorDetail(const void *opaque_data) +{ + MultiXactId multixid = *(const MultiXactId *) opaque_data; - return (MultiXactOffsetPrecedes(offset1, offset2) && - MultiXactOffsetPrecedes(offset1, - offset2 + MULTIXACT_MEMBERS_PER_PAGE - 1)); + return errdetail("Could not access offset of multixact %u.", multixid); +} + +static int +MultiXactMemberIoErrorDetail(const void *opaque_data) +{ + const MultiXactMemberSlruReadContext *context = opaque_data; + + if (MultiXactIdIsValid(context->multi)) + return errdetail("Could not access member of multixact %u at offset %" PRIu64 ".", + context->multi, context->offset); + else + return errdetail("Could not access multixact member at offset %" PRIu64 ".", + context->offset); } /* @@ -3336,29 +2884,6 @@ MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2) } -/* - * Decide which of two offsets is earlier. - */ -static bool -MultiXactOffsetPrecedes(MultiXactOffset offset1, MultiXactOffset offset2) -{ - int32 diff = (int32) (offset1 - offset2); - - return (diff < 0); -} - -/* - * Write an xlog record reflecting the zeroing of either a MEMBERs or - * OFFSETs page (info shows which) - */ -static void -WriteMZeroPageXlogRec(int64 pageno, uint8 info) -{ - XLogBeginInsert(); - XLogRegisterData(&pageno, sizeof(pageno)); - (void) XLogInsert(RM_MULTIXACT_ID, info); -} - /* * Write a TRUNCATE xlog record * @@ -3367,19 +2892,15 @@ WriteMZeroPageXlogRec(int64 pageno, uint8 info) */ static void WriteMTruncateXlogRec(Oid oldestMultiDB, - MultiXactId startTruncOff, MultiXactId endTruncOff, - MultiXactOffset startTruncMemb, MultiXactOffset endTruncMemb) + MultiXactId oldestMulti, + MultiXactOffset oldestOffset) { XLogRecPtr recptr; xl_multixact_truncate xlrec; xlrec.oldestMultiDB = oldestMultiDB; - - xlrec.startTruncOff = startTruncOff; - xlrec.endTruncOff = endTruncOff; - - xlrec.startTruncMemb = startTruncMemb; - xlrec.endTruncMemb = endTruncMemb; + xlrec.oldestMulti = oldestMulti; + xlrec.oldestOffset = oldestOffset; XLogBeginInsert(); XLogRegisterData(&xlrec, SizeOfMultiXactTruncate); @@ -3401,36 +2922,16 @@ multixact_redo(XLogReaderState *record) if (info == XLOG_MULTIXACT_ZERO_OFF_PAGE) { int64 pageno; - int slotno; - LWLock *lock; memcpy(&pageno, XLogRecGetData(record), sizeof(pageno)); - - lock = SimpleLruGetBankLock(MultiXactOffsetCtl, pageno); - LWLockAcquire(lock, LW_EXCLUSIVE); - - slotno = ZeroMultiXactOffsetPage(pageno, false); - SimpleLruWritePage(MultiXactOffsetCtl, slotno); - Assert(!MultiXactOffsetCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); + SimpleLruZeroAndWritePage(MultiXactOffsetCtl, pageno); } else if (info == XLOG_MULTIXACT_ZERO_MEM_PAGE) { int64 pageno; - int slotno; - LWLock *lock; memcpy(&pageno, XLogRecGetData(record), sizeof(pageno)); - - lock = SimpleLruGetBankLock(MultiXactMemberCtl, pageno); - LWLockAcquire(lock, LW_EXCLUSIVE); - - slotno = ZeroMultiXactMemberPage(pageno, false); - SimpleLruWritePage(MultiXactMemberCtl, slotno); - Assert(!MultiXactMemberCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); + SimpleLruZeroAndWritePage(MultiXactMemberCtl, pageno); } else if (info == XLOG_MULTIXACT_CREATE_ID) { @@ -3444,7 +2945,7 @@ multixact_redo(XLogReaderState *record) xlrec->members); /* Make sure nextMXact/nextOffset are beyond what this record has */ - MultiXactAdvanceNextMXact(xlrec->mid + 1, + MultiXactAdvanceNextMXact(NextMultiXactId(xlrec->mid), xlrec->moff + xlrec->nmembers); /* @@ -3464,20 +2965,17 @@ multixact_redo(XLogReaderState *record) else if (info == XLOG_MULTIXACT_TRUNCATE_ID) { xl_multixact_truncate xlrec; - int64 pageno; memcpy(&xlrec, XLogRecGetData(record), SizeOfMultiXactTruncate); elog(DEBUG1, "replaying multixact truncation: " - "offsets [%u, %u), offsets segments [%" PRIx64 ", %" PRIx64 "), " - "members [%u, %u), members segments [%" PRIx64 ", %" PRIx64 ")", - xlrec.startTruncOff, xlrec.endTruncOff, - MultiXactIdToOffsetSegment(xlrec.startTruncOff), - MultiXactIdToOffsetSegment(xlrec.endTruncOff), - xlrec.startTruncMemb, xlrec.endTruncMemb, - MXOffsetToMemberSegment(xlrec.startTruncMemb), - MXOffsetToMemberSegment(xlrec.endTruncMemb)); + "oldestMulti %u (offsets segment %" PRIx64 "), " + "oldestOffset %" PRIu64 " (members segment %" PRIx64 ")", + xlrec.oldestMulti, + MultiXactIdToOffsetSegment(xlrec.oldestMulti), + xlrec.oldestOffset, + MXOffsetToMemberSegment(xlrec.oldestOffset)); /* should not be required, but more than cheap enough */ LWLockAcquire(MultiXactTruncationLock, LW_EXCLUSIVE); @@ -3486,19 +2984,10 @@ multixact_redo(XLogReaderState *record) * Advance the horizon values, so they're current at the end of * recovery. */ - SetMultiXactIdLimit(xlrec.endTruncOff, xlrec.oldestMultiDB, false); + SetMultiXactIdLimit(xlrec.oldestMulti, xlrec.oldestMultiDB); - PerformMembersTruncation(xlrec.startTruncMemb, xlrec.endTruncMemb); - - /* - * During XLOG replay, latest_page_number isn't necessarily set up - * yet; insert a suitable value to bypass the sanity test in - * SimpleLruTruncate. - */ - pageno = MultiXactIdToOffsetPage(xlrec.endTruncOff); - pg_atomic_write_u64(&MultiXactOffsetCtl->shared->latest_page_number, - pageno); - PerformOffsetsTruncation(xlrec.startTruncOff, xlrec.endTruncOff); + PerformMembersTruncation(xlrec.oldestOffset); + PerformOffsetsTruncation(xlrec.oldestMulti); LWLockRelease(MultiXactTruncationLock); } @@ -3506,68 +2995,6 @@ multixact_redo(XLogReaderState *record) elog(PANIC, "multixact_redo: unknown op code %u", info); } -Datum -pg_get_multixact_members(PG_FUNCTION_ARGS) -{ - typedef struct - { - MultiXactMember *members; - int nmembers; - int iter; - } mxact; - MultiXactId mxid = PG_GETARG_TRANSACTIONID(0); - mxact *multi; - FuncCallContext *funccxt; - - if (mxid < FirstMultiXactId) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid MultiXactId: %u", mxid))); - - if (SRF_IS_FIRSTCALL()) - { - MemoryContext oldcxt; - TupleDesc tupdesc; - - funccxt = SRF_FIRSTCALL_INIT(); - oldcxt = MemoryContextSwitchTo(funccxt->multi_call_memory_ctx); - - multi = palloc(sizeof(mxact)); - /* no need to allow for old values here */ - multi->nmembers = GetMultiXactIdMembers(mxid, &multi->members, false, - false); - multi->iter = 0; - - if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) - elog(ERROR, "return type must be a row type"); - funccxt->tuple_desc = tupdesc; - funccxt->attinmeta = TupleDescGetAttInMetadata(tupdesc); - funccxt->user_fctx = multi; - - MemoryContextSwitchTo(oldcxt); - } - - funccxt = SRF_PERCALL_SETUP(); - multi = (mxact *) funccxt->user_fctx; - - while (multi->iter < multi->nmembers) - { - HeapTuple tuple; - char *values[2]; - - values[0] = psprintf("%u", multi->members[multi->iter].xid); - values[1] = mxstatus_to_string(multi->members[multi->iter].status); - - tuple = BuildTupleFromCStrings(funccxt->attinmeta, values); - - multi->iter++; - pfree(values[0]); - SRF_RETURN_NEXT(funccxt, HeapTupleGetDatum(tuple)); - } - - SRF_RETURN_DONE(funccxt); -} - /* * Entrypoint for sync.c to sync offsets files. */ diff --git a/src/backend/access/transam/parallel.c b/src/backend/access/transam/parallel.c index 94db1ec30126a..89e9d224eec7d 100644 --- a/src/backend/access/transam/parallel.c +++ b/src/backend/access/transam/parallel.c @@ -3,7 +3,7 @@ * parallel.c * Infrastructure for launching parallel workers * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -36,6 +36,7 @@ #include "pgstat.h" #include "storage/ipc.h" #include "storage/predicate.h" +#include "storage/proc.h" #include "storage/spin.h" #include "tcop/tcopprot.h" #include "utils/combocid.h" @@ -44,6 +45,7 @@ #include "utils/memutils.h" #include "utils/relmapper.h" #include "utils/snapmgr.h" +#include "utils/wait_event.h" /* * We don't want to waste a lot of memory on an error queue which, most of @@ -186,7 +188,7 @@ CreateParallelContext(const char *library_name, const char *function_name, oldcontext = MemoryContextSwitchTo(TopTransactionContext); /* Initialize a new ParallelContext. */ - pcxt = palloc0(sizeof(ParallelContext)); + pcxt = palloc0_object(ParallelContext); pcxt->subid = GetCurrentSubTransactionId(); pcxt->nworkers = nworkers; pcxt->nworkers_to_launch = nworkers; @@ -266,6 +268,10 @@ InitializeParallelDSM(ParallelContext *pcxt) if (pcxt->nworkers > 0) { + StaticAssertDecl(BUFFERALIGN(PARALLEL_ERROR_QUEUE_SIZE) == + PARALLEL_ERROR_QUEUE_SIZE, + "parallel error queue size not buffer-aligned"); + /* Estimate space for various kinds of state sharing. */ library_len = EstimateLibraryStateSpace(); shm_toc_estimate_chunk(&pcxt->estimator, library_len); @@ -297,9 +303,6 @@ InitializeParallelDSM(ParallelContext *pcxt) shm_toc_estimate_keys(&pcxt->estimator, 12); /* Estimate space need for error queues. */ - StaticAssertStmt(BUFFERALIGN(PARALLEL_ERROR_QUEUE_SIZE) == - PARALLEL_ERROR_QUEUE_SIZE, - "parallel error queue size not buffer-aligned"); shm_toc_estimate_chunk(&pcxt->estimator, mul_size(PARALLEL_ERROR_QUEUE_SIZE, pcxt->nworkers)); @@ -356,7 +359,7 @@ InitializeParallelDSM(ParallelContext *pcxt) fps->stmt_ts = GetCurrentStatementStartTimestamp(); fps->serializable_xact_handle = ShareSerializableXact(); SpinLockInit(&fps->mutex); - fps->last_xlog_end = 0; + fps->last_xlog_end = InvalidXLogRecPtr; shm_toc_insert(pcxt->toc, PARALLEL_KEY_FIXED, fps); /* We can skip the rest of this if we're not budgeting for any workers. */ @@ -453,7 +456,7 @@ InitializeParallelDSM(ParallelContext *pcxt) clientconninfospace); /* Allocate space for worker information. */ - pcxt->worker = palloc0(sizeof(ParallelWorkerInfo) * pcxt->nworkers); + pcxt->worker = palloc0_array(ParallelWorkerInfo, pcxt->nworkers); /* * Establish error queues in dynamic shared memory. @@ -507,8 +510,12 @@ InitializeParallelDSM(ParallelContext *pcxt) void ReinitializeParallelDSM(ParallelContext *pcxt) { + MemoryContext oldcontext; FixedParallelState *fps; + /* We might be running in a very short-lived memory context. */ + oldcontext = MemoryContextSwitchTo(TopTransactionContext); + /* Wait for any old workers to exit. */ if (pcxt->nworkers_launched > 0) { @@ -525,7 +532,7 @@ ReinitializeParallelDSM(ParallelContext *pcxt) /* Reset a few bits of fixed parallel state to a clean state. */ fps = shm_toc_lookup(pcxt->toc, PARALLEL_KEY_FIXED, false); - fps->last_xlog_end = 0; + fps->last_xlog_end = InvalidXLogRecPtr; /* Recreate error queues (if they exist). */ if (pcxt->nworkers > 0) @@ -546,6 +553,9 @@ ReinitializeParallelDSM(ParallelContext *pcxt) pcxt->worker[i].error_mqh = shm_mq_attach(mq, pcxt->seg, NULL); } } + + /* Restore previous memory context. */ + MemoryContextSwitchTo(oldcontext); } /* @@ -648,8 +658,7 @@ LaunchParallelWorkers(ParallelContext *pcxt) */ if (pcxt->nworkers_launched > 0) { - pcxt->known_attached_workers = - palloc0(sizeof(bool) * pcxt->nworkers_launched); + pcxt->known_attached_workers = palloc0_array(bool, pcxt->nworkers_launched); pcxt->nknown_attached_workers = 0; } @@ -877,7 +886,7 @@ WaitForParallelWorkersToFinish(ParallelContext *pcxt) * the worker writes messages and terminates after the * CHECK_FOR_INTERRUPTS() near the top of this function and * before the call to GetBackgroundWorkerPid(). In that case, - * or latch should have been set as well and the right things + * our latch should have been set as well and the right things * will happen on the next pass through the loop. */ } @@ -1038,7 +1047,7 @@ HandleParallelMessageInterrupt(void) { InterruptPending = true; ParallelMessagePending = true; - SetLatch(MyLatch); + /* latch will be set by procsignal_sigusr1_handler */ } /* @@ -1320,7 +1329,6 @@ ParallelWorkerMain(Datum main_arg) InitializingParallelWorker = true; /* Establish signal handlers. */ - pqsignal(SIGTERM, die); BackgroundWorkerUnblockSignals(); /* Determine and set our parallel worker number. */ diff --git a/src/backend/access/transam/rmgr.c b/src/backend/access/transam/rmgr.c index 1b7499726eb02..4fda03a3cfcc6 100644 --- a/src/backend/access/transam/rmgr.c +++ b/src/backend/access/transam/rmgr.c @@ -33,7 +33,7 @@ #include "access/xact.h" #include "catalog/storage_xlog.h" #include "commands/dbcommands_xlog.h" -#include "commands/sequence.h" +#include "commands/sequence_xlog.h" #include "commands/tablespace.h" #include "replication/decode.h" #include "replication/message.h" diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c index fe56286d9a972..47dd52d67492b 100644 --- a/src/backend/access/transam/slru.c +++ b/src/backend/access/transam/slru.c @@ -49,7 +49,7 @@ * by re-setting the page's page_dirty flag. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/slru.c @@ -70,7 +70,10 @@ #include "pgstat.h" #include "storage/fd.h" #include "storage/shmem.h" +#include "storage/shmem_internal.h" #include "utils/guc.h" +#include "utils/memutils.h" +#include "utils/wait_event.h" /* * Converts segment number to the filename of the segment. @@ -88,9 +91,9 @@ * dir/123456 for [2^20, 2^24-1] */ static inline int -SlruFileName(SlruCtl ctl, char *path, int64 segno) +SlruFileName(SlruDesc *ctl, char *path, int64 segno) { - if (ctl->long_segment_names) + if (ctl->options.long_segment_names) { /* * We could use 16 characters here but the disadvantage would be that @@ -100,7 +103,7 @@ SlruFileName(SlruCtl ctl, char *path, int64 segno) * that in the future we can't decrease SLRU_PAGES_PER_SEGMENT easily. */ Assert(segno >= 0 && segno <= INT64CONST(0xFFFFFFFFFFFFFFF)); - return snprintf(path, MAXPGPATH, "%s/%015" PRIX64, ctl->Dir, segno); + return snprintf(path, MAXPGPATH, "%s/%015" PRIX64, ctl->options.Dir, segno); } else { @@ -109,7 +112,7 @@ SlruFileName(SlruCtl ctl, char *path, int64 segno) * integers are allowed. See SlruCorrectSegmentFilenameLength() */ Assert(segno >= 0 && segno <= INT64CONST(0xFFFFFF)); - return snprintf(path, MAXPGPATH, "%s/%04X", (ctl)->Dir, + return snprintf(path, MAXPGPATH, "%s/%04X", (ctl)->options.Dir, (unsigned int) segno); } } @@ -175,18 +178,19 @@ static SlruErrorCause slru_errcause; static int slru_errno; -static void SimpleLruZeroLSNs(SlruCtl ctl, int slotno); -static void SimpleLruWaitIO(SlruCtl ctl, int slotno); -static void SlruInternalWritePage(SlruCtl ctl, int slotno, SlruWriteAll fdata); -static bool SlruPhysicalReadPage(SlruCtl ctl, int64 pageno, int slotno); -static bool SlruPhysicalWritePage(SlruCtl ctl, int64 pageno, int slotno, +static void SimpleLruZeroLSNs(SlruDesc *ctl, int slotno); +static void SimpleLruWaitIO(SlruDesc *ctl, int slotno); +static void SlruInternalWritePage(SlruDesc *ctl, int slotno, SlruWriteAll fdata); +static bool SlruPhysicalReadPage(SlruDesc *ctl, int64 pageno, int slotno); +static bool SlruPhysicalWritePage(SlruDesc *ctl, int64 pageno, int slotno, SlruWriteAll fdata); -static void SlruReportIOError(SlruCtl ctl, int64 pageno, TransactionId xid); -static int SlruSelectLRUPage(SlruCtl ctl, int64 pageno); +static void SlruReportIOError(SlruDesc *ctl, int64 pageno, + const void *opaque_data); +static int SlruSelectLRUPage(SlruDesc *ctl, int64 pageno); -static bool SlruScanDirCbDeleteCutoff(SlruCtl ctl, char *filename, +static bool SlruScanDirCbDeleteCutoff(SlruDesc *ctl, char *filename, int64 segpage, void *data); -static void SlruInternalDeleteSegment(SlruCtl ctl, int64 segno); +static void SlruInternalDeleteSegment(SlruDesc *ctl, int64 segno); static inline void SlruRecentlyUsed(SlruShared shared, int slotno); @@ -194,7 +198,7 @@ static inline void SlruRecentlyUsed(SlruShared shared, int slotno); * Initialization of shared memory */ -Size +static Size SimpleLruShmemSize(int nslots, int nlsns) { int nbanks = nslots / SLRU_BANK_SIZE; @@ -236,116 +240,135 @@ SimpleLruAutotuneBuffers(int divisor, int max) } /* - * Initialize, or attach to, a simple LRU cache in shared memory. - * - * ctl: address of local (unshared) control structure. - * name: name of SLRU. (This is user-visible, pick with care!) - * nslots: number of page slots to use. - * nlsns: number of LSN groups per page (set to zero if not relevant). - * subdir: PGDATA-relative subdirectory that will contain the files. - * buffer_tranche_id: tranche ID to use for the SLRU's per-buffer LWLocks. - * bank_tranche_id: tranche ID to use for the bank LWLocks. - * sync_handler: which set of functions to use to handle sync requests + * Register a simple LRU cache in shared memory. */ void -SimpleLruInit(SlruCtl ctl, const char *name, int nslots, int nlsns, - const char *subdir, int buffer_tranche_id, int bank_tranche_id, - SyncRequestHandler sync_handler, bool long_segment_names) +SimpleLruRequestWithOpts(const SlruOpts *options) +{ + SlruOpts *options_copy; + + Assert(options->name != NULL); + Assert(options->nslots > 0); + Assert(options->PagePrecedes != NULL); + Assert(options->errdetail_for_io_error != NULL); + + options_copy = MemoryContextAlloc(TopMemoryContext, + sizeof(SlruOpts)); + memcpy(options_copy, options, sizeof(SlruOpts)); + + options_copy->base.name = options->name; + options_copy->base.size = SimpleLruShmemSize(options_copy->nslots, options_copy->nlsns); + + ShmemRequestInternal(&options_copy->base, SHMEM_KIND_SLRU); +} + +/* Initialize locks and shared memory area */ +void +shmem_slru_init(void *location, ShmemStructOpts *base_options) { + SlruOpts *options = (SlruOpts *) base_options; + SlruDesc *desc = (SlruDesc *) options->desc; + char namebuf[NAMEDATALEN]; SlruShared shared; - bool found; + int nslots = options->nslots; int nbanks = nslots / SLRU_BANK_SIZE; + int nlsns = options->nlsns; + char *ptr; + Size offset; + + shared = (SlruShared) location; + desc->shared = shared; + desc->nbanks = nbanks; + memcpy(&desc->options, options, sizeof(SlruOpts)); + + /* assign new tranche IDs, if not given */ + if (desc->options.buffer_tranche_id == 0) + { + snprintf(namebuf, sizeof(namebuf), "%s buffer", desc->options.name); + desc->options.buffer_tranche_id = LWLockNewTrancheId(namebuf); + } + if (desc->options.bank_tranche_id == 0) + { + snprintf(namebuf, sizeof(namebuf), "%s bank", desc->options.name); + desc->options.bank_tranche_id = LWLockNewTrancheId(namebuf); + } Assert(nslots <= SLRU_MAX_ALLOWED_BUFFERS); - shared = (SlruShared) ShmemInitStruct(name, - SimpleLruShmemSize(nslots, nlsns), - &found); + memset(shared, 0, sizeof(SlruSharedData)); - if (!IsUnderPostmaster) - { - /* Initialize locks and shared memory area */ - char *ptr; - Size offset; - - Assert(!found); - - memset(shared, 0, sizeof(SlruSharedData)); - - shared->num_slots = nslots; - shared->lsn_groups_per_page = nlsns; - - pg_atomic_init_u64(&shared->latest_page_number, 0); - - shared->slru_stats_idx = pgstat_get_slru_index(name); - - ptr = (char *) shared; - offset = MAXALIGN(sizeof(SlruSharedData)); - shared->page_buffer = (char **) (ptr + offset); - offset += MAXALIGN(nslots * sizeof(char *)); - shared->page_status = (SlruPageStatus *) (ptr + offset); - offset += MAXALIGN(nslots * sizeof(SlruPageStatus)); - shared->page_dirty = (bool *) (ptr + offset); - offset += MAXALIGN(nslots * sizeof(bool)); - shared->page_number = (int64 *) (ptr + offset); - offset += MAXALIGN(nslots * sizeof(int64)); - shared->page_lru_count = (int *) (ptr + offset); - offset += MAXALIGN(nslots * sizeof(int)); - - /* Initialize LWLocks */ - shared->buffer_locks = (LWLockPadded *) (ptr + offset); - offset += MAXALIGN(nslots * sizeof(LWLockPadded)); - shared->bank_locks = (LWLockPadded *) (ptr + offset); - offset += MAXALIGN(nbanks * sizeof(LWLockPadded)); - shared->bank_cur_lru_count = (int *) (ptr + offset); - offset += MAXALIGN(nbanks * sizeof(int)); - - if (nlsns > 0) - { - shared->group_lsn = (XLogRecPtr *) (ptr + offset); - offset += MAXALIGN(nslots * nlsns * sizeof(XLogRecPtr)); - } + shared->num_slots = nslots; + shared->lsn_groups_per_page = nlsns; - ptr += BUFFERALIGN(offset); - for (int slotno = 0; slotno < nslots; slotno++) - { - LWLockInitialize(&shared->buffer_locks[slotno].lock, - buffer_tranche_id); + pg_atomic_init_u64(&shared->latest_page_number, 0); - shared->page_buffer[slotno] = ptr; - shared->page_status[slotno] = SLRU_PAGE_EMPTY; - shared->page_dirty[slotno] = false; - shared->page_lru_count[slotno] = 0; - ptr += BLCKSZ; - } + shared->slru_stats_idx = pgstat_get_slru_index(desc->options.name); - /* Initialize the slot banks. */ - for (int bankno = 0; bankno < nbanks; bankno++) - { - LWLockInitialize(&shared->bank_locks[bankno].lock, bank_tranche_id); - shared->bank_cur_lru_count[bankno] = 0; - } + ptr = (char *) shared; + offset = MAXALIGN(sizeof(SlruSharedData)); + shared->page_buffer = (char **) (ptr + offset); + offset += MAXALIGN(nslots * sizeof(char *)); + shared->page_status = (SlruPageStatus *) (ptr + offset); + offset += MAXALIGN(nslots * sizeof(SlruPageStatus)); + shared->page_dirty = (bool *) (ptr + offset); + offset += MAXALIGN(nslots * sizeof(bool)); + shared->page_number = (int64 *) (ptr + offset); + offset += MAXALIGN(nslots * sizeof(int64)); + shared->page_lru_count = (int *) (ptr + offset); + offset += MAXALIGN(nslots * sizeof(int)); - /* Should fit to estimated shmem size */ - Assert(ptr - (char *) shared <= SimpleLruShmemSize(nslots, nlsns)); + /* Initialize LWLocks */ + shared->buffer_locks = (LWLockPadded *) (ptr + offset); + offset += MAXALIGN(nslots * sizeof(LWLockPadded)); + shared->bank_locks = (LWLockPadded *) (ptr + offset); + offset += MAXALIGN(nbanks * sizeof(LWLockPadded)); + shared->bank_cur_lru_count = (int *) (ptr + offset); + offset += MAXALIGN(nbanks * sizeof(int)); + + if (nlsns > 0) + { + shared->group_lsn = (XLogRecPtr *) (ptr + offset); + offset += MAXALIGN(nslots * nlsns * sizeof(XLogRecPtr)); } - else + + ptr += BUFFERALIGN(offset); + for (int slotno = 0; slotno < nslots; slotno++) { - Assert(found); - Assert(shared->num_slots == nslots); + LWLockInitialize(&shared->buffer_locks[slotno].lock, + desc->options.buffer_tranche_id); + + shared->page_buffer[slotno] = ptr; + shared->page_status[slotno] = SLRU_PAGE_EMPTY; + shared->page_dirty[slotno] = false; + shared->page_lru_count[slotno] = 0; + ptr += BLCKSZ; } - /* - * Initialize the unshared control struct, including directory path. We - * assume caller set PagePrecedes. - */ - ctl->shared = shared; - ctl->sync_handler = sync_handler; - ctl->long_segment_names = long_segment_names; - ctl->nbanks = nbanks; - strlcpy(ctl->Dir, subdir, sizeof(ctl->Dir)); + /* Initialize the slot banks. */ + for (int bankno = 0; bankno < nbanks; bankno++) + { + LWLockInitialize(&shared->bank_locks[bankno].lock, desc->options.bank_tranche_id); + shared->bank_cur_lru_count[bankno] = 0; + } + + /* Should fit to estimated shmem size */ + Assert(ptr - (char *) shared <= SimpleLruShmemSize(nslots, nlsns)); } +void +shmem_slru_attach(void *location, ShmemStructOpts *base_options) +{ + SlruOpts *options = (SlruOpts *) base_options; + SlruDesc *desc = (SlruDesc *) options->desc; + int nslots = options->nslots; + int nbanks = nslots / SLRU_BANK_SIZE; + + desc->shared = (SlruShared) location; + desc->nbanks = nbanks; + memcpy(&desc->options, options, sizeof(SlruOpts)); +} + + /* * Helper function for GUC check_hook to check whether slru buffers are in * multiples of SLRU_BANK_SIZE. @@ -371,7 +394,7 @@ check_slru_buffers(const char *name, int *newval) * Bank lock must be held at entry, and will be held at exit. */ int -SimpleLruZeroPage(SlruCtl ctl, int64 pageno) +SimpleLruZeroPage(SlruDesc *ctl, int64 pageno) { SlruShared shared = ctl->shared; int slotno; @@ -400,15 +423,15 @@ SimpleLruZeroPage(SlruCtl ctl, int64 pageno) /* * Assume this page is now the latest active page. * - * Note that because both this routine and SlruSelectLRUPage run with - * ControlLock held, it is not possible for this to be zeroing a page that - * SlruSelectLRUPage is going to evict simultaneously. Therefore, there's - * no memory barrier here. + * Note that because both this routine and SlruSelectLRUPage run with a + * SLRU bank lock held, it is not possible for this to be zeroing a page + * that SlruSelectLRUPage is going to evict simultaneously. Therefore, + * there's no memory barrier here. */ pg_atomic_write_u64(&shared->latest_page_number, pageno); /* update the stats counter of zeroed pages */ - pgstat_count_slru_page_zeroed(shared->slru_stats_idx); + pgstat_count_slru_blocks_zeroed(shared->slru_stats_idx); return slotno; } @@ -424,7 +447,7 @@ SimpleLruZeroPage(SlruCtl ctl, int64 pageno) * This assumes that InvalidXLogRecPtr is bitwise-all-0. */ static void -SimpleLruZeroLSNs(SlruCtl ctl, int slotno) +SimpleLruZeroLSNs(SlruDesc *ctl, int slotno) { SlruShared shared = ctl->shared; @@ -433,6 +456,31 @@ SimpleLruZeroLSNs(SlruCtl ctl, int slotno) shared->lsn_groups_per_page * sizeof(XLogRecPtr)); } +/* + * This is a convenience wrapper for the common case of zeroing a page and + * immediately flushing it to disk. + * + * SLRU bank lock is acquired and released here. + */ +void +SimpleLruZeroAndWritePage(SlruDesc *ctl, int64 pageno) +{ + int slotno; + LWLock *lock; + + lock = SimpleLruGetBankLock(ctl, pageno); + LWLockAcquire(lock, LW_EXCLUSIVE); + + /* Create and zero the page */ + slotno = SimpleLruZeroPage(ctl, pageno); + + /* Make sure it's written out */ + SimpleLruWritePage(ctl, slotno); + Assert(!ctl->shared->page_dirty[slotno]); + + LWLockRelease(lock); +} + /* * Wait for any active I/O on a page slot to finish. (This does not * guarantee that new I/O hasn't been started before we return, though. @@ -441,7 +489,7 @@ SimpleLruZeroLSNs(SlruCtl ctl, int slotno) * Bank lock must be held at entry, and will be held at exit. */ static void -SimpleLruWaitIO(SlruCtl ctl, int slotno) +SimpleLruWaitIO(SlruDesc *ctl, int slotno) { SlruShared shared = ctl->shared; int bankno = SlotGetBankNumber(slotno); @@ -489,8 +537,9 @@ SimpleLruWaitIO(SlruCtl ctl, int slotno) * that modification of the page is safe. If write_ok is false then we * will not return the page until it is not undergoing active I/O. * - * The passed-in xid is used only for error reporting, and may be - * InvalidTransactionId if no specific xid is associated with the action. + * On error, the passed-in 'opaque_data' is passed to the + * 'errdetail_for_io_error' callback, to provide details on the operation that + * failed. It is only used for error reporting. * * Return value is the shared-buffer slot number now holding the page. * The buffer's LRU access info is updated. @@ -498,8 +547,8 @@ SimpleLruWaitIO(SlruCtl ctl, int slotno) * The correct bank lock must be held at entry, and will be held at exit. */ int -SimpleLruReadPage(SlruCtl ctl, int64 pageno, bool write_ok, - TransactionId xid) +SimpleLruReadPage(SlruDesc *ctl, int64 pageno, bool write_ok, + const void *opaque_data) { SlruShared shared = ctl->shared; LWLock *banklock = SimpleLruGetBankLock(ctl, pageno); @@ -535,7 +584,7 @@ SimpleLruReadPage(SlruCtl ctl, int64 pageno, bool write_ok, SlruRecentlyUsed(shared, slotno); /* update the stats counter of pages found in the SLRU */ - pgstat_count_slru_page_hit(shared->slru_stats_idx); + pgstat_count_slru_blocks_hit(shared->slru_stats_idx); return slotno; } @@ -575,12 +624,12 @@ SimpleLruReadPage(SlruCtl ctl, int64 pageno, bool write_ok, /* Now it's okay to ereport if we failed */ if (!ok) - SlruReportIOError(ctl, pageno, xid); + SlruReportIOError(ctl, pageno, opaque_data); SlruRecentlyUsed(shared, slotno); /* update the stats counter of pages not found in SLRU */ - pgstat_count_slru_page_read(shared->slru_stats_idx); + pgstat_count_slru_blocks_read(shared->slru_stats_idx); return slotno; } @@ -591,8 +640,9 @@ SimpleLruReadPage(SlruCtl ctl, int64 pageno, bool write_ok, * The page number must correspond to an already-initialized page. * The caller must intend only read-only access to the page. * - * The passed-in xid is used only for error reporting, and may be - * InvalidTransactionId if no specific xid is associated with the action. + * On error, the passed-in 'opaque_data' is passed to the + * 'errdetail_for_io_error' callback, to provide details on the operation that + * failed. It is only used for error reporting. * * Return value is the shared-buffer slot number now holding the page. * The buffer's LRU access info is updated. @@ -601,7 +651,7 @@ SimpleLruReadPage(SlruCtl ctl, int64 pageno, bool write_ok, * It is unspecified whether the lock will be shared or exclusive. */ int -SimpleLruReadPage_ReadOnly(SlruCtl ctl, int64 pageno, TransactionId xid) +SimpleLruReadPage_ReadOnly(SlruDesc *ctl, int64 pageno, const void *opaque_data) { SlruShared shared = ctl->shared; LWLock *banklock = SimpleLruGetBankLock(ctl, pageno); @@ -619,11 +669,11 @@ SimpleLruReadPage_ReadOnly(SlruCtl ctl, int64 pageno, TransactionId xid) shared->page_number[slotno] == pageno && shared->page_status[slotno] != SLRU_PAGE_READ_IN_PROGRESS) { - /* See comments for SlruRecentlyUsed macro */ + /* See comments for SlruRecentlyUsed() */ SlruRecentlyUsed(shared, slotno); /* update the stats counter of pages found in the SLRU */ - pgstat_count_slru_page_hit(shared->slru_stats_idx); + pgstat_count_slru_blocks_hit(shared->slru_stats_idx); return slotno; } @@ -633,7 +683,7 @@ SimpleLruReadPage_ReadOnly(SlruCtl ctl, int64 pageno, TransactionId xid) LWLockRelease(banklock); LWLockAcquire(banklock, LW_EXCLUSIVE); - return SimpleLruReadPage(ctl, pageno, true, xid); + return SimpleLruReadPage(ctl, pageno, true, opaque_data); } /* @@ -648,7 +698,7 @@ SimpleLruReadPage_ReadOnly(SlruCtl ctl, int64 pageno, TransactionId xid) * Bank lock must be held at entry, and will be held at exit. */ static void -SlruInternalWritePage(SlruCtl ctl, int slotno, SlruWriteAll fdata) +SlruInternalWritePage(SlruDesc *ctl, int slotno, SlruWriteAll fdata) { SlruShared shared = ctl->shared; int64 pageno = shared->page_number[slotno]; @@ -713,7 +763,7 @@ SlruInternalWritePage(SlruCtl ctl, int slotno, SlruWriteAll fdata) /* Now it's okay to ereport if we failed */ if (!ok) - SlruReportIOError(ctl, pageno, InvalidTransactionId); + SlruReportIOError(ctl, pageno, NULL); /* If part of a checkpoint, count this as a SLRU buffer written. */ if (fdata) @@ -728,7 +778,7 @@ SlruInternalWritePage(SlruCtl ctl, int slotno, SlruWriteAll fdata) * fdata is always passed a NULL here. */ void -SimpleLruWritePage(SlruCtl ctl, int slotno) +SimpleLruWritePage(SlruDesc *ctl, int slotno) { Assert(ctl->shared->page_status[slotno] != SLRU_PAGE_EMPTY); @@ -742,7 +792,7 @@ SimpleLruWritePage(SlruCtl ctl, int slotno) * large enough to contain the given page. */ bool -SimpleLruDoesPhysicalPageExist(SlruCtl ctl, int64 pageno) +SimpleLruDoesPhysicalPageExist(SlruDesc *ctl, int64 pageno) { int64 segno = pageno / SLRU_PAGES_PER_SEGMENT; int rpageno = pageno % SLRU_PAGES_PER_SEGMENT; @@ -753,7 +803,7 @@ SimpleLruDoesPhysicalPageExist(SlruCtl ctl, int64 pageno) off_t endpos; /* update the stats counter of checked pages */ - pgstat_count_slru_page_exists(ctl->shared->slru_stats_idx); + pgstat_count_slru_blocks_exists(ctl->shared->slru_stats_idx); SlruFileName(ctl, path, segno); @@ -767,14 +817,14 @@ SimpleLruDoesPhysicalPageExist(SlruCtl ctl, int64 pageno) /* report error normally */ slru_errcause = SLRU_OPEN_FAILED; slru_errno = errno; - SlruReportIOError(ctl, pageno, 0); + SlruReportIOError(ctl, pageno, NULL); } if ((endpos = lseek(fd, 0, SEEK_END)) < 0) { slru_errcause = SLRU_SEEK_FAILED; slru_errno = errno; - SlruReportIOError(ctl, pageno, 0); + SlruReportIOError(ctl, pageno, NULL); } result = endpos >= (off_t) (offset + BLCKSZ); @@ -800,7 +850,7 @@ SimpleLruDoesPhysicalPageExist(SlruCtl ctl, int64 pageno) * read/write operations. We could cache one virtual file pointer ... */ static bool -SlruPhysicalReadPage(SlruCtl ctl, int64 pageno, int slotno) +SlruPhysicalReadPage(SlruDesc *ctl, int64 pageno, int slotno) { SlruShared shared = ctl->shared; int64 segno = pageno / SLRU_PAGES_PER_SEGMENT; @@ -872,7 +922,7 @@ SlruPhysicalReadPage(SlruCtl ctl, int64 pageno, int slotno) * SimpleLruWriteAll. */ static bool -SlruPhysicalWritePage(SlruCtl ctl, int64 pageno, int slotno, SlruWriteAll fdata) +SlruPhysicalWritePage(SlruDesc *ctl, int64 pageno, int slotno, SlruWriteAll fdata) { SlruShared shared = ctl->shared; int64 segno = pageno / SLRU_PAGES_PER_SEGMENT; @@ -882,7 +932,7 @@ SlruPhysicalWritePage(SlruCtl ctl, int64 pageno, int slotno, SlruWriteAll fdata) int fd = -1; /* update the stats counter of written pages */ - pgstat_count_slru_page_written(shared->slru_stats_idx); + pgstat_count_slru_blocks_written(shared->slru_stats_idx); /* * Honor the write-WAL-before-data rule, if appropriate, so that we do not @@ -911,7 +961,7 @@ SlruPhysicalWritePage(SlruCtl ctl, int64 pageno, int slotno, SlruWriteAll fdata) max_lsn = this_lsn; } - if (!XLogRecPtrIsInvalid(max_lsn)) + if (XLogRecPtrIsValid(max_lsn)) { /* * As noted above, elog(ERROR) is not acceptable here, so if @@ -1004,11 +1054,11 @@ SlruPhysicalWritePage(SlruCtl ctl, int64 pageno, int slotno, SlruWriteAll fdata) pgstat_report_wait_end(); /* Queue up a sync request for the checkpointer. */ - if (ctl->sync_handler != SYNC_HANDLER_NONE) + if (ctl->options.sync_handler != SYNC_HANDLER_NONE) { FileTag tag; - INIT_SLRUFILETAG(tag, ctl->sync_handler, segno); + INIT_SLRUFILETAG(tag, ctl->options.sync_handler, segno); if (!RegisterSyncRequest(&tag, SYNC_REQUEST, false)) { /* No space to enqueue sync request. Do it synchronously. */ @@ -1044,7 +1094,7 @@ SlruPhysicalWritePage(SlruCtl ctl, int64 pageno, int slotno, SlruWriteAll fdata) * SlruPhysicalWritePage. Call this after cleaning up shared-memory state. */ static void -SlruReportIOError(SlruCtl ctl, int64 pageno, TransactionId xid) +SlruReportIOError(SlruDesc *ctl, int64 pageno, const void *opaque_data) { int64 segno = pageno / SLRU_PAGES_PER_SEGMENT; int rpageno = pageno % SLRU_PAGES_PER_SEGMENT; @@ -1058,54 +1108,55 @@ SlruReportIOError(SlruCtl ctl, int64 pageno, TransactionId xid) case SLRU_OPEN_FAILED: ereport(ERROR, (errcode_for_file_access(), - errmsg("could not access status of transaction %u", xid), - errdetail("Could not open file \"%s\": %m.", path))); + errmsg("could not open file \"%s\": %m", path), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); break; case SLRU_SEEK_FAILED: ereport(ERROR, (errcode_for_file_access(), - errmsg("could not access status of transaction %u", xid), - errdetail("Could not seek in file \"%s\" to offset %d: %m.", - path, offset))); + errmsg("could not seek in file \"%s\" to offset %d: %m", + path, offset), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); break; case SLRU_READ_FAILED: if (errno) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not access status of transaction %u", xid), - errdetail("Could not read from file \"%s\" at offset %d: %m.", - path, offset))); + errmsg("could not read from file \"%s\" at offset %d: %m", + path, offset), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); else ereport(ERROR, - (errmsg("could not access status of transaction %u", xid), - errdetail("Could not read from file \"%s\" at offset %d: read too few bytes.", path, offset))); + (errmsg("could not read from file \"%s\" at offset %d: read too few bytes", + path, offset), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); break; case SLRU_WRITE_FAILED: if (errno) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not access status of transaction %u", xid), - errdetail("Could not write to file \"%s\" at offset %d: %m.", - path, offset))); + errmsg("Could not write to file \"%s\" at offset %d: %m", + path, offset), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); else ereport(ERROR, - (errmsg("could not access status of transaction %u", xid), - errdetail("Could not write to file \"%s\" at offset %d: wrote too few bytes.", - path, offset))); + (errmsg("Could not write to file \"%s\" at offset %d: wrote too few bytes.", + path, offset), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); break; case SLRU_FSYNC_FAILED: ereport(data_sync_elevel(ERROR), (errcode_for_file_access(), - errmsg("could not access status of transaction %u", xid), - errdetail("Could not fsync file \"%s\": %m.", - path))); + errmsg("could not fsync file \"%s\": %m", + path), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); break; case SLRU_CLOSE_FAILED: ereport(ERROR, (errcode_for_file_access(), - errmsg("could not access status of transaction %u", xid), - errdetail("Could not close file \"%s\": %m.", - path))); + errmsg("could not close file \"%s\": %m", + path), + opaque_data ? ctl->options.errdetail_for_io_error(opaque_data) : 0)); break; default: /* can't get here, we trust */ @@ -1165,7 +1216,7 @@ SlruRecentlyUsed(SlruShared shared, int slotno) * The correct bank lock must be held at entry, and will be held at exit. */ static int -SlruSelectLRUPage(SlruCtl ctl, int64 pageno) +SlruSelectLRUPage(SlruDesc *ctl, int64 pageno) { SlruShared shared = ctl->shared; @@ -1257,8 +1308,8 @@ SlruSelectLRUPage(SlruCtl ctl, int64 pageno) { if (this_delta > best_valid_delta || (this_delta == best_valid_delta && - ctl->PagePrecedes(this_page_number, - best_valid_page_number))) + ctl->options.PagePrecedes(this_page_number, + best_valid_page_number))) { bestvalidslot = slotno; best_valid_delta = this_delta; @@ -1269,8 +1320,8 @@ SlruSelectLRUPage(SlruCtl ctl, int64 pageno) { if (this_delta > best_invalid_delta || (this_delta == best_invalid_delta && - ctl->PagePrecedes(this_page_number, - best_invalid_page_number))) + ctl->options.PagePrecedes(this_page_number, + best_invalid_page_number))) { bestinvalidslot = slotno; best_invalid_delta = this_delta; @@ -1318,7 +1369,7 @@ SlruSelectLRUPage(SlruCtl ctl, int64 pageno) * entries are on disk. */ void -SimpleLruWriteAll(SlruCtl ctl, bool allow_redirtied) +SimpleLruWriteAll(SlruDesc *ctl, bool allow_redirtied) { SlruShared shared = ctl->shared; SlruWriteAllData fdata; @@ -1385,11 +1436,11 @@ SimpleLruWriteAll(SlruCtl ctl, bool allow_redirtied) } } if (!ok) - SlruReportIOError(ctl, pageno, InvalidTransactionId); + SlruReportIOError(ctl, pageno, NULL); /* Ensure that directory entries for new files are on disk. */ - if (ctl->sync_handler != SYNC_HANDLER_NONE) - fsync_fname(ctl->Dir, true); + if (ctl->options.sync_handler != SYNC_HANDLER_NONE) + fsync_fname(ctl->options.Dir, true); } /* @@ -1404,7 +1455,7 @@ SimpleLruWriteAll(SlruCtl ctl, bool allow_redirtied) * after it has accrued freshly-written data. */ void -SimpleLruTruncate(SlruCtl ctl, int64 cutoffPage) +SimpleLruTruncate(SlruDesc *ctl, int64 cutoffPage) { SlruShared shared = ctl->shared; int prevbank; @@ -1426,12 +1477,12 @@ SimpleLruTruncate(SlruCtl ctl, int64 cutoffPage) * bugs elsewhere in SLRU handling, so we don't care if we read a slightly * outdated value; therefore we don't add a memory barrier. */ - if (ctl->PagePrecedes(pg_atomic_read_u64(&shared->latest_page_number), - cutoffPage)) + if (ctl->options.PagePrecedes(pg_atomic_read_u64(&shared->latest_page_number), + cutoffPage)) { ereport(LOG, (errmsg("could not truncate directory \"%s\": apparent wraparound", - ctl->Dir))); + ctl->options.Dir))); return; } @@ -1454,7 +1505,7 @@ SimpleLruTruncate(SlruCtl ctl, int64 cutoffPage) if (shared->page_status[slotno] == SLRU_PAGE_EMPTY) continue; - if (!ctl->PagePrecedes(shared->page_number[slotno], cutoffPage)) + if (!ctl->options.PagePrecedes(shared->page_number[slotno], cutoffPage)) continue; /* @@ -1499,16 +1550,16 @@ SimpleLruTruncate(SlruCtl ctl, int64 cutoffPage) * they either can't yet contain anything, or have already been cleaned out. */ static void -SlruInternalDeleteSegment(SlruCtl ctl, int64 segno) +SlruInternalDeleteSegment(SlruDesc *ctl, int64 segno) { char path[MAXPGPATH]; /* Forget any fsync requests queued for this segment. */ - if (ctl->sync_handler != SYNC_HANDLER_NONE) + if (ctl->options.sync_handler != SYNC_HANDLER_NONE) { FileTag tag; - INIT_SLRUFILETAG(tag, ctl->sync_handler, segno); + INIT_SLRUFILETAG(tag, ctl->options.sync_handler, segno); RegisterSyncRequest(&tag, SYNC_FORGET_REQUEST, true); } @@ -1522,7 +1573,7 @@ SlruInternalDeleteSegment(SlruCtl ctl, int64 segno) * Delete an individual SLRU segment, identified by the segment number. */ void -SlruDeleteSegment(SlruCtl ctl, int64 segno) +SlruDeleteSegment(SlruDesc *ctl, int64 segno) { SlruShared shared = ctl->shared; int prevbank = SlotGetBankNumber(0); @@ -1599,19 +1650,19 @@ SlruDeleteSegment(SlruCtl ctl, int64 segno) * first>=cutoff && last>=cutoff: no; every page of this segment is too young */ static bool -SlruMayDeleteSegment(SlruCtl ctl, int64 segpage, int64 cutoffPage) +SlruMayDeleteSegment(SlruDesc *ctl, int64 segpage, int64 cutoffPage) { int64 seg_last_page = segpage + SLRU_PAGES_PER_SEGMENT - 1; Assert(segpage % SLRU_PAGES_PER_SEGMENT == 0); - return (ctl->PagePrecedes(segpage, cutoffPage) && - ctl->PagePrecedes(seg_last_page, cutoffPage)); + return (ctl->options.PagePrecedes(segpage, cutoffPage) && + ctl->options.PagePrecedes(seg_last_page, cutoffPage)); } #ifdef USE_ASSERT_CHECKING static void -SlruPagePrecedesTestOffset(SlruCtl ctl, int per_page, uint32 offset) +SlruPagePrecedesTestOffset(SlruDesc *ctl, int per_page, uint32 offset) { TransactionId lhs, rhs; @@ -1620,6 +1671,9 @@ SlruPagePrecedesTestOffset(SlruCtl ctl, int per_page, uint32 offset) TransactionId newestXact, oldestXact; + /* This must be called after the Slru has been initialized */ + Assert(ctl->options.PagePrecedes); + /* * Compare an XID pair having undefined order (see RFC 1982), a pair at * "opposite ends" of the XID space. TransactionIdPrecedes() treats each @@ -1636,19 +1690,19 @@ SlruPagePrecedesTestOffset(SlruCtl ctl, int per_page, uint32 offset) Assert(!TransactionIdPrecedes(rhs, lhs + 1)); Assert(!TransactionIdFollowsOrEquals(lhs, rhs)); Assert(!TransactionIdFollowsOrEquals(rhs, lhs)); - Assert(!ctl->PagePrecedes(lhs / per_page, lhs / per_page)); - Assert(!ctl->PagePrecedes(lhs / per_page, rhs / per_page)); - Assert(!ctl->PagePrecedes(rhs / per_page, lhs / per_page)); - Assert(!ctl->PagePrecedes((lhs - per_page) / per_page, rhs / per_page)); - Assert(ctl->PagePrecedes(rhs / per_page, (lhs - 3 * per_page) / per_page)); - Assert(ctl->PagePrecedes(rhs / per_page, (lhs - 2 * per_page) / per_page)); - Assert(ctl->PagePrecedes(rhs / per_page, (lhs - 1 * per_page) / per_page) + Assert(!ctl->options.PagePrecedes(lhs / per_page, lhs / per_page)); + Assert(!ctl->options.PagePrecedes(lhs / per_page, rhs / per_page)); + Assert(!ctl->options.PagePrecedes(rhs / per_page, lhs / per_page)); + Assert(!ctl->options.PagePrecedes((lhs - per_page) / per_page, rhs / per_page)); + Assert(ctl->options.PagePrecedes(rhs / per_page, (lhs - 3 * per_page) / per_page)); + Assert(ctl->options.PagePrecedes(rhs / per_page, (lhs - 2 * per_page) / per_page)); + Assert(ctl->options.PagePrecedes(rhs / per_page, (lhs - 1 * per_page) / per_page) || (1U << 31) % per_page != 0); /* See CommitTsPagePrecedes() */ - Assert(ctl->PagePrecedes((lhs + 1 * per_page) / per_page, rhs / per_page) + Assert(ctl->options.PagePrecedes((lhs + 1 * per_page) / per_page, rhs / per_page) || (1U << 31) % per_page != 0); - Assert(ctl->PagePrecedes((lhs + 2 * per_page) / per_page, rhs / per_page)); - Assert(ctl->PagePrecedes((lhs + 3 * per_page) / per_page, rhs / per_page)); - Assert(!ctl->PagePrecedes(rhs / per_page, (lhs + per_page) / per_page)); + Assert(ctl->options.PagePrecedes((lhs + 2 * per_page) / per_page, rhs / per_page)); + Assert(ctl->options.PagePrecedes((lhs + 3 * per_page) / per_page, rhs / per_page)); + Assert(!ctl->options.PagePrecedes(rhs / per_page, (lhs + per_page) / per_page)); /* * GetNewTransactionId() has assigned the last XID it can safely use, and @@ -1693,7 +1747,7 @@ SlruPagePrecedesTestOffset(SlruCtl ctl, int per_page, uint32 offset) * do not apply to them.) */ void -SlruPagePrecedesUnitTests(SlruCtl ctl, int per_page) +SlruPagePrecedesUnitTests(SlruDesc *ctl, int per_page) { /* Test first, middle and last entries of a page. */ SlruPagePrecedesTestOffset(ctl, per_page, 0); @@ -1708,7 +1762,7 @@ SlruPagePrecedesUnitTests(SlruCtl ctl, int per_page) * one containing the page passed as "data". */ bool -SlruScanDirCbReportPresence(SlruCtl ctl, char *filename, int64 segpage, +SlruScanDirCbReportPresence(SlruDesc *ctl, char *filename, int64 segpage, void *data) { int64 cutoffPage = *(int64 *) data; @@ -1724,7 +1778,7 @@ SlruScanDirCbReportPresence(SlruCtl ctl, char *filename, int64 segpage, * This callback deletes segments prior to the one passed in as "data". */ static bool -SlruScanDirCbDeleteCutoff(SlruCtl ctl, char *filename, int64 segpage, +SlruScanDirCbDeleteCutoff(SlruDesc *ctl, char *filename, int64 segpage, void *data) { int64 cutoffPage = *(int64 *) data; @@ -1740,7 +1794,7 @@ SlruScanDirCbDeleteCutoff(SlruCtl ctl, char *filename, int64 segpage, * This callback deletes all segments. */ bool -SlruScanDirCbDeleteAll(SlruCtl ctl, char *filename, int64 segpage, void *data) +SlruScanDirCbDeleteAll(SlruDesc *ctl, char *filename, int64 segpage, void *data) { SlruInternalDeleteSegment(ctl, segpage / SLRU_PAGES_PER_SEGMENT); @@ -1754,9 +1808,9 @@ SlruScanDirCbDeleteAll(SlruCtl ctl, char *filename, int64 segpage, void *data) * SLRU segment. */ static inline bool -SlruCorrectSegmentFilenameLength(SlruCtl ctl, size_t len) +SlruCorrectSegmentFilenameLength(SlruDesc *ctl, size_t len) { - if (ctl->long_segment_names) + if (ctl->options.long_segment_names) return (len == 15); /* see SlruFileName() */ else @@ -1787,7 +1841,7 @@ SlruCorrectSegmentFilenameLength(SlruCtl ctl, size_t len) * Note that no locking is applied. */ bool -SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data) +SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *data) { bool retval = false; DIR *cldir; @@ -1795,8 +1849,8 @@ SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data) int64 segno; int64 segpage; - cldir = AllocateDir(ctl->Dir); - while ((clde = ReadDir(cldir, ctl->Dir)) != NULL) + cldir = AllocateDir(ctl->options.Dir); + while ((clde = ReadDir(cldir, ctl->options.Dir)) != NULL) { size_t len; @@ -1809,7 +1863,7 @@ SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data) segpage = segno * SLRU_PAGES_PER_SEGMENT; elog(DEBUG2, "SlruScanDirectory invoking callback on %s/%s", - ctl->Dir, clde->d_name); + ctl->options.Dir, clde->d_name); retval = callback(ctl, clde->d_name, segpage, data); if (retval) break; @@ -1827,7 +1881,7 @@ SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data) * performs the fsync. */ int -SlruSyncFileTag(SlruCtl ctl, const FileTag *ftag, char *path) +SlruSyncFileTag(SlruDesc *ctl, const FileTag *ftag, char *path) { int fd; int save_errno; diff --git a/src/backend/access/transam/subtrans.c b/src/backend/access/transam/subtrans.c index 15153618fad16..b79e648b899f2 100644 --- a/src/backend/access/transam/subtrans.c +++ b/src/backend/access/transam/subtrans.c @@ -19,7 +19,7 @@ * data across crashes. During database startup, we simply force the * currently-active page of SUBTRANS to zeroes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/subtrans.c @@ -33,6 +33,7 @@ #include "access/transam.h" #include "miscadmin.h" #include "pg_trace.h" +#include "storage/subsystems.h" #include "utils/guc_hooks.h" #include "utils/snapmgr.h" @@ -66,16 +67,22 @@ TransactionIdToPage(TransactionId xid) #define TransactionIdToEntry(xid) ((xid) % (TransactionId) SUBTRANS_XACTS_PER_PAGE) +static void SUBTRANSShmemRequest(void *arg); +static void SUBTRANSShmemInit(void *arg); +static bool SubTransPagePrecedes(int64 page1, int64 page2); +static int subtrans_errdetail_for_io_error(const void *opaque_data); + +const ShmemCallbacks SUBTRANSShmemCallbacks = { + .request_fn = SUBTRANSShmemRequest, + .init_fn = SUBTRANSShmemInit, +}; + /* * Link to shared-memory data structures for SUBTRANS control */ -static SlruCtlData SubTransCtlData; - -#define SubTransCtl (&SubTransCtlData) - +static SlruDesc SubTransSlruDesc; -static int ZeroSUBTRANSPage(int64 pageno); -static bool SubTransPagePrecedes(int64 page1, int64 page2); +#define SubTransCtl (&SubTransSlruDesc) /* @@ -96,7 +103,7 @@ SubTransSetParent(TransactionId xid, TransactionId parent) lock = SimpleLruGetBankLock(SubTransCtl, pageno); LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = SimpleLruReadPage(SubTransCtl, pageno, true, xid); + slotno = SimpleLruReadPage(SubTransCtl, pageno, true, &xid); ptr = (TransactionId *) SubTransCtl->shared->page_buffer[slotno]; ptr += entryno; @@ -136,7 +143,7 @@ SubTransGetParent(TransactionId xid) /* lock is acquired by SimpleLruReadPage_ReadOnly */ - slotno = SimpleLruReadPage_ReadOnly(SubTransCtl, pageno, xid); + slotno = SimpleLruReadPage_ReadOnly(SubTransCtl, pageno, &xid); ptr = (TransactionId *) SubTransCtl->shared->page_buffer[slotno]; ptr += entryno; @@ -207,17 +214,13 @@ SUBTRANSShmemBuffers(void) return Min(Max(16, subtransaction_buffers), SLRU_MAX_ALLOWED_BUFFERS); } + + /* - * Initialization of shared memory for SUBTRANS + * Register shared memory for SUBTRANS */ -Size -SUBTRANSShmemSize(void) -{ - return SimpleLruShmemSize(SUBTRANSShmemBuffers(), 0); -} - -void -SUBTRANSShmemInit(void) +static void +SUBTRANSShmemRequest(void *arg) { /* If auto-tuning is requested, now is the time to do it */ if (subtransaction_buffers == 0) @@ -240,10 +243,25 @@ SUBTRANSShmemInit(void) } Assert(subtransaction_buffers != 0); - SubTransCtl->PagePrecedes = SubTransPagePrecedes; - SimpleLruInit(SubTransCtl, "subtransaction", SUBTRANSShmemBuffers(), 0, - "pg_subtrans", LWTRANCHE_SUBTRANS_BUFFER, - LWTRANCHE_SUBTRANS_SLRU, SYNC_HANDLER_NONE, false); + SimpleLruRequest(.desc = &SubTransSlruDesc, + .name = "subtransaction", + .Dir = "pg_subtrans", + .long_segment_names = false, + + .nslots = SUBTRANSShmemBuffers(), + + .sync_handler = SYNC_HANDLER_NONE, + .PagePrecedes = SubTransPagePrecedes, + .errdetail_for_io_error = subtrans_errdetail_for_io_error, + + .buffer_tranche_id = LWTRANCHE_SUBTRANS_BUFFER, + .bank_tranche_id = LWTRANCHE_SUBTRANS_SLRU, + ); +} + +static void +SUBTRANSShmemInit(void *arg) +{ SlruPagePrecedesUnitTests(SubTransCtl, SUBTRANS_XACTS_PER_PAGE); } @@ -269,33 +287,8 @@ check_subtrans_buffers(int *newval, void **extra, GucSource source) void BootStrapSUBTRANS(void) { - int slotno; - LWLock *lock = SimpleLruGetBankLock(SubTransCtl, 0); - - LWLockAcquire(lock, LW_EXCLUSIVE); - - /* Create and zero the first page of the subtrans log */ - slotno = ZeroSUBTRANSPage(0); - - /* Make sure it's written out */ - SimpleLruWritePage(SubTransCtl, slotno); - Assert(!SubTransCtl->shared->page_dirty[slotno]); - - LWLockRelease(lock); -} - -/* - * Initialize (or reinitialize) a page of SUBTRANS to zeroes. - * - * The page is not actually written, just set up in shared memory. - * The slot number of the new page is returned. - * - * Control lock must be held at entry, and will be held at exit. - */ -static int -ZeroSUBTRANSPage(int64 pageno) -{ - return SimpleLruZeroPage(SubTransCtl, pageno); + /* Zero the initial page and flush it to disk */ + SimpleLruZeroAndWritePage(SubTransCtl, 0); } /* @@ -335,7 +328,7 @@ StartupSUBTRANS(TransactionId oldestActiveXID) prevlock = lock; } - (void) ZeroSUBTRANSPage(startPage); + (void) SimpleLruZeroPage(SubTransCtl, startPage); if (startPage == endPage) break; @@ -395,7 +388,7 @@ ExtendSUBTRANS(TransactionId newestXact) LWLockAcquire(lock, LW_EXCLUSIVE); /* Zero the page */ - ZeroSUBTRANSPage(pageno); + SimpleLruZeroPage(SubTransCtl, pageno); LWLockRelease(lock); } @@ -445,3 +438,11 @@ SubTransPagePrecedes(int64 page1, int64 page2) return (TransactionIdPrecedes(xid1, xid2) && TransactionIdPrecedes(xid1, xid2 + SUBTRANS_XACTS_PER_PAGE - 1)); } + +static int +subtrans_errdetail_for_io_error(const void *opaque_data) +{ + TransactionId xid = *(const TransactionId *) opaque_data; + + return errdetail("Could not access subtransaction status of transaction %u.", xid); +} diff --git a/src/backend/access/transam/timeline.c b/src/backend/access/transam/timeline.c index a27f27cc037d1..68e5f692d26dd 100644 --- a/src/backend/access/transam/timeline.c +++ b/src/backend/access/transam/timeline.c @@ -21,7 +21,7 @@ * The fields are separated by tabs. Lines beginning with # are comments, and * are ignored. Empty lines are also ignored. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/timeline.c @@ -41,6 +41,7 @@ #include "access/xlogdefs.h" #include "pgstat.h" #include "storage/fd.h" +#include "utils/wait_event.h" /* * Copies all timeline history files with id's between 'begin' and 'end' @@ -87,7 +88,7 @@ readTimeLineHistory(TimeLineID targetTLI) /* Timeline 1 does not have a history file, so no need to check */ if (targetTLI == 1) { - entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry)); + entry = palloc_object(TimeLineHistoryEntry); entry->tli = targetTLI; entry->begin = entry->end = InvalidXLogRecPtr; return list_make1(entry); @@ -110,7 +111,7 @@ readTimeLineHistory(TimeLineID targetTLI) (errcode_for_file_access(), errmsg("could not open file \"%s\": %m", path))); /* Not there, so assume no parents */ - entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry)); + entry = palloc_object(TimeLineHistoryEntry); entry->tli = targetTLI; entry->begin = entry->end = InvalidXLogRecPtr; return list_make1(entry); @@ -154,7 +155,7 @@ readTimeLineHistory(TimeLineID targetTLI) if (*ptr == '\0' || *ptr == '#') continue; - nfields = sscanf(fline, "%u\t%X/%X", &tli, &switchpoint_hi, &switchpoint_lo); + nfields = sscanf(fline, "%u\t%X/%08X", &tli, &switchpoint_hi, &switchpoint_lo); if (nfields < 1) { @@ -175,7 +176,7 @@ readTimeLineHistory(TimeLineID targetTLI) lasttli = tli; - entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry)); + entry = palloc_object(TimeLineHistoryEntry); entry->tli = tli; entry->begin = prevend; entry->end = ((uint64) (switchpoint_hi)) << 32 | (uint64) switchpoint_lo; @@ -198,7 +199,7 @@ readTimeLineHistory(TimeLineID targetTLI) * Create one more entry for the "tip" of the timeline, which has no entry * in the history file. */ - entry = (TimeLineHistoryEntry *) palloc(sizeof(TimeLineHistoryEntry)); + entry = palloc_object(TimeLineHistoryEntry); entry->tli = targetTLI; entry->begin = prevend; entry->end = InvalidXLogRecPtr; @@ -399,7 +400,7 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, * parent file failed to end with one. */ snprintf(buffer, sizeof(buffer), - "%s%u\t%X/%X\t%s\n", + "%s%u\t%X/%08X\t%s\n", (srcfd < 0) ? "" : "\n", parentTLI, LSN_FORMAT_ARGS(switchpoint), @@ -549,8 +550,8 @@ tliOfPointInHistory(XLogRecPtr ptr, List *history) { TimeLineHistoryEntry *tle = (TimeLineHistoryEntry *) lfirst(cell); - if ((XLogRecPtrIsInvalid(tle->begin) || tle->begin <= ptr) && - (XLogRecPtrIsInvalid(tle->end) || ptr < tle->end)) + if ((!XLogRecPtrIsValid(tle->begin) || tle->begin <= ptr) && + (!XLogRecPtrIsValid(tle->end) || ptr < tle->end)) { /* found it */ return tle->tli; diff --git a/src/backend/access/transam/transam.c b/src/backend/access/transam/transam.c index 9a39451a29a96..682182fb4ab75 100644 --- a/src/backend/access/transam/transam.c +++ b/src/backend/access/transam/transam.c @@ -3,7 +3,7 @@ * transam.c * postgres transaction (commit) log interface routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -273,70 +273,6 @@ TransactionIdAbortTree(TransactionId xid, int nxids, TransactionId *xids) TRANSACTION_STATUS_ABORTED, InvalidXLogRecPtr); } -/* - * TransactionIdPrecedes --- is id1 logically < id2? - */ -bool -TransactionIdPrecedes(TransactionId id1, TransactionId id2) -{ - /* - * If either ID is a permanent XID then we can just do unsigned - * comparison. If both are normal, do a modulo-2^32 comparison. - */ - int32 diff; - - if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) - return (id1 < id2); - - diff = (int32) (id1 - id2); - return (diff < 0); -} - -/* - * TransactionIdPrecedesOrEquals --- is id1 logically <= id2? - */ -bool -TransactionIdPrecedesOrEquals(TransactionId id1, TransactionId id2) -{ - int32 diff; - - if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) - return (id1 <= id2); - - diff = (int32) (id1 - id2); - return (diff <= 0); -} - -/* - * TransactionIdFollows --- is id1 logically > id2? - */ -bool -TransactionIdFollows(TransactionId id1, TransactionId id2) -{ - int32 diff; - - if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) - return (id1 > id2); - - diff = (int32) (id1 - id2); - return (diff > 0); -} - -/* - * TransactionIdFollowsOrEquals --- is id1 logically >= id2? - */ -bool -TransactionIdFollowsOrEquals(TransactionId id1, TransactionId id2) -{ - int32 diff; - - if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) - return (id1 >= id2); - - diff = (int32) (id1 - id2); - return (diff >= 0); -} - /* * TransactionIdLatest --- get latest XID among a main xact and its children diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c index 73a80559194e7..1035e8b3fc795 100644 --- a/src/backend/access/transam/twophase.c +++ b/src/backend/access/transam/twophase.c @@ -3,7 +3,7 @@ * twophase.c * Two-phase commit support functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -102,9 +102,12 @@ #include "storage/predicate.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/subsystems.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* * Directory where Two-phase commit files reside within PGDATA @@ -159,7 +162,7 @@ typedef struct GlobalTransactionData */ XLogRecPtr prepare_start_lsn; /* XLOG offset of prepare record start */ XLogRecPtr prepare_end_lsn; /* XLOG offset of prepare record end */ - TransactionId xid; /* The GXACT id */ + FullTransactionId fxid; /* The GXACT full xid */ Oid owner; /* ID of user that executed the xact */ ProcNumber locking_backend; /* backend currently working on the xact */ @@ -167,7 +170,7 @@ typedef struct GlobalTransactionData bool ondisk; /* true if prepare state file is on disk */ bool inredo; /* true if entry was added via xlog_redo */ char gid[GIDSIZE]; /* The GID assigned to the prepared xact */ -} GlobalTransactionData; +} GlobalTransactionData; /* * Two Phase Commit shared state. Access to this struct is protected @@ -187,6 +190,14 @@ typedef struct TwoPhaseStateData static TwoPhaseStateData *TwoPhaseState; +static void TwoPhaseShmemRequest(void *arg); +static void TwoPhaseShmemInit(void *arg); + +const ShmemCallbacks TwoPhaseShmemCallbacks = { + .request_fn = TwoPhaseShmemRequest, + .init_fn = TwoPhaseShmemInit, +}; + /* * Global transaction entry currently locked by us, if any. Note that any * access to the entry pointed to by this variable must be protected by @@ -197,6 +208,7 @@ static GlobalTransaction MyLockedGxact = NULL; static bool twophaseExitRegistered = false; +static void PrepareRedoRemoveFull(FullTransactionId fxid, bool giveWarning); static void RecordTransactionCommitPrepared(TransactionId xid, int nchildren, TransactionId *children, @@ -216,25 +228,25 @@ static void RecordTransactionAbortPrepared(TransactionId xid, int nstats, xl_xact_stats_item *stats, const char *gid); -static void ProcessRecords(char *bufptr, TransactionId xid, +static void ProcessRecords(char *bufptr, FullTransactionId fxid, const TwoPhaseCallback callbacks[]); static void RemoveGXact(GlobalTransaction gxact); static void XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, int *len); -static char *ProcessTwoPhaseBuffer(TransactionId xid, +static char *ProcessTwoPhaseBuffer(FullTransactionId fxid, XLogRecPtr prepare_start_lsn, bool fromdisk, bool setParent, bool setNextXid); -static void MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, +static void MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid, const char *gid, TimestampTz prepared_at, Oid owner, Oid databaseid); -static void RemoveTwoPhaseFile(TransactionId xid, bool giveWarning); -static void RecreateTwoPhaseFile(TransactionId xid, void *content, int len); +static void RemoveTwoPhaseFile(FullTransactionId fxid, bool giveWarning); +static void RecreateTwoPhaseFile(FullTransactionId fxid, void *content, int len); /* - * Initialization of shared memory + * Register shared memory for two-phase state. */ -Size -TwoPhaseShmemSize(void) +static void +TwoPhaseShmemRequest(void *arg) { Size size; @@ -245,46 +257,40 @@ TwoPhaseShmemSize(void) size = MAXALIGN(size); size = add_size(size, mul_size(max_prepared_xacts, sizeof(GlobalTransactionData))); - - return size; + ShmemRequestStruct(.name = "Prepared Transaction Table", + .size = size, + .ptr = (void **) &TwoPhaseState, + ); } -void -TwoPhaseShmemInit(void) +/* + * Initialize shared memory for two-phase state. + */ +static void +TwoPhaseShmemInit(void *arg) { - bool found; - - TwoPhaseState = ShmemInitStruct("Prepared Transaction Table", - TwoPhaseShmemSize(), - &found); - if (!IsUnderPostmaster) - { - GlobalTransaction gxacts; - int i; + GlobalTransaction gxacts; + int i; - Assert(!found); - TwoPhaseState->freeGXacts = NULL; - TwoPhaseState->numPrepXacts = 0; + TwoPhaseState->freeGXacts = NULL; + TwoPhaseState->numPrepXacts = 0; - /* - * Initialize the linked list of free GlobalTransactionData structs - */ - gxacts = (GlobalTransaction) - ((char *) TwoPhaseState + - MAXALIGN(offsetof(TwoPhaseStateData, prepXacts) + - sizeof(GlobalTransaction) * max_prepared_xacts)); - for (i = 0; i < max_prepared_xacts; i++) - { - /* insert into linked list */ - gxacts[i].next = TwoPhaseState->freeGXacts; - TwoPhaseState->freeGXacts = &gxacts[i]; + /* + * Initialize the linked list of free GlobalTransactionData structs + */ + gxacts = (GlobalTransaction) + ((char *) TwoPhaseState + + MAXALIGN(offsetof(TwoPhaseStateData, prepXacts) + + sizeof(GlobalTransaction) * max_prepared_xacts)); + for (i = 0; i < max_prepared_xacts; i++) + { + /* insert into linked list */ + gxacts[i].next = TwoPhaseState->freeGXacts; + TwoPhaseState->freeGXacts = &gxacts[i]; - /* associate it with a PGPROC assigned by InitProcGlobal */ - gxacts[i].pgprocno = GetNumberFromPGProc(&PreparedXactProcs[i]); - } + /* associate it with a PGPROC assigned by ProcGlobalShmemInit */ + gxacts[i].pgprocno = GetNumberFromPGProc(&PreparedXactProcs[i]); } - else - Assert(found); } /* @@ -356,7 +362,7 @@ PostPrepare_Twophase(void) * Reserve the GID for the given transaction. */ GlobalTransaction -MarkAsPreparing(TransactionId xid, const char *gid, +MarkAsPreparing(FullTransactionId fxid, const char *gid, TimestampTz prepared_at, Oid owner, Oid databaseid) { GlobalTransaction gxact; @@ -407,7 +413,7 @@ MarkAsPreparing(TransactionId xid, const char *gid, gxact = TwoPhaseState->freeGXacts; TwoPhaseState->freeGXacts = gxact->next; - MarkAsPreparingGuts(gxact, xid, gid, prepared_at, owner, databaseid); + MarkAsPreparingGuts(gxact, fxid, gid, prepared_at, owner, databaseid); gxact->ondisk = false; @@ -430,11 +436,13 @@ MarkAsPreparing(TransactionId xid, const char *gid, * Note: This function should be called with appropriate locks held. */ static void -MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid, - TimestampTz prepared_at, Oid owner, Oid databaseid) +MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid, + const char *gid, TimestampTz prepared_at, Oid owner, + Oid databaseid) { PGPROC *proc; int i; + TransactionId xid = XidFromFullTransactionId(fxid); Assert(LWLockHeldByMeInMode(TwoPhaseStateLock, LW_EXCLUSIVE)); @@ -443,7 +451,6 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid, /* Initialize the PGPROC entry */ MemSet(proc, 0, sizeof(PGPROC)); - dlist_node_init(&proc->links); proc->waitStatus = PROC_WAIT_STATUS_OK; if (LocalTransactionIdIsValid(MyProc->vxid.lxid)) { @@ -466,10 +473,11 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid, proc->databaseId = databaseid; proc->roleId = owner; proc->tempNamespaceId = InvalidOid; - proc->isRegularBackend = false; + proc->backendType = B_INVALID; proc->lwWaiting = LW_WS_NOT_WAITING; proc->lwWaitMode = 0; proc->waitLock = NULL; + dlist_node_init(&proc->waitLink); proc->waitProcLock = NULL; pg_atomic_init_u64(&proc->waitStart, 0); for (i = 0; i < NUM_LOCK_PARTITIONS; i++) @@ -479,7 +487,7 @@ MarkAsPreparingGuts(GlobalTransaction gxact, TransactionId xid, const char *gid, proc->subxidStatus.count = 0; gxact->prepared_at = prepared_at; - gxact->xid = xid; + gxact->fxid = fxid; gxact->owner = owner; gxact->locking_backend = MyProcNumber; gxact->valid = false; @@ -680,7 +688,7 @@ GetPreparedTransactionList(GlobalTransaction *gxacts) } num = TwoPhaseState->numPrepXacts; - array = (GlobalTransaction) palloc(sizeof(GlobalTransactionData) * num); + array = palloc_array(GlobalTransactionData, num); *gxacts = array; for (i = 0; i < num; i++) memcpy(array + i, TwoPhaseState->prepXacts[i], @@ -740,13 +748,14 @@ pg_prepared_xact(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 5, "dbid", OIDOID, -1, 0); + TupleDescFinalize(tupdesc); funcctx->tuple_desc = BlessTupleDesc(tupdesc); /* * Collect all the 2PC status information that we will format and send * out as a result set. */ - status = (Working_State *) palloc(sizeof(Working_State)); + status = palloc_object(Working_State); funcctx->user_fctx = status; status->ngxacts = GetPreparedTransactionList(&status->array); @@ -797,12 +806,12 @@ pg_prepared_xact(PG_FUNCTION_ARGS) * caller had better hold it. */ static GlobalTransaction -TwoPhaseGetGXact(TransactionId xid, bool lock_held) +TwoPhaseGetGXact(FullTransactionId fxid, bool lock_held) { GlobalTransaction result = NULL; int i; - static TransactionId cached_xid = InvalidTransactionId; + static FullTransactionId cached_fxid = {InvalidTransactionId}; static GlobalTransaction cached_gxact = NULL; Assert(!lock_held || LWLockHeldByMe(TwoPhaseStateLock)); @@ -811,7 +820,7 @@ TwoPhaseGetGXact(TransactionId xid, bool lock_held) * During a recovery, COMMIT PREPARED, or ABORT PREPARED, we'll be called * repeatedly for the same XID. We can save work with a simple cache. */ - if (xid == cached_xid) + if (FullTransactionIdEquals(fxid, cached_fxid)) return cached_gxact; if (!lock_held) @@ -821,7 +830,7 @@ TwoPhaseGetGXact(TransactionId xid, bool lock_held) { GlobalTransaction gxact = TwoPhaseState->prepXacts[i]; - if (gxact->xid == xid) + if (FullTransactionIdEquals(gxact->fxid, fxid)) { result = gxact; break; @@ -832,9 +841,10 @@ TwoPhaseGetGXact(TransactionId xid, bool lock_held) LWLockRelease(TwoPhaseStateLock); if (result == NULL) /* should not happen */ - elog(ERROR, "failed to find GlobalTransaction for xid %u", xid); + elog(ERROR, "failed to find GlobalTransaction for xid %u", + XidFromFullTransactionId(fxid)); - cached_xid = xid; + cached_fxid = fxid; cached_gxact = result; return result; @@ -881,7 +891,7 @@ TwoPhaseGetXidByVirtualXID(VirtualTransactionId vxid, *have_more = true; break; } - result = gxact->xid; + result = XidFromFullTransactionId(gxact->fxid); } } @@ -892,32 +902,33 @@ TwoPhaseGetXidByVirtualXID(VirtualTransactionId vxid, /* * TwoPhaseGetDummyProcNumber - * Get the dummy proc number for prepared transaction specified by XID + * Get the dummy proc number for prepared transaction * * Dummy proc numbers are similar to proc numbers of real backends. They - * start at MaxBackends, and are unique across all currently active real - * backends and prepared transactions. If lock_held is set to true, - * TwoPhaseStateLock will not be taken, so the caller had better hold it. + * start at FIRST_PREPARED_XACT_PROC_NUMBER, and are unique across all + * currently active real backends and prepared transactions. If lock_held is + * set to true, TwoPhaseStateLock will not be taken, so the caller had better + * hold it. */ ProcNumber -TwoPhaseGetDummyProcNumber(TransactionId xid, bool lock_held) +TwoPhaseGetDummyProcNumber(FullTransactionId fxid, bool lock_held) { - GlobalTransaction gxact = TwoPhaseGetGXact(xid, lock_held); + GlobalTransaction gxact = TwoPhaseGetGXact(fxid, lock_held); return gxact->pgprocno; } /* * TwoPhaseGetDummyProc - * Get the PGPROC that represents a prepared transaction specified by XID + * Get the PGPROC that represents a prepared transaction * * If lock_held is set to true, TwoPhaseStateLock will not be taken, so the * caller had better hold it. */ PGPROC * -TwoPhaseGetDummyProc(TransactionId xid, bool lock_held) +TwoPhaseGetDummyProc(FullTransactionId fxid, bool lock_held) { - GlobalTransaction gxact = TwoPhaseGetGXact(xid, lock_held); + GlobalTransaction gxact = TwoPhaseGetGXact(fxid, lock_held); return GetPGProcByNumber(gxact->pgprocno); } @@ -942,10 +953,8 @@ AdjustToFullTransactionId(TransactionId xid) } static inline int -TwoPhaseFilePath(char *path, TransactionId xid) +TwoPhaseFilePath(char *path, FullTransactionId fxid) { - FullTransactionId fxid = AdjustToFullTransactionId(xid); - return snprintf(path, MAXPGPATH, TWOPHASE_DIR "/%08X%08X", EpochFromFullTransactionId(fxid), XidFromFullTransactionId(fxid)); @@ -1024,7 +1033,7 @@ save_state_data(const void *data, uint32 len) if (padlen > records.bytes_free) { - records.tail->next = palloc0(sizeof(StateFileChunk)); + records.tail->next = palloc0_object(StateFileChunk); records.tail = records.tail->next; records.tail->len = 0; records.tail->next = NULL; @@ -1034,7 +1043,7 @@ save_state_data(const void *data, uint32 len) records.tail->data = palloc(records.bytes_free); } - memcpy(((char *) records.tail->data) + records.tail->len, data, len); + memcpy(records.tail->data + records.tail->len, data, len); records.tail->len += padlen; records.bytes_free -= padlen; records.total_len += padlen; @@ -1049,7 +1058,7 @@ void StartPrepare(GlobalTransaction gxact) { PGPROC *proc = GetPGProcByNumber(gxact->pgprocno); - TransactionId xid = gxact->xid; + TransactionId xid = XidFromFullTransactionId(gxact->fxid); TwoPhaseFileHeader hdr; TransactionId *children; RelFileLocator *commitrels; @@ -1059,7 +1068,7 @@ StartPrepare(GlobalTransaction gxact) SharedInvalidationMessage *invalmsgs; /* Initialize linked list */ - records.head = palloc0(sizeof(StateFileChunk)); + records.head = palloc0_object(StateFileChunk); records.head->len = 0; records.head->next = NULL; @@ -1154,13 +1163,13 @@ EndPrepare(GlobalTransaction gxact) Assert(hdr->magic == TWOPHASE_MAGIC); hdr->total_len = records.total_len + sizeof(pg_crc32c); - replorigin = (replorigin_session_origin != InvalidRepOriginId && - replorigin_session_origin != DoNotReplicateId); + replorigin = (replorigin_xact_state.origin != InvalidReplOriginId && + replorigin_xact_state.origin != DoNotReplicateId); if (replorigin) { - hdr->origin_lsn = replorigin_session_origin_lsn; - hdr->origin_timestamp = replorigin_session_origin_timestamp; + hdr->origin_lsn = replorigin_xact_state.origin_lsn; + hdr->origin_timestamp = replorigin_xact_state.origin_timestamp; } /* @@ -1181,7 +1190,11 @@ EndPrepare(GlobalTransaction gxact) * starting immediately after the WAL record is inserted could complete * without fsync'ing our state file. (This is essentially the same kind * of race condition as the COMMIT-to-clog-write case that - * RecordTransactionCommit uses DELAY_CHKPT_START for; see notes there.) + * RecordTransactionCommit uses DELAY_CHKPT_IN_COMMIT for; see notes + * there.) Note that DELAY_CHKPT_IN_COMMIT is used to find transactions in + * the critical commit section. We need to know about such transactions + * for conflict detection in logical replication. See + * GetOldestActiveTransactionId(true, false) and its use. * * We save the PREPARE record's location in the gxact for later use by * CheckPointTwoPhase. @@ -1204,7 +1217,7 @@ EndPrepare(GlobalTransaction gxact) if (replorigin) { /* Move LSNs forward for this replication origin */ - replorigin_session_advance(replorigin_session_origin_lsn, + replorigin_session_advance(replorigin_xact_state.origin_lsn, gxact->prepare_end_lsn); } @@ -1281,10 +1294,11 @@ RegisterTwoPhaseRecord(TwoPhaseRmgrId rmid, uint16 info, * If it looks OK (has a valid magic number and CRC), return the palloc'd * contents of the file, issuing an error when finding corrupted data. If * missing_ok is true, which indicates that missing files can be safely - * ignored, then return NULL. This state can be reached when doing recovery. + * ignored, then return NULL. This state can be reached when doing recovery + * after discarding two-phase files from frozen epochs. */ static char * -ReadTwoPhaseFile(TransactionId xid, bool missing_ok) +ReadTwoPhaseFile(FullTransactionId fxid, bool missing_ok) { char path[MAXPGPATH]; char *buf; @@ -1296,7 +1310,7 @@ ReadTwoPhaseFile(TransactionId xid, bool missing_ok) file_crc; int r; - TwoPhaseFilePath(path, xid); + TwoPhaseFilePath(path, fxid); fd = OpenTransientFile(path, O_RDONLY | PG_BINARY); if (fd < 0) @@ -1426,12 +1440,12 @@ XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, int *len) if (errormsg) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read two-phase state from WAL at %X/%X: %s", + errmsg("could not read two-phase state from WAL at %X/%08X: %s", LSN_FORMAT_ARGS(lsn), errormsg))); else ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read two-phase state from WAL at %X/%X", + errmsg("could not read two-phase state from WAL at %X/%08X", LSN_FORMAT_ARGS(lsn)))); } @@ -1439,13 +1453,13 @@ XlogReadTwoPhaseData(XLogRecPtr lsn, char **buf, int *len) (XLogRecGetInfo(xlogreader) & XLOG_XACT_OPMASK) != XLOG_XACT_PREPARE) ereport(ERROR, (errcode_for_file_access(), - errmsg("expected two-phase state data is not present in WAL at %X/%X", + errmsg("expected two-phase state data is not present in WAL at %X/%08X", LSN_FORMAT_ARGS(lsn)))); if (len != NULL) *len = XLogRecGetDataLen(xlogreader); - *buf = palloc(sizeof(char) * XLogRecGetDataLen(xlogreader)); + *buf = palloc_array(char, XLogRecGetDataLen(xlogreader)); memcpy(*buf, XLogRecGetData(xlogreader), sizeof(char) * XLogRecGetDataLen(xlogreader)); XLogReaderFree(xlogreader); @@ -1461,6 +1475,7 @@ StandbyTransactionIdIsPrepared(TransactionId xid) char *buf; TwoPhaseFileHeader *hdr; bool result; + FullTransactionId fxid; Assert(TransactionIdIsValid(xid)); @@ -1468,7 +1483,8 @@ StandbyTransactionIdIsPrepared(TransactionId xid) return false; /* nothing to do */ /* Read and validate file */ - buf = ReadTwoPhaseFile(xid, true); + fxid = AdjustToFullTransactionId(xid); + buf = ReadTwoPhaseFile(fxid, true); if (buf == NULL) return false; @@ -1488,6 +1504,7 @@ FinishPreparedTransaction(const char *gid, bool isCommit) { GlobalTransaction gxact; PGPROC *proc; + FullTransactionId fxid; TransactionId xid; bool ondisk; char *buf; @@ -1509,7 +1526,8 @@ FinishPreparedTransaction(const char *gid, bool isCommit) */ gxact = LockGXact(gid, GetUserId()); proc = GetPGProcByNumber(gxact->pgprocno); - xid = gxact->xid; + fxid = gxact->fxid; + xid = XidFromFullTransactionId(fxid); /* * Read and validate 2PC state data. State data will typically be stored @@ -1517,7 +1535,7 @@ FinishPreparedTransaction(const char *gid, bool isCommit) * to disk if for some reason they have lived for a long time. */ if (gxact->ondisk) - buf = ReadTwoPhaseFile(xid, false); + buf = ReadTwoPhaseFile(fxid, false); else XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, NULL); @@ -1636,11 +1654,11 @@ FinishPreparedTransaction(const char *gid, bool isCommit) /* And now do the callbacks */ if (isCommit) - ProcessRecords(bufptr, xid, twophase_postcommit_callbacks); + ProcessRecords(bufptr, fxid, twophase_postcommit_callbacks); else - ProcessRecords(bufptr, xid, twophase_postabort_callbacks); + ProcessRecords(bufptr, fxid, twophase_postabort_callbacks); - PredicateLockTwoPhaseFinish(xid, isCommit); + PredicateLockTwoPhaseFinish(fxid, isCommit); /* * Read this value while holding the two-phase lock, as the on-disk 2PC @@ -1664,7 +1682,7 @@ FinishPreparedTransaction(const char *gid, bool isCommit) * And now we can clean up any files we may have left. */ if (ondisk) - RemoveTwoPhaseFile(xid, true); + RemoveTwoPhaseFile(fxid, true); MyLockedGxact = NULL; @@ -1677,7 +1695,7 @@ FinishPreparedTransaction(const char *gid, bool isCommit) * Scan 2PC state data in memory and call the indicated callbacks for each 2PC record. */ static void -ProcessRecords(char *bufptr, TransactionId xid, +ProcessRecords(char *bufptr, FullTransactionId fxid, const TwoPhaseCallback callbacks[]) { for (;;) @@ -1691,24 +1709,28 @@ ProcessRecords(char *bufptr, TransactionId xid, bufptr += MAXALIGN(sizeof(TwoPhaseRecordOnDisk)); if (callbacks[record->rmid] != NULL) - callbacks[record->rmid] (xid, record->info, bufptr, record->len); + callbacks[record->rmid] (fxid, record->info, bufptr, record->len); bufptr += MAXALIGN(record->len); } } /* - * Remove the 2PC file for the specified XID. + * Remove the 2PC file. * * If giveWarning is false, do not complain about file-not-present; * this is an expected case during WAL replay. + * + * This routine is used at early stages at recovery where future and + * past orphaned files are checked, hence the FullTransactionId to build + * a complete file name fit for the removal. */ static void -RemoveTwoPhaseFile(TransactionId xid, bool giveWarning) +RemoveTwoPhaseFile(FullTransactionId fxid, bool giveWarning) { char path[MAXPGPATH]; - TwoPhaseFilePath(path, xid); + TwoPhaseFilePath(path, fxid); if (unlink(path)) if (errno != ENOENT || giveWarning) ereport(WARNING, @@ -1723,7 +1745,7 @@ RemoveTwoPhaseFile(TransactionId xid, bool giveWarning) * Note: content and len don't include CRC. */ static void -RecreateTwoPhaseFile(TransactionId xid, void *content, int len) +RecreateTwoPhaseFile(FullTransactionId fxid, void *content, int len) { char path[MAXPGPATH]; pg_crc32c statefile_crc; @@ -1734,7 +1756,7 @@ RecreateTwoPhaseFile(TransactionId xid, void *content, int len) COMP_CRC32C(statefile_crc, content, len); FIN_CRC32C(statefile_crc); - TwoPhaseFilePath(path, xid); + TwoPhaseFilePath(path, fxid); fd = OpenTransientFile(path, O_CREAT | O_TRUNC | O_WRONLY | PG_BINARY); @@ -1846,7 +1868,7 @@ CheckPointTwoPhase(XLogRecPtr redo_horizon) int len; XlogReadTwoPhaseData(gxact->prepare_start_lsn, &buf, &len); - RecreateTwoPhaseFile(gxact->xid, buf, len); + RecreateTwoPhaseFile(gxact->fxid, buf, len); gxact->ondisk = true; gxact->prepare_start_lsn = InvalidXLogRecPtr; gxact->prepare_end_lsn = InvalidXLogRecPtr; @@ -1897,20 +1919,18 @@ restoreTwoPhaseData(void) if (strlen(clde->d_name) == 16 && strspn(clde->d_name, "0123456789ABCDEF") == 16) { - TransactionId xid; FullTransactionId fxid; char *buf; fxid = FullTransactionIdFromU64(strtou64(clde->d_name, NULL, 16)); - xid = XidFromFullTransactionId(fxid); - buf = ProcessTwoPhaseBuffer(xid, InvalidXLogRecPtr, + buf = ProcessTwoPhaseBuffer(fxid, InvalidXLogRecPtr, true, false, false); if (buf == NULL) continue; - PrepareRedoAdd(buf, InvalidXLogRecPtr, - InvalidXLogRecPtr, InvalidRepOriginId); + PrepareRedoAdd(fxid, buf, InvalidXLogRecPtr, + InvalidXLogRecPtr, InvalidReplOriginId); } } LWLockRelease(TwoPhaseStateLock); @@ -1968,9 +1988,7 @@ PrescanPreparedTransactions(TransactionId **xids_p, int *nxids_p) Assert(gxact->inredo); - xid = gxact->xid; - - buf = ProcessTwoPhaseBuffer(xid, + buf = ProcessTwoPhaseBuffer(gxact->fxid, gxact->prepare_start_lsn, gxact->ondisk, false, true); @@ -1981,6 +1999,7 @@ PrescanPreparedTransactions(TransactionId **xids_p, int *nxids_p) * OK, we think this file is valid. Incorporate xid into the * running-minimum result. */ + xid = XidFromFullTransactionId(gxact->fxid); if (TransactionIdPrecedes(xid, result)) result = xid; @@ -2036,15 +2055,12 @@ StandbyRecoverPreparedTransactions(void) LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE); for (i = 0; i < TwoPhaseState->numPrepXacts; i++) { - TransactionId xid; char *buf; GlobalTransaction gxact = TwoPhaseState->prepXacts[i]; Assert(gxact->inredo); - xid = gxact->xid; - - buf = ProcessTwoPhaseBuffer(xid, + buf = ProcessTwoPhaseBuffer(gxact->fxid, gxact->prepare_start_lsn, gxact->ondisk, true, false); if (buf != NULL) @@ -2077,16 +2093,14 @@ RecoverPreparedTransactions(void) LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE); for (i = 0; i < TwoPhaseState->numPrepXacts; i++) { - TransactionId xid; char *buf; GlobalTransaction gxact = TwoPhaseState->prepXacts[i]; + FullTransactionId fxid = gxact->fxid; char *bufptr; TwoPhaseFileHeader *hdr; TransactionId *subxids; const char *gid; - xid = gxact->xid; - /* * Reconstruct subtrans state for the transaction --- needed because * pg_subtrans is not preserved over a restart. Note that we are @@ -2096,17 +2110,20 @@ RecoverPreparedTransactions(void) * SubTransSetParent has been set before, if the prepared transaction * generated xid assignment records. */ - buf = ProcessTwoPhaseBuffer(xid, + buf = ProcessTwoPhaseBuffer(gxact->fxid, gxact->prepare_start_lsn, gxact->ondisk, true, false); if (buf == NULL) continue; ereport(LOG, - (errmsg("recovering prepared transaction %u from shared memory", xid))); + (errmsg("recovering prepared transaction %u of epoch %u from shared memory", + XidFromFullTransactionId(gxact->fxid), + EpochFromFullTransactionId(gxact->fxid)))); hdr = (TwoPhaseFileHeader *) buf; - Assert(TransactionIdEquals(hdr->xid, xid)); + Assert(TransactionIdEquals(hdr->xid, + XidFromFullTransactionId(gxact->fxid))); bufptr = buf + MAXALIGN(sizeof(TwoPhaseFileHeader)); gid = (const char *) bufptr; bufptr += MAXALIGN(hdr->gidlen); @@ -2122,7 +2139,7 @@ RecoverPreparedTransactions(void) * Recreate its GXACT and dummy PGPROC. But, check whether it was * added in redo and already has a shmem entry for it. */ - MarkAsPreparingGuts(gxact, xid, gid, + MarkAsPreparingGuts(gxact, gxact->fxid, gid, hdr->prepared_at, hdr->owner, hdr->database); @@ -2137,7 +2154,7 @@ RecoverPreparedTransactions(void) /* * Recover other state (notably locks) using resource managers. */ - ProcessRecords(bufptr, xid, twophase_recover_callbacks); + ProcessRecords(bufptr, fxid, twophase_recover_callbacks); /* * Release locks held by the standby process after we process each @@ -2145,7 +2162,7 @@ RecoverPreparedTransactions(void) * additional locks at any one time. */ if (InHotStandby) - StandbyReleaseLockTree(xid, hdr->nsubxacts, subxids); + StandbyReleaseLockTree(hdr->xid, hdr->nsubxacts, subxids); /* * We're done with recovering this transaction. Clear MyLockedGxact, @@ -2164,7 +2181,7 @@ RecoverPreparedTransactions(void) /* * ProcessTwoPhaseBuffer * - * Given a transaction id, read it either from disk or read it directly + * Given a FullTransactionId, read it either from disk or read it directly * via shmem xlog record pointer using the provided "prepare_start_lsn". * * If setParent is true, set up subtransaction parent linkages. @@ -2173,13 +2190,12 @@ RecoverPreparedTransactions(void) * value scanned. */ static char * -ProcessTwoPhaseBuffer(TransactionId xid, +ProcessTwoPhaseBuffer(FullTransactionId fxid, XLogRecPtr prepare_start_lsn, bool fromdisk, bool setParent, bool setNextXid) { FullTransactionId nextXid = TransamVariables->nextXid; - TransactionId origNextXid = XidFromFullTransactionId(nextXid); TransactionId *subxids; char *buf; TwoPhaseFileHeader *hdr; @@ -2188,44 +2204,49 @@ ProcessTwoPhaseBuffer(TransactionId xid, Assert(LWLockHeldByMeInMode(TwoPhaseStateLock, LW_EXCLUSIVE)); if (!fromdisk) - Assert(prepare_start_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(prepare_start_lsn)); /* Already processed? */ - if (TransactionIdDidCommit(xid) || TransactionIdDidAbort(xid)) + if (TransactionIdDidCommit(XidFromFullTransactionId(fxid)) || + TransactionIdDidAbort(XidFromFullTransactionId(fxid))) { if (fromdisk) { ereport(WARNING, - (errmsg("removing stale two-phase state file for transaction %u", - xid))); - RemoveTwoPhaseFile(xid, true); + (errmsg("removing stale two-phase state file for transaction %u of epoch %u", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)))); + RemoveTwoPhaseFile(fxid, true); } else { ereport(WARNING, - (errmsg("removing stale two-phase state from memory for transaction %u", - xid))); - PrepareRedoRemove(xid, true); + (errmsg("removing stale two-phase state from memory for transaction %u of epoch %u", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)))); + PrepareRedoRemoveFull(fxid, true); } return NULL; } /* Reject XID if too new */ - if (TransactionIdFollowsOrEquals(xid, origNextXid)) + if (FullTransactionIdFollowsOrEquals(fxid, nextXid)) { if (fromdisk) { ereport(WARNING, - (errmsg("removing future two-phase state file for transaction %u", - xid))); - RemoveTwoPhaseFile(xid, true); + (errmsg("removing future two-phase state file for transaction %u of epoch %u", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)))); + RemoveTwoPhaseFile(fxid, true); } else { ereport(WARNING, - (errmsg("removing future two-phase state from memory for transaction %u", - xid))); - PrepareRedoRemove(xid, true); + (errmsg("removing future two-phase state from memory for transaction %u of epoch %u", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)))); + PrepareRedoRemoveFull(fxid, true); } return NULL; } @@ -2233,7 +2254,7 @@ ProcessTwoPhaseBuffer(TransactionId xid, if (fromdisk) { /* Read and validate file */ - buf = ReadTwoPhaseFile(xid, false); + buf = ReadTwoPhaseFile(fxid, false); } else { @@ -2243,18 +2264,20 @@ ProcessTwoPhaseBuffer(TransactionId xid, /* Deconstruct header */ hdr = (TwoPhaseFileHeader *) buf; - if (!TransactionIdEquals(hdr->xid, xid)) + if (!TransactionIdEquals(hdr->xid, XidFromFullTransactionId(fxid))) { if (fromdisk) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted two-phase state file for transaction %u", - xid))); + errmsg("corrupted two-phase state file for transaction %u of epoch %u", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)))); else ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted two-phase state in memory for transaction %u", - xid))); + errmsg("corrupted two-phase state in memory for transaction %u of epoch %u", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)))); } /* @@ -2268,14 +2291,14 @@ ProcessTwoPhaseBuffer(TransactionId xid, { TransactionId subxid = subxids[i]; - Assert(TransactionIdFollows(subxid, xid)); + Assert(TransactionIdFollows(subxid, XidFromFullTransactionId(fxid))); /* update nextXid if needed */ if (setNextXid) AdvanceNextFullTransactionIdPastXid(subxid); if (setParent) - SubTransSetParent(subxid, xid); + SubTransSetParent(subxid, XidFromFullTransactionId(fxid)); } return buf; @@ -2286,7 +2309,7 @@ ProcessTwoPhaseBuffer(TransactionId xid, * RecordTransactionCommitPrepared * * This is basically the same as RecordTransactionCommit (q.v. if you change - * this function): in particular, we must set DELAY_CHKPT_START to avoid a + * this function): in particular, we must set DELAY_CHKPT_IN_COMMIT to avoid a * race condition. * * We know the transaction made at least one XLOG entry (its PREPARE), @@ -2306,21 +2329,42 @@ RecordTransactionCommitPrepared(TransactionId xid, const char *gid) { XLogRecPtr recptr; - TimestampTz committs = GetCurrentTimestamp(); + TimestampTz committs; bool replorigin; /* * Are we using the replication origins feature? Or, in other words, are * we replaying remote actions? */ - replorigin = (replorigin_session_origin != InvalidRepOriginId && - replorigin_session_origin != DoNotReplicateId); + replorigin = (replorigin_xact_state.origin != InvalidReplOriginId && + replorigin_xact_state.origin != DoNotReplicateId); + + /* Load the injection point before entering the critical section */ + INJECTION_POINT_LOAD("commit-after-delay-checkpoint"); START_CRIT_SECTION(); /* See notes in RecordTransactionCommit */ - Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) == 0); - MyProc->delayChkptFlags |= DELAY_CHKPT_START; + Assert((MyProc->delayChkptFlags & DELAY_CHKPT_IN_COMMIT) == 0); + MyProc->delayChkptFlags |= DELAY_CHKPT_IN_COMMIT; + + INJECTION_POINT_CACHED("commit-after-delay-checkpoint", NULL); + + /* + * Ensures the DELAY_CHKPT_IN_COMMIT flag write is globally visible before + * commit time is written. + */ + pg_write_barrier(); + + /* + * Note it is important to set committs value after marking ourselves as + * in the commit critical section (DELAY_CHKPT_IN_COMMIT). This is because + * we want to ensure all transactions that have acquired commit timestamp + * are finished before we allow the logical replication client to advance + * its xid which is used to hold back dead rows for conflict detection. + * See comments atop worker.c. + */ + committs = GetCurrentTimestamp(); /* * Emit the XLOG commit record. Note that we mark 2PC commits as @@ -2338,23 +2382,23 @@ RecordTransactionCommitPrepared(TransactionId xid, if (replorigin) /* Move LSNs forward for this replication origin */ - replorigin_session_advance(replorigin_session_origin_lsn, + replorigin_session_advance(replorigin_xact_state.origin_lsn, XactLastRecEnd); /* * Record commit timestamp. The value comes from plain commit timestamp * if replorigin is not enabled, or replorigin already set a value for us - * in replorigin_session_origin_timestamp otherwise. + * in replorigin_xact_state.origin_timestamp otherwise. * * We don't need to WAL-log anything here, as the commit record written * above already contains the data. */ - if (!replorigin || replorigin_session_origin_timestamp == 0) - replorigin_session_origin_timestamp = committs; + if (!replorigin || replorigin_xact_state.origin_timestamp == 0) + replorigin_xact_state.origin_timestamp = committs; TransactionTreeSetCommitTsData(xid, nchildren, children, - replorigin_session_origin_timestamp, - replorigin_session_origin); + replorigin_xact_state.origin_timestamp, + replorigin_xact_state.origin); /* * We don't currently try to sleep before flush here ... nor is there any @@ -2369,7 +2413,7 @@ RecordTransactionCommitPrepared(TransactionId xid, TransactionIdCommitTree(xid, nchildren, children); /* Checkpoint can proceed now */ - MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + MyProc->delayChkptFlags &= ~DELAY_CHKPT_IN_COMMIT; END_CRIT_SECTION(); @@ -2407,8 +2451,8 @@ RecordTransactionAbortPrepared(TransactionId xid, * Are we using the replication origins feature? Or, in other words, are * we replaying remote actions? */ - replorigin = (replorigin_session_origin != InvalidRepOriginId && - replorigin_session_origin != DoNotReplicateId); + replorigin = (replorigin_xact_state.origin != InvalidReplOriginId && + replorigin_xact_state.origin != DoNotReplicateId); /* * Catch the scenario where we aborted partway through @@ -2434,7 +2478,7 @@ RecordTransactionAbortPrepared(TransactionId xid, if (replorigin) /* Move LSNs forward for this replication origin */ - replorigin_session_advance(replorigin_session_origin_lsn, + replorigin_session_advance(replorigin_xact_state.origin_lsn, XactLastRecEnd); /* Always flush, since we're about to remove the 2PC state file */ @@ -2466,8 +2510,9 @@ RecordTransactionAbortPrepared(TransactionId xid, * data, the entry is marked as located on disk. */ void -PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, - XLogRecPtr end_lsn, RepOriginId origin_id) +PrepareRedoAdd(FullTransactionId fxid, char *buf, + XLogRecPtr start_lsn, XLogRecPtr end_lsn, + ReplOriginId origin_id) { TwoPhaseFileHeader *hdr = (TwoPhaseFileHeader *) buf; char *bufptr; @@ -2477,6 +2522,13 @@ PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, Assert(LWLockHeldByMeInMode(TwoPhaseStateLock, LW_EXCLUSIVE)); Assert(RecoveryInProgress()); + if (!FullTransactionIdIsValid(fxid)) + { + Assert(InRecovery); + fxid = FullTransactionIdFromAllowableAt(TransamVariables->nextXid, + hdr->xid); + } + bufptr = buf + MAXALIGN(sizeof(TwoPhaseFileHeader)); gid = (const char *) bufptr; @@ -2501,18 +2553,19 @@ PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, * the record is added to TwoPhaseState and it should have no * corresponding file in pg_twophase. */ - if (!XLogRecPtrIsInvalid(start_lsn)) + if (XLogRecPtrIsValid(start_lsn)) { char path[MAXPGPATH]; - TwoPhaseFilePath(path, hdr->xid); + Assert(InRecovery); + TwoPhaseFilePath(path, fxid); if (access(path, F_OK) == 0) { ereport(reachedConsistency ? ERROR : WARNING, (errmsg("could not recover two-phase state file for transaction %u", hdr->xid), - errdetail("Two-phase state file has been found in WAL record %X/%X, but this transaction has already been restored from disk.", + errdetail("Two-phase state file has been found in WAL record %X/%08X, but this transaction has already been restored from disk.", LSN_FORMAT_ARGS(start_lsn)))); return; } @@ -2536,11 +2589,11 @@ PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, gxact->prepared_at = hdr->prepared_at; gxact->prepare_start_lsn = start_lsn; gxact->prepare_end_lsn = end_lsn; - gxact->xid = hdr->xid; + gxact->fxid = fxid; gxact->owner = hdr->owner; gxact->locking_backend = INVALID_PROC_NUMBER; gxact->valid = false; - gxact->ondisk = XLogRecPtrIsInvalid(start_lsn); + gxact->ondisk = !XLogRecPtrIsValid(start_lsn); gxact->inredo = true; /* yes, added in redo */ strcpy(gxact->gid, gid); @@ -2548,18 +2601,20 @@ PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, Assert(TwoPhaseState->numPrepXacts < max_prepared_xacts); TwoPhaseState->prepXacts[TwoPhaseState->numPrepXacts++] = gxact; - if (origin_id != InvalidRepOriginId) + if (origin_id != InvalidReplOriginId) { /* recover apply progress */ replorigin_advance(origin_id, hdr->origin_lsn, end_lsn, false /* backward */ , false /* WAL */ ); } - elog(DEBUG2, "added 2PC data in shared memory for transaction %u", gxact->xid); + elog(DEBUG2, "added 2PC data in shared memory for transaction %u of epoch %u", + XidFromFullTransactionId(gxact->fxid), + EpochFromFullTransactionId(gxact->fxid)); } /* - * PrepareRedoRemove + * PrepareRedoRemoveFull * * Remove the corresponding gxact entry from TwoPhaseState. Also remove * the 2PC file if a prepared transaction was saved via an earlier checkpoint. @@ -2567,8 +2622,8 @@ PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, * Caller must hold TwoPhaseStateLock in exclusive mode, because TwoPhaseState * is updated. */ -void -PrepareRedoRemove(TransactionId xid, bool giveWarning) +static void +PrepareRedoRemoveFull(FullTransactionId fxid, bool giveWarning) { GlobalTransaction gxact = NULL; int i; @@ -2581,7 +2636,7 @@ PrepareRedoRemove(TransactionId xid, bool giveWarning) { gxact = TwoPhaseState->prepXacts[i]; - if (gxact->xid == xid) + if (FullTransactionIdEquals(gxact->fxid, fxid)) { Assert(gxact->inredo); found = true; @@ -2598,12 +2653,28 @@ PrepareRedoRemove(TransactionId xid, bool giveWarning) /* * And now we can clean up any files we may have left. */ - elog(DEBUG2, "removing 2PC data for transaction %u", xid); + elog(DEBUG2, "removing 2PC data for transaction %u of epoch %u ", + XidFromFullTransactionId(fxid), + EpochFromFullTransactionId(fxid)); + if (gxact->ondisk) - RemoveTwoPhaseFile(xid, giveWarning); + RemoveTwoPhaseFile(fxid, giveWarning); + RemoveGXact(gxact); } +/* + * Wrapper of PrepareRedoRemoveFull(), for TransactionIds. + */ +void +PrepareRedoRemove(TransactionId xid, bool giveWarning) +{ + FullTransactionId fxid = + FullTransactionIdFromAllowableAt(TransamVariables->nextXid, xid); + + PrepareRedoRemoveFull(fxid, giveWarning); +} + /* * LookupGXact * Check if the prepared transaction with the given GID, lsn and timestamp @@ -2648,7 +2719,7 @@ LookupGXact(const char *gid, XLogRecPtr prepare_end_lsn, * between publisher and subscriber. */ if (gxact->ondisk) - buf = ReadTwoPhaseFile(gxact->xid, false); + buf = ReadTwoPhaseFile(gxact->fxid, false); else { Assert(gxact->prepare_start_lsn); @@ -2750,3 +2821,58 @@ LookupGXactBySubid(Oid subid) return found; } + +/* + * TwoPhaseGetOldestXidInCommit + * Return the oldest transaction ID from prepared transactions that are + * currently in the commit critical section. + * + * This function only considers transactions in the currently connected + * database. If no matching transactions are found, it returns + * InvalidTransactionId. + */ +TransactionId +TwoPhaseGetOldestXidInCommit(void) +{ + TransactionId oldestRunningXid = InvalidTransactionId; + + LWLockAcquire(TwoPhaseStateLock, LW_SHARED); + + for (int i = 0; i < TwoPhaseState->numPrepXacts; i++) + { + GlobalTransaction gxact = TwoPhaseState->prepXacts[i]; + PGPROC *commitproc; + TransactionId xid; + + if (!gxact->valid) + continue; + + if (gxact->locking_backend == INVALID_PROC_NUMBER) + continue; + + /* + * Get the backend that is handling the transaction. It's safe to + * access this backend while holding TwoPhaseStateLock, as the backend + * can only be destroyed after either removing or unlocking the + * current global transaction, both of which require an exclusive + * TwoPhaseStateLock. + */ + commitproc = GetPGProcByNumber(gxact->locking_backend); + + if (MyDatabaseId != commitproc->databaseId) + continue; + + if ((commitproc->delayChkptFlags & DELAY_CHKPT_IN_COMMIT) == 0) + continue; + + xid = XidFromFullTransactionId(gxact->fxid); + + if (!TransactionIdIsValid(oldestRunningXid) || + TransactionIdPrecedes(xid, oldestRunningXid)) + oldestRunningXid = xid; + } + + LWLockRelease(TwoPhaseStateLock); + + return oldestRunningXid; +} diff --git a/src/backend/access/transam/twophase_rmgr.c b/src/backend/access/transam/twophase_rmgr.c index b638e0949e77e..fae254c6e2364 100644 --- a/src/backend/access/transam/twophase_rmgr.c +++ b/src/backend/access/transam/twophase_rmgr.c @@ -3,7 +3,7 @@ * twophase_rmgr.c * Two-phase-commit resource managers tables * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/access/transam/varsup.c b/src/backend/access/transam/varsup.c index fe895787cb72d..dc5e32d86f349 100644 --- a/src/backend/access/transam/varsup.c +++ b/src/backend/access/transam/varsup.c @@ -3,7 +3,7 @@ * varsup.c * postgres OID & XID variables support routines * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/access/transam/varsup.c @@ -19,46 +19,37 @@ #include "access/transam.h" #include "access/xact.h" #include "access/xlogutils.h" -#include "commands/dbcommands.h" #include "miscadmin.h" #include "postmaster/autovacuum.h" #include "storage/pmsignal.h" #include "storage/proc.h" +#include "storage/subsystems.h" +#include "utils/lsyscache.h" #include "utils/syscache.h" /* Number of OIDs to prefetch (preallocate) per XLOG write */ #define VAR_OID_PREFETCH 8192 +static void VarsupShmemRequest(void *arg); + /* pointer to variables struct in shared memory */ TransamVariablesData *TransamVariables = NULL; +const ShmemCallbacks VarsupShmemCallbacks = { + .request_fn = VarsupShmemRequest, +}; /* - * Initialization of shared memory for TransamVariables. + * Request shared memory for TransamVariables. */ -Size -VarsupShmemSize(void) -{ - return sizeof(TransamVariablesData); -} - -void -VarsupShmemInit(void) +static void +VarsupShmemRequest(void *arg) { - bool found; - - /* Initialize our shared state struct */ - TransamVariables = ShmemInitStruct("TransamVariables", - sizeof(TransamVariablesData), - &found); - if (!IsUnderPostmaster) - { - Assert(!found); - memset(TransamVariables, 0, sizeof(TransamVariablesData)); - } - else - Assert(found); + ShmemRequestStruct(.name = "TransamVariables", + .size = sizeof(TransamVariablesData), + .ptr = (void **) &TransamVariables, + ); } /* @@ -175,6 +166,8 @@ GetNewTransactionId(bool isSubXact) (errmsg("database \"%s\" must be vacuumed within %u transactions", oldest_datname, xidWrapLimit - xid), + errdetail("Approximately %.2f%% of transaction IDs are available for use.", + (double) (xidWrapLimit - xid) / (MaxTransactionId / 2) * 100), errhint("To avoid transaction ID assignment failures, execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); else @@ -182,6 +175,8 @@ GetNewTransactionId(bool isSubXact) (errmsg("database with OID %u must be vacuumed within %u transactions", oldest_datoid, xidWrapLimit - xid), + errdetail("Approximately %.2f%% of transaction IDs are available for use.", + (double) (xidWrapLimit - xid) / (MaxTransactionId / 2) * 100), errhint("To avoid XID assignment failures, execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); } @@ -407,16 +402,16 @@ SetTransactionIdLimit(TransactionId oldest_datfrozenxid, Oid oldest_datoid) xidStopLimit -= FirstNormalTransactionId; /* - * We'll start complaining loudly when we get within 40M transactions of + * We'll start complaining loudly when we get within 100M transactions of * data loss. This is kind of arbitrary, but if you let your gas gauge - * get down to 2% of full, would you be looking for the next gas station? + * get down to 5% of full, would you be looking for the next gas station? * We need to be fairly liberal about this number because there are lots * of scenarios where most transactions are done by automatic clients that * won't pay attention to warnings. (No, we're not gonna make this * configurable. If you know enough to configure it, you know enough to * not get in this kind of trouble in the first place.) */ - xidWarnLimit = xidWrapLimit - 40000000; + xidWarnLimit = xidWrapLimit - 100000000; if (xidWarnLimit < FirstNormalTransactionId) xidWarnLimit -= FirstNormalTransactionId; @@ -490,6 +485,8 @@ SetTransactionIdLimit(TransactionId oldest_datfrozenxid, Oid oldest_datoid) (errmsg("database \"%s\" must be vacuumed within %u transactions", oldest_datname, xidWrapLimit - curXid), + errdetail("Approximately %.2f%% of transaction IDs are available for use.", + (double) (xidWrapLimit - curXid) / (MaxTransactionId / 2) * 100), errhint("To avoid XID assignment failures, execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); else @@ -497,6 +494,8 @@ SetTransactionIdLimit(TransactionId oldest_datfrozenxid, Oid oldest_datoid) (errmsg("database with OID %u must be vacuumed within %u transactions", oldest_datoid, xidWrapLimit - curXid), + errdetail("Approximately %.2f%% of transaction IDs are available for use.", + (double) (xidWrapLimit - curXid) / (MaxTransactionId / 2) * 100), errhint("To avoid XID assignment failures, execute a database-wide VACUUM in that database.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); } diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index b885513f76541..48bc90c967353 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -5,7 +5,7 @@ * * See src/backend/access/transam/README for more information. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -31,6 +31,7 @@ #include "access/xloginsert.h" #include "access/xlogrecovery.h" #include "access/xlogutils.h" +#include "access/xlogwait.h" #include "catalog/index.h" #include "catalog/namespace.h" #include "catalog/pg_enum.h" @@ -71,6 +72,7 @@ #include "utils/timeout.h" #include "utils/timestamp.h" #include "utils/typcache.h" +#include "utils/wait_event.h" /* * User-tweakable parameters @@ -551,9 +553,9 @@ MarkCurrentTransactionIdLoggedIfAny(void) * operation in a subtransaction. We require that for logical decoding, see * LogicalDecodingProcessRecord. * - * This returns true if wal_level >= logical and we are inside a valid - * subtransaction, for which the assignment was not yet written to any WAL - * record. + * This returns true if effective_wal_level is logical and we are inside + * a valid subtransaction, for which the assignment was not yet written to + * any WAL record. */ bool IsSubxactTopXidLogPending(void) @@ -562,7 +564,7 @@ IsSubxactTopXidLogPending(void) if (CurrentTransactionState->topXidLogged) return false; - /* wal_level has to be logical */ + /* effective_wal_level has to be logical */ if (!XLogLogicalInfoActive()) return false; @@ -663,7 +665,7 @@ AssignTransactionId(TransactionState s) TransactionState *parents; size_t parentOffset = 0; - parents = palloc(sizeof(TransactionState) * s->nestingLevel); + parents = palloc_array(TransactionState, s->nestingLevel); while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId)) { parents[parentOffset++] = p; @@ -681,14 +683,14 @@ AssignTransactionId(TransactionState s) } /* - * When wal_level=logical, guarantee that a subtransaction's xid can only - * be seen in the WAL stream if its toplevel xid has been logged before. - * If necessary we log an xact_assignment record with fewer than - * PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't set - * for a transaction even though it appears in a WAL record, we just might - * superfluously log something. That can happen when an xid is included - * somewhere inside a wal record, but not in XLogRecord->xl_xid, like in - * xl_standby_locks. + * When effective_wal_level is logical, guarantee that a subtransaction's + * xid can only be seen in the WAL stream if its toplevel xid has been + * logged before. If necessary we log an xact_assignment record with fewer + * than PGPROC_MAX_CACHED_SUBXIDS. Note that it is fine if didLogXid isn't + * set for a transaction even though it appears in a WAL record, we just + * might superfluously log something. That can happen when an xid is + * included somewhere inside a wal record, but not in XLogRecord->xl_xid, + * like in xl_standby_locks. */ if (isSubXact && XLogLogicalInfoActive() && !TopTransactionStateData.didLogXid) @@ -1044,6 +1046,34 @@ TransactionStartedDuringRecovery(void) return CurrentTransactionState->startedInRecovery; } +/* + * GetTopReadOnlyTransactionNestLevel + * + * Note: this will return zero when not inside any transaction or when neither + * a top-level transaction nor subtransactions are read-only, one when the + * top-level transaction is read-only, two when one level of subtransaction is + * read-only, etc. + * + * Note: subtransactions of the topmost read-only transaction are also + * read-only, because they inherit read-only mode from the transaction, and + * thus can't change to read-write mode (see check_transaction_read_only). + */ +int +GetTopReadOnlyTransactionNestLevel(void) +{ + TransactionState s = CurrentTransactionState; + + if (!XactReadOnly) + return 0; + while (s->nestingLevel > 1) + { + if (!s->prevXactReadOnly) + return s->nestingLevel; + s = s->parent; + } + return s->nestingLevel; +} + /* * EnterParallelMode */ @@ -1412,8 +1442,8 @@ RecordTransactionCommit(void) * Are we using the replication origins feature? Or, in other words, * are we replaying remote actions? */ - replorigin = (replorigin_session_origin != InvalidRepOriginId && - replorigin_session_origin != DoNotReplicateId); + replorigin = (replorigin_xact_state.origin != InvalidReplOriginId && + replorigin_xact_state.origin != DoNotReplicateId); /* * Mark ourselves as within our "commit critical section". This @@ -1431,10 +1461,22 @@ RecordTransactionCommit(void) * without holding the ProcArrayLock, since we're the only one * modifying it. This makes checkpoint's determination of which xacts * are delaying the checkpoint a bit fuzzy, but it doesn't matter. + * + * Note, it is important to get the commit timestamp after marking the + * transaction in the commit critical section. See + * RecordTransactionCommitPrepared. */ - Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) == 0); + Assert((MyProc->delayChkptFlags & DELAY_CHKPT_IN_COMMIT) == 0); START_CRIT_SECTION(); - MyProc->delayChkptFlags |= DELAY_CHKPT_START; + MyProc->delayChkptFlags |= DELAY_CHKPT_IN_COMMIT; + + Assert(xactStopTimestamp == 0); + + /* + * Ensures the DELAY_CHKPT_IN_COMMIT flag write is globally visible + * before commit time is written. + */ + pg_write_barrier(); /* * Insert the commit XLOG record. @@ -1449,25 +1491,25 @@ RecordTransactionCommit(void) if (replorigin) /* Move LSNs forward for this replication origin */ - replorigin_session_advance(replorigin_session_origin_lsn, + replorigin_session_advance(replorigin_xact_state.origin_lsn, XactLastRecEnd); /* * Record commit timestamp. The value comes from plain commit * timestamp if there's no replication origin; otherwise, the - * timestamp was already set in replorigin_session_origin_timestamp by - * replication. + * timestamp was already set in replorigin_xact_state.origin_timestamp + * by replication. * * We don't need to WAL-log anything here, as the commit record * written above already contains the data. */ - if (!replorigin || replorigin_session_origin_timestamp == 0) - replorigin_session_origin_timestamp = GetCurrentTransactionStopTimestamp(); + if (!replorigin || replorigin_xact_state.origin_timestamp == 0) + replorigin_xact_state.origin_timestamp = GetCurrentTransactionStopTimestamp(); TransactionTreeSetCommitTsData(xid, nchildren, children, - replorigin_session_origin_timestamp, - replorigin_session_origin); + replorigin_xact_state.origin_timestamp, + replorigin_xact_state.origin); } /* @@ -1537,7 +1579,7 @@ RecordTransactionCommit(void) */ if (markXidCommitted) { - MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + MyProc->delayChkptFlags &= ~DELAY_CHKPT_IN_COMMIT; END_CRIT_SECTION(); } @@ -1797,8 +1839,8 @@ RecordTransactionAbort(bool isSubXact) * Are we using the replication origins feature? Or, in other words, are * we replaying remote actions? */ - replorigin = (replorigin_session_origin != InvalidRepOriginId && - replorigin_session_origin != DoNotReplicateId); + replorigin = (replorigin_xact_state.origin != InvalidReplOriginId && + replorigin_xact_state.origin != DoNotReplicateId); /* Fetch the data we need for the abort record */ nrels = smgrGetPendingDeletes(false, &rels); @@ -1825,7 +1867,7 @@ RecordTransactionAbort(bool isSubXact) if (replorigin) /* Move LSNs forward for this replication origin */ - replorigin_session_advance(replorigin_session_origin_lsn, + replorigin_session_advance(replorigin_xact_state.origin_lsn, XactLastRecEnd); /* @@ -2476,6 +2518,7 @@ CommitTransaction(void) AtEOXact_Snapshot(true, false); AtEOXact_ApplyLauncher(true); AtEOXact_LogicalRepWorkers(true); + AtEOXact_LogicalCtl(); pgstat_report_xact_timestamp(0); ResourceOwnerDelete(TopTransactionResourceOwner); @@ -2515,7 +2558,7 @@ static void PrepareTransaction(void) { TransactionState s = CurrentTransactionState; - TransactionId xid = GetCurrentTransactionId(); + FullTransactionId fxid = GetCurrentFullTransactionId(); GlobalTransaction gxact; TimestampTz prepared_at; @@ -2644,7 +2687,7 @@ PrepareTransaction(void) * Reserve the GID for this transaction. This could fail if the requested * GID is invalid or already in use. */ - gxact = MarkAsPreparing(xid, prepareGID, prepared_at, + gxact = MarkAsPreparing(fxid, prepareGID, prepared_at, GetUserId(), MyDatabaseId); prepareGID = NULL; @@ -2694,7 +2737,7 @@ PrepareTransaction(void) * ProcArrayClearTransaction(). Otherwise, a GetLockConflicts() would * conclude "xact already committed or aborted" for our locks. */ - PostPrepare_Locks(xid); + PostPrepare_Locks(fxid); /* * Let others know about no transaction in progress by me. This has to be @@ -2738,9 +2781,9 @@ PrepareTransaction(void) PostPrepare_smgr(); - PostPrepare_MultiXact(xid); + PostPrepare_MultiXact(fxid); - PostPrepare_PredicateLocks(xid); + PostPrepare_PredicateLocks(fxid); ResourceOwnerRelease(TopTransactionResourceOwner, RESOURCE_RELEASE_LOCKS, @@ -2771,6 +2814,7 @@ PrepareTransaction(void) /* we treat PREPARE as ROLLBACK so far as waking workers goes */ AtEOXact_ApplyLauncher(false); AtEOXact_LogicalRepWorkers(false); + AtEOXact_LogicalCtl(); pgstat_report_xact_timestamp(0); CurrentResourceOwner = NULL; @@ -2831,6 +2875,11 @@ AbortTransaction(void) */ LWLockReleaseAll(); + /* + * Cleanup waiting for LSN if any. + */ + WaitLSNCleanup(); + /* Clear wait information and command progress indicator */ pgstat_report_wait_end(); pgstat_progress_end_command(); @@ -2993,6 +3042,7 @@ AbortTransaction(void) AtEOXact_PgStat(false, is_parallel_worker); AtEOXact_ApplyLauncher(false); AtEOXact_LogicalRepWorkers(false); + AtEOXact_LogicalCtl(); pgstat_report_xact_timestamp(0); } @@ -3674,7 +3724,8 @@ PreventInTransactionBlock(bool isTopLevel, const char *stmtType) ereport(ERROR, (errcode(ERRCODE_ACTIVE_SQL_TRANSACTION), /* translator: %s represents an SQL statement name */ - errmsg("%s cannot be executed from a function", stmtType))); + errmsg("%s cannot be executed from a function or procedure", + stmtType))); /* If we got past IsTransactionBlock test, should be in default state */ if (CurrentTransactionState->blockState != TBLOCK_DEFAULT && @@ -4523,13 +4574,13 @@ ReleaseSavepoint(const char *name) break; } - for (target = s; PointerIsValid(target); target = target->parent) + for (target = s; target; target = target->parent) { - if (PointerIsValid(target->name) && strcmp(target->name, name) == 0) + if (target->name && strcmp(target->name, name) == 0) break; } - if (!PointerIsValid(target)) + if (!target) ereport(ERROR, (errcode(ERRCODE_S_E_INVALID_SPECIFICATION), errmsg("savepoint \"%s\" does not exist", name))); @@ -4553,7 +4604,7 @@ ReleaseSavepoint(const char *name) if (xact == target) break; xact = xact->parent; - Assert(PointerIsValid(xact)); + Assert(xact); } } @@ -4632,13 +4683,13 @@ RollbackToSavepoint(const char *name) break; } - for (target = s; PointerIsValid(target); target = target->parent) + for (target = s; target; target = target->parent) { - if (PointerIsValid(target->name) && strcmp(target->name, name) == 0) + if (target->name && strcmp(target->name, name) == 0) break; } - if (!PointerIsValid(target)) + if (!target) ereport(ERROR, (errcode(ERRCODE_S_E_INVALID_SPECIFICATION), errmsg("savepoint \"%s\" does not exist", name))); @@ -4667,7 +4718,7 @@ RollbackToSavepoint(const char *name) elog(FATAL, "RollbackToSavepoint: unexpected state %s", BlockStateAsString(xact->blockState)); xact = xact->parent; - Assert(PointerIsValid(xact)); + Assert(xact); } /* And mark the target as "restart pending" */ @@ -5688,12 +5739,12 @@ ShowTransactionStateRec(const char *str, TransactionState s) ereport(DEBUG5, (errmsg_internal("%s(%d) name: %s; blockState: %s; state: %s, xid/subid/cid: %u/%u/%u%s%s", str, s->nestingLevel, - PointerIsValid(s->name) ? s->name : "unnamed", + s->name ? s->name : "unnamed", BlockStateAsString(s->blockState), TransStateAsString(s->state), - (unsigned int) XidFromFullTransactionId(s->fullTransactionId), - (unsigned int) s->subTransactionId, - (unsigned int) currentCommandId, + XidFromFullTransactionId(s->fullTransactionId), + s->subTransactionId, + currentCommandId, currentCommandIdUsed ? " (used)" : "", buf.data))); pfree(buf.data); @@ -5906,12 +5957,12 @@ XactLogCommitRecord(TimestampTz commit_time, } /* dump transaction origin information */ - if (replorigin_session_origin != InvalidRepOriginId) + if (replorigin_xact_state.origin != InvalidReplOriginId) { xl_xinfo.xinfo |= XACT_XINFO_HAS_ORIGIN; - xl_origin.origin_lsn = replorigin_session_origin_lsn; - xl_origin.origin_timestamp = replorigin_session_origin_timestamp; + xl_origin.origin_lsn = replorigin_xact_state.origin_lsn; + xl_origin.origin_timestamp = replorigin_xact_state.origin_timestamp; } if (xl_xinfo.xinfo != 0) @@ -6059,12 +6110,12 @@ XactLogAbortRecord(TimestampTz abort_time, * Dump transaction origin information. We need this during recovery to * update the replication origin progress. */ - if (replorigin_session_origin != InvalidRepOriginId) + if (replorigin_xact_state.origin != InvalidReplOriginId) { xl_xinfo.xinfo |= XACT_XINFO_HAS_ORIGIN; - xl_origin.origin_lsn = replorigin_session_origin_lsn; - xl_origin.origin_timestamp = replorigin_session_origin_timestamp; + xl_origin.origin_lsn = replorigin_xact_state.origin_lsn; + xl_origin.origin_timestamp = replorigin_xact_state.origin_timestamp; } if (xl_xinfo.xinfo != 0) @@ -6130,7 +6181,7 @@ static void xact_redo_commit(xl_xact_parsed_commit *parsed, TransactionId xid, XLogRecPtr lsn, - RepOriginId origin_id) + ReplOriginId origin_id) { TransactionId max_xid; TimestampTz commit_time; @@ -6143,7 +6194,7 @@ xact_redo_commit(xl_xact_parsed_commit *parsed, AdvanceNextFullTransactionIdPastXid(max_xid); Assert(((parsed->xinfo & XACT_XINFO_HAS_ORIGIN) == 0) == - (origin_id == InvalidRepOriginId)); + (origin_id == InvalidReplOriginId)); if (parsed->xinfo & XACT_XINFO_HAS_ORIGIN) commit_time = parsed->origin_timestamp; @@ -6282,7 +6333,7 @@ xact_redo_commit(xl_xact_parsed_commit *parsed, */ static void xact_redo_abort(xl_xact_parsed_abort *parsed, TransactionId xid, - XLogRecPtr lsn, RepOriginId origin_id) + XLogRecPtr lsn, ReplOriginId origin_id) { TransactionId max_xid; @@ -6420,7 +6471,8 @@ xact_redo(XLogReaderState *record) * gxact entry. */ LWLockAcquire(TwoPhaseStateLock, LW_EXCLUSIVE); - PrepareRedoAdd(XLogRecGetData(record), + PrepareRedoAdd(InvalidFullTransactionId, + XLogRecGetData(record), record->ReadRecPtr, record->EndRecPtr, XLogRecGetOrigin(record)); diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 1914859b2eed7..e39af79c03b5e 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -28,7 +28,7 @@ * the current system state, and for starting/stopping backups. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/xlog.c @@ -62,6 +62,7 @@ #include "access/xlogreader.h" #include "access/xlogrecovery.h" #include "access/xlogutils.h" +#include "access/xlogwait.h" #include "backup/basebackup.h" #include "catalog/catversion.h" #include "catalog/pg_control.h" @@ -74,11 +75,13 @@ #include "pgstat.h" #include "port/atomics.h" #include "postmaster/bgwriter.h" +#include "postmaster/datachecksum_state.h" #include "postmaster/startup.h" #include "postmaster/walsummarizer.h" #include "postmaster/walwriter.h" #include "replication/origin.h" #include "replication/slot.h" +#include "replication/slotsync.h" #include "replication/snapbuild.h" #include "replication/walreceiver.h" #include "replication/walsender.h" @@ -90,18 +93,22 @@ #include "storage/predicate.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/procsignal.h" #include "storage/reinit.h" #include "storage/spin.h" +#include "storage/subsystems.h" #include "storage/sync.h" #include "utils/guc_hooks.h" #include "utils/guc_tables.h" #include "utils/injection_point.h" +#include "utils/pgstat_internal.h" #include "utils/ps_status.h" #include "utils/relmapper.h" #include "utils/snapmgr.h" #include "utils/timeout.h" #include "utils/timestamp.h" #include "utils/varlena.h" +#include "utils/wait_event.h" #ifdef WAL_DEBUG #include "utils/memutils.h" @@ -302,6 +309,11 @@ static bool doPageWrites; * so it's a plain spinlock. The other locks are held longer (potentially * over I/O operations), so we use LWLocks for them. These locks are: * + * WALBufMappingLock: must be held to replace a page in the WAL buffer cache. + * It is only held while initializing and changing the mapping. If the + * contents of the buffer being replaced haven't been written yet, the mapping + * lock is released while the write is done, and reacquired afterwards. + * * WALWriteLock: must be held to write WAL buffers to disk (XLogWrite or * XLogFlush). * @@ -449,7 +461,6 @@ typedef struct XLogCtlData /* Protected by info_lck: */ XLogwrtRqst LogwrtRqst; XLogRecPtr RedoRecPtr; /* a recent copy of Insert->RedoRecPtr */ - FullTransactionId ckptFullXid; /* nextXid of latest checkpoint */ XLogRecPtr asyncXactLSN; /* LSN of newest async commit/abort */ XLogRecPtr replicationSlotMinLSN; /* oldest LSN needed by any slot */ @@ -468,37 +479,21 @@ typedef struct XLogCtlData pg_atomic_uint64 logFlushResult; /* last byte + 1 flushed */ /* - * First initialized page in the cache (first byte position). - */ - XLogRecPtr InitializedFrom; - - /* - * Latest reserved for initialization page in the cache (last byte - * position + 1). + * Latest initialized page in the cache (last byte position + 1). * - * To change the identity of a buffer, you need to advance - * InitializeReserved first. To change the identity of a buffer that's + * To change the identity of a buffer (and InitializedUpTo), you need to + * hold WALBufMappingLock. To change the identity of a buffer that's * still dirty, the old page needs to be written out first, and for that * you need WALWriteLock, and you need to ensure that there are no * in-progress insertions to the page by calling * WaitXLogInsertionsToFinish(). */ - pg_atomic_uint64 InitializeReserved; - - /* - * Latest initialized page in the cache (last byte position + 1). - * - * InitializedUpTo is updated after the buffer initialization. After - * update, waiters got notification using InitializedUpToCondVar. - */ - pg_atomic_uint64 InitializedUpTo; - ConditionVariable InitializedUpToCondVar; + XLogRecPtr InitializedUpTo; /* * These values do not change after startup, although the pointed-to pages - * and xlblocks values certainly do. xlblocks values are changed - * lock-free according to the check for the xlog write position and are - * accompanied by changes of InitializeReserved and InitializedUpTo. + * and xlblocks values certainly do. xlblocks values are protected by + * WALBufMappingLock. */ char *pages; /* buffers for unwritten XLOG pages */ pg_atomic_uint64 *xlblocks; /* 1st byte ptr-s + XLOG_BLCKSZ */ @@ -561,6 +556,9 @@ typedef struct XLogCtlData */ XLogRecPtr lastFpwDisableRecPtr; + /* last data_checksum_version we've seen */ + uint32 data_checksum_version; + slock_t info_lck; /* locks shared variables shown above */ } XLogCtlData; @@ -582,8 +580,19 @@ static WALInsertLockPadded *WALInsertLocks = NULL; /* * We maintain an image of pg_control in shared memory. */ +static ControlFileData *LocalControlFile = NULL; static ControlFileData *ControlFile = NULL; +static void XLOGShmemRequest(void *arg); +static void XLOGShmemInit(void *arg); +static void XLOGShmemAttach(void *arg); + +const ShmemCallbacks XLOGShmemCallbacks = { + .request_fn = XLOGShmemRequest, + .init_fn = XLOGShmemInit, + .attach_fn = XLOGShmemAttach, +}; + /* * Calculate the amount of space left on the page after 'endptr'. Beware * multiple evaluation! @@ -658,6 +667,21 @@ static XLogRecPtr LocalMinRecoveryPoint; static TimeLineID LocalMinRecoveryPointTLI; static bool updateMinRecoveryPoint = true; +/* + * Local state for ControlFile data_checksum_version. After initialization + * this is only updated when absorbing a procsignal barrier during interrupt + * processing. The reason for keeping a copy in backend-private memory is to + * avoid locking for interrogating the data checksum state. Possible values + * are the data checksum versions defined in storage/checksum.h. + */ +static ChecksumStateType LocalDataChecksumState = 0; + +/* + * Variable backing the GUC, keep it in sync with LocalDataChecksumState. + * See SetLocalDataChecksumState(). + */ +int data_checksums = 0; + /* For WALInsertLockAcquire/Release functions */ static int MyLockNo = 0; static bool holdingAllLocks = false; @@ -678,7 +702,6 @@ static XLogRecPtr CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn, TimeLineID newTLI); static void CheckPointGuts(XLogRecPtr checkPointRedo, int flags); static void KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo); -static XLogRecPtr XLogGetReplicationSlotMinimumLSN(void); static void AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic); @@ -703,7 +726,7 @@ static void InitControlFile(uint64 sysidentifier, uint32 data_checksum_version); static void WriteControlFile(void); static void ReadControlFile(void); static void UpdateControlFile(void); -static char *str_time(pg_time_t tnow); +static char *str_time(pg_time_t tnow, char *buf, size_t bufsize); static int get_sync_bit(int method); @@ -726,6 +749,8 @@ static void WALInsertLockAcquireExclusive(void); static void WALInsertLockRelease(void); static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt); +static void XLogChecksums(uint32 new_type); + /* * Insert an XLOG record represented by an already-constructed chain of data * chunks. This is a low-level routine; to construct the WAL record header @@ -760,6 +785,7 @@ XLogInsertRecord(XLogRecData *rdata, XLogRecPtr fpw_lsn, uint8 flags, int num_fpi, + uint64 fpi_bytes, bool topxid_included) { XLogCtlInsert *Insert = &XLogCtl->Insert; @@ -821,9 +847,9 @@ XLogInsertRecord(XLogRecData *rdata, * fullPageWrites from changing until the insertion is finished. * * Step 2 can usually be done completely in parallel. If the required WAL - * page is not initialized yet, you have to go through AdvanceXLInsertBuffer, - * which will ensure it is initialized. But the WAL writer tries to do that - * ahead of insertions to avoid that from happening in the critical path. + * page is not initialized yet, you have to grab WALBufMappingLock to + * initialize it, but the WAL writer tries to do that ahead of insertions + * to avoid that from happening in the critical path. * *---------- */ @@ -858,7 +884,7 @@ XLogInsertRecord(XLogRecData *rdata, if (doPageWrites && (!prevDoPageWrites || - (fpw_lsn != InvalidXLogRecPtr && fpw_lsn <= RedoRecPtr))) + (XLogRecPtrIsValid(fpw_lsn) && fpw_lsn <= RedoRecPtr))) { /* * Oops, some buffer now needs to be backed up that the caller @@ -892,7 +918,7 @@ XLogInsertRecord(XLogRecData *rdata, * Those checks are only needed for records that can contain buffer * references, and an XLOG_SWITCH record never does. */ - Assert(fpw_lsn == InvalidXLogRecPtr); + Assert(!XLogRecPtrIsValid(fpw_lsn)); WALInsertLockAcquireExclusive(); inserted = ReserveXLogSwitch(&StartPos, &EndPos, &rechdr->xl_prev); } @@ -907,7 +933,7 @@ XLogInsertRecord(XLogRecData *rdata, * not check RedoRecPtr before inserting the record; we just need to * update it afterwards. */ - Assert(fpw_lsn == InvalidXLogRecPtr); + Assert(!XLogRecPtrIsValid(fpw_lsn)); WALInsertLockAcquireExclusive(); ReserveXLogInsertLocation(rechdr->xl_tot_len, &StartPos, &EndPos, &rechdr->xl_prev); @@ -1028,7 +1054,7 @@ XLogInsertRecord(XLogRecData *rdata, oldCxt = MemoryContextSwitchTo(walDebugCxt); initStringInfo(&buf); - appendStringInfo(&buf, "INSERT @ %X/%X: ", LSN_FORMAT_ARGS(EndPos)); + appendStringInfo(&buf, "INSERT @ %X/%08X: ", LSN_FORMAT_ARGS(EndPos)); /* * We have to piece together the WAL record data from the XLogRecData @@ -1092,6 +1118,10 @@ XLogInsertRecord(XLogRecData *rdata, pgWalUsage.wal_bytes += rechdr->xl_tot_len; pgWalUsage.wal_records++; pgWalUsage.wal_fpi += num_fpi; + pgWalUsage.wal_fpi_bytes += fpi_bytes; + + /* Required for the flush of pending stats WAL data */ + pgstat_report_fixed = true; } return EndPos; @@ -1549,8 +1579,8 @@ WaitXLogInsertionsToFinish(XLogRecPtr upto) if (upto > reservedUpto) { ereport(LOG, - (errmsg("request to flush past end of generated WAL; request %X/%X, current position %X/%X", - LSN_FORMAT_ARGS(upto), LSN_FORMAT_ARGS(reservedUpto)))); + errmsg("request to flush past end of generated WAL; request %X/%08X, current position %X/%08X", + LSN_FORMAT_ARGS(upto), LSN_FORMAT_ARGS(reservedUpto))); upto = reservedUpto; } @@ -1608,7 +1638,7 @@ WaitXLogInsertionsToFinish(XLogRecPtr upto) */ } while (insertingat < upto); - if (insertingat != InvalidXLogRecPtr && insertingat < finishedUpto) + if (XLogRecPtrIsValid(insertingat) && insertingat < finishedUpto) finishedUpto = insertingat; } @@ -1716,7 +1746,7 @@ GetXLogBuffer(XLogRecPtr ptr, TimeLineID tli) endptr = pg_atomic_read_u64(&XLogCtl->xlblocks[idx]); if (expectedEndPtr != endptr) - elog(PANIC, "could not find WAL buffer for %X/%X", + elog(PANIC, "could not find WAL buffer for %X/%08X", LSN_FORMAT_ARGS(ptr)); } else @@ -1767,7 +1797,7 @@ WALReadFromBuffers(char *dstbuf, XLogRecPtr startptr, Size count, if (RecoveryInProgress() || tli != GetWALInsertionTimeLine()) return 0; - Assert(!XLogRecPtrIsInvalid(startptr)); + Assert(XLogRecPtrIsValid(startptr)); /* * Caller should ensure that the requested data has been inserted into WAL @@ -1776,7 +1806,7 @@ WALReadFromBuffers(char *dstbuf, XLogRecPtr startptr, Size count, inserted = pg_atomic_read_u64(&XLogCtl->logInsertResult); if (startptr + count > inserted) ereport(ERROR, - errmsg("cannot read past end of generated WAL: requested %X/%X, current position %X/%X", + errmsg("cannot read past end of generated WAL: requested %X/%08X, current position %X/%08X", LSN_FORMAT_ARGS(startptr + count), LSN_FORMAT_ARGS(inserted))); @@ -1995,86 +2025,38 @@ XLogRecPtrToBytePos(XLogRecPtr ptr) static void AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic) { - XLogCtlInsert *Insert = &XLogCtl->Insert; int nextidx; XLogRecPtr OldPageRqstPtr; XLogwrtRqst WriteRqst; XLogRecPtr NewPageEndPtr = InvalidXLogRecPtr; XLogRecPtr NewPageBeginPtr; XLogPageHeader NewPage; - XLogRecPtr ReservedPtr; int npages pg_attribute_unused() = 0; - /* - * We must run the loop below inside the critical section as we expect - * XLogCtl->InitializedUpTo to eventually keep up. The most of callers - * already run inside the critical section. Except for WAL writer, which - * passed 'opportunistic == true', and therefore we don't perform - * operations that could error out. - * - * Start an explicit critical section anyway though. - */ - Assert(CritSectionCount > 0 || opportunistic); - START_CRIT_SECTION(); + LWLockAcquire(WALBufMappingLock, LW_EXCLUSIVE); - /*-- - * Loop till we get all the pages in WAL buffer before 'upto' reserved for - * initialization. Multiple process can initialize different buffers with - * this loop in parallel as following. - * - * 1. Reserve page for initialization using XLogCtl->InitializeReserved. - * 2. Initialize the reserved page. - * 3. Attempt to advance XLogCtl->InitializedUpTo, + /* + * Now that we have the lock, check if someone initialized the page + * already. */ - ReservedPtr = pg_atomic_read_u64(&XLogCtl->InitializeReserved); - while (upto >= ReservedPtr || opportunistic) + while (upto >= XLogCtl->InitializedUpTo || opportunistic) { - Assert(ReservedPtr % XLOG_BLCKSZ == 0); + nextidx = XLogRecPtrToBufIdx(XLogCtl->InitializedUpTo); /* - * Get ending-offset of the buffer page we need to replace. - * - * We don't lookup into xlblocks, but rather calculate position we - * must wait to be written. If it was written, xlblocks will have this - * position (or uninitialized) + * Get ending-offset of the buffer page we need to replace (this may + * be zero if the buffer hasn't been used yet). Fall through if it's + * already written out. */ - if (ReservedPtr + XLOG_BLCKSZ > XLogCtl->InitializedFrom + XLOG_BLCKSZ * XLOGbuffers) - OldPageRqstPtr = ReservedPtr + XLOG_BLCKSZ - (XLogRecPtr) XLOG_BLCKSZ * XLOGbuffers; - else - OldPageRqstPtr = InvalidXLogRecPtr; - - if (LogwrtResult.Write < OldPageRqstPtr && opportunistic) + OldPageRqstPtr = pg_atomic_read_u64(&XLogCtl->xlblocks[nextidx]); + if (LogwrtResult.Write < OldPageRqstPtr) { /* - * If we just want to pre-initialize as much as we can without - * flushing, give up now. + * Nope, got work to do. If we just want to pre-initialize as much + * as we can without flushing, give up now. */ - upto = ReservedPtr - 1; - break; - } - - /* - * Attempt to reserve the page for initialization. Failure means that - * this page got reserved by another process. - */ - if (!pg_atomic_compare_exchange_u64(&XLogCtl->InitializeReserved, - &ReservedPtr, - ReservedPtr + XLOG_BLCKSZ)) - continue; - - /* - * Wait till page gets correctly initialized up to OldPageRqstPtr. - */ - nextidx = XLogRecPtrToBufIdx(ReservedPtr); - while (pg_atomic_read_u64(&XLogCtl->InitializedUpTo) < OldPageRqstPtr) - ConditionVariableSleep(&XLogCtl->InitializedUpToCondVar, WAIT_EVENT_WAL_BUFFER_INIT); - ConditionVariableCancelSleep(); - Assert(pg_atomic_read_u64(&XLogCtl->xlblocks[nextidx]) == OldPageRqstPtr); - - /* Fall through if it's already written out. */ - if (LogwrtResult.Write < OldPageRqstPtr) - { - /* Nope, got work to do. */ + if (opportunistic) + break; /* Advance shared memory write request position */ SpinLockAcquire(&XLogCtl->info_lck); @@ -2089,6 +2071,14 @@ AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic) RefreshXLogWriteResult(LogwrtResult); if (LogwrtResult.Write < OldPageRqstPtr) { + /* + * Must acquire write lock. Release WALBufMappingLock first, + * to make sure that all insertions that we need to wait for + * can finish (up to this same position). Otherwise we risk + * deadlock. + */ + LWLockRelease(WALBufMappingLock); + WaitXLogInsertionsToFinish(OldPageRqstPtr); LWLockAcquire(WALWriteLock, LW_EXCLUSIVE); @@ -2104,12 +2094,21 @@ AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic) /* Have to write it ourselves */ TRACE_POSTGRESQL_WAL_BUFFER_WRITE_DIRTY_START(); WriteRqst.Write = OldPageRqstPtr; - WriteRqst.Flush = 0; + WriteRqst.Flush = InvalidXLogRecPtr; XLogWrite(WriteRqst, tli, false); LWLockRelease(WALWriteLock); pgWalUsage.wal_buffers_full++; TRACE_POSTGRESQL_WAL_BUFFER_WRITE_DIRTY_DONE(); + + /* + * Required for the flush of pending stats WAL data, per + * update of pgWalUsage. + */ + pgstat_report_fixed = true; } + /* Re-acquire WALBufMappingLock and retry */ + LWLockAcquire(WALBufMappingLock, LW_EXCLUSIVE); + continue; } } @@ -2117,9 +2116,11 @@ AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic) * Now the next buffer slot is free and we can set it up to be the * next output page. */ - NewPageBeginPtr = ReservedPtr; + NewPageBeginPtr = XLogCtl->InitializedUpTo; NewPageEndPtr = NewPageBeginPtr + XLOG_BLCKSZ; + Assert(XLogRecPtrToBufIdx(NewPageBeginPtr) == nextidx); + NewPage = (XLogPageHeader) (XLogCtl->pages + nextidx * (Size) XLOG_BLCKSZ); /* @@ -2147,22 +2148,6 @@ AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic) /* NewPage->xlp_rem_len = 0; */ /* done by memset */ - /* - * If online backup is not in progress, mark the header to indicate - * that WAL records beginning in this page have removable backup - * blocks. This allows the WAL archiver to know whether it is safe to - * compress archived WAL data by transforming full-block records into - * the non-full-block format. It is sufficient to record this at the - * page level because we force a page switch (in fact a segment - * switch) when starting a backup, so the flag will be off before any - * records can be written during the backup. At the end of a backup, - * the last page will be marked as all unsafe when perhaps only part - * is unsafe, but at worst the archiver would miss the opportunity to - * compress a few records. - */ - if (Insert->runningBackups == 0) - NewPage->xlp_info |= XLP_BKP_REMOVABLE; - /* * If first page of an XLOG segment file, make it a long header. */ @@ -2183,105 +2168,17 @@ AdvanceXLInsertBuffer(XLogRecPtr upto, TimeLineID tli, bool opportunistic) */ pg_write_barrier(); - /*----- - * Update the value of XLogCtl->xlblocks[nextidx] and try to advance - * XLogCtl->InitializedUpTo in a lock-less manner. - * - * First, let's provide a formal proof of the algorithm. Let it be 'n' - * process with the following variables in shared memory: - * f - an array of 'n' boolean flags, - * v - atomic integer variable. - * - * Also, let - * i - a number of a process, - * j - local integer variable, - * CAS(var, oldval, newval) - compare-and-swap atomic operation - * returning true on success, - * write_barrier()/read_barrier() - memory barriers. - * - * The pseudocode for each process is the following. - * - * j := i - * f[i] := true - * write_barrier() - * while CAS(v, j, j + 1): - * j := j + 1 - * read_barrier() - * if not f[j]: - * break - * - * Let's prove that v eventually reaches the value of n. - * 1. Prove by contradiction. Assume v doesn't reach n and stucks - * on k, where k < n. - * 2. Process k attempts CAS(v, k, k + 1). 1). If, as we assumed, v - * gets stuck at k, then this CAS operation must fail. Therefore, - * v < k when process k attempts CAS(v, k, k + 1). - * 3. If, as we assumed, v gets stuck at k, then the value k of v - * must be achieved by some process m, where m < k. The process - * m must observe f[k] == false. Otherwise, it will later attempt - * CAS(v, k, k + 1) with success. - * 4. Therefore, corresponding read_barrier() (while j == k) on - * process m reached before write_barrier() of process k. But then - * process k attempts CAS(v, k, k + 1) after process m successfully - * incremented v to k, and that CAS operation must succeed. - * That leads to a contradiction. So, there is no such k (k < n) - * where v gets stuck. Q.E.D. - * - * To apply this proof to the code below, we assume - * XLogCtl->InitializedUpTo will play the role of v with XLOG_BLCKSZ - * granularity. We also assume setting XLogCtl->xlblocks[nextidx] to - * NewPageEndPtr to play the role of setting f[i] to true. Also, note - * that processes can't concurrently map different xlog locations to - * the same nextidx because we previously requested that - * XLogCtl->InitializedUpTo >= OldPageRqstPtr. So, a xlog buffer can - * be taken for initialization only once the previous initialization - * takes effect on XLogCtl->InitializedUpTo. - */ - pg_atomic_write_u64(&XLogCtl->xlblocks[nextidx], NewPageEndPtr); - - pg_write_barrier(); - - while (pg_atomic_compare_exchange_u64(&XLogCtl->InitializedUpTo, &NewPageBeginPtr, NewPageEndPtr)) - { - NewPageBeginPtr = NewPageEndPtr; - NewPageEndPtr = NewPageBeginPtr + XLOG_BLCKSZ; - nextidx = XLogRecPtrToBufIdx(NewPageBeginPtr); - - pg_read_barrier(); - - if (pg_atomic_read_u64(&XLogCtl->xlblocks[nextidx]) != NewPageEndPtr) - { - /* - * Page at nextidx wasn't initialized yet, so we can't move - * InitializedUpto further. It will be moved by backend which - * will initialize nextidx. - */ - ConditionVariableBroadcast(&XLogCtl->InitializedUpToCondVar); - break; - } - } + XLogCtl->InitializedUpTo = NewPageEndPtr; npages++; } - - END_CRIT_SECTION(); - - /* - * All the pages in WAL buffer before 'upto' were reserved for - * initialization. However, some pages might be reserved by concurrent - * processes. Wait till they finish initialization. - */ - while (upto >= pg_atomic_read_u64(&XLogCtl->InitializedUpTo)) - ConditionVariableSleep(&XLogCtl->InitializedUpToCondVar, WAIT_EVENT_WAL_BUFFER_INIT); - ConditionVariableCancelSleep(); - - pg_read_barrier(); + LWLockRelease(WALBufMappingLock); #ifdef WAL_DEBUG if (XLOG_DEBUG && npages > 0) { - elog(DEBUG1, "initialized %d pages, up to %X/%X", + elog(DEBUG1, "initialized %d pages, up to %X/%08X", npages, LSN_FORMAT_ARGS(NewPageEndPtr)); } #endif @@ -2346,25 +2243,6 @@ check_wal_segment_size(int *newval, void **extra, GucSource source) return true; } -/* - * GUC check_hook for max_slot_wal_keep_size - * - * We don't allow the value of max_slot_wal_keep_size other than -1 during the - * binary upgrade. See start_postmaster() in pg_upgrade for more details. - */ -bool -check_max_slot_wal_keep_size(int *newval, void **extra, GucSource source) -{ - if (IsBinaryUpgrade && *newval != -1) - { - GUC_check_errdetail("\"%s\" must be set to -1 during binary upgrade mode.", - "max_slot_wal_keep_size"); - return false; - } - - return true; -} - /* * At a checkpoint, how many WAL segments to recycle as preallocated future * XLOG segments? Returns the highest segment that should be preallocated. @@ -2492,7 +2370,7 @@ XLogWrite(XLogwrtRqst WriteRqst, TimeLineID tli, bool flexible) XLogRecPtr EndPtr = pg_atomic_read_u64(&XLogCtl->xlblocks[curridx]); if (LogwrtResult.Write >= EndPtr) - elog(PANIC, "xlog write request %X/%X is past end of log %X/%X", + elog(PANIC, "xlog write request %X/%08X is past end of log %X/%08X", LSN_FORMAT_ARGS(LogwrtResult.Write), LSN_FORMAT_ARGS(EndPtr)); @@ -2818,7 +2696,7 @@ XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn) * Return the oldest LSN we must retain to satisfy the needs of some * replication slot. */ -static XLogRecPtr +XLogRecPtr XLogGetReplicationSlotMinimumLSN(void) { XLogRecPtr retval; @@ -2857,7 +2735,7 @@ UpdateMinRecoveryPoint(XLogRecPtr lsn, bool force) * available is replayed in this case. This also saves from extra locks * taken on the control file from the startup process. */ - if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint) && InRecovery) + if (!XLogRecPtrIsValid(LocalMinRecoveryPoint) && InRecovery) { updateMinRecoveryPoint = false; return; @@ -2869,7 +2747,7 @@ UpdateMinRecoveryPoint(XLogRecPtr lsn, bool force) LocalMinRecoveryPoint = ControlFile->minRecoveryPoint; LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI; - if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint)) + if (!XLogRecPtrIsValid(LocalMinRecoveryPoint)) updateMinRecoveryPoint = false; else if (force || LocalMinRecoveryPoint < lsn) { @@ -2892,7 +2770,7 @@ UpdateMinRecoveryPoint(XLogRecPtr lsn, bool force) newMinRecoveryPoint = GetCurrentReplayRecPtr(&newMinRecoveryPointTLI); if (!force && newMinRecoveryPoint < lsn) elog(WARNING, - "xlog min recovery request %X/%X is past current point %X/%X", + "xlog min recovery request %X/%08X is past current point %X/%08X", LSN_FORMAT_ARGS(lsn), LSN_FORMAT_ARGS(newMinRecoveryPoint)); /* update control file */ @@ -2905,9 +2783,9 @@ UpdateMinRecoveryPoint(XLogRecPtr lsn, bool force) LocalMinRecoveryPointTLI = newMinRecoveryPointTLI; ereport(DEBUG2, - (errmsg_internal("updated min recovery point to %X/%X on timeline %u", - LSN_FORMAT_ARGS(newMinRecoveryPoint), - newMinRecoveryPointTLI))); + errmsg_internal("updated min recovery point to %X/%08X on timeline %u", + LSN_FORMAT_ARGS(newMinRecoveryPoint), + newMinRecoveryPointTLI)); } } LWLockRelease(ControlFileLock); @@ -2945,7 +2823,7 @@ XLogFlush(XLogRecPtr record) #ifdef WAL_DEBUG if (XLOG_DEBUG) - elog(LOG, "xlog flush request %X/%X; write %X/%X; flush %X/%X", + elog(LOG, "xlog flush request %X/%08X; write %X/%08X; flush %X/%08X", LSN_FORMAT_ARGS(record), LSN_FORMAT_ARGS(LogwrtResult.Write), LSN_FORMAT_ARGS(LogwrtResult.Flush)); @@ -3024,7 +2902,9 @@ XLogFlush(XLogRecPtr record) if (CommitDelay > 0 && enableFsync && MinimumActiveBackends(CommitSiblings)) { + pgstat_report_wait_start(WAIT_EVENT_COMMIT_DELAY); pg_usleep(CommitDelay); + pgstat_report_wait_end(); /* * Re-check how far we can now flush the WAL. It's generally not @@ -3055,6 +2935,14 @@ XLogFlush(XLogRecPtr record) /* wake up walsenders now that we've released heavily contended locks */ WalSndWakeupProcessRequests(true, !RecoveryInProgress()); + /* + * If we flushed an LSN that someone was waiting for, notify the waiters. + */ + if (waitLSNState && + (LogwrtResult.Flush >= + pg_atomic_read_u64(&waitLSNState->minWaitedLSN[WAIT_LSN_TYPE_PRIMARY_FLUSH]))) + WaitLSNWakeup(WAIT_LSN_TYPE_PRIMARY_FLUSH, LogwrtResult.Flush); + /* * If we still haven't flushed to the request point then we have a * problem; most likely, the requested flush point is past end of XLOG. @@ -3078,9 +2966,16 @@ XLogFlush(XLogRecPtr record) */ if (LogwrtResult.Flush < record) elog(ERROR, - "xlog flush request %X/%X is not satisfied --- flushed only to %X/%X", + "xlog flush request %X/%08X is not satisfied --- flushed only to %X/%08X", LSN_FORMAT_ARGS(record), LSN_FORMAT_ARGS(LogwrtResult.Flush)); + + /* + * Cross-check XLogNeedsFlush(). Some of the checks of XLogFlush() and + * XLogNeedsFlush() are duplicated, and this assertion ensures that these + * remain consistent. + */ + Assert(!XLogNeedsFlush(record)); } /* @@ -3200,12 +3095,12 @@ XLogBackgroundFlush(void) else { /* no flushing, this time round */ - WriteRqst.Flush = 0; + WriteRqst.Flush = InvalidXLogRecPtr; } #ifdef WAL_DEBUG if (XLOG_DEBUG) - elog(LOG, "xlog bg flush request write %X/%X; flush: %X/%X, current is write %X/%X; flush %X/%X", + elog(LOG, "xlog bg flush request write %X/%08X; flush: %X/%08X, current is write %X/%08X; flush %X/%08X", LSN_FORMAT_ARGS(WriteRqst.Write), LSN_FORMAT_ARGS(WriteRqst.Flush), LSN_FORMAT_ARGS(LogwrtResult.Write), @@ -3230,6 +3125,14 @@ XLogBackgroundFlush(void) /* wake up walsenders now that we've released heavily contended locks */ WalSndWakeupProcessRequests(true, !RecoveryInProgress()); + /* + * If we flushed an LSN that someone was waiting for, notify the waiters. + */ + if (waitLSNState && + (LogwrtResult.Flush >= + pg_atomic_read_u64(&waitLSNState->minWaitedLSN[WAIT_LSN_TYPE_PRIMARY_FLUSH]))) + WaitLSNWakeup(WAIT_LSN_TYPE_PRIMARY_FLUSH, LogwrtResult.Flush); + /* * Great, done. To take some work off the critical path, try to initialize * as many of the no-longer-needed WAL buffers for future use as we can. @@ -3245,10 +3148,16 @@ XLogBackgroundFlush(void) } /* - * Test whether XLOG data has been flushed up to (at least) the given position. + * Test whether XLOG data has been flushed up to (at least) the given + * position, or whether the minimum recovery point has been updated past + * the given position. * - * Returns true if a flush is still needed. (It may be that someone else - * is already in process of flushing that far, however.) + * Returns true if a flush is still needed, or if the minimum recovery point + * must be updated. + * + * It is possible that someone else is already in the process of flushing + * that far, or has updated the minimum recovery point up to the given + * position. */ bool XLogNeedsFlush(XLogRecPtr record) @@ -3257,9 +3166,17 @@ XLogNeedsFlush(XLogRecPtr record) * During recovery, we don't flush WAL but update minRecoveryPoint * instead. So "needs flush" is taken to mean whether minRecoveryPoint * would need to be updated. + * + * Using XLogInsertAllowed() rather than RecoveryInProgress() matters for + * the case of an end-of-recovery checkpoint, where WAL data is flushed. + * This check should be consistent with the one in XLogFlush(). */ - if (RecoveryInProgress()) + if (!XLogInsertAllowed()) { + /* Quick exit if already known to be updated or cannot be updated */ + if (!updateMinRecoveryPoint || record <= LocalMinRecoveryPoint) + return false; + /* * An invalid minRecoveryPoint means that we need to recover all the * WAL, i.e., we're doing crash recovery. We never modify the control @@ -3268,12 +3185,11 @@ XLogNeedsFlush(XLogRecPtr record) * which cannot update its local copy of minRecoveryPoint as long as * it has not replayed all WAL available when doing crash recovery. */ - if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint) && InRecovery) + if (!XLogRecPtrIsValid(LocalMinRecoveryPoint) && InRecovery) + { updateMinRecoveryPoint = false; - - /* Quick exit if already known to be updated or cannot be updated */ - if (record <= LocalMinRecoveryPoint || !updateMinRecoveryPoint) return false; + } /* * Update local copy of minRecoveryPoint. But if the lock is busy, @@ -3290,7 +3206,7 @@ XLogNeedsFlush(XLogRecPtr record) * process doing crash recovery, which should not update the control * file value if crash recovery is still running. */ - if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint)) + if (!XLogRecPtrIsValid(LocalMinRecoveryPoint)) updateMinRecoveryPoint = false; /* check again */ @@ -4372,6 +4288,12 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version) ControlFile->wal_log_hints = wal_log_hints; ControlFile->track_commit_timestamp = track_commit_timestamp; ControlFile->data_checksum_version = data_checksum_version; + + /* + * Set the data_checksum_version value into XLogCtl, which is where all + * processes get the current value from. + */ + XLogCtl->data_checksum_version = data_checksum_version; } static void @@ -4391,6 +4313,7 @@ WriteControlFile(void) ControlFile->blcksz = BLCKSZ; ControlFile->relseg_size = RELSEG_SIZE; + ControlFile->slru_pages_per_segment = SLRU_PAGES_PER_SEGMENT; ControlFile->xlog_blcksz = XLOG_BLCKSZ; ControlFile->xlog_seg_size = wal_segment_size; @@ -4400,7 +4323,7 @@ WriteControlFile(void) ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE; ControlFile->loblksize = LOBLKSIZE; - ControlFile->float8ByVal = FLOAT8PASSBYVAL; + ControlFile->float8ByVal = true; /* vestigial */ /* * Initialize the default 'char' signedness. @@ -4610,6 +4533,16 @@ ReadControlFile(void) "RELSEG_SIZE", ControlFile->relseg_size, "RELSEG_SIZE", RELSEG_SIZE), errhint("It looks like you need to recompile or initdb."))); + if (ControlFile->slru_pages_per_segment != SLRU_PAGES_PER_SEGMENT) + ereport(FATAL, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("database files are incompatible with server"), + /* translator: %s is a variable name and %d is its value */ + errdetail("The database cluster was initialized with %s %d," + " but the server was compiled with %s %d.", + "SLRU_PAGES_PER_SEGMENT", ControlFile->slru_pages_per_segment, + "SLRU_PAGES_PER_SEGMENT", SLRU_PAGES_PER_SEGMENT), + errhint("It looks like you need to recompile or initdb."))); if (ControlFile->xlog_blcksz != XLOG_BLCKSZ) ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), @@ -4661,23 +4594,7 @@ ReadControlFile(void) "LOBLKSIZE", (int) LOBLKSIZE), errhint("It looks like you need to recompile or initdb."))); -#ifdef USE_FLOAT8_BYVAL - if (ControlFile->float8ByVal != true) - ereport(FATAL, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("database files are incompatible with server"), - errdetail("The database cluster was initialized without USE_FLOAT8_BYVAL" - " but the server was compiled with USE_FLOAT8_BYVAL."), - errhint("It looks like you need to recompile or initdb."))); -#else - if (ControlFile->float8ByVal != false) - ereport(FATAL, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("database files are incompatible with server"), - errdetail("The database cluster was initialized with USE_FLOAT8_BYVAL" - " but the server was compiled without USE_FLOAT8_BYVAL."), - errhint("It looks like you need to recompile or initdb."))); -#endif + Assert(ControlFile->float8ByVal); /* vestigial, not worth an error msg */ wal_segment_size = ControlFile->xlog_seg_size; @@ -4710,51 +4627,357 @@ ReadControlFile(void) (wal_segment_size / XLOG_BLCKSZ * UsableBytesInPage) - (SizeOfXLogLongPHD - SizeOfXLogShortPHD); - CalculateCheckpointSegments(); + CalculateCheckpointSegments(); +} + +/* + * Utility wrapper to update the control file. Note that the control + * file gets flushed. + */ +static void +UpdateControlFile(void) +{ + update_controlfile(DataDir, ControlFile, true); +} + +/* + * Returns the unique system identifier from control file. + */ +uint64 +GetSystemIdentifier(void) +{ + Assert(ControlFile != NULL); + return ControlFile->system_identifier; +} + +/* + * Returns the random nonce from control file. + */ +char * +GetMockAuthenticationNonce(void) +{ + Assert(ControlFile != NULL); + return ControlFile->mock_authentication_nonce; +} + +/* + * DataChecksumsNeedWrite + * Returns whether data checksums must be written or not + * + * Returns true if data checksums are enabled, or are in the process of being + * enabled. During "inprogress-on" and "inprogress-off" states checksums must + * be written even though they are not verified (see datachecksum_state.c for + * a longer discussion). + * + * This function is intended for callsites which are about to write a data page + * to storage, and need to know whether to re-calculate the checksum for the + * page header. Calling this function must be performed as close to the write + * operation as possible to keep the critical section short. + */ +bool +DataChecksumsNeedWrite(void) +{ + return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION || + LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON || + LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_OFF); +} + +bool +DataChecksumsInProgressOn(void) +{ + return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON; +} + +/* + * DataChecksumsNeedVerify + * Returns whether data checksums must be verified or not + * + * Data checksums are only verified if they are fully enabled in the cluster. + * During the "inprogress-on" and "inprogress-off" states they are only + * updated, not verified (see datachecksum_state.c for a longer discussion). + * + * This function is intended for callsites which have read data and are about + * to perform checksum validation based on the result of this. Calling this + * function must be performed as close to the validation call as possible to + * keep the critical section short. This is in order to protect against time of + * check/time of use situations around data checksum validation. + */ +bool +DataChecksumsNeedVerify(void) +{ + return (LocalDataChecksumState == PG_DATA_CHECKSUM_VERSION); +} + +/* + * SetDataChecksumsOnInProgress + * Sets the data checksum state to "inprogress-on" to enable checksums + * + * To start the process of enabling data checksums in a running cluster the + * data_checksum_version state must be changed to "inprogress-on". See + * SetDataChecksumsOn below for a description on how this state change works. + * This function blocks until all backends in the cluster have acknowledged the + * state transition. + */ +void +SetDataChecksumsOnInProgress(void) +{ + uint64 barrier; + + Assert(ControlFile != NULL); + + /* + * The state transition is performed in a critical section with + * checkpoints held off to provide crash safety. + */ + START_CRIT_SECTION(); + MyProc->delayChkptFlags |= DELAY_CHKPT_START; + + XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; + SpinLockRelease(&XLogCtl->info_lck); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + /* + * Update the controlfile before waiting since if we have an immediate + * shutdown while waiting we want to come back up with checksums enabled. + */ + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + /* + * Await state change in all backends to ensure that all backends are in + * "inprogress-on". Once done we know that all backends are writing data + * checksums. + */ + WaitForProcSignalBarrier(barrier); +} + +/* + * SetDataChecksumsOn + * Set data checksums state to 'on' cluster-wide + * + * Enabling data checksums is performed using two barriers, the first one to + * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress()) + * and the second one to set the state to "on" (done here). Below is a short + * description of the processing, a more detailed write-up can be found in + * datachecksum_state.c. + * + * To start the process of enabling data checksums in a running cluster the + * data_checksum_version state must be changed to "inprogress-on". This state + * requires data checksums to be written but not verified. This ensures that + * all data pages can be checksummed without the risk of false negatives in + * validation during the process. When all existing pages are guaranteed to + * have checksums, and all new pages will be initiated with checksums, the + * state can be changed to "on". Once the state is "on" checksums will be both + * written and verified. + * + * This function blocks until all backends in the cluster have acknowledged the + * state transition. + */ +void +SetDataChecksumsOn(void) +{ + uint64 barrier; + + Assert(ControlFile != NULL); + + SpinLockAcquire(&XLogCtl->info_lck); + + /* + * The only allowed state transition to "on" is from "inprogress-on" since + * that state ensures that all pages will have data checksums written. No + * such state transition exists, if it does happen it's likely due to a + * programmer error. + */ + if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON) + { + SpinLockRelease(&XLogCtl->info_lck); + elog(WARNING, + "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling"); + SetDataChecksumsOff(); + return; + } + + SpinLockRelease(&XLogCtl->info_lck); + + INJECTION_POINT("datachecksums-enable-checksums-delay", NULL); + START_CRIT_SECTION(); + MyProc->delayChkptFlags |= DELAY_CHKPT_START; + + XLogChecksums(PG_DATA_CHECKSUM_VERSION); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION; + SpinLockRelease(&XLogCtl->info_lck); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + /* + * Update the controlfile before waiting since if we have an immediate + * shutdown while waiting we want to come back up with checksums enabled. + */ + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + + /* + * Await state transition to "on" in all backends. When done we know that + * data checksums are both written and verified in all backends. + */ + WaitForProcSignalBarrier(barrier); +} + +/* + * SetDataChecksumsOff + * Disables data checksums cluster-wide + * + * Disabling data checksums must be performed with two sets of barriers, each + * carrying a different state. The state is first set to "inprogress-off" + * during which checksums are still written but not verified. This ensures that + * backends which have yet to observe the state change from "on" won't get + * validation errors on concurrently modified pages. Once all backends have + * changed to "inprogress-off", the barrier for moving to "off" can be emitted. + * This function blocks until all backends in the cluster have acknowledged the + * state transition. + */ +void +SetDataChecksumsOff(void) +{ + uint64 barrier; + + Assert(ControlFile != NULL); + + SpinLockAcquire(&XLogCtl->info_lck); + + /* If data checksums are already disabled there is nothing to do */ + if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_OFF) + { + SpinLockRelease(&XLogCtl->info_lck); + return; + } + + /* + * If data checksums are currently enabled we first transition to the + * "inprogress-off" state during which backends continue to write + * checksums without verifying them. When all backends are in + * "inprogress-off" the next transition to "off" can be performed, after + * which all data checksum processing is disabled. + */ + if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION) + { + SpinLockRelease(&XLogCtl->info_lck); + + START_CRIT_SECTION(); + MyProc->delayChkptFlags |= DELAY_CHKPT_START; + + XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF; + SpinLockRelease(&XLogCtl->info_lck); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); + + /* + * Update local state in all backends to ensure that any backend in + * "on" state is changed to "inprogress-off". + */ + WaitForProcSignalBarrier(barrier); + + /* + * At this point we know that no backends are verifying data checksums + * during reading. Next, we can safely move to state "off" to also + * stop writing checksums. + */ + } + else + { + /* + * Ending up here implies that the checksums state is "inprogress-on" + * or "inprogress-off" and we can transition directly to "off" from + * there. + */ + SpinLockRelease(&XLogCtl->info_lck); + } + + START_CRIT_SECTION(); + /* Ensure that we don't incur a checkpoint during disabling checksums */ + MyProc->delayChkptFlags |= DELAY_CHKPT_START; + + XLogChecksums(PG_DATA_CHECKSUM_OFF); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; + SpinLockRelease(&XLogCtl->info_lck); + + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF); + + MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; + END_CRIT_SECTION(); + + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF; + UpdateControlFile(); + LWLockRelease(ControlFileLock); + + RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST); - /* Make the initdb settings visible as GUC variables, too */ - SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no", - PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT); + WaitForProcSignalBarrier(barrier); } /* - * Utility wrapper to update the control file. Note that the control - * file gets flushed. + * InitLocalDataChecksumState + * + * Set up backend local caches of controldata variables which may change at + * any point during runtime and thus require special cased locking. So far + * this only applies to data_checksum_version, but it's intended to be general + * purpose enough to handle future cases. */ -static void -UpdateControlFile(void) +void +InitLocalDataChecksumState(void) { - update_controlfile(DataDir, ControlFile, true); + SpinLockAcquire(&XLogCtl->info_lck); + SetLocalDataChecksumState(XLogCtl->data_checksum_version); + SpinLockRelease(&XLogCtl->info_lck); } -/* - * Returns the unique system identifier from control file. - */ -uint64 -GetSystemIdentifier(void) +void +SetLocalDataChecksumState(uint32 data_checksum_version) { - Assert(ControlFile != NULL); - return ControlFile->system_identifier; -} + LocalDataChecksumState = data_checksum_version; -/* - * Returns the random nonce from control file. - */ -char * -GetMockAuthenticationNonce(void) -{ - Assert(ControlFile != NULL); - return ControlFile->mock_authentication_nonce; + data_checksums = data_checksum_version; } -/* - * Are checksums enabled for data pages? - */ -bool -DataChecksumsEnabled(void) +/* guc hook */ +const char * +show_data_checksums(void) { - Assert(ControlFile != NULL); - return (ControlFile->data_checksum_version > 0); + return get_checksum_state_string(LocalDataChecksumState); } /* @@ -4822,7 +5045,7 @@ check_wal_buffers(int *newval, void **extra, GucSource source) { /* * If we haven't yet changed the boot_val default of -1, just let it - * be. We'll fix it when XLOGShmemSize is called. + * be. We'll fix it when XLOGShmemRequest is called. */ if (XLOGbuffers == -1) return true; @@ -5011,6 +5234,25 @@ show_in_hot_standby(void) return RecoveryInProgress() ? "on" : "off"; } +/* + * GUC show_hook for effective_wal_level + */ +const char * +show_effective_wal_level(void) +{ + if (wal_level == WAL_LEVEL_MINIMAL) + return "minimal"; + + /* + * During recovery, effective_wal_level reflects the primary's + * configuration rather than the local wal_level value. + */ + if (RecoveryInProgress()) + return IsXLogLogicalInfoEnabled() ? "logical" : "replica"; + + return XLogLogicalInfoActive() ? "logical" : "replica"; +} + /* * Read the control file, set respective GUCs. * @@ -5027,8 +5269,10 @@ void LocalProcessControlFile(bool reset) { Assert(reset || ControlFile == NULL); - ControlFile = palloc(sizeof(ControlFileData)); + LocalControlFile = palloc_object(ControlFileData); + ControlFile = LocalControlFile; ReadControlFile(); + SetLocalDataChecksumState(ControlFile->data_checksum_version); } /* @@ -5043,10 +5287,10 @@ GetActiveWalLevelOnStandby(void) } /* - * Initialization of shared memory for XLOG + * Register shared memory for XLOG. */ -Size -XLOGShmemSize(void) +static void +XLOGShmemRequest(void *arg) { Size size; @@ -5086,23 +5330,24 @@ XLOGShmemSize(void) /* and the buffers themselves */ size = add_size(size, mul_size(XLOG_BLCKSZ, XLOGbuffers)); - /* - * Note: we don't count ControlFileData, it comes out of the "slop factor" - * added by CreateSharedMemoryAndSemaphores. This lets us use this - * routine again below to compute the actual allocation size. - */ - - return size; + ShmemRequestStruct(.name = "XLOG Ctl", + .size = size, + .ptr = (void **) &XLogCtl, + ); + ShmemRequestStruct(.name = "Control File", + .size = sizeof(ControlFileData), + .ptr = (void **) &ControlFile, + ); } -void -XLOGShmemInit(void) +/* + * XLOGShmemInit - initialize the XLogCtl shared memory area. + */ +static void +XLOGShmemInit(void *arg) { - bool foundCFile, - foundXLog; char *allocptr; int i; - ControlFileData *localControlFile; #ifdef WAL_DEBUG @@ -5120,36 +5365,17 @@ XLOGShmemInit(void) } #endif - - XLogCtl = (XLogCtlData *) - ShmemInitStruct("XLOG Ctl", XLOGShmemSize(), &foundXLog); - - localControlFile = ControlFile; - ControlFile = (ControlFileData *) - ShmemInitStruct("Control File", sizeof(ControlFileData), &foundCFile); - - if (foundCFile || foundXLog) - { - /* both should be present or neither */ - Assert(foundCFile && foundXLog); - - /* Initialize local copy of WALInsertLocks */ - WALInsertLocks = XLogCtl->Insert.WALInsertLocks; - - if (localControlFile) - pfree(localControlFile); - return; - } memset(XLogCtl, 0, sizeof(XLogCtlData)); /* * Already have read control file locally, unless in bootstrap mode. Move * contents into shared memory. */ - if (localControlFile) + if (LocalControlFile) { - memcpy(ControlFile, localControlFile, sizeof(ControlFileData)); - pfree(localControlFile); + memcpy(ControlFile, LocalControlFile, sizeof(ControlFileData)); + pfree(LocalControlFile); + LocalControlFile = NULL; } /* @@ -5198,16 +5424,26 @@ XLOGShmemInit(void) XLogCtl->InstallXLogFileSegmentActive = false; XLogCtl->WalWriterSleeping = false; + /* Use the checksum info from control file */ + XLogCtl->data_checksum_version = ControlFile->data_checksum_version; + + SetLocalDataChecksumState(XLogCtl->data_checksum_version); + SpinLockInit(&XLogCtl->Insert.insertpos_lck); SpinLockInit(&XLogCtl->info_lck); pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr); pg_atomic_init_u64(&XLogCtl->logWriteResult, InvalidXLogRecPtr); pg_atomic_init_u64(&XLogCtl->logFlushResult, InvalidXLogRecPtr); pg_atomic_init_u64(&XLogCtl->unloggedLSN, InvalidXLogRecPtr); +} - pg_atomic_init_u64(&XLogCtl->InitializeReserved, InvalidXLogRecPtr); - pg_atomic_init_u64(&XLogCtl->InitializedUpTo, InvalidXLogRecPtr); - ConditionVariableInit(&XLogCtl->InitializedUpToCondVar); +/* + * XLOGShmemAttach - re-establish WALInsertLocks pointer after attaching. + */ +static void +XLOGShmemAttach(void *arg) +{ + WALInsertLocks = XLogCtl->Insert.WALInsertLocks; } /* @@ -5218,7 +5454,7 @@ void BootStrapXLOG(uint32 data_checksum_version) { CheckPoint checkPoint; - char *buffer; + PGAlignedXLogBlock buffer; XLogPageHeader page; XLogLongPageHeader longpage; XLogRecord *record; @@ -5247,10 +5483,8 @@ BootStrapXLOG(uint32 data_checksum_version) sysidentifier |= ((uint64) tv.tv_usec) << 12; sysidentifier |= getpid() & 0xFFF; - /* page buffer must be aligned suitably for O_DIRECT */ - buffer = (char *) palloc(XLOG_BLCKSZ + XLOG_BLCKSZ); - page = (XLogPageHeader) TYPEALIGN(XLOG_BLCKSZ, buffer); - memset(page, 0, XLOG_BLCKSZ); + memset(&buffer, 0, sizeof buffer); + page = (XLogPageHeader) &buffer; /* * Set up information for the initial checkpoint record @@ -5263,12 +5497,13 @@ BootStrapXLOG(uint32 data_checksum_version) checkPoint.ThisTimeLineID = BootstrapTimeLineID; checkPoint.PrevTimeLineID = BootstrapTimeLineID; checkPoint.fullPageWrites = fullPageWrites; + checkPoint.logicalDecodingEnabled = (wal_level == WAL_LEVEL_LOGICAL); checkPoint.wal_level = wal_level; checkPoint.nextXid = FullTransactionIdFromEpochAndXid(0, FirstNormalTransactionId); checkPoint.nextOid = FirstGenbkiObjectId; checkPoint.nextMulti = FirstMultiXactId; - checkPoint.nextMultiOffset = 0; + checkPoint.nextMultiOffset = 1; checkPoint.oldestXid = FirstNormalTransactionId; checkPoint.oldestXidDB = Template1DbOid; checkPoint.oldestMulti = FirstMultiXactId; @@ -5277,6 +5512,7 @@ BootStrapXLOG(uint32 data_checksum_version) checkPoint.newestCommitTsXid = InvalidTransactionId; checkPoint.time = (pg_time_t) time(NULL); checkPoint.oldestActiveXid = InvalidTransactionId; + checkPoint.dataChecksumState = data_checksum_version; TransamVariables->nextXid = checkPoint.nextXid; TransamVariables->nextOid = checkPoint.nextOid; @@ -5284,7 +5520,7 @@ BootStrapXLOG(uint32 data_checksum_version) MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset); AdvanceOldestClogXid(checkPoint.oldestXid); SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB); - SetMultiXactIdLimit(checkPoint.oldestMulti, checkPoint.oldestMultiDB, true); + SetMultiXactIdLimit(checkPoint.oldestMulti, checkPoint.oldestMultiDB); SetCommitTsLimit(InvalidTransactionId, InvalidTransactionId); /* Set up the XLOG page header */ @@ -5300,7 +5536,7 @@ BootStrapXLOG(uint32 data_checksum_version) /* Insert the initial checkpoint record */ recptr = ((char *) page + SizeOfXLogLongPHD); record = (XLogRecord *) recptr; - record->xl_prev = 0; + record->xl_prev = InvalidXLogRecPtr; record->xl_xid = InvalidTransactionId; record->xl_tot_len = SizeOfXLogRecord + SizeOfXLogRecordDataHeaderShort + sizeof(checkPoint); record->xl_info = XLOG_CHECKPOINT_SHUTDOWN; @@ -5331,7 +5567,7 @@ BootStrapXLOG(uint32 data_checksum_version) /* Write the first page with the initial record */ errno = 0; pgstat_report_wait_start(WAIT_EVENT_WAL_BOOTSTRAP_WRITE); - if (write(openLogFile, page, XLOG_BLCKSZ) != XLOG_BLCKSZ) + if (write(openLogFile, &buffer, XLOG_BLCKSZ) != XLOG_BLCKSZ) { /* if write didn't set errno, assume problem is no disk space */ if (errno == 0) @@ -5371,8 +5607,6 @@ BootStrapXLOG(uint32 data_checksum_version) BootStrapSUBTRANS(); BootStrapMultiXact(); - pfree(buffer); - /* * Force control file to be read - in contrast to normal processing we'd * otherwise never run the checks and GUC related initializations therein. @@ -5381,11 +5615,9 @@ BootStrapXLOG(uint32 data_checksum_version) } static char * -str_time(pg_time_t tnow) +str_time(pg_time_t tnow, char *buf, size_t bufsize) { - char *buf = palloc(128); - - pg_strftime(buf, 128, + pg_strftime(buf, bufsize, "%Y-%m-%d %H:%M:%S %Z", pg_localtime(&tnow, log_timezone)); @@ -5628,6 +5860,7 @@ StartupXLOG(void) XLogRecPtr missingContrecPtr; TransactionId oldestActiveXID; bool promoted = false; + char timebuf[128]; /* * We should have an aux process resource owner to use, and we should not @@ -5656,25 +5889,29 @@ StartupXLOG(void) */ ereport(IsPostmasterEnvironment ? LOG : NOTICE, (errmsg("database system was shut down at %s", - str_time(ControlFile->time)))); + str_time(ControlFile->time, + timebuf, sizeof(timebuf))))); break; case DB_SHUTDOWNED_IN_RECOVERY: ereport(LOG, (errmsg("database system was shut down in recovery at %s", - str_time(ControlFile->time)))); + str_time(ControlFile->time, + timebuf, sizeof(timebuf))))); break; case DB_SHUTDOWNING: ereport(LOG, (errmsg("database system shutdown was interrupted; last known up at %s", - str_time(ControlFile->time)))); + str_time(ControlFile->time, + timebuf, sizeof(timebuf))))); break; case DB_IN_CRASH_RECOVERY: ereport(LOG, (errmsg("database system was interrupted while in recovery at %s", - str_time(ControlFile->time)), + str_time(ControlFile->time, + timebuf, sizeof(timebuf))), errhint("This probably means that some data is corrupted and" " you will have to use the last backup for recovery."))); break; @@ -5682,7 +5919,8 @@ StartupXLOG(void) case DB_IN_ARCHIVE_RECOVERY: ereport(LOG, (errmsg("database system was interrupted while in recovery at log time %s", - str_time(ControlFile->checkPointCopy.time)), + str_time(ControlFile->checkPointCopy.time, + timebuf, sizeof(timebuf))), errhint("If this has occurred more than once some data might be corrupted" " and you might need to choose an earlier recovery target."))); break; @@ -5690,7 +5928,8 @@ StartupXLOG(void) case DB_IN_PRODUCTION: ereport(LOG, (errmsg("database system was interrupted; last known up at %s", - str_time(ControlFile->time)))); + str_time(ControlFile->time, + timebuf, sizeof(timebuf))))); break; default: @@ -5760,10 +5999,9 @@ StartupXLOG(void) MultiXactSetNextMXact(checkPoint.nextMulti, checkPoint.nextMultiOffset); AdvanceOldestClogXid(checkPoint.oldestXid); SetTransactionIdLimit(checkPoint.oldestXid, checkPoint.oldestXidDB); - SetMultiXactIdLimit(checkPoint.oldestMulti, checkPoint.oldestMultiDB, true); + SetMultiXactIdLimit(checkPoint.oldestMulti, checkPoint.oldestMultiDB); SetCommitTsLimit(checkPoint.oldestCommitTsXid, checkPoint.newestCommitTsXid); - XLogCtl->ckptFullXid = checkPoint.nextXid; /* * Clear out any old relcache cache files. This is *necessary* if we do @@ -5785,6 +6023,12 @@ StartupXLOG(void) */ StartupReplicationSlots(); + /* + * Startup the logical decoding status with the last status stored in the + * checkpoint record. + */ + StartupLogicalDecodingStatus(checkPoint.logicalDecodingEnabled); + /* * Startup logical state, needs to be setup now so we have proper data * during crash recovery. @@ -6071,7 +6315,7 @@ StartupXLOG(void) */ if (InRecovery && (EndOfLog < LocalMinRecoveryPoint || - !XLogRecPtrIsInvalid(ControlFile->backupStartPoint))) + XLogRecPtrIsValid(ControlFile->backupStartPoint))) { /* * Ran off end of WAL before reaching end-of-backup WAL record, or @@ -6081,7 +6325,7 @@ StartupXLOG(void) */ if (ArchiveRecoveryRequested || ControlFile->backupEndRequired) { - if (!XLogRecPtrIsInvalid(ControlFile->backupStartPoint) || ControlFile->backupEndRequired) + if (XLogRecPtrIsValid(ControlFile->backupStartPoint) || ControlFile->backupEndRequired) ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("WAL ends before end of online backup"), @@ -6183,7 +6427,7 @@ StartupXLOG(void) * (It's critical to first write an OVERWRITE_CONTRECORD message, which * we'll do as soon as we're open for writing new WAL.) */ - if (!XLogRecPtrIsInvalid(missingContrecPtr)) + if (XLogRecPtrIsValid(missingContrecPtr)) { /* * We should only have a missingContrecPtr if we're not switching to a @@ -6193,7 +6437,7 @@ StartupXLOG(void) * disregard. */ Assert(newTLI == endOfRecoveryInfo->lastRecTLI); - Assert(!XLogRecPtrIsInvalid(abortedRecPtr)); + Assert(XLogRecPtrIsValid(abortedRecPtr)); EndOfLog = missingContrecPtr; } @@ -6227,8 +6471,7 @@ StartupXLOG(void) memset(page + len, 0, XLOG_BLCKSZ - len); pg_atomic_write_u64(&XLogCtl->xlblocks[firstIdx], endOfRecoveryInfo->lastPageBeginPtr + XLOG_BLCKSZ); - pg_atomic_write_u64(&XLogCtl->InitializedUpTo, endOfRecoveryInfo->lastPageBeginPtr + XLOG_BLCKSZ); - XLogCtl->InitializedFrom = endOfRecoveryInfo->lastPageBeginPtr; + XLogCtl->InitializedUpTo = endOfRecoveryInfo->lastPageBeginPtr + XLOG_BLCKSZ; } else { @@ -6237,10 +6480,8 @@ StartupXLOG(void) * let the first attempt to insert a log record to initialize the next * buffer. */ - pg_atomic_write_u64(&XLogCtl->InitializedUpTo, EndOfLog); - XLogCtl->InitializedFrom = EndOfLog; + XLogCtl->InitializedUpTo = EndOfLog; } - pg_atomic_write_u64(&XLogCtl->InitializeReserved, pg_atomic_read_u64(&XLogCtl->InitializedUpTo)); /* * Update local and shared status. This is OK to do without any locks @@ -6300,9 +6541,9 @@ StartupXLOG(void) LocalSetXLogInsertAllowed(); /* If necessary, write overwrite-contrecord before doing anything else */ - if (!XLogRecPtrIsInvalid(abortedRecPtr)) + if (XLogRecPtrIsValid(abortedRecPtr)) { - Assert(!XLogRecPtrIsInvalid(missingContrecPtr)); + Assert(XLogRecPtrIsValid(missingContrecPtr)); CreateOverwriteContrecordRecord(abortedRecPtr, missingContrecPtr, newTLI); } @@ -6336,6 +6577,59 @@ StartupXLOG(void) */ CompleteCommitTsInitialization(); + /* + * Update logical decoding status in shared memory and write an + * XLOG_LOGICAL_DECODING_STATUS_CHANGE, if necessary. + */ + UpdateLogicalDecodingStatusEndOfRecovery(); + + /* Clean up EndOfWalRecoveryInfo data to appease Valgrind leak checking */ + if (endOfRecoveryInfo->lastPage) + pfree(endOfRecoveryInfo->lastPage); + pfree(endOfRecoveryInfo->recoveryStopReason); + pfree(endOfRecoveryInfo); + + /* + * If we reach this point with checksums in the state inprogress-on, it + * means that data checksums were in the process of being enabled when the + * cluster shut down. Since processing didn't finish, the operation will + * have to be restarted from scratch since there is no capability to + * continue where it was when the cluster shut down. Thus, revert the + * state back to off, and inform the user with a warning message. Being + * able to restart processing is a TODO, but it wouldn't be possible to + * restart here since we cannot launch a dynamic background worker + * directly from here (it has to be from a regular backend). + */ + if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON) + { + XLogChecksums(PG_DATA_CHECKSUM_OFF); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; + SetLocalDataChecksumState(XLogCtl->data_checksum_version); + SpinLockRelease(&XLogCtl->info_lck); + + ereport(WARNING, + errmsg("enabling data checksums was interrupted"), + errhint("Data checksum processing must be manually restarted for checksums to be enabled")); + } + + /* + * If data checksums were being disabled when the cluster was shut down, + * we know that we have a state where all backends have stopped validating + * checksums and we can move to off instead of prompting the user to + * perform any action. + */ + if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF) + { + XLogChecksums(PG_DATA_CHECKSUM_OFF); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF; + SetLocalDataChecksumState(XLogCtl->data_checksum_version); + SpinLockRelease(&XLogCtl->info_lck); + } + /* * All done with end-of-recovery actions. * @@ -6361,6 +6655,20 @@ StartupXLOG(void) UpdateControlFile(); LWLockRelease(ControlFileLock); + /* + * Wake up the checkpointer process as there might be a request to disable + * logical decoding by concurrent slot drop. + */ + WakeupCheckpointer(); + + /* + * Wake up all waiters. They need to report an error that recovery was + * ended before reaching the target LSN. + */ + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_REPLAY, InvalidXLogRecPtr); + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_WRITE, InvalidXLogRecPtr); + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_FLUSH, InvalidXLogRecPtr); + /* * Shutdown the recovery environment. This must occur after * RecoverPreparedTransactions() (see notes in lock_twophase_recover()) @@ -6505,7 +6813,7 @@ PerformRecoveryXLogAction(void) else { RequestCheckpoint(CHECKPOINT_END_OF_RECOVERY | - CHECKPOINT_IMMEDIATE | + CHECKPOINT_FAST | CHECKPOINT_WAIT); } @@ -6627,7 +6935,7 @@ GetRedoRecPtr(void) XLogRecPtr ptr; /* - * The possibly not up-to-date copy in XlogCtl is enough. Even if we + * The possibly not up-to-date copy in XLogCtl is enough. Even if we * grabbed a WAL insertion lock to read the authoritative value in * Insert->RedoRecPtr, someone might update it just after we've released * the lock. @@ -6814,7 +7122,7 @@ ShutdownXLOG(int code, Datum arg) WalSndWaitStopping(); if (RecoveryInProgress()) - CreateRestartPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE); + CreateRestartPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_FAST); else { /* @@ -6826,10 +7134,32 @@ ShutdownXLOG(int code, Datum arg) if (XLogArchivingActive()) RequestXLogSwitch(false); - CreateCheckPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE); + CreateCheckPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_FAST); } } +/* + * Format checkpoint request flags as a space-separated string for + * log messages. + */ +static const char * +CheckpointFlagsString(int flags) +{ + static char buf[128]; + + snprintf(buf, sizeof(buf), "%s%s%s%s%s%s%s%s", + (flags & CHECKPOINT_IS_SHUTDOWN) ? " shutdown" : "", + (flags & CHECKPOINT_END_OF_RECOVERY) ? " end-of-recovery" : "", + (flags & CHECKPOINT_FAST) ? " fast" : "", + (flags & CHECKPOINT_FORCE) ? " force" : "", + (flags & CHECKPOINT_WAIT) ? " wait" : "", + (flags & CHECKPOINT_CAUSE_XLOG) ? " wal" : "", + (flags & CHECKPOINT_CAUSE_TIME) ? " time" : "", + (flags & CHECKPOINT_FLUSH_UNLOGGED) ? " flush-unlogged" : ""); + + return buf; +} + /* * Log start of a checkpoint. */ @@ -6838,35 +7168,21 @@ LogCheckpointStart(int flags, bool restartpoint) { if (restartpoint) ereport(LOG, - /* translator: the placeholders show checkpoint options */ - (errmsg("restartpoint starting:%s%s%s%s%s%s%s%s", - (flags & CHECKPOINT_IS_SHUTDOWN) ? " shutdown" : "", - (flags & CHECKPOINT_END_OF_RECOVERY) ? " end-of-recovery" : "", - (flags & CHECKPOINT_IMMEDIATE) ? " immediate" : "", - (flags & CHECKPOINT_FORCE) ? " force" : "", - (flags & CHECKPOINT_WAIT) ? " wait" : "", - (flags & CHECKPOINT_CAUSE_XLOG) ? " wal" : "", - (flags & CHECKPOINT_CAUSE_TIME) ? " time" : "", - (flags & CHECKPOINT_FLUSH_ALL) ? " flush-all" : ""))); + /* translator: the placeholder shows checkpoint options */ + (errmsg("restartpoint starting:%s", + CheckpointFlagsString(flags)))); else ereport(LOG, - /* translator: the placeholders show checkpoint options */ - (errmsg("checkpoint starting:%s%s%s%s%s%s%s%s", - (flags & CHECKPOINT_IS_SHUTDOWN) ? " shutdown" : "", - (flags & CHECKPOINT_END_OF_RECOVERY) ? " end-of-recovery" : "", - (flags & CHECKPOINT_IMMEDIATE) ? " immediate" : "", - (flags & CHECKPOINT_FORCE) ? " force" : "", - (flags & CHECKPOINT_WAIT) ? " wait" : "", - (flags & CHECKPOINT_CAUSE_XLOG) ? " wal" : "", - (flags & CHECKPOINT_CAUSE_TIME) ? " time" : "", - (flags & CHECKPOINT_FLUSH_ALL) ? " flush-all" : ""))); + /* translator: the placeholder shows checkpoint options */ + (errmsg("checkpoint starting:%s", + CheckpointFlagsString(flags)))); } /* * Log end of a checkpoint. */ static void -LogCheckpointEnd(bool restartpoint) +LogCheckpointEnd(bool restartpoint, int flags) { long write_msecs, sync_msecs, @@ -6916,12 +7232,13 @@ LogCheckpointEnd(bool restartpoint) */ if (restartpoint) ereport(LOG, - (errmsg("restartpoint complete: wrote %d buffers (%.1f%%), " + (errmsg("restartpoint complete:%s: wrote %d buffers (%.1f%%), " "wrote %d SLRU buffers; %d WAL file(s) added, " "%d removed, %d recycled; write=%ld.%03d s, " "sync=%ld.%03d s, total=%ld.%03d s; sync files=%d, " "longest=%ld.%03d s, average=%ld.%03d s; distance=%d kB, " - "estimate=%d kB; lsn=%X/%X, redo lsn=%X/%X", + "estimate=%d kB; lsn=%X/%08X, redo lsn=%X/%08X", + CheckpointFlagsString(flags), CheckpointStats.ckpt_bufs_written, (double) CheckpointStats.ckpt_bufs_written * 100 / NBuffers, CheckpointStats.ckpt_slru_written, @@ -6940,12 +7257,13 @@ LogCheckpointEnd(bool restartpoint) LSN_FORMAT_ARGS(ControlFile->checkPointCopy.redo)))); else ereport(LOG, - (errmsg("checkpoint complete: wrote %d buffers (%.1f%%), " + (errmsg("checkpoint complete:%s: wrote %d buffers (%.1f%%), " "wrote %d SLRU buffers; %d WAL file(s) added, " "%d removed, %d recycled; write=%ld.%03d s, " "sync=%ld.%03d s, total=%ld.%03d s; sync files=%d, " "longest=%ld.%03d s, average=%ld.%03d s; distance=%d kB, " - "estimate=%d kB; lsn=%X/%X, redo lsn=%X/%X", + "estimate=%d kB; lsn=%X/%08X, redo lsn=%X/%08X", + CheckpointFlagsString(flags), CheckpointStats.ckpt_bufs_written, (double) CheckpointStats.ckpt_bufs_written * 100 / NBuffers, CheckpointStats.ckpt_slru_written, @@ -7042,12 +7360,12 @@ update_checkpoint_display(int flags, bool restartpoint, bool reset) * flags is a bitwise OR of the following: * CHECKPOINT_IS_SHUTDOWN: checkpoint is for database shutdown. * CHECKPOINT_END_OF_RECOVERY: checkpoint is for end of WAL recovery. - * CHECKPOINT_IMMEDIATE: finish the checkpoint ASAP, - * ignoring checkpoint_completion_target parameter. + * CHECKPOINT_FAST: finish the checkpoint ASAP, ignoring + * checkpoint_completion_target parameter. * CHECKPOINT_FORCE: force a checkpoint even if no XLOG activity has occurred * since the last one (implied by CHECKPOINT_IS_SHUTDOWN or * CHECKPOINT_END_OF_RECOVERY). - * CHECKPOINT_FLUSH_ALL: also flush buffers of unlogged tables. + * CHECKPOINT_FLUSH_UNLOGGED: also flush buffers of unlogged tables. * * Note: flags contains other bits, of interest here only for logging purposes. * In particular note that this routine is synchronous and does not pay @@ -7119,6 +7437,10 @@ CreateCheckPoint(int flags) */ SyncPreCheckpoint(); + /* Run these points outside the critical section. */ + INJECTION_POINT("create-checkpoint-initial", NULL); + INJECTION_POINT_LOAD("create-checkpoint-run"); + /* * Use a critical section to force system panic if we have trouble. */ @@ -7142,7 +7464,7 @@ CreateCheckPoint(int flags) * starting snapshot of locks and transactions. */ if (!shutdown && XLogStandbyInfoActive()) - checkPoint.oldestActiveXid = GetOldestActiveTransactionId(); + checkPoint.oldestActiveXid = GetOldestActiveTransactionId(false, true); else checkPoint.oldestActiveXid = InvalidTransactionId; @@ -7191,6 +7513,12 @@ CreateCheckPoint(int flags) checkPoint.fullPageWrites = Insert->fullPageWrites; checkPoint.wal_level = wal_level; + /* + * Get the current data_checksum_version value from xlogctl, valid at the + * time of the checkpoint. + */ + checkPoint.dataChecksumState = XLogCtl->data_checksum_version; + if (shutdown) { XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos); @@ -7243,9 +7571,18 @@ CreateCheckPoint(int flags) */ if (!shutdown) { + xl_checkpoint_redo redo_rec; + + WALInsertLockAcquire(); + redo_rec.wal_level = wal_level; + SpinLockAcquire(&XLogCtl->info_lck); + redo_rec.data_checksum_version = XLogCtl->data_checksum_version; + SpinLockRelease(&XLogCtl->info_lck); + WALInsertLockRelease(); + /* Include WAL level in record for WAL summarizer's benefit. */ XLogBeginInsert(); - XLogRegisterData(&wal_level, sizeof(wal_level)); + XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo)); (void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO); /* @@ -7269,6 +7606,8 @@ CreateCheckPoint(int flags) if (log_checkpoints) LogCheckpointStart(flags, false); + INJECTION_POINT_CACHED("create-checkpoint-run", NULL); + /* Update the process title */ update_checkpoint_display(flags, false, false); @@ -7299,6 +7638,12 @@ CreateCheckPoint(int flags) checkPoint.nextOid += TransamVariables->oidCount; LWLockRelease(OidGenLock); + SpinLockAcquire(&XLogCtl->info_lck); + checkPoint.dataChecksumState = XLogCtl->data_checksum_version; + SpinLockRelease(&XLogCtl->info_lck); + + checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled(); + MultiXactGetCheckptMulti(shutdown, &checkPoint.nextMulti, &checkPoint.nextMultiOffset, @@ -7390,7 +7735,7 @@ CreateCheckPoint(int flags) * recovery we don't need to write running xact data. */ if (!shutdown && XLogStandbyInfoActive()) - LogStandbySnapshot(); + LogStandbySnapshot(InvalidOid); START_CRIT_SECTION(); @@ -7446,6 +7791,9 @@ CreateCheckPoint(int flags) ControlFile->minRecoveryPoint = InvalidXLogRecPtr; ControlFile->minRecoveryPointTLI = 0; + /* make sure we start with the checksum version as of the checkpoint */ + ControlFile->data_checksum_version = checkPoint.dataChecksumState; + /* * Persist unloggedLSN value. It's reset on crash recovery, so this goes * unused on non-shutdown checkpoints, but seems useful to store it always @@ -7456,11 +7804,6 @@ CreateCheckPoint(int flags) UpdateControlFile(); LWLockRelease(ControlFileLock); - /* Update shared-memory copy of checkpoint XID/epoch */ - SpinLockAcquire(&XLogCtl->info_lck); - XLogCtl->ckptFullXid = checkPoint.nextXid; - SpinLockRelease(&XLogCtl->info_lck); - /* * We are now done with critical updates; no need for system panic if we * have trouble while fooling with old log segments. @@ -7495,9 +7838,11 @@ CreateCheckPoint(int flags) * Update the average distance between checkpoints if the prior checkpoint * exists. */ - if (PriorRedoPtr != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(PriorRedoPtr)) UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr); + INJECTION_POINT("checkpoint-before-old-wal-removal", NULL); + /* * Delete old log files, those no longer needed for last checkpoint to * prevent the disk holding the xlog from growing full. @@ -7537,7 +7882,7 @@ CreateCheckPoint(int flags) TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning()); /* Real work is done; log and update stats. */ - LogCheckpointEnd(false); + LogCheckpointEnd(false, flags); /* Reset the process title */ update_checkpoint_display(flags, false, true); @@ -7592,6 +7937,12 @@ CreateEndOfRecoveryRecord(void) LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->minRecoveryPoint = recptr; ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID; + + /* start with the latest checksum version (as of the end of recovery) */ + SpinLockAcquire(&XLogCtl->info_lck); + ControlFile->data_checksum_version = XLogCtl->data_checksum_version; + SpinLockRelease(&XLogCtl->info_lck); + UpdateControlFile(); LWLockRelease(ControlFileLock); @@ -7637,7 +7988,7 @@ CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn, XLogRecPtr pagePtr, if (!RecoveryInProgress()) elog(ERROR, "can only be used at end of recovery"); if (pagePtr % XLOG_BLCKSZ != 0) - elog(ERROR, "invalid position for missing continuation record %X/%X", + elog(ERROR, "invalid position for missing continuation record %X/%08X", LSN_FORMAT_ARGS(pagePtr)); /* The current WAL insert position should be right after the page header */ @@ -7648,7 +7999,7 @@ CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn, XLogRecPtr pagePtr, startPos += SizeOfXLogShortPHD; recptr = GetXLogInsertRecPtr(); if (recptr != startPos) - elog(ERROR, "invalid WAL insert position %X/%X for OVERWRITE_CONTRECORD", + elog(ERROR, "invalid WAL insert position %X/%08X for OVERWRITE_CONTRECORD", LSN_FORMAT_ARGS(recptr)); START_CRIT_SECTION(); @@ -7678,7 +8029,7 @@ CreateOverwriteContrecordRecord(XLogRecPtr aborted_lsn, XLogRecPtr pagePtr, /* check that the record was inserted to the right place */ if (ProcLastRecPtr != startPos) - elog(ERROR, "OVERWRITE_CONTRECORD was inserted to unexpected position %X/%X", + elog(ERROR, "OVERWRITE_CONTRECORD was inserted to unexpected position %X/%08X", LSN_FORMAT_ARGS(ProcLastRecPtr)); XLogFlush(recptr); @@ -7747,8 +8098,7 @@ RecoveryRestartPoint(const CheckPoint *checkPoint, XLogReaderState *record) if (XLogHaveInvalidPages()) { elog(DEBUG2, - "could not record restart point at %X/%X because there " - "are unresolved references to invalid pages", + "could not record restart point at %X/%08X because there are unresolved references to invalid pages", LSN_FORMAT_ARGS(checkPoint->redo)); return; } @@ -7824,12 +8174,12 @@ CreateRestartPoint(int flags) * restartpoint. It's assumed that flushing the buffers will do that as a * side-effect. */ - if (XLogRecPtrIsInvalid(lastCheckPointRecPtr) || + if (!XLogRecPtrIsValid(lastCheckPointRecPtr) || lastCheckPoint.redo <= ControlFile->checkPointCopy.redo) { ereport(DEBUG2, - (errmsg_internal("skipping restartpoint, already performed at %X/%X", - LSN_FORMAT_ARGS(lastCheckPoint.redo)))); + errmsg_internal("skipping restartpoint, already performed at %X/%08X", + LSN_FORMAT_ARGS(lastCheckPoint.redo))); UpdateMinRecoveryPoint(InvalidXLogRecPtr, true); if (flags & CHECKPOINT_IS_SHUTDOWN) @@ -7934,6 +8284,10 @@ CreateRestartPoint(int flags) if (flags & CHECKPOINT_IS_SHUTDOWN) ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY; } + + /* we shall start with the latest checksum version */ + ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState; + UpdateControlFile(); } LWLockRelease(ControlFileLock); @@ -7942,7 +8296,7 @@ CreateRestartPoint(int flags) * Update the average distance between checkpoints/restartpoints if the * prior checkpoint exists. */ - if (PriorRedoPtr != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(PriorRedoPtr)) UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr); /* @@ -7959,6 +8313,9 @@ CreateRestartPoint(int flags) replayPtr = GetXLogReplayRecPtr(&replayTLI); endptr = (receivePtr < replayPtr) ? replayPtr : receivePtr; KeepLogSeg(endptr, &_logSegNo); + + INJECTION_POINT("restartpoint-before-slot-invalidation", NULL); + if (InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_REMOVED | RS_INVAL_IDLE_TIMEOUT, _logSegNo, InvalidOid, InvalidTransactionId)) @@ -8006,17 +8363,17 @@ CreateRestartPoint(int flags) TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning()); /* Real work is done; log and update stats. */ - LogCheckpointEnd(true); + LogCheckpointEnd(true, flags); /* Reset the process title */ update_checkpoint_display(flags, true, true); xtime = GetLatestXTime(); ereport((log_checkpoints ? LOG : DEBUG2), - (errmsg("recovery restart point at %X/%X", - LSN_FORMAT_ARGS(lastCheckPoint.redo)), - xtime ? errdetail("Last completed transaction was at log time %s.", - timestamptz_to_str(xtime)) : 0)); + errmsg("recovery restart point at %X/%08X", + LSN_FORMAT_ARGS(lastCheckPoint.redo)), + xtime ? errdetail("Last completed transaction was at log time %s.", + timestamptz_to_str(xtime)) : 0); /* * Finally, execute archive_cleanup_command, if any. @@ -8067,7 +8424,7 @@ GetWALAvailability(XLogRecPtr targetLSN) /* * slot does not reserve WAL. Either deactivated, or has never been active */ - if (XLogRecPtrIsInvalid(targetLSN)) + if (!XLogRecPtrIsValid(targetLSN)) return WALAVAIL_INVALID_LSN; /* @@ -8147,17 +8504,19 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo) XLByteToSeg(recptr, currSegNo, wal_segment_size); segno = currSegNo; - /* - * Calculate how many segments are kept by slots first, adjusting for - * max_slot_wal_keep_size. - */ + /* Calculate how many segments are kept by slots. */ keep = XLogGetReplicationSlotMinimumLSN(); - if (keep != InvalidXLogRecPtr && keep < recptr) + if (XLogRecPtrIsValid(keep) && keep < recptr) { XLByteToSeg(keep, segno, wal_segment_size); - /* Cap by max_slot_wal_keep_size ... */ - if (max_slot_wal_keep_size_mb >= 0) + /* + * Account for max_slot_wal_keep_size to avoid keeping more than + * configured. However, don't do that during a binary upgrade: if + * slots were to be invalidated because of this, it would not be + * possible to preserve logical ones during the upgrade. + */ + if (max_slot_wal_keep_size_mb >= 0 && !IsBinaryUpgrade) { uint64 slot_keep_segs; @@ -8174,7 +8533,7 @@ KeepLogSeg(XLogRecPtr recptr, XLogSegNo *logSegNo) * summarized. */ keep = GetOldestUnsummarizedLSN(NULL, NULL); - if (keep != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(keep)) { XLogSegNo unsummarized_segno; @@ -8277,12 +8636,36 @@ XLogRestorePoint(const char *rpName) RecPtr = XLogInsert(RM_XLOG_ID, XLOG_RESTORE_POINT); ereport(LOG, - (errmsg("restore point \"%s\" created at %X/%X", - rpName, LSN_FORMAT_ARGS(RecPtr)))); + errmsg("restore point \"%s\" created at %X/%08X", + rpName, LSN_FORMAT_ARGS(RecPtr))); return RecPtr; } +/* + * Write an empty XLOG record to assign a distinct LSN. + * + * This is used by some index AMs when building indexes on permanent relations + * with wal_level=minimal. In that scenario, WAL-logging will start after + * commit, but the index AM needs distinct LSNs to detect concurrent page + * modifications. When the current WAL insert position hasn't advanced since + * the last call, we emit a dummy record to ensure we get a new, distinct LSN. + */ +XLogRecPtr +XLogAssignLSN(void) +{ + int dummy = 0; + + /* + * Records other than XLOG_SWITCH must have content. We use an integer 0 + * to satisfy this restriction. + */ + XLogBeginInsert(); + XLogSetRecordFlags(XLOG_MARK_UNIMPORTANT); + XLogRegisterData(&dummy, sizeof(dummy)); + return XLogInsert(RM_XLOG_ID, XLOG_ASSIGN_LSN); +} + /* * Check if any of the GUC parameters that are critical for hot standby * have changed, and update the value in pg_control file if necessary. @@ -8343,6 +8726,24 @@ XLogReportParameters(void) } } +/* + * Log the new state of checksums + */ +static void +XLogChecksums(uint32 new_type) +{ + xl_checksum_state xlrec; + XLogRecPtr recptr; + + xlrec.new_checksum_state = new_type; + + XLogBeginInsert(); + XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state)); + + recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS); + XLogFlush(recptr); +} + /* * Update full_page_writes in shared memory, and write an * XLOG_FPW_CHANGE record if necessary. @@ -8469,6 +8870,11 @@ xlog_redo(XLogReaderState *record) MultiXactAdvanceOldest(checkPoint.oldestMulti, checkPoint.oldestMultiDB); + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = checkPoint.dataChecksumState; + SetLocalDataChecksumState(checkPoint.dataChecksumState); + SpinLockRelease(&XLogCtl->info_lck); + /* * No need to set oldestClogXid here as well; it'll be set when we * redo an xl_clog_truncate if it changed since initialization. @@ -8481,8 +8887,8 @@ xlog_redo(XLogReaderState *record) * never arrive. */ if (ArchiveRecoveryRequested && - !XLogRecPtrIsInvalid(ControlFile->backupStartPoint) && - XLogRecPtrIsInvalid(ControlFile->backupEndPoint)) + XLogRecPtrIsValid(ControlFile->backupStartPoint) && + !XLogRecPtrIsValid(ControlFile->backupEndPoint)) ereport(PANIC, (errmsg("online backup was canceled, recovery cannot continue"))); @@ -8528,13 +8934,9 @@ xlog_redo(XLogReaderState *record) /* ControlFile->checkPointCopy always tracks the latest ckpt XID */ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->checkPointCopy.nextXid = checkPoint.nextXid; + ControlFile->data_checksum_version = checkPoint.dataChecksumState; LWLockRelease(ControlFileLock); - /* Update shared-memory copy of checkpoint XID/epoch */ - SpinLockAcquire(&XLogCtl->info_lck); - XLogCtl->ckptFullXid = checkPoint.nextXid; - SpinLockRelease(&XLogCtl->info_lck); - /* * We should've already switched to the new TLI before replaying this * record. @@ -8546,11 +8948,21 @@ xlog_redo(XLogReaderState *record) checkPoint.ThisTimeLineID, replayTLI))); RecoveryRestartPoint(&checkPoint, record); + + /* + * After replaying a checkpoint record, free all smgr objects. + * Otherwise we would never do so for dropped relations, as the + * startup does not process shared invalidation messages or call + * AtEOXact_SMgr(). + */ + smgrdestroyall(); } else if (info == XLOG_CHECKPOINT_ONLINE) { CheckPoint checkPoint; TimeLineID replayTLI; + bool new_state = false; + int old_state; memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint)); /* In an ONLINE checkpoint, treat the XID counter as a minimum */ @@ -8589,13 +9001,10 @@ xlog_redo(XLogReaderState *record) /* ControlFile->checkPointCopy always tracks the latest ckpt XID */ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->checkPointCopy.nextXid = checkPoint.nextXid; + old_state = ControlFile->data_checksum_version; + ControlFile->data_checksum_version = checkPoint.dataChecksumState; LWLockRelease(ControlFileLock); - /* Update shared-memory copy of checkpoint XID/epoch */ - SpinLockAcquire(&XLogCtl->info_lck); - XLogCtl->ckptFullXid = checkPoint.nextXid; - SpinLockRelease(&XLogCtl->info_lck); - /* TLI should not change in an on-line checkpoint */ (void) GetCurrentReplayRecPtr(&replayTLI); if (checkPoint.ThisTimeLineID != replayTLI) @@ -8604,6 +9013,26 @@ xlog_redo(XLogReaderState *record) checkPoint.ThisTimeLineID, replayTLI))); RecoveryRestartPoint(&checkPoint, record); + + /* + * If the data checksum state change we need to emit a barrier. + */ + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = checkPoint.dataChecksumState; + if (checkPoint.dataChecksumState != old_state) + new_state = true; + SpinLockRelease(&XLogCtl->info_lck); + + if (new_state) + EmitAndWaitDataChecksumsBarrier(checkPoint.dataChecksumState); + + /* + * After replaying a checkpoint record, free all smgr objects. + * Otherwise we would never do so for dropped relations, as the + * startup does not process shared invalidation messages or call + * AtEOXact_SMgr(). + */ + smgrdestroyall(); } else if (info == XLOG_OVERWRITE_CONTRECORD) { @@ -8644,6 +9073,10 @@ xlog_redo(XLogReaderState *record) { /* nothing to do here, handled in xlogrecovery.c */ } + else if (info == XLOG_ASSIGN_LSN) + { + /* nothing to do here, see XLogGetFakeLSN() */ + } else if (info == XLOG_FPI || info == XLOG_FPI_FOR_HINT) { /* @@ -8689,21 +9122,6 @@ xlog_redo(XLogReaderState *record) /* Update our copy of the parameters in pg_control */ memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_parameter_change)); - /* - * Invalidate logical slots if we are in hot standby and the primary - * does not have a WAL level sufficient for logical decoding. No need - * to search for potentially conflicting logically slots if standby is - * running with wal_level lower than logical, because in that case, we - * would have either disallowed creation of logical slots or - * invalidated existing ones. - */ - if (InRecovery && InHotStandby && - xlrec.wal_level < WAL_LEVEL_LOGICAL && - wal_level >= WAL_LEVEL_LOGICAL) - InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_LEVEL, - 0, InvalidOid, - InvalidTransactionId); - LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->MaxConnections = xlrec.MaxConnections; ControlFile->max_worker_processes = xlrec.max_worker_processes; @@ -8726,7 +9144,7 @@ xlog_redo(XLogReaderState *record) LocalMinRecoveryPoint = ControlFile->minRecoveryPoint; LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI; } - if (LocalMinRecoveryPoint != InvalidXLogRecPtr && LocalMinRecoveryPoint < lsn) + if (XLogRecPtrIsValid(LocalMinRecoveryPoint) && LocalMinRecoveryPoint < lsn) { TimeLineID replayTLI; @@ -8769,7 +9187,92 @@ xlog_redo(XLogReaderState *record) } else if (info == XLOG_CHECKPOINT_REDO) { - /* nothing to do here, just for informational purposes */ + xl_checkpoint_redo redo_rec; + bool new_state = false; + + memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo)); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = redo_rec.data_checksum_version; + if (redo_rec.data_checksum_version != ControlFile->data_checksum_version) + new_state = true; + SpinLockRelease(&XLogCtl->info_lck); + + if (new_state) + EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version); + } + else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE) + { + bool status; + + memcpy(&status, XLogRecGetData(record), sizeof(bool)); + + /* + * We need to toggle the logical decoding status and update the + * XLogLogicalInfo cache of processes synchronously because + * XLogLogicalInfoActive() is used even during read-only queries + * (e.g., via RelationIsAccessibleInLogicalDecoding()). In the + * 'disable' case, it is safe to invalidate existing slots after + * disabling logical decoding because logical decoding cannot process + * subsequent WAL records, which may not contain logical information. + */ + if (status) + EnableLogicalDecoding(); + else + DisableLogicalDecoding(); + + elog(DEBUG1, "update logical decoding status to %d during recovery", + status); + + if (InRecovery && InHotStandby) + { + if (!status) + { + /* + * Invalidate logical slots if we are in hot standby and the + * primary disabled logical decoding. + */ + InvalidateObsoleteReplicationSlots(RS_INVAL_WAL_LEVEL, + 0, InvalidOid, + InvalidTransactionId); + } + else if (sync_replication_slots) + { + /* + * Signal the postmaster to launch the slotsync worker. + * + * XXX: For simplicity, we keep the slotsync worker running + * even after logical decoding is disabled. A future + * improvement can consider starting and stopping the worker + * based on logical decoding status change. + */ + kill(PostmasterPid, SIGUSR1); + } + } + } +} + +void +xlog2_redo(XLogReaderState *record) +{ + uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; + + if (info == XLOG2_CHECKSUMS) + { + xl_checksum_state state; + + memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state)); + + SpinLockAcquire(&XLogCtl->info_lck); + XLogCtl->data_checksum_version = state.new_checksum_state; + SpinLockRelease(&XLogCtl->info_lck); + + /* + * Block on a procsignalbarrier to await all processes having seen the + * change to checksum status. Once the barrier has been passed we can + * initiate the corresponding processing. + */ + EmitAndWaitDataChecksumsBarrier(state.new_checksum_state); } } @@ -8943,9 +9446,8 @@ issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli) * backup state and tablespace map. * * Input parameters are "state" (the backup state), "fast" (if true, we do - * the checkpoint in immediate mode to make it faster), and "tablespaces" - * (if non-NULL, indicates a list of tablespaceinfo structs describing the - * cluster's tablespaces.). + * the checkpoint in fast mode), and "tablespaces" (if non-NULL, indicates a + * list of tablespaceinfo structs describing the cluster's tablespaces.). * * The tablespace map contents are appended to passed-in parameter * tablespace_map and the caller is responsible for including it in the backup @@ -9022,7 +9524,7 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, * work correctly, it is critical that sessionBackupState is only updated * after this block is over. */ - PG_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, DatumGetBool(true)); + PG_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, BoolGetDatum(true)); { bool gotUniqueStartpoint = false; DIR *tblspcdir; @@ -9041,12 +9543,6 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, * pg_wal directory was not included in the base backup and the WAL * archive was cleared too before starting the backup. * - * This also ensures that we have emitted a WAL page header that has - * XLP_BKP_REMOVABLE off before we emit the checkpoint record. - * Therefore, if a WAL archiver (such as pglesslog) is trying to - * compress out removable backup blocks, it won't remove any that - * occur after this point. - * * During recovery, we skip forcing XLOG file switch, which means that * the backup taken during recovery is not available for the special * recovery case described above. @@ -9073,11 +9569,11 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, * during recovery means that checkpointer is running, we can use * RequestCheckpoint() to establish a restartpoint. * - * We use CHECKPOINT_IMMEDIATE only if requested by user (via - * passing fast = true). Otherwise this can take awhile. + * We use CHECKPOINT_FAST only if requested by user (via passing + * fast = true). Otherwise this can take awhile. */ RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | - (fast ? CHECKPOINT_IMMEDIATE : 0)); + (fast ? CHECKPOINT_FAST : 0)); /* * Now we need to fetch the checkpoint record location, and also @@ -9248,7 +9744,7 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, continue; } - ti = palloc(sizeof(tablespaceinfo)); + ti = palloc_object(tablespaceinfo); ti->oid = tsoid; ti->path = pstrdup(linkpath); ti->rpath = relpath; @@ -9261,7 +9757,7 @@ do_pg_backup_start(const char *backupidstr, bool fast, List **tablespaces, state->starttime = (pg_time_t) time(NULL); } - PG_END_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, DatumGetBool(true)); + PG_END_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, BoolGetDatum(true)); state->started_in_recovery = backup_started_in_recovery; @@ -9601,7 +10097,7 @@ register_persistent_abort_backup_handler(void) if (already_done) return; - before_shmem_exit(do_pg_abort_backup, DatumGetBool(false)); + before_shmem_exit(do_pg_abort_backup, BoolGetDatum(false)); already_done = true; } @@ -9621,6 +10117,22 @@ GetXLogInsertRecPtr(void) return XLogBytePosToRecPtr(current_bytepos); } +/* + * Get latest WAL record end pointer + */ +XLogRecPtr +GetXLogInsertEndRecPtr(void) +{ + XLogCtlInsert *Insert = &XLogCtl->Insert; + uint64 current_bytepos; + + SpinLockAcquire(&Insert->insertpos_lck); + current_bytepos = Insert->CurrBytePos; + SpinLockRelease(&Insert->insertpos_lck); + + return XLogBytePosToEndRecPtr(current_bytepos); +} + /* * Get latest WAL write pointer */ @@ -9649,11 +10161,10 @@ GetOldestRestartPoint(XLogRecPtr *oldrecptr, TimeLineID *oldtli) void XLogShutdownWalRcv(void) { - ShutdownWalRcv(); + Assert(AmStartupProcess() || !IsUnderPostmaster); - LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); - XLogCtl->InstallXLogFileSegmentActive = false; - LWLockRelease(ControlFileLock); + ShutdownWalRcv(); + ResetInstallXLogFileSegmentActive(); } /* Enable WAL file recycling and preallocation. */ @@ -9665,6 +10176,15 @@ SetInstallXLogFileSegmentActive(void) LWLockRelease(ControlFileLock); } +/* Disable WAL file recycling and preallocation. */ +void +ResetInstallXLogFileSegmentActive(void) +{ + LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); + XLogCtl->InstallXLogFileSegmentActive = false; + LWLockRelease(ControlFileLock); +} + bool IsInstallXLogFileSegmentActive(void) { diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c index 1ef1713c91a49..9a0c8097cb15c 100644 --- a/src/backend/access/transam/xlogarchive.c +++ b/src/backend/access/transam/xlogarchive.c @@ -4,7 +4,7 @@ * Functions for archiving WAL files and restoring from the archive. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/xlogarchive.c @@ -31,6 +31,7 @@ #include "replication/walsender.h" #include "storage/fd.h" #include "storage/ipc.h" +#include "utils/wait_event.h" /* * Attempt to retrieve the specified file from off-line archival storage. diff --git a/src/backend/access/transam/xlogbackup.c b/src/backend/access/transam/xlogbackup.c index 342590e0a46d3..cf5cc8ead9689 100644 --- a/src/backend/access/transam/xlogbackup.c +++ b/src/backend/access/transam/xlogbackup.c @@ -3,7 +3,7 @@ * xlogbackup.c * Internal routines for base backups. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -31,18 +31,19 @@ build_backup_content(BackupState *state, bool ishistoryfile) char startstrbuf[128]; char startxlogfile[MAXFNAMELEN]; /* backup start WAL file */ XLogSegNo startsegno; - StringInfo result = makeStringInfo(); - char *data; + StringInfoData result; Assert(state != NULL); + initStringInfo(&result); + /* Use the log timezone here, not the session timezone */ pg_strftime(startstrbuf, sizeof(startstrbuf), "%Y-%m-%d %H:%M:%S %Z", pg_localtime(&state->starttime, log_timezone)); XLByteToSeg(state->startpoint, startsegno, wal_segment_size); XLogFileName(startxlogfile, state->starttli, startsegno, wal_segment_size); - appendStringInfo(result, "START WAL LOCATION: %X/%X (file %s)\n", + appendStringInfo(&result, "START WAL LOCATION: %X/%08X (file %s)\n", LSN_FORMAT_ARGS(state->startpoint), startxlogfile); if (ishistoryfile) @@ -52,18 +53,18 @@ build_backup_content(BackupState *state, bool ishistoryfile) XLByteToSeg(state->stoppoint, stopsegno, wal_segment_size); XLogFileName(stopxlogfile, state->stoptli, stopsegno, wal_segment_size); - appendStringInfo(result, "STOP WAL LOCATION: %X/%X (file %s)\n", + appendStringInfo(&result, "STOP WAL LOCATION: %X/%08X (file %s)\n", LSN_FORMAT_ARGS(state->stoppoint), stopxlogfile); } - appendStringInfo(result, "CHECKPOINT LOCATION: %X/%X\n", + appendStringInfo(&result, "CHECKPOINT LOCATION: %X/%08X\n", LSN_FORMAT_ARGS(state->checkpointloc)); - appendStringInfoString(result, "BACKUP METHOD: streamed\n"); - appendStringInfo(result, "BACKUP FROM: %s\n", + appendStringInfoString(&result, "BACKUP METHOD: streamed\n"); + appendStringInfo(&result, "BACKUP FROM: %s\n", state->started_in_recovery ? "standby" : "primary"); - appendStringInfo(result, "START TIME: %s\n", startstrbuf); - appendStringInfo(result, "LABEL: %s\n", state->name); - appendStringInfo(result, "START TIMELINE: %u\n", state->starttli); + appendStringInfo(&result, "START TIME: %s\n", startstrbuf); + appendStringInfo(&result, "LABEL: %s\n", state->name); + appendStringInfo(&result, "START TIMELINE: %u\n", state->starttli); if (ishistoryfile) { @@ -73,22 +74,19 @@ build_backup_content(BackupState *state, bool ishistoryfile) pg_strftime(stopstrfbuf, sizeof(stopstrfbuf), "%Y-%m-%d %H:%M:%S %Z", pg_localtime(&state->stoptime, log_timezone)); - appendStringInfo(result, "STOP TIME: %s\n", stopstrfbuf); - appendStringInfo(result, "STOP TIMELINE: %u\n", state->stoptli); + appendStringInfo(&result, "STOP TIME: %s\n", stopstrfbuf); + appendStringInfo(&result, "STOP TIMELINE: %u\n", state->stoptli); } /* either both istartpoint and istarttli should be set, or neither */ - Assert(XLogRecPtrIsInvalid(state->istartpoint) == (state->istarttli == 0)); - if (!XLogRecPtrIsInvalid(state->istartpoint)) + Assert(XLogRecPtrIsValid(state->istartpoint) == (state->istarttli != 0)); + if (XLogRecPtrIsValid(state->istartpoint)) { - appendStringInfo(result, "INCREMENTAL FROM LSN: %X/%X\n", + appendStringInfo(&result, "INCREMENTAL FROM LSN: %X/%08X\n", LSN_FORMAT_ARGS(state->istartpoint)); - appendStringInfo(result, "INCREMENTAL FROM TLI: %u\n", + appendStringInfo(&result, "INCREMENTAL FROM TLI: %u\n", state->istarttli); } - data = result->data; - pfree(result); - - return data; + return result.data; } diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c index 8c3090165f001..0f5979691e6bf 100644 --- a/src/backend/access/transam/xlogfuncs.c +++ b/src/backend/access/transam/xlogfuncs.c @@ -7,7 +7,7 @@ * This file contains WAL control and information functions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/xlogfuncs.c @@ -22,10 +22,12 @@ #include "access/xlog_internal.h" #include "access/xlogbackup.h" #include "access/xlogrecovery.h" +#include "catalog/pg_authid.h" #include "catalog/pg_type.h" #include "funcapi.h" #include "miscadmin.h" #include "pgstat.h" +#include "utils/acl.h" #include "replication/walreceiver.h" #include "storage/fd.h" #include "storage/latch.h" @@ -34,6 +36,7 @@ #include "utils/memutils.h" #include "utils/pg_lsn.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* * Backup-related variables. @@ -44,6 +47,33 @@ static StringInfo tablespace_map = NULL; /* Session-level context for the SQL-callable backup functions */ static MemoryContext backupcontext = NULL; + +/* + * Return a string constant representing the recovery pause state. This is + * used in system functions and views, and should *not* be translated. + */ +static const char * +GetRecoveryPauseStateString(RecoveryPauseState pause_state) +{ + const char *statestr = NULL; + + switch (pause_state) + { + case RECOVERY_NOT_PAUSED: + statestr = "not paused"; + break; + case RECOVERY_PAUSE_REQUESTED: + statestr = "pause requested"; + break; + case RECOVERY_PAUSED: + statestr = "paused"; + break; + } + + Assert(statestr != NULL); + return statestr; +} + /* * pg_backup_start: set up for taking an on-line backup dump * @@ -90,7 +120,7 @@ pg_backup_start(PG_FUNCTION_ARGS) } oldcontext = MemoryContextSwitchTo(backupcontext); - backup_state = (BackupState *) palloc0(sizeof(BackupState)); + backup_state = palloc0_object(BackupState); tablespace_map = makeStringInfo(); MemoryContextSwitchTo(oldcontext); @@ -215,7 +245,7 @@ pg_log_standby_snapshot(PG_FUNCTION_ARGS) (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("pg_log_standby_snapshot() can only be used if \"wal_level\" >= \"replica\""))); - recptr = LogStandbySnapshot(); + recptr = LogStandbySnapshot(InvalidOid); /* * As a convenience, return the WAL location of the last inserted record @@ -341,7 +371,7 @@ pg_last_wal_receive_lsn(PG_FUNCTION_ARGS) recptr = GetWalRcvFlushRecPtr(NULL, NULL); - if (recptr == 0) + if (!XLogRecPtrIsValid(recptr)) PG_RETURN_NULL(); PG_RETURN_LSN(recptr); @@ -360,7 +390,7 @@ pg_last_wal_replay_lsn(PG_FUNCTION_ARGS) recptr = GetXLogReplayRecPtr(NULL); - if (recptr == 0) + if (!XLogRecPtrIsValid(recptr)) PG_RETURN_NULL(); PG_RETURN_LSN(recptr); @@ -400,6 +430,7 @@ pg_walfile_name_offset(PG_FUNCTION_ARGS) TupleDescInitEntry(resultTupleDesc, (AttrNumber) 2, "file_offset", INT4OID, -1, 0); + TupleDescFinalize(resultTupleDesc); resultTupleDesc = BlessTupleDesc(resultTupleDesc); /* @@ -479,7 +510,7 @@ pg_split_walfile_name(PG_FUNCTION_ARGS) /* Capitalize WAL file name. */ for (p = fname_upper; *p; p++) - *p = pg_toupper((unsigned char) *p); + *p = pg_ascii_toupper((unsigned char) *p); if (!IsXLogFileName(fname_upper)) ereport(ERROR, @@ -592,7 +623,7 @@ pg_is_wal_replay_paused(PG_FUNCTION_ARGS) Datum pg_get_wal_replay_pause_state(PG_FUNCTION_ARGS) { - char *statestr = NULL; + RecoveryPauseState state; if (!RecoveryInProgress()) ereport(ERROR, @@ -600,22 +631,10 @@ pg_get_wal_replay_pause_state(PG_FUNCTION_ARGS) errmsg("recovery is not in progress"), errhint("Recovery control functions can only be executed during recovery."))); - /* get the recovery pause state */ - switch (GetRecoveryPauseState()) - { - case RECOVERY_NOT_PAUSED: - statestr = "not paused"; - break; - case RECOVERY_PAUSE_REQUESTED: - statestr = "pause requested"; - break; - case RECOVERY_PAUSED: - statestr = "paused"; - break; - } + state = GetRecoveryPauseState(); - Assert(statestr != NULL); - PG_RETURN_TEXT_P(cstring_to_text(statestr)); + /* get the recovery pause state */ + PG_RETURN_TEXT_P(cstring_to_text(GetRecoveryPauseStateString(state))); } /* @@ -672,7 +691,7 @@ pg_promote(PG_FUNCTION_ARGS) bool wait = PG_GETARG_BOOL(0); int wait_seconds = PG_GETARG_INT32(1); FILE *promote_file; - int i; + TimestampTz end_time; if (!RecoveryInProgress()) ereport(ERROR, @@ -713,8 +732,8 @@ pg_promote(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); /* wait for the amount of time wanted until promotion */ -#define WAITS_PER_SECOND 10 - for (i = 0; i < WAITS_PER_SECOND * wait_seconds; i++) + end_time = TimestampTzPlusSeconds(GetCurrentTimestamp(), wait_seconds); + while (GetCurrentTimestamp() < end_time) { int rc; @@ -727,7 +746,7 @@ pg_promote(PG_FUNCTION_ARGS) rc = WaitLatch(MyLatch, WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, - 1000L / WAITS_PER_SECOND, + 100L, WAIT_EVENT_PROMOTE); /* @@ -748,3 +767,94 @@ pg_promote(PG_FUNCTION_ARGS) wait_seconds))); PG_RETURN_BOOL(false); } + +/* + * pg_stat_get_recovery - returns information about WAL recovery state + * + * Returns NULL when not in recovery or when the caller lacks + * pg_read_all_stats privileges; one row otherwise. + */ +Datum +pg_stat_get_recovery(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum *values; + bool *nulls; + + /* Local copies of shared state */ + bool promote_triggered; + XLogRecPtr last_replayed_read_lsn; + XLogRecPtr last_replayed_end_lsn; + TimeLineID last_replayed_tli; + XLogRecPtr replay_end_lsn; + TimeLineID replay_end_tli; + TimestampTz recovery_last_xact_time; + TimestampTz current_chunk_start_time; + RecoveryPauseState pause_state; + + if (!RecoveryInProgress()) + PG_RETURN_NULL(); + + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + PG_RETURN_NULL(); + + /* Take a lock to ensure value consistency */ + SpinLockAcquire(&XLogRecoveryCtl->info_lck); + promote_triggered = XLogRecoveryCtl->SharedPromoteIsTriggered; + last_replayed_read_lsn = XLogRecoveryCtl->lastReplayedReadRecPtr; + last_replayed_end_lsn = XLogRecoveryCtl->lastReplayedEndRecPtr; + last_replayed_tli = XLogRecoveryCtl->lastReplayedTLI; + replay_end_lsn = XLogRecoveryCtl->replayEndRecPtr; + replay_end_tli = XLogRecoveryCtl->replayEndTLI; + recovery_last_xact_time = XLogRecoveryCtl->recoveryLastXTime; + current_chunk_start_time = XLogRecoveryCtl->currentChunkStartTime; + pause_state = XLogRecoveryCtl->recoveryPauseState; + SpinLockRelease(&XLogRecoveryCtl->info_lck); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + values = palloc0_array(Datum, tupdesc->natts); + nulls = palloc0_array(bool, tupdesc->natts); + + values[0] = BoolGetDatum(promote_triggered); + + if (XLogRecPtrIsValid(last_replayed_read_lsn)) + values[1] = LSNGetDatum(last_replayed_read_lsn); + else + nulls[1] = true; + + if (XLogRecPtrIsValid(last_replayed_end_lsn)) + values[2] = LSNGetDatum(last_replayed_end_lsn); + else + nulls[2] = true; + + if (XLogRecPtrIsValid(last_replayed_end_lsn)) + values[3] = Int32GetDatum(last_replayed_tli); + else + nulls[3] = true; + + if (XLogRecPtrIsValid(replay_end_lsn)) + values[4] = LSNGetDatum(replay_end_lsn); + else + nulls[4] = true; + + if (XLogRecPtrIsValid(replay_end_lsn)) + values[5] = Int32GetDatum(replay_end_tli); + else + nulls[5] = true; + + if (recovery_last_xact_time != 0) + values[6] = TimestampTzGetDatum(recovery_last_xact_time); + else + nulls[6] = true; + + if (current_chunk_start_time != 0) + values[7] = TimestampTzGetDatum(current_chunk_start_time); + else + nulls[7] = true; + + values[8] = CStringGetTextDatum(GetRecoveryPauseStateString(pause_state)); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} diff --git a/src/backend/access/transam/xloginsert.c b/src/backend/access/transam/xloginsert.c index 5ee9d0b028eae..f2e10b82b7d3e 100644 --- a/src/backend/access/transam/xloginsert.c +++ b/src/backend/access/transam/xloginsert.c @@ -9,7 +9,7 @@ * of XLogRecData structs by a call to XLogRecordAssemble(). See * access/transam/README for details. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/xloginsert.c @@ -33,12 +33,15 @@ #include "access/xloginsert.h" #include "catalog/pg_control.h" #include "common/pg_lzcompress.h" +#include "executor/instrument.h" #include "miscadmin.h" #include "pg_trace.h" #include "replication/origin.h" #include "storage/bufmgr.h" #include "storage/proc.h" #include "utils/memutils.h" +#include "utils/pgstat_internal.h" +#include "utils/rel.h" /* * Guess the maximum buffer size required to store a compressed version of @@ -113,7 +116,7 @@ static uint8 curinsert_flags = 0; static XLogRecData hdr_rdt; static char *hdr_scratch = NULL; -#define SizeOfXlogOrigin (sizeof(RepOriginId) + sizeof(char)) +#define SizeOfXlogOrigin (sizeof(ReplOriginId) + sizeof(char)) #define SizeOfXLogTransactionId (sizeof(TransactionId) + sizeof(char)) #define HEADER_SCRATCH_SIZE \ @@ -137,6 +140,7 @@ static MemoryContext xloginsert_cxt; static XLogRecData *XLogRecordAssemble(RmgrId rmid, uint8 info, XLogRecPtr RedoRecPtr, bool doPageWrites, XLogRecPtr *fpw_lsn, int *num_fpi, + uint64 *fpi_bytes, bool *topxid_included); static bool XLogCompressBackupBlock(const PageData *page, uint16 hole_offset, uint16 hole_length, void *dest, uint16 *dlen); @@ -248,9 +252,9 @@ XLogRegisterBuffer(uint8 block_id, Buffer buffer, uint8 flags) Assert(begininsert_called); /* - * Ordinarily, buffer should be exclusive-locked and marked dirty before - * we get here, otherwise we could end up violating one of the rules in - * access/transam/README. + * Ordinarily, the buffer should be exclusive-locked (or share-exclusive + * in case of hint bits) and marked dirty before we get here, otherwise we + * could end up violating one of the rules in access/transam/README. * * Some callers intentionally register a clean page and never update that * page's LSN; in that case they can pass the flag REGBUF_NO_CHANGE to @@ -258,7 +262,11 @@ XLogRegisterBuffer(uint8 block_id, Buffer buffer, uint8 flags) */ #ifdef USE_ASSERT_CHECKING if (!(flags & REGBUF_NO_CHANGE)) - Assert(BufferIsExclusiveLocked(buffer) && BufferIsDirty(buffer)); + { + Assert(BufferIsDirty(buffer)); + Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE) || + BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_SHARE_EXCLUSIVE)); + } #endif if (block_id >= max_registered_block_id) @@ -509,6 +517,7 @@ XLogInsert(RmgrId rmid, uint8 info) XLogRecPtr fpw_lsn; XLogRecData *rdt; int num_fpi = 0; + uint64 fpi_bytes = 0; /* * Get values needed to decide whether to do full-page writes. Since @@ -518,17 +527,81 @@ XLogInsert(RmgrId rmid, uint8 info) GetFullPageWriteInfo(&RedoRecPtr, &doPageWrites); rdt = XLogRecordAssemble(rmid, info, RedoRecPtr, doPageWrites, - &fpw_lsn, &num_fpi, &topxid_included); + &fpw_lsn, &num_fpi, &fpi_bytes, + &topxid_included); EndPos = XLogInsertRecord(rdt, fpw_lsn, curinsert_flags, num_fpi, - topxid_included); - } while (EndPos == InvalidXLogRecPtr); + fpi_bytes, topxid_included); + } while (!XLogRecPtrIsValid(EndPos)); XLogResetInsertion(); return EndPos; } +/* + * Simple wrapper to XLogInsert to insert a WAL record with elementary + * contents (only an int64 is supported as value currently). + */ +XLogRecPtr +XLogSimpleInsertInt64(RmgrId rmid, uint8 info, int64 value) +{ + XLogBeginInsert(); + XLogRegisterData(&value, sizeof(value)); + return XLogInsert(rmid, info); +} + +/* + * XLogGetFakeLSN - get a fake LSN for an index page that isn't WAL-logged. + * + * Some index AMs use LSNs to detect concurrent page modifications, but not + * all index pages are WAL-logged. This function provides a sequence of fake + * LSNs for that purpose. + */ +XLogRecPtr +XLogGetFakeLSN(Relation rel) +{ + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) + { + /* + * Temporary relations are only accessible in our session, so a simple + * backend-local counter will do. + */ + static XLogRecPtr counter = FirstNormalUnloggedLSN; + + return counter++; + } + else if (rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED) + { + /* + * Unlogged relations are accessible from other backends, and survive + * (clean) restarts. GetFakeLSNForUnloggedRel() handles that for us. + */ + return GetFakeLSNForUnloggedRel(); + } + else + { + /* + * WAL-logging on this relation will start after commit, so its LSNs + * must be distinct numbers smaller than the LSN at the next commit. + * Emit a dummy WAL record if insert-LSN hasn't advanced after the + * last call. + */ + static XLogRecPtr lastlsn = InvalidXLogRecPtr; + XLogRecPtr currlsn = GetXLogInsertEndRecPtr(); + + Assert(!RelationNeedsWAL(rel)); + Assert(RelationIsPermanent(rel)); + + /* No need for an actual record if we already have a distinct LSN */ + if (XLogRecPtrIsValid(lastlsn) && lastlsn == currlsn) + currlsn = XLogAssignLSN(); + + lastlsn = currlsn; + return currlsn; + } +} + /* * Assemble a WAL record from the registered data and buffers into an * XLogRecData chain, ready for insertion with XLogInsertRecord(). @@ -547,7 +620,8 @@ XLogInsert(RmgrId rmid, uint8 info) static XLogRecData * XLogRecordAssemble(RmgrId rmid, uint8 info, XLogRecPtr RedoRecPtr, bool doPageWrites, - XLogRecPtr *fpw_lsn, int *num_fpi, bool *topxid_included) + XLogRecPtr *fpw_lsn, int *num_fpi, uint64 *fpi_bytes, + bool *topxid_included) { XLogRecData *rdt; uint64 total_len = 0; @@ -620,7 +694,7 @@ XLogRecordAssemble(RmgrId rmid, uint8 info, needs_backup = (page_lsn <= RedoRecPtr); if (!needs_backup) { - if (*fpw_lsn == InvalidXLogRecPtr || page_lsn < *fpw_lsn) + if (!XLogRecPtrIsValid(*fpw_lsn) || page_lsn < *fpw_lsn) *fpw_lsn = page_lsn; } } @@ -658,8 +732,8 @@ XLogRecordAssemble(RmgrId rmid, uint8 info, if (regbuf->flags & REGBUF_STANDARD) { /* Assume we can omit data between pd_lower and pd_upper */ - uint16 lower = ((PageHeader) page)->pd_lower; - uint16 upper = ((PageHeader) page)->pd_upper; + uint16 lower = ((const PageHeaderData *) page)->pd_lower; + uint16 upper = ((const PageHeaderData *) page)->pd_upper; if (lower >= SizeOfPageHeaderData && upper > lower && @@ -783,6 +857,9 @@ XLogRecordAssemble(RmgrId rmid, uint8 info, } total_len += bimg.length; + + /* Track the WAL full page images in bytes */ + *fpi_bytes += bimg.length; } if (needs_data) @@ -839,11 +916,11 @@ XLogRecordAssemble(RmgrId rmid, uint8 info, /* followed by the record's origin, if any */ if ((curinsert_flags & XLOG_INCLUDE_ORIGIN) && - replorigin_session_origin != InvalidRepOriginId) + replorigin_xact_state.origin != InvalidReplOriginId) { *(scratch++) = (char) XLR_BLOCK_ID_ORIGIN; - memcpy(scratch, &replorigin_session_origin, sizeof(replorigin_session_origin)); - scratch += sizeof(replorigin_session_origin); + memcpy(scratch, &replorigin_xact_state.origin, sizeof(replorigin_xact_state.origin)); + scratch += sizeof(replorigin_xact_state.origin); } /* followed by toplevel XID, if not already included in previous record */ @@ -1044,22 +1121,14 @@ XLogCheckBufferNeedsBackup(Buffer buffer) * Write a backup block if needed when we are setting a hint. Note that * this may be called for a variety of page types, not just heaps. * - * Callable while holding just share lock on the buffer content. - * - * We can't use the plain backup block mechanism since that relies on the - * Buffer being exclusively locked. Since some modifications (setting LSN, hint - * bits) are allowed in a sharelocked buffer that can lead to wal checksum - * failures. So instead we copy the page and insert the copied data as normal - * record data. + * Callable while holding just a share-exclusive lock on the buffer + * content. That suffices to prevent concurrent modifications of the + * buffer. The buffer already needs to have been marked dirty by + * MarkBufferDirtyHint(). * * We only need to do something if page has not yet been full page written in * this checkpoint round. The LSN of the inserted wal record is returned if we * had to write, InvalidXLogRecPtr otherwise. - * - * It is possible that multiple concurrent backends could attempt to write WAL - * records. In that case, multiple copies of the same block would be recorded - * in separate WAL records by different backends, though that is still OK from - * a correctness perspective. */ XLogRecPtr XLogSaveBufferForHint(Buffer buffer, bool buffer_std) @@ -1068,58 +1137,37 @@ XLogSaveBufferForHint(Buffer buffer, bool buffer_std) XLogRecPtr lsn; XLogRecPtr RedoRecPtr; - /* - * Ensure no checkpoint can change our view of RedoRecPtr. - */ - Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) != 0); + /* this also verifies that we hold an appropriate lock */ + Assert(BufferIsDirty(buffer)); /* - * Update RedoRecPtr so that we can make the right decision + * Update RedoRecPtr so that we can make the right decision. It's possible + * that a new checkpoint will start just after GetRedoRecPtr(), but that + * is ok, as the buffer is already dirty, ensuring that any BufferSync() + * started after the buffer was marked dirty cannot complete without + * flushing this buffer. If a checkpoint started between marking the + * buffer dirty and this check, we will emit an unnecessary WAL record (as + * the buffer will be written out as part of the checkpoint), but the + * window for that is not big. */ RedoRecPtr = GetRedoRecPtr(); /* * We assume page LSN is first data on *every* page that can be passed to - * XLogInsert, whether it has the standard page layout or not. Since we're - * only holding a share-lock on the page, we must take the buffer header - * lock when we look at the LSN. + * XLogInsert, whether it has the standard page layout or not. */ - lsn = BufferGetLSNAtomic(buffer); + lsn = PageGetLSN(BufferGetPage(buffer)); if (lsn <= RedoRecPtr) { int flags = 0; - PGAlignedBlock copied_buffer; - char *origdata = (char *) BufferGetBlock(buffer); - RelFileLocator rlocator; - ForkNumber forkno; - BlockNumber blkno; - - /* - * Copy buffer so we don't have to worry about concurrent hint bit or - * lsn updates. We assume pd_lower/upper cannot be changed without an - * exclusive lock, so the contents bkp are not racy. - */ - if (buffer_std) - { - /* Assume we can omit data between pd_lower and pd_upper */ - Page page = BufferGetPage(buffer); - uint16 lower = ((PageHeader) page)->pd_lower; - uint16 upper = ((PageHeader) page)->pd_upper; - - memcpy(copied_buffer.data, origdata, lower); - memcpy(copied_buffer.data + upper, origdata + upper, BLCKSZ - upper); - } - else - memcpy(copied_buffer.data, origdata, BLCKSZ); XLogBeginInsert(); if (buffer_std) flags |= REGBUF_STANDARD; - BufferGetTag(buffer, &rlocator, &forkno, &blkno); - XLogRegisterBlock(0, &rlocator, forkno, blkno, copied_buffer.data, flags); + XLogRegisterBuffer(0, buffer, flags); recptr = XLogInsert(RM_XLOG_ID, XLOG_FPI_FOR_HINT); } @@ -1333,11 +1381,12 @@ log_newpage_range(Relation rel, ForkNumber forknum, recptr = XLogInsert(RM_XLOG_ID, XLOG_FPI); for (i = 0; i < nbufs; i++) - { PageSetLSN(BufferGetPage(bufpack[i]), recptr); - UnlockReleaseBuffer(bufpack[i]); - } + END_CRIT_SECTION(); + + for (i = 0; i < nbufs; i++) + UnlockReleaseBuffer(bufpack[i]); } } diff --git a/src/backend/access/transam/xlogprefetcher.c b/src/backend/access/transam/xlogprefetcher.c index 7735562db01d1..83a3f97a57c8c 100644 --- a/src/backend/access/transam/xlogprefetcher.c +++ b/src/backend/access/transam/xlogprefetcher.c @@ -3,7 +3,7 @@ * xlogprefetcher.c * Prefetching support for recovery. * - * Portions Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2022-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -36,12 +36,15 @@ #include "miscadmin.h" #include "port/atomics.h" #include "storage/bufmgr.h" +#include "storage/fd.h" #include "storage/shmem.h" #include "storage/smgr.h" +#include "storage/subsystems.h" #include "utils/fmgrprotos.h" #include "utils/guc_hooks.h" #include "utils/hsearch.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" /* * Every time we process this much WAL, we'll update the values in @@ -198,6 +201,14 @@ static LsnReadQueueNextStatus XLogPrefetcherNextBlock(uintptr_t pgsr_private, static XLogPrefetchStats *SharedStats; +static void XLogPrefetchShmemRequest(void *arg); +static void XLogPrefetchShmemInit(void *arg); + +const ShmemCallbacks XLogPrefetchShmemCallbacks = { + .request_fn = XLogPrefetchShmemRequest, + .init_fn = XLogPrefetchShmemInit, +}; + static inline LsnReadQueue * lrq_alloc(uint32 max_distance, uint32 max_inflight, @@ -290,10 +301,25 @@ lrq_complete_lsn(LsnReadQueue *lrq, XLogRecPtr lsn) lrq_prefetch(lrq); } -size_t -XLogPrefetchShmemSize(void) +static void +XLogPrefetchShmemRequest(void *arg) +{ + ShmemRequestStruct(.name = "XLogPrefetchStats", + .size = sizeof(XLogPrefetchStats), + .ptr = (void **) &SharedStats, + ); +} + +static void +XLogPrefetchShmemInit(void *arg) { - return sizeof(XLogPrefetchStats); + pg_atomic_init_u64(&SharedStats->reset_time, GetCurrentTimestamp()); + pg_atomic_init_u64(&SharedStats->prefetch, 0); + pg_atomic_init_u64(&SharedStats->hit, 0); + pg_atomic_init_u64(&SharedStats->skip_init, 0); + pg_atomic_init_u64(&SharedStats->skip_new, 0); + pg_atomic_init_u64(&SharedStats->skip_fpw, 0); + pg_atomic_init_u64(&SharedStats->skip_rep, 0); } /* @@ -311,27 +337,6 @@ XLogPrefetchResetStats(void) pg_atomic_write_u64(&SharedStats->skip_rep, 0); } -void -XLogPrefetchShmemInit(void) -{ - bool found; - - SharedStats = (XLogPrefetchStats *) - ShmemInitStruct("XLogPrefetchStats", - sizeof(XLogPrefetchStats), - &found); - - if (!found) - { - pg_atomic_init_u64(&SharedStats->reset_time, GetCurrentTimestamp()); - pg_atomic_init_u64(&SharedStats->prefetch, 0); - pg_atomic_init_u64(&SharedStats->hit, 0); - pg_atomic_init_u64(&SharedStats->skip_init, 0); - pg_atomic_init_u64(&SharedStats->skip_new, 0); - pg_atomic_init_u64(&SharedStats->skip_fpw, 0); - pg_atomic_init_u64(&SharedStats->skip_rep, 0); - } -} /* * Called when any GUC is changed that affects prefetching. @@ -364,7 +369,7 @@ XLogPrefetcherAllocate(XLogReaderState *reader) XLogPrefetcher *prefetcher; HASHCTL ctl; - prefetcher = palloc0(sizeof(XLogPrefetcher)); + prefetcher = palloc0_object(XLogPrefetcher); prefetcher->reader = reader; ctl.keysize = sizeof(RelFileLocator); @@ -546,7 +551,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "suppressing all readahead until %X/%X is replayed due to possible TLI change", + "suppressing all readahead until %X/%08X is replayed due to possible TLI change", LSN_FORMAT_ARGS(record->lsn)); #endif @@ -579,7 +584,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "suppressing prefetch in database %u until %X/%X is replayed due to raw file copy", + "suppressing prefetch in database %u until %X/%08X is replayed due to raw file copy", rlocator.dbOid, LSN_FORMAT_ARGS(record->lsn)); #endif @@ -607,7 +612,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "suppressing prefetch in relation %u/%u/%u until %X/%X is replayed, which creates the relation", + "suppressing prefetch in relation %u/%u/%u until %X/%08X is replayed, which creates the relation", xlrec->rlocator.spcOid, xlrec->rlocator.dbOid, xlrec->rlocator.relNumber, @@ -630,7 +635,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "suppressing prefetch in relation %u/%u/%u from block %u until %X/%X is replayed, which truncates the relation", + "suppressing prefetch in relation %u/%u/%u from block %u until %X/%08X is replayed, which truncates the relation", xlrec->rlocator.spcOid, xlrec->rlocator.dbOid, xlrec->rlocator.relNumber, @@ -729,7 +734,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) { #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "suppressing all prefetch in relation %u/%u/%u until %X/%X is replayed, because the relation does not exist on disk", + "suppressing all prefetch in relation %u/%u/%u until %X/%08X is replayed, because the relation does not exist on disk", reln->smgr_rlocator.locator.spcOid, reln->smgr_rlocator.locator.dbOid, reln->smgr_rlocator.locator.relNumber, @@ -750,7 +755,7 @@ XLogPrefetcherNextBlock(uintptr_t pgsr_private, XLogRecPtr *lsn) { #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "suppressing prefetch in relation %u/%u/%u from block %u until %X/%X is replayed, because the relation is too small", + "suppressing prefetch in relation %u/%u/%u from block %u until %X/%08X is replayed, because the relation is too small", reln->smgr_rlocator.locator.spcOid, reln->smgr_rlocator.locator.dbOid, reln->smgr_rlocator.locator.relNumber, @@ -928,7 +933,7 @@ XLogPrefetcherIsFiltered(XLogPrefetcher *prefetcher, RelFileLocator rlocator, { #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "prefetch of %u/%u/%u block %u suppressed; filtering until LSN %X/%X is replayed (blocks >= %u filtered)", + "prefetch of %u/%u/%u block %u suppressed; filtering until LSN %X/%08X is replayed (blocks >= %u filtered)", rlocator.spcOid, rlocator.dbOid, rlocator.relNumber, blockno, LSN_FORMAT_ARGS(filter->filter_until_replayed), filter->filter_from_block); @@ -944,7 +949,7 @@ XLogPrefetcherIsFiltered(XLogPrefetcher *prefetcher, RelFileLocator rlocator, { #ifdef XLOGPREFETCHER_DEBUG_LEVEL elog(XLOGPREFETCHER_DEBUG_LEVEL, - "prefetch of %u/%u/%u block %u suppressed; filtering until LSN %X/%X is replayed (whole database)", + "prefetch of %u/%u/%u block %u suppressed; filtering until LSN %X/%08X is replayed (whole database)", rlocator.spcOid, rlocator.dbOid, rlocator.relNumber, blockno, LSN_FORMAT_ARGS(filter->filter_until_replayed)); #endif @@ -967,7 +972,7 @@ XLogPrefetcherBeginRead(XLogPrefetcher *prefetcher, XLogRecPtr recPtr) /* Book-keeping to avoid readahead on first read. */ prefetcher->begin_ptr = recPtr; - prefetcher->no_readahead_until = 0; + prefetcher->no_readahead_until = InvalidXLogRecPtr; /* This will forget about any queued up records in the decoder. */ XLogBeginRead(prefetcher->reader, recPtr); diff --git a/src/backend/access/transam/xlogreader.c b/src/backend/access/transam/xlogreader.c index 2790ade1f91e8..8849610db005d 100644 --- a/src/backend/access/transam/xlogreader.c +++ b/src/backend/access/transam/xlogreader.c @@ -3,7 +3,7 @@ * xlogreader.c * Generic XLog reading facility * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/access/transam/xlogreader.c @@ -36,6 +36,7 @@ #ifndef FRONTEND #include "pgstat.h" #include "storage/bufmgr.h" +#include "utils/wait_event.h" #else #include "common/logging.h" #endif @@ -231,7 +232,7 @@ WALOpenSegmentInit(WALOpenSegment *seg, WALSegmentContext *segcxt, void XLogBeginRead(XLogReaderState *state, XLogRecPtr RecPtr) { - Assert(!XLogRecPtrIsInvalid(RecPtr)); + Assert(XLogRecPtrIsValid(RecPtr)); ResetDecoder(state); @@ -343,7 +344,7 @@ XLogNextRecord(XLogReaderState *state, char **errormsg) * XLogBeginRead() or XLogNextRecord(), and is the location of the * error. */ - Assert(!XLogRecPtrIsInvalid(state->EndRecPtr)); + Assert(XLogRecPtrIsValid(state->EndRecPtr)); return NULL; } @@ -558,7 +559,7 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) RecPtr = state->NextRecPtr; - if (state->DecodeRecPtr != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(state->DecodeRecPtr)) { /* read the record after the one we just read */ @@ -617,7 +618,7 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) } else if (targetRecOff < pageHeaderSize) { - report_invalid_record(state, "invalid record offset at %X/%X: expected at least %u, got %u", + report_invalid_record(state, "invalid record offset at %X/%08X: expected at least %u, got %u", LSN_FORMAT_ARGS(RecPtr), pageHeaderSize, targetRecOff); goto err; @@ -626,7 +627,7 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) if ((((XLogPageHeader) state->readBuf)->xlp_info & XLP_FIRST_IS_CONTRECORD) && targetRecOff == pageHeaderSize) { - report_invalid_record(state, "contrecord is requested by %X/%X", + report_invalid_record(state, "contrecord is requested by %X/%08X", LSN_FORMAT_ARGS(RecPtr)); goto err; } @@ -667,7 +668,7 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) if (total_len < SizeOfXLogRecord) { report_invalid_record(state, - "invalid record length at %X/%X: expected at least %u, got %u", + "invalid record length at %X/%08X: expected at least %u, got %u", LSN_FORMAT_ARGS(RecPtr), (uint32) SizeOfXLogRecord, total_len); goto err; @@ -723,11 +724,12 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) /* Calculate pointer to beginning of next page */ targetPagePtr += XLOG_BLCKSZ; - /* Wait for the next page to become available */ - readOff = ReadPageInternal(state, targetPagePtr, - Min(total_len - gotlen + SizeOfXLogShortPHD, - XLOG_BLCKSZ)); - + /* + * Read the page header before processing the record data, so we + * can handle the case where the previous record ended as being a + * partial one. + */ + readOff = ReadPageInternal(state, targetPagePtr, SizeOfXLogShortPHD); if (readOff == XLREAD_WOULDBLOCK) return XLREAD_WOULDBLOCK; else if (readOff < 0) @@ -756,7 +758,7 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) if (!(pageHeader->xlp_info & XLP_FIRST_IS_CONTRECORD)) { report_invalid_record(state, - "there is no contrecord flag at %X/%X", + "there is no contrecord flag at %X/%08X", LSN_FORMAT_ARGS(RecPtr)); goto err; } @@ -769,13 +771,22 @@ XLogDecodeNextRecord(XLogReaderState *state, bool nonblocking) total_len != (pageHeader->xlp_rem_len + gotlen)) { report_invalid_record(state, - "invalid contrecord length %u (expected %lld) at %X/%X", + "invalid contrecord length %u (expected %lld) at %X/%08X", pageHeader->xlp_rem_len, ((long long) total_len) - gotlen, LSN_FORMAT_ARGS(RecPtr)); goto err; } + /* Wait for the next page to become available */ + readOff = ReadPageInternal(state, targetPagePtr, + Min(total_len - gotlen + SizeOfXLogShortPHD, + XLOG_BLCKSZ)); + if (readOff == XLREAD_WOULDBLOCK) + return XLREAD_WOULDBLOCK; + else if (readOff < 0) + goto err; + /* Append the continuation from this page to the buffer */ pageHeaderSize = XLogPageHeaderSize(pageHeader); @@ -1132,7 +1143,7 @@ ValidXLogRecordHeader(XLogReaderState *state, XLogRecPtr RecPtr, if (record->xl_tot_len < SizeOfXLogRecord) { report_invalid_record(state, - "invalid record length at %X/%X: expected at least %u, got %u", + "invalid record length at %X/%08X: expected at least %u, got %u", LSN_FORMAT_ARGS(RecPtr), (uint32) SizeOfXLogRecord, record->xl_tot_len); return false; @@ -1140,7 +1151,7 @@ ValidXLogRecordHeader(XLogReaderState *state, XLogRecPtr RecPtr, if (!RmgrIdIsValid(record->xl_rmid)) { report_invalid_record(state, - "invalid resource manager ID %u at %X/%X", + "invalid resource manager ID %u at %X/%08X", record->xl_rmid, LSN_FORMAT_ARGS(RecPtr)); return false; } @@ -1153,7 +1164,7 @@ ValidXLogRecordHeader(XLogReaderState *state, XLogRecPtr RecPtr, if (!(record->xl_prev < RecPtr)) { report_invalid_record(state, - "record with incorrect prev-link %X/%X at %X/%X", + "record with incorrect prev-link %X/%08X at %X/%08X", LSN_FORMAT_ARGS(record->xl_prev), LSN_FORMAT_ARGS(RecPtr)); return false; @@ -1169,7 +1180,7 @@ ValidXLogRecordHeader(XLogReaderState *state, XLogRecPtr RecPtr, if (record->xl_prev != PrevRecPtr) { report_invalid_record(state, - "record with incorrect prev-link %X/%X at %X/%X", + "record with incorrect prev-link %X/%08X at %X/%08X", LSN_FORMAT_ARGS(record->xl_prev), LSN_FORMAT_ARGS(RecPtr)); return false; @@ -1207,7 +1218,7 @@ ValidXLogRecord(XLogReaderState *state, XLogRecord *record, XLogRecPtr recptr) if (!EQ_CRC32C(record->xl_crc, crc)) { report_invalid_record(state, - "incorrect resource manager data checksum in record at %X/%X", + "incorrect resource manager data checksum in record at %X/%08X", LSN_FORMAT_ARGS(recptr)); return false; } @@ -1241,7 +1252,7 @@ XLogReaderValidatePageHeader(XLogReaderState *state, XLogRecPtr recptr, XLogFileName(fname, state->seg.ws_tli, segno, state->segcxt.ws_segsize); report_invalid_record(state, - "invalid magic number %04X in WAL segment %s, LSN %X/%X, offset %u", + "invalid magic number %04X in WAL segment %s, LSN %X/%08X, offset %u", hdr->xlp_magic, fname, LSN_FORMAT_ARGS(recptr), @@ -1256,7 +1267,7 @@ XLogReaderValidatePageHeader(XLogReaderState *state, XLogRecPtr recptr, XLogFileName(fname, state->seg.ws_tli, segno, state->segcxt.ws_segsize); report_invalid_record(state, - "invalid info bits %04X in WAL segment %s, LSN %X/%X, offset %u", + "invalid info bits %04X in WAL segment %s, LSN %X/%08X, offset %u", hdr->xlp_info, fname, LSN_FORMAT_ARGS(recptr), @@ -1298,7 +1309,7 @@ XLogReaderValidatePageHeader(XLogReaderState *state, XLogRecPtr recptr, /* hmm, first page of file doesn't have a long header? */ report_invalid_record(state, - "invalid info bits %04X in WAL segment %s, LSN %X/%X, offset %u", + "invalid info bits %04X in WAL segment %s, LSN %X/%08X, offset %u", hdr->xlp_info, fname, LSN_FORMAT_ARGS(recptr), @@ -1318,7 +1329,7 @@ XLogReaderValidatePageHeader(XLogReaderState *state, XLogRecPtr recptr, XLogFileName(fname, state->seg.ws_tli, segno, state->segcxt.ws_segsize); report_invalid_record(state, - "unexpected pageaddr %X/%X in WAL segment %s, LSN %X/%X, offset %u", + "unexpected pageaddr %X/%08X in WAL segment %s, LSN %X/%08X, offset %u", LSN_FORMAT_ARGS(hdr->xlp_pageaddr), fname, LSN_FORMAT_ARGS(recptr), @@ -1344,7 +1355,7 @@ XLogReaderValidatePageHeader(XLogReaderState *state, XLogRecPtr recptr, XLogFileName(fname, state->seg.ws_tli, segno, state->segcxt.ws_segsize); report_invalid_record(state, - "out-of-sequence timeline ID %u (after %u) in WAL segment %s, LSN %X/%X, offset %u", + "out-of-sequence timeline ID %u (after %u) in WAL segment %s, LSN %X/%08X, offset %u", hdr->xlp_tli, state->latestPageTLI, fname, @@ -1379,16 +1390,23 @@ XLogReaderResetError(XLogReaderState *state) * * This positions the reader, like XLogBeginRead(), so that the next call to * XLogReadRecord() will read the next valid record. + * + * On failure, InvalidXLogRecPtr is returned, and *errormsg is set to a string + * with details of the failure. + * + * When set, *errormsg points to an internal buffer that's valid until the next + * call to XLogReadRecord. */ XLogRecPtr -XLogFindNextRecord(XLogReaderState *state, XLogRecPtr RecPtr) +XLogFindNextRecord(XLogReaderState *state, XLogRecPtr RecPtr, char **errormsg) { XLogRecPtr tmpRecPtr; XLogRecPtr found = InvalidXLogRecPtr; XLogPageHeader header; - char *errormsg; - Assert(!XLogRecPtrIsInvalid(RecPtr)); + *errormsg = NULL; + + Assert(XLogRecPtrIsValid(RecPtr)); /* Make sure ReadPageInternal() can't return XLREAD_WOULDBLOCK. */ state->nonblocking = false; @@ -1471,7 +1489,7 @@ XLogFindNextRecord(XLogReaderState *state, XLogRecPtr RecPtr) * or we just jumped over the remaining data of a continuation. */ XLogBeginRead(state, tmpRecPtr); - while (XLogReadRecord(state, &errormsg) != NULL) + while (XLogReadRecord(state, errormsg) != NULL) { /* past the record we've found, break out */ if (RecPtr <= state->ReadRecPtr) @@ -1486,6 +1504,17 @@ XLogFindNextRecord(XLogReaderState *state, XLogRecPtr RecPtr) err: XLogReaderInvalReadState(state); + /* + * We may have reported errors due to invalid WAL header, propagate the + * error message to the caller. + */ + if (state->errormsg_deferred) + { + if (state->errormsg_buf[0] != '\0') + *errormsg = state->errormsg_buf; + state->errormsg_deferred = false; + } + return InvalidXLogRecPtr; } @@ -1564,7 +1593,7 @@ WALRead(XLogReaderState *state, /* Reset errno first; eases reporting non-errno-affecting errors */ errno = 0; - readbytes = pg_pread(state->seg.ws_file, p, segbytes, (off_t) startoff); + readbytes = pg_pread(state->seg.ws_file, p, segbytes, (pgoff_t) startoff); #ifndef FRONTEND pgstat_report_wait_end(); @@ -1697,7 +1726,7 @@ DecodeXLogRecord(XLogReaderState *state, decoded->header = *record; decoded->lsn = lsn; decoded->next = NULL; - decoded->record_origin = InvalidRepOriginId; + decoded->record_origin = InvalidReplOriginId; decoded->toplevel_xid = InvalidTransactionId; decoded->main_data = NULL; decoded->main_data_len = 0; @@ -1737,7 +1766,7 @@ DecodeXLogRecord(XLogReaderState *state, } else if (block_id == XLR_BLOCK_ID_ORIGIN) { - COPY_HEADER_FIELD(&decoded->record_origin, sizeof(RepOriginId)); + COPY_HEADER_FIELD(&decoded->record_origin, sizeof(ReplOriginId)); } else if (block_id == XLR_BLOCK_ID_TOPLEVEL_XID) { @@ -1756,7 +1785,7 @@ DecodeXLogRecord(XLogReaderState *state, if (block_id <= decoded->max_block_id) { report_invalid_record(state, - "out-of-order block_id %u at %X/%X", + "out-of-order block_id %u at %X/%08X", block_id, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; @@ -1780,15 +1809,15 @@ DecodeXLogRecord(XLogReaderState *state, if (blk->has_data && blk->data_len == 0) { report_invalid_record(state, - "BKPBLOCK_HAS_DATA set, but no data included at %X/%X", + "BKPBLOCK_HAS_DATA set, but no data included at %X/%08X", LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } if (!blk->has_data && blk->data_len != 0) { report_invalid_record(state, - "BKPBLOCK_HAS_DATA not set, but data length is %u at %X/%X", - (unsigned int) blk->data_len, + "BKPBLOCK_HAS_DATA not set, but data length is %d at %X/%08X", + blk->data_len, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1823,10 +1852,10 @@ DecodeXLogRecord(XLogReaderState *state, blk->bimg_len == BLCKSZ)) { report_invalid_record(state, - "BKPIMAGE_HAS_HOLE set, but hole offset %u length %u block image length %u at %X/%X", - (unsigned int) blk->hole_offset, - (unsigned int) blk->hole_length, - (unsigned int) blk->bimg_len, + "BKPIMAGE_HAS_HOLE set, but hole offset %d length %d block image length %d at %X/%08X", + blk->hole_offset, + blk->hole_length, + blk->bimg_len, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1839,9 +1868,9 @@ DecodeXLogRecord(XLogReaderState *state, (blk->hole_offset != 0 || blk->hole_length != 0)) { report_invalid_record(state, - "BKPIMAGE_HAS_HOLE not set, but hole offset %u length %u at %X/%X", - (unsigned int) blk->hole_offset, - (unsigned int) blk->hole_length, + "BKPIMAGE_HAS_HOLE not set, but hole offset %d length %d at %X/%08X", + blk->hole_offset, + blk->hole_length, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1853,8 +1882,8 @@ DecodeXLogRecord(XLogReaderState *state, blk->bimg_len == BLCKSZ) { report_invalid_record(state, - "BKPIMAGE_COMPRESSED set, but block image length %u at %X/%X", - (unsigned int) blk->bimg_len, + "BKPIMAGE_COMPRESSED set, but block image length %d at %X/%08X", + blk->bimg_len, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1868,8 +1897,8 @@ DecodeXLogRecord(XLogReaderState *state, blk->bimg_len != BLCKSZ) { report_invalid_record(state, - "neither BKPIMAGE_HAS_HOLE nor BKPIMAGE_COMPRESSED set, but block image length is %u at %X/%X", - (unsigned int) blk->data_len, + "neither BKPIMAGE_HAS_HOLE nor BKPIMAGE_COMPRESSED set, but block image length is %d at %X/%08X", + blk->data_len, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1884,7 +1913,7 @@ DecodeXLogRecord(XLogReaderState *state, if (rlocator == NULL) { report_invalid_record(state, - "BKPBLOCK_SAME_REL set but no previous rel at %X/%X", + "BKPBLOCK_SAME_REL set but no previous rel at %X/%08X", LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1896,7 +1925,7 @@ DecodeXLogRecord(XLogReaderState *state, else { report_invalid_record(state, - "invalid block_id %u at %X/%X", + "invalid block_id %u at %X/%08X", block_id, LSN_FORMAT_ARGS(state->ReadRecPtr)); goto err; } @@ -1963,7 +1992,7 @@ DecodeXLogRecord(XLogReaderState *state, shortdata_err: report_invalid_record(state, - "record with invalid length at %X/%X", + "record with invalid length at %X/%08X", LSN_FORMAT_ARGS(state->ReadRecPtr)); err: *errormsg = state->errormsg_buf; @@ -2073,14 +2102,14 @@ RestoreBlockImage(XLogReaderState *record, uint8 block_id, char *page) !record->record->blocks[block_id].in_use) { report_invalid_record(record, - "could not restore image at %X/%X with invalid block %d specified", + "could not restore image at %X/%08X with invalid block %d specified", LSN_FORMAT_ARGS(record->ReadRecPtr), block_id); return false; } if (!record->record->blocks[block_id].has_image) { - report_invalid_record(record, "could not restore image at %X/%X with invalid state, block %d", + report_invalid_record(record, "could not restore image at %X/%08X with invalid state, block %d", LSN_FORMAT_ARGS(record->ReadRecPtr), block_id); return false; @@ -2107,7 +2136,7 @@ RestoreBlockImage(XLogReaderState *record, uint8 block_id, char *page) bkpb->bimg_len, BLCKSZ - bkpb->hole_length) <= 0) decomp_success = false; #else - report_invalid_record(record, "could not restore image at %X/%X compressed with %s not supported by build, block %d", + report_invalid_record(record, "could not restore image at %X/%08X compressed with %s not supported by build, block %d", LSN_FORMAT_ARGS(record->ReadRecPtr), "LZ4", block_id); @@ -2124,7 +2153,7 @@ RestoreBlockImage(XLogReaderState *record, uint8 block_id, char *page) if (ZSTD_isError(decomp_result)) decomp_success = false; #else - report_invalid_record(record, "could not restore image at %X/%X compressed with %s not supported by build, block %d", + report_invalid_record(record, "could not restore image at %X/%08X compressed with %s not supported by build, block %d", LSN_FORMAT_ARGS(record->ReadRecPtr), "zstd", block_id); @@ -2133,7 +2162,7 @@ RestoreBlockImage(XLogReaderState *record, uint8 block_id, char *page) } else { - report_invalid_record(record, "could not restore image at %X/%X compressed with unknown method, block %d", + report_invalid_record(record, "could not restore image at %X/%08X compressed with unknown method, block %d", LSN_FORMAT_ARGS(record->ReadRecPtr), block_id); return false; @@ -2141,7 +2170,7 @@ RestoreBlockImage(XLogReaderState *record, uint8 block_id, char *page) if (!decomp_success) { - report_invalid_record(record, "could not decompress image at %X/%X, block %d", + report_invalid_record(record, "could not decompress image at %X/%08X, block %d", LSN_FORMAT_ARGS(record->ReadRecPtr), block_id); return false; diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 6ce979f2d8bc4..c236e2b796928 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -14,7 +14,7 @@ * for interrogating recovery state and controlling the recovery process. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/xlogrecovery.c @@ -25,7 +25,6 @@ #include "postgres.h" #include -#include #include #include #include @@ -40,11 +39,13 @@ #include "access/xlogreader.h" #include "access/xlogrecovery.h" #include "access/xlogutils.h" +#include "access/xlogwait.h" #include "backup/basebackup.h" #include "catalog/pg_control.h" #include "commands/tablespace.h" #include "common/file_utils.h" #include "miscadmin.h" +#include "nodes/miscnodes.h" #include "pgstat.h" #include "postmaster/bgwriter.h" #include "postmaster/startup.h" @@ -57,6 +58,7 @@ #include "storage/pmsignal.h" #include "storage/procarray.h" #include "storage/spin.h" +#include "storage/subsystems.h" #include "utils/datetime.h" #include "utils/fmgrprotos.h" #include "utils/guc_hooks.h" @@ -64,6 +66,7 @@ #include "utils/pg_lsn.h" #include "utils/ps_status.h" #include "utils/pg_rusage.h" +#include "utils/wait_event.h" /* Unsupported old recovery command file names (relative to $PGDATA) */ #define RECOVERY_COMMAND_FILE "recovery.conf" @@ -260,7 +263,7 @@ static TimestampTz XLogReceiptTime = 0; static XLogSource XLogReceiptSource = XLOG_FROM_ANY; /* Local copy of WalRcv->flushedUpto */ -static XLogRecPtr flushedUpto = 0; +static XLogRecPtr flushedUpto = InvalidXLogRecPtr; static TimeLineID receiveTLI = 0; /* @@ -303,71 +306,15 @@ bool reachedConsistency = false; static char *replay_image_masked = NULL; static char *primary_image_masked = NULL; +XLogRecoveryCtlData *XLogRecoveryCtl = NULL; -/* - * Shared-memory state for WAL recovery. - */ -typedef struct XLogRecoveryCtlData -{ - /* - * SharedHotStandbyActive indicates if we allow hot standby queries to be - * run. Protected by info_lck. - */ - bool SharedHotStandbyActive; - - /* - * SharedPromoteIsTriggered indicates if a standby promotion has been - * triggered. Protected by info_lck. - */ - bool SharedPromoteIsTriggered; - - /* - * recoveryWakeupLatch is used to wake up the startup process to continue - * WAL replay, if it is waiting for WAL to arrive or promotion to be - * requested. - * - * Note that the startup process also uses another latch, its procLatch, - * to wait for recovery conflict. If we get rid of recoveryWakeupLatch for - * signaling the startup process in favor of using its procLatch, which - * comports better with possible generic signal handlers using that latch. - * But we should not do that because the startup process doesn't assume - * that it's waken up by walreceiver process or SIGHUP signal handler - * while it's waiting for recovery conflict. The separate latches, - * recoveryWakeupLatch and procLatch, should be used for inter-process - * communication for WAL replay and recovery conflict, respectively. - */ - Latch recoveryWakeupLatch; - - /* - * Last record successfully replayed. - */ - XLogRecPtr lastReplayedReadRecPtr; /* start position */ - XLogRecPtr lastReplayedEndRecPtr; /* end+1 position */ - TimeLineID lastReplayedTLI; /* timeline */ +static void XLogRecoveryShmemRequest(void *arg); +static void XLogRecoveryShmemInit(void *arg); - /* - * When we're currently replaying a record, ie. in a redo function, - * replayEndRecPtr points to the end+1 of the record being replayed, - * otherwise it's equal to lastReplayedEndRecPtr. - */ - XLogRecPtr replayEndRecPtr; - TimeLineID replayEndTLI; - /* timestamp of last COMMIT/ABORT record replayed (or being replayed) */ - TimestampTz recoveryLastXTime; - - /* - * timestamp of when we started replaying the current chunk of WAL data, - * only relevant for replication or archive recovery - */ - TimestampTz currentChunkStartTime; - /* Recovery pause state */ - RecoveryPauseState recoveryPauseState; - ConditionVariable recoveryNotPausedCV; - - slock_t info_lck; /* locks shared variables shown above */ -} XLogRecoveryCtlData; - -static XLogRecoveryCtlData *XLogRecoveryCtl = NULL; +const ShmemCallbacks XLogRecoveryShmemCallbacks = { + .request_fn = XLogRecoveryShmemRequest, + .init_fn = XLogRecoveryShmemInit, +}; /* * abortedRecPtr is the start pointer of a broken record at end of WAL when @@ -447,28 +394,20 @@ static void SetCurrentChunkStartTime(TimestampTz xtime); static void SetLatestXTime(TimestampTz xtime); /* - * Initialization of shared memory for WAL recovery + * Register shared memory for WAL recovery */ -Size -XLogRecoveryShmemSize(void) +static void +XLogRecoveryShmemRequest(void *arg) { - Size size; - - /* XLogRecoveryCtl */ - size = sizeof(XLogRecoveryCtlData); - - return size; + ShmemRequestStruct(.name = "XLOG Recovery Ctl", + .size = sizeof(XLogRecoveryCtlData), + .ptr = (void **) &XLogRecoveryCtl, + ); } -void -XLogRecoveryShmemInit(void) +static void +XLogRecoveryShmemInit(void *arg) { - bool found; - - XLogRecoveryCtl = (XLogRecoveryCtlData *) - ShmemInitStruct("XLOG Recovery Ctl", XLogRecoveryShmemSize(), &found); - if (found) - return; memset(XLogRecoveryCtl, 0, sizeof(XLogRecoveryCtlData)); SpinLockInit(&XLogRecoveryCtl->info_lck); @@ -557,7 +496,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, * Set the WAL reading processor now, as it will be needed when reading * the checkpoint record required (backup_label or not). */ - private = palloc0(sizeof(XLogPageReadPrivate)); + private = palloc0_object(XLogPageReadPrivate); xlogreader = XLogReaderAllocate(wal_segment_size, NULL, XL_ROUTINE(.page_read = &XLogPageRead, @@ -620,10 +559,10 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, * than ControlFile->checkPoint is used. */ ereport(LOG, - (errmsg("starting backup recovery with redo LSN %X/%X, checkpoint LSN %X/%X, on timeline ID %u", - LSN_FORMAT_ARGS(RedoStartLSN), - LSN_FORMAT_ARGS(CheckPointLoc), - CheckPointTLI))); + errmsg("starting backup recovery with redo LSN %X/%08X, checkpoint LSN %X/%08X, on timeline ID %u", + LSN_FORMAT_ARGS(RedoStartLSN), + LSN_FORMAT_ARGS(CheckPointLoc), + CheckPointTLI)); /* * When a backup_label file is present, we want to roll forward from @@ -636,8 +575,8 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, memcpy(&checkPoint, XLogRecGetData(xlogreader), sizeof(CheckPoint)); wasShutdown = ((record->xl_info & ~XLR_INFO_MASK) == XLOG_CHECKPOINT_SHUTDOWN); ereport(DEBUG1, - (errmsg_internal("checkpoint record is at %X/%X", - LSN_FORMAT_ARGS(CheckPointLoc)))); + errmsg_internal("checkpoint record is at %X/%08X", + LSN_FORMAT_ARGS(CheckPointLoc))); InRecovery = true; /* force recovery even if SHUTDOWNED */ /* @@ -652,23 +591,23 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, if (!ReadRecord(xlogprefetcher, LOG, false, checkPoint.ThisTimeLineID)) ereport(FATAL, - (errmsg("could not find redo location %X/%X referenced by checkpoint record at %X/%X", - LSN_FORMAT_ARGS(checkPoint.redo), LSN_FORMAT_ARGS(CheckPointLoc)), - errhint("If you are restoring from a backup, touch \"%s/recovery.signal\" or \"%s/standby.signal\" and add required recovery options.\n" - "If you are not restoring from a backup, try removing the file \"%s/backup_label\".\n" - "Be careful: removing \"%s/backup_label\" will result in a corrupt cluster if restoring from a backup.", - DataDir, DataDir, DataDir, DataDir))); + errmsg("could not find redo location %X/%08X referenced by checkpoint record at %X/%08X", + LSN_FORMAT_ARGS(checkPoint.redo), LSN_FORMAT_ARGS(CheckPointLoc)), + errhint("If you are restoring from a backup, touch \"%s/recovery.signal\" or \"%s/standby.signal\" and add required recovery options.\n" + "If you are not restoring from a backup, try removing the file \"%s/backup_label\".\n" + "Be careful: removing \"%s/backup_label\" will result in a corrupt cluster if restoring from a backup.", + DataDir, DataDir, DataDir, DataDir)); } } else { ereport(FATAL, - (errmsg("could not locate required checkpoint record at %X/%X", - LSN_FORMAT_ARGS(CheckPointLoc)), - errhint("If you are restoring from a backup, touch \"%s/recovery.signal\" or \"%s/standby.signal\" and add required recovery options.\n" - "If you are not restoring from a backup, try removing the file \"%s/backup_label\".\n" - "Be careful: removing \"%s/backup_label\" will result in a corrupt cluster if restoring from a backup.", - DataDir, DataDir, DataDir, DataDir))); + errmsg("could not locate required checkpoint record at %X/%08X", + LSN_FORMAT_ARGS(CheckPointLoc)), + errhint("If you are restoring from a backup, touch \"%s/recovery.signal\" or \"%s/standby.signal\" and add required recovery options.\n" + "If you are not restoring from a backup, try removing the file \"%s/backup_label\".\n" + "Be careful: removing \"%s/backup_label\" will result in a corrupt cluster if restoring from a backup.", + DataDir, DataDir, DataDir, DataDir)); wasShutdown = false; /* keep compiler quiet */ } @@ -756,9 +695,9 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, * end-of-backup record), and we can enter archive recovery directly. */ if (ArchiveRecoveryRequested && - (ControlFile->minRecoveryPoint != InvalidXLogRecPtr || + (XLogRecPtrIsValid(ControlFile->minRecoveryPoint) || ControlFile->backupEndRequired || - ControlFile->backupEndPoint != InvalidXLogRecPtr || + XLogRecPtrIsValid(ControlFile->backupEndPoint) || ControlFile->state == DB_SHUTDOWNED)) { InArchiveRecovery = true; @@ -771,10 +710,10 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, * emit a log message when we continue initializing from a base * backup. */ - if (!XLogRecPtrIsInvalid(ControlFile->backupStartPoint)) + if (XLogRecPtrIsValid(ControlFile->backupStartPoint)) ereport(LOG, - (errmsg("restarting backup recovery with redo LSN %X/%X", - LSN_FORMAT_ARGS(ControlFile->backupStartPoint)))); + errmsg("restarting backup recovery with redo LSN %X/%08X", + LSN_FORMAT_ARGS(ControlFile->backupStartPoint))); /* Get the last valid checkpoint record. */ CheckPointLoc = ControlFile->checkPoint; @@ -786,8 +725,8 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, if (record != NULL) { ereport(DEBUG1, - (errmsg_internal("checkpoint record is at %X/%X", - LSN_FORMAT_ARGS(CheckPointLoc)))); + errmsg_internal("checkpoint record is at %X/%08X", + LSN_FORMAT_ARGS(CheckPointLoc))); } else { @@ -797,12 +736,22 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, * can't read the last checkpoint because this allows us to * simplify processing around checkpoints. */ - ereport(PANIC, - (errmsg("could not locate a valid checkpoint record at %X/%X", - LSN_FORMAT_ARGS(CheckPointLoc)))); + ereport(FATAL, + errmsg("could not locate a valid checkpoint record at %X/%08X", + LSN_FORMAT_ARGS(CheckPointLoc))); } memcpy(&checkPoint, XLogRecGetData(xlogreader), sizeof(CheckPoint)); wasShutdown = ((record->xl_info & ~XLR_INFO_MASK) == XLOG_CHECKPOINT_SHUTDOWN); + + /* Make sure that REDO location exists. */ + if (checkPoint.redo < CheckPointLoc) + { + XLogPrefetcherBeginRead(xlogprefetcher, checkPoint.redo); + if (!ReadRecord(xlogprefetcher, LOG, false, checkPoint.ThisTimeLineID)) + ereport(FATAL, + errmsg("could not find redo location %X/%08X referenced by checkpoint record at %X/%08X", + LSN_FORMAT_ARGS(checkPoint.redo), LSN_FORMAT_ARGS(CheckPointLoc))); + } } if (ArchiveRecoveryRequested) @@ -824,8 +773,8 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, recoveryTargetName))); else if (recoveryTarget == RECOVERY_TARGET_LSN) ereport(LOG, - (errmsg("starting point-in-time recovery to WAL location (LSN) \"%X/%X\"", - LSN_FORMAT_ARGS(recoveryTargetLSN)))); + errmsg("starting point-in-time recovery to WAL location (LSN) \"%X/%08X\"", + LSN_FORMAT_ARGS(recoveryTargetLSN))); else if (recoveryTarget == RECOVERY_TARGET_IMMEDIATE) ereport(LOG, (errmsg("starting point-in-time recovery to earliest consistent point"))); @@ -855,7 +804,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, (errmsg("requested timeline %u is not a child of this server's history", recoveryTargetTLI), /* translator: %s is a backup_label file or a pg_control file */ - errdetail("Latest checkpoint in file \"%s\" is at %X/%X on timeline %u, but in the history of the requested timeline, the server forked off from that timeline at %X/%X.", + errdetail("Latest checkpoint in file \"%s\" is at %X/%08X on timeline %u, but in the history of the requested timeline, the server forked off from that timeline at %X/%08X.", haveBackupLabel ? "backup_label" : "pg_control", LSN_FORMAT_ARGS(CheckPointLoc), CheckPointTLI, @@ -866,25 +815,25 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, * The min recovery point should be part of the requested timeline's * history, too. */ - if (!XLogRecPtrIsInvalid(ControlFile->minRecoveryPoint) && + if (XLogRecPtrIsValid(ControlFile->minRecoveryPoint) && tliOfPointInHistory(ControlFile->minRecoveryPoint - 1, expectedTLEs) != ControlFile->minRecoveryPointTLI) ereport(FATAL, - (errmsg("requested timeline %u does not contain minimum recovery point %X/%X on timeline %u", - recoveryTargetTLI, - LSN_FORMAT_ARGS(ControlFile->minRecoveryPoint), - ControlFile->minRecoveryPointTLI))); + errmsg("requested timeline %u does not contain minimum recovery point %X/%08X on timeline %u", + recoveryTargetTLI, + LSN_FORMAT_ARGS(ControlFile->minRecoveryPoint), + ControlFile->minRecoveryPointTLI)); ereport(DEBUG1, - (errmsg_internal("redo record is at %X/%X; shutdown %s", - LSN_FORMAT_ARGS(checkPoint.redo), - wasShutdown ? "true" : "false"))); + errmsg_internal("redo record is at %X/%08X; shutdown %s", + LSN_FORMAT_ARGS(checkPoint.redo), + wasShutdown ? "true" : "false")); ereport(DEBUG1, (errmsg_internal("next transaction ID: " UINT64_FORMAT "; next OID: %u", U64FromFullTransactionId(checkPoint.nextXid), checkPoint.nextOid))); ereport(DEBUG1, - (errmsg_internal("next MultiXactId: %u; next MultiXactOffset: %u", + (errmsg_internal("next MultiXactId: %u; next MultiXactOffset: %" PRIu64, checkPoint.nextMulti, checkPoint.nextMultiOffset))); ereport(DEBUG1, (errmsg_internal("oldest unfrozen transaction ID: %u, in database %u", @@ -1057,9 +1006,6 @@ readRecoverySignalFile(void) * Check for recovery signal files and if found, fsync them since they * represent server state information. We don't sweat too much about the * possibility of fsync failure, however. - * - * If present, standby signal file takes precedence. If neither is present - * then we won't enter archive recovery. */ if (stat(STANDBY_SIGNAL_FILE, &stat_buf) == 0) { @@ -1074,7 +1020,8 @@ readRecoverySignalFile(void) } standby_signal_file_found = true; } - else if (stat(RECOVERY_SIGNAL_FILE, &stat_buf) == 0) + + if (stat(RECOVERY_SIGNAL_FILE, &stat_buf) == 0) { int fd; @@ -1088,6 +1035,10 @@ readRecoverySignalFile(void) recovery_signal_file_found = true; } + /* + * If both signal files are present, standby signal file takes precedence. + * If neither is present then we won't enter archive recovery. + */ StandbyModeRequested = false; ArchiveRecoveryRequested = false; if (standby_signal_file_found) @@ -1253,14 +1204,14 @@ read_backup_label(XLogRecPtr *checkPointLoc, TimeLineID *backupLabelTLI, * is pretty crude, but we are not expecting any variability in the file * format). */ - if (fscanf(lfp, "START WAL LOCATION: %X/%X (file %08X%16s)%c", + if (fscanf(lfp, "START WAL LOCATION: %X/%08X (file %08X%16s)%c", &hi, &lo, &tli_from_walseg, startxlogfilename, &ch) != 5 || ch != '\n') ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("invalid data in file \"%s\"", BACKUP_LABEL_FILE))); RedoStartLSN = ((uint64) hi) << 32 | lo; RedoStartTLI = tli_from_walseg; - if (fscanf(lfp, "CHECKPOINT LOCATION: %X/%X%c", + if (fscanf(lfp, "CHECKPOINT LOCATION: %X/%08X%c", &hi, &lo, &ch) != 3 || ch != '\n') ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), @@ -1332,7 +1283,7 @@ read_backup_label(XLogRecPtr *checkPointLoc, TimeLineID *backupLabelTLI, tli_from_file, BACKUP_LABEL_FILE))); } - if (fscanf(lfp, "INCREMENTAL FROM LSN: %X/%X\n", &hi, &lo) > 0) + if (fscanf(lfp, "INCREMENTAL FROM LSN: %X/%08X\n", &hi, &lo) > 0) ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("this is an incremental backup, not a data directory"), @@ -1414,7 +1365,7 @@ read_tablespace_map(List **tablespaces) errmsg("invalid data in file \"%s\"", TABLESPACE_MAP))); str[n++] = '\0'; - ti = palloc0(sizeof(tablespaceinfo)); + ti = palloc0_object(tablespaceinfo); errno = 0; ti->oid = strtoul(str, &endp, 10); if (*endp != '\0' || errno == EINVAL || errno == ERANGE) @@ -1465,7 +1416,7 @@ read_tablespace_map(List **tablespaces) EndOfWalRecoveryInfo * FinishWalRecovery(void) { - EndOfWalRecoveryInfo *result = palloc(sizeof(EndOfWalRecoveryInfo)); + EndOfWalRecoveryInfo *result = palloc_object(EndOfWalRecoveryInfo); XLogRecPtr lastRec; TimeLineID lastRecTLI; XLogRecPtr endOfLog; @@ -1626,6 +1577,7 @@ ShutdownWalRecovery(void) close(readFile); readFile = -1; } + pfree(xlogreader->private_data); XLogReaderFree(xlogreader); XLogPrefetcherFree(xlogprefetcher); @@ -1722,8 +1674,8 @@ PerformWalRecovery(void) if (record->xl_rmid != RM_XLOG_ID || (record->xl_info & ~XLR_INFO_MASK) != XLOG_CHECKPOINT_REDO) ereport(FATAL, - (errmsg("unexpected record type found at redo point %X/%X", - LSN_FORMAT_ARGS(xlogreader->ReadRecPtr)))); + errmsg("unexpected record type found at redo point %X/%08X", + LSN_FORMAT_ARGS(xlogreader->ReadRecPtr))); } else { @@ -1745,8 +1697,8 @@ PerformWalRecovery(void) RmgrStartup(); ereport(LOG, - (errmsg("redo starts at %X/%X", - LSN_FORMAT_ARGS(xlogreader->ReadRecPtr)))); + errmsg("redo starts at %X/%08X", + LSN_FORMAT_ARGS(xlogreader->ReadRecPtr))); /* Prepare to report progress of the redo phase. */ if (!StandbyMode) @@ -1758,7 +1710,7 @@ PerformWalRecovery(void) do { if (!StandbyMode) - ereport_startup_progress("redo in progress, elapsed time: %ld.%02d s, current LSN: %X/%X", + ereport_startup_progress("redo in progress, elapsed time: %ld.%02d s, current LSN: %X/%08X", LSN_FORMAT_ARGS(xlogreader->ReadRecPtr)); #ifdef WAL_DEBUG @@ -1767,7 +1719,7 @@ PerformWalRecovery(void) StringInfoData buf; initStringInfo(&buf); - appendStringInfo(&buf, "REDO @ %X/%X; LSN %X/%X: ", + appendStringInfo(&buf, "REDO @ %X/%08X; LSN %X/%08X: ", LSN_FORMAT_ARGS(xlogreader->ReadRecPtr), LSN_FORMAT_ARGS(xlogreader->EndRecPtr)); xlog_outrec(&buf, xlogreader); @@ -1829,6 +1781,16 @@ PerformWalRecovery(void) */ ApplyWalRecord(xlogreader, record, &replayTLI); + /* + * If we replayed an LSN that someone was waiting for then walk + * over the shared memory array and set latches to notify the + * waiters. + */ + if (waitLSNState && + (XLogRecoveryCtl->lastReplayedEndRecPtr >= + pg_atomic_read_u64(&waitLSNState->minWaitedLSN[WAIT_LSN_TYPE_STANDBY_REPLAY]))) + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_REPLAY, XLogRecoveryCtl->lastReplayedEndRecPtr); + /* Exit loop if we reached inclusive recovery target */ if (recoveryStopsAfter(xlogreader)) { @@ -1871,6 +1833,7 @@ PerformWalRecovery(void) recoveryPausesHere(true); /* drop into promote */ + pg_fallthrough; case RECOVERY_TARGET_ACTION_PROMOTE: break; @@ -1880,9 +1843,9 @@ PerformWalRecovery(void) RmgrCleanup(); ereport(LOG, - (errmsg("redo done at %X/%X system usage: %s", - LSN_FORMAT_ARGS(xlogreader->ReadRecPtr), - pg_rusage_show(&ru0)))); + errmsg("redo done at %X/%08X system usage: %s", + LSN_FORMAT_ARGS(xlogreader->ReadRecPtr), + pg_rusage_show(&ru0))); xtime = GetLatestXTime(); if (xtime) ereport(LOG, @@ -2053,7 +2016,7 @@ ApplyWalRecord(XLogReaderState *xlogreader, XLogRecord *record, TimeLineID *repl if (doRequestWalReceiverReply) { doRequestWalReceiverReply = false; - WalRcvForceReply(); + WalRcvRequestApplyReply(); } /* Allow read-only connections if we're consistent now */ @@ -2092,7 +2055,7 @@ xlogrecovery_redo(XLogReaderState *record, TimeLineID replayTLI) memcpy(&xlrec, XLogRecGetData(record), sizeof(xl_overwrite_contrecord)); if (xlrec.overwritten_lsn != record->overwrittenRecPtr) - elog(FATAL, "mismatching overwritten LSN %X/%X -> %X/%X", + elog(FATAL, "mismatching overwritten LSN %X/%08X -> %X/%08X", LSN_FORMAT_ARGS(xlrec.overwritten_lsn), LSN_FORMAT_ARGS(record->overwrittenRecPtr)); @@ -2101,9 +2064,9 @@ xlogrecovery_redo(XLogReaderState *record, TimeLineID replayTLI) missingContrecPtr = InvalidXLogRecPtr; ereport(LOG, - (errmsg("successfully skipped missing contrecord at %X/%X, overwritten at %s", - LSN_FORMAT_ARGS(xlrec.overwritten_lsn), - timestamptz_to_str(xlrec.overwrite_time)))); + errmsg("successfully skipped missing contrecord at %X/%08X, overwritten at %s", + LSN_FORMAT_ARGS(xlrec.overwritten_lsn), + timestamptz_to_str(xlrec.overwrite_time))); /* Verifying the record should only happen once */ record->overwrittenRecPtr = InvalidXLogRecPtr; @@ -2129,7 +2092,7 @@ xlogrecovery_redo(XLogReaderState *record, TimeLineID replayTLI) backupEndPoint = lsn; } else - elog(DEBUG1, "saw end-of-backup record for backup starting at %X/%X, waiting for %X/%X", + elog(DEBUG1, "saw end-of-backup record for backup starting at %X/%08X, waiting for %X/%08X", LSN_FORMAT_ARGS(startpoint), LSN_FORMAT_ARGS(backupStartPoint)); } } @@ -2191,7 +2154,7 @@ CheckRecoveryConsistency(void) * During crash recovery, we don't reach a consistent state until we've * replayed all the WAL. */ - if (XLogRecPtrIsInvalid(minRecoveryPoint)) + if (!XLogRecPtrIsValid(minRecoveryPoint)) return; Assert(InArchiveRecovery); @@ -2206,7 +2169,7 @@ CheckRecoveryConsistency(void) /* * Have we reached the point where our base backup was completed? */ - if (!XLogRecPtrIsInvalid(backupEndPoint) && + if (XLogRecPtrIsValid(backupEndPoint) && backupEndPoint <= lastReplayedEndRecPtr) { XLogRecPtr saveBackupStartPoint = backupStartPoint; @@ -2224,9 +2187,9 @@ CheckRecoveryConsistency(void) backupEndRequired = false; ereport(LOG, - (errmsg("completed backup recovery with redo LSN %X/%X and end LSN %X/%X", - LSN_FORMAT_ARGS(saveBackupStartPoint), - LSN_FORMAT_ARGS(saveBackupEndPoint)))); + errmsg("completed backup recovery with redo LSN %X/%08X and end LSN %X/%08X", + LSN_FORMAT_ARGS(saveBackupStartPoint), + LSN_FORMAT_ARGS(saveBackupEndPoint))); } /* @@ -2255,8 +2218,8 @@ CheckRecoveryConsistency(void) reachedConsistency = true; SendPostmasterSignal(PMSIGNAL_RECOVERY_CONSISTENT); ereport(LOG, - (errmsg("consistent recovery state reached at %X/%X", - LSN_FORMAT_ARGS(lastReplayedEndRecPtr)))); + errmsg("consistent recovery state reached at %X/%08X", + LSN_FORMAT_ARGS(lastReplayedEndRecPtr))); } /* @@ -2293,7 +2256,7 @@ rm_redo_error_callback(void *arg) xlog_block_info(&buf, record); /* translator: %s is a WAL record description */ - errcontext("WAL redo at %X/%X for %s", + errcontext("WAL redo at %X/%08X for %s", LSN_FORMAT_ARGS(record->ReadRecPtr), buf.data); @@ -2328,7 +2291,7 @@ xlog_outdesc(StringInfo buf, XLogReaderState *record) static void xlog_outrec(StringInfo buf, XLogReaderState *record) { - appendStringInfo(buf, "prev %X/%X; xid %u", + appendStringInfo(buf, "prev %X/%08X; xid %u", LSN_FORMAT_ARGS(XLogRecGetPrev(record)), XLogRecGetXid(record)); @@ -2412,14 +2375,14 @@ checkTimeLineSwitch(XLogRecPtr lsn, TimeLineID newTLI, TimeLineID prevTLI, * branched before the timeline the min recovery point is on, and you * attempt to do PITR to the new timeline. */ - if (!XLogRecPtrIsInvalid(minRecoveryPoint) && + if (XLogRecPtrIsValid(minRecoveryPoint) && lsn < minRecoveryPoint && newTLI > minRecoveryPointTLI) ereport(PANIC, - (errmsg("unexpected timeline ID %u in checkpoint record, before reaching minimum recovery point %X/%X on timeline %u", - newTLI, - LSN_FORMAT_ARGS(minRecoveryPoint), - minRecoveryPointTLI))); + errmsg("unexpected timeline ID %u in checkpoint record, before reaching minimum recovery point %X/%08X on timeline %u", + newTLI, + LSN_FORMAT_ARGS(minRecoveryPoint), + minRecoveryPointTLI)); /* Looks good */ } @@ -2621,8 +2584,8 @@ recoveryStopsBefore(XLogReaderState *record) recoveryStopTime = 0; recoveryStopName[0] = '\0'; ereport(LOG, - (errmsg("recovery stopping before WAL location (LSN) \"%X/%X\"", - LSN_FORMAT_ARGS(recoveryStopLSN)))); + errmsg("recovery stopping before WAL location (LSN) \"%X/%08X\"", + LSN_FORMAT_ARGS(recoveryStopLSN))); return true; } @@ -2789,8 +2752,8 @@ recoveryStopsAfter(XLogReaderState *record) recoveryStopTime = 0; recoveryStopName[0] = '\0'; ereport(LOG, - (errmsg("recovery stopping after WAL location (LSN) \"%X/%X\"", - LSN_FORMAT_ARGS(recoveryStopLSN)))); + errmsg("recovery stopping after WAL location (LSN) \"%X/%08X\"", + LSN_FORMAT_ARGS(recoveryStopLSN))); return true; } @@ -2910,7 +2873,7 @@ getRecoveryStopReason(void) timestamptz_to_str(recoveryStopTime)); else if (recoveryTarget == RECOVERY_TARGET_LSN) snprintf(reason, sizeof(reason), - "%s LSN %X/%X\n", + "%s LSN %X/%08X\n", recoveryStopAfter ? "after" : "before", LSN_FORMAT_ARGS(recoveryStopLSN)); else if (recoveryTarget == RECOVERY_TARGET_NAME) @@ -3146,10 +3109,12 @@ ReadRecord(XLogPrefetcher *xlogprefetcher, int emode, XLogReaderState *xlogreader = XLogPrefetcherGetReader(xlogprefetcher); XLogPageReadPrivate *private = (XLogPageReadPrivate *) xlogreader->private_data; + Assert(AmStartupProcess() || !IsUnderPostmaster); + /* Pass through parameters to XLogPageRead */ private->fetching_ckpt = fetching_ckpt; private->emode = emode; - private->randAccess = (xlogreader->ReadRecPtr == InvalidXLogRecPtr); + private->randAccess = !XLogRecPtrIsValid(xlogreader->ReadRecPtr); private->replayTLI = replayTLI; /* This is the first attempt to read this page. */ @@ -3175,7 +3140,7 @@ ReadRecord(XLogPrefetcher *xlogprefetcher, int emode, * overwrite contrecord in the wrong place, breaking everything. */ if (!ArchiveRecoveryRequested && - !XLogRecPtrIsInvalid(xlogreader->abortedRecPtr)) + XLogRecPtrIsValid(xlogreader->abortedRecPtr)) { abortedRecPtr = xlogreader->abortedRecPtr; missingContrecPtr = xlogreader->missingContrecPtr; @@ -3213,11 +3178,11 @@ ReadRecord(XLogPrefetcher *xlogprefetcher, int emode, XLogFileName(fname, xlogreader->seg.ws_tli, segno, wal_segment_size); ereport(emode_for_corrupt_record(emode, xlogreader->EndRecPtr), - (errmsg("unexpected timeline ID %u in WAL segment %s, LSN %X/%X, offset %u", - xlogreader->latestPageTLI, - fname, - LSN_FORMAT_ARGS(xlogreader->latestPagePtr), - offset))); + errmsg("unexpected timeline ID %u in WAL segment %s, LSN %X/%08X, offset %u", + xlogreader->latestPageTLI, + fname, + LSN_FORMAT_ARGS(xlogreader->latestPagePtr), + offset)); record = NULL; } @@ -3317,6 +3282,8 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, int r; instr_time io_start; + Assert(AmStartupProcess() || !IsUnderPostmaster); + XLByteToSeg(targetPagePtr, targetSegNo, wal_segment_size); targetPageOff = XLogSegmentOffset(targetPagePtr, wal_segment_size); @@ -3412,7 +3379,7 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, io_start = pgstat_prepare_io_time(track_wal_io_timing); pgstat_report_wait_start(WAIT_EVENT_WAL_READ); - r = pg_pread(readFile, readBuf, XLOG_BLCKSZ, (off_t) readOff); + r = pg_pread(readFile, readBuf, XLOG_BLCKSZ, (pgoff_t) readOff); if (r != XLOG_BLCKSZ) { char fname[MAXFNAMELEN]; @@ -3429,14 +3396,14 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, errno = save_errno; ereport(emode_for_corrupt_record(emode, targetPagePtr + reqLen), (errcode_for_file_access(), - errmsg("could not read from WAL segment %s, LSN %X/%X, offset %u: %m", + errmsg("could not read from WAL segment %s, LSN %X/%08X, offset %u: %m", fname, LSN_FORMAT_ARGS(targetPagePtr), readOff))); } else ereport(emode_for_corrupt_record(emode, targetPagePtr + reqLen), (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("could not read from WAL segment %s, LSN %X/%X, offset %u: read %d of %zu", + errmsg("could not read from WAL segment %s, LSN %X/%08X, offset %u: read %d of %zu", fname, LSN_FORMAT_ARGS(targetPagePtr), readOff, r, (Size) XLOG_BLCKSZ))); goto next_record_is_invalid; @@ -3548,9 +3515,9 @@ XLogPageRead(XLogReaderState *xlogreader, XLogRecPtr targetPagePtr, int reqLen, * timelines, we can reject a switch to a timeline that branched off before * this point. * - * If the record is not immediately available, the function returns false - * if we're not in standby mode. In standby mode, waits for it to become - * available. + * If the record is not immediately available, the function returns XLREAD_FAIL + * if we're not in standby mode. In standby mode, the function waits for it to + * become available. * * When the requested record becomes available, the function opens the file * containing it (if not open already), and returns XLREAD_SUCCESS. When end @@ -3685,8 +3652,27 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, * Before we leave XLOG_FROM_STREAM state, make sure that * walreceiver is not active, so that it won't overwrite * WAL that we restore from archive. + * + * If walreceiver is actively streaming (or attempting to + * connect), we must shut it down. However, if it's + * already in WAITING state (e.g., due to timeline + * divergence), we only need to reset the install flag to + * allow archive restoration. */ - XLogShutdownWalRcv(); + if (WalRcvStreaming()) + XLogShutdownWalRcv(); + else + { + /* + * WALRCV_STOPPING state is a transient state while + * the startup process is in ShutdownWalRcv(). It + * should never appear here since we would be waiting + * for the walreceiver to reach WALRCV_STOPPED in that + * case. + */ + Assert(WalRcvGetState() != WALRCV_STOPPING); + ResetInstallXLogFileSegmentActive(); + } /* * Before we sleep, re-scan for possible new timelines if @@ -3718,7 +3704,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, wait_time = wal_retrieve_retry_interval - TimestampDifferenceMilliseconds(last_fail_time, now); - elog(LOG, "waiting for WAL to become available at %X/%X", + elog(LOG, "waiting for WAL to become available at %X/%08X", LSN_FORMAT_ARGS(RecPtr)); /* Do background tasks that might benefit us later. */ @@ -3864,7 +3850,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, tli = tliOfPointInHistory(tliRecPtr, expectedTLEs); if (curFileTLI > 0 && tli < curFileTLI) - elog(ERROR, "according to history file, WAL location %X/%X belongs to timeline %u, but previous recovered WAL file came from timeline %u", + elog(ERROR, "according to history file, WAL location %X/%08X belongs to timeline %u, but previous recovered WAL file came from timeline %u", LSN_FORMAT_ARGS(tliRecPtr), tli, curFileTLI); } @@ -3873,7 +3859,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, RequestXLogStreaming(tli, ptr, PrimaryConnInfo, PrimarySlotName, wal_receiver_create_temp_slot); - flushedUpto = 0; + flushedUpto = InvalidXLogRecPtr; } /* @@ -3985,7 +3971,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, */ if (!streaming_reply_sent) { - WalRcvForceReply(); + WalRcvRequestApplyReply(); streaming_reply_sent = true; } @@ -4051,7 +4037,7 @@ WaitForWALToBecomeAvailable(XLogRecPtr RecPtr, bool randAccess, static int emode_for_corrupt_record(int emode, XLogRecPtr RecPtr) { - static XLogRecPtr lastComplaint = 0; + static XLogRecPtr lastComplaint = InvalidXLogRecPtr; if (readSource == XLOG_FROM_PG_WAL && emode == LOG) { @@ -4177,10 +4163,10 @@ rescanLatestTimeLine(TimeLineID replayTLI, XLogRecPtr replayLSN) if (currentTle->end < replayLSN) { ereport(LOG, - (errmsg("new timeline %u forked off current database system timeline %u before current recovery point %X/%X", - newtarget, - replayTLI, - LSN_FORMAT_ARGS(replayLSN)))); + errmsg("new timeline %u forked off current database system timeline %u before current recovery point %X/%08X", + newtarget, + replayTLI, + LSN_FORMAT_ARGS(replayLSN))); return false; } @@ -4334,7 +4320,7 @@ XLogFileReadAnyTLI(XLogSegNo segno, XLogSource source) * Skip scanning the timeline ID that the logfile segment to read * doesn't belong to */ - if (hent->begin != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(hent->begin)) { XLogSegNo beginseg = 0; @@ -4759,9 +4745,20 @@ RecoveryRequiresIntParameter(const char *param_name, int currValue, int minValue bool check_primary_slot_name(char **newval, void **extra, GucSource source) { + int err_code; + char *err_msg = NULL; + char *err_hint = NULL; + if (*newval && strcmp(*newval, "") != 0 && - !ReplicationSlotValidateName(*newval, WARNING)) + !ReplicationSlotValidateNameInternal(*newval, false, &err_code, + &err_msg, &err_hint)) + { + GUC_check_errcode(err_code); + GUC_check_errdetail("%s", err_msg); + if (err_hint != NULL) + GUC_check_errhint("%s", err_hint); return false; + } return true; } @@ -4833,10 +4830,10 @@ check_recovery_target_lsn(char **newval, void **extra, GucSource source) { XLogRecPtr lsn; XLogRecPtr *myextra; - bool have_error = false; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - lsn = pg_lsn_in_internal(*newval, &have_error); - if (have_error) + lsn = pg_lsn_in_safe(*newval, (Node *) &escontext); + if (escontext.error_occurred) return false; myextra = (XLogRecPtr *) guc_malloc(LOG, sizeof(XLogRecPtr)); @@ -4994,13 +4991,25 @@ check_recovery_target_timeline(char **newval, void **extra, GucSource source) rttg = RECOVERY_TARGET_TIMELINE_LATEST; else { + char *endp; + uint64 timeline; + rttg = RECOVERY_TARGET_TIMELINE_NUMERIC; errno = 0; - strtoul(*newval, NULL, 0); - if (errno == EINVAL || errno == ERANGE) + timeline = strtou64(*newval, &endp, 0); + + if (*endp != '\0' || errno == EINVAL || errno == ERANGE) + { + GUC_check_errdetail("\"%s\" is not a valid number.", + "recovery_target_timeline"); + return false; + } + + if (timeline < 1 || timeline > PG_UINT32_MAX) { - GUC_check_errdetail("\"recovery_target_timeline\" is not a valid number."); + GUC_check_errdetail("\"%s\" must be between %u and %u.", + "recovery_target_timeline", 1, PG_UINT32_MAX); return false; } } @@ -5037,11 +5046,38 @@ check_recovery_target_xid(char **newval, void **extra, GucSource source) { TransactionId xid; TransactionId *myextra; + char *endp; + char *val; errno = 0; - xid = (TransactionId) strtou64(*newval, NULL, 0); - if (errno == EINVAL || errno == ERANGE) + + /* + * Consume leading whitespace to determine if number is negative + */ + val = *newval; + + while (isspace((unsigned char) *val)) + val++; + + /* + * This cast will remove the epoch, if any + */ + xid = (TransactionId) strtou64(val, &endp, 0); + + if (*endp != '\0' || errno == EINVAL || errno == ERANGE || *val == '-') + { + GUC_check_errdetail("\"%s\" is not a valid number.", + "recovery_target_xid"); return false; + } + + if (xid < FirstNormalTransactionId) + { + GUC_check_errdetail("\"%s\" without epoch must be greater than or equal to %u.", + "recovery_target_xid", + FirstNormalTransactionId); + return false; + } myextra = (TransactionId *) guc_malloc(LOG, sizeof(TransactionId)); if (!myextra) diff --git a/src/backend/access/transam/xlogstats.c b/src/backend/access/transam/xlogstats.c index f92d9e13b174e..f0cec2075f80c 100644 --- a/src/backend/access/transam/xlogstats.c +++ b/src/backend/access/transam/xlogstats.c @@ -1,9 +1,9 @@ /*------------------------------------------------------------------------- * * xlogstats.c - * Functions for WAL Statitstics + * Functions for WAL Statistics * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/access/transam/xlogstats.c diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c index c389b27f77d47..5fbe39133b806 100644 --- a/src/backend/access/transam/xlogutils.c +++ b/src/backend/access/transam/xlogutils.c @@ -8,7 +8,7 @@ * None of this code is used during normal system operation. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/access/transam/xlogutils.c @@ -523,7 +523,7 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum, if (mode == RBM_NORMAL) { /* check that page has been initialized */ - Page page = (Page) BufferGetPage(buffer); + Page page = BufferGetPage(buffer); /* * We assume that PageIsNew is safe without a lock. During recovery, @@ -574,7 +574,7 @@ CreateFakeRelcacheEntry(RelFileLocator rlocator) Relation rel; /* Allocate the Relation struct and all related space in one block. */ - fakeentry = palloc0(sizeof(FakeRelCacheEntryData)); + fakeentry = palloc0_object(FakeRelCacheEntryData); rel = (Relation) fakeentry; rel->rd_rel = &fakeentry->pgc; @@ -710,7 +710,7 @@ XLogReadDetermineTimeline(XLogReaderState *state, XLogRecPtr wantPage, const XLogRecPtr lastReadPage = (state->seg.ws_segno * state->segcxt.ws_segsize + state->segoff); - Assert(wantPage != InvalidXLogRecPtr && wantPage % XLOG_BLCKSZ == 0); + Assert(XLogRecPtrIsValid(wantPage) && wantPage % XLOG_BLCKSZ == 0); Assert(wantLength <= XLOG_BLCKSZ); Assert(state->readLen == 0 || state->readLen <= XLOG_BLCKSZ); Assert(currTLI != 0); @@ -741,7 +741,7 @@ XLogReadDetermineTimeline(XLogReaderState *state, XLogRecPtr wantPage, */ if (state->currTLI == currTLI && wantPage >= lastReadPage) { - Assert(state->currTLIValidUntil == InvalidXLogRecPtr); + Assert(!XLogRecPtrIsValid(state->currTLIValidUntil)); return; } @@ -750,7 +750,7 @@ XLogReadDetermineTimeline(XLogReaderState *state, XLogRecPtr wantPage, * timeline and the timeline we're reading from is valid until the end of * the current segment we can just keep reading. */ - if (state->currTLIValidUntil != InvalidXLogRecPtr && + if (XLogRecPtrIsValid(state->currTLIValidUntil) && state->currTLI != currTLI && state->currTLI != 0 && ((wantPage + wantLength) / state->segcxt.ws_segsize) < @@ -790,12 +790,12 @@ XLogReadDetermineTimeline(XLogReaderState *state, XLogRecPtr wantPage, state->currTLIValidUntil = tliSwitchPoint(state->currTLI, timelineHistory, &state->nextTLI); - Assert(state->currTLIValidUntil == InvalidXLogRecPtr || + Assert(!XLogRecPtrIsValid(state->currTLIValidUntil) || wantPage + wantLength < state->currTLIValidUntil); list_free_deep(timelineHistory); - elog(DEBUG3, "switched to timeline %u valid until %X/%X", + elog(DEBUG3, "switched to timeline %u valid until %X/%08X", state->currTLI, LSN_FORMAT_ARGS(state->currTLIValidUntil)); } diff --git a/src/backend/access/transam/xlogwait.c b/src/backend/access/transam/xlogwait.c new file mode 100644 index 0000000000000..2e31c0d67d762 --- /dev/null +++ b/src/backend/access/transam/xlogwait.c @@ -0,0 +1,471 @@ +/*------------------------------------------------------------------------- + * + * xlogwait.c + * Implements waiting for WAL operations to reach specific LSNs. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/access/transam/xlogwait.c + * + * NOTES + * This file implements waiting for WAL operations to reach specific LSNs + * on both physical standby and primary servers. The core idea is simple: + * every process that wants to wait publishes the LSN it needs to the + * shared memory, and the appropriate process (startup on standby, + * walreceiver on standby, or WAL writer/backend on primary) wakes it + * once that LSN has been reached. + * + * The shared memory used by this module comprises a procInfos + * per-backend array with the information of the awaited LSN for each + * of the backend processes. The elements of that array are organized + * into pairing heaps (waitersHeap), one for each WaitLSNType, which + * allows for very fast finding of the least awaited LSN for each type. + * + * In addition, the least-awaited LSN for each type is cached in the + * minWaitedLSN array. The waiter process publishes information about + * itself to the shared memory and waits on the latch until it is woken + * up by the appropriate process, standby is promoted, or the postmaster + * dies. Then, it cleans information about itself in the shared memory. + * + * On standby servers: + * - After replaying a WAL record, the startup process performs a fast + * path check minWaitedLSN[REPLAY] > replayLSN. If this check is + * negative, it checks waitersHeap[REPLAY] and wakes up the backends + * whose awaited LSNs are reached. + * - After receiving WAL, the walreceiver process performs similar checks + * against the flush and write LSNs, waking up waiters in the FLUSH + * and WRITE heaps, respectively. + * + * On primary servers: After flushing WAL, the WAL writer or backend + * process performs a similar check against the flush LSN and wakes up + * waiters whose target flush LSNs have been reached. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include + +#include "access/xlog.h" +#include "access/xlogrecovery.h" +#include "access/xlogwait.h" +#include "miscadmin.h" +#include "pgstat.h" +#include "replication/walreceiver.h" +#include "storage/latch.h" +#include "storage/proc.h" +#include "storage/shmem.h" +#include "storage/subsystems.h" +#include "utils/fmgrprotos.h" +#include "utils/pg_lsn.h" +#include "utils/snapmgr.h" +#include "utils/wait_event.h" + + +static int waitlsn_cmp(const pairingheap_node *a, const pairingheap_node *b, + void *arg); + +struct WaitLSNState *waitLSNState = NULL; + +static void WaitLSNShmemRequest(void *arg); +static void WaitLSNShmemInit(void *arg); + +const ShmemCallbacks WaitLSNShmemCallbacks = { + .request_fn = WaitLSNShmemRequest, + .init_fn = WaitLSNShmemInit, +}; + +/* + * Wait event for each WaitLSNType, used with WaitLatch() to report + * the wait in pg_stat_activity. + */ +static const uint32 WaitLSNWaitEvents[] = { + [WAIT_LSN_TYPE_STANDBY_REPLAY] = WAIT_EVENT_WAIT_FOR_WAL_REPLAY, + [WAIT_LSN_TYPE_STANDBY_WRITE] = WAIT_EVENT_WAIT_FOR_WAL_WRITE, + [WAIT_LSN_TYPE_STANDBY_FLUSH] = WAIT_EVENT_WAIT_FOR_WAL_FLUSH, + [WAIT_LSN_TYPE_PRIMARY_FLUSH] = WAIT_EVENT_WAIT_FOR_WAL_FLUSH, +}; + +StaticAssertDecl(lengthof(WaitLSNWaitEvents) == WAIT_LSN_TYPE_COUNT, + "WaitLSNWaitEvents must match WaitLSNType enum"); + +/* + * Get the current LSN for the specified wait type. + */ +XLogRecPtr +GetCurrentLSNForWaitType(WaitLSNType lsnType) +{ + Assert(lsnType >= 0 && lsnType < WAIT_LSN_TYPE_COUNT); + + switch (lsnType) + { + case WAIT_LSN_TYPE_STANDBY_REPLAY: + return GetXLogReplayRecPtr(NULL); + + case WAIT_LSN_TYPE_STANDBY_WRITE: + return GetWalRcvWriteRecPtr(); + + case WAIT_LSN_TYPE_STANDBY_FLUSH: + return GetWalRcvFlushRecPtr(NULL, NULL); + + case WAIT_LSN_TYPE_PRIMARY_FLUSH: + return GetFlushRecPtr(NULL); + } + + elog(ERROR, "invalid LSN wait type: %d", lsnType); + pg_unreachable(); +} + +/* Register the shared memory space needed for WaitLSNState. */ +static void +WaitLSNShmemRequest(void *arg) +{ + Size size; + + size = offsetof(WaitLSNState, procInfos); + size = add_size(size, mul_size(MaxBackends + NUM_AUXILIARY_PROCS, sizeof(WaitLSNProcInfo))); + ShmemRequestStruct(.name = "WaitLSNState", + .size = size, + .ptr = (void **) &waitLSNState, + ); +} + +/* Initialize the WaitLSNState in the shared memory. */ +static void +WaitLSNShmemInit(void *arg) +{ + /* Initialize heaps and tracking */ + for (int i = 0; i < WAIT_LSN_TYPE_COUNT; i++) + { + pg_atomic_init_u64(&waitLSNState->minWaitedLSN[i], PG_UINT64_MAX); + pairingheap_initialize(&waitLSNState->waitersHeap[i], waitlsn_cmp, NULL); + } + + /* Initialize process info array */ + memset(&waitLSNState->procInfos, 0, + (MaxBackends + NUM_AUXILIARY_PROCS) * sizeof(WaitLSNProcInfo)); +} + +/* + * Comparison function for LSN waiters heaps. Waiting processes are ordered by + * LSN, so that the waiter with smallest LSN is at the top. + */ +static int +waitlsn_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg) +{ + const WaitLSNProcInfo *aproc = pairingheap_const_container(WaitLSNProcInfo, heapNode, a); + const WaitLSNProcInfo *bproc = pairingheap_const_container(WaitLSNProcInfo, heapNode, b); + + if (aproc->waitLSN < bproc->waitLSN) + return 1; + else if (aproc->waitLSN > bproc->waitLSN) + return -1; + else + return 0; +} + +/* + * Update minimum waited LSN for the specified LSN type + */ +static void +updateMinWaitedLSN(WaitLSNType lsnType) +{ + XLogRecPtr minWaitedLSN = PG_UINT64_MAX; + int i = (int) lsnType; + + Assert(i >= 0 && i < WAIT_LSN_TYPE_COUNT); + + if (!pairingheap_is_empty(&waitLSNState->waitersHeap[i])) + { + pairingheap_node *node = pairingheap_first(&waitLSNState->waitersHeap[i]); + WaitLSNProcInfo *procInfo = pairingheap_container(WaitLSNProcInfo, heapNode, node); + + minWaitedLSN = procInfo->waitLSN; + } + pg_atomic_write_u64(&waitLSNState->minWaitedLSN[i], minWaitedLSN); +} + +/* + * Add current process to appropriate waiters heap based on LSN type + */ +static void +addLSNWaiter(XLogRecPtr lsn, WaitLSNType lsnType) +{ + WaitLSNProcInfo *procInfo = &waitLSNState->procInfos[MyProcNumber]; + int i = (int) lsnType; + + Assert(i >= 0 && i < WAIT_LSN_TYPE_COUNT); + + LWLockAcquire(WaitLSNLock, LW_EXCLUSIVE); + + procInfo->procno = MyProcNumber; + procInfo->waitLSN = lsn; + procInfo->lsnType = lsnType; + + Assert(!procInfo->inHeap); + pairingheap_add(&waitLSNState->waitersHeap[i], &procInfo->heapNode); + procInfo->inHeap = true; + updateMinWaitedLSN(lsnType); + + LWLockRelease(WaitLSNLock); +} + +/* + * Remove current process from appropriate waiters heap based on LSN type + */ +static void +deleteLSNWaiter(WaitLSNType lsnType) +{ + WaitLSNProcInfo *procInfo = &waitLSNState->procInfos[MyProcNumber]; + int i = (int) lsnType; + + Assert(i >= 0 && i < WAIT_LSN_TYPE_COUNT); + + LWLockAcquire(WaitLSNLock, LW_EXCLUSIVE); + + Assert(procInfo->lsnType == lsnType); + + if (procInfo->inHeap) + { + pairingheap_remove(&waitLSNState->waitersHeap[i], &procInfo->heapNode); + procInfo->inHeap = false; + updateMinWaitedLSN(lsnType); + } + + LWLockRelease(WaitLSNLock); +} + +/* + * Size of a static array of procs to wakeup by WaitLSNWakeup() allocated + * on the stack. It should be enough to take single iteration for most cases. + */ +#define WAKEUP_PROC_STATIC_ARRAY_SIZE (16) + +/* + * Remove waiters whose LSN has been reached from the heap and set their + * latches. If InvalidXLogRecPtr is given, remove all waiters from the heap + * and set latches for all waiters. + * + * This function first accumulates waiters to wake up into an array, then + * wakes them up without holding a WaitLSNLock. The array size is static and + * equal to WAKEUP_PROC_STATIC_ARRAY_SIZE. That should be more than enough + * to wake up all the waiters at once in the vast majority of cases. However, + * if there are more waiters, this function will loop to process them in + * multiple chunks. + */ +static void +wakeupWaiters(WaitLSNType lsnType, XLogRecPtr currentLSN) +{ + ProcNumber wakeUpProcs[WAKEUP_PROC_STATIC_ARRAY_SIZE]; + int numWakeUpProcs; + int i = (int) lsnType; + + Assert(i >= 0 && i < WAIT_LSN_TYPE_COUNT); + + do + { + int j; + + numWakeUpProcs = 0; + LWLockAcquire(WaitLSNLock, LW_EXCLUSIVE); + + /* + * Iterate the waiters heap until we find LSN not yet reached. Record + * process numbers to wake up, but send wakeups after releasing lock. + */ + while (!pairingheap_is_empty(&waitLSNState->waitersHeap[i])) + { + pairingheap_node *node = pairingheap_first(&waitLSNState->waitersHeap[i]); + WaitLSNProcInfo *procInfo; + + /* Get procInfo using appropriate heap node */ + procInfo = pairingheap_container(WaitLSNProcInfo, heapNode, node); + + if (XLogRecPtrIsValid(currentLSN) && procInfo->waitLSN > currentLSN) + break; + + Assert(numWakeUpProcs < WAKEUP_PROC_STATIC_ARRAY_SIZE); + wakeUpProcs[numWakeUpProcs++] = procInfo->procno; + (void) pairingheap_remove_first(&waitLSNState->waitersHeap[i]); + + /* Update appropriate flag */ + procInfo->inHeap = false; + + if (numWakeUpProcs == WAKEUP_PROC_STATIC_ARRAY_SIZE) + break; + } + + updateMinWaitedLSN(lsnType); + LWLockRelease(WaitLSNLock); + + /* + * Set latches for processes whose waited LSNs have been reached. + * Since SetLatch() is a time-consuming operation, we do this outside + * of WaitLSNLock. This is safe because procLatch is never freed, so + * at worst we may set a latch for the wrong process or for no process + * at all, which is harmless. + */ + for (j = 0; j < numWakeUpProcs; j++) + SetLatch(&GetPGProcByNumber(wakeUpProcs[j])->procLatch); + + } while (numWakeUpProcs == WAKEUP_PROC_STATIC_ARRAY_SIZE); +} + +/* + * Wake up processes waiting for LSN to reach currentLSN + */ +void +WaitLSNWakeup(WaitLSNType lsnType, XLogRecPtr currentLSN) +{ + int i = (int) lsnType; + + Assert(i >= 0 && i < WAIT_LSN_TYPE_COUNT); + + /* + * Fast path check. Skip if currentLSN is InvalidXLogRecPtr, which means + * "wake all waiters" (e.g., during promotion when recovery ends). + */ + if (XLogRecPtrIsValid(currentLSN) && + pg_atomic_read_u64(&waitLSNState->minWaitedLSN[i]) > currentLSN) + return; + + wakeupWaiters(lsnType, currentLSN); +} + +/* + * Clean up LSN waiters for exiting process + */ +void +WaitLSNCleanup(void) +{ + if (waitLSNState) + { + /* + * We do a fast-path check of the inHeap flag without the lock. This + * flag is set to true only by the process itself. So, it's only + * possible to get a false positive. But that will be eliminated by a + * recheck inside deleteLSNWaiter(). + */ + if (waitLSNState->procInfos[MyProcNumber].inHeap) + deleteLSNWaiter(waitLSNState->procInfos[MyProcNumber].lsnType); + } +} + +/* + * Check if the given LSN type requires recovery to be in progress. + * Standby wait types (replay, write, flush) require recovery; + * primary wait types (flush) do not. + */ +static inline bool +WaitLSNTypeRequiresRecovery(WaitLSNType t) +{ + return t == WAIT_LSN_TYPE_STANDBY_REPLAY || + t == WAIT_LSN_TYPE_STANDBY_WRITE || + t == WAIT_LSN_TYPE_STANDBY_FLUSH; +} + +/* + * Wait using MyLatch till the given LSN is reached, the replica gets + * promoted, or the postmaster dies. + * + * Returns WAIT_LSN_RESULT_SUCCESS if target LSN was reached. + * Returns WAIT_LSN_RESULT_NOT_IN_RECOVERY if run not in recovery, + * or replica got promoted before the target LSN reached. + */ +WaitLSNResult +WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, int64 timeout) +{ + XLogRecPtr currentLSN; + TimestampTz endtime = 0; + int wake_events = WL_LATCH_SET | WL_POSTMASTER_DEATH; + + /* Shouldn't be called when shmem isn't initialized */ + Assert(waitLSNState); + + /* Should have a valid proc number */ + Assert(MyProcNumber >= 0 && MyProcNumber < MaxBackends + NUM_AUXILIARY_PROCS); + + if (timeout > 0) + { + endtime = TimestampTzPlusMilliseconds(GetCurrentTimestamp(), timeout); + wake_events |= WL_TIMEOUT; + } + + /* + * Add our process to the waiters heap. It might happen that target LSN + * gets reached before we do. The check at the beginning of the loop + * below prevents the race condition. + */ + addLSNWaiter(targetLSN, lsnType); + + for (;;) + { + int rc; + long delay_ms = -1; + + /* Get current LSN for the wait type */ + currentLSN = GetCurrentLSNForWaitType(lsnType); + + /* Check that recovery is still in-progress */ + if (WaitLSNTypeRequiresRecovery(lsnType) && !RecoveryInProgress()) + { + /* + * Recovery was ended, but check if target LSN was already + * reached. + */ + deleteLSNWaiter(lsnType); + + if (PromoteIsTriggered() && targetLSN <= currentLSN) + return WAIT_LSN_RESULT_SUCCESS; + return WAIT_LSN_RESULT_NOT_IN_RECOVERY; + } + else + { + /* Check if the waited LSN has been reached */ + if (targetLSN <= currentLSN) + break; + } + + if (timeout > 0) + { + delay_ms = TimestampDifferenceMilliseconds(GetCurrentTimestamp(), endtime); + if (delay_ms <= 0) + break; + } + + CHECK_FOR_INTERRUPTS(); + + rc = WaitLatch(MyLatch, wake_events, delay_ms, + WaitLSNWaitEvents[lsnType]); + + /* + * Emergency bailout if postmaster has died. This is to avoid the + * necessity for manual cleanup of all postmaster children. + */ + if (rc & WL_POSTMASTER_DEATH) + ereport(FATAL, + errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("terminating connection due to unexpected postmaster exit"), + errcontext("while waiting for LSN")); + + if (rc & WL_LATCH_SET) + ResetLatch(MyLatch); + } + + /* + * Delete our process from the shared memory heap. We might already be + * deleted by the startup process. The 'inHeap' flags prevents us from + * the double deletion. + */ + deleteLSNWaiter(lsnType); + + /* + * If we didn't reach the target LSN, we must be exited by timeout. + */ + if (targetLSN > currentLSN) + return WAIT_LSN_RESULT_TIMEOUT; + + return WAIT_LSN_RESULT_SUCCESS; +} diff --git a/src/backend/archive/meson.build b/src/backend/archive/meson.build index 2c3ad10199457..bc91b0e2a4cf2 100644 --- a/src/backend/archive/meson.build +++ b/src/backend/archive/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group backend_sources += files( 'shell_archive.c' diff --git a/src/backend/archive/shell_archive.c b/src/backend/archive/shell_archive.c index 828723afe4769..0b427a6880993 100644 --- a/src/backend/archive/shell_archive.c +++ b/src/backend/archive/shell_archive.c @@ -6,7 +6,7 @@ * archive_command GUC) to copy write-ahead log files. It is used as the * default, but other modules may define their own custom archiving logic. * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/archive/shell_archive.c @@ -22,6 +22,7 @@ #include "archive/shell_archive.h" #include "common/percentrepl.h" #include "pgstat.h" +#include "utils/wait_event.h" static bool shell_archive_configured(ArchiveModuleState *state); static bool shell_archive_file(ArchiveModuleState *state, diff --git a/src/backend/backup/backup_manifest.c b/src/backend/backup/backup_manifest.c index 22e2be37c95c3..3760b00390779 100644 --- a/src/backend/backup/backup_manifest.c +++ b/src/backend/backup/backup_manifest.c @@ -3,7 +3,7 @@ * backup_manifest.c * code for generating and sending a backup manifest * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/backup_manifest.c @@ -242,7 +242,7 @@ AddWALInfoToBackupManifest(backup_manifest_info *manifest, XLogRecPtr startptr, * entry->end is InvalidXLogRecPtr, it means that the timeline has not * yet ended.) */ - if (!XLogRecPtrIsInvalid(entry->end) && entry->end < startptr) + if (XLogRecPtrIsValid(entry->end) && entry->end < startptr) continue; /* @@ -253,7 +253,7 @@ AddWALInfoToBackupManifest(backup_manifest_info *manifest, XLogRecPtr startptr, if (first_wal_range && endtli != entry->tli) ereport(ERROR, errmsg("expected end timeline %u but found timeline %u", - starttli, entry->tli)); + endtli, entry->tli)); /* * If this timeline entry matches with the timeline on which the @@ -274,14 +274,14 @@ AddWALInfoToBackupManifest(backup_manifest_info *manifest, XLogRecPtr startptr, * better have arrived at the expected starting TLI. If not, * something's gone horribly wrong. */ - if (XLogRecPtrIsInvalid(entry->begin)) + if (!XLogRecPtrIsValid(entry->begin)) ereport(ERROR, errmsg("expected start timeline %u but found timeline %u", starttli, entry->tli)); } AppendToManifest(manifest, - "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%X\", \"End-LSN\": \"%X/%X\" }", + "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%08X\", \"End-LSN\": \"%X/%08X\" }", first_wal_range ? "" : ",\n", entry->tli, LSN_FORMAT_ARGS(tl_beginptr), @@ -388,7 +388,7 @@ AppendStringToManifest(backup_manifest_info *manifest, const char *s) Assert(manifest != NULL); if (manifest->still_checksumming) { - if (pg_cryptohash_update(manifest->manifest_ctx, (uint8 *) s, len) < 0) + if (pg_cryptohash_update(manifest->manifest_ctx, (const uint8 *) s, len) < 0) elog(ERROR, "failed to update checksum of backup manifest: %s", pg_cryptohash_error(manifest->manifest_ctx)); } diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c index f0f88838dc21a..9c79dadaacc55 100644 --- a/src/backend/backup/basebackup.c +++ b/src/backend/backup/basebackup.c @@ -3,7 +3,7 @@ * basebackup.c * code for taking a base backup and streaming it to a standby * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup.c @@ -48,6 +48,7 @@ #include "utils/ps_status.h" #include "utils/relcache.h" #include "utils/resowner.h" +#include "utils/wait_event.h" /* * How much data do we want to send in one CopyData message? Note that @@ -78,6 +79,11 @@ typedef struct pg_checksum_type manifest_checksum_type; } basebackup_options; +#define TAR_NUM_TERMINATION_BLOCKS 2 + +StaticAssertDecl(TAR_NUM_TERMINATION_BLOCKS * TAR_BLOCK_SIZE <= BLCKSZ, + "BLCKSZ too small for " CppAsString2(TAR_NUM_TERMINATION_BLOCKS) " tar termination blocks"); + static int64 sendTablespace(bbsink *sink, char *path, Oid spcoid, bool sizeonly, struct backup_manifest_info *manifest, IncrementalBackupInfo *ib); @@ -239,7 +245,7 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, TimeLineID endtli; backup_manifest_info manifest; BackupState *backup_state; - StringInfo tablespace_map; + StringInfoData tablespace_map; /* Initial backup state, insofar as we know it now. */ state.tablespaces = NIL; @@ -262,12 +268,12 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, total_checksum_failures = 0; /* Allocate backup related variables. */ - backup_state = (BackupState *) palloc0(sizeof(BackupState)); - tablespace_map = makeStringInfo(); + backup_state = palloc0_object(BackupState); + initStringInfo(&tablespace_map); basebackup_progress_wait_checkpoint(); do_pg_backup_start(opt->label, opt->fastcheckpoint, &state.tablespaces, - backup_state, tablespace_map); + backup_state, &tablespace_map); state.startptr = backup_state->startpoint; state.starttli = backup_state->starttli; @@ -289,7 +295,7 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, PrepareForIncrementalBackup(ib, backup_state); /* Add a node for the base directory at the end */ - newti = palloc0(sizeof(tablespaceinfo)); + newti = palloc0_object(tablespaceinfo); newti->size = -1; state.tablespaces = lappend(state.tablespaces, newti); @@ -342,7 +348,7 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, if (opt->sendtblspcmapfile) { sendFileWithContent(sink, TABLESPACE_MAP, - tablespace_map->data, -1, &manifest); + tablespace_map.data, -1, &manifest); sendtblspclinks = false; } @@ -382,10 +388,8 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, else { /* Properly terminate the tarfile. */ - StaticAssertDecl(2 * TAR_BLOCK_SIZE <= BLCKSZ, - "BLCKSZ too small for 2 tar blocks"); - memset(sink->bbs_buffer, 0, 2 * TAR_BLOCK_SIZE); - bbsink_archive_contents(sink, 2 * TAR_BLOCK_SIZE); + memset(sink->bbs_buffer, 0, TAR_NUM_TERMINATION_BLOCKS * TAR_BLOCK_SIZE); + bbsink_archive_contents(sink, TAR_NUM_TERMINATION_BLOCKS * TAR_BLOCK_SIZE); /* OK, that's the end of the archive. */ bbsink_end_archive(sink); @@ -399,7 +403,7 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, endtli = backup_state->stoptli; /* Deallocate backup-related variables. */ - destroyStringInfo(tablespace_map); + pfree(tablespace_map.data); pfree(backup_state); } PG_END_ENSURE_ERROR_CLEANUP(do_pg_abort_backup, BoolGetDatum(false)); @@ -635,10 +639,8 @@ perform_base_backup(basebackup_options *opt, bbsink *sink, } /* Properly terminate the tar file. */ - StaticAssertStmt(2 * TAR_BLOCK_SIZE <= BLCKSZ, - "BLCKSZ too small for 2 tar blocks"); - memset(sink->bbs_buffer, 0, 2 * TAR_BLOCK_SIZE); - bbsink_archive_contents(sink, 2 * TAR_BLOCK_SIZE); + memset(sink->bbs_buffer, 0, TAR_NUM_TERMINATION_BLOCKS * TAR_BLOCK_SIZE); + bbsink_archive_contents(sink, TAR_NUM_TERMINATION_BLOCKS * TAR_BLOCK_SIZE); /* OK, that's the end of the archive. */ bbsink_end_archive(sink); @@ -808,8 +810,8 @@ parse_basebackup_options(List *options, basebackup_options *opt) if (maxrate < MAX_RATE_LOWER || maxrate > MAX_RATE_UPPER) ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("%d is outside the valid range for parameter \"%s\" (%d .. %d)", - (int) maxrate, "MAX_RATE", MAX_RATE_LOWER, MAX_RATE_UPPER))); + errmsg("%" PRId64 " is outside the valid range for parameter \"%s\" (%d .. %d)", + maxrate, "MAX_RATE", MAX_RATE_LOWER, MAX_RATE_UPPER))); opt->maxrate = (uint32) maxrate; o_maxrate = true; @@ -1048,7 +1050,7 @@ SendBaseBackup(BaseBackupCmd *cmd, IncrementalBackupInfo *ib) sink = bbsink_zstd_new(sink, &opt.compression_specification); /* Set up progress reporting. */ - sink = bbsink_progress_new(sink, opt.progress); + sink = bbsink_progress_new(sink, opt.progress, opt.incremental); /* * Perform the base backup, but make sure we clean up the bbsink even if @@ -1104,7 +1106,7 @@ sendFileWithContent(bbsink *sink, const char *filename, const char *content, _tarWriteHeader(sink, filename, NULL, &statbuf, false); - if (pg_checksum_update(&checksum_ctx, (uint8 *) content, len) < 0) + if (pg_checksum_update(&checksum_ctx, (const uint8 *) content, len) < 0) elog(ERROR, "could not update checksum of file \"%s\"", filename); @@ -1206,7 +1208,7 @@ sendDir(bbsink *sink, const char *path, int basepathlen, bool sizeonly, * But we don't need it at all if this is not an incremental backup. */ if (ib != NULL) - relative_block_numbers = palloc(sizeof(BlockNumber) * RELSEG_SIZE); + relative_block_numbers = palloc_array(BlockNumber, RELSEG_SIZE); /* * Determine if the current path is a database directory that can contain @@ -1611,10 +1613,11 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename, /* * If we weren't told not to verify checksums, and if checksums are * enabled for this cluster, and if this is a relation file, then verify - * the checksum. + * the checksum. We cannot at this point check if checksums are enabled + * or disabled as that might change, thus we check at each point where we + * could be validating a checksum. */ - if (!noverify_checksums && DataChecksumsEnabled() && - RelFileNumberIsValid(relfilenumber)) + if (!noverify_checksums && RelFileNumberIsValid(relfilenumber)) verify_checksum = true; /* @@ -1747,7 +1750,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename, * If the amount of data we were able to read was not a multiple of * BLCKSZ, we cannot verify checksums, which are block-level. */ - if (verify_checksum && (cnt % BLCKSZ != 0)) + if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0)) { ereport(WARNING, (errmsg("could not verify checksum in file \"%s\", block " @@ -1842,9 +1845,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename, * 'blkno' is the block number of the first page in the bbsink's buffer * relative to the start of the relation. * - * 'verify_checksum' indicates whether we should try to verify checksums - * for the blocks we read. If we do this, we'll update *checksum_failures - * and issue warnings as appropriate. + * 'verify_checksum' determines if the user has asked to verify checksums, but + * since data checksums can be disabled, or become disabled, we need to check + * state before verifying individual pages. If we do this, we'll update + * *checksum_failures and issue warnings as appropriate. */ static off_t read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd, @@ -1870,6 +1874,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd, int reread_cnt; uint16 expected_checksum; + /* + * The data checksum state can change at any point, so we need to + * re-check before each page. + */ + if (!DataChecksumsNeedVerify()) + return cnt; + page = sink->bbs_buffer + BLCKSZ * i; /* If the page is OK, go on to the next one. */ @@ -1892,7 +1903,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd, * allows us to wait until we can be certain that no write to the * block is in progress. Since we don't have any such thing right now, * we just do this and hope for the best. + * + * The data checksum state may also have changed concurrently so check + * again. */ + if (!DataChecksumsNeedVerify()) + return cnt; reread_cnt = basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i, BLCKSZ, offset + BLCKSZ * i, @@ -2007,6 +2023,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno, if (PageIsNew(page) || PageGetLSN(page) >= start_lsn) return true; + if (!DataChecksumsNeedVerify()) + return true; + /* Perform the actual checksum calculation. */ checksum = pg_checksum_page(page, blkno); diff --git a/src/backend/backup/basebackup_copy.c b/src/backend/backup/basebackup_copy.c index a284ce318ff7d..6c3453efd80a5 100644 --- a/src/backend/backup/basebackup_copy.c +++ b/src/backend/backup/basebackup_copy.c @@ -16,7 +16,7 @@ * An older method that sent each archive using a separate COPY OUT * operation is no longer supported. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_copy.c @@ -66,7 +66,7 @@ typedef struct bbsink_copystream * frequently. Ideally, we'd like to send a message when the time since the * last message reaches PROGRESS_REPORT_MILLISECOND_THRESHOLD, but checking * the system time every time we send a tiny bit of data seems too expensive. - * So we only check it after the number of bytes sine the last check reaches + * So we only check it after the number of bytes since the last check reaches * PROGRESS_REPORT_BYTE_INTERVAL. */ #define PROGRESS_REPORT_BYTE_INTERVAL 65536 @@ -107,7 +107,7 @@ static const bbsink_ops bbsink_copystream_ops = { bbsink * bbsink_copystream_new(bool send_to_client) { - bbsink_copystream *sink = palloc0(sizeof(bbsink_copystream)); + bbsink_copystream *sink = palloc0_object(bbsink_copystream); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_copystream_ops; sink->send_to_client = send_to_client; @@ -143,7 +143,7 @@ bbsink_copystream_begin_backup(bbsink *sink) buf = palloc(mysink->base.bbs_buffer_length + MAXIMUM_ALIGNOF); mysink->msgbuffer = buf + (MAXIMUM_ALIGNOF - 1); mysink->base.bbs_buffer = buf + MAXIMUM_ALIGNOF; - mysink->msgbuffer[0] = 'd'; /* archive or manifest data */ + mysink->msgbuffer[0] = PqMsg_CopyData; /* archive or manifest data */ /* Tell client the backup start location. */ SendXlogRecPtrResult(state->startptr, state->starttli); @@ -170,7 +170,7 @@ bbsink_copystream_begin_archive(bbsink *sink, const char *archive_name) ti = list_nth(state->tablespaces, state->tablespace_num); pq_beginmessage(&buf, PqMsg_CopyData); - pq_sendbyte(&buf, 'n'); /* New archive */ + pq_sendbyte(&buf, PqBackupMsg_NewArchive); pq_sendstring(&buf, archive_name); pq_sendstring(&buf, ti->path == NULL ? "" : ti->path); pq_endmessage(&buf); @@ -191,7 +191,7 @@ bbsink_copystream_archive_contents(bbsink *sink, size_t len) if (mysink->send_to_client) { /* Add one because we're also sending a leading type byte. */ - pq_putmessage('d', mysink->msgbuffer, len + 1); + pq_putmessage(PqMsg_CopyData, mysink->msgbuffer, len + 1); } /* Consider whether to send a progress report to the client. */ @@ -221,7 +221,7 @@ bbsink_copystream_archive_contents(bbsink *sink, size_t len) mysink->last_progress_report_time = now; pq_beginmessage(&buf, PqMsg_CopyData); - pq_sendbyte(&buf, 'p'); /* Progress report */ + pq_sendbyte(&buf, PqBackupMsg_ProgressReport); pq_sendint64(&buf, state->bytes_done); pq_endmessage(&buf); pq_flush_if_writable(); @@ -247,7 +247,7 @@ bbsink_copystream_end_archive(bbsink *sink) mysink->bytes_done_at_last_time_check = state->bytes_done; mysink->last_progress_report_time = GetCurrentTimestamp(); pq_beginmessage(&buf, PqMsg_CopyData); - pq_sendbyte(&buf, 'p'); /* Progress report */ + pq_sendbyte(&buf, PqBackupMsg_ProgressReport); pq_sendint64(&buf, state->bytes_done); pq_endmessage(&buf); pq_flush_if_writable(); @@ -262,7 +262,7 @@ bbsink_copystream_begin_manifest(bbsink *sink) StringInfoData buf; pq_beginmessage(&buf, PqMsg_CopyData); - pq_sendbyte(&buf, 'm'); /* Manifest */ + pq_sendbyte(&buf, PqBackupMsg_Manifest); pq_endmessage(&buf); } @@ -277,7 +277,7 @@ bbsink_copystream_manifest_contents(bbsink *sink, size_t len) if (mysink->send_to_client) { /* Add one because we're also sending a leading type byte. */ - pq_putmessage('d', mysink->msgbuffer, len + 1); + pq_putmessage(PqMsg_CopyData, mysink->msgbuffer, len + 1); } } @@ -357,11 +357,13 @@ SendXlogRecPtrResult(XLogRecPtr ptr, TimeLineID tli) */ TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "tli", INT8OID, -1, 0); + TupleDescFinalize(tupdesc); + /* send RowDescription */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); /* Data row */ - values[0] = CStringGetTextDatum(psprintf("%X/%X", LSN_FORMAT_ARGS(ptr))); + values[0] = CStringGetTextDatum(psprintf("%X/%08X", LSN_FORMAT_ARGS(ptr))); values[1] = Int64GetDatum(tli); do_tup_output(tstate, values, nulls); @@ -388,6 +390,7 @@ SendTablespaceList(List *tablespaces) TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "spcoid", OIDOID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "spclocation", TEXTOID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "size", INT8OID, -1, 0); + TupleDescFinalize(tupdesc); /* send RowDescription */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); diff --git a/src/backend/backup/basebackup_gzip.c b/src/backend/backup/basebackup_gzip.c index c4cbb5f527644..c5e4c4143e80c 100644 --- a/src/backend/backup/basebackup_gzip.c +++ b/src/backend/backup/basebackup_gzip.c @@ -3,7 +3,7 @@ * basebackup_gzip.c * Basebackup sink implementing gzip compression. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_gzip.c @@ -32,6 +32,9 @@ typedef struct bbsink_gzip /* Number of bytes staged in output buffer. */ size_t bytes_written; + + /* Has the zstream been initialized? */ + bool zstream_initialized; } bbsink_gzip; static void bbsink_gzip_begin_backup(bbsink *sink); @@ -39,6 +42,7 @@ static void bbsink_gzip_begin_archive(bbsink *sink, const char *archive_name); static void bbsink_gzip_archive_contents(bbsink *sink, size_t len); static void bbsink_gzip_manifest_contents(bbsink *sink, size_t len); static void bbsink_gzip_end_archive(bbsink *sink); +static void bbsink_gzip_cleanup(bbsink *sink); static void *gzip_palloc(void *opaque, unsigned items, unsigned size); static void gzip_pfree(void *opaque, void *address); @@ -51,7 +55,7 @@ static const bbsink_ops bbsink_gzip_ops = { .manifest_contents = bbsink_gzip_manifest_contents, .end_manifest = bbsink_forward_end_manifest, .end_backup = bbsink_forward_end_backup, - .cleanup = bbsink_forward_cleanup + .cleanup = bbsink_gzip_cleanup }; #endif @@ -76,7 +80,7 @@ bbsink_gzip_new(bbsink *next, pg_compress_specification *compress) Assert((compresslevel >= 1 && compresslevel <= 9) || compresslevel == Z_DEFAULT_COMPRESSION); - sink = palloc0(sizeof(bbsink_gzip)); + sink = palloc0_object(bbsink_gzip); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_gzip_ops; sink->base.bbs_next = next; sink->compresslevel = compresslevel; @@ -141,6 +145,7 @@ bbsink_gzip_begin_archive(bbsink *sink, const char *archive_name) ereport(ERROR, errcode(ERRCODE_INTERNAL_ERROR), errmsg("could not initialize compression library")); + mysink->zstream_initialized = true; /* * Add ".gz" to the archive name. Note that the pg_basebackup -z produces @@ -266,6 +271,10 @@ bbsink_gzip_end_archive(bbsink *sink) mysink->bytes_written = 0; } + /* Release the compression resources. */ + deflateEnd(zs); + mysink->zstream_initialized = false; + /* Must also pass on the information that this archive has ended. */ bbsink_forward_end_archive(sink); } @@ -301,4 +310,20 @@ gzip_pfree(void *opaque, void *address) pfree(address); } +/* + * In case the backup fails, make sure we free the compression context by + * calling deflateEnd() if needed to avoid a resource leak. + */ +static void +bbsink_gzip_cleanup(bbsink *sink) +{ + bbsink_gzip *mysink = (bbsink_gzip *) sink; + + if (mysink->zstream_initialized) + { + deflateEnd(&mysink->zstream); + mysink->zstream_initialized = false; + } +} + #endif diff --git a/src/backend/backup/basebackup_incremental.c b/src/backend/backup/basebackup_incremental.c index 28491b1e0ab08..6f3552c6a4a54 100644 --- a/src/backend/backup/basebackup_incremental.c +++ b/src/backend/backup/basebackup_incremental.c @@ -10,7 +10,7 @@ * backup manifest supplied by the user taking the incremental backup * and extract the required information from it. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_incremental.c @@ -157,7 +157,7 @@ CreateIncrementalBackupInfo(MemoryContext mcxt) oldcontext = MemoryContextSwitchTo(mcxt); - ib = palloc0(sizeof(IncrementalBackupInfo)); + ib = palloc0_object(IncrementalBackupInfo); ib->mcxt = mcxt; initStringInfo(&ib->buf); @@ -169,7 +169,7 @@ CreateIncrementalBackupInfo(MemoryContext mcxt) */ ib->manifest_files = backup_file_create(mcxt, 10000, NULL); - context = palloc0(sizeof(JsonManifestParseContext)); + context = palloc0_object(JsonManifestParseContext); /* Parse the manifest. */ context->private_data = ib; context->version_cb = manifest_process_version; @@ -270,7 +270,6 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, ListCell *lc; TimeLineHistoryEntry **tlep; int num_wal_ranges; - int i; bool found_backup_start_tli = false; TimeLineID earliest_wal_range_tli = 0; XLogRecPtr earliest_wal_range_start_lsn = InvalidXLogRecPtr; @@ -312,7 +311,7 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, */ expectedTLEs = readTimeLineHistory(backup_state->starttli); tlep = palloc0(num_wal_ranges * sizeof(TimeLineHistoryEntry *)); - for (i = 0; i < num_wal_ranges; ++i) + for (int i = 0; i < num_wal_ranges; ++i) { backup_wal_range *range = list_nth(ib->manifest_wal_ranges, i); bool saw_earliest_wal_range_tli = false; @@ -400,7 +399,7 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, * anything here. However, if there's a problem staring us right in the * face, it's best to report it, so we do. */ - for (i = 0; i < num_wal_ranges; ++i) + for (int i = 0; i < num_wal_ranges; ++i) { backup_wal_range *range = list_nth(ib->manifest_wal_ranges, i); @@ -409,7 +408,7 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, if (range->start_lsn < tlep[i]->begin) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("manifest requires WAL from initial timeline %u starting at %X/%X, but that timeline begins at %X/%X", + errmsg("manifest requires WAL from initial timeline %u starting at %X/%08X, but that timeline begins at %X/%08X", range->tli, LSN_FORMAT_ARGS(range->start_lsn), LSN_FORMAT_ARGS(tlep[i]->begin)))); @@ -419,7 +418,7 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, if (range->start_lsn != tlep[i]->begin) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("manifest requires WAL from continuation timeline %u starting at %X/%X, but that timeline begins at %X/%X", + errmsg("manifest requires WAL from continuation timeline %u starting at %X/%08X, but that timeline begins at %X/%08X", range->tli, LSN_FORMAT_ARGS(range->start_lsn), LSN_FORMAT_ARGS(tlep[i]->begin)))); @@ -430,7 +429,7 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, if (range->end_lsn > backup_state->startpoint) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("manifest requires WAL from final timeline %u ending at %X/%X, but this backup starts at %X/%X", + errmsg("manifest requires WAL from final timeline %u ending at %X/%08X, but this backup starts at %X/%08X", range->tli, LSN_FORMAT_ARGS(range->end_lsn), LSN_FORMAT_ARGS(backup_state->startpoint)), @@ -441,7 +440,7 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, if (range->end_lsn != tlep[i]->end) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("manifest requires WAL from non-final timeline %u ending at %X/%X, but this server switched timelines at %X/%X", + errmsg("manifest requires WAL from non-final timeline %u ending at %X/%08X, but this server switched timelines at %X/%08X", range->tli, LSN_FORMAT_ARGS(range->end_lsn), LSN_FORMAT_ARGS(tlep[i]->end)))); @@ -519,21 +518,21 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, if (!WalSummariesAreComplete(tli_wslist, tli_start_lsn, tli_end_lsn, &tli_missing_lsn)) { - if (XLogRecPtrIsInvalid(tli_missing_lsn)) + if (!XLogRecPtrIsValid(tli_missing_lsn)) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("WAL summaries are required on timeline %u from %X/%X to %X/%X, but no summaries for that timeline and LSN range exist", + errmsg("WAL summaries are required on timeline %u from %X/%08X to %X/%08X, but no summaries for that timeline and LSN range exist", tle->tli, LSN_FORMAT_ARGS(tli_start_lsn), LSN_FORMAT_ARGS(tli_end_lsn)))); else ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("WAL summaries are required on timeline %u from %X/%X to %X/%X, but the summaries for that timeline and LSN range are incomplete", + errmsg("WAL summaries are required on timeline %u from %X/%08X to %X/%08X, but the summaries for that timeline and LSN range are incomplete", tle->tli, LSN_FORMAT_ARGS(tli_start_lsn), LSN_FORMAT_ARGS(tli_end_lsn)), - errdetail("The first unsummarized LSN in this range is %X/%X.", + errdetail("The first unsummarized LSN in this range is %X/%08X.", LSN_FORMAT_ARGS(tli_missing_lsn)))); } @@ -595,15 +594,14 @@ PrepareForIncrementalBackup(IncrementalBackupInfo *ib, while (1) { - unsigned nblocks; - unsigned i; + unsigned int nblocks; nblocks = BlockRefTableReaderGetBlocks(reader, blocks, BLOCKS_PER_READ); if (nblocks == 0) break; - for (i = 0; i < nblocks; ++i) + for (unsigned int i = 0; i < nblocks; ++i) BlockRefTableMarkBlockModified(ib->brtab, &rlocator, forknum, blocks[i]); } @@ -850,8 +848,22 @@ GetFileBackupMethod(IncrementalBackupInfo *ib, const char *path, { unsigned relative_limit = limit_block - segno * RELSEG_SIZE; + /* + * We can't set a truncation_block_length in excess of the limit block + * number (relativized to the current segment). To do so would be to + * treat blocks from older backups as valid current contents even if + * they were subsequently truncated away. + */ if (*truncation_block_length < relative_limit) *truncation_block_length = relative_limit; + + /* + * We also can't set a truncation_block_length in excess of the + * segment size, since the reconstructed file can't be larger than + * that. + */ + if (*truncation_block_length > RELSEG_SIZE) + *truncation_block_length = RELSEG_SIZE; } /* Send it incrementally. */ @@ -916,7 +928,7 @@ GetIncrementalFileSize(unsigned num_blocks_required) static uint32 hash_string_pointer(const char *s) { - unsigned char *ss = (unsigned char *) s; + const unsigned char *ss = (const unsigned char *) s; return hash_bytes(ss, strlen(s)); } @@ -993,7 +1005,7 @@ manifest_process_wal_range(JsonManifestParseContext *context, XLogRecPtr end_lsn) { IncrementalBackupInfo *ib = context->private_data; - backup_wal_range *range = palloc(sizeof(backup_wal_range)); + backup_wal_range *range = palloc_object(backup_wal_range); range->tli = tli; range->start_lsn = start_lsn; @@ -1035,8 +1047,8 @@ manifest_report_error(JsonManifestParseContext *context, const char *fmt,...) static int compare_block_numbers(const void *a, const void *b) { - BlockNumber aa = *(BlockNumber *) a; - BlockNumber bb = *(BlockNumber *) b; + BlockNumber aa = *(const BlockNumber *) a; + BlockNumber bb = *(const BlockNumber *) b; return pg_cmp_u32(aa, bb); } diff --git a/src/backend/backup/basebackup_lz4.c b/src/backend/backup/basebackup_lz4.c index c5ceccb846f57..ca487ebfe5990 100644 --- a/src/backend/backup/basebackup_lz4.c +++ b/src/backend/backup/basebackup_lz4.c @@ -3,7 +3,7 @@ * basebackup_lz4.c * Basebackup sink implementing lz4 compression. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_lz4.c @@ -75,7 +75,7 @@ bbsink_lz4_new(bbsink *next, pg_compress_specification *compress) compresslevel = compress->level; Assert(compresslevel >= 0 && compresslevel <= 12); - sink = palloc0(sizeof(bbsink_lz4)); + sink = palloc0_object(bbsink_lz4); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_lz4_ops; sink->base.bbs_next = next; sink->compresslevel = compresslevel; diff --git a/src/backend/backup/basebackup_progress.c b/src/backend/backup/basebackup_progress.c index 1d22b541f89af..fb9e57f04dfed 100644 --- a/src/backend/backup/basebackup_progress.c +++ b/src/backend/backup/basebackup_progress.c @@ -22,7 +22,7 @@ * the logic directly into that file as it's fairly simple, but it seems * cleaner to have everything related to progress reporting in one place.) * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_progress.c @@ -56,23 +56,28 @@ static const bbsink_ops bbsink_progress_ops = { * forwards data to a successor sink. */ bbsink * -bbsink_progress_new(bbsink *next, bool estimate_backup_size) +bbsink_progress_new(bbsink *next, bool estimate_backup_size, bool incremental) { bbsink *sink; Assert(next != NULL); - sink = palloc0(sizeof(bbsink)); + sink = palloc0_object(bbsink); *((const bbsink_ops **) &sink->bbs_ops) = &bbsink_progress_ops; sink->bbs_next = next; /* * Report that a base backup is in progress, and set the total size of the * backup to -1, which will get translated to NULL. If we're estimating - * the backup size, we'll insert the real estimate when we have it. + * the backup size, we'll insert the real estimate when we have it. Also, + * the backup type is set. */ pgstat_progress_start_command(PROGRESS_COMMAND_BASEBACKUP, InvalidOid); pgstat_progress_update_param(PROGRESS_BASEBACKUP_BACKUP_TOTAL, -1); + pgstat_progress_update_param(PROGRESS_BASEBACKUP_BACKUP_TYPE, + incremental + ? PROGRESS_BASEBACKUP_BACKUP_TYPE_INCREMENTAL + : PROGRESS_BASEBACKUP_BACKUP_TYPE_FULL); return sink; } diff --git a/src/backend/backup/basebackup_server.c b/src/backend/backup/basebackup_server.c index f5c0c61640a94..0d44a148f017c 100644 --- a/src/backend/backup/basebackup_server.c +++ b/src/backend/backup/basebackup_server.c @@ -59,7 +59,7 @@ static const bbsink_ops bbsink_server_ops = { bbsink * bbsink_server_new(bbsink *next, char *pathname) { - bbsink_server *sink = palloc0(sizeof(bbsink_server)); + bbsink_server *sink = palloc0_object(bbsink_server); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_server_ops; sink->pathname = pathname; @@ -176,9 +176,9 @@ bbsink_server_archive_contents(bbsink *sink, size_t len) /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %u", + errmsg("could not write file \"%s\": wrote only %d of %zu bytes at offset %u", FilePathName(mysink->file), - nbytes, (int) len, (unsigned) mysink->filepos), + nbytes, len, (unsigned) mysink->filepos), errhint("Check free disk space."))); } @@ -269,9 +269,9 @@ bbsink_server_manifest_contents(bbsink *sink, size_t len) /* short write: complain appropriately */ ereport(ERROR, (errcode(ERRCODE_DISK_FULL), - errmsg("could not write file \"%s\": wrote only %d of %d bytes at offset %u", + errmsg("could not write file \"%s\": wrote only %d of %zu bytes at offset %u", FilePathName(mysink->file), - nbytes, (int) len, (unsigned) mysink->filepos), + nbytes, len, (unsigned) mysink->filepos), errhint("Check free disk space."))); } diff --git a/src/backend/backup/basebackup_sink.c b/src/backend/backup/basebackup_sink.c index e962f8f0b8d62..bb98c68889552 100644 --- a/src/backend/backup/basebackup_sink.c +++ b/src/backend/backup/basebackup_sink.c @@ -3,7 +3,7 @@ * basebackup_sink.c * Default implementations for bbsink (basebackup sink) callbacks. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/backend/backup/basebackup_sink.c * diff --git a/src/backend/backup/basebackup_target.c b/src/backend/backup/basebackup_target.c index 84b1309d3bdc8..1c250d2895cb5 100644 --- a/src/backend/backup/basebackup_target.c +++ b/src/backend/backup/basebackup_target.c @@ -6,7 +6,7 @@ * Furthermore, new targets can be defined by extensions. This file * contains code to support that functionality. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_target.c @@ -96,7 +96,7 @@ BaseBackupAddTarget(char *name, * name into a newly-allocated chunk of memory. */ oldcontext = MemoryContextSwitchTo(TopMemoryContext); - newtype = palloc(sizeof(BaseBackupTargetType)); + newtype = palloc_object(BaseBackupTargetType); newtype->name = pstrdup(name); newtype->check_detail = check_detail; newtype->get_sink = get_sink; @@ -132,7 +132,7 @@ BaseBackupGetTargetHandle(char *target, char *target_detail) BaseBackupTargetHandle *handle; /* Found the target. */ - handle = palloc(sizeof(BaseBackupTargetHandle)); + handle = palloc_object(BaseBackupTargetHandle); handle->type = ttype; handle->detail_arg = ttype->check_detail(target, target_detail); diff --git a/src/backend/backup/basebackup_throttle.c b/src/backend/backup/basebackup_throttle.c index b2b743238f9d0..4d8d90f356bb8 100644 --- a/src/backend/backup/basebackup_throttle.c +++ b/src/backend/backup/basebackup_throttle.c @@ -5,7 +5,7 @@ * next base backup sink in the chain at a rate no greater than the * configured maximum. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_throttle.c @@ -19,6 +19,7 @@ #include "pgstat.h" #include "storage/latch.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" typedef struct bbsink_throttle { @@ -72,7 +73,7 @@ bbsink_throttle_new(bbsink *next, uint32 maxrate) Assert(next != NULL); Assert(maxrate > 0); - sink = palloc0(sizeof(bbsink_throttle)); + sink = palloc0_object(bbsink_throttle); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_throttle_ops; sink->base.bbs_next = next; diff --git a/src/backend/backup/basebackup_zstd.c b/src/backend/backup/basebackup_zstd.c index 18b2e8fb0b3b6..731fb42eb7696 100644 --- a/src/backend/backup/basebackup_zstd.c +++ b/src/backend/backup/basebackup_zstd.c @@ -3,7 +3,7 @@ * basebackup_zstd.c * Basebackup sink implementing zstd compression. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/backup/basebackup_zstd.c @@ -70,7 +70,7 @@ bbsink_zstd_new(bbsink *next, pg_compress_specification *compress) Assert(next != NULL); - sink = palloc0(sizeof(bbsink_zstd)); + sink = palloc0_object(bbsink_zstd); *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_zstd_ops; sink->base.bbs_next = next; sink->compress = compress; diff --git a/src/backend/backup/meson.build b/src/backend/backup/meson.build index 460025a3046ba..f3ff92c25e197 100644 --- a/src/backend/backup/meson.build +++ b/src/backend/backup/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'backup_manifest.c', diff --git a/src/backend/backup/walsummary.c b/src/backend/backup/walsummary.c index c7a2c65cc6a7a..4cd1824fbc6b9 100644 --- a/src/backend/backup/walsummary.c +++ b/src/backend/backup/walsummary.c @@ -3,7 +3,7 @@ * walsummary.c * Functions for accessing and managing WAL summary data. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/backend/backup/walsummary.c * @@ -67,13 +67,13 @@ GetWalSummaries(TimeLineID tli, XLogRecPtr start_lsn, XLogRecPtr end_lsn) /* Skip if it doesn't match the filter criteria. */ if (tli != 0 && tli != file_tli) continue; - if (!XLogRecPtrIsInvalid(start_lsn) && start_lsn >= file_end_lsn) + if (XLogRecPtrIsValid(start_lsn) && start_lsn >= file_end_lsn) continue; - if (!XLogRecPtrIsInvalid(end_lsn) && end_lsn <= file_start_lsn) + if (XLogRecPtrIsValid(end_lsn) && end_lsn <= file_start_lsn) continue; /* Add it to the list. */ - ws = palloc(sizeof(WalSummaryFile)); + ws = palloc_object(WalSummaryFile); ws->tli = file_tli; ws->start_lsn = file_start_lsn; ws->end_lsn = file_end_lsn; @@ -111,9 +111,9 @@ FilterWalSummaries(List *wslist, TimeLineID tli, /* Skip if it doesn't match the filter criteria. */ if (tli != 0 && tli != ws->tli) continue; - if (!XLogRecPtrIsInvalid(start_lsn) && start_lsn > ws->end_lsn) + if (XLogRecPtrIsValid(start_lsn) && start_lsn > ws->end_lsn) continue; - if (!XLogRecPtrIsInvalid(end_lsn) && end_lsn < ws->start_lsn) + if (XLogRecPtrIsValid(end_lsn) && end_lsn < ws->start_lsn) continue; /* Add it to the result list. */ @@ -214,7 +214,7 @@ OpenWalSummaryFile(WalSummaryFile *ws, bool missing_ok) LSN_FORMAT_ARGS(ws->end_lsn)); file = PathNameOpenFile(path, O_RDONLY); - if (file < 0 && (errno != EEXIST || !missing_ok)) + if (file < 0 && (errno != ENOENT || !missing_ok)) ereport(ERROR, (errcode_for_file_access(), errmsg("could not open file \"%s\": %m", path))); @@ -251,7 +251,7 @@ RemoveWalSummaryIfOlderThan(WalSummaryFile *ws, time_t cutoff_time) if (unlink(path) != 0) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not stat file \"%s\": %m", path))); + errmsg("could not remove file \"%s\": %m", path))); ereport(DEBUG2, (errmsg_internal("removing file \"%s\"", path))); } diff --git a/src/backend/backup/walsummaryfuncs.c b/src/backend/backup/walsummaryfuncs.c index d6dd131da145b..f83c1604263e9 100644 --- a/src/backend/backup/walsummaryfuncs.c +++ b/src/backend/backup/walsummaryfuncs.c @@ -3,7 +3,7 @@ * walsummaryfuncs.c * SQL-callable functions for accessing WAL summary data. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/backend/backup/walsummaryfuncs.c * @@ -12,6 +12,7 @@ #include "postgres.h" +#include "access/htup_details.h" #include "backup/walsummary.h" #include "common/blkreftable.h" #include "funcapi.h" @@ -19,6 +20,7 @@ #include "postmaster/walsummarizer.h" #include "utils/fmgrprotos.h" #include "utils/pg_lsn.h" +#include "utils/tuplestore.h" #define NUM_WS_ATTS 3 #define NUM_SUMMARY_ATTS 6 diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y index 9833f52c1bed6..943ff4733d332 100644 --- a/src/backend/bootstrap/bootparse.y +++ b/src/backend/bootstrap/bootparse.y @@ -4,7 +4,7 @@ * bootparse.y * yacc grammar for the "bootstrap" mode (BKI file format) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -308,7 +308,8 @@ Boot_DeclareIndexStmt: relationId = RangeVarGetRelid(stmt->relation, NoLock, false); - DefineIndex(relationId, + DefineIndex(NULL, + relationId, stmt, $4, InvalidOid, @@ -361,7 +362,8 @@ Boot_DeclareUniqueIndexStmt: relationId = RangeVarGetRelid(stmt->relation, NoLock, false); - DefineIndex(relationId, + DefineIndex(NULL, + relationId, stmt, $5, InvalidOid, @@ -415,6 +417,7 @@ boot_index_param: n->opclass = list_make1(makeString($2)); n->ordering = SORTBY_DEFAULT; n->nulls_ordering = SORTBY_NULLS_DEFAULT; + n->location = -1; $$ = n; } ; diff --git a/src/backend/bootstrap/bootscanner.l b/src/backend/bootstrap/bootscanner.l index 50713912fb149..9674f2795d141 100644 --- a/src/backend/bootstrap/bootscanner.l +++ b/src/backend/bootstrap/bootscanner.l @@ -4,7 +4,7 @@ * bootscanner.l * a lexical scanner for the bootstrap parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index 6db864892d0dd..b0dcd9876c56f 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -4,7 +4,7 @@ * routines to support running postgres in 'bootstrap' mode * bootstrap mode is used to create the initial template database * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -25,16 +25,21 @@ #include "access/xact.h" #include "bootstrap/bootstrap.h" #include "catalog/index.h" +#include "catalog/pg_authid.h" #include "catalog/pg_collation.h" +#include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "common/link-canary.h" #include "miscadmin.h" #include "nodes/makefuncs.h" -#include "pg_getopt.h" +#include "port/pg_getopt_ctx.h" #include "postmaster/postmaster.h" #include "storage/bufpage.h" +#include "storage/checksum.h" +#include "storage/fd.h" #include "storage/ipc.h" #include "storage/proc.h" +#include "storage/shmem_internal.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/guc.h" @@ -46,6 +51,7 @@ static void CheckerModeMain(void); static void bootstrap_signals(void); static Form_pg_attribute AllocateAttribute(void); +static void InsertOneProargdefaultsValue(char *value); static void populate_typ_list(void); static Oid gettype(char *type); static void cleanup(void); @@ -91,34 +97,30 @@ static const struct typinfo TypInfo[] = { F_BYTEAIN, F_BYTEAOUT}, {"char", CHAROID, 0, 1, true, TYPALIGN_CHAR, TYPSTORAGE_PLAIN, InvalidOid, F_CHARIN, F_CHAROUT}, + {"cstring", CSTRINGOID, 0, -2, false, TYPALIGN_CHAR, TYPSTORAGE_PLAIN, InvalidOid, + F_CSTRING_IN, F_CSTRING_OUT}, {"int2", INT2OID, 0, 2, true, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid, F_INT2IN, F_INT2OUT}, {"int4", INT4OID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, F_INT4IN, F_INT4OUT}, + {"int8", INT8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid, + F_INT8IN, F_INT8OUT}, {"float4", FLOAT4OID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, F_FLOAT4IN, F_FLOAT4OUT}, + {"float8", FLOAT8OID, 0, 8, true, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid, + F_FLOAT8IN, F_FLOAT8OUT}, {"name", NAMEOID, CHAROID, NAMEDATALEN, false, TYPALIGN_CHAR, TYPSTORAGE_PLAIN, C_COLLATION_OID, F_NAMEIN, F_NAMEOUT}, - {"regclass", REGCLASSOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, - F_REGCLASSIN, F_REGCLASSOUT}, {"regproc", REGPROCOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, F_REGPROCIN, F_REGPROCOUT}, - {"regtype", REGTYPEOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, - F_REGTYPEIN, F_REGTYPEOUT}, - {"regrole", REGROLEOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, - F_REGROLEIN, F_REGROLEOUT}, - {"regnamespace", REGNAMESPACEOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, - F_REGNAMESPACEIN, F_REGNAMESPACEOUT}, {"text", TEXTOID, 0, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, DEFAULT_COLLATION_OID, F_TEXTIN, F_TEXTOUT}, + {"jsonb", JSONBOID, 0, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, + F_JSONB_IN, F_JSONB_OUT}, {"oid", OIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, F_OIDIN, F_OIDOUT}, - {"tid", TIDOID, 0, 6, false, TYPALIGN_SHORT, TYPSTORAGE_PLAIN, InvalidOid, - F_TIDIN, F_TIDOUT}, - {"xid", XIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, - F_XIDIN, F_XIDOUT}, - {"cid", CIDOID, 0, 4, true, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, - F_CIDIN, F_CIDOUT}, + {"aclitem", ACLITEMOID, 0, 16, false, TYPALIGN_DOUBLE, TYPSTORAGE_PLAIN, InvalidOid, + F_ACLITEMIN, F_ACLITEMOUT}, {"pg_node_tree", PG_NODE_TREEOID, 0, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, DEFAULT_COLLATION_OID, F_PG_NODE_TREE_IN, F_PG_NODE_TREE_OUT}, {"int2vector", INT2VECTOROID, INT2OID, -1, false, TYPALIGN_INT, TYPSTORAGE_PLAIN, InvalidOid, @@ -127,13 +129,13 @@ static const struct typinfo TypInfo[] = { F_OIDVECTORIN, F_OIDVECTOROUT}, {"_int4", INT4ARRAYOID, INT4OID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, F_ARRAY_IN, F_ARRAY_OUT}, - {"_text", 1009, TEXTOID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, DEFAULT_COLLATION_OID, + {"_text", TEXTARRAYOID, TEXTOID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, DEFAULT_COLLATION_OID, F_ARRAY_IN, F_ARRAY_OUT}, - {"_oid", 1028, OIDOID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, + {"_oid", OIDARRAYOID, OIDOID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, F_ARRAY_IN, F_ARRAY_OUT}, - {"_char", 1002, CHAROID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, + {"_char", CHARARRAYOID, CHAROID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, F_ARRAY_IN, F_ARRAY_OUT}, - {"_aclitem", 1034, ACLITEMOID, -1, false, TYPALIGN_INT, TYPSTORAGE_EXTENDED, InvalidOid, + {"_aclitem", ACLITEMARRAYOID, ACLITEMOID, -1, false, TYPALIGN_DOUBLE, TYPSTORAGE_EXTENDED, InvalidOid, F_ARRAY_IN, F_ARRAY_OUT} }; @@ -148,6 +150,43 @@ struct typmap static List *Typ = NIL; /* List of struct typmap* */ static struct typmap *Ap = NULL; +/* + * Basic information about built-in roles. + * + * Presently, this need only list roles that are mentioned in aclitem arrays + * in the catalog .dat files. We might as well list everything that is in + * pg_authid.dat, since there aren't that many. Like pg_authid.dat, we + * represent the bootstrap superuser's name as "POSTGRES", even though it + * (probably) won't be that in the finished installation; this means aclitem + * entries in .dat files must spell it like that. + */ +struct rolinfo +{ + const char *rolname; + Oid oid; +}; + +static const struct rolinfo RolInfo[] = { + {"POSTGRES", BOOTSTRAP_SUPERUSERID}, + {"pg_database_owner", ROLE_PG_DATABASE_OWNER}, + {"pg_read_all_data", ROLE_PG_READ_ALL_DATA}, + {"pg_write_all_data", ROLE_PG_WRITE_ALL_DATA}, + {"pg_monitor", ROLE_PG_MONITOR}, + {"pg_read_all_settings", ROLE_PG_READ_ALL_SETTINGS}, + {"pg_read_all_stats", ROLE_PG_READ_ALL_STATS}, + {"pg_stat_scan_tables", ROLE_PG_STAT_SCAN_TABLES}, + {"pg_read_server_files", ROLE_PG_READ_SERVER_FILES}, + {"pg_write_server_files", ROLE_PG_WRITE_SERVER_FILES}, + {"pg_execute_server_program", ROLE_PG_EXECUTE_SERVER_PROGRAM}, + {"pg_signal_backend", ROLE_PG_SIGNAL_BACKEND}, + {"pg_checkpoint", ROLE_PG_CHECKPOINT}, + {"pg_maintain", ROLE_PG_MAINTAIN}, + {"pg_use_reserved_connections", ROLE_PG_USE_RESERVED_CONNECTIONS}, + {"pg_create_subscription", ROLE_PG_CREATE_SUBSCRIPTION}, + {"pg_signal_autovacuum_worker", ROLE_PG_SIGNAL_AUTOVACUUM_WORKER} +}; + + static Datum values[MAXATTR]; /* current row's attribute values */ static bool Nulls[MAXATTR]; @@ -199,9 +238,10 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) { int i; char *progname = argv[0]; + pg_getopt_ctx optctx; int flag; char *userDoption = NULL; - uint32 bootstrap_data_checksum_version = 0; /* No checksum */ + uint32 bootstrap_data_checksum_version = PG_DATA_CHECKSUM_OFF; yyscan_t scanner; Assert(!IsUnderPostmaster); @@ -218,12 +258,13 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) argv++; argc--; - while ((flag = getopt(argc, argv, "B:c:d:D:Fkr:X:-:")) != -1) + pg_getopt_start(&optctx, argc, argv, "B:c:d:D:Fkr:X:-:"); + while ((flag = pg_getopt_next(&optctx)) != -1) { switch (flag) { case 'B': - SetConfigOption("shared_buffers", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("shared_buffers", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case '-': @@ -233,30 +274,30 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) * returns DISPATCH_POSTMASTER if it doesn't find a match, so * error for anything else. */ - if (parse_dispatch_option(optarg) != DISPATCH_POSTMASTER) + if (parse_dispatch_option(optctx.optarg) != DISPATCH_POSTMASTER) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("--%s must be first argument", optarg))); + errmsg("--%s must be first argument", optctx.optarg))); - /* FALLTHROUGH */ + pg_fallthrough; case 'c': { char *name, *value; - ParseLongOption(optarg, &name, &value); + ParseLongOption(optctx.optarg, &name, &value); if (!value) { if (flag == '-') ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("--%s requires a value", - optarg))); + optctx.optarg))); else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("-c %s requires a value", - optarg))); + optctx.optarg))); } SetConfigOption(name, value, PGC_POSTMASTER, PGC_S_ARGV); @@ -265,14 +306,14 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) break; } case 'D': - userDoption = pstrdup(optarg); + userDoption = pstrdup(optctx.optarg); break; case 'd': { /* Turn on debugging for the bootstrap process. */ char *debugstr; - debugstr = psprintf("debug%s", optarg); + debugstr = psprintf("debug%s", optctx.optarg); SetConfigOption("log_min_messages", debugstr, PGC_POSTMASTER, PGC_S_ARGV); SetConfigOption("client_min_messages", debugstr, @@ -287,10 +328,10 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) bootstrap_data_checksum_version = PG_DATA_CHECKSUM_VERSION; break; case 'r': - strlcpy(OutputFileName, optarg, MAXPGPATH); + strlcpy(OutputFileName, optctx.optarg, MAXPGPATH); break; case 'X': - SetConfigOption("wal_segment_size", optarg, PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT); + SetConfigOption("wal_segment_size", optctx.optarg, PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT); break; default: write_stderr("Try \"%s --help\" for more information.\n", @@ -300,7 +341,7 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) } } - if (argc != optind) + if (argc != optctx.optind) { write_stderr("%s: invalid command-line arguments\n", progname); proc_exit(1); @@ -322,6 +363,8 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) SetProcessingMode(BootstrapProcessing); IgnoreSystemIndexes = true; + RegisterBuiltinShmemCallbacks(); + InitializeMaxBackends(); /* @@ -333,6 +376,7 @@ BootstrapModeMain(int argc, char *argv[], bool check_only) InitializeFastPathLocks(); + ShmemCallRequestCallbacks(); CreateSharedMemoryAndSemaphores(); /* @@ -419,10 +463,10 @@ bootstrap_signals(void) * mode; "curl up and die" is a sufficient response for all these cases. * Let's set that handling explicitly, as documentation if nothing else. */ - pqsignal(SIGHUP, SIG_DFL); - pqsignal(SIGINT, SIG_DFL); - pqsignal(SIGTERM, SIG_DFL); - pqsignal(SIGQUIT, SIG_DFL); + pqsignal(SIGHUP, PG_SIG_DFL); + pqsignal(SIGINT, PG_SIG_DFL); + pqsignal(SIGTERM, PG_SIG_DFL); + pqsignal(SIGQUIT, PG_SIG_DFL); } /* ---------------------------------------------------------------- @@ -656,6 +700,7 @@ InsertOneTuple(void) void InsertOneValue(char *value, int i) { + Form_pg_attribute attr; Oid typoid; int16 typlen; bool typbyval; @@ -664,19 +709,42 @@ InsertOneValue(char *value, int i) Oid typioparam; Oid typinput; Oid typoutput; + Oid typcollation; Assert(i >= 0 && i < MAXATTR); elog(DEBUG4, "inserting column %d value \"%s\"", i, value); - typoid = TupleDescAttr(boot_reldesc->rd_att, i)->atttypid; + attr = TupleDescAttr(RelationGetDescr(boot_reldesc), i); + typoid = attr->atttypid; boot_get_type_io_data(typoid, &typlen, &typbyval, &typalign, &typdelim, &typioparam, - &typinput, &typoutput); + &typinput, &typoutput, + &typcollation); - values[i] = OidInputFunctionCall(typinput, value, typioparam, -1); + /* + * pg_node_tree values can't be inserted normally (pg_node_tree_in would + * just error out), so provide special cases for such columns that we + * would like to fill during bootstrap. + */ + if (typoid == PG_NODE_TREEOID) + { + /* pg_proc.proargdefaults */ + if (RelationGetRelid(boot_reldesc) == ProcedureRelationId && + i == Anum_pg_proc_proargdefaults - 1) + InsertOneProargdefaultsValue(value); + else /* maybe other cases later */ + elog(ERROR, "can't handle pg_node_tree input for %s.%s", + RelationGetRelationName(boot_reldesc), + NameStr(attr->attname)); + } + else + { + /* Normal case */ + values[i] = OidInputFunctionCall(typinput, value, typioparam, -1); + } /* * We use ereport not elog here so that parameters aren't evaluated unless @@ -687,6 +755,111 @@ InsertOneValue(char *value, int i) OidOutputFunctionCall(typoutput, values[i])))); } +/* ---------------- + * InsertOneProargdefaultsValue + * + * In general, proargdefaults can be a list of any expressions, but + * for bootstrap we only support a list of Const nodes. The input + * has the form of a text array, and we feed non-null elements to the + * typinput functions for the appropriate parameters. + * ---------------- + */ +static void +InsertOneProargdefaultsValue(char *value) +{ + int pronargs; + oidvector *proargtypes; + Datum arrayval; + Datum *array_datums; + bool *array_nulls; + int array_count; + List *proargdefaults; + char *nodestring; + + /* The pg_proc columns we need to use must have been filled already */ + StaticAssertDecl(Anum_pg_proc_pronargs < Anum_pg_proc_proargdefaults, + "pronargs must come before proargdefaults"); + StaticAssertDecl(Anum_pg_proc_pronargdefaults < Anum_pg_proc_proargdefaults, + "pronargdefaults must come before proargdefaults"); + StaticAssertDecl(Anum_pg_proc_proargtypes < Anum_pg_proc_proargdefaults, + "proargtypes must come before proargdefaults"); + if (Nulls[Anum_pg_proc_pronargs - 1]) + elog(ERROR, "pronargs must not be null"); + if (Nulls[Anum_pg_proc_proargtypes - 1]) + elog(ERROR, "proargtypes must not be null"); + pronargs = DatumGetInt16(values[Anum_pg_proc_pronargs - 1]); + proargtypes = DatumGetPointer(values[Anum_pg_proc_proargtypes - 1]); + Assert(pronargs == proargtypes->dim1); + + /* Parse the input string as an array value, then deconstruct to Datums */ + arrayval = OidFunctionCall3(F_ARRAY_IN, + CStringGetDatum(value), + ObjectIdGetDatum(CSTRINGOID), + Int32GetDatum(-1)); + deconstruct_array_builtin(DatumGetArrayTypeP(arrayval), CSTRINGOID, + &array_datums, &array_nulls, &array_count); + + /* The values should correspond to the last N argtypes */ + if (array_count > pronargs) + elog(ERROR, "too many proargdefaults entries"); + + /* Build the List of Const nodes */ + proargdefaults = NIL; + for (int i = 0; i < array_count; i++) + { + Oid argtype = proargtypes->values[pronargs - array_count + i]; + int16 typlen; + bool typbyval; + char typalign; + char typdelim; + Oid typioparam; + Oid typinput; + Oid typoutput; + Oid typcollation; + Datum defval; + bool defnull; + Const *defConst; + + boot_get_type_io_data(argtype, + &typlen, &typbyval, &typalign, + &typdelim, &typioparam, + &typinput, &typoutput, + &typcollation); + + defnull = array_nulls[i]; + if (defnull) + defval = (Datum) 0; + else + defval = OidInputFunctionCall(typinput, + DatumGetCString(array_datums[i]), + typioparam, -1); + + defConst = makeConst(argtype, + -1, /* never any typmod */ + typcollation, + typlen, + defval, + defnull, + typbyval); + proargdefaults = lappend(proargdefaults, defConst); + } + + /* + * Flatten the List to a node-tree string, then convert to a text datum, + * which is the storage representation of pg_node_tree. + */ + nodestring = nodeToString(proargdefaults); + values[Anum_pg_proc_proargdefaults - 1] = CStringGetTextDatum(nodestring); + Nulls[Anum_pg_proc_proargdefaults - 1] = false; + + /* + * Hack: fill in pronargdefaults with the right value. This is surely + * ugly, but it beats making the programmer do it. + */ + values[Anum_pg_proc_pronargdefaults - 1] = Int16GetDatum(array_count); + Nulls[Anum_pg_proc_pronargdefaults - 1] = false; +} + /* ---------------- * InsertOneNull * ---------------- @@ -740,7 +913,7 @@ populate_typ_list(void) Form_pg_type typForm = (Form_pg_type) GETSTRUCT(tup); struct typmap *newtyp; - newtyp = (struct typmap *) palloc(sizeof(struct typmap)); + newtyp = palloc_object(struct typmap); Typ = lappend(Typ, newtyp); newtyp->am_oid = typForm->oid; @@ -827,10 +1000,11 @@ gettype(char *type) * boot_get_type_io_data * * Obtain type I/O information at bootstrap time. This intentionally has - * almost the same API as lsyscache.c's get_type_io_data, except that + * an API very close to that of lsyscache.c's get_type_io_data, except that * we only support obtaining the typinput and typoutput routines, not - * the binary I/O routines. It is exported so that array_in and array_out - * can be made to work during early bootstrap. + * the binary I/O routines, and we also return the type's collation. + * This is exported so that array_in and array_out can be made to work + * during early bootstrap. * ---------------- */ void @@ -841,7 +1015,8 @@ boot_get_type_io_data(Oid typid, char *typdelim, Oid *typioparam, Oid *typinput, - Oid *typoutput) + Oid *typoutput, + Oid *typcollation) { if (Typ != NIL) { @@ -872,6 +1047,8 @@ boot_get_type_io_data(Oid typid, *typinput = ap->am_typ.typinput; *typoutput = ap->am_typ.typoutput; + + *typcollation = ap->am_typ.typcollation; } else { @@ -900,7 +1077,28 @@ boot_get_type_io_data(Oid typid, *typinput = TypInfo[typeindex].inproc; *typoutput = TypInfo[typeindex].outproc; + + *typcollation = TypInfo[typeindex].collation; + } +} + +/* ---------------- + * boot_get_role_oid + * + * Look up a role name at bootstrap time. This is equivalent to + * get_role_oid(rolname, true): return the role OID or InvalidOid if + * not found. We only need to cope with built-in role names. + * ---------------- + */ +Oid +boot_get_role_oid(const char *rolname) +{ + for (int i = 0; i < lengthof(RolInfo); i++) + { + if (strcmp(RolInfo[i].rolname, rolname) == 0) + return RolInfo[i].oid; } + return InvalidOid; } /* ---------------- @@ -949,10 +1147,10 @@ index_register(Oid heap, oldcxt = MemoryContextSwitchTo(nogc); - newind = (IndexList *) palloc(sizeof(IndexList)); + newind = palloc_object(IndexList); newind->il_heap = heap; newind->il_ind = ind; - newind->il_info = (IndexInfo *) palloc(sizeof(IndexInfo)); + newind->il_info = palloc_object(IndexInfo); memcpy(newind->il_info, indexInfo, sizeof(IndexInfo)); /* expressions will likely be null, but may as well copy it */ @@ -990,7 +1188,7 @@ build_indices(void) heap = table_open(ILHead->il_heap, NoLock); ind = index_open(ILHead->il_ind, NoLock); - index_build(heap, ind, ILHead->il_info, false, false); + index_build(heap, ind, ILHead->il_info, false, false, false); index_close(ind, NoLock); table_close(heap, NoLock); diff --git a/src/backend/bootstrap/meson.build b/src/backend/bootstrap/meson.build index 29726c1ab4ff1..2f9115fc97ce6 100644 --- a/src/backend/bootstrap/meson.build +++ b/src/backend/bootstrap/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'bootstrap.c') diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm index 5a912549b82c0..219af5884d99a 100644 --- a/src/backend/catalog/Catalog.pm +++ b/src/backend/catalog/Catalog.pm @@ -4,7 +4,7 @@ # Perl module that extracts info from catalog files into Perl # data structures # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/catalog/Catalog.pm diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index c090094ed08d5..26fa0c9b18c32 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -2,7 +2,7 @@ # # Makefile for backend/catalog # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/catalog/Makefile @@ -44,6 +44,7 @@ OBJS = \ pg_range.o \ pg_shdepend.o \ pg_subscription.o \ + pg_tablespace.o \ pg_type.o \ storage.o \ toasting.o diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 9ca8a88dc9104..67424fe3b0c83 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -3,7 +3,7 @@ * aclchk.c * Routines to check access control permissions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -64,7 +64,6 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -99,6 +98,7 @@ typedef struct AclMode privileges; List *grantees; bool grant_option; + RoleSpec *grantor; DropBehavior behavior; } InternalDefaultACL; @@ -291,6 +291,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs, case OBJECT_PARAMETER_ACL: whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_PROPGRAPH: + whole_mask = ACL_ALL_RIGHTS_PROPGRAPH; + break; default: elog(ERROR, "unrecognized object type: %d", objtype); /* not reached, but keep compiler quiet */ @@ -396,22 +399,6 @@ ExecuteGrantStmt(GrantStmt *stmt) const char *errormsg; AclMode all_privileges; - if (stmt->grantor) - { - Oid grantor; - - grantor = get_rolespec_oid(stmt->grantor, false); - - /* - * Currently, this clause is only for SQL compatibility, not very - * interesting otherwise. - */ - if (grantor != GetUserId()) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("grantor must be current user"))); - } - /* * Turn the regular GrantStmt into the InternalGrant form. */ @@ -439,6 +426,7 @@ ExecuteGrantStmt(GrantStmt *stmt) istmt.col_privs = NIL; /* may get filled below */ istmt.grantees = NIL; /* filled below */ istmt.grant_option = stmt->grant_option; + istmt.grantor = stmt->grantor; istmt.behavior = stmt->behavior; /* @@ -535,6 +523,10 @@ ExecuteGrantStmt(GrantStmt *stmt) all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL; errormsg = gettext_noop("invalid privilege type %s for parameter"); break; + case OBJECT_PROPGRAPH: + all_privileges = ACL_ALL_RIGHTS_PROPGRAPH; + errormsg = gettext_noop("invalid privilege type %s for property graph"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) stmt->objtype); @@ -581,7 +573,7 @@ ExecuteGrantStmt(GrantStmt *stmt) elog(ERROR, "AccessPriv node must specify privilege or columns"); priv = string_to_privilege(privnode->priv_name); - if (priv & ~((AclMode) all_privileges)) + if (priv & ~all_privileges) ereport(ERROR, (errcode(ERRCODE_INVALID_GRANT_OPERATION), errmsg(errormsg, privilege_to_string(priv)))); @@ -605,6 +597,7 @@ ExecGrantStmt_oids(InternalGrant *istmt) { case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_PROPGRAPH: ExecGrant_Relation(istmt); break; case OBJECT_DATABASE: @@ -659,6 +652,20 @@ ExecGrantStmt_oids(InternalGrant *istmt) * objectNamesToOids * * Turn a list of object names of a given type into an Oid list. + * + * XXX This function intentionally takes only an AccessShareLock. In the face + * of concurrent DDL, we might easily latch onto an old version of an object, + * causing the GRANT or REVOKE statement to fail. But it does prevent the + * object from disappearing altogether. To do better, we would need to use a + * self-exclusive lock, perhaps ShareUpdateExclusiveLock, here and before + * *every* CatalogTupleUpdate() of a row that GRANT/REVOKE can affect. + * Besides that additional work, this could have operational costs. For + * example, it would make GRANT ALL TABLES IN SCHEMA terminate every + * autovacuum running in the schema and consume a shared lock table entry per + * table in the schema. The user-visible benefit of that additional work is + * just changing "ERROR: tuple concurrently updated" to blocking. That's not + * nothing, but it might not outweigh autovacuum termination and lock table + * consumption spikes. */ static List * objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) @@ -687,6 +694,7 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant) case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_PROPGRAPH: /* * Here, we don't use get_object_address(). It requires that the @@ -804,6 +812,10 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames) objs = getRelationsInNamespace(namespaceId, RELKIND_SEQUENCE); objects = list_concat(objects, objs); break; + case OBJECT_PROPGRAPH: + objs = getRelationsInNamespace(namespaceId, RELKIND_PROPGRAPH); + objects = list_concat(objects, objs); + break; case OBJECT_FUNCTION: case OBJECT_PROCEDURE: case OBJECT_ROUTINE: @@ -947,6 +959,7 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s /* privileges to be filled below */ iacls.grantees = NIL; /* filled below */ iacls.grant_option = action->grant_option; + iacls.grantor = action->grantor; iacls.behavior = action->behavior; /* @@ -1009,6 +1022,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s all_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; errormsg = gettext_noop("invalid privilege type %s for large object"); break; + case OBJECT_PROPGRAPH: + all_privileges = ACL_ALL_RIGHTS_PROPGRAPH; + errormsg = gettext_noop("invalid privilege type %s for property graph"); + break; default: elog(ERROR, "unrecognized GrantStmt.objtype: %d", (int) action->objtype); @@ -1046,7 +1063,7 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s elog(ERROR, "AccessPriv node must specify privilege"); priv = string_to_privilege(privnode->priv_name); - if (priv & ~((AclMode) all_privileges)) + if (priv & ~all_privileges) ereport(ERROR, (errcode(ERRCODE_INVALID_GRANT_OPERATION), errmsg(errormsg, privilege_to_string(priv)))); @@ -1194,7 +1211,8 @@ SetDefaultACL(InternalDefaultACL *iacls) if (OidIsValid(iacls->nspid)) ereport(ERROR, (errcode(ERRCODE_INVALID_GRANT_OPERATION), - errmsg("cannot use IN SCHEMA clause when using GRANT/REVOKE ON SCHEMAS"))); + errmsg("cannot use IN SCHEMA clause when using %s", + "GRANT/REVOKE ON SCHEMAS"))); objtype = DEFACLOBJ_NAMESPACE; if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_SCHEMA; @@ -1204,7 +1222,8 @@ SetDefaultACL(InternalDefaultACL *iacls) if (OidIsValid(iacls->nspid)) ereport(ERROR, (errcode(ERRCODE_INVALID_GRANT_OPERATION), - errmsg("cannot use IN SCHEMA clause when using GRANT/REVOKE ON LARGE OBJECTS"))); + errmsg("cannot use IN SCHEMA clause when using %s", + "GRANT/REVOKE ON LARGE OBJECTS"))); objtype = DEFACLOBJ_LARGEOBJECT; if (iacls->all_privs && this_privileges == ACL_NO_RIGHTS) this_privileges = ACL_ALL_RIGHTS_LARGEOBJECT; @@ -1471,6 +1490,7 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) iacls.privileges = ACL_NO_RIGHTS; iacls.grantees = list_make1_oid(roleid); iacls.grant_option = false; + iacls.grantor = NULL; iacls.behavior = DROP_CASCADE; /* Do it */ @@ -1527,6 +1547,7 @@ RemoveRoleFromObjectACL(Oid roleid, Oid classid, Oid objid) istmt.col_privs = NIL; istmt.grantees = list_make1_oid(roleid); istmt.grant_option = false; + istmt.grantor = NULL; istmt.behavior = DROP_CASCADE; ExecGrantStmt_oids(&istmt); @@ -1681,7 +1702,7 @@ ExecGrant_Attribute(InternalGrant *istmt, Oid relOid, const char *relname, merged_acl = aclconcat(old_rel_acl, old_acl); /* Determine ID to do the grant as, and available grant options */ - select_best_grantor(GetUserId(), col_privileges, + select_best_grantor(istmt->grantor, col_privileges, merged_acl, ownerId, &grantorId, &avail_goptions); @@ -1821,11 +1842,20 @@ ExecGrant_Relation(InternalGrant *istmt) errmsg("\"%s\" is not a sequence", NameStr(pg_class_tuple->relname)))); + if (istmt->objtype == OBJECT_PROPGRAPH && + pg_class_tuple->relkind != RELKIND_PROPGRAPH) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a property graph", + NameStr(pg_class_tuple->relname)))); + /* Adjust the default permissions based on object type */ if (istmt->all_privs && istmt->privileges == ACL_NO_RIGHTS) { if (pg_class_tuple->relkind == RELKIND_SEQUENCE) this_privileges = ACL_ALL_RIGHTS_SEQUENCE; + else if (pg_class_tuple->relkind == RELKIND_PROPGRAPH) + this_privileges = ACL_ALL_RIGHTS_PROPGRAPH; else this_privileges = ACL_ALL_RIGHTS_RELATION; } @@ -1919,6 +1949,9 @@ ExecGrant_Relation(InternalGrant *istmt) case RELKIND_SEQUENCE: old_acl = acldefault(OBJECT_SEQUENCE, ownerId); break; + case RELKIND_PROPGRAPH: + old_acl = acldefault(OBJECT_PROPGRAPH, ownerId); + break; default: old_acl = acldefault(OBJECT_TABLE, ownerId); break; @@ -1954,7 +1987,7 @@ ExecGrant_Relation(InternalGrant *istmt) ObjectType objtype; /* Determine ID to do the grant as, and available grant options */ - select_best_grantor(GetUserId(), this_privileges, + select_best_grantor(istmt->grantor, this_privileges, old_acl, ownerId, &grantorId, &avail_goptions); @@ -2100,7 +2133,7 @@ static void ExecGrant_common(InternalGrant *istmt, Oid classid, AclMode default_privs, void (*object_check) (InternalGrant *istmt, HeapTuple tuple)) { - int cacheid; + SysCacheIdentifier cacheid; Relation relation; ListCell *cell; @@ -2169,7 +2202,7 @@ ExecGrant_common(InternalGrant *istmt, Oid classid, AclMode default_privs, } /* Determine ID to do the grant as, and available grant options */ - select_best_grantor(GetUserId(), istmt->privileges, + select_best_grantor(istmt->grantor, istmt->privileges, old_acl, ownerId, &grantorId, &avail_goptions); @@ -2324,7 +2357,7 @@ ExecGrant_Largeobject(InternalGrant *istmt) } /* Determine ID to do the grant as, and available grant options */ - select_best_grantor(GetUserId(), istmt->privileges, + select_best_grantor(istmt->grantor, istmt->privileges, old_acl, ownerId, &grantorId, &avail_goptions); @@ -2470,7 +2503,7 @@ ExecGrant_Parameter(InternalGrant *istmt) } /* Determine ID to do the grant as, and available grant options */ - select_best_grantor(GetUserId(), istmt->privileges, + select_best_grantor(istmt->grantor, istmt->privileges, old_acl, ownerId, &grantorId, &avail_goptions); @@ -2716,6 +2749,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_PROCEDURE: msg = gettext_noop("permission denied for procedure %s"); break; + case OBJECT_PROPGRAPH: + msg = gettext_noop("permission denied for property graph %s"); + break; case OBJECT_PUBLICATION: msg = gettext_noop("permission denied for publication %s"); break; @@ -2842,6 +2878,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_PROCEDURE: msg = gettext_noop("must be owner of procedure %s"); break; + case OBJECT_PROPGRAPH: + msg = gettext_noop("must be owner of property graph %s"); + break; case OBJECT_PUBLICATION: msg = gettext_noop("must be owner of publication %s"); break; @@ -2978,6 +3017,7 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid, pg_attribute_aclmask(object_oid, attnum, roleid, mask, how); case OBJECT_TABLE: case OBJECT_SEQUENCE: + case OBJECT_PROPGRAPH: return pg_class_aclmask(object_oid, roleid, mask, how); case OBJECT_DATABASE: return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how); @@ -3043,7 +3083,7 @@ object_aclmask_ext(Oid classid, Oid objectid, Oid roleid, AclMode mask, AclMaskHow how, bool *is_missing) { - int cacheid; + SysCacheIdentifier cacheid; AclMode result; HeapTuple tuple; Datum aclDatum; @@ -3112,7 +3152,7 @@ object_aclmask_ext(Oid classid, Oid objectid, Oid roleid, result = aclmask(acl, roleid, ownerId, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(tuple); @@ -3242,7 +3282,7 @@ pg_attribute_aclmask_ext(Oid table_oid, AttrNumber attnum, Oid roleid, result = aclmask(acl, roleid, ownerId, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(attTuple); @@ -3349,7 +3389,7 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask, result = aclmask(acl, roleid, ownerId, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(tuple); @@ -3441,7 +3481,7 @@ pg_parameter_aclmask(const char *name, Oid roleid, AclMode mask, AclMaskHow how) result = aclmask(acl, roleid, BOOTSTRAP_SUPERUSERID, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(tuple); @@ -3496,7 +3536,7 @@ pg_parameter_acl_aclmask(Oid acl_oid, Oid roleid, AclMode mask, AclMaskHow how) result = aclmask(acl, roleid, BOOTSTRAP_SUPERUSERID, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(tuple); @@ -3576,13 +3616,31 @@ pg_largeobject_aclmask_snapshot(Oid lobj_oid, Oid roleid, result = aclmask(acl, roleid, ownerId, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); systable_endscan(scan); table_close(pg_lo_meta, AccessShareLock); + /* + * Check if ACL_SELECT is being checked and, if so, and not set already as + * part of the result, then check if the user has privileges of the + * pg_read_all_data role, which allows read access to all large objects. + */ + if (mask & ACL_SELECT && !(result & ACL_SELECT) && + has_privs_of_role(roleid, ROLE_PG_READ_ALL_DATA)) + result |= ACL_SELECT; + + /* + * Check if ACL_UPDATE is being checked and, if so, and not set already as + * part of the result, then check if the user has privileges of the + * pg_write_all_data role, which allows write access to all large objects. + */ + if (mask & ACL_UPDATE && !(result & ACL_UPDATE) && + has_privs_of_role(roleid, ROLE_PG_WRITE_ALL_DATA)) + result |= ACL_UPDATE; + return result; } @@ -3670,7 +3728,7 @@ pg_namespace_aclmask_ext(Oid nsp_oid, Oid roleid, result = aclmask(acl, roleid, ownerId, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(tuple); @@ -3806,7 +3864,7 @@ pg_type_aclmask_ext(Oid type_oid, Oid roleid, AclMode mask, AclMaskHow how, result = aclmask(acl, roleid, ownerId, mask, how); /* if we have a detoasted copy, free it */ - if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) + if (acl && acl != DatumGetPointer(aclDatum)) pfree(acl); ReleaseSysCache(tuple); @@ -3990,7 +4048,7 @@ pg_attribute_aclcheck_all_ext(Oid table_oid, Oid roleid, attmask = aclmask(acl, roleid, ownerId, mode, ACLMASK_ANY); /* if we have a detoasted copy, free it */ - if ((Pointer) acl != DatumGetPointer(aclDatum)) + if (acl != DatumGetPointer(aclDatum)) pfree(acl); } @@ -4074,7 +4132,7 @@ pg_largeobject_aclcheck_snapshot(Oid lobj_oid, Oid roleid, AclMode mode, bool object_ownercheck(Oid classid, Oid objectid, Oid roleid) { - int cacheid; + SysCacheIdentifier cacheid; Oid ownerId; /* Superusers bypass all permission checking. */ @@ -4086,7 +4144,7 @@ object_ownercheck(Oid classid, Oid objectid, Oid roleid) classid = LargeObjectMetadataRelationId; cacheid = get_object_catcache_oid(classid); - if (cacheid != -1) + if (cacheid != SYSCACHEID_INVALID) { /* we can get the object's tuple from the syscache */ HeapTuple tuple; @@ -4471,7 +4529,7 @@ recordExtObjInitPriv(Oid objoid, Oid classoid) /* This will error on unsupported classoid. */ else if (get_object_attnum_acl(classoid) != InvalidAttrNumber) { - int cacheid; + SysCacheIdentifier cacheid; Datum aclDatum; bool isNull; HeapTuple tuple; @@ -4855,7 +4913,7 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid) ScanKeyData key[3]; SysScanDesc scan; HeapTuple oldtuple; - int cacheid; + SysCacheIdentifier cacheid; HeapTuple objtuple; Oid ownerId; Datum oldAclDatum; diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c index 59caae8f1bc23..7be4903293464 100644 --- a/src/backend/catalog/catalog.c +++ b/src/backend/catalog/catalog.c @@ -5,7 +5,7 @@ * bits of hard-wired knowledge * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 18316a3968bcf..fdb8e67e1f5e6 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -4,7 +4,7 @@ * Routines to support inter-object dependencies. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,6 +22,7 @@ #include "catalog/dependency.h" #include "catalog/heap.h" #include "catalog/index.h" +#include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am.h" #include "catalog/pg_amop.h" @@ -50,6 +51,11 @@ #include "catalog/pg_parameter_acl.h" #include "catalog/pg_policy.h" #include "catalog/pg_proc.h" +#include "catalog/pg_propgraph_element.h" +#include "catalog/pg_propgraph_element_label.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_label_property.h" +#include "catalog/pg_propgraph_property.h" #include "catalog/pg_publication.h" #include "catalog/pg_publication_namespace.h" #include "catalog/pg_publication_rel.h" @@ -320,13 +326,63 @@ performDeletion(const ObjectAddress *object, } /* - * performMultipleDeletions: Similar to performDeletion, but act on multiple + * performDeletionCheck: Check whether a specific object can be safely deleted. + * This function does not perform any deletion; instead, it raises an error + * if the object cannot be deleted due to existing dependencies. + * + * It can be useful when you need to delete some objects later. See comments + * in performDeletion too. + * The behavior must be specified as DROP_RESTRICT. + */ +void +performDeletionCheck(const ObjectAddress *object, + DropBehavior behavior, int flags) +{ + Relation depRel; + ObjectAddresses *targetObjects; + + Assert(behavior == DROP_RESTRICT); + + depRel = table_open(DependRelationId, RowExclusiveLock); + + AcquireDeletionLock(object, 0); + + /* + * Construct a list of objects we want to delete later (ie, the given + * object plus everything directly or indirectly dependent on it). + */ + targetObjects = new_object_addresses(); + + findDependentObjects(object, + DEPFLAG_ORIGINAL, + flags, + NULL, /* empty stack */ + targetObjects, + NULL, /* no pendingObjects */ + &depRel); + + /* + * Check if deletion is allowed. + */ + reportDependentObjects(targetObjects, + behavior, + flags, + object); + + /* And clean up */ + free_object_addresses(targetObjects); + + table_close(depRel, RowExclusiveLock); +} + +/* + * performMultipleDeletions: Similar to performDeletion, but acts on multiple * objects at once. * * The main difference from issuing multiple performDeletion calls is that the * list of objects that would be implicitly dropped, for each object to be * dropped, is the union of the implicit-object list for all objects. This - * makes each check be more relaxed. + * makes each check more relaxed. */ void performMultipleDeletions(const ObjectAddresses *objects, @@ -590,7 +646,7 @@ findDependentObjects(const ObjectAddress *object, break; /* Otherwise, treat this like an internal dependency */ - /* FALL THRU */ + pg_fallthrough; case DEPENDENCY_INTERNAL: @@ -800,8 +856,7 @@ findDependentObjects(const ObjectAddress *object, * regression testing.) */ maxDependentObjects = 128; /* arbitrary initial allocation */ - dependentObjects = (ObjectAddressAndFlags *) - palloc(maxDependentObjects * sizeof(ObjectAddressAndFlags)); + dependentObjects = palloc_array(ObjectAddressAndFlags, maxDependentObjects); numDependentObjects = 0; ScanKeyInit(&key[0], @@ -845,6 +900,17 @@ findDependentObjects(const ObjectAddress *object, object->objectSubId == 0) continue; + /* + * Check that the dependent object is not in a shared catalog, which + * is not supported by doDeletion(). + */ + if (IsSharedRelation(otherObject.classId)) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST), + errmsg("cannot drop %s because %s depends on it", + getObjectDescription(object, false), + getObjectDescription(&otherObject, false)))); + /* * Must lock the dependent object before recursing to it. */ @@ -1188,7 +1254,7 @@ reportDependentObjects(const ObjectAddresses *targetObjects, static void DropObjectById(const ObjectAddress *object) { - int cacheId; + SysCacheIdentifier cacheId; Relation rel; HeapTuple tup; @@ -1453,6 +1519,11 @@ doDeletion(const ObjectAddress *object, int flags) case AccessMethodRelationId: case AccessMethodOperatorRelationId: case AccessMethodProcedureRelationId: + case PropgraphElementRelationId: + case PropgraphElementLabelRelationId: + case PropgraphLabelRelationId: + case PropgraphLabelPropertyRelationId: + case PropgraphPropertyRelationId: case NamespaceRelationId: case TSParserRelationId: case TSDictionaryRelationId: @@ -1554,25 +1625,57 @@ recordDependencyOnExpr(const ObjectAddress *depender, Node *expr, List *rtable, DependencyType behavior) { - find_expr_references_context context; + ObjectAddresses *addrs; - context.addrs = new_object_addresses(); + addrs = new_object_addresses(); - /* Set up interpretation for Vars at varlevelsup = 0 */ - context.rtables = list_make1(rtable); - - /* Scan the expression tree for referenceable objects */ - find_expr_references_walker(expr, &context); + /* Collect all dependencies from the expression */ + collectDependenciesOfExpr(addrs, expr, rtable); - /* Remove any duplicates */ - eliminate_duplicate_dependencies(context.addrs); + /* Remove duplicates */ + eliminate_duplicate_dependencies(addrs); /* And record 'em */ recordMultipleDependencies(depender, - context.addrs->refs, context.addrs->numrefs, + addrs->refs, addrs->numrefs, behavior); - free_object_addresses(context.addrs); + free_object_addresses(addrs); +} + +/* + * collectDependenciesOfExpr - collect expression dependencies + * + * This function analyzes an expression or query in node-tree form to + * find all the objects it refers to (tables, columns, operators, + * functions, etc.) and adds them to the provided ObjectAddresses + * structure. Unlike recordDependencyOnExpr, this function does not + * immediately record the dependencies, allowing the caller to add to, + * filter, or modify the collected dependencies before recording them. + * + * rtable is the rangetable to be used to interpret Vars with varlevelsup=0. + * It can be NIL if no such variables are expected. + * + * Note: the returned list may well contain duplicates. The caller should + * de-duplicate before recording the dependencies. Within this file, callers + * must call eliminate_duplicate_dependencies(). External callers typically + * go through record_object_address_dependencies() which will see to that. + * This choice allows collecting dependencies from multiple sources without + * redundant de-duplication work. + */ +void +collectDependenciesOfExpr(ObjectAddresses *addrs, + Node *expr, List *rtable) +{ + find_expr_references_context context; + + context.addrs = addrs; + + /* Set up interpretation for Vars at varlevelsup = 0 */ + context.rtables = list_make1(rtable); + + /* Scan the expression tree for referenceable objects */ + find_expr_references_walker(expr, &context); } /* @@ -1850,6 +1953,17 @@ find_expr_references_walker(Node *node, errmsg("constant of the type %s cannot be used here", "regrole"))); break; + + /* + * Dependencies for regdatabase should be shared among all + * databases, so explicitly inhibit to have dependencies. + */ + case REGDATABASEOID: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("constant of the type %s cannot be used here", + "regdatabase"))); + break; } } return false; @@ -2163,6 +2277,7 @@ find_expr_references_walker(Node *node, switch (rte->rtekind) { case RTE_RELATION: + case RTE_GRAPH_TABLE: add_object_address(RelationRelationId, rte->relid, 0, context->addrs); break; @@ -2391,6 +2506,75 @@ process_function_rte_ref(RangeTblEntry *rte, AttrNumber attnum, attnum, rte->eref->aliasname))); } +/* + * find_temp_object - search an array of dependency references for temp objects + * + * Scan an ObjectAddresses array for references to temporary objects (objects + * in temporary namespaces), ignoring those in our own temp namespace if + * local_temp_okay is true. If one is found, return true after storing its + * address in *foundobj. + * + * Current callers only use this to deliver helpful notices, so reporting + * one such object seems sufficient. We return the first one, which should + * be a stable result for a given query since it depends only on the order + * in which this module searches query trees. (However, it's important to + * call this before de-duplicating the objects, else OID order would affect + * the result.) + */ +bool +find_temp_object(const ObjectAddresses *addrs, bool local_temp_okay, + ObjectAddress *foundobj) +{ + for (int i = 0; i < addrs->numrefs; i++) + { + const ObjectAddress *thisobj = addrs->refs + i; + Oid objnamespace; + + /* + * Use get_object_namespace() to see if this object belongs to a + * schema. If not, we can skip it. + */ + objnamespace = get_object_namespace(thisobj); + + /* + * If the object is in a temporary namespace, complain, except if + * local_temp_okay and it's our own temp namespace. + */ + if (OidIsValid(objnamespace) && isAnyTempNamespace(objnamespace) && + !(local_temp_okay && isTempNamespace(objnamespace))) + { + *foundobj = *thisobj; + return true; + } + } + return false; +} + +/* + * query_uses_temp_object - convenience wrapper for find_temp_object + * + * If the Query includes any use of a temporary object, fill *temp_object + * with the address of one such object and return true. + */ +bool +query_uses_temp_object(Query *query, ObjectAddress *temp_object) +{ + bool result; + ObjectAddresses *addrs; + + addrs = new_object_addresses(); + + /* Collect all dependencies from the Query */ + collectDependenciesOfExpr(addrs, (Node *) query, NIL); + + /* Look for one that is temp */ + result = find_temp_object(addrs, false, temp_object); + + free_object_addresses(addrs); + + return result; +} + /* * Given an array of dependency references, eliminate any duplicates. */ @@ -2503,12 +2687,11 @@ new_object_addresses(void) { ObjectAddresses *addrs; - addrs = palloc(sizeof(ObjectAddresses)); + addrs = palloc_object(ObjectAddresses); addrs->numrefs = 0; addrs->maxrefs = 32; - addrs->refs = (ObjectAddress *) - palloc(addrs->maxrefs * sizeof(ObjectAddress)); + addrs->refs = palloc_array(ObjectAddress, addrs->maxrefs); addrs->extras = NULL; /* until/unless needed */ return addrs; diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl index df3231fcd41c2..86f3135f9c79e 100644 --- a/src/backend/catalog/genbki.pl +++ b/src/backend/catalog/genbki.pl @@ -6,7 +6,7 @@ # headers from specially formatted header files and data files. # postgres.bki is used to initialize the postgres template database. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/catalog/genbki.pl @@ -795,9 +795,12 @@ # Now generate syscache info print_boilerplate($syscache_ids_fh, "syscache_ids.h", "SysCache identifiers"); -print $syscache_ids_fh "enum SysCacheIdentifier +print $syscache_ids_fh "#ifndef SYSCACHE_IDS_H +#define SYSCACHE_IDS_H + +typedef enum SysCacheIdentifier { -"; +\tSYSCACHEID_INVALID = -1,\n"; print_boilerplate($syscache_info_fh, "syscache_info.h", "SysCache definitions"); @@ -812,7 +815,14 @@ my $last_syscache; foreach my $syscache (sort keys %syscaches) { - print $syscache_ids_fh "\t$syscache,\n"; + if (not defined $last_syscache) + { + print $syscache_ids_fh "\t$syscache = 0,\n"; + } + else + { + print $syscache_ids_fh "\t$syscache,\n"; + } $last_syscache = $syscache; print $syscache_info_fh "\t[$syscache] = {\n"; @@ -825,8 +835,11 @@ print $syscache_info_fh "\t},\n"; } -print $syscache_ids_fh "};\n"; -print $syscache_ids_fh "#define SysCacheSize ($last_syscache + 1)\n"; +print $syscache_ids_fh "} SysCacheIdentifier;\n"; +print $syscache_ids_fh "#define SysCacheSize ($last_syscache + 1)\n\n"; + +# Closing boilerplate for syscache_ids.h +print $syscache_ids_fh "#endif\t\t\t\t\t\t\t/* SYSCACHE_IDS_H */\n"; print $syscache_info_fh "};\n"; @@ -1054,8 +1067,7 @@ sub morph_row_for_schemapg } # Expand booleans from 'f'/'t' to 'false'/'true'. - # Some values might be other macros (eg FLOAT8PASSBYVAL), - # don't change. + # Some values might be other macros, if so don't change. elsif ($atttype eq 'bool') { $row->{$attname} = 'true' if $row->{$attname} eq 't'; @@ -1162,7 +1174,7 @@ sub print_boilerplate * %s * %s * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index fbaed5359ad7c..ae6b7cda3ddfe 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -3,7 +3,7 @@ * heap.c * code to create and destroy POSTGRES heap relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -664,6 +664,15 @@ CheckAttributeType(const char *attname, flags); } + /* + * For consistency with check_virtual_generated_security(). + */ + if ((flags & CHKATYPE_IS_VIRTUAL) && atttypid >= FirstUnpinnedObjectId) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("virtual generated column \"%s\" cannot have a user-defined type", attname), + errdetail("Virtual generated columns that make use of user-defined types are not yet supported.")); + /* * This might not be strictly invalid per SQL standard, but it is pretty * useless, and it cannot be dumped, so we must disallow it. @@ -723,7 +732,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel, /* Initialize the number of slots to use */ nslots = Min(tupdesc->natts, (MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_attribute))); - slot = palloc(sizeof(TupleTableSlot *) * nslots); + slot = palloc_array(TupleTableSlot *, nslots); for (int i = 0; i < nslots; i++) slot[i] = MakeSingleTupleTableSlot(td, &TTSOpsHeapTuple); @@ -1100,6 +1109,7 @@ AddNewRelationType(const char *typeName, * if false, relacl is always set NULL * allow_system_table_mods: true to allow creation in system namespaces * is_internal: is this a system-generated catalog? + * relrewrite: link to original relation during a table rewrite * * Output parameters: * typaddress: if not null, gets the object address of the new pg_type entry @@ -2449,7 +2459,7 @@ AddRelationNewConstraints(Relation rel, defOid = StoreAttrDefault(rel, colDef->attnum, expr, is_internal); - cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + cooked = palloc_object(CookedConstraint); cooked->contype = CONSTR_DEFAULT; cooked->conoid = defOid; cooked->name = NULL; @@ -2583,7 +2593,7 @@ AddRelationNewConstraints(Relation rel, numchecks++; - cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + cooked = palloc_object(CookedConstraint); cooked->contype = CONSTR_CHECK; cooked->conoid = constrOid; cooked->name = ccname; @@ -2625,6 +2635,7 @@ AddRelationNewConstraints(Relation rel, * requested validity. */ if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum, + cdef->conname, is_local, cdef->is_no_inherit, cdef->skip_validation)) continue; @@ -2659,7 +2670,7 @@ AddRelationNewConstraints(Relation rel, inhcount, cdef->is_no_inherit); - nncooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + nncooked = palloc_object(CookedConstraint); nncooked->contype = CONSTR_NOTNULL; nncooked->conoid = constrOid; nncooked->name = nnname; @@ -2875,14 +2886,16 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr, * for each column, giving priority to user-specified ones, and setting * inhcount according to how many parents cause each column to get a * not-null constraint. If a user-specified name clashes with another - * user-specified name, an error is raised. + * user-specified name, an error is raised. 'existing_constraints' + * is a list of already defined constraint names, which should be avoided + * when generating further ones. * * Returns a list of AttrNumber for columns that need to have the attnotnull * flag set. */ List * AddRelationNotNullConstraints(Relation rel, List *constraints, - List *old_notnulls) + List *old_notnulls, List *existing_constraints) { List *givennames; List *nnnames; @@ -2894,7 +2907,7 @@ AddRelationNotNullConstraints(Relation rel, List *constraints, * because we must raise error for user-generated name conflicts, but for * system-generated name conflicts we just generate another. */ - nnnames = NIL; + nnnames = list_copy(existing_constraints); /* don't scribble on input */ givennames = NIL; /* @@ -2996,7 +3009,7 @@ AddRelationNotNullConstraints(Relation rel, List *constraints, if (constr->is_no_inherit) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot define not-null constraint on column \"%s\" with NO INHERIT", + errmsg("cannot define not-null constraint with NO INHERIT on column \"%s\"", strVal(linitial(constr->keys))), errdetail("The column has an inherited not-null constraint."))); @@ -3214,6 +3227,86 @@ check_nested_generated(ParseState *pstate, Node *node) check_nested_generated_walker(node, pstate); } +/* + * Check security of virtual generated column expression. + * + * Just like selecting from a view is exploitable (CVE-2024-7348), selecting + * from a table with virtual generated columns is exploitable. Users who are + * concerned about this can avoid selecting from views, but telling them to + * avoid selecting from tables is less practical. + * + * To address this, this restricts generation expressions for virtual + * generated columns are restricted to using built-in functions and types. We + * assume that built-in functions and types cannot be exploited for this + * purpose. Note the overall security also requires that all functions in use + * a immutable. (For example, there are some built-in non-immutable functions + * that can run arbitrary SQL.) The immutability is checked elsewhere, since + * that is a property that needs to hold independent of security + * considerations. + * + * In the future, this could be expanded by some new mechanism to declare + * other functions and types as safe or trusted for this purpose, but that is + * to be designed. + */ + +/* + * Callback for check_functions_in_node() that determines whether a function + * is user-defined. + */ +static bool +contains_user_functions_checker(Oid func_id, void *context) +{ + return (func_id >= FirstUnpinnedObjectId); +} + +/* + * Checks for all the things we don't want in the generation expressions of + * virtual generated columns for security reasons. Errors out if it finds + * one. + */ +static bool +check_virtual_generated_security_walker(Node *node, void *context) +{ + ParseState *pstate = context; + + if (node == NULL) + return false; + + if (!IsA(node, List)) + { + if (check_functions_in_node(node, contains_user_functions_checker, NULL)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("generation expression uses user-defined function"), + errdetail("Virtual generated columns that make use of user-defined functions are not yet supported."), + parser_errposition(pstate, exprLocation(node))); + + /* + * check_functions_in_node() doesn't check some node types (see + * comment there). We handle CoerceToDomain and MinMaxExpr by + * checking for built-in types. The other listed node types cannot + * call user-definable SQL-visible functions. + * + * We furthermore need this type check to handle built-in, immutable + * polymorphic functions such as array_eq(). + */ + if (exprType(node) >= FirstUnpinnedObjectId) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("generation expression uses user-defined type"), + errdetail("Virtual generated columns that make use of user-defined types are not yet supported."), + parser_errposition(pstate, exprLocation(node))); + } + + return expression_tree_walker(node, check_virtual_generated_security_walker, context); +} + +static void +check_virtual_generated_security(ParseState *pstate, Node *node) +{ + check_virtual_generated_security_walker(node, pstate); +} + /* * Take a raw default and convert it to a cooked format ready for * storage. @@ -3253,6 +3346,10 @@ cookDefault(ParseState *pstate, ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("generation expression is not immutable"))); + + /* Check security of expressions for virtual generated column */ + if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + check_virtual_generated_security(pstate, expr); } else { @@ -3473,7 +3570,8 @@ RelationTruncateIndexes(Relation heapRelation) /* Initialize the index and rebuild */ /* Note: we do not need to re-establish pkey setting */ - index_build(heapRelation, currentIndex, indexInfo, true, false); + index_build(heapRelation, currentIndex, indexInfo, true, false, + true); /* We're done with this index */ index_close(currentIndex, NoLock); diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 739a92bdcc1ca..9407c357f2716 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -3,7 +3,7 @@ * index.c * code to create and destroy POSTGRES index relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -24,6 +24,7 @@ #include #include "access/amapi.h" +#include "access/attmap.h" #include "access/heapam.h" #include "access/multixact.h" #include "access/relscan.h" @@ -289,7 +290,7 @@ ConstructTupleDescriptor(Relation heapRelation, int numkeyatts = indexInfo->ii_NumIndexKeyAttrs; ListCell *colnames_item = list_head(indexColNames); ListCell *indexpr_item = list_head(indexInfo->ii_Expressions); - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; TupleDesc heapTupDesc; TupleDesc indexTupDesc; int natts; /* #atts in heap rel --- for error checks */ @@ -481,7 +482,7 @@ ConstructTupleDescriptor(Relation heapRelation, populate_compact_attribute(indexTupDesc, i); } - pfree(amroutine); + TupleDescFinalize(indexTupDesc); return indexTupDesc; } @@ -714,6 +715,9 @@ UpdateIndexRelation(Oid indexoid, * already exists. * INDEX_CREATE_PARTITIONED: * create a partitioned index (table must be partitioned) + * INDEX_CREATE_SUPPRESS_PROGRESS: + * don't report progress during the index build. + * * constr_flags: flags passed to index_constraint_create * (only if INDEX_CREATE_ADD_CONSTRAINT is set) * allow_system_table_mods: allow table to be a system catalog @@ -739,8 +743,8 @@ index_create(Relation heapRelation, const int16 *coloptions, const NullableDatum *stattargets, Datum reloptions, - bits16 flags, - bits16 constr_flags, + uint16 flags, + uint16 constr_flags, bool allow_system_table_mods, bool is_internal, Oid *constraintId) @@ -759,6 +763,7 @@ index_create(Relation heapRelation, bool invalid = (flags & INDEX_CREATE_INVALID) != 0; bool concurrent = (flags & INDEX_CREATE_CONCURRENT) != 0; bool partitioned = (flags & INDEX_CREATE_PARTITIONED) != 0; + bool progress = (flags & INDEX_CREATE_SUPPRESS_PROGRESS) == 0; char relkind; TransactionId relfrozenxid; MultiXactId relminmxid; @@ -800,11 +805,11 @@ index_create(Relation heapRelation, errmsg("user-defined indexes on system catalog tables are not supported"))); /* - * Btree text_pattern_ops uses text_eq as the equality operator, which is - * fine as long as the collation is deterministic; text_eq then reduces to + * Btree text_pattern_ops uses texteq as the equality operator, which is + * fine as long as the collation is deterministic; texteq then reduces to * bitwise equality and so it is semantically compatible with the other * operators and functions in that opclass. But with a nondeterministic - * collation, text_eq could yield results that are incompatible with the + * collation, texteq could yield results that are incompatible with the * actual behavior of the index (which is determined by the opclass's * comparison function). We prevent such problems by refusing creation of * an index with that opclass and a nondeterministic collation. @@ -814,7 +819,7 @@ index_create(Relation heapRelation, * opclasses as incompatible with nondeterminism; but for now, this small * hack suffices. * - * Another solution is to use a special operator, not text_eq, as the + * Another solution is to use a special operator, not texteq, as the * equality opclass member; but that is undesirable because it would * prevent index usage in many queries that work fine today. */ @@ -1275,7 +1280,8 @@ index_create(Relation heapRelation, } else { - index_build(heapRelation, indexRelation, indexInfo, false, true); + index_build(heapRelation, indexRelation, indexInfo, false, true, + progress); } /* @@ -1288,22 +1294,23 @@ index_create(Relation heapRelation, } /* - * index_concurrently_create_copy + * index_create_copy * - * Create concurrently an index based on the definition of the one provided by - * caller. The index is inserted into catalogs and needs to be built later - * on. This is called during concurrent reindex processing. + * Create an index based on the definition of the one provided by caller. The + * index is inserted into catalogs. 'flags' are passed directly to + * index_create. * * "tablespaceOid" is the tablespace to use for this index. */ Oid -index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, - Oid tablespaceOid, const char *newName) +index_create_copy(Relation heapRelation, uint16 flags, + Oid oldIndexId, Oid tablespaceOid, const char *newName) { Relation indexRelation; IndexInfo *oldInfo, *newInfo; Oid newIndexId = InvalidOid; + bool concurrently = (flags & INDEX_CREATE_CONCURRENT) != 0; HeapTuple indexTuple, classTuple; Datum indclassDatum, @@ -1327,7 +1334,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, * Concurrent build of an index with exclusion constraints is not * supported. */ - if (oldInfo->ii_ExclusionOps != NULL) + if (oldInfo->ii_ExclusionOps != NULL && concurrently) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("concurrent index creation for exclusion constraints is not supported"))); @@ -1383,9 +1390,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, } /* - * Build the index information for the new index. Note that rebuild of - * indexes with exclusion constraints is not supported, hence there is no - * need to fill all the ii_Exclusion* fields. + * Build the index information for the new index. */ newInfo = makeIndexInfo(oldInfo->ii_NumIndexAttrs, oldInfo->ii_NumIndexKeyAttrs, @@ -1394,11 +1399,24 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indexPreds, oldInfo->ii_Unique, oldInfo->ii_NullsNotDistinct, - false, /* not ready for inserts */ - true, + !concurrently, /* isready */ + concurrently, /* concurrent */ indexRelation->rd_indam->amsummarizing, oldInfo->ii_WithoutOverlaps); + /* fetch exclusion constraint info if any */ + if (indexRelation->rd_index->indisexclusion) + { + /* + * XXX Beware: we're making newInfo point to oldInfo-owned memory. It + * would be more orthodox to palloc+memcpy, but we don't need that + * here at present. + */ + newInfo->ii_ExclusionOps = oldInfo->ii_ExclusionOps; + newInfo->ii_ExclusionProcs = oldInfo->ii_ExclusionProcs; + newInfo->ii_ExclusionStrats = oldInfo->ii_ExclusionStrats; + } + /* * Extract the list of column names and the column numbers for the new * index information. All this information will be used for the index @@ -1414,7 +1432,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, } /* Extract opclass options for each attribute */ - opclassOptions = palloc0(sizeof(Datum) * newInfo->ii_NumIndexAttrs); + opclassOptions = palloc0_array(Datum, newInfo->ii_NumIndexAttrs); for (int i = 0; i < newInfo->ii_NumIndexAttrs; i++) opclassOptions[i] = get_attoptions(oldIndexId, i + 1); @@ -1458,7 +1476,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indcoloptions->values, stattargets, reloptionsDatum, - INDEX_CREATE_SKIP_BUILD | INDEX_CREATE_CONCURRENT, + flags, 0, true, /* allow table to be a system catalog? */ false, /* is_internal? */ @@ -1522,7 +1540,7 @@ index_concurrently_build(Oid heapRelationId, indexInfo->ii_BrokenHotChain = false; /* Now build the index */ - index_build(heapRel, indexRelation, indexInfo, false, true); + index_build(heapRel, indexRelation, indexInfo, false, true, true); /* Roll back any GUC changes executed by index functions */ AtEOXact_GUC(false, save_nestlevel); @@ -1888,7 +1906,7 @@ index_constraint_create(Relation heapRelation, const IndexInfo *indexInfo, const char *constraintName, char constraintType, - bits16 constr_flags, + uint16 constr_flags, bool allow_system_table_mods, bool is_internal) { @@ -2678,9 +2696,9 @@ BuildSpeculativeIndexInfo(Relation index, IndexInfo *ii) */ Assert(ii->ii_Unique); - ii->ii_UniqueOps = (Oid *) palloc(sizeof(Oid) * indnkeyatts); - ii->ii_UniqueProcs = (Oid *) palloc(sizeof(Oid) * indnkeyatts); - ii->ii_UniqueStrats = (uint16 *) palloc(sizeof(uint16) * indnkeyatts); + ii->ii_UniqueOps = palloc_array(Oid, indnkeyatts); + ii->ii_UniqueProcs = palloc_array(Oid, indnkeyatts); + ii->ii_UniqueStrats = palloc_array(uint16, indnkeyatts); /* * We have to look up the operator's strategy number. This provides a @@ -2993,6 +3011,7 @@ index_update_stats(Relation rel, * * isreindex indicates we are recreating a previously-existing index. * parallel indicates if parallelism may be useful. + * progress indicates if the backend should update its progress info. * * Note: before Postgres 8.2, the passed-in heap and index Relations * were automatically closed by this routine. This is no longer the case. @@ -3003,7 +3022,8 @@ index_build(Relation heapRelation, Relation indexRelation, IndexInfo *indexInfo, bool isreindex, - bool parallel) + bool parallel, + bool progress) { IndexBuildResult *stats; Oid save_userid; @@ -3014,13 +3034,13 @@ index_build(Relation heapRelation, * sanity checks */ Assert(RelationIsValid(indexRelation)); - Assert(PointerIsValid(indexRelation->rd_indam)); - Assert(PointerIsValid(indexRelation->rd_indam->ambuild)); - Assert(PointerIsValid(indexRelation->rd_indam->ambuildempty)); + Assert(indexRelation->rd_indam); + Assert(indexRelation->rd_indam->ambuild); + Assert(indexRelation->rd_indam->ambuildempty); /* * Determine worker process details for parallel CREATE INDEX. Currently, - * only btree and BRIN have support for parallel builds. + * only btree, GIN, and BRIN have support for parallel builds. * * Note that planner considers parallel safety for us. */ @@ -3054,6 +3074,7 @@ index_build(Relation heapRelation, RestrictSearchPath(); /* Set up initial progress report status */ + if (progress) { const int progress_index[] = { PROGRESS_CREATEIDX_PHASE, @@ -3077,7 +3098,7 @@ index_build(Relation heapRelation, */ stats = indexRelation->rd_indam->ambuild(heapRelation, indexRelation, indexInfo); - Assert(PointerIsValid(stats)); + Assert(stats); /* * If this is an unlogged index, we may need to write out an init fork for @@ -3707,7 +3728,7 @@ reindex_index(const ReindexStmt *stmt, Oid indexId, ObjectAddressSet(address, RelationRelationId, indexId); EventTriggerCollectSimpleCommand(address, InvalidObjectAddress, - (Node *) stmt); + (const Node *) stmt); } /* @@ -3811,7 +3832,7 @@ reindex_index(const ReindexStmt *stmt, Oid indexId, /* Initialize the index and rebuild */ /* Note: we do not need to re-establish pkey setting */ - index_build(heapRelation, iRel, indexInfo, true, true); + index_build(heapRelation, iRel, indexInfo, true, true, progress); /* Re-allow use of target index */ ResetReindexProcessing(); @@ -4079,7 +4100,7 @@ reindex_relation(const ReindexStmt *stmt, Oid relid, int flags, Assert(!ReindexIsProcessingIndex(indexOid)); /* Set index rebuild count */ - pgstat_progress_update_param(PROGRESS_CLUSTER_INDEX_REBUILD_COUNT, + pgstat_progress_update_param(PROGRESS_REPACK_INDEX_REBUILD_COUNT, i); i++; } diff --git a/src/backend/catalog/indexing.c b/src/backend/catalog/indexing.c index 25c4b6bdc87f0..fd7d2ec0e3aba 100644 --- a/src/backend/catalog/indexing.c +++ b/src/backend/catalog/indexing.c @@ -4,7 +4,7 @@ * This file contains routines to support indexes defined on system * catalogs. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -201,7 +201,7 @@ CatalogTupleCheckConstraints(Relation heapRel, HeapTuple tup) if (HeapTupleHasNulls(tup)) { TupleDesc tupdesc = RelationGetDescr(heapRel); - bits8 *bp = tup->t_data->t_bits; + uint8 *bp = tup->t_data->t_bits; for (int attnum = 0; attnum < tupdesc->natts; attnum++) { @@ -310,7 +310,7 @@ CatalogTuplesMultiInsertWithInfo(Relation heapRel, TupleTableSlot **slot, * (Use CatalogTupleUpdateWithInfo in such cases.) */ void -CatalogTupleUpdate(Relation heapRel, ItemPointer otid, HeapTuple tup) +CatalogTupleUpdate(Relation heapRel, const ItemPointerData *otid, HeapTuple tup) { CatalogIndexState indstate; TU_UpdateIndexes updateIndexes = TU_All; @@ -334,7 +334,7 @@ CatalogTupleUpdate(Relation heapRel, ItemPointer otid, HeapTuple tup) * so that callers needn't trouble over this ... but we don't do so today. */ void -CatalogTupleUpdateWithInfo(Relation heapRel, ItemPointer otid, HeapTuple tup, +CatalogTupleUpdateWithInfo(Relation heapRel, const ItemPointerData *otid, HeapTuple tup, CatalogIndexState indstate) { TU_UpdateIndexes updateIndexes = TU_All; @@ -362,7 +362,7 @@ CatalogTupleUpdateWithInfo(Relation heapRel, ItemPointer otid, HeapTuple tup, * it might be better to do something about caching CatalogIndexState. */ void -CatalogTupleDelete(Relation heapRel, ItemPointer tid) +CatalogTupleDelete(Relation heapRel, const ItemPointerData *tid) { simple_heap_delete(heapRel, tid); } diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index a7bffca93d1da..4f0e249293774 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -2,7 +2,7 @@ * SQL Information Schema * as defined in ISO/IEC 9075-11:2023 * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/backend/catalog/information_schema.sql * @@ -3009,3 +3009,369 @@ CREATE VIEW user_mappings AS FROM _pg_user_mappings; GRANT SELECT ON user_mappings TO PUBLIC; + + +-- SQL/PGQ views; these use section numbers from part 16 of the standard. + +/* + * 15.2 + * PG_DEFINED_LABEL_SETS view + */ + +-- TODO + + +/* + * 15.3 + * PG_DEFINED_LABEL_SET_LABELS view + */ + +-- TODO + + +/* + * 15.4 + * PG_EDGE_DEFINED_LABEL_SETS view + */ + +-- TODO + + +/* + * 15.5 + * PG_EDGE_TABLE_COMPONENTS view + */ + +CREATE VIEW pg_edge_table_components AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(eg.pgealias AS sql_identifier) AS edge_table_alias, + CAST(v.pgealias AS sql_identifier) AS vertex_table_alias, + CAST(CASE eg.end WHEN 'src' THEN 'SOURCE' WHEN 'dest' THEN 'DESTINATION' END AS character_data) AS edge_end, + CAST(ae.attname AS sql_identifier) AS edge_table_column_name, + CAST(av.attname AS sql_identifier) AS vertex_table_column_name, + CAST((eg.egkey).n AS cardinal_number) AS ordinal_position + FROM pg_namespace npg + JOIN + (SELECT * FROM pg_class WHERE relkind = 'g') AS pg + ON npg.oid = pg.relnamespace + JOIN + (SELECT pgepgid, pgealias, pgerelid, 'src' AS end, pgesrcvertexid AS vertexid, _pg_expandarray(pgesrckey) AS egkey, _pg_expandarray(pgesrcref) AS egref FROM pg_propgraph_element WHERE pgekind = 'e' + UNION ALL + SELECT pgepgid, pgealias, pgerelid, 'dest' AS end, pgedestvertexid AS vertexid, _pg_expandarray(pgedestkey) AS egkey, _pg_expandarray(pgedestref) AS egref FROM pg_propgraph_element WHERE pgekind = 'e' + ) AS eg + ON pg.oid = eg.pgepgid + JOIN + (SELECT * FROM pg_propgraph_element WHERE pgekind = 'v') AS v + ON eg.vertexid = v.oid + JOIN + (SELECT * FROM pg_attribute WHERE NOT attisdropped) AS ae + ON eg.pgerelid = ae.attrelid AND (eg.egkey).x = ae.attnum + JOIN + (SELECT * FROM pg_attribute WHERE NOT attisdropped) AS av + ON v.pgerelid = av.attrelid AND (eg.egref).x = av.attnum + WHERE NOT pg_is_other_temp_schema(npg.oid) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_edge_table_components TO PUBLIC; + + +/* + * 15.6 + * PG_EDGE_TRIPLETS view + */ + +-- TODO + + +/* + * 15.7 + * PG_ELEMENT_TABLE_KEY_COLUMNS view + */ + +CREATE VIEW pg_element_table_key_columns AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(pgealias AS sql_identifier) AS element_table_alias, + CAST(a.attname AS sql_identifier) AS column_name, + CAST((el.ekey).n AS cardinal_number) AS ordinal_position + FROM pg_namespace npg + JOIN + (SELECT * FROM pg_class WHERE relkind = 'g') AS pg + ON npg.oid = pg.relnamespace + JOIN + (SELECT pgepgid, pgealias, pgerelid, _pg_expandarray(pgekey) AS ekey FROM pg_propgraph_element) AS el + ON pg.oid = el.pgepgid + JOIN + (SELECT * FROM pg_attribute WHERE NOT attisdropped) AS a + ON el.pgerelid = a.attrelid AND (el.ekey).x = a.attnum + WHERE NOT pg_is_other_temp_schema(npg.oid) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_element_table_key_columns TO PUBLIC; + + +/* + * 15.8 + * PG_ELEMENT_TABLE_LABELS view + */ + +CREATE VIEW pg_element_table_labels AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(e.pgealias AS sql_identifier) AS element_table_alias, + CAST(l.pgllabel AS sql_identifier) AS label_name + FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_element_label el, pg_propgraph_label l + WHERE pg.relnamespace = npg.oid + AND e.pgepgid = pg.oid + AND el.pgelelid = e.oid + AND el.pgellabelid = l.oid + AND pg.relkind = 'g' + AND (NOT pg_is_other_temp_schema(npg.oid)) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_element_table_labels TO PUBLIC; + + +/* + * 15.9 + * PG_ELEMENT_TABLE_PROPERTIES view + */ + +CREATE VIEW pg_element_table_properties AS + SELECT DISTINCT + CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(e.pgealias AS sql_identifier) AS element_table_alias, + CAST(pr.pgpname AS sql_identifier) AS property_name, + CAST(pg_get_expr(plp.plpexpr, e.pgerelid) AS character_data) AS property_expression + FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_element_label el, pg_propgraph_label_property plp, pg_propgraph_property pr + WHERE pg.relnamespace = npg.oid + AND e.pgepgid = pg.oid + AND el.pgelelid = e.oid + AND plp.plpellabelid = el.oid + AND pr.oid = plp.plppropid + AND pg.relkind = 'g' + AND (NOT pg_is_other_temp_schema(npg.oid)) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_element_table_properties TO PUBLIC; + + +/* + * 15.10 + * PG_ELEMENT_TABLES view + */ + +CREATE VIEW pg_element_tables AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(e.pgealias AS sql_identifier) AS element_table_alias, + CAST(CASE e.pgekind WHEN 'e' THEN 'EDGE' WHEN 'v' THEN 'VERTEX' END AS character_data) AS element_table_kind, + CAST(current_database() AS sql_identifier) AS table_catalog, + CAST(nt.nspname AS sql_identifier) AS table_schema, + CAST(t.relname AS sql_identifier) AS table_name, + CAST(NULL AS character_data) AS element_table_definition + FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_class t, pg_namespace nt + WHERE pg.relnamespace = npg.oid + AND e.pgepgid = pg.oid + AND e.pgerelid = t.oid + AND t.relnamespace = nt.oid + AND pg.relkind = 'g' + AND (NOT pg_is_other_temp_schema(npg.oid)) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_element_tables TO PUBLIC; + + +/* + * 15.11 + * PG_LABEL_PROPERTIES view + */ + +CREATE VIEW pg_label_properties AS + SELECT DISTINCT + CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(l.pgllabel AS sql_identifier) AS label_name, + CAST(pr.pgpname AS sql_identifier) AS property_name + FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_label l, pg_propgraph_element_label el, pg_propgraph_label_property plp, pg_propgraph_property pr + WHERE pg.relnamespace = npg.oid + AND e.pgepgid = pg.oid + AND el.pgelelid = e.oid + AND plp.plpellabelid = el.oid + AND pr.oid = plp.plppropid + AND el.pgellabelid = l.oid + AND pg.relkind = 'g' + AND (NOT pg_is_other_temp_schema(npg.oid)) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_label_properties TO PUBLIC; + + +/* + * 15.12 + * PG_LABELS view + */ + +CREATE VIEW pg_labels AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(l.pgllabel AS sql_identifier) AS label_name + FROM pg_namespace npg, pg_class pg, pg_propgraph_label l + WHERE pg.relnamespace = npg.oid + AND l.pglpgid = pg.oid + AND pg.relkind = 'g' + AND (NOT pg_is_other_temp_schema(npg.oid)) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_labels TO PUBLIC; + + +/* + * 15.13 + * PG_PROPERTY_DATA_TYPES view + */ + +CREATE VIEW pg_property_data_types AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(npg.nspname AS sql_identifier) AS property_graph_schema, + CAST(pg.relname AS sql_identifier) AS property_graph_name, + CAST(pgp.pgpname AS sql_identifier) AS property_name, + + CAST( + CASE WHEN t.typtype = 'd' THEN + CASE WHEN bt.typelem <> 0 AND bt.typlen = -1 THEN 'ARRAY' + WHEN nbt.nspname = 'pg_catalog' THEN format_type(t.typbasetype, null) + ELSE 'USER-DEFINED' END + ELSE + CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY' + WHEN nt.nspname = 'pg_catalog' THEN format_type(pgp.pgptypid, null) + ELSE 'USER-DEFINED' END + END + AS character_data) + AS data_type, + + CAST(null AS cardinal_number) AS character_maximum_length, + CAST(null AS cardinal_number) AS character_octet_length, + CAST(null AS sql_identifier) AS character_set_catalog, + CAST(null AS sql_identifier) AS character_set_schema, + CAST(null AS sql_identifier) AS character_set_name, + CAST(current_database() AS sql_identifier) AS collation_catalog, + CAST(nc.nspname AS sql_identifier) AS collation_schema, + CAST(c.collname AS sql_identifier) AS collation_name, + CAST(null AS cardinal_number) AS numeric_precision, + CAST(null AS cardinal_number) AS numeric_precision_radix, + CAST(null AS cardinal_number) AS numeric_scale, + CAST(null AS cardinal_number) AS datetime_precision, + CAST(null AS character_data) AS interval_type, + CAST(null AS cardinal_number) AS interval_precision, + + CAST(current_database() AS sql_identifier) AS user_defined_type_catalog, + CAST(coalesce(nbt.nspname, nt.nspname) AS sql_identifier) AS user_defined_type_schema, + CAST(coalesce(bt.typname, t.typname) AS sql_identifier) AS user_defined_type_name, + + CAST(null AS sql_identifier) AS scope_catalog, + CAST(null AS sql_identifier) AS scope_schema, + CAST(null AS sql_identifier) AS scope_name, + + CAST(null AS cardinal_number) AS maximum_cardinality, + CAST(pgp.pgpname AS sql_identifier) AS dtd_identifier + + FROM pg_propgraph_property pgp + JOIN (pg_class pg JOIN pg_namespace npg ON (pg.relnamespace = npg.oid)) ON pgp.pgppgid = pg.oid + JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON pgp.pgptypid = t.oid + LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON (bt.typnamespace = nbt.oid)) + ON (t.typtype = 'd' AND t.typbasetype = bt.oid) + LEFT JOIN (pg_collation c JOIN pg_namespace nc ON (c.collnamespace = nc.oid)) + ON pgp.pgpcollation = c.oid AND (nc.nspname, c.collname) <> ('pg_catalog', 'default') + + WHERE pg.relkind = 'g' + AND (NOT pg_is_other_temp_schema(npg.oid)) + AND (pg_has_role(pg.relowner, 'USAGE') + OR has_table_privilege(pg.oid, 'SELECT')); + +GRANT SELECT ON pg_property_data_types TO PUBLIC; + + +/* + * 15.14 + * PG_PROPERTY_GRAPH_PRIVILEGES view + */ + +CREATE VIEW pg_property_graph_privileges AS + SELECT CAST(u_grantor.rolname AS sql_identifier) AS grantor, + CAST(grantee.rolname AS sql_identifier) AS grantee, + CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(nc.nspname AS sql_identifier) AS property_graph_schema, + CAST(c.relname AS sql_identifier) AS property_graph_name, + CAST(c.prtype AS character_data) AS privilege_type, + CAST( + CASE WHEN + -- object owner always has grant options + pg_has_role(grantee.oid, c.relowner, 'USAGE') + OR c.grantable + THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_grantable + + FROM ( + SELECT oid, relname, relnamespace, relkind, relowner, (aclexplode(coalesce(relacl, acldefault('r', relowner)))).* FROM pg_class + ) AS c (oid, relname, relnamespace, relkind, relowner, grantor, grantee, prtype, grantable), + pg_namespace nc, + pg_authid u_grantor, + ( + SELECT oid, rolname FROM pg_authid + UNION ALL + SELECT 0::oid, 'PUBLIC' + ) AS grantee (oid, rolname) + + WHERE c.relnamespace = nc.oid + AND c.relkind IN ('g') + AND c.grantee = grantee.oid + AND c.grantor = u_grantor.oid + AND c.prtype IN ('SELECT') + AND (pg_has_role(u_grantor.oid, 'USAGE') + OR pg_has_role(grantee.oid, 'USAGE') + OR grantee.rolname = 'PUBLIC'); + +GRANT SELECT ON pg_property_graph_privileges TO PUBLIC; + + +/* + * 15.15 + * PG_VERTEX_DEFINED_LABEL_SETS view + */ + +-- TODO + + +/* + * 15.16 + * PROPERTY_GRAPHS view + */ + +CREATE VIEW property_graphs AS + SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog, + CAST(nc.nspname AS sql_identifier) AS property_graph_schema, + CAST(c.relname AS sql_identifier) AS property_graph_name + FROM pg_namespace nc, pg_class c + WHERE c.relnamespace = nc.oid + AND c.relkind = 'g' + AND (NOT pg_is_other_temp_schema(nc.oid)) + AND (pg_has_role(c.relowner, 'USAGE') + OR has_table_privilege(c.oid, 'SELECT')); + +GRANT SELECT ON property_graphs TO PUBLIC; diff --git a/src/backend/catalog/meson.build b/src/backend/catalog/meson.build index 1958ea9238a76..11d21c5ad6ba5 100644 --- a/src/backend/catalog/meson.build +++ b/src/backend/catalog/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'aclchk.c', @@ -31,6 +31,7 @@ backend_sources += files( 'pg_range.c', 'pg_shdepend.c', 'pg_subscription.c', + 'pg_tablespace.c', 'pg_type.c', 'storage.c', 'toasting.c', diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index d97d632a7ef55..56b87d878e884 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -9,7 +9,7 @@ * and implementing search-path-controlled searches. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -41,7 +41,6 @@ #include "catalog/pg_ts_parser.h" #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "common/hashfn_unstable.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -49,6 +48,7 @@ #include "nodes/makefuncs.h" #include "storage/ipc.h" #include "storage/lmgr.h" +#include "storage/proc.h" #include "storage/procarray.h" #include "utils/acl.h" #include "utils/builtins.h" @@ -230,10 +230,11 @@ static void AccessTempTableNamespace(bool force); static void InitTempTableNamespace(void); static void RemoveTempRelations(Oid tempNamespaceId); static void RemoveTempRelationsCallback(int code, Datum arg); -static void InvalidationCallback(Datum arg, int cacheid, uint32 hashvalue); +static void InvalidationCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static bool MatchNamedCall(HeapTuple proctup, int nargs, List *argnames, bool include_out_arguments, int pronargs, - int **argnumbers); + int **argnumbers, int *fgc_flags); /* * Recomputing the namespace path can be costly when done frequently, such as @@ -1118,15 +1119,15 @@ TypeIsVisibleExt(Oid typid, bool *is_missing) /* * FuncnameGetCandidates - * Given a possibly-qualified function name and argument count, + * Given a possibly-qualified routine name, argument count, and arg names, * retrieve a list of the possible matches. * - * If nargs is -1, we return all functions matching the given name, + * If nargs is -1, we return all routines matching the given name, * regardless of argument count. (argnames must be NIL, and expand_variadic * and expand_defaults must be false, in this case.) * * If argnames isn't NIL, we are considering a named- or mixed-notation call, - * and only functions having all the listed argument names will be returned. + * and only routines having all the listed argument names will be returned. * (We assume that length(argnames) <= nargs and all the passed-in names are * distinct.) The returned structs will include an argnumbers array showing * the actual argument index for each logical argument position. @@ -1184,14 +1185,21 @@ TypeIsVisibleExt(Oid typid, bool *is_missing) * The caller might end up discarding such an entry anyway, but if it selects * such an entry it should react as though the call were ambiguous. * - * If missing_ok is true, an empty list (NULL) is returned if the name was - * schema-qualified with a schema that does not exist. Likewise if no - * candidate is found for other reasons. + * We return an empty list (NULL) if no suitable matches can be found. + * If the function name was schema-qualified with a schema that does not + * exist, then we return an empty list if missing_ok is true and otherwise + * throw an error. (missing_ok does not affect the behavior otherwise.) + * + * The output argument *fgc_flags is filled with a bitmask indicating how + * far we were able to match the supplied information. This is not of much + * interest if any candidates were found, but if not, it can help callers + * produce an on-point error message. */ FuncCandidateList FuncnameGetCandidates(List *names, int nargs, List *argnames, bool expand_variadic, bool expand_defaults, - bool include_out_arguments, bool missing_ok) + bool include_out_arguments, bool missing_ok, + int *fgc_flags) { FuncCandidateList resultList = NULL; bool any_special = false; @@ -1204,15 +1212,20 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, /* check for caller error */ Assert(nargs >= 0 || !(expand_variadic | expand_defaults)); + /* initialize output fgc_flags to empty */ + *fgc_flags = 0; + /* deconstruct the name list */ DeconstructQualifiedName(names, &schemaname, &funcname); if (schemaname) { /* use exact schema given */ + *fgc_flags |= FGC_SCHEMA_GIVEN; /* report that a schema is given */ namespaceId = LookupExplicitNamespace(schemaname, missing_ok); if (!OidIsValid(namespaceId)) return NULL; + *fgc_flags |= FGC_SCHEMA_EXISTS; /* report that the schema exists */ } else { @@ -1238,6 +1251,8 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, int *argnumbers = NULL; FuncCandidateList newResult; + *fgc_flags |= FGC_NAME_EXISTS; /* the name is present in pg_proc */ + if (OidIsValid(namespaceId)) { /* Consider only procs in specified namespace */ @@ -1263,6 +1278,8 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, continue; /* proc is not in search path */ } + *fgc_flags |= FGC_NAME_VISIBLE; /* routine is in the right schema */ + /* * If we are asked to match to OUT arguments, then use the * proallargtypes array (which includes those); otherwise use @@ -1297,16 +1314,6 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, /* * Call uses named or mixed notation * - * Named or mixed notation can match a variadic function only if - * expand_variadic is off; otherwise there is no way to match the - * presumed-nameless parameters expanded from the variadic array. - */ - if (OidIsValid(procform->provariadic) && expand_variadic) - continue; - va_elem_type = InvalidOid; - variadic = false; - - /* * Check argument count. */ Assert(nargs >= 0); /* -1 not supported with argnames */ @@ -1325,12 +1332,33 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, if (pronargs != nargs && !use_defaults) continue; + /* We found a routine with a suitable number of arguments */ + *fgc_flags |= FGC_ARGCOUNT_MATCH; + /* Check for argument name match, generate positional mapping */ if (!MatchNamedCall(proctup, nargs, argnames, include_out_arguments, pronargs, - &argnumbers)) + &argnumbers, fgc_flags)) continue; + /* + * Named or mixed notation can match a variadic function only if + * expand_variadic is off; otherwise there is no way to match the + * presumed-nameless parameters expanded from the variadic array. + * However, we postpone the check until here because we want to + * perform argument name matching anyway (using the variadic array + * argument's name). This allows us to give an on-point error + * message if the user forgets to say VARIADIC in what would have + * been a valid call with it. + */ + if (OidIsValid(procform->provariadic) && expand_variadic) + continue; + va_elem_type = InvalidOid; + variadic = false; + + /* We found a fully-valid call using argument names */ + *fgc_flags |= FGC_ARGNAMES_VALID; + /* Named argument matching is always "special" */ any_special = true; } @@ -1372,6 +1400,9 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, /* Ignore if it doesn't match requested argument count */ if (nargs >= 0 && pronargs != nargs && !variadic && !use_defaults) continue; + + /* We found a routine with a suitable number of arguments */ + *fgc_flags |= FGC_ARGCOUNT_MATCH; } /* @@ -1580,11 +1611,13 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames, * the mapping from call argument positions to actual function argument * numbers. Defaulted arguments are included in this map, at positions * after the last supplied argument. + * + * We also add flag bits to *fgc_flags reporting on how far the match got. */ static bool MatchNamedCall(HeapTuple proctup, int nargs, List *argnames, bool include_out_arguments, int pronargs, - int **argnumbers) + int **argnumbers, int *fgc_flags) { Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(proctup); int numposargs = nargs - list_length(argnames); @@ -1593,6 +1626,7 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames, char **p_argnames; char *p_argmodes; bool arggiven[FUNC_MAX_ARGS]; + bool arg_filled_twice = false; bool isnull; int ap; /* call args position */ int pp; /* proargs position */ @@ -1646,9 +1680,9 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames, continue; if (p_argnames[i] && strcmp(p_argnames[i], argname) == 0) { - /* fail if argname matches a positional argument */ + /* note if argname matches a positional argument */ if (arggiven[pp]) - return false; + arg_filled_twice = true; arggiven[pp] = true; (*argnumbers)[ap] = pp; found = true; @@ -1665,6 +1699,16 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames, Assert(ap == nargs); /* processed all actual parameters */ + /* If we get here, the function did match all the supplied argnames */ + *fgc_flags |= FGC_ARGNAMES_MATCH; + + /* ... however, some of them might have been placed wrong */ + if (arg_filled_twice) + return false; /* some argname matched a positional argument */ + + /* If we get here, the call doesn't have invalid mixed notation */ + *fgc_flags |= FGC_ARGNAMES_NONDUP; + /* Check for default arguments */ if (nargs < pronargs) { @@ -1683,6 +1727,9 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames, Assert(ap == pronargs); /* processed all function parameters */ + /* If we get here, the call supplies all the required arguments */ + *fgc_flags |= FGC_ARGNAMES_ALL; + return true; } @@ -1746,11 +1793,13 @@ FunctionIsVisibleExt(Oid funcid, bool *is_missing) char *proname = NameStr(procform->proname); int nargs = procform->pronargs; FuncCandidateList clist; + int fgc_flags; visible = false; clist = FuncnameGetCandidates(list_make1(makeString(proname)), - nargs, NIL, false, false, false, false); + nargs, NIL, false, false, false, false, + &fgc_flags); for (; clist; clist = clist->next) { @@ -1883,9 +1932,20 @@ OpernameGetOprid(List *names, Oid oprleft, Oid oprright) * * The returned items always have two args[] entries --- the first will be * InvalidOid for a prefix oprkind. nargs is always 2, too. + * + * We return an empty list (NULL) if no suitable matches can be found. If the + * operator name was schema-qualified with a schema that does not exist, then + * we return an empty list if missing_schema_ok is true and otherwise throw an + * error. (missing_schema_ok does not affect the behavior otherwise.) + * + * The output argument *fgc_flags is filled with a bitmask indicating how + * far we were able to match the supplied information. This is not of much + * interest if any candidates were found, but if not, it can help callers + * produce an on-point error message. */ FuncCandidateList -OpernameGetCandidates(List *names, char oprkind, bool missing_schema_ok) +OpernameGetCandidates(List *names, char oprkind, bool missing_schema_ok, + int *fgc_flags) { FuncCandidateList resultList = NULL; char *resultSpace = NULL; @@ -1896,15 +1956,20 @@ OpernameGetCandidates(List *names, char oprkind, bool missing_schema_ok) CatCList *catlist; int i; + /* initialize output fgc_flags to empty */ + *fgc_flags = 0; + /* deconstruct the name list */ DeconstructQualifiedName(names, &schemaname, &opername); if (schemaname) { /* use exact schema given */ + *fgc_flags |= FGC_SCHEMA_GIVEN; /* report that a schema is given */ namespaceId = LookupExplicitNamespace(schemaname, missing_schema_ok); - if (missing_schema_ok && !OidIsValid(namespaceId)) + if (!OidIsValid(namespaceId)) return NULL; + *fgc_flags |= FGC_SCHEMA_EXISTS; /* report that the schema exists */ } else { @@ -1942,6 +2007,8 @@ OpernameGetCandidates(List *names, char oprkind, bool missing_schema_ok) if (oprkind && operform->oprkind != oprkind) continue; + *fgc_flags |= FGC_NAME_EXISTS; /* the name is present in pg_operator */ + if (OidIsValid(namespaceId)) { /* Consider only opers in specified namespace */ @@ -2015,6 +2082,8 @@ OpernameGetCandidates(List *names, char oprkind, bool missing_schema_ok) } } + *fgc_flags |= FGC_NAME_VISIBLE; /* operator is in the right schema */ + /* * Okay to add it to result list */ @@ -2686,6 +2755,9 @@ StatisticsObjIsVisibleExt(Oid stxid, bool *is_missing) { Oid namespaceId = lfirst_oid(l); + if (namespaceId == myTempNamespace) + continue; /* do not look in temp namespace */ + if (namespaceId == stxnamespace) { /* Found it first in path */ @@ -3859,7 +3931,7 @@ GetSearchPathMatcher(MemoryContext context) oldcxt = MemoryContextSwitchTo(context); - result = (SearchPathMatcher *) palloc0(sizeof(SearchPathMatcher)); + result = palloc0_object(SearchPathMatcher); schemas = list_copy(activeSearchPath); while (schemas && linitial_oid(schemas) != activeCreationNamespace) { @@ -3890,7 +3962,7 @@ CopySearchPathMatcher(SearchPathMatcher *path) { SearchPathMatcher *result; - result = (SearchPathMatcher *) palloc(sizeof(SearchPathMatcher)); + result = palloc_object(SearchPathMatcher); result->schemas = list_copy(path->schemas); result->addCatalog = path->addCatalog; result->addTemp = path->addTemp; @@ -4793,7 +4865,7 @@ InitializeSearchPath(void) * Syscache inval callback function */ static void -InvalidationCallback(Datum arg, int cacheid, uint32 hashvalue) +InvalidationCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { /* * Force search path to be recomputed on next use, also invalidating the diff --git a/src/backend/catalog/objectaccess.c b/src/backend/catalog/objectaccess.c index 0853983f5e165..44900d6461603 100644 --- a/src/backend/catalog/objectaccess.c +++ b/src/backend/catalog/objectaccess.c @@ -3,7 +3,7 @@ * objectaccess.c * functions for object_access_hook on various events * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * ------------------------------------------------------------------------- diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index b63fd57dc04bb..c186280957781 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -3,7 +3,7 @@ * objectaddress.c * functions for working with ObjectAddresses * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -47,6 +47,11 @@ #include "catalog/pg_parameter_acl.h" #include "catalog/pg_policy.h" #include "catalog/pg_proc.h" +#include "catalog/pg_propgraph_element.h" +#include "catalog/pg_propgraph_element_label.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_label_property.h" +#include "catalog/pg_propgraph_property.h" #include "catalog/pg_publication.h" #include "catalog/pg_publication_namespace.h" #include "catalog/pg_publication_rel.h" @@ -62,7 +67,6 @@ #include "catalog/pg_ts_template.h" #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" -#include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" @@ -100,10 +104,11 @@ typedef struct * error messages */ Oid class_oid; /* oid of catalog */ Oid oid_index_oid; /* oid of index on system oid column */ - int oid_catcache_id; /* id of catcache on system oid column */ - int name_catcache_id; /* id of catcache on (name,namespace), or - * (name) if the object does not live in a - * namespace */ + SysCacheIdentifier oid_catcache_id; /* id of catcache on system oid column */ + SysCacheIdentifier name_catcache_id; /* id of catcache on + * (name,namespace), or (name) if + * the object does not live in a + * namespace */ AttrNumber attnum_oid; /* attribute number of oid column */ AttrNumber attnum_name; /* attnum of name field */ AttrNumber attnum_namespace; /* attnum of namespace field */ @@ -136,8 +141,8 @@ static const ObjectPropertyType ObjectProperty[] = "access method operator", AccessMethodOperatorRelationId, AccessMethodOperatorOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_amop_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -150,8 +155,8 @@ static const ObjectPropertyType ObjectProperty[] = "access method procedure", AccessMethodProcedureRelationId, AccessMethodProcedureOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_amproc_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -164,8 +169,8 @@ static const ObjectPropertyType ObjectProperty[] = "cast", CastRelationId, CastOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_cast_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -179,7 +184,7 @@ static const ObjectPropertyType ObjectProperty[] = CollationRelationId, CollationOidIndexId, COLLOID, - -1, /* COLLNAMEENCNSP also takes encoding */ + SYSCACHEID_INVALID, /* COLLNAMEENCNSP also takes encoding */ Anum_pg_collation_oid, Anum_pg_collation_collname, Anum_pg_collation_collnamespace, @@ -193,7 +198,7 @@ static const ObjectPropertyType ObjectProperty[] = ConstraintRelationId, ConstraintOidIndexId, CONSTROID, - -1, + SYSCACHEID_INVALID, Anum_pg_constraint_oid, Anum_pg_constraint_conname, Anum_pg_constraint_connamespace, @@ -221,7 +226,7 @@ static const ObjectPropertyType ObjectProperty[] = DatabaseRelationId, DatabaseOidIndexId, DATABASEOID, - -1, + SYSCACHEID_INVALID, Anum_pg_database_oid, Anum_pg_database_datname, InvalidAttrNumber, @@ -234,8 +239,8 @@ static const ObjectPropertyType ObjectProperty[] = "default ACL", DefaultAclRelationId, DefaultAclOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_default_acl_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -248,8 +253,8 @@ static const ObjectPropertyType ObjectProperty[] = "extension", ExtensionRelationId, ExtensionOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_extension_oid, Anum_pg_extension_extname, InvalidAttrNumber, /* extension doesn't belong to extnamespace */ @@ -291,7 +296,7 @@ static const ObjectPropertyType ObjectProperty[] = ProcedureRelationId, ProcedureOidIndexId, PROCOID, - -1, /* PROCNAMEARGSNSP also takes argument types */ + SYSCACHEID_INVALID, /* PROCNAMEARGSNSP also takes argument types */ Anum_pg_proc_oid, Anum_pg_proc_proname, Anum_pg_proc_pronamespace, @@ -318,8 +323,8 @@ static const ObjectPropertyType ObjectProperty[] = "large object metadata", LargeObjectMetadataRelationId, LargeObjectMetadataOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_largeobject_metadata_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -333,7 +338,7 @@ static const ObjectPropertyType ObjectProperty[] = OperatorClassRelationId, OpclassOidIndexId, CLAOID, - -1, /* CLAAMNAMENSP also takes opcmethod */ + SYSCACHEID_INVALID, /* CLAAMNAMENSP also takes opcmethod */ Anum_pg_opclass_oid, Anum_pg_opclass_opcname, Anum_pg_opclass_opcnamespace, @@ -347,7 +352,7 @@ static const ObjectPropertyType ObjectProperty[] = OperatorRelationId, OperatorOidIndexId, OPEROID, - -1, /* OPERNAMENSP also takes left and right type */ + SYSCACHEID_INVALID, /* OPERNAMENSP also takes left and right type */ Anum_pg_operator_oid, Anum_pg_operator_oprname, Anum_pg_operator_oprnamespace, @@ -361,7 +366,7 @@ static const ObjectPropertyType ObjectProperty[] = OperatorFamilyRelationId, OpfamilyOidIndexId, OPFAMILYOID, - -1, /* OPFAMILYAMNAMENSP also takes opfmethod */ + SYSCACHEID_INVALID, /* OPFAMILYAMNAMENSP also takes opfmethod */ Anum_pg_opfamily_oid, Anum_pg_opfamily_opfname, Anum_pg_opfamily_opfnamespace, @@ -370,6 +375,76 @@ static const ObjectPropertyType ObjectProperty[] = OBJECT_OPFAMILY, true }, + { + "property graph element", + PropgraphElementRelationId, + PropgraphElementObjectIndexId, + PROPGRAPHELOID, + PROPGRAPHELALIAS, + Anum_pg_propgraph_element_oid, + Anum_pg_propgraph_element_pgealias, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, + { + "property graph element label", + PropgraphElementLabelRelationId, + PropgraphElementLabelObjectIndexId, + -1, + -1, + Anum_pg_propgraph_element_label_oid, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, + { + "property graph label", + PropgraphLabelRelationId, + PropgraphLabelObjectIndexId, + PROPGRAPHLABELOID, + PROPGRAPHLABELNAME, + Anum_pg_propgraph_label_oid, + Anum_pg_propgraph_label_pgllabel, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, + { + "property graph label property", + PropgraphLabelPropertyRelationId, + PropgraphLabelPropertyObjectIndexId, + -1, + -1, + Anum_pg_propgraph_label_property_oid, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, + { + "property graph property", + PropgraphPropertyRelationId, + PropgraphPropertyObjectIndexId, + -1, + PROPGRAPHPROPNAME, + Anum_pg_propgraph_property_oid, + Anum_pg_propgraph_property_pgpname, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, { "role", AuthIdRelationId, @@ -388,8 +463,8 @@ static const ObjectPropertyType ObjectProperty[] = "role membership", AuthMemRelationId, AuthMemOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_auth_members_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -402,8 +477,8 @@ static const ObjectPropertyType ObjectProperty[] = "rule", RewriteRelationId, RewriteOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_rewrite_oid, Anum_pg_rewrite_rulename, InvalidAttrNumber, @@ -445,7 +520,7 @@ static const ObjectPropertyType ObjectProperty[] = TableSpaceRelationId, TablespaceOidIndexId, TABLESPACEOID, - -1, + SYSCACHEID_INVALID, Anum_pg_tablespace_oid, Anum_pg_tablespace_spcname, InvalidAttrNumber, @@ -459,7 +534,7 @@ static const ObjectPropertyType ObjectProperty[] = TransformRelationId, TransformOidIndexId, TRFOID, - -1, + SYSCACHEID_INVALID, Anum_pg_transform_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -472,8 +547,8 @@ static const ObjectPropertyType ObjectProperty[] = "trigger", TriggerRelationId, TriggerOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_trigger_oid, Anum_pg_trigger_tgname, InvalidAttrNumber, @@ -486,8 +561,8 @@ static const ObjectPropertyType ObjectProperty[] = "policy", PolicyRelationId, PolicyOidIndexId, - -1, - -1, + SYSCACHEID_INVALID, + SYSCACHEID_INVALID, Anum_pg_policy_oid, Anum_pg_policy_polname, InvalidAttrNumber, @@ -627,7 +702,7 @@ static const ObjectPropertyType ObjectProperty[] = UserMappingRelationId, UserMappingOidIndexId, USERMAPPINGOID, - -1, + SYSCACHEID_INVALID, Anum_pg_user_mapping_oid, InvalidAttrNumber, InvalidAttrNumber, @@ -679,6 +754,9 @@ static const struct object_type_map { "foreign table", OBJECT_FOREIGN_TABLE }, + { + "property graph", OBJECT_PROPGRAPH + }, { "table column", OBJECT_COLUMN }, @@ -814,6 +892,15 @@ static const struct object_type_map { "policy", OBJECT_POLICY }, + { + "property graph element", -1 + }, + { + "property graph label", -1 + }, + { + "property graph property", -1 + }, { "publication", OBJECT_PUBLICATION }, @@ -949,6 +1036,7 @@ get_object_address(ObjectType objtype, Node *object, case OBJECT_VIEW: case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: + case OBJECT_PROPGRAPH: address = get_relation_by_qualified_name(objtype, castNode(List, object), &relation, lockmode, @@ -1361,6 +1449,13 @@ get_relation_by_qualified_name(ObjectType objtype, List *object, errmsg("\"%s\" is not an index", RelationGetRelationName(relation)))); break; + case OBJECT_PROPGRAPH: + if (relation->rd_rel->relkind != RELKIND_PROPGRAPH) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a property graph", + RelationGetRelationName(relation)))); + break; case OBJECT_SEQUENCE: if (relation->rd_rel->relkind != RELKIND_SEQUENCE) ereport(ERROR, @@ -2232,7 +2327,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("name list length must be exactly %d", 1))); /* fall through to check args length */ - /* FALLTHROUGH */ + pg_fallthrough; case OBJECT_DOMCONSTRAINT: case OBJECT_CAST: case OBJECT_PUBLICATION_REL: @@ -2257,7 +2352,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("name list length must be at least %d", 3))); /* fall through to check args length */ - /* FALLTHROUGH */ + pg_fallthrough; case OBJECT_OPERATOR: if (list_length(args) != 2) ereport(ERROR, @@ -2280,6 +2375,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_MATVIEW: case OBJECT_INDEX: case OBJECT_FOREIGN_TABLE: + case OBJECT_PROPGRAPH: case OBJECT_COLUMN: case OBJECT_ATTRIBUTE: case OBJECT_COLLATION: @@ -2399,6 +2495,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_VIEW: case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: + case OBJECT_PROPGRAPH: case OBJECT_COLUMN: case OBJECT_RULE: case OBJECT_TRIGGER: @@ -2572,7 +2669,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, Oid get_object_namespace(const ObjectAddress *address) { - int cache; + SysCacheIdentifier cache; HeapTuple tuple; Oid oid; const ObjectPropertyType *property; @@ -2584,7 +2681,7 @@ get_object_namespace(const ObjectAddress *address) /* Currently, we can only handle object types with system caches. */ cache = property->oid_catcache_id; - Assert(cache != -1); + Assert(cache != SYSCACHEID_INVALID); /* Fetch tuple from syscache and extract namespace attribute. */ tuple = SearchSysCache1(cache, ObjectIdGetDatum(address->objectId)); @@ -2641,7 +2738,7 @@ get_object_oid_index(Oid class_id) return prop->oid_index_oid; } -int +SysCacheIdentifier get_object_catcache_oid(Oid class_id) { const ObjectPropertyType *prop = get_object_property_data(class_id); @@ -2649,7 +2746,7 @@ get_object_catcache_oid(Oid class_id) return prop->oid_catcache_id; } -int +SysCacheIdentifier get_object_catcache_name(Oid class_id) { const ObjectPropertyType *prop = get_object_property_data(class_id); @@ -2807,9 +2904,9 @@ get_catalog_object_by_oid_extended(Relation catalog, { HeapTuple tuple; Oid classId = RelationGetRelid(catalog); - int oidCacheId = get_object_catcache_oid(classId); + SysCacheIdentifier oidCacheId = get_object_catcache_oid(classId); - if (oidCacheId > 0) + if (oidCacheId >= 0) { if (locktup) tuple = SearchSysCacheLockedCopy1(oidCacheId, @@ -2942,7 +3039,7 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) case ProcedureRelationId: { - bits16 flags = FORMAT_PROC_INVALID_AS_NULL; + uint16 flags = FORMAT_PROC_INVALID_AS_NULL; char *proname = format_procedure_extended(object->objectId, flags); @@ -2955,7 +3052,7 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) case TypeRelationId: { - bits16 flags = FORMAT_TYPE_INVALID_AS_NULL; + uint16 flags = FORMAT_TYPE_INVALID_AS_NULL; char *typname = format_type_extended(object->objectId, -1, flags); @@ -3148,7 +3245,7 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) case OperatorRelationId: { - bits16 flags = FORMAT_OPERATOR_INVALID_AS_NULL; + uint16 flags = FORMAT_OPERATOR_INVALID_AS_NULL; char *oprname = format_operator_extended(object->objectId, flags); @@ -3976,6 +4073,156 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case PropgraphElementRelationId: + { + HeapTuple tup; + Form_pg_propgraph_element pgeform; + + tup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for property graph element %u", + object->objectId); + break; + } + + pgeform = (Form_pg_propgraph_element) GETSTRUCT(tup); + + if (pgeform->pgekind == PGEKIND_VERTEX) + /* translator: followed by, e.g., "property graph %s" */ + appendStringInfo(&buffer, _("vertex %s of "), NameStr(pgeform->pgealias)); + else if (pgeform->pgekind == PGEKIND_EDGE) + /* translator: followed by, e.g., "property graph %s" */ + appendStringInfo(&buffer, _("edge %s of "), NameStr(pgeform->pgealias)); + else + appendStringInfo(&buffer, "??? element %s of ", NameStr(pgeform->pgealias)); + getRelationDescription(&buffer, pgeform->pgepgid, false); + + ReleaseSysCache(tup); + break; + } + + case PropgraphElementLabelRelationId: + { + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + Form_pg_propgraph_element_label pgelform; + ObjectAddress oa; + + rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(object->objectId)); + + scan = systable_beginscan(rel, PropgraphElementLabelObjectIndexId, true, NULL, 1, key); + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for element label %u", object->objectId); + + systable_endscan(scan); + table_close(rel, AccessShareLock); + break; + } + + pgelform = (Form_pg_propgraph_element_label) GETSTRUCT(tuple); + + appendStringInfo(&buffer, _("label %s of "), get_propgraph_label_name(pgelform->pgellabelid)); + ObjectAddressSet(oa, PropgraphElementRelationId, pgelform->pgelelid); + appendStringInfoString(&buffer, getObjectDescription(&oa, false)); + + systable_endscan(scan); + table_close(rel, AccessShareLock); + break; + } + + case PropgraphLabelRelationId: + { + HeapTuple tuple; + Form_pg_propgraph_label pglform; + + tuple = SearchSysCache1(PROPGRAPHLABELOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tuple)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for label %u", object->objectId); + break; + } + + pglform = (Form_pg_propgraph_label) GETSTRUCT(tuple); + + /* translator: followed by, e.g., "property graph %s" */ + appendStringInfo(&buffer, _("label %s of "), NameStr(pglform->pgllabel)); + getRelationDescription(&buffer, pglform->pglpgid, false); + ReleaseSysCache(tuple); + break; + } + + case PropgraphLabelPropertyRelationId: + { + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + Form_pg_propgraph_label_property plpform; + ObjectAddress oa; + + rel = table_open(PropgraphLabelPropertyRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_label_property_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(object->objectId)); + + scan = systable_beginscan(rel, PropgraphLabelPropertyObjectIndexId, true, NULL, 1, key); + tuple = systable_getnext(scan); + if (!HeapTupleIsValid(tuple)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for label property %u", object->objectId); + + systable_endscan(scan); + table_close(rel, AccessShareLock); + break; + } + + plpform = (Form_pg_propgraph_label_property) GETSTRUCT(tuple); + + appendStringInfo(&buffer, _("property %s of "), get_propgraph_property_name(plpform->plppropid)); + ObjectAddressSet(oa, PropgraphElementLabelRelationId, plpform->plpellabelid); + appendStringInfoString(&buffer, getObjectDescription(&oa, false)); + + systable_endscan(scan); + table_close(rel, AccessShareLock); + break; + } + + case PropgraphPropertyRelationId: + { + HeapTuple tuple; + Form_pg_propgraph_property pgpform; + + tuple = SearchSysCache1(PROPGRAPHPROPOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tuple)) + { + if (!missing_ok) + elog(ERROR, "could not find tuple for property %u", object->objectId); + break; + } + + pgpform = (Form_pg_propgraph_property) GETSTRUCT(tuple); + + /* translator: followed by, e.g., "property graph %s" */ + appendStringInfo(&buffer, _("property %s of "), NameStr(pgpform->pgpname)); + getRelationDescription(&buffer, pgpform->pgppgid, false); + ReleaseSysCache(tuple); + break; + } + case PublicationRelationId: { char *pubname = get_publication_name(object->objectId, @@ -4161,6 +4408,10 @@ getRelationDescription(StringInfo buffer, Oid relid, bool missing_ok) appendStringInfo(buffer, _("foreign table %s"), relname); break; + case RELKIND_PROPGRAPH: + appendStringInfo(buffer, _("property graph %s"), + relname); + break; default: /* shouldn't get here */ appendStringInfo(buffer, _("relation %s"), @@ -4283,8 +4534,8 @@ pg_identify_object(PG_FUNCTION_ARGS) nspAttnum = get_object_attnum_namespace(address.classId); if (nspAttnum != InvalidAttrNumber) { - schema_oid = heap_getattr(objtup, nspAttnum, - RelationGetDescr(catalog), &isnull); + schema_oid = DatumGetObjectId(heap_getattr(objtup, nspAttnum, + RelationGetDescr(catalog), &isnull)); if (isnull) elog(ERROR, "invalid null namespace in object %u/%u/%d", address.classId, address.objectId, address.objectSubId); @@ -4650,6 +4901,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) appendStringInfoString(&buffer, "policy"); break; + case PropgraphElementRelationId: + appendStringInfoString(&buffer, "property graph element"); + break; + + case PropgraphLabelRelationId: + appendStringInfoString(&buffer, "property graph label"); + break; + + case PropgraphPropertyRelationId: + appendStringInfoString(&buffer, "property graph property"); + break; + case PublicationRelationId: appendStringInfoString(&buffer, "publication"); break; @@ -4731,6 +4994,9 @@ getRelationTypeDescription(StringInfo buffer, Oid relid, int32 objectSubId, case RELKIND_FOREIGN_TABLE: appendStringInfoString(buffer, "foreign table"); break; + case RELKIND_PROPGRAPH: + appendStringInfoString(buffer, "property graph"); + break; default: /* shouldn't get here */ appendStringInfoString(buffer, "relation"); @@ -4850,7 +5116,7 @@ getObjectIdentityParts(const ObjectAddress *object, * will be initialized in all cases inside the switch; but we do it anyway * so that we can test below that no branch leaves it unset. */ - Assert(PointerIsValid(objname) == PointerIsValid(objargs)); + Assert((objname != NULL) == (objargs != NULL)); if (objname) { *objname = NIL; @@ -4894,7 +5160,7 @@ getObjectIdentityParts(const ObjectAddress *object, case ProcedureRelationId: { - bits16 flags = FORMAT_PROC_FORCE_QUALIFY | FORMAT_PROC_INVALID_AS_NULL; + uint16 flags = FORMAT_PROC_FORCE_QUALIFY | FORMAT_PROC_INVALID_AS_NULL; char *proname = format_procedure_extended(object->objectId, flags); @@ -4910,7 +5176,7 @@ getObjectIdentityParts(const ObjectAddress *object, case TypeRelationId: { - bits16 flags = FORMAT_TYPE_INVALID_AS_NULL | FORMAT_TYPE_FORCE_QUALIFY; + uint16 flags = FORMAT_TYPE_INVALID_AS_NULL | FORMAT_TYPE_FORCE_QUALIFY; char *typeout; typeout = format_type_extended(object->objectId, -1, flags); @@ -5117,7 +5383,7 @@ getObjectIdentityParts(const ObjectAddress *object, case OperatorRelationId: { - bits16 flags = FORMAT_OPERATOR_FORCE_QUALIFY | FORMAT_OPERATOR_INVALID_AS_NULL; + uint16 flags = FORMAT_OPERATOR_FORCE_QUALIFY | FORMAT_OPERATOR_INVALID_AS_NULL; char *oprname = format_operator_extended(object->objectId, flags); @@ -5895,6 +6161,73 @@ getObjectIdentityParts(const ObjectAddress *object, break; } + case PropgraphElementRelationId: + { + HeapTuple tup; + Form_pg_propgraph_element pge; + + tup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for property graph element %u", object->objectId); + break; + } + pge = (Form_pg_propgraph_element) GETSTRUCT(tup); + appendStringInfo(&buffer, "%s of ", quote_identifier(NameStr(pge->pgealias))); + + getRelationIdentity(&buffer, pge->pgepgid, objname, false); + if (objname) + *objname = lappend(*objname, pstrdup(NameStr(pge->pgealias))); + + ReleaseSysCache(tup); + break; + } + + case PropgraphLabelRelationId: + { + HeapTuple tup; + Form_pg_propgraph_label pgl; + + tup = SearchSysCache1(PROPGRAPHLABELOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for property graph label %u", object->objectId); + break; + } + + pgl = (Form_pg_propgraph_label) GETSTRUCT(tup); + appendStringInfo(&buffer, "%s of ", quote_identifier(NameStr(pgl->pgllabel))); + getRelationIdentity(&buffer, pgl->pglpgid, objname, false); + if (objname) + *objname = lappend(*objname, pstrdup(NameStr(pgl->pgllabel))); + ReleaseSysCache(tup); + break; + } + + case PropgraphPropertyRelationId: + { + HeapTuple tup; + Form_pg_propgraph_property pgp; + + tup = SearchSysCache1(PROPGRAPHPROPOID, ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(tup)) + { + if (!missing_ok) + elog(ERROR, "cache lookup failed for property graph property %u", object->objectId); + break; + } + + pgp = (Form_pg_propgraph_property) GETSTRUCT(tup); + appendStringInfo(&buffer, "%s of ", quote_identifier(NameStr(pgp->pgpname))); + getRelationIdentity(&buffer, pgp->pgppgid, objname, false); + if (objname) + *objname = lappend(*objname, pstrdup(NameStr(pgp->pgpname))); + ReleaseSysCache(tup); + break; + } + case PublicationRelationId: { char *pubname; @@ -6145,8 +6478,8 @@ strlist_to_textarray(List *list) ALLOCSET_DEFAULT_SIZES); oldcxt = MemoryContextSwitchTo(memcxt); - datums = (Datum *) palloc(sizeof(Datum) * list_length(list)); - nulls = palloc(sizeof(bool) * list_length(list)); + datums = palloc_array(Datum, list_length(list)); + nulls = palloc_array(bool, list_length(list)); foreach(cell, list) { @@ -6201,6 +6534,8 @@ get_relkind_objtype(char relkind) return OBJECT_MATVIEW; case RELKIND_FOREIGN_TABLE: return OBJECT_FOREIGN_TABLE; + case RELKIND_PROPGRAPH: + return OBJECT_PROPGRAPH; case RELKIND_TOASTVALUE: return OBJECT_TABLE; default: diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c index 93d72157a46ae..28f3cade6ffec 100644 --- a/src/backend/catalog/partition.c +++ b/src/backend/catalog/partition.c @@ -3,7 +3,7 @@ * partition.c * Partitioning related data structures and functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c index a05f8a87c1f83..243b952b9ccd7 100644 --- a/src/backend/catalog/pg_aggregate.c +++ b/src/backend/catalog/pg_aggregate.c @@ -3,7 +3,7 @@ * pg_aggregate.c * routines to support manipulation of the pg_aggregate relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -654,7 +654,7 @@ AggregateCreate(const char *aggName, for (i = 0; i < Natts_pg_aggregate; i++) { nulls[i] = false; - values[i] = (Datum) NULL; + values[i] = (Datum) 0; replaces[i] = true; } values[Anum_pg_aggregate_aggfnoid - 1] = ObjectIdGetDatum(procOid); @@ -836,6 +836,7 @@ lookup_agg_function(List *fnName, Oid vatype; Oid *true_oid_array; FuncDetailCode fdresult; + int fgc_flags; AclResult aclresult; int i; @@ -848,6 +849,7 @@ lookup_agg_function(List *fnName, */ fdresult = func_get_detail(fnName, NIL, NIL, nargs, input_types, false, false, false, + &fgc_flags, &fnOid, rettype, &retset, &nvargs, &vatype, &true_oid_array, NULL); diff --git a/src/backend/catalog/pg_attrdef.c b/src/backend/catalog/pg_attrdef.c index 1b6270b121324..24815090d3d79 100644 --- a/src/backend/catalog/pg_attrdef.c +++ b/src/backend/catalog/pg_attrdef.c @@ -3,7 +3,7 @@ * pg_attrdef.c * routines to support manipulation of the pg_attrdef relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,8 @@ */ #include "postgres.h" +#include "access/genam.h" +#include "access/htup_details.h" #include "access/relation.h" #include "access/table.h" #include "catalog/dependency.h" diff --git a/src/backend/catalog/pg_cast.c b/src/backend/catalog/pg_cast.c index 1773c9c549160..5119c2acda2ad 100644 --- a/src/backend/catalog/pg_cast.c +++ b/src/backend/catalog/pg_cast.c @@ -3,7 +3,7 @@ * pg_cast.c * routines to support manipulation of the pg_cast relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/catalog/pg_class.c b/src/backend/catalog/pg_class.c index 18eecbdfc0648..0a927ac46f2cd 100644 --- a/src/backend/catalog/pg_class.c +++ b/src/backend/catalog/pg_class.c @@ -3,7 +3,7 @@ * pg_class.c * routines to support manipulation of the pg_class relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -45,6 +45,8 @@ errdetail_relkind_not_supported(char relkind) return errdetail("This operation is not supported for partitioned tables."); case RELKIND_PARTITIONED_INDEX: return errdetail("This operation is not supported for partitioned indexes."); + case RELKIND_PROPGRAPH: + return errdetail("This operation is not supported for property graphs."); default: elog(ERROR, "unrecognized relkind: '%c'", relkind); return 0; diff --git a/src/backend/catalog/pg_collation.c b/src/backend/catalog/pg_collation.c index 469635b35808d..26d42c3e51ac1 100644 --- a/src/backend/catalog/pg_collation.c +++ b/src/backend/catalog/pg_collation.c @@ -3,7 +3,7 @@ * pg_collation.c * routines to support manipulation of the pg_collation relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 2d5ac1ea8138b..b12765ae69142 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -3,7 +3,7 @@ * pg_constraint.c * routines to support manipulation of the pg_constraint relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -179,7 +179,7 @@ CreateConstraintEntry(const char *constraintName, for (i = 0; i < Natts_pg_constraint; i++) { nulls[i] = false; - values[i] = (Datum) NULL; + values[i] = (Datum) 0; } conOid = GetNewOidWithIndex(conDesc, ConstraintOidIndexId, @@ -731,14 +731,15 @@ extractNotNullColumn(HeapTuple constrTup) * If a constraint exists but the connoinherit flag is not what the caller * wants, throw an error about the incompatibility. If the desired * constraint is valid but the existing constraint is not valid, also - * throw an error about that (the opposite case is acceptable). + * throw an error about that (the opposite case is acceptable). If + * the proposed constraint has a different name, also throw an error. * * If everything checks out, we adjust conislocal/coninhcount and return * true. If is_local is true we flip conislocal true, or do nothing if * it's already true; otherwise we increment coninhcount by 1. */ bool -AdjustNotNullInheritance(Oid relid, AttrNumber attnum, +AdjustNotNullInheritance(Oid relid, AttrNumber attnum, const char *new_conname, bool is_local, bool is_no_inherit, bool is_notvalid) { HeapTuple tup; @@ -777,6 +778,22 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum, errhint("You might need to validate it using %s.", "ALTER TABLE ... VALIDATE CONSTRAINT")); + /* + * If, for a new constraint that is being defined locally (i.e., not + * being passed down via inheritance), a name was specified, then + * verify that the existing constraint has the same name. Otherwise + * throw an error. Names of inherited constraints are ignored because + * they are not directly user-specified, so matching is not important. + */ + if (is_local && new_conname && + strcmp(new_conname, NameStr(conform->conname)) != 0) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot create not-null constraint \"%s\" on column \"%s\" of table \"%s\"", + new_conname, get_attname(relid, attnum, false), get_rel_name(relid)), + errdetail("A not-null constraint named \"%s\" already exists for this column.", + NameStr(conform->conname))); + if (!is_local) { if (pg_add_s16_overflow(conform->coninhcount, 1, @@ -846,7 +863,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh) { CookedConstraint *cooked; - cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + cooked = palloc_object(CookedConstraint); cooked->contype = CONSTR_NOTNULL; cooked->conoid = conForm->oid; @@ -875,7 +892,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh) false))); constr->is_enforced = true; constr->skip_validation = !conForm->convalidated; - constr->initially_valid = true; + constr->initially_valid = conForm->convalidated; constr->is_no_inherit = conForm->connoinherit; notnulls = lappend(notnulls, constr); } @@ -937,10 +954,12 @@ RemoveConstraintById(Oid conId) con->conrelid); classForm = (Form_pg_class) GETSTRUCT(relTup); - if (classForm->relchecks == 0) /* should not happen */ - elog(ERROR, "relation \"%s\" has relchecks = 0", - RelationGetRelationName(rel)); - classForm->relchecks--; + if (classForm->relchecks > 0) + classForm->relchecks--; + else + /* should not happen */ + elog(WARNING, "relation \"%s\" has relchecks = %d", + RelationGetRelationName(rel), classForm->relchecks); CatalogTupleUpdate(pgrel, &relTup->t_self, relTup); @@ -1542,7 +1561,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks, if (numkeys <= 0 || numkeys > INDEX_MAX_KEYS) elog(ERROR, "foreign key constraint cannot have %d columns", numkeys); memcpy(conkey, ARR_DATA_PTR(arr), numkeys * sizeof(int16)); - if ((Pointer) arr != DatumGetPointer(adatum)) + if (arr != DatumGetPointer(adatum)) pfree(arr); /* free de-toasted copy, if any */ adatum = SysCacheGetAttrNotNull(CONSTROID, tuple, @@ -1554,7 +1573,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks, ARR_ELEMTYPE(arr) != INT2OID) elog(ERROR, "confkey is not a 1-D smallint array"); memcpy(confkey, ARR_DATA_PTR(arr), numkeys * sizeof(int16)); - if ((Pointer) arr != DatumGetPointer(adatum)) + if (arr != DatumGetPointer(adatum)) pfree(arr); /* free de-toasted copy, if any */ if (pf_eq_oprs) @@ -1569,7 +1588,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks, ARR_ELEMTYPE(arr) != OIDOID) elog(ERROR, "conpfeqop is not a 1-D Oid array"); memcpy(pf_eq_oprs, ARR_DATA_PTR(arr), numkeys * sizeof(Oid)); - if ((Pointer) arr != DatumGetPointer(adatum)) + if (arr != DatumGetPointer(adatum)) pfree(arr); /* free de-toasted copy, if any */ } @@ -1584,7 +1603,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks, ARR_ELEMTYPE(arr) != OIDOID) elog(ERROR, "conppeqop is not a 1-D Oid array"); memcpy(pp_eq_oprs, ARR_DATA_PTR(arr), numkeys * sizeof(Oid)); - if ((Pointer) arr != DatumGetPointer(adatum)) + if (arr != DatumGetPointer(adatum)) pfree(arr); /* free de-toasted copy, if any */ } @@ -1599,7 +1618,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks, ARR_ELEMTYPE(arr) != OIDOID) elog(ERROR, "conffeqop is not a 1-D Oid array"); memcpy(ff_eq_oprs, ARR_DATA_PTR(arr), numkeys * sizeof(Oid)); - if ((Pointer) arr != DatumGetPointer(adatum)) + if (arr != DatumGetPointer(adatum)) pfree(arr); /* free de-toasted copy, if any */ } @@ -1622,7 +1641,7 @@ DeconstructFkConstraintRow(HeapTuple tuple, int *numfks, elog(ERROR, "confdelsetcols is not a 1-D smallint array"); num_delete_cols = ARR_DIMS(arr)[0]; memcpy(fk_del_set_cols, ARR_DATA_PTR(arr), num_delete_cols * sizeof(int16)); - if ((Pointer) arr != DatumGetPointer(adatum)) + if (arr != DatumGetPointer(adatum)) pfree(arr); /* free de-toasted copy, if any */ *num_fk_del_set_cols = num_delete_cols; diff --git a/src/backend/catalog/pg_conversion.c b/src/backend/catalog/pg_conversion.c index 04cc375caea8c..0accc83cb1e36 100644 --- a/src/backend/catalog/pg_conversion.c +++ b/src/backend/catalog/pg_conversion.c @@ -3,7 +3,7 @@ * pg_conversion.c * routines to support manipulation of the pg_conversion relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -87,7 +87,7 @@ ConversionCreate(const char *conname, Oid connamespace, for (i = 0; i < Natts_pg_conversion; i++) { nulls[i] = false; - values[i] = (Datum) NULL; + values[i] = (Datum) 0; } /* form a tuple */ diff --git a/src/backend/catalog/pg_db_role_setting.c b/src/backend/catalog/pg_db_role_setting.c index 090fc07c28acb..57d5f5f391b3b 100644 --- a/src/backend/catalog/pg_db_role_setting.c +++ b/src/backend/catalog/pg_db_role_setting.c @@ -2,7 +2,7 @@ * pg_db_role_setting.c * Routines to support manipulation of the pg_db_role_setting relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -151,6 +151,15 @@ AlterSetting(Oid databaseid, Oid roleid, VariableSetStmt *setstmt) CatalogTupleInsert(rel, newtuple); } + else + { + /* + * RESET doesn't need to change any state if there's no pre-existing + * pg_db_role_setting entry, but for consistency we should still check + * that the option is valid and we're allowed to set it. + */ + (void) GUCArrayDelete(NULL, setstmt->name); + } InvokeObjectPostAlterHookArg(DbRoleSettingRelationId, databaseid, 0, roleid, false); diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c index c8b11f887e274..07c2d41c18968 100644 --- a/src/backend/catalog/pg_depend.c +++ b/src/backend/catalog/pg_depend.c @@ -3,7 +3,7 @@ * pg_depend.c * routines to support manipulation of the pg_depend relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -23,12 +23,14 @@ #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" #include "catalog/pg_extension.h" +#include "catalog/pg_type.h" #include "catalog/partition.h" #include "commands/extension.h" #include "miscadmin.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/syscache.h" static bool isObjectPinned(const ObjectAddress *object); @@ -88,7 +90,7 @@ recordMultipleDependencies(const ObjectAddress *depender, */ max_slots = Min(nreferenced, MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_depend)); - slot = palloc(sizeof(TupleTableSlot *) * max_slots); + slot = palloc_array(TupleTableSlot *, max_slots); /* Don't open indexes unless we need to make an update */ indstate = NULL; @@ -813,6 +815,77 @@ getAutoExtensionsOfObject(Oid classId, Oid objectId) return result; } +/* + * Look up a type belonging to an extension. + * + * Returns the type's OID, or InvalidOid if not found. + * + * Notice that the type is specified by name only, without a schema. + * That's because this will typically be used by relocatable extensions + * which can't make a-priori assumptions about which schema their objects + * are in. As long as the extension only defines one type of this name, + * the answer is unique anyway. + * + * We might later add the ability to look up functions, operators, etc. + */ +Oid +getExtensionType(Oid extensionOid, const char *typname) +{ + Oid result = InvalidOid; + Relation depRel; + ScanKeyData key[3]; + SysScanDesc scan; + HeapTuple tup; + + depRel = table_open(DependRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_depend_refclassid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ExtensionRelationId)); + ScanKeyInit(&key[1], + Anum_pg_depend_refobjid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(extensionOid)); + ScanKeyInit(&key[2], + Anum_pg_depend_refobjsubid, + BTEqualStrategyNumber, F_INT4EQ, + Int32GetDatum(0)); + + scan = systable_beginscan(depRel, DependReferenceIndexId, true, + NULL, 3, key); + + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_depend depform = (Form_pg_depend) GETSTRUCT(tup); + + if (depform->classid == TypeRelationId && + depform->deptype == DEPENDENCY_EXTENSION) + { + Oid typoid = depform->objid; + HeapTuple typtup; + + typtup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typoid)); + if (!HeapTupleIsValid(typtup)) + continue; /* should we throw an error? */ + if (strcmp(NameStr(((Form_pg_type) GETSTRUCT(typtup))->typname), + typname) == 0) + { + result = typoid; + ReleaseSysCache(typtup); + break; /* no need to keep searching */ + } + ReleaseSysCache(typtup); + } + } + + systable_endscan(scan); + + table_close(depRel, AccessShareLock); + + return result; +} + /* * Detect whether a sequence is marked as "owned" by a column * diff --git a/src/backend/catalog/pg_enum.c b/src/backend/catalog/pg_enum.c index a1634e58eecdd..33a461484d418 100644 --- a/src/backend/catalog/pg_enum.c +++ b/src/backend/catalog/pg_enum.c @@ -3,7 +3,7 @@ * pg_enum.c * routines to support manipulation of the pg_enum relation * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -110,12 +110,6 @@ EnumValuesCreate(Oid enumTypeOid, List *vals) num_elems = list_length(vals); - /* - * We do not bother to check the list of values for duplicates --- if you - * have any, you'll get a less-than-friendly unique-index violation. It is - * probably not worth trying harder. - */ - pg_enum = table_open(EnumRelationId, RowExclusiveLock); /* @@ -126,7 +120,7 @@ EnumValuesCreate(Oid enumTypeOid, List *vals) * allocating the next), trouble could only occur if the OID counter wraps * all the way around before we finish. Which seems unlikely. */ - oids = (Oid *) palloc(num_elems * sizeof(Oid)); + oids = palloc_array(Oid, num_elems); for (elemno = 0; elemno < num_elems; elemno++) { @@ -154,7 +148,7 @@ EnumValuesCreate(Oid enumTypeOid, List *vals) /* allocate the slots to use and initialize them */ nslots = Min(num_elems, MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_enum)); - slot = palloc(sizeof(TupleTableSlot *) * nslots); + slot = palloc_array(TupleTableSlot *, nslots); for (int i = 0; i < nslots; i++) slot[i] = MakeSingleTupleTableSlot(RelationGetDescr(pg_enum), &TTSOpsHeapTuple); @@ -164,6 +158,7 @@ EnumValuesCreate(Oid enumTypeOid, List *vals) { char *lab = strVal(lfirst(lc)); Name enumlabel = palloc0(NAMEDATALEN); + ListCell *lc2; /* * labels are stored in a name field, for easier syscache lookup, so @@ -176,6 +171,24 @@ EnumValuesCreate(Oid enumTypeOid, List *vals) errdetail("Labels must be %d bytes or less.", NAMEDATALEN - 1))); + /* + * Check for duplicate labels. The unique index on pg_enum would catch + * that anyway, but we prefer a friendlier error message. + */ + foreach(lc2, vals) + { + /* Only need to compare lc to earlier entries */ + if (lc2 == lc) + break; + + if (strcmp(lab, strVal(lfirst(lc2))) == 0) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("enum label \"%s\" used more than once", + lab))); + } + + /* OK, construct a tuple for this label */ ExecClearTuple(slot[slotCount]); memset(slot[slotCount]->tts_isnull, false, @@ -362,7 +375,7 @@ AddEnumLabel(Oid enumTypeOid, nelems = list->n_members; /* Sort the existing members by enumsortorder */ - existing = (HeapTuple *) palloc(nelems * sizeof(HeapTuple)); + existing = palloc_array(HeapTuple, nelems); for (i = 0; i < nelems; i++) existing[i] = &(list->members[i]->tuple); diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c index 929bb53b620fe..4b9802aafccc4 100644 --- a/src/backend/catalog/pg_inherits.c +++ b/src/backend/catalog/pg_inherits.c @@ -8,7 +8,7 @@ * Perhaps someday that code should be moved here, but it'd have to be * disentangled from other stuff such as pg_depend updates. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -28,6 +28,7 @@ #include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/fmgroids.h" +#include "utils/hsearch.h" #include "utils/snapmgr.h" #include "utils/syscache.h" diff --git a/src/backend/catalog/pg_largeobject.c b/src/backend/catalog/pg_largeobject.c index 89fc810215099..0e1dd22d9082a 100644 --- a/src/backend/catalog/pg_largeobject.c +++ b/src/backend/catalog/pg_largeobject.c @@ -3,7 +3,7 @@ * pg_largeobject.c * routines to support manipulation of the pg_largeobject relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,8 @@ */ #include "postgres.h" +#include "access/genam.h" +#include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" #include "catalog/indexing.h" diff --git a/src/backend/catalog/pg_namespace.c b/src/backend/catalog/pg_namespace.c index 6f5634a4de69b..f35ef5a344ea1 100644 --- a/src/backend/catalog/pg_namespace.c +++ b/src/backend/catalog/pg_namespace.c @@ -3,7 +3,7 @@ * pg_namespace.c * routines to support manipulation of the pg_namespace relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -76,7 +76,7 @@ NamespaceCreate(const char *nspName, Oid ownerId, bool isTemp) for (i = 0; i < Natts_pg_namespace; i++) { nulls[i] = false; - values[i] = (Datum) NULL; + values[i] = (Datum) 0; } nspoid = GetNewOidWithIndex(nspdesc, NamespaceOidIndexId, diff --git a/src/backend/catalog/pg_operator.c b/src/backend/catalog/pg_operator.c index bfcfa643464ac..6b90c774c18c2 100644 --- a/src/backend/catalog/pg_operator.c +++ b/src/backend/catalog/pg_operator.c @@ -3,7 +3,7 @@ * pg_operator.c * routines to support manipulation of the pg_operator relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -225,7 +225,7 @@ OperatorShellMake(const char *operatorName, for (i = 0; i < Natts_pg_operator; ++i) { nulls[i] = false; - values[i] = (Datum) NULL; /* redundant, but safe */ + values[i] = (Datum) 0; /* redundant, but safe */ } /* @@ -453,7 +453,7 @@ OperatorCreate(const char *operatorName, for (i = 0; i < Natts_pg_operator; ++i) { - values[i] = (Datum) NULL; + values[i] = (Datum) 0; replaces[i] = true; nulls[i] = false; } diff --git a/src/backend/catalog/pg_parameter_acl.c b/src/backend/catalog/pg_parameter_acl.c index 62a05783eb333..4d2fc158a7468 100644 --- a/src/backend/catalog/pg_parameter_acl.c +++ b/src/backend/catalog/pg_parameter_acl.c @@ -3,7 +3,7 @@ * pg_parameter_acl.c * routines to support manipulation of the pg_parameter_acl relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" #include "catalog/indexing.h" diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index 5fdcf24d5f8de..5df4b3f7a91e7 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -3,7 +3,7 @@ * pg_proc.c * routines to support manipulation of the pg_proc relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -20,6 +20,7 @@ #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" +#include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_language.h" #include "catalog/pg_namespace.h" @@ -141,7 +142,8 @@ ProcedureCreate(const char *procedureName, TupleDesc tupDesc; bool is_update; ObjectAddress myself, - referenced; + referenced, + temp_object; char *detailmsg; int i; ObjectAddresses *addrs; @@ -149,7 +151,7 @@ ProcedureCreate(const char *procedureName, /* * sanity checks */ - Assert(PointerIsValid(prosrc)); + Assert(prosrc); parameterCount = parameterTypes->dim1; if (parameterCount < 0 || parameterCount > FUNC_MAX_ARGS) @@ -658,17 +660,40 @@ ProcedureCreate(const char *procedureName, add_exact_object_address(&referenced, addrs); } - record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); - free_object_addresses(addrs); - - /* dependency on SQL routine body */ + /* dependencies appearing in new-style SQL routine body */ if (languageObjectId == SQLlanguageId && prosqlbody) - recordDependencyOnExpr(&myself, prosqlbody, NIL, DEPENDENCY_NORMAL); + collectDependenciesOfExpr(addrs, prosqlbody, NIL); /* dependency on parameter default expressions */ if (parameterDefaults) - recordDependencyOnExpr(&myself, (Node *) parameterDefaults, - NIL, DEPENDENCY_NORMAL); + collectDependenciesOfExpr(addrs, (Node *) parameterDefaults, NIL); + + /* + * Now that we have all the normal dependencies, thumb through them and + * warn if any are to temporary objects. This informs the user if their + * supposedly non-temp function will silently go away at session exit, due + * to a dependency on a temp object. However, do not complain when a + * function created in our own pg_temp namespace refers to other objects + * in that namespace, since then they'll have similar lifespans anyway. + */ + if (find_temp_object(addrs, isTempNamespace(procNamespace), &temp_object)) + ereport(NOTICE, + (errmsg("function \"%s\" will be effectively temporary", + procedureName), + errdetail("It depends on temporary %s.", + getObjectDescription(&temp_object, false)))); + + /* + * Now record all normal dependencies at once. This will also remove any + * duplicates in the list. (Role and extension dependencies are handled + * separately below. Role dependencies would have to be separate anyway + * since they are shared dependencies. An extension dependency could be + * folded into the addrs list, but pg_depend.c doesn't make that easy, and + * it won't duplicate anything we've collected so far anyway.) + */ + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + + free_object_addresses(addrs); /* dependency on owner */ if (!is_update) @@ -1181,7 +1206,7 @@ match_prosrc_to_literal(const char *prosrc, const char *literal, if (cursorpos > 0) newcp++; } - chlen = pg_mblen(prosrc); + chlen = pg_mblen_cstr(prosrc); if (strncmp(prosrc, literal, chlen) != 0) goto fail; prosrc += chlen; @@ -1212,6 +1237,6 @@ oid_array_to_list(Datum datum) deconstruct_array_builtin(array, OIDOID, &values, NULL, &nelems); for (i = 0; i < nelems; i++) - result = lappend_oid(result, values[i]); + result = lappend_oid(result, DatumGetObjectId(values[i])); return result; } diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index d6f94db5d999b..a43d385c60577 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -3,7 +3,7 @@ * pg_publication.c * publication C API manipulation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -53,37 +53,48 @@ typedef struct * error if not. */ static void -check_publication_add_relation(Relation targetrel) +check_publication_add_relation(PublicationRelInfo *pri) { + Relation targetrel = pri->relation; + const char *errormsg; + + if (pri->except) + errormsg = gettext_noop("cannot specify relation \"%s\" in the publication EXCEPT clause"); + else + errormsg = gettext_noop("cannot add relation \"%s\" to publication"); + + /* If in EXCEPT clause, must be root partitioned table */ + if (pri->except && targetrel->rd_rel->relispartition) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg(errormsg, RelationGetRelationName(targetrel)), + errdetail("This operation is not supported for individual partitions."))); + /* Must be a regular or partitioned table */ if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION && RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind))); /* Can't be system table */ if (IsCatalogRelation(targetrel)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail("This operation is not supported for system tables."))); /* UNLOGGED and TEMP relations cannot be part of publication. */ if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail("This operation is not supported for temporary tables."))); else if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot add relation \"%s\" to publication", - RelationGetRelationName(targetrel)), + errmsg(errormsg, RelationGetRelationName(targetrel)), errdetail("This operation is not supported for unlogged tables."))); } @@ -115,8 +126,10 @@ check_publication_add_schema(Oid schemaid) * Returns if relation represented by oid and Form_pg_class entry * is publishable. * - * Does same checks as check_publication_add_relation() above, but does not - * need relation to be opened and also does not throw errors. + * Does same checks as check_publication_add_relation() above except for + * RELKIND_SEQUENCE, but does not need relation to be opened and also does + * not throw errors. Here, the additional check is to support ALL SEQUENCES + * publication. * * XXX This also excludes all tables with relid < FirstNormalObjectId, * ie all tables created during initdb. This mainly affects the preinstalled @@ -134,7 +147,8 @@ static bool is_publishable_class(Oid relid, Form_pg_class reltuple) { return (reltuple->relkind == RELKIND_RELATION || - reltuple->relkind == RELKIND_PARTITIONED_TABLE) && + reltuple->relkind == RELKIND_PARTITIONED_TABLE || + reltuple->relkind == RELKIND_SEQUENCE) && !IsCatalogRelationOid(relid) && reltuple->relpersistence == RELPERSISTENCE_PERMANENT && relid >= FirstNormalObjectId; @@ -149,6 +163,37 @@ is_publishable_relation(Relation rel) return is_publishable_class(RelationGetRelid(rel), rel->rd_rel); } +/* + * Similar to is_publishable_class() but checks whether the given OID + * is a publishable "table" or not. + */ +static bool +is_publishable_table(Oid tableoid) +{ + HeapTuple tuple; + Form_pg_class relform; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(tableoid)); + if (!HeapTupleIsValid(tuple)) + return false; + + relform = (Form_pg_class) GETSTRUCT(tuple); + + /* + * is_publishable_class() includes sequences, so we need to explicitly + * check the relkind to filter them out here. + */ + if (relform->relkind != RELKIND_SEQUENCE && + is_publishable_class(tableoid, relform)) + { + ReleaseSysCache(tuple); + return true; + } + + ReleaseSysCache(tuple); + return false; +} + /* * SQL-callable variant of the above * @@ -256,6 +301,49 @@ is_schema_publication(Oid pubid) return result; } +/* + * Returns true if the publication has explicitly included relation (i.e., + * not marked as EXCEPT). + */ +bool +is_table_publication(Oid pubid) +{ + Relation pubrelsrel; + ScanKeyData scankey; + SysScanDesc scan; + HeapTuple tup; + bool result = false; + + pubrelsrel = table_open(PublicationRelRelationId, AccessShareLock); + ScanKeyInit(&scankey, + Anum_pg_publication_rel_prpubid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(pubid)); + + scan = systable_beginscan(pubrelsrel, + PublicationRelPrpubidIndexId, + true, NULL, 1, &scankey); + tup = systable_getnext(scan); + if (HeapTupleIsValid(tup)) + { + Form_pg_publication_rel pubrel; + + pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); + + /* + * For any publication, pg_publication_rel contains either only EXCEPT + * entries or only explicitly included tables. Therefore, examining + * the first tuple is sufficient to determine table inclusion. + */ + result = !pubrel->prexcept; + } + + systable_endscan(scan); + table_close(pubrelsrel, AccessShareLock); + + return result; +} + /* * Returns true if the relation has column list associated with the * publication, false otherwise. @@ -363,7 +451,7 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level foreach(lc, ancestors) { Oid ancestor = lfirst_oid(lc); - List *apubids = GetRelationPublications(ancestor); + List *apubids = GetRelationIncludedPublications(ancestor); List *aschemaPubids = NIL; level++; @@ -426,7 +514,7 @@ attnumstoint2vector(Bitmapset *attrs) */ ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri, - bool if_not_exists) + bool if_not_exists, AlterPublicationStmt *alter_stmt) { Relation rel; HeapTuple tup; @@ -441,6 +529,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, referenced; List *relids = NIL; int i; + bool inval_except_table; rel = table_open(PublicationRelRelationId, RowExclusiveLock); @@ -463,7 +552,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, RelationGetRelationName(targetrel), pub->name))); } - check_publication_add_relation(targetrel); + check_publication_add_relation(pri); /* Validate and translate column names into a Bitmapset of attnums. */ attnums = pub_collist_validate(pri->relation, pri->columns); @@ -479,6 +568,8 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, ObjectIdGetDatum(pubid); values[Anum_pg_publication_rel_prrelid - 1] = ObjectIdGetDatum(relid); + values[Anum_pg_publication_rel_prexcept - 1] = + BoolGetDatum(pri->except); /* Add qualifications, if available */ if (pri->whereClause != NULL) @@ -527,17 +618,38 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri, table_close(rel, RowExclusiveLock); /* - * Invalidate relcache so that publication info is rebuilt. + * Determine whether EXCEPT tables require explicit relcache invalidation. + * + * For CREATE PUBLICATION with EXCEPT tables, invalidation is skipped + * here, as CreatePublication() function invalidates all relations as part + * of defining a FOR ALL TABLES publication. * - * For the partitioned tables, we must invalidate all partitions contained - * in the respective partition hierarchies, not just the one explicitly - * mentioned in the publication. This is required because we implicitly - * publish the child tables when the parent table is published. + * For ALTER PUBLICATION, invalidation is needed only when adding an + * EXCEPT table to a publication already marked as ALL TABLES. For + * publications that were originally empty or defined as ALL SEQUENCES and + * are being converted to ALL TABLES, invalidation is skipped here, as + * AlterPublicationAllFlags() function invalidates all relations while + * marking the publication as ALL TABLES publication. */ - relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL, - relid); + inval_except_table = (alter_stmt != NULL) && pub->alltables && + (alter_stmt->for_all_tables && pri->except); + + if (!pri->except || inval_except_table) + { + /* + * Invalidate relcache so that publication info is rebuilt. + * + * For the partitioned tables, we must invalidate all partitions + * contained in the respective partition hierarchies, not just the one + * explicitly mentioned in the publication. This is required because + * we implicitly publish the child tables when the parent table is + * published. + */ + relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL, + relid); - InvalidatePublicationRels(relids); + InvalidatePublicationRels(relids); + } return myself; } @@ -746,23 +858,30 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists) return myself; } -/* Gets list of publication oids for a relation */ -List * -GetRelationPublications(Oid relid) +/* + * Internal function to get the list of publication oids for a relation. + * + * If except_flag is true, returns the list of publication that specified the + * relation in the EXCEPT clause; otherwise, returns the list of publications + * in which relation is included. + */ +static List * +get_relation_publications(Oid relid, bool except_flag) { List *result = NIL; CatCList *pubrellist; - int i; /* Find all publications associated with the relation. */ pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP, ObjectIdGetDatum(relid)); - for (i = 0; i < pubrellist->n_members; i++) + for (int i = 0; i < pubrellist->n_members; i++) { HeapTuple tup = &pubrellist->members[i]->tuple; - Oid pubid = ((Form_pg_publication_rel) GETSTRUCT(tup))->prpubid; + Form_pg_publication_rel pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); + Oid pubid = pubrel->prpubid; - result = lappend_oid(result, pubid); + if (pubrel->prexcept == except_flag) + result = lappend_oid(result, pubid); } ReleaseSysCacheList(pubrellist); @@ -771,13 +890,33 @@ GetRelationPublications(Oid relid) } /* - * Gets list of relation oids for a publication. - * - * This should only be used FOR TABLE publications, the FOR ALL TABLES - * should use GetAllTablesPublicationRelations(). + * Gets list of publication oids for a relation. */ List * -GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) +GetRelationIncludedPublications(Oid relid) +{ + return get_relation_publications(relid, false); +} + +/* + * Gets list of publication oids which has relation in the EXCEPT clause. + */ +List * +GetRelationExcludedPublications(Oid relid) +{ + return get_relation_publications(relid, true); +} + +/* + * Internal function to get the list of relation oids for a publication. + * + * If except_flag is true, returns the list of relations specified in the + * EXCEPT clause of the publication; otherwise, returns the list of relations + * included in the publication. + */ +static List * +get_publication_relations(Oid pubid, PublicationPartOpt pub_partopt, + bool except_flag) { List *result; Relation pubrelsrel; @@ -785,7 +924,7 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) SysScanDesc scan; HeapTuple tup; - /* Find all publications associated with the relation. */ + /* Find all relations associated with the publication. */ pubrelsrel = table_open(PublicationRelRelationId, AccessShareLock); ScanKeyInit(&scankey, @@ -802,8 +941,10 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) Form_pg_publication_rel pubrel; pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); - result = GetPubPartitionOptionRelations(result, pub_partopt, - pubrel->prrelid); + + if (except_flag == pubrel->prexcept) + result = GetPubPartitionOptionRelations(result, pub_partopt, + pubrel->prrelid); } systable_endscan(scan); @@ -816,6 +957,34 @@ GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) return result; } +/* + * Gets list of relation oids that are associated with a publication. + * + * This should only be used FOR TABLE publications, the FOR ALL TABLES/SEQUENCES + * should use GetAllPublicationRelations(). + */ +List * +GetIncludedPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt) +{ + Assert(!GetPublication(pubid)->alltables); + + return get_publication_relations(pubid, pub_partopt, false); +} + +/* + * Gets list of table oids that were specified in the EXCEPT clause for a + * publication. + * + * This should only be used FOR ALL TABLES publications. + */ +List * +GetExcludedPublicationTables(Oid pubid, PublicationPartOpt pub_partopt) +{ + Assert(GetPublication(pubid)->alltables); + + return get_publication_relations(pubid, pub_partopt, true); +} + /* * Gets list of publication oids for publications marked as FOR ALL TABLES. */ @@ -854,27 +1023,41 @@ GetAllTablesPublications(void) } /* - * Gets list of all relation published by FOR ALL TABLES publication(s). + * Gets list of all relations published by FOR ALL TABLES/SEQUENCES + * publication. * * If the publication publishes partition changes via their respective root * partitioned tables, we must exclude partitions in favor of including the - * root partitioned tables. + * root partitioned tables. This is not applicable to FOR ALL SEQUENCES + * publication. + * + * For a FOR ALL TABLES publication, the returned list excludes tables mentioned + * in the EXCEPT clause. */ List * -GetAllTablesPublicationRelations(bool pubviaroot) +GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot) { Relation classRel; ScanKeyData key[1]; TableScanDesc scan; HeapTuple tuple; List *result = NIL; + List *exceptlist = NIL; + + Assert(!(relkind == RELKIND_SEQUENCE && pubviaroot)); + + /* EXCEPT filtering applies only to relations, not sequences */ + if (relkind == RELKIND_RELATION) + exceptlist = GetExcludedPublicationTables(pubid, pubviaroot ? + PUBLICATION_PART_ROOT : + PUBLICATION_PART_LEAF); classRel = table_open(RelationRelationId, AccessShareLock); ScanKeyInit(&key[0], Anum_pg_class_relkind, BTEqualStrategyNumber, F_CHAREQ, - CharGetDatum(RELKIND_RELATION)); + CharGetDatum(relkind)); scan = table_beginscan_catalog(classRel, 1, key); @@ -884,7 +1067,8 @@ GetAllTablesPublicationRelations(bool pubviaroot) Oid relid = relForm->oid; if (is_publishable_class(relid, relForm) && - !(relForm->relispartition && pubviaroot)) + !(relForm->relispartition && pubviaroot) && + !list_member_oid(exceptlist, relid)) result = lappend_oid(result, relid); } @@ -905,7 +1089,8 @@ GetAllTablesPublicationRelations(bool pubviaroot) Oid relid = relForm->oid; if (is_publishable_class(relid, relForm) && - !relForm->relispartition) + !relForm->relispartition && + !list_member_oid(exceptlist, relid)) result = lappend_oid(result, relid); } @@ -1001,7 +1186,7 @@ GetSchemaPublicationRelations(Oid schemaid, PublicationPartOpt pub_partopt) ScanKeyInit(&key[0], Anum_pg_class_relnamespace, BTEqualStrategyNumber, F_OIDEQ, - schemaid); + ObjectIdGetDatum(schemaid)); /* get all the relations present in the specified schema */ scan = table_beginscan_catalog(classRel, 1, key); @@ -1079,10 +1264,11 @@ GetPublication(Oid pubid) pubform = (Form_pg_publication) GETSTRUCT(tup); - pub = (Publication *) palloc(sizeof(Publication)); + pub = palloc_object(Publication); pub->oid = pubid; pub->name = pstrdup(NameStr(pubform->pubname)); pub->alltables = pubform->puballtables; + pub->allsequences = pubform->puballsequences; pub->pubactions.pubinsert = pubform->pubinsert; pub->pubactions.pubupdate = pubform->pubupdate; pub->pubactions.pubdelete = pubform->pubdelete; @@ -1109,12 +1295,116 @@ GetPublicationByName(const char *pubname, bool missing_ok) } /* - * Get information of the tables in the given publication array. + * A helper function for pg_get_publication_tables() to check whether the + * table with the given relid is published in the specified publication. + * + * This function evaluates the effective published OID based on the + * publish_via_partition_root setting, rather than just checking catalog entries + * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is + * false, it returns false for a parent partitioned table and returns true + * for its leaf partitions, even if the parent is the one explicitly added + * to the publication. * - * Returns pubid, relid, column list, row filter for each table. + * For performance reasons, this function avoids the overhead of constructing + * the complete list of published tables during the evaluation. It can execute + * quickly even when the publication contains a large number of relations. + * + * Note: this leaks memory for the ancestors list into the current memory + * context. */ -Datum -pg_get_publication_tables(PG_FUNCTION_ARGS) +static bool +is_table_publishable_in_publication(Oid relid, Publication *pub) +{ + bool relispartition; + List *ancestors = NIL; + + /* + * For non-pubviaroot publications, a partitioned table is never the + * effective published OID; only its leaf partitions can be. + */ + if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE) + return false; + + relispartition = get_rel_relispartition(relid); + + if (relispartition) + ancestors = get_partition_ancestors(relid); + + if (pub->alltables) + { + /* + * ALL TABLES with pubviaroot includes only regular tables or top-most + * partitioned tables -- never child partitions. + */ + if (pub->pubviaroot && relispartition) + return false; + + /* + * For ALL TABLES publications, the table is published unless it + * appears in the EXCEPT clause. Only the top-most can appear in the + * EXCEPT clause, so exclusion must be evaluated at the top-most + * ancestor if it has. These publications store only EXCEPT'ed tables + * in pg_publication_rel, so checking existence is sufficient. + * + * Note that this existence check below would incorrectly return true + * (published) for partitions when pubviaroot is enabled; however, + * that case is already caught and returned false by the above check. + */ + return !SearchSysCacheExists2(PUBLICATIONRELMAP, + ObjectIdGetDatum(ancestors + ? llast_oid(ancestors) : relid), + ObjectIdGetDatum(pub->oid)); + } + + /* + * Non-ALL-TABLE publication cases. + * + * A table is published if it (or a containing schema) was explicitly + * added, or if it is a partition whose ancestor was added. + */ + + /* + * If an ancestor is published, the partition's status depends on + * publish_via_partition_root value. + * + * If it's true, the ancestor's relation OID is the effective published + * OID, so the partition itself should be excluded (return false). + * + * If it's false, the partition is covered by its ancestor's presence in + * the publication, it should be included (return true). + */ + if (relispartition && + OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL))) + return !pub->pubviaroot; + + /* + * Check whether the table is explicitly published via pg_publication_rel + * or pg_publication_namespace. + */ + return (SearchSysCacheExists2(PUBLICATIONRELMAP, + ObjectIdGetDatum(relid), + ObjectIdGetDatum(pub->oid)) || + SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP, + ObjectIdGetDatum(get_rel_namespace(relid)), + ObjectIdGetDatum(pub->oid))); +} + +/* + * Helper function to get information of the tables in the given + * publication(s). + * + * If filter_by_relid is true, only the row(s) for target_relid is returned; + * if target_relid does not exist or is not part of the publications, zero + * rows are returned. If filter_by_relid is false, rows for all tables + * within the specified publications are returned and target_relid is + * ignored. + * + * Returns pubid, relid, column list, and row filter for each table. + */ +static Datum +pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames, + Oid target_relid, bool filter_by_relid, + bool pub_missing_ok) { #define NUM_PUBLICATION_TABLES_ELEM 4 FuncCallContext *funcctx; @@ -1125,7 +1415,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) { TupleDesc tupdesc; MemoryContext oldcontext; - ArrayType *arr; Datum *elems; int nelems, i; @@ -1134,6 +1423,14 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) /* create a function context for cross-call persistence */ funcctx = SRF_FIRSTCALL_INIT(); + /* + * Preliminary check if the specified table can be published in the + * first place. If not, we can return early without checking the given + * publications and the table. + */ + if (filter_by_relid && !is_publishable_table(target_relid)) + SRF_RETURN_DONE(funcctx); + /* switch to memory context appropriate for multiple function calls */ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); @@ -1141,8 +1438,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) * Deconstruct the parameter into elements where each element is a * publication name. */ - arr = PG_GETARG_ARRAYTYPE_P(0); - deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems); + deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems); /* Get Oids of tables from each publication. */ for (i = 0; i < nelems; i++) @@ -1151,30 +1447,48 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) List *pub_elem_tables = NIL; ListCell *lc; - pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false); + pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), + pub_missing_ok); - /* - * Publications support partitioned tables. If - * publish_via_partition_root is false, all changes are replicated - * using leaf partition identity and schema, so we only need - * those. Otherwise, get the partitioned table itself. - */ - if (pub_elem->alltables) - pub_elem_tables = GetAllTablesPublicationRelations(pub_elem->pubviaroot); + if (pub_elem == NULL) + continue; + + if (filter_by_relid) + { + /* Check if the given table is published for the publication */ + if (is_table_publishable_in_publication(target_relid, pub_elem)) + { + pub_elem_tables = list_make1_oid(target_relid); + } + } else { - List *relids, - *schemarelids; - - relids = GetPublicationRelations(pub_elem->oid, - pub_elem->pubviaroot ? - PUBLICATION_PART_ROOT : - PUBLICATION_PART_LEAF); - schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid, - pub_elem->pubviaroot ? - PUBLICATION_PART_ROOT : - PUBLICATION_PART_LEAF); - pub_elem_tables = list_concat_unique_oid(relids, schemarelids); + /* + * Publications support partitioned tables. If + * publish_via_partition_root is false, all changes are + * replicated using leaf partition identity and schema, so we + * only need those. Otherwise, get the partitioned table + * itself. + */ + if (pub_elem->alltables) + pub_elem_tables = GetAllPublicationRelations(pub_elem->oid, + RELKIND_RELATION, + pub_elem->pubviaroot); + else + { + List *relids, + *schemarelids; + + relids = GetIncludedPublicationRelations(pub_elem->oid, + pub_elem->pubviaroot ? + PUBLICATION_PART_ROOT : + PUBLICATION_PART_LEAF); + schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid, + pub_elem->pubviaroot ? + PUBLICATION_PART_ROOT : + PUBLICATION_PART_LEAF); + pub_elem_tables = list_concat_unique_oid(relids, schemarelids); + } } /* @@ -1187,7 +1501,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) */ foreach(lc, pub_elem_tables) { - published_rel *table_info = (published_rel *) palloc(sizeof(published_rel)); + published_rel *table_info = palloc_object(published_rel); table_info->relid = lfirst_oid(lc); table_info->pubid = pub_elem->oid; @@ -1221,6 +1535,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 4, "qual", PG_NODE_TREEOID, -1, 0); + TupleDescFinalize(tupdesc); funcctx->tuple_desc = BlessTupleDesc(tupdesc); funcctx->user_fctx = table_infos; @@ -1290,7 +1605,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) TupleDesc desc = RelationGetDescr(rel); int i; - attnums = (int16 *) palloc(desc->natts * sizeof(int16)); + attnums = palloc_array(int16, desc->natts); for (i = 0; i < desc->natts; i++) { @@ -1332,3 +1647,75 @@ pg_get_publication_tables(PG_FUNCTION_ARGS) SRF_RETURN_DONE(funcctx); } + +Datum +pg_get_publication_tables_a(PG_FUNCTION_ARGS) +{ + /* + * Get information for all tables in the given publications. + * filter_by_relid is false so all tables are returned; pub_missing_ok is + * false for backward compatibility. + */ + return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), + InvalidOid, false, false); +} + +Datum +pg_get_publication_tables_b(PG_FUNCTION_ARGS) +{ + /* + * Get information for the specified table in the given publications. The + * SQL-level function is declared STRICT, so target_relid is guaranteed to + * be non-NULL here. + */ + return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), + PG_GETARG_OID(1), true, true); +} + +/* + * Returns Oids of sequences in a publication. + */ +Datum +pg_get_publication_sequences(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + List *sequences = NIL; + + /* stuff done only on the first call of the function */ + if (SRF_IS_FIRSTCALL()) + { + char *pubname = text_to_cstring(PG_GETARG_TEXT_PP(0)); + Publication *publication; + MemoryContext oldcontext; + + /* create a function context for cross-call persistence */ + funcctx = SRF_FIRSTCALL_INIT(); + + /* switch to memory context appropriate for multiple function calls */ + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + publication = GetPublicationByName(pubname, false); + + if (publication->allsequences) + sequences = GetAllPublicationRelations(publication->oid, + RELKIND_SEQUENCE, + false); + + funcctx->user_fctx = sequences; + + MemoryContextSwitchTo(oldcontext); + } + + /* stuff done on every call of the function */ + funcctx = SRF_PERCALL_SETUP(); + sequences = (List *) funcctx->user_fctx; + + if (funcctx->call_cntr < list_length(sequences)) + { + Oid relid = list_nth_oid(sequences, funcctx->call_cntr); + + SRF_RETURN_NEXT(funcctx, ObjectIdGetDatum(relid)); + } + + SRF_RETURN_DONE(funcctx); +} diff --git a/src/backend/catalog/pg_range.c b/src/backend/catalog/pg_range.c index 8df73e7ab71b4..cb8c79d0e83f3 100644 --- a/src/backend/catalog/pg_range.c +++ b/src/backend/catalog/pg_range.c @@ -3,7 +3,7 @@ * pg_range.c * routines to support manipulation of the pg_range relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -35,7 +35,9 @@ void RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, Oid rangeSubOpclass, RegProcedure rangeCanonical, - RegProcedure rangeSubDiff, Oid multirangeTypeOid) + RegProcedure rangeSubDiff, Oid multirangeTypeOid, + RegProcedure rangeConstruct2, RegProcedure rangeConstruct3, + RegProcedure mltrngConstruct0, RegProcedure mltrngConstruct1, RegProcedure mltrngConstruct2) { Relation pg_range; Datum values[Natts_pg_range]; @@ -57,6 +59,11 @@ RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, values[Anum_pg_range_rngcanonical - 1] = ObjectIdGetDatum(rangeCanonical); values[Anum_pg_range_rngsubdiff - 1] = ObjectIdGetDatum(rangeSubDiff); values[Anum_pg_range_rngmultitypid - 1] = ObjectIdGetDatum(multirangeTypeOid); + values[Anum_pg_range_rngconstruct2 - 1] = ObjectIdGetDatum(rangeConstruct2); + values[Anum_pg_range_rngconstruct3 - 1] = ObjectIdGetDatum(rangeConstruct3); + values[Anum_pg_range_rngmltconstruct0 - 1] = ObjectIdGetDatum(mltrngConstruct0); + values[Anum_pg_range_rngmltconstruct1 - 1] = ObjectIdGetDatum(mltrngConstruct1); + values[Anum_pg_range_rngmltconstruct2 - 1] = ObjectIdGetDatum(mltrngConstruct2); tup = heap_form_tuple(RelationGetDescr(pg_range), values, nulls); diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c index 536191284e803..c9998531b2f4c 100644 --- a/src/backend/catalog/pg_shdepend.c +++ b/src/backend/catalog/pg_shdepend.c @@ -3,7 +3,7 @@ * pg_shdepend.c * routines to support manipulation of the pg_shdepend relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -47,7 +47,6 @@ #include "catalog/pg_type.h" #include "catalog/pg_user_mapping.h" #include "commands/alter.h" -#include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/policy.h" @@ -61,6 +60,7 @@ #include "storage/lmgr.h" #include "utils/acl.h" #include "utils/fmgroids.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" @@ -791,7 +791,7 @@ checkSharedDependencies(Oid classId, Oid objectId, } if (!stored) { - dep = (remoteDep *) palloc(sizeof(remoteDep)); + dep = palloc_object(remoteDep); dep->dbOid = sdepForm->dbid; dep->count = 1; remDeps = lappend(remDeps, dep); @@ -913,7 +913,7 @@ copyTemplateDependencies(Oid templateDbId, Oid newDbId) * know that they will be used. */ max_slots = MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_shdepend); - slot = palloc(sizeof(TupleTableSlot *) * max_slots); + slot = palloc_array(TupleTableSlot *, max_slots); indstate = CatalogOpenIndexes(sdepRel); @@ -956,12 +956,12 @@ copyTemplateDependencies(Oid templateDbId, Oid newDbId) shdep = (Form_pg_shdepend) GETSTRUCT(tup); slot[slot_stored_count]->tts_values[Anum_pg_shdepend_dbid - 1] = ObjectIdGetDatum(newDbId); - slot[slot_stored_count]->tts_values[Anum_pg_shdepend_classid - 1] = shdep->classid; - slot[slot_stored_count]->tts_values[Anum_pg_shdepend_objid - 1] = shdep->objid; - slot[slot_stored_count]->tts_values[Anum_pg_shdepend_objsubid - 1] = shdep->objsubid; - slot[slot_stored_count]->tts_values[Anum_pg_shdepend_refclassid - 1] = shdep->refclassid; - slot[slot_stored_count]->tts_values[Anum_pg_shdepend_refobjid - 1] = shdep->refobjid; - slot[slot_stored_count]->tts_values[Anum_pg_shdepend_deptype - 1] = shdep->deptype; + slot[slot_stored_count]->tts_values[Anum_pg_shdepend_classid - 1] = ObjectIdGetDatum(shdep->classid); + slot[slot_stored_count]->tts_values[Anum_pg_shdepend_objid - 1] = ObjectIdGetDatum(shdep->objid); + slot[slot_stored_count]->tts_values[Anum_pg_shdepend_objsubid - 1] = Int32GetDatum(shdep->objsubid); + slot[slot_stored_count]->tts_values[Anum_pg_shdepend_refclassid - 1] = ObjectIdGetDatum(shdep->refclassid); + slot[slot_stored_count]->tts_values[Anum_pg_shdepend_refobjid - 1] = ObjectIdGetDatum(shdep->refobjid); + slot[slot_stored_count]->tts_values[Anum_pg_shdepend_deptype - 1] = CharGetDatum(shdep->deptype); ExecStoreVirtualTuple(slot[slot_stored_count]); slot_stored_count++; @@ -1458,7 +1458,7 @@ shdepDropOwned(List *roleids, DropBehavior behavior) sdepForm->objid); break; } - /* FALLTHROUGH */ + pg_fallthrough; case SHARED_DEPENDENCY_OWNER: diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index 1395032413e3d..1f1fdc75af6f4 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -3,7 +3,7 @@ * pg_subscription.c * replication subscriptions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -19,15 +19,20 @@ #include "access/htup_details.h" #include "access/tableam.h" #include "catalog/indexing.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "storage/lmgr.h" +#include "storage/lock.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/pg_lsn.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -69,13 +74,15 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal) * Fetch the subscription from the syscache. */ Subscription * -GetSubscription(Oid subid, bool missing_ok) +GetSubscription(Oid subid, bool missing_ok, bool aclcheck) { HeapTuple tup; Subscription *sub; Form_pg_subscription subform; Datum datum; bool isnull; + MemoryContext cxt; + MemoryContext oldcxt; tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid)); @@ -87,9 +94,14 @@ GetSubscription(Oid subid, bool missing_ok) elog(ERROR, "cache lookup failed for subscription %u", subid); } + cxt = AllocSetContextCreate(CurrentMemoryContext, "subscription", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(cxt); + subform = (Form_pg_subscription) GETSTRUCT(tup); - sub = (Subscription *) palloc(sizeof(Subscription)); + sub = palloc_object(Subscription); + sub->cxt = cxt; sub->oid = subid; sub->dbid = subform->subdbid; sub->skiplsn = subform->subskiplsn; @@ -103,12 +115,43 @@ GetSubscription(Oid subid, bool missing_ok) sub->passwordrequired = subform->subpasswordrequired; sub->runasowner = subform->subrunasowner; sub->failover = subform->subfailover; + sub->retaindeadtuples = subform->subretaindeadtuples; + sub->maxretention = subform->submaxretention; + sub->retentionactive = subform->subretentionactive; /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, - tup, - Anum_pg_subscription_subconninfo); - sub->conninfo = TextDatumGetCString(datum); + if (OidIsValid(subform->subserver)) + { + AclResult aclresult; + ForeignServer *server; + + server = GetForeignServer(subform->subserver); + + /* recheck ACL if requested */ + if (aclcheck) + { + aclresult = object_aclcheck(ForeignServerRelationId, + subform->subserver, + subform->subowner, ACL_USAGE); + + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(subform->subowner, false), + server->servername))); + } + + sub->conninfo = ForeignServerConnectionString(subform->subowner, + server); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, + tup, + Anum_pg_subscription_subconninfo); + sub->conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, @@ -126,6 +169,12 @@ GetSubscription(Oid subid, bool missing_ok) Anum_pg_subscription_subsynccommit); sub->synccommit = TextDatumGetCString(datum); + /* Get walrcvtimeout */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, + tup, + Anum_pg_subscription_subwalrcvtimeout); + sub->walrcvtimeout = TextDatumGetCString(datum); + /* Get publications */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, @@ -143,6 +192,8 @@ GetSubscription(Oid subid, bool missing_ok) ReleaseSysCache(tup); + MemoryContextSwitchTo(oldcxt); + return sub; } @@ -179,20 +230,6 @@ CountDBSubscriptions(Oid dbid) return nsubs; } -/* - * Free memory allocated by subscription struct. - */ -void -FreeSubscription(Subscription *sub) -{ - pfree(sub->name); - pfree(sub->conninfo); - if (sub->slotname) - pfree(sub->slotname); - list_free_deep(sub->publications); - pfree(sub); -} - /* * Disable the given subscription. */ @@ -281,7 +318,7 @@ AddSubscriptionRelState(Oid subid, Oid relid, char state, ObjectIdGetDatum(relid), ObjectIdGetDatum(subid)); if (HeapTupleIsValid(tup)) - elog(ERROR, "subscription table %u in subscription %u already exists", + elog(ERROR, "subscription relation %u in subscription %u already exists", relid, subid); /* Form the tuple. */ @@ -290,7 +327,7 @@ AddSubscriptionRelState(Oid subid, Oid relid, char state, values[Anum_pg_subscription_rel_srsubid - 1] = ObjectIdGetDatum(subid); values[Anum_pg_subscription_rel_srrelid - 1] = ObjectIdGetDatum(relid); values[Anum_pg_subscription_rel_srsubstate - 1] = CharGetDatum(state); - if (sublsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(sublsn)) values[Anum_pg_subscription_rel_srsublsn - 1] = LSNGetDatum(sublsn); else nulls[Anum_pg_subscription_rel_srsublsn - 1] = true; @@ -319,7 +356,7 @@ AddSubscriptionRelState(Oid subid, Oid relid, char state, */ void UpdateSubscriptionRelState(Oid subid, Oid relid, char state, - XLogRecPtr sublsn) + XLogRecPtr sublsn, bool already_locked) { Relation rel; HeapTuple tup; @@ -327,16 +364,31 @@ UpdateSubscriptionRelState(Oid subid, Oid relid, char state, Datum values[Natts_pg_subscription_rel]; bool replaces[Natts_pg_subscription_rel]; - LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + if (already_locked) + { +#ifdef USE_ASSERT_CHECKING + LOCKTAG tag; - rel = table_open(SubscriptionRelRelationId, RowExclusiveLock); + Assert(CheckRelationOidLockedByMe(SubscriptionRelRelationId, + RowExclusiveLock, true)); + SET_LOCKTAG_OBJECT(tag, InvalidOid, SubscriptionRelationId, subid, 0); + Assert(LockHeldByMe(&tag, AccessShareLock, true)); +#endif + + rel = table_open(SubscriptionRelRelationId, NoLock); + } + else + { + LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + rel = table_open(SubscriptionRelRelationId, RowExclusiveLock); + } /* Try finding existing mapping. */ tup = SearchSysCacheCopy2(SUBSCRIPTIONRELMAP, ObjectIdGetDatum(relid), ObjectIdGetDatum(subid)); if (!HeapTupleIsValid(tup)) - elog(ERROR, "subscription table %u in subscription %u does not exist", + elog(ERROR, "subscription relation %u in subscription %u does not exist", relid, subid); /* Update the tuple. */ @@ -348,7 +400,7 @@ UpdateSubscriptionRelState(Oid subid, Oid relid, char state, values[Anum_pg_subscription_rel_srsubstate - 1] = CharGetDatum(state); replaces[Anum_pg_subscription_rel_srsublsn - 1] = true; - if (sublsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(sublsn)) values[Anum_pg_subscription_rel_srsublsn - 1] = LSNGetDatum(sublsn); else nulls[Anum_pg_subscription_rel_srsublsn - 1] = true; @@ -460,9 +512,13 @@ RemoveSubscriptionRel(Oid subid, Oid relid) * synchronization is in progress unless the caller updates the * corresponding subscription as well. This is to ensure that we don't * leave tablesync slots or origins in the system when the - * corresponding table is dropped. + * corresponding table is dropped. For sequences, however, it's ok to + * drop them since no separate slots or origins are created during + * synchronization. */ - if (!OidIsValid(subid) && subrel->srsubstate != SUBREL_STATE_READY) + if (!OidIsValid(subid) && + subrel->srsubstate != SUBREL_STATE_READY && + get_rel_relkind(subrel->srrelid) != RELKIND_SEQUENCE) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -488,18 +544,19 @@ RemoveSubscriptionRel(Oid subid, Oid relid) } /* - * Does the subscription have any relations? + * Does the subscription have any tables? * * Use this function only to know true/false, and when you have no need for the * List returned by GetSubscriptionRelations. */ bool -HasSubscriptionRelations(Oid subid) +HasSubscriptionTables(Oid subid) { Relation rel; ScanKeyData skey[1]; SysScanDesc scan; - bool has_subrels; + HeapTuple tup; + bool has_subtables = false; rel = table_open(SubscriptionRelRelationId, AccessShareLock); @@ -511,14 +568,27 @@ HasSubscriptionRelations(Oid subid) scan = systable_beginscan(rel, InvalidOid, false, NULL, 1, skey); - /* If even a single tuple exists then the subscription has tables. */ - has_subrels = HeapTupleIsValid(systable_getnext(scan)); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_subscription_rel subrel; + char relkind; + + subrel = (Form_pg_subscription_rel) GETSTRUCT(tup); + relkind = get_rel_relkind(subrel->srrelid); + + if (relkind == RELKIND_RELATION || + relkind == RELKIND_PARTITIONED_TABLE) + { + has_subtables = true; + break; + } + } /* Cleanup */ systable_endscan(scan); table_close(rel, AccessShareLock); - return has_subrels; + return has_subtables; } /* @@ -529,7 +599,8 @@ HasSubscriptionRelations(Oid subid) * returned list is palloc'ed in the current memory context. */ List * -GetSubscriptionRelations(Oid subid, bool not_ready) +GetSubscriptionRelations(Oid subid, bool tables, bool sequences, + bool not_ready) { List *res = NIL; Relation rel; @@ -538,6 +609,9 @@ GetSubscriptionRelations(Oid subid, bool not_ready) ScanKeyData skey[2]; SysScanDesc scan; + /* One or both of 'tables' and 'sequences' must be true. */ + Assert(tables || sequences); + rel = table_open(SubscriptionRelRelationId, AccessShareLock); ScanKeyInit(&skey[nkeys++], @@ -560,10 +634,25 @@ GetSubscriptionRelations(Oid subid, bool not_ready) SubscriptionRelState *relstate; Datum d; bool isnull; + char relkind; subrel = (Form_pg_subscription_rel) GETSTRUCT(tup); - relstate = (SubscriptionRelState *) palloc(sizeof(SubscriptionRelState)); + /* Relation is either a sequence or a table */ + relkind = get_rel_relkind(subrel->srrelid); + Assert(relkind == RELKIND_SEQUENCE || relkind == RELKIND_RELATION || + relkind == RELKIND_PARTITIONED_TABLE); + + /* Skip sequences if they were not requested */ + if ((relkind == RELKIND_SEQUENCE) && !sequences) + continue; + + /* Skip tables if they were not requested */ + if ((relkind == RELKIND_RELATION || + relkind == RELKIND_PARTITIONED_TABLE) && !tables) + continue; + + relstate = palloc_object(SubscriptionRelState); relstate->relid = subrel->srrelid; relstate->state = subrel->srsubstate; d = SysCacheGetAttr(SUBSCRIPTIONRELMAP, tup, @@ -582,3 +671,42 @@ GetSubscriptionRelations(Oid subid, bool not_ready) return res; } + +/* + * Update the dead tuple retention status for the given subscription. + */ +void +UpdateDeadTupleRetentionStatus(Oid subid, bool active) +{ + Relation rel; + bool nulls[Natts_pg_subscription]; + bool replaces[Natts_pg_subscription]; + Datum values[Natts_pg_subscription]; + HeapTuple tup; + + /* Look up the subscription in the catalog */ + rel = table_open(SubscriptionRelationId, RowExclusiveLock); + tup = SearchSysCacheCopy1(SUBSCRIPTIONOID, ObjectIdGetDatum(subid)); + + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for subscription %u", subid); + + LockSharedObject(SubscriptionRelationId, subid, 0, AccessShareLock); + + /* Form a new tuple. */ + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + /* Set the subscription to disabled. */ + values[Anum_pg_subscription_subretentionactive - 1] = BoolGetDatum(active); + replaces[Anum_pg_subscription_subretentionactive - 1] = true; + + /* Update the catalog */ + tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, + replaces); + CatalogTupleUpdate(rel, &tup->t_self, tup); + heap_freetuple(tup); + + table_close(rel, NoLock); +} diff --git a/src/backend/catalog/pg_tablespace.c b/src/backend/catalog/pg_tablespace.c new file mode 100644 index 0000000000000..99978bfbdbb6e --- /dev/null +++ b/src/backend/catalog/pg_tablespace.c @@ -0,0 +1,90 @@ +/*------------------------------------------------------------------------- + * + * pg_tablespace.c + * routines to support manipulation of the pg_tablespace relation + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/catalog/pg_tablespace.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include +#include + +#include "catalog/pg_tablespace.h" +#include "commands/tablespace.h" +#include "miscadmin.h" + + +/* + * get_tablespace_location + * Get a tablespace's location as a C-string, by its OID + */ +char * +get_tablespace_location(Oid tablespaceOid) +{ + char sourcepath[MAXPGPATH]; + char targetpath[MAXPGPATH]; + int rllen; + struct stat st; + + /* + * It's useful to apply this to pg_class.reltablespace, wherein zero means + * "the database's default tablespace". So, rather than throwing an error + * for zero, we choose to assume that's what is meant. + */ + if (tablespaceOid == InvalidOid) + tablespaceOid = MyDatabaseTableSpace; + + /* + * Return empty string for the cluster's default tablespaces + */ + if (tablespaceOid == DEFAULTTABLESPACE_OID || + tablespaceOid == GLOBALTABLESPACE_OID) + return pstrdup(""); + + /* + * Find the location of the tablespace by reading the symbolic link that + * is in pg_tblspc/. + */ + snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid); + + /* + * Before reading the link, check if the source path is a link or a + * junction point. Note that a directory is possible for a tablespace + * created with allow_in_place_tablespaces enabled. If a directory is + * found, a relative path to the data directory is returned. + */ + if (lstat(sourcepath, &st) < 0) + ereport(ERROR, + errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + sourcepath)); + + if (!S_ISLNK(st.st_mode)) + return pstrdup(sourcepath); + + /* + * In presence of a link or a junction point, return the path pointed to. + */ + rllen = readlink(sourcepath, targetpath, sizeof(targetpath)); + if (rllen < 0) + ereport(ERROR, + errcode_for_file_access(), + errmsg("could not read symbolic link \"%s\": %m", + sourcepath)); + if (rllen >= sizeof(targetpath)) + ereport(ERROR, + errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("symbolic link \"%s\" target is too long", + sourcepath)); + targetpath[rllen] = '\0'; + + return pstrdup(targetpath); +} diff --git a/src/backend/catalog/pg_type.c b/src/backend/catalog/pg_type.c index b36f81afb9d3f..fc369c35aa66b 100644 --- a/src/backend/catalog/pg_type.c +++ b/src/backend/catalog/pg_type.c @@ -3,7 +3,7 @@ * pg_type.c * routines to support manipulation of the pg_type relation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -66,7 +66,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId) NameData name; ObjectAddress address; - Assert(PointerIsValid(typeName)); + Assert(typeName); /* * open pg_type @@ -80,7 +80,7 @@ TypeShellMake(const char *typeName, Oid typeNamespace, Oid ownerId) for (i = 0; i < Natts_pg_type; ++i) { nulls[i] = false; - values[i] = (Datum) NULL; /* redundant, but safe */ + values[i] = (Datum) 0; /* redundant, but safe */ } /* @@ -285,8 +285,7 @@ TypeCreate(Oid newTypeOid, errmsg("alignment \"%c\" is invalid for passed-by-value type of size %d", alignment, internalSize))); } -#if SIZEOF_DATUM == 8 - else if (internalSize == (int16) sizeof(Datum)) + else if (internalSize == (int16) sizeof(int64)) { if (alignment != TYPALIGN_DOUBLE) ereport(ERROR, @@ -294,7 +293,6 @@ TypeCreate(Oid newTypeOid, errmsg("alignment \"%c\" is invalid for passed-by-value type of size %d", alignment, internalSize))); } -#endif else ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), @@ -950,7 +948,7 @@ char * makeMultirangeTypeName(const char *rangeTypeName, Oid typeNamespace) { char *buf; - char *rangestr; + const char *rangestr; /* * If the range type name contains "range" then change that to diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index ebe85337c2877..626054cbcefd8 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -348,6 +348,106 @@ F866 FETCH FIRST clause: PERCENT option NO F867 FETCH FIRST clause: WITH TIES option YES F868 ORDER BY in grouped table YES F869 SQL implementation info population YES +G000 Graph pattern YES SQL/PGQ required +G001 Repeatable-elements match mode YES SQL/PGQ required +G002 Different-edges match mode NO +G003 Explicit REPEATABLE ELEMENTS keyword NO +G004 Path variables NO +G005 Path search prefix in a path pattern NO +G006 Graph pattern KEEP clause: path mode prefix NO +G007 Graph pattern KEEP clause: path search prefix NO +G008 Graph pattern WHERE clause YES SQL/PGQ required +G010 Explicit WALK keyword NO +G011 Advanced path modes: TRAIL NO +G012 Advanced path modes: SIMPLE NO +G013 Advanced path modes: ACYCLIC NO +G014 Explicit PATH/PATHS keywords NO +G015 All path search: explicit ALL keyword NO +G016 Any path search NO +G017 All shortest path search NO +G018 Any shortest path search NO +G019 Counted shortest path search NO +G020 Counted shortest group search NO +G030 Path multiset alternation NO +G031 Path multiset alternation: variable length path operands NO +G032 Path pattern union NO +G033 Path pattern union: variable length path operands NO +G034 Path concatenation YES SQL/PGQ required +G035 Quantified paths NO +G036 Quantified edges NO +G037 Questioned paths NO +G038 Parenthesized path pattern expression NO +G039 Simplified path pattern expression: full defaulting NO +G040 Vertex pattern YES SQL/PGQ required +G041 Non-local element pattern predicates NO +G042 Basic full edge patterns YES SQL/PGQ required +G043 Complete full edge patterns NO +G044 Basic abbreviated edge patterns YES +G045 Complete abbreviated edge patterns NO +G046 Relaxed topological consistency: adjacent vertex patterns NO +G047 Relaxed topological consistency: concise edge patterns NO +G048 Parenthesized path pattern: subpath variable declaration NO +G049 Parenthesized path pattern: path mode prefix NO +G050 Parenthesized path pattern: WHERE clause NO +G051 Parenthesized path pattern: non-local predicates NO +G060 Bounded graph pattern quantifiers NO +G061 Unbounded graph pattern quantifiers NO +G070 Label expression: label disjunction YES SQL/PGQ required +G071 Label expression: label conjunction NO +G072 Label expression: label negation NO +G073 Label expression: individual label name YES SQL/PGQ required +G074 Label expression: wildcard label NO +G075 Parenthesized label expression NO +G080 Simplified path pattern expression: basic defaulting NO +G081 Simplified path pattern expression: full overrides NO +G082 Simplified path pattern expression: basic overrides NO +G090 Property reference YES SQL/PGQ required +G100 ELEMENT_ID function NO +G110 IS DIRECTED predicate NO +G111 IS LABELED predicate NO +G112 IS SOURCE and IS DESTINATION predicate NO +G113 ALL_DIFFERENT predicate NO +G114 SAME predicate NO +G115 PROPERTY_EXISTS predicate NO +G120 Within-match aggregates NO +G800 PATH_NAME function NO +G801 ELEMENT_NUMBER function NO +G802 PATH_LENGTH function NO +G803 MATCHNUM function NO +G810 IS BOUND predicate NO +G811 IS BOUND predicate: AS option NO +G820 BINDING_COUNT NO +G830 Colon in 'is label' expression NO +G840 Path-ordered aggregates NO +G850 SQL/PGQ Information Schema views YES +G860 GET DIAGNOSTICS enhancements for SQL-property graphs NO +G900 GRAPH_TABLE YES SQL/PGQ required +G901 GRAPH_TABLE: ONE ROW PER VERTEX NO +G902 GRAPH_TABLE: ONE ROW PER STEP NO +G903 GRAPH_TABLE: explicit ONE ROW PER MATCH keywords NO +G904 All properties reference NO +G905 GRAPH_TABLE: optional COLUMNS clause NO +G906 GRAPH_TABLE: explicit EXPORT ALL NO +G907 GRAPH_TABLE: EXPORT ALL EXCEPT NO +G908 GRAPH_TABLE: EXPORT SINGLETONS list NO +G909 GRAPH_TABLE: explicit EXPORT NO SINGLETONS NO +G910 GRAPH_TABLE: 'in paths clause' NO +G920 DDL-based SQL-property graphs YES SQL/PGQ required +G921 Empty SQL-property graph YES +G922 Views as element tables YES +G923 In-line views as element tables NO +G924 Explicit key clause for element tables YES SQL/PGQ required +G925 Explicit label and properties clause for element tables YES SQL/PGQ required +G926 More than one label for vertex tables YES +G927 More than one label for edge tables YES +G928 Value expressions as properties and renaming of properties YES +G929 Labels and properties: EXCEPT list NO +G940 Multi-sourced/destined edges YES +G941 Implicit removal of incomplete edges YES +G950 Alter property graph statement: ADD/DROP element table YES +G960 Alter element table definition: ADD/DROP LABEL YES +G970 Alter element table definition: ALTER LABEL YES +G980 DROP PROPERTY GRAPH: CASCADE drop behavior YES R010 Row pattern recognition: FROM clause NO R020 Row pattern recognition: WINDOW clause NO R030 Row pattern recognition: full aggregate support NO @@ -518,7 +618,7 @@ T612 Advanced OLAP operations YES T613 Sampling YES T614 NTILE function YES T615 LEAD and LAG functions YES -T616 Null treatment option for LEAD and LAG functions NO +T616 Null treatment option for LEAD and LAG functions YES T617 FIRST_VALUE and LAST_VALUE functions YES T618 NTH_VALUE function NO function exists, but some options missing T619 Nested window functions NO diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c index 227df90f89c97..e443a4993c5e6 100644 --- a/src/backend/catalog/storage.c +++ b/src/backend/catalog/storage.c @@ -3,7 +3,7 @@ * storage.c * code to create and destroy physical storage for relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -546,7 +546,7 @@ RelationCopyStorage(SMgrRelation src, SMgrRelation dst, ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("invalid page in block %u of relation %s", + errmsg("invalid page in block %u of relation \"%s\"", blkno, relpath.str))); } @@ -586,7 +586,7 @@ RelFileLocatorSkippingWAL(RelFileLocator rlocator) Size EstimatePendingSyncsSpace(void) { - long entries; + int64 entries; entries = pendingSyncHash ? hash_get_num_entries(pendingSyncHash) : 0; return mul_size(1 + entries, sizeof(RelFileLocator)); @@ -707,12 +707,12 @@ smgrDoPendingDeletes(bool isCommit) if (maxrels == 0) { maxrels = 8; - srels = palloc(sizeof(SMgrRelation) * maxrels); + srels = palloc_array(SMgrRelation, maxrels); } else if (maxrels <= nrels) { maxrels *= 2; - srels = repalloc(srels, sizeof(SMgrRelation) * maxrels); + srels = repalloc_array(srels, SMgrRelation, maxrels); } srels[nrels++] = srel; @@ -829,12 +829,12 @@ smgrDoPendingSyncs(bool isCommit, bool isParallelWorker) if (maxrels == 0) { maxrels = 8; - srels = palloc(sizeof(SMgrRelation) * maxrels); + srels = palloc_array(SMgrRelation, maxrels); } else if (maxrels <= nrels) { maxrels *= 2; - srels = repalloc(srels, sizeof(SMgrRelation) * maxrels); + srels = repalloc_array(srels, SMgrRelation, maxrels); } srels[nrels++] = srel; @@ -909,7 +909,7 @@ smgrGetPendingDeletes(bool forCommit, RelFileLocator **ptr) *ptr = NULL; return 0; } - rptr = (RelFileLocator *) palloc(nrels * sizeof(RelFileLocator)); + rptr = palloc_array(RelFileLocator, nrels); *ptr = rptr; for (pending = pendingDeletes; pending != NULL; pending = pending->next) { diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 566f308e4439d..c3c0a6e84ed7c 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -1,24 +1,21 @@ /* * PostgreSQL System Functions * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/backend/catalog/system_functions.sql * * This file redefines certain built-in functions that are impractical * to fully define in pg_proc.dat. In most cases that's because they use - * SQL-standard function bodies and/or default expressions. The node + * SQL-standard function bodies and/or default expressions. (But defaults + * that are just constants can be entered in pg_proc.dat.) The node * tree representations of those are too unreadable, platform-dependent, * and changeable to want to deal with them manually. Hence, we put stub * definitions of such functions into pg_proc.dat and then replace them * here. The stub definitions would be unnecessary were it not that we'd * like these functions to have stable OIDs, the same as other built-in - * functions. - * - * This file also takes care of adjusting privileges for those functions - * that should not have the default public-EXECUTE privileges. (However, - * a small number of functions that exist mainly to underlie system views - * are dealt with in system_views.sql, instead.) + * functions. (That's important, for example, to their treatment by + * postgres_fdw.) * * Note: this file is read in single-user -j mode, which means that the * command terminator is semicolon-newline-newline; whenever the backend @@ -66,13 +63,6 @@ CREATE OR REPLACE FUNCTION bit_length(text) IMMUTABLE PARALLEL SAFE STRICT COST 1 RETURN octet_length($1) * 8; -CREATE OR REPLACE FUNCTION - random_normal(mean float8 DEFAULT 0, stddev float8 DEFAULT 1) - RETURNS float8 - LANGUAGE internal - VOLATILE PARALLEL RESTRICTED STRICT COST 1 -AS 'drandom_normal'; - CREATE OR REPLACE FUNCTION log(numeric) RETURNS numeric LANGUAGE sql @@ -109,12 +99,6 @@ CREATE OR REPLACE FUNCTION path_contain_pt(path, point) IMMUTABLE PARALLEL SAFE STRICT COST 1 RETURN on_ppath($2, $1); -CREATE OR REPLACE FUNCTION polygon(circle) - RETURNS polygon - LANGUAGE sql - IMMUTABLE PARALLEL SAFE STRICT COST 1 -RETURN polygon(12, $1); - CREATE OR REPLACE FUNCTION age(timestamptz) RETURNS interval LANGUAGE sql @@ -382,427 +366,3 @@ CREATE OR REPLACE FUNCTION ts_debug(document text, BEGIN ATOMIC SELECT * FROM ts_debug(get_current_ts_config(), $1); END; - -CREATE OR REPLACE FUNCTION - pg_backup_start(label text, fast boolean DEFAULT false) - RETURNS pg_lsn STRICT VOLATILE LANGUAGE internal AS 'pg_backup_start' - PARALLEL RESTRICTED; - -CREATE OR REPLACE FUNCTION pg_backup_stop ( - wait_for_archive boolean DEFAULT true, OUT lsn pg_lsn, - OUT labelfile text, OUT spcmapfile text) - RETURNS record STRICT VOLATILE LANGUAGE internal as 'pg_backup_stop' - PARALLEL RESTRICTED; - -CREATE OR REPLACE FUNCTION - pg_promote(wait boolean DEFAULT true, wait_seconds integer DEFAULT 60) - RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS 'pg_promote' - PARALLEL SAFE; - -CREATE OR REPLACE FUNCTION - pg_terminate_backend(pid integer, timeout int8 DEFAULT 0) - RETURNS boolean STRICT VOLATILE LANGUAGE INTERNAL AS 'pg_terminate_backend' - PARALLEL SAFE; - --- legacy definition for compatibility with 9.3 -CREATE OR REPLACE FUNCTION - json_populate_record(base anyelement, from_json json, use_json_as_text boolean DEFAULT false) - RETURNS anyelement LANGUAGE internal STABLE AS 'json_populate_record' PARALLEL SAFE; - --- legacy definition for compatibility with 9.3 -CREATE OR REPLACE FUNCTION - json_populate_recordset(base anyelement, from_json json, use_json_as_text boolean DEFAULT false) - RETURNS SETOF anyelement LANGUAGE internal STABLE ROWS 100 AS 'json_populate_recordset' PARALLEL SAFE; - -CREATE OR REPLACE FUNCTION pg_logical_slot_get_changes( - IN slot_name name, IN upto_lsn pg_lsn, IN upto_nchanges int, VARIADIC options text[] DEFAULT '{}', - OUT lsn pg_lsn, OUT xid xid, OUT data text) -RETURNS SETOF RECORD -LANGUAGE INTERNAL -VOLATILE ROWS 1000 COST 1000 -AS 'pg_logical_slot_get_changes'; - -CREATE OR REPLACE FUNCTION pg_logical_slot_peek_changes( - IN slot_name name, IN upto_lsn pg_lsn, IN upto_nchanges int, VARIADIC options text[] DEFAULT '{}', - OUT lsn pg_lsn, OUT xid xid, OUT data text) -RETURNS SETOF RECORD -LANGUAGE INTERNAL -VOLATILE ROWS 1000 COST 1000 -AS 'pg_logical_slot_peek_changes'; - -CREATE OR REPLACE FUNCTION pg_logical_slot_get_binary_changes( - IN slot_name name, IN upto_lsn pg_lsn, IN upto_nchanges int, VARIADIC options text[] DEFAULT '{}', - OUT lsn pg_lsn, OUT xid xid, OUT data bytea) -RETURNS SETOF RECORD -LANGUAGE INTERNAL -VOLATILE ROWS 1000 COST 1000 -AS 'pg_logical_slot_get_binary_changes'; - -CREATE OR REPLACE FUNCTION pg_logical_slot_peek_binary_changes( - IN slot_name name, IN upto_lsn pg_lsn, IN upto_nchanges int, VARIADIC options text[] DEFAULT '{}', - OUT lsn pg_lsn, OUT xid xid, OUT data bytea) -RETURNS SETOF RECORD -LANGUAGE INTERNAL -VOLATILE ROWS 1000 COST 1000 -AS 'pg_logical_slot_peek_binary_changes'; - -CREATE OR REPLACE FUNCTION pg_logical_emit_message( - transactional boolean, - prefix text, - message text, - flush boolean DEFAULT false) -RETURNS pg_lsn -LANGUAGE INTERNAL -STRICT VOLATILE -AS 'pg_logical_emit_message_text'; - -CREATE OR REPLACE FUNCTION pg_logical_emit_message( - transactional boolean, - prefix text, - message bytea, - flush boolean DEFAULT false) -RETURNS pg_lsn -LANGUAGE INTERNAL -STRICT VOLATILE -AS 'pg_logical_emit_message_bytea'; - -CREATE OR REPLACE FUNCTION pg_create_physical_replication_slot( - IN slot_name name, IN immediately_reserve boolean DEFAULT false, - IN temporary boolean DEFAULT false, - OUT slot_name name, OUT lsn pg_lsn) -RETURNS RECORD -LANGUAGE INTERNAL -STRICT VOLATILE -AS 'pg_create_physical_replication_slot'; - -CREATE OR REPLACE FUNCTION pg_create_logical_replication_slot( - IN slot_name name, IN plugin name, - IN temporary boolean DEFAULT false, - IN twophase boolean DEFAULT false, - IN failover boolean DEFAULT false, - OUT slot_name name, OUT lsn pg_lsn) -RETURNS RECORD -LANGUAGE INTERNAL -STRICT VOLATILE -AS 'pg_create_logical_replication_slot'; - -CREATE OR REPLACE FUNCTION - make_interval(years int4 DEFAULT 0, months int4 DEFAULT 0, weeks int4 DEFAULT 0, - days int4 DEFAULT 0, hours int4 DEFAULT 0, mins int4 DEFAULT 0, - secs double precision DEFAULT 0.0) -RETURNS interval -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'make_interval'; - -CREATE OR REPLACE FUNCTION - jsonb_set(jsonb_in jsonb, path text[] , replacement jsonb, - create_if_missing boolean DEFAULT true) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_set'; - -CREATE OR REPLACE FUNCTION - jsonb_set_lax(jsonb_in jsonb, path text[] , replacement jsonb, - create_if_missing boolean DEFAULT true, - null_value_treatment text DEFAULT 'use_json_null') -RETURNS jsonb -LANGUAGE INTERNAL -CALLED ON NULL INPUT IMMUTABLE PARALLEL SAFE -AS 'jsonb_set_lax'; - -CREATE OR REPLACE FUNCTION - parse_ident(str text, strict boolean DEFAULT true) -RETURNS text[] -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'parse_ident'; - -CREATE OR REPLACE FUNCTION - jsonb_insert(jsonb_in jsonb, path text[] , replacement jsonb, - insert_after boolean DEFAULT false) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_insert'; - -CREATE OR REPLACE FUNCTION - jsonb_path_exists(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS boolean -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_path_exists'; - -CREATE OR REPLACE FUNCTION - jsonb_path_match(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS boolean -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_path_match'; - -CREATE OR REPLACE FUNCTION - jsonb_path_query(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS SETOF jsonb -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_path_query'; - -CREATE OR REPLACE FUNCTION - jsonb_path_query_array(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_path_query_array'; - -CREATE OR REPLACE FUNCTION - jsonb_path_query_first(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT IMMUTABLE PARALLEL SAFE -AS 'jsonb_path_query_first'; - -CREATE OR REPLACE FUNCTION - jsonb_path_exists_tz(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS boolean -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'jsonb_path_exists_tz'; - -CREATE OR REPLACE FUNCTION - jsonb_path_match_tz(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS boolean -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'jsonb_path_match_tz'; - -CREATE OR REPLACE FUNCTION - jsonb_path_query_tz(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS SETOF jsonb -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'jsonb_path_query_tz'; - -CREATE OR REPLACE FUNCTION - jsonb_path_query_array_tz(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'jsonb_path_query_array_tz'; - -CREATE OR REPLACE FUNCTION - jsonb_path_query_first_tz(target jsonb, path jsonpath, vars jsonb DEFAULT '{}', - silent boolean DEFAULT false) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'jsonb_path_query_first_tz'; - -CREATE OR REPLACE FUNCTION - jsonb_strip_nulls(target jsonb, strip_in_arrays boolean DEFAULT false) -RETURNS jsonb -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'jsonb_strip_nulls'; - -CREATE OR REPLACE FUNCTION - json_strip_nulls(target json, strip_in_arrays boolean DEFAULT false) -RETURNS json -LANGUAGE INTERNAL -STRICT STABLE PARALLEL SAFE -AS 'json_strip_nulls'; - --- default normalization form is NFC, per SQL standard -CREATE OR REPLACE FUNCTION - "normalize"(text, text DEFAULT 'NFC') -RETURNS text -LANGUAGE internal -STRICT IMMUTABLE PARALLEL SAFE -AS 'unicode_normalize_func'; - -CREATE OR REPLACE FUNCTION - is_normalized(text, text DEFAULT 'NFC') -RETURNS boolean -LANGUAGE internal -STRICT IMMUTABLE PARALLEL SAFE -AS 'unicode_is_normalized'; - -CREATE OR REPLACE FUNCTION - pg_stat_reset_shared(target text DEFAULT NULL) -RETURNS void -LANGUAGE INTERNAL -CALLED ON NULL INPUT VOLATILE PARALLEL SAFE -AS 'pg_stat_reset_shared'; - -CREATE OR REPLACE FUNCTION - pg_stat_reset_slru(target text DEFAULT NULL) -RETURNS void -LANGUAGE INTERNAL -CALLED ON NULL INPUT VOLATILE PARALLEL SAFE -AS 'pg_stat_reset_slru'; - --- --- The default permissions for functions mean that anyone can execute them. --- A number of functions shouldn't be executable by just anyone, but rather --- than use explicit 'superuser()' checks in those functions, we use the GRANT --- system to REVOKE access to those functions at initdb time. Administrators --- can later change who can access these functions, or leave them as only --- available to superuser / cluster owner, if they choose. --- - -REVOKE EXECUTE ON FUNCTION pg_backup_start(text, boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_backup_stop(boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_create_restore_point(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_switch_wal() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_log_standby_snapshot() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_wal_replay_pause() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_wal_replay_resume() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_rotate_logfile() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_reload_conf() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_current_logfile() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_current_logfile(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_promote(boolean, integer) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_shared(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_slru(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_single_table_counters(oid) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_single_function_counters(oid) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_backend_stats(integer) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_replication_slot(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_have_stats(text, oid, int8) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_reset_subscription_stats(oid) FROM public; - -REVOKE EXECUTE ON FUNCTION lo_import(text) FROM public; - -REVOKE EXECUTE ON FUNCTION lo_import(text, oid) FROM public; - -REVOKE EXECUTE ON FUNCTION lo_export(oid, text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_logdir() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_waldir() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_archive_statusdir() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_summariesdir() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_tmpdir() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_tmpdir(oid) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_file(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_file(text,boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_file(text,bigint,bigint) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_file(text,bigint,bigint,boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_binary_file(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_binary_file(text,boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_binary_file(text,bigint,bigint) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_read_binary_file(text,bigint,bigint,boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_advance(text, pg_lsn) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_create(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_drop(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_oid(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_progress(text, boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_session_is_setup() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_session_progress(boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_session_reset() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_session_setup(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_xact_reset() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_replication_origin_xact_setup(pg_lsn, timestamp with time zone) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_show_replication_origin_status() FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_file(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_stat_file(text,boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_dir(text) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_ls_dir(text,boolean,boolean) FROM public; - -REVOKE EXECUTE ON FUNCTION pg_log_backend_memory_contexts(integer) FROM PUBLIC; - -REVOKE EXECUTE ON FUNCTION pg_ls_logicalsnapdir() FROM PUBLIC; - -REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; - -REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; - --- --- We also set up some things as accessible to standard roles. --- - -GRANT EXECUTE ON FUNCTION pg_ls_logdir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_waldir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_archive_statusdir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_summariesdir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_tmpdir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_tmpdir(oid) TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_logicalsnapdir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_logicalmapdir() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_ls_replslotdir(text) TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; - -GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; - -GRANT pg_read_all_settings TO pg_monitor; - -GRANT pg_read_all_stats TO pg_monitor; - -GRANT pg_stat_scan_tables TO pg_monitor; diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 08f780a2e6382..73a1c1c46703a 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1,10 +1,15 @@ /* * PostgreSQL System Views * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/backend/catalog/system_views.sql * + * Some of these views are not meant to be publicly readable. The + * underlying function(s) for such a view should not be publicly + * executable either --- but by default they will be. So don't forget to + * adjust function permissions (in pg_proc.dat) when adding a new view. + * * Note: this file is read in single-user -j mode, which means that the * command terminator is semicolon-newline-newline; whenever the backend * sees that, it stops and executes what it's got. If you write a lot of @@ -186,7 +191,9 @@ CREATE VIEW pg_stats WITH (security_barrier) AS SELECT nspname AS schemaname, relname AS tablename, + attrelid AS tableid, attname AS attname, + attnum, stainherit AS inherited, stanullfrac AS null_frac, stawidth AS avg_width, @@ -273,8 +280,10 @@ REVOKE ALL ON pg_statistic FROM public; CREATE VIEW pg_stats_ext WITH (security_barrier) AS SELECT cn.nspname AS schemaname, c.relname AS tablename, + s.stxrelid AS tableid, sn.nspname AS statistics_schemaname, s.stxname AS statistics_name, + s.oid AS statistics_id, pg_get_userbyid(s.stxowner) AS statistics_owner, ( SELECT array_agg(a.attname ORDER BY a.attnum) FROM unnest(s.stxkeys) k @@ -307,8 +316,10 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS SELECT cn.nspname AS schemaname, c.relname AS tablename, + s.stxrelid AS tableid, sn.nspname AS statistics_schemaname, s.stxname AS statistics_name, + s.oid AS statistics_id, pg_get_userbyid(s.stxowner) AS statistics_owner, stat.expr, sd.stxdinherit AS inherited, @@ -363,7 +374,28 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS WHEN (stat.a).stakind3 = 5 THEN (stat.a).stanumbers3 WHEN (stat.a).stakind4 = 5 THEN (stat.a).stanumbers4 WHEN (stat.a).stakind5 = 5 THEN (stat.a).stanumbers5 - END) AS elem_count_histogram + END) AS elem_count_histogram, + (CASE + WHEN (stat.a).stakind1 = 6 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 6 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 6 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 6 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 6 THEN (stat.a).stavalues5 + END) AS range_length_histogram, + (CASE + WHEN (stat.a).stakind1 = 6 THEN (stat.a).stanumbers1[1] + WHEN (stat.a).stakind2 = 6 THEN (stat.a).stanumbers2[1] + WHEN (stat.a).stakind3 = 6 THEN (stat.a).stanumbers3[1] + WHEN (stat.a).stakind4 = 6 THEN (stat.a).stanumbers4[1] + WHEN (stat.a).stakind5 = 6 THEN (stat.a).stanumbers5[1] + END) AS range_empty_frac, + (CASE + WHEN (stat.a).stakind1 = 7 THEN (stat.a).stavalues1 + WHEN (stat.a).stakind2 = 7 THEN (stat.a).stavalues2 + WHEN (stat.a).stakind3 = 7 THEN (stat.a).stavalues3 + WHEN (stat.a).stakind4 = 7 THEN (stat.a).stavalues4 + WHEN (stat.a).stakind5 = 7 THEN (stat.a).stavalues5 + END) AS range_bounds_histogram FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid) LEFT JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid) LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace) @@ -394,6 +426,16 @@ CREATE VIEW pg_publication_tables AS pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE C.oid = GPT.relid; +CREATE VIEW pg_publication_sequences AS + SELECT + P.pubname AS pubname, + N.nspname AS schemaname, + C.relname AS sequencename + FROM pg_publication P, + LATERAL pg_get_publication_sequences(P.pubname) GPS, + pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace) + WHERE C.oid = GPS.relid; + CREATE VIEW pg_locks AS SELECT * FROM pg_lock_status() AS L; @@ -402,14 +444,14 @@ CREATE VIEW pg_cursors AS CREATE VIEW pg_available_extensions AS SELECT E.name, E.default_version, X.extversion AS installed_version, - E.comment + E.location, E.comment FROM pg_available_extensions() AS E LEFT JOIN pg_extension AS X ON E.name = X.extname; CREATE VIEW pg_available_extension_versions AS SELECT E.name, E.version, (X.extname IS NOT NULL) AS installed, E.superuser, E.trusted, E.relocatable, - E.schema, E.requires, E.comment + E.schema, E.requires, E.location, E.comment FROM pg_available_extension_versions() AS E LEFT JOIN pg_extension AS X ON E.name = X.extname AND E.version = X.extversion; @@ -619,19 +661,16 @@ CREATE VIEW pg_file_settings AS SELECT * FROM pg_show_all_file_settings() AS A; REVOKE ALL ON pg_file_settings FROM PUBLIC; -REVOKE EXECUTE ON FUNCTION pg_show_all_file_settings() FROM PUBLIC; CREATE VIEW pg_hba_file_rules AS SELECT * FROM pg_hba_file_rules() AS A; REVOKE ALL ON pg_hba_file_rules FROM PUBLIC; -REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC; CREATE VIEW pg_ident_file_mappings AS SELECT * FROM pg_ident_file_mappings() AS A; REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC; -REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC; CREATE VIEW pg_timezone_abbrevs AS SELECT * FROM pg_timezone_abbrevs_zone() z @@ -648,31 +687,30 @@ CREATE VIEW pg_config AS SELECT * FROM pg_config(); REVOKE ALL ON pg_config FROM PUBLIC; -REVOKE EXECUTE ON FUNCTION pg_config() FROM PUBLIC; CREATE VIEW pg_shmem_allocations AS SELECT * FROM pg_get_shmem_allocations(); REVOKE ALL ON pg_shmem_allocations FROM PUBLIC; GRANT SELECT ON pg_shmem_allocations TO pg_read_all_stats; -REVOKE EXECUTE ON FUNCTION pg_get_shmem_allocations() FROM PUBLIC; -GRANT EXECUTE ON FUNCTION pg_get_shmem_allocations() TO pg_read_all_stats; CREATE VIEW pg_shmem_allocations_numa AS SELECT * FROM pg_get_shmem_allocations_numa(); REVOKE ALL ON pg_shmem_allocations_numa FROM PUBLIC; GRANT SELECT ON pg_shmem_allocations_numa TO pg_read_all_stats; -REVOKE EXECUTE ON FUNCTION pg_get_shmem_allocations_numa() FROM PUBLIC; -GRANT EXECUTE ON FUNCTION pg_get_shmem_allocations_numa() TO pg_read_all_stats; + +CREATE VIEW pg_dsm_registry_allocations AS + SELECT * FROM pg_get_dsm_registry_allocations(); + +REVOKE ALL ON pg_dsm_registry_allocations FROM PUBLIC; +GRANT SELECT ON pg_dsm_registry_allocations TO pg_read_all_stats; CREATE VIEW pg_backend_memory_contexts AS SELECT * FROM pg_get_backend_memory_contexts(); REVOKE ALL ON pg_backend_memory_contexts FROM PUBLIC; GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; -REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; -GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; -- Statistics views @@ -708,7 +746,8 @@ CREATE VIEW pg_stat_all_tables AS pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time, pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time, pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time, - pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time + pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time, + pg_stat_get_stat_reset_time(C.oid) AS stats_reset FROM pg_class C LEFT JOIN pg_index I ON C.oid = I.indrelid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) @@ -756,6 +795,24 @@ CREATE VIEW pg_stat_xact_user_tables AS WHERE schemaname NOT IN ('pg_catalog', 'information_schema') AND schemaname !~ '^pg_toast'; +CREATE VIEW pg_stat_autovacuum_scores AS + SELECT + s.oid AS relid, + n.nspname AS schemaname, + c.relname AS relname, + s.score, + s.xid_score, + s.mxid_score, + s.vacuum_score, + s.vacuum_insert_score, + s.analyze_score, + s.do_vacuum, + s.do_analyze, + s.for_wraparound + FROM pg_stat_get_autovacuum_scores() s + JOIN pg_class c on c.oid = s.oid + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace; + CREATE VIEW pg_statio_all_tables AS SELECT C.oid AS relid, @@ -770,7 +827,8 @@ CREATE VIEW pg_statio_all_tables AS pg_stat_get_blocks_hit(T.oid) AS toast_blks_read, pg_stat_get_blocks_hit(T.oid) AS toast_blks_hit, X.idx_blks_read AS tidx_blks_read, - X.idx_blks_hit AS tidx_blks_hit + X.idx_blks_hit AS tidx_blks_hit, + pg_stat_get_stat_reset_time(C.oid) AS stats_reset FROM pg_class C LEFT JOIN pg_class T ON C.reltoastrelid = T.oid LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) @@ -810,7 +868,8 @@ CREATE VIEW pg_stat_all_indexes AS pg_stat_get_numscans(I.oid) AS idx_scan, pg_stat_get_lastscan(I.oid) AS last_idx_scan, pg_stat_get_tuples_returned(I.oid) AS idx_tup_read, - pg_stat_get_tuples_fetched(I.oid) AS idx_tup_fetch + pg_stat_get_tuples_fetched(I.oid) AS idx_tup_fetch, + pg_stat_get_stat_reset_time(I.oid) AS stats_reset FROM pg_class C JOIN pg_index X ON C.oid = X.indrelid JOIN pg_class I ON I.oid = X.indexrelid @@ -836,7 +895,8 @@ CREATE VIEW pg_statio_all_indexes AS I.relname AS indexrelname, pg_stat_get_blocks_fetched(I.oid) - pg_stat_get_blocks_hit(I.oid) AS idx_blks_read, - pg_stat_get_blocks_hit(I.oid) AS idx_blks_hit + pg_stat_get_blocks_hit(I.oid) AS idx_blks_hit, + pg_stat_get_stat_reset_time(I.oid) AS stats_reset FROM pg_class C JOIN pg_index X ON C.oid = X.indrelid JOIN pg_class I ON I.oid = X.indexrelid @@ -860,7 +920,8 @@ CREATE VIEW pg_statio_all_sequences AS C.relname AS relname, pg_stat_get_blocks_fetched(C.oid) - pg_stat_get_blocks_hit(C.oid) AS blks_read, - pg_stat_get_blocks_hit(C.oid) AS blks_hit + pg_stat_get_blocks_hit(C.oid) AS blks_hit, + pg_stat_get_stat_reset_time(C.oid) AS stats_reset FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE C.relkind = 'S'; @@ -895,7 +956,7 @@ CREATE VIEW pg_stat_activity AS S.wait_event, S.state, S.backend_xid, - s.backend_xmin, + S.backend_xmin, S.query_id, S.query, S.backend_type @@ -942,6 +1003,15 @@ CREATE VIEW pg_stat_slru AS s.stats_reset FROM pg_stat_get_slru() s; +CREATE VIEW pg_stat_lock AS + SELECT + l.locktype, + l.waits, + l.wait_time, + l.fastpath_exceeded, + l.stats_reset + FROM pg_stat_get_lock() l; + CREATE VIEW pg_stat_wal_receiver AS SELECT s.pid, @@ -962,6 +1032,20 @@ CREATE VIEW pg_stat_wal_receiver AS FROM pg_stat_get_wal_receiver() s WHERE s.pid IS NOT NULL; +CREATE VIEW pg_stat_recovery AS + SELECT + s.promote_triggered, + s.last_replayed_read_lsn, + s.last_replayed_end_lsn, + s.last_replayed_tli, + s.replay_end_lsn, + s.replay_end_tli, + s.recovery_last_xact_time, + s.current_chunk_start_time, + s.pause_state + FROM pg_stat_get_recovery() s + WHERE s.promote_triggered IS NOT NULL; + CREATE VIEW pg_stat_recovery_prefetch AS SELECT s.stats_reset, @@ -1038,7 +1122,8 @@ CREATE VIEW pg_replication_slots AS L.conflicting, L.invalidation_reason, L.failover, - L.synced + L.synced, + L.slotsync_skip_reason FROM pg_get_replication_slots() AS L LEFT JOIN pg_database D ON (L.datoid = D.oid); @@ -1051,8 +1136,11 @@ CREATE VIEW pg_stat_replication_slots AS s.stream_txns, s.stream_count, s.stream_bytes, + s.mem_exceeded_count, s.total_txns, s.total_bytes, + s.slotsync_skip_count, + s.slotsync_last_skip, s.stats_reset FROM pg_replication_slots as r, LATERAL pg_stat_get_replication_slot(slot_name) as s @@ -1109,7 +1197,8 @@ CREATE VIEW pg_stat_database_conflicts AS pg_stat_get_db_conflict_snapshot(D.oid) AS confl_snapshot, pg_stat_get_db_conflict_bufferpin(D.oid) AS confl_bufferpin, pg_stat_get_db_conflict_startup_deadlock(D.oid) AS confl_deadlock, - pg_stat_get_db_conflict_logicalslot(D.oid) AS confl_active_logicalslot + pg_stat_get_db_conflict_logicalslot(D.oid) AS confl_active_logicalslot, + pg_stat_get_db_stat_reset_time(D.oid) AS stats_reset FROM pg_database D; CREATE VIEW pg_stat_user_functions AS @@ -1119,7 +1208,8 @@ CREATE VIEW pg_stat_user_functions AS P.proname AS funcname, pg_stat_get_function_calls(P.oid) AS calls, pg_stat_get_function_total_time(P.oid) AS total_time, - pg_stat_get_function_self_time(P.oid) AS self_time + pg_stat_get_function_self_time(P.oid) AS self_time, + pg_stat_get_function_stat_reset_time(P.oid) AS stats_reset FROM pg_proc P LEFT JOIN pg_namespace N ON (N.oid = P.pronamespace) WHERE P.prolang != 12 -- fast check to eliminate built-in functions AND pg_stat_get_function_calls(P.oid) IS NOT NULL; @@ -1197,6 +1287,7 @@ CREATE VIEW pg_stat_wal AS w.wal_records, w.wal_fpi, w.wal_bytes, + w.wal_fpi_bytes, w.wal_buffers_full, w.stats_reset FROM pg_stat_get_wal() w; @@ -1219,7 +1310,10 @@ CREATE VIEW pg_stat_progress_analyze AS S.param6 AS child_tables_total, S.param7 AS child_tables_done, CAST(S.param8 AS oid) AS current_child_table_relid, - S.param9 / 1000000::double precision AS delay_time + S.param9 / 1000000::double precision AS delay_time, + CASE S.param10 WHEN 1 THEN 'manual' + WHEN 2 THEN 'autovacuum' + ELSE NULL END AS started_by FROM pg_stat_get_progress_info('ANALYZE') AS S LEFT JOIN pg_database D ON S.datid = D.oid; @@ -1240,37 +1334,69 @@ CREATE VIEW pg_stat_progress_vacuum AS S.param6 AS max_dead_tuple_bytes, S.param7 AS dead_tuple_bytes, S.param8 AS num_dead_item_ids, S.param9 AS indexes_total, S.param10 AS indexes_processed, - S.param11 / 1000000::double precision AS delay_time + S.param11 / 1000000::double precision AS delay_time, + CASE S.param12 WHEN 1 THEN 'normal' + WHEN 2 THEN 'aggressive' + WHEN 3 THEN 'failsafe' + ELSE NULL END AS mode, + CASE S.param13 WHEN 1 THEN 'manual' + WHEN 2 THEN 'autovacuum' + WHEN 3 THEN 'autovacuum_wraparound' + ELSE NULL END AS started_by FROM pg_stat_get_progress_info('VACUUM') AS S LEFT JOIN pg_database D ON S.datid = D.oid; -CREATE VIEW pg_stat_progress_cluster AS +CREATE VIEW pg_stat_progress_repack AS SELECT S.pid AS pid, S.datid AS datid, D.datname AS datname, S.relid AS relid, CASE S.param1 WHEN 1 THEN 'CLUSTER' - WHEN 2 THEN 'VACUUM FULL' + WHEN 2 THEN 'REPACK' + WHEN 3 THEN 'VACUUM FULL' END AS command, CASE S.param2 WHEN 0 THEN 'initializing' WHEN 1 THEN 'seq scanning heap' WHEN 2 THEN 'index scanning heap' WHEN 3 THEN 'sorting tuples' WHEN 4 THEN 'writing new heap' - WHEN 5 THEN 'swapping relation files' - WHEN 6 THEN 'rebuilding index' - WHEN 7 THEN 'performing final cleanup' + WHEN 5 THEN 'catch-up' + WHEN 6 THEN 'swapping relation files' + WHEN 7 THEN 'rebuilding index' + WHEN 8 THEN 'performing final cleanup' END AS phase, - CAST(S.param3 AS oid) AS cluster_index_relid, + CAST(S.param3 AS oid) AS repack_index_relid, S.param4 AS heap_tuples_scanned, - S.param5 AS heap_tuples_written, - S.param6 AS heap_blks_total, - S.param7 AS heap_blks_scanned, - S.param8 AS index_rebuild_count - FROM pg_stat_get_progress_info('CLUSTER') AS S + S.param5 AS heap_tuples_inserted, + S.param6 AS heap_tuples_updated, + S.param7 AS heap_tuples_deleted, + S.param8 AS heap_blks_total, + S.param9 AS heap_blks_scanned, + S.param10 AS index_rebuild_count + FROM pg_stat_get_progress_info('REPACK') AS S LEFT JOIN pg_database D ON S.datid = D.oid; +-- This view is as the one above, except for renaming a column and avoiding +-- 'REPACK' as a command name to report. +CREATE VIEW pg_stat_progress_cluster AS + SELECT + pid, + datid, + datname, + relid, + CASE WHEN command IN ('CLUSTER', 'VACUUM FULL') THEN command + WHEN repack_index_relid = 0 THEN 'VACUUM FULL' + ELSE 'CLUSTER' END AS command, + phase, + repack_index_relid AS cluster_index_relid, + heap_tuples_scanned, + heap_tuples_inserted + heap_tuples_updated AS heap_tuples_written, + heap_blks_total, + heap_blks_scanned, + index_rebuild_count + FROM pg_stat_progress_repack; + CREATE VIEW pg_stat_progress_create_index AS SELECT S.pid AS pid, S.datid AS datid, D.datname AS datname, @@ -1319,7 +1445,10 @@ CREATE VIEW pg_stat_progress_basebackup AS CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS backup_total, S.param3 AS backup_streamed, S.param4 AS tablespaces_total, - S.param5 AS tablespaces_streamed + S.param5 AS tablespaces_streamed, + CASE S.param6 WHEN 1 THEN 'full' + WHEN 2 THEN 'incremental' + END AS backup_type FROM pg_stat_get_progress_info('BASEBACKUP') AS S; @@ -1343,6 +1472,25 @@ CREATE VIEW pg_stat_progress_copy AS FROM pg_stat_get_progress_info('COPY') AS S LEFT JOIN pg_database D ON S.datid = D.oid; +CREATE VIEW pg_stat_progress_data_checksums AS + SELECT + S.pid AS pid, S.datid, D.datname AS datname, + CASE S.param1 WHEN 0 THEN 'enabling' + WHEN 1 THEN 'disabling' + WHEN 2 THEN 'waiting on temporary tables' + WHEN 3 THEN 'waiting on barrier' + WHEN 4 THEN 'done' + END AS phase, + CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total, + S.param3 AS databases_done, + CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total, + CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done, + CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total, + CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done + FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S + LEFT JOIN pg_database D ON S.datid = D.oid + ORDER BY S.datid; -- return the launcher process first + CREATE VIEW pg_user_mappings AS SELECT U.oid AS umid, @@ -1378,7 +1526,8 @@ REVOKE ALL ON pg_subscription FROM public; GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled, subbinary, substream, subtwophasestate, subdisableonerr, subpasswordrequired, subrunasowner, subfailover, - subslotname, subsynccommit, subpublications, suborigin) + subretaindeadtuples, submaxretention, subretentionactive, + subserver, subslotname, subsynccommit, subpublications, suborigin) ON pg_subscription TO public; CREATE VIEW pg_stat_subscription_stats AS @@ -1386,10 +1535,12 @@ CREATE VIEW pg_stat_subscription_stats AS ss.subid, s.subname, ss.apply_error_count, - ss.sync_error_count, + ss.sync_seq_error_count, + ss.sync_table_error_count, ss.confl_insert_exists, ss.confl_update_origin_differs, ss.confl_update_exists, + ss.confl_update_deleted, ss.confl_update_missing, ss.confl_delete_origin_differs, ss.confl_delete_missing, @@ -1405,5 +1556,3 @@ CREATE VIEW pg_aios AS SELECT * FROM pg_get_aios(); REVOKE ALL ON pg_aios FROM PUBLIC; GRANT SELECT ON pg_aios TO pg_read_all_stats; -REVOKE EXECUTE ON FUNCTION pg_get_aios() FROM PUBLIC; -GRANT EXECUTE ON FUNCTION pg_get_aios() TO pg_read_all_stats; diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 874a8fc89adb3..4aa52a4bd2531 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -4,7 +4,7 @@ * This file contains routines to support creation of toast tables * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/genam.h" #include "access/heapam.h" #include "access/toast_compression.h" #include "access/xact.h" @@ -229,6 +230,12 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, TupleDescAttr(tupdesc, 1)->attcompression = InvalidCompressionMethod; TupleDescAttr(tupdesc, 2)->attcompression = InvalidCompressionMethod; + populate_compact_attribute(tupdesc, 0); + populate_compact_attribute(tupdesc, 1); + populate_compact_attribute(tupdesc, 2); + + TupleDescFinalize(tupdesc); + /* * Toast tables for regular relations go in pg_toast; those for temp * relations go into the per-backend temp-toast-table namespace. diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index cb2fbdc7c6018..5b9d084977e48 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -18,7 +18,6 @@ OBJS = \ amcmds.o \ analyze.o \ async.o \ - cluster.o \ collationcmds.o \ comment.o \ constraint.o \ @@ -49,10 +48,14 @@ OBJS = \ portalcmds.o \ prepare.o \ proclang.o \ + propgraphcmds.o \ publicationcmds.o \ + repack.o \ + repack_worker.o \ schemacmds.o \ seclabel.o \ sequence.o \ + sequence_xlog.o \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ @@ -64,6 +67,7 @@ OBJS = \ vacuum.o \ vacuumparallel.o \ variable.o \ - view.o + view.o \ + wait.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/commands/aggregatecmds.c b/src/backend/commands/aggregatecmds.c index 4268adfe78729..41b45dc6402a7 100644 --- a/src/backend/commands/aggregatecmds.c +++ b/src/backend/commands/aggregatecmds.c @@ -4,7 +4,7 @@ * * Routines for aggregate-manipulation commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index c801c869c1cfc..74ceb5fe20d4a 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -3,7 +3,7 @@ * alter.c * Drivers for generic alter commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -117,27 +117,21 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid) switch (classId) { case ConversionRelationId: - Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\""); break; case StatisticExtRelationId: - Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("statistics object \"%s\" already exists in schema \"%s\""); break; case TSParserRelationId: - Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search parser \"%s\" already exists in schema \"%s\""); break; case TSDictionaryRelationId: - Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search dictionary \"%s\" already exists in schema \"%s\""); break; case TSTemplateRelationId: - Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search template \"%s\" already exists in schema \"%s\""); break; case TSConfigRelationId: - Assert(OidIsValid(nspOid)); msgfmt = gettext_noop("text search configuration \"%s\" already exists in schema \"%s\""); break; default: @@ -165,8 +159,8 @@ static void AlterObjectRename_internal(Relation rel, Oid objectId, const char *new_name) { Oid classId = RelationGetRelid(rel); - int oidCacheId = get_object_catcache_oid(classId); - int nameCacheId = get_object_catcache_name(classId); + SysCacheIdentifier oidCacheId = get_object_catcache_oid(classId); + SysCacheIdentifier nameCacheId = get_object_catcache_name(classId); AttrNumber Anum_name = get_object_attnum_name(classId); AttrNumber Anum_namespace = get_object_attnum_namespace(classId); AttrNumber Anum_owner = get_object_attnum_owner(classId); @@ -220,7 +214,7 @@ AlterObjectRename_internal(Relation rel, Oid objectId, const char *new_name) Assert(!isnull); ownerId = DatumGetObjectId(datum); - if (!has_privs_of_role(GetUserId(), DatumGetObjectId(ownerId))) + if (!has_privs_of_role(GetUserId(), ownerId)) aclcheck_error(ACLCHECK_NOT_OWNER, get_object_type(classId, objectId), old_name); @@ -396,6 +390,7 @@ ExecRenameStmt(RenameStmt *stmt) case OBJECT_MATVIEW: case OBJECT_INDEX: case OBJECT_FOREIGN_TABLE: + case OBJECT_PROPGRAPH: return RenameRelation(stmt); case OBJECT_COLUMN: @@ -549,6 +544,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt, case OBJECT_TABLE: case OBJECT_VIEW: case OBJECT_MATVIEW: + case OBJECT_PROPGRAPH: address = AlterTableNamespace(stmt, oldSchemaAddr ? &oldNspOid : NULL); break; @@ -692,8 +688,8 @@ static Oid AlterObjectNamespace_internal(Relation rel, Oid objid, Oid nspOid) { Oid classId = RelationGetRelid(rel); - int oidCacheId = get_object_catcache_oid(classId); - int nameCacheId = get_object_catcache_name(classId); + SysCacheIdentifier oidCacheId = get_object_catcache_oid(classId); + SysCacheIdentifier nameCacheId = get_object_catcache_name(classId); AttrNumber Anum_name = get_object_attnum_name(classId); AttrNumber Anum_namespace = get_object_attnum_namespace(classId); AttrNumber Anum_owner = get_object_attnum_owner(classId); @@ -882,6 +878,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) case OBJECT_OPCLASS: case OBJECT_OPFAMILY: case OBJECT_PROCEDURE: + case OBJECT_PROPGRAPH: case OBJECT_ROUTINE: case OBJECT_STATISTIC_EXT: case OBJECT_TABLESPACE: @@ -890,11 +887,26 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt) { ObjectAddress address; - address = get_object_address(stmt->objectType, - stmt->object, - NULL, - AccessExclusiveLock, - false); + if (stmt->relation) + { + Relation relation; + + address = get_object_address_rv(stmt->objectType, + stmt->relation, + NIL, + &relation, + AccessExclusiveLock, + false); + relation_close(relation, NoLock); + } + else + { + address = get_object_address(stmt->objectType, + stmt->object, + NULL, + AccessExclusiveLock, + false); + } AlterObjectOwner_internal(address.classId, address.objectId, newowner); diff --git a/src/backend/commands/amcmds.c b/src/backend/commands/amcmds.c index 58ed9d216cc01..21825e8c3f54f 100644 --- a/src/backend/commands/amcmds.c +++ b/src/backend/commands/amcmds.c @@ -3,7 +3,7 @@ * amcmds.c * Routines for SQL commands that manipulate access methods. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c index 4fffb76e55735..020a5919b846f 100644 --- a/src/backend/commands/analyze.c +++ b/src/backend/commands/analyze.c @@ -3,7 +3,7 @@ * analyze.c * the Postgres statistics generator * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,12 +29,12 @@ #include "catalog/index.h" #include "catalog/indexing.h" #include "catalog/pg_inherits.h" -#include "commands/dbcommands.h" #include "commands/progress.h" #include "commands/tablecmds.h" #include "commands/vacuum.h" #include "common/pg_prng.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -76,13 +76,14 @@ static BufferAccessStrategy vac_strategy; static void do_analyze_rel(Relation onerel, - VacuumParams *params, List *va_cols, + const VacuumParams *params, List *va_cols, AcquireSampleRowsFunc acquirefunc, BlockNumber relpages, bool inh, bool in_outer_xact, int elevel); static void compute_index_stats(Relation onerel, double totalrows, AnlIndexData *indexdata, int nindexes, HeapTuple *rows, int numrows, MemoryContext col_context); +static void validate_va_cols_list(Relation onerel, List *va_cols); static VacAttrStats *examine_attribute(Relation onerel, int attnum, Node *index_expr); static int acquire_sample_rows(Relation onerel, int elevel, @@ -107,13 +108,14 @@ static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull); */ void analyze_rel(Oid relid, RangeVar *relation, - VacuumParams *params, List *va_cols, bool in_outer_xact, + const VacuumParams *params, List *va_cols, bool in_outer_xact, BufferAccessStrategy bstrategy) { Relation onerel; int elevel; AcquireSampleRowsFunc acquirefunc = NULL; BlockNumber relpages = 0; + bool stats_imported = false; /* Select logging level */ if (params->options & VACOPT_VERBOSE) @@ -139,7 +141,7 @@ analyze_rel(Oid relid, RangeVar *relation, * Make sure to generate only logs for ANALYZE in this case. */ onerel = vacuum_open_relation(relid, relation, params->options & ~(VACOPT_VACUUM), - params->log_min_duration >= 0, + params->log_analyze_min_duration >= 0, ShareUpdateExclusiveLock); /* leave if relation could not be opened or locked */ @@ -182,6 +184,28 @@ analyze_rel(Oid relid, RangeVar *relation, return; } + /* + * Check the given list of columns + */ + if (va_cols != NIL) + validate_va_cols_list(onerel, va_cols); + + /* + * Initialize progress reporting before setup for regular/foreign tables. + * (For the former, the time spent on it would be negligible, but for the + * latter, if FDWs support statistics import or analysis, they'd do some + * work that needs the remote access, so the time might be + * non-negligible.) + */ + pgstat_progress_start_command(PROGRESS_COMMAND_ANALYZE, + RelationGetRelid(onerel)); + if (AmAutoVacuumWorkerProcess()) + pgstat_progress_update_param(PROGRESS_ANALYZE_STARTED_BY, + PROGRESS_ANALYZE_STARTED_BY_AUTOVACUUM); + else + pgstat_progress_update_param(PROGRESS_ANALYZE_STARTED_BY, + PROGRESS_ANALYZE_STARTED_BY_MANUAL); + /* * Check that it's of an analyzable relkind, and set up appropriately. */ @@ -196,26 +220,33 @@ analyze_rel(Oid relid, RangeVar *relation, else if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) { /* - * For a foreign table, call the FDW's hook function to see whether it - * supports analysis. + * For a foreign table, call the FDW's hook functions to see whether + * it supports statistics import or analysis. */ FdwRoutine *fdwroutine; - bool ok = false; fdwroutine = GetFdwRoutineForRelation(onerel, false); - if (fdwroutine->AnalyzeForeignTable != NULL) - ok = fdwroutine->AnalyzeForeignTable(onerel, - &acquirefunc, - &relpages); - - if (!ok) + if (fdwroutine->ImportForeignStatistics != NULL && + fdwroutine->ImportForeignStatistics(onerel, va_cols, elevel)) + stats_imported = true; + else { - ereport(WARNING, - (errmsg("skipping \"%s\" --- cannot analyze this foreign table", - RelationGetRelationName(onerel)))); - relation_close(onerel, ShareUpdateExclusiveLock); - return; + bool ok = false; + + if (fdwroutine->AnalyzeForeignTable != NULL) + ok = fdwroutine->AnalyzeForeignTable(onerel, + &acquirefunc, + &relpages); + + if (!ok) + { + ereport(WARNING, + errmsg("skipping \"%s\" -- cannot analyze this foreign table.", + RelationGetRelationName(onerel))); + relation_close(onerel, ShareUpdateExclusiveLock); + goto out; + } } } else if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) @@ -232,20 +263,16 @@ analyze_rel(Oid relid, RangeVar *relation, (errmsg("skipping \"%s\" --- cannot analyze non-tables or special system tables", RelationGetRelationName(onerel)))); relation_close(onerel, ShareUpdateExclusiveLock); - return; + goto out; } - /* - * OK, let's do it. First, initialize progress reporting. - */ - pgstat_progress_start_command(PROGRESS_COMMAND_ANALYZE, - RelationGetRelid(onerel)); - /* * Do the normal non-recursive ANALYZE. We can skip this for partitioned - * tables, which don't contain any rows. + * tables, which don't contain any rows, and foreign tables that + * successfully imported statistics. */ - if (onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + if ((onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + && !stats_imported) do_analyze_rel(onerel, params, va_cols, acquirefunc, relpages, false, in_outer_xact, elevel); @@ -264,6 +291,7 @@ analyze_rel(Oid relid, RangeVar *relation, */ relation_close(onerel, NoLock); +out: pgstat_progress_end_command(); } @@ -275,7 +303,7 @@ analyze_rel(Oid relid, RangeVar *relation, * appropriate acquirefunc for each child table. */ static void -do_analyze_rel(Relation onerel, VacuumParams *params, +do_analyze_rel(Relation onerel, const VacuumParams *params, List *va_cols, AcquireSampleRowsFunc acquirefunc, BlockNumber relpages, bool inh, bool in_outer_xact, int elevel) @@ -311,7 +339,7 @@ do_analyze_rel(Relation onerel, VacuumParams *params, verbose = (params->options & VACOPT_VERBOSE) != 0; instrument = (verbose || (AmAutoVacuumWorkerProcess() && - params->log_min_duration >= 0)); + params->log_analyze_min_duration >= 0)); if (inh) ereport(elevel, (errmsg("analyzing \"%s.%s\" inheritance tree", @@ -362,16 +390,10 @@ do_analyze_rel(Relation onerel, VacuumParams *params, starttime = GetCurrentTimestamp(); /* - * Determine which columns to analyze - * - * Note that system attributes are never analyzed, so we just reject them - * at the lookup stage. We also reject duplicate column mentions. (We - * could alternatively ignore duplicates, but analyzing a column twice - * won't work; we'd end up making a conflicting update in pg_statistic.) + * Determine which columns to analyze. */ if (va_cols != NIL) { - Bitmapset *unique_cols = NULL; ListCell *le; vacattrstats = (VacAttrStats **) palloc(list_length(va_cols) * @@ -382,18 +404,7 @@ do_analyze_rel(Relation onerel, VacuumParams *params, char *col = strVal(lfirst(le)); i = attnameAttNum(onerel, col, false); - if (i == InvalidAttrNumber) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - col, RelationGetRelationName(onerel)))); - if (bms_is_member(i, unique_cols)) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_COLUMN), - errmsg("column \"%s\" of relation \"%s\" appears more than once", - col, RelationGetRelationName(onerel)))); - unique_cols = bms_add_member(unique_cols, i); - + Assert(i != InvalidAttrNumber); vacattrstats[tcnt] = examine_attribute(onerel, i, NULL); if (vacattrstats[tcnt] != NULL) tcnt++; @@ -690,8 +701,8 @@ do_analyze_rel(Relation onerel, VacuumParams *params, * only do it for inherited stats. (We're never called for not-inherited * stats on partitioned tables anyway.) * - * Reset the changes_since_analyze counter only if we analyzed all - * columns; otherwise, there is still work for auto-analyze to do. + * Reset the mod_since_analyze counter only if we analyzed all columns; + * otherwise, there is still work for auto-analyze to do. */ if (!inh) pgstat_report_analyze(onerel, totalrows, totaldeadrows, @@ -736,9 +747,9 @@ do_analyze_rel(Relation onerel, VacuumParams *params, { TimestampTz endtime = GetCurrentTimestamp(); - if (verbose || params->log_min_duration == 0 || + if (verbose || params->log_analyze_min_duration == 0 || TimestampDifferenceExceeds(starttime, endtime, - params->log_min_duration)) + params->log_analyze_min_duration)) { long delay_in_ms; WalUsage walusage; @@ -832,10 +843,11 @@ do_analyze_rel(Relation onerel, VacuumParams *params, total_blks_read, total_blks_dirtied); appendStringInfo(&buf, - _("WAL usage: %" PRId64 " records, %" PRId64 " full page images, %" PRIu64 " bytes, %" PRId64 " buffers full\n"), + _("WAL usage: %" PRId64 " records, %" PRId64 " full page images, %" PRIu64 " bytes, %" PRIu64 " full page image bytes, %" PRId64 " buffers full\n"), walusage.wal_records, walusage.wal_fpi, walusage.wal_bytes, + walusage.wal_fpi_bytes, walusage.wal_buffers_full); appendStringInfo(&buf, _("system usage: %s"), pg_rusage_show(&ru0)); @@ -1023,6 +1035,40 @@ compute_index_stats(Relation onerel, double totalrows, MemoryContextDelete(ind_context); } +/* + * validate_va_cols_list -- validate the columns list given to analyze_rel + * + * Note that system attributes are never analyzed, so we just reject them at + * the lookup stage. We also reject duplicate column mentions. (We could + * alternatively ignore duplicates, but analyzing a column twice won't work; + * we'd end up making a conflicting update in pg_statistic.) + */ +static void +validate_va_cols_list(Relation onerel, List *va_cols) +{ + Bitmapset *unique_cols = NULL; + ListCell *le; + + Assert(va_cols != NIL); + foreach(le, va_cols) + { + char *col = strVal(lfirst(le)); + int i = attnameAttNum(onerel, col, false); + + if (i == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + col, RelationGetRelationName(onerel)))); + if (bms_is_member(i, unique_cols)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("column \"%s\" of relation \"%s\" appears more than once", + col, RelationGetRelationName(onerel)))); + unique_cols = bms_add_member(unique_cols, i); + } +} + /* * examine_attribute -- pre-analysis of a single column * @@ -1037,43 +1083,21 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr) { Form_pg_attribute attr = TupleDescAttr(onerel->rd_att, attnum - 1); int attstattarget; - HeapTuple atttuple; - Datum dat; - bool isnull; HeapTuple typtuple; VacAttrStats *stats; int i; bool ok; - /* Never analyze dropped columns */ - if (attr->attisdropped) - return NULL; - - /* Don't analyze virtual generated columns */ - if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - return NULL; - /* - * Get attstattarget value. Set to -1 if null. (Analyze functions expect - * -1 to mean use default_statistics_target; see for example - * std_typanalyze.) + * Check if the column is analyzable. */ - atttuple = SearchSysCache2(ATTNUM, ObjectIdGetDatum(RelationGetRelid(onerel)), Int16GetDatum(attnum)); - if (!HeapTupleIsValid(atttuple)) - elog(ERROR, "cache lookup failed for attribute %d of relation %u", - attnum, RelationGetRelid(onerel)); - dat = SysCacheGetAttr(ATTNUM, atttuple, Anum_pg_attribute_attstattarget, &isnull); - attstattarget = isnull ? -1 : DatumGetInt16(dat); - ReleaseSysCache(atttuple); - - /* Don't analyze column if user has specified not to */ - if (attstattarget == 0) + if (!attribute_is_analyzable(onerel, attnum, attr, &attstattarget)) return NULL; /* * Create the VacAttrStats struct. */ - stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats)); + stats = palloc0_object(VacAttrStats); stats->attstattarget = attstattarget; /* @@ -1148,6 +1172,45 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr) return stats; } +bool +attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr, + int *p_attstattarget) +{ + int attstattarget; + HeapTuple atttuple; + Datum dat; + bool isnull; + + /* Never analyze dropped columns */ + if (attr->attisdropped) + return false; + + /* Don't analyze virtual generated columns */ + if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + return false; + + /* + * Get attstattarget value. Set to -1 if null. (Analyze functions expect + * -1 to mean use default_statistics_target; see for example + * std_typanalyze.) + */ + atttuple = SearchSysCache2(ATTNUM, ObjectIdGetDatum(RelationGetRelid(onerel)), Int16GetDatum(attnum)); + if (!HeapTupleIsValid(atttuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, RelationGetRelid(onerel)); + dat = SysCacheGetAttr(ATTNUM, atttuple, Anum_pg_attribute_attstattarget, &isnull); + attstattarget = isnull ? -1 : DatumGetInt16(dat); + ReleaseSysCache(atttuple); + + /* Don't analyze column if user has specified not to */ + if (attstattarget == 0) + return false; + + if (p_attstattarget) + *p_attstattarget = attstattarget; + return true; +} + /* * Read stream callback returning the next BlockNumber as chosen by the * BlockSampling algorithm. @@ -1207,7 +1270,6 @@ acquire_sample_rows(Relation onerel, int elevel, double rowstoskip = -1; /* -1 means not set yet */ uint32 randseed; /* Seed for block sampler(s) */ BlockNumber totalblocks; - TransactionId OldestXmin; BlockSamplerData bs; ReservoirStateData rstate; TupleTableSlot *slot; @@ -1220,9 +1282,6 @@ acquire_sample_rows(Relation onerel, int elevel, totalblocks = RelationGetNumberOfBlocks(onerel); - /* Need a cutoff xmin for HeapTupleSatisfiesVacuum */ - OldestXmin = GetOldestNonRemovableTransactionId(onerel); - /* Prepare for sampling block numbers */ randseed = pg_prng_uint32(&pg_global_prng_state); nblocks = BlockSampler_Init(&bs, totalblocks, targrows, randseed); @@ -1255,7 +1314,7 @@ acquire_sample_rows(Relation onerel, int elevel, { vacuum_delay_point(true); - while (table_scan_analyze_next_tuple(scan, OldestXmin, &liverows, &deadrows, slot)) + while (table_scan_analyze_next_tuple(scan, &liverows, &deadrows, slot)) { /* * The first targrows sample rows are simply copied into the @@ -1712,10 +1771,9 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats) i = Anum_pg_statistic_stanumbers1 - 1; for (k = 0; k < STATISTIC_NUM_SLOTS; k++) { - int nnum = stats->numnumbers[k]; - - if (nnum > 0) + if (stats->stanumbers[k] != NULL) { + int nnum = stats->numnumbers[k]; Datum *numdatums = (Datum *) palloc(nnum * sizeof(Datum)); ArrayType *arry; @@ -1733,7 +1791,7 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats) i = Anum_pg_statistic_stavalues1 - 1; for (k = 0; k < STATISTIC_NUM_SLOTS; k++) { - if (stats->numvalues[k] > 0) + if (stats->stavalues[k] != NULL) { ArrayType *arry; @@ -1905,7 +1963,7 @@ std_typanalyze(VacAttrStats *stats) NULL); /* Save the operator info for compute_stats routines */ - mystats = (StdAnalyzeData *) palloc(sizeof(StdAnalyzeData)); + mystats = palloc_object(StdAnalyzeData); mystats->eqopr = eqopr; mystats->eqfunc = OidIsValid(eqopr) ? get_opcode(eqopr) : InvalidOid; mystats->ltopr = ltopr; @@ -2860,7 +2918,7 @@ compute_scalar_stats(VacAttrStatsP stats, /* Must copy the target values into anl_context */ old_context = MemoryContextSwitchTo(stats->anl_context); - corrs = (float4 *) palloc(sizeof(float4)); + corrs = palloc_object(float4); MemoryContextSwitchTo(old_context); /*---------- diff --git a/src/backend/commands/async.c b/src/backend/commands/async.c index 4bd37d5beb559..db6a9a6561bd9 100644 --- a/src/backend/commands/async.c +++ b/src/backend/commands/async.c @@ -3,7 +3,7 @@ * async.c * Asynchronous notification: NOTIFY, LISTEN, UNLISTEN * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -13,19 +13,16 @@ */ /*------------------------------------------------------------------------- - * Async Notification Model as of 9.0: + * Async Notification Model as of v19: * - * 1. Multiple backends on same machine. Multiple backends listening on - * several channels. (Channels are also called "conditions" in other - * parts of the code.) + * 1. Multiple backends on same machine. Multiple backends may be listening + * on each of several channels. * * 2. There is one central queue in disk-based storage (directory pg_notify/), * with actively-used pages mapped into shared memory by the slru.c module. * All notification messages are placed in the queue and later read out - * by listening backends. - * - * There is no central knowledge of which backend listens on which channel; - * every backend has its own list of interesting channels. + * by listening backends. The single queue allows us to guarantee that + * notifications are received in commit order. * * Although there is only one queue, notifications are treated as being * database-local; this is done by including the sender's database OID @@ -62,22 +59,17 @@ * page number and the offset in that page. This is done before marking the * transaction as committed in clog. If we run into problems writing the * notifications, we can still call elog(ERROR, ...) and the transaction - * will roll back. + * will roll back safely. * * Once we have put all of the notifications into the queue, we return to * CommitTransaction() which will then do the actual transaction commit. * * After commit we are called another time (AtCommit_Notify()). Here we - * make any actual updates to the effective listen state (listenChannels). + * make any required updates to the effective listen state (see below). * Then we signal any backends that may be interested in our messages * (including our own backend, if listening). This is done by - * SignalBackends(), which scans the list of listening backends and sends a - * PROCSIG_NOTIFY_INTERRUPT signal to every listening backend (we don't - * know which backend is listening on which channel so we must signal them - * all). We can exclude backends that are already up to date, though, and - * we can also exclude backends that are in other databases (unless they - * are way behind and should be kicked to make them advance their - * pointers). + * SignalBackends(), which sends a PROCSIG_NOTIFY_INTERRUPT signal to + * each relevant backend, as described below. * * Finally, after we are out of the transaction altogether and about to go * idle, we scan the queue for messages that need to be sent to our @@ -109,6 +101,47 @@ * often. We make sending backends do this work if they advanced the queue * head into a new page, but only once every QUEUE_CLEANUP_DELAY pages. * + * 7. So far we have not discussed how backends change their listening state, + * nor how notification senders know which backends to awaken. To handle + * the latter, we maintain a global channel table (implemented as a dynamic + * shared hash table, or dshash) that maps channel names to the set of + * backends listening on each channel. This table is created lazily on the + * first LISTEN command and grows dynamically as needed. There is also a + * local channel table (a plain dynahash table) in each listening backend, + * tracking which channels that backend is listening to. The local table + * serves to reduce the number of accesses needed to the shared table. + * + * If the current transaction has executed any LISTEN/UNLISTEN actions, + * PreCommit_Notify() prepares to commit those. For LISTEN, it + * pre-allocates entries in both the per-backend localChannelTable and the + * shared globalChannelTable (with listening=false so that these entries + * are no-ops for the moment). It also records the final per-channel + * intent in pendingListenActions, so post-commit/abort processing can + * apply that in a single step. Since all these allocations happen before + * committing to clog, we can safely abort the transaction on failure. + * + * After commit, AtCommit_Notify() runs through pendingListenActions and + * updates the backend's per-channel listening flags to activate or + * deactivate listening. This happens before sending signals. + * + * SignalBackends() consults the shared global channel table to identify + * listeners for the channels that the current transaction sent + * notification(s) to. Each selected backend is marked as having a wakeup + * pending to avoid duplicate signals, and a PROCSIG_NOTIFY_INTERRUPT + * signal is sent to it. + * + * 8. While writing notifications, PreCommit_Notify() records the queue head + * position both before and after the write. Because all writers serialize + * on a cluster-wide heavyweight lock, no other backend can insert entries + * between these two points. SignalBackends() uses this fact to directly + * advance the queue pointer for any backend that is still positioned at + * the old head, or within the range written, but is not interested in any + * of our notifications. This avoids unnecessary wakeups for idle + * listeners that have nothing to read. Backends that are not interested + * in our notifications, but cannot be directly advanced, are signaled only + * if they are far behind the current queue head; that is to ensure that + * we can advance the queue tail without undue delay. + * * An application that listens on the same channel it notifies will get * NOTIFY messages for its own NOTIFYs. These can be ignored, if not useful, * by comparing be_pid in the NOTIFY message to the application's own backend's @@ -119,7 +152,7 @@ * The amount of shared memory used for notify management (notify_buffers) * can be varied without affecting anything but performance. The maximum * amount of notification data that can be queued at one time is determined - * by max_notify_queue_pages GUC. + * by the max_notify_queue_pages GUC. *------------------------------------------------------------------------- */ @@ -137,14 +170,19 @@ #include "commands/async.h" #include "common/hashfn.h" #include "funcapi.h" +#include "lib/dshash.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/ipc.h" +#include "storage/latch.h" #include "storage/lmgr.h" #include "storage/procsignal.h" +#include "storage/subsystems.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" +#include "utils/dsa.h" #include "utils/guc_hooks.h" #include "utils/memutils.h" #include "utils/ps_status.h" @@ -224,11 +262,17 @@ typedef struct QueuePosition (x).page != (y).page ? (x) : \ (x).offset > (y).offset ? (x) : (y)) +/* returns true if x comes before y in queue order */ +#define QUEUE_POS_PRECEDES(x,y) \ + (asyncQueuePagePrecedes((x).page, (y).page) || \ + ((x).page == (y).page && (x).offset < (y).offset)) + /* * Parameter determining how often we try to advance the tail pointer: * we do that after every QUEUE_CLEANUP_DELAY pages of NOTIFY data. This is - * also the distance by which a backend in another database needs to be - * behind before we'll decide we need to wake it up to advance its pointer. + * also the distance by which a backend that's not interested in our + * notifications needs to be behind before we'll decide we need to wake it + * up so it can advance its pointer. * * Resist the temptation to make this really large. While that would save * work in some places, it would add cost in others. In particular, this @@ -246,6 +290,8 @@ typedef struct QueueBackendStatus Oid dboid; /* backend's database OID, or InvalidOid */ ProcNumber nextListener; /* id of next listener, or INVALID_PROC_NUMBER */ QueuePosition pos; /* backend has read queue up to here */ + bool wakeupPending; /* signal sent to backend, not yet processed */ + bool isAdvancing; /* backend is advancing its position */ } QueueBackendStatus; /* @@ -260,14 +306,18 @@ typedef struct QueueBackendStatus * (since no other backend will inspect it). * * When holding NotifyQueueLock in EXCLUSIVE mode, backends can inspect the - * entries of other backends and also change the head pointer. When holding - * both NotifyQueueLock and NotifyQueueTailLock in EXCLUSIVE mode, backends - * can change the tail pointers. + * entries of other backends and also change the head pointer. They can + * also advance other backends' queue positions, unless the other backend + * has isAdvancing set (i.e., is in process of doing that itself). + * + * When holding both NotifyQueueLock and NotifyQueueTailLock in EXCLUSIVE + * mode, backends can change the tail pointers. * * SLRU buffer pool is divided in banks and bank wise SLRU lock is used as * the control lock for the pg_notify SLRU buffers. * In order to avoid deadlocks, whenever we need multiple locks, we first get - * NotifyQueueTailLock, then NotifyQueueLock, and lastly SLRU bank lock. + * NotifyQueueTailLock, then NotifyQueueLock, then SLRU bank lock, and lastly + * globalChannelTable partition locks. * * Each backend uses the backend[] array entry with index equal to its * ProcNumber. We rely on this to make SendProcSignal fast. @@ -288,11 +338,23 @@ typedef struct AsyncQueueControl ProcNumber firstListener; /* id of first listener, or * INVALID_PROC_NUMBER */ TimestampTz lastQueueFillWarn; /* time of last queue-full msg */ + dsa_handle globalChannelTableDSA; /* global channel table's DSA handle */ + dshash_table_handle globalChannelTableDSH; /* and its dshash handle */ + /* Array with room for MaxBackends entries: */ QueueBackendStatus backend[FLEXIBLE_ARRAY_MEMBER]; } AsyncQueueControl; static AsyncQueueControl *asyncQueueControl; +static void AsyncShmemRequest(void *arg); +static void AsyncShmemInit(void *arg); + +const ShmemCallbacks AsyncShmemCallbacks = { + .request_fn = AsyncShmemRequest, + .init_fn = AsyncShmemInit, +}; + + #define QUEUE_HEAD (asyncQueueControl->head) #define QUEUE_TAIL (asyncQueueControl->tail) #define QUEUE_STOP_PAGE (asyncQueueControl->stopPage) @@ -301,28 +363,72 @@ static AsyncQueueControl *asyncQueueControl; #define QUEUE_BACKEND_DBOID(i) (asyncQueueControl->backend[i].dboid) #define QUEUE_NEXT_LISTENER(i) (asyncQueueControl->backend[i].nextListener) #define QUEUE_BACKEND_POS(i) (asyncQueueControl->backend[i].pos) +#define QUEUE_BACKEND_WAKEUP_PENDING(i) (asyncQueueControl->backend[i].wakeupPending) +#define QUEUE_BACKEND_IS_ADVANCING(i) (asyncQueueControl->backend[i].isAdvancing) /* * The SLRU buffer area through which we access the notification queue */ -static SlruCtlData NotifyCtlData; +static inline bool asyncQueuePagePrecedes(int64 p, int64 q); +static int asyncQueueErrdetailForIoError(const void *opaque_data); + +static SlruDesc NotifySlruDesc; + -#define NotifyCtl (&NotifyCtlData) +#define NotifyCtl (&NotifySlruDesc) #define QUEUE_PAGESIZE BLCKSZ #define QUEUE_FULL_WARN_INTERVAL 5000 /* warn at most once every 5s */ /* - * listenChannels identifies the channels we are actually listening to - * (ie, have committed a LISTEN on). It is a simple list of channel names, - * allocated in TopMemoryContext. + * Global channel table definitions + * + * This hash table maps (database OID, channel name) keys to arrays of + * ProcNumbers representing the backends listening or about to listen + * on each channel. The "listening" flags allow us to create hash table + * entries pre-commit and not have to assume that creating them post-commit + * will succeed. + */ +#define INITIAL_LISTENERS_ARRAY_SIZE 4 + +typedef struct GlobalChannelKey +{ + Oid dboid; + char channel[NAMEDATALEN]; +} GlobalChannelKey; + +typedef struct ListenerEntry +{ + ProcNumber procNo; /* listener's ProcNumber */ + bool listening; /* true if committed listener */ +} ListenerEntry; + +typedef struct GlobalChannelEntry +{ + GlobalChannelKey key; /* hash key */ + dsa_pointer listenersArray; /* DSA pointer to ListenerEntry array */ + int numListeners; /* Number of listeners currently stored */ + int allocatedListeners; /* Allocated size of array */ +} GlobalChannelEntry; + +static dshash_table *globalChannelTable = NULL; +static dsa_area *globalChannelDSA = NULL; + +/* + * localChannelTable caches the channel names this backend is listening on + * (including those we have staged to be listened on, but not yet committed). + * Used by IsListeningOn() for fast lookups when reading notifications. */ -static List *listenChannels = NIL; /* list of C strings */ +static HTAB *localChannelTable = NULL; + +/* We test this condition to detect that we're not listening at all */ +#define LocalChannelTableIsEmpty() \ + (localChannelTable == NULL || hash_get_num_entries(localChannelTable) == 0) /* * State for pending LISTEN/UNLISTEN actions consists of an ordered list of * all actions requested in the current transaction. As explained above, - * we don't actually change listenChannels until we reach transaction commit. + * we don't actually change listen state until we reach transaction commit. * * The list is kept in CurTransactionContext. In subtransactions, each * subtransaction has its own list in its own CurTransactionContext, but @@ -351,6 +457,28 @@ typedef struct ActionList static ActionList *pendingActions = NULL; +/* + * Hash table recording the final listen/unlisten intent per channel for + * the current transaction. Key is channel name, value is PENDING_LISTEN or + * PENDING_UNLISTEN. This keeps critical commit/abort processing to one step + * per channel instead of replaying every action. This is built from the + * pendingActions list by PreCommit_Notify, then used by AtCommit_Notify or + * AtAbort_Notify. + */ +typedef enum +{ + PENDING_LISTEN, + PENDING_UNLISTEN, +} PendingListenAction; + +typedef struct PendingListenEntry +{ + char channel[NAMEDATALEN]; /* hash key */ + PendingListenAction action; /* which action should we perform? */ +} PendingListenEntry; + +static HTAB *pendingListenActions = NULL; + /* * State for outbound notifies consists of a list of all channels+payloads * NOTIFYed in the current transaction. We do not actually perform a NOTIFY @@ -391,6 +519,8 @@ typedef struct NotificationList int nestingLevel; /* current transaction nesting depth */ List *events; /* list of Notification structs */ HTAB *hashtab; /* hash of NotificationHash structs, or NULL */ + List *uniqueChannelNames; /* unique channel names being notified */ + HTAB *uniqueChannelHash; /* hash of unique channel names, or NULL */ struct NotificationList *upper; /* details for upper transaction levels */ } NotificationList; @@ -403,6 +533,15 @@ struct NotificationHash static NotificationList *pendingNotifies = NULL; +/* + * Hash entry in NotificationList.uniqueChannelHash or localChannelTable + * (both just carry the channel name, with no payload). + */ +typedef struct ChannelName +{ + char channel[NAMEDATALEN]; /* hash key */ +} ChannelName; + /* * Inbound notifications are initially processed by HandleNotifyInterrupt(), * called from inside a signal handler. That just sets the @@ -418,6 +557,23 @@ static bool unlistenExitRegistered = false; /* True if we're currently registered as a listener in asyncQueueControl */ static bool amRegisteredListener = false; +/* + * Queue head positions for direct advancement. + * These are captured during PreCommit_Notify while holding the heavyweight + * lock on database 0, ensuring no other backend can insert notifications + * between them. SignalBackends uses these to advance idle backends. + */ +static QueuePosition queueHeadBeforeWrite; +static QueuePosition queueHeadAfterWrite; + +/* + * Workspace arrays for SignalBackends. These are preallocated in + * PreCommit_Notify to avoid needing memory allocation after committing to + * clog. + */ +static int32 *signalPids = NULL; +static ProcNumber *signalProcnos = NULL; + /* have we advanced to a page that's a multiple of QUEUE_CLEANUP_DELAY? */ static bool tryAdvanceTail = false; @@ -429,13 +585,23 @@ int max_notify_queue_pages = 1048576; /* local function prototypes */ static inline int64 asyncQueuePageDiff(int64 p, int64 q); -static inline bool asyncQueuePagePrecedes(int64 p, int64 q); +static inline void GlobalChannelKeyInit(GlobalChannelKey *key, Oid dboid, + const char *channel); +static dshash_hash globalChannelTableHash(const void *key, size_t size, + void *arg); +static void initGlobalChannelTable(void); +static void initLocalChannelTable(void); static void queue_listen(ListenActionKind action, const char *channel); static void Async_UnlistenOnExit(int code, Datum arg); -static void Exec_ListenPreCommit(void); -static void Exec_ListenCommit(const char *channel); -static void Exec_UnlistenCommit(const char *channel); -static void Exec_UnlistenAllCommit(void); +static void BecomeRegisteredListener(void); +static void PrepareTableEntriesForListen(const char *channel); +static void PrepareTableEntriesForUnlisten(const char *channel); +static void PrepareTableEntriesForUnlistenAll(void); +static void RemoveListenerFromChannel(GlobalChannelEntry **entry_ptr, + ListenerEntry *listeners, + int idx); +static void ApplyPendingListenActions(bool isCommit); +static void CleanupListenersOnExit(void); static bool IsListeningOn(const char *channel); static void asyncQueueUnregister(void); static bool asyncQueueIsFull(void); @@ -446,9 +612,8 @@ static double asyncQueueUsage(void); static void asyncQueueFillWarning(void); static void SignalBackends(void); static void asyncQueueReadAllNotifications(void); -static bool asyncQueueProcessPageEntries(volatile QueuePosition *current, +static bool asyncQueueProcessPageEntries(QueuePosition *current, QueuePosition stop, - char *page_buffer, Snapshot snapshot); static void asyncQueueAdvanceTail(void); static void ProcessIncomingNotify(bool flush); @@ -458,6 +623,15 @@ static uint32 notification_hash(const void *key, Size keysize); static int notification_match(const void *key1, const void *key2, Size keysize); static void ClearPendingActionsAndNotifies(void); +static int +asyncQueueErrdetailForIoError(const void *opaque_data) +{ + const QueuePosition *position = opaque_data; + + return errdetail("Could not access async queue at page %" PRId64 ", offset %d.", + position->page, position->offset); +} + /* * Compute the difference between two queue page numbers. * Previously this function accounted for a wraparound. @@ -479,73 +653,202 @@ asyncQueuePagePrecedes(int64 p, int64 q) } /* - * Report space needed for our shared memory area + * GlobalChannelKeyInit + * Prepare a global channel table key for hashing. */ -Size -AsyncShmemSize(void) +static inline void +GlobalChannelKeyInit(GlobalChannelKey *key, Oid dboid, const char *channel) { - Size size; + memset(key, 0, sizeof(GlobalChannelKey)); + key->dboid = dboid; + strlcpy(key->channel, channel, NAMEDATALEN); +} - /* This had better match AsyncShmemInit */ - size = mul_size(MaxBackends, sizeof(QueueBackendStatus)); - size = add_size(size, offsetof(AsyncQueueControl, backend)); +/* + * globalChannelTableHash + * Hash function for global channel table keys. + */ +static dshash_hash +globalChannelTableHash(const void *key, size_t size, void *arg) +{ + const GlobalChannelKey *k = (const GlobalChannelKey *) key; + dshash_hash h; + + h = DatumGetUInt32(hash_uint32(k->dboid)); + h ^= DatumGetUInt32(hash_any((const unsigned char *) k->channel, + strnlen(k->channel, NAMEDATALEN))); - size = add_size(size, SimpleLruShmemSize(notify_buffers, 0)); + return h; +} + +/* parameters for the global channel table */ +static const dshash_parameters globalChannelTableDSHParams = { + sizeof(GlobalChannelKey), + sizeof(GlobalChannelEntry), + dshash_memcmp, + globalChannelTableHash, + dshash_memcpy, + LWTRANCHE_NOTIFY_CHANNEL_HASH +}; + +/* + * initGlobalChannelTable + * Lazy initialization of the global channel table. + */ +static void +initGlobalChannelTable(void) +{ + MemoryContext oldcontext; + + /* Quick exit if we already did this */ + if (asyncQueueControl->globalChannelTableDSH != DSHASH_HANDLE_INVALID && + globalChannelTable != NULL) + return; + + /* Otherwise, use a lock to ensure only one process creates the table */ + LWLockAcquire(NotifyQueueLock, LW_EXCLUSIVE); + + /* Be sure any local memory allocated by DSA routines is persistent */ + oldcontext = MemoryContextSwitchTo(TopMemoryContext); - return size; + if (asyncQueueControl->globalChannelTableDSH == DSHASH_HANDLE_INVALID) + { + /* Initialize dynamic shared hash table for global channels */ + globalChannelDSA = dsa_create(LWTRANCHE_NOTIFY_CHANNEL_HASH); + dsa_pin(globalChannelDSA); + dsa_pin_mapping(globalChannelDSA); + globalChannelTable = dshash_create(globalChannelDSA, + &globalChannelTableDSHParams, + NULL); + + /* Store handles in shared memory for other backends to use */ + asyncQueueControl->globalChannelTableDSA = dsa_get_handle(globalChannelDSA); + asyncQueueControl->globalChannelTableDSH = + dshash_get_hash_table_handle(globalChannelTable); + } + else if (!globalChannelTable) + { + /* Attach to existing dynamic shared hash table */ + globalChannelDSA = dsa_attach(asyncQueueControl->globalChannelTableDSA); + dsa_pin_mapping(globalChannelDSA); + globalChannelTable = dshash_attach(globalChannelDSA, + &globalChannelTableDSHParams, + asyncQueueControl->globalChannelTableDSH, + NULL); + } + + MemoryContextSwitchTo(oldcontext); + LWLockRelease(NotifyQueueLock); } /* - * Initialize our shared memory area + * initLocalChannelTable + * Lazy initialization of the local channel table. + * Once created, this table lasts for the life of the session. */ -void -AsyncShmemInit(void) +static void +initLocalChannelTable(void) +{ + HASHCTL hash_ctl; + + /* Quick exit if we already did this */ + if (localChannelTable != NULL) + return; + + /* Initialize local hash table for this backend's listened channels */ + hash_ctl.keysize = NAMEDATALEN; + hash_ctl.entrysize = sizeof(ChannelName); + + localChannelTable = + hash_create("Local Listen Channels", + 64, + &hash_ctl, + HASH_ELEM | HASH_STRINGS); +} + +/* + * initPendingListenActions + * Lazy initialization of the pending listen actions hash table. + * This is allocated in CurTransactionContext during PreCommit_Notify, + * and destroyed at transaction end. + */ +static void +initPendingListenActions(void) +{ + HASHCTL hash_ctl; + + if (pendingListenActions != NULL) + return; + + hash_ctl.keysize = NAMEDATALEN; + hash_ctl.entrysize = sizeof(PendingListenEntry); + hash_ctl.hcxt = CurTransactionContext; + + pendingListenActions = + hash_create("Pending Listen Actions", + list_length(pendingActions->actions), + &hash_ctl, + HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); +} + +/* + * Register our shared memory needs + */ +static void +AsyncShmemRequest(void *arg) { - bool found; Size size; - /* - * Create or attach to the AsyncQueueControl structure. - */ size = mul_size(MaxBackends, sizeof(QueueBackendStatus)); size = add_size(size, offsetof(AsyncQueueControl, backend)); - asyncQueueControl = (AsyncQueueControl *) - ShmemInitStruct("Async Queue Control", size, &found); + ShmemRequestStruct(.name = "Async Queue Control", + .size = size, + .ptr = (void **) &asyncQueueControl, + ); - if (!found) + SimpleLruRequest(.desc = &NotifySlruDesc, + .name = "notify", + .Dir = "pg_notify", + + /* long segment names are used in order to avoid wraparound */ + .long_segment_names = true, + + .nslots = notify_buffers, + + .sync_handler = SYNC_HANDLER_NONE, + .PagePrecedes = asyncQueuePagePrecedes, + .errdetail_for_io_error = asyncQueueErrdetailForIoError, + + .buffer_tranche_id = LWTRANCHE_NOTIFY_BUFFER, + .bank_tranche_id = LWTRANCHE_NOTIFY_SLRU, + ); +} + +static void +AsyncShmemInit(void *arg) +{ + SET_QUEUE_POS(QUEUE_HEAD, 0, 0); + SET_QUEUE_POS(QUEUE_TAIL, 0, 0); + QUEUE_STOP_PAGE = 0; + QUEUE_FIRST_LISTENER = INVALID_PROC_NUMBER; + asyncQueueControl->lastQueueFillWarn = 0; + asyncQueueControl->globalChannelTableDSA = DSA_HANDLE_INVALID; + asyncQueueControl->globalChannelTableDSH = DSHASH_HANDLE_INVALID; + for (int i = 0; i < MaxBackends; i++) { - /* First time through, so initialize it */ - SET_QUEUE_POS(QUEUE_HEAD, 0, 0); - SET_QUEUE_POS(QUEUE_TAIL, 0, 0); - QUEUE_STOP_PAGE = 0; - QUEUE_FIRST_LISTENER = INVALID_PROC_NUMBER; - asyncQueueControl->lastQueueFillWarn = 0; - for (int i = 0; i < MaxBackends; i++) - { - QUEUE_BACKEND_PID(i) = InvalidPid; - QUEUE_BACKEND_DBOID(i) = InvalidOid; - QUEUE_NEXT_LISTENER(i) = INVALID_PROC_NUMBER; - SET_QUEUE_POS(QUEUE_BACKEND_POS(i), 0, 0); - } + QUEUE_BACKEND_PID(i) = InvalidPid; + QUEUE_BACKEND_DBOID(i) = InvalidOid; + QUEUE_NEXT_LISTENER(i) = INVALID_PROC_NUMBER; + SET_QUEUE_POS(QUEUE_BACKEND_POS(i), 0, 0); + QUEUE_BACKEND_WAKEUP_PENDING(i) = false; + QUEUE_BACKEND_IS_ADVANCING(i) = false; } /* - * Set up SLRU management of the pg_notify data. Note that long segment - * names are used in order to avoid wraparound. + * During start or reboot, clean out the pg_notify directory. */ - NotifyCtl->PagePrecedes = asyncQueuePagePrecedes; - SimpleLruInit(NotifyCtl, "notify", notify_buffers, 0, - "pg_notify", LWTRANCHE_NOTIFY_BUFFER, LWTRANCHE_NOTIFY_SLRU, - SYNC_HANDLER_NONE, true); - - if (!found) - { - /* - * During start or reboot, clean out the pg_notify directory. - */ - (void) SlruScanDirectory(NotifyCtl, SlruScanDirCbDeleteAll, NULL); - } + (void) SlruScanDirectory(NotifyCtl, SlruScanDirCbDeleteAll, NULL); } @@ -657,6 +960,9 @@ Async_Notify(const char *channel, const char *payload) notifies->events = list_make1(n); /* We certainly don't need a hashtable yet */ notifies->hashtab = NULL; + /* We won't build uniqueChannelNames/Hash till later, either */ + notifies->uniqueChannelNames = NIL; + notifies->uniqueChannelHash = NULL; notifies->upper = pendingNotifies; pendingNotifies = notifies; } @@ -683,8 +989,8 @@ Async_Notify(const char *channel, const char *payload) * Common code for listen, unlisten, unlisten all commands. * * Adds the request to the list of pending actions. - * Actual update of the listenChannels list happens during transaction - * commit. + * Actual update of localChannelTable and globalChannelTable happens during + * PreCommit_Notify, with staged changes committed in AtCommit_Notify. */ static void queue_listen(ListenActionKind action, const char *channel) @@ -694,10 +1000,9 @@ queue_listen(ListenActionKind action, const char *channel) int my_level = GetCurrentTransactionNestLevel(); /* - * Unlike Async_Notify, we don't try to collapse out duplicates. It would - * be too complicated to ensure we get the right interactions of - * conflicting LISTEN/UNLISTEN/UNLISTEN_ALL, and it's unlikely that there - * would be any performance benefit anyway in sane applications. + * Unlike Async_Notify, we don't try to collapse out duplicates here. We + * keep the ordered list to preserve interactions like UNLISTEN ALL; the + * final per-channel intent is computed during PreCommit_Notify. */ oldcontext = MemoryContextSwitchTo(CurTransactionContext); @@ -783,30 +1088,49 @@ Async_UnlistenAll(void) * SQL function: return a set of the channel names this backend is actively * listening to. * - * Note: this coding relies on the fact that the listenChannels list cannot + * Note: this coding relies on the fact that the localChannelTable cannot * change within a transaction. */ Datum pg_listening_channels(PG_FUNCTION_ARGS) { FuncCallContext *funcctx; + HASH_SEQ_STATUS *status; /* stuff done only on the first call of the function */ if (SRF_IS_FIRSTCALL()) { /* create a function context for cross-call persistence */ funcctx = SRF_FIRSTCALL_INIT(); + + /* Initialize hash table iteration if we have any channels */ + if (localChannelTable != NULL) + { + MemoryContext oldcontext; + + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + status = (HASH_SEQ_STATUS *) palloc(sizeof(HASH_SEQ_STATUS)); + hash_seq_init(status, localChannelTable); + funcctx->user_fctx = status; + MemoryContextSwitchTo(oldcontext); + } + else + { + funcctx->user_fctx = NULL; + } } /* stuff done on every call of the function */ funcctx = SRF_PERCALL_SETUP(); + status = (HASH_SEQ_STATUS *) funcctx->user_fctx; - if (funcctx->call_cntr < list_length(listenChannels)) + if (status != NULL) { - char *channel = (char *) list_nth(listenChannels, - funcctx->call_cntr); + ChannelName *entry; - SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(channel)); + entry = (ChannelName *) hash_seq_search(status); + if (entry != NULL) + SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(entry->channel)); } SRF_RETURN_DONE(funcctx); @@ -822,7 +1146,7 @@ pg_listening_channels(PG_FUNCTION_ARGS) static void Async_UnlistenOnExit(int code, Datum arg) { - Exec_UnlistenAllCommit(); + CleanupListenersOnExit(); asyncQueueUnregister(); } @@ -869,8 +1193,16 @@ PreCommit_Notify(void) elog(DEBUG1, "PreCommit_Notify"); /* Preflight for any pending listen/unlisten actions */ + initGlobalChannelTable(); + if (pendingActions != NULL) { + /* Ensure we have a local channel table */ + initLocalChannelTable(); + /* Create pendingListenActions hash table for this transaction */ + initPendingListenActions(); + + /* Stage all the actions this transaction wants to perform */ foreach(p, pendingActions->actions) { ListenAction *actrec = (ListenAction *) lfirst(p); @@ -878,13 +1210,14 @@ PreCommit_Notify(void) switch (actrec->action) { case LISTEN_LISTEN: - Exec_ListenPreCommit(); + BecomeRegisteredListener(); + PrepareTableEntriesForListen(actrec->channel); break; case LISTEN_UNLISTEN: - /* there is no Exec_UnlistenPreCommit() */ + PrepareTableEntriesForUnlisten(actrec->channel); break; case LISTEN_UNLISTEN_ALL: - /* there is no Exec_UnlistenAllPreCommit() */ + PrepareTableEntriesForUnlistenAll(); break; } } @@ -894,6 +1227,60 @@ PreCommit_Notify(void) if (pendingNotifies) { ListCell *nextNotify; + bool firstIteration = true; + + /* + * Build list of unique channel names being notified for use by + * SignalBackends(). + * + * If uniqueChannelHash is available, use it to efficiently get the + * unique channels. Otherwise, fall back to the O(N^2) approach. + */ + pendingNotifies->uniqueChannelNames = NIL; + if (pendingNotifies->uniqueChannelHash != NULL) + { + HASH_SEQ_STATUS status; + ChannelName *channelEntry; + + hash_seq_init(&status, pendingNotifies->uniqueChannelHash); + while ((channelEntry = (ChannelName *) hash_seq_search(&status)) != NULL) + pendingNotifies->uniqueChannelNames = + lappend(pendingNotifies->uniqueChannelNames, + channelEntry->channel); + } + else + { + /* O(N^2) approach is better for small number of notifications */ + foreach_ptr(Notification, n, pendingNotifies->events) + { + char *channel = n->data; + bool found = false; + + /* Name present in list? */ + foreach_ptr(char, oldchan, pendingNotifies->uniqueChannelNames) + { + if (strcmp(oldchan, channel) == 0) + { + found = true; + break; + } + } + /* Add if not already in list */ + if (!found) + pendingNotifies->uniqueChannelNames = + lappend(pendingNotifies->uniqueChannelNames, + channel); + } + } + + /* Preallocate workspace that will be needed by SignalBackends() */ + if (signalPids == NULL) + signalPids = MemoryContextAlloc(TopMemoryContext, + MaxBackends * sizeof(int32)); + + if (signalProcnos == NULL) + signalProcnos = MemoryContextAlloc(TopMemoryContext, + MaxBackends * sizeof(ProcNumber)); /* * Make sure that we have an XID assigned to the current transaction. @@ -922,6 +1309,23 @@ PreCommit_Notify(void) LockSharedObject(DatabaseRelationId, InvalidOid, 0, AccessExclusiveLock); + /* + * For the direct advancement optimization in SignalBackends(), we + * need to ensure that no other backend can insert queue entries + * between queueHeadBeforeWrite and queueHeadAfterWrite. The + * heavyweight lock above provides this guarantee, since it serializes + * all writers. + * + * Note: if the heavyweight lock were ever removed for scalability + * reasons, we could achieve the same guarantee by holding + * NotifyQueueLock in EXCLUSIVE mode across all our insertions, rather + * than releasing and reacquiring it for each page as we do below. + */ + + /* Initialize values to a safe default in case list is empty */ + SET_QUEUE_POS(queueHeadBeforeWrite, 0, 0); + SET_QUEUE_POS(queueHeadAfterWrite, 0, 0); + /* Now push the notifications into the queue */ nextNotify = list_head(pendingNotifies->events); while (nextNotify != NULL) @@ -939,12 +1343,18 @@ PreCommit_Notify(void) * point in time we can still roll the transaction back. */ LWLockAcquire(NotifyQueueLock, LW_EXCLUSIVE); + if (firstIteration) + { + queueHeadBeforeWrite = QUEUE_HEAD; + firstIteration = false; + } asyncQueueFillWarning(); if (asyncQueueIsFull()) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("too many notifications in the NOTIFY queue"))); nextNotify = asyncQueueAddEntries(nextNotify); + queueHeadAfterWrite = QUEUE_HEAD; LWLockRelease(NotifyQueueLock); } @@ -957,7 +1367,7 @@ PreCommit_Notify(void) * * This is called at transaction commit, after committing to clog. * - * Update listenChannels and clear transaction-local state. + * Apply pending listen/unlisten changes and clear transaction-local state. * * If we issued any notifications in the transaction, send signals to * listening backends (possibly including ourselves) to process them. @@ -967,8 +1377,6 @@ PreCommit_Notify(void) void AtCommit_Notify(void) { - ListCell *p; - /* * Allow transactions that have not executed LISTEN/UNLISTEN/NOTIFY to * return as soon as possible @@ -979,30 +1387,11 @@ AtCommit_Notify(void) if (Trace_notify) elog(DEBUG1, "AtCommit_Notify"); - /* Perform any pending listen/unlisten actions */ - if (pendingActions != NULL) - { - foreach(p, pendingActions->actions) - { - ListenAction *actrec = (ListenAction *) lfirst(p); - - switch (actrec->action) - { - case LISTEN_LISTEN: - Exec_ListenCommit(actrec->channel); - break; - case LISTEN_UNLISTEN: - Exec_UnlistenCommit(actrec->channel); - break; - case LISTEN_UNLISTEN_ALL: - Exec_UnlistenAllCommit(); - break; - } - } - } + /* Apply staged listen/unlisten changes */ + ApplyPendingListenActions(true); /* If no longer listening to anything, get out of listener array */ - if (amRegisteredListener && listenChannels == NIL) + if (amRegisteredListener && LocalChannelTableIsEmpty()) asyncQueueUnregister(); /* @@ -1033,12 +1422,12 @@ AtCommit_Notify(void) } /* - * Exec_ListenPreCommit --- subroutine for PreCommit_Notify + * BecomeRegisteredListener --- subroutine for PreCommit_Notify * * This function must make sure we are ready to catch any incoming messages. */ static void -Exec_ListenPreCommit(void) +BecomeRegisteredListener(void) { QueuePosition head; QueuePosition max; @@ -1052,7 +1441,7 @@ Exec_ListenPreCommit(void) return; if (Trace_notify) - elog(DEBUG1, "Exec_ListenPreCommit(%d)", MyProcPid); + elog(DEBUG1, "BecomeRegisteredListener(%d)", MyProcPid); /* * Before registering, make sure we will unlisten before dying. (Note: @@ -1099,6 +1488,8 @@ Exec_ListenPreCommit(void) QUEUE_BACKEND_POS(MyProcNumber) = max; QUEUE_BACKEND_PID(MyProcNumber) = MyProcPid; QUEUE_BACKEND_DBOID(MyProcNumber) = MyDatabaseId; + QUEUE_BACKEND_WAKEUP_PENDING(MyProcNumber) = false; + QUEUE_BACKEND_IS_ADVANCING(MyProcNumber) = false; /* Insert backend into list of listeners at correct position */ if (prevListener != INVALID_PROC_NUMBER) { @@ -1128,99 +1519,393 @@ Exec_ListenPreCommit(void) } /* - * Exec_ListenCommit --- subroutine for AtCommit_Notify + * PrepareTableEntriesForListen --- subroutine for PreCommit_Notify * - * Add the channel to the list of channels we are listening on. + * Prepare a LISTEN by recording it in pendingListenActions, pre-allocating + * an entry in localChannelTable, and pre-allocating an entry in the shared + * globalChannelTable with listening=false. The listening flag will be set + * to true in AtCommit_Notify. If we abort later, unwanted table entries + * will be removed. */ static void -Exec_ListenCommit(const char *channel) +PrepareTableEntriesForListen(const char *channel) { - MemoryContext oldcontext; + GlobalChannelKey key; + GlobalChannelEntry *entry; + bool found; + ListenerEntry *listeners; + PendingListenEntry *pending; + + /* + * Record in local pending hash that we want to LISTEN, overwriting any + * earlier attempt to UNLISTEN. + */ + pending = (PendingListenEntry *) + hash_search(pendingListenActions, channel, HASH_ENTER, NULL); + pending->action = PENDING_LISTEN; + + /* + * Ensure that there is an entry for the channel in localChannelTable. + * (Should this fail, we can just roll back.) If the transaction fails + * after this point, we will remove the entry if appropriate during + * ApplyPendingListenActions. Note that this entry allows IsListeningOn() + * to return TRUE; we assume nothing is going to consult that before + * AtCommit_Notify/AtAbort_Notify. However, if later actions attempt to + * UNLISTEN this channel or UNLISTEN *, we need to have the local entry + * present to ensure they do the right things; see + * PrepareTableEntriesForUnlisten and PrepareTableEntriesForUnlistenAll. + */ + (void) hash_search(localChannelTable, channel, HASH_ENTER, NULL); + + /* Pre-allocate entry in shared globalChannelTable with listening=false */ + GlobalChannelKeyInit(&key, MyDatabaseId, channel); + entry = dshash_find_or_insert(globalChannelTable, &key, &found); + + if (!found) + { + /* New channel entry, so initialize it to a safe state */ + entry->listenersArray = InvalidDsaPointer; + entry->numListeners = 0; + entry->allocatedListeners = 0; + } + + /* + * Create listenersArray if entry doesn't have one. It's tempting to fold + * this into the !found case, but this coding allows us to cope in case + * dsa_allocate() failed in an earlier attempt. + */ + if (!DsaPointerIsValid(entry->listenersArray)) + { + entry->listenersArray = dsa_allocate(globalChannelDSA, + sizeof(ListenerEntry) * INITIAL_LISTENERS_ARRAY_SIZE); + entry->allocatedListeners = INITIAL_LISTENERS_ARRAY_SIZE; + } + + listeners = (ListenerEntry *) + dsa_get_address(globalChannelDSA, entry->listenersArray); + + /* + * Check if we already have a ListenerEntry (possibly from earlier in this + * transaction) + */ + for (int i = 0; i < entry->numListeners; i++) + { + if (listeners[i].procNo == MyProcNumber) + { + /* Already have an entry; listening flag stays as-is until commit */ + dshash_release_lock(globalChannelTable, entry); + return; + } + } + + /* Need to add a new entry; grow array if necessary */ + if (entry->numListeners >= entry->allocatedListeners) + { + int new_size = entry->allocatedListeners * 2; + dsa_pointer old_array = entry->listenersArray; + dsa_pointer new_array = dsa_allocate(globalChannelDSA, + sizeof(ListenerEntry) * new_size); + ListenerEntry *new_listeners = (ListenerEntry *) dsa_get_address(globalChannelDSA, new_array); + + memcpy(new_listeners, listeners, sizeof(ListenerEntry) * entry->numListeners); + entry->listenersArray = new_array; + entry->allocatedListeners = new_size; + dsa_free(globalChannelDSA, old_array); + listeners = new_listeners; + } - /* Do nothing if we are already listening on this channel */ - if (IsListeningOn(channel)) + listeners[entry->numListeners].procNo = MyProcNumber; + listeners[entry->numListeners].listening = false; /* staged, not yet + * committed */ + entry->numListeners++; + + dshash_release_lock(globalChannelTable, entry); +} + +/* + * PrepareTableEntriesForUnlisten --- subroutine for PreCommit_Notify + * + * Prepare an UNLISTEN by recording it in pendingListenActions, but only if + * we're currently listening (committed or staged). We don't touch + * globalChannelTable yet - the listener keeps receiving signals until + * commit, when the entry is removed. + */ +static void +PrepareTableEntriesForUnlisten(const char *channel) +{ + PendingListenEntry *pending; + + /* + * If the channel name is not in localChannelTable, then we are neither + * listening on it nor preparing to listen on it, so we don't need to + * record an UNLISTEN action. + */ + Assert(localChannelTable != NULL); + if (hash_search(localChannelTable, channel, HASH_FIND, NULL) == NULL) return; /* - * Add the new channel name to listenChannels. - * - * XXX It is theoretically possible to get an out-of-memory failure here, - * which would be bad because we already committed. For the moment it - * doesn't seem worth trying to guard against that, but maybe improve this - * later. + * Record in local pending hash that we want to UNLISTEN, overwriting any + * earlier attempt to LISTEN. Don't touch localChannelTable or + * globalChannelTable yet - we keep receiving signals until commit. */ - oldcontext = MemoryContextSwitchTo(TopMemoryContext); - listenChannels = lappend(listenChannels, pstrdup(channel)); - MemoryContextSwitchTo(oldcontext); + pending = (PendingListenEntry *) + hash_search(pendingListenActions, channel, HASH_ENTER, NULL); + pending->action = PENDING_UNLISTEN; } /* - * Exec_UnlistenCommit --- subroutine for AtCommit_Notify + * PrepareTableEntriesForUnlistenAll --- subroutine for PreCommit_Notify * - * Remove the specified channel name from listenChannels. + * Prepare UNLISTEN * by recording an UNLISTEN for all listened or + * about-to-be-listened channels in pendingListenActions. */ static void -Exec_UnlistenCommit(const char *channel) +PrepareTableEntriesForUnlistenAll(void) { - ListCell *q; + HASH_SEQ_STATUS seq; + ChannelName *channelEntry; + PendingListenEntry *pending; - if (Trace_notify) - elog(DEBUG1, "Exec_UnlistenCommit(%s,%d)", channel, MyProcPid); + /* + * Scan localChannelTable, which will have the names of all channels that + * we are listening on or have prepared to listen on. Record an UNLISTEN + * action for each one, overwriting any earlier attempt to LISTEN. + */ + hash_seq_init(&seq, localChannelTable); + while ((channelEntry = (ChannelName *) hash_seq_search(&seq)) != NULL) + { + pending = (PendingListenEntry *) + hash_search(pendingListenActions, channelEntry->channel, HASH_ENTER, NULL); + pending->action = PENDING_UNLISTEN; + } +} + +/* + * RemoveListenerFromChannel --- remove idx'th listener from global channel entry + * + * Decrements numListeners, compacts the array, and frees the entry if empty. + * Sets *entry_ptr to NULL if the entry was deleted. + * + * We could get the listeners pointer from the entry, but all callers + * already have it at hand. + */ +static void +RemoveListenerFromChannel(GlobalChannelEntry **entry_ptr, + ListenerEntry *listeners, + int idx) +{ + GlobalChannelEntry *entry = *entry_ptr; + + entry->numListeners--; + if (idx < entry->numListeners) + memmove(&listeners[idx], &listeners[idx + 1], + sizeof(ListenerEntry) * (entry->numListeners - idx)); + + if (entry->numListeners == 0) + { + dsa_free(globalChannelDSA, entry->listenersArray); + dshash_delete_entry(globalChannelTable, entry); + /* tells caller not to release the entry's lock: */ + *entry_ptr = NULL; + } +} - foreach(q, listenChannels) +/* + * ApplyPendingListenActions + * + * Apply, or revert, staged listen/unlisten changes to the local and global + * hash tables. + */ +static void +ApplyPendingListenActions(bool isCommit) +{ + HASH_SEQ_STATUS seq; + PendingListenEntry *pending; + + /* Quick exit if nothing to do */ + if (pendingListenActions == NULL) + return; + + /* We made a globalChannelTable before building pendingListenActions */ + if (globalChannelTable == NULL) + elog(PANIC, "global channel table missing post-commit/abort"); + + /* For each staged action ... */ + hash_seq_init(&seq, pendingListenActions); + while ((pending = (PendingListenEntry *) hash_seq_search(&seq)) != NULL) { - char *lchan = (char *) lfirst(q); + GlobalChannelKey key; + GlobalChannelEntry *entry; + bool removeLocal = true; + bool foundListener = false; - if (strcmp(lchan, channel) == 0) + /* + * Find the global entry for this channel. If isCommit, it had better + * exist (it was created in PreCommit). In an abort, it might not + * exist, in which case we are not listening and should discard any + * local entry that PreCommit may have managed to create. + */ + GlobalChannelKeyInit(&key, MyDatabaseId, pending->channel); + entry = dshash_find(globalChannelTable, &key, true); + if (entry != NULL) { - listenChannels = foreach_delete_current(listenChannels, q); - pfree(lchan); - break; + /* Scan entry to find the ListenerEntry for this backend */ + ListenerEntry *listeners; + + listeners = (ListenerEntry *) + dsa_get_address(globalChannelDSA, entry->listenersArray); + + for (int i = 0; i < entry->numListeners; i++) + { + if (listeners[i].procNo != MyProcNumber) + continue; + foundListener = true; + if (isCommit) + { + if (pending->action == PENDING_LISTEN) + { + /* + * LISTEN being committed: set listening=true. + * localChannelTable entry was created during + * PreCommit and should be kept. + */ + listeners[i].listening = true; + removeLocal = false; + } + else + { + /* + * UNLISTEN being committed: remove pre-allocated + * entries from both tables. + */ + RemoveListenerFromChannel(&entry, listeners, i); + } + } + else + { + /* + * Note: this part is reachable only if the transaction + * aborts after PreCommit_Notify() has made some + * pendingListenActions entries, so it's pretty hard to + * test. + */ + if (!listeners[i].listening) + { + /* + * Staged LISTEN (or LISTEN+UNLISTEN) being aborted, + * and we weren't listening before, so remove + * pre-allocated entries from both tables. + */ + RemoveListenerFromChannel(&entry, listeners, i); + } + else + { + /* + * We're aborting, but the previous state was that + * we're listening, so keep localChannelTable entry. + */ + removeLocal = false; + } + } + break; /* there shouldn't be another match */ + } + + /* We might have already released the entry by removing it */ + if (entry != NULL) + dshash_release_lock(globalChannelTable, entry); } - } - /* - * We do not complain about unlistening something not being listened; - * should we? - */ + /* + * If we're committing a LISTEN action, we should have found a + * matching ListenerEntry, but otherwise it's okay if we didn't. + */ + if (isCommit && pending->action == PENDING_LISTEN && !foundListener) + elog(PANIC, "could not find listener entry for channel \"%s\" backend %d", + pending->channel, MyProcNumber); + + /* + * If we did not find a globalChannelTable entry for our backend, or + * if we are unlistening, remove any localChannelTable entry that may + * exist. (Note in particular that this cleans up if we created a + * localChannelTable entry and then failed while trying to create a + * globalChannelTable entry.) + */ + if (removeLocal && localChannelTable != NULL) + (void) hash_search(localChannelTable, pending->channel, + HASH_REMOVE, NULL); + } } /* - * Exec_UnlistenAllCommit --- subroutine for AtCommit_Notify + * CleanupListenersOnExit --- called from Async_UnlistenOnExit * - * Unlisten on all channels for this backend. + * Remove this backend from all channels in the shared global table. */ static void -Exec_UnlistenAllCommit(void) +CleanupListenersOnExit(void) { + dshash_seq_status status; + GlobalChannelEntry *entry; + if (Trace_notify) - elog(DEBUG1, "Exec_UnlistenAllCommit(%d)", MyProcPid); + elog(DEBUG1, "CleanupListenersOnExit(%d)", MyProcPid); - list_free_deep(listenChannels); - listenChannels = NIL; + /* Clear our local cache (not really necessary, but be consistent) */ + if (localChannelTable != NULL) + { + hash_destroy(localChannelTable); + localChannelTable = NULL; + } + + /* Now remove our entries from the shared globalChannelTable */ + if (globalChannelTable == NULL) + return; + + dshash_seq_init(&status, globalChannelTable, true); + while ((entry = dshash_seq_next(&status)) != NULL) + { + ListenerEntry *listeners; + + if (entry->key.dboid != MyDatabaseId) + continue; /* not relevant */ + + listeners = (ListenerEntry *) + dsa_get_address(globalChannelDSA, entry->listenersArray); + + for (int i = 0; i < entry->numListeners; i++) + { + if (listeners[i].procNo == MyProcNumber) + { + entry->numListeners--; + if (i < entry->numListeners) + memmove(&listeners[i], &listeners[i + 1], + sizeof(ListenerEntry) * (entry->numListeners - i)); + + if (entry->numListeners == 0) + { + dsa_free(globalChannelDSA, entry->listenersArray); + dshash_delete_current(&status); + } + break; + } + } + } + dshash_seq_term(&status); } /* * Test whether we are actively listening on the given channel name. * * Note: this function is executed for every notification found in the queue. - * Perhaps it is worth further optimization, eg convert the list to a sorted - * array so we can binary-search it. In practice the list is likely to be - * fairly short, though. */ static bool IsListeningOn(const char *channel) { - ListCell *p; - - foreach(p, listenChannels) - { - char *lchan = (char *) lfirst(p); + if (localChannelTable == NULL) + return false; - if (strcmp(lchan, channel) == 0) - return true; - } - return false; + return (hash_search(localChannelTable, channel, HASH_FIND, NULL) != NULL); } /* @@ -1230,7 +1915,7 @@ IsListeningOn(const char *channel) static void asyncQueueUnregister(void) { - Assert(listenChannels == NIL); /* else caller error */ + Assert(LocalChannelTableIsEmpty()); /* else caller error */ if (!amRegisteredListener) /* nothing to do */ return; @@ -1242,6 +1927,8 @@ asyncQueueUnregister(void) /* Mark our entry as invalid */ QUEUE_BACKEND_PID(MyProcNumber) = InvalidPid; QUEUE_BACKEND_DBOID(MyProcNumber) = InvalidOid; + QUEUE_BACKEND_WAKEUP_PENDING(MyProcNumber) = false; + QUEUE_BACKEND_IS_ADVANCING(MyProcNumber) = false; /* and remove it from the list */ if (QUEUE_FIRST_LISTENER == MyProcNumber) QUEUE_FIRST_LISTENER = QUEUE_NEXT_LISTENER(MyProcNumber); @@ -1389,8 +2076,7 @@ asyncQueueAddEntries(ListCell *nextNotify) if (QUEUE_POS_IS_ZERO(queue_head)) slotno = SimpleLruZeroPage(NotifyCtl, pageno); else - slotno = SimpleLruReadPage(NotifyCtl, pageno, true, - InvalidTransactionId); + slotno = SimpleLruReadPage(NotifyCtl, pageno, true, &queue_head); /* Note we mark the page dirty before writing in it */ NotifyCtl->shared->page_dirty[slotno] = true; @@ -1419,6 +2105,7 @@ asyncQueueAddEntries(ListCell *nextNotify) */ qe.length = QUEUE_PAGESIZE - offset; qe.dboid = InvalidOid; + qe.xid = InvalidTransactionId; qe.data[0] = '\0'; /* empty channel */ qe.data[1] = '\0'; /* empty payload */ } @@ -1565,11 +2252,9 @@ asyncQueueFillWarning(void) /* * Send signals to listening backends. * - * Normally we signal only backends in our own database, since only those - * backends could be interested in notifies we send. However, if there's - * notify traffic in our database but no traffic in another database that - * does have listener(s), those listeners will fall further and further - * behind. Waken them anyway if they're far enough behind, so that they'll + * Normally we signal only backends that are interested in the notifies that + * we just sent. However, that will leave idle listeners falling further and + * further behind. Waken them anyway if they're far enough behind, so they'll * advance their queue position pointers, allowing the global tail to advance. * * Since we know the ProcNumber and the Pid the signaling is quite cheap. @@ -1580,60 +2265,124 @@ asyncQueueFillWarning(void) static void SignalBackends(void) { - int32 *pids; - ProcNumber *procnos; int count; + /* Can't get here without PreCommit_Notify having made the global table */ + Assert(globalChannelTable != NULL); + + /* It should have set up these arrays, too */ + Assert(signalPids != NULL && signalProcnos != NULL); + /* * Identify backends that we need to signal. We don't want to send - * signals while holding the NotifyQueueLock, so this loop just builds a - * list of target PIDs. - * - * XXX in principle these pallocs could fail, which would be bad. Maybe - * preallocate the arrays? They're not that large, though. + * signals while holding the NotifyQueueLock, so this part just builds a + * list of target PIDs in signalPids[] and signalProcnos[]. */ - pids = (int32 *) palloc(MaxBackends * sizeof(int32)); - procnos = (ProcNumber *) palloc(MaxBackends * sizeof(ProcNumber)); count = 0; LWLockAcquire(NotifyQueueLock, LW_EXCLUSIVE); + + /* Scan each channel name that we notified in this transaction */ + foreach_ptr(char, channel, pendingNotifies->uniqueChannelNames) + { + GlobalChannelKey key; + GlobalChannelEntry *entry; + ListenerEntry *listeners; + + GlobalChannelKeyInit(&key, MyDatabaseId, channel); + entry = dshash_find(globalChannelTable, &key, false); + if (entry == NULL) + continue; /* nobody is listening */ + + listeners = (ListenerEntry *) dsa_get_address(globalChannelDSA, + entry->listenersArray); + + /* Identify listeners that now need waking, add them to arrays */ + for (int j = 0; j < entry->numListeners; j++) + { + ProcNumber i; + int32 pid; + QueuePosition pos; + + if (!listeners[j].listening) + continue; /* ignore not-yet-committed listeners */ + + i = listeners[j].procNo; + + if (QUEUE_BACKEND_WAKEUP_PENDING(i)) + continue; /* already signaled, no need to repeat */ + + pid = QUEUE_BACKEND_PID(i); + pos = QUEUE_BACKEND_POS(i); + + if (QUEUE_POS_EQUAL(pos, QUEUE_HEAD)) + continue; /* it's fully caught up already */ + + Assert(pid != InvalidPid); + + QUEUE_BACKEND_WAKEUP_PENDING(i) = true; + signalPids[count] = pid; + signalProcnos[count] = i; + count++; + } + + dshash_release_lock(globalChannelTable, entry); + } + + /* + * Scan all listeners. Any that are not already pending wakeup must not + * be interested in our notifications (else we'd have set their wakeup + * flags above). Check to see if we can directly advance their queue + * pointers to save a wakeup. Otherwise, if they are far behind, wake + * them anyway so they will catch up. + */ for (ProcNumber i = QUEUE_FIRST_LISTENER; i != INVALID_PROC_NUMBER; i = QUEUE_NEXT_LISTENER(i)) { - int32 pid = QUEUE_BACKEND_PID(i); + int32 pid; QueuePosition pos; - Assert(pid != InvalidPid); + if (QUEUE_BACKEND_WAKEUP_PENDING(i)) + continue; + + /* If it's currently advancing, we should not touch it */ + if (QUEUE_BACKEND_IS_ADVANCING(i)) + continue; + + pid = QUEUE_BACKEND_PID(i); pos = QUEUE_BACKEND_POS(i); - if (QUEUE_BACKEND_DBOID(i) == MyDatabaseId) + + /* + * We can directly advance the other backend's queue pointer if it's + * not currently advancing (else there are race conditions), and its + * current pointer is not behind queueHeadBeforeWrite (else we'd make + * it miss some older messages), and we'd not be moving the pointer + * backward. + */ + if (!QUEUE_POS_PRECEDES(pos, queueHeadBeforeWrite) && + QUEUE_POS_PRECEDES(pos, queueHeadAfterWrite)) { - /* - * Always signal listeners in our own database, unless they're - * already caught up (unlikely, but possible). - */ - if (QUEUE_POS_EQUAL(pos, QUEUE_HEAD)) - continue; + /* We can directly advance its pointer past what we wrote */ + QUEUE_BACKEND_POS(i) = queueHeadAfterWrite; } - else + else if (asyncQueuePageDiff(QUEUE_POS_PAGE(QUEUE_HEAD), + QUEUE_POS_PAGE(pos)) >= QUEUE_CLEANUP_DELAY) { - /* - * Listeners in other databases should be signaled only if they - * are far behind. - */ - if (asyncQueuePageDiff(QUEUE_POS_PAGE(QUEUE_HEAD), - QUEUE_POS_PAGE(pos)) < QUEUE_CLEANUP_DELAY) - continue; + /* It's idle and far behind, so wake it up */ + Assert(pid != InvalidPid); + + QUEUE_BACKEND_WAKEUP_PENDING(i) = true; + signalPids[count] = pid; + signalProcnos[count] = i; + count++; } - /* OK, need to signal this one */ - pids[count] = pid; - procnos[count] = i; - count++; } + LWLockRelease(NotifyQueueLock); /* Now send signals */ for (int i = 0; i < count; i++) { - int32 pid = pids[i]; + int32 pid = signalPids[i]; /* * If we are signaling our own process, no need to involve the kernel; @@ -1651,12 +2400,9 @@ SignalBackends(void) * NotifyQueueLock; which is unlikely but certainly possible. So we * just log a low-level debug message if it happens. */ - if (SendProcSignal(pid, PROCSIG_NOTIFY_INTERRUPT, procnos[i]) < 0) + if (SendProcSignal(pid, PROCSIG_NOTIFY_INTERRUPT, signalProcnos[i]) < 0) elog(DEBUG3, "could not signal backend with PID %d: %m", pid); } - - pfree(pids); - pfree(procnos); } /* @@ -1664,18 +2410,18 @@ SignalBackends(void) * * This is called at transaction abort. * - * Gets rid of pending actions and outbound notifies that we would have - * executed if the transaction got committed. + * Revert any staged listen/unlisten changes and clean up transaction state. + * This only does anything if we abort after PreCommit_Notify has staged + * some entries. */ void AtAbort_Notify(void) { - /* - * If we LISTEN but then roll back the transaction after PreCommit_Notify, - * we have registered as a listener but have not made any entry in - * listenChannels. In that case, deregister again. - */ - if (amRegisteredListener && listenChannels == NIL) + /* Revert staged listen/unlisten changes */ + ApplyPendingListenActions(false); + + /* If we're no longer listening on anything, unregister */ + if (amRegisteredListener && LocalChannelTableIsEmpty()) asyncQueueUnregister(); /* And clean up */ @@ -1811,8 +2557,7 @@ HandleNotifyInterrupt(void) /* signal that work needs to be done */ notifyInterruptPending = true; - /* make sure the event is processed in due course */ - SetLatch(MyLatch); + /* latch will be set by procsignal_sigusr1_handler */ } /* @@ -1850,31 +2595,31 @@ ProcessNotifyInterrupt(bool flush) static void asyncQueueReadAllNotifications(void) { - volatile QueuePosition pos; + QueuePosition pos; QueuePosition head; Snapshot snapshot; - /* page_buffer must be adequately aligned, so use a union */ - union - { - char buf[QUEUE_PAGESIZE]; - AsyncQueueEntry align; - } page_buffer; - - /* Fetch current state */ + /* + * Fetch current state, indicate to others that we have woken up, and that + * we are in process of advancing our position. + */ LWLockAcquire(NotifyQueueLock, LW_SHARED); /* Assert checks that we have a valid state entry */ Assert(MyProcPid == QUEUE_BACKEND_PID(MyProcNumber)); + QUEUE_BACKEND_WAKEUP_PENDING(MyProcNumber) = false; pos = QUEUE_BACKEND_POS(MyProcNumber); head = QUEUE_HEAD; - LWLockRelease(NotifyQueueLock); if (QUEUE_POS_EQUAL(pos, head)) { /* Nothing to do, we have read all notifications already. */ + LWLockRelease(NotifyQueueLock); return; } + QUEUE_BACKEND_IS_ADVANCING(MyProcNumber) = true; + LWLockRelease(NotifyQueueLock); + /*---------- * Get snapshot we'll use to decide which xacts are still in progress. * This is trickier than it might seem, because of race conditions. @@ -1909,7 +2654,7 @@ asyncQueueReadAllNotifications(void) * * What we do guarantee is that we'll see all notifications from * transactions committing after the snapshot we take here. - * Exec_ListenPreCommit has already added us to the listener array, + * BecomeRegisteredListener has already added us to the listener array, * so no not-yet-committed messages can be removed from the queue * before we see them. *---------- @@ -1920,49 +2665,27 @@ asyncQueueReadAllNotifications(void) * It is possible that we fail while trying to send a message to our * frontend (for example, because of encoding conversion failure). If * that happens it is critical that we not try to send the same message - * over and over again. Therefore, we place a PG_TRY block here that will - * forcibly advance our queue position before we lose control to an error. - * (We could alternatively retake NotifyQueueLock and move the position - * before handling each individual message, but that seems like too much - * lock traffic.) + * over and over again. Therefore, we set ExitOnAnyError to upgrade any + * ERRORs to FATAL, causing the client connection to be closed on error. + * + * We used to only skip over the offending message and try to soldier on, + * but it was somewhat questionable to lose a notification and give the + * client an ERROR instead. A client application is not be prepared for + * that and can't tell that a notification was missed. It was also not + * very useful in practice because notifications are often processed while + * a connection is idle and reading a message from the client, and in that + * state, any error is upgraded to FATAL anyway. Closing the connection + * is a clear signal to the application that it might have missed + * notifications. */ - PG_TRY(); { + bool save_ExitOnAnyError = ExitOnAnyError; bool reachedStop; + ExitOnAnyError = true; + do { - int64 curpage = QUEUE_POS_PAGE(pos); - int curoffset = QUEUE_POS_OFFSET(pos); - int slotno; - int copysize; - - /* - * We copy the data from SLRU into a local buffer, so as to avoid - * holding the SLRU lock while we are examining the entries and - * possibly transmitting them to our frontend. Copy only the part - * of the page we will actually inspect. - */ - slotno = SimpleLruReadPage_ReadOnly(NotifyCtl, curpage, - InvalidTransactionId); - if (curpage == QUEUE_POS_PAGE(head)) - { - /* we only want to read as far as head */ - copysize = QUEUE_POS_OFFSET(head) - curoffset; - if (copysize < 0) - copysize = 0; /* just for safety */ - } - else - { - /* fetch all the rest of the page */ - copysize = QUEUE_PAGESIZE - curoffset; - } - memcpy(page_buffer.buf + curoffset, - NotifyCtl->shared->page_buffer[slotno] + curoffset, - copysize); - /* Release lock that we got from SimpleLruReadPage_ReadOnly() */ - LWLockRelease(SimpleLruGetBankLock(NotifyCtl, curpage)); - /* * Process messages up to the stop position, end of page, or an * uncommitted message. @@ -1978,19 +2701,17 @@ asyncQueueReadAllNotifications(void) * rewrite pages under us. Especially we don't want to hold a lock * while sending the notifications to the frontend. */ - reachedStop = asyncQueueProcessPageEntries(&pos, head, - page_buffer.buf, - snapshot); + reachedStop = asyncQueueProcessPageEntries(&pos, head, snapshot); } while (!reachedStop); - } - PG_FINALLY(); - { + /* Update shared state */ LWLockAcquire(NotifyQueueLock, LW_SHARED); QUEUE_BACKEND_POS(MyProcNumber) = pos; + QUEUE_BACKEND_IS_ADVANCING(MyProcNumber) = false; LWLockRelease(NotifyQueueLock); + + ExitOnAnyError = save_ExitOnAnyError; } - PG_END_TRY(); /* Done with snapshot */ UnregisterSnapshot(snapshot); @@ -2000,31 +2721,37 @@ asyncQueueReadAllNotifications(void) * Fetch notifications from the shared queue, beginning at position current, * and deliver relevant ones to my frontend. * - * The current page must have been fetched into page_buffer from shared - * memory. (We could access the page right in shared memory, but that - * would imply holding the SLRU bank lock throughout this routine.) - * - * We stop if we reach the "stop" position, or reach a notification from an - * uncommitted transaction, or reach the end of the page. - * * The function returns true once we have reached the stop position or an * uncommitted notification, and false if we have finished with the page. * In other words: once it returns true there is no need to look further. * The QueuePosition *current is advanced past all processed messages. */ static bool -asyncQueueProcessPageEntries(volatile QueuePosition *current, +asyncQueueProcessPageEntries(QueuePosition *current, QueuePosition stop, - char *page_buffer, Snapshot snapshot) { + int64 curpage = QUEUE_POS_PAGE(*current); + int slotno; + char *page_buffer; bool reachedStop = false; bool reachedEndOfPage; - AsyncQueueEntry *qe; + + /* + * We copy the entries into a local buffer to avoid holding the SLRU lock + * while we transmit them to our frontend. The local buffer must be + * adequately aligned. + */ + alignas(AsyncQueueEntry) char local_buf[QUEUE_PAGESIZE]; + char *local_buf_end = local_buf; + + slotno = SimpleLruReadPage_ReadOnly(NotifyCtl, curpage, current); + page_buffer = NotifyCtl->shared->page_buffer[slotno]; do { QueuePosition thisentry = *current; + AsyncQueueEntry *qe; if (QUEUE_POS_EQUAL(thisentry, stop)) break; @@ -2066,18 +2793,25 @@ asyncQueueProcessPageEntries(volatile QueuePosition *current, reachedStop = true; break; } - else if (TransactionIdDidCommit(qe->xid)) - { - /* qe->data is the null-terminated channel name */ - char *channel = qe->data; - if (IsListeningOn(channel)) - { - /* payload follows channel name */ - char *payload = qe->data + strlen(channel) + 1; + /* + * Quick check for the case that we're not listening on any + * channels, before calling TransactionIdDidCommit(). This makes + * that case a little faster, but more importantly, it ensures + * that if there's a bad entry in the queue for which + * TransactionIdDidCommit() fails for some reason, we can skip + * over it on the first LISTEN in a session, and not get stuck on + * it indefinitely. (This is a little trickier than it looks: it + * works because BecomeRegisteredListener runs this code before we + * have made the first entry in localChannelTable.) + */ + if (LocalChannelTableIsEmpty()) + continue; - NotifyMyFrontEnd(channel, payload, qe->srcPid); - } + if (TransactionIdDidCommit(qe->xid)) + { + memcpy(local_buf_end, qe, qe->length); + local_buf_end += qe->length; } else { @@ -2091,6 +2825,32 @@ asyncQueueProcessPageEntries(volatile QueuePosition *current, /* Loop back if we're not at end of page */ } while (!reachedEndOfPage); + /* Release lock that we got from SimpleLruReadPage_ReadOnly() */ + LWLockRelease(SimpleLruGetBankLock(NotifyCtl, curpage)); + + /* + * Now that we have let go of the SLRU bank lock, send the notifications + * to our backend + */ + Assert(local_buf_end - local_buf <= BLCKSZ); + for (char *p = local_buf; p < local_buf_end;) + { + AsyncQueueEntry *qe = (AsyncQueueEntry *) p; + + /* qe->data is the null-terminated channel name */ + char *channel = qe->data; + + if (IsListeningOn(channel)) + { + /* payload follows channel name */ + char *payload = qe->data + strlen(channel) + 1; + + NotifyMyFrontEnd(channel, payload, qe->srcPid); + } + + p += qe->length; + } + if (QUEUE_POS_EQUAL(*current, stop)) reachedStop = true; @@ -2168,6 +2928,119 @@ asyncQueueAdvanceTail(void) LWLockRelease(NotifyQueueTailLock); } +/* + * AsyncNotifyFreezeXids + * + * Prepare the async notification queue for CLOG truncation by freezing + * transaction IDs that are about to become inaccessible. + * + * This function is called by VACUUM before advancing datfrozenxid. It scans + * the notification queue and replaces XIDs that would become inaccessible + * after CLOG truncation with special markers: + * - Committed transactions are set to FrozenTransactionId + * - Aborted/crashed transactions are set to InvalidTransactionId + * + * Only XIDs < newFrozenXid are processed, as those are the ones whose CLOG + * pages will be truncated. If XID < newFrozenXid, it cannot still be running + * (or it would have held back newFrozenXid through ProcArray). + * Therefore, if TransactionIdDidCommit returns false, we know the transaction + * either aborted explicitly or crashed, and we can safely mark it invalid. + */ +void +AsyncNotifyFreezeXids(TransactionId newFrozenXid) +{ + QueuePosition pos; + QueuePosition head; + int64 curpage = -1; + int slotno = -1; + char *page_buffer = NULL; + bool page_dirty = false; + + /* + * Acquire locks in the correct order to avoid deadlocks. As per the + * locking protocol: NotifyQueueTailLock, then NotifyQueueLock, then SLRU + * bank locks. + * + * We only need SHARED mode since we're just reading the head/tail + * positions, not modifying them. + */ + LWLockAcquire(NotifyQueueTailLock, LW_SHARED); + LWLockAcquire(NotifyQueueLock, LW_SHARED); + + pos = QUEUE_TAIL; + head = QUEUE_HEAD; + + /* Release NotifyQueueLock early, we only needed to read the positions */ + LWLockRelease(NotifyQueueLock); + + /* + * Scan the queue from tail to head, freezing XIDs as needed. We hold + * NotifyQueueTailLock throughout to ensure the tail doesn't move while + * we're working. + */ + while (!QUEUE_POS_EQUAL(pos, head)) + { + AsyncQueueEntry *qe; + TransactionId xid; + int64 pageno = QUEUE_POS_PAGE(pos); + int offset = QUEUE_POS_OFFSET(pos); + + /* If we need a different page, release old lock and get new one */ + if (pageno != curpage) + { + LWLock *lock; + + /* Release previous page if any */ + if (slotno >= 0) + { + if (page_dirty) + { + NotifyCtl->shared->page_dirty[slotno] = true; + page_dirty = false; + } + LWLockRelease(SimpleLruGetBankLock(NotifyCtl, curpage)); + } + + lock = SimpleLruGetBankLock(NotifyCtl, pageno); + LWLockAcquire(lock, LW_EXCLUSIVE); + slotno = SimpleLruReadPage(NotifyCtl, pageno, true, &pos); + page_buffer = NotifyCtl->shared->page_buffer[slotno]; + curpage = pageno; + } + + qe = (AsyncQueueEntry *) (page_buffer + offset); + xid = qe->xid; + + if (TransactionIdIsNormal(xid) && + TransactionIdPrecedes(xid, newFrozenXid)) + { + if (TransactionIdDidCommit(xid)) + { + qe->xid = FrozenTransactionId; + page_dirty = true; + } + else + { + qe->xid = InvalidTransactionId; + page_dirty = true; + } + } + + /* Advance to next entry */ + asyncQueueAdvance(&pos, qe->length); + } + + /* Release final page lock if we acquired one */ + if (slotno >= 0) + { + if (page_dirty) + NotifyCtl->shared->page_dirty[slotno] = true; + LWLockRelease(SimpleLruGetBankLock(NotifyCtl, curpage)); + } + + LWLockRelease(NotifyQueueTailLock); +} + /* * ProcessIncomingNotify * @@ -2186,7 +3059,7 @@ ProcessIncomingNotify(bool flush) notifyInterruptPending = false; /* Do nothing else if we aren't actively listening */ - if (listenChannels == NIL) + if (LocalChannelTableIsEmpty()) return; if (Trace_notify) @@ -2290,7 +3163,7 @@ AddEventToPendingNotifies(Notification *n) { Assert(pendingNotifies->events != NIL); - /* Create the hash table if it's time to */ + /* Create the hash tables if it's time to */ if (list_length(pendingNotifies->events) >= MIN_HASHABLE_NOTIFIES && pendingNotifies->hashtab == NULL) { @@ -2309,10 +3182,22 @@ AddEventToPendingNotifies(Notification *n) &hash_ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_CONTEXT); + /* Create the unique channel name table */ + Assert(pendingNotifies->uniqueChannelHash == NULL); + hash_ctl.keysize = NAMEDATALEN; + hash_ctl.entrysize = sizeof(ChannelName); + hash_ctl.hcxt = CurTransactionContext; + pendingNotifies->uniqueChannelHash = + hash_create("Pending Notify Channel Names", + 64L, + &hash_ctl, + HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); + /* Insert all the already-existing events */ foreach(l, pendingNotifies->events) { Notification *oldn = (Notification *) lfirst(l); + char *channel = oldn->data; bool found; (void) hash_search(pendingNotifies->hashtab, @@ -2320,15 +3205,22 @@ AddEventToPendingNotifies(Notification *n) HASH_ENTER, &found); Assert(!found); + + /* Add channel name to uniqueChannelHash; might be there already */ + (void) hash_search(pendingNotifies->uniqueChannelHash, + channel, + HASH_ENTER, + NULL); } } /* Add new event to the list, in order */ pendingNotifies->events = lappend(pendingNotifies->events, n); - /* Add event to the hash table if needed */ + /* Add event to the hash tables if needed */ if (pendingNotifies->hashtab != NULL) { + char *channel = n->data; bool found; (void) hash_search(pendingNotifies->hashtab, @@ -2336,6 +3228,12 @@ AddEventToPendingNotifies(Notification *n) HASH_ENTER, &found); Assert(!found); + + /* Add channel name to uniqueChannelHash; might be there already */ + (void) hash_search(pendingNotifies->uniqueChannelHash, + channel, + HASH_ENTER, + NULL); } } @@ -2385,6 +3283,8 @@ ClearPendingActionsAndNotifies(void) */ pendingActions = NULL; pendingNotifies = NULL; + /* Also clear pendingListenActions, which is derived from pendingActions */ + pendingListenActions = NULL; } /* diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c deleted file mode 100644 index 54a08e4102e14..0000000000000 --- a/src/backend/commands/cluster.c +++ /dev/null @@ -1,1754 +0,0 @@ -/*------------------------------------------------------------------------- - * - * cluster.c - * CLUSTER a table on an index. This is now also used for VACUUM FULL. - * - * There is hardly anything left of Paul Brown's original implementation... - * - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994-5, Regents of the University of California - * - * - * IDENTIFICATION - * src/backend/commands/cluster.c - * - *------------------------------------------------------------------------- - */ -#include "postgres.h" - -#include "access/amapi.h" -#include "access/heapam.h" -#include "access/multixact.h" -#include "access/relscan.h" -#include "access/tableam.h" -#include "access/toast_internals.h" -#include "access/transam.h" -#include "access/xact.h" -#include "catalog/catalog.h" -#include "catalog/dependency.h" -#include "catalog/heap.h" -#include "catalog/index.h" -#include "catalog/namespace.h" -#include "catalog/objectaccess.h" -#include "catalog/pg_am.h" -#include "catalog/pg_inherits.h" -#include "catalog/toasting.h" -#include "commands/cluster.h" -#include "commands/defrem.h" -#include "commands/progress.h" -#include "commands/tablecmds.h" -#include "commands/vacuum.h" -#include "miscadmin.h" -#include "optimizer/optimizer.h" -#include "pgstat.h" -#include "storage/bufmgr.h" -#include "storage/lmgr.h" -#include "storage/predicate.h" -#include "utils/acl.h" -#include "utils/fmgroids.h" -#include "utils/guc.h" -#include "utils/inval.h" -#include "utils/lsyscache.h" -#include "utils/memutils.h" -#include "utils/pg_rusage.h" -#include "utils/relmapper.h" -#include "utils/snapmgr.h" -#include "utils/syscache.h" - -/* - * This struct is used to pass around the information on tables to be - * clustered. We need this so we can make a list of them when invoked without - * a specific table/index pair. - */ -typedef struct -{ - Oid tableOid; - Oid indexOid; -} RelToCluster; - - -static void cluster_multiple_rels(List *rtcs, ClusterParams *params); -static void rebuild_relation(Relation OldHeap, Relation index, bool verbose); -static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, - bool verbose, bool *pSwapToastByContent, - TransactionId *pFreezeXid, MultiXactId *pCutoffMulti); -static List *get_tables_to_cluster(MemoryContext cluster_context); -static List *get_tables_to_cluster_partitioned(MemoryContext cluster_context, - Oid indexOid); -static bool cluster_is_permitted_for_relation(Oid relid, Oid userid); - - -/*--------------------------------------------------------------------------- - * This cluster code allows for clustering multiple tables at once. Because - * of this, we cannot just run everything on a single transaction, or we - * would be forced to acquire exclusive locks on all the tables being - * clustered, simultaneously --- very likely leading to deadlock. - * - * To solve this we follow a similar strategy to VACUUM code, - * clustering each relation in a separate transaction. For this to work, - * we need to: - * - provide a separate memory context so that we can pass information in - * a way that survives across transactions - * - start a new transaction every time a new relation is clustered - * - check for validity of the information on to-be-clustered relations, - * as someone might have deleted a relation behind our back, or - * clustered one on a different index - * - end the transaction - * - * The single-relation case does not have any such overhead. - * - * We also allow a relation to be specified without index. In that case, - * the indisclustered bit will be looked up, and an ERROR will be thrown - * if there is no index with the bit set. - *--------------------------------------------------------------------------- - */ -void -cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel) -{ - ListCell *lc; - ClusterParams params = {0}; - bool verbose = false; - Relation rel = NULL; - Oid indexOid = InvalidOid; - MemoryContext cluster_context; - List *rtcs; - - /* Parse option list */ - foreach(lc, stmt->params) - { - DefElem *opt = (DefElem *) lfirst(lc); - - if (strcmp(opt->defname, "verbose") == 0) - verbose = defGetBoolean(opt); - else - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized CLUSTER option \"%s\"", - opt->defname), - parser_errposition(pstate, opt->location))); - } - - params.options = (verbose ? CLUOPT_VERBOSE : 0); - - if (stmt->relation != NULL) - { - /* This is the single-relation case. */ - Oid tableOid; - - /* - * Find, lock, and check permissions on the table. We obtain - * AccessExclusiveLock right away to avoid lock-upgrade hazard in the - * single-transaction case. - */ - tableOid = RangeVarGetRelidExtended(stmt->relation, - AccessExclusiveLock, - 0, - RangeVarCallbackMaintainsTable, - NULL); - rel = table_open(tableOid, NoLock); - - /* - * Reject clustering a remote temp table ... their local buffer - * manager is not going to cope. - */ - if (RELATION_IS_OTHER_TEMP(rel)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster temporary tables of other sessions"))); - - if (stmt->indexname == NULL) - { - ListCell *index; - - /* We need to find the index that has indisclustered set. */ - foreach(index, RelationGetIndexList(rel)) - { - indexOid = lfirst_oid(index); - if (get_index_isclustered(indexOid)) - break; - indexOid = InvalidOid; - } - - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("there is no previously clustered index for table \"%s\"", - stmt->relation->relname))); - } - else - { - /* - * The index is expected to be in the same namespace as the - * relation. - */ - indexOid = get_relname_relid(stmt->indexname, - rel->rd_rel->relnamespace); - if (!OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("index \"%s\" for table \"%s\" does not exist", - stmt->indexname, stmt->relation->relname))); - } - - /* For non-partitioned tables, do what we came here to do. */ - if (rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) - { - cluster_rel(rel, indexOid, ¶ms); - /* cluster_rel closes the relation, but keeps lock */ - - return; - } - } - - /* - * By here, we know we are in a multi-table situation. In order to avoid - * holding locks for too long, we want to process each table in its own - * transaction. This forces us to disallow running inside a user - * transaction block. - */ - PreventInTransactionBlock(isTopLevel, "CLUSTER"); - - /* Also, we need a memory context to hold our list of relations */ - cluster_context = AllocSetContextCreate(PortalContext, - "Cluster", - ALLOCSET_DEFAULT_SIZES); - - /* - * Either we're processing a partitioned table, or we were not given any - * table name at all. In either case, obtain a list of relations to - * process. - * - * In the former case, an index name must have been given, so we don't - * need to recheck its "indisclustered" bit, but we have to check that it - * is an index that we can cluster on. In the latter case, we set the - * option bit to have indisclustered verified. - * - * Rechecking the relation itself is necessary here in all cases. - */ - params.options |= CLUOPT_RECHECK; - if (rel != NULL) - { - Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); - check_index_is_clusterable(rel, indexOid, AccessShareLock); - rtcs = get_tables_to_cluster_partitioned(cluster_context, indexOid); - - /* close relation, releasing lock on parent table */ - table_close(rel, AccessExclusiveLock); - } - else - { - rtcs = get_tables_to_cluster(cluster_context); - params.options |= CLUOPT_RECHECK_ISCLUSTERED; - } - - /* Do the job. */ - cluster_multiple_rels(rtcs, ¶ms); - - /* Start a new transaction for the cleanup work. */ - StartTransactionCommand(); - - /* Clean up working storage */ - MemoryContextDelete(cluster_context); -} - -/* - * Given a list of relations to cluster, process each of them in a separate - * transaction. - * - * We expect to be in a transaction at start, but there isn't one when we - * return. - */ -static void -cluster_multiple_rels(List *rtcs, ClusterParams *params) -{ - ListCell *lc; - - /* Commit to get out of starting transaction */ - PopActiveSnapshot(); - CommitTransactionCommand(); - - /* Cluster the tables, each in a separate transaction */ - foreach(lc, rtcs) - { - RelToCluster *rtc = (RelToCluster *) lfirst(lc); - Relation rel; - - /* Start a new transaction for each relation. */ - StartTransactionCommand(); - - /* functions in indexes may want a snapshot set */ - PushActiveSnapshot(GetTransactionSnapshot()); - - rel = table_open(rtc->tableOid, AccessExclusiveLock); - - /* Process this table */ - cluster_rel(rel, rtc->indexOid, params); - /* cluster_rel closes the relation, but keeps lock */ - - PopActiveSnapshot(); - CommitTransactionCommand(); - } -} - -/* - * cluster_rel - * - * This clusters the table by creating a new, clustered table and - * swapping the relfilenumbers of the new table and the old table, so - * the OID of the original table is preserved. Thus we do not lose - * GRANT, inheritance nor references to this table. - * - * Indexes are rebuilt too, via REINDEX. Since we are effectively bulk-loading - * the new table, it's better to create the indexes afterwards than to fill - * them incrementally while we load the table. - * - * If indexOid is InvalidOid, the table will be rewritten in physical order - * instead of index order. This is the new implementation of VACUUM FULL, - * and error messages should refer to the operation as VACUUM not CLUSTER. - */ -void -cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params) -{ - Oid tableOid = RelationGetRelid(OldHeap); - Oid save_userid; - int save_sec_context; - int save_nestlevel; - bool verbose = ((params->options & CLUOPT_VERBOSE) != 0); - bool recheck = ((params->options & CLUOPT_RECHECK) != 0); - Relation index; - - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false)); - - /* Check for user-requested abort. */ - CHECK_FOR_INTERRUPTS(); - - pgstat_progress_start_command(PROGRESS_COMMAND_CLUSTER, tableOid); - if (OidIsValid(indexOid)) - pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, - PROGRESS_CLUSTER_COMMAND_CLUSTER); - else - pgstat_progress_update_param(PROGRESS_CLUSTER_COMMAND, - PROGRESS_CLUSTER_COMMAND_VACUUM_FULL); - - /* - * Switch to the table owner's userid, so that any index functions are run - * as that user. Also lock down security-restricted operations and - * arrange to make GUC variable changes local to this command. - */ - GetUserIdAndSecContext(&save_userid, &save_sec_context); - SetUserIdAndSecContext(OldHeap->rd_rel->relowner, - save_sec_context | SECURITY_RESTRICTED_OPERATION); - save_nestlevel = NewGUCNestLevel(); - RestrictSearchPath(); - - /* - * Since we may open a new transaction for each relation, we have to check - * that the relation still is what we think it is. - * - * If this is a single-transaction CLUSTER, we can skip these tests. We - * *must* skip the one on indisclustered since it would reject an attempt - * to cluster a not-previously-clustered index. - */ - if (recheck) - { - /* Check that the user still has privileges for the relation */ - if (!cluster_is_permitted_for_relation(tableOid, save_userid)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - /* - * Silently skip a temp table for a remote session. Only doing this - * check in the "recheck" case is appropriate (which currently means - * somebody is executing a database-wide CLUSTER or on a partitioned - * table), because there is another check in cluster() which will stop - * any attempt to cluster remote temp tables by name. There is - * another check in cluster_rel which is redundant, but we leave it - * for extra safety. - */ - if (RELATION_IS_OTHER_TEMP(OldHeap)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - if (OidIsValid(indexOid)) - { - /* - * Check that the index still exists - */ - if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - /* - * Check that the index is still the one with indisclustered set, - * if needed. - */ - if ((params->options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && - !get_index_isclustered(indexOid)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - } - } - - /* - * We allow VACUUM FULL, but not CLUSTER, on shared catalogs. CLUSTER - * would work in most respects, but the index would only get marked as - * indisclustered in the current database, leading to unexpected behavior - * if CLUSTER were later invoked in another database. - */ - if (OidIsValid(indexOid) && OldHeap->rd_rel->relisshared) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster a shared catalog"))); - - /* - * Don't process temp tables of other backends ... their local buffer - * manager is not going to cope. - */ - if (RELATION_IS_OTHER_TEMP(OldHeap)) - { - if (OidIsValid(indexOid)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster temporary tables of other sessions"))); - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot vacuum temporary tables of other sessions"))); - } - - /* - * Also check for active uses of the relation in the current transaction, - * including open scans and pending AFTER trigger events. - */ - CheckTableNotInUse(OldHeap, OidIsValid(indexOid) ? "CLUSTER" : "VACUUM"); - - /* Check heap and index are valid to cluster on */ - if (OidIsValid(indexOid)) - { - /* verify the index is good and lock it */ - check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock); - /* also open it */ - index = index_open(indexOid, NoLock); - } - else - index = NULL; - - /* - * Quietly ignore the request if this is a materialized view which has not - * been populated from its query. No harm is done because there is no data - * to deal with, and we don't want to throw an error if this is part of a - * multi-relation request -- for example, CLUSTER was run on the entire - * database. - */ - if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW && - !RelationIsPopulated(OldHeap)) - { - relation_close(OldHeap, AccessExclusiveLock); - goto out; - } - - Assert(OldHeap->rd_rel->relkind == RELKIND_RELATION || - OldHeap->rd_rel->relkind == RELKIND_MATVIEW || - OldHeap->rd_rel->relkind == RELKIND_TOASTVALUE); - - /* - * All predicate locks on the tuples or pages are about to be made - * invalid, because we move tuples around. Promote them to relation - * locks. Predicate locks on indexes will be promoted when they are - * reindexed. - */ - TransferPredicateLocksToHeapRelation(OldHeap); - - /* rebuild_relation does all the dirty work */ - rebuild_relation(OldHeap, index, verbose); - /* rebuild_relation closes OldHeap, and index if valid */ - -out: - /* Roll back any GUC changes executed by index functions */ - AtEOXact_GUC(false, save_nestlevel); - - /* Restore userid and security context */ - SetUserIdAndSecContext(save_userid, save_sec_context); - - pgstat_progress_end_command(); -} - -/* - * Verify that the specified heap and index are valid to cluster on - * - * Side effect: obtains lock on the index. The caller may - * in some cases already have AccessExclusiveLock on the table, but - * not in all cases so we can't rely on the table-level lock for - * protection here. - */ -void -check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode) -{ - Relation OldIndex; - - OldIndex = index_open(indexOid, lockmode); - - /* - * Check that index is in fact an index on the given relation - */ - if (OldIndex->rd_index == NULL || - OldIndex->rd_index->indrelid != RelationGetRelid(OldHeap)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not an index for table \"%s\"", - RelationGetRelationName(OldIndex), - RelationGetRelationName(OldHeap)))); - - /* Index AM must allow clustering */ - if (!OldIndex->rd_indam->amclusterable) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster on index \"%s\" because access method does not support clustering", - RelationGetRelationName(OldIndex)))); - - /* - * Disallow clustering on incomplete indexes (those that might not index - * every row of the relation). We could relax this by making a separate - * seqscan pass over the table to copy the missing rows, but that seems - * expensive and tedious. - */ - if (!heap_attisnull(OldIndex->rd_indextuple, Anum_pg_index_indpred, NULL)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster on partial index \"%s\"", - RelationGetRelationName(OldIndex)))); - - /* - * Disallow if index is left over from a failed CREATE INDEX CONCURRENTLY; - * it might well not contain entries for every heap row, or might not even - * be internally consistent. (But note that we don't check indcheckxmin; - * the worst consequence of following broken HOT chains would be that we - * might put recently-dead tuples out-of-order in the new table, and there - * is little harm in that.) - */ - if (!OldIndex->rd_index->indisvalid) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot cluster on invalid index \"%s\"", - RelationGetRelationName(OldIndex)))); - - /* Drop relcache refcnt on OldIndex, but keep lock */ - index_close(OldIndex, NoLock); -} - -/* - * mark_index_clustered: mark the specified index as the one clustered on - * - * With indexOid == InvalidOid, will mark all indexes of rel not-clustered. - */ -void -mark_index_clustered(Relation rel, Oid indexOid, bool is_internal) -{ - HeapTuple indexTuple; - Form_pg_index indexForm; - Relation pg_index; - ListCell *index; - - /* Disallow applying to a partitioned table */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot mark index clustered in partitioned table"))); - - /* - * If the index is already marked clustered, no need to do anything. - */ - if (OidIsValid(indexOid)) - { - if (get_index_isclustered(indexOid)) - return; - } - - /* - * Check each index of the relation and set/clear the bit as needed. - */ - pg_index = table_open(IndexRelationId, RowExclusiveLock); - - foreach(index, RelationGetIndexList(rel)) - { - Oid thisIndexOid = lfirst_oid(index); - - indexTuple = SearchSysCacheCopy1(INDEXRELID, - ObjectIdGetDatum(thisIndexOid)); - if (!HeapTupleIsValid(indexTuple)) - elog(ERROR, "cache lookup failed for index %u", thisIndexOid); - indexForm = (Form_pg_index) GETSTRUCT(indexTuple); - - /* - * Unset the bit if set. We know it's wrong because we checked this - * earlier. - */ - if (indexForm->indisclustered) - { - indexForm->indisclustered = false; - CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); - } - else if (thisIndexOid == indexOid) - { - /* this was checked earlier, but let's be real sure */ - if (!indexForm->indisvalid) - elog(ERROR, "cannot cluster on invalid index %u", indexOid); - indexForm->indisclustered = true; - CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); - } - - InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0, - InvalidOid, is_internal); - - heap_freetuple(indexTuple); - } - - table_close(pg_index, RowExclusiveLock); -} - -/* - * rebuild_relation: rebuild an existing relation in index or physical order - * - * OldHeap: table to rebuild. - * index: index to cluster by, or NULL to rewrite in physical order. - * - * On entry, heap and index (if one is given) must be open, and - * AccessExclusiveLock held on them. - * On exit, they are closed, but locks on them are not released. - */ -static void -rebuild_relation(Relation OldHeap, Relation index, bool verbose) -{ - Oid tableOid = RelationGetRelid(OldHeap); - Oid accessMethod = OldHeap->rd_rel->relam; - Oid tableSpace = OldHeap->rd_rel->reltablespace; - Oid OIDNewHeap; - Relation NewHeap; - char relpersistence; - bool is_system_catalog; - bool swap_toast_by_content; - TransactionId frozenXid; - MultiXactId cutoffMulti; - - Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) && - (index == NULL || CheckRelationLockedByMe(index, AccessExclusiveLock, false))); - - if (index) - /* Mark the correct index as clustered */ - mark_index_clustered(OldHeap, RelationGetRelid(index), true); - - /* Remember info about rel before closing OldHeap */ - relpersistence = OldHeap->rd_rel->relpersistence; - is_system_catalog = IsSystemRelation(OldHeap); - - /* - * Create the transient table that will receive the re-ordered data. - * - * OldHeap is already locked, so no need to lock it again. make_new_heap - * obtains AccessExclusiveLock on the new heap and its toast table. - */ - OIDNewHeap = make_new_heap(tableOid, tableSpace, - accessMethod, - relpersistence, - NoLock); - Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); - NewHeap = table_open(OIDNewHeap, NoLock); - - /* Copy the heap data into the new table in the desired order */ - copy_table_data(NewHeap, OldHeap, index, verbose, - &swap_toast_by_content, &frozenXid, &cutoffMulti); - - - /* Close relcache entries, but keep lock until transaction commit */ - table_close(OldHeap, NoLock); - if (index) - index_close(index, NoLock); - - /* - * Close the new relation so it can be dropped as soon as the storage is - * swapped. The relation is not visible to others, so no need to unlock it - * explicitly. - */ - table_close(NewHeap, NoLock); - - /* - * Swap the physical files of the target and transient tables, then - * rebuild the target's indexes and throw away the transient table. - */ - finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog, - swap_toast_by_content, false, true, - frozenXid, cutoffMulti, - relpersistence); -} - - -/* - * Create the transient table that will be filled with new data during - * CLUSTER, ALTER TABLE, and similar operations. The transient table - * duplicates the logical structure of the OldHeap; but will have the - * specified physical storage properties NewTableSpace, NewAccessMethod, and - * relpersistence. - * - * After this, the caller should load the new heap with transferred/modified - * data, then call finish_heap_swap to complete the operation. - */ -Oid -make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, - char relpersistence, LOCKMODE lockmode) -{ - TupleDesc OldHeapDesc; - char NewHeapName[NAMEDATALEN]; - Oid OIDNewHeap; - Oid toastid; - Relation OldHeap; - HeapTuple tuple; - Datum reloptions; - bool isNull; - Oid namespaceid; - - OldHeap = table_open(OIDOldHeap, lockmode); - OldHeapDesc = RelationGetDescr(OldHeap); - - /* - * Note that the NewHeap will not receive any of the defaults or - * constraints associated with the OldHeap; we don't need 'em, and there's - * no reason to spend cycles inserting them into the catalogs only to - * delete them. - */ - - /* - * But we do want to use reloptions of the old heap for new heap. - */ - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(OIDOldHeap)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", OIDOldHeap); - reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isNull); - if (isNull) - reloptions = (Datum) 0; - - if (relpersistence == RELPERSISTENCE_TEMP) - namespaceid = LookupCreationNamespace("pg_temp"); - else - namespaceid = RelationGetNamespace(OldHeap); - - /* - * Create the new heap, using a temporary name in the same namespace as - * the existing table. NOTE: there is some risk of collision with user - * relnames. Working around this seems more trouble than it's worth; in - * particular, we can't create the new heap in a different namespace from - * the old, or we will have problems with the TEMP status of temp tables. - * - * Note: the new heap is not a shared relation, even if we are rebuilding - * a shared rel. However, we do make the new heap mapped if the source is - * mapped. This simplifies swap_relation_files, and is absolutely - * necessary for rebuilding pg_class, for reasons explained there. - */ - snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap); - - OIDNewHeap = heap_create_with_catalog(NewHeapName, - namespaceid, - NewTableSpace, - InvalidOid, - InvalidOid, - InvalidOid, - OldHeap->rd_rel->relowner, - NewAccessMethod, - OldHeapDesc, - NIL, - RELKIND_RELATION, - relpersistence, - false, - RelationIsMapped(OldHeap), - ONCOMMIT_NOOP, - reloptions, - false, - true, - true, - OIDOldHeap, - NULL); - Assert(OIDNewHeap != InvalidOid); - - ReleaseSysCache(tuple); - - /* - * Advance command counter so that the newly-created relation's catalog - * tuples will be visible to table_open. - */ - CommandCounterIncrement(); - - /* - * If necessary, create a TOAST table for the new relation. - * - * If the relation doesn't have a TOAST table already, we can't need one - * for the new relation. The other way around is possible though: if some - * wide columns have been dropped, NewHeapCreateToastTable can decide that - * no TOAST table is needed for the new table. - * - * Note that NewHeapCreateToastTable ends with CommandCounterIncrement, so - * that the TOAST table will be visible for insertion. - */ - toastid = OldHeap->rd_rel->reltoastrelid; - if (OidIsValid(toastid)) - { - /* keep the existing toast table's reloptions, if any */ - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); - if (!HeapTupleIsValid(tuple)) - elog(ERROR, "cache lookup failed for relation %u", toastid); - reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, - &isNull); - if (isNull) - reloptions = (Datum) 0; - - NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid); - - ReleaseSysCache(tuple); - } - - table_close(OldHeap, NoLock); - - return OIDNewHeap; -} - -/* - * Do the physical copying of table data. - * - * There are three output parameters: - * *pSwapToastByContent is set true if toast tables must be swapped by content. - * *pFreezeXid receives the TransactionId used as freeze cutoff point. - * *pCutoffMulti receives the MultiXactId used as a cutoff point. - */ -static void -copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verbose, - bool *pSwapToastByContent, TransactionId *pFreezeXid, - MultiXactId *pCutoffMulti) -{ - Relation relRelation; - HeapTuple reltup; - Form_pg_class relform; - TupleDesc oldTupDesc PG_USED_FOR_ASSERTS_ONLY; - TupleDesc newTupDesc PG_USED_FOR_ASSERTS_ONLY; - VacuumParams params; - struct VacuumCutoffs cutoffs; - bool use_sort; - double num_tuples = 0, - tups_vacuumed = 0, - tups_recently_dead = 0; - BlockNumber num_pages; - int elevel = verbose ? INFO : DEBUG2; - PGRUsage ru0; - char *nspname; - - pg_rusage_init(&ru0); - - /* Store a copy of the namespace name for logging purposes */ - nspname = get_namespace_name(RelationGetNamespace(OldHeap)); - - /* - * Their tuple descriptors should be exactly alike, but here we only need - * assume that they have the same number of columns. - */ - oldTupDesc = RelationGetDescr(OldHeap); - newTupDesc = RelationGetDescr(NewHeap); - Assert(newTupDesc->natts == oldTupDesc->natts); - - /* - * If the OldHeap has a toast table, get lock on the toast table to keep - * it from being vacuumed. This is needed because autovacuum processes - * toast tables independently of their main tables, with no lock on the - * latter. If an autovacuum were to start on the toast table after we - * compute our OldestXmin below, it would use a later OldestXmin, and then - * possibly remove as DEAD toast tuples belonging to main tuples we think - * are only RECENTLY_DEAD. Then we'd fail while trying to copy those - * tuples. - * - * We don't need to open the toast relation here, just lock it. The lock - * will be held till end of transaction. - */ - if (OldHeap->rd_rel->reltoastrelid) - LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); - - /* - * If both tables have TOAST tables, perform toast swap by content. It is - * possible that the old table has a toast table but the new one doesn't, - * if toastable columns have been dropped. In that case we have to do - * swap by links. This is okay because swap by content is only essential - * for system catalogs, and we don't support schema changes for them. - */ - if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid) - { - *pSwapToastByContent = true; - - /* - * When doing swap by content, any toast pointers written into NewHeap - * must use the old toast table's OID, because that's where the toast - * data will eventually be found. Set this up by setting rd_toastoid. - * This also tells toast_save_datum() to preserve the toast value - * OIDs, which we want so as not to invalidate toast pointers in - * system catalog caches, and to avoid making multiple copies of a - * single toast value. - * - * Note that we must hold NewHeap open until we are done writing data, - * since the relcache will not guarantee to remember this setting once - * the relation is closed. Also, this technique depends on the fact - * that no one will try to read from the NewHeap until after we've - * finished writing it and swapping the rels --- otherwise they could - * follow the toast pointers to the wrong place. (It would actually - * work for values copied over from the old toast table, but not for - * any values that we toast which were previously not toasted.) - */ - NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid; - } - else - *pSwapToastByContent = false; - - /* - * Compute xids used to freeze and weed out dead tuples and multixacts. - * Since we're going to rewrite the whole table anyway, there's no reason - * not to be aggressive about this. - */ - memset(¶ms, 0, sizeof(VacuumParams)); - vacuum_get_cutoffs(OldHeap, ¶ms, &cutoffs); - - /* - * FreezeXid will become the table's new relfrozenxid, and that mustn't go - * backwards, so take the max. - */ - { - TransactionId relfrozenxid = OldHeap->rd_rel->relfrozenxid; - - if (TransactionIdIsValid(relfrozenxid) && - TransactionIdPrecedes(cutoffs.FreezeLimit, relfrozenxid)) - cutoffs.FreezeLimit = relfrozenxid; - } - - /* - * MultiXactCutoff, similarly, shouldn't go backwards either. - */ - { - MultiXactId relminmxid = OldHeap->rd_rel->relminmxid; - - if (MultiXactIdIsValid(relminmxid) && - MultiXactIdPrecedes(cutoffs.MultiXactCutoff, relminmxid)) - cutoffs.MultiXactCutoff = relminmxid; - } - - /* - * Decide whether to use an indexscan or seqscan-and-optional-sort to scan - * the OldHeap. We know how to use a sort to duplicate the ordering of a - * btree index, and will use seqscan-and-sort for that case if the planner - * tells us it's cheaper. Otherwise, always indexscan if an index is - * provided, else plain seqscan. - */ - if (OldIndex != NULL && OldIndex->rd_rel->relam == BTREE_AM_OID) - use_sort = plan_cluster_use_sort(RelationGetRelid(OldHeap), - RelationGetRelid(OldIndex)); - else - use_sort = false; - - /* Log what we're doing */ - if (OldIndex != NULL && !use_sort) - ereport(elevel, - (errmsg("clustering \"%s.%s\" using index scan on \"%s\"", - nspname, - RelationGetRelationName(OldHeap), - RelationGetRelationName(OldIndex)))); - else if (use_sort) - ereport(elevel, - (errmsg("clustering \"%s.%s\" using sequential scan and sort", - nspname, - RelationGetRelationName(OldHeap)))); - else - ereport(elevel, - (errmsg("vacuuming \"%s.%s\"", - nspname, - RelationGetRelationName(OldHeap)))); - - /* - * Hand off the actual copying to AM specific function, the generic code - * cannot know how to deal with visibility across AMs. Note that this - * routine is allowed to set FreezeXid / MultiXactCutoff to different - * values (e.g. because the AM doesn't use freezing). - */ - table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort, - cutoffs.OldestXmin, &cutoffs.FreezeLimit, - &cutoffs.MultiXactCutoff, - &num_tuples, &tups_vacuumed, - &tups_recently_dead); - - /* return selected values to caller, get set as relfrozenxid/minmxid */ - *pFreezeXid = cutoffs.FreezeLimit; - *pCutoffMulti = cutoffs.MultiXactCutoff; - - /* Reset rd_toastoid just to be tidy --- it shouldn't be looked at again */ - NewHeap->rd_toastoid = InvalidOid; - - num_pages = RelationGetNumberOfBlocks(NewHeap); - - /* Log what we did */ - ereport(elevel, - (errmsg("\"%s.%s\": found %.0f removable, %.0f nonremovable row versions in %u pages", - nspname, - RelationGetRelationName(OldHeap), - tups_vacuumed, num_tuples, - RelationGetNumberOfBlocks(OldHeap)), - errdetail("%.0f dead row versions cannot be removed yet.\n" - "%s.", - tups_recently_dead, - pg_rusage_show(&ru0)))); - - /* Update pg_class to reflect the correct values of pages and tuples. */ - relRelation = table_open(RelationRelationId, RowExclusiveLock); - - reltup = SearchSysCacheCopy1(RELOID, - ObjectIdGetDatum(RelationGetRelid(NewHeap))); - if (!HeapTupleIsValid(reltup)) - elog(ERROR, "cache lookup failed for relation %u", - RelationGetRelid(NewHeap)); - relform = (Form_pg_class) GETSTRUCT(reltup); - - relform->relpages = num_pages; - relform->reltuples = num_tuples; - - /* Don't update the stats for pg_class. See swap_relation_files. */ - if (RelationGetRelid(OldHeap) != RelationRelationId) - CatalogTupleUpdate(relRelation, &reltup->t_self, reltup); - else - CacheInvalidateRelcacheByTuple(reltup); - - /* Clean up. */ - heap_freetuple(reltup); - table_close(relRelation, RowExclusiveLock); - - /* Make the update visible */ - CommandCounterIncrement(); -} - -/* - * Swap the physical files of two given relations. - * - * We swap the physical identity (reltablespace, relfilenumber) while keeping - * the same logical identities of the two relations. relpersistence is also - * swapped, which is critical since it determines where buffers live for each - * relation. - * - * We can swap associated TOAST data in either of two ways: recursively swap - * the physical content of the toast tables (and their indexes), or swap the - * TOAST links in the given relations' pg_class entries. The former is needed - * to manage rewrites of shared catalogs (where we cannot change the pg_class - * links) while the latter is the only way to handle cases in which a toast - * table is added or removed altogether. - * - * Additionally, the first relation is marked with relfrozenxid set to - * frozenXid. It seems a bit ugly to have this here, but the caller would - * have to do it anyway, so having it here saves a heap_update. Note: in - * the swap-toast-links case, we assume we don't need to change the toast - * table's relfrozenxid: the new version of the toast table should already - * have relfrozenxid set to RecentXmin, which is good enough. - * - * Lastly, if r2 and its toast table and toast index (if any) are mapped, - * their OIDs are emitted into mapped_tables[]. This is hacky but beats - * having to look the information up again later in finish_heap_swap. - */ -static void -swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, - bool swap_toast_by_content, - bool is_internal, - TransactionId frozenXid, - MultiXactId cutoffMulti, - Oid *mapped_tables) -{ - Relation relRelation; - HeapTuple reltup1, - reltup2; - Form_pg_class relform1, - relform2; - RelFileNumber relfilenumber1, - relfilenumber2; - RelFileNumber swaptemp; - char swptmpchr; - Oid relam1, - relam2; - - /* We need writable copies of both pg_class tuples. */ - relRelation = table_open(RelationRelationId, RowExclusiveLock); - - reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r1)); - if (!HeapTupleIsValid(reltup1)) - elog(ERROR, "cache lookup failed for relation %u", r1); - relform1 = (Form_pg_class) GETSTRUCT(reltup1); - - reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r2)); - if (!HeapTupleIsValid(reltup2)) - elog(ERROR, "cache lookup failed for relation %u", r2); - relform2 = (Form_pg_class) GETSTRUCT(reltup2); - - relfilenumber1 = relform1->relfilenode; - relfilenumber2 = relform2->relfilenode; - relam1 = relform1->relam; - relam2 = relform2->relam; - - if (RelFileNumberIsValid(relfilenumber1) && - RelFileNumberIsValid(relfilenumber2)) - { - /* - * Normal non-mapped relations: swap relfilenumbers, reltablespaces, - * relpersistence - */ - Assert(!target_is_pg_class); - - swaptemp = relform1->relfilenode; - relform1->relfilenode = relform2->relfilenode; - relform2->relfilenode = swaptemp; - - swaptemp = relform1->reltablespace; - relform1->reltablespace = relform2->reltablespace; - relform2->reltablespace = swaptemp; - - swaptemp = relform1->relam; - relform1->relam = relform2->relam; - relform2->relam = swaptemp; - - swptmpchr = relform1->relpersistence; - relform1->relpersistence = relform2->relpersistence; - relform2->relpersistence = swptmpchr; - - /* Also swap toast links, if we're swapping by links */ - if (!swap_toast_by_content) - { - swaptemp = relform1->reltoastrelid; - relform1->reltoastrelid = relform2->reltoastrelid; - relform2->reltoastrelid = swaptemp; - } - } - else - { - /* - * Mapped-relation case. Here we have to swap the relation mappings - * instead of modifying the pg_class columns. Both must be mapped. - */ - if (RelFileNumberIsValid(relfilenumber1) || - RelFileNumberIsValid(relfilenumber2)) - elog(ERROR, "cannot swap mapped relation \"%s\" with non-mapped relation", - NameStr(relform1->relname)); - - /* - * We can't change the tablespace nor persistence of a mapped rel, and - * we can't handle toast link swapping for one either, because we must - * not apply any critical changes to its pg_class row. These cases - * should be prevented by upstream permissions tests, so these checks - * are non-user-facing emergency backstop. - */ - if (relform1->reltablespace != relform2->reltablespace) - elog(ERROR, "cannot change tablespace of mapped relation \"%s\"", - NameStr(relform1->relname)); - if (relform1->relpersistence != relform2->relpersistence) - elog(ERROR, "cannot change persistence of mapped relation \"%s\"", - NameStr(relform1->relname)); - if (relform1->relam != relform2->relam) - elog(ERROR, "cannot change access method of mapped relation \"%s\"", - NameStr(relform1->relname)); - if (!swap_toast_by_content && - (relform1->reltoastrelid || relform2->reltoastrelid)) - elog(ERROR, "cannot swap toast by links for mapped relation \"%s\"", - NameStr(relform1->relname)); - - /* - * Fetch the mappings --- shouldn't fail, but be paranoid - */ - relfilenumber1 = RelationMapOidToFilenumber(r1, relform1->relisshared); - if (!RelFileNumberIsValid(relfilenumber1)) - elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u", - NameStr(relform1->relname), r1); - relfilenumber2 = RelationMapOidToFilenumber(r2, relform2->relisshared); - if (!RelFileNumberIsValid(relfilenumber2)) - elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u", - NameStr(relform2->relname), r2); - - /* - * Send replacement mappings to relmapper. Note these won't actually - * take effect until CommandCounterIncrement. - */ - RelationMapUpdateMap(r1, relfilenumber2, relform1->relisshared, false); - RelationMapUpdateMap(r2, relfilenumber1, relform2->relisshared, false); - - /* Pass OIDs of mapped r2 tables back to caller */ - *mapped_tables++ = r2; - } - - /* - * Recognize that rel1's relfilenumber (swapped from rel2) is new in this - * subtransaction. The rel2 storage (swapped from rel1) may or may not be - * new. - */ - { - Relation rel1, - rel2; - - rel1 = relation_open(r1, NoLock); - rel2 = relation_open(r2, NoLock); - rel2->rd_createSubid = rel1->rd_createSubid; - rel2->rd_newRelfilelocatorSubid = rel1->rd_newRelfilelocatorSubid; - rel2->rd_firstRelfilelocatorSubid = rel1->rd_firstRelfilelocatorSubid; - RelationAssumeNewRelfilelocator(rel1); - relation_close(rel1, NoLock); - relation_close(rel2, NoLock); - } - - /* - * In the case of a shared catalog, these next few steps will only affect - * our own database's pg_class row; but that's okay, because they are all - * noncritical updates. That's also an important fact for the case of a - * mapped catalog, because it's possible that we'll commit the map change - * and then fail to commit the pg_class update. - */ - - /* set rel1's frozen Xid and minimum MultiXid */ - if (relform1->relkind != RELKIND_INDEX) - { - Assert(!TransactionIdIsValid(frozenXid) || - TransactionIdIsNormal(frozenXid)); - relform1->relfrozenxid = frozenXid; - relform1->relminmxid = cutoffMulti; - } - - /* swap size statistics too, since new rel has freshly-updated stats */ - { - int32 swap_pages; - float4 swap_tuples; - int32 swap_allvisible; - int32 swap_allfrozen; - - swap_pages = relform1->relpages; - relform1->relpages = relform2->relpages; - relform2->relpages = swap_pages; - - swap_tuples = relform1->reltuples; - relform1->reltuples = relform2->reltuples; - relform2->reltuples = swap_tuples; - - swap_allvisible = relform1->relallvisible; - relform1->relallvisible = relform2->relallvisible; - relform2->relallvisible = swap_allvisible; - - swap_allfrozen = relform1->relallfrozen; - relform1->relallfrozen = relform2->relallfrozen; - relform2->relallfrozen = swap_allfrozen; - } - - /* - * Update the tuples in pg_class --- unless the target relation of the - * swap is pg_class itself. In that case, there is zero point in making - * changes because we'd be updating the old data that we're about to throw - * away. Because the real work being done here for a mapped relation is - * just to change the relation map settings, it's all right to not update - * the pg_class rows in this case. The most important changes will instead - * performed later, in finish_heap_swap() itself. - */ - if (!target_is_pg_class) - { - CatalogIndexState indstate; - - indstate = CatalogOpenIndexes(relRelation); - CatalogTupleUpdateWithInfo(relRelation, &reltup1->t_self, reltup1, - indstate); - CatalogTupleUpdateWithInfo(relRelation, &reltup2->t_self, reltup2, - indstate); - CatalogCloseIndexes(indstate); - } - else - { - /* no update ... but we do still need relcache inval */ - CacheInvalidateRelcacheByTuple(reltup1); - CacheInvalidateRelcacheByTuple(reltup2); - } - - /* - * Now that pg_class has been updated with its relevant information for - * the swap, update the dependency of the relations to point to their new - * table AM, if it has changed. - */ - if (relam1 != relam2) - { - if (changeDependencyFor(RelationRelationId, - r1, - AccessMethodRelationId, - relam1, - relam2) != 1) - elog(ERROR, "could not change access method dependency for relation \"%s.%s\"", - get_namespace_name(get_rel_namespace(r1)), - get_rel_name(r1)); - if (changeDependencyFor(RelationRelationId, - r2, - AccessMethodRelationId, - relam2, - relam1) != 1) - elog(ERROR, "could not change access method dependency for relation \"%s.%s\"", - get_namespace_name(get_rel_namespace(r2)), - get_rel_name(r2)); - } - - /* - * Post alter hook for modified relations. The change to r2 is always - * internal, but r1 depends on the invocation context. - */ - InvokeObjectPostAlterHookArg(RelationRelationId, r1, 0, - InvalidOid, is_internal); - InvokeObjectPostAlterHookArg(RelationRelationId, r2, 0, - InvalidOid, true); - - /* - * If we have toast tables associated with the relations being swapped, - * deal with them too. - */ - if (relform1->reltoastrelid || relform2->reltoastrelid) - { - if (swap_toast_by_content) - { - if (relform1->reltoastrelid && relform2->reltoastrelid) - { - /* Recursively swap the contents of the toast tables */ - swap_relation_files(relform1->reltoastrelid, - relform2->reltoastrelid, - target_is_pg_class, - swap_toast_by_content, - is_internal, - frozenXid, - cutoffMulti, - mapped_tables); - } - else - { - /* caller messed up */ - elog(ERROR, "cannot swap toast files by content when there's only one"); - } - } - else - { - /* - * We swapped the ownership links, so we need to change dependency - * data to match. - * - * NOTE: it is possible that only one table has a toast table. - * - * NOTE: at present, a TOAST table's only dependency is the one on - * its owning table. If more are ever created, we'd need to use - * something more selective than deleteDependencyRecordsFor() to - * get rid of just the link we want. - */ - ObjectAddress baseobject, - toastobject; - long count; - - /* - * We disallow this case for system catalogs, to avoid the - * possibility that the catalog we're rebuilding is one of the - * ones the dependency changes would change. It's too late to be - * making any data changes to the target catalog. - */ - if (IsSystemClass(r1, relform1)) - elog(ERROR, "cannot swap toast files by links for system catalogs"); - - /* Delete old dependencies */ - if (relform1->reltoastrelid) - { - count = deleteDependencyRecordsFor(RelationRelationId, - relform1->reltoastrelid, - false); - if (count != 1) - elog(ERROR, "expected one dependency record for TOAST table, found %ld", - count); - } - if (relform2->reltoastrelid) - { - count = deleteDependencyRecordsFor(RelationRelationId, - relform2->reltoastrelid, - false); - if (count != 1) - elog(ERROR, "expected one dependency record for TOAST table, found %ld", - count); - } - - /* Register new dependencies */ - baseobject.classId = RelationRelationId; - baseobject.objectSubId = 0; - toastobject.classId = RelationRelationId; - toastobject.objectSubId = 0; - - if (relform1->reltoastrelid) - { - baseobject.objectId = r1; - toastobject.objectId = relform1->reltoastrelid; - recordDependencyOn(&toastobject, &baseobject, - DEPENDENCY_INTERNAL); - } - - if (relform2->reltoastrelid) - { - baseobject.objectId = r2; - toastobject.objectId = relform2->reltoastrelid; - recordDependencyOn(&toastobject, &baseobject, - DEPENDENCY_INTERNAL); - } - } - } - - /* - * If we're swapping two toast tables by content, do the same for their - * valid index. The swap can actually be safely done only if the relations - * have indexes. - */ - if (swap_toast_by_content && - relform1->relkind == RELKIND_TOASTVALUE && - relform2->relkind == RELKIND_TOASTVALUE) - { - Oid toastIndex1, - toastIndex2; - - /* Get valid index for each relation */ - toastIndex1 = toast_get_valid_index(r1, - AccessExclusiveLock); - toastIndex2 = toast_get_valid_index(r2, - AccessExclusiveLock); - - swap_relation_files(toastIndex1, - toastIndex2, - target_is_pg_class, - swap_toast_by_content, - is_internal, - InvalidTransactionId, - InvalidMultiXactId, - mapped_tables); - } - - /* Clean up. */ - heap_freetuple(reltup1); - heap_freetuple(reltup2); - - table_close(relRelation, RowExclusiveLock); -} - -/* - * Remove the transient table that was built by make_new_heap, and finish - * cleaning up (including rebuilding all indexes on the old heap). - */ -void -finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, - bool is_system_catalog, - bool swap_toast_by_content, - bool check_constraints, - bool is_internal, - TransactionId frozenXid, - MultiXactId cutoffMulti, - char newrelpersistence) -{ - ObjectAddress object; - Oid mapped_tables[4]; - int reindex_flags; - ReindexParams reindex_params = {0}; - int i; - - /* Report that we are now swapping relation files */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES); - - /* Zero out possible results from swapped_relation_files */ - memset(mapped_tables, 0, sizeof(mapped_tables)); - - /* - * Swap the contents of the heap relations (including any toast tables). - * Also set old heap's relfrozenxid to frozenXid. - */ - swap_relation_files(OIDOldHeap, OIDNewHeap, - (OIDOldHeap == RelationRelationId), - swap_toast_by_content, is_internal, - frozenXid, cutoffMulti, mapped_tables); - - /* - * If it's a system catalog, queue a sinval message to flush all catcaches - * on the catalog when we reach CommandCounterIncrement. - */ - if (is_system_catalog) - CacheInvalidateCatalog(OIDOldHeap); - - /* - * Rebuild each index on the relation (but not the toast table, which is - * all-new at this point). It is important to do this before the DROP - * step because if we are processing a system catalog that will be used - * during DROP, we want to have its indexes available. There is no - * advantage to the other order anyway because this is all transactional, - * so no chance to reclaim disk space before commit. We do not need a - * final CommandCounterIncrement() because reindex_relation does it. - * - * Note: because index_build is called via reindex_relation, it will never - * set indcheckxmin true for the indexes. This is OK even though in some - * sense we are building new indexes rather than rebuilding existing ones, - * because the new heap won't contain any HOT chains at all, let alone - * broken ones, so it can't be necessary to set indcheckxmin. - */ - reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE; - if (check_constraints) - reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS; - - /* - * Ensure that the indexes have the same persistence as the parent - * relation. - */ - if (newrelpersistence == RELPERSISTENCE_UNLOGGED) - reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED; - else if (newrelpersistence == RELPERSISTENCE_PERMANENT) - reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT; - - /* Report that we are now reindexing relations */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_REBUILD_INDEX); - - reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params); - - /* Report that we are now doing clean up */ - pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE, - PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP); - - /* - * If the relation being rebuilt is pg_class, swap_relation_files() - * couldn't update pg_class's own pg_class entry (check comments in - * swap_relation_files()), thus relfrozenxid was not updated. That's - * annoying because a potential reason for doing a VACUUM FULL is a - * imminent or actual anti-wraparound shutdown. So, now that we can - * access the new relation using its indices, update relfrozenxid. - * pg_class doesn't have a toast relation, so we don't need to update the - * corresponding toast relation. Not that there's little point moving all - * relfrozenxid updates here since swap_relation_files() needs to write to - * pg_class for non-mapped relations anyway. - */ - if (OIDOldHeap == RelationRelationId) - { - Relation relRelation; - HeapTuple reltup; - Form_pg_class relform; - - relRelation = table_open(RelationRelationId, RowExclusiveLock); - - reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(OIDOldHeap)); - if (!HeapTupleIsValid(reltup)) - elog(ERROR, "cache lookup failed for relation %u", OIDOldHeap); - relform = (Form_pg_class) GETSTRUCT(reltup); - - relform->relfrozenxid = frozenXid; - relform->relminmxid = cutoffMulti; - - CatalogTupleUpdate(relRelation, &reltup->t_self, reltup); - - table_close(relRelation, RowExclusiveLock); - } - - /* Destroy new heap with old filenumber */ - object.classId = RelationRelationId; - object.objectId = OIDNewHeap; - object.objectSubId = 0; - - /* - * The new relation is local to our transaction and we know nothing - * depends on it, so DROP_RESTRICT should be OK. - */ - performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); - - /* performDeletion does CommandCounterIncrement at end */ - - /* - * Now we must remove any relation mapping entries that we set up for the - * transient table, as well as its toast table and toast index if any. If - * we fail to do this before commit, the relmapper will complain about new - * permanent map entries being added post-bootstrap. - */ - for (i = 0; OidIsValid(mapped_tables[i]); i++) - RelationMapRemoveMapping(mapped_tables[i]); - - /* - * At this point, everything is kosher except that, if we did toast swap - * by links, the toast table's name corresponds to the transient table. - * The name is irrelevant to the backend because it's referenced by OID, - * but users looking at the catalogs could be confused. Rename it to - * prevent this problem. - * - * Note no lock required on the relation, because we already hold an - * exclusive lock on it. - */ - if (!swap_toast_by_content) - { - Relation newrel; - - newrel = table_open(OIDOldHeap, NoLock); - if (OidIsValid(newrel->rd_rel->reltoastrelid)) - { - Oid toastidx; - char NewToastName[NAMEDATALEN]; - - /* Get the associated valid index to be renamed */ - toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid, - NoLock); - - /* rename the toast table ... */ - snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u", - OIDOldHeap); - RenameRelationInternal(newrel->rd_rel->reltoastrelid, - NewToastName, true, false); - - /* ... and its valid index too. */ - snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u_index", - OIDOldHeap); - - RenameRelationInternal(toastidx, - NewToastName, true, true); - - /* - * Reset the relrewrite for the toast. The command-counter - * increment is required here as we are about to update the tuple - * that is updated as part of RenameRelationInternal. - */ - CommandCounterIncrement(); - ResetRelRewrite(newrel->rd_rel->reltoastrelid); - } - relation_close(newrel, NoLock); - } - - /* if it's not a catalog table, clear any missing attribute settings */ - if (!is_system_catalog) - { - Relation newrel; - - newrel = table_open(OIDOldHeap, NoLock); - RelationClearMissing(newrel); - relation_close(newrel, NoLock); - } -} - - -/* - * Get a list of tables that the current user has privileges on and - * have indisclustered set. Return the list in a List * of RelToCluster - * (stored in the specified memory context), each one giving the tableOid - * and the indexOid on which the table is already clustered. - */ -static List * -get_tables_to_cluster(MemoryContext cluster_context) -{ - Relation indRelation; - TableScanDesc scan; - ScanKeyData entry; - HeapTuple indexTuple; - Form_pg_index index; - MemoryContext old_context; - List *rtcs = NIL; - - /* - * Get all indexes that have indisclustered set and that the current user - * has the appropriate privileges for. - */ - indRelation = table_open(IndexRelationId, AccessShareLock); - ScanKeyInit(&entry, - Anum_pg_index_indisclustered, - BTEqualStrategyNumber, F_BOOLEQ, - BoolGetDatum(true)); - scan = table_beginscan_catalog(indRelation, 1, &entry); - while ((indexTuple = heap_getnext(scan, ForwardScanDirection)) != NULL) - { - RelToCluster *rtc; - - index = (Form_pg_index) GETSTRUCT(indexTuple); - - if (!cluster_is_permitted_for_relation(index->indrelid, GetUserId())) - continue; - - /* Use a permanent memory context for the result list */ - old_context = MemoryContextSwitchTo(cluster_context); - - rtc = (RelToCluster *) palloc(sizeof(RelToCluster)); - rtc->tableOid = index->indrelid; - rtc->indexOid = index->indexrelid; - rtcs = lappend(rtcs, rtc); - - MemoryContextSwitchTo(old_context); - } - table_endscan(scan); - - relation_close(indRelation, AccessShareLock); - - return rtcs; -} - -/* - * Given an index on a partitioned table, return a list of RelToCluster for - * all the children leaves tables/indexes. - * - * Like expand_vacuum_rel, but here caller must hold AccessExclusiveLock - * on the table containing the index. - */ -static List * -get_tables_to_cluster_partitioned(MemoryContext cluster_context, Oid indexOid) -{ - List *inhoids; - ListCell *lc; - List *rtcs = NIL; - MemoryContext old_context; - - /* Do not lock the children until they're processed */ - inhoids = find_all_inheritors(indexOid, NoLock, NULL); - - foreach(lc, inhoids) - { - Oid indexrelid = lfirst_oid(lc); - Oid relid = IndexGetRelation(indexrelid, false); - RelToCluster *rtc; - - /* consider only leaf indexes */ - if (get_rel_relkind(indexrelid) != RELKIND_INDEX) - continue; - - /* - * It's possible that the user does not have privileges to CLUSTER the - * leaf partition despite having such privileges on the partitioned - * table. We skip any partitions which the user is not permitted to - * CLUSTER. - */ - if (!cluster_is_permitted_for_relation(relid, GetUserId())) - continue; - - /* Use a permanent memory context for the result list */ - old_context = MemoryContextSwitchTo(cluster_context); - - rtc = (RelToCluster *) palloc(sizeof(RelToCluster)); - rtc->tableOid = relid; - rtc->indexOid = indexrelid; - rtcs = lappend(rtcs, rtc); - - MemoryContextSwitchTo(old_context); - } - - return rtcs; -} - -/* - * Return whether userid has privileges to CLUSTER relid. If not, this - * function emits a WARNING. - */ -static bool -cluster_is_permitted_for_relation(Oid relid, Oid userid) -{ - if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) == ACLCHECK_OK) - return true; - - ereport(WARNING, - (errmsg("permission denied to cluster \"%s\", skipping it", - get_rel_name(relid)))); - return false; -} diff --git a/src/backend/commands/collationcmds.c b/src/backend/commands/collationcmds.c index 8acbfbbeda041..0bc31ec2b6f19 100644 --- a/src/backend/commands/collationcmds.c +++ b/src/backend/commands/collationcmds.c @@ -3,7 +3,7 @@ * collationcmds.c * collation-related commands support code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,10 @@ */ #include "postgres.h" +#ifdef USE_ICU +#include +#endif + #include "access/htup_details.h" #include "access/table.h" #include "access/xact.h" @@ -30,6 +34,7 @@ #include "common/string.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "storage/fd.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/lsyscache.h" diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c index f67a8b95d29de..771aba2a69f2b 100644 --- a/src/backend/commands/comment.c +++ b/src/backend/commands/comment.c @@ -4,7 +4,7 @@ * * PostgreSQL object comments utility code. * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/commands/comment.c @@ -20,10 +20,10 @@ #include "access/table.h" #include "catalog/indexing.h" #include "catalog/objectaddress.h" +#include "catalog/pg_database.h" #include "catalog/pg_description.h" #include "catalog/pg_shdescription.h" #include "commands/comment.h" -#include "commands/dbcommands.h" #include "miscadmin.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -41,6 +41,7 @@ CommentObject(CommentStmt *stmt) { Relation relation; ObjectAddress address = InvalidObjectAddress; + bool missing_ok; /* * When loading a dump, we may see a COMMENT ON DATABASE for the old name @@ -63,6 +64,14 @@ CommentObject(CommentStmt *stmt) } } + /* + * During binary upgrade, allow nonexistent large objects so that we don't + * have to create them during schema restoration. pg_upgrade will + * transfer the contents of pg_largeobject_metadata via COPY or by + * copying/linking its files from the old cluster later on. + */ + missing_ok = IsBinaryUpgrade && stmt->objtype == OBJECT_LARGEOBJECT; + /* * Translate the parser representation that identifies this object into an * ObjectAddress. get_object_address() will throw an error if the object @@ -70,7 +79,8 @@ CommentObject(CommentStmt *stmt) * against concurrent DROP operations. */ address = get_object_address(stmt->objtype, stmt->object, - &relation, ShareUpdateExclusiveLock, false); + &relation, ShareUpdateExclusiveLock, + missing_ok); /* Require ownership of the target object. */ check_object_ownership(GetUserId(), stmt->objtype, address, diff --git a/src/backend/commands/constraint.c b/src/backend/commands/constraint.c index 3497a8221f29a..421d8c359f0f9 100644 --- a/src/backend/commands/constraint.c +++ b/src/backend/commands/constraint.c @@ -3,7 +3,7 @@ * constraint.c * PostgreSQL CONSTRAINT support code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -106,7 +106,8 @@ unique_key_recheck(PG_FUNCTION_ARGS) */ tmptid = checktid; { - IndexFetchTableData *scan = table_index_fetch_begin(trigdata->tg_relation); + IndexFetchTableData *scan = table_index_fetch_begin(trigdata->tg_relation, + SO_NONE); bool call_again = false; if (!table_index_fetch_tuple(scan, &tmptid, SnapshotSelf, slot, diff --git a/src/backend/commands/conversioncmds.c b/src/backend/commands/conversioncmds.c index d3ecc76d97b14..5f2022d307203 100644 --- a/src/backend/commands/conversioncmds.c +++ b/src/backend/commands/conversioncmds.c @@ -3,7 +3,7 @@ * conversioncmds.c * conversion creation command support code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,6 +19,7 @@ #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/conversioncmds.h" +#include "fmgr.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "parser/parse_func.h" diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c index 74ae42b19a710..003b70852bb90 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -3,7 +3,7 @@ * copy.c * Implements the COPY utility command * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -28,6 +28,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/makefuncs.h" +#include "nodes/miscnodes.h" #include "optimizer/optimizer.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -133,18 +134,77 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, if (stmt->whereClause) { - /* add nsitem to query namespace */ + Bitmapset *expr_attrs = NULL; + int i; + + /* Add nsitem to query namespace */ addNSItemToQuery(pstate, nsitem, false, true, true); /* Transform the raw expression tree */ whereClause = transformExpr(pstate, stmt->whereClause, EXPR_KIND_COPY_WHERE); - /* Make sure it yields a boolean result. */ + /* Make sure it yields a boolean result */ whereClause = coerce_to_boolean(pstate, whereClause, "WHERE"); - /* we have to fix its collations too */ + /* We have to fix its collations too */ assign_expr_collations(pstate, whereClause); + /* + * Identify all columns used in the WHERE clause's expression. If + * there's a whole-row reference, replace it with a range of all + * the user columns (caution: that'll include dropped columns). + */ + pull_varattnos(whereClause, 1, &expr_attrs); + if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs)) + { + expr_attrs = bms_add_range(expr_attrs, + 1 - FirstLowInvalidHeapAttributeNumber, + RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber); + expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber); + } + + /* Now we can scan each column needed in the WHERE clause */ + i = -1; + while ((i = bms_next_member(expr_attrs, i)) >= 0) + { + AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; + Form_pg_attribute att; + + Assert(attno != 0); /* removed above */ + + /* + * Prohibit system columns in the WHERE clause. They won't + * have been filled yet when the filtering happens. (We could + * allow tableoid, but right now it isn't really useful: it + * will read as the target table's OID. Any conceivable use + * for such a WHERE clause would probably wish it to read as + * the target partition's OID, which is not known yet. + * Disallow it to keep flexibility to change that sometime.) + */ + if (attno < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("system columns are not supported in COPY FROM WHERE conditions"), + errdetail("Column \"%s\" is a system column.", + get_attname(RelationGetRelid(rel), attno, false))); + + /* + * Prohibit generated columns in the WHERE clause. Stored + * generated columns are not yet computed when the filtering + * happens. Virtual generated columns could probably work (we + * would need to expand them somewhere around here), but for + * now we keep them consistent with the stored variant. + */ + att = TupleDescAttr(RelationGetDescr(rel), attno - 1); + if (att->attgenerated && !att->attisdropped) + ereport(ERROR, + errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("generated columns are not supported in COPY FROM WHERE conditions"), + errdetail("Column \"%s\" is a generated column.", + get_attname(RelationGetRelid(rel), attno, false))); + } + + /* Reduce WHERE clause to standard list-of-AND-terms form */ whereClause = eval_const_expressions(NULL, whereClause); whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false); @@ -251,11 +311,15 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, * relation which we have opened and locked. Use "ONLY" so that * COPY retrieves rows from only the target table not any * inheritance children, the same as when RLS doesn't apply. + * + * However, when copying data from a partitioned table, we don't + * use "ONLY", since we need to retrieve rows from its descendant + * tables too. */ from = makeRangeVar(get_namespace_name(RelationGetNamespace(rel)), pstrdup(RelationGetRelationName(rel)), -1); - from->inh = false; /* apply ONLY */ + from->inh = (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); /* Build query */ select = makeNode(SelectStmt); @@ -322,12 +386,17 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt, } /* - * Extract a CopyHeaderChoice value from a DefElem. This is like - * defGetBoolean() but also accepts the special value "match". + * Extract the CopyFormatOptions.header_line value from a DefElem. + * + * Parses the HEADER option for COPY, which can be a boolean, an integer greater + * than or equal to zero (number of lines to skip), or the special value + * "match". */ -static CopyHeaderChoice -defGetCopyHeaderChoice(DefElem *def, bool is_from) +static int +defGetCopyHeaderOption(DefElem *def, bool is_from) { + int ival = COPY_HEADER_FALSE; + /* * If no parameter value given, assume "true" is meant. */ @@ -335,21 +404,14 @@ defGetCopyHeaderChoice(DefElem *def, bool is_from) return COPY_HEADER_TRUE; /* - * Allow 0, 1, "true", "false", "on", "off", or "match". + * Allow an integer value greater than or equal to zero (integers + * specified as strings are also accepted, mainly for file_fdw foreign + * table options), "true", "false", "on", "off", or "match". */ switch (nodeTag(def->arg)) { case T_Integer: - switch (intVal(def->arg)) - { - case 0: - return COPY_HEADER_FALSE; - case 1: - return COPY_HEADER_TRUE; - default: - /* otherwise, error out below */ - break; - } + ival = intVal(def->arg); break; default: { @@ -376,14 +438,38 @@ defGetCopyHeaderChoice(DefElem *def, bool is_from) sval))); return COPY_HEADER_MATCH; } + else + { + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + /* Check if the header is a valid integer */ + ival = pg_strtoint32_safe(sval, (Node *) &escontext); + if (escontext.error_occurred) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + /*- translator: first %s is the name of a COPY option, e.g. ON_ERROR, + second %s is the special value "match" for that option */ + errmsg("%s requires a Boolean value, an integer " + "value greater than or equal to zero, " + "or the string \"%s\"", + def->defname, "match"))); + } } break; } - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("%s requires a Boolean value or \"match\"", - def->defname))); - return COPY_HEADER_FALSE; /* keep compiler quiet */ + + if (ival < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("a negative integer value cannot be " + "specified for %s", def->defname))); + + if (!is_from && ival > 1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot use multi-line header in COPY TO"))); + + return ival; } /* @@ -402,13 +488,12 @@ defGetCopyOnErrorChoice(DefElem *def, ParseState *pstate, bool is_from) errmsg("COPY %s cannot be used with %s", "ON_ERROR", "COPY TO"), parser_errposition(pstate, def->location))); - /* - * Allow "stop", or "ignore" values. - */ if (pg_strcasecmp(sval, "stop") == 0) return COPY_ON_ERROR_STOP; if (pg_strcasecmp(sval, "ignore") == 0) return COPY_ON_ERROR_IGNORE; + if (pg_strcasecmp(sval, "set_null") == 0) + return COPY_ON_ERROR_SET_NULL; ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -435,7 +520,7 @@ defGetCopyRejectLimitOption(DefElem *def) (errcode(ERRCODE_SYNTAX_ERROR), errmsg("%s requires a numeric value", def->defname))); - else if (nodeTag(def->arg) == T_String) + else if (IsA(def->arg, String)) reject_limit = pg_strtoint64(strVal(def->arg)); else reject_limit = defGetInt64(def); @@ -504,13 +589,16 @@ ProcessCopyOptions(ParseState *pstate, bool on_error_specified = false; bool log_verbosity_specified = false; bool reject_limit_specified = false; + bool force_array_specified = false; ListCell *option; /* Support external use for option sanity checking */ if (opts_out == NULL) - opts_out = (CopyFormatOptions *) palloc0(sizeof(CopyFormatOptions)); + opts_out = palloc0_object(CopyFormatOptions); opts_out->file_encoding = -1; + /* default format */ + opts_out->format = COPY_FORMAT_TEXT; /* Extract options from the statement node tree */ foreach(option, options) @@ -525,11 +613,13 @@ ProcessCopyOptions(ParseState *pstate, errorConflictingDefElem(defel, pstate); format_specified = true; if (strcmp(fmt, "text") == 0) - /* default format */ ; + opts_out->format = COPY_FORMAT_TEXT; else if (strcmp(fmt, "csv") == 0) - opts_out->csv_mode = true; + opts_out->format = COPY_FORMAT_CSV; else if (strcmp(fmt, "binary") == 0) - opts_out->binary = true; + opts_out->format = COPY_FORMAT_BINARY; + else if (strcmp(fmt, "json") == 0) + opts_out->format = COPY_FORMAT_JSON; else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -566,7 +656,7 @@ ProcessCopyOptions(ParseState *pstate, if (header_specified) errorConflictingDefElem(defel, pstate); header_specified = true; - opts_out->header_line = defGetCopyHeaderChoice(defel, is_from); + opts_out->header_line = defGetCopyHeaderOption(defel, is_from); } else if (strcmp(defel->defname, "quote") == 0) { @@ -656,6 +746,13 @@ ProcessCopyOptions(ParseState *pstate, defel->defname), parser_errposition(pstate, defel->location))); } + else if (strcmp(defel->defname, "force_array") == 0) + { + if (force_array_specified) + errorConflictingDefElem(defel, pstate); + force_array_specified = true; + opts_out->force_array = defGetBoolean(defel); + } else if (strcmp(defel->defname, "on_error") == 0) { if (on_error_specified) @@ -689,31 +786,42 @@ ProcessCopyOptions(ParseState *pstate, * Check for incompatible options (must do these three before inserting * defaults) */ - if (opts_out->binary && opts_out->delim) + if (opts_out->delim && + (opts_out->format == COPY_FORMAT_BINARY || + opts_out->format == COPY_FORMAT_JSON)) ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ - errmsg("cannot specify %s in BINARY mode", "DELIMITER"))); - - if (opts_out->binary && opts_out->null_print) + errcode(ERRCODE_SYNTAX_ERROR), + opts_out->format == COPY_FORMAT_BINARY + ? errmsg("cannot specify %s in BINARY mode", "DELIMITER") + : errmsg("cannot specify %s in JSON mode", "DELIMITER")); + + if (opts_out->null_print && + (opts_out->format == COPY_FORMAT_BINARY || + opts_out->format == COPY_FORMAT_JSON)) ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("cannot specify %s in BINARY mode", "NULL"))); - - if (opts_out->binary && opts_out->default_print) + errcode(ERRCODE_SYNTAX_ERROR), + opts_out->format == COPY_FORMAT_BINARY + ? errmsg("cannot specify %s in BINARY mode", "NULL") + : errmsg("cannot specify %s in JSON mode", "NULL")); + + if (opts_out->default_print && + (opts_out->format == COPY_FORMAT_BINARY || + opts_out->format == COPY_FORMAT_JSON)) ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("cannot specify %s in BINARY mode", "DEFAULT"))); + errcode(ERRCODE_SYNTAX_ERROR), + opts_out->format == COPY_FORMAT_BINARY + ? errmsg("cannot specify %s in BINARY mode", "DEFAULT") + : errmsg("cannot specify %s in JSON mode", "DEFAULT")); /* Set defaults for omitted options */ if (!opts_out->delim) - opts_out->delim = opts_out->csv_mode ? "," : "\t"; + opts_out->delim = (opts_out->format == COPY_FORMAT_CSV) ? "," : "\t"; if (!opts_out->null_print) - opts_out->null_print = opts_out->csv_mode ? "" : "\\N"; + opts_out->null_print = (opts_out->format == COPY_FORMAT_CSV) ? "" : "\\N"; opts_out->null_print_len = strlen(opts_out->null_print); - if (opts_out->csv_mode) + if (opts_out->format == COPY_FORMAT_CSV) { if (!opts_out->quote) opts_out->quote = "\""; @@ -761,7 +869,7 @@ ProcessCopyOptions(ParseState *pstate, * future-proofing. Likewise we disallow all digits though only octal * digits are actually dangerous. */ - if (!opts_out->csv_mode && + if (opts_out->format != COPY_FORMAT_CSV && strchr("\\.abcdefghijklmnopqrstuvwxyz0123456789", opts_out->delim[0]) != NULL) ereport(ERROR, @@ -769,43 +877,47 @@ ProcessCopyOptions(ParseState *pstate, errmsg("COPY delimiter cannot be \"%s\"", opts_out->delim))); /* Check header */ - if (opts_out->binary && opts_out->header_line) + if (opts_out->header_line != COPY_HEADER_FALSE && + (opts_out->format == COPY_FORMAT_BINARY || + opts_out->format == COPY_FORMAT_JSON)) ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ - errmsg("cannot specify %s in BINARY mode", "HEADER"))); + opts_out->format == COPY_FORMAT_BINARY + ? errmsg("cannot specify %s in BINARY mode", "HEADER") + : errmsg("cannot specify %s in JSON mode", "HEADER")); /* Check quote */ - if (!opts_out->csv_mode && opts_out->quote != NULL) + if (opts_out->format != COPY_FORMAT_CSV && opts_out->quote != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ errmsg("COPY %s requires CSV mode", "QUOTE"))); - if (opts_out->csv_mode && strlen(opts_out->quote) != 1) + if (opts_out->format == COPY_FORMAT_CSV && strlen(opts_out->quote) != 1) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("COPY quote must be a single one-byte character"))); - if (opts_out->csv_mode && opts_out->delim[0] == opts_out->quote[0]) + if (opts_out->format == COPY_FORMAT_CSV && opts_out->delim[0] == opts_out->quote[0]) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("COPY delimiter and quote must be different"))); /* Check escape */ - if (!opts_out->csv_mode && opts_out->escape != NULL) + if (opts_out->format != COPY_FORMAT_CSV && opts_out->escape != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ errmsg("COPY %s requires CSV mode", "ESCAPE"))); - if (opts_out->csv_mode && strlen(opts_out->escape) != 1) + if (opts_out->format == COPY_FORMAT_CSV && strlen(opts_out->escape) != 1) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("COPY escape must be a single one-byte character"))); /* Check force_quote */ - if (!opts_out->csv_mode && (opts_out->force_quote || opts_out->force_quote_all)) + if (opts_out->format != COPY_FORMAT_CSV && (opts_out->force_quote || opts_out->force_quote_all)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ @@ -819,8 +931,8 @@ ProcessCopyOptions(ParseState *pstate, "COPY FROM"))); /* Check force_notnull */ - if (!opts_out->csv_mode && (opts_out->force_notnull != NIL || - opts_out->force_notnull_all)) + if (opts_out->format != COPY_FORMAT_CSV && (opts_out->force_notnull != NIL || + opts_out->force_notnull_all)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ @@ -835,8 +947,8 @@ ProcessCopyOptions(ParseState *pstate, "COPY TO"))); /* Check force_null */ - if (!opts_out->csv_mode && (opts_out->force_null != NIL || - opts_out->force_null_all)) + if (opts_out->format != COPY_FORMAT_CSV && (opts_out->force_null != NIL || + opts_out->force_null_all)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /*- translator: %s is the name of a COPY option, e.g. ON_ERROR */ @@ -860,7 +972,7 @@ ProcessCopyOptions(ParseState *pstate, "NULL"))); /* Don't allow the CSV quote char to appear in the null string. */ - if (opts_out->csv_mode && + if (opts_out->format == COPY_FORMAT_CSV && strchr(opts_out->null_print, opts_out->quote[0]) != NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -877,6 +989,17 @@ ProcessCopyOptions(ParseState *pstate, errmsg("COPY %s cannot be used with %s", "FREEZE", "COPY TO"))); + /* Check json format */ + if (opts_out->format == COPY_FORMAT_JSON && is_from) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY %s is not supported for %s", "FORMAT JSON", "COPY FROM")); + + if (opts_out->format != COPY_FORMAT_JSON && opts_out->force_array) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("COPY %s can only be used with JSON mode", "FORCE_ARRAY")); + if (opts_out->default_print) { if (!is_from) @@ -896,7 +1019,7 @@ ProcessCopyOptions(ParseState *pstate, "DEFAULT"))); /* Don't allow the CSV quote char to appear in the default string. */ - if (opts_out->csv_mode && + if (opts_out->format == COPY_FORMAT_CSV && strchr(opts_out->default_print, opts_out->quote[0]) != NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), @@ -913,12 +1036,12 @@ ProcessCopyOptions(ParseState *pstate, errmsg("NULL specification and DEFAULT specification cannot be the same"))); } /* Check on_error */ - if (opts_out->binary && opts_out->on_error != COPY_ON_ERROR_STOP) + if (opts_out->format == COPY_FORMAT_BINARY && opts_out->on_error != COPY_ON_ERROR_STOP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("only ON_ERROR STOP is allowed in BINARY mode"))); - if (opts_out->reject_limit && !opts_out->on_error) + if (opts_out->reject_limit && opts_out->on_error != COPY_ON_ERROR_IGNORE) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), /*- translator: first and second %s are the names of COPY option, e.g. diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c index fbbbc09a97b17..64ac3063c61a9 100644 --- a/src/backend/commands/copyfrom.c +++ b/src/backend/commands/copyfrom.c @@ -9,7 +9,7 @@ * Reading data from the input file or client and parsing it into Datums * is handled in copyfromparse.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -26,6 +26,7 @@ #include "access/heapam.h" #include "access/tableam.h" +#include "access/tupconvert.h" #include "access/xact.h" #include "catalog/namespace.h" #include "commands/copyapi.h" @@ -50,6 +51,7 @@ #include "utils/portal.h" #include "utils/rel.h" #include "utils/snapmgr.h" +#include "utils/typcache.h" /* * No more than this many tuples per CopyMultiInsertBuffer @@ -99,7 +101,7 @@ typedef struct CopyMultiInsertInfo CopyFromState cstate; /* Copy state for this CopyMultiInsertInfo */ EState *estate; /* Executor state used for COPY */ CommandId mycid; /* Command Id used for COPY */ - int ti_options; /* table insert options */ + uint32 ti_options; /* table insert options */ } CopyMultiInsertInfo; @@ -155,9 +157,9 @@ static const CopyFromRoutine CopyFromRoutineBinary = { static const CopyFromRoutine * CopyFromGetRoutine(const CopyFormatOptions *opts) { - if (opts->csv_mode) + if (opts->format == COPY_FORMAT_CSV) return &CopyFromRoutineCSV; - else if (opts->binary) + else if (opts->format == COPY_FORMAT_BINARY) return &CopyFromRoutineBinary; /* default is text */ @@ -261,7 +263,7 @@ CopyFromErrorCallback(void *arg) cstate->cur_relname); return; } - if (cstate->opts.binary) + if (cstate->opts.format == COPY_FORMAT_BINARY) { /* can't usefully display the data */ if (cstate->cur_attname) @@ -364,7 +366,7 @@ CopyMultiInsertBufferInit(ResultRelInfo *rri) { CopyMultiInsertBuffer *buffer; - buffer = (CopyMultiInsertBuffer *) palloc(sizeof(CopyMultiInsertBuffer)); + buffer = palloc_object(CopyMultiInsertBuffer); memset(buffer->slots, 0, sizeof(TupleTableSlot *) * MAX_BUFFERED_TUPLES); buffer->resultRelInfo = rri; buffer->bistate = (rri->ri_FdwRoutine == NULL) ? GetBulkInsertState() : NULL; @@ -399,7 +401,7 @@ CopyMultiInsertInfoSetupBuffer(CopyMultiInsertInfo *miinfo, static void CopyMultiInsertInfoInit(CopyMultiInsertInfo *miinfo, ResultRelInfo *rri, CopyFromState cstate, EState *estate, CommandId mycid, - int ti_options) + uint32 ti_options) { miinfo->multiInsertBuffers = NIL; miinfo->bufferedTuples = 0; @@ -533,7 +535,7 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo, else { CommandId mycid = miinfo->mycid; - int ti_options = miinfo->ti_options; + uint32 ti_options = miinfo->ti_options; bool line_buf_valid = cstate->line_buf_valid; uint64 save_cur_lineno = cstate->cur_lineno; MemoryContext oldcontext; @@ -572,8 +574,8 @@ CopyMultiInsertBufferFlush(CopyMultiInsertInfo *miinfo, cstate->cur_lineno = buffer->linenos[i]; recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - buffer->slots[i], estate, false, - false, NULL, NIL, false); + estate, 0, buffer->slots[i], + NIL, NULL); ExecARInsertTriggers(estate, resultRelInfo, slots[i], recheckIndexes, cstate->transition_capture); @@ -790,7 +792,7 @@ CopyFrom(CopyFromState cstate) PartitionTupleRouting *proute = NULL; ErrorContextCallback errcallback; CommandId mycid = GetCurrentCommandId(true); - int ti_options = 0; /* start with default options for insert */ + uint32 ti_options = 0; /* start with default options for insert */ BulkInsertState bistate = NULL; CopyInsertMethod insertMethod; CopyMultiInsertInfo multiInsertInfo = {0}; /* pacify compiler */ @@ -919,7 +921,7 @@ CopyFrom(CopyFromState cstate) ExecInitResultRelation(estate, resultRelInfo, 1); /* Verify the named relation is a valid target for INSERT */ - CheckValidResultRel(resultRelInfo, CMD_INSERT, NIL); + CheckValidResultRel(resultRelInfo, CMD_INSERT, ONCONFLICT_NONE, NIL); ExecOpenIndices(resultRelInfo, false); @@ -1429,13 +1431,9 @@ CopyFrom(CopyFromState cstate) if (resultRelInfo->ri_NumIndices > 0) recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - myslot, - estate, - false, - false, - NULL, - NIL, - false); + estate, 0, + myslot, NIL, + NULL); } /* AFTER ROW INSERT Triggers */ @@ -1467,14 +1465,22 @@ CopyFrom(CopyFromState cstate) /* Done, clean up */ error_context_stack = errcallback.previous; - if (cstate->opts.on_error != COPY_ON_ERROR_STOP && - cstate->num_errors > 0 && + if (cstate->num_errors > 0 && cstate->opts.log_verbosity >= COPY_LOG_VERBOSITY_DEFAULT) - ereport(NOTICE, - errmsg_plural("%" PRIu64 " row was skipped due to data type incompatibility", - "%" PRIu64 " rows were skipped due to data type incompatibility", - cstate->num_errors, - cstate->num_errors)); + { + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + ereport(NOTICE, + errmsg_plural("%" PRIu64 " row was skipped due to data type incompatibility", + "%" PRIu64 " rows were skipped due to data type incompatibility", + cstate->num_errors, + cstate->num_errors)); + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + ereport(NOTICE, + errmsg_plural("in %" PRIu64 " row, columns were set to null due to data type incompatibility", + "in %" PRIu64 " rows, columns were set to null due to data type incompatibility", + cstate->num_errors, + cstate->num_errors)); + } if (bistate != NULL) FreeBulkInsertState(bistate); @@ -1558,7 +1564,7 @@ BeginCopyFrom(ParseState *pstate, }; /* Allocate workspace and zero all fields */ - cstate = (CopyFromStateData *) palloc0(sizeof(CopyFromStateData)); + cstate = palloc0_object(CopyFromStateData); /* * We allocate everything used by a cstate in a new memory context. This @@ -1621,16 +1627,37 @@ BeginCopyFrom(ParseState *pstate, cstate->escontext->type = T_ErrorSaveContext; cstate->escontext->error_occurred = false; - /* - * Currently we only support COPY_ON_ERROR_IGNORE. We'll add other - * options later - */ - if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE || + cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) cstate->escontext->details_wanted = false; } else cstate->escontext = NULL; + if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + { + int attr_count = list_length(cstate->attnumlist); + + /* + * When data type conversion fails and ON_ERROR is SET_NULL, we need + * ensure that the input column allow null values. ExecConstraints() + * will cover most of the cases, but it does not verify domain + * constraints. Therefore, for constrained domains, the null value + * check must be performed during the initial string-to-datum + * conversion (see CopyFromTextLikeOneRow()). + */ + cstate->domain_with_constraint = palloc0_array(bool, attr_count); + + foreach_int(attno, cstate->attnumlist) + { + int i = foreach_current_index(attno); + + Form_pg_attribute att = TupleDescAttr(tupDesc, attno - 1); + + cstate->domain_with_constraint[i] = DomainHasConstraints(att->atttypid, NULL); + } + } + /* Convert FORCE_NULL name list to per-column flags, check validity */ cstate->opts.force_null_flags = (bool *) palloc0(num_phys_attrs * sizeof(bool)); if (cstate->opts.force_null_all) @@ -1720,6 +1747,7 @@ BeginCopyFrom(ParseState *pstate, cstate->cur_attname = NULL; cstate->cur_attval = NULL; cstate->relname_only = false; + cstate->simd_enabled = true; /* * Allocate buffers for the input pipeline. diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c index f5fc346e2013b..65fd5a0ab4f9d 100644 --- a/src/backend/commands/copyfromparse.c +++ b/src/backend/commands/copyfromparse.c @@ -47,7 +47,7 @@ * and 'attribute_buf' are expanded on demand, to hold the longest line * encountered so far. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -71,9 +71,12 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "pgstat.h" +#include "port/pg_bitutils.h" #include "port/pg_bswap.h" +#include "port/simd.h" #include "utils/builtins.h" #include "utils/rel.h" +#include "utils/wait_event.h" #define ISOCTAL(c) (((c) >= '0') && ((c) <= '7')) #define OCTVALUE(c) ((c) - '0') @@ -141,7 +144,8 @@ static const char BinarySignature[11] = "PGCOPY\n\377\r\n\0"; /* non-export function prototypes */ static bool CopyReadLine(CopyFromState cstate, bool is_csv); -static bool CopyReadLineText(CopyFromState cstate, bool is_csv); +static pg_attribute_always_inline bool CopyReadLineText(CopyFromState cstate, + bool is_csv); static int CopyReadAttributesText(CopyFromState cstate); static int CopyReadAttributesCSV(CopyFromState cstate); static Datum CopyReadBinaryAttribute(CopyFromState cstate, FmgrInfo *flinfo, @@ -171,7 +175,7 @@ ReceiveCopyBegin(CopyFromState cstate) { StringInfoData buf; int natts = list_length(cstate->attnumlist); - int16 format = (cstate->opts.binary ? 1 : 0); + int16 format = (cstate->opts.format == COPY_FORMAT_BINARY ? 1 : 0); int i; pq_beginmessage(&buf, PqMsg_CopyInResponse); @@ -249,7 +253,9 @@ CopyGetData(CopyFromState cstate, void *databuf, int minread, int maxread) switch (cstate->copy_src) { case COPY_FILE: + pgstat_report_wait_start(WAIT_EVENT_COPY_FROM_READ); bytesread = fread(databuf, 1, maxread, cstate->copy_file); + pgstat_report_wait_end(); if (ferror(cstate->copy_file)) ereport(ERROR, (errcode_for_file_access(), @@ -335,7 +341,7 @@ CopyGetData(CopyFromState cstate, void *databuf, int minread, int maxread) if (avail > maxread) avail = maxread; pq_copymsgbytes(cstate->fe_msgbuf, databuf, avail); - databuf = (void *) ((char *) databuf + avail); + databuf = (char *) databuf + avail; maxread -= avail; bytesread += avail; } @@ -747,7 +753,7 @@ bool NextCopyFromRawFields(CopyFromState cstate, char ***fields, int *nfields) { return NextCopyFromRawFieldsInternal(cstate, fields, nfields, - cstate->opts.csv_mode); + cstate->opts.format == COPY_FORMAT_CSV); } /* @@ -771,21 +777,31 @@ static pg_attribute_always_inline bool NextCopyFromRawFieldsInternal(CopyFromState cstate, char ***fields, int *nfields, bool is_csv) { int fldct; - bool done; + bool done = false; /* only available for text or csv input */ - Assert(!cstate->opts.binary); + Assert(cstate->opts.format == COPY_FORMAT_TEXT || + cstate->opts.format == COPY_FORMAT_CSV); /* on input check that the header line is correct if needed */ - if (cstate->cur_lineno == 0 && cstate->opts.header_line) + if (cstate->cur_lineno == 0 && cstate->opts.header_line != COPY_HEADER_FALSE) { ListCell *cur; TupleDesc tupDesc; + int lines_to_skip = cstate->opts.header_line; + + /* If set to "match", one header line is skipped */ + if (cstate->opts.header_line == COPY_HEADER_MATCH) + lines_to_skip = 1; tupDesc = RelationGetDescr(cstate->rel); - cstate->cur_lineno++; - done = CopyReadLine(cstate, is_csv); + for (int i = 0; i < lines_to_skip; i++) + { + cstate->cur_lineno++; + if ((done = CopyReadLine(cstate, is_csv))) + break; + } if (cstate->opts.header_line == COPY_HEADER_MATCH) { @@ -947,6 +963,7 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, int fldct; int fieldno; char *string; + bool current_row_erroneous = false; tupDesc = RelationGetDescr(cstate->rel); attr_count = list_length(cstate->attnumlist); @@ -1024,7 +1041,7 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, } /* - * If ON_ERROR is specified with IGNORE, skip rows with soft errors + * If ON_ERROR is specified, handle the different options */ else if (!InputFunctionCallSafe(&in_functions[m], string, @@ -1035,7 +1052,55 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, { Assert(cstate->opts.on_error != COPY_ON_ERROR_STOP); - cstate->num_errors++; + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + cstate->num_errors++; + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + { + /* + * Reset error state so the subsequent InputFunctionCallSafe + * call (for domain constraint check) can properly report + * whether it succeeded or failed. + */ + cstate->escontext->error_occurred = false; + + Assert(cstate->domain_with_constraint != NULL); + + /* + * For constrained domains, we need an additional + * InputFunctionCallSafe() to ensure that an error is thrown + * if the domain constraint rejects null values. + */ + if (!cstate->domain_with_constraint[m] || + InputFunctionCallSafe(&in_functions[m], + NULL, + typioparams[m], + att->atttypmod, + (Node *) cstate->escontext, + &values[m])) + { + nulls[m] = true; + values[m] = (Datum) 0; + } + else + ereport(ERROR, + errcode(ERRCODE_NOT_NULL_VIOLATION), + errmsg("domain %s does not allow null values", + format_type_be(typioparams[m])), + errdetail("ON_ERROR SET_NULL cannot be applied because column \"%s\" (domain %s) does not accept null values.", + cstate->cur_attname, + format_type_be(typioparams[m])), + errdatatype(typioparams[m])); + + /* + * We count only the number of rows (not fields) where + * ON_ERROR SET_NULL was applied. + */ + if (!current_row_erroneous) + { + current_row_erroneous = true; + cstate->num_errors++; + } + } if (cstate->opts.log_verbosity == COPY_LOG_VERBOSITY_VERBOSE) { @@ -1052,24 +1117,37 @@ CopyFromTextLikeOneRow(CopyFromState cstate, ExprContext *econtext, char *attval; attval = CopyLimitPrintoutLength(cstate->cur_attval); - ereport(NOTICE, - errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": \"%s\"", - cstate->cur_lineno, - cstate->cur_attname, - attval)); + + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + ereport(NOTICE, + errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": \"%s\"", + cstate->cur_lineno, + cstate->cur_attname, + attval)); + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + ereport(NOTICE, + errmsg("setting to null due to data type incompatibility at line %" PRIu64 " for column \"%s\": \"%s\"", + cstate->cur_lineno, + cstate->cur_attname, + attval)); pfree(attval); } else - ereport(NOTICE, - errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": null input", - cstate->cur_lineno, - cstate->cur_attname)); - + { + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + ereport(NOTICE, + errmsg("skipping row due to data type incompatibility at line %" PRIu64 " for column \"%s\": null input", + cstate->cur_lineno, + cstate->cur_attname)); + } /* reset relname_only */ cstate->relname_only = false; } - return true; + if (cstate->opts.on_error == COPY_ON_ERROR_IGNORE) + return true; + else if (cstate->opts.on_error == COPY_ON_ERROR_SET_NULL) + continue; } cstate->cur_attname = NULL; @@ -1127,7 +1205,7 @@ CopyFromBinaryOneRow(CopyFromState cstate, ExprContext *econtext, Datum *values, ereport(ERROR, (errcode(ERRCODE_BAD_COPY_FILE_FORMAT), errmsg("row field count is %d, expected %d", - (int) fld_count, attr_count))); + fld_count, attr_count))); foreach(cur, cstate->attnumlist) { @@ -1162,8 +1240,17 @@ CopyReadLine(CopyFromState cstate, bool is_csv) resetStringInfo(&cstate->line_buf); cstate->line_buf_valid = false; - /* Parse data and transfer into line_buf */ - result = CopyReadLineText(cstate, is_csv); + /* + * Parse data and transfer into line_buf. + * + * Because this is performance critical, we inline CopyReadLineText() and + * pass the boolean parameters as constants to allow the compiler to emit + * specialized code with fewer branches. + */ + if (is_csv) + result = CopyReadLineText(cstate, true); + else + result = CopyReadLineText(cstate, false); if (result) { @@ -1227,10 +1314,156 @@ CopyReadLine(CopyFromState cstate, bool is_csv) return result; } +#ifndef USE_NO_SIMD /* - * CopyReadLineText - inner loop of CopyReadLine for text mode + * Helper function for CopyReadLineText() that uses SIMD instructions to scan + * the input buffer for special characters. This can be much faster. + * + * Note that we disable SIMD for the remainder of the COPY FROM command upon + * encountering a special character (except for end-of-line characters) or a + * short line. This is perhaps too conservative, but it should help avoid + * regressions. It could probably be made more lenient in the future via + * fine-tuned heuristics. */ static bool +CopyReadLineTextSIMDHelper(CopyFromState cstate, bool is_csv, + bool *hit_eof_p, int *input_buf_ptr_p) +{ + char *copy_input_buf; + int input_buf_ptr; + int copy_buf_len; + bool unique_esc_char; /* for csv, do quote/esc chars differ? */ + bool first = true; + bool result = false; + const Vector8 nl_vec = vector8_broadcast('\n'); + const Vector8 cr_vec = vector8_broadcast('\r'); + Vector8 bs_or_quote_vec; /* '\' for text, quote for csv */ + Vector8 esc_vec; /* only for csv */ + + if (is_csv) + { + char quote = cstate->opts.quote[0]; + char esc = cstate->opts.escape[0]; + + bs_or_quote_vec = vector8_broadcast(quote); + esc_vec = vector8_broadcast(esc); + unique_esc_char = (quote != esc); + } + else + { + bs_or_quote_vec = vector8_broadcast('\\'); + unique_esc_char = false; + } + + /* + * For a little extra speed within the loop, we copy some state members + * into local variables. Note that we need to use a separate local + * variable for input_buf_ptr so that the REFILL_LINEBUF macro works. We + * copy its value into the input_buf_ptr_p argument before returning. + */ + copy_input_buf = cstate->input_buf; + input_buf_ptr = cstate->input_buf_index; + copy_buf_len = cstate->input_buf_len; + + /* + * See the corresponding loop in CopyReadLineText() for more information + * about the purpose of this loop. This one does the same thing using + * SIMD instructions, although we are quick to bail out to the scalar path + * if we encounter a special character. + */ + for (;;) + { + Vector8 chunk; + Vector8 match; + + /* Load more data if needed. */ + if (copy_buf_len - input_buf_ptr < sizeof(Vector8)) + { + REFILL_LINEBUF; + + CopyLoadInputBuf(cstate); + /* update our local variables */ + *hit_eof_p = cstate->input_reached_eof; + input_buf_ptr = cstate->input_buf_index; + copy_buf_len = cstate->input_buf_len; + + /* + * If we are completely out of data, break out of the loop, + * reporting EOF. + */ + if (INPUT_BUF_BYTES(cstate) <= 0) + { + result = true; + break; + } + } + + /* + * If we still don't have enough data for the SIMD path, fall back to + * the scalar code. Note that this doesn't necessarily mean we + * encountered a short line, so we leave cstate->simd_enabled set to + * true. + */ + if (copy_buf_len - input_buf_ptr < sizeof(Vector8)) + break; + + /* + * If we made it here, we have at least enough data to fit in a + * Vector8, so we can use SIMD instructions to scan for special + * characters. + */ + vector8_load(&chunk, (const uint8 *) ©_input_buf[input_buf_ptr]); + + /* + * Check for \n, \r, \\ (for text), quotes (for csv), and escapes (for + * csv, if different from quotes). + */ + match = vector8_eq(chunk, nl_vec); + match = vector8_or(match, vector8_eq(chunk, cr_vec)); + match = vector8_or(match, vector8_eq(chunk, bs_or_quote_vec)); + if (unique_esc_char) + match = vector8_or(match, vector8_eq(chunk, esc_vec)); + + /* + * If we found a special character, advance to it and hand off to the + * scalar path. Except for end-of-line characters, we also disable + * SIMD processing for the remainder of the COPY FROM command. + */ + if (vector8_is_highbit_set(match)) + { + uint32 mask; + char c; + + mask = vector8_highbit_mask(match); + input_buf_ptr += pg_rightmost_one_pos32(mask); + + /* + * Don't disable SIMD if we found \n or \r, else we'd stop using + * SIMD instructions after the first line. As an exception, we do + * disable it if this is the first vector we processed, as that + * means the line is too short for SIMD. + */ + c = copy_input_buf[input_buf_ptr]; + if (first || (c != '\n' && c != '\r')) + cstate->simd_enabled = false; + + break; + } + + /* That chunk was clear of special characters, so we can skip it. */ + input_buf_ptr += sizeof(Vector8); + first = false; + } + + *input_buf_ptr_p = input_buf_ptr; + return result; +} +#endif /* ! USE_NO_SIMD */ + +/* + * CopyReadLineText - inner loop of CopyReadLine for text mode + */ +static pg_attribute_always_inline bool CopyReadLineText(CopyFromState cstate, bool is_csv) { char *copy_input_buf; @@ -1277,11 +1510,43 @@ CopyReadLineText(CopyFromState cstate, bool is_csv) * input_buf_ptr have been determined to be part of the line, but not yet * transferred to line_buf. * - * For a little extra speed within the loop, we copy input_buf and - * input_buf_len into local variables. + * For a little extra speed within the loop, we copy some state + * information into local variables. input_buf_ptr could be changed in + * the SIMD path, so we must set that one before it. The others are set + * afterwards. */ - copy_input_buf = cstate->input_buf; input_buf_ptr = cstate->input_buf_index; + + /* + * We first try to use SIMD for the task described above, falling back to + * the scalar path (i.e., the loop below) if needed. + */ +#ifndef USE_NO_SIMD + if (cstate->simd_enabled) + { + /* + * Using temporary variables seems to encourage the compiler to keep + * them in a register, which is beneficial for performance. + */ + bool tmp_hit_eof = false; + int tmp_input_buf_ptr = 0; /* silence compiler warning */ + + result = CopyReadLineTextSIMDHelper(cstate, is_csv, &tmp_hit_eof, + &tmp_input_buf_ptr); + hit_eof = tmp_hit_eof; + input_buf_ptr = tmp_input_buf_ptr; + + if (result) + { + /* Transfer any still-uncopied data to line_buf. */ + REFILL_LINEBUF; + + return result; + } + } +#endif /* ! USE_NO_SIMD */ + + copy_input_buf = cstate->input_buf; copy_buf_len = cstate->input_buf_len; for (;;) @@ -1538,7 +1803,7 @@ GetDecimalFromHex(char hex) if (isdigit((unsigned char) hex)) return hex - '0'; else - return tolower((unsigned char) hex) - 'a' + 10; + return pg_ascii_tolower((unsigned char) hex) - 'a' + 10; } /* diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c index ea6f18f2c8008..85d15353647a6 100644 --- a/src/backend/commands/copyto.c +++ b/src/backend/commands/copyto.c @@ -3,7 +3,7 @@ * copyto.c * COPY TO file/program/client * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,12 +18,16 @@ #include #include +#include "access/table.h" #include "access/tableam.h" +#include "access/tupconvert.h" +#include "catalog/pg_inherits.h" #include "commands/copyapi.h" #include "commands/progress.h" #include "executor/execdesc.h" #include "executor/executor.h" #include "executor/tuptable.h" +#include "funcapi.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "mb/pg_wchar.h" @@ -31,10 +35,12 @@ #include "pgstat.h" #include "storage/fd.h" #include "tcop/tcopprot.h" +#include "utils/json.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" #include "utils/snapmgr.h" +#include "utils/wait_event.h" /* * Represents the different dest cases we need to worry about at @@ -82,10 +88,19 @@ typedef struct CopyToStateData List *attnumlist; /* integer list of attnums to copy */ char *filename; /* filename, or NULL for STDOUT */ bool is_program; /* is 'filename' a program to popen? */ + bool json_row_delim_needed; /* need delimiter before next row */ + StringInfo json_buf; /* reusable buffer for JSON output, + * initialized in BeginCopyTo */ + TupleDesc tupDesc; /* Descriptor for JSON output; for a column + * list this is a projected descriptor */ + Datum *json_projvalues; /* pre-allocated projection values, or + * NULL */ + bool *json_projnulls; /* pre-allocated projection nulls, or NULL */ copy_data_dest_cb data_dest_cb; /* function for writing data */ CopyFormatOptions opts; Node *whereClause; /* WHERE condition (or NULL) */ + List *partitions; /* OID list of partitions to copy data from */ /* * Working state @@ -116,6 +131,8 @@ static void CopyOneRowTo(CopyToState cstate, TupleTableSlot *slot); static void CopyAttributeOutText(CopyToState cstate, const char *string); static void CopyAttributeOutCSV(CopyToState cstate, const char *string, bool use_quote); +static void CopyRelationTo(CopyToState cstate, Relation rel, Relation root_rel, + uint64 *processed); /* built-in format-specific routines */ static void CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc); @@ -125,6 +142,8 @@ static void CopyToCSVOneRow(CopyToState cstate, TupleTableSlot *slot); static void CopyToTextLikeOneRow(CopyToState cstate, TupleTableSlot *slot, bool is_csv); static void CopyToTextLikeEnd(CopyToState cstate); +static void CopyToJsonOneRow(CopyToState cstate, TupleTableSlot *slot); +static void CopyToJsonEnd(CopyToState cstate); static void CopyToBinaryStart(CopyToState cstate, TupleDesc tupDesc); static void CopyToBinaryOutFunc(CopyToState cstate, Oid atttypid, FmgrInfo *finfo); static void CopyToBinaryOneRow(CopyToState cstate, TupleTableSlot *slot); @@ -143,9 +162,6 @@ static void CopySendInt16(CopyToState cstate, int16 val); /* * COPY TO routines for built-in formats. - * - * CSV and text formats share the same TextLike routines except for the - * one-row callback. */ /* text format */ @@ -164,6 +180,14 @@ static const CopyToRoutine CopyToRoutineCSV = { .CopyToEnd = CopyToTextLikeEnd, }; +/* json format */ +static const CopyToRoutine CopyToRoutineJson = { + .CopyToStart = CopyToTextLikeStart, + .CopyToOutFunc = CopyToTextLikeOutFunc, + .CopyToOneRow = CopyToJsonOneRow, + .CopyToEnd = CopyToJsonEnd, +}; + /* binary format */ static const CopyToRoutine CopyToRoutineBinary = { .CopyToStart = CopyToBinaryStart, @@ -176,16 +200,18 @@ static const CopyToRoutine CopyToRoutineBinary = { static const CopyToRoutine * CopyToGetRoutine(const CopyFormatOptions *opts) { - if (opts->csv_mode) + if (opts->format == COPY_FORMAT_CSV) return &CopyToRoutineCSV; - else if (opts->binary) + else if (opts->format == COPY_FORMAT_BINARY) return &CopyToRoutineBinary; + else if (opts->format == COPY_FORMAT_JSON) + return &CopyToRoutineJson; /* default is text */ return &CopyToRoutineText; } -/* Implementation of the start callback for text and CSV formats */ +/* Implementation of the start callback for text, CSV, and json formats */ static void CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc) { @@ -199,11 +225,13 @@ CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc) cstate->file_encoding); /* if a header has been requested send the line */ - if (cstate->opts.header_line) + if (cstate->opts.header_line == COPY_HEADER_TRUE) { ListCell *cur; bool hdr_delim = false; + Assert(cstate->opts.format != COPY_FORMAT_JSON); + foreach(cur, cstate->attnumlist) { int attnum = lfirst_int(cur); @@ -215,7 +243,7 @@ CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc) colname = NameStr(TupleDescAttr(tupDesc, attnum - 1)->attname); - if (cstate->opts.csv_mode) + if (cstate->opts.format == COPY_FORMAT_CSV) CopyAttributeOutCSV(cstate, colname, false); else CopyAttributeOutText(cstate, colname); @@ -223,10 +251,19 @@ CopyToTextLikeStart(CopyToState cstate, TupleDesc tupDesc) CopySendTextLikeEndOfRow(cstate); } + + /* + * If FORCE_ARRAY has been specified, send the opening bracket. + */ + if (cstate->opts.format == COPY_FORMAT_JSON && cstate->opts.force_array) + { + CopySendChar(cstate, '['); + CopySendTextLikeEndOfRow(cstate); + } } /* - * Implementation of the outfunc callback for text and CSV formats. Assign + * Implementation of the outfunc callback for text, CSV, and json formats. Assign * the output function data to the given *finfo. */ static void @@ -306,6 +343,95 @@ CopyToTextLikeEnd(CopyToState cstate) /* Nothing to do here */ } +/* Implementation of the end callback for json format */ +static void +CopyToJsonEnd(CopyToState cstate) +{ + if (cstate->opts.force_array) + { + CopySendChar(cstate, ']'); + CopySendTextLikeEndOfRow(cstate); + } +} + +/* Implementation of per-row callback for json format */ +static void +CopyToJsonOneRow(CopyToState cstate, TupleTableSlot *slot) +{ + Datum rowdata; + + resetStringInfo(cstate->json_buf); + + if (cstate->json_projvalues != NULL) + { + /* + * Column list case: project selected column values into sequential + * positions matching the custom TupleDesc, then form a new tuple. + */ + HeapTuple tup; + int i = 0; + + foreach_int(attnum, cstate->attnumlist) + { + cstate->json_projvalues[i] = slot->tts_values[attnum - 1]; + cstate->json_projnulls[i] = slot->tts_isnull[attnum - 1]; + i++; + } + + tup = heap_form_tuple(cstate->tupDesc, + cstate->json_projvalues, + cstate->json_projnulls); + + /* + * heap_form_tuple already stamps the datum-length, type-id, and + * type-mod fields on t_data, so we can use it directly as a composite + * Datum without the extra pallocmemcpy that heap_copy_tuple_as_datum + * would do. Any TOAST pointers in the projected values will be + * detoasted by the per-column output functions called from + * composite_to_json. + */ + rowdata = HeapTupleGetDatum(tup); + } + else + { + /* + * Full table or query without column list. For queries, the slot's + * TupleDesc may carry RECORDOID, which is not registered in the type + * cache and would cause composite_to_json's lookup_rowtype_tupdesc + * call to fail. Build a HeapTuple stamped with the blessed + * descriptor so the type can be looked up correctly. + */ + if (!cstate->rel && slot->tts_tupleDescriptor->tdtypeid == RECORDOID) + { + HeapTuple tup = heap_form_tuple(cstate->tupDesc, + slot->tts_values, + slot->tts_isnull); + + rowdata = HeapTupleGetDatum(tup); + } + else + rowdata = ExecFetchSlotHeapTupleDatum(slot); + } + + composite_to_json(rowdata, cstate->json_buf, false); + + if (cstate->opts.force_array) + { + if (cstate->json_row_delim_needed) + CopySendChar(cstate, ','); + else + { + /* first row needs no delimiter */ + CopySendChar(cstate, ' '); + cstate->json_row_delim_needed = true; + } + } + + CopySendData(cstate, cstate->json_buf->data, cstate->json_buf->len); + + CopySendTextLikeEndOfRow(cstate); +} + /* * Implementation of the start callback for binary format. Send a header * for a binary copy. @@ -392,14 +518,28 @@ SendCopyBegin(CopyToState cstate) { StringInfoData buf; int natts = list_length(cstate->attnumlist); - int16 format = (cstate->opts.binary ? 1 : 0); + int16 format = (cstate->opts.format == COPY_FORMAT_BINARY ? 1 : 0); int i; pq_beginmessage(&buf, PqMsg_CopyOutResponse); pq_sendbyte(&buf, format); /* overall format */ - pq_sendint16(&buf, natts); - for (i = 0; i < natts; i++) - pq_sendint16(&buf, format); /* per-column formats */ + if (cstate->opts.format != COPY_FORMAT_JSON) + { + pq_sendint16(&buf, natts); + for (i = 0; i < natts; i++) + pq_sendint16(&buf, format); /* per-column formats */ + } + else + { + /* + * For JSON format, report one text-format column. Each CopyData + * message contains one complete JSON object, not individual column + * values, so the per-column count is always 1. + */ + pq_sendint16(&buf, 1); + pq_sendint16(&buf, 0); + } + pq_endmessage(&buf); cstate->copy_dest = COPY_FRONTEND; } @@ -449,6 +589,7 @@ CopySendEndOfRow(CopyToState cstate) switch (cstate->copy_dest) { case COPY_FILE: + pgstat_report_wait_start(WAIT_EVENT_COPY_TO_WRITE); if (fwrite(fe_msgbuf->data, fe_msgbuf->len, 1, cstate->copy_file) != 1 || ferror(cstate->copy_file)) @@ -481,6 +622,7 @@ CopySendEndOfRow(CopyToState cstate) (errcode_for_file_access(), errmsg("could not write to COPY file: %m"))); } + pgstat_report_wait_end(); break; case COPY_FRONTEND: /* Dump the accumulated row as one CopyData message */ @@ -499,7 +641,7 @@ CopySendEndOfRow(CopyToState cstate) } /* - * Wrapper function of CopySendEndOfRow for text and CSV formats. Sends the + * Wrapper function of CopySendEndOfRow for text, CSV, and json formats. Sends the * line termination and do common appropriate things for the end of row. */ static inline void @@ -581,7 +723,7 @@ ClosePipeToProgram(CopyToState cstate) } /* - * Release resources allocated in a cstate for COPY TO/FROM. + * Release resources allocated in a cstate for COPY TO. */ static void EndCopy(CopyToState cstate) @@ -602,6 +744,10 @@ EndCopy(CopyToState cstate) pgstat_progress_end_command(); MemoryContextDelete(cstate->copycontext); + + if (cstate->partitions) + list_free(cstate->partitions); + pfree(cstate); } @@ -643,6 +789,7 @@ BeginCopyTo(ParseState *pstate, PROGRESS_COPY_COMMAND_TO, 0 }; + List *children = NIL; if (rel != NULL && rel->rd_rel->relkind != RELKIND_RELATION) { @@ -673,11 +820,34 @@ BeginCopyTo(ParseState *pstate, errmsg("cannot copy from sequence \"%s\"", RelationGetRelationName(rel)))); else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot copy from partitioned table \"%s\"", - RelationGetRelationName(rel)), - errhint("Try the COPY (SELECT ...) TO variant."))); + { + /* + * Collect OIDs of relation containing data, so that later + * DoCopyTo can copy the data from them. + */ + children = find_all_inheritors(RelationGetRelid(rel), AccessShareLock, NULL); + + foreach_oid(child, children) + { + char relkind = get_rel_relkind(child); + + if (relkind == RELKIND_FOREIGN_TABLE) + { + char *relation_name = get_rel_name(child); + + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot copy from foreign table \"%s\"", relation_name), + errdetail("Partition \"%s\" is a foreign table in partitioned table \"%s\"", + relation_name, RelationGetRelationName(rel)), + errhint("Try the COPY (SELECT ...) TO variant.")); + } + + /* Exclude tables with no data */ + if (RELKIND_HAS_PARTITIONS(relkind)) + children = foreach_delete_current(children, child); + } + } else ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -687,7 +857,7 @@ BeginCopyTo(ParseState *pstate, /* Allocate workspace and zero all fields */ - cstate = (CopyToStateData *) palloc0(sizeof(CopyToStateData)); + cstate = palloc0_object(CopyToStateData); /* * We allocate everything used by a cstate in a new memory context. This @@ -713,6 +883,8 @@ BeginCopyTo(ParseState *pstate, cstate->rel = rel; tupDesc = RelationGetDescr(cstate->rel); + cstate->partitions = children; + cstate->tupDesc = tupDesc; } else { @@ -722,6 +894,7 @@ BeginCopyTo(ParseState *pstate, DestReceiver *dest; cstate->rel = NULL; + cstate->partitions = NIL; /* * Run parse analysis and rewrite. Note this also acquires sufficient @@ -796,7 +969,7 @@ BeginCopyTo(ParseState *pstate, /* plan the query */ plan = pg_plan_query(query, pstate->p_sourcetext, - CURSOR_OPT_PARALLEL_OK, NULL); + CURSOR_OPT_PARALLEL_OK, NULL, NULL); /* * With row-level security and a user using "COPY relation TO", we @@ -848,11 +1021,53 @@ BeginCopyTo(ParseState *pstate, ExecutorStart(cstate->queryDesc, 0); tupDesc = cstate->queryDesc->tupDesc; + tupDesc = BlessTupleDesc(tupDesc); + cstate->tupDesc = tupDesc; } /* Generate or convert list of attributes to process */ cstate->attnumlist = CopyGetAttnums(tupDesc, cstate->rel, attnamelist); + /* Set up JSON-specific state */ + if (cstate->opts.format == COPY_FORMAT_JSON) + { + cstate->json_buf = makeStringInfo(); + + if (rel && list_length(cstate->attnumlist) < tupDesc->natts) + { + int natts = list_length(cstate->attnumlist); + TupleDesc resultDesc; + + /* + * Build a TupleDesc describing only the selected columns so that + * composite_to_json() emits the right column names and types. + */ + resultDesc = CreateTemplateTupleDesc(natts); + + foreach_int(attnum, cstate->attnumlist) + { + Form_pg_attribute attr = TupleDescAttr(tupDesc, attnum - 1); + + TupleDescInitEntry(resultDesc, + foreach_current_index(attnum) + 1, + NameStr(attr->attname), + attr->atttypid, + attr->atttypmod, + attr->attndims); + } + + TupleDescFinalize(resultDesc); + cstate->tupDesc = BlessTupleDesc(resultDesc); + + /* + * Pre-allocate arrays for projecting selected column values into + * sequential positions matching the custom TupleDesc. + */ + cstate->json_projvalues = palloc_array(Datum, natts); + cstate->json_projnulls = palloc_array(bool, natts); + } + } + num_phys_attrs = tupDesc->natts; /* Convert FORCE_QUOTE name list to per-column flags, check validity */ @@ -1030,7 +1245,7 @@ DoCopyTo(CopyToState cstate) TupleDesc tupDesc; int num_phys_attrs; ListCell *cur; - uint64 processed; + uint64 processed = 0; if (fe_copy) SendCopyBegin(cstate); @@ -1070,33 +1285,24 @@ DoCopyTo(CopyToState cstate) if (cstate->rel) { - TupleTableSlot *slot; - TableScanDesc scandesc; - - scandesc = table_beginscan(cstate->rel, GetActiveSnapshot(), 0, NULL); - slot = table_slot_create(cstate->rel, NULL); - - processed = 0; - while (table_scan_getnextslot(scandesc, ForwardScanDirection, slot)) + /* + * If COPY TO source table is a partitioned table, then open each + * partition and process each individual partition. + */ + if (cstate->rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - CHECK_FOR_INTERRUPTS(); - - /* Deconstruct the tuple ... */ - slot_getallattrs(slot); - - /* Format and send the data */ - CopyOneRowTo(cstate, slot); + foreach_oid(child, cstate->partitions) + { + Relation scan_rel; - /* - * Increment the number of processed tuples, and report the - * progress. - */ - pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED, - ++processed); + /* We already got the lock in BeginCopyTo */ + scan_rel = table_open(child, NoLock); + CopyRelationTo(cstate, scan_rel, cstate->rel, &processed); + table_close(scan_rel, NoLock); + } } - - ExecDropSingleTupleTableSlot(slot); - table_endscan(scandesc); + else + CopyRelationTo(cstate, cstate->rel, NULL, &processed); } else { @@ -1115,6 +1321,74 @@ DoCopyTo(CopyToState cstate) return processed; } +/* + * Scans a single table and exports its rows to the COPY destination. + * + * root_rel can be set to the root table of rel if rel is a partition + * table so that we can send tuples in root_rel's rowtype, which might + * differ from individual partitions. +*/ +static void +CopyRelationTo(CopyToState cstate, Relation rel, Relation root_rel, uint64 *processed) +{ + TupleTableSlot *slot; + TableScanDesc scandesc; + AttrMap *map = NULL; + TupleTableSlot *root_slot = NULL; + + scandesc = table_beginscan(rel, GetActiveSnapshot(), 0, NULL, + SO_NONE); + slot = table_slot_create(rel, NULL); + + /* + * If we are exporting partition data here, we check if converting tuples + * to the root table's rowtype, because a partition might have column + * order different than its root table. + */ + if (root_rel != NULL) + { + root_slot = table_slot_create(root_rel, NULL); + map = build_attrmap_by_name_if_req(RelationGetDescr(root_rel), + RelationGetDescr(rel), + false); + } + + while (table_scan_getnextslot(scandesc, ForwardScanDirection, slot)) + { + TupleTableSlot *copyslot; + + CHECK_FOR_INTERRUPTS(); + + if (map != NULL) + copyslot = execute_attr_map_slot(map, slot, root_slot); + else + { + /* Deconstruct the tuple */ + slot_getallattrs(slot); + copyslot = slot; + } + + /* Format and send the data */ + CopyOneRowTo(cstate, copyslot); + + /* + * Increment the number of processed tuples, and report the progress. + */ + pgstat_progress_update_param(PROGRESS_COPY_TUPLES_PROCESSED, + ++(*processed)); + } + + ExecDropSingleTupleTableSlot(slot); + + if (root_slot != NULL) + ExecDropSingleTupleTableSlot(root_slot); + + if (map != NULL) + free_attrmap(map); + + table_endscan(scandesc); +} + /* * Emit one row during DoCopyTo(). */ @@ -1434,7 +1708,7 @@ copy_dest_destroy(DestReceiver *self) DestReceiver * CreateCopyDestReceiver(void) { - DR_copy *self = (DR_copy *) palloc(sizeof(DR_copy)); + DR_copy *self = palloc_object(DR_copy); self->pub.receiveSlot = copy_dest_receive; self->pub.rStartup = copy_dest_startup; diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c index dfd2ab8e8628c..6dbb831ca890d 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -13,7 +13,7 @@ * we must return a tuples-processed count in the QueryCompletion. (We no * longer do that for CTAS ... WITH NO DATA, however.) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -56,7 +56,7 @@ typedef struct Relation rel; /* relation to write to */ ObjectAddress reladdr; /* address of rel, for ExecCreateTableAs */ CommandId output_cid; /* cmin to insert in output tuples */ - int ti_options; /* table_tuple_insert performance options */ + uint32 ti_options; /* table_tuple_insert performance options */ BulkInsertState bistate; /* bulk insert state */ } DR_intorel; @@ -321,7 +321,7 @@ ExecCreateTableAs(ParseState *pstate, CreateTableAsStmt *stmt, /* plan the query */ plan = pg_plan_query(query, pstate->p_sourcetext, - CURSOR_OPT_PARALLEL_OK, params); + CURSOR_OPT_PARALLEL_OK, params, NULL); /* * Use a snapshot with an updated command ID to ensure this query sees @@ -439,7 +439,7 @@ CreateTableAsRelExists(CreateTableAsStmt *ctas) DestReceiver * CreateIntoRelDestReceiver(IntoClause *intoClause) { - DR_intorel *self = (DR_intorel *) palloc0(sizeof(DR_intorel)); + DR_intorel *self = palloc0_object(DR_intorel); self->pub.receiveSlot = intorel_receive; self->pub.rStartup = intorel_startup; diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index 5fbbcdaabb1d2..f0819d15ab701 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -8,7 +8,7 @@ * stepping on each others' toes. Formerly we used table-level locks * on pg_database, but that's too coarse-grained. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -60,14 +60,17 @@ #include "storage/lmgr.h" #include "storage/md.h" #include "storage/procarray.h" +#include "storage/procsignal.h" #include "storage/smgr.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/fmgroids.h" +#include "utils/lsyscache.h" #include "utils/pg_locale.h" #include "utils/relmapper.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/wait_event.h" /* * Create database strategy. @@ -430,7 +433,7 @@ ScanSourceDatabasePgClassTuple(HeapTupleData *tuple, Oid tbid, Oid dbid, classForm->oid); /* Prepare a rel info element and add it to the list. */ - relinfo = (CreateDBRelInfo *) palloc(sizeof(CreateDBRelInfo)); + relinfo = palloc_object(CreateDBRelInfo); if (OidIsValid(classForm->reltablespace)) relinfo->rlocator.spcOid = classForm->reltablespace; else @@ -570,8 +573,8 @@ CreateDatabaseUsingFileCopy(Oid src_dboid, Oid dst_dboid, Oid src_tsid, * any CREATE DATABASE commands. */ if (!IsBinaryUpgrade) - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | - CHECKPOINT_WAIT | CHECKPOINT_FLUSH_ALL); + RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | + CHECKPOINT_WAIT | CHECKPOINT_FLUSH_UNLOGGED); /* * Iterate through all tablespaces of the template database, and copy each @@ -673,7 +676,7 @@ CreateDatabaseUsingFileCopy(Oid src_dboid, Oid dst_dboid, Oid src_tsid, * strategy that avoids these problems. */ if (!IsBinaryUpgrade) - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | + RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | CHECKPOINT_WAIT); } @@ -741,6 +744,12 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) CreateDBStrategy dbstrategy = CREATEDB_WAL_LOG; createdb_failure_params fparms; + /* Report error if name has \n or \r character. */ + if (strpbrk(dbname, "\n\r")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("database name \"%s\" contains a newline or carriage return character", dbname))); + /* Extract options from the statement node tree */ foreach(option, stmt->options) { @@ -1035,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) if (pg_strcasecmp(strategy, "wal_log") == 0) dbstrategy = CREATEDB_WAL_LOG; else if (pg_strcasecmp(strategy, "file_copy") == 0) + { + if (DataChecksumsInProgressOn()) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled", + strategy)); dbstrategy = CREATEDB_FILE_COPY; + } else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -1052,7 +1068,7 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) dbctype = src_ctype; if (dblocprovider == '\0') dblocprovider = src_locprovider; - if (dblocale == NULL) + if (dblocale == NULL && dblocprovider == src_locprovider) dblocale = src_locale; if (dbicurules == NULL) dbicurules = src_icurules; @@ -1065,16 +1081,41 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) /* Check that the chosen locales are valid, and get canonical spellings */ if (!check_locale(LC_COLLATE, dbcollate, &canonname)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("invalid LC_COLLATE locale name: \"%s\"", dbcollate), - errhint("If the locale name is specific to ICU, use ICU_LOCALE."))); + { + if (dblocprovider == COLLPROVIDER_BUILTIN) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("invalid LC_COLLATE locale name: \"%s\"", dbcollate), + errhint("If the locale name is specific to the builtin provider, use BUILTIN_LOCALE."))); + else if (dblocprovider == COLLPROVIDER_ICU) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("invalid LC_COLLATE locale name: \"%s\"", dbcollate), + errhint("If the locale name is specific to the ICU provider, use ICU_LOCALE."))); + else + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("invalid LC_COLLATE locale name: \"%s\"", dbcollate))); + } dbcollate = canonname; if (!check_locale(LC_CTYPE, dbctype, &canonname)) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("invalid LC_CTYPE locale name: \"%s\"", dbctype), - errhint("If the locale name is specific to ICU, use ICU_LOCALE."))); + { + if (dblocprovider == COLLPROVIDER_BUILTIN) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("invalid LC_CTYPE locale name: \"%s\"", dbctype), + errhint("If the locale name is specific to the builtin provider, use BUILTIN_LOCALE."))); + else if (dblocprovider == COLLPROVIDER_ICU) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("invalid LC_CTYPE locale name: \"%s\"", dbctype), + errhint("If the locale name is specific to the ICU provider, use ICU_LOCALE."))); + else + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("invalid LC_CTYPE locale name: \"%s\"", dbctype))); + } + dbctype = canonname; check_encoding_locale_matches(encoding, dbcollate, dbctype); @@ -1845,7 +1886,7 @@ dropdb(const char *dbname, bool missing_ok, bool force) * Force a checkpoint to make sure the checkpointer has received the * message sent by ForgetDatabaseSyncRequests. */ - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | CHECKPOINT_WAIT); + RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | CHECKPOINT_WAIT); /* Close all smgr fds in all backends. */ WaitForProcSignalBarrier(EmitProcSignalBarrier(PROCSIGNAL_BARRIER_SMGRRELEASE)); @@ -1884,6 +1925,12 @@ RenameDatabase(const char *oldname, const char *newname) int npreparedxacts; ObjectAddress address; + /* Report error if name has \n or \r character. */ + if (strpbrk(newname, "\n\r")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("database name \"%s\" contains a newline or carriage return character", newname))); + /* * Look up the target database's OID, and get exclusive lock on it. We * need this for the same reasons as DROP DATABASE. @@ -2095,8 +2142,8 @@ movedb(const char *dbname, const char *tblspcname) * On Windows, this also ensures that background procs don't hold any open * files, which would cause rmdir() to fail. */ - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | CHECKPOINT_WAIT - | CHECKPOINT_FLUSH_ALL); + RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | CHECKPOINT_WAIT + | CHECKPOINT_FLUSH_UNLOGGED); /* Close all smgr fds in all backends. */ WaitForProcSignalBarrier(EmitProcSignalBarrier(PROCSIGNAL_BARRIER_SMGRRELEASE)); @@ -2227,7 +2274,7 @@ movedb(const char *dbname, const char *tblspcname) * any unlogged operations done in the new DB tablespace before the * next checkpoint. */ - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | CHECKPOINT_WAIT); + RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | CHECKPOINT_WAIT); /* * Force synchronous commit, thus minimizing the window between @@ -2328,7 +2375,8 @@ DropDatabase(ParseState *pstate, DropdbStmt *stmt) else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized DROP DATABASE option \"%s\"", opt->defname), + errmsg("unrecognized %s option \"%s\"", + "DROP DATABASE", opt->defname), parser_errposition(pstate, opt->location))); } @@ -3179,30 +3227,6 @@ get_database_oid(const char *dbname, bool missing_ok) } -/* - * get_database_name - given a database OID, look up the name - * - * Returns a palloc'd string, or NULL if no such database. - */ -char * -get_database_name(Oid dbid) -{ - HeapTuple dbtuple; - char *result; - - dbtuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid)); - if (HeapTupleIsValid(dbtuple)) - { - result = pstrdup(NameStr(((Form_pg_database) GETSTRUCT(dbtuple))->datname)); - ReleaseSysCache(dbtuple); - } - else - result = NULL; - - return result; -} - - /* * While dropping a database the pg_database row is marked invalid, but the * catalog contents still exist. Connections to such a database are not @@ -3373,6 +3397,7 @@ dbase_redo(XLogReaderState *record) parent_path = pstrdup(dbpath); get_parent_directory(parent_path); recovery_create_dbdir(parent_path, true); + pfree(parent_path); /* Create the database directory with the version file. */ CreateDirAndVersionFile(dbpath, xlrec->db_id, xlrec->tablespace_id, diff --git a/src/backend/commands/define.c b/src/backend/commands/define.c index 5e1b867e6f733..4172cc9bacbff 100644 --- a/src/backend/commands/define.c +++ b/src/backend/commands/define.c @@ -4,7 +4,7 @@ * Support routines for various kinds of object creation. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -20,7 +20,6 @@ #include "postgres.h" #include -#include #include "catalog/namespace.h" #include "commands/defrem.h" @@ -42,7 +41,7 @@ defGetString(DefElem *def) switch (nodeTag(def->arg)) { case T_Integer: - return psprintf("%ld", (long) intVal(def->arg)); + return psprintf("%d", intVal(def->arg)); case T_Float: return castNode(Float, def->arg)->fval; case T_Boolean: @@ -349,7 +348,7 @@ defGetStringList(DefElem *def) (errcode(ERRCODE_SYNTAX_ERROR), errmsg("%s requires a parameter", def->defname))); - if (nodeTag(def->arg) != T_List) + if (!IsA(def->arg, List)) elog(ERROR, "unrecognized node type: %d", (int) nodeTag(def->arg)); foreach(cell, (List *) def->arg) diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c index 81339a75a5286..17d172df07692 100644 --- a/src/backend/commands/discard.c +++ b/src/backend/commands/discard.c @@ -3,7 +3,7 @@ * discard.c * The implementation of the DISCARD command * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -19,6 +19,7 @@ #include "commands/discard.h" #include "commands/prepare.h" #include "commands/sequence.h" +#include "storage/lock.h" #include "utils/guc.h" #include "utils/portal.h" diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index ceb9a229b63b2..88a2df65c6993 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -3,7 +3,7 @@ * dropcmds.c * handle various "DROP" operations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -482,6 +482,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object) case OBJECT_FOREIGN_TABLE: case OBJECT_INDEX: case OBJECT_MATVIEW: + case OBJECT_PROPGRAPH: case OBJECT_ROLE: case OBJECT_SEQUENCE: case OBJECT_SUBSCRIPTION: diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index edc2c988e2934..dcd2f5a09bb06 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -3,7 +3,7 @@ * event_trigger.c * PostgreSQL EVENT TRIGGER support code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -13,6 +13,7 @@ */ #include "postgres.h" +#include "access/genam.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/table.h" @@ -21,6 +22,7 @@ #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_authid.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_database.h" @@ -29,6 +31,7 @@ #include "catalog/pg_opclass.h" #include "catalog/pg_opfamily.h" #include "catalog/pg_parameter_acl.h" +#include "catalog/pg_policy.h" #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" @@ -55,6 +58,7 @@ #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" typedef struct EventTriggerQueryState { @@ -109,6 +113,8 @@ static Oid insert_event_trigger_tuple(const char *trigname, const char *eventnam static void validate_ddl_tags(const char *filtervar, List *taglist); static void validate_table_rewrite_tags(const char *filtervar, List *taglist); static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata); +static bool obtain_object_name_namespace(const ObjectAddress *object, + SQLDropObject *obj); static const char *stringify_grant_objtype(ObjectType objtype); static const char *stringify_adefprivs_objtype(ObjectType objtype); static void SetDatabaseHasLoginEventTriggers(void); @@ -360,7 +366,7 @@ filter_list_to_array(List *filterlist) int i = 0, l = list_length(filterlist); - data = (Datum *) palloc(l * sizeof(Datum)); + data = palloc_array(Datum, l); foreach(lc, filterlist) { @@ -1280,34 +1286,179 @@ EventTriggerSQLDropAddObject(const ObjectAddress *object, bool original, bool no Assert(EventTriggerSupportsObject(object)); - /* don't report temp schemas except my own */ - if (object->classId == NamespaceRelationId && - (isAnyTempNamespace(object->objectId) && - !isTempNamespace(object->objectId))) - return; - oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - obj = palloc0(sizeof(SQLDropObject)); + obj = palloc0_object(SQLDropObject); obj->address = *object; obj->original = original; obj->normal = normal; + if (object->classId == NamespaceRelationId) + { + /* Special handling is needed for temp namespaces */ + if (isTempNamespace(object->objectId)) + obj->istemp = true; + else if (isAnyTempNamespace(object->objectId)) + { + /* don't report temp schemas except my own */ + pfree(obj); + MemoryContextSwitchTo(oldcxt); + return; + } + obj->objname = get_namespace_name(object->objectId); + } + else if (object->classId == AttrDefaultRelationId) + { + /* We treat a column default as temp if its table is temp */ + ObjectAddress colobject; + + colobject = GetAttrDefaultColumnAddress(object->objectId); + if (OidIsValid(colobject.objectId)) + { + if (!obtain_object_name_namespace(&colobject, obj)) + { + pfree(obj); + MemoryContextSwitchTo(oldcxt); + return; + } + } + } + else if (object->classId == TriggerRelationId) + { + /* Similarly, a trigger is temp if its table is temp */ + /* Sadly, there's no lsyscache.c support for trigger objects */ + Relation pg_trigger_rel; + ScanKeyData skey[1]; + SysScanDesc sscan; + HeapTuple tuple; + Oid relid; + + /* Fetch the trigger's table OID the hard way */ + pg_trigger_rel = table_open(TriggerRelationId, AccessShareLock); + ScanKeyInit(&skey[0], + Anum_pg_trigger_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(object->objectId)); + sscan = systable_beginscan(pg_trigger_rel, TriggerOidIndexId, true, + NULL, 1, skey); + tuple = systable_getnext(sscan); + if (HeapTupleIsValid(tuple)) + relid = ((Form_pg_trigger) GETSTRUCT(tuple))->tgrelid; + else + relid = InvalidOid; /* shouldn't happen */ + systable_endscan(sscan); + table_close(pg_trigger_rel, AccessShareLock); + /* Do nothing if we didn't find the trigger */ + if (OidIsValid(relid)) + { + ObjectAddress relobject; + + relobject.classId = RelationRelationId; + relobject.objectId = relid; + /* Arbitrarily set objectSubId nonzero so as not to fill objname */ + relobject.objectSubId = 1; + if (!obtain_object_name_namespace(&relobject, obj)) + { + pfree(obj); + MemoryContextSwitchTo(oldcxt); + return; + } + } + } + else if (object->classId == PolicyRelationId) + { + /* Similarly, a policy is temp if its table is temp */ + /* Sadly, there's no lsyscache.c support for policy objects */ + Relation pg_policy_rel; + ScanKeyData skey[1]; + SysScanDesc sscan; + HeapTuple tuple; + Oid relid; + + /* Fetch the policy's table OID the hard way */ + pg_policy_rel = table_open(PolicyRelationId, AccessShareLock); + ScanKeyInit(&skey[0], + Anum_pg_policy_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(object->objectId)); + sscan = systable_beginscan(pg_policy_rel, PolicyOidIndexId, true, + NULL, 1, skey); + tuple = systable_getnext(sscan); + if (HeapTupleIsValid(tuple)) + relid = ((Form_pg_policy) GETSTRUCT(tuple))->polrelid; + else + relid = InvalidOid; /* shouldn't happen */ + systable_endscan(sscan); + table_close(pg_policy_rel, AccessShareLock); + /* Do nothing if we didn't find the policy */ + if (OidIsValid(relid)) + { + ObjectAddress relobject; + + relobject.classId = RelationRelationId; + relobject.objectId = relid; + /* Arbitrarily set objectSubId nonzero so as not to fill objname */ + relobject.objectSubId = 1; + if (!obtain_object_name_namespace(&relobject, obj)) + { + pfree(obj); + MemoryContextSwitchTo(oldcxt); + return; + } + } + } + else + { + /* Generic handling for all other object classes */ + if (!obtain_object_name_namespace(object, obj)) + { + /* don't report temp objects except my own */ + pfree(obj); + MemoryContextSwitchTo(oldcxt); + return; + } + } + + /* object identity, objname and objargs */ + obj->objidentity = + getObjectIdentityParts(&obj->address, &obj->addrnames, &obj->addrargs, + false); + + /* object type */ + obj->objecttype = getObjectTypeDescription(&obj->address, false); + + slist_push_head(&(currentEventTriggerState->SQLDropList), &obj->next); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Fill obj->objname, obj->schemaname, and obj->istemp based on object. + * + * Returns true if this object should be reported, false if it should + * be ignored because it is a temporary object of another session. + */ +static bool +obtain_object_name_namespace(const ObjectAddress *object, SQLDropObject *obj) +{ /* * Obtain schema names from the object's catalog tuple, if one exists; * this lets us skip objects in temp schemas. We trust that * ObjectProperty contains all object classes that can be * schema-qualified. + * + * Currently, this function does nothing for object classes that are not + * in ObjectProperty, but we might sometime add special cases for that. */ if (is_objectclass_supported(object->classId)) { Relation catalog; HeapTuple tuple; - catalog = table_open(obj->address.classId, AccessShareLock); + catalog = table_open(object->classId, AccessShareLock); tuple = get_catalog_object_by_oid(catalog, get_object_attnum_oid(object->classId), - obj->address.objectId); + object->objectId); if (tuple) { @@ -1315,7 +1466,7 @@ EventTriggerSQLDropAddObject(const ObjectAddress *object, bool original, bool no Datum datum; bool isnull; - attnum = get_object_attnum_namespace(obj->address.classId); + attnum = get_object_attnum_namespace(object->classId); if (attnum != InvalidAttrNumber) { datum = heap_getattr(tuple, attnum, @@ -1333,10 +1484,9 @@ EventTriggerSQLDropAddObject(const ObjectAddress *object, bool original, bool no } else if (isAnyTempNamespace(namespaceId)) { - pfree(obj); + /* no need to fill any fields of *obj */ table_close(catalog, AccessShareLock); - MemoryContextSwitchTo(oldcxt); - return; + return false; } else { @@ -1346,10 +1496,10 @@ EventTriggerSQLDropAddObject(const ObjectAddress *object, bool original, bool no } } - if (get_object_namensp_unique(obj->address.classId) && - obj->address.objectSubId == 0) + if (get_object_namensp_unique(object->classId) && + object->objectSubId == 0) { - attnum = get_object_attnum_name(obj->address.classId); + attnum = get_object_attnum_name(object->classId); if (attnum != InvalidAttrNumber) { datum = heap_getattr(tuple, attnum, @@ -1362,24 +1512,8 @@ EventTriggerSQLDropAddObject(const ObjectAddress *object, bool original, bool no table_close(catalog, AccessShareLock); } - else - { - if (object->classId == NamespaceRelationId && - isTempNamespace(object->objectId)) - obj->istemp = true; - } - - /* object identity, objname and objargs */ - obj->objidentity = - getObjectIdentityParts(&obj->address, &obj->addrnames, &obj->addrargs, - false); - /* object type */ - obj->objecttype = getObjectTypeDescription(&obj->address, false); - - slist_push_head(&(currentEventTriggerState->SQLDropList), &obj->next); - - MemoryContextSwitchTo(oldcxt); + return true; } /* @@ -1582,7 +1716,7 @@ EventTriggerUndoInhibitCommandCollection(void) void EventTriggerCollectSimpleCommand(ObjectAddress address, ObjectAddress secondaryObject, - Node *parsetree) + const Node *parsetree) { MemoryContext oldcxt; CollectedCommand *command; @@ -1594,7 +1728,7 @@ EventTriggerCollectSimpleCommand(ObjectAddress address, oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - command = palloc(sizeof(CollectedCommand)); + command = palloc_object(CollectedCommand); command->type = SCT_Simple; command->in_extension = creating_extension; @@ -1618,7 +1752,7 @@ EventTriggerCollectSimpleCommand(ObjectAddress address, * add it to the command list. */ void -EventTriggerAlterTableStart(Node *parsetree) +EventTriggerAlterTableStart(const Node *parsetree) { MemoryContext oldcxt; CollectedCommand *command; @@ -1630,7 +1764,7 @@ EventTriggerAlterTableStart(Node *parsetree) oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - command = palloc(sizeof(CollectedCommand)); + command = palloc_object(CollectedCommand); command->type = SCT_AlterTable; command->in_extension = creating_extension; @@ -1670,7 +1804,7 @@ EventTriggerAlterTableRelid(Oid objectId) * internally, so that's all that this code needs to handle at the moment. */ void -EventTriggerCollectAlterTableSubcmd(Node *subcmd, ObjectAddress address) +EventTriggerCollectAlterTableSubcmd(const Node *subcmd, ObjectAddress address) { MemoryContext oldcxt; CollectedATSubcmd *newsub; @@ -1686,7 +1820,7 @@ EventTriggerCollectAlterTableSubcmd(Node *subcmd, ObjectAddress address) oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - newsub = palloc(sizeof(CollectedATSubcmd)); + newsub = palloc_object(CollectedATSubcmd); newsub->address = address; newsub->parsetree = copyObject(subcmd); @@ -1760,7 +1894,7 @@ EventTriggerCollectGrant(InternalGrant *istmt) /* * This is tedious, but necessary. */ - icopy = palloc(sizeof(InternalGrant)); + icopy = palloc_object(InternalGrant); memcpy(icopy, istmt, sizeof(InternalGrant)); icopy->objects = list_copy(istmt->objects); icopy->grantees = list_copy(istmt->grantees); @@ -1769,7 +1903,7 @@ EventTriggerCollectGrant(InternalGrant *istmt) icopy->col_privs = lappend(icopy->col_privs, copyObject(lfirst(cell))); /* Now collect it, using the copied InternalGrant */ - command = palloc(sizeof(CollectedCommand)); + command = palloc_object(CollectedCommand); command->type = SCT_Grant; command->in_extension = creating_extension; command->d.grant.istmt = icopy; @@ -1787,7 +1921,7 @@ EventTriggerCollectGrant(InternalGrant *istmt) * executed */ void -EventTriggerCollectAlterOpFam(AlterOpFamilyStmt *stmt, Oid opfamoid, +EventTriggerCollectAlterOpFam(const AlterOpFamilyStmt *stmt, Oid opfamoid, List *operators, List *procedures) { MemoryContext oldcxt; @@ -1800,7 +1934,7 @@ EventTriggerCollectAlterOpFam(AlterOpFamilyStmt *stmt, Oid opfamoid, oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - command = palloc(sizeof(CollectedCommand)); + command = palloc_object(CollectedCommand); command->type = SCT_AlterOpFamily; command->in_extension = creating_extension; ObjectAddressSet(command->d.opfam.address, @@ -1820,7 +1954,7 @@ EventTriggerCollectAlterOpFam(AlterOpFamilyStmt *stmt, Oid opfamoid, * Save data about a CREATE OPERATOR CLASS command being executed */ void -EventTriggerCollectCreateOpClass(CreateOpClassStmt *stmt, Oid opcoid, +EventTriggerCollectCreateOpClass(const CreateOpClassStmt *stmt, Oid opcoid, List *operators, List *procedures) { MemoryContext oldcxt; @@ -1833,7 +1967,7 @@ EventTriggerCollectCreateOpClass(CreateOpClassStmt *stmt, Oid opcoid, oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - command = palloc0(sizeof(CollectedCommand)); + command = palloc0_object(CollectedCommand); command->type = SCT_CreateOpClass; command->in_extension = creating_extension; ObjectAddressSet(command->d.createopc.address, @@ -1854,7 +1988,7 @@ EventTriggerCollectCreateOpClass(CreateOpClassStmt *stmt, Oid opcoid, * executed */ void -EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, Oid cfgId, +EventTriggerCollectAlterTSConfig(const AlterTSConfigurationStmt *stmt, Oid cfgId, Oid *dictIds, int ndicts) { MemoryContext oldcxt; @@ -1867,13 +2001,16 @@ EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, Oid cfgId, oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - command = palloc0(sizeof(CollectedCommand)); + command = palloc0_object(CollectedCommand); command->type = SCT_AlterTSConfig; command->in_extension = creating_extension; ObjectAddressSet(command->d.atscfg.address, TSConfigRelationId, cfgId); - command->d.atscfg.dictIds = palloc(sizeof(Oid) * ndicts); - memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts); + if (ndicts > 0) + { + command->d.atscfg.dictIds = palloc_array(Oid, ndicts); + memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts); + } command->d.atscfg.ndicts = ndicts; command->parsetree = (Node *) copyObject(stmt); @@ -1889,7 +2026,7 @@ EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, Oid cfgId, * executed */ void -EventTriggerCollectAlterDefPrivs(AlterDefaultPrivilegesStmt *stmt) +EventTriggerCollectAlterDefPrivs(const AlterDefaultPrivilegesStmt *stmt) { MemoryContext oldcxt; CollectedCommand *command; @@ -1901,7 +2038,7 @@ EventTriggerCollectAlterDefPrivs(AlterDefaultPrivilegesStmt *stmt) oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); - command = palloc0(sizeof(CollectedCommand)); + command = palloc0_object(CollectedCommand); command->type = SCT_AlterDefaultPrivileges; command->d.defprivs.objtype = stmt->action->objtype; command->in_extension = creating_extension; @@ -2021,8 +2158,8 @@ pg_event_trigger_ddl_commands(PG_FUNCTION_ARGS) elog(ERROR, "cache lookup failed for object %u/%u", addr.classId, addr.objectId); schema_oid = - heap_getattr(objtup, nspAttnum, - RelationGetDescr(catalog), &isnull); + DatumGetObjectId(heap_getattr(objtup, nspAttnum, + RelationGetDescr(catalog), &isnull)); if (isnull) elog(ERROR, "invalid null namespace in object %u/%u/%d", @@ -2170,6 +2307,7 @@ stringify_grant_objtype(ObjectType objtype) case OBJECT_OPERATOR: case OBJECT_OPFAMILY: case OBJECT_POLICY: + case OBJECT_PROPGRAPH: case OBJECT_PUBLICATION: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: @@ -2254,6 +2392,7 @@ stringify_adefprivs_objtype(ObjectType objtype) case OBJECT_OPFAMILY: case OBJECT_PARAMETER_ACL: case OBJECT_POLICY: + case OBJECT_PROPGRAPH: case OBJECT_PUBLICATION: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index bfa83fbc3fec8..112c17b0d6428 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3,7 +3,7 @@ * explain.c * Explain query execution plans * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * IDENTIFICATION @@ -13,6 +13,7 @@ */ #include "postgres.h" +#include "access/relscan.h" #include "access/xact.h" #include "catalog/pg_type.h" #include "commands/createas.h" @@ -42,6 +43,7 @@ #include "utils/ruleutils.h" #include "utils/snapmgr.h" #include "utils/tuplesort.h" +#include "utils/tuplestore.h" #include "utils/typcache.h" #include "utils/xml.h" @@ -138,6 +140,8 @@ static void show_hashagg_info(AggState *aggstate, ExplainState *es); static void show_indexsearches_info(PlanState *planstate, ExplainState *es); static void show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es); +static void show_scan_io_usage(ScanState *planstate, + ExplainState *es); static void show_instrumentation_count(const char *qlabel, int which, PlanState *planstate, ExplainState *es); static void show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es); @@ -147,6 +151,7 @@ static void show_buffer_usage(ExplainState *es, const BufferUsage *usage); static void show_wal_usage(ExplainState *es, const WalUsage *usage); static void show_memory_counters(ExplainState *es, const MemoryContextCounters *mem_counters); +static void show_result_replacement_info(Result *result, ExplainState *es); static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir, ExplainState *es); static void ExplainScanTarget(Scan *plan, ExplainState *es); @@ -280,6 +285,7 @@ ExplainResultDesc(ExplainStmt *stmt) tupdesc = CreateTemplateTupleDesc(1); TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN", result_type, -1, 0); + TupleDescFinalize(tupdesc); return tupdesc; } @@ -350,7 +356,7 @@ standard_ExplainOneQuery(Query *query, int cursorOptions, INSTR_TIME_SET_CURRENT(planstart); /* plan the query */ - plan = pg_plan_query(query, queryString, cursorOptions, params); + plan = pg_plan_query(query, queryString, cursorOptions, params, es); INSTR_TIME_SET_CURRENT(planduration); INSTR_TIME_SUBTRACT(planduration, planstart); @@ -516,6 +522,8 @@ ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, ExplainState *es, instrument_option |= INSTRUMENT_BUFFERS; if (es->wal) instrument_option |= INSTRUMENT_WAL; + if (es->io) + instrument_option |= INSTRUMENT_IO; /* * We always collect timing for the entire statement, even when node-level @@ -811,14 +819,10 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc) * the queryid in any of the EXPLAIN plans to keep stable the results * generated by regression test suites. */ - if (es->verbose && queryDesc->plannedstmt->queryId != UINT64CONST(0) && + if (es->verbose && queryDesc->plannedstmt->queryId != INT64CONST(0) && compute_query_id != COMPUTE_QUERY_ID_REGRESS) { - /* - * Output the queryid as an int64 rather than a uint64 so we match - * what would be seen in the BIGINT pg_stat_statements.queryid column. - */ - ExplainPropertyInteger("Query Identifier", NULL, (int64) + ExplainPropertyInteger("Query Identifier", NULL, queryDesc->plannedstmt->queryId, es); } } @@ -1102,18 +1106,15 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es) for (nt = 0; nt < rInfo->ri_TrigDesc->numtriggers; nt++) { Trigger *trig = rInfo->ri_TrigDesc->triggers + nt; - Instrumentation *instr = rInfo->ri_TrigInstrument + nt; + TriggerInstrumentation *tginstr = rInfo->ri_TrigInstrument + nt; char *relname; char *conname = NULL; - /* Must clean up instrumentation state */ - InstrEndLoop(instr); - /* * We ignore triggers that were never invoked; they likely aren't * relevant to the current query type. */ - if (instr->ntuples == 0) + if (tginstr->firings == 0) continue; ExplainOpenGroup("Trigger", NULL, true, es); @@ -1138,10 +1139,12 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es) if (show_relname) appendStringInfo(es->str, " on %s", relname); if (es->timing) - appendStringInfo(es->str, ": time=%.3f calls=%.0f\n", - 1000.0 * instr->total, instr->ntuples); + appendStringInfo(es->str, ": time=%.3f calls=%" PRId64 "\n", + INSTR_TIME_GET_MILLISEC(tginstr->instr.total), + tginstr->firings); else - appendStringInfo(es->str, ": calls=%.0f\n", instr->ntuples); + appendStringInfo(es->str, ": calls=%" PRId64 "\n", + tginstr->firings); } else { @@ -1150,9 +1153,10 @@ report_triggers(ResultRelInfo *rInfo, bool show_relname, ExplainState *es) ExplainPropertyText("Constraint Name", conname, es); ExplainPropertyText("Relation", relname, es); if (es->timing) - ExplainPropertyFloat("Time", "ms", 1000.0 * instr->total, 3, + ExplainPropertyFloat("Time", "ms", + INSTR_TIME_GET_MILLISEC(tginstr->instr.total), 3, es); - ExplainPropertyFloat("Calls", NULL, instr->ntuples, 0, es); + ExplainPropertyInteger("Calls", NULL, tginstr->firings, es); } if (conname) @@ -1233,6 +1237,10 @@ ExplainPreScanNode(PlanState *planstate, Bitmapset **rels_used) *rels_used = bms_add_members(*rels_used, ((MergeAppend *) plan)->apprelids); break; + case T_Result: + *rels_used = bms_add_members(*rels_used, + ((Result *) plan)->relids); + break; default: break; } @@ -1833,10 +1841,11 @@ ExplainNode(PlanState *planstate, List *ancestors, if (es->analyze && planstate->instrument && planstate->instrument->nloops > 0) { - double nloops = planstate->instrument->nloops; - double startup_ms = 1000.0 * planstate->instrument->startup / nloops; - double total_ms = 1000.0 * planstate->instrument->total / nloops; - double rows = planstate->instrument->ntuples / nloops; + NodeInstrumentation *instr = planstate->instrument; + double nloops = instr->nloops; + double startup_ms = INSTR_TIME_GET_MILLISEC(instr->startup) / nloops; + double total_ms = INSTR_TIME_GET_MILLISEC(instr->instr.total) / nloops; + double rows = instr->ntuples / nloops; if (es->format == EXPLAIN_FORMAT_TEXT) { @@ -1888,11 +1897,11 @@ ExplainNode(PlanState *planstate, List *ancestors, /* prepare per-worker general execution details */ if (es->workers_state && es->verbose) { - WorkerInstrumentation *w = planstate->worker_instrument; + WorkerNodeInstrumentation *w = planstate->worker_instrument; for (int n = 0; n < w->num_workers; n++) { - Instrumentation *instrument = &w->instrument[n]; + NodeInstrumentation *instrument = &w->instrument[n]; double nloops = instrument->nloops; double startup_ms; double total_ms; @@ -1900,8 +1909,8 @@ ExplainNode(PlanState *planstate, List *ancestors, if (nloops <= 0) continue; - startup_ms = 1000.0 * instrument->startup / nloops; - total_ms = 1000.0 * instrument->total / nloops; + startup_ms = INSTR_TIME_GET_MILLISEC(instrument->startup) / nloops; + total_ms = INSTR_TIME_GET_MILLISEC(instrument->instr.total) / nloops; rows = instrument->ntuples / nloops; ExplainOpenWorker(n, es); @@ -2004,12 +2013,13 @@ ExplainNode(PlanState *planstate, List *ancestors, show_instrumentation_count("Rows Removed by Filter", 1, planstate, es); show_tidbitmap_info((BitmapHeapScanState *) planstate, es); + show_scan_io_usage((ScanState *) planstate, es); break; case T_SampleScan: show_tablesample(((SampleScan *) plan)->tablesample, planstate, ancestors, es); /* fall through to print additional fields the same as SeqScan */ - /* FALLTHROUGH */ + pg_fallthrough; case T_SeqScan: case T_ValuesScan: case T_CteScan: @@ -2022,6 +2032,7 @@ ExplainNode(PlanState *planstate, List *ancestors, planstate, es); if (IsA(plan, CteScan)) show_ctescan_info(castNode(CteScanState, planstate), es); + show_scan_io_usage((ScanState *) planstate, es); break; case T_Gather: { @@ -2138,6 +2149,7 @@ ExplainNode(PlanState *planstate, List *ancestors, if (plan->qual) show_instrumentation_count("Rows Removed by Filter", 1, planstate, es); + show_scan_io_usage((ScanState *) planstate, es); } break; case T_ForeignScan: @@ -2236,6 +2248,7 @@ ExplainNode(PlanState *planstate, List *ancestors, ancestors, es); break; case T_Result: + show_result_replacement_info(castNode(Result, plan), es); show_upper_qual((List *) ((Result *) plan)->resconstantqual, "One-Time Filter", planstate, ancestors, es); show_upper_qual(plan->qual, "Filter", planstate, ancestors, es); @@ -2287,18 +2300,18 @@ ExplainNode(PlanState *planstate, List *ancestors, /* Show buffer/WAL usage */ if (es->buffers && planstate->instrument) - show_buffer_usage(es, &planstate->instrument->bufusage); + show_buffer_usage(es, &planstate->instrument->instr.bufusage); if (es->wal && planstate->instrument) - show_wal_usage(es, &planstate->instrument->walusage); + show_wal_usage(es, &planstate->instrument->instr.walusage); /* Prepare per-worker buffer/WAL usage */ if (es->workers_state && (es->buffers || es->wal) && es->verbose) { - WorkerInstrumentation *w = planstate->worker_instrument; + WorkerNodeInstrumentation *w = planstate->worker_instrument; for (int n = 0; n < w->num_workers; n++) { - Instrumentation *instrument = &w->instrument[n]; + NodeInstrumentation *instrument = &w->instrument[n]; double nloops = instrument->nloops; if (nloops <= 0) @@ -2306,9 +2319,9 @@ ExplainNode(PlanState *planstate, List *ancestors, ExplainOpenWorker(n, es); if (es->buffers) - show_buffer_usage(es, &instrument->bufusage); + show_buffer_usage(es, &instrument->instr.bufusage); if (es->wal) - show_wal_usage(es, &instrument->walusage); + show_wal_usage(es, &instrument->instr.walusage); ExplainCloseWorker(n, es); } } @@ -3586,6 +3599,7 @@ static void show_memoize_info(MemoizeState *mstate, List *ancestors, ExplainState *es) { Plan *plan = ((PlanState *) mstate)->plan; + Memoize *mplan = (Memoize *) plan; ListCell *lc; List *context; StringInfoData keystr; @@ -3606,7 +3620,7 @@ show_memoize_info(MemoizeState *mstate, List *ancestors, ExplainState *es) plan, ancestors); - foreach(lc, ((Memoize *) plan)->param_exprs) + foreach(lc, mplan->param_exprs) { Node *expr = (Node *) lfirst(lc); @@ -3622,6 +3636,24 @@ show_memoize_info(MemoizeState *mstate, List *ancestors, ExplainState *es) pfree(keystr.data); + if (es->costs) + { + if (es->format == EXPLAIN_FORMAT_TEXT) + { + ExplainIndentText(es); + appendStringInfo(es->str, "Estimates: capacity=%u distinct keys=%.0f lookups=%.0f hit percent=%.2f%%\n", + mplan->est_entries, mplan->est_unique_keys, + mplan->est_calls, mplan->est_hit_ratio * 100.0); + } + else + { + ExplainPropertyUInteger("Estimated Capacity", NULL, mplan->est_entries, es); + ExplainPropertyFloat("Estimated Distinct Lookup Keys", NULL, mplan->est_unique_keys, 0, es); + ExplainPropertyFloat("Estimated Lookups", NULL, mplan->est_calls, 0, es); + ExplainPropertyFloat("Estimated Hit Percent", NULL, mplan->est_hit_ratio * 100.0, 2, es); + } + } + if (!es->analyze) return; @@ -3855,7 +3887,7 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) { IndexScanState *indexstate = ((IndexScanState *) planstate); - nsearches = indexstate->iss_Instrument.nsearches; + nsearches = indexstate->iss_Instrument->nsearches; SharedInfo = indexstate->iss_SharedInfo; break; } @@ -3863,7 +3895,7 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) { IndexOnlyScanState *indexstate = ((IndexOnlyScanState *) planstate); - nsearches = indexstate->ioss_Instrument.nsearches; + nsearches = indexstate->ioss_Instrument->nsearches; SharedInfo = indexstate->ioss_SharedInfo; break; } @@ -3871,7 +3903,7 @@ show_indexsearches_info(PlanState *planstate, ExplainState *es) { BitmapIndexScanState *indexstate = ((BitmapIndexScanState *) planstate); - nsearches = indexstate->biss_Instrument.nsearches; + nsearches = indexstate->biss_Instrument->nsearches; SharedInfo = indexstate->biss_SharedInfo; break; } @@ -3924,7 +3956,7 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es) } /* Display stats for each parallel worker */ - if (planstate->pstate != NULL) + if (planstate->sinstrument != NULL) { for (int n = 0; n < planstate->sinstrument->num_workers; n++) { @@ -3960,6 +3992,176 @@ show_tidbitmap_info(BitmapHeapScanState *planstate, ExplainState *es) } } +/* + * Print I/O stats - prefetching and I/O performed + * + * This prints two types of stats - "prefetch" about the prefetching done by + * ReadStream, and "I/O" issued by the stream. The prefetch stats are based + * on buffers pulled from the stream (even if no I/O is needed). The I/O + * information is related to I/O requests issued by the stream. + * + * The prefetch stats are printed if any buffer was pulled from the stream. + * For the I/O stats it depend on the output format. In non-text formats the + * information is printed if prefetch stats were printed. In text format it + * gets printed only if there were any I/O requests. + */ +static void +print_io_usage(ExplainState *es, IOStats *stats) +{ + /* don't print prefetch stats if there's nothing to report */ + if (stats->prefetch_count > 0) + { + if (es->format == EXPLAIN_FORMAT_TEXT) + { + /* prefetch distance info */ + ExplainIndentText(es); + appendStringInfo(es->str, "Prefetch: avg=%.2f max=%d capacity=%d\n", + (stats->distance_sum * 1.0 / stats->prefetch_count), + stats->distance_max, + stats->distance_capacity); + + /* prefetch I/O info (only if there were actual I/Os) */ + if (stats->io_count > 0) + { + ExplainIndentText(es); + appendStringInfo(es->str, "I/O: count=%" PRIu64 " waits=%" PRIu64 + " size=%.2f in-progress=%.2f\n", + stats->io_count, stats->wait_count, + (stats->io_nblocks * 1.0 / stats->io_count), + (stats->io_in_progress * 1.0 / stats->io_count)); + } + } + else + { + ExplainPropertyFloat("Average Prefetch Distance", NULL, + (stats->distance_sum * 1.0 / stats->prefetch_count), 3, es); + ExplainPropertyInteger("Max Prefetch Distance", NULL, + stats->distance_max, es); + ExplainPropertyInteger("Prefetch Capacity", NULL, + stats->distance_capacity, es); + + ExplainPropertyUInteger("I/O Count", NULL, + stats->io_count, es); + ExplainPropertyUInteger("I/O Waits", NULL, + stats->wait_count, es); + ExplainPropertyFloat("Average I/O Size", NULL, + (stats->io_nblocks * 1.0 / Max(1, stats->io_count)), 3, es); + ExplainPropertyFloat("Average I/Os In Progress", NULL, + (stats->io_in_progress * 1.0 / Max(1, stats->io_count)), 3, es); + } + } +} + +/* + * Show information about prefetch and I/O in a scan node. + */ +static void +show_scan_io_usage(ScanState *planstate, ExplainState *es) +{ + Plan *plan = planstate->ps.plan; + IOStats stats = {0}; + + if (!es->io) + return; + + /* + * Initialize counters with stats from the local process first. + * + * The scan descriptor may not exist, e.g. if the scan did not start, or + * because of debug_parallel_query=regress. We still want to collect data + * from workers. + */ + if (planstate->ss_currentScanDesc && + planstate->ss_currentScanDesc->rs_instrument) + { + stats = planstate->ss_currentScanDesc->rs_instrument->io; + } + + /* + * Accumulate data from parallel workers (if any). + */ + switch (nodeTag(plan)) + { + case T_BitmapHeapScan: + { + SharedBitmapHeapInstrumentation *sinstrument + = ((BitmapHeapScanState *) planstate)->sinstrument; + + if (sinstrument) + { + for (int i = 0; i < sinstrument->num_workers; ++i) + { + BitmapHeapScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; + + AccumulateIOStats(&stats, &winstrument->stats.io); + + if (!es->workers_state) + continue; + + ExplainOpenWorker(i, es); + print_io_usage(es, &winstrument->stats.io); + ExplainCloseWorker(i, es); + } + } + + break; + } + case T_SeqScan: + { + SharedSeqScanInstrumentation *sinstrument + = ((SeqScanState *) planstate)->sinstrument; + + if (sinstrument) + { + for (int i = 0; i < sinstrument->num_workers; ++i) + { + SeqScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; + + AccumulateIOStats(&stats, &winstrument->stats.io); + + if (!es->workers_state) + continue; + + ExplainOpenWorker(i, es); + print_io_usage(es, &winstrument->stats.io); + ExplainCloseWorker(i, es); + } + } + + break; + } + case T_TidRangeScan: + { + SharedTidRangeScanInstrumentation *sinstrument + = ((TidRangeScanState *) planstate)->trss_sinstrument; + + if (sinstrument) + { + for (int i = 0; i < sinstrument->num_workers; ++i) + { + TidRangeScanInstrumentation *winstrument = &sinstrument->sinstrument[i]; + + AccumulateIOStats(&stats, &winstrument->stats.io); + + if (!es->workers_state) + continue; + + ExplainOpenWorker(i, es); + print_io_usage(es, &winstrument->stats.io); + ExplainCloseWorker(i, es); + } + } + + break; + } + default: + /* ignore other plans */ + return; + } + + print_io_usage(es, &stats); +} + /* * If it's EXPLAIN ANALYZE, show instrumentation information for a plan node * @@ -4262,7 +4464,8 @@ show_wal_usage(ExplainState *es, const WalUsage *usage) { /* Show only positive counter values. */ if ((usage->wal_records > 0) || (usage->wal_fpi > 0) || - (usage->wal_bytes > 0) || (usage->wal_buffers_full > 0)) + (usage->wal_bytes > 0) || (usage->wal_buffers_full > 0) || + (usage->wal_fpi_bytes > 0)) { ExplainIndentText(es); appendStringInfoString(es->str, "WAL:"); @@ -4276,6 +4479,9 @@ show_wal_usage(ExplainState *es, const WalUsage *usage) if (usage->wal_bytes > 0) appendStringInfo(es->str, " bytes=%" PRIu64, usage->wal_bytes); + if (usage->wal_fpi_bytes > 0) + appendStringInfo(es->str, " fpi bytes=%" PRIu64, + usage->wal_fpi_bytes); if (usage->wal_buffers_full > 0) appendStringInfo(es->str, " buffers full=%" PRId64, usage->wal_buffers_full); @@ -4290,6 +4496,8 @@ show_wal_usage(ExplainState *es, const WalUsage *usage) usage->wal_fpi, es); ExplainPropertyUInteger("WAL Bytes", NULL, usage->wal_bytes, es); + ExplainPropertyUInteger("WAL FPI Bytes", NULL, + usage->wal_fpi_bytes, es); ExplainPropertyInteger("WAL Buffers Full", NULL, usage->wal_buffers_full, es); } @@ -4643,10 +4851,36 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, if (node->onConflictAction != ONCONFLICT_NONE) { - ExplainPropertyText("Conflict Resolution", - node->onConflictAction == ONCONFLICT_NOTHING ? - "NOTHING" : "UPDATE", - es); + const char *resolution = NULL; + + if (node->onConflictAction == ONCONFLICT_NOTHING) + resolution = "NOTHING"; + else if (node->onConflictAction == ONCONFLICT_UPDATE) + resolution = "UPDATE"; + else + { + Assert(node->onConflictAction == ONCONFLICT_SELECT); + switch (node->onConflictLockStrength) + { + case LCS_NONE: + resolution = "SELECT"; + break; + case LCS_FORKEYSHARE: + resolution = "SELECT FOR KEY SHARE"; + break; + case LCS_FORSHARE: + resolution = "SELECT FOR SHARE"; + break; + case LCS_FORNOKEYUPDATE: + resolution = "SELECT FOR NO KEY UPDATE"; + break; + case LCS_FORUPDATE: + resolution = "SELECT FOR UPDATE"; + break; + } + } + + ExplainPropertyText("Conflict Resolution", resolution, es); /* * Don't display arbiter indexes at all when DO NOTHING variant @@ -4655,7 +4889,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, if (idxNames) ExplainPropertyList("Conflict Arbiter Indexes", idxNames, es); - /* ON CONFLICT DO UPDATE WHERE qual is specially displayed */ + /* ON CONFLICT DO SELECT/UPDATE WHERE qual is specially displayed */ if (node->onConflictWhere) { show_upper_qual((List *) node->onConflictWhere, "Conflict Filter", @@ -4735,6 +4969,102 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors, ExplainCloseGroup("Target Tables", "Target Tables", false, es); } +/* + * Explain what a "Result" node replaced. + */ +static void +show_result_replacement_info(Result *result, ExplainState *es) +{ + StringInfoData buf; + int nrels = 0; + int rti = -1; + bool found_non_result = false; + char *replacement_type = "???"; + + /* If the Result node has a subplan, it didn't replace anything. */ + if (result->plan.lefttree != NULL) + return; + + /* Gating result nodes should have a subplan, and we don't. */ + Assert(result->result_type != RESULT_TYPE_GATING); + + switch (result->result_type) + { + case RESULT_TYPE_GATING: + replacement_type = "Gating"; + break; + case RESULT_TYPE_SCAN: + replacement_type = "Scan"; + break; + case RESULT_TYPE_JOIN: + replacement_type = "Join"; + break; + case RESULT_TYPE_UPPER: + /* a small white lie */ + replacement_type = "Aggregate"; + break; + case RESULT_TYPE_MINMAX: + replacement_type = "MinMaxAggregate"; + break; + } + + /* + * Build up a comma-separated list of user-facing names for the range + * table entries in the relids set. + */ + initStringInfo(&buf); + while ((rti = bms_next_member(result->relids, rti)) >= 0) + { + RangeTblEntry *rte = rt_fetch(rti, es->rtable); + char *refname; + + /* + * add_outer_joins_to_relids will add join RTIs to the relids set of a + * join; if that join is then replaced with a Result node, we may see + * such RTIs here. But we want to completely ignore those here, + * because "a LEFT JOIN b ON whatever" is a join between a and b, not + * a join between a, b, and an unnamed join. + */ + if (rte->rtekind == RTE_JOIN) + continue; + + /* Count the number of rels that aren't ignored completely. */ + ++nrels; + + /* Work out what reference name to use and add it to the string. */ + refname = (char *) list_nth(es->rtable_names, rti - 1); + if (refname == NULL) + refname = rte->eref->aliasname; + if (buf.len > 0) + appendStringInfoString(&buf, ", "); + appendStringInfoString(&buf, refname); + + /* Keep track of whether we see anything other than RTE_RESULT. */ + if (rte->rtekind != RTE_RESULT) + found_non_result = true; + } + + /* + * If this Result node is because of a single RTE that is RTE_RESULT, it + * is not really replacing anything at all, because there's no other + * method for implementing a scan of such an RTE, so we don't display the + * Replaces line in such cases. + */ + if (nrels <= 1 && !found_non_result && + result->result_type == RESULT_TYPE_SCAN) + return; + + /* Say what we replaced, with list of rels if available. */ + if (buf.len == 0) + ExplainPropertyText("Replaces", replacement_type, es); + else + { + char *s = psprintf("%s on %s", replacement_type, buf.data); + + ExplainPropertyText("Replaces", s, es); + } +} + /* * Explain the constituent plans of an Append, MergeAppend, * BitmapAnd, or BitmapOr node. @@ -4784,6 +5114,7 @@ ExplainSubPlans(List *plans, List *ancestors, { SubPlanState *sps = (SubPlanState *) lfirst(lst); SubPlan *sp = sps->subplan; + char *cooked_plan_name; /* * There can be multiple SubPlan nodes referencing the same physical @@ -4807,8 +5138,20 @@ ExplainSubPlans(List *plans, List *ancestors, */ ancestors = lcons(sp, ancestors); + /* + * The plan has a name like exists_1 or rowcompare_2, but here we want + * to prefix that with CTE, InitPlan, or SubPlan, as appropriate, for + * display purposes. + */ + if (sp->subLinkType == CTE_SUBLINK) + cooked_plan_name = psprintf("CTE %s", sp->plan_name); + else if (sp->isInitPlan) + cooked_plan_name = psprintf("InitPlan %s", sp->plan_name); + else + cooked_plan_name = psprintf("SubPlan %s", sp->plan_name); + ExplainNode(sps->planstate, ancestors, - relationship, sp->plan_name, es); + relationship, cooked_plan_name, es); ancestors = list_delete_first(ancestors); } @@ -4844,7 +5187,7 @@ ExplainCreateWorkersState(int num_workers) { ExplainWorkersState *wstate; - wstate = (ExplainWorkersState *) palloc(sizeof(ExplainWorkersState)); + wstate = palloc_object(ExplainWorkersState); wstate->num_workers = num_workers; wstate->worker_inited = (bool *) palloc0(num_workers * sizeof(bool)); wstate->worker_str = (StringInfoData *) diff --git a/src/backend/commands/explain_dr.c b/src/backend/commands/explain_dr.c index 5715546cf437b..3c96061cf32ab 100644 --- a/src/backend/commands/explain_dr.c +++ b/src/backend/commands/explain_dr.c @@ -3,11 +3,11 @@ * explain_dr.c * Explain DestReceiver to measure serialization overhead * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * IDENTIFICATION - * src/backend/commands/explain.c + * src/backend/commands/explain_dr.c * *------------------------------------------------------------------------- */ @@ -19,6 +19,7 @@ #include "libpq/pqformat.h" #include "libpq/protocol.h" #include "utils/lsyscache.h" +#include "varatt.h" /* * DestReceiver functions for SERIALIZE option @@ -275,7 +276,7 @@ CreateExplainSerializeDestReceiver(ExplainState *es) { SerializeDestReceiver *self; - self = (SerializeDestReceiver *) palloc0(sizeof(SerializeDestReceiver)); + self = palloc0_object(SerializeDestReceiver); self->pub.receiveSlot = serializeAnalyzeReceive; self->pub.rStartup = serializeAnalyzeStartup; diff --git a/src/backend/commands/explain_format.c b/src/backend/commands/explain_format.c index 752691d56dbc7..db9c0cfc3e777 100644 --- a/src/backend/commands/explain_format.c +++ b/src/backend/commands/explain_format.c @@ -3,7 +3,7 @@ * explain_format.c * Format routines for explaining query execution plans * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/commands/explain_state.c b/src/backend/commands/explain_state.c index 60d98d63a62e2..a0ee0a664be0e 100644 --- a/src/backend/commands/explain_state.c +++ b/src/backend/commands/explain_state.c @@ -3,7 +3,7 @@ * explain_state.c * Code for initializing and accessing ExplainState objects * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * In-core options have hard-coded fields inside ExplainState; e.g. if @@ -36,6 +36,8 @@ #include "commands/defrem.h" #include "commands/explain.h" #include "commands/explain_state.h" +#include "utils/builtins.h" +#include "utils/guc.h" /* Hook to perform additional EXPLAIN options validation */ explain_validate_options_hook_type explain_validate_options_hook = NULL; @@ -44,6 +46,7 @@ typedef struct { const char *option_name; ExplainOptionHandler option_handler; + ExplainOptionGUCCheckHandler guc_check_handler; } ExplainExtensionOption; static const char **ExplainExtensionNameArray = NULL; @@ -60,7 +63,7 @@ static int ExplainExtensionOptionsAllocated = 0; ExplainState * NewExplainState(void) { - ExplainState *es = (ExplainState *) palloc0(sizeof(ExplainState)); + ExplainState *es = palloc0_object(ExplainState); /* Set default options (most fields can be left as zeroes). */ es->costs = true; @@ -130,8 +133,8 @@ ParseExplainOptionList(ExplainState *es, List *options, ParseState *pstate) else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"", - opt->defname, p), + errmsg("unrecognized value for %s option \"%s\": \"%s\"", + "EXPLAIN", opt->defname, p), parser_errposition(pstate, opt->location))); } else @@ -155,15 +158,17 @@ ParseExplainOptionList(ExplainState *es, List *options, ParseState *pstate) else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"", - opt->defname, p), + errmsg("unrecognized value for %s option \"%s\": \"%s\"", + "EXPLAIN", opt->defname, p), parser_errposition(pstate, opt->location))); } + else if (strcmp(opt->defname, "io") == 0) + es->io = defGetBoolean(opt); else if (!ApplyExtensionExplainOption(es, opt, pstate)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized EXPLAIN option \"%s\"", - opt->defname), + errmsg("unrecognized %s option \"%s\"", + "EXPLAIN", opt->defname), parser_errposition(pstate, opt->location))); } @@ -185,6 +190,12 @@ ParseExplainOptionList(ExplainState *es, List *options, ParseState *pstate) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("EXPLAIN option %s requires ANALYZE", "TIMING"))); + /* check that IO is used with EXPLAIN ANALYZE */ + if (es->io && !es->analyze) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("EXPLAIN option %s requires ANALYZE", "IO"))); + /* check that serialize is used with EXPLAIN ANALYZE */ if (es->serialize != EXPLAIN_SERIALIZE_NONE && !es->analyze) ereport(ERROR, @@ -195,7 +206,8 @@ ParseExplainOptionList(ExplainState *es, List *options, ParseState *pstate) if (es->generic && es->analyze) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together"))); + errmsg("%s options %s and %s cannot be used together", + "EXPLAIN", "ANALYZE", "GENERIC_PLAN"))); /* if the summary was not set explicitly, set default value */ es->summary = (summary_set) ? es->summary : es->analyze; @@ -281,7 +293,8 @@ SetExplainExtensionState(ExplainState *es, int extension_id, void *opaque) /* If there is no array yet, create one. */ if (es->extension_state == NULL) { - es->extension_state_allocated = 16; + es->extension_state_allocated = + Max(16, pg_nextpower2_32(extension_id + 1)); es->extension_state = palloc0(es->extension_state_allocated * sizeof(void *)); } @@ -291,11 +304,8 @@ SetExplainExtensionState(ExplainState *es, int extension_id, void *opaque) { int i; - i = pg_nextpower2_32(es->extension_state_allocated + 1); - es->extension_state = (void **) - repalloc0(es->extension_state, - es->extension_state_allocated * sizeof(void *), - i * sizeof(void *)); + i = pg_nextpower2_32(extension_id + 1); + es->extension_state = repalloc0_array(es->extension_state, void *, es->extension_state_allocated, i); es->extension_state_allocated = i; } @@ -305,26 +315,39 @@ SetExplainExtensionState(ExplainState *es, int extension_id, void *opaque) /* * Register a new EXPLAIN option. * + * option_name is assumed to be a constant string or allocated in storage + * that will never be freed. + * * When option_name is used as an EXPLAIN option, handler will be called and * should update the ExplainState passed to it. See comments at top of file * for a more detailed explanation. * - * option_name is assumed to be a constant string or allocated in storage - * that will never be freed. + * guc_check_handler is a function that can be safely called from a + * GUC check hook to validate a proposed value for a custom EXPLAIN option. + * Boolean-valued options can pass GUCCheckBooleanExplainOption. See the + * comments for GUCCheckBooleanExplainOption for further information on + * how a guc_check_handler should behave. */ void RegisterExtensionExplainOption(const char *option_name, - ExplainOptionHandler handler) + ExplainOptionHandler handler, + ExplainOptionGUCCheckHandler guc_check_handler) { ExplainExtensionOption *exopt; + Assert(handler != NULL); + Assert(guc_check_handler != NULL); + /* Search for an existing option by this name; if found, update handler. */ for (int i = 0; i < ExplainExtensionOptionsAssigned; ++i) { if (strcmp(ExplainExtensionOptionArray[i].option_name, option_name) == 0) { - ExplainExtensionOptionArray[i].option_handler = handler; + exopt = &ExplainExtensionOptionArray[i]; + + exopt->option_handler = handler; + exopt->guc_check_handler = guc_check_handler; return; } } @@ -336,7 +359,7 @@ RegisterExtensionExplainOption(const char *option_name, ExplainExtensionOptionArray = (ExplainExtensionOption *) MemoryContextAlloc(TopMemoryContext, ExplainExtensionOptionsAllocated - * sizeof(char *)); + * sizeof(ExplainExtensionOption)); } /* If there's an array but it's currently full, expand it. */ @@ -345,7 +368,7 @@ RegisterExtensionExplainOption(const char *option_name, int i = pg_nextpower2_32(ExplainExtensionOptionsAssigned + 1); ExplainExtensionOptionArray = (ExplainExtensionOption *) - repalloc(ExplainExtensionOptionArray, i * sizeof(char *)); + repalloc(ExplainExtensionOptionArray, i * sizeof(ExplainExtensionOption)); ExplainExtensionOptionsAllocated = i; } @@ -353,6 +376,7 @@ RegisterExtensionExplainOption(const char *option_name, exopt = &ExplainExtensionOptionArray[ExplainExtensionOptionsAssigned++]; exopt->option_name = option_name; exopt->option_handler = handler; + exopt->guc_check_handler = guc_check_handler; } /* @@ -376,3 +400,99 @@ ApplyExtensionExplainOption(ExplainState *es, DefElem *opt, ParseState *pstate) return false; } + +/* + * Determine whether an EXPLAIN extension option will be accepted without + * error. Returns true if so, and false if not. See the comments for + * GUCCheckBooleanExplainOption for more details. + * + * The caller need not know that the option_name is valid; this function + * will indicate that the option is unrecognized if that is the case. + */ +bool +GUCCheckExplainExtensionOption(const char *option_name, + const char *option_value, + NodeTag option_type) +{ + for (int i = 0; i < ExplainExtensionOptionsAssigned; i++) + { + ExplainExtensionOption *exopt = &ExplainExtensionOptionArray[i]; + + if (strcmp(exopt->option_name, option_name) == 0) + return exopt->guc_check_handler(option_name, option_value, + option_type); + } + + /* Unrecognized option name. */ + GUC_check_errmsg("unrecognized EXPLAIN option \"%s\"", option_name); + return false; +} + +/* + * guc_check_handler for Boolean-valued EXPLAIN extension options. + * + * After receiving a "true" value from this or any other GUC check handler + * for an EXPLAIN extension option, the caller is entitled to assume that + * a suitably constructed DefElem passed to the main option handler will + * not cause an error. To construct this DefElem, the caller should set + * the DefElem's defname to option_name. If option_value is NULL, arg + * should be NULL. Otherwise, arg should be of the type given by + * option_type, with option_value as the associated value. The only option + * types that should be passed are T_String, T_Float, and T_Integer; in + * the last case, the caller will need to perform a string-to-integer + * conversion. + * + * A guc_check_handler should not throw an error, and should not allocate + * memory. If it returns false to indicate that the option_value is not + * acceptable, it may use GUC_check_errmsg(), GUC_check_errdetail(), etc. + * to clarify the nature of the problem. + * + * Since we're concerned with Boolean options here, the logic below must + * exactly match the semantics of defGetBoolean. + */ +bool +GUCCheckBooleanExplainOption(const char *option_name, + const char *option_value, + NodeTag option_type) +{ + bool valid = false; + + if (option_value == NULL) + { + /* defGetBoolean treats no argument as valid */ + valid = true; + } + else if (option_type == T_String) + { + /* defGetBoolean accepts exactly these string values */ + if (pg_strcasecmp(option_value, "true") == 0 || + pg_strcasecmp(option_value, "false") == 0 || + pg_strcasecmp(option_value, "on") == 0 || + pg_strcasecmp(option_value, "off") == 0) + valid = true; + } + else if (option_type == T_Integer) + { + long value; + char *end; + + /* + * defGetBoolean accepts only 0 and 1, but those can be spelled in + * various ways (e.g. 01, 0x01). + */ + errno = 0; + value = strtol(option_value, &end, 0); + if (errno == 0 && *end == '\0' && end != option_value && + value == (int) value && (value == 0 || value == 1)) + valid = true; + } + + if (!valid) + { + GUC_check_errmsg("EXPLAIN option \"%s\" requires a Boolean value", + option_name); + return false; + } + + return true; +} diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index e6f9ab6dfd66b..a330b5fd6cec5 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -12,7 +12,7 @@ * postgresql.conf. An extension also has an installation script file, * containing SQL commands to create the extension's objects. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -45,6 +45,7 @@ #include "catalog/pg_depend.h" #include "catalog/pg_extension.h" #include "catalog/pg_namespace.h" +#include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/alter.h" #include "commands/comment.h" @@ -62,11 +63,13 @@ #include "utils/builtins.h" #include "utils/conffiles.h" #include "utils/fmgroids.h" +#include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/varlena.h" @@ -126,7 +129,42 @@ typedef struct ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } script_error_callback_arg; +/* + * A location based on the extension_control_path GUC. + * + * The macro field stores the name of a macro (for example “$systemâ€) that + * the extension_control_path processing supports, and which can be replaced + * by a system value stored in loc. + * + * For non-system paths the macro field is NULL. + */ +typedef struct +{ + char *macro; + char *loc; +} ExtensionLocation; + +/* + * Cache structure for get_function_sibling_type (and maybe later, + * allied lookup functions). + */ +typedef struct ExtensionSiblingCache +{ + struct ExtensionSiblingCache *next; /* list link */ + /* lookup key: requesting function's OID and type name */ + Oid reqfuncoid; + const char *typname; + bool valid; /* is entry currently valid? */ + uint32 exthash; /* cache hash of owning extension's OID */ + Oid typeoid; /* OID associated with typname */ +} ExtensionSiblingCache; + +/* Head of linked list of ExtensionSiblingCache structs */ +static ExtensionSiblingCache *ext_sibling_list = NULL; + /* Local functions */ +static void ext_sibling_callback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static List *find_update_path(List *evi_list, ExtensionVersionInfo *evi_start, ExtensionVersionInfo *evi_target, @@ -140,7 +178,8 @@ static Oid get_required_extension(char *reqExtensionName, bool is_create); static void get_available_versions_for_extension(ExtensionControlFile *pcontrol, Tuplestorestate *tupstore, - TupleDesc tupdesc); + TupleDesc tupdesc, + ExtensionLocation *location); static Datum convert_requires_to_datum(List *requires); static void ApplyExtensionUpdates(Oid extensionOid, ExtensionControlFile *pcontrol, @@ -157,6 +196,29 @@ static ExtensionControlFile *new_ExtensionControlFile(const char *extname); char *find_in_paths(const char *basename, List *paths); +/* + * Return the extension location. If the current user doesn't have sufficient + * privilege, don't show the location. + */ +static char * +get_extension_location(ExtensionLocation *loc) +{ + /* We only want to show extension paths for superusers. */ + if (superuser()) + { + /* Return the macro value if present to avoid showing system paths. */ + if (loc->macro != NULL) + return loc->macro; + else + return loc->loc; + } + else + { + /* Similar to pg_stat_activity for unprivileged users */ + return ""; + } +} + /* * get_extension_oid - given an extension name, look up the OID * @@ -224,6 +286,114 @@ get_extension_schema(Oid ext_oid) return result; } +/* + * get_function_sibling_type - find a type belonging to same extension as func + * + * Returns the type's OID, or InvalidOid if not found. + * + * This is useful in extensions, which won't have fixed object OIDs. + * We work from the calling function's own OID, which it can get from its + * FunctionCallInfo parameter, and look up the owning extension and thence + * a type belonging to the same extension. + * + * Notice that the type is specified by name only, without a schema. + * That's because this will typically be used by relocatable extensions + * which can't make a-priori assumptions about which schema their objects + * are in. As long as the extension only defines one type of this name, + * the answer is unique anyway. + * + * We might later add the ability to look up functions, operators, etc. + * + * This code is simply a frontend for some pg_depend lookups. Those lookups + * are fairly expensive, so we provide a simple cache facility. We assume + * that the passed typname is actually a C constant, or at least permanently + * allocated, so that we need not copy that string. + */ +Oid +get_function_sibling_type(Oid funcoid, const char *typname) +{ + ExtensionSiblingCache *cache_entry; + Oid extoid; + Oid typeoid; + + /* + * See if we have the answer cached. Someday there may be enough callers + * to justify a hash table, but for now, a simple linked list is fine. + */ + for (cache_entry = ext_sibling_list; cache_entry != NULL; + cache_entry = cache_entry->next) + { + if (funcoid == cache_entry->reqfuncoid && + strcmp(typname, cache_entry->typname) == 0) + break; + } + if (cache_entry && cache_entry->valid) + return cache_entry->typeoid; + + /* + * Nope, so do the expensive lookups. We do not expect failures, so we do + * not cache negative results. + */ + extoid = getExtensionOfObject(ProcedureRelationId, funcoid); + if (!OidIsValid(extoid)) + return InvalidOid; + typeoid = getExtensionType(extoid, typname); + if (!OidIsValid(typeoid)) + return InvalidOid; + + /* + * Build, or revalidate, cache entry. + */ + if (cache_entry == NULL) + { + /* Register invalidation hook if this is first entry */ + if (ext_sibling_list == NULL) + CacheRegisterSyscacheCallback(EXTENSIONOID, + ext_sibling_callback, + (Datum) 0); + + /* Momentarily zero the space to ensure valid flag is false */ + cache_entry = (ExtensionSiblingCache *) + MemoryContextAllocZero(CacheMemoryContext, + sizeof(ExtensionSiblingCache)); + cache_entry->next = ext_sibling_list; + ext_sibling_list = cache_entry; + } + + cache_entry->reqfuncoid = funcoid; + cache_entry->typname = typname; + cache_entry->exthash = GetSysCacheHashValue1(EXTENSIONOID, + ObjectIdGetDatum(extoid)); + cache_entry->typeoid = typeoid; + /* Mark it valid only once it's fully populated */ + cache_entry->valid = true; + + return typeoid; +} + +/* + * ext_sibling_callback + * Syscache inval callback function for EXTENSIONOID cache + * + * It seems sufficient to invalidate ExtensionSiblingCache entries when + * the owning extension's pg_extension entry is modified or deleted. + * Neither a requesting function's OID, nor the OID of the object it's + * looking for, could change without an extension update or drop/recreate. + */ +static void +ext_sibling_callback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) +{ + ExtensionSiblingCache *cache_entry; + + for (cache_entry = ext_sibling_list; cache_entry != NULL; + cache_entry = cache_entry->next) + { + if (hashvalue == 0 || + cache_entry->exthash == hashvalue) + cache_entry->valid = false; + } +} + /* * Utility functions to check validity of extension and version names */ @@ -338,7 +508,7 @@ is_extension_script_filename(const char *filename) } /* - * Return a list of directories declared on extension_control_path GUC. + * Return a list of directories declared in the extension_control_path GUC. */ static List * get_extension_control_directories(void) @@ -354,7 +524,11 @@ get_extension_control_directories(void) if (strlen(Extension_control_path) == 0) { - paths = lappend(paths, system_dir); + ExtensionLocation *location = palloc_object(ExtensionLocation); + + location->macro = NULL; + location->loc = system_dir; + paths = lappend(paths, location); } else { @@ -366,6 +540,7 @@ get_extension_control_directories(void) int len; char *mangled; char *piece = first_path_var_separator(ecp); + ExtensionLocation *location = palloc_object(ExtensionLocation); /* Get the length of the next path on ecp */ if (piece == NULL) @@ -382,15 +557,21 @@ get_extension_control_directories(void) * suffix if it is a custom extension control path. */ if (strcmp(piece, "$system") == 0) + { + location->macro = pstrdup(piece); mangled = substitute_path_macro(piece, "$system", system_dir); + } else + { + location->macro = NULL; mangled = psprintf("%s/extension", piece); - + } pfree(piece); /* Canonicalize the path based on the OS and add to the list */ canonicalize_path(mangled); - paths = lappend(paths, mangled); + location->loc = mangled; + paths = lappend(paths, location); /* Break if ecp is empty or move to the next path on ecp */ if (ecp[len] == '\0') @@ -404,9 +585,9 @@ get_extension_control_directories(void) } /* - * Find control file for extension with name in control->name, looking in the - * path. Return the full file name, or NULL if not found. If found, the - * directory is recorded in control->control_dir. + * Find control file for extension with name in control->name, looking in + * available paths. Return the full file name, or NULL if not found. + * If found, the directory is recorded in control->control_dir. */ static char * find_extension_control_filename(ExtensionControlFile *control) @@ -440,7 +621,7 @@ get_extension_script_directory(ExtensionControlFile *control) /* * The directory parameter can be omitted, absolute, or relative to the * installation's base directory, which can be the sharedir or a custom - * path that it was set extension_control_path. It depends where the + * path that was set via extension_control_path. It depends on where the * .control file was found. */ if (!control->directory) @@ -499,10 +680,8 @@ get_extension_script_filename(ExtensionControlFile *control, * fields of *control. We parse primary file if version == NULL, * else the optional auxiliary file for that version. * - * The control file will be search on Extension_control_path paths if - * control->control_dir is NULL, otherwise it will use the value of control_dir - * to read and parse the .control file, so it assume that the control_dir is a - * valid path for the control file being parsed. + * If control->control_dir is not NULL, use that to read and parse the + * control file, otherwise search for the file in extension_control_path. * * Control files are supposed to be very short, half a dozen lines, * so we don't worry about memory allocation risks here. Also we don't @@ -724,7 +903,7 @@ read_extension_aux_control_file(const ExtensionControlFile *pcontrol, /* * Flat-copy the struct. Pointer fields share values with original. */ - acontrol = (ExtensionControlFile *) palloc(sizeof(ExtensionControlFile)); + acontrol = palloc_object(ExtensionControlFile); memcpy(acontrol, pcontrol, sizeof(ExtensionControlFile)); /* @@ -931,7 +1110,7 @@ execute_sql_string(const char *sql, const char *filename) callback_arg.stmt_len = -1; scripterrcontext.callback = script_error_callback; - scripterrcontext.arg = (void *) &callback_arg; + scripterrcontext.arg = &callback_arg; scripterrcontext.previous = error_context_stack; error_context_stack = &scripterrcontext; @@ -1143,7 +1322,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, (void) set_config_option("client_min_messages", "warning", PGC_USERSET, PGC_S_SESSION, GUC_ACTION_SAVE, true, 0, false); - if (log_min_messages < WARNING) + if (log_min_messages[MyBackendType] < WARNING) (void) set_config_option_ext("log_min_messages", "warning", PGC_SUSET, PGC_S_SESSION, BOOTSTRAP_SUPERUSERID, @@ -1349,7 +1528,7 @@ get_ext_ver_info(const char *versionname, List **evi_list) return evi; } - evi = (ExtensionVersionInfo *) palloc(sizeof(ExtensionVersionInfo)); + evi = palloc_object(ExtensionVersionInfo); evi->name = pstrdup(versionname); evi->reachable = NIL; evi->installable = false; @@ -1773,14 +1952,17 @@ CreateExtensionInternal(char *extensionName, if (!OidIsValid(schemaOid)) { + ParseState *pstate = make_parsestate(NULL); CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt); + pstate->p_sourcetext = "(generated CREATE SCHEMA command)"; + csstmt->schemaname = schemaName; csstmt->authrole = NULL; /* will be created by current user */ csstmt->schemaElts = NIL; csstmt->if_not_exists = false; - CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)", - -1, -1); + + CreateSchemaCommand(pstate, csstmt, -1, -1); /* * CreateSchemaCommand includes CommandCounterIncrement, so new @@ -2208,15 +2390,16 @@ pg_available_extensions(PG_FUNCTION_ARGS) List *locations; DIR *dir; struct dirent *de; + List *found_ext = NIL; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); locations = get_extension_control_directories(); - foreach_ptr(char, location, locations) + foreach_ptr(ExtensionLocation, location, locations) { - dir = AllocateDir(location); + dir = AllocateDir(location->loc); /* * If the control directory doesn't exist, we want to silently return @@ -2228,12 +2411,13 @@ pg_available_extensions(PG_FUNCTION_ARGS) } else { - while ((de = ReadDir(dir, location)) != NULL) + while ((de = ReadDir(dir, location->loc)) != NULL) { ExtensionControlFile *control; char *extname; - Datum values[3]; - bool nulls[3]; + String *extname_str; + Datum values[4]; + bool nulls[4]; if (!is_extension_control_filename(de->d_name)) continue; @@ -2246,8 +2430,18 @@ pg_available_extensions(PG_FUNCTION_ARGS) if (strstr(extname, "--")) continue; + /* + * Ignore already-found names. They are not reachable by the + * path search, so don't show them. + */ + extname_str = makeString(extname); + if (list_member(found_ext, extname_str)) + continue; + else + found_ext = lappend(found_ext, extname_str); + control = new_ExtensionControlFile(extname); - control->control_dir = pstrdup(location); + control->control_dir = pstrdup(location->loc); parse_extension_control_file(control, NULL); memset(values, 0, sizeof(values)); @@ -2261,11 +2455,15 @@ pg_available_extensions(PG_FUNCTION_ARGS) nulls[1] = true; else values[1] = CStringGetTextDatum(control->default_version); + + /* location */ + values[2] = CStringGetTextDatum(get_extension_location(location)); + /* comment */ if (control->comment == NULL) - nulls[2] = true; + nulls[3] = true; else - values[2] = CStringGetTextDatum(control->comment); + values[3] = CStringGetTextDatum(control->comment); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); @@ -2294,15 +2492,16 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) List *locations; DIR *dir; struct dirent *de; + List *found_ext = NIL; /* Build tuplestore to hold the result rows */ InitMaterializedSRF(fcinfo, 0); locations = get_extension_control_directories(); - foreach_ptr(char, location, locations) + foreach_ptr(ExtensionLocation, location, locations) { - dir = AllocateDir(location); + dir = AllocateDir(location->loc); /* * If the control directory doesn't exist, we want to silently return @@ -2314,10 +2513,11 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) } else { - while ((de = ReadDir(dir, location)) != NULL) + while ((de = ReadDir(dir, location->loc)) != NULL) { ExtensionControlFile *control; char *extname; + String *extname_str; if (!is_extension_control_filename(de->d_name)) continue; @@ -2330,14 +2530,25 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) if (strstr(extname, "--")) continue; + /* + * Ignore already-found names. They are not reachable by the + * path search, so don't show them. + */ + extname_str = makeString(extname); + if (list_member(found_ext, extname_str)) + continue; + else + found_ext = lappend(found_ext, extname_str); + /* read the control file */ control = new_ExtensionControlFile(extname); - control->control_dir = pstrdup(location); + control->control_dir = pstrdup(location->loc); parse_extension_control_file(control, NULL); /* scan extension's script directory for install scripts */ get_available_versions_for_extension(control, rsinfo->setResult, - rsinfo->setDesc); + rsinfo->setDesc, + location); } FreeDir(dir); @@ -2354,7 +2565,8 @@ pg_available_extension_versions(PG_FUNCTION_ARGS) static void get_available_versions_for_extension(ExtensionControlFile *pcontrol, Tuplestorestate *tupstore, - TupleDesc tupdesc) + TupleDesc tupdesc, + ExtensionLocation *location) { List *evi_list; ListCell *lc; @@ -2367,8 +2579,8 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, { ExtensionVersionInfo *evi = (ExtensionVersionInfo *) lfirst(lc); ExtensionControlFile *control; - Datum values[8]; - bool nulls[8]; + Datum values[9]; + bool nulls[9]; ListCell *lc2; if (!evi->installable) @@ -2404,11 +2616,15 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, nulls[6] = true; else values[6] = convert_requires_to_datum(control->requires); + + /* location */ + values[7] = CStringGetTextDatum(get_extension_location(location)); + /* comment */ if (control->comment == NULL) - nulls[7] = true; + nulls[8] = true; else - values[7] = CStringGetTextDatum(control->comment); + values[8] = CStringGetTextDatum(control->comment); tuplestore_putvalues(tupstore, tupdesc, values, nulls); @@ -2449,7 +2665,7 @@ get_available_versions_for_extension(ExtensionControlFile *pcontrol, values[6] = convert_requires_to_datum(control->requires); nulls[6] = false; } - /* comment stays the same */ + /* comment and location stay the same */ tuplestore_putvalues(tupstore, tupdesc, values, nulls); } @@ -2475,9 +2691,9 @@ extension_file_exists(const char *extensionName) locations = get_extension_control_directories(); - foreach_ptr(char, location, locations) + foreach_ptr(ExtensionLocation, location, locations) { - dir = AllocateDir(location); + dir = AllocateDir(location->loc); /* * If the control directory doesn't exist, we want to silently return @@ -2489,7 +2705,7 @@ extension_file_exists(const char *extensionName) } else { - while ((de = ReadDir(dir, location)) != NULL) + while ((de = ReadDir(dir, location->loc)) != NULL) { char *extname; @@ -3865,12 +4081,10 @@ new_ExtensionControlFile(const char *extname) } /* - * Work in a very similar way with find_in_path but it receives an already - * parsed List of paths to search the basename and it do not support macro - * replacement or custom error messages (for simplicity). + * Search for the basename in the list of paths. * - * By "already parsed List of paths" this function expected that paths already - * have all macros replaced. + * Similar to find_in_path but for simplicity does not support custom error + * messages and expects that paths already have all macros replaced. */ char * find_in_paths(const char *basename, List *paths) @@ -3879,7 +4093,8 @@ find_in_paths(const char *basename, List *paths) foreach(cell, paths) { - char *path = lfirst(cell); + ExtensionLocation *location = lfirst(cell); + char *path = location->loc; char *full; Assert(path != NULL); diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index c14e038d54f14..c4852be2eb29c 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -3,7 +3,7 @@ * foreigncmds.c * foreign-data wrapper/server creation/manipulation commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -71,15 +71,26 @@ optionListToArray(List *options) foreach(cell, options) { DefElem *def = lfirst(cell); + const char *name; const char *value; Size len; text *t; + name = def->defname; value = defGetString(def); - len = VARHDRSZ + strlen(def->defname) + 1 + strlen(value); + + /* Insist that name not contain "=", else "a=b=c" is ambiguous */ + if (strchr(name, '=') != NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid option name \"%s\": must not contain \"=\"", + name))); + + len = VARHDRSZ + strlen(name) + 1 + strlen(value); + /* +1 leaves room for sprintf's trailing null */ t = palloc(len + 1); SET_VARSIZE(t, len); - sprintf(VARDATA(t), "%s=%s", def->defname, value); + sprintf(VARDATA(t), "%s=%s", name, value); astate = accumArrayResult(astate, PointerGetDatum(t), false, TEXTOID, @@ -511,21 +522,53 @@ lookup_fdw_validator_func(DefElem *validator) /* validator's return value is ignored, so we don't check the type */ } +/* + * Convert a connection string function name passed from the parser to an Oid. + */ +static Oid +lookup_fdw_connection_func(DefElem *connection) +{ + Oid connectionOid; + Oid funcargtypes[3]; + + if (connection == NULL || connection->arg == NULL) + return InvalidOid; + + /* connection string functions take user oid, server oid */ + funcargtypes[0] = OIDOID; + funcargtypes[1] = OIDOID; + funcargtypes[2] = INTERNALOID; + + connectionOid = LookupFuncName((List *) connection->arg, 3, funcargtypes, false); + + /* check that connection string function has correct return type */ + if (get_func_rettype(connectionOid) != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("function %s must return type %s", + NameListToString((List *) connection->arg), "text"))); + + return connectionOid; +} + /* * Process function options of CREATE/ALTER FDW */ static void parse_func_options(ParseState *pstate, List *func_options, bool *handler_given, Oid *fdwhandler, - bool *validator_given, Oid *fdwvalidator) + bool *validator_given, Oid *fdwvalidator, + bool *connection_given, Oid *fdwconnection) { ListCell *cell; *handler_given = false; *validator_given = false; + *connection_given = false; /* return InvalidOid if not given */ *fdwhandler = InvalidOid; *fdwvalidator = InvalidOid; + *fdwconnection = InvalidOid; foreach(cell, func_options) { @@ -545,6 +588,13 @@ parse_func_options(ParseState *pstate, List *func_options, *validator_given = true; *fdwvalidator = lookup_fdw_validator_func(def); } + else if (strcmp(def->defname, "connection") == 0) + { + if (*connection_given) + errorConflictingDefElem(def, pstate); + *connection_given = true; + *fdwconnection = lookup_fdw_connection_func(def); + } else elog(ERROR, "option \"%s\" not recognized", def->defname); @@ -564,8 +614,10 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) Oid fdwId; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; Datum fdwoptions; Oid ownerId; ObjectAddress myself; @@ -609,10 +661,12 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) /* Lookup handler and validator functions, if given */ parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); values[Anum_pg_foreign_data_wrapper_fdwhandler - 1] = ObjectIdGetDatum(fdwhandler); values[Anum_pg_foreign_data_wrapper_fdwvalidator - 1] = ObjectIdGetDatum(fdwvalidator); + values[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); nulls[Anum_pg_foreign_data_wrapper_fdwacl - 1] = true; @@ -621,7 +675,7 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) stmt->options, fdwvalidator); - if (PointerIsValid(DatumGetPointer(fdwoptions))) + if (DatumGetPointer(fdwoptions) != NULL) values[Anum_pg_foreign_data_wrapper_fdwoptions - 1] = fdwoptions; else nulls[Anum_pg_foreign_data_wrapper_fdwoptions - 1] = true; @@ -653,6 +707,14 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } + if (OidIsValid(fdwconnection)) + { + referenced.classId = ProcedureRelationId; + referenced.objectId = fdwconnection; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + recordDependencyOnOwner(ForeignDataWrapperRelationId, fdwId, ownerId); /* dependency on extension */ @@ -684,8 +746,10 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) Datum datum; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; ObjectAddress myself; rel = table_open(ForeignDataWrapperRelationId, RowExclusiveLock); @@ -715,7 +779,8 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); if (handler_given) { @@ -729,6 +794,11 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) ereport(WARNING, (errmsg("changing the foreign-data wrapper handler can change behavior of existing foreign tables"))); } + else + { + /* handler unchanged */ + fdwhandler = fdwForm->fdwhandler; + } if (validator_given) { @@ -753,6 +823,34 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) fdwvalidator = fdwForm->fdwvalidator; } + if (connection_given) + { + repl_val[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); + repl_repl[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = true; + + /* + * If the connection function is changed, behavior of dependent + * subscriptions can change. If NO CONNECTION, dependent + * subscriptions will fail. + */ + if (OidIsValid(fdwForm->fdwconnection)) + { + if (OidIsValid(fdwconnection)) + ereport(WARNING, + (errmsg("changing the foreign-data wrapper connection function can cause " + "the options for dependent objects to become invalid"))); + else + ereport(WARNING, + (errmsg("removing the foreign-data wrapper connection function will cause " + "dependent subscriptions to fail"))); + } + } + else + { + /* connection function unchanged */ + fdwconnection = fdwForm->fdwconnection; + } + /* * If options specified, validate and update. */ @@ -772,7 +870,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) stmt->options, fdwvalidator); - if (PointerIsValid(DatumGetPointer(datum))) + if (DatumGetPointer(datum) != NULL) repl_val[Anum_pg_foreign_data_wrapper_fdwoptions - 1] = datum; else repl_null[Anum_pg_foreign_data_wrapper_fdwoptions - 1] = true; @@ -791,7 +889,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) ObjectAddressSet(myself, ForeignDataWrapperRelationId, fdwId); /* Update function dependencies if we changed them */ - if (handler_given || validator_given) + if (handler_given || validator_given || connection_given) { ObjectAddress referenced; @@ -821,6 +919,14 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) referenced.objectSubId = 0; recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } + + if (OidIsValid(fdwconnection)) + { + referenced.classId = ProcedureRelationId; + referenced.objectId = fdwconnection; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } } InvokeObjectPostAlterHook(ForeignDataWrapperRelationId, fdwId, 0); @@ -932,7 +1038,7 @@ CreateForeignServer(CreateForeignServerStmt *stmt) stmt->options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(srvoptions))) + if (DatumGetPointer(srvoptions) != NULL) values[Anum_pg_foreign_server_srvoptions - 1] = srvoptions; else nulls[Anum_pg_foreign_server_srvoptions - 1] = true; @@ -1040,7 +1146,7 @@ AlterForeignServer(AlterForeignServerStmt *stmt) stmt->options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(datum))) + if (DatumGetPointer(datum) != NULL) repl_val[Anum_pg_foreign_server_srvoptions - 1] = datum; else repl_null[Anum_pg_foreign_server_srvoptions - 1] = true; @@ -1176,7 +1282,7 @@ CreateUserMapping(CreateUserMappingStmt *stmt) stmt->options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(useoptions))) + if (DatumGetPointer(useoptions) != NULL) values[Anum_pg_user_mapping_umoptions - 1] = useoptions; else nulls[Anum_pg_user_mapping_umoptions - 1] = true; @@ -1290,7 +1396,7 @@ AlterUserMapping(AlterUserMappingStmt *stmt) stmt->options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(datum))) + if (DatumGetPointer(datum) != NULL) repl_val[Anum_pg_user_mapping_umoptions - 1] = datum; else repl_null[Anum_pg_user_mapping_umoptions - 1] = true; @@ -1453,7 +1559,7 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid) stmt->options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(ftoptions))) + if (DatumGetPointer(ftoptions) != NULL) values[Anum_pg_foreign_table_ftoptions - 1] = ftoptions; else nulls[Anum_pg_foreign_table_ftoptions - 1] = true; @@ -1577,6 +1683,7 @@ ImportForeignSchema(ImportForeignSchemaStmt *stmt) pstmt->utilityStmt = (Node *) cstmt; pstmt->stmt_location = rs->stmt_location; pstmt->stmt_len = rs->stmt_len; + pstmt->planOrigin = PLAN_STMT_INTERNAL; /* Execute statement */ ProcessUtility(pstmt, cmd, false, diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c index 0335e982b318b..3afd762e9dccf 100644 --- a/src/backend/commands/functioncmds.c +++ b/src/backend/commands/functioncmds.c @@ -5,7 +5,7 @@ * Routines for CREATE and DROP FUNCTION commands and CREATE and DROP * CAST commands. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -34,6 +34,7 @@ #include "access/htup_details.h" #include "access/table.h" +#include "access/xact.h" #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" @@ -153,6 +154,8 @@ compute_return_type(TypeName *returnType, Oid languageOid, address = TypeShellMake(typname, namespaceId, GetUserId()); rettype = address.objectId; Assert(OidIsValid(rettype)); + /* Ensure the new shell type is visible to ProcedureCreate */ + CommandCounterIncrement(); } aclresult = object_aclcheck(TypeRelationId, rettype, GetUserId(), ACL_USAGE); @@ -911,7 +914,7 @@ interpret_AS_clause(Oid languageOid, const char *languageName, { SQLFunctionParseInfoPtr pinfo; - pinfo = (SQLFunctionParseInfoPtr) palloc0(sizeof(SQLFunctionParseInfo)); + pinfo = palloc0_object(SQLFunctionParseInfo); pinfo->fname = funcname; pinfo->nargs = list_length(parameterTypes); @@ -2421,6 +2424,7 @@ CallStmtResultDesc(CallStmt *stmt) -1, 0); } + TupleDescFinalize(tupdesc); } return tupdesc; diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index d962fe392cd27..9ab74c8df0a1b 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -3,7 +3,7 @@ * indexcmds.c * POSTGRES define and remove index code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,7 @@ #include "postgres.h" #include "access/amapi.h" +#include "access/attmap.h" #include "access/gist.h" #include "access/heapam.h" #include "access/htup_details.h" @@ -38,7 +39,6 @@ #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" #include "commands/comment.h" -#include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/progress.h" @@ -76,7 +76,8 @@ /* non-export function prototypes */ static bool CompareOpclassOptions(const Datum *opts1, const Datum *opts2, int natts); static void CheckPredicate(Expr *predicate); -static void ComputeIndexAttrs(IndexInfo *indexInfo, +static void ComputeIndexAttrs(ParseState *pstate, + IndexInfo *indexInfo, Oid *typeOids, Oid *collationOids, Oid *opclassOids, @@ -191,7 +192,7 @@ CheckIndexCompatible(Oid oldId, HeapTuple tuple; Form_pg_index indexForm; Form_pg_am accessMethodForm; - IndexAmRoutine *amRoutine; + const IndexAmRoutine *amRoutine; bool amcanorder; bool amsummarizing; int16 *coloptions; @@ -249,7 +250,7 @@ CheckIndexCompatible(Oid oldId, opclassIds = palloc_array(Oid, numberOfAttributes); opclassOptions = palloc_array(Datum, numberOfAttributes); coloptions = palloc_array(int16, numberOfAttributes); - ComputeIndexAttrs(indexInfo, + ComputeIndexAttrs(NULL, indexInfo, typeIds, collationIds, opclassIds, opclassOptions, coloptions, attributeList, exclusionOpNames, relationId, @@ -516,6 +517,8 @@ WaitForOlderSnapshots(TransactionId limitXmin, bool progress) * consider offering one DDL command for catalog setup and a separate DDL * command for steps that run opaque expressions. * + * 'pstate': ParseState struct (used only for error reports; pass NULL if + * not available) * 'tableId': the OID of the table relation on which the index is to be * created * 'stmt': IndexStmt describing the properties of the new index. @@ -539,8 +542,9 @@ WaitForOlderSnapshots(TransactionId limitXmin, bool progress) * Returns the object address of the created index. */ ObjectAddress -DefineIndex(Oid tableId, - IndexStmt *stmt, +DefineIndex(ParseState *pstate, + Oid tableId, + const IndexStmt *stmt, Oid indexRelationId, Oid parentIndexId, Oid parentConstraintId, @@ -567,7 +571,7 @@ DefineIndex(Oid tableId, Relation rel; HeapTuple tuple; Form_pg_am accessMethodForm; - IndexAmRoutine *amRoutine; + const IndexAmRoutine *amRoutine; bool amcanorder; bool amissummarizing; amoptions_function amoptions; @@ -577,8 +581,8 @@ DefineIndex(Oid tableId, Datum reloptions; int16 *coloptions; IndexInfo *indexInfo; - bits16 flags; - bits16 constr_flags; + uint16 flags; + uint16 constr_flags; int numberOfAttributes; int numberOfKeyAttributes; TransactionId limitXmin; @@ -896,7 +900,6 @@ DefineIndex(Oid tableId, amoptions = amRoutine->amoptions; amissummarizing = amRoutine->amsummarizing; - pfree(amRoutine); ReleaseSysCache(tuple); /* @@ -935,7 +938,8 @@ DefineIndex(Oid tableId, opclassIds = palloc_array(Oid, numberOfAttributes); opclassOptions = palloc_array(Datum, numberOfAttributes); coloptions = palloc_array(int16, numberOfAttributes); - ComputeIndexAttrs(indexInfo, + ComputeIndexAttrs(pstate, + indexInfo, typeIds, collationIds, opclassIds, opclassOptions, coloptions, allIndexParams, stmt->excludeOpNames, tableId, @@ -1090,7 +1094,10 @@ DefineIndex(Oid tableId, key->partattrs[i] - 1); ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("unique constraint on partitioned table must include all partitioning columns"), + /* translator: %s is UNIQUE, PRIMARY KEY, etc */ + errmsg("%s constraint on partitioned table must include all partitioning columns", + constraint_type), + /* translator: first %s is UNIQUE, PRIMARY KEY, etc */ errdetail("%s constraint on table \"%s\" lacks column \"%s\" which is part of the partition key.", constraint_type, RelationGetRelationName(rel), NameStr(att->attname)))); @@ -1116,7 +1123,8 @@ DefineIndex(Oid tableId, errmsg("index creation on system columns is not supported"))); - if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + if (attno > 0 && + TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED), stmt->primary ? @@ -1157,7 +1165,8 @@ DefineIndex(Oid tableId, { AttrNumber attno = j + FirstLowInvalidHeapAttributeNumber; - if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + if (attno > 0 && + TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), stmt->isconstraint ? @@ -1519,7 +1528,8 @@ DefineIndex(Oid tableId, SetUserIdAndSecContext(root_save_userid, root_save_sec_context); childAddr = - DefineIndex(childRelid, childStmt, + DefineIndex(NULL, /* original pstate not applicable */ + childRelid, childStmt, InvalidOid, /* no predefined OID */ indexRelationId, /* this is our child */ createdConstraintId, @@ -1790,6 +1800,7 @@ DefineIndex(Oid tableId, * before the reference snap was taken, we have to wait out any * transactions that might have older snapshots. */ + INJECTION_POINT("define-index-before-set-valid", NULL); pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE, PROGRESS_CREATEIDX_PHASE_WAIT_3); WaitForOlderSnapshots(limitXmin, true); @@ -1867,7 +1878,8 @@ CheckPredicate(Expr *predicate) * InvalidOid, and other ddl_* arguments are undefined. */ static void -ComputeIndexAttrs(IndexInfo *indexInfo, +ComputeIndexAttrs(ParseState *pstate, + IndexInfo *indexInfo, Oid *typeOids, Oid *collationOids, Oid *opclassOids, @@ -1952,12 +1964,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo, ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" named in key does not exist", - attribute->name))); + attribute->name), + parser_errposition(pstate, attribute->location))); else ereport(ERROR, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" does not exist", - attribute->name))); + attribute->name), + parser_errposition(pstate, attribute->location))); } attform = (Form_pg_attribute) GETSTRUCT(atttuple); indexInfo->ii_IndexAttrNumbers[attn] = attform->attnum; @@ -1975,7 +1989,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, if (attn >= nkeycols) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("expressions are not supported in included columns"))); + errmsg("expressions are not supported in included columns"), + parser_errposition(pstate, attribute->location))); atttype = exprType(expr); attcollation = exprCollation(expr); @@ -2016,7 +2031,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, if (contain_mutable_functions_after_planning((Expr *) expr)) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("functions in index expression must be marked IMMUTABLE"))); + errmsg("functions in index expression must be marked IMMUTABLE"), + parser_errposition(pstate, attribute->location))); } } @@ -2031,19 +2047,23 @@ ComputeIndexAttrs(IndexInfo *indexInfo, if (attribute->collation) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("including column does not support a collation"))); + errmsg("including column does not support a collation"), + parser_errposition(pstate, attribute->location))); if (attribute->opclass) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("including column does not support an operator class"))); + errmsg("including column does not support an operator class"), + parser_errposition(pstate, attribute->location))); if (attribute->ordering != SORTBY_DEFAULT) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("including column does not support ASC/DESC options"))); + errmsg("including column does not support ASC/DESC options"), + parser_errposition(pstate, attribute->location))); if (attribute->nulls_ordering != SORTBY_NULLS_DEFAULT) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("including column does not support NULLS FIRST/LAST options"))); + errmsg("including column does not support NULLS FIRST/LAST options"), + parser_errposition(pstate, attribute->location))); opclassOids[attn] = InvalidOid; opclassOptions[attn] = (Datum) 0; @@ -2087,7 +2107,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, ereport(ERROR, (errcode(ERRCODE_INDETERMINATE_COLLATION), errmsg("could not determine which collation to use for index expression"), - errhint("Use the COLLATE clause to set the collation explicitly."))); + errhint("Use the COLLATE clause to set the collation explicitly."), + parser_errposition(pstate, attribute->location))); } else { @@ -2095,7 +2116,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("collations are not supported by type %s", - format_type_be(atttype)))); + format_type_be(atttype)), + parser_errposition(pstate, attribute->location))); } collationOids[attn] = attcollation; @@ -2163,7 +2185,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("operator %s is not commutative", format_operator(opid)), - errdetail("Only commutative operators can be used in exclusion constraints."))); + errdetail("Only commutative operators can be used in exclusion constraints."), + parser_errposition(pstate, attribute->location))); /* * Operator must be a member of the right opfamily, too @@ -2176,7 +2199,8 @@ ComputeIndexAttrs(IndexInfo *indexInfo, errmsg("operator %s is not a member of operator family \"%s\"", format_operator(opid), get_opfamily_name(opfamily, false)), - errdetail("The exclusion operator must be related to the index operator class for the constraint."))); + errdetail("The exclusion operator must be related to the index operator class for the constraint."), + parser_errposition(pstate, attribute->location))); indexInfo->ii_ExclusionOps[attn] = opid; indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid); @@ -2226,12 +2250,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo, ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("access method \"%s\" does not support ASC/DESC options", - accessMethodName))); + accessMethodName), + parser_errposition(pstate, attribute->location))); if (attribute->nulls_ordering != SORTBY_NULLS_DEFAULT) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("access method \"%s\" does not support NULLS FIRST/LAST options", - accessMethodName))); + accessMethodName), + parser_errposition(pstate, attribute->location))); } /* Set up the per-column opclass options (attoptions field). */ @@ -2440,8 +2466,8 @@ GetDefaultOpClass(Oid type_id, Oid am_id) * Finds an operator from a CompareType. This is used for temporal index * constraints (and other temporal features) to look up equality and overlaps * operators. We ask an opclass support function to translate from the - * compare type to the internal strategy numbers. If the function isn't - * defined or it gives no result, we set *strat to InvalidStrategy. + * compare type to the internal strategy numbers. Raises ERROR on search + * failure. */ void GetOperatorFromCompareType(Oid opclass, Oid rhstype, CompareType cmptype, @@ -2453,34 +2479,36 @@ GetOperatorFromCompareType(Oid opclass, Oid rhstype, CompareType cmptype, Assert(cmptype == COMPARE_EQ || cmptype == COMPARE_OVERLAP || cmptype == COMPARE_CONTAINED_BY); - amid = get_opclass_method(opclass); + /* + * Use the opclass to get the opfamily, opcintype, and access method. If + * any of this fails, quit early. + */ + if (!get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype)) + elog(ERROR, "cache lookup failed for opclass %u", opclass); - *opid = InvalidOid; + amid = get_opclass_method(opclass); - if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype)) - { - /* - * Ask the index AM to translate to its internal stratnum - */ - *strat = IndexAmTranslateCompareType(cmptype, amid, opfamily, true); - if (*strat == InvalidStrategy) - ereport(ERROR, - errcode(ERRCODE_UNDEFINED_OBJECT), - cmptype == COMPARE_EQ ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) : - cmptype == COMPARE_OVERLAP ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) : - cmptype == COMPARE_CONTAINED_BY ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0, - errdetail("Could not translate compare type %d for operator family \"%s\", input type %s, access method \"%s\".", - cmptype, get_opfamily_name(opfamily, false), format_type_be(opcintype), get_am_name(amid))); + /* + * Ask the index AM to translate to its internal stratnum + */ + *strat = IndexAmTranslateCompareType(cmptype, amid, opfamily, true); + if (*strat == InvalidStrategy) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + cmptype == COMPARE_EQ ? errmsg("could not identify an equality operator for type %s", format_type_be(opcintype)) : + cmptype == COMPARE_OVERLAP ? errmsg("could not identify an overlaps operator for type %s", format_type_be(opcintype)) : + cmptype == COMPARE_CONTAINED_BY ? errmsg("could not identify a contained-by operator for type %s", format_type_be(opcintype)) : 0, + errdetail("Could not translate compare type %d for operator family \"%s\" of access method \"%s\".", + cmptype, get_opfamily_name(opfamily, false), get_am_name(amid))); - /* - * We parameterize rhstype so foreign keys can ask for a <@ operator - * whose rhs matches the aggregate function. For example range_agg - * returns anymultirange. - */ - if (!OidIsValid(rhstype)) - rhstype = opcintype; - *opid = get_opfamily_member(opfamily, opcintype, rhstype, *strat); - } + /* + * We parameterize rhstype so foreign keys can ask for a <@ operator whose + * rhs matches the aggregate function. For example range_agg returns + * anymultirange. + */ + if (!OidIsValid(rhstype)) + rhstype = opcintype; + *opid = get_opfamily_member(opfamily, opcintype, rhstype, *strat); if (!OidIsValid(*opid)) ereport(ERROR, @@ -2592,7 +2620,9 @@ makeObjectName(const char *name1, const char *name2, const char *label) * constraint names.) * * Note: it is theoretically possible to get a collision anyway, if someone - * else chooses the same name concurrently. This is fairly unlikely to be + * else chooses the same name concurrently. We shorten the race condition + * window by checking for conflicting relations using SnapshotDirty, but + * that doesn't close the window entirely. This is fairly unlikely to be * a problem in practice, especially if one is holding an exclusive lock on * the relation identified by name1. However, if choosing multiple names * within a single command, you'd better create the new object and do @@ -2608,15 +2638,45 @@ ChooseRelationName(const char *name1, const char *name2, int pass = 0; char *relname = NULL; char modlabel[NAMEDATALEN]; + SnapshotData SnapshotDirty; + Relation pgclassrel; + + /* prepare to search pg_class with a dirty snapshot */ + InitDirtySnapshot(SnapshotDirty); + pgclassrel = table_open(RelationRelationId, AccessShareLock); /* try the unmodified label first */ strlcpy(modlabel, label, sizeof(modlabel)); for (;;) { + ScanKeyData key[2]; + SysScanDesc scan; + bool collides; + relname = makeObjectName(name1, name2, modlabel); - if (!OidIsValid(get_relname_relid(relname, namespaceid))) + /* is there any conflicting relation name? */ + ScanKeyInit(&key[0], + Anum_pg_class_relname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(relname)); + ScanKeyInit(&key[1], + Anum_pg_class_relnamespace, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(namespaceid)); + + scan = systable_beginscan(pgclassrel, ClassNameNspIndexId, + true /* indexOK */ , + &SnapshotDirty, + 2, key); + + collides = HeapTupleIsValid(systable_getnext(scan)); + + systable_endscan(scan); + + /* break out of loop if no conflict */ + if (!collides) { if (!isconstraint || !ConstraintNameExists(relname, namespaceid)) @@ -2628,6 +2688,8 @@ ChooseRelationName(const char *name1, const char *name2, snprintf(modlabel, sizeof(modlabel), "%s%d", label, ++pass); } + table_close(pgclassrel, AccessShareLock); + return relname; } @@ -2809,8 +2871,8 @@ ExecReindex(ParseState *pstate, const ReindexStmt *stmt, bool isTopLevel) else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized REINDEX option \"%s\"", - opt->defname), + errmsg("unrecognized %s option \"%s\"", + "REINDEX", opt->defname), parser_errposition(pstate, opt->location))); } @@ -2943,6 +3005,7 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation, struct ReindexIndexCallbackState *state = arg; LOCKMODE table_lockmode; Oid table_oid; + AclResult aclresult; /* * Lock level here should match table lock in reindex_index() for @@ -2967,43 +3030,42 @@ RangeVarCallbackForReindexIndex(const RangeVar *relation, if (!OidIsValid(relId)) return; - /* - * If the relation does exist, check whether it's an index. But note that - * the relation might have been dropped between the time we did the name - * lookup and now. In that case, there's nothing to do. - */ + /* If the relation does exist, check whether it's an index. */ relkind = get_rel_relkind(relId); - if (!relkind) - return; if (relkind != RELKIND_INDEX && relkind != RELKIND_PARTITIONED_INDEX) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not an index", relation->relname))); - /* Check permissions */ - table_oid = IndexGetRelation(relId, true); - if (OidIsValid(table_oid)) - { - AclResult aclresult; + /* Look up the index's table. */ + table_oid = IndexGetRelation(relId, false); - aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_INDEX, relation->relname); - } + /* + * In the unlikely event that, upon retry, we get the same index OID with + * a different table OID, fail. RangeVarGetRelidExtended() will have + * already locked the index in this case, and it won't retry again, so we + * can't lock the newly discovered table OID without risking deadlock. + * Also, while this corner case is indeed possible, it is extremely + * unlikely to happen in practice, so it's probably not worth any more + * effort than this. + */ + if (relId == oldRelId && table_oid != state->locked_table_oid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" was concurrently dropped", + relation->relname))); + + /* Check permissions. */ + aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_INDEX, relation->relname); /* Lock heap before index to avoid deadlock. */ if (relId != oldRelId) { - /* - * If the OID isn't valid, it means the index was concurrently - * dropped, which is not a problem for us; just return normally. - */ - if (OidIsValid(table_oid)) - { - LockRelationOid(table_oid, table_lockmode); - state->locked_table_oid = table_oid; - } + LockRelationOid(table_oid, table_lockmode); + state->locked_table_oid = table_oid; } } @@ -3927,10 +3989,13 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein tablespaceid = indexRel->rd_rel->reltablespace; /* Create new index definition based on given index */ - newIndexId = index_concurrently_create_copy(heapRel, - idx->indexId, - tablespaceid, - concurrentName); + newIndexId = index_create_copy(heapRel, + INDEX_CREATE_CONCURRENT | + INDEX_CREATE_SKIP_BUILD | + INDEX_CREATE_SUPPRESS_PROGRESS, + idx->indexId, + tablespaceid, + concurrentName); /* * Now open the relation of the new index, a session-level lock is @@ -3988,7 +4053,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein ObjectAddressSet(address, RelationRelationId, newIndexId); EventTriggerCollectSimpleCommand(address, InvalidObjectAddress, - (Node *) stmt); + (const Node *) stmt); } } @@ -4196,6 +4261,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein * indexes with the correct names. */ + INJECTION_POINT("reindex-relation-concurrently-before-swap", NULL); StartTransactionCommand(); /* @@ -4226,7 +4292,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein false); /* - * Updating pg_index might involve TOAST table access, so ensure we + * Swapping the indexes might involve TOAST table access, so ensure we * have a valid snapshot. */ PushActiveSnapshot(GetTransactionSnapshot()); @@ -4274,6 +4340,7 @@ ReindexRelationConcurrently(const ReindexStmt *stmt, Oid relationOid, const Rein * index_drop() for more details. */ + INJECTION_POINT("reindex-relation-concurrently-before-set-dead", NULL); pgstat_progress_update_param(PROGRESS_CREATEIDX_PHASE, PROGRESS_CREATEIDX_PHASE_WAIT_4); WaitForLockersMultiple(lockTags, AccessExclusiveLock, true); diff --git a/src/backend/commands/lockcmds.c b/src/backend/commands/lockcmds.c index 616da19671499..f66b8f17b9b3f 100644 --- a/src/backend/commands/lockcmds.c +++ b/src/backend/commands/lockcmds.c @@ -3,7 +3,7 @@ * lockcmds.c * LOCK command support code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c index 27c2cb26ef5f3..f7d8007f796b0 100644 --- a/src/backend/commands/matview.c +++ b/src/backend/commands/matview.c @@ -3,7 +3,7 @@ * matview.c * materialized view support * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -24,8 +24,8 @@ #include "catalog/namespace.h" #include "catalog/pg_am.h" #include "catalog/pg_opclass.h" -#include "commands/cluster.h" #include "commands/matview.h" +#include "commands/repack.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" #include "executor/executor.h" @@ -49,7 +49,7 @@ typedef struct /* These fields are filled by transientrel_startup: */ Relation transientrel; /* relation to write to */ CommandId output_cid; /* cmin to insert in output tuples */ - int ti_options; /* table_tuple_insert performance options */ + uint32 ti_options; /* table_tuple_insert performance options */ BulkInsertState bistate; /* bulk insert state */ } DR_transientrel; @@ -61,7 +61,6 @@ static void transientrel_shutdown(DestReceiver *self); static void transientrel_destroy(DestReceiver *self); static uint64 refresh_matview_datafill(DestReceiver *dest, Query *query, const char *queryString, bool is_create); -static char *make_temptable_name_n(char *tempname, int n); static void refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, int save_sec_context); static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence); @@ -211,8 +210,8 @@ RefreshMatViewByOid(Oid matviewOid, bool is_create, bool skipData, if (concurrent && skipData) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("%s and %s options cannot be used together", - "CONCURRENTLY", "WITH NO DATA"))); + errmsg("%s options %s and %s cannot be used together", + "REFRESH", "CONCURRENTLY", "WITH NO DATA"))); /* * Check that everything is correct for a refresh. Problems at this point @@ -426,7 +425,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query, CHECK_FOR_INTERRUPTS(); /* Plan the query which will generate data for the refresh. */ - plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL); + plan = pg_plan_query(query, queryString, CURSOR_OPT_PARALLEL_OK, NULL, NULL); /* * Use a snapshot with an updated command ID to ensure this query sees @@ -464,7 +463,7 @@ refresh_matview_datafill(DestReceiver *dest, Query *query, DestReceiver * CreateTransientRelDestReceiver(Oid transientoid) { - DR_transientrel *self = (DR_transientrel *) palloc0(sizeof(DR_transientrel)); + DR_transientrel *self = palloc0_object(DR_transientrel); self->pub.receiveSlot = transientrel_receive; self->pub.rStartup = transientrel_startup; @@ -556,28 +555,6 @@ transientrel_destroy(DestReceiver *self) pfree(self); } - -/* - * Given a qualified temporary table name, append an underscore followed by - * the given integer, to make a new table name based on the old one. - * The result is a palloc'd string. - * - * As coded, this would fail to make a valid SQL name if the given name were, - * say, "FOO"."BAR". Currently, the table name portion of the input will - * never be double-quoted because it's of the form "pg_temp_NNN", cf - * make_new_heap(). But we might have to work harder someday. - */ -static char * -make_temptable_name_n(char *tempname, int n) -{ - StringInfoData namebuf; - - initStringInfo(&namebuf); - appendStringInfoString(&namebuf, tempname); - appendStringInfo(&namebuf, "_%d", n); - return namebuf.data; -} - /* * refresh_by_match_merge * @@ -620,6 +597,9 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, char *matviewname; char *tempname; char *diffname; + char *temprelname; + char *diffrelname; + char *nsp; TupleDesc tupdesc; bool foundUniqueIndex; List *indexoidlist; @@ -632,9 +612,17 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, matviewname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(matviewRel)), RelationGetRelationName(matviewRel)); tempRel = table_open(tempOid, NoLock); - tempname = quote_qualified_identifier(get_namespace_name(RelationGetNamespace(tempRel)), - RelationGetRelationName(tempRel)); - diffname = make_temptable_name_n(tempname, 2); + + /* + * Build qualified names of the temporary table and the diff table. The + * only difference between them is the "_2" suffix on the diff table name. + */ + nsp = get_namespace_name(RelationGetNamespace(tempRel)); + temprelname = RelationGetRelationName(tempRel); + diffrelname = psprintf("%s_2", temprelname); + + tempname = quote_qualified_identifier(nsp, temprelname); + diffname = quote_qualified_identifier(nsp, diffrelname); relnatts = RelationGetNumberOfAttributes(matviewRel); @@ -725,7 +713,7 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, * include all rows. */ tupdesc = matviewRel->rd_att; - opUsedForQual = (Oid *) palloc0(sizeof(Oid) * relnatts); + opUsedForQual = palloc0_array(Oid, relnatts); foundUniqueIndex = false; indexoidlist = RelationGetIndexList(matviewRel); @@ -835,7 +823,8 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner, if (!foundUniqueIndex) ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("could not find suitable unique index on materialized view")); + errmsg("could not find suitable unique index on materialized view \"%s\"", + RelationGetRelationName(matviewRel))); appendStringInfoString(&querybuf, " AND newdata.* OPERATOR(pg_catalog.*=) mv.*) " @@ -904,6 +893,7 @@ static void refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence) { finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true, + true, /* reindex */ RecentXmin, ReadNextMultiXactId(), relpersistence); } diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index dd4cde41d32cc..9f258d566ebf6 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'aggregatecmds.c', @@ -6,7 +6,6 @@ backend_sources += files( 'amcmds.c', 'analyze.c', 'async.c', - 'cluster.c', 'collationcmds.c', 'comment.c', 'constraint.c', @@ -37,10 +36,14 @@ backend_sources += files( 'portalcmds.c', 'prepare.c', 'proclang.c', + 'propgraphcmds.c', 'publicationcmds.c', + 'repack.c', + 'repack_worker.c', 'schemacmds.c', 'seclabel.c', 'sequence.c', + 'sequence_xlog.c', 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', @@ -53,4 +56,5 @@ backend_sources += files( 'vacuumparallel.c', 'variable.c', 'view.c', + 'wait.c', ) diff --git a/src/backend/commands/opclasscmds.c b/src/backend/commands/opclasscmds.c index a6dd8eab5186b..7493a9ccc0665 100644 --- a/src/backend/commands/opclasscmds.c +++ b/src/backend/commands/opclasscmds.c @@ -4,7 +4,7 @@ * * Routines for opclass (and opfamily) manipulation commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -343,13 +343,14 @@ DefineOpClass(CreateOpClassStmt *stmt) optsProcNumber, /* amoptsprocnum value */ maxProcNumber; /* amsupport value */ bool amstorage; /* amstorage flag */ + bool isDefault = stmt->isDefault; List *operators; /* OpFamilyMember list for operators */ List *procedures; /* OpFamilyMember list for support procs */ ListCell *l; Relation rel; HeapTuple tup; Form_pg_am amform; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; Datum values[Natts_pg_opclass]; bool nulls[Natts_pg_opclass]; AclResult aclresult; @@ -523,7 +524,7 @@ DefineOpClass(CreateOpClassStmt *stmt) #endif /* Save the info */ - member = (OpFamilyMember *) palloc0(sizeof(OpFamilyMember)); + member = palloc0_object(OpFamilyMember); member->is_func = false; member->object = operOid; member->number = item->number; @@ -547,7 +548,7 @@ DefineOpClass(CreateOpClassStmt *stmt) get_func_name(funcOid)); #endif /* Save the info */ - member = (OpFamilyMember *) palloc0(sizeof(OpFamilyMember)); + member = palloc0_object(OpFamilyMember); member->is_func = true; member->object = funcOid; member->number = item->number; @@ -610,12 +611,31 @@ DefineOpClass(CreateOpClassStmt *stmt) errmsg("operator class \"%s\" for access method \"%s\" already exists", opcname, stmt->amname))); + /* + * HACK: if we're trying to create btree_gist's gist_inet_ops or + * gist_cidr_ops during a binary upgrade, avoid failure in the next stanza + * by silently making the new opclass non-default. Without this kluge, we + * would fail to upgrade databases containing pre-1.9 versions of + * contrib/btree_gist. We can remove it sometime in the far future when + * we don't expect any such databases to exist. (The result of this hack + * is that the installed version of btree_gist will approximate btree_gist + * 1.9, how closely depending on whether it's 1.8 or something older. + * ALTER EXTENSION UPDATE can be used to bring it up to real 1.9.) + */ + if (isDefault && IsBinaryUpgrade) + { + if (amoid == GIST_AM_OID && + ((typeoid == INETOID && strcmp(opcname, "gist_inet_ops") == 0) || + (typeoid == CIDROID && strcmp(opcname, "gist_cidr_ops") == 0))) + isDefault = false; + } + /* * If we are creating a default opclass, check there isn't one already. * (Note we do not restrict this test to visible opclasses; this ensures * that typcache.c can find unique solutions to its questions.) */ - if (stmt->isDefault) + if (isDefault) { ScanKeyData skey[1]; SysScanDesc scan; @@ -661,7 +681,7 @@ DefineOpClass(CreateOpClassStmt *stmt) values[Anum_pg_opclass_opcowner - 1] = ObjectIdGetDatum(GetUserId()); values[Anum_pg_opclass_opcfamily - 1] = ObjectIdGetDatum(opfamilyoid); values[Anum_pg_opclass_opcintype - 1] = ObjectIdGetDatum(typeoid); - values[Anum_pg_opclass_opcdefault - 1] = BoolGetDatum(stmt->isDefault); + values[Anum_pg_opclass_opcdefault - 1] = BoolGetDatum(isDefault); values[Anum_pg_opclass_opckeytype - 1] = ObjectIdGetDatum(storageoid); tup = heap_form_tuple(rel->rd_att, values, nulls); @@ -823,7 +843,7 @@ AlterOpFamily(AlterOpFamilyStmt *stmt) maxProcNumber; /* amsupport value */ HeapTuple tup; Form_pg_am amform; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; /* Get necessary info about access method */ tup = SearchSysCache1(AMNAME, CStringGetDatum(stmt->amname)); @@ -882,7 +902,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid, int maxOpNumber, int maxProcNumber, int optsProcNumber, List *items) { - IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false); + const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false); List *operators; /* OpFamilyMember list for operators */ List *procedures; /* OpFamilyMember list for support procs */ ListCell *l; @@ -940,7 +960,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid, #endif /* Save the info */ - member = (OpFamilyMember *) palloc0(sizeof(OpFamilyMember)); + member = palloc0_object(OpFamilyMember); member->is_func = false; member->object = operOid; member->number = item->number; @@ -970,7 +990,7 @@ AlterOpFamilyAdd(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid, #endif /* Save the info */ - member = (OpFamilyMember *) palloc0(sizeof(OpFamilyMember)); + member = palloc0_object(OpFamilyMember); member->is_func = true; member->object = funcOid; member->number = item->number; @@ -1058,7 +1078,7 @@ AlterOpFamilyDrop(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid, item->number, maxOpNumber))); processTypesSpec(item->class_args, &lefttype, &righttype); /* Save the info */ - member = (OpFamilyMember *) palloc0(sizeof(OpFamilyMember)); + member = palloc0_object(OpFamilyMember); member->is_func = false; member->number = item->number; member->lefttype = lefttype; @@ -1074,7 +1094,7 @@ AlterOpFamilyDrop(AlterOpFamilyStmt *stmt, Oid amoid, Oid opfamilyoid, item->number, maxProcNumber))); processTypesSpec(item->class_args, &lefttype, &righttype); /* Save the info */ - member = (OpFamilyMember *) palloc0(sizeof(OpFamilyMember)); + member = palloc0_object(OpFamilyMember); member->is_func = true; member->number = item->number; member->lefttype = lefttype; @@ -1165,9 +1185,7 @@ assignOperTypes(OpFamilyMember *member, Oid amoid, Oid typeoid) * the family has been created but not yet populated with the required * operators.) */ - IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false); - - if (!amroutine->amcanorderbyop) + if (!GetIndexAmRoutineByAmId(amoid, false)->amcanorderbyop) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("access method \"%s\" does not support ordering operators", diff --git a/src/backend/commands/operatorcmds.c b/src/backend/commands/operatorcmds.c index 673648f1fc6f5..3e7b09b349421 100644 --- a/src/backend/commands/operatorcmds.c +++ b/src/backend/commands/operatorcmds.c @@ -4,7 +4,7 @@ * * Routines for operator manipulation commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -276,7 +276,6 @@ ValidateRestrictionEstimator(List *restrictionName) { Oid typeId[4]; Oid restrictionOid; - AclResult aclresult; typeId[0] = INTERNALOID; /* PlannerInfo */ typeId[1] = OIDOID; /* operator OID */ @@ -292,11 +291,33 @@ ValidateRestrictionEstimator(List *restrictionName) errmsg("restriction estimator function %s must return type %s", NameListToString(restrictionName), "float8"))); - /* Require EXECUTE rights for the estimator */ - aclresult = object_aclcheck(ProcedureRelationId, restrictionOid, GetUserId(), ACL_EXECUTE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_FUNCTION, - NameListToString(restrictionName)); + /* + * If the estimator is not a built-in function, require superuser + * privilege to install it. This protects against using something that is + * not a restriction estimator or has hard-wired assumptions about what + * data types it is working with. (Built-in estimators are required to + * defend themselves adequately against unexpected data type choices, but + * it seems impractical to expect that of extensions' estimators.) + * + * If it is built-in, only require EXECUTE rights. + */ + if (restrictionOid >= FirstGenbkiObjectId) + { + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to specify a non-built-in restriction estimator function"))); + } + else + { + AclResult aclresult; + + aclresult = object_aclcheck(ProcedureRelationId, restrictionOid, + GetUserId(), ACL_EXECUTE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FUNCTION, + NameListToString(restrictionName)); + } return restrictionOid; } @@ -312,7 +333,6 @@ ValidateJoinEstimator(List *joinName) Oid typeId[5]; Oid joinOid; Oid joinOid2; - AclResult aclresult; typeId[0] = INTERNALOID; /* PlannerInfo */ typeId[1] = OIDOID; /* operator OID */ @@ -350,11 +370,24 @@ ValidateJoinEstimator(List *joinName) errmsg("join estimator function %s must return type %s", NameListToString(joinName), "float8"))); - /* Require EXECUTE rights for the estimator */ - aclresult = object_aclcheck(ProcedureRelationId, joinOid, GetUserId(), ACL_EXECUTE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_FUNCTION, - NameListToString(joinName)); + /* privilege checks are the same as in ValidateRestrictionEstimator */ + if (joinOid >= FirstGenbkiObjectId) + { + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to specify a non-built-in join estimator function"))); + } + else + { + AclResult aclresult; + + aclresult = object_aclcheck(ProcedureRelationId, joinOid, + GetUserId(), ACL_EXECUTE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FUNCTION, + NameListToString(joinName)); + } return joinOid; } diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c index 83056960fe47e..21b8eebe32de2 100644 --- a/src/backend/commands/policy.c +++ b/src/backend/commands/policy.c @@ -3,7 +3,7 @@ * policy.c * Commands for manipulating policies. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/commands/policy.c @@ -144,7 +144,7 @@ policy_role_list_to_array(List *roles, int *num_roles) if (roles == NIL) { *num_roles = 1; - role_oids = (Datum *) palloc(*num_roles * sizeof(Datum)); + role_oids = palloc_array(Datum, *num_roles); role_oids[0] = ObjectIdGetDatum(ACL_ID_PUBLIC); return role_oids; @@ -471,7 +471,7 @@ RemoveRoleFromObjectPolicy(Oid roleid, Oid classid, Oid policy_id) * Ordinarily there'd be exactly one, but we must cope with duplicate * mentions, since CREATE/ALTER POLICY historically have allowed that. */ - role_oids = (Datum *) palloc(num_roles * sizeof(Datum)); + role_oids = palloc_array(Datum, num_roles); for (i = 0, j = 0; i < num_roles; i++) { if (roles[i] != roleid) @@ -945,7 +945,7 @@ AlterPolicy(AlterPolicyStmt *stmt) nitems = ARR_DIMS(policy_roles)[0]; - role_oids = (Datum *) palloc(nitems * sizeof(Datum)); + role_oids = palloc_array(Datum, nitems); for (i = 0; i < nitems; i++) role_oids[i] = ObjectIdGetDatum(roles[i]); diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c index e7c8171c10207..01efac3319e99 100644 --- a/src/backend/commands/portalcmds.c +++ b/src/backend/commands/portalcmds.c @@ -9,7 +9,7 @@ * storage management for portals (but doesn't run any queries in them). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -99,7 +99,8 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa elog(ERROR, "non-SELECT statement in DECLARE CURSOR"); /* Plan the query, applying the specified options */ - plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params); + plan = pg_plan_query(query, pstate->p_sourcetext, cstmt->options, params, + NULL); /* * Create a portal and copy the plan and query string into its memory. diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 34b6410d6a26c..876aad2100aeb 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -7,7 +7,7 @@ * accessed via the extended FE/BE query protocol. * * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/commands/prepare.c @@ -34,8 +34,10 @@ #include "tcop/pquery.h" #include "tcop/utility.h" #include "utils/builtins.h" +#include "utils/hsearch.h" #include "utils/snapmgr.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" /* diff --git a/src/backend/commands/proclang.c b/src/backend/commands/proclang.c index 5036ac03639d6..d4c6a16cd01ca 100644 --- a/src/backend/commands/proclang.c +++ b/src/backend/commands/proclang.c @@ -3,7 +3,7 @@ * proclang.c * PostgreSQL LANGUAGE support code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -13,6 +13,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "access/table.h" #include "catalog/catalog.h" #include "catalog/dependency.h" diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c new file mode 100644 index 0000000000000..bea74b71b6359 --- /dev/null +++ b/src/backend/commands/propgraphcmds.c @@ -0,0 +1,1882 @@ +/*------------------------------------------------------------------------- + * + * propgraphcmds.c + * property graph manipulation + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/commands/propgraphcmds.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/htup_details.h" +#include "access/nbtree.h" +#include "access/table.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/pg_class.h" +#include "catalog/pg_collation_d.h" +#include "catalog/pg_operator_d.h" +#include "catalog/pg_propgraph_element.h" +#include "catalog/pg_propgraph_element_label.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_label_property.h" +#include "catalog/pg_propgraph_property.h" +#include "commands/defrem.h" +#include "commands/propgraphcmds.h" +#include "commands/tablecmds.h" +#include "nodes/nodeFuncs.h" +#include "parser/parse_coerce.h" +#include "parser/parse_collate.h" +#include "parser/parse_oper.h" +#include "parser/parse_relation.h" +#include "parser/parse_target.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/ruleutils.h" +#include "utils/syscache.h" + + +struct element_info +{ + Oid elementid; + char kind; + Oid relid; + char *aliasname; + ArrayType *key; + + char *srcvertex; + Oid srcvertexid; + Oid srcrelid; + ArrayType *srckey; + ArrayType *srcref; + ArrayType *srceqop; + + char *destvertex; + Oid destvertexid; + Oid destrelid; + ArrayType *destkey; + ArrayType *destref; + ArrayType *desteqop; + + List *labels; +}; + + +static ArrayType *propgraph_element_get_key(ParseState *pstate, const List *key_clause, Relation element_rel, + const char *aliasname, int location); +static void propgraph_edge_get_ref_keys(ParseState *pstate, const List *keycols, const List *refcols, + Relation edge_rel, Relation ref_rel, + const char *aliasname, int location, const char *type, + ArrayType **outkey, ArrayType **outref, ArrayType **outeqop); +static AttrNumber *array_from_column_list(ParseState *pstate, const List *colnames, int location, Relation element_rel); +static ArrayType *array_from_attnums(int numattrs, const AttrNumber *attnums); +static Oid insert_element_record(ObjectAddress pgaddress, struct element_info *einfo); +static Oid insert_label_record(Oid graphid, Oid peoid, const char *label); +static void insert_property_records(Oid graphid, Oid ellabeloid, Oid pgerelid, const PropGraphProperties *properties); +static void insert_property_record(Oid graphid, Oid ellabeloid, Oid pgerelid, const char *propname, const Expr *expr); +static void check_element_properties(Oid peoid); +static void check_element_label_properties(Oid ellabeloid); +static void check_all_labels_properties(Oid pgrelid); +static Oid get_vertex_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location); +static Oid get_edge_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location); +static Oid get_element_relid(Oid peid); +static List *get_graph_label_ids(Oid graphid); +static List *get_label_element_label_ids(Oid labelid); +static List *get_element_label_property_names(Oid ellabeloid); +static List *get_graph_property_ids(Oid graphid); + + +/* + * CREATE PROPERTY GRAPH + */ +ObjectAddress +CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt) +{ + CreateStmt *cstmt = makeNode(CreateStmt); + char components_persistence; + ListCell *lc; + ObjectAddress pgaddress; + List *vertex_infos = NIL; + List *edge_infos = NIL; + List *element_aliases = NIL; + List *element_oids = NIL; + + if (stmt->pgname->relpersistence == RELPERSISTENCE_UNLOGGED) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("property graphs cannot be unlogged because they do not have storage"))); + + components_persistence = RELPERSISTENCE_PERMANENT; + + foreach(lc, stmt->vertex_tables) + { + PropGraphVertex *vertex = lfirst_node(PropGraphVertex, lc); + struct element_info *vinfo; + Relation rel; + + vinfo = palloc0_object(struct element_info); + vinfo->kind = PGEKIND_VERTEX; + + vinfo->relid = RangeVarGetRelidExtended(vertex->vtable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL); + + rel = table_open(vinfo->relid, NoLock); + + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) + components_persistence = RELPERSISTENCE_TEMP; + + if (vertex->vtable->alias) + vinfo->aliasname = vertex->vtable->alias->aliasname; + else + vinfo->aliasname = vertex->vtable->relname; + + if (list_member(element_aliases, makeString(vinfo->aliasname))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("alias \"%s\" used more than once as element table", vinfo->aliasname), + parser_errposition(pstate, vertex->location))); + + vinfo->key = propgraph_element_get_key(pstate, vertex->vkey, rel, vinfo->aliasname, vertex->location); + + vinfo->labels = vertex->labels; + + table_close(rel, NoLock); + + vertex_infos = lappend(vertex_infos, vinfo); + + element_aliases = lappend(element_aliases, makeString(vinfo->aliasname)); + } + + foreach(lc, stmt->edge_tables) + { + PropGraphEdge *edge = lfirst_node(PropGraphEdge, lc); + struct element_info *einfo; + Relation rel; + ListCell *lc2; + Oid srcrelid; + Oid destrelid; + Relation srcrel; + Relation destrel; + + einfo = palloc0_object(struct element_info); + einfo->kind = PGEKIND_EDGE; + + einfo->relid = RangeVarGetRelidExtended(edge->etable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL); + + rel = table_open(einfo->relid, NoLock); + + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) + components_persistence = RELPERSISTENCE_TEMP; + + if (edge->etable->alias) + einfo->aliasname = edge->etable->alias->aliasname; + else + einfo->aliasname = edge->etable->relname; + + if (list_member(element_aliases, makeString(einfo->aliasname))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("alias \"%s\" used more than once as element table", einfo->aliasname), + parser_errposition(pstate, edge->location))); + + einfo->key = propgraph_element_get_key(pstate, edge->ekey, rel, einfo->aliasname, edge->location); + + einfo->srcvertex = edge->esrcvertex; + einfo->destvertex = edge->edestvertex; + + srcrelid = 0; + destrelid = 0; + foreach(lc2, vertex_infos) + { + struct element_info *vinfo = lfirst(lc2); + + if (strcmp(vinfo->aliasname, edge->esrcvertex) == 0) + srcrelid = vinfo->relid; + + if (strcmp(vinfo->aliasname, edge->edestvertex) == 0) + destrelid = vinfo->relid; + + if (srcrelid && destrelid) + break; + } + if (!srcrelid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("source vertex \"%s\" of edge \"%s\" does not exist", + edge->esrcvertex, einfo->aliasname), + parser_errposition(pstate, edge->location))); + if (!destrelid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("destination vertex \"%s\" of edge \"%s\" does not exist", + edge->edestvertex, einfo->aliasname), + parser_errposition(pstate, edge->location))); + + srcrel = table_open(srcrelid, NoLock); + destrel = table_open(destrelid, NoLock); + + propgraph_edge_get_ref_keys(pstate, edge->esrckey, edge->esrcvertexcols, rel, srcrel, + einfo->aliasname, edge->location, "SOURCE", + &einfo->srckey, &einfo->srcref, &einfo->srceqop); + propgraph_edge_get_ref_keys(pstate, edge->edestkey, edge->edestvertexcols, rel, destrel, + einfo->aliasname, edge->location, "DESTINATION", + &einfo->destkey, &einfo->destref, &einfo->desteqop); + + einfo->labels = edge->labels; + + table_close(destrel, NoLock); + table_close(srcrel, NoLock); + + table_close(rel, NoLock); + + edge_infos = lappend(edge_infos, einfo); + + element_aliases = lappend(element_aliases, makeString(einfo->aliasname)); + } + + cstmt->relation = stmt->pgname; + cstmt->oncommit = ONCOMMIT_NOOP; + + /* + * Automatically make it temporary if any component tables are temporary + * (see also DefineView()). + */ + if (stmt->pgname->relpersistence == RELPERSISTENCE_PERMANENT + && components_persistence == RELPERSISTENCE_TEMP) + { + cstmt->relation = copyObject(cstmt->relation); + cstmt->relation->relpersistence = RELPERSISTENCE_TEMP; + ereport(NOTICE, + (errmsg("property graph \"%s\" will be temporary", + stmt->pgname->relname))); + } + + pgaddress = DefineRelation(cstmt, RELKIND_PROPGRAPH, InvalidOid, NULL, NULL); + + foreach(lc, vertex_infos) + { + struct element_info *vinfo = lfirst(lc); + Oid peoid; + + peoid = insert_element_record(pgaddress, vinfo); + element_oids = lappend_oid(element_oids, peoid); + } + + foreach(lc, edge_infos) + { + struct element_info *einfo = lfirst(lc); + Oid peoid; + ListCell *lc2; + + /* + * Look up the vertices again. Now the vertices have OIDs assigned, + * which we need. + */ + foreach(lc2, vertex_infos) + { + struct element_info *vinfo = lfirst(lc2); + + if (strcmp(vinfo->aliasname, einfo->srcvertex) == 0) + { + einfo->srcvertexid = vinfo->elementid; + einfo->srcrelid = vinfo->relid; + } + if (strcmp(vinfo->aliasname, einfo->destvertex) == 0) + { + einfo->destvertexid = vinfo->elementid; + einfo->destrelid = vinfo->relid; + } + if (einfo->srcvertexid && einfo->destvertexid) + break; + } + Assert(einfo->srcvertexid); + Assert(einfo->destvertexid); + Assert(einfo->srcrelid); + Assert(einfo->destrelid); + peoid = insert_element_record(pgaddress, einfo); + element_oids = lappend_oid(element_oids, peoid); + } + + CommandCounterIncrement(); + + foreach_oid(peoid, element_oids) + check_element_properties(peoid); + check_all_labels_properties(pgaddress.objectId); + + return pgaddress; +} + +/* + * Process the key clause specified for an element. If key_clause is non-NIL, + * then it is a list of column names. Otherwise, the primary key of the + * relation is used. The return value is an array of column numbers. + */ +static ArrayType * +propgraph_element_get_key(ParseState *pstate, const List *key_clause, Relation element_rel, const char *aliasname, int location) +{ + ArrayType *a; + + if (key_clause == NIL) + { + Oid pkidx = RelationGetPrimaryKeyIndex(element_rel, false); + + if (!pkidx) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("no key specified and no suitable primary key exists for definition of element \"%s\"", aliasname), + parser_errposition(pstate, location)); + else + { + Relation indexDesc; + + indexDesc = index_open(pkidx, AccessShareLock); + a = array_from_attnums(indexDesc->rd_index->indkey.dim1, indexDesc->rd_index->indkey.values); + index_close(indexDesc, NoLock); + } + } + else + { + a = array_from_attnums(list_length(key_clause), + array_from_column_list(pstate, key_clause, location, element_rel)); + } + + return a; +} + +/* + * Process the source or destination link of an edge. + * + * keycols and refcols are column names representing the local and referenced + * (vertex) columns. If they are both NIL, a matching foreign key is looked + * up. + * + * edge_rel and ref_rel are the local and referenced element tables. + * + * aliasname, location, and type are for error messages. type is either + * "SOURCE" or "DESTINATION". + * + * The outputs are arrays of column numbers in outkey and outref. + */ +static void +propgraph_edge_get_ref_keys(ParseState *pstate, const List *keycols, const List *refcols, + Relation edge_rel, Relation ref_rel, + const char *aliasname, int location, const char *type, + ArrayType **outkey, ArrayType **outref, ArrayType **outeqop) +{ + int nkeys; + AttrNumber *keyattnums; + AttrNumber *refattnums; + Oid *keyeqops; + Datum *datums; + + Assert((keycols && refcols) || (!keycols && !refcols)); + + if (keycols) + { + if (list_length(keycols) != list_length(refcols)) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("mismatching number of columns in %s vertex definition of edge \"%s\"", type, aliasname), + parser_errposition(pstate, location)); + + nkeys = list_length(keycols); + keyattnums = array_from_column_list(pstate, keycols, location, edge_rel); + refattnums = array_from_column_list(pstate, refcols, location, ref_rel); + keyeqops = palloc_array(Oid, nkeys); + + for (int i = 0; i < nkeys; i++) + { + Oid keytype; + int32 keytypmod; + Oid keycoll; + Oid reftype; + int32 reftypmod; + Oid refcoll; + Oid opc; + Oid opf; + StrategyNumber strategy; + + /* + * Lookup equality operator to be used for edge and vertex key. + * Vertex key is equivalent to primary key and edge key is similar + * to foreign key since edge key references vertex key. Hence + * vertex key is used as left operand and edge key is used as + * right operand. The method used to find the equality operators + * is similar to the method used to find equality operators for + * FK/PK comparison in ATAddForeignKeyConstraint() except that + * opclass of the the vertex key type is used as a starting point. + * Since we need only equality operators we use both BT and HASH + * strategies. + * + * If the required operators do not exist, we can not construct + * quals linking an edge to its adjacent vertexes. + */ + get_atttypetypmodcoll(RelationGetRelid(edge_rel), keyattnums[i], &keytype, &keytypmod, &keycoll); + get_atttypetypmodcoll(RelationGetRelid(ref_rel), refattnums[i], &reftype, &reftypmod, &refcoll); + keyeqops[i] = InvalidOid; + strategy = BTEqualStrategyNumber; + opc = GetDefaultOpClass(reftype, BTREE_AM_OID); + if (!OidIsValid(opc)) + { + opc = GetDefaultOpClass(reftype, HASH_AM_OID); + strategy = HTEqualStrategyNumber; + } + if (OidIsValid(opc)) + { + opf = get_opclass_family(opc); + if (OidIsValid(opf)) + { + keyeqops[i] = get_opfamily_member(opf, reftype, keytype, strategy); + if (!OidIsValid(keyeqops[i])) + { + /* Last resort, implicit cast. */ + if (can_coerce_type(1, &keytype, &reftype, COERCION_IMPLICIT)) + keyeqops[i] = get_opfamily_member(opf, reftype, reftype, strategy); + } + } + } + + if (!OidIsValid(keyeqops[i])) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("no equality operator exists for %s key comparison of edge \"%s\"", + type, aliasname), + parser_errposition(pstate, location)); + + /* + * If collations of key attribute and referenced attribute are + * different, an edge may end up being adjacent to undesired + * vertexes. Prohibit such a case. + * + * PK/FK allows different collations as long as they are + * deterministic for backward compatibility. But we can be a bit + * stricter here and follow SQL standard. + */ + if (keycoll != refcoll && + keycoll != DEFAULT_COLLATION_OID && refcoll != DEFAULT_COLLATION_OID && + OidIsValid(keycoll) && OidIsValid(refcoll)) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("collation mismatch in %s key of edge \"%s\": %s vs. %s", + type, aliasname, + get_collation_name(keycoll), get_collation_name(refcoll)), + parser_errposition(pstate, location)); + } + } + else + { + ForeignKeyCacheInfo *fk = NULL; + + foreach_node(ForeignKeyCacheInfo, tmp, RelationGetFKeyList(edge_rel)) + { + if (tmp->confrelid == RelationGetRelid(ref_rel)) + { + if (fk) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("more than one suitable foreign key exists for %s key of edge \"%s\"", type, aliasname), + parser_errposition(pstate, location)); + fk = tmp; + } + } + + if (!fk) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("no %s key specified and no suitable foreign key exists for definition of edge \"%s\"", type, aliasname), + parser_errposition(pstate, location)); + + nkeys = fk->nkeys; + keyattnums = fk->conkey; + refattnums = fk->confkey; + keyeqops = fk->conpfeqop; + } + + *outkey = array_from_attnums(nkeys, keyattnums); + *outref = array_from_attnums(nkeys, refattnums); + datums = palloc_array(Datum, nkeys); + for (int i = 0; i < nkeys; i++) + datums[i] = ObjectIdGetDatum(keyeqops[i]); + *outeqop = construct_array_builtin(datums, nkeys, OIDOID); +} + +/* + * Convert list of column names in the specified relation into an array of + * column numbers. + */ +static AttrNumber * +array_from_column_list(ParseState *pstate, const List *colnames, int location, Relation element_rel) +{ + int numattrs; + AttrNumber *attnums; + int i; + ListCell *lc; + + numattrs = list_length(colnames); + attnums = palloc_array(AttrNumber, numattrs); + + i = 0; + foreach(lc, colnames) + { + char *colname = strVal(lfirst(lc)); + Oid relid = RelationGetRelid(element_rel); + AttrNumber attnum; + + attnum = get_attnum(relid, colname); + if (!attnum) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + colname, get_rel_name(relid)), + parser_errposition(pstate, location))); + attnums[i++] = attnum; + } + + for (int j = 0; j < numattrs; j++) + { + for (int k = j + 1; k < numattrs; k++) + { + if (attnums[j] == attnums[k]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("graph key columns list must not contain duplicates"), + parser_errposition(pstate, location))); + } + } + + return attnums; +} + +static ArrayType * +array_from_attnums(int numattrs, const AttrNumber *attnums) +{ + Datum *attnumsd; + + attnumsd = palloc_array(Datum, numattrs); + + for (int i = 0; i < numattrs; i++) + attnumsd[i] = Int16GetDatum(attnums[i]); + + return construct_array_builtin(attnumsd, numattrs, INT2OID); +} + +static void +array_of_attnums_to_objectaddrs(Oid relid, ArrayType *arr, ObjectAddresses *addrs) +{ + Datum *attnumsd; + int numattrs; + + deconstruct_array_builtin(arr, INT2OID, &attnumsd, NULL, &numattrs); + + for (int i = 0; i < numattrs; i++) + { + ObjectAddress referenced; + + ObjectAddressSubSet(referenced, RelationRelationId, relid, DatumGetInt16(attnumsd[i])); + add_exact_object_address(&referenced, addrs); + } +} + +static void +array_of_opers_to_objectaddrs(ArrayType *arr, ObjectAddresses *addrs) +{ + Datum *opersd; + int numopers; + + deconstruct_array_builtin(arr, OIDOID, &opersd, NULL, &numopers); + + for (int i = 0; i < numopers; i++) + { + ObjectAddress referenced; + + ObjectAddressSet(referenced, OperatorRelationId, DatumGetObjectId(opersd[i])); + add_exact_object_address(&referenced, addrs); + } +} + +/* + * Insert a record for an element into the pg_propgraph_element catalog. Also + * inserts labels and properties into their respective catalogs. + */ +static Oid +insert_element_record(ObjectAddress pgaddress, struct element_info *einfo) +{ + Oid graphid = pgaddress.objectId; + Relation rel; + NameData aliasname; + Oid peoid; + Datum values[Natts_pg_propgraph_element] = {0}; + bool nulls[Natts_pg_propgraph_element] = {0}; + HeapTuple tup; + ObjectAddress myself; + ObjectAddress referenced; + ObjectAddresses *addrs; + + rel = table_open(PropgraphElementRelationId, RowExclusiveLock); + + peoid = GetNewOidWithIndex(rel, PropgraphElementObjectIndexId, Anum_pg_propgraph_element_oid); + einfo->elementid = peoid; + values[Anum_pg_propgraph_element_oid - 1] = ObjectIdGetDatum(peoid); + values[Anum_pg_propgraph_element_pgepgid - 1] = ObjectIdGetDatum(graphid); + values[Anum_pg_propgraph_element_pgerelid - 1] = ObjectIdGetDatum(einfo->relid); + namestrcpy(&aliasname, einfo->aliasname); + values[Anum_pg_propgraph_element_pgealias - 1] = NameGetDatum(&aliasname); + values[Anum_pg_propgraph_element_pgekind - 1] = CharGetDatum(einfo->kind); + values[Anum_pg_propgraph_element_pgesrcvertexid - 1] = ObjectIdGetDatum(einfo->srcvertexid); + values[Anum_pg_propgraph_element_pgedestvertexid - 1] = ObjectIdGetDatum(einfo->destvertexid); + values[Anum_pg_propgraph_element_pgekey - 1] = PointerGetDatum(einfo->key); + + if (einfo->srckey) + values[Anum_pg_propgraph_element_pgesrckey - 1] = PointerGetDatum(einfo->srckey); + else + nulls[Anum_pg_propgraph_element_pgesrckey - 1] = true; + if (einfo->srcref) + values[Anum_pg_propgraph_element_pgesrcref - 1] = PointerGetDatum(einfo->srcref); + else + nulls[Anum_pg_propgraph_element_pgesrcref - 1] = true; + if (einfo->srceqop) + values[Anum_pg_propgraph_element_pgesrceqop - 1] = PointerGetDatum(einfo->srceqop); + else + nulls[Anum_pg_propgraph_element_pgesrceqop - 1] = true; + if (einfo->destkey) + values[Anum_pg_propgraph_element_pgedestkey - 1] = PointerGetDatum(einfo->destkey); + else + nulls[Anum_pg_propgraph_element_pgedestkey - 1] = true; + if (einfo->destref) + values[Anum_pg_propgraph_element_pgedestref - 1] = PointerGetDatum(einfo->destref); + else + nulls[Anum_pg_propgraph_element_pgedestref - 1] = true; + if (einfo->desteqop) + values[Anum_pg_propgraph_element_pgedesteqop - 1] = PointerGetDatum(einfo->desteqop); + else + nulls[Anum_pg_propgraph_element_pgedesteqop - 1] = true; + + tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); + CatalogTupleInsert(rel, tup); + heap_freetuple(tup); + + ObjectAddressSet(myself, PropgraphElementRelationId, peoid); + + /* Add dependency on the property graph */ + recordDependencyOn(&myself, &pgaddress, DEPENDENCY_AUTO); + + addrs = new_object_addresses(); + + /* Add dependency on the relation */ + ObjectAddressSet(referenced, RelationRelationId, einfo->relid); + add_exact_object_address(&referenced, addrs); + array_of_attnums_to_objectaddrs(einfo->relid, einfo->key, addrs); + + /* + * Add dependencies on vertices and equality operators used for key + * comparison. + */ + if (einfo->srcvertexid) + { + ObjectAddressSet(referenced, PropgraphElementRelationId, einfo->srcvertexid); + add_exact_object_address(&referenced, addrs); + array_of_attnums_to_objectaddrs(einfo->relid, einfo->srckey, addrs); + array_of_attnums_to_objectaddrs(einfo->srcrelid, einfo->srcref, addrs); + array_of_opers_to_objectaddrs(einfo->srceqop, addrs); + } + if (einfo->destvertexid) + { + ObjectAddressSet(referenced, PropgraphElementRelationId, einfo->destvertexid); + add_exact_object_address(&referenced, addrs); + array_of_attnums_to_objectaddrs(einfo->relid, einfo->destkey, addrs); + array_of_attnums_to_objectaddrs(einfo->destrelid, einfo->destref, addrs); + array_of_opers_to_objectaddrs(einfo->desteqop, addrs); + } + + record_object_address_dependencies(&myself, addrs, DEPENDENCY_NORMAL); + + table_close(rel, NoLock); + + if (einfo->labels) + { + ListCell *lc; + + foreach(lc, einfo->labels) + { + PropGraphLabelAndProperties *lp = lfirst_node(PropGraphLabelAndProperties, lc); + Oid ellabeloid; + + if (lp->label) + ellabeloid = insert_label_record(graphid, peoid, lp->label); + else + ellabeloid = insert_label_record(graphid, peoid, einfo->aliasname); + insert_property_records(graphid, ellabeloid, einfo->relid, lp->properties); + + CommandCounterIncrement(); + } + } + else + { + Oid ellabeloid; + PropGraphProperties *pr = makeNode(PropGraphProperties); + + pr->all = true; + pr->location = -1; + + ellabeloid = insert_label_record(graphid, peoid, einfo->aliasname); + insert_property_records(graphid, ellabeloid, einfo->relid, pr); + } + + return peoid; +} + +/* + * Insert records for a label into the pg_propgraph_label and + * pg_propgraph_element_label catalogs, and register dependencies. + * + * Returns the OID of the new pg_propgraph_element_label record. + */ +static Oid +insert_label_record(Oid graphid, Oid peoid, const char *label) +{ + Oid labeloid; + Oid ellabeloid; + + /* + * Insert into pg_propgraph_label if not already existing. + */ + labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME, Anum_pg_propgraph_label_oid, ObjectIdGetDatum(graphid), CStringGetDatum(label)); + if (!labeloid) + { + Relation rel; + Datum values[Natts_pg_propgraph_label] = {0}; + bool nulls[Natts_pg_propgraph_label] = {0}; + NameData labelname; + HeapTuple tup; + ObjectAddress myself; + ObjectAddress referenced; + + rel = table_open(PropgraphLabelRelationId, RowExclusiveLock); + + labeloid = GetNewOidWithIndex(rel, PropgraphLabelObjectIndexId, Anum_pg_propgraph_label_oid); + values[Anum_pg_propgraph_label_oid - 1] = ObjectIdGetDatum(labeloid); + values[Anum_pg_propgraph_label_pglpgid - 1] = ObjectIdGetDatum(graphid); + namestrcpy(&labelname, label); + values[Anum_pg_propgraph_label_pgllabel - 1] = NameGetDatum(&labelname); + + tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); + CatalogTupleInsert(rel, tup); + heap_freetuple(tup); + + ObjectAddressSet(myself, PropgraphLabelRelationId, labeloid); + + ObjectAddressSet(referenced, RelationRelationId, graphid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + + table_close(rel, NoLock); + } + + /* + * Insert into pg_propgraph_element_label + */ + { + Relation rel; + Datum values[Natts_pg_propgraph_element_label] = {0}; + bool nulls[Natts_pg_propgraph_element_label] = {0}; + HeapTuple tup; + ObjectAddress myself; + ObjectAddress referenced; + + rel = table_open(PropgraphElementLabelRelationId, RowExclusiveLock); + + ellabeloid = GetNewOidWithIndex(rel, PropgraphElementLabelObjectIndexId, Anum_pg_propgraph_element_label_oid); + values[Anum_pg_propgraph_element_label_oid - 1] = ObjectIdGetDatum(ellabeloid); + values[Anum_pg_propgraph_element_label_pgellabelid - 1] = ObjectIdGetDatum(labeloid); + values[Anum_pg_propgraph_element_label_pgelelid - 1] = ObjectIdGetDatum(peoid); + + tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); + CatalogTupleInsert(rel, tup); + heap_freetuple(tup); + + ObjectAddressSet(myself, PropgraphElementLabelRelationId, ellabeloid); + + ObjectAddressSet(referenced, PropgraphLabelRelationId, labeloid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + ObjectAddressSet(referenced, PropgraphElementRelationId, peoid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + + table_close(rel, NoLock); + } + + return ellabeloid; +} + +/* + * Insert records for properties into the pg_propgraph_property catalog. + */ +static void +insert_property_records(Oid graphid, Oid ellabeloid, Oid pgerelid, const PropGraphProperties *properties) +{ + List *proplist = NIL; + ParseState *pstate; + ParseNamespaceItem *nsitem; + List *tp; + Relation rel; + ListCell *lc; + + if (properties->all) + { + Relation attRelation; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple attributeTuple; + + attRelation = table_open(AttributeRelationId, RowShareLock); + ScanKeyInit(&key[0], + Anum_pg_attribute_attrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(pgerelid)); + scan = systable_beginscan(attRelation, AttributeRelidNumIndexId, + true, NULL, 1, key); + while (HeapTupleIsValid(attributeTuple = systable_getnext(scan))) + { + Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple); + ColumnRef *cr; + ResTarget *rt; + + if (att->attnum <= 0 || att->attisdropped) + continue; + + cr = makeNode(ColumnRef); + rt = makeNode(ResTarget); + + cr->fields = list_make1(makeString(pstrdup(NameStr(att->attname)))); + cr->location = -1; + + rt->name = pstrdup(NameStr(att->attname)); + rt->val = (Node *) cr; + rt->location = -1; + + proplist = lappend(proplist, rt); + } + systable_endscan(scan); + table_close(attRelation, RowShareLock); + } + else + { + proplist = properties->properties; + + foreach(lc, proplist) + { + ResTarget *rt = lfirst_node(ResTarget, lc); + + if (!rt->name && !IsA(rt->val, ColumnRef)) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("property name required"), + parser_errposition(NULL, rt->location)); + } + } + + rel = table_open(pgerelid, AccessShareLock); + + pstate = make_parsestate(NULL); + nsitem = addRangeTableEntryForRelation(pstate, + rel, + AccessShareLock, + NULL, + false, + true); + addNSItemToQuery(pstate, nsitem, true, true, true); + + table_close(rel, NoLock); + + tp = transformTargetList(pstate, proplist, EXPR_KIND_PROPGRAPH_PROPERTY); + assign_expr_collations(pstate, (Node *) tp); + + foreach(lc, tp) + { + TargetEntry *te = lfirst_node(TargetEntry, lc); + + insert_property_record(graphid, ellabeloid, pgerelid, te->resname, te->expr); + } +} + +/* + * Insert records for a property into the pg_propgraph_property and + * pg_propgraph_label_property catalogs, and register dependencies. + */ +static void +insert_property_record(Oid graphid, Oid ellabeloid, Oid pgerelid, const char *propname, const Expr *expr) +{ + Oid propoid; + Oid exprtypid = exprType((const Node *) expr); + int32 exprtypmod = exprTypmod((const Node *) expr); + Oid exprcollation = exprCollation((const Node *) expr); + + /* + * Insert into pg_propgraph_property if not already existing. + */ + propoid = GetSysCacheOid2(PROPGRAPHPROPNAME, Anum_pg_propgraph_property_oid, ObjectIdGetDatum(graphid), CStringGetDatum(propname)); + if (!OidIsValid(propoid)) + { + Relation rel; + NameData propnamedata; + Datum values[Natts_pg_propgraph_property] = {0}; + bool nulls[Natts_pg_propgraph_property] = {0}; + HeapTuple tup; + ObjectAddress myself; + ObjectAddress referenced; + + rel = table_open(PropgraphPropertyRelationId, RowExclusiveLock); + + propoid = GetNewOidWithIndex(rel, PropgraphPropertyObjectIndexId, Anum_pg_propgraph_property_oid); + values[Anum_pg_propgraph_property_oid - 1] = ObjectIdGetDatum(propoid); + values[Anum_pg_propgraph_property_pgppgid - 1] = ObjectIdGetDatum(graphid); + namestrcpy(&propnamedata, propname); + values[Anum_pg_propgraph_property_pgpname - 1] = NameGetDatum(&propnamedata); + values[Anum_pg_propgraph_property_pgptypid - 1] = ObjectIdGetDatum(exprtypid); + values[Anum_pg_propgraph_property_pgptypmod - 1] = Int32GetDatum(exprtypmod); + values[Anum_pg_propgraph_property_pgpcollation - 1] = ObjectIdGetDatum(exprcollation); + + tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); + CatalogTupleInsert(rel, tup); + heap_freetuple(tup); + + ObjectAddressSet(myself, PropgraphPropertyRelationId, propoid); + + ObjectAddressSet(referenced, RelationRelationId, graphid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + ObjectAddressSet(referenced, TypeRelationId, exprtypid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + if (OidIsValid(exprcollation) && exprcollation != DEFAULT_COLLATION_OID) + { + ObjectAddressSet(referenced, CollationRelationId, exprcollation); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + + table_close(rel, NoLock); + } + else + { + HeapTuple pgptup = SearchSysCache1(PROPGRAPHPROPOID, ObjectIdGetDatum(propoid)); + Form_pg_propgraph_property pgpform = (Form_pg_propgraph_property) GETSTRUCT(pgptup); + Oid proptypid = pgpform->pgptypid; + int32 proptypmod = pgpform->pgptypmod; + Oid propcollation = pgpform->pgpcollation; + + ReleaseSysCache(pgptup); + + /* + * Check that in the graph, all properties with the same name have the + * same type (independent of which label they are on). (See SQL/PGQ + * subclause "Consistency check of a tabular property graph + * descriptor".) + */ + if (proptypid != exprtypid || proptypmod != exprtypmod) + { + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("property \"%s\" data type mismatch: %s vs. %s", + propname, format_type_with_typemod(proptypid, proptypmod), format_type_with_typemod(exprtypid, exprtypmod)), + errdetail("In a property graph, a property of the same name has to have the same data type in each label.")); + } + + /* Similarly for collation */ + if (propcollation != exprcollation) + { + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("property \"%s\" collation mismatch: %s vs. %s", + propname, get_collation_name(propcollation), get_collation_name(exprcollation)), + errdetail("In a property graph, a property of the same name has to have the same collation in each label.")); + } + } + + /* + * Insert into pg_propgraph_label_property + */ + { + Relation rel; + Datum values[Natts_pg_propgraph_label_property] = {0}; + bool nulls[Natts_pg_propgraph_label_property] = {0}; + Oid plpoid; + HeapTuple tup; + ObjectAddress myself; + ObjectAddress referenced; + + rel = table_open(PropgraphLabelPropertyRelationId, RowExclusiveLock); + + plpoid = GetNewOidWithIndex(rel, PropgraphLabelPropertyObjectIndexId, Anum_pg_propgraph_label_property_oid); + values[Anum_pg_propgraph_label_property_oid - 1] = ObjectIdGetDatum(plpoid); + values[Anum_pg_propgraph_label_property_plppropid - 1] = ObjectIdGetDatum(propoid); + values[Anum_pg_propgraph_label_property_plpellabelid - 1] = ObjectIdGetDatum(ellabeloid); + values[Anum_pg_propgraph_label_property_plpexpr - 1] = CStringGetTextDatum(nodeToString(expr)); + + tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); + CatalogTupleInsert(rel, tup); + heap_freetuple(tup); + + ObjectAddressSet(myself, PropgraphLabelPropertyRelationId, plpoid); + + ObjectAddressSet(referenced, PropgraphPropertyRelationId, propoid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + + ObjectAddressSet(referenced, PropgraphElementLabelRelationId, ellabeloid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + + recordDependencyOnSingleRelExpr(&myself, (Node *) copyObject(expr), pgerelid, DEPENDENCY_NORMAL, DEPENDENCY_NORMAL, false); + + table_close(rel, NoLock); + } +} + +/* + * Check that for the given graph element, all properties with the same name + * have the same expression for each label. (See SQL/PGQ subclause "Creation + * of an element table descriptor".) + * + * We check this after all the catalog records are already inserted. This + * makes it easier to share this code between CREATE PROPERTY GRAPH and ALTER + * PROPERTY GRAPH. We pass in the element OID so that ALTER PROPERTY GRAPH + * only has to check the element it has just operated on. CREATE PROPERTY + * GROUP checks all elements it has created. + */ +static void +check_element_properties(Oid peoid) +{ + Relation rel1; + ScanKeyData key1[1]; + SysScanDesc scan1; + HeapTuple tuple1; + List *propoids = NIL; + List *propexprs = NIL; + + rel1 = table_open(PropgraphElementLabelRelationId, AccessShareLock); + ScanKeyInit(&key1[0], + Anum_pg_propgraph_element_label_pgelelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(peoid)); + + scan1 = systable_beginscan(rel1, PropgraphElementLabelElementLabelIndexId, true, NULL, 1, key1); + while (HeapTupleIsValid(tuple1 = systable_getnext(scan1))) + { + Form_pg_propgraph_element_label ellabel = (Form_pg_propgraph_element_label) GETSTRUCT(tuple1); + Relation rel2; + ScanKeyData key2[1]; + SysScanDesc scan2; + HeapTuple tuple2; + + rel2 = table_open(PropgraphLabelPropertyRelationId, AccessShareLock); + ScanKeyInit(&key2[0], + Anum_pg_propgraph_label_property_plpellabelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ellabel->oid)); + + scan2 = systable_beginscan(rel2, PropgraphLabelPropertyLabelPropIndexId, true, NULL, 1, key2); + while (HeapTupleIsValid(tuple2 = systable_getnext(scan2))) + { + Form_pg_propgraph_label_property lprop = (Form_pg_propgraph_label_property) GETSTRUCT(tuple2); + Oid propoid; + Datum datum; + bool isnull; + char *propexpr; + ListCell *lc1, + *lc2; + bool found; + + propoid = lprop->plppropid; + datum = heap_getattr(tuple2, Anum_pg_propgraph_label_property_plpexpr, RelationGetDescr(rel2), &isnull); + Assert(!isnull); + propexpr = TextDatumGetCString(datum); + + found = false; + forboth(lc1, propoids, lc2, propexprs) + { + if (propoid == lfirst_oid(lc1)) + { + Node *na, + *nb; + + na = stringToNode(propexpr); + nb = stringToNode(lfirst(lc2)); + + found = true; + + if (!equal(na, nb)) + { + HeapTuple tuple3; + Form_pg_propgraph_element elform; + List *dpcontext; + char *dpa, + *dpb; + + tuple3 = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(peoid)); + if (!tuple3) + elog(ERROR, "cache lookup failed for property graph element %u", peoid); + elform = (Form_pg_propgraph_element) GETSTRUCT(tuple3); + dpcontext = deparse_context_for(get_rel_name(elform->pgerelid), elform->pgerelid); + + dpa = deparse_expression(na, dpcontext, false, false); + dpb = deparse_expression(nb, dpcontext, false, false); + + /* + * show in sorted order to keep output independent of + * index order + */ + if (strcmp(dpa, dpb) > 0) + { + char *tmp; + + tmp = dpa; + dpa = dpb; + dpb = tmp; + } + + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("element \"%s\" property \"%s\" expression mismatch: %s vs. %s", + NameStr(elform->pgealias), get_propgraph_property_name(propoid), dpa, dpb), + errdetail("In a property graph element, a property of the same name has to have the same expression in each label.")); + + ReleaseSysCache(tuple3); + } + + break; + } + } + + if (!found) + { + propoids = lappend_oid(propoids, propoid); + propexprs = lappend(propexprs, propexpr); + } + } + systable_endscan(scan2); + table_close(rel2, AccessShareLock); + } + + systable_endscan(scan1); + table_close(rel1, AccessShareLock); +} + +/* + * Check that for the given element label, all labels of the same name in the + * graph have the same number and names of properties (independent of which + * element they are on). (See SQL/PGQ subclause "Consistency check of a + * tabular property graph descriptor".) + * + * We check this after all the catalog records are already inserted. This + * makes it easier to share this code between CREATE PROPERTY GRAPH and ALTER + * PROPERTY GRAPH. We pass in the element label OID so that some variants of + * ALTER PROPERTY GRAPH only have to check the element label it has just + * operated on. CREATE PROPERTY GRAPH and other ALTER PROPERTY GRAPH variants + * check all labels. + */ +static void +check_element_label_properties(Oid ellabeloid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + Oid labelid = InvalidOid; + Oid ref_ellabeloid = InvalidOid; + List *myprops, + *refprops; + List *diff1, + *diff2; + + rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + + /* + * Get element label info + */ + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_oid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(ellabeloid)); + scan = systable_beginscan(rel, PropgraphElementLabelObjectIndexId, true, NULL, 1, key); + if (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_propgraph_element_label ellabel = (Form_pg_propgraph_element_label) GETSTRUCT(tuple); + + labelid = ellabel->pgellabelid; + } + systable_endscan(scan); + if (!labelid) + elog(ERROR, "element label %u not found", ellabeloid); + + /* + * Find a reference element label to fetch label properties. The + * reference element label has to have the label OID as the one being + * checked but be distinct from the one being checked. + */ + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_pgellabelid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(labelid)); + scan = systable_beginscan(rel, PropgraphElementLabelLabelIndexId, true, NULL, 1, key); + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_propgraph_element_label otherellabel = (Form_pg_propgraph_element_label) GETSTRUCT(tuple); + + if (otherellabel->oid != ellabeloid) + { + ref_ellabeloid = otherellabel->oid; + break; + } + } + systable_endscan(scan); + + table_close(rel, AccessShareLock); + + /* + * If there is not previous definition of this label, then we are done. + */ + if (!ref_ellabeloid) + return; + + /* + * Now check number and names. + * + * XXX We could provide more detail in the error messages, but that would + * probably only be useful for some ALTER commands, because otherwise it's + * not really clear which label definition is the wrong one, and so you'd + * have to construct a rather verbose report to be of any use. Let's keep + * it simple for now. + */ + + myprops = get_element_label_property_names(ellabeloid); + refprops = get_element_label_property_names(ref_ellabeloid); + + if (list_length(refprops) != list_length(myprops)) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("mismatching number of properties in definition of label \"%s\"", get_propgraph_label_name(labelid))); + + diff1 = list_difference(myprops, refprops); + diff2 = list_difference(refprops, myprops); + + if (diff1 || diff2) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("mismatching properties names in definition of label \"%s\"", get_propgraph_label_name(labelid))); +} + +/* + * As above, but check all labels of a graph. + */ +static void +check_all_labels_properties(Oid pgrelid) +{ + foreach_oid(labeloid, get_graph_label_ids(pgrelid)) + { + foreach_oid(ellabeloid, get_label_element_label_ids(labeloid)) + { + check_element_label_properties(ellabeloid); + } + } +} + +/* + * ALTER PROPERTY GRAPH + */ +ObjectAddress +AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt) +{ + Oid pgrelid; + ListCell *lc; + ObjectAddress pgaddress; + + pgrelid = RangeVarGetRelidExtended(stmt->pgname, + ShareRowExclusiveLock, + stmt->missing_ok ? RVR_MISSING_OK : 0, + RangeVarCallbackOwnsRelation, + NULL); + if (pgrelid == InvalidOid) + { + ereport(NOTICE, + (errmsg("relation \"%s\" does not exist, skipping", + stmt->pgname->relname))); + return InvalidObjectAddress; + } + + ObjectAddressSet(pgaddress, RelationRelationId, pgrelid); + + foreach(lc, stmt->add_vertex_tables) + { + PropGraphVertex *vertex = lfirst_node(PropGraphVertex, lc); + struct element_info *vinfo; + Relation rel; + Oid peoid; + + vinfo = palloc0_object(struct element_info); + vinfo->kind = PGEKIND_VERTEX; + + vinfo->relid = RangeVarGetRelidExtended(vertex->vtable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL); + + rel = table_open(vinfo->relid, NoLock); + + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && get_rel_persistence(pgrelid) != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot add temporary element table to non-temporary property graph"), + errdetail("Table \"%s\" is a temporary table.", get_rel_name(vinfo->relid)), + parser_errposition(pstate, vertex->vtable->location))); + + if (vertex->vtable->alias) + vinfo->aliasname = vertex->vtable->alias->aliasname; + else + vinfo->aliasname = vertex->vtable->relname; + + vinfo->key = propgraph_element_get_key(pstate, vertex->vkey, rel, vinfo->aliasname, vertex->location); + + vinfo->labels = vertex->labels; + + table_close(rel, NoLock); + + if (SearchSysCacheExists2(PROPGRAPHELALIAS, + ObjectIdGetDatum(pgrelid), + CStringGetDatum(vinfo->aliasname))) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("alias \"%s\" already exists in property graph \"%s\"", + vinfo->aliasname, stmt->pgname->relname), + parser_errposition(pstate, vertex->vtable->location)); + + peoid = insert_element_record(pgaddress, vinfo); + + CommandCounterIncrement(); + check_element_properties(peoid); + check_all_labels_properties(pgrelid); + } + + foreach(lc, stmt->add_edge_tables) + { + PropGraphEdge *edge = lfirst_node(PropGraphEdge, lc); + struct element_info *einfo; + Relation rel; + Relation srcrel; + Relation destrel; + Oid peoid; + + einfo = palloc0_object(struct element_info); + einfo->kind = PGEKIND_EDGE; + + einfo->relid = RangeVarGetRelidExtended(edge->etable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL); + + rel = table_open(einfo->relid, NoLock); + + if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && get_rel_persistence(pgrelid) != RELPERSISTENCE_TEMP) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot add temporary element table to non-temporary property graph"), + errdetail("Table \"%s\" is a temporary table.", get_rel_name(einfo->relid)), + parser_errposition(pstate, edge->etable->location))); + + if (edge->etable->alias) + einfo->aliasname = edge->etable->alias->aliasname; + else + einfo->aliasname = edge->etable->relname; + + einfo->key = propgraph_element_get_key(pstate, edge->ekey, rel, einfo->aliasname, edge->location); + + einfo->srcvertexid = get_vertex_oid(pstate, pgrelid, edge->esrcvertex, edge->location); + einfo->destvertexid = get_vertex_oid(pstate, pgrelid, edge->edestvertex, edge->location); + + einfo->srcrelid = get_element_relid(einfo->srcvertexid); + einfo->destrelid = get_element_relid(einfo->destvertexid); + + srcrel = table_open(einfo->srcrelid, AccessShareLock); + destrel = table_open(einfo->destrelid, AccessShareLock); + + propgraph_edge_get_ref_keys(pstate, edge->esrckey, edge->esrcvertexcols, rel, srcrel, + einfo->aliasname, edge->location, "SOURCE", + &einfo->srckey, &einfo->srcref, &einfo->srceqop); + propgraph_edge_get_ref_keys(pstate, edge->edestkey, edge->edestvertexcols, rel, destrel, + einfo->aliasname, edge->location, "DESTINATION", + &einfo->destkey, &einfo->destref, &einfo->desteqop); + + einfo->labels = edge->labels; + + table_close(destrel, NoLock); + table_close(srcrel, NoLock); + + table_close(rel, NoLock); + + if (SearchSysCacheExists2(PROPGRAPHELALIAS, + ObjectIdGetDatum(pgrelid), + CStringGetDatum(einfo->aliasname))) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("alias \"%s\" already exists in property graph \"%s\"", + einfo->aliasname, stmt->pgname->relname), + parser_errposition(pstate, edge->etable->location)); + + peoid = insert_element_record(pgaddress, einfo); + + CommandCounterIncrement(); + check_element_properties(peoid); + check_all_labels_properties(pgrelid); + } + + foreach(lc, stmt->drop_vertex_tables) + { + char *alias = strVal(lfirst(lc)); + Oid peoid; + ObjectAddress obj; + + peoid = get_vertex_oid(pstate, pgrelid, alias, -1); + ObjectAddressSet(obj, PropgraphElementRelationId, peoid); + performDeletion(&obj, stmt->drop_behavior, 0); + } + + foreach(lc, stmt->drop_edge_tables) + { + char *alias = strVal(lfirst(lc)); + Oid peoid; + ObjectAddress obj; + + peoid = get_edge_oid(pstate, pgrelid, alias, -1); + ObjectAddressSet(obj, PropgraphElementRelationId, peoid); + performDeletion(&obj, stmt->drop_behavior, 0); + } + + /* Remove any orphaned pg_propgraph_label entries */ + if (stmt->drop_vertex_tables || stmt->drop_edge_tables) + { + foreach_oid(labeloid, get_graph_label_ids(pgrelid)) + { + if (!get_label_element_label_ids(labeloid)) + { + ObjectAddress obj; + + ObjectAddressSet(obj, PropgraphLabelRelationId, labeloid); + performDeletion(&obj, stmt->drop_behavior, 0); + } + } + } + + foreach(lc, stmt->add_labels) + { + PropGraphLabelAndProperties *lp = lfirst_node(PropGraphLabelAndProperties, lc); + Oid peoid; + Oid pgerelid; + Oid ellabeloid; + + Assert(lp->label); + + if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX) + peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1); + else + peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1); + + pgerelid = get_element_relid(peoid); + + ellabeloid = insert_label_record(pgrelid, peoid, lp->label); + insert_property_records(pgrelid, ellabeloid, pgerelid, lp->properties); + + CommandCounterIncrement(); + check_element_properties(peoid); + check_element_label_properties(ellabeloid); + } + + if (stmt->drop_label) + { + Oid peoid; + Oid labeloid; + Oid ellabeloid; + ObjectAddress obj; + + if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX) + peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1); + else + peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1); + + labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME, + Anum_pg_propgraph_label_oid, + ObjectIdGetDatum(pgrelid), + CStringGetDatum(stmt->drop_label)); + if (!labeloid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->drop_label), + parser_errposition(pstate, -1)); + + ellabeloid = GetSysCacheOid2(PROPGRAPHELEMENTLABELELEMENTLABEL, + Anum_pg_propgraph_element_label_oid, + ObjectIdGetDatum(peoid), + ObjectIdGetDatum(labeloid)); + + if (!ellabeloid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->drop_label), + parser_errposition(pstate, -1)); + + ObjectAddressSet(obj, PropgraphElementLabelRelationId, ellabeloid); + performDeletion(&obj, stmt->drop_behavior, 0); + + /* Remove any orphaned pg_propgraph_label entries */ + if (!get_label_element_label_ids(labeloid)) + { + ObjectAddressSet(obj, PropgraphLabelRelationId, labeloid); + performDeletion(&obj, stmt->drop_behavior, 0); + } + } + + if (stmt->add_properties) + { + Oid peoid; + Oid pgerelid; + Oid labeloid; + Oid ellabeloid; + + if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX) + peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1); + else + peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1); + + labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME, + Anum_pg_propgraph_label_oid, + ObjectIdGetDatum(pgrelid), + CStringGetDatum(stmt->alter_label)); + if (!labeloid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label), + parser_errposition(pstate, -1)); + + ellabeloid = GetSysCacheOid2(PROPGRAPHELEMENTLABELELEMENTLABEL, + Anum_pg_propgraph_element_label_oid, + ObjectIdGetDatum(peoid), + ObjectIdGetDatum(labeloid)); + if (!ellabeloid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label), + parser_errposition(pstate, -1)); + + pgerelid = get_element_relid(peoid); + + insert_property_records(pgrelid, ellabeloid, pgerelid, stmt->add_properties); + + CommandCounterIncrement(); + check_element_properties(peoid); + check_element_label_properties(ellabeloid); + } + + if (stmt->drop_properties) + { + Oid peoid; + Oid labeloid; + Oid ellabeloid; + ObjectAddress obj; + + if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX) + peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1); + else + peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1); + + labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME, + Anum_pg_propgraph_label_oid, + ObjectIdGetDatum(pgrelid), + CStringGetDatum(stmt->alter_label)); + if (!labeloid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label), + parser_errposition(pstate, -1)); + + ellabeloid = GetSysCacheOid2(PROPGRAPHELEMENTLABELELEMENTLABEL, + Anum_pg_propgraph_element_label_oid, + ObjectIdGetDatum(peoid), + ObjectIdGetDatum(labeloid)); + + if (!ellabeloid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label), + parser_errposition(pstate, -1)); + + foreach(lc, stmt->drop_properties) + { + char *propname = strVal(lfirst(lc)); + Oid propoid; + Oid plpoid; + + propoid = GetSysCacheOid2(PROPGRAPHPROPNAME, + Anum_pg_propgraph_property_oid, + ObjectIdGetDatum(pgrelid), + CStringGetDatum(propname)); + if (!propoid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" element \"%s\" label \"%s\" has no property \"%s\"", + get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label, propname), + parser_errposition(pstate, -1)); + + plpoid = GetSysCacheOid2(PROPGRAPHLABELPROP, Anum_pg_propgraph_label_property_oid, ObjectIdGetDatum(ellabeloid), ObjectIdGetDatum(propoid)); + + ObjectAddressSet(obj, PropgraphLabelPropertyRelationId, plpoid); + performDeletion(&obj, stmt->drop_behavior, 0); + } + + check_element_label_properties(ellabeloid); + } + + /* Remove any orphaned pg_propgraph_property entries */ + if (stmt->drop_properties || stmt->drop_vertex_tables || stmt->drop_edge_tables) + { + foreach_oid(propoid, get_graph_property_ids(pgrelid)) + { + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + + rel = table_open(PropgraphLabelPropertyRelationId, RowShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_label_property_plppropid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(propoid)); + /* XXX no suitable index */ + scan = systable_beginscan(rel, InvalidOid, true, NULL, 1, key); + if (!systable_getnext(scan)) + { + ObjectAddress obj; + + ObjectAddressSet(obj, PropgraphPropertyRelationId, propoid); + performDeletion(&obj, stmt->drop_behavior, 0); + } + + systable_endscan(scan); + table_close(rel, RowShareLock); + } + } + + /* + * Invalidate relcache entry of the property graph so that the queries in + * the cached plans referencing the property graph will be rewritten + * considering changes to the propert graph. + */ + CacheInvalidateRelcacheByRelid(pgrelid); + + return pgaddress; +} + +/* + * Get OID of vertex from graph OID and element alias. Element must be a + * vertex, otherwise error. + */ +static Oid +get_vertex_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location) +{ + HeapTuple tuple; + Oid peoid; + + tuple = SearchSysCache2(PROPGRAPHELALIAS, ObjectIdGetDatum(pgrelid), CStringGetDatum(alias)); + if (!tuple) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" has no element with alias \"%s\"", + get_rel_name(pgrelid), alias), + parser_errposition(pstate, location)); + + if (((Form_pg_propgraph_element) GETSTRUCT(tuple))->pgekind != PGEKIND_VERTEX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("element \"%s\" of property graph \"%s\" is not a vertex", + alias, get_rel_name(pgrelid)), + parser_errposition(pstate, location)); + + peoid = ((Form_pg_propgraph_element) GETSTRUCT(tuple))->oid; + + ReleaseSysCache(tuple); + + return peoid; +} + +/* + * Get OID of edge from graph OID and element alias. Element must be an edge, + * otherwise error. + */ +static Oid +get_edge_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location) +{ + HeapTuple tuple; + Oid peoid; + + tuple = SearchSysCache2(PROPGRAPHELALIAS, ObjectIdGetDatum(pgrelid), CStringGetDatum(alias)); + if (!tuple) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property graph \"%s\" has no element with alias \"%s\"", + get_rel_name(pgrelid), alias), + parser_errposition(pstate, location)); + + if (((Form_pg_propgraph_element) GETSTRUCT(tuple))->pgekind != PGEKIND_EDGE) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("element \"%s\" of property graph \"%s\" is not an edge", + alias, get_rel_name(pgrelid)), + parser_errposition(pstate, location)); + + peoid = ((Form_pg_propgraph_element) GETSTRUCT(tuple))->oid; + + ReleaseSysCache(tuple); + + return peoid; +} + +/* + * Get the element table relation OID from the OID of the element. + */ +static Oid +get_element_relid(Oid peid) +{ + HeapTuple tuple; + Oid pgerelid; + + tuple = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(peid)); + if (!tuple) + elog(ERROR, "cache lookup failed for property graph element %u", peid); + + pgerelid = ((Form_pg_propgraph_element) GETSTRUCT(tuple))->pgerelid; + + ReleaseSysCache(tuple); + + return pgerelid; +} + +/* + * Get a list of all label OIDs of a graph. + */ +static List * +get_graph_label_ids(Oid graphid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + List *result = NIL; + + rel = table_open(PropgraphLabelRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_label_pglpgid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(graphid)); + scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId, true, NULL, 1, key); + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + result = lappend_oid(result, ((Form_pg_propgraph_label) GETSTRUCT(tuple))->oid); + } + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return result; +} + +/* + * Get a list of all element label OIDs for a label. + */ +static List * +get_label_element_label_ids(Oid labelid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + List *result = NIL; + + rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_pgellabelid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(labelid)); + scan = systable_beginscan(rel, PropgraphElementLabelLabelIndexId, true, NULL, 1, key); + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + result = lappend_oid(result, ((Form_pg_propgraph_element_label) GETSTRUCT(tuple))->oid); + } + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return result; +} + +/* + * Get the names of properties associated with the given element label OID. + * + * The result is a list of String nodes (so we can use list functions to + * detect differences). + */ +static List * +get_element_label_property_names(Oid ellabeloid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + List *result = NIL; + + rel = table_open(PropgraphLabelPropertyRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_propgraph_label_property_plpellabelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ellabeloid)); + + scan = systable_beginscan(rel, PropgraphLabelPropertyLabelPropIndexId, true, NULL, 1, key); + + while ((tuple = systable_getnext(scan))) + { + Form_pg_propgraph_label_property plpform = (Form_pg_propgraph_label_property) GETSTRUCT(tuple); + + result = lappend(result, makeString(get_propgraph_property_name(plpform->plppropid))); + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return result; +} + +/* + * Get a list of all property OIDs of a graph. + */ +static List * +get_graph_property_ids(Oid graphid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tuple; + List *result = NIL; + + rel = table_open(PropgraphPropertyRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_property_pgppgid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(graphid)); + scan = systable_beginscan(rel, PropgraphPropertyNameIndexId, true, NULL, 1, key); + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + result = lappend_oid(result, ((Form_pg_propgraph_property) GETSTRUCT(tuple))->oid); + } + systable_endscan(scan); + table_close(rel, AccessShareLock); + + return result; +} diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index 0b23d94c38e20..440adb356ad43 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -3,7 +3,7 @@ * publicationcmds.c * publication manipulation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -29,7 +29,6 @@ #include "catalog/pg_publication.h" #include "catalog/pg_publication_namespace.h" #include "catalog/pg_publication_rel.h" -#include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/publicationcmds.h" @@ -122,7 +121,12 @@ parse_publication_options(ParseState *pstate, pubactions->pubtruncate = false; *publish_given = true; - publish = defGetString(defel); + + /* + * SplitIdentifierString destructively modifies its input, so make + * a copy so we don't modify the memory of the executing statement + */ + publish = pstrdup(defGetString(defel)); if (!SplitIdentifierString(publish, ',', &publish_list)) ereport(ERROR, @@ -177,7 +181,7 @@ parse_publication_options(ParseState *pstate, */ static void ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, - List **rels, List **schemas) + List **rels, List **exceptrels, List **schemas) { ListCell *cell; PublicationObjSpec *pubobj; @@ -194,7 +198,12 @@ ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, switch (pubobj->pubobjtype) { + case PUBLICATIONOBJ_EXCEPT_TABLE: + pubobj->pubtable->except = true; + *exceptrels = lappend(*exceptrels, pubobj->pubtable); + break; case PUBLICATIONOBJ_TABLE: + pubobj->pubtable->except = false; *rels = lappend(*rels, pubobj->pubtable); break; case PUBLICATIONOBJ_TABLES_IN_SCHEMA: @@ -515,8 +524,8 @@ InvalidatePubRelSyncCache(Oid pubid, bool puballtables) * a target. However, WAL records for TRUNCATE specify both a root and * its leaves. */ - relids = GetPublicationRelations(pubid, - PUBLICATION_PART_ALL); + relids = GetIncludedPublicationRelations(pubid, + PUBLICATION_PART_ALL); schemarelids = GetAllSchemaPublicationRelations(pubid, PUBLICATION_PART_ALL); @@ -840,6 +849,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) char publish_generated_columns; AclResult aclresult; List *relations = NIL; + List *exceptrelations = NIL; List *schemaidlist = NIL; /* must have CREATE privilege on database */ @@ -848,11 +858,14 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) aclcheck_error(aclresult, OBJECT_DATABASE, get_database_name(MyDatabaseId)); - /* FOR ALL TABLES requires superuser */ - if (stmt->for_all_tables && !superuser()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("must be superuser to create FOR ALL TABLES publication"))); + /* FOR ALL TABLES and FOR ALL SEQUENCES requires superuser */ + if (!superuser()) + { + if (stmt->for_all_tables || stmt->for_all_sequences) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to create a FOR ALL TABLES or ALL SEQUENCES publication")); + } rel = table_open(PublicationRelationId, RowExclusiveLock); @@ -881,11 +894,20 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) &publish_generated_columns_given, &publish_generated_columns); + if (stmt->for_all_sequences && + (publish_given || publish_via_partition_root_given || + publish_generated_columns_given)) + ereport(NOTICE, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication parameters are not applicable to sequence synchronization and will be ignored for sequences")); + puboid = GetNewOidWithIndex(rel, PublicationObjectIndexId, Anum_pg_publication_oid); values[Anum_pg_publication_oid - 1] = ObjectIdGetDatum(puboid); values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(stmt->for_all_tables); + values[Anum_pg_publication_puballsequences - 1] = + BoolGetDatum(stmt->for_all_sequences); values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(pubactions.pubinsert); values[Anum_pg_publication_pubupdate - 1] = @@ -913,16 +935,30 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) CommandCounterIncrement(); /* Associate objects with the publication. */ + ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, + &exceptrelations, &schemaidlist); + if (stmt->for_all_tables) { - /* Invalidate relcache so that publication info is rebuilt. */ + /* Process EXCEPT table list */ + if (exceptrelations != NIL) + { + List *rels; + + rels = OpenTableList(exceptrelations); + PublicationAddTables(puboid, rels, true, NULL); + CloseTableList(rels); + } + + /* + * Invalidate relcache so that publication info is rebuilt. Sequences + * publication doesn't require invalidation, as replica identity + * checks don't apply to them. + */ CacheInvalidateRelcacheAll(); } - else + else if (!stmt->for_all_sequences) { - ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, - &schemaidlist); - /* FOR TABLES IN SCHEMA requires superuser */ if (schemaidlist != NIL && !superuser()) ereport(ERROR, @@ -960,11 +996,16 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0); - if (wal_level != WAL_LEVEL_LOGICAL) + /* + * We don't need this warning message when wal_level >= 'replica' since + * logical decoding is automatically enabled up on a logical slot + * creation. + */ + if (wal_level < WAL_LEVEL_REPLICA) ereport(WARNING, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("\"wal_level\" is insufficient to publish logical changes"), - errhint("Set \"wal_level\" to \"logical\" before creating subscriptions."))); + errmsg("logical decoding must be enabled to publish logical changes"), + errhint("Before creating subscriptions, ensure that \"wal_level\" is set to \"replica\" or higher."))); return myself; } @@ -990,6 +1031,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, List *root_relids = NIL; ListCell *lc; + pubform = (Form_pg_publication) GETSTRUCT(tup); + parse_publication_options(pstate, stmt->options, &publish_given, &pubactions, @@ -998,7 +1041,12 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, &publish_generated_columns_given, &publish_generated_columns); - pubform = (Form_pg_publication) GETSTRUCT(tup); + if (pubform->puballsequences && + (publish_given || publish_via_partition_root_given || + publish_generated_columns_given)) + ereport(NOTICE, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication parameters are not applicable to sequence synchronization and will be ignored for sequences")); /* * If the publication doesn't publish changes via the root partitioned @@ -1018,8 +1066,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, LockDatabaseObject(PublicationRelationId, pubform->oid, 0, AccessShareLock); - root_relids = GetPublicationRelations(pubform->oid, - PUBLICATION_PART_ROOT); + root_relids = GetIncludedPublicationRelations(pubform->oid, + PUBLICATION_PART_ROOT); foreach(lc, root_relids) { @@ -1138,8 +1186,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, * trees, not just those explicitly mentioned in the publication. */ if (root_relids == NIL) - relids = GetPublicationRelations(pubform->oid, - PUBLICATION_PART_ALL); + relids = GetIncludedPublicationRelations(pubform->oid, + PUBLICATION_PART_ALL); else { /* @@ -1224,15 +1272,37 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, PublicationDropTables(pubid, rels, false); else /* AP_SetObjects */ { - List *oldrelids = GetPublicationRelations(pubid, - PUBLICATION_PART_ROOT); + List *oldrelids = NIL; List *delrels = NIL; ListCell *oldlc; - TransformPubWhereClauses(rels, queryString, pubform->pubviaroot); + if (stmt->for_all_tables || stmt->for_all_sequences) + { + /* + * In FOR ALL TABLES mode, relations are tracked as exclusions + * (EXCEPT clause). Fetch the current excluded relations so they + * can be reconciled with the specified EXCEPT list. + * + * This applies only if the existing publication is already + * defined as FOR ALL TABLES; otherwise, there are no exclusion + * entries to process. + */ + if (pubform->puballtables) + { + oldrelids = GetExcludedPublicationTables(pubid, + PUBLICATION_PART_ROOT); + } + } + else + { + oldrelids = GetIncludedPublicationRelations(pubid, + PUBLICATION_PART_ROOT); - CheckPubRelationColumnList(stmt->pubname, rels, publish_schema, - pubform->pubviaroot); + TransformPubWhereClauses(rels, queryString, pubform->pubviaroot); + + CheckPubRelationColumnList(stmt->pubname, rels, publish_schema, + pubform->pubviaroot); + } /* * To recreate the relation list for the publication, look for @@ -1323,9 +1393,10 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, */ if (!found) { - oldrel = palloc(sizeof(PublicationRelInfo)); + oldrel = palloc_object(PublicationRelInfo); oldrel->whereClause = NULL; oldrel->columns = NIL; + oldrel->except = false; oldrel->relation = table_open(oldrelid, ShareUpdateExclusiveLock); delrels = lappend(delrels, oldrel); @@ -1376,7 +1447,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt, ListCell *lc; List *reloids; - reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT); + reloids = GetIncludedPublicationRelations(pubform->oid, + PUBLICATION_PART_ROOT); foreach(lc, reloids) { @@ -1448,24 +1520,131 @@ CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to add or set schemas"))); + if (stmt->for_all_tables && !superuser()) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to set ALL TABLES")); + + if (stmt->for_all_sequences && !superuser()) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to set ALL SEQUENCES")); + /* * Check that user is allowed to manipulate the publication tables in * schema */ - if (schemaidlist && pubform->puballtables) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("publication \"%s\" is defined as FOR ALL TABLES", - NameStr(pubform->pubname)), - errdetail("Schemas cannot be added to or dropped from FOR ALL TABLES publications."))); + if (schemaidlist && (pubform->puballtables || pubform->puballsequences)) + { + if (pubform->puballtables && pubform->puballsequences) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is defined as FOR ALL TABLES, ALL SEQUENCES", + NameStr(pubform->pubname)), + errdetail("Schemas cannot be added to or dropped from FOR ALL TABLES, ALL SEQUENCES publications.")); + else if (pubform->puballtables) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is defined as FOR ALL TABLES", + NameStr(pubform->pubname)), + errdetail("Schemas cannot be added to or dropped from FOR ALL TABLES publications.")); + else + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is defined as FOR ALL SEQUENCES", + NameStr(pubform->pubname)), + errdetail("Schemas cannot be added to or dropped from FOR ALL SEQUENCES publications.")); + } /* Check that user is allowed to manipulate the publication tables. */ - if (tables && pubform->puballtables) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("publication \"%s\" is defined as FOR ALL TABLES", - NameStr(pubform->pubname)), - errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications."))); + if (tables && (pubform->puballtables || pubform->puballsequences)) + { + if (pubform->puballtables && pubform->puballsequences) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is defined as FOR ALL TABLES, ALL SEQUENCES", + NameStr(pubform->pubname)), + errdetail("Tables or sequences cannot be added to or dropped from FOR ALL TABLES, ALL SEQUENCES publications.")); + else if (pubform->puballtables) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is defined as FOR ALL TABLES", + NameStr(pubform->pubname)), + errdetail("Tables or sequences cannot be added to or dropped from FOR ALL TABLES publications.")); + else + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is defined as FOR ALL SEQUENCES", + NameStr(pubform->pubname)), + errdetail("Tables or sequences cannot be added to or dropped from FOR ALL SEQUENCES publications.")); + } + + if (stmt->for_all_tables || stmt->for_all_sequences) + { + /* + * If the publication already contains specific tables or schemas, we + * prevent switching to a ALL state. + */ + if (is_table_publication(pubform->oid) || + is_schema_publication(pubform->oid)) + { + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + stmt->for_all_tables ? + errmsg("publication \"%s\" does not support ALL TABLES operations", NameStr(pubform->pubname)) : + errmsg("publication \"%s\" does not support ALL SEQUENCES operations", NameStr(pubform->pubname)), + errdetail("This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty.")); + } + } +} + +/* + * Update FOR ALL TABLES / FOR ALL SEQUENCES flags of a publication. + */ +static void +AlterPublicationAllFlags(AlterPublicationStmt *stmt, Relation rel, + HeapTuple tup) +{ + Form_pg_publication pubform; + bool nulls[Natts_pg_publication] = {0}; + bool replaces[Natts_pg_publication] = {0}; + Datum values[Natts_pg_publication] = {0}; + bool dirty = false; + + if (!stmt->for_all_tables && !stmt->for_all_sequences) + return; + + pubform = (Form_pg_publication) GETSTRUCT(tup); + + /* Update FOR ALL TABLES flag if changed */ + if (stmt->for_all_tables != pubform->puballtables) + { + values[Anum_pg_publication_puballtables - 1] = + BoolGetDatum(stmt->for_all_tables); + replaces[Anum_pg_publication_puballtables - 1] = true; + dirty = true; + } + + /* Update FOR ALL SEQUENCES flag if changed */ + if (stmt->for_all_sequences != pubform->puballsequences) + { + values[Anum_pg_publication_puballsequences - 1] = + BoolGetDatum(stmt->for_all_sequences); + replaces[Anum_pg_publication_puballsequences - 1] = true; + dirty = true; + } + + if (dirty) + { + tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, + nulls, replaces); + CatalogTupleUpdate(rel, &tup->t_self, tup); + CommandCounterIncrement(); + + /* For ALL TABLES, we must invalidate all relcache entries */ + if (replaces[Anum_pg_publication_puballtables - 1]) + CacheInvalidateRelcacheAll(); + } } /* @@ -1504,11 +1683,12 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) else { List *relations = NIL; + List *exceptrelations = NIL; List *schemaidlist = NIL; Oid pubid = pubform->oid; ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, - &schemaidlist); + &exceptrelations, &schemaidlist); CheckAlterPublication(stmt, tup, relations, schemaidlist); @@ -1531,9 +1711,11 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) errmsg("publication \"%s\" does not exist", stmt->pubname)); + relations = list_concat(relations, exceptrelations); AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext, schemaidlist != NIL); AlterPublicationSchemas(stmt, tup, schemaidlist); + AlterPublicationAllFlags(stmt, rel, tup); } /* Cleanup. */ @@ -1705,10 +1887,11 @@ OpenTableList(List *tables) continue; } - pub_rel = palloc(sizeof(PublicationRelInfo)); + pub_rel = palloc_object(PublicationRelInfo); pub_rel->relation = rel; pub_rel->whereClause = t->whereClause; pub_rel->columns = t->columns; + pub_rel->except = t->except; rels = lappend(rels, pub_rel); relids = lappend_oid(relids, myrelid); @@ -1774,13 +1957,14 @@ OpenTableList(List *tables) /* find_all_inheritors already got lock */ rel = table_open(childrelid, NoLock); - pub_rel = palloc(sizeof(PublicationRelInfo)); + pub_rel = palloc_object(PublicationRelInfo); pub_rel->relation = rel; /* child inherits WHERE clause from parent */ pub_rel->whereClause = t->whereClause; /* child inherits column list from parent */ pub_rel->columns = t->columns; + pub_rel->except = t->except; rels = lappend(rels, pub_rel); relids = lappend_oid(relids, childrelid); @@ -1856,8 +2040,6 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, { ListCell *lc; - Assert(!stmt || !stmt->for_all_tables); - foreach(lc, rels) { PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc); @@ -1869,7 +2051,7 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind), RelationGetRelationName(rel)); - obj = publication_add_relation(pubid, pub_rel, if_not_exists); + obj = publication_add_relation(pubid, pub_rel, if_not_exists, stmt); if (stmt) { EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress, @@ -1935,8 +2117,6 @@ PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists, { ListCell *lc; - Assert(!stmt || !stmt->for_all_tables); - foreach(lc, schemas) { Oid schemaid = lfirst_oid(lc); @@ -2019,19 +2199,16 @@ AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId) aclcheck_error(aclresult, OBJECT_DATABASE, get_database_name(MyDatabaseId)); - if (form->puballtables && !superuser_arg(newOwnerId)) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied to change owner of publication \"%s\"", - NameStr(form->pubname)), - errhint("The owner of a FOR ALL TABLES publication must be a superuser."))); - - if (!superuser_arg(newOwnerId) && is_schema_publication(form->oid)) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied to change owner of publication \"%s\"", - NameStr(form->pubname)), - errhint("The owner of a FOR TABLES IN SCHEMA publication must be a superuser."))); + if (!superuser_arg(newOwnerId)) + { + if (form->puballtables || form->puballsequences || + is_schema_publication(form->oid)) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to change owner of publication \"%s\"", + NameStr(form->pubname)), + errhint("The owner of a FOR ALL TABLES or ALL SEQUENCES or TABLES IN SCHEMA publication must be a superuser.")); + } } form->pubowner = newOwnerId; @@ -2113,25 +2290,25 @@ AlterPublicationOwner_oid(Oid pubid, Oid newOwnerId) static char defGetGeneratedColsOption(DefElem *def) { - char *sval; + char *sval = ""; /* - * If no parameter value given, assume "stored" is meant. + * A parameter value is required. */ - if (!def->arg) - return PUBLISH_GENCOLS_STORED; - - sval = defGetString(def); + if (def->arg) + { + sval = defGetString(def); - if (pg_strcasecmp(sval, "none") == 0) - return PUBLISH_GENCOLS_NONE; - if (pg_strcasecmp(sval, "stored") == 0) - return PUBLISH_GENCOLS_STORED; + if (pg_strcasecmp(sval, "none") == 0) + return PUBLISH_GENCOLS_NONE; + if (pg_strcasecmp(sval, "stored") == 0) + return PUBLISH_GENCOLS_STORED; + } ereport(ERROR, errcode(ERRCODE_SYNTAX_ERROR), - errmsg("%s requires a \"none\" or \"stored\" value", - def->defname)); + errmsg("invalid value for publication parameter \"%s\": \"%s\"", def->defname, sval), + errdetail("Valid values are \"%s\" and \"%s\".", "none", "stored")); return PUBLISH_GENCOLS_NONE; /* keep compiler quiet */ } diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c new file mode 100644 index 0000000000000..bafdca80810ec --- /dev/null +++ b/src/backend/commands/repack.c @@ -0,0 +1,3643 @@ +/*------------------------------------------------------------------------- + * + * repack.c + * REPACK a table; formerly known as CLUSTER. VACUUM FULL also uses + * parts of this code. + * + * There are two somewhat different ways to rewrite a table. In non- + * concurrent mode, it's easy: take AccessExclusiveLock, create a new + * transient relation, copy the tuples over to the relfilenode of the new + * relation, swap the relfilenodes, then drop the old relation. + * + * In concurrent mode, we lock the table with only ShareUpdateExclusiveLock, + * then do an initial copy as above. However, while the tuples are being + * copied, concurrent transactions could modify the table. To cope with those + * changes, we rely on logical decoding to obtain them from WAL. A bgworker + * consumes WAL while the initial copy is ongoing (to prevent excessive WAL + * from being reserved), and accumulates the changes in a file. Once the + * initial copy is complete, we read the changes from the file and re-apply + * them on the new heap. Then we upgrade our ShareUpdateExclusiveLock to + * AccessExclusiveLock and swap the relfilenodes. This way, the time we hold + * a strong lock on the table is much reduced, and the bloat is eliminated. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/repack.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/amapi.h" +#include "access/heapam.h" +#include "access/multixact.h" +#include "access/relscan.h" +#include "access/tableam.h" +#include "access/toast_internals.h" +#include "access/transam.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "catalog/heap.h" +#include "catalog/index.h" +#include "catalog/namespace.h" +#include "catalog/objectaccess.h" +#include "catalog/pg_am.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_inherits.h" +#include "catalog/toasting.h" +#include "commands/defrem.h" +#include "commands/progress.h" +#include "commands/repack.h" +#include "commands/repack_internal.h" +#include "commands/tablecmds.h" +#include "commands/vacuum.h" +#include "executor/executor.h" +#include "libpq/pqformat.h" +#include "libpq/pqmq.h" +#include "miscadmin.h" +#include "optimizer/optimizer.h" +#include "pgstat.h" +#include "storage/bufmgr.h" +#include "storage/lmgr.h" +#include "storage/predicate.h" +#include "storage/proc.h" +#include "utils/acl.h" +#include "utils/fmgroids.h" +#include "utils/guc.h" +#include "utils/injection_point.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/pg_rusage.h" +#include "utils/relmapper.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" +#include "utils/wait_event_types.h" + +/* + * This struct is used to pass around the information on tables to be + * clustered. We need this so we can make a list of them when invoked without + * a specific table/index pair. + */ +typedef struct +{ + Oid tableOid; + Oid indexOid; +} RelToCluster; + +/* + * The first file exported by the decoding worker must contain a snapshot, the + * following ones contain the data changes. + */ +#define WORKER_FILE_SNAPSHOT 0 + +/* + * Information needed to apply concurrent data changes. + */ +typedef struct ChangeContext +{ + /* The relation the changes are applied to. */ + Relation cc_rel; + + /* Needed to update indexes of cc_rel. */ + ResultRelInfo *cc_rri; + EState *cc_estate; + + /* + * Existing tuples to UPDATE and DELETE are located via this index. We + * keep the scankey in partially initialized state to avoid repeated work. + * sk_argument is completed on the fly. + */ + Relation cc_ident_index; + ScanKey cc_ident_key; + int cc_ident_key_nentries; + + /* Sequential number of the file containing the changes. */ + int cc_file_seq; +} ChangeContext; + +/* + * Backend-local information to control the decoding worker. + */ +typedef struct DecodingWorker +{ + /* The worker. */ + BackgroundWorkerHandle *handle; + + /* DecodingWorkerShared is in this segment. */ + dsm_segment *seg; + + /* Handle of the error queue. */ + shm_mq_handle *error_mqh; +} DecodingWorker; + +/* Pointer to currently running decoding worker. */ +static DecodingWorker *decoding_worker = NULL; + +/* + * Is there a message sent by a repack worker that the backend needs to + * receive? + */ +volatile sig_atomic_t RepackMessagePending = false; + +static LOCKMODE RepackLockLevel(bool concurrent); +static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, + Oid indexOid, Oid userid, LOCKMODE lmode, + int options); +static void check_concurrent_repack_requirements(Relation rel, + Oid *ident_idx_p); +static void rebuild_relation(Relation OldHeap, Relation index, bool verbose, + Oid ident_idx); +static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, + Snapshot snapshot, + bool verbose, + bool *pSwapToastByContent, + TransactionId *pFreezeXid, + MultiXactId *pCutoffMulti); +static List *get_tables_to_repack(RepackCommand cmd, bool usingindex, + MemoryContext permcxt); +static List *get_tables_to_repack_partitioned(RepackCommand cmd, + Oid relid, bool rel_is_index, + MemoryContext permcxt); +static bool repack_is_permitted_for_relation(RepackCommand cmd, + Oid relid, Oid userid); + +static void apply_concurrent_changes(BufFile *file, ChangeContext *chgcxt); +static void apply_concurrent_insert(Relation rel, TupleTableSlot *slot, + ChangeContext *chgcxt); +static void apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple, + TupleTableSlot *ondisk_tuple, + ChangeContext *chgcxt); +static void apply_concurrent_delete(Relation rel, TupleTableSlot *slot); +static void restore_tuple(BufFile *file, Relation relation, + TupleTableSlot *slot); +static void adjust_toast_pointers(Relation relation, TupleTableSlot *dest, + TupleTableSlot *src); +static bool find_target_tuple(Relation rel, ChangeContext *chgcxt, + TupleTableSlot *locator, + TupleTableSlot *retrieved); +static void process_concurrent_changes(XLogRecPtr end_of_wal, + ChangeContext *chgcxt, + bool done); +static void initialize_change_context(ChangeContext *chgcxt, + Relation relation, + Oid ident_index_id); +static void release_change_context(ChangeContext *chgcxt); +static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, + Oid identIdx, + TransactionId frozenXid, + MultiXactId cutoffMulti); +static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes); +static void copy_index_constraints(Relation old_index, Oid new_index_id, + Oid new_heap_id); +static Relation process_single_relation(RepackStmt *stmt, + LOCKMODE lockmode, + bool isTopLevel, + ClusterParams *params); +static Oid determine_clustered_index(Relation rel, bool usingindex, + const char *indexname); + +static void start_repack_decoding_worker(Oid relid); +static void stop_repack_decoding_worker(void); +static Snapshot get_initial_snapshot(DecodingWorker *worker); + +static void ProcessRepackMessage(StringInfo msg); +static const char *RepackCommandAsString(RepackCommand cmd); + + +/* + * The repack code allows for processing multiple tables at once. Because + * of this, we cannot just run everything on a single transaction, or we + * would be forced to acquire exclusive locks on all the tables being + * clustered, simultaneously --- very likely leading to deadlock. + * + * To solve this we follow a similar strategy to VACUUM code, processing each + * relation in a separate transaction. For this to work, we need to: + * + * - provide a separate memory context so that we can pass information in + * a way that survives across transactions + * - start a new transaction every time a new relation is clustered + * - check for validity of the information on to-be-clustered relations, + * as someone might have deleted a relation behind our back, or + * clustered one on a different index + * - end the transaction + * + * The single-relation case does not have any such overhead. + * + * We also allow a relation to be repacked following an index, but without + * naming a specific one. In that case, the indisclustered bit will be + * looked up, and an ERROR will be thrown if no so-marked index is found. + */ +void +ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel) +{ + ClusterParams params = {0}; + Relation rel = NULL; + MemoryContext repack_context; + LOCKMODE lockmode; + List *rtcs; + + /* Parse option list */ + foreach_node(DefElem, opt, stmt->params) + { + if (strcmp(opt->defname, "verbose") == 0) + params.options |= defGetBoolean(opt) ? CLUOPT_VERBOSE : 0; + else if (strcmp(opt->defname, "analyze") == 0 || + strcmp(opt->defname, "analyse") == 0) + params.options |= defGetBoolean(opt) ? CLUOPT_ANALYZE : 0; + else if (strcmp(opt->defname, "concurrently") == 0 && + defGetBoolean(opt)) + { + if (stmt->command != REPACK_COMMAND_REPACK) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("CONCURRENTLY option not supported for %s", + RepackCommandAsString(stmt->command))); + params.options |= CLUOPT_CONCURRENT; + } + else + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized %s option \"%s\"", + RepackCommandAsString(stmt->command), + opt->defname), + parser_errposition(pstate, opt->location)); + } + + /* Determine the lock mode to use. */ + lockmode = RepackLockLevel((params.options & CLUOPT_CONCURRENT) != 0); + + if ((params.options & CLUOPT_CONCURRENT) != 0) + { + /* + * Make sure we're not in a transaction block. + * + * The reason is that repack_setup_logical_decoding() could wait + * indefinitely for our XID to complete. (The deadlock detector would + * not recognize it because we'd be waiting for ourselves, i.e. no + * real lock conflict.) It would be possible to run in a transaction + * block if we had no XID, but this restriction is simpler for users + * to understand and we don't lose any functionality. + */ + PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)"); + } + + /* + * If a single relation is specified, process it and we're done ... unless + * the relation is a partitioned table, in which case we fall through. + */ + if (stmt->relation != NULL) + { + rel = process_single_relation(stmt, lockmode, isTopLevel, ¶ms); + if (rel == NULL) + return; /* all done */ + } + + /* + * Don't allow ANALYZE in the multiple-relation case for now. Maybe we + * can add support for this later. + */ + if (params.options & CLUOPT_ANALYZE) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot execute %s on multiple tables", + "REPACK (ANALYZE)")); + + /* + * By here, we know we are in a multi-table situation. + * + * Concurrent processing is currently considered rather special (e.g. in + * terms of resources consumed) so it is not performed in bulk. + */ + if (params.options & CLUOPT_CONCURRENT) + { + if (rel != NULL) + { + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("REPACK (CONCURRENTLY) is not supported for partitioned tables"), + errhint("Consider running the command on individual partitions.")); + } + else + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("REPACK (CONCURRENTLY) requires an explicit table name")); + } + + /* + * In order to avoid holding locks for too long, we want to process each + * table in its own transaction. This forces us to disallow running + * inside a user transaction block. + */ + PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command)); + + /* Also, we need a memory context to hold our list of relations */ + repack_context = AllocSetContextCreate(PortalContext, + "Repack", + ALLOCSET_DEFAULT_SIZES); + + /* + * Since we open a new transaction for each relation, we have to check + * that the relation still is what we think it is. + * + * In single-transaction CLUSTER, we don't need the overhead. + */ + params.options |= CLUOPT_RECHECK; + + /* + * If we don't have a relation yet, determine a relation list. If we do, + * then it must be a partitioned table, and we want to process its + * partitions. + */ + if (rel == NULL) + { + Assert(stmt->indexname == NULL); + rtcs = get_tables_to_repack(stmt->command, stmt->usingindex, + repack_context); + params.options |= CLUOPT_RECHECK_ISCLUSTERED; + } + else + { + Oid relid; + bool rel_is_index; + + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + + /* + * If USING INDEX was specified, resolve the index name now and pass + * it down. + */ + if (stmt->usingindex) + { + /* + * If no index name was specified when repacking a partitioned + * table, punt for now. Maybe we can improve this later. + */ + if (!stmt->indexname) + { + if (stmt->command == REPACK_COMMAND_CLUSTER) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("there is no previously clustered index for table \"%s\"", + RelationGetRelationName(rel))); + else + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + /*- translator: first %s is name of a SQL command, eg. REPACK */ + errmsg("cannot execute %s on partitioned table \"%s\" USING INDEX with no index name", + RepackCommandAsString(stmt->command), + RelationGetRelationName(rel))); + } + + relid = determine_clustered_index(rel, stmt->usingindex, + stmt->indexname); + if (!OidIsValid(relid)) + elog(ERROR, "unable to determine index to cluster on"); + check_index_is_clusterable(rel, relid, AccessExclusiveLock); + + rel_is_index = true; + } + else + { + relid = RelationGetRelid(rel); + rel_is_index = false; + } + + rtcs = get_tables_to_repack_partitioned(stmt->command, + relid, rel_is_index, + repack_context); + + /* close parent relation, releasing lock on it */ + table_close(rel, AccessExclusiveLock); + rel = NULL; + } + + /* Commit to get out of starting transaction */ + PopActiveSnapshot(); + CommitTransactionCommand(); + + /* Cluster the tables, each in a separate transaction */ + Assert(rel == NULL); + foreach_ptr(RelToCluster, rtc, rtcs) + { + /* Start a new transaction for each relation. */ + StartTransactionCommand(); + + /* + * Open the target table, coping with the case where it has been + * dropped. + */ + rel = try_table_open(rtc->tableOid, lockmode); + if (rel == NULL) + { + CommitTransactionCommand(); + continue; + } + + /* functions in indexes may want a snapshot set */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* Process this table */ + cluster_rel(stmt->command, rel, rtc->indexOid, ¶ms, isTopLevel); + /* cluster_rel closes the relation, but keeps lock */ + + PopActiveSnapshot(); + CommitTransactionCommand(); + } + + /* Start a new transaction for the cleanup work. */ + StartTransactionCommand(); + + /* Clean up working storage */ + MemoryContextDelete(repack_context); +} + +/* + * In the non-concurrent case, we obtain AccessExclusiveLock throughout the + * operation to avoid any lock-upgrade hazards. In the concurrent case, we + * grab ShareUpdateExclusiveLock (just like VACUUM) for most of the + * processing and only acquire AccessExclusiveLock at the end, to swap the + * relation -- supposedly for a short time. + */ +static LOCKMODE +RepackLockLevel(bool concurrent) +{ + if (concurrent) + return ShareUpdateExclusiveLock; + else + return AccessExclusiveLock; +} + +/* + * cluster_rel + * + * This clusters the table by creating a new, clustered table and + * swapping the relfilenumbers of the new table and the old table, so + * the OID of the original table is preserved. Thus we do not lose + * GRANT, inheritance nor references to this table. + * + * Indexes are rebuilt too, via REINDEX. Since we are effectively bulk-loading + * the new table, it's better to create the indexes afterwards than to fill + * them incrementally while we load the table. + * + * If indexOid is InvalidOid, the table will be rewritten in physical order + * instead of index order. + * + * Note that, in the concurrent case, the function releases the lock at some + * point, in order to get AccessExclusiveLock for the final steps (i.e. to + * swap the relation files). To make things simpler, the caller should expect + * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The + * AccessExclusiveLock is kept till the end of the transaction.) + * + * 'cmd' indicates which command is being executed, to be used for error + * messages. + */ +void +cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, + ClusterParams *params, bool isTopLevel) +{ + Oid tableOid = RelationGetRelid(OldHeap); + Relation index; + LOCKMODE lmode; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + bool verbose = ((params->options & CLUOPT_VERBOSE) != 0); + bool recheck = ((params->options & CLUOPT_RECHECK) != 0); + bool concurrent = ((params->options & CLUOPT_CONCURRENT) != 0); + Oid ident_idx = InvalidOid; + + /* Determine the lock mode to use. */ + lmode = RepackLockLevel(concurrent); + + /* + * Check some preconditions in the concurrent case. This also obtains the + * replica index OID. + */ + if (concurrent) + check_concurrent_repack_requirements(OldHeap, &ident_idx); + + /* Check for user-requested abort. */ + CHECK_FOR_INTERRUPTS(); + + pgstat_progress_start_command(PROGRESS_COMMAND_REPACK, tableOid); + pgstat_progress_update_param(PROGRESS_REPACK_COMMAND, cmd); + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also lock down security-restricted operations and + * arrange to make GUC variable changes local to this command. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(OldHeap->rd_rel->relowner, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* + * Recheck that the relation is still what it was when we started. + * + * Note that it's critical to skip this in single-relation CLUSTER; + * otherwise, we would reject an attempt to cluster using a + * not-previously-clustered index. + */ + if (recheck && + !cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid, + lmode, params->options)) + goto out; + + /* + * We allow repacking shared catalogs only when not using an index. It + * would work to use an index in most respects, but the index would only + * get marked as indisclustered in the current database, leading to + * unexpected behavior if CLUSTER were later invoked in another database. + */ + if (OidIsValid(indexOid) && OldHeap->rd_rel->relisshared) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /*- translator: first %s is name of a SQL command, eg. REPACK */ + errmsg("cannot execute %s on a shared catalog", + RepackCommandAsString(cmd))); + + /* + * The CONCURRENTLY case should have been rejected earlier because it does + * not support system catalogs. + */ + Assert(!(OldHeap->rd_rel->relisshared && concurrent)); + + /* + * Don't process temp tables of other backends ... their local buffer + * manager is not going to cope. + */ + if (RELATION_IS_OTHER_TEMP(OldHeap)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /*- translator: first %s is name of a SQL command, eg. REPACK */ + errmsg("cannot execute %s on temporary tables of other sessions", + RepackCommandAsString(cmd))); + + /* + * Also check for active uses of the relation in the current transaction, + * including open scans and pending AFTER trigger events. + */ + CheckTableNotInUse(OldHeap, RepackCommandAsString(cmd)); + + /* Check heap and index are valid to cluster on */ + if (OidIsValid(indexOid)) + { + /* verify the index is good and lock it */ + check_index_is_clusterable(OldHeap, indexOid, lmode); + /* also open it */ + index = index_open(indexOid, NoLock); + } + else + index = NULL; + + /* + * When allow_system_table_mods is turned off, we disallow repacking a + * catalog on a particular index unless that's already the clustered index + * for that catalog. + * + * XXX We don't check for this in CLUSTER, because it's historically been + * allowed. + */ + if (cmd != REPACK_COMMAND_CLUSTER && + !allowSystemTableMods && OidIsValid(indexOid) && + IsCatalogRelation(OldHeap) && !index->rd_index->indisclustered) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + RelationGetRelationName(OldHeap)), + errdetail("System catalogs can only be clustered by the index they're already clustered on, if any, unless \"%s\" is enabled.", + "allow_system_table_mods")); + + /* + * Quietly ignore the request if this is a materialized view which has not + * been populated from its query. No harm is done because there is no data + * to deal with, and we don't want to throw an error if this is part of a + * multi-relation request -- for example, CLUSTER was run on the entire + * database. + */ + if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW && + !RelationIsPopulated(OldHeap)) + { + if (index) + index_close(index, lmode); + relation_close(OldHeap, lmode); + goto out; + } + + Assert(OldHeap->rd_rel->relkind == RELKIND_RELATION || + OldHeap->rd_rel->relkind == RELKIND_MATVIEW || + OldHeap->rd_rel->relkind == RELKIND_TOASTVALUE); + + /* + * All predicate locks on the tuples or pages are about to be made + * invalid, because we move tuples around. Promote them to relation + * locks. Predicate locks on indexes will be promoted when they are + * reindexed. + * + * During concurrent processing, the heap as well as its indexes stay in + * operation, so we postpone this step until they are locked using + * AccessExclusiveLock near the end of the processing. + */ + if (!concurrent) + TransferPredicateLocksToHeapRelation(OldHeap); + + /* rebuild_relation does all the dirty work */ + PG_TRY(); + { + rebuild_relation(OldHeap, index, verbose, ident_idx); + } + PG_FINALLY(); + { + if (concurrent) + { + /* + * Since during normal operation the worker was already asked to + * exit, stopping it explicitly is especially important on ERROR. + * However it still seems a good practice to make sure that the + * worker never survives the REPACK command. + */ + stop_repack_decoding_worker(); + } + } + PG_END_TRY(); + + /* rebuild_relation closes OldHeap, and index if valid */ + +out: + /* Roll back any GUC changes executed by index functions */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore userid and security context */ + SetUserIdAndSecContext(save_userid, save_sec_context); + + pgstat_progress_end_command(); +} + +/* + * Check if the table (and its index) still meets the requirements of + * cluster_rel(). + */ +static bool +cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid, + Oid userid, LOCKMODE lmode, int options) +{ + Oid tableOid = RelationGetRelid(OldHeap); + + /* Check that the user still has privileges for the relation */ + if (!repack_is_permitted_for_relation(cmd, tableOid, userid)) + { + relation_close(OldHeap, lmode); + return false; + } + + /* + * Silently skip a temp table for a remote session. Only doing this check + * in the "recheck" case is appropriate (which currently means somebody is + * executing a database-wide CLUSTER or on a partitioned table), because + * there is another check in cluster() which will stop any attempt to + * cluster remote temp tables by name. There is another check in + * cluster_rel which is redundant, but we leave it for extra safety. + */ + if (RELATION_IS_OTHER_TEMP(OldHeap)) + { + relation_close(OldHeap, lmode); + return false; + } + + if (OidIsValid(indexOid)) + { + /* + * Check that the index still exists + */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid))) + { + relation_close(OldHeap, lmode); + return false; + } + + /* + * Check that the index is still the one with indisclustered set, if + * needed. + */ + if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 && + !get_index_isclustered(indexOid)) + { + relation_close(OldHeap, lmode); + return false; + } + } + + return true; +} + +/* + * Verify that the specified heap and index are valid to cluster on + * + * Side effect: obtains lock on the index. The caller may + * in some cases already have a lock of the same strength on the table, but + * not in all cases so we can't rely on the table-level lock for + * protection here. + */ +void +check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode) +{ + Relation OldIndex; + + OldIndex = index_open(indexOid, lockmode); + + /* + * Check that index is in fact an index on the given relation + */ + if (OldIndex->rd_index == NULL || + OldIndex->rd_index->indrelid != RelationGetRelid(OldHeap)) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an index for table \"%s\"", + RelationGetRelationName(OldIndex), + RelationGetRelationName(OldHeap)))); + + /* Index AM must allow clustering */ + if (!OldIndex->rd_indam->amclusterable) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot cluster on index \"%s\" because access method does not support clustering", + RelationGetRelationName(OldIndex)))); + + /* + * Disallow clustering on incomplete indexes (those that might not index + * every row of the relation). We could relax this by making a separate + * seqscan pass over the table to copy the missing rows, but that seems + * expensive and tedious. + */ + if (!heap_attisnull(OldIndex->rd_indextuple, Anum_pg_index_indpred, NULL)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot cluster on partial index \"%s\"", + RelationGetRelationName(OldIndex)))); + + /* + * Disallow if index is left over from a failed CREATE INDEX CONCURRENTLY; + * it might well not contain entries for every heap row, or might not even + * be internally consistent. (But note that we don't check indcheckxmin; + * the worst consequence of following broken HOT chains would be that we + * might put recently-dead tuples out-of-order in the new table, and there + * is little harm in that.) + */ + if (!OldIndex->rd_index->indisvalid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot cluster on invalid index \"%s\"", + RelationGetRelationName(OldIndex)))); + + /* Drop relcache refcnt on OldIndex, but keep lock */ + index_close(OldIndex, NoLock); +} + +/* + * mark_index_clustered: mark the specified index as the one clustered on + * + * With indexOid == InvalidOid, will mark all indexes of rel not-clustered. + */ +void +mark_index_clustered(Relation rel, Oid indexOid, bool is_internal) +{ + HeapTuple indexTuple; + Form_pg_index indexForm; + Relation pg_index; + ListCell *index; + + Assert(rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE); + + /* + * If the index is already marked clustered, no need to do anything. + */ + if (OidIsValid(indexOid)) + { + if (get_index_isclustered(indexOid)) + return; + } + + /* + * Check each index of the relation and set/clear the bit as needed. + */ + pg_index = table_open(IndexRelationId, RowExclusiveLock); + + foreach(index, RelationGetIndexList(rel)) + { + Oid thisIndexOid = lfirst_oid(index); + + indexTuple = SearchSysCacheCopy1(INDEXRELID, + ObjectIdGetDatum(thisIndexOid)); + if (!HeapTupleIsValid(indexTuple)) + elog(ERROR, "cache lookup failed for index %u", thisIndexOid); + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + + /* + * Unset the bit if set. We know it's wrong because we checked this + * earlier. + */ + if (indexForm->indisclustered) + { + indexForm->indisclustered = false; + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); + } + else if (thisIndexOid == indexOid) + { + /* this was checked earlier, but let's be real sure */ + if (!indexForm->indisvalid) + elog(ERROR, "cannot cluster on invalid index %u", indexOid); + indexForm->indisclustered = true; + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); + } + + InvokeObjectPostAlterHookArg(IndexRelationId, thisIndexOid, 0, + InvalidOid, is_internal); + + heap_freetuple(indexTuple); + } + + table_close(pg_index, RowExclusiveLock); +} + +/* + * Check if the CONCURRENTLY option is legal for the relation. + * + * *Ident_idx_p receives OID of the identity index. + */ +static void +check_concurrent_repack_requirements(Relation rel, Oid *ident_idx_p) +{ + char relpersistence, + replident; + Oid ident_idx; + + /* Data changes in system relations are not logically decoded. */ + if (IsCatalogRelation(rel)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("REPACK CONCURRENTLY is not supported for catalog relations.")); + + /* + * reorderbuffer.c does not seem to handle processing of TOAST relation + * alone. + */ + if (IsToastRelation(rel)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("REPACK CONCURRENTLY is not supported for TOAST relations")); + + relpersistence = rel->rd_rel->relpersistence; + if (relpersistence != RELPERSISTENCE_PERMANENT) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("REPACK CONCURRENTLY is only allowed for permanent relations.")); + + /* With NOTHING, WAL does not contain the old tuple. */ + replident = rel->rd_rel->relreplident; + if (replident == REPLICA_IDENTITY_NOTHING) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot repack relation \"%s\"", + RelationGetRelationName(rel)), + errhint("Relation \"%s\" has insufficient replication identity.", + RelationGetRelationName(rel))); + + /* + * Obtain the replica identity index -- either one that has been set + * explicitly, or the primary key. If none of these cases apply, the + * table cannot be repacked concurrently. It might be possible to have + * repack work with a FULL replica identity; however that requires more + * work and is not implemented yet. + */ + ident_idx = RelationGetReplicaIndex(rel); + if (!OidIsValid(ident_idx) && OidIsValid(rel->rd_pkindex)) + ident_idx = rel->rd_pkindex; + if (!OidIsValid(ident_idx)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot process relation \"%s\"", + RelationGetRelationName(rel)), + errhint("Relation \"%s\" has no identity index.", + RelationGetRelationName(rel))); + + *ident_idx_p = ident_idx; +} + + +/* + * rebuild_relation: rebuild an existing relation in index or physical order + * + * OldHeap: table to rebuild. See cluster_rel() for comments on the required + * lock strength. + * + * index: index to cluster by, or NULL to rewrite in physical order. + * + * ident_idx: identity index, to handle replaying of concurrent data changes + * to the new heap. InvalidOid if there's no CONCURRENTLY option. + * + * On entry, heap and index (if one is given) must be open, and the + * appropriate lock held on them -- AccessExclusiveLock for exclusive + * processing and ShareUpdateExclusiveLock for concurrent processing. + * + * On exit, they are closed, but still locked with AccessExclusiveLock. + * (The function handles the lock upgrade if 'concurrent' is true.) + */ +static void +rebuild_relation(Relation OldHeap, Relation index, bool verbose, + Oid ident_idx) +{ + Oid tableOid = RelationGetRelid(OldHeap); + Oid accessMethod = OldHeap->rd_rel->relam; + Oid tableSpace = OldHeap->rd_rel->reltablespace; + Oid OIDNewHeap; + Relation NewHeap; + char relpersistence; + bool swap_toast_by_content; + TransactionId frozenXid; + MultiXactId cutoffMulti; + bool concurrent = OidIsValid(ident_idx); + Snapshot snapshot = NULL; +#if USE_ASSERT_CHECKING + LOCKMODE lmode; + + lmode = RepackLockLevel(concurrent); + + Assert(CheckRelationLockedByMe(OldHeap, lmode, false)); + Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false)); +#endif + + if (concurrent) + { + /* + * The worker needs to be member of the locking group we're the leader + * of. We ought to become the leader before the worker starts. The + * worker will join the group as soon as it starts. + * + * This is to make sure that the deadlock described below is + * detectable by deadlock.c: if the worker waits for a transaction to + * complete and we are waiting for the worker output, then effectively + * we (i.e. this backend) are waiting for that transaction. + */ + BecomeLockGroupLeader(); + + /* + * Start the worker that decodes data changes applied while we're + * copying the table contents. + * + * Note that the worker has to wait for all transactions with XID + * already assigned to finish. If some of those transactions is + * waiting for a lock conflicting with ShareUpdateExclusiveLock on our + * table (e.g. it runs CREATE INDEX), we can end up in a deadlock. + * Not sure this risk is worth unlocking/locking the table (and its + * clustering index) and checking again if it's still eligible for + * REPACK CONCURRENTLY. + */ + start_repack_decoding_worker(tableOid); + + /* + * Wait until the worker has the initial snapshot and retrieve it. + */ + snapshot = get_initial_snapshot(decoding_worker); + + PushActiveSnapshot(snapshot); + } + + /* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */ + if (index != NULL) + mark_index_clustered(OldHeap, RelationGetRelid(index), true); + + /* Remember info about rel before closing OldHeap */ + relpersistence = OldHeap->rd_rel->relpersistence; + + /* + * Create the transient table that will receive the re-ordered data. + * + * OldHeap is already locked, so no need to lock it again. make_new_heap + * obtains AccessExclusiveLock on the new heap and its toast table. + */ + OIDNewHeap = make_new_heap(tableOid, tableSpace, + accessMethod, + relpersistence, + NoLock); + Assert(CheckRelationOidLockedByMe(OIDNewHeap, AccessExclusiveLock, false)); + NewHeap = table_open(OIDNewHeap, NoLock); + + /* Copy the heap data into the new table in the desired order */ + copy_table_data(NewHeap, OldHeap, index, snapshot, verbose, + &swap_toast_by_content, &frozenXid, &cutoffMulti); + + /* The historic snapshot won't be needed anymore. */ + if (snapshot) + { + PopActiveSnapshot(); + UpdateActiveSnapshotCommandId(); + } + + if (concurrent) + { + Assert(!swap_toast_by_content); + + /* + * Close the index, but keep the lock. Both heaps will be closed by + * the following call. + */ + if (index) + index_close(index, NoLock); + + rebuild_relation_finish_concurrent(NewHeap, OldHeap, ident_idx, + frozenXid, cutoffMulti); + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); + } + else + { + bool is_system_catalog = IsSystemRelation(OldHeap); + + /* Close relcache entries, but keep lock until transaction commit */ + table_close(OldHeap, NoLock); + if (index) + index_close(index, NoLock); + + /* + * Close the new relation so it can be dropped as soon as the storage + * is swapped. The relation is not visible to others, so no need to + * unlock it explicitly. + */ + table_close(NewHeap, NoLock); + + /* + * Swap the physical files of the target and transient tables, then + * rebuild the target's indexes and throw away the transient table. + */ + finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog, + swap_toast_by_content, false, true, + true, /* reindex */ + frozenXid, cutoffMulti, + relpersistence); + } +} + + +/* + * Create the transient table that will be filled with new data during + * CLUSTER, ALTER TABLE, and similar operations. The transient table + * duplicates the logical structure of the OldHeap; but will have the + * specified physical storage properties NewTableSpace, NewAccessMethod, and + * relpersistence. + * + * After this, the caller should load the new heap with transferred/modified + * data, then call finish_heap_swap to complete the operation. + */ +Oid +make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod, + char relpersistence, LOCKMODE lockmode) +{ + TupleDesc OldHeapDesc; + char NewHeapName[NAMEDATALEN]; + Oid OIDNewHeap; + Oid toastid; + Relation OldHeap; + HeapTuple tuple; + Datum reloptions; + bool isNull; + Oid namespaceid; + + OldHeap = table_open(OIDOldHeap, lockmode); + OldHeapDesc = RelationGetDescr(OldHeap); + + /* + * Note that the NewHeap will not receive any of the defaults or + * constraints associated with the OldHeap; we don't need 'em, and there's + * no reason to spend cycles inserting them into the catalogs only to + * delete them. + */ + + /* + * But we do want to use reloptions of the old heap for new heap. + */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(OIDOldHeap)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", OIDOldHeap); + reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, + &isNull); + if (isNull) + reloptions = (Datum) 0; + + if (relpersistence == RELPERSISTENCE_TEMP) + namespaceid = LookupCreationNamespace("pg_temp"); + else + namespaceid = RelationGetNamespace(OldHeap); + + /* + * Create the new heap, using a temporary name in the same namespace as + * the existing table. NOTE: there is some risk of collision with user + * relnames. Working around this seems more trouble than it's worth; in + * particular, we can't create the new heap in a different namespace from + * the old, or we will have problems with the TEMP status of temp tables. + * + * Note: the new heap is not a shared relation, even if we are rebuilding + * a shared rel. However, we do make the new heap mapped if the source is + * mapped. This simplifies swap_relation_files, and is absolutely + * necessary for rebuilding pg_class, for reasons explained there. + */ + snprintf(NewHeapName, sizeof(NewHeapName), "pg_temp_%u", OIDOldHeap); + + OIDNewHeap = heap_create_with_catalog(NewHeapName, + namespaceid, + NewTableSpace, + InvalidOid, + InvalidOid, + InvalidOid, + OldHeap->rd_rel->relowner, + NewAccessMethod, + OldHeapDesc, + NIL, + RELKIND_RELATION, + relpersistence, + false, + RelationIsMapped(OldHeap), + ONCOMMIT_NOOP, + reloptions, + false, + true, + true, + OIDOldHeap, + NULL); + Assert(OIDNewHeap != InvalidOid); + + ReleaseSysCache(tuple); + + /* + * Advance command counter so that the newly-created relation's catalog + * tuples will be visible to table_open. + */ + CommandCounterIncrement(); + + /* + * If necessary, create a TOAST table for the new relation. + * + * If the relation doesn't have a TOAST table already, we can't need one + * for the new relation. The other way around is possible though: if some + * wide columns have been dropped, NewHeapCreateToastTable can decide that + * no TOAST table is needed for the new table. + * + * Note that NewHeapCreateToastTable ends with CommandCounterIncrement, so + * that the TOAST table will be visible for insertion. + */ + toastid = OldHeap->rd_rel->reltoastrelid; + if (OidIsValid(toastid)) + { + /* keep the existing toast table's reloptions, if any */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", toastid); + reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, + &isNull); + if (isNull) + reloptions = (Datum) 0; + + NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid); + + ReleaseSysCache(tuple); + } + + table_close(OldHeap, NoLock); + + return OIDNewHeap; +} + +/* + * Do the physical copying of table data. + * + * 'snapshot' and 'decoding_ctx': see table_relation_copy_for_cluster(). Pass + * iff concurrent processing is required. + * + * There are three output parameters: + * *pSwapToastByContent is set true if toast tables must be swapped by content. + * *pFreezeXid receives the TransactionId used as freeze cutoff point. + * *pCutoffMulti receives the MultiXactId used as a cutoff point. + */ +static void +copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, + Snapshot snapshot, bool verbose, bool *pSwapToastByContent, + TransactionId *pFreezeXid, MultiXactId *pCutoffMulti) +{ + Relation relRelation; + HeapTuple reltup; + Form_pg_class relform; + TupleDesc oldTupDesc PG_USED_FOR_ASSERTS_ONLY; + TupleDesc newTupDesc PG_USED_FOR_ASSERTS_ONLY; + VacuumParams params; + struct VacuumCutoffs cutoffs; + bool use_sort; + double num_tuples = 0, + tups_vacuumed = 0, + tups_recently_dead = 0; + BlockNumber num_pages; + int elevel = verbose ? INFO : DEBUG2; + PGRUsage ru0; + char *nspname; + bool concurrent = snapshot != NULL; + LOCKMODE lmode; + + lmode = RepackLockLevel(concurrent); + + pg_rusage_init(&ru0); + + /* Store a copy of the namespace name for logging purposes */ + nspname = get_namespace_name(RelationGetNamespace(OldHeap)); + + /* + * Their tuple descriptors should be exactly alike, but here we only need + * assume that they have the same number of columns. + */ + oldTupDesc = RelationGetDescr(OldHeap); + newTupDesc = RelationGetDescr(NewHeap); + Assert(newTupDesc->natts == oldTupDesc->natts); + + /* + * If the OldHeap has a toast table, get lock on the toast table to keep + * it from being vacuumed. This is needed because autovacuum processes + * toast tables independently of their main tables, with no lock on the + * latter. If an autovacuum were to start on the toast table after we + * compute our OldestXmin below, it would use a later OldestXmin, and then + * possibly remove as DEAD toast tuples belonging to main tuples we think + * are only RECENTLY_DEAD. Then we'd fail while trying to copy those + * tuples. + * + * We don't need to open the toast relation here, just lock it. The lock + * will be held till end of transaction. + */ + if (OldHeap->rd_rel->reltoastrelid) + LockRelationOid(OldHeap->rd_rel->reltoastrelid, lmode); + + /* + * If both tables have TOAST tables, perform toast swap by content. It is + * possible that the old table has a toast table but the new one doesn't, + * if toastable columns have been dropped. In that case we have to do + * swap by links. This is okay because swap by content is only essential + * for system catalogs, and we don't support schema changes for them. + */ + if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid && + !concurrent) + { + *pSwapToastByContent = true; + + /* + * When doing swap by content, any toast pointers written into NewHeap + * must use the old toast table's OID, because that's where the toast + * data will eventually be found. Set this up by setting rd_toastoid. + * This also tells toast_save_datum() to preserve the toast value + * OIDs, which we want so as not to invalidate toast pointers in + * system catalog caches, and to avoid making multiple copies of a + * single toast value. + * + * Note that we must hold NewHeap open until we are done writing data, + * since the relcache will not guarantee to remember this setting once + * the relation is closed. Also, this technique depends on the fact + * that no one will try to read from the NewHeap until after we've + * finished writing it and swapping the rels --- otherwise they could + * follow the toast pointers to the wrong place. (It would actually + * work for values copied over from the old toast table, but not for + * any values that we toast which were previously not toasted.) + * + * This would not work with CONCURRENTLY because we may need to delete + * TOASTed tuples from the new heap. With this hack, we'd delete them + * from the old heap. + */ + NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid; + } + else + *pSwapToastByContent = false; + + /* + * Compute xids used to freeze and weed out dead tuples and multixacts. + * Since we're going to rewrite the whole table anyway, there's no reason + * not to be aggressive about this. + */ + memset(¶ms, 0, sizeof(VacuumParams)); + vacuum_get_cutoffs(OldHeap, ¶ms, &cutoffs); + + /* + * FreezeXid will become the table's new relfrozenxid, and that mustn't go + * backwards, so take the max. + */ + { + TransactionId relfrozenxid = OldHeap->rd_rel->relfrozenxid; + + if (TransactionIdIsValid(relfrozenxid) && + TransactionIdPrecedes(cutoffs.FreezeLimit, relfrozenxid)) + cutoffs.FreezeLimit = relfrozenxid; + } + + /* + * MultiXactCutoff, similarly, shouldn't go backwards either. + */ + { + MultiXactId relminmxid = OldHeap->rd_rel->relminmxid; + + if (MultiXactIdIsValid(relminmxid) && + MultiXactIdPrecedes(cutoffs.MultiXactCutoff, relminmxid)) + cutoffs.MultiXactCutoff = relminmxid; + } + + /* + * Decide whether to use an indexscan or seqscan-and-optional-sort to scan + * the OldHeap. We know how to use a sort to duplicate the ordering of a + * btree index, and will use seqscan-and-sort for that case if the planner + * tells us it's cheaper. Otherwise, always indexscan if an index is + * provided, else plain seqscan. + */ + if (OldIndex != NULL && OldIndex->rd_rel->relam == BTREE_AM_OID) + use_sort = plan_cluster_use_sort(RelationGetRelid(OldHeap), + RelationGetRelid(OldIndex)); + else + use_sort = false; + + /* Log what we're doing */ + if (OldIndex != NULL && !use_sort) + ereport(elevel, + errmsg("repacking \"%s.%s\" using index scan on \"%s\"", + nspname, + RelationGetRelationName(OldHeap), + RelationGetRelationName(OldIndex))); + else if (use_sort) + ereport(elevel, + errmsg("repacking \"%s.%s\" using sequential scan and sort", + nspname, + RelationGetRelationName(OldHeap))); + else + ereport(elevel, + errmsg("repacking \"%s.%s\" in physical order", + nspname, + RelationGetRelationName(OldHeap))); + + /* + * Hand off the actual copying to AM specific function, the generic code + * cannot know how to deal with visibility across AMs. Note that this + * routine is allowed to set FreezeXid / MultiXactCutoff to different + * values (e.g. because the AM doesn't use freezing). + */ + table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort, + cutoffs.OldestXmin, snapshot, + &cutoffs.FreezeLimit, + &cutoffs.MultiXactCutoff, + &num_tuples, &tups_vacuumed, + &tups_recently_dead); + + /* return selected values to caller, get set as relfrozenxid/minmxid */ + *pFreezeXid = cutoffs.FreezeLimit; + *pCutoffMulti = cutoffs.MultiXactCutoff; + + /* + * Reset rd_toastoid just to be tidy --- it shouldn't be looked at again. + * In the CONCURRENTLY case, we need to set it again before applying the + * concurrent changes. + */ + NewHeap->rd_toastoid = InvalidOid; + + num_pages = RelationGetNumberOfBlocks(NewHeap); + + /* Log what we did */ + ereport(elevel, + (errmsg("\"%s.%s\": found %.0f removable, %.0f nonremovable row versions in %u pages", + nspname, + RelationGetRelationName(OldHeap), + tups_vacuumed, num_tuples, + RelationGetNumberOfBlocks(OldHeap)), + errdetail("%.0f dead row versions cannot be removed yet.\n" + "%s.", + tups_recently_dead, + pg_rusage_show(&ru0)))); + + /* Update pg_class to reflect the correct values of pages and tuples. */ + relRelation = table_open(RelationRelationId, RowExclusiveLock); + + reltup = SearchSysCacheCopy1(RELOID, + ObjectIdGetDatum(RelationGetRelid(NewHeap))); + if (!HeapTupleIsValid(reltup)) + elog(ERROR, "cache lookup failed for relation %u", + RelationGetRelid(NewHeap)); + relform = (Form_pg_class) GETSTRUCT(reltup); + + relform->relpages = num_pages; + relform->reltuples = num_tuples; + + /* Don't update the stats for pg_class. See swap_relation_files. */ + if (RelationGetRelid(OldHeap) != RelationRelationId) + CatalogTupleUpdate(relRelation, &reltup->t_self, reltup); + else + CacheInvalidateRelcacheByTuple(reltup); + + /* Clean up. */ + heap_freetuple(reltup); + table_close(relRelation, RowExclusiveLock); + + /* Make the update visible */ + CommandCounterIncrement(); +} + +/* + * Swap the physical files of two given relations. + * + * We swap the physical identity (reltablespace, relfilenumber) while keeping + * the same logical identities of the two relations. relpersistence is also + * swapped, which is critical since it determines where buffers live for each + * relation. + * + * We can swap associated TOAST data in either of two ways: recursively swap + * the physical content of the toast tables (and their indexes), or swap the + * TOAST links in the given relations' pg_class entries. The former is needed + * to manage rewrites of shared catalogs (where we cannot change the pg_class + * links) while the latter is the only way to handle cases in which a toast + * table is added or removed altogether. + * + * Additionally, the first relation is marked with relfrozenxid set to + * frozenXid. It seems a bit ugly to have this here, but the caller would + * have to do it anyway, so having it here saves a heap_update. Note: in + * the swap-toast-links case, we assume we don't need to change the toast + * table's relfrozenxid: the new version of the toast table should already + * have relfrozenxid set to RecentXmin, which is good enough. + * + * Lastly, if r2 and its toast table and toast index (if any) are mapped, + * their OIDs are emitted into mapped_tables[]. This is hacky but beats + * having to look the information up again later in finish_heap_swap. + */ +static void +swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class, + bool swap_toast_by_content, + bool is_internal, + TransactionId frozenXid, + MultiXactId cutoffMulti, + Oid *mapped_tables) +{ + Relation relRelation; + HeapTuple reltup1, + reltup2; + Form_pg_class relform1, + relform2; + RelFileNumber relfilenumber1, + relfilenumber2; + RelFileNumber swaptemp; + char swptmpchr; + Oid relam1, + relam2; + + /* We need writable copies of both pg_class tuples. */ + relRelation = table_open(RelationRelationId, RowExclusiveLock); + + reltup1 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r1)); + if (!HeapTupleIsValid(reltup1)) + elog(ERROR, "cache lookup failed for relation %u", r1); + relform1 = (Form_pg_class) GETSTRUCT(reltup1); + + reltup2 = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(r2)); + if (!HeapTupleIsValid(reltup2)) + elog(ERROR, "cache lookup failed for relation %u", r2); + relform2 = (Form_pg_class) GETSTRUCT(reltup2); + + relfilenumber1 = relform1->relfilenode; + relfilenumber2 = relform2->relfilenode; + relam1 = relform1->relam; + relam2 = relform2->relam; + + if (RelFileNumberIsValid(relfilenumber1) && + RelFileNumberIsValid(relfilenumber2)) + { + /* + * Normal non-mapped relations: swap relfilenumbers, reltablespaces, + * relpersistence + */ + Assert(!target_is_pg_class); + + swaptemp = relform1->relfilenode; + relform1->relfilenode = relform2->relfilenode; + relform2->relfilenode = swaptemp; + + swaptemp = relform1->reltablespace; + relform1->reltablespace = relform2->reltablespace; + relform2->reltablespace = swaptemp; + + swaptemp = relform1->relam; + relform1->relam = relform2->relam; + relform2->relam = swaptemp; + + swptmpchr = relform1->relpersistence; + relform1->relpersistence = relform2->relpersistence; + relform2->relpersistence = swptmpchr; + + /* Also swap toast links, if we're swapping by links */ + if (!swap_toast_by_content) + { + swaptemp = relform1->reltoastrelid; + relform1->reltoastrelid = relform2->reltoastrelid; + relform2->reltoastrelid = swaptemp; + } + } + else + { + /* + * Mapped-relation case. Here we have to swap the relation mappings + * instead of modifying the pg_class columns. Both must be mapped. + */ + if (RelFileNumberIsValid(relfilenumber1) || + RelFileNumberIsValid(relfilenumber2)) + elog(ERROR, "cannot swap mapped relation \"%s\" with non-mapped relation", + NameStr(relform1->relname)); + + /* + * We can't change the tablespace nor persistence of a mapped rel, and + * we can't handle toast link swapping for one either, because we must + * not apply any critical changes to its pg_class row. These cases + * should be prevented by upstream permissions tests, so these checks + * are non-user-facing emergency backstop. + */ + if (relform1->reltablespace != relform2->reltablespace) + elog(ERROR, "cannot change tablespace of mapped relation \"%s\"", + NameStr(relform1->relname)); + if (relform1->relpersistence != relform2->relpersistence) + elog(ERROR, "cannot change persistence of mapped relation \"%s\"", + NameStr(relform1->relname)); + if (relform1->relam != relform2->relam) + elog(ERROR, "cannot change access method of mapped relation \"%s\"", + NameStr(relform1->relname)); + if (!swap_toast_by_content && + (relform1->reltoastrelid || relform2->reltoastrelid)) + elog(ERROR, "cannot swap toast by links for mapped relation \"%s\"", + NameStr(relform1->relname)); + + /* + * Fetch the mappings --- shouldn't fail, but be paranoid + */ + relfilenumber1 = RelationMapOidToFilenumber(r1, relform1->relisshared); + if (!RelFileNumberIsValid(relfilenumber1)) + elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u", + NameStr(relform1->relname), r1); + relfilenumber2 = RelationMapOidToFilenumber(r2, relform2->relisshared); + if (!RelFileNumberIsValid(relfilenumber2)) + elog(ERROR, "could not find relation mapping for relation \"%s\", OID %u", + NameStr(relform2->relname), r2); + + /* + * Send replacement mappings to relmapper. Note these won't actually + * take effect until CommandCounterIncrement. + */ + RelationMapUpdateMap(r1, relfilenumber2, relform1->relisshared, false); + RelationMapUpdateMap(r2, relfilenumber1, relform2->relisshared, false); + + /* Pass OIDs of mapped r2 tables back to caller */ + *mapped_tables++ = r2; + } + + /* + * Recognize that rel1's relfilenumber (swapped from rel2) is new in this + * subtransaction. The rel2 storage (swapped from rel1) may or may not be + * new. + */ + { + Relation rel1, + rel2; + + rel1 = relation_open(r1, NoLock); + rel2 = relation_open(r2, NoLock); + rel2->rd_createSubid = rel1->rd_createSubid; + rel2->rd_newRelfilelocatorSubid = rel1->rd_newRelfilelocatorSubid; + rel2->rd_firstRelfilelocatorSubid = rel1->rd_firstRelfilelocatorSubid; + RelationAssumeNewRelfilelocator(rel1); + relation_close(rel1, NoLock); + relation_close(rel2, NoLock); + } + + /* + * In the case of a shared catalog, these next few steps will only affect + * our own database's pg_class row; but that's okay, because they are all + * noncritical updates. That's also an important fact for the case of a + * mapped catalog, because it's possible that we'll commit the map change + * and then fail to commit the pg_class update. + */ + + /* set rel1's frozen Xid and minimum MultiXid */ + if (relform1->relkind != RELKIND_INDEX) + { + Assert(!TransactionIdIsValid(frozenXid) || + TransactionIdIsNormal(frozenXid)); + relform1->relfrozenxid = frozenXid; + relform1->relminmxid = cutoffMulti; + } + + /* swap size statistics too, since new rel has freshly-updated stats */ + { + int32 swap_pages; + float4 swap_tuples; + int32 swap_allvisible; + int32 swap_allfrozen; + + swap_pages = relform1->relpages; + relform1->relpages = relform2->relpages; + relform2->relpages = swap_pages; + + swap_tuples = relform1->reltuples; + relform1->reltuples = relform2->reltuples; + relform2->reltuples = swap_tuples; + + swap_allvisible = relform1->relallvisible; + relform1->relallvisible = relform2->relallvisible; + relform2->relallvisible = swap_allvisible; + + swap_allfrozen = relform1->relallfrozen; + relform1->relallfrozen = relform2->relallfrozen; + relform2->relallfrozen = swap_allfrozen; + } + + /* + * Update the tuples in pg_class --- unless the target relation of the + * swap is pg_class itself. In that case, there is zero point in making + * changes because we'd be updating the old data that we're about to throw + * away. Because the real work being done here for a mapped relation is + * just to change the relation map settings, it's all right to not update + * the pg_class rows in this case. The most important changes will instead + * performed later, in finish_heap_swap() itself. + */ + if (!target_is_pg_class) + { + CatalogIndexState indstate; + + indstate = CatalogOpenIndexes(relRelation); + CatalogTupleUpdateWithInfo(relRelation, &reltup1->t_self, reltup1, + indstate); + CatalogTupleUpdateWithInfo(relRelation, &reltup2->t_self, reltup2, + indstate); + CatalogCloseIndexes(indstate); + } + else + { + /* no update ... but we do still need relcache inval */ + CacheInvalidateRelcacheByTuple(reltup1); + CacheInvalidateRelcacheByTuple(reltup2); + } + + /* + * Now that pg_class has been updated with its relevant information for + * the swap, update the dependency of the relations to point to their new + * table AM, if it has changed. + */ + if (relam1 != relam2) + { + if (changeDependencyFor(RelationRelationId, + r1, + AccessMethodRelationId, + relam1, + relam2) != 1) + elog(ERROR, "could not change access method dependency for relation \"%s.%s\"", + get_namespace_name(get_rel_namespace(r1)), + get_rel_name(r1)); + if (changeDependencyFor(RelationRelationId, + r2, + AccessMethodRelationId, + relam2, + relam1) != 1) + elog(ERROR, "could not change access method dependency for relation \"%s.%s\"", + get_namespace_name(get_rel_namespace(r2)), + get_rel_name(r2)); + } + + /* + * Post alter hook for modified relations. The change to r2 is always + * internal, but r1 depends on the invocation context. + */ + InvokeObjectPostAlterHookArg(RelationRelationId, r1, 0, + InvalidOid, is_internal); + InvokeObjectPostAlterHookArg(RelationRelationId, r2, 0, + InvalidOid, true); + + /* + * If we have toast tables associated with the relations being swapped, + * deal with them too. + */ + if (relform1->reltoastrelid || relform2->reltoastrelid) + { + if (swap_toast_by_content) + { + if (relform1->reltoastrelid && relform2->reltoastrelid) + { + /* Recursively swap the contents of the toast tables */ + swap_relation_files(relform1->reltoastrelid, + relform2->reltoastrelid, + target_is_pg_class, + swap_toast_by_content, + is_internal, + frozenXid, + cutoffMulti, + mapped_tables); + } + else + { + /* caller messed up */ + elog(ERROR, "cannot swap toast files by content when there's only one"); + } + } + else + { + /* + * We swapped the ownership links, so we need to change dependency + * data to match. + * + * NOTE: it is possible that only one table has a toast table. + * + * NOTE: at present, a TOAST table's only dependency is the one on + * its owning table. If more are ever created, we'd need to use + * something more selective than deleteDependencyRecordsFor() to + * get rid of just the link we want. + */ + ObjectAddress baseobject, + toastobject; + long count; + + /* + * We disallow this case for system catalogs, to avoid the + * possibility that the catalog we're rebuilding is one of the + * ones the dependency changes would change. It's too late to be + * making any data changes to the target catalog. + */ + if (IsSystemClass(r1, relform1)) + elog(ERROR, "cannot swap toast files by links for system catalogs"); + + /* Delete old dependencies */ + if (relform1->reltoastrelid) + { + count = deleteDependencyRecordsFor(RelationRelationId, + relform1->reltoastrelid, + false); + if (count != 1) + elog(ERROR, "expected one dependency record for TOAST table, found %ld", + count); + } + if (relform2->reltoastrelid) + { + count = deleteDependencyRecordsFor(RelationRelationId, + relform2->reltoastrelid, + false); + if (count != 1) + elog(ERROR, "expected one dependency record for TOAST table, found %ld", + count); + } + + /* Register new dependencies */ + baseobject.classId = RelationRelationId; + baseobject.objectSubId = 0; + toastobject.classId = RelationRelationId; + toastobject.objectSubId = 0; + + if (relform1->reltoastrelid) + { + baseobject.objectId = r1; + toastobject.objectId = relform1->reltoastrelid; + recordDependencyOn(&toastobject, &baseobject, + DEPENDENCY_INTERNAL); + } + + if (relform2->reltoastrelid) + { + baseobject.objectId = r2; + toastobject.objectId = relform2->reltoastrelid; + recordDependencyOn(&toastobject, &baseobject, + DEPENDENCY_INTERNAL); + } + } + } + + /* + * If we're swapping two toast tables by content, do the same for their + * valid index. The swap can actually be safely done only if the relations + * have indexes. + */ + if (swap_toast_by_content && + relform1->relkind == RELKIND_TOASTVALUE && + relform2->relkind == RELKIND_TOASTVALUE) + { + Oid toastIndex1, + toastIndex2; + + /* Get valid index for each relation */ + toastIndex1 = toast_get_valid_index(r1, + AccessExclusiveLock); + toastIndex2 = toast_get_valid_index(r2, + AccessExclusiveLock); + + swap_relation_files(toastIndex1, + toastIndex2, + target_is_pg_class, + swap_toast_by_content, + is_internal, + InvalidTransactionId, + InvalidMultiXactId, + mapped_tables); + } + + /* Clean up. */ + heap_freetuple(reltup1); + heap_freetuple(reltup2); + + table_close(relRelation, RowExclusiveLock); +} + +/* + * Remove the transient table that was built by make_new_heap, and finish + * cleaning up (including rebuilding all indexes on the old heap). + */ +void +finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, + bool is_system_catalog, + bool swap_toast_by_content, + bool check_constraints, + bool is_internal, + bool reindex, + TransactionId frozenXid, + MultiXactId cutoffMulti, + char newrelpersistence) +{ + ObjectAddress object; + Oid mapped_tables[4]; + int i; + + /* Report that we are now swapping relation files */ + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SWAP_REL_FILES); + + /* Zero out possible results from swapped_relation_files */ + memset(mapped_tables, 0, sizeof(mapped_tables)); + + /* + * Swap the contents of the heap relations (including any toast tables). + * Also set old heap's relfrozenxid to frozenXid. + */ + swap_relation_files(OIDOldHeap, OIDNewHeap, + (OIDOldHeap == RelationRelationId), + swap_toast_by_content, is_internal, + frozenXid, cutoffMulti, mapped_tables); + + /* + * If it's a system catalog, queue a sinval message to flush all catcaches + * on the catalog when we reach CommandCounterIncrement. + */ + if (is_system_catalog) + CacheInvalidateCatalog(OIDOldHeap); + + if (reindex) + { + int reindex_flags; + ReindexParams reindex_params = {0}; + + /* + * Rebuild each index on the relation (but not the toast table, which + * is all-new at this point). It is important to do this before the + * DROP step because if we are processing a system catalog that will + * be used during DROP, we want to have its indexes available. There + * is no advantage to the other order anyway because this is all + * transactional, so no chance to reclaim disk space before commit. We + * do not need a final CommandCounterIncrement() because + * reindex_relation does it. + * + * Note: because index_build is called via reindex_relation, it will + * never set indcheckxmin true for the indexes. This is OK even + * though in some sense we are building new indexes rather than + * rebuilding existing ones, because the new heap won't contain any + * HOT chains at all, let alone broken ones, so it can't be necessary + * to set indcheckxmin. + */ + reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE; + if (check_constraints) + reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS; + + /* + * Ensure that the indexes have the same persistence as the parent + * relation. + */ + if (newrelpersistence == RELPERSISTENCE_UNLOGGED) + reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED; + else if (newrelpersistence == RELPERSISTENCE_PERMANENT) + reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT; + + /* Report that we are now reindexing relations */ + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); + + reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params); + } + + /* Report that we are now doing clean up */ + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_FINAL_CLEANUP); + + /* + * If the relation being rebuilt is pg_class, swap_relation_files() + * couldn't update pg_class's own pg_class entry (check comments in + * swap_relation_files()), thus relfrozenxid was not updated. That's + * annoying because a potential reason for doing a VACUUM FULL is a + * imminent or actual anti-wraparound shutdown. So, now that we can + * access the new relation using its indices, update relfrozenxid. + * pg_class doesn't have a toast relation, so we don't need to update the + * corresponding toast relation. Not that there's little point moving all + * relfrozenxid updates here since swap_relation_files() needs to write to + * pg_class for non-mapped relations anyway. + */ + if (OIDOldHeap == RelationRelationId) + { + Relation relRelation; + HeapTuple reltup; + Form_pg_class relform; + + relRelation = table_open(RelationRelationId, RowExclusiveLock); + + reltup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(OIDOldHeap)); + if (!HeapTupleIsValid(reltup)) + elog(ERROR, "cache lookup failed for relation %u", OIDOldHeap); + relform = (Form_pg_class) GETSTRUCT(reltup); + + relform->relfrozenxid = frozenXid; + relform->relminmxid = cutoffMulti; + + CatalogTupleUpdate(relRelation, &reltup->t_self, reltup); + + table_close(relRelation, RowExclusiveLock); + } + + /* Destroy new heap with old filenumber */ + object.classId = RelationRelationId; + object.objectId = OIDNewHeap; + object.objectSubId = 0; + + if (!reindex) + { + /* + * Make sure the changes in pg_class are visible. This is especially + * important if !swap_toast_by_content, so that the correct TOAST + * relation is dropped. (reindex_relation() above did not help in this + * case)) + */ + CommandCounterIncrement(); + } + + /* + * The new relation is local to our transaction and we know nothing + * depends on it, so DROP_RESTRICT should be OK. + */ + performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + + /* performDeletion does CommandCounterIncrement at end */ + + /* + * Now we must remove any relation mapping entries that we set up for the + * transient table, as well as its toast table and toast index if any. If + * we fail to do this before commit, the relmapper will complain about new + * permanent map entries being added post-bootstrap. + */ + for (i = 0; OidIsValid(mapped_tables[i]); i++) + RelationMapRemoveMapping(mapped_tables[i]); + + /* + * At this point, everything is kosher except that, if we did toast swap + * by links, the toast table's name corresponds to the transient table. + * The name is irrelevant to the backend because it's referenced by OID, + * but users looking at the catalogs could be confused. Rename it to + * prevent this problem. + * + * Note no lock required on the relation, because we already hold an + * exclusive lock on it. + */ + if (!swap_toast_by_content) + { + Relation newrel; + + newrel = table_open(OIDOldHeap, NoLock); + if (OidIsValid(newrel->rd_rel->reltoastrelid)) + { + Oid toastidx; + char NewToastName[NAMEDATALEN]; + + /* Get the associated valid index to be renamed */ + toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid, + AccessExclusiveLock); + + /* rename the toast table ... */ + snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u", + OIDOldHeap); + RenameRelationInternal(newrel->rd_rel->reltoastrelid, + NewToastName, true, false); + + /* ... and its valid index too. */ + snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u_index", + OIDOldHeap); + + RenameRelationInternal(toastidx, + NewToastName, true, true); + + /* + * Reset the relrewrite for the toast. The command-counter + * increment is required here as we are about to update the tuple + * that is updated as part of RenameRelationInternal. + */ + CommandCounterIncrement(); + ResetRelRewrite(newrel->rd_rel->reltoastrelid); + } + relation_close(newrel, NoLock); + } + + /* if it's not a catalog table, clear any missing attribute settings */ + if (!is_system_catalog) + { + Relation newrel; + + newrel = table_open(OIDOldHeap, NoLock); + RelationClearMissing(newrel); + relation_close(newrel, NoLock); + } +} + +/* + * Determine which relations to process, when REPACK/CLUSTER is called + * without specifying a table name. The exact process depends on whether + * USING INDEX was given or not, and in any case we only return tables and + * materialized views that the current user has privileges to repack/cluster. + * + * If USING INDEX was given, we scan pg_index to find those that have + * indisclustered set; if it was not given, scan pg_class and return all + * tables. + * + * Return it as a list of RelToCluster in the given memory context. + */ +static List * +get_tables_to_repack(RepackCommand cmd, bool usingindex, MemoryContext permcxt) +{ + Relation catalog; + TableScanDesc scan; + HeapTuple tuple; + List *rtcs = NIL; + + if (usingindex) + { + ScanKeyData entry; + + /* + * For USING INDEX, scan pg_index to find those with indisclustered. + */ + catalog = table_open(IndexRelationId, AccessShareLock); + ScanKeyInit(&entry, + Anum_pg_index_indisclustered, + BTEqualStrategyNumber, F_BOOLEQ, + BoolGetDatum(true)); + scan = table_beginscan_catalog(catalog, 1, &entry); + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + RelToCluster *rtc; + Form_pg_index index; + MemoryContext oldcxt; + + index = (Form_pg_index) GETSTRUCT(tuple); + + /* + * Try to obtain a light lock on the index's table, to ensure it + * doesn't go away while we collect the list. If we cannot, just + * disregard it. Be sure to release this if we ultimately decide + * not to process the table! + */ + if (!ConditionalLockRelationOid(index->indrelid, AccessShareLock)) + continue; + + /* Verify that the table still exists; skip if not */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(index->indrelid))) + { + UnlockRelationOid(index->indrelid, AccessShareLock); + continue; + } + + /* noisily skip rels which the user can't process */ + if (!repack_is_permitted_for_relation(cmd, index->indrelid, + GetUserId())) + { + UnlockRelationOid(index->indrelid, AccessShareLock); + continue; + } + + /* Use a permanent memory context for the result list */ + oldcxt = MemoryContextSwitchTo(permcxt); + rtc = palloc_object(RelToCluster); + rtc->tableOid = index->indrelid; + rtc->indexOid = index->indexrelid; + rtcs = lappend(rtcs, rtc); + MemoryContextSwitchTo(oldcxt); + } + } + else + { + catalog = table_open(RelationRelationId, AccessShareLock); + scan = table_beginscan_catalog(catalog, 0, NULL); + + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + RelToCluster *rtc; + Form_pg_class class; + MemoryContext oldcxt; + + class = (Form_pg_class) GETSTRUCT(tuple); + + /* + * Try to obtain a light lock on the table, to ensure it doesn't + * go away while we collect the list. If we cannot, just + * disregard the table. Be sure to release this if we ultimately + * decide not to process the table! + */ + if (!ConditionalLockRelationOid(class->oid, AccessShareLock)) + continue; + + /* Verify that the table still exists */ + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(class->oid))) + { + UnlockRelationOid(class->oid, AccessShareLock); + continue; + } + + /* Can only process plain tables and matviews */ + if (class->relkind != RELKIND_RELATION && + class->relkind != RELKIND_MATVIEW) + { + UnlockRelationOid(class->oid, AccessShareLock); + continue; + } + + /* noisily skip rels which the user can't process */ + if (!repack_is_permitted_for_relation(cmd, class->oid, + GetUserId())) + { + UnlockRelationOid(class->oid, AccessShareLock); + continue; + } + + /* Use a permanent memory context for the result list */ + oldcxt = MemoryContextSwitchTo(permcxt); + rtc = palloc_object(RelToCluster); + rtc->tableOid = class->oid; + rtc->indexOid = InvalidOid; + rtcs = lappend(rtcs, rtc); + MemoryContextSwitchTo(oldcxt); + } + } + + table_endscan(scan); + relation_close(catalog, AccessShareLock); + + return rtcs; +} + +/* + * Given a partitioned table or its index, return a list of RelToCluster for + * all the leaf child tables/indexes. + * + * 'rel_is_index' tells whether 'relid' is that of an index (true) or of the + * owning relation. + */ +static List * +get_tables_to_repack_partitioned(RepackCommand cmd, Oid relid, + bool rel_is_index, MemoryContext permcxt) +{ + List *inhoids; + List *rtcs = NIL; + + /* + * Do not lock the children until they're processed. Note that we do hold + * a lock on the parent partitioned table. + */ + inhoids = find_all_inheritors(relid, NoLock, NULL); + foreach_oid(child_oid, inhoids) + { + Oid table_oid, + index_oid; + RelToCluster *rtc; + MemoryContext oldcxt; + + if (rel_is_index) + { + /* consider only leaf indexes */ + if (get_rel_relkind(child_oid) != RELKIND_INDEX) + continue; + + table_oid = IndexGetRelation(child_oid, false); + index_oid = child_oid; + } + else + { + /* consider only leaf relations */ + if (get_rel_relkind(child_oid) != RELKIND_RELATION) + continue; + + table_oid = child_oid; + index_oid = InvalidOid; + } + + /* + * It's possible that the user does not have privileges to CLUSTER the + * leaf partition despite having them on the partitioned table. Skip + * if so. + */ + if (!repack_is_permitted_for_relation(cmd, table_oid, GetUserId())) + continue; + + /* Use a permanent memory context for the result list */ + oldcxt = MemoryContextSwitchTo(permcxt); + rtc = palloc_object(RelToCluster); + rtc->tableOid = table_oid; + rtc->indexOid = index_oid; + rtcs = lappend(rtcs, rtc); + MemoryContextSwitchTo(oldcxt); + } + + return rtcs; +} + + +/* + * Return whether userid has privileges to REPACK relid. If not, this + * function emits a WARNING. + */ +static bool +repack_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid) +{ + Assert(cmd == REPACK_COMMAND_CLUSTER || cmd == REPACK_COMMAND_REPACK); + + if (pg_class_aclcheck(relid, userid, ACL_MAINTAIN) == ACLCHECK_OK) + return true; + + ereport(WARNING, + errmsg("permission denied to execute %s on \"%s\", skipping it", + RepackCommandAsString(cmd), + get_rel_name(relid))); + + return false; +} + + +/* + * Given a RepackStmt with an indicated relation name, resolve the relation + * name, obtain lock on it, then determine what to do based on the relation + * type: if it's table and not partitioned, repack it as indicated (using an + * existing clustered index, or following the given one), and return NULL. + * + * On the other hand, if the table is partitioned, do nothing further and + * instead return the opened and locked relcache entry, so that caller can + * process the partitions using the multiple-table handling code. In this + * case, if an index name is given, it's up to the caller to resolve it. + */ +static Relation +process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel, + ClusterParams *params) +{ + Relation rel; + Oid tableOid; + + Assert(stmt->relation != NULL); + Assert(stmt->command == REPACK_COMMAND_CLUSTER || + stmt->command == REPACK_COMMAND_REPACK); + + /* + * Make sure ANALYZE is specified if a column list is present. + */ + if ((params->options & CLUOPT_ANALYZE) == 0 && stmt->relation->va_cols != NIL) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("ANALYZE option must be specified when a column list is provided")); + + /* Find, lock, and check permissions on the table. */ + tableOid = RangeVarGetRelidExtended(stmt->relation->relation, + lockmode, + 0, + RangeVarCallbackMaintainsTable, + NULL); + rel = table_open(tableOid, NoLock); + + /* + * Reject clustering a remote temp table ... their local buffer manager is + * not going to cope. + */ + if (RELATION_IS_OTHER_TEMP(rel)) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /*- translator: first %s is name of a SQL command, eg. REPACK */ + errmsg("cannot execute %s on temporary tables of other sessions", + RepackCommandAsString(stmt->command))); + + /* + * For partitioned tables, let caller handle this. Otherwise, process it + * here and we're done. + */ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + return rel; + else + { + Oid indexOid = InvalidOid; + + indexOid = determine_clustered_index(rel, stmt->usingindex, + stmt->indexname); + if (OidIsValid(indexOid)) + check_index_is_clusterable(rel, indexOid, lockmode); + + cluster_rel(stmt->command, rel, indexOid, params, isTopLevel); + + /* + * Do an analyze, if requested. We close the transaction and start a + * new one, so that we don't hold the stronger lock for longer than + * needed. + */ + if (params->options & CLUOPT_ANALYZE) + { + VacuumParams vac_params = {0}; + + PopActiveSnapshot(); + CommitTransactionCommand(); + + StartTransactionCommand(); + PushActiveSnapshot(GetTransactionSnapshot()); + + vac_params.options |= VACOPT_ANALYZE; + if (params->options & CLUOPT_VERBOSE) + vac_params.options |= VACOPT_VERBOSE; + analyze_rel(tableOid, NULL, &vac_params, + stmt->relation->va_cols, true, NULL); + PopActiveSnapshot(); + CommandCounterIncrement(); + } + + return NULL; + } +} + +/* + * Given a relation and the usingindex/indexname options in a + * REPACK USING INDEX or CLUSTER command, return the OID of the + * index to use for clustering the table. + * + * Caller must hold lock on the relation so that the set of indexes + * doesn't change, and must call check_index_is_clusterable. + */ +static Oid +determine_clustered_index(Relation rel, bool usingindex, const char *indexname) +{ + Oid indexOid; + + if (indexname == NULL && usingindex) + { + /* + * If USING INDEX with no name is given, find a clustered index, or + * error out if none. + */ + indexOid = InvalidOid; + foreach_oid(idxoid, RelationGetIndexList(rel)) + { + if (get_index_isclustered(idxoid)) + { + indexOid = idxoid; + break; + } + } + + if (!OidIsValid(indexOid)) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("there is no previously clustered index for table \"%s\"", + RelationGetRelationName(rel))); + } + else if (indexname != NULL) + { + /* An index was specified; obtain its OID. */ + indexOid = get_relname_relid(indexname, rel->rd_rel->relnamespace); + if (!OidIsValid(indexOid)) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" for table \"%s\" does not exist", + indexname, RelationGetRelationName(rel))); + } + else + indexOid = InvalidOid; + + return indexOid; +} + +static const char * +RepackCommandAsString(RepackCommand cmd) +{ + switch (cmd) + { + case REPACK_COMMAND_REPACK: + return "REPACK"; + case REPACK_COMMAND_VACUUMFULL: + return "VACUUM"; + case REPACK_COMMAND_CLUSTER: + return "CLUSTER"; + } + return "???"; /* keep compiler quiet */ +} + +/* + * Apply all the changes stored in 'file'. + */ +static void +apply_concurrent_changes(BufFile *file, ChangeContext *chgcxt) +{ + ConcurrentChangeKind kind = '\0'; + Relation rel = chgcxt->cc_rel; + TupleTableSlot *spilled_tuple; + TupleTableSlot *old_update_tuple; + TupleTableSlot *ondisk_tuple; + bool have_old_tuple = false; + MemoryContext oldcxt; + + spilled_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel), + &TTSOpsVirtual); + ondisk_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel), + table_slot_callbacks(rel)); + old_update_tuple = MakeSingleTupleTableSlot(RelationGetDescr(rel), + &TTSOpsVirtual); + + oldcxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(chgcxt->cc_estate)); + + while (true) + { + size_t nread; + ConcurrentChangeKind prevkind = kind; + + CHECK_FOR_INTERRUPTS(); + + nread = BufFileReadMaybeEOF(file, &kind, 1, true); + if (nread == 0) /* done with the file? */ + break; + + /* + * If this is the old tuple for an update, read it into the tuple slot + * and go to the next one. The update itself will be executed on the + * next iteration, when we receive the NEW tuple. + */ + if (kind == CHANGE_UPDATE_OLD) + { + restore_tuple(file, rel, old_update_tuple); + have_old_tuple = true; + continue; + } + + /* + * Just before an UPDATE or DELETE, we must update the command + * counter, because the change could refer to a tuple that we have + * just inserted; and before an INSERT, we have to do this also if the + * previous command was either update or delete. + * + * With this approach we don't spend so many CCIs for long strings of + * only INSERTs, which can't affect one another. + */ + if (kind == CHANGE_UPDATE_NEW || kind == CHANGE_DELETE || + (kind == CHANGE_INSERT && (prevkind == CHANGE_UPDATE_NEW || + prevkind == CHANGE_DELETE))) + { + CommandCounterIncrement(); + UpdateActiveSnapshotCommandId(); + } + + /* + * Now restore the tuple into the slot and execute the change. + */ + restore_tuple(file, rel, spilled_tuple); + + if (kind == CHANGE_INSERT) + { + apply_concurrent_insert(rel, spilled_tuple, chgcxt); + } + else if (kind == CHANGE_DELETE) + { + bool found; + + /* Find the tuple to be deleted */ + found = find_target_tuple(rel, chgcxt, spilled_tuple, ondisk_tuple); + if (!found) + elog(ERROR, "failed to find target tuple"); + apply_concurrent_delete(rel, ondisk_tuple); + } + else if (kind == CHANGE_UPDATE_NEW) + { + TupleTableSlot *key; + bool found; + + if (have_old_tuple) + key = old_update_tuple; + else + key = spilled_tuple; + + /* Find the tuple to be updated or deleted. */ + found = find_target_tuple(rel, chgcxt, key, ondisk_tuple); + if (!found) + elog(ERROR, "failed to find target tuple"); + + /* + * If 'tup' contains TOAST pointers, they point to the old + * relation's toast. Copy the corresponding TOAST pointers for the + * new relation from the existing tuple. (The fact that we + * received a TOAST pointer here implies that the attribute hasn't + * changed.) + */ + adjust_toast_pointers(rel, spilled_tuple, ondisk_tuple); + + apply_concurrent_update(rel, spilled_tuple, ondisk_tuple, chgcxt); + + ExecClearTuple(old_update_tuple); + have_old_tuple = false; + } + else + elog(ERROR, "unrecognized kind of change: %d", kind); + + ResetPerTupleExprContext(chgcxt->cc_estate); + } + + /* Cleanup. */ + ExecDropSingleTupleTableSlot(spilled_tuple); + ExecDropSingleTupleTableSlot(ondisk_tuple); + ExecDropSingleTupleTableSlot(old_update_tuple); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * Apply an insert from the spill of concurrent changes to the new copy of the + * table. + */ +static void +apply_concurrent_insert(Relation rel, TupleTableSlot *slot, + ChangeContext *chgcxt) +{ + /* Put the tuple in the table, but make sure it won't be decoded */ + table_tuple_insert(rel, slot, GetCurrentCommandId(true), + TABLE_INSERT_NO_LOGICAL, NULL); + + /* Update indexes with this new tuple. */ + ExecInsertIndexTuples(chgcxt->cc_rri, + chgcxt->cc_estate, + 0, + slot, + NIL, NULL); + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, 1); +} + +/* + * Apply an update from the spill of concurrent changes to the new copy of the + * table. + */ +static void +apply_concurrent_update(Relation rel, TupleTableSlot *spilled_tuple, + TupleTableSlot *ondisk_tuple, + ChangeContext *chgcxt) +{ + LockTupleMode lockmode; + TM_FailureData tmfd; + TU_UpdateIndexes update_indexes; + TM_Result res; + + /* + * Carry out the update, skipping logical decoding for it. + */ + res = table_tuple_update(rel, &(ondisk_tuple->tts_tid), spilled_tuple, + GetCurrentCommandId(true), + TABLE_UPDATE_NO_LOGICAL, + InvalidSnapshot, + InvalidSnapshot, + false, + &tmfd, &lockmode, &update_indexes); + if (res != TM_Ok) + ereport(ERROR, + errmsg("failed to apply concurrent UPDATE")); + + if (update_indexes != TU_None) + { + uint32 flags = EIIT_IS_UPDATE; + + if (update_indexes == TU_Summarizing) + flags |= EIIT_ONLY_SUMMARIZING; + ExecInsertIndexTuples(chgcxt->cc_rri, + chgcxt->cc_estate, + flags, + spilled_tuple, + NIL, NULL); + } + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_UPDATED, 1); +} + +static void +apply_concurrent_delete(Relation rel, TupleTableSlot *slot) +{ + TM_Result res; + TM_FailureData tmfd; + + /* + * Delete tuple from the new heap, skipping logical decoding for it. + */ + res = table_tuple_delete(rel, &(slot->tts_tid), + GetCurrentCommandId(true), + TABLE_DELETE_NO_LOGICAL, + InvalidSnapshot, InvalidSnapshot, + false, + &tmfd); + + if (res != TM_Ok) + ereport(ERROR, + errmsg("failed to apply concurrent DELETE")); + + pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1); +} + +/* + * Read tuple from file and put it in the input slot. All memory is allocated + * in the current memory context; caller is responsible for freeing it as + * appropriate. + * + * External attributes are stored in separate memory chunks, in order to avoid + * exceeding MaxAllocSize - that could happen if the individual attributes are + * smaller than MaxAllocSize but the whole tuple is bigger. + */ +static void +restore_tuple(BufFile *file, Relation relation, TupleTableSlot *slot) +{ + uint32 t_len; + HeapTuple tup; + int natt_ext; + + /* Read the tuple. */ + BufFileReadExact(file, &t_len, sizeof(t_len)); + tup = (HeapTuple) palloc(HEAPTUPLESIZE + t_len); + tup->t_data = (HeapTupleHeader) ((char *) tup + HEAPTUPLESIZE); + BufFileReadExact(file, tup->t_data, t_len); + tup->t_len = t_len; + ItemPointerSetInvalid(&tup->t_self); + tup->t_tableOid = RelationGetRelid(relation); + + /* + * Put the tuple we read in a slot. This deforms it, so that we can hack + * the external attributes in place. + */ + ExecForceStoreHeapTuple(tup, slot, false); + + /* + * Next, read any attributes we stored separately into the tts_values + * array elements expecting them, if any. This matches + * repack_store_change. + */ + BufFileReadExact(file, &natt_ext, sizeof(natt_ext)); + if (natt_ext > 0) + { + TupleDesc desc = slot->tts_tupleDescriptor; + + for (int i = 0; i < desc->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(desc, i); + varlena *varlen; + uint64 chunk_header; + void *value; + Size varlensz; + + if (attr->attisdropped || attr->attlen != -1) + continue; + if (slot_attisnull(slot, i + 1)) + continue; + varlen = (varlena *) DatumGetPointer(slot->tts_values[i]); + if (!VARATT_IS_EXTERNAL_INDIRECT(varlen)) + continue; + slot_getsomeattrs(slot, i + 1); + + BufFileReadExact(file, &chunk_header, VARHDRSZ); + varlensz = VARSIZE_ANY(&chunk_header); + + value = palloc(varlensz); + SET_VARSIZE(value, VARSIZE_ANY(&chunk_header)); + BufFileReadExact(file, (char *) value + VARHDRSZ, varlensz - VARHDRSZ); + + slot->tts_values[i] = PointerGetDatum(value); + natt_ext--; + if (natt_ext < 0) + ereport(ERROR, + errcode(ERRCODE_DATA_CORRUPTED), + errmsg("insufficient number of attributes stored separately")); + } + } +} + +/* + * Adjust 'dest' replacing any EXTERNAL_ONDISK toast pointers with the + * corresponding ones from 'src'. + */ +static void +adjust_toast_pointers(Relation relation, TupleTableSlot *dest, TupleTableSlot *src) +{ + TupleDesc desc = dest->tts_tupleDescriptor; + + for (int i = 0; i < desc->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(desc, i); + varlena *varlena_dst; + + if (attr->attisdropped) + continue; + if (attr->attlen != -1) + continue; + if (slot_attisnull(dest, i + 1)) + continue; + + slot_getsomeattrs(dest, i + 1); + + varlena_dst = (varlena *) DatumGetPointer(dest->tts_values[i]); + if (!VARATT_IS_EXTERNAL_ONDISK(varlena_dst)) + continue; + slot_getsomeattrs(src, i + 1); + + dest->tts_values[i] = src->tts_values[i]; + } +} + +/* + * Find the tuple to be updated or deleted by the given data change, whose + * tuple has already been loaded into locator. + * + * If the tuple is found, put it in retrieved and return true. If the tuple is + * not found, return false. + */ +static bool +find_target_tuple(Relation rel, ChangeContext *chgcxt, TupleTableSlot *locator, + TupleTableSlot *retrieved) +{ + Form_pg_index idx = chgcxt->cc_ident_index->rd_index; + IndexScanDesc scan; + bool retval; + + /* + * Scan key is passed by caller, so it does not have to be constructed + * multiple times. Key entries have all fields initialized, except for + * sk_argument. + * + * Use the incoming tuple to finalize the scan key. + */ + for (int i = 0; i < chgcxt->cc_ident_key_nentries; i++) + { + ScanKey entry = &chgcxt->cc_ident_key[i]; + AttrNumber attno = idx->indkey.values[i]; + + entry->sk_argument = locator->tts_values[attno - 1]; + Assert(!locator->tts_isnull[attno - 1]); + } + + /* XXX no instrumentation for now */ + scan = index_beginscan(rel, chgcxt->cc_ident_index, GetActiveSnapshot(), + NULL, chgcxt->cc_ident_key_nentries, 0, 0); + index_rescan(scan, chgcxt->cc_ident_key, chgcxt->cc_ident_key_nentries, NULL, 0); + retval = index_getnext_slot(scan, ForwardScanDirection, retrieved); + index_endscan(scan); + + return retval; +} + +/* + * Decode and apply concurrent changes, up to (and including) the record whose + * LSN is 'end_of_wal'. + * + * XXX the names "process_concurrent_changes" and "apply_concurrent_changes" + * are far too similar to each other. + */ +static void +process_concurrent_changes(XLogRecPtr end_of_wal, ChangeContext *chgcxt, bool done) +{ + DecodingWorkerShared *shared; + char fname[MAXPGPATH]; + BufFile *file; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_CATCH_UP); + + /* Ask the worker for the file. */ + shared = (DecodingWorkerShared *) dsm_segment_address(decoding_worker->seg); + SpinLockAcquire(&shared->mutex); + shared->lsn_upto = end_of_wal; + shared->done = done; + SpinLockRelease(&shared->mutex); + + /* + * The worker needs to finish processing of the current WAL record. Even + * if it's idle, it'll need to close the output file. Thus we're likely to + * wait, so prepare for sleep. + */ + ConditionVariablePrepareToSleep(&shared->cv); + for (;;) + { + int last_exported; + + SpinLockAcquire(&shared->mutex); + last_exported = shared->last_exported; + SpinLockRelease(&shared->mutex); + + /* + * Has the worker exported the file we are waiting for? + */ + if (last_exported == chgcxt->cc_file_seq) + break; + + ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT); + } + ConditionVariableCancelSleep(); + + /* Open the file. */ + DecodingWorkerFileName(fname, shared->relid, chgcxt->cc_file_seq); + file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false); + apply_concurrent_changes(file, chgcxt); + + BufFileClose(file); + + /* Get ready for the next file. */ + chgcxt->cc_file_seq++; +} + +/* + * Initialize the ChangeContext struct for the given relation, with + * the given index as identity index. + */ +static void +initialize_change_context(ChangeContext *chgcxt, + Relation relation, Oid ident_index_id) +{ + chgcxt->cc_rel = relation; + + /* Only initialize fields needed by ExecInsertIndexTuples(). */ + chgcxt->cc_estate = CreateExecutorState(); + + chgcxt->cc_rri = (ResultRelInfo *) palloc(sizeof(ResultRelInfo)); + InitResultRelInfo(chgcxt->cc_rri, relation, 0, 0, 0); + ExecOpenIndices(chgcxt->cc_rri, false); + + /* + * The table's relcache entry already has the relcache entry for the + * identity index; find that. + */ + chgcxt->cc_ident_index = NULL; + for (int i = 0; i < chgcxt->cc_rri->ri_NumIndices; i++) + { + Relation ind_rel; + + ind_rel = chgcxt->cc_rri->ri_IndexRelationDescs[i]; + if (ind_rel->rd_id == ident_index_id) + { + chgcxt->cc_ident_index = ind_rel; + break; + } + } + if (chgcxt->cc_ident_index == NULL) + elog(ERROR, "failed to find identity index"); + + /* Set up for scanning said identity index */ + { + Form_pg_index indexForm; + + indexForm = chgcxt->cc_ident_index->rd_index; + chgcxt->cc_ident_key_nentries = indexForm->indnkeyatts; + chgcxt->cc_ident_key = (ScanKey) palloc_array(ScanKeyData, indexForm->indnkeyatts); + for (int i = 0; i < indexForm->indnkeyatts; i++) + { + ScanKey entry; + Oid opfamily, + opcintype, + opno, + opcode; + + entry = &chgcxt->cc_ident_key[i]; + + opfamily = chgcxt->cc_ident_index->rd_opfamily[i]; + opcintype = chgcxt->cc_ident_index->rd_opcintype[i]; + opno = get_opfamily_member(opfamily, opcintype, opcintype, + BTEqualStrategyNumber); + if (!OidIsValid(opno)) + elog(ERROR, "failed to find = operator for type %u", opcintype); + opcode = get_opcode(opno); + if (!OidIsValid(opcode)) + elog(ERROR, "failed to find = operator for operator %u", opno); + + /* Initialize everything but argument. */ + ScanKeyInit(entry, + i + 1, + BTEqualStrategyNumber, opcode, + (Datum) 0); + entry->sk_collation = chgcxt->cc_ident_index->rd_indcollation[i]; + } + } + + chgcxt->cc_file_seq = WORKER_FILE_SNAPSHOT + 1; +} + +/* + * Free up resources taken by a ChangeContext. + */ +static void +release_change_context(ChangeContext *chgcxt) +{ + ExecCloseIndices(chgcxt->cc_rri); + FreeExecutorState(chgcxt->cc_estate); + /* XXX are these pfrees necessary? */ + pfree(chgcxt->cc_rri); + pfree(chgcxt->cc_ident_key); +} + +/* + * The final steps of rebuild_relation() for concurrent processing. + * + * On entry, NewHeap is locked in AccessExclusiveLock mode. OldHeap and its + * clustering index (if one is passed) are still locked in a mode that allows + * concurrent data changes. On exit, both tables and their indexes are closed, + * but locked in AccessExclusiveLock mode. + */ +static void +rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, + Oid identIdx, TransactionId frozenXid, + MultiXactId cutoffMulti) +{ + List *ind_oids_new; + Oid old_table_oid = RelationGetRelid(OldHeap); + Oid new_table_oid = RelationGetRelid(NewHeap); + List *ind_oids_old = RelationGetIndexList(OldHeap); + ListCell *lc, + *lc2; + char relpersistence; + bool is_system_catalog; + Oid ident_idx_new; + XLogRecPtr end_of_wal; + List *indexrels; + ChangeContext chgcxt; + + Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false)); + Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false)); + + /* + * Unlike the exclusive case, we build new indexes for the new relation + * rather than swapping the storage and reindexing the old relation. The + * point is that the index build can take some time, so we do it before we + * get AccessExclusiveLock on the old heap and therefore we cannot swap + * the heap storage yet. + * + * index_create() will lock the new indexes using AccessExclusiveLock - no + * need to change that. At the same time, we use ShareUpdateExclusiveLock + * to lock the existing indexes - that should be enough to prevent others + * from changing them while we're repacking the relation. The lock on + * table should prevent others from changing the index column list, but + * might not be enough for commands like ALTER INDEX ... SET ... (Those + * are not necessarily dangerous, but can make user confused if the + * changes they do get lost due to REPACK.) + */ + ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old); + + /* + * The identity index in the new relation appears in the same relative + * position as the corresponding index in the old relation. Find it. + */ + ident_idx_new = InvalidOid; + foreach_oid(ind_old, ind_oids_old) + { + if (identIdx == ind_old) + { + int pos = foreach_current_index(ind_old); + + if (unlikely(list_length(ind_oids_new) < pos)) + elog(ERROR, "list of new indexes too short"); + ident_idx_new = list_nth_oid(ind_oids_new, pos); + break; + } + } + if (!OidIsValid(ident_idx_new)) + elog(ERROR, "could not find index matching \"%s\" at the new relation", + get_rel_name(identIdx)); + + /* Gather information to apply concurrent changes. */ + initialize_change_context(&chgcxt, NewHeap, ident_idx_new); + + /* + * During testing, wait for another backend to perform concurrent data + * changes which we will process below. + */ + INJECTION_POINT("repack-concurrently-before-lock", NULL); + + /* + * Flush all WAL records inserted so far (possibly except for the last + * incomplete page; see GetInsertRecPtr), to minimize the amount of data + * we need to flush while holding exclusive lock on the source table. + */ + XLogFlush(GetXLogInsertEndRecPtr()); + end_of_wal = GetFlushRecPtr(NULL); + + /* + * Apply concurrent changes first time, to minimize the time we need to + * hold AccessExclusiveLock. (Quite some amount of WAL could have been + * written during the data copying and index creation.) + */ + process_concurrent_changes(end_of_wal, &chgcxt, false); + + /* + * Acquire AccessExclusiveLock on the table, its TOAST relation (if there + * is one), all its indexes, so that we can swap the files. + */ + LockRelationOid(old_table_oid, AccessExclusiveLock); + + /* + * Lock all indexes now, not only the clustering one: all indexes need to + * have their files swapped. While doing that, store their relation + * references in a zero-terminated array, to handle predicate locks below. + */ + indexrels = NIL; + foreach_oid(ind_oid, ind_oids_old) + { + Relation index; + + index = index_open(ind_oid, AccessExclusiveLock); + + /* + * Some things about the index may have changed before we locked the + * index, such as ALTER INDEX RENAME. We don't need to do anything + * here to absorb those changes in the new index. + */ + indexrels = lappend(indexrels, index); + } + + /* + * Lock the OldHeap's TOAST relation exclusively - again, the lock is + * needed to swap the files. + */ + if (OidIsValid(OldHeap->rd_rel->reltoastrelid)) + LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock); + + /* + * Tuples and pages of the old heap will be gone, but the heap will stay. + */ + TransferPredicateLocksToHeapRelation(OldHeap); + foreach_ptr(RelationData, index, indexrels) + { + TransferPredicateLocksToHeapRelation(index); + index_close(index, NoLock); + } + list_free(indexrels); + + /* + * Flush WAL again, to make sure that all changes committed while we were + * waiting for the exclusive lock are available for decoding. + */ + XLogFlush(GetXLogInsertEndRecPtr()); + end_of_wal = GetFlushRecPtr(NULL); + + /* + * Apply the concurrent changes again. Indicate that the decoding worker + * won't be needed anymore. + */ + process_concurrent_changes(end_of_wal, &chgcxt, true); + + /* Remember info about rel before closing OldHeap */ + relpersistence = OldHeap->rd_rel->relpersistence; + is_system_catalog = IsSystemRelation(OldHeap); + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_SWAP_REL_FILES); + + /* + * Even ShareUpdateExclusiveLock should have prevented others from + * creating / dropping indexes (even using the CONCURRENTLY option), so we + * do not need to check whether the lists match. + */ + forboth(lc, ind_oids_old, lc2, ind_oids_new) + { + Oid ind_old = lfirst_oid(lc); + Oid ind_new = lfirst_oid(lc2); + Oid mapped_tables[4] = {0}; + + swap_relation_files(ind_old, ind_new, + (old_table_oid == RelationRelationId), + false, /* swap_toast_by_content */ + true, + InvalidTransactionId, + InvalidMultiXactId, + mapped_tables); + +#ifdef USE_ASSERT_CHECKING + + /* + * Concurrent processing is not supported for system relations, so + * there should be no mapped tables. + */ + for (int i = 0; i < 4; i++) + Assert(!OidIsValid(mapped_tables[i])); +#endif + } + + /* The new indexes must be visible for deletion. */ + CommandCounterIncrement(); + + /* Close the old heap but keep lock until transaction commit. */ + table_close(OldHeap, NoLock); + /* Close the new heap. (We didn't have to open its indexes). */ + table_close(NewHeap, NoLock); + + /* Cleanup what we don't need anymore. (And close the identity index.) */ + release_change_context(&chgcxt); + + /* + * Swap the relations and their TOAST relations and TOAST indexes. This + * also drops the new relation and its indexes. + * + * (System catalogs are currently not supported.) + */ + Assert(!is_system_catalog); + finish_heap_swap(old_table_oid, new_table_oid, + is_system_catalog, + false, /* swap_toast_by_content */ + false, + true, + false, /* reindex */ + frozenXid, cutoffMulti, + relpersistence); +} + +/* + * Build indexes on NewHeap according to those on OldHeap. + * + * OldIndexes is the list of index OIDs on OldHeap. The contained indexes end + * up locked using ShareUpdateExclusiveLock. + * + * A list of OIDs of the corresponding indexes created on NewHeap is + * returned. The order of items does match, so we can use these arrays to swap + * index storage. + */ +static List * +build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes) +{ + List *result = NIL; + + pgstat_progress_update_param(PROGRESS_REPACK_PHASE, + PROGRESS_REPACK_PHASE_REBUILD_INDEX); + + foreach_oid(oldindex, OldIndexes) + { + Oid newindex; + char *newName; + Relation ind; + + ind = index_open(oldindex, ShareUpdateExclusiveLock); + + newName = ChooseRelationName(get_rel_name(oldindex), + NULL, + "repacknew", + get_rel_namespace(ind->rd_index->indrelid), + false); + newindex = index_create_copy(NewHeap, INDEX_CREATE_SUPPRESS_PROGRESS, + oldindex, ind->rd_rel->reltablespace, + newName); + copy_index_constraints(ind, newindex, RelationGetRelid(NewHeap)); + result = lappend_oid(result, newindex); + + index_close(ind, NoLock); + } + + return result; +} + +/* + * Create a transient copy of a constraint -- supported by a transient + * copy of the index that supports the original constraint. + * + * When repacking a table that contains exclusion constraints, the executor + * relies on these constraints being properly catalogued. These copies are + * to support that. + * + * We don't need the constraints for anything else (the original constraints + * will be there once repack completes), so we add pg_depend entries so that + * the are dropped when the transient table is dropped. + */ +static void +copy_index_constraints(Relation old_index, Oid new_index_id, Oid new_heap_id) +{ + ScanKeyData skey; + Relation rel; + TupleDesc desc; + SysScanDesc scan; + HeapTuple tup; + ObjectAddress objrel; + + rel = table_open(ConstraintRelationId, RowExclusiveLock); + ObjectAddressSet(objrel, RelationRelationId, new_heap_id); + + /* + * Retrieve the constraints supported by the old index and create an + * identical one that points to the new index. + */ + ScanKeyInit(&skey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(old_index->rd_index->indrelid)); + scan = systable_beginscan(rel, ConstraintRelidTypidNameIndexId, true, + NULL, 1, &skey); + desc = RelationGetDescr(rel); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(tup); + Oid oid; + Datum values[Natts_pg_constraint] = {0}; + bool nulls[Natts_pg_constraint] = {0}; + bool replaces[Natts_pg_constraint] = {0}; + HeapTuple new_tup; + ObjectAddress objcon; + + if (conform->conindid != RelationGetRelid(old_index)) + continue; + + oid = GetNewOidWithIndex(rel, ConstraintOidIndexId, + Anum_pg_constraint_oid); + values[Anum_pg_constraint_oid - 1] = ObjectIdGetDatum(oid); + replaces[Anum_pg_constraint_oid - 1] = true; + values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(new_heap_id); + replaces[Anum_pg_constraint_conrelid - 1] = true; + values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(new_index_id); + replaces[Anum_pg_constraint_conindid - 1] = true; + + new_tup = heap_modify_tuple(tup, desc, values, nulls, replaces); + + /* Insert it into the catalog. */ + CatalogTupleInsert(rel, new_tup); + + /* Create a dependency so it's removed when we drop the new heap. */ + ObjectAddressSet(objcon, ConstraintRelationId, oid); + recordDependencyOn(&objcon, &objrel, DEPENDENCY_AUTO); + } + systable_endscan(scan); + + table_close(rel, RowExclusiveLock); + + CommandCounterIncrement(); +} + +/* + * Try to start a background worker to perform logical decoding of data + * changes applied to relation while REPACK CONCURRENTLY is copying its + * contents to a new table. + */ +static void +start_repack_decoding_worker(Oid relid) +{ + Size size; + dsm_segment *seg; + DecodingWorkerShared *shared; + shm_mq *mq; + shm_mq_handle *mqh; + BackgroundWorker bgw; + + /* Setup shared memory. */ + size = BUFFERALIGN(offsetof(DecodingWorkerShared, error_queue)) + + BUFFERALIGN(REPACK_ERROR_QUEUE_SIZE); + seg = dsm_create(size, 0); + shared = (DecodingWorkerShared *) dsm_segment_address(seg); + shared->initialized = false; + shared->lsn_upto = InvalidXLogRecPtr; + shared->done = false; + SharedFileSetInit(&shared->sfs, seg); + shared->last_exported = -1; + SpinLockInit(&shared->mutex); + shared->dbid = MyDatabaseId; + + /* + * This is the UserId set in cluster_rel(). Security context shouldn't be + * needed for decoding worker. + */ + shared->roleid = GetUserId(); + shared->relid = relid; + ConditionVariableInit(&shared->cv); + shared->backend_proc = MyProc; + shared->backend_pid = MyProcPid; + shared->backend_proc_number = MyProcNumber; + + mq = shm_mq_create((char *) BUFFERALIGN(shared->error_queue), + REPACK_ERROR_QUEUE_SIZE); + shm_mq_set_receiver(mq, MyProc); + mqh = shm_mq_attach(mq, seg, NULL); + + memset(&bgw, 0, sizeof(bgw)); + snprintf(bgw.bgw_name, BGW_MAXLEN, + "REPACK decoding worker for relation \"%s\"", + get_rel_name(relid)); + snprintf(bgw.bgw_type, BGW_MAXLEN, "REPACK decoding worker"); + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | + BGWORKER_BACKEND_DATABASE_CONNECTION; + bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; + bgw.bgw_restart_time = BGW_NEVER_RESTART; + snprintf(bgw.bgw_library_name, MAXPGPATH, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "RepackWorkerMain"); + bgw.bgw_main_arg = UInt32GetDatum(dsm_segment_handle(seg)); + bgw.bgw_notify_pid = MyProcPid; + + decoding_worker = palloc0_object(DecodingWorker); + if (!RegisterDynamicBackgroundWorker(&bgw, &decoding_worker->handle)) + ereport(ERROR, + errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), + errmsg("out of background worker slots"), + errhint("You might need to increase \"%s\".", "max_worker_processes")); + + decoding_worker->seg = seg; + decoding_worker->error_mqh = mqh; + + /* + * The decoding setup must be done before the caller can have XID assigned + * for any reason, otherwise the worker might end up in a deadlock, + * waiting for the caller's transaction to end. Therefore wait here until + * the worker indicates that it has the logical decoding initialized. + */ + ConditionVariablePrepareToSleep(&shared->cv); + for (;;) + { + bool initialized; + + SpinLockAcquire(&shared->mutex); + initialized = shared->initialized; + SpinLockRelease(&shared->mutex); + + if (initialized) + break; + + ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT); + } + ConditionVariableCancelSleep(); +} + +/* + * Stop the decoding worker and cleanup the related resources. + * + * The worker stops on its own when it knows there is no more work to do, but + * we need to stop it explicitly at least on ERROR in the launching backend. + */ +static void +stop_repack_decoding_worker(void) +{ + BgwHandleStatus status; + + /* Haven't reached the worker startup? */ + if (decoding_worker == NULL) + return; + + /* Could not register the worker? */ + if (decoding_worker->handle == NULL) + return; + + TerminateBackgroundWorker(decoding_worker->handle); + /* The worker should really exit before the REPACK command does. */ + HOLD_INTERRUPTS(); + status = WaitForBackgroundWorkerShutdown(decoding_worker->handle); + RESUME_INTERRUPTS(); + + if (status == BGWH_POSTMASTER_DIED) + ereport(FATAL, + errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("postmaster exited during REPACK command")); + + shm_mq_detach(decoding_worker->error_mqh); + + /* + * If we could not cancel the current sleep due to ERROR, do that before + * we detach from the shared memory the condition variable is located in. + * If we did not, the bgworker ERROR handling code would try and fail + * badly. + */ + ConditionVariableCancelSleep(); + + dsm_detach(decoding_worker->seg); + pfree(decoding_worker); + decoding_worker = NULL; +} + +/* + * Get the initial snapshot from the decoding worker. + */ +static Snapshot +get_initial_snapshot(DecodingWorker *worker) +{ + DecodingWorkerShared *shared; + char fname[MAXPGPATH]; + BufFile *file; + Size snap_size; + char *snap_space; + Snapshot snapshot; + + shared = (DecodingWorkerShared *) dsm_segment_address(worker->seg); + + /* + * The worker needs to initialize the logical decoding, which usually + * takes some time. Therefore it makes sense to prepare for the sleep + * first. + */ + ConditionVariablePrepareToSleep(&shared->cv); + for (;;) + { + int last_exported; + + SpinLockAcquire(&shared->mutex); + last_exported = shared->last_exported; + SpinLockRelease(&shared->mutex); + + /* + * Has the worker exported the file we are waiting for? + */ + if (last_exported == WORKER_FILE_SNAPSHOT) + break; + + ConditionVariableSleep(&shared->cv, WAIT_EVENT_REPACK_WORKER_EXPORT); + } + ConditionVariableCancelSleep(); + + /* Read the snapshot from a file. */ + DecodingWorkerFileName(fname, shared->relid, WORKER_FILE_SNAPSHOT); + file = BufFileOpenFileSet(&shared->sfs.fs, fname, O_RDONLY, false); + BufFileReadExact(file, &snap_size, sizeof(snap_size)); + snap_space = (char *) palloc(snap_size); + BufFileReadExact(file, snap_space, snap_size); + BufFileClose(file); + + /* Restore it. */ + snapshot = RestoreSnapshot(snap_space); + pfree(snap_space); + + return snapshot; +} + +/* + * Generate worker's file name into 'fname', which must be of size MAXPGPATH. + * If relations of the same 'relid' happen to be processed at the same time, + * they must be from different databases and therefore different backends must + * be involved. + */ +void +DecodingWorkerFileName(char *fname, Oid relid, uint32 seq) +{ + /* The PID is already present in the fileset name, so we needn't add it */ + snprintf(fname, MAXPGPATH, "%u-%u", relid, seq); +} + +/* + * Handle receipt of an interrupt indicating a repack worker message. + * + * Note: this is called within a signal handler! All we can do is set + * a flag that will cause the next CHECK_FOR_INTERRUPTS() to invoke + * ProcessRepackMessages(). + */ +void +HandleRepackMessageInterrupt(void) +{ + InterruptPending = true; + RepackMessagePending = true; + SetLatch(MyLatch); +} + +/* + * Process any queued protocol messages received from the repack worker. + */ +void +ProcessRepackMessages(void) +{ + MemoryContext oldcontext; + static MemoryContext hpm_context = NULL; + + /* + * Nothing to do if we haven't launched the worker yet or have already + * terminated it. + */ + if (decoding_worker == NULL) + return; + + /* + * This is invoked from ProcessInterrupts(), and since some of the + * functions it calls contain CHECK_FOR_INTERRUPTS(), there is a potential + * for recursive calls if more signals are received while this runs. It's + * unclear that recursive entry would be safe, and it doesn't seem useful + * even if it is safe, so let's block interrupts until done. + */ + HOLD_INTERRUPTS(); + + /* + * Moreover, CurrentMemoryContext might be pointing almost anywhere. We + * don't want to risk leaking data into long-lived contexts, so let's do + * our work here in a private context that we can reset on each use. + */ + if (hpm_context == NULL) /* first time through? */ + hpm_context = AllocSetContextCreate(TopMemoryContext, + "ProcessRepackMessages", + ALLOCSET_DEFAULT_SIZES); + else + MemoryContextReset(hpm_context); + + oldcontext = MemoryContextSwitchTo(hpm_context); + + /* OK to process messages. Reset the flag saying there are more to do. */ + RepackMessagePending = false; + + /* + * Read as many messages as we can from the worker, but stop when no more + * messages can be read from the worker without blocking. + */ + while (true) + { + shm_mq_result res; + Size nbytes; + void *data; + + res = shm_mq_receive(decoding_worker->error_mqh, &nbytes, + &data, true); + if (res == SHM_MQ_WOULD_BLOCK) + break; + else if (res == SHM_MQ_SUCCESS) + { + StringInfoData msg; + + initStringInfo(&msg); + appendBinaryStringInfo(&msg, data, nbytes); + ProcessRepackMessage(&msg); + pfree(msg.data); + } + else + { + /* + * The decoding worker is special in that it exits as soon as it + * has its work done. Thus the DETACHED result code is fine. + */ + Assert(res == SHM_MQ_DETACHED); + + break; + } + } + + MemoryContextSwitchTo(oldcontext); + + /* Might as well clear the context on our way out */ + MemoryContextReset(hpm_context); + + RESUME_INTERRUPTS(); +} + +/* + * Process a single protocol message received from a single parallel worker. + */ +static void +ProcessRepackMessage(StringInfo msg) +{ + char msgtype; + + msgtype = pq_getmsgbyte(msg); + + switch (msgtype) + { + case PqMsg_ErrorResponse: + case PqMsg_NoticeResponse: + { + ErrorData edata; + + /* Parse ErrorResponse or NoticeResponse. */ + pq_parse_errornotice(msg, &edata); + + /* Death of a worker isn't enough justification for suicide. */ + edata.elevel = Min(edata.elevel, ERROR); + + /* + * Add a context line to show that this is a message + * propagated from the worker. Otherwise, it can sometimes be + * confusing to understand what actually happened. + */ + if (edata.context) + edata.context = psprintf("%s\n%s", edata.context, + _("REPACK decoding worker")); + else + edata.context = pstrdup(_("REPACK decoding worker")); + + /* Rethrow error or print notice. */ + ThrowErrorData(&edata); + + break; + } + + default: + { + elog(ERROR, "unrecognized message type received from decoding worker: %c (message length %d bytes)", + msgtype, msg->len); + } + } +} diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c new file mode 100644 index 0000000000000..c40f8c98e0660 --- /dev/null +++ b/src/backend/commands/repack_worker.c @@ -0,0 +1,537 @@ +/*------------------------------------------------------------------------- + * + * repack_worker.c + * Implementation of the background worker for ad-hoc logical decoding + * during REPACK (CONCURRENTLY). + * + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/backend/commands/repack_worker.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/table.h" +#include "access/xlog_internal.h" +#include "access/xlogutils.h" +#include "access/xlogwait.h" +#include "commands/repack.h" +#include "commands/repack_internal.h" +#include "libpq/pqmq.h" +#include "replication/snapbuild.h" +#include "storage/ipc.h" +#include "storage/proc.h" +#include "tcop/tcopprot.h" +#include "utils/memutils.h" + +#define REPL_PLUGIN_NAME "pgrepack" + +static void RepackWorkerShutdown(int code, Datum arg); +static LogicalDecodingContext *repack_setup_logical_decoding(Oid relid); +static void repack_cleanup_logical_decoding(LogicalDecodingContext *ctx); +static void export_initial_snapshot(Snapshot snapshot, + DecodingWorkerShared *shared); +static bool decode_concurrent_changes(LogicalDecodingContext *ctx, + DecodingWorkerShared *shared); + +/* Is this process a REPACK worker? */ +static bool am_repack_worker = false; + +/* The WAL segment being decoded. */ +static XLogSegNo repack_current_segment = 0; + +/* Our DSM segment, for shutting down */ +static dsm_segment *worker_dsm_segment = NULL; + +/* + * Keep track of the table we're processing, to skip logical decoding of data + * from other relations. + */ +static RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid}; +static RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid}; + + +/* REPACK decoding worker entry point */ +void +RepackWorkerMain(Datum main_arg) +{ + dsm_segment *seg; + DecodingWorkerShared *shared; + shm_mq *mq; + shm_mq_handle *mqh; + LogicalDecodingContext *decoding_ctx; + SharedFileSet *sfs; + Snapshot snapshot; + + am_repack_worker = true; + + /* + * Override the default bgworker_die() with die() so we can use + * CHECK_FOR_INTERRUPTS(). + */ + pqsignal(SIGTERM, die); + BackgroundWorkerUnblockSignals(); + + seg = dsm_attach(DatumGetUInt32(main_arg)); + if (seg == NULL) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not map dynamic shared memory segment")); + worker_dsm_segment = seg; + + shared = (DecodingWorkerShared *) dsm_segment_address(seg); + + /* Arrange to signal the leader if we exit. */ + before_shmem_exit(RepackWorkerShutdown, PointerGetDatum(shared)); + + /* + * Join locking group - see the comments around the call of + * start_repack_decoding_worker(). + */ + if (!BecomeLockGroupMember(shared->backend_proc, shared->backend_pid)) + return; /* The leader is not running anymore. */ + + /* + * Setup a queue to send error messages to the backend that launched this + * worker. + */ + mq = (shm_mq *) (char *) BUFFERALIGN(shared->error_queue); + shm_mq_set_sender(mq, MyProc); + mqh = shm_mq_attach(mq, seg, NULL); + pq_redirect_to_shm_mq(seg, mqh); + pq_set_parallel_leader(shared->backend_pid, + shared->backend_proc_number); + + /* Connect to the database. LOGIN is not required. */ + BackgroundWorkerInitializeConnectionByOid(shared->dbid, shared->roleid, + BGWORKER_BYPASS_ROLELOGINCHECK); + + /* + * Transaction is needed to open relation, and it also provides us with a + * resource owner. + */ + StartTransactionCommand(); + + shared = (DecodingWorkerShared *) dsm_segment_address(seg); + + /* + * Not sure the spinlock is needed here - the backend should not change + * anything in the shared memory until we have serialized the snapshot. + */ + SpinLockAcquire(&shared->mutex); + Assert(!XLogRecPtrIsValid(shared->lsn_upto)); + sfs = &shared->sfs; + SpinLockRelease(&shared->mutex); + + SharedFileSetAttach(sfs, seg); + + /* + * Prepare to capture the concurrent data changes ourselves. + */ + decoding_ctx = repack_setup_logical_decoding(shared->relid); + + /* Announce that we're ready. */ + SpinLockAcquire(&shared->mutex); + shared->initialized = true; + SpinLockRelease(&shared->mutex); + ConditionVariableSignal(&shared->cv); + + /* There doesn't seem to a nice API to set these */ + XactIsoLevel = XACT_REPEATABLE_READ; + XactReadOnly = true; + + /* Build the initial snapshot and export it. */ + snapshot = SnapBuildInitialSnapshot(decoding_ctx->snapshot_builder); + export_initial_snapshot(snapshot, shared); + + /* + * Only historic snapshots should be used now. Do not let us restrict the + * progress of xmin horizon. + */ + InvalidateCatalogSnapshot(); + + for (;;) + { + bool stop = decode_concurrent_changes(decoding_ctx, shared); + + if (stop) + break; + + } + + /* Cleanup. */ + repack_cleanup_logical_decoding(decoding_ctx); + CommitTransactionCommand(); +} + +/* + * See ParallelWorkerShutdown for details. + */ +static void +RepackWorkerShutdown(int code, Datum arg) +{ + DecodingWorkerShared *shared = (DecodingWorkerShared *) DatumGetPointer(arg); + + SendProcSignal(shared->backend_pid, + PROCSIG_REPACK_MESSAGE, + shared->backend_proc_number); + + dsm_detach(worker_dsm_segment); +} + +bool +AmRepackWorker(void) +{ + return am_repack_worker; +} + +/* + * This function is much like pg_create_logical_replication_slot() except that + * the new slot is neither released (if anyone else could read changes from + * our slot, we could miss changes other backends do while we copy the + * existing data into temporary table), nor persisted (it's easier to handle + * crash by restarting all the work from scratch). + */ +static LogicalDecodingContext * +repack_setup_logical_decoding(Oid relid) +{ + Relation rel; + Oid toastrelid; + LogicalDecodingContext *ctx; + NameData slotname; + RepackDecodingState *dstate; + MemoryContext oldcxt; + + /* + * REPACK CONCURRENTLY is not allowed in a transaction block, so this + * should never fire. + */ + Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny())); + + /* + * Make sure we can use logical decoding. + */ + CheckLogicalDecodingRequirements(true); + + /* + * A single backend should not execute multiple REPACK commands at a time, + * so use PID to make the slot unique. + * + * RS_TEMPORARY so that the slot gets cleaned up on ERROR. + */ + snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid); + ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, true, + false, false); + + EnsureLogicalDecodingEnabled(); + + /* + * Neither prepare_write nor do_write callback nor update_progress is + * useful for us. + */ + ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, + NIL, + true, + true, + InvalidXLogRecPtr, + XL_ROUTINE(.page_read = read_local_xlog_page, + .segment_open = wal_segment_open, + .segment_close = wal_segment_close), + NULL, NULL, NULL); + + /* + * We don't have control on setting fast_forward, so at least check it. + */ + Assert(!ctx->fast_forward); + + /* Avoid logical decoding of other relations. */ + rel = table_open(relid, AccessShareLock); + repacked_rel_locator = rel->rd_locator; + toastrelid = rel->rd_rel->reltoastrelid; + if (OidIsValid(toastrelid)) + { + Relation toastrel; + + /* Avoid logical decoding of other TOAST relations. */ + toastrel = table_open(toastrelid, AccessShareLock); + repacked_rel_toast_locator = toastrel->rd_locator; + table_close(toastrel, AccessShareLock); + } + table_close(rel, AccessShareLock); + + DecodingContextFindStartpoint(ctx); + + /* + * decode_concurrent_changes() needs non-blocking callback. + */ + ctx->reader->routine.page_read = read_local_xlog_page_no_wait; + + /* Some WAL records should have been read. */ + Assert(XLogRecPtrIsValid(ctx->reader->EndRecPtr)); + + /* + * Initialize repack_current_segment so that we can notice WAL segment + * boundaries. + */ + XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment, + wal_segment_size); + + /* Our private state belongs to the decoding context. */ + oldcxt = MemoryContextSwitchTo(ctx->context); + + /* + * read_local_xlog_page_no_wait() needs to be able to indicate the end of + * WAL. + */ + ctx->reader->private_data = palloc0_object(ReadLocalXLogPageNoWaitPrivate); + dstate = palloc0_object(RepackDecodingState); + MemoryContextSwitchTo(oldcxt); + +#ifdef USE_ASSERT_CHECKING + dstate->relid = relid; +#endif + + dstate->change_cxt = AllocSetContextCreate(ctx->context, + "REPACK - change", + ALLOCSET_DEFAULT_SIZES); + + /* The file will be set as soon as we have it opened. */ + dstate->file = NULL; + + /* + * Memory context and resource owner for long-lived resources. + */ + dstate->worker_cxt = CurrentMemoryContext; + dstate->worker_resowner = CurrentResourceOwner; + + ctx->output_writer_private = dstate; + + return ctx; +} + +static void +repack_cleanup_logical_decoding(LogicalDecodingContext *ctx) +{ + RepackDecodingState *dstate; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + if (dstate->slot) + ExecDropSingleTupleTableSlot(dstate->slot); + + FreeDecodingContext(ctx); + ReplicationSlotDropAcquired(); +} + +/* + * Make snapshot available to the backend that launched the decoding worker. + */ +static void +export_initial_snapshot(Snapshot snapshot, DecodingWorkerShared *shared) +{ + char fname[MAXPGPATH]; + BufFile *file; + Size snap_size; + char *snap_space; + + snap_size = EstimateSnapshotSpace(snapshot); + snap_space = (char *) palloc(snap_size); + SerializeSnapshot(snapshot, snap_space); + + DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1); + file = BufFileCreateFileSet(&shared->sfs.fs, fname); + /* To make restoration easier, write the snapshot size first. */ + BufFileWrite(file, &snap_size, sizeof(snap_size)); + BufFileWrite(file, snap_space, snap_size); + BufFileClose(file); + pfree(snap_space); + + /* Increase the counter to tell the backend that the file is available. */ + SpinLockAcquire(&shared->mutex); + shared->last_exported++; + SpinLockRelease(&shared->mutex); + ConditionVariableSignal(&shared->cv); +} + +/* + * Decode logical changes from the WAL sequence and store them to a file. + * + * If true is returned, there is no more work for the worker. + */ +static bool +decode_concurrent_changes(LogicalDecodingContext *ctx, + DecodingWorkerShared *shared) +{ + RepackDecodingState *dstate; + XLogRecPtr lsn_upto; + bool done; + char fname[MAXPGPATH]; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + + /* Open the output file. */ + DecodingWorkerFileName(fname, shared->relid, shared->last_exported + 1); + dstate->file = BufFileCreateFileSet(&shared->sfs.fs, fname); + + SpinLockAcquire(&shared->mutex); + lsn_upto = shared->lsn_upto; + done = shared->done; + SpinLockRelease(&shared->mutex); + + while (true) + { + XLogRecord *record; + XLogSegNo segno_new; + char *errm = NULL; + XLogRecPtr end_lsn; + + CHECK_FOR_INTERRUPTS(); + + record = XLogReadRecord(ctx->reader, &errm); + if (record) + { + LogicalDecodingProcessRecord(ctx, ctx->reader); + + /* + * If WAL segment boundary has been crossed, inform the decoding + * system that the catalog_xmin can advance. + */ + end_lsn = ctx->reader->EndRecPtr; + XLByteToSeg(end_lsn, segno_new, wal_segment_size); + if (segno_new != repack_current_segment) + { + LogicalConfirmReceivedLocation(end_lsn); + elog(DEBUG1, "REPACK: confirmed receive location %X/%X", + (uint32) (end_lsn >> 32), (uint32) end_lsn); + repack_current_segment = segno_new; + } + } + else + { + ReadLocalXLogPageNoWaitPrivate *priv; + + if (errm) + ereport(ERROR, + errmsg("%s", errm)); + + /* + * In the decoding loop we do not want to get blocked when there + * is no more WAL available, otherwise the loop would become + * uninterruptible. + */ + priv = (ReadLocalXLogPageNoWaitPrivate *) ctx->reader->private_data; + if (priv->end_of_wal) + /* Do not miss the end of WAL condition next time. */ + priv->end_of_wal = false; + else + ereport(ERROR, + errmsg("could not read WAL record")); + } + + /* + * Whether we could read new record or not, keep checking if + * 'lsn_upto' was specified. + */ + if (!XLogRecPtrIsValid(lsn_upto)) + { + SpinLockAcquire(&shared->mutex); + lsn_upto = shared->lsn_upto; + /* 'done' should be set at the same time as 'lsn_upto' */ + done = shared->done; + SpinLockRelease(&shared->mutex); + } + if (XLogRecPtrIsValid(lsn_upto) && + ctx->reader->EndRecPtr >= lsn_upto) + break; + + if (record == NULL) + { + int64 timeout = 0; + WaitLSNResult res; + + /* + * Before we retry reading, wait until new WAL is flushed. + * + * There is a race condition such that the backend executing + * REPACK determines 'lsn_upto', but before it sets the shared + * variable, we reach the end of WAL. In that case we'd need to + * wait until the next WAL flush (unrelated to REPACK). Although + * that should not be a problem in a busy system, it might be + * noticeable in other cases, including regression tests (which + * are not necessarily executed in parallel). Therefore it makes + * sense to use timeout. + * + * If lsn_upto is valid, WAL records having LSN lower than that + * should already have been flushed to disk. + */ + if (!XLogRecPtrIsValid(lsn_upto)) + timeout = 100L; + res = WaitForLSN(WAIT_LSN_TYPE_PRIMARY_FLUSH, + ctx->reader->EndRecPtr + 1, + timeout); + if (res != WAIT_LSN_RESULT_SUCCESS && + res != WAIT_LSN_RESULT_TIMEOUT) + ereport(ERROR, + errmsg("waiting for WAL failed")); + } + } + + /* + * Close the file so we can make it available to the backend. + */ + BufFileClose(dstate->file); + dstate->file = NULL; + SpinLockAcquire(&shared->mutex); + shared->lsn_upto = InvalidXLogRecPtr; + shared->last_exported++; + SpinLockRelease(&shared->mutex); + ConditionVariableSignal(&shared->cv); + + return done; +} + +/* + * Does the WAL record contain a data change that this backend does not need + * to decode on behalf of REPACK (CONCURRENTLY)? + */ +bool +change_useless_for_repack(XLogRecordBuffer *buf) +{ + XLogReaderState *r = buf->record; + RelFileLocator locator; + + /* TOAST locator should not be set unless the main is. */ + Assert(!OidIsValid(repacked_rel_toast_locator.relNumber) || + OidIsValid(repacked_rel_locator.relNumber)); + + /* + * Backends not involved in REPACK (CONCURRENTLY) should not do the + * filtering. + */ + if (!OidIsValid(repacked_rel_locator.relNumber)) + return false; + + /* + * If the record does not contain the block 0, it's probably not INSERT / + * UPDATE / DELETE. In any case, we do not have enough information to + * filter the change out. + */ + if (!XLogRecGetBlockTagExtended(r, 0, &locator, NULL, NULL, NULL)) + return false; + + /* + * Decode the change if it belongs to the table we are repacking, or if it + * belongs to its TOAST relation. + */ + if (RelFileLocatorEquals(locator, repacked_rel_locator)) + return false; + if (OidIsValid(repacked_rel_toast_locator.relNumber) && + RelFileLocatorEquals(locator, repacked_rel_toast_locator)) + return false; + + /* Filter out changes of other tables. */ + return true; +} diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c index 546160f09410e..bfaa4743cd8ca 100644 --- a/src/backend/commands/schemacmds.c +++ b/src/backend/commands/schemacmds.c @@ -3,7 +3,7 @@ * schemacmds.c * schema creation/manipulation commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,7 +25,6 @@ #include "catalog/pg_authid.h" #include "catalog/pg_database.h" #include "catalog/pg_namespace.h" -#include "commands/dbcommands.h" #include "commands/event_trigger.h" #include "commands/schemacmds.h" #include "miscadmin.h" @@ -34,6 +33,7 @@ #include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -49,7 +49,7 @@ static void AlterSchemaOwner_internal(HeapTuple tup, Relation rel, Oid newOwnerI * a subquery. */ Oid -CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, +CreateSchemaCommand(ParseState *pstate, CreateSchemaStmt *stmt, int stmt_location, int stmt_len) { const char *schemaName = stmt->schemaname; @@ -189,12 +189,12 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, /* * Examine the list of commands embedded in the CREATE SCHEMA command, and - * reorganize them into a sequentially executable order with no forward - * references. Note that the result is still a list of raw parsetrees --- - * we cannot, in general, run parse analysis on one statement until we - * have actually executed the prior ones. + * do preliminary transformations. Note that the result is still a list + * of raw parsetrees --- we cannot, in general, run parse analysis on one + * statement until we have actually executed the prior ones. */ - parsetree_list = transformCreateSchemaStmtElements(stmt->schemaElts, + parsetree_list = transformCreateSchemaStmtElements(pstate, + stmt->schemaElts, schemaName); /* @@ -215,10 +215,11 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString, wrapper->utilityStmt = stmt; wrapper->stmt_location = stmt_location; wrapper->stmt_len = stmt_len; + wrapper->planOrigin = PLAN_STMT_INTERNAL; /* do this step */ ProcessUtility(wrapper, - queryString, + pstate->p_sourcetext, false, PROCESS_UTILITY_SUBCOMMAND, NULL, diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index cee5d7bbb9c7e..77542d04200af 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -3,7 +3,7 @@ * seclabel.c * routines to support security label feature. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * ------------------------------------------------------------------------- @@ -80,6 +80,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_OPFAMILY: case OBJECT_PARAMETER_ACL: case OBJECT_POLICY: + case OBJECT_PROPGRAPH: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_RULE: @@ -118,6 +119,7 @@ ExecSecLabelStmt(SecLabelStmt *stmt) ObjectAddress address; Relation relation; ListCell *lc; + bool missing_ok; /* * Find the named label provider, or if none specified, check whether @@ -159,6 +161,14 @@ ExecSecLabelStmt(SecLabelStmt *stmt) (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("security labels are not supported for this type of object"))); + /* + * During binary upgrade, allow nonexistent large objects so that we don't + * have to create them during schema restoration. pg_upgrade will + * transfer the contents of pg_largeobject_metadata via COPY or by + * copying/linking its files from the old cluster later on. + */ + missing_ok = IsBinaryUpgrade && stmt->objtype == OBJECT_LARGEOBJECT; + /* * Translate the parser representation which identifies this object into * an ObjectAddress. get_object_address() will throw an error if the @@ -166,7 +176,8 @@ ExecSecLabelStmt(SecLabelStmt *stmt) * guard against concurrent modifications. */ address = get_object_address(stmt->objtype, stmt->object, - &relation, ShareUpdateExclusiveLock, false); + &relation, ShareUpdateExclusiveLock, + missing_ok); /* Require ownership of the target object. */ check_object_ownership(GetUserId(), stmt->objtype, address, @@ -573,7 +584,7 @@ register_label_provider(const char *provider_name, check_object_relabel_type hoo MemoryContext oldcxt; oldcxt = MemoryContextSwitchTo(TopMemoryContext); - provider = palloc(sizeof(LabelProvider)); + provider = palloc_object(LabelProvider); provider->provider_name = pstrdup(provider_name); provider->hook = hook; label_provider_list = lappend(label_provider_list, provider); diff --git a/src/backend/commands/sequence.c b/src/backend/commands/sequence.c index 451ae6f7f6940..551667650ba63 100644 --- a/src/backend/commands/sequence.c +++ b/src/backend/commands/sequence.c @@ -3,7 +3,7 @@ * sequence.c * PostgreSQL sequences support code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,7 +14,6 @@ */ #include "postgres.h" -#include "access/bufmask.h" #include "access/htup_details.h" #include "access/multixact.h" #include "access/relation.h" @@ -22,9 +21,7 @@ #include "access/table.h" #include "access/transam.h" #include "access/xact.h" -#include "access/xlog.h" #include "access/xloginsert.h" -#include "access/xlogutils.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/namespace.h" @@ -34,17 +31,20 @@ #include "catalog/storage_xlog.h" #include "commands/defrem.h" #include "commands/sequence.h" +#include "commands/sequence_xlog.h" #include "commands/tablecmds.h" #include "funcapi.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "parser/parse_type.h" +#include "storage/bufmgr.h" #include "storage/lmgr.h" #include "storage/proc.h" #include "storage/smgr.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/lsyscache.h" +#include "utils/pg_lsn.h" #include "utils/resowner.h" #include "utils/syscache.h" #include "utils/varlena.h" @@ -57,16 +57,6 @@ */ #define SEQ_LOG_VALS 32 -/* - * The "special area" of a sequence's buffer page looks like this. - */ -#define SEQ_MAGIC 0x1717 - -typedef struct sequence_magic -{ - uint32 magic; -} sequence_magic; - /* * We store a SeqTable item for every sequence we have touched in the current * session. This is needed to hold onto nextval/currval state. (We can't @@ -106,10 +96,11 @@ static Form_pg_sequence_data read_seq_tuple(Relation rel, static void init_params(ParseState *pstate, List *options, bool for_identity, bool isInit, Form_pg_sequence seqform, - Form_pg_sequence_data seqdataform, + int64 *last_value, + bool *reset_state, + bool *is_called, bool *need_seq_rewrite, List **owned_by); -static void do_setval(Oid relid, int64 next, bool iscalled); static void process_owned_by(Relation seqrel, List *owned_by, bool for_identity); @@ -121,7 +112,9 @@ ObjectAddress DefineSequence(ParseState *pstate, CreateSeqStmt *seq) { FormData_pg_sequence seqform; - FormData_pg_sequence_data seqdataform; + int64 last_value; + bool reset_state; + bool is_called; bool need_seq_rewrite; List *owned_by; CreateStmt *stmt = makeNode(CreateStmt); @@ -164,7 +157,7 @@ DefineSequence(ParseState *pstate, CreateSeqStmt *seq) /* Check and set all option values */ init_params(pstate, seq->options, seq->for_identity, true, - &seqform, &seqdataform, + &seqform, &last_value, &reset_state, &is_called, &need_seq_rewrite, &owned_by); /* @@ -179,7 +172,7 @@ DefineSequence(ParseState *pstate, CreateSeqStmt *seq) { case SEQ_COL_LASTVAL: coldef = makeColumnDef("last_value", INT8OID, -1, InvalidOid); - value[i - 1] = Int64GetDatumFast(seqdataform.last_value); + value[i - 1] = Int64GetDatumFast(last_value); break; case SEQ_COL_LOG: coldef = makeColumnDef("log_cnt", INT8OID, -1, InvalidOid); @@ -399,8 +392,7 @@ fill_seq_fork_with_data(Relation rel, HeapTuple tuple, ForkNumber forkNum) MarkBufferDirty(buf); - offnum = PageAddItem(page, (Item) tuple->t_data, tuple->t_len, - InvalidOffsetNumber, false, false); + offnum = PageAddItem(page, tuple->t_data, tuple->t_len, InvalidOffsetNumber, false, false); if (offnum != FirstOffsetNumber) elog(ERROR, "failed to add sequence tuple to page"); @@ -448,6 +440,9 @@ AlterSequence(ParseState *pstate, AlterSeqStmt *stmt) ObjectAddress address; Relation rel; HeapTuple seqtuple; + bool reset_state = false; + bool is_called; + int64 last_value; HeapTuple newdatatuple; /* Open and lock sequence, and check for ownership along the way. */ @@ -481,12 +476,14 @@ AlterSequence(ParseState *pstate, AlterSeqStmt *stmt) /* copy the existing sequence data tuple, so it can be modified locally */ newdatatuple = heap_copytuple(&datatuple); newdataform = (Form_pg_sequence_data) GETSTRUCT(newdatatuple); + last_value = newdataform->last_value; + is_called = newdataform->is_called; UnlockReleaseBuffer(buf); /* Check and set new values */ init_params(pstate, stmt->options, stmt->for_identity, false, - seqform, newdataform, + seqform, &last_value, &reset_state, &is_called, &need_seq_rewrite, &owned_by); /* If needed, rewrite the sequence relation itself */ @@ -513,6 +510,10 @@ AlterSequence(ParseState *pstate, AlterSeqStmt *stmt) /* * Insert the modified tuple into the new storage file. */ + newdataform->last_value = last_value; + newdataform->is_called = is_called; + if (reset_state) + newdataform->log_cnt = 0; fill_seq_with_data(seqrel, newdatatuple); } @@ -941,8 +942,8 @@ lastval(PG_FUNCTION_ARGS) * it is the only way to clear the is_called flag in an existing * sequence. */ -static void -do_setval(Oid relid, int64 next, bool iscalled) +void +SetSequence(Oid relid, int64 next, bool iscalled) { SeqTable elm; Relation seqrel; @@ -1043,7 +1044,7 @@ do_setval(Oid relid, int64 next, bool iscalled) /* * Implement the 2 arg setval procedure. - * See do_setval for discussion. + * See SetSequence for discussion. */ Datum setval_oid(PG_FUNCTION_ARGS) @@ -1051,14 +1052,14 @@ setval_oid(PG_FUNCTION_ARGS) Oid relid = PG_GETARG_OID(0); int64 next = PG_GETARG_INT64(1); - do_setval(relid, next, true); + SetSequence(relid, next, true); PG_RETURN_INT64(next); } /* * Implement the 3 arg setval procedure. - * See do_setval for discussion. + * See SetSequence for discussion. */ Datum setval3_oid(PG_FUNCTION_ARGS) @@ -1067,7 +1068,7 @@ setval3_oid(PG_FUNCTION_ARGS) int64 next = PG_GETARG_INT64(1); bool iscalled = PG_GETARG_BOOL(2); - do_setval(relid, next, iscalled); + SetSequence(relid, next, iscalled); PG_RETURN_INT64(next); } @@ -1236,17 +1237,19 @@ read_seq_tuple(Relation rel, Buffer *buf, HeapTuple seqdatatuple) /* * init_params: process the options list of CREATE or ALTER SEQUENCE, and * store the values into appropriate fields of seqform, for changes that go - * into the pg_sequence catalog, and fields of seqdataform for changes to the - * sequence relation itself. Set *need_seq_rewrite to true if we changed any - * parameters that require rewriting the sequence's relation (interesting for - * ALTER SEQUENCE). Also set *owned_by to any OWNED BY option, or to NIL if - * there is none. + * into the pg_sequence catalog, and fields for changes to the sequence + * relation itself (*is_called, *last_value and *reset_state). Set + * *need_seq_rewrite to true if we changed any parameters that require + * rewriting the sequence's relation (interesting for ALTER SEQUENCE). Also + * set *owned_by to any OWNED BY option, or to NIL if there is none. Set + * *reset_state to true if the internal state of the sequence needs to be + * reset, affecting future nextval() calls, for example with WAL logging. * * If isInit is true, fill any unspecified options with default values; * otherwise, do not change existing options that aren't explicitly overridden. * * Note: we force a sequence rewrite whenever we change parameters that affect - * generation of future sequence values, even if the seqdataform per se is not + * generation of future sequence values, even if the metadata per se is not * changed. This allows ALTER SEQUENCE to behave transactionally. Currently, * the only option that doesn't cause that is OWNED BY. It's *necessary* for * ALTER SEQUENCE OWNED BY to not rewrite the sequence, because that would @@ -1257,7 +1260,9 @@ static void init_params(ParseState *pstate, List *options, bool for_identity, bool isInit, Form_pg_sequence seqform, - Form_pg_sequence_data seqdataform, + int64 *last_value, + bool *reset_state, + bool *is_called, bool *need_seq_rewrite, List **owned_by) { @@ -1363,11 +1368,11 @@ init_params(ParseState *pstate, List *options, bool for_identity, } /* - * We must reset log_cnt when isInit or when changing any parameters that - * would affect future nextval allocations. + * We must reset the state of the sequence when isInit or when changing + * any parameters that would affect future nextval allocations. */ if (isInit) - seqdataform->log_cnt = 0; + *reset_state = true; /* AS type */ if (as_type != NULL) @@ -1416,7 +1421,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("INCREMENT must not be zero"))); - seqdataform->log_cnt = 0; + *reset_state = true; } else if (isInit) { @@ -1428,7 +1433,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, { seqform->seqcycle = boolVal(is_cycled->arg); Assert(BoolIsValid(seqform->seqcycle)); - seqdataform->log_cnt = 0; + *reset_state = true; } else if (isInit) { @@ -1439,7 +1444,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, if (max_value != NULL && max_value->arg) { seqform->seqmax = defGetInt64(max_value); - seqdataform->log_cnt = 0; + *reset_state = true; } else if (isInit || max_value != NULL || reset_max_value) { @@ -1455,7 +1460,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, } else seqform->seqmax = -1; /* descending seq */ - seqdataform->log_cnt = 0; + *reset_state = true; } /* Validate maximum value. No need to check INT8 as seqmax is an int64 */ @@ -1471,7 +1476,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, if (min_value != NULL && min_value->arg) { seqform->seqmin = defGetInt64(min_value); - seqdataform->log_cnt = 0; + *reset_state = true; } else if (isInit || min_value != NULL || reset_min_value) { @@ -1487,7 +1492,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, } else seqform->seqmin = 1; /* ascending seq */ - seqdataform->log_cnt = 0; + *reset_state = true; } /* Validate minimum value. No need to check INT8 as seqmin is an int64 */ @@ -1538,30 +1543,30 @@ init_params(ParseState *pstate, List *options, bool for_identity, if (restart_value != NULL) { if (restart_value->arg != NULL) - seqdataform->last_value = defGetInt64(restart_value); + *last_value = defGetInt64(restart_value); else - seqdataform->last_value = seqform->seqstart; - seqdataform->is_called = false; - seqdataform->log_cnt = 0; + *last_value = seqform->seqstart; + *is_called = false; + *reset_state = true; } else if (isInit) { - seqdataform->last_value = seqform->seqstart; - seqdataform->is_called = false; + *last_value = seqform->seqstart; + *is_called = false; } /* crosscheck RESTART (or current value, if changing MIN/MAX) */ - if (seqdataform->last_value < seqform->seqmin) + if (*last_value < seqform->seqmin) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("RESTART value (%" PRId64 ") cannot be less than MINVALUE (%" PRId64 ")", - seqdataform->last_value, + *last_value, seqform->seqmin))); - if (seqdataform->last_value > seqform->seqmax) + if (*last_value > seqform->seqmax) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("RESTART value (%" PRId64 ") cannot be greater than MAXVALUE (%" PRId64 ")", - seqdataform->last_value, + *last_value, seqform->seqmax))); /* CACHE */ @@ -1573,7 +1578,7 @@ init_params(ParseState *pstate, List *options, bool for_identity, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("CACHE (%" PRId64 ") must be greater than zero", seqform->seqcache))); - seqdataform->log_cnt = 0; + *reset_state = true; } else if (isInit) { @@ -1778,17 +1783,17 @@ pg_sequence_parameters(PG_FUNCTION_ARGS) /* - * Return the sequence tuple. + * Return the sequence tuple along with its page LSN. * - * This is primarily intended for use by pg_dump to gather sequence data - * without needing to individually query each sequence relation. + * This is primarily used by pg_dump to efficiently collect sequence data + * without querying each sequence individually, and is also leveraged by + * logical replication while synchronizing sequences. */ Datum pg_get_sequence_data(PG_FUNCTION_ARGS) { -#define PG_GET_SEQUENCE_DATA_COLS 2 +#define PG_GET_SEQUENCE_DATA_COLS 3 Oid relid = PG_GETARG_OID(0); - SeqTable elm; Relation seqrel; Datum values[PG_GET_SEQUENCE_DATA_COLS] = {0}; bool isnull[PG_GET_SEQUENCE_DATA_COLS] = {0}; @@ -1801,33 +1806,42 @@ pg_get_sequence_data(PG_FUNCTION_ARGS) INT8OID, -1, 0); TupleDescInitEntry(resultTupleDesc, (AttrNumber) 2, "is_called", BOOLOID, -1, 0); + TupleDescInitEntry(resultTupleDesc, (AttrNumber) 3, "page_lsn", + LSNOID, -1, 0); + TupleDescFinalize(resultTupleDesc); resultTupleDesc = BlessTupleDesc(resultTupleDesc); - init_sequence(relid, &elm, &seqrel); + seqrel = try_relation_open(relid, AccessShareLock); /* - * Return all NULLs for sequences for which we lack privileges, other - * sessions' temporary sequences, and unlogged sequences on standbys. + * Return all NULLs for missing sequences, sequences for which we lack + * privileges, other sessions' temporary sequences, and unlogged sequences + * on standbys. */ - if (pg_class_aclcheck(relid, GetUserId(), ACL_SELECT) == ACLCHECK_OK && + if (seqrel && seqrel->rd_rel->relkind == RELKIND_SEQUENCE && + pg_class_aclcheck(relid, GetUserId(), ACL_SELECT) == ACLCHECK_OK && !RELATION_IS_OTHER_TEMP(seqrel) && (RelationIsPermanent(seqrel) || !RecoveryInProgress())) { Buffer buf; HeapTupleData seqtuple; Form_pg_sequence_data seq; + Page page; seq = read_seq_tuple(seqrel, &buf, &seqtuple); + page = BufferGetPage(buf); values[0] = Int64GetDatum(seq->last_value); values[1] = BoolGetDatum(seq->is_called); + values[2] = LSNGetDatum(PageGetLSN(page)); UnlockReleaseBuffer(buf); } else memset(isnull, true, sizeof(isnull)); - sequence_close(seqrel, NoLock); + if (seqrel) + relation_close(seqrel, AccessShareLock); resultHeapTuple = heap_form_tuple(resultTupleDesc, values, isnull); result = HeapTupleGetDatum(resultHeapTuple); @@ -1885,57 +1899,6 @@ pg_sequence_last_value(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } - -void -seq_redo(XLogReaderState *record) -{ - XLogRecPtr lsn = record->EndRecPtr; - uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; - Buffer buffer; - Page page; - Page localpage; - char *item; - Size itemsz; - xl_seq_rec *xlrec = (xl_seq_rec *) XLogRecGetData(record); - sequence_magic *sm; - - if (info != XLOG_SEQ_LOG) - elog(PANIC, "seq_redo: unknown op code %u", info); - - buffer = XLogInitBufferForRedo(record, 0); - page = (Page) BufferGetPage(buffer); - - /* - * We always reinit the page. However, since this WAL record type is also - * used for updating sequences, it's possible that a hot-standby backend - * is examining the page concurrently; so we mustn't transiently trash the - * buffer. The solution is to build the correct new page contents in - * local workspace and then memcpy into the buffer. Then only bytes that - * are supposed to change will change, even transiently. We must palloc - * the local page for alignment reasons. - */ - localpage = (Page) palloc(BufferGetPageSize(buffer)); - - PageInit(localpage, BufferGetPageSize(buffer), sizeof(sequence_magic)); - sm = (sequence_magic *) PageGetSpecialPointer(localpage); - sm->magic = SEQ_MAGIC; - - item = (char *) xlrec + sizeof(xl_seq_rec); - itemsz = XLogRecGetDataLen(record) - sizeof(xl_seq_rec); - - if (PageAddItem(localpage, (Item) item, itemsz, - FirstOffsetNumber, false, false) == InvalidOffsetNumber) - elog(PANIC, "seq_redo: failed to add item to page"); - - PageSetLSN(localpage, lsn); - - memcpy(page, localpage, BufferGetPageSize(buffer)); - MarkBufferDirty(buffer); - UnlockReleaseBuffer(buffer); - - pfree(localpage); -} - /* * Flush cached sequence information. */ @@ -1950,14 +1913,3 @@ ResetSequenceCaches(void) last_used_seq = NULL; } - -/* - * Mask a Sequence page before performing consistency checks on it. - */ -void -seq_mask(char *page, BlockNumber blkno) -{ - mask_page_lsn_and_checksum(page); - - mask_unused_space(page); -} diff --git a/src/backend/commands/sequence_xlog.c b/src/backend/commands/sequence_xlog.c new file mode 100644 index 0000000000000..d0aed48e26801 --- /dev/null +++ b/src/backend/commands/sequence_xlog.c @@ -0,0 +1,80 @@ +/*------------------------------------------------------------------------- + * + * sequence.c + * RMGR WAL routines for sequences. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/sequence_xlog.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/bufmask.h" +#include "access/xlogutils.h" +#include "commands/sequence_xlog.h" +#include "storage/bufmgr.h" + +void +seq_redo(XLogReaderState *record) +{ + XLogRecPtr lsn = record->EndRecPtr; + uint8 info = XLogRecGetInfo(record) & ~XLR_INFO_MASK; + Buffer buffer; + Page page; + Page localpage; + char *item; + Size itemsz; + xl_seq_rec *xlrec = (xl_seq_rec *) XLogRecGetData(record); + sequence_magic *sm; + + if (info != XLOG_SEQ_LOG) + elog(PANIC, "seq_redo: unknown op code %u", info); + + buffer = XLogInitBufferForRedo(record, 0); + page = BufferGetPage(buffer); + + /* + * We always reinit the page. However, since this WAL record type is also + * used for updating sequences, it's possible that a hot-standby backend + * is examining the page concurrently; so we mustn't transiently trash the + * buffer. The solution is to build the correct new page contents in + * local workspace and then memcpy into the buffer. Then only bytes that + * are supposed to change will change, even transiently. We must palloc + * the local page for alignment reasons. + */ + localpage = (Page) palloc(BufferGetPageSize(buffer)); + + PageInit(localpage, BufferGetPageSize(buffer), sizeof(sequence_magic)); + sm = (sequence_magic *) PageGetSpecialPointer(localpage); + sm->magic = SEQ_MAGIC; + + item = (char *) xlrec + sizeof(xl_seq_rec); + itemsz = XLogRecGetDataLen(record) - sizeof(xl_seq_rec); + + if (PageAddItem(localpage, item, itemsz, FirstOffsetNumber, false, false) == InvalidOffsetNumber) + elog(PANIC, "seq_redo: failed to add item to page"); + + PageSetLSN(localpage, lsn); + + memcpy(page, localpage, BufferGetPageSize(buffer)); + MarkBufferDirty(buffer); + UnlockReleaseBuffer(buffer); + + pfree(localpage); +} + +/* + * Mask a Sequence page before performing consistency checks on it. + */ +void +seq_mask(char *page, BlockNumber blkno) +{ + mask_page_lsn_and_checksum(page); + + mask_unused_space(page); +} diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index e24d540cd45ba..b354723be4435 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -3,7 +3,7 @@ * statscmds.c * Commands for creating and altering extended statistics objects * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "access/relation.h" #include "access/table.h" #include "catalog/catalog.h" @@ -27,6 +28,7 @@ #include "commands/comment.h" #include "commands/defrem.h" #include "miscadmin.h" +#include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "optimizer/optimizer.h" #include "statistics/statistics.h" @@ -59,7 +61,7 @@ compare_int16(const void *a, const void *b) * CREATE STATISTICS */ ObjectAddress -CreateStatistics(CreateStatsStmt *stmt) +CreateStatistics(CreateStatsStmt *stmt, bool check_rights) { int16 attnums[STATS_MAX_DIMENSIONS]; int nattnums = 0; @@ -134,7 +136,13 @@ CreateStatistics(CreateStatsStmt *stmt) RelationGetRelationName(rel)), errdetail_relkind_not_supported(rel->rd_rel->relkind))); - /* You must own the relation to create stats on it */ + /* + * You must own the relation to create stats on it. + * + * NB: Concurrent changes could cause this function's lookup to find a + * different relation than a previous lookup by the caller, so we must + * perform this check even when check_rights == false. + */ if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), stxowner)) aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind), RelationGetRelationName(rel)); @@ -169,6 +177,21 @@ CreateStatistics(CreateStatsStmt *stmt) } namestrcpy(&stxname, namestr); + /* + * Check we have creation rights in target namespace. Skip check if + * caller doesn't want it. + */ + if (check_rights) + { + AclResult aclresult; + + aclresult = object_aclcheck(NamespaceRelationId, namespaceId, + GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(namespaceId)); + } + /* * Deal with the possibility that the statistics object already exists. */ @@ -210,7 +233,8 @@ CreateStatistics(CreateStatsStmt *stmt) * Convert the expression list to a simple array of attnums, but also keep * a list of more complex expressions. While at it, enforce some * constraints - we don't allow extended statistics on system attributes, - * and we require the data type to have a less-than operator. + * and we require the data type to have a less-than operator, if we're + * building multivariate statistics. * * There are many ways to "mask" a simple attribute reference as an * expression, for example "(a+0)" etc. We can't possibly detect all of @@ -246,22 +270,40 @@ CreateStatistics(CreateStatsStmt *stmt) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("statistics creation on system columns is not supported"))); - /* Disallow use of virtual generated columns in extended stats */ - if (attForm->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("statistics creation on virtual generated columns is not supported"))); - - /* Disallow data types without a less-than operator */ - type = lookup_type_cache(attForm->atttypid, TYPECACHE_LT_OPR); - if (type->lt_opr == InvalidOid) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("column \"%s\" cannot be used in statistics because its type %s has no default btree operator class", - attname, format_type_be(attForm->atttypid)))); + /* + * Disallow data types without a less-than operator in + * multivariate statistics. + */ + if (numcols > 1) + { + type = lookup_type_cache(attForm->atttypid, TYPECACHE_LT_OPR); + if (type->lt_opr == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot create multivariate statistics on column \"%s\"", + attname), + errdetail("The type %s has no default btree operator class.", + format_type_be(attForm->atttypid)))); + } - attnums[nattnums] = attForm->attnum; - nattnums++; + /* Treat virtual generated columns as expressions */ + if (attForm->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + { + Node *expr; + + expr = (Node *) makeVar(1, + attForm->attnum, + attForm->atttypid, + attForm->atttypmod, + attForm->attcollation, + 0); + stxexprs = lappend(stxexprs, expr); + } + else + { + attnums[nattnums] = attForm->attnum; + nattnums++; + } ReleaseSysCache(atttuple); } else if (IsA(selem->expr, Var)) /* column reference in parens */ @@ -275,22 +317,32 @@ CreateStatistics(CreateStatsStmt *stmt) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("statistics creation on system columns is not supported"))); - /* Disallow use of virtual generated columns in extended stats */ - if (get_attgenerated(relid, var->varattno) == ATTRIBUTE_GENERATED_VIRTUAL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("statistics creation on virtual generated columns is not supported"))); - - /* Disallow data types without a less-than operator */ - type = lookup_type_cache(var->vartype, TYPECACHE_LT_OPR); - if (type->lt_opr == InvalidOid) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("column \"%s\" cannot be used in statistics because its type %s has no default btree operator class", - get_attname(relid, var->varattno, false), format_type_be(var->vartype)))); + /* + * Disallow data types without a less-than operator in + * multivariate statistics. + */ + if (numcols > 1) + { + type = lookup_type_cache(var->vartype, TYPECACHE_LT_OPR); + if (type->lt_opr == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot create multivariate statistics on column \"%s\"", + get_attname(relid, var->varattno, false)), + errdetail("The type %s has no default btree operator class.", + format_type_be(var->vartype)))); + } - attnums[nattnums] = var->varattno; - nattnums++; + /* Treat virtual generated columns as expressions */ + if (get_attgenerated(relid, var->varattno) == ATTRIBUTE_GENERATED_VIRTUAL) + { + stxexprs = lappend(stxexprs, (Node *) var); + } + else + { + attnums[nattnums] = var->varattno; + nattnums++; + } } else /* expression */ { @@ -314,30 +366,22 @@ CreateStatistics(CreateStatsStmt *stmt) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("statistics creation on system columns is not supported"))); - - /* Disallow use of virtual generated columns in extended stats */ - if (get_attgenerated(relid, attnum) == ATTRIBUTE_GENERATED_VIRTUAL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("statistics creation on virtual generated columns is not supported"))); } /* - * Disallow data types without a less-than operator. - * - * We ignore this for statistics on a single expression, in which - * case we'll build the regular statistics only (and that code can - * deal with such data types). + * Disallow data types without a less-than operator in + * multivariate statistics. */ - if (list_length(stmt->exprs) > 1) + if (numcols > 1) { atttype = exprType(expr); type = lookup_type_cache(atttype, TYPECACHE_LT_OPR); if (type->lt_opr == InvalidOid) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("expression cannot be used in multivariate statistics because its type %s has no default btree operator class", - format_type_be(atttype)))); + errmsg("cannot create multivariate statistics on this expression"), + errdetail("The type %s has no default btree operator class.", + format_type_be(atttype)))); } stxexprs = lappend(stxexprs, expr); @@ -345,22 +389,25 @@ CreateStatistics(CreateStatsStmt *stmt) } /* - * Parse the statistics kinds. - * - * First check that if this is the case with a single expression, there - * are no statistics kinds specified (we don't allow that for the simple - * CREATE STATISTICS form). + * Check that at least two columns were specified in the statement, or + * that we're building statistics on a single expression (or virtual + * generated column). */ - if ((list_length(stmt->exprs) == 1) && (list_length(stxexprs) == 1)) - { - /* statistics kinds not specified */ - if (stmt->stat_types != NIL) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("when building statistics on a single expression, statistics kinds may not be specified"))); - } + if (numcols < 2 && list_length(stxexprs) != 1) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot create extended statistics on a single non-virtual column"), + errdetail("Univariate statistics are already built for each individual non-virtual table column.")); + + /* + * Parse the statistics kinds (not allowed when building univariate + * statistics). + */ + if (numcols == 1 && stmt->stat_types != NIL) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot specify statistics kinds when building univariate statistics")); - /* OK, let's check that we recognize the statistics kinds. */ build_ndistinct = false; build_dependencies = false; build_mcv = false; @@ -408,15 +455,6 @@ CreateStatistics(CreateStatsStmt *stmt) */ build_expressions = (stxexprs != NIL); - /* - * Check that at least two columns were specified in the statement, or - * that we're building statistics on a single expression. - */ - if ((numcols < 2) && (list_length(stxexprs) != 1)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("extended statistics require at least 2 columns"))); - /* * Sort the attnums, which makes detecting duplicates somewhat easier, and * it does not hurt (it does not matter for the contents, unlike for diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 4aec73bcc6bbc..d512e87cfe361 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -3,7 +3,7 @@ * subscriptioncmds.c * subscription catalog manipulation functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -14,6 +14,7 @@ #include "postgres.h" +#include "access/commit_ts.h" #include "access/htup_details.h" #include "access/table.h" #include "access/twophase.h" @@ -26,14 +27,16 @@ #include "catalog/objectaddress.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_database_d.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" +#include "catalog/pg_user_mapping.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/subscriptioncmds.h" #include "executor/executor.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "pgstat.h" @@ -71,8 +74,11 @@ #define SUBOPT_PASSWORD_REQUIRED 0x00000800 #define SUBOPT_RUN_AS_OWNER 0x00001000 #define SUBOPT_FAILOVER 0x00002000 -#define SUBOPT_LSN 0x00004000 -#define SUBOPT_ORIGIN 0x00008000 +#define SUBOPT_RETAIN_DEAD_TUPLES 0x00004000 +#define SUBOPT_MAX_RETENTION_DURATION 0x00008000 +#define SUBOPT_WAL_RECEIVER_TIMEOUT 0x00010000 +#define SUBOPT_LSN 0x00020000 +#define SUBOPT_ORIGIN 0x00040000 /* check if the 'val' has 'bits' set */ #define IsSet(val, bits) (((val) & (bits)) == (bits)) @@ -83,7 +89,7 @@ */ typedef struct SubOpts { - bits32 specified_opts; + uint32 specified_opts; char *slot_name; char *synchronous_commit; bool connect; @@ -98,15 +104,37 @@ typedef struct SubOpts bool passwordrequired; bool runasowner; bool failover; + bool retaindeadtuples; + int32 maxretention; char *origin; XLogRecPtr lsn; + char *wal_receiver_timeout; } SubOpts; -static List *fetch_table_list(WalReceiverConn *wrconn, List *publications); -static void check_publications_origin(WalReceiverConn *wrconn, - List *publications, bool copydata, - char *origin, Oid *subrel_local_oids, - int subrel_count, char *subname); +/* + * PublicationRelKind represents a relation included in a publication. + * It stores the schema-qualified relation name (rv) and its kind (relkind). + */ +typedef struct PublicationRelKind +{ + RangeVar *rv; + char relkind; +} PublicationRelKind; + +static List *fetch_relation_list(WalReceiverConn *wrconn, List *publications); +static void check_publications_origin_tables(WalReceiverConn *wrconn, + List *publications, bool copydata, + bool retain_dead_tuples, + char *origin, + Oid *subrel_local_oids, + int subrel_count, char *subname); +static void check_publications_origin_sequences(WalReceiverConn *wrconn, + List *publications, + bool copydata, char *origin, + Oid *subrel_local_oids, + int subrel_count, + char *subname); +static void check_pub_dead_tuple_retention(WalReceiverConn *wrconn); static void check_duplicates_in_publist(List *publist, Datum *datums); static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *subname); static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err); @@ -122,7 +150,7 @@ static void CheckAlterSubOption(Subscription *sub, const char *option, */ static void parse_subscription_options(ParseState *pstate, List *stmt_options, - bits32 supported_opts, SubOpts *opts) + uint32 supported_opts, SubOpts *opts) { ListCell *lc; @@ -162,6 +190,10 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, opts->runasowner = false; if (IsSet(supported_opts, SUBOPT_FAILOVER)) opts->failover = false; + if (IsSet(supported_opts, SUBOPT_RETAIN_DEAD_TUPLES)) + opts->retaindeadtuples = false; + if (IsSet(supported_opts, SUBOPT_MAX_RETENTION_DURATION)) + opts->maxretention = 0; if (IsSet(supported_opts, SUBOPT_ORIGIN)) opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY); @@ -210,7 +242,7 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, if (strcmp(opts->slot_name, "none") == 0) opts->slot_name = NULL; else - ReplicationSlotValidateName(opts->slot_name, ERROR); + ReplicationSlotValidateName(opts->slot_name, false, ERROR); } else if (IsSet(supported_opts, SUBOPT_COPY_DATA) && strcmp(defel->defname, "copy_data") == 0) @@ -307,6 +339,24 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, opts->specified_opts |= SUBOPT_FAILOVER; opts->failover = defGetBoolean(defel); } + else if (IsSet(supported_opts, SUBOPT_RETAIN_DEAD_TUPLES) && + strcmp(defel->defname, "retain_dead_tuples") == 0) + { + if (IsSet(opts->specified_opts, SUBOPT_RETAIN_DEAD_TUPLES)) + errorConflictingDefElem(defel, pstate); + + opts->specified_opts |= SUBOPT_RETAIN_DEAD_TUPLES; + opts->retaindeadtuples = defGetBoolean(defel); + } + else if (IsSet(supported_opts, SUBOPT_MAX_RETENTION_DURATION) && + strcmp(defel->defname, "max_retention_duration") == 0) + { + if (IsSet(opts->specified_opts, SUBOPT_MAX_RETENTION_DURATION)) + errorConflictingDefElem(defel, pstate); + + opts->specified_opts |= SUBOPT_MAX_RETENTION_DURATION; + opts->maxretention = defGetInt32(defel); + } else if (IsSet(supported_opts, SUBOPT_ORIGIN) && strcmp(defel->defname, "origin") == 0) { @@ -348,7 +398,7 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, lsn = DatumGetLSN(DirectFunctionCall1(pg_lsn_in, CStringGetDatum(lsn_str))); - if (XLogRecPtrIsInvalid(lsn)) + if (!XLogRecPtrIsValid(lsn)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid WAL location (LSN): %s", lsn_str))); @@ -357,6 +407,30 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, opts->specified_opts |= SUBOPT_LSN; opts->lsn = lsn; } + else if (IsSet(supported_opts, SUBOPT_WAL_RECEIVER_TIMEOUT) && + strcmp(defel->defname, "wal_receiver_timeout") == 0) + { + bool parsed; + int val; + + if (IsSet(opts->specified_opts, SUBOPT_WAL_RECEIVER_TIMEOUT)) + errorConflictingDefElem(defel, pstate); + + opts->specified_opts |= SUBOPT_WAL_RECEIVER_TIMEOUT; + opts->wal_receiver_timeout = defGetString(defel); + + /* + * Test if the given value is valid for wal_receiver_timeout GUC. + * Skip this test if the value is -1, since -1 is allowed for the + * wal_receiver_timeout subscription option, but not for the GUC + * itself. + */ + parsed = parse_int(opts->wal_receiver_timeout, &val, 0, NULL); + if (!parsed || val != -1) + (void) set_config_option("wal_receiver_timeout", opts->wal_receiver_timeout, + PGC_BACKEND, PGC_S_TEST, GUC_ACTION_SET, + false, 0, false); + } else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -446,20 +520,20 @@ static void check_publications(WalReceiverConn *wrconn, List *publications) { WalRcvExecResult *res; - StringInfo cmd; + StringInfoData cmd; TupleTableSlot *slot; List *publicationsCopy = NIL; Oid tableRow[1] = {TEXTOID}; - cmd = makeStringInfo(); - appendStringInfoString(cmd, "SELECT t.pubname FROM\n" + initStringInfo(&cmd); + appendStringInfoString(&cmd, "SELECT t.pubname FROM\n" " pg_catalog.pg_publication t WHERE\n" " t.pubname IN ("); - GetPublicationsStr(publications, cmd, true); - appendStringInfoChar(cmd, ')'); + GetPublicationsStr(publications, &cmd, true); + appendStringInfoChar(&cmd, ')'); - res = walrcv_exec(wrconn, cmd->data, 1, tableRow); - destroyStringInfo(cmd); + res = walrcv_exec(wrconn, cmd.data, 1, tableRow); + pfree(cmd.data); if (res->status != WALRCV_OK_TUPLES) ereport(ERROR, @@ -490,15 +564,17 @@ check_publications(WalReceiverConn *wrconn, List *publications) if (list_length(publicationsCopy)) { /* Prepare the list of non-existent publication(s) for error message. */ - StringInfo pubnames = makeStringInfo(); + StringInfoData pubnames; + + initStringInfo(&pubnames); - GetPublicationsStr(publicationsCopy, pubnames, false); + GetPublicationsStr(publicationsCopy, &pubnames, false); ereport(WARNING, errcode(ERRCODE_UNDEFINED_OBJECT), errmsg_plural("publication %s does not exist on the publisher", "publications %s do not exist on the publisher", list_length(publicationsCopy), - pubnames->data)); + pubnames.data)); } } @@ -519,7 +595,7 @@ publicationListToArray(List *publist) ALLOCSET_DEFAULT_SIZES); oldcxt = MemoryContextSwitchTo(memcxt); - datums = (Datum *) palloc(sizeof(Datum) * list_length(publist)); + datums = palloc_array(Datum, list_length(publist)); check_duplicates_in_publist(publist, datums); @@ -546,10 +622,11 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Datum values[Natts_pg_subscription]; Oid owner = GetUserId(); HeapTuple tup; + Oid serverid; char *conninfo; char originname[NAMEDATALEN]; List *publications; - bits32 supported_opts; + uint32 supported_opts; SubOpts opts = {0}; AclResult aclresult; @@ -563,7 +640,10 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY | SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT | SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED | - SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER | SUBOPT_ORIGIN); + SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER | + SUBOPT_RETAIN_DEAD_TUPLES | + SUBOPT_MAX_RETENTION_DURATION | + SUBOPT_WAL_RECEIVER_TIMEOUT | SUBOPT_ORIGIN); parse_subscription_options(pstate, stmt->options, supported_opts, &opts); /* @@ -621,7 +701,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, /* Check if name is used */ subid = GetSysCacheOid2(SUBSCRIPTIONNAME, Anum_pg_subscription_oid, - MyDatabaseId, CStringGetDatum(stmt->subname)); + ObjectIdGetDatum(MyDatabaseId), CStringGetDatum(stmt->subname)); if (OidIsValid(subid)) { ereport(ERROR, @@ -630,6 +710,14 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, stmt->subname))); } + /* + * Ensure that system configuration parameters are set appropriately to + * support retain_dead_tuples and max_retention_duration. + */ + CheckSubDeadTupleRetention(true, !opts.enabled, WARNING, + opts.retaindeadtuples, opts.retaindeadtuples, + (opts.maxretention > 0)); + if (!IsSet(opts.specified_opts, SUBOPT_SLOT_NAME) && opts.slot_name == NULL) opts.slot_name = stmt->subname; @@ -638,15 +726,48 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.synchronous_commit == NULL) opts.synchronous_commit = "off"; - conninfo = stmt->conninfo; - publications = stmt->publication; + /* + * The default for wal_receiver_timeout of subscriptions is -1, which + * means the value is inherited from the server configuration, command + * line, or role/database settings. + */ + if (opts.wal_receiver_timeout == NULL) + opts.wal_receiver_timeout = "-1"; /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); + if (stmt->servername) + { + ForeignServer *server; + + Assert(!stmt->conninfo); + conninfo = NULL; + + server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, owner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server->servername); + + /* make sure a user mapping exists */ + GetUserMapping(owner, server->serverid); + + serverid = server->serverid; + conninfo = ForeignServerConnectionString(owner, server); + } + else + { + Assert(stmt->conninfo); + + serverid = InvalidOid; + conninfo = stmt->conninfo; + } + /* Check the connection info string. */ walrcv_check_conninfo(conninfo, opts.passwordrequired && !superuser()); + publications = stmt->publication; + /* Everything ok, form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); @@ -670,8 +791,18 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, values[Anum_pg_subscription_subpasswordrequired - 1] = BoolGetDatum(opts.passwordrequired); values[Anum_pg_subscription_subrunasowner - 1] = BoolGetDatum(opts.runasowner); values[Anum_pg_subscription_subfailover - 1] = BoolGetDatum(opts.failover); - values[Anum_pg_subscription_subconninfo - 1] = - CStringGetTextDatum(conninfo); + values[Anum_pg_subscription_subretaindeadtuples - 1] = + BoolGetDatum(opts.retaindeadtuples); + values[Anum_pg_subscription_submaxretention - 1] = + Int32GetDatum(opts.maxretention); + values[Anum_pg_subscription_subretentionactive - 1] = + Int32GetDatum(opts.retaindeadtuples); + values[Anum_pg_subscription_subserver - 1] = ObjectIdGetDatum(serverid); + if (!OidIsValid(serverid)) + values[Anum_pg_subscription_subconninfo - 1] = + CStringGetTextDatum(conninfo); + else + nulls[Anum_pg_subscription_subconninfo - 1] = true; if (opts.slot_name) values[Anum_pg_subscription_subslotname - 1] = DirectFunctionCall1(namein, CStringGetDatum(opts.slot_name)); @@ -679,6 +810,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, nulls[Anum_pg_subscription_subslotname - 1] = true; values[Anum_pg_subscription_subsynccommit - 1] = CStringGetTextDatum(opts.synchronous_commit); + values[Anum_pg_subscription_subwalrcvtimeout - 1] = + CStringGetTextDatum(opts.wal_receiver_timeout); values[Anum_pg_subscription_subpublications - 1] = publicationListToArray(publications); values[Anum_pg_subscription_suborigin - 1] = @@ -692,20 +825,39 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, recordDependencyOnOwner(SubscriptionRelationId, subid, owner); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + + if (stmt->servername) + { + ObjectAddress referenced; + + Assert(OidIsValid(serverid)); + + ObjectAddressSet(referenced, ForeignServerRelationId, serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + + /* + * A replication origin is currently created for all subscriptions, + * including those that only contain sequences or are otherwise empty. + * + * XXX: While this is technically unnecessary, optimizing it would require + * additional logic to skip origin creation during DDL operations and + * apply workers initialization, and to handle origin creation dynamically + * when tables are added to the subscription. It is not clear whether + * preventing creation of origins is worth additional complexity. + */ ReplicationOriginNameForLogicalRep(subid, InvalidOid, originname, sizeof(originname)); replorigin_create(originname); /* * Connect to remote side to execute requested commands and fetch table - * info. + * and sequence info. */ if (opts.connect) { char *err; WalReceiverConn *wrconn; - List *tables; - ListCell *lc; - char table_state; bool must_use_password; /* Try to connect to the publisher. */ @@ -720,33 +872,48 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, PG_TRY(); { + bool has_tables = false; + List *pubrels; + char relation_state; + check_publications(wrconn, publications); - check_publications_origin(wrconn, publications, opts.copy_data, - opts.origin, NULL, 0, stmt->subname); + check_publications_origin_tables(wrconn, publications, + opts.copy_data, + opts.retaindeadtuples, opts.origin, + NULL, 0, stmt->subname); + check_publications_origin_sequences(wrconn, publications, + opts.copy_data, opts.origin, + NULL, 0, stmt->subname); + + if (opts.retaindeadtuples) + check_pub_dead_tuple_retention(wrconn); /* * Set sync state based on if we were asked to do data copy or * not. */ - table_state = opts.copy_data ? SUBREL_STATE_INIT : SUBREL_STATE_READY; + relation_state = opts.copy_data ? SUBREL_STATE_INIT : SUBREL_STATE_READY; /* - * Get the table list from publisher and build local table status - * info. + * Build local relation status info. Relations are for both tables + * and sequences from the publisher. */ - tables = fetch_table_list(wrconn, publications); - foreach(lc, tables) + pubrels = fetch_relation_list(wrconn, publications); + + foreach_ptr(PublicationRelKind, pubrelinfo, pubrels) { - RangeVar *rv = (RangeVar *) lfirst(lc); Oid relid; + char relkind; + RangeVar *rv = pubrelinfo->rv; relid = RangeVarGetRelid(rv, AccessShareLock, false); + relkind = get_rel_relkind(relid); /* Check for supported relkind. */ - CheckSubscriptionRelkind(get_rel_relkind(relid), + CheckSubscriptionRelkind(relkind, pubrelinfo->relkind, rv->schemaname, rv->relname); - - AddSubscriptionRelState(subid, relid, table_state, + has_tables |= (relkind != RELKIND_SEQUENCE); + AddSubscriptionRelState(subid, relid, relation_state, InvalidXLogRecPtr, true); } @@ -754,6 +921,10 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, * If requested, create permanent slot for the subscription. We * won't use the initial snapshot for anything, so no need to * export it. + * + * XXX: Similar to origins, it is not clear whether preventing the + * slot creation for empty and sequence-only subscriptions is + * worth additional complexity. */ if (opts.create_slot) { @@ -777,7 +948,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, * PENDING, to allow ALTER SUBSCRIPTION ... REFRESH * PUBLICATION to work. */ - if (opts.twophase && !opts.copy_data && tables != NIL) + if (opts.twophase && !opts.copy_data && has_tables) twophase_enabled = true; walrcv_create_slot(wrconn, opts.slot_name, false, twophase_enabled, @@ -800,17 +971,25 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, else ereport(WARNING, (errmsg("subscription was created, but is not connected"), - errhint("To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription."))); + errhint("To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications."))); table_close(rel, RowExclusiveLock); pgstat_create_subscription(subid); - if (opts.enabled) + /* + * Notify the launcher to start the apply worker if the subscription is + * enabled, or to create the conflict detection slot if retain_dead_tuples + * is enabled. + * + * Creating the conflict detection slot is essential even when the + * subscription is not enabled. This ensures that dead tuples are + * retained, which is necessary for accurately identifying the type of + * conflict during replication. + */ + if (opts.enabled || opts.retaindeadtuples) ApplyLauncherWakeupAtCommit(); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostCreateHook(SubscriptionRelationId, subid, 0); return myself; @@ -821,21 +1000,24 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, List *validate_publications) { char *err; - List *pubrel_names; + List *pubrels = NIL; + Oid *pubrel_local_oids; List *subrel_states; + List *sub_remove_rels = NIL; Oid *subrel_local_oids; - Oid *pubrel_local_oids; + Oid *subseq_local_oids; + int subrel_count; ListCell *lc; int off; - int remove_rel_len; - int subrel_count; + int tbl_count = 0; + int seq_count = 0; Relation rel = NULL; typedef struct SubRemoveRels { Oid relid; char state; } SubRemoveRels; - SubRemoveRels *sub_remove_rels; + WalReceiverConn *wrconn; bool must_use_password; @@ -857,71 +1039,84 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, if (validate_publications) check_publications(wrconn, validate_publications); - /* Get the table list from publisher. */ - pubrel_names = fetch_table_list(wrconn, sub->publications); + /* Get the relation list from publisher. */ + pubrels = fetch_relation_list(wrconn, sub->publications); - /* Get local table list. */ - subrel_states = GetSubscriptionRelations(sub->oid, false); + /* Get local relation list. */ + subrel_states = GetSubscriptionRelations(sub->oid, true, true, false); subrel_count = list_length(subrel_states); /* - * Build qsorted array of local table oids for faster lookup. This can - * potentially contain all tables in the database so speed of lookup - * is important. + * Build qsorted arrays of local table oids and sequence oids for + * faster lookup. This can potentially contain all tables and + * sequences in the database so speed of lookup is important. + * + * We do not yet know the exact count of tables and sequences, so we + * allocate separate arrays for table OIDs and sequence OIDs based on + * the total number of relations (subrel_count). */ subrel_local_oids = palloc(subrel_count * sizeof(Oid)); - off = 0; + subseq_local_oids = palloc(subrel_count * sizeof(Oid)); foreach(lc, subrel_states) { SubscriptionRelState *relstate = (SubscriptionRelState *) lfirst(lc); - subrel_local_oids[off++] = relstate->relid; + if (get_rel_relkind(relstate->relid) == RELKIND_SEQUENCE) + subseq_local_oids[seq_count++] = relstate->relid; + else + subrel_local_oids[tbl_count++] = relstate->relid; } - qsort(subrel_local_oids, subrel_count, - sizeof(Oid), oid_cmp); - check_publications_origin(wrconn, sub->publications, copy_data, - sub->origin, subrel_local_oids, - subrel_count, sub->name); + qsort(subrel_local_oids, tbl_count, sizeof(Oid), oid_cmp); + check_publications_origin_tables(wrconn, sub->publications, copy_data, + sub->retaindeadtuples, sub->origin, + subrel_local_oids, tbl_count, + sub->name); - /* - * Rels that we want to remove from subscription and drop any slots - * and origins corresponding to them. - */ - sub_remove_rels = palloc(subrel_count * sizeof(SubRemoveRels)); + qsort(subseq_local_oids, seq_count, sizeof(Oid), oid_cmp); + check_publications_origin_sequences(wrconn, sub->publications, + copy_data, sub->origin, + subseq_local_oids, seq_count, + sub->name); /* - * Walk over the remote tables and try to match them to locally known - * tables. If the table is not known locally create a new state for - * it. + * Walk over the remote relations and try to match them to locally + * known relations. If the relation is not known locally create a new + * state for it. * - * Also builds array of local oids of remote tables for the next step. + * Also builds array of local oids of remote relations for the next + * step. */ off = 0; - pubrel_local_oids = palloc(list_length(pubrel_names) * sizeof(Oid)); + pubrel_local_oids = palloc(list_length(pubrels) * sizeof(Oid)); - foreach(lc, pubrel_names) + foreach_ptr(PublicationRelKind, pubrelinfo, pubrels) { - RangeVar *rv = (RangeVar *) lfirst(lc); + RangeVar *rv = pubrelinfo->rv; Oid relid; + char relkind; relid = RangeVarGetRelid(rv, AccessShareLock, false); + relkind = get_rel_relkind(relid); /* Check for supported relkind. */ - CheckSubscriptionRelkind(get_rel_relkind(relid), + CheckSubscriptionRelkind(relkind, pubrelinfo->relkind, rv->schemaname, rv->relname); pubrel_local_oids[off++] = relid; if (!bsearch(&relid, subrel_local_oids, - subrel_count, sizeof(Oid), oid_cmp)) + tbl_count, sizeof(Oid), oid_cmp) && + !bsearch(&relid, subseq_local_oids, + seq_count, sizeof(Oid), oid_cmp)) { AddSubscriptionRelState(sub->oid, relid, copy_data ? SUBREL_STATE_INIT : SUBREL_STATE_READY, InvalidXLogRecPtr, true); ereport(DEBUG1, - (errmsg_internal("table \"%s.%s\" added to subscription \"%s\"", - rv->schemaname, rv->relname, sub->name))); + errmsg_internal("%s \"%s.%s\" added to subscription \"%s\"", + relkind == RELKIND_SEQUENCE ? "sequence" : "table", + rv->schemaname, rv->relname, sub->name)); } } @@ -929,19 +1124,18 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, * Next remove state for tables we should not care about anymore using * the data we collected above */ - qsort(pubrel_local_oids, list_length(pubrel_names), - sizeof(Oid), oid_cmp); + qsort(pubrel_local_oids, list_length(pubrels), sizeof(Oid), oid_cmp); - remove_rel_len = 0; - for (off = 0; off < subrel_count; off++) + for (off = 0; off < tbl_count; off++) { Oid relid = subrel_local_oids[off]; if (!bsearch(&relid, pubrel_local_oids, - list_length(pubrel_names), sizeof(Oid), oid_cmp)) + list_length(pubrels), sizeof(Oid), oid_cmp)) { char state; XLogRecPtr statelsn; + SubRemoveRels *remove_rel = palloc_object(SubRemoveRels); /* * Lock pg_subscription_rel with AccessExclusiveLock to @@ -963,12 +1157,14 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, /* Last known rel state. */ state = GetSubscriptionRelState(sub->oid, relid, &statelsn); - sub_remove_rels[remove_rel_len].relid = relid; - sub_remove_rels[remove_rel_len++].state = state; - RemoveSubscriptionRel(sub->oid, relid); - logicalrep_worker_stop(sub->oid, relid); + remove_rel->relid = relid; + remove_rel->state = state; + + sub_remove_rels = lappend(sub_remove_rels, remove_rel); + + logicalrep_worker_stop(WORKERTYPE_TABLESYNC, sub->oid, relid); /* * For READY state, we would have already dropped the @@ -983,10 +1179,10 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, * * It is possible that the origin is not yet created for * tablesync worker, this can happen for the states before - * SUBREL_STATE_FINISHEDCOPY. The tablesync worker or - * apply worker can also concurrently try to drop the - * origin and by this time the origin might be already - * removed. For these reasons, passing missing_ok = true. + * SUBREL_STATE_DATASYNC. The tablesync worker or apply + * worker can also concurrently try to drop the origin and + * by this time the origin might be already removed. For + * these reasons, passing missing_ok = true. */ ReplicationOriginNameForLogicalRep(sub->oid, relid, originname, sizeof(originname)); @@ -1006,10 +1202,10 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, * to be at the end because otherwise if there is an error while doing * the database operations we won't be able to rollback dropped slots. */ - for (off = 0; off < remove_rel_len; off++) + foreach_ptr(SubRemoveRels, sub_remove_rel, sub_remove_rels) { - if (sub_remove_rels[off].state != SUBREL_STATE_READY && - sub_remove_rels[off].state != SUBREL_STATE_SYNCDONE) + if (sub_remove_rel->state != SUBREL_STATE_READY && + sub_remove_rel->state != SUBREL_STATE_SYNCDONE) { char syncslotname[NAMEDATALEN] = {0}; @@ -1023,11 +1219,39 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, * dropped slots and fail. For these reasons, we allow * missing_ok = true for the drop. */ - ReplicationSlotNameForTablesync(sub->oid, sub_remove_rels[off].relid, + ReplicationSlotNameForTablesync(sub->oid, sub_remove_rel->relid, syncslotname, sizeof(syncslotname)); ReplicationSlotDropAtPubNode(wrconn, syncslotname, true); } } + + /* + * Next remove state for sequences we should not care about anymore + * using the data we collected above + */ + for (off = 0; off < seq_count; off++) + { + Oid relid = subseq_local_oids[off]; + + if (!bsearch(&relid, pubrel_local_oids, + list_length(pubrels), sizeof(Oid), oid_cmp)) + { + /* + * This locking ensures that the state of rels won't change + * till we are done with this refresh operation. + */ + if (!rel) + rel = table_open(SubscriptionRelRelationId, AccessExclusiveLock); + + RemoveSubscriptionRel(sub->oid, relid); + + ereport(DEBUG1, + errmsg_internal("sequence \"%s.%s\" removed from subscription \"%s\"", + get_namespace_name(get_rel_namespace(relid)), + get_rel_name(relid), + sub->name)); + } + } } PG_FINALLY(); { @@ -1040,18 +1264,74 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data, } /* - * Common checks for altering failover and two_phase options. + * Marks all sequences with INIT state. + */ +static void +AlterSubscription_refresh_seq(Subscription *sub) +{ + char *err = NULL; + WalReceiverConn *wrconn; + bool must_use_password; + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + + /* Try to connect to the publisher. */ + must_use_password = sub->passwordrequired && !sub->ownersuperuser; + wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password, + sub->name, &err); + if (!wrconn) + ereport(ERROR, + errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("subscription \"%s\" could not connect to the publisher: %s", + sub->name, err)); + + PG_TRY(); + { + List *subrel_states; + + check_publications_origin_sequences(wrconn, sub->publications, true, + sub->origin, NULL, 0, sub->name); + + /* Get local sequence list. */ + subrel_states = GetSubscriptionRelations(sub->oid, false, true, false); + foreach_ptr(SubscriptionRelState, subrel, subrel_states) + { + Oid relid = subrel->relid; + + UpdateSubscriptionRelState(sub->oid, relid, SUBREL_STATE_INIT, + InvalidXLogRecPtr, false); + ereport(DEBUG1, + errmsg_internal("sequence \"%s.%s\" of subscription \"%s\" set to INIT state", + get_namespace_name(get_rel_namespace(relid)), + get_rel_name(relid), + sub->name)); + } + } + PG_FINALLY(); + { + walrcv_disconnect(wrconn); + } + PG_END_TRY(); +} + +/* + * Common checks for altering failover, two_phase, and retain_dead_tuples + * options. */ static void CheckAlterSubOption(Subscription *sub, const char *option, bool slot_needs_update, bool isTopLevel) { + Assert(strcmp(option, "failover") == 0 || + strcmp(option, "two_phase") == 0 || + strcmp(option, "retain_dead_tuples") == 0); + /* - * The checks in this function are required only for failover and - * two_phase options. + * Altering the retain_dead_tuples option does not update the slot on the + * publisher. */ - Assert(strcmp(option, "failover") == 0 || - strcmp(option, "two_phase") == 0); + Assert(!slot_needs_update || strcmp(option, "retain_dead_tuples") != 0); /* * Do not allow changing the option if the subscription is enabled. This @@ -1063,6 +1343,39 @@ CheckAlterSubOption(Subscription *sub, const char *option, * the publisher by the existing walsender, so we could have allowed that * even when the subscription is enabled. But we kept this restriction for * the sake of consistency and simplicity. + * + * Additionally, do not allow changing the retain_dead_tuples option when + * the subscription is enabled to prevent race conditions arising from the + * new option value being acknowledged asynchronously by the launcher and + * apply workers. + * + * Without the restriction, a race condition may arise when a user + * disables and immediately re-enables the retain_dead_tuples option. In + * this case, the launcher might drop the slot upon noticing the disabled + * action, while the apply worker may keep maintaining + * oldest_nonremovable_xid without noticing the option change. During this + * period, a transaction ID wraparound could falsely make this ID appear + * as if it originates from the future w.r.t the transaction ID stored in + * the slot maintained by launcher. + * + * Similarly, if the user enables retain_dead_tuples concurrently with the + * launcher starting the worker, the apply worker may start calculating + * oldest_nonremovable_xid before the launcher notices the enable action. + * Consequently, the launcher may update slot.xmin to a newer value than + * that maintained by the worker. In subsequent cycles, upon integrating + * the worker's oldest_nonremovable_xid, the launcher might detect a + * retreat in the calculated xmin, necessitating additional handling. + * + * XXX To address the above race conditions, we can define + * oldest_nonremovable_xid as FullTransactionId and adds the check to + * disallow retreating the conflict slot's xmin. For now, we kept the + * implementation simple by disallowing change to the retain_dead_tuples, + * but in the future we can change this after some more analysis. + * + * Note that we could restrict only the enabling of retain_dead_tuples to + * avoid the race conditions described above, but we maintain the + * restriction for both enable and disable operations for the sake of + * consistency. */ if (sub->enabled) ereport(ERROR, @@ -1110,15 +1423,20 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, bool update_tuple = false; bool update_failover = false; bool update_two_phase = false; + bool check_pub_rdt = false; + bool retain_dead_tuples; + int max_retention; + bool retention_active; + char *origin; Subscription *sub; Form_pg_subscription form; - bits32 supported_opts; + uint32 supported_opts; SubOpts opts = {0}; rel = table_open(SubscriptionRelationId, RowExclusiveLock); /* Fetch the existing tuple. */ - tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, MyDatabaseId, + tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId), CStringGetDatum(stmt->subname)); if (!HeapTupleIsValid(tup)) @@ -1135,7 +1453,19 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION, stmt->subname); - sub = GetSubscription(subid, false); + /* + * Skip ACL checks on the subscription's foreign server, if any. If + * changing the server (or replacing it with a raw connection), then the + * old one will be removed anyway. If changing something unrelated, + * there's no need to do an additional ACL check here; that will be done + * by the subscription worker anyway. + */ + sub = GetSubscription(subid, false, false); + + retain_dead_tuples = sub->retaindeadtuples; + origin = sub->origin; + max_retention = sub->maxretention; + retention_active = sub->retentionactive; /* * Don't allow non-superuser modification of a subscription with @@ -1155,6 +1485,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, memset(nulls, false, sizeof(nulls)); memset(replaces, false, sizeof(replaces)); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + switch (stmt->kind) { case ALTER_SUBSCRIPTION_OPTIONS: @@ -1165,6 +1497,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED | SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER | + SUBOPT_RETAIN_DEAD_TUPLES | + SUBOPT_MAX_RETENTION_DURATION | + SUBOPT_WAL_RECEIVER_TIMEOUT | SUBOPT_ORIGIN); parse_subscription_options(pstate, stmt->options, @@ -1267,7 +1602,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, IsSet(opts.specified_opts, SUBOPT_SLOT_NAME)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("slot_name and two_phase cannot be altered at the same time"))); + errmsg("\"slot_name\" and \"two_phase\" cannot be altered at the same time"))); /* * Note that workers may still survive even if the @@ -1283,7 +1618,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, if (logicalrep_workers_find(subid, true, true)) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot alter two_phase when logical replication worker is still running"), + errmsg("cannot alter \"two_phase\" when logical replication worker is still running"), errhint("Try again after some time."))); /* @@ -1297,7 +1632,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, LookupGXactBySubid(subid)) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot disable two_phase when prepared transactions are present"), + errmsg("cannot disable \"two_phase\" when prepared transactions exist"), errhint("Resolve these transactions and try again."))); /* Change system catalog accordingly */ @@ -1325,11 +1660,106 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, replaces[Anum_pg_subscription_subfailover - 1] = true; } + if (IsSet(opts.specified_opts, SUBOPT_RETAIN_DEAD_TUPLES)) + { + values[Anum_pg_subscription_subretaindeadtuples - 1] = + BoolGetDatum(opts.retaindeadtuples); + replaces[Anum_pg_subscription_subretaindeadtuples - 1] = true; + + /* + * Update the retention status only if there's a change in + * the retain_dead_tuples option value. + * + * Automatically marking retention as active when + * retain_dead_tuples is enabled may not always be ideal, + * especially if retention was previously stopped and the + * user toggles retain_dead_tuples without adjusting the + * publisher workload. However, this behavior provides a + * convenient way for users to manually refresh the + * retention status. Since retention will be stopped again + * unless the publisher workload is reduced, this approach + * is acceptable for now. + */ + if (opts.retaindeadtuples != sub->retaindeadtuples) + { + values[Anum_pg_subscription_subretentionactive - 1] = + BoolGetDatum(opts.retaindeadtuples); + replaces[Anum_pg_subscription_subretentionactive - 1] = true; + + retention_active = opts.retaindeadtuples; + } + + CheckAlterSubOption(sub, "retain_dead_tuples", false, isTopLevel); + + /* + * Workers may continue running even after the + * subscription has been disabled. + * + * To prevent race conditions (as described in + * CheckAlterSubOption()), ensure that all worker + * processes have already exited before proceeding. + */ + if (logicalrep_workers_find(subid, true, true)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot alter retain_dead_tuples when logical replication worker is still running"), + errhint("Try again after some time."))); + + /* + * Notify the launcher to manage the replication slot for + * conflict detection. This ensures that replication slot + * is efficiently handled (created, updated, or dropped) + * in response to any configuration changes. + */ + ApplyLauncherWakeupAtCommit(); + + check_pub_rdt = opts.retaindeadtuples; + retain_dead_tuples = opts.retaindeadtuples; + } + + if (IsSet(opts.specified_opts, SUBOPT_MAX_RETENTION_DURATION)) + { + values[Anum_pg_subscription_submaxretention - 1] = + Int32GetDatum(opts.maxretention); + replaces[Anum_pg_subscription_submaxretention - 1] = true; + + max_retention = opts.maxretention; + } + + /* + * Ensure that system configuration parameters are set + * appropriately to support retain_dead_tuples and + * max_retention_duration. + */ + if (IsSet(opts.specified_opts, SUBOPT_RETAIN_DEAD_TUPLES) || + IsSet(opts.specified_opts, SUBOPT_MAX_RETENTION_DURATION)) + CheckSubDeadTupleRetention(true, !sub->enabled, NOTICE, + retain_dead_tuples, + retention_active, + (max_retention > 0)); + if (IsSet(opts.specified_opts, SUBOPT_ORIGIN)) { values[Anum_pg_subscription_suborigin - 1] = CStringGetTextDatum(opts.origin); replaces[Anum_pg_subscription_suborigin - 1] = true; + + /* + * Check if changes from different origins may be received + * from the publisher when the origin is changed to ANY + * and retain_dead_tuples is enabled. + */ + check_pub_rdt = retain_dead_tuples && + pg_strcasecmp(opts.origin, LOGICALREP_ORIGIN_ANY) == 0; + + origin = opts.origin; + } + + if (IsSet(opts.specified_opts, SUBOPT_WAL_RECEIVER_TIMEOUT)) + { + values[Anum_pg_subscription_subwalrcvtimeout - 1] = + CStringGetTextDatum(opts.wal_receiver_timeout); + replaces[Anum_pg_subscription_subwalrcvtimeout - 1] = true; } update_tuple = true; @@ -1347,6 +1777,15 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot enable subscription that does not have a slot name"))); + /* + * Check track_commit_timestamp only when enabling the + * subscription in case it was disabled after creation. See + * comments atop CheckSubDeadTupleRetention() for details. + */ + CheckSubDeadTupleRetention(opts.enabled, !opts.enabled, + WARNING, sub->retaindeadtuples, + sub->retentionactive, false); + values[Anum_pg_subscription_subenabled - 1] = BoolGetDatum(opts.enabled); replaces[Anum_pg_subscription_subenabled - 1] = true; @@ -1355,10 +1794,89 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, ApplyLauncherWakeupAtCommit(); update_tuple = true; + + /* + * The subscription might be initially created with + * connect=false and retain_dead_tuples=true, meaning the + * remote server's status may not be checked. Ensure this + * check is conducted now. + */ + check_pub_rdt = sub->retaindeadtuples && opts.enabled; break; } + case ALTER_SUBSCRIPTION_SERVER: + { + ForeignServer *new_server; + ObjectAddress referenced; + AclResult aclresult; + char *conninfo; + + /* + * Remove what was there before, either another foreign server + * or a connection string. + */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + } + else + { + nulls[Anum_pg_subscription_subconninfo - 1] = true; + replaces[Anum_pg_subscription_subconninfo - 1] = true; + } + + /* + * Check that the subscription owner has USAGE privileges on + * the server. + */ + new_server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, + new_server->serverid, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + new_server->servername)); + + /* make sure a user mapping exists */ + GetUserMapping(form->subowner, new_server->serverid); + + conninfo = ForeignServerConnectionString(form->subowner, + new_server); + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + /* Check the connection info string. */ + walrcv_check_conninfo(conninfo, + sub->passwordrequired && !sub->ownersuperuser); + + values[Anum_pg_subscription_subserver - 1] = ObjectIdGetDatum(new_server->serverid); + replaces[Anum_pg_subscription_subserver - 1] = true; + + ObjectAddressSet(referenced, ForeignServerRelationId, new_server->serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + update_tuple = true; + } + break; + case ALTER_SUBSCRIPTION_CONNECTION: + /* remove reference to foreign server and dependencies, if present */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + + values[Anum_pg_subscription_subserver - 1] = ObjectIdGetDatum(InvalidOid); + replaces[Anum_pg_subscription_subserver - 1] = true; + } + /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); /* Check the connection info string. */ @@ -1369,6 +1887,13 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, CStringGetTextDatum(stmt->conninfo); replaces[Anum_pg_subscription_subconninfo - 1] = true; update_tuple = true; + + /* + * Since the remote server configuration might have changed, + * perform a check to ensure it permits enabling + * retain_dead_tuples. + */ + check_pub_rdt = sub->retaindeadtuples; break; case ALTER_SUBSCRIPTION_SET_PUBLICATION: @@ -1393,8 +1918,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, errhint("Use ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false)."))); /* - * See ALTER_SUBSCRIPTION_REFRESH for details why this is - * not allowed. + * See ALTER_SUBSCRIPTION_REFRESH_PUBLICATION for details + * why this is not allowed. */ if (sub->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED && opts.copy_data) ereport(ERROR, @@ -1448,8 +1973,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, "ALTER SUBSCRIPTION ... DROP PUBLICATION ... WITH (refresh = false)"))); /* - * See ALTER_SUBSCRIPTION_REFRESH for details why this is - * not allowed. + * See ALTER_SUBSCRIPTION_REFRESH_PUBLICATION for details + * why this is not allowed. */ if (sub->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED && opts.copy_data) ereport(ERROR, @@ -1473,12 +1998,13 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, break; } - case ALTER_SUBSCRIPTION_REFRESH: + case ALTER_SUBSCRIPTION_REFRESH_PUBLICATION: { if (!sub->enabled) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions"))); + errmsg("%s is not allowed for disabled subscriptions", + "ALTER SUBSCRIPTION ... REFRESH PUBLICATION"))); parse_subscription_options(pstate, stmt->options, SUBOPT_COPY_DATA, &opts); @@ -1490,8 +2016,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, * * But, having reached this two-phase commit "enabled" state * we must not allow any subsequent table initialization to - * occur. So the ALTER SUBSCRIPTION ... REFRESH is disallowed - * when the user had requested two_phase = on mode. + * occur. So the ALTER SUBSCRIPTION ... REFRESH PUBLICATION is + * disallowed when the user had requested two_phase = on mode. * * The exception to this restriction is when copy_data = * false, because when copy_data is false the tablesync will @@ -1503,16 +2029,29 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, if (sub->twophasestate == LOGICALREP_TWOPHASE_STATE_ENABLED && opts.copy_data) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("ALTER SUBSCRIPTION ... REFRESH with copy_data is not allowed when two_phase is enabled"), - errhint("Use ALTER SUBSCRIPTION ... REFRESH with copy_data = false, or use DROP/CREATE SUBSCRIPTION."))); + errmsg("ALTER SUBSCRIPTION ... REFRESH PUBLICATION with copy_data is not allowed when two_phase is enabled"), + errhint("Use ALTER SUBSCRIPTION ... REFRESH PUBLICATION with copy_data = false, or use DROP/CREATE SUBSCRIPTION."))); - PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH"); + PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH PUBLICATION"); AlterSubscription_refresh(sub, opts.copy_data, NULL); break; } + case ALTER_SUBSCRIPTION_REFRESH_SEQUENCES: + { + if (!sub->enabled) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("%s is not allowed for disabled subscriptions", + "ALTER SUBSCRIPTION ... REFRESH SEQUENCES")); + + AlterSubscription_refresh_seq(sub); + + break; + } + case ALTER_SUBSCRIPTION_SKIP: { parse_subscription_options(pstate, stmt->options, SUBOPT_LSN, &opts); @@ -1524,9 +2063,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, * If the user sets subskiplsn, we do a sanity check to make * sure that the specified LSN is a probable value. */ - if (!XLogRecPtrIsInvalid(opts.lsn)) + if (XLogRecPtrIsValid(opts.lsn)) { - RepOriginId originid; + ReplOriginId originid; char originname[NAMEDATALEN]; XLogRecPtr remote_lsn; @@ -1536,10 +2075,10 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, remote_lsn = replorigin_get_progress(originid, false); /* Check the given LSN is at least a future LSN */ - if (!XLogRecPtrIsInvalid(remote_lsn) && opts.lsn < remote_lsn) + if (XLogRecPtrIsValid(remote_lsn) && opts.lsn < remote_lsn) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("skip WAL location (LSN %X/%X) must be greater than origin LSN %X/%X", + errmsg("skip WAL location (LSN %X/%08X) must be greater than origin LSN %X/%08X", LSN_FORMAT_ARGS(opts.lsn), LSN_FORMAT_ARGS(remote_lsn)))); } @@ -1568,14 +2107,15 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, } /* - * Try to acquire the connection necessary for altering the slot, if - * needed. + * Try to acquire the connection necessary either for modifying the slot + * or for checking if the remote server permits enabling + * retain_dead_tuples. * * This has to be at the end because otherwise if there is an error while * doing the database operations we won't be able to rollback altered * slot. */ - if (update_failover || update_two_phase) + if (update_failover || update_two_phase || check_pub_rdt) { bool must_use_password; char *err; @@ -1584,10 +2124,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); - /* Try to connect to the publisher. */ + /* + * Try to connect to the publisher, using the new connection string if + * available. + */ must_use_password = sub->passwordrequired && !sub->ownersuperuser; - wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password, - sub->name, &err); + wrconn = walrcv_connect(stmt->conninfo ? stmt->conninfo : sub->conninfo, + true, true, must_use_password, sub->name, + &err); if (!wrconn) ereport(ERROR, (errcode(ERRCODE_CONNECTION_FAILURE), @@ -1596,9 +2140,17 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, PG_TRY(); { - walrcv_alter_slot(wrconn, sub->slotname, - update_failover ? &opts.failover : NULL, - update_two_phase ? &opts.twophase : NULL); + if (retain_dead_tuples) + check_pub_dead_tuple_retention(wrconn); + + check_publications_origin_tables(wrconn, sub->publications, false, + retain_dead_tuples, origin, NULL, 0, + sub->name); + + if (update_failover || update_two_phase) + walrcv_alter_slot(wrconn, sub->slotname, + update_failover ? &opts.failover : NULL, + update_two_phase ? &opts.twophase : NULL); } PG_FINALLY(); { @@ -1609,8 +2161,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, table_close(rel, RowExclusiveLock); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostAlterHook(SubscriptionRelationId, subid, 0); /* Wake up related replication workers to handle this change quickly. */ @@ -1639,18 +2189,20 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) ListCell *lc; char originname[NAMEDATALEN]; char *err = NULL; - WalReceiverConn *wrconn; + WalReceiverConn *wrconn = NULL; Form_pg_subscription form; List *rstates; bool must_use_password; /* - * Lock pg_subscription with AccessExclusiveLock to ensure that the - * launcher doesn't restart new worker during dropping the subscription + * The launcher may concurrently start a new worker for this subscription. + * During initialization, the worker checks for subscription validity and + * exits if the subscription has already been dropped. See + * InitializeLogRepWorker. */ - rel = table_open(SubscriptionRelationId, AccessExclusiveLock); + rel = table_open(SubscriptionRelationId, RowExclusiveLock); - tup = SearchSysCache2(SUBSCRIPTIONNAME, MyDatabaseId, + tup = SearchSysCache2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId), CStringGetDatum(stmt->subname)); if (!HeapTupleIsValid(tup)) @@ -1695,9 +2247,37 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) subname = pstrdup(NameStr(*DatumGetName(datum))); /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, - Anum_pg_subscription_subconninfo); - conninfo = TextDatumGetCString(datum); + if (OidIsValid(form->subserver)) + { + AclResult aclresult; + ForeignServer *server; + + server = GetForeignServer(form->subserver); + aclresult = object_aclcheck(ForeignServerRelationId, form->subserver, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + { + /* + * Unable to generate connection string because permissions on the + * foreign server have been removed. Follow the same logic as an + * unusable subconninfo (which will result in an ERROR later + * unless slot_name = NONE). + */ + err = psprintf(_("subscription owner \"%s\" does not have permission on foreign server \"%s\""), + GetUserNameFromId(form->subowner, false), + server->servername); + conninfo = NULL; + } + else + conninfo = ForeignServerConnectionString(form->subowner, + server); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo); + conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, @@ -1750,7 +2330,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) { LogicalRepWorker *w = (LogicalRepWorker *) lfirst(lc); - logicalrep_worker_stop(w->subid, w->relid); + logicalrep_worker_stop(w->type, w->subid, w->relid); } list_free(subworkers); @@ -1773,7 +2353,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) * the apply and tablesync workers and they can't restart because of * exclusive lock on the subscription. */ - rstates = GetSubscriptionRelations(subid, true); + rstates = GetSubscriptionRelations(subid, true, false, true); foreach(lc, rstates) { SubscriptionRelState *rstate = (SubscriptionRelState *) lfirst(lc); @@ -1788,7 +2368,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) * * It is possible that the origin is not yet created for tablesync * worker so passing missing_ok = true. This can happen for the states - * before SUBREL_STATE_FINISHEDCOPY. + * before SUBREL_STATE_DATASYNC. */ ReplicationOriginNameForLogicalRep(subid, relid, originname, sizeof(originname)); @@ -1796,6 +2376,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) } /* Clean up dependencies */ + deleteDependencyRecordsFor(SubscriptionRelationId, subid, false); deleteSharedDependencyRecordsFor(SubscriptionRelationId, subid, 0); /* Remove any associated relation synchronization states. */ @@ -1834,8 +2415,10 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) */ load_file("libpqwalreceiver", false); - wrconn = walrcv_connect(conninfo, true, true, must_use_password, - subname, &err); + if (conninfo) + wrconn = walrcv_connect(conninfo, true, true, must_use_password, + subname, &err); + if (wrconn == NULL) { if (!slotname) @@ -2005,6 +2588,27 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId) aclcheck_error(aclresult, OBJECT_DATABASE, get_database_name(MyDatabaseId)); + /* + * If the subscription uses a server, check that the new owner has USAGE + * privileges on the server and that a user mapping exists. Note: does not + * re-check the resulting connection string. + */ + if (OidIsValid(form->subserver)) + { + ForeignServer *server = GetForeignServer(form->subserver); + + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, newOwnerId, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("new subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(newOwnerId, false), + server->servername)); + + /* make sure a user mapping exists */ + GetUserMapping(newOwnerId, server->serverid); + } + form->subowner = newOwnerId; CatalogTupleUpdate(rel, &tup->t_self, tup); @@ -2035,7 +2639,7 @@ AlterSubscriptionOwner(const char *name, Oid newOwnerId) rel = table_open(SubscriptionRelationId, RowExclusiveLock); - tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, MyDatabaseId, + tup = SearchSysCacheCopy2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId), CStringGetDatum(name)); if (!HeapTupleIsValid(tup)) @@ -2086,21 +2690,30 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId) * Check and log a warning if the publisher has subscribed to the same table, * its partition ancestors (if it's a partition), or its partition children (if * it's a partitioned table), from some other publishers. This check is - * required only if "copy_data = true" and "origin = none" for CREATE - * SUBSCRIPTION and ALTER SUBSCRIPTION ... REFRESH statements to notify the - * user that data having origin might have been copied. + * required in the following scenarios: * - * This check need not be performed on the tables that are already added - * because incremental sync for those tables will happen through WAL and the - * origin of the data can be identified from the WAL records. + * 1) For CREATE SUBSCRIPTION and ALTER SUBSCRIPTION ... REFRESH PUBLICATION + * statements with "copy_data = true" and "origin = none": + * - Warn the user that data with an origin might have been copied. + * - This check is skipped for tables already added, as incremental sync via + * WAL allows origin tracking. The list of such tables is in + * subrel_local_oids. * - * subrel_local_oids contains the list of relation oids that are already - * present on the subscriber. + * 2) For CREATE SUBSCRIPTION and ALTER SUBSCRIPTION ... REFRESH PUBLICATION + * statements with "retain_dead_tuples = true" and "origin = any", and for + * ALTER SUBSCRIPTION statements that modify retain_dead_tuples or origin, + * or when the publisher's status changes (e.g., due to a connection string + * update): + * - Warn the user that only conflict detection info for local changes on + * the publisher is retained. Data from other origins may lack sufficient + * details for reliable conflict detection. + * - See comments atop worker.c for more details. */ static void -check_publications_origin(WalReceiverConn *wrconn, List *publications, - bool copydata, char *origin, Oid *subrel_local_oids, - int subrel_count, char *subname) +check_publications_origin_tables(WalReceiverConn *wrconn, List *publications, + bool copydata, bool retain_dead_tuples, + char *origin, Oid *subrel_local_oids, + int subrel_count, char *subname) { WalRcvExecResult *res; StringInfoData cmd; @@ -2108,9 +2721,29 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications, Oid tableRow[1] = {TEXTOID}; List *publist = NIL; int i; + bool check_rdt; + bool check_table_sync; + bool origin_none = origin && + pg_strcasecmp(origin, LOGICALREP_ORIGIN_NONE) == 0; + + /* + * Enable retain_dead_tuples checks only when origin is set to 'any', + * since with origin='none' only local changes are replicated to the + * subscriber. + */ + check_rdt = retain_dead_tuples && !origin_none; + + /* + * Enable table synchronization checks only when origin is 'none', to + * ensure that data from other origins is not inadvertently copied. + */ + check_table_sync = copydata && origin_none; + + /* retain_dead_tuples and table sync checks occur separately */ + Assert(!(check_rdt && check_table_sync)); - if (!copydata || !origin || - (pg_strcasecmp(origin, LOGICALREP_ORIGIN_NONE) != 0)) + /* Return if no checks are required */ + if (!check_rdt && !check_table_sync) return; initStringInfo(&cmd); @@ -2127,18 +2760,25 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications, appendStringInfoString(&cmd, ")\n"); /* - * In case of ALTER SUBSCRIPTION ... REFRESH, subrel_local_oids contains - * the list of relation oids that are already present on the subscriber. - * This check should be skipped for these tables. + * In case of ALTER SUBSCRIPTION ... REFRESH PUBLICATION, + * subrel_local_oids contains the list of relation oids that are already + * present on the subscriber. This check should be skipped for these + * tables if checking for table sync scenario. However, when handling the + * retain_dead_tuples scenario, ensure all tables are checked, as some + * existing tables may now include changes from other origins due to newly + * created subscriptions on the publisher. */ - for (i = 0; i < subrel_count; i++) + if (check_table_sync) { - Oid relid = subrel_local_oids[i]; - char *schemaname = get_namespace_name(get_rel_namespace(relid)); - char *tablename = get_rel_name(relid); + for (i = 0; i < subrel_count; i++) + { + Oid relid = subrel_local_oids[i]; + char *schemaname = get_namespace_name(get_rel_namespace(relid)); + char *tablename = get_rel_name(relid); - appendStringInfo(&cmd, "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n", - schemaname, tablename); + appendStringInfo(&cmd, "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n", + schemaname, tablename); + } } res = walrcv_exec(wrconn, cmd.data, 1, tableRow); @@ -2150,7 +2790,7 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications, errmsg("could not receive list of replicated tables from the publisher: %s", res->err))); - /* Process tables. */ + /* Process publications. */ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple); while (tuplestore_gettupleslot(res->tuplestore, true, false, slot)) { @@ -2173,22 +2813,140 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications, * XXX: For simplicity, we don't check whether the table has any data or * not. If the table doesn't have any data then we don't need to * distinguish between data having origin and data not having origin so we - * can avoid logging a warning in that case. + * can avoid logging a warning for table sync scenario. */ if (publist) { - StringInfo pubnames = makeStringInfo(); + StringInfoData pubnames; /* Prepare the list of publication(s) for warning message. */ - GetPublicationsStr(publist, pubnames, false); + initStringInfo(&pubnames); + GetPublicationsStr(publist, &pubnames, false); + + if (check_table_sync) + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("subscription \"%s\" requested copy_data with origin = NONE but might copy data that had a different origin", + subname), + errdetail_plural("The subscription subscribes to a publication (%s) that contains tables that are written to by other subscriptions.", + "The subscription subscribes to publications (%s) that contain tables that are written to by other subscriptions.", + list_length(publist), pubnames.data), + errhint("Verify that initial data copied from the publisher tables did not come from other origins.")); + else + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("subscription \"%s\" enabled retain_dead_tuples but might not reliably detect conflicts for changes from different origins", + subname), + errdetail_plural("The subscription subscribes to a publication (%s) that contains tables that are written to by other subscriptions.", + "The subscription subscribes to publications (%s) that contain tables that are written to by other subscriptions.", + list_length(publist), pubnames.data), + errhint("Consider using origin = NONE or disabling retain_dead_tuples.")); + } + + ExecDropSingleTupleTableSlot(slot); + + walrcv_clear_result(res); +} + +/* + * This function is similar to check_publications_origin_tables and serves + * same purpose for sequences. + */ +static void +check_publications_origin_sequences(WalReceiverConn *wrconn, List *publications, + bool copydata, char *origin, + Oid *subrel_local_oids, int subrel_count, + char *subname) +{ + WalRcvExecResult *res; + StringInfoData cmd; + TupleTableSlot *slot; + Oid tableRow[1] = {TEXTOID}; + List *publist = NIL; + + /* + * Enable sequence synchronization checks only when origin is 'none' , to + * ensure that sequence data from other origins is not inadvertently + * copied. This check is necessary if the publisher is running PG19 or + * later, where logical replication sequence synchronization is supported. + */ + if (!copydata || pg_strcasecmp(origin, LOGICALREP_ORIGIN_NONE) != 0 || + walrcv_server_version(wrconn) < 190000) + return; + + initStringInfo(&cmd); + appendStringInfoString(&cmd, + "SELECT DISTINCT P.pubname AS pubname\n" + "FROM pg_publication P,\n" + " LATERAL pg_get_publication_sequences(P.pubname) GPS\n" + " JOIN pg_subscription_rel PS ON (GPS.relid = PS.srrelid),\n" + " pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)\n" + "WHERE C.oid = GPS.relid AND P.pubname IN ("); + + GetPublicationsStr(publications, &cmd, true); + appendStringInfoString(&cmd, ")\n"); + + /* + * In case of ALTER SUBSCRIPTION ... REFRESH PUBLICATION, + * subrel_local_oids contains the list of relations that are already + * present on the subscriber. This check should be skipped as these will + * not be re-synced. + */ + for (int i = 0; i < subrel_count; i++) + { + Oid relid = subrel_local_oids[i]; + char *schemaname = get_namespace_name(get_rel_namespace(relid)); + char *seqname = get_rel_name(relid); + + appendStringInfo(&cmd, + "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n", + schemaname, seqname); + } + + res = walrcv_exec(wrconn, cmd.data, 1, tableRow); + pfree(cmd.data); + + if (res->status != WALRCV_OK_TUPLES) + ereport(ERROR, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("could not receive list of replicated sequences from the publisher: %s", + res->err))); + + /* Process publications. */ + slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple); + while (tuplestore_gettupleslot(res->tuplestore, true, false, slot)) + { + char *pubname; + bool isnull; + + pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull)); + Assert(!isnull); + + ExecClearTuple(slot); + publist = list_append_unique(publist, makeString(pubname)); + } + + /* + * Log a warning if the publisher has subscribed to the same sequence from + * some other publisher. We cannot know the origin of sequences data + * during the initial sync. + */ + if (publist) + { + StringInfoData pubnames; + + /* Prepare the list of publication(s) for warning message. */ + initStringInfo(&pubnames); + GetPublicationsStr(publist, &pubnames, false); + ereport(WARNING, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("subscription \"%s\" requested copy_data with origin = NONE but might copy data that had a different origin", subname), - errdetail_plural("The subscription being created subscribes to a publication (%s) that contains tables that are written to by other subscriptions.", - "The subscription being created subscribes to publications (%s) that contain tables that are written to by other subscriptions.", - list_length(publist), pubnames->data), - errhint("Verify that initial data copied from the publisher tables did not come from other origins.")); + errdetail_plural("The subscription subscribes to a publication (%s) that contains sequences that are written to by other subscriptions.", + "The subscription subscribes to publications (%s) that contain sequences that are written to by other subscriptions.", + list_length(publist), pubnames.data), + errhint("Verify that initial data copied from the publisher sequences did not come from other origins.")); } ExecDropSingleTupleTableSlot(slot); @@ -2197,8 +2955,134 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications, } /* - * Get the list of tables which belong to specified publications on the - * publisher connection. + * Determine whether the retain_dead_tuples can be enabled based on the + * publisher's status. + * + * This option is disallowed if the publisher is running a version earlier + * than the PG19, or if the publisher is in recovery (i.e., it is a standby + * server). + * + * See comments atop worker.c for a detailed explanation. + */ +static void +check_pub_dead_tuple_retention(WalReceiverConn *wrconn) +{ + WalRcvExecResult *res; + Oid RecoveryRow[1] = {BOOLOID}; + TupleTableSlot *slot; + bool isnull; + bool remote_in_recovery; + + if (walrcv_server_version(wrconn) < 190000) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot enable retain_dead_tuples if the publisher is running a version earlier than PostgreSQL 19")); + + res = walrcv_exec(wrconn, "SELECT pg_is_in_recovery()", 1, RecoveryRow); + + if (res->status != WALRCV_OK_TUPLES) + ereport(ERROR, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("could not obtain recovery progress from the publisher: %s", + res->err))); + + slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple); + if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot)) + elog(ERROR, "failed to fetch tuple for the recovery progress"); + + remote_in_recovery = DatumGetBool(slot_getattr(slot, 1, &isnull)); + + if (remote_in_recovery) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot enable retain_dead_tuples if the publisher is in recovery")); + + ExecDropSingleTupleTableSlot(slot); + + walrcv_clear_result(res); +} + +/* + * Check if the subscriber's configuration is adequate to enable the + * retain_dead_tuples option. + * + * Issue an ERROR if the wal_level does not support the use of replication + * slots when check_guc is set to true. + * + * Issue a WARNING if track_commit_timestamp is not enabled when check_guc is + * set to true. This is only to highlight the importance of enabling + * track_commit_timestamp instead of catching all the misconfigurations, as + * this setting can be adjusted after subscription creation. Without it, the + * apply worker will simply skip conflict detection. + * + * Issue a WARNING or NOTICE if the subscription is disabled and the retention + * is active. Do not raise an ERROR since users can only modify + * retain_dead_tuples for disabled subscriptions. And as long as the + * subscription is enabled promptly, it will not pose issues. + * + * Issue a NOTICE to inform users that max_retention_duration is + * ineffective when retain_dead_tuples is disabled for a subscription. An ERROR + * is not issued because setting max_retention_duration causes no harm, + * even when it is ineffective. + */ +void +CheckSubDeadTupleRetention(bool check_guc, bool sub_disabled, + int elevel_for_sub_disabled, + bool retain_dead_tuples, bool retention_active, + bool max_retention_set) +{ + Assert(elevel_for_sub_disabled == NOTICE || + elevel_for_sub_disabled == WARNING); + + if (retain_dead_tuples) + { + if (check_guc && wal_level < WAL_LEVEL_REPLICA) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("\"wal_level\" is insufficient to create the replication slot required by retain_dead_tuples"), + errhint("\"wal_level\" must be set to \"replica\" or \"logical\" at server start.")); + + if (check_guc && !track_commit_timestamp) + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("commit timestamp and origin data required for detecting conflicts won't be retained"), + errhint("Consider setting \"%s\" to true.", + "track_commit_timestamp")); + + if (sub_disabled && retention_active) + ereport(elevel_for_sub_disabled, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("deleted rows to detect conflicts would not be removed until the subscription is enabled"), + (elevel_for_sub_disabled > NOTICE) + ? errhint("Consider setting %s to false.", + "retain_dead_tuples") : 0); + } + else if (max_retention_set) + { + ereport(NOTICE, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("max_retention_duration is ineffective when retain_dead_tuples is disabled")); + } +} + +/* + * Return true iff 'rv' is a member of the list. + */ +static bool +list_member_rangevar(const List *list, RangeVar *rv) +{ + foreach_ptr(PublicationRelKind, relinfo, list) + { + if (equal(relinfo->rv, rv)) + return true; + } + + return false; +} + +/* + * Get the list of tables and sequences which belong to specified publications + * on the publisher connection. * * Note that we don't support the case where the column list is different for * the same table in different publications to avoid sending unwanted column @@ -2206,26 +3090,28 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications, * list and row filter are specified for different publications. */ static List * -fetch_table_list(WalReceiverConn *wrconn, List *publications) +fetch_relation_list(WalReceiverConn *wrconn, List *publications) { WalRcvExecResult *res; StringInfoData cmd; TupleTableSlot *slot; - Oid tableRow[3] = {TEXTOID, TEXTOID, InvalidOid}; - List *tablelist = NIL; + Oid tableRow[4] = {TEXTOID, TEXTOID, CHAROID, InvalidOid}; + List *relationlist = NIL; int server_version = walrcv_server_version(wrconn); bool check_columnlist = (server_version >= 150000); - StringInfo pub_names = makeStringInfo(); + int column_count = check_columnlist ? 4 : 3; + StringInfoData pub_names; initStringInfo(&cmd); + initStringInfo(&pub_names); /* Build the pub_names comma-separated string. */ - GetPublicationsStr(publications, pub_names, true); + GetPublicationsStr(publications, &pub_names, true); - /* Get the list of tables from the publisher. */ + /* Get the list of relations from the publisher */ if (server_version >= 160000) { - tableRow[2] = INT2VECTOROID; + tableRow[3] = INT2VECTOROID; /* * From version 16, we allowed passing multiple publications to the @@ -2240,19 +3126,28 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications) * to worry if different publications have specified them in a * different order. See pub_collist_validate. */ - appendStringInfo(&cmd, "SELECT DISTINCT n.nspname, c.relname, gpt.attrs\n" - " FROM pg_class c\n" + appendStringInfo(&cmd, "SELECT DISTINCT n.nspname, c.relname, c.relkind, gpt.attrs\n" + " FROM pg_class c\n" " JOIN pg_namespace n ON n.oid = c.relnamespace\n" " JOIN ( SELECT (pg_get_publication_tables(VARIADIC array_agg(pubname::text))).*\n" " FROM pg_publication\n" " WHERE pubname IN ( %s )) AS gpt\n" " ON gpt.relid = c.oid\n", - pub_names->data); + pub_names.data); + + /* From version 19, inclusion of sequences in the target is supported */ + if (server_version >= 190000) + appendStringInfo(&cmd, + "UNION ALL\n" + " SELECT DISTINCT s.schemaname, s.sequencename, " CppAsString2(RELKIND_SEQUENCE) "::\"char\" AS relkind, NULL::int2vector AS attrs\n" + " FROM pg_catalog.pg_publication_sequences s\n" + " WHERE s.pubname IN ( %s )", + pub_names.data); } else { - tableRow[2] = NAMEARRAYOID; - appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename \n"); + tableRow[3] = NAMEARRAYOID; + appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename, " CppAsString2(RELKIND_RELATION) "::\"char\" AS relkind \n"); /* Get column lists for each relation if the publisher supports it */ if (check_columnlist) @@ -2260,12 +3155,12 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications) appendStringInfo(&cmd, "FROM pg_catalog.pg_publication_tables t\n" " WHERE t.pubname IN ( %s )", - pub_names->data); + pub_names.data); } - destroyStringInfo(pub_names); + pfree(pub_names.data); - res = walrcv_exec(wrconn, cmd.data, check_columnlist ? 3 : 2, tableRow); + res = walrcv_exec(wrconn, cmd.data, column_count, tableRow); pfree(cmd.data); if (res->status != WALRCV_OK_TUPLES) @@ -2281,22 +3176,28 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications) char *nspname; char *relname; bool isnull; - RangeVar *rv; + char relkind; + PublicationRelKind *relinfo = palloc_object(PublicationRelKind); nspname = TextDatumGetCString(slot_getattr(slot, 1, &isnull)); Assert(!isnull); relname = TextDatumGetCString(slot_getattr(slot, 2, &isnull)); Assert(!isnull); + relkind = DatumGetChar(slot_getattr(slot, 3, &isnull)); + Assert(!isnull); - rv = makeRangeVar(nspname, relname, -1); + relinfo->rv = makeRangeVar(nspname, relname, -1); + relinfo->relkind = relkind; - if (check_columnlist && list_member(tablelist, rv)) + if (relkind != RELKIND_SEQUENCE && + check_columnlist && + list_member_rangevar(relationlist, relinfo->rv)) ereport(ERROR, errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot use different column lists for table \"%s.%s\" in different publications", nspname, relname)); else - tablelist = lappend(tablelist, rv); + relationlist = lappend(relationlist, relinfo); ExecClearTuple(slot); } @@ -2304,7 +3205,7 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications) walrcv_clear_result(res); - return tablelist; + return relationlist; } /* diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 54ad38247aa32..32db5a899dc88 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -3,7 +3,7 @@ * tablecmds.c * Commands for creating and altering table structures and settings * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "access/sysattr.h" #include "access/tableam.h" #include "access/toast_compression.h" +#include "access/tupconvert.h" #include "access/xact.h" #include "access/xlog.h" #include "access/xloginsert.h" @@ -39,9 +40,11 @@ #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" +#include "catalog/pg_extension_d.h" #include "catalog/pg_foreign_table.h" #include "catalog/pg_inherits.h" #include "catalog/pg_largeobject.h" +#include "catalog/pg_largeobject_metadata.h" #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" #include "catalog/pg_policy.h" @@ -55,10 +58,11 @@ #include "catalog/storage.h" #include "catalog/storage_xlog.h" #include "catalog/toasting.h" -#include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" #include "commands/event_trigger.h" +#include "commands/extension.h" +#include "commands/repack.h" #include "commands/sequence.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" @@ -307,6 +311,12 @@ static const struct dropmsgstrings dropmsgstringarray[] = { gettext_noop("index \"%s\" does not exist, skipping"), gettext_noop("\"%s\" is not an index"), gettext_noop("Use DROP INDEX to remove an index.")}, + {RELKIND_PROPGRAPH, + ERRCODE_UNDEFINED_OBJECT, + gettext_noop("property graph \"%s\" does not exist"), + gettext_noop("property graph \"%s\" does not exist, skipping"), + gettext_noop("\"%s\" is not a property graph"), + gettext_noop("Use DROP PROPERTY GRAPH to remove a property graph.")}, {'\0', 0, NULL, NULL, NULL, NULL} }; @@ -357,6 +367,27 @@ typedef enum addFkConstraintSides addFkBothSides, } addFkConstraintSides; +/* + * Hold extension dependencies of one partition index, during + * MERGE/SPLIT PARTITION processing. + * + * collectPartitionIndexExtDeps() builds a list of these entries sorted by + * parentIndexOid with exactly one entry per parent partitioned index; the + * list is then consumed by applyPartitionIndexExtDeps() to re-record the + * same dependencies on the newly created partition's indexes. + * + * extensionOids is kept sorted ascending so that equality checks between + * entries from different partitions can be done in a single pass. + * indexOid is carried only so that conflict errors can cite specific + * partition index names. + */ +typedef struct PartitionIndexExtDepEntry +{ + Oid parentIndexOid; /* OID of the parent partitioned index */ + Oid indexOid; /* OID of a representative partition index */ + List *extensionOids; /* OIDs of dependent extensions, sorted asc */ +} PartitionIndexExtDepEntry; + /* * Partition tables are expected to be dropped when the parent partitioned * table gets dropped. Hence for partitioning we use AUTO dependency. @@ -395,14 +426,18 @@ static ObjectAddress ATExecAlterConstraint(List **wqueue, Relation rel, static bool ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, HeapTuple contuple, bool recurse, LOCKMODE lockmode); -static bool ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, - Oid fkrelid, Oid pkrelid, - HeapTuple contuple, LOCKMODE lockmode, - Oid ReferencedParentDelTrigger, - Oid ReferencedParentUpdTrigger, - Oid ReferencingParentInsTrigger, - Oid ReferencingParentUpdTrigger); +static bool ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, + Oid fkrelid, Oid pkrelid, + HeapTuple contuple, LOCKMODE lockmode, + Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger); +static bool ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, HeapTuple contuple, + bool recurse, bool recursing, + LOCKMODE lockmode); static bool ATExecAlterConstrDeferrability(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, HeapTuple contuple, bool recurse, @@ -413,14 +448,18 @@ static bool ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cm static void AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, bool deferrable, bool initdeferred, List **otherrelids); -static void AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, - Oid fkrelid, Oid pkrelid, - HeapTuple contuple, LOCKMODE lockmode, - Oid ReferencedParentDelTrigger, - Oid ReferencedParentUpdTrigger, - Oid ReferencingParentInsTrigger, - Oid ReferencingParentUpdTrigger); +static void AlterFKConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, + Oid fkrelid, Oid pkrelid, + HeapTuple contuple, LOCKMODE lockmode, + Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger); +static void AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Oid conrelid, + bool recurse, bool recursing, + LOCKMODE lockmode); static void AlterConstrDeferrabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, Relation conrel, Relation tgrel, Relation rel, HeapTuple contuple, bool recurse, @@ -430,8 +469,8 @@ static void AlterConstrUpdateConstraintEntry(ATAlterConstraint *cmdcon, Relation static ObjectAddress ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, bool recurse, bool recursing, LOCKMODE lockmode); -static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, - HeapTuple contuple, LOCKMODE lockmode); +static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, + Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode); static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, char *constrName, HeapTuple contuple, bool recurse, bool recursing, LOCKMODE lockmode); @@ -685,7 +724,7 @@ static void ATExecEnableDisableTrigger(Relation rel, const char *trigname, LOCKMODE lockmode); static void ATExecEnableDisableRule(Relation rel, const char *rulename, char fires_when, LOCKMODE lockmode); -static void ATPrepAddInherit(Relation child_rel); +static void ATPrepChangeInherit(Relation child_rel); static ObjectAddress ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode); static ObjectAddress ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode); static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid, @@ -721,7 +760,6 @@ static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, List *partConstraint, bool validate_default); static void CloneRowTriggersToPartition(Relation parent, Relation partition); -static void DetachAddConstraintIfNeeded(List **wqueue, Relation partRel); static void DropClonedTriggersFromPartition(Oid partitionId); static ObjectAddress ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, RangeVar *name, @@ -740,6 +778,14 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, const char *compression); static char GetAttributeStorage(Oid atttypid, const char *storagemode); +static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context); +static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, + Relation rel, PartitionCmd *cmd, + AlterTableUtilityContext *context); +static List *collectPartitionIndexExtDeps(List *partitionOids); +static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState); +static void freePartitionIndexExtDeps(List *extDepState); /* ---------------------------------------------------------------- * DefineRelation @@ -776,6 +822,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, List *rawDefaults; List *cookedDefaults; List *nncols; + List *connames = NIL; Datum reloptions; ListCell *listptr; AttrNumber attnum; @@ -999,7 +1046,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, Assert(colDef->cooked_default == NULL); - rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); + rawEnt = palloc_object(RawColumnDefault); rawEnt->attnum = attnum; rawEnt->raw_default = colDef->raw_default; rawEnt->generated = colDef->generated; @@ -1009,7 +1056,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, { CookedConstraint *cooked; - cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint)); + cooked = palloc_object(CookedConstraint); cooked->contype = CONSTR_DEFAULT; cooked->conoid = InvalidOid; /* until created */ cooked->name = NULL; @@ -1024,6 +1071,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, } } + TupleDescFinalize(descriptor); + /* * For relations with table AM and partitioned tables, select access * method to use: an explicitly indicated one, or (in the case of a @@ -1297,7 +1346,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, idxstmt = generateClonedIndexStmt(NULL, idxRel, attmap, &constraintOid); - DefineIndex(RelationGetRelid(rel), + DefineIndex(NULL, + RelationGetRelid(rel), idxstmt, InvalidOid, RelationGetRelid(idxRel), @@ -1329,11 +1379,20 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, /* * Now add any newly specified CHECK constraints to the new relation. Same * as for defaults above, but these need to come after partitioning is set - * up. + * up. We save the constraint names that were used, to avoid dupes below. */ if (stmt->constraints) - AddRelationNewConstraints(rel, NIL, stmt->constraints, - true, true, false, queryString); + { + List *conlist; + + conlist = AddRelationNewConstraints(rel, NIL, stmt->constraints, + true, true, false, queryString); + foreach_ptr(CookedConstraint, cons, conlist) + { + if (cons->name != NULL) + connames = lappend(connames, cons->name); + } + } /* * Finally, merge the not-null constraints that are declared directly with @@ -1342,7 +1401,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, * columns that don't yet have it. */ nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints, - old_notnulls); + old_notnulls, connames); foreach_int(attrnum, nncols) set_attnotnull(NULL, rel, attrnum, true, false); @@ -1442,6 +1501,8 @@ BuildDescForRelation(const List *columns) populate_compact_attribute(desc, attnum - 1); } + TupleDescFinalize(desc); + return desc; } @@ -1522,7 +1583,7 @@ DropErrorMsgWrongType(const char *relname, char wrongkind, char rightkind) /* * RemoveRelations * Implements DROP TABLE, DROP INDEX, DROP SEQUENCE, DROP VIEW, - * DROP MATERIALIZED VIEW, DROP FOREIGN TABLE + * DROP MATERIALIZED VIEW, DROP FOREIGN TABLE, DROP PROPERTY GRAPH */ void RemoveRelations(DropStmt *drop) @@ -1586,6 +1647,10 @@ RemoveRelations(DropStmt *drop) relkind = RELKIND_FOREIGN_TABLE; break; + case OBJECT_PROPGRAPH: + relkind = RELKIND_PROPGRAPH; + break; + default: elog(ERROR, "unrecognized drop object type: %d", (int) drop->removeType); @@ -2294,7 +2359,7 @@ ExecuteTruncateGuts(List *explicit_rels, xl_heap_truncate xlrec; int i = 0; - /* should only get here if wal_level >= logical */ + /* should only get here if effective_wal_level is 'logical' */ Assert(XLogLogicalInfoActive()); logrelids = palloc(list_length(relids_logged) * sizeof(Oid)); @@ -2389,12 +2454,15 @@ truncate_check_rel(Oid relid, Form_pg_class reltuple) /* * Most system catalogs can't be truncated at all, or at least not unless * allow_system_table_mods=on. As an exception, however, we allow - * pg_largeobject to be truncated as part of pg_upgrade, because we need - * to change its relfilenode to match the old cluster, and allowing a - * TRUNCATE command to be executed is the easiest way of doing that. + * pg_largeobject and pg_largeobject_metadata to be truncated as part of + * pg_upgrade, because we need to change its relfilenode to match the old + * cluster, and allowing a TRUNCATE command to be executed is the easiest + * way of doing that. */ if (!allowSystemTableMods && IsSystemClass(relid, reltuple) - && (!IsBinaryUpgrade || relid != LargeObjectRelationId)) + && (!IsBinaryUpgrade || + (relid != LargeObjectRelationId && + relid != LargeObjectMetadataRelationId))) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied: \"%s\" is a system catalog", @@ -2711,8 +2779,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence, RelationGetRelationName(relation)))); /* If existing rel is temp, it must belong to this session */ - if (relation->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - !relation->rd_islocaltemp) + if (RELATION_IS_OTHER_TEMP(relation)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg(!is_partition @@ -4189,7 +4256,7 @@ RenameConstraint(RenameStmt *stmt) } /* - * Execute ALTER TABLE/INDEX/SEQUENCE/VIEW/MATERIALIZED VIEW/FOREIGN TABLE + * Execute ALTER TABLE/INDEX/SEQUENCE/VIEW/MATERIALIZED VIEW/FOREIGN TABLE/PROPERTY GRAPH * RENAME */ ObjectAddress @@ -4834,6 +4901,11 @@ AlterTableGetLockLevel(List *cmds) cmd_lockmode = ShareUpdateExclusiveLock; break; + case AT_MergePartitions: + case AT_SplitPartition: + cmd_lockmode = AccessExclusiveLock; + break; + default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -5129,7 +5201,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, case AT_ClusterOn: /* CLUSTER ON */ case AT_DropCluster: /* SET WITHOUT CLUSTER */ ATSimplePermissions(cmd->subtype, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_MATVIEW); + ATT_TABLE | ATT_MATVIEW); /* These commands never recurse */ /* No command-specific prep needed */ pass = AT_PASS_MISC; @@ -5181,16 +5253,16 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, break; case AT_AddInherit: /* INHERIT */ ATSimplePermissions(cmd->subtype, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + ATT_TABLE | ATT_FOREIGN_TABLE); /* This command never recurses */ - ATPrepAddInherit(rel); + ATPrepChangeInherit(rel); pass = AT_PASS_MISC; break; case AT_DropInherit: /* NO INHERIT */ ATSimplePermissions(cmd->subtype, rel, - ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE); + ATT_TABLE | ATT_FOREIGN_TABLE); /* This command never recurses */ - /* No command-specific prep needed */ + ATPrepChangeInherit(rel); pass = AT_PASS_MISC; break; case AT_AlterConstraint: /* ALTER CONSTRAINT */ @@ -5269,6 +5341,12 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* No command-specific prep needed */ pass = AT_PASS_MISC; break; + case AT_MergePartitions: + case AT_SplitPartition: + ATSimplePermissions(cmd->subtype, rel, ATT_PARTITIONED_TABLE); + /* No command-specific prep needed */ + pass = AT_PASS_MISC; + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -5665,6 +5743,22 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, case AT_DetachPartitionFinalize: address = ATExecDetachPartitionFinalize(rel, ((PartitionCmd *) cmd->def)->name); break; + case AT_MergePartitions: + cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode, + cur_pass, context); + Assert(cmd != NULL); + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + ATExecMergePartitions(wqueue, tab, rel, (PartitionCmd *) cmd->def, + context); + break; + case AT_SplitPartition: + cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode, + cur_pass, context); + Assert(cmd != NULL); + Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE); + ATExecSplitPartition(wqueue, tab, rel, (PartitionCmd *) cmd->def, + context); + break; default: /* oops */ elog(ERROR, "unrecognized alter table type: %d", (int) cmd->subtype); @@ -5990,6 +6084,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, finish_heap_swap(tab->relid, OIDNewHeap, false, false, true, !OidIsValid(tab->newTableSpace), + true, /* reindex */ RecentXmin, ReadNextMultiXactId(), persistence); @@ -6127,7 +6222,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) EState *estate; CommandId mycid; BulkInsertState bistate; - int ti_options; + uint32 ti_options; ExprState *partqualstate = NULL; /* @@ -6203,7 +6298,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) NewColumnValue *ex = lfirst(l); /* expr already planned */ - ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL); + ex->exprstate = ExecInitExpr(ex->expr, NULL); } notnull_attrs = notnull_virtual_attrs = NIL; @@ -6343,7 +6438,8 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap) * checking all the constraints. */ snapshot = RegisterSnapshot(GetLatestSnapshot()); - scan = table_beginscan(oldrel, snapshot, 0, NULL); + scan = table_beginscan(oldrel, snapshot, 0, NULL, + SO_NONE); /* * Switch to per-tuple memory context and reset it for each tuple @@ -6566,7 +6662,7 @@ ATGetQueueEntry(List **wqueue, Relation rel) * Not there, so add it. Note that we make a copy of the relation's * existing descriptor before anything interesting can happen to it. */ - tab = (AlteredTableInfo *) palloc0(sizeof(AlteredTableInfo)); + tab = palloc0_object(AlteredTableInfo); tab->relid = relid; tab->rel = NULL; /* set later */ tab->relkind = rel->rd_rel->relkind; @@ -6705,6 +6801,10 @@ alter_table_type_to_string(AlterTableType cmdtype) return "DETACH PARTITION"; case AT_DetachPartitionFinalize: return "DETACH PARTITION ... FINALIZE"; + case AT_MergePartitions: + return "MERGE PARTITIONS"; + case AT_SplitPartition: + return "SPLIT PARTITION"; case AT_AddIdentity: return "ALTER COLUMN ... ADD IDENTITY"; case AT_SetIdentity: @@ -7374,7 +7474,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, /* make sure datatype is legal for a column */ CheckAttributeType(NameStr(attribute->attname), attribute->atttypid, attribute->attcollation, list_make1_oid(rel->rd_rel->reltype), - 0); + (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); InsertPgAttributeTuples(attrdesc, tupdesc, myrelid, NULL, NULL); @@ -7404,7 +7504,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, { RawColumnDefault *rawEnt; - rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); + rawEnt = palloc_object(RawColumnDefault); rawEnt->attnum = attribute->attnum; rawEnt->raw_default = copyObject(colDef->raw_default); rawEnt->generated = colDef->generated; @@ -7429,15 +7529,6 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, * NULL if so, so without any modification of the tuple data we will get * the effect of NULL values in the new column. * - * An exception occurs when the new column is of a domain type: the domain - * might have a not-null constraint, or a check constraint that indirectly - * rejects nulls. If there are any domain constraints then we construct - * an explicit NULL default value that will be passed through - * CoerceToDomain processing. (This is a tad inefficient, since it causes - * rewriting the table which we really wouldn't have to do; but we do it - * to preserve the historical behavior that such a failure will be raised - * only if the table currently contains some rows.) - * * Note: we use build_column_default, and not just the cooked default * returned by AddRelationNewConstraints, so that the right thing happens * when a datatype's default applies. @@ -7456,6 +7547,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, { bool has_domain_constraints; bool has_missing = false; + bool has_volatile = false; /* * For an identity column, we can't use build_column_default(), @@ -7473,8 +7565,18 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, else defval = (Expr *) build_column_default(rel, attribute->attnum); + has_domain_constraints = + DomainHasConstraints(attribute->atttypid, &has_volatile); + + /* + * If the domain has volatile constraints, we must do a table rewrite + * since the constraint result could differ per row and cannot be + * evaluated once and cached as a missing value. + */ + if (has_volatile) + tab->rewrite |= AT_REWRITE_DEFAULT_VAL; + /* Build CoerceToDomain(NULL) expression if needed */ - has_domain_constraints = DomainHasConstraints(attribute->atttypid); if (!defval && has_domain_constraints) { Oid baseTypeId; @@ -7505,7 +7607,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, defval = expression_planner(defval); /* Add the new default to the newvals list */ - newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); + newval = palloc0_object(NewColumnValue); newval->attnum = attribute->attnum; newval->expr = defval; newval->is_generated = (colDef->generated != '\0'); @@ -7516,27 +7618,50 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, * Attempt to skip a complete table rewrite by storing the * specified DEFAULT value outside of the heap. This is only * allowed for plain relations and non-generated columns, and the - * default expression can't be volatile (stable is OK). Note that - * contain_volatile_functions deems CoerceToDomain immutable, but - * here we consider that coercion to a domain with constraints is - * volatile; else it might fail even when the table is empty. + * default expression can't be volatile (stable is OK), and the + * domain constraint expressions can't be volatile (stable is OK). + * + * Note that contain_volatile_functions considers CoerceToDomain + * immutable, so we rely on DomainHasConstraints (called above) + * rather than checking defval alone. + * + * For domains with non-volatile constraints, we evaluate the + * default using soft error handling: if the constraint check + * fails (e.g., CHECK(value > 10) with DEFAULT 8), we fall back to + * a table rewrite. This preserves the historical behavior that + * such a failure is only raised when the table has rows. */ if (rel->rd_rel->relkind == RELKIND_RELATION && !colDef->generated && - !has_domain_constraints && + !has_volatile && !contain_volatile_functions((Node *) defval)) { EState *estate; ExprState *exprState; Datum missingval; bool missingIsNull; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - /* Evaluate the default expression */ + /* Evaluate the default expression with soft errors */ estate = CreateExecutorState(); - exprState = ExecPrepareExpr(defval, estate); + exprState = ExecPrepareExprWithContext(defval, estate, + (Node *) &escontext); missingval = ExecEvalExpr(exprState, GetPerTupleExprContext(estate), &missingIsNull); + + /* + * If the domain constraint check failed (via errsave), + * missingval is unreliable. Fall back to a table rewrite; + * Phase 3 will re-evaluate with hard errors, so the user gets + * an error only if the table has rows. + */ + if (escontext.error_occurred) + { + missingIsNull = true; + tab->rewrite |= AT_REWRITE_DEFAULT_VAL; + } + /* If it turns out NULL, nothing to do; else store it */ if (!missingIsNull) { @@ -8040,12 +8165,12 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName, ccon = linitial(cooked); ObjectAddressSet(address, ConstraintRelationId, ccon->conoid); - InvokeObjectPostAlterHook(RelationRelationId, - RelationGetRelid(rel), attnum); - /* Mark pg_attribute.attnotnull for the column and queue validation */ set_attnotnull(wqueue, rel, attnum, true, true); + InvokeObjectPostAlterHook(RelationRelationId, + RelationGetRelid(rel), attnum); + /* * Recurse to propagate the constraint to children that don't have one. */ @@ -8174,7 +8299,7 @@ ATExecColumnDefault(Relation rel, const char *colName, /* SET DEFAULT */ RawColumnDefault *rawEnt; - rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); + rawEnt = palloc_object(RawColumnDefault); rawEnt->attnum = attnum; rawEnt->raw_default = newDefault; rawEnt->generated = '\0'; @@ -8279,6 +8404,31 @@ ATExecAddIdentity(Relation rel, const char *colName, errmsg("column \"%s\" of relation \"%s\" must be declared NOT NULL before identity can be added", colName, RelationGetRelationName(rel)))); + /* + * On the other hand, if a not-null constraint exists, then verify that + * it's compatible. + */ + if (attTup->attnotnull) + { + HeapTuple contup; + Form_pg_constraint conForm; + + contup = findNotNullConstraintAttnum(RelationGetRelid(rel), + attnum); + if (!HeapTupleIsValid(contup)) + elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"", + colName, RelationGetRelationName(rel)); + + conForm = (Form_pg_constraint) GETSTRUCT(contup); + if (!conForm->convalidated) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("incompatible NOT VALID constraint \"%s\" on relation \"%s\"", + NameStr(conForm->conname), RelationGetRelationName(rel)), + errhint("You might need to validate it using %s.", + "ALTER TABLE ... VALIDATE CONSTRAINT")); + } + if (attTup->attidentity) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), @@ -8601,18 +8751,6 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, errmsg("column \"%s\" of relation \"%s\" is not a generated column", colName, RelationGetRelationName(rel)))); - /* - * TODO: This could be done, just need to recheck any constraints - * afterwards. - */ - if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && - rel->rd_att->constr && rel->rd_att->constr->num_check > 0) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints"), - errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.", - colName, RelationGetRelationName(rel)))); - if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && attTup->attnotnull) tab->verify_new_notnull = true; @@ -8624,10 +8762,10 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, * expressions. */ if (attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && - GetRelationPublications(RelationGetRelid(rel)) != NIL) + GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication"), + errmsg("ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication"), errdetail("Column \"%s\" of relation \"%s\" is a virtual generated column.", colName, RelationGetRelationName(rel)))); @@ -8645,15 +8783,14 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, /* make sure we don't conflict with later attribute modifications */ CommandCounterIncrement(); - - /* - * Find everything that depends on the column (constraints, indexes, - * etc), and record enough information to let us recreate the objects - * after rewrite. - */ - RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName); } + /* + * Find everything that depends on the column (constraints, indexes, etc), + * and record enough information to let us recreate the objects. + */ + RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName); + /* * Drop the dependency records of the GENERATED expression, in particular * its INTERNAL dependency on the column, which would otherwise cause @@ -8677,7 +8814,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, false, false); /* Prepare to store the new expression, in the catalogs */ - rawEnt = (RawColumnDefault *) palloc(sizeof(RawColumnDefault)); + rawEnt = palloc_object(RawColumnDefault); rawEnt->attnum = attnum; rawEnt->raw_default = newExpr; rawEnt->generated = attgenerated; @@ -8694,7 +8831,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName, /* Prepare for table rewrite */ defval = (Expr *) build_column_default(rel, attnum); - newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); + newval = palloc0_object(NewColumnValue); newval->attnum = attnum; newval->expr = expression_planner(defval); newval->is_generated = true; @@ -8986,7 +9123,7 @@ ATExecSetStatistics(Relation rel, const char *colName, int16 colNum, Node *newVa memset(repl_null, false, sizeof(repl_null)); memset(repl_repl, false, sizeof(repl_repl)); if (!newtarget_default) - repl_val[Anum_pg_attribute_attstattarget - 1] = newtarget; + repl_val[Anum_pg_attribute_attstattarget - 1] = Int16GetDatum(newtarget); else repl_null[Anum_pg_attribute_attstattarget - 1] = true; repl_repl[Anum_pg_attribute_attstattarget - 1] = true; @@ -9603,7 +9740,8 @@ ATExecAddIndex(AlteredTableInfo *tab, Relation rel, /* suppress notices when rebuilding existing index */ quiet = is_rebuild; - address = DefineIndex(RelationGetRelid(rel), + address = DefineIndex(NULL, + RelationGetRelid(rel), stmt, InvalidOid, /* no predefined OID */ InvalidOid, /* no parent index */ @@ -9655,7 +9793,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel, /* The CreateStatsStmt has already been through transformStatsStmt */ Assert(stmt->transformed); - address = CreateStatistics(stmt); + address = CreateStatistics(stmt, !is_rebuild); return address; } @@ -9676,7 +9814,7 @@ ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel, char *constraintName; char constraintType; ObjectAddress address; - bits16 flags; + uint16 flags; Assert(IsA(stmt, IndexStmt)); Assert(OidIsValid(index_oid)); @@ -9922,7 +10060,7 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, { NewConstraint *newcon; - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon = palloc0_object(NewConstraint); newcon->name = ccon->name; newcon->contype = ccon->contype; newcon->qual = ccon->expr; @@ -10189,7 +10327,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, if (pk_has_without_overlaps && !with_period) ereport(ERROR, errcode(ERRCODE_INVALID_FOREIGN_KEY), - errmsg("foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS")); + errmsg("foreign key must use PERIOD when referencing a primary key using WITHOUT OVERLAPS")); /* * Now we can check permissions. @@ -10330,8 +10468,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, for_overlaps ? errmsg("could not identify an overlaps operator for foreign key") : errmsg("could not identify an equality operator for foreign key"), - errdetail("Could not translate compare type %d for operator family \"%s\", input type %s, access method \"%s\".", - cmptype, get_opfamily_name(opfamily, false), format_type_be(opcintype), get_am_name(amid))); + errdetail("Could not translate compare type %d for operator family \"%s\" of access method \"%s\".", + cmptype, get_opfamily_name(opfamily, false), get_am_name(amid))); /* * There had better be a primary equality operator for the index. @@ -10919,7 +11057,7 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel, false); if (map) { - mapped_pkattnum = palloc(sizeof(AttrNumber) * numfks); + mapped_pkattnum = palloc_array(AttrNumber, numfks); for (int j = 0; j < numfks; j++) mapped_pkattnum[j] = map->attnums[pkattnum[j] - 1]; } @@ -11054,7 +11192,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, tab = ATGetQueueEntry(wqueue, rel); - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon = palloc0_object(NewConstraint); newcon->name = get_constraint_name(parentConstr); newcon->contype = CONSTR_FOREIGN; newcon->refrelid = RelationGetRelid(pkrel); @@ -11858,6 +11996,7 @@ AttachPartitionForeignKey(List **wqueue, if (queueValidation) { Relation conrel; + Oid confrelid; conrel = table_open(ConstraintRelationId, RowExclusiveLock); @@ -11865,9 +12004,11 @@ AttachPartitionForeignKey(List **wqueue, if (!HeapTupleIsValid(partcontup)) elog(ERROR, "cache lookup failed for constraint %u", partConstrOid); + confrelid = ((Form_pg_constraint) GETSTRUCT(partcontup))->confrelid; + /* Use the same lock as for AT_ValidateConstraint */ - QueueFKConstraintValidation(wqueue, conrel, partition, partcontup, - ShareUpdateExclusiveLock); + QueueFKConstraintValidation(wqueue, conrel, partition, confrelid, + partcontup, ShareUpdateExclusiveLock); ReleaseSysCache(partcontup); table_close(conrel, RowExclusiveLock); } @@ -12151,7 +12292,7 @@ GetForeignKeyCheckTriggers(Relation trigrel, * * Update the attributes of a constraint. * - * Currently only works for Foreign Key and not null constraints. + * Currently works for Foreign Key, Check, and not null constraints. * * If the constraint is modified, returns its address; otherwise, return * InvalidObjectAddress. @@ -12213,11 +12354,13 @@ ATExecAlterConstraint(List **wqueue, Relation rel, ATAlterConstraint *cmdcon, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key constraint", cmdcon->conname, RelationGetRelationName(rel)))); - if (cmdcon->alterEnforceability && currcon->contype != CONSTRAINT_FOREIGN) + if (cmdcon->alterEnforceability && + (currcon->contype != CONSTRAINT_FOREIGN && currcon->contype != CONSTRAINT_CHECK)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot alter enforceability of constraint \"%s\" of relation \"%s\"", - cmdcon->conname, RelationGetRelationName(rel)))); + cmdcon->conname, RelationGetRelationName(rel)), + errhint("Only foreign key and check constraints can change enforceability."))); if (cmdcon->alterInheritability && currcon->contype != CONSTRAINT_NOTNULL) ereport(ERROR, @@ -12319,17 +12462,24 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, * enforceability, we don't need to explicitly update multiple entries in * pg_trigger related to deferrability. * - * Modifying enforceability involves either creating or dropping the - * trigger, during which the deferrability setting will be adjusted - * automatically. + * Modifying foreign key enforceability involves either creating or + * dropping the trigger, during which the deferrability setting will be + * adjusted automatically. */ - if (cmdcon->alterEnforceability && - ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, - currcon->conrelid, currcon->confrelid, - contuple, lockmode, InvalidOid, - InvalidOid, InvalidOid, InvalidOid)) - changed = true; - + if (cmdcon->alterEnforceability) + { + if (currcon->contype == CONSTRAINT_FOREIGN) + changed = ATExecAlterFKConstrEnforceability(wqueue, cmdcon, conrel, tgrel, + currcon->conrelid, + currcon->confrelid, + contuple, lockmode, + InvalidOid, InvalidOid, + InvalidOid, InvalidOid); + else if (currcon->contype == CONSTRAINT_CHECK) + changed = ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, + contuple, recurse, false, + lockmode); + } else if (cmdcon->alterDeferrability && ATExecAlterConstrDeferrability(wqueue, cmdcon, conrel, tgrel, rel, contuple, recurse, &otherrelids, @@ -12359,7 +12509,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, } /* - * Returns true if the constraint's enforceability is altered. + * Returns true if the foreign key constraint's enforceability is altered. * * Depending on whether the constraint is being set to ENFORCED or NOT * ENFORCED, it creates or drops the trigger accordingly. @@ -12371,14 +12521,14 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon, * enforced, as descendant constraints cannot be different in that case. */ static bool -ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, - Oid fkrelid, Oid pkrelid, - HeapTuple contuple, LOCKMODE lockmode, - Oid ReferencedParentDelTrigger, - Oid ReferencedParentUpdTrigger, - Oid ReferencingParentInsTrigger, - Oid ReferencingParentUpdTrigger) +ATExecAlterFKConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, + Oid fkrelid, Oid pkrelid, + HeapTuple contuple, LOCKMODE lockmode, + Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) { Form_pg_constraint currcon; Oid conoid; @@ -12414,10 +12564,10 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) - AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, - fkrelid, pkrelid, contuple, - lockmode, InvalidOid, InvalidOid, - InvalidOid, InvalidOid); + AlterFKConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, + fkrelid, pkrelid, contuple, + lockmode, InvalidOid, InvalidOid, + InvalidOid, InvalidOid); /* Drop all the triggers */ DropForeignKeyConstraintTriggers(tgrel, conoid, InvalidOid, InvalidOid); @@ -12436,6 +12586,8 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, fkconstraint->fk_matchtype = currcon->confmatchtype; fkconstraint->fk_upd_action = currcon->confupdtype; fkconstraint->fk_del_action = currcon->confdeltype; + fkconstraint->deferrable = currcon->condeferrable; + fkconstraint->initdeferred = currcon->condeferred; /* Create referenced triggers */ if (currcon->conrelid == fkrelid) @@ -12463,14 +12615,17 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, /* * Tell Phase 3 to check that the constraint is satisfied by existing - * rows. + * rows. Only applies to leaf partitions, and (for constraints that + * reference a partitioned table) only if this is not one of the + * pg_constraint rows that exist solely to support action triggers. */ - if (rel->rd_rel->relkind == RELKIND_RELATION) + if (rel->rd_rel->relkind == RELKIND_RELATION && + currcon->confrelid == pkrelid) { AlteredTableInfo *tab; NewConstraint *newcon; - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon = palloc0_object(NewConstraint); newcon->name = fkconstraint->conname; newcon->contype = CONSTR_FOREIGN; newcon->refrelid = currcon->confrelid; @@ -12490,12 +12645,122 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, */ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || get_rel_relkind(currcon->confrelid) == RELKIND_PARTITIONED_TABLE) - AlterConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, - fkrelid, pkrelid, contuple, - lockmode, ReferencedDelTriggerOid, - ReferencedUpdTriggerOid, - ReferencingInsTriggerOid, - ReferencingUpdTriggerOid); + AlterFKConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, tgrel, + fkrelid, pkrelid, contuple, + lockmode, + ReferencedDelTriggerOid, + ReferencedUpdTriggerOid, + ReferencingInsTriggerOid, + ReferencingUpdTriggerOid); + } + + table_close(rel, NoLock); + + return changed; +} + +/* + * Returns true if the CHECK constraint's enforceability is altered. + */ +static bool +ATExecAlterCheckConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, HeapTuple contuple, + bool recurse, bool recursing, LOCKMODE lockmode) +{ + Form_pg_constraint currcon; + Relation rel; + bool changed = false; + List *children = NIL; + + /* Since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + Assert(cmdcon->alterEnforceability); + + currcon = (Form_pg_constraint) GETSTRUCT(contuple); + + Assert(currcon->contype == CONSTRAINT_CHECK); + + /* + * Parent relation already locked by caller, children will be locked by + * find_all_inheritors. So NoLock is fine here. + */ + rel = table_open(currcon->conrelid, NoLock); + + if (currcon->conenforced != cmdcon->is_enforced) + { + AlterConstrUpdateConstraintEntry(cmdcon, conrel, contuple); + changed = true; + } + + /* + * Note that we must recurse even when trying to change a check constraint + * to not enforced if it is already not enforced, in case descendant + * constraints might be enforced and need to be changed to not enforced. + * Conversely, we should do nothing if a constraint is being set to + * enforced and is already enforced, as descendant constraints cannot be + * different in that case. + */ + if (!cmdcon->is_enforced || changed) + { + /* + * If we're recursing, the parent has already done this, so skip it. + * Also, if the constraint is a NO INHERIT constraint, we shouldn't + * try to look for it in the children. + */ + if (!recursing && !currcon->connoinherit) + children = find_all_inheritors(RelationGetRelid(rel), + lockmode, NULL); + + foreach_oid(childoid, children) + { + if (childoid == RelationGetRelid(rel)) + continue; + + /* + * If we are told not to recurse, there had better not be any + * child tables, because we can't change constraint enforceability + * on the parent unless we have changed enforceability for all + * child. + */ + if (!recurse) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraint must be altered on child tables too"), + errhint("Do not specify the ONLY keyword.")); + + AlterCheckConstrEnforceabilityRecurse(wqueue, cmdcon, conrel, + childoid, false, true, + lockmode); + } + } + + /* + * Tell Phase 3 to check that the constraint is satisfied by existing + * rows. We only need do this when altering the constraint from NOT + * ENFORCED to ENFORCED. + */ + if (rel->rd_rel->relkind == RELKIND_RELATION && + !currcon->conenforced && + cmdcon->is_enforced) + { + AlteredTableInfo *tab; + NewConstraint *newcon; + Datum val; + char *conbin; + + newcon = palloc0_object(NewConstraint); + newcon->name = pstrdup(NameStr(currcon->conname)); + newcon->contype = CONSTR_CHECK; + + val = SysCacheGetAttrNotNull(CONSTROID, contuple, + Anum_pg_constraint_conbin); + conbin = TextDatumGetCString(val); + newcon->qual = expand_generated_columns_in_expr(stringToNode(conbin), rel, 1); + + /* Find or create work queue entry for this table */ + tab = ATGetQueueEntry(wqueue, rel); + tab->constraints = lappend(tab->constraints, newcon); } table_close(rel, NoLock); @@ -12503,6 +12768,54 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon, return changed; } +/* + * Invokes ATExecAlterCheckConstrEnforceability for each CHECK constraint that + * is a child of the specified constraint. + * + * We rely on the parent and child tables having identical CHECK constraint + * names to retrieve the child's pg_constraint tuple. + * + * The arguments to this function have the same meaning as the arguments to + * ATExecAlterCheckConstrEnforceability. + */ +static void +AlterCheckConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Oid conrelid, + bool recurse, bool recursing, + LOCKMODE lockmode) +{ + SysScanDesc pscan; + HeapTuple childtup; + ScanKeyData skey[3]; + + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(conrelid)); + ScanKeyInit(&skey[1], + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + ScanKeyInit(&skey[2], + Anum_pg_constraint_conname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(cmdcon->conname)); + + pscan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId, true, + NULL, 3, skey); + + if (!HeapTupleIsValid(childtup = systable_getnext(pscan))) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("constraint \"%s\" of relation \"%s\" does not exist", + cmdcon->conname, get_rel_name(conrelid))); + + ATExecAlterCheckConstrEnforceability(wqueue, cmdcon, conrel, childtup, + recurse, recursing, lockmode); + + systable_endscan(pscan); +} + /* * Returns true if the constraint's deferrability is altered. * @@ -12708,25 +13021,25 @@ AlterConstrTriggerDeferrability(Oid conoid, Relation tgrel, Relation rel, } /* - * Invokes ATExecAlterConstrEnforceability for each constraint that is a child of - * the specified constraint. + * Invokes ATExecAlterFKConstrEnforceability for each foreign key constraint + * that is a child of the specified constraint. * * Note that this doesn't handle recursion the normal way, viz. by scanning the * list of child relations and recursing; instead it uses the conparentid * relationships. This may need to be reconsidered. * * The arguments to this function have the same meaning as the arguments to - * ATExecAlterConstrEnforceability. + * ATExecAlterFKConstrEnforceability. */ static void -AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, - Relation conrel, Relation tgrel, - Oid fkrelid, Oid pkrelid, - HeapTuple contuple, LOCKMODE lockmode, - Oid ReferencedParentDelTrigger, - Oid ReferencedParentUpdTrigger, - Oid ReferencingParentInsTrigger, - Oid ReferencingParentUpdTrigger) +AlterFKConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, + Relation conrel, Relation tgrel, + Oid fkrelid, Oid pkrelid, + HeapTuple contuple, LOCKMODE lockmode, + Oid ReferencedParentDelTrigger, + Oid ReferencedParentUpdTrigger, + Oid ReferencingParentInsTrigger, + Oid ReferencingParentUpdTrigger) { Form_pg_constraint currcon; Oid conoid; @@ -12746,12 +13059,12 @@ AlterConstrEnforceabilityRecurse(List **wqueue, ATAlterConstraint *cmdcon, true, NULL, 1, &pkey); while (HeapTupleIsValid(childtup = systable_getnext(pscan))) - ATExecAlterConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, - pkrelid, childtup, lockmode, - ReferencedParentDelTrigger, - ReferencedParentUpdTrigger, - ReferencingParentInsTrigger, - ReferencingParentUpdTrigger); + ATExecAlterFKConstrEnforceability(wqueue, cmdcon, conrel, tgrel, fkrelid, + pkrelid, childtup, lockmode, + ReferencedParentDelTrigger, + ReferencedParentUpdTrigger, + ReferencingParentInsTrigger, + ReferencingParentUpdTrigger); systable_endscan(pscan); } @@ -12907,8 +13220,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, con->contype != CONSTRAINT_NOTNULL) ereport(ERROR, errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key, check, or not-null constraint", - constrName, RelationGetRelationName(rel))); + errmsg("cannot validate constraint \"%s\" of relation \"%s\"", + constrName, RelationGetRelationName(rel)), + errdetail("This operation is not supported for this type of constraint.")); if (!con->conenforced) ereport(ERROR, @@ -12919,7 +13233,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, { if (con->contype == CONSTRAINT_FOREIGN) { - QueueFKConstraintValidation(wqueue, conrel, rel, tuple, lockmode); + QueueFKConstraintValidation(wqueue, conrel, rel, con->confrelid, + tuple, lockmode); } else if (con->contype == CONSTRAINT_CHECK) { @@ -12952,8 +13267,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, * for the specified relation and all its children. */ static void -QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, - HeapTuple contuple, LOCKMODE lockmode) +QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, + Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode) { Form_pg_constraint con; AlteredTableInfo *tab; @@ -12964,7 +13279,17 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, Assert(con->contype == CONSTRAINT_FOREIGN); Assert(!con->convalidated); - if (rel->rd_rel->relkind == RELKIND_RELATION) + /* + * Add the validation to phase 3's queue; not needed for partitioned + * tables themselves, only for their partitions. + * + * When the referenced table (pkrelid) is partitioned, the referencing + * table (fkrel) has one pg_constraint row pointing to each partition + * thereof. These rows are there only to support action triggers and no + * table scan is needed, therefore skip this for them as well. + */ + if (fkrel->rd_rel->relkind == RELKIND_RELATION && + con->confrelid == pkrelid) { NewConstraint *newcon; Constraint *fkconstraint; @@ -12974,7 +13299,7 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, /* for now this is all we need */ fkconstraint->conname = pstrdup(NameStr(con->conname)); - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon = palloc0_object(NewConstraint); newcon->name = fkconstraint->conname; newcon->contype = CONSTR_FOREIGN; newcon->refrelid = con->confrelid; @@ -12983,15 +13308,16 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, newcon->qual = (Node *) fkconstraint; /* Find or create work queue entry for this table */ - tab = ATGetQueueEntry(wqueue, rel); + tab = ATGetQueueEntry(wqueue, fkrel); tab->constraints = lappend(tab->constraints, newcon); } /* * If the table at either end of the constraint is partitioned, we need to - * recurse and handle every constraint that is a child of this constraint. + * recurse and handle every unvalidated constraint that is a child of this + * constraint. */ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || + if (fkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE || get_rel_relkind(con->confrelid) == RELKIND_PARTITIONED_TABLE) { ScanKeyData pkey; @@ -13023,8 +13349,12 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, childrel = table_open(childcon->conrelid, lockmode); - QueueFKConstraintValidation(wqueue, conrel, childrel, childtup, - lockmode); + /* + * NB: Note that pkrelid should be passed as-is during recursion, + * as it is required to identify the root referenced table. + */ + QueueFKConstraintValidation(wqueue, conrel, childrel, pkrelid, + childtup, lockmode); table_close(childrel, NoLock); } @@ -13032,7 +13362,11 @@ QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation rel, } /* - * Now update the catalog, while we have the door open. + * Now mark the pg_constraint row as validated (even if we didn't check, + * notably the ones for partitions on the referenced side). + * + * We rely on transaction abort to roll back this change if phase 3 + * ultimately finds violating rows. This is a bit ugly. */ copyTuple = heap_copytuple(contuple); copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); @@ -13113,7 +13447,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel, } /* Queue validation for phase 3 */ - newcon = (NewConstraint *) palloc0(sizeof(NewConstraint)); + newcon = palloc0_object(NewConstraint); newcon->name = constrName; newcon->contype = CONSTR_CHECK; newcon->refrelid = InvalidOid; @@ -13676,8 +14010,8 @@ validateForeignKeyConstraint(char *conname, */ snapshot = RegisterSnapshot(GetLatestSnapshot()); slot = table_slot_create(rel, NULL); - scan = table_beginscan(rel, snapshot, 0, NULL); - + scan = table_beginscan(rel, snapshot, 0, NULL, + SO_NONE); perTupCxt = AllocSetContextCreate(CurrentMemoryContext, "validateForeignKeyConstraint", ALLOCSET_SMALL_SIZES); @@ -14400,7 +14734,7 @@ ATPrepAlterColumnType(List **wqueue, /* make sure datatype is legal for a column */ CheckAttributeType(colName, targettype, targetcollid, list_make1_oid(rel->rd_rel->reltype), - 0); + (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL ? CHKATYPE_IS_VIRTUAL : 0)); if (attTup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) { @@ -14458,6 +14792,9 @@ ATPrepAlterColumnType(List **wqueue, /* Fix collations after all else */ assign_expr_collations(pstate, transform); + /* Expand virtual generated columns in the expr. */ + transform = expand_generated_columns_in_expr(transform, rel, 1); + /* Plan the expr now so we can accurately assess the need to rewrite. */ transform = (Node *) expression_planner((Expr *) transform); @@ -14465,7 +14802,7 @@ ATPrepAlterColumnType(List **wqueue, * Add a work queue item to make ATRewriteTable update the column * contents. */ - newval = (NewColumnValue *) palloc0(sizeof(NewColumnValue)); + newval = palloc0_object(NewColumnValue); newval->attnum = attnum; newval->expr = (Expr *) transform; newval->is_generated = false; @@ -14626,7 +14963,7 @@ ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno) { CoerceToDomain *d = (CoerceToDomain *) expr; - if (DomainHasConstraints(d->resulttype)) + if (DomainHasConstraints(d->resulttype, NULL)) return true; expr = (Node *) d->arg; } @@ -15385,9 +15722,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) /* * Re-parse the index and constraint definitions, and attach them to the * appropriate work queue entries. We do this before dropping because in - * the case of a FOREIGN KEY constraint, we might not yet have exclusive - * lock on the table the constraint is attached to, and we need to get - * that before reparsing/dropping. + * the case of a constraint on another table, we might not yet have + * exclusive lock on the table the constraint is attached to, and we need + * to get that before reparsing/dropping. (That's possible at least for + * FOREIGN KEY, CHECK, and EXCLUSION constraints; in non-FK cases it + * requires a dependency on the target table's composite type in the other + * table's constraint expressions.) * * We can't rely on the output of deparsing to tell us which relation to * operate on, because concurrent activity might have made the name @@ -15403,7 +15743,6 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) Form_pg_constraint con; Oid relid; Oid confrelid; - char contype; bool conislocal; tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(oldId)); @@ -15420,7 +15759,6 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) elog(ERROR, "could not identify relation associated with constraint %u", oldId); } confrelid = con->confrelid; - contype = con->contype; conislocal = con->conislocal; ReleaseSysCache(tup); @@ -15438,12 +15776,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) continue; /* - * When rebuilding an FK constraint that references the table we're - * modifying, we might not yet have any lock on the FK's table, so get - * one now. We'll need AccessExclusiveLock for the DROP CONSTRAINT - * step, so there's no value in asking for anything weaker. + * When rebuilding another table's constraint that references the + * table we're modifying, we might not yet have any lock on the other + * table, so get one now. We'll need AccessExclusiveLock for the DROP + * CONSTRAINT step, so there's no value in asking for anything weaker. */ - if (relid != tab->relid && contype == CONSTRAINT_FOREIGN) + if (relid != tab->relid) LockRelationOid(relid, AccessExclusiveLock); ATPostAlterTypeParse(oldId, relid, confrelid, @@ -15457,6 +15795,14 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) Oid relid; relid = IndexGetRelation(oldId, false); + + /* + * As above, make sure we have lock on the index's table if it's not + * the same table. + */ + if (relid != tab->relid) + LockRelationOid(relid, AccessExclusiveLock); + ATPostAlterTypeParse(oldId, relid, InvalidOid, (char *) lfirst(def_item), wqueue, lockmode, tab->rewrite); @@ -15473,6 +15819,20 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode) Oid relid; relid = StatisticsGetRelation(oldId, false); + + /* + * As above, make sure we have lock on the statistics object's table + * if it's not the same table. However, we take + * ShareUpdateExclusiveLock here, aligning with the lock level used in + * CreateStatistics and RemoveStatisticsById. + * + * CAUTION: this should be done after all cases that grab + * AccessExclusiveLock, else we risk causing deadlock due to needing + * to promote our table lock. + */ + if (relid != tab->relid) + LockRelationOid(relid, ShareUpdateExclusiveLock); + ATPostAlterTypeParse(oldId, relid, InvalidOid, (char *) lfirst(def_item), wqueue, lockmode, tab->rewrite); @@ -15696,7 +16056,7 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, { AlterDomainStmt *stmt = (AlterDomainStmt *) stm; - if (stmt->subtype == 'C') /* ADD CONSTRAINT */ + if (stmt->subtype == AD_AddConstraint) { Constraint *con = castNode(Constraint, stmt->def); AlterTableCmd *cmd = makeNode(AlterTableCmd); @@ -15939,7 +16299,7 @@ ATExecAlterColumnGenericOptions(Relation rel, options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(datum))) + if (DatumGetPointer(datum) != NULL) repl_val[Anum_pg_attribute_attfdwoptions - 1] = datum; else repl_null[Anum_pg_attribute_attfdwoptions - 1] = true; @@ -16010,6 +16370,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock case RELKIND_MATVIEW: case RELKIND_FOREIGN_TABLE: case RELKIND_PARTITIONED_TABLE: + case RELKIND_PROPGRAPH: /* ok to change owner */ break; case RELKIND_INDEX: @@ -16074,7 +16435,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock case RELKIND_TOASTVALUE: if (recursing) break; - /* FALL THRU */ + pg_fallthrough; default: ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -17141,14 +17502,12 @@ ATExecEnableDisableRule(Relation rel, const char *rulename, } /* - * ALTER TABLE INHERIT + * Preparation phase of [NO] INHERIT * - * Add a parent to the child's parents. This verifies that all the columns and - * check constraints of the parent appear in the child and that they have the - * same data types and expressions. + * Check the relation defined as a child. */ static void -ATPrepAddInherit(Relation child_rel) +ATPrepChangeInherit(Relation child_rel) { if (child_rel->rd_rel->reloftype) ereport(ERROR, @@ -17159,14 +17518,11 @@ ATPrepAddInherit(Relation child_rel) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot change inheritance of a partition"))); - - if (child_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of partitioned table"))); } /* + * ALTER TABLE INHERIT + * * Return the address of the new parent relation. */ static ObjectAddress @@ -17199,15 +17555,13 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) RelationGetRelationName(parent_rel)))); /* If parent rel is temp, it must belong to this session */ - if (parent_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - !parent_rel->rd_islocaltemp) + if (RELATION_IS_OTHER_TEMP(parent_rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot inherit from temporary relation of another session"))); /* Ditto for the child */ - if (child_rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - !child_rel->rd_islocaltemp) + if (RELATION_IS_OTHER_TEMP(child_rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot inherit to temporary relation of another session"))); @@ -17280,6 +17634,9 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode) * Catalog manipulation portion of creating inheritance between a child * table and a parent table. * + * This verifies that all the columns and check constraints of the parent + * appear in the child and that they have the same data types and expressions. + * * Common to ATExecAddInherit() and ATExecAttachPartition(). */ static void @@ -17739,11 +18096,6 @@ ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode) ObjectAddress address; Relation parent_rel; - if (rel->rd_rel->relispartition) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot change inheritance of a partition"))); - /* * AccessShareLock on the parent is probably enough, seeing that DROP * TABLE doesn't lock parent tables at all. We need some lock since we'll @@ -18619,7 +18971,7 @@ ATExecGenericOptions(Relation rel, List *options) options, fdw->fdwvalidator); - if (PointerIsValid(DatumGetPointer(datum))) + if (DatumGetPointer(datum) != NULL) repl_val[Anum_pg_foreign_table_ftoptions - 1] = datum; else repl_null[Anum_pg_foreign_table_ftoptions - 1] = true; @@ -18768,7 +19120,7 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged) * UNLOGGED, as UNLOGGED tables can't be published. */ if (!toLogged && - GetRelationPublications(RelationGetRelid(rel)) != NIL) + GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot change table \"%s\" to unlogged because it is part of a publication", @@ -19184,7 +19536,7 @@ register_on_commit_action(Oid relid, OnCommitAction action) oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - oc = (OnCommitItem *) palloc(sizeof(OnCommitItem)); + oc = palloc_object(OnCommitItem); oc->relid = relid; oc->oncommit = action; oc->creating_subid = GetCurrentSubTransactionId(); @@ -19580,6 +19932,11 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("\"%s\" is not a composite type", rv->relname))); + if (reltype == OBJECT_PROPGRAPH && relkind != RELKIND_PROPGRAPH) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a property graph", rv->relname))); + if (reltype == OBJECT_INDEX && relkind != RELKIND_INDEX && relkind != RELKIND_PARTITIONED_INDEX && !IsA(stmt, RenameStmt)) @@ -19757,6 +20114,8 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu /* Expression */ Node *expr = pelem->expr; char partattname[16]; + Bitmapset *expr_attrs = NULL; + int i; Assert(expr != NULL); atttype = exprType(expr); @@ -19780,43 +20139,36 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu while (IsA(expr, CollateExpr)) expr = (Node *) ((CollateExpr *) expr)->arg; - if (IsA(expr, Var) && - ((Var *) expr)->varattno > 0) + /* + * Examine all the columns in the partition key expression. When + * the whole-row reference is present, examine all the columns of + * the partitioned table. + */ + pull_varattnos(expr, 1, &expr_attrs); + if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, expr_attrs)) { - /* - * User wrote "(column)" or "(column COLLATE something)". - * Treat it like simple attribute anyway. - */ - partattrs[attn] = ((Var *) expr)->varattno; + expr_attrs = bms_add_range(expr_attrs, + 1 - FirstLowInvalidHeapAttributeNumber, + RelationGetNumberOfAttributes(rel) - FirstLowInvalidHeapAttributeNumber); + expr_attrs = bms_del_member(expr_attrs, 0 - FirstLowInvalidHeapAttributeNumber); } - else - { - Bitmapset *expr_attrs = NULL; - int i; - partattrs[attn] = 0; /* marks the column as expression */ - *partexprs = lappend(*partexprs, expr); + i = -1; + while ((i = bms_next_member(expr_attrs, i)) >= 0) + { + AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; - /* - * transformPartitionSpec() should have already rejected - * subqueries, aggregates, window functions, and SRFs, based - * on the EXPR_KIND_ for partition expressions. - */ + Assert(attno != 0); /* * Cannot allow system column references, since that would * make partition routing impossible: their values won't be * known yet when we need to do that. */ - pull_varattnos(expr, 1, &expr_attrs); - for (i = FirstLowInvalidHeapAttributeNumber; i < 0; i++) - { - if (bms_is_member(i - FirstLowInvalidHeapAttributeNumber, - expr_attrs)) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("partition key expressions cannot contain system column references"))); - } + if (attno < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("partition key expressions cannot contain system column references"))); /* * Stored generated columns cannot work: They are computed @@ -19826,20 +20178,35 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu * SET EXPRESSION would need to check whether the column is * used in partition keys). Seems safer to prohibit for now. */ - i = -1; - while ((i = bms_next_member(expr_attrs, i)) >= 0) - { - AttrNumber attno = i + FirstLowInvalidHeapAttributeNumber; + if (TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use generated column in partition key"), + errdetail("Column \"%s\" is a generated column.", + get_attname(RelationGetRelid(rel), attno, false)), + parser_errposition(pstate, pelem->location))); + } - if (attno > 0 && - TupleDescAttr(RelationGetDescr(rel), attno - 1)->attgenerated) - ereport(ERROR, - (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), - errmsg("cannot use generated column in partition key"), - errdetail("Column \"%s\" is a generated column.", - get_attname(RelationGetRelid(rel), attno, false)), - parser_errposition(pstate, pelem->location))); - } + if (IsA(expr, Var) && + ((Var *) expr)->varattno > 0) + { + + /* + * User wrote "(column)" or "(column COLLATE something)". + * Treat it like simple attribute anyway. + */ + partattrs[attn] = ((Var *) expr)->varattno; + } + else + { + partattrs[attn] = 0; /* marks the column as expression */ + *partexprs = lappend(*partexprs, expr); + + /* + * transformPartitionSpec() should have already rejected + * subqueries, aggregates, window functions, and SRFs, based + * on the EXPR_KIND_ for partition expressions. + */ /* * Preprocess the expression before checking for mutability. @@ -20143,6 +20510,40 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, } } +/* + * attachPartitionTable: attach a new partition to the partitioned table + * + * wqueue: the ALTER TABLE work queue; can be NULL when not running as part + * of an ALTER TABLE sequence. + * rel: partitioned relation; + * attachrel: relation of attached partition; + * bound: bounds of attached relation. + */ +static void +attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound) +{ + /* + * Create an inheritance; the relevant checks are performed inside the + * function. + */ + CreateInheritance(attachrel, rel, true); + + /* Update the pg_class entry. */ + StorePartitionBound(attachrel, rel, bound); + + /* Ensure there exists a correct set of indexes in the partition. */ + AttachPartitionEnsureIndexes(wqueue, rel, attachrel); + + /* and triggers */ + CloneRowTriggersToPartition(rel, attachrel); + + /* + * Clone foreign key constraints. Callee is responsible for setting up + * for phase 3 constraint verification. + */ + CloneForeignKeyConstraints(wqueue, rel, attachrel); +} + /* * ALTER TABLE ATTACH PARTITION FOR VALUES * @@ -20165,6 +20566,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, const char *trigger_name; Oid defaultPartOid; List *partBoundConstraint; + List *exceptpuboids = NIL; ParseState *pstate = make_parsestate(NULL); pstate->p_sourcetext = context->queryString; @@ -20204,6 +20606,50 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot attach a typed table as partition"))); + /* + * Disallow attaching a partition if the table is referenced in a + * publication EXCEPT clause. Changing the partition hierarchy could alter + * the effective publication membership. + */ + exceptpuboids = GetRelationExcludedPublications(RelationGetRelid(attachrel)); + if (exceptpuboids != NIL) + { + bool first = true; + StringInfoData pubnames; + + initStringInfo(&pubnames); + + foreach_oid(pubid, exceptpuboids) + { + char *pubname = get_publication_name(pubid, false); + + if (!first) + { + /* + * translator: This is a separator in a list of publication + * names. + */ + appendStringInfoString(&pubnames, _(", ")); + } + + first = false; + + appendStringInfo(&pubnames, _("\"%s\""), pubname); + } + + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg_plural("cannot attach table \"%s\" as partition because it is referenced in publication %s EXCEPT clause", + "cannot attach table \"%s\" as partition because it is referenced in publications %s EXCEPT clause", + list_length(exceptpuboids), + RelationGetRelationName(attachrel), + pubnames.data), + errdetail("The publication EXCEPT clause cannot contain tables that are partitions."), + errhint("Change the publication's EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES.")); + } + + list_free(exceptpuboids); + /* * Table being attached should not already be part of inheritance; either * as a child table... @@ -20278,15 +20724,13 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, RelationGetRelationName(rel)))); /* If the parent is temp, it must belong to this session */ - if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - !rel->rd_islocaltemp) + if (RELATION_IS_OTHER_TEMP(rel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot attach as partition of temporary relation of another session"))); /* Ditto for the partition */ - if (attachrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP && - !attachrel->rd_islocaltemp) + if (RELATION_IS_OTHER_TEMP(attachrel)) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot attach temporary relation of another session as partition"))); @@ -20346,26 +20790,10 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, check_new_partition_bound(RelationGetRelationName(attachrel), rel, cmd->bound, pstate); - /* OK to create inheritance. Rest of the checks performed there */ - CreateInheritance(attachrel, rel, true); - - /* Update the pg_class entry. */ - StorePartitionBound(attachrel, rel, cmd->bound); - - /* Ensure there exists a correct set of indexes in the partition. */ - AttachPartitionEnsureIndexes(wqueue, rel, attachrel); - - /* and triggers */ - CloneRowTriggersToPartition(rel, attachrel); - - /* - * Clone foreign key constraints. Callee is responsible for setting up - * for phase 3 constraint verification. - */ - CloneForeignKeyConstraints(wqueue, rel, attachrel); + attachPartitionTable(wqueue, rel, attachrel, cmd->bound); /* - * Generate partition constraint from the partition bound specification. + * Generate a partition constraint from the partition bound specification. * If the parent itself is a partition, make sure to include its * constraint as well. */ @@ -20489,8 +20917,8 @@ AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) idxes = RelationGetIndexList(rel); attachRelIdxs = RelationGetIndexList(attachrel); - attachrelIdxRels = palloc(sizeof(Relation) * list_length(attachRelIdxs)); - attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs)); + attachrelIdxRels = palloc_array(Relation, list_length(attachRelIdxs)); + attachInfos = palloc_array(IndexInfo *, list_length(attachRelIdxs)); /* Build arrays of all existing indexes and their IndexInfos */ foreach_oid(cldIdxId, attachRelIdxs) @@ -20630,7 +21058,8 @@ AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel) stmt = generateClonedIndexStmt(NULL, idxRel, attmap, &conOid); - DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid, + DefineIndex(NULL, + RelationGetRelid(attachrel), stmt, InvalidOid, RelationGetRelid(idxRel), conOid, -1, @@ -20889,12 +21318,6 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, char *parentrelname; char *partrelname; - /* - * Add a new constraint to the partition being detached, which - * supplants the partition constraint (unless there is one already). - */ - DetachAddConstraintIfNeeded(wqueue, partRel); - /* * We're almost done now; the only traces that remain are the * pg_inherits tuple and the partition's relpartbounds. Before we can @@ -20964,9 +21387,17 @@ ATExecDetachPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, tab->rel = rel; } + /* + * Detaching the partition might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + /* Do the final part of detaching */ DetachPartitionFinalize(rel, partRel, concurrent, defaultPartOid); + PopActiveSnapshot(); + ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partRel)); /* keep our lock until commit */ @@ -21343,49 +21774,6 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name) return address; } -/* - * DetachAddConstraintIfNeeded - * Subroutine for ATExecDetachPartition. Create a constraint that - * takes the place of the partition constraint, but avoid creating - * a dupe if a constraint already exists which implies the needed - * constraint. - */ -static void -DetachAddConstraintIfNeeded(List **wqueue, Relation partRel) -{ - List *constraintExpr; - - constraintExpr = RelationGetPartitionQual(partRel); - constraintExpr = (List *) eval_const_expressions(NULL, (Node *) constraintExpr); - - /* - * Avoid adding a new constraint if the needed constraint is implied by an - * existing constraint - */ - if (!PartConstraintImpliedByRelConstraint(partRel, constraintExpr)) - { - AlteredTableInfo *tab; - Constraint *n; - - tab = ATGetQueueEntry(wqueue, partRel); - - /* Add constraint on partition, equivalent to the partition constraint */ - n = makeNode(Constraint); - n->contype = CONSTR_CHECK; - n->conname = NULL; - n->location = -1; - n->is_no_inherit = false; - n->raw_expr = NULL; - n->cooked_expr = nodeToString(make_ands_explicit(constraintExpr)); - n->is_enforced = true; - n->initially_valid = true; - n->skip_validation = true; - /* It's a re-add, since it nominally already exists */ - ATAddCheckNNConstraint(wqueue, tab, partRel, n, - true, false, true, ShareUpdateExclusiveLock); - } -} - /* * DropClonedTriggersFromPartition * subroutine for ATExecDetachPartition to remove any triggers that were @@ -21559,7 +21947,10 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) ObjectAddressSet(address, RelationRelationId, RelationGetRelid(partIdx)); - /* Silently do nothing if already in the right state */ + /* + * Check if the index is already attached to the correct parent, + * ultimately attempting one round of validation if already the case. + */ currParent = partIdx->rd_rel->relispartition ? get_partition_parent(partIdxId, false) : InvalidOid; if (currParent != RelationGetRelid(parentIdx)) @@ -21668,6 +22059,14 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name) validatePartitionedIndex(parentIdx, parentTbl); } + else if (!parentIdx->rd_index->indisvalid) + { + /* + * The index is attached, but the parent is still invalid; see if it + * can be validated now. + */ + validatePartitionedIndex(parentIdx, parentTbl); + } relation_close(parentTbl, AccessShareLock); /* keep these locks till commit */ @@ -21694,7 +22093,8 @@ refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, Relation partitionTb errmsg("cannot attach index \"%s\" as a partition of index \"%s\"", RelationGetRelationName(partIdx), RelationGetRelationName(parentIdx)), - errdetail("Another index is already attached for partition \"%s\".", + errdetail("Another index \"%s\" is already attached for partition \"%s\".", + get_rel_name(existingIdx), RelationGetRelationName(partitionTbl)))); } @@ -22001,3 +22401,1498 @@ GetAttributeStorage(Oid atttypid, const char *storagemode) return cstorage; } + +/* + * buildExpressionExecutionStates: build the needed expression execution states + * for new partition (newPartRel) checks and initialize expressions for + * generated columns. All expressions should be created in "tab" + * (AlteredTableInfo structure). + */ +static void +buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate) +{ + /* + * Build the needed expression execution states. Here, we expect only NOT + * NULL and CHECK constraint. + */ + foreach_ptr(NewConstraint, con, tab->constraints) + { + switch (con->contype) + { + case CONSTR_CHECK: + + /* + * We already expanded virtual expression in + * createTableConstraints. + */ + con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate); + break; + case CONSTR_NOTNULL: + /* Nothing to do here. */ + break; + default: + elog(ERROR, "unrecognized constraint type: %d", + (int) con->contype); + } + } + + /* Expression already planned in createTableConstraints */ + foreach_ptr(NewColumnValue, ex, tab->newvals) + ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL); +} + +/* + * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated + * expressions for "tab" (AlteredTableInfo structure) whose inputs come from + * the new tuple (insertslot) of the new partition (newPartRel). + */ +static void +evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab, + Relation newPartRel, + TupleTableSlot *insertslot, + ExprContext *econtext) +{ + econtext->ecxt_scantuple = insertslot; + + foreach_ptr(NewColumnValue, ex, tab->newvals) + { + if (!ex->is_generated) + continue; + + insertslot->tts_values[ex->attnum - 1] + = ExecEvalExpr(ex->exprstate, + econtext, + &insertslot->tts_isnull[ex->attnum - 1]); + } + + foreach_ptr(NewConstraint, con, tab->constraints) + { + switch (con->contype) + { + case CONSTR_CHECK: + if (!ExecCheck(con->qualstate, econtext)) + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row", + con->name, RelationGetRelationName(newPartRel)), + errtableconstraint(newPartRel, con->name)); + break; + case CONSTR_NOTNULL: + case CONSTR_FOREIGN: + /* Nothing to do here */ + break; + default: + elog(ERROR, "unrecognized constraint type: %d", + (int) con->contype); + } + } +} + +/* + * getAttributesList: build a list of columns (ColumnDef) based on parent_rel + */ +static List * +getAttributesList(Relation parent_rel) +{ + AttrNumber parent_attno; + TupleDesc modelDesc; + List *colList = NIL; + + modelDesc = RelationGetDescr(parent_rel); + + for (parent_attno = 1; parent_attno <= modelDesc->natts; + parent_attno++) + { + Form_pg_attribute attribute = TupleDescAttr(modelDesc, + parent_attno - 1); + ColumnDef *def; + + /* Ignore dropped columns in the parent. */ + if (attribute->attisdropped) + continue; + + def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, + attribute->atttypmod, attribute->attcollation); + + def->is_not_null = attribute->attnotnull; + + /* Copy identity. */ + def->identity = attribute->attidentity; + + /* Copy attgenerated. */ + def->generated = attribute->attgenerated; + + def->storage = attribute->attstorage; + + /* Likewise, copy compression. */ + if (CompressionMethodIsValid(attribute->attcompression)) + def->compression = + pstrdup(GetCompressionMethodName(attribute->attcompression)); + else + def->compression = NULL; + + /* Add to column list. */ + colList = lappend(colList, def); + } + + return colList; +} + +/* + * createTableConstraints: + * create check constraints, default values, and generated values for newRel + * based on parent_rel. tab is pending-work queue for newRel, we may need it in + * MergePartitionsMoveRows. + */ +static void +createTableConstraints(List **wqueue, AlteredTableInfo *tab, + Relation parent_rel, Relation newRel) +{ + TupleDesc tupleDesc; + TupleConstr *constr; + AttrMap *attmap; + AttrNumber parent_attno; + int ccnum; + List *constraints = NIL; + List *cookedConstraints = NIL; + + tupleDesc = RelationGetDescr(parent_rel); + constr = tupleDesc->constr; + + if (!constr) + return; + + /* + * Construct a map from the parent relation's attnos to the child rel's. + * This re-checks type match, etc, although it shouldn't be possible to + * have a failure since both tables are locked. + */ + attmap = build_attrmap_by_name(RelationGetDescr(newRel), + tupleDesc, + false); + + /* Cycle for default values. */ + for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) + { + Form_pg_attribute attribute = TupleDescAttr(tupleDesc, + parent_attno - 1); + + /* Ignore dropped columns in the parent. */ + if (attribute->attisdropped) + continue; + + /* Copy the default, if present, and it should be copied. */ + if (attribute->atthasdef) + { + Node *this_default = NULL; + bool found_whole_row; + AttrNumber num; + Node *def; + NewColumnValue *newval; + + if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + this_default = build_generation_expression(parent_rel, attribute->attnum); + else + { + this_default = TupleDescGetDefault(tupleDesc, attribute->attnum); + if (this_default == NULL) + elog(ERROR, "default expression not found for attribute %d of relation \"%s\"", + attribute->attnum, RelationGetRelationName(parent_rel)); + } + + num = attmap->attnums[parent_attno - 1]; + def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row); + + if (found_whole_row && attribute->attgenerated != '\0') + elog(ERROR, "cannot convert whole-row table reference"); + + /* Add a pre-cooked default expression. */ + StoreAttrDefault(newRel, num, def, true); + + /* + * Stored generated column expressions in parent_rel might + * reference the tableoid. newRel, parent_rel tableoid clear is + * not the same. If so, these stored generated columns require + * recomputation for newRel within MergePartitionsMoveRows. + */ + if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED) + { + newval = palloc0_object(NewColumnValue); + newval->attnum = num; + newval->expr = expression_planner((Expr *) def); + newval->is_generated = (attribute->attgenerated != '\0'); + tab->newvals = lappend(tab->newvals, newval); + } + } + } + + /* Cycle for CHECK constraints. */ + for (ccnum = 0; ccnum < constr->num_check; ccnum++) + { + char *ccname = constr->check[ccnum].ccname; + char *ccbin = constr->check[ccnum].ccbin; + bool ccenforced = constr->check[ccnum].ccenforced; + bool ccnoinherit = constr->check[ccnum].ccnoinherit; + bool ccvalid = constr->check[ccnum].ccvalid; + Node *ccbin_node; + bool found_whole_row; + Constraint *constr; + + /* + * The partitioned table can not have a NO INHERIT check constraint + * (see StoreRelCheck function for details). + */ + Assert(!ccnoinherit); + + ccbin_node = map_variable_attnos(stringToNode(ccbin), + 1, 0, + attmap, + InvalidOid, &found_whole_row); + + /* + * For the moment we have to reject whole-row variables (as for CREATE + * TABLE LIKE and inheritances). + */ + if (found_whole_row) + elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".", + ccname, + RelationGetRelationName(parent_rel)); + + constr = makeNode(Constraint); + constr->contype = CONSTR_CHECK; + constr->conname = pstrdup(ccname); + constr->deferrable = false; + constr->initdeferred = false; + constr->is_enforced = ccenforced; + constr->skip_validation = !ccvalid; + constr->initially_valid = ccvalid; + constr->is_no_inherit = ccnoinherit; + constr->raw_expr = NULL; + constr->cooked_expr = nodeToString(ccbin_node); + constr->location = -1; + constraints = lappend(constraints, constr); + } + + /* Install all CHECK constraints. */ + cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints, + false, true, true, NULL); + + /* Make the additional catalog changes visible. */ + CommandCounterIncrement(); + + /* + * parent_rel check constraint expression may reference tableoid, so later + * in MergePartitionsMoveRows, we need to evaluate the check constraint + * again for the newRel. We can check whether the check constraint + * contains a tableoid reference via pull_varattnos. + */ + foreach_ptr(CookedConstraint, ccon, cookedConstraints) + { + if (!ccon->skip_validation) + { + Node *qual; + Bitmapset *attnums = NULL; + + Assert(ccon->contype == CONSTR_CHECK); + qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1); + pull_varattnos(qual, 1, &attnums); + + /* + * Add a check only if it contains a tableoid + * (TableOidAttributeNumber). + */ + if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber, + attnums)) + { + NewConstraint *newcon; + + newcon = palloc0_object(NewConstraint); + newcon->name = ccon->name; + newcon->contype = CONSTR_CHECK; + newcon->qual = qual; + + tab->constraints = lappend(tab->constraints, newcon); + } + } + } + + /* Don't need the cookedConstraints anymore. */ + list_free_deep(cookedConstraints); + + /* Reproduce not-null constraints. */ + if (constr->has_not_null) + { + List *nnconstraints; + + /* + * The "include_noinh" argument is false because a partitioned table + * can't have NO INHERIT constraint. + */ + nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel), + false, false); + + Assert(list_length(nnconstraints) > 0); + + /* + * We already set pg_attribute.attnotnull in createPartitionTable. No + * need call set_attnotnull again. + */ + AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL); + } +} + +/* + * createPartitionTable: + * + * Create a new partition (newPartName) for the partitioned table (parent_rel). + * ownerId is determined by the partition on which the operation is performed, + * so it is passed separately. The new partition will inherit the access method + * and persistence type from the parent table. + * + * Returns the created relation (locked in AccessExclusiveLock mode). + */ +static Relation +createPartitionTable(List **wqueue, RangeVar *newPartName, + Relation parent_rel, Oid ownerId) +{ + Relation newRel; + Oid newRelId; + Oid existingRelid; + TupleDesc descriptor; + List *colList = NIL; + Oid relamId; + Oid namespaceId; + AlteredTableInfo *new_partrel_tab; + Form_pg_class parent_relform = parent_rel->rd_rel; + + /* If the existing rel is temp, it must belong to this session. */ + if (RELATION_IS_OTHER_TEMP(parent_rel)) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create as partition of temporary relation of another session")); + + /* Look up inheritance ancestors and generate the relation schema. */ + colList = getAttributesList(parent_rel); + + /* Create a tuple descriptor from the relation schema. */ + descriptor = BuildDescForRelation(colList); + + /* Look up the access method for the new relation. */ + relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID; + + /* Look up the namespace in which we are supposed to create the relation. */ + namespaceId = + RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid); + if (OidIsValid(existingRelid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", newPartName->relname)); + + /* + * We intended to create the partition with the same persistence as the + * parent table, but we still need to recheck because that might be + * affected by the search_path. If the parent is permanent, so must be + * all of its partitions. + */ + if (parent_relform->relpersistence != RELPERSISTENCE_TEMP && + newPartName->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* Permanent rels cannot be partitions belonging to a temporary parent. */ + if (newPartName->relpersistence != RELPERSISTENCE_TEMP && + parent_relform->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* Create the relation. */ + newRelId = heap_create_with_catalog(newPartName->relname, + namespaceId, + parent_relform->reltablespace, + InvalidOid, + InvalidOid, + InvalidOid, + ownerId, + relamId, + descriptor, + NIL, + RELKIND_RELATION, + newPartName->relpersistence, + false, + false, + ONCOMMIT_NOOP, + (Datum) 0, + true, + allowSystemTableMods, + true, + InvalidOid, + NULL); + + /* + * We must bump the command counter to make the newly-created relation + * tuple visible for opening. + */ + CommandCounterIncrement(); + + /* + * Open the new partition with no lock, because we already have an + * AccessExclusiveLock placed there after creation. + */ + newRel = table_open(newRelId, NoLock); + + /* Find or create a work queue entry for the newly created table. */ + new_partrel_tab = ATGetQueueEntry(wqueue, newRel); + + /* Create constraints, default values, and generated values. */ + createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel); + + /* + * Need to call CommandCounterIncrement, so a fresh relcache entry has + * newly installed constraint info. + */ + CommandCounterIncrement(); + + return newRel; +} + +/* + * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions) + * of the partitioned table and move rows into the new partition + * (newPartRel). We also verify check constraints against these rows. + */ +static void +MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel) +{ + CommandId mycid; + EState *estate; + AlteredTableInfo *tab; + ListCell *ltab; + + /* The FSM is empty, so don't bother using it. */ + uint32 ti_options = TABLE_INSERT_SKIP_FSM; + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; + + /* Find the work queue entry for the new partition table: newPartRel. */ + tab = ATGetQueueEntry(wqueue, newPartRel); + + /* Generate the constraint and default execution states. */ + estate = CreateExecutorState(); + + buildExpressionExecutionStates(tab, newPartRel, estate); + + mycid = GetCurrentCommandId(true); + + /* Prepare a BulkInsertState for table_tuple_insert. */ + bistate = GetBulkInsertState(); + + /* Create the necessary tuple slot. */ + dstslot = table_slot_create(newPartRel, NULL); + + foreach_oid(merging_oid, mergingPartitions) + { + ExprContext *econtext; + TupleTableSlot *srcslot; + TupleConversionMap *tuple_map; + TableScanDesc scan; + MemoryContext oldCxt; + Snapshot snapshot; + Relation mergingPartition; + + econtext = GetPerTupleExprContext(estate); + + /* + * Partition is already locked in the transformPartitionCmdForMerge + * function. + */ + mergingPartition = table_open(merging_oid, NoLock); + + /* Create a source tuple slot for the partition being merged. */ + srcslot = table_slot_create(mergingPartition, NULL); + + /* + * Map computing for moving attributes of the merged partition to the + * new partition. + */ + tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition), + RelationGetDescr(newPartRel)); + + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(mergingPartition, snapshot, 0, NULL, + SO_NONE); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) + { + TupleTableSlot *insertslot; + + CHECK_FOR_INTERRUPTS(); + + if (tuple_map) + { + /* Need to use a map to copy attributes. */ + insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot); + } + else + { + slot_getallattrs(srcslot); + + /* Copy attributes directly. */ + insertslot = dstslot; + + ExecClearTuple(insertslot); + + memcpy(insertslot->tts_values, srcslot->tts_values, + sizeof(Datum) * srcslot->tts_nvalid); + memcpy(insertslot->tts_isnull, srcslot->tts_isnull, + sizeof(bool) * srcslot->tts_nvalid); + + ExecStoreVirtualTuple(insertslot); + } + + /* + * Constraints and GENERATED expressions might reference the + * tableoid column, so fill tts_tableOid with the desired value. + * (We must do this each time, because it gets overwritten with + * newrel's OID during storing.) + */ + insertslot->tts_tableOid = RelationGetRelid(newPartRel); + + /* + * Now, evaluate any generated expressions whose inputs come from + * the new tuple. We assume these columns won't reference each + * other, so that there's no ordering dependency. + */ + evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel, + insertslot, econtext); + + /* Write the tuple out to the new relation. */ + table_tuple_insert(newPartRel, insertslot, mycid, + ti_options, bistate); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldCxt); + table_endscan(scan); + UnregisterSnapshot(snapshot); + + if (tuple_map) + free_conversion_map(tuple_map); + + ExecDropSingleTupleTableSlot(srcslot); + table_close(mergingPartition, NoLock); + } + + FreeExecutorState(estate); + ExecDropSingleTupleTableSlot(dstslot); + FreeBulkInsertState(bistate); + + table_finish_bulk_insert(newPartRel, ti_options); + + /* + * We don't need to process this newPartRel since we already processed it + * here, so delete the ALTER TABLE queue for it. + */ + foreach(ltab, *wqueue) + { + tab = (AlteredTableInfo *) lfirst(ltab); + if (tab->relid == RelationGetRelid(newPartRel)) + { + *wqueue = list_delete_cell(*wqueue, ltab); + break; + } + } +} + +/* + * detachPartitionTable: detach partition "child_rel" from partitioned table + * "parent_rel" with default partition identifier "defaultPartOid" + */ +static void +detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid) +{ + /* Remove the pg_inherits row first. */ + RemoveInheritance(child_rel, parent_rel, false); + + /* + * Detaching the partition might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* Do the final part of detaching. */ + DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid); + + PopActiveSnapshot(); +} + +/* + * equal_oid_lists: return true if two OID lists, each sorted in ascending + * order, contain the same OIDs in the same order. + */ +static bool +equal_oid_lists(const List *a, const List *b) +{ + ListCell *la, + *lb; + + if (list_length(a) != list_length(b)) + return false; + + forboth(la, a, lb, b) + { + if (lfirst_oid(la) != lfirst_oid(lb)) + return false; + } + return true; +} + +/* + * Comparator for list_sort() on a list of PartitionIndexExtDepEntry *. + * Orders by parentIndexOid, then by indexOid as a tiebreaker so conflict + * reports for different parent indexes are deterministic. + */ +static int +cmp_partition_index_ext_dep(const ListCell *a, const ListCell *b) +{ + const PartitionIndexExtDepEntry *ea = lfirst(a); + const PartitionIndexExtDepEntry *eb = lfirst(b); + + if (ea->parentIndexOid != eb->parentIndexOid) + return pg_cmp_u32(ea->parentIndexOid, eb->parentIndexOid); + return pg_cmp_u32(ea->indexOid, eb->indexOid); +} + +/* + * collectPartitionIndexExtDeps: collect extension dependencies from indexes + * on the given partitions. + * + * For each partition index that has a parent partitioned index, we collect + * extension dependencies. All source partition indexes sharing the same + * parent partitioned index must depend on exactly the same set of + * extensions; otherwise an error is raised so that we neither silently drop + * nor silently add dependencies on the merged partition's index. + * + * Indexes that don't have a parent partitioned index (i.e., indexes created + * directly on a partition without a corresponding parent index) are skipped. + * + * The returned list is sorted by parentIndexOid with exactly one entry per + * parent partitioned index, so applyPartitionIndexExtDeps() can scan it + * linearly. + */ +static List * +collectPartitionIndexExtDeps(List *partitionOids) +{ + List *collected = NIL; + List *result = NIL; + PartitionIndexExtDepEntry *prev = NULL; + + /* + * Phase 1: collect one entry per (partition index -> parent index) pair, + * with its extension dependency OIDs sorted ascending. + */ + foreach_oid(partOid, partitionOids) + { + Relation partRel; + List *indexList; + + /* + * Use NoLock since the caller already holds AccessExclusiveLock on + * these partitions. + */ + partRel = table_open(partOid, NoLock); + indexList = RelationGetIndexList(partRel); + + foreach_oid(indexOid, indexList) + { + Oid parentIndexOid; + PartitionIndexExtDepEntry *entry; + + if (!get_rel_relispartition(indexOid)) + continue; + + parentIndexOid = get_partition_parent(indexOid, true); + if (!OidIsValid(parentIndexOid)) + continue; + + entry = palloc(sizeof(PartitionIndexExtDepEntry)); + entry->parentIndexOid = parentIndexOid; + entry->indexOid = indexOid; + entry->extensionOids = getAutoExtensionsOfObject(RelationRelationId, + indexOid); + list_sort(entry->extensionOids, list_oid_cmp); + + collected = lappend(collected, entry); + } + + list_free(indexList); + table_close(partRel, NoLock); + } + + /* + * Phase 2: sort by parentIndexOid so entries sharing a parent index sit + * adjacent. + */ + list_sort(collected, cmp_partition_index_ext_dep); + + /* + * Phase 3: single linear pass verifying that adjacent entries sharing a + * parent index have identical extension dependencies, and keeping one + * representative entry per parent index. + */ + foreach_ptr(PartitionIndexExtDepEntry, entry, collected) + { + if (prev != NULL && prev->parentIndexOid == entry->parentIndexOid) + { + if (!equal_oid_lists(prev->extensionOids, entry->extensionOids)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot merge partitions with conflicting extension dependencies"), + errdetail("Partition indexes \"%s\" and \"%s\" depend on different extensions.", + get_rel_name(prev->indexOid), + get_rel_name(entry->indexOid)))); + + /* Duplicate entry for the same parent index; discard. */ + list_free(entry->extensionOids); + pfree(entry); + continue; + } + + result = lappend(result, entry); + prev = entry; + } + + list_free(collected); + + return result; +} + +/* + * applyPartitionIndexExtDeps: apply collected extension dependencies to + * indexes on a new partition. + * + * For each index on the new partition, look up its parent index in the + * extDepState list. If found, record extension dependencies on the new index. + * extDepState is sorted by parentIndexOid, so the inner scan can bail out + * as soon as it passes the target OID. + */ +static void +applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState) +{ + Relation partRel; + List *indexList; + + if (extDepState == NIL) + return; + + /* + * Use NoLock since the caller already holds AccessExclusiveLock on the + * new partition. + */ + partRel = table_open(newPartOid, NoLock); + indexList = RelationGetIndexList(partRel); + + foreach_oid(indexOid, indexList) + { + Oid parentIdxOid; + + if (!get_rel_relispartition(indexOid)) + continue; + + parentIdxOid = get_partition_parent(indexOid, true); + if (!OidIsValid(parentIdxOid)) + continue; + + foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState) + { + ObjectAddress indexAddr; + + if (entry->parentIndexOid > parentIdxOid) + break; + if (entry->parentIndexOid < parentIdxOid) + continue; + + ObjectAddressSet(indexAddr, RelationRelationId, indexOid); + + foreach_oid(extOid, entry->extensionOids) + { + ObjectAddress extAddr; + + ObjectAddressSet(extAddr, ExtensionRelationId, extOid); + recordDependencyOn(&indexAddr, &extAddr, + DEPENDENCY_AUTO_EXTENSION); + } + break; + } + } + + list_free(indexList); + table_close(partRel, NoLock); +} + +/* + * freePartitionIndexExtDeps: free memory allocated by collectPartitionIndexExtDeps. + */ +static void +freePartitionIndexExtDeps(List *extDepState) +{ + foreach_ptr(PartitionIndexExtDepEntry, entry, extDepState) + { + list_free(entry->extensionOids); + pfree(entry); + } + list_free(extDepState); +} + +/* + * ALTER TABLE MERGE PARTITIONS INTO + */ +static void +ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context) +{ + Relation newPartRel; + List *mergingPartitions = NIL; + List *extDepState = NIL; + Oid defaultPartOid; + Oid existingRelid; + Oid ownerId = InvalidOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + + /* + * Check ownership of merged partitions - partitions with different owners + * cannot be merged. Also, collect the OIDs of these partitions during the + * check. + */ + foreach_node(RangeVar, name, cmd->partlist) + { + Relation mergingPartition; + + /* + * We are going to detach and remove this partition. We already took + * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here, + * NoLock is fine. + */ + mergingPartition = table_openrv_extended(name, NoLock, false); + Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false)); + + if (OidIsValid(ownerId)) + { + /* Do the partitions being merged have different owners? */ + if (ownerId != mergingPartition->rd_rel->relowner) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partitions being merged have different owners")); + } + else + ownerId = mergingPartition->rd_rel->relowner; + + /* Store the next merging partition into the list. */ + mergingPartitions = lappend_oid(mergingPartitions, + RelationGetRelid(mergingPartition)); + + table_close(mergingPartition, NoLock); + } + + /* Look up the existing relation by the new partition name. */ + RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid); + + /* + * Check if this name is already taken. This helps us to detect the + * situation when one of the merging partitions has the same name as the + * new partition. Otherwise, this would fail later on anyway, but + * catching this here allows us to emit a nicer error message. + */ + if (OidIsValid(existingRelid)) + { + if (list_member_oid(mergingPartitions, existingRelid)) + { + /* + * The new partition has the same name as one of the merging + * partitions. + */ + char tmpRelName[NAMEDATALEN]; + + /* Generate a temporary name. */ + sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + + /* + * Rename the existing partition with a temporary name, leaving it + * free for the new partition. We don't need to care about this + * in the future because we're going to eventually drop the + * existing partition anyway. + */ + RenameRelationInternal(existingRelid, tmpRelName, true, false); + + /* + * We must bump the command counter to make the new partition + * tuple visible for rename. + */ + CommandCounterIncrement(); + } + else + { + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", cmd->name->relname)); + } + } + + defaultPartOid = + get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + + /* + * Collect extension dependencies from indexes on the merging partitions. + * We must do this before detaching them, so we can restore the + * dependencies on the new partition's indexes later. + */ + extDepState = collectPartitionIndexExtDeps(mergingPartitions); + + /* Detach all merging partitions. */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + Relation child_rel; + + child_rel = table_open(mergingPartitionOid, NoLock); + + detachPartitionTable(rel, child_rel, defaultPartOid); + + table_close(child_rel, NoLock); + } + + /* + * Perform a preliminary check to determine whether it's safe to drop all + * merging partitions before we actually do so later. After merging rows + * into the new partitions via MergePartitionsMoveRows, all old partitions + * need to be dropped. However, since the drop behavior is DROP_RESTRICT + * and the merge process (MergePartitionsMoveRows) can be time-consuming, + * performing an early check on the drop eligibility of old partitions is + * preferable. + */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + ObjectAddress object; + + /* Get oid of the later to be dropped relation. */ + object.objectId = mergingPartitionOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + + performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + } + + /* + * Create a table for the new partition, using the partitioned table as a + * model. + */ + Assert(OidIsValid(ownerId)); + newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId); + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also, lockdown security-restricted operations and + * arrange to make GUC variable changes local to this command. + * + * Need to do it after determining the namespace in the + * createPartitionTable() call. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(ownerId, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* Copy data from merged partitions to the new partition. */ + MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel); + + /* Drop the current partitions before attaching the new one. */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + ObjectAddress object; + + object.objectId = mergingPartitionOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + + performDeletion(&object, DROP_RESTRICT, 0); + } + + list_free(mergingPartitions); + + /* + * Attach a new partition to the partitioned table. wqueue = NULL: + * verification for each cloned constraint is not needed. + */ + attachPartitionTable(NULL, rel, newPartRel, cmd->bound); + + /* + * Apply extension dependencies to the new partition's indexes. This + * preserves any "DEPENDS ON EXTENSION" settings from the merged + * partitions. + */ + applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState); + + freePartitionIndexExtDeps(extDepState); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + + /* Roll back any GUC changes executed by index functions. */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore the userid and security context. */ + SetUserIdAndSecContext(save_userid, save_sec_context); +} + +/* + * Struct with the context of the new partition for inserting rows from the + * split partition. + */ +typedef struct SplitPartitionContext +{ + ExprState *partqualstate; /* expression for checking a slot for a + * partition (NULL for DEFAULT partition) */ + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; /* slot for inserting row into partition */ + AlteredTableInfo *tab; /* structure with generated column expressions + * and check constraint expressions. */ + Relation partRel; /* relation for partition */ +} SplitPartitionContext; + +/* + * createSplitPartitionContext: create context for partition and fill it + */ +static SplitPartitionContext * +createSplitPartitionContext(Relation partRel) +{ + SplitPartitionContext *pc; + + pc = palloc0_object(SplitPartitionContext); + pc->partRel = partRel; + + /* + * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so + * don't bother using it. + */ + pc->bistate = GetBulkInsertState(); + + /* Create a destination tuple slot for the new partition. */ + pc->dstslot = table_slot_create(pc->partRel, NULL); + + return pc; +} + +/* + * deleteSplitPartitionContext: delete context for partition + */ +static void +deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, uint32 ti_options) +{ + ListCell *ltab; + + ExecDropSingleTupleTableSlot(pc->dstslot); + FreeBulkInsertState(pc->bistate); + + table_finish_bulk_insert(pc->partRel, ti_options); + + /* + * We don't need to process this pc->partRel so delete the ALTER TABLE + * queue of it. + */ + foreach(ltab, *wqueue) + { + AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); + + if (tab->relid == RelationGetRelid(pc->partRel)) + { + *wqueue = list_delete_cell(*wqueue, ltab); + break; + } + } + + pfree(pc); +} + +/* + * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table + * (rel) and move rows into new partitions. + * + * New partitions description: + * partlist: list of pointers to SinglePartitionSpec structures. It contains + * the partition specification details for all new partitions. + * newPartRels: list of Relations, new partitions created in + * ATExecSplitPartition. + */ +static void +SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel, + List *partlist, List *newPartRels) +{ + /* The FSM is empty, so don't bother using it. */ + uint32 ti_options = TABLE_INSERT_SKIP_FSM; + CommandId mycid; + EState *estate; + ListCell *listptr, + *listptr2; + TupleTableSlot *srcslot; + ExprContext *econtext; + TableScanDesc scan; + Snapshot snapshot; + MemoryContext oldCxt; + List *partContexts = NIL; + TupleConversionMap *tuple_map; + SplitPartitionContext *defaultPartCtx = NULL, + *pc; + + mycid = GetCurrentCommandId(true); + + estate = CreateExecutorState(); + + forboth(listptr, partlist, listptr2, newPartRels) + { + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + + pc = createSplitPartitionContext((Relation) lfirst(listptr2)); + + /* Find the work queue entry for the new partition table: newPartRel. */ + pc->tab = ATGetQueueEntry(wqueue, pc->partRel); + + buildExpressionExecutionStates(pc->tab, pc->partRel, estate); + + if (sps->bound->is_default) + { + /* + * We should not create a structure to check the partition + * constraint for the new DEFAULT partition. + */ + defaultPartCtx = pc; + } + else + { + List *partConstraint; + + /* Build expression execution states for partition check quals. */ + partConstraint = get_qual_from_partbound(rel, sps->bound); + partConstraint = + (List *) eval_const_expressions(NULL, + (Node *) partConstraint); + /* Make a boolean expression for ExecCheck(). */ + partConstraint = list_make1(make_ands_explicit(partConstraint)); + + /* + * Map the vars in the constraint expression from rel's attnos to + * splitRel's. + */ + partConstraint = map_partition_varattnos(partConstraint, + 1, splitRel, rel); + + pc->partqualstate = + ExecPrepareExpr((Expr *) linitial(partConstraint), estate); + Assert(pc->partqualstate != NULL); + } + + /* Store partition context into a list. */ + partContexts = lappend(partContexts, pc); + } + + econtext = GetPerTupleExprContext(estate); + + /* Create the necessary tuple slot. */ + srcslot = table_slot_create(splitRel, NULL); + + /* + * Map computing for moving attributes of the split partition to the new + * partition (for the first new partition, but other new partitions can + * use the same map). + */ + pc = (SplitPartitionContext *) lfirst(list_head(partContexts)); + tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel), + RelationGetDescr(pc->partRel)); + + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(splitRel, snapshot, 0, NULL, + SO_NONE); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) + { + bool found = false; + TupleTableSlot *insertslot; + + CHECK_FOR_INTERRUPTS(); + + econtext->ecxt_scantuple = srcslot; + + /* Search partition for the current slot, srcslot. */ + foreach(listptr, partContexts) + { + pc = (SplitPartitionContext *) lfirst(listptr); + + /* skip DEFAULT partition */ + if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext)) + { + found = true; + break; + } + } + if (!found) + { + /* Use the DEFAULT partition if it exists. */ + if (defaultPartCtx) + pc = defaultPartCtx; + else + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("can not find partition for split partition row"), + errtable(splitRel)); + } + + if (tuple_map) + { + /* Need to use a map to copy attributes. */ + insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot); + } + else + { + /* Extract data from the old tuple. */ + slot_getallattrs(srcslot); + + /* Copy attributes directly. */ + insertslot = pc->dstslot; + + ExecClearTuple(insertslot); + + memcpy(insertslot->tts_values, srcslot->tts_values, + sizeof(Datum) * srcslot->tts_nvalid); + memcpy(insertslot->tts_isnull, srcslot->tts_isnull, + sizeof(bool) * srcslot->tts_nvalid); + + ExecStoreVirtualTuple(insertslot); + } + + /* + * Constraints and GENERATED expressions might reference the tableoid + * column, so fill tts_tableOid with the desired value. (We must do + * this each time, because it gets overwritten with newrel's OID + * during storing.) + */ + insertslot->tts_tableOid = RelationGetRelid(pc->partRel); + + /* + * Now, evaluate any generated expressions whose inputs come from the + * new tuple. We assume these columns won't reference each other, so + * that there's no ordering dependency. + */ + evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel, + insertslot, econtext); + + /* Write the tuple out to the new relation. */ + table_tuple_insert(pc->partRel, insertslot, mycid, + ti_options, pc->bistate); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldCxt); + + table_endscan(scan); + UnregisterSnapshot(snapshot); + + if (tuple_map) + free_conversion_map(tuple_map); + + ExecDropSingleTupleTableSlot(srcslot); + + FreeExecutorState(estate); + + foreach_ptr(SplitPartitionContext, spc, partContexts) + deleteSplitPartitionContext(spc, wqueue, ti_options); +} + +/* + * ALTER TABLE SPLIT PARTITION INTO + */ +static void +ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context) +{ + Relation splitRel; + Oid splitRelOid; + ListCell *listptr, + *listptr2; + bool isSameName = false; + char tmpRelName[NAMEDATALEN]; + List *newPartRels = NIL; + List *extDepState = NIL; + ObjectAddress object; + Oid defaultPartOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + List *splitPartList; + + defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + + /* + * Partition is already locked in the transformPartitionCmdForSplit + * function. + */ + splitRel = table_openrv(cmd->name, NoLock); + + splitRelOid = RelationGetRelid(splitRel); + + /* Check descriptions of new partitions. */ + foreach_node(SinglePartitionSpec, sps, cmd->partlist) + { + Oid existingRelid; + + /* Look up the existing relation by the new partition name. */ + RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid); + + /* + * This would fail later on anyway if the relation already exists. But + * by catching it here, we can emit a nicer error message. + */ + if (existingRelid == splitRelOid && !isSameName) + /* One new partition can have the same name as a split partition. */ + isSameName = true; + else if (OidIsValid(existingRelid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", sps->name->relname)); + } + + /* + * Collect extension dependencies from indexes on the split partition. We + * must do this before detaching it, so we can restore the dependencies on + * the new partitions' indexes later. + */ + splitPartList = list_make1_oid(splitRelOid); + + extDepState = collectPartitionIndexExtDeps(splitPartList); + list_free(splitPartList); + + /* Detach the split partition. */ + detachPartitionTable(rel, splitRel, defaultPartOid); + + /* + * Perform a preliminary check to determine whether it's safe to drop the + * split partition before we actually do so later. After merging rows into + * the new partitions via SplitPartitionMoveRows, all old partitions need + * to be dropped. However, since the drop behavior is DROP_RESTRICT and + * the merge process (SplitPartitionMoveRows) can be time-consuming, + * performing an early check on the drop eligibility of old partitions is + * preferable. + */ + object.objectId = splitRelOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + + /* + * If a new partition has the same name as the split partition, then we + * should rename the split partition to reuse its name. + */ + if (isSameName) + { + /* + * We must bump the command counter to make the split partition tuple + * visible for renaming. + */ + CommandCounterIncrement(); + /* Rename partition. */ + sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + RenameRelationInternal(splitRelOid, tmpRelName, true, false); + + /* + * We must bump the command counter to make the split partition tuple + * visible after renaming. + */ + CommandCounterIncrement(); + } + + /* Create new partitions (like a split partition), without indexes. */ + foreach_node(SinglePartitionSpec, sps, cmd->partlist) + { + Relation newPartRel; + + newPartRel = createPartitionTable(wqueue, sps->name, rel, + splitRel->rd_rel->relowner); + newPartRels = lappend(newPartRels, newPartRel); + } + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also, lockdown security-restricted operations and + * arrange to make GUC variable changes local to this command. + * + * Need to do it after determining the namespace in the + * createPartitionTable() call. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(splitRel->rd_rel->relowner, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* Copy data from the split partition to the new partitions. */ + SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels); + /* Keep the lock until commit. */ + table_close(splitRel, NoLock); + + /* Attach new partitions to the partitioned table. */ + forboth(listptr, cmd->partlist, listptr2, newPartRels) + { + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + Relation newPartRel = (Relation) lfirst(listptr2); + + /* + * wqueue = NULL: verification for each cloned constraint is not + * needed. + */ + attachPartitionTable(NULL, rel, newPartRel, sps->bound); + + /* + * Apply extension dependencies to the new partition's indexes. This + * preserves any "DEPENDS ON EXTENSION" settings from the split + * partition. + */ + applyPartitionIndexExtDeps(RelationGetRelid(newPartRel), extDepState); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + } + + freePartitionIndexExtDeps(extDepState); + + /* Drop the split partition. */ + object.classId = RelationRelationId; + object.objectId = splitRelOid; + object.objectSubId = 0; + /* Probably DROP_CASCADE is not needed. */ + performDeletion(&object, DROP_RESTRICT, 0); + + /* Roll back any GUC changes executed by index functions. */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore the userid and security context. */ + SetUserIdAndSecContext(save_userid, save_sec_context); +} diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c index a9005cc7212b6..d91fcf0facf8b 100644 --- a/src/backend/commands/tablespace.c +++ b/src/backend/commands/tablespace.c @@ -35,7 +35,7 @@ * and munge the system catalogs of the new database. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -70,6 +70,8 @@ #include "miscadmin.h" #include "postmaster/bgwriter.h" #include "storage/fd.h" +#include "storage/lwlock.h" +#include "storage/procsignal.h" #include "storage/standby.h" #include "utils/acl.h" #include "utils/builtins.h" @@ -241,6 +243,12 @@ CreateTableSpace(CreateTableSpaceStmt *stmt) (errcode(ERRCODE_INVALID_NAME), errmsg("tablespace location cannot contain single quotes"))); + /* Report error if name has \n or \r character. */ + if (strpbrk(stmt->tablespacename, "\n\r")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("tablespace name \"%s\" contains a newline or carriage return character", stmt->tablespacename))); + in_place = allow_in_place_tablespaces && strlen(location) == 0; /* @@ -500,7 +508,7 @@ DropTableSpace(DropTableSpaceStmt *stmt) * mustn't delete. So instead, we force a checkpoint which will clean * out any lingering files, and try again. */ - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_FORCE | CHECKPOINT_WAIT); + RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | CHECKPOINT_WAIT); /* * On Windows, an unlinked file persists in the directory listing @@ -970,6 +978,12 @@ RenameTableSpace(const char *oldname, const char *newname) errmsg("unacceptable tablespace name \"%s\"", newname), errdetail("The prefix \"pg_\" is reserved for system tablespaces."))); + /* Report error if name has \n or \r character. */ + if (strpbrk(newname, "\n\r")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("tablespace name \"%s\" contains a newline or carriage return character", newname))); + /* * If built with appropriate switch, whine when regression-testing * conventions for tablespace names are violated. diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 67f8e70f9c166..da0d1ba679127 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -3,7 +3,7 @@ * trigger.c * PostgreSQL TRIGGERs support code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -19,6 +19,7 @@ #include "access/sysattr.h" #include "access/table.h" #include "access/tableam.h" +#include "access/tupconvert.h" #include "access/xact.h" #include "catalog/catalog.h" #include "catalog/dependency.h" @@ -30,9 +31,9 @@ #include "catalog/pg_proc.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "commands/trigger.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "miscadmin.h" #include "nodes/bitmapset.h" #include "nodes/makefuncs.h" @@ -80,6 +81,7 @@ static bool GetTupleForTrigger(EState *estate, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot *oldslot, + bool do_epq_recheck, TupleTableSlot **epqslot, TM_Result *tmresultp, TM_FailureData *tmfdp); @@ -90,7 +92,7 @@ static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo, static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata, int tgindx, FmgrInfo *finfo, - Instrumentation *instr, + TriggerInstrumentation *instr, MemoryContext per_tuple_context); static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, ResultRelInfo *src_partinfo, @@ -157,7 +159,7 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t * (but see CloneRowTriggersToPartition). */ ObjectAddress -CreateTrigger(CreateTrigStmt *stmt, const char *queryString, +CreateTrigger(const CreateTrigStmt *stmt, const char *queryString, Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid, Oid funcoid, Oid parentTriggerOid, Node *whenClause, bool isInternal, bool in_partition) @@ -174,7 +176,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, * (always/origin/replica/disabled) can be specified. */ ObjectAddress -CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, +CreateTriggerFiringOn(const CreateTrigStmt *stmt, const char *queryString, Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid, Oid funcoid, Oid parentTriggerOid, Node *whenClause, bool isInternal, bool in_partition, @@ -871,7 +873,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, CStringGetDatum(trigname)); values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid); values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype); - values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when; + values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(trigger_fires_when); values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal); values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid); values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid); @@ -1991,7 +1993,7 @@ RelationBuildTriggers(Relation relation) } /* Build trigdesc */ - trigdesc = (TriggerDesc *) palloc0(sizeof(TriggerDesc)); + trigdesc = palloc0_object(TriggerDesc); trigdesc->triggers = triggers; trigdesc->numtriggers = numtrigs; for (i = 0; i < numtrigs; i++) @@ -2096,7 +2098,7 @@ CopyTriggerDesc(TriggerDesc *trigdesc) if (trigdesc == NULL || trigdesc->numtriggers <= 0) return NULL; - newdesc = (TriggerDesc *) palloc(sizeof(TriggerDesc)); + newdesc = palloc_object(TriggerDesc); memcpy(newdesc, trigdesc, sizeof(TriggerDesc)); trigger = (Trigger *) palloc(trigdesc->numtriggers * sizeof(Trigger)); @@ -2284,6 +2286,8 @@ FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc) { Trigger *trigger = &trigdesc->triggers[i]; + if (!TRIGGER_FOR_ROW(trigger->tgtype)) + continue; if (trigger->tgoldtable != NULL || trigger->tgnewtable != NULL) return trigger->tgname; } @@ -2307,7 +2311,7 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata, int tgindx, FmgrInfo *finfo, - Instrumentation *instr, + TriggerInstrumentation *instr, MemoryContext per_tuple_context) { LOCAL_FCINFO(fcinfo, 0); @@ -2342,7 +2346,7 @@ ExecCallTriggerFunc(TriggerData *trigdata, * If doing EXPLAIN ANALYZE, start charging time to this trigger. */ if (instr) - InstrStartNode(instr + tgindx); + InstrStartTrigger(instr + tgindx); /* * Do the function evaluation in the per-tuple memory context, so that @@ -2387,10 +2391,10 @@ ExecCallTriggerFunc(TriggerData *trigdata, /* * If doing EXPLAIN ANALYZE, stop charging time to this trigger, and count - * one "tuple returned" (really the number of firings). + * the firing of the trigger. */ if (instr) - InstrStopNode(instr + tgindx, 1); + InstrStopTrigger(instr + tgindx, 1); return (HeapTuple) DatumGetPointer(result); } @@ -2544,6 +2548,15 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + if (relinfo->ri_FdwRoutine && transition_capture && + transition_capture->tcs_insert_new_table) + { + Assert(relinfo->ri_RootResultRelInfo); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot collect transition tuples from child foreign tables"))); + } + if ((trigdesc && trigdesc->trig_insert_after_row) || (transition_capture && transition_capture->tcs_insert_new_table)) AfterTriggerSaveEvent(estate, relinfo, NULL, NULL, @@ -2693,7 +2706,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, HeapTuple fdw_trigtuple, TupleTableSlot **epqslot, TM_Result *tmresult, - TM_FailureData *tmfd) + TM_FailureData *tmfd, + bool is_merge_delete) { TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo); TriggerDesc *trigdesc = relinfo->ri_TrigDesc; @@ -2708,9 +2722,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, { TupleTableSlot *epqslot_candidate = NULL; + /* + * Get a copy of the on-disk tuple we are planning to delete. In + * general, if the tuple has been concurrently updated, we should + * recheck it using EPQ. However, if this is a MERGE DELETE action, + * we skip this EPQ recheck and leave it to the caller (it must do + * additional rechecking, and might end up executing a different + * action entirely). + */ if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid, - LockTupleExclusive, slot, &epqslot_candidate, - tmresult, tmfd)) + LockTupleExclusive, slot, !is_merge_delete, + &epqslot_candidate, tmresult, tmfd)) return false; /* @@ -2787,6 +2809,15 @@ ExecARDeleteTriggers(EState *estate, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + if (relinfo->ri_FdwRoutine && transition_capture && + transition_capture->tcs_delete_old_table) + { + Assert(relinfo->ri_RootResultRelInfo); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot collect transition tuples from child foreign tables"))); + } + if ((trigdesc && trigdesc->trig_delete_after_row) || (transition_capture && transition_capture->tcs_delete_old_table)) { @@ -2800,6 +2831,7 @@ ExecARDeleteTriggers(EState *estate, tupleid, LockTupleExclusive, slot, + false, NULL, NULL, NULL); @@ -2944,7 +2976,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, HeapTuple fdw_trigtuple, TupleTableSlot *newslot, TM_Result *tmresult, - TM_FailureData *tmfd) + TM_FailureData *tmfd, + bool is_merge_update) { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo); @@ -2965,10 +2998,17 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, { TupleTableSlot *epqslot_candidate = NULL; - /* get a copy of the on-disk tuple we are planning to update */ + /* + * Get a copy of the on-disk tuple we are planning to update. In + * general, if the tuple has been concurrently updated, we should + * recheck it using EPQ. However, if this is a MERGE UPDATE action, + * we skip this EPQ recheck and leave it to the caller (it must do + * additional rechecking, and might end up executing a different + * action entirely). + */ if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid, - lockmode, oldslot, &epqslot_candidate, - tmresult, tmfd)) + lockmode, oldslot, !is_merge_update, + &epqslot_candidate, tmresult, tmfd)) return false; /* cancel the update action */ /* @@ -3115,6 +3155,16 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, { TriggerDesc *trigdesc = relinfo->ri_TrigDesc; + if (relinfo->ri_FdwRoutine && transition_capture && + (transition_capture->tcs_update_old_table || + transition_capture->tcs_update_new_table)) + { + Assert(relinfo->ri_RootResultRelInfo); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot collect transition tuples from child foreign tables"))); + } + if ((trigdesc && trigdesc->trig_update_after_row) || (transition_capture && (transition_capture->tcs_update_old_table || @@ -3142,6 +3192,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, tupleid, LockTupleExclusive, oldslot, + false, NULL, NULL, NULL); @@ -3298,6 +3349,7 @@ GetTupleForTrigger(EState *estate, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot *oldslot, + bool do_epq_recheck, TupleTableSlot **epqslot, TM_Result *tmresultp, TM_FailureData *tmfdp) @@ -3357,29 +3409,30 @@ GetTupleForTrigger(EState *estate, if (tmfd.traversed) { /* - * Recheck the tuple using EPQ. For MERGE, we leave this - * to the caller (it must do additional rechecking, and - * might end up executing a different action entirely). + * Recheck the tuple using EPQ, if requested. Otherwise, + * just return that it was concurrently updated. */ - if (estate->es_plannedstmt->commandType == CMD_MERGE) + if (do_epq_recheck) { - if (tmresultp) - *tmresultp = TM_Updated; - return false; + *epqslot = EvalPlanQual(epqstate, + relation, + relinfo->ri_RangeTableIndex, + oldslot); + + /* + * If PlanQual failed for updated tuple - we must not + * process this tuple! + */ + if (TupIsNull(*epqslot)) + { + *epqslot = NULL; + return false; + } } - - *epqslot = EvalPlanQual(epqstate, - relation, - relinfo->ri_RangeTableIndex, - oldslot); - - /* - * If PlanQual failed for updated tuple - we must not - * process this tuple! - */ - if (TupIsNull(*epqslot)) + else { - *epqslot = NULL; + if (tmresultp) + *tmresultp = TM_Updated; return false; } } @@ -3840,6 +3893,18 @@ typedef struct AfterTriggersData /* per-subtransaction-level data: */ AfterTriggersTransData *trans_stack; /* array of structs shown below */ int maxtransdepth; /* allocated len of above array */ + + List *batch_callbacks; /* List of AfterTriggerCallbackItem; for + * deferred constraints */ + bool firing_batch_callbacks; /* true when in + * FireAfterTriggerBatchCallbacks() */ + + /* + * Incremented around the trigger-firing loops in AfterTriggerEndQuery, + * AfterTriggerFireDeferred, and AfterTriggerSetState. Used by + * AfterTriggerIsActive() to signal that after-trigger firing is active. + */ + int firing_depth; } AfterTriggersData; struct AfterTriggersQueryData @@ -3847,6 +3912,7 @@ struct AfterTriggersQueryData AfterTriggerEventList events; /* events pending from this query */ Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */ List *tables; /* list of AfterTriggersTableData, see below */ + List *batch_callbacks; /* List of AfterTriggerCallbackItem */ }; struct AfterTriggersTransData @@ -3868,25 +3934,21 @@ struct AfterTriggersTableData bool after_trig_done; /* did we already queue AS triggers? */ AfterTriggerEventList after_trig_events; /* if so, saved list pointer */ - /* - * We maintain separate transition tables for UPDATE/INSERT/DELETE since - * MERGE can run all three actions in a single statement. Note that UPDATE - * needs both old and new transition tables whereas INSERT needs only new, - * and DELETE needs only old. - */ - - /* "old" transition table for UPDATE, if any */ - Tuplestorestate *old_upd_tuplestore; - /* "new" transition table for UPDATE, if any */ - Tuplestorestate *new_upd_tuplestore; - /* "old" transition table for DELETE, if any */ - Tuplestorestate *old_del_tuplestore; - /* "new" transition table for INSERT, if any */ - Tuplestorestate *new_ins_tuplestore; + /* "old" transition table for UPDATE/DELETE, if any */ + Tuplestorestate *old_tuplestore; + /* "new" transition table for INSERT/UPDATE, if any */ + Tuplestorestate *new_tuplestore; TupleTableSlot *storeslot; /* for converting to tuplestore's format */ }; +/* Entry in afterTriggers.batch_callbacks */ +typedef struct AfterTriggerCallbackItem +{ + AfterTriggerBatchCallback callback; + void *arg; +} AfterTriggerCallbackItem; + static AfterTriggersData afterTriggers; static void AfterTriggerExecute(EState *estate, @@ -3896,7 +3958,7 @@ static void AfterTriggerExecute(EState *estate, ResultRelInfo *dst_relInfo, TriggerDesc *trigdesc, FmgrInfo *finfo, - Instrumentation *instr, + TriggerInstrumentation *instr, MemoryContext per_tuple_context, TupleTableSlot *trig_tuple_slot1, TupleTableSlot *trig_tuple_slot2); @@ -3909,6 +3971,7 @@ static Tuplestorestate *GetAfterTriggersTransitionTable(int event, TupleTableSlot *newslot, TransitionCaptureState *transition_capture); static void TransitionTableAddTuple(EState *estate, + int event, TransitionCaptureState *transition_capture, ResultRelInfo *relinfo, TupleTableSlot *slot, @@ -3921,6 +3984,7 @@ static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, Oid tgoid, bool tgisdeferred); static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent); +static void FireAfterTriggerBatchCallbacks(List *callbacks); /* * Get the FDW tuplestore for the current trigger query level, creating it @@ -4289,7 +4353,7 @@ AfterTriggerExecute(EState *estate, ResultRelInfo *src_relInfo, ResultRelInfo *dst_relInfo, TriggerDesc *trigdesc, - FmgrInfo *finfo, Instrumentation *instr, + FmgrInfo *finfo, TriggerInstrumentation *instr, MemoryContext per_tuple_context, TupleTableSlot *trig_tuple_slot1, TupleTableSlot *trig_tuple_slot2) @@ -4330,7 +4394,7 @@ AfterTriggerExecute(EState *estate, * to include time spent re-fetching tuples in the trigger cost. */ if (instr) - InstrStartNode(instr + tgindx); + InstrStartTrigger(instr + tgindx); /* * Fetch the required tuple(s). @@ -4351,7 +4415,7 @@ AfterTriggerExecute(EState *estate, trig_tuple_slot2)) elog(ERROR, "failed to fetch tuple2 for AFTER trigger"); } - /* fall through */ + pg_fallthrough; case AFTER_TRIGGER_FDW_REUSE: /* @@ -4476,19 +4540,13 @@ AfterTriggerExecute(EState *estate, { if (LocTriggerData.tg_trigger->tgoldtable) { - if (TRIGGER_FIRED_BY_UPDATE(evtshared->ats_event)) - LocTriggerData.tg_oldtable = evtshared->ats_table->old_upd_tuplestore; - else - LocTriggerData.tg_oldtable = evtshared->ats_table->old_del_tuplestore; + LocTriggerData.tg_oldtable = evtshared->ats_table->old_tuplestore; evtshared->ats_table->closed = true; } if (LocTriggerData.tg_trigger->tgnewtable) { - if (TRIGGER_FIRED_BY_INSERT(evtshared->ats_event)) - LocTriggerData.tg_newtable = evtshared->ats_table->new_ins_tuplestore; - else - LocTriggerData.tg_newtable = evtshared->ats_table->new_upd_tuplestore; + LocTriggerData.tg_newtable = evtshared->ats_table->new_tuplestore; evtshared->ats_table->closed = true; } } @@ -4553,10 +4611,10 @@ AfterTriggerExecute(EState *estate, /* * If doing EXPLAIN ANALYZE, stop charging time to this trigger, and count - * one "tuple returned" (really the number of firings). + * the firing of the trigger. */ if (instr) - InstrStopNode(instr + tgindx, 1); + InstrStopTrigger(instr + tgindx, 1); } @@ -4672,7 +4730,7 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events, Relation rel = NULL; TriggerDesc *trigdesc = NULL; FmgrInfo *finfo = NULL; - Instrumentation *instr = NULL; + TriggerInstrumentation *instr = NULL; TupleTableSlot *slot1 = NULL, *slot2 = NULL; @@ -4835,6 +4893,11 @@ GetAfterTriggersTableData(Oid relid, CmdType cmdType) MemoryContext oldcxt; ListCell *lc; + /* At this level, cmdType should not be, eg, CMD_MERGE */ + Assert(cmdType == CMD_INSERT || + cmdType == CMD_UPDATE || + cmdType == CMD_DELETE); + /* Caller should have ensured query_depth is OK. */ Assert(afterTriggers.query_depth >= 0 && afterTriggers.query_depth < afterTriggers.maxquerydepth); @@ -4850,7 +4913,7 @@ GetAfterTriggersTableData(Oid relid, CmdType cmdType) oldcxt = MemoryContextSwitchTo(CurTransactionContext); - table = (AfterTriggersTableData *) palloc0(sizeof(AfterTriggersTableData)); + table = palloc0_object(AfterTriggersTableData); table->relid = relid; table->cmdType = cmdType; qs->tables = lappend(qs->tables, table); @@ -4921,7 +4984,9 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType) need_new_upd, need_old_del, need_new_ins; - AfterTriggersTableData *table; + AfterTriggersTableData *ins_table; + AfterTriggersTableData *upd_table; + AfterTriggersTableData *del_table; MemoryContext oldcxt; ResourceOwner saveResourceOwner; @@ -4968,10 +5033,15 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType) AfterTriggerEnlargeQueryState(); /* - * Find or create an AfterTriggersTableData struct to hold the + * Find or create AfterTriggersTableData struct(s) to hold the * tuplestore(s). If there's a matching struct but it's marked closed, * ignore it; we need a newer one. * + * Note: MERGE must use the same AfterTriggersTableData structs as INSERT, + * UPDATE, and DELETE, so that any MERGE'd tuples are added to the same + * tuplestores as tuples from any INSERT, UPDATE, or DELETE commands + * running in the same top-level command (e.g., in a writable CTE). + * * Note: the AfterTriggersTableData list, as well as the tuplestores, are * allocated in the current (sub)transaction's CurTransactionContext, and * the tuplestores are managed by the (sub)transaction's resource owner. @@ -4979,32 +5049,47 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType) * transition tables to be deferrable; they will be fired during * AfterTriggerEndQuery, after which it's okay to delete the data. */ - table = GetAfterTriggersTableData(relid, cmdType); + if (need_new_ins) + ins_table = GetAfterTriggersTableData(relid, CMD_INSERT); + else + ins_table = NULL; + + if (need_old_upd || need_new_upd) + upd_table = GetAfterTriggersTableData(relid, CMD_UPDATE); + else + upd_table = NULL; + + if (need_old_del) + del_table = GetAfterTriggersTableData(relid, CMD_DELETE); + else + del_table = NULL; /* Now create required tuplestore(s), if we don't have them already. */ oldcxt = MemoryContextSwitchTo(CurTransactionContext); saveResourceOwner = CurrentResourceOwner; CurrentResourceOwner = CurTransactionResourceOwner; - if (need_old_upd && table->old_upd_tuplestore == NULL) - table->old_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem); - if (need_new_upd && table->new_upd_tuplestore == NULL) - table->new_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem); - if (need_old_del && table->old_del_tuplestore == NULL) - table->old_del_tuplestore = tuplestore_begin_heap(false, false, work_mem); - if (need_new_ins && table->new_ins_tuplestore == NULL) - table->new_ins_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_old_upd && upd_table->old_tuplestore == NULL) + upd_table->old_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_new_upd && upd_table->new_tuplestore == NULL) + upd_table->new_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_old_del && del_table->old_tuplestore == NULL) + del_table->old_tuplestore = tuplestore_begin_heap(false, false, work_mem); + if (need_new_ins && ins_table->new_tuplestore == NULL) + ins_table->new_tuplestore = tuplestore_begin_heap(false, false, work_mem); CurrentResourceOwner = saveResourceOwner; MemoryContextSwitchTo(oldcxt); /* Now build the TransitionCaptureState struct, in caller's context */ - state = (TransitionCaptureState *) palloc0(sizeof(TransitionCaptureState)); + state = palloc0_object(TransitionCaptureState); state->tcs_delete_old_table = need_old_del; state->tcs_update_old_table = need_old_upd; state->tcs_update_new_table = need_new_upd; state->tcs_insert_new_table = need_new_ins; - state->tcs_private = table; + state->tcs_insert_private = ins_table; + state->tcs_update_private = upd_table; + state->tcs_delete_private = del_table; return state; } @@ -5025,6 +5110,9 @@ AfterTriggerBeginXact(void) */ afterTriggers.firing_counter = (CommandId) 1; /* mustn't be 0 */ afterTriggers.query_depth = -1; + afterTriggers.firing_depth = 0; + afterTriggers.batch_callbacks = NIL; + afterTriggers.firing_batch_callbacks = false; /* * Verify that there is no leftover state remaining. If these assertions @@ -5109,6 +5197,7 @@ AfterTriggerEndQuery(EState *estate) */ qs = &afterTriggers.query_stack[afterTriggers.query_depth]; + afterTriggers.firing_depth++; for (;;) { if (afterTriggerMarkEvents(&qs->events, &afterTriggers.events, true)) @@ -5146,10 +5235,18 @@ AfterTriggerEndQuery(EState *estate) break; } + /* + * Fire batch callbacks before releasing query-level storage and before + * decrementing query_depth. Callbacks may do real work (index probes, + * error reporting). + */ + FireAfterTriggerBatchCallbacks(qs->batch_callbacks); + /* Release query-level-local storage, including tuplestores if any */ AfterTriggerFreeQuery(&afterTriggers.query_stack[afterTriggers.query_depth]); afterTriggers.query_depth--; + afterTriggers.firing_depth--; } @@ -5182,20 +5279,12 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) { AfterTriggersTableData *table = (AfterTriggersTableData *) lfirst(lc); - ts = table->old_upd_tuplestore; - table->old_upd_tuplestore = NULL; + ts = table->old_tuplestore; + table->old_tuplestore = NULL; if (ts) tuplestore_end(ts); - ts = table->new_upd_tuplestore; - table->new_upd_tuplestore = NULL; - if (ts) - tuplestore_end(ts); - ts = table->old_del_tuplestore; - table->old_del_tuplestore = NULL; - if (ts) - tuplestore_end(ts); - ts = table->new_ins_tuplestore; - table->new_ins_tuplestore = NULL; + ts = table->new_tuplestore; + table->new_tuplestore = NULL; if (ts) tuplestore_end(ts); if (table->storeslot) @@ -5214,6 +5303,9 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs) */ qs->tables = NIL; list_free_deep(tables); + + list_free_deep(qs->batch_callbacks); + qs->batch_callbacks = NIL; } @@ -5253,6 +5345,7 @@ AfterTriggerFireDeferred(void) * Run all the remaining triggers. Loop until they are all gone, in case * some trigger queues more for us to do. */ + afterTriggers.firing_depth++; while (afterTriggerMarkEvents(events, NULL, false)) { CommandId firing_id = afterTriggers.firing_counter++; @@ -5261,9 +5354,15 @@ AfterTriggerFireDeferred(void) break; /* all fired */ } + /* Flush any fast-path batches accumulated by the triggers just fired. */ + FireAfterTriggerBatchCallbacks(afterTriggers.batch_callbacks); + + afterTriggers.firing_depth--; + /* - * We don't bother freeing the event list, since it will go away anyway - * (and more efficiently than via pfree) in AfterTriggerEndXact. + * We don't bother freeing the event list or batch_callbacks, since they + * will go away anyway (and more efficiently than via pfree) in + * AfterTriggerEndXact. */ if (snap_pushed) @@ -5325,6 +5424,12 @@ AfterTriggerEndXact(bool isCommit) /* No more afterTriggers manipulation until next transaction starts. */ afterTriggers.query_depth = -1; + + afterTriggers.firing_depth = 0; + + list_free_deep(afterTriggers.batch_callbacks); + afterTriggers.batch_callbacks = NIL; + afterTriggers.firing_batch_callbacks = false; } /* @@ -5471,6 +5576,9 @@ AfterTriggerEndSubXact(bool isCommit) } } } + + /* Reset in case a callback threw an error while firing. */ + afterTriggers.firing_batch_callbacks = false; } /* @@ -5506,17 +5614,17 @@ GetAfterTriggersTransitionTable(int event, { Assert(TupIsNull(newslot)); if (event == TRIGGER_EVENT_DELETE && delete_old_table) - tuplestore = transition_capture->tcs_private->old_del_tuplestore; + tuplestore = transition_capture->tcs_delete_private->old_tuplestore; else if (event == TRIGGER_EVENT_UPDATE && update_old_table) - tuplestore = transition_capture->tcs_private->old_upd_tuplestore; + tuplestore = transition_capture->tcs_update_private->old_tuplestore; } else if (!TupIsNull(newslot)) { Assert(TupIsNull(oldslot)); if (event == TRIGGER_EVENT_INSERT && insert_new_table) - tuplestore = transition_capture->tcs_private->new_ins_tuplestore; + tuplestore = transition_capture->tcs_insert_private->new_tuplestore; else if (event == TRIGGER_EVENT_UPDATE && update_new_table) - tuplestore = transition_capture->tcs_private->new_upd_tuplestore; + tuplestore = transition_capture->tcs_update_private->new_tuplestore; } return tuplestore; @@ -5530,6 +5638,7 @@ GetAfterTriggersTransitionTable(int event, */ static void TransitionTableAddTuple(EState *estate, + int event, TransitionCaptureState *transition_capture, ResultRelInfo *relinfo, TupleTableSlot *slot, @@ -5548,9 +5657,26 @@ TransitionTableAddTuple(EState *estate, tuplestore_puttupleslot(tuplestore, original_insert_tuple); else if ((map = ExecGetChildToRootMap(relinfo)) != NULL) { - AfterTriggersTableData *table = transition_capture->tcs_private; + AfterTriggersTableData *table; TupleTableSlot *storeslot; + switch (event) + { + case TRIGGER_EVENT_INSERT: + table = transition_capture->tcs_insert_private; + break; + case TRIGGER_EVENT_UPDATE: + table = transition_capture->tcs_update_private; + break; + case TRIGGER_EVENT_DELETE: + table = transition_capture->tcs_delete_private; + break; + default: + elog(ERROR, "invalid after-trigger event code: %d", event); + table = NULL; /* keep compiler quiet */ + break; + } + storeslot = GetAfterTriggersStoreSlot(table, map->outdesc); execute_attr_map_slot(map->attrMap, slot, storeslot); tuplestore_puttupleslot(tuplestore, storeslot); @@ -5607,6 +5733,7 @@ AfterTriggerEnlargeQueryState(void) qs->events.tailfree = NULL; qs->fdw_tuplestore = NULL; qs->tables = NIL; + qs->batch_callbacks = NIL; ++init_depth; } @@ -5956,6 +6083,7 @@ AfterTriggerSetState(ConstraintsSetStmt *stmt) AfterTriggerEventList *events = &afterTriggers.events; bool snapshot_set = false; + afterTriggers.firing_depth++; while (afterTriggerMarkEvents(events, NULL, true)) { CommandId firing_id = afterTriggers.firing_counter++; @@ -5985,6 +6113,14 @@ AfterTriggerSetState(ConstraintsSetStmt *stmt) break; /* all fired */ } + /* + * Flush any fast-path batches accumulated by the triggers just fired. + */ + FireAfterTriggerBatchCallbacks(afterTriggers.batch_callbacks); + afterTriggers.firing_depth--; + list_free_deep(afterTriggers.batch_callbacks); + afterTriggers.batch_callbacks = NIL; + if (snapshot_set) PopActiveSnapshot(); } @@ -6144,7 +6280,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, oldslot, NULL, transition_capture); - TransitionTableAddTuple(estate, transition_capture, relinfo, + TransitionTableAddTuple(estate, event, transition_capture, relinfo, oldslot, NULL, old_tuplestore); } @@ -6160,7 +6296,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, NULL, newslot, transition_capture); - TransitionTableAddTuple(estate, transition_capture, relinfo, + TransitionTableAddTuple(estate, event, transition_capture, relinfo, newslot, original_insert_tuple, new_tuplestore); } @@ -6463,7 +6599,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, new_shared.ats_firing_id = 0; if ((trigger->tgoldtable || trigger->tgnewtable) && transition_capture != NULL) - new_shared.ats_table = transition_capture->tcs_private; + { + switch (event) + { + case TRIGGER_EVENT_INSERT: + new_shared.ats_table = transition_capture->tcs_insert_private; + break; + case TRIGGER_EVENT_UPDATE: + new_shared.ats_table = transition_capture->tcs_update_private; + break; + case TRIGGER_EVENT_DELETE: + new_shared.ats_table = transition_capture->tcs_delete_private; + break; + default: + /* Must be TRUNCATE, see switch above */ + new_shared.ats_table = NULL; + break; + } + } else new_shared.ats_table = NULL; new_shared.ats_modifiedcols = modifiedCols; @@ -6664,3 +6817,85 @@ check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple tuple) return tuple; } + +/* + * RegisterAfterTriggerBatchCallback + * Register a function to be called when the current trigger-firing + * batch completes. + * + * Must be called from within a trigger function's execution context + * (i.e., while afterTriggers state is active). + * + * The callback list is cleared after invocation, so the caller must + * re-register for each new batch if needed. + */ +void +RegisterAfterTriggerBatchCallback(AfterTriggerBatchCallback callback, + void *arg) +{ + AfterTriggerCallbackItem *item; + MemoryContext oldcxt; + + /* + * Allocate in TopTransactionContext so the item survives for the duration + * of the batch, which may span multiple trigger invocations. + * + * Must be called while afterTriggers is active; callbacks registered + * outside a trigger-firing context would never fire. + */ + Assert(afterTriggers.firing_depth > 0); + Assert(!afterTriggers.firing_batch_callbacks); + oldcxt = MemoryContextSwitchTo(TopTransactionContext); + item = palloc(sizeof(AfterTriggerCallbackItem)); + item->callback = callback; + item->arg = arg; + if (afterTriggers.query_depth >= 0) + { + AfterTriggersQueryData *qs = + &afterTriggers.query_stack[afterTriggers.query_depth]; + + qs->batch_callbacks = lappend(qs->batch_callbacks, item); + } + else + afterTriggers.batch_callbacks = + lappend(afterTriggers.batch_callbacks, item); + MemoryContextSwitchTo(oldcxt); +} + +/* + * FireAfterTriggerBatchCallbacks + * Invoke all callbacks in the given list. + * + * Memory cleanup of the list and its items is handled by the caller + * (AfterTriggerFreeQuery for query-level callbacks, AfterTriggerEndXact + * for top-level deferred callbacks). + */ +static void +FireAfterTriggerBatchCallbacks(List *callbacks) +{ + ListCell *lc; + + Assert(afterTriggers.firing_depth > 0); + afterTriggers.firing_batch_callbacks = true; + foreach(lc, callbacks) + { + AfterTriggerCallbackItem *item = lfirst(lc); + + item->callback(item->arg); + } + afterTriggers.firing_batch_callbacks = false; +} + +/* + * AfterTriggerIsActive + * Returns true if we're inside the after-trigger framework where + * registered batch callbacks will actually be invoked. + * + * This is false during validateForeignKeyConstraint(), which calls + * RI trigger functions directly outside the after-trigger framework. + */ +bool +AfterTriggerIsActive(void) +{ + return afterTriggers.firing_depth > 0; +} diff --git a/src/backend/commands/tsearchcmds.c b/src/backend/commands/tsearchcmds.c index ab16d42ad56ba..a12ce33bae4ed 100644 --- a/src/backend/commands/tsearchcmds.c +++ b/src/backend/commands/tsearchcmds.c @@ -4,7 +4,7 @@ * * Routines for tsearch manipulation commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -1027,7 +1027,7 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied) * know that they will be used. */ max_slots = MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_ts_config_map); - slot = palloc(sizeof(TupleTableSlot *) * max_slots); + slot = palloc_array(TupleTableSlot *, max_slots); ScanKeyInit(&skey, Anum_pg_ts_config_map_mapcfg, @@ -1058,10 +1058,10 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied) memset(slot[slot_stored_count]->tts_isnull, false, slot[slot_stored_count]->tts_tupleDescriptor->natts * sizeof(bool)); - slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapcfg - 1] = cfgOid; - slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_maptokentype - 1] = cfgmap->maptokentype; - slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = cfgmap->mapseqno; - slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = cfgmap->mapdict; + slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapcfg - 1] = ObjectIdGetDatum(cfgOid); + slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_maptokentype - 1] = Int32GetDatum(cfgmap->maptokentype); + slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = Int32GetDatum(cfgmap->mapseqno); + slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(cfgmap->mapdict); ExecStoreVirtualTuple(slot[slot_stored_count]); slot_stored_count++; @@ -1261,7 +1261,7 @@ getTokenTypes(Oid prsId, List *tokennames) { if (strcmp(strVal(val), list[j].alias) == 0) { - TSTokenTypeItem *ts = (TSTokenTypeItem *) palloc0(sizeof(TSTokenTypeItem)); + TSTokenTypeItem *ts = palloc0_object(TSTokenTypeItem); ts->num = list[j].lexid; ts->name = pstrdup(strVal(val)); @@ -1344,7 +1344,7 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt, * Convert list of dictionary names to array of dict OIDs */ ndict = list_length(stmt->dicts); - dictIds = (Oid *) palloc(sizeof(Oid) * ndict); + dictIds = palloc_array(Oid, ndict); i = 0; foreach(c, stmt->dicts) { @@ -1432,7 +1432,7 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt, /* Allocate the slots to use and initialize them */ nslots = Min(ntoken * ndict, MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_ts_config_map)); - slot = palloc(sizeof(TupleTableSlot *) * nslots); + slot = palloc_array(TupleTableSlot *, nslots); for (i = 0; i < nslots; i++) slot[i] = MakeSingleTupleTableSlot(RelationGetDescr(relMap), &TTSOpsHeapTuple); diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 45ae7472ab5ad..cd38e9cddf46b 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3,7 +3,7 @@ * typecmds.c * Routines for SQL commands that manipulate types (and domains). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -111,10 +111,12 @@ Oid binary_upgrade_next_mrng_pg_type_oid = InvalidOid; Oid binary_upgrade_next_mrng_array_pg_type_oid = InvalidOid; static void makeRangeConstructors(const char *name, Oid namespace, - Oid rangeOid, Oid subtype); + Oid rangeOid, Oid subtype, + Oid *rangeConstruct2_p, Oid *rangeConstruct3_p); static void makeMultirangeConstructors(const char *name, Oid namespace, Oid multirangeOid, Oid rangeOid, - Oid rangeArrayOid, Oid *castFuncOid); + Oid rangeArrayOid, + Oid *mltrngConstruct0_p, Oid *mltrngConstruct1_p, Oid *mltrngConstruct2_p); static Oid findTypeInputFunction(List *procname, Oid typeOid); static Oid findTypeOutputFunction(List *procname, Oid typeOid); static Oid findTypeReceiveFunction(List *procname, Oid typeOid); @@ -126,7 +128,7 @@ static Oid findTypeSubscriptingFunction(List *procname, Oid typeOid); static Oid findRangeSubOpclass(List *opcname, Oid subtype); static Oid findRangeCanonicalFunction(List *procname, Oid typeOid); static Oid findRangeSubtypeDiffFunction(List *procname, Oid subtype); -static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin); +static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin, LOCKMODE lockmode); static void validateDomainNotNullConstraint(Oid domainoid); static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode); static void checkEnumOwner(HeapTuple tup); @@ -939,11 +941,19 @@ DefineDomain(ParseState *pstate, CreateDomainStmt *stmt) break; case CONSTR_NOTNULL: - if (nullDefined && !typNotNull) + if (nullDefined) + { + if (!typNotNull) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting NULL/NOT NULL constraints"), + parser_errposition(pstate, constr->location)); + ereport(ERROR, - errcode(ERRCODE_SYNTAX_ERROR), - errmsg("conflicting NULL/NOT NULL constraints"), + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("redundant NOT NULL constraint definition"), parser_errposition(pstate, constr->location)); + } if (constr->is_no_inherit) ereport(ERROR, errcode(ERRCODE_INVALID_OBJECT_DEFINITION), @@ -1398,6 +1408,11 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt) ListCell *lc; ObjectAddress address; ObjectAddress mltrngaddress PG_USED_FOR_ASSERTS_ONLY; + Oid rangeConstruct2Oid = InvalidOid; + Oid rangeConstruct3Oid = InvalidOid; + Oid mltrngConstruct0Oid = InvalidOid; + Oid mltrngConstruct1Oid = InvalidOid; + Oid mltrngConstruct2Oid = InvalidOid; Oid castFuncOid; /* Convert list of names to a name and namespace */ @@ -1653,10 +1668,6 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt) InvalidOid); /* type's collation (ranges never have one) */ Assert(multirangeOid == mltrngaddress.objectId); - /* Create the entry in pg_range */ - RangeCreate(typoid, rangeSubtype, rangeCollation, rangeSubOpclass, - rangeCanonical, rangeSubtypeDiff, multirangeOid); - /* * Create the array type that goes with it. */ @@ -1734,11 +1745,22 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt) false, /* Type NOT NULL */ InvalidOid); /* typcollation */ + /* Ensure these new types are visible to ProcedureCreate */ + CommandCounterIncrement(); + /* And create the constructor functions for this range type */ - makeRangeConstructors(typeName, typeNamespace, typoid, rangeSubtype); + makeRangeConstructors(typeName, typeNamespace, typoid, rangeSubtype, + &rangeConstruct2Oid, &rangeConstruct3Oid); makeMultirangeConstructors(multirangeTypeName, typeNamespace, multirangeOid, typoid, rangeArrayOid, - &castFuncOid); + &mltrngConstruct0Oid, &mltrngConstruct1Oid, &mltrngConstruct2Oid); + castFuncOid = mltrngConstruct1Oid; + + /* Create the entry in pg_range */ + RangeCreate(typoid, rangeSubtype, rangeCollation, rangeSubOpclass, + rangeCanonical, rangeSubtypeDiff, multirangeOid, + rangeConstruct2Oid, rangeConstruct3Oid, + mltrngConstruct0Oid, mltrngConstruct1Oid, mltrngConstruct2Oid); /* Create cast from the range type to its multirange type */ CastCreate(typoid, multirangeOid, castFuncOid, InvalidOid, InvalidOid, @@ -1756,12 +1778,16 @@ DefineRange(ParseState *pstate, CreateRangeStmt *stmt) * impossible to define a polymorphic constructor; we have to generate new * constructor functions explicitly for each range type. * - * We actually define 4 functions, with 0 through 3 arguments. This is just + * We actually define 2 functions, with 2 through 3 arguments. This is just * to offer more convenience for the user. + * + * The OIDs of the created functions are returned through the pointer + * arguments. */ static void makeRangeConstructors(const char *name, Oid namespace, - Oid rangeOid, Oid subtype) + Oid rangeOid, Oid subtype, + Oid *rangeConstruct2_p, Oid *rangeConstruct3_p) { static const char *const prosrc[2] = {"range_constructor2", "range_constructor3"}; @@ -1822,6 +1848,11 @@ makeRangeConstructors(const char *name, Oid namespace, * pg_dump depends on this choice to avoid dumping the constructors. */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); + + if (pronargs[i] == 2) + *rangeConstruct2_p = myself.objectId; + else if (pronargs[i] == 3) + *rangeConstruct3_p = myself.objectId; } } @@ -1831,13 +1862,13 @@ makeRangeConstructors(const char *name, Oid namespace, * If we had an anyrangearray polymorphic type we could use it here, * but since each type has its own constructor name there's no need. * - * Sets castFuncOid to the oid of the new constructor that can be used - * to cast from a range to a multirange. + * The OIDs of the created functions are returned through the pointer + * arguments. */ static void makeMultirangeConstructors(const char *name, Oid namespace, Oid multirangeOid, Oid rangeOid, Oid rangeArrayOid, - Oid *castFuncOid) + Oid *mltrngConstruct0_p, Oid *mltrngConstruct1_p, Oid *mltrngConstruct2_p) { ObjectAddress myself, referenced; @@ -1888,6 +1919,7 @@ makeMultirangeConstructors(const char *name, Oid namespace, * depends on this choice to avoid dumping the constructors. */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); + *mltrngConstruct0_p = myself.objectId; pfree(argtypes); /* @@ -1928,8 +1960,8 @@ makeMultirangeConstructors(const char *name, Oid namespace, 0.0); /* prorows */ /* ditto */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); + *mltrngConstruct1_p = myself.objectId; pfree(argtypes); - *castFuncOid = myself.objectId; /* n-arg constructor - vararg */ argtypes = buildoidvector(&rangeArrayOid, 1); @@ -1967,6 +1999,7 @@ makeMultirangeConstructors(const char *name, Oid namespace, 0.0); /* prorows */ /* ditto */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); + *mltrngConstruct2_p = myself.objectId; pfree(argtypes); pfree(allParameterTypes); pfree(parameterModes); @@ -2978,7 +3011,7 @@ AlterDomainAddConstraint(List *names, Node *newConstraint, * to. */ if (!constr->skip_validation) - validateDomainCheckConstraint(domainoid, ccbin); + validateDomainCheckConstraint(domainoid, ccbin, ShareLock); /* * We must send out an sinval message for the domain, to ensure that @@ -3019,6 +3052,9 @@ AlterDomainAddConstraint(List *names, Node *newConstraint, * AlterDomainValidateConstraint * * Implements the ALTER DOMAIN .. VALIDATE CONSTRAINT statement. + * + * Return value is the address of the validated constraint. If the constraint + * was already validated, InvalidObjectAddress is returned. */ ObjectAddress AlterDomainValidateConstraint(List *names, const char *constrName) @@ -3036,7 +3072,7 @@ AlterDomainValidateConstraint(List *names, const char *constrName) HeapTuple tuple; HeapTuple copyTuple; ScanKeyData skey[3]; - ObjectAddress address; + ObjectAddress address = InvalidObjectAddress; /* Make a TypeName so we can use standard type lookup machinery */ typename = makeTypeNameFromNameList(names); @@ -3087,24 +3123,32 @@ AlterDomainValidateConstraint(List *names, const char *constrName) errmsg("constraint \"%s\" of domain \"%s\" is not a check constraint", constrName, TypeNameToString(typename)))); - val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin); - conbin = TextDatumGetCString(val); + if (!con->convalidated) + { + val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin); + conbin = TextDatumGetCString(val); - validateDomainCheckConstraint(domainoid, conbin); + /* + * Locking related relations with ShareUpdateExclusiveLock is ok + * because not-yet-valid constraints are still enforced against + * concurrent inserts or updates. + */ + validateDomainCheckConstraint(domainoid, conbin, ShareUpdateExclusiveLock); - /* - * Now update the catalog, while we have the door open. - */ - copyTuple = heap_copytuple(tuple); - copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); - copy_con->convalidated = true; - CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); + /* + * Now update the catalog, while we have the door open. + */ + copyTuple = heap_copytuple(tuple); + copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple); + copy_con->convalidated = true; + CatalogTupleUpdate(conrel, ©Tuple->t_self, copyTuple); - InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); + InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0); - ObjectAddressSet(address, TypeRelationId, domainoid); + ObjectAddressSet(address, TypeRelationId, domainoid); - heap_freetuple(copyTuple); + heap_freetuple(copyTuple); + } systable_endscan(scan); @@ -3141,7 +3185,8 @@ validateDomainNotNullConstraint(Oid domainoid) /* Scan all tuples in this relation */ snapshot = RegisterSnapshot(GetLatestSnapshot()); - scan = table_beginscan(testrel, snapshot, 0, NULL); + scan = table_beginscan(testrel, snapshot, 0, NULL, + SO_NONE); slot = table_slot_create(testrel, NULL); while (table_scan_getnextslot(scan, ForwardScanDirection, slot)) { @@ -3183,9 +3228,16 @@ validateDomainNotNullConstraint(Oid domainoid) /* * Verify that all columns currently using the domain satisfy the given check * constraint expression. + * + * It is used to validate existing constraints and to add newly created check + * constraints to a domain. + * + * The lockmode is used for relations using the domain. It should be + * ShareLock when adding a new constraint to domain. It can be + * ShareUpdateExclusiveLock when validating an existing constraint. */ static void -validateDomainCheckConstraint(Oid domainoid, const char *ccbin) +validateDomainCheckConstraint(Oid domainoid, const char *ccbin, LOCKMODE lockmode) { Expr *expr = (Expr *) stringToNode(ccbin); List *rels; @@ -3202,9 +3254,7 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin) exprstate = ExecPrepareExpr(expr, estate); /* Fetch relation list with attributes based on this domain */ - /* ShareLock is sufficient to prevent concurrent data changes */ - - rels = get_rels_with_domain(domainoid, ShareLock); + rels = get_rels_with_domain(domainoid, lockmode); foreach(rt, rels) { @@ -3217,7 +3267,8 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin) /* Scan all tuples in this relation */ snapshot = RegisterSnapshot(GetLatestSnapshot()); - scan = table_beginscan(testrel, snapshot, 0, NULL); + scan = table_beginscan(testrel, snapshot, 0, NULL, + SO_NONE); slot = table_slot_create(testrel, NULL); while (table_scan_getnextslot(scan, ForwardScanDirection, slot)) { @@ -3230,7 +3281,6 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin) Datum d; bool isNull; Datum conResult; - Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); d = slot_getattr(slot, attnum, &isNull); @@ -3243,6 +3293,8 @@ validateDomainCheckConstraint(Oid domainoid, const char *ccbin) if (!isNull && !DatumGetBool(conResult)) { + Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1); + /* * In principle the auxiliary information for this error * should be errdomainconstraint(), but errtablecol() @@ -3425,10 +3477,10 @@ get_rels_with_domain(Oid domainOid, LOCKMODE lockmode) } /* Build the RelToCheck entry with enough space for all atts */ - rtc = (RelToCheck *) palloc(sizeof(RelToCheck)); + rtc = palloc_object(RelToCheck); rtc->rel = rel; rtc->natts = 0; - rtc->atts = (int *) palloc(sizeof(int) * RelationGetNumberOfAttributes(rel)); + rtc->atts = palloc_array(int, RelationGetNumberOfAttributes(rel)); result = lappend(result, rtc); } diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 0d638e29d0066..be11c49f919d0 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -3,7 +3,7 @@ * user.c * Commands for manipulating roles (formerly called users). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/commands/user.c @@ -30,9 +30,9 @@ #include "commands/defrem.h" #include "commands/seclabel.h" #include "commands/user.h" -#include "lib/qunique.h" #include "libpq/crypt.h" #include "miscadmin.h" +#include "port/pg_bitutils.h" #include "storage/lmgr.h" #include "utils/acl.h" #include "utils/builtins.h" @@ -171,6 +171,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) DefElem *dbypassRLS = NULL; GrantRoleOptions popt; + /* Report error if name has \n or \r character. */ + if (strpbrk(stmt->role, "\n\r")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("role name \"%s\" contains a newline or carriage return character", stmt->role))); + /* The defaults can vary depending on the original statement type */ switch (stmt->stmt_type) { @@ -490,7 +496,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) * Advance command counter so we can see new record; else tests in * AddRoleMems may fail. */ - CommandCounterIncrement(); + if (addroleto || adminmembers || rolemembers) + CommandCounterIncrement(); /* Default grant. */ InitGrantRoleOptions(&popt); @@ -1347,6 +1354,12 @@ RenameRole(const char *oldname, const char *newname) ObjectAddress address; Form_pg_authid authform; + /* Report error if name has \n or \r character. */ + if (strpbrk(newname, "\n\r")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("role name \"%s\" contains a newline or carriage return character", newname))); + rel = table_open(AuthIdRelationId, RowExclusiveLock); dsc = RelationGetDescr(rel); @@ -1904,8 +1917,7 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid, else { Oid objectId; - Oid *newmembers = (Oid *) palloc(3 * sizeof(Oid)); - int nnewmembers; + Oid *newmembers = palloc_object(Oid); /* * The values for these options can be taken directly from 'popt'. @@ -1924,7 +1936,7 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid, */ if ((popt->specified & GRANT_ROLE_SPECIFIED_INHERIT) != 0) new_record[Anum_pg_auth_members_inherit_option - 1] = - popt->inherit; + BoolGetDatum(popt->inherit); else { HeapTuple mrtup; @@ -1935,34 +1947,24 @@ AddRoleMems(Oid currentUserId, const char *rolename, Oid roleid, elog(ERROR, "cache lookup failed for role %u", memberid); mrform = (Form_pg_authid) GETSTRUCT(mrtup); new_record[Anum_pg_auth_members_inherit_option - 1] = - mrform->rolinherit; + BoolGetDatum(mrform->rolinherit); ReleaseSysCache(mrtup); } /* get an OID for the new row and insert it */ objectId = GetNewOidWithIndex(pg_authmem_rel, AuthMemOidIndexId, Anum_pg_auth_members_oid); - new_record[Anum_pg_auth_members_oid - 1] = objectId; + new_record[Anum_pg_auth_members_oid - 1] = ObjectIdGetDatum(objectId); tuple = heap_form_tuple(pg_authmem_dsc, new_record, new_record_nulls); CatalogTupleInsert(pg_authmem_rel, tuple); - /* - * Record dependencies on the roleid, member, and grantor, as if a - * pg_auth_members entry were an object ACL. - * updateAclDependencies() requires an input array that is - * palloc'd (it will free it), sorted, and de-duped. - */ - newmembers[0] = roleid; - newmembers[1] = memberid; - newmembers[2] = grantorId; - qsort(newmembers, 3, sizeof(Oid), oid_cmp); - nnewmembers = qunique(newmembers, 3, sizeof(Oid), oid_cmp); - + /* updateAclDependencies wants to pfree array inputs */ + newmembers[0] = grantorId; updateAclDependencies(AuthMemRelationId, objectId, 0, InvalidOid, 0, NULL, - nnewmembers, newmembers); + 1, newmembers); } /* CCI after each change, in case there are duplicates in list */ @@ -2306,7 +2308,7 @@ initialize_revoke_actions(CatCList *memlist) if (memlist->n_members == 0) return NULL; - result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members); + result = palloc_array(RevokeRoleGrantAction, memlist->n_members); for (i = 0; i < memlist->n_members; i++) result[i] = RRG_NOOP; return result; diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 33a33bf6b1cfa..99d0db82ed7f4 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -9,10 +9,10 @@ * * VACUUM for heap AM is implemented in vacuumlazy.c, parallel vacuum in * vacuumparallel.c, ANALYZE in analyze.c, and VACUUM FULL is a variant of - * CLUSTER, handled in cluster.c. + * REPACK, handled in repack.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -37,9 +37,10 @@ #include "catalog/namespace.h" #include "catalog/pg_database.h" #include "catalog/pg_inherits.h" -#include "commands/cluster.h" +#include "commands/async.h" #include "commands/defrem.h" #include "commands/progress.h" +#include "commands/repack.h" #include "commands/vacuum.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -56,9 +57,11 @@ #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/guc_hooks.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/wait_event.h" /* * Minimum interval for cost-based vacuum delay reports from a parallel worker. @@ -123,8 +126,8 @@ static void vac_truncate_clog(TransactionId frozenXID, MultiXactId minMulti, TransactionId lastSaneFrozenXid, MultiXactId lastSaneMinMulti); -static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, - BufferAccessStrategy bstrategy); +static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, + BufferAccessStrategy bstrategy, bool isTopLevel); static double compute_parallel_delay(void); static VacOptValue get_vacoptval_from_boolean(DefElem *def); static bool vac_tid_reaped(ItemPointer itemptr, void *state); @@ -219,9 +222,10 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("BUFFER_USAGE_LIMIT option must be 0 or between %d kB and %d kB", + errmsg("%s option must be 0 or between %d kB and %d kB", + "BUFFER_USAGE_LIMIT", MIN_BAS_VAC_RING_SIZE_KB, MAX_BAS_VAC_RING_SIZE_KB), - hintmsg ? errhint("%s", _(hintmsg)) : 0)); + hintmsg ? errhint_internal("%s", _(hintmsg)) : 0)); } ring_size = result; @@ -229,7 +233,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) else if (!vacstmt->is_vacuumcmd) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized ANALYZE option \"%s\"", opt->defname), + errmsg("unrecognized %s option \"%s\"", + "ANALYZE", opt->defname), parser_errposition(pstate, opt->location))); /* Parse options available on VACUUM */ @@ -265,35 +270,24 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) params.truncate = get_vacoptval_from_boolean(opt); else if (strcmp(opt->defname, "parallel") == 0) { - if (opt->arg == NULL) - { + int nworkers = defGetInt32(opt); + + if (nworkers < 0 || nworkers > MAX_PARALLEL_WORKER_LIMIT) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("parallel option requires a value between 0 and %d", + errmsg("%s option must be between 0 and %d", + "PARALLEL", MAX_PARALLEL_WORKER_LIMIT), parser_errposition(pstate, opt->location))); - } - else - { - int nworkers; - nworkers = defGetInt32(opt); - if (nworkers < 0 || nworkers > MAX_PARALLEL_WORKER_LIMIT) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("parallel workers for vacuum must be between 0 and %d", - MAX_PARALLEL_WORKER_LIMIT), - parser_errposition(pstate, opt->location))); - - /* - * Disable parallel vacuum, if user has specified parallel - * degree as zero. - */ - if (nworkers == 0) - params.nworkers = -1; - else - params.nworkers = nworkers; - } + /* + * Disable parallel vacuum, if user has specified parallel degree + * as zero. + */ + if (nworkers == 0) + params.nworkers = -1; + else + params.nworkers = nworkers; } else if (strcmp(opt->defname, "skip_database_stats") == 0) skip_database_stats = defGetBoolean(opt); @@ -302,7 +296,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("unrecognized VACUUM option \"%s\"", opt->defname), + errmsg("unrecognized %s option \"%s\"", + "VACUUM", opt->defname), parser_errposition(pstate, opt->location))); } @@ -357,7 +352,6 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) } } - /* * Sanity check DISABLE_PAGE_SKIPPING option. */ @@ -415,8 +409,12 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) /* user-invoked vacuum is never "for wraparound" */ params.is_wraparound = false; - /* user-invoked vacuum uses VACOPT_VERBOSE instead of log_min_duration */ - params.log_min_duration = -1; + /* + * user-invoked vacuum uses VACOPT_VERBOSE instead of + * log_vacuum_min_duration and log_analyze_min_duration + */ + params.log_vacuum_min_duration = -1; + params.log_analyze_min_duration = -1; /* * Later, in vacuum_rel(), we check if a reloption override was specified. @@ -493,7 +491,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) * memory context that will not disappear at transaction commit. */ void -vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy, +vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy, MemoryContext vac_context, bool isTopLevel) { static bool in_vacuum = false; @@ -502,8 +500,6 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy, volatile bool in_outer_xact, use_own_xacts; - Assert(params != NULL); - stmttype = (params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE"; /* @@ -634,7 +630,8 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy, if (params->options & VACOPT_VACUUM) { - if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy)) + if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy, + isTopLevel)) continue; } @@ -721,7 +718,7 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy, */ bool vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple, - bits32 options) + uint32 options) { char *relname; @@ -772,7 +769,7 @@ vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple, * or locked, a log is emitted if possible. */ Relation -vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options, +vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options, bool verbose, LOCKMODE lmode) { Relation rel; @@ -1151,8 +1148,8 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params, /* * Also compute the multixact age for which freezing is urgent. This is - * normally autovacuum_multixact_freeze_max_age, but may be less if we are - * short of multixact member space. + * normally autovacuum_multixact_freeze_max_age, but may be less if + * multixact members are bloated. */ effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold(); @@ -1669,9 +1666,11 @@ vac_update_datfrozenxid(void) while ((classTup = systable_getnext(scan)) != NULL) { - volatile FormData_pg_class *classForm = (Form_pg_class) GETSTRUCT(classTup); - TransactionId relfrozenxid = classForm->relfrozenxid; - TransactionId relminmxid = classForm->relminmxid; + Form_pg_class classForm = (Form_pg_class) GETSTRUCT(classTup); + volatile TransactionId *relfrozenxid_p = &classForm->relfrozenxid; + volatile TransactionId *relminmxid_p = &classForm->relminmxid; + TransactionId relfrozenxid = *relfrozenxid_p; + TransactionId relminmxid = *relminmxid_p; /* * Only consider relations able to hold unfrozen XIDs (anything else @@ -1873,9 +1872,11 @@ vac_truncate_clog(TransactionId frozenXID, while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { - volatile FormData_pg_database *dbform = (Form_pg_database) GETSTRUCT(tuple); - TransactionId datfrozenxid = dbform->datfrozenxid; - TransactionId datminmxid = dbform->datminmxid; + Form_pg_database dbform = (Form_pg_database) GETSTRUCT(tuple); + volatile TransactionId *datfrozenxid_p = &dbform->datfrozenxid; + volatile TransactionId *datminmxid_p = &dbform->datminmxid; + TransactionId datfrozenxid = *datfrozenxid_p; + TransactionId datminmxid = *datminmxid_p; Assert(TransactionIdIsNormal(datfrozenxid)); Assert(MultiXactIdIsValid(datminmxid)); @@ -1948,6 +1949,12 @@ vac_truncate_clog(TransactionId frozenXID, return; } + /* + * Freeze any old transaction IDs in the async notification queue before + * CLOG truncation. + */ + AsyncNotifyFreezeXids(frozenXID); + /* * Advance the oldest value for commit timestamps before truncating, so * that if a user requests a timestamp for a transaction we're truncating @@ -1971,7 +1978,7 @@ vac_truncate_clog(TransactionId frozenXID, * signaling twice? */ SetTransactionIdLimit(frozenXID, oldestxid_datoid); - SetMultiXactIdLimit(minMulti, minmulti_datoid, false); + SetMultiXactIdLimit(minMulti, minmulti_datoid); LWLockRelease(WrapLimitsVacuumLock); } @@ -1997,8 +2004,8 @@ vac_truncate_clog(TransactionId frozenXID, * At entry and exit, we are not inside a transaction. */ static bool -vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, - BufferAccessStrategy bstrategy) +vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, + BufferAccessStrategy bstrategy, bool isTopLevel) { LOCKMODE lmode; Relation rel; @@ -2008,13 +2015,18 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, Oid save_userid; int save_sec_context; int save_nestlevel; + VacuumParams toast_vacuum_params; - Assert(params != NULL); + /* + * This function scribbles on the parameters, so make a copy early to + * avoid affecting the TOAST table (if we do end up recursing to it). + */ + memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams)); /* Begin a transaction for vacuuming this relation */ StartTransactionCommand(); - if (!(params->options & VACOPT_FULL)) + if (!(params.options & VACOPT_FULL)) { /* * In lazy vacuum, we can set the PROC_IN_VACUUM flag, which lets @@ -2040,7 +2052,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, */ LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); MyProc->statusFlags |= PROC_IN_VACUUM; - if (params->is_wraparound) + if (params.is_wraparound) MyProc->statusFlags |= PROC_VACUUM_FOR_WRAPAROUND; ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; LWLockRelease(ProcArrayLock); @@ -2064,12 +2076,12 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, * vacuum, but just ShareUpdateExclusiveLock for concurrent vacuum. Either * way, we can be sure that no other backend is vacuuming the same table. */ - lmode = (params->options & VACOPT_FULL) ? + lmode = (params.options & VACOPT_FULL) ? AccessExclusiveLock : ShareUpdateExclusiveLock; /* open the relation and get the appropriate lock on it */ - rel = vacuum_open_relation(relid, relation, params->options, - params->log_min_duration >= 0, lmode); + rel = vacuum_open_relation(relid, relation, params.options, + params.log_vacuum_min_duration >= 0, lmode); /* leave if relation could not be opened or locked */ if (!rel) @@ -2084,8 +2096,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, * This is only safe to do because we hold a session lock on the main * relation that prevents concurrent deletion. */ - if (OidIsValid(params->toast_parent)) - priv_relid = params->toast_parent; + if (OidIsValid(params.toast_parent)) + priv_relid = params.toast_parent; else priv_relid = RelationGetRelid(rel); @@ -2098,7 +2110,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, */ if (!vacuum_is_permitted_for_relation(priv_relid, rel->rd_rel, - params->options & ~VACOPT_ANALYZE)) + params.options & ~VACOPT_ANALYZE)) { relation_close(rel, lmode); PopActiveSnapshot(); @@ -2169,7 +2181,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, * Set index_cleanup option based on index_cleanup reloption if it wasn't * specified in VACUUM command, or when running in an autovacuum worker */ - if (params->index_cleanup == VACOPTVALUE_UNSPECIFIED) + if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED) { StdRdOptIndexCleanup vacuum_index_cleanup; @@ -2180,56 +2192,74 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup; if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO) - params->index_cleanup = VACOPTVALUE_AUTO; + params.index_cleanup = VACOPTVALUE_AUTO; else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON) - params->index_cleanup = VACOPTVALUE_ENABLED; + params.index_cleanup = VACOPTVALUE_ENABLED; else { Assert(vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF); - params->index_cleanup = VACOPTVALUE_DISABLED; + params.index_cleanup = VACOPTVALUE_DISABLED; } } +#ifdef USE_INJECTION_POINTS + if (params.index_cleanup == VACOPTVALUE_AUTO) + INJECTION_POINT("vacuum-index-cleanup-auto", NULL); + else if (params.index_cleanup == VACOPTVALUE_DISABLED) + INJECTION_POINT("vacuum-index-cleanup-disabled", NULL); + else if (params.index_cleanup == VACOPTVALUE_ENABLED) + INJECTION_POINT("vacuum-index-cleanup-enabled", NULL); +#endif + /* * Check if the vacuum_max_eager_freeze_failure_rate table storage * parameter was specified. This overrides the GUC value. */ if (rel->rd_options != NULL && ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0) - params->max_eager_freeze_failure_rate = + params.max_eager_freeze_failure_rate = ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate; /* * Set truncate option based on truncate reloption or GUC if it wasn't * specified in VACUUM command, or when running in an autovacuum worker */ - if (params->truncate == VACOPTVALUE_UNSPECIFIED) + if (params.truncate == VACOPTVALUE_UNSPECIFIED) { StdRdOptions *opts = (StdRdOptions *) rel->rd_options; - if (opts && opts->vacuum_truncate_set) + if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET) { - if (opts->vacuum_truncate) - params->truncate = VACOPTVALUE_ENABLED; + if (opts->vacuum_truncate == PG_TERNARY_TRUE) + params.truncate = VACOPTVALUE_ENABLED; else - params->truncate = VACOPTVALUE_DISABLED; + params.truncate = VACOPTVALUE_DISABLED; } else if (vacuum_truncate) - params->truncate = VACOPTVALUE_ENABLED; + params.truncate = VACOPTVALUE_ENABLED; else - params->truncate = VACOPTVALUE_DISABLED; + params.truncate = VACOPTVALUE_DISABLED; } +#ifdef USE_INJECTION_POINTS + if (params.truncate == VACOPTVALUE_AUTO) + INJECTION_POINT("vacuum-truncate-auto", NULL); + else if (params.truncate == VACOPTVALUE_DISABLED) + INJECTION_POINT("vacuum-truncate-disabled", NULL); + else if (params.truncate == VACOPTVALUE_ENABLED) + INJECTION_POINT("vacuum-truncate-enabled", NULL); +#endif + /* * Remember the relation's TOAST relation for later, if the caller asked * us to process it. In VACUUM FULL, though, the toast table is * automatically rebuilt by cluster_rel so we shouldn't recurse to it, * unless PROCESS_MAIN is disabled. */ - if ((params->options & VACOPT_PROCESS_TOAST) != 0 && - ((params->options & VACOPT_FULL) == 0 || - (params->options & VACOPT_PROCESS_MAIN) == 0)) + if ((params.options & VACOPT_PROCESS_TOAST) != 0 && + ((params.options & VACOPT_FULL) == 0 || + (params.options & VACOPT_PROCESS_MAIN) == 0)) toast_relid = rel->rd_rel->reltoastrelid; else toast_relid = InvalidOid; @@ -2252,26 +2282,27 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, * table is required (e.g., PROCESS_TOAST is set), we force PROCESS_MAIN * to be set when we recurse to the TOAST table. */ - if (params->options & VACOPT_PROCESS_MAIN) + if (params.options & VACOPT_PROCESS_MAIN) { /* * Do the actual work --- either FULL or "lazy" vacuum */ - if (params->options & VACOPT_FULL) + if (params.options & VACOPT_FULL) { ClusterParams cluster_params = {0}; - if ((params->options & VACOPT_VERBOSE) != 0) + if ((params.options & VACOPT_VERBOSE) != 0) cluster_params.options |= CLUOPT_VERBOSE; - /* VACUUM FULL is now a variant of CLUSTER; see cluster.c */ - cluster_rel(rel, InvalidOid, &cluster_params); + /* VACUUM FULL is a variant of REPACK; see repack.c */ + cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid, + &cluster_params, isTopLevel); /* cluster_rel closes the relation, but keeps lock */ rel = NULL; } else - table_relation_vacuum(rel, params, bstrategy); + table_relation_vacuum(rel, ¶ms, bstrategy); } /* Roll back any GUC changes executed by index functions */ @@ -2299,19 +2330,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, */ if (toast_relid != InvalidOid) { - VacuumParams toast_vacuum_params; - /* * Force VACOPT_PROCESS_MAIN so vacuum_rel() processes it. Likewise, * set toast_parent so that the privilege checks are done on the main * relation. NB: This is only safe to do because we hold a session * lock on the main relation that prevents concurrent deletion. */ - memcpy(&toast_vacuum_params, params, sizeof(VacuumParams)); toast_vacuum_params.options |= VACOPT_PROCESS_MAIN; toast_vacuum_params.toast_parent = relid; - vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy); + vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy, + isTopLevel); } /* @@ -2408,8 +2437,20 @@ vacuum_delay_point(bool is_analyze) /* Always check for interrupts */ CHECK_FOR_INTERRUPTS(); - if (InterruptPending || - (!VacuumCostActive && !ConfigReloadPending)) + if (InterruptPending) + return; + + if (IsParallelWorker()) + { + /* + * Update cost-based vacuum delay parameters for a parallel autovacuum + * worker if any changes are detected. It might enable cost-based + * delay so it needs to be called before VacuumCostActive check. + */ + parallel_vacuum_update_shared_delay_params(); + } + + if (!VacuumCostActive && !ConfigReloadPending) return; /* @@ -2423,6 +2464,12 @@ vacuum_delay_point(bool is_analyze) ConfigReloadPending = false; ProcessConfigFile(PGC_SIGHUP); VacuumUpdateCosts(); + + /* + * Propagate cost-based vacuum delay parameters to shared memory if + * any of them have changed during the config reload. + */ + parallel_vacuum_propagate_shared_delay_params(); } /* diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 2b9d548cdeb10..41cefcfde54fe 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -1,7 +1,9 @@ /*------------------------------------------------------------------------- * * vacuumparallel.c - * Support routines for parallel vacuum execution. + * Support routines for parallel vacuum and autovacuum execution. In the + * comments below, the word "vacuum" will refer to both vacuum and + * autovacuum. * * This file contains routines that are intended to support setting up, using, * and tearing down a ParallelVacuumState. @@ -16,7 +18,14 @@ * the parallel context is re-initialized so that the same DSM can be used for * multiple passes of index bulk-deletion and index cleanup. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * For parallel autovacuum, we need to propagate cost-based vacuum delay + * parameters from the leader to its workers, as the leader's parameters can + * change even while processing a table (e.g., due to a config reload). + * The PVSharedCostParams struct manages these parameters using a + * generation counter. Each parallel worker polls this shared state and + * refreshes its local delay parameters whenever a change is detected. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -35,6 +44,7 @@ #include "optimizer/paths.h" #include "pgstat.h" #include "storage/bufmgr.h" +#include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -50,6 +60,33 @@ #define PARALLEL_VACUUM_KEY_WAL_USAGE 4 #define PARALLEL_VACUUM_KEY_INDEX_STATS 5 +/* + * Struct for cost-based vacuum delay related parameters to share among an + * autovacuum worker and its parallel vacuum workers. + */ +typedef struct PVSharedCostParams +{ + /* + * The generation counter is incremented by the leader process each time + * it updates the shared cost-based vacuum delay parameters. Parallel + * vacuum workers compare it with their local generation, + * shared_params_generation_local, to detect whether they need to refresh + * their local parameters. The generation starts from 1 so that a freshly + * started worker (whose local copy is 0) will always load the initial + * parameters on its first check. + */ + pg_atomic_uint32 generation; + + slock_t mutex; /* protects all fields below */ + + /* Parameters to share with parallel workers */ + double cost_delay; + int cost_limit; + int cost_page_dirty; + int cost_page_hit; + int cost_page_miss; +} PVSharedCostParams; + /* * Shared information among parallel workers. So this is allocated in the DSM * segment. @@ -63,7 +100,7 @@ typedef struct PVShared */ Oid relid; int elevel; - uint64 queryid; + int64 queryid; /* * Fields for both index vacuum and cleanup. @@ -119,6 +156,18 @@ typedef struct PVShared /* Statistics of shared dead items */ VacDeadItemsInfo dead_items_info; + + /* + * If 'true' then we are running parallel autovacuum. Otherwise, we are + * running parallel maintenance VACUUM. + */ + bool is_autovacuum; + + /* + * Cost-based vacuum delay parameters shared between the autovacuum leader + * and its parallel workers. + */ + PVSharedCostParams cost_params; } PVShared; /* Status used during parallel index vacuum or cleanup */ @@ -221,10 +270,21 @@ struct ParallelVacuumState PVIndVacStatus status; }; +static PVSharedCostParams *pv_shared_cost_params = NULL; + +/* + * Worker-local copy of the last cost-parameter generation this worker has + * applied. Initialized to 0; since the leader initializes the shared + * generation counter to 1, the first call to + * parallel_vacuum_update_shared_delay_params() will always detect a + * mismatch and read the initial parameters from shared memory. + */ +static uint32 shared_params_generation_local = 0; + static int parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, bool *will_parallel_vacuum); static void parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scans, - bool vacuum); + bool vacuum, PVWorkerStats *wstats); static void parallel_vacuum_process_safe_indexes(ParallelVacuumState *pvs); static void parallel_vacuum_process_unsafe_indexes(ParallelVacuumState *pvs); static void parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, @@ -232,6 +292,8 @@ static void parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation static bool parallel_vacuum_index_is_parallel_safe(Relation indrel, int num_index_scans, bool vacuum); static void parallel_vacuum_error_callback(void *arg); +static inline void parallel_vacuum_set_cost_parameters(PVSharedCostParams *params); +static void parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg); /* * Try to enter parallel mode and create a parallel context. Then initialize @@ -268,7 +330,7 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, /* * Compute the number of parallel vacuum workers to launch */ - will_parallel_vacuum = (bool *) palloc0(sizeof(bool) * nindexes); + will_parallel_vacuum = palloc0_array(bool, nindexes); parallel_workers = parallel_vacuum_compute_workers(indrels, nindexes, nrequested_workers, will_parallel_vacuum); @@ -279,7 +341,7 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, return NULL; } - pvs = (ParallelVacuumState *) palloc0(sizeof(ParallelVacuumState)); + pvs = palloc0_object(ParallelVacuumState); pvs->indrels = indrels; pvs->nindexes = nindexes; pvs->will_parallel_vacuum = will_parallel_vacuum; @@ -373,8 +435,9 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, shared->queryid = pgstat_get_my_query_id(); shared->maintenance_work_mem_worker = (nindexes_mwm > 0) ? - maintenance_work_mem / Min(parallel_workers, nindexes_mwm) : - maintenance_work_mem; + vac_work_mem / Min(parallel_workers, nindexes_mwm) : + vac_work_mem; + shared->dead_items_info.max_bytes = vac_work_mem * (size_t) 1024; /* Prepare DSA space for dead items */ @@ -391,6 +454,22 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes, pg_atomic_init_u32(&(shared->active_nworkers), 0); pg_atomic_init_u32(&(shared->idx), 0); + shared->is_autovacuum = AmAutoVacuumWorkerProcess(); + + /* + * Initialize shared cost-based vacuum delay parameters if it's for + * autovacuum. + */ + if (shared->is_autovacuum) + { + parallel_vacuum_set_cost_parameters(&shared->cost_params); + pg_atomic_init_u32(&shared->cost_params.generation, 1); + SpinLockInit(&shared->cost_params.mutex); + + pv_shared_cost_params = &(shared->cost_params); + on_dsm_detach(pcxt->seg, parallel_vacuum_dsm_detach, (Datum) 0); + } + shm_toc_insert(pcxt->toc, PARALLEL_VACUUM_KEY_SHARED, shared); pvs->shared = shared; @@ -444,7 +523,7 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats) if (indstats->istat_updated) { - istats[i] = (IndexBulkDeleteResult *) palloc0(sizeof(IndexBulkDeleteResult)); + istats[i] = palloc0_object(IndexBulkDeleteResult); memcpy(istats[i], &indstats->istat, sizeof(IndexBulkDeleteResult)); } else @@ -456,10 +535,26 @@ parallel_vacuum_end(ParallelVacuumState *pvs, IndexBulkDeleteResult **istats) DestroyParallelContext(pvs->pcxt); ExitParallelMode(); + if (AmAutoVacuumWorkerProcess()) + pv_shared_cost_params = NULL; + pfree(pvs->will_parallel_vacuum); pfree(pvs); } +/* + * DSM detach callback. This is invoked when an autovacuum worker detaches + * from the DSM segment holding PVShared. It ensures to reset the local pointer + * to the shared state even if parallel vacuum raises an error and doesn't + * call parallel_vacuum_end(). + */ +static void +parallel_vacuum_dsm_detach(dsm_segment *seg, Datum arg) +{ + Assert(AmAutoVacuumWorkerProcess()); + pv_shared_cost_params = NULL; +} + /* * Returns the dead items space and dead items information. */ @@ -498,7 +593,7 @@ parallel_vacuum_reset_dead_items(ParallelVacuumState *pvs) */ void parallel_vacuum_bulkdel_all_indexes(ParallelVacuumState *pvs, long num_table_tuples, - int num_index_scans) + int num_index_scans, PVWorkerStats *wstats) { Assert(!IsParallelWorker()); @@ -509,7 +604,7 @@ parallel_vacuum_bulkdel_all_indexes(ParallelVacuumState *pvs, long num_table_tup pvs->shared->reltuples = num_table_tuples; pvs->shared->estimated_count = true; - parallel_vacuum_process_all_indexes(pvs, num_index_scans, true); + parallel_vacuum_process_all_indexes(pvs, num_index_scans, true, wstats); } /* @@ -517,7 +612,8 @@ parallel_vacuum_bulkdel_all_indexes(ParallelVacuumState *pvs, long num_table_tup */ void parallel_vacuum_cleanup_all_indexes(ParallelVacuumState *pvs, long num_table_tuples, - int num_index_scans, bool estimated_count) + int num_index_scans, bool estimated_count, + PVWorkerStats *wstats) { Assert(!IsParallelWorker()); @@ -529,7 +625,104 @@ parallel_vacuum_cleanup_all_indexes(ParallelVacuumState *pvs, long num_table_tup pvs->shared->reltuples = num_table_tuples; pvs->shared->estimated_count = estimated_count; - parallel_vacuum_process_all_indexes(pvs, num_index_scans, false); + parallel_vacuum_process_all_indexes(pvs, num_index_scans, false, wstats); +} + +/* + * Fill in the given structure with cost-based vacuum delay parameter values. + */ +static inline void +parallel_vacuum_set_cost_parameters(PVSharedCostParams *params) +{ + params->cost_delay = vacuum_cost_delay; + params->cost_limit = vacuum_cost_limit; + params->cost_page_dirty = VacuumCostPageDirty; + params->cost_page_hit = VacuumCostPageHit; + params->cost_page_miss = VacuumCostPageMiss; +} + +/* + * Updates the cost-based vacuum delay parameters for parallel autovacuum + * workers. + * + * For non-autovacuum parallel workers, this function will have no effect. + */ +void +parallel_vacuum_update_shared_delay_params(void) +{ + uint32 params_generation; + + Assert(IsParallelWorker()); + + /* Quick return if the worker is not running for the autovacuum */ + if (pv_shared_cost_params == NULL) + return; + + params_generation = pg_atomic_read_u32(&pv_shared_cost_params->generation); + Assert(shared_params_generation_local <= params_generation); + + /* Return if parameters had not changed in the leader */ + if (params_generation == shared_params_generation_local) + return; + + SpinLockAcquire(&pv_shared_cost_params->mutex); + VacuumCostDelay = pv_shared_cost_params->cost_delay; + VacuumCostLimit = pv_shared_cost_params->cost_limit; + VacuumCostPageDirty = pv_shared_cost_params->cost_page_dirty; + VacuumCostPageHit = pv_shared_cost_params->cost_page_hit; + VacuumCostPageMiss = pv_shared_cost_params->cost_page_miss; + SpinLockRelease(&pv_shared_cost_params->mutex); + + VacuumUpdateCosts(); + + shared_params_generation_local = params_generation; + + elog(DEBUG2, + "parallel autovacuum worker updated cost params: cost_limit=%d, cost_delay=%g, cost_page_miss=%d, cost_page_dirty=%d, cost_page_hit=%d", + vacuum_cost_limit, + vacuum_cost_delay, + VacuumCostPageMiss, + VacuumCostPageDirty, + VacuumCostPageHit); +} + +/* + * Store the cost-based vacuum delay parameters in the shared memory so that + * parallel vacuum workers can consume them (see + * parallel_vacuum_update_shared_delay_params()). + */ +void +parallel_vacuum_propagate_shared_delay_params(void) +{ + Assert(AmAutoVacuumWorkerProcess()); + + /* + * Quick return if the leader process is not sharing the delay parameters. + */ + if (pv_shared_cost_params == NULL) + return; + + /* + * Check if any delay parameters have changed. We can read them without + * locks as only the leader can modify them. + */ + if (vacuum_cost_delay == pv_shared_cost_params->cost_delay && + vacuum_cost_limit == pv_shared_cost_params->cost_limit && + VacuumCostPageDirty == pv_shared_cost_params->cost_page_dirty && + VacuumCostPageHit == pv_shared_cost_params->cost_page_hit && + VacuumCostPageMiss == pv_shared_cost_params->cost_page_miss) + return; + + /* Update the shared delay parameters */ + SpinLockAcquire(&pv_shared_cost_params->mutex); + parallel_vacuum_set_cost_parameters(pv_shared_cost_params); + SpinLockRelease(&pv_shared_cost_params->mutex); + + /* + * Increment the generation of the parameters, i.e. let parallel workers + * know that they should re-read shared cost params. + */ + pg_atomic_fetch_add_u32(&pv_shared_cost_params->generation, 1); } /* @@ -553,12 +746,17 @@ parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, int nindexes_parallel_bulkdel = 0; int nindexes_parallel_cleanup = 0; int parallel_workers; + int max_workers; + + max_workers = AmAutoVacuumWorkerProcess() ? + autovacuum_max_parallel_workers : + max_parallel_maintenance_workers; /* * We don't allow performing parallel operation in standalone backend or * when parallelism is disabled. */ - if (!IsUnderPostmaster || max_parallel_maintenance_workers == 0) + if (!IsUnderPostmaster || max_workers == 0) return 0; /* @@ -597,8 +795,8 @@ parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, parallel_workers = (nrequested > 0) ? Min(nrequested, nindexes_parallel) : nindexes_parallel; - /* Cap by max_parallel_maintenance_workers */ - parallel_workers = Min(parallel_workers, max_parallel_maintenance_workers); + /* Cap by GUC variable */ + parallel_workers = Min(parallel_workers, max_workers); return parallel_workers; } @@ -606,10 +804,12 @@ parallel_vacuum_compute_workers(Relation *indrels, int nindexes, int nrequested, /* * Perform index vacuum or index cleanup with parallel workers. This function * must be used by the parallel vacuum leader process. + * + * If wstats is not NULL, the parallel worker statistics are updated. */ static void parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scans, - bool vacuum) + bool vacuum, PVWorkerStats *wstats) { int nworkers; PVIndVacStatus new_status; @@ -646,6 +846,10 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan */ nworkers = Min(nworkers, pvs->pcxt->nworkers); + /* Update the statistics, if we asked to */ + if (wstats != NULL && nworkers > 0) + wstats->nplanned += nworkers; + /* * Set index vacuum status and mark whether parallel vacuum worker can * process it. @@ -702,6 +906,10 @@ parallel_vacuum_process_all_indexes(ParallelVacuumState *pvs, int num_index_scan /* Enable shared cost balance for leader backend */ VacuumSharedCostBalance = &(pvs->shared->cost_balance); VacuumActiveNWorkers = &(pvs->shared->active_nworkers); + + /* Update the statistics, if we asked to */ + if (wstats != NULL) + wstats->nlaunched += pvs->pcxt->nworkers_launched; } if (vacuum) @@ -1052,7 +1260,21 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) shared->dead_items_handle); /* Set cost-based vacuum delay */ - VacuumUpdateCosts(); + if (shared->is_autovacuum) + { + /* + * Parallel autovacuum workers initialize cost-based delay parameters + * from the leader's shared state rather than GUC defaults, because + * the leader may have applied per-table or autovacuum-specific + * overrides. pv_shared_cost_params must be set before calling + * parallel_vacuum_update_shared_delay_params(). + */ + pv_shared_cost_params = &(shared->cost_params); + parallel_vacuum_update_shared_delay_params(); + } + else + VacuumUpdateCosts(); + VacuumCostBalance = 0; VacuumCostBalanceLocal = 0; VacuumSharedCostBalance = &(shared->cost_balance); @@ -1107,6 +1329,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) vac_close_indexes(nindexes, indrels, RowExclusiveLock); table_close(rel, ShareUpdateExclusiveLock); FreeAccessStrategy(pvs.bstrategy); + + if (shared->is_autovacuum) + pv_shared_cost_params = NULL; } /* diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c index 608f10d9412da..8afd252fc8c03 100644 --- a/src/backend/commands/variable.c +++ b/src/backend/commands/variable.c @@ -4,7 +4,7 @@ * Routines for handling specialized SET variables. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -1257,3 +1257,39 @@ check_ssl(bool *newval, void **extra, GucSource source) #endif return true; } + +bool +check_ssl_sni(bool *newval, void **extra, GucSource source) +{ +#ifndef USE_SSL + if (*newval) + { + GUC_check_errmsg("SSL is not supported by this build"); + return false; + } +#else +#ifndef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + if (*newval) + { + GUC_check_errmsg("SNI requires OpenSSL 1.1.1 or later"); + return false; + } +#endif +#endif + return true; +} + +bool +check_standard_conforming_strings(bool *newval, void **extra, GucSource source) +{ + if (!*newval) + { + /* check the GUC's definition for an explanation */ + GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED); + GUC_check_errmsg("non-standard string literals are not supported"); + + return false; + } + + return true; +} diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 6f0301555e0ae..1bd78a4cdf0ae 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -3,7 +3,7 @@ * view.c * use rewrite rules to construct views * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -22,7 +22,6 @@ #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "parser/analyze.h" -#include "parser/parse_relation.h" #include "rewrite/rewriteDefine.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteSupport.h" @@ -362,6 +361,7 @@ DefineView(ViewStmt *stmt, const char *queryString, ListCell *cell; bool check_option; ObjectAddress address; + ObjectAddress temp_object; /* * Run parse analysis to convert the raw parse tree to a Query. Note this @@ -484,12 +484,14 @@ DefineView(ViewStmt *stmt, const char *queryString, */ view = copyObject(stmt->view); /* don't corrupt original command */ if (view->relpersistence == RELPERSISTENCE_PERMANENT - && isQueryUsingTempRelation(viewParse)) + && query_uses_temp_object(viewParse, &temp_object)) { view->relpersistence = RELPERSISTENCE_TEMP; ereport(NOTICE, (errmsg("view \"%s\" will be a temporary view", - view->relname))); + view->relname), + errdetail("It depends on temporary %s.", + getObjectDescription(&temp_object, false)))); } /* diff --git a/src/backend/commands/wait.c b/src/backend/commands/wait.c new file mode 100644 index 0000000000000..382d5c2d44ffb --- /dev/null +++ b/src/backend/commands/wait.c @@ -0,0 +1,362 @@ +/*------------------------------------------------------------------------- + * + * wait.c + * Implements WAIT FOR, which allows waiting for events such as + * time passing or LSN having been replayed, flushed, or written. + * + * Portions Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/commands/wait.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#include "access/xlog.h" +#include "access/xlogrecovery.h" +#include "access/xlogwait.h" +#include "catalog/pg_type_d.h" +#include "commands/defrem.h" +#include "commands/wait.h" +#include "executor/executor.h" +#include "parser/parse_node.h" +#include "storage/proc.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/pg_lsn.h" +#include "utils/snapmgr.h" + + +void +ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel, + DestReceiver *dest) +{ + XLogRecPtr lsn; + int64 timeout = 0; + WaitLSNResult waitLSNResult; + WaitLSNType lsnType = WAIT_LSN_TYPE_STANDBY_REPLAY; /* default */ + bool throw = true; + TupleDesc tupdesc; + TupOutputState *tstate; + const char *result = ""; + bool timeout_specified = false; + bool no_throw_specified = false; + bool mode_specified = false; + + /* + * WAIT FOR must not be run as a non-top-level statement (e.g., inside a + * function, procedure, or DO block). Forbid this case upfront. + */ + if (!isTopLevel) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s can only be executed as a top-level statement", + "WAIT FOR"), + errdetail("WAIT FOR cannot be used within a function, procedure, or DO block."))); + + /* Parse and validate the mandatory LSN */ + lsn = DatumGetLSN(DirectFunctionCall1(pg_lsn_in, + CStringGetDatum(stmt->lsn_literal))); + + foreach_node(DefElem, defel, stmt->options) + { + if (strcmp(defel->defname, "mode") == 0) + { + char *mode_str; + + if (mode_specified) + errorConflictingDefElem(defel, pstate); + mode_specified = true; + + mode_str = defGetString(defel); + + if (pg_strcasecmp(mode_str, "standby_replay") == 0) + lsnType = WAIT_LSN_TYPE_STANDBY_REPLAY; + else if (pg_strcasecmp(mode_str, "standby_write") == 0) + lsnType = WAIT_LSN_TYPE_STANDBY_WRITE; + else if (pg_strcasecmp(mode_str, "standby_flush") == 0) + lsnType = WAIT_LSN_TYPE_STANDBY_FLUSH; + else if (pg_strcasecmp(mode_str, "primary_flush") == 0) + lsnType = WAIT_LSN_TYPE_PRIMARY_FLUSH; + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized value for %s option \"%s\": \"%s\"", + "WAIT", defel->defname, mode_str), + parser_errposition(pstate, defel->location))); + } + else if (strcmp(defel->defname, "timeout") == 0) + { + char *timeout_str; + const char *hintmsg; + double result; + + if (timeout_specified) + errorConflictingDefElem(defel, pstate); + timeout_specified = true; + + timeout_str = defGetString(defel); + + if (!parse_real(timeout_str, &result, GUC_UNIT_MS, &hintmsg)) + { + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid timeout value: \"%s\"", timeout_str), + hintmsg ? errhint("%s", _(hintmsg)) : 0); + } + + /* + * Get rid of any fractional part in the input. This is so we + * don't fail on just-out-of-range values that would round into + * range. + */ + result = rint(result); + + /* Range check */ + if (unlikely(isnan(result) || !FLOAT8_FITS_IN_INT64(result))) + ereport(ERROR, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("timeout value is out of range")); + + if (result < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("timeout cannot be negative")); + + timeout = (int64) result; + } + else if (strcmp(defel->defname, "no_throw") == 0) + { + if (no_throw_specified) + errorConflictingDefElem(defel, pstate); + + no_throw_specified = true; + + throw = !defGetBoolean(defel); + } + else + { + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("option \"%s\" not recognized", + defel->defname), + parser_errposition(pstate, defel->location)); + } + } + + /* + * We are going to wait for the LSN. We should first care that we don't + * hold a snapshot and correspondingly our MyProc->xmin is invalid. + * Otherwise, our snapshot could prevent the replay of WAL records + * implying a kind of self-deadlock. This is the reason why WAIT FOR is a + * command, not a procedure or function. + * + * Non-top-level contexts are rejected above, but be defensive and pop any + * active snapshot if one is present. PortalRunUtility() can tolerate + * utility commands that remove the active snapshot. + */ + if (ActiveSnapshotSet()) + PopActiveSnapshot(); + + /* + * At second, invalidate a catalog snapshot if any. And we should be done + * with the preparation. + */ + InvalidateCatalogSnapshot(); + + /* Give up if there is still an active or registered snapshot. */ + if (HaveRegisteredOrActiveSnapshot()) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("WAIT FOR must be called without an active or registered snapshot"), + errdetail("WAIT FOR cannot be executed within a transaction with an isolation level higher than READ COMMITTED.")); + + /* + * As the result we should hold no snapshot, and correspondingly our xmin + * should be unset. + */ + Assert(MyProc->xmin == InvalidTransactionId); + + /* + * Validate that the requested mode matches the current server state. + * Primary modes can only be used on a primary. + */ + if (lsnType == WAIT_LSN_TYPE_PRIMARY_FLUSH) + { + if (RecoveryInProgress()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is in progress"), + errhint("Waiting for primary_flush can only be done on a primary server. " + "Use standby_flush mode on a standby server."))); + } + + /* Now wait for the LSN */ + waitLSNResult = WaitForLSN(lsnType, lsn, timeout); + + /* + * Process the result of WaitForLSN(). Throw appropriate error if needed. + */ + switch (waitLSNResult) + { + case WAIT_LSN_RESULT_SUCCESS: + /* Nothing to do on success */ + result = "success"; + break; + + case WAIT_LSN_RESULT_TIMEOUT: + if (throw) + { + XLogRecPtr currentLSN = GetCurrentLSNForWaitType(lsnType); + + switch (lsnType) + { + case WAIT_LSN_TYPE_STANDBY_REPLAY: + ereport(ERROR, + errcode(ERRCODE_QUERY_CANCELED), + errmsg("timed out while waiting for target LSN %X/%08X to be replayed; current standby_replay LSN %X/%08X", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + case WAIT_LSN_TYPE_STANDBY_WRITE: + ereport(ERROR, + errcode(ERRCODE_QUERY_CANCELED), + errmsg("timed out while waiting for target LSN %X/%08X to be written; current standby_write LSN %X/%08X", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + case WAIT_LSN_TYPE_STANDBY_FLUSH: + ereport(ERROR, + errcode(ERRCODE_QUERY_CANCELED), + errmsg("timed out while waiting for target LSN %X/%08X to be flushed; current standby_flush LSN %X/%08X", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + case WAIT_LSN_TYPE_PRIMARY_FLUSH: + ereport(ERROR, + errcode(ERRCODE_QUERY_CANCELED), + errmsg("timed out while waiting for target LSN %X/%08X to be flushed; current primary_flush LSN %X/%08X", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + default: + elog(ERROR, "unexpected wait LSN type %d", lsnType); + } + } + else + result = "timeout"; + break; + + case WAIT_LSN_RESULT_NOT_IN_RECOVERY: + if (throw) + { + if (PromoteIsTriggered()) + { + XLogRecPtr currentLSN = GetCurrentLSNForWaitType(lsnType); + + switch (lsnType) + { + case WAIT_LSN_TYPE_STANDBY_REPLAY: + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is not in progress"), + errdetail("Recovery ended before target LSN %X/%08X was replayed; last standby_replay LSN %X/%08X.", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + case WAIT_LSN_TYPE_STANDBY_WRITE: + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is not in progress"), + errdetail("Recovery ended before target LSN %X/%08X was written; last standby_write LSN %X/%08X.", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + case WAIT_LSN_TYPE_STANDBY_FLUSH: + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is not in progress"), + errdetail("Recovery ended before target LSN %X/%08X was flushed; last standby_flush LSN %X/%08X.", + LSN_FORMAT_ARGS(lsn), + LSN_FORMAT_ARGS(currentLSN))); + break; + + default: + elog(ERROR, "unexpected wait LSN type %d", lsnType); + } + } + else + { + switch (lsnType) + { + case WAIT_LSN_TYPE_STANDBY_REPLAY: + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is not in progress"), + errhint("Waiting for the standby_replay LSN can only be executed during recovery.")); + break; + + case WAIT_LSN_TYPE_STANDBY_WRITE: + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is not in progress"), + errhint("Waiting for the standby_write LSN can only be executed during recovery.")); + break; + + case WAIT_LSN_TYPE_STANDBY_FLUSH: + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is not in progress"), + errhint("Waiting for the standby_flush LSN can only be executed during recovery.")); + break; + + default: + elog(ERROR, "unexpected wait LSN type %d", lsnType); + } + } + } + else + result = "not in recovery"; + break; + } + + /* need a tuple descriptor representing a single TEXT column */ + tupdesc = WaitStmtResultDesc(stmt); + + /* prepare for projection of tuples */ + tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); + + /* Send it */ + do_text_output_oneline(tstate, result); + + end_tup_output(tstate); +} + +TupleDesc +WaitStmtResultDesc(WaitStmt *stmt) +{ + TupleDesc tupdesc; + + /* + * Need a tuple descriptor representing a single TEXT column. + * + * We use TupleDescInitBuiltinEntry instead of TupleDescInitEntry to avoid + * syscache access. This is important because WaitStmtResultDesc may be + * called after snapshots have been released, and we must not re-establish + * a catalog snapshot which could cause recovery conflicts on a standby. + */ + tupdesc = CreateTemplateTupleDesc(1); + TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "status", + TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); + return tupdesc; +} diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c index 1d0e8ad57b4a0..37fe03fdc372f 100644 --- a/src/backend/executor/execAmi.c +++ b/src/backend/executor/execAmi.c @@ -3,7 +3,7 @@ * execAmi.c * miscellaneous executor access method routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/executor/execAmi.c @@ -16,6 +16,7 @@ #include "access/htup_details.h" #include "catalog/pg_class.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeAgg.h" #include "executor/nodeAppend.h" #include "executor/nodeBitmapAnd.h" @@ -605,7 +606,7 @@ IndexSupportsBackwardScan(Oid indexid) bool result; HeapTuple ht_idxrel; Form_pg_class idxrelrec; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; /* Fetch the pg_class tuple of the index relation */ ht_idxrel = SearchSysCache1(RELOID, ObjectIdGetDatum(indexid)); @@ -618,7 +619,6 @@ IndexSupportsBackwardScan(Oid indexid) result = amroutine->amcanbackward; - pfree(amroutine); ReleaseSysCache(ht_idxrel); return result; diff --git a/src/backend/executor/execAsync.c b/src/backend/executor/execAsync.c index 5d3cabe73e346..cf7ddbb01f430 100644 --- a/src/backend/executor/execAsync.c +++ b/src/backend/executor/execAsync.c @@ -3,7 +3,7 @@ * execAsync.c * Support routines for asynchronous execution * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -16,6 +16,7 @@ #include "executor/execAsync.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeAppend.h" #include "executor/nodeForeignscan.h" diff --git a/src/backend/executor/execCurrent.c b/src/backend/executor/execCurrent.c index 3bfdc0230ff9f..99f2b2d0c6f08 100644 --- a/src/backend/executor/execCurrent.c +++ b/src/backend/executor/execCurrent.c @@ -3,7 +3,7 @@ * execCurrent.c * executor support for WHERE CURRENT OF cursor * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/executor/execCurrent.c diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b529d..77229141b380d 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -19,7 +19,7 @@ * and "Expression Evaluation" sections. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -141,6 +141,26 @@ static void ExecInitJsonCoercion(ExprState *state, JsonReturning *returning, */ ExprState * ExecInitExpr(Expr *node, PlanState *parent) +{ + return ExecInitExprWithContext(node, parent, NULL); +} + +/* + * ExecInitExprWithContext: same as ExecInitExpr, but with an optional + * ErrorSaveContext for soft error handling. + * + * When 'escontext' is non-NULL, expression nodes that support soft errors + * (currently CoerceToDomain's NOT NULL and CHECK constraint steps) will use + * errsave() instead of ereport(), allowing the caller to detect and handle + * failures without a transaction abort. + * + * The escontext must be provided at initialization time (not after), because + * it is copied into per-step data during expression compilation. + * + * Not all expression node types support soft errors. If in doubt, pass NULL. + */ +ExprState * +ExecInitExprWithContext(Expr *node, PlanState *parent, Node *escontext) { ExprState *state; ExprEvalStep scratch = {0}; @@ -154,6 +174,7 @@ ExecInitExpr(Expr *node, PlanState *parent) state->expr = node; state->parent = parent; state->ext_params = NULL; + state->escontext = (ErrorSaveContext *) escontext; /* Insert setup steps as needed */ ExecCreateExprSetupSteps(state, (Node *) node); @@ -763,6 +784,18 @@ ExecBuildUpdateProjection(List *targetList, */ ExprState * ExecPrepareExpr(Expr *node, EState *estate) +{ + return ExecPrepareExprWithContext(node, estate, NULL); +} + +/* + * ExecPrepareExprWithContext: same as ExecPrepareExpr, but with an optional + * ErrorSaveContext for soft error handling. + * + * See ExecInitExprWithContext for details on the escontext parameter. + */ +ExprState * +ExecPrepareExprWithContext(Expr *node, EState *estate, Node *escontext) { ExprState *result; MemoryContext oldcontext; @@ -771,7 +804,7 @@ ExecPrepareExpr(Expr *node, EState *estate) node = expression_planner(node); - result = ExecInitExpr(node, NULL); + result = ExecInitExprWithContext(node, NULL, escontext); MemoryContextSwitchTo(oldcontext); @@ -1312,7 +1345,7 @@ ExecInitExprRec(Expr *node, ExprState *state, } /* Set up the primary fmgr lookup information */ - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(2)); fmgr_info(cmpfuncid, finfo); fmgr_info_set_expr((Node *) node, finfo); @@ -1388,7 +1421,7 @@ ExecInitExprRec(Expr *node, ExprState *state, /* allocate scratch memory used by all steps of AND/OR */ if (boolexpr->boolop != NOT_EXPR) - scratch.d.boolexpr.anynull = (bool *) palloc(sizeof(bool)); + scratch.d.boolexpr.anynull = palloc_object(bool); /* * For each argument evaluate the argument itself, then @@ -1521,11 +1554,11 @@ ExecInitExprRec(Expr *node, ExprState *state, ReleaseTupleDesc(tupDesc); /* create workspace for column values */ - values = (Datum *) palloc(sizeof(Datum) * ncolumns); - nulls = (bool *) palloc(sizeof(bool) * ncolumns); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); /* create shared composite-type-lookup cache struct */ - rowcachep = palloc(sizeof(ExprEvalRowtypeCache)); + rowcachep = palloc_object(ExprEvalRowtypeCache); rowcachep->cacheptr = NULL; /* emit code to evaluate the composite input value */ @@ -1634,7 +1667,7 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.opcode = EEOP_IOCOERCE_SAFE; /* lookup the source type's output function */ - scratch.d.iocoerce.finfo_out = palloc0(sizeof(FmgrInfo)); + scratch.d.iocoerce.finfo_out = palloc0_object(FmgrInfo); scratch.d.iocoerce.fcinfo_data_out = palloc0(SizeForFunctionCallInfo(1)); getTypeOutputInfo(exprType((Node *) iocoerce->arg), @@ -1646,7 +1679,7 @@ ExecInitExprRec(Expr *node, ExprState *state, 1, InvalidOid, NULL, NULL); /* lookup the result type's input function */ - scratch.d.iocoerce.finfo_in = palloc0(sizeof(FmgrInfo)); + scratch.d.iocoerce.finfo_in = palloc0_object(FmgrInfo); scratch.d.iocoerce.fcinfo_data_in = palloc0(SizeForFunctionCallInfo(3)); getTypeInputInfo(iocoerce->resulttype, @@ -1699,8 +1732,8 @@ ExecInitExprRec(Expr *node, ExprState *state, elemstate->parent = state->parent; elemstate->ext_params = state->ext_params; - elemstate->innermost_caseval = (Datum *) palloc(sizeof(Datum)); - elemstate->innermost_casenull = (bool *) palloc(sizeof(bool)); + elemstate->innermost_caseval = palloc_object(Datum); + elemstate->innermost_casenull = palloc_object(bool); ExecInitExprRec(acoerce->elemexpr, elemstate, &elemstate->resvalue, &elemstate->resnull); @@ -1727,8 +1760,7 @@ ExecInitExprRec(Expr *node, ExprState *state, if (elemstate) { /* Set up workspace for array_map */ - scratch.d.arraycoerce.amstate = - (ArrayMapState *) palloc0(sizeof(ArrayMapState)); + scratch.d.arraycoerce.amstate = palloc0_object(ArrayMapState); } else { @@ -1783,8 +1815,8 @@ ExecInitExprRec(Expr *node, ExprState *state, if (caseExpr->arg != NULL) { /* Evaluate testexpr into caseval/casenull workspace */ - caseval = palloc(sizeof(Datum)); - casenull = palloc(sizeof(bool)); + caseval = palloc_object(Datum); + casenull = palloc_object(bool); ExecInitExprRec(caseExpr->arg, state, caseval, casenull); @@ -1930,9 +1962,9 @@ ExecInitExprRec(Expr *node, ExprState *state, */ scratch.opcode = EEOP_ARRAYEXPR; scratch.d.arrayexpr.elemvalues = - (Datum *) palloc(sizeof(Datum) * nelems); + palloc_array(Datum, nelems); scratch.d.arrayexpr.elemnulls = - (bool *) palloc(sizeof(bool) * nelems); + palloc_array(bool, nelems); scratch.d.arrayexpr.nelems = nelems; /* fill remaining fields of step */ @@ -2006,9 +2038,9 @@ ExecInitExprRec(Expr *node, ExprState *state, /* space for the individual field datums */ scratch.d.row.elemvalues = - (Datum *) palloc(sizeof(Datum) * nelems); + palloc_array(Datum, nelems); scratch.d.row.elemnulls = - (bool *) palloc(sizeof(bool) * nelems); + palloc_array(bool, nelems); /* as explained above, make sure any extra columns are null */ memset(scratch.d.row.elemnulls, true, sizeof(bool) * nelems); @@ -2109,7 +2141,7 @@ ExecInitExprRec(Expr *node, ExprState *state, BTORDER_PROC, lefttype, righttype, opfamily); /* Set up the primary fmgr lookup information */ - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(2)); fmgr_info(proc, finfo); fmgr_info_set_expr((Node *) node, finfo); @@ -2252,7 +2284,7 @@ ExecInitExprRec(Expr *node, ExprState *state, */ /* Perform function lookup */ - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(2)); fmgr_info(typentry->cmp_proc, finfo); fmgr_info_set_expr((Node *) node, finfo); @@ -2261,10 +2293,8 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.opcode = EEOP_MINMAX; /* allocate space to store arguments */ - scratch.d.minmax.values = - (Datum *) palloc(sizeof(Datum) * nelems); - scratch.d.minmax.nulls = - (bool *) palloc(sizeof(bool) * nelems); + scratch.d.minmax.values = palloc_array(Datum, nelems); + scratch.d.minmax.nulls = palloc_array(bool, nelems); scratch.d.minmax.nelems = nelems; scratch.d.minmax.op = minmaxexpr->op; @@ -2313,10 +2343,8 @@ ExecInitExprRec(Expr *node, ExprState *state, /* allocate space for storing all the arguments */ if (nnamed) { - scratch.d.xmlexpr.named_argvalue = - (Datum *) palloc(sizeof(Datum) * nnamed); - scratch.d.xmlexpr.named_argnull = - (bool *) palloc(sizeof(bool) * nnamed); + scratch.d.xmlexpr.named_argvalue = palloc_array(Datum, nnamed); + scratch.d.xmlexpr.named_argnull = palloc_array(bool, nnamed); } else { @@ -2326,10 +2354,8 @@ ExecInitExprRec(Expr *node, ExprState *state, if (nargs) { - scratch.d.xmlexpr.argvalue = - (Datum *) palloc(sizeof(Datum) * nargs); - scratch.d.xmlexpr.argnull = - (bool *) palloc(sizeof(bool) * nargs); + scratch.d.xmlexpr.argvalue = palloc_array(Datum, nargs); + scratch.d.xmlexpr.argnull = palloc_array(bool, nargs); } else { @@ -2398,15 +2424,15 @@ ExecInitExprRec(Expr *node, ExprState *state, { JsonConstructorExprState *jcstate; - jcstate = palloc0(sizeof(JsonConstructorExprState)); + jcstate = palloc0_object(JsonConstructorExprState); scratch.opcode = EEOP_JSON_CONSTRUCTOR; scratch.d.json_constructor.jcstate = jcstate; jcstate->constructor = ctor; - jcstate->arg_values = (Datum *) palloc(sizeof(Datum) * nargs); - jcstate->arg_nulls = (bool *) palloc(sizeof(bool) * nargs); - jcstate->arg_types = (Oid *) palloc(sizeof(Oid) * nargs); + jcstate->arg_values = palloc_array(Datum, nargs); + jcstate->arg_nulls = palloc_array(bool, nargs); + jcstate->arg_types = palloc_array(Oid, nargs); jcstate->nargs = nargs; foreach(lc, args) @@ -2680,7 +2706,7 @@ ExprEvalPushStep(ExprState *es, const ExprEvalStep *s) if (es->steps_alloc == 0) { es->steps_alloc = 16; - es->steps = palloc(sizeof(ExprEvalStep) * es->steps_alloc); + es->steps = palloc_array(ExprEvalStep, es->steps_alloc); } else if (es->steps_alloc == es->steps_len) { @@ -2732,7 +2758,7 @@ ExecInitFunc(ExprEvalStep *scratch, Expr *node, List *args, Oid funcid, FUNC_MAX_ARGS))); /* Allocate function lookup data and parameter workspace for this call */ - scratch->d.func.finfo = palloc0(sizeof(FmgrInfo)); + scratch->d.func.finfo = palloc0_object(FmgrInfo); scratch->d.func.fcinfo_data = palloc0(SizeForFunctionCallInfo(nargs)); flinfo = scratch->d.func.finfo; fcinfo = scratch->d.func.fcinfo_data; @@ -2833,12 +2859,13 @@ ExecInitSubPlanExpr(SubPlan *subplan, /* * Generate steps to evaluate input arguments for the subplan. * - * We evaluate the argument expressions into ExprState's resvalue/resnull, - * and then use PARAM_SET to update the parameter. We do that, instead of - * evaluating directly into the param, to avoid depending on the pointer - * value remaining stable / being included in the generated expression. No - * danger of conflicts with other uses of resvalue/resnull as storing and - * using the value always is in subsequent steps. + * We evaluate the argument expressions into resv/resnull, and then use + * PARAM_SET to update the parameter. We do that, instead of evaluating + * directly into the param, to avoid depending on the pointer value + * remaining stable / being included in the generated expression. It's ok + * to use resv/resnull for multiple params, as each parameter evaluation + * is immediately followed by an EEOP_PARAM_SET (and thus are saved before + * they could be overwritten again). * * Any calculation we have to do can be done in the parent econtext, since * the Param values don't need to have per-query lifetime. @@ -2849,10 +2876,11 @@ ExecInitSubPlanExpr(SubPlan *subplan, int paramid = lfirst_int(l); Expr *arg = (Expr *) lfirst(pvar); - ExecInitExprRec(arg, state, - &state->resvalue, &state->resnull); + ExecInitExprRec(arg, state, resv, resnull); scratch.opcode = EEOP_PARAM_SET; + scratch.resvalue = resv; + scratch.resnull = resnull; scratch.d.param.paramid = paramid; /* paramtype's not actually used, but we might as well fill it */ scratch.d.param.paramtype = exprType((Node *) arg); @@ -3557,8 +3585,7 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest, * during executor initialization. That means we don't need typcache.c to * provide compiled exprs. */ - constraint_ref = (DomainConstraintRef *) - palloc(sizeof(DomainConstraintRef)); + constraint_ref = palloc_object(DomainConstraintRef); InitDomainConstraintRef(ctest->resulttype, constraint_ref, CurrentMemoryContext, @@ -3588,9 +3615,9 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest, if (scratch->d.domaincheck.checkvalue == NULL) { scratch->d.domaincheck.checkvalue = - (Datum *) palloc(sizeof(Datum)); + palloc_object(Datum); scratch->d.domaincheck.checknull = - (bool *) palloc(sizeof(bool)); + palloc_object(bool); } /* @@ -3608,8 +3635,8 @@ ExecInitCoerceToDomain(ExprEvalStep *scratch, CoerceToDomain *ctest, ExprEvalStep scratch2 = {0}; /* Yes, so make output workspace for MAKE_READONLY */ - domainval = (Datum *) palloc(sizeof(Datum)); - domainnull = (bool *) palloc(sizeof(bool)); + domainval = palloc_object(Datum); + domainnull = palloc_object(bool); /* Emit MAKE_READONLY */ scratch2.opcode = EEOP_MAKE_READONLY; @@ -4159,7 +4186,7 @@ ExecBuildHash32FromAttrs(TupleDesc desc, const TupleTableSlotOps *ops, * one column to hash or an initial value plus one column. */ if ((int64) numCols + (init_value != 0) > 1) - iresult = palloc(sizeof(NullableDatum)); + iresult = palloc_object(NullableDatum); /* find the highest attnum so we deform the tuple to that point */ for (int i = 0; i < numCols; i++) @@ -4282,25 +4309,27 @@ ExecBuildHash32FromAttrs(TupleDesc desc, const TupleTableSlotOps *ops, * 'hash_exprs'. When multiple expressions are present, the hash values * returned by each hash function are combined to produce a single hash value. * + * If any hash_expr yields NULL and the corresponding hash operator is strict, + * the created ExprState will return NULL. (If the operator is not strict, + * we treat NULL values as having a hash value of zero. The hash functions + * themselves are always treated as strict.) + * * desc: tuple descriptor for the to-be-hashed expressions * ops: TupleTableSlotOps for the TupleDesc * hashfunc_oids: Oid for each hash function to call, one for each 'hash_expr' - * collations: collation to use when calling the hash function. - * hash_expr: list of expressions to hash the value of - * opstrict: array corresponding to the 'hashfunc_oids' to store op_strict() + * collations: collation to use when calling the hash function + * hash_exprs: list of expressions to hash the value of + * opstrict: strictness flag for each hash function's comparison operator * parent: PlanState node that the 'hash_exprs' will be evaluated at * init_value: Normally 0, but can be set to other values to seed the hash * with some other value. Using non-zero is slightly less efficient but can * be useful. - * keep_nulls: if true, evaluation of the returned ExprState will abort early - * returning NULL if the given hash function is strict and the Datum to hash - * is null. When set to false, any NULL input Datums are skipped. */ ExprState * ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops, const Oid *hashfunc_oids, const List *collations, const List *hash_exprs, const bool *opstrict, - PlanState *parent, uint32 init_value, bool keep_nulls) + PlanState *parent, uint32 init_value) { ExprState *state = makeNode(ExprState); ExprEvalStep scratch = {0}; @@ -4325,7 +4354,7 @@ ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops, * than one expression to hash or an initial value plus one expression. */ if ((int64) num_exprs + (init_value != 0) > 1) - iresult = palloc(sizeof(NullableDatum)); + iresult = palloc_object(NullableDatum); if (init_value == 0) { @@ -4371,14 +4400,14 @@ ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops, funcid = hashfunc_oids[i]; /* Allocate hash function lookup data. */ - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(1)); fmgr_info(funcid, finfo); /* - * Build the steps to evaluate the hash function's argument have it so - * the value of that is stored in the 0th argument of the hash func. + * Build the steps to evaluate the hash function's argument, placing + * the value in the 0th argument of the hash func. */ ExecInitExprRec(expr, state, @@ -4413,7 +4442,7 @@ ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops, scratch.d.hashdatum.fcinfo_data = fcinfo; scratch.d.hashdatum.fn_addr = finfo->fn_addr; - scratch.opcode = opstrict[i] && !keep_nulls ? strict_opcode : opcode; + scratch.opcode = opstrict[i] ? strict_opcode : opcode; scratch.d.hashdatum.jumpdone = -1; ExprEvalPushStep(state, &scratch); @@ -4540,7 +4569,7 @@ ExecBuildGroupingEqual(TupleDesc ldesc, TupleDesc rdesc, InvokeFunctionExecuteHook(foid); /* Set up the primary fmgr lookup information */ - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(2)); fmgr_info(foid, finfo); fmgr_info_set_expr(NULL, finfo); @@ -4676,7 +4705,7 @@ ExecBuildParamSetEqual(TupleDesc desc, InvokeFunctionExecuteHook(foid); /* Set up the primary fmgr lookup information */ - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(2)); fmgr_info(foid, finfo); fmgr_info_set_expr(NULL, finfo); @@ -4749,7 +4778,7 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, Datum *resv, bool *resnull, ExprEvalStep *scratch) { - JsonExprState *jsestate = palloc0(sizeof(JsonExprState)); + JsonExprState *jsestate = palloc0_object(JsonExprState); ListCell *argexprlc; ListCell *argnamelc; List *jumps_return_null = NIL; @@ -4800,14 +4829,14 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, { Expr *argexpr = (Expr *) lfirst(argexprlc); String *argname = lfirst_node(String, argnamelc); - JsonPathVariable *var = palloc(sizeof(*var)); + JsonPathVariable *var = palloc_object(JsonPathVariable); var->name = argname->sval; var->namelen = strlen(var->name); var->typid = exprType((Node *) argexpr); var->typmod = exprTypmod((Node *) argexpr); - ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + ExecInitExprRec(argexpr, state, &var->value, &var->isnull); jsestate->args = lappend(jsestate->args, var); } @@ -4874,7 +4903,7 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, FunctionCallInfo fcinfo; getTypeInputInfo(jsexpr->returning->typid, &typinput, &typioparam); - finfo = palloc0(sizeof(FmgrInfo)); + finfo = palloc0_object(FmgrInfo); fcinfo = palloc0(SizeForFunctionCallInfo(3)); fmgr_info(typinput, finfo); fmgr_info_set_expr((Node *) jsexpr->returning, finfo); @@ -5067,6 +5096,6 @@ ExecInitJsonCoercion(ExprState *state, JsonReturning *returning, scratch.d.jsonexpr_coercion.exists_cast_to_int = exists_coerce && getBaseType(returning->typid) == INT4OID; scratch.d.jsonexpr_coercion.exists_check_domain = exists_coerce && - DomainHasConstraints(returning->typid); + DomainHasConstraints(returning->typid, NULL); ExprEvalPushStep(state, &scratch); } diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 8a72b5e70a4ec..3c4843cde868e 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -46,7 +46,7 @@ * exported rather than being "static" in this file.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -57,6 +57,7 @@ #include "postgres.h" #include "access/heaptoast.h" +#include "access/tupconvert.h" #include "catalog/pg_type.h" #include "commands/sequence.h" #include "executor/execExpr.h" @@ -77,6 +78,7 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/timestamp.h" +#include "utils/tuplesort.h" #include "utils/typcache.h" #include "utils/xml.h" @@ -2815,7 +2817,7 @@ ExecJustHashVarImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) *isnull = false; if (!fcinfo->args[0].isnull) - return DatumGetUInt32(hashop->d.hashdatum.fn_addr(fcinfo)); + return hashop->d.hashdatum.fn_addr(fcinfo); else return (Datum) 0; } @@ -2849,7 +2851,7 @@ ExecJustHashVarVirtImpl(ExprState *state, TupleTableSlot *slot, bool *isnull) *isnull = false; if (!fcinfo->args[0].isnull) - return DatumGetUInt32(hashop->d.hashdatum.fn_addr(fcinfo)); + return hashop->d.hashdatum.fn_addr(fcinfo); else return (Datum) 0; } @@ -2892,7 +2894,7 @@ ExecJustHashOuterVarStrict(ExprState *state, ExprContext *econtext, if (!fcinfo->args[0].isnull) { *isnull = false; - return DatumGetUInt32(hashop->d.hashdatum.fn_addr(fcinfo)); + return hashop->d.hashdatum.fn_addr(fcinfo); } else { @@ -3107,7 +3109,7 @@ ExecEvalParamExtern(ExprState *state, ExprEvalStep *op, ExprContext *econtext) /* * Set value of a param (currently always PARAM_EXEC) from - * state->res{value,null}. + * op->res{value,null}. */ void ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) @@ -3119,8 +3121,8 @@ ExecEvalParamSet(ExprState *state, ExprEvalStep *op, ExprContext *econtext) /* Shouldn't have a pending evaluation anymore */ Assert(prm->execPlan == NULL); - prm->value = state->resvalue; - prm->isnull = state->resnull; + prm->value = *op->resvalue; + prm->isnull = *op->resnull; } /* @@ -3283,7 +3285,7 @@ ExecEvalNextValueExpr(ExprState *state, ExprEvalStep *op) *op->resvalue = Int32GetDatum((int32) newval); break; case INT8OID: - *op->resvalue = Int64GetDatum((int64) newval); + *op->resvalue = Int64GetDatum(newval); break; default: elog(ERROR, "unsupported sequence type %u", @@ -3440,7 +3442,7 @@ ExecEvalArrayExpr(ExprState *state, ExprEvalStep *op) bool havenulls = false; bool haveempty = false; char **subdata; - bits8 **subbitmaps; + uint8 **subbitmaps; int *subbytes; int *subnitems; int32 dataoffset; @@ -3448,7 +3450,7 @@ ExecEvalArrayExpr(ExprState *state, ExprEvalStep *op) int iitem; subdata = (char **) palloc(nelems * sizeof(char *)); - subbitmaps = (bits8 **) palloc(nelems * sizeof(bits8 *)); + subbitmaps = (uint8 **) palloc(nelems * sizeof(uint8 *)); subbytes = (int *) palloc(nelems * sizeof(int)); subnitems = (int *) palloc(nelems * sizeof(int)); @@ -4032,8 +4034,9 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) int16 typlen; bool typbyval; char typalign; + uint8 typalignby; char *s; - bits8 *bitmap; + uint8 *bitmap; int bitmask; /* @@ -4086,6 +4089,7 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) typlen = op->d.scalararrayop.typlen; typbyval = op->d.scalararrayop.typbyval; typalign = op->d.scalararrayop.typalign; + typalignby = typalign_to_alignby(typalign); /* Initialize result appropriately depending on useOr */ result = BoolGetDatum(!useOr); @@ -4111,7 +4115,7 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op) { elt = fetch_att(s, typbyval, typlen); s = att_addlength_pointer(s, typlen, s); - s = (char *) att_align_nominal(s, typalign); + s = (char *) att_nominal_alignby(s, typalignby); fcinfo->args[1].value = elt; fcinfo->args[1].isnull = false; } @@ -4255,10 +4259,11 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco int16 typlen; bool typbyval; char typalign; + uint8 typalignby; int nitems; bool has_nulls = false; char *s; - bits8 *bitmap; + uint8 *bitmap; int bitmask; MemoryContext oldcontext; ArrayType *arr; @@ -4272,6 +4277,7 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco &typlen, &typbyval, &typalign); + typalignby = typalign_to_alignby(typalign); oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory); @@ -4318,7 +4324,7 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco element = fetch_att(s, typbyval, typlen); s = att_addlength_pointer(s, typlen, s); - s = (char *) att_align_nominal(s, typalign); + s = (char *) att_nominal_alignby(s, typalignby); saophash_insert(elements_tab->hashtab, element, &hashfound); } @@ -4393,7 +4399,7 @@ ExecEvalHashedScalarArrayOp(ExprState *state, ExprEvalStep *op, ExprContext *eco * is the equality function and we need not-equals. */ if (!inclause) - result = !result; + result = BoolGetDatum(!DatumGetBool(result)); } } @@ -4542,7 +4548,7 @@ ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op) *op->resvalue = PointerGetDatum(xmlparse(data, xexpr->xmloption, - preserve_whitespace)); + preserve_whitespace, NULL)); *op->resnull = false; } break; @@ -4736,7 +4742,7 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) { JsonIsPredicate *pred = op->d.is_json.pred; Datum js = *op->resvalue; - Oid exprtype; + Oid exprtype = pred->exprBaseType; bool res; if (*op->resnull) @@ -4745,8 +4751,6 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op) return; } - exprtype = exprType(pred->expr); - if (exprtype == TEXTOID || exprtype == JSONOID) { text *json = DatumGetTextP(js); @@ -5228,7 +5232,6 @@ ExecEvalJsonCoercionFinish(ExprState *state, ExprEvalStep *op) * JsonBehavior expression. */ jsestate->escontext.error_occurred = false; - jsestate->escontext.error_occurred = false; jsestate->escontext.details_wanted = true; } } diff --git a/src/backend/executor/execGrouping.c b/src/backend/executor/execGrouping.c index 255bd795361a2..c107514a85d2a 100644 --- a/src/backend/executor/execGrouping.c +++ b/src/backend/executor/execGrouping.c @@ -3,7 +3,7 @@ * execGrouping.c * executor utility routines for grouping, hashing, and aggregation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,15 +14,18 @@ */ #include "postgres.h" +#include + +#include "access/htup_details.h" #include "access/parallel.h" #include "common/hashfn.h" #include "executor/executor.h" #include "miscadmin.h" #include "utils/lsyscache.h" -static int TupleHashTableMatch(struct tuplehash_hash *tb, const MinimalTuple tuple1, const MinimalTuple tuple2); +static int TupleHashTableMatch(struct tuplehash_hash *tb, MinimalTuple tuple1, MinimalTuple tuple2); static inline uint32 TupleHashTableHash_internal(struct tuplehash_hash *tb, - const MinimalTuple tuple); + MinimalTuple tuple); static inline TupleHashEntry LookupTupleHashEntry_internal(TupleHashTable hashtable, TupleTableSlot *slot, bool *isnew, uint32 hash); @@ -143,10 +146,10 @@ execTuplesHashPrepare(int numCols, * eqfuncoids: OIDs of equality comparison functions to use * hashfunctions: FmgrInfos of datatype-specific hashing functions to use * collations: collations to use in comparisons - * nbuckets: initial estimate of hashtable size - * additionalsize: size of data stored in ->additional - * metacxt: memory context for long-lived allocation, but not per-entry data - * tablecxt: memory context in which to store table entries + * nelements: initial estimate of hashtable size + * additionalsize: size of data that may be stored along with the hash entry + * metacxt: memory context for long-lived data and the simplehash table + * tuplescxt: memory context in which to store the hashed tuples themselves * tempcxt: short-lived context for evaluation hash and comparison functions * use_variable_hash_iv: if true, adjust hash IV per-parallel-worker * @@ -156,6 +159,26 @@ execTuplesHashPrepare(int numCols, * * Note that the keyColIdx, hashfunctions, and collations arrays must be * allocated in storage that will live as long as the hashtable does. + * + * The metacxt and tuplescxt are separate because it's usually desirable for + * tuplescxt to be a BumpContext to avoid memory wastage, while metacxt must + * support pfree in case the simplehash table needs to be enlarged. (We could + * simplify the API of TupleHashTables by managing the tuplescxt internally. + * But that would be disadvantageous to nodeAgg.c and nodeSubplan.c, which use + * a single tuplescxt for multiple TupleHashTables that are reset together.) + * + * LookupTupleHashEntry, FindTupleHashEntry, and related functions may leak + * memory in the tempcxt. It is caller's responsibility to reset that context + * reasonably often, typically once per tuple. (We do it that way, rather + * than managing an extra context within the hashtable, because in many cases + * the caller can specify a tempcxt that it needs to reset per-tuple anyway.) + * + * We don't currently provide DestroyTupleHashTable functionality; the hash + * table will be cleaned up at destruction of the metacxt. (Some callers + * bother to delete the tuplescxt explicitly, though it'd be sufficient to + * ensure it's a child of the metacxt.) There's not much point in working + * harder than this so long as the expression-evaluation infrastructure + * behaves similarly. */ TupleHashTable BuildTupleHashTable(PlanState *parent, @@ -166,37 +189,47 @@ BuildTupleHashTable(PlanState *parent, const Oid *eqfuncoids, FmgrInfo *hashfunctions, Oid *collations, - long nbuckets, + double nelements, Size additionalsize, MemoryContext metacxt, - MemoryContext tablecxt, + MemoryContext tuplescxt, MemoryContext tempcxt, bool use_variable_hash_iv) { TupleHashTable hashtable; - Size entrysize; - Size hash_mem_limit; + uint32 nbuckets; MemoryContext oldcontext; - bool allow_jit; uint32 hash_iv = 0; - Assert(nbuckets > 0); - additionalsize = MAXALIGN(additionalsize); - entrysize = sizeof(TupleHashEntryData) + additionalsize; + /* + * tuplehash_create requires a uint32 element count, so we had better + * clamp the given nelements to fit in that. As long as we have to do + * that, we might as well protect against completely insane input like + * zero or NaN. But it is not our job here to enforce issues like staying + * within hash_mem: the caller should have done that, and we don't have + * enough info to second-guess. + */ + if (isnan(nelements) || nelements <= 0) + nbuckets = 1; + else if (nelements >= PG_UINT32_MAX) + nbuckets = PG_UINT32_MAX; + else + nbuckets = (uint32) nelements; - /* Limit initial table size request to not more than hash_mem */ - hash_mem_limit = get_hash_memory_limit() / entrysize; - if (nbuckets > hash_mem_limit) - nbuckets = hash_mem_limit; + /* tuplescxt must be separate, else ResetTupleHashTable breaks things */ + Assert(metacxt != tuplescxt); + + /* ensure additionalsize is maxalign'ed */ + additionalsize = MAXALIGN(additionalsize); oldcontext = MemoryContextSwitchTo(metacxt); - hashtable = (TupleHashTable) palloc(sizeof(TupleHashTableData)); + hashtable = palloc_object(TupleHashTableData); hashtable->numCols = numCols; hashtable->keyColIdx = keyColIdx; hashtable->tab_collations = collations; - hashtable->tablecxt = tablecxt; + hashtable->tuplescxt = tuplescxt; hashtable->tempcxt = tempcxt; hashtable->additionalsize = additionalsize; hashtable->tableslot = NULL; /* will be made on first lookup */ @@ -224,16 +257,6 @@ BuildTupleHashTable(PlanState *parent, hashtable->tableslot = MakeSingleTupleTableSlot(CreateTupleDescCopy(inputDesc), &TTSOpsMinimalTuple); - /* - * If the caller fails to make the metacxt different from the tablecxt, - * allowing JIT would lead to the generated functions to a) live longer - * than the query or b) be re-generated each time the table is being - * reset. Therefore prevent JIT from being used in that case, by not - * providing a parent node (which prevents accessing the JitContext in the - * EState). - */ - allow_jit = (metacxt != tablecxt); - /* build hash ExprState for all columns */ hashtable->tab_hash_expr = ExecBuildHash32FromAttrs(inputDesc, inputOps, @@ -241,7 +264,7 @@ BuildTupleHashTable(PlanState *parent, collations, numCols, keyColIdx, - allow_jit ? parent : NULL, + parent, hash_iv); /* build comparator for all columns */ @@ -250,7 +273,7 @@ BuildTupleHashTable(PlanState *parent, &TTSOpsMinimalTuple, numCols, keyColIdx, eqfuncoids, collations, - allow_jit ? parent : NULL); + parent); /* * While not pretty, it's ok to not shut down this context, but instead @@ -267,13 +290,77 @@ BuildTupleHashTable(PlanState *parent, /* * Reset contents of the hashtable to be empty, preserving all the non-content - * state. Note that the tablecxt passed to BuildTupleHashTable() should - * also be reset, otherwise there will be leaks. + * state. + * + * Note: in usages where several TupleHashTables share a tuplescxt, all must + * be reset together, as the first one's reset call will destroy all their + * data. The additional reset calls for the rest will redundantly reset the + * tuplescxt. But because of mcxt.c's isReset flag, that's cheap enough that + * we need not avoid it. */ void ResetTupleHashTable(TupleHashTable hashtable) { tuplehash_reset(hashtable->hashtab); + MemoryContextReset(hashtable->tuplescxt); +} + +/* + * Estimate the amount of space needed for a TupleHashTable with nentries + * entries, if the tuples have average data width tupleWidth and the caller + * requires additionalsize extra space per entry. + * + * Return SIZE_MAX if it'd overflow size_t. + * + * nentries is "double" because this is meant for use by the planner, + * which typically works with double rowcount estimates. So we'd need to + * clamp to integer somewhere and that might as well be here. We do expect + * the value not to be NaN or negative, else the result will be garbage. + */ +Size +EstimateTupleHashTableSpace(double nentries, + Size tupleWidth, + Size additionalsize) +{ + Size sh_space; + double tuples_space; + + /* First estimate the space needed for the simplehash table */ + sh_space = tuplehash_estimate_space(nentries); + + /* Give up if that's already too big */ + if (sh_space >= SIZE_MAX) + return sh_space; + + /* + * Compute space needed for hashed tuples with additional data. nentries + * must be somewhat sane, so it should be safe to compute this product. + * + * We assume that the hashed tuples will be kept in a BumpContext so that + * there is not additional per-tuple overhead. + * + * (Note that this is only accurate if MEMORY_CONTEXT_CHECKING is off, + * else bump.c will add a MemoryChunk header to each tuple. However, it + * seems undesirable for debug builds to make different planning choices + * than production builds, so we assume the production behavior always.) + */ + tuples_space = nentries * (MAXALIGN(SizeofMinimalTupleHeader) + + MAXALIGN(tupleWidth) + + MAXALIGN(additionalsize)); + + /* + * Check for size_t overflow. This coding is trickier than it may appear, + * because on 64-bit machines SIZE_MAX cannot be represented exactly as a + * double. We must cast it explicitly to suppress compiler warnings about + * an inexact conversion, and we must trust that any double value that + * compares strictly less than "(double) SIZE_MAX" will cast to a + * representable size_t value. + */ + if (sh_space + tuples_space >= (double) SIZE_MAX) + return SIZE_MAX; + + /* We don't bother estimating size of the miscellaneous overhead data */ + return (Size) (sh_space + tuples_space); } /* @@ -288,7 +375,7 @@ ResetTupleHashTable(TupleHashTable hashtable) * * If isnew isn't NULL, then a new entry is created if no existing entry * matches. On return, *isnew is true if the entry is newly created, - * false if it existed already. ->additional_data in the new entry has + * false if it existed already. The additional data in the new entry has * been zeroed. */ TupleHashEntry @@ -413,7 +500,7 @@ FindTupleHashEntry(TupleHashTable hashtable, TupleTableSlot *slot, */ static uint32 TupleHashTableHash_internal(struct tuplehash_hash *tb, - const MinimalTuple tuple) + MinimalTuple tuple) { TupleHashTable hashtable = (TupleHashTable) tb->private_data; uint32 hashkey; @@ -483,10 +570,10 @@ LookupTupleHashEntry_internal(TupleHashTable hashtable, TupleTableSlot *slot, /* created new entry */ *isnew = true; - MemoryContextSwitchTo(hashtable->tablecxt); + MemoryContextSwitchTo(hashtable->tuplescxt); /* - * Copy the first tuple into the table context, and request + * Copy the first tuple into the tuples context, and request * additionalsize extra bytes before the allocation. * * The caller can get a pointer to the additional data with @@ -511,7 +598,7 @@ LookupTupleHashEntry_internal(TupleHashTable hashtable, TupleTableSlot *slot, * See whether two tuples (presumably of the same hash value) match */ static int -TupleHashTableMatch(struct tuplehash_hash *tb, const MinimalTuple tuple1, const MinimalTuple tuple2) +TupleHashTableMatch(struct tuplehash_hash *tb, MinimalTuple tuple1, MinimalTuple tuple2) { TupleTableSlot *slot1; TupleTableSlot *slot2; diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c index bdf862b24062e..eb383812901aa 100644 --- a/src/backend/executor/execIndexing.c +++ b/src/backend/executor/execIndexing.c @@ -54,9 +54,9 @@ * --------------------- * * Speculative insertion is a two-phase mechanism used to implement - * INSERT ... ON CONFLICT DO UPDATE/NOTHING. The tuple is first inserted - * to the heap and update the indexes as usual, but if a constraint is - * violated, we can still back out the insertion without aborting the whole + * INSERT ... ON CONFLICT. The tuple is first inserted into the heap + * and the indexes are updated as usual, but if a constraint is violated, + * we can still back out of the insertion without aborting the whole * transaction. In an INSERT ... ON CONFLICT statement, if a conflict is * detected, the inserted tuple is backed out and the ON CONFLICT action is * executed instead. @@ -95,7 +95,7 @@ * with the higher XID backs out. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -114,6 +114,8 @@ #include "executor/executor.h" #include "nodes/nodeFuncs.h" #include "storage/lmgr.h" +#include "utils/injection_point.h" +#include "utils/lsyscache.h" #include "utils/multirangetypes.h" #include "utils/rangetypes.h" #include "utils/snapmgr.h" @@ -128,7 +130,7 @@ typedef enum static bool check_exclusion_or_unique_constraint(Relation heap, Relation index, IndexInfo *indexInfo, - ItemPointer tupleid, + const ItemPointerData *tupleid, const Datum *values, const bool *isnull, EState *estate, bool newIndex, CEOUC_WAIT_MODE waitMode, @@ -187,8 +189,8 @@ ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative) /* * allocate space for result arrays */ - relationDescs = (RelationPtr) palloc(len * sizeof(Relation)); - indexInfoArray = (IndexInfo **) palloc(len * sizeof(IndexInfo *)); + relationDescs = palloc_array(Relation, len); + indexInfoArray = palloc_array(IndexInfo *, len); resultRelInfo->ri_NumIndices = len; resultRelInfo->ri_IndexRelationDescs = relationDescs; @@ -275,45 +277,43 @@ ExecCloseIndices(ResultRelInfo *resultRelInfo) * into all the relations indexing the result relation * when a heap tuple is inserted into the result relation. * - * When 'update' is true and 'onlySummarizing' is false, + * When EIIT_IS_UPDATE is set and EIIT_ONLY_SUMMARIZING isn't, * executor is performing an UPDATE that could not use an * optimization like heapam's HOT (in more general terms a * call to table_tuple_update() took place and set - * 'update_indexes' to TUUI_All). Receiving this hint makes + * 'update_indexes' to TU_All). Receiving this hint makes * us consider if we should pass down the 'indexUnchanged' * hint in turn. That's something that we figure out for - * each index_insert() call iff 'update' is true. - * (When 'update' is false we already know not to pass the + * each index_insert() call iff EIIT_IS_UPDATE is set. + * (When that flag is not set we already know not to pass the * hint to any index.) * - * If onlySummarizing is set, an equivalent optimization to + * If EIIT_ONLY_SUMMARIZING is set, an equivalent optimization to * HOT has been applied and any updated columns are indexed * only by summarizing indexes (or in more general terms a * call to table_tuple_update() took place and set - * 'update_indexes' to TUUI_Summarizing). We can (and must) + * 'update_indexes' to TU_Summarizing). We can (and must) * therefore only update the indexes that have * 'amsummarizing' = true. * * Unique and exclusion constraints are enforced at the same * time. This returns a list of index OIDs for any unique or * exclusion constraints that are deferred and that had - * potential (unconfirmed) conflicts. (if noDupErr == true, + * potential (unconfirmed) conflicts. (if EIIT_NO_DUPE_ERROR, * the same is done for non-deferred constraints, but report * if conflict was speculative or deferred conflict to caller) * - * If 'arbiterIndexes' is nonempty, noDupErr applies only to - * those indexes. NIL means noDupErr applies to all indexes. + * If 'arbiterIndexes' is nonempty, EIIT_NO_DUPE_ERROR applies only to + * those indexes. NIL means EIIT_NO_DUPE_ERROR applies to all indexes. * ---------------------------------------------------------------- */ List * ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, - TupleTableSlot *slot, EState *estate, - bool update, - bool noDupErr, - bool *specConflict, + uint32 flags, + TupleTableSlot *slot, List *arbiterIndexes, - bool onlySummarizing) + bool *specConflict) { ItemPointer tupleid = &slot->tts_tid; List *result = NIL; @@ -373,7 +373,7 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, * Skip processing of non-summarizing indexes if we only update * summarizing indexes */ - if (onlySummarizing && !indexInfo->ii_Summarizing) + if ((flags & EIIT_ONLY_SUMMARIZING) && !indexInfo->ii_Summarizing) continue; /* Check for partial index */ @@ -408,7 +408,7 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, isnull); /* Check whether to apply noDupErr to this index */ - applyNoDupErr = noDupErr && + applyNoDupErr = (flags & EIIT_NO_DUPE_ERROR) && (arbiterIndexes == NIL || list_member_oid(arbiterIndexes, indexRelation->rd_index->indexrelid)); @@ -440,10 +440,11 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, * index. If we're being called as part of an UPDATE statement, * consider if the 'indexUnchanged' = true hint should be passed. */ - indexUnchanged = update && index_unchanged_by_update(resultRelInfo, - estate, - indexInfo, - indexRelation); + indexUnchanged = ((flags & EIIT_IS_UPDATE) && + index_unchanged_by_update(resultRelInfo, + estate, + indexInfo, + indexRelation)); satisfiesConstraint = index_insert(indexRelation, /* index relation */ @@ -541,7 +542,7 @@ ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, bool ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate, ItemPointer conflictTid, - ItemPointer tupleid, List *arbiterIndexes) + const ItemPointerData *tupleid, List *arbiterIndexes) { int i; int numIndices; @@ -703,7 +704,7 @@ ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, static bool check_exclusion_or_unique_constraint(Relation heap, Relation index, IndexInfo *indexInfo, - ItemPointer tupleid, + const ItemPointerData *tupleid, const Datum *values, const bool *isnull, EState *estate, bool newIndex, CEOUC_WAIT_MODE waitMode, @@ -753,11 +754,18 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index, { TupleDesc tupdesc = RelationGetDescr(heap); Form_pg_attribute att = TupleDescAttr(tupdesc, attno - 1); - TypeCacheEntry *typcache = lookup_type_cache(att->atttypid, 0); + TypeCacheEntry *typcache = lookup_type_cache(att->atttypid, + TYPECACHE_DOMAIN_BASE_INFO); + char typtype; + + if (OidIsValid(typcache->domainBaseType)) + typtype = get_typtype(typcache->domainBaseType); + else + typtype = typcache->typtype; ExecWithoutOverlapsNotEmpty(heap, att->attname, values[indnkeyatts - 1], - typcache->typtype, att->atttypid); + typtype, att->atttypid); } } @@ -815,7 +823,9 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index, retry: conflict = false; found_self = false; - index_scan = index_beginscan(heap, index, &DirtySnapshot, NULL, indnkeyatts, 0); + index_scan = index_beginscan(heap, index, + &DirtySnapshot, NULL, indnkeyatts, 0, + SO_NONE); index_rescan(index_scan, scankeys, indnkeyatts, NULL, 0); while (index_getnext_slot(index_scan, ForwardScanDirection, existing_slot)) @@ -943,6 +953,11 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index, ExecDropSingleTupleTableSlot(existing_slot); +#ifdef USE_INJECTION_POINTS + if (!conflict) + INJECTION_POINT("check-exclusion-or-unique-constraint-no-conflict", NULL); +#endif + return !conflict; } @@ -955,7 +970,7 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index, void check_exclusion_constraint(Relation heap, Relation index, IndexInfo *indexInfo, - ItemPointer tupleid, + const ItemPointerData *tupleid, const Datum *values, const bool *isnull, EState *estate, bool newIndex) { diff --git a/src/backend/executor/execJunk.c b/src/backend/executor/execJunk.c index 3f196de1ad286..3736bea3b3c3d 100644 --- a/src/backend/executor/execJunk.c +++ b/src/backend/executor/execJunk.c @@ -3,7 +3,7 @@ * execJunk.c * Junk attribute support stuff.... * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 0391798dd2c33..4b30f7686801a 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -26,7 +26,7 @@ * before ExecutorEnd. This can be omitted only in case of EXPLAIN, * which should also omit ExecutorRun. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -40,6 +40,7 @@ #include "access/sysattr.h" #include "access/table.h" #include "access/tableam.h" +#include "access/tupconvert.h" #include "access/xact.h" #include "catalog/namespace.h" #include "catalog/partition.h" @@ -47,6 +48,7 @@ #include "commands/trigger.h" #include "executor/executor.h" #include "executor/execPartition.h" +#include "executor/instrument.h" #include "executor/nodeSubplan.h" #include "foreign/fdwapi.h" #include "mb/pg_wchar.h" @@ -84,7 +86,6 @@ static void ExecutePlan(QueryDesc *queryDesc, uint64 numberTuples, ScanDirection direction, DestReceiver *dest); -static bool ExecCheckOneRelPerms(RTEPermissionInfo *perminfo); static bool ExecCheckPermissionsModified(Oid relOid, Oid userid, Bitmapset *modifiedCols, AclMode requiredPerms); @@ -190,7 +191,7 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) nParamExec = list_length(queryDesc->plannedstmt->paramExecTypes); estate->es_param_exec_vals = (ParamExecData *) - palloc0(nParamExec * sizeof(ParamExecData)); + palloc0_array(ParamExecData, nParamExec); } /* We now require all callers to provide sourceText */ @@ -249,6 +250,15 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags) estate->es_instrument = queryDesc->instrument_options; estate->es_jit_flags = queryDesc->plannedstmt->jitFlags; + /* + * Set up query-level instrumentation if extensions have requested it via + * query_instr_options. Ensure an extension has not allocated query_instr + * itself. + */ + Assert(queryDesc->query_instr == NULL); + if (queryDesc->query_instr_options) + queryDesc->query_instr = InstrAlloc(queryDesc->query_instr_options); + /* * Set up an AFTER-trigger statement context, unless told not to, or * unless it's EXPLAIN-only mode (when ExecutorFinish won't be called). @@ -331,8 +341,8 @@ standard_ExecutorRun(QueryDesc *queryDesc, oldcontext = MemoryContextSwitchTo(estate->es_query_cxt); /* Allow instrumentation of Executor overall runtime */ - if (queryDesc->totaltime) - InstrStartNode(queryDesc->totaltime); + if (queryDesc->query_instr) + InstrStart(queryDesc->query_instr); /* * extract information from the query descriptor and the query feature. @@ -383,8 +393,8 @@ standard_ExecutorRun(QueryDesc *queryDesc, if (sendTuples) dest->rShutdown(dest); - if (queryDesc->totaltime) - InstrStopNode(queryDesc->totaltime, estate->es_processed); + if (queryDesc->query_instr) + InstrStop(queryDesc->query_instr); MemoryContextSwitchTo(oldcontext); } @@ -433,8 +443,8 @@ standard_ExecutorFinish(QueryDesc *queryDesc) oldcontext = MemoryContextSwitchTo(estate->es_query_cxt); /* Allow instrumentation of Executor overall runtime */ - if (queryDesc->totaltime) - InstrStartNode(queryDesc->totaltime); + if (queryDesc->query_instr) + InstrStart(queryDesc->query_instr); /* Run ModifyTable nodes to completion */ ExecPostprocessPlan(estate); @@ -443,8 +453,8 @@ standard_ExecutorFinish(QueryDesc *queryDesc) if (!(estate->es_top_eflags & EXEC_FLAG_SKIP_TRIGGERS)) AfterTriggerEndQuery(estate); - if (queryDesc->totaltime) - InstrStopNode(queryDesc->totaltime, 0); + if (queryDesc->query_instr) + InstrStop(queryDesc->query_instr); MemoryContextSwitchTo(oldcontext); @@ -523,7 +533,7 @@ standard_ExecutorEnd(QueryDesc *queryDesc) queryDesc->tupDesc = NULL; queryDesc->estate = NULL; queryDesc->planstate = NULL; - queryDesc->totaltime = NULL; + queryDesc->query_instr = NULL; } /* ---------------------------------------------------------------- @@ -600,11 +610,11 @@ ExecCheckPermissions(List *rangeTable, List *rteperminfos, /* * Only relation RTEs and subquery RTEs that were once relation - * RTEs (views) have their perminfoindex set. + * RTEs (views, property graphs) have their perminfoindex set. */ Assert(rte->rtekind == RTE_RELATION || (rte->rtekind == RTE_SUBQUERY && - rte->relkind == RELKIND_VIEW)); + (rte->relkind == RELKIND_VIEW || rte->relkind == RELKIND_PROPGRAPH))); (void) getRTEPermissionInfo(rteperminfos, rte); /* Many-to-one mapping not allowed */ @@ -643,7 +653,7 @@ ExecCheckPermissions(List *rangeTable, List *rteperminfos, * ExecCheckOneRelPerms * Check access permissions for a single relation. */ -static bool +bool ExecCheckOneRelPerms(RTEPermissionInfo *perminfo) { AclMode requiredPerms; @@ -877,25 +887,29 @@ InitPlan(QueryDesc *queryDesc, int eflags) if (plannedstmt->rowMarks) { estate->es_rowmarks = (ExecRowMark **) - palloc0(estate->es_range_table_size * sizeof(ExecRowMark *)); + palloc0_array(ExecRowMark *, estate->es_range_table_size); foreach(l, plannedstmt->rowMarks) { PlanRowMark *rc = (PlanRowMark *) lfirst(l); + RangeTblEntry *rte = exec_rt_fetch(rc->rti, estate); Oid relid; Relation relation; ExecRowMark *erm; + /* ignore "parent" rowmarks; they are irrelevant at runtime */ + if (rc->isParent) + continue; + /* - * Ignore "parent" rowmarks, because they are irrelevant at - * runtime. Also ignore the rowmarks belonging to child tables - * that have been pruned in ExecDoInitialPruning(). + * Also ignore rowmarks belonging to child tables that have been + * pruned in ExecDoInitialPruning(). */ - if (rc->isParent || + if (rte->rtekind == RTE_RELATION && !bms_is_member(rc->rti, estate->es_unpruned_relids)) continue; /* get relation's OID (will produce InvalidOid if subquery) */ - relid = exec_rt_fetch(rc->rti, estate)->relid; + relid = rte->relid; /* open relation, if we need to access it for this mark type */ switch (rc->markType) @@ -921,7 +935,7 @@ InitPlan(QueryDesc *queryDesc, int eflags) if (relation) CheckValidRowMarkRel(relation, rc->markType); - erm = (ExecRowMark *) palloc(sizeof(ExecRowMark)); + erm = palloc_object(ExecRowMark); erm->relation = relation; erm->relid = relid; erm->rti = rc->rti; @@ -1037,6 +1051,9 @@ InitPlan(QueryDesc *queryDesc, int eflags) * Generally the parser and/or planner should have noticed any such mistake * already, but let's make sure. * + * For INSERT ON CONFLICT, the result relation is required to support the + * onConflictAction, regardless of whether a conflict actually occurs. + * * For MERGE, mergeActions is the list of actions that may be performed. The * result relation is required to support every action, regardless of whether * or not they are all executed. @@ -1046,7 +1063,7 @@ InitPlan(QueryDesc *queryDesc, int eflags) */ void CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, - List *mergeActions) + OnConflictAction onConflictAction, List *mergeActions) { Relation resultRel = resultRelInfo->ri_RelationDesc; FdwRoutine *fdwroutine; @@ -1059,7 +1076,23 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, { case RELKIND_RELATION: case RELKIND_PARTITIONED_TABLE: - CheckCmdReplicaIdentity(resultRel, operation); + + /* + * For MERGE, check that the target relation supports each action. + * For other operations, just check the operation itself. + */ + if (operation == CMD_MERGE) + foreach_node(MergeAction, action, mergeActions) + CheckCmdReplicaIdentity(resultRel, action->commandType); + else + CheckCmdReplicaIdentity(resultRel, operation); + + /* + * For INSERT ON CONFLICT DO UPDATE, additionally check that the + * target relation supports UPDATE. + */ + if (onConflictAction == ONCONFLICT_UPDATE) + CheckCmdReplicaIdentity(resultRel, CMD_UPDATE); break; case RELKIND_SEQUENCE: ereport(ERROR, @@ -1141,6 +1174,12 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, break; } break; + case RELKIND_PROPGRAPH: + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot change property graph \"%s\"", + RelationGetRelationName(resultRel)))); + break; default: ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -1205,6 +1244,13 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType) errmsg("cannot lock rows in foreign table \"%s\"", RelationGetRelationName(rel)))); break; + case RELKIND_PROPGRAPH: + /* Should not get here; rewriter should have expanded the graph */ + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg_internal("cannot lock rows in property graph \"%s\"", + RelationGetRelationName(rel)))); + break; default: ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -1244,11 +1290,11 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, int n = resultRelInfo->ri_TrigDesc->numtriggers; resultRelInfo->ri_TrigFunctions = (FmgrInfo *) - palloc0(n * sizeof(FmgrInfo)); + palloc0_array(FmgrInfo, n); resultRelInfo->ri_TrigWhenExprs = (ExprState **) - palloc0(n * sizeof(ExprState *)); + palloc0_array(ExprState *, n); if (instrument_options) - resultRelInfo->ri_TrigInstrument = InstrAlloc(n, instrument_options, false); + resultRelInfo->ri_TrigInstrument = InstrAllocTrigger(n, instrument_options); } else { @@ -1277,6 +1323,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, resultRelInfo->ri_projectReturning = NULL; resultRelInfo->ri_onConflictArbiterIndexes = NIL; resultRelInfo->ri_onConflict = NULL; + resultRelInfo->ri_forPortionOf = NULL; resultRelInfo->ri_ReturningSlot = NULL; resultRelInfo->ri_TrigOldSlot = NULL; resultRelInfo->ri_TrigNewSlot = NULL; @@ -1308,10 +1355,9 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, * Get a ResultRelInfo for a trigger target relation. * * Most of the time, triggers are fired on one of the result relations of the - * query, and so we can just return a member of the es_result_relations array, - * or the es_tuple_routing_result_relations list (if any). (Note: in self-join - * situations there might be multiple members with the same OID; if so it - * doesn't matter which one we pick.) + * query, and so we can just return a suitable one we already made and stored + * in the es_opened_result_relations or es_tuple_routing_result_relations + * Lists. * * However, it is sometimes necessary to fire triggers on other relations; * this happens mainly when an RI update trigger queues additional triggers @@ -1331,11 +1377,20 @@ ExecGetTriggerResultRel(EState *estate, Oid relid, Relation rel; MemoryContext oldcontext; + /* + * Before creating a new ResultRelInfo, check if we've already made and + * cached one for this relation. We must ensure that the given + * 'rootRelInfo' matches the one stored in the cached ResultRelInfo as + * trigger handling for partitions can result in mixed requirements for + * what ri_RootResultRelInfo is set to. + */ + /* Search through the query result relations */ foreach(l, estate->es_opened_result_relations) { rInfo = lfirst(l); - if (RelationGetRelid(rInfo->ri_RelationDesc) == relid) + if (RelationGetRelid(rInfo->ri_RelationDesc) == relid && + rInfo->ri_RootResultRelInfo == rootRelInfo) return rInfo; } @@ -1346,7 +1401,8 @@ ExecGetTriggerResultRel(EState *estate, Oid relid, foreach(l, estate->es_tuple_routing_result_relations) { rInfo = (ResultRelInfo *) lfirst(l); - if (RelationGetRelid(rInfo->ri_RelationDesc) == relid) + if (RelationGetRelid(rInfo->ri_RelationDesc) == relid && + rInfo->ri_RootResultRelInfo == rootRelInfo) return rInfo; } @@ -1354,7 +1410,8 @@ ExecGetTriggerResultRel(EState *estate, Oid relid, foreach(l, estate->es_trig_target_relations) { rInfo = (ResultRelInfo *) lfirst(l); - if (RelationGetRelid(rInfo->ri_RelationDesc) == relid) + if (RelationGetRelid(rInfo->ri_RelationDesc) == relid && + rInfo->ri_RootResultRelInfo == rootRelInfo) return rInfo; } /* Nope, so we need a new one */ @@ -1912,7 +1969,7 @@ ExecPartitionCheckEmitError(ResultRelInfo *resultRelInfo, */ if (map != NULL) slot = execute_attr_map_slot(map, slot, - MakeTupleTableSlot(tupdesc, &TTSOpsVirtual)); + MakeTupleTableSlot(tupdesc, &TTSOpsVirtual, 0)); modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate), ExecGetUpdatedCols(rootrel, estate)); } @@ -2028,7 +2085,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo, */ if (map != NULL) slot = execute_attr_map_slot(map, slot, - MakeTupleTableSlot(tupdesc, &TTSOpsVirtual)); + MakeTupleTableSlot(tupdesc, &TTSOpsVirtual, 0)); modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate), ExecGetUpdatedCols(rootrel, estate)); rel = rootrel->ri_RelationDesc; @@ -2164,7 +2221,7 @@ ReportNotNullViolationError(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, */ if (map != NULL) slot = execute_attr_map_slot(map, slot, - MakeTupleTableSlot(tupdesc, &TTSOpsVirtual)); + MakeTupleTableSlot(tupdesc, &TTSOpsVirtual, 0)); modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate), ExecGetUpdatedCols(rootrel, estate)); rel = rootrel->ri_RelationDesc; @@ -2272,7 +2329,7 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo, */ if (map != NULL) slot = execute_attr_map_slot(map, slot, - MakeTupleTableSlot(tupdesc, &TTSOpsVirtual)); + MakeTupleTableSlot(tupdesc, &TTSOpsVirtual, 0)); modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate), ExecGetUpdatedCols(rootrel, estate)); @@ -2550,7 +2607,7 @@ ExecFindRowMark(EState *estate, Index rti, bool missing_ok) ExecAuxRowMark * ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist) { - ExecAuxRowMark *aerm = (ExecAuxRowMark *) palloc0(sizeof(ExecAuxRowMark)); + ExecAuxRowMark *aerm = palloc0_object(ExecAuxRowMark); char resname[32]; aerm->rowmark = erm; @@ -2706,8 +2763,7 @@ EvalPlanQualInit(EPQState *epqstate, EState *parentestate, * EvalPlanQualBegin(). */ epqstate->tuple_table = NIL; - epqstate->relsubs_slot = (TupleTableSlot **) - palloc0(rtsize * sizeof(TupleTableSlot *)); + epqstate->relsubs_slot = palloc0_array(TupleTableSlot *, rtsize); /* ... and remember data that EvalPlanQualBegin will need */ epqstate->plan = subplan; @@ -3046,8 +3102,7 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree) /* now make the internal param workspace ... */ i = list_length(parentestate->es_plannedstmt->paramExecTypes); - rcestate->es_param_exec_vals = (ParamExecData *) - palloc0(i * sizeof(ParamExecData)); + rcestate->es_param_exec_vals = palloc0_array(ParamExecData, i); /* ... and copy down all values, whether really needed or not */ while (--i >= 0) { @@ -3066,6 +3121,18 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree) */ rcestate->es_unpruned_relids = parentestate->es_unpruned_relids; + /* + * Also make the PartitionPruneInfo and the results of pruning available. + * These need to match exactly so that we initialize all the same Append + * and MergeAppend subplans as the parent did. + */ + rcestate->es_part_prune_infos = parentestate->es_part_prune_infos; + rcestate->es_part_prune_states = parentestate->es_part_prune_states; + rcestate->es_part_prune_results = parentestate->es_part_prune_results; + + /* We'll also borrow the es_partition_directory from the parent state */ + rcestate->es_partition_directory = parentestate->es_partition_directory; + /* * Initialize private state information for each SubPlan. We must do this * before running ExecInitNode on the main query tree, since @@ -3090,8 +3157,7 @@ EvalPlanQualStart(EPQState *epqstate, Plan *planTree) * EvalPlanQualFetchRowMark() can efficiently access the to be fetched * rowmark. */ - epqstate->relsubs_rowmark = (ExecAuxRowMark **) - palloc0(rtsize * sizeof(ExecAuxRowMark *)); + epqstate->relsubs_rowmark = palloc0_array(ExecAuxRowMark *, rtsize); foreach(l, epqstate->arowMarks) { ExecAuxRowMark *earm = (ExecAuxRowMark *) lfirst(l); @@ -3183,6 +3249,13 @@ EvalPlanQualEnd(EPQState *epqstate) MemoryContextSwitchTo(oldcontext); + /* + * NULLify the partition directory before freeing the executor state. + * Since EvalPlanQualStart() just borrowed the parent EState's directory, + * we'd better leave it up to the parent to delete it. + */ + estate->es_partition_directory = NULL; + FreeExecutorState(estate); /* Mark EPQState idle */ diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c index f3e77bda27906..81b87d82fab47 100644 --- a/src/backend/executor/execParallel.c +++ b/src/backend/executor/execParallel.c @@ -3,7 +3,7 @@ * execParallel.c * Support routines for parallel execution. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * This file contains routines that are intended to support setting up, @@ -40,10 +40,12 @@ #include "executor/nodeSeqscan.h" #include "executor/nodeSort.h" #include "executor/nodeSubplan.h" +#include "executor/nodeTidrangescan.h" #include "executor/tqueue.h" #include "jit/jit.h" #include "nodes/nodeFuncs.h" #include "pgstat.h" +#include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/datum.h" #include "utils/dsa.h" @@ -85,7 +87,7 @@ typedef struct FixedParallelExecutorState * instrument_options: Same meaning here as in instrument.c. * * instrument_offset: Offset, relative to the start of this structure, - * of the first Instrumentation object. This will depend on the length of + * of the first NodeInstrumentation object. This will depend on the length of * the plan_node_id array. * * num_workers: Number of workers. @@ -102,11 +104,15 @@ struct SharedExecutorInstrumentation int num_workers; int num_plan_nodes; int plan_node_id[FLEXIBLE_ARRAY_MEMBER]; - /* array of num_plan_nodes * num_workers Instrumentation objects follows */ + + /* + * Array of num_plan_nodes * num_workers NodeInstrumentation objects + * follows. + */ }; #define GetInstrumentationArray(sei) \ - (AssertVariableIsOfTypeMacro(sei, SharedExecutorInstrumentation *), \ - (Instrumentation *) (((char *) sei) + sei->instrument_offset)) + (StaticAssertVariableIsOfTypeMacro(sei, SharedExecutorInstrumentation *), \ + (NodeInstrumentation *) (((char *) sei) + sei->instrument_offset)) /* Context object for ExecParallelEstimate. */ typedef struct ExecParallelEstimateContext @@ -187,8 +193,8 @@ ExecSerializePlan(Plan *plan, EState *estate) pstmt->rtable = estate->es_range_table; pstmt->unprunableRelids = estate->es_unpruned_relids; pstmt->permInfos = estate->es_rteperminfos; - pstmt->resultRelations = NIL; pstmt->appendRelations = NIL; + pstmt->planOrigin = PLAN_STMT_INTERNAL; /* * Transfer only parallel-safe subplans, leaving a NULL "hole" in the list @@ -209,6 +215,13 @@ ExecSerializePlan(Plan *plan, EState *estate) pstmt->rewindPlanIDs = NULL; pstmt->rowMarks = NIL; + + /* + * Pass the row mark and result relation relids to parallel workers. They + * may need to check them to inform heuristics. + */ + pstmt->rowMarkRelids = estate->es_plannedstmt->rowMarkRelids; + pstmt->resultRelationRelids = estate->es_plannedstmt->resultRelationRelids; pstmt->relationOids = NIL; pstmt->invalItems = NIL; /* workers can't replan anyway... */ pstmt->paramExecTypes = estate->es_plannedstmt->paramExecTypes; @@ -244,16 +257,25 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e) if (planstate->plan->parallel_aware) ExecSeqScanEstimate((SeqScanState *) planstate, e->pcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecSeqScanInstrumentEstimate((SeqScanState *) planstate, + e->pcxt); break; case T_IndexScanState: + if (planstate->plan->parallel_aware) + ExecIndexScanEstimate((IndexScanState *) planstate, + e->pcxt); /* even when not parallel-aware, for EXPLAIN ANALYZE */ - ExecIndexScanEstimate((IndexScanState *) planstate, - e->pcxt); + ExecIndexScanInstrumentEstimate((IndexScanState *) planstate, + e->pcxt); break; case T_IndexOnlyScanState: + if (planstate->plan->parallel_aware) + ExecIndexOnlyScanEstimate((IndexOnlyScanState *) planstate, + e->pcxt); /* even when not parallel-aware, for EXPLAIN ANALYZE */ - ExecIndexOnlyScanEstimate((IndexOnlyScanState *) planstate, - e->pcxt); + ExecIndexOnlyScanInstrumentEstimate((IndexOnlyScanState *) planstate, + e->pcxt); break; case T_BitmapIndexScanState: /* even when not parallel-aware, for EXPLAIN ANALYZE */ @@ -265,6 +287,14 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e) ExecForeignScanEstimate((ForeignScanState *) planstate, e->pcxt); break; + case T_TidRangeScanState: + if (planstate->plan->parallel_aware) + ExecTidRangeScanEstimate((TidRangeScanState *) planstate, + e->pcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecTidRangeScanInstrumentEstimate((TidRangeScanState *) planstate, + e->pcxt); + break; case T_AppendState: if (planstate->plan->parallel_aware) ExecAppendEstimate((AppendState *) planstate, @@ -279,6 +309,9 @@ ExecParallelEstimate(PlanState *planstate, ExecParallelEstimateContext *e) if (planstate->plan->parallel_aware) ExecBitmapHeapEstimate((BitmapHeapScanState *) planstate, e->pcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecBitmapHeapInstrumentEstimate((BitmapHeapScanState *) planstate, + e->pcxt); break; case T_HashJoinState: if (planstate->plan->parallel_aware) @@ -473,15 +506,25 @@ ExecParallelInitializeDSM(PlanState *planstate, if (planstate->plan->parallel_aware) ExecSeqScanInitializeDSM((SeqScanState *) planstate, d->pcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecSeqScanInstrumentInitDSM((SeqScanState *) planstate, + d->pcxt); break; case T_IndexScanState: + if (planstate->plan->parallel_aware) + ExecIndexScanInitializeDSM((IndexScanState *) planstate, + d->pcxt); /* even when not parallel-aware, for EXPLAIN ANALYZE */ - ExecIndexScanInitializeDSM((IndexScanState *) planstate, d->pcxt); + ExecIndexScanInstrumentInitDSM((IndexScanState *) planstate, + d->pcxt); break; case T_IndexOnlyScanState: + if (planstate->plan->parallel_aware) + ExecIndexOnlyScanInitializeDSM((IndexOnlyScanState *) planstate, + d->pcxt); /* even when not parallel-aware, for EXPLAIN ANALYZE */ - ExecIndexOnlyScanInitializeDSM((IndexOnlyScanState *) planstate, - d->pcxt); + ExecIndexOnlyScanInstrumentInitDSM((IndexOnlyScanState *) planstate, + d->pcxt); break; case T_BitmapIndexScanState: /* even when not parallel-aware, for EXPLAIN ANALYZE */ @@ -492,6 +535,14 @@ ExecParallelInitializeDSM(PlanState *planstate, ExecForeignScanInitializeDSM((ForeignScanState *) planstate, d->pcxt); break; + case T_TidRangeScanState: + if (planstate->plan->parallel_aware) + ExecTidRangeScanInitializeDSM((TidRangeScanState *) planstate, + d->pcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecTidRangeScanInstrumentInitDSM((TidRangeScanState *) planstate, + d->pcxt); + break; case T_AppendState: if (planstate->plan->parallel_aware) ExecAppendInitializeDSM((AppendState *) planstate, @@ -506,6 +557,9 @@ ExecParallelInitializeDSM(PlanState *planstate, if (planstate->plan->parallel_aware) ExecBitmapHeapInitializeDSM((BitmapHeapScanState *) planstate, d->pcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecBitmapHeapInstrumentInitDSM((BitmapHeapScanState *) planstate, + d->pcxt); break; case T_HashJoinState: if (planstate->plan->parallel_aware) @@ -635,7 +689,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, ExecSetParamPlanMulti(sendParams, GetPerTupleExprContext(estate)); /* Allocate object for return value. */ - pei = palloc0(sizeof(ParallelExecutorInfo)); + pei = palloc0_object(ParallelExecutorInfo); pei->finished = false; pei->planstate = planstate; @@ -712,7 +766,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, instrumentation_len = MAXALIGN(instrumentation_len); instrument_offset = instrumentation_len; instrumentation_len += - mul_size(sizeof(Instrumentation), + mul_size(sizeof(NodeInstrumentation), mul_size(e.nnodes, nworkers)); shm_toc_estimate_chunk(&pcxt->estimator, instrumentation_len); shm_toc_estimate_keys(&pcxt->estimator, 1); @@ -798,7 +852,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, */ if (estate->es_instrument) { - Instrumentation *instrument; + NodeInstrumentation *instrument; int i; instrumentation = shm_toc_allocate(pcxt->toc, instrumentation_len); @@ -808,7 +862,7 @@ ExecInitParallelPlan(PlanState *planstate, EState *estate, instrumentation->num_plan_nodes = e.nnodes; instrument = GetInstrumentationArray(instrumentation); for (i = 0; i < nworkers * e.nnodes; ++i) - InstrInit(&instrument[i], estate->es_instrument); + InstrInitNode(&instrument[i], estate->es_instrument, false); shm_toc_insert(pcxt->toc, PARALLEL_KEY_INSTRUMENTATION, instrumentation); pei->instrumentation = instrumentation; @@ -993,6 +1047,11 @@ ExecParallelReInitializeDSM(PlanState *planstate, ExecForeignScanReInitializeDSM((ForeignScanState *) planstate, pcxt); break; + case T_TidRangeScanState: + if (planstate->plan->parallel_aware) + ExecTidRangeScanReInitializeDSM((TidRangeScanState *) planstate, + pcxt); + break; case T_AppendState: if (planstate->plan->parallel_aware) ExecAppendReInitializeDSM((AppendState *) planstate, pcxt); @@ -1035,7 +1094,7 @@ static bool ExecParallelRetrieveInstrumentation(PlanState *planstate, SharedExecutorInstrumentation *instrumentation) { - Instrumentation *instrument; + NodeInstrumentation *instrument; int i; int n; int ibytes; @@ -1063,9 +1122,9 @@ ExecParallelRetrieveInstrumentation(PlanState *planstate, * Switch into per-query memory context. */ oldcontext = MemoryContextSwitchTo(planstate->state->es_query_cxt); - ibytes = mul_size(instrumentation->num_workers, sizeof(Instrumentation)); + ibytes = mul_size(instrumentation->num_workers, sizeof(NodeInstrumentation)); planstate->worker_instrument = - palloc(ibytes + offsetof(WorkerInstrumentation, instrument)); + palloc(ibytes + offsetof(WorkerNodeInstrumentation, instrument)); MemoryContextSwitchTo(oldcontext); planstate->worker_instrument->num_workers = instrumentation->num_workers; @@ -1101,6 +1160,12 @@ ExecParallelRetrieveInstrumentation(PlanState *planstate, case T_BitmapHeapScanState: ExecBitmapHeapRetrieveInstrumentation((BitmapHeapScanState *) planstate); break; + case T_SeqScanState: + ExecSeqScanRetrieveInstrumentation((SeqScanState *) planstate); + break; + case T_TidRangeScanState: + ExecTidRangeScanRetrieveInstrumentation((TidRangeScanState *) planstate); + break; default: break; } @@ -1295,7 +1360,7 @@ ExecParallelReportInstrumentation(PlanState *planstate, { int i; int plan_node_id = planstate->plan->plan_node_id; - Instrumentation *instrument; + NodeInstrumentation *instrument; InstrEndLoop(planstate->instrument); @@ -1341,15 +1406,24 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt) case T_SeqScanState: if (planstate->plan->parallel_aware) ExecSeqScanInitializeWorker((SeqScanState *) planstate, pwcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecSeqScanInstrumentInitWorker((SeqScanState *) planstate, pwcxt); break; case T_IndexScanState: + if (planstate->plan->parallel_aware) + ExecIndexScanInitializeWorker((IndexScanState *) planstate, + pwcxt); /* even when not parallel-aware, for EXPLAIN ANALYZE */ - ExecIndexScanInitializeWorker((IndexScanState *) planstate, pwcxt); + ExecIndexScanInstrumentInitWorker((IndexScanState *) planstate, + pwcxt); break; case T_IndexOnlyScanState: + if (planstate->plan->parallel_aware) + ExecIndexOnlyScanInitializeWorker((IndexOnlyScanState *) planstate, + pwcxt); /* even when not parallel-aware, for EXPLAIN ANALYZE */ - ExecIndexOnlyScanInitializeWorker((IndexOnlyScanState *) planstate, - pwcxt); + ExecIndexOnlyScanInstrumentInitWorker((IndexOnlyScanState *) planstate, + pwcxt); break; case T_BitmapIndexScanState: /* even when not parallel-aware, for EXPLAIN ANALYZE */ @@ -1361,6 +1435,14 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt) ExecForeignScanInitializeWorker((ForeignScanState *) planstate, pwcxt); break; + case T_TidRangeScanState: + if (planstate->plan->parallel_aware) + ExecTidRangeScanInitializeWorker((TidRangeScanState *) planstate, + pwcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecTidRangeScanInstrumentInitWorker((TidRangeScanState *) planstate, + pwcxt); + break; case T_AppendState: if (planstate->plan->parallel_aware) ExecAppendInitializeWorker((AppendState *) planstate, pwcxt); @@ -1374,6 +1456,9 @@ ExecParallelInitializeWorker(PlanState *planstate, ParallelWorkerContext *pwcxt) if (planstate->plan->parallel_aware) ExecBitmapHeapInitializeWorker((BitmapHeapScanState *) planstate, pwcxt); + /* even when not parallel-aware, for EXPLAIN ANALYZE */ + ExecBitmapHeapInstrumentInitWorker((BitmapHeapScanState *) planstate, + pwcxt); break; case T_HashJoinState: if (planstate->plan->parallel_aware) diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index 514eae1037dc3..d96d4f9947b79 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -3,7 +3,7 @@ * execPartition.c * Support routines for partitioning. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -15,6 +15,8 @@ #include "access/table.h" #include "access/tableam.h" +#include "access/tupconvert.h" +#include "catalog/index.h" #include "catalog/partition.h" #include "executor/execPartition.h" #include "executor/executor.h" @@ -27,6 +29,7 @@ #include "partitioning/partprune.h" #include "rewrite/rewriteManip.h" #include "utils/acl.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/partcache.h" #include "utils/rls.h" @@ -173,11 +176,11 @@ static void FormPartitionKeyDatum(PartitionDispatch pd, EState *estate, Datum *values, bool *isnull); -static int get_partition_for_tuple(PartitionDispatch pd, Datum *values, - bool *isnull); +static int get_partition_for_tuple(PartitionDispatch pd, const Datum *values, + const bool *isnull); static char *ExecBuildSlotPartitionKeyDescription(Relation rel, - Datum *values, - bool *isnull, + const Datum *values, + const bool *isnull, int maxfieldlen); static List *adjust_partition_colnos(List *colnos, ResultRelInfo *leaf_part_rri); static List *adjust_partition_colnos_using_map(List *colnos, AttrMap *attrMap); @@ -226,7 +229,7 @@ ExecSetupPartitionTupleRouting(EState *estate, Relation rel) * The reason for this is that a common case is for INSERT to insert a * single tuple into a partitioned table and this must be fast. */ - proute = (PartitionTupleRouting *) palloc0(sizeof(PartitionTupleRouting)); + proute = palloc0_object(PartitionTupleRouting); proute->partition_root = rel; proute->memcxt = CurrentMemoryContext; /* Rest of members initialized by zeroing */ @@ -360,8 +363,12 @@ ExecFindPartition(ModifyTableState *mtstate, true, false); if (rri) { + ModifyTable *node = (ModifyTable *) mtstate->ps.plan; + /* Verify this ResultRelInfo allows INSERTs */ - CheckValidResultRel(rri, CMD_INSERT, NIL); + CheckValidResultRel(rri, CMD_INSERT, + node ? node->onConflictAction : ONCONFLICT_NONE, + NIL); /* * Initialize information needed to insert this and @@ -486,6 +493,65 @@ ExecFindPartition(ModifyTableState *mtstate, return rri; } +/* + * IsIndexCompatibleAsArbiter + * Return true if two indexes are identical for INSERT ON CONFLICT + * purposes. + * + * Only indexes of the same relation are supported. + */ +static bool +IsIndexCompatibleAsArbiter(Relation arbiterIndexRelation, + IndexInfo *arbiterIndexInfo, + Relation indexRelation, + IndexInfo *indexInfo) +{ + Assert(arbiterIndexRelation->rd_index->indrelid == indexRelation->rd_index->indrelid); + + /* must match whether they're unique */ + if (arbiterIndexInfo->ii_Unique != indexInfo->ii_Unique) + return false; + + /* No support currently for comparing exclusion indexes. */ + if (arbiterIndexInfo->ii_ExclusionOps != NULL || + indexInfo->ii_ExclusionOps != NULL) + return false; + + /* the "nulls not distinct" criterion must match */ + if (arbiterIndexInfo->ii_NullsNotDistinct != + indexInfo->ii_NullsNotDistinct) + return false; + + /* number of key attributes must match */ + if (arbiterIndexInfo->ii_NumIndexKeyAttrs != + indexInfo->ii_NumIndexKeyAttrs) + return false; + + for (int i = 0; i < arbiterIndexInfo->ii_NumIndexKeyAttrs; i++) + { + if (arbiterIndexRelation->rd_indcollation[i] != + indexRelation->rd_indcollation[i]) + return false; + + if (arbiterIndexRelation->rd_opfamily[i] != + indexRelation->rd_opfamily[i]) + return false; + + if (arbiterIndexRelation->rd_index->indkey.values[i] != + indexRelation->rd_index->indkey.values[i]) + return false; + } + + if (list_difference(RelationGetIndexExpressions(arbiterIndexRelation), + RelationGetIndexExpressions(indexRelation)) != NIL) + return false; + + if (list_difference(RelationGetIndexPredicate(arbiterIndexRelation), + RelationGetIndexPredicate(indexRelation)) != NIL) + return false; + return true; +} + /* * ExecInitPartitionInfo * Lock the partition and initialize ResultRelInfo. Also setup other @@ -527,7 +593,8 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, * partition-key becomes a DELETE+INSERT operation, so this check is still * required when the operation is CMD_UPDATE. */ - CheckValidResultRel(leaf_part_rri, CMD_INSERT, NIL); + CheckValidResultRel(leaf_part_rri, CMD_INSERT, + node ? node->onConflictAction : ONCONFLICT_NONE, NIL); /* * Open partition indices. The user may have asked to check for conflicts @@ -684,62 +751,160 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, { TupleDesc partrelDesc = RelationGetDescr(partrel); ExprContext *econtext = mtstate->ps.ps_ExprContext; - ListCell *lc; List *arbiterIndexes = NIL; + int additional_arbiters = 0; /* * If there is a list of arbiter indexes, map it to a list of indexes - * in the partition. We do that by scanning the partition's index - * list and searching for ancestry relationships to each index in the - * ancestor table. + * in the partition. We also add any "identical indexes" to any of + * those, to cover the case where one of them is concurrently being + * reindexed. */ if (rootResultRelInfo->ri_onConflictArbiterIndexes != NIL) { - List *childIdxs; - - childIdxs = RelationGetIndexList(leaf_part_rri->ri_RelationDesc); + List *unparented_idxs = NIL, + *arbiters_listidxs = NIL, + *ancestors_seen = NIL; - foreach(lc, childIdxs) + for (int listidx = 0; listidx < leaf_part_rri->ri_NumIndices; listidx++) { - Oid childIdx = lfirst_oid(lc); + Oid indexoid; List *ancestors; - ListCell *lc2; - ancestors = get_partition_ancestors(childIdx); - foreach(lc2, rootResultRelInfo->ri_onConflictArbiterIndexes) + /* + * If one of this index's ancestors is in the root's arbiter + * list, then use this index as arbiter for this partition. + * Otherwise, if this index has no parent, track it for later, + * in case REINDEX CONCURRENTLY is working on one of the + * arbiters. + * + * However, if two indexes appear to have the same parent, + * treat the second of these as if it had no parent. This + * sounds counterintuitive, but it can happen if a transaction + * running REINDEX CONCURRENTLY commits right between those + * two indexes are checked by another process in this loop. + * This will have the effect of also treating that second + * index as arbiter. + * + * XXX get_partition_ancestors scans pg_inherits, which is not + * only slow, but also means the catalog snapshot can get + * invalidated each time through the loop (cf. + * GetNonHistoricCatalogSnapshot). Consider a syscache or + * some other way to cache? + */ + indexoid = RelationGetRelid(leaf_part_rri->ri_IndexRelationDescs[listidx]); + ancestors = get_partition_ancestors(indexoid); + INJECTION_POINT("exec-init-partition-after-get-partition-ancestors", NULL); + + if (ancestors != NIL && + !list_member_oid(ancestors_seen, linitial_oid(ancestors))) { - if (list_member_oid(ancestors, lfirst_oid(lc2))) - arbiterIndexes = lappend_oid(arbiterIndexes, childIdx); + foreach_oid(parent_idx, rootResultRelInfo->ri_onConflictArbiterIndexes) + { + if (list_member_oid(ancestors, parent_idx)) + { + ancestors_seen = lappend_oid(ancestors_seen, linitial_oid(ancestors)); + arbiterIndexes = lappend_oid(arbiterIndexes, indexoid); + arbiters_listidxs = lappend_int(arbiters_listidxs, listidx); + break; + } + } } + else + unparented_idxs = lappend_int(unparented_idxs, listidx); + list_free(ancestors); } + + /* + * If we found any indexes with no ancestors, it's possible that + * some arbiter index is undergoing concurrent reindex. Match all + * unparented indexes against arbiters; add unparented matching + * ones as "additional arbiters". + * + * This is critical so that all concurrent transactions use the + * same set as arbiters during REINDEX CONCURRENTLY, to avoid + * spurious "duplicate key" errors. + */ + if (unparented_idxs && arbiterIndexes) + { + foreach_int(unparented_i, unparented_idxs) + { + Relation unparented_rel; + IndexInfo *unparented_ii; + + unparented_rel = leaf_part_rri->ri_IndexRelationDescs[unparented_i]; + unparented_ii = leaf_part_rri->ri_IndexRelationInfo[unparented_i]; + + Assert(!list_member_oid(arbiterIndexes, + unparented_rel->rd_index->indexrelid)); + + /* Ignore indexes not ready */ + if (!unparented_ii->ii_ReadyForInserts) + continue; + + foreach_int(arbiter_i, arbiters_listidxs) + { + Relation arbiter_rel; + IndexInfo *arbiter_ii; + + arbiter_rel = leaf_part_rri->ri_IndexRelationDescs[arbiter_i]; + arbiter_ii = leaf_part_rri->ri_IndexRelationInfo[arbiter_i]; + + /* + * If the non-ancestor index is compatible with the + * arbiter, use the non-ancestor as arbiter too. + */ + if (IsIndexCompatibleAsArbiter(arbiter_rel, + arbiter_ii, + unparented_rel, + unparented_ii)) + { + arbiterIndexes = lappend_oid(arbiterIndexes, + unparented_rel->rd_index->indexrelid); + additional_arbiters++; + break; + } + } + } + } + list_free(unparented_idxs); + list_free(arbiters_listidxs); + list_free(ancestors_seen); } /* - * If the resulting lists are of inequal length, something is wrong. - * (This shouldn't happen, since arbiter index selection should not - * pick up an invalid index.) + * We expect to find as many arbiter indexes on this partition as the + * root has, plus however many "additional arbiters" (to wit: those + * being concurrently rebuilt) we found. */ if (list_length(rootResultRelInfo->ri_onConflictArbiterIndexes) != - list_length(arbiterIndexes)) + list_length(arbiterIndexes) - additional_arbiters) elog(ERROR, "invalid arbiter index list"); leaf_part_rri->ri_onConflictArbiterIndexes = arbiterIndexes; /* - * In the DO UPDATE case, we have some more state to initialize. + * In the DO UPDATE and DO SELECT cases, we have some more state to + * initialize. */ - if (node->onConflictAction == ONCONFLICT_UPDATE) + if (node->onConflictAction == ONCONFLICT_UPDATE || + node->onConflictAction == ONCONFLICT_SELECT) { - OnConflictSetState *onconfl = makeNode(OnConflictSetState); + OnConflictActionState *onconfl = makeNode(OnConflictActionState); TupleConversionMap *map; map = ExecGetRootToChildMap(leaf_part_rri, estate); - Assert(node->onConflictSet != NIL); + Assert(node->onConflictSet != NIL || + node->onConflictAction == ONCONFLICT_SELECT); Assert(rootResultRelInfo->ri_onConflict != NULL); leaf_part_rri->ri_onConflict = onconfl; + /* Lock strength for DO SELECT [FOR UPDATE/SHARE] */ + onconfl->oc_LockStrength = + rootResultRelInfo->ri_onConflict->oc_LockStrength; + /* * Need a separate existing slot for each partition, as the * partition could be of a different AM, even if the tuple @@ -752,7 +917,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, /* * If the partition's tuple descriptor matches exactly the root * parent (the common case), we can re-use most of the parent's ON - * CONFLICT SET state, skipping a bunch of work. Otherwise, we + * CONFLICT action state, skipping a bunch of work. Otherwise, we * need to create state specific to this partition. */ if (map == NULL) @@ -760,7 +925,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, /* * It's safe to reuse these from the partition root, as we * only process one tuple at a time (therefore we won't - * overwrite needed data in slots), and the results of + * overwrite needed data in slots), and the results of any * projections are independent of the underlying storage. * Projections and where clauses themselves don't store state * / are independent of the underlying storage. @@ -774,66 +939,81 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, } else { - List *onconflset; - List *onconflcols; - /* - * Translate expressions in onConflictSet to account for - * different attribute numbers. For that, map partition - * varattnos twice: first to catch the EXCLUDED - * pseudo-relation (INNER_VAR), and second to handle the main - * target relation (firstVarno). + * For ON CONFLICT DO UPDATE, translate expressions in + * onConflictSet to account for different attribute numbers. + * For that, map partition varattnos twice: first to catch the + * EXCLUDED pseudo-relation (INNER_VAR), and second to handle + * the main target relation (firstVarno). */ - onconflset = copyObject(node->onConflictSet); - if (part_attmap == NULL) - part_attmap = - build_attrmap_by_name(RelationGetDescr(partrel), - RelationGetDescr(firstResultRel), - false); - onconflset = (List *) - map_variable_attnos((Node *) onconflset, - INNER_VAR, 0, - part_attmap, - RelationGetForm(partrel)->reltype, - &found_whole_row); - /* We ignore the value of found_whole_row. */ - onconflset = (List *) - map_variable_attnos((Node *) onconflset, - firstVarno, 0, - part_attmap, - RelationGetForm(partrel)->reltype, - &found_whole_row); - /* We ignore the value of found_whole_row. */ - - /* Finally, adjust the target colnos to match the partition. */ - onconflcols = adjust_partition_colnos(node->onConflictCols, - leaf_part_rri); - - /* create the tuple slot for the UPDATE SET projection */ - onconfl->oc_ProjSlot = - table_slot_create(partrel, - &mtstate->ps.state->es_tupleTable); + if (node->onConflictAction == ONCONFLICT_UPDATE) + { + List *onconflset; + List *onconflcols; + + onconflset = copyObject(node->onConflictSet); + if (part_attmap == NULL) + part_attmap = + build_attrmap_by_name(RelationGetDescr(partrel), + RelationGetDescr(firstResultRel), + false); + onconflset = (List *) + map_variable_attnos((Node *) onconflset, + INNER_VAR, 0, + part_attmap, + RelationGetForm(partrel)->reltype, + &found_whole_row); + /* We ignore the value of found_whole_row. */ + onconflset = (List *) + map_variable_attnos((Node *) onconflset, + firstVarno, 0, + part_attmap, + RelationGetForm(partrel)->reltype, + &found_whole_row); + /* We ignore the value of found_whole_row. */ - /* build UPDATE SET projection state */ - onconfl->oc_ProjInfo = - ExecBuildUpdateProjection(onconflset, - true, - onconflcols, - partrelDesc, - econtext, - onconfl->oc_ProjSlot, - &mtstate->ps); + /* + * Finally, adjust the target colnos to match the + * partition. + */ + onconflcols = adjust_partition_colnos(node->onConflictCols, + leaf_part_rri); + + /* create the tuple slot for the UPDATE SET projection */ + onconfl->oc_ProjSlot = + table_slot_create(partrel, + &mtstate->ps.state->es_tupleTable); + + /* build UPDATE SET projection state */ + onconfl->oc_ProjInfo = + ExecBuildUpdateProjection(onconflset, + true, + onconflcols, + partrelDesc, + econtext, + onconfl->oc_ProjSlot, + &mtstate->ps); + } /* - * If there is a WHERE clause, initialize state where it will - * be evaluated, mapping the attribute numbers appropriately. - * As with onConflictSet, we need to map partition varattnos - * to the partition's tupdesc. + * For both ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT, + * there may be a WHERE clause. If so, initialize state where + * it will be evaluated, mapping the attribute numbers + * appropriately. As with onConflictSet, we need to map + * partition varattnos twice, to catch both the EXCLUDED + * pseudo-relation (INNER_VAR), and the main target relation + * (firstVarno). */ if (node->onConflictWhere) { List *clause; + if (part_attmap == NULL) + part_attmap = + build_attrmap_by_name(RelationGetDescr(partrel), + RelationGetDescr(firstResultRel), + false); + clause = copyObject((List *) node->onConflictWhere); clause = (List *) map_variable_attnos((Node *) clause, @@ -850,7 +1030,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, &found_whole_row); /* We ignore the value of found_whole_row. */ onconfl->oc_WhereClause = - ExecInitQual((List *) clause, &mtstate->ps); + ExecInitQual(clause, &mtstate->ps); } } } @@ -1060,10 +1240,8 @@ ExecInitRoutingInfo(ModifyTableState *mtstate, if (proute->max_partitions == 0) { proute->max_partitions = 8; - proute->partitions = (ResultRelInfo **) - palloc(sizeof(ResultRelInfo *) * proute->max_partitions); - proute->is_borrowed_rel = (bool *) - palloc(sizeof(bool) * proute->max_partitions); + proute->partitions = palloc_array(ResultRelInfo *, proute->max_partitions); + proute->is_borrowed_rel = palloc_array(bool, proute->max_partitions); } else { @@ -1178,10 +1356,8 @@ ExecInitPartitionDispatchInfo(EState *estate, if (proute->max_dispatch == 0) { proute->max_dispatch = 4; - proute->partition_dispatch_info = (PartitionDispatch *) - palloc(sizeof(PartitionDispatch) * proute->max_dispatch); - proute->nonleaf_partitions = (ResultRelInfo **) - palloc(sizeof(ResultRelInfo *) * proute->max_dispatch); + proute->partition_dispatch_info = palloc_array(PartitionDispatch, proute->max_dispatch); + proute->nonleaf_partitions = palloc_array(ResultRelInfo *, proute->max_dispatch); } else { @@ -1391,7 +1567,7 @@ FormPartitionKeyDatum(PartitionDispatch pd, * found or -1 if none found. */ static int -get_partition_for_tuple(PartitionDispatch pd, Datum *values, bool *isnull) +get_partition_for_tuple(PartitionDispatch pd, const Datum *values, const bool *isnull) { int bound_offset = -1; int part_index = -1; @@ -1612,8 +1788,8 @@ get_partition_for_tuple(PartitionDispatch pd, Datum *values, bool *isnull) */ static char * ExecBuildSlotPartitionKeyDescription(Relation rel, - Datum *values, - bool *isnull, + const Datum *values, + const bool *isnull, int maxfieldlen) { StringInfoData buf; @@ -2073,7 +2249,7 @@ CreatePartitionPruneState(EState *estate, PartitionPruneInfo *pruneinfo, * arrays are in partition bounds order. */ pprune->nparts = partdesc->nparts; - pprune->subplan_map = palloc(sizeof(int) * partdesc->nparts); + pprune->subplan_map = palloc_array(int, partdesc->nparts); if (partdesc->nparts == pinfo->nparts && memcmp(partdesc->oids, pinfo->relid_map, @@ -2100,8 +2276,8 @@ CreatePartitionPruneState(EState *estate, PartitionPruneInfo *pruneinfo, * attached. Cope with that by creating a map that skips any * mismatches. */ - pprune->subpart_map = palloc(sizeof(int) * partdesc->nparts); - pprune->leafpart_rti_map = palloc(sizeof(int) * partdesc->nparts); + pprune->subpart_map = palloc_array(int, partdesc->nparts); + pprune->leafpart_rti_map = palloc_array(int, partdesc->nparts); for (pp_idx = 0; pp_idx < partdesc->nparts; pp_idx++) { @@ -2252,16 +2428,14 @@ InitPartitionPruneContext(PartitionPruneContext *context, context->partsupfunc = partkey->partsupfunc; /* We'll look up type-specific support functions as needed */ - context->stepcmpfuncs = (FmgrInfo *) - palloc0(sizeof(FmgrInfo) * n_steps * partnatts); + context->stepcmpfuncs = palloc0_array(FmgrInfo, n_steps * partnatts); context->ppccontext = CurrentMemoryContext; context->planstate = planstate; context->exprcontext = econtext; /* Initialize expression state for each expression we need */ - context->exprstates = (ExprState **) - palloc0(sizeof(ExprState *) * n_steps * partnatts); + context->exprstates = palloc0_array(ExprState *, n_steps * partnatts); foreach(lc, pruning_steps) { PartitionPruneStepOp *step = (PartitionPruneStepOp *) lfirst(lc); @@ -2362,7 +2536,7 @@ InitExecPartitionPruneContexts(PartitionPruneState *prunestate, * indexes to new ones. For convenience of initialization, we use * 1-based indexes in this array and leave pruned items as 0. */ - new_subplan_indexes = (int *) palloc0(sizeof(int) * n_total_subplans); + new_subplan_indexes = palloc0_array(int, n_total_subplans); newidx = 1; i = -1; while ((i = bms_next_member(initially_valid_subplans, i)) >= 0) diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c index f5f9cfbeeadad..7c4c66e323fed 100644 --- a/src/backend/executor/execProcnode.c +++ b/src/backend/executor/execProcnode.c @@ -7,7 +7,7 @@ * ExecProcNode, or ExecEndNode on its subnodes and do the appropriate * processing. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -73,6 +73,7 @@ #include "postgres.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeAgg.h" #include "executor/nodeAppend.h" #include "executor/nodeBitmapAnd.h" @@ -120,7 +121,6 @@ #include "nodes/nodeFuncs.h" static TupleTableSlot *ExecProcNodeFirst(PlanState *node); -static TupleTableSlot *ExecProcNodeInstr(PlanState *node); static bool ExecShutdownNode_walker(PlanState *node, void *context); @@ -413,8 +413,8 @@ ExecInitNode(Plan *node, EState *estate, int eflags) /* Set up instrumentation for this node if requested */ if (estate->es_instrument) - result->instrument = InstrAlloc(1, estate->es_instrument, - result->async_capable); + result->instrument = InstrAllocNode(estate->es_instrument, + result->async_capable); return result; } @@ -470,25 +470,6 @@ ExecProcNodeFirst(PlanState *node) } -/* - * ExecProcNode wrapper that performs instrumentation calls. By keeping - * this a separate function, we avoid overhead in the normal case where - * no instrumentation is wanted. - */ -static TupleTableSlot * -ExecProcNodeInstr(PlanState *node) -{ - TupleTableSlot *result; - - InstrStartNode(node->instrument); - - result = node->ExecProcNodeReal(node); - - InstrStopNode(node->instrument, TupIsNull(result) ? 0.0 : 1.0); - - return result; -} - /* ---------------------------------------------------------------- * MultiExecProcNode diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 53ddd25c42db9..b2ca5cbf11761 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -3,7 +3,7 @@ * execReplication.c * miscellaneous executor routines for logical replication * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -14,12 +14,15 @@ #include "postgres.h" +#include "access/amapi.h" +#include "access/commit_ts.h" #include "access/genam.h" #include "access/gist.h" #include "access/relscan.h" #include "access/tableam.h" #include "access/transam.h" #include "access/xact.h" +#include "access/heapam.h" #include "catalog/pg_am_d.h" #include "commands/trigger.h" #include "executor/executor.h" @@ -36,7 +39,7 @@ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2, - TypeCacheEntry **eq); + TypeCacheEntry **eq, Bitmapset *columns); /* * Setup a ScanKey for a search in the relation 'rel' for a tuple 'key' that @@ -44,8 +47,8 @@ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2, * * Returns how many columns to use for the index scan. * - * This is not generic routine, idxrel must be PK, RI, or an index that can be - * used for REPLICA IDENTITY FULL table. See FindUsableIndexForReplicaIdentityFull() + * This is not a generic routine, idxrel must be PK, RI, or an index that can be + * used for a REPLICA IDENTITY FULL table. See FindUsableIndexForReplicaIdentityFull() * for details. * * By definition, replication identity of a rel meets all limitations associated @@ -202,7 +205,8 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid, skey_attoff = build_replindex_scan_key(skey, rel, idxrel, searchslot); /* Start an index scan. */ - scan = index_beginscan(rel, idxrel, &snap, NULL, skey_attoff, 0); + scan = index_beginscan(rel, idxrel, + &snap, NULL, skey_attoff, 0, SO_NONE); retry: found = false; @@ -219,9 +223,9 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid, if (!isIdxSafeToSkipDuplicates) { if (eq == NULL) - eq = palloc0(sizeof(*eq) * outslot->tts_tupleDescriptor->natts); + eq = palloc0_array(TypeCacheEntry *, outslot->tts_tupleDescriptor->natts); - if (!tuples_equal(outslot, searchslot, eq)) + if (!tuples_equal(outslot, searchslot, eq, NULL)) continue; } @@ -277,10 +281,13 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid, /* * Compare the tuples in the slots by checking if they have equal values. + * + * If 'columns' is not null, only the columns specified within it will be + * considered for the equality check, ignoring all other columns. */ static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2, - TypeCacheEntry **eq) + TypeCacheEntry **eq, Bitmapset *columns) { int attrnum; @@ -305,6 +312,14 @@ tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2, if (att->attisdropped || att->attgenerated) continue; + /* + * Ignore columns that are not listed for checking. + */ + if (columns && + !bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber, + columns)) + continue; + /* * If one value is NULL and other is not, then they are certainly not * equal @@ -365,11 +380,12 @@ RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode, Assert(equalTupleDescs(desc, outslot->tts_tupleDescriptor)); - eq = palloc0(sizeof(*eq) * outslot->tts_tupleDescriptor->natts); + eq = palloc0_array(TypeCacheEntry *, outslot->tts_tupleDescriptor->natts); /* Start a heap scan. */ InitDirtySnapshot(snap); - scan = table_beginscan(rel, &snap, 0, NULL); + scan = table_beginscan(rel, &snap, 0, NULL, + SO_NONE); scanslot = table_slot_create(rel, NULL); retry: @@ -380,7 +396,7 @@ RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode, /* Try to find the tuple */ while (table_scan_getnextslot(scan, ForwardScanDirection, scanslot)) { - if (!tuples_equal(scanslot, searchslot, eq)) + if (!tuples_equal(scanslot, searchslot, eq, NULL)) continue; found = true; @@ -455,6 +471,238 @@ BuildConflictIndexInfo(ResultRelInfo *resultRelInfo, Oid conflictindex) } } +/* + * If the tuple is recently dead and was deleted by a transaction with a newer + * commit timestamp than previously recorded, update the associated transaction + * ID, commit time, and origin. This helps ensure that conflict detection uses + * the most recent and relevant deletion metadata. + */ +static void +update_most_recent_deletion_info(TupleTableSlot *scanslot, + TransactionId oldestxmin, + TransactionId *delete_xid, + TimestampTz *delete_time, + ReplOriginId *delete_origin) +{ + BufferHeapTupleTableSlot *hslot; + HeapTuple tuple; + Buffer buf; + bool recently_dead = false; + TransactionId xmax; + TimestampTz localts; + ReplOriginId localorigin; + + hslot = (BufferHeapTupleTableSlot *) scanslot; + + tuple = ExecFetchSlotHeapTuple(scanslot, false, NULL); + buf = hslot->buffer; + + LockBuffer(buf, BUFFER_LOCK_SHARE); + + /* + * We do not consider HEAPTUPLE_DEAD status because it indicates either + * tuples whose inserting transaction was aborted (meaning there is no + * commit timestamp or origin), or tuples deleted by a transaction older + * than oldestxmin, making it safe to ignore them during conflict + * detection (See comments atop worker.c for details). + */ + if (HeapTupleSatisfiesVacuum(tuple, oldestxmin, buf) == HEAPTUPLE_RECENTLY_DEAD) + recently_dead = true; + + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + + if (!recently_dead) + return; + + xmax = HeapTupleHeaderGetUpdateXid(tuple->t_data); + if (!TransactionIdIsValid(xmax)) + return; + + /* Select the dead tuple with the most recent commit timestamp */ + if (TransactionIdGetCommitTsData(xmax, &localts, &localorigin) && + TimestampDifferenceExceeds(*delete_time, localts, 0)) + { + *delete_xid = xmax; + *delete_time = localts; + *delete_origin = localorigin; + } +} + +/* + * Searches the relation 'rel' for the most recently deleted tuple that matches + * the values in 'searchslot' and is not yet removable by VACUUM. The function + * returns the transaction ID, origin, and commit timestamp of the transaction + * that deleted this tuple. + * + * 'oldestxmin' acts as a cutoff transaction ID. Tuples deleted by transactions + * with IDs >= 'oldestxmin' are considered recently dead and are eligible for + * conflict detection. + * + * Instead of stopping at the first match, we scan all matching dead tuples to + * identify most recent deletion. This is crucial because only the latest + * deletion is relevant for resolving conflicts. + * + * For example, consider a scenario on the subscriber where a row is deleted, + * re-inserted, and then deleted again only on the subscriber: + * + * - (pk, 1) - deleted at 9:00, + * - (pk, 1) - deleted at 9:02, + * + * Now, a remote update arrives: (pk, 1) -> (pk, 2), timestamped at 9:01. + * + * If we mistakenly return the older deletion (9:00), the system may wrongly + * apply the remote update using a last-update-wins strategy. Instead, we must + * recognize the more recent deletion at 9:02 and skip the update. See + * comments atop worker.c for details. Note, as of now, conflict resolution + * is not implemented. Consequently, the system may incorrectly report the + * older tuple as the conflicted one, leading to misleading results. + * + * The commit timestamp of the deleting transaction is used to determine which + * tuple was deleted most recently. + */ +bool +RelationFindDeletedTupleInfoSeq(Relation rel, TupleTableSlot *searchslot, + TransactionId oldestxmin, + TransactionId *delete_xid, + ReplOriginId *delete_origin, + TimestampTz *delete_time) +{ + TupleTableSlot *scanslot; + TableScanDesc scan; + TypeCacheEntry **eq; + Bitmapset *indexbitmap; + TupleDesc desc PG_USED_FOR_ASSERTS_ONLY = RelationGetDescr(rel); + + Assert(equalTupleDescs(desc, searchslot->tts_tupleDescriptor)); + + *delete_xid = InvalidTransactionId; + *delete_origin = InvalidReplOriginId; + *delete_time = 0; + + /* + * If the relation has a replica identity key or a primary key that is + * unusable for locating deleted tuples (see + * IsIndexUsableForFindingDeletedTuple), a full table scan becomes + * necessary. In such cases, comparing the entire tuple is not required, + * since the remote tuple might not include all column values. Instead, + * the indexed columns alone are sufficient to identify the target tuple + * (see logicalrep_rel_mark_updatable). + */ + indexbitmap = RelationGetIndexAttrBitmap(rel, + INDEX_ATTR_BITMAP_IDENTITY_KEY); + + /* fallback to PK if no replica identity */ + if (!indexbitmap) + indexbitmap = RelationGetIndexAttrBitmap(rel, + INDEX_ATTR_BITMAP_PRIMARY_KEY); + + eq = palloc0_array(TypeCacheEntry *, searchslot->tts_tupleDescriptor->natts); + + /* + * Start a heap scan using SnapshotAny to identify dead tuples that are + * not visible under a standard MVCC snapshot. Tuples from transactions + * not yet committed or those just committed prior to the scan are + * excluded in update_most_recent_deletion_info(). + */ + scan = table_beginscan(rel, SnapshotAny, 0, NULL, + SO_NONE); + scanslot = table_slot_create(rel, NULL); + + table_rescan(scan, NULL); + + /* Try to find the tuple */ + while (table_scan_getnextslot(scan, ForwardScanDirection, scanslot)) + { + if (!tuples_equal(scanslot, searchslot, eq, indexbitmap)) + continue; + + update_most_recent_deletion_info(scanslot, oldestxmin, delete_xid, + delete_time, delete_origin); + } + + table_endscan(scan); + ExecDropSingleTupleTableSlot(scanslot); + + return *delete_time != 0; +} + +/* + * Similar to RelationFindDeletedTupleInfoSeq() but using index scan to locate + * the deleted tuple. + */ +bool +RelationFindDeletedTupleInfoByIndex(Relation rel, Oid idxoid, + TupleTableSlot *searchslot, + TransactionId oldestxmin, + TransactionId *delete_xid, + ReplOriginId *delete_origin, + TimestampTz *delete_time) +{ + Relation idxrel; + ScanKeyData skey[INDEX_MAX_KEYS]; + int skey_attoff; + IndexScanDesc scan; + TupleTableSlot *scanslot; + TypeCacheEntry **eq = NULL; + bool isIdxSafeToSkipDuplicates; + TupleDesc desc PG_USED_FOR_ASSERTS_ONLY = RelationGetDescr(rel); + + Assert(equalTupleDescs(desc, searchslot->tts_tupleDescriptor)); + Assert(OidIsValid(idxoid)); + + *delete_xid = InvalidTransactionId; + *delete_time = 0; + *delete_origin = InvalidReplOriginId; + + isIdxSafeToSkipDuplicates = (GetRelationIdentityOrPK(rel) == idxoid); + + scanslot = table_slot_create(rel, NULL); + + idxrel = index_open(idxoid, RowExclusiveLock); + + /* Build scan key. */ + skey_attoff = build_replindex_scan_key(skey, rel, idxrel, searchslot); + + /* + * Start an index scan using SnapshotAny to identify dead tuples that are + * not visible under a standard MVCC snapshot. Tuples from transactions + * not yet committed or those just committed prior to the scan are + * excluded in update_most_recent_deletion_info(). + */ + scan = index_beginscan(rel, idxrel, + SnapshotAny, NULL, skey_attoff, 0, SO_NONE); + + index_rescan(scan, skey, skey_attoff, NULL, 0); + + /* Try to find the tuple */ + while (index_getnext_slot(scan, ForwardScanDirection, scanslot)) + { + /* + * Avoid expensive equality check if the index is primary key or + * replica identity index. + */ + if (!isIdxSafeToSkipDuplicates) + { + if (eq == NULL) + eq = palloc0_array(TypeCacheEntry *, scanslot->tts_tupleDescriptor->natts); + + if (!tuples_equal(scanslot, searchslot, eq, NULL)) + continue; + } + + update_most_recent_deletion_info(scanslot, oldestxmin, delete_xid, + delete_time, delete_origin); + } + + index_endscan(scan); + + index_close(idxrel, NoLock); + + ExecDropSingleTupleTableSlot(scanslot); + + return *delete_time != 0; +} + /* * Find the tuple that violates the passed unique index (conflictindex). * @@ -602,17 +850,24 @@ ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo, conflictindexes = resultRelInfo->ri_onConflictArbiterIndexes; if (resultRelInfo->ri_NumIndices > 0) + { + uint32 flags; + + if (conflictindexes != NIL) + flags = EIIT_NO_DUPE_ERROR; + else + flags = 0; recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - slot, estate, false, - conflictindexes ? true : false, - &conflict, - conflictindexes, false); + estate, flags, + slot, conflictindexes, + &conflict); + } /* - * Checks the conflict indexes to fetch the conflicting local tuple - * and reports the conflict. We perform this check here, instead of + * Checks the conflict indexes to fetch the conflicting local row and + * reports the conflict. We perform this check here, instead of * performing an additional index scan before the actual insertion and - * reporting the conflict if any conflicting tuples are found. This is + * reporting the conflict if any conflicting rows are found. This is * to avoid the overhead of executing the extra scan for each INSERT * operation, even when no conflict arises, which could introduce * significant overhead to replication, particularly in cases where @@ -670,7 +925,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo, resultRelInfo->ri_TrigDesc->trig_update_before_row) { if (!ExecBRUpdateTriggers(estate, epqstate, resultRelInfo, - tid, NULL, slot, NULL, NULL)) + tid, NULL, slot, NULL, NULL, false)) skip_tuple = true; /* "do nothing" */ } @@ -699,11 +954,18 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo, conflictindexes = resultRelInfo->ri_onConflictArbiterIndexes; if (resultRelInfo->ri_NumIndices > 0 && (update_indexes != TU_None)) + { + uint32 flags = EIIT_IS_UPDATE; + + if (conflictindexes != NIL) + flags |= EIIT_NO_DUPE_ERROR; + if (update_indexes == TU_Summarizing) + flags |= EIIT_ONLY_SUMMARIZING; recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - slot, estate, true, - conflictindexes ? true : false, - &conflict, conflictindexes, - (update_indexes == TU_Summarizing)); + estate, flags, + slot, conflictindexes, + &conflict); + } /* * Refer to the comments above the call to CheckAndReportConflict() in @@ -746,7 +1008,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo, resultRelInfo->ri_TrigDesc->trig_delete_before_row) { skip_tuple = !ExecBRDeleteTriggers(estate, epqstate, resultRelInfo, - tid, NULL, NULL, NULL, NULL); + tid, NULL, NULL, NULL, NULL, false); } if (!skip_tuple) @@ -869,18 +1131,36 @@ CheckCmdReplicaIdentity(Relation rel, CmdType cmd) /* - * Check if we support writing into specific relkind. + * Check if we support writing into specific relkind of local relation and check + * if it aligns with the relkind of the relation on the publisher. * * The nspname and relname are only needed for error reporting. */ void -CheckSubscriptionRelkind(char relkind, const char *nspname, - const char *relname) +CheckSubscriptionRelkind(char localrelkind, char remoterelkind, + const char *nspname, const char *relname) { - if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE) + if (localrelkind != RELKIND_RELATION && + localrelkind != RELKIND_PARTITIONED_TABLE && + localrelkind != RELKIND_SEQUENCE) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot use relation \"%s.%s\" as logical replication target", nspname, relname), - errdetail_relkind_not_supported(relkind))); + errdetail_relkind_not_supported(localrelkind))); + + /* + * Allow RELKIND_RELATION and RELKIND_PARTITIONED_TABLE to be treated + * interchangeably, but ensure that sequences (RELKIND_SEQUENCE) match + * exactly on both publisher and subscriber. + */ + if ((localrelkind == RELKIND_SEQUENCE && remoterelkind != RELKIND_SEQUENCE) || + (localrelkind != RELKIND_SEQUENCE && remoterelkind == RELKIND_SEQUENCE)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + /* translator: 3rd and 4th %s are "sequence" or "table" */ + errmsg("relation \"%s.%s\" type mismatch: source \"%s\", target \"%s\"", + nspname, relname, + remoterelkind == RELKIND_SEQUENCE ? "sequence" : "table", + localrelkind == RELKIND_SEQUENCE ? "sequence" : "table")); } diff --git a/src/backend/executor/execSRF.c b/src/backend/executor/execSRF.c index a03fe780a02c9..8aedcc6a4591b 100644 --- a/src/backend/executor/execSRF.c +++ b/src/backend/executor/execSRF.c @@ -7,7 +7,7 @@ * common code for calling set-returning functions according to the * ReturnSetInfo API. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -30,6 +30,7 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/tuplestore.h" #include "utils/typcache.h" @@ -272,6 +273,7 @@ ExecMakeTableFunctionResult(SetExprState *setexpr, funcrettype, -1, 0); + TupleDescFinalize(tupdesc); rsinfo.setDesc = tupdesc; } MemoryContextSwitchTo(oldcontext); @@ -776,6 +778,7 @@ init_sexpr(Oid foid, Oid input_collation, Expr *node, funcrettype, -1, 0); + TupleDescFinalize(tupdesc); sexpr->funcResultDesc = tupdesc; sexpr->funcReturnsTuple = false; } diff --git a/src/backend/executor/execScan.c b/src/backend/executor/execScan.c index 90726949a8708..9f68be17b998e 100644 --- a/src/backend/executor/execScan.c +++ b/src/backend/executor/execScan.c @@ -7,7 +7,7 @@ * stuff - checking the qualification and projecting the tuple * appropriately. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -134,7 +134,7 @@ ExecScanReScan(ScanState *node) /* * If an FDW or custom scan provider has replaced the join with a - * scan, there are multiple RTIs; reset the epqScanDone flag for + * scan, there are multiple RTIs; reset the relsubs_done flag for * all of them. */ if (IsA(node->ps.plan, ForeignScan)) diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c index 8e02d68824fad..b0a0028b165bd 100644 --- a/src/backend/executor/execTuples.c +++ b/src/backend/executor/execTuples.c @@ -46,7 +46,7 @@ * to avoid physically constructing projection tuples in many cases. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -73,7 +73,7 @@ static TupleDesc ExecTypeFromTLInternal(List *targetList, bool skipjunk); static pg_attribute_always_inline void slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, - int natts); + int reqnatts, bool support_cstring); static inline void tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, Buffer buffer, @@ -349,7 +349,7 @@ tts_heap_getsomeattrs(TupleTableSlot *slot, int natts) Assert(!TTS_EMPTY(slot)); - slot_deform_heap_tuple(slot, hslot->tuple, &hslot->off, natts); + slot_deform_heap_tuple(slot, hslot->tuple, &hslot->off, natts, false); } static Datum @@ -512,7 +512,7 @@ tts_minimal_init(TupleTableSlot *slot) /* * Initialize the heap tuple pointer to access attributes of the minimal - * tuple contained in the slot as if its a heap tuple. + * tuple contained in the slot as if it's a heap tuple. */ mslot->tuple = &mslot->minhdr; } @@ -547,7 +547,7 @@ tts_minimal_getsomeattrs(TupleTableSlot *slot, int natts) Assert(!TTS_EMPTY(slot)); - slot_deform_heap_tuple(slot, mslot->tuple, &mslot->off, natts); + slot_deform_heap_tuple(slot, mslot->tuple, &mslot->off, natts, true); } /* @@ -754,7 +754,7 @@ tts_buffer_heap_getsomeattrs(TupleTableSlot *slot, int natts) Assert(!TTS_EMPTY(slot)); - slot_deform_heap_tuple(slot, bslot->base.tuple, &bslot->base.off, natts); + slot_deform_heap_tuple(slot, bslot->base.tuple, &bslot->base.off, natts, false); } static Datum @@ -993,218 +993,267 @@ tts_buffer_heap_store_tuple(TupleTableSlot *slot, HeapTuple tuple, } /* - * slot_deform_heap_tuple_internal - * An always inline helper function for use in slot_deform_heap_tuple to - * allow the compiler to emit specialized versions of this function for - * various combinations of "slow" and "hasnulls". For example, if a - * given tuple has no nulls, then we needn't check "hasnulls" for every - * attribute that we're deforming. The caller can just call this - * function with hasnulls set to constant-false and have the compiler - * remove the constant-false branches and emit more optimal code. - * - * Returns the next attnum to deform, which can be equal to natts when the - * function manages to deform all requested attributes. *offp is an input and - * output parameter which is the byte offset within the tuple to start deforming - * from which, on return, gets set to the offset where the next attribute - * should be deformed from. *slowp is set to true when subsequent deforming - * of this tuple must use a version of this function with "slow" passed as - * true. - * - * Callers cannot assume when we return "attnum" (i.e. all requested - * attributes have been deformed) that slow mode isn't required for any - * additional deforming as the final attribute may have caused a switch to - * slow mode. + * slot_deform_heap_tuple + * Given a TupleTableSlot, extract data from the slot's physical tuple + * into its Datum/isnull arrays. Data is extracted up through the + * reqnatts'th column. If there are insufficient attributes in the given + * tuple, then slot_getmissingattrs() is called to populate the + * remainder. If reqnatts is above the number of attributes in the + * slot's TupleDesc, an error is raised. + * + * This is essentially an incremental version of heap_deform_tuple: + * on each call we extract attributes up to the one needed, without + * re-computing information about previously extracted attributes. + * slot->tts_nvalid is the number of attributes already extracted. + * + * This is marked as always inline, so the different offp for different types + * of slots gets optimized away. + * + * support_cstring should be passed as a const to allow the compiler only + * emit code during inlining for cstring deforming when it's required. + * cstrings can exist in MinimalTuples, but not in HeapTuples. */ -static pg_attribute_always_inline int -slot_deform_heap_tuple_internal(TupleTableSlot *slot, HeapTuple tuple, - int attnum, int natts, bool slow, - bool hasnulls, uint32 *offp, bool *slowp) +static pg_attribute_always_inline void +slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, + int reqnatts, bool support_cstring) { + CompactAttribute *cattrs; + CompactAttribute *cattr; TupleDesc tupleDesc = slot->tts_tupleDescriptor; - Datum *values = slot->tts_values; - bool *isnull = slot->tts_isnull; HeapTupleHeader tup = tuple->t_data; + size_t attnum; + int firstNonCacheOffsetAttr; + int firstNonGuaranteedAttr; + int firstNullAttr; + int natts; + Datum *values; + bool *isnull; char *tp; /* ptr to tuple data */ - bits8 *bp = tup->t_bits; /* ptr to null bitmap in tuple */ - bool slownext = false; + uint32 off; /* offset in tuple data */ - tp = (char *) tup + tup->t_hoff; + /* Did someone forget to call TupleDescFinalize()? */ + Assert(tupleDesc->firstNonCachedOffsetAttr >= 0); - for (; attnum < natts; attnum++) + isnull = slot->tts_isnull; + + /* + * Some callers may form and deform tuples prior to NOT NULL constraints + * being checked. Here we'd like to optimize the case where we only need + * to fetch attributes before or up to the point where the attribute is + * guaranteed to exist in the tuple. We rely on the slot flag being set + * correctly to only enable this optimization when it's valid to do so. + * This optimization allows us to save fetching the number of attributes + * from the tuple and saves the additional cost of handling non-byval + * attrs. + */ + firstNonGuaranteedAttr = Min(reqnatts, slot->tts_first_nonguaranteed); + + firstNonCacheOffsetAttr = tupleDesc->firstNonCachedOffsetAttr; + + if (HeapTupleHasNulls(tuple)) { - CompactAttribute *thisatt = TupleDescCompactAttr(tupleDesc, attnum); + natts = HeapTupleHeaderGetNatts(tup); + tp = (char *) tup + MAXALIGN(offsetof(HeapTupleHeaderData, t_bits) + + BITMAPLEN(natts)); - if (hasnulls && att_isnull(attnum, bp)) + natts = Min(natts, reqnatts); + if (natts > firstNonGuaranteedAttr) { - values[attnum] = (Datum) 0; - isnull[attnum] = true; - if (!slow) - { - *slowp = true; - return attnum + 1; - } - else - continue; - } + uint8 *bp = tup->t_bits; - isnull[attnum] = false; + /* Find the first NULL attr */ + firstNullAttr = first_null_attr(bp, natts); - /* calculate the offset of this attribute */ - if (!slow && thisatt->attcacheoff >= 0) - *offp = thisatt->attcacheoff; - else if (thisatt->attlen == -1) - { /* - * We can only cache the offset for a varlena attribute if the - * offset is already suitably aligned, so that there would be no - * pad bytes in any case: then the offset will be valid for either - * an aligned or unaligned value. + * And populate the isnull array for all attributes being fetched + * from the tuple. */ - if (!slow && *offp == att_nominal_alignby(*offp, thisatt->attalignby)) - thisatt->attcacheoff = *offp; - else - { - *offp = att_pointer_alignby(*offp, - thisatt->attalignby, - -1, - tp + *offp); - - if (!slow) - slownext = true; - } + populate_isnull_array(bp, natts, isnull); } else { - /* not varlena, so safe to use att_nominal_alignby */ - *offp = att_nominal_alignby(*offp, thisatt->attalignby); - - if (!slow) - thisatt->attcacheoff = *offp; + /* Otherwise all required columns are guaranteed to exist */ + firstNullAttr = natts; } + } + else + { + tp = (char *) tup + MAXALIGN(offsetof(HeapTupleHeaderData, t_bits)); - values[attnum] = fetchatt(thisatt, tp + *offp); - - *offp = att_addlength_pointer(*offp, thisatt->attlen, tp + *offp); - - /* check if we need to switch to slow mode */ - if (!slow) + /* + * We only need to look at the tuple's natts if we need more than the + * guaranteed number of columns + */ + if (reqnatts > firstNonGuaranteedAttr) + natts = Min(HeapTupleHeaderGetNatts(tup), reqnatts); + else { - /* - * We're unable to deform any further if the above code set - * 'slownext', or if this isn't a fixed-width attribute. - */ - if (slownext || thisatt->attlen <= 0) - { - *slowp = true; - return attnum + 1; - } + /* No need to access the number of attributes in the tuple */ + natts = reqnatts; } - } - - return natts; -} -/* - * slot_deform_heap_tuple - * Given a TupleTableSlot, extract data from the slot's physical tuple - * into its Datum/isnull arrays. Data is extracted up through the - * natts'th column (caller must ensure this is a legal column number). - * - * This is essentially an incremental version of heap_deform_tuple: - * on each call we extract attributes up to the one needed, without - * re-computing information about previously extracted attributes. - * slot->tts_nvalid is the number of attributes already extracted. - * - * This is marked as always inline, so the different offp for different types - * of slots gets optimized away. - */ -static pg_attribute_always_inline void -slot_deform_heap_tuple(TupleTableSlot *slot, HeapTuple tuple, uint32 *offp, - int natts) -{ - bool hasnulls = HeapTupleHasNulls(tuple); - int attnum; - uint32 off; /* offset in tuple data */ - bool slow; /* can we use/set attcacheoff? */ + /* All attrs can be fetched without checking for NULLs */ + firstNullAttr = natts; + } - /* We can only fetch as many attributes as the tuple has. */ - natts = Min(HeapTupleHeaderGetNatts(tuple->t_data), natts); + attnum = slot->tts_nvalid; + values = slot->tts_values; + slot->tts_nvalid = reqnatts; /* - * Check whether the first call for this tuple, and initialize or restore - * loop state. + * We store the tupleDesc's CompactAttribute array in 'cattrs' as gcc + * seems to be unwilling to optimize accessing the CompactAttribute + * element efficiently when accessing it via TupleDescCompactAttr(). */ - attnum = slot->tts_nvalid; - if (attnum == 0) + cattrs = tupleDesc->compact_attrs; + + /* Ensure we calculated tp correctly */ + Assert(tp == (char *) tup + tup->t_hoff); + + if (attnum < firstNonGuaranteedAttr) { - /* Start from the first attribute */ - off = 0; - slow = false; + int attlen; + + do + { + isnull[attnum] = false; + cattr = &cattrs[attnum]; + attlen = cattr->attlen; + + /* We don't expect any non-byval types */ + pg_assume(attlen > 0); + Assert(cattr->attbyval == true); + + off = cattr->attcacheoff; + values[attnum] = fetch_att_noerr(tp + off, true, attlen); + attnum++; + } while (attnum < firstNonGuaranteedAttr); + + off += attlen; + + if (attnum == reqnatts) + goto done; } else { - /* Restore state from previous execution */ + /* + * We may be incrementally deforming the tuple, so set 'off' to the + * previously cached value. This may be 0, if the slot has just + * received a new tuple. + */ off = *offp; - slow = TTS_SLOW(slot); + + /* We expect *offp to be set to 0 when attnum == 0 */ + Assert(off == 0 || attnum > 0); } + /* We can use attcacheoff up until the first NULL */ + firstNonCacheOffsetAttr = Min(firstNonCacheOffsetAttr, firstNullAttr); + /* - * If 'slow' isn't set, try deforming using deforming code that does not - * contain any of the extra checks required for non-fixed offset - * deforming. During deforming, if or when we find a NULL or a variable - * length attribute, we'll switch to a deforming method which includes the - * extra code required for non-fixed offset deforming, a.k.a slow mode. - * Because this is performance critical, we inline - * slot_deform_heap_tuple_internal passing the 'slow' and 'hasnull' - * parameters as constants to allow the compiler to emit specialized code - * with the known-const false comparisons and subsequent branches removed. + * Handle the portion of the tuple that we have cached the offset for up + * to the first NULL attribute. The offset is effectively fixed for + * these, so we can use the CompactAttribute's attcacheoff. */ - if (!slow) + if (attnum < firstNonCacheOffsetAttr) { - /* Tuple without any NULLs? We can skip doing any NULL checking */ - if (!hasnulls) - attnum = slot_deform_heap_tuple_internal(slot, - tuple, - attnum, - natts, - false, /* slow */ - false, /* hasnulls */ - &off, - &slow); - else - attnum = slot_deform_heap_tuple_internal(slot, - tuple, - attnum, - natts, - false, /* slow */ - true, /* hasnulls */ - &off, - &slow); + int attlen; + + do + { + isnull[attnum] = false; + cattr = &cattrs[attnum]; + attlen = cattr->attlen; + off = cattr->attcacheoff; + values[attnum] = fetch_att_noerr(tp + off, + cattr->attbyval, + attlen); + attnum++; + } while (attnum < firstNonCacheOffsetAttr); + + /* + * Point the offset after the end of the last attribute with a cached + * offset. We expect the final cached offset attribute to have a + * fixed width, so just add the attlen to the attcacheoff + */ + Assert(attlen > 0); + off += attlen; } - /* If there's still work to do then we must be in slow mode */ - if (attnum < natts) + /* + * Handle any portion of the tuple that doesn't have a fixed offset up + * until the first NULL attribute. This loop only differs from the one + * after it by the NULL checks. + */ + for (; attnum < firstNullAttr; attnum++) { - /* XXX is it worth adding a separate call when hasnulls is false? */ - attnum = slot_deform_heap_tuple_internal(slot, - tuple, - attnum, - natts, - true, /* slow */ - hasnulls, - &off, - &slow); + int attlen; + + isnull[attnum] = false; + cattr = &cattrs[attnum]; + attlen = cattr->attlen; + + /* + * Only emit the cstring-related code in align_fetch_then_add() when + * cstring support is needed. We assume support_cstring will be + * passed as a const to allow the compiler to eliminate this branch. + */ + if (!support_cstring) + pg_assume(attlen > 0 || attlen == -1); + + /* align 'off', fetch the datum, and increment off beyond the datum */ + values[attnum] = align_fetch_then_add(tp, + &off, + cattr->attbyval, + attlen, + cattr->attalignby); } /* - * Save state for next execution + * Now handle any remaining attributes in the tuple up to the requested + * attnum. This time, include NULL checks as we're now at the first NULL + * attribute. */ - slot->tts_nvalid = attnum; + for (; attnum < natts; attnum++) + { + int attlen; + + if (isnull[attnum]) + { + values[attnum] = (Datum) 0; + continue; + } + + cattr = &cattrs[attnum]; + attlen = cattr->attlen; + + /* As above, only emit cstring code when needed. */ + if (!support_cstring) + pg_assume(attlen > 0 || attlen == -1); + + /* align 'off', fetch the datum, and increment off beyond the datum */ + values[attnum] = align_fetch_then_add(tp, + &off, + cattr->attbyval, + attlen, + cattr->attalignby); + } + + /* Fetch any missing attrs and raise an error if reqnatts is invalid */ + if (unlikely(attnum < reqnatts)) + { + /* + * Cache the offset before calling the function to allow the compiler + * to implement a tail-call optimization + */ + *offp = off; + slot_getmissingattrs(slot, attnum, reqnatts); + return; + } +done: + + /* Save current offset for next execution */ *offp = off; - if (slow) - slot->tts_flags |= TTS_FLAG_SLOW; - else - slot->tts_flags &= ~TTS_FLAG_SLOW; } const TupleTableSlotOps TTSOpsVirtual = { @@ -1294,12 +1343,13 @@ const TupleTableSlotOps TTSOpsBufferHeapTuple = { * Basic routine to make an empty TupleTableSlot of given * TupleTableSlotType. If tupleDesc is specified the slot's descriptor is * fixed for its lifetime, gaining some efficiency. If that's - * undesirable, pass NULL. + * undesirable, pass NULL. 'flags' allows any of non-TTS_FLAGS_TRANSIENT + * flags to be set in tts_flags. * -------------------------------- */ TupleTableSlot * MakeTupleTableSlot(TupleDesc tupleDesc, - const TupleTableSlotOps *tts_ops) + const TupleTableSlotOps *tts_ops, uint16 flags) { Size basesz, allocsz; @@ -1307,14 +1357,21 @@ MakeTupleTableSlot(TupleDesc tupleDesc, basesz = tts_ops->base_slot_size; + /* Ensure callers don't have any way to set transient flags permanently */ + flags &= ~TTS_FLAGS_TRANSIENT; + /* * When a fixed descriptor is specified, we can reduce overhead by * allocating the entire slot in one go. + * + * We round the size of tts_isnull up to the next highest multiple of 8. + * This is needed as populate_isnull_array() operates on 8 elements at a + * time when converting a tuple's NULL bitmap into a boolean array. */ if (tupleDesc) allocsz = MAXALIGN(basesz) + MAXALIGN(tupleDesc->natts * sizeof(Datum)) + - MAXALIGN(tupleDesc->natts * sizeof(bool)); + TYPEALIGN(8, tupleDesc->natts * sizeof(bool)); else allocsz = basesz; @@ -1322,7 +1379,7 @@ MakeTupleTableSlot(TupleDesc tupleDesc, /* const for optimization purposes, OK to modify at allocation time */ *((const TupleTableSlotOps **) &slot->tts_ops) = tts_ops; slot->type = T_TupleTableSlot; - slot->tts_flags |= TTS_FLAG_EMPTY; + slot->tts_flags = TTS_FLAG_EMPTY | flags; if (tupleDesc != NULL) slot->tts_flags |= TTS_FLAG_FIXED; slot->tts_tupleDescriptor = tupleDesc; @@ -1334,12 +1391,25 @@ MakeTupleTableSlot(TupleDesc tupleDesc, slot->tts_values = (Datum *) (((char *) slot) + MAXALIGN(basesz)); + slot->tts_isnull = (bool *) (((char *) slot) + MAXALIGN(basesz) + MAXALIGN(tupleDesc->natts * sizeof(Datum))); PinTupleDesc(tupleDesc); + + /* + * Precalculate the maximum guaranteed attribute that has to exist in + * every tuple which gets deformed into this slot. When the + * TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS flag is enabled, we simply take + * the pre-calculated value from the tupleDesc, otherwise the + * optimization is disabled, and we set the value to 0. + */ + if ((flags & TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS) != 0) + slot->tts_first_nonguaranteed = tupleDesc->firstNonGuaranteedAttr; + else + slot->tts_first_nonguaranteed = 0; } /* @@ -1358,9 +1428,9 @@ MakeTupleTableSlot(TupleDesc tupleDesc, */ TupleTableSlot * ExecAllocTableSlot(List **tupleTable, TupleDesc desc, - const TupleTableSlotOps *tts_ops) + const TupleTableSlotOps *tts_ops, uint16 flags) { - TupleTableSlot *slot = MakeTupleTableSlot(desc, tts_ops); + TupleTableSlot *slot = MakeTupleTableSlot(desc, tts_ops, flags); *tupleTable = lappend(*tupleTable, slot); @@ -1427,7 +1497,7 @@ TupleTableSlot * MakeSingleTupleTableSlot(TupleDesc tupdesc, const TupleTableSlotOps *tts_ops) { - TupleTableSlot *slot = MakeTupleTableSlot(tupdesc, tts_ops); + TupleTableSlot *slot = MakeTupleTableSlot(tupdesc, tts_ops, 0); return slot; } @@ -1507,8 +1577,14 @@ ExecSetSlotDescriptor(TupleTableSlot *slot, /* slot to change */ */ slot->tts_values = (Datum *) MemoryContextAlloc(slot->tts_mcxt, tupdesc->natts * sizeof(Datum)); + + /* + * We round the size of tts_isnull up to the next highest multiple of 8. + * This is needed as populate_isnull_array() operates on 8 elements at a + * time when converting a tuple's NULL bitmap into a boolean array. + */ slot->tts_isnull = (bool *) - MemoryContextAlloc(slot->tts_mcxt, tupdesc->natts * sizeof(bool)); + MemoryContextAlloc(slot->tts_mcxt, TYPEALIGN(8, tupdesc->natts * sizeof(bool))); } /* -------------------------------- @@ -1970,7 +2046,7 @@ ExecInitResultSlot(PlanState *planstate, const TupleTableSlotOps *tts_ops) TupleTableSlot *slot; slot = ExecAllocTableSlot(&planstate->state->es_tupleTable, - planstate->ps_ResultTupleDesc, tts_ops); + planstate->ps_ResultTupleDesc, tts_ops, 0); planstate->ps_ResultTupleSlot = slot; planstate->resultopsfixed = planstate->ps_ResultTupleDesc != NULL; @@ -1998,10 +2074,11 @@ ExecInitResultTupleSlotTL(PlanState *planstate, */ void ExecInitScanTupleSlot(EState *estate, ScanState *scanstate, - TupleDesc tupledesc, const TupleTableSlotOps *tts_ops) + TupleDesc tupledesc, const TupleTableSlotOps *tts_ops, + uint16 flags) { scanstate->ss_ScanTupleSlot = ExecAllocTableSlot(&estate->es_tupleTable, - tupledesc, tts_ops); + tupledesc, tts_ops, flags); scanstate->ps.scandesc = tupledesc; scanstate->ps.scanopsfixed = tupledesc != NULL; scanstate->ps.scanops = tts_ops; @@ -2021,7 +2098,7 @@ ExecInitExtraTupleSlot(EState *estate, TupleDesc tupledesc, const TupleTableSlotOps *tts_ops) { - return ExecAllocTableSlot(&estate->es_tupleTable, tupledesc, tts_ops); + return ExecAllocTableSlot(&estate->es_tupleTable, tupledesc, tts_ops, 0); } /* ---------------- @@ -2058,34 +2135,36 @@ slot_getmissingattrs(TupleTableSlot *slot, int startAttNum, int lastAttNum) { AttrMissing *attrmiss = NULL; + /* Check for invalid attnums */ + if (unlikely(lastAttNum > slot->tts_tupleDescriptor->natts)) + elog(ERROR, "invalid attribute number %d", lastAttNum); + if (slot->tts_tupleDescriptor->constr) attrmiss = slot->tts_tupleDescriptor->constr->missing; if (!attrmiss) { /* no missing values array at all, so just fill everything in as NULL */ - memset(slot->tts_values + startAttNum, 0, - (lastAttNum - startAttNum) * sizeof(Datum)); - memset(slot->tts_isnull + startAttNum, 1, - (lastAttNum - startAttNum) * sizeof(bool)); + for (int attnum = startAttNum; attnum < lastAttNum; attnum++) + { + slot->tts_values[attnum] = (Datum) 0; + slot->tts_isnull[attnum] = true; + } } else { - int missattnum; - - /* if there is a missing values array we must process them one by one */ - for (missattnum = startAttNum; - missattnum < lastAttNum; - missattnum++) + /* use attrmiss to set the missing values */ + for (int attnum = startAttNum; attnum < lastAttNum; attnum++) { - slot->tts_values[missattnum] = attrmiss[missattnum].am_value; - slot->tts_isnull[missattnum] = !attrmiss[missattnum].am_present; + slot->tts_values[attnum] = attrmiss[attnum].am_value; + slot->tts_isnull[attnum] = !attrmiss[attnum].am_present; } } } /* - * slot_getsomeattrs_int - workhorse for slot_getsomeattrs() + * slot_getsomeattrs_int + * external function to call getsomeattrs() for use in JIT */ void slot_getsomeattrs_int(TupleTableSlot *slot, int attnum) @@ -2094,21 +2173,13 @@ slot_getsomeattrs_int(TupleTableSlot *slot, int attnum) Assert(slot->tts_nvalid < attnum); /* checked in slot_getsomeattrs */ Assert(attnum > 0); - if (unlikely(attnum > slot->tts_tupleDescriptor->natts)) - elog(ERROR, "invalid attribute number %d", attnum); - /* Fetch as many attributes as possible from the underlying tuple. */ slot->tts_ops->getsomeattrs(slot, attnum); /* - * If the underlying tuple doesn't have enough attributes, tuple - * descriptor must have the missing attributes. + * Avoid putting new code here as that would prevent the compiler from + * using the sibling call optimization for the above function. */ - if (unlikely(slot->tts_nvalid < attnum)) - { - slot_getmissingattrs(slot, slot->tts_nvalid, attnum); - slot->tts_nvalid = attnum; - } } /* ---------------------------------------------------------------- @@ -2173,6 +2244,8 @@ ExecTypeFromTLInternal(List *targetList, bool skipjunk) cur_resno++; } + TupleDescFinalize(typeInfo); + return typeInfo; } @@ -2207,6 +2280,8 @@ ExecTypeFromExprList(List *exprList) cur_resno++; } + TupleDescFinalize(typeInfo); + return typeInfo; } @@ -2255,10 +2330,16 @@ ExecTypeSetColNames(TupleDesc typeInfo, List *namesList) * This happens "for free" if the tupdesc came from a relcache entry, but * not if we have manufactured a tupdesc for a transient RECORD datatype. * In that case we have to notify typcache.c of the existence of the type. + * + * TupleDescFinalize() must be called on the TupleDesc before calling this + * function. */ TupleDesc BlessTupleDesc(TupleDesc tupdesc) { + /* Did someone forget to call TupleDescFinalize()? */ + Assert(tupdesc->firstNonCachedOffsetAttr >= 0); + if (tupdesc->tdtypeid == RECORDOID && tupdesc->tdtypmod < 0) assign_record_type_typmod(tupdesc); @@ -2283,7 +2364,7 @@ TupleDescGetAttInMetadata(TupleDesc tupdesc) int32 *atttypmods; AttInMetadata *attinmeta; - attinmeta = (AttInMetadata *) palloc(sizeof(AttInMetadata)); + attinmeta = palloc_object(AttInMetadata); /* "Bless" the tupledesc so that we can make rowtype datums with it */ attinmeta->tupdesc = BlessTupleDesc(tupdesc); @@ -2447,7 +2528,7 @@ begin_tup_output_tupdesc(DestReceiver *dest, { TupOutputState *tstate; - tstate = (TupOutputState *) palloc(sizeof(TupOutputState)); + tstate = palloc_object(TupOutputState); tstate->slot = MakeSingleTupleTableSlot(tupdesc, tts_ops); tstate->dest = dest; diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c index fdc65c2b42b33..1eb6b9f1f4068 100644 --- a/src/backend/executor/execUtils.c +++ b/src/backend/executor/execUtils.c @@ -3,7 +3,7 @@ * execUtils.c * miscellaneous executor utility routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -48,6 +48,7 @@ #include "access/parallel.h" #include "access/table.h" #include "access/tableam.h" +#include "access/tupconvert.h" #include "executor/executor.h" #include "executor/nodeModifyTable.h" #include "jit/jit.h" @@ -55,6 +56,7 @@ #include "miscadmin.h" #include "parser/parse_relation.h" #include "partitioning/partdesc.h" +#include "port/pg_bitutils.h" #include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/memutils.h" @@ -119,6 +121,9 @@ CreateExecutorState(void) estate->es_rteperminfos = NIL; estate->es_plannedstmt = NULL; estate->es_part_prune_infos = NIL; + estate->es_part_prune_states = NIL; + estate->es_part_prune_results = NIL; + estate->es_unpruned_relids = NULL; estate->es_junkFilter = NULL; @@ -321,7 +326,7 @@ CreateExprContext(EState *estate) ExprContext * CreateWorkExprContext(EState *estate) { - Size maxBlockSize = ALLOCSET_DEFAULT_MAXSIZE; + Size maxBlockSize; maxBlockSize = pg_prevpower2_size_t(work_mem * (Size) 1024 / 16); @@ -711,7 +716,7 @@ ExecCreateScanSlotFromOuterPlan(EState *estate, outerPlan = outerPlanState(scanstate); tupDesc = ExecGetResultType(outerPlan); - ExecInitScanTupleSlot(estate, scanstate, tupDesc, tts_ops); + ExecInitScanTupleSlot(estate, scanstate, tupDesc, tts_ops, 0); } /* ---------------------------------------------------------------- @@ -728,7 +733,28 @@ ExecCreateScanSlotFromOuterPlan(EState *estate, bool ExecRelationIsTargetRelation(EState *estate, Index scanrelid) { - return list_member_int(estate->es_plannedstmt->resultRelations, scanrelid); + return bms_is_member(scanrelid, estate->es_plannedstmt->resultRelationRelids); +} + +/* + * Return true if the scan node's relation is not modified by the query. + * + * This is not perfectly accurate. INSERT ... SELECT from the same table does + * not add the scan relation to resultRelationRelids, so it will be reported + * as read-only even though the query modifies it. + * + * Conversely, when any relation in the query has a modifying row mark, all + * other relations get a ROW_MARK_REFERENCE, causing them to be reported as + * not read-only even though they may be. + */ +bool +ScanRelIsReadOnly(ScanState *ss) +{ + Index scanrelid = ((Scan *) ss->ps.plan)->scanrelid; + PlannedStmt *pstmt = ss->ps.state->es_plannedstmt; + + return !bms_is_member(scanrelid, pstmt->resultRelationRelids) && + !bms_is_member(scanrelid, pstmt->rowMarkRelids); } /* ---------------------------------------------------------------- diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index 359aafea681b9..8810934881797 100644 --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -3,7 +3,7 @@ * functions.c * Execution of SQL-language functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -37,6 +37,7 @@ #include "utils/plancache.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" /* @@ -143,6 +144,7 @@ typedef struct SQLFunctionCache { SQLFunctionHashEntry *func; /* associated SQLFunctionHashEntry */ + bool active; /* are we executing this cache entry? */ bool lazyEvalOK; /* true if lazyEval is safe */ bool shutdown_reg; /* true if registered shutdown callback */ bool lazyEval; /* true if using lazyEval for result query */ @@ -255,7 +257,7 @@ prepare_sql_fn_parse_info(HeapTuple procedureTuple, Form_pg_proc procedureStruct = (Form_pg_proc) GETSTRUCT(procedureTuple); int nargs; - pinfo = (SQLFunctionParseInfoPtr) palloc0(sizeof(SQLFunctionParseInfo)); + pinfo = (SQLFunctionParseInfoPtr) palloc0_object(SQLFunctionParseInfo); /* Function's name (only) can be used to qualify argument names */ pinfo->fname = pstrdup(NameStr(procedureStruct->proname)); @@ -556,6 +558,28 @@ init_sql_fcache(FunctionCallInfo fcinfo, bool lazyEvalOK) finfo->fn_extra = fcache; } + /* + * If the SQLFunctionCache is marked as active, we must have errored out + * of a prior execution. Reset state. (It might seem that we could also + * reach this during recursive invocation of a SQL function, but we won't + * because that case won't involve re-use of the same FmgrInfo.) + */ + if (fcache->active) + { + /* + * In general, this stanza should clear all the same fields that + * ShutdownSQLFunction would. Note we must clear fcache->cplan + * without doing ReleaseCachedPlan, because error cleanup from the + * prior execution would have taken care of releasing that plan. + * Likewise, if tstore is still set then it is pointing at garbage. + */ + fcache->cplan = NULL; + fcache->eslist = NULL; + fcache->tstore = NULL; + fcache->shutdown_reg = false; + fcache->active = false; + } + /* * If we are resuming execution of a set-returning function, just keep * using the same cache. We do not ask funccache.c to re-validate the @@ -1597,6 +1621,9 @@ fmgr_sql(PG_FUNCTION_ARGS) */ fcache = init_sql_fcache(fcinfo, lazyEvalOK); + /* Mark fcache as active */ + fcache->active = true; + /* Remember info that we might need later to construct tuplestore */ fcache->tscontext = tscontext; fcache->randomAccess = randomAccess; @@ -1853,6 +1880,9 @@ fmgr_sql(PG_FUNCTION_ARGS) if (es == NULL) fcache->eslist = NULL; + /* Mark fcache as inactive */ + fcache->active = false; + error_context_stack = sqlerrcontext.previous; return result; @@ -2454,7 +2484,7 @@ check_sql_stmt_retval(List *queryTreeList, rte = makeNode(RangeTblEntry); rte->rtekind = RTE_SUBQUERY; rte->subquery = parse; - rte->eref = rte->alias = makeAlias("*SELECT*", colnames); + rte->eref = makeAlias("unnamed_subquery", colnames); rte->lateral = false; rte->inh = false; rte->inFromCl = true; @@ -2587,7 +2617,7 @@ get_sql_fn_result_tlist(List *queryTreeList) DestReceiver * CreateSQLFunctionDestReceiver(void) { - DR_sqlfunction *self = (DR_sqlfunction *) palloc0(sizeof(DR_sqlfunction)); + DR_sqlfunction *self = palloc0_object(DR_sqlfunction); self->pub.receiveSlot = sqlfunction_receive; self->pub.rStartup = sqlfunction_startup; diff --git a/src/backend/executor/instrument.c b/src/backend/executor/instrument.c index 56e635f47000d..ffbcd57213396 100644 --- a/src/backend/executor/instrument.c +++ b/src/backend/executor/instrument.c @@ -4,7 +4,7 @@ * functions for instrumentation of plan execution * * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/executor/instrument.c @@ -15,7 +15,12 @@ #include +#include "executor/executor.h" #include "executor/instrument.h" +#include "executor/tuptable.h" +#include "nodes/execnodes.h" +#include "portability/instr_time.h" +#include "utils/guc_hooks.h" BufferUsage pgBufferUsage; static BufferUsage save_pgBufferUsage; @@ -26,52 +31,36 @@ static void BufferUsageAdd(BufferUsage *dst, const BufferUsage *add); static void WalUsageAdd(WalUsage *dst, WalUsage *add); -/* Allocate new instrumentation structure(s) */ +/* General purpose instrumentation handling */ Instrumentation * -InstrAlloc(int n, int instrument_options, bool async_mode) +InstrAlloc(int instrument_options) { - Instrumentation *instr; - - /* initialize all fields to zeroes, then modify as needed */ - instr = palloc0(n * sizeof(Instrumentation)); - if (instrument_options & (INSTRUMENT_BUFFERS | INSTRUMENT_TIMER | INSTRUMENT_WAL)) - { - bool need_buffers = (instrument_options & INSTRUMENT_BUFFERS) != 0; - bool need_wal = (instrument_options & INSTRUMENT_WAL) != 0; - bool need_timer = (instrument_options & INSTRUMENT_TIMER) != 0; - int i; - - for (i = 0; i < n; i++) - { - instr[i].need_bufusage = need_buffers; - instr[i].need_walusage = need_wal; - instr[i].need_timer = need_timer; - instr[i].async_mode = async_mode; - } - } + Instrumentation *instr = palloc0_object(Instrumentation); + InstrInitOptions(instr, instrument_options); return instr; } -/* Initialize a pre-allocated instrumentation structure. */ void -InstrInit(Instrumentation *instr, int instrument_options) +InstrInitOptions(Instrumentation *instr, int instrument_options) { - memset(instr, 0, sizeof(Instrumentation)); instr->need_bufusage = (instrument_options & INSTRUMENT_BUFFERS) != 0; instr->need_walusage = (instrument_options & INSTRUMENT_WAL) != 0; instr->need_timer = (instrument_options & INSTRUMENT_TIMER) != 0; } -/* Entry to a plan node */ -void -InstrStartNode(Instrumentation *instr) +inline void +InstrStart(Instrumentation *instr) { - if (instr->need_timer && - !INSTR_TIME_SET_CURRENT_LAZY(instr->starttime)) - elog(ERROR, "InstrStartNode called twice in a row"); + if (instr->need_timer) + { + if (!INSTR_TIME_IS_ZERO(instr->starttime)) + elog(ERROR, "InstrStart called twice in a row"); + else + INSTR_TIME_SET_CURRENT_FAST(instr->starttime); + } - /* save buffer usage totals at node entry, if needed */ + /* save buffer usage totals at start, if needed */ if (instr->need_bufusage) instr->bufusage_start = pgBufferUsage; @@ -79,29 +68,28 @@ InstrStartNode(Instrumentation *instr) instr->walusage_start = pgWalUsage; } -/* Exit from a plan node */ -void -InstrStopNode(Instrumentation *instr, double nTuples) +/* + * Helper for InstrStop() and InstrStopNode(), to avoid code duplication + * despite slightly different needs about how time is accumulated. + */ +static inline void +InstrStopCommon(Instrumentation *instr, instr_time *accum_time) { - double save_tuplecount = instr->tuplecount; instr_time endtime; - /* count the returned tuples */ - instr->tuplecount += nTuples; - - /* let's update the time only if the timer was requested */ + /* update the time only if the timer was requested */ if (instr->need_timer) { if (INSTR_TIME_IS_ZERO(instr->starttime)) - elog(ERROR, "InstrStopNode called without start"); + elog(ERROR, "InstrStop called without start"); - INSTR_TIME_SET_CURRENT(endtime); - INSTR_TIME_ACCUM_DIFF(instr->counter, endtime, instr->starttime); + INSTR_TIME_SET_CURRENT_FAST(endtime); + INSTR_TIME_ACCUM_DIFF(*accum_time, endtime, instr->starttime); INSTR_TIME_SET_ZERO(instr->starttime); } - /* Add delta of buffer usage since entry to node's totals */ + /* Add delta of buffer usage since InstrStart to the totals */ if (instr->need_bufusage) BufferUsageAccumDiff(&instr->bufusage, &pgBufferUsage, &instr->bufusage_start); @@ -109,12 +97,66 @@ InstrStopNode(Instrumentation *instr, double nTuples) if (instr->need_walusage) WalUsageAccumDiff(&instr->walusage, &pgWalUsage, &instr->walusage_start); +} + +void +InstrStop(Instrumentation *instr) +{ + InstrStopCommon(instr, &instr->total); +} + +/* Node instrumentation handling */ + +/* Allocate new node instrumentation structure */ +NodeInstrumentation * +InstrAllocNode(int instrument_options, bool async_mode) +{ + NodeInstrumentation *instr = palloc_object(NodeInstrumentation); + + InstrInitNode(instr, instrument_options, async_mode); + + return instr; +} + +/* Initialize a pre-allocated instrumentation structure. */ +void +InstrInitNode(NodeInstrumentation *instr, int instrument_options, bool async_mode) +{ + memset(instr, 0, sizeof(NodeInstrumentation)); + InstrInitOptions(&instr->instr, instrument_options); + instr->async_mode = async_mode; +} + +/* Entry to a plan node */ +inline void +InstrStartNode(NodeInstrumentation *instr) +{ + InstrStart(&instr->instr); +} + +/* Exit from a plan node */ +inline void +InstrStopNode(NodeInstrumentation *instr, double nTuples) +{ + double save_tuplecount = instr->tuplecount; + + /* count the returned tuples */ + instr->tuplecount += nTuples; + + /* + * Note that in contrast to InstrStop() the time is accumulated into + * NodeInstrumentation->counter, with total only getting updated in + * InstrEndLoop. We need the separate counter variable because we need to + * calculate start-up time for the first tuple in each cycle, and then + * accumulate it together. + */ + InstrStopCommon(&instr->instr, &instr->counter); /* Is this the first tuple of this cycle? */ if (!instr->running) { instr->running = true; - instr->firsttuple = INSTR_TIME_GET_DOUBLE(instr->counter); + instr->firsttuple = instr->counter; } else { @@ -123,13 +165,35 @@ InstrStopNode(Instrumentation *instr, double nTuples) * this might be the first tuple */ if (instr->async_mode && save_tuplecount < 1.0) - instr->firsttuple = INSTR_TIME_GET_DOUBLE(instr->counter); + instr->firsttuple = instr->counter; } } +/* + * ExecProcNode wrapper that performs instrumentation calls. By keeping + * this a separate function, we avoid overhead in the normal case where + * no instrumentation is wanted. + * + * This is implemented in instrument.c as all the functions it calls directly + * are here, allowing them to be inlined even when not using LTO. + */ +TupleTableSlot * +ExecProcNodeInstr(PlanState *node) +{ + TupleTableSlot *result; + + InstrStartNode(node->instrument); + + result = node->ExecProcNodeReal(node); + + InstrStopNode(node->instrument, TupIsNull(result) ? 0.0 : 1.0); + + return result; +} + /* Update tuple count */ void -InstrUpdateTupleCount(Instrumentation *instr, double nTuples) +InstrUpdateTupleCount(NodeInstrumentation *instr, double nTuples) { /* count the returned tuples */ instr->tuplecount += nTuples; @@ -137,62 +201,77 @@ InstrUpdateTupleCount(Instrumentation *instr, double nTuples) /* Finish a run cycle for a plan node */ void -InstrEndLoop(Instrumentation *instr) +InstrEndLoop(NodeInstrumentation *instr) { - double totaltime; - /* Skip if nothing has happened, or already shut down */ if (!instr->running) return; - if (!INSTR_TIME_IS_ZERO(instr->starttime)) + if (!INSTR_TIME_IS_ZERO(instr->instr.starttime)) elog(ERROR, "InstrEndLoop called on running node"); /* Accumulate per-cycle statistics into totals */ - totaltime = INSTR_TIME_GET_DOUBLE(instr->counter); - - instr->startup += instr->firsttuple; - instr->total += totaltime; + INSTR_TIME_ADD(instr->startup, instr->firsttuple); + INSTR_TIME_ADD(instr->instr.total, instr->counter); instr->ntuples += instr->tuplecount; instr->nloops += 1; /* Reset for next cycle (if any) */ instr->running = false; - INSTR_TIME_SET_ZERO(instr->starttime); + INSTR_TIME_SET_ZERO(instr->instr.starttime); INSTR_TIME_SET_ZERO(instr->counter); - instr->firsttuple = 0; + INSTR_TIME_SET_ZERO(instr->firsttuple); instr->tuplecount = 0; } -/* aggregate instrumentation information */ +/* + * Aggregate instrumentation from parallel workers. Must be called after + * InstrEndLoop. + */ void -InstrAggNode(Instrumentation *dst, Instrumentation *add) +InstrAggNode(NodeInstrumentation *dst, NodeInstrumentation *add) { - if (!dst->running && add->running) - { - dst->running = true; - dst->firsttuple = add->firsttuple; - } - else if (dst->running && add->running && dst->firsttuple > add->firsttuple) - dst->firsttuple = add->firsttuple; + Assert(!add->running); - INSTR_TIME_ADD(dst->counter, add->counter); - - dst->tuplecount += add->tuplecount; - dst->startup += add->startup; - dst->total += add->total; + INSTR_TIME_ADD(dst->startup, add->startup); + INSTR_TIME_ADD(dst->instr.total, add->instr.total); dst->ntuples += add->ntuples; dst->ntuples2 += add->ntuples2; dst->nloops += add->nloops; dst->nfiltered1 += add->nfiltered1; dst->nfiltered2 += add->nfiltered2; - /* Add delta of buffer usage since entry to node's totals */ - if (dst->need_bufusage) - BufferUsageAdd(&dst->bufusage, &add->bufusage); + if (dst->instr.need_bufusage) + BufferUsageAdd(&dst->instr.bufusage, &add->instr.bufusage); + + if (dst->instr.need_walusage) + WalUsageAdd(&dst->instr.walusage, &add->instr.walusage); +} + +/* Trigger instrumentation handling */ +TriggerInstrumentation * +InstrAllocTrigger(int n, int instrument_options) +{ + TriggerInstrumentation *tginstr = palloc0_array(TriggerInstrumentation, n); + int i; + + for (i = 0; i < n; i++) + InstrInitOptions(&tginstr[i].instr, instrument_options); - if (dst->need_walusage) - WalUsageAdd(&dst->walusage, &add->walusage); + return tginstr; +} + +void +InstrStartTrigger(TriggerInstrumentation *tginstr) +{ + InstrStart(&tginstr->instr); +} + +void +InstrStopTrigger(TriggerInstrumentation *tginstr, int64 firings) +{ + InstrStop(&tginstr->instr); + tginstr->firings += firings; } /* note current values during parallel executor startup */ @@ -244,7 +323,7 @@ BufferUsageAdd(BufferUsage *dst, const BufferUsage *add) } /* dst += add - sub */ -void +inline void BufferUsageAccumDiff(BufferUsage *dst, const BufferUsage *add, const BufferUsage *sub) @@ -274,20 +353,94 @@ BufferUsageAccumDiff(BufferUsage *dst, } /* helper functions for WAL usage accumulation */ -static void +static inline void WalUsageAdd(WalUsage *dst, WalUsage *add) { dst->wal_bytes += add->wal_bytes; dst->wal_records += add->wal_records; dst->wal_fpi += add->wal_fpi; + dst->wal_fpi_bytes += add->wal_fpi_bytes; dst->wal_buffers_full += add->wal_buffers_full; } -void +inline void WalUsageAccumDiff(WalUsage *dst, const WalUsage *add, const WalUsage *sub) { dst->wal_bytes += add->wal_bytes - sub->wal_bytes; dst->wal_records += add->wal_records - sub->wal_records; dst->wal_fpi += add->wal_fpi - sub->wal_fpi; + dst->wal_fpi_bytes += add->wal_fpi_bytes - sub->wal_fpi_bytes; dst->wal_buffers_full += add->wal_buffers_full - sub->wal_buffers_full; } + +/* GUC hooks for timing_clock_source */ + +bool +check_timing_clock_source(int *newval, void **extra, GucSource source) +{ + /* + * Do nothing if timing is not initialized. This is only expected on child + * processes in EXEC_BACKEND builds, as GUC hooks can be called during + * InitializeGUCOptions() before InitProcessGlobals() has had a chance to + * run pg_initialize_timing(). Instead, TSC will be initialized via + * restore_backend_variables. + */ +#ifdef EXEC_BACKEND + if (!timing_initialized) + return true; +#else + Assert(timing_initialized); +#endif + +#if PG_INSTR_TSC_CLOCK + pg_initialize_timing_tsc(); + + if (*newval == TIMING_CLOCK_SOURCE_TSC && timing_tsc_frequency_khz <= 0) + { + GUC_check_errdetail("TSC is not supported as timing clock source"); + return false; + } +#endif + + return true; +} + +void +assign_timing_clock_source(int newval, void *extra) +{ +#ifdef EXEC_BACKEND + if (!timing_initialized) + return; +#else + Assert(timing_initialized); +#endif + + /* + * Ignore the return code since the check hook already verified TSC is + * usable if it's explicitly requested. + */ + pg_set_timing_clock_source(newval); +} + +const char * +show_timing_clock_source(void) +{ + switch (timing_clock_source) + { + case TIMING_CLOCK_SOURCE_AUTO: +#if PG_INSTR_TSC_CLOCK + if (pg_current_timing_clock_source() == TIMING_CLOCK_SOURCE_TSC) + return "auto (tsc)"; +#endif + return "auto (system)"; + case TIMING_CLOCK_SOURCE_SYSTEM: + return "system"; +#if PG_INSTR_TSC_CLOCK + case TIMING_CLOCK_SOURCE_TSC: + return "tsc"; +#endif + } + + /* unreachable */ + return "?"; +} diff --git a/src/backend/executor/meson.build b/src/backend/executor/meson.build index 2cea41f877113..dc45be0b2ce97 100644 --- a/src/backend/executor/meson.build +++ b/src/backend/executor/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'execAmi.c', diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c index 377e016d73225..925caadd2cea9 100644 --- a/src/backend/executor/nodeAgg.c +++ b/src/backend/executor/nodeAgg.c @@ -237,7 +237,7 @@ * to filter expressions having to be evaluated early, and allows to JIT * the entire expression into one native function. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -257,6 +257,7 @@ #include "common/hashfn.h" #include "executor/execExpr.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeAgg.h" #include "lib/hyperloglog.h" #include "miscadmin.h" @@ -264,10 +265,10 @@ #include "optimizer/optimizer.h" #include "parser/parse_agg.h" #include "parser/parse_coerce.h" +#include "port/pg_bitutils.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/datum.h" -#include "utils/dynahash.h" #include "utils/expandeddatum.h" #include "utils/injection_point.h" #include "utils/logtape.h" @@ -403,12 +404,12 @@ static void find_cols(AggState *aggstate, Bitmapset **aggregated, Bitmapset **unaggregated); static bool find_cols_walker(Node *node, FindColsContext *context); static void build_hash_tables(AggState *aggstate); -static void build_hash_table(AggState *aggstate, int setno, long nbuckets); +static void build_hash_table(AggState *aggstate, int setno, double nbuckets); static void hashagg_recompile_expressions(AggState *aggstate, bool minslot, bool nullcheck); static void hash_create_memory(AggState *aggstate); -static long hash_choose_num_buckets(double hashentrysize, - long ngroups, Size memory); +static double hash_choose_num_buckets(double hashentrysize, + double ngroups, Size memory); static int hash_choose_num_partitions(double input_groups, double hashentrysize, int used_bits, @@ -1458,7 +1459,7 @@ find_cols_walker(Node *node, FindColsContext *context) * We have a separate hashtable and associated perhash data structure for each * grouping set for which we're doing hashing. * - * The contents of the hash tables always live in the hashcontext's per-tuple + * The contents of the hash tables live in the aggstate's hash_tuplescxt * memory context (there is only one of these for all tables together, since * they are all reset at the same time). */ @@ -1470,7 +1471,7 @@ build_hash_tables(AggState *aggstate) for (setno = 0; setno < aggstate->num_hashes; ++setno) { AggStatePerHash perhash = &aggstate->perhash[setno]; - long nbuckets; + double nbuckets; Size memory; if (perhash->hashtable != NULL) @@ -1479,8 +1480,6 @@ build_hash_tables(AggState *aggstate) continue; } - Assert(perhash->aggnode->numGroups > 0); - memory = aggstate->hash_mem_limit / aggstate->num_hashes; /* choose reasonable number of buckets per hashtable */ @@ -1506,11 +1505,11 @@ build_hash_tables(AggState *aggstate) * Build a single hashtable for this grouping set. */ static void -build_hash_table(AggState *aggstate, int setno, long nbuckets) +build_hash_table(AggState *aggstate, int setno, double nbuckets) { AggStatePerHash perhash = &aggstate->perhash[setno]; MemoryContext metacxt = aggstate->hash_metacxt; - MemoryContext tablecxt = aggstate->hash_tablecxt; + MemoryContext tuplescxt = aggstate->hash_tuplescxt; MemoryContext tmpcxt = aggstate->tmpcontext->ecxt_per_tuple_memory; Size additionalsize; @@ -1536,7 +1535,7 @@ build_hash_table(AggState *aggstate, int setno, long nbuckets) nbuckets, additionalsize, metacxt, - tablecxt, + tuplescxt, tmpcxt, DO_AGGSPLIT_SKIPFINAL(aggstate->aggsplit)); } @@ -1685,7 +1684,7 @@ find_hash_columns(AggState *aggstate) &perhash->hashfunctions); perhash->hashslot = ExecAllocTableSlot(&estate->es_tupleTable, hashDesc, - &TTSOpsMinimalTuple); + &TTSOpsMinimalTuple, 0); list_free(hashTlist); bms_free(colnos); @@ -1869,7 +1868,7 @@ hash_agg_check_limits(AggState *aggstate) uint64 ngroups = aggstate->hash_ngroups_current; Size meta_mem = MemoryContextMemAllocated(aggstate->hash_metacxt, true); - Size entry_mem = MemoryContextMemAllocated(aggstate->hash_tablecxt, + Size entry_mem = MemoryContextMemAllocated(aggstate->hash_tuplescxt, true); Size tval_mem = MemoryContextMemAllocated(aggstate->hashcontext->ecxt_per_tuple_memory, true); @@ -1923,7 +1922,7 @@ hash_agg_enter_spill_mode(AggState *aggstate) aggstate->hash_tapeset = LogicalTapeSetCreate(true, NULL, -1); - aggstate->hash_spills = palloc(sizeof(HashAggSpill) * aggstate->num_hashes); + aggstate->hash_spills = palloc_array(HashAggSpill, aggstate->num_hashes); for (int setno = 0; setno < aggstate->num_hashes; setno++) { @@ -1960,7 +1959,7 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions) meta_mem = MemoryContextMemAllocated(aggstate->hash_metacxt, true); /* memory for hash entries */ - entry_mem = MemoryContextMemAllocated(aggstate->hash_tablecxt, true); + entry_mem = MemoryContextMemAllocated(aggstate->hash_tuplescxt, true); /* memory for byref transition states */ hashkey_mem = MemoryContextMemAllocated(aggstate->hashcontext->ecxt_per_tuple_memory, true); @@ -2043,22 +2042,22 @@ hash_create_memory(AggState *aggstate) /* and no smaller than ALLOCSET_DEFAULT_INITSIZE */ maxBlockSize = Max(maxBlockSize, ALLOCSET_DEFAULT_INITSIZE); - aggstate->hash_tablecxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt, - "HashAgg table context", - ALLOCSET_DEFAULT_MINSIZE, - ALLOCSET_DEFAULT_INITSIZE, - maxBlockSize); + aggstate->hash_tuplescxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt, + "HashAgg hashed tuples", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + maxBlockSize); } /* * Choose a reasonable number of buckets for the initial hash table size. */ -static long -hash_choose_num_buckets(double hashentrysize, long ngroups, Size memory) +static double +hash_choose_num_buckets(double hashentrysize, double ngroups, Size memory) { - long max_nbuckets; - long nbuckets = ngroups; + double max_nbuckets; + double nbuckets = ngroups; max_nbuckets = memory / hashentrysize; @@ -2066,12 +2065,16 @@ hash_choose_num_buckets(double hashentrysize, long ngroups, Size memory) * Underestimating is better than overestimating. Too many buckets crowd * out space for group keys and transition state values. */ - max_nbuckets >>= 1; + max_nbuckets /= 2; if (nbuckets > max_nbuckets) nbuckets = max_nbuckets; - return Max(nbuckets, 1); + /* + * BuildTupleHashTable will clamp any obviously-insane result, so we don't + * need to be too careful here. + */ + return nbuckets; } /* @@ -2115,7 +2118,7 @@ hash_choose_num_partitions(double input_groups, double hashentrysize, npartitions = (int) dpartitions; /* ceil(log2(npartitions)) */ - partition_bits = my_log2(npartitions); + partition_bits = pg_ceil_log2_32(npartitions); /* make sure that we don't exhaust the hash bits */ if (partition_bits + used_bits >= 32) @@ -2256,7 +2259,7 @@ ExecAgg(PlanState *pstate) case AGG_HASHED: if (!node->table_filled) agg_fill_hash_table(node); - /* FALLTHROUGH */ + pg_fallthrough; case AGG_MIXED: result = agg_retrieve_hash_table(node); break; @@ -2708,7 +2711,6 @@ agg_refill_hash_table(AggState *aggstate) /* free memory and reset hash tables */ ReScanExprContext(aggstate->hashcontext); - MemoryContextReset(aggstate->hash_tablecxt); for (int setno = 0; setno < aggstate->num_hashes; setno++) ResetTupleHashTable(aggstate->perhash[setno].hashtable); @@ -2912,7 +2914,7 @@ agg_retrieve_hash_table_in_memory(AggState *aggstate) perhash = &aggstate->perhash[aggstate->current_set]; - ResetTupleHashIterator(hashtable, &perhash->hashiter); + ResetTupleHashIterator(perhash->hashtable, &perhash->hashiter); continue; } @@ -2999,9 +3001,9 @@ hashagg_spill_init(HashAggSpill *spill, LogicalTapeSet *tapeset, int used_bits, } #endif - spill->partitions = palloc0(sizeof(LogicalTape *) * npartitions); - spill->ntuples = palloc0(sizeof(int64) * npartitions); - spill->hll_card = palloc0(sizeof(hyperLogLogState) * npartitions); + spill->partitions = palloc0_array(LogicalTape *, npartitions); + spill->ntuples = palloc0_array(int64, npartitions); + spill->hll_card = palloc0_array(hyperLogLogState, npartitions); for (int i = 0; i < npartitions; i++) spill->partitions[i] = LogicalTapeCreate(tapeset); @@ -3097,7 +3099,7 @@ static HashAggBatch * hashagg_batch_new(LogicalTape *input_tape, int setno, int64 input_tuples, double input_card, int used_bits) { - HashAggBatch *batch = palloc0(sizeof(HashAggBatch)); + HashAggBatch *batch = palloc0_object(HashAggBatch); batch->setno = setno; batch->used_bits = used_bits; @@ -3368,8 +3370,7 @@ ExecInitAgg(Agg *node, EState *estate, int eflags) aggstate->maxsets = numGroupingSets; aggstate->numphases = numPhases; - aggstate->aggcontexts = (ExprContext **) - palloc0(sizeof(ExprContext *) * numGroupingSets); + aggstate->aggcontexts = palloc0_array(ExprContext *, numGroupingSets); /* * Create expression contexts. We need three or more, one for @@ -3492,15 +3493,15 @@ ExecInitAgg(Agg *node, EState *estate, int eflags) * For each phase, prepare grouping set data and fmgr lookup data for * compare functions. Accumulate all_grouped_cols in passing. */ - aggstate->phases = palloc0(numPhases * sizeof(AggStatePerPhaseData)); + aggstate->phases = palloc0_array(AggStatePerPhaseData, numPhases); aggstate->num_hashes = numHashes; if (numHashes) { - aggstate->perhash = palloc0(sizeof(AggStatePerHashData) * numHashes); + aggstate->perhash = palloc0_array(AggStatePerHashData, numHashes); aggstate->phases[0].numsets = 0; - aggstate->phases[0].gset_lengths = palloc(numHashes * sizeof(int)); - aggstate->phases[0].grouped_cols = palloc(numHashes * sizeof(Bitmapset *)); + aggstate->phases[0].gset_lengths = palloc_array(int, numHashes); + aggstate->phases[0].grouped_cols = palloc_array(Bitmapset *, numHashes); } phase = 0; @@ -3598,8 +3599,7 @@ ExecInitAgg(Agg *node, EState *estate, int eflags) * Build a separate function for each subset of columns that * need to be compared. */ - phasedata->eqfunctions = - (ExprState **) palloc0(aggnode->numCols * sizeof(ExprState *)); + phasedata->eqfunctions = palloc0_array(ExprState *, aggnode->numCols); /* for each grouping set */ for (int k = 0; k < phasedata->numsets; k++) @@ -3655,27 +3655,24 @@ ExecInitAgg(Agg *node, EState *estate, int eflags) * allocate my private per-agg working storage */ econtext = aggstate->ss.ps.ps_ExprContext; - econtext->ecxt_aggvalues = (Datum *) palloc0(sizeof(Datum) * numaggs); - econtext->ecxt_aggnulls = (bool *) palloc0(sizeof(bool) * numaggs); + econtext->ecxt_aggvalues = palloc0_array(Datum, numaggs); + econtext->ecxt_aggnulls = palloc0_array(bool, numaggs); - peraggs = (AggStatePerAgg) palloc0(sizeof(AggStatePerAggData) * numaggs); - pertransstates = (AggStatePerTrans) palloc0(sizeof(AggStatePerTransData) * numtrans); + peraggs = palloc0_array(AggStatePerAggData, numaggs); + pertransstates = palloc0_array(AggStatePerTransData, numtrans); aggstate->peragg = peraggs; aggstate->pertrans = pertransstates; - aggstate->all_pergroups = - (AggStatePerGroup *) palloc0(sizeof(AggStatePerGroup) - * (numGroupingSets + numHashes)); + aggstate->all_pergroups = palloc0_array(AggStatePerGroup, numGroupingSets + numHashes); pergroups = aggstate->all_pergroups; if (node->aggstrategy != AGG_HASHED) { for (i = 0; i < numGroupingSets; i++) { - pergroups[i] = (AggStatePerGroup) palloc0(sizeof(AggStatePerGroupData) - * numaggs); + pergroups[i] = palloc0_array(AggStatePerGroupData, numaggs); } aggstate->pergroups = pergroups; @@ -3688,7 +3685,7 @@ ExecInitAgg(Agg *node, EState *estate, int eflags) if (use_hashing) { Plan *outerplan = outerPlan(node); - uint64 totalGroups = 0; + double totalGroups = 0; aggstate->hash_spill_rslot = ExecInitExtraTupleSlot(estate, scanDesc, &TTSOpsMinimalTuple); @@ -4375,8 +4372,7 @@ build_pertrans_for_aggref(AggStatePerTrans pertrans, pfree(ops); } - pertrans->sortstates = (Tuplesortstate **) - palloc0(sizeof(Tuplesortstate *) * numGroupingSets); + pertrans->sortstates = palloc0_array(Tuplesortstate *, numGroupingSets); } @@ -4413,7 +4409,7 @@ ExecEndAgg(AggState *node) { AggregateInstrumentation *si; - Assert(ParallelWorkerNumber <= node->shared_info->num_workers); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; si->hash_batches_used = node->hash_batches_used; si->hash_disk_used = node->hash_disk_used; @@ -4429,18 +4425,18 @@ ExecEndAgg(AggState *node) hashagg_reset_spill_state(node); + /* Release hash tables too */ if (node->hash_metacxt != NULL) { MemoryContextDelete(node->hash_metacxt); node->hash_metacxt = NULL; } - if (node->hash_tablecxt != NULL) + if (node->hash_tuplescxt != NULL) { - MemoryContextDelete(node->hash_tablecxt); - node->hash_tablecxt = NULL; + MemoryContextDelete(node->hash_tuplescxt); + node->hash_tuplescxt = NULL; } - for (transno = 0; transno < node->numtrans; transno++) { AggStatePerTrans pertrans = &node->pertrans[transno]; @@ -4556,8 +4552,7 @@ ExecReScanAgg(AggState *node) node->hash_ngroups_current = 0; ReScanExprContext(node->hashcontext); - MemoryContextReset(node->hash_tablecxt); - /* Rebuild an empty hash table */ + /* Rebuild empty hash table(s) */ build_hash_tables(node); node->table_filled = false; /* iterator will be reset when the table is filled */ diff --git a/src/backend/executor/nodeAppend.c b/src/backend/executor/nodeAppend.c index a11b36c717662..85c85569b5ed9 100644 --- a/src/backend/executor/nodeAppend.c +++ b/src/backend/executor/nodeAppend.c @@ -3,7 +3,7 @@ * nodeAppend.c * routines to handle append nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -64,6 +64,8 @@ #include "miscadmin.h" #include "pgstat.h" #include "storage/latch.h" +#include "storage/lwlock.h" +#include "utils/wait_event.h" /* Shared state for parallel-aware Append. */ struct ParallelAppendState @@ -263,7 +265,7 @@ ExecInitAppend(Append *node, EState *estate, int eflags) { AsyncRequest *areq; - areq = palloc(sizeof(AsyncRequest)); + areq = palloc_object(AsyncRequest); areq->requestor = (PlanState *) appendstate; areq->requestee = appendplanstates[i]; areq->request_index = i; diff --git a/src/backend/executor/nodeBitmapAnd.c b/src/backend/executor/nodeBitmapAnd.c index 939907b6fcd4b..9007dda3802b9 100644 --- a/src/backend/executor/nodeBitmapAnd.c +++ b/src/backend/executor/nodeBitmapAnd.c @@ -3,7 +3,7 @@ * nodeBitmapAnd.c * routines to handle BitmapAnd nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,7 +29,9 @@ #include "postgres.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeBitmapAnd.h" +#include "nodes/tidbitmap.h" /* ---------------------------------------------------------------- diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c index bf24f3d7fe0a8..83d6478bc2b63 100644 --- a/src/backend/executor/nodeBitmapHeapscan.c +++ b/src/backend/executor/nodeBitmapHeapscan.c @@ -16,7 +16,7 @@ * required index qual conditions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -35,18 +35,20 @@ */ #include "postgres.h" -#include - #include "access/relscan.h" #include "access/tableam.h" #include "access/visibilitymap.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeBitmapHeapscan.h" #include "miscadmin.h" #include "pgstat.h" #include "storage/bufmgr.h" +#include "storage/condition_variable.h" +#include "utils/dsa.h" #include "utils/rel.h" #include "utils/spccache.h" +#include "utils/wait_event.h" static void BitmapTableScanSetup(BitmapHeapScanState *node); static TupleTableSlot *BitmapHeapNext(BitmapHeapScanState *node); @@ -54,6 +56,43 @@ static inline void BitmapDoneInitializingSharedState(ParallelBitmapHeapState *ps static bool BitmapShouldInitializeSharedState(ParallelBitmapHeapState *pstate); +/* ---------------- + * SharedBitmapState information + * + * BM_INITIAL TIDBitmap creation is not yet started, so first worker + * to see this state will set the state to BM_INPROGRESS + * and that process will be responsible for creating + * TIDBitmap. + * BM_INPROGRESS TIDBitmap creation is in progress; workers need to + * sleep until it's finished. + * BM_FINISHED TIDBitmap creation is done, so now all workers can + * proceed to iterate over TIDBitmap. + * ---------------- + */ +typedef enum +{ + BM_INITIAL, + BM_INPROGRESS, + BM_FINISHED, +} SharedBitmapState; + +/* ---------------- + * ParallelBitmapHeapState information + * tbmiterator iterator for scanning current pages + * mutex mutual exclusion for state + * state current state of the TIDBitmap + * cv conditional wait variable + * ---------------- + */ +typedef struct ParallelBitmapHeapState +{ + dsa_pointer tbmiterator; + slock_t mutex; + SharedBitmapState state; + ConditionVariable cv; +} ParallelBitmapHeapState; + + /* * Do the underlying index scan, build the bitmap, set up the parallel state * needed for parallel workers to iterate through the bitmap, and set up the @@ -105,11 +144,20 @@ BitmapTableScanSetup(BitmapHeapScanState *node) */ if (!node->ss.ss_currentScanDesc) { + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (node->ss.ps.state->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; + node->ss.ss_currentScanDesc = table_beginscan_bm(node->ss.ss_currentRelation, node->ss.ps.state->es_snapshot, 0, - NULL); + NULL, + flags); } node->ss.ss_currentScanDesc->st.rs_tbmiterator = tbmiterator; @@ -277,7 +325,7 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node) { BitmapHeapScanInstrumentation *si; - Assert(ParallelWorkerNumber <= node->sinstrument->num_workers); + Assert(ParallelWorkerNumber < node->sinstrument->num_workers); si = &node->sinstrument->sinstrument[ParallelWorkerNumber]; /* @@ -289,6 +337,14 @@ ExecEndBitmapHeapScan(BitmapHeapScanState *node) */ si->exact_pages += node->stats.exact_pages; si->lossy_pages += node->stats.lossy_pages; + + /* collect I/O instrumentation for this process */ + if (node->ss.ss_currentScanDesc && + node->ss.ss_currentScanDesc->rs_instrument) + { + AccumulateIOStats(&si->stats.io, + &node->ss.ss_currentScanDesc->rs_instrument->io); + } } /* @@ -383,7 +439,8 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags) */ ExecInitScanTupleSlot(estate, &scanstate->ss, RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation)); + table_slot_callbacks(currentRelation), + TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); /* * Initialize result type and projection. @@ -453,18 +510,8 @@ void ExecBitmapHeapEstimate(BitmapHeapScanState *node, ParallelContext *pcxt) { - Size size; - - size = MAXALIGN(sizeof(ParallelBitmapHeapState)); - - /* account for instrumentation, if required */ - if (node->ss.ps.instrument && pcxt->nworkers > 0) - { - size = add_size(size, offsetof(SharedBitmapHeapInstrumentation, sinstrument)); - size = add_size(size, mul_size(pcxt->nworkers, sizeof(BitmapHeapScanInstrumentation))); - } - - shm_toc_estimate_chunk(&pcxt->estimator, size); + shm_toc_estimate_chunk(&pcxt->estimator, + MAXALIGN(sizeof(ParallelBitmapHeapState))); shm_toc_estimate_keys(&pcxt->estimator, 1); } @@ -479,27 +526,15 @@ ExecBitmapHeapInitializeDSM(BitmapHeapScanState *node, ParallelContext *pcxt) { ParallelBitmapHeapState *pstate; - SharedBitmapHeapInstrumentation *sinstrument = NULL; dsa_area *dsa = node->ss.ps.state->es_query_dsa; - char *ptr; - Size size; /* If there's no DSA, there are no workers; initialize nothing. */ if (dsa == NULL) return; - size = MAXALIGN(sizeof(ParallelBitmapHeapState)); - if (node->ss.ps.instrument && pcxt->nworkers > 0) - { - size = add_size(size, offsetof(SharedBitmapHeapInstrumentation, sinstrument)); - size = add_size(size, mul_size(pcxt->nworkers, sizeof(BitmapHeapScanInstrumentation))); - } - - ptr = shm_toc_allocate(pcxt->toc, size); - pstate = (ParallelBitmapHeapState *) ptr; - ptr += MAXALIGN(sizeof(ParallelBitmapHeapState)); - if (node->ss.ps.instrument && pcxt->nworkers > 0) - sinstrument = (SharedBitmapHeapInstrumentation *) ptr; + pstate = (ParallelBitmapHeapState *) + shm_toc_allocate(pcxt->toc, + MAXALIGN(sizeof(ParallelBitmapHeapState))); pstate->tbmiterator = 0; @@ -509,18 +544,8 @@ ExecBitmapHeapInitializeDSM(BitmapHeapScanState *node, ConditionVariableInit(&pstate->cv); - if (sinstrument) - { - sinstrument->num_workers = pcxt->nworkers; - - /* ensure any unfilled slots will contain zeroes */ - memset(sinstrument->sinstrument, 0, - pcxt->nworkers * sizeof(BitmapHeapScanInstrumentation)); - } - shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, pstate); node->pstate = pstate; - node->sinstrument = sinstrument; } /* ---------------------------------------------------------------- @@ -558,17 +583,72 @@ void ExecBitmapHeapInitializeWorker(BitmapHeapScanState *node, ParallelWorkerContext *pwcxt) { - char *ptr; - Assert(node->ss.ps.state->es_query_dsa != NULL); - ptr = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); + node->pstate = (ParallelBitmapHeapState *) + shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); +} + +/* + * Compute the amount of space we'll need for the shared instrumentation and + * inform pcxt->estimator. + */ +void +ExecBitmapHeapInstrumentEstimate(BitmapHeapScanState *node, + ParallelContext *pcxt) +{ + Size size; - node->pstate = (ParallelBitmapHeapState *) ptr; - ptr += MAXALIGN(sizeof(ParallelBitmapHeapState)); + if (!node->ss.ps.instrument || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedBitmapHeapInstrumentation, sinstrument), + mul_size(pcxt->nworkers, sizeof(BitmapHeapScanInstrumentation))); + shm_toc_estimate_chunk(&pcxt->estimator, size); + shm_toc_estimate_keys(&pcxt->estimator, 1); +} + +/* + * Set up parallel bitmap heap scan instrumentation. + */ +void +ExecBitmapHeapInstrumentInitDSM(BitmapHeapScanState *node, + ParallelContext *pcxt) +{ + Size size; + + if (!node->ss.ps.instrument || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedBitmapHeapInstrumentation, sinstrument), + mul_size(pcxt->nworkers, sizeof(BitmapHeapScanInstrumentation))); + node->sinstrument = + (SharedBitmapHeapInstrumentation *) shm_toc_allocate(pcxt->toc, size); + + /* Each per-worker area must start out as zeroes */ + memset(node->sinstrument, 0, size); + node->sinstrument->num_workers = pcxt->nworkers; + shm_toc_insert(pcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + node->sinstrument); +} + +/* + * Look up and save the location of the shared instrumentation. + */ +void +ExecBitmapHeapInstrumentInitWorker(BitmapHeapScanState *node, + ParallelWorkerContext *pwcxt) +{ + if (!node->ss.ps.instrument) + return; - if (node->ss.ps.instrument) - node->sinstrument = (SharedBitmapHeapInstrumentation *) ptr; + node->sinstrument = (SharedBitmapHeapInstrumentation *) + shm_toc_lookup(pwcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + false); } /* ---------------------------------------------------------------- diff --git a/src/backend/executor/nodeBitmapIndexscan.c b/src/backend/executor/nodeBitmapIndexscan.c index abbb033881a22..7978514e1bc94 100644 --- a/src/backend/executor/nodeBitmapIndexscan.c +++ b/src/backend/executor/nodeBitmapIndexscan.c @@ -3,7 +3,7 @@ * nodeBitmapIndexscan.c * Routines to support bitmapped index scans of relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -23,9 +23,11 @@ #include "access/genam.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeBitmapIndexscan.h" #include "executor/nodeIndexscan.h" #include "miscadmin.h" +#include "nodes/tidbitmap.h" /* ---------------------------------------------------------------- @@ -192,7 +194,7 @@ ExecEndBitmapIndexScan(BitmapIndexScanState *node) { IndexScanInstrumentation *winstrument; - Assert(ParallelWorkerNumber <= node->biss_SharedInfo->num_workers); + Assert(ParallelWorkerNumber < node->biss_SharedInfo->num_workers); winstrument = &node->biss_SharedInfo->winstrument[ParallelWorkerNumber]; /* @@ -201,7 +203,7 @@ ExecEndBitmapIndexScan(BitmapIndexScanState *node) * shutdown on the workers. On rescan it will spin up new workers * which will have a new BitmapIndexScanState and zeroed stats. */ - winstrument->nsearches += node->biss_Instrument.nsearches; + winstrument->nsearches += node->biss_Instrument->nsearches; } /* @@ -272,6 +274,10 @@ ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags) if (eflags & EXEC_FLAG_EXPLAIN_ONLY) return indexstate; + /* Set up instrumentation of bitmap index scans if requested */ + if (estate->es_instrument) + indexstate->biss_Instrument = palloc0_object(IndexScanInstrumentation); + /* Open the index relation. */ lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode; indexstate->biss_RelationDesc = index_open(node->indexid, lockmode); @@ -323,7 +329,7 @@ ExecInitBitmapIndexScan(BitmapIndexScan *node, EState *estate, int eflags) indexstate->biss_ScanDesc = index_beginscan_bitmap(indexstate->biss_RelationDesc, estate->es_snapshot, - &indexstate->biss_Instrument, + indexstate->biss_Instrument, indexstate->biss_NumScanKeys); /* @@ -388,7 +394,9 @@ ExecBitmapIndexScanInitializeDSM(BitmapIndexScanState *node, node->biss_SharedInfo = (SharedIndexScanInstrumentation *) shm_toc_allocate(pcxt->toc, size); - shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, + shm_toc_insert(pcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, node->biss_SharedInfo); /* Each per-worker area must start out as zeroes */ @@ -411,7 +419,10 @@ ExecBitmapIndexScanInitializeWorker(BitmapIndexScanState *node, return; node->biss_SharedInfo = (SharedIndexScanInstrumentation *) - shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); + shm_toc_lookup(pwcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + false); } /* ---------------------------------------------------------------- diff --git a/src/backend/executor/nodeBitmapOr.c b/src/backend/executor/nodeBitmapOr.c index 231760ec93d57..148c80fdae615 100644 --- a/src/backend/executor/nodeBitmapOr.c +++ b/src/backend/executor/nodeBitmapOr.c @@ -3,7 +3,7 @@ * nodeBitmapOr.c * routines to handle BitmapOr nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,7 +29,9 @@ #include "postgres.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeBitmapOr.h" +#include "nodes/tidbitmap.h" #include "miscadmin.h" diff --git a/src/backend/executor/nodeCtescan.c b/src/backend/executor/nodeCtescan.c index e1675f66b4341..d3a8551e801fd 100644 --- a/src/backend/executor/nodeCtescan.c +++ b/src/backend/executor/nodeCtescan.c @@ -3,7 +3,7 @@ * nodeCtescan.c * routines to handle CteScan nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,6 +18,7 @@ #include "executor/executor.h" #include "executor/nodeCtescan.h" #include "miscadmin.h" +#include "utils/tuplestore.h" static TupleTableSlot *CteScanNext(CteScanState *node); @@ -261,7 +262,7 @@ ExecInitCteScan(CteScan *node, EState *estate, int eflags) */ ExecInitScanTupleSlot(estate, &scanstate->ss, ExecGetResultType(scanstate->cteplanstate), - &TTSOpsMinimalTuple); + &TTSOpsMinimalTuple, 0); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeCustom.c b/src/backend/executor/nodeCustom.c index ac2196b64c7ad..b7cc890cd208c 100644 --- a/src/backend/executor/nodeCustom.c +++ b/src/backend/executor/nodeCustom.c @@ -3,7 +3,7 @@ * nodeCustom.c * Routines to handle execution of custom scan node * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * ------------------------------------------------------------------------ @@ -79,14 +79,14 @@ ExecInitCustomScan(CustomScan *cscan, EState *estate, int eflags) TupleDesc scan_tupdesc; scan_tupdesc = ExecTypeFromTL(cscan->custom_scan_tlist); - ExecInitScanTupleSlot(estate, &css->ss, scan_tupdesc, slotOps); + ExecInitScanTupleSlot(estate, &css->ss, scan_tupdesc, slotOps, 0); /* Node's targetlist will contain Vars with varno = INDEX_VAR */ tlistvarno = INDEX_VAR; } else { ExecInitScanTupleSlot(estate, &css->ss, RelationGetDescr(scan_rel), - slotOps); + slotOps, 0); /* Node's targetlist will contain Vars with varno = scanrelid */ tlistvarno = scanrelid; } diff --git a/src/backend/executor/nodeForeignscan.c b/src/backend/executor/nodeForeignscan.c index 9c56c2f3acfe2..6f0daddce074a 100644 --- a/src/backend/executor/nodeForeignscan.c +++ b/src/backend/executor/nodeForeignscan.c @@ -3,7 +3,7 @@ * nodeForeignscan.c * Routines to support scans of foreign tables * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -191,7 +191,7 @@ ExecInitForeignScan(ForeignScan *node, EState *estate, int eflags) scan_tupdesc = ExecTypeFromTL(node->fdw_scan_tlist); ExecInitScanTupleSlot(estate, &scanstate->ss, scan_tupdesc, - &TTSOpsHeapTuple); + &TTSOpsHeapTuple, 0); /* Node's targetlist will contain Vars with varno = INDEX_VAR */ tlistvarno = INDEX_VAR; } @@ -202,7 +202,7 @@ ExecInitForeignScan(ForeignScan *node, EState *estate, int eflags) /* don't trust FDWs to return tuples fulfilling NOT NULL constraints */ scan_tupdesc = CreateTupleDescCopy(RelationGetDescr(currentRelation)); ExecInitScanTupleSlot(estate, &scanstate->ss, scan_tupdesc, - &TTSOpsHeapTuple); + &TTSOpsHeapTuple, 0); /* Node's targetlist will contain Vars with varno = scanrelid */ tlistvarno = scanrelid; } diff --git a/src/backend/executor/nodeFunctionscan.c b/src/backend/executor/nodeFunctionscan.c index 644363582d913..1416f1f09ae5e 100644 --- a/src/backend/executor/nodeFunctionscan.c +++ b/src/backend/executor/nodeFunctionscan.c @@ -3,7 +3,7 @@ * nodeFunctionscan.c * Support routines for scanning RangeFunctions (functions in rangetable). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -27,6 +27,7 @@ #include "funcapi.h" #include "nodes/nodeFuncs.h" #include "utils/memutils.h" +#include "utils/tuplestore.h" /* @@ -333,7 +334,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) */ ExecAssignExprContext(estate, &scanstate->ss.ps); - scanstate->funcstates = palloc(nfuncs * sizeof(FunctionScanPerFuncState)); + scanstate->funcstates = palloc_array(FunctionScanPerFuncState, nfuncs); natts = 0; i = 0; @@ -414,6 +415,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) TupleDescInitEntryCollation(tupdesc, (AttrNumber) 1, exprCollation(funcexpr)); + TupleDescFinalize(tupdesc); } else { @@ -485,6 +487,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) 0); } + TupleDescFinalize(scan_tupdesc); Assert(attno == natts); } @@ -492,7 +495,7 @@ ExecInitFunctionScan(FunctionScan *node, EState *estate, int eflags) * Initialize scan slot and type. */ ExecInitScanTupleSlot(estate, &scanstate->ss, scan_tupdesc, - &TTSOpsMinimalTuple); + &TTSOpsMinimalTuple, 0); /* * Initialize result slot, type and projection. diff --git a/src/backend/executor/nodeGather.c b/src/backend/executor/nodeGather.c index dc7d1830259f5..114693abb3207 100644 --- a/src/backend/executor/nodeGather.c +++ b/src/backend/executor/nodeGather.c @@ -3,7 +3,7 @@ * nodeGather.c * Support routines for scanning a plan via multiple workers. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * A Gather executor launches parallel workers to run multiple copies of a @@ -36,6 +36,7 @@ #include "executor/tqueue.h" #include "miscadmin.h" #include "optimizer/optimizer.h" +#include "storage/latch.h" #include "utils/wait_event.h" diff --git a/src/backend/executor/nodeGatherMerge.c b/src/backend/executor/nodeGatherMerge.c index 15f8459706773..c2ac5e0792cba 100644 --- a/src/backend/executor/nodeGatherMerge.c +++ b/src/backend/executor/nodeGatherMerge.c @@ -3,7 +3,7 @@ * nodeGatherMerge.c * Scan a plan in multiple workers, and do order-preserving merge. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -14,6 +14,7 @@ #include "postgres.h" +#include "access/htup_details.h" #include "executor/executor.h" #include "executor/execParallel.h" #include "executor/nodeGatherMerge.h" @@ -21,6 +22,7 @@ #include "lib/binaryheap.h" #include "miscadmin.h" #include "optimizer/optimizer.h" +#include "utils/sortsupport.h" /* * When we read tuples from workers, it's a good idea to read several at once @@ -144,8 +146,7 @@ ExecInitGatherMerge(GatherMerge *node, EState *estate, int eflags) int i; gm_state->gm_nkeys = node->numCols; - gm_state->gm_sortkeys = - palloc0(sizeof(SortSupportData) * node->numCols); + gm_state->gm_sortkeys = palloc0_array(SortSupportData, node->numCols); for (i = 0; i < node->numCols; i++) { @@ -417,8 +418,7 @@ gather_merge_setup(GatherMergeState *gm_state) for (i = 0; i < nreaders; i++) { /* Allocate the tuple array with length MAX_TUPLE_STORE */ - gm_state->gm_tuple_buffers[i].tuple = - (MinimalTuple *) palloc0(sizeof(MinimalTuple) * MAX_TUPLE_STORE); + gm_state->gm_tuple_buffers[i].tuple = palloc0_array(MinimalTuple, MAX_TUPLE_STORE); /* Initialize tuple slot for worker */ gm_state->gm_slots[i + 1] = diff --git a/src/backend/executor/nodeGroup.c b/src/backend/executor/nodeGroup.c index 05fdd96f83584..3699d8a374659 100644 --- a/src/backend/executor/nodeGroup.c +++ b/src/backend/executor/nodeGroup.c @@ -3,7 +3,7 @@ * nodeGroup.c * Routines to handle group nodes (used for queries with GROUP BY clause). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -23,6 +23,7 @@ #include "postgres.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeGroup.h" #include "miscadmin.h" diff --git a/src/backend/executor/nodeHash.c b/src/backend/executor/nodeHash.c index 8d2201ab67fa5..8825bb6fa23fe 100644 --- a/src/backend/executor/nodeHash.c +++ b/src/backend/executor/nodeHash.c @@ -3,7 +3,7 @@ * nodeHash.c * Routines to hash relations for hashjoin * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -32,14 +32,15 @@ #include "commands/tablespace.h" #include "executor/executor.h" #include "executor/hashjoin.h" +#include "executor/instrument.h" #include "executor/nodeHash.h" #include "executor/nodeHashjoin.h" #include "miscadmin.h" #include "port/pg_bitutils.h" -#include "utils/dynahash.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/wait_event.h" static void ExecHashIncreaseNumBatches(HashJoinTable hashtable); @@ -115,7 +116,7 @@ MultiExecHash(HashState *node) /* must provide our own instrumentation support */ if (node->ps.instrument) - InstrStopNode(node->ps.instrument, node->hashtable->partialTuples); + InstrStopNode(node->ps.instrument, node->hashtable->reportTuples); /* * We do not return the hash table directly because it's not a subtype of @@ -141,6 +142,7 @@ MultiExecPrivateHash(HashState *node) HashJoinTable hashtable; TupleTableSlot *slot; ExprContext *econtext; + double nullTuples = 0; /* * get state info from node @@ -154,8 +156,11 @@ MultiExecPrivateHash(HashState *node) econtext = node->ps.ps_ExprContext; /* - * Get all tuples from the node below the Hash node and insert into the - * hash table (or temp files). + * Get all tuples from the node below the Hash node and insert the + * potentially-matchable ones into the hash table (or temp files). Tuples + * that can't possibly match because they have null join keys are dumped + * into a separate tuplestore, or just summarily discarded if we don't + * need to emit them with null-extension. */ for (;;) { @@ -175,6 +180,7 @@ MultiExecPrivateHash(HashState *node) if (!isnull) { + /* normal case with a non-null join key */ uint32 hashvalue = DatumGetUInt32(hashdatum); int bucketNumber; @@ -184,7 +190,6 @@ MultiExecPrivateHash(HashState *node) /* It's a skew tuple, so put it into that hash table */ ExecHashSkewTableInsert(hashtable, slot, hashvalue, bucketNumber); - hashtable->skewTuples += 1; } else { @@ -193,6 +198,15 @@ MultiExecPrivateHash(HashState *node) } hashtable->totalTuples += 1; } + else if (node->keep_null_tuples) + { + /* null join key, but we must save tuple to be emitted later */ + if (node->null_tuple_store == NULL) + node->null_tuple_store = ExecHashBuildNullTupleStore(hashtable); + tuplestore_puttupleslot(node->null_tuple_store, slot); + nullTuples += 1; + } + /* else we can discard the tuple immediately */ } /* resize the hash table if needed (NTUP_PER_BUCKET exceeded) */ @@ -204,7 +218,8 @@ MultiExecPrivateHash(HashState *node) if (hashtable->spaceUsed > hashtable->spacePeak) hashtable->spacePeak = hashtable->spaceUsed; - hashtable->partialTuples = hashtable->totalTuples; + /* Report total number of tuples output (but not those discarded) */ + hashtable->reportTuples = hashtable->totalTuples + nullTuples; } /* ---------------------------------------------------------------- @@ -223,7 +238,6 @@ MultiExecParallelHash(HashState *node) HashJoinTable hashtable; TupleTableSlot *slot; ExprContext *econtext; - uint32 hashvalue; Barrier *build_barrier; int i; @@ -259,7 +273,7 @@ MultiExecParallelHash(HashState *node) * way, wait for everyone to arrive here so we can proceed. */ BarrierArriveAndWait(build_barrier, WAIT_EVENT_HASH_BUILD_ALLOCATE); - /* Fall through. */ + pg_fallthrough; case PHJ_BUILD_HASH_INNER: @@ -283,6 +297,7 @@ MultiExecParallelHash(HashState *node) for (;;) { bool isnull; + uint32 hashvalue; slot = ExecProcNode(outerNode); if (TupIsNull(slot)) @@ -296,8 +311,20 @@ MultiExecParallelHash(HashState *node) &isnull)); if (!isnull) + { + /* normal case with a non-null join key */ ExecParallelHashTableInsert(hashtable, slot, hashvalue); - hashtable->partialTuples++; + hashtable->reportTuples++; + } + else if (node->keep_null_tuples) + { + /* null join key, but save tuple to be emitted later */ + if (node->null_tuple_store == NULL) + node->null_tuple_store = ExecHashBuildNullTupleStore(hashtable); + tuplestore_puttupleslot(node->null_tuple_store, slot); + hashtable->reportTuples++; + } + /* else we can discard the tuple immediately */ } /* @@ -336,11 +363,13 @@ MultiExecParallelHash(HashState *node) /* * We're not yet attached to a batch. We all agree on the dimensions and - * number of inner tuples (for the empty table optimization). + * number of inner tuples. (In parallel mode, totalTuples isn't used in + * this module, but we must report it for nodeHashjoin.c's empty-table + * optimization.) */ hashtable->curbatch = -1; hashtable->nbuckets = pstate->nbuckets; - hashtable->log2_nbuckets = my_log2(hashtable->nbuckets); + hashtable->log2_nbuckets = pg_ceil_log2_32(hashtable->nbuckets); hashtable->totalTuples = pstate->total_tuples; /* @@ -405,14 +434,10 @@ ExecInitHash(Hash *node, EState *estate, int eflags) Assert(node->plan.qual == NIL); - /* - * Delay initialization of hash_expr until ExecInitHashJoin(). We cannot - * build the ExprState here as we don't yet know the join type we're going - * to be hashing values for and we need to know that before calling - * ExecBuildHash32Expr as the keep_nulls parameter depends on the join - * type. - */ + /* these fields will be filled by ExecInitHashJoin() */ hashstate->hash_expr = NULL; + hashstate->null_tuple_store = NULL; + hashstate->keep_null_tuples = false; return hashstate; } @@ -480,7 +505,7 @@ ExecHashTableCreate(HashState *state) &nbuckets, &nbatch, &num_skew_mcvs); /* nbuckets must be a power of 2 */ - log2_nbuckets = my_log2(nbuckets); + log2_nbuckets = pg_ceil_log2_32(nbuckets); Assert(nbuckets == (1 << log2_nbuckets)); /* @@ -508,7 +533,7 @@ ExecHashTableCreate(HashState *state) hashtable->nbatch_outstart = nbatch; hashtable->growEnabled = true; hashtable->totalTuples = 0; - hashtable->partialTuples = 0; + hashtable->reportTuples = 0; hashtable->skewTuples = 0; hashtable->innerBatchFile = NULL; hashtable->outerBatchFile = NULL; @@ -851,85 +876,91 @@ ExecChooseHashTableSize(double ntuples, int tupwidth, bool useskew, /* * Optimize the total amount of memory consumed by the hash node. * - * The nbatch calculation above focuses on the size of the in-memory hash - * table, assuming no per-batch overhead. Now adjust the number of batches - * and the size of the hash table to minimize total memory consumed by the - * hash node. - * - * Each batch file has a BLCKSZ buffer, and we may need two files per - * batch (inner and outer side). So with enough batches this can be - * significantly more memory than the hashtable itself. + * The nbatch calculation above focuses on the in-memory hash table, + * assuming no per-batch overhead. But each batch may have two files, each + * with a BLCKSZ buffer. For large nbatch values these buffers may use + * significantly more memory than the hash table. * * The total memory usage may be expressed by this formula: * - * (inner_rel_bytes / nbatch) + (2 * nbatch * BLCKSZ) <= hash_table_bytes + * (inner_rel_bytes / nbatch) + (2 * nbatch * BLCKSZ) * * where (inner_rel_bytes / nbatch) is the size of the in-memory hash * table and (2 * nbatch * BLCKSZ) is the amount of memory used by file - * buffers. But for sufficiently large values of inner_rel_bytes value - * there may not be a nbatch value that would make both parts fit into - * hash_table_bytes. + * buffers. * - * In this case we can't enforce the memory limit - we're going to exceed - * it. We can however minimize the impact and use as little memory as - * possible. (We haven't really enforced it before either, as we simply - * ignored the batch files.) + * The nbatch calculation however ignores the second part. And for very + * large inner_rel_bytes, there may be no nbatch that keeps total memory + * usage under the budget (work_mem * hash_mem_multiplier). To deal with + * that, we will adjust nbatch to minimize total memory consumption across + * both the hashtable and file buffers. * - * The formula for total memory usage says that given an inner relation of - * size inner_rel_bytes, we may divide it into an arbitrary number of - * batches. This determines both the size of the in-memory hash table and - * the amount of memory needed for batch files. These two terms work in - * opposite ways - when one decreases, the other increases. + * As we increase the size of the hashtable, the number of batches + * decreases, and the total memory usage follows a U-shaped curve. We find + * the minimum nbatch by "walking back" -- checking if halving nbatch + * would lower the total memory usage. We stop when it no longer helps. * - * For low nbatch values, the hash table takes most of the memory, but at - * some point the batch files start to dominate. If you combine these two - * terms, the memory consumption (for a fixed size of the inner relation) - * has a u-shape, with a minimum at some nbatch value. + * We only reduce the number of batches. Adding batches reduces memory + * usage only when most of the memory is used by the hash table, with + * total memory usage within the limit or not far from it. We don't want + * to start batching when not needed, even if that would reduce memory + * usage. * - * Our goal is to find this nbatch value, minimizing the memory usage. We - * calculate the memory usage with half the batches (i.e. nbatch/2), and - * if it's lower than the current memory usage we know it's better to use - * fewer batches. We repeat this until reducing the number of batches does - * not reduce the memory usage - we found the optimum. We know the optimum - * exists, thanks to the u-shape. + * While growing the hashtable, we also adjust the number of buckets to + * maintain a load factor of NTUP_PER_BUCKET while squeezing tuples back + * from batches into the hashtable. * - * We only want to do this when exceeding the memory limit, not every - * time. The goal is not to minimize memory usage in every case, but to - * minimize the memory usage when we can't stay within the memory limit. + * Note that we can only change nbuckets during initial hashtable sizing. + * Once we start building the hash, nbuckets is fixed (we may still grow + * the hash table). * - * For this reason we only consider reducing the number of batches. We - * could try the opposite direction too, but that would save memory only - * when most of the memory is used by the hash table. And the hash table - * was used for the initial sizing, so we shouldn't be exceeding the - * memory limit too much. We might save memory by using more batches, but - * it would result in spilling more batch files, which does not seem like - * a great trade off. - * - * While growing the hashtable, we also adjust the number of buckets, to - * not have more than one tuple per bucket (load factor 1). We can only do - * this during the initial sizing - once we start building the hash, - * nbucket is fixed. + * We double several parameters (space_allowed, nbuckets, num_skew_mcvs), + * which introduces a risk of overflow. We avoid this by exiting the loop. + * We could do something smarter (e.g. capping nbuckets and continue), but + * the complexity is not worth it. Such cases are extremely rare, and this + * is a best-effort attempt to reduce memory usage. */ - while (nbatch > 0) + while (nbatch > 1) { - /* how much memory are we using with current nbatch value */ - size_t current_space = hash_table_bytes + (2 * nbatch * BLCKSZ); + /* Check that buckets won't overflow MaxAllocSize */ + if (nbuckets > (MaxAllocSize / sizeof(HashJoinTuple) / 2)) + break; - /* how much memory would we use with half the batches */ - size_t new_space = hash_table_bytes * 2 + (nbatch * BLCKSZ); + /* num_skew_mcvs should be less than nbuckets */ + Assert((*num_skew_mcvs) < (INT_MAX / 2)); - /* If the memory usage would not decrease, we found the optimum. */ - if (current_space < new_space) + /* + * Check that space_allowed won't overflow SIZE_MAX. + * + * We don't use hash_table_bytes here, because it does not include the + * skew buckets. And we want to limit the overall memory limit. + */ + if ((*space_allowed) > (SIZE_MAX / 2)) break; /* - * It's better to use half the batches, so do that and adjust the - * nbucket in the opposite direction, and double the allowance. + * Will halving the number of batches and doubling the size of the + * hashtable reduce overall memory usage? + * + * This is the same as (S = space_allowed): + * + * (S + 2 * nbatch * BLCKSZ) < (S * 2 + nbatch * BLCKSZ) + * + * but avoiding intermediate overflow. + */ + if (nbatch < (*space_allowed) / BLCKSZ) + break; + + /* + * MaxAllocSize is sufficiently small that we are not worried about + * overflowing nbuckets. */ - nbatch /= 2; nbuckets *= 2; + *num_skew_mcvs = (*num_skew_mcvs) * 2; *space_allowed = (*space_allowed) * 2; + + nbatch /= 2; } Assert(nbuckets > 0); @@ -995,14 +1026,14 @@ ExecHashIncreaseBatchSize(HashJoinTable hashtable) * How much additional memory would doubling nbatch use? Each batch may * require two buffered files (inner/outer), with a BLCKSZ buffer. */ - size_t batchSpace = (hashtable->nbatch * 2 * BLCKSZ); + size_t batchSpace = (hashtable->nbatch * 2 * (size_t) BLCKSZ); /* * Compare the new space needed for doubling nbatch and for enlarging the * in-memory hash table. If doubling the hash table needs less memory, * just do that. Otherwise, continue with doubling the nbatch. * - * We're either doubling spaceAllowed of batchSpace, so which of those + * We're either doubling spaceAllowed or batchSpace, so which of those * increases the memory usage the least is the same as comparing the * values directly. */ @@ -1325,13 +1356,13 @@ ExecParallelHashIncreaseNumBatches(HashJoinTable hashtable) /* All other participants just flush their tuples to disk. */ ExecParallelHashCloseBatchAccessors(hashtable); } - /* Fall through. */ + pg_fallthrough; case PHJ_GROW_BATCHES_REALLOCATE: /* Wait for the above to be finished. */ BarrierArriveAndWait(&pstate->grow_batches_barrier, WAIT_EVENT_HASH_GROW_BATCHES_REALLOCATE); - /* Fall through. */ + pg_fallthrough; case PHJ_GROW_BATCHES_REPARTITION: /* Make sure that we have the current dimensions and buckets. */ @@ -1344,7 +1375,7 @@ ExecParallelHashIncreaseNumBatches(HashJoinTable hashtable) /* Wait for the above to be finished. */ BarrierArriveAndWait(&pstate->grow_batches_barrier, WAIT_EVENT_HASH_GROW_BATCHES_REPARTITION); - /* Fall through. */ + pg_fallthrough; case PHJ_GROW_BATCHES_DECIDE: @@ -1406,7 +1437,7 @@ ExecParallelHashIncreaseNumBatches(HashJoinTable hashtable) dsa_free(hashtable->area, pstate->old_batches); pstate->old_batches = InvalidDsaPointer; } - /* Fall through. */ + pg_fallthrough; case PHJ_GROW_BATCHES_FINISH: /* Wait for the above to complete. */ @@ -1684,13 +1715,13 @@ ExecParallelHashIncreaseNumBuckets(HashJoinTable hashtable) /* Clear the flag. */ pstate->growth = PHJ_GROWTH_OK; } - /* Fall through. */ + pg_fallthrough; case PHJ_GROW_BUCKETS_REALLOCATE: /* Wait for the above to complete. */ BarrierArriveAndWait(&pstate->grow_buckets_barrier, WAIT_EVENT_HASH_GROW_BUCKETS_REALLOCATE); - /* Fall through. */ + pg_fallthrough; case PHJ_GROW_BUCKETS_REINSERT: /* Reinsert all tuples into the hash table. */ @@ -1762,7 +1793,6 @@ ExecHashTableInsert(HashJoinTable hashtable, */ HashJoinTuple hashTuple; int hashTupleSize; - double ntuples = (hashtable->totalTuples - hashtable->skewTuples); /* Create the HashJoinTuple */ hashTupleSize = HJTUPLE_OVERHEAD + tuple->t_len; @@ -1786,10 +1816,12 @@ ExecHashTableInsert(HashJoinTable hashtable, /* * Increase the (optimal) number of buckets if we just exceeded the * NTUP_PER_BUCKET threshold, but only when there's still a single - * batch. + * batch. Note that totalTuples - skewTuples is a reliable indicator + * of the hash table's size only as long as there's just one batch. */ if (hashtable->nbatch == 1 && - ntuples > (hashtable->nbuckets_optimal * NTUP_PER_BUCKET)) + (hashtable->totalTuples - hashtable->skewTuples) > + (hashtable->nbuckets_optimal * NTUP_PER_BUCKET)) { /* Guard against integer overflow and alloc size overflow */ if (hashtable->nbuckets_optimal <= INT_MAX / 2 && @@ -2616,6 +2648,7 @@ ExecHashSkewTableInsert(HashJoinTable hashtable, Assert(hashTuple != hashTuple->next.unshared); /* Account for space used, and back off if we've used too much */ + hashtable->skewTuples += 1; hashtable->spaceUsed += hashTupleSize; hashtable->spaceUsedSkew += hashTupleSize; if (hashtable->spaceUsed > hashtable->spacePeak) @@ -2708,6 +2741,12 @@ ExecHashRemoveNextSkewBucket(HashJoinTable hashtable) hashtable->spaceUsedSkew -= tupleSize; } + /* + * We must reduce skewTuples, but totalTuples doesn't change since it + * counts both main and skew tuples. + */ + hashtable->skewTuples -= 1; + hashTuple = nextHashTuple; /* allow this loop to be cancellable */ @@ -2748,6 +2787,31 @@ ExecHashRemoveNextSkewBucket(HashJoinTable hashtable) } } +/* + * Build a tuplestore suitable for holding null-keyed input tuples. + * (This function doesn't care whether it's for outer or inner tuples.) + * + * Note that in a parallel hash join, each worker has its own tuplestore(s) + * for these. There's no need to interact with other workers to decide + * what to do with them. So they're always in private storage. + */ +Tuplestorestate * +ExecHashBuildNullTupleStore(HashJoinTable hashtable) +{ + Tuplestorestate *tstore; + MemoryContext oldcxt; + + /* + * We keep the tuplestore in the hashCxt to ensure it won't go away too + * soon. Size it at work_mem/16 so that it doesn't bloat the node's space + * consumption too much. + */ + oldcxt = MemoryContextSwitchTo(hashtable->hashCxt); + tstore = tuplestore_begin_heap(false, false, work_mem / 16); + MemoryContextSwitchTo(oldcxt); + return tstore; +} + /* * Reserve space in the DSM segment for instrumentation data. */ @@ -3499,7 +3563,7 @@ ExecParallelHashTableSetCurrentBatch(HashJoinTable hashtable, int batchno) dsa_get_address(hashtable->area, hashtable->batches[batchno].shared->buckets); hashtable->nbuckets = hashtable->parallel_state->nbuckets; - hashtable->log2_nbuckets = my_log2(hashtable->nbuckets); + hashtable->log2_nbuckets = pg_ceil_log2_32(hashtable->nbuckets); hashtable->current_chunk = NULL; hashtable->current_chunk_shared = InvalidDsaPointer; hashtable->batches[batchno].at_least_one_chunk = false; diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c index 5661ad7683004..0b365d5b4751e 100644 --- a/src/backend/executor/nodeHashjoin.c +++ b/src/backend/executor/nodeHashjoin.c @@ -3,7 +3,7 @@ * nodeHashjoin.c * Routines to handle hash join nodes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -166,11 +166,13 @@ #include "access/parallel.h" #include "executor/executor.h" #include "executor/hashjoin.h" +#include "executor/instrument.h" #include "executor/nodeHash.h" #include "executor/nodeHashjoin.h" #include "miscadmin.h" #include "utils/lsyscache.h" #include "utils/sharedtuplestore.h" +#include "utils/tuplestore.h" #include "utils/wait_event.h" @@ -182,7 +184,9 @@ #define HJ_SCAN_BUCKET 3 #define HJ_FILL_OUTER_TUPLE 4 #define HJ_FILL_INNER_TUPLES 5 -#define HJ_NEED_NEW_BATCH 6 +#define HJ_FILL_OUTER_NULL_TUPLES 6 +#define HJ_FILL_INNER_NULL_TUPLES 7 +#define HJ_NEED_NEW_BATCH 8 /* Returns true if doing null-fill on outer relation */ #define HJ_FILL_OUTER(hjstate) ((hjstate)->hj_NullInnerTupleSlot != NULL) @@ -346,9 +350,16 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) /* * If the inner relation is completely empty, and we're not * doing a left outer join, we can quit without scanning the - * outer relation. + * outer relation. (If the inner relation contains only + * null-keyed tuples that we need to emit, we'll fall through + * and do the outer-relation scan. In principle we could go + * emit those tuples then quit, but it would complicate the + * state machine logic. The case seems rare enough to not be + * worth optimizing.) */ - if (hashtable->totalTuples == 0 && !HJ_FILL_OUTER(node)) + if (hashtable->totalTuples == 0 && + hashNode->null_tuple_store == NULL && + !HJ_FILL_OUTER(node)) { if (parallel) { @@ -395,28 +406,31 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) ExecParallelHashJoinPartitionOuter(node); BarrierArriveAndWait(build_barrier, WAIT_EVENT_HASH_BUILD_HASH_OUTER); - } - else if (BarrierPhase(build_barrier) == PHJ_BUILD_FREE) - { - /* - * If we attached so late that the job is finished and - * the batch state has been freed, we can return - * immediately. - */ - return NULL; + Assert(BarrierPhase(build_barrier) == PHJ_BUILD_RUN); } - /* Each backend should now select a batch to work on. */ - Assert(BarrierPhase(build_barrier) == PHJ_BUILD_RUN); + /* + * Each backend should now select a batch to work on. + * However, if we've already collected some null-keyed + * tuples, dump them first. (That is critical when we + * arrive late enough that no more batches are available; + * otherwise we'd fail to dump those tuples at all.) + */ hashtable->curbatch = -1; - node->hj_JoinState = HJ_NEED_NEW_BATCH; + + if (node->hj_NullOuterTupleStore) + node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES; + else if (hashNode->null_tuple_store) + node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES; + else + node->hj_JoinState = HJ_NEED_NEW_BATCH; continue; } else node->hj_JoinState = HJ_NEED_NEW_OUTER; - /* FALL THRU */ + pg_fallthrough; case HJ_NEED_NEW_OUTER: @@ -440,12 +454,17 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) if (parallel) { /* - * Only one process is currently allow to handle + * Only one process is currently allowed to handle * each batch's unmatched tuples, in a parallel - * join. + * join. However, each process must deal with any + * null-keyed tuples it found. */ if (ExecParallelPrepHashTableForUnmatched(node)) node->hj_JoinState = HJ_FILL_INNER_TUPLES; + else if (node->hj_NullOuterTupleStore) + node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES; + else if (hashNode->null_tuple_store) + node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES; else node->hj_JoinState = HJ_NEED_NEW_BATCH; } @@ -456,7 +475,14 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) } } else - node->hj_JoinState = HJ_NEED_NEW_BATCH; + { + /* might have outer null-keyed tuples to fill */ + Assert(hashNode->null_tuple_store == NULL); + if (node->hj_NullOuterTupleStore) + node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES; + else + node->hj_JoinState = HJ_NEED_NEW_BATCH; + } continue; } @@ -505,7 +531,7 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) /* OK, let's scan the bucket for matches */ node->hj_JoinState = HJ_SCAN_BUCKET; - /* FALL THRU */ + pg_fallthrough; case HJ_SCAN_BUCKET: @@ -632,8 +658,13 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) if (!(parallel ? ExecParallelScanHashTableForUnmatched(node, econtext) : ExecScanHashTableForUnmatched(node, econtext))) { - /* no more unmatched tuples */ - node->hj_JoinState = HJ_NEED_NEW_BATCH; + /* no more unmatched tuples, but maybe there are nulls */ + if (node->hj_NullOuterTupleStore) + node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES; + else if (hashNode->null_tuple_store) + node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES; + else + node->hj_JoinState = HJ_NEED_NEW_BATCH; continue; } @@ -649,6 +680,93 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel) InstrCountFiltered2(node, 1); break; + case HJ_FILL_OUTER_NULL_TUPLES: + + /* + * We have finished a batch, but we are doing left/full join, + * so any null-keyed outer tuples have to be emitted before we + * continue to the next batch. + * + * (We could delay this till the end of the join, but there + * seems little percentage in that.) + * + * We have to use tuplestore_gettupleslot_force because + * hj_OuterTupleSlot may not be able to store a MinimalTuple. + */ + while (tuplestore_gettupleslot_force(node->hj_NullOuterTupleStore, + true, false, + node->hj_OuterTupleSlot)) + { + /* + * Generate a fake join tuple with nulls for the inner + * tuple, and return it if it passes the non-join quals. + */ + econtext->ecxt_outertuple = node->hj_OuterTupleSlot; + econtext->ecxt_innertuple = node->hj_NullInnerTupleSlot; + + if (otherqual == NULL || ExecQual(otherqual, econtext)) + return ExecProject(node->js.ps.ps_ProjInfo); + else + InstrCountFiltered2(node, 1); + + ResetExprContext(econtext); + + /* allow this loop to be cancellable */ + CHECK_FOR_INTERRUPTS(); + } + + /* We don't need the tuplestore any more, so discard it. */ + tuplestore_end(node->hj_NullOuterTupleStore); + node->hj_NullOuterTupleStore = NULL; + + /* Fill inner tuples too if it's a full join, else advance. */ + if (hashNode->null_tuple_store) + node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES; + else + node->hj_JoinState = HJ_NEED_NEW_BATCH; + break; + + case HJ_FILL_INNER_NULL_TUPLES: + + /* + * We have finished a batch, but we are doing + * right/right-anti/full join, so any null-keyed inner tuples + * have to be emitted before we continue to the next batch. + * + * (We could delay this till the end of the join, but there + * seems little percentage in that.) + */ + while (tuplestore_gettupleslot(hashNode->null_tuple_store, + true, false, + node->hj_HashTupleSlot)) + { + /* + * Generate a fake join tuple with nulls for the outer + * tuple, and return it if it passes the non-join quals. + */ + econtext->ecxt_outertuple = node->hj_NullOuterTupleSlot; + econtext->ecxt_innertuple = node->hj_HashTupleSlot; + + if (otherqual == NULL || ExecQual(otherqual, econtext)) + return ExecProject(node->js.ps.ps_ProjInfo); + else + InstrCountFiltered2(node, 1); + + ResetExprContext(econtext); + + /* allow this loop to be cancellable */ + CHECK_FOR_INTERRUPTS(); + } + + /* + * Ideally we'd discard the tuplestore now, but we can't + * because we might need it for rescans. + */ + + /* Now we can advance to the next batch. */ + node->hj_JoinState = HJ_NEED_NEW_BATCH; + break; + case HJ_NEED_NEW_BATCH: /* @@ -831,10 +949,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) /* * Build ExprStates to obtain hash values for either side of the join. - * This must be done here as ExecBuildHash32Expr needs to know how to - * handle NULL inputs and the required handling of that depends on the - * jointype. We don't know the join type in ExecInitHash() and we - * must build the ExprStates before ExecHashTableCreate() so we + * Note: must build the ExprStates before ExecHashTableCreate() so we * properly attribute any SubPlans that exist in the hash expressions * to the correct PlanState. */ @@ -846,7 +961,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) /* * Determine the hash function for each side of the join for the given - * hash operator. + * join operator, and detect whether the join operator is strict. */ foreach(lc, node->hashoperators) { @@ -864,11 +979,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) /* * Build an ExprState to generate the hash value for the expressions - * on the outer of the join. This ExprState must finish generating - * the hash value when HJ_FILL_OUTER() is true. Otherwise, - * ExecBuildHash32Expr will set up the ExprState to abort early if it - * finds a NULL. In these cases, we don't need to store these tuples - * in the hash table as the jointype does not require it. + * on the outer side of the join. */ hjstate->hj_OuterHash = ExecBuildHash32Expr(hjstate->js.ps.ps_ResultTupleDesc, @@ -878,8 +989,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) node->hashkeys, hash_strict, &hjstate->js.ps, - 0, - HJ_FILL_OUTER(hjstate)); + 0); /* As above, but for the inner side of the join */ hashstate->hash_expr = @@ -890,8 +1000,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) hash->hashkeys, hash_strict, &hashstate->ps, - 0, - HJ_FILL_INNER(hjstate)); + 0); + + /* Remember whether we need to save tuples with null join keys */ + hjstate->hj_KeepNullTuples = HJ_FILL_OUTER(hjstate); + hashstate->keep_null_tuples = HJ_FILL_INNER(hjstate); /* * Set up the skew table hash function while we have a record of the @@ -899,7 +1012,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) */ if (OidIsValid(hash->skewTable)) { - hashstate->skew_hashfunction = palloc0(sizeof(FmgrInfo)); + hashstate->skew_hashfunction = palloc0_object(FmgrInfo); hashstate->skew_collation = linitial_oid(node->hashcollations); fmgr_info(outer_hashfuncid[0], hashstate->skew_hashfunction); } @@ -924,6 +1037,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) * initialize hash-specific info */ hjstate->hj_HashTable = NULL; + hjstate->hj_NullOuterTupleStore = NULL; hjstate->hj_FirstOuterTupleSlot = NULL; hjstate->hj_CurHashValue = 0; @@ -947,6 +1061,23 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags) void ExecEndHashJoin(HashJoinState *node) { + HashState *hashNode = castNode(HashState, innerPlanState(node)); + + /* + * Free tuple stores if we made them (must do this before + * ExecHashTableDestroy deletes hashCxt) + */ + if (node->hj_NullOuterTupleStore) + { + tuplestore_end(node->hj_NullOuterTupleStore); + node->hj_NullOuterTupleStore = NULL; + } + if (hashNode->null_tuple_store) + { + tuplestore_end(hashNode->null_tuple_store); + hashNode->null_tuple_store = NULL; + } + /* * Free hash table */ @@ -1015,11 +1146,19 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode, if (!isnull) { + /* normal case with a non-null join key */ /* remember outer relation is not empty for possible rescan */ hjstate->hj_OuterNotEmpty = true; return slot; } + else if (hjstate->hj_KeepNullTuples) + { + /* null join key, but we must save tuple to be emitted later */ + if (hjstate->hj_NullOuterTupleStore == NULL) + hjstate->hj_NullOuterTupleStore = ExecHashBuildNullTupleStore(hashtable); + tuplestore_puttupleslot(hjstate->hj_NullOuterTupleStore, slot); + } /* * That tuple couldn't match because of a NULL, so discard it and @@ -1087,7 +1226,17 @@ ExecParallelHashJoinOuterGetTuple(PlanState *outerNode, &isnull)); if (!isnull) + { + /* normal case with a non-null join key */ return slot; + } + else if (hjstate->hj_KeepNullTuples) + { + /* null join key, but we must save tuple to be emitted later */ + if (hjstate->hj_NullOuterTupleStore == NULL) + hjstate->hj_NullOuterTupleStore = ExecHashBuildNullTupleStore(hashtable); + tuplestore_puttupleslot(hjstate->hj_NullOuterTupleStore, slot); + } /* * That tuple couldn't match because of a NULL, so discard it and @@ -1274,6 +1423,14 @@ ExecParallelHashJoinNewBatch(HashJoinState *hjstate) int start_batchno; int batchno; + /* + * If we are a very slow worker, MultiExecParallelHash could have observed + * build_barrier phase PHJ_BUILD_FREE and not bothered to set up batch + * accessors. In that case we must be done. + */ + if (hashtable->batches == NULL) + return false; + /* * If we were already attached to a batch, remember not to bother checking * it again, and detach from it (possibly freeing the hash table if we are @@ -1313,13 +1470,13 @@ ExecParallelHashJoinNewBatch(HashJoinState *hjstate) if (BarrierArriveAndWait(batch_barrier, WAIT_EVENT_HASH_BATCH_ELECT)) ExecParallelHashTableAlloc(hashtable, batchno); - /* Fall through. */ + pg_fallthrough; case PHJ_BATCH_ALLOCATE: /* Wait for allocation to complete. */ BarrierArriveAndWait(batch_barrier, WAIT_EVENT_HASH_BATCH_ALLOCATE); - /* Fall through. */ + pg_fallthrough; case PHJ_BATCH_LOAD: /* Start (or join in) loading tuples. */ @@ -1339,7 +1496,7 @@ ExecParallelHashJoinNewBatch(HashJoinState *hjstate) sts_end_parallel_scan(inner_tuples); BarrierArriveAndWait(batch_barrier, WAIT_EVENT_HASH_BATCH_LOAD); - /* Fall through. */ + pg_fallthrough; case PHJ_BATCH_PROBE: @@ -1496,6 +1653,17 @@ ExecReScanHashJoin(HashJoinState *node) PlanState *outerPlan = outerPlanState(node); PlanState *innerPlan = innerPlanState(node); + /* + * We're always going to rescan the outer rel, so drop the associated + * null-keys tuplestore; we'll rebuild it during the rescan. (Must do + * this before ExecHashTableDestroy deletes hashCxt.) + */ + if (node->hj_NullOuterTupleStore) + { + tuplestore_end(node->hj_NullOuterTupleStore); + node->hj_NullOuterTupleStore = NULL; + } + /* * In a multi-batch join, we currently have to do rescans the hard way, * primarily because batch temp files may have already been released. But @@ -1505,6 +1673,10 @@ ExecReScanHashJoin(HashJoinState *node) */ if (node->hj_HashTable != NULL) { + HashState *hashNode = castNode(HashState, innerPlan); + + Assert(hashNode->hashtable == node->hj_HashTable); + if (node->hj_HashTable->nbatch == 1 && innerPlan->chgParam == NULL) { @@ -1529,23 +1701,35 @@ ExecReScanHashJoin(HashJoinState *node) */ node->hj_OuterNotEmpty = false; + /* + * Also, rewind inner null-key tuplestore so that we can return + * those tuples again. + */ + if (hashNode->null_tuple_store) + tuplestore_rescan(hashNode->null_tuple_store); + /* ExecHashJoin can skip the BUILD_HASHTABLE step */ node->hj_JoinState = HJ_NEED_NEW_OUTER; } else { /* must destroy and rebuild hash table */ - HashState *hashNode = castNode(HashState, innerPlan); - Assert(hashNode->hashtable == node->hj_HashTable); /* accumulate stats from old hash table, if wanted */ /* (this should match ExecShutdownHash) */ if (hashNode->ps.instrument && !hashNode->hinstrument) - hashNode->hinstrument = (HashInstrumentation *) - palloc0(sizeof(HashInstrumentation)); + hashNode->hinstrument = palloc0_object(HashInstrumentation); if (hashNode->hinstrument) ExecHashAccumInstrumentation(hashNode->hinstrument, hashNode->hashtable); + + /* free inner null-key tuplestore before ExecHashTableDestroy */ + if (hashNode->null_tuple_store) + { + tuplestore_end(hashNode->null_tuple_store); + hashNode->null_tuple_store = NULL; + } + /* for safety, be sure to clear child plan node's pointer too */ hashNode->hashtable = NULL; @@ -1601,7 +1785,6 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate) ExprContext *econtext = hjstate->js.ps.ps_ExprContext; HashJoinTable hashtable = hjstate->hj_HashTable; TupleTableSlot *slot; - uint32 hashvalue; int i; Assert(hjstate->hj_FirstOuterTupleSlot == NULL); @@ -1610,6 +1793,7 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate) for (;;) { bool isnull; + uint32 hashvalue; slot = ExecProcNode(outerState); if (TupIsNull(slot)) @@ -1624,6 +1808,7 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate) if (!isnull) { + /* normal case with a non-null join key */ int batchno; int bucketno; bool shouldFree; @@ -1637,6 +1822,15 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate) if (shouldFree) heap_free_minimal_tuple(mintup); } + else if (hjstate->hj_KeepNullTuples) + { + /* null join key, but we must save tuple to be emitted later */ + if (hjstate->hj_NullOuterTupleStore == NULL) + hjstate->hj_NullOuterTupleStore = ExecHashBuildNullTupleStore(hashtable); + tuplestore_puttupleslot(hjstate->hj_NullOuterTupleStore, slot); + } + /* else we can just discard the tuple immediately */ + CHECK_FOR_INTERRUPTS(); } @@ -1715,6 +1909,7 @@ ExecHashJoinReInitializeDSM(HashJoinState *state, ParallelContext *pcxt) { int plan_node_id = state->js.ps.plan->plan_node_id; ParallelHashJoinState *pstate; + HashState *hashNode; /* Nothing to do if we failed to create a DSM segment. */ if (pcxt->seg == NULL) @@ -1744,6 +1939,20 @@ ExecHashJoinReInitializeDSM(HashJoinState *state, ParallelContext *pcxt) /* Clear any shared batch files. */ SharedFileSetDeleteAll(&pstate->fileset); + /* We'd better clear our local null-key tuplestores, too. */ + if (state->hj_NullOuterTupleStore) + { + tuplestore_end(state->hj_NullOuterTupleStore); + state->hj_NullOuterTupleStore = NULL; + } + hashNode = (HashState *) innerPlanState(state); + if (hashNode->null_tuple_store) + { + tuplestore_end(hashNode->null_tuple_store); + hashNode->null_tuple_store = NULL; + } + + /* Reset build_barrier to PHJ_BUILD_ELECT so we can go around again. */ BarrierInit(&pstate->build_barrier, 0); } diff --git a/src/backend/executor/nodeIncrementalSort.c b/src/backend/executor/nodeIncrementalSort.c index 975b0397e7aa8..1d831049b65aa 100644 --- a/src/backend/executor/nodeIncrementalSort.c +++ b/src/backend/executor/nodeIncrementalSort.c @@ -3,7 +3,7 @@ * nodeIncrementalSort.c * Routines to handle incremental sorting of relations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -102,7 +102,7 @@ if ((node)->shared_info && (node)->am_worker) \ { \ Assert(IsParallelWorker()); \ - Assert(ParallelWorkerNumber <= (node)->shared_info->num_workers); \ + Assert(ParallelWorkerNumber < (node)->shared_info->num_workers); \ instrumentSortedGroup(&(node)->shared_info->sinfo[ParallelWorkerNumber].groupName##GroupInfo, \ (node)->groupName##_state); \ } \ diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c index f464cca9507a5..d52012e8a6987 100644 --- a/src/backend/executor/nodeIndexonlyscan.c +++ b/src/backend/executor/nodeIndexonlyscan.c @@ -3,7 +3,7 @@ * nodeIndexonlyscan.c * Routines to support index-only scans * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -37,6 +37,7 @@ #include "access/visibilitymap.h" #include "catalog/pg_type.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeIndexonlyscan.h" #include "executor/nodeIndexscan.h" #include "miscadmin.h" @@ -92,9 +93,11 @@ IndexOnlyNext(IndexOnlyScanState *node) scandesc = index_beginscan(node->ss.ss_currentRelation, node->ioss_RelationDesc, estate->es_snapshot, - &node->ioss_Instrument, + node->ioss_Instrument, node->ioss_NumScanKeys, - node->ioss_NumOrderByKeys); + node->ioss_NumOrderByKeys, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); node->ioss_ScanDesc = scandesc; @@ -423,7 +426,7 @@ ExecEndIndexOnlyScan(IndexOnlyScanState *node) { IndexScanInstrumentation *winstrument; - Assert(ParallelWorkerNumber <= node->ioss_SharedInfo->num_workers); + Assert(ParallelWorkerNumber < node->ioss_SharedInfo->num_workers); winstrument = &node->ioss_SharedInfo->winstrument[ParallelWorkerNumber]; /* @@ -432,7 +435,7 @@ ExecEndIndexOnlyScan(IndexOnlyScanState *node) * shutdown on the workers. On rescan it will spin up new workers * which will have a new IndexOnlyScanState and zeroed stats. */ - winstrument->nsearches += node->ioss_Instrument.nsearches; + winstrument->nsearches += node->ioss_Instrument->nsearches; } /* @@ -567,7 +570,8 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags) */ tupDesc = ExecTypeFromTL(node->indextlist); ExecInitScanTupleSlot(estate, &indexstate->ss, tupDesc, - &TTSOpsVirtual); + &TTSOpsVirtual, + 0); /* * We need another slot, in a format that's suitable for the table AM, for @@ -576,7 +580,7 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags) indexstate->ioss_TableSlot = ExecAllocTableSlot(&estate->es_tupleTable, RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation)); + table_slot_callbacks(currentRelation), 0); /* * Initialize result type and projection info. The node's targetlist will @@ -604,6 +608,10 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags) if (eflags & EXEC_FLAG_EXPLAIN_ONLY) return indexstate; + /* Set up instrumentation of index-only scans if requested */ + if (estate->es_instrument) + indexstate->ioss_Instrument = palloc0_object(IndexScanInstrumentation); + /* Open the index relation. */ lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode; indexRelation = index_open(node->indexid, lockmode); @@ -693,8 +701,7 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags) * Now create an array to mark the attribute numbers of the keys that * need to be converted from cstring to name. */ - indexstate->ioss_NameCStringAttNums = (AttrNumber *) - palloc(sizeof(AttrNumber) * namecount); + indexstate->ioss_NameCStringAttNums = palloc_array(AttrNumber, namecount); for (int attnum = 0; attnum < indnkeyatts; attnum++) { @@ -729,21 +736,11 @@ ExecIndexOnlyScanEstimate(IndexOnlyScanState *node, ParallelContext *pcxt) { EState *estate = node->ss.ps.state; - bool instrument = (node->ss.ps.instrument != NULL); - bool parallel_aware = node->ss.ps.plan->parallel_aware; - - if (!instrument && !parallel_aware) - { - /* No DSM required by the scan */ - return; - } node->ioss_PscanLen = index_parallelscan_estimate(node->ioss_RelationDesc, node->ioss_NumScanKeys, node->ioss_NumOrderByKeys, - estate->es_snapshot, - instrument, parallel_aware, - pcxt->nworkers); + estate->es_snapshot); shm_toc_estimate_chunk(&pcxt->estimator, node->ioss_PscanLen); shm_toc_estimate_keys(&pcxt->estimator, 1); } @@ -760,36 +757,23 @@ ExecIndexOnlyScanInitializeDSM(IndexOnlyScanState *node, { EState *estate = node->ss.ps.state; ParallelIndexScanDesc piscan; - bool instrument = node->ss.ps.instrument != NULL; - bool parallel_aware = node->ss.ps.plan->parallel_aware; - - if (!instrument && !parallel_aware) - { - /* No DSM required by the scan */ - return; - } piscan = shm_toc_allocate(pcxt->toc, node->ioss_PscanLen); index_parallelscan_initialize(node->ss.ss_currentRelation, node->ioss_RelationDesc, estate->es_snapshot, - instrument, parallel_aware, pcxt->nworkers, - &node->ioss_SharedInfo, piscan); + piscan); shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, piscan); - if (!parallel_aware) - { - /* Only here to initialize SharedInfo in DSM */ - return; - } - node->ioss_ScanDesc = index_beginscan_parallel(node->ss.ss_currentRelation, node->ioss_RelationDesc, - &node->ioss_Instrument, + node->ioss_Instrument, node->ioss_NumScanKeys, node->ioss_NumOrderByKeys, - piscan); + piscan, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); node->ioss_ScanDesc->xs_want_itup = true; node->ioss_VMBuffer = InvalidBuffer; @@ -828,34 +812,18 @@ ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node, ParallelWorkerContext *pwcxt) { ParallelIndexScanDesc piscan; - bool instrument = node->ss.ps.instrument != NULL; - bool parallel_aware = node->ss.ps.plan->parallel_aware; - - if (!instrument && !parallel_aware) - { - /* No DSM required by the scan */ - return; - } piscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); - if (instrument) - node->ioss_SharedInfo = (SharedIndexScanInstrumentation *) - OffsetToPointer(piscan, piscan->ps_offset_ins); - - if (!parallel_aware) - { - /* Only here to set up worker node's SharedInfo */ - return; - } - node->ioss_ScanDesc = index_beginscan_parallel(node->ss.ss_currentRelation, node->ioss_RelationDesc, - &node->ioss_Instrument, + node->ioss_Instrument, node->ioss_NumScanKeys, node->ioss_NumOrderByKeys, - piscan); + piscan, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); node->ioss_ScanDesc->xs_want_itup = true; /* @@ -868,6 +836,73 @@ ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node, node->ioss_OrderByKeys, node->ioss_NumOrderByKeys); } +/* + * Compute the amount of space we'll need for the shared instrumentation and + * inform pcxt->estimator. + */ +void +ExecIndexOnlyScanInstrumentEstimate(IndexOnlyScanState *node, + ParallelContext *pcxt) +{ + Size size; + + if (!node->ss.ps.instrument || pcxt->nworkers == 0) + return; + + /* + * This size calculation is trivial enough that we don't bother saving it + * in the IndexOnlyScanState. We'll recalculate the needed size in + * ExecIndexOnlyScanInstrumentInitDSM(). + */ + size = add_size(offsetof(SharedIndexScanInstrumentation, winstrument), + mul_size(pcxt->nworkers, sizeof(IndexScanInstrumentation))); + shm_toc_estimate_chunk(&pcxt->estimator, size); + shm_toc_estimate_keys(&pcxt->estimator, 1); +} + +/* + * Set up parallel index-only scan instrumentation. + */ +void +ExecIndexOnlyScanInstrumentInitDSM(IndexOnlyScanState *node, + ParallelContext *pcxt) +{ + Size size; + + if (!node->ss.ps.instrument || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedIndexScanInstrumentation, winstrument), + mul_size(pcxt->nworkers, sizeof(IndexScanInstrumentation))); + node->ioss_SharedInfo = + (SharedIndexScanInstrumentation *) shm_toc_allocate(pcxt->toc, size); + + /* Each per-worker area must start out as zeroes */ + memset(node->ioss_SharedInfo, 0, size); + node->ioss_SharedInfo->num_workers = pcxt->nworkers; + shm_toc_insert(pcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + node->ioss_SharedInfo); +} + +/* + * Look up and save the location of the shared instrumentation. + */ +void +ExecIndexOnlyScanInstrumentInitWorker(IndexOnlyScanState *node, + ParallelWorkerContext *pwcxt) +{ + if (!node->ss.ps.instrument) + return; + + node->ioss_SharedInfo = (SharedIndexScanInstrumentation *) + shm_toc_lookup(pwcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + false); +} + /* ---------------------------------------------------------------- * ExecIndexOnlyScanRetrieveInstrumentation * diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c index 7fcaa37fe6253..39f6691ee35ed 100644 --- a/src/backend/executor/nodeIndexscan.c +++ b/src/backend/executor/nodeIndexscan.c @@ -3,7 +3,7 @@ * nodeIndexscan.c * Routines to support indexed scans of relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -34,6 +34,7 @@ #include "access/tableam.h" #include "catalog/pg_am.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeIndexscan.h" #include "lib/pairingheap.h" #include "miscadmin.h" @@ -42,6 +43,7 @@ #include "utils/datum.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/sortsupport.h" /* * When an ordering operator is used, tuples fetched from the index that @@ -65,7 +67,7 @@ static int cmp_orderbyvals(const Datum *adist, const bool *anulls, static int reorderqueue_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg); static void reorderqueue_push(IndexScanState *node, TupleTableSlot *slot, - Datum *orderbyvals, bool *orderbynulls); + const Datum *orderbyvals, const bool *orderbynulls); static HeapTuple reorderqueue_pop(IndexScanState *node); @@ -109,9 +111,11 @@ IndexNext(IndexScanState *node) scandesc = index_beginscan(node->ss.ss_currentRelation, node->iss_RelationDesc, estate->es_snapshot, - &node->iss_Instrument, + node->iss_Instrument, node->iss_NumScanKeys, - node->iss_NumOrderByKeys); + node->iss_NumOrderByKeys, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); node->iss_ScanDesc = scandesc; @@ -205,9 +209,11 @@ IndexNextWithReorder(IndexScanState *node) scandesc = index_beginscan(node->ss.ss_currentRelation, node->iss_RelationDesc, estate->es_snapshot, - &node->iss_Instrument, + node->iss_Instrument, node->iss_NumScanKeys, - node->iss_NumOrderByKeys); + node->iss_NumOrderByKeys, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); node->iss_ScanDesc = scandesc; @@ -443,8 +449,8 @@ static int reorderqueue_cmp(const pairingheap_node *a, const pairingheap_node *b, void *arg) { - ReorderTuple *rta = (ReorderTuple *) a; - ReorderTuple *rtb = (ReorderTuple *) b; + const ReorderTuple *rta = (const ReorderTuple *) a; + const ReorderTuple *rtb = (const ReorderTuple *) b; IndexScanState *node = (IndexScanState *) arg; /* exchange argument order to invert the sort order */ @@ -458,7 +464,7 @@ reorderqueue_cmp(const pairingheap_node *a, const pairingheap_node *b, */ static void reorderqueue_push(IndexScanState *node, TupleTableSlot *slot, - Datum *orderbyvals, bool *orderbynulls) + const Datum *orderbyvals, const bool *orderbynulls) { IndexScanDesc scandesc = node->iss_ScanDesc; EState *estate = node->ss.ps.state; @@ -466,12 +472,10 @@ reorderqueue_push(IndexScanState *node, TupleTableSlot *slot, ReorderTuple *rt; int i; - rt = (ReorderTuple *) palloc(sizeof(ReorderTuple)); + rt = palloc_object(ReorderTuple); rt->htup = ExecCopySlotHeapTuple(slot); - rt->orderbyvals = - (Datum *) palloc(sizeof(Datum) * scandesc->numberOfOrderBys); - rt->orderbynulls = - (bool *) palloc(sizeof(bool) * scandesc->numberOfOrderBys); + rt->orderbyvals = palloc_array(Datum, scandesc->numberOfOrderBys); + rt->orderbynulls = palloc_array(bool, scandesc->numberOfOrderBys); for (i = 0; i < node->iss_NumOrderByKeys; i++) { if (!orderbynulls[i]) @@ -804,7 +808,7 @@ ExecEndIndexScan(IndexScanState *node) { IndexScanInstrumentation *winstrument; - Assert(ParallelWorkerNumber <= node->iss_SharedInfo->num_workers); + Assert(ParallelWorkerNumber < node->iss_SharedInfo->num_workers); winstrument = &node->iss_SharedInfo->winstrument[ParallelWorkerNumber]; /* @@ -813,7 +817,7 @@ ExecEndIndexScan(IndexScanState *node) * shutdown on the workers. On rescan it will spin up new workers * which will have a new IndexOnlyScanState and zeroed stats. */ - winstrument->nsearches += node->iss_Instrument.nsearches; + winstrument->nsearches += node->iss_Instrument->nsearches; } /* @@ -940,7 +944,8 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) */ ExecInitScanTupleSlot(estate, &indexstate->ss, RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation)); + table_slot_callbacks(currentRelation), + TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); /* * Initialize result type and projection. @@ -973,6 +978,10 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) if (eflags & EXEC_FLAG_EXPLAIN_ONLY) return indexstate; + /* Set up instrumentation of index scans if requested */ + if (estate->es_instrument) + indexstate->iss_Instrument = palloc0_object(IndexScanInstrumentation); + /* Open the index relation. */ lockmode = exec_rt_fetch(node->scan.scanrelid, estate)->rellockmode; indexstate->iss_RelationDesc = index_open(node->indexid, lockmode); @@ -1665,21 +1674,11 @@ ExecIndexScanEstimate(IndexScanState *node, ParallelContext *pcxt) { EState *estate = node->ss.ps.state; - bool instrument = node->ss.ps.instrument != NULL; - bool parallel_aware = node->ss.ps.plan->parallel_aware; - - if (!instrument && !parallel_aware) - { - /* No DSM required by the scan */ - return; - } node->iss_PscanLen = index_parallelscan_estimate(node->iss_RelationDesc, node->iss_NumScanKeys, node->iss_NumOrderByKeys, - estate->es_snapshot, - instrument, parallel_aware, - pcxt->nworkers); + estate->es_snapshot); shm_toc_estimate_chunk(&pcxt->estimator, node->iss_PscanLen); shm_toc_estimate_keys(&pcxt->estimator, 1); } @@ -1696,36 +1695,23 @@ ExecIndexScanInitializeDSM(IndexScanState *node, { EState *estate = node->ss.ps.state; ParallelIndexScanDesc piscan; - bool instrument = node->ss.ps.instrument != NULL; - bool parallel_aware = node->ss.ps.plan->parallel_aware; - - if (!instrument && !parallel_aware) - { - /* No DSM required by the scan */ - return; - } piscan = shm_toc_allocate(pcxt->toc, node->iss_PscanLen); index_parallelscan_initialize(node->ss.ss_currentRelation, node->iss_RelationDesc, estate->es_snapshot, - instrument, parallel_aware, pcxt->nworkers, - &node->iss_SharedInfo, piscan); + piscan); shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, piscan); - if (!parallel_aware) - { - /* Only here to initialize SharedInfo in DSM */ - return; - } - node->iss_ScanDesc = index_beginscan_parallel(node->ss.ss_currentRelation, node->iss_RelationDesc, - &node->iss_Instrument, + node->iss_Instrument, node->iss_NumScanKeys, node->iss_NumOrderByKeys, - piscan); + piscan, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); /* * If no run-time keys to calculate or they are ready, go ahead and pass @@ -1762,34 +1748,18 @@ ExecIndexScanInitializeWorker(IndexScanState *node, ParallelWorkerContext *pwcxt) { ParallelIndexScanDesc piscan; - bool instrument = node->ss.ps.instrument != NULL; - bool parallel_aware = node->ss.ps.plan->parallel_aware; - - if (!instrument && !parallel_aware) - { - /* No DSM required by the scan */ - return; - } piscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); - if (instrument) - node->iss_SharedInfo = (SharedIndexScanInstrumentation *) - OffsetToPointer(piscan, piscan->ps_offset_ins); - - if (!parallel_aware) - { - /* Only here to set up worker node's SharedInfo */ - return; - } - node->iss_ScanDesc = index_beginscan_parallel(node->ss.ss_currentRelation, node->iss_RelationDesc, - &node->iss_Instrument, + node->iss_Instrument, node->iss_NumScanKeys, node->iss_NumOrderByKeys, - piscan); + piscan, + ScanRelIsReadOnly(&node->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); /* * If no run-time keys to calculate or they are ready, go ahead and pass @@ -1801,6 +1771,73 @@ ExecIndexScanInitializeWorker(IndexScanState *node, node->iss_OrderByKeys, node->iss_NumOrderByKeys); } +/* + * Compute the amount of space we'll need for the shared instrumentation and + * inform pcxt->estimator. + */ +void +ExecIndexScanInstrumentEstimate(IndexScanState *node, + ParallelContext *pcxt) +{ + Size size; + + if (!node->ss.ps.instrument || pcxt->nworkers == 0) + return; + + /* + * This size calculation is trivial enough that we don't bother saving it + * in the IndexScanState. We'll recalculate the needed size in + * ExecIndexScanInstrumentInitDSM(). + */ + size = add_size(offsetof(SharedIndexScanInstrumentation, winstrument), + mul_size(pcxt->nworkers, sizeof(IndexScanInstrumentation))); + shm_toc_estimate_chunk(&pcxt->estimator, size); + shm_toc_estimate_keys(&pcxt->estimator, 1); +} + +/* + * Set up parallel index scan instrumentation. + */ +void +ExecIndexScanInstrumentInitDSM(IndexScanState *node, + ParallelContext *pcxt) +{ + Size size; + + if (!node->ss.ps.instrument || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedIndexScanInstrumentation, winstrument), + mul_size(pcxt->nworkers, sizeof(IndexScanInstrumentation))); + node->iss_SharedInfo = + (SharedIndexScanInstrumentation *) shm_toc_allocate(pcxt->toc, size); + + /* Each per-worker area must start out as zeroes */ + memset(node->iss_SharedInfo, 0, size); + node->iss_SharedInfo->num_workers = pcxt->nworkers; + shm_toc_insert(pcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + node->iss_SharedInfo); +} + +/* + * Look up and save the location of the shared instrumentation. + */ +void +ExecIndexScanInstrumentInitWorker(IndexScanState *node, + ParallelWorkerContext *pwcxt) +{ + if (!node->ss.ps.instrument) + return; + + node->iss_SharedInfo = (SharedIndexScanInstrumentation *) + shm_toc_lookup(pwcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + false); +} + /* ---------------------------------------------------------------- * ExecIndexScanRetrieveInstrumentation * diff --git a/src/backend/executor/nodeLimit.c b/src/backend/executor/nodeLimit.c index f957da4470e7a..8f75cbbead2a9 100644 --- a/src/backend/executor/nodeLimit.c +++ b/src/backend/executor/nodeLimit.c @@ -3,7 +3,7 @@ * nodeLimit.c * Routines to handle limiting of query results where appropriate * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -68,7 +68,7 @@ ExecLimit(PlanState *pstate) */ recompute_limits(node); - /* FALL THRU */ + pg_fallthrough; case LIMIT_RESCAN: @@ -215,7 +215,7 @@ ExecLimit(PlanState *pstate) } Assert(node->lstate == LIMIT_WINDOWEND_TIES); - /* FALL THRU */ + pg_fallthrough; case LIMIT_WINDOWEND_TIES: if (ScanDirectionIsForward(direction)) diff --git a/src/backend/executor/nodeLockRows.c b/src/backend/executor/nodeLockRows.c index a8afbf93b4882..8d865470780eb 100644 --- a/src/backend/executor/nodeLockRows.c +++ b/src/backend/executor/nodeLockRows.c @@ -3,7 +3,7 @@ * nodeLockRows.c * Routines to handle FOR UPDATE/FOR SHARE row locking * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -344,15 +344,19 @@ ExecInitLockRows(LockRows *node, EState *estate, int eflags) foreach(lc, node->rowMarks) { PlanRowMark *rc = lfirst_node(PlanRowMark, lc); + RangeTblEntry *rte = exec_rt_fetch(rc->rti, estate); ExecRowMark *erm; ExecAuxRowMark *aerm; + /* ignore "parent" rowmarks; they are irrelevant at runtime */ + if (rc->isParent) + continue; + /* - * Ignore "parent" rowmarks, because they are irrelevant at runtime. - * Also ignore the rowmarks belonging to child tables that have been + * Also ignore rowmarks belonging to child tables that have been * pruned in ExecDoInitialPruning(). */ - if (rc->isParent || + if (rte->rtekind == RTE_RELATION && !bms_is_member(rc->rti, estate->es_unpruned_relids)) continue; diff --git a/src/backend/executor/nodeMaterial.c b/src/backend/executor/nodeMaterial.c index 9798bb753651e..e5f387612bcde 100644 --- a/src/backend/executor/nodeMaterial.c +++ b/src/backend/executor/nodeMaterial.c @@ -3,7 +3,7 @@ * nodeMaterial.c * Routines to handle materialization nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -24,6 +24,7 @@ #include "executor/executor.h" #include "executor/nodeMaterial.h" #include "miscadmin.h" +#include "utils/tuplestore.h" /* ---------------------------------------------------------------- * ExecMaterial diff --git a/src/backend/executor/nodeMemoize.c b/src/backend/executor/nodeMemoize.c index 609deb12afb2a..fdca97d7426f1 100644 --- a/src/backend/executor/nodeMemoize.c +++ b/src/backend/executor/nodeMemoize.c @@ -3,7 +3,7 @@ * nodeMemoize.c * Routines to handle caching of results from parameterized nodes * - * Portions Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2021-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -66,6 +66,7 @@ #include "postgres.h" +#include "access/htup_details.h" #include "common/hashfn.h" #include "executor/executor.h" #include "executor/nodeMemoize.h" @@ -554,7 +555,7 @@ cache_lookup(MemoizeState *mstate, bool *found) oldcontext = MemoryContextSwitchTo(mstate->tableContext); /* Allocate a new key */ - entry->key = key = (MemoizeKey *) palloc(sizeof(MemoizeKey)); + entry->key = key = palloc_object(MemoizeKey); key->params = ExecCopySlotMinimalTuple(mstate->probeslot); /* Update the total cache memory utilization */ @@ -633,7 +634,7 @@ cache_store_tuple(MemoizeState *mstate, TupleTableSlot *slot) oldcontext = MemoryContextSwitchTo(mstate->tableContext); - tuple = (MemoizeTuple *) palloc(sizeof(MemoizeTuple)); + tuple = palloc_object(MemoizeTuple); tuple->mintuple = ExecCopySlotMinimalTuple(slot); tuple->next = NULL; @@ -1122,7 +1123,7 @@ ExecEndMemoize(MemoizeState *node) if (node->stats.mem_peak == 0) node->stats.mem_peak = node->mem_used; - Assert(ParallelWorkerNumber <= node->shared_info->num_workers); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; memcpy(si, &node->stats, sizeof(MemoizeInstrumentation)); } diff --git a/src/backend/executor/nodeMergeAppend.c b/src/backend/executor/nodeMergeAppend.c index 405e8f942857f..72eebd50bdf83 100644 --- a/src/backend/executor/nodeMergeAppend.c +++ b/src/backend/executor/nodeMergeAppend.c @@ -3,7 +3,7 @@ * nodeMergeAppend.c * routines to handle MergeAppend nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -43,6 +43,7 @@ #include "executor/nodeMergeAppend.h" #include "lib/binaryheap.h" #include "miscadmin.h" +#include "utils/sortsupport.h" /* * We have one slot for each item in the heap array. We use SlotNumber @@ -122,11 +123,11 @@ ExecInitMergeAppend(MergeAppend *node, EState *estate, int eflags) mergestate->ms_prune_state = NULL; } - mergeplanstates = (PlanState **) palloc(nplans * sizeof(PlanState *)); + mergeplanstates = palloc_array(PlanState *, nplans); mergestate->mergeplans = mergeplanstates; mergestate->ms_nplans = nplans; - mergestate->ms_slots = (TupleTableSlot **) palloc0(sizeof(TupleTableSlot *) * nplans); + mergestate->ms_slots = palloc0_array(TupleTableSlot *, nplans); mergestate->ms_heap = binaryheap_allocate(nplans, heap_compare_slots, mergestate); @@ -174,7 +175,7 @@ ExecInitMergeAppend(MergeAppend *node, EState *estate, int eflags) * initialize sort-key information */ mergestate->ms_nkeys = node->numCols; - mergestate->ms_sortkeys = palloc0(sizeof(SortSupportData) * node->numCols); + mergestate->ms_sortkeys = palloc0_array(SortSupportData, node->numCols); for (i = 0; i < node->numCols; i++) { diff --git a/src/backend/executor/nodeMergejoin.c b/src/backend/executor/nodeMergejoin.c index a233313128acb..f8421a74c75c4 100644 --- a/src/backend/executor/nodeMergejoin.c +++ b/src/backend/executor/nodeMergejoin.c @@ -3,7 +3,7 @@ * nodeMergejoin.c * routines supporting merge joins * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -94,9 +94,11 @@ #include "access/nbtree.h" #include "executor/execdebug.h" +#include "executor/instrument.h" #include "executor/nodeMergejoin.h" #include "miscadmin.h" #include "utils/lsyscache.h" +#include "utils/sortsupport.h" /* diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 2bc89bf84dc3f..4cb057ca4f94c 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -3,7 +3,7 @@ * nodeModifyTable.c * routines to handle ModifyTable nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -54,19 +54,25 @@ #include "access/htup_details.h" #include "access/tableam.h" +#include "access/tupconvert.h" #include "access/xact.h" #include "commands/trigger.h" #include "executor/execPartition.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeModifyTable.h" #include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/optimizer.h" +#include "pgstat.h" #include "rewrite/rewriteHandler.h" +#include "rewrite/rewriteManip.h" #include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/injection_point.h" +#include "utils/rangetypes.h" #include "utils/rel.h" #include "utils/snapmgr.h" @@ -145,12 +151,28 @@ static void ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context, ItemPointer tupleid, TupleTableSlot *oldslot, TupleTableSlot *newslot); +static bool ExecOnConflictLockRow(ModifyTableContext *context, + TupleTableSlot *existing, + ItemPointer conflictTid, + Relation relation, + LockTupleMode lockmode, + bool isUpdate); static bool ExecOnConflictUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer conflictTid, TupleTableSlot *excludedSlot, bool canSetTag, TupleTableSlot **returning); +static bool ExecOnConflictSelect(ModifyTableContext *context, + ResultRelInfo *resultRelInfo, + ItemPointer conflictTid, + TupleTableSlot *excludedSlot, + bool canSetTag, + TupleTableSlot **returning); +static void ExecForPortionOfLeftovers(ModifyTableContext *context, + EState *estate, + ResultRelInfo *resultRelInfo, + ItemPointer tupleid); static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate, EState *estate, PartitionTupleRouting *proute, @@ -173,6 +195,9 @@ static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context, static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, bool canSetTag); +static void ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate); +static void fireBSTriggers(ModifyTableState *node); +static void fireASTriggers(ModifyTableState *node); /* @@ -272,7 +297,7 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList) * * context: context for the ModifyTable operation * resultRelInfo: current result rel - * cmdType: operation/merge action performed (INSERT, UPDATE, or DELETE) + * isDelete: true if the operation/merge action is a DELETE * oldSlot: slot holding old tuple deleted or updated * newSlot: slot holding new tuple inserted or updated * planSlot: slot holding tuple returned by top subplan node @@ -281,12 +306,15 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList) * econtext's scan tuple and its old & new tuples are not needed (FDW direct- * modify is disabled if the RETURNING list refers to any OLD/NEW values). * + * Note: For the SELECT path of INSERT ... ON CONFLICT DO SELECT, oldSlot and + * newSlot are both the existing tuple, since it's not changed. + * * Returns a slot holding the result tuple */ static TupleTableSlot * ExecProcessReturning(ModifyTableContext *context, ResultRelInfo *resultRelInfo, - CmdType cmdType, + bool isDelete, TupleTableSlot *oldSlot, TupleTableSlot *newSlot, TupleTableSlot *planSlot) @@ -296,23 +324,17 @@ ExecProcessReturning(ModifyTableContext *context, ExprContext *econtext = projectReturning->pi_exprContext; /* Make tuple and any needed join variables available to ExecProject */ - switch (cmdType) + if (isDelete) { - case CMD_INSERT: - case CMD_UPDATE: - /* return new tuple by default */ - if (newSlot) - econtext->ecxt_scantuple = newSlot; - break; - - case CMD_DELETE: - /* return old tuple by default */ - if (oldSlot) - econtext->ecxt_scantuple = oldSlot; - break; - - default: - elog(ERROR, "unrecognized commandType: %d", (int) cmdType); + /* return old tuple by default */ + if (oldSlot) + econtext->ecxt_scantuple = oldSlot; + } + else + { + /* return new tuple by default */ + if (newSlot) + econtext->ecxt_scantuple = newSlot; } econtext->ecxt_outertuple = planSlot; @@ -579,8 +601,8 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo, oldContext = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); - values = palloc(sizeof(*values) * natts); - nulls = palloc(sizeof(*nulls) * natts); + values = palloc_array(Datum, natts); + nulls = palloc_array(bool, natts); slot_getallattrs(slot); memcpy(nulls, slot->tts_isnull, sizeof(*nulls) * natts); @@ -960,10 +982,8 @@ ExecInsert(ModifyTableContext *context, if (resultRelInfo->ri_Slots == NULL) { - resultRelInfo->ri_Slots = palloc(sizeof(TupleTableSlot *) * - resultRelInfo->ri_BatchSize); - resultRelInfo->ri_PlanSlots = palloc(sizeof(TupleTableSlot *) * - resultRelInfo->ri_BatchSize); + resultRelInfo->ri_Slots = palloc_array(TupleTableSlot *, resultRelInfo->ri_BatchSize); + resultRelInfo->ri_PlanSlots = palloc_array(TupleTableSlot *, resultRelInfo->ri_BatchSize); } /* @@ -1158,6 +1178,26 @@ ExecInsert(ModifyTableContext *context, else goto vlock; } + else if (onconflict == ONCONFLICT_SELECT) + { + /* + * In case of ON CONFLICT DO SELECT, optionally lock the + * conflicting tuple, fetch it and project RETURNING on + * it. Be prepared to retry if locking fails because of a + * concurrent UPDATE/DELETE to the conflict tuple. + */ + TupleTableSlot *returning = NULL; + + if (ExecOnConflictSelect(context, resultRelInfo, + &conflictTid, slot, canSetTag, + &returning)) + { + InstrCountTuples2(&mtstate->ps, 1); + return returning; + } + else + goto vlock; + } else { /* @@ -1185,6 +1225,7 @@ ExecInsert(ModifyTableContext *context, * if we're going to go ahead with the insertion, instead of * waiting for the whole transaction to complete. */ + INJECTION_POINT("exec-insert-before-insert-speculative", NULL); specToken = SpeculativeInsertionLockAcquire(GetCurrentTransactionId()); /* insert the tuple, with the speculative token */ @@ -1196,10 +1237,9 @@ ExecInsert(ModifyTableContext *context, /* insert index entries for tuple */ recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - slot, estate, false, true, - &specConflict, - arbiterIndexes, - false); + estate, EIIT_NO_DUPE_ERROR, + slot, arbiterIndexes, + &specConflict); /* adjust the tuple's state accordingly */ table_tuple_complete_speculative(resultRelationDesc, slot, @@ -1236,10 +1276,9 @@ ExecInsert(ModifyTableContext *context, /* insert index entries for tuple */ if (resultRelInfo->ri_NumIndices > 0) - recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - slot, estate, false, - false, NULL, NIL, - false); + recheckIndexes = ExecInsertIndexTuples(resultRelInfo, estate, + 0, slot, NIL, + NULL); } } @@ -1328,7 +1367,7 @@ ExecInsert(ModifyTableContext *context, } } - result = ExecProcessReturning(context, resultRelInfo, CMD_INSERT, + result = ExecProcessReturning(context, resultRelInfo, false, oldSlot, slot, planSlot); /* @@ -1354,6 +1393,246 @@ ExecInsert(ModifyTableContext *context, return result; } +/* ---------------------------------------------------------------- + * ExecForPortionOfLeftovers + * + * Insert tuples for the untouched portion of a row in a FOR + * PORTION OF UPDATE/DELETE + * ---------------------------------------------------------------- + */ +static void +ExecForPortionOfLeftovers(ModifyTableContext *context, + EState *estate, + ResultRelInfo *resultRelInfo, + ItemPointer tupleid) +{ + ModifyTableState *mtstate = context->mtstate; + ModifyTable *node = (ModifyTable *) mtstate->ps.plan; + ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf; + AttrNumber rangeAttno; + Datum oldRange; + TypeCacheEntry *typcache; + ForPortionOfState *fpoState; + TupleTableSlot *oldtupleSlot; + TupleTableSlot *leftoverSlot; + TupleConversionMap *map = NULL; + HeapTuple oldtuple = NULL; + CmdType oldOperation; + TransitionCaptureState *oldTcs; + FmgrInfo flinfo; + PgStat_FunctionCallUsage fcusage; + ReturnSetInfo rsi; + bool didInit = false; + bool shouldFree = false; + + LOCAL_FCINFO(fcinfo, 2); + + if (!resultRelInfo->ri_forPortionOf) + { + /* + * If we don't have a ForPortionOfState yet, we must be a partition + * child being hit for the first time. Make a copy from the root, with + * our own TupleTableSlot. We do this lazily so that we don't pay the + * price of unused partitions. + */ + ForPortionOfState *leafState = makeNode(ForPortionOfState); + + if (!mtstate->rootResultRelInfo) + elog(ERROR, "no root relation but ri_forPortionOf is uninitialized"); + + fpoState = mtstate->rootResultRelInfo->ri_forPortionOf; + Assert(fpoState); + + leafState->fp_rangeName = fpoState->fp_rangeName; + leafState->fp_rangeType = fpoState->fp_rangeType; + leafState->fp_rangeAttno = fpoState->fp_rangeAttno; + leafState->fp_targetRange = fpoState->fp_targetRange; + leafState->fp_Leftover = fpoState->fp_Leftover; + /* Each partition needs a slot matching its tuple descriptor */ + leafState->fp_Existing = + table_slot_create(resultRelInfo->ri_RelationDesc, + &mtstate->ps.state->es_tupleTable); + + resultRelInfo->ri_forPortionOf = leafState; + } + fpoState = resultRelInfo->ri_forPortionOf; + oldtupleSlot = fpoState->fp_Existing; + leftoverSlot = fpoState->fp_Leftover; + + /* + * Get the old pre-UPDATE/DELETE tuple. We will use its range to compute + * untouched parts of history, and if necessary we will insert copies with + * truncated start/end times. + * + * We have already locked the tuple in ExecUpdate/ExecDelete, and it has + * passed EvalPlanQual. This ensures that concurrent updates in READ + * COMMITTED can't insert conflicting temporal leftovers. + * + * It does *not* protect against concurrent update/deletes overlooking + * each others' leftovers though. See our isolation tests for details + * about that and a viable workaround. + */ + if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, oldtupleSlot)) + elog(ERROR, "failed to fetch tuple for FOR PORTION OF"); + + /* + * Get the old range of the record being updated/deleted. Must read with + * the attno of the leaf partition being updated. + */ + + rangeAttno = forPortionOf->rangeVar->varattno; + if (resultRelInfo->ri_RootResultRelInfo) + map = ExecGetChildToRootMap(resultRelInfo); + if (map != NULL) + rangeAttno = map->attrMap->attnums[rangeAttno - 1]; + slot_getallattrs(oldtupleSlot); + + if (oldtupleSlot->tts_isnull[rangeAttno - 1]) + elog(ERROR, "found a NULL range in a temporal table"); + oldRange = oldtupleSlot->tts_values[rangeAttno - 1]; + + /* + * Get the range's type cache entry. This is worth caching for the whole + * UPDATE/DELETE as range functions do. + */ + + typcache = fpoState->fp_leftoverstypcache; + if (typcache == NULL) + { + typcache = lookup_type_cache(forPortionOf->rangeType, 0); + fpoState->fp_leftoverstypcache = typcache; + } + + /* + * Get the ranges to the left/right of the targeted range. We call a SETOF + * support function and insert as many temporal leftovers as it gives us. + * Although rangetypes have 0/1/2 leftovers, multiranges have 0/1, and + * other types may have more. + */ + + fmgr_info(forPortionOf->withoutPortionProc, &flinfo); + rsi.type = T_ReturnSetInfo; + rsi.econtext = mtstate->ps.ps_ExprContext; + rsi.expectedDesc = NULL; + rsi.allowedModes = (int) (SFRM_ValuePerCall); + rsi.returnMode = SFRM_ValuePerCall; + /* isDone is filled below */ + rsi.setResult = NULL; + rsi.setDesc = NULL; + + InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, (Node *) &rsi); + fcinfo->args[0].value = oldRange; + fcinfo->args[0].isnull = false; + fcinfo->args[1].value = fpoState->fp_targetRange; + fcinfo->args[1].isnull = false; + + /* + * If there are partitions, we must insert into the root table, so we get + * tuple routing. We already set up leftoverSlot with the root tuple + * descriptor. + */ + if (resultRelInfo->ri_RootResultRelInfo) + resultRelInfo = resultRelInfo->ri_RootResultRelInfo; + + /* + * Insert a leftover for each value returned by the without_portion helper + * function + */ + while (true) + { + Datum leftover; + + /* Call the function one time */ + pgstat_init_function_usage(fcinfo, &fcusage); + + fcinfo->isnull = false; + rsi.isDone = ExprSingleResult; + leftover = FunctionCallInvoke(fcinfo); + + pgstat_end_function_usage(&fcusage, + rsi.isDone != ExprMultipleResult); + + if (rsi.returnMode != SFRM_ValuePerCall) + elog(ERROR, "without_portion function violated function call protocol"); + + /* Are we done? */ + if (rsi.isDone == ExprEndResult) + break; + + if (fcinfo->isnull) + elog(ERROR, "got a null from without_portion function"); + + /* + * Does the new Datum violate domain checks? Row-level CHECK + * constraints are validated by ExecInsert, so we don't need to do + * anything here for those. + */ + if (forPortionOf->isDomain) + domain_check(leftover, false, forPortionOf->rangeVar->vartype, NULL, NULL); + + if (!didInit) + { + /* + * Make a copy of the pre-UPDATE row. Then we'll overwrite the + * range column below. Convert oldtuple to the base table's format + * if necessary. We need to insert temporal leftovers through the + * root partition so they get routed correctly. + */ + if (map != NULL) + { + leftoverSlot = execute_attr_map_slot(map->attrMap, + oldtupleSlot, + leftoverSlot); + } + else + { + oldtuple = ExecFetchSlotHeapTuple(oldtupleSlot, false, &shouldFree); + ExecForceStoreHeapTuple(oldtuple, leftoverSlot, false); + } + + /* + * Save some mtstate things so we can restore them below. XXX: + * Should we create our own ModifyTableState instead? + */ + oldOperation = mtstate->operation; + mtstate->operation = CMD_INSERT; + oldTcs = mtstate->mt_transition_capture; + + didInit = true; + } + + leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover; + leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false; + ExecMaterializeSlot(leftoverSlot); + + /* + * The standard says that each temporal leftover should execute its + * own INSERT statement, firing all statement and row triggers, but + * skipping insert permission checks. Therefore we give each insert + * its own transition table. If we just push & pop a new trigger level + * for each insert, we get exactly what we need. + * + * We have to make sure that the inserts don't add to the ROW_COUNT + * diagnostic or the command tag, so we pass false for canSetTag. + */ + AfterTriggerBeginQuery(); + ExecSetupTransitionCaptureState(mtstate, estate); + fireBSTriggers(mtstate); + ExecInsert(context, resultRelInfo, leftoverSlot, false, NULL, NULL); + fireASTriggers(mtstate); + AfterTriggerEndQuery(estate); + } + + if (didInit) + { + mtstate->operation = oldOperation; + mtstate->mt_transition_capture = oldTcs; + + if (shouldFree) + heap_freetuple(oldtuple); + } +} + /* ---------------------------------------------------------------- * ExecBatchInsert * @@ -1473,7 +1752,8 @@ ExecDeletePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, return ExecBRDeleteTriggers(context->estate, context->epqstate, resultRelInfo, tupleid, oldtuple, - epqreturnslot, result, &context->tmfd); + epqreturnslot, result, &context->tmfd, + context->mtstate->operation == CMD_MERGE); } return true; @@ -1491,14 +1771,18 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer tupleid, bool changingPart) { EState *estate = context->estate; + uint32 options = 0; + + if (changingPart) + options |= TABLE_DELETE_CHANGING_PARTITION; return table_tuple_delete(resultRelInfo->ri_RelationDesc, tupleid, estate->es_output_cid, + options, estate->es_snapshot, estate->es_crosscheck_snapshot, true /* wait for commit */ , - &context->tmfd, - changingPart); + &context->tmfd); } /* @@ -1506,7 +1790,8 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * * Closing steps of tuple deletion; this invokes AFTER FOR EACH ROW triggers, * including the UPDATE triggers if the deletion is being done as part of a - * cross-partition tuple move. + * cross-partition tuple move. It also inserts temporal leftovers from a + * DELETE FOR PORTION OF. */ static void ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, @@ -1539,6 +1824,10 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ar_delete_trig_tcs = NULL; } + /* Compute temporal leftovers in FOR PORTION OF */ + if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf) + ExecForPortionOfLeftovers(context, estate, resultRelInfo, tupleid); + /* AFTER ROW DELETE Triggers */ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, ar_delete_trig_tcs, changingPart); @@ -1888,7 +2177,7 @@ ExecDelete(ModifyTableContext *context, return NULL; } - rslot = ExecProcessReturning(context, resultRelInfo, CMD_DELETE, + rslot = ExecProcessReturning(context, resultRelInfo, true, slot, NULL, context->planSlot); /* @@ -1964,7 +2253,10 @@ ExecCrossPartitionUpdate(ModifyTableContext *context, if (resultRelInfo == mtstate->rootResultRelInfo) ExecPartitionCheckEmitError(resultRelInfo, slot, estate); - /* Initialize tuple routing info if not already done. */ + /* + * Initialize tuple routing info if not already done. Note whatever we do + * here must be done in ExecInitModifyTable for FOR PORTION OF as well. + */ if (mtstate->mt_partition_tuple_routing == NULL) { Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc; @@ -2116,7 +2408,8 @@ ExecUpdatePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, return ExecBRUpdateTriggers(context->estate, context->epqstate, resultRelInfo, tupleid, oldtuple, slot, - result, &context->tmfd); + result, &context->tmfd, + context->mtstate->operation == CMD_MERGE); } return true; @@ -2299,6 +2592,7 @@ ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, */ result = table_tuple_update(resultRelationDesc, tupleid, slot, estate->es_output_cid, + 0, estate->es_snapshot, estate->es_crosscheck_snapshot, true /* wait for commit */ , @@ -2312,7 +2606,8 @@ ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * ExecUpdateEpilogue -- subroutine for ExecUpdate * * Closing steps of updating a tuple. Must be called if ExecUpdateAct - * returns indicating that the tuple was updated. + * returns indicating that the tuple was updated. It also inserts temporal + * leftovers from an UPDATE FOR PORTION OF. */ static void ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, @@ -2324,11 +2619,19 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, /* insert index entries for tuple if necessary */ if (resultRelInfo->ri_NumIndices > 0 && (updateCxt->updateIndexes != TU_None)) - recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - slot, context->estate, - true, false, - NULL, NIL, - (updateCxt->updateIndexes == TU_Summarizing)); + { + uint32 flags = EIIT_IS_UPDATE; + + if (updateCxt->updateIndexes == TU_Summarizing) + flags |= EIIT_ONLY_SUMMARIZING; + recheckIndexes = ExecInsertIndexTuples(resultRelInfo, context->estate, + flags, slot, NIL, + NULL); + } + + /* Compute temporal leftovers in FOR PORTION OF */ + if (((ModifyTable *) context->mtstate->ps.plan)->forPortionOf) + ExecForPortionOfLeftovers(context, context->estate, resultRelInfo, tupleid); /* AFTER ROW UPDATE Triggers */ ExecARUpdateTriggers(context->estate, resultRelInfo, @@ -2689,56 +2992,37 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) - return ExecProcessReturning(context, resultRelInfo, CMD_UPDATE, + return ExecProcessReturning(context, resultRelInfo, false, oldSlot, slot, context->planSlot); return NULL; } /* - * ExecOnConflictUpdate --- execute UPDATE of INSERT ON CONFLICT DO UPDATE + * ExecOnConflictLockRow --- lock the row for ON CONFLICT DO SELECT/UPDATE * - * Try to lock tuple for update as part of speculative insertion. If - * a qual originating from ON CONFLICT DO UPDATE is satisfied, update - * (but still lock row, even though it may not satisfy estate's - * snapshot). + * Try to lock tuple for update as part of speculative insertion for ON + * CONFLICT DO UPDATE or ON CONFLICT DO SELECT FOR UPDATE/SHARE. * - * Returns true if we're done (with or without an update), or false if - * the caller must retry the INSERT from scratch. + * Returns true if the row is successfully locked, or false if the caller must + * retry the INSERT from scratch. */ static bool -ExecOnConflictUpdate(ModifyTableContext *context, - ResultRelInfo *resultRelInfo, - ItemPointer conflictTid, - TupleTableSlot *excludedSlot, - bool canSetTag, - TupleTableSlot **returning) +ExecOnConflictLockRow(ModifyTableContext *context, + TupleTableSlot *existing, + ItemPointer conflictTid, + Relation relation, + LockTupleMode lockmode, + bool isUpdate) { - ModifyTableState *mtstate = context->mtstate; - ExprContext *econtext = mtstate->ps.ps_ExprContext; - Relation relation = resultRelInfo->ri_RelationDesc; - ExprState *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause; - TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing; TM_FailureData tmfd; - LockTupleMode lockmode; TM_Result test; Datum xminDatum; TransactionId xmin; bool isnull; /* - * Parse analysis should have blocked ON CONFLICT for all system - * relations, which includes these. There's no fundamental obstacle to - * supporting this; we'd just need to handle LOCKTAG_TUPLE like the other - * ExecUpdate() caller. - */ - Assert(!resultRelInfo->ri_needLockTagTuple); - - /* Determine lock mode to use */ - lockmode = ExecUpdateLockMode(context->estate, resultRelInfo); - - /* - * Lock tuple for update. Don't follow updates when tuple cannot be + * Lock tuple with lockmode. Don't follow updates when tuple cannot be * locked without doing so. A row locking conflict here means our * previous conclusion that the tuple is conclusively committed is not * true anymore. @@ -2783,7 +3067,7 @@ ExecOnConflictUpdate(ModifyTableContext *context, (errcode(ERRCODE_CARDINALITY_VIOLATION), /* translator: %s is a SQL command name */ errmsg("%s command cannot affect row a second time", - "ON CONFLICT DO UPDATE"), + isUpdate ? "ON CONFLICT DO UPDATE" : "ON CONFLICT DO SELECT"), errhint("Ensure that no rows proposed for insertion within the same command have duplicate constrained values."))); /* This shouldn't happen */ @@ -2806,14 +3090,6 @@ ExecOnConflictUpdate(ModifyTableContext *context, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); - /* - * As long as we don't support an UPDATE of INSERT ON CONFLICT for - * a partitioned table we shouldn't reach to a case where tuple to - * be lock is moved to another partition due to concurrent update - * of the partition key. - */ - Assert(!ItemPointerIndicatesMovedPartitions(&tmfd.ctid)); - /* * Tell caller to try again from the very start. * @@ -2831,7 +3107,6 @@ ExecOnConflictUpdate(ModifyTableContext *context, errmsg("could not serialize access due to concurrent delete"))); /* see TM_Updated case */ - Assert(!ItemPointerIndicatesMovedPartitions(&tmfd.ctid)); ExecClearTuple(existing); return false; @@ -2840,6 +3115,50 @@ ExecOnConflictUpdate(ModifyTableContext *context, } /* Success, the tuple is locked. */ + return true; +} + +/* + * ExecOnConflictUpdate --- execute UPDATE of INSERT ON CONFLICT DO UPDATE + * + * Try to lock tuple for update as part of speculative insertion. If + * a qual originating from ON CONFLICT DO UPDATE is satisfied, update + * (but still lock row, even though it may not satisfy estate's + * snapshot). + * + * Returns true if we're done (with or without an update), or false if + * the caller must retry the INSERT from scratch. + */ +static bool +ExecOnConflictUpdate(ModifyTableContext *context, + ResultRelInfo *resultRelInfo, + ItemPointer conflictTid, + TupleTableSlot *excludedSlot, + bool canSetTag, + TupleTableSlot **returning) +{ + ModifyTableState *mtstate = context->mtstate; + ExprContext *econtext = mtstate->ps.ps_ExprContext; + Relation relation = resultRelInfo->ri_RelationDesc; + ExprState *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause; + TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing; + LockTupleMode lockmode; + + /* + * Parse analysis should have blocked ON CONFLICT for all system + * relations, which includes these. There's no fundamental obstacle to + * supporting this; we'd just need to handle LOCKTAG_TUPLE like the other + * ExecUpdate() caller. + */ + Assert(!resultRelInfo->ri_needLockTagTuple); + + /* Determine lock mode to use */ + lockmode = ExecUpdateLockMode(context->estate, resultRelInfo); + + /* Lock tuple for update */ + if (!ExecOnConflictLockRow(context, existing, conflictTid, + resultRelInfo->ri_RelationDesc, lockmode, true)) + return false; /* * Verify that the tuple is visible to our MVCC snapshot if the current @@ -2881,11 +3200,13 @@ ExecOnConflictUpdate(ModifyTableContext *context, * security barrier quals (if any), enforced here as RLS checks/WCOs. * * The rewriter creates UPDATE RLS checks/WCOs for UPDATE security - * quals, and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK, - * but that's almost the extent of its special handling for ON - * CONFLICT DO UPDATE. + * quals, and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK. + * Since SELECT permission on the target table is always required for + * INSERT ... ON CONFLICT DO UPDATE, the rewriter also adds SELECT RLS + * checks/WCOs for SELECT security quals, using WCOs of the same kind, + * and this check enforces them too. * - * The rewriter will also have associated UPDATE applicable straight + * The rewriter will also have associated UPDATE-applicable straight * RLS checks/WCOs for the benefit of the ExecUpdate() call that * follows. INSERTs and UPDATEs naturally have mutually exclusive WCO * kinds, so there is no danger of spurious over-enforcement in the @@ -2930,6 +3251,141 @@ ExecOnConflictUpdate(ModifyTableContext *context, return true; } +/* + * ExecOnConflictSelect --- execute SELECT of INSERT ON CONFLICT DO SELECT + * + * If SELECT FOR UPDATE/SHARE is specified, try to lock tuple as part of + * speculative insertion. If a qual originating from ON CONFLICT DO SELECT is + * satisfied, select (but still lock row, even though it may not satisfy + * estate's snapshot). + * + * Returns true if we're done (with or without a select), or false if the + * caller must retry the INSERT from scratch. + */ +static bool +ExecOnConflictSelect(ModifyTableContext *context, + ResultRelInfo *resultRelInfo, + ItemPointer conflictTid, + TupleTableSlot *excludedSlot, + bool canSetTag, + TupleTableSlot **returning) +{ + ModifyTableState *mtstate = context->mtstate; + ExprContext *econtext = mtstate->ps.ps_ExprContext; + Relation relation = resultRelInfo->ri_RelationDesc; + ExprState *onConflictSelectWhere = resultRelInfo->ri_onConflict->oc_WhereClause; + TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing; + LockClauseStrength lockStrength = resultRelInfo->ri_onConflict->oc_LockStrength; + + /* + * Parse analysis should have blocked ON CONFLICT for all system + * relations, which includes these. There's no fundamental obstacle to + * supporting this; we'd just need to handle LOCKTAG_TUPLE appropriately. + */ + Assert(!resultRelInfo->ri_needLockTagTuple); + + /* Fetch/lock existing tuple, according to the requested lock strength */ + if (lockStrength == LCS_NONE) + { + if (!table_tuple_fetch_row_version(relation, + conflictTid, + SnapshotAny, + existing)) + elog(ERROR, "failed to fetch conflicting tuple for ON CONFLICT"); + } + else + { + LockTupleMode lockmode; + + switch (lockStrength) + { + case LCS_FORKEYSHARE: + lockmode = LockTupleKeyShare; + break; + case LCS_FORSHARE: + lockmode = LockTupleShare; + break; + case LCS_FORNOKEYUPDATE: + lockmode = LockTupleNoKeyExclusive; + break; + case LCS_FORUPDATE: + lockmode = LockTupleExclusive; + break; + default: + elog(ERROR, "Unexpected lock strength %d", (int) lockStrength); + } + + if (!ExecOnConflictLockRow(context, existing, conflictTid, + resultRelInfo->ri_RelationDesc, lockmode, false)) + return false; + } + + /* + * Verify that the tuple is visible to our MVCC snapshot if the current + * isolation level mandates that. See comments in ExecOnConflictUpdate(). + */ + ExecCheckTupleVisible(context->estate, relation, existing); + + /* + * Make tuple and any needed join variables available to ExecQual. The + * EXCLUDED tuple is installed in ecxt_innertuple, while the target's + * existing tuple is installed in the scantuple. EXCLUDED has been made + * to reference INNER_VAR in setrefs.c, but there is no other redirection. + */ + econtext->ecxt_scantuple = existing; + econtext->ecxt_innertuple = excludedSlot; + econtext->ecxt_outertuple = NULL; + + if (!ExecQual(onConflictSelectWhere, econtext)) + { + ExecClearTuple(existing); /* see return below */ + InstrCountFiltered1(&mtstate->ps, 1); + return true; /* done with the tuple */ + } + + if (resultRelInfo->ri_WithCheckOptions != NIL) + { + /* + * Check target's existing tuple against SELECT-applicable USING + * security barrier quals (if any), enforced here as RLS checks/WCOs. + * + * The rewriter creates WCOs from the USING quals of SELECT policies, + * and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK. If FOR + * UPDATE/SHARE was specified, UPDATE permissions are required on the + * target table, and the rewriter also adds WCOs built from the USING + * quals of UPDATE policies, using WCOs of the same kind, and this + * check enforces them too. + */ + ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK, resultRelInfo, + existing, + mtstate->ps.state); + } + + /* RETURNING is required for DO SELECT */ + Assert(resultRelInfo->ri_projectReturning); + + *returning = ExecProcessReturning(context, resultRelInfo, false, + existing, existing, context->planSlot); + + if (canSetTag) + context->estate->es_processed++; + + /* + * Before releasing the existing tuple, make sure that the returning slot + * has a local copy of any pass-by-reference values. + */ + ExecMaterializeSlot(*returning); + + /* + * Clear out existing tuple, as there might not be another conflict among + * the next input rows. Don't want to hold resources till the end of the + * query. + */ + ExecClearTuple(existing); + + return true; +} + /* * Perform MERGE. */ @@ -3360,6 +3816,11 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, *inputslot; LockTupleMode lockmode; + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + /* * The target tuple was concurrently updated by some other * transaction. If we are currently processing a MATCHED @@ -3399,7 +3860,7 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, * the tuple moved, and setting our current * resultRelInfo to that. */ - if (ItemPointerIndicatesMovedPartitions(&context->tmfd.ctid)) + if (ItemPointerIndicatesMovedPartitions(tupleid)) ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("tuple to be merged was already moved to another partition due to concurrent update"))); @@ -3447,12 +3908,13 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, if (ItemPointerIsValid(&lockedtid)) UnlockTuple(resultRelInfo->ri_RelationDesc, &lockedtid, InplaceUpdateTupleLock); - LockTuple(resultRelInfo->ri_RelationDesc, &context->tmfd.ctid, + LockTuple(resultRelInfo->ri_RelationDesc, tupleid, InplaceUpdateTupleLock); - lockedtid = context->tmfd.ctid; + lockedtid = *tupleid; } + if (!table_tuple_fetch_row_version(resultRelationDesc, - &context->tmfd.ctid, + tupleid, SnapshotAny, resultRelInfo->ri_oldTupleSlot)) elog(ERROR, "failed to fetch the target tuple"); @@ -3463,7 +3925,28 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, /* Switch lists, if necessary */ if (!*matched) + { actionStates = mergeActions[MERGE_WHEN_NOT_MATCHED_BY_SOURCE]; + + /* + * If we have both NOT MATCHED BY SOURCE + * and NOT MATCHED BY TARGET actions (a + * full join between the source and target + * relations), the single previously + * matched tuple from the outer plan node + * is treated as two not matched tuples, + * in the same way as if they had not + * matched to start with. Therefore, we + * must adjust the outer plan node's tuple + * count, if we're instrumenting the + * query, to get the correct "skipped" row + * count --- see show_modifytable_info(). + */ + if (outerPlanState(mtstate)->instrument && + mergeActions[MERGE_WHEN_NOT_MATCHED_BY_SOURCE] && + mergeActions[MERGE_WHEN_NOT_MATCHED_BY_TARGET]) + InstrUpdateTupleCount(outerPlanState(mtstate)->instrument, 1.0); + } } /* @@ -3533,7 +4016,7 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, case CMD_UPDATE: rslot = ExecProcessReturning(context, resultRelInfo, - CMD_UPDATE, + false, resultRelInfo->ri_oldTupleSlot, newslot, context->planSlot); @@ -3542,7 +4025,7 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, case CMD_DELETE: rslot = ExecProcessReturning(context, resultRelInfo, - CMD_DELETE, + true, resultRelInfo->ri_oldTupleSlot, NULL, context->planSlot); @@ -3735,6 +4218,7 @@ ExecInitMerge(ModifyTableState *mtstate, EState *estate) switch (action->commandType) { case CMD_INSERT: + /* INSERT actions always use rootRelInfo */ ExecCheckPlanOutput(rootRelInfo->ri_RelationDesc, action->targetList); @@ -3774,9 +4258,23 @@ ExecInitMerge(ModifyTableState *mtstate, EState *estate) } else { - /* not partitioned? use the stock relation and slot */ - tgtslot = resultRelInfo->ri_newTupleSlot; - tgtdesc = RelationGetDescr(resultRelInfo->ri_RelationDesc); + /* + * If the MERGE targets an inherited table, we insert + * into the root table, so we must initialize its + * "new" tuple slot, if not already done, and use its + * relation descriptor for the projection. + * + * For non-inherited tables, rootRelInfo and + * resultRelInfo are the same, and the "new" tuple + * slot will already have been initialized. + */ + if (rootRelInfo->ri_newTupleSlot == NULL) + rootRelInfo->ri_newTupleSlot = + table_slot_create(rootRelInfo->ri_RelationDesc, + &estate->es_tupleTable); + + tgtslot = rootRelInfo->ri_newTupleSlot; + tgtdesc = RelationGetDescr(rootRelInfo->ri_RelationDesc); } action_state->mas_proj = @@ -3809,6 +4307,114 @@ ExecInitMerge(ModifyTableState *mtstate, EState *estate) } } } + + /* + * If the MERGE targets an inherited table, any INSERT actions will use + * rootRelInfo, and rootRelInfo will not be in the resultRelInfo array. + * Therefore we must initialize its WITH CHECK OPTION constraints and + * RETURNING projection, as ExecInitModifyTable did for the resultRelInfo + * entries. + * + * Note that the planner does not build a withCheckOptionList or + * returningList for the root relation, but as in ExecInitPartitionInfo, + * we can use the first resultRelInfo entry as a reference to calculate + * the attno's for the root table. + */ + if (rootRelInfo != mtstate->resultRelInfo && + rootRelInfo->ri_RelationDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE && + (mtstate->mt_merge_subcommands & MERGE_INSERT) != 0) + { + ModifyTable *node = (ModifyTable *) mtstate->ps.plan; + Relation rootRelation = rootRelInfo->ri_RelationDesc; + Relation firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc; + int firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex; + AttrMap *part_attmap = NULL; + bool found_whole_row; + + if (node->withCheckOptionLists != NIL) + { + List *wcoList; + List *wcoExprs = NIL; + + /* There should be as many WCO lists as result rels */ + Assert(list_length(node->withCheckOptionLists) == + list_length(node->resultRelations)); + + /* + * Use the first WCO list as a reference. In the most common case, + * this will be for the same relation as rootRelInfo, and so there + * will be no need to adjust its attno's. + */ + wcoList = linitial(node->withCheckOptionLists); + if (rootRelation != firstResultRel) + { + /* Convert any Vars in it to contain the root's attno's */ + part_attmap = + build_attrmap_by_name(RelationGetDescr(rootRelation), + RelationGetDescr(firstResultRel), + false); + + wcoList = (List *) + map_variable_attnos((Node *) wcoList, + firstVarno, 0, + part_attmap, + RelationGetForm(rootRelation)->reltype, + &found_whole_row); + } + + foreach(lc, wcoList) + { + WithCheckOption *wco = lfirst_node(WithCheckOption, lc); + ExprState *wcoExpr = ExecInitQual(castNode(List, wco->qual), + &mtstate->ps); + + wcoExprs = lappend(wcoExprs, wcoExpr); + } + + rootRelInfo->ri_WithCheckOptions = wcoList; + rootRelInfo->ri_WithCheckOptionExprs = wcoExprs; + } + + if (node->returningLists != NIL) + { + List *returningList; + + /* There should be as many returning lists as result rels */ + Assert(list_length(node->returningLists) == + list_length(node->resultRelations)); + + /* + * Use the first returning list as a reference. In the most common + * case, this will be for the same relation as rootRelInfo, and so + * there will be no need to adjust its attno's. + */ + returningList = linitial(node->returningLists); + if (rootRelation != firstResultRel) + { + /* Convert any Vars in it to contain the root's attno's */ + if (part_attmap == NULL) + part_attmap = + build_attrmap_by_name(RelationGetDescr(rootRelation), + RelationGetDescr(firstResultRel), + false); + + returningList = (List *) + map_variable_attnos((Node *) returningList, + firstVarno, 0, + part_attmap, + RelationGetForm(rootRelation)->reltype, + &found_whole_row); + } + rootRelInfo->ri_returningList = returningList; + + /* Initialize the RETURNING projection */ + rootRelInfo->ri_projectReturning = + ExecBuildProjectionInfo(returningList, econtext, + mtstate->ps.ps_ResultTupleSlot, + &mtstate->ps, + RelationGetDescr(rootRelation)); + } + } } /* @@ -4190,7 +4796,8 @@ ExecModifyTable(PlanState *pstate) Assert((resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD) == 0 && (resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_NEW) == 0); - slot = ExecProcessReturning(&context, resultRelInfo, operation, + slot = ExecProcessReturning(&context, resultRelInfo, + operation == CMD_DELETE, NULL, NULL, context.planSlot); return slot; @@ -4221,8 +4828,12 @@ ExecModifyTable(PlanState *pstate) relkind == RELKIND_MATVIEW || relkind == RELKIND_PARTITIONED_TABLE) { - /* ri_RowIdAttNo refers to a ctid attribute */ - Assert(AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)); + /* + * ri_RowIdAttNo refers to a ctid attribute. See the comment + * in ExecInitModifyTable(). + */ + Assert(AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo) || + relkind == RELKIND_PARTITIONED_TABLE); datum = ExecGetJunkAttribute(slot, resultRelInfo->ri_RowIdAttNo, &isNull); @@ -4595,8 +5206,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) mtstate->mt_done = false; mtstate->mt_nrels = nrels; - mtstate->resultRelInfo = (ResultRelInfo *) - palloc(nrels * sizeof(ResultRelInfo)); + mtstate->resultRelInfo = palloc_array(ResultRelInfo, nrels); mtstate->mt_merge_pending_not_matched = NULL; mtstate->mt_merge_inserted = 0; @@ -4685,7 +5295,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) /* * Verify result relation is a valid target for the current operation */ - CheckValidResultRel(resultRelInfo, operation, mergeActions); + CheckValidResultRel(resultRelInfo, operation, node->onConflictAction, + mergeActions); resultRelInfo++; i++; @@ -4735,7 +5346,16 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) { resultRelInfo->ri_RowIdAttNo = ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid"); - if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) + + /* + * For heap relations, a ctid junk attribute must be present. + * Partitioned tables should only appear here when all leaf + * partitions were pruned, in which case no rows can be + * produced and ctid is not needed. + */ + if (relkind == RELKIND_PARTITIONED_TABLE) + Assert(nrels == 1); + else if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) elog(ERROR, "could not find junk ctid column"); } else if (relkind == RELKIND_FOREIGN_TABLE) @@ -4879,49 +5499,60 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } /* - * If needed, Initialize target list, projection and qual for ON CONFLICT - * DO UPDATE. + * For ON CONFLICT DO SELECT/UPDATE, initialize the ON CONFLICT action + * state. */ - if (node->onConflictAction == ONCONFLICT_UPDATE) + if (node->onConflictAction == ONCONFLICT_UPDATE || + node->onConflictAction == ONCONFLICT_SELECT) { - OnConflictSetState *onconfl = makeNode(OnConflictSetState); - ExprContext *econtext; - TupleDesc relationDesc; + OnConflictActionState *onconfl = makeNode(OnConflictActionState); /* already exists if created by RETURNING processing above */ if (mtstate->ps.ps_ExprContext == NULL) ExecAssignExprContext(estate, &mtstate->ps); - econtext = mtstate->ps.ps_ExprContext; - relationDesc = resultRelInfo->ri_RelationDesc->rd_att; - - /* create state for DO UPDATE SET operation */ + /* action state for DO SELECT/UPDATE */ resultRelInfo->ri_onConflict = onconfl; + /* lock strength for DO SELECT [FOR UPDATE/SHARE] */ + onconfl->oc_LockStrength = node->onConflictLockStrength; + /* initialize slot for the existing tuple */ onconfl->oc_Existing = table_slot_create(resultRelInfo->ri_RelationDesc, &mtstate->ps.state->es_tupleTable); /* - * Create the tuple slot for the UPDATE SET projection. We want a slot - * of the table's type here, because the slot will be used to insert - * into the table, and for RETURNING processing - which may access - * system attributes. + * For ON CONFLICT DO UPDATE, initialize target list and projection. */ - onconfl->oc_ProjSlot = - table_slot_create(resultRelInfo->ri_RelationDesc, - &mtstate->ps.state->es_tupleTable); + if (node->onConflictAction == ONCONFLICT_UPDATE) + { + ExprContext *econtext; + TupleDesc relationDesc; - /* build UPDATE SET projection state */ - onconfl->oc_ProjInfo = - ExecBuildUpdateProjection(node->onConflictSet, - true, - node->onConflictCols, - relationDesc, - econtext, - onconfl->oc_ProjSlot, - &mtstate->ps); + econtext = mtstate->ps.ps_ExprContext; + relationDesc = resultRelInfo->ri_RelationDesc->rd_att; + + /* + * Create the tuple slot for the UPDATE SET projection. We want a + * slot of the table's type here, because the slot will be used to + * insert into the table, and for RETURNING processing - which may + * access system attributes. + */ + onconfl->oc_ProjSlot = + table_slot_create(resultRelInfo->ri_RelationDesc, + &mtstate->ps.state->es_tupleTable); + + /* build UPDATE SET projection state */ + onconfl->oc_ProjInfo = + ExecBuildUpdateProjection(node->onConflictSet, + true, + node->onConflictCols, + relationDesc, + econtext, + onconfl->oc_ProjSlot, + &mtstate->ps); + } /* initialize state to evaluate the WHERE clause, if any */ if (node->onConflictWhere) @@ -4934,6 +5565,108 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } } + /* + * If needed, initialize the target range for FOR PORTION OF. + */ + if (node->forPortionOf) + { + ResultRelInfo *rootRelInfo; + TupleDesc tupDesc; + ForPortionOfExpr *forPortionOf; + Datum targetRange; + bool isNull; + ExprContext *econtext; + ExprState *exprState; + ForPortionOfState *fpoState; + + rootRelInfo = mtstate->resultRelInfo; + if (rootRelInfo->ri_RootResultRelInfo) + rootRelInfo = rootRelInfo->ri_RootResultRelInfo; + + tupDesc = rootRelInfo->ri_RelationDesc->rd_att; + forPortionOf = (ForPortionOfExpr *) node->forPortionOf; + + /* Eval the FOR PORTION OF target */ + if (mtstate->ps.ps_ExprContext == NULL) + ExecAssignExprContext(estate, &mtstate->ps); + econtext = mtstate->ps.ps_ExprContext; + + exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate); + targetRange = ExecEvalExpr(exprState, econtext, &isNull); + + /* + * FOR PORTION OF ... TO ... FROM should never give us a NULL target, + * but FOR PORTION OF (...) could. + */ + if (isNull) + ereport(ERROR, + (errmsg("FOR PORTION OF target was null")), + executor_errposition(estate, forPortionOf->targetLocation)); + + /* Create state for FOR PORTION OF operation */ + + fpoState = makeNode(ForPortionOfState); + fpoState->fp_rangeName = forPortionOf->range_name; + fpoState->fp_rangeType = forPortionOf->rangeType; + fpoState->fp_rangeAttno = forPortionOf->rangeVar->varattno; + fpoState->fp_targetRange = targetRange; + + /* Initialize slot for the existing tuple */ + + fpoState->fp_Existing = + table_slot_create(rootRelInfo->ri_RelationDesc, + &mtstate->ps.state->es_tupleTable); + + /* Create the tuple slot for INSERTing the temporal leftovers */ + + fpoState->fp_Leftover = + ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc, &TTSOpsVirtual); + + rootRelInfo->ri_forPortionOf = fpoState; + + /* + * Make sure the root relation has the FOR PORTION OF clause too. Each + * partition needs its own TupleTableSlot, since they can have + * different descriptors, so they'll use the root fpoState to + * initialize one if necessary. + */ + if (node->rootRelation > 0) + mtstate->rootResultRelInfo->ri_forPortionOf = fpoState; + + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && + mtstate->mt_partition_tuple_routing == NULL) + { + /* + * We will need tuple routing to insert temporal leftovers. Since + * we are initializing things before ExecCrossPartitionUpdate + * runs, we must do everything it needs as well. + */ + Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc; + MemoryContext oldcxt; + + /* Things built here have to last for the query duration. */ + oldcxt = MemoryContextSwitchTo(estate->es_query_cxt); + + mtstate->mt_partition_tuple_routing = + ExecSetupPartitionTupleRouting(estate, rootRel); + + /* + * Before a partition's tuple can be re-routed, it must first be + * converted to the root's format, so we'll need a slot for + * storing such tuples. + */ + Assert(mtstate->mt_root_tuple_slot == NULL); + mtstate->mt_root_tuple_slot = table_slot_create(rootRel, NULL); + + MemoryContextSwitchTo(oldcxt); + } + + /* + * Don't free the ExprContext here because the result must last for + * the whole query. + */ + } + /* * If we have any secondary relations in an UPDATE or DELETE, they need to * be treated like non-locked relations in SELECT FOR UPDATE, i.e., the @@ -4944,15 +5677,19 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) foreach(l, node->rowMarks) { PlanRowMark *rc = lfirst_node(PlanRowMark, l); + RangeTblEntry *rte = exec_rt_fetch(rc->rti, estate); ExecRowMark *erm; ExecAuxRowMark *aerm; + /* ignore "parent" rowmarks; they are irrelevant at runtime */ + if (rc->isParent) + continue; + /* - * Ignore "parent" rowmarks, because they are irrelevant at runtime. - * Also ignore the rowmarks belonging to child tables that have been + * Also ignore rowmarks belonging to child tables that have been * pruned in ExecDoInitialPruning(). */ - if (rc->isParent || + if (rte->rtekind == RTE_RELATION && !bms_is_member(rc->rti, estate->es_unpruned_relids)) continue; diff --git a/src/backend/executor/nodeNamedtuplestorescan.c b/src/backend/executor/nodeNamedtuplestorescan.c index 047788d9e4ea3..4c94674cecad3 100644 --- a/src/backend/executor/nodeNamedtuplestorescan.c +++ b/src/backend/executor/nodeNamedtuplestorescan.c @@ -3,7 +3,7 @@ * nodeNamedtuplestorescan.c * routines to handle NamedTuplestoreScan nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,6 +18,7 @@ #include "executor/executor.h" #include "executor/nodeNamedtuplestorescan.h" #include "utils/queryenvironment.h" +#include "utils/tuplestore.h" static TupleTableSlot *NamedTuplestoreScanNext(NamedTuplestoreScanState *node); @@ -137,7 +138,7 @@ ExecInitNamedTuplestoreScan(NamedTuplestoreScan *node, EState *estate, int eflag * The scan tuple type is specified for the tuplestore. */ ExecInitScanTupleSlot(estate, &scanstate->ss, scanstate->tupdesc, - &TTSOpsMinimalTuple); + &TTSOpsMinimalTuple, 0); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c index 5cd1a251625ca..809311ab51330 100644 --- a/src/backend/executor/nodeNestloop.c +++ b/src/backend/executor/nodeNestloop.c @@ -3,7 +3,7 @@ * nodeNestloop.c * routines to support nest-loop joins * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -22,6 +22,7 @@ #include "postgres.h" #include "executor/execdebug.h" +#include "executor/instrument.h" #include "executor/nodeNestloop.h" #include "miscadmin.h" diff --git a/src/backend/executor/nodeProjectSet.c b/src/backend/executor/nodeProjectSet.c index 880f39fb2ff1e..33b5c209f5a87 100644 --- a/src/backend/executor/nodeProjectSet.c +++ b/src/backend/executor/nodeProjectSet.c @@ -11,7 +11,7 @@ * can't be inside more-complex expressions. If that'd otherwise be * the case, the planner adds additional ProjectSet nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -267,10 +267,8 @@ ExecInitProjectSet(ProjectSet *node, EState *estate, int eflags) /* Create workspace for per-tlist-entry expr state & SRF-is-done state */ state->nelems = list_length(node->plan.targetlist); - state->elems = (Node **) - palloc(sizeof(Node *) * state->nelems); - state->elemdone = (ExprDoneCond *) - palloc(sizeof(ExprDoneCond) * state->nelems); + state->elems = palloc_array(Node *, state->nelems); + state->elemdone = palloc_array(ExprDoneCond, state->nelems); /* * Build expressions to evaluate targetlist. We can't use diff --git a/src/backend/executor/nodeRecursiveunion.c b/src/backend/executor/nodeRecursiveunion.c index 40f66fd0680b2..7166397e59b96 100644 --- a/src/backend/executor/nodeRecursiveunion.c +++ b/src/backend/executor/nodeRecursiveunion.c @@ -7,7 +7,7 @@ * already seen. The hash key is computed from the grouping columns. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -22,6 +22,7 @@ #include "executor/nodeRecursiveunion.h" #include "miscadmin.h" #include "utils/memutils.h" +#include "utils/tuplestore.h" @@ -35,7 +36,6 @@ build_hash_table(RecursiveUnionState *rustate) TupleDesc desc = ExecGetResultType(outerPlanState(rustate)); Assert(node->numCols > 0); - Assert(node->numGroups > 0); /* * If both child plans deliver the same fixed tuple slot type, we can tell @@ -53,7 +53,7 @@ build_hash_table(RecursiveUnionState *rustate) node->numGroups, 0, rustate->ps.state->es_query_cxt, - rustate->tableContext, + rustate->tuplesContext, rustate->tempContext, false); } @@ -197,7 +197,7 @@ ExecInitRecursiveUnion(RecursiveUnion *node, EState *estate, int eflags) rustate->hashfunctions = NULL; rustate->hashtable = NULL; rustate->tempContext = NULL; - rustate->tableContext = NULL; + rustate->tuplesContext = NULL; /* initialize processing state */ rustate->recursing = false; @@ -209,7 +209,8 @@ ExecInitRecursiveUnion(RecursiveUnion *node, EState *estate, int eflags) * If hashing, we need a per-tuple memory context for comparisons, and a * longer-lived context to store the hash table. The table can't just be * kept in the per-query context because we want to be able to throw it - * away when rescanning. + * away when rescanning. We can use a BumpContext to save storage, + * because we will have no need to delete individual table entries. */ if (node->numCols > 0) { @@ -217,10 +218,10 @@ ExecInitRecursiveUnion(RecursiveUnion *node, EState *estate, int eflags) AllocSetContextCreate(CurrentMemoryContext, "RecursiveUnion", ALLOCSET_DEFAULT_SIZES); - rustate->tableContext = - AllocSetContextCreate(CurrentMemoryContext, - "RecursiveUnion hash table", - ALLOCSET_DEFAULT_SIZES); + rustate->tuplesContext = + BumpContextCreate(CurrentMemoryContext, + "RecursiveUnion hashed tuples", + ALLOCSET_DEFAULT_SIZES); } /* @@ -288,11 +289,11 @@ ExecEndRecursiveUnion(RecursiveUnionState *node) tuplestore_end(node->working_table); tuplestore_end(node->intermediate_table); - /* free subsidiary stuff including hashtable */ + /* free subsidiary stuff including hashtable data */ if (node->tempContext) MemoryContextDelete(node->tempContext); - if (node->tableContext) - MemoryContextDelete(node->tableContext); + if (node->tuplesContext) + MemoryContextDelete(node->tuplesContext); /* * close down subplans @@ -328,10 +329,6 @@ ExecReScanRecursiveUnion(RecursiveUnionState *node) if (outerPlan->chgParam == NULL) ExecReScan(outerPlan); - /* Release any hashtable storage */ - if (node->tableContext) - MemoryContextReset(node->tableContext); - /* Empty hashtable if needed */ if (plan->numCols > 0) ResetTupleHashTable(node->hashtable); diff --git a/src/backend/executor/nodeResult.c b/src/backend/executor/nodeResult.c index 06842a48ecae6..660561aed8d96 100644 --- a/src/backend/executor/nodeResult.c +++ b/src/backend/executor/nodeResult.c @@ -34,7 +34,7 @@ * plan normally and pass back the results. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/executor/nodeSamplescan.c b/src/backend/executor/nodeSamplescan.c index 6b3db7548ed99..f3d273e1c5e8f 100644 --- a/src/backend/executor/nodeSamplescan.c +++ b/src/backend/executor/nodeSamplescan.c @@ -3,7 +3,7 @@ * nodeSamplescan.c * Support routines for sample scans of relations (table sampling). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -128,7 +128,8 @@ ExecInitSampleScan(SampleScan *node, EState *estate, int eflags) /* and create slot with appropriate rowtype */ ExecInitScanTupleSlot(estate, &scanstate->ss, RelationGetDescr(scanstate->ss.ss_currentRelation), - table_slot_callbacks(scanstate->ss.ss_currentRelation)); + table_slot_callbacks(scanstate->ss.ss_currentRelation), + TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); /* * Initialize result type and projection. @@ -228,7 +229,7 @@ tablesample_init(SampleScanState *scanstate) ListCell *arg; scanstate->donetuples = 0; - params = (Datum *) palloc(list_length(scanstate->args) * sizeof(Datum)); + params = palloc_array(Datum, list_length(scanstate->args)); i = 0; foreach(arg, scanstate->args) @@ -297,7 +298,9 @@ tablesample_init(SampleScanState *scanstate) 0, NULL, scanstate->use_bulkread, allow_sync, - scanstate->use_pagemode); + scanstate->use_pagemode, + ScanRelIsReadOnly(&scanstate->ss) ? + SO_HINT_REL_READ_ONLY : SO_NONE); } else { diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c index ed35c58c2c346..5bcb0a861d74e 100644 --- a/src/backend/executor/nodeSeqscan.c +++ b/src/backend/executor/nodeSeqscan.c @@ -3,7 +3,7 @@ * nodeSeqscan.c * Support routines for sequential scans of relations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,6 +29,7 @@ #include "access/relscan.h" #include "access/tableam.h" +#include "executor/execParallel.h" #include "executor/execScan.h" #include "executor/executor.h" #include "executor/nodeSeqscan.h" @@ -47,7 +48,7 @@ static TupleTableSlot *SeqNext(SeqScanState *node); * This is a workhorse for ExecSeqScan * ---------------------------------------------------------------- */ -static TupleTableSlot * +static pg_attribute_always_inline TupleTableSlot * SeqNext(SeqScanState *node) { TableScanDesc scandesc; @@ -65,13 +66,21 @@ SeqNext(SeqScanState *node) if (scandesc == NULL) { + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (estate->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; + /* * We reach here if the scan is not parallel, or if we're serially * executing a scan that was planned to be parallel. */ scandesc = table_beginscan(node->ss.ss_currentRelation, estate->es_snapshot, - 0, NULL); + 0, NULL, flags); node->ss.ss_currentScanDesc = scandesc; } @@ -86,7 +95,7 @@ SeqNext(SeqScanState *node) /* * SeqRecheck -- access method routine to recheck a tuple in EvalPlanQual */ -static bool +static pg_attribute_always_inline bool SeqRecheck(SeqScanState *node, TupleTableSlot *slot) { /* @@ -131,8 +140,12 @@ ExecSeqScanWithQual(PlanState *pstate) { SeqScanState *node = castNode(SeqScanState, pstate); + /* + * Use pg_assume() for != NULL tests to make the compiler realize no + * runtime check for the field is needed in ExecScanExtended(). + */ Assert(pstate->state->es_epq_active == NULL); - Assert(pstate->qual != NULL); + pg_assume(pstate->qual != NULL); Assert(pstate->ps_ProjInfo == NULL); return ExecScanExtended(&node->ss, @@ -153,7 +166,7 @@ ExecSeqScanWithProject(PlanState *pstate) Assert(pstate->state->es_epq_active == NULL); Assert(pstate->qual == NULL); - Assert(pstate->ps_ProjInfo != NULL); + pg_assume(pstate->ps_ProjInfo != NULL); return ExecScanExtended(&node->ss, (ExecScanAccessMtd) SeqNext, @@ -173,8 +186,8 @@ ExecSeqScanWithQualProject(PlanState *pstate) SeqScanState *node = castNode(SeqScanState, pstate); Assert(pstate->state->es_epq_active == NULL); - Assert(pstate->qual != NULL); - Assert(pstate->ps_ProjInfo != NULL); + pg_assume(pstate->qual != NULL); + pg_assume(pstate->ps_ProjInfo != NULL); return ExecScanExtended(&node->ss, (ExecScanAccessMtd) SeqNext, @@ -240,7 +253,8 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags) /* and create slot with the appropriate rowtype */ ExecInitScanTupleSlot(estate, &scanstate->ss, RelationGetDescr(scanstate->ss.ss_currentRelation), - table_slot_callbacks(scanstate->ss.ss_currentRelation)); + table_slot_callbacks(scanstate->ss.ss_currentRelation), + TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); /* * Initialize result type and projection. @@ -295,6 +309,22 @@ ExecEndSeqScan(SeqScanState *node) */ scanDesc = node->ss.ss_currentScanDesc; + /* + * Collect I/O stats for this process into shared instrumentation. + */ + if (node->sinstrument != NULL && IsParallelWorker()) + { + SeqScanInstrumentation *si; + + Assert(ParallelWorkerNumber < node->sinstrument->num_workers); + si = &node->sinstrument->sinstrument[ParallelWorkerNumber]; + + if (scanDesc && scanDesc->rs_instrument) + { + AccumulateIOStats(&si->stats.io, &scanDesc->rs_instrument->io); + } + } + /* * close heap scan */ @@ -363,14 +393,22 @@ ExecSeqScanInitializeDSM(SeqScanState *node, { EState *estate = node->ss.ps.state; ParallelTableScanDesc pscan; + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (estate->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; pscan = shm_toc_allocate(pcxt->toc, node->pscan_len); table_parallelscan_initialize(node->ss.ss_currentRelation, pscan, estate->es_snapshot); shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, pscan); + node->ss.ss_currentScanDesc = - table_beginscan_parallel(node->ss.ss_currentRelation, pscan); + table_beginscan_parallel(node->ss.ss_currentRelation, pscan, flags); } /* ---------------------------------------------------------------- @@ -400,8 +438,97 @@ ExecSeqScanInitializeWorker(SeqScanState *node, ParallelWorkerContext *pwcxt) { ParallelTableScanDesc pscan; + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (node->ss.ps.state->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); node->ss.ss_currentScanDesc = - table_beginscan_parallel(node->ss.ss_currentRelation, pscan); + table_beginscan_parallel(node->ss.ss_currentRelation, pscan, flags); +} + +/* + * Compute the amount of space we'll need for the shared instrumentation and + * inform pcxt->estimator. + */ +void +ExecSeqScanInstrumentEstimate(SeqScanState *node, ParallelContext *pcxt) +{ + EState *estate = node->ss.ps.state; + Size size; + + if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedSeqScanInstrumentation, sinstrument), + mul_size(pcxt->nworkers, sizeof(SeqScanInstrumentation))); + + shm_toc_estimate_chunk(&pcxt->estimator, size); + shm_toc_estimate_keys(&pcxt->estimator, 1); +} + +/* + * Set up parallel sequential scan instrumentation. + */ +void +ExecSeqScanInstrumentInitDSM(SeqScanState *node, ParallelContext *pcxt) +{ + EState *estate = node->ss.ps.state; + SharedSeqScanInstrumentation *sinstrument; + Size size; + + if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedSeqScanInstrumentation, sinstrument), + mul_size(pcxt->nworkers, sizeof(SeqScanInstrumentation))); + sinstrument = shm_toc_allocate(pcxt->toc, size); + memset(sinstrument, 0, size); + sinstrument->num_workers = pcxt->nworkers; + shm_toc_insert(pcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + sinstrument); + node->sinstrument = sinstrument; +} + +/* + * Look up and save the location of the shared instrumentation. + */ +void +ExecSeqScanInstrumentInitWorker(SeqScanState *node, + ParallelWorkerContext *pwcxt) +{ + EState *estate = node->ss.ps.state; + + if ((estate->es_instrument & INSTRUMENT_IO) == 0) + return; + + node->sinstrument = shm_toc_lookup(pwcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + false); +} + +/* + * Transfer sequential scan instrumentation from DSM to private memory. + */ +void +ExecSeqScanRetrieveInstrumentation(SeqScanState *node) +{ + SharedSeqScanInstrumentation *sinstrument = node->sinstrument; + Size size; + + if (sinstrument == NULL) + return; + + size = offsetof(SharedSeqScanInstrumentation, sinstrument) + + sinstrument->num_workers * sizeof(SeqScanInstrumentation); + + node->sinstrument = palloc(size); + memcpy(node->sinstrument, sinstrument, size); } diff --git a/src/backend/executor/nodeSetOp.c b/src/backend/executor/nodeSetOp.c index 4068481a52392..24709778384b5 100644 --- a/src/backend/executor/nodeSetOp.c +++ b/src/backend/executor/nodeSetOp.c @@ -33,7 +33,7 @@ * input group. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -50,6 +50,7 @@ #include "executor/nodeSetOp.h" #include "miscadmin.h" #include "utils/memutils.h" +#include "utils/sortsupport.h" /* @@ -88,7 +89,6 @@ build_hash_table(SetOpState *setopstate) TupleDesc desc = ExecGetResultType(outerPlanState(setopstate)); Assert(node->strategy == SETOP_HASHED); - Assert(node->numGroups > 0); /* * If both child plans deliver the same fixed tuple slot type, we can tell @@ -106,11 +106,20 @@ build_hash_table(SetOpState *setopstate) node->numGroups, sizeof(SetOpStatePerGroupData), setopstate->ps.state->es_query_cxt, - setopstate->tableContext, + setopstate->tuplesContext, econtext->ecxt_per_tuple_memory, false); } +/* Planner support routine to estimate space needed for hash table */ +Size +EstimateSetOpHashTableSpace(double nentries, Size tupleWidth) +{ + return EstimateTupleHashTableSpace(nentries, + tupleWidth, + sizeof(SetOpStatePerGroupData)); +} + /* * We've completed processing a tuple group. Decide how many copies (if any) * of its representative row to emit, and store the count into numOutput. @@ -589,13 +598,15 @@ ExecInitSetOp(SetOp *node, EState *estate, int eflags) /* * If hashing, we also need a longer-lived context to store the hash * table. The table can't just be kept in the per-query context because - * we want to be able to throw it away in ExecReScanSetOp. + * we want to be able to throw it away in ExecReScanSetOp. We can use a + * BumpContext to save storage, because we will have no need to delete + * individual table entries. */ if (node->strategy == SETOP_HASHED) - setopstate->tableContext = - AllocSetContextCreate(CurrentMemoryContext, - "SetOp hash table", - ALLOCSET_DEFAULT_SIZES); + setopstate->tuplesContext = + BumpContextCreate(CurrentMemoryContext, + "SetOp hashed tuples", + ALLOCSET_DEFAULT_SIZES); /* * initialize child nodes @@ -680,9 +691,9 @@ ExecInitSetOp(SetOp *node, EState *estate, int eflags) void ExecEndSetOp(SetOpState *node) { - /* free subsidiary stuff including hashtable */ - if (node->tableContext) - MemoryContextDelete(node->tableContext); + /* free subsidiary stuff including hashtable data */ + if (node->tuplesContext) + MemoryContextDelete(node->tuplesContext); ExecEndNode(outerPlanState(node)); ExecEndNode(innerPlanState(node)); @@ -721,11 +732,7 @@ ExecReScanSetOp(SetOpState *node) return; } - /* Release any hashtable storage */ - if (node->tableContext) - MemoryContextReset(node->tableContext); - - /* And rebuild an empty hashtable */ + /* Else, we must rebuild the hashtable */ ResetTupleHashTable(node->hashtable); node->table_filled = false; } diff --git a/src/backend/executor/nodeSort.c b/src/backend/executor/nodeSort.c index f603337ecd344..e02313f7813e1 100644 --- a/src/backend/executor/nodeSort.c +++ b/src/backend/executor/nodeSort.c @@ -3,7 +3,7 @@ * nodeSort.c * Routines to handle sorting of relations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -175,7 +175,7 @@ ExecSort(PlanState *pstate) TuplesortInstrumentation *si; Assert(IsParallelWorker()); - Assert(ParallelWorkerNumber <= node->shared_info->num_workers); + Assert(ParallelWorkerNumber < node->shared_info->num_workers); si = &node->shared_info->sinstrument[ParallelWorkerNumber]; tuplesort_get_stats(tuplesortstate, si); } diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c index f7f6fc2da0b95..8285c7101c289 100644 --- a/src/backend/executor/nodeSubplan.c +++ b/src/backend/executor/nodeSubplan.c @@ -11,7 +11,7 @@ * subplans, which are re-evaluated every time their result is required. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -26,15 +26,12 @@ */ #include "postgres.h" -#include - #include "access/htup_details.h" #include "executor/executor.h" #include "executor/nodeSubplan.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" -#include "optimizer/optimizer.h" #include "utils/array.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -102,6 +99,7 @@ ExecHashSubPlan(SubPlanState *node, ExprContext *econtext, bool *isNull) { + bool result = false; SubPlan *subplan = node->subplan; PlanState *planstate = node->planstate; TupleTableSlot *slot; @@ -132,14 +130,6 @@ ExecHashSubPlan(SubPlanState *node, node->projLeft->pi_exprContext = econtext; slot = ExecProject(node->projLeft); - /* - * Note: because we are typically called in a per-tuple context, we have - * to explicitly clear the projected tuple before returning. Otherwise, - * we'll have a double-free situation: the per-tuple context will probably - * be reset before we're called again, and then the tuple slot will think - * it still needs to free the tuple. - */ - /* * If the LHS is all non-null, probe for an exact match in the main hash * table. If we find one, the result is TRUE. Otherwise, scan the @@ -161,19 +151,10 @@ ExecHashSubPlan(SubPlanState *node, slot, node->cur_eq_comp, node->lhs_hash_expr) != NULL) - { - ExecClearTuple(slot); - return BoolGetDatum(true); - } - if (node->havenullrows && - findPartialMatch(node->hashnulls, slot, node->cur_eq_funcs)) - { - ExecClearTuple(slot); + result = true; + else if (node->havenullrows && + findPartialMatch(node->hashnulls, slot, node->cur_eq_funcs)) *isNull = true; - return BoolGetDatum(false); - } - ExecClearTuple(slot); - return BoolGetDatum(false); } /* @@ -186,34 +167,31 @@ ExecHashSubPlan(SubPlanState *node, * aren't provably unequal to the LHS; if so, the result is UNKNOWN. * Otherwise, the result is FALSE. */ - if (node->hashnulls == NULL) - { - ExecClearTuple(slot); - return BoolGetDatum(false); - } - if (slotAllNulls(slot)) - { - ExecClearTuple(slot); + else if (node->hashnulls == NULL) + /* just return FALSE */ ; + else if (slotAllNulls(slot)) *isNull = true; - return BoolGetDatum(false); - } /* Scan partly-null table first, since more likely to get a match */ - if (node->havenullrows && - findPartialMatch(node->hashnulls, slot, node->cur_eq_funcs)) - { - ExecClearTuple(slot); + else if (node->havenullrows && + findPartialMatch(node->hashnulls, slot, node->cur_eq_funcs)) *isNull = true; - return BoolGetDatum(false); - } - if (node->havehashrows && - findPartialMatch(node->hashtable, slot, node->cur_eq_funcs)) - { - ExecClearTuple(slot); + else if (node->havehashrows && + findPartialMatch(node->hashtable, slot, node->cur_eq_funcs)) *isNull = true; - return BoolGetDatum(false); - } + + /* + * Note: because we are typically called in a per-tuple context, we have + * to explicitly clear the projected tuple before returning. Otherwise, + * we'll have a double-free situation: the per-tuple context will probably + * be reset before we're called again, and then the tuple slot will think + * it still needs to free the tuple. + */ ExecClearTuple(slot); - return BoolGetDatum(false); + + /* Also must reset the innerecontext after each hashtable lookup. */ + ResetExprContext(node->innerecontext); + + return BoolGetDatum(result); } /* @@ -500,7 +478,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) int ncols = node->numCols; ExprContext *innerecontext = node->innerecontext; MemoryContext oldcontext; - long nbuckets; + double nentries; TupleTableSlot *slot; Assert(subplan->subLinkType == ANY_SUBLINK); @@ -525,13 +503,10 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) * saves a needless fetch inner op step for the hashing ExprState created * in BuildTupleHashTable(). */ - MemoryContextReset(node->hashtablecxt); node->havehashrows = false; node->havenullrows = false; - nbuckets = clamp_cardinality_to_long(planstate->plan->plan_rows); - if (nbuckets < 1) - nbuckets = 1; + nentries = planstate->plan->plan_rows; if (node->hashtable) ResetTupleHashTable(node->hashtable); @@ -544,22 +519,22 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) node->tab_eq_funcoids, node->tab_hash_funcs, node->tab_collations, - nbuckets, - 0, + nentries, + 0, /* no additional data */ node->planstate->state->es_query_cxt, - node->hashtablecxt, - node->hashtempcxt, + node->tuplesContext, + innerecontext->ecxt_per_tuple_memory, false); if (!subplan->unknownEqFalse) { if (ncols == 1) - nbuckets = 1; /* there can only be one entry */ + nentries = 1; /* there can only be one entry */ else { - nbuckets /= 16; - if (nbuckets < 1) - nbuckets = 1; + nentries /= 16; + if (nentries < 1) + nentries = 1; } if (node->hashnulls) @@ -573,11 +548,11 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) node->tab_eq_funcoids, node->tab_hash_funcs, node->tab_collations, - nbuckets, - 0, + nentries, + 0, /* no additional data */ node->planstate->state->es_query_cxt, - node->hashtablecxt, - node->hashtempcxt, + node->tuplesContext, + innerecontext->ecxt_per_tuple_memory, false); } else @@ -639,7 +614,7 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) /* * Reset innerecontext after each inner tuple to free any memory used - * during ExecProject. + * during ExecProject and hashtable lookup. */ ResetExprContext(innerecontext); } @@ -656,6 +631,55 @@ buildSubPlanHash(SubPlanState *node, ExprContext *econtext) MemoryContextSwitchTo(oldcontext); } +/* Planner support routine to estimate space needed for hash table(s) */ +Size +EstimateSubplanHashTableSpace(double nentries, + Size tupleWidth, + bool unknownEqFalse) +{ + Size tab1space, + tab2space; + + /* Estimate size of main hashtable */ + tab1space = EstimateTupleHashTableSpace(nentries, + tupleWidth, + 0 /* no additional data */ ); + + /* Give up if that's already too big */ + if (tab1space >= SIZE_MAX) + return tab1space; + + /* Done if we don't need a hashnulls table */ + if (unknownEqFalse) + return tab1space; + + /* + * Adjust the rowcount estimate in the same way that buildSubPlanHash + * will, except that we don't bother with the special case for a single + * hash column. (We skip that detail because it'd be notationally painful + * for our caller to provide the column count, and this table has + * relatively little impact on the total estimate anyway.) + */ + nentries /= 16; + if (nentries < 1) + nentries = 1; + + /* + * It might be sane to also reduce the tupleWidth, but on the other hand + * we are not accounting for the space taken by the tuples' null bitmaps. + * Leave it alone for now. + */ + tab2space = EstimateTupleHashTableSpace(nentries, + tupleWidth, + 0 /* no additional data */ ); + + /* Guard against overflow */ + if (tab2space >= SIZE_MAX - tab1space) + return SIZE_MAX; + + return tab1space + tab2space; +} + /* * execTuplesUnequal * Return true if two tuples are definitely unequal in the indicated @@ -857,8 +881,7 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent) sstate->projRight = NULL; sstate->hashtable = NULL; sstate->hashnulls = NULL; - sstate->hashtablecxt = NULL; - sstate->hashtempcxt = NULL; + sstate->tuplesContext = NULL; sstate->innerecontext = NULL; sstate->keyColIdx = NULL; sstate->tab_eq_funcoids = NULL; @@ -909,16 +932,11 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent) *righttlist; ListCell *l; - /* We need a memory context to hold the hash table(s) */ - sstate->hashtablecxt = - AllocSetContextCreate(CurrentMemoryContext, - "Subplan HashTable Context", - ALLOCSET_DEFAULT_SIZES); - /* and a small one for the hash tables to use as temp storage */ - sstate->hashtempcxt = - AllocSetContextCreate(CurrentMemoryContext, - "Subplan HashTable Temp Context", - ALLOCSET_SMALL_SIZES); + /* We need a memory context to hold the hash table(s)' tuples */ + sstate->tuplesContext = + BumpContextCreate(CurrentMemoryContext, + "SubPlan hashed tuples", + ALLOCSET_DEFAULT_SIZES); /* and a short-lived exprcontext for function evaluation */ sstate->innerecontext = CreateExprContext(estate); diff --git a/src/backend/executor/nodeSubqueryscan.c b/src/backend/executor/nodeSubqueryscan.c index 8dd1ae4630832..70914e8189c5b 100644 --- a/src/backend/executor/nodeSubqueryscan.c +++ b/src/backend/executor/nodeSubqueryscan.c @@ -7,7 +7,7 @@ * we need two sets of code. Ought to look at trying to unify the cases. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -130,7 +130,8 @@ ExecInitSubqueryScan(SubqueryScan *node, EState *estate, int eflags) */ ExecInitScanTupleSlot(estate, &subquerystate->ss, ExecGetResultType(subquerystate->subplan), - ExecGetResultSlotOps(subquerystate->subplan, NULL)); + ExecGetResultSlotOps(subquerystate->subplan, NULL), + 0); /* * The slot used as the scantuple isn't the slot above (outside of EPQ), diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 83ade3f943763..9394156f40549 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -3,7 +3,7 @@ * nodeTableFuncscan.c * Support routines for scanning RangeTableFunc (XMLTABLE like functions). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -31,6 +31,7 @@ #include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/tuplestore.h" #include "utils/xml.h" static TupleTableSlot *TableFuncNext(TableFuncScanState *node); @@ -148,7 +149,7 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) tf->colcollations); /* and the corresponding scan slot */ ExecInitScanTupleSlot(estate, &scanstate->ss, tupdesc, - &TTSOpsMinimalTuple); + &TTSOpsMinimalTuple, 0); /* * Initialize result type and projection. @@ -192,8 +193,8 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->notnulls = tf->notnulls; /* these are allocated now and initialized later */ - scanstate->in_functions = palloc(sizeof(FmgrInfo) * tupdesc->natts); - scanstate->typioparams = palloc(sizeof(Oid) * tupdesc->natts); + scanstate->in_functions = palloc_array(FmgrInfo, tupdesc->natts); + scanstate->typioparams = palloc_array(Oid, tupdesc->natts); /* * Fill in the necessary fmgr infos. @@ -363,7 +364,7 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) char *ns_uri; char *ns_name; - value = ExecEvalExpr((ExprState *) expr, econtext, &isnull); + value = ExecEvalExpr(expr, econtext, &isnull); if (isnull) ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), diff --git a/src/backend/executor/nodeTidrangescan.c b/src/backend/executor/nodeTidrangescan.c index ab2eab9596e42..b387ed6c30836 100644 --- a/src/backend/executor/nodeTidrangescan.c +++ b/src/backend/executor/nodeTidrangescan.c @@ -3,7 +3,7 @@ * nodeTidrangescan.c * Routines to support TID range scans of relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,7 +18,9 @@ #include "access/sysattr.h" #include "access/tableam.h" #include "catalog/pg_operator.h" +#include "executor/execParallel.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeTidrangescan.h" #include "nodes/nodeFuncs.h" #include "utils/rel.h" @@ -72,20 +74,20 @@ MakeTidOpExpr(OpExpr *expr, TidRangeScanState *tidstate) else elog(ERROR, "could not identify CTID variable"); - tidopexpr = (TidOpExpr *) palloc(sizeof(TidOpExpr)); + tidopexpr = palloc_object(TidOpExpr); tidopexpr->inclusive = false; /* for now */ switch (expr->opno) { case TIDLessEqOperator: tidopexpr->inclusive = true; - /* fall through */ + pg_fallthrough; case TIDLessOperator: tidopexpr->exprtype = invert ? TIDEXPR_LOWER_BOUND : TIDEXPR_UPPER_BOUND; break; case TIDGreaterEqOperator: tidopexpr->inclusive = true; - /* fall through */ + pg_fallthrough; case TIDGreaterOperator: tidopexpr->exprtype = invert ? TIDEXPR_UPPER_BOUND : TIDEXPR_LOWER_BOUND; break; @@ -128,9 +130,11 @@ TidExprListCreate(TidRangeScanState *tidrangestate) * TidRangeEval * * Compute and set node's block and offset range to scan by evaluating - * the trss_tidexprs. Returns false if we detect the range cannot + * node->trss_tidexprs. Returns false if we detect the range cannot * contain any tuples. Returns true if it's possible for the range to - * contain tuples. + * contain tuples. We don't bother validating that trss_mintid is less + * than or equal to trss_maxtid, as the scan_set_tidrange() table AM + * function will handle that. * ---------------------------------------------------------------- */ static bool @@ -240,10 +244,19 @@ TidRangeNext(TidRangeScanState *node) if (scandesc == NULL) { + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (estate->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; + scandesc = table_beginscan_tidrange(node->ss.ss_currentRelation, estate->es_snapshot, &node->trss_mintid, - &node->trss_maxtid); + &node->trss_maxtid, + flags); node->ss.ss_currentScanDesc = scandesc; } else @@ -272,6 +285,16 @@ TidRangeNext(TidRangeScanState *node) static bool TidRangeRecheck(TidRangeScanState *node, TupleTableSlot *slot) { + if (!TidRangeEval(node)) + return false; + + Assert(ItemPointerIsValid(&slot->tts_tid)); + + /* Recheck the ctid is still within range */ + if (ItemPointerCompare(&slot->tts_tid, &node->trss_mintid) < 0 || + ItemPointerCompare(&slot->tts_tid, &node->trss_maxtid) > 0) + return false; + return true; } @@ -328,6 +351,20 @@ ExecEndTidRangeScan(TidRangeScanState *node) { TableScanDesc scan = node->ss.ss_currentScanDesc; + /* Collect IO stats for this process into shared instrumentation */ + if (node->trss_sinstrument != NULL && IsParallelWorker()) + { + TidRangeScanInstrumentation *si; + + Assert(ParallelWorkerNumber < node->trss_sinstrument->num_workers); + si = &node->trss_sinstrument->sinstrument[ParallelWorkerNumber]; + + if (scan && scan->rs_instrument) + { + AccumulateIOStats(&si->stats.io, &scan->rs_instrument->io); + } + } + if (scan != NULL) table_endscan(scan); } @@ -382,7 +419,8 @@ ExecInitTidRangeScan(TidRangeScan *node, EState *estate, int eflags) */ ExecInitScanTupleSlot(estate, &tidrangestate->ss, RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation)); + table_slot_callbacks(currentRelation), + TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); /* * Initialize result type and projection. @@ -403,3 +441,181 @@ ExecInitTidRangeScan(TidRangeScan *node, EState *estate, int eflags) */ return tidrangestate; } + +/* ---------------------------------------------------------------- + * Parallel Scan Support + * ---------------------------------------------------------------- + */ + +/* ---------------------------------------------------------------- + * ExecTidRangeScanEstimate + * + * Compute the amount of space we'll need in the parallel + * query DSM, and inform pcxt->estimator about our needs. + * ---------------------------------------------------------------- + */ +void +ExecTidRangeScanEstimate(TidRangeScanState *node, ParallelContext *pcxt) +{ + EState *estate = node->ss.ps.state; + + node->trss_pscanlen = + table_parallelscan_estimate(node->ss.ss_currentRelation, + estate->es_snapshot); + shm_toc_estimate_chunk(&pcxt->estimator, node->trss_pscanlen); + shm_toc_estimate_keys(&pcxt->estimator, 1); +} + +/* ---------------------------------------------------------------- + * ExecTidRangeScanInitializeDSM + * + * Set up a parallel TID range scan descriptor. + * ---------------------------------------------------------------- + */ +void +ExecTidRangeScanInitializeDSM(TidRangeScanState *node, ParallelContext *pcxt) +{ + EState *estate = node->ss.ps.state; + ParallelTableScanDesc pscan; + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (estate->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; + + pscan = shm_toc_allocate(pcxt->toc, node->trss_pscanlen); + table_parallelscan_initialize(node->ss.ss_currentRelation, + pscan, + estate->es_snapshot); + shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, pscan); + node->ss.ss_currentScanDesc = + table_beginscan_parallel_tidrange(node->ss.ss_currentRelation, + pscan, flags); +} + +/* ---------------------------------------------------------------- + * ExecTidRangeScanReInitializeDSM + * + * Reset shared state before beginning a fresh scan. + * ---------------------------------------------------------------- + */ +void +ExecTidRangeScanReInitializeDSM(TidRangeScanState *node, + ParallelContext *pcxt) +{ + ParallelTableScanDesc pscan; + + pscan = node->ss.ss_currentScanDesc->rs_parallel; + table_parallelscan_reinitialize(node->ss.ss_currentRelation, pscan); +} + +/* ---------------------------------------------------------------- + * ExecTidRangeScanInitializeWorker + * + * Copy relevant information from TOC into planstate. + * ---------------------------------------------------------------- + */ +void +ExecTidRangeScanInitializeWorker(TidRangeScanState *node, + ParallelWorkerContext *pwcxt) +{ + ParallelTableScanDesc pscan; + uint32 flags = SO_NONE; + + if (ScanRelIsReadOnly(&node->ss)) + flags |= SO_HINT_REL_READ_ONLY; + + if (node->ss.ps.state->es_instrument & INSTRUMENT_IO) + flags |= SO_SCAN_INSTRUMENT; + + pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false); + node->ss.ss_currentScanDesc = + table_beginscan_parallel_tidrange(node->ss.ss_currentRelation, + pscan, flags); +} + +/* + * Compute the amount of space we'll need for the shared instrumentation and + * inform pcxt->estimator. + */ +void +ExecTidRangeScanInstrumentEstimate(TidRangeScanState *node, + ParallelContext *pcxt) +{ + EState *estate = node->ss.ps.state; + Size size; + + if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedTidRangeScanInstrumentation, sinstrument), + mul_size(pcxt->nworkers, sizeof(TidRangeScanInstrumentation))); + + shm_toc_estimate_chunk(&pcxt->estimator, size); + shm_toc_estimate_keys(&pcxt->estimator, 1); +} + +/* + * Set up parallel scan instrumentation. + */ +void +ExecTidRangeScanInstrumentInitDSM(TidRangeScanState *node, + ParallelContext *pcxt) +{ + EState *estate = node->ss.ps.state; + SharedTidRangeScanInstrumentation *sinstrument; + Size size; + + if ((estate->es_instrument & INSTRUMENT_IO) == 0 || pcxt->nworkers == 0) + return; + + size = add_size(offsetof(SharedTidRangeScanInstrumentation, sinstrument), + mul_size(pcxt->nworkers, sizeof(TidRangeScanInstrumentation))); + sinstrument = shm_toc_allocate(pcxt->toc, size); + memset(sinstrument, 0, size); + sinstrument->num_workers = pcxt->nworkers; + shm_toc_insert(pcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + sinstrument); + node->trss_sinstrument = sinstrument; +} + +/* + * Look up and save the location of the shared instrumentation. + */ +void +ExecTidRangeScanInstrumentInitWorker(TidRangeScanState *node, + ParallelWorkerContext *pwcxt) +{ + EState *estate = node->ss.ps.state; + + if ((estate->es_instrument & INSTRUMENT_IO) == 0) + return; + + node->trss_sinstrument = shm_toc_lookup(pwcxt->toc, + node->ss.ps.plan->plan_node_id + + PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET, + false); +} + +/* + * Transfer scan instrumentation from DSM to private memory. + */ +void +ExecTidRangeScanRetrieveInstrumentation(TidRangeScanState *node) +{ + SharedTidRangeScanInstrumentation *sinstrument = node->trss_sinstrument; + Size size; + + if (sinstrument == NULL) + return; + + size = offsetof(SharedTidRangeScanInstrumentation, sinstrument) + + sinstrument->num_workers * sizeof(TidRangeScanInstrumentation); + + node->trss_sinstrument = palloc(size); + memcpy(node->trss_sinstrument, sinstrument, size); +} diff --git a/src/backend/executor/nodeTidscan.c b/src/backend/executor/nodeTidscan.c index 5e56e29a15fc4..6641df1099912 100644 --- a/src/backend/executor/nodeTidscan.c +++ b/src/backend/executor/nodeTidscan.c @@ -3,7 +3,7 @@ * nodeTidscan.c * Routines to support direct tid scans of relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -78,7 +78,7 @@ TidExprListCreate(TidScanState *tidstate) foreach(l, node->tidquals) { Expr *expr = (Expr *) lfirst(l); - TidExpr *tidexpr = (TidExpr *) palloc0(sizeof(TidExpr)); + TidExpr *tidexpr = palloc0_object(TidExpr); if (is_opclause(expr)) { @@ -402,12 +402,23 @@ TidNext(TidScanState *node) static bool TidRecheck(TidScanState *node, TupleTableSlot *slot) { + ItemPointer match; + + /* WHERE CURRENT OF always intends to resolve to the latest tuple */ + if (node->tss_isCurrentOf) + return true; + + if (node->tss_TidList == NULL) + TidListEval(node); + /* - * XXX shouldn't we check here to make sure tuple matches TID list? In - * runtime-key case this is not certain, is it? However, in the WHERE - * CURRENT OF case it might not match anyway ... + * Binary search the TidList to see if this ctid is mentioned and return + * true if it is. */ - return true; + match = (ItemPointer) bsearch(&slot->tts_tid, node->tss_TidList, + node->tss_NumTids, sizeof(ItemPointerData), + itemptr_comparator); + return match != NULL; } @@ -525,7 +536,8 @@ ExecInitTidScan(TidScan *node, EState *estate, int eflags) */ ExecInitScanTupleSlot(estate, &tidstate->ss, RelationGetDescr(currentRelation), - table_slot_callbacks(currentRelation)); + table_slot_callbacks(currentRelation), + TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeUnique.c b/src/backend/executor/nodeUnique.c index 3854ad285c4eb..0898d4e693bf8 100644 --- a/src/backend/executor/nodeUnique.c +++ b/src/backend/executor/nodeUnique.c @@ -11,7 +11,7 @@ * (It's debatable whether the savings justifies carrying two plan node * types, though.) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/executor/nodeValuesscan.c b/src/backend/executor/nodeValuesscan.c index 8e85a5f2e9a9c..effc896ea1ce3 100644 --- a/src/backend/executor/nodeValuesscan.c +++ b/src/backend/executor/nodeValuesscan.c @@ -4,7 +4,7 @@ * Support routines for scanning Values lists * ("VALUES (...), (...), ..." in rangetable). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -247,7 +247,7 @@ ExecInitValuesScan(ValuesScan *node, EState *estate, int eflags) * Get info about values list, initialize scan slot with it. */ tupdesc = ExecTypeFromExprList((List *) linitial(node->values_lists)); - ExecInitScanTupleSlot(estate, &scanstate->ss, tupdesc, &TTSOpsVirtual); + ExecInitScanTupleSlot(estate, &scanstate->ss, tupdesc, &TTSOpsVirtual, 0); /* * Initialize result type and projection. diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index 9a1acce2b5d36..f52a7aae8434c 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -23,7 +23,7 @@ * aggregate function over all rows in the current row's window frame. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -37,7 +37,9 @@ #include "catalog/objectaccess.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_proc.h" +#include "common/int.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "executor/nodeWindowAgg.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" @@ -53,6 +55,7 @@ #include "utils/memutils.h" #include "utils/regproc.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "windowapi.h" /* @@ -69,6 +72,16 @@ typedef struct WindowObjectData int readptr; /* tuplestore read pointer for this fn */ int64 markpos; /* row that markptr is positioned on */ int64 seekpos; /* row that readptr is positioned on */ + uint8 **notnull_info; /* not null info for each func args */ + int64 *num_notnull_info; /* track size (number of tuples in + * partition) of the notnull_info array + * for each func args */ + + /* + * Null treatment options. One of: NO_NULLTREATMENT, PARSER_IGNORE_NULLS, + * PARSER_RESPECT_NULLS or IGNORE_NULLS. + */ + int ignore_nulls; } WindowObjectData; /* @@ -96,9 +109,10 @@ typedef struct WindowStatePerFuncData bool plain_agg; /* is it just a plain aggregate function? */ int aggno; /* if so, index of its WindowStatePerAggData */ + uint8 ignore_nulls; /* ignore nulls */ WindowObject winobj; /* object used in window function API */ -} WindowStatePerFuncData; +} WindowStatePerFuncData; /* * For plain aggregate window functions, we also have one of these. @@ -182,8 +196,8 @@ static void begin_partition(WindowAggState *winstate); static void spool_tuples(WindowAggState *winstate, int64 pos); static void release_partition(WindowAggState *winstate); -static int row_is_in_frame(WindowAggState *winstate, int64 pos, - TupleTableSlot *slot); +static int row_is_in_frame(WindowObject winobj, int64 pos, + TupleTableSlot *slot, bool fetch_tuple); static void update_frameheadpos(WindowAggState *winstate); static void update_frametailpos(WindowAggState *winstate); static void update_grouptailpos(WindowAggState *winstate); @@ -198,6 +212,38 @@ static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1, static bool window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot); +static Datum ignorenulls_getfuncarginframe(WindowObject winobj, int argno, + int relpos, int seektype, + bool set_mark, bool *isnull, + bool *isout); +static Datum gettuple_eval_partition(WindowObject winobj, int argno, + int64 abs_pos, bool *isnull, + bool *isout); +static void init_notnull_info(WindowObject winobj, + WindowStatePerFunc perfuncstate); +static void grow_notnull_info(WindowObject winobj, + int64 pos, int argno); +static uint8 get_notnull_info(WindowObject winobj, + int64 pos, int argno); +static void put_notnull_info(WindowObject winobj, + int64 pos, int argno, bool isnull); + +/* + * Not null info bit array consists of 2-bit items + */ +#define NN_UNKNOWN 0x00 /* value not calculated yet */ +#define NN_NULL 0x01 /* NULL */ +#define NN_NOTNULL 0x02 /* NOT NULL */ +#define NN_MASK 0x03 /* mask for NOT NULL MAP */ +#define NN_BITS_PER_MEMBER 2 /* number of bits in not null map */ +/* number of items per variable */ +#define NN_ITEM_PER_VAR (BITS_PER_BYTE / NN_BITS_PER_MEMBER) +/* convert map position to byte offset */ +#define NN_POS_TO_BYTES(pos) ((pos) / NN_ITEM_PER_VAR) +/* bytes offset to map position */ +#define NN_BYTES_TO_POS(bytes) ((bytes) * NN_ITEM_PER_VAR) +/* calculate shift bits */ +#define NN_SHIFT(pos) ((pos) % NN_ITEM_PER_VAR) * NN_BITS_PER_MEMBER /* * initialize_windowaggregate @@ -942,7 +988,8 @@ eval_windowaggregates(WindowAggState *winstate) * Exit loop if no more rows can be in frame. Skip aggregation if * current row is not in frame but there might be more in the frame. */ - ret = row_is_in_frame(winstate, winstate->aggregatedupto, agg_row_slot); + ret = row_is_in_frame(agg_winobj, winstate->aggregatedupto, + agg_row_slot, false); if (ret < 0) break; if (ret == 0) @@ -1263,6 +1310,22 @@ begin_partition(WindowAggState *winstate) winobj->markpos = -1; winobj->seekpos = -1; + + /* reset null map */ + if (winobj->ignore_nulls == IGNORE_NULLS || + winobj->ignore_nulls == PARSER_IGNORE_NULLS) + { + int numargs = perfuncstate->numArguments; + + for (int j = 0; j < numargs; j++) + { + int n = winobj->num_notnull_info[j]; + + if (n > 0) + memset(winobj->notnull_info[j], 0, + NN_POS_TO_BYTES(n)); + } + } } } @@ -1412,8 +1475,8 @@ release_partition(WindowAggState *winstate) * to our window framing rule * * The caller must have already determined that the row is in the partition - * and fetched it into a slot. This function just encapsulates the framing - * rules. + * and fetched it into a slot if fetch_tuple is false. + * This function just encapsulates the framing rules. * * Returns: * -1, if the row is out of frame and no succeeding rows can be in frame @@ -1423,8 +1486,10 @@ release_partition(WindowAggState *winstate) * May clobber winstate->temp_slot_2. */ static int -row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot) +row_is_in_frame(WindowObject winobj, int64 pos, TupleTableSlot *slot, + bool fetch_tuple) { + WindowAggState *winstate = winobj->winstate; int frameOptions = winstate->frameOptions; Assert(pos >= 0); /* else caller error */ @@ -1453,9 +1518,14 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot) else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS)) { /* following row that is not peer is out of frame */ - if (pos > winstate->currentpos && - !are_peers(winstate, slot, winstate->ss.ss_ScanTupleSlot)) - return -1; + if (pos > winstate->currentpos) + { + if (fetch_tuple) /* need to fetch tuple? */ + if (!window_gettupleslot(winobj, pos, slot)) + return -1; + if (!are_peers(winstate, slot, winstate->ss.ss_ScanTupleSlot)) + return -1; + } } else Assert(false); @@ -1465,12 +1535,21 @@ row_is_in_frame(WindowAggState *winstate, int64 pos, TupleTableSlot *slot) if (frameOptions & FRAMEOPTION_ROWS) { int64 offset = DatumGetInt64(winstate->endOffsetValue); + int64 frameendpos = 0; /* rows after current row + offset are out of frame */ if (frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) offset = -offset; - if (pos > winstate->currentpos + offset) + /* + * If we have an overflow, it means the frame end is beyond the + * range of int64. Since currentpos >= 0, this can only be a + * positive overflow. We treat this as meaning that the frame + * extends to end of partition. + */ + if (!pg_add_s64_overflow(winstate->currentpos, offset, + &frameendpos) && + pos > frameendpos) return -1; } else if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS)) @@ -1605,7 +1684,16 @@ update_frameheadpos(WindowAggState *winstate) if (frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) offset = -offset; - winstate->frameheadpos = winstate->currentpos + offset; + /* + * If we have an overflow, it means the frame head is beyond the + * range of int64. Since currentpos >= 0, this can only be a + * positive overflow. We treat this as being beyond end of + * partition. + */ + if (pg_add_s64_overflow(winstate->currentpos, offset, + &winstate->frameheadpos)) + winstate->frameheadpos = PG_INT64_MAX; + /* frame head can't go before first row */ if (winstate->frameheadpos < 0) winstate->frameheadpos = 0; @@ -1717,12 +1805,21 @@ update_frameheadpos(WindowAggState *winstate) * framehead_slot empty. */ int64 offset = DatumGetInt64(winstate->startOffsetValue); - int64 minheadgroup; + int64 minheadgroup = 0; if (frameOptions & FRAMEOPTION_START_OFFSET_PRECEDING) minheadgroup = winstate->currentgroup - offset; else - minheadgroup = winstate->currentgroup + offset; + { + /* + * If we have an overflow, it means the target group is beyond + * the range of int64. We treat this as "infinity", which + * ensures the loop below advances to end of partition. + */ + if (pg_add_s64_overflow(winstate->currentgroup, offset, + &minheadgroup)) + minheadgroup = PG_INT64_MAX; + } tuplestore_select_read_pointer(winstate->buffer, winstate->framehead_ptr); @@ -1859,7 +1956,18 @@ update_frametailpos(WindowAggState *winstate) if (frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) offset = -offset; - winstate->frametailpos = winstate->currentpos + offset + 1; + /* + * If we have an overflow, it means the frame tail is beyond the + * range of int64. Since currentpos >= 0, this can only be a + * positive overflow. We treat this as being beyond end of + * partition. + */ + if (pg_add_s64_overflow(winstate->currentpos, offset, + &winstate->frametailpos) || + pg_add_s64_overflow(winstate->frametailpos, 1, + &winstate->frametailpos)) + winstate->frametailpos = PG_INT64_MAX; + /* smallest allowable value of frametailpos is 0 */ if (winstate->frametailpos < 0) winstate->frametailpos = 0; @@ -1971,12 +2079,21 @@ update_frametailpos(WindowAggState *winstate) * leave frametailpos = end+1 and frametail_slot empty. */ int64 offset = DatumGetInt64(winstate->endOffsetValue); - int64 maxtailgroup; + int64 maxtailgroup = 0; if (frameOptions & FRAMEOPTION_END_OFFSET_PRECEDING) maxtailgroup = winstate->currentgroup - offset; else - maxtailgroup = winstate->currentgroup + offset; + { + /* + * If we have an overflow, it means the target group is beyond + * the range of int64. We treat this as "infinity", which + * ensures the loop below advances to end of partition. + */ + if (pg_add_s64_overflow(winstate->currentgroup, offset, + &maxtailgroup)) + maxtailgroup = PG_INT64_MAX; + } tuplestore_select_read_pointer(winstate->buffer, winstate->frametail_ptr); @@ -2594,14 +2711,14 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) numfuncs = winstate->numfuncs; numaggs = winstate->numaggs; econtext = winstate->ss.ps.ps_ExprContext; - econtext->ecxt_aggvalues = (Datum *) palloc0(sizeof(Datum) * numfuncs); - econtext->ecxt_aggnulls = (bool *) palloc0(sizeof(bool) * numfuncs); + econtext->ecxt_aggvalues = palloc0_array(Datum, numfuncs); + econtext->ecxt_aggnulls = palloc0_array(bool, numfuncs); /* * allocate per-wfunc/per-agg state information. */ - perfunc = (WindowStatePerFunc) palloc0(sizeof(WindowStatePerFuncData) * numfuncs); - peragg = (WindowStatePerAgg) palloc0(sizeof(WindowStatePerAggData) * numaggs); + perfunc = palloc0_array(WindowStatePerFuncData, numfuncs); + peragg = palloc0_array(WindowStatePerAggData, numaggs); winstate->perfunc = perfunc; winstate->peragg = peragg; @@ -2619,14 +2736,17 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) elog(ERROR, "WindowFunc with winref %u assigned to WindowAgg with winref %u", wfunc->winref, node->winref); - /* Look for a previous duplicate window function */ + /* + * Look for a previous duplicate window function, which needs the same + * ignore_nulls value + */ for (i = 0; i <= wfuncno; i++) { if (equal(wfunc, perfunc[i].wfunc) && !contain_volatile_functions((Node *) wfunc)) break; } - if (i <= wfuncno) + if (i <= wfuncno && wfunc->ignore_nulls == perfunc[i].ignore_nulls) { /* Found a match to an existing entry, so just mark it */ wfuncstate->wfuncno = i; @@ -2679,6 +2799,8 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) winobj->argstates = wfuncstate->args; winobj->localmem = NULL; perfuncstate->winobj = winobj; + winobj->ignore_nulls = wfunc->ignore_nulls; + init_notnull_info(winobj, perfuncstate); /* It's a real window function, so set up to call it. */ fmgr_info_cxt(wfunc->winfnoid, &perfuncstate->flinfo, @@ -3214,12 +3336,315 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot) return true; } +/* gettuple_eval_partition + * get tuple in a partition and evaluate the window function's argument + * expression on it. + */ +static Datum +gettuple_eval_partition(WindowObject winobj, int argno, + int64 abs_pos, bool *isnull, bool *isout) +{ + WindowAggState *winstate; + ExprContext *econtext; + TupleTableSlot *slot; + + winstate = winobj->winstate; + slot = winstate->temp_slot_1; + if (!window_gettupleslot(winobj, abs_pos, slot)) + { + /* out of partition */ + if (isout) + *isout = true; + *isnull = true; + return (Datum) 0; + } + + if (isout) + *isout = false; + econtext = winstate->ss.ps.ps_ExprContext; + econtext->ecxt_outertuple = slot; + return ExecEvalExpr((ExprState *) list_nth + (winobj->argstates, argno), + econtext, isnull); +} + +/* + * ignorenulls_getfuncarginframe + * For IGNORE NULLS, get the next nonnull value in the frame, moving forward + * or backward until we find a value or reach the frame's end. + */ +static Datum +ignorenulls_getfuncarginframe(WindowObject winobj, int argno, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout) +{ + WindowAggState *winstate; + ExprContext *econtext; + TupleTableSlot *slot; + Datum datum; + int64 abs_pos; + int64 mark_pos; + int notnull_offset; + int notnull_relpos; + int forward; + + Assert(WindowObjectIsValid(winobj)); + winstate = winobj->winstate; + econtext = winstate->ss.ps.ps_ExprContext; + slot = winstate->temp_slot_1; + datum = (Datum) 0; + notnull_offset = 0; + notnull_relpos = abs(relpos); + + switch (seektype) + { + case WINDOW_SEEK_CURRENT: + elog(ERROR, "WINDOW_SEEK_CURRENT is not supported for WinGetFuncArgInFrame"); + abs_pos = mark_pos = 0; /* keep compiler quiet */ + break; + case WINDOW_SEEK_HEAD: + /* rejecting relpos < 0 is easy and simplifies code below */ + if (relpos < 0) + goto out_of_frame; + update_frameheadpos(winstate); + abs_pos = winstate->frameheadpos; + mark_pos = winstate->frameheadpos; + forward = 1; + break; + case WINDOW_SEEK_TAIL: + /* rejecting relpos > 0 is easy and simplifies code below */ + if (relpos > 0) + goto out_of_frame; + update_frametailpos(winstate); + abs_pos = winstate->frametailpos - 1; + mark_pos = 0; /* keep compiler quiet */ + forward = -1; + break; + default: + elog(ERROR, "unrecognized window seek type: %d", seektype); + abs_pos = mark_pos = 0; /* keep compiler quiet */ + break; + } + + /* + * Get the next nonnull value in the frame, moving forward or backward + * until we find a value or reach the frame's end. + */ + do + { + int inframe; + int v; + + /* + * Check apparent out of frame case. We need to do this because we + * may not call window_gettupleslot before row_is_in_frame, which + * supposes abs_pos is never negative. + */ + if (abs_pos < 0) + goto out_of_frame; + + /* check whether row is in frame */ + inframe = row_is_in_frame(winobj, abs_pos, slot, true); + if (inframe == -1) + goto out_of_frame; + else if (inframe == 0) + goto advance; + + if (isout) + *isout = false; + + v = get_notnull_info(winobj, abs_pos, argno); + if (v == NN_NULL) /* this row is known to be NULL */ + goto advance; + + else if (v == NN_UNKNOWN) /* need to check NULL or not */ + { + if (!window_gettupleslot(winobj, abs_pos, slot)) + goto out_of_frame; + + econtext->ecxt_outertuple = slot; + datum = ExecEvalExpr( + (ExprState *) list_nth(winobj->argstates, + argno), econtext, + isnull); + if (!*isnull) + notnull_offset++; + + /* record the row status */ + put_notnull_info(winobj, abs_pos, argno, *isnull); + } + else /* this row is known to be NOT NULL */ + { + notnull_offset++; + if (notnull_offset > notnull_relpos) + { + /* to prepare exiting this loop, datum needs to be set */ + if (!window_gettupleslot(winobj, abs_pos, slot)) + goto out_of_frame; + + econtext->ecxt_outertuple = slot; + datum = ExecEvalExpr( + (ExprState *) list_nth + (winobj->argstates, argno), + econtext, isnull); + } + } +advance: + abs_pos += forward; + } while (notnull_offset <= notnull_relpos); + + if (set_mark) + WinSetMarkPosition(winobj, mark_pos); + + return datum; + +out_of_frame: + if (isout) + *isout = true; + *isnull = true; + return (Datum) 0; +} + + +/* + * init_notnull_info + * Initialize non null map. + */ +static void +init_notnull_info(WindowObject winobj, WindowStatePerFunc perfuncstate) +{ + int numargs = perfuncstate->numArguments; + + if (winobj->ignore_nulls == PARSER_IGNORE_NULLS) + { + winobj->notnull_info = palloc0_array(uint8 *, numargs); + winobj->num_notnull_info = palloc0_array(int64, numargs); + } +} + +/* + * grow_notnull_info + * expand notnull_info if necessary. + * pos: not null info position + * argno: argument number +*/ +static void +grow_notnull_info(WindowObject winobj, int64 pos, int argno) +{ +/* initial number of notnull info members */ +#define INIT_NOT_NULL_INFO_NUM 128 + + if (pos >= winobj->num_notnull_info[argno]) + { + /* We may be called in a short-lived context */ + MemoryContext oldcontext = MemoryContextSwitchTo + (winobj->winstate->ss.ps.ps_ExprContext->ecxt_per_query_memory); + + for (;;) + { + Size oldsize = NN_POS_TO_BYTES + (winobj->num_notnull_info[argno]); + Size newsize; + + if (oldsize == 0) /* memory has not been allocated yet for this + * arg */ + { + newsize = NN_POS_TO_BYTES(INIT_NOT_NULL_INFO_NUM); + winobj->notnull_info[argno] = palloc0(newsize); + } + else + { + newsize = oldsize * 2; + winobj->notnull_info[argno] = + repalloc0(winobj->notnull_info[argno], oldsize, newsize); + } + winobj->num_notnull_info[argno] = NN_BYTES_TO_POS(newsize); + if (winobj->num_notnull_info[argno] > pos) + break; + } + MemoryContextSwitchTo(oldcontext); + } +} + +/* + * get_notnull_info + * retrieve a map + * pos: map position + * argno: argument number + */ +static uint8 +get_notnull_info(WindowObject winobj, int64 pos, int argno) +{ + uint8 *mbp; + uint8 mb; + int64 bpos; + + grow_notnull_info(winobj, pos, argno); + bpos = NN_POS_TO_BYTES(pos); + mbp = winobj->notnull_info[argno]; + mb = mbp[bpos]; + return (mb >> (NN_SHIFT(pos))) & NN_MASK; +} + +/* + * put_notnull_info + * update map + * pos: map position + * argno: argument number + * isnull: indicate NULL or NOT + */ +static void +put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull) +{ + uint8 *mbp; + uint8 mb; + int64 bpos; + uint8 val = isnull ? NN_NULL : NN_NOTNULL; + int shift; + + grow_notnull_info(winobj, pos, argno); + bpos = NN_POS_TO_BYTES(pos); + mbp = winobj->notnull_info[argno]; + mb = mbp[bpos]; + shift = NN_SHIFT(pos); + mb &= ~(NN_MASK << shift); /* clear map */ + mb |= (val << shift); /* update map */ + mbp[bpos] = mb; +} /*********************************************************************** * API exposed to window functions ***********************************************************************/ +/* + * WinCheckAndInitializeNullTreatment + * Check null treatment clause and sets ignore_nulls + * + * Window functions should call this to check if they are being called with + * a null treatment clause when they don't allow it, or to set ignore_nulls. + */ +void +WinCheckAndInitializeNullTreatment(WindowObject winobj, + bool allowNullTreatment, + FunctionCallInfo fcinfo) +{ + Assert(WindowObjectIsValid(winobj)); + if (winobj->ignore_nulls != NO_NULLTREATMENT && !allowNullTreatment) + { + const char *funcname = get_func_name(fcinfo->flinfo->fn_oid); + + if (!funcname) + elog(ERROR, "could not get function name"); + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("function %s does not allow RESPECT/IGNORE NULLS", + funcname))); + } + else if (winobj->ignore_nulls == PARSER_IGNORE_NULLS) + winobj->ignore_nulls = IGNORE_NULLS; +} + /* * WinGetPartitionLocalMemory * Get working memory that lives till end of partition processing @@ -3378,23 +3803,33 @@ WinGetFuncArgInPartition(WindowObject winobj, int argno, bool *isnull, bool *isout) { WindowAggState *winstate; - ExprContext *econtext; - TupleTableSlot *slot; - bool gottuple; int64 abs_pos; + int64 mark_pos; + Datum datum; + bool null_treatment; + int notnull_offset; + int notnull_relpos; + int forward; + bool myisout; Assert(WindowObjectIsValid(winobj)); winstate = winobj->winstate; - econtext = winstate->ss.ps.ps_ExprContext; - slot = winstate->temp_slot_1; + + null_treatment = (winobj->ignore_nulls == IGNORE_NULLS && relpos != 0); switch (seektype) { case WINDOW_SEEK_CURRENT: - abs_pos = winstate->currentpos + relpos; + if (null_treatment) + abs_pos = winstate->currentpos; + else + abs_pos = winstate->currentpos + relpos; break; case WINDOW_SEEK_HEAD: - abs_pos = relpos; + if (null_treatment) + abs_pos = 0; + else + abs_pos = relpos; break; case WINDOW_SEEK_TAIL: spool_tuples(winstate, -1); @@ -3406,25 +3841,94 @@ WinGetFuncArgInPartition(WindowObject winobj, int argno, break; } - gottuple = window_gettupleslot(winobj, abs_pos, slot); - - if (!gottuple) + /* Easy case if IGNORE NULLS is not specified */ + if (!null_treatment) { + /* get tuple and evaluate in partition */ + datum = gettuple_eval_partition(winobj, argno, + abs_pos, isnull, &myisout); + if (!myisout && set_mark) + WinSetMarkPosition(winobj, abs_pos); if (isout) - *isout = true; - *isnull = true; - return (Datum) 0; + *isout = myisout; + return datum; } + + /* Prepare for loop */ + notnull_offset = 0; + notnull_relpos = abs(relpos); + forward = relpos > 0 ? 1 : -1; + myisout = false; + datum = 0; + + /* + * IGNORE NULLS + WINDOW_SEEK_CURRENT + relpos > 0 case, we would fetch + * beyond the current row + relpos to find out the target row. If we mark + * at abs_pos, next call to WinGetFuncArgInPartition or + * WinGetFuncArgInFrame (in case when a window function have multiple + * args) could fail with "cannot fetch row before WindowObject's mark + * position". So keep the mark position at currentpos. + */ + if (seektype == WINDOW_SEEK_CURRENT && relpos > 0) + mark_pos = winstate->currentpos; else { - if (isout) - *isout = false; - if (set_mark) - WinSetMarkPosition(winobj, abs_pos); - econtext->ecxt_outertuple = slot; - return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), - econtext, isnull); + /* + * For other cases we have no idea what position of row callers would + * fetch next time. Also for relpos < 0 case (we go backward), we + * cannot set mark either. For those cases we always set mark at 0. + */ + mark_pos = 0; } + + /* + * Get the next nonnull value in the partition, moving forward or backward + * until we find a value or reach the partition's end. We cache the + * nullness status because we may repeat this process many times. + */ + do + { + int nn_info; /* NOT NULL status */ + + abs_pos += forward; + if (abs_pos < 0) /* clearly out of partition */ + break; + + /* check NOT NULL cached info */ + nn_info = get_notnull_info(winobj, abs_pos, argno); + if (nn_info == NN_NOTNULL) /* this row is known to be NOT NULL */ + notnull_offset++; + else if (nn_info == NN_NULL) /* this row is known to be NULL */ + continue; /* keep on moving forward or backward */ + else /* need to check NULL or not */ + { + /* + * NOT NULL info does not exist yet. Get tuple and evaluate func + * arg in partition. We ignore the return value from + * gettuple_eval_partition because we are just interested in + * whether we are inside or outside of partition, NULL or NOT + * NULL. + */ + (void) gettuple_eval_partition(winobj, argno, + abs_pos, isnull, &myisout); + if (myisout) /* out of partition? */ + break; + if (!*isnull) + notnull_offset++; + /* record the row status */ + put_notnull_info(winobj, abs_pos, argno, *isnull); + } + } while (notnull_offset < notnull_relpos); + + /* get tuple and evaluate func arg in partition */ + datum = gettuple_eval_partition(winobj, argno, + abs_pos, isnull, &myisout); + if (!myisout && set_mark) + WinSetMarkPosition(winobj, mark_pos); + if (isout) + *isout = myisout; + + return datum; } /* @@ -3476,6 +3980,10 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, econtext = winstate->ss.ps.ps_ExprContext; slot = winstate->temp_slot_1; + if (winobj->ignore_nulls == IGNORE_NULLS) + return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype, + set_mark, isnull, isout); + switch (seektype) { case WINDOW_SEEK_CURRENT: @@ -3624,7 +4132,7 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, goto out_of_frame; /* The code above does not detect all out-of-frame cases, so check */ - if (row_is_in_frame(winstate, abs_pos, slot) <= 0) + if (row_is_in_frame(winobj, abs_pos, slot, false) <= 0) goto out_of_frame; if (isout) diff --git a/src/backend/executor/nodeWorktablescan.c b/src/backend/executor/nodeWorktablescan.c index f6379c35d2f14..bbceb9b9437ab 100644 --- a/src/backend/executor/nodeWorktablescan.c +++ b/src/backend/executor/nodeWorktablescan.c @@ -3,7 +3,7 @@ * nodeWorktablescan.c * routines to handle WorkTableScan nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include "executor/executor.h" #include "executor/nodeWorktablescan.h" +#include "utils/tuplestore.h" static TupleTableSlot *WorkTableScanNext(WorkTableScanState *node); @@ -165,7 +166,7 @@ ExecInitWorkTableScan(WorkTableScan *node, EState *estate, int eflags) scanstate->ss.ps.resultopsset = true; scanstate->ss.ps.resultopsfixed = false; - ExecInitScanTupleSlot(estate, &scanstate->ss, NULL, &TTSOpsMinimalTuple); + ExecInitScanTupleSlot(estate, &scanstate->ss, NULL, &TTSOpsMinimalTuple, 0); /* * initialize child expressions diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index ecb2e4ccaa1ca..52f3b11301c55 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -3,7 +3,7 @@ * spi.c * Server Programming Interface * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -32,6 +32,7 @@ #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/typcache.h" @@ -68,7 +69,7 @@ static int _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options, bool fire_triggers); static ParamListInfo _SPI_convert_params(int nargs, Oid *argtypes, - Datum *Values, const char *Nulls); + const Datum *Values, const char *Nulls); static int _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount); @@ -669,7 +670,7 @@ SPI_execute_extended(const char *src, /* Execute a previously prepared plan */ int -SPI_execute_plan(SPIPlanPtr plan, Datum *Values, const char *Nulls, +SPI_execute_plan(SPIPlanPtr plan, const Datum *Values, const char *Nulls, bool read_only, long tcount) { SPIExecuteOptions options; @@ -771,7 +772,7 @@ SPI_execute_plan_with_paramlist(SPIPlanPtr plan, ParamListInfo params, */ int SPI_execute_snapshot(SPIPlanPtr plan, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, Snapshot snapshot, Snapshot crosscheck_snapshot, bool read_only, bool fire_triggers, long tcount) { @@ -811,7 +812,7 @@ SPI_execute_snapshot(SPIPlanPtr plan, int SPI_execute_with_args(const char *src, int nargs, Oid *argtypes, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, bool read_only, long tcount) { int res; @@ -1130,8 +1131,8 @@ SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum, SPI_result = 0; numberOfAttributes = rel->rd_att->natts; - v = (Datum *) palloc(numberOfAttributes * sizeof(Datum)); - n = (bool *) palloc(numberOfAttributes * sizeof(bool)); + v = palloc_array(Datum, numberOfAttributes); + n = palloc_array(bool, numberOfAttributes); /* fetch old values and nulls */ heap_deform_tuple(tuple, rel->rd_att, v, n); @@ -1258,7 +1259,7 @@ SPI_getbinval(HeapTuple tuple, TupleDesc tupdesc, int fnumber, bool *isnull) { SPI_result = SPI_ERROR_NOATTRIBUTE; *isnull = true; - return (Datum) NULL; + return (Datum) 0; } return heap_getattr(tuple, fnumber, tupdesc, isnull); @@ -1443,7 +1444,7 @@ SPI_freetuptable(SPITupleTable *tuptable) */ Portal SPI_cursor_open(const char *name, SPIPlanPtr plan, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, bool read_only) { Portal portal; @@ -2141,8 +2142,7 @@ spi_dest_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ALLOCSET_DEFAULT_SIZES); MemoryContextSwitchTo(tuptabcxt); - _SPI_current->tuptable = tuptable = (SPITupleTable *) - palloc0(sizeof(SPITupleTable)); + _SPI_current->tuptable = tuptable = palloc0_object(SPITupleTable); tuptable->tuptabcxt = tuptabcxt; tuptable->subid = GetCurrentSubTransactionId(); @@ -2155,7 +2155,7 @@ spi_dest_startup(DestReceiver *self, int operation, TupleDesc typeinfo) /* set up initial allocations */ tuptable->alloced = 128; - tuptable->vals = (HeapTuple *) palloc(tuptable->alloced * sizeof(HeapTuple)); + tuptable->vals = palloc_array(HeapTuple, tuptable->alloced); tuptable->numvals = 0; tuptable->tupdesc = CreateTupleDescCopy(typeinfo); @@ -2847,7 +2847,7 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options, */ static ParamListInfo _SPI_convert_params(int nargs, Oid *argtypes, - Datum *Values, const char *Nulls) + const Datum *Values, const char *Nulls) { ParamListInfo paramLI; @@ -3162,7 +3162,7 @@ _SPI_make_plan_non_temp(SPIPlanPtr plan) oldcxt = MemoryContextSwitchTo(plancxt); /* Copy the _SPI_plan struct and subsidiary data into the new context */ - newplan = (SPIPlanPtr) palloc0(sizeof(_SPI_plan)); + newplan = palloc0_object(_SPI_plan); newplan->magic = _SPI_PLAN_MAGIC; newplan->plancxt = plancxt; newplan->parse_mode = plan->parse_mode; @@ -3170,7 +3170,7 @@ _SPI_make_plan_non_temp(SPIPlanPtr plan) newplan->nargs = plan->nargs; if (plan->nargs > 0) { - newplan->argtypes = (Oid *) palloc(plan->nargs * sizeof(Oid)); + newplan->argtypes = palloc_array(Oid, plan->nargs); memcpy(newplan->argtypes, plan->argtypes, plan->nargs * sizeof(Oid)); } else @@ -3227,7 +3227,7 @@ _SPI_save_plan(SPIPlanPtr plan) oldcxt = MemoryContextSwitchTo(plancxt); /* Copy the SPI plan into its own context */ - newplan = (SPIPlanPtr) palloc0(sizeof(_SPI_plan)); + newplan = palloc0_object(_SPI_plan); newplan->magic = _SPI_PLAN_MAGIC; newplan->plancxt = plancxt; newplan->parse_mode = plan->parse_mode; @@ -3235,7 +3235,7 @@ _SPI_save_plan(SPIPlanPtr plan) newplan->nargs = plan->nargs; if (plan->nargs > 0) { - newplan->argtypes = (Oid *) palloc(plan->nargs * sizeof(Oid)); + newplan->argtypes = palloc_array(Oid, plan->nargs); memcpy(newplan->argtypes, plan->argtypes, plan->nargs * sizeof(Oid)); } else @@ -3369,7 +3369,7 @@ SPI_register_trigger_data(TriggerData *tdata) if (tdata->tg_newtable) { EphemeralNamedRelation enr = - palloc(sizeof(EphemeralNamedRelationData)); + palloc_object(EphemeralNamedRelationData); int rc; enr->md.name = tdata->tg_trigger->tgnewtable; @@ -3386,7 +3386,7 @@ SPI_register_trigger_data(TriggerData *tdata) if (tdata->tg_oldtable) { EphemeralNamedRelation enr = - palloc(sizeof(EphemeralNamedRelationData)); + palloc_object(EphemeralNamedRelationData); int rc; enr->md.name = tdata->tg_trigger->tgoldtable; diff --git a/src/backend/executor/tqueue.c b/src/backend/executor/tqueue.c index 6c5e1f1262d82..ef81e783184dd 100644 --- a/src/backend/executor/tqueue.c +++ b/src/backend/executor/tqueue.c @@ -8,7 +8,7 @@ * * A TupleQueueReader reads tuples from a shm_mq and returns the tuples. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -120,7 +120,7 @@ CreateTupleQueueDestReceiver(shm_mq_handle *handle) { TQueueDestReceiver *self; - self = (TQueueDestReceiver *) palloc0(sizeof(TQueueDestReceiver)); + self = palloc0_object(TQueueDestReceiver); self->pub.receiveSlot = tqueueReceiveSlot; self->pub.rStartup = tqueueStartupReceiver; @@ -138,7 +138,7 @@ CreateTupleQueueDestReceiver(shm_mq_handle *handle) TupleQueueReader * CreateTupleQueueReader(shm_mq_handle *handle) { - TupleQueueReader *reader = palloc0(sizeof(TupleQueueReader)); + TupleQueueReader *reader = palloc0_object(TupleQueueReader); reader->queue = handle; diff --git a/src/backend/executor/tstoreReceiver.c b/src/backend/executor/tstoreReceiver.c index 562de67645771..8531d4ca43213 100644 --- a/src/backend/executor/tstoreReceiver.c +++ b/src/backend/executor/tstoreReceiver.c @@ -11,7 +11,7 @@ * Also optionally, we can apply a tuple conversion map before storing. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -25,6 +25,7 @@ #include "access/detoast.h" #include "access/tupconvert.h" #include "executor/tstoreReceiver.h" +#include "varatt.h" typedef struct @@ -160,7 +161,7 @@ tstoreReceiveSlot_detoast(TupleTableSlot *slot, DestReceiver *self) { if (VARATT_IS_EXTERNAL(DatumGetPointer(val))) { - val = PointerGetDatum(detoast_external_attr((struct varlena *) + val = PointerGetDatum(detoast_external_attr((varlena *) DatumGetPointer(val))); myState->tofree[nfree++] = val; } @@ -237,7 +238,7 @@ tstoreDestroyReceiver(DestReceiver *self) DestReceiver * CreateTuplestoreDestReceiver(void) { - TStoreState *self = (TStoreState *) palloc0(sizeof(TStoreState)); + TStoreState *self = palloc0_object(TStoreState); self->pub.receiveSlot = tstoreReceiveSlot_notoast; /* might change */ self->pub.rStartup = tstoreStartupReceiver; diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index a57e59f27ea64..821d45c1e110d 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -3,7 +3,7 @@ * foreign.c * support for foreign-data wrappers, servers and user mappings. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/foreign/foreign.c @@ -28,6 +28,7 @@ #include "utils/memutils.h" #include "utils/rel.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/varlena.h" @@ -47,7 +48,7 @@ GetForeignDataWrapper(Oid fdwid) * be found instead of raising an error. */ ForeignDataWrapper * -GetForeignDataWrapperExtended(Oid fdwid, bits16 flags) +GetForeignDataWrapperExtended(Oid fdwid, uint16 flags) { Form_pg_foreign_data_wrapper fdwform; ForeignDataWrapper *fdw; @@ -66,12 +67,13 @@ GetForeignDataWrapperExtended(Oid fdwid, bits16 flags) fdwform = (Form_pg_foreign_data_wrapper) GETSTRUCT(tp); - fdw = (ForeignDataWrapper *) palloc(sizeof(ForeignDataWrapper)); + fdw = palloc_object(ForeignDataWrapper); fdw->fdwid = fdwid; fdw->owner = fdwform->fdwowner; fdw->fdwname = pstrdup(NameStr(fdwform->fdwname)); fdw->fdwhandler = fdwform->fdwhandler; fdw->fdwvalidator = fdwform->fdwvalidator; + fdw->fdwconnection = fdwform->fdwconnection; /* Extract the fdwoptions */ datum = SysCacheGetAttr(FOREIGNDATAWRAPPEROID, @@ -121,7 +123,7 @@ GetForeignServer(Oid serverid) * instead of raising an error. */ ForeignServer * -GetForeignServerExtended(Oid serverid, bits16 flags) +GetForeignServerExtended(Oid serverid, uint16 flags) { Form_pg_foreign_server serverform; ForeignServer *server; @@ -140,7 +142,7 @@ GetForeignServerExtended(Oid serverid, bits16 flags) serverform = (Form_pg_foreign_server) GETSTRUCT(tp); - server = (ForeignServer *) palloc(sizeof(ForeignServer)); + server = palloc_object(ForeignServer); server->serverid = serverid; server->servername = pstrdup(NameStr(serverform->srvname)); server->owner = serverform->srvowner; @@ -191,6 +193,35 @@ GetForeignServerByName(const char *srvname, bool missing_ok) } +/* + * Retrieve connection string from server's FDW. + * + * NB: leaks into CurrentMemoryContext. + */ +char * +ForeignServerConnectionString(Oid userid, ForeignServer *server) +{ + ForeignDataWrapper *fdw; + Datum connection_datum; + + fdw = GetForeignDataWrapper(server->fdwid); + + if (!OidIsValid(fdw->fdwconnection)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("foreign data wrapper \"%s\" does not support subscription connections", + fdw->fdwname), + errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); + + connection_datum = OidFunctionCall3(fdw->fdwconnection, + ObjectIdGetDatum(userid), + ObjectIdGetDatum(server->serverid), + PointerGetDatum(NULL)); + + return text_to_cstring(DatumGetTextPP(connection_datum)); +} + + /* * GetUserMapping - look up the user mapping. * @@ -227,7 +258,7 @@ GetUserMapping(Oid userid, Oid serverid) MappingUserName(userid), server->servername))); } - um = (UserMapping *) palloc(sizeof(UserMapping)); + um = palloc_object(UserMapping); um->umid = ((Form_pg_user_mapping) GETSTRUCT(tp))->oid; um->userid = userid; um->serverid = serverid; @@ -265,7 +296,7 @@ GetForeignTable(Oid relid) elog(ERROR, "cache lookup failed for foreign table %u", relid); tableform = (Form_pg_foreign_table) GETSTRUCT(tp); - ft = (ForeignTable *) palloc(sizeof(ForeignTable)); + ft = palloc_object(ForeignTable); ft->relid = relid; ft->serverid = tableform->ftserver; @@ -463,7 +494,7 @@ GetFdwRoutineForRelation(Relation relation, bool makecopy) /* We have valid cached data --- does the caller want a copy? */ if (makecopy) { - fdwroutine = (FdwRoutine *) palloc(sizeof(FdwRoutine)); + fdwroutine = palloc_object(FdwRoutine); memcpy(fdwroutine, relation->rd_fdwroutine, sizeof(FdwRoutine)); return fdwroutine; } diff --git a/src/backend/foreign/meson.build b/src/backend/foreign/meson.build index 219c32f535105..0613a2b7f9e96 100644 --- a/src/backend/foreign/meson.build +++ b/src/backend/foreign/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'foreign.c' diff --git a/src/backend/jit/jit.c b/src/backend/jit/jit.c index d2ccef9de8515..3dc82b7b268bc 100644 --- a/src/backend/jit/jit.c +++ b/src/backend/jit/jit.c @@ -8,7 +8,7 @@ * should end up here. * * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/jit/jit.c @@ -26,10 +26,11 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "portability/instr_time.h" +#include "storage/fd.h" #include "utils/fmgrprotos.h" /* GUCs */ -bool jit_enabled = true; +bool jit_enabled = false; char *jit_provider = NULL; bool jit_debugging_support = false; bool jit_dump_bitcode = false; diff --git a/src/backend/jit/llvm/Makefile b/src/backend/jit/llvm/Makefile index e8c12060b93df..7a65290a85aa2 100644 --- a/src/backend/jit/llvm/Makefile +++ b/src/backend/jit/llvm/Makefile @@ -22,16 +22,10 @@ endif PGFILEDESC = "llvmjit - JIT using LLVM" NAME = llvmjit -# LLVM 14 produces deprecation warnings. We'll need to make some changes -# before the relevant functions are removed, but for now silence the warnings. -ifeq ($(GCC), yes) -LLVM_CFLAGS += -Wno-deprecated-declarations -endif - # All files in this directory use LLVM. CFLAGS += $(LLVM_CFLAGS) CXXFLAGS += $(LLVM_CXXFLAGS) -override CPPFLAGS := $(LLVM_CPPFLAGS) $(CPPFLAGS) +override CPPFLAGS += $(LLVM_CPPFLAGS) SHLIB_LINK += $(LLVM_LIBS) # Because this module includes C++ files, we need to use a C++ diff --git a/src/backend/jit/llvm/SectionMemoryManager.cpp b/src/backend/jit/llvm/SectionMemoryManager.cpp index 2171db5f382d0..ba00fd4cf3fe3 100644 --- a/src/backend/jit/llvm/SectionMemoryManager.cpp +++ b/src/backend/jit/llvm/SectionMemoryManager.cpp @@ -1,18 +1,11 @@ /* - * This file is from https://github.com/llvm/llvm-project/pull/71968 - * with minor modifications to avoid name clash and work with older - * LLVM versions. The llvm::backport::SectionMemoryManager class is a - * drop-in replacement for llvm::SectionMemoryManager, for use with - * llvm::RuntimeDyld. It fixes a memory layout bug on large memory - * ARM systems (see pull request for details). If the LLVM project - * eventually commits the change, we may need to resynchronize our - * copy with any further modifications, but they would be unlikely to - * backport it into the LLVM versions that we target so we would still - * need this copy. + * This file is from LLVM 22 (originally pull request #71968), with minor + * modifications to avoid name clash and work with older LLVM versions. It + * replaces llvm::SectionMemoryManager, and is injected into llvm::RuntimeDyld + * to fix a memory layout bug on large memory ARM systems on LLVM < 22. * - * In the future we will switch to using JITLink instead of - * RuntimeDyld where possible, and later remove this code (.cpp, .h, - * .LICENSE) after all LLVM versions that we target allow it. + * We can remove this code (.cpp, .h, .LICENSE) once LLVM 22 is our minimum + * supported version or we've switched to JITLink for at least Aarch64. * * This file is a modified copy of a part of the LLVM source code that * we would normally access from the LLVM library. It is therefore diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c index 46511624f0166..1d8b5f9be54d8 100644 --- a/src/backend/jit/llvm/llvmjit.c +++ b/src/backend/jit/llvm/llvmjit.c @@ -3,7 +3,7 @@ * llvmjit.c * Core part of the LLVM JIT provider. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/jit/llvm/llvmjit.c @@ -54,6 +54,7 @@ typedef struct LLVMJitHandle /* types & functions commonly needed for JITing */ LLVMTypeRef TypeSizeT; +LLVMTypeRef TypeDatum; LLVMTypeRef TypeParamBool; LLVMTypeRef TypeStorageBool; LLVMTypeRef TypePGFunction; @@ -499,7 +500,7 @@ llvm_copy_attributes_at_index(LLVMValueRef v_from, LLVMValueRef v_to, uint32 ind if (num_attributes == 0) return; - attrs = palloc(sizeof(LLVMAttributeRef) * num_attributes); + attrs = palloc_array(LLVMAttributeRef, num_attributes); LLVMGetAttributesAtIndex(v_from, index, attrs); for (int attno = 0; attno < num_attributes; attno++) @@ -673,7 +674,11 @@ llvm_optimize_module(LLVMJitContext *context, LLVMModuleRef module) if (context->base.flags & PGJIT_OPT3) passes = "default"; + else if (context->base.flags & PGJIT_INLINE) + /* if doing inlining, but no expensive optimization, add inline pass */ + passes = "default,mem2reg,inline"; else + /* default includes always-inline pass */ passes = "default,mem2reg"; options = LLVMCreatePassBuilderOptions(); @@ -1011,6 +1016,7 @@ llvm_create_types(void) LLVMDisposeMemoryBuffer(buf); TypeSizeT = llvm_pg_var_type("TypeSizeT"); + TypeDatum = llvm_pg_var_type("TypeDatum"); TypeParamBool = load_return_type(llvm_types_module, "FunctionReturningBool"); TypeStorageBool = llvm_pg_var_type("TypeStorageBool"); TypePGFunction = llvm_pg_var_type("TypePGFunction"); @@ -1056,7 +1062,7 @@ llvm_split_symbol_name(const char *name, char **modname, char **funcname) * Symbol names cannot contain a ., therefore we can split based on * first and last occurrence of one. */ - *funcname = rindex(name, '.'); + *funcname = strrchr(name, '.'); (*funcname)++; /* jump over . */ *modname = pnstrdup(name + strlen("pgextern."), @@ -1121,9 +1127,9 @@ llvm_resolve_symbols(LLVMOrcDefinitionGeneratorRef GeneratorObj, void *Ctx, LLVMOrcCLookupSet LookupSet, size_t LookupSetSize) { #if LLVM_VERSION_MAJOR > 14 - LLVMOrcCSymbolMapPairs symbols = palloc0(sizeof(LLVMOrcCSymbolMapPair) * LookupSetSize); + LLVMOrcCSymbolMapPairs symbols = palloc0_array(LLVMOrcCSymbolMapPair, LookupSetSize); #else - LLVMOrcCSymbolMapPairs symbols = palloc0(sizeof(LLVMJITCSymbolMapPair) * LookupSetSize); + LLVMOrcCSymbolMapPairs symbols = palloc0_array(LLVMJITCSymbolMapPair, LookupSetSize); #endif LLVMErrorRef error; LLVMOrcMaterializationUnitRef mu; @@ -1170,7 +1176,10 @@ llvm_log_jit_error(void *ctx, LLVMErrorRef error) static LLVMOrcObjectLayerRef llvm_create_object_layer(void *Ctx, LLVMOrcExecutionSessionRef ES, const char *Triple) { -#ifdef USE_LLVM_BACKPORT_SECTION_MEMORY_MANAGER +#if LLVM_VERSION_MAJOR >= 22 + LLVMOrcObjectLayerRef objlayer = + LLVMOrcCreateRTDyldObjectLinkingLayerWithSectionMemoryManagerReserveAlloc(ES, true); +#elif defined(USE_LLVM_BACKPORT_SECTION_MEMORY_MANAGER) LLVMOrcObjectLayerRef objlayer = LLVMOrcCreateRTDyldObjectLinkingLayerWithSafeSectionMemoryManager(ES); #else @@ -1178,24 +1187,20 @@ llvm_create_object_layer(void *Ctx, LLVMOrcExecutionSessionRef ES, const char *T LLVMOrcCreateRTDyldObjectLinkingLayerWithSectionMemoryManager(ES); #endif - -#if defined(HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER) && HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER if (jit_debugging_support) { LLVMJITEventListenerRef l = LLVMCreateGDBRegistrationListener(); LLVMOrcRTDyldObjectLinkingLayerRegisterJITEventListener(objlayer, l); } -#endif -#if defined(HAVE_DECL_LLVMCREATEPERFJITEVENTLISTENER) && HAVE_DECL_LLVMCREATEPERFJITEVENTLISTENER if (jit_profiling_support) { LLVMJITEventListenerRef l = LLVMCreatePerfJITEventListener(); - LLVMOrcRTDyldObjectLinkingLayerRegisterJITEventListener(objlayer, l); + if (l) + LLVMOrcRTDyldObjectLinkingLayerRegisterJITEventListener(objlayer, l); } -#endif return objlayer; } diff --git a/src/backend/jit/llvm/llvmjit_deform.c b/src/backend/jit/llvm/llvmjit_deform.c index c562edd094bb2..12521e3e46a3b 100644 --- a/src/backend/jit/llvm/llvmjit_deform.c +++ b/src/backend/jit/llvm/llvmjit_deform.c @@ -7,7 +7,7 @@ * knowledge of the tuple descriptor. Fixed column widths, NOT NULLness, etc * can be taken advantage of. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -62,7 +62,6 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, LLVMValueRef v_tts_values; LLVMValueRef v_tts_nulls; LLVMValueRef v_slotoffp; - LLVMValueRef v_flagsp; LLVMValueRef v_nvalidp; LLVMValueRef v_nvalid; LLVMValueRef v_maxatt; @@ -156,12 +155,12 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, b = LLVMCreateBuilderInContext(lc); - attcheckattnoblocks = palloc(sizeof(LLVMBasicBlockRef) * natts); - attstartblocks = palloc(sizeof(LLVMBasicBlockRef) * natts); - attisnullblocks = palloc(sizeof(LLVMBasicBlockRef) * natts); - attcheckalignblocks = palloc(sizeof(LLVMBasicBlockRef) * natts); - attalignblocks = palloc(sizeof(LLVMBasicBlockRef) * natts); - attstoreblocks = palloc(sizeof(LLVMBasicBlockRef) * natts); + attcheckattnoblocks = palloc_array(LLVMBasicBlockRef, natts); + attstartblocks = palloc_array(LLVMBasicBlockRef, natts); + attisnullblocks = palloc_array(LLVMBasicBlockRef, natts); + attcheckalignblocks = palloc_array(LLVMBasicBlockRef, natts); + attalignblocks = palloc_array(LLVMBasicBlockRef, natts); + attstoreblocks = palloc_array(LLVMBasicBlockRef, natts); known_alignment = 0; @@ -178,7 +177,6 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, v_tts_nulls = l_load_struct_gep(b, StructTupleTableSlot, v_slot, FIELDNO_TUPLETABLESLOT_ISNULL, "tts_ISNULL"); - v_flagsp = l_struct_gep(b, StructTupleTableSlot, v_slot, FIELDNO_TUPLETABLESLOT_FLAGS, ""); v_nvalidp = l_struct_gep(b, StructTupleTableSlot, v_slot, FIELDNO_TUPLETABLESLOT_NVALID, ""); if (ops == &TTSOpsHeapTuple || ops == &TTSOpsBufferHeapTuple) @@ -479,8 +477,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, l_gep(b, LLVMInt8TypeInContext(lc), v_tts_nulls, &l_attno, 1, "")); /* store zero datum */ LLVMBuildStore(b, - l_sizet_const(0), - l_gep(b, TypeSizeT, v_tts_values, &l_attno, 1, "")); + l_datum_const(0), + l_gep(b, TypeDatum, v_tts_values, &l_attno, 1, "")); LLVMBuildBr(b, b_next); attguaranteedalign = false; @@ -644,7 +642,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, } /* compute address to store value at */ - v_resultp = l_gep(b, TypeSizeT, v_tts_values, &l_attno, 1, ""); + v_resultp = l_gep(b, TypeDatum, v_tts_values, &l_attno, 1, ""); /* store null-byte (false) */ LLVMBuildStore(b, l_int8_const(lc, 0), @@ -663,7 +661,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, v_tmp_loaddata = LLVMBuildPointerCast(b, v_attdatap, vartypep, ""); v_tmp_loaddata = l_load(b, vartype, v_tmp_loaddata, "attr_byval"); - v_tmp_loaddata = LLVMBuildZExt(b, v_tmp_loaddata, TypeSizeT, ""); + v_tmp_loaddata = LLVMBuildSExt(b, v_tmp_loaddata, TypeDatum, ""); LLVMBuildStore(b, v_tmp_loaddata, v_resultp); } @@ -675,7 +673,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, v_tmp_loaddata = LLVMBuildPtrToInt(b, v_attdatap, - TypeSizeT, + TypeDatum, "attr_ptr"); LLVMBuildStore(b, v_tmp_loaddata, v_resultp); } @@ -747,14 +745,10 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc, { LLVMValueRef v_off = l_load(b, TypeSizeT, v_offp, ""); - LLVMValueRef v_flags; LLVMBuildStore(b, l_int16_const(lc, natts), v_nvalidp); v_off = LLVMBuildTrunc(b, v_off, LLVMInt32TypeInContext(lc), ""); LLVMBuildStore(b, v_off, v_slotoffp); - v_flags = l_load(b, LLVMInt16TypeInContext(lc), v_flagsp, "tts_flags"); - v_flags = LLVMBuildOr(b, v_flags, l_int16_const(lc, TTS_FLAG_SLOW), ""); - LLVMBuildStore(b, v_flags, v_flagsp); LLVMBuildRetVoid(b); } diff --git a/src/backend/jit/llvm/llvmjit_error.cpp b/src/backend/jit/llvm/llvmjit_error.cpp index b16444d978ef6..eb04515d036cb 100644 --- a/src/backend/jit/llvm/llvmjit_error.cpp +++ b/src/backend/jit/llvm/llvmjit_error.cpp @@ -6,7 +6,7 @@ * Unfortunately neither (re)setting the C++ new handler, nor the LLVM OOM * handler are exposed to C. Therefore this file wraps the necessary code. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/jit/llvm/llvmjit_error.cpp diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c index 890bcb0b0a79d..0e160b8502c3d 100644 --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -3,7 +3,7 @@ * llvmjit_expr.c * JIT compile expressions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -62,7 +62,9 @@ static LLVMValueRef build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, LLVMValueRef v_state, ExprEvalStep *op, int natts, LLVMValueRef *v_args); +#if LLVM_VERSION_MAJOR < 22 static LLVMValueRef create_LifetimeEnd(LLVMModuleRef mod); +#endif /* macro making it easier to call ExecEval* functions */ #define build_EvalXFunc(b, mod, funcname, v_state, op, ...) \ @@ -297,7 +299,7 @@ llvm_compile_expr(ExprState *state) "v.econtext.aggnulls"); /* allocate blocks for each op upfront, so we can do jumps easily */ - opblocks = palloc(sizeof(LLVMBasicBlockRef) * state->steps_len); + opblocks = palloc_array(LLVMBasicBlockRef, state->steps_len); for (int opno = 0; opno < state->steps_len; opno++) opblocks[opno] = l_bb_append_v(eval_fn, "b.op.%d.start", opno); @@ -316,7 +318,7 @@ llvm_compile_expr(ExprState *state) op = &state->steps[opno]; opcode = ExecEvalStepOp(state, op); - v_resvaluep = l_ptr_const(op->resvalue, l_ptr(TypeSizeT)); + v_resvaluep = l_ptr_const(op->resvalue, l_ptr(TypeDatum)); v_resnullp = l_ptr_const(op->resnull, l_ptr(TypeStorageBool)); switch (opcode) @@ -326,7 +328,7 @@ llvm_compile_expr(ExprState *state) LLVMValueRef v_tmpisnull; LLVMValueRef v_tmpvalue; - v_tmpvalue = l_load(b, TypeSizeT, v_tmpvaluep, ""); + v_tmpvalue = l_load(b, TypeDatum, v_tmpvaluep, ""); v_tmpisnull = l_load(b, TypeStorageBool, v_tmpisnullp, ""); LLVMBuildStore(b, v_tmpisnull, v_isnullp); @@ -336,7 +338,7 @@ llvm_compile_expr(ExprState *state) } case EEOP_DONE_NO_RETURN: - LLVMBuildRet(b, l_sizet_const(0)); + LLVMBuildRet(b, l_datum_const(0)); break; case EEOP_INNER_FETCHSOME: @@ -478,7 +480,7 @@ llvm_compile_expr(ExprState *state) } v_attnum = l_int32_const(lc, op->d.var.attnum); - value = l_load_gep1(b, TypeSizeT, v_values, v_attnum, ""); + value = l_load_gep1(b, TypeDatum, v_values, v_attnum, ""); isnull = l_load_gep1(b, TypeStorageBool, v_nulls, v_attnum, ""); LLVMBuildStore(b, value, v_resvaluep); LLVMBuildStore(b, isnull, v_resnullp); @@ -562,13 +564,13 @@ llvm_compile_expr(ExprState *state) /* load data */ v_attnum = l_int32_const(lc, op->d.assign_var.attnum); - v_value = l_load_gep1(b, TypeSizeT, v_values, v_attnum, ""); + v_value = l_load_gep1(b, TypeDatum, v_values, v_attnum, ""); v_isnull = l_load_gep1(b, TypeStorageBool, v_nulls, v_attnum, ""); /* compute addresses of targets */ v_resultnum = l_int32_const(lc, op->d.assign_var.resultnum); v_rvaluep = l_gep(b, - TypeSizeT, + TypeDatum, v_resultvalues, &v_resultnum, 1, ""); v_risnullp = l_gep(b, @@ -595,13 +597,13 @@ llvm_compile_expr(ExprState *state) size_t resultnum = op->d.assign_tmp.resultnum; /* load data */ - v_value = l_load(b, TypeSizeT, v_tmpvaluep, ""); + v_value = l_load(b, TypeDatum, v_tmpvaluep, ""); v_isnull = l_load(b, TypeStorageBool, v_tmpisnullp, ""); /* compute addresses of targets */ v_resultnum = l_int32_const(lc, resultnum); v_rvaluep = - l_gep(b, TypeSizeT, v_resultvalues, &v_resultnum, 1, ""); + l_gep(b, TypeDatum, v_resultvalues, &v_resultnum, 1, ""); v_risnullp = l_gep(b, TypeStorageBool, v_resultnulls, &v_resultnum, 1, ""); @@ -650,7 +652,7 @@ llvm_compile_expr(ExprState *state) LLVMValueRef v_constvalue, v_constnull; - v_constvalue = l_sizet_const(op->d.constval.value); + v_constvalue = l_datum_const(op->d.constval.value); v_constnull = l_sbool_const(op->d.constval.isnull); LLVMBuildStore(b, v_constvalue, v_resvaluep); @@ -698,8 +700,8 @@ llvm_compile_expr(ExprState *state) LLVMBuildStore(b, l_sbool_const(1), v_resnullp); /* create blocks for checking args, one for each */ - b_checkargnulls = - palloc(sizeof(LLVMBasicBlockRef *) * op->d.func.nargs); + b_checkargnulls = (LLVMBasicBlockRef *) + palloc(sizeof(LLVMBasicBlockRef) * op->d.func.nargs); for (int argno = 0; argno < op->d.func.nargs; argno++) b_checkargnulls[argno] = l_bb_before_v(b_nonull, "b.%d.isnull.%d", opno, @@ -798,7 +800,7 @@ llvm_compile_expr(ExprState *state) LLVMBuildStore(b, l_sbool_const(0), v_boolanynullp); v_boolnull = l_load(b, TypeStorageBool, v_resnullp, ""); - v_boolvalue = l_load(b, TypeSizeT, v_resvaluep, ""); + v_boolvalue = l_load(b, TypeDatum, v_resvaluep, ""); /* check if current input is NULL */ LLVMBuildCondBr(b, @@ -818,7 +820,7 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_boolcheckfalse); LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntEQ, v_boolvalue, - l_sizet_const(0), ""), + l_datum_const(0), ""), b_boolisfalse, b_boolcont); @@ -846,7 +848,7 @@ llvm_compile_expr(ExprState *state) /* set resnull to true */ LLVMBuildStore(b, l_sbool_const(1), v_resnullp); /* reset resvalue */ - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); LLVMBuildBr(b, opblocks[opno + 1]); break; @@ -889,7 +891,7 @@ llvm_compile_expr(ExprState *state) if (opcode == EEOP_BOOL_OR_STEP_FIRST) LLVMBuildStore(b, l_sbool_const(0), v_boolanynullp); v_boolnull = l_load(b, TypeStorageBool, v_resnullp, ""); - v_boolvalue = l_load(b, TypeSizeT, v_resvaluep, ""); + v_boolvalue = l_load(b, TypeDatum, v_resvaluep, ""); LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntEQ, v_boolnull, @@ -908,7 +910,7 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_boolchecktrue); LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntEQ, v_boolvalue, - l_sizet_const(1), ""), + l_datum_const(1), ""), b_boolistrue, b_boolcont); @@ -936,7 +938,7 @@ llvm_compile_expr(ExprState *state) /* set resnull to true */ LLVMBuildStore(b, l_sbool_const(1), v_resnullp); /* reset resvalue */ - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); LLVMBuildBr(b, opblocks[opno + 1]); break; @@ -948,13 +950,13 @@ llvm_compile_expr(ExprState *state) LLVMValueRef v_negbool; /* compute !boolvalue */ - v_boolvalue = l_load(b, TypeSizeT, v_resvaluep, ""); + v_boolvalue = l_load(b, TypeDatum, v_resvaluep, ""); v_negbool = LLVMBuildZExt(b, LLVMBuildICmp(b, LLVMIntEQ, v_boolvalue, - l_sizet_const(0), + l_datum_const(0), ""), - TypeSizeT, ""); + TypeDatum, ""); /* * Store it back in resvalue. We can ignore resnull here; @@ -977,7 +979,7 @@ llvm_compile_expr(ExprState *state) b_qualfail = l_bb_before_v(opblocks[opno + 1], "op.%d.qualfail", opno); - v_resvalue = l_load(b, TypeSizeT, v_resvaluep, ""); + v_resvalue = l_load(b, TypeDatum, v_resvaluep, ""); v_resnull = l_load(b, TypeStorageBool, v_resnullp, ""); v_nullorfalse = @@ -985,7 +987,7 @@ llvm_compile_expr(ExprState *state) LLVMBuildICmp(b, LLVMIntEQ, v_resnull, l_sbool_const(1), ""), LLVMBuildICmp(b, LLVMIntEQ, v_resvalue, - l_sizet_const(0), ""), + l_datum_const(0), ""), ""); LLVMBuildCondBr(b, @@ -998,7 +1000,7 @@ llvm_compile_expr(ExprState *state) /* set resnull to false */ LLVMBuildStore(b, l_sbool_const(0), v_resnullp); /* set resvalue to false */ - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); /* and jump out */ LLVMBuildBr(b, opblocks[op->d.qualexpr.jumpdone]); break; @@ -1051,7 +1053,7 @@ llvm_compile_expr(ExprState *state) /* Transfer control if current result is null or false */ - v_resvalue = l_load(b, TypeSizeT, v_resvaluep, ""); + v_resvalue = l_load(b, TypeDatum, v_resvaluep, ""); v_resnull = l_load(b, TypeStorageBool, v_resnullp, ""); v_nullorfalse = @@ -1059,7 +1061,7 @@ llvm_compile_expr(ExprState *state) LLVMBuildICmp(b, LLVMIntEQ, v_resnull, l_sbool_const(1), ""), LLVMBuildICmp(b, LLVMIntEQ, v_resvalue, - l_sizet_const(0), ""), + l_datum_const(0), ""), ""); LLVMBuildCondBr(b, @@ -1078,8 +1080,8 @@ llvm_compile_expr(ExprState *state) LLVMBuildSelect(b, LLVMBuildICmp(b, LLVMIntEQ, v_resnull, l_sbool_const(1), ""), - l_sizet_const(1), - l_sizet_const(0), + l_datum_const(1), + l_datum_const(0), ""); LLVMBuildStore(b, v_resvalue, v_resvaluep); LLVMBuildStore(b, l_sbool_const(0), v_resnullp); @@ -1097,8 +1099,8 @@ llvm_compile_expr(ExprState *state) LLVMBuildSelect(b, LLVMBuildICmp(b, LLVMIntEQ, v_resnull, l_sbool_const(1), ""), - l_sizet_const(0), - l_sizet_const(1), + l_datum_const(0), + l_datum_const(1), ""); LLVMBuildStore(b, v_resvalue, v_resvaluep); LLVMBuildStore(b, l_sbool_const(0), v_resnullp); @@ -1148,11 +1150,11 @@ llvm_compile_expr(ExprState *state) if (opcode == EEOP_BOOLTEST_IS_TRUE || opcode == EEOP_BOOLTEST_IS_FALSE) { - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); } else { - LLVMBuildStore(b, l_sizet_const(1), v_resvaluep); + LLVMBuildStore(b, l_datum_const(1), v_resvaluep); } LLVMBuildBr(b, opblocks[opno + 1]); @@ -1170,14 +1172,14 @@ llvm_compile_expr(ExprState *state) else { LLVMValueRef v_value = - l_load(b, TypeSizeT, v_resvaluep, ""); + l_load(b, TypeDatum, v_resvaluep, ""); v_value = LLVMBuildZExt(b, LLVMBuildICmp(b, LLVMIntEQ, v_value, - l_sizet_const(0), + l_datum_const(0), ""), - TypeSizeT, ""); + TypeDatum, ""); LLVMBuildStore(b, v_value, v_resvaluep); } LLVMBuildBr(b, opblocks[opno + 1]); @@ -1279,11 +1281,11 @@ llvm_compile_expr(ExprState *state) v_casenull; v_casevaluep = l_ptr_const(op->d.casetest.value, - l_ptr(TypeSizeT)); + l_ptr(TypeDatum)); v_casenullp = l_ptr_const(op->d.casetest.isnull, l_ptr(TypeStorageBool)); - v_casevalue = l_load(b, TypeSizeT, v_casevaluep, ""); + v_casevalue = l_load(b, TypeDatum, v_casevaluep, ""); v_casenull = l_load(b, TypeStorageBool, v_casenullp, ""); LLVMBuildStore(b, v_casevalue, v_resvaluep); LLVMBuildStore(b, v_casenull, v_resnullp); @@ -1345,9 +1347,9 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_notnull); v_valuep = l_ptr_const(op->d.make_readonly.value, - l_ptr(TypeSizeT)); + l_ptr(TypeDatum)); - v_value = l_load(b, TypeSizeT, v_valuep, ""); + v_value = l_load(b, TypeDatum, v_valuep, ""); v_params[0] = v_value; v_ret = @@ -1415,11 +1417,11 @@ llvm_compile_expr(ExprState *state) b_calloutput); LLVMPositionBuilderAtEnd(b, b_skipoutput); - v_output_skip = l_sizet_const(0); + v_output_skip = l_datum_const(0); LLVMBuildBr(b, b_input); LLVMPositionBuilderAtEnd(b, b_calloutput); - v_resvalue = l_load(b, TypeSizeT, v_resvaluep, ""); + v_resvalue = l_load(b, TypeDatum, v_resvaluep, ""); /* set arg[0] */ LLVMBuildStore(b, @@ -1449,7 +1451,7 @@ llvm_compile_expr(ExprState *state) incoming_values[1] = v_output; incoming_blocks[1] = b_calloutput; - v_output = LLVMBuildPhi(b, TypeSizeT, "output"); + v_output = LLVMBuildPhi(b, TypeDatum, "output"); LLVMAddIncoming(v_output, incoming_values, incoming_blocks, lengthof(incoming_blocks)); @@ -1463,7 +1465,7 @@ llvm_compile_expr(ExprState *state) { LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntEQ, v_output, - l_sizet_const(0), ""), + l_datum_const(0), ""), opblocks[opno + 1], b_inputcall); } @@ -1564,9 +1566,9 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_bothargnull); LLVMBuildStore(b, l_sbool_const(0), v_resnullp); if (opcode == EEOP_NOT_DISTINCT) - LLVMBuildStore(b, l_sizet_const(1), v_resvaluep); + LLVMBuildStore(b, l_datum_const(1), v_resvaluep); else - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); LLVMBuildBr(b, opblocks[opno + 1]); @@ -1574,9 +1576,9 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_anyargnull); LLVMBuildStore(b, l_sbool_const(0), v_resnullp); if (opcode == EEOP_NOT_DISTINCT) - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); else - LLVMBuildStore(b, l_sizet_const(1), v_resvaluep); + LLVMBuildStore(b, l_datum_const(1), v_resvaluep); LLVMBuildBr(b, opblocks[opno + 1]); /* neither argument is null: compare */ @@ -1592,8 +1594,8 @@ llvm_compile_expr(ExprState *state) LLVMBuildZExt(b, LLVMBuildICmp(b, LLVMIntEQ, v_result, - l_sizet_const(0), ""), - TypeSizeT, ""); + l_datum_const(0), ""), + TypeDatum, ""); } LLVMBuildStore(b, v_fcinfo_isnull, v_resnullp); @@ -1691,7 +1693,7 @@ llvm_compile_expr(ExprState *state) ""), LLVMBuildICmp(b, LLVMIntEQ, v_retval, - l_sizet_const(1), + l_datum_const(1), ""), ""); LLVMBuildCondBr(b, v_argsequal, b_argsequal, b_hasnull); @@ -1699,7 +1701,7 @@ llvm_compile_expr(ExprState *state) /* build block setting result to NULL, if args are equal */ LLVMPositionBuilderAtEnd(b, b_argsequal); LLVMBuildStore(b, l_sbool_const(1), v_resnullp); - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); LLVMBuildBr(b, opblocks[opno + 1]); break; @@ -1755,7 +1757,7 @@ llvm_compile_expr(ExprState *state) LLVMPositionBuilderAtEnd(b, b_isnull); - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); LLVMBuildStore(b, l_sbool_const(1), v_resnullp); LLVMBuildBr(b, opblocks[op->d.returningexpr.jumpdone]); @@ -1861,7 +1863,7 @@ llvm_compile_expr(ExprState *state) LLVMBuildICmp(b, LLVMIntEQ, v_retval, - l_sizet_const(0), ""), + l_datum_const(0), ""), opblocks[opno + 1], opblocks[op->d.rowcompare_step.jumpdone]); @@ -1891,7 +1893,7 @@ llvm_compile_expr(ExprState *state) */ v_cmpresult = LLVMBuildTrunc(b, - l_load(b, TypeSizeT, v_resvaluep, ""), + l_load(b, TypeDatum, v_resvaluep, ""), LLVMInt32TypeInContext(lc), ""); switch (cmptype) @@ -1920,7 +1922,7 @@ llvm_compile_expr(ExprState *state) v_cmpresult, l_int32_const(lc, 0), ""); - v_result = LLVMBuildZExt(b, v_result, TypeSizeT, ""); + v_result = LLVMBuildZExt(b, v_result, TypeDatum, ""); LLVMBuildStore(b, l_sbool_const(0), v_resnullp); LLVMBuildStore(b, v_result, v_resvaluep); @@ -1961,11 +1963,11 @@ llvm_compile_expr(ExprState *state) v_casenull; v_casevaluep = l_ptr_const(op->d.casetest.value, - l_ptr(TypeSizeT)); + l_ptr(TypeDatum)); v_casenullp = l_ptr_const(op->d.casetest.isnull, l_ptr(TypeStorageBool)); - v_casevalue = l_load(b, TypeSizeT, v_casevaluep, ""); + v_casevalue = l_load(b, TypeDatum, v_casevaluep, ""); v_casenull = l_load(b, TypeStorageBool, v_casenullp, ""); LLVMBuildStore(b, v_casevalue, v_resvaluep); LLVMBuildStore(b, v_casenull, v_resnullp); @@ -2014,7 +2016,7 @@ llvm_compile_expr(ExprState *state) { LLVMValueRef v_initvalue; - v_initvalue = l_sizet_const(op->d.hashdatum_initvalue.init_value); + v_initvalue = l_datum_const(op->d.hashdatum_initvalue.init_value); LLVMBuildStore(b, v_initvalue, v_resvaluep); LLVMBuildStore(b, l_sbool_const(0), v_resnullp); @@ -2053,24 +2055,24 @@ llvm_compile_expr(ExprState *state) LLVMValueRef tmp; tmp = l_ptr_const(&op->d.hashdatum.iresult->value, - l_ptr(TypeSizeT)); + l_ptr(TypeDatum)); /* * Fetch the previously hashed value from where the * previous hash operation stored it. */ - v_prevhash = l_load(b, TypeSizeT, tmp, "prevhash"); + v_prevhash = l_load(b, TypeDatum, tmp, "prevhash"); /* * Rotate bits left by 1 bit. Be careful not to - * overflow uint32 when working with size_t. + * overflow uint32 when working with Datum. */ - v_tmp1 = LLVMBuildShl(b, v_prevhash, l_sizet_const(1), + v_tmp1 = LLVMBuildShl(b, v_prevhash, l_datum_const(1), ""); v_tmp1 = LLVMBuildAnd(b, v_tmp1, - l_sizet_const(0xffffffff), ""); + l_datum_const(0xffffffff), ""); v_tmp2 = LLVMBuildLShr(b, v_prevhash, - l_sizet_const(31), ""); + l_datum_const(31), ""); v_prevhash = LLVMBuildOr(b, v_tmp1, v_tmp2, "rotatedhash"); } @@ -2113,7 +2115,7 @@ llvm_compile_expr(ExprState *state) * the NULL result and goto jumpdone. */ LLVMBuildStore(b, l_sbool_const(1), v_resnullp); - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); LLVMBuildBr(b, opblocks[op->d.hashdatum.jumpdone]); } else @@ -2145,7 +2147,7 @@ llvm_compile_expr(ExprState *state) * Store a zero Datum when the Datum to hash is * NULL */ - LLVMBuildStore(b, l_sizet_const(0), v_resvaluep); + LLVMBuildStore(b, l_datum_const(0), v_resvaluep); } LLVMBuildBr(b, opblocks[opno + 1]); @@ -2178,24 +2180,24 @@ llvm_compile_expr(ExprState *state) LLVMValueRef tmp; tmp = l_ptr_const(&op->d.hashdatum.iresult->value, - l_ptr(TypeSizeT)); + l_ptr(TypeDatum)); /* * Fetch the previously hashed value from where the * previous hash operation stored it. */ - v_prevhash = l_load(b, TypeSizeT, tmp, "prevhash"); + v_prevhash = l_load(b, TypeDatum, tmp, "prevhash"); /* * Rotate bits left by 1 bit. Be careful not to - * overflow uint32 when working with size_t. + * overflow uint32 when working with Datum. */ - v_tmp1 = LLVMBuildShl(b, v_prevhash, l_sizet_const(1), + v_tmp1 = LLVMBuildShl(b, v_prevhash, l_datum_const(1), ""); v_tmp1 = LLVMBuildAnd(b, v_tmp1, - l_sizet_const(0xffffffff), ""); + l_datum_const(0xffffffff), ""); v_tmp2 = LLVMBuildLShr(b, v_prevhash, - l_sizet_const(31), ""); + l_datum_const(31), ""); v_prevhash = LLVMBuildOr(b, v_tmp1, v_tmp2, "rotatedhash"); } @@ -2373,7 +2375,7 @@ llvm_compile_expr(ExprState *state) v_aggno = l_int32_const(lc, op->d.aggref.aggno); /* load agg value / null */ - value = l_load_gep1(b, TypeSizeT, v_aggvalues, v_aggno, "aggvalue"); + value = l_load_gep1(b, TypeDatum, v_aggvalues, v_aggno, "aggvalue"); isnull = l_load_gep1(b, TypeStorageBool, v_aggnulls, v_aggno, "aggnull"); /* and store result */ @@ -2408,7 +2410,7 @@ llvm_compile_expr(ExprState *state) v_wfuncno = l_load(b, LLVMInt32TypeInContext(lc), v_wfuncnop, "v_wfuncno"); /* load window func value / null */ - value = l_load_gep1(b, TypeSizeT, v_aggvalues, v_wfuncno, + value = l_load_gep1(b, TypeDatum, v_aggvalues, v_wfuncno, "windowvalue"); isnull = l_load_gep1(b, TypeStorageBool, v_aggnulls, v_wfuncno, "windownull"); @@ -2505,7 +2507,7 @@ llvm_compile_expr(ExprState *state) v_nullsp = l_ptr_const(nulls, l_ptr(TypeStorageBool)); /* create blocks for checking args */ - b_checknulls = palloc(sizeof(LLVMBasicBlockRef *) * nargs); + b_checknulls = palloc_array(LLVMBasicBlockRef, nargs); for (int argno = 0; argno < nargs; argno++) { b_checknulls[argno] = @@ -2585,8 +2587,8 @@ llvm_compile_expr(ExprState *state) LLVMBuildCondBr(b, LLVMBuildICmp(b, LLVMIntEQ, - LLVMBuildPtrToInt(b, v_pergroup_allaggs, TypeSizeT, ""), - l_sizet_const(0), ""), + LLVMBuildPtrToInt(b, v_pergroup_allaggs, TypeDatum, ""), + l_datum_const(0), ""), opblocks[jumpnull], opblocks[opno + 1]); break; @@ -2788,7 +2790,7 @@ llvm_compile_expr(ExprState *state) "transnullp"); LLVMBuildStore(b, l_load(b, - TypeSizeT, + TypeDatum, v_transvaluep, "transvalue"), l_funcvaluep(b, v_fcinfo, 0)); @@ -2826,7 +2828,7 @@ llvm_compile_expr(ExprState *state) b_nocall = l_bb_before_v(opblocks[opno + 1], "op.%d.transnocall", opno); - v_transvalue = l_load(b, TypeSizeT, v_transvaluep, ""); + v_transvalue = l_load(b, TypeDatum, v_transvaluep, ""); v_transnull = l_load(b, TypeStorageBool, v_transnullp, ""); /* @@ -2956,7 +2958,7 @@ llvm_compile_expr(ExprState *state) */ { - CompiledExprState *cstate = palloc0(sizeof(CompiledExprState)); + CompiledExprState *cstate = palloc0_object(CompiledExprState); cstate->context = context; cstate->funcname = funcname; @@ -3007,14 +3009,11 @@ BuildV1Call(LLVMJitContext *context, LLVMBuilderRef b, LLVMModuleRef mod, FunctionCallInfo fcinfo, LLVMValueRef *v_fcinfo_isnull) { - LLVMContextRef lc; LLVMValueRef v_fn; LLVMValueRef v_fcinfo_isnullp; LLVMValueRef v_retval; LLVMValueRef v_fcinfo; - lc = LLVMGetModuleContext(mod); - v_fn = llvm_function_reference(context, b, mod, fcinfo); v_fcinfo = l_ptr_const(fcinfo, l_ptr(StructFunctionCallInfoData)); @@ -3030,11 +3029,14 @@ BuildV1Call(LLVMJitContext *context, LLVMBuilderRef b, if (v_fcinfo_isnull) *v_fcinfo_isnull = l_load(b, TypeStorageBool, v_fcinfo_isnullp, ""); +#if LLVM_VERSION_MAJOR < 22 + /* * Add lifetime-end annotation, signaling that writes to memory don't have * to be retained (important for inlining potential). */ { + LLVMContextRef lc = LLVMGetModuleContext(mod); LLVMValueRef v_lifetime = create_LifetimeEnd(mod); LLVMValueRef params[2]; @@ -3046,6 +3048,7 @@ BuildV1Call(LLVMJitContext *context, LLVMBuilderRef b, params[1] = l_ptr_const(&fcinfo->isnull, l_ptr(LLVMInt8TypeInContext(lc))); l_call(b, LLVMGetFunctionType(v_lifetime), v_lifetime, params, lengthof(params), ""); } +#endif return v_retval; } @@ -3068,7 +3071,7 @@ build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, const char *funcname, elog(ERROR, "parameter mismatch: %s expects %d passed %d", funcname, LLVMCountParams(v_fn), nargs + 2); - params = palloc(sizeof(LLVMValueRef) * (2 + nargs)); + params = palloc_array(LLVMValueRef, (2 + nargs)); params[argno++] = v_state; params[argno++] = l_ptr_const(op, l_ptr(StructExprEvalStep)); @@ -3083,6 +3086,7 @@ build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, const char *funcname, return v_ret; } +#if LLVM_VERSION_MAJOR < 22 static LLVMValueRef create_LifetimeEnd(LLVMModuleRef mod) { @@ -3112,3 +3116,4 @@ create_LifetimeEnd(LLVMModuleRef mod) return fn; } +#endif diff --git a/src/backend/jit/llvm/llvmjit_inline.cpp b/src/backend/jit/llvm/llvmjit_inline.cpp index 2764c3bbe2f03..ebc0fe92b736e 100644 --- a/src/backend/jit/llvm/llvmjit_inline.cpp +++ b/src/backend/jit/llvm/llvmjit_inline.cpp @@ -11,7 +11,7 @@ * so for all external functions, all the referenced functions (and * prerequisites) will be imported. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/llvmjit/llvmjit_inline.cpp @@ -238,7 +238,11 @@ llvm_build_inline_plan(LLVMContextRef lc, llvm::Module *mod) llvm_split_symbol_name(symbolName.data(), &cmodname, &cfuncname); +#if LLVM_VERSION_MAJOR >= 21 + funcGUID = llvm::GlobalValue::getGUIDAssumingExternalLinkage(cfuncname); +#else funcGUID = llvm::GlobalValue::getGUID(cfuncname); +#endif /* already processed */ if (inlineState.processed) diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c index dbe0282e98f4b..c8a1f84129385 100644 --- a/src/backend/jit/llvm/llvmjit_types.c +++ b/src/backend/jit/llvm/llvmjit_types.c @@ -16,7 +16,7 @@ * bitcode. * * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/jit/llvm/llvmjit_types.c @@ -47,6 +47,7 @@ */ PGFunction TypePGFunction; size_t TypeSizeT; +Datum TypeDatum; bool TypeStorageBool; ExecEvalSubroutine TypeExecEvalSubroutine; @@ -80,7 +81,7 @@ extern Datum AttributeTemplate(PG_FUNCTION_ARGS); Datum AttributeTemplate(PG_FUNCTION_ARGS) { - AssertVariableIsOfType(&AttributeTemplate, PGFunction); + StaticAssertVariableIsOfType(&AttributeTemplate, PGFunction); PG_RETURN_NULL(); } @@ -98,8 +99,8 @@ ExecEvalSubroutineTemplate(ExprState *state, struct ExprEvalStep *op, ExprContext *econtext) { - AssertVariableIsOfType(&ExecEvalSubroutineTemplate, - ExecEvalSubroutine); + StaticAssertVariableIsOfType(&ExecEvalSubroutineTemplate, + ExecEvalSubroutine); } extern bool ExecEvalBoolSubroutineTemplate(ExprState *state, @@ -110,8 +111,8 @@ ExecEvalBoolSubroutineTemplate(ExprState *state, struct ExprEvalStep *op, ExprContext *econtext) { - AssertVariableIsOfType(&ExecEvalBoolSubroutineTemplate, - ExecEvalBoolSubroutine); + StaticAssertVariableIsOfType(&ExecEvalBoolSubroutineTemplate, + ExecEvalBoolSubroutine); return false; } diff --git a/src/backend/jit/llvm/llvmjit_wrap.cpp b/src/backend/jit/llvm/llvmjit_wrap.cpp index da850d67ab647..9cba4b96e45a1 100644 --- a/src/backend/jit/llvm/llvmjit_wrap.cpp +++ b/src/backend/jit/llvm/llvmjit_wrap.cpp @@ -3,7 +3,7 @@ * llvmjit_wrap.cpp * Parts of the LLVM interface not (yet) exposed to C. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/llvm/llvmjit_wrap.cpp @@ -53,7 +53,14 @@ DEFINE_SIMPLE_CONVERSION_FUNCTIONS(llvm::orc::ObjectLayer, LLVMOrcObjectLayerRef LLVMOrcObjectLayerRef LLVMOrcCreateRTDyldObjectLinkingLayerWithSafeSectionMemoryManager(LLVMOrcExecutionSessionRef ES) { +#if LLVM_VERSION_MAJOR >= 21 + return wrap(new llvm::orc::RTDyldObjectLinkingLayer( + *unwrap(ES), [](const llvm::MemoryBuffer&) { + return std::make_unique(nullptr, true); + })); +#else return wrap(new llvm::orc::RTDyldObjectLinkingLayer( *unwrap(ES), [] { return std::make_unique(nullptr, true); })); +#endif } #endif diff --git a/src/backend/jit/llvm/meson.build b/src/backend/jit/llvm/meson.build index c8e06dfbe351b..7df8453ad6fc4 100644 --- a/src/backend/jit/llvm/meson.build +++ b/src/backend/jit/llvm/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not llvm.found() subdir_done() @@ -53,7 +53,7 @@ llvm_irgen_args = [ if ccache.found() llvm_irgen_command = ccache - llvm_irgen_args = [clang.path()] + llvm_irgen_args + llvm_irgen_args = [clang.full_path()] + llvm_irgen_args else llvm_irgen_command = clang endif diff --git a/src/backend/jit/meson.build b/src/backend/jit/meson.build index c83d056254584..de7701064706f 100644 --- a/src/backend/jit/meson.build +++ b/src/backend/jit/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'jit.c' diff --git a/src/backend/lib/README b/src/backend/lib/README index f2fb591237dba..c28cbe356f0b3 100644 --- a/src/backend/lib/README +++ b/src/backend/lib/README @@ -1,8 +1,6 @@ This directory contains a general purpose data structures, for use anywhere in the backend: -binaryheap.c - a binary heap - bipartite_match.c - Hopcroft-Karp maximum cardinality algorithm for bipartite graphs bloomfilter.c - probabilistic, space-efficient set membership testing @@ -21,8 +19,6 @@ pairingheap.c - a pairing heap rbtree.c - a red-black tree -stringinfo.c - an extensible string type - Aside from the inherent characteristics of the data structures, there are a few practical differences between the binary heap and the pairing heap. The diff --git a/src/backend/lib/bipartite_match.c b/src/backend/lib/bipartite_match.c index 5af789652c794..12520dc91b44f 100644 --- a/src/backend/lib/bipartite_match.c +++ b/src/backend/lib/bipartite_match.c @@ -7,7 +7,7 @@ * * https://en.wikipedia.org/w/index.php?title=Hopcroft%E2%80%93Karp_algorithm&oldid=593898016 * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/bipartite_match.c @@ -38,7 +38,7 @@ static bool hk_depth_search(BipartiteMatchState *state, int u); BipartiteMatchState * BipartiteMatch(int u_size, int v_size, short **adjacency) { - BipartiteMatchState *state = palloc(sizeof(BipartiteMatchState)); + BipartiteMatchState *state = palloc_object(BipartiteMatchState); if (u_size < 0 || u_size >= SHRT_MAX || v_size < 0 || v_size >= SHRT_MAX) diff --git a/src/backend/lib/bloomfilter.c b/src/backend/lib/bloomfilter.c index d3f935170db4b..73b3768a17232 100644 --- a/src/backend/lib/bloomfilter.c +++ b/src/backend/lib/bloomfilter.c @@ -24,7 +24,7 @@ * caller many authoritative lookups, such as expensive probes of a much larger * on-disk structure. * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/bloomfilter.c diff --git a/src/backend/lib/dshash.c b/src/backend/lib/dshash.c index b8d031f201520..1999989c14f71 100644 --- a/src/backend/lib/dshash.c +++ b/src/backend/lib/dshash.c @@ -20,7 +20,7 @@ * Future versions may support iterators and incremental resizing; for now * the implementation is minimalist. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -31,6 +31,8 @@ #include "postgres.h" +#include + #include "common/hashfn.h" #include "lib/dshash.h" #include "storage/lwlock.h" @@ -165,7 +167,8 @@ struct dshash_table static void delete_item(dshash_table *hash_table, dshash_table_item *item); -static void resize(dshash_table *hash_table, size_t new_size_log2); +static bool resize(dshash_table *hash_table, size_t new_size_log2, + int flags); static inline void ensure_valid_bucket_pointers(dshash_table *hash_table); static inline dshash_table_item *find_in_bucket(dshash_table *hash_table, const void *key, @@ -176,7 +179,8 @@ static void insert_item_into_bucket(dshash_table *hash_table, dsa_pointer *bucket); static dshash_table_item *insert_into_bucket(dshash_table *hash_table, const void *key, - dsa_pointer *bucket); + dsa_pointer *bucket, + int flags); static bool delete_key_from_bucket(dshash_table *hash_table, const void *key, dsa_pointer *bucket_head); @@ -209,7 +213,7 @@ dshash_create(dsa_area *area, const dshash_parameters *params, void *arg) dsa_pointer control; /* Allocate the backend-local object representing the hash table. */ - hash_table = palloc(sizeof(dshash_table)); + hash_table = palloc_object(dshash_table); /* Allocate the control object in shared memory. */ control = dsa_allocate(area, sizeof(dshash_table_control)); @@ -274,7 +278,7 @@ dshash_attach(dsa_area *area, const dshash_parameters *params, dsa_pointer control; /* Allocate the backend-local object representing the hash table. */ - hash_table = palloc(sizeof(dshash_table)); + hash_table = palloc_object(dshash_table); /* Find the control object in shared memory. */ control = handle; @@ -420,19 +424,25 @@ dshash_find(dshash_table *hash_table, const void *key, bool exclusive) } /* - * Returns a pointer to an exclusively locked item which must be released with - * dshash_release_lock. If the key is found in the hash table, 'found' is set - * to true and a pointer to the existing entry is returned. If the key is not - * found, 'found' is set to false, and a pointer to a newly created entry is - * returned. + * Find an existing entry in a dshash_table, or insert a new one. + * + * DSHASH_INSERT_NO_OOM causes this function to return NULL when no memory is + * available for the new entry. Otherwise, such allocations will result in + * an ERROR. + * + * Any entry returned by this function is exclusively locked, and the caller + * must release that lock using dshash_release_lock. Notes above dshash_find() + * regarding locking and error handling equally apply here. + * + * On return, *found is set to true if an existing entry was found in the + * hash table, and otherwise false. * - * Notes above dshash_find() regarding locking and error handling equally - * apply here. */ void * -dshash_find_or_insert(dshash_table *hash_table, - const void *key, - bool *found) +dshash_find_or_insert_extended(dshash_table *hash_table, + const void *key, + bool *found, + int flags) { dshash_hash hash; size_t partition_index; @@ -475,14 +485,25 @@ dshash_find_or_insert(dshash_table *hash_table, * reacquire all the locks in the right order to avoid deadlocks. */ LWLockRelease(PARTITION_LOCK(hash_table, partition_index)); - resize(hash_table, hash_table->size_log2 + 1); + if (!resize(hash_table, hash_table->size_log2 + 1, flags)) + { + Assert((flags & DSHASH_INSERT_NO_OOM) != 0); + return NULL; + } goto restart; } /* Finally we can try to insert the new item. */ item = insert_into_bucket(hash_table, key, - &BUCKET_FOR_HASH(hash_table, hash)); + &BUCKET_FOR_HASH(hash_table, hash), + flags); + if (item == NULL) + { + Assert((flags & DSHASH_INSERT_NO_OOM) != 0); + LWLockRelease(PARTITION_LOCK(hash_table, partition_index)); + return NULL; + } item->hash = hash; /* Adjust per-lock-partition counter for load factor knowledge. */ ++partition->count; @@ -852,10 +873,14 @@ delete_item(dshash_table *hash_table, dshash_table_item *item) * Grow the hash table if necessary to the requested number of buckets. The * requested size must be double some previously observed size. * + * If an out-of-memory condition is observed, this function returns false if + * flags includes DSHASH_INSERT_NO_OOM, and otherwise throws an ERROR. In all + * other cases, it returns true. + * * Must be called without any partition lock held. */ -static void -resize(dshash_table *hash_table, size_t new_size_log2) +static bool +resize(dshash_table *hash_table, size_t new_size_log2, int flags) { dsa_pointer old_buckets; dsa_pointer new_buckets_shared; @@ -863,6 +888,7 @@ resize(dshash_table *hash_table, size_t new_size_log2) size_t size; size_t new_size = ((size_t) 1) << new_size_log2; size_t i; + int dsa_flags = DSA_ALLOC_HUGE | DSA_ALLOC_ZERO; /* * Acquire the locks for all lock partitions. This is expensive, but we @@ -880,23 +906,34 @@ resize(dshash_table *hash_table, size_t new_size_log2) * obtaining all the locks and return early. */ LWLockRelease(PARTITION_LOCK(hash_table, 0)); - return; + return true; } } Assert(new_size_log2 == hash_table->control->size_log2 + 1); /* Allocate the space for the new table. */ + if (flags & DSHASH_INSERT_NO_OOM) + dsa_flags |= DSA_ALLOC_NO_OOM; new_buckets_shared = dsa_allocate_extended(hash_table->area, sizeof(dsa_pointer) * new_size, - DSA_ALLOC_HUGE | DSA_ALLOC_ZERO); - new_buckets = dsa_get_address(hash_table->area, new_buckets_shared); + dsa_flags); + + /* If DSHASH_INSERT_NO_OOM was specified, allocation may have failed. */ + if (!DsaPointerIsValid(new_buckets_shared)) + { + /* Release all the locks and return without resizing. */ + for (i = 0; i < DSHASH_NUM_PARTITIONS; ++i) + LWLockRelease(PARTITION_LOCK(hash_table, i)); + return false; + } /* * We've allocated the new bucket array; all that remains to do now is to * reinsert all items, which amounts to adjusting all the pointers. */ + new_buckets = dsa_get_address(hash_table->area, new_buckets_shared); size = ((size_t) 1) << hash_table->control->size_log2; for (i = 0; i < size; ++i) { @@ -926,6 +963,8 @@ resize(dshash_table *hash_table, size_t new_size_log2) /* Release all the locks. */ for (i = 0; i < DSHASH_NUM_PARTITIONS; ++i) LWLockRelease(PARTITION_LOCK(hash_table, i)); + + return true; } /* @@ -980,19 +1019,26 @@ insert_item_into_bucket(dshash_table *hash_table, /* * Allocate space for an entry with the given key and insert it into the - * provided bucket. + * provided bucket. Returns NULL if out of memory and DSHASH_INSERT_NO_OOM + * was specified in flags. */ static dshash_table_item * insert_into_bucket(dshash_table *hash_table, const void *key, - dsa_pointer *bucket) + dsa_pointer *bucket, + int flags) { dsa_pointer item_pointer; dshash_table_item *item; - - item_pointer = dsa_allocate(hash_table->area, - hash_table->params.entry_size + - MAXALIGN(sizeof(dshash_table_item))); + int dsa_flags; + + dsa_flags = (flags & DSHASH_INSERT_NO_OOM) ? DSA_ALLOC_NO_OOM : 0; + item_pointer = dsa_allocate_extended(hash_table->area, + hash_table->params.entry_size + + MAXALIGN(sizeof(dshash_table_item)), + dsa_flags); + if (!DsaPointerIsValid(item_pointer)) + return NULL; item = dsa_get_address(hash_table->area, item_pointer); copy_key(hash_table, ENTRY_FROM_ITEM(item), key); insert_item_into_bucket(hash_table, item_pointer, item, bucket); diff --git a/src/backend/lib/hyperloglog.c b/src/backend/lib/hyperloglog.c index 0144ecb5d6345..3bc6aa0548fff 100644 --- a/src/backend/lib/hyperloglog.c +++ b/src/backend/lib/hyperloglog.c @@ -3,7 +3,7 @@ * hyperloglog.c * HyperLogLog cardinality estimator * - * Portions Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2014-2026, PostgreSQL Global Development Group * * Based on Hideaki Ohno's C++ implementation. This is probably not ideally * suited to estimating the cardinality of very large sets; in particular, we @@ -228,7 +228,7 @@ estimateHyperLogLog(hyperLogLogState *cState) * starting from the first, reading from most significant to least significant * bits. * - * Example (when considering fist 10 bits of x): + * Example (when considering first 10 bits of x): * * rho(x = 0b1000000000) returns 1 * rho(x = 0b0010000000) returns 3 diff --git a/src/backend/lib/ilist.c b/src/backend/lib/ilist.c index 2862dce479672..d238858e24f0a 100644 --- a/src/backend/lib/ilist.c +++ b/src/backend/lib/ilist.c @@ -3,7 +3,7 @@ * ilist.c * support for integrated/inline doubly- and singly- linked lists * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/lib/integerset.c b/src/backend/lib/integerset.c index f4153b0e15a24..0a525d4a3e633 100644 --- a/src/backend/lib/integerset.c +++ b/src/backend/lib/integerset.c @@ -61,7 +61,7 @@ * (https://doi.org/10.1002/spe.948) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -284,7 +284,7 @@ intset_create(void) { IntegerSet *intset; - intset = (IntegerSet *) palloc(sizeof(IntegerSet)); + intset = palloc_object(IntegerSet); intset->context = CurrentMemoryContext; intset->mem_used = GetMemoryChunkSpace(intset); diff --git a/src/backend/lib/knapsack.c b/src/backend/lib/knapsack.c index 5b3697a090fa6..586f1881fd5f9 100644 --- a/src/backend/lib/knapsack.c +++ b/src/backend/lib/knapsack.c @@ -15,7 +15,7 @@ * allows approximate solutions in polynomial time (the general case of the * exact problem is NP-hard). * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/knapsack.c @@ -24,7 +24,6 @@ */ #include "postgres.h" -#include #include #include "lib/knapsack.h" diff --git a/src/backend/lib/meson.build b/src/backend/lib/meson.build index 463612fe97636..8e38fb20f17ac 100644 --- a/src/backend/lib/meson.build +++ b/src/backend/lib/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'bipartite_match.c', diff --git a/src/backend/lib/pairingheap.c b/src/backend/lib/pairingheap.c index 0aef8a88f1b5e..9bb3dd68140ca 100644 --- a/src/backend/lib/pairingheap.c +++ b/src/backend/lib/pairingheap.c @@ -14,7 +14,7 @@ * The pairing heap: a new form of self-adjusting heap. * Algorithmica 1, 1 (January 1986), pages 111-129. DOI: 10.1007/BF01840439 * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/pairingheap.c @@ -43,13 +43,27 @@ pairingheap_allocate(pairingheap_comparator compare, void *arg) { pairingheap *heap; - heap = (pairingheap *) palloc(sizeof(pairingheap)); + heap = palloc_object(pairingheap); + pairingheap_initialize(heap, compare, arg); + + return heap; +} + +/* + * pairingheap_initialize + * + * Same as pairingheap_allocate(), but initializes the pairing heap in-place + * rather than allocating a new chunk of memory. Useful to store the pairing + * heap in a shared memory. + */ +void +pairingheap_initialize(pairingheap *heap, pairingheap_comparator compare, + void *arg) +{ heap->ph_compare = compare; heap->ph_arg = arg; heap->ph_root = NULL; - - return heap; } /* diff --git a/src/backend/lib/rbtree.c b/src/backend/lib/rbtree.c index 3b5e5faa9bf5f..d7b410d4430ad 100644 --- a/src/backend/lib/rbtree.c +++ b/src/backend/lib/rbtree.c @@ -7,9 +7,9 @@ * This code comes from Thomas Niemann's "Sorting and Searching Algorithms: * a Cookbook". * - * See http://www.cs.auckland.ac.nz/software/AlgAnim/niemann/s_man.htm for - * license terms: "Source code, when part of a software project, may be used - * freely without reference to the author." + * See https://web.archive.org/web/20131202103513/http://www.cs.auckland.ac.nz/software/AlgAnim/niemann/s_man.htm + * for license terms: "Source code, when part of a software project, may be + * used freely without reference to the author." * * Red-black trees are a type of balanced binary tree wherein (1) any child of * a red node is always black, and (2) every path from root to leaf traverses @@ -17,7 +17,7 @@ * longest path from root to leaf is only about twice as long as the shortest, * so lookups are guaranteed to run in O(lg n) time. * - * Copyright (c) 2009-2025, PostgreSQL Global Development Group + * Copyright (c) 2009-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/lib/rbtree.c @@ -106,7 +106,7 @@ rbt_create(Size node_size, rbt_freefunc freefunc, void *arg) { - RBTree *tree = (RBTree *) palloc(sizeof(RBTree)); + RBTree *tree = palloc_object(RBTree); Assert(node_size > sizeof(RBTNode)); diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c index 27f7af7be0024..ea34ebdb73368 100644 --- a/src/backend/libpq/auth-oauth.c +++ b/src/backend/libpq/auth-oauth.c @@ -6,7 +6,7 @@ * See the following RFC for more details: * - RFC 7628: https://datatracker.ietf.org/doc/html/rfc7628 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/auth-oauth.c @@ -25,6 +25,7 @@ #include "libpq/hba.h" #include "libpq/oauth.h" #include "libpq/sasl.h" +#include "miscadmin.h" #include "storage/fd.h" #include "storage/ipc.h" #include "utils/json.h" @@ -40,10 +41,15 @@ static int oauth_exchange(void *opaq, const char *input, int inputlen, static void load_validator_library(const char *libname); static void shutdown_validator_library(void *arg); +static bool check_validator_hba_options(Port *port, const char **logdetail); static ValidatorModuleState *validator_module_state; static const OAuthValidatorCallbacks *ValidatorCallbacks; +static MemoryContext ValidatorMemoryContext; +static List *ValidatorOptions; +static bool ValidatorOptionsChecked; + /* Mechanism declaration */ const pg_be_sasl_mech pg_be_oauth_mech = { .get_mechanisms = oauth_get_mechanisms, @@ -58,6 +64,7 @@ enum oauth_state { OAUTH_STATE_INIT = 0, OAUTH_STATE_ERROR, + OAUTH_STATE_ERROR_DISCOVERY, OAUTH_STATE_FINISHED, }; @@ -73,7 +80,7 @@ struct oauth_ctx static char *sanitize_char(char c); static char *parse_kvpairs_for_auth(char **input); static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen); -static bool validate(Port *port, const char *auth); +static bool validate(Port *port, const char *auth, const char **logdetail); /* Constants seen in an OAUTHBEARER client initial response. */ #define KVSEP 0x01 /* separator byte for key/value pairs */ @@ -108,7 +115,10 @@ oauth_init(Port *port, const char *selected_mech, const char *shadow_pass) errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("client selected an invalid SASL authentication mechanism")); - ctx = palloc0(sizeof(*ctx)); + /* Save our memory context for later use by client API calls. */ + ValidatorMemoryContext = CurrentMemoryContext; + + ctx = palloc0_object(struct oauth_ctx); ctx->state = OAUTH_STATE_INIT; ctx->port = port; @@ -181,6 +191,7 @@ oauth_exchange(void *opaq, const char *input, int inputlen, break; case OAUTH_STATE_ERROR: + case OAUTH_STATE_ERROR_DISCOVERY: /* * Only one response is valid for the client during authentication @@ -192,7 +203,19 @@ oauth_exchange(void *opaq, const char *input, int inputlen, errmsg("malformed OAUTHBEARER message"), errdetail("Client did not send a kvsep response.")); - /* The (failed) handshake is now complete. */ + /* + * The (failed) handshake is now complete. Don't report discovery + * requests in the server log unless the log level is high enough. + */ + if (ctx->state == OAUTH_STATE_ERROR_DISCOVERY) + { + ereport(DEBUG1, errmsg("OAuth issuer discovery requested")); + + ctx->state = OAUTH_STATE_FINISHED; + return PG_SASL_EXCHANGE_ABANDONED; + } + + /* We're not in discovery, so this is just a normal auth failure. */ ctx->state = OAUTH_STATE_FINISHED; return PG_SASL_EXCHANGE_FAILURE; @@ -279,7 +302,29 @@ oauth_exchange(void *opaq, const char *input, int inputlen, errmsg("malformed OAUTHBEARER message"), errdetail("Message contains additional data after the final terminator.")); - if (!validate(ctx->port, auth)) + /* + * Make sure all custom HBA options are understood by the validator before + * continuing, since we couldn't check them during server start/reload. + */ + if (!check_validator_hba_options(ctx->port, logdetail)) + { + ctx->state = OAUTH_STATE_FINISHED; + return PG_SASL_EXCHANGE_FAILURE; + } + + if (auth[0] == '\0') + { + /* + * An empty auth value represents a discovery request; the client + * expects it to fail. Skip validation entirely and move directly to + * the error response. + */ + generate_error_response(ctx, output, outputlen); + + ctx->state = OAUTH_STATE_ERROR_DISCOVERY; + status = PG_SASL_EXCHANGE_CONTINUE; + } + else if (!validate(ctx->port, auth, logdetail)) { generate_error_response(ctx, output, outputlen); @@ -564,19 +609,8 @@ validate_token_format(const char *header) /* Missing auth headers should be handled by the caller. */ Assert(header); - - if (header[0] == '\0') - { - /* - * A completely empty auth header represents a query for - * authentication parameters. The client expects it to fail; there's - * no need to make any extra noise in the logs. - * - * TODO: should we find a way to return STATUS_EOF at the top level, - * to suppress the authentication error entirely? - */ - return NULL; - } + /* Empty auth (discovery) should be handled before calling validate(). */ + Assert(header[0] != '\0'); if (pg_strncasecmp(header, BEARER_SCHEME, strlen(BEARER_SCHEME))) { @@ -635,7 +669,7 @@ validate_token_format(const char *header) * authorization. Returns true if validation succeeds. */ static bool -validate(Port *port, const char *auth) +validate(Port *port, const char *auth, const char **logdetail) { int map_status; ValidatorModuleResult *ret; @@ -656,13 +690,16 @@ validate(Port *port, const char *auth) errmsg("validation of OAuth token requested without a validator loaded")); /* Call the validation function from the validator module */ - ret = palloc0(sizeof(ValidatorModuleResult)); + ret = palloc0_object(ValidatorModuleResult); if (!ValidatorCallbacks->validate_cb(validator_module_state, token, port->user_name, ret)) { ereport(WARNING, errcode(ERRCODE_INTERNAL_ERROR), - errmsg("internal error in OAuth validator module")); + errmsg("internal error in OAuth validator module"), + ret->error_detail ? errdetail_log("%s", ret->error_detail) : 0); + + *logdetail = ret->error_detail; return false; } @@ -675,10 +712,10 @@ validate(Port *port, const char *auth) if (!ret->authorized) { - ereport(LOG, - errmsg("OAuth bearer authentication failed for user \"%s\"", - port->user_name), - errdetail_log("Validator failed to authorize the provided token.")); + if (ret->error_detail) + *logdetail = ret->error_detail; + else + *logdetail = _("Validator failed to authorize the provided token."); status = false; goto cleanup; @@ -699,10 +736,7 @@ validate(Port *port, const char *auth) /* Make sure the validator authenticated the user. */ if (ret->authn_id == NULL || ret->authn_id[0] == '\0') { - ereport(LOG, - errmsg("OAuth bearer authentication failed for user \"%s\"", - port->user_name), - errdetail_log("Validator provided no identity.")); + *logdetail = _("Validator provided no identity."); status = false; goto cleanup; @@ -785,14 +819,14 @@ load_validator_library(const char *libname) "OAuth validator", libname, "validate_cb")); /* Allocate memory for validator library private state data */ - validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState)); + validator_module_state = palloc0_object(ValidatorModuleState); validator_module_state->sversion = PG_VERSION_NUM; if (ValidatorCallbacks->startup_cb != NULL) ValidatorCallbacks->startup_cb(validator_module_state); /* Shut down the library before cleaning up its state. */ - mcb = palloc0(sizeof(*mcb)); + mcb = palloc0_object(MemoryContextCallback); mcb->func = shutdown_validator_library; MemoryContextRegisterResetCallback(CurrentMemoryContext, mcb); @@ -807,6 +841,9 @@ shutdown_validator_library(void *arg) { if (ValidatorCallbacks->shutdown_cb != NULL) ValidatorCallbacks->shutdown_cb(validator_module_state); + + /* The backing memory for this is about to disappear. */ + ValidatorOptions = NIL; } /* @@ -892,3 +929,206 @@ check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg) return (*err_msg == NULL); } + +/* + * Client APIs for validator implementations + * + * Since we're currently not threaded, we only allow one validator in the + * process at a time. So we can make use of globals for now instead of looking + * up information using the state pointer. We probably shouldn't assume that the + * module hasn't temporarily changed memory contexts on us, though; functions + * here should defensively use an appropriate context when making global + * allocations. + */ + +/* + * Adds to the list of allowed validator.* HBA options. Used during the + * startup_cb. + */ +void +RegisterOAuthHBAOptions(ValidatorModuleState *state, int num, + const char *opts[]) +{ + MemoryContext oldcontext; + + if (!state) + { + Assert(false); + return; + } + + oldcontext = MemoryContextSwitchTo(ValidatorMemoryContext); + + for (int i = 0; i < num; i++) + { + if (!valid_oauth_hba_option_name(opts[i])) + { + /* + * The user can't set this option in the HBA, so GetOAuthHBAOption + * would always return NULL. + */ + ereport(WARNING, + errmsg("HBA option name \"%s\" is invalid and will be ignored", + opts[i]), + /* translator: the second %s is a function name */ + errcontext("validator module \"%s\", in call to %s", + MyProcPort->hba->oauth_validator, + "RegisterOAuthHBAOptions")); + continue; + } + + ValidatorOptions = lappend(ValidatorOptions, pstrdup(opts[i])); + } + + MemoryContextSwitchTo(oldcontext); + + /* + * Wait to validate the HBA against the registered options until later + * (see check_validator_hba_options()). + * + * Delaying allows the validator to make multiple registration calls, to + * append to the list; it lets us make the check in a place where we can + * report the error without leaking details to the client; and it avoids + * exporting the order of operations between HBA matching and the + * startup_cb call as an API guarantee. (The last issue may become + * relevant with a threaded model.) + */ +} + +/* + * Restrict the names available to custom HBA options, so that we don't + * accidentally prevent future syntax extensions to HBA files. + */ +bool +valid_oauth_hba_option_name(const char *name) +{ + /* + * This list is not incredibly principled, since the goal is just to bound + * compatibility guarantees for our HBA parser. Alphanumerics seem + * obviously fine, and it's difficult to argue against the punctuation + * that's already included in some HBA option names and identifiers. + */ + static const char *name_allowed_set = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789_-"; + + size_t span; + + if (!name[0]) + return false; + + span = strspn(name, name_allowed_set); + return name[span] == '\0'; +} + +/* + * Verifies that all validator.* HBA options specified by the user were actually + * registered by the validator library in use. + */ +static bool +check_validator_hba_options(Port *port, const char **logdetail) +{ + HbaLine *hba = port->hba; + + foreach_ptr(char, key, hba->oauth_opt_keys) + { + bool found = false; + + /* O(n^2) shouldn't be a problem here in practice. */ + foreach_ptr(char, optname, ValidatorOptions) + { + if (strcmp(key, optname) == 0) + { + found = true; + break; + } + } + + if (!found) + { + /* + * Unknown option name. Mirror the error messages in hba.c here, + * keeping in mind that the original "validator." prefix was + * stripped from the key during parsing. + * + * Since this is affecting live connections, which is unusual for + * HBA, be noisy with a WARNING. (Warnings aren't sent to clients + * prior to successful authentication, so this won't disclose the + * server config.) It'll duplicate some of the information in the + * logdetail, but that should make it hard to miss the connection + * between the two. + */ + char *name = psprintf("validator.%s", key); + + *logdetail = psprintf(_("unrecognized authentication option name: \"%s\""), + name); + ereport(WARNING, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("unrecognized authentication option name: \"%s\"", + name), + /* translator: the first %s is the name of the module */ + errdetail("The installed validator module (\"%s\") did not define an option named \"%s\".", + hba->oauth_validator, key), + errhint("All OAuth connections matching this line will fail. Correct the option and reload the server configuration."), + errcontext("line %d of configuration file \"%s\"", + hba->linenumber, hba->sourcefile)); + + return false; + } + } + + ValidatorOptionsChecked = true; /* unfetter GetOAuthHBAOption() */ + return true; +} + +/* + * Retrieves the setting for a validator.* HBA option, or NULL if not found. + * This may only be used during the validate_cb and shutdown_cb. + */ +const char * +GetOAuthHBAOption(const ValidatorModuleState *state, const char *optname) +{ + HbaLine *hba = MyProcPort->hba; + ListCell *lc_k; + ListCell *lc_v; + const char *ret = NULL; + + if (!ValidatorOptionsChecked) + { + /* + * Prevent the startup_cb from retrieving HBA options that it has just + * registered. This probably seems strange -- why refuse to hand out + * information we already know? -- but this lets us reserve the + * ability to perform the startup_cb call earlier, before we know + * which HBA line is matched by a connection, without breaking this + * API. + */ + return NULL; + } + + if (!state || !hba) + { + Assert(false); + return NULL; + } + + Assert(list_length(hba->oauth_opt_keys) == list_length(hba->oauth_opt_vals)); + + forboth(lc_k, hba->oauth_opt_keys, lc_v, hba->oauth_opt_vals) + { + const char *key = lfirst(lc_k); + const char *val = lfirst(lc_v); + + if (strcmp(key, optname) == 0) + { + /* + * Don't return yet -- when regular HBA options are specified more + * than once, the last one wins. Do the same for these options. + */ + ret = val; + } + } + + return ret; +} diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c index 52c79882c1137..59ac38fca50fa 100644 --- a/src/backend/libpq/auth-sasl.c +++ b/src/backend/libpq/auth-sasl.c @@ -3,7 +3,7 @@ * auth-sasl.c * Routines to handle authentication via SASL * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -30,6 +30,12 @@ * be found for the role (or the user does not exist), and the mechanism * should fail the authentication exchange. * + * Some SASL mechanisms (e.g. OAUTHBEARER) define special exchanges for + * parameter discovery. These exchanges will always result in STATUS_ERROR, + * since we can't let the connection continue, but we shouldn't consider them to + * be failed authentication attempts. *abandoned will be set to true in this + * case. + * * Mechanisms must take care not to reveal to the client that a user entry * does not exist; ideally, the external failure mode is identical to that * of an incorrect password. Mechanisms may instead use the logdetail @@ -42,7 +48,7 @@ */ int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, - const char **logdetail) + const char **logdetail, bool *abandoned) { StringInfoData sasl_mechs; int mtype; @@ -167,7 +173,7 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, * PG_SASL_EXCHANGE_FAILURE with some output is forbidden by SASL. * Make sure here that the mechanism used got that right. */ - if (result == PG_SASL_EXCHANGE_FAILURE) + if (result == PG_SASL_EXCHANGE_FAILURE || result == PG_SASL_EXCHANGE_ABANDONED) elog(ERROR, "output message found after SASL exchange failure"); /* @@ -184,6 +190,20 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, } } while (result == PG_SASL_EXCHANGE_CONTINUE); + if (result == PG_SASL_EXCHANGE_ABANDONED) + { + if (!abandoned) + { + /* + * Programmer error: caller needs to track the abandoned state for + * this mechanism. + */ + elog(ERROR, "SASL exchange was abandoned, but CheckSASLAuth isn't tracking it"); + } + + *abandoned = true; + } + /* Oops, Something bad happened */ if (result != PG_SASL_EXCHANGE_SUCCESS) { diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index db778405724ad..0267edb29cd9e 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -80,7 +80,7 @@ * general, after logging in, but let's do what we can here. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/auth-scram.c @@ -134,8 +134,6 @@ typedef struct { scram_state_enum state; - const char *username; /* username from startup packet */ - Port *port; bool channel_binding_in_use; @@ -242,7 +240,7 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass) scram_state *state; bool got_secret; - state = (scram_state *) palloc0(sizeof(scram_state)); + state = palloc0_object(scram_state); state->port = port; state->state = SCRAM_AUTH_INIT; @@ -1492,8 +1490,8 @@ scram_mock_salt(const char *username, pg_cryptohash_type hash_type, ctx = pg_cryptohash_create(hash_type); if (pg_cryptohash_init(ctx) < 0 || - pg_cryptohash_update(ctx, (uint8 *) username, strlen(username)) < 0 || - pg_cryptohash_update(ctx, (uint8 *) mock_auth_nonce, MOCK_AUTH_NONCE_LEN) < 0 || + pg_cryptohash_update(ctx, (const uint8 *) username, strlen(username)) < 0 || + pg_cryptohash_update(ctx, (const uint8 *) mock_auth_nonce, MOCK_AUTH_NONCE_LEN) < 0 || pg_cryptohash_final(ctx, sha_digest, key_length) < 0) { pg_cryptohash_free(ctx); diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 9f4d05ffbd453..2af5615e54a45 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -3,7 +3,7 @@ * auth.c * Routines to handle network authentication * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -45,7 +45,8 @@ * Global authentication functions *---------------------------------------------------------------- */ -static void auth_failed(Port *port, int status, const char *logdetail); +static void auth_failed(Port *port, int elevel, int status, + const char *logdetail); static char *recv_password_packet(Port *port); @@ -70,14 +71,14 @@ static int CheckMD5Auth(Port *port, char *shadow_pass, /* Standard TCP port number for Ident service. Assigned by IANA */ #define IDENT_PORT 113 -static int ident_inet(hbaPort *port); +static int ident_inet(Port *port); /*---------------------------------------------------------------- * Peer authentication *---------------------------------------------------------------- */ -static int auth_peer(hbaPort *port); +static int auth_peer(Port *port); /*---------------------------------------------------------------- @@ -94,8 +95,16 @@ static int auth_peer(hbaPort *port); #define PGSQL_PAM_SERVICE "postgresql" /* Service name passed to PAM */ +/* Work around original Solaris' lack of "const" in the conv_proc signature */ +#ifdef _PAM_LEGACY_NONCONST +#define PG_PAM_CONST +#else +#define PG_PAM_CONST const +#endif + static int CheckPAMAuth(Port *port, const char *user, const char *password); -static int pam_passwd_conv_proc(int num_msg, const struct pam_message **msg, +static int pam_passwd_conv_proc(int num_msg, + PG_PAM_CONST struct pam_message **msg, struct pam_response **resp, void *appdata_ptr); static struct pam_conv pam_passw_conv = { @@ -194,13 +203,6 @@ static int pg_SSPI_make_upn(char *accountname, bool update_accountname); #endif -/*---------------------------------------------------------------- - * RADIUS Authentication - *---------------------------------------------------------------- - */ -static int CheckRADIUSAuth(Port *port); -static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd); - /*---------------------------------------------------------------- * Global authentication functions @@ -225,15 +227,18 @@ ClientAuthentication_hook_type ClientAuthentication_hook = NULL; * anyway. * Note that many sorts of failure report additional information in the * postmaster log, which we hope is only readable by good guys. In - * particular, if logdetail isn't NULL, we send that string to the log. + * particular, if logdetail isn't NULL, we send that string to the log + * when the elevel allows. */ static void -auth_failed(Port *port, int status, const char *logdetail) +auth_failed(Port *port, int elevel, int status, const char *logdetail) { const char *errstr; char *cdetail; int errcode_return = ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION; + Assert(elevel >= FATAL); /* we must exit here */ + /* * If we failed due to EOF from client, just quit; there's no point in * trying to send a message to the client, and not much point in logging @@ -287,9 +292,6 @@ auth_failed(Port *port, int status, const char *logdetail) case uaCert: errstr = gettext_noop("certificate authentication failed for user \"%s\""); break; - case uaRADIUS: - errstr = gettext_noop("RADIUS authentication failed for user \"%s\""); - break; case uaOAuth: errstr = gettext_noop("OAuth bearer authentication failed for user \"%s\""); break; @@ -306,12 +308,13 @@ auth_failed(Port *port, int status, const char *logdetail) else logdetail = cdetail; - ereport(FATAL, + ereport(elevel, (errcode(errcode_return), errmsg(errstr, port->user_name), logdetail ? errdetail_log("%s", logdetail) : 0)); /* doesn't return */ + pg_unreachable(); } @@ -373,6 +376,15 @@ ClientAuthentication(Port *port) int status = STATUS_ERROR; const char *logdetail = NULL; + /* + * "Abandoned" is a SASL-specific state similar to STATUS_EOF, in that we + * don't want to generate any server logs. But it's caused by an in-band + * client action that requires a server response, not an out-of-band + * connection closure, so we can't just proc_exit() like we do with + * STATUS_EOF. + */ + bool abandoned = false; + /* * Get the authentication method to use for this frontend/database * combination. Note: we do not parse the file at this point; this has @@ -608,16 +620,14 @@ ClientAuthentication(Port *port) Assert(false); #endif break; - case uaRADIUS: - status = CheckRADIUSAuth(port); - break; case uaCert: /* uaCert will be treated as if clientcert=verify-full (uaTrust) */ case uaTrust: status = STATUS_OK; break; case uaOAuth: - status = CheckSASLAuth(&pg_be_oauth_mech, port, NULL, NULL); + status = CheckSASLAuth(&pg_be_oauth_mech, port, NULL, &logdetail, + &abandoned); break; } @@ -658,7 +668,10 @@ ClientAuthentication(Port *port) if (status == STATUS_OK) sendAuthRequest(port, AUTH_REQ_OK, NULL, 0); else - auth_failed(port, status, logdetail); + auth_failed(port, + abandoned ? FATAL_CLIENT_ONLY : FATAL, + status, + logdetail); } @@ -749,7 +762,7 @@ recv_password_packet(Port *port) * We rely on that for MD5 and SCRAM authentication, but we still need * this check here, to prevent an empty password from being used with * authentication methods that check the password against an external - * system, like PAM, LDAP and RADIUS. + * system, like PAM and LDAP. */ if (buf.len == 1) ereport(ERROR, @@ -852,7 +865,7 @@ CheckPWChallengeAuth(Port *port, const char **logdetail) auth_result = CheckMD5Auth(port, shadow_pass, logdetail); else auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass, - logdetail); + logdetail, NULL /* can't abandon SCRAM */ ); if (shadow_pass) pfree(shadow_pass); @@ -990,8 +1003,8 @@ pg_GSS_recvauth(Port *port) gbuf.length = buf.len; gbuf.value = buf.data; - elog(DEBUG4, "processing received GSS token of length %u", - (unsigned int) gbuf.length); + elog(DEBUG4, "processing received GSS token of length %zu", + gbuf.length); maj_stat = gss_accept_sec_context(&min_stat, &port->gss->ctx, @@ -1009,9 +1022,9 @@ pg_GSS_recvauth(Port *port) pfree(buf.data); elog(DEBUG5, "gss_accept_sec_context major: %u, " - "minor: %u, outlen: %u, outflags: %x", + "minor: %u, outlen: %zu, outflags: %x", maj_stat, min_stat, - (unsigned int) port->gss->outbuf.length, gflags); + port->gss->outbuf.length, gflags); CHECK_FOR_INTERRUPTS(); @@ -1026,8 +1039,8 @@ pg_GSS_recvauth(Port *port) /* * Negotiation generated data to be sent to the client. */ - elog(DEBUG4, "sending GSS response token of length %u", - (unsigned int) port->gss->outbuf.length); + elog(DEBUG4, "sending GSS response token of length %zu", + port->gss->outbuf.length); sendAuthRequest(port, AUTH_REQ_GSS_CONT, port->gss->outbuf.value, port->gss->outbuf.length); @@ -1572,6 +1585,15 @@ pg_SSPI_make_upn(char *accountname, *---------------------------------------------------------------- */ +/* + * Per RFC 1413, space and tab are whitespace in ident messages. + */ +static bool +is_ident_whitespace(const char c) +{ + return c == ' ' || c == '\t'; +} + /* * Parse the string "*ident_response" as a response from a query to an Ident * server. If it's a normal response indicating a user name, return true @@ -1605,14 +1627,14 @@ interpret_ident_response(const char *ident_response, int i; /* Index into *response_type */ cursor++; /* Go over colon */ - while (pg_isblank(*cursor)) + while (is_ident_whitespace(*cursor)) cursor++; /* skip blanks */ i = 0; - while (*cursor != ':' && *cursor != '\r' && !pg_isblank(*cursor) && + while (*cursor != ':' && *cursor != '\r' && !is_ident_whitespace(*cursor) && i < (int) (sizeof(response_type) - 1)) response_type[i++] = *cursor++; response_type[i] = '\0'; - while (pg_isblank(*cursor)) + while (is_ident_whitespace(*cursor)) cursor++; /* skip blanks */ if (strcmp(response_type, "USERID") != 0) return false; @@ -1635,7 +1657,7 @@ interpret_ident_response(const char *ident_response, else { cursor++; /* Go over colon */ - while (pg_isblank(*cursor)) + while (is_ident_whitespace(*cursor)) cursor++; /* skip blanks */ /* Rest of line is user name. Copy it over. */ i = 0; @@ -1660,7 +1682,7 @@ interpret_ident_response(const char *ident_response, * latch was set would improve the responsiveness to timeouts/cancellations. */ static int -ident_inet(hbaPort *port) +ident_inet(Port *port) { const SockAddr remote_addr = port->raddr; const SockAddr local_addr = port->laddr; @@ -1845,7 +1867,7 @@ ident_inet(hbaPort *port) * Iff authorized, return STATUS_OK, otherwise return STATUS_ERROR. */ static int -auth_peer(hbaPort *port) +auth_peer(Port *port) { uid_t uid; gid_t gid; @@ -1917,7 +1939,7 @@ auth_peer(hbaPort *port) */ static int -pam_passwd_conv_proc(int num_msg, const struct pam_message **msg, +pam_passwd_conv_proc(int num_msg, PG_PAM_CONST struct pam_message **msg, struct pam_response **resp, void *appdata_ptr) { const char *passwd; @@ -1985,7 +2007,7 @@ pam_passwd_conv_proc(int num_msg, const struct pam_message **msg, ereport(LOG, (errmsg("error from underlying PAM layer: %s", msg[i]->msg))); - /* FALL THROUGH */ + pg_fallthrough; case PAM_TEXT_INFO: /* we don't bother to log TEXT_INFO messages */ if ((reply[i].resp = strdup("")) == NULL) @@ -2223,8 +2245,8 @@ InitializeLDAPConnection(Port *port, LDAP **ldap) if (!*ldap) { ereport(LOG, - (errmsg("could not initialize LDAP: error code %d", - (int) LdapGetLastError()))); + (errmsg("could not initialize LDAP: error code %lu", + LdapGetLastError()))); return STATUS_ERROR; } @@ -2755,499 +2777,3 @@ CheckCertAuth(Port *port) return status_check_usermap; } #endif - - -/*---------------------------------------------------------------- - * RADIUS authentication - *---------------------------------------------------------------- - */ - -/* - * RADIUS authentication is described in RFC2865 (and several others). - */ - -#define RADIUS_VECTOR_LENGTH 16 -#define RADIUS_HEADER_LENGTH 20 -#define RADIUS_MAX_PASSWORD_LENGTH 128 - -/* Maximum size of a RADIUS packet we will create or accept */ -#define RADIUS_BUFFER_SIZE 1024 - -typedef struct -{ - uint8 attribute; - uint8 length; - uint8 data[FLEXIBLE_ARRAY_MEMBER]; -} radius_attribute; - -typedef struct -{ - uint8 code; - uint8 id; - uint16 length; - uint8 vector[RADIUS_VECTOR_LENGTH]; - /* this is a bit longer than strictly necessary: */ - char pad[RADIUS_BUFFER_SIZE - RADIUS_VECTOR_LENGTH]; -} radius_packet; - -/* RADIUS packet types */ -#define RADIUS_ACCESS_REQUEST 1 -#define RADIUS_ACCESS_ACCEPT 2 -#define RADIUS_ACCESS_REJECT 3 - -/* RADIUS attributes */ -#define RADIUS_USER_NAME 1 -#define RADIUS_PASSWORD 2 -#define RADIUS_SERVICE_TYPE 6 -#define RADIUS_NAS_IDENTIFIER 32 - -/* RADIUS service types */ -#define RADIUS_AUTHENTICATE_ONLY 8 - -/* Seconds to wait - XXX: should be in a config variable! */ -#define RADIUS_TIMEOUT 3 - -static void -radius_add_attribute(radius_packet *packet, uint8 type, const unsigned char *data, int len) -{ - radius_attribute *attr; - - if (packet->length + len > RADIUS_BUFFER_SIZE) - { - /* - * With remotely realistic data, this can never happen. But catch it - * just to make sure we don't overrun a buffer. We'll just skip adding - * the broken attribute, which will in the end cause authentication to - * fail. - */ - elog(WARNING, - "adding attribute code %d with length %d to radius packet would create oversize packet, ignoring", - type, len); - return; - } - - attr = (radius_attribute *) ((unsigned char *) packet + packet->length); - attr->attribute = type; - attr->length = len + 2; /* total size includes type and length */ - memcpy(attr->data, data, len); - packet->length += attr->length; -} - -static int -CheckRADIUSAuth(Port *port) -{ - char *passwd; - ListCell *server, - *secrets, - *radiusports, - *identifiers; - - /* Make sure struct alignment is correct */ - Assert(offsetof(radius_packet, vector) == 4); - - /* Verify parameters */ - if (port->hba->radiusservers == NIL) - { - ereport(LOG, - (errmsg("RADIUS server not specified"))); - return STATUS_ERROR; - } - - if (port->hba->radiussecrets == NIL) - { - ereport(LOG, - (errmsg("RADIUS secret not specified"))); - return STATUS_ERROR; - } - - /* Send regular password request to client, and get the response */ - sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); - - passwd = recv_password_packet(port); - if (passwd == NULL) - return STATUS_EOF; /* client wouldn't send password */ - - if (strlen(passwd) > RADIUS_MAX_PASSWORD_LENGTH) - { - ereport(LOG, - (errmsg("RADIUS authentication does not support passwords longer than %d characters", RADIUS_MAX_PASSWORD_LENGTH))); - pfree(passwd); - return STATUS_ERROR; - } - - /* - * Loop over and try each server in order. - */ - secrets = list_head(port->hba->radiussecrets); - radiusports = list_head(port->hba->radiusports); - identifiers = list_head(port->hba->radiusidentifiers); - foreach(server, port->hba->radiusservers) - { - int ret = PerformRadiusTransaction(lfirst(server), - lfirst(secrets), - radiusports ? lfirst(radiusports) : NULL, - identifiers ? lfirst(identifiers) : NULL, - port->user_name, - passwd); - - /*------ - * STATUS_OK = Login OK - * STATUS_ERROR = Login not OK, but try next server - * STATUS_EOF = Login not OK, and don't try next server - *------ - */ - if (ret == STATUS_OK) - { - set_authn_id(port, port->user_name); - - pfree(passwd); - return STATUS_OK; - } - else if (ret == STATUS_EOF) - { - pfree(passwd); - return STATUS_ERROR; - } - - /* - * secret, port and identifiers either have length 0 (use default), - * length 1 (use the same everywhere) or the same length as servers. - * So if the length is >1, we advance one step. In other cases, we - * don't and will then reuse the correct value. - */ - if (list_length(port->hba->radiussecrets) > 1) - secrets = lnext(port->hba->radiussecrets, secrets); - if (list_length(port->hba->radiusports) > 1) - radiusports = lnext(port->hba->radiusports, radiusports); - if (list_length(port->hba->radiusidentifiers) > 1) - identifiers = lnext(port->hba->radiusidentifiers, identifiers); - } - - /* No servers left to try, so give up */ - pfree(passwd); - return STATUS_ERROR; -} - -static int -PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd) -{ - radius_packet radius_send_pack; - radius_packet radius_recv_pack; - radius_packet *packet = &radius_send_pack; - radius_packet *receivepacket = &radius_recv_pack; - void *radius_buffer = &radius_send_pack; - void *receive_buffer = &radius_recv_pack; - int32 service = pg_hton32(RADIUS_AUTHENTICATE_ONLY); - uint8 *cryptvector; - int encryptedpasswordlen; - uint8 encryptedpassword[RADIUS_MAX_PASSWORD_LENGTH]; - uint8 *md5trailer; - int packetlength; - pgsocket sock; - - struct sockaddr_in6 localaddr; - struct sockaddr_in6 remoteaddr; - struct addrinfo hint; - struct addrinfo *serveraddrs; - int port; - socklen_t addrsize; - fd_set fdset; - struct timeval endtime; - int i, - j, - r; - - /* Assign default values */ - if (portstr == NULL) - portstr = "1812"; - if (identifier == NULL) - identifier = "postgresql"; - - MemSet(&hint, 0, sizeof(hint)); - hint.ai_socktype = SOCK_DGRAM; - hint.ai_family = AF_UNSPEC; - port = atoi(portstr); - - r = pg_getaddrinfo_all(server, portstr, &hint, &serveraddrs); - if (r || !serveraddrs) - { - ereport(LOG, - (errmsg("could not translate RADIUS server name \"%s\" to address: %s", - server, gai_strerror(r)))); - if (serveraddrs) - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - return STATUS_ERROR; - } - /* XXX: add support for multiple returned addresses? */ - - /* Construct RADIUS packet */ - packet->code = RADIUS_ACCESS_REQUEST; - packet->length = RADIUS_HEADER_LENGTH; - if (!pg_strong_random(packet->vector, RADIUS_VECTOR_LENGTH)) - { - ereport(LOG, - (errmsg("could not generate random encryption vector"))); - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - return STATUS_ERROR; - } - packet->id = packet->vector[0]; - radius_add_attribute(packet, RADIUS_SERVICE_TYPE, (const unsigned char *) &service, sizeof(service)); - radius_add_attribute(packet, RADIUS_USER_NAME, (const unsigned char *) user_name, strlen(user_name)); - radius_add_attribute(packet, RADIUS_NAS_IDENTIFIER, (const unsigned char *) identifier, strlen(identifier)); - - /* - * RADIUS password attributes are calculated as: e[0] = p[0] XOR - * MD5(secret + Request Authenticator) for the first group of 16 octets, - * and then: e[i] = p[i] XOR MD5(secret + e[i-1]) for the following ones - * (if necessary) - */ - encryptedpasswordlen = ((strlen(passwd) + RADIUS_VECTOR_LENGTH - 1) / RADIUS_VECTOR_LENGTH) * RADIUS_VECTOR_LENGTH; - cryptvector = palloc(strlen(secret) + RADIUS_VECTOR_LENGTH); - memcpy(cryptvector, secret, strlen(secret)); - - /* for the first iteration, we use the Request Authenticator vector */ - md5trailer = packet->vector; - for (i = 0; i < encryptedpasswordlen; i += RADIUS_VECTOR_LENGTH) - { - const char *errstr = NULL; - - memcpy(cryptvector + strlen(secret), md5trailer, RADIUS_VECTOR_LENGTH); - - /* - * .. and for subsequent iterations the result of the previous XOR - * (calculated below) - */ - md5trailer = encryptedpassword + i; - - if (!pg_md5_binary(cryptvector, strlen(secret) + RADIUS_VECTOR_LENGTH, - encryptedpassword + i, &errstr)) - { - ereport(LOG, - (errmsg("could not perform MD5 encryption of password: %s", - errstr))); - pfree(cryptvector); - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - return STATUS_ERROR; - } - - for (j = i; j < i + RADIUS_VECTOR_LENGTH; j++) - { - if (j < strlen(passwd)) - encryptedpassword[j] = passwd[j] ^ encryptedpassword[j]; - else - encryptedpassword[j] = '\0' ^ encryptedpassword[j]; - } - } - pfree(cryptvector); - - radius_add_attribute(packet, RADIUS_PASSWORD, encryptedpassword, encryptedpasswordlen); - - /* Length needs to be in network order on the wire */ - packetlength = packet->length; - packet->length = pg_hton16(packet->length); - - sock = socket(serveraddrs[0].ai_family, SOCK_DGRAM, 0); - if (sock == PGINVALID_SOCKET) - { - ereport(LOG, - (errmsg("could not create RADIUS socket: %m"))); - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - return STATUS_ERROR; - } - - memset(&localaddr, 0, sizeof(localaddr)); - localaddr.sin6_family = serveraddrs[0].ai_family; - localaddr.sin6_addr = in6addr_any; - if (localaddr.sin6_family == AF_INET6) - addrsize = sizeof(struct sockaddr_in6); - else - addrsize = sizeof(struct sockaddr_in); - - if (bind(sock, (struct sockaddr *) &localaddr, addrsize)) - { - ereport(LOG, - (errmsg("could not bind local RADIUS socket: %m"))); - closesocket(sock); - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - return STATUS_ERROR; - } - - if (sendto(sock, radius_buffer, packetlength, 0, - serveraddrs[0].ai_addr, serveraddrs[0].ai_addrlen) < 0) - { - ereport(LOG, - (errmsg("could not send RADIUS packet: %m"))); - closesocket(sock); - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - return STATUS_ERROR; - } - - /* Don't need the server address anymore */ - pg_freeaddrinfo_all(hint.ai_family, serveraddrs); - - /* - * Figure out at what time we should time out. We can't just use a single - * call to select() with a timeout, since somebody can be sending invalid - * packets to our port thus causing us to retry in a loop and never time - * out. - * - * XXX: Using WaitLatchOrSocket() and doing a CHECK_FOR_INTERRUPTS() if - * the latch was set would improve the responsiveness to - * timeouts/cancellations. - */ - gettimeofday(&endtime, NULL); - endtime.tv_sec += RADIUS_TIMEOUT; - - while (true) - { - struct timeval timeout; - struct timeval now; - int64 timeoutval; - const char *errstr = NULL; - - gettimeofday(&now, NULL); - timeoutval = (endtime.tv_sec * 1000000 + endtime.tv_usec) - (now.tv_sec * 1000000 + now.tv_usec); - if (timeoutval <= 0) - { - ereport(LOG, - (errmsg("timeout waiting for RADIUS response from %s", - server))); - closesocket(sock); - return STATUS_ERROR; - } - timeout.tv_sec = timeoutval / 1000000; - timeout.tv_usec = timeoutval % 1000000; - - FD_ZERO(&fdset); - FD_SET(sock, &fdset); - - r = select(sock + 1, &fdset, NULL, NULL, &timeout); - if (r < 0) - { - if (errno == EINTR) - continue; - - /* Anything else is an actual error */ - ereport(LOG, - (errmsg("could not check status on RADIUS socket: %m"))); - closesocket(sock); - return STATUS_ERROR; - } - if (r == 0) - { - ereport(LOG, - (errmsg("timeout waiting for RADIUS response from %s", - server))); - closesocket(sock); - return STATUS_ERROR; - } - - /* - * Attempt to read the response packet, and verify the contents. - * - * Any packet that's not actually a RADIUS packet, or otherwise does - * not validate as an explicit reject, is just ignored and we retry - * for another packet (until we reach the timeout). This is to avoid - * the possibility to denial-of-service the login by flooding the - * server with invalid packets on the port that we're expecting the - * RADIUS response on. - */ - - addrsize = sizeof(remoteaddr); - packetlength = recvfrom(sock, receive_buffer, RADIUS_BUFFER_SIZE, 0, - (struct sockaddr *) &remoteaddr, &addrsize); - if (packetlength < 0) - { - ereport(LOG, - (errmsg("could not read RADIUS response: %m"))); - closesocket(sock); - return STATUS_ERROR; - } - - if (remoteaddr.sin6_port != pg_hton16(port)) - { - ereport(LOG, - (errmsg("RADIUS response from %s was sent from incorrect port: %d", - server, pg_ntoh16(remoteaddr.sin6_port)))); - continue; - } - - if (packetlength < RADIUS_HEADER_LENGTH) - { - ereport(LOG, - (errmsg("RADIUS response from %s too short: %d", server, packetlength))); - continue; - } - - if (packetlength != pg_ntoh16(receivepacket->length)) - { - ereport(LOG, - (errmsg("RADIUS response from %s has corrupt length: %d (actual length %d)", - server, pg_ntoh16(receivepacket->length), packetlength))); - continue; - } - - if (packet->id != receivepacket->id) - { - ereport(LOG, - (errmsg("RADIUS response from %s is to a different request: %d (should be %d)", - server, receivepacket->id, packet->id))); - continue; - } - - /* - * Verify the response authenticator, which is calculated as - * MD5(Code+ID+Length+RequestAuthenticator+Attributes+Secret) - */ - cryptvector = palloc(packetlength + strlen(secret)); - - memcpy(cryptvector, receivepacket, 4); /* code+id+length */ - memcpy(cryptvector + 4, packet->vector, RADIUS_VECTOR_LENGTH); /* request - * authenticator, from - * original packet */ - if (packetlength > RADIUS_HEADER_LENGTH) /* there may be no - * attributes at all */ - memcpy(cryptvector + RADIUS_HEADER_LENGTH, - (char *) receive_buffer + RADIUS_HEADER_LENGTH, - packetlength - RADIUS_HEADER_LENGTH); - memcpy(cryptvector + packetlength, secret, strlen(secret)); - - if (!pg_md5_binary(cryptvector, - packetlength + strlen(secret), - encryptedpassword, &errstr)) - { - ereport(LOG, - (errmsg("could not perform MD5 encryption of received packet: %s", - errstr))); - pfree(cryptvector); - continue; - } - pfree(cryptvector); - - if (memcmp(receivepacket->vector, encryptedpassword, RADIUS_VECTOR_LENGTH) != 0) - { - ereport(LOG, - (errmsg("RADIUS response from %s has incorrect MD5 signature", - server))); - continue; - } - - if (receivepacket->code == RADIUS_ACCESS_ACCEPT) - { - closesocket(sock); - return STATUS_OK; - } - else if (receivepacket->code == RADIUS_ACCESS_REJECT) - { - closesocket(sock); - return STATUS_EOF; - } - else - { - ereport(LOG, - (errmsg("RADIUS response from %s has invalid code (%d) for user \"%s\"", - server, receivepacket->code, user_name))); - continue; - } - } /* while (true) */ -} diff --git a/src/backend/libpq/be-fsstubs.c b/src/backend/libpq/be-fsstubs.c index e5a34c6193165..f27e374c4eeed 100644 --- a/src/backend/libpq/be-fsstubs.c +++ b/src/backend/libpq/be-fsstubs.c @@ -3,7 +3,7 @@ * be-fsstubs.c * Builtin functions for open/close/read/write operations on large objects * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/libpq/be-gssapi-common.c b/src/backend/libpq/be-gssapi-common.c index 7adea3060e17e..034c54895cc32 100644 --- a/src/backend/libpq/be-gssapi-common.c +++ b/src/backend/libpq/be-gssapi-common.c @@ -3,7 +3,7 @@ * be-gssapi-common.c * Common code for GSSAPI authentication and encryption * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c index e8b837d1fa78d..ad04bedaa1de1 100644 --- a/src/backend/libpq/be-secure-common.c +++ b/src/backend/libpq/be-secure-common.c @@ -8,7 +8,7 @@ * communications code calls, this file contains support routines that are * used by the library-specific implementations such as be-secure-openssl.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -26,18 +26,25 @@ #include "common/string.h" #include "libpq/libpq.h" #include "storage/fd.h" +#include "utils/builtins.h" +#include "utils/guc.h" + +static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel); /* * Run ssl_passphrase_command * * prompt will be substituted for %p. is_server_start determines the loglevel - * of error messages. + * of error messages from executing the command, the loglevel for failures in + * param substitution will be ERROR regardless of is_server_start. The actual + * command used depends on the configuration for the current host. * * The result will be put in buffer buf, which is of size size. The return * value is the length of the actual result. */ int -run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size) +run_ssl_passphrase_command(const char *cmd, const char *prompt, + bool is_server_start, char *buf, int size) { int loglevel = is_server_start ? ERROR : LOG; char *command; @@ -49,7 +56,7 @@ run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, Assert(size > 0); buf[0] = '\0'; - command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt); + command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt); fh = OpenPipeStream(command, "r"); if (fh == NULL) @@ -175,3 +182,257 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart) return true; } + +/* + * parse_hosts_line + * + * Parses a loaded line from the pg_hosts.conf configuration and pulls out the + * hostname, certificate, key and CA parts in order to build an SNI config in + * the TLS backend. Validation of the parsed values is left for the TLS backend + * to implement. + */ +static HostsLine * +parse_hosts_line(TokenizedAuthLine *tok_line, int elevel) +{ + HostsLine *parsedline; + List *tokens; + ListCell *field; + AuthToken *token; + + parsedline = palloc0(sizeof(HostsLine)); + parsedline->sourcefile = pstrdup(tok_line->file_name); + parsedline->linenumber = tok_line->line_num; + parsedline->rawline = pstrdup(tok_line->raw_line); + parsedline->hostnames = NIL; + + /* Initialize optional fields */ + parsedline->ssl_passphrase_cmd = NULL; + parsedline->ssl_passphrase_reload = false; + + /* Hostname */ + field = list_head(tok_line->fields); + tokens = lfirst(field); + foreach_ptr(AuthToken, hostname, tokens) + { + if ((tokens->length > 1) && + (strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0)) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("default and non-SNI entries cannot be mixed with other entries"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + + parsedline->hostnames = lappend(parsedline->hostnames, pstrdup(hostname->string)); + } + + /* SSL Certificate (Required) */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("missing entry at end of line"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for SSL certificate"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + token = linitial(tokens); + parsedline->ssl_cert = pstrdup(token->string); + + /* SSL key (Required) */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("missing entry at end of line"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for SSL key"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + token = linitial(tokens); + parsedline->ssl_key = pstrdup(token->string); + + /* SSL CA (optional) */ + field = lnext(tok_line->fields, field); + if (!field) + return parsedline; + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for SSL CA"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + token = linitial(tokens); + parsedline->ssl_ca = pstrdup(token->string); + + /* SSL Passphrase Command (optional) */ + field = lnext(tok_line->fields, field); + if (field) + { + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for SSL passphrase command"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + token = linitial(tokens); + parsedline->ssl_passphrase_cmd = pstrdup(token->string); + + /* + * SSL Passphrase Command support reload (optional). This field is + * only supported if there was a passphrase command parsed first, so + * nest it under the previous token. + */ + field = lnext(tok_line->fields, field); + if (field) + { + tokens = lfirst(field); + token = linitial(tokens); + + /* + * There should be no more tokens after this, if there are break + * parsing and report error to avoid silently accepting incorrect + * config. + */ + if (lnext(tok_line->fields, field)) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("extra fields at end of line"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + + if (tokens->length > 1 || !parse_bool(token->string, &parsedline->ssl_passphrase_reload)) + { + ereport(elevel, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"), + errcontext("line %d of configuration file \"%s\"", + tok_line->line_num, tok_line->file_name)); + return NULL; + } + } + } + + return parsedline; +} + +/* + * load_hosts + * + * Reads and parses the pg_hosts.conf configuration file and passes back a List + * of HostsLine elements containing the parsed lines, or NIL in case of an empty + * file. The list is returned in the hosts parameter. The function will return + * a HostsFileLoadResult value detailing the result of the operation. When + * the hosts configuration failed to load, the err_msg variable may have more + * information in case it was passed as non-NULL. + */ +int +load_hosts(List **hosts, char **err_msg) +{ + FILE *file; + ListCell *line; + List *hosts_lines = NIL; + List *parsed_lines = NIL; + HostsLine *newline; + bool ok = true; + + /* + * If we cannot return results then error out immediately. This implies + * API misuse or a similar kind of programmer error. + */ + if (!hosts) + { + if (err_msg) + *err_msg = psprintf("cannot load config from \"%s\", return variable missing", + HostsFileName); + return HOSTSFILE_LOAD_FAILED; + } + *hosts = NIL; + + /* + * This is not an auth file per se, but it is using the same file format + * as the pg_hba and pg_ident files and thus the same code infrastructure. + * A future TODO might be to rename the supporting code with a more + * generic name? + */ + file = open_auth_file(HostsFileName, LOG, 0, err_msg); + if (file == NULL) + { + if (errno == ENOENT) + return HOSTSFILE_MISSING; + + return HOSTSFILE_LOAD_FAILED; + } + + tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0); + + foreach(line, hosts_lines) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line); + + /* + * Mark processing as not-ok in case lines are found with errors in + * tokenization (.err_msg is set) or during parsing. + */ + if ((tok_line->err_msg != NULL) || + ((newline = parse_hosts_line(tok_line, LOG)) == NULL)) + { + ok = false; + continue; + } + + parsed_lines = lappend(parsed_lines, newline); + } + + /* Free memory from tokenizer */ + free_auth_file(file, 0); + *hosts = parsed_lines; + + if (!ok) + { + if (err_msg) + *err_msg = psprintf("loading config from \"%s\" failed due to parsing error", + HostsFileName); + return HOSTSFILE_LOAD_FAILED; + } + + if (parsed_lines == NIL) + return HOSTSFILE_EMPTY; + + return HOSTSFILE_LOAD_OK; +} diff --git a/src/backend/libpq/be-secure-gssapi.c b/src/backend/libpq/be-secure-gssapi.c index 717ba9824f914..540ed62a5ccf6 100644 --- a/src/backend/libpq/be-secure-gssapi.c +++ b/src/backend/libpq/be-secure-gssapi.c @@ -3,7 +3,7 @@ * be-secure-gssapi.c * GSSAPI encryption support * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/libpq/be-secure-gssapi.c @@ -21,8 +21,10 @@ #include "miscadmin.h" #include "pgstat.h" #include "port/pg_bswap.h" +#include "storage/latch.h" #include "utils/injection_point.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* @@ -46,11 +48,18 @@ * don't want the other side to send arbitrarily huge packets as we * would have to allocate memory for them to then pass them to GSSAPI. * - * Therefore, these two #define's are effectively part of the protocol + * Therefore, this #define is effectively part of the protocol * spec and can't ever be changed. */ -#define PQ_GSS_SEND_BUFFER_SIZE 16384 -#define PQ_GSS_RECV_BUFFER_SIZE 16384 +#define PQ_GSS_MAX_PACKET_SIZE 16384 /* includes uint32 header word */ + +/* + * However, during the authentication exchange we must cope with whatever + * message size the GSSAPI library wants to send (because our protocol + * doesn't support splitting those messages). Depending on configuration + * those messages might be as much as 64kB. + */ +#define PQ_GSS_AUTH_BUFFER_SIZE 65536 /* includes uint32 header word */ /* * Since we manage at most one GSS-encrypted connection per backend, @@ -114,9 +123,9 @@ be_gssapi_write(Port *port, const void *ptr, size_t len) * again, so if it offers a len less than that, something is wrong. * * Note: it may seem attractive to report partial write completion once - * we've successfully sent any encrypted packets. However, that can cause - * problems for callers; notably, pqPutMsgEnd's heuristic to send only - * full 8K blocks interacts badly with such a hack. We won't save much, + * we've successfully sent any encrypted packets. However, doing that + * expands the state space of this processing and has been responsible for + * bugs in the past (cf. commit d053a879b). We won't save much, * typically, by letting callers discard data early, so don't risk it. */ if (len < PqGSSSendConsumed) @@ -210,12 +219,12 @@ be_gssapi_write(Port *port, const void *ptr, size_t len) errno = ECONNRESET; return -1; } - if (output.length > PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)) + if (output.length > PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)) { ereport(COMMERROR, (errmsg("server tried to send oversize GSSAPI packet (%zu > %zu)", (size_t) output.length, - PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)))); + PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)))); errno = ECONNRESET; return -1; } @@ -346,12 +355,12 @@ be_gssapi_read(Port *port, void *ptr, size_t len) /* Decode the packet length and check for overlength packet */ input.length = pg_ntoh32(*(uint32 *) PqGSSRecvBuffer); - if (input.length > PQ_GSS_RECV_BUFFER_SIZE - sizeof(uint32)) + if (input.length > PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)) { ereport(COMMERROR, (errmsg("oversize GSSAPI packet sent by the client (%zu > %zu)", (size_t) input.length, - PQ_GSS_RECV_BUFFER_SIZE - sizeof(uint32)))); + PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)))); errno = ECONNRESET; return -1; } @@ -517,10 +526,13 @@ secure_open_gssapi(Port *port) * that will never use them, and we ensure that the buffers are * sufficiently aligned for the length-word accesses that we do in some * places in this file. + * + * We'll use PQ_GSS_AUTH_BUFFER_SIZE-sized buffers until transport + * negotiation is complete, then switch to PQ_GSS_MAX_PACKET_SIZE. */ - PqGSSSendBuffer = malloc(PQ_GSS_SEND_BUFFER_SIZE); - PqGSSRecvBuffer = malloc(PQ_GSS_RECV_BUFFER_SIZE); - PqGSSResultBuffer = malloc(PQ_GSS_RECV_BUFFER_SIZE); + PqGSSSendBuffer = malloc(PQ_GSS_AUTH_BUFFER_SIZE); + PqGSSRecvBuffer = malloc(PQ_GSS_AUTH_BUFFER_SIZE); + PqGSSResultBuffer = malloc(PQ_GSS_AUTH_BUFFER_SIZE); if (!PqGSSSendBuffer || !PqGSSRecvBuffer || !PqGSSResultBuffer) ereport(FATAL, (errcode(ERRCODE_OUT_OF_MEMORY), @@ -568,16 +580,16 @@ secure_open_gssapi(Port *port) /* * During initialization, packets are always fully consumed and - * shouldn't ever be over PQ_GSS_RECV_BUFFER_SIZE in length. + * shouldn't ever be over PQ_GSS_AUTH_BUFFER_SIZE in total length. * * Verify on our side that the client doesn't do something funny. */ - if (input.length > PQ_GSS_RECV_BUFFER_SIZE) + if (input.length > PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)) { ereport(COMMERROR, - (errmsg("oversize GSSAPI packet sent by the client (%zu > %d)", + (errmsg("oversize GSSAPI packet sent by the client (%zu > %zu)", (size_t) input.length, - PQ_GSS_RECV_BUFFER_SIZE))); + PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)))); return -1; } @@ -631,12 +643,12 @@ secure_open_gssapi(Port *port) { uint32 netlen = pg_hton32(output.length); - if (output.length > PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)) + if (output.length > PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)) { ereport(COMMERROR, (errmsg("server tried to send oversize GSSAPI packet (%zu > %zu)", (size_t) output.length, - PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)))); + PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)))); gss_release_buffer(&minor, &output); return -1; } @@ -691,12 +703,29 @@ secure_open_gssapi(Port *port) break; } + /* + * Release the large authentication buffers and allocate the ones we want + * for normal operation. + */ + free(PqGSSSendBuffer); + free(PqGSSRecvBuffer); + free(PqGSSResultBuffer); + PqGSSSendBuffer = malloc(PQ_GSS_MAX_PACKET_SIZE); + PqGSSRecvBuffer = malloc(PQ_GSS_MAX_PACKET_SIZE); + PqGSSResultBuffer = malloc(PQ_GSS_MAX_PACKET_SIZE); + if (!PqGSSSendBuffer || !PqGSSRecvBuffer || !PqGSSResultBuffer) + ereport(FATAL, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + PqGSSSendLength = PqGSSSendNext = PqGSSSendConsumed = 0; + PqGSSRecvLength = PqGSSResultLength = PqGSSResultNext = 0; + /* * Determine the max packet size which will fit in our buffer, after * accounting for the length. be_gssapi_write will need this. */ major = gss_wrap_size_limit(&minor, port->gss->ctx, 1, GSS_C_QOP_DEFAULT, - PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32), + PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32), &PqGSSMaxPktSize); if (GSS_ERROR(major)) diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 64ff3ce3d6a7a..f64b2787f668b 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -4,7 +4,7 @@ * functions for OpenSSL support in the backend. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -27,6 +27,7 @@ #include #include +#include "common/hashfn.h" #include "common/string.h" #include "libpq/libpq.h" #include "miscadmin.h" @@ -35,6 +36,7 @@ #include "storage/latch.h" #include "utils/guc.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* * These SSL-related #includes must come after all system-provided headers. @@ -51,6 +53,27 @@ #endif #include +/* + * Simplehash for tracking configured hostnames to guard against duplicate + * entries. Each list of hosts is traversed and added to the hash during + * parsing and if a duplicate error is detected an error will be thrown. + */ +typedef struct +{ + uint32 status; + const char *hostname; +} HostCacheEntry; +static uint32 host_cache_pointer(const char *key); +#define SH_PREFIX host_cache +#define SH_ELEMENT_TYPE HostCacheEntry +#define SH_KEY_TYPE const char * +#define SH_KEY hostname +#define SH_HASH_KEY(tb, key) host_cache_pointer(key) +#define SH_EQUAL(tb, a, b) (pg_strcasecmp(a, b) == 0) +#define SH_SCOPE static inline +#define SH_DECLARE +#define SH_DEFINE +#include "lib/simplehash.h" /* default init hook can be overridden by a shared library */ static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart); @@ -77,18 +100,48 @@ static bool initialize_dh(SSL_CTX *context, bool isServerStart); static bool initialize_ecdh(SSL_CTX *context, bool isServerStart); static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement); static const char *SSLerrmessage(unsigned long ecode); +static bool init_host_context(HostsLine *host, bool isServerStart); +static void host_context_cleanup_cb(void *arg); +#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB +static int sni_clienthello_cb(SSL *ssl, int *al, void *arg); +#endif static char *X509_NAME_to_cstring(X509_NAME *name); static SSL_CTX *SSL_context = NULL; +static MemoryContext SSL_hosts_memcxt = NULL; +static struct hosts +{ + /* + * List of HostsLine structures containing SSL configurations for + * connections with hostnames defined in the SNI extension. + */ + List *sni; + + /* The SSL configuration to use for connections without SNI */ + HostsLine *no_sni; + + /* + * The default SSL configuration to use as a fallback in case no hostname + * matches the supplied hostname in the SNI extension. + */ + HostsLine *default_host; +} *SSL_hosts; + static bool dummy_ssl_passwd_cb_called = false; static bool ssl_is_server_start; static int ssl_protocol_version_to_openssl(int v); static const char *ssl_protocol_version_to_string(int v); -/* for passing data back from verify_cb() */ -static const char *cert_errdetail; +struct CallbackErr +{ + /* + * Storage for passing certificate verification error logging from the + * callback. + */ + char *cert_errdetail; +}; /* ------------------------------------------------------------ */ /* Public interface */ @@ -97,88 +150,269 @@ static const char *cert_errdetail; int be_tls_init(bool isServerStart) { - SSL_CTX *context; + List *pg_hosts = NIL; + ListCell *line; + MemoryContext oldcxt; + MemoryContext host_memcxt = NULL; + MemoryContextCallback *host_memcxt_cb; + char *err_msg = NULL; + int res; + struct hosts *new_hosts; + SSL_CTX *context = NULL; int ssl_ver_min = -1; int ssl_ver_max = -1; + host_cache_hash *host_cache = NULL; /* - * Create a new SSL context into which we'll load all the configuration - * settings. If we fail partway through, we can avoid memory leakage by - * freeing this context; we don't install it as active until the end. + * Since we don't know which host we're using until the ClientHello is + * sent, ssl_loaded_verify_locations *always* starts out as false. The + * only place it's set to true is in sni_clienthello_cb(). + */ + ssl_loaded_verify_locations = false; + + host_memcxt = AllocSetContextCreate(CurrentMemoryContext, + "hosts file parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(host_memcxt); + + /* Allocate a tentative replacement for SSL_hosts. */ + new_hosts = palloc0_object(struct hosts); + + /* + * Register a reset callback for the memory context which is responsible + * for freeing OpenSSL managed allocations upon context deletion. The + * callback is allocated here to make sure it gets cleaned up along with + * the memory context it's registered for. + */ + host_memcxt_cb = palloc0_object(MemoryContextCallback); + host_memcxt_cb->func = host_context_cleanup_cb; + host_memcxt_cb->arg = new_hosts; + MemoryContextRegisterResetCallback(host_memcxt, host_memcxt_cb); + + /* + * If ssl_sni is enabled, attempt to load and parse TLS configuration from + * the pg_hosts.conf file with the set of hosts returned as a list. If + * there are hosts configured they take precedence over the configuration + * in postgresql.conf. Make sure to allocate the parsed rows in their own + * memory context so that we can delete them easily in case parsing fails. + * If ssl_sni is disabled then set the state accordingly to make sure we + * instead parse the config from postgresql.conf. * - * We use SSLv23_method() because it can negotiate use of the highest - * mutually supported protocol version, while alternatives like - * TLSv1_2_method() permit only one specific version. Note that we don't - * actually allow SSL v2 or v3, only TLS protocols (see below). + * The reason for not doing everything in this if-else conditional is that + * we want to use the same processing of postgresql.conf for when ssl_sni + * is off as well as when it's on but the hosts file is missing etc. Thus + * we set res to the state and continue with a new conditional instead of + * duplicating logic and risk it diverging over time. */ - context = SSL_CTX_new(SSLv23_method()); - if (!context) + if (ssl_sni) { + /* + * The GUC check hook should have already blocked this but to be on + * the safe side we double-check here. + */ +#ifndef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB ereport(isServerStart ? FATAL : LOG, - (errmsg("could not create SSL context: %s", - SSLerrmessage(ERR_get_error())))); + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("ssl_sni is not supported with LibreSSL")); goto error; +#endif + + /* Attempt to load configuration from pg_hosts.conf */ + res = load_hosts(&pg_hosts, &err_msg); + + /* + * pg_hosts.conf is not required to contain configuration, but if it + * does we error out in case it fails to load rather than continue to + * try the postgresql.conf configuration to avoid silently falling + * back on an undesired configuration. + */ + if (res == HOSTSFILE_LOAD_FAILED) + { + ereport(isServerStart ? FATAL : LOG, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load \"%s\": %s", "pg_hosts.conf", + err_msg ? err_msg : "unknown error")); + goto error; + } } + else + res = HOSTSFILE_DISABLED; /* - * Disable OpenSSL's moving-write-buffer sanity check, because it causes - * unnecessary failures in nonblocking send cases. + * Loading and parsing the hosts file was successful, create configs for + * each host entry and add to the list of hosts to be checked during + * login. */ - SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + if (res == HOSTSFILE_LOAD_OK) + { + Assert(ssl_sni); + + foreach(line, pg_hosts) + { + HostsLine *host = lfirst(line); + + if (!init_host_context(host, isServerStart)) + goto error; + + /* + * The hostname in the config will be set to NULL for the default + * host as well as in configs used for non-SNI connections. Lists + * of hostnames in pg_hosts.conf are not allowed to contain the + * default '*' entry or a '/no_sni/' entry and this is checked + * during parsing. Thus we can inspect the head of the hostnames + * list for these since they will never be anywhere else. + */ + if (strcmp(linitial(host->hostnames), "*") == 0) + { + if (new_hosts->default_host) + { + ereport(isServerStart ? FATAL : LOG, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple default hosts specified"), + errcontext("line %d of configuration file \"%s\"", + host->linenumber, host->sourcefile)); + goto error; + } + + new_hosts->default_host = host; + } + else if (strcmp(linitial(host->hostnames), "/no_sni/") == 0) + { + if (new_hosts->no_sni) + { + ereport(isServerStart ? FATAL : LOG, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple no_sni hosts specified"), + errcontext("line %d of configuration file \"%s\"", + host->linenumber, host->sourcefile)); + goto error; + } + + new_hosts->no_sni = host; + } + else + { + /* Check the hostnames for duplicates */ + if (!host_cache) + host_cache = host_cache_create(host_memcxt, 32, NULL); + + foreach_ptr(char, hostname, host->hostnames) + { + HostCacheEntry *entry; + bool found; + + entry = host_cache_insert(host_cache, hostname, &found); + if (found) + { + ereport(isServerStart ? FATAL : LOG, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple entries for host \"%s\" specified", + hostname), + errcontext("line %d of configuration file \"%s\"", + host->linenumber, host->sourcefile)); + goto error; + } + else + entry->hostname = pstrdup(hostname); + } + + /* + * At this point we know we have a configuration with a list + * of distinct 1..n hostnames for literal string matching with + * the SNI extension from the user. + */ + new_hosts->sni = lappend(new_hosts->sni, host); + } + } + } /* - * Call init hook (usually to set password callback) + * If SNI is disabled, then we load configuration from postgresql.conf. If + * SNI is enabled but the pg_hosts.conf file doesn't exist, or is empty, + * then we also load the config from postgresql.conf. */ - (*openssl_tls_init_hook) (context, isServerStart); + else if (res == HOSTSFILE_DISABLED || res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING) + { + HostsLine *pgconf = palloc0(sizeof(HostsLine)); - /* used by the callback */ - ssl_is_server_start = isServerStart; +#ifdef USE_ASSERT_CHECKING + if (res == HOSTSFILE_DISABLED) + Assert(ssl_sni == false); +#endif + + pgconf->ssl_cert = ssl_cert_file; + pgconf->ssl_key = ssl_key_file; + pgconf->ssl_ca = ssl_ca_file; + pgconf->ssl_passphrase_cmd = ssl_passphrase_command; + pgconf->ssl_passphrase_reload = ssl_passphrase_command_supports_reload; + + if (!init_host_context(pgconf, isServerStart)) + goto error; + + /* + * If postgresql.conf is used to configure SSL then by definition it + * will be the default context as we don't have per-host config. + */ + new_hosts->default_host = pgconf; + } /* - * Load and verify server's certificate and private key + * Make sure we have at least one configuration loaded to use, without + * that we cannot drive a connection so exit. */ - if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1) + if (new_hosts->sni == NIL && !new_hosts->default_host && !new_hosts->no_sni) { ereport(isServerStart ? FATAL : LOG, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not load server certificate file \"%s\": %s", - ssl_cert_file, SSLerrmessage(ERR_get_error())))); + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("no SSL configurations loaded"), + /*- translator: The two %s contain filenames */ + errhint("If ssl_sni is enabled then add configuration to \"%s\", else \"%s\"", + "pg_hosts.conf", "postgresql.conf")); goto error; } - if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart)) - goto error; +#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB /* - * OK, try to load the private key file. + * Create a new SSL context into which we'll load all the configuration + * settings. If we fail partway through, we can avoid memory leakage by + * freeing this context; we don't install it as active until the end. + * + * We use SSLv23_method() because it can negotiate use of the highest + * mutually supported protocol version, while alternatives like + * TLSv1_2_method() permit only one specific version. Note that we don't + * actually allow SSL v2 or v3, only TLS protocols (see below). */ - dummy_ssl_passwd_cb_called = false; - - if (SSL_CTX_use_PrivateKey_file(context, - ssl_key_file, - SSL_FILETYPE_PEM) != 1) - { - if (dummy_ssl_passwd_cb_called) - ereport(isServerStart ? FATAL : LOG, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", - ssl_key_file))); - else - ereport(isServerStart ? FATAL : LOG, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not load private key file \"%s\": %s", - ssl_key_file, SSLerrmessage(ERR_get_error())))); - goto error; - } - - if (SSL_CTX_check_private_key(context) != 1) + context = SSL_CTX_new(SSLv23_method()); + if (!context) { ereport(isServerStart ? FATAL : LOG, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("check of private key failed: %s", + (errmsg("could not create SSL context: %s", SSLerrmessage(ERR_get_error())))); goto error; } +#else + + /* + * If the client hello callback isn't supported we want to use the default + * context as the one to drive the handshake so avoid creating a new one + * and use the already existing default one instead. + */ + context = new_hosts->default_host->ssl_ctx; + + /* + * Since we don't allocate a new SSL_CTX here like we do when SNI has been + * enabled we need to bump the reference count on context to avoid double + * free of the context when using the same cleanup logic across the cases. + */ + SSL_CTX_up_ref(context); +#endif + + /* + * Disable OpenSSL's moving-write-buffer sanity check, because it causes + * unnecessary failures in nonblocking send cases. + */ + SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); if (ssl_min_protocol_version) { @@ -316,20 +550,207 @@ be_tls_init(bool isServerStart) if (SSLPreferServerCiphers) SSL_CTX_set_options(context, SSL_OP_CIPHER_SERVER_PREFERENCE); + /* + * Success! Replace any existing SSL_context and host configurations. + */ + if (SSL_context) + { + SSL_CTX_free(SSL_context); + SSL_context = NULL; + } + + MemoryContextSwitchTo(oldcxt); + + if (SSL_hosts_memcxt) + MemoryContextDelete(SSL_hosts_memcxt); + + SSL_hosts_memcxt = host_memcxt; + SSL_hosts = new_hosts; + SSL_context = context; + + return 0; + + /* + * Clean up by releasing working SSL contexts as well as allocations + * performed during parsing. Since all our allocations are done in a + * local memory context all we need to do is delete it. + */ +error: + if (context) + SSL_CTX_free(context); + + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(host_memcxt); + return -1; +} + +/* + * host_context_cleanup_cb + * + * Memory context reset callback for clearing OpenSSL managed resources when + * hosts are reloaded and the previous set of configured hosts are freed. As + * all hosts are allocated in a single context we don't need to free each host + * individually, just resources managed by OpenSSL. + */ +static void +host_context_cleanup_cb(void *arg) +{ + struct hosts *hosts = arg; + + foreach_ptr(HostsLine, host, hosts->sni) + { + if (host->ssl_ctx != NULL) + SSL_CTX_free(host->ssl_ctx); + } + + if (hosts->no_sni && hosts->no_sni->ssl_ctx) + SSL_CTX_free(hosts->no_sni->ssl_ctx); + + if (hosts->default_host && hosts->default_host->ssl_ctx) + SSL_CTX_free(hosts->default_host->ssl_ctx); +} + +static bool +init_host_context(HostsLine *host, bool isServerStart) +{ + SSL_CTX *ctx = SSL_CTX_new(SSLv23_method()); + static bool init_warned = false; + + if (!ctx) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("could not create SSL context: %s", + SSLerrmessage(ERR_get_error())))); + goto error; + } + + /* + * Call init hook (usually to set password callback) in case SNI hasn't + * been enabled. If SNI is enabled the hook won't operate on the actual + * TLS context used so it cannot function properly; we warn if one has + * been installed. + * + * If SNI is enabled, we set password callback based what was configured. + */ + if (!ssl_sni) + (*openssl_tls_init_hook) (ctx, isServerStart); + else + { + if (openssl_tls_init_hook != default_openssl_tls_init && !init_warned) + { + ereport(WARNING, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("SNI is enabled; installed TLS init hook will be ignored"), + /*- translator: first %s is a GUC, second %s contains a filename */ + errhint("TLS init hooks are incompatible with SNI. " + "Set \"%s\" to \"off\" to make use of the hook " + "that is currently installed, or remove the hook " + "and use per-host passphrase commands in \"%s\".", + "ssl_sni", "pg_hosts.conf")); + init_warned = true; + } + + /* + * Set up the password callback, if configured. + */ + if (isServerStart) + { + if (host->ssl_passphrase_cmd && host->ssl_passphrase_cmd[0]) + { + SSL_CTX_set_default_passwd_cb(ctx, ssl_external_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(ctx, host->ssl_passphrase_cmd); + } + } + else + { + /* + * If ssl_passphrase_reload is true then ssl_passphrase_cmd cannot + * be NULL due to their parsing order, but just in case and to + * self-document the code we replicate the nullness checks. + */ + if (host->ssl_passphrase_reload && + (host->ssl_passphrase_cmd && host->ssl_passphrase_cmd[0])) + { + SSL_CTX_set_default_passwd_cb(ctx, ssl_external_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(ctx, host->ssl_passphrase_cmd); + } + else + { + /* + * If reloading and no external command is configured, + * override OpenSSL's default handling of passphrase-protected + * files, because we don't want to prompt for a passphrase in + * an already-running server. + */ + SSL_CTX_set_default_passwd_cb(ctx, dummy_ssl_passwd_cb); + } + } + } + + /* + * Load and verify server's certificate and private key + */ + if (SSL_CTX_use_certificate_chain_file(ctx, host->ssl_cert) != 1) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load server certificate file \"%s\": %s", + host->ssl_cert, SSLerrmessage(ERR_get_error())))); + goto error; + } + + if (!check_ssl_key_file_permissions(host->ssl_key, isServerStart)) + goto error; + + + /* used by the callback */ + ssl_is_server_start = isServerStart; + + /* + * OK, try to load the private key file. + */ + dummy_ssl_passwd_cb_called = false; + + if (SSL_CTX_use_PrivateKey_file(ctx, + host->ssl_key, + SSL_FILETYPE_PEM) != 1) + { + if (dummy_ssl_passwd_cb_called) + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", + host->ssl_key))); + else + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load private key file \"%s\": %s", + host->ssl_key, SSLerrmessage(ERR_get_error())))); + goto error; + } + + if (SSL_CTX_check_private_key(ctx) != 1) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("check of private key failed: %s", + SSLerrmessage(ERR_get_error())))); + goto error; + } + /* * Load CA store, so we can verify client certificates if needed. */ - if (ssl_ca_file[0]) + if (host->ssl_ca && host->ssl_ca[0]) { STACK_OF(X509_NAME) * root_cert_list; - if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 || - (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL) + if (SSL_CTX_load_verify_locations(ctx, host->ssl_ca, NULL) != 1 || + (root_cert_list = SSL_load_client_CA_file(host->ssl_ca)) == NULL) { ereport(isServerStart ? FATAL : LOG, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("could not load root certificate file \"%s\": %s", - ssl_ca_file, SSLerrmessage(ERR_get_error())))); + host->ssl_ca, SSLerrmessage(ERR_get_error())))); goto error; } @@ -340,17 +761,7 @@ be_tls_init(bool isServerStart) * that the SSL context will "own" the root_cert_list and remember to * free it when no longer needed. */ - SSL_CTX_set_client_CA_list(context, root_cert_list); - - /* - * Always ask for SSL client cert, but don't fail if it's not - * presented. We might fail such connections later, depending on what - * we find in pg_hba.conf. - */ - SSL_CTX_set_verify(context, - (SSL_VERIFY_PEER | - SSL_VERIFY_CLIENT_ONCE), - verify_cb); + SSL_CTX_set_client_CA_list(ctx, root_cert_list); } /*---------- @@ -360,7 +771,7 @@ be_tls_init(bool isServerStart) */ if (ssl_crl_file[0] || ssl_crl_dir[0]) { - X509_STORE *cvstore = SSL_CTX_get_cert_store(context); + X509_STORE *cvstore = SSL_CTX_get_cert_store(ctx); if (cvstore) { @@ -401,29 +812,13 @@ be_tls_init(bool isServerStart) } } - /* - * Success! Replace any existing SSL_context. - */ - if (SSL_context) - SSL_CTX_free(SSL_context); - - SSL_context = context; - - /* - * Set flag to remember whether CA store has been loaded into SSL_context. - */ - if (ssl_ca_file[0]) - ssl_loaded_verify_locations = true; - else - ssl_loaded_verify_locations = false; - - return 0; + host->ssl_ctx = ctx; + return true; - /* Clean up by releasing working context. */ error: - if (context) - SSL_CTX_free(context); - return -1; + if (ctx) + SSL_CTX_free(ctx); + return false; } void @@ -443,6 +838,7 @@ be_tls_open_server(Port *port) int waitfor; unsigned long ecode; bool give_proto_hint; + static struct CallbackErr err_context; Assert(!port->ssl); Assert(!port->peer); @@ -477,6 +873,42 @@ be_tls_open_server(Port *port) SSLerrmessage(ERR_get_error())))); return -1; } + + /* + * If the underlying TLS library supports the client hello callback we use + * that in order to support host based configuration using the SNI TLS + * extension. If the user has disabled SNI via the ssl_sni GUC we still + * make use of the callback in order to have consistent handling of + * OpenSSL contexts, except in that case the callback will install the + * default configuration regardless of the hostname sent by the user in + * the handshake. + * + * In case the TLS library does not support the client hello callback, as + * of this writing LibreSSL does not, we need to install the client cert + * verification callback here (if the user configured a CA) since we + * cannot use the OpenSSL context update functionality. + */ +#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + SSL_CTX_set_client_hello_cb(SSL_context, sni_clienthello_cb, NULL); +#else + if (SSL_hosts->default_host->ssl_ca && SSL_hosts->default_host->ssl_ca[0]) + { + /* + * Always ask for SSL client cert, but don't fail if it's not + * presented. We might fail such connections later, depending on what + * we find in pg_hba.conf. + */ + SSL_set_verify(port->ssl, + (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE), + verify_cb); + + ssl_loaded_verify_locations = true; + } +#endif + + err_context.cert_errdetail = NULL; + SSL_set_ex_data(port->ssl, 0, &err_context); + port->ssl_in_use = true; aloop: @@ -576,7 +1008,7 @@ be_tls_open_server(Port *port) (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("could not accept SSL connection: %s", SSLerrmessage(ecode)), - cert_errdetail ? errdetail_internal("%s", cert_errdetail) : 0, + err_context.cert_errdetail ? errdetail_internal("%s", err_context.cert_errdetail) : 0, give_proto_hint ? errhint("This may indicate that the client does not support any SSL protocol version between %s and %s.", ssl_min_protocol_version ? @@ -585,7 +1017,8 @@ be_tls_open_server(Port *port) ssl_max_protocol_version ? ssl_protocol_version_to_string(ssl_max_protocol_version) : MAX_OPENSSL_TLS_VERSION) : 0)); - cert_errdetail = NULL; + if (err_context.cert_errdetail) + pfree(err_context.cert_errdetail); break; case SSL_ERROR_ZERO_RETURN: ereport(COMMERROR, @@ -1129,10 +1562,11 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata) { /* same prompt as OpenSSL uses internally */ const char *prompt = "Enter PEM pass phrase:"; + const char *cmd = userdata; Assert(rwflag == 0); - return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size); + return run_ssl_passphrase_command(cmd, prompt, ssl_is_server_start, buf, size); } /* @@ -1209,6 +1643,8 @@ verify_cb(int ok, X509_STORE_CTX *ctx) const char *errstring; StringInfoData str; X509 *cert; + SSL *ssl; + struct CallbackErr *cb_err; if (ok) { @@ -1221,6 +1657,13 @@ verify_cb(int ok, X509_STORE_CTX *ctx) errcode = X509_STORE_CTX_get_error(ctx); errstring = X509_verify_cert_error_string(errcode); + /* + * Extract the current SSL and CallbackErr object to use for passing error + * detail back from the callback. + */ + ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + cb_err = (struct CallbackErr *) SSL_get_ex_data(ssl, 0); + initStringInfo(&str); appendStringInfo(&str, _("Client certificate verification failed at depth %d: %s."), @@ -1271,7 +1714,7 @@ verify_cb(int ok, X509_STORE_CTX *ctx) } /* Store our detail message to be logged later. */ - cert_errdetail = str.data; + cb_err->cert_errdetail = str.data; return ok; } @@ -1369,6 +1812,254 @@ alpn_cb(SSL *ssl, } } +#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB +/* + * ssl_update_ssl + * + * Replace certificate/key and CA in an SSL object to match the, via the SNI + * extension, selected host configuration for the connection. The SSL_CTX + * object to use should be passed in as ctx. This function will update the + * SSL object in-place. + */ +static bool +ssl_update_ssl(SSL *ssl, HostsLine *host_config) +{ + SSL_CTX *ctx = host_config->ssl_ctx; + + X509 *cert; + EVP_PKEY *key; + + STACK_OF(X509) * chain; + + Assert(ctx != NULL); + /*- + * Make use of the already-loaded certificate chain and key. At first + * glance, SSL_set_SSL_CTX() looks like the easiest way to do this, but + * beware -- it has very odd behavior: + * + * https://github.com/openssl/openssl/issues/6109 + */ + cert = SSL_CTX_get0_certificate(ctx); + key = SSL_CTX_get0_privatekey(ctx); + + Assert(cert && key); + + if (!SSL_CTX_get0_chain_certs(ctx, &chain) + || !SSL_use_cert_and_key(ssl, cert, key, chain, 1 /* override */ ) + || !SSL_check_private_key(ssl)) + { + /* + * This shouldn't really be possible, since the inputs came from a + * SSL_CTX that was already populated by OpenSSL. + */ + ereport(COMMERROR, + errcode(ERRCODE_INTERNAL_ERROR), + errmsg_internal("could not update certificate chain: %s", + SSLerrmessage(ERR_get_error()))); + return false; + } + + if (host_config->ssl_ca && host_config->ssl_ca[0]) + { + /* + * Copy the trust store and list of roots over from the SSL_CTX. + */ + X509_STORE *ca_store = SSL_CTX_get_cert_store(ctx); + + STACK_OF(X509_NAME) * roots; + + /* + * The trust store appears to be the only setting that this function + * can't override via the (SSL *) pointer directly. Instead, share it + * with the active SSL_CTX (this should always be SSL_context). + */ + Assert(SSL_context == SSL_get_SSL_CTX(ssl)); + SSL_CTX_set1_cert_store(SSL_context, ca_store); + + /* + * SSL_set_client_CA_list() will take ownership of its argument, so we + * need to duplicate it. + */ + if ((roots = SSL_CTX_get_client_CA_list(ctx)) == NULL + || (roots = SSL_dup_CA_list(roots)) == NULL) + { + ereport(COMMERROR, + errcode(ERRCODE_INTERNAL_ERROR), + errmsg_internal("could not duplicate SSL_CTX CA list: %s", + SSLerrmessage(ERR_get_error()))); + return false; + } + + SSL_set_client_CA_list(ssl, roots); + + /* + * Always ask for SSL client cert, but don't fail if it's not + * presented. We might fail such connections later, depending on what + * we find in pg_hba.conf. + */ + SSL_set_verify(ssl, + (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE), + verify_cb); + + ssl_loaded_verify_locations = true; + } + + return true; +} + +/* + * sni_clienthello_cb + * + * Callback for extracting the servername extension from the TLS handshake + * during ClientHello. There is a callback in OpenSSL for the servername + * specifically but OpenSSL themselves advice against using it as it is more + * dependent on ordering for execution. + */ +static int +sni_clienthello_cb(SSL *ssl, int *al, void *arg) +{ + const char *tlsext_hostname; + const unsigned char *tlsext; + size_t left, + len; + HostsLine *install_config = NULL; + + if (!ssl_sni) + { + install_config = SSL_hosts->default_host; + goto found; + } + + if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &tlsext, &left)) + { + if (left <= 2) + { + *al = SSL_AD_DECODE_ERROR; + return 0; + } + len = (*(tlsext++) << 8); + len += *(tlsext)++; + if (len + 2 != left) + { + *al = SSL_AD_DECODE_ERROR; + return 0; + } + + left = len; + + if (left == 0 || *tlsext++ != TLSEXT_NAMETYPE_host_name) + { + *al = SSL_AD_DECODE_ERROR; + return 0; + } + + left--; + + /* + * Now we can finally pull out the byte array with the actual + * hostname. + */ + if (left <= 2) + { + *al = SSL_AD_DECODE_ERROR; + return 0; + } + len = (*(tlsext++) << 8); + len += *(tlsext++); + if (len + 2 > left) + { + *al = SSL_AD_DECODE_ERROR; + return 0; + } + left = len; + tlsext_hostname = (const char *) tlsext; + + /* + * We have a requested hostname from the client, match against all + * entries in the pg_hosts configuration and attempt to find a match. + * Matching is done case insensitive as per RFC 952 and RFC 921. + */ + foreach_ptr(HostsLine, host, SSL_hosts->sni) + { + foreach_ptr(char, hostname, host->hostnames) + { + if (strlen(hostname) == len && + pg_strncasecmp(hostname, tlsext_hostname, len) == 0) + { + install_config = host; + goto found; + } + } + } + + /* + * If no host specific match was found, and there is a default config, + * then fall back to using that. + */ + if (!install_config && SSL_hosts->default_host) + install_config = SSL_hosts->default_host; + } + + /* + * No hostname TLS extension in the handshake, use the default or no_sni + * configurations if available. + */ + else + { + tlsext_hostname = NULL; + + if (SSL_hosts->no_sni) + install_config = SSL_hosts->no_sni; + else if (SSL_hosts->default_host) + install_config = SSL_hosts->default_host; + else + { + /* + * Reaching here means that we didn't get a hostname in the TLS + * extension and the server has been configured to not allow any + * connections without a specified hostname. + * + * The error message for a missing server_name should, according + * to RFC 8446, be missing_extension. This isn't entirely ideal + * since the user won't be able to tell which extension the server + * considered missing. Sending unrecognized_name would be a more + * helpful error, but for now we stick to the RFC. + */ + *al = SSL_AD_MISSING_EXTENSION; + + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("no hostname provided in callback, and no fallback configured"))); + return SSL_CLIENT_HELLO_ERROR; + } + } + + /* + * If we reach here without a context chosen as the session context then + * fail the handshake and terminate the connection. + */ + if (install_config == NULL) + { + if (tlsext_hostname) + *al = SSL_AD_UNRECOGNIZED_NAME; + else + *al = SSL_AD_MISSING_EXTENSION; + return SSL_CLIENT_HELLO_ERROR; + } + +found: + if (!ssl_update_ssl(ssl, install_config)) + { + *al = SSL_AD_INTERNAL_ERROR; + ereport(COMMERROR, + errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("failed to switch to SSL configuration for host, terminating connection")); + return SSL_CLIENT_HELLO_ERROR; + } + + return SSL_CLIENT_HELLO_SUCCESS; +} +#endif /* HAVE_SSL_CTX_SET_CLIENT_HELLO_CB */ /* * Set DH parameters for generating ephemeral DH keys. The @@ -1436,10 +2127,10 @@ initialize_ecdh(SSL_CTX *context, bool isServerStart) */ ereport(isServerStart ? FATAL : LOG, errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("failed to set group names specified in ssl_groups: %s", + errmsg("could not set group names specified in ssl_groups: %s", SSLerrmessageExt(ERR_get_error(), _("No valid groups found"))), - errhint("Ensure that each group name is spelled correctly and supported by the installed version of OpenSSL")); + errhint("Ensure that each group name is spelled correctly and supported by the installed version of OpenSSL.")); return false; } #endif @@ -1769,6 +2460,20 @@ ssl_protocol_version_to_string(int v) return "(unrecognized)"; } +static uint32 +host_cache_pointer(const char *key) +{ + uint32 hash; + char *lkey = pstrdup(key); + int len = strlen(key); + + for (int i = 0; i < len; i++) + lkey[i] = pg_tolower(lkey[i]); + + hash = string_hash((const void *) lkey, len); + pfree(lkey); + return hash; +} static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart) @@ -1776,12 +2481,18 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart) if (isServerStart) { if (ssl_passphrase_command[0]) + { SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(context, ssl_passphrase_command); + } } else { if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload) + { SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(context, ssl_passphrase_command); + } else /* diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index d723e74e8137b..617704bb99338 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -6,7 +6,7 @@ * message integrity and endpoint authentication. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,6 +29,7 @@ #include "libpq/libpq.h" #include "miscadmin.h" +#include "storage/latch.h" #include "tcop/tcopprot.h" #include "utils/injection_point.h" #include "utils/wait_event.h" @@ -60,6 +61,9 @@ bool SSLPreferServerCiphers; int ssl_min_protocol_version = PG_TLS1_2_VERSION; int ssl_max_protocol_version = PG_TLS_ANY; +/* GUC variable: if false, discards hostname extensions in handshake */ +bool ssl_sni = false; + /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ /* ------------------------------------------------------------ */ diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index f6b641e726ec3..37ccec355c79c 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -4,7 +4,7 @@ * Functions for dealing with encrypted passwords stored in * pg_authid.rolpassword. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/crypt.c @@ -20,10 +20,15 @@ #include "common/scram-common.h" #include "libpq/crypt.h" #include "libpq/scram.h" +#include "miscadmin.h" #include "utils/builtins.h" +#include "utils/memutils.h" #include "utils/syscache.h" #include "utils/timestamp.h" +/* Threshold for password expiration warnings. */ +int password_expiration_warning_threshold = 604800; + /* Enables deprecation warnings for MD5 passwords. */ bool md5_password_warnings = true; @@ -71,13 +76,71 @@ get_role_password(const char *role, const char **logdetail) ReleaseSysCache(roleTup); /* - * Password OK, but check to be sure we are not past rolvaliduntil + * Password OK, but check to be sure we are not past rolvaliduntil or + * password_expiration_warning_threshold. */ - if (!isnull && vuntil < GetCurrentTimestamp()) + if (!isnull) { - *logdetail = psprintf(_("User \"%s\" has an expired password."), - role); - return NULL; + TimestampTz now = GetCurrentTimestamp(); + uint64 expire_time = TimestampDifferenceMicroseconds(now, vuntil); + + /* + * If we're past rolvaliduntil, the connection attempt should fail, so + * update logdetail and return NULL. + */ + if (vuntil < now) + { + *logdetail = psprintf(_("User \"%s\" has an expired password."), + role); + return NULL; + } + + /* + * If we're past the warning threshold, the connection attempt should + * succeed, but we still want to emit a warning. To do so, we queue + * the warning message using StoreConnectionWarning() so that it will + * be emitted at the end of InitPostgres(), and we return normally. + */ + if (expire_time / USECS_PER_SEC < password_expiration_warning_threshold) + { + MemoryContext oldcontext; + int days; + int hours; + int minutes; + char *warning; + char *detail; + + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + + days = expire_time / USECS_PER_DAY; + hours = (expire_time % USECS_PER_DAY) / USECS_PER_HOUR; + minutes = (expire_time % USECS_PER_HOUR) / USECS_PER_MINUTE; + + warning = pstrdup(_("role password will expire soon")); + + if (days > 0) + detail = psprintf(ngettext("The password for role \"%s\" will expire in %d day.", + "The password for role \"%s\" will expire in %d days.", + days), + role, days); + else if (hours > 0) + detail = psprintf(ngettext("The password for role \"%s\" will expire in %d hour.", + "The password for role \"%s\" will expire in %d hours.", + hours), + role, hours); + else if (minutes > 0) + detail = psprintf(ngettext("The password for role \"%s\" will expire in %d minute.", + "The password for role \"%s\" will expire in %d minutes.", + minutes), + role, minutes); + else + detail = psprintf(_("The password for role \"%s\" will expire in less than 1 minute."), + role); + + StoreConnectionWarning(warning, detail); + + MemoryContextSwitchTo(oldcontext); + } } return shadow_pass; @@ -136,7 +199,7 @@ encrypt_password(PasswordType target_type, const char *role, case PASSWORD_TYPE_MD5: encrypted_password = palloc(MD5_PASSWD_LEN + 1); - if (!pg_md5_encrypt(password, (uint8 *) role, strlen(role), + if (!pg_md5_encrypt(password, (const uint8 *) role, strlen(role), encrypted_password, &errstr)) elog(ERROR, "password encryption failed: %s", errstr); break; @@ -231,7 +294,24 @@ md5_crypt_verify(const char *role, const char *shadow_pass, } if (strcmp(client_pass, crypt_pwd) == 0) + { retval = STATUS_OK; + + if (md5_password_warnings) + { + MemoryContext oldcontext; + char *warning; + char *detail; + + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + + warning = pstrdup(_("authenticated with an MD5-encrypted password")); + detail = pstrdup(_("MD5 password support is deprecated and will be removed in a future release of PostgreSQL.")); + StoreConnectionWarning(warning, detail); + + MemoryContextSwitchTo(oldcontext); + } + } else { *logdetail = psprintf(_("Password does not match for user \"%s\"."), @@ -284,7 +364,7 @@ plain_crypt_verify(const char *role, const char *shadow_pass, case PASSWORD_TYPE_MD5: if (!pg_md5_encrypt(client_pass, - (uint8 *) role, + (const uint8 *) role, strlen(role), crypt_client_pass, &errstr)) diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 332fad278351c..d47eab2cba0c0 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -5,7 +5,7 @@ * wherein you authenticate a user by seeing what IP address the system * says he comes from and choosing authentication method based on it). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -114,7 +114,6 @@ static const char *const UserAuthName[] = "bsd", "ldap", "cert", - "radius", "peer", "oauth", }; @@ -138,14 +137,11 @@ static int regexec_auth_token(const char *match, AuthToken *token, static void tokenize_error_callback(void *arg); -/* - * isblank() exists in the ISO C99 spec, but it's not very portable yet, - * so provide our own version. - */ -bool +static bool pg_isblank(const char c) { - return c == ' ' || c == '\t' || c == '\r'; + /* don't accept non-ASCII data */ + return (!IS_HIGHBIT_SET(c) && isblank(c)); } @@ -312,7 +308,7 @@ regcomp_auth_token(AuthToken *token, char *filename, int line_num, if (token->string[0] != '/') return 0; /* nothing to compile */ - token->regex = (regex_t *) palloc0(sizeof(regex_t)); + token->regex = palloc0_object(regex_t); wstr = palloc((strlen(token->string + 1) + 1) * sizeof(pg_wchar)); wlen = pg_mb2wchar_with_len(token->string + 1, wstr, strlen(token->string + 1)); @@ -894,7 +890,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, * to this list. */ oldcxt = MemoryContextSwitchTo(tokenize_context); - tok_line = (TokenizedAuthLine *) palloc0(sizeof(TokenizedAuthLine)); + tok_line = palloc0_object(TokenizedAuthLine); tok_line->fields = current_line; tok_line->file_name = pstrdup(filename); tok_line->line_num = line_number; @@ -1075,7 +1071,7 @@ hostname_match(const char *pattern, const char *actual_hostname) * Check to see if a connecting IP matches a given host name. */ static bool -check_hostname(hbaPort *port, const char *hostname) +check_hostname(Port *port, const char *hostname) { struct addrinfo *gai_result, *gai; @@ -1342,7 +1338,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) AuthToken *token; HbaLine *parsedline; - parsedline = palloc0(sizeof(HbaLine)); + parsedline = palloc0_object(HbaLine); parsedline->sourcefile = pstrdup(file_name); parsedline->linenumber = line_num; parsedline->rawline = pstrdup(tok_line->raw_line); @@ -1747,8 +1743,6 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) #else unsupauth = "cert"; #endif - else if (strcmp(token->string, "radius") == 0) - parsedline->auth_method = uaRADIUS; else if (strcmp(token->string, "oauth") == 0) parsedline->auth_method = uaOAuth; else @@ -1950,87 +1944,6 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) } } - if (parsedline->auth_method == uaRADIUS) - { - MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius"); - MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius"); - - if (parsedline->radiusservers == NIL) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("list of RADIUS servers cannot be empty"), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - *err_msg = "list of RADIUS servers cannot be empty"; - return NULL; - } - - if (parsedline->radiussecrets == NIL) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("list of RADIUS secrets cannot be empty"), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - *err_msg = "list of RADIUS secrets cannot be empty"; - return NULL; - } - - /* - * Verify length of option lists - each can be 0 (except for secrets, - * but that's already checked above), 1 (use the same value - * everywhere) or the same as the number of servers. - */ - if (!(list_length(parsedline->radiussecrets) == 1 || - list_length(parsedline->radiussecrets) == list_length(parsedline->radiusservers))) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)", - list_length(parsedline->radiussecrets), - list_length(parsedline->radiusservers)), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - *err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)", - list_length(parsedline->radiussecrets), - list_length(parsedline->radiusservers)); - return NULL; - } - if (!(list_length(parsedline->radiusports) == 0 || - list_length(parsedline->radiusports) == 1 || - list_length(parsedline->radiusports) == list_length(parsedline->radiusservers))) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)", - list_length(parsedline->radiusports), - list_length(parsedline->radiusservers)), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - *err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)", - list_length(parsedline->radiusports), - list_length(parsedline->radiusservers)); - return NULL; - } - if (!(list_length(parsedline->radiusidentifiers) == 0 || - list_length(parsedline->radiusidentifiers) == 1 || - list_length(parsedline->radiusidentifiers) == list_length(parsedline->radiusservers))) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)", - list_length(parsedline->radiusidentifiers), - list_length(parsedline->radiusservers)), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - *err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)", - list_length(parsedline->radiusidentifiers), - list_length(parsedline->radiusservers)); - return NULL; - } - } - /* * Enforce any parameters implied by other settings. */ @@ -2353,152 +2266,46 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, else hbaline->upn_username = false; } - else if (strcmp(name, "radiusservers") == 0) + else if (strcmp(name, "issuer") == 0) { - struct addrinfo *gai_result; - struct addrinfo hints; - int ret; - List *parsed_servers; - ListCell *l; - char *dupval = pstrdup(val); - - REQUIRE_AUTH_OPTION(uaRADIUS, "radiusservers", "radius"); - - if (!SplitGUCList(dupval, ',', &parsed_servers)) - { - /* syntax error in list */ - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not parse RADIUS server list \"%s\"", - val), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - return false; - } - - /* For each entry in the list, translate it */ - foreach(l, parsed_servers) - { - MemSet(&hints, 0, sizeof(hints)); - hints.ai_socktype = SOCK_DGRAM; - hints.ai_family = AF_UNSPEC; - - ret = pg_getaddrinfo_all((char *) lfirst(l), NULL, &hints, &gai_result); - if (ret || !gai_result) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not translate RADIUS server name \"%s\" to address: %s", - (char *) lfirst(l), gai_strerror(ret)), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - if (gai_result) - pg_freeaddrinfo_all(hints.ai_family, gai_result); - - list_free(parsed_servers); - return false; - } - pg_freeaddrinfo_all(hints.ai_family, gai_result); - } - - /* All entries are OK, so store them */ - hbaline->radiusservers = parsed_servers; - hbaline->radiusservers_s = pstrdup(val); + REQUIRE_AUTH_OPTION(uaOAuth, "issuer", "oauth"); + hbaline->oauth_issuer = pstrdup(val); } - else if (strcmp(name, "radiusports") == 0) + else if (strcmp(name, "scope") == 0) { - List *parsed_ports; - ListCell *l; - char *dupval = pstrdup(val); - - REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius"); - - if (!SplitGUCList(dupval, ',', &parsed_ports)) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not parse RADIUS port list \"%s\"", - val), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - *err_msg = psprintf("invalid RADIUS port number: \"%s\"", val); - return false; - } - - foreach(l, parsed_ports) - { - if (atoi(lfirst(l)) == 0) - { - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("invalid RADIUS port number: \"%s\"", val), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - - return false; - } - } - hbaline->radiusports = parsed_ports; - hbaline->radiusports_s = pstrdup(val); + REQUIRE_AUTH_OPTION(uaOAuth, "scope", "oauth"); + hbaline->oauth_scope = pstrdup(val); } - else if (strcmp(name, "radiussecrets") == 0) + else if (strcmp(name, "validator") == 0) { - List *parsed_secrets; - char *dupval = pstrdup(val); - - REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius"); - - if (!SplitGUCList(dupval, ',', &parsed_secrets)) - { - /* syntax error in list */ - ereport(elevel, - (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not parse RADIUS secret list \"%s\"", - val), - errcontext("line %d of configuration file \"%s\"", - line_num, file_name))); - return false; - } - - hbaline->radiussecrets = parsed_secrets; - hbaline->radiussecrets_s = pstrdup(val); + REQUIRE_AUTH_OPTION(uaOAuth, "validator", "oauth"); + hbaline->oauth_validator = pstrdup(val); } - else if (strcmp(name, "radiusidentifiers") == 0) + else if (strncmp(name, "validator.", strlen("validator.")) == 0) { - List *parsed_identifiers; - char *dupval = pstrdup(val); + const char *key = name + strlen("validator."); - REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius"); + REQUIRE_AUTH_OPTION(uaOAuth, name, "oauth"); - if (!SplitGUCList(dupval, ',', &parsed_identifiers)) + /* + * Validator modules may register their own per-HBA-line options. + * Unfortunately, since we don't want to require these modules to be + * loaded into the postmaster, we don't know if the options are valid + * yet and must store them for later. Perform only a basic syntax + * check here. + */ + if (!valid_oauth_hba_option_name(key)) { - /* syntax error in list */ ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not parse RADIUS identifiers list \"%s\"", - val), + errmsg("invalid OAuth validator option name: \"%s\"", name), errcontext("line %d of configuration file \"%s\"", line_num, file_name))); return false; } - hbaline->radiusidentifiers = parsed_identifiers; - hbaline->radiusidentifiers_s = pstrdup(val); - } - else if (strcmp(name, "issuer") == 0) - { - REQUIRE_AUTH_OPTION(uaOAuth, "issuer", "oauth"); - hbaline->oauth_issuer = pstrdup(val); - } - else if (strcmp(name, "scope") == 0) - { - REQUIRE_AUTH_OPTION(uaOAuth, "scope", "oauth"); - hbaline->oauth_scope = pstrdup(val); - } - else if (strcmp(name, "validator") == 0) - { - REQUIRE_AUTH_OPTION(uaOAuth, "validator", "oauth"); - hbaline->oauth_validator = pstrdup(val); + hbaline->oauth_opt_keys = lappend(hbaline->oauth_opt_keys, pstrdup(key)); + hbaline->oauth_opt_vals = lappend(hbaline->oauth_opt_vals, pstrdup(val)); } else if (strcmp(name, "delegate_ident_mapping") == 0) { @@ -2528,7 +2335,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, * request. */ static void -check_hba(hbaPort *port) +check_hba(Port *port) { Oid roleid; ListCell *line; @@ -2625,7 +2432,7 @@ check_hba(hbaPort *port) } /* If no matching entry was found, then implicitly reject. */ - hba = palloc0(sizeof(HbaLine)); + hba = palloc0_object(HbaLine); hba->auth_method = uaImplicitReject; port->hba = hba; } @@ -2761,7 +2568,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel) Assert(tok_line->fields != NIL); field = list_head(tok_line->fields); - parsedline = palloc0(sizeof(IdentLine)); + parsedline = palloc0_object(IdentLine); parsedline->linenumber = line_num; /* Get the map token (must exist) */ @@ -2873,8 +2680,11 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name, !token_has_regexp(identLine->pg_user) && (ofs = strstr(identLine->pg_user->string, "\\1")) != NULL) { + const char *repl_str; + size_t repl_len; + char *old_pg_user; char *expanded_pg_user; - int offset; + size_t offset; /* substitution of the first argument requested */ if (matches[1].rm_so < 0) @@ -2886,18 +2696,33 @@ check_ident_usermap(IdentLine *identLine, const char *usermap_name, *error_p = true; return; } + repl_str = system_user + matches[1].rm_so; + repl_len = matches[1].rm_eo - matches[1].rm_so; /* - * length: original length minus length of \1 plus length of match - * plus null terminator + * It's allowed to have more than one \1 in the string, and we'll + * replace them all. But that's pretty unusual so we optimize on + * the assumption of only one occurrence, which motivates doing + * repeated replacements instead of making two passes over the + * string to determine the final length right away. */ - expanded_pg_user = palloc0(strlen(identLine->pg_user->string) - 2 + (matches[1].rm_eo - matches[1].rm_so) + 1); - offset = ofs - identLine->pg_user->string; - memcpy(expanded_pg_user, identLine->pg_user->string, offset); - memcpy(expanded_pg_user + offset, - system_user + matches[1].rm_so, - matches[1].rm_eo - matches[1].rm_so); - strcat(expanded_pg_user, ofs + 2); + old_pg_user = identLine->pg_user->string; + do + { + /* + * length: current length minus length of \1 plus length of + * replacement plus null terminator + */ + expanded_pg_user = palloc(strlen(old_pg_user) - 2 + repl_len + 1); + /* ofs points into the old_pg_user string at this point */ + offset = ofs - old_pg_user; + memcpy(expanded_pg_user, old_pg_user, offset); + memcpy(expanded_pg_user + offset, repl_str, repl_len); + strcpy(expanded_pg_user + offset + repl_len, ofs + 2); + if (old_pg_user != identLine->pg_user->string) + pfree(old_pg_user); + old_pg_user = expanded_pg_user; + } while ((ofs = strstr(old_pg_user + offset + repl_len, "\\1")) != NULL); /* * Mark the token as quoted, so it will only be compared literally @@ -3107,7 +2932,7 @@ load_ident(void) * method = uaImplicitReject. */ void -hba_getauthmethod(hbaPort *port) +hba_getauthmethod(Port *port) { check_hba(port); } diff --git a/src/backend/libpq/ifaddr.c b/src/backend/libpq/ifaddr.c index 1303a4543e7a3..5eea00a85b19f 100644 --- a/src/backend/libpq/ifaddr.c +++ b/src/backend/libpq/ifaddr.c @@ -3,7 +3,7 @@ * ifaddr.c * IP netmask calculations, and enumerating network interfaces. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build index 31aa2faae1ecc..8571f65284417 100644 --- a/src/backend/libpq/meson.build +++ b/src/backend/libpq/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'auth-oauth.c', @@ -31,5 +31,6 @@ endif install_data( 'pg_hba.conf.sample', 'pg_ident.conf.sample', + 'pg_hosts.conf.sample', install_dir: dir_data, ) diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample index b64c8dea97c31..2c8db77747966 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -53,8 +53,8 @@ # directly connected to. # # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", -# "gss", "sspi", "ident", "peer", "pam", "oauth", "ldap", "radius" or -# "cert". Note that "password" sends passwords in clear text; "md5" or +# "gss", "sspi", "ident", "peer", "pam", "oauth", "ldap" or "cert". +# Note that "password" sends passwords in clear text; "md5" or # "scram-sha-256" are preferred since they send encrypted passwords. # # OPTIONS are a set of options for the authentication in the format @@ -109,14 +109,14 @@ # TYPE DATABASE USER ADDRESS METHOD -@remove-line-for-nolocal@# "local" is for Unix domain socket connections only -@remove-line-for-nolocal@local all all @authmethodlocal@ +# "local" is for Unix domain socket connections only +local all all @authmethodlocal@ # IPv4 local connections: host all all 127.0.0.1/32 @authmethodhost@ # IPv6 local connections: host all all ::1/128 @authmethodhost@ # Allow replication connections from localhost, by a user with the # replication privilege. -@remove-line-for-nolocal@local replication all @authmethodlocal@ +local replication all @authmethodlocal@ host replication all 127.0.0.1/32 @authmethodhost@ host replication all ::1/128 @authmethodhost@ diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample new file mode 100644 index 0000000000000..a31c49b01f771 --- /dev/null +++ b/src/backend/libpq/pg_hosts.conf.sample @@ -0,0 +1,4 @@ +# PostgreSQL SNI Hostname mappings +# ================================ + +# HOSTNAME SSL CERTIFICATE SSL KEY SSL CA PASSPHRASE COMMAND PASSPHRASE COMMAND RELOAD diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample index f5225f26cdf2c..8ee6c0ba31576 100644 --- a/src/backend/libpq/pg_ident.conf.sample +++ b/src/backend/libpq/pg_ident.conf.sample @@ -13,25 +13,25 @@ # user names to their corresponding PostgreSQL user names. Records # are of the form: # -# MAPNAME SYSTEM-USERNAME PG-USERNAME +# MAPNAME SYSTEM-USERNAME DATABASE-USERNAME # # (The uppercase quantities must be replaced by actual values.) # # MAPNAME is the (otherwise freely chosen) map name that was used in # pg_hba.conf. SYSTEM-USERNAME is the detected user name of the -# client. PG-USERNAME is the requested PostgreSQL user name. The -# existence of a record specifies that SYSTEM-USERNAME may connect as -# PG-USERNAME. +# client. DATABASE-USERNAME is the requested PostgreSQL user name. +# The existence of a record specifies that SYSTEM-USERNAME may connect +# as DATABASE-USERNAME. # -# If SYSTEM-USERNAME starts with a slash (/), it will be treated as a -# regular expression. Optionally this can contain a capture (a -# parenthesized subexpression). The substring matching the capture -# will be substituted for \1 (backslash-one) if present in -# PG-USERNAME. +# If SYSTEM-USERNAME starts with a slash (/), the rest of it will be +# treated as a regular expression. Optionally this can contain a capture +# (a parenthesized subexpression). The substring matching the capture +# will be substituted for \1 (backslash-one) if that appears in +# DATABASE-USERNAME. # -# PG-USERNAME can be "all", a user name, a group name prefixed with "+", or -# a regular expression (if it starts with a slash (/)). If it is a regular -# expression, the substring matching with \1 has no effect. +# DATABASE-USERNAME can be "all", a user name, a group name prefixed with "+", +# or a regular expression (if it starts with a slash (/)). If it is a regular +# expression, no substitution for \1 will occur. # # Multiple maps may be specified in this file and used by pg_hba.conf. # @@ -69,4 +69,4 @@ # Put your actual configuration here # ---------------------------------- -# MAPNAME SYSTEM-USERNAME PG-USERNAME +# MAPNAME SYSTEM-USERNAME DATABASE-USERNAME diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c index e5171467de18d..4a442f22df60b 100644 --- a/src/backend/libpq/pqcomm.c +++ b/src/backend/libpq/pqcomm.c @@ -17,7 +17,7 @@ * the backend's "backend/libpq" is quite separate from "interfaces/libpq". * All that remains is similarities of names to trap the unwary... * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/pqcomm.c @@ -78,6 +78,7 @@ #include "port/pg_bswap.h" #include "postmaster/postmaster.h" #include "storage/ipc.h" +#include "storage/latch.h" #include "utils/guc_hooks.h" #include "utils/memutils.h" @@ -178,7 +179,7 @@ pq_init(ClientSocket *client_sock) int latch_pos PG_USED_FOR_ASSERTS_ONLY; /* allocate the Port struct and copy the ClientSocket contents to it */ - port = palloc0(sizeof(Port)); + port = palloc0_object(Port); port->sock = client_sock->sock; memcpy(&port->raddr.addr, &client_sock->raddr.addr, client_sock->raddr.salen); port->raddr.salen = client_sock->raddr.salen; @@ -454,9 +455,9 @@ ListenServerPort(int family, const char *hostName, unsigned short portNumber, if (strlen(unixSocketPath) >= UNIXSOCK_PATH_BUFLEN) { ereport(LOG, - (errmsg("Unix-domain socket path \"%s\" is too long (maximum %d bytes)", + (errmsg("Unix-domain socket path \"%s\" is too long (maximum %zu bytes)", unixSocketPath, - (int) (UNIXSOCK_PATH_BUFLEN - 1)))); + (UNIXSOCK_PATH_BUFLEN - 1)))); return STATUS_ERROR; } if (Lock_AF_UNIX(unixSocketDir, unixSocketPath) != STATUS_OK) @@ -618,10 +619,10 @@ ListenServerPort(int family, const char *hostName, unsigned short portNumber, saved_errno == EADDRINUSE ? (addr->ai_family == AF_UNIX ? errhint("Is another postmaster already running on port %d?", - (int) portNumber) : + portNumber) : errhint("Is another postmaster already running on port %d?" " If not, wait a few seconds and retry.", - (int) portNumber)) : 0)); + portNumber)) : 0)); closesocket(fd); continue; } @@ -662,7 +663,7 @@ ListenServerPort(int family, const char *hostName, unsigned short portNumber, ereport(LOG, /* translator: first %s is IPv4 or IPv6 */ (errmsg("listening on %s address \"%s\", port %d", - familyDesc, addrDesc, (int) portNumber))); + familyDesc, addrDesc, portNumber))); ListenSockets[*NumListenSockets] = fd; (*NumListenSockets)++; @@ -858,7 +859,6 @@ RemoveSocketFiles(void) (void) unlink(sock_path); } /* Since we're about to exit, no need to reclaim storage */ - sock_paths = NIL; } diff --git a/src/backend/libpq/pqformat.c b/src/backend/libpq/pqformat.c index 1cc126772f7c0..ebb09db157a69 100644 --- a/src/backend/libpq/pqformat.c +++ b/src/backend/libpq/pqformat.c @@ -21,7 +21,7 @@ * are different. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/pqformat.c @@ -307,9 +307,8 @@ pq_endmessage(StringInfo buf) * * The data buffer is *not* freed, allowing to reuse the buffer with * pq_beginmessage_reuse. - -------------------------------- + * -------------------------------- */ - void pq_endmessage_reuse(StringInfo buf) { diff --git a/src/backend/libpq/pqmq.c b/src/backend/libpq/pqmq.c index f1a08bc32ca17..21ce180c78ddf 100644 --- a/src/backend/libpq/pqmq.c +++ b/src/backend/libpq/pqmq.c @@ -3,7 +3,7 @@ * pqmq.c * Use the frontend/backend protocol for communication over a shm_mq * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/pqmq.c @@ -14,16 +14,19 @@ #include "postgres.h" #include "access/parallel.h" +#include "commands/repack.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "libpq/pqmq.h" #include "miscadmin.h" #include "pgstat.h" #include "replication/logicalworker.h" +#include "storage/latch.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" +#include "utils/wait_event.h" -static shm_mq_handle *pq_mq_handle; +static shm_mq_handle *pq_mq_handle = NULL; static bool pq_mq_busy = false; static pid_t pq_mq_parallel_leader_pid = 0; static ProcNumber pq_mq_parallel_leader_proc_number = INVALID_PROC_NUMBER; @@ -66,7 +69,11 @@ pq_redirect_to_shm_mq(dsm_segment *seg, shm_mq_handle *mqh) static void pq_cleanup_redirect_to_shm_mq(dsm_segment *seg, Datum arg) { - pq_mq_handle = NULL; + if (pq_mq_handle != NULL) + { + pfree(pq_mq_handle); + pq_mq_handle = NULL; + } whereToSendOutput = DestNone; } @@ -131,8 +138,11 @@ mq_putmessage(char msgtype, const char *s, size_t len) if (pq_mq_busy) { if (pq_mq_handle != NULL) + { shm_mq_detach(pq_mq_handle); - pq_mq_handle = NULL; + pfree(pq_mq_handle); + pq_mq_handle = NULL; + } return EOF; } @@ -152,8 +162,6 @@ mq_putmessage(char msgtype, const char *s, size_t len) iov[1].data = s; iov[1].len = len; - Assert(pq_mq_handle != NULL); - for (;;) { /* @@ -161,6 +169,7 @@ mq_putmessage(char msgtype, const char *s, size_t len) * that the shared memory value is updated before we send the parallel * message signal right after this. */ + Assert(pq_mq_handle != NULL); result = shm_mq_sendv(pq_mq_handle, iov, 2, true, true); if (pq_mq_parallel_leader_pid != 0) @@ -169,6 +178,10 @@ mq_putmessage(char msgtype, const char *s, size_t len) SendProcSignal(pq_mq_parallel_leader_pid, PROCSIG_PARALLEL_APPLY_MESSAGE, pq_mq_parallel_leader_proc_number); + else if (AmRepackWorker()) + SendProcSignal(pq_mq_parallel_leader_pid, + PROCSIG_REPACK_MESSAGE, + pq_mq_parallel_leader_proc_number); else { Assert(IsParallelWorker()); @@ -323,7 +336,7 @@ pq_parse_errornotice(StringInfo msg, ErrorData *edata) edata->funcname = pstrdup(value); break; default: - elog(ERROR, "unrecognized error field code: %d", (int) code); + elog(ERROR, "unrecognized error field code: %d", code); break; } } diff --git a/src/backend/libpq/pqsignal.c b/src/backend/libpq/pqsignal.c index d866307a4dc2e..9d03e3ed7b930 100644 --- a/src/backend/libpq/pqsignal.c +++ b/src/backend/libpq/pqsignal.c @@ -3,7 +3,7 @@ * pqsignal.c * Backend signal(2) support (see also src/port/pqsignal.c) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/main/main.c b/src/backend/main/main.c index 7d63cf94a6b44..7b9b602f3c4b0 100644 --- a/src/backend/main/main.c +++ b/src/backend/main/main.c @@ -9,7 +9,7 @@ * proper FooMain() routine for the incarnation. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -125,13 +125,17 @@ main(int argc, char *argv[]) set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("postgres")); /* - * In the postmaster, absorb the environment values for LC_COLLATE and - * LC_CTYPE. Individual backends will change these later to settings - * taken from pg_database, but the postmaster cannot do that. If we leave - * these set to "C" then message localization might not work well in the - * postmaster. + * Collation is handled by pg_locale.c, and the behavior is dependent on + * the provider. strcoll(), etc., should not be called directly. + */ + init_locale("LC_COLLATE", LC_COLLATE, "C"); + + /* + * In the postmaster, absorb the environment value for LC_CTYPE. + * Individual backends will change it later to pg_database.datctype, but + * the postmaster cannot do that. If we leave it set to "C" then message + * localization might not work well in the postmaster. */ - init_locale("LC_COLLATE", LC_COLLATE, ""); init_locale("LC_CTYPE", LC_CTYPE, ""); /* @@ -482,20 +486,29 @@ check_root(const char *progname) /* * At least on linux, set_ps_display() breaks /proc/$pid/environ. The * sanitizer library uses /proc/$pid/environ to implement getenv() as it wants - * to work independent of libc. When just using undefined and alignment - * sanitizers, the sanitizer library is only initialized when the first error - * occurs, by which time we've often already called set_ps_display(), - * preventing the sanitizer libraries from seeing the options. + * to work independent of libc. Depending on which sanitizers are enabled, + * the sanitizer library may not get initialized until after we've called + * set_ps_display(), preventing the sanitizer from seeing environment-supplied + * options. * * We can work around that by defining __ubsan_default_options, a weak symbol * libsanitizer uses to get defaults from the application, and return * getenv("UBSAN_OPTIONS"). But only if main already was reached, so that we * don't end up relying on a not-yet-working getenv(). * + * On the other hand, with different sanitizers enabled, libsanitizer can + * call this so early that it's not fully initialized itself, resulting in + * recursion and a core dump within libsanitizer. To prevent that, ensure + * that this function is built without any sanitizer callbacks in it. + * * As this function won't get called when not running a sanitizer, it doesn't * seem necessary to only compile it conditionally. */ const char *__ubsan_default_options(void); + +#if __has_attribute(disable_sanitizer_instrumentation) +__attribute__((disable_sanitizer_instrumentation)) +#endif const char * __ubsan_default_options(void) { diff --git a/src/backend/main/meson.build b/src/backend/main/meson.build index bb0c8ff8198ed..da59734e6c976 100644 --- a/src/backend/main/meson.build +++ b/src/backend/main/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group main_file = files('main.c') backend_sources += main_file diff --git a/src/backend/meson.build b/src/backend/meson.build index 2b0db21480470..f737d799c610f 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_build_deps = [backend_code] backend_sources = [] @@ -41,6 +41,12 @@ backend_link_args = [] backend_link_depends = [] +# On Windows also make the backend depend on dbghelp, for backtrace support +if host_system == 'windows' and cc.get_id() == 'msvc' + backend_build_deps += cc.find_library('dbghelp') +endif + + # On windows when compiling with msvc we need to make postgres export all its # symbols so that extension libraries can use them. For that we need to scan # the constituting objects and generate a file specifying all the functions as @@ -91,6 +97,25 @@ if cc.get_id() == 'msvc' # be restricted to b_pch=true. backend_link_with += postgres_lib +elif host_system == 'aix' + # The '.' argument leads mkldexport.sh to emit "#! .", which refers to the + # main executable, allowing extension libraries to resolve their undefined + # symbols to symbols in the postgres binary. + postgres_imp = custom_target('postgres.imp', + command: [files('port/aix/mkldexport.sh'), '@INPUT@', '.'], + input: postgres_lib, + output: 'postgres.imp', + capture: true, + install: true, + install_dir: dir_lib_pkg, + build_by_default: false, + ) + # -Wl,-bE:exportfile indicates these symbols are exported by postgres binary + backend_link_args += '-Wl,-bE:@0@'.format(postgres_imp.full_path()) + backend_link_depends += postgres_imp + install_data( + 'port/aix/mkldexport.sh', + install_dir: dir_pgxs / 'src/backend/port/aix') endif backend_input = [] @@ -141,7 +166,7 @@ postgres = executable('postgres', backend_targets += postgres pg_mod_c_args = cflags_mod -pg_mod_cpp_args = cxxflags_mod +pg_mod_cxx_args = cxxflags_mod pg_mod_link_args = ldflags_sl + ldflags_mod pg_mod_link_depend = [] @@ -169,14 +194,14 @@ backend_mod_code = declare_dependency( compile_args: pg_mod_c_args, include_directories: postgres_inc, link_args: pg_mod_link_args, - sources: generated_headers + generated_backend_headers, + sources: generated_backend_headers_stamp, dependencies: backend_mod_deps, ) # normal extension modules pg_mod_args = default_mod_args + { 'dependencies': [backend_mod_code], - 'cpp_args': pg_mod_cpp_args, + 'cpp_args': pg_mod_cxx_args, 'link_depends': pg_mod_link_depend, } @@ -194,5 +219,6 @@ pg_test_mod_args = pg_mod_args + { subdir('jit/llvm') subdir('replication/libpqwalreceiver') subdir('replication/pgoutput') +subdir('replication/pgrepack') subdir('snowball') subdir('utils/mb/conversion_procs') diff --git a/src/backend/nls.mk b/src/backend/nls.mk index b7d5dd46e4513..698b1083f4bd6 100644 --- a/src/backend/nls.mk +++ b/src/backend/nls.mk @@ -28,7 +28,7 @@ GETTEXT_FLAGS = $(BACKEND_COMMON_GETTEXT_FLAGS) \ error_cb:2:c-format gettext-files: generated-parser-sources generated-headers - find $(srcdir) $(srcdir)/../common $(srcdir)/../port -name '*.c' -print | LC_ALL=C sort >$@ + find $(srcdir) $(srcdir)/../common $(srcdir)/../port $(srcdir)/../include/ \( -name '*.c' -o -name "proctypelist.h" \) -print | LC_ALL=C sort >$@ my-clean: rm -f gettext-files diff --git a/src/backend/nodes/bitmapset.c b/src/backend/nodes/bitmapset.c index bf512cf806ff7..f053d8c4d64ac 100644 --- a/src/backend/nodes/bitmapset.c +++ b/src/backend/nodes/bitmapset.c @@ -29,7 +29,7 @@ * any users of the old set will be accessing pfree'd memory. This option is * only intended to be used for debugging. * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/nodes/bitmapset.c @@ -538,7 +538,6 @@ bms_is_member(int x, const Bitmapset *a) int bms_member_index(Bitmapset *a, int x) { - int i; int bitnum; int wordnum; int result = 0; @@ -554,14 +553,8 @@ bms_member_index(Bitmapset *a, int x) bitnum = BITNUM(x); /* count bits in preceding words */ - for (i = 0; i < wordnum; i++) - { - bitmapword w = a->words[i]; - - /* No need to count the bits in a zero word */ - if (w != 0) - result += bmw_popcount(w); - } + result += pg_popcount((const char *) a->words, + wordnum * sizeof(bitmapword)); /* * Now add bits of the last word, but only those before the item. We can @@ -750,26 +743,17 @@ bms_get_singleton_member(const Bitmapset *a, int *member) int bms_num_members(const Bitmapset *a) { - int result = 0; - int nwords; - int wordnum; - Assert(bms_is_valid_set(a)); if (a == NULL) return 0; - nwords = a->nwords; - wordnum = 0; - do - { - bitmapword w = a->words[wordnum]; + /* fast-path for common case */ + if (a->nwords == 1) + return bmw_popcount(a->words[0]); - /* No need to count the bits in a zero word */ - if (w != 0) - result += bmw_popcount(w); - } while (++wordnum < nwords); - return result; + return pg_popcount((const char *) a->words, + a->nwords * sizeof(bitmapword)); } /* @@ -1305,8 +1289,8 @@ bms_join(Bitmapset *a, Bitmapset *b) int bms_next_member(const Bitmapset *a, int prevbit) { + unsigned int currbit = prevbit; int nwords; - int wordnum; bitmapword mask; Assert(bms_is_valid_set(a)); @@ -1314,13 +1298,15 @@ bms_next_member(const Bitmapset *a, int prevbit) if (a == NULL) return -2; nwords = a->nwords; - prevbit++; - mask = (~(bitmapword) 0) << BITNUM(prevbit); - for (wordnum = WORDNUM(prevbit); wordnum < nwords; wordnum++) + + /* use an unsigned int to avoid the risk that int overflows */ + currbit++; + mask = (~(bitmapword) 0) << BITNUM(currbit); + for (int wordnum = WORDNUM(currbit); wordnum < nwords; wordnum++) { bitmapword w = a->words[wordnum]; - /* ignore bits before prevbit */ + /* ignore bits before currbit */ w &= mask; if (w != 0) @@ -1343,7 +1329,7 @@ bms_next_member(const Bitmapset *a, int prevbit) * * Returns largest member less than "prevbit", or -2 if there is none. * "prevbit" must NOT be more than one above the highest possible bit that can - * be set at the Bitmapset at its current size. + * be set in the Bitmapset at its current size. * * To ease finding the highest set bit for the initial loop, the special * prevbit value of -1 can be passed to have the function find the highest @@ -1362,11 +1348,10 @@ bms_next_member(const Bitmapset *a, int prevbit) * It makes no difference in simple loop usage, but complex iteration logic * might need such an ability. */ - int bms_prev_member(const Bitmapset *a, int prevbit) { - int wordnum; + unsigned int currbit; int ushiftbits; bitmapword mask; @@ -1379,19 +1364,26 @@ bms_prev_member(const Bitmapset *a, int prevbit) if (a == NULL || prevbit == 0) return -2; - /* transform -1 to the highest possible bit we could have set */ - if (prevbit == -1) - prevbit = a->nwords * BITS_PER_BITMAPWORD - 1; + /* Validate callers didn't give us something out of range */ + Assert(prevbit < 0 || prevbit <= (unsigned int) (a->nwords * BITS_PER_BITMAPWORD)); + + /* + * Transform -1 (or any negative number) to the highest possible bit we + * could have set. We do this in unsigned math to avoid the risk of + * overflowing a signed int. + */ + if (prevbit < 0) + currbit = (unsigned int) a->nwords * BITS_PER_BITMAPWORD - 1; else - prevbit--; + currbit = prevbit - 1; - ushiftbits = BITS_PER_BITMAPWORD - (BITNUM(prevbit) + 1); + ushiftbits = BITS_PER_BITMAPWORD - (BITNUM(currbit) + 1); mask = (~(bitmapword) 0) >> ushiftbits; - for (wordnum = WORDNUM(prevbit); wordnum >= 0; wordnum--) + for (int wordnum = WORDNUM(currbit); wordnum >= 0; wordnum--) { bitmapword w = a->words[wordnum]; - /* mask out bits left of prevbit */ + /* mask out bits left of currbit */ w &= mask; if (w != 0) diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 475693b08bc5a..ff22a04abe5a9 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -4,7 +4,7 @@ * Copy functions for Postgres tree nodes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -204,7 +204,7 @@ copyObjectImpl(const void *from) default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(from)); - retval = 0; /* keep compiler quiet */ + retval = NULL; /* keep compiler quiet */ break; } diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index f2598a1b69a32..3d1a1adf86e7e 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -8,7 +8,7 @@ * "x" to be considered equal() to another reference to "x" in the query. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/nodes/extensible.c b/src/backend/nodes/extensible.c index 3ede1ee0f5d64..0d43d66c1cdbd 100644 --- a/src/backend/nodes/extensible.c +++ b/src/backend/nodes/extensible.c @@ -10,7 +10,7 @@ * and GetExtensibleNodeMethods to get information about a previously * registered type of extensible node. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/nodes/gen_node_support.pl b/src/backend/nodes/gen_node_support.pl index 77659b0f76020..f4b1317e99f2f 100644 --- a/src/backend/nodes/gen_node_support.pl +++ b/src/backend/nodes/gen_node_support.pl @@ -8,7 +8,7 @@ # - readfuncs # - outfuncs # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/nodes/gen_node_support.pl @@ -96,20 +96,6 @@ sub elem nodes/supportnodes.h ); -# ARM ABI STABILITY CHECK HERE: -# -# In stable branches, set $last_nodetag to the name of the last node type -# that should receive an auto-generated nodetag number, and $last_nodetag_no -# to its number. (Find these values in the last line of the current -# nodetags.h file.) The script will then complain if those values don't -# match reality, providing a cross-check that we haven't broken ABI by -# adding or removing nodetags. -# In HEAD, these variables should be left undef, since we don't promise -# ABI stability during development. - -my $last_nodetag = undef; -my $last_nodetag_no = undef; - # output file names my @output_files; @@ -135,7 +121,7 @@ sub elem # types that are copied by straight assignment my @scalar_types = qw( - bits32 bool char double int int8 int16 int32 int64 long uint8 uint16 uint32 uint64 + bool char double int int8 int16 int32 int64 long uint8 uint16 uint32 uint64 AclMode AttrNumber Cardinality Cost Index Oid RelFileNumber Selectivity Size StrategyNumber SubTransactionId TimeLineID XLogRecPtr ); @@ -591,7 +577,7 @@ sub elem * %s * Generated node infrastructure code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES @@ -615,30 +601,21 @@ sub elem printf $nt $header_comment, 'nodetags.h'; my $tagno = 0; -my $last_tag = undef; foreach my $n (@node_types, @extra_tags) { next if elem $n, @abstract_types; if (defined $manual_nodetag_number{$n}) { - # do not change $tagno or $last_tag + # do not change $tagno print $nt "\tT_${n} = $manual_nodetag_number{$n},\n"; } else { $tagno++; - $last_tag = $n; print $nt "\tT_${n} = $tagno,\n"; } } -# verify that last auto-assigned nodetag stays stable -die "ABI stability break: last nodetag is $last_tag not $last_nodetag\n" - if (defined $last_nodetag && $last_nodetag ne $last_tag); -die - "ABI stability break: last nodetag number is $tagno not $last_nodetag_no\n" - if (defined $last_nodetag_no && $last_nodetag_no != $tagno); - close $nt; @@ -1031,7 +1008,6 @@ sub elem print $rff "\tREAD_INT_FIELD($f);\n" unless $no_read; } elsif ($t eq 'uint32' - || $t eq 'bits32' || $t eq 'BlockNumber' || $t eq 'Index' || $t eq 'SubTransactionId') @@ -1039,6 +1015,11 @@ sub elem print $off "\tWRITE_UINT_FIELD($f);\n"; print $rff "\tREAD_UINT_FIELD($f);\n" unless $no_read; } + elsif ($t eq 'int64') + { + print $off "\tWRITE_INT64_FIELD($f);\n"; + print $rff "\tREAD_INT64_FIELD($f);\n" unless $no_read; + } elsif ($t eq 'uint64' || $t eq 'AclMode') { @@ -1324,7 +1305,7 @@ sub elem # Node type. Squash constants if requested. if ($query_jumble_squash) { - print $jff "\tJUMBLE_ELEMENTS($f);\n" + print $jff "\tJUMBLE_ELEMENTS($f, node);\n" unless $query_jumble_ignore; } else diff --git a/src/backend/nodes/list.c b/src/backend/nodes/list.c index 1597b8e812756..98fc2b44b5096 100644 --- a/src/backend/nodes/list.c +++ b/src/backend/nodes/list.c @@ -6,7 +6,7 @@ * See comments in pg_list.h. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index e2d9e9be41a65..3cd35c5c457ee 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -4,7 +4,7 @@ * creator functions for various nodes. The functions here are for the * most frequently created nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -984,7 +984,7 @@ makeJsonKeyValue(Node *key, Node *value) */ Node * makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, - bool unique_keys, int location) + bool unique_keys, Oid exprBaseType, int location) { JsonIsPredicate *n = makeNode(JsonIsPredicate); @@ -992,6 +992,7 @@ makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, n->format = format; n->item_type = item_type; n->unique_keys = unique_keys; + n->exprBaseType = exprBaseType; n->location = location; return (Node *) n; diff --git a/src/backend/nodes/meson.build b/src/backend/nodes/meson.build index 9a1c1b7b987bd..bfd46047af814 100644 --- a/src/backend/nodes/meson.build +++ b/src/backend/nodes/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'bitmapset.c', diff --git a/src/backend/nodes/multibitmapset.c b/src/backend/nodes/multibitmapset.c index d88e39c1a89c1..d421508d0338a 100644 --- a/src/backend/nodes/multibitmapset.c +++ b/src/backend/nodes/multibitmapset.c @@ -18,7 +18,7 @@ * a small fraction of that has been built out; we'll add more as needed. * * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/nodes/multibitmapset.c diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1b3..c0b880ec233b2 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3,7 +3,7 @@ * nodeFuncs.c * Various general-purpose manipulations of Node trees * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -284,6 +284,9 @@ exprType(const Node *expr) case T_PlaceHolderVar: type = exprType((Node *) ((const PlaceHolderVar *) expr)->phexpr); break; + case T_GraphPropertyRef: + type = ((const GraphPropertyRef *) expr)->typeId; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); type = InvalidOid; /* keep compiler quiet */ @@ -536,6 +539,8 @@ exprTypmod(const Node *expr) return exprTypmod((Node *) ((const ReturningExpr *) expr)->retexpr); case T_PlaceHolderVar: return exprTypmod((Node *) ((const PlaceHolderVar *) expr)->phexpr); + case T_GraphPropertyRef: + return ((const GraphPropertyRef *) expr)->typmod; default: break; } @@ -1009,14 +1014,14 @@ exprCollation(const Node *expr) break; case T_JsonExpr: { - const JsonExpr *jsexpr = (JsonExpr *) expr; + const JsonExpr *jsexpr = (const JsonExpr *) expr; coll = jsexpr->collation; } break; case T_JsonBehavior: { - const JsonBehavior *behavior = (JsonBehavior *) expr; + const JsonBehavior *behavior = (const JsonBehavior *) expr; if (behavior->expr) coll = exprCollation(behavior->expr); @@ -1058,6 +1063,9 @@ exprCollation(const Node *expr) case T_PlaceHolderVar: coll = exprCollation((Node *) ((const PlaceHolderVar *) expr)->phexpr); break; + case T_GraphPropertyRef: + coll = ((const GraphPropertyRef *) expr)->collation; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); coll = InvalidOid; /* keep compiler quiet */ @@ -1274,12 +1282,8 @@ exprSetCollation(Node *expr, Oid collation) } break; case T_JsonBehavior: - { - JsonBehavior *behavior = (JsonBehavior *) expr; - - if (behavior->expr) - exprSetCollation(behavior->expr, collation); - } + Assert(((JsonBehavior *) expr)->expr == NULL || + exprCollation(((JsonBehavior *) expr)->expr) == collation); break; case T_NullTest: /* NullTest's result is boolean ... */ @@ -1597,7 +1601,7 @@ exprLocation(const Node *expr) } break; case T_JsonBehavior: - loc = exprLocation(((JsonBehavior *) expr)->expr); + loc = exprLocation(((const JsonBehavior *) expr)->expr); break; case T_NullTest: { @@ -1730,6 +1734,9 @@ exprLocation(const Node *expr) case T_ColumnDef: loc = ((const ColumnDef *) expr)->location; break; + case T_IndexElem: + loc = ((const IndexElem *) expr)->location; + break; case T_Constraint: loc = ((const Constraint *) expr)->location; break; @@ -2128,6 +2135,7 @@ expression_tree_walker_impl(Node *node, case T_RangeTblRef: case T_SortGroupClause: case T_CTESearchClause: + case T_GraphPropertyRef: case T_MergeSupportFunc: /* primitive node types with no expression subnodes */ break; @@ -2571,6 +2579,20 @@ expression_tree_walker_impl(Node *node, return true; } break; + case T_ForPortionOfExpr: + { + ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node; + + if (WALK(forPortionOf->targetFrom)) + return true; + if (WALK(forPortionOf->targetTo)) + return true; + if (WALK(forPortionOf->targetRange)) + return true; + if (WALK(forPortionOf->overlapsExpr)) + return true; + } + break; case T_PartitionPruneStepOp: { PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node; @@ -2668,6 +2690,26 @@ expression_tree_walker_impl(Node *node, return true; } break; + case T_GraphElementPattern: + { + GraphElementPattern *gep = (GraphElementPattern *) node; + + if (WALK(gep->subexpr)) + return true; + if (WALK(gep->whereClause)) + return true; + } + break; + case T_GraphPattern: + { + GraphPattern *gp = (GraphPattern *) node; + + if (LIST_WALK(gp->path_pattern_list)) + return true; + if (WALK(gp->whereClause)) + return true; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); @@ -2719,6 +2761,8 @@ query_tree_walker_impl(Query *query, return true; if (WALK(query->mergeJoinCondition)) return true; + if (WALK(query->forPortionOf)) + return true; if (WALK(query->returningList)) return true; if (WALK(query->jointree)) @@ -2861,6 +2905,12 @@ range_table_entry_walker_impl(RangeTblEntry *rte, if (WALK(rte->values_lists)) return true; break; + case RTE_GRAPH_TABLE: + if (WALK(rte->graph_pattern)) + return true; + if (WALK(rte->graph_table_columns)) + return true; + break; case RTE_CTE: case RTE_NAMEDTUPLESTORE: case RTE_RESULT: @@ -2957,7 +3007,7 @@ expression_tree_mutator_impl(Node *node, */ #define FLATCOPY(newnode, node, nodetype) \ - ( (newnode) = (nodetype *) palloc(sizeof(nodetype)), \ + ( (newnode) = palloc_object(nodetype), \ memcpy((newnode), (node), sizeof(nodetype)) ) #define MUTATE(newfield, oldfield, fieldtype) \ @@ -3613,6 +3663,22 @@ expression_tree_mutator_impl(Node *node, return (Node *) newnode; } break; + case T_ForPortionOfExpr: + { + ForPortionOfExpr *fpo = (ForPortionOfExpr *) node; + ForPortionOfExpr *newnode; + + FLATCOPY(newnode, fpo, ForPortionOfExpr); + MUTATE(newnode->rangeVar, fpo->rangeVar, Var *); + MUTATE(newnode->targetFrom, fpo->targetFrom, Node *); + MUTATE(newnode->targetTo, fpo->targetTo, Node *); + MUTATE(newnode->targetRange, fpo->targetRange, Node *); + MUTATE(newnode->overlapsExpr, fpo->overlapsExpr, Node *); + MUTATE(newnode->rangeTargetList, fpo->rangeTargetList, List *); + + return (Node *) newnode; + } + break; case T_PartitionPruneStepOp: { PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node; @@ -3794,6 +3860,7 @@ query_tree_mutator_impl(Query *query, MUTATE(query->onConflict, query->onConflict, OnConflictExpr *); MUTATE(query->mergeActionList, query->mergeActionList, List *); MUTATE(query->mergeJoinCondition, query->mergeJoinCondition, Node *); + MUTATE(query->forPortionOf, query->forPortionOf, ForPortionOfExpr *); MUTATE(query->returningList, query->returningList, List *); MUTATE(query->jointree, query->jointree, FromExpr *); MUTATE(query->setOperations, query->setOperations, Node *); @@ -3913,6 +3980,10 @@ range_table_mutator_impl(List *rtable, case RTE_VALUES: MUTATE(newrte->values_lists, rte->values_lists, List *); break; + case RTE_GRAPH_TABLE: + MUTATE(newrte->graph_pattern, rte->graph_pattern, GraphPattern *); + MUTATE(newrte->graph_table_columns, rte->graph_table_columns, List *); + break; case RTE_CTE: case RTE_NAMEDTUPLESTORE: case RTE_RESULT: @@ -4547,6 +4618,18 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_RangeGraphTable: + { + RangeGraphTable *rgt = (RangeGraphTable *) node; + + if (WALK(rgt->graph_pattern)) + return true; + if (WALK(rgt->columns)) + return true; + if (WALK(rgt->alias)) + return true; + } + break; case T_TypeName: { TypeName *tn = (TypeName *) node; @@ -4705,6 +4788,26 @@ raw_expression_tree_walker_impl(Node *node, return true; } break; + case T_GraphElementPattern: + { + GraphElementPattern *gep = (GraphElementPattern *) node; + + if (WALK(gep->subexpr)) + return true; + if (WALK(gep->whereClause)) + return true; + } + break; + case T_GraphPattern: + { + GraphPattern *gp = (GraphPattern *) node; + + if (WALK(gp->path_pattern_list)) + return true; + if (WALK(gp->whereClause)) + return true; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); @@ -4830,9 +4933,7 @@ planstate_walk_members(PlanState **planstates, int nplans, planstate_tree_walker_callback walker, void *context) { - int j; - - for (j = 0; j < nplans; j++) + for (int j = 0; j < nplans; j++) { if (PSWALK(planstates[j])) return true; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index ceac3fd862014..953c5797c5d64 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -3,7 +3,7 @@ * outfuncs.c * Output functions for Postgres tree nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -51,6 +51,12 @@ static void outDouble(StringInfo str, double d); #define WRITE_UINT_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %u", node->fldname) +/* Write a signed integer field (anything written with INT64_FORMAT) */ +#define WRITE_INT64_FIELD(fldname) \ + appendStringInfo(str, \ + " :" CppAsString(fldname) " " INT64_FORMAT, \ + node->fldname) + /* Write an unsigned integer field (anything written with UINT64_FORMAT) */ #define WRITE_UINT64_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " " UINT64_FORMAT, \ @@ -340,8 +346,7 @@ outBitmapset(StringInfo str, const Bitmapset *bms) void outDatum(StringInfo str, Datum value, int typlen, bool typbyval) { - Size length, - i; + Size length; char *s; length = datumGetSize(value, typbyval, typlen); @@ -349,20 +354,20 @@ outDatum(StringInfo str, Datum value, int typlen, bool typbyval) if (typbyval) { s = (char *) (&value); - appendStringInfo(str, "%u [ ", (unsigned int) length); - for (i = 0; i < (Size) sizeof(Datum); i++) + appendStringInfo(str, "%zu [ ", length); + for (Size i = 0; i < (Size) sizeof(Datum); i++) appendStringInfo(str, "%d ", (int) (s[i])); appendStringInfoChar(str, ']'); } else { s = (char *) DatumGetPointer(value); - if (!PointerIsValid(s)) + if (!s) appendStringInfoString(str, "0 [ ]"); else { - appendStringInfo(str, "%u [ ", (unsigned int) length); - for (i = 0; i < length; i++) + appendStringInfo(str, "%zu [ ", length); + for (Size i = 0; i < length; i++) appendStringInfo(str, "%d ", (int) (s[i])); appendStringInfoChar(str, ']'); } @@ -428,8 +433,6 @@ _outBoolExpr(StringInfo str, const BoolExpr *node) static void _outForeignKeyOptInfo(StringInfo str, const ForeignKeyOptInfo *node) { - int i; - WRITE_NODE_TYPE("FOREIGNKEYOPTINFO"); WRITE_UINT_FIELD(con_relid); @@ -444,10 +447,10 @@ _outForeignKeyOptInfo(StringInfo str, const ForeignKeyOptInfo *node) WRITE_INT_FIELD(nmatched_ri); /* for compactness, just print the number of matches per column: */ appendStringInfoString(str, " :eclass"); - for (i = 0; i < node->nkeys; i++) + for (int i = 0; i < node->nkeys; i++) appendStringInfo(str, " %d", (node->eclass[i] != NULL)); appendStringInfoString(str, " :rinfos"); - for (i = 0; i < node->nkeys; i++) + for (int i = 0; i < node->nkeys; i++) appendStringInfo(str, " %d", list_length(node->rinfos[i])); } @@ -562,6 +565,15 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node) /* we re-use these RELATION fields, too: */ WRITE_OID_FIELD(relid); break; + case RTE_GRAPH_TABLE: + WRITE_NODE_FIELD(graph_pattern); + WRITE_NODE_FIELD(graph_table_columns); + /* we re-use these RELATION fields, too: */ + WRITE_OID_FIELD(relid); + WRITE_CHAR_FIELD(relkind); + WRITE_INT_FIELD(rellockmode); + WRITE_UINT_FIELD(perminfoindex); + break; case RTE_RESULT: /* no extra fields */ break; @@ -647,6 +659,8 @@ _outA_Expr(StringInfo str, const A_Expr *node) WRITE_NODE_FIELD(lexpr); WRITE_NODE_FIELD(rexpr); + WRITE_LOCATION_FIELD(rexpr_list_start); + WRITE_LOCATION_FIELD(rexpr_list_end); WRITE_LOCATION_FIELD(location); } @@ -731,17 +745,17 @@ outNode(StringInfo str, const void *obj) _outList(str, obj); /* nodeRead does not want to see { } around these! */ else if (IsA(obj, Integer)) - _outInteger(str, (Integer *) obj); + _outInteger(str, (const Integer *) obj); else if (IsA(obj, Float)) - _outFloat(str, (Float *) obj); + _outFloat(str, (const Float *) obj); else if (IsA(obj, Boolean)) - _outBoolean(str, (Boolean *) obj); + _outBoolean(str, (const Boolean *) obj); else if (IsA(obj, String)) - _outString(str, (String *) obj); + _outString(str, (const String *) obj); else if (IsA(obj, BitString)) - _outBitString(str, (BitString *) obj); + _outBitString(str, (const BitString *) obj); else if (IsA(obj, Bitmapset)) - outBitmapset(str, (Bitmapset *) obj); + outBitmapset(str, (const Bitmapset *) obj); else { appendStringInfoChar(str, '{'); diff --git a/src/backend/nodes/params.c b/src/backend/nodes/params.c index ec5946c5777dc..6830e75721e75 100644 --- a/src/backend/nodes/params.c +++ b/src/backend/nodes/params.c @@ -4,7 +4,7 @@ * Support for finding the values associated with Param nodes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -166,13 +166,12 @@ paramlist_param_ref(ParseState *pstate, ParamRef *pref) Size EstimateParamListSpace(ParamListInfo paramLI) { - int i; Size sz = sizeof(int); if (paramLI == NULL || paramLI->numParams <= 0) return sz; - for (i = 0; i < paramLI->numParams; i++) + for (int i = 0; i < paramLI->numParams; i++) { ParamExternData *prm; ParamExternData prmdata; @@ -229,7 +228,6 @@ void SerializeParamList(ParamListInfo paramLI, char **start_address) { int nparams; - int i; /* Write number of parameters. */ if (paramLI == NULL || paramLI->numParams <= 0) @@ -240,7 +238,7 @@ SerializeParamList(ParamListInfo paramLI, char **start_address) *start_address += sizeof(int); /* Write each parameter in turn. */ - for (i = 0; i < nparams; i++) + for (int i = 0; i < nparams; i++) { ParamExternData *prm; ParamExternData prmdata; diff --git a/src/backend/nodes/print.c b/src/backend/nodes/print.c index 65011aaf278ac..17da5543c52f1 100644 --- a/src/backend/nodes/print.c +++ b/src/backend/nodes/print.c @@ -3,7 +3,7 @@ * print.c * various print routines (used mostly for debugging) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -304,6 +304,10 @@ print_rt(const List *rtable) printf("%d\t%s\t[group]", i, rte->eref->aliasname); break; + case RTE_GRAPH_TABLE: + printf("%d\t%s\t[graph table]", + i, rte->eref->aliasname); + break; default: printf("%d\t%s\t[unknown rtekind]", i, rte->eref->aliasname); diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c index d1e82a63f09a8..7c63766a51c5d 100644 --- a/src/backend/nodes/queryjumblefuncs.c +++ b/src/backend/nodes/queryjumblefuncs.c @@ -21,7 +21,12 @@ * tree(s) generated from the query. The executor can then use this value * to blame query costs on the proper queryId. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Arrays of two or more constants and PARAM_EXTERN parameters are "squashed" + * and contribute only once to the jumble. This has the effect that queries + * that differ only on the length of such lists have the same queryId. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -35,10 +40,12 @@ #include "access/transam.h" #include "catalog/pg_proc.h" #include "common/hashfn.h" +#include "common/int.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "nodes/queryjumble.h" #include "utils/lsyscache.h" +#include "parser/scanner.h" #include "parser/scansup.h" #define JUMBLE_SIZE 1024 /* query serialization buffer size */ @@ -56,16 +63,18 @@ int compute_query_id = COMPUTE_QUERY_ID_AUTO; bool query_id_enabled = false; static JumbleState *InitJumble(void); -static uint64 DoJumble(JumbleState *jstate, Node *node); +static int64 DoJumble(JumbleState *jstate, Node *node); static void AppendJumble(JumbleState *jstate, const unsigned char *value, Size size); static void FlushPendingNulls(JumbleState *jstate); static void RecordConstLocation(JumbleState *jstate, - int location, bool squashed); + bool extern_param, + int location, int len); static void _jumbleNode(JumbleState *jstate, Node *node); -static void _jumbleElements(JumbleState *jstate, List *elements); -static void _jumbleA_Const(JumbleState *jstate, Node *node); static void _jumbleList(JumbleState *jstate, Node *node); +static void _jumbleElements(JumbleState *jstate, List *elements, Node *node); +static void _jumbleParam(JumbleState *jstate, Node *node); +static void _jumbleA_Const(JumbleState *jstate, Node *node); static void _jumbleVariableSetStmt(JumbleState *jstate, Node *node); static void _jumbleRangeTblEntry_eref(JumbleState *jstate, RangeTblEntry *rte, @@ -141,12 +150,12 @@ JumbleQuery(Query *query) * If we are unlucky enough to get a hash of zero, use 1 instead for * normal statements and 2 for utility queries. */ - if (query->queryId == UINT64CONST(0)) + if (query->queryId == INT64CONST(0)) { if (query->utilityStmt) - query->queryId = UINT64CONST(2); + query->queryId = INT64CONST(2); else - query->queryId = UINT64CONST(1); + query->queryId = INT64CONST(1); } return jstate; @@ -174,7 +183,7 @@ InitJumble(void) { JumbleState *jstate; - jstate = (JumbleState *) palloc(sizeof(JumbleState)); + jstate = palloc_object(JumbleState); /* Set up workspace for query jumbling */ jstate->jumble = (unsigned char *) palloc(JUMBLE_SIZE); @@ -185,6 +194,7 @@ InitJumble(void) jstate->clocations_count = 0; jstate->highest_extern_param_id = 0; jstate->pending_nulls = 0; + jstate->has_squashed_lists = false; #ifdef USE_ASSERT_CHECKING jstate->total_jumble_len = 0; #endif @@ -197,7 +207,7 @@ InitJumble(void) * Jumble the given Node using the given JumbleState and return the resulting * jumble hash. */ -static uint64 +static int64 DoJumble(JumbleState *jstate, Node *node) { /* Jumble the given node */ @@ -207,10 +217,14 @@ DoJumble(JumbleState *jstate, Node *node) if (jstate->pending_nulls > 0) FlushPendingNulls(jstate); + /* Squashed list found, reset highest_extern_param_id */ + if (jstate->has_squashed_lists) + jstate->highest_extern_param_id = 0; + /* Process the jumble buffer and produce the hash value */ - return DatumGetUInt64(hash_any_extended(jstate->jumble, - jstate->jumble_len, - 0)); + return DatumGetInt64(hash_any_extended(jstate->jumble, + jstate->jumble_len, + 0)); } /* @@ -256,10 +270,10 @@ AppendJumbleInternal(JumbleState *jstate, const unsigned char *item, if (unlikely(jumble_len >= JUMBLE_SIZE)) { - uint64 start_hash; + int64 start_hash; - start_hash = DatumGetUInt64(hash_any_extended(jumble, - JUMBLE_SIZE, 0)); + start_hash = DatumGetInt64(hash_any_extended(jumble, + JUMBLE_SIZE, 0)); memcpy(jumble, &start_hash, sizeof(start_hash)); jumble_len = sizeof(start_hash); } @@ -373,15 +387,17 @@ FlushPendingNulls(JumbleState *jstate) /* - * Record location of constant within query string of query tree that is - * currently being walked. + * Record the location of some kind of constant within a query string. + * These are not only bare constants but also expressions that ultimately + * constitute a constant, such as those inside casts and simple function + * calls; if extern_param, then it corresponds to a PARAM_EXTERN Param. * - * 'squashed' signals that the constant represents the first or the last - * element in a series of merged constants, and everything but the first/last - * element contributes nothing to the jumble hash. + * If length is -1, it indicates a single such constant element. If + * it's a positive integer, it indicates the length of a squashable + * list of them. */ static void -RecordConstLocation(JumbleState *jstate, int location, bool squashed) +RecordConstLocation(JumbleState *jstate, bool extern_param, int location, int len) { /* -1 indicates unknown or undefined location */ if (location >= 0) @@ -396,9 +412,15 @@ RecordConstLocation(JumbleState *jstate, int location, bool squashed) sizeof(LocationLen)); } jstate->clocations[jstate->clocations_count].location = location; - /* initialize lengths to -1 to simplify third-party module usage */ - jstate->clocations[jstate->clocations_count].squashed = squashed; - jstate->clocations[jstate->clocations_count].length = -1; + + /* + * Lengths are either positive integers (indicating a squashable + * list), or -1. + */ + Assert(len > -1 || len == -1); + jstate->clocations[jstate->clocations_count].length = len; + jstate->clocations[jstate->clocations_count].squashed = (len > -1); + jstate->clocations[jstate->clocations_count].extern_param = extern_param; jstate->clocations_count++; } } @@ -407,47 +429,74 @@ RecordConstLocation(JumbleState *jstate, int location, bool squashed) * Subroutine for _jumbleElements: Verify a few simple cases where we can * deduce that the expression is a constant: * - * - Ignore a possible wrapping RelabelType and CoerceViaIO. - * - If it's a FuncExpr, check that the function is an implicit + * - See through any wrapping RelabelType and CoerceViaIO layers. + * - If it's a FuncExpr, check that the function is a builtin * cast and its arguments are Const. - * - Otherwise test if the expression is a simple Const. + * - Otherwise test if the expression is a simple Const or a + * PARAM_EXTERN param. */ static bool -IsSquashableConst(Node *element) +IsSquashableConstant(Node *element) { - if (IsA(element, RelabelType)) - element = (Node *) ((RelabelType *) element)->arg; - - if (IsA(element, CoerceViaIO)) - element = (Node *) ((CoerceViaIO *) element)->arg; - - if (IsA(element, FuncExpr)) +restart: + switch (nodeTag(element)) { - FuncExpr *func = (FuncExpr *) element; - ListCell *temp; + case T_RelabelType: + /* Unwrap RelabelType */ + element = (Node *) ((RelabelType *) element)->arg; + goto restart; - if (func->funcformat != COERCE_IMPLICIT_CAST && - func->funcformat != COERCE_EXPLICIT_CAST) - return false; + case T_CoerceViaIO: + /* Unwrap CoerceViaIO */ + element = (Node *) ((CoerceViaIO *) element)->arg; + goto restart; - if (func->funcid > FirstGenbkiObjectId) - return false; + case T_Const: + return true; - foreach(temp, func->args) - { - Node *arg = lfirst(temp); + case T_Param: + return castNode(Param, element)->paramkind == PARAM_EXTERN; - if (!IsA(arg, Const)) /* XXX we could recurse here instead */ - return false; - } + case T_FuncExpr: + { + FuncExpr *func = (FuncExpr *) element; + ListCell *temp; - return true; - } + if (func->funcformat != COERCE_IMPLICIT_CAST && + func->funcformat != COERCE_EXPLICIT_CAST) + return false; - if (!IsA(element, Const)) - return false; + if (func->funcid > FirstGenbkiObjectId) + return false; - return true; + /* + * We can check function arguments recursively, being careful + * about recursing too deep. At each recursion level it's + * enough to test the stack on the first element. (Note that + * I wasn't able to hit this without bloating the stack + * artificially in this function: the parser errors out before + * stack size becomes a problem here.) + */ + foreach(temp, func->args) + { + Node *arg = lfirst(temp); + + if (!IsA(arg, Const)) + { + if (foreach_current_index(temp) == 0 && + stack_is_too_deep()) + return false; + else if (!IsSquashableConstant(arg)) + return false; + } + } + + return true; + } + + default: + return false; + } } /* @@ -457,39 +506,33 @@ IsSquashableConst(Node *element) * Return value indicates if squashing is possible. * * Note that this function searches only for explicit Const nodes with - * possibly very simple decorations on top, and does not try to simplify - * expressions. + * possibly very simple decorations on top and PARAM_EXTERN parameters, + * and does not try to simplify expressions. */ static bool -IsSquashableConstList(List *elements, Node **firstExpr, Node **lastExpr) +IsSquashableConstantList(List *elements) { ListCell *temp; - /* - * If squashing is disabled, or the list is too short, we don't try to - * squash it. - */ + /* If the list is too short, we don't try to squash it. */ if (list_length(elements) < 2) return false; foreach(temp, elements) { - if (!IsSquashableConst(lfirst(temp))) + if (!IsSquashableConstant(lfirst(temp))) return false; } - *firstExpr = linitial(elements); - *lastExpr = llast(elements); - return true; } #define JUMBLE_NODE(item) \ _jumbleNode(jstate, (Node *) expr->item) -#define JUMBLE_ELEMENTS(list) \ - _jumbleElements(jstate, (List *) expr->list) +#define JUMBLE_ELEMENTS(list, node) \ + _jumbleElements(jstate, (List *) expr->list, node) #define JUMBLE_LOCATION(location) \ - RecordConstLocation(jstate, expr->location, false) + RecordConstLocation(jstate, false, expr->location, -1) #define JUMBLE_FIELD(item) \ do { \ if (sizeof(expr->item) == 8) \ @@ -516,42 +559,6 @@ do { \ #include "queryjumblefuncs.funcs.c" -/* - * We jumble lists of constant elements as one individual item regardless - * of how many elements are in the list. This means different queries - * jumble to the same query_id, if the only difference is the number of - * elements in the list. - */ -static void -_jumbleElements(JumbleState *jstate, List *elements) -{ - Node *first, - *last; - - if (IsSquashableConstList(elements, &first, &last)) - { - /* - * If this list of elements is squashable, keep track of the location - * of its first and last elements. When reading back the locations - * array, we'll see two consecutive locations with ->squashed set to - * true, indicating the location of initial and final elements of this - * list. - * - * For the limited set of cases we support now (implicit coerce via - * FuncExpr, Const) it's fine to use exprLocation of the 'last' - * expression, but if more complex composite expressions are to be - * supported (e.g., OpExpr or FuncExpr as an explicit call), more - * sophisticated tracking will be needed. - */ - RecordConstLocation(jstate, exprLocation(first), true); - RecordConstLocation(jstate, exprLocation(last), true); - } - else - { - _jumbleNode(jstate, (Node *) elements); - } -} - static void _jumbleNode(JumbleState *jstate, Node *node) { @@ -593,26 +600,6 @@ _jumbleNode(JumbleState *jstate, Node *node) break; } - /* Special cases to handle outside the automated code */ - switch (nodeTag(expr)) - { - case T_Param: - { - Param *p = (Param *) node; - - /* - * Update the highest Param id seen, in order to start - * normalization correctly. - */ - if (p->paramkind == PARAM_EXTERN && - p->paramid > jstate->highest_extern_param_id) - jstate->highest_extern_param_id = p->paramid; - } - break; - default: - break; - } - /* Ensure we added something to the jumble buffer */ Assert(jstate->total_jumble_len > prev_jumble_len); } @@ -648,6 +635,79 @@ _jumbleList(JumbleState *jstate, Node *node) } } +/* + * We try to jumble lists of expressions as one individual item regardless + * of how many elements are in the list. This is know as squashing, which + * results in different queries jumbling to the same query_id, if the only + * difference is the number of elements in the list. + * + * We allow constants and PARAM_EXTERN parameters to be squashed. To normalize + * such queries, we use the start and end locations of the list of elements in + * a list. + */ +static void +_jumbleElements(JumbleState *jstate, List *elements, Node *node) +{ + bool normalize_list = false; + + if (IsSquashableConstantList(elements)) + { + if (IsA(node, ArrayExpr)) + { + ArrayExpr *aexpr = (ArrayExpr *) node; + + if (aexpr->list_start > 0 && aexpr->list_end > 0) + { + RecordConstLocation(jstate, + false, + aexpr->list_start + 1, + (aexpr->list_end - aexpr->list_start) - 1); + normalize_list = true; + jstate->has_squashed_lists = true; + } + } + } + + if (!normalize_list) + { + _jumbleNode(jstate, (Node *) elements); + } +} + +/* + * We store the highest param ID of extern params. This can later be used + * to start the numbering of the placeholder for squashed lists. + */ +static void +_jumbleParam(JumbleState *jstate, Node *node) +{ + Param *expr = (Param *) node; + + JUMBLE_FIELD(paramkind); + JUMBLE_FIELD(paramid); + JUMBLE_FIELD(paramtype); + /* paramtypmod and paramcollid are ignored */ + + if (expr->paramkind == PARAM_EXTERN) + { + /* + * At this point, only external parameter locations outside of + * squashable lists will be recorded. + */ + RecordConstLocation(jstate, true, expr->location, -1); + + /* + * Update the highest Param id seen, in order to start normalization + * correctly. + * + * Note: This value is reset at the end of jumbling if there exists a + * squashable list. See the comment in the definition of JumbleState. + */ + if (expr->paramid > jstate->highest_extern_param_id) + jstate->highest_extern_param_id = expr->paramid; + } +} + static void _jumbleA_Const(JumbleState *jstate, Node *node) { @@ -715,3 +775,156 @@ _jumbleRangeTblEntry_eref(JumbleState *jstate, */ JUMBLE_STRING(aliasname); } + +/* + * CompLocation: comparator for qsorting LocationLen structs by location + */ +static int +CompLocation(const void *a, const void *b) +{ + int l = ((const LocationLen *) a)->location; + int r = ((const LocationLen *) b)->location; + + return pg_cmp_s32(l, r); +} + +/* + * Given a valid SQL string and an array of constant-location records, return + * the textual lengths of those constants in a newly allocated LocationLen + * array, or NULL if there are no constants. + * + * The constants may use any allowed constant syntax, such as float literals, + * bit-strings, single-quoted strings and dollar-quoted strings. This is + * accomplished by using the public API for the core scanner. + * + * It is the caller's job to ensure that the string is a valid SQL statement + * with constants at the indicated locations. Since in practice the string + * has already been parsed, and the locations that the caller provides will + * have originated from within the authoritative parser, this should not be + * a problem. + * + * Multiple constants can have the same location. We reset lengths of those + * past the first to -1 so that they can later be ignored. + * + * If query_loc > 0, then "query" has been advanced by that much compared to + * the original string start, as is the case with multi-statement strings, so + * we need to translate the provided locations to compensate. (This lets us + * avoid re-scanning statements before the one of interest, so it's worth + * doing.) + * + * N.B. There is an assumption that a '-' character at a Const location begins + * a negative numeric constant. This precludes there ever being another + * reason for a constant to start with a '-'. + * + * It is the caller's responsibility to free the result, if necessary. + */ +LocationLen * +ComputeConstantLengths(const JumbleState *jstate, const char *query, + int query_loc) +{ + LocationLen *locs; + core_yyscan_t yyscanner; + core_yy_extra_type yyextra; + core_YYSTYPE yylval; + YYLTYPE yylloc; + + if (jstate->clocations_count == 0) + return NULL; + + /* Copy constant locations to avoid modifying jstate */ + locs = palloc_array(LocationLen, jstate->clocations_count); + memcpy(locs, jstate->clocations, jstate->clocations_count * sizeof(LocationLen)); + + /* + * Sort the records by location so that we can process them in order while + * scanning the query text. + */ + if (jstate->clocations_count > 1) + qsort(locs, jstate->clocations_count, + sizeof(LocationLen), CompLocation); + + /* initialize the flex scanner --- should match raw_parser() */ + yyscanner = scanner_init(query, + &yyextra, + &ScanKeywords, + ScanKeywordTokens); + + /* Search for each constant, in sequence */ + for (int i = 0; i < jstate->clocations_count; i++) + { + int loc; + int tok; + + /* Ignore constants after the first one in the same location */ + if (i > 0 && locs[i].location == locs[i - 1].location) + { + locs[i].length = -1; + continue; + } + + if (locs[i].squashed) + continue; /* squashable list, ignore */ + + /* + * Adjust the constant's location using the provided starting location + * of the current statement. This allows us to avoid scanning a + * multi-statement string from the beginning. + */ + loc = locs[i].location - query_loc; + Assert(loc >= 0); + + /* + * We have a valid location for a constant that's not a dupe. Lex + * tokens until we find the desired constant. + */ + for (;;) + { + tok = core_yylex(&yylval, &yylloc, yyscanner); + + /* We should not hit end-of-string, but if we do, behave sanely */ + if (tok == 0) + break; /* out of inner for-loop */ + + /* + * We should find the token position exactly, but if we somehow + * run past it, work with that. + */ + if (yylloc >= loc) + { + if (query[loc] == '-') + { + /* + * It's a negative value - this is the one and only case + * where we replace more than a single token. + * + * Do not compensate for the special-case adjustment of + * location to that of the leading '-' operator in the + * event of a negative constant (see doNegate() in + * gram.y). It is also useful for our purposes to start + * from the minus symbol. In this way, queries like + * "select * from foo where bar = 1" and "select * from + * foo where bar = -2" can be treated similarly. + */ + tok = core_yylex(&yylval, &yylloc, yyscanner); + if (tok == 0) + break; /* out of inner for-loop */ + } + + /* + * We now rely on the assumption that flex has placed a zero + * byte after the text of the current token in scanbuf. + */ + locs[i].length = strlen(yyextra.scanbuf + loc); + break; /* out of inner for-loop */ + } + } + + /* If we hit end-of-string, give up, leaving remaining lengths -1 */ + if (tok == 0) + break; + } + + scanner_finish(yyscanner); + + return locs; +} diff --git a/src/backend/nodes/read.c b/src/backend/nodes/read.c index ce335dd3ff154..f85cf65ea48e4 100644 --- a/src/backend/nodes/read.c +++ b/src/backend/nodes/read.c @@ -4,7 +4,7 @@ * routines to convert a string (legal ascii representation of node) back * to nodes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 64d3a09f765bb..b6b2ce6c792aa 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -3,7 +3,7 @@ * readfuncs.c * Reader functions for Postgres tree nodes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -26,8 +26,6 @@ */ #include "postgres.h" -#include - #include "miscadmin.h" #include "nodes/bitmapset.h" #include "nodes/readfuncs.h" @@ -68,6 +66,12 @@ token = pg_strtok(&length); /* get field value */ \ local_node->fldname = atoui(token) +/* Read a signed integer field (anything written using INT64_FORMAT) */ +#define READ_INT64_FIELD(fldname) \ + token = pg_strtok(&length); /* skip :fldname */ \ + token = pg_strtok(&length); /* get field value */ \ + local_node->fldname = strtoi64(token, NULL, 10) + /* Read an unsigned integer field (anything written using UINT64_FORMAT) */ #define READ_UINT64_FIELD(fldname) \ token = pg_strtok(&length); /* skip :fldname */ \ @@ -419,6 +423,15 @@ _readRangeTblEntry(void) /* we re-use these RELATION fields, too: */ READ_OID_FIELD(relid); break; + case RTE_GRAPH_TABLE: + READ_NODE_FIELD(graph_pattern); + READ_NODE_FIELD(graph_table_columns); + /* we re-use these RELATION fields, too: */ + READ_OID_FIELD(relid); + READ_CHAR_FIELD(relkind); + READ_INT_FIELD(rellockmode); + READ_UINT_FIELD(perminfoindex); + break; case RTE_RESULT: /* no extra fields */ break; @@ -520,6 +533,8 @@ _readA_Expr(void) READ_NODE_FIELD(lexpr); READ_NODE_FIELD(rexpr); + READ_LOCATION_FIELD(rexpr_list_start); + READ_LOCATION_FIELD(rexpr_list_end); READ_LOCATION_FIELD(location); READ_DONE(); @@ -591,8 +606,7 @@ parseNodeString(void) Datum readDatum(bool typbyval) { - Size length, - i; + Size length; int tokenLength; const char *token; Datum res; @@ -615,18 +629,18 @@ readDatum(bool typbyval) elog(ERROR, "byval datum but length = %zu", length); res = (Datum) 0; s = (char *) (&res); - for (i = 0; i < (Size) sizeof(Datum); i++) + for (Size i = 0; i < (Size) sizeof(Datum); i++) { token = pg_strtok(&tokenLength); s[i] = (char) atoi(token); } } else if (length <= 0) - res = (Datum) NULL; + res = (Datum) 0; else { s = (char *) palloc(length); - for (i = 0; i < length; i++) + for (Size i = 0; i < length; i++) { token = pg_strtok(&tokenLength); s[i] = (char) atoi(token); diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c index 41031aa8f2fa8..f1f925cb13b99 100644 --- a/src/backend/nodes/tidbitmap.c +++ b/src/backend/nodes/tidbitmap.c @@ -29,7 +29,7 @@ * and a non-lossy page. * * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/nodes/tidbitmap.c @@ -40,6 +40,7 @@ #include +#include "access/htup_details.h" #include "common/hashfn.h" #include "common/int.h" #include "nodes/bitmapset.h" @@ -363,15 +364,14 @@ tbm_free_shared_area(dsa_area *dsa, dsa_pointer dp) * TBMIterateResult when any of these tuples are reported out. */ void -tbm_add_tuples(TIDBitmap *tbm, const ItemPointer tids, int ntids, +tbm_add_tuples(TIDBitmap *tbm, const ItemPointerData *tids, int ntids, bool recheck) { BlockNumber currblk = InvalidBlockNumber; PagetableEntry *page = NULL; /* only valid when currblk is valid */ - int i; Assert(tbm->iterating == TBM_NOT_ITERATING); - for (i = 0; i < ntids; i++) + for (int i = 0; i < ntids; i++) { BlockNumber blk = ItemPointerGetBlockNumber(tids + i); OffsetNumber off = ItemPointerGetOffsetNumber(tids + i); @@ -470,12 +470,11 @@ static void tbm_union_page(TIDBitmap *a, const PagetableEntry *bpage) { PagetableEntry *apage; - int wordnum; if (bpage->ischunk) { /* Scan b's chunk, mark each indicated page lossy in a */ - for (wordnum = 0; wordnum < WORDS_PER_CHUNK; wordnum++) + for (int wordnum = 0; wordnum < WORDS_PER_CHUNK; wordnum++) { bitmapword w = bpage->words[wordnum]; @@ -510,7 +509,7 @@ tbm_union_page(TIDBitmap *a, const PagetableEntry *bpage) else { /* Both pages are exact, merge at the bit level */ - for (wordnum = 0; wordnum < WORDS_PER_PAGE; wordnum++) + for (int wordnum = 0; wordnum < WORDS_PER_PAGE; wordnum++) apage->words[wordnum] |= bpage->words[wordnum]; apage->recheck |= bpage->recheck; } @@ -578,14 +577,13 @@ static bool tbm_intersect_page(TIDBitmap *a, PagetableEntry *apage, const TIDBitmap *b) { const PagetableEntry *bpage; - int wordnum; if (apage->ischunk) { /* Scan each bit in chunk, try to clear */ bool candelete = true; - for (wordnum = 0; wordnum < WORDS_PER_CHUNK; wordnum++) + for (int wordnum = 0; wordnum < WORDS_PER_CHUNK; wordnum++) { bitmapword w = apage->words[wordnum]; @@ -639,7 +637,7 @@ tbm_intersect_page(TIDBitmap *a, PagetableEntry *apage, const TIDBitmap *b) { /* Both pages are exact, merge at the bit level */ Assert(!bpage->ischunk); - for (wordnum = 0; wordnum < WORDS_PER_PAGE; wordnum++) + for (int wordnum = 0; wordnum < WORDS_PER_PAGE; wordnum++) { apage->words[wordnum] &= bpage->words[wordnum]; if (apage->words[wordnum] != 0) @@ -685,7 +683,7 @@ tbm_begin_private_iterate(TIDBitmap *tbm) * Create the TBMPrivateIterator struct, with enough trailing space to * serve the needs of the TBMIterateResult sub-struct. */ - iterator = (TBMPrivateIterator *) palloc(sizeof(TBMPrivateIterator)); + iterator = palloc_object(TBMPrivateIterator); iterator->tbm = tbm; /* @@ -903,10 +901,9 @@ tbm_extract_page_tuple(TBMIterateResult *iteritem, uint32 max_offsets) { PagetableEntry *page = iteritem->internal_page; - int wordnum; int ntuples = 0; - for (wordnum = 0; wordnum < WORDS_PER_PAGE; wordnum++) + for (int wordnum = 0; wordnum < WORDS_PER_PAGE; wordnum++) { bitmapword w = page->words[wordnum]; @@ -1442,8 +1439,8 @@ static int tbm_shared_comparator(const void *left, const void *right, void *arg) { PagetableEntry *base = (PagetableEntry *) arg; - PagetableEntry *lpage = &base[*(int *) left]; - PagetableEntry *rpage = &base[*(int *) right]; + PagetableEntry *lpage = &base[*(const int *) left]; + PagetableEntry *rpage = &base[*(const int *) right]; if (lpage->blockno < rpage->blockno) return -1; @@ -1471,7 +1468,7 @@ tbm_attach_shared_iterate(dsa_area *dsa, dsa_pointer dp) * Create the TBMSharedIterator struct, with enough trailing space to * serve the needs of the TBMIterateResult sub-struct. */ - iterator = (TBMSharedIterator *) palloc0(sizeof(TBMSharedIterator)); + iterator = palloc0_object(TBMSharedIterator); istate = (TBMSharedIteratorState *) dsa_get_address(dsa, dp); diff --git a/src/backend/nodes/value.c b/src/backend/nodes/value.c index 5a8c1ce24781c..fa2565ac05f42 100644 --- a/src/backend/nodes/value.c +++ b/src/backend/nodes/value.c @@ -4,7 +4,7 @@ * implementation of value nodes * * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/optimizer/Makefile b/src/backend/optimizer/Makefile index f523e5e33ea3d..0e7e76a71506d 100644 --- a/src/backend/optimizer/Makefile +++ b/src/backend/optimizer/Makefile @@ -8,6 +8,11 @@ subdir = src/backend/optimizer top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = geqo path plan prep util +SUBDIRS = \ + geqo \ + path \ + plan \ + prep \ + util include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/optimizer/README b/src/backend/optimizer/README index 9c724ccfabf83..6c35baceedb2f 100644 --- a/src/backend/optimizer/README +++ b/src/backend/optimizer/README @@ -640,7 +640,6 @@ RelOptInfo - a relation or joined relations GroupResultPath - childless Result plan node (used for degenerate grouping) MaterialPath - a Material plan node MemoizePath - a Memoize plan node for caching tuples from sub-paths - UniquePath - remove duplicate rows (either by hashing or sorting) GatherPath - collect the results of parallel workers GatherMergePath - collect parallel results, preserving their common sort order ProjectionPath - a Result plan node with child (used for projection) @@ -648,7 +647,7 @@ RelOptInfo - a relation or joined relations SortPath - a Sort plan node applied to some sub-path IncrementalSortPath - an IncrementalSort plan node applied to some sub-path GroupPath - a Group plan node applied to some sub-path - UpperUniquePath - a Unique plan node applied to some sub-path + UniquePath - a Unique plan node applied to some sub-path AggPath - an Agg plan node applied to some sub-path GroupingSetsPath - an Agg plan node used to implement GROUPING SETS MinMaxAggPath - a Result plan node with subplans performing MIN/MAX @@ -1501,3 +1500,113 @@ breaking down aggregation or grouping over a partitioned relation into aggregation or grouping over its partitions is called partitionwise aggregation. Especially when the partition keys match the GROUP BY clause, this can be significantly faster than the regular method. + +Eager aggregation +----------------- + +Eager aggregation is a query optimization technique that partially +pushes aggregation past a join, and finalizes it once all the +relations are joined. Eager aggregation may reduce the number of +input rows to the join and thus could result in a better overall plan. + +To prove that the transformation is correct, let's first consider the +case where only inner joins are involved. In this case, we partition +the tables in the FROM clause into two groups: those that contain at +least one aggregation column, and those that do not contain any +aggregation columns. Each group can be treated as a single relation +formed by the Cartesian product of the tables within that group. +Therefore, without loss of generality, we can assume that the FROM +clause contains exactly two relations, R1 and R2, where R1 represents +the relation containing all aggregation columns, and R2 represents the +relation without any aggregation columns. + +Let the query be of the form: + +SELECT G, AGG(A) +FROM R1 JOIN R2 ON J +GROUP BY G; + +where G is the set of grouping keys that may include columns from R1 +and/or R2; AGG(A) is an aggregate function over columns A from R1; J +is the join condition between R1 and R2. + +The transformation of eager aggregation is: + + GROUP BY G, AGG(A) on (R1 JOIN R2 ON J) + = + GROUP BY G, AGG(agg_A) on ((GROUP BY G1, AGG(A) AS agg_A on R1) JOIN R2 ON J) + +This equivalence holds under the following conditions: + +1) AGG is decomposable, meaning that it can be computed in two stages: +a partial aggregation followed by a final aggregation; +2) The set G1 used in the pre-aggregation of R1 includes: + * all columns from R1 that are part of the grouping keys G, and + * all columns from R1 that appear in the join condition J. +3) The grouping operator for any column in G1 must be compatible with +the operator used for that column in the join condition J. + +Since G1 includes all columns from R1 that appear in either the +grouping keys G or the join condition J, all rows within each partial +group have identical values for both the grouping keys and the +join-relevant columns from R1, assuming compatible operators are used. +As a result, the rows within a partial group are indistinguishable in +terms of their contribution to the aggregation and their behavior in +the join. This ensures that all rows in the same partial group share +the same "destiny": they either all match or all fail to match a given +row in R2. Because the aggregate function AGG is decomposable, +aggregating the partial results after the join yields the same final +result as aggregating after the full join, thereby preserving query +semantics. Q.E.D. + +In the case where there are any outer joins, the situation becomes +more complex due to join order constraints and the semantics of +null-extension in outer joins. If the relations that contain at least +one aggregation column cannot be treated as a single relation because +of the join order constraints, partial aggregation paths will not be +generated, and thus the transformation is not applicable. Otherwise, +let R1 be the relation containing all aggregation columns, and R2, R3, +... be the remaining relations. From the inner join case, under the +aforementioned conditions, we have the equivalence: + + GROUP BY G, AGG(A) on (R1 JOIN R2 JOIN R3 ...) + = + GROUP BY G, AGG(agg_A) on ((GROUP BY G1, AGG(A) AS agg_A on R1) JOIN R2 JOIN R3 ...) + +To preserve correctness when outer joins are involved, we require an +additional condition: + +4) R1 must not be on the nullable side of any outer join. + +This condition ensures that partial aggregation over R1 does not +suppress any null-extended rows that would be introduced by outer +joins. If R1 is on the nullable side of an outer join, the +NULL-extended rows produced by the outer join would not be available +when we perform the partial aggregation, while with a +non-eager-aggregation plan these rows are available for the top-level +aggregation. Pushing partial aggregation in this case may result in +the rows being grouped differently than expected, or produce incorrect +values from the aggregate functions. + +During the construction of the join tree, we evaluate each base or +join relation to determine if eager aggregation can be applied. If +feasible, we create a separate RelOptInfo called a "grouped relation" +and generate grouped paths by adding sorted and hashed partial +aggregation paths on top of the non-grouped paths. To limit planning +time, we consider only the cheapest or suitably-sorted non-grouped +paths in this step. + +Another way to generate grouped paths is to join a grouped relation +with a non-grouped relation. Joining two grouped relations is +currently not supported. + +To further limit planning time, we currently adopt a strategy where +partial aggregation is pushed only to the lowest feasible level in the +join tree where it provides a significant reduction in row count. +This strategy also helps ensure that all grouped paths for the same +grouped relation produce the same set of rows, which is important to +support a fundamental assumption of the planner. + +If we have generated a grouped relation for the topmost join relation, +we need to finalize its paths at the end. The final paths will +compete in the usual way with paths built from regular planning. diff --git a/src/backend/optimizer/geqo/geqo_copy.c b/src/backend/optimizer/geqo/geqo_copy.c index cbf21dc72e8f1..91a03566ddf72 100644 --- a/src/backend/optimizer/geqo/geqo_copy.c +++ b/src/backend/optimizer/geqo/geqo_copy.c @@ -2,7 +2,7 @@ * * geqo_copy.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_copy.c diff --git a/src/backend/optimizer/geqo/geqo_erx.c b/src/backend/optimizer/geqo/geqo_erx.c index af289f7eeb713..f11a59e4a289e 100644 --- a/src/backend/optimizer/geqo/geqo_erx.c +++ b/src/backend/optimizer/geqo/geqo_erx.c @@ -62,7 +62,7 @@ alloc_edge_table(PlannerInfo *root, int num_gene) * directly; 0 will not be used */ - edge_table = (Edge *) palloc((num_gene + 1) * sizeof(Edge)); + edge_table = palloc_array(Edge, num_gene + 1); return edge_table; } diff --git a/src/backend/optimizer/geqo/geqo_eval.c b/src/backend/optimizer/geqo/geqo_eval.c index f07d1dc8ac69b..56ad3df98fa1d 100644 --- a/src/backend/optimizer/geqo/geqo_eval.c +++ b/src/backend/optimizer/geqo/geqo_eval.c @@ -3,7 +3,7 @@ * geqo_eval.c * Routines to evaluate query trees * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_eval.c @@ -23,7 +23,6 @@ #include #include -#include #include "optimizer/geqo.h" #include "optimizer/joininfo.h" @@ -162,7 +161,7 @@ geqo_eval(PlannerInfo *root, Gene *tour, int num_gene) RelOptInfo * gimme_tree(PlannerInfo *root, Gene *tour, int num_gene) { - GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private; + GeqoPrivateData *private = GetGeqoPrivateData(root); List *clumps; int rel_count; @@ -191,7 +190,7 @@ gimme_tree(PlannerInfo *root, Gene *tour, int num_gene) cur_rel_index - 1); /* Make it into a single-rel clump */ - cur_clump = (Clump *) palloc(sizeof(Clump)); + cur_clump = palloc_object(Clump); cur_clump->joinrel = cur_rel; cur_clump->size = 1; @@ -264,6 +263,9 @@ merge_clump(PlannerInfo *root, List *clumps, Clump *new_clump, int num_gene, /* Keep searching if join order is not valid */ if (joinrel) { + bool is_top_rel = bms_equal(joinrel->relids, + root->all_query_rels); + /* Create paths for partitionwise joins. */ generate_partitionwise_join_paths(root, joinrel); @@ -273,12 +275,28 @@ merge_clump(PlannerInfo *root, List *clumps, Clump *new_clump, int num_gene, * rel once we know the final targetlist (see * grouping_planner). */ - if (!bms_equal(joinrel->relids, root->all_query_rels)) + if (!is_top_rel) generate_useful_gather_paths(root, joinrel, false); /* Find and save the cheapest paths for this joinrel */ set_cheapest(joinrel); + /* + * Except for the topmost scan/join rel, consider generating + * partial aggregation paths for the grouped relation on top + * of the paths of this rel. After that, we're done creating + * paths for the grouped relation, so run set_cheapest(). + */ + if (joinrel->grouped_rel != NULL && !is_top_rel) + { + RelOptInfo *grouped_rel = joinrel->grouped_rel; + + Assert(IS_GROUPED_REL(grouped_rel)); + + generate_grouped_paths(root, grouped_rel, joinrel); + set_cheapest(grouped_rel); + } + /* Absorb new clump into old */ old_clump->joinrel = joinrel; old_clump->size += new_clump->size; diff --git a/src/backend/optimizer/geqo/geqo_main.c b/src/backend/optimizer/geqo/geqo_main.c index 38402ce58db25..f2dd9d843812b 100644 --- a/src/backend/optimizer/geqo/geqo_main.c +++ b/src/backend/optimizer/geqo/geqo_main.c @@ -4,7 +4,7 @@ * solution to the query optimization problem * by means of a Genetic Algorithm (GA) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_main.c @@ -47,6 +47,8 @@ int Geqo_generations; double Geqo_selection_bias; double Geqo_seed; +/* GEQO is treated as an in-core planner extension */ +int Geqo_planner_extension_id = -1; static int gimme_pool_size(int nr_rel); static int gimme_number_generations(int pool_size); @@ -98,10 +100,16 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels) int mutations = 0; #endif + if (Geqo_planner_extension_id < 0) + Geqo_planner_extension_id = GetPlannerExtensionId("geqo"); + /* set up private information */ - root->join_search_private = &private; + SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, &private); private.initial_rels = initial_rels; +/* inform core planner that we may replan */ + root->assumeReplanning = true; + /* initialize private number generator */ geqo_set_seed(root, Geqo_seed); @@ -304,7 +312,7 @@ geqo(PlannerInfo *root, int number_of_rels, List *initial_rels) free_pool(root, pool); /* ... clear root pointer to our private storage */ - root->join_search_private = NULL; + SetPlannerInfoExtensionState(root, Geqo_planner_extension_id, NULL); return best_rel; } diff --git a/src/backend/optimizer/geqo/geqo_misc.c b/src/backend/optimizer/geqo/geqo_misc.c index b8fcc9b6a2f96..42604385c8fcb 100644 --- a/src/backend/optimizer/geqo/geqo_misc.c +++ b/src/backend/optimizer/geqo/geqo_misc.c @@ -3,7 +3,7 @@ * geqo_misc.c * misc. printout and debug stuff * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_misc.c diff --git a/src/backend/optimizer/geqo/geqo_pmx.c b/src/backend/optimizer/geqo/geqo_pmx.c index 01d5571192543..af1cb86839154 100644 --- a/src/backend/optimizer/geqo/geqo_pmx.c +++ b/src/backend/optimizer/geqo/geqo_pmx.c @@ -48,10 +48,10 @@ void pmx(PlannerInfo *root, Gene *tour1, Gene *tour2, Gene *offspring, int num_gene) { - int *failed = (int *) palloc((num_gene + 1) * sizeof(int)); - int *from = (int *) palloc((num_gene + 1) * sizeof(int)); - int *indx = (int *) palloc((num_gene + 1) * sizeof(int)); - int *check_list = (int *) palloc((num_gene + 1) * sizeof(int)); + int *failed = palloc_array(int, num_gene + 1); + int *from = palloc_array(int, num_gene + 1); + int *indx = palloc_array(int, num_gene + 1); + int *check_list = palloc_array(int, num_gene + 1); int left, right, diff --git a/src/backend/optimizer/geqo/geqo_pool.c b/src/backend/optimizer/geqo/geqo_pool.c index b6de0d93f2817..f330c739d3d53 100644 --- a/src/backend/optimizer/geqo/geqo_pool.c +++ b/src/backend/optimizer/geqo/geqo_pool.c @@ -3,7 +3,7 @@ * geqo_pool.c * Genetic Algorithm (GA) pool stuff * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_pool.c @@ -25,7 +25,6 @@ #include #include -#include #include "optimizer/geqo_copy.h" #include "optimizer/geqo_pool.h" @@ -46,17 +45,17 @@ alloc_pool(PlannerInfo *root, int pool_size, int string_length) int i; /* pool */ - new_pool = (Pool *) palloc(sizeof(Pool)); - new_pool->size = (int) pool_size; - new_pool->string_length = (int) string_length; + new_pool = palloc_object(Pool); + new_pool->size = pool_size; + new_pool->string_length = string_length; /* all chromosome */ - new_pool->data = (Chromosome *) palloc(pool_size * sizeof(Chromosome)); + new_pool->data = palloc_array(Chromosome, pool_size); /* all gene */ chromo = (Chromosome *) new_pool->data; /* vector of all chromos */ for (i = 0; i < pool_size; i++) - chromo[i].string = palloc((string_length + 1) * sizeof(Gene)); + chromo[i].string = palloc_array(Gene, string_length + 1); return new_pool; } @@ -163,8 +162,8 @@ alloc_chromo(PlannerInfo *root, int string_length) { Chromosome *chromo; - chromo = (Chromosome *) palloc(sizeof(Chromosome)); - chromo->string = (Gene *) palloc((string_length + 1) * sizeof(Gene)); + chromo = palloc_object(Chromosome); + chromo->string = palloc_array(Gene, string_length + 1); return chromo; } diff --git a/src/backend/optimizer/geqo/geqo_random.c b/src/backend/optimizer/geqo/geqo_random.c index 6c7a411f69f44..a10a60169ea6e 100644 --- a/src/backend/optimizer/geqo/geqo_random.c +++ b/src/backend/optimizer/geqo/geqo_random.c @@ -3,7 +3,7 @@ * geqo_random.c * random number generator * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_random.c @@ -15,11 +15,10 @@ #include "optimizer/geqo_random.h" - void geqo_set_seed(PlannerInfo *root, double seed) { - GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private; + GeqoPrivateData *private = GetGeqoPrivateData(root); pg_prng_fseed(&private->random_state, seed); } @@ -27,7 +26,7 @@ geqo_set_seed(PlannerInfo *root, double seed) double geqo_rand(PlannerInfo *root) { - GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private; + GeqoPrivateData *private = GetGeqoPrivateData(root); return pg_prng_double(&private->random_state); } @@ -35,7 +34,7 @@ geqo_rand(PlannerInfo *root) int geqo_randint(PlannerInfo *root, int upper, int lower) { - GeqoPrivateData *private = (GeqoPrivateData *) root->join_search_private; + GeqoPrivateData *private = GetGeqoPrivateData(root); /* * In current usage, "lower" is never negative so we can just use diff --git a/src/backend/optimizer/geqo/geqo_recombination.c b/src/backend/optimizer/geqo/geqo_recombination.c index a5d3e47ad115e..41d35c179e14e 100644 --- a/src/backend/optimizer/geqo/geqo_recombination.c +++ b/src/backend/optimizer/geqo/geqo_recombination.c @@ -74,7 +74,7 @@ alloc_city_table(PlannerInfo *root, int num_gene) * palloc one extra location so that nodes numbered 1..n can be indexed * directly; 0 will not be used */ - city_table = (City *) palloc((num_gene + 1) * sizeof(City)); + city_table = palloc_array(City, num_gene + 1); return city_table; } diff --git a/src/backend/optimizer/geqo/geqo_selection.c b/src/backend/optimizer/geqo/geqo_selection.c index 9e0a5d4fec86d..a37316e8fd5fe 100644 --- a/src/backend/optimizer/geqo/geqo_selection.c +++ b/src/backend/optimizer/geqo/geqo_selection.c @@ -3,7 +3,7 @@ * geqo_selection.c * linear selection scheme for the genetic query optimizer * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/optimizer/geqo/geqo_selection.c diff --git a/src/backend/optimizer/geqo/meson.build b/src/backend/optimizer/geqo/meson.build index b476489143560..020d95f605f30 100644 --- a/src/backend/optimizer/geqo/meson.build +++ b/src/backend/optimizer/geqo/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'geqo_copy.c', diff --git a/src/backend/optimizer/meson.build b/src/backend/optimizer/meson.build index d4924e3022a25..1bdd2510c6d1b 100644 --- a/src/backend/optimizer/meson.build +++ b/src/backend/optimizer/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('geqo') subdir('path') diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 6cc6966b0600a..61093f222a124 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -3,7 +3,7 @@ * allpaths.c * Routines to find possible search paths for processing a query * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -40,6 +40,7 @@ #include "optimizer/paths.h" #include "optimizer/plancat.h" #include "optimizer/planner.h" +#include "optimizer/prep.h" #include "optimizer/tlist.h" #include "parser/parse_clause.h" #include "parser/parsetree.h" @@ -47,6 +48,7 @@ #include "port/pg_bitutils.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" +#include "utils/selfuncs.h" /* Bitmask flags for pushdown_safety_info.unsafeFlags */ @@ -77,7 +79,9 @@ typedef enum pushdown_safe_type /* These parameters are set by GUC */ bool enable_geqo = false; /* just in case GUC doesn't set it */ +bool enable_eager_aggregate = true; int geqo_threshold; +double min_eager_agg_group_size; int min_parallel_table_scan_size; int min_parallel_index_scan_size; @@ -90,6 +94,7 @@ join_search_hook_type join_search_hook = NULL; static void set_base_rel_consider_startup(PlannerInfo *root); static void set_base_rel_sizes(PlannerInfo *root); +static void setup_simple_grouped_rels(PlannerInfo *root); static void set_base_rel_pathlists(PlannerInfo *root); static void set_rel_size(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); @@ -114,6 +119,7 @@ static void set_append_rel_size(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); static void set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); +static void set_grouped_rel_pathlist(PlannerInfo *root, RelOptInfo *rel); static void generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, List *live_childrels, List *all_child_pathkeys); @@ -122,8 +128,10 @@ static Path *get_cheapest_parameterized_child_path(PlannerInfo *root, Relids required_outer); static void accumulate_append_subpath(Path *path, List **subpaths, - List **special_subpaths); -static Path *get_singleton_append_subpath(Path *path); + List **special_subpaths, + List **child_append_relid_sets); +static Path *get_singleton_append_subpath(Path *path, + List **child_append_relid_sets); static void set_dummy_rel_pathlist(RelOptInfo *rel); static void set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeTblEntry *rte); @@ -182,6 +190,12 @@ make_one_rel(PlannerInfo *root, List *joinlist) */ set_base_rel_sizes(root); + /* + * Build grouped relations for simple rels (i.e., base or "other" member + * relations) where possible. + */ + setup_simple_grouped_rels(root); + /* * We should now have size estimates for every actual table involved in * the query, and we also know which if any have been deleted from the @@ -323,6 +337,39 @@ set_base_rel_sizes(PlannerInfo *root) } } +/* + * setup_simple_grouped_rels + * For each simple relation, build a grouped simple relation if eager + * aggregation is possible and if this relation can produce grouped paths. + */ +static void +setup_simple_grouped_rels(PlannerInfo *root) +{ + Index rti; + + /* + * If there are no aggregate expressions or grouping expressions, eager + * aggregation is not possible. + */ + if (root->agg_clause_list == NIL || + root->group_expr_list == NIL) + return; + + for (rti = 1; rti < root->simple_rel_array_size; rti++) + { + RelOptInfo *rel = root->simple_rel_array[rti]; + + /* there may be empty slots corresponding to non-baserel RTEs */ + if (rel == NULL) + continue; + + Assert(rel->relid == rti); /* sanity check on array */ + Assert(IS_SIMPLE_REL(rel)); /* sanity check on rel */ + + (void) build_simple_grouped_rel(root, rel); + } +} + /* * set_base_rel_pathlists * Finds all paths available for scanning each base-relation entry. @@ -559,6 +606,15 @@ set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, /* Now find the cheapest of the paths for this rel */ set_cheapest(rel); + /* + * If a grouped relation for this rel exists, build partial aggregation + * paths for it. + * + * Note that this can only happen after we've called set_cheapest() for + * this base rel, because we need its cheapest paths. + */ + set_grouped_rel_pathlist(root, rel); + #ifdef OPTIMIZER_DEBUG pprint(rel); #endif @@ -731,6 +787,16 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel, case RTE_RESULT: /* RESULT RTEs, in themselves, are no problem. */ break; + + case RTE_GRAPH_TABLE: + + /* + * Shouldn't happen since these are replaced by subquery RTEs when + * rewriting queries. + */ + Assert(false); + return; + case RTE_GROUP: /* Shouldn't happen; we're only considering baserels here. */ Assert(false); @@ -898,7 +964,7 @@ set_tablesample_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry * bms_membership(root->all_query_rels) != BMS_SINGLETON) && !(GetTsmRoutine(rte->tablesample->tsmhandler)->repeatable_across_scans)) { - path = (Path *) create_material_path(rel, path); + path = (Path *) create_material_path(rel, path, true); } add_path(rel, path); @@ -1305,6 +1371,35 @@ set_append_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, add_paths_to_append_rel(root, rel, live_childrels); } +/* + * set_grouped_rel_pathlist + * If a grouped relation for the given 'rel' exists, build partial + * aggregation paths for it. + */ +static void +set_grouped_rel_pathlist(PlannerInfo *root, RelOptInfo *rel) +{ + RelOptInfo *grouped_rel; + + /* + * If there are no aggregate expressions or grouping expressions, eager + * aggregation is not possible. + */ + if (root->agg_clause_list == NIL || + root->group_expr_list == NIL) + return; + + /* Add paths to the grouped base relation if one exists. */ + grouped_rel = rel->grouped_rel; + if (grouped_rel) + { + Assert(IS_GROUPED_REL(grouped_rel)); + + generate_grouped_paths(root, grouped_rel, rel); + set_cheapest(grouped_rel); + } +} + /* * add_paths_to_append_rel @@ -1321,22 +1416,21 @@ void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, List *live_childrels) { - List *subpaths = NIL; - bool subpaths_valid = true; - List *startup_subpaths = NIL; - bool startup_subpaths_valid = true; - List *partial_subpaths = NIL; - List *pa_partial_subpaths = NIL; - List *pa_nonpartial_subpaths = NIL; - bool partial_subpaths_valid = true; - bool pa_subpaths_valid; + AppendPathInput unparameterized = {0}; + AppendPathInput startup = {0}; + AppendPathInput partial_only = {0}; + AppendPathInput parallel_append = {0}; + bool unparameterized_valid = true; + bool startup_valid = true; + bool partial_only_valid = true; + bool parallel_append_valid = true; List *all_child_pathkeys = NIL; List *all_child_outers = NIL; ListCell *l; double partial_rows = -1; /* If appropriate, consider parallel append */ - pa_subpaths_valid = enable_parallel_append && rel->consider_parallel; + parallel_append_valid = enable_parallel_append && rel->consider_parallel; /* * For every non-dummy child, remember the cheapest path. Also, identify @@ -1360,9 +1454,9 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, if (childrel->pathlist != NIL && childrel->cheapest_total_path->param_info == NULL) accumulate_append_subpath(childrel->cheapest_total_path, - &subpaths, NULL); + &unparameterized.subpaths, NULL, &unparameterized.child_append_relid_sets); else - subpaths_valid = false; + unparameterized_valid = false; /* * When the planner is considering cheap startup plans, we'll also @@ -1388,11 +1482,12 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, /* cheapest_startup_path must not be a parameterized path. */ Assert(cheapest_path->param_info == NULL); accumulate_append_subpath(cheapest_path, - &startup_subpaths, - NULL); + &startup.subpaths, + NULL, + &startup.child_append_relid_sets); } else - startup_subpaths_valid = false; + startup_valid = false; /* Same idea, but for a partial plan. */ @@ -1400,16 +1495,17 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, { cheapest_partial_path = linitial(childrel->partial_pathlist); accumulate_append_subpath(cheapest_partial_path, - &partial_subpaths, NULL); + &partial_only.partial_subpaths, NULL, + &partial_only.child_append_relid_sets); } else - partial_subpaths_valid = false; + partial_only_valid = false; /* * Same idea, but for a parallel append mixing partial and non-partial * paths. */ - if (pa_subpaths_valid) + if (parallel_append_valid) { Path *nppath = NULL; @@ -1419,7 +1515,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, if (cheapest_partial_path == NULL && nppath == NULL) { /* Neither a partial nor a parallel-safe path? Forget it. */ - pa_subpaths_valid = false; + parallel_append_valid = false; } else if (nppath == NULL || (cheapest_partial_path != NULL && @@ -1428,8 +1524,9 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, /* Partial path is cheaper or the only option. */ Assert(cheapest_partial_path != NULL); accumulate_append_subpath(cheapest_partial_path, - &pa_partial_subpaths, - &pa_nonpartial_subpaths); + ¶llel_append.partial_subpaths, + ¶llel_append.subpaths, + ¶llel_append.child_append_relid_sets); } else { @@ -1447,8 +1544,9 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * figure that out. */ accumulate_append_subpath(nppath, - &pa_nonpartial_subpaths, - NULL); + ¶llel_append.subpaths, + NULL, + ¶llel_append.child_append_relid_sets); } } @@ -1522,28 +1620,28 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * unparameterized Append path for the rel. (Note: this is correct even * if we have zero or one live subpath due to constraint exclusion.) */ - if (subpaths_valid) - add_path(rel, (Path *) create_append_path(root, rel, subpaths, NIL, + if (unparameterized_valid) + add_path(rel, (Path *) create_append_path(root, rel, unparameterized, NIL, NULL, 0, false, -1)); /* build an AppendPath for the cheap startup paths, if valid */ - if (startup_subpaths_valid) - add_path(rel, (Path *) create_append_path(root, rel, startup_subpaths, - NIL, NIL, NULL, 0, false, -1)); + if (startup_valid) + add_path(rel, (Path *) create_append_path(root, rel, startup, + NIL, NULL, 0, false, -1)); /* * Consider an append of unordered, unparameterized partial paths. Make * it parallel-aware if possible. */ - if (partial_subpaths_valid && partial_subpaths != NIL) + if (partial_only_valid && partial_only.partial_subpaths != NIL) { AppendPath *appendpath; ListCell *lc; int parallel_workers = 0; /* Find the highest number of workers requested for any subpath. */ - foreach(lc, partial_subpaths) + foreach(lc, partial_only.partial_subpaths) { Path *path = lfirst(lc); @@ -1570,7 +1668,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, Assert(parallel_workers > 0); /* Generate a partial append path. */ - appendpath = create_append_path(root, rel, NIL, partial_subpaths, + appendpath = create_append_path(root, rel, partial_only, NIL, NULL, parallel_workers, enable_parallel_append, -1); @@ -1591,7 +1689,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * a non-partial path that is substantially cheaper than any partial path; * otherwise, we should use the append path added in the previous step.) */ - if (pa_subpaths_valid && pa_nonpartial_subpaths != NIL) + if (parallel_append_valid && parallel_append.subpaths != NIL) { AppendPath *appendpath; ListCell *lc; @@ -1601,7 +1699,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * Find the highest number of workers requested for any partial * subpath. */ - foreach(lc, pa_partial_subpaths) + foreach(lc, parallel_append.partial_subpaths) { Path *path = lfirst(lc); @@ -1619,8 +1717,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, max_parallel_workers_per_gather); Assert(parallel_workers > 0); - appendpath = create_append_path(root, rel, pa_nonpartial_subpaths, - pa_partial_subpaths, + appendpath = create_append_path(root, rel, parallel_append, NIL, NULL, parallel_workers, true, partial_rows); add_partial_path(rel, (Path *) appendpath); @@ -1630,7 +1727,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * Also build unparameterized ordered append paths based on the collected * list of child pathkeys. */ - if (subpaths_valid) + if (unparameterized_valid) generate_orderedappend_paths(root, rel, live_childrels, all_child_pathkeys); @@ -1651,10 +1748,10 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, { Relids required_outer = (Relids) lfirst(l); ListCell *lcr; + AppendPathInput parameterized = {0}; + bool parameterized_valid = true; /* Select the child paths for an Append with this parameterization */ - subpaths = NIL; - subpaths_valid = true; foreach(lcr, live_childrels) { RelOptInfo *childrel = (RelOptInfo *) lfirst(lcr); @@ -1663,7 +1760,7 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, if (childrel->pathlist == NIL) { /* failed to make a suitable path for this child */ - subpaths_valid = false; + parameterized_valid = false; break; } @@ -1673,15 +1770,16 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, if (subpath == NULL) { /* failed to make a suitable path for this child */ - subpaths_valid = false; + parameterized_valid = false; break; } - accumulate_append_subpath(subpath, &subpaths, NULL); + accumulate_append_subpath(subpath, ¶meterized.subpaths, NULL, + ¶meterized.child_append_relid_sets); } - if (subpaths_valid) + if (parameterized_valid) add_path(rel, (Path *) - create_append_path(root, rel, subpaths, NIL, + create_append_path(root, rel, parameterized, NIL, required_outer, 0, false, -1)); } @@ -1702,13 +1800,14 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, { Path *path = (Path *) lfirst(l); AppendPath *appendpath; + AppendPathInput append = {0}; /* skip paths with no pathkeys. */ if (path->pathkeys == NIL) continue; - appendpath = create_append_path(root, rel, NIL, list_make1(path), - NIL, NULL, + append.partial_subpaths = list_make1(path); + appendpath = create_append_path(root, rel, append, NIL, NULL, path->parallel_workers, true, partial_rows); add_partial_path(rel, (Path *) appendpath); @@ -1727,9 +1826,11 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, * We generate a path for each ordering (pathkey list) appearing in * all_child_pathkeys. * - * We consider both cheapest-startup and cheapest-total cases, ie, for each - * interesting ordering, collect all the cheapest startup subpaths and all the - * cheapest total paths, and build a suitable path for each case. + * We consider the cheapest-startup and cheapest-total cases, and also the + * cheapest-fractional case when not all tuples need to be retrieved. For each + * interesting ordering, we collect all the cheapest startup subpaths, all the + * cheapest total paths, and, if applicable, all the cheapest fractional paths, + * and build a suitable path for each case. * * We don't currently generate any parameterized ordered paths here. While * it would not take much more code here to do so, it's very unclear that it @@ -1788,10 +1889,11 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, foreach(lcp, all_child_pathkeys) { List *pathkeys = (List *) lfirst(lcp); - List *startup_subpaths = NIL; - List *total_subpaths = NIL; - List *fractional_subpaths = NIL; + AppendPathInput startup = {0}; + AppendPathInput total = {0}; + AppendPathInput fractional = {0}; bool startup_neq_total = false; + bool fraction_neq_total = false; bool match_partition_order; bool match_partition_order_desc; int end_index; @@ -1894,14 +1996,18 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, double path_fraction = root->tuple_fraction; /* - * Merge Append considers only live children relations. Dummy - * relations must be filtered out before. + * We should not have a dummy child relation here. However, + * we cannot use childrel->rows to compute the tuple fraction, + * as childrel can be an upper relation with an unset row + * estimate. Instead, we use the row estimate from the + * cheapest_total path, which should already have been forced + * to a sane value. */ - Assert(childrel->rows > 0); + Assert(cheapest_total->rows > 0); /* Convert absolute limit to a path fraction */ if (path_fraction >= 1.0) - path_fraction /= childrel->rows; + path_fraction /= cheapest_total->rows; cheapest_fractional = get_cheapest_fractional_path_for_pathkeys(childrel->pathlist, @@ -1916,15 +2022,21 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, * XXX We might consider partially sorted paths too (with an * incremental sort on top). But we'd have to build all the * incremental paths, do the costing etc. + * + * Also, notice whether we actually have different paths for + * the "fractional" and "total" cases. This helps avoid + * generating two identical ordered append paths. */ - if (!cheapest_fractional) + if (cheapest_fractional == NULL) cheapest_fractional = cheapest_total; + else if (cheapest_fractional != cheapest_total) + fraction_neq_total = true; } /* * Notice whether we actually have different paths for the - * "cheapest" and "total" cases; frequently there will be no point - * in two create_merge_append_path() calls. + * "cheapest" and "total" cases. This helps avoid generating two + * identical ordered append paths. */ if (cheapest_startup != cheapest_total) startup_neq_total = true; @@ -1942,16 +2054,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, * just a single subpath (and hence aren't doing anything * useful). */ - cheapest_startup = get_singleton_append_subpath(cheapest_startup); - cheapest_total = get_singleton_append_subpath(cheapest_total); + cheapest_startup = + get_singleton_append_subpath(cheapest_startup, + &startup.child_append_relid_sets); + cheapest_total = + get_singleton_append_subpath(cheapest_total, + &total.child_append_relid_sets); - startup_subpaths = lappend(startup_subpaths, cheapest_startup); - total_subpaths = lappend(total_subpaths, cheapest_total); + startup.subpaths = lappend(startup.subpaths, cheapest_startup); + total.subpaths = lappend(total.subpaths, cheapest_total); if (cheapest_fractional) { - cheapest_fractional = get_singleton_append_subpath(cheapest_fractional); - fractional_subpaths = lappend(fractional_subpaths, cheapest_fractional); + cheapest_fractional = + get_singleton_append_subpath(cheapest_fractional, + &fractional.child_append_relid_sets); + fractional.subpaths = + lappend(fractional.subpaths, cheapest_fractional); } } else @@ -1961,13 +2080,16 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, * child paths for the MergeAppend. */ accumulate_append_subpath(cheapest_startup, - &startup_subpaths, NULL); + &startup.subpaths, NULL, + &startup.child_append_relid_sets); accumulate_append_subpath(cheapest_total, - &total_subpaths, NULL); + &total.subpaths, NULL, + &total.child_append_relid_sets); if (cheapest_fractional) accumulate_append_subpath(cheapest_fractional, - &fractional_subpaths, NULL); + &fractional.subpaths, NULL, + &fractional.child_append_relid_sets); } } @@ -1977,8 +2099,7 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, /* We only need Append */ add_path(rel, (Path *) create_append_path(root, rel, - startup_subpaths, - NIL, + startup, pathkeys, NULL, 0, @@ -1987,19 +2108,17 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, if (startup_neq_total) add_path(rel, (Path *) create_append_path(root, rel, - total_subpaths, - NIL, + total, pathkeys, NULL, 0, false, -1)); - if (fractional_subpaths) + if (fractional.subpaths && fraction_neq_total) add_path(rel, (Path *) create_append_path(root, rel, - fractional_subpaths, - NIL, + fractional, pathkeys, NULL, 0, @@ -2011,20 +2130,23 @@ generate_orderedappend_paths(PlannerInfo *root, RelOptInfo *rel, /* We need MergeAppend */ add_path(rel, (Path *) create_merge_append_path(root, rel, - startup_subpaths, + startup.subpaths, + startup.child_append_relid_sets, pathkeys, NULL)); if (startup_neq_total) add_path(rel, (Path *) create_merge_append_path(root, rel, - total_subpaths, + total.subpaths, + total.child_append_relid_sets, pathkeys, NULL)); - if (fractional_subpaths) + if (fractional.subpaths && fraction_neq_total) add_path(rel, (Path *) create_merge_append_path(root, rel, - fractional_subpaths, + fractional.subpaths, + fractional.child_append_relid_sets, pathkeys, NULL)); } @@ -2127,7 +2249,8 @@ get_cheapest_parameterized_child_path(PlannerInfo *root, RelOptInfo *rel, * paths). */ static void -accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths) +accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths, + List **child_append_relid_sets) { if (IsA(path, AppendPath)) { @@ -2136,6 +2259,11 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths) if (!apath->path.parallel_aware || apath->first_partial_path == 0) { *subpaths = list_concat(*subpaths, apath->subpaths); + *child_append_relid_sets = + lappend(*child_append_relid_sets, path->parent->relids); + *child_append_relid_sets = + list_concat(*child_append_relid_sets, + apath->child_append_relid_sets); return; } else if (special_subpaths != NULL) @@ -2150,6 +2278,11 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths) apath->first_partial_path); *special_subpaths = list_concat(*special_subpaths, new_special_subpaths); + *child_append_relid_sets = + lappend(*child_append_relid_sets, path->parent->relids); + *child_append_relid_sets = + list_concat(*child_append_relid_sets, + apath->child_append_relid_sets); return; } } @@ -2158,6 +2291,11 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths) MergeAppendPath *mpath = (MergeAppendPath *) path; *subpaths = list_concat(*subpaths, mpath->subpaths); + *child_append_relid_sets = + lappend(*child_append_relid_sets, path->parent->relids); + *child_append_relid_sets = + list_concat(*child_append_relid_sets, + mpath->child_append_relid_sets); return; } @@ -2169,10 +2307,15 @@ accumulate_append_subpath(Path *path, List **subpaths, List **special_subpaths) * Returns the single subpath of an Append/MergeAppend, or just * return 'path' if it's not a single sub-path Append/MergeAppend. * + * As a side effect, whenever we return a single subpath rather than the + * original path, add the relid sets for the original path to + * child_append_relid_sets, so that those relids don't entirely disappear + * from the final plan. + * * Note: 'path' must not be a parallel-aware path. */ static Path * -get_singleton_append_subpath(Path *path) +get_singleton_append_subpath(Path *path, List **child_append_relid_sets) { Assert(!path->parallel_aware); @@ -2181,14 +2324,28 @@ get_singleton_append_subpath(Path *path) AppendPath *apath = (AppendPath *) path; if (list_length(apath->subpaths) == 1) + { + *child_append_relid_sets = + lappend(*child_append_relid_sets, path->parent->relids); + *child_append_relid_sets = + list_concat(*child_append_relid_sets, + apath->child_append_relid_sets); return (Path *) linitial(apath->subpaths); + } } else if (IsA(path, MergeAppendPath)) { MergeAppendPath *mpath = (MergeAppendPath *) path; if (list_length(mpath->subpaths) == 1) + { + *child_append_relid_sets = + lappend(*child_append_relid_sets, path->parent->relids); + *child_append_relid_sets = + list_concat(*child_append_relid_sets, + mpath->child_append_relid_sets); return (Path *) linitial(mpath->subpaths); + } } return path; @@ -2208,6 +2365,8 @@ get_singleton_append_subpath(Path *path) static void set_dummy_rel_pathlist(RelOptInfo *rel) { + AppendPathInput in = {0}; + /* Set dummy size estimates --- we leave attr_widths[] as zeroes */ rel->rows = 0; rel->reltarget->width = 0; @@ -2217,7 +2376,7 @@ set_dummy_rel_pathlist(RelOptInfo *rel) rel->partial_pathlist = NIL; /* Set up the dummy path */ - add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, + add_path(rel, (Path *) create_append_path(NULL, rel, in, NIL, rel->lateral_relids, 0, false, -1)); @@ -2254,10 +2413,9 @@ set_dummy_rel_pathlist(RelOptInfo *rel) * return false. */ static bool -find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti, - AttrNumber attno, WindowFunc *wfunc, OpExpr *opexpr, - bool wfunc_left, bool *keep_original, - Bitmapset **run_cond_attrs) +find_window_run_conditions(Query *subquery, AttrNumber attno, + WindowFunc *wfunc, OpExpr *opexpr, bool wfunc_left, + bool *keep_original, Bitmapset **run_cond_attrs) { Oid prosupport; Expr *otherexpr; @@ -2445,8 +2603,8 @@ find_window_run_conditions(Query *subquery, RangeTblEntry *rte, Index rti, * will use the runCondition to stop returning tuples. */ static bool -check_and_push_window_quals(Query *subquery, RangeTblEntry *rte, Index rti, - Node *clause, Bitmapset **run_cond_attrs) +check_and_push_window_quals(Query *subquery, Node *clause, + Bitmapset **run_cond_attrs) { OpExpr *opexpr = (OpExpr *) clause; bool keep_original = true; @@ -2485,9 +2643,8 @@ check_and_push_window_quals(Query *subquery, RangeTblEntry *rte, Index rti, TargetEntry *tle = list_nth(subquery->targetList, var1->varattno - 1); WindowFunc *wfunc = (WindowFunc *) tle->expr; - if (find_window_run_conditions(subquery, rte, rti, tle->resno, wfunc, - opexpr, true, &keep_original, - run_cond_attrs)) + if (find_window_run_conditions(subquery, tle->resno, wfunc, opexpr, + true, &keep_original, run_cond_attrs)) return keep_original; } @@ -2498,9 +2655,8 @@ check_and_push_window_quals(Query *subquery, RangeTblEntry *rte, Index rti, TargetEntry *tle = list_nth(subquery->targetList, var2->varattno - 1); WindowFunc *wfunc = (WindowFunc *) tle->expr; - if (find_window_run_conditions(subquery, rte, rti, tle->resno, wfunc, - opexpr, false, &keep_original, - run_cond_attrs)) + if (find_window_run_conditions(subquery, tle->resno, wfunc, opexpr, + false, &keep_original, run_cond_attrs)) return keep_original; } @@ -2532,6 +2688,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, RelOptInfo *sub_final_rel; Bitmapset *run_cond_attrs = NULL; ListCell *lc; + char *plan_name; /* * Must copy the Query so that planning doesn't mess up the RTE contents @@ -2622,7 +2779,7 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, * runCondition. */ if (!subquery->hasWindowFuncs || - check_and_push_window_quals(subquery, rte, rti, clause, + check_and_push_window_quals(subquery, clause, &run_cond_attrs)) { /* @@ -2674,8 +2831,9 @@ set_subquery_pathlist(PlannerInfo *root, RelOptInfo *rel, Assert(root->plan_params == NIL); /* Generate a subroot and Paths for the subquery */ - rel->subroot = subquery_planner(root->glob, subquery, root, false, - tuple_fraction, NULL); + plan_name = choose_plan_name(root->glob, rte->eref->aliasname, false); + rel->subroot = subquery_planner(root->glob, subquery, plan_name, + root, NULL, false, tuple_fraction, NULL); /* Isolate the params needed by this specific subplan */ rel->subplan_params = root->plan_params; @@ -3335,6 +3493,345 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r } } +/* + * generate_grouped_paths + * Generate paths for a grouped relation by adding sorted and hashed + * partial aggregation paths on top of paths of the ungrouped relation. + * + * The information needed is provided by the RelAggInfo structure stored in + * "grouped_rel". + */ +void +generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, + RelOptInfo *rel) +{ + RelAggInfo *agg_info = grouped_rel->agg_info; + AggClauseCosts agg_costs; + bool can_hash; + bool can_sort; + Path *cheapest_total_path = NULL; + Path *cheapest_partial_path = NULL; + double dNumGroups = 0; + double dNumPartialGroups = 0; + List *group_pathkeys = NIL; + + if (IS_DUMMY_REL(rel)) + { + mark_dummy_rel(grouped_rel); + return; + } + + /* + * We push partial aggregation only to the lowest possible level in the + * join tree that is deemed useful. + */ + if (!bms_equal(agg_info->apply_agg_at, rel->relids) || + !agg_info->agg_useful) + return; + + MemSet(&agg_costs, 0, sizeof(AggClauseCosts)); + get_agg_clause_costs(root, AGGSPLIT_INITIAL_SERIAL, &agg_costs); + + /* + * Determine whether it's possible to perform sort-based implementations + * of grouping, and generate the pathkeys that represent the grouping + * requirements in that case. + */ + can_sort = grouping_is_sortable(agg_info->group_clauses); + if (can_sort) + { + RelOptInfo *top_grouped_rel; + List *top_group_tlist; + + top_grouped_rel = IS_OTHER_REL(rel) ? + rel->top_parent->grouped_rel : grouped_rel; + top_group_tlist = + make_tlist_from_pathtarget(top_grouped_rel->agg_info->target); + + group_pathkeys = + make_pathkeys_for_sortclauses(root, agg_info->group_clauses, + top_group_tlist); + } + + /* + * Determine whether we should consider hash-based implementations of + * grouping. + */ + Assert(root->numOrderedAggs == 0); + can_hash = (agg_info->group_clauses != NIL && + grouping_is_hashable(agg_info->group_clauses)); + + /* + * Consider whether we should generate partially aggregated non-partial + * paths. We can only do this if we have a non-partial path. + */ + if (rel->pathlist != NIL) + { + cheapest_total_path = rel->cheapest_total_path; + Assert(cheapest_total_path != NULL); + } + + /* + * If parallelism is possible for grouped_rel, then we should consider + * generating partially-grouped partial paths. However, if the ungrouped + * rel has no partial paths, then we can't. + */ + if (grouped_rel->consider_parallel && rel->partial_pathlist != NIL) + { + cheapest_partial_path = linitial(rel->partial_pathlist); + Assert(cheapest_partial_path != NULL); + } + + /* Estimate number of partial groups. */ + if (cheapest_total_path != NULL) + dNumGroups = estimate_num_groups(root, + agg_info->group_exprs, + cheapest_total_path->rows, + NULL, NULL); + if (cheapest_partial_path != NULL) + dNumPartialGroups = estimate_num_groups(root, + agg_info->group_exprs, + cheapest_partial_path->rows, + NULL, NULL); + + if (can_sort && cheapest_total_path != NULL) + { + ListCell *lc; + + /* + * Use any available suitably-sorted path as input, and also consider + * sorting the cheapest-total path and incremental sort on any paths + * with presorted keys. + * + * To save planning time, we ignore parameterized input paths unless + * they are the cheapest-total path. + */ + foreach(lc, rel->pathlist) + { + Path *input_path = (Path *) lfirst(lc); + Path *path; + bool is_sorted; + int presorted_keys; + + /* + * Ignore parameterized paths that are not the cheapest-total + * path. + */ + if (input_path->param_info && + input_path != cheapest_total_path) + continue; + + is_sorted = pathkeys_count_contained_in(group_pathkeys, + input_path->pathkeys, + &presorted_keys); + + /* + * Ignore paths that are not suitably or partially sorted, unless + * they are the cheapest total path (no need to deal with paths + * which have presorted keys when incremental sort is disabled). + */ + if (!is_sorted && input_path != cheapest_total_path && + (presorted_keys == 0 || !enable_incremental_sort)) + continue; + + /* + * Since the path originates from a non-grouped relation that is + * not aware of eager aggregation, we must ensure that it provides + * the correct input for partial aggregation. + */ + path = (Path *) create_projection_path(root, + grouped_rel, + input_path, + agg_info->agg_input); + + if (!is_sorted) + { + /* + * We've no need to consider both a sort and incremental sort. + * We'll just do a sort if there are no presorted keys and an + * incremental sort when there are presorted keys. + */ + if (presorted_keys == 0 || !enable_incremental_sort) + path = (Path *) create_sort_path(root, + grouped_rel, + path, + group_pathkeys, + -1.0); + else + path = (Path *) create_incremental_sort_path(root, + grouped_rel, + path, + group_pathkeys, + presorted_keys, + -1.0); + } + + /* + * qual is NIL because the HAVING clause cannot be evaluated until + * the final value of the aggregate is known. + */ + path = (Path *) create_agg_path(root, + grouped_rel, + path, + agg_info->target, + AGG_SORTED, + AGGSPLIT_INITIAL_SERIAL, + agg_info->group_clauses, + NIL, + &agg_costs, + dNumGroups); + + add_path(grouped_rel, path); + } + } + + if (can_sort && cheapest_partial_path != NULL) + { + ListCell *lc; + + /* Similar to above logic, but for partial paths. */ + foreach(lc, rel->partial_pathlist) + { + Path *input_path = (Path *) lfirst(lc); + Path *path; + bool is_sorted; + int presorted_keys; + + is_sorted = pathkeys_count_contained_in(group_pathkeys, + input_path->pathkeys, + &presorted_keys); + + /* + * Ignore paths that are not suitably or partially sorted, unless + * they are the cheapest partial path (no need to deal with paths + * which have presorted keys when incremental sort is disabled). + */ + if (!is_sorted && input_path != cheapest_partial_path && + (presorted_keys == 0 || !enable_incremental_sort)) + continue; + + /* + * Since the path originates from a non-grouped relation that is + * not aware of eager aggregation, we must ensure that it provides + * the correct input for partial aggregation. + */ + path = (Path *) create_projection_path(root, + grouped_rel, + input_path, + agg_info->agg_input); + + if (!is_sorted) + { + /* + * We've no need to consider both a sort and incremental sort. + * We'll just do a sort if there are no presorted keys and an + * incremental sort when there are presorted keys. + */ + if (presorted_keys == 0 || !enable_incremental_sort) + path = (Path *) create_sort_path(root, + grouped_rel, + path, + group_pathkeys, + -1.0); + else + path = (Path *) create_incremental_sort_path(root, + grouped_rel, + path, + group_pathkeys, + presorted_keys, + -1.0); + } + + /* + * qual is NIL because the HAVING clause cannot be evaluated until + * the final value of the aggregate is known. + */ + path = (Path *) create_agg_path(root, + grouped_rel, + path, + agg_info->target, + AGG_SORTED, + AGGSPLIT_INITIAL_SERIAL, + agg_info->group_clauses, + NIL, + &agg_costs, + dNumPartialGroups); + + add_partial_path(grouped_rel, path); + } + } + + /* + * Add a partially-grouped HashAgg Path where possible + */ + if (can_hash && cheapest_total_path != NULL) + { + Path *path; + + /* + * Since the path originates from a non-grouped relation that is not + * aware of eager aggregation, we must ensure that it provides the + * correct input for partial aggregation. + */ + path = (Path *) create_projection_path(root, + grouped_rel, + cheapest_total_path, + agg_info->agg_input); + + /* + * qual is NIL because the HAVING clause cannot be evaluated until the + * final value of the aggregate is known. + */ + path = (Path *) create_agg_path(root, + grouped_rel, + path, + agg_info->target, + AGG_HASHED, + AGGSPLIT_INITIAL_SERIAL, + agg_info->group_clauses, + NIL, + &agg_costs, + dNumGroups); + + add_path(grouped_rel, path); + } + + /* + * Now add a partially-grouped HashAgg partial Path where possible + */ + if (can_hash && cheapest_partial_path != NULL) + { + Path *path; + + /* + * Since the path originates from a non-grouped relation that is not + * aware of eager aggregation, we must ensure that it provides the + * correct input for partial aggregation. + */ + path = (Path *) create_projection_path(root, + grouped_rel, + cheapest_partial_path, + agg_info->agg_input); + + /* + * qual is NIL because the HAVING clause cannot be evaluated until the + * final value of the aggregate is known. + */ + path = (Path *) create_agg_path(root, + grouped_rel, + path, + agg_info->target, + AGG_HASHED, + AGGSPLIT_INITIAL_SERIAL, + agg_info->group_clauses, + NIL, + &agg_costs, + dNumPartialGroups); + + add_partial_path(grouped_rel, path); + } +} + /* * make_rel_from_joinlist * Build access paths using a "joinlist" to guide the join path search. @@ -3494,11 +3991,19 @@ standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels) * * After that, we're done creating paths for the joinrel, so run * set_cheapest(). + * + * In addition, we also run generate_grouped_paths() for the grouped + * relation of each just-processed joinrel, and run set_cheapest() for + * the grouped relation afterwards. */ foreach(lc, root->join_rel_level[lev]) { + bool is_top_rel; + rel = (RelOptInfo *) lfirst(lc); + is_top_rel = bms_equal(rel->relids, root->all_query_rels); + /* Create paths for partitionwise joins. */ generate_partitionwise_join_paths(root, rel); @@ -3508,12 +4013,28 @@ standard_join_search(PlannerInfo *root, int levels_needed, List *initial_rels) * once we know the final targetlist (see grouping_planner's and * its call to apply_scanjoin_target_to_paths). */ - if (!bms_equal(rel->relids, root->all_query_rels)) + if (!is_top_rel) generate_useful_gather_paths(root, rel, false); /* Find and save the cheapest paths for this rel */ set_cheapest(rel); + /* + * Except for the topmost scan/join rel, consider generating + * partial aggregation paths for the grouped relation on top of + * the paths of this rel. After that, we're done creating paths + * for the grouped relation, so run set_cheapest(). + */ + if (rel->grouped_rel != NULL && !is_top_rel) + { + RelOptInfo *grouped_rel = rel->grouped_rel; + + Assert(IS_GROUPED_REL(grouped_rel)); + + generate_grouped_paths(root, grouped_rel, rel); + set_cheapest(grouped_rel); + } + #ifdef OPTIMIZER_DEBUG pprint(rel); #endif @@ -3746,9 +4267,38 @@ recurse_pushdown_safe(Node *setOp, Query *topquery, static void check_output_expressions(Query *subquery, pushdown_safety_info *safetyInfo) { + List *flattened_targetList = subquery->targetList; ListCell *lc; - foreach(lc, subquery->targetList) + /* + * We must be careful with grouping Vars and join alias Vars in the + * subquery's outputs, as they hide the underlying expressions. + * + * We need to expand grouping Vars to their underlying expressions (the + * grouping clauses) because the grouping expressions themselves might be + * volatile or set-returning. However, we do not need to expand join + * alias Vars, as their underlying structure does not introduce volatile + * or set-returning functions at the current level. + * + * In neither case do we need to recursively examine the Vars contained in + * these underlying expressions. Even if they reference outputs from + * lower-level subqueries (at any depth), those references are guaranteed + * not to expand to volatile or set-returning functions, because + * subqueries containing such functions in their targetlists are never + * pulled up. + */ + if (subquery->hasGroupRTE) + { + /* + * We can safely pass NULL for the root here. This function uses the + * expanded expressions solely to check for volatile or set-returning + * functions, which is independent of the Vars' nullingrels. + */ + flattened_targetList = (List *) + flatten_group_exprs(NULL, subquery, (Node *) subquery->targetList); + } + + foreach(lc, flattened_targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); @@ -4383,6 +4933,25 @@ generate_partitionwise_join_paths(PlannerInfo *root, RelOptInfo *rel) if (IS_DUMMY_REL(child_rel)) continue; + /* + * Except for the topmost scan/join rel, consider generating partial + * aggregation paths for the grouped relation on top of the paths of + * this partitioned child-join. After that, we're done creating paths + * for the grouped relation, so run set_cheapest(). + */ + if (child_rel->grouped_rel != NULL && + !bms_equal(IS_OTHER_REL(rel) ? + rel->top_parent_relids : rel->relids, + root->all_query_rels)) + { + RelOptInfo *grouped_rel = child_rel->grouped_rel; + + Assert(IS_GROUPED_REL(grouped_rel)); + + generate_grouped_paths(root, grouped_rel, child_rel); + set_cheapest(grouped_rel); + } + #ifdef OPTIMIZER_DEBUG pprint(child_rel); #endif diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c index 5d51f97f21906..25c4d177ad92d 100644 --- a/src/backend/optimizer/path/clausesel.c +++ b/src/backend/optimizer/path/clausesel.c @@ -3,7 +3,7 @@ * clausesel.c * Routines to compute clause selectivities * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -495,7 +495,7 @@ addRangeClause(RangeQueryClause **rqlist, Node *clause, } /* No matching var found, so make a new clause-pair data structure */ - rqelem = (RangeQueryClause *) palloc(sizeof(RangeQueryClause)); + rqelem = palloc_object(RangeQueryClause); rqelem->var = var; if (is_lobound) { @@ -874,6 +874,10 @@ clause_selectivity_ext(PlannerInfo *root, varRelid, jointype, sjinfo); + + /* If no support, fall back on boolvarsel */ + if (s1 < 0) + s1 = boolvarsel(root, clause, varRelid); } else if (IsA(clause, ScalarArrayOpExpr)) { diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 3d44815ed5adf..1c575e56ff607 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -71,7 +71,7 @@ * values. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -95,6 +95,7 @@ #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "nodes/tidbitmap.h" #include "optimizer/clauses.h" #include "optimizer/cost.h" #include "optimizer/optimizer.h" @@ -257,32 +258,6 @@ clamp_width_est(int64 tuple_width) return (int32) tuple_width; } -/* - * clamp_cardinality_to_long - * Cast a Cardinality value to a sane long value. - */ -long -clamp_cardinality_to_long(Cardinality x) -{ - /* - * Just for paranoia's sake, ensure we do something sane with negative or - * NaN values. - */ - if (isnan(x)) - return LONG_MAX; - if (x <= 0) - return 0; - - /* - * If "long" is 64 bits, then LONG_MAX cannot be represented exactly as a - * double. Casting it to double and back may well result in overflow due - * to rounding, so avoid doing that. We trust that any double value that - * compares strictly less than "(double) LONG_MAX" will cast to a - * representable "long" value. - */ - return (x < (double) LONG_MAX) ? (long) x : LONG_MAX; -} - /* * cost_seqscan @@ -301,6 +276,7 @@ cost_seqscan(Path *path, PlannerInfo *root, double spc_seq_page_cost; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = PGS_SEQSCAN; /* Should only be applied to base relations */ Assert(baserel->relid > 0); @@ -353,8 +329,11 @@ cost_seqscan(Path *path, PlannerInfo *root, */ path->rows = clamp_row_est(path->rows / parallel_divisor); } + else + enable_mask |= PGS_CONSIDER_NONPARTIAL; - path->disabled_nodes = enable_seqscan ? 0 : 1; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1; path->startup_cost = startup_cost; path->total_cost = startup_cost + cpu_run_cost + disk_run_cost; } @@ -380,6 +359,7 @@ cost_samplescan(Path *path, PlannerInfo *root, spc_page_cost; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = 0; /* Should only be applied to base relations with tablesample clauses */ Assert(baserel->relid > 0); @@ -427,7 +407,11 @@ cost_samplescan(Path *path, PlannerInfo *root, startup_cost += path->pathtarget->cost.startup; run_cost += path->pathtarget->cost.per_tuple * path->rows; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -466,7 +450,8 @@ cost_gather(GatherPath *path, PlannerInfo *root, startup_cost += parallel_setup_cost; run_cost += parallel_tuple_cost * path->path.rows; - path->path.disabled_nodes = path->subpath->disabled_nodes; + path->path.disabled_nodes = path->subpath->disabled_nodes + + ((rel->pgs_mask & PGS_GATHER) != 0 ? 0 : 1); path->path.startup_cost = startup_cost; path->path.total_cost = (startup_cost + run_cost); } @@ -532,8 +517,8 @@ cost_gather_merge(GatherMergePath *path, PlannerInfo *root, startup_cost += parallel_setup_cost; run_cost += parallel_tuple_cost * path->path.rows * 1.05; - path->path.disabled_nodes = input_disabled_nodes - + (enable_gathermerge ? 0 : 1); + path->path.disabled_nodes = path->subpath->disabled_nodes + + ((rel->pgs_mask & PGS_GATHER_MERGE) != 0 ? 0 : 1); path->path.startup_cost = startup_cost + input_startup_cost; path->path.total_cost = (startup_cost + run_cost + input_total_cost); } @@ -583,6 +568,7 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count, double pages_fetched; double rand_heap_pages; double index_pages; + uint64 enable_mask; /* Should only be applied to base relations */ Assert(IsA(baserel, RelOptInfo) && @@ -614,8 +600,11 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count, path->indexclauses); } - /* we don't need to check enable_indexonlyscan; indxpath.c does that */ - path->path.disabled_nodes = enable_indexscan ? 0 : 1; + /* is this scan type disabled? */ + enable_mask = (indexonly ? PGS_INDEXONLYSCAN : PGS_INDEXSCAN) + | (partial_path ? 0 : PGS_CONSIDER_NONPARTIAL); + path->path.disabled_nodes = + (baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1; /* * Call index-access-method-specific code to estimate the processing cost @@ -1036,6 +1025,7 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel, double spc_seq_page_cost, spc_random_page_cost; double T; + uint64 enable_mask = PGS_BITMAPSCAN; /* Should only be applied to base relations */ Assert(IsA(baserel, RelOptInfo)); @@ -1101,6 +1091,8 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel, path->rows = clamp_row_est(path->rows / parallel_divisor); } + else + enable_mask |= PGS_CONSIDER_NONPARTIAL; run_cost += cpu_run_cost; @@ -1109,7 +1101,8 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel, startup_cost += path->pathtarget->cost.startup; run_cost += path->pathtarget->cost.per_tuple * path->rows; - path->disabled_nodes = enable_bitmapscan ? 0 : 1; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) == enable_mask ? 0 : 1; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1266,6 +1259,7 @@ cost_tidscan(Path *path, PlannerInfo *root, double ntuples; ListCell *l; double spc_random_page_cost; + uint64 enable_mask = 0; /* Should only be applied to base relations */ Assert(baserel->relid > 0); @@ -1287,10 +1281,10 @@ cost_tidscan(Path *path, PlannerInfo *root, /* * We must use a TID scan for CurrentOfExpr; in any other case, we - * should be generating a TID scan only if enable_tidscan=true. Also, - * if CurrentOfExpr is the qual, there should be only one. + * should be generating a TID scan only if TID scans are allowed. + * Also, if CurrentOfExpr is the qual, there should be only one. */ - Assert(enable_tidscan || IsA(qual, CurrentOfExpr)); + Assert((baserel->pgs_mask & PGS_TIDSCAN) != 0 || IsA(qual, CurrentOfExpr)); Assert(list_length(tidquals) == 1 || !IsA(qual, CurrentOfExpr)); if (IsA(qual, ScalarArrayOpExpr)) @@ -1342,10 +1336,14 @@ cost_tidscan(Path *path, PlannerInfo *root, /* * There are assertions above verifying that we only reach this function - * either when enable_tidscan=true or when the TID scan is the only legal - * path, so it's safe to set disabled_nodes to zero here. + * either when baserel->pgs_mask includes PGS_TIDSCAN or when the TID scan + * is the only legal path, so we only need to consider the effects of + * PGS_CONSIDER_NONPARTIAL here. */ - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1366,8 +1364,9 @@ cost_tidrangescan(Path *path, PlannerInfo *root, { Selectivity selectivity; double pages; - Cost startup_cost = 0; - Cost run_cost = 0; + Cost startup_cost; + Cost cpu_run_cost; + Cost disk_run_cost; QualCost qpqual_cost; Cost cpu_per_tuple; QualCost tid_qual_cost; @@ -1375,6 +1374,7 @@ cost_tidrangescan(Path *path, PlannerInfo *root, double nseqpages; double spc_random_page_cost; double spc_seq_page_cost; + uint64 enable_mask = PGS_TIDSCAN; /* Should only be applied to base relations */ Assert(baserel->relid > 0); @@ -1399,8 +1399,8 @@ cost_tidrangescan(Path *path, PlannerInfo *root, * page is just a normal sequential page read. NOTE: it's desirable for * TID Range Scans to cost more than the equivalent Sequential Scans, * because Seq Scans have some performance advantages such as scan - * synchronization and parallelizability, and we'd prefer one of them to - * be picked unless a TID Range Scan really is better. + * synchronization, and we'd prefer one of them to be picked unless a TID + * Range Scan really is better. */ ntuples = selectivity * baserel->tuples; nseqpages = pages - 1.0; @@ -1417,7 +1417,7 @@ cost_tidrangescan(Path *path, PlannerInfo *root, &spc_seq_page_cost); /* disk costs; 1 random page and the remainder as seq pages */ - run_cost += spc_random_page_cost + spc_seq_page_cost * nseqpages; + disk_run_cost = spc_random_page_cost + spc_seq_page_cost * nseqpages; /* Add scanning CPU costs */ get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost); @@ -1429,20 +1429,41 @@ cost_tidrangescan(Path *path, PlannerInfo *root, * can't be removed, this is a mistake and we're going to underestimate * the CPU cost a bit.) */ - startup_cost += qpqual_cost.startup + tid_qual_cost.per_tuple; + startup_cost = qpqual_cost.startup + tid_qual_cost.per_tuple; cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple - tid_qual_cost.per_tuple; - run_cost += cpu_per_tuple * ntuples; + cpu_run_cost = cpu_per_tuple * ntuples; /* tlist eval costs are paid per output row, not per tuple scanned */ startup_cost += path->pathtarget->cost.startup; - run_cost += path->pathtarget->cost.per_tuple * path->rows; + cpu_run_cost += path->pathtarget->cost.per_tuple * path->rows; + + /* Adjust costing for parallelism, if used. */ + if (path->parallel_workers > 0) + { + double parallel_divisor = get_parallel_divisor(path); + + /* The CPU cost is divided among all the workers. */ + cpu_run_cost /= parallel_divisor; - /* we should not generate this path type when enable_tidscan=false */ - Assert(enable_tidscan); - path->disabled_nodes = 0; + /* + * In the case of a parallel plan, the row count needs to represent + * the number of tuples processed per worker. + */ + path->rows = clamp_row_est(path->rows / parallel_divisor); + } + + /* + * We should not generate this path type when PGS_TIDSCAN is unset, but we + * might need to disable this path due to PGS_CONSIDER_NONPARTIAL. + */ + Assert((baserel->pgs_mask & PGS_TIDSCAN) != 0); + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; - path->total_cost = startup_cost + run_cost; + path->total_cost = startup_cost + cpu_run_cost + disk_run_cost; } /* @@ -1463,6 +1484,7 @@ cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root, List *qpquals; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = 0; /* Should only be applied to base relations that are subqueries */ Assert(baserel->relid > 0); @@ -1493,7 +1515,10 @@ cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root, * SubqueryScan node, plus cpu_tuple_cost to account for selection and * projection overhead. */ - path->path.disabled_nodes = path->subpath->disabled_nodes; + if (path->path.parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->path.disabled_nodes = path->subpath->disabled_nodes + + (((baserel->pgs_mask & enable_mask) != enable_mask) ? 1 : 0); path->path.startup_cost = path->subpath->startup_cost; path->path.total_cost = path->subpath->total_cost; @@ -1544,6 +1569,7 @@ cost_functionscan(Path *path, PlannerInfo *root, Cost cpu_per_tuple; RangeTblEntry *rte; QualCost exprcost; + uint64 enable_mask = 0; /* Should only be applied to base relations that are functions */ Assert(baserel->relid > 0); @@ -1584,7 +1610,10 @@ cost_functionscan(Path *path, PlannerInfo *root, startup_cost += path->pathtarget->cost.startup; run_cost += path->pathtarget->cost.per_tuple * path->rows; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1606,6 +1635,7 @@ cost_tablefuncscan(Path *path, PlannerInfo *root, Cost cpu_per_tuple; RangeTblEntry *rte; QualCost exprcost; + uint64 enable_mask = 0; /* Should only be applied to base relations that are functions */ Assert(baserel->relid > 0); @@ -1641,7 +1671,10 @@ cost_tablefuncscan(Path *path, PlannerInfo *root, startup_cost += path->pathtarget->cost.startup; run_cost += path->pathtarget->cost.per_tuple * path->rows; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1661,6 +1694,7 @@ cost_valuesscan(Path *path, PlannerInfo *root, Cost run_cost = 0; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = 0; /* Should only be applied to base relations that are values lists */ Assert(baserel->relid > 0); @@ -1689,7 +1723,10 @@ cost_valuesscan(Path *path, PlannerInfo *root, startup_cost += path->pathtarget->cost.startup; run_cost += path->pathtarget->cost.per_tuple * path->rows; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1712,6 +1749,7 @@ cost_ctescan(Path *path, PlannerInfo *root, Cost run_cost = 0; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = 0; /* Should only be applied to base relations that are CTEs */ Assert(baserel->relid > 0); @@ -1737,7 +1775,10 @@ cost_ctescan(Path *path, PlannerInfo *root, startup_cost += path->pathtarget->cost.startup; run_cost += path->pathtarget->cost.per_tuple * path->rows; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1754,6 +1795,7 @@ cost_namedtuplestorescan(Path *path, PlannerInfo *root, Cost run_cost = 0; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = 0; /* Should only be applied to base relations that are Tuplestores */ Assert(baserel->relid > 0); @@ -1775,7 +1817,10 @@ cost_namedtuplestorescan(Path *path, PlannerInfo *root, cpu_per_tuple += cpu_tuple_cost + qpqual_cost.per_tuple; run_cost += cpu_per_tuple * baserel->tuples; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1792,6 +1837,7 @@ cost_resultscan(Path *path, PlannerInfo *root, Cost run_cost = 0; QualCost qpqual_cost; Cost cpu_per_tuple; + uint64 enable_mask = 0; /* Should only be applied to RTE_RESULT base relations */ Assert(baserel->relid > 0); @@ -1810,7 +1856,10 @@ cost_resultscan(Path *path, PlannerInfo *root, cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple; run_cost += cpu_per_tuple * baserel->tuples; - path->disabled_nodes = 0; + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + path->disabled_nodes = + (baserel->pgs_mask & enable_mask) != enable_mask ? 1 : 0; path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -1828,6 +1877,7 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm) Cost startup_cost; Cost total_cost; double total_rows; + uint64 enable_mask = 0; /* We probably have decent estimates for the non-recursive term */ startup_cost = nrterm->startup_cost; @@ -1850,7 +1900,10 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm) */ total_cost += cpu_tuple_cost * total_rows; - runion->disabled_nodes = nrterm->disabled_nodes + rterm->disabled_nodes; + if (runion->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + runion->disabled_nodes = + (runion->parent->pgs_mask & enable_mask) != enable_mask ? 1 : 0; runion->startup_cost = startup_cost; runion->total_cost = total_cost; runion->rows = total_rows; @@ -2120,7 +2173,11 @@ cost_incremental_sort(Path *path, path->rows = input_tuples; - /* should not generate these paths when enable_incremental_sort=false */ + /* + * We should not generate these paths when enable_incremental_sort=false. + * We can ignore PGS_CONSIDER_NONPARTIAL here, because if it's relevant, + * it will have already affected the input path. + */ Assert(enable_incremental_sort); path->disabled_nodes = input_disabled_nodes; @@ -2158,6 +2215,10 @@ cost_sort(Path *path, PlannerInfo *root, startup_cost += input_cost; + /* + * We can ignore PGS_CONSIDER_NONPARTIAL here, because if it's relevant, + * it will have already affected the input path. + */ path->rows = tuples; path->disabled_nodes = input_disabled_nodes + (enable_sort ? 0 : 1); path->startup_cost = startup_cost; @@ -2189,7 +2250,7 @@ append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers) * whichever is less. */ arrlen = Min(parallel_workers, numpaths); - costarr = (Cost *) palloc(sizeof(Cost) * arrlen); + costarr = palloc_array(Cost, arrlen); /* The first few paths will each be claimed by a different worker. */ path_index = 0; @@ -2247,11 +2308,17 @@ append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers) * Determines and returns the cost of an Append node. */ void -cost_append(AppendPath *apath) +cost_append(AppendPath *apath, PlannerInfo *root) { + RelOptInfo *rel = apath->path.parent; ListCell *l; + uint64 enable_mask = PGS_APPEND; + + if (apath->path.parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; - apath->path.disabled_nodes = 0; + apath->path.disabled_nodes = + (rel->pgs_mask & enable_mask) == enable_mask ? 0 : 1; apath->path.startup_cost = 0; apath->path.total_cost = 0; apath->path.rows = 0; @@ -2309,26 +2376,52 @@ cost_append(AppendPath *apath) foreach(l, apath->subpaths) { Path *subpath = (Path *) lfirst(l); - Path sort_path; /* dummy for result of cost_sort */ + int presorted_keys; + Path sort_path; /* dummy for result of + * cost_sort/cost_incremental_sort */ - if (!pathkeys_contained_in(pathkeys, subpath->pathkeys)) + if (!pathkeys_count_contained_in(pathkeys, subpath->pathkeys, + &presorted_keys)) { /* * We'll need to insert a Sort node, so include costs for - * that. We can use the parent's LIMIT if any, since we + * that. We choose to use incremental sort if it is + * enabled and there are presorted keys; otherwise we use + * full sort. + * + * We can use the parent's LIMIT if any, since we * certainly won't pull more than that many tuples from * any child. */ - cost_sort(&sort_path, - NULL, /* doesn't currently need root */ - pathkeys, - subpath->disabled_nodes, - subpath->total_cost, - subpath->rows, - subpath->pathtarget->width, - 0.0, - work_mem, - apath->limit_tuples); + if (enable_incremental_sort && presorted_keys > 0) + { + cost_incremental_sort(&sort_path, + root, + pathkeys, + presorted_keys, + subpath->disabled_nodes, + subpath->startup_cost, + subpath->total_cost, + subpath->rows, + subpath->pathtarget->width, + 0.0, + work_mem, + apath->limit_tuples); + } + else + { + cost_sort(&sort_path, + root, + pathkeys, + subpath->disabled_nodes, + subpath->total_cost, + subpath->rows, + subpath->pathtarget->width, + 0.0, + work_mem, + apath->limit_tuples); + } + subpath = &sort_path; } @@ -2435,11 +2528,16 @@ cost_merge_append(Path *path, PlannerInfo *root, Cost input_startup_cost, Cost input_total_cost, double tuples) { + RelOptInfo *rel = path->parent; Cost startup_cost = 0; Cost run_cost = 0; Cost comparison_cost; double N; double logN; + uint64 enable_mask = PGS_MERGE_APPEND; + + if (path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; /* * Avoid log(0)... @@ -2462,7 +2560,9 @@ cost_merge_append(Path *path, PlannerInfo *root, */ run_cost += cpu_tuple_cost * APPEND_CPU_COST_MULTIPLIER * tuples; - path->disabled_nodes = input_disabled_nodes; + path->disabled_nodes = + (rel->pgs_mask & enable_mask) == enable_mask ? 0 : 1; + path->disabled_nodes += input_disabled_nodes; path->startup_cost = startup_cost + input_startup_cost; path->total_cost = startup_cost + run_cost + input_total_cost; } @@ -2481,7 +2581,7 @@ cost_merge_append(Path *path, PlannerInfo *root, */ void cost_material(Path *path, - int input_disabled_nodes, + bool enabled, int input_disabled_nodes, Cost input_startup_cost, Cost input_total_cost, double tuples, int width) { @@ -2519,7 +2619,7 @@ cost_material(Path *path, run_cost += seq_page_cost * npages; } - path->disabled_nodes = input_disabled_nodes + (enable_material ? 0 : 1); + path->disabled_nodes = input_disabled_nodes + (enabled ? 0 : 1); path->startup_cost = startup_cost; path->total_cost = startup_cost + run_cost; } @@ -2546,13 +2646,13 @@ cost_memoize_rescan(PlannerInfo *root, MemoizePath *mpath, Cost input_startup_cost = mpath->subpath->startup_cost; Cost input_total_cost = mpath->subpath->total_cost; double tuples = mpath->subpath->rows; - double calls = mpath->calls; + Cardinality est_calls = mpath->est_calls; int width = mpath->subpath->pathtarget->width; double hash_mem_bytes; double est_entry_bytes; - double est_cache_entries; - double ndistinct; + Cardinality est_cache_entries; + Cardinality ndistinct; double evict_ratio; double hit_ratio; Cost startup_cost; @@ -2578,7 +2678,7 @@ cost_memoize_rescan(PlannerInfo *root, MemoizePath *mpath, est_cache_entries = floor(hash_mem_bytes / est_entry_bytes); /* estimate on the distinct number of parameter values */ - ndistinct = estimate_num_groups(root, mpath->param_exprs, calls, NULL, + ndistinct = estimate_num_groups(root, mpath->param_exprs, est_calls, NULL, &estinfo); /* @@ -2590,7 +2690,10 @@ cost_memoize_rescan(PlannerInfo *root, MemoizePath *mpath, * certainly mean a MemoizePath will never survive add_path(). */ if ((estinfo.flags & SELFLAG_USED_DEFAULT) != 0) - ndistinct = calls; + ndistinct = est_calls; + + /* Remember the ndistinct estimate for EXPLAIN */ + mpath->est_unique_keys = ndistinct; /* * Since we've already estimated the maximum number of entries we can @@ -2618,9 +2721,12 @@ cost_memoize_rescan(PlannerInfo *root, MemoizePath *mpath, * must look at how many scans are estimated in total for this node and * how many of those scans we expect to get a cache hit. */ - hit_ratio = ((calls - ndistinct) / calls) * + hit_ratio = ((est_calls - ndistinct) / est_calls) * (est_cache_entries / Max(ndistinct, est_cache_entries)); + /* Remember the hit ratio estimate for EXPLAIN */ + mpath->est_hit_ratio = hit_ratio; + Assert(hit_ratio >= 0 && hit_ratio <= 1.0); /* @@ -3265,7 +3371,7 @@ cost_group(Path *path, PlannerInfo *root, */ void initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace, - JoinType jointype, + JoinType jointype, uint64 enable_mask, Path *outer_path, Path *inner_path, JoinPathExtraData *extra) { @@ -3279,7 +3385,7 @@ initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace, Cost inner_rescan_run_cost; /* Count up disabled nodes. */ - disabled_nodes = enable_nestloop ? 0 : 1; + disabled_nodes = (extra->pgs_mask & enable_mask) == enable_mask ? 0 : 1; disabled_nodes += inner_path->disabled_nodes; disabled_nodes += outer_path->disabled_nodes; @@ -3679,7 +3785,19 @@ initial_cost_mergejoin(PlannerInfo *root, JoinCostWorkspace *workspace, Assert(outerstartsel <= outerendsel); Assert(innerstartsel <= innerendsel); - disabled_nodes = enable_mergejoin ? 0 : 1; + /* + * We don't decide whether to materialize the inner path until we get to + * final_cost_mergejoin(), so we don't know whether to check the pgs_mask + * against PGS_MERGEJOIN_PLAIN or PGS_MERGEJOIN_MATERIALIZE. Instead, we + * just account for any child nodes here and assume that this node is not + * itself disabled; we can sort out the details in final_cost_mergejoin(). + * + * (We could be more precise here by setting disabled_nodes to 1 at this + * stage if both PGS_MERGEJOIN_PLAIN and PGS_MERGEJOIN_MATERIALIZE are + * disabled, but that seems to against the idea of making this function + * produce a quick, optimistic approximation of the final cost.) + */ + disabled_nodes = 0; /* cost of source data */ @@ -3858,9 +3976,7 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, double mergejointuples, rescannedtuples; double rescanratio; - - /* Set the number of disabled nodes. */ - path->jpath.path.disabled_nodes = workspace->disabled_nodes; + uint64 enable_mask = 0; /* Protect some assumptions below that rowcounts aren't zero */ if (inner_path_rows <= 0) @@ -3934,10 +4050,12 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, * when we should not. Can we do better without expensive selectivity * computations? * - * The whole issue is moot if we are working from a unique-ified outer - * input, or if we know we don't need to mark/restore at all. + * The whole issue is moot if we know we don't need to mark/restore at + * all, or if we are working from a unique-ified outer input. */ - if (IsA(outer_path, UniquePath) || path->skip_mark_restore) + if (path->skip_mark_restore || + RELATION_WAS_MADE_UNIQUE(outer_path->parent, extra->sjinfo, + path->jpath.jointype)) rescannedtuples = 0; else { @@ -3988,16 +4106,20 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, path->materialize_inner = false; /* - * Prefer materializing if it looks cheaper, unless the user has asked to - * suppress materialization. + * If merge joins with materialization are enabled, then choose + * materialization if either (a) it looks cheaper or (b) merge joins + * without materialization are disabled. */ - else if (enable_material && mat_inner_cost < bare_inner_cost) + else if ((extra->pgs_mask & PGS_MERGEJOIN_MATERIALIZE) != 0 && + (mat_inner_cost < bare_inner_cost || + (extra->pgs_mask & PGS_MERGEJOIN_PLAIN) == 0)) path->materialize_inner = true; /* - * Even if materializing doesn't look cheaper, we *must* do it if the - * inner path is to be used directly (without sorting) and it doesn't - * support mark/restore. + * Regardless of what plan shapes are enabled and what the costs seem to + * be, we *must* materialize it if the inner path is to be used directly + * (without sorting) and it doesn't support mark/restore. Planner failure + * is not an option! * * Since the inner side must be ordered, and only Sorts and IndexScans can * create order to begin with, and they both support mark/restore, you @@ -4005,10 +4127,6 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, * merge joins can *preserve* the order of their inputs, so they can be * selected as the input of a mergejoin, and they don't support * mark/restore at present. - * - * We don't test the value of enable_material here, because - * materialization is required for correctness in this case, and turning - * it off does not entitle us to deliver an invalid plan. */ else if (innersortkeys == NIL && !ExecSupportsMarkRestore(inner_path)) @@ -4022,10 +4140,11 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, * though. * * Since materialization is a performance optimization in this case, - * rather than necessary for correctness, we skip it if enable_material is - * off. + * rather than necessary for correctness, we skip it if materialization is + * switched off. */ - else if (enable_material && innersortkeys != NIL && + else if ((extra->pgs_mask & PGS_MERGEJOIN_MATERIALIZE) != 0 && + innersortkeys != NIL && relation_byte_size(inner_path_rows, inner_path->pathtarget->width) > work_mem * (Size) 1024) @@ -4033,11 +4152,29 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, else path->materialize_inner = false; - /* Charge the right incremental cost for the chosen case */ + /* Get the number of disabled nodes, not yet including this one. */ + path->jpath.path.disabled_nodes = workspace->disabled_nodes; + + /* + * Charge the right incremental cost for the chosen case, and update + * enable_mask as appropriate. + */ if (path->materialize_inner) + { run_cost += mat_inner_cost; + enable_mask |= PGS_MERGEJOIN_MATERIALIZE; + } else + { run_cost += bare_inner_cost; + enable_mask |= PGS_MERGEJOIN_PLAIN; + } + + /* Incremental count of disabled nodes if this node is disabled. */ + if (path->jpath.path.parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; + if ((extra->pgs_mask & enable_mask) != enable_mask) + ++path->jpath.path.disabled_nodes; /* CPU costs */ @@ -4113,7 +4250,7 @@ cached_scansel(PlannerInfo *root, RestrictInfo *rinfo, PathKey *pathkey) /* Cache the result in suitably long-lived workspace */ oldcontext = MemoryContextSwitchTo(root->planner_cxt); - cache = (MergeScanSelCache *) palloc(sizeof(MergeScanSelCache)); + cache = palloc_object(MergeScanSelCache); cache->opfamily = pathkey->pk_opfamily; cache->collation = pathkey->pk_eclass->ec_collation; cache->cmptype = pathkey->pk_cmptype; @@ -4175,9 +4312,13 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace, int numbatches; int num_skew_mcvs; size_t space_allowed; /* unused */ + uint64 enable_mask = PGS_HASHJOIN; + + if (outer_path->parallel_workers == 0) + enable_mask |= PGS_CONSIDER_NONPARTIAL; /* Count up disabled nodes. */ - disabled_nodes = enable_hashjoin ? 0 : 1; + disabled_nodes = (extra->pgs_mask & enable_mask) == enable_mask ? 0 : 1; disabled_nodes += inner_path->disabled_nodes; disabled_nodes += outer_path->disabled_nodes; @@ -4332,10 +4473,11 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path, * because we avoid contaminating the cache with a value that's wrong for * non-unique-ified paths. */ - if (IsA(inner_path, UniquePath)) + if (RELATION_WAS_MADE_UNIQUE(inner_path->parent, extra->sjinfo, + path->jpath.jointype)) { innerbucketsize = 1.0 / virtualbuckets; - innermcvfreq = 0.0; + innermcvfreq = 1.0 / inner_path_rows_total; } else { @@ -4403,7 +4545,8 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path, if (innerbucketsize > thisbucketsize) innerbucketsize = thisbucketsize; - if (innermcvfreq > thismcvfreq) + /* Disregard zero for MCV freq, it means we have no data */ + if (thismcvfreq > 0.0 && innermcvfreq > thismcvfreq) innermcvfreq = thismcvfreq; } } @@ -4535,10 +4678,24 @@ cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan) { QualCost sp_cost; - /* Figure any cost for evaluating the testexpr */ + /* + * Figure any cost for evaluating the testexpr. + * + * Usually, SubPlan nodes are built very early, before we have constructed + * any RelOptInfos for the parent query level, which means the parent root + * does not yet contain enough information to safely consult statistics. + * Therefore, we pass root as NULL here. cost_qual_eval() is already + * well-equipped to handle a NULL root. + * + * One exception is SubPlan nodes built for the initplans of MIN/MAX + * aggregates from indexes (cf. SS_make_initplan_from_plan). In this + * case, having a NULL root is safe because testexpr will be NULL. + * Besides, an initplan will by definition not consult anything from the + * parent plan. + */ cost_qual_eval(&sp_cost, make_ands_implicit((Expr *) subplan->testexpr), - root); + NULL); if (subplan->useHashTable) { @@ -4604,6 +4761,7 @@ cost_subplan(PlannerInfo *root, SubPlan *subplan, Plan *plan) sp_cost.per_tuple += plan->startup_cost; } + subplan->disabled_nodes = plan->disabled_nodes; subplan->startup_cost = sp_cost.startup; subplan->per_call_cost = sp_cost.per_tuple; } diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c index 441f12f6c50cf..e3697df51a244 100644 --- a/src/backend/optimizer/path/equivclass.c +++ b/src/backend/optimizer/path/equivclass.c @@ -6,7 +6,7 @@ * See src/backend/optimizer/README for discussion of EquivalenceClasses. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -1007,8 +1007,7 @@ find_computable_ec_member(PlannerInfo *root, exprvars = pull_var_clause((Node *) exprs, PVC_INCLUDE_AGGREGATES | PVC_INCLUDE_WINDOWFUNCS | - PVC_INCLUDE_PLACEHOLDERS | - PVC_INCLUDE_CONVERTROWTYPES); + PVC_INCLUDE_PLACEHOLDERS); setup_eclass_member_iterator(&it, ec, relids); while ((em = eclass_member_iterator_next(&it)) != NULL) diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c index 601354ea3e056..f76a5373c4bea 100644 --- a/src/backend/optimizer/path/indxpath.c +++ b/src/backend/optimizer/path/indxpath.c @@ -4,7 +4,7 @@ * Routines to determine which indexes are usable for scanning a * given relation, and create Paths accordingly. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -15,10 +15,9 @@ */ #include "postgres.h" -#include - #include "access/stratnum.h" #include "access/sysattr.h" +#include "access/transam.h" #include "catalog/pg_am.h" #include "catalog/pg_amop.h" #include "catalog/pg_operator.h" @@ -31,6 +30,7 @@ #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/placeholder.h" #include "optimizer/prep.h" #include "optimizer/restrictinfo.h" #include "utils/lsyscache.h" @@ -1187,7 +1187,7 @@ typedef struct Oid inputcollid; /* OID of the OpClause input collation */ int argindex; /* index of the clause in the list of * arguments */ - int groupindex; /* value of argindex for the fist clause in + int groupindex; /* value of argindex for the first clause in * the group of similar clauses */ } OrArgIndexMatch; @@ -1290,7 +1290,7 @@ group_similar_or_args(PlannerInfo *root, RelOptInfo *rel, RestrictInfo *rinfo) * which will be used to sort these arguments at the next step. */ i = -1; - matches = (OrArgIndexMatch *) palloc(sizeof(OrArgIndexMatch) * n); + matches = palloc_array(OrArgIndexMatch, n); foreach(lc, orargs) { Node *arg = lfirst(lc); @@ -1852,8 +1852,7 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths) * same set of clauses; keep only the cheapest-to-scan of any such groups. * The surviving paths are put into an array for qsort'ing. */ - pathinfoarray = (PathClauseUsage **) - palloc(npaths * sizeof(PathClauseUsage *)); + pathinfoarray = palloc_array(PathClauseUsage *, npaths); clauselist = NIL; npaths = 0; foreach(l, paths) @@ -2089,7 +2088,7 @@ classify_index_clause_usage(Path *path, List **clauselist) Bitmapset *clauseids; ListCell *lc; - result = (PathClauseUsage *) palloc(sizeof(PathClauseUsage)); + result = palloc_object(PathClauseUsage); result->path = path; /* Recursively find the quals and preds used by the path */ @@ -2232,8 +2231,8 @@ check_index_only(RelOptInfo *rel, IndexOptInfo *index) ListCell *lc; int i; - /* Index-only scans must be enabled */ - if (!enable_indexonlyscan) + /* If we're not allowed to consider index-only scans, give up now */ + if ((rel->pgs_mask & PGS_CONSIDER_INDEXONLY) == 0) return false; /* @@ -3289,8 +3288,8 @@ match_rowcompare_to_indexcol(PlannerInfo *root, * * In this routine, we attempt to transform a list of OR-clause args into a * single SAOP expression matching the target index column. On success, - * return an IndexClause, containing the transformed expression or NULL, - * if failed. + * return an IndexClause containing the transformed expression. + * Return NULL if the transformation fails. */ static IndexClause * match_orclause_to_indexcol(PlannerInfo *root, @@ -3298,85 +3297,59 @@ match_orclause_to_indexcol(PlannerInfo *root, int indexcol, IndexOptInfo *index) { - ListCell *lc; BoolExpr *orclause = (BoolExpr *) rinfo->orclause; - Node *indexExpr = NULL; List *consts = NIL; - ScalarArrayOpExpr *saopexpr = NULL; + Node *indexExpr = NULL; Oid matchOpno = InvalidOid; - IndexClause *iclause; Oid consttype = InvalidOid; Oid arraytype = InvalidOid; Oid inputcollid = InvalidOid; bool firstTime = true; bool haveNonConst = false; Index indexRelid = index->rel->relid; + ScalarArrayOpExpr *saopexpr; + IndexClause *iclause; + ListCell *lc; - Assert(IsA(orclause, BoolExpr)); - Assert(orclause->boolop == OR_EXPR); - - /* Ignore index if it doesn't support SAOP clauses */ + /* Forget it if index doesn't support SAOP clauses */ if (!index->amsearcharray) return NULL; /* * Try to convert a list of OR-clauses to a single SAOP expression. Each * OR entry must be in the form: (indexkey operator constant) or (constant - * operator indexkey). Operators of all the entries must match. To be - * effective, give up on the first non-matching entry. Exit is - * implemented as a break from the loop, which is catched afterwards. + * operator indexkey). Operators of all the entries must match. On + * discovery of anything unsupported, we give up by breaking out of the + * loop immediately and returning NULL. */ foreach(lc, orclause->args) { - RestrictInfo *subRinfo; + RestrictInfo *subRinfo = (RestrictInfo *) lfirst(lc); OpExpr *subClause; Oid opno; Node *leftop, *rightop; Node *constExpr; - if (!IsA(lfirst(lc), RestrictInfo)) + /* If it's not a RestrictInfo (i.e. it's a sub-AND), we can't use it */ + if (!IsA(subRinfo, RestrictInfo)) break; - subRinfo = (RestrictInfo *) lfirst(lc); - - /* Only operator clauses can match */ + /* Only operator clauses can match */ if (!IsA(subRinfo->clause, OpExpr)) break; subClause = (OpExpr *) subRinfo->clause; opno = subClause->opno; - /* Only binary operators can match */ + /* Only binary operators can match */ if (list_length(subClause->args) != 2) break; - /* - * The parameters below must match between sub-rinfo and its parent as - * make_restrictinfo() fills them with the same values, and further - * modifications are also the same for the whole subtree. However, - * still make a sanity check. - */ - Assert(subRinfo->is_pushed_down == rinfo->is_pushed_down); - Assert(subRinfo->is_clone == rinfo->is_clone); - Assert(subRinfo->security_level == rinfo->security_level); - Assert(bms_equal(subRinfo->incompatible_relids, rinfo->incompatible_relids)); - Assert(bms_equal(subRinfo->outer_relids, rinfo->outer_relids)); - - /* - * Also, check that required_relids in sub-rinfo is subset of parent's - * required_relids. - */ - Assert(bms_is_subset(subRinfo->required_relids, rinfo->required_relids)); - - /* Only the operator returning a boolean suit the transformation. */ - if (get_op_rettype(opno) != BOOLOID) - break; - /* * Check for clauses of the form: (indexkey operator constant) or - * (constant operator indexkey). See match_clause_to_indexcol's notes - * about const-ness. + * (constant operator indexkey). These tests should agree with + * match_opclause_to_indexcol. */ leftop = (Node *) linitial(subClause->args); rightop = (Node *) lsecond(subClause->args); @@ -3405,22 +3378,6 @@ match_orclause_to_indexcol(PlannerInfo *root, break; } - /* - * Ignore any RelabelType node above the operands. This is needed to - * be able to apply indexscanning in binary-compatible-operator cases. - * Note: we can assume there is at most one RelabelType node; - * eval_const_expressions() will have simplified if more than one. - */ - if (IsA(constExpr, RelabelType)) - constExpr = (Node *) ((RelabelType *) constExpr)->arg; - if (IsA(indexExpr, RelabelType)) - indexExpr = (Node *) ((RelabelType *) indexExpr)->arg; - - /* Forbid transformation for composite types, records. */ - if (type_is_rowtype(exprType(constExpr)) || - type_is_rowtype(exprType(indexExpr))) - break; - /* * Save information about the operator, type, and collation for the * first matching qual. Then, check that subsequent quals match the @@ -3438,54 +3395,71 @@ match_orclause_to_indexcol(PlannerInfo *root, * the expression collation matches the index collation. Also, * there must be an array type to construct an array later. */ - if (!IndexCollMatchesExprColl(index->indexcollations[indexcol], inputcollid) || + if (!IndexCollMatchesExprColl(index->indexcollations[indexcol], + inputcollid) || !op_in_opfamily(matchOpno, index->opfamily[indexcol]) || !OidIsValid(arraytype)) break; + + /* + * Disallow if either type is RECORD, mainly because we can't be + * positive that all the RHS expressions are the same record type. + */ + if (consttype == RECORDOID || exprType(indexExpr) == RECORDOID) + break; + firstTime = false; } else { - if (opno != matchOpno || + if (matchOpno != opno || inputcollid != subClause->inputcollid || consttype != exprType(constExpr)) break; } /* - * Check if our list of constants in match_clause_to_indexcol's - * understanding of const-ness have something other than Const. + * The righthand inputs don't necessarily have to be plain Consts, but + * make_SAOP_expr needs to know if any are not. */ if (!IsA(constExpr, Const)) haveNonConst = true; + consts = lappend(consts, constExpr); } /* - * Catch the break from the loop above. Normally, a foreach() loop ends - * up with a NULL list cell. A non-NULL list cell indicates a break from - * the foreach() loop. Free the consts list and return NULL then. + * Handle failed conversion from breaking out of the loop because of an + * unsupported qual. Also check that we have an indexExpr, just in case + * the OR list was somehow empty (it shouldn't be). Return NULL to + * indicate the conversion failed. */ - if (lc != NULL) + if (lc != NULL || indexExpr == NULL) { - list_free(consts); + list_free(consts); /* might as well */ return NULL; } + /* + * Build the new SAOP node. We use the indexExpr from the last OR arm; + * since all the arms passed match_index_to_operand, it shouldn't matter + * which one we use. But using "inputcollid" twice is a bit of a cheat: + * we might end up with an array Const node that is labeled with a + * collation despite its elements being of a noncollatable type. But + * nothing is likely to complain about that, so we don't bother being more + * accurate. + */ saopexpr = make_SAOP_expr(matchOpno, indexExpr, consttype, inputcollid, inputcollid, consts, haveNonConst); + Assert(saopexpr != NULL); /* - * Finally, build an IndexClause based on the SAOP node. Use - * make_simple_restrictinfo() to get RestrictInfo with clean selectivity - * estimations, because they may differ from the estimation made for an OR - * clause. Although it is not a lossy expression, keep the original rinfo - * in iclause->rinfo as prescribed. + * Finally, build an IndexClause based on the SAOP node. It's not lossy. */ iclause = makeNode(IndexClause); iclause->rinfo = rinfo; iclause->indexquals = list_make1(make_simple_restrictinfo(root, - &saopexpr->xpr)); + (Expr *) saopexpr)); iclause->lossy = false; iclause->indexcol = indexcol; iclause->indexcols = NIL; @@ -4075,6 +4049,16 @@ check_index_predicates(PlannerInfo *root, RelOptInfo *rel) if (is_target_rel) continue; + /* + * If index is !amoptionalkey, also leave indrestrictinfo as set + * above. Otherwise we risk removing all quals for the first index + * key and then not being able to generate an indexscan at all. It + * would be better to be more selective, but we've not yet identified + * which if any of the quals match the first index key. + */ + if (!index->amoptionalkey) + continue; + /* Else compute indrestrictinfo as the non-implied quals */ index->indrestrictinfo = NIL; foreach(lcr, rel->baserestrictinfo) @@ -4142,47 +4126,26 @@ ec_member_matches_indexcol(PlannerInfo *root, RelOptInfo *rel, * a set of equality conditions, because the conditions constrain all * columns of some unique index. * - * The conditions can be represented in either or both of two ways: - * 1. A list of RestrictInfo nodes, where the caller has already determined - * that each condition is a mergejoinable equality with an expression in - * this relation on one side, and an expression not involving this relation - * on the other. The transient outer_is_left flag is used to identify which - * side we should look at: left side if outer_is_left is false, right side - * if it is true. - * 2. A list of expressions in this relation, and a corresponding list of - * equality operators. The caller must have already checked that the operators - * represent equality. (Note: the operators could be cross-type; the - * expressions should correspond to their RHS inputs.) + * The conditions are provided as a list of RestrictInfo nodes, where the + * caller has already determined that each condition is a mergejoinable + * equality with an expression in this relation on one side, and an + * expression not involving this relation on the other. The transient + * outer_is_left flag is used to identify which side we should look at: + * left side if outer_is_left is false, right side if it is true. * * The caller need only supply equality conditions arising from joins; * this routine automatically adds in any usable baserestrictinfo clauses. * (Note that the passed-in restrictlist will be destructively modified!) + * + * If extra_clauses isn't NULL, return baserestrictinfo clauses which were used + * to derive uniqueness. */ bool relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel, - List *restrictlist, - List *exprlist, List *oprlist) -{ - return relation_has_unique_index_ext(root, rel, restrictlist, - exprlist, oprlist, NULL); -} - -/* - * relation_has_unique_index_ext - * Same as relation_has_unique_index_for(), but supports extra_clauses - * parameter. If extra_clauses isn't NULL, return baserestrictinfo clauses - * which were used to derive uniqueness. - */ -bool -relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, - List *restrictlist, - List *exprlist, List *oprlist, - List **extra_clauses) + List *restrictlist, List **extra_clauses) { ListCell *ic; - Assert(list_length(exprlist) == list_length(oprlist)); - /* Short-circuit if no indexes... */ if (rel->indexlist == NIL) return false; @@ -4225,7 +4188,7 @@ relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, } /* Short-circuit the easy case */ - if (restrictlist == NIL && exprlist == NIL) + if (restrictlist == NIL) return false; /* Examine each index of the relation ... */ @@ -4247,14 +4210,12 @@ relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, continue; /* - * Try to find each index column in the lists of conditions. This is + * Try to find each index column in the list of conditions. This is * O(N^2) or worse, but we expect all the lists to be short. */ for (c = 0; c < ind->nkeycolumns; c++) { - bool matched = false; ListCell *lc; - ListCell *lc2; foreach(lc, restrictlist) { @@ -4284,8 +4245,6 @@ relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, if (match_index_to_operand(rexpr, c, ind)) { - matched = true; /* column is unique */ - if (bms_membership(rinfo->clause_relids) == BMS_SINGLETON) { MemoryContext oldMemCtx = @@ -4303,43 +4262,11 @@ relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, MemoryContextSwitchTo(oldMemCtx); } - break; + break; /* found a match; column is unique */ } } - if (matched) - continue; - - forboth(lc, exprlist, lc2, oprlist) - { - Node *expr = (Node *) lfirst(lc); - Oid opr = lfirst_oid(lc2); - - /* See if the expression matches the index key */ - if (!match_index_to_operand(expr, c, ind)) - continue; - - /* - * The equality operator must be a member of the index - * opfamily, else it is not asserting the right kind of - * equality behavior for this index. We assume the caller - * determined it is an equality operator, so we don't need to - * check any more tightly than this. - */ - if (!op_in_opfamily(opr, ind->opfamily[c])) - continue; - - /* - * XXX at some point we may need to check collations here too. - * For the moment we assume all collations reduce to the same - * notion of equality. - */ - - matched = true; /* column is unique */ - break; - } - - if (!matched) + if (lc == NULL) break; /* no match; this index doesn't help us */ } @@ -4430,12 +4357,23 @@ match_index_to_operand(Node *operand, int indkey; /* - * Ignore any RelabelType node above the operand. This is needed to be - * able to apply indexscanning in binary-compatible-operator cases. Note: - * we can assume there is at most one RelabelType node; - * eval_const_expressions() will have simplified if more than one. + * Ignore any PlaceHolderVar node contained in the operand. This is + * needed to be able to apply indexscanning in cases where the operand (or + * a subtree) has been wrapped in PlaceHolderVars to enforce separate + * identity or as a result of outer joins. + */ + operand = strip_noop_phvs(operand); + + /* + * Ignore any RelabelType node above the operand. This is needed to be + * able to apply indexscanning in binary-compatible-operator cases. + * + * Note: we must handle nested RelabelType nodes here. While + * eval_const_expressions() will have simplified them to at most one + * layer, our prior stripping of PlaceHolderVars may have brought separate + * RelabelTypes into adjacency. */ - if (operand && IsA(operand, RelabelType)) + while (operand && IsA(operand, RelabelType)) operand = (Node *) ((RelabelType *) operand)->arg; indkey = index->indexkeys[indexcol]; diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c index 26f0336f1e409..713283a73aa41 100644 --- a/src/backend/optimizer/path/joinpath.c +++ b/src/backend/optimizer/path/joinpath.c @@ -3,7 +3,7 @@ * joinpath.c * Routines to find all possible paths for processing a set of joins * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,8 +14,6 @@ */ #include "postgres.h" -#include - #include "executor/executor.h" #include "foreign/fdwapi.h" #include "nodes/nodeFuncs.h" @@ -29,8 +27,9 @@ #include "utils/lsyscache.h" #include "utils/typcache.h" -/* Hook for plugins to get control in add_paths_to_joinrel() */ +/* Hooks for plugins to get control in add_paths_to_joinrel() */ set_join_pathlist_hook_type set_join_pathlist_hook = NULL; +join_path_setup_hook_type join_path_setup_hook = NULL; /* * Paths parameterized by a parent rel can be considered to be parameterized @@ -112,12 +111,12 @@ static void generate_mergejoin_paths(PlannerInfo *root, * "flipped around" if we are considering joining the rels in the opposite * direction from what's indicated in sjinfo. * - * Also, this routine and others in this module accept the special JoinTypes - * JOIN_UNIQUE_OUTER and JOIN_UNIQUE_INNER to indicate that we should - * unique-ify the outer or inner relation and then apply a regular inner - * join. These values are not allowed to propagate outside this module, - * however. Path cost estimation code may need to recognize that it's - * dealing with such a case --- the combination of nominal jointype INNER + * Also, this routine accepts the special JoinTypes JOIN_UNIQUE_OUTER and + * JOIN_UNIQUE_INNER to indicate that the outer or inner relation has been + * unique-ified and a regular inner join should then be applied. These values + * are not allowed to propagate outside this routine, however. Path cost + * estimation code, as well as match_unsorted_outer, may need to recognize that + * it's dealing with such a case --- the combination of nominal jointype INNER * with sjinfo->jointype == JOIN_SEMI indicates that. */ void @@ -129,6 +128,7 @@ add_paths_to_joinrel(PlannerInfo *root, SpecialJoinInfo *sjinfo, List *restrictlist) { + JoinType save_jointype = jointype; JoinPathExtraData extra; bool mergejoin_allowed = true; ListCell *lc; @@ -150,31 +150,56 @@ add_paths_to_joinrel(PlannerInfo *root, extra.mergeclause_list = NIL; extra.sjinfo = sjinfo; extra.param_source_rels = NULL; + extra.pgs_mask = joinrel->pgs_mask; + + /* + * Give extensions a chance to take control. In particular, an extension + * might want to modify extra.pgs_mask. It's possible to override pgs_mask + * on a query-wide basis using join_search_hook, or for a particular + * relation using joinrel_setup_hook, but extensions that want to provide + * different advice for the same joinrel based on the choice of innerrel + * and outerrel will need to use this hook. + * + * A very simple way for an extension to use this hook is to set + * extra.pgs_mask &= ~PGS_JOIN_ANY, if it simply doesn't want any of the + * paths generated by this call to add_paths_to_joinrel() to be selected. + * An extension could use this technique to constrain the join order, + * since it could thereby arrange to reject all paths from join orders + * that it does not like. An extension can also selectively clear bits + * from extra.pgs_mask to rule out specific techniques for specific joins, + * or could even set additional bits to re-allow methods disabled at some + * higher level. + * + * NB: Below this point, this function should be careful to reference + * extra.pgs_mask rather than rel->pgs_mask to avoid disregarding any + * changes made by the hook we're about to call. + */ + if (join_path_setup_hook) + join_path_setup_hook(root, joinrel, outerrel, innerrel, + jointype, &extra); /* * See if the inner relation is provably unique for this outer rel. * - * We have some special cases: for JOIN_SEMI and JOIN_ANTI, it doesn't - * matter since the executor can make the equivalent optimization anyway; - * we need not expend planner cycles on proofs. For JOIN_UNIQUE_INNER, we - * must be considering a semijoin whose inner side is not provably unique - * (else reduce_unique_semijoins would've simplified it), so there's no - * point in calling innerrel_is_unique. However, if the LHS covers all of - * the semijoin's min_lefthand, then it's appropriate to set inner_unique - * because the path produced by create_unique_path will be unique relative - * to the LHS. (If we have an LHS that's only part of the min_lefthand, - * that is *not* true.) For JOIN_UNIQUE_OUTER, pass JOIN_INNER to avoid - * letting that value escape this module. + * We have some special cases: for JOIN_SEMI, it doesn't matter since the + * executor can make the equivalent optimization anyway. It also doesn't + * help enable use of Memoize, since a semijoin with a provably unique + * inner side should have been reduced to an inner join in that case. + * Therefore, we need not expend planner cycles on proofs. (For + * JOIN_ANTI, although it doesn't help the executor for the same reason, + * it can benefit Memoize paths.) For JOIN_UNIQUE_INNER, we must be + * considering a semijoin whose inner side is not provably unique (else + * reduce_unique_semijoins would've simplified it), so there's no point in + * calling innerrel_is_unique. However, if the LHS covers all of the + * semijoin's min_lefthand, then it's appropriate to set inner_unique + * because the unique relation produced by create_unique_paths will be + * unique relative to the LHS. (If we have an LHS that's only part of the + * min_lefthand, that is *not* true.) For JOIN_UNIQUE_OUTER, pass + * JOIN_INNER to avoid letting that value escape this module. */ switch (jointype) { case JOIN_SEMI: - case JOIN_ANTI: - - /* - * XXX it may be worth proving this to allow a Memoize to be - * considered for Nested Loop Semi/Anti Joins. - */ extra.inner_unique = false; /* well, unproven */ break; case JOIN_UNIQUE_INNER: @@ -201,13 +226,20 @@ add_paths_to_joinrel(PlannerInfo *root, break; } + /* + * If the outer or inner relation has been unique-ified, handle as a plain + * inner join. + */ + if (jointype == JOIN_UNIQUE_OUTER || jointype == JOIN_UNIQUE_INNER) + jointype = JOIN_INNER; + /* * Find potential mergejoin clauses. We can skip this if we are not * interested in doing a mergejoin. However, mergejoin may be our only - * way of implementing a full outer join, so override enable_mergejoin if - * it's a full join. + * way of implementing a full outer join, so in that case we don't care + * whether mergejoins are disabled. */ - if (enable_mergejoin || jointype == JOIN_FULL) + if ((extra.pgs_mask & PGS_MERGEJOIN_ANY) != 0 || jointype == JOIN_FULL) extra.mergeclause_list = select_mergejoin_clauses(root, joinrel, outerrel, @@ -315,10 +347,10 @@ add_paths_to_joinrel(PlannerInfo *root, /* * 4. Consider paths where both outer and inner relations must be hashed - * before being joined. As above, disregard enable_hashjoin for full - * joins, because there may be no other alternative. + * before being joined. As above, when it's a full join, we must try this + * even when the path type is disabled, because it may be our only option. */ - if (enable_hashjoin || jointype == JOIN_FULL) + if ((extra.pgs_mask & PGS_HASHJOIN) != 0 || jointype == JOIN_FULL) hash_inner_and_outer(root, joinrel, outerrel, innerrel, jointype, &extra); @@ -327,21 +359,26 @@ add_paths_to_joinrel(PlannerInfo *root, * to the same server and assigned to the same user to check access * permissions as, give the FDW a chance to push down joins. */ - if (joinrel->fdwroutine && + if ((extra.pgs_mask & PGS_FOREIGNJOIN) != 0 && joinrel->fdwroutine && joinrel->fdwroutine->GetForeignJoinPaths) joinrel->fdwroutine->GetForeignJoinPaths(root, joinrel, outerrel, innerrel, - jointype, &extra); + save_jointype, &extra); /* * 6. Finally, give extensions a chance to manipulate the path list. They * could add new paths (such as CustomPaths) by calling add_path(), or - * add_partial_path() if parallel aware. They could also delete or modify - * paths added by the core code. + * add_partial_path() if parallel aware. + * + * In theory, extensions could also use this hook to delete or modify + * paths added by the core code, but in practice this is difficult to make + * work, since it's too late to get back any paths that have already been + * discarded by add_path() or add_partial_path(). If you're trying to + * suppress paths, consider using join_path_setup_hook instead. */ if (set_join_pathlist_hook) set_join_pathlist_hook(root, joinrel, outerrel, innerrel, - jointype, &extra); + save_jointype, &extra); } /* @@ -684,7 +721,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel, List *ph_lateral_vars; /* Obviously not if it's disabled */ - if (!enable_memoize) + if ((extra->pgs_mask & PGS_NESTLOOP_MEMOIZE) == 0) return NULL; /* @@ -692,8 +729,13 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel, * than one inner scan. The first scan is always going to be a cache * miss. This would likely fail later anyway based on costs, so this is * really just to save some wasted effort. + * + * However, if the "plain nested loop" strategy is disabled, then it is no + * longer certain that any path we'd construct here would lose on cost. + * So, in that case, continue and let cost comparison sort things out. */ - if (outer_path->parent->rows < 2) + if (outer_path->parent->rows < 2 && + (extra->pgs_mask & PGS_NESTLOOP_PLAIN) != 0) return NULL; /* @@ -715,16 +757,21 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel, return NULL; /* - * Currently we don't do this for SEMI and ANTI joins unless they're - * marked as inner_unique. This is because nested loop SEMI/ANTI joins - * don't scan the inner node to completion, which will mean memoize cannot - * mark the cache entry as complete. - * - * XXX Currently we don't attempt to mark SEMI/ANTI joins as inner_unique - * = true. Should we? See add_paths_to_joinrel() + * Currently we don't do this for SEMI and ANTI joins, because nested loop + * SEMI/ANTI joins don't scan the inner node to completion, which means + * memoize cannot mark the cache entry as complete. Nor can we mark the + * cache entry as complete after fetching the first inner tuple, because + * if that tuple and the current outer tuple don't satisfy the join + * clauses, a second inner tuple that satisfies the parameters would find + * the cache entry already marked as complete. The only exception is when + * the inner relation is provably unique, as in that case, there won't be + * a second matching tuple and we can safely mark the cache entry as + * complete after fetching the first inner tuple. Note that in such + * cases, the SEMI join should have been reduced to an inner join by + * reduce_unique_semijoins. */ - if (!extra->inner_unique && (jointype == JOIN_SEMI || - jointype == JOIN_ANTI)) + if ((jointype == JOIN_SEMI || jointype == JOIN_ANTI) && + !extra->inner_unique) return NULL; /* @@ -834,6 +881,7 @@ try_nestloop_path(PlannerInfo *root, Path *inner_path, List *pathkeys, JoinType jointype, + uint64 nestloop_subtype, JoinPathExtraData *extra) { Relids required_outer; @@ -876,16 +924,13 @@ try_nestloop_path(PlannerInfo *root, /* * Check to see if proposed path is still parameterized, and reject if the * parameterization wouldn't be sensible --- unless allow_star_schema_join - * says to allow it anyway. Also, we must reject if have_dangerous_phv - * doesn't like the look of it, which could only happen if the nestloop is - * still parameterized. + * says to allow it anyway. */ required_outer = calc_nestloop_required_outer(outerrelids, outer_paramrels, innerrelids, inner_paramrels); if (required_outer && - ((!bms_overlap(required_outer, extra->param_source_rels) && - !allow_star_schema_join(root, outerrelids, inner_paramrels)) || - have_dangerous_phv(root, outerrelids, inner_paramrels))) + !bms_overlap(required_outer, extra->param_source_rels) && + !allow_star_schema_join(root, outerrelids, inner_paramrels)) { /* Waste no memory when we reject a path here */ bms_free(required_outer); @@ -919,6 +964,7 @@ try_nestloop_path(PlannerInfo *root, * methodology worthwhile. */ initial_cost_nestloop(root, &workspace, jointype, + nestloop_subtype | PGS_CONSIDER_NONPARTIAL, outer_path, inner_path, extra); if (add_path_precheck(joinrel, workspace.disabled_nodes, @@ -956,6 +1002,7 @@ try_partial_nestloop_path(PlannerInfo *root, Path *inner_path, List *pathkeys, JoinType jointype, + uint64 nestloop_subtype, JoinPathExtraData *extra) { JoinCostWorkspace workspace; @@ -1003,9 +1050,10 @@ try_partial_nestloop_path(PlannerInfo *root, * Before creating a path, get a quick lower bound on what it is likely to * cost. Bail out right away if it looks terrible. */ - initial_cost_nestloop(root, &workspace, jointype, + initial_cost_nestloop(root, &workspace, jointype, nestloop_subtype, outer_path, inner_path, extra); if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes, + workspace.startup_cost, workspace.total_cost, pathkeys)) return; @@ -1195,6 +1243,7 @@ try_partial_mergejoin_path(PlannerInfo *root, extra); if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes, + workspace.startup_cost, workspace.total_cost, pathkeys)) return; @@ -1327,6 +1376,7 @@ try_partial_hashjoin_path(PlannerInfo *root, initial_cost_hashjoin(root, &workspace, jointype, hashclauses, outer_path, inner_path, extra, parallel_hash); if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes, + workspace.startup_cost, workspace.total_cost, NIL)) return; @@ -1364,7 +1414,6 @@ sort_inner_and_outer(PlannerInfo *root, JoinType jointype, JoinPathExtraData *extra) { - JoinType save_jointype = jointype; Path *outer_path; Path *inner_path; Path *cheapest_partial_outer = NULL; @@ -1402,38 +1451,16 @@ sort_inner_and_outer(PlannerInfo *root, PATH_PARAM_BY_REL(inner_path, outerrel)) return; - /* - * If unique-ification is requested, do it and then handle as a plain - * inner join. - */ - if (jointype == JOIN_UNIQUE_OUTER) - { - outer_path = (Path *) create_unique_path(root, outerrel, - outer_path, extra->sjinfo); - Assert(outer_path); - jointype = JOIN_INNER; - } - else if (jointype == JOIN_UNIQUE_INNER) - { - inner_path = (Path *) create_unique_path(root, innerrel, - inner_path, extra->sjinfo); - Assert(inner_path); - jointype = JOIN_INNER; - } - /* * If the joinrel is parallel-safe, we may be able to consider a partial - * merge join. However, we can't handle JOIN_UNIQUE_OUTER, because the - * outer path will be partial, and therefore we won't be able to properly - * guarantee uniqueness. Similarly, we can't handle JOIN_FULL, JOIN_RIGHT - * and JOIN_RIGHT_ANTI, because they can produce false null extended rows. + * merge join. However, we can't handle JOIN_FULL, JOIN_RIGHT and + * JOIN_RIGHT_ANTI, because they can produce false null extended rows. * Also, the resulting path must not be parameterized. */ if (joinrel->consider_parallel && - save_jointype != JOIN_UNIQUE_OUTER && - save_jointype != JOIN_FULL && - save_jointype != JOIN_RIGHT && - save_jointype != JOIN_RIGHT_ANTI && + jointype != JOIN_FULL && + jointype != JOIN_RIGHT && + jointype != JOIN_RIGHT_ANTI && outerrel->partial_pathlist != NIL && bms_is_empty(joinrel->lateral_relids)) { @@ -1441,7 +1468,7 @@ sort_inner_and_outer(PlannerInfo *root, if (inner_path->parallel_safe) cheapest_safe_inner = inner_path; - else if (save_jointype != JOIN_UNIQUE_INNER) + else cheapest_safe_inner = get_cheapest_parallel_safe_total_inner(innerrel->pathlist); } @@ -1580,13 +1607,9 @@ generate_mergejoin_paths(PlannerInfo *root, List *trialsortkeys; Path *cheapest_startup_inner; Path *cheapest_total_inner; - JoinType save_jointype = jointype; int num_sortkeys; int sortkeycnt; - if (jointype == JOIN_UNIQUE_OUTER || jointype == JOIN_UNIQUE_INNER) - jointype = JOIN_INNER; - /* Look for useful mergeclauses (if any) */ mergeclauses = find_mergeclauses_for_outer_pathkeys(root, @@ -1636,10 +1659,6 @@ generate_mergejoin_paths(PlannerInfo *root, extra, is_partial); - /* Can't do anything else if inner path needs to be unique'd */ - if (save_jointype == JOIN_UNIQUE_INNER) - return; - /* * Look for presorted inner paths that satisfy the innersortkey list --- * or any truncation thereof, if we are allowed to build a mergejoin using @@ -1819,7 +1838,6 @@ match_unsorted_outer(PlannerInfo *root, JoinType jointype, JoinPathExtraData *extra) { - JoinType save_jointype = jointype; bool nestjoinOK; bool useallclauses; Path *inner_cheapest_total = innerrel->cheapest_total_path; @@ -1855,12 +1873,6 @@ match_unsorted_outer(PlannerInfo *root, nestjoinOK = false; useallclauses = true; break; - case JOIN_UNIQUE_OUTER: - case JOIN_UNIQUE_INNER: - jointype = JOIN_INNER; - nestjoinOK = true; - useallclauses = false; - break; default: elog(ERROR, "unrecognized join type: %d", (int) jointype); @@ -1873,34 +1885,39 @@ match_unsorted_outer(PlannerInfo *root, * If inner_cheapest_total is parameterized by the outer rel, ignore it; * we will consider it below as a member of cheapest_parameterized_paths, * but the other possibilities considered in this routine aren't usable. + * + * Furthermore, if the inner side is a unique-ified relation, we cannot + * generate any valid paths here, because the inner rel's dependency on + * the outer rel makes unique-ification meaningless. */ if (PATH_PARAM_BY_REL(inner_cheapest_total, outerrel)) + { inner_cheapest_total = NULL; - /* - * If we need to unique-ify the inner path, we will consider only the - * cheapest-total inner. - */ - if (save_jointype == JOIN_UNIQUE_INNER) - { - /* No way to do this with an inner path parameterized by outer rel */ - if (inner_cheapest_total == NULL) + if (RELATION_WAS_MADE_UNIQUE(innerrel, extra->sjinfo, jointype)) return; - inner_cheapest_total = (Path *) - create_unique_path(root, innerrel, inner_cheapest_total, extra->sjinfo); - Assert(inner_cheapest_total); } - else if (nestjoinOK) + + if (nestjoinOK) { /* - * Consider materializing the cheapest inner path, unless - * enable_material is off or the path in question materializes its - * output anyway. + * Consider materializing the cheapest inner path, unless that is + * disabled or the path in question materializes its output anyway. + * + * At present, we only consider materialization for non-partial outer + * paths, so it's correct to test PGS_CONSIDER_NONPARTIAL here. If we + * ever want to consider materialization for partial paths, we'll need + * to create matpath whenever PGS_NESTLOOP_MATERIALIZE is set, use it + * for partial paths either way, and use it for non-partial paths only + * when PGS_CONSIDER_NONPARTIAL is also set. */ - if (enable_material && inner_cheapest_total != NULL && + if ((extra->pgs_mask & + (PGS_NESTLOOP_MATERIALIZE | PGS_CONSIDER_NONPARTIAL)) == + (PGS_NESTLOOP_MATERIALIZE | PGS_CONSIDER_NONPARTIAL) && + inner_cheapest_total != NULL && !ExecMaterializesOutput(inner_cheapest_total->pathtype)) matpath = (Path *) - create_material_path(innerrel, inner_cheapest_total); + create_material_path(innerrel, inner_cheapest_total, true); } foreach(lc1, outerrel->pathlist) @@ -1914,20 +1931,6 @@ match_unsorted_outer(PlannerInfo *root, if (PATH_PARAM_BY_REL(outerpath, innerrel)) continue; - /* - * If we need to unique-ify the outer path, it's pointless to consider - * any but the cheapest outer. (XXX we don't consider parameterized - * outers, nor inners, for unique-ified cases. Should we?) - */ - if (save_jointype == JOIN_UNIQUE_OUTER) - { - if (outerpath != outerrel->cheapest_total_path) - continue; - outerpath = (Path *) create_unique_path(root, outerrel, - outerpath, extra->sjinfo); - Assert(outerpath); - } - /* * The result will have this sort order (even if it is implemented as * a nestloop, and even if some of the mergeclauses are implemented by @@ -1936,21 +1939,7 @@ match_unsorted_outer(PlannerInfo *root, merge_pathkeys = build_join_pathkeys(root, joinrel, jointype, outerpath->pathkeys); - if (save_jointype == JOIN_UNIQUE_INNER) - { - /* - * Consider nestloop join, but only with the unique-ified cheapest - * inner path - */ - try_nestloop_path(root, - joinrel, - outerpath, - inner_cheapest_total, - merge_pathkeys, - jointype, - extra); - } - else if (nestjoinOK) + if (nestjoinOK) { /* * Consider nestloop joins using this outer path and various @@ -1971,6 +1960,7 @@ match_unsorted_outer(PlannerInfo *root, innerpath, merge_pathkeys, jointype, + PGS_NESTLOOP_PLAIN, extra); /* @@ -1987,6 +1977,7 @@ match_unsorted_outer(PlannerInfo *root, mpath, merge_pathkeys, jointype, + PGS_NESTLOOP_MEMOIZE, extra); } @@ -1998,20 +1989,17 @@ match_unsorted_outer(PlannerInfo *root, matpath, merge_pathkeys, jointype, + PGS_NESTLOOP_MATERIALIZE, extra); } - /* Can't do anything else if outer path needs to be unique'd */ - if (save_jointype == JOIN_UNIQUE_OUTER) - continue; - /* Can't do anything else if inner rel is parameterized by outer */ if (inner_cheapest_total == NULL) continue; /* Generate merge join paths */ generate_mergejoin_paths(root, joinrel, innerrel, outerpath, - save_jointype, extra, useallclauses, + jointype, extra, useallclauses, inner_cheapest_total, merge_pathkeys, false); } @@ -2019,41 +2007,35 @@ match_unsorted_outer(PlannerInfo *root, /* * Consider partial nestloop and mergejoin plan if outerrel has any * partial path and the joinrel is parallel-safe. However, we can't - * handle JOIN_UNIQUE_OUTER, because the outer path will be partial, and - * therefore we won't be able to properly guarantee uniqueness. Nor can - * we handle joins needing lateral rels, since partial paths must not be - * parameterized. Similarly, we can't handle JOIN_FULL, JOIN_RIGHT and + * handle joins needing lateral rels, since partial paths must not be + * parameterized. Similarly, we can't handle JOIN_FULL, JOIN_RIGHT and * JOIN_RIGHT_ANTI, because they can produce false null extended rows. */ if (joinrel->consider_parallel && - save_jointype != JOIN_UNIQUE_OUTER && - save_jointype != JOIN_FULL && - save_jointype != JOIN_RIGHT && - save_jointype != JOIN_RIGHT_ANTI && + jointype != JOIN_FULL && + jointype != JOIN_RIGHT && + jointype != JOIN_RIGHT_ANTI && outerrel->partial_pathlist != NIL && bms_is_empty(joinrel->lateral_relids)) { if (nestjoinOK) consider_parallel_nestloop(root, joinrel, outerrel, innerrel, - save_jointype, extra); + jointype, extra); /* * If inner_cheapest_total is NULL or non parallel-safe then find the - * cheapest total parallel safe path. If doing JOIN_UNIQUE_INNER, we - * can't use any alternative inner path. + * cheapest total parallel safe path. */ if (inner_cheapest_total == NULL || !inner_cheapest_total->parallel_safe) { - if (save_jointype == JOIN_UNIQUE_INNER) - return; - - inner_cheapest_total = get_cheapest_parallel_safe_total_inner(innerrel->pathlist); + inner_cheapest_total = + get_cheapest_parallel_safe_total_inner(innerrel->pathlist); } if (inner_cheapest_total) consider_parallel_mergejoin(root, joinrel, outerrel, innerrel, - save_jointype, extra, + jointype, extra, inner_cheapest_total); } } @@ -2118,29 +2100,23 @@ consider_parallel_nestloop(PlannerInfo *root, JoinType jointype, JoinPathExtraData *extra) { - JoinType save_jointype = jointype; Path *inner_cheapest_total = innerrel->cheapest_total_path; Path *matpath = NULL; ListCell *lc1; - if (jointype == JOIN_UNIQUE_INNER) - jointype = JOIN_INNER; - /* - * Consider materializing the cheapest inner path, unless: 1) we're doing - * JOIN_UNIQUE_INNER, because in this case we have to unique-ify the - * cheapest inner path, 2) enable_material is off, 3) the cheapest inner - * path is not parallel-safe, 4) the cheapest inner path is parameterized - * by the outer rel, or 5) the cheapest inner path materializes its output - * anyway. + * Consider materializing the cheapest inner path, unless: 1) + * materialization is disabled here, 2) the cheapest inner path is not + * parallel-safe, 3) the cheapest inner path is parameterized by the outer + * rel, or 4) the cheapest inner path materializes its output anyway. */ - if (save_jointype != JOIN_UNIQUE_INNER && - enable_material && inner_cheapest_total->parallel_safe && + if ((extra->pgs_mask & PGS_NESTLOOP_MATERIALIZE) != 0 && + inner_cheapest_total->parallel_safe && !PATH_PARAM_BY_REL(inner_cheapest_total, outerrel) && !ExecMaterializesOutput(inner_cheapest_total->pathtype)) { matpath = (Path *) - create_material_path(innerrel, inner_cheapest_total); + create_material_path(innerrel, inner_cheapest_total, true); Assert(matpath->parallel_safe); } @@ -2169,25 +2145,9 @@ consider_parallel_nestloop(PlannerInfo *root, if (!innerpath->parallel_safe) continue; - /* - * If we're doing JOIN_UNIQUE_INNER, we can only use the inner's - * cheapest_total_path, and we have to unique-ify it. (We might - * be able to relax this to allow other safe, unparameterized - * inner paths, but right now create_unique_path is not on board - * with that.) - */ - if (save_jointype == JOIN_UNIQUE_INNER) - { - if (innerpath != innerrel->cheapest_total_path) - continue; - innerpath = (Path *) create_unique_path(root, innerrel, - innerpath, - extra->sjinfo); - Assert(innerpath); - } - try_partial_nestloop_path(root, joinrel, outerpath, innerpath, - pathkeys, jointype, extra); + pathkeys, jointype, + PGS_NESTLOOP_PLAIN, extra); /* * Try generating a memoize path and see if that makes the nested @@ -2198,13 +2158,15 @@ consider_parallel_nestloop(PlannerInfo *root, extra); if (mpath != NULL) try_partial_nestloop_path(root, joinrel, outerpath, mpath, - pathkeys, jointype, extra); + pathkeys, jointype, + PGS_NESTLOOP_MEMOIZE, extra); } /* Also consider materialized form of the cheapest inner path */ if (matpath != NULL) try_partial_nestloop_path(root, joinrel, outerpath, matpath, - pathkeys, jointype, extra); + pathkeys, jointype, + PGS_NESTLOOP_MATERIALIZE, extra); } } @@ -2227,7 +2189,6 @@ hash_inner_and_outer(PlannerInfo *root, JoinType jointype, JoinPathExtraData *extra) { - JoinType save_jointype = jointype; bool isouterjoin = IS_OUTER_JOIN(jointype); List *hashclauses; ListCell *l; @@ -2290,6 +2251,8 @@ hash_inner_and_outer(PlannerInfo *root, Path *cheapest_startup_outer = outerrel->cheapest_startup_path; Path *cheapest_total_outer = outerrel->cheapest_total_path; Path *cheapest_total_inner = innerrel->cheapest_total_path; + ListCell *lc1; + ListCell *lc2; /* * If either cheapest-total path is parameterized by the other rel, we @@ -2301,114 +2264,74 @@ hash_inner_and_outer(PlannerInfo *root, PATH_PARAM_BY_REL(cheapest_total_inner, outerrel)) return; - /* Unique-ify if need be; we ignore parameterized possibilities */ - if (jointype == JOIN_UNIQUE_OUTER) - { - cheapest_total_outer = (Path *) - create_unique_path(root, outerrel, - cheapest_total_outer, extra->sjinfo); - Assert(cheapest_total_outer); - jointype = JOIN_INNER; - try_hashjoin_path(root, - joinrel, - cheapest_total_outer, - cheapest_total_inner, - hashclauses, - jointype, - extra); - /* no possibility of cheap startup here */ - } - else if (jointype == JOIN_UNIQUE_INNER) - { - cheapest_total_inner = (Path *) - create_unique_path(root, innerrel, - cheapest_total_inner, extra->sjinfo); - Assert(cheapest_total_inner); - jointype = JOIN_INNER; + /* + * Consider the cheapest startup outer together with the cheapest + * total inner, and then consider pairings of cheapest-total paths + * including parameterized ones. There is no use in generating + * parameterized paths on the basis of possibly cheap startup cost, so + * this is sufficient. + */ + if (cheapest_startup_outer != NULL) try_hashjoin_path(root, joinrel, - cheapest_total_outer, + cheapest_startup_outer, cheapest_total_inner, hashclauses, jointype, extra); - if (cheapest_startup_outer != NULL && - cheapest_startup_outer != cheapest_total_outer) - try_hashjoin_path(root, - joinrel, - cheapest_startup_outer, - cheapest_total_inner, - hashclauses, - jointype, - extra); - } - else + + foreach(lc1, outerrel->cheapest_parameterized_paths) { + Path *outerpath = (Path *) lfirst(lc1); + /* - * For other jointypes, we consider the cheapest startup outer - * together with the cheapest total inner, and then consider - * pairings of cheapest-total paths including parameterized ones. - * There is no use in generating parameterized paths on the basis - * of possibly cheap startup cost, so this is sufficient. + * We cannot use an outer path that is parameterized by the inner + * rel. */ - ListCell *lc1; - ListCell *lc2; - - if (cheapest_startup_outer != NULL) - try_hashjoin_path(root, - joinrel, - cheapest_startup_outer, - cheapest_total_inner, - hashclauses, - jointype, - extra); + if (PATH_PARAM_BY_REL(outerpath, innerrel)) + continue; - foreach(lc1, outerrel->cheapest_parameterized_paths) + foreach(lc2, innerrel->cheapest_parameterized_paths) { - Path *outerpath = (Path *) lfirst(lc1); + Path *innerpath = (Path *) lfirst(lc2); /* - * We cannot use an outer path that is parameterized by the - * inner rel. + * We cannot use an inner path that is parameterized by the + * outer rel, either. */ - if (PATH_PARAM_BY_REL(outerpath, innerrel)) + if (PATH_PARAM_BY_REL(innerpath, outerrel)) continue; - foreach(lc2, innerrel->cheapest_parameterized_paths) - { - Path *innerpath = (Path *) lfirst(lc2); - - /* - * We cannot use an inner path that is parameterized by - * the outer rel, either. - */ - if (PATH_PARAM_BY_REL(innerpath, outerrel)) - continue; + if (outerpath == cheapest_startup_outer && + innerpath == cheapest_total_inner) + continue; /* already tried it */ - if (outerpath == cheapest_startup_outer && - innerpath == cheapest_total_inner) - continue; /* already tried it */ - - try_hashjoin_path(root, - joinrel, - outerpath, - innerpath, - hashclauses, - jointype, - extra); - } + try_hashjoin_path(root, + joinrel, + outerpath, + innerpath, + hashclauses, + jointype, + extra); } } /* * If the joinrel is parallel-safe, we may be able to consider a - * partial hash join. However, we can't handle JOIN_UNIQUE_OUTER, - * because the outer path will be partial, and therefore we won't be - * able to properly guarantee uniqueness. Also, the resulting path - * must not be parameterized. + * partial hash join. + * + * However, we can't handle JOIN_RIGHT_SEMI, because the hash table is + * either a shared hash table or a private hash table per backend. In + * the shared case, there is no concurrency protection for the match + * flags, so multiple workers could inspect and set the flags + * concurrently, potentially producing incorrect results. In the + * private case, each worker has its own copy of the hash table, so no + * single process has all the match flags. + * + * Also, the resulting path must not be parameterized. */ if (joinrel->consider_parallel && - save_jointype != JOIN_UNIQUE_OUTER && + jointype != JOIN_RIGHT_SEMI && outerrel->partial_pathlist != NIL && bms_is_empty(joinrel->lateral_relids)) { @@ -2421,11 +2344,9 @@ hash_inner_and_outer(PlannerInfo *root, /* * Can we use a partial inner plan too, so that we can build a - * shared hash table in parallel? We can't handle - * JOIN_UNIQUE_INNER because we can't guarantee uniqueness. + * shared hash table in parallel? */ if (innerrel->partial_pathlist != NIL && - save_jointype != JOIN_UNIQUE_INNER && enable_parallel_hash) { cheapest_partial_inner = @@ -2441,19 +2362,17 @@ hash_inner_and_outer(PlannerInfo *root, * Normally, given that the joinrel is parallel-safe, the cheapest * total inner path will also be parallel-safe, but if not, we'll * have to search for the cheapest safe, unparameterized inner - * path. If doing JOIN_UNIQUE_INNER, we can't use any alternative - * inner path. If full, right, right-semi or right-anti join, we - * can't use parallelism (building the hash table in each backend) - * because no one process has all the match bits. + * path. If full, right, or right-anti join, we can't use + * parallelism (building the hash table in each backend) because + * no one process has all the match bits. */ - if (save_jointype == JOIN_FULL || - save_jointype == JOIN_RIGHT || - save_jointype == JOIN_RIGHT_SEMI || - save_jointype == JOIN_RIGHT_ANTI) + if (jointype == JOIN_FULL || + jointype == JOIN_RIGHT || + jointype == JOIN_RIGHT_ANTI) cheapest_safe_inner = NULL; else if (cheapest_total_inner->parallel_safe) cheapest_safe_inner = cheapest_total_inner; - else if (save_jointype != JOIN_UNIQUE_INNER) + else cheapest_safe_inner = get_cheapest_parallel_safe_total_inner(innerrel->pathlist); diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c index 60d65762b5d5e..443e2dca7c0af 100644 --- a/src/backend/optimizer/path/joinrels.c +++ b/src/backend/optimizer/path/joinrels.c @@ -3,7 +3,7 @@ * joinrels.c * Routines to determine which relations should be joined * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,9 +16,11 @@ #include "miscadmin.h" #include "optimizer/appendinfo.h" +#include "optimizer/cost.h" #include "optimizer/joininfo.h" #include "optimizer/pathnode.h" #include "optimizer/paths.h" +#include "optimizer/planner.h" #include "partitioning/partbounds.h" #include "utils/memutils.h" @@ -35,6 +37,9 @@ static bool has_legal_joinclause(PlannerInfo *root, RelOptInfo *rel); static bool restriction_is_constant_false(List *restrictlist, RelOptInfo *joinrel, bool only_pushed_down); +static void make_grouped_join_rel(PlannerInfo *root, RelOptInfo *rel1, + RelOptInfo *rel2, RelOptInfo *joinrel, + SpecialJoinInfo *sjinfo, List *restrictlist); static void populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, RelOptInfo *joinrel, SpecialJoinInfo *sjinfo, List *restrictlist); @@ -444,8 +449,7 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, } else if (sjinfo->jointype == JOIN_SEMI && bms_equal(sjinfo->syn_righthand, rel2->relids) && - create_unique_path(root, rel2, rel2->cheapest_total_path, - sjinfo) != NULL) + create_unique_paths(root, rel2, sjinfo) != NULL) { /*---------- * For a semijoin, we can join the RHS to anything else by @@ -477,8 +481,7 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, } else if (sjinfo->jointype == JOIN_SEMI && bms_equal(sjinfo->syn_righthand, rel1->relids) && - create_unique_path(root, rel1, rel1->cheapest_total_path, - sjinfo) != NULL) + create_unique_paths(root, rel1, sjinfo) != NULL) { /* Reversed semijoin case */ if (match_sjinfo) @@ -565,9 +568,6 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, * Also, if the lateral reference is only indirect, we should reject * the join; whatever rel(s) the reference chain goes through must be * joined to first. - * - * Another case that might keep us from building a valid plan is the - * implementation restriction described by have_dangerous_phv(). */ lateral_fwd = bms_overlap(rel1->relids, rel2->lateral_relids); lateral_rev = bms_overlap(rel2->relids, rel1->lateral_relids); @@ -584,9 +584,6 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, /* check there is a direct reference from rel2 to rel1 */ if (!bms_overlap(rel1->relids, rel2->direct_lateral_relids)) return false; /* only indirect refs, so reject */ - /* check we won't have a dangerous PHV */ - if (have_dangerous_phv(root, rel1->relids, rel2->lateral_relids)) - return false; /* might be unable to handle required PHV */ } else if (lateral_rev) { @@ -599,9 +596,6 @@ join_is_legal(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, /* check there is a direct reference from rel1 to rel2 */ if (!bms_overlap(rel2->relids, rel1->direct_lateral_relids)) return false; /* only indirect refs, so reject */ - /* check we won't have a dangerous PHV */ - if (have_dangerous_phv(root, rel2->relids, rel1->lateral_relids)) - return false; /* might be unable to handle required PHV */ } /* @@ -772,6 +766,10 @@ make_join_rel(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2) return joinrel; } + /* Build a grouped join relation for 'joinrel' if possible. */ + make_grouped_join_rel(root, rel1, rel2, joinrel, sjinfo, + restrictlist); + /* Add paths to the join relation. */ populate_joinrel_with_paths(root, rel1, rel2, joinrel, sjinfo, restrictlist); @@ -883,6 +881,186 @@ add_outer_joins_to_relids(PlannerInfo *root, Relids input_relids, return input_relids; } +/* + * make_grouped_join_rel + * Build a grouped join relation for the given "joinrel" if eager + * aggregation is applicable and the resulting grouped paths are considered + * useful. + * + * There are two strategies for generating grouped paths for a join relation: + * + * 1. Join a grouped (partially aggregated) input relation with a non-grouped + * input (e.g., AGG(B) JOIN A). + * + * 2. Apply partial aggregation (sorted or hashed) on top of existing + * non-grouped join paths (e.g., AGG(A JOIN B)). + * + * To limit planning effort and avoid an explosion of alternatives, we adopt a + * strategy where partial aggregation is only pushed to the lowest possible + * level in the join tree that is deemed useful. That is, if grouped paths can + * be built using the first strategy, we skip consideration of the second + * strategy for the same join level. + * + * Additionally, if there are multiple lowest useful levels where partial + * aggregation could be applied, such as in a join tree with relations A, B, + * and C where both "AGG(A JOIN B) JOIN C" and "A JOIN AGG(B JOIN C)" are valid + * placements, we choose only the first one encountered during join search. + * This avoids generating multiple versions of the same grouped relation based + * on different aggregation placements. + * + * These heuristics also ensure that all grouped paths for the same grouped + * relation produce the same set of rows, which is a basic assumption in the + * planner. + */ +static void +make_grouped_join_rel(PlannerInfo *root, RelOptInfo *rel1, + RelOptInfo *rel2, RelOptInfo *joinrel, + SpecialJoinInfo *sjinfo, List *restrictlist) +{ + RelOptInfo *grouped_rel; + RelOptInfo *grouped_rel1; + RelOptInfo *grouped_rel2; + bool rel1_empty; + bool rel2_empty; + Relids apply_agg_at; + + /* + * If there are no aggregate expressions or grouping expressions, eager + * aggregation is not possible. + */ + if (root->agg_clause_list == NIL || + root->group_expr_list == NIL) + return; + + /* Retrieve the grouped relations for the two input rels */ + grouped_rel1 = rel1->grouped_rel; + grouped_rel2 = rel2->grouped_rel; + + rel1_empty = (grouped_rel1 == NULL || IS_DUMMY_REL(grouped_rel1)); + rel2_empty = (grouped_rel2 == NULL || IS_DUMMY_REL(grouped_rel2)); + + /* Find or construct a grouped joinrel for this joinrel */ + grouped_rel = joinrel->grouped_rel; + if (grouped_rel == NULL) + { + RelAggInfo *agg_info = NULL; + + /* + * Prepare the information needed to create grouped paths for this + * join relation. + */ + agg_info = create_rel_agg_info(root, joinrel, rel1_empty == rel2_empty); + if (agg_info == NULL) + return; + + /* + * If grouped paths for the given join relation are not considered + * useful, and no grouped paths can be built by joining grouped input + * relations, skip building the grouped join relation. + */ + if (!agg_info->agg_useful && + (rel1_empty == rel2_empty)) + return; + + /* build the grouped relation */ + grouped_rel = build_grouped_rel(root, joinrel); + grouped_rel->reltarget = agg_info->target; + + if (rel1_empty != rel2_empty) + { + /* + * If there is exactly one grouped input relation, then we can + * build grouped paths by joining the input relations. Set size + * estimates for the grouped join relation based on the input + * relations, and update the set of relids where partial + * aggregation is applied to that of the grouped input relation. + */ + set_joinrel_size_estimates(root, grouped_rel, + rel1_empty ? rel1 : grouped_rel1, + rel2_empty ? rel2 : grouped_rel2, + sjinfo, restrictlist); + agg_info->apply_agg_at = rel1_empty ? + grouped_rel2->agg_info->apply_agg_at : + grouped_rel1->agg_info->apply_agg_at; + } + else + { + /* + * Otherwise, grouped paths can be built by applying partial + * aggregation on top of existing non-grouped join paths. Set + * size estimates for the grouped join relation based on the + * estimated number of groups, and track the set of relids where + * partial aggregation is applied. Note that these values may be + * updated later if it is determined that grouped paths can be + * constructed by joining other input relations. + */ + grouped_rel->rows = agg_info->grouped_rows; + agg_info->apply_agg_at = bms_copy(joinrel->relids); + } + + grouped_rel->agg_info = agg_info; + joinrel->grouped_rel = grouped_rel; + } + + Assert(IS_GROUPED_REL(grouped_rel)); + + /* We may have already proven this grouped join relation to be dummy. */ + if (IS_DUMMY_REL(grouped_rel)) + return; + + /* + * Nothing to do if there's no grouped input relation. Also, joining two + * grouped relations is not currently supported. + */ + if (rel1_empty == rel2_empty) + return; + + /* + * Get the set of relids where partial aggregation is applied among the + * given input relations. + */ + apply_agg_at = rel1_empty ? + grouped_rel2->agg_info->apply_agg_at : + grouped_rel1->agg_info->apply_agg_at; + + /* + * If it's not the designated level, skip building grouped paths. + * + * One exception is when it is a subset of the previously recorded level. + * In that case, we need to update the designated level to this one, and + * adjust the size estimates for the grouped join relation accordingly. + * For example, suppose partial aggregation can be applied on top of (B + * JOIN C). If we first construct the join as ((A JOIN B) JOIN C), we'd + * record the designated level as including all three relations (A B C). + * Later, when we consider (A JOIN (B JOIN C)), we encounter the smaller + * (B C) join level directly. Since this is a subset of the previous + * level and still valid for partial aggregation, we update the designated + * level to (B C), and adjust the size estimates accordingly. + */ + if (!bms_equal(apply_agg_at, grouped_rel->agg_info->apply_agg_at)) + { + if (bms_is_subset(apply_agg_at, grouped_rel->agg_info->apply_agg_at)) + { + /* Adjust the size estimates for the grouped join relation. */ + set_joinrel_size_estimates(root, grouped_rel, + rel1_empty ? rel1 : grouped_rel1, + rel2_empty ? rel2 : grouped_rel2, + sjinfo, restrictlist); + grouped_rel->agg_info->apply_agg_at = apply_agg_at; + } + else + return; + } + + /* Make paths for the grouped join relation. */ + populate_joinrel_with_paths(root, + rel1_empty ? rel1 : grouped_rel1, + rel2_empty ? rel2 : grouped_rel2, + grouped_rel, + sjinfo, + restrictlist); +} + /* * populate_joinrel_with_paths * Add paths to the given joinrel for given pair of joining relations. The @@ -895,6 +1073,8 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, RelOptInfo *joinrel, SpecialJoinInfo *sjinfo, List *restrictlist) { + RelOptInfo *unique_rel2; + /* * Consider paths using each rel as both outer and inner. Depending on * the join type, a provably empty outer or inner rel might mean the join @@ -1000,14 +1180,13 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1, /* * If we know how to unique-ify the RHS and one input rel is * exactly the RHS (not a superset) we can consider unique-ifying - * it and then doing a regular join. (The create_unique_path + * it and then doing a regular join. (The create_unique_paths * check here is probably redundant with what join_is_legal did, * but if so the check is cheap because it's cached. So test * anyway to be sure.) */ if (bms_equal(sjinfo->syn_righthand, rel2->relids) && - create_unique_path(root, rel2, rel2->cheapest_total_path, - sjinfo) != NULL) + (unique_rel2 = create_unique_paths(root, rel2, sjinfo)) != NULL) { if (is_dummy_rel(rel1) || is_dummy_rel(rel2) || restriction_is_constant_false(restrictlist, joinrel, false)) @@ -1015,10 +1194,10 @@ populate_joinrel_with_paths(PlannerInfo *root, RelOptInfo *rel1, mark_dummy_rel(joinrel); break; } - add_paths_to_joinrel(root, joinrel, rel1, rel2, + add_paths_to_joinrel(root, joinrel, rel1, unique_rel2, JOIN_UNIQUE_INNER, sjinfo, restrictlist); - add_paths_to_joinrel(root, joinrel, rel2, rel1, + add_paths_to_joinrel(root, joinrel, unique_rel2, rel1, JOIN_UNIQUE_OUTER, sjinfo, restrictlist); } @@ -1278,57 +1457,6 @@ has_legal_joinclause(PlannerInfo *root, RelOptInfo *rel) } -/* - * There's a pitfall for creating parameterized nestloops: suppose the inner - * rel (call it A) has a parameter that is a PlaceHolderVar, and that PHV's - * minimum eval_at set includes the outer rel (B) and some third rel (C). - * We might think we could create a B/A nestloop join that's parameterized by - * C. But we would end up with a plan in which the PHV's expression has to be - * evaluated as a nestloop parameter at the B/A join; and the executor is only - * set up to handle simple Vars as NestLoopParams. Rather than add complexity - * and overhead to the executor for such corner cases, it seems better to - * forbid the join. (Note that we can still make use of A's parameterized - * path with pre-joined B+C as the outer rel. have_join_order_restriction() - * ensures that we will consider making such a join even if there are not - * other reasons to do so.) - * - * So we check whether any PHVs used in the query could pose such a hazard. - * We don't have any simple way of checking whether a risky PHV would actually - * be used in the inner plan, and the case is so unusual that it doesn't seem - * worth working very hard on it. - * - * This needs to be checked in two places. If the inner rel's minimum - * parameterization would trigger the restriction, then join_is_legal() should - * reject the join altogether, because there will be no workable paths for it. - * But joinpath.c has to check again for every proposed nestloop path, because - * the inner path might have more than the minimum parameterization, causing - * some PHV to be dangerous for it that otherwise wouldn't be. - */ -bool -have_dangerous_phv(PlannerInfo *root, - Relids outer_relids, Relids inner_params) -{ - ListCell *lc; - - foreach(lc, root->placeholder_list) - { - PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc); - - if (!bms_is_subset(phinfo->ph_eval_at, inner_params)) - continue; /* ignore, could not be a nestloop param */ - if (!bms_overlap(phinfo->ph_eval_at, outer_relids)) - continue; /* ignore, not relevant to this join */ - if (bms_is_subset(phinfo->ph_eval_at, outer_relids)) - continue; /* safe, it can be eval'd within outerrel */ - /* Otherwise, it's potentially unsafe, so reject the join */ - return true; - } - - /* OK to perform the join */ - return false; -} - - /* * is_dummy_rel --- has relation been proven empty? */ @@ -1385,6 +1513,7 @@ void mark_dummy_rel(RelOptInfo *rel) { MemoryContext oldcontext; + AppendPathInput in = {0}; /* Already marked? */ if (is_dummy_rel(rel)) @@ -1401,7 +1530,7 @@ mark_dummy_rel(RelOptInfo *rel) rel->partial_pathlist = NIL; /* Set up the dummy path */ - add_path(rel, (Path *) create_append_path(NULL, rel, NIL, NIL, + add_path(rel, (Path *) create_append_path(NULL, rel, in, NIL, rel->lateral_relids, 0, false, -1)); @@ -1675,6 +1804,11 @@ try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2, adjust_child_relids(joinrel->relids, nappinfos, appinfos))); + /* Build a grouped join relation for 'child_joinrel' if possible */ + make_grouped_join_rel(root, child_rel1, child_rel2, + child_joinrel, child_sjinfo, + child_restrictlist); + /* And make paths for the child join */ populate_joinrel_with_paths(root, child_rel1, child_rel2, child_joinrel, child_sjinfo, @@ -1857,8 +1991,7 @@ compute_partition_bounds(PlannerInfo *root, RelOptInfo *rel1, Assert(nparts > 0); joinrel->boundinfo = boundinfo; joinrel->nparts = nparts; - joinrel->part_rels = - (RelOptInfo **) palloc0(sizeof(RelOptInfo *) * nparts); + joinrel->part_rels = palloc0_array(RelOptInfo *, nparts); } else { diff --git a/src/backend/optimizer/path/meson.build b/src/backend/optimizer/path/meson.build index 12f36d85cb65b..98f3ebd5192fd 100644 --- a/src/backend/optimizer/path/meson.build +++ b/src/backend/optimizer/path/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'allpaths.c', diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c index 8b04d40d36d73..5eb71635d15fd 100644 --- a/src/backend/optimizer/path/pathkeys.c +++ b/src/backend/optimizer/path/pathkeys.c @@ -7,7 +7,7 @@ * the nature and use of path keys. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -2147,154 +2147,126 @@ right_merge_direction(PlannerInfo *root, PathKey *pathkey) } /* - * pathkeys_useful_for_ordering - * Count the number of pathkeys that are useful for meeting the - * query's requested output ordering. - * - * Because we the have the possibility of incremental sort, a prefix list of - * keys is potentially useful for improving the performance of the requested - * ordering. Thus we return 0, if no valuable keys are found, or the number - * of leading keys shared by the list and the requested ordering.. + * count_common_leading_pathkeys_ordered + * Returns the number of leading pathkeys which both lists have in common */ static int -pathkeys_useful_for_ordering(PlannerInfo *root, List *pathkeys) +count_common_leading_pathkeys_ordered(List *keys1, List *keys2) { - int n_common_pathkeys; + int ncommon; - (void) pathkeys_count_contained_in(root->query_pathkeys, pathkeys, - &n_common_pathkeys); + (void) pathkeys_count_contained_in(keys1, keys2, &ncommon); - return n_common_pathkeys; + return ncommon; } /* - * pathkeys_useful_for_grouping - * Count the number of pathkeys that are useful for grouping (instead of - * explicit sort) - * - * Group pathkeys could be reordered to benefit from the ordering. The - * ordering may not be "complete" and may require incremental sort, but that's - * fine. So we simply count prefix pathkeys with a matching group key, and - * stop once we find the first pathkey without a match. - * - * So e.g. with pathkeys (a,b,c) and group keys (a,b,e) this determines (a,b) - * pathkeys are useful for grouping, and we might do incremental sort to get - * path ordered by (a,b,e). - * - * This logic is necessary to retain paths with ordering not matching grouping - * keys directly, without the reordering. - * - * Returns the length of pathkey prefix with matching group keys. + * count_common_leading_pathkeys_unordered + * Returns the number of leading PathKeys in 'keys2' which exist in + * 'keys1'. */ static int -pathkeys_useful_for_grouping(PlannerInfo *root, List *pathkeys) +count_common_leading_pathkeys_unordered(List *keys1, List *keys2) { - ListCell *key; - int n = 0; + int ncommon = 0; - /* no special ordering requested for grouping */ - if (root->group_pathkeys == NIL) + /* No point in searching keys2 when keys1 is empty */ + if (keys1 == NIL) return 0; - /* walk the pathkeys and search for matching group key */ - foreach(key, pathkeys) + /* walk keys2 and search for matching PathKeys in keys1 */ + foreach_node(PathKey, pathkey, keys2) { - PathKey *pathkey = (PathKey *) lfirst(key); - - /* no matching group key, we're done */ - if (!list_member_ptr(root->group_pathkeys, pathkey)) + /* + * return the number of matches so far as soon as keys1 doesn't + * contain the given keys2 key. + */ + if (!list_member_ptr(keys1, pathkey)) break; - n++; + ncommon++; } - return n; + return ncommon; } /* - * pathkeys_useful_for_distinct - * Count the number of pathkeys that are useful for DISTINCT or DISTINCT - * ON clause. - * - * DISTINCT keys could be reordered to benefit from the given pathkey list. As - * with pathkeys_useful_for_grouping, we return the number of leading keys in - * the list that are shared by the distinctClause pathkeys. + * truncate_useless_pathkeys + * Shorten the given PathKey List to just the useful PathKeys. If all + * PathKeys are useful, return the input List, otherwise return a new + * List containing only the useful PathKeys. */ -static int -pathkeys_useful_for_distinct(PlannerInfo *root, List *pathkeys) +List * +truncate_useless_pathkeys(PlannerInfo *root, + RelOptInfo *rel, + List *pathkeys) { - int n_common_pathkeys; + int nuseful; + int nuseful2; + int ntotal = list_length(pathkeys); /* - * distinct_pathkeys may have become empty if all of the pathkeys were - * determined to be redundant. Return 0 in this case. + * Here we determine how many items in 'pathkeys' might be useful for + * various Path sort ordering requirements the planner has. Operations + * such as ORDER BY require a Path's pathkeys to match the PathKeys of the + * ORDER BY in the same order, however operations such as GROUP BY and + * DISTINCT are less critical as a Unique or GroupAggregate only need to + * care that all PathKeys exist in their subpath, and don't need to care + * if they're in the same order as the clause in the query. */ - if (root->distinct_pathkeys == NIL) - return 0; + nuseful = count_common_leading_pathkeys_ordered(root->sort_pathkeys, + pathkeys); - /* walk the pathkeys and search for matching DISTINCT key */ - n_common_pathkeys = 0; - foreach_node(PathKey, pathkey, pathkeys) - { - /* no matching DISTINCT key, we're done */ - if (!list_member_ptr(root->distinct_pathkeys, pathkey)) - break; + /* Short-circuit at any point we discover *all* pathkeys are useful */ + if (nuseful == ntotal) + return pathkeys; - n_common_pathkeys++; - } + nuseful2 = count_common_leading_pathkeys_ordered(root->window_pathkeys, + pathkeys); + if (nuseful2 == ntotal) + return pathkeys; - return n_common_pathkeys; -} + nuseful = Max(nuseful, nuseful2); + nuseful2 = count_common_leading_pathkeys_ordered(root->setop_pathkeys, + pathkeys); + if (nuseful2 == ntotal) + return pathkeys; -/* - * pathkeys_useful_for_setop - * Count the number of leading common pathkeys root's 'setop_pathkeys' in - * 'pathkeys'. - */ -static int -pathkeys_useful_for_setop(PlannerInfo *root, List *pathkeys) -{ - int n_common_pathkeys; + nuseful = Max(nuseful, nuseful2); - (void) pathkeys_count_contained_in(root->setop_pathkeys, pathkeys, - &n_common_pathkeys); + /* + * Check if these pathkeys are useful for GROUP BY or DISTINCT. The order + * of the pathkeys does not matter here as Unique and GroupAggregate for + * these operations can take advantage of Paths presorted by any of the + * GROUP BY/DISTINCT pathkeys. + */ + nuseful2 = count_common_leading_pathkeys_unordered(root->group_pathkeys, + pathkeys); + if (nuseful2 == ntotal) + return pathkeys; - return n_common_pathkeys; -} + nuseful = Max(nuseful, nuseful2); + nuseful2 = count_common_leading_pathkeys_unordered(root->distinct_pathkeys, + pathkeys); -/* - * truncate_useless_pathkeys - * Shorten the given pathkey list to just the useful pathkeys. - */ -List * -truncate_useless_pathkeys(PlannerInfo *root, - RelOptInfo *rel, - List *pathkeys) -{ - int nuseful; - int nuseful2; + if (nuseful2 == ntotal) + return pathkeys; + + nuseful = Max(nuseful, nuseful2); - nuseful = pathkeys_useful_for_merging(root, rel, pathkeys); - nuseful2 = pathkeys_useful_for_ordering(root, pathkeys); - if (nuseful2 > nuseful) - nuseful = nuseful2; - nuseful2 = pathkeys_useful_for_grouping(root, pathkeys); - if (nuseful2 > nuseful) - nuseful = nuseful2; - nuseful2 = pathkeys_useful_for_distinct(root, pathkeys); - if (nuseful2 > nuseful) - nuseful = nuseful2; - nuseful2 = pathkeys_useful_for_setop(root, pathkeys); - if (nuseful2 > nuseful) - nuseful = nuseful2; + /* + * Finally, check how many PathKeys might be useful for Merge Joins. This + * is a bit more expensive, so do it last and only if we've not figured + * out that all the pathkeys are useful already. + */ + nuseful2 = pathkeys_useful_for_merging(root, rel, pathkeys); + nuseful = Max(nuseful, nuseful2); /* * Note: not safe to modify input list destructively, but we can avoid * copying the list if we're not actually going to change it */ - if (nuseful == 0) - return NIL; - else if (nuseful == list_length(pathkeys)) + if (nuseful == ntotal) return pathkeys; else return list_copy_head(pathkeys, nuseful); @@ -2320,9 +2292,8 @@ has_useful_pathkeys(PlannerInfo *root, RelOptInfo *rel) { if (rel->joininfo != NIL || rel->has_eclass_joins) return true; /* might be able to use pathkeys for merging */ - if (root->group_pathkeys != NIL) - return true; /* might be able to use pathkeys for grouping */ if (root->query_pathkeys != NIL) - return true; /* might be able to use them for ordering */ + return true; /* the upper planner might need them */ + return false; /* definitely useless */ } diff --git a/src/backend/optimizer/path/tidpath.c b/src/backend/optimizer/path/tidpath.c index 2bfb338b81ced..18a3654720bf3 100644 --- a/src/backend/optimizer/path/tidpath.c +++ b/src/backend/optimizer/path/tidpath.c @@ -27,7 +27,7 @@ * "CTID relop pseudoconstant", where relop is one of >,>=,<,<=, and * AND-clauses composed of such conditions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -490,9 +490,8 @@ ec_member_matches_ctid(PlannerInfo *root, RelOptInfo *rel, /* * create_tidscan_paths - * Create paths corresponding to direct TID scans of the given rel. - * - * Candidate paths are added to the rel's pathlist (using add_path). + * Create paths corresponding to direct TID scans of the given rel and add + * them to the corresponding path list via add_path or add_partial_path. */ bool create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) @@ -500,18 +499,19 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) List *tidquals; List *tidrangequals; bool isCurrentOf; + bool enabled = (rel->pgs_mask & PGS_TIDSCAN) != 0; /* * If any suitable quals exist in the rel's baserestrict list, generate a * plain (unparameterized) TidPath with them. * - * We skip this when enable_tidscan = false, except when the qual is + * We skip this when TID scans are disabled, except when the qual is * CurrentOfExpr. In that case, a TID scan is the only correct path. */ tidquals = TidQualFromRestrictInfoList(root, rel->baserestrictinfo, rel, &isCurrentOf); - if (tidquals != NIL && (enable_tidscan || isCurrentOf)) + if (tidquals != NIL && (enabled || isCurrentOf)) { /* * This path uses no join clauses, but it could still have required @@ -533,7 +533,7 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) } /* Skip the rest if TID scans are disabled. */ - if (!enable_tidscan) + if (!enabled) return false; /* @@ -553,7 +553,24 @@ create_tidscan_paths(PlannerInfo *root, RelOptInfo *rel) add_path(rel, (Path *) create_tidrangescan_path(root, rel, tidrangequals, - required_outer)); + required_outer, + 0)); + + /* If appropriate, consider parallel tid range scan. */ + if (rel->consider_parallel && required_outer == NULL) + { + int parallel_workers; + + parallel_workers = compute_parallel_worker(rel, rel->pages, -1, + max_parallel_workers_per_gather); + + if (parallel_workers > 0) + add_partial_path(rel, (Path *) create_tidrangescan_path(root, + rel, + tidrangequals, + required_outer, + parallel_workers)); + } } /* diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 4d55c2ea59162..03056bdf3e09c 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -11,7 +11,7 @@ * is that we have to work harder to clean up after ourselves when we modify * the query, since the derived data structures have to be updated too. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -31,6 +31,7 @@ #include "optimizer/placeholder.h" #include "optimizer/planmain.h" #include "optimizer/restrictinfo.h" +#include "parser/parse_agg.h" #include "rewrite/rewriteManip.h" #include "utils/lsyscache.h" @@ -56,11 +57,13 @@ bool enable_self_join_elimination; static bool join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo); static void remove_leftjoinrel_from_query(PlannerInfo *root, int relid, SpecialJoinInfo *sjinfo); +static void remove_rel_from_query(PlannerInfo *root, int relid, + int subst, SpecialJoinInfo *sjinfo, + Relids joinrelids); static void remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid); static void remove_rel_from_eclass(EquivalenceClass *ec, - SpecialJoinInfo *sjinfo, - int relid, int subst); + int relid, int ojrelid); static List *remove_rel_from_joinlist(List *joinlist, int relid, int *nremoved); static bool rel_supports_distinctness(PlannerInfo *root, RelOptInfo *rel); static bool rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, @@ -311,24 +314,151 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) return false; } +/* + * Remove the target relid and references to the target join from the + * planner's data structures, having determined that there is no need + * to include them in the query. + * + * We are not terribly thorough here. We only bother to update parts of + * the planner's data structures that will actually be consulted later. + */ +static void +remove_leftjoinrel_from_query(PlannerInfo *root, int relid, + SpecialJoinInfo *sjinfo) +{ + RelOptInfo *rel = find_base_rel(root, relid); + int ojrelid = sjinfo->ojrelid; + Relids joinrelids; + Relids join_plus_commute; + List *joininfos; + ListCell *l; + + /* Compute the relid set for the join we are considering */ + joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand); + Assert(ojrelid != 0); + joinrelids = bms_add_member(joinrelids, ojrelid); + + remove_rel_from_query(root, relid, -1, sjinfo, joinrelids); + + /* + * Remove any joinquals referencing the rel from the joininfo lists. + * + * In some cases, a joinqual has to be put back after deleting its + * reference to the target rel. This can occur for pseudoconstant and + * outerjoin-delayed quals, which can get marked as requiring the rel in + * order to force them to be evaluated at or above the join. We can't + * just discard them, though. Only quals that logically belonged to the + * outer join being discarded should be removed from the query. + * + * We might encounter a qual that is a clone of a deletable qual with some + * outer-join relids added (see deconstruct_distribute_oj_quals). To + * ensure we get rid of such clones as well, add the relids of all OJs + * commutable with this one to the set we test against for + * pushed-down-ness. + */ + join_plus_commute = bms_union(joinrelids, + sjinfo->commute_above_r); + join_plus_commute = bms_add_members(join_plus_commute, + sjinfo->commute_below_l); + + /* + * We must make a copy of the rel's old joininfo list before starting the + * loop, because otherwise remove_join_clause_from_rels would destroy the + * list while we're scanning it. + */ + joininfos = list_copy(rel->joininfo); + foreach(l, joininfos) + { + RestrictInfo *rinfo = (RestrictInfo *) lfirst(l); + + remove_join_clause_from_rels(root, rinfo, rinfo->required_relids); + + if (RINFO_IS_PUSHED_DOWN(rinfo, join_plus_commute)) + { + /* + * There might be references to relid or ojrelid in the + * RestrictInfo's relid sets, as a consequence of PHVs having had + * ph_eval_at sets that include those. We already checked above + * that any such PHV is safe (and updated its ph_eval_at), so we + * can just drop those references. + */ + remove_rel_from_restrictinfo(rinfo, relid, ojrelid); + + /* + * Cross-check that the clause itself does not reference the + * target rel or join. + */ +#ifdef USE_ASSERT_CHECKING + { + Relids clause_varnos = pull_varnos(root, + (Node *) rinfo->clause); + + Assert(!bms_is_member(relid, clause_varnos)); + Assert(!bms_is_member(ojrelid, clause_varnos)); + } +#endif + /* Now throw it back into the joininfo lists */ + distribute_restrictinfo_to_rels(root, rinfo); + } + } + + /* + * There may be references to the rel in root->fkey_list, but if so, + * match_foreign_keys_to_quals() will get rid of them. + */ + + /* + * Now remove the rel from the baserel array to prevent it from being + * referenced again. (We can't do this earlier because + * remove_join_clause_from_rels will touch it.) + */ + root->simple_rel_array[relid] = NULL; + root->simple_rte_array[relid] = NULL; + + /* And nuke the RelOptInfo, just in case there's another access path */ + pfree(rel); + + /* + * Now repeat construction of attr_needed bits coming from all other + * sources. + */ + rebuild_placeholder_attr_needed(root); + rebuild_joinclause_attr_needed(root); + rebuild_eclass_attr_needed(root); + rebuild_lateral_attr_needed(root); +} /* - * Remove the target rel->relid and references to the target join from the + * Remove the target relid and references to the target join from the * planner's data structures, having determined that there is no need - * to include them in the query. Optionally replace them with subst if subst - * is non-negative. + * to include them in the query. Optionally replace references to the + * removed relid with subst if this is a self-join removal. * - * This function updates only parts needed for both left-join removal and - * self-join removal. + * This function serves as the common infrastructure for left-join removal + * and self-join elimination. It is intentionally scoped to update only the + * shared planner data structures that are universally affected by relation + * removal. Each specific caller remains responsible for updating any + * remaining data structures required by its unique removal logic. + * + * The specific type of removal being performed is dictated by the combination + * of the sjinfo and subst parameters. A non-NULL sjinfo indicates left-join + * removal. When sjinfo is NULL, a positive subst value indicates self-join + * elimination (where references are replaced with subst). */ static void -remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, +remove_rel_from_query(PlannerInfo *root, int relid, int subst, SpecialJoinInfo *sjinfo, Relids joinrelids) { - int relid = rel->relid; + int ojrelid = sjinfo ? sjinfo->ojrelid : 0; Index rti; ListCell *l; + bool is_outer_join = (sjinfo != NULL); + bool is_self_join = (!is_outer_join && subst > 0); + + Assert(is_outer_join || is_self_join); + Assert(!is_outer_join || ojrelid > 0); + Assert(!is_outer_join || joinrelids != NULL); /* * Update all_baserels and related relid sets. @@ -336,21 +466,20 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, root->all_baserels = adjust_relid_set(root->all_baserels, relid, subst); root->all_query_rels = adjust_relid_set(root->all_query_rels, relid, subst); - if (sjinfo != NULL) + if (is_outer_join) { - root->outer_join_rels = bms_del_member(root->outer_join_rels, - sjinfo->ojrelid); - root->all_query_rels = bms_del_member(root->all_query_rels, - sjinfo->ojrelid); + root->outer_join_rels = bms_del_member(root->outer_join_rels, ojrelid); + root->all_query_rels = bms_del_member(root->all_query_rels, ojrelid); } /* * Likewise remove references from SpecialJoinInfo data structures. * - * This is relevant in case the outer join we're deleting is nested inside - * other outer joins: the upper joins' relid sets have to be adjusted. The - * RHS of the target outer join will be made empty here, but that's OK - * since caller will delete that SpecialJoinInfo entirely. + * This is relevant in case the relation we're deleting is part of the + * relid sets of special joins: those sets have to be adjusted. If we are + * removing an outer join, the RHS of the target outer join will be made + * empty here, but that's OK since the caller will delete that + * SpecialJoinInfo entirely. */ foreach(l, root->join_info_list) { @@ -366,39 +495,32 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, sjinf->min_righthand = bms_copy(sjinf->min_righthand); sjinf->syn_lefthand = bms_copy(sjinf->syn_lefthand); sjinf->syn_righthand = bms_copy(sjinf->syn_righthand); - /* Now remove relid from the sets: */ + + /* Now adjust relid bit in the sets: */ sjinf->min_lefthand = adjust_relid_set(sjinf->min_lefthand, relid, subst); sjinf->min_righthand = adjust_relid_set(sjinf->min_righthand, relid, subst); sjinf->syn_lefthand = adjust_relid_set(sjinf->syn_lefthand, relid, subst); sjinf->syn_righthand = adjust_relid_set(sjinf->syn_righthand, relid, subst); - if (sjinfo != NULL) + if (is_outer_join) { - Assert(subst <= 0); - - /* Remove sjinfo->ojrelid bits from the sets: */ - sjinf->min_lefthand = bms_del_member(sjinf->min_lefthand, - sjinfo->ojrelid); - sjinf->min_righthand = bms_del_member(sjinf->min_righthand, - sjinfo->ojrelid); - sjinf->syn_lefthand = bms_del_member(sjinf->syn_lefthand, - sjinfo->ojrelid); - sjinf->syn_righthand = bms_del_member(sjinf->syn_righthand, - sjinfo->ojrelid); + /* Remove ojrelid bit from the sets: */ + sjinf->min_lefthand = bms_del_member(sjinf->min_lefthand, ojrelid); + sjinf->min_righthand = bms_del_member(sjinf->min_righthand, ojrelid); + sjinf->syn_lefthand = bms_del_member(sjinf->syn_lefthand, ojrelid); + sjinf->syn_righthand = bms_del_member(sjinf->syn_righthand, ojrelid); /* relid cannot appear in these fields, but ojrelid can: */ - sjinf->commute_above_l = bms_del_member(sjinf->commute_above_l, - sjinfo->ojrelid); - sjinf->commute_above_r = bms_del_member(sjinf->commute_above_r, - sjinfo->ojrelid); - sjinf->commute_below_l = bms_del_member(sjinf->commute_below_l, - sjinfo->ojrelid); - sjinf->commute_below_r = bms_del_member(sjinf->commute_below_r, - sjinfo->ojrelid); + sjinf->commute_above_l = bms_del_member(sjinf->commute_above_l, ojrelid); + sjinf->commute_above_r = bms_del_member(sjinf->commute_above_r, ojrelid); + sjinf->commute_below_l = bms_del_member(sjinf->commute_below_l, ojrelid); + sjinf->commute_below_r = bms_del_member(sjinf->commute_below_r, ojrelid); } else { - Assert(subst > 0); - + /* + * For self-join removal, replace relid references in + * semi_rhs_exprs. + */ ChangeVarNodesExtended((Node *) sjinf->semi_rhs_exprs, relid, subst, 0, replace_relid_callback); } @@ -406,8 +528,8 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, /* * Likewise remove references from PlaceHolderVar data structures, - * removing any no-longer-needed placeholders entirely. We remove PHV - * only for left-join removal. With self-join elimination, PHVs already + * removing any no-longer-needed placeholders entirely. We only remove + * PHVs for left-join removal. With self-join elimination, PHVs already * get moved to the remaining relation, where they might still be needed. * It might also happen that we skip the removal of some PHVs that could * be removed. However, the overhead of extra PHVs is small compared to @@ -427,17 +549,13 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, { PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(l); - Assert(sjinfo == NULL || !bms_is_member(relid, phinfo->ph_lateral)); - if (sjinfo != NULL && + Assert(!is_outer_join || !bms_is_member(relid, phinfo->ph_lateral)); + + if (is_outer_join && bms_is_subset(phinfo->ph_needed, joinrelids) && bms_is_member(relid, phinfo->ph_eval_at) && - !bms_is_member(sjinfo->ojrelid, phinfo->ph_eval_at)) + !bms_is_member(ojrelid, phinfo->ph_eval_at)) { - /* - * This code shouldn't be executed if one relation is substituted - * with another: in this case, the placeholder may be employed in - * a filter inside the scan node the SJE removes. - */ root->placeholder_list = foreach_delete_current(root->placeholder_list, l); root->placeholder_array[phinfo->phid] = NULL; @@ -447,33 +565,42 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, PlaceHolderVar *phv = phinfo->ph_var; phinfo->ph_eval_at = adjust_relid_set(phinfo->ph_eval_at, relid, subst); - if (sjinfo != NULL) - phinfo->ph_eval_at = adjust_relid_set(phinfo->ph_eval_at, - sjinfo->ojrelid, subst); + if (is_outer_join) + phinfo->ph_eval_at = bms_del_member(phinfo->ph_eval_at, ojrelid); Assert(!bms_is_empty(phinfo->ph_eval_at)); /* checked previously */ + /* Reduce ph_needed to contain only "relation 0"; see below */ if (bms_is_member(0, phinfo->ph_needed)) phinfo->ph_needed = bms_make_singleton(0); else phinfo->ph_needed = NULL; - phinfo->ph_lateral = adjust_relid_set(phinfo->ph_lateral, relid, subst); + phv->phrels = adjust_relid_set(phv->phrels, relid, subst); + if (is_outer_join) + phv->phrels = bms_del_member(phv->phrels, ojrelid); + Assert(!bms_is_empty(phv->phrels)); /* - * ph_lateral might contain rels mentioned in ph_eval_at after the - * replacement, remove them. + * For self-join removal, update Var nodes within the PHV's + * expression to reference the replacement relid, and adjust + * ph_lateral for the relid substitution. (For left-join removal, + * we're removing rather than replacing, and any surviving PHV + * shouldn't reference the removed rel in its expression. Also, + * relid can't appear in ph_lateral for outer joins.) */ - phinfo->ph_lateral = bms_difference(phinfo->ph_lateral, phinfo->ph_eval_at); - /* ph_lateral might or might not be empty */ - - phv->phrels = adjust_relid_set(phv->phrels, relid, subst); - if (sjinfo != NULL) - phv->phrels = adjust_relid_set(phv->phrels, - sjinfo->ojrelid, subst); - Assert(!bms_is_empty(phv->phrels)); + if (is_self_join) + { + ChangeVarNodesExtended((Node *) phv->phexpr, relid, subst, 0, + replace_relid_callback); + phinfo->ph_lateral = adjust_relid_set(phinfo->ph_lateral, relid, subst); - ChangeVarNodesExtended((Node *) phv->phexpr, relid, subst, 0, - replace_relid_callback); + /* + * ph_lateral might contain rels mentioned in ph_eval_at after + * the replacement, remove them. + */ + phinfo->ph_lateral = bms_difference(phinfo->ph_lateral, phinfo->ph_eval_at); + /* ph_lateral might or might not be empty */ + } Assert(phv->phnullingrels == NULL); /* no need to adjust */ } @@ -481,29 +608,40 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, /* * Likewise remove references from EquivalenceClasses. + * + * For self-join removal, the caller has already updated the + * EquivalenceClasses, so we can skip this step. */ - foreach(l, root->eq_classes) + if (is_outer_join) { - EquivalenceClass *ec = (EquivalenceClass *) lfirst(l); + foreach(l, root->eq_classes) + { + EquivalenceClass *ec = (EquivalenceClass *) lfirst(l); - if (bms_is_member(relid, ec->ec_relids) || - (sjinfo == NULL || bms_is_member(sjinfo->ojrelid, ec->ec_relids))) - remove_rel_from_eclass(ec, sjinfo, relid, subst); + if (bms_is_member(relid, ec->ec_relids) || + bms_is_member(ojrelid, ec->ec_relids)) + remove_rel_from_eclass(ec, relid, ojrelid); + } } /* - * Finally, we must recompute per-Var attr_needed and per-PlaceHolderVar - * ph_needed relid sets. These have to be known accurately, else we may - * fail to remove other now-removable outer joins. And our removal of the - * join clause(s) for this outer join may mean that Vars that were - * formerly needed no longer are. So we have to do this honestly by - * repeating the construction of those relid sets. We can cheat to one - * small extent: we can avoid re-examining the targetlist and HAVING qual - * by preserving "relation 0" bits from the existing relid sets. This is - * safe because we'd never remove such references. + * Finally, we must prepare for the caller to recompute per-Var + * attr_needed and per-PlaceHolderVar ph_needed relid sets. These have to + * be known accurately, else we may fail to remove other now-removable + * joins. Because the caller removes the join clause(s) associated with + * the removed join, Vars that were formerly needed may no longer be. + * + * The actual reconstruction of these relid sets is performed by the + * specific caller. Here, we simply clear out the existing attr_needed + * sets (we already did this above for ph_needed) to ensure they are + * rebuilt from scratch. We can cheat to one small extent: we can avoid + * re-examining the targetlist and HAVING qual by preserving "relation 0" + * bits from the existing relid sets. This is safe because we'd never + * remove such references. * - * So, start by removing all other bits from attr_needed sets and - * lateral_vars lists. (We already did this above for ph_needed.) + * Additionally, if we are performing self-join elimination, we must + * replace references to the removed relid with subst within the + * lateral_vars lists. */ for (rti = 1; rti < root->simple_rel_array_size; rti++) { @@ -526,129 +664,16 @@ remove_rel_from_query(PlannerInfo *root, RelOptInfo *rel, otherrel->attr_needed[attroff] = NULL; } - if (subst > 0) + if (is_self_join) ChangeVarNodesExtended((Node *) otherrel->lateral_vars, relid, subst, 0, replace_relid_callback); } } -/* - * Remove the target relid and references to the target join from the - * planner's data structures, having determined that there is no need - * to include them in the query. - * - * We are not terribly thorough here. We only bother to update parts of - * the planner's data structures that will actually be consulted later. - */ -static void -remove_leftjoinrel_from_query(PlannerInfo *root, int relid, - SpecialJoinInfo *sjinfo) -{ - RelOptInfo *rel = find_base_rel(root, relid); - int ojrelid = sjinfo->ojrelid; - Relids joinrelids; - Relids join_plus_commute; - List *joininfos; - ListCell *l; - - /* Compute the relid set for the join we are considering */ - joinrelids = bms_union(sjinfo->min_lefthand, sjinfo->min_righthand); - Assert(ojrelid != 0); - joinrelids = bms_add_member(joinrelids, ojrelid); - - remove_rel_from_query(root, rel, -1, sjinfo, joinrelids); - - /* - * Remove any joinquals referencing the rel from the joininfo lists. - * - * In some cases, a joinqual has to be put back after deleting its - * reference to the target rel. This can occur for pseudoconstant and - * outerjoin-delayed quals, which can get marked as requiring the rel in - * order to force them to be evaluated at or above the join. We can't - * just discard them, though. Only quals that logically belonged to the - * outer join being discarded should be removed from the query. - * - * We might encounter a qual that is a clone of a deletable qual with some - * outer-join relids added (see deconstruct_distribute_oj_quals). To - * ensure we get rid of such clones as well, add the relids of all OJs - * commutable with this one to the set we test against for - * pushed-down-ness. - */ - join_plus_commute = bms_union(joinrelids, - sjinfo->commute_above_r); - join_plus_commute = bms_add_members(join_plus_commute, - sjinfo->commute_below_l); - - /* - * We must make a copy of the rel's old joininfo list before starting the - * loop, because otherwise remove_join_clause_from_rels would destroy the - * list while we're scanning it. - */ - joininfos = list_copy(rel->joininfo); - foreach(l, joininfos) - { - RestrictInfo *rinfo = (RestrictInfo *) lfirst(l); - - remove_join_clause_from_rels(root, rinfo, rinfo->required_relids); - - if (RINFO_IS_PUSHED_DOWN(rinfo, join_plus_commute)) - { - /* - * There might be references to relid or ojrelid in the - * RestrictInfo's relid sets, as a consequence of PHVs having had - * ph_eval_at sets that include those. We already checked above - * that any such PHV is safe (and updated its ph_eval_at), so we - * can just drop those references. - */ - remove_rel_from_restrictinfo(rinfo, relid, ojrelid); - - /* - * Cross-check that the clause itself does not reference the - * target rel or join. - */ -#ifdef USE_ASSERT_CHECKING - { - Relids clause_varnos = pull_varnos(root, - (Node *) rinfo->clause); - - Assert(!bms_is_member(relid, clause_varnos)); - Assert(!bms_is_member(ojrelid, clause_varnos)); - } -#endif - /* Now throw it back into the joininfo lists */ - distribute_restrictinfo_to_rels(root, rinfo); - } - } - - /* - * There may be references to the rel in root->fkey_list, but if so, - * match_foreign_keys_to_quals() will get rid of them. - */ - - /* - * Now remove the rel from the baserel array to prevent it from being - * referenced again. (We can't do this earlier because - * remove_join_clause_from_rels will touch it.) - */ - root->simple_rel_array[relid] = NULL; - - /* And nuke the RelOptInfo, just in case there's another access path */ - pfree(rel); - - /* - * Now repeat construction of attr_needed bits coming from all other - * sources. - */ - rebuild_placeholder_attr_needed(root); - rebuild_joinclause_attr_needed(root); - rebuild_eclass_attr_needed(root); - rebuild_lateral_attr_needed(root); -} - /* * Remove any references to relid or ojrelid from the RestrictInfo. * - * We only bother to clean out bits in clause_relids and required_relids, + * We only bother to clean out bits in the RestrictInfo's various relid sets, * not nullingrel bits in contained Vars and PHVs. (This might have to be * improved sometime.) However, if the RestrictInfo contains an OR clause * we have to also clean up the sub-clauses. @@ -670,6 +695,22 @@ remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid) rinfo->required_relids = bms_copy(rinfo->required_relids); rinfo->required_relids = bms_del_member(rinfo->required_relids, relid); rinfo->required_relids = bms_del_member(rinfo->required_relids, ojrelid); + /* Likewise for incompatible_relids */ + rinfo->incompatible_relids = bms_copy(rinfo->incompatible_relids); + rinfo->incompatible_relids = bms_del_member(rinfo->incompatible_relids, relid); + rinfo->incompatible_relids = bms_del_member(rinfo->incompatible_relids, ojrelid); + /* Likewise for outer_relids */ + rinfo->outer_relids = bms_copy(rinfo->outer_relids); + rinfo->outer_relids = bms_del_member(rinfo->outer_relids, relid); + rinfo->outer_relids = bms_del_member(rinfo->outer_relids, ojrelid); + /* Likewise for left_relids */ + rinfo->left_relids = bms_copy(rinfo->left_relids); + rinfo->left_relids = bms_del_member(rinfo->left_relids, relid); + rinfo->left_relids = bms_del_member(rinfo->left_relids, ojrelid); + /* Likewise for right_relids */ + rinfo->right_relids = bms_copy(rinfo->right_relids); + rinfo->right_relids = bms_del_member(rinfo->right_relids, relid); + rinfo->right_relids = bms_del_member(rinfo->right_relids, ojrelid); /* If it's an OR, recurse to clean up sub-clauses */ if (restriction_is_or_clause(rinfo)) @@ -705,8 +746,7 @@ remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid) } /* - * Remove any references to relid or sjinfo->ojrelid (if sjinfo != NULL) - * from the EquivalenceClass. + * Remove any references to relid or ojrelid from the EquivalenceClass. * * Like remove_rel_from_restrictinfo, we don't worry about cleaning out * any nullingrel bits in contained Vars and PHVs. (This might have to be @@ -715,16 +755,13 @@ remove_rel_from_restrictinfo(RestrictInfo *rinfo, int relid, int ojrelid) * level(s). */ static void -remove_rel_from_eclass(EquivalenceClass *ec, SpecialJoinInfo *sjinfo, - int relid, int subst) +remove_rel_from_eclass(EquivalenceClass *ec, int relid, int ojrelid) { ListCell *lc; /* Fix up the EC's overall relids */ - ec->ec_relids = adjust_relid_set(ec->ec_relids, relid, subst); - if (sjinfo != NULL) - ec->ec_relids = adjust_relid_set(ec->ec_relids, - sjinfo->ojrelid, subst); + ec->ec_relids = bms_del_member(ec->ec_relids, relid); + ec->ec_relids = bms_del_member(ec->ec_relids, ojrelid); /* * We don't expect any EC child members to exist at this point. Ensure @@ -743,14 +780,13 @@ remove_rel_from_eclass(EquivalenceClass *ec, SpecialJoinInfo *sjinfo, EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc); if (bms_is_member(relid, cur_em->em_relids) || - (sjinfo != NULL && bms_is_member(sjinfo->ojrelid, - cur_em->em_relids))) + bms_is_member(ojrelid, cur_em->em_relids)) { Assert(!cur_em->em_is_const); - cur_em->em_relids = adjust_relid_set(cur_em->em_relids, relid, subst); - if (sjinfo != NULL) - cur_em->em_relids = adjust_relid_set(cur_em->em_relids, - sjinfo->ojrelid, subst); + /* em_relids is likely to be shared with some RestrictInfo */ + cur_em->em_relids = bms_copy(cur_em->em_relids); + cur_em->em_relids = bms_del_member(cur_em->em_relids, relid); + cur_em->em_relids = bms_del_member(cur_em->em_relids, ojrelid); if (bms_is_empty(cur_em->em_relids)) ec->ec_members = foreach_delete_current(ec->ec_members, lc); } @@ -761,11 +797,7 @@ remove_rel_from_eclass(EquivalenceClass *ec, SpecialJoinInfo *sjinfo, { RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc); - if (sjinfo == NULL) - ChangeVarNodesExtended((Node *) rinfo, relid, subst, 0, - replace_relid_callback); - else - remove_rel_from_restrictinfo(rinfo, relid, sjinfo->ojrelid); + remove_rel_from_restrictinfo(rinfo, relid, ojrelid); } /* @@ -990,11 +1022,10 @@ rel_is_distinct_for(PlannerInfo *root, RelOptInfo *rel, List *clause_list, { /* * Examine the indexes to see if we have a matching unique index. - * relation_has_unique_index_ext automatically adds any usable + * relation_has_unique_index_for automatically adds any usable * restriction clauses for the rel, so we needn't do that here. */ - if (relation_has_unique_index_ext(root, rel, clause_list, NIL, NIL, - extra_clauses)) + if (relation_has_unique_index_for(root, rel, clause_list, extra_clauses)) return true; } else if (rel->rtekind == RTE_SUBQUERY) @@ -1175,6 +1206,8 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) } else if (query->groupingSets) { + List *gsets; + /* * If we have grouping sets with expressions, we probably don't have * uniqueness and analysis would be hard. Punt. @@ -1184,15 +1217,17 @@ query_is_distinct_for(Query *query, List *colnos, List *opids) /* * If we have no groupClause (therefore no grouping expressions), we - * might have one or many empty grouping sets. If there's just one, - * then we're returning only one row and are certainly unique. But - * otherwise, we know we're certainly not unique. + * might have one or many empty grouping sets. If there's just one, + * or if the DISTINCT clause is used on the GROUP BY, then we're + * returning only one row and are certainly unique. But otherwise, we + * know we're certainly not unique. */ - if (list_length(query->groupingSets) == 1 && - ((GroupingSet *) linitial(query->groupingSets))->kind == GROUPING_SET_EMPTY) + if (query->groupDistinct) return true; - else - return false; + + gsets = expand_grouping_sets(query->groupingSets, false, -1); + + return (list_length(gsets) == 1); } else { @@ -1425,17 +1460,14 @@ innerrel_is_unique_ext(PlannerInfo *root, * * However, in normal planning mode, caching this knowledge is totally * pointless; it won't be queried again, because we build up joinrels - * from smaller to larger. It is useful in GEQO mode, where the - * knowledge can be carried across successive planning attempts; and - * it's likely to be useful when using join-search plugins, too. Hence - * cache when join_search_private is non-NULL. (Yeah, that's a hack, - * but it seems reasonable.) + * from smaller to larger. It's only useful when using GEQO or + * another planner extension that attempts planning multiple times. * * Also, allow callers to override that heuristic and force caching; * that's useful for reduce_unique_semijoins, which calls here before * the normal join search starts. */ - if (force_cache || root->join_search_private) + if (force_cache || root->assumeReplanning) { old_context = MemoryContextSwitchTo(root->planner_cxt); innerrel->non_unique_for_rels = @@ -1686,14 +1718,14 @@ add_non_redundant_clauses(PlannerInfo *root, } /* - * A custom callback for ChangeVarNodesExtended() providing - * Self-join elimination (SJE) related functionality + * A custom callback for ChangeVarNodesExtended() providing Self-join + * elimination (SJE) related functionality * - * SJE needs to skip the RangeTblRef node - * type. During SJE's last step, remove_rel_from_joinlist() removes - * remaining RangeTblRefs with target relid. If ChangeVarNodes() replaces - * the target relid before, remove_rel_from_joinlist() fails to identify - * the nodes to delete. + * SJE needs to skip the RangeTblRef node type. During SJE's last + * step, remove_rel_from_joinlist() removes remaining RangeTblRefs + * with target relid. If ChangeVarNodes() replaces the target relid + * before, remove_rel_from_joinlist() would fail to identify the nodes + * to delete. * * SJE also needs to change the relids within RestrictInfo's. */ @@ -1772,8 +1804,8 @@ replace_relid_callback(Node *node, ChangeVarNodes_context *context) /* * For self-join elimination, changing varnos could transform * "t1.a = t2.a" into "t1.a = t1.a". That is always true as long - * as "t1.a" is not null. We use qual() to check for such a case, - * and then we replace the qual for a check for not null + * as "t1.a" is not null. We use equal() to check for such a + * case, and then we replace the qual with a check for not null * (NullTest). */ if (leftOp != NULL && equal(leftOp, rightOp)) @@ -1956,14 +1988,11 @@ remove_self_join_rel(PlannerInfo *root, PlanRowMark *kmark, PlanRowMark *rmark, 0, replace_relid_callback); /* Replace links in the planner info */ - remove_rel_from_query(root, toRemove, toKeep->relid, NULL, NULL); + remove_rel_from_query(root, toRemove->relid, toKeep->relid, NULL, NULL); - /* At last, replace varno in root targetlist and HAVING clause */ + /* Replace varno in the fully-processed targetlist */ ChangeVarNodesExtended((Node *) root->processed_tlist, toRemove->relid, toKeep->relid, 0, replace_relid_callback); - ChangeVarNodesExtended((Node *) root->processed_groupClause, - toRemove->relid, toKeep->relid, 0, - replace_relid_callback); adjust_relid_set(root->all_result_relids, toRemove->relid, toKeep->relid); adjust_relid_set(root->leaf_result_relids, toRemove->relid, toKeep->relid); @@ -1979,10 +2008,12 @@ remove_self_join_rel(PlannerInfo *root, PlanRowMark *kmark, PlanRowMark *rmark, * remove_join_clause_from_rels will touch it.) */ root->simple_rel_array[toRemove->relid] = NULL; + root->simple_rte_array[toRemove->relid] = NULL; /* And nuke the RelOptInfo, just in case there's another access path. */ pfree(toRemove); + /* * Now repeat construction of attr_needed bits coming from all other * sources. @@ -2142,21 +2173,21 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) while ((r = bms_next_member(relids, r)) > 0) { - RelOptInfo *inner = root->simple_rel_array[r]; + RelOptInfo *rrel = root->simple_rel_array[r]; k = r; while ((k = bms_next_member(relids, k)) > 0) { Relids joinrelids = NULL; - RelOptInfo *outer = root->simple_rel_array[k]; + RelOptInfo *krel = root->simple_rel_array[k]; List *restrictlist; List *selfjoinquals; List *otherjoinquals; ListCell *lc; bool jinfo_check = true; - PlanRowMark *omark = NULL; - PlanRowMark *imark = NULL; + PlanRowMark *kmark = NULL; + PlanRowMark *rmark = NULL; List *uclauses = NIL; /* A sanity check: the relations have the same Oid. */ @@ -2194,21 +2225,21 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) { PlanRowMark *rowMark = (PlanRowMark *) lfirst(lc); - if (rowMark->rti == k) + if (rowMark->rti == r) { - Assert(imark == NULL); - imark = rowMark; + Assert(rmark == NULL); + rmark = rowMark; } - else if (rowMark->rti == r) + else if (rowMark->rti == k) { - Assert(omark == NULL); - omark = rowMark; + Assert(kmark == NULL); + kmark = rowMark; } - if (omark && imark) + if (kmark && rmark) break; } - if (omark && imark && omark->markType != imark->markType) + if (kmark && rmark && kmark->markType != rmark->markType) continue; /* @@ -2229,8 +2260,8 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) * build_joinrel_restrictlist() routine. */ restrictlist = generate_join_implied_equalities(root, joinrelids, - inner->relids, - outer, NULL); + rrel->relids, + krel, NULL); if (restrictlist == NIL) continue; @@ -2240,7 +2271,7 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) * otherjoinquals. */ split_selfjoin_quals(root, restrictlist, &selfjoinquals, - &otherjoinquals, inner->relid, outer->relid); + &otherjoinquals, rrel->relid, krel->relid); Assert(list_length(restrictlist) == (list_length(selfjoinquals) + list_length(otherjoinquals))); @@ -2251,17 +2282,17 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) * degenerate case works only if both sides have the same clause. * So doesn't matter which side to add. */ - selfjoinquals = list_concat(selfjoinquals, outer->baserestrictinfo); + selfjoinquals = list_concat(selfjoinquals, krel->baserestrictinfo); /* - * Determine if the inner table can duplicate outer rows. We must - * bypass the unique rel cache here since we're possibly using a - * subset of join quals. We can use 'force_cache' == true when all - * join quals are self-join quals. Otherwise, we could end up - * putting false negatives in the cache. + * Determine if the rrel can duplicate outer rows. We must bypass + * the unique rel cache here since we're possibly using a subset + * of join quals. We can use 'force_cache' == true when all join + * quals are self-join quals. Otherwise, we could end up putting + * false negatives in the cache. */ - if (!innerrel_is_unique_ext(root, joinrelids, inner->relids, - outer, JOIN_INNER, selfjoinquals, + if (!innerrel_is_unique_ext(root, joinrelids, rrel->relids, + krel, JOIN_INNER, selfjoinquals, list_length(otherjoinquals) == 0, &uclauses)) continue; @@ -2277,14 +2308,14 @@ remove_self_joins_one_group(PlannerInfo *root, Relids relids) * expressions, or we won't match the same row on each side of the * join. */ - if (!match_unique_clauses(root, inner, uclauses, outer->relid)) + if (!match_unique_clauses(root, rrel, uclauses, krel->relid)) continue; /* - * We can remove either relation, so remove the inner one in order - * to simplify this loop. + * Remove rrel RelOptInfo from the planner structures and the + * corresponding row mark. */ - remove_self_join_rel(root, omark, imark, outer, inner, restrictlist); + remove_self_join_rel(root, kmark, rmark, krel, rrel, restrictlist); result = bms_add_member(result, r); @@ -2359,8 +2390,7 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove) * In order to find relations with the same oid we first build an array of * candidates and then sort it by oid. */ - candidates = (SelfJoinCandidate *) palloc(sizeof(SelfJoinCandidate) * - numRels); + candidates = palloc_array(SelfJoinCandidate, numRels); i = -1; j = 0; while ((i = bms_next_member(relids, i)) >= 0) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 4ad30b7627e6e..de6a183da7996 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -5,7 +5,7 @@ * Planning is complete, we just need to convert the selected * Path into a Plan. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,9 +16,8 @@ */ #include "postgres.h" -#include - #include "access/sysattr.h" +#include "access/transam.h" #include "catalog/pg_class.h" #include "foreign/fdwapi.h" #include "miscadmin.h" @@ -95,19 +94,17 @@ static Material *create_material_plan(PlannerInfo *root, MaterialPath *best_path int flags); static Memoize *create_memoize_plan(PlannerInfo *root, MemoizePath *best_path, int flags); -static Plan *create_unique_plan(PlannerInfo *root, UniquePath *best_path, - int flags); static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path); static Plan *create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags); -static Plan *inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe); +static Plan *inject_projection_plan(Plan *subplan, List *tlist, + bool parallel_safe); static Sort *create_sort_plan(PlannerInfo *root, SortPath *best_path, int flags); static IncrementalSort *create_incrementalsort_plan(PlannerInfo *root, IncrementalSortPath *best_path, int flags); static Group *create_group_plan(PlannerInfo *root, GroupPath *best_path); -static Unique *create_upper_unique_plan(PlannerInfo *root, UpperUniquePath *best_path, - int flags); +static Unique *create_unique_plan(PlannerInfo *root, UniquePath *best_path, int flags); static Agg *create_agg_plan(PlannerInfo *root, AggPath *best_path); static Plan *create_groupingsets_plan(PlannerInfo *root, GroupingSetsPath *best_path); static Result *create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path); @@ -228,7 +225,7 @@ static RecursiveUnion *make_recursive_union(List *tlist, Plan *righttree, int wtParam, List *distinctList, - long numGroups); + Cardinality numGroups); static BitmapAnd *make_bitmap_and(List *bitmapplans); static BitmapOr *make_bitmap_or(List *bitmapplans); static NestLoop *make_nestloop(List *tlist, @@ -284,7 +281,10 @@ static Material *make_material(Plan *lefttree); static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators, Oid *collations, List *param_exprs, bool singlerow, bool binary_mode, - uint32 est_entries, Bitmapset *keyparamids); + uint32 est_entries, Bitmapset *keyparamids, + Cardinality est_calls, + Cardinality est_unique_keys, + double est_hit_ratio); static WindowAgg *make_windowagg(List *tlist, WindowClause *wc, int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, @@ -293,27 +293,29 @@ static WindowAgg *make_windowagg(List *tlist, WindowClause *wc, static Group *make_group(List *tlist, List *qual, int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations, Plan *lefttree); -static Unique *make_unique_from_sortclauses(Plan *lefttree, List *distinctList); static Unique *make_unique_from_pathkeys(Plan *lefttree, - List *pathkeys, int numCols); + List *pathkeys, int numCols, + Relids relids); static Gather *make_gather(List *qptlist, List *qpqual, int nworkers, int rescan_param, bool single_copy, Plan *subplan); static SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy, List *tlist, Plan *lefttree, Plan *righttree, - List *groupList, long numGroups); + List *groupList, Cardinality numGroups); static LockRows *make_lockrows(Plan *lefttree, List *rowMarks, int epqParam); -static Result *make_result(List *tlist, Node *resconstantqual, Plan *subplan); +static Result *make_gating_result(List *tlist, Node *resconstantqual, + Plan *subplan); +static Result *make_one_row_result(List *tlist, Node *resconstantqual, + RelOptInfo *rel); static ProjectSet *make_project_set(List *tlist, Plan *subplan); static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan, CmdType operation, bool canSetTag, Index nominalRelation, Index rootRelation, - bool partColsUpdated, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, List *mergeActionLists, List *mergeJoinConditions, - int epqParam); + ForPortionOfExpr *forPortionOf, int epqParam); static GatherMerge *create_gather_merge_plan(PlannerInfo *root, GatherMergePath *best_path); @@ -467,19 +469,9 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags) flags); break; case T_Unique: - if (IsA(best_path, UpperUniquePath)) - { - plan = (Plan *) create_upper_unique_plan(root, - (UpperUniquePath *) best_path, - flags); - } - else - { - Assert(IsA(best_path, UniquePath)); - plan = create_unique_plan(root, - (UniquePath *) best_path, - flags); - } + plan = (Plan *) create_unique_plan(root, + (UniquePath *) best_path, + flags); break; case T_Gather: plan = (Plan *) create_gather_plan(root, @@ -1022,36 +1014,36 @@ static Plan * create_gating_plan(PlannerInfo *root, Path *path, Plan *plan, List *gating_quals) { - Plan *gplan; - Plan *splan; + Result *gplan; Assert(gating_quals); /* - * We might have a trivial Result plan already. Stacking one Result atop - * another is silly, so if that applies, just discard the input plan. + * Since we need a Result node anyway, always return the path's requested + * tlist; that's never a wrong choice, even if the parent node didn't ask + * for CP_EXACT_TLIST. + */ + gplan = make_gating_result(build_path_tlist(root, path), + (Node *) gating_quals, plan); + + /* + * We might have had a trivial Result plan already. Stacking one Result + * atop another is silly, so if that applies, just discard the input plan. * (We're assuming its targetlist is uninteresting; it should be either - * the same as the result of build_path_tlist, or a simplified version.) + * the same as the result of build_path_tlist, or a simplified version. + * However, we preserve the set of relids that it purports to scan and + * attribute that to our replacement Result instead, and likewise for the + * result_type.) */ - splan = plan; if (IsA(plan, Result)) { Result *rplan = (Result *) plan; - if (rplan->plan.lefttree == NULL && - rplan->resconstantqual == NULL) - splan = NULL; + gplan->plan.lefttree = NULL; + gplan->relids = rplan->relids; + gplan->result_type = rplan->result_type; } - /* - * Since we need a Result node anyway, always return the path's requested - * tlist; that's never a wrong choice, even if the parent node didn't ask - * for CP_EXACT_TLIST. - */ - gplan = (Plan *) make_result(build_path_tlist(root, path), - (Node *) gating_quals, - splan); - /* * Notice that we don't change cost or size estimates when doing gating. * The costs of qual eval were already included in the subplan's cost. @@ -1064,12 +1056,12 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan, * in most cases we have only a very bad idea of the probability of the * gating qual being true. */ - copy_plan_costsize(gplan, plan); + copy_plan_costsize(&gplan->plan, plan); /* Gating quals could be unsafe, so better use the Path's safety flag */ - gplan->parallel_safe = path->parallel_safe; + gplan->plan.parallel_safe = path->parallel_safe; - return gplan; + return &gplan->plan; } /* @@ -1245,10 +1237,10 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags) /* Generate a Result plan with constant-FALSE gating qual */ Plan *plan; - plan = (Plan *) make_result(tlist, - (Node *) list_make1(makeBoolConst(false, - false)), - NULL); + plan = (Plan *) make_one_row_result(tlist, + (Node *) list_make1(makeBoolConst(false, + false)), + best_path->path.parent); copy_generic_path_info(plan, (Path *) best_path); @@ -1272,6 +1264,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags) plan->plan.lefttree = NULL; plan->plan.righttree = NULL; plan->apprelids = rel->relids; + plan->child_append_relid_sets = best_path->child_append_relid_sets; if (pathkeys != NIL) { @@ -1318,6 +1311,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags) Oid *sortOperators; Oid *collations; bool *nullsFirst; + int presorted_keys; /* * Compute sort column info, and adjust subplan's tlist as needed. @@ -1353,14 +1347,38 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags) numsortkeys * sizeof(bool)) == 0); /* Now, insert a Sort node if subplan isn't sufficiently ordered */ - if (!pathkeys_contained_in(pathkeys, subpath->pathkeys)) + if (!pathkeys_count_contained_in(pathkeys, subpath->pathkeys, + &presorted_keys)) { - Sort *sort = make_sort(subplan, numsortkeys, + Plan *sort_plan; + + /* + * We choose to use incremental sort if it is enabled and + * there are presorted keys; otherwise we use full sort. + */ + if (enable_incremental_sort && presorted_keys > 0) + { + sort_plan = (Plan *) + make_incrementalsort(subplan, numsortkeys, presorted_keys, sortColIdx, sortOperators, collations, nullsFirst); - label_sort_with_costsize(root, sort, best_path->limit_tuples); - subplan = (Plan *) sort; + label_incrementalsort_with_costsize(root, + (IncrementalSort *) sort_plan, + pathkeys, + best_path->limit_tuples); + } + else + { + sort_plan = (Plan *) make_sort(subplan, numsortkeys, + sortColIdx, sortOperators, + collations, nullsFirst); + + label_sort_with_costsize(root, (Sort *) sort_plan, + best_path->limit_tuples); + } + + subplan = sort_plan; } } @@ -1459,6 +1477,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path, plan->lefttree = NULL; plan->righttree = NULL; node->apprelids = rel->relids; + node->child_append_relid_sets = best_path->child_append_relid_sets; /* * Compute sort column info, and adjust MergeAppend's tlist as needed. @@ -1491,6 +1510,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path, Oid *sortOperators; Oid *collations; bool *nullsFirst; + int presorted_keys; /* Build the child plan */ /* Must insist that all children return the same tlist */ @@ -1525,14 +1545,38 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path, numsortkeys * sizeof(bool)) == 0); /* Now, insert a Sort node if subplan isn't sufficiently ordered */ - if (!pathkeys_contained_in(pathkeys, subpath->pathkeys)) + if (!pathkeys_count_contained_in(pathkeys, subpath->pathkeys, + &presorted_keys)) { - Sort *sort = make_sort(subplan, numsortkeys, + Plan *sort_plan; + + /* + * We choose to use incremental sort if it is enabled and there + * are presorted keys; otherwise we use full sort. + */ + if (enable_incremental_sort && presorted_keys > 0) + { + sort_plan = (Plan *) + make_incrementalsort(subplan, numsortkeys, presorted_keys, sortColIdx, sortOperators, collations, nullsFirst); - label_sort_with_costsize(root, sort, best_path->limit_tuples); - subplan = (Plan *) sort; + label_incrementalsort_with_costsize(root, + (IncrementalSort *) sort_plan, + pathkeys, + best_path->limit_tuples); + } + else + { + sort_plan = (Plan *) make_sort(subplan, numsortkeys, + sortColIdx, sortOperators, + collations, nullsFirst); + + label_sort_with_costsize(root, (Sort *) sort_plan, + best_path->limit_tuples); + } + + subplan = sort_plan; } subplans = lappend(subplans, subplan); @@ -1596,7 +1640,7 @@ create_group_result_plan(PlannerInfo *root, GroupResultPath *best_path) /* best_path->quals is just bare clauses */ quals = order_qual_clauses(root, best_path->quals); - plan = make_result(tlist, (Node *) quals, NULL); + plan = make_one_row_result(tlist, (Node *) quals, best_path->path.parent); copy_generic_path_info(&plan->plan, (Path *) best_path); @@ -1703,214 +1747,14 @@ create_memoize_plan(PlannerInfo *root, MemoizePath *best_path, int flags) plan = make_memoize(subplan, operators, collations, param_exprs, best_path->singlerow, best_path->binary_mode, - best_path->est_entries, keyparamids); + best_path->est_entries, keyparamids, best_path->est_calls, + best_path->est_unique_keys, best_path->est_hit_ratio); copy_generic_path_info(&plan->plan, (Path *) best_path); return plan; } -/* - * create_unique_plan - * Create a Unique plan for 'best_path' and (recursively) plans - * for its subpaths. - * - * Returns a Plan node. - */ -static Plan * -create_unique_plan(PlannerInfo *root, UniquePath *best_path, int flags) -{ - Plan *plan; - Plan *subplan; - List *in_operators; - List *uniq_exprs; - List *newtlist; - int nextresno; - bool newitems; - int numGroupCols; - AttrNumber *groupColIdx; - Oid *groupCollations; - int groupColPos; - ListCell *l; - - /* Unique doesn't project, so tlist requirements pass through */ - subplan = create_plan_recurse(root, best_path->subpath, flags); - - /* Done if we don't need to do any actual unique-ifying */ - if (best_path->umethod == UNIQUE_PATH_NOOP) - return subplan; - - /* - * As constructed, the subplan has a "flat" tlist containing just the Vars - * needed here and at upper levels. The values we are supposed to - * unique-ify may be expressions in these variables. We have to add any - * such expressions to the subplan's tlist. - * - * The subplan may have a "physical" tlist if it is a simple scan plan. If - * we're going to sort, this should be reduced to the regular tlist, so - * that we don't sort more data than we need to. For hashing, the tlist - * should be left as-is if we don't need to add any expressions; but if we - * do have to add expressions, then a projection step will be needed at - * runtime anyway, so we may as well remove unneeded items. Therefore - * newtlist starts from build_path_tlist() not just a copy of the - * subplan's tlist; and we don't install it into the subplan unless we are - * sorting or stuff has to be added. - */ - in_operators = best_path->in_operators; - uniq_exprs = best_path->uniq_exprs; - - /* initialize modified subplan tlist as just the "required" vars */ - newtlist = build_path_tlist(root, &best_path->path); - nextresno = list_length(newtlist) + 1; - newitems = false; - - foreach(l, uniq_exprs) - { - Expr *uniqexpr = lfirst(l); - TargetEntry *tle; - - tle = tlist_member(uniqexpr, newtlist); - if (!tle) - { - tle = makeTargetEntry((Expr *) uniqexpr, - nextresno, - NULL, - false); - newtlist = lappend(newtlist, tle); - nextresno++; - newitems = true; - } - } - - /* Use change_plan_targetlist in case we need to insert a Result node */ - if (newitems || best_path->umethod == UNIQUE_PATH_SORT) - subplan = change_plan_targetlist(subplan, newtlist, - best_path->path.parallel_safe); - - /* - * Build control information showing which subplan output columns are to - * be examined by the grouping step. Unfortunately we can't merge this - * with the previous loop, since we didn't then know which version of the - * subplan tlist we'd end up using. - */ - newtlist = subplan->targetlist; - numGroupCols = list_length(uniq_exprs); - groupColIdx = (AttrNumber *) palloc(numGroupCols * sizeof(AttrNumber)); - groupCollations = (Oid *) palloc(numGroupCols * sizeof(Oid)); - - groupColPos = 0; - foreach(l, uniq_exprs) - { - Expr *uniqexpr = lfirst(l); - TargetEntry *tle; - - tle = tlist_member(uniqexpr, newtlist); - if (!tle) /* shouldn't happen */ - elog(ERROR, "failed to find unique expression in subplan tlist"); - groupColIdx[groupColPos] = tle->resno; - groupCollations[groupColPos] = exprCollation((Node *) tle->expr); - groupColPos++; - } - - if (best_path->umethod == UNIQUE_PATH_HASH) - { - Oid *groupOperators; - - /* - * Get the hashable equality operators for the Agg node to use. - * Normally these are the same as the IN clause operators, but if - * those are cross-type operators then the equality operators are the - * ones for the IN clause operators' RHS datatype. - */ - groupOperators = (Oid *) palloc(numGroupCols * sizeof(Oid)); - groupColPos = 0; - foreach(l, in_operators) - { - Oid in_oper = lfirst_oid(l); - Oid eq_oper; - - if (!get_compatible_hash_operators(in_oper, NULL, &eq_oper)) - elog(ERROR, "could not find compatible hash operator for operator %u", - in_oper); - groupOperators[groupColPos++] = eq_oper; - } - - /* - * Since the Agg node is going to project anyway, we can give it the - * minimum output tlist, without any stuff we might have added to the - * subplan tlist. - */ - plan = (Plan *) make_agg(build_path_tlist(root, &best_path->path), - NIL, - AGG_HASHED, - AGGSPLIT_SIMPLE, - numGroupCols, - groupColIdx, - groupOperators, - groupCollations, - NIL, - NIL, - best_path->path.rows, - 0, - subplan); - } - else - { - List *sortList = NIL; - Sort *sort; - - /* Create an ORDER BY list to sort the input compatibly */ - groupColPos = 0; - foreach(l, in_operators) - { - Oid in_oper = lfirst_oid(l); - Oid sortop; - Oid eqop; - TargetEntry *tle; - SortGroupClause *sortcl; - - sortop = get_ordering_op_for_equality_op(in_oper, false); - if (!OidIsValid(sortop)) /* shouldn't happen */ - elog(ERROR, "could not find ordering operator for equality operator %u", - in_oper); - - /* - * The Unique node will need equality operators. Normally these - * are the same as the IN clause operators, but if those are - * cross-type operators then the equality operators are the ones - * for the IN clause operators' RHS datatype. - */ - eqop = get_equality_op_for_ordering_op(sortop, NULL); - if (!OidIsValid(eqop)) /* shouldn't happen */ - elog(ERROR, "could not find equality operator for ordering operator %u", - sortop); - - tle = get_tle_by_resno(subplan->targetlist, - groupColIdx[groupColPos]); - Assert(tle != NULL); - - sortcl = makeNode(SortGroupClause); - sortcl->tleSortGroupRef = assignSortGroupRef(tle, - subplan->targetlist); - sortcl->eqop = eqop; - sortcl->sortop = sortop; - sortcl->reverse_sort = false; - sortcl->nulls_first = false; - sortcl->hashable = false; /* no need to make this accurate */ - sortList = lappend(sortList, sortcl); - groupColPos++; - } - sort = make_sort_from_sortclauses(sortList, subplan); - label_sort_with_costsize(root, sort, -1.0); - plan = (Plan *) make_unique_from_sortclauses((Plan *) sort, sortList); - } - - /* Copy cost data from Path to Plan */ - copy_generic_path_info(plan, &best_path->path); - - return plan; -} - /* * create_gather_plan * @@ -2093,8 +1937,7 @@ create_projection_plan(PlannerInfo *root, ProjectionPath *best_path, int flags) } else { - /* We need a Result node */ - plan = (Plan *) make_result(tlist, NULL, subplan); + plan = (Plan *) make_gating_result(tlist, NULL, subplan); copy_generic_path_info(plan, (Path *) best_path); } @@ -2118,7 +1961,7 @@ inject_projection_plan(Plan *subplan, List *tlist, bool parallel_safe) { Plan *plan; - plan = (Plan *) make_result(tlist, NULL, subplan); + plan = (Plan *) make_gating_result(tlist, NULL, subplan); /* * In principle, we should charge tlist eval cost plus cpu_per_tuple per @@ -2268,13 +2111,13 @@ create_group_plan(PlannerInfo *root, GroupPath *best_path) } /* - * create_upper_unique_plan + * create_unique_plan * * Create a Unique plan for 'best_path' and (recursively) plans * for its subpaths. */ static Unique * -create_upper_unique_plan(PlannerInfo *root, UpperUniquePath *best_path, int flags) +create_unique_plan(PlannerInfo *root, UniquePath *best_path, int flags) { Unique *plan; Plan *subplan; @@ -2286,9 +2129,17 @@ create_upper_unique_plan(PlannerInfo *root, UpperUniquePath *best_path, int flag subplan = create_plan_recurse(root, best_path->subpath, flags | CP_LABEL_TLIST); + /* + * make_unique_from_pathkeys calls find_ec_member_matching_expr, which + * will ignore any child EC members that don't belong to the given relids. + * Thus, if this unique path is based on a child relation, we must pass + * its relids. + */ plan = make_unique_from_pathkeys(subplan, best_path->path.pathkeys, - best_path->numkeys); + best_path->numkeys, + IS_OTHER_REL(best_path->path.parent) ? + best_path->path.parent->relids : NULL); copy_generic_path_info(&plan->plan, (Path *) best_path); @@ -2357,7 +2208,7 @@ remap_groupColIdx(PlannerInfo *root, List *groupClause) Assert(grouping_map); - new_grpColIdx = palloc0(sizeof(AttrNumber) * list_length(groupClause)); + new_grpColIdx = palloc0_array(AttrNumber, list_length(groupClause)); i = 0; foreach(lc, groupClause) @@ -2588,7 +2439,9 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path) /* Generate the output plan --- basically just a Result */ tlist = build_path_tlist(root, &best_path->path); - plan = make_result(tlist, (Node *) best_path->quals, NULL); + plan = make_one_row_result(tlist, (Node *) best_path->quals, + best_path->path.parent); + plan->result_type = RESULT_TYPE_MINMAX; copy_generic_path_info(&plan->plan, (Path *) best_path); @@ -2644,9 +2497,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) * Convert SortGroupClause lists into arrays of attr indexes and equality * operators, as wanted by executor. */ - partColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numPart); - partOperators = (Oid *) palloc(sizeof(Oid) * numPart); - partCollations = (Oid *) palloc(sizeof(Oid) * numPart); + partColIdx = palloc_array(AttrNumber, numPart); + partOperators = palloc_array(Oid, numPart); + partCollations = palloc_array(Oid, numPart); partNumCols = 0; foreach(lc, wc->partitionClause) @@ -2661,9 +2514,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) partNumCols++; } - ordColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numOrder); - ordOperators = (Oid *) palloc(sizeof(Oid) * numOrder); - ordCollations = (Oid *) palloc(sizeof(Oid) * numOrder); + ordColIdx = palloc_array(AttrNumber, numOrder); + ordOperators = palloc_array(Oid, numOrder); + ordCollations = palloc_array(Oid, numOrder); ordNumCols = 0; foreach(lc, wc->orderClause) @@ -2712,7 +2565,6 @@ create_setop_plan(PlannerInfo *root, SetOpPath *best_path, int flags) List *tlist = build_path_tlist(root, &best_path->path); Plan *leftplan; Plan *rightplan; - long numGroups; /* * SetOp doesn't project, so tlist requirements pass through; moreover we @@ -2723,16 +2575,13 @@ create_setop_plan(PlannerInfo *root, SetOpPath *best_path, int flags) rightplan = create_plan_recurse(root, best_path->rightpath, flags | CP_LABEL_TLIST); - /* Convert numGroups to long int --- but 'ware overflow! */ - numGroups = clamp_cardinality_to_long(best_path->numGroups); - plan = make_setop(best_path->cmd, best_path->strategy, tlist, leftplan, rightplan, best_path->groupList, - numGroups); + best_path->numGroups); copy_generic_path_info(&plan->plan, (Path *) best_path); @@ -2752,7 +2601,6 @@ create_recursiveunion_plan(PlannerInfo *root, RecursiveUnionPath *best_path) Plan *leftplan; Plan *rightplan; List *tlist; - long numGroups; /* Need both children to produce same tlist, so force it */ leftplan = create_plan_recurse(root, best_path->leftpath, CP_EXACT_TLIST); @@ -2760,15 +2608,12 @@ create_recursiveunion_plan(PlannerInfo *root, RecursiveUnionPath *best_path) tlist = build_path_tlist(root, &best_path->path); - /* Convert numGroups to long int --- but 'ware overflow! */ - numGroups = clamp_cardinality_to_long(best_path->numGroups); - plan = make_recursive_union(tlist, leftplan, rightplan, best_path->wtParam, best_path->distinctList, - numGroups); + best_path->numGroups); copy_generic_path_info(&plan->plan, (Path *) best_path); @@ -2823,7 +2668,6 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) best_path->canSetTag, best_path->nominalRelation, best_path->rootRelation, - best_path->partColsUpdated, best_path->resultRelations, best_path->updateColnosLists, best_path->withCheckOptionLists, @@ -2832,6 +2676,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) best_path->onconflict, best_path->mergeActionLists, best_path->mergeJoinConditions, + best_path->forPortionOf, best_path->epqParam); copy_generic_path_info(&plan->plan, &best_path->path); @@ -4039,7 +3884,8 @@ create_resultscan_plan(PlannerInfo *root, Path *best_path, replace_nestloop_params(root, (Node *) scan_clauses); } - scan_plan = make_result(tlist, (Node *) scan_clauses, NULL); + scan_plan = make_one_row_result(tlist, (Node *) scan_clauses, + best_path->parent); copy_generic_path_info(&scan_plan->plan, best_path); @@ -4344,13 +4190,16 @@ create_nestloop_plan(PlannerInfo *root, NestLoop *join_plan; Plan *outer_plan; Plan *inner_plan; + Relids outerrelids; List *tlist = build_path_tlist(root, &best_path->jpath.path); List *joinrestrictclauses = best_path->jpath.joinrestrictinfo; List *joinclauses; List *otherclauses; - Relids outerrelids; List *nestParams; + List *outer_tlist; + bool outer_parallel_safe; Relids saveOuterRels = root->curOuterRels; + ListCell *lc; /* * If the inner path is parameterized by the topmost parent of the outer @@ -4372,8 +4221,8 @@ create_nestloop_plan(PlannerInfo *root, outer_plan = create_plan_recurse(root, best_path->jpath.outerjoinpath, 0); /* For a nestloop, include outer relids in curOuterRels for inner side */ - root->curOuterRels = bms_union(root->curOuterRels, - best_path->jpath.outerjoinpath->parent->relids); + outerrelids = best_path->jpath.outerjoinpath->parent->relids; + root->curOuterRels = bms_union(root->curOuterRels, outerrelids); inner_plan = create_plan_recurse(root, best_path->jpath.innerjoinpath, 0); @@ -4412,9 +4261,66 @@ create_nestloop_plan(PlannerInfo *root, * Identify any nestloop parameters that should be supplied by this join * node, and remove them from root->curOuterParams. */ - outerrelids = best_path->jpath.outerjoinpath->parent->relids; - nestParams = identify_current_nestloop_params(root, outerrelids); + nestParams = identify_current_nestloop_params(root, + outerrelids, + PATH_REQ_OUTER((Path *) best_path)); + + /* + * While nestloop parameters that are Vars had better be available from + * the outer_plan already, there are edge cases where nestloop parameters + * that are PHVs won't be. In such cases we must add them to the + * outer_plan's tlist, since the executor's NestLoopParam machinery + * requires the params to be simple outer-Var references to that tlist. + * (This is cheating a little bit, because the outer path's required-outer + * relids might not be enough to allow evaluating such a PHV. But in + * practice, if we could have evaluated the PHV at the nestloop node, we + * can do so in the outer plan too.) + */ + outer_tlist = outer_plan->targetlist; + outer_parallel_safe = outer_plan->parallel_safe; + foreach(lc, nestParams) + { + NestLoopParam *nlp = (NestLoopParam *) lfirst(lc); + PlaceHolderVar *phv; + TargetEntry *tle; + + if (IsA(nlp->paramval, Var)) + continue; /* nothing to do for simple Vars */ + /* Otherwise it must be a PHV */ + phv = castNode(PlaceHolderVar, nlp->paramval); + + if (tlist_member((Expr *) phv, outer_tlist)) + continue; /* already available */ + + /* + * It's possible that nestloop parameter PHVs selected to evaluate + * here contain references to surviving root->curOuterParams items + * (that is, they reference values that will be supplied by some + * higher-level nestloop). Those need to be converted to Params now. + * Note: it's safe to do this after the tlist_member() check, because + * equal() won't pay attention to phv->phexpr. + */ + phv->phexpr = (Expr *) replace_nestloop_params(root, + (Node *) phv->phexpr); + + /* Make a shallow copy of outer_tlist, if we didn't already */ + if (outer_tlist == outer_plan->targetlist) + outer_tlist = list_copy(outer_tlist); + /* ... and add the needed expression */ + tle = makeTargetEntry((Expr *) copyObject(phv), + list_length(outer_tlist) + 1, + NULL, + true); + outer_tlist = lappend(outer_tlist, tle); + /* ... and track whether tlist is (still) parallel-safe */ + if (outer_parallel_safe) + outer_parallel_safe = is_parallel_safe(root, (Node *) phv); + } + if (outer_tlist != outer_plan->targetlist) + outer_plan = change_plan_targetlist(outer_plan, outer_tlist, + outer_parallel_safe); + /* And finally, we can build the join plan node */ join_plan = make_nestloop(tlist, joinclauses, otherclauses, @@ -5196,7 +5102,8 @@ fix_indexqual_clause(PlannerInfo *root, IndexOptInfo *index, int indexcol, * equal to the index's attribute number (index column position). * * Most of the code here is just for sanity cross-checking that the given - * expression actually matches the index column it's claimed to. + * expression actually matches the index column it's claimed to. It should + * match the logic in match_index_to_operand(). */ static Node * fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol) @@ -5205,14 +5112,19 @@ fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol) int pos; ListCell *indexpr_item; + Assert(indexcol >= 0 && indexcol < index->ncolumns); + + /* + * Remove any PlaceHolderVar wrapping of the indexkey + */ + node = strip_noop_phvs(node); + /* * Remove any binary-compatible relabeling of the indexkey */ - if (IsA(node, RelabelType)) + while (IsA(node, RelabelType)) node = (Node *) ((RelabelType *) node)->arg; - Assert(indexcol >= 0 && indexcol < index->ncolumns); - if (index->indexkeys[indexcol] != 0) { /* It's a simple index column */ @@ -5933,7 +5845,7 @@ make_recursive_union(List *tlist, Plan *righttree, int wtParam, List *distinctList, - long numGroups) + Cardinality numGroups) { RecursiveUnion *node = makeNode(RecursiveUnion); Plan *plan = &node->plan; @@ -5958,9 +5870,9 @@ make_recursive_union(List *tlist, Oid *dupCollations; ListCell *slitem; - dupColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numCols); - dupOperators = (Oid *) palloc(sizeof(Oid) * numCols); - dupCollations = (Oid *) palloc(sizeof(Oid) * numCols); + dupColIdx = palloc_array(AttrNumber, numCols); + dupOperators = palloc_array(Oid, numCols); + dupCollations = palloc_array(Oid, numCols); foreach(slitem, distinctList) { @@ -6597,7 +6509,7 @@ Plan * materialize_finished_plan(Plan *subplan) { Plan *matplan; - Path matpath; /* dummy for result of cost_material */ + Path matpath; /* dummy for cost_material */ Cost initplan_cost; bool unsafe_initplans; @@ -6620,6 +6532,7 @@ materialize_finished_plan(Plan *subplan) /* Set cost data */ cost_material(&matpath, + enable_material, subplan->disabled_nodes, subplan->startup_cost, subplan->total_cost, @@ -6639,7 +6552,9 @@ materialize_finished_plan(Plan *subplan) static Memoize * make_memoize(Plan *lefttree, Oid *hashoperators, Oid *collations, List *param_exprs, bool singlerow, bool binary_mode, - uint32 est_entries, Bitmapset *keyparamids) + uint32 est_entries, Bitmapset *keyparamids, + Cardinality est_calls, Cardinality est_unique_keys, + double est_hit_ratio) { Memoize *node = makeNode(Memoize); Plan *plan = &node->plan; @@ -6657,6 +6572,9 @@ make_memoize(Plan *lefttree, Oid *hashoperators, Oid *collations, node->binary_mode = binary_mode; node->est_entries = est_entries; node->keyparamids = keyparamids; + node->est_calls = est_calls; + node->est_unique_keys = est_unique_keys; + node->est_hit_ratio = est_hit_ratio; return node; } @@ -6665,15 +6583,11 @@ Agg * make_agg(List *tlist, List *qual, AggStrategy aggstrategy, AggSplit aggsplit, int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations, - List *groupingSets, List *chain, double dNumGroups, + List *groupingSets, List *chain, Cardinality numGroups, Size transitionSpace, Plan *lefttree) { Agg *node = makeNode(Agg); Plan *plan = &node->plan; - long numGroups; - - /* Reduce to long, but 'ware overflow! */ - numGroups = clamp_cardinality_to_long(dNumGroups); node->aggstrategy = aggstrategy; node->aggsplit = aggsplit; @@ -6761,61 +6675,14 @@ make_group(List *tlist, } /* - * distinctList is a list of SortGroupClauses, identifying the targetlist items - * that should be considered by the Unique filter. The input path must - * already be sorted accordingly. - */ -static Unique * -make_unique_from_sortclauses(Plan *lefttree, List *distinctList) -{ - Unique *node = makeNode(Unique); - Plan *plan = &node->plan; - int numCols = list_length(distinctList); - int keyno = 0; - AttrNumber *uniqColIdx; - Oid *uniqOperators; - Oid *uniqCollations; - ListCell *slitem; - - plan->targetlist = lefttree->targetlist; - plan->qual = NIL; - plan->lefttree = lefttree; - plan->righttree = NULL; - - /* - * convert SortGroupClause list into arrays of attr indexes and equality - * operators, as wanted by executor - */ - Assert(numCols > 0); - uniqColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numCols); - uniqOperators = (Oid *) palloc(sizeof(Oid) * numCols); - uniqCollations = (Oid *) palloc(sizeof(Oid) * numCols); - - foreach(slitem, distinctList) - { - SortGroupClause *sortcl = (SortGroupClause *) lfirst(slitem); - TargetEntry *tle = get_sortgroupclause_tle(sortcl, plan->targetlist); - - uniqColIdx[keyno] = tle->resno; - uniqOperators[keyno] = sortcl->eqop; - uniqCollations[keyno] = exprCollation((Node *) tle->expr); - Assert(OidIsValid(uniqOperators[keyno])); - keyno++; - } - - node->numCols = numCols; - node->uniqColIdx = uniqColIdx; - node->uniqOperators = uniqOperators; - node->uniqCollations = uniqCollations; - - return node; -} - -/* - * as above, but use pathkeys to identify the sort columns and semantics + * pathkeys is a list of PathKeys, identifying the sort columns and semantics. + * The input plan must already be sorted accordingly. + * + * relids identifies the child relation being unique-ified, if any. */ static Unique * -make_unique_from_pathkeys(Plan *lefttree, List *pathkeys, int numCols) +make_unique_from_pathkeys(Plan *lefttree, List *pathkeys, int numCols, + Relids relids) { Unique *node = makeNode(Unique); Plan *plan = &node->plan; @@ -6836,9 +6703,9 @@ make_unique_from_pathkeys(Plan *lefttree, List *pathkeys, int numCols) * prepare_sort_from_pathkeys ... maybe unify sometime? */ Assert(numCols >= 0 && numCols <= list_length(pathkeys)); - uniqColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numCols); - uniqOperators = (Oid *) palloc(sizeof(Oid) * numCols); - uniqCollations = (Oid *) palloc(sizeof(Oid) * numCols); + uniqColIdx = palloc_array(AttrNumber, numCols); + uniqOperators = palloc_array(Oid, numCols); + uniqCollations = palloc_array(Oid, numCols); foreach(lc, pathkeys) { @@ -6878,7 +6745,7 @@ make_unique_from_pathkeys(Plan *lefttree, List *pathkeys, int numCols) foreach(j, plan->targetlist) { tle = (TargetEntry *) lfirst(j); - em = find_ec_member_matching_expr(ec, tle->expr, NULL); + em = find_ec_member_matching_expr(ec, tle->expr, relids); if (em) { /* found expr already in tlist */ @@ -6952,7 +6819,7 @@ make_gather(List *qptlist, static SetOp * make_setop(SetOpCmd cmd, SetOpStrategy strategy, List *tlist, Plan *lefttree, Plan *righttree, - List *groupList, long numGroups) + List *groupList, Cardinality numGroups) { SetOp *node = makeNode(SetOp); Plan *plan = &node->plan; @@ -6973,10 +6840,10 @@ make_setop(SetOpCmd cmd, SetOpStrategy strategy, * convert SortGroupClause list into arrays of attr indexes and comparison * operators, as wanted by executor */ - cmpColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numCols); - cmpOperators = (Oid *) palloc(sizeof(Oid) * numCols); - cmpCollations = (Oid *) palloc(sizeof(Oid) * numCols); - cmpNullsFirst = (bool *) palloc(sizeof(bool) * numCols); + cmpColIdx = palloc_array(AttrNumber, numCols); + cmpOperators = palloc_array(Oid, numCols); + cmpCollations = palloc_array(Oid, numCols); + cmpNullsFirst = palloc_array(bool, numCols); foreach(slitem, groupList) { @@ -7056,22 +6923,57 @@ make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount, } /* - * make_result - * Build a Result plan node + * make_gating_result + * Build a Result plan node that performs projection of a subplan, and/or + * applies a one time filter (resconstantqual) */ static Result * -make_result(List *tlist, - Node *resconstantqual, - Plan *subplan) +make_gating_result(List *tlist, + Node *resconstantqual, + Plan *subplan) { Result *node = makeNode(Result); Plan *plan = &node->plan; + Assert(subplan != NULL); + plan->targetlist = tlist; plan->qual = NIL; plan->lefttree = subplan; plan->righttree = NULL; + node->result_type = RESULT_TYPE_GATING; node->resconstantqual = resconstantqual; + node->relids = NULL; + + return node; +} + +/* + * make_one_row_result + * Build a Result plan node that returns a single row (or possibly no rows, + * if the one-time filtered defined by resconstantqual returns false) + * + * 'rel' should be this path's RelOptInfo. In essence, we're saying that this + * Result node generates all the tuples for that RelOptInfo. Note that the same + * consideration can never arise in make_gating_result(), because in that case + * the tuples are always coming from some subordinate node. + */ +static Result * +make_one_row_result(List *tlist, + Node *resconstantqual, + RelOptInfo *rel) +{ + Result *node = makeNode(Result); + Plan *plan = &node->plan; + + plan->targetlist = tlist; + plan->qual = NIL; + plan->lefttree = NULL; + plan->righttree = NULL; + node->result_type = IS_UPPER_REL(rel) ? RESULT_TYPE_UPPER : + IS_JOIN_REL(rel) ? RESULT_TYPE_JOIN : RESULT_TYPE_SCAN; + node->resconstantqual = resconstantqual; + node->relids = rel->relids; return node; } @@ -7103,17 +7005,18 @@ static ModifyTable * make_modifytable(PlannerInfo *root, Plan *subplan, CmdType operation, bool canSetTag, Index nominalRelation, Index rootRelation, - bool partColsUpdated, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, List *mergeActionLists, List *mergeJoinConditions, - int epqParam) + ForPortionOfExpr *forPortionOf, int epqParam) { ModifyTable *node = makeNode(ModifyTable); bool returning_old_or_new = false; bool returning_old_or_new_valid = false; + bool transition_tables = false; + bool transition_tables_valid = false; List *fdw_private_list; Bitmapset *direct_modify_plans; ListCell *lc; @@ -7138,11 +7041,11 @@ make_modifytable(PlannerInfo *root, Plan *subplan, node->canSetTag = canSetTag; node->nominalRelation = nominalRelation; node->rootRelation = rootRelation; - node->partColsUpdated = partColsUpdated; node->resultRelations = resultRelations; if (!onconflict) { node->onConflictAction = ONCONFLICT_NONE; + node->onConflictLockStrength = LCS_NONE; node->onConflictSet = NIL; node->onConflictCols = NIL; node->onConflictWhere = NULL; @@ -7154,6 +7057,9 @@ make_modifytable(PlannerInfo *root, Plan *subplan, { node->onConflictAction = onconflict->action; + /* Lock strength for ON CONFLICT DO SELECT [FOR UPDATE/SHARE] */ + node->onConflictLockStrength = onconflict->lockStrength; + /* * Here we convert the ON CONFLICT UPDATE tlist, if any, to the * executor's convention of having consecutive resno's. The actual @@ -7177,6 +7083,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan, node->exclRelTlist = onconflict->exclRelTlist; } node->updateColnosLists = updateColnosLists; + node->forPortionOf = (Node *) forPortionOf; node->withCheckOptionLists = withCheckOptionLists; node->returningOldAlias = root->parse->returningOldAlias; node->returningNewAlias = root->parse->returningNewAlias; @@ -7260,8 +7167,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan, * callback functions needed for that and (2) there are no local * structures that need to be run for each modified row: row-level * triggers on the foreign table, stored generated columns, WITH CHECK - * OPTIONs from parent views, or Vars returning OLD/NEW in the - * RETURNING list. + * OPTIONs from parent views, Vars returning OLD/NEW in the RETURNING + * list, or transition tables on the named relation. */ direct_modify = false; if (fdwroutine != NULL && @@ -7273,7 +7180,10 @@ make_modifytable(PlannerInfo *root, Plan *subplan, !has_row_triggers(root, rti, operation) && !has_stored_generated_columns(root, rti)) { - /* returning_old_or_new is the same for all result relations */ + /* + * returning_old_or_new and transition_tables are the same for all + * result relations, respectively + */ if (!returning_old_or_new_valid) { returning_old_or_new = @@ -7282,7 +7192,18 @@ make_modifytable(PlannerInfo *root, Plan *subplan, returning_old_or_new_valid = true; } if (!returning_old_or_new) - direct_modify = fdwroutine->PlanDirectModify(root, node, rti, i); + { + if (!transition_tables_valid) + { + transition_tables = has_transition_tables(root, + nominalRelation, + operation); + transition_tables_valid = true; + } + if (!transition_tables) + direct_modify = fdwroutine->PlanDirectModify(root, node, + rti, i); + } } if (direct_modify) direct_modify_plans = bms_add_member(direct_modify_plans, i); diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 01804b085b3ba..96ee312ebdf52 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -3,7 +3,7 @@ * initsplan.c * Target list, group by, qualification, joininfo initialization routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,8 @@ */ #include "postgres.h" +#include "access/nbtree.h" +#include "access/sysattr.h" #include "catalog/pg_constraint.h" #include "catalog/pg_type.h" #include "nodes/makefuncs.h" @@ -81,6 +83,12 @@ typedef struct JoinTreeItem } JoinTreeItem; +static bool is_partial_agg_memory_risky(PlannerInfo *root); +static void create_agg_clause_infos(PlannerInfo *root); +static void create_grouping_expr_infos(PlannerInfo *root); +static EquivalenceClass *get_eclass_for_sortgroupclause(PlannerInfo *root, + SortGroupClause *sgc, + Expr *expr); static void extract_lateral_references(PlannerInfo *root, RelOptInfo *brel, Index rtindex); static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode, @@ -431,8 +439,7 @@ remove_useless_groupby_columns(PlannerInfo *root) * Fill groupbyattnos[k] with a bitmapset of the column attnos of RTE k * that are GROUP BY items. */ - groupbyattnos = (Bitmapset **) palloc0(sizeof(Bitmapset *) * - (list_length(parse->rtable) + 1)); + groupbyattnos = palloc0_array(Bitmapset *, list_length(parse->rtable) + 1); foreach(lc, root->processed_groupClause) { SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); @@ -590,8 +597,7 @@ remove_useless_groupby_columns(PlannerInfo *root) * allocate the surplusvars[] array until we find something. */ if (surplusvars == NULL) - surplusvars = (Bitmapset **) palloc0(sizeof(Bitmapset *) * - (list_length(parse->rtable) + 1)); + surplusvars = palloc0_array(Bitmapset *, list_length(parse->rtable) + 1); /* Remember the attnos of the removable columns */ surplusvars[relid] = bms_difference(relattnos, best_keycolumns); @@ -628,6 +634,387 @@ remove_useless_groupby_columns(PlannerInfo *root) } } +/* + * setup_eager_aggregation + * Check if eager aggregation is applicable, and if so collect suitable + * aggregate expressions and grouping expressions in the query. + */ +void +setup_eager_aggregation(PlannerInfo *root) +{ + /* + * Don't apply eager aggregation if disabled by user. + */ + if (!enable_eager_aggregate) + return; + + /* + * Don't apply eager aggregation if there are no available GROUP BY + * clauses. + */ + if (!root->processed_groupClause) + return; + + /* + * For now we don't try to support grouping sets. + */ + if (root->parse->groupingSets) + return; + + /* + * For now we don't try to support DISTINCT or ORDER BY aggregates. + */ + if (root->numOrderedAggs > 0) + return; + + /* + * If there are any aggregates that do not support partial mode, or any + * partial aggregates that are non-serializable, do not apply eager + * aggregation. + */ + if (root->hasNonPartialAggs || root->hasNonSerialAggs) + return; + + /* + * We don't try to apply eager aggregation if there are set-returning + * functions in targetlist. + */ + if (root->parse->hasTargetSRFs) + return; + + /* + * Eager aggregation only makes sense if there are multiple base rels in + * the query. + */ + if (bms_membership(root->all_baserels) != BMS_MULTIPLE) + return; + + /* + * Don't apply eager aggregation if any aggregate poses a risk of + * excessive memory usage during partial aggregation. + */ + if (is_partial_agg_memory_risky(root)) + return; + + /* + * Collect aggregate expressions and plain Vars that appear in the + * targetlist and havingQual. + */ + create_agg_clause_infos(root); + + /* + * If there are no suitable aggregate expressions, we cannot apply eager + * aggregation. + */ + if (root->agg_clause_list == NIL) + return; + + /* + * Collect grouping expressions that appear in grouping clauses. + */ + create_grouping_expr_infos(root); +} + +/* + * is_partial_agg_memory_risky + * Check if any aggregate poses a risk of excessive memory usage during + * partial aggregation. + * + * We check if any aggregate has a negative aggtransspace value, which + * indicates that its transition state data can grow unboundedly in size. + * Applying eager aggregation in such cases risks high memory usage since + * partial aggregation results might be stored in join hash tables or + * materialized nodes. + */ +static bool +is_partial_agg_memory_risky(PlannerInfo *root) +{ + ListCell *lc; + + foreach(lc, root->aggtransinfos) + { + AggTransInfo *transinfo = lfirst_node(AggTransInfo, lc); + + if (transinfo->aggtransspace < 0) + return true; + } + + return false; +} + +/* + * create_agg_clause_infos + * Search the targetlist and havingQual for Aggrefs and plain Vars, and + * create an AggClauseInfo for each Aggref node. + */ +static void +create_agg_clause_infos(PlannerInfo *root) +{ + List *tlist_exprs; + List *agg_clause_list = NIL; + List *tlist_vars = NIL; + Relids aggregate_relids = NULL; + bool eager_agg_applicable = true; + ListCell *lc; + + Assert(root->agg_clause_list == NIL); + Assert(root->tlist_vars == NIL); + + tlist_exprs = pull_var_clause((Node *) root->processed_tlist, + PVC_INCLUDE_AGGREGATES | + PVC_RECURSE_WINDOWFUNCS | + PVC_RECURSE_PLACEHOLDERS); + + /* + * Aggregates within the HAVING clause need to be processed in the same + * way as those in the targetlist. Note that HAVING can contain Aggrefs + * but not WindowFuncs. + */ + if (root->parse->havingQual != NULL) + { + List *having_exprs; + + having_exprs = pull_var_clause((Node *) root->parse->havingQual, + PVC_INCLUDE_AGGREGATES | + PVC_RECURSE_PLACEHOLDERS); + if (having_exprs != NIL) + { + tlist_exprs = list_concat(tlist_exprs, having_exprs); + list_free(having_exprs); + } + } + + foreach(lc, tlist_exprs) + { + Expr *expr = (Expr *) lfirst(lc); + Aggref *aggref; + Relids agg_eval_at; + AggClauseInfo *ac_info; + + /* For now we don't try to support GROUPING() expressions */ + if (IsA(expr, GroupingFunc)) + { + eager_agg_applicable = false; + break; + } + + /* Collect plain Vars for future reference */ + if (IsA(expr, Var)) + { + tlist_vars = list_append_unique(tlist_vars, expr); + continue; + } + + aggref = castNode(Aggref, expr); + + Assert(aggref->aggorder == NIL); + Assert(aggref->aggdistinct == NIL); + + /* + * We cannot push down aggregates that contain volatile functions. + * Doing so would change the number of times the function is + * evaluated. + */ + if (contain_volatile_functions((Node *) aggref)) + { + eager_agg_applicable = false; + break; + } + + /* + * If there are any securityQuals, do not try to apply eager + * aggregation if any non-leakproof aggregate functions are present. + * This is overly strict, but for now... + */ + if (root->qual_security_level > 0 && + !get_func_leakproof(aggref->aggfnoid)) + { + eager_agg_applicable = false; + break; + } + + agg_eval_at = pull_varnos(root, (Node *) aggref); + + /* + * If all base relations in the query are referenced by aggregate + * functions, then eager aggregation is not applicable. + */ + aggregate_relids = bms_add_members(aggregate_relids, agg_eval_at); + if (bms_is_subset(root->all_baserels, aggregate_relids)) + { + eager_agg_applicable = false; + break; + } + + /* OK, create the AggClauseInfo node */ + ac_info = makeNode(AggClauseInfo); + ac_info->aggref = aggref; + ac_info->agg_eval_at = agg_eval_at; + + /* ... and add it to the list */ + agg_clause_list = list_append_unique(agg_clause_list, ac_info); + } + + list_free(tlist_exprs); + + if (eager_agg_applicable) + { + root->agg_clause_list = agg_clause_list; + root->tlist_vars = tlist_vars; + } + else + { + list_free_deep(agg_clause_list); + list_free(tlist_vars); + } +} + +/* + * create_grouping_expr_infos + * Create a GroupingExprInfo for each expression usable as grouping key. + * + * If any grouping expression is not suitable, we will just return with + * root->group_expr_list being NIL. + */ +static void +create_grouping_expr_infos(PlannerInfo *root) +{ + List *exprs = NIL; + List *sortgrouprefs = NIL; + List *ecs = NIL; + ListCell *lc, + *lc1, + *lc2, + *lc3; + + Assert(root->group_expr_list == NIL); + + foreach(lc, root->processed_groupClause) + { + SortGroupClause *sgc = lfirst_node(SortGroupClause, lc); + TargetEntry *tle = get_sortgroupclause_tle(sgc, root->processed_tlist); + TypeCacheEntry *tce; + Oid equalimageproc; + + Assert(tle->ressortgroupref > 0); + + /* + * For now we only support plain Vars as grouping expressions. + */ + if (!IsA(tle->expr, Var)) + return; + + /* + * Eager aggregation is only possible if equality implies image + * equality for each grouping key. Otherwise, placing keys with + * different byte images into the same group may result in the loss of + * information that could be necessary to evaluate upper qual clauses. + * + * For instance, the NUMERIC data type is not supported, as values + * that are considered equal by the equality operator (e.g., 0 and + * 0.0) can have different scales. + */ + tce = lookup_type_cache(exprType((Node *) tle->expr), + TYPECACHE_BTREE_OPFAMILY); + if (!OidIsValid(tce->btree_opf) || + !OidIsValid(tce->btree_opintype)) + return; + + equalimageproc = get_opfamily_proc(tce->btree_opf, + tce->btree_opintype, + tce->btree_opintype, + BTEQUALIMAGE_PROC); + + /* + * If there is no BTEQUALIMAGE_PROC, eager aggregation is assumed to + * be unsafe. Otherwise, we call the procedure to check. We must be + * careful to pass the expression's actual collation, rather than the + * data type's default collation, to ensure that non-deterministic + * collations are correctly handled. + */ + if (!OidIsValid(equalimageproc) || + !DatumGetBool(OidFunctionCall1Coll(equalimageproc, + exprCollation((Node *) tle->expr), + ObjectIdGetDatum(tce->btree_opintype)))) + return; + + exprs = lappend(exprs, tle->expr); + sortgrouprefs = lappend_int(sortgrouprefs, tle->ressortgroupref); + ecs = lappend(ecs, get_eclass_for_sortgroupclause(root, sgc, tle->expr)); + } + + /* + * Construct a GroupingExprInfo for each expression. + */ + forthree(lc1, exprs, lc2, sortgrouprefs, lc3, ecs) + { + Expr *expr = (Expr *) lfirst(lc1); + int sortgroupref = lfirst_int(lc2); + EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc3); + GroupingExprInfo *ge_info; + + ge_info = makeNode(GroupingExprInfo); + ge_info->expr = (Expr *) copyObject(expr); + ge_info->sortgroupref = sortgroupref; + ge_info->ec = ec; + + root->group_expr_list = lappend(root->group_expr_list, ge_info); + } +} + +/* + * get_eclass_for_sortgroupclause + * Given a group clause and an expression, find an existing equivalence + * class that the expression is a member of; return NULL if none. + */ +static EquivalenceClass * +get_eclass_for_sortgroupclause(PlannerInfo *root, SortGroupClause *sgc, + Expr *expr) +{ + Oid opfamily, + opcintype, + collation; + CompareType cmptype; + Oid equality_op; + List *opfamilies; + + /* Punt if the group clause is not sortable */ + if (!OidIsValid(sgc->sortop)) + return NULL; + + /* Find the operator in pg_amop --- failure shouldn't happen */ + if (!get_ordering_op_properties(sgc->sortop, + &opfamily, &opcintype, &cmptype)) + elog(ERROR, "operator %u is not a valid ordering operator", + sgc->sortop); + + /* Because SortGroupClause doesn't carry collation, consult the expr */ + collation = exprCollation((Node *) expr); + + /* + * EquivalenceClasses need to contain opfamily lists based on the family + * membership of mergejoinable equality operators, which could belong to + * more than one opfamily. So we have to look up the opfamily's equality + * operator and get its membership. + */ + equality_op = get_opfamily_member_for_cmptype(opfamily, + opcintype, + opcintype, + COMPARE_EQ); + if (!OidIsValid(equality_op)) /* shouldn't happen */ + elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", + COMPARE_EQ, opcintype, opcintype, opfamily); + opfamilies = get_mergejoin_opfamilies(equality_op); + if (!opfamilies) /* certainly should find some */ + elog(ERROR, "could not find opfamilies for equality operator %u", + equality_op); + + /* Now find a matching EquivalenceClass */ + return get_eclass_for_sort_expr(root, expr, opfamilies, opcintype, + collation, sgc->tleSortGroupRef, + NULL, false); +} + /***************************************************************************** * * LATERAL REFERENCES @@ -3044,42 +3431,6 @@ add_base_clause_to_rel(PlannerInfo *root, Index relid, restrictinfo->security_level); } -/* - * expr_is_nonnullable - * Check to see if the Expr cannot be NULL - * - * If the Expr is a simple Var that is defined NOT NULL and meanwhile is not - * nulled by any outer joins, then we can know that it cannot be NULL. - */ -static bool -expr_is_nonnullable(PlannerInfo *root, Expr *expr) -{ - RelOptInfo *rel; - Var *var; - - /* For now only check simple Vars */ - if (!IsA(expr, Var)) - return false; - - var = (Var *) expr; - - /* could the Var be nulled by any outer joins? */ - if (!bms_is_empty(var->varnullingrels)) - return false; - - /* system columns cannot be NULL */ - if (var->varattno < 0) - return true; - - /* is the column defined NOT NULL? */ - rel = find_base_rel(root, var->varno); - if (var->varattno > 0 && - bms_is_member(var->varattno, rel->notnullattnums)) - return true; - - return false; -} - /* * restriction_is_always_true * Check to see if the RestrictInfo is always true. @@ -3116,7 +3467,7 @@ restriction_is_always_true(PlannerInfo *root, if (nulltest->argisrow) return false; - return expr_is_nonnullable(root, nulltest->arg); + return expr_is_nonnullable(root, nulltest->arg, NOTNULL_SOURCE_RELOPT); } /* If it's an OR, check its sub-clauses */ @@ -3181,7 +3532,7 @@ restriction_is_always_false(PlannerInfo *root, if (nulltest->argisrow) return false; - return expr_is_nonnullable(root, nulltest->arg); + return expr_is_nonnullable(root, nulltest->arg, NOTNULL_SOURCE_RELOPT); } /* If it's an OR, check its sub-clauses */ diff --git a/src/backend/optimizer/plan/meson.build b/src/backend/optimizer/plan/meson.build index edf64fe7c51a9..c565b2adbccd6 100644 --- a/src/backend/optimizer/plan/meson.build +++ b/src/backend/optimizer/plan/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'analyzejoins.c', diff --git a/src/backend/optimizer/plan/planagg.c b/src/backend/optimizer/plan/planagg.c index 64605be31781f..75f6475cb562e 100644 --- a/src/backend/optimizer/plan/planagg.c +++ b/src/backend/optimizer/plan/planagg.c @@ -17,7 +17,7 @@ * scan all the rows anyway. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -38,6 +38,7 @@ #include "optimizer/pathnode.h" #include "optimizer/paths.h" #include "optimizer/planmain.h" +#include "optimizer/planner.h" #include "optimizer/subselect.h" #include "optimizer/tlist.h" #include "parser/parse_clause.h" @@ -335,10 +336,13 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo, * than before. (This means that when we are done, there will be no Vars * of level 1, which is why the subquery can become an initplan.) */ - subroot = (PlannerInfo *) palloc(sizeof(PlannerInfo)); + subroot = palloc_object(PlannerInfo); memcpy(subroot, root, sizeof(PlannerInfo)); subroot->query_level++; subroot->parent_root = root; + subroot->plan_name = choose_plan_name(root->glob, "minmax", true); + subroot->alternative_plan_name = root->plan_name; + /* reset subplan-related stuff */ subroot->plan_params = NIL; subroot->outer_params = NULL; @@ -410,7 +414,7 @@ build_minmax_path(PlannerInfo *root, MinMaxAggInfo *mminfo, parse->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64), Int64GetDatum(1), false, - FLOAT8PASSBYVAL); + true); /* * Generate the best paths for this query, telling query_planner that we diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c index 5467e094ca7e0..02495e22e24f7 100644 --- a/src/backend/optimizer/plan/planmain.c +++ b/src/backend/optimizer/plan/planmain.c @@ -9,7 +9,7 @@ * shorn of features like subselects, inheritance, aggregates, grouping, * and so on. (Those are the things planner.c deals with.) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -76,6 +76,9 @@ query_planner(PlannerInfo *root, root->placeholder_list = NIL; root->placeholder_array = NULL; root->placeholder_array_size = 0; + root->agg_clause_list = NIL; + root->group_expr_list = NIL; + root->tlist_vars = NIL; root->fkey_list = NIL; root->initial_rels = NIL; @@ -265,6 +268,12 @@ query_planner(PlannerInfo *root, */ extract_restriction_or_clauses(root); + /* + * Check if eager aggregation is applicable, and if so, set up + * root->agg_clause_list and root->group_expr_list. + */ + setup_eager_aggregation(root); + /* * Now expand appendrels by adding "otherrels" for their children. We * delay this to the end so that we have as much information as possible diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index ff65867eebee7..4ec76ce31a907 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -3,7 +3,7 @@ * planner.c * The query optimizer external interface. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -58,6 +58,7 @@ #include "parser/parsetree.h" #include "partitioning/partdesc.h" #include "rewrite/rewriteManip.h" +#include "utils/acl.h" #include "utils/backend_status.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -72,6 +73,12 @@ bool enable_distinct_reordering = true; /* Hook for plugins to get control in planner() */ planner_hook_type planner_hook = NULL; +/* Hook for plugins to get control after PlannerGlobal is initialized */ +planner_setup_hook_type planner_setup_hook = NULL; + +/* Hook for plugins to get control before PlannerGlobal is discarded */ +planner_shutdown_hook_type planner_shutdown_hook = NULL; + /* Hook for plugins to get control when grouping_planner() plans upper rels */ create_upper_paths_hook_type create_upper_paths_hook = NULL; @@ -231,7 +238,6 @@ static void add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, RelOptInfo *partially_grouped_rel, const AggClauseCosts *agg_costs, grouping_sets_data *gd, - double dNumGroups, GroupPathExtraData *extra); static RelOptInfo *create_partial_grouping_paths(PlannerInfo *root, RelOptInfo *grouped_rel, @@ -267,12 +273,35 @@ static bool group_by_has_partkey(RelOptInfo *input_rel, static int common_prefix_cmp(const void *a, const void *b); static List *generate_setop_child_grouplist(SetOperationStmt *op, List *targetlist); +static void create_final_unique_paths(PlannerInfo *root, RelOptInfo *input_rel, + List *sortPathkeys, List *groupClause, + SpecialJoinInfo *sjinfo, RelOptInfo *unique_rel); +static void create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel, + List *sortPathkeys, List *groupClause, + SpecialJoinInfo *sjinfo, RelOptInfo *unique_rel); /***************************************************************************** * * Query optimizer entry point * + * Inputs: + * parse: an analyzed-and-rewritten query tree for an optimizable statement + * query_string: source text for the query tree (used for error reports) + * cursorOptions: bitmask of CURSOR_OPT_XXX flags, see parsenodes.h + * boundParams: passed-in parameter values, or NULL if none + * es: ExplainState if being called from EXPLAIN, else NULL + * + * The result is a PlannedStmt tree. + * + * PARAM_EXTERN Param nodes within the parse tree can be replaced by Consts + * using values from boundParams, if those values are marked PARAM_FLAG_CONST. + * Parameter values not so marked are still relied on for estimation purposes. + * + * The ExplainState pointer is not currently used by the core planner, but it + * is passed through to some planner hooks so that they can report information + * back to EXPLAIN extension hooks. + * * To support loadable plugins that monitor or modify planner behavior, * we provide a hook variable that lets a plugin get control before and * after the standard planning process. The plugin would normally call @@ -284,14 +313,16 @@ static List *generate_setop_child_grouplist(SetOperationStmt *op, *****************************************************************************/ PlannedStmt * planner(Query *parse, const char *query_string, int cursorOptions, - ParamListInfo boundParams) + ParamListInfo boundParams, ExplainState *es) { PlannedStmt *result; if (planner_hook) - result = (*planner_hook) (parse, query_string, cursorOptions, boundParams); + result = (*planner_hook) (parse, query_string, cursorOptions, + boundParams, es); else - result = standard_planner(parse, query_string, cursorOptions, boundParams); + result = standard_planner(parse, query_string, cursorOptions, + boundParams, es); pgstat_report_plan_id(result->planId, false); @@ -300,7 +331,7 @@ planner(Query *parse, const char *query_string, int cursorOptions, PlannedStmt * standard_planner(Query *parse, const char *query_string, int cursorOptions, - ParamListInfo boundParams) + ParamListInfo boundParams, ExplainState *es) { PlannedStmt *result; PlannerGlobal *glob; @@ -310,7 +341,8 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, Path *best_path; Plan *top_plan; ListCell *lp, - *lr; + *lr, + *lc; /* * Set up global state for this planner invocation. This data is needed @@ -342,6 +374,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, glob->transientPlan = false; glob->dependsOnRole = false; glob->partition_directory = NULL; + glob->rel_notnullatts_hash = NULL; /* * Assess whether it's feasible to use parallel mode for this query. We @@ -430,8 +463,61 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, tuple_fraction = 0.0; } + /* + * Compute the initial path generation strategy mask. + * + * Some strategies, such as PGS_FOREIGNJOIN, have no corresponding enable_* + * GUC, and so the corresponding bits are always set in the default + * strategy mask. + * + * It may seem surprising that enable_indexscan sets both PGS_INDEXSCAN + * and PGS_INDEXONLYSCAN. However, the historical behavior of this GUC + * corresponds to this exactly: enable_indexscan=off disables both + * index-scan and index-only scan paths, whereas enable_indexonlyscan=off + * converts the index-only scan paths that we would have considered into + * index scan paths. + */ + glob->default_pgs_mask = PGS_APPEND | PGS_MERGE_APPEND | PGS_FOREIGNJOIN | + PGS_GATHER | PGS_CONSIDER_NONPARTIAL; + if (enable_tidscan) + glob->default_pgs_mask |= PGS_TIDSCAN; + if (enable_seqscan) + glob->default_pgs_mask |= PGS_SEQSCAN; + if (enable_indexscan) + glob->default_pgs_mask |= PGS_INDEXSCAN | PGS_INDEXONLYSCAN; + if (enable_indexonlyscan) + glob->default_pgs_mask |= PGS_CONSIDER_INDEXONLY; + if (enable_bitmapscan) + glob->default_pgs_mask |= PGS_BITMAPSCAN; + if (enable_mergejoin) + { + glob->default_pgs_mask |= PGS_MERGEJOIN_PLAIN; + if (enable_material) + glob->default_pgs_mask |= PGS_MERGEJOIN_MATERIALIZE; + } + if (enable_nestloop) + { + glob->default_pgs_mask |= PGS_NESTLOOP_PLAIN; + if (enable_material) + glob->default_pgs_mask |= PGS_NESTLOOP_MATERIALIZE; + if (enable_memoize) + glob->default_pgs_mask |= PGS_NESTLOOP_MEMOIZE; + } + if (enable_hashjoin) + glob->default_pgs_mask |= PGS_HASHJOIN; + if (enable_gathermerge) + glob->default_pgs_mask |= PGS_GATHER_MERGE; + if (enable_partitionwise_join) + glob->default_pgs_mask |= PGS_CONSIDER_PARTITIONWISE; + + /* Allow plugins to take control after we've initialized "glob" */ + if (planner_setup_hook) + (*planner_setup_hook) (glob, parse, query_string, cursorOptions, + &tuple_fraction, es); + /* primary planning entry point (may recurse for subqueries) */ - root = subquery_planner(glob, parse, NULL, false, tuple_fraction, NULL); + root = subquery_planner(glob, parse, NULL, NULL, NULL, false, + tuple_fraction, NULL); /* Select best Path and turn it into a Plan */ final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL); @@ -557,6 +643,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->commandType = parse->commandType; result->queryId = parse->queryId; + result->planOrigin = PLAN_STMT_STANDARD; result->hasReturning = (parse->returningList != NIL); result->hasModifyingCTE = parse->hasModifyingCTE; result->canSetTag = parse->canSetTag; @@ -569,16 +656,29 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->unprunableRelids = bms_difference(glob->allRelids, glob->prunableRelids); result->permInfos = glob->finalrteperminfos; - result->resultRelations = glob->resultRelations; + result->subrtinfos = glob->subrtinfos; result->appendRelations = glob->appendRelations; result->subplans = glob->subplans; result->rewindPlanIDs = glob->rewindPlanIDs; result->rowMarks = glob->finalrowmarks; + + /* + * Compute resultRelationRelids and rowMarkRelids from resultRelations and + * rowMarks. These can be used for cheap membership checks. + */ + foreach(lc, glob->resultRelations) + result->resultRelationRelids = bms_add_member(result->resultRelationRelids, + lfirst_int(lc)); + foreach(lc, glob->finalrowmarks) + result->rowMarkRelids = bms_add_member(result->rowMarkRelids, + ((PlanRowMark *) lfirst(lc))->rti); + result->relationOids = glob->relationOids; result->invalItems = glob->invalItems; result->paramExecTypes = glob->paramExecTypes; /* utilityStmt should be null, but we might as well copy it */ result->utilityStmt = parse->utilityStmt; + result->elidedNodes = glob->elidedNodes; result->stmt_location = parse->stmt_location; result->stmt_len = parse->stmt_len; @@ -607,6 +707,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->jitFlags |= PGJIT_DEFORM; } + /* Allow plugins to take control before we discard "glob" */ + if (planner_shutdown_hook) + (*planner_shutdown_hook) (glob, parse, query_string, result); + if (glob->partition_directory != NULL) DestroyPartitionDirectory(glob->partition_directory); @@ -621,7 +725,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, * * glob is the global state for the current planner run. * parse is the querytree produced by the parser & rewriter. + * plan_name is the name to assign to this subplan (NULL at the top level). * parent_root is the immediate parent Query's info (NULL at the top level). + * alternative_root is a previously created PlannerInfo for which this query + * level is an alternative implementation, or else NULL. * hasRecursion is true if this is a recursive WITH query. * tuple_fraction is the fraction of tuples we expect will be retrieved. * tuple_fraction is interpreted as explained for grouping_planner, below. @@ -647,7 +754,8 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, *-------------------- */ PlannerInfo * -subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, +subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name, + PlannerInfo *parent_root, PlannerInfo *alternative_root, bool hasRecursion, double tuple_fraction, SetOperationStmt *setops) { @@ -664,6 +772,11 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, root->parse = parse; root->glob = glob; root->query_level = parent_root ? parent_root->query_level + 1 : 1; + root->plan_name = plan_name; + if (alternative_root != NULL) + root->alternative_plan_name = alternative_root->plan_name; + else + root->alternative_plan_name = plan_name; root->parent_root = parent_root; root->plan_params = NIL; root->outer_params = NULL; @@ -694,12 +807,12 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, root->hasAlternativeSubPlans = false; root->placeholdersFrozen = false; root->hasRecursion = hasRecursion; + root->assumeReplanning = false; if (hasRecursion) root->wt_param_id = assign_special_exec_param(root); else root->wt_param_id = -1; root->non_recursive_path = NULL; - root->partColsUpdated = false; /* * Create the top-level join domain. This won't have valid contents until @@ -720,6 +833,18 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ transform_MERGE_to_join(parse); + /* + * Scan the rangetable for relation RTEs and retrieve the necessary + * catalog information for each relation. Using this information, clear + * the inh flag for any relation that has no children, collect not-null + * attribute numbers for any relation that has column not-null + * constraints, and expand virtual generated columns for any relation that + * contains them. Note that this step does not descend into sublinks and + * subqueries; if we pull up any sublinks or subqueries below, their + * relation RTEs are processed just before pulling them up. + */ + parse = root->parse = preprocess_relation_rtes(root); + /* * If the FROM clause is empty, replace it with a dummy RTE_RESULT RTE, so * that we don't need so many special cases to deal with that situation. @@ -743,14 +868,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, */ preprocess_function_rtes(root); - /* - * Scan the rangetable for relations with virtual generated columns, and - * replace all Var nodes in the query that reference these columns with - * the generation expressions. Recursion issues here are handled in the - * same way as for SubLinks. - */ - parse = root->parse = expand_virtual_generated_columns(root); - /* * Check to see if any subqueries in the jointree can be merged into this * query. @@ -787,23 +904,6 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, switch (rte->rtekind) { - case RTE_RELATION: - if (rte->inh) - { - /* - * Check to see if the relation actually has any children; - * if not, clear the inh flag so we can treat it as a - * plain base relation. - * - * Note: this could give a false-positive result, if the - * rel once had children but no longer does. We used to - * be able to clear rte->inh later on when we discovered - * that, but no more; we have to handle such cases as - * full-fledged inheritance. - */ - rte->inh = has_subclass(rte->relid); - } - break; case RTE_JOIN: root->hasJoinRTEs = true; if (IS_OUTER_JOIN(rte->jointype)) @@ -848,6 +948,38 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, bms_make_singleton(parse->resultRelation); } + /* + * This would be a convenient time to check access permissions for all + * relations mentioned in the query, since it would be better to fail now, + * before doing any detailed planning. However, for historical reasons, + * we leave this to be done at executor startup. + * + * Note, however, that we do need to check access permissions for any view + * relations mentioned in the query, in order to prevent information being + * leaked by selectivity estimation functions, which only check view owner + * permissions on underlying tables (see all_rows_selectable() and its + * callers). This is a little ugly, because it means that access + * permissions for views will be checked twice, which is another reason + * why it would be better to do all the ACL checks here. + */ + foreach(l, parse->rtable) + { + RangeTblEntry *rte = lfirst_node(RangeTblEntry, l); + + if (rte->perminfoindex != 0 && + rte->relkind == RELKIND_VIEW) + { + RTEPermissionInfo *perminfo; + bool result; + + perminfo = getRTEPermissionInfo(parse->rteperminfos, rte); + result = ExecCheckOneRelPerms(perminfo); + if (!result) + aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_VIEW, + get_rel_name(perminfo->relid)); + } + } + /* * Preprocess RowMark information. We need to do this after subquery * pullup, so that all base relations are present. @@ -1064,15 +1196,28 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, if (parse->hasTargetSRFs) parse->hasTargetSRFs = expression_returns_set((Node *) parse->targetList); + /* + * If we have grouping sets, expand the groupingSets tree of this query to + * a flat list of grouping sets. We need to do this before optimizing + * HAVING, since we can't easily tell if there's an empty grouping set + * until we have this representation. + */ + if (parse->groupingSets) + { + parse->groupingSets = + expand_grouping_sets(parse->groupingSets, parse->groupDistinct, -1); + } + /* * In some cases we may want to transfer a HAVING clause into WHERE. We * cannot do so if the HAVING clause contains aggregates (obviously) or * volatile functions (since a HAVING clause is supposed to be executed - * only once per group). We also can't do this if there are any nonempty - * grouping sets and the clause references any columns that are nullable - * by the grouping sets; moving such a clause into WHERE would potentially - * change the results. (If there are only empty grouping sets, then the - * HAVING clause must be degenerate as discussed below.) + * only once per group). We also can't do this if there are any grouping + * sets and the clause references any columns that are nullable by the + * grouping sets; the nulled values of those columns are not available + * before the grouping step. (The test on groupClause might seem wrong, + * but it's okay: it's just an optimization to avoid running pull_varnos + * when there cannot be any Vars in the HAVING clause.) * * Also, it may be that the clause is so expensive to execute that we're * better off doing it only once per group, despite the loss of @@ -1082,19 +1227,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, * clause into WHERE, in hopes of eliminating tuples before aggregation * instead of after. * - * If the query has explicit grouping then we can simply move such a + * If the query has no empty grouping set then we can simply move such a * clause into WHERE; any group that fails the clause will not be in the * output because none of its tuples will reach the grouping or - * aggregation stage. Otherwise we must have a degenerate (variable-free) - * HAVING clause, which we put in WHERE so that query_planner() can use it - * in a gating Result node, but also keep in HAVING to ensure that we - * don't emit a bogus aggregated row. (This could be done better, but it - * seems not worth optimizing.) + * aggregation stage. Otherwise we have to keep the clause in HAVING to + * ensure that we don't emit a bogus aggregated row. But then the HAVING + * clause must be degenerate (variable-free), so we can copy it into WHERE + * so that query_planner() can use it in a gating Result node. (This could + * be done better, but it seems not worth optimizing.) * * Note that a HAVING clause may contain expressions that are not fully * preprocessed. This can happen if these expressions are part of * grouping items. In such cases, they are replaced with GROUP Vars in - * the parser and then replaced back after we've done with expression + * the parser and then replaced back after we're done with expression * preprocessing on havingQual. This is not an issue if the clause * remains in HAVING, because these expressions will be matched to lower * target items in setrefs.c. However, if the clause is moved or copied @@ -1119,8 +1264,11 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, /* keep it in HAVING */ newHaving = lappend(newHaving, havingclause); } - else if (parse->groupClause) + else if (parse->groupClause && + (parse->groupingSets == NIL || + (List *) linitial(parse->groupingSets) != NIL)) { + /* There is GROUP BY, but no empty grouping set */ Node *whereclause; /* Preprocess the HAVING clause fully */ @@ -1133,6 +1281,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root, } else { + /* There is an empty grouping set (perhaps implicitly) */ Node *whereclause; /* Preprocess the HAVING clause fully */ @@ -1694,9 +1843,10 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, sort_input_target = linitial_node(PathTarget, sort_input_targets); Assert(!linitial_int(sort_input_targets_contain_srfs)); /* likewise for grouping_target vs. scanjoin_target */ - split_pathtarget_at_srfs(root, grouping_target, scanjoin_target, - &grouping_targets, - &grouping_targets_contain_srfs); + split_pathtarget_at_srfs_grouping(root, + grouping_target, scanjoin_target, + &grouping_targets, + &grouping_targets_contain_srfs); grouping_target = linitial_node(PathTarget, grouping_targets); Assert(!linitial_int(grouping_targets_contain_srfs)); /* scanjoin_target will not have any SRFs precomputed for it */ @@ -2063,7 +2213,6 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, parse->canSetTag, parse->resultRelation, rootRelation, - root->partColsUpdated, resultRelations, updateColnosLists, withCheckOptionLists, @@ -2072,6 +2221,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, parse->onConflict, mergeActionLists, mergeJoinConditions, + parse->forPortionOf, assign_special_exec_param(root)); } @@ -2119,10 +2269,13 @@ grouping_planner(PlannerInfo *root, double tuple_fraction, } /* - * Do preprocessing for groupingSets clause and related data. This handles the - * preliminary steps of expanding the grouping sets, organizing them into lists - * of rollups, and preparing annotations which will later be filled in with - * size estimates. + * Do preprocessing for groupingSets clause and related data. + * + * We expect that parse->groupingSets has already been expanded into a flat + * list of grouping sets (that is, just integer Lists of ressortgroupref + * numbers) by expand_grouping_sets(). This function handles the preliminary + * steps of organizing the grouping sets into lists of rollups, and preparing + * annotations which will later be filled in with size estimates. */ static grouping_sets_data * preprocess_grouping_sets(PlannerInfo *root) @@ -2131,14 +2284,7 @@ preprocess_grouping_sets(PlannerInfo *root) List *sets; int maxref = 0; ListCell *lc_set; - grouping_sets_data *gd = palloc0(sizeof(grouping_sets_data)); - - parse->groupingSets = expand_grouping_sets(parse->groupingSets, parse->groupDistinct, -1); - - gd->any_hashable = false; - gd->unhashable_refs = NULL; - gd->unsortable_refs = NULL; - gd->unsortable_sets = NIL; + grouping_sets_data *gd = palloc0_object(grouping_sets_data); /* * We don't currently make any attempt to optimize the groupClause when @@ -2146,6 +2292,12 @@ preprocess_grouping_sets(PlannerInfo *root) */ root->processed_groupClause = parse->groupClause; + /* Detect unhashable and unsortable grouping expressions */ + gd->any_hashable = false; + gd->unhashable_refs = NULL; + gd->unsortable_refs = NULL; + gd->unsortable_sets = NIL; + if (parse->groupClause) { ListCell *lc; @@ -3332,11 +3484,11 @@ adjust_group_pathkeys_for_groupagg(PlannerInfo *root) case PATHKEYS_BETTER2: /* 'pathkeys' are stronger, use these ones instead */ currpathkeys = pathkeys; - /* FALLTHROUGH */ + pg_fallthrough; case PATHKEYS_BETTER1: /* 'pathkeys' are less strict */ - /* FALLTHROUGH */ + pg_fallthrough; case PATHKEYS_EQUAL: /* mark this aggregate as covered by 'currpathkeys' */ @@ -3868,9 +4020,12 @@ make_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, * target list and HAVING quals are parallel-safe. */ if (input_rel->consider_parallel && target_parallel_safe && - is_parallel_safe(root, (Node *) havingQual)) + is_parallel_safe(root, havingQual)) grouped_rel->consider_parallel = true; + /* Assume that the same path generation strategies are allowed */ + grouped_rel->pgs_mask = input_rel->pgs_mask; + /* * If the input rel belongs to a single FDW, so does the grouped rel. */ @@ -3928,7 +4083,7 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, * might get between 0 and N output rows. Offhand I think that's * desired.) */ - List *paths = NIL; + AppendPathInput append = {0}; while (--nrows >= 0) { @@ -3936,13 +4091,12 @@ create_degenerate_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, create_group_result_path(root, grouped_rel, grouped_rel->reltarget, (List *) parse->havingQual); - paths = lappend(paths, path); + append.subpaths = lappend(append.subpaths, path); } path = (Path *) create_append_path(root, grouped_rel, - paths, - NIL, + append, NIL, NULL, 0, @@ -3982,9 +4136,7 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, GroupPathExtraData *extra, RelOptInfo **partially_grouped_rel_p) { - Path *cheapest_path = input_rel->cheapest_total_path; RelOptInfo *partially_grouped_rel = NULL; - double dNumGroups; PartitionwiseAggregateType patype = PARTITIONWISE_AGGREGATE_NONE; /* @@ -4066,23 +4218,16 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel, /* Gather any partially grouped partial paths. */ if (partially_grouped_rel && partially_grouped_rel->partial_pathlist) - { gather_grouping_paths(root, partially_grouped_rel); - set_cheapest(partially_grouped_rel); - } - /* - * Estimate number of groups. - */ - dNumGroups = get_number_of_groups(root, - cheapest_path->rows, - gd, - extra->targetList); + /* Now choose the best path(s) for partially_grouped_rel. */ + if (partially_grouped_rel && partially_grouped_rel->pathlist) + set_cheapest(partially_grouped_rel); /* Build final grouping paths */ add_paths_to_grouping_rel(root, input_rel, grouped_rel, partially_grouped_rel, agg_costs, gd, - dNumGroups, extra); + extra); /* Give a helpful error if we failed to find any implementation */ if (grouped_rel->pathlist == NIL) @@ -4893,7 +5038,7 @@ create_partial_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64), Int64GetDatum(1), false, - FLOAT8PASSBYVAL); + true); /* * Apply a LimitPath onto the partial path to restrict the @@ -4917,10 +5062,10 @@ create_partial_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, else { add_partial_path(partial_distinct_rel, (Path *) - create_upper_unique_path(root, partial_distinct_rel, - sorted_path, - list_length(root->distinct_pathkeys), - numDistinctRows)); + create_unique_path(root, partial_distinct_rel, + sorted_path, + list_length(root->distinct_pathkeys), + numDistinctRows)); } } } @@ -5096,7 +5241,7 @@ create_final_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64), Int64GetDatum(1), false, - FLOAT8PASSBYVAL); + true); /* * If the query already has a LIMIT clause, then we could @@ -5111,10 +5256,10 @@ create_final_distinct_paths(PlannerInfo *root, RelOptInfo *input_rel, else { add_path(distinct_rel, (Path *) - create_upper_unique_path(root, distinct_rel, - sorted_path, - list_length(root->distinct_pathkeys), - numDistinctRows)); + create_unique_path(root, distinct_rel, + sorted_path, + list_length(root->distinct_pathkeys), + numDistinctRows)); } } } @@ -5273,6 +5418,9 @@ create_ordered_paths(PlannerInfo *root, if (input_rel->consider_parallel && target_parallel_safe) ordered_rel->consider_parallel = true; + /* Assume that the same path generation strategies are allowed. */ + ordered_rel->pgs_mask = input_rel->pgs_mask; + /* * If the input rel belongs to a single FDW, so does the ordered_rel. */ @@ -5905,8 +6053,8 @@ select_active_windows(PlannerInfo *root, WindowFuncLists *wflists) List *result = NIL; ListCell *lc; int nActive = 0; - WindowClauseSortData *actives = palloc(sizeof(WindowClauseSortData) - * list_length(windowClause)); + WindowClauseSortData *actives = palloc_array(WindowClauseSortData, + list_length(windowClause)); /* First, construct an array of the active windows */ foreach(lc, windowClause) @@ -6879,7 +7027,7 @@ plan_cluster_use_sort(Oid tableOid, Oid indexOid) * * tableOid is the table on which the index is to be built. indexOid is the * OID of an index to be created or reindexed (which must be an index with - * support for parallel builds - currently btree or BRIN). + * support for parallel builds - currently btree, GIN, or BRIN). * * Return value is the number of parallel worker processes to request. It * may be unsafe to proceed if this is 0. Note that this does not include the @@ -7027,16 +7175,42 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, RelOptInfo *grouped_rel, RelOptInfo *partially_grouped_rel, const AggClauseCosts *agg_costs, - grouping_sets_data *gd, double dNumGroups, + grouping_sets_data *gd, GroupPathExtraData *extra) { Query *parse = root->parse; Path *cheapest_path = input_rel->cheapest_total_path; + Path *cheapest_partially_grouped_path = NULL; ListCell *lc; bool can_hash = (extra->flags & GROUPING_CAN_USE_HASH) != 0; bool can_sort = (extra->flags & GROUPING_CAN_USE_SORT) != 0; List *havingQual = (List *) extra->havingQual; AggClauseCosts *agg_final_costs = &extra->agg_final_costs; + double dNumGroups = 0; + double dNumFinalGroups = 0; + + /* + * Estimate number of groups for non-split aggregation. + */ + dNumGroups = get_number_of_groups(root, + cheapest_path->rows, + gd, + extra->targetList); + + if (partially_grouped_rel && partially_grouped_rel->pathlist) + { + cheapest_partially_grouped_path = + partially_grouped_rel->cheapest_total_path; + + /* + * Estimate number of groups for final phase of partial aggregation. + */ + dNumFinalGroups = + get_number_of_groups(root, + cheapest_partially_grouped_path->rows, + gd, + extra->targetList); + } if (can_sort) { @@ -7149,7 +7323,7 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, path = make_ordered_path(root, grouped_rel, path, - partially_grouped_rel->cheapest_total_path, + cheapest_partially_grouped_path, info->pathkeys, -1.0); @@ -7167,7 +7341,7 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, info->clauses, havingQual, agg_final_costs, - dNumGroups)); + dNumFinalGroups)); else add_path(grouped_rel, (Path *) create_group_path(root, @@ -7175,7 +7349,7 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, path, info->clauses, havingQual, - dNumGroups)); + dNumFinalGroups)); } } @@ -7217,19 +7391,17 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel, */ if (partially_grouped_rel && partially_grouped_rel->pathlist) { - Path *path = partially_grouped_rel->cheapest_total_path; - add_path(grouped_rel, (Path *) create_agg_path(root, grouped_rel, - path, + cheapest_partially_grouped_path, grouped_rel->reltarget, AGG_HASHED, AGGSPLIT_FINAL_DESERIAL, root->processed_groupClause, havingQual, agg_final_costs, - dNumGroups)); + dNumFinalGroups)); } } @@ -7269,6 +7441,7 @@ create_partial_grouping_paths(PlannerInfo *root, { Query *parse = root->parse; RelOptInfo *partially_grouped_rel; + RelOptInfo *eager_agg_rel = NULL; AggClauseCosts *agg_partial_costs = &extra->agg_partial_costs; AggClauseCosts *agg_final_costs = &extra->agg_final_costs; Path *cheapest_partial_path = NULL; @@ -7279,6 +7452,15 @@ create_partial_grouping_paths(PlannerInfo *root, bool can_hash = (extra->flags & GROUPING_CAN_USE_HASH) != 0; bool can_sort = (extra->flags & GROUPING_CAN_USE_SORT) != 0; + /* + * Check whether any partially aggregated paths have been generated + * through eager aggregation. + */ + if (input_rel->grouped_rel && + !IS_DUMMY_REL(input_rel->grouped_rel) && + input_rel->grouped_rel->pathlist != NIL) + eager_agg_rel = input_rel->grouped_rel; + /* * Consider whether we should generate partially aggregated non-partial * paths. We can only do this if we have a non-partial path, and only if @@ -7300,11 +7482,13 @@ create_partial_grouping_paths(PlannerInfo *root, /* * If we can't partially aggregate partial paths, and we can't partially - * aggregate non-partial paths, then don't bother creating the new + * aggregate non-partial paths, and no partially aggregated paths were + * generated by eager aggregation, then don't bother creating the new * RelOptInfo at all, unless the caller specified force_rel_creation. */ if (cheapest_total_path == NULL && cheapest_partial_path == NULL && + eager_agg_rel == NULL && !force_rel_creation) return NULL; @@ -7317,6 +7501,7 @@ create_partial_grouping_paths(PlannerInfo *root, grouped_rel->relids); partially_grouped_rel->consider_parallel = grouped_rel->consider_parallel; + partially_grouped_rel->pgs_mask = grouped_rel->pgs_mask; partially_grouped_rel->reloptkind = grouped_rel->reloptkind; partially_grouped_rel->serverid = grouped_rel->serverid; partially_grouped_rel->userid = grouped_rel->userid; @@ -7529,6 +7714,51 @@ create_partial_grouping_paths(PlannerInfo *root, dNumPartialPartialGroups)); } + /* + * Add any partially aggregated paths generated by eager aggregation to + * the new upper relation after applying projection steps as needed. + */ + if (eager_agg_rel) + { + /* Add the paths */ + foreach(lc, eager_agg_rel->pathlist) + { + Path *path = (Path *) lfirst(lc); + + /* Shouldn't have any parameterized paths anymore */ + Assert(path->param_info == NULL); + + path = (Path *) create_projection_path(root, + partially_grouped_rel, + path, + partially_grouped_rel->reltarget); + + add_path(partially_grouped_rel, path); + } + + /* + * Likewise add the partial paths, but only if parallelism is possible + * for partially_grouped_rel. + */ + if (partially_grouped_rel->consider_parallel) + { + foreach(lc, eager_agg_rel->partial_pathlist) + { + Path *path = (Path *) lfirst(lc); + + /* Shouldn't have any parameterized paths anymore */ + Assert(path->param_info == NULL); + + path = (Path *) create_projection_path(root, + partially_grouped_rel, + path, + partially_grouped_rel->reltarget); + + add_partial_path(partially_grouped_rel, path); + } + } + } + /* * If there is an FDW that's responsible for all baserels of the query, * let it consider adding partially grouped ForeignPaths. @@ -7753,17 +7983,23 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, check_stack_depth(); /* - * If the rel is partitioned, we want to drop its existing paths and - * generate new ones. This function would still be correct if we kept the - * existing paths: we'd modify them to generate the correct target above - * the partitioning Append, and then they'd compete on cost with paths - * generating the target below the Append. However, in our current cost - * model the latter way is always the same or cheaper cost, so modifying - * the existing paths would just be useless work. Moreover, when the cost - * is the same, varying roundoff errors might sometimes allow an existing - * path to be picked, resulting in undesirable cross-platform plan - * variations. So we drop old paths and thereby force the work to be done - * below the Append, except in the case of a non-parallel-safe target. + * If the rel only has Append and MergeAppend paths, we want to drop its + * existing paths and generate new ones. This function would still be + * correct if we kept the existing paths: we'd modify them to generate the + * correct target above the partitioning Append, and then they'd compete + * on cost with paths generating the target below the Append. However, in + * our current cost model the latter way is always the same or cheaper + * cost, so modifying the existing paths would just be useless work. + * Moreover, when the cost is the same, varying roundoff errors might + * sometimes allow an existing path to be picked, resulting in undesirable + * cross-platform plan variations. So we drop old paths and thereby force + * the work to be done below the Append. + * + * However, there are several cases when this optimization is not safe. If + * the rel isn't partitioned, then none of the paths will be Append or + * MergeAppend paths, so we should definitely not do this. If it is + * partitioned but is a joinrel, it may have Append and MergeAppend paths, + * but it can also have join paths that we can't afford to discard. * * Some care is needed, because we have to allow * generate_useful_gather_paths to see the old partial paths in the next @@ -7771,7 +8007,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, * generate_useful_gather_paths to add path(s) to the main list, and * finally zap the partial pathlist. */ - if (rel_is_partitioned) + if (rel_is_partitioned && IS_SIMPLE_REL(rel)) rel->pathlist = NIL; /* @@ -7797,7 +8033,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, } /* Finish dropping old paths for a partitioned rel, per comment above */ - if (rel_is_partitioned) + if (rel_is_partitioned && IS_SIMPLE_REL(rel)) rel->partial_pathlist = NIL; /* Extract SRF-free scan/join target. */ @@ -8092,13 +8328,6 @@ create_partitionwise_grouping_paths(PlannerInfo *root, add_paths_to_append_rel(root, partially_grouped_rel, partially_grouped_live_children); - - /* - * We need call set_cheapest, since the finalization step will use the - * cheapest path from the rel. - */ - if (partially_grouped_rel->pathlist) - set_cheapest(partially_grouped_rel); } /* If possible, create append paths for fully grouped children. */ @@ -8248,3 +8477,627 @@ generate_setop_child_grouplist(SetOperationStmt *op, List *targetlist) return grouplist; } + +/* + * create_unique_paths + * Build a new RelOptInfo containing Paths that represent elimination of + * distinct rows from the input data. Distinct-ness is defined according to + * the needs of the semijoin represented by sjinfo. If it is not possible + * to identify how to make the data unique, NULL is returned. + * + * If used at all, this is likely to be called repeatedly on the same rel, + * so we cache the result. + */ +RelOptInfo * +create_unique_paths(PlannerInfo *root, RelOptInfo *rel, SpecialJoinInfo *sjinfo) +{ + RelOptInfo *unique_rel; + List *sortPathkeys = NIL; + List *groupClause = NIL; + MemoryContext oldcontext; + + /* Caller made a mistake if SpecialJoinInfo is the wrong one */ + Assert(sjinfo->jointype == JOIN_SEMI); + Assert(bms_equal(rel->relids, sjinfo->syn_righthand)); + + /* If result already cached, return it */ + if (rel->unique_rel) + return rel->unique_rel; + + /* If it's not possible to unique-ify, return NULL */ + if (!(sjinfo->semi_can_btree || sjinfo->semi_can_hash)) + return NULL; + + /* + * Punt if this is a child relation and we failed to build a unique-ified + * relation for its parent. This can happen if all the RHS columns were + * found to be equated to constants when unique-ifying the parent table, + * leaving no columns to unique-ify. + */ + if (IS_OTHER_REL(rel) && rel->top_parent->unique_rel == NULL) + return NULL; + + /* + * When called during GEQO join planning, we are in a short-lived memory + * context. We must make sure that the unique rel and any subsidiary data + * structures created for a baserel survive the GEQO cycle, else the + * baserel is trashed for future GEQO cycles. On the other hand, when we + * are creating those for a joinrel during GEQO, we don't want them to + * clutter the main planning context. Upshot is that the best solution is + * to explicitly allocate memory in the same context the given RelOptInfo + * is in. + */ + oldcontext = MemoryContextSwitchTo(GetMemoryChunkContext(rel)); + + unique_rel = makeNode(RelOptInfo); + memcpy(unique_rel, rel, sizeof(RelOptInfo)); + + /* + * clear path info + */ + unique_rel->pathlist = NIL; + unique_rel->ppilist = NIL; + unique_rel->partial_pathlist = NIL; + unique_rel->cheapest_startup_path = NULL; + unique_rel->cheapest_total_path = NULL; + unique_rel->cheapest_parameterized_paths = NIL; + + /* + * Build the target list for the unique rel. We also build the pathkeys + * that represent the ordering requirements for the sort-based + * implementation, and the list of SortGroupClause nodes that represent + * the columns to be grouped on for the hash-based implementation. + * + * For a child rel, we can construct these fields from those of its + * parent. + */ + if (IS_OTHER_REL(rel)) + { + PathTarget *child_unique_target; + PathTarget *parent_unique_target; + + parent_unique_target = rel->top_parent->unique_rel->reltarget; + + child_unique_target = copy_pathtarget(parent_unique_target); + + /* Translate the target expressions */ + child_unique_target->exprs = (List *) + adjust_appendrel_attrs_multilevel(root, + (Node *) parent_unique_target->exprs, + rel, + rel->top_parent); + + unique_rel->reltarget = child_unique_target; + + sortPathkeys = rel->top_parent->unique_pathkeys; + groupClause = rel->top_parent->unique_groupclause; + } + else + { + List *newtlist; + int nextresno; + List *sortList = NIL; + ListCell *lc1; + ListCell *lc2; + + /* + * The values we are supposed to unique-ify may be expressions in the + * variables of the input rel's targetlist. We have to add any such + * expressions to the unique rel's targetlist. + * + * To complicate matters, some of the values to be unique-ified may be + * known redundant by the EquivalenceClass machinery (e.g., because + * they have been equated to constants). There is no need to compare + * such values during unique-ification, and indeed we had better not + * try because the Vars involved may not have propagated as high as + * the semijoin's level. We use make_pathkeys_for_sortclauses to + * detect such cases, which is a tad inefficient but it doesn't seem + * worth building specialized infrastructure for this. + */ + newtlist = make_tlist_from_pathtarget(rel->reltarget); + nextresno = list_length(newtlist) + 1; + + forboth(lc1, sjinfo->semi_rhs_exprs, lc2, sjinfo->semi_operators) + { + Expr *uniqexpr = lfirst(lc1); + Oid in_oper = lfirst_oid(lc2); + Oid sortop; + TargetEntry *tle; + bool made_tle = false; + + tle = tlist_member(uniqexpr, newtlist); + if (!tle) + { + tle = makeTargetEntry(uniqexpr, + nextresno, + NULL, + false); + newtlist = lappend(newtlist, tle); + nextresno++; + made_tle = true; + } + + /* + * Try to build an ORDER BY list to sort the input compatibly. We + * do this for each sortable clause even when the clauses are not + * all sortable, so that we can detect clauses that are redundant + * according to the pathkey machinery. + */ + sortop = get_ordering_op_for_equality_op(in_oper, false); + if (OidIsValid(sortop)) + { + Oid eqop; + SortGroupClause *sortcl; + + /* + * The Unique node will need equality operators. Normally + * these are the same as the IN clause operators, but if those + * are cross-type operators then the equality operators are + * the ones for the IN clause operators' RHS datatype. + */ + eqop = get_equality_op_for_ordering_op(sortop, NULL); + if (!OidIsValid(eqop)) /* shouldn't happen */ + elog(ERROR, "could not find equality operator for ordering operator %u", + sortop); + + sortcl = makeNode(SortGroupClause); + sortcl->tleSortGroupRef = assignSortGroupRef(tle, newtlist); + sortcl->eqop = eqop; + sortcl->sortop = sortop; + sortcl->reverse_sort = false; + sortcl->nulls_first = false; + sortcl->hashable = false; /* no need to make this accurate */ + sortList = lappend(sortList, sortcl); + + /* + * At each step, convert the SortGroupClause list to pathkey + * form. If the just-added SortGroupClause is redundant, the + * result will be shorter than the SortGroupClause list. + */ + sortPathkeys = make_pathkeys_for_sortclauses(root, sortList, + newtlist); + if (list_length(sortPathkeys) != list_length(sortList)) + { + /* Drop the redundant SortGroupClause */ + sortList = list_delete_last(sortList); + Assert(list_length(sortPathkeys) == list_length(sortList)); + /* Undo tlist addition, if we made one */ + if (made_tle) + { + newtlist = list_delete_last(newtlist); + nextresno--; + } + /* We need not consider this clause for hashing, either */ + continue; + } + } + else if (sjinfo->semi_can_btree) /* shouldn't happen */ + elog(ERROR, "could not find ordering operator for equality operator %u", + in_oper); + + if (sjinfo->semi_can_hash) + { + /* Create a GROUP BY list for the Agg node to use */ + Oid eq_oper; + SortGroupClause *groupcl; + + /* + * Get the hashable equality operators for the Agg node to + * use. Normally these are the same as the IN clause + * operators, but if those are cross-type operators then the + * equality operators are the ones for the IN clause + * operators' RHS datatype. + */ + if (!get_compatible_hash_operators(in_oper, NULL, &eq_oper)) + elog(ERROR, "could not find compatible hash operator for operator %u", + in_oper); + + groupcl = makeNode(SortGroupClause); + groupcl->tleSortGroupRef = assignSortGroupRef(tle, newtlist); + groupcl->eqop = eq_oper; + groupcl->sortop = sortop; + groupcl->reverse_sort = false; + groupcl->nulls_first = false; + groupcl->hashable = true; + groupClause = lappend(groupClause, groupcl); + } + } + + /* + * Done building the sortPathkeys and groupClause. But the + * sortPathkeys are bogus if not all the clauses were sortable. + */ + if (!sjinfo->semi_can_btree) + sortPathkeys = NIL; + + /* + * It can happen that all the RHS columns are equated to constants. + * We'd have to do something special to unique-ify in that case, and + * it's such an unlikely-in-the-real-world case that it's not worth + * the effort. So just punt if we found no columns to unique-ify. + */ + if (sortPathkeys == NIL && groupClause == NIL) + { + MemoryContextSwitchTo(oldcontext); + return NULL; + } + + /* Convert the required targetlist back to PathTarget form */ + unique_rel->reltarget = create_pathtarget(root, newtlist); + } + + /* build unique paths based on input rel's pathlist */ + create_final_unique_paths(root, rel, sortPathkeys, groupClause, + sjinfo, unique_rel); + + /* build unique paths based on input rel's partial_pathlist */ + create_partial_unique_paths(root, rel, sortPathkeys, groupClause, + sjinfo, unique_rel); + + /* Now choose the best path(s) */ + set_cheapest(unique_rel); + + /* + * There shouldn't be any partial paths for the unique relation; + * otherwise, we won't be able to properly guarantee uniqueness. + */ + Assert(unique_rel->partial_pathlist == NIL); + + /* Cache the result */ + rel->unique_rel = unique_rel; + rel->unique_pathkeys = sortPathkeys; + rel->unique_groupclause = groupClause; + + MemoryContextSwitchTo(oldcontext); + + return unique_rel; +} + +/* + * create_final_unique_paths + * Create unique paths in 'unique_rel' based on 'input_rel' pathlist + */ +static void +create_final_unique_paths(PlannerInfo *root, RelOptInfo *input_rel, + List *sortPathkeys, List *groupClause, + SpecialJoinInfo *sjinfo, RelOptInfo *unique_rel) +{ + Path *cheapest_input_path = input_rel->cheapest_total_path; + + /* Estimate number of output rows */ + unique_rel->rows = estimate_num_groups(root, + sjinfo->semi_rhs_exprs, + cheapest_input_path->rows, + NULL, + NULL); + + /* Consider sort-based implementations, if possible. */ + if (sjinfo->semi_can_btree) + { + ListCell *lc; + + /* + * Use any available suitably-sorted path as input, and also consider + * sorting the cheapest-total path and incremental sort on any paths + * with presorted keys. + * + * To save planning time, we ignore parameterized input paths unless + * they are the cheapest-total path. + */ + foreach(lc, input_rel->pathlist) + { + Path *input_path = (Path *) lfirst(lc); + Path *path; + bool is_sorted; + int presorted_keys; + + /* + * Ignore parameterized paths that are not the cheapest-total + * path. + */ + if (input_path->param_info && + input_path != cheapest_input_path) + continue; + + is_sorted = pathkeys_count_contained_in(sortPathkeys, + input_path->pathkeys, + &presorted_keys); + + /* + * Ignore paths that are not suitably or partially sorted, unless + * they are the cheapest total path (no need to deal with paths + * which have presorted keys when incremental sort is disabled). + */ + if (!is_sorted && input_path != cheapest_input_path && + (presorted_keys == 0 || !enable_incremental_sort)) + continue; + + /* + * Make a separate ProjectionPath in case we need a Result node. + */ + path = (Path *) create_projection_path(root, + unique_rel, + input_path, + unique_rel->reltarget); + + if (!is_sorted) + { + /* + * We've no need to consider both a sort and incremental sort. + * We'll just do a sort if there are no presorted keys and an + * incremental sort when there are presorted keys. + */ + if (presorted_keys == 0 || !enable_incremental_sort) + path = (Path *) create_sort_path(root, + unique_rel, + path, + sortPathkeys, + -1.0); + else + path = (Path *) create_incremental_sort_path(root, + unique_rel, + path, + sortPathkeys, + presorted_keys, + -1.0); + } + + path = (Path *) create_unique_path(root, unique_rel, path, + list_length(sortPathkeys), + unique_rel->rows); + + add_path(unique_rel, path); + } + } + + /* Consider hash-based implementation, if possible. */ + if (sjinfo->semi_can_hash) + { + Path *path; + + /* + * Make a separate ProjectionPath in case we need a Result node. + */ + path = (Path *) create_projection_path(root, + unique_rel, + cheapest_input_path, + unique_rel->reltarget); + + path = (Path *) create_agg_path(root, + unique_rel, + path, + cheapest_input_path->pathtarget, + AGG_HASHED, + AGGSPLIT_SIMPLE, + groupClause, + NIL, + NULL, + unique_rel->rows); + + add_path(unique_rel, path); + } +} + +/* + * create_partial_unique_paths + * Create unique paths in 'unique_rel' based on 'input_rel' partial_pathlist + */ +static void +create_partial_unique_paths(PlannerInfo *root, RelOptInfo *input_rel, + List *sortPathkeys, List *groupClause, + SpecialJoinInfo *sjinfo, RelOptInfo *unique_rel) +{ + RelOptInfo *partial_unique_rel; + Path *cheapest_partial_path; + + /* nothing to do when there are no partial paths in the input rel */ + if (!input_rel->consider_parallel || input_rel->partial_pathlist == NIL) + return; + + /* + * nothing to do if there's anything in the targetlist that's + * parallel-restricted. + */ + if (!is_parallel_safe(root, (Node *) unique_rel->reltarget->exprs)) + return; + + cheapest_partial_path = linitial(input_rel->partial_pathlist); + + partial_unique_rel = makeNode(RelOptInfo); + memcpy(partial_unique_rel, input_rel, sizeof(RelOptInfo)); + + /* + * clear path info + */ + partial_unique_rel->pathlist = NIL; + partial_unique_rel->ppilist = NIL; + partial_unique_rel->partial_pathlist = NIL; + partial_unique_rel->cheapest_startup_path = NULL; + partial_unique_rel->cheapest_total_path = NULL; + partial_unique_rel->cheapest_parameterized_paths = NIL; + + /* Estimate number of output rows */ + partial_unique_rel->rows = estimate_num_groups(root, + sjinfo->semi_rhs_exprs, + cheapest_partial_path->rows, + NULL, + NULL); + partial_unique_rel->reltarget = unique_rel->reltarget; + + /* Consider sort-based implementations, if possible. */ + if (sjinfo->semi_can_btree) + { + ListCell *lc; + + /* + * Use any available suitably-sorted path as input, and also consider + * sorting the cheapest partial path and incremental sort on any paths + * with presorted keys. + */ + foreach(lc, input_rel->partial_pathlist) + { + Path *input_path = (Path *) lfirst(lc); + Path *path; + bool is_sorted; + int presorted_keys; + + is_sorted = pathkeys_count_contained_in(sortPathkeys, + input_path->pathkeys, + &presorted_keys); + + /* + * Ignore paths that are not suitably or partially sorted, unless + * they are the cheapest partial path (no need to deal with paths + * which have presorted keys when incremental sort is disabled). + */ + if (!is_sorted && input_path != cheapest_partial_path && + (presorted_keys == 0 || !enable_incremental_sort)) + continue; + + /* + * Make a separate ProjectionPath in case we need a Result node. + */ + path = (Path *) create_projection_path(root, + partial_unique_rel, + input_path, + partial_unique_rel->reltarget); + + if (!is_sorted) + { + /* + * We've no need to consider both a sort and incremental sort. + * We'll just do a sort if there are no presorted keys and an + * incremental sort when there are presorted keys. + */ + if (presorted_keys == 0 || !enable_incremental_sort) + path = (Path *) create_sort_path(root, + partial_unique_rel, + path, + sortPathkeys, + -1.0); + else + path = (Path *) create_incremental_sort_path(root, + partial_unique_rel, + path, + sortPathkeys, + presorted_keys, + -1.0); + } + + path = (Path *) create_unique_path(root, partial_unique_rel, path, + list_length(sortPathkeys), + partial_unique_rel->rows); + + add_partial_path(partial_unique_rel, path); + } + } + + /* Consider hash-based implementation, if possible. */ + if (sjinfo->semi_can_hash) + { + Path *path; + + /* + * Make a separate ProjectionPath in case we need a Result node. + */ + path = (Path *) create_projection_path(root, + partial_unique_rel, + cheapest_partial_path, + partial_unique_rel->reltarget); + + path = (Path *) create_agg_path(root, + partial_unique_rel, + path, + cheapest_partial_path->pathtarget, + AGG_HASHED, + AGGSPLIT_SIMPLE, + groupClause, + NIL, + NULL, + partial_unique_rel->rows); + + add_partial_path(partial_unique_rel, path); + } + + if (partial_unique_rel->partial_pathlist != NIL) + { + generate_useful_gather_paths(root, partial_unique_rel, true); + set_cheapest(partial_unique_rel); + + /* + * Finally, create paths to unique-ify the final result. This step is + * needed to remove any duplicates due to combining rows from parallel + * workers. + */ + create_final_unique_paths(root, partial_unique_rel, + sortPathkeys, groupClause, + sjinfo, unique_rel); + } +} + +/* + * Choose a unique name for some subroot. + * + * Modifies glob->subplanNames to track names already used. + */ +char * +choose_plan_name(PlannerGlobal *glob, const char *name, bool always_number) +{ + unsigned n; + + /* + * If a numeric suffix is not required, then search the list of + * previously-assigned names for a match. If none is found, then we can + * use the provided name without modification. + */ + if (!always_number) + { + bool found = false; + + foreach_ptr(char, subplan_name, glob->subplanNames) + { + if (strcmp(subplan_name, name) == 0) + { + found = true; + break; + } + } + + if (!found) + { + /* pstrdup here is just to avoid cast-away-const */ + char *chosen_name = pstrdup(name); + + glob->subplanNames = lappend(glob->subplanNames, chosen_name); + return chosen_name; + } + } + + /* + * If a numeric suffix is required or if the un-suffixed name is already + * in use, then loop until we find a positive integer that produces a + * novel name. + */ + for (n = 1; true; ++n) + { + char *proposed_name = psprintf("%s_%u", name, n); + bool found = false; + + foreach_ptr(char, subplan_name, glob->subplanNames) + { + if (strcmp(subplan_name, proposed_name) == 0) + { + found = true; + break; + } + } + + if (!found) + { + glob->subplanNames = lappend(glob->subplanNames, proposed_name); + return proposed_name; + } + + pfree(proposed_name); + } +} diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 846e44186c366..ff0e875f2a227 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -4,7 +4,7 @@ * Post-processing of a completed plan tree: fix references to subplan * vars, compute regproc values for operators, etc * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); +static void record_elided_node(PlannerGlobal *glob, int plan_node_id, + NodeTag elided_type, Bitmapset *relids); + /***************************************************************************** * @@ -307,8 +310,12 @@ set_plan_references(PlannerInfo *root, Plan *plan) PlanRowMark *rc = lfirst_node(PlanRowMark, lc); PlanRowMark *newrc; + /* sanity check on existing row marks */ + Assert(root->simple_rel_array[rc->rti] != NULL && + root->simple_rte_array[rc->rti] != NULL); + /* flat copy is enough since all fields are scalars */ - newrc = (PlanRowMark *) palloc(sizeof(PlanRowMark)); + newrc = palloc_object(PlanRowMark); memcpy(newrc, rc, sizeof(PlanRowMark)); /* adjust indexes ... but *not* the rowmarkId */ @@ -395,6 +402,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing) Index rti; ListCell *lc; + /* + * Record enough information to make it possible for code that looks at + * the final range table to understand how it was constructed. (If + * finalrtable is still NIL, then this is the very topmost PlannerInfo, + * which will always have plan_name == NULL and rtoffset == 0; we omit the + * degenerate list entry.) + */ + if (root->glob->finalrtable != NIL) + { + SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo); + + rtinfo->plan_name = root->plan_name; + rtinfo->rtoffset = list_length(root->glob->finalrtable); + + /* When recursing = true, it's an unplanned or dummy subquery. */ + rtinfo->dummy = recursing; + + root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo); + } + /* * Add the query's own RTEs to the flattened rangetable. * @@ -541,7 +568,7 @@ add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos, RangeTblEntry *newrte; /* flat copy to duplicate all the scalar fields */ - newrte = (RangeTblEntry *) palloc(sizeof(RangeTblEntry)); + newrte = palloc_object(RangeTblEntry); memcpy(newrte, rte, sizeof(RangeTblEntry)); /* zap unneeded sub-structure */ @@ -1030,16 +1057,35 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) * expected to occur here, it seems safer to special-case * it here and keep the assertions that ROWID_VARs * shouldn't be seen by fix_scan_expr. + * + * We also must handle the case where set operations have + * been short-circuited resulting in a dummy Result node. + * prepunion.c uses varno==0 for the set op targetlist. + * See generate_setop_tlist() and generate_setop_tlist(). + * Here we rewrite these to use varno==1, which is the + * varno of the first set-op child. Without this, EXPLAIN + * will have trouble displaying targetlists of dummy set + * operations. */ foreach(l, splan->plan.targetlist) { TargetEntry *tle = (TargetEntry *) lfirst(l); Var *var = (Var *) tle->expr; - if (var && IsA(var, Var) && var->varno == ROWID_VAR) - tle->expr = (Expr *) makeNullConst(var->vartype, - var->vartypmod, - var->varcollid); + if (var && IsA(var, Var)) + { + if (var->varno == ROWID_VAR) + tle->expr = (Expr *) makeNullConst(var->vartype, + var->vartypmod, + var->varcollid); + else if (var->varno == 0) + tle->expr = (Expr *) makeVar(1, + var->varattno, + var->vartype, + var->vartypmod, + var->varcollid, + var->varlevelsup); + } } splan->plan.targetlist = @@ -1052,6 +1098,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) /* resconstantqual can't contain any subplan variable refs */ splan->resconstantqual = fix_scan_expr(root, splan->resconstantqual, rtoffset, 1); + /* adjust the relids set */ + splan->relids = offset_relid_set(splan->relids, rtoffset); } break; case T_ProjectSet: @@ -1115,7 +1163,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) * those are already used by RETURNING and it seems better to * be non-conflicting. */ - if (splan->onConflictSet) + if (splan->onConflictAction == ONCONFLICT_UPDATE || + splan->onConflictAction == ONCONFLICT_SELECT) { indexed_tlist *itlist; @@ -1415,10 +1464,17 @@ set_subqueryscan_references(PlannerInfo *root, if (trivial_subqueryscan(plan)) { + Index scanrelid; + /* * We can omit the SubqueryScan node and just pull up the subplan. */ result = clean_up_removed_plan_level((Plan *) plan, plan->subplan); + + /* Remember that we removed a SubqueryScan */ + scanrelid = plan->scan.scanrelid + rtoffset; + record_elided_node(root->glob, plan->subplan->plan_node_id, + T_SubqueryScan, bms_make_singleton(scanrelid)); } else { @@ -1846,7 +1902,17 @@ set_append_references(PlannerInfo *root, Plan *p = (Plan *) linitial(aplan->appendplans); if (p->parallel_aware == aplan->plan.parallel_aware) - return clean_up_removed_plan_level((Plan *) aplan, p); + { + Plan *result; + + result = clean_up_removed_plan_level((Plan *) aplan, p); + + /* Remember that we removed an Append */ + record_elided_node(root->glob, p->plan_node_id, T_Append, + offset_relid_set(aplan->apprelids, rtoffset)); + + return result; + } } /* @@ -1914,7 +1980,17 @@ set_mergeappend_references(PlannerInfo *root, Plan *p = (Plan *) linitial(mplan->mergeplans); if (p->parallel_aware == mplan->plan.parallel_aware) - return clean_up_removed_plan_level((Plan *) mplan, p); + { + Plan *result; + + result = clean_up_removed_plan_level((Plan *) mplan, p); + + /* Remember that we removed a MergeAppend */ + record_elided_node(root->glob, p->plan_node_id, T_MergeAppend, + offset_relid_set(mplan->apprelids, rtoffset)); + + return result; + } } /* @@ -2003,7 +2079,7 @@ offset_relid_set(Relids relids, int rtoffset) static inline Var * copyVar(Var *var) { - Var *newvar = (Var *) palloc(sizeof(Var)); + Var *newvar = palloc_object(Var); *newvar = *var; return newvar; @@ -2158,9 +2234,12 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, /* * Compute the estimated cost of each subplan assuming num_exec - * executions, and keep the cheapest one. In event of exact equality of - * estimates, we prefer the later plan; this is a bit arbitrary, but in - * current usage it biases us to break ties against fast-start subplans. + * executions, and keep the cheapest one. If one subplan has more + * disabled nodes than another, choose the one with fewer disabled nodes + * regardless of cost; this parallels compare_path_costs. In event of + * exact equality of estimates, we prefer the later plan; this is a bit + * arbitrary, but in current usage it biases us to break ties against + * fast-start subplans. */ Assert(asplan->subplans != NIL); @@ -2170,7 +2249,10 @@ fix_alternative_subplan(PlannerInfo *root, AlternativeSubPlan *asplan, Cost curcost; curcost = curplan->startup_cost + num_exec * curplan->per_call_cost; - if (bestplan == NULL || curcost <= bestcost) + if (bestplan == NULL || + curplan->disabled_nodes < bestplan->disabled_nodes || + (curplan->disabled_nodes == bestplan->disabled_nodes && + curcost <= bestcost)) { bestplan = curplan; bestcost = curcost; @@ -3071,7 +3153,7 @@ search_indexed_tlist_for_sortgroupref(Expr *node, * other-relation Vars by OUTER_VAR references, while leaving target Vars * alone. Thus inner_itlist = NULL and acceptable_rel = the ID of the * target relation should be passed. - * 3) ON CONFLICT UPDATE SET/WHERE clauses. Here references to EXCLUDED are + * 3) ON CONFLICT SET and WHERE clauses. Here references to EXCLUDED are * to be replaced with INNER_VAR references, while leaving target Vars (the * to-be-updated relation) alone. Correspondingly inner_itlist is to be * EXCLUDED elements, outer_itlist = NULL and acceptable_rel the target @@ -3729,3 +3811,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) return expression_tree_walker(node, extract_query_dependencies_walker, context); } + +/* + * Record some details about a node removed from the plan during setrefs + * processing, for the benefit of code trying to reconstruct planner decisions + * from examination of the final plan tree. + */ +static void +record_elided_node(PlannerGlobal *glob, int plan_node_id, + NodeTag elided_type, Bitmapset *relids) +{ + ElidedNode *n = makeNode(ElidedNode); + + n->plan_node_id = plan_node_id; + n->elided_type = elided_type; + n->relids = relids; + + glob->elidedNodes = lappend(glob->elidedNodes, n); +} diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c index e7cb3fede6658..ccec1eaa7fe16 100644 --- a/src/backend/optimizer/plan/subselect.c +++ b/src/backend/optimizer/plan/subselect.c @@ -6,7 +6,7 @@ * This module deals with SubLinks and CTEs, but not subquery RTEs (i.e., * not sub-SELECT-in-FROM cases). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -20,6 +20,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_type.h" #include "executor/executor.h" +#include "executor/nodeSubplan.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -79,8 +80,8 @@ static Node *convert_testexpr(PlannerInfo *root, List *subst_nodes); static Node *convert_testexpr_mutator(Node *node, convert_testexpr_context *context); -static bool subplan_is_hashable(Plan *plan); -static bool subpath_is_hashable(Path *path); +static bool subplan_is_hashable(Plan *plan, bool unknownEqFalse); +static bool subpath_is_hashable(Path *path, bool unknownEqFalse); static bool testexpr_is_hashable(Node *testexpr, List *param_ids); static bool test_opexpr_is_hashable(OpExpr *testexpr, List *param_ids); static bool hash_ok_operator(OpExpr *expr); @@ -90,6 +91,7 @@ static bool contain_outer_selfref(Node *node); static bool contain_outer_selfref_walker(Node *node, Index *depth); static void inline_cte(PlannerInfo *root, CommonTableExpr *cte); static bool inline_cte_walker(Node *node, inline_cte_walker_context *context); +static bool sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink); static bool simplify_EXISTS_query(PlannerInfo *root, Query *query); static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, Node **testexpr, List **paramIds); @@ -103,6 +105,7 @@ static Bitmapset *finalize_plan(PlannerInfo *root, Bitmapset *scan_params); static bool finalize_primnode(Node *node, finalize_primnode_context *context); static bool finalize_agg_primnode(Node *node, finalize_primnode_context *context); +static const char *sublinktype_to_string(SubLinkType subLinkType); /* @@ -172,6 +175,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery, Plan *plan; List *plan_params; Node *result; + const char *sublinkstr = sublinktype_to_string(subLinkType); /* * Copy the source Query node. This is a quick and dirty kluge to resolve @@ -218,8 +222,9 @@ make_subplan(PlannerInfo *root, Query *orig_subquery, Assert(root->plan_params == NIL); /* Generate Paths for the subquery */ - subroot = subquery_planner(root->glob, subquery, root, false, - tuple_fraction, NULL); + subroot = subquery_planner(root->glob, subquery, + choose_plan_name(root->glob, sublinkstr, true), + root, NULL, false, tuple_fraction, NULL); /* Isolate the params needed by this specific subplan */ plan_params = root->plan_params; @@ -264,9 +269,12 @@ make_subplan(PlannerInfo *root, Query *orig_subquery, &newtestexpr, ¶mIds); if (subquery) { + char *plan_name; + /* Generate Paths for the ANY subquery; we'll need all rows */ - subroot = subquery_planner(root->glob, subquery, root, false, 0.0, - NULL); + plan_name = choose_plan_name(root->glob, sublinkstr, true); + subroot = subquery_planner(root->glob, subquery, plan_name, + root, subroot, false, 0.0, NULL); /* Isolate the params needed by this specific subplan */ plan_params = root->plan_params; @@ -277,7 +285,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery, best_path = final_rel->cheapest_total_path; /* Now we can check if it'll fit in hash_mem */ - if (subpath_is_hashable(best_path)) + if (subpath_is_hashable(best_path, true)) { SubPlan *hashplan; AlternativeSubPlan *asplan; @@ -324,15 +332,16 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, { Node *result; SubPlan *splan; - bool isInitPlan; ListCell *lc; /* - * Initialize the SubPlan node. Note plan_id, plan_name, and cost fields - * are set further down. + * Initialize the SubPlan node. + * + * Note: plan_id and cost fields are set further down. */ splan = makeNode(SubPlan); splan->subLinkType = subLinkType; + splan->plan_name = subroot->plan_name; splan->testexpr = NULL; splan->paramIds = NIL; get_first_col_type(plan, &splan->firstColType, &splan->firstColTypmod, @@ -391,7 +400,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, Assert(testexpr == NULL); prm = generate_new_exec_param(root, BOOLOID, -1, InvalidOid); splan->setParam = list_make1_int(prm->paramid); - isInitPlan = true; + splan->isInitPlan = true; result = (Node *) prm; } else if (splan->parParam == NIL && subLinkType == EXPR_SUBLINK) @@ -406,7 +415,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, exprTypmod((Node *) te->expr), exprCollation((Node *) te->expr)); splan->setParam = list_make1_int(prm->paramid); - isInitPlan = true; + splan->isInitPlan = true; result = (Node *) prm; } else if (splan->parParam == NIL && subLinkType == ARRAY_SUBLINK) @@ -426,7 +435,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, exprTypmod((Node *) te->expr), exprCollation((Node *) te->expr)); splan->setParam = list_make1_int(prm->paramid); - isInitPlan = true; + splan->isInitPlan = true; result = (Node *) prm; } else if (splan->parParam == NIL && subLinkType == ROWCOMPARE_SUBLINK) @@ -442,7 +451,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, testexpr, params); splan->setParam = list_copy(splan->paramIds); - isInitPlan = true; + splan->isInitPlan = true; /* * The executable expression is returned to become part of the outer @@ -476,12 +485,12 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, /* It can be an initplan if there are no parParams. */ if (splan->parParam == NIL) { - isInitPlan = true; + splan->isInitPlan = true; result = (Node *) makeNullConst(RECORDOID, -1, InvalidOid); } else { - isInitPlan = false; + splan->isInitPlan = false; result = (Node *) splan; } } @@ -517,7 +526,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, */ if (subLinkType == ANY_SUBLINK && splan->parParam == NIL && - subplan_is_hashable(plan) && + subplan_is_hashable(plan, unknownEqFalse) && testexpr_is_hashable(splan->testexpr, splan->paramIds)) splan->useHashTable = true; @@ -536,7 +545,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, plan = materialize_finished_plan(plan); result = (Node *) splan; - isInitPlan = false; + splan->isInitPlan = false; } /* @@ -547,7 +556,7 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, root->glob->subroots = lappend(root->glob->subroots, subroot); splan->plan_id = list_length(root->glob->subplans); - if (isInitPlan) + if (splan->isInitPlan) root->init_plans = lappend(root->init_plans, splan); /* @@ -557,15 +566,10 @@ build_subplan(PlannerInfo *root, Plan *plan, Path *path, * there's no point since it won't get re-run without parameter changes * anyway. The input of a hashed subplan doesn't need REWIND either. */ - if (splan->parParam == NIL && !isInitPlan && !splan->useHashTable) + if (splan->parParam == NIL && !splan->isInitPlan && !splan->useHashTable) root->glob->rewindPlanIDs = bms_add_member(root->glob->rewindPlanIDs, splan->plan_id); - /* Label the subplan for EXPLAIN purposes */ - splan->plan_name = psprintf("%s %d", - isInitPlan ? "InitPlan" : "SubPlan", - splan->plan_id); - /* Lastly, fill in the cost estimates for use later */ cost_subplan(root, splan, plan); @@ -709,19 +713,19 @@ convert_testexpr_mutator(Node *node, * is suitable for hashing. We only look at the subquery itself. */ static bool -subplan_is_hashable(Plan *plan) +subplan_is_hashable(Plan *plan, bool unknownEqFalse) { - double subquery_size; + Size hashtablesize; /* - * The estimated size of the subquery result must fit in hash_mem. (Note: - * we use heap tuple overhead here even though the tuples will actually be - * stored as MinimalTuples; this provides some fudge factor for hashtable - * overhead.) + * The estimated size of the hashtable holding the subquery result must + * fit in hash_mem. (Note: reject on equality, to ensure that an estimate + * of SIZE_MAX disables hashing regardless of the hash_mem limit.) */ - subquery_size = plan->plan_rows * - (MAXALIGN(plan->plan_width) + MAXALIGN(SizeofHeapTupleHeader)); - if (subquery_size > get_hash_memory_limit()) + hashtablesize = EstimateSubplanHashTableSpace(plan->plan_rows, + plan->plan_width, + unknownEqFalse); + if (hashtablesize >= get_hash_memory_limit()) return false; return true; @@ -733,19 +737,19 @@ subplan_is_hashable(Plan *plan) * Identical to subplan_is_hashable, but work from a Path for the subplan. */ static bool -subpath_is_hashable(Path *path) +subpath_is_hashable(Path *path, bool unknownEqFalse) { - double subquery_size; + Size hashtablesize; /* - * The estimated size of the subquery result must fit in hash_mem. (Note: - * we use heap tuple overhead here even though the tuples will actually be - * stored as MinimalTuples; this provides some fudge factor for hashtable - * overhead.) + * The estimated size of the hashtable holding the subquery result must + * fit in hash_mem. (Note: reject on equality, to ensure that an estimate + * of SIZE_MAX disables hashing regardless of the hash_mem limit.) */ - subquery_size = path->rows * - (MAXALIGN(path->pathtarget->width) + MAXALIGN(SizeofHeapTupleHeader)); - if (subquery_size > get_hash_memory_limit()) + hashtablesize = EstimateSubplanHashTableSpace(path->rows, + path->pathtarget->width, + unknownEqFalse); + if (hashtablesize >= get_hash_memory_limit()) return false; return true; @@ -965,8 +969,9 @@ SS_process_ctes(PlannerInfo *root) * Generate Paths for the CTE query. Always plan for full retrieval * --- we don't have enough info to predict otherwise. */ - subroot = subquery_planner(root->glob, subquery, root, - cte->cterecursive, 0.0, NULL); + subroot = subquery_planner(root->glob, subquery, + choose_plan_name(root->glob, cte->ctename, false), + root, NULL, cte->cterecursive, 0.0, NULL); /* * Since the current query level doesn't yet contain any RTEs, it @@ -989,10 +994,11 @@ SS_process_ctes(PlannerInfo *root) * Make a SubPlan node for it. This is just enough unlike * build_subplan that we can't share code. * - * Note plan_id, plan_name, and cost fields are set further down. + * Note: plan_id and cost fields are set further down. */ splan = makeNode(SubPlan); splan->subLinkType = CTE_SUBLINK; + splan->plan_name = subroot->plan_name; splan->testexpr = NULL; splan->paramIds = NIL; get_first_col_type(plan, &splan->firstColType, &splan->firstColTypmod, @@ -1039,9 +1045,6 @@ SS_process_ctes(PlannerInfo *root) root->cte_plan_ids = lappend_int(root->cte_plan_ids, splan->plan_id); - /* Label the subplan for EXPLAIN purposes */ - splan->plan_name = psprintf("CTE %s", cte->ctename); - /* Lastly, fill in the cost estimates for use later */ cost_subplan(root, splan, plan); } @@ -1304,11 +1307,14 @@ convert_VALUES_to_ANY(PlannerInfo *root, Node *testexpr, Query *values) * If so, form a JoinExpr and return it. Return NULL if the SubLink cannot * be converted to a join. * - * The only non-obvious input parameter is available_rels: this is the set - * of query rels that can safely be referenced in the sublink expression. - * (We must restrict this to avoid changing the semantics when a sublink - * is present in an outer join's ON qual.) The conversion must fail if - * the converted qual would reference any but these parent-query relids. + * If under_not is true, the caller actually found NOT (ANY SubLink), so + * that what we must try to build is an ANTI not SEMI join. + * + * available_rels is the set of query rels that can safely be referenced + * in the sublink expression. (We must restrict this to avoid changing + * the semantics when a sublink is present in an outer join's ON qual.) + * The conversion must fail if the converted qual would reference any but + * these parent-query relids. * * On success, the returned JoinExpr has larg = NULL and rarg = the jointree * item representing the pulled-up subquery. The caller must set larg to @@ -1331,7 +1337,7 @@ convert_VALUES_to_ANY(PlannerInfo *root, Node *testexpr, Query *values) */ JoinExpr * convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink, - Relids available_rels) + bool under_not, Relids available_rels) { JoinExpr *result; Query *parse = root->parse; @@ -1349,6 +1355,19 @@ convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink, Assert(sublink->subLinkType == ANY_SUBLINK); + /* + * Per SQL spec, NOT IN is not ordinarily equivalent to an anti-join, so + * that by default we have to fail when under_not. However, if we can + * prove that neither the outer query's expressions nor the sub-select's + * output columns can be NULL, and further that the operator itself cannot + * return NULL for non-null inputs, then the logic is identical and it's + * safe to convert NOT IN to an anti-join. + */ + if (under_not && + (!sublink_testexpr_is_not_nullable(root, sublink) || + !query_outputs_are_not_nullable(subselect))) + return NULL; + /* * If the sub-select contains any Vars of the parent query, we treat it as * LATERAL. (Vars from higher levels don't matter here.) @@ -1397,7 +1416,7 @@ convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink, */ nsitem = addRangeTableEntryForSubquery(pstate, subselect, - makeAlias("ANY_subquery", NIL), + NULL, use_lateral, false); rte = nsitem->p_rte; @@ -1426,7 +1445,7 @@ convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink, * And finally, build the JoinExpr node. */ result = makeNode(JoinExpr); - result->jointype = JOIN_SEMI; + result->jointype = under_not ? JOIN_ANTI : JOIN_SEMI; result->isNatural = false; result->larg = NULL; /* caller must fill this in */ result->rarg = (Node *) rtr; @@ -1439,12 +1458,134 @@ convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink, return result; } +/* + * sublink_testexpr_is_not_nullable: verify that testexpr of an ANY_SUBLINK + * guarantees a non-null result, assuming the inner side is also non-null. + * + * To ensure the expression never returns NULL, we require both that the outer + * expressions are provably non-nullable and that the operator itself is safe. + * We validate operator safety by checking for membership in a standard index + * operator family (B-tree or Hash); this acts as a proxy for standard boolean + * behavior, ensuring the operator does not produce NULL results from non-null + * inputs. + * + * We handle the three standard parser representations for ANY sublinks: a + * single OpExpr for single-column comparisons, a BoolExpr containing a list of + * OpExprs for multi-column equality or inequality checks (where equality + * becomes an AND and inequality becomes an OR), and a RowCompareExpr for + * multi-column ordering checks. In all cases, we validate the operators and + * the outer expressions. + * + * It is acceptable for this check not to be exhaustive. We can err on the + * side of conservatism: if we're not sure, it's okay to return FALSE. + */ +static bool +sublink_testexpr_is_not_nullable(PlannerInfo *root, SubLink *sublink) +{ + Node *testexpr = sublink->testexpr; + List *outer_exprs = NIL; + + /* Punt if sublink is not in the expected format */ + if (sublink->subLinkType != ANY_SUBLINK || testexpr == NULL) + return false; + + if (IsA(testexpr, OpExpr)) + { + /* single-column comparison */ + OpExpr *opexpr = (OpExpr *) testexpr; + + /* standard ANY structure should be op(outer_var, param) */ + if (list_length(opexpr->args) != 2) + return false; + + /* + * We rely on membership in a B-tree or Hash operator family as a + * guarantee that the operator acts as a proper boolean comparison and + * does not yield NULL for valid non-null inputs. + */ + if (!op_is_safe_index_member(opexpr->opno)) + return false; + + outer_exprs = lappend(outer_exprs, linitial(opexpr->args)); + } + else if (is_andclause(testexpr) || is_orclause(testexpr)) + { + /* multi-column equality or inequality checks */ + BoolExpr *bexpr = (BoolExpr *) testexpr; + + foreach_ptr(OpExpr, opexpr, bexpr->args) + { + if (!IsA(opexpr, OpExpr)) + return false; + + /* standard ANY structure should be op(outer_var, param) */ + if (list_length(opexpr->args) != 2) + return false; + + /* verify operator safety; see comment above */ + if (!op_is_safe_index_member(opexpr->opno)) + return false; + + outer_exprs = lappend(outer_exprs, linitial(opexpr->args)); + } + } + else if (IsA(testexpr, RowCompareExpr)) + { + /* multi-column ordering checks */ + RowCompareExpr *rcexpr = (RowCompareExpr *) testexpr; + + foreach_oid(opno, rcexpr->opnos) + { + /* verify operator safety; see comment above */ + if (!op_is_safe_index_member(opno)) + return false; + } + + outer_exprs = list_concat(outer_exprs, rcexpr->largs); + } + else + { + /* Punt if other node types */ + return false; + } + + /* + * Since the query hasn't yet been through expression preprocessing, we + * must apply flatten_join_alias_vars to the outer expressions to avoid + * being fooled by join aliases. + * + * We do not need to apply flatten_group_exprs though, since grouping Vars + * cannot appear in jointree quals. + */ + outer_exprs = (List *) + flatten_join_alias_vars(root, root->parse, (Node *) outer_exprs); + + /* Check that every outer expression is non-nullable */ + foreach_ptr(Expr, expr, outer_exprs) + { + /* + * We have already collected relation-level not-null constraints for + * the outer query, so we can consult the global hash table for + * nullability information. + */ + if (!expr_is_nonnullable(root, expr, NOTNULL_SOURCE_HASHTABLE)) + return false; + + /* + * Note: It is possible to further prove non-nullability by examining + * the qual clauses available at or below the jointree node where this + * NOT IN clause is evaluated, but for the moment it doesn't seem + * worth the extra complication. + */ + } + + return true; +} + /* * convert_EXISTS_sublink_to_join: try to convert an EXISTS SubLink to a join * - * The API of this function is identical to convert_ANY_sublink_to_join's, - * except that we also support the case where the caller has found NOT EXISTS, - * so we need an additional input parameter "under_not". + * The API of this function is identical to convert_ANY_sublink_to_join's. */ JoinExpr * convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, @@ -1454,6 +1595,7 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, Query *parse = root->parse; Query *subselect = (Query *) sublink->subselect; Node *whereClause; + PlannerInfo subroot; int rtoffset; int varno; Relids clause_varnos; @@ -1515,6 +1657,35 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, if (contain_volatile_functions(whereClause)) return NULL; + /* + * Scan the rangetable for relation RTEs and retrieve the necessary + * catalog information for each relation. Using this information, clear + * the inh flag for any relation that has no children, collect not-null + * attribute numbers for any relation that has column not-null + * constraints, and expand virtual generated columns for any relation that + * contains them. + * + * Note: we construct up an entirely dummy PlannerInfo for use here. This + * is fine because only the "glob" and "parse" links will be used in this + * case. + * + * Note: we temporarily assign back the WHERE clause so that any virtual + * generated column references within it can be expanded. It should be + * separated out again afterward. + */ + MemSet(&subroot, 0, sizeof(subroot)); + subroot.type = T_PlannerInfo; + subroot.glob = root->glob; + subroot.parse = subselect; + subselect->jointree->quals = whereClause; + subselect = preprocess_relation_rtes(&subroot); + + /* + * Now separate out the WHERE clause again. + */ + whereClause = subselect->jointree->quals; + subselect->jointree->quals = NULL; + /* * The subquery must have a nonempty jointree, but we can make it so. */ @@ -1611,15 +1782,19 @@ convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, * Note: by suppressing the targetlist we could cause an observable behavioral * change, namely that any errors that might occur in evaluating the tlist * won't occur, nor will other side-effects of volatile functions. This seems - * unlikely to bother anyone in practice. + * unlikely to bother anyone in practice. Note that any column privileges are + * still checked even if the reference is removed here. + * + * The SQL standard specifies that a SELECT * immediately inside EXISTS + * expands to not all columns but an arbitrary literal. That is kind of the + * same idea, but our optimization goes further in that it throws away the + * entire targetlist, and not only if it was written as *. * * Returns true if was able to discard the targetlist, else false. */ static bool simplify_EXISTS_query(PlannerInfo *root, Query *query) { - ListCell *lc; - /* * We don't try to simplify at all if the query uses set operations, * aggregates, grouping sets, SRFs, modifying CTEs, HAVING, OFFSET, or FOR @@ -1689,25 +1864,27 @@ simplify_EXISTS_query(PlannerInfo *root, Query *query) query->hasDistinctOn = false; /* - * Since we have thrown away the GROUP BY clauses, we'd better remove the - * RTE_GROUP RTE and clear the hasGroupRTE flag. + * Since we have thrown away the GROUP BY clauses, we'd better get rid of + * the RTE_GROUP RTE and clear the hasGroupRTE flag. To safely get rid of + * the RTE_GROUP RTE without shifting the index of any subsequent RTE in + * the rtable, we convert the RTE to be RTE_RESULT type in-place, and zero + * out RTE_GROUP-specific fields. */ - foreach(lc, query->rtable) + if (query->hasGroupRTE) { - RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); - - /* - * Remove the RTE_GROUP RTE and clear the hasGroupRTE flag. (Since - * we'll exit the foreach loop immediately, we don't bother with - * foreach_delete_current.) - */ - if (rte->rtekind == RTE_GROUP) + foreach_node(RangeTblEntry, rte, query->rtable) { - Assert(query->hasGroupRTE); - query->rtable = list_delete_cell(query->rtable, lc); - query->hasGroupRTE = false; - break; + if (rte->rtekind == RTE_GROUP) + { + rte->rtekind = RTE_RESULT; + rte->groupexprs = NIL; + + /* A query should only have one RTE_GROUP, so we can stop. */ + break; + } } + + query->hasGroupRTE = false; } return true; @@ -1732,6 +1909,7 @@ convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, Node **testexpr, List **paramIds) { Node *whereClause; + PlannerInfo subroot; List *leftargs, *rightargs, *opids, @@ -1791,12 +1969,15 @@ convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect, * parent aliases were flattened already, and we're not going to pull any * child Vars (of any description) into the parent. * - * Note: passing the parent's root to eval_const_expressions is - * technically wrong, but we can get away with it since only the - * boundParams (if any) are used, and those would be the same in a - * subroot. - */ - whereClause = eval_const_expressions(root, whereClause); + * Note: we construct up an entirely dummy PlannerInfo to pass to + * eval_const_expressions. This is fine because only the "glob" and + * "parse" links are used by eval_const_expressions. + */ + MemSet(&subroot, 0, sizeof(subroot)); + subroot.type = T_PlannerInfo; + subroot.glob = root->glob; + subroot.parse = subselect; + whereClause = eval_const_expressions(&subroot, whereClause); whereClause = (Node *) canonicalize_qual((Expr *) whereClause, false); whereClause = (Node *) make_ands_implicit((Expr *) whereClause); @@ -3151,7 +3332,8 @@ SS_make_initplan_from_plan(PlannerInfo *root, node = makeNode(SubPlan); node->subLinkType = EXPR_SUBLINK; node->plan_id = list_length(root->glob->subplans); - node->plan_name = psprintf("InitPlan %d", node->plan_id); + node->plan_name = subroot->plan_name; + node->isInitPlan = true; get_first_col_type(plan, &node->firstColType, &node->firstColTypmod, &node->firstColCollation); node->parallel_safe = plan->parallel_safe; @@ -3167,3 +3349,32 @@ SS_make_initplan_from_plan(PlannerInfo *root, /* Set costs of SubPlan using info from the plan tree */ cost_subplan(subroot, node, plan); } + +/* + * Get a string equivalent of a given subLinkType. + */ +static const char * +sublinktype_to_string(SubLinkType subLinkType) +{ + switch (subLinkType) + { + case EXISTS_SUBLINK: + return "exists"; + case ALL_SUBLINK: + return "all"; + case ANY_SUBLINK: + return "any"; + case ROWCOMPARE_SUBLINK: + return "rowcompare"; + case EXPR_SUBLINK: + return "expr"; + case MULTIEXPR_SUBLINK: + return "multiexpr"; + case ARRAY_SUBLINK: + return "array"; + case CTE_SUBLINK: + return "cte"; + } + Assert(false); + return "???"; +} diff --git a/src/backend/optimizer/prep/meson.build b/src/backend/optimizer/prep/meson.build index 4e219148a7b8b..6b1d4a0094154 100644 --- a/src/backend/optimizer/prep/meson.build +++ b/src/backend/optimizer/prep/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'prepagg.c', diff --git a/src/backend/optimizer/prep/prepagg.c b/src/backend/optimizer/prep/prepagg.c index c0a2f04a8c30f..3737cc15ba198 100644 --- a/src/backend/optimizer/prep/prepagg.c +++ b/src/backend/optimizer/prep/prepagg.c @@ -22,7 +22,7 @@ * at executor startup. The Agg nodes are constructed much later in the * planning, however, so it's not trivial. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 87dc6f56b576f..4424fdbe906b6 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -4,10 +4,10 @@ * Planner preprocessing for subqueries and join tree manipulation. * * NOTE: the intended sequence for invoking these operations is + * preprocess_relation_rtes * replace_empty_jointree * pull_up_sublinks * preprocess_function_rtes - * expand_virtual_generated_columns * pull_up_subqueries * flatten_simple_union_all * do expression preprocessing (including flattening JOIN alias vars) @@ -15,7 +15,7 @@ * remove_useless_result_rtes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -36,6 +36,7 @@ #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "optimizer/placeholder.h" +#include "optimizer/plancat.h" #include "optimizer/prep.h" #include "optimizer/subselect.h" #include "optimizer/tlist.h" @@ -87,6 +88,8 @@ typedef struct reduce_outer_joins_pass1_state { Relids relids; /* base relids within this subtree */ bool contains_outer; /* does subtree contain outer join(s)? */ + Relids nullable_rels; /* base relids that are nullable within this + * subtree */ List *sub_states; /* List of states for subtree components */ } reduce_outer_joins_pass1_state; @@ -102,6 +105,9 @@ typedef struct reduce_outer_joins_partial_state Relids unreduced_side; /* relids in its still-nullable side */ } reduce_outer_joins_partial_state; +static Query *expand_virtual_generated_columns(PlannerInfo *root, Query *parse, + RangeTblEntry *rte, int rt_index, + Relation relation); static Node *pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, Relids *relids); static Node *pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, @@ -144,7 +150,7 @@ static void replace_vars_in_jointree(Node *jtnode, pullup_replace_vars_context *context); static Node *pullup_replace_vars(Node *expr, pullup_replace_vars_context *context); -static Node *pullup_replace_vars_callback(Var *var, +static Node *pullup_replace_vars_callback(const Var *var, replace_rte_variables_context *context); static Query *pullup_replace_vars_subquery(Query *query, pullup_replace_vars_context *context); @@ -157,6 +163,8 @@ static void reduce_outer_joins_pass2(Node *jtnode, List *forced_null_vars); static void report_reduced_full_join(reduce_outer_joins_pass2_state *state2, int rtindex, Relids relids); +static bool has_notnull_forced_var(PlannerInfo *root, List *forced_null_vars, + reduce_outer_joins_pass1_state *right_state); static Node *remove_useless_results_recurse(PlannerInfo *root, Node *jtnode, Node **parent_quals, Relids *dropped_outer_joins); @@ -392,6 +400,200 @@ transform_MERGE_to_join(Query *parse) parse->mergeJoinCondition = NULL; /* join condition not needed */ } +/* + * preprocess_relation_rtes + * Do the preprocessing work for any relation RTEs in the FROM clause. + * + * This scans the rangetable for relation RTEs and retrieves the necessary + * catalog information for each relation. Using this information, it clears + * the inh flag for any relation that has no children, collects not-null + * attribute numbers for any relation that has column not-null constraints, and + * expands virtual generated columns for any relation that contains them. + * + * Note that expanding virtual generated columns may cause the query tree to + * have new copies of rangetable entries. Therefore, we have to use list_nth + * instead of foreach when iterating over the query's rangetable. + * + * Returns a modified copy of the query tree, if any relations with virtual + * generated columns are present. + */ +Query * +preprocess_relation_rtes(PlannerInfo *root) +{ + Query *parse = root->parse; + int rtable_size; + int rt_index; + + rtable_size = list_length(parse->rtable); + + for (rt_index = 0; rt_index < rtable_size; rt_index++) + { + RangeTblEntry *rte = rt_fetch(rt_index + 1, parse->rtable); + Relation relation; + + /* We only care about relation RTEs. */ + if (rte->rtekind != RTE_RELATION) + continue; + + /* + * We need not lock the relation since it was already locked by the + * rewriter. + */ + relation = table_open(rte->relid, NoLock); + + /* + * Check to see if the relation actually has any children; if not, + * clear the inh flag so we can treat it as a plain base relation. + * + * Note: this could give a false-positive result, if the rel once had + * children but no longer does. We used to be able to clear rte->inh + * later on when we discovered that, but no more; we have to handle + * such cases as full-fledged inheritance. + */ + if (rte->inh) + rte->inh = relation->rd_rel->relhassubclass; + + /* + * Check to see if the relation has any column not-null constraints; + * if so, retrieve the constraint information and store it in a + * relation OID based hash table. + */ + get_relation_notnullatts(root, relation); + + /* + * Check to see if the relation has any virtual generated columns; if + * so, replace all Var nodes in the query that reference these columns + * with the generation expressions. + */ + parse = expand_virtual_generated_columns(root, parse, + rte, rt_index + 1, + relation); + + table_close(relation, NoLock); + } + + return parse; +} + +/* + * expand_virtual_generated_columns + * Expand virtual generated columns for the given relation. + * + * This checks whether the given relation has any virtual generated columns, + * and if so, replaces all Var nodes in the query that reference those columns + * with their generation expressions. + * + * Returns a modified copy of the query tree if the relation contains virtual + * generated columns. + */ +static Query * +expand_virtual_generated_columns(PlannerInfo *root, Query *parse, + RangeTblEntry *rte, int rt_index, + Relation relation) +{ + TupleDesc tupdesc; + + /* Only normal relations can have virtual generated columns */ + Assert(rte->rtekind == RTE_RELATION); + + tupdesc = RelationGetDescr(relation); + if (tupdesc->constr && tupdesc->constr->has_generated_virtual) + { + List *tlist = NIL; + pullup_replace_vars_context rvcontext; + List *save_exclRelTlist = NIL; + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + TargetEntry *tle; + + if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + { + Node *defexpr; + + defexpr = build_generation_expression(relation, i + 1); + ChangeVarNodes(defexpr, 1, rt_index, 0); + + tle = makeTargetEntry((Expr *) defexpr, i + 1, 0, false); + tlist = lappend(tlist, tle); + } + else + { + Var *var; + + var = makeVar(rt_index, + i + 1, + attr->atttypid, + attr->atttypmod, + attr->attcollation, + 0); + + tle = makeTargetEntry((Expr *) var, i + 1, 0, false); + tlist = lappend(tlist, tle); + } + } + + Assert(list_length(tlist) > 0); + Assert(!rte->lateral); + + /* + * The relation's targetlist items are now in the appropriate form to + * insert into the query, except that we may need to wrap them in + * PlaceHolderVars. Set up required context data for + * pullup_replace_vars. + */ + rvcontext.root = root; + rvcontext.targetlist = tlist; + rvcontext.target_rte = rte; + rvcontext.result_relation = parse->resultRelation; + /* won't need these values */ + rvcontext.relids = NULL; + rvcontext.nullinfo = NULL; + /* pass NULL for outer_hasSubLinks */ + rvcontext.outer_hasSubLinks = NULL; + rvcontext.varno = rt_index; + /* this flag will be set below, if needed */ + rvcontext.wrap_option = REPLACE_WRAP_NONE; + /* initialize cache array with indexes 0 .. length(tlist) */ + rvcontext.rv_cache = palloc0((list_length(tlist) + 1) * + sizeof(Node *)); + + /* + * If the query uses grouping sets, we need a PlaceHolderVar for each + * expression of the relation's targetlist items. (See comments in + * pull_up_simple_subquery().) + */ + if (parse->groupingSets) + rvcontext.wrap_option = REPLACE_WRAP_ALL; + + /* + * Apply pullup variable replacement throughout the query tree. + * + * We intentionally do not touch the EXCLUDED pseudo-relation's + * targetlist here. Various places in the planner assume that it + * contains only Vars, and we want that to remain the case. More + * importantly, we don't want setrefs.c to turn any expanded + * EXCLUDED.virtual_column expressions in other parts of the query + * back into Vars referencing the original virtual column, which + * set_plan_refs() would do if exclRelTlist contained matching + * expressions. + */ + if (parse->onConflict) + { + save_exclRelTlist = parse->onConflict->exclRelTlist; + parse->onConflict->exclRelTlist = NIL; + } + + parse = (Query *) pullup_replace_vars((Node *) parse, &rvcontext); + + if (parse->onConflict) + parse->onConflict->exclRelTlist = save_exclRelTlist; + } + + return parse; +} + /* * replace_empty_jointree * If the Query's jointree is empty, replace it with a dummy RTE_RESULT @@ -562,7 +764,7 @@ pull_up_sublinks_jointree_recurse(PlannerInfo *root, Node *jtnode, * Make a modifiable copy of join node, but don't bother copying its * subnodes (yet). */ - j = (JoinExpr *) palloc(sizeof(JoinExpr)); + j = palloc_object(JoinExpr); memcpy(j, jtnode, sizeof(JoinExpr)); jtlink = (Node *) j; @@ -669,14 +871,15 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, if ((saop = convert_VALUES_to_ANY(root, sublink->testexpr, (Query *) sublink->subselect)) != NULL) - + { /* * The VALUES sequence was simplified. Nothing more to do * here. */ return (Node *) saop; + } - if ((j = convert_ANY_sublink_to_join(root, sublink, + if ((j = convert_ANY_sublink_to_join(root, sublink, false, available_rels1)) != NULL) { /* Yes; insert the new join node into the join tree */ @@ -702,7 +905,7 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, return NULL; } if (available_rels2 != NULL && - (j = convert_ANY_sublink_to_join(root, sublink, + (j = convert_ANY_sublink_to_join(root, sublink, false, available_rels2)) != NULL) { /* Yes; insert the new join node into the join tree */ @@ -787,14 +990,68 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, } if (is_notclause(node)) { - /* If the immediate argument of NOT is EXISTS, try to convert */ + /* If the immediate argument of NOT is ANY or EXISTS, try to convert */ SubLink *sublink = (SubLink *) get_notclausearg((Expr *) node); JoinExpr *j; Relids child_rels; if (sublink && IsA(sublink, SubLink)) { - if (sublink->subLinkType == EXISTS_SUBLINK) + if (sublink->subLinkType == ANY_SUBLINK) + { + if ((j = convert_ANY_sublink_to_join(root, sublink, true, + available_rels1)) != NULL) + { + /* Yes; insert the new join node into the join tree */ + j->larg = *jtlink1; + *jtlink1 = (Node *) j; + /* Recursively process pulled-up jointree nodes */ + j->rarg = pull_up_sublinks_jointree_recurse(root, + j->rarg, + &child_rels); + + /* + * Now recursively process the pulled-up quals. Because + * we are underneath a NOT, we can't pull up sublinks that + * reference the left-hand stuff, but it's still okay to + * pull up sublinks referencing j->rarg. + */ + j->quals = pull_up_sublinks_qual_recurse(root, + j->quals, + &j->rarg, + child_rels, + NULL, NULL); + /* Return NULL representing constant TRUE */ + return NULL; + } + if (available_rels2 != NULL && + (j = convert_ANY_sublink_to_join(root, sublink, true, + available_rels2)) != NULL) + { + /* Yes; insert the new join node into the join tree */ + j->larg = *jtlink2; + *jtlink2 = (Node *) j; + /* Recursively process pulled-up jointree nodes */ + j->rarg = pull_up_sublinks_jointree_recurse(root, + j->rarg, + &child_rels); + + /* + * Now recursively process the pulled-up quals. Because + * we are underneath a NOT, we can't pull up sublinks that + * reference the left-hand stuff, but it's still okay to + * pull up sublinks referencing j->rarg. + */ + j->quals = pull_up_sublinks_qual_recurse(root, + j->quals, + &j->rarg, + child_rels, + NULL, NULL); + /* Return NULL representing constant TRUE */ + return NULL; + } + } + else if (sublink->subLinkType == EXISTS_SUBLINK) { if ((j = convert_EXISTS_sublink_to_join(root, sublink, true, available_rels1)) != NULL) @@ -887,13 +1144,15 @@ pull_up_sublinks_qual_recurse(PlannerInfo *root, Node *node, /* * preprocess_function_rtes * Constant-simplify any FUNCTION RTEs in the FROM clause, and then - * attempt to "inline" any that are set-returning functions. + * attempt to "inline" any that can be converted to simple subqueries. * - * If an RTE_FUNCTION rtable entry invokes a set-returning function that + * If an RTE_FUNCTION rtable entry invokes a set-returning SQL function that * contains just a simple SELECT, we can convert the rtable entry to an - * RTE_SUBQUERY entry exposing the SELECT directly. This is especially - * useful if the subquery can then be "pulled up" for further optimization, - * but we do it even if not, to reduce executor overhead. + * RTE_SUBQUERY entry exposing the SELECT directly. Other sorts of functions + * are also inline-able if they have a support function that can generate + * the replacement sub-Query. This is especially useful if the subquery can + * then be "pulled up" for further optimization, but we do it even if not, + * to reduce executor overhead. * * This has to be done before we have started to do any optimization of * subqueries, else any such steps wouldn't get applied to subqueries @@ -928,7 +1187,7 @@ preprocess_function_rtes(PlannerInfo *root) eval_const_expressions(root, (Node *) rte->functions); /* Check safety of expansion, and expand if possible */ - funcquery = inline_set_returning_function(root, rte); + funcquery = inline_function_in_from(root, rte); if (funcquery) { /* Successful expansion, convert the RTE to a subquery */ @@ -949,128 +1208,6 @@ preprocess_function_rtes(PlannerInfo *root) } } -/* - * expand_virtual_generated_columns - * Expand all virtual generated column references in a query. - * - * This scans the rangetable for relations with virtual generated columns, and - * replaces all Var nodes in the query that reference these columns with the - * generation expressions. Note that we do not descend into subqueries; that - * is taken care of when the subqueries are planned. - * - * This has to be done after we have pulled up any SubLinks within the query's - * quals; otherwise any virtual generated column references within the SubLinks - * that should be transformed into joins wouldn't get expanded. - * - * Returns a modified copy of the query tree, if any relations with virtual - * generated columns are present. - */ -Query * -expand_virtual_generated_columns(PlannerInfo *root) -{ - Query *parse = root->parse; - int rt_index; - ListCell *lc; - - rt_index = 0; - foreach(lc, parse->rtable) - { - RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); - Relation rel; - TupleDesc tupdesc; - - ++rt_index; - - /* - * Only normal relations can have virtual generated columns. - */ - if (rte->rtekind != RTE_RELATION) - continue; - - rel = table_open(rte->relid, NoLock); - - tupdesc = RelationGetDescr(rel); - if (tupdesc->constr && tupdesc->constr->has_generated_virtual) - { - List *tlist = NIL; - pullup_replace_vars_context rvcontext; - - for (int i = 0; i < tupdesc->natts; i++) - { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - TargetEntry *tle; - - if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - { - Node *defexpr; - - defexpr = build_generation_expression(rel, i + 1); - ChangeVarNodes(defexpr, 1, rt_index, 0); - - tle = makeTargetEntry((Expr *) defexpr, i + 1, 0, false); - tlist = lappend(tlist, tle); - } - else - { - Var *var; - - var = makeVar(rt_index, - i + 1, - attr->atttypid, - attr->atttypmod, - attr->attcollation, - 0); - - tle = makeTargetEntry((Expr *) var, i + 1, 0, false); - tlist = lappend(tlist, tle); - } - } - - Assert(list_length(tlist) > 0); - Assert(!rte->lateral); - - /* - * The relation's targetlist items are now in the appropriate form - * to insert into the query, except that we may need to wrap them - * in PlaceHolderVars. Set up required context data for - * pullup_replace_vars. - */ - rvcontext.root = root; - rvcontext.targetlist = tlist; - rvcontext.target_rte = rte; - rvcontext.result_relation = parse->resultRelation; - /* won't need these values */ - rvcontext.relids = NULL; - rvcontext.nullinfo = NULL; - /* pass NULL for outer_hasSubLinks */ - rvcontext.outer_hasSubLinks = NULL; - rvcontext.varno = rt_index; - /* this flag will be set below, if needed */ - rvcontext.wrap_option = REPLACE_WRAP_NONE; - /* initialize cache array with indexes 0 .. length(tlist) */ - rvcontext.rv_cache = palloc0((list_length(tlist) + 1) * - sizeof(Node *)); - - /* - * If the query uses grouping sets, we need a PlaceHolderVar for - * each expression of the relation's targetlist items. (See - * comments in pull_up_simple_subquery().) - */ - if (parse->groupingSets) - rvcontext.wrap_option = REPLACE_WRAP_ALL; - - /* - * Apply pullup variable replacement throughout the query tree. - */ - parse = (Query *) pullup_replace_vars((Node *) parse, &rvcontext); - } - - table_close(rel, NoLock); - } - - return parse; -} - /* * pull_up_subqueries * Look for subqueries in the rangetable that can be pulled up into @@ -1299,6 +1436,8 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, subroot->parse = subquery; subroot->glob = root->glob; subroot->query_level = root->query_level; + subroot->plan_name = root->plan_name; + subroot->alternative_plan_name = root->alternative_plan_name; subroot->parent_root = root->parent_root; subroot->plan_params = NIL; subroot->outer_params = NULL; @@ -1326,6 +1465,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, subroot->qual_security_level = 0; subroot->placeholdersFrozen = false; subroot->hasRecursion = false; + subroot->assumeReplanning = false; subroot->wt_param_id = -1; subroot->non_recursive_path = NULL; /* We don't currently need a top JoinDomain for the subroot */ @@ -1333,6 +1473,16 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, /* No CTEs to worry about */ Assert(subquery->cteList == NIL); + /* + * Scan the rangetable for relation RTEs and retrieve the necessary + * catalog information for each relation. Using this information, clear + * the inh flag for any relation that has no children, collect not-null + * attribute numbers for any relation that has column not-null + * constraints, and expand virtual generated columns for any relation that + * contains them. + */ + subquery = subroot->parse = preprocess_relation_rtes(subroot); + /* * If the FROM clause is empty, replace it with a dummy RTE_RESULT RTE, so * that we don't need so many special cases to deal with that situation. @@ -1352,13 +1502,6 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, */ preprocess_function_rtes(subroot); - /* - * Scan the rangetable for relations with virtual generated columns, and - * replace all Var nodes in the query that reference these columns with - * the generation expressions. - */ - subquery = subroot->parse = expand_virtual_generated_columns(subroot); - /* * Recursively pull up the subquery's subqueries, so that * pull_up_subqueries' processing is complete for its jointree and @@ -1508,6 +1651,10 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, case RTE_GROUP: /* these can't contain any lateral references */ break; + case RTE_GRAPH_TABLE: + /* shouldn't happen here */ + Assert(false); + break; } } } @@ -2573,6 +2720,10 @@ replace_vars_in_jointree(Node *jtnode, /* these shouldn't be marked LATERAL */ Assert(false); break; + case RTE_GRAPH_TABLE: + /* shouldn't happen here */ + Assert(false); + break; } } } @@ -2630,7 +2781,7 @@ pullup_replace_vars(Node *expr, pullup_replace_vars_context *context) } static Node * -pullup_replace_vars_callback(Var *var, +pullup_replace_vars_callback(const Var *var, replace_rte_variables_context *context) { pullup_replace_vars_context *rcon = (pullup_replace_vars_context *) context->callback_arg; @@ -3080,13 +3231,16 @@ flatten_simple_union_all(PlannerInfo *root) * to each side separately.) * * Another transformation we apply here is to recognize cases like - * SELECT ... FROM a LEFT JOIN b ON (a.x = b.y) WHERE b.y IS NULL; - * If the join clause is strict for b.y, then only null-extended rows could - * pass the upper WHERE, and we can conclude that what the query is really - * specifying is an anti-semijoin. We change the join type from JOIN_LEFT - * to JOIN_ANTI. The IS NULL clause then becomes redundant, and must be - * removed to prevent bogus selectivity calculations, but we leave it to - * distribute_qual_to_rels to get rid of such clauses. + * SELECT ... FROM a LEFT JOIN b ON (a.x = b.y) WHERE b.z IS NULL; + * If we can prove that b.z must be non-null for any matching row, either + * because the join clause is strict for b.z and b.z happens to be the join + * key b.y, or because b.z is defined NOT NULL by table constraints and is + * not nullable due to lower-level outer joins, then only null-extended rows + * could pass the upper WHERE, and we can conclude that what the query is + * really specifying is an anti-semijoin. We change the join type from + * JOIN_LEFT to JOIN_ANTI. The IS NULL clause then becomes redundant, and + * must be removed to prevent bogus selectivity calculations, but we leave + * it to distribute_qual_to_rels to get rid of such clauses. * * Also, we get rid of JOIN_RIGHT cases by flipping them around to become * JOIN_LEFT. This saves some code here and in some later planner routines; @@ -3110,8 +3264,9 @@ reduce_outer_joins(PlannerInfo *root) * to stop descending the jointree as soon as there are no outer joins * below our current point. This consideration forces a two-pass process. * The first pass gathers information about which base rels appear below - * each side of each join clause, and about whether there are outer - * join(s) below each side of each join clause. The second pass examines + * each side of each join clause, about whether there are outer join(s) + * below each side of each join clause, and about which base rels are from + * the nullable side of those outer join(s). The second pass examines * qual clauses and changes join types as it descends the tree. */ state1 = reduce_outer_joins_pass1((Node *) root->parse->jointree); @@ -3176,10 +3331,10 @@ reduce_outer_joins_pass1(Node *jtnode) { reduce_outer_joins_pass1_state *result; - result = (reduce_outer_joins_pass1_state *) - palloc(sizeof(reduce_outer_joins_pass1_state)); + result = palloc_object(reduce_outer_joins_pass1_state); result->relids = NULL; result->contains_outer = false; + result->nullable_rels = NULL; result->sub_states = NIL; if (jtnode == NULL) @@ -3203,29 +3358,62 @@ reduce_outer_joins_pass1(Node *jtnode) result->relids = bms_add_members(result->relids, sub_state->relids); result->contains_outer |= sub_state->contains_outer; + result->nullable_rels = bms_add_members(result->nullable_rels, + sub_state->nullable_rels); result->sub_states = lappend(result->sub_states, sub_state); } } else if (IsA(jtnode, JoinExpr)) { JoinExpr *j = (JoinExpr *) jtnode; - reduce_outer_joins_pass1_state *sub_state; + reduce_outer_joins_pass1_state *left_state; + reduce_outer_joins_pass1_state *right_state; + + /* Recurse to children */ + left_state = reduce_outer_joins_pass1(j->larg); + right_state = reduce_outer_joins_pass1(j->rarg); /* join's own RT index is not wanted in result->relids */ - if (IS_OUTER_JOIN(j->jointype)) - result->contains_outer = true; - - sub_state = reduce_outer_joins_pass1(j->larg); - result->relids = bms_add_members(result->relids, - sub_state->relids); - result->contains_outer |= sub_state->contains_outer; - result->sub_states = lappend(result->sub_states, sub_state); - - sub_state = reduce_outer_joins_pass1(j->rarg); - result->relids = bms_add_members(result->relids, - sub_state->relids); - result->contains_outer |= sub_state->contains_outer; - result->sub_states = lappend(result->sub_states, sub_state); + result->relids = bms_union(left_state->relids, right_state->relids); + + /* Store children's states for pass 2 */ + result->sub_states = list_make2(left_state, right_state); + + /* Collect outer join information */ + switch (j->jointype) + { + case JOIN_INNER: + case JOIN_SEMI: + /* No new nullability; propagate state from children */ + result->contains_outer = left_state->contains_outer || + right_state->contains_outer; + result->nullable_rels = bms_union(left_state->nullable_rels, + right_state->nullable_rels); + break; + case JOIN_LEFT: + case JOIN_ANTI: + /* RHS is nullable; LHS keeps existing status */ + result->contains_outer = true; + result->nullable_rels = bms_union(left_state->nullable_rels, + right_state->relids); + break; + case JOIN_RIGHT: + /* LHS is nullable; RHS keeps existing status */ + result->contains_outer = true; + result->nullable_rels = bms_union(left_state->relids, + right_state->nullable_rels); + break; + case JOIN_FULL: + /* Both sides are nullable */ + result->contains_outer = true; + result->nullable_rels = bms_union(left_state->relids, + right_state->relids); + break; + default: + elog(ERROR, "unrecognized join type: %d", + (int) j->jointype); + break; + } } else elog(ERROR, "unrecognized node type: %d", @@ -3377,15 +3565,16 @@ reduce_outer_joins_pass2(Node *jtnode, /* * See if we can reduce JOIN_LEFT to JOIN_ANTI. This is the case if - * the join's own quals are strict for any var that was forced null by - * higher qual levels. NOTE: there are other ways that we could - * detect an anti-join, in particular if we were to check whether Vars - * coming from the RHS must be non-null because of table constraints. - * That seems complicated and expensive though (in particular, one - * would have to be wary of lower outer joins). For the moment this - * seems sufficient. + * any var from the RHS was forced null by higher qual levels, but is + * known to be non-nullable. We detect this either by seeing if the + * join's own quals are strict for the var, or by checking if the var + * is defined NOT NULL by table constraints (being careful to exclude + * vars that are nullable due to lower-level outer joins). In either + * case, the only way the higher qual clause's requirement for NULL + * can be met is if the join fails to match, producing a null-extended + * row. Thus, we can treat this as an anti-join. */ - if (jointype == JOIN_LEFT) + if (jointype == JOIN_LEFT && forced_null_vars != NIL) { List *nonnullable_vars; Bitmapset *overlap; @@ -3397,9 +3586,13 @@ reduce_outer_joins_pass2(Node *jtnode, * It's not sufficient to check whether nonnullable_vars and * forced_null_vars overlap: we need to know if the overlap * includes any RHS variables. + * + * Also check if any forced-null var is defined NOT NULL by table + * constraints. */ overlap = mbms_overlap_sets(nonnullable_vars, forced_null_vars); - if (bms_overlap(overlap, right_state->relids)) + if (bms_overlap(overlap, right_state->relids) || + has_notnull_forced_var(root, forced_null_vars, right_state)) jointype = JOIN_ANTI; } @@ -3529,12 +3722,110 @@ report_reduced_full_join(reduce_outer_joins_pass2_state *state2, { reduce_outer_joins_partial_state *statep; - statep = palloc(sizeof(reduce_outer_joins_partial_state)); + statep = palloc_object(reduce_outer_joins_partial_state); statep->full_join_rti = rtindex; statep->unreduced_side = relids; state2->partial_reduced = lappend(state2->partial_reduced, statep); } +/* + * has_notnull_forced_var + * Check if "forced_null_vars" contains any Vars belonging to the subtree + * indicated by "right_state" that are known to be non-nullable due to + * table constraints. + * + * Note that we must also consider the situation where a NOT NULL Var can be + * nulled by lower-level outer joins. + * + * Helper for reduce_outer_joins_pass2. + */ +static bool +has_notnull_forced_var(PlannerInfo *root, List *forced_null_vars, + reduce_outer_joins_pass1_state *right_state) +{ + int varno = -1; + + foreach_node(Bitmapset, attrs, forced_null_vars) + { + RangeTblEntry *rte; + Bitmapset *notnullattnums; + Bitmapset *forcednullattnums = NULL; + int attno; + + varno++; + + /* Skip empty bitmaps */ + if (bms_is_empty(attrs)) + continue; + + /* Skip Vars that do not belong to the target relations */ + if (!bms_is_member(varno, right_state->relids)) + continue; + + /* + * Skip Vars that can be nulled by lower-level outer joins within the + * given subtree. These Vars might be NULL even if the schema defines + * them as NOT NULL. + */ + if (bms_is_member(varno, right_state->nullable_rels)) + continue; + + /* + * Iterate over attributes and adjust the bitmap indexes by + * FirstLowInvalidHeapAttributeNumber to get the actual attribute + * numbers. + */ + attno = -1; + while ((attno = bms_next_member(attrs, attno)) >= 0) + { + AttrNumber real_attno = attno + FirstLowInvalidHeapAttributeNumber; + + /* system columns cannot be NULL */ + if (real_attno < 0) + return true; + + forcednullattnums = bms_add_member(forcednullattnums, real_attno); + } + + rte = rt_fetch(varno, root->parse->rtable); + + /* We can only reason about ordinary relations */ + if (rte->rtekind != RTE_RELATION) + { + bms_free(forcednullattnums); + continue; + } + + /* + * We must skip inheritance parent tables, as some child tables may + * have a NOT NULL constraint for a column while others may not. This + * cannot happen with partitioned tables, though. + */ + if (rte->inh && rte->relkind != RELKIND_PARTITIONED_TABLE) + { + bms_free(forcednullattnums); + continue; + } + + /* Get the column not-null constraint information for this relation */ + notnullattnums = find_relation_notnullatts(root, rte->relid); + + /* + * Check if any forced-null attributes are defined as NOT NULL by + * table constraints. + */ + if (bms_overlap(notnullattnums, forcednullattnums)) + { + bms_free(forcednullattnums); + return true; + } + + bms_free(forcednullattnums); + } + + return false; +} + /* * remove_useless_result_rtes diff --git a/src/backend/optimizer/prep/prepqual.c b/src/backend/optimizer/prep/prepqual.c index 008dce2478c58..c71729bb1b020 100644 --- a/src/backend/optimizer/prep/prepqual.c +++ b/src/backend/optimizer/prep/prepqual.c @@ -19,7 +19,7 @@ * tree after local transformations that might introduce nested AND/ORs. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index ffc9d6c3f301c..1647066a13d7a 100644 --- a/src/backend/optimizer/prep/preptlist.c +++ b/src/backend/optimizer/prep/preptlist.c @@ -25,7 +25,7 @@ * rewriter's work is more concerned with SQL semantics. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -36,7 +36,9 @@ #include "postgres.h" +#include "access/sysattr.h" #include "access/table.h" +#include "catalog/pg_type_d.h" #include "nodes/makefuncs.h" #include "optimizer/appendinfo.h" #include "optimizer/optimizer.h" diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c index eab44da65b8f0..d1f022c5bfddc 100644 --- a/src/backend/optimizer/prep/prepunion.c +++ b/src/backend/optimizer/prep/prepunion.c @@ -12,7 +12,7 @@ * case, but most of the heavy lifting for that is done elsewhere, * notably in prepjointree.c and allpaths.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -23,6 +23,8 @@ */ #include "postgres.h" +#include + #include "access/htup_details.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -35,6 +37,7 @@ #include "optimizer/prep.h" #include "optimizer/tlist.h" #include "parser/parse_coerce.h" +#include "port/pg_bitutils.h" #include "utils/selfuncs.h" @@ -74,6 +77,8 @@ static List *generate_append_tlist(List *colTypes, List *colCollations, List *input_tlists, List *refnames_tlist); static List *generate_setop_grouplist(SetOperationStmt *op, List *targetlist); +static PathTarget *create_setop_pathtarget(PlannerInfo *root, List *tlist, + List *child_pathlist); /* @@ -228,6 +233,7 @@ recurse_set_operations(Node *setOp, PlannerInfo *root, PlannerInfo *subroot; List *tlist; bool trivial_tlist; + char *plan_name; Assert(subquery != NULL); @@ -242,7 +248,9 @@ recurse_set_operations(Node *setOp, PlannerInfo *root, * parentOp, pass that down to encourage subquery_planner to consider * suitably-sorted Paths. */ - subroot = rel->subroot = subquery_planner(root->glob, subquery, root, + plan_name = choose_plan_name(root->glob, "setop", true); + subroot = rel->subroot = subquery_planner(root->glob, subquery, + plan_name, root, NULL, false, root->tuple_fraction, parentOp); @@ -519,6 +527,13 @@ build_setop_child_paths(PlannerInfo *root, RelOptInfo *rel, bool is_sorted; int presorted_keys; + /* If the input rel is dummy, propagate that to this query level */ + if (is_dummy_rel(final_rel)) + { + mark_dummy_rel(rel); + continue; + } + /* * Include the cheapest path as-is so that the set operation can be * cheaply implemented using a method which does not require the input @@ -682,9 +697,9 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, ListCell *lc; ListCell *lc2; ListCell *lc3; - List *cheapest_pathlist = NIL; - List *ordered_pathlist = NIL; - List *partial_pathlist = NIL; + AppendPathInput cheapest = {0}; + AppendPathInput ordered = {0}; + AppendPathInput partial = {0}; bool partial_paths_valid = true; bool consider_parallel = true; List *rellist; @@ -759,7 +774,17 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, RelOptInfo *rel = lfirst(lc); Path *ordered_path; - cheapest_pathlist = lappend(cheapest_pathlist, + /* + * Record the relids so that we can identify the correct + * UPPERREL_SETOP RelOptInfo below. + */ + relids = bms_add_members(relids, rel->relids); + + /* Skip any UNION children that are proven not to yield any rows */ + if (is_dummy_rel(rel)) + continue; + + cheapest.subpaths = lappend(cheapest.subpaths, rel->cheapest_total_path); if (try_sorted) @@ -771,7 +796,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, false); if (ordered_path != NULL) - ordered_pathlist = lappend(ordered_pathlist, ordered_path); + ordered.subpaths = lappend(ordered.subpaths, ordered_path); else { /* @@ -794,25 +819,32 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, else if (rel->partial_pathlist == NIL) partial_paths_valid = false; else - partial_pathlist = lappend(partial_pathlist, - linitial(rel->partial_pathlist)); + partial.partial_subpaths = lappend(partial.partial_subpaths, + linitial(rel->partial_pathlist)); } - - relids = bms_union(relids, rel->relids); } /* Build result relation. */ result_rel = fetch_upper_rel(root, UPPERREL_SETOP, relids); - result_rel->reltarget = create_pathtarget(root, tlist); + result_rel->reltarget = create_setop_pathtarget(root, tlist, + cheapest.subpaths); result_rel->consider_parallel = consider_parallel; result_rel->consider_startup = (root->tuple_fraction > 0); + /* If all UNION children were dummy rels, make the resulting rel dummy */ + if (cheapest.subpaths == NIL) + { + mark_dummy_rel(result_rel); + + return result_rel; + } + /* * Append the child results together using the cheapest paths from each * union child. */ - apath = (Path *) create_append_path(root, result_rel, cheapest_pathlist, - NIL, NIL, NULL, 0, false, -1); + apath = (Path *) create_append_path(root, result_rel, cheapest, + NIL, NULL, 0, false, -1); /* * Estimate number of groups. For now we just assume the output is unique @@ -831,7 +863,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, int parallel_workers = 0; /* Find the highest number of workers requested for any subpath. */ - foreach(lc, partial_pathlist) + foreach(lc, partial.partial_subpaths) { Path *subpath = lfirst(lc); @@ -850,14 +882,14 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, if (enable_parallel_append) { parallel_workers = Max(parallel_workers, - pg_leftmost_one_pos32(list_length(partial_pathlist)) + 1); + pg_leftmost_one_pos32(list_length(partial.partial_subpaths)) + 1); parallel_workers = Min(parallel_workers, max_parallel_workers_per_gather); } Assert(parallel_workers > 0); papath = (Path *) - create_append_path(root, result_rel, NIL, partial_pathlist, + create_append_path(root, result_rel, partial, NIL, NULL, parallel_workers, enable_parallel_append, -1); gpath = (Path *) @@ -870,16 +902,37 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, double dNumGroups; bool can_sort = grouping_is_sortable(groupList); bool can_hash = grouping_is_hashable(groupList); + Path *first_path = linitial(cheapest.subpaths); /* - * XXX for the moment, take the number of distinct groups as equal to - * the total input size, i.e., the worst case. This is too - * conservative, but it's not clear how to get a decent estimate of - * the true size. One should note as well the propensity of novices - * to write UNION rather than UNION ALL even when they don't expect - * any duplicates... + * Estimate the number of UNION output rows. In the case when only a + * single UNION child remains, we can use estimate_num_groups() on + * that child. We must be careful not to do this when that child is + * the result of some other set operation as the targetlist will + * contain Vars with varno==0, which estimate_num_groups() wouldn't + * like. */ - dNumGroups = apath->rows; + if (list_length(cheapest.subpaths) == 1 && + first_path->parent->reloptkind != RELOPT_UPPER_REL) + { + dNumGroups = estimate_num_groups(root, + first_path->pathtarget->exprs, + first_path->rows, + NULL, + NULL); + } + else + { + /* + * Otherwise, for the moment, take the number of distinct groups + * as equal to the total input size, i.e., the worst case. This + * is too conservative, but it's not clear how to get a decent + * estimate of the true size. One should note as well the + * propensity of novices to write UNION rather than UNION ALL even + * when they don't expect any duplicates... + */ + dNumGroups = apath->rows; + } if (can_hash) { @@ -892,7 +945,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, path = (Path *) create_agg_path(root, result_rel, apath, - create_pathtarget(root, tlist), + result_rel->reltarget, AGG_HASHED, AGGSPLIT_SIMPLE, groupList, @@ -908,7 +961,7 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, path = (Path *) create_agg_path(root, result_rel, gpath, - create_pathtarget(root, tlist), + result_rel->reltarget, AGG_HASHED, AGGSPLIT_SIMPLE, groupList, @@ -929,11 +982,11 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, make_pathkeys_for_sortclauses(root, groupList, tlist), -1.0); - path = (Path *) create_upper_unique_path(root, - result_rel, - path, - list_length(path->pathkeys), - dNumGroups); + path = (Path *) create_unique_path(root, + result_rel, + path, + list_length(path->pathkeys), + dNumGroups); add_path(result_rel, path); @@ -946,11 +999,11 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, make_pathkeys_for_sortclauses(root, groupList, tlist), -1.0); - path = (Path *) create_upper_unique_path(root, - result_rel, - path, - list_length(path->pathkeys), - dNumGroups); + path = (Path *) create_unique_path(root, + result_rel, + path, + list_length(path->pathkeys), + dNumGroups); add_path(result_rel, path); } } @@ -965,16 +1018,17 @@ generate_union_paths(SetOperationStmt *op, PlannerInfo *root, path = (Path *) create_merge_append_path(root, result_rel, - ordered_pathlist, + ordered.subpaths, + NIL, union_pathkeys, NULL); /* and make the MergeAppend unique */ - path = (Path *) create_upper_unique_path(root, - result_rel, - path, - list_length(tlist), - dNumGroups); + path = (Path *) create_unique_path(root, + result_rel, + path, + list_length(tlist), + dNumGroups); add_path(result_rel, path); } @@ -1130,7 +1184,85 @@ generate_nonunion_paths(SetOperationStmt *op, PlannerInfo *root, /* Build result relation. */ result_rel = fetch_upper_rel(root, UPPERREL_SETOP, bms_union(lrel->relids, rrel->relids)); - result_rel->reltarget = create_pathtarget(root, tlist); + + /* + * Create the PathTarget and set the width accordingly. For EXCEPT, since + * the set op result won't contain rows from the rpath, we only account + * for the width of the lpath. For INTERSECT, use both input paths. + */ + if (op->op == SETOP_EXCEPT) + result_rel->reltarget = create_setop_pathtarget(root, tlist, + list_make1(lpath)); + else + result_rel->reltarget = create_setop_pathtarget(root, tlist, + list_make2(lpath, rpath)); + + /* Check for provably empty setop inputs and add short-circuit paths. */ + if (op->op == SETOP_EXCEPT) + { + /* + * For EXCEPTs, if the left side is dummy then there's no need to + * inspect the right-hand side as scanning the right to find tuples to + * remove won't make the left-hand input any more empty. + */ + if (is_dummy_rel(lrel)) + { + mark_dummy_rel(result_rel); + + return result_rel; + } + + /* Handle EXCEPTs with dummy right input */ + if (is_dummy_rel(rrel)) + { + if (op->all) + { + Path *apath; + AppendPathInput append = {0}; + + append.subpaths = list_make1(lpath); + + /* + * EXCEPT ALL: If the right-hand input is dummy then we can + * simply scan the left-hand input. To keep createplan.c + * happy, use a single child Append to handle the translation + * between the set op targetlist and the targetlist of the + * left input. The Append will be removed in setrefs.c. + */ + apath = (Path *) create_append_path(root, result_rel, + append, NIL, NULL, 0, + false, -1); + + add_path(result_rel, apath); + + return result_rel; + } + else + { + /* + * To make EXCEPT with a dummy RHS work means having to + * deduplicate the left input. That could be done with + * AggPaths, but it doesn't seem worth the effort. Let the + * normal path generation code below handle this one. + */ + } + } + } + else + { + /* + * For INTERSECT, if either input is a dummy rel then we can mark the + * result_rel as dummy since intersecting with an empty relation can + * never yield any results. This is true regardless of INTERSECT or + * INTERSECT ALL. + */ + if (is_dummy_rel(lrel) || is_dummy_rel(rrel)) + { + mark_dummy_rel(result_rel); + + return result_rel; + } + } /* * Estimate number of distinct groups that we'll need hashtable entries @@ -1503,7 +1635,7 @@ generate_append_tlist(List *colTypes, List *colCollations, * If the inputs all agree on type and typmod of a particular column, use * that typmod; else use -1. */ - colTypmods = (int32 *) palloc(list_length(colTypes) * sizeof(int32)); + colTypmods = palloc_array(int32, list_length(colTypes)); foreach(tlistl, input_tlists) { @@ -1619,3 +1751,38 @@ generate_setop_grouplist(SetOperationStmt *op, List *targetlist) Assert(lg == NULL); return grouplist; } + +/* + * create_setop_pathtarget + * Do the normal create_pathtarget() work, plus set the resulting + * PathTarget's width to the average width of the Paths in child_pathlist + * weighted using the estimated row count of each path. + * + * Note: This is required because set op target lists use varno==0, which + * results in a type default width estimate rather than one that's based on + * statistics of the columns from the set op children. + */ +static PathTarget * +create_setop_pathtarget(PlannerInfo *root, List *tlist, List *child_pathlist) +{ + PathTarget *reltarget; + ListCell *lc; + double parent_rows = 0; + double parent_size = 0; + + reltarget = create_pathtarget(root, tlist); + + /* Calculate the total rows and total size. */ + foreach(lc, child_pathlist) + { + Path *path = (Path *) lfirst(lc); + + parent_rows += path->rows; + parent_size += path->parent->reltarget->width * path->rows; + } + + if (parent_rows > 0) + reltarget->width = rint(parent_size / parent_rows); + + return reltarget; +} diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile index 4fb115cb118f5..87b4c3c086984 100644 --- a/src/backend/optimizer/util/Makefile +++ b/src/backend/optimizer/util/Makefile @@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global OBJS = \ appendinfo.o \ clauses.o \ + extendplan.o \ inherit.o \ joininfo.o \ orclauses.o \ diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c index 5b3dc0d865399..778e4662f6e94 100644 --- a/src/backend/optimizer/util/appendinfo.c +++ b/src/backend/optimizer/util/appendinfo.c @@ -3,7 +3,7 @@ * appendinfo.c * Routines for mapping between append parent(s) and children * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -15,6 +15,7 @@ #include "postgres.h" #include "access/htup_details.h" +#include "access/sysattr.h" #include "access/table.h" #include "foreign/fdwapi.h" #include "nodes/makefuncs.h" @@ -237,8 +238,9 @@ adjust_appendrel_attrs_mutator(Node *node, * You might think we need to adjust var->varnullingrels, but that * shouldn't need any changes. It will contain outer-join relids, * while the transformation we are making affects only baserels. - * Below, we just propagate var->varnullingrels into the translated - * Var. + * Below, we just merge var->varnullingrels into the translated Var. + * (We must merge not just copy: the child Var could have some + * nullingrel bits set already, and we mustn't drop those.) * * If var->varnullingrels isn't empty, and the translation wouldn't be * a Var, we have to fail. One could imagine wrapping the translated @@ -291,8 +293,11 @@ adjust_appendrel_attrs_mutator(Node *node, var->varattno, get_rel_name(appinfo->parent_reloid)); if (IsA(newnode, Var)) { - ((Var *) newnode)->varreturningtype = var->varreturningtype; - ((Var *) newnode)->varnullingrels = var->varnullingrels; + Var *newvar = (Var *) newnode; + + newvar->varreturningtype = var->varreturningtype; + newvar->varnullingrels = bms_add_members(newvar->varnullingrels, + var->varnullingrels); } else { @@ -516,6 +521,57 @@ adjust_appendrel_attrs_mutator(Node *node, return (Node *) newinfo; } + /* + * We have to process RelAggInfo nodes specially. + */ + if (IsA(node, RelAggInfo)) + { + RelAggInfo *oldinfo = (RelAggInfo *) node; + RelAggInfo *newinfo = makeNode(RelAggInfo); + + newinfo->target = (PathTarget *) + adjust_appendrel_attrs_mutator((Node *) oldinfo->target, + context); + + newinfo->agg_input = (PathTarget *) + adjust_appendrel_attrs_mutator((Node *) oldinfo->agg_input, + context); + + newinfo->group_clauses = oldinfo->group_clauses; + + newinfo->group_exprs = (List *) + adjust_appendrel_attrs_mutator((Node *) oldinfo->group_exprs, + context); + + return (Node *) newinfo; + } + + /* + * We have to process PathTarget nodes specially. + */ + if (IsA(node, PathTarget)) + { + PathTarget *oldtarget = (PathTarget *) node; + PathTarget *newtarget = makeNode(PathTarget); + + /* Copy all flat-copiable fields */ + memcpy(newtarget, oldtarget, sizeof(PathTarget)); + + newtarget->exprs = (List *) + adjust_appendrel_attrs_mutator((Node *) oldtarget->exprs, + context); + + if (oldtarget->sortgrouprefs) + { + Size nbytes = list_length(oldtarget->exprs) * sizeof(Index); + + newtarget->sortgrouprefs = (Index *) palloc(nbytes); + memcpy(newtarget->sortgrouprefs, oldtarget->sortgrouprefs, nbytes); + } + + return (Node *) newtarget; + } + /* * NOTE: we do not need to recurse into sublinks, because they should * already have been converted to subplans before we see them. @@ -757,8 +813,7 @@ find_appinfos_by_relids(PlannerInfo *root, Relids relids, int *nappinfos) int i; /* Allocate an array that's certainly big enough */ - appinfos = (AppendRelInfo **) - palloc(sizeof(AppendRelInfo *) * bms_num_members(relids)); + appinfos = palloc_array(AppendRelInfo *, bms_num_members(relids)); i = -1; while ((i = bms_next_member(relids, i)) >= 0) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 26a3e0500866c..fcf6d7fff2ae4 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -3,7 +3,7 @@ * clauses.c * routines to manipulate qualification clauses * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -20,6 +20,9 @@ #include "postgres.h" #include "access/htup_details.h" +#include "access/table.h" +#include "catalog/pg_class.h" +#include "catalog/pg_inherits.h" #include "catalog/pg_language.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" @@ -36,6 +39,7 @@ #include "optimizer/clauses.h" #include "optimizer/cost.h" #include "optimizer/optimizer.h" +#include "optimizer/pathnode.h" #include "optimizer/plancat.h" #include "optimizer/planmain.h" #include "parser/analyze.h" @@ -43,6 +47,7 @@ #include "parser/parse_collate.h" #include "parser/parse_func.h" #include "parser/parse_oper.h" +#include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "tcop/tcopprot.h" @@ -55,6 +60,7 @@ #include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/rel.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -79,7 +85,7 @@ typedef struct int nargs; List *args; int sublevels_up; -} substitute_actual_srf_parameters_context; +} substitute_actual_parameters_in_from_context; typedef struct { @@ -109,6 +115,7 @@ static bool contain_context_dependent_node_walker(Node *node, int *flags); static bool contain_leaked_vars_walker(Node *node, void *context); static Relids find_nonnullable_rels_walker(Node *node, bool top_level); static List *find_nonnullable_vars_walker(Node *node, bool top_level); +static void find_subquery_safe_quals(Node *jtnode, List **safe_quals); static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK); static bool convert_saop_to_hashed_saop_walker(Node *node, void *context); static Node *eval_const_expressions_mutator(Node *node, @@ -128,6 +135,8 @@ static Expr *simplify_function(Oid funcid, Oid result_collid, Oid input_collid, List **args_p, bool funcvariadic, bool process_args, bool allow_non_const, eval_const_expressions_context *context); +static Node *simplify_aggref(Aggref *aggref, + eval_const_expressions_context *context); static List *reorder_function_arguments(List *args, int pronargs, HeapTuple func_tuple); static List *add_function_defaults(List *args, int pronargs, @@ -151,10 +160,16 @@ static Node *substitute_actual_parameters(Node *expr, int nargs, List *args, static Node *substitute_actual_parameters_mutator(Node *node, substitute_actual_parameters_context *context); static void sql_inline_error_callback(void *arg); -static Query *substitute_actual_srf_parameters(Query *expr, - int nargs, List *args); -static Node *substitute_actual_srf_parameters_mutator(Node *node, - substitute_actual_srf_parameters_context *context); +static Query *inline_sql_function_in_from(PlannerInfo *root, + RangeTblFunction *rtfunc, + FuncExpr *fexpr, + HeapTuple func_tuple, + Form_pg_proc funcform, + const char *src); +static Query *substitute_actual_parameters_in_from(Query *expr, + int nargs, List *args); +static Node *substitute_actual_parameters_in_from_mutator(Node *node, + substitute_actual_parameters_in_from_context *context); static bool pull_paramids_walker(Node *node, Bitmapset **context); @@ -228,7 +243,7 @@ contain_window_function(Node *clause) WindowFuncLists * find_window_functions(Node *clause, Index maxWinRef) { - WindowFuncLists *lists = palloc(sizeof(WindowFuncLists)); + WindowFuncLists *lists = palloc_object(WindowFuncLists); lists->numWindowFuncs = 0; lists->maxWinRef = maxWinRef; @@ -250,13 +265,10 @@ find_window_functions_walker(Node *node, WindowFuncLists *lists) if (wfunc->winref > lists->maxWinRef) elog(ERROR, "WindowFunc contains out-of-range winref %u", wfunc->winref); - /* eliminate duplicates, so that we avoid repeated computation */ - if (!list_member(lists->windowFuncs[wfunc->winref], wfunc)) - { - lists->windowFuncs[wfunc->winref] = - lappend(lists->windowFuncs[wfunc->winref], wfunc); - lists->numWindowFuncs++; - } + + lists->windowFuncs[wfunc->winref] = + lappend(lists->windowFuncs[wfunc->winref], wfunc); + lists->numWindowFuncs++; /* * We assume that the parser checked that there are no window @@ -1112,6 +1124,8 @@ contain_nonstrict_functions_walker(Node *node, void *context) return true; if (IsA(node, BooleanTest)) return true; + if (IsA(node, JsonConstructorExpr)) + return true; /* Check other function-containing nodes */ if (check_functions_in_node(node, contain_nonstrict_functions_checker, @@ -1423,6 +1437,10 @@ contain_leaked_vars_walker(Node *node, void *context) context); } +/***************************************************************************** + * Nullability analysis + *****************************************************************************/ + /* * find_nonnullable_rels * Determine which base rels are forced nonnullable by given clause. @@ -1537,7 +1555,7 @@ find_nonnullable_rels_walker(Node *node, bool top_level) * the intersection of the sets of nonnullable rels, just as * for OR. Fall through to share code. */ - /* FALL THRU */ + pg_fallthrough; case OR_EXPR: /* @@ -1691,7 +1709,7 @@ find_nonnullable_rels_walker(Node *node, bool top_level) * but here we assume that the input is a Boolean expression, and wish to * see if NULL inputs will provably cause a FALSE-or-NULL result. We expect * the expression to have been AND/OR flattened and converted to implicit-AND - * format. + * format (but the results are still good if it wasn't AND/OR flattened). * * Attnos of the identified Vars are returned in a multibitmapset (a List of * Bitmapsets). List indexes correspond to relids (varnos), while the per-rel @@ -1795,7 +1813,7 @@ find_nonnullable_vars_walker(Node *node, bool top_level) * the intersection of the sets of nonnullable vars, just as * for OR. Fall through to share code. */ - /* FALL THRU */ + pg_fallthrough; case OR_EXPR: /* @@ -2011,6 +2029,231 @@ find_forced_null_var(Node *node) return NULL; } +/* + * query_outputs_are_not_nullable + * Returns TRUE if the output values of the Query are certainly not NULL. + * All output columns must return non-NULL to answer TRUE. + * + * The reason this takes a Query, and not just an individual tlist expression, + * is so that we can make use of the query's WHERE/ON clauses to prove it does + * not return nulls. + * + * In current usage, the passed sub-Query hasn't yet been through any planner + * processing. This means that applying find_nonnullable_vars() to its WHERE + * clauses isn't really ideal: for lack of const-simplification, we might be + * unable to prove not-nullness in some cases where we could have proved it + * afterwards. However, we should not get any false positive results. + * + * Like the other forms of nullability analysis above, we can err on the + * side of conservatism: if we're not sure, it's okay to return FALSE. + */ +bool +query_outputs_are_not_nullable(Query *query) +{ + PlannerInfo subroot; + List *safe_quals = NIL; + List *nonnullable_vars = NIL; + bool computed_nonnullable_vars = false; + + /* + * If the query contains set operations, punt. The set ops themselves + * couldn't introduce nulls that weren't in their inputs, but the tlist + * present in the top-level query is just dummy and won't give us useful + * info. We could get an answer by recursing to examine each leaf query, + * but for the moment it doesn't seem worth the extra complication. + */ + if (query->setOperations) + return false; + + /* + * If the query contains grouping sets, punt. Grouping sets can introduce + * NULL values, and we currently lack the PlannerInfo needed to flatten + * grouping Vars in the query's outputs. + */ + if (query->groupingSets) + return false; + + /* + * We need a PlannerInfo to pass to expr_is_nonnullable. Fortunately, we + * can cons up an entirely dummy one, because only the "parse" link in the + * struct is used by expr_is_nonnullable. + */ + MemSet(&subroot, 0, sizeof(subroot)); + subroot.parse = query; + + /* + * Examine each targetlist entry to prove that it can't produce NULL. + */ + foreach_node(TargetEntry, tle, query->targetList) + { + Expr *expr = tle->expr; + + /* Resjunk columns can be ignored: they don't produce output values */ + if (tle->resjunk) + continue; + + /* + * Look through binary relabelings, since we know those don't + * introduce nulls. + */ + while (expr && IsA(expr, RelabelType)) + expr = ((RelabelType *) expr)->arg; + + if (expr == NULL) /* paranoia */ + return false; + + /* + * Since the subquery hasn't yet been through expression + * preprocessing, we must explicitly flatten grouping Vars and join + * alias Vars in the given expression. Note that flatten_group_exprs + * must be applied before flatten_join_alias_vars, as grouping Vars + * can wrap join alias Vars. + * + * We must also apply flatten_join_alias_vars to the quals extracted + * by find_subquery_safe_quals. We do not need to apply + * flatten_group_exprs to these quals, though, because grouping Vars + * cannot appear in jointree quals. + */ + + /* + * We have verified that the query does not contain grouping sets, + * meaning the grouping Vars will not have varnullingrels that need + * preserving, so it's safe to use NULL as the root here. + */ + if (query->hasGroupRTE) + expr = (Expr *) flatten_group_exprs(NULL, query, (Node *) expr); + + /* + * We won't be dealing with arbitrary expressions, so it's safe to use + * NULL as the root, so long as adjust_standard_join_alias_expression + * can handle everything the parser would make as a join alias + * expression. + */ + expr = (Expr *) flatten_join_alias_vars(NULL, query, (Node *) expr); + + /* + * Check to see if the expr cannot be NULL. Since we're on a raw + * parse tree, we need to look up the not-null constraints from the + * system catalogs. + */ + if (expr_is_nonnullable(&subroot, expr, NOTNULL_SOURCE_CATALOG)) + continue; + + if (IsA(expr, Var)) + { + Var *var = (Var *) expr; + + /* + * For a plain Var, even if that didn't work, we can conclude that + * the Var is not nullable if find_nonnullable_vars can find a + * "var IS NOT NULL" or similarly strict condition among the quals + * on non-outerjoined-rels. Compute the list of Vars having such + * quals if we didn't already. + */ + if (!computed_nonnullable_vars) + { + find_subquery_safe_quals((Node *) query->jointree, &safe_quals); + safe_quals = (List *) + flatten_join_alias_vars(NULL, query, (Node *) safe_quals); + nonnullable_vars = find_nonnullable_vars((Node *) safe_quals); + computed_nonnullable_vars = true; + } + + if (!mbms_is_member(var->varno, + var->varattno - FirstLowInvalidHeapAttributeNumber, + nonnullable_vars)) + return false; /* we failed to prove the Var non-null */ + } + else + { + /* Punt otherwise */ + return false; + } + } + + return true; +} + +/* + * find_subquery_safe_quals + * Traverse jointree to locate quals on non-outerjoined-rels. + * + * We locate all WHERE and JOIN/ON quals that constrain the rels that are not + * below the nullable side of any outer join, and add them to the *safe_quals + * list (forming a list with implicit-AND semantics). These quals can be used + * to prove non-nullability of the subquery's outputs. + * + * Top-level caller must initialize *safe_quals to NIL. + */ +static void +find_subquery_safe_quals(Node *jtnode, List **safe_quals) +{ + if (jtnode == NULL) + return; + if (IsA(jtnode, RangeTblRef)) + { + /* Leaf node: nothing to do */ + return; + } + else if (IsA(jtnode, FromExpr)) + { + FromExpr *f = (FromExpr *) jtnode; + + /* All elements of the FROM list are allowable */ + foreach_ptr(Node, child_node, f->fromlist) + find_subquery_safe_quals(child_node, safe_quals); + /* ... and its WHERE quals are too */ + if (f->quals) + *safe_quals = lappend(*safe_quals, f->quals); + } + else if (IsA(jtnode, JoinExpr)) + { + JoinExpr *j = (JoinExpr *) jtnode; + + switch (j->jointype) + { + case JOIN_INNER: + /* visit both children */ + find_subquery_safe_quals(j->larg, safe_quals); + find_subquery_safe_quals(j->rarg, safe_quals); + /* and grab the ON quals too */ + if (j->quals) + *safe_quals = lappend(*safe_quals, j->quals); + break; + + case JOIN_LEFT: + case JOIN_SEMI: + case JOIN_ANTI: + + /* + * Only the left input is possibly non-nullable; furthermore, + * the quals of this join don't constrain the left input. + * Note: we probably can't see SEMI or ANTI joins at this + * point, but if we do, we can treat them like LEFT joins. + */ + find_subquery_safe_quals(j->larg, safe_quals); + break; + + case JOIN_RIGHT: + /* Reverse of the above case */ + find_subquery_safe_quals(j->rarg, safe_quals); + break; + + case JOIN_FULL: + /* Neither side is non-nullable, so stop descending */ + break; + + default: + elog(ERROR, "unrecognized join type: %d", + (int) j->jointype); + break; + } + } + else + elog(ERROR, "unrecognized node type: %d", + (int) nodeTag(jtnode)); +} + /* * Can we treat a ScalarArrayOpExpr as strict? * @@ -2242,7 +2485,8 @@ rowtype_field_matches(Oid rowtypeid, int fieldnum, * only operators and functions that are reasonable to try to execute. * * NOTE: "root" can be passed as NULL if the caller never wants to do any - * Param substitutions nor receive info about inlined functions. + * Param substitutions nor receive info about inlined functions nor reduce + * NullTest for Vars to constant true or constant false. * * NOTE: the planner assumes that this will always flatten nested AND and * OR clauses into N-argument form. See comments in prepqual.c. @@ -2572,6 +2816,7 @@ eval_const_expressions_mutator(Node *node, newexpr->winref = expr->winref; newexpr->winstar = expr->winstar; newexpr->winagg = expr->winagg; + newexpr->ignore_nulls = expr->ignore_nulls; newexpr->location = expr->location; return (Node *) newexpr; @@ -2621,6 +2866,11 @@ eval_const_expressions_mutator(Node *node, newexpr->location = expr->location; return (Node *) newexpr; } + case T_Aggref: + node = ece_generic_processing(node); + if (context->root != NULL) + return simplify_aggref((Aggref *) node, context); + return node; case T_OpExpr: { OpExpr *expr = (OpExpr *) node; @@ -2688,6 +2938,7 @@ eval_const_expressions_mutator(Node *node, bool has_null_input = false; bool all_null_input = true; bool has_nonconst_input = false; + bool has_nullable_nonconst = false; Expr *simple; DistinctExpr *newexpr; @@ -2704,7 +2955,8 @@ eval_const_expressions_mutator(Node *node, /* * We must do our own check for NULLs because DistinctExpr has * different results for NULL input than the underlying - * operator does. + * operator does. We also check if any non-constant input is + * potentially nullable. */ foreach(arg, args) { @@ -2714,12 +2966,25 @@ eval_const_expressions_mutator(Node *node, all_null_input &= ((Const *) lfirst(arg))->constisnull; } else + { has_nonconst_input = true; + all_null_input = false; + + if (!has_nullable_nonconst && + !expr_is_nonnullable(context->root, + (Expr *) lfirst(arg), + NOTNULL_SOURCE_HASHTABLE)) + has_nullable_nonconst = true; + } } - /* all constants? then can optimize this out */ if (!has_nonconst_input) { + /* + * All inputs are constants. We can optimize this out + * completely. + */ + /* all nulls? then not distinct */ if (all_null_input) return makeBoolConst(false, false); @@ -2764,6 +3029,72 @@ eval_const_expressions_mutator(Node *node, return (Node *) csimple; } } + else if (!has_nullable_nonconst) + { + /* + * There are non-constant inputs, but since all of them + * are proven non-nullable, "IS DISTINCT FROM" semantics + * are much simpler. + */ + + OpExpr *eqexpr; + + /* + * If one input is an explicit NULL constant, and the + * other is a non-nullable expression, the result is + * always TRUE. + */ + if (has_null_input) + return makeBoolConst(true, false); + + /* + * Otherwise, both inputs are known non-nullable. In this + * case, "IS DISTINCT FROM" is equivalent to the standard + * inequality operator (usually "<>"). We convert this to + * an OpExpr, which is a more efficient representation for + * the planner. It can enable the use of partial indexes + * and constraint exclusion. Furthermore, if the clause + * is negated (ie, "IS NOT DISTINCT FROM"), the resulting + * "=" operator can allow the planner to use index scans, + * merge joins, hash joins, and EC-based qual deductions. + */ + eqexpr = makeNode(OpExpr); + eqexpr->opno = expr->opno; + eqexpr->opfuncid = expr->opfuncid; + eqexpr->opresulttype = BOOLOID; + eqexpr->opretset = expr->opretset; + eqexpr->opcollid = expr->opcollid; + eqexpr->inputcollid = expr->inputcollid; + eqexpr->args = args; + eqexpr->location = expr->location; + + return eval_const_expressions_mutator(negate_clause((Node *) eqexpr), + context); + } + else if (has_null_input) + { + /* + * One input is a nullable non-constant expression, and + * the other is an explicit NULL constant. We can + * transform this to a NullTest with !argisrow, which is + * much more amenable to optimization. + */ + + NullTest *nt = makeNode(NullTest); + + nt->arg = (Expr *) (IsA(linitial(args), Const) ? + lsecond(args) : linitial(args)); + nt->nulltesttype = IS_NOT_NULL; + + /* + * argisrow = false is correct whether or not arg is + * composite + */ + nt->argisrow = false; + nt->location = expr->location; + + return eval_const_expressions_mutator((Node *) nt, context); + } /* * The expression cannot be simplified any further, so build @@ -3305,10 +3636,10 @@ eval_const_expressions_mutator(Node *node, context); /* - * We can remove null constants from the list. For a - * non-null constant, if it has not been preceded by any - * other non-null-constant expressions then it is the - * result. Otherwise, it's the next argument, but we can + * We can remove null constants from the list. For a + * nonnullable expression, if it has not been preceded by + * any non-null-constant expressions then it is the + * result. Otherwise, it's the next argument, but we can * drop following arguments since they will never be * reached. */ @@ -3321,6 +3652,15 @@ eval_const_expressions_mutator(Node *node, newargs = lappend(newargs, e); break; } + if (expr_is_nonnullable(context->root, (Expr *) e, + NOTNULL_SOURCE_HASHTABLE)) + { + if (newargs == NIL) + return e; /* first expr */ + newargs = lappend(newargs, e); + break; + } + newargs = lappend(newargs, e); } @@ -3333,6 +3673,13 @@ eval_const_expressions_mutator(Node *node, -1, coalesceexpr->coalescecollid); + /* + * If there's exactly one surviving argument, we no longer + * need COALESCE at all: the result is that argument + */ + if (list_length(newargs) == 1) + return (Node *) linitial(newargs); + newcoalesce = makeNode(CoalesceExpr); newcoalesce->coalescetype = coalesceexpr->coalescetype; newcoalesce->coalescecollid = coalesceexpr->coalescecollid; @@ -3493,6 +3840,20 @@ eval_const_expressions_mutator(Node *node, continue; } + /* + * A proven non-nullable field refutes the whole + * NullTest if the test is IS NULL; else we can + * discard it. + */ + if (relem && + expr_is_nonnullable(context->root, (Expr *) relem, + NOTNULL_SOURCE_HASHTABLE)) + { + if (ntest->nulltesttype == IS_NULL) + return makeBoolConst(false, false); + continue; + } + /* * Else, make a scalar (argisrow == false) NullTest * for this field. Scalar semantics are required @@ -3537,6 +3898,29 @@ eval_const_expressions_mutator(Node *node, return makeBoolConst(result, false); } + if (!ntest->argisrow && arg && + expr_is_nonnullable(context->root, (Expr *) arg, + NOTNULL_SOURCE_HASHTABLE)) + { + bool result; + + switch (ntest->nulltesttype) + { + case IS_NULL: + result = false; + break; + case IS_NOT_NULL: + result = true; + break; + default: + elog(ERROR, "unrecognized nulltesttype: %d", + (int) ntest->nulltesttype); + result = false; /* keep compiler quiet */ + break; + } + + return makeBoolConst(result, false); + } newntest = makeNode(NullTest); newntest->arg = (Expr *) arg; @@ -3562,6 +3946,9 @@ eval_const_expressions_mutator(Node *node, context); if (arg && IsA(arg, Const)) { + /* + * If arg is Const, simplify to constant. + */ Const *carg = (Const *) arg; bool result; @@ -3598,6 +3985,36 @@ eval_const_expressions_mutator(Node *node, return makeBoolConst(result, false); } + if (arg && + expr_is_nonnullable(context->root, (Expr *) arg, + NOTNULL_SOURCE_HASHTABLE)) + { + /* + * If arg is proven non-nullable, simplify to boolean + * expression or constant. + */ + switch (btest->booltesttype) + { + case IS_TRUE: + case IS_NOT_FALSE: + return arg; + + case IS_FALSE: + case IS_NOT_TRUE: + return (Node *) make_notclause((Expr *) arg); + + case IS_UNKNOWN: + return makeBoolConst(false, false); + + case IS_NOT_UNKNOWN: + return makeBoolConst(true, false); + + default: + elog(ERROR, "unrecognized booltesttype: %d", + (int) btest->booltesttype); + break; + } + } newbtest = makeNode(BooleanTest); newbtest->arg = (Expr *) arg; @@ -3624,7 +4041,7 @@ eval_const_expressions_mutator(Node *node, arg = eval_const_expressions_mutator((Node *) cdomain->arg, context); if (context->estimate || - !DomainHasConstraints(cdomain->resulttype)) + !DomainHasConstraints(cdomain->resulttype, NULL)) { /* Record dependency, if this isn't estimation mode */ if (context->root && !context->estimate) @@ -4156,73 +4573,391 @@ simplify_function(Oid funcid, Oid result_type, int32 result_typmod, } /* - * expand_function_arguments: convert named-notation args to positional args - * and/or insert default args, as needed + * simplify_aggref + * Call the Aggref.aggfnoid's prosupport function to allow it to + * determine if simplification of the Aggref is possible. Returns the + * newly simplified node if conversion took place; otherwise, returns the + * original Aggref. * - * Returns a possibly-transformed version of the args list. - * - * If include_out_arguments is true, then the args list and the result - * include OUT arguments. - * - * The expected result type of the call must be given, for sanity-checking - * purposes. Also, we ask the caller to provide the function's actual - * pg_proc tuple, not just its OID. - * - * If we need to change anything, the input argument list is copied, not - * modified. - * - * Note: this gets applied to operator argument lists too, even though the - * cases it handles should never occur there. This should be OK since it - * will fall through very quickly if there's nothing to do. + * See SupportRequestSimplifyAggref comments in supportnodes.h for further + * details. */ -List * -expand_function_arguments(List *args, bool include_out_arguments, - Oid result_type, HeapTuple func_tuple) +static Node * +simplify_aggref(Aggref *aggref, eval_const_expressions_context *context) { - Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple); - Oid *proargtypes = funcform->proargtypes.values; - int pronargs = funcform->pronargs; - bool has_named_args = false; - ListCell *lc; + Oid prosupport = get_func_support(aggref->aggfnoid); - /* - * If we are asked to match to OUT arguments, then use the proallargtypes - * array (which includes those); otherwise use proargtypes (which - * doesn't). Of course, if proallargtypes is null, we always use - * proargtypes. (Fetching proallargtypes is annoyingly expensive - * considering that we may have nothing to do here, but fortunately the - * common case is include_out_arguments == false.) - */ - if (include_out_arguments) + if (OidIsValid(prosupport)) { - Datum proallargtypes; - bool isNull; + SupportRequestSimplifyAggref req; + Node *newnode; - proallargtypes = SysCacheGetAttr(PROCOID, func_tuple, - Anum_pg_proc_proallargtypes, - &isNull); - if (!isNull) - { - ArrayType *arr = DatumGetArrayTypeP(proallargtypes); + /* + * Build a SupportRequestSimplifyAggref node to pass to the support + * function. + */ + req.type = T_SupportRequestSimplifyAggref; + req.root = context->root; + req.aggref = aggref; - pronargs = ARR_DIMS(arr)[0]; - if (ARR_NDIM(arr) != 1 || - pronargs < 0 || - ARR_HASNULL(arr) || - ARR_ELEMTYPE(arr) != OIDOID) - elog(ERROR, "proallargtypes is not a 1-D Oid array or it contains nulls"); - Assert(pronargs >= funcform->pronargs); - proargtypes = (Oid *) ARR_DATA_PTR(arr); - } + newnode = (Node *) DatumGetPointer(OidFunctionCall1(prosupport, + PointerGetDatum(&req))); + + /* + * We expect the support function to return either a new Node or NULL + * (when simplification isn't possible). + */ + Assert(newnode != (Node *) aggref || newnode == NULL); + + if (newnode != NULL) + return newnode; } - /* Do we have any named arguments? */ - foreach(lc, args) - { - Node *arg = (Node *) lfirst(lc); + return (Node *) aggref; +} - if (IsA(arg, NamedArgExpr)) - { +/* + * var_is_nonnullable: check to see if the Var cannot be NULL + * + * If the Var is defined NOT NULL and meanwhile is not nulled by any outer + * joins or grouping sets, then we can know that it cannot be NULL. + * + * "source" specifies where we should look for NOT NULL proofs. + */ +bool +var_is_nonnullable(PlannerInfo *root, Var *var, NotNullSource source) +{ + Assert(IsA(var, Var)); + + /* skip upper-level Vars */ + if (var->varlevelsup != 0) + return false; + + /* could the Var be nulled by any outer joins or grouping sets? */ + if (!bms_is_empty(var->varnullingrels)) + return false; + + /* + * If the Var has a non-default returning type, it could be NULL + * regardless of any NOT NULL constraint. For example, OLD.col is NULL + * for INSERT, and NEW.col is NULL for DELETE. + */ + if (var->varreturningtype != VAR_RETURNING_DEFAULT) + return false; + + /* system columns cannot be NULL */ + if (var->varattno < 0) + return true; + + /* we don't trust whole-row Vars */ + if (var->varattno == 0) + return false; + + /* Check if the Var is defined as NOT NULL. */ + switch (source) + { + case NOTNULL_SOURCE_RELOPT: + { + /* + * We retrieve the column NOT NULL constraint information from + * the corresponding RelOptInfo. + */ + RelOptInfo *rel; + Bitmapset *notnullattnums; + + rel = find_base_rel(root, var->varno); + notnullattnums = rel->notnullattnums; + + return bms_is_member(var->varattno, notnullattnums); + } + case NOTNULL_SOURCE_HASHTABLE: + { + /* + * We retrieve the column NOT NULL constraint information from + * the hash table. + */ + RangeTblEntry *rte; + Bitmapset *notnullattnums; + + rte = planner_rt_fetch(var->varno, root); + + /* We can only reason about ordinary relations */ + if (rte->rtekind != RTE_RELATION) + return false; + + /* + * We must skip inheritance parent tables, as some child + * tables may have a NOT NULL constraint for a column while + * others may not. This cannot happen with partitioned + * tables, though. + */ + if (rte->inh && rte->relkind != RELKIND_PARTITIONED_TABLE) + return false; + + notnullattnums = find_relation_notnullatts(root, rte->relid); + + return bms_is_member(var->varattno, notnullattnums); + } + case NOTNULL_SOURCE_CATALOG: + { + /* + * We check the attnullability field in the tuple descriptor. + * This is necessary rather than checking the attnotnull field + * from the attribute relation, because attnotnull is also set + * for invalid (NOT VALID) NOT NULL constraints, which do not + * guarantee the absence of NULLs. + */ + RangeTblEntry *rte; + Relation rel; + CompactAttribute *attr; + bool result; + + rte = planner_rt_fetch(var->varno, root); + + /* We can only reason about ordinary relations */ + if (rte->rtekind != RTE_RELATION) + return false; + + /* + * We must skip inheritance parent tables, as some child + * tables may have a NOT NULL constraint for a column while + * others may not. This cannot happen with partitioned + * tables, though. + * + * Note that we need to check if the relation actually has any + * children, as we might not have done that yet. + */ + if (rte->inh && has_subclass(rte->relid) && + rte->relkind != RELKIND_PARTITIONED_TABLE) + return false; + + /* We need not lock the relation since it was already locked */ + rel = table_open(rte->relid, NoLock); + attr = TupleDescCompactAttr(RelationGetDescr(rel), + var->varattno - 1); + result = (attr->attnullability == ATTNULLABLE_VALID); + table_close(rel, NoLock); + + return result; + } + default: + elog(ERROR, "unrecognized NotNullSource: %d", + (int) source); + break; + } + + return false; +} + +/* + * expr_is_nonnullable: check to see if the Expr cannot be NULL + * + * Returns true iff the given 'expr' cannot produce SQL NULLs. + * + * source: specifies where we should look for NOT NULL proofs for Vars. + * - NOTNULL_SOURCE_RELOPT: Used when RelOptInfos have been generated. We + * retrieve nullability information directly from the RelOptInfo corresponding + * to the Var. + * - NOTNULL_SOURCE_HASHTABLE: Used when RelOptInfos are not yet available, + * but we have already collected relation-level not-null constraints into the + * global hash table. + * - NOTNULL_SOURCE_CATALOG: Used for raw parse trees where neither + * RelOptInfos nor the hash table are available. In this case, we check the + * column's attnullability in the tuple descriptor. + * + * For now, we support only a limited set of expression types. Support for + * additional node types can be added in the future. + */ +bool +expr_is_nonnullable(PlannerInfo *root, Expr *expr, NotNullSource source) +{ + /* since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + switch (nodeTag(expr)) + { + case T_Var: + { + if (root) + return var_is_nonnullable(root, (Var *) expr, source); + } + break; + case T_Const: + return !((Const *) expr)->constisnull; + case T_CoalesceExpr: + { + /* + * A CoalesceExpr returns NULL if and only if all its + * arguments are NULL. Therefore, we can determine that a + * CoalesceExpr cannot be NULL if at least one of its + * arguments can be proven non-nullable. + */ + CoalesceExpr *coalesceexpr = (CoalesceExpr *) expr; + + foreach_ptr(Expr, arg, coalesceexpr->args) + { + if (expr_is_nonnullable(root, arg, source)) + return true; + } + } + break; + case T_MinMaxExpr: + { + /* + * Like CoalesceExpr, a MinMaxExpr returns NULL only if all + * its arguments evaluate to NULL. + */ + MinMaxExpr *minmaxexpr = (MinMaxExpr *) expr; + + foreach_ptr(Expr, arg, minmaxexpr->args) + { + if (expr_is_nonnullable(root, arg, source)) + return true; + } + } + break; + case T_CaseExpr: + { + /* + * A CASE expression is non-nullable if all branch results are + * non-nullable. We must also verify that the default result + * (ELSE) exists and is non-nullable. + */ + CaseExpr *caseexpr = (CaseExpr *) expr; + + /* The default result must be present and non-nullable */ + if (caseexpr->defresult == NULL || + !expr_is_nonnullable(root, caseexpr->defresult, source)) + return false; + + /* All branch results must be non-nullable */ + foreach_ptr(CaseWhen, casewhen, caseexpr->args) + { + if (!expr_is_nonnullable(root, casewhen->result, source)) + return false; + } + + return true; + } + break; + case T_ArrayExpr: + { + /* + * An ARRAY[] expression always returns a valid Array object, + * even if it is empty (ARRAY[]) or contains NULLs + * (ARRAY[NULL]). It never evaluates to a SQL NULL. + */ + return true; + } + case T_NullTest: + { + /* + * An IS NULL / IS NOT NULL expression always returns a + * boolean value. It never returns SQL NULL. + */ + return true; + } + case T_BooleanTest: + { + /* + * A BooleanTest expression always evaluates to a boolean + * value. It never returns SQL NULL. + */ + return true; + } + case T_DistinctExpr: + { + /* + * IS DISTINCT FROM never returns NULL, effectively acting as + * though NULL were a normal data value. + */ + return true; + } + case T_RelabelType: + { + /* + * RelabelType does not change the nullability of the data. + * The result is non-nullable if and only if the argument is + * non-nullable. + */ + return expr_is_nonnullable(root, ((RelabelType *) expr)->arg, + source); + } + default: + break; + } + + return false; +} + +/* + * expand_function_arguments: convert named-notation args to positional args + * and/or insert default args, as needed + * + * Returns a possibly-transformed version of the args list. + * + * If include_out_arguments is true, then the args list and the result + * include OUT arguments. + * + * The expected result type of the call must be given, for sanity-checking + * purposes. Also, we ask the caller to provide the function's actual + * pg_proc tuple, not just its OID. + * + * If we need to change anything, the input argument list is copied, not + * modified. + * + * Note: this gets applied to operator argument lists too, even though the + * cases it handles should never occur there. This should be OK since it + * will fall through very quickly if there's nothing to do. + */ +List * +expand_function_arguments(List *args, bool include_out_arguments, + Oid result_type, HeapTuple func_tuple) +{ + Form_pg_proc funcform = (Form_pg_proc) GETSTRUCT(func_tuple); + Oid *proargtypes = funcform->proargtypes.values; + int pronargs = funcform->pronargs; + bool has_named_args = false; + ListCell *lc; + + /* + * If we are asked to match to OUT arguments, then use the proallargtypes + * array (which includes those); otherwise use proargtypes (which + * doesn't). Of course, if proallargtypes is null, we always use + * proargtypes. (Fetching proallargtypes is annoyingly expensive + * considering that we may have nothing to do here, but fortunately the + * common case is include_out_arguments == false.) + */ + if (include_out_arguments) + { + Datum proallargtypes; + bool isNull; + + proallargtypes = SysCacheGetAttr(PROCOID, func_tuple, + Anum_pg_proc_proallargtypes, + &isNull); + if (!isNull) + { + ArrayType *arr = DatumGetArrayTypeP(proallargtypes); + + pronargs = ARR_DIMS(arr)[0]; + if (ARR_NDIM(arr) != 1 || + pronargs < 0 || + ARR_HASNULL(arr) || + ARR_ELEMTYPE(arr) != OIDOID) + elog(ERROR, "proallargtypes is not a 1-D Oid array or it contains nulls"); + Assert(pronargs >= funcform->pronargs); + proargtypes = (Oid *) ARR_DATA_PTR(arr); + } + } + + /* Do we have any named arguments? */ + foreach(lc, args) + { + Node *arg = (Node *) lfirst(lc); + + if (IsA(arg, NamedArgExpr)) + { has_named_args = true; break; } @@ -5049,50 +5784,42 @@ evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod, /* - * inline_set_returning_function - * Attempt to "inline" a set-returning function in the FROM clause. + * inline_function_in_from + * Attempt to "inline" a function in the FROM clause. * * "rte" is an RTE_FUNCTION rangetable entry. If it represents a call of a - * set-returning SQL function that can safely be inlined, expand the function - * and return the substitute Query structure. Otherwise, return NULL. + * function that can be inlined, expand the function and return the + * substitute Query structure. Otherwise, return NULL. * * We assume that the RTE's expression has already been put through * eval_const_expressions(), which among other things will take care of * default arguments and named-argument notation. * * This has a good deal of similarity to inline_function(), but that's - * for the non-set-returning case, and there are enough differences to + * for the general-expression case, and there are enough differences to * justify separate functions. */ Query * -inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) +inline_function_in_from(PlannerInfo *root, RangeTblEntry *rte) { RangeTblFunction *rtfunc; FuncExpr *fexpr; Oid func_oid; HeapTuple func_tuple; Form_pg_proc funcform; - char *src; - Datum tmp; - bool isNull; MemoryContext oldcxt; MemoryContext mycxt; + Datum tmp; + char *src; inline_error_callback_arg callback_arg; ErrorContextCallback sqlerrcontext; - SQLFunctionParseInfoPtr pinfo; - TypeFuncClass functypclass; - TupleDesc rettupdesc; - List *raw_parsetree_list; - List *querytree_list; - Query *querytree; + Query *querytree = NULL; Assert(rte->rtekind == RTE_FUNCTION); /* - * It doesn't make a lot of sense for a SQL SRF to refer to itself in its - * own FROM clause, since that must cause infinite recursion at runtime. - * It will cause this code to recurse too, so check for stack overflow. - * (There's no need to do more.) + * Guard against infinite recursion during expansion by checking for stack + * overflow. (There's no need to do more.) */ check_stack_depth(); @@ -5111,14 +5838,6 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) func_oid = fexpr->funcid; - /* - * The function must be declared to return a set, else inlining would - * change the results if the contained SELECT didn't return exactly one - * row. - */ - if (!fexpr->funcretset) - return NULL; - /* * Refuse to inline if the arguments contain any volatile functions or * sub-selects. Volatile functions are rejected because inlining may @@ -5149,24 +5868,10 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) funcform = (Form_pg_proc) GETSTRUCT(func_tuple); /* - * Forget it if the function is not SQL-language or has other showstopper - * properties. In particular it mustn't be declared STRICT, since we - * couldn't enforce that. It also mustn't be VOLATILE, because that is - * supposed to cause it to be executed with its own snapshot, rather than - * sharing the snapshot of the calling query. We also disallow returning - * SETOF VOID, because inlining would result in exposing the actual result - * of the function's last SELECT, which should not happen in that case. - * (Rechecking prokind, proretset, and pronargs is just paranoia.) + * If the function SETs any configuration parameters, inlining would cause + * us to miss making those changes. */ - if (funcform->prolang != SQLlanguageId || - funcform->prokind != PROKIND_FUNCTION || - funcform->proisstrict || - funcform->provolatile == PROVOLATILE_VOLATILE || - funcform->prorettype == VOIDOID || - funcform->prosecdef || - !funcform->proretset || - list_length(fexpr->args) != funcform->pronargs || - !heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL)) + if (!heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL)) { ReleaseSysCache(func_tuple); return NULL; @@ -5174,10 +5879,11 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) /* * Make a temporary memory context, so that we don't leak all the stuff - * that parsing might create. + * that parsing and rewriting might create. If we succeed, we'll copy + * just the finished query tree back up to the caller's context. */ mycxt = AllocSetContextCreate(CurrentMemoryContext, - "inline_set_returning_function", + "inline_function_in_from", ALLOCSET_DEFAULT_SIZES); oldcxt = MemoryContextSwitchTo(mycxt); @@ -5185,9 +5891,30 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) tmp = SysCacheGetAttrNotNull(PROCOID, func_tuple, Anum_pg_proc_prosrc); src = TextDatumGetCString(tmp); + /* + * If the function has an attached support function that can handle + * SupportRequestInlineInFrom, then attempt to inline with that. + */ + if (funcform->prosupport) + { + SupportRequestInlineInFrom req; + + req.type = T_SupportRequestInlineInFrom; + req.root = root; + req.rtfunc = rtfunc; + req.proc = func_tuple; + + querytree = (Query *) + DatumGetPointer(OidFunctionCall1(funcform->prosupport, + PointerGetDatum(&req))); + } + /* * Setup error traceback support for ereport(). This is so that we can - * finger the function that bad information came from. + * finger the function that bad information came from. We don't install + * this while running the support function, since it'd be likely to do the + * wrong thing: any parse errors reported during that are very likely not + * against the raw function source text. */ callback_arg.proname = NameStr(funcform->proname); callback_arg.prosrc = src; @@ -5197,33 +5924,158 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) sqlerrcontext.previous = error_context_stack; error_context_stack = &sqlerrcontext; + /* + * If SupportRequestInlineInFrom didn't work, try our built-in inlining + * mechanism. + */ + if (!querytree) + querytree = inline_sql_function_in_from(root, rtfunc, fexpr, + func_tuple, funcform, src); + + if (!querytree) + goto fail; /* no luck there either, fail */ + + /* + * The result had better be a SELECT Query. + */ + Assert(IsA(querytree, Query)); + Assert(querytree->commandType == CMD_SELECT); + + /* + * Looks good --- substitute parameters into the query. + */ + querytree = substitute_actual_parameters_in_from(querytree, + funcform->pronargs, + fexpr->args); + + /* + * Copy the modified query out of the temporary memory context, and clean + * up. + */ + MemoryContextSwitchTo(oldcxt); + + querytree = copyObject(querytree); + + MemoryContextDelete(mycxt); + error_context_stack = sqlerrcontext.previous; + ReleaseSysCache(func_tuple); + + /* + * We don't have to fix collations here because the upper query is already + * parsed, ie, the collations in the RTE are what count. + */ + + /* + * Since there is now no trace of the function in the plan tree, we must + * explicitly record the plan's dependency on the function. + */ + record_plan_function_dependency(root, func_oid); + + /* + * We must also notice if the inserted query adds a dependency on the + * calling role due to RLS quals. + */ + if (querytree->hasRowSecurity) + root->glob->dependsOnRole = true; + + return querytree; + + /* Here if func is not inlinable: release temp memory and return NULL */ +fail: + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(mycxt); + error_context_stack = sqlerrcontext.previous; + ReleaseSysCache(func_tuple); + + return NULL; +} + +/* + * inline_sql_function_in_from + * + * This implements inline_function_in_from for SQL-language functions. + * Returns NULL if the function couldn't be inlined. + * + * The division of labor between here and inline_function_in_from is based + * on the rule that inline_function_in_from should make all checks that are + * certain to be required in both this case and the support-function case. + * Support functions might also want to make checks analogous to the ones + * made here, but then again they might not, or they might just assume that + * the function they are attached to can validly be inlined. + */ +static Query * +inline_sql_function_in_from(PlannerInfo *root, + RangeTblFunction *rtfunc, + FuncExpr *fexpr, + HeapTuple func_tuple, + Form_pg_proc funcform, + const char *src) +{ + Datum sqlbody; + bool isNull; + List *querytree_list; + Query *querytree; + TypeFuncClass functypclass; + TupleDesc rettupdesc; + + /* + * The function must be declared to return a set, else inlining would + * change the results if the contained SELECT didn't return exactly one + * row. + */ + if (!fexpr->funcretset) + return NULL; + + /* + * Forget it if the function is not SQL-language or has other showstopper + * properties. In particular it mustn't be declared STRICT, since we + * couldn't enforce that. It also mustn't be VOLATILE, because that is + * supposed to cause it to be executed with its own snapshot, rather than + * sharing the snapshot of the calling query. We also disallow returning + * SETOF VOID, because inlining would result in exposing the actual result + * of the function's last SELECT, which should not happen in that case. + * (Rechecking prokind, proretset, and pronargs is just paranoia.) + */ + if (funcform->prolang != SQLlanguageId || + funcform->prokind != PROKIND_FUNCTION || + funcform->proisstrict || + funcform->provolatile == PROVOLATILE_VOLATILE || + funcform->prorettype == VOIDOID || + funcform->prosecdef || + !funcform->proretset || + list_length(fexpr->args) != funcform->pronargs) + return NULL; + /* If we have prosqlbody, pay attention to that not prosrc */ - tmp = SysCacheGetAttr(PROCOID, - func_tuple, - Anum_pg_proc_prosqlbody, - &isNull); + sqlbody = SysCacheGetAttr(PROCOID, + func_tuple, + Anum_pg_proc_prosqlbody, + &isNull); if (!isNull) { Node *n; - n = stringToNode(TextDatumGetCString(tmp)); + n = stringToNode(TextDatumGetCString(sqlbody)); if (IsA(n, List)) querytree_list = linitial_node(List, castNode(List, n)); else querytree_list = list_make1(n); if (list_length(querytree_list) != 1) - goto fail; + return NULL; querytree = linitial(querytree_list); /* Acquire necessary locks, then apply rewriter. */ AcquireRewriteLocks(querytree, true, false); querytree_list = pg_rewrite_query(querytree); if (list_length(querytree_list) != 1) - goto fail; + return NULL; querytree = linitial(querytree_list); } else { + SQLFunctionParseInfoPtr pinfo; + List *raw_parsetree_list; + /* * Set up to handle parameters while parsing the function body. We * can use the FuncExpr just created as the input for @@ -5240,14 +6092,14 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) */ raw_parsetree_list = pg_parse_query(src); if (list_length(raw_parsetree_list) != 1) - goto fail; + return NULL; querytree_list = pg_analyze_and_rewrite_withcb(linitial(raw_parsetree_list), src, (ParserSetupHook) sql_fn_parser_setup, pinfo, NULL); if (list_length(querytree_list) != 1) - goto fail; + return NULL; querytree = linitial(querytree_list); } @@ -5272,7 +6124,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) */ if (!IsA(querytree, Query) || querytree->commandType != CMD_SELECT) - goto fail; + return NULL; /* * Make sure the function (still) returns what it's declared to. This @@ -5294,7 +6146,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) (functypclass == TYPEFUNC_COMPOSITE || functypclass == TYPEFUNC_COMPOSITE_DOMAIN || functypclass == TYPEFUNC_RECORD)) - goto fail; /* reject not-whole-tuple-result cases */ + return NULL; /* reject not-whole-tuple-result cases */ /* * check_sql_fn_retval might've inserted a projection step, but that's @@ -5302,53 +6154,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) */ querytree = linitial_node(Query, querytree_list); - /* - * Looks good --- substitute parameters into the query. - */ - querytree = substitute_actual_srf_parameters(querytree, - funcform->pronargs, - fexpr->args); - - /* - * Copy the modified query out of the temporary memory context, and clean - * up. - */ - MemoryContextSwitchTo(oldcxt); - - querytree = copyObject(querytree); - - MemoryContextDelete(mycxt); - error_context_stack = sqlerrcontext.previous; - ReleaseSysCache(func_tuple); - - /* - * We don't have to fix collations here because the upper query is already - * parsed, ie, the collations in the RTE are what count. - */ - - /* - * Since there is now no trace of the function in the plan tree, we must - * explicitly record the plan's dependency on the function. - */ - record_plan_function_dependency(root, func_oid); - - /* - * We must also notice if the inserted query adds a dependency on the - * calling role due to RLS quals. - */ - if (querytree->hasRowSecurity) - root->glob->dependsOnRole = true; - return querytree; - - /* Here if func is not inlinable: release temp memory and return NULL */ -fail: - MemoryContextSwitchTo(oldcxt); - MemoryContextDelete(mycxt); - error_context_stack = sqlerrcontext.previous; - ReleaseSysCache(func_tuple); - - return NULL; } /* @@ -5358,23 +6164,23 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte) * that it needs its own code. */ static Query * -substitute_actual_srf_parameters(Query *expr, int nargs, List *args) +substitute_actual_parameters_in_from(Query *expr, int nargs, List *args) { - substitute_actual_srf_parameters_context context; + substitute_actual_parameters_in_from_context context; context.nargs = nargs; context.args = args; context.sublevels_up = 1; return query_tree_mutator(expr, - substitute_actual_srf_parameters_mutator, + substitute_actual_parameters_in_from_mutator, &context, 0); } static Node * -substitute_actual_srf_parameters_mutator(Node *node, - substitute_actual_srf_parameters_context *context) +substitute_actual_parameters_in_from_mutator(Node *node, + substitute_actual_parameters_in_from_context *context) { Node *result; @@ -5384,7 +6190,7 @@ substitute_actual_srf_parameters_mutator(Node *node, { context->sublevels_up++; result = (Node *) query_tree_mutator((Query *) node, - substitute_actual_srf_parameters_mutator, + substitute_actual_parameters_in_from_mutator, context, 0); context->sublevels_up--; @@ -5409,7 +6215,7 @@ substitute_actual_srf_parameters_mutator(Node *node, } } return expression_tree_mutator(node, - substitute_actual_srf_parameters_mutator, + substitute_actual_parameters_in_from_mutator, context); } @@ -5492,8 +6298,8 @@ make_SAOP_expr(Oid oper, Node *leftexpr, Oid coltype, Oid arraycollid, get_typlenbyvalalign(coltype, &typlen, &typbyval, &typalign); - elems = (Datum *) palloc(sizeof(Datum) * list_length(exprs)); - nulls = (bool *) palloc(sizeof(bool) * list_length(exprs)); + elems = palloc_array(Datum, list_length(exprs)); + nulls = palloc_array(bool, list_length(exprs)); foreach_node(Const, value, exprs) { elems[i] = value->constvalue; diff --git a/src/backend/optimizer/util/extendplan.c b/src/backend/optimizer/util/extendplan.c new file mode 100644 index 0000000000000..40f37f0c8ded9 --- /dev/null +++ b/src/backend/optimizer/util/extendplan.c @@ -0,0 +1,177 @@ +/*------------------------------------------------------------------------- + * + * extendplan.c + * Extend core planner objects with additional private state + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994-5, Regents of the University of California + * + * The interfaces defined in this file make it possible for loadable + * modules to store their own private state inside of key planner data + * structures -- specifically, the PlannerGlobal, PlannerInfo, and + * RelOptInfo structures. This can make it much easier to write + * reasonably efficient planner extensions; for instance, code that + * uses set_join_pathlist_hook can arrange to compute a key intermediate + * result once per joinrel rather than on every call. + * + * IDENTIFICATION + * src/backend/optimizer/util/extendplan.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "optimizer/extendplan.h" +#include "port/pg_bitutils.h" +#include "utils/memutils.h" + +static const char **PlannerExtensionNameArray = NULL; +static int PlannerExtensionNamesAssigned = 0; +static int PlannerExtensionNamesAllocated = 0; + +/* + * Map the name of a planner extension to an integer ID. + * + * Within the lifetime of a particular backend, the same name will be mapped + * to the same ID every time. IDs are not stable across backends. Use the ID + * that you get from this function to call the remaining functions in this + * file. + */ +int +GetPlannerExtensionId(const char *extension_name) +{ + /* Search for an existing extension by this name; if found, return ID. */ + for (int i = 0; i < PlannerExtensionNamesAssigned; ++i) + if (strcmp(PlannerExtensionNameArray[i], extension_name) == 0) + return i; + + /* If there is no array yet, create one. */ + if (PlannerExtensionNameArray == NULL) + { + PlannerExtensionNamesAllocated = 16; + PlannerExtensionNameArray = (const char **) + MemoryContextAlloc(TopMemoryContext, + PlannerExtensionNamesAllocated + * sizeof(char *)); + } + + /* If there's an array but it's currently full, expand it. */ + if (PlannerExtensionNamesAssigned >= PlannerExtensionNamesAllocated) + { + int i = pg_nextpower2_32(PlannerExtensionNamesAssigned + 1); + + PlannerExtensionNameArray = (const char **) + repalloc(PlannerExtensionNameArray, i * sizeof(char *)); + PlannerExtensionNamesAllocated = i; + } + + /* Assign and return new ID. */ + PlannerExtensionNameArray[PlannerExtensionNamesAssigned] = extension_name; + return PlannerExtensionNamesAssigned++; +} + +/* + * Store extension-specific state into a PlannerGlobal. + */ +void +SetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id, + void *opaque) +{ + Assert(extension_id >= 0); + + /* If there is no array yet, create one. */ + if (glob->extension_state == NULL) + { + MemoryContext planner_cxt; + Size sz; + + planner_cxt = GetMemoryChunkContext(glob); + glob->extension_state_allocated = + Max(4, pg_nextpower2_32(extension_id + 1)); + sz = glob->extension_state_allocated * sizeof(void *); + glob->extension_state = MemoryContextAllocZero(planner_cxt, sz); + } + + /* If there's an array but it's currently full, expand it. */ + if (extension_id >= glob->extension_state_allocated) + { + int i; + + i = pg_nextpower2_32(extension_id + 1); + glob->extension_state = repalloc0_array(glob->extension_state, void *, + glob->extension_state_allocated, i); + glob->extension_state_allocated = i; + } + + glob->extension_state[extension_id] = opaque; +} + +/* + * Store extension-specific state into a PlannerInfo. + */ +void +SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id, + void *opaque) +{ + Assert(extension_id >= 0); + + /* If there is no array yet, create one. */ + if (root->extension_state == NULL) + { + Size sz; + + root->extension_state_allocated = + Max(4, pg_nextpower2_32(extension_id + 1)); + sz = root->extension_state_allocated * sizeof(void *); + root->extension_state = MemoryContextAllocZero(root->planner_cxt, sz); + } + + /* If there's an array but it's currently full, expand it. */ + if (extension_id >= root->extension_state_allocated) + { + int i; + + i = pg_nextpower2_32(extension_id + 1); + root->extension_state = repalloc0_array(root->extension_state, void *, + root->extension_state_allocated, i); + root->extension_state_allocated = i; + } + + root->extension_state[extension_id] = opaque; +} + +/* + * Store extension-specific state into a RelOptInfo. + */ +void +SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id, + void *opaque) +{ + Assert(extension_id >= 0); + + /* If there is no array yet, create one. */ + if (rel->extension_state == NULL) + { + MemoryContext planner_cxt; + Size sz; + + planner_cxt = GetMemoryChunkContext(rel); + rel->extension_state_allocated = + Max(4, pg_nextpower2_32(extension_id + 1)); + sz = rel->extension_state_allocated * sizeof(void *); + rel->extension_state = MemoryContextAllocZero(planner_cxt, sz); + } + + /* If there's an array but it's currently full, expand it. */ + if (extension_id >= rel->extension_state_allocated) + { + int i; + + i = pg_nextpower2_32(extension_id + 1); + rel->extension_state = repalloc0_array(rel->extension_state, void *, + rel->extension_state_allocated, i); + rel->extension_state_allocated = i; + } + + rel->extension_state[extension_id] = opaque; +} diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c index 17e51cd75d744..6a7b9edff3fce 100644 --- a/src/backend/optimizer/util/inherit.c +++ b/src/backend/optimizer/util/inherit.c @@ -3,7 +3,7 @@ * inherit.c * Routines to process child relations in inheritance trees * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,8 @@ */ #include "postgres.h" +#include + #include "access/sysattr.h" #include "access/table.h" #include "catalog/partition.h" @@ -322,7 +324,6 @@ expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo *relinfo, PlanRowMark *top_parentrc, LOCKMODE lockmode) { PartitionDesc partdesc; - Bitmapset *live_parts; int num_live_parts; int i; @@ -336,16 +337,6 @@ expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo *relinfo, /* A partitioned table should always have a partition descriptor. */ Assert(partdesc); - /* - * Note down whether any partition key cols are being updated. Though it's - * the root partitioned table's updatedCols we are interested in, - * parent_updatedCols provided by the caller contains the root partrel's - * updatedCols translated to match the attribute ordering of parentrel. - */ - if (!root->partColsUpdated) - root->partColsUpdated = - has_partition_attrs(parentrel, parent_updatedCols, NULL); - /* Nothing further to do here if there are no partitions. */ if (partdesc->nparts == 0) return; @@ -356,10 +347,10 @@ expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo *relinfo, * that survive pruning. Below, we will initialize child objects for the * surviving partitions. */ - relinfo->live_parts = live_parts = prune_append_rel_partitions(relinfo); + relinfo->live_parts = prune_append_rel_partitions(relinfo); /* Expand simple_rel_array and friends to hold child objects. */ - num_live_parts = bms_num_members(live_parts); + num_live_parts = bms_num_members(relinfo->live_parts); if (num_live_parts > 0) expand_planner_arrays(root, num_live_parts); @@ -378,7 +369,7 @@ expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo *relinfo, * table itself, because it's not going to be scanned. */ i = -1; - while ((i = bms_next_member(live_parts, i)) >= 0) + while ((i = bms_next_member(relinfo->live_parts, i)) >= 0) { Oid childOID = partdesc->oids[i]; Relation childrel; @@ -466,8 +457,7 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte, Index *childRTindex_p) { Query *parse = root->parse; - Oid parentOID PG_USED_FOR_ASSERTS_ONLY = - RelationGetRelid(parentrel); + Oid parentOID = RelationGetRelid(parentrel); Oid childOID = RelationGetRelid(childrel); RangeTblEntry *childrte; Index childRTindex; @@ -513,6 +503,13 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte, *childrte_p = childrte; *childRTindex_p = childRTindex; + /* + * Retrieve column not-null constraint information for the child relation + * if its relation OID is different from the parent's. + */ + if (childOID != parentOID) + get_relation_notnullatts(root, childrel); + /* * Build an AppendRelInfo struct for each parent/child pair. */ @@ -831,8 +828,7 @@ expand_appendrel_subquery(PlannerInfo *root, RelOptInfo *rel, /* * apply_child_basequals * Populate childrel's base restriction quals from parent rel's quals, - * translating Vars using appinfo and re-checking for quals which are - * constant-TRUE or constant-FALSE when applied to this child relation. + * translating them using appinfo. * * If any of the resulting clauses evaluate to constant false or NULL, we * return false and don't apply any quals. Caller should mark the relation as @@ -907,16 +903,9 @@ apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel, rinfo->security_level, NULL, NULL, NULL); - /* Restriction is proven always false */ - if (restriction_is_always_false(root, childrinfo)) - return false; - /* Restriction is proven always true, so drop it */ - if (restriction_is_always_true(root, childrinfo)) - continue; - childquals = lappend(childquals, childrinfo); /* track minimum security level among child quals */ - cq_min_security = Min(cq_min_security, rinfo->security_level); + cq_min_security = Min(cq_min_security, childrinfo->security_level); } } diff --git a/src/backend/optimizer/util/joininfo.c b/src/backend/optimizer/util/joininfo.c index f26e38c6552f0..ef2f054c39e2e 100644 --- a/src/backend/optimizer/util/joininfo.c +++ b/src/backend/optimizer/util/joininfo.c @@ -3,7 +3,7 @@ * joininfo.c * joininfo list manipulation routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/optimizer/util/meson.build b/src/backend/optimizer/util/meson.build index b3bf913d09658..c50bcc4f74ca8 100644 --- a/src/backend/optimizer/util/meson.build +++ b/src/backend/optimizer/util/meson.build @@ -1,8 +1,9 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'appendinfo.c', 'clauses.c', + 'extendplan.c', 'inherit.c', 'joininfo.c', 'orclauses.c', diff --git a/src/backend/optimizer/util/orclauses.c b/src/backend/optimizer/util/orclauses.c index 2ccc670be51c1..0751c84e045ea 100644 --- a/src/backend/optimizer/util/orclauses.c +++ b/src/backend/optimizer/util/orclauses.c @@ -3,7 +3,7 @@ * orclauses.c * Routines to extract restriction OR clauses from join OR clauses * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c index 3bd3ce37c8fce..222de1450b209 100644 --- a/src/backend/optimizer/util/paramassign.c +++ b/src/backend/optimizer/util/paramassign.c @@ -40,7 +40,7 @@ * doesn't really save much executor work anyway. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -599,38 +599,46 @@ process_subquery_nestloop_params(PlannerInfo *root, List *subplan_params) } /* - * Identify any NestLoopParams that should be supplied by a NestLoop plan - * node with the specified lefthand rels. Remove them from the active - * root->curOuterParams list and return them as the result list. + * Identify any NestLoopParams that should be supplied by a NestLoop + * plan node with the specified lefthand rels and required-outer rels. + * Remove them from the active root->curOuterParams list and return + * them as the result list. * - * XXX Here we also hack up the returned Vars and PHVs so that they do not - * contain nullingrel sets exceeding what is available from the outer side. - * This is needed if we have applied outer join identity 3, - * (A leftjoin B on (Pab)) leftjoin C on (Pb*c) - * = A leftjoin (B leftjoin C on (Pbc)) on (Pab) - * and C contains lateral references to B. It's still safe to apply the - * identity, but the parser will have created those references in the form - * "b*" (i.e., with varnullingrels listing the A/B join), while what we will - * have available from the nestloop's outer side is just "b". We deal with - * that here by stripping the nullingrels down to what is available from the - * outer side according to leftrelids. - * - * That fixes matters for the case of forward application of identity 3. - * If the identity was applied in the reverse direction, we will have - * parameter Vars containing too few nullingrel bits rather than too many. - * Currently, that causes no problems because setrefs.c applies only a - * subset check to nullingrels in NestLoopParams, but we'd have to work - * harder if we ever want to tighten that check. This is all pretty annoying - * because it greatly weakens setrefs.c's cross-check, but the alternative + * Vars and PHVs appearing in the result list must have nullingrel sets + * that could validly appear in the lefthand rel's output. Ordinarily that + * would be true already, but if we have applied outer join identity 3, + * there could be more or fewer nullingrel bits in the nodes appearing in + * curOuterParams than are in the nominal leftrelids. We deal with that by + * forcing their nullingrel sets to include exactly the outer-join relids + * that appear in leftrelids and can null the respective Var or PHV. + * This fix is a bit ad-hoc and intellectually unsatisfactory, because it's + * essentially jumping to the conclusion that we've placed evaluation of + * the nestloop parameters correctly, and thus it defeats the intent of the + * subsequent nullingrel cross-checks in setrefs.c. But the alternative * seems to be to generate multiple versions of each laterally-parameterized * subquery, which'd be unduly expensive. */ List * -identify_current_nestloop_params(PlannerInfo *root, Relids leftrelids) +identify_current_nestloop_params(PlannerInfo *root, + Relids leftrelids, + Relids outerrelids) { List *result; + Relids allleftrelids; ListCell *cell; + /* + * We'll be able to evaluate a PHV in the lefthand path if it uses the + * lefthand rels plus any available required-outer rels. But don't do so + * if it uses *only* required-outer rels; in that case it should be + * evaluated higher in the tree. For Vars, no such hair-splitting is + * necessary since they depend on only one relid. + */ + if (outerrelids) + allleftrelids = bms_union(leftrelids, outerrelids); + else + allleftrelids = leftrelids; + result = NIL; foreach(cell, root->curOuterParams) { @@ -646,25 +654,60 @@ identify_current_nestloop_params(PlannerInfo *root, Relids leftrelids) bms_is_member(nlp->paramval->varno, leftrelids)) { Var *var = (Var *) nlp->paramval; + RelOptInfo *rel = root->simple_rel_array[var->varno]; root->curOuterParams = foreach_delete_current(root->curOuterParams, cell); - var->varnullingrels = bms_intersect(var->varnullingrels, + var->varnullingrels = bms_intersect(rel->nulling_relids, leftrelids); result = lappend(result, nlp); } - else if (IsA(nlp->paramval, PlaceHolderVar) && - bms_is_subset(find_placeholder_info(root, - (PlaceHolderVar *) nlp->paramval)->ph_eval_at, - leftrelids)) + else if (IsA(nlp->paramval, PlaceHolderVar)) { PlaceHolderVar *phv = (PlaceHolderVar *) nlp->paramval; + PlaceHolderInfo *phinfo = find_placeholder_info(root, phv); + Relids eval_at = phinfo->ph_eval_at; - root->curOuterParams = foreach_delete_current(root->curOuterParams, - cell); - phv->phnullingrels = bms_intersect(phv->phnullingrels, - leftrelids); - result = lappend(result, nlp); + if (bms_is_subset(eval_at, allleftrelids) && + bms_overlap(eval_at, leftrelids)) + { + root->curOuterParams = foreach_delete_current(root->curOuterParams, + cell); + + /* + * Deal with an edge case: if the PHV was pulled up out of a + * subquery and it contains a subquery that was originally + * pushed down from this query level, then that will still be + * represented as a SubLink, because SS_process_sublinks won't + * recurse into outer PHVs, so it didn't get transformed + * during expression preprocessing in the subquery. We need a + * version of the PHV that has a SubPlan, which we can get + * from the current query level's placeholder_list. This is + * quite grotty of course, but dealing with it earlier in the + * handling of subplan params would be just as grotty, and it + * might end up being a waste of cycles if we don't decide to + * treat the PHV as a NestLoopParam. (Perhaps that whole + * mechanism should be redesigned someday, but today is not + * that day.) + */ + if (root->parse->hasSubLinks) + { + phv = copyObject(phinfo->ph_var); + + /* + * The ph_var will have empty nullingrels, but that + * doesn't matter since we're about to overwrite + * phv->phnullingrels. Other fields should be OK already. + */ + nlp->paramval = (Var *) phv; + } + + phv->phnullingrels = + bms_intersect(get_placeholder_nulling_relids(root, phinfo), + leftrelids); + + result = lappend(result, nlp); + } } } return result; diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index e0192d4a491d2..73518c8f87018 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3,7 +3,7 @@ * pathnode.c * Routines to manipulate pathlists and create path nodes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,8 +14,8 @@ */ #include "postgres.h" -#include - +#include "access/htup_details.h" +#include "executor/nodeSetOp.h" #include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/extensible.h" @@ -46,7 +46,6 @@ typedef enum */ #define STD_FUZZ_FACTOR 1.01 -static List *translate_sub_tlist(List *tlist, int relid); static int append_total_cost_compare(const ListCell *a, const ListCell *b); static int append_startup_cost_compare(const ListCell *a, const ListCell *b); static List *reparameterize_pathlist_by_child(PlannerInfo *root, @@ -381,7 +380,6 @@ set_cheapest(RelOptInfo *parent_rel) parent_rel->cheapest_startup_path = cheapest_startup_path; parent_rel->cheapest_total_path = cheapest_total_path; - parent_rel->cheapest_unique_path = NULL; /* computed only if needed */ parent_rel->cheapest_parameterized_paths = parameterized_paths; } @@ -760,9 +758,10 @@ add_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, * parallel such that each worker will generate a subset of the path's * overall result. * - * As in add_path, the partial_pathlist is kept sorted with the cheapest - * total path in front. This is depended on by multiple places, which - * just take the front entry as the cheapest path without searching. + * As in add_path, the partial_pathlist is kept sorted first by smallest + * number of disabled nodes and then by lowest total cost. This is depended + * on by multiple places, which just take the front entry as the cheapest + * path without searching. * * We don't generate parameterized partial paths for several reasons. Most * importantly, they're not safe to execute, because there's nothing to @@ -779,10 +778,9 @@ add_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, * * Because we don't consider parameterized paths here, we also don't * need to consider the row counts as a measure of quality: every path will - * produce the same number of rows. Neither do we need to consider startup - * costs: parallelism is only used for plans that will be run to completion. - * Therefore, this routine is much simpler than add_path: it needs to - * consider only disabled nodes, pathkeys and total cost. + * produce the same number of rows. However, we do need to consider the + * startup costs: this partial path could be used beneath a Limit node, + * so a fast-start plan could be correct. * * As with add_path, we pfree paths that are found to be dominated by * another partial path; this requires that there be no other references to @@ -820,52 +818,41 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path) /* Compare pathkeys. */ keyscmp = compare_pathkeys(new_path->pathkeys, old_path->pathkeys); - /* Unless pathkeys are incompatible, keep just one of the two paths. */ + /* + * Unless pathkeys are incompatible, see if one of the paths dominates + * the other (both in startup and total cost). It may happen that one + * path has lower startup cost, the other has lower total cost. + */ if (keyscmp != PATHKEYS_DIFFERENT) { - if (unlikely(new_path->disabled_nodes != old_path->disabled_nodes)) + PathCostComparison costcmp; + + /* + * Do a fuzzy cost comparison with standard fuzziness limit. + */ + costcmp = compare_path_costs_fuzzily(new_path, old_path, + STD_FUZZ_FACTOR); + if (costcmp == COSTS_BETTER1) { - if (new_path->disabled_nodes > old_path->disabled_nodes) - accept_new = false; - else + if (keyscmp != PATHKEYS_BETTER2) remove_old = true; } - else if (new_path->total_cost > old_path->total_cost - * STD_FUZZ_FACTOR) + else if (costcmp == COSTS_BETTER2) { - /* New path costs more; keep it only if pathkeys are better. */ if (keyscmp != PATHKEYS_BETTER1) accept_new = false; } - else if (old_path->total_cost > new_path->total_cost - * STD_FUZZ_FACTOR) + else if (costcmp == COSTS_EQUAL) { - /* Old path costs more; keep it only if pathkeys are better. */ - if (keyscmp != PATHKEYS_BETTER2) + if (keyscmp == PATHKEYS_BETTER1) remove_old = true; - } - else if (keyscmp == PATHKEYS_BETTER1) - { - /* Costs are about the same, new path has better pathkeys. */ - remove_old = true; - } - else if (keyscmp == PATHKEYS_BETTER2) - { - /* Costs are about the same, old path has better pathkeys. */ - accept_new = false; - } - else if (old_path->total_cost > new_path->total_cost * 1.0000000001) - { - /* Pathkeys are the same, and the old path costs more. */ - remove_old = true; - } - else - { - /* - * Pathkeys are the same, and new path isn't materially - * cheaper. - */ - accept_new = false; + else if (keyscmp == PATHKEYS_BETTER2) + accept_new = false; + else if (compare_path_costs_fuzzily(new_path, old_path, + 1.0000000001) == COSTS_BETTER1) + remove_old = true; + else + accept_new = false; } } @@ -880,8 +867,13 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path) } else { - /* new belongs after this old path if it has cost >= old's */ - if (new_path->total_cost >= old_path->total_cost) + /* + * new belongs after this old path if it has more disabled nodes + * or if it has the same number of nodes but a greater total cost + */ + if (new_path->disabled_nodes > old_path->disabled_nodes || + (new_path->disabled_nodes == old_path->disabled_nodes && + new_path->total_cost >= old_path->total_cost)) insert_at = foreach_current_index(p1) + 1; } @@ -911,16 +903,16 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path) * add_partial_path_precheck * Check whether a proposed new partial path could possibly get accepted. * - * Unlike add_path_precheck, we can ignore startup cost and parameterization, - * since they don't matter for partial paths (see add_partial_path). But - * we do want to make sure we don't add a partial path if there's already - * a complete path that dominates it, since in that case the proposed path - * is surely a loser. + * Unlike add_path_precheck, we can ignore parameterization, since it doesn't + * matter for partial paths (see add_partial_path). But we do want to make + * sure we don't add a partial path if there's already a complete path that + * dominates it, since in that case the proposed path is surely a loser. */ bool add_partial_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, - Cost total_cost, List *pathkeys) + Cost startup_cost, Cost total_cost, List *pathkeys) { + bool consider_startup = parent_rel->consider_startup; ListCell *p1; /* @@ -930,25 +922,81 @@ add_partial_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, * is clearly superior to some existing partial path -- at least, modulo * final cost computations. If so, we definitely want to consider it. * - * Unlike add_path(), we always compare pathkeys here. This is because we - * expect partial_pathlist to be very short, and getting a definitive - * answer at this stage avoids the need to call add_path_precheck. + * Unlike add_path(), we never try to exit this loop early. This is + * because we expect partial_pathlist to be very short, and getting a + * definitive answer at this stage avoids the need to call + * add_path_precheck. */ foreach(p1, parent_rel->partial_pathlist) { Path *old_path = (Path *) lfirst(p1); + PathCostComparison costcmp; PathKeysComparison keyscmp; - keyscmp = compare_pathkeys(pathkeys, old_path->pathkeys); - if (keyscmp != PATHKEYS_DIFFERENT) + /* + * First, compare costs and disabled nodes. This logic should be + * identical to compare_path_costs_fuzzily, except that one of the + * paths hasn't been created yet, and the fuzz factor is always + * STD_FUZZ_FACTOR. + */ + if (unlikely(old_path->disabled_nodes != disabled_nodes)) { - if (total_cost > old_path->total_cost * STD_FUZZ_FACTOR && - keyscmp != PATHKEYS_BETTER1) - return false; - if (old_path->total_cost > total_cost * STD_FUZZ_FACTOR && - keyscmp != PATHKEYS_BETTER2) - return true; + if (disabled_nodes < old_path->disabled_nodes) + costcmp = COSTS_BETTER1; + else + costcmp = COSTS_BETTER2; } + else if (total_cost > old_path->total_cost * STD_FUZZ_FACTOR) + { + if (consider_startup && + old_path->startup_cost > startup_cost * STD_FUZZ_FACTOR) + costcmp = COSTS_DIFFERENT; + else + costcmp = COSTS_BETTER2; + } + else if (old_path->total_cost > total_cost * STD_FUZZ_FACTOR) + { + if (consider_startup && + startup_cost > old_path->startup_cost * STD_FUZZ_FACTOR) + costcmp = COSTS_DIFFERENT; + else + costcmp = COSTS_BETTER1; + } + else if (startup_cost > old_path->startup_cost * STD_FUZZ_FACTOR) + costcmp = COSTS_BETTER2; + else if (old_path->startup_cost > startup_cost * STD_FUZZ_FACTOR) + costcmp = COSTS_BETTER1; + else + costcmp = COSTS_EQUAL; + + /* + * If one path wins on startup cost and the other on total cost, we + * can't say for sure which is better. + */ + if (costcmp == COSTS_DIFFERENT) + continue; + + /* + * If the two paths have different pathkeys, we can't say for sure + * which is better. + */ + keyscmp = compare_pathkeys(pathkeys, old_path->pathkeys); + if (keyscmp == PATHKEYS_DIFFERENT) + continue; + + /* + * If the existing path is cheaper and the pathkeys are equal or + * worse, the new path is not interesting. + */ + if (costcmp == COSTS_BETTER2 && keyscmp != PATHKEYS_BETTER1) + return false; + + /* + * If the new path is cheaper and the pathkeys are equal or better, it + * is definitely interesting. + */ + if (costcmp == COSTS_BETTER1 && keyscmp != PATHKEYS_BETTER2) + return true; } /* @@ -956,14 +1004,9 @@ add_partial_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, * clearly good enough that it might replace one. Compare it to * non-parallel plans. If it loses even before accounting for the cost of * the Gather node, we should definitely reject it. - * - * Note that we pass the total_cost to add_path_precheck twice. This is - * because it's never advantageous to consider the startup cost of a - * partial path; the resulting plans, if run in parallel, will be run to - * completion. */ - if (!add_path_precheck(parent_rel, disabled_nodes, total_cost, total_cost, - pathkeys, NULL)) + if (!add_path_precheck(parent_rel, disabled_nodes, startup_cost, + total_cost, pathkeys, NULL)) return false; return true; @@ -1079,6 +1122,14 @@ create_index_path(PlannerInfo *root, cost_index(pathnode, root, loop_count, partial_path); + /* + * cost_index will set disabled_nodes to 1 if this rel is not allowed to + * use index scans in general, but it doesn't have the IndexOptInfo to + * know whether this specific index has been disabled. + */ + if (index->disabled) + pathnode->path.disabled_nodes = 1; + return pathnode; } @@ -1262,7 +1313,8 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals, */ TidRangePath * create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, - List *tidrangequals, Relids required_outer) + List *tidrangequals, Relids required_outer, + int parallel_workers) { TidRangePath *pathnode = makeNode(TidRangePath); @@ -1271,9 +1323,9 @@ create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, pathnode->path.pathtarget = rel->reltarget; pathnode->path.param_info = get_baserel_parampathinfo(root, rel, required_outer); - pathnode->path.parallel_aware = false; + pathnode->path.parallel_aware = (parallel_workers > 0); pathnode->path.parallel_safe = rel->consider_parallel; - pathnode->path.parallel_workers = 0; + pathnode->path.parallel_workers = parallel_workers; pathnode->path.pathkeys = NIL; /* always unordered */ pathnode->tidrangequals = tidrangequals; @@ -1299,7 +1351,7 @@ create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, AppendPath * create_append_path(PlannerInfo *root, RelOptInfo *rel, - List *subpaths, List *partial_subpaths, + AppendPathInput input, List *pathkeys, Relids required_outer, int parallel_workers, bool parallel_aware, double rows) @@ -1309,6 +1361,7 @@ create_append_path(PlannerInfo *root, Assert(!parallel_aware || parallel_workers > 0); + pathnode->child_append_relid_sets = input.child_append_relid_sets; pathnode->path.pathtype = T_Append; pathnode->path.parent = rel; pathnode->path.pathtarget = rel->reltarget; @@ -1324,7 +1377,7 @@ create_append_path(PlannerInfo *root, * on the simpler get_appendrel_parampathinfo. There's no point in doing * the more expensive thing for a dummy path, either. */ - if (rel->reloptkind == RELOPT_BASEREL && root && subpaths != NIL) + if (rel->reloptkind == RELOPT_BASEREL && root && input.subpaths != NIL) pathnode->path.param_info = get_baserel_parampathinfo(root, rel, required_outer); @@ -1355,11 +1408,11 @@ create_append_path(PlannerInfo *root, */ Assert(pathkeys == NIL); - list_sort(subpaths, append_total_cost_compare); - list_sort(partial_subpaths, append_startup_cost_compare); + list_sort(input.subpaths, append_total_cost_compare); + list_sort(input.partial_subpaths, append_startup_cost_compare); } - pathnode->first_partial_path = list_length(subpaths); - pathnode->subpaths = list_concat(subpaths, partial_subpaths); + pathnode->first_partial_path = list_length(input.subpaths); + pathnode->subpaths = list_concat(input.subpaths, input.partial_subpaths); /* * Apply query-wide LIMIT if known and path is for sole base relation. @@ -1404,12 +1457,12 @@ create_append_path(PlannerInfo *root, pathnode->path.total_cost = child->total_cost; } else - cost_append(pathnode); + cost_append(pathnode, root); /* Must do this last, else cost_append complains */ pathnode->path.pathkeys = child->pathkeys; } else - cost_append(pathnode); + cost_append(pathnode, root); /* If the caller provided a row estimate, override the computed value. */ if (rows >= 0) @@ -1471,6 +1524,7 @@ MergeAppendPath * create_merge_append_path(PlannerInfo *root, RelOptInfo *rel, List *subpaths, + List *child_append_relid_sets, List *pathkeys, Relids required_outer) { @@ -1486,6 +1540,7 @@ create_merge_append_path(PlannerInfo *root, */ Assert(bms_is_empty(rel->lateral_relids) && bms_is_empty(required_outer)); + pathnode->child_append_relid_sets = child_append_relid_sets; pathnode->path.pathtype = T_MergeAppend; pathnode->path.parent = rel; pathnode->path.pathtarget = rel->reltarget; @@ -1515,6 +1570,9 @@ create_merge_append_path(PlannerInfo *root, foreach(l, subpaths) { Path *subpath = (Path *) lfirst(l); + int presorted_keys; + Path sort_path; /* dummy for result of + * cost_sort/cost_incremental_sort */ /* All child paths should be unparameterized */ Assert(bms_is_empty(PATH_REQ_OUTER(subpath))); @@ -1523,32 +1581,52 @@ create_merge_append_path(PlannerInfo *root, pathnode->path.parallel_safe = pathnode->path.parallel_safe && subpath->parallel_safe; - if (pathkeys_contained_in(pathkeys, subpath->pathkeys)) - { - /* Subpath is adequately ordered, we won't need to sort it */ - input_disabled_nodes += subpath->disabled_nodes; - input_startup_cost += subpath->startup_cost; - input_total_cost += subpath->total_cost; - } - else + if (!pathkeys_count_contained_in(pathkeys, subpath->pathkeys, + &presorted_keys)) { - /* We'll need to insert a Sort node, so include cost for that */ - Path sort_path; /* dummy for result of cost_sort */ + /* + * We'll need to insert a Sort node, so include costs for that. We + * choose to use incremental sort if it is enabled and there are + * presorted keys; otherwise we use full sort. + * + * We can use the parent's LIMIT if any, since we certainly won't + * pull more than that many tuples from any child. + */ + if (enable_incremental_sort && presorted_keys > 0) + { + cost_incremental_sort(&sort_path, + root, + pathkeys, + presorted_keys, + subpath->disabled_nodes, + subpath->startup_cost, + subpath->total_cost, + subpath->rows, + subpath->pathtarget->width, + 0.0, + work_mem, + pathnode->limit_tuples); + } + else + { + cost_sort(&sort_path, + root, + pathkeys, + subpath->disabled_nodes, + subpath->total_cost, + subpath->rows, + subpath->pathtarget->width, + 0.0, + work_mem, + pathnode->limit_tuples); + } - cost_sort(&sort_path, - root, - pathkeys, - subpath->disabled_nodes, - subpath->total_cost, - subpath->rows, - subpath->pathtarget->width, - 0.0, - work_mem, - pathnode->limit_tuples); - input_disabled_nodes += sort_path.disabled_nodes; - input_startup_cost += sort_path.startup_cost; - input_total_cost += sort_path.total_cost; + subpath = &sort_path; } + + input_disabled_nodes += subpath->disabled_nodes; + input_startup_cost += subpath->startup_cost; + input_total_cost += subpath->total_cost; } /* @@ -1631,7 +1709,7 @@ create_group_result_path(PlannerInfo *root, RelOptInfo *rel, * pathnode. */ MaterialPath * -create_material_path(RelOptInfo *rel, Path *subpath) +create_material_path(RelOptInfo *rel, Path *subpath, bool enabled) { MaterialPath *pathnode = makeNode(MaterialPath); @@ -1650,6 +1728,7 @@ create_material_path(RelOptInfo *rel, Path *subpath) pathnode->subpath = subpath; cost_material(&pathnode->path, + enabled, subpath->disabled_nodes, subpath->startup_cost, subpath->total_cost, @@ -1666,7 +1745,7 @@ create_material_path(RelOptInfo *rel, Path *subpath) MemoizePath * create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, List *param_exprs, List *hash_operators, - bool singlerow, bool binary_mode, double calls) + bool singlerow, bool binary_mode, Cardinality est_calls) { MemoizePath *pathnode = makeNode(MemoizePath); @@ -1687,7 +1766,6 @@ create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, pathnode->param_exprs = param_exprs; pathnode->singlerow = singlerow; pathnode->binary_mode = binary_mode; - pathnode->calls = clamp_row_est(calls); /* * For now we set est_entries to 0. cost_memoize_rescan() does all the @@ -1697,8 +1775,21 @@ create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, */ pathnode->est_entries = 0; - /* we should not generate this path type when enable_memoize=false */ - Assert(enable_memoize); + pathnode->est_calls = clamp_row_est(est_calls); + + /* These will also be set later in cost_memoize_rescan() */ + pathnode->est_unique_keys = 0.0; + pathnode->est_hit_ratio = 0.0; + + /* + * We should not be asked to generate this path type when memoization is + * disabled, so set our count of disabled nodes equal to the subpath's + * count. + * + * It would be nice to also Assert that memoization is enabled, but the + * value of enable_memoize is not controlling: what we would need to check + * is that the JoinPathExtraData's pgs_mask included PGS_NESTLOOP_MEMOIZE. + */ pathnode->path.disabled_nodes = subpath->disabled_nodes; /* @@ -1712,246 +1803,6 @@ create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, return pathnode; } -/* - * create_unique_path - * Creates a path representing elimination of distinct rows from the - * input data. Distinct-ness is defined according to the needs of the - * semijoin represented by sjinfo. If it is not possible to identify - * how to make the data unique, NULL is returned. - * - * If used at all, this is likely to be called repeatedly on the same rel; - * and the input subpath should always be the same (the cheapest_total path - * for the rel). So we cache the result. - */ -UniquePath * -create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, - SpecialJoinInfo *sjinfo) -{ - UniquePath *pathnode; - Path sort_path; /* dummy for result of cost_sort */ - Path agg_path; /* dummy for result of cost_agg */ - MemoryContext oldcontext; - int numCols; - - /* Caller made a mistake if subpath isn't cheapest_total ... */ - Assert(subpath == rel->cheapest_total_path); - Assert(subpath->parent == rel); - /* ... or if SpecialJoinInfo is the wrong one */ - Assert(sjinfo->jointype == JOIN_SEMI); - Assert(bms_equal(rel->relids, sjinfo->syn_righthand)); - - /* If result already cached, return it */ - if (rel->cheapest_unique_path) - return (UniquePath *) rel->cheapest_unique_path; - - /* If it's not possible to unique-ify, return NULL */ - if (!(sjinfo->semi_can_btree || sjinfo->semi_can_hash)) - return NULL; - - /* - * When called during GEQO join planning, we are in a short-lived memory - * context. We must make sure that the path and any subsidiary data - * structures created for a baserel survive the GEQO cycle, else the - * baserel is trashed for future GEQO cycles. On the other hand, when we - * are creating those for a joinrel during GEQO, we don't want them to - * clutter the main planning context. Upshot is that the best solution is - * to explicitly allocate memory in the same context the given RelOptInfo - * is in. - */ - oldcontext = MemoryContextSwitchTo(GetMemoryChunkContext(rel)); - - pathnode = makeNode(UniquePath); - - pathnode->path.pathtype = T_Unique; - pathnode->path.parent = rel; - pathnode->path.pathtarget = rel->reltarget; - pathnode->path.param_info = subpath->param_info; - pathnode->path.parallel_aware = false; - pathnode->path.parallel_safe = rel->consider_parallel && - subpath->parallel_safe; - pathnode->path.parallel_workers = subpath->parallel_workers; - - /* - * Assume the output is unsorted, since we don't necessarily have pathkeys - * to represent it. (This might get overridden below.) - */ - pathnode->path.pathkeys = NIL; - - pathnode->subpath = subpath; - - /* - * Under GEQO and when planning child joins, the sjinfo might be - * short-lived, so we'd better make copies of data structures we extract - * from it. - */ - pathnode->in_operators = copyObject(sjinfo->semi_operators); - pathnode->uniq_exprs = copyObject(sjinfo->semi_rhs_exprs); - - /* - * If the input is a relation and it has a unique index that proves the - * semi_rhs_exprs are unique, then we don't need to do anything. Note - * that relation_has_unique_index_for automatically considers restriction - * clauses for the rel, as well. - */ - if (rel->rtekind == RTE_RELATION && sjinfo->semi_can_btree && - relation_has_unique_index_for(root, rel, NIL, - sjinfo->semi_rhs_exprs, - sjinfo->semi_operators)) - { - pathnode->umethod = UNIQUE_PATH_NOOP; - pathnode->path.rows = rel->rows; - pathnode->path.disabled_nodes = subpath->disabled_nodes; - pathnode->path.startup_cost = subpath->startup_cost; - pathnode->path.total_cost = subpath->total_cost; - pathnode->path.pathkeys = subpath->pathkeys; - - rel->cheapest_unique_path = (Path *) pathnode; - - MemoryContextSwitchTo(oldcontext); - - return pathnode; - } - - /* - * If the input is a subquery whose output must be unique already, then we - * don't need to do anything. The test for uniqueness has to consider - * exactly which columns we are extracting; for example "SELECT DISTINCT - * x,y" doesn't guarantee that x alone is distinct. So we cannot check for - * this optimization unless semi_rhs_exprs consists only of simple Vars - * referencing subquery outputs. (Possibly we could do something with - * expressions in the subquery outputs, too, but for now keep it simple.) - */ - if (rel->rtekind == RTE_SUBQUERY) - { - RangeTblEntry *rte = planner_rt_fetch(rel->relid, root); - - if (query_supports_distinctness(rte->subquery)) - { - List *sub_tlist_colnos; - - sub_tlist_colnos = translate_sub_tlist(sjinfo->semi_rhs_exprs, - rel->relid); - - if (sub_tlist_colnos && - query_is_distinct_for(rte->subquery, - sub_tlist_colnos, - sjinfo->semi_operators)) - { - pathnode->umethod = UNIQUE_PATH_NOOP; - pathnode->path.rows = rel->rows; - pathnode->path.disabled_nodes = subpath->disabled_nodes; - pathnode->path.startup_cost = subpath->startup_cost; - pathnode->path.total_cost = subpath->total_cost; - pathnode->path.pathkeys = subpath->pathkeys; - - rel->cheapest_unique_path = (Path *) pathnode; - - MemoryContextSwitchTo(oldcontext); - - return pathnode; - } - } - } - - /* Estimate number of output rows */ - pathnode->path.rows = estimate_num_groups(root, - sjinfo->semi_rhs_exprs, - rel->rows, - NULL, - NULL); - numCols = list_length(sjinfo->semi_rhs_exprs); - - if (sjinfo->semi_can_btree) - { - /* - * Estimate cost for sort+unique implementation - */ - cost_sort(&sort_path, root, NIL, - subpath->disabled_nodes, - subpath->total_cost, - rel->rows, - subpath->pathtarget->width, - 0.0, - work_mem, - -1.0); - - /* - * Charge one cpu_operator_cost per comparison per input tuple. We - * assume all columns get compared at most of the tuples. (XXX - * probably this is an overestimate.) This should agree with - * create_upper_unique_path. - */ - sort_path.total_cost += cpu_operator_cost * rel->rows * numCols; - } - - if (sjinfo->semi_can_hash) - { - /* - * Estimate the overhead per hashtable entry at 64 bytes (same as in - * planner.c). - */ - int hashentrysize = subpath->pathtarget->width + 64; - - if (hashentrysize * pathnode->path.rows > get_hash_memory_limit()) - { - /* - * We should not try to hash. Hack the SpecialJoinInfo to - * remember this, in case we come through here again. - */ - sjinfo->semi_can_hash = false; - } - else - cost_agg(&agg_path, root, - AGG_HASHED, NULL, - numCols, pathnode->path.rows, - NIL, - subpath->disabled_nodes, - subpath->startup_cost, - subpath->total_cost, - rel->rows, - subpath->pathtarget->width); - } - - if (sjinfo->semi_can_btree && sjinfo->semi_can_hash) - { - if (agg_path.disabled_nodes < sort_path.disabled_nodes || - (agg_path.disabled_nodes == sort_path.disabled_nodes && - agg_path.total_cost < sort_path.total_cost)) - pathnode->umethod = UNIQUE_PATH_HASH; - else - pathnode->umethod = UNIQUE_PATH_SORT; - } - else if (sjinfo->semi_can_btree) - pathnode->umethod = UNIQUE_PATH_SORT; - else if (sjinfo->semi_can_hash) - pathnode->umethod = UNIQUE_PATH_HASH; - else - { - /* we can get here only if we abandoned hashing above */ - MemoryContextSwitchTo(oldcontext); - return NULL; - } - - if (pathnode->umethod == UNIQUE_PATH_HASH) - { - pathnode->path.disabled_nodes = agg_path.disabled_nodes; - pathnode->path.startup_cost = agg_path.startup_cost; - pathnode->path.total_cost = agg_path.total_cost; - } - else - { - pathnode->path.disabled_nodes = sort_path.disabled_nodes; - pathnode->path.startup_cost = sort_path.startup_cost; - pathnode->path.total_cost = sort_path.total_cost; - } - - rel->cheapest_unique_path = (Path *) pathnode; - - MemoryContextSwitchTo(oldcontext); - - return pathnode; -} - /* * create_gather_merge_path * @@ -2003,36 +1854,6 @@ create_gather_merge_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, return pathnode; } -/* - * translate_sub_tlist - get subquery column numbers represented by tlist - * - * The given targetlist usually contains only Vars referencing the given relid. - * Extract their varattnos (ie, the column numbers of the subquery) and return - * as an integer List. - * - * If any of the tlist items is not a simple Var, we cannot determine whether - * the subquery's uniqueness condition (if any) matches ours, so punt and - * return NIL. - */ -static List * -translate_sub_tlist(List *tlist, int relid) -{ - List *result = NIL; - ListCell *l; - - foreach(l, tlist) - { - Var *var = (Var *) lfirst(l); - - if (!var || !IsA(var, Var) || - var->varno != relid) - return NIL; /* punt */ - - result = lappend_int(result, var->varattno); - } - return result; -} - /* * create_gather_path * Creates a path corresponding to a gather scan, returning the @@ -2790,8 +2611,7 @@ create_projection_path(PlannerInfo *root, pathnode->path.pathtype = T_Result; pathnode->path.parent = rel; pathnode->path.pathtarget = target; - /* For now, assume we are above any joins, so no parameterization */ - pathnode->path.param_info = NULL; + pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && subpath->parallel_safe && @@ -3046,8 +2866,7 @@ create_incremental_sort_path(PlannerInfo *root, pathnode->path.parent = rel; /* Sort doesn't project, so use source path's pathtarget */ pathnode->path.pathtarget = subpath->pathtarget; - /* For now, assume we are above any joins, so no parameterization */ - pathnode->path.param_info = NULL; + pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && subpath->parallel_safe; @@ -3094,8 +2913,7 @@ create_sort_path(PlannerInfo *root, pathnode->path.parent = rel; /* Sort doesn't project, so use source path's pathtarget */ pathnode->path.pathtarget = subpath->pathtarget; - /* For now, assume we are above any joins, so no parameterization */ - pathnode->path.param_info = NULL; + pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && subpath->parallel_safe; @@ -3171,13 +2989,10 @@ create_group_path(PlannerInfo *root, } /* - * create_upper_unique_path + * create_unique_path * Creates a pathnode that represents performing an explicit Unique step * on presorted input. * - * This produces a Unique plan node, but the use-case is so different from - * create_unique_path that it doesn't seem worth trying to merge the two. - * * 'rel' is the parent relation associated with the result * 'subpath' is the path representing the source of data * 'numCols' is the number of grouping columns @@ -3186,21 +3001,20 @@ create_group_path(PlannerInfo *root, * The input path must be sorted on the grouping columns, plus possibly * additional columns; so the first numCols pathkeys are the grouping columns */ -UpperUniquePath * -create_upper_unique_path(PlannerInfo *root, - RelOptInfo *rel, - Path *subpath, - int numCols, - double numGroups) +UniquePath * +create_unique_path(PlannerInfo *root, + RelOptInfo *rel, + Path *subpath, + int numCols, + double numGroups) { - UpperUniquePath *pathnode = makeNode(UpperUniquePath); + UniquePath *pathnode = makeNode(UniquePath); pathnode->path.pathtype = T_Unique; pathnode->path.parent = rel; /* Unique doesn't project, so use source path's pathtarget */ pathnode->path.pathtarget = subpath->pathtarget; - /* For now, assume we are above any joins, so no parameterization */ - pathnode->path.param_info = NULL; + pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && subpath->parallel_safe; @@ -3256,8 +3070,7 @@ create_agg_path(PlannerInfo *root, pathnode->path.pathtype = T_Agg; pathnode->path.parent = rel; pathnode->path.pathtarget = target; - /* For now, assume we are above any joins, so no parameterization */ - pathnode->path.param_info = NULL; + pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && subpath->parallel_safe; @@ -3712,7 +3525,7 @@ create_setop_path(PlannerInfo *root, } else { - Size hashentrysize; + Size hashtablesize; /* * In hashed mode, we must read all the input before we can emit @@ -3741,11 +3554,12 @@ create_setop_path(PlannerInfo *root, /* * Also disable if it doesn't look like the hashtable will fit into - * hash_mem. + * hash_mem. (Note: reject on equality, to ensure that an estimate of + * SIZE_MAX disables hashing regardless of the hash_mem limit.) */ - hashentrysize = MAXALIGN(leftpath->pathtarget->width) + - MAXALIGN(SizeofMinimalTupleHeader); - if (hashentrysize * numGroups > get_hash_memory_limit()) + hashtablesize = EstimateSetOpHashTableSpace(numGroups, + leftpath->pathtarget->width); + if (hashtablesize >= get_hash_memory_limit()) pathnode->path.disabled_nodes++; } pathnode->path.rows = outputRows; @@ -3863,8 +3677,6 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel, * 'canSetTag' is true if we set the command tag/es_processed * 'nominalRelation' is the parent RT index for use of EXPLAIN * 'rootRelation' is the partitioned/inherited table root RTI, or 0 if none - * 'partColsUpdated' is true if any partitioning columns are being updated, - * either from the target relation or a descendent partitioned table. * 'resultRelations' is an integer list of actual RT indexes of target rel(s) * 'updateColnosLists' is a list of UPDATE target column number lists * (one sublist per rel); or NIL if not an UPDATE @@ -3881,13 +3693,12 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, CmdType operation, bool canSetTag, Index nominalRelation, Index rootRelation, - bool partColsUpdated, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, List *mergeActionLists, List *mergeJoinConditions, - int epqParam) + ForPortionOfExpr *forPortionOf, int epqParam) { ModifyTablePath *pathnode = makeNode(ModifyTablePath); @@ -3947,13 +3758,13 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, pathnode->canSetTag = canSetTag; pathnode->nominalRelation = nominalRelation; pathnode->rootRelation = rootRelation; - pathnode->partColsUpdated = partColsUpdated; pathnode->resultRelations = resultRelations; pathnode->updateColnosLists = updateColnosLists; pathnode->withCheckOptionLists = withCheckOptionLists; pathnode->returningLists = returningLists; pathnode->rowMarks = rowMarks; pathnode->onconflict = onconflict; + pathnode->forPortionOf = forPortionOf; pathnode->epqParam = epqParam; pathnode->mergeActionLists = mergeActionLists; pathnode->mergeJoinConditions = mergeJoinConditions; @@ -4117,7 +3928,7 @@ reparameterize_path(PlannerInfo *root, Path *path, case T_SeqScan: return create_seqscan_path(root, rel, required_outer, 0); case T_SampleScan: - return (Path *) create_samplescan_path(root, rel, required_outer); + return create_samplescan_path(root, rel, required_outer); case T_IndexScan: case T_IndexOnlyScan: { @@ -4178,11 +3989,12 @@ reparameterize_path(PlannerInfo *root, Path *path, case T_Append: { AppendPath *apath = (AppendPath *) path; - List *childpaths = NIL; - List *partialpaths = NIL; + AppendPathInput new_append = {0}; int i; ListCell *lc; + new_append.child_append_relid_sets = apath->child_append_relid_sets; + /* Reparameterize the children */ i = 0; foreach(lc, apath->subpaths) @@ -4196,13 +4008,13 @@ reparameterize_path(PlannerInfo *root, Path *path, return NULL; /* We have to re-split the regular and partial paths */ if (i < apath->first_partial_path) - childpaths = lappend(childpaths, spath); + new_append.subpaths = lappend(new_append.subpaths, spath); else - partialpaths = lappend(partialpaths, spath); + new_append.partial_subpaths = lappend(new_append.partial_subpaths, spath); i++; } return (Path *) - create_append_path(root, rel, childpaths, partialpaths, + create_append_path(root, rel, new_append, apath->path.pathkeys, required_outer, apath->path.parallel_workers, apath->path.parallel_aware, @@ -4212,13 +4024,16 @@ reparameterize_path(PlannerInfo *root, Path *path, { MaterialPath *mpath = (MaterialPath *) path; Path *spath = mpath->subpath; + bool enabled; spath = reparameterize_path(root, spath, required_outer, loop_count); if (spath == NULL) return NULL; - return (Path *) create_material_path(rel, spath); + enabled = + (mpath->path.disabled_nodes <= spath->disabled_nodes); + return (Path *) create_material_path(rel, spath, enabled); } case T_Memoize: { @@ -4236,7 +4051,7 @@ reparameterize_path(PlannerInfo *root, Path *path, mpath->hash_operators, mpath->singlerow, mpath->binary_mode, - mpath->calls); + mpath->est_calls); } default: break; diff --git a/src/backend/optimizer/util/placeholder.c b/src/backend/optimizer/util/placeholder.c index 41a4c81e94a75..dd9b11885af19 100644 --- a/src/backend/optimizer/util/placeholder.c +++ b/src/backend/optimizer/util/placeholder.c @@ -4,7 +4,7 @@ * PlaceHolderVar and PlaceHolderInfo manipulation routines * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -35,6 +35,8 @@ static void find_placeholders_recurse(PlannerInfo *root, Node *jtnode); static void find_placeholders_in_expr(PlannerInfo *root, Node *expr); static bool contain_placeholder_references_walker(Node *node, contain_placeholder_references_context *context); +static bool contain_noop_phv_walker(Node *node, void *context); +static Node *strip_noop_phvs_mutator(Node *node, void *context); /* @@ -545,3 +547,132 @@ contain_placeholder_references_walker(Node *node, return expression_tree_walker(node, contain_placeholder_references_walker, context); } + +/* + * Compute the set of outer-join relids that can null a placeholder. + * + * This is analogous to RelOptInfo.nulling_relids for Vars, but we compute it + * on-the-fly rather than saving it somewhere. Currently the value is needed + * at most once per query, so there's little value in doing otherwise. If it + * ever gains more widespread use, perhaps we should cache the result in + * PlaceHolderInfo. + */ +Relids +get_placeholder_nulling_relids(PlannerInfo *root, PlaceHolderInfo *phinfo) +{ + Relids result = NULL; + int relid = -1; + + /* + * Form the union of all potential nulling OJs for each baserel included + * in ph_eval_at. + */ + while ((relid = bms_next_member(phinfo->ph_eval_at, relid)) > 0) + { + RelOptInfo *rel = root->simple_rel_array[relid]; + + /* ignore the RTE_GROUP RTE */ + if (relid == root->group_rtindex) + continue; + + if (rel == NULL) /* must be an outer join */ + { + Assert(bms_is_member(relid, root->outer_join_rels)); + continue; + } + result = bms_add_members(result, rel->nulling_relids); + } + + /* Now remove any OJs already included in ph_eval_at, and we're done. */ + result = bms_del_members(result, phinfo->ph_eval_at); + return result; +} + +/* + * strip_noop_phvs + * Strip no-op PlaceHolderVar nodes from the given expression tree. + * + * A PlaceHolderVar that is not marked as nullable (i.e., its phnullingrels + * is empty) is effectively a no-op when it appears in a relation-scan-level + * expression. This function strips such PlaceHolderVars, which is useful + * for matching expressions to index keys or partition keys in cases where + * the expression has been wrapped in PlaceHolderVars during subquery pullup. + * + * IMPORTANT: the caller must ensure that the expression is a scan-level + * expression, so that non-nullable PlaceHolderVars in it are indeed no-ops. + * + * The removal is performed recursively because PlaceHolderVars can be nested + * or interleaved with other node types. We must peel back all layers to + * expose the base expression. + * + * As a performance optimization, we first use a lightweight walker to check + * for the presence of strippable PlaceHolderVars. The expensive mutator is + * invoked only if a candidate is found, avoiding unnecessary memory allocation + * and tree copying in the common case where no PlaceHolderVars are present. + */ +Node * +strip_noop_phvs(Node *node) +{ + /* Don't mutate/copy if no target PHVs exist */ + if (!contain_noop_phv_walker(node, NULL)) + return node; + + return strip_noop_phvs_mutator(node, NULL); +} + +/* + * contain_noop_phv_walker + * Detect if there are any PlaceHolderVars in the tree that are candidates + * for stripping. + * + * We identify a PlaceHolderVar as strippable only if its phnullingrels is + * empty. + */ +static bool +contain_noop_phv_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + + if (bms_is_empty(phv->phnullingrels)) + return true; + } + + return expression_tree_walker(node, contain_noop_phv_walker, + context); +} + +/* + * strip_noop_phvs_mutator + * Recursively remove PlaceHolderVars that are not marked nullable. + * + * We strip a PlaceHolderVar only if its phnullingrels is empty, replacing it + * with its contained expression. + */ +static Node * +strip_noop_phvs_mutator(Node *node, void *context) +{ + if (node == NULL) + return NULL; + + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv = (PlaceHolderVar *) node; + + if (bms_is_empty(phv->phnullingrels)) + { + /* Recurse on its contained expression */ + return strip_noop_phvs_mutator((Node *) phv->phexpr, + context); + } + + /* Otherwise, keep this PHV but check its contained expression */ + } + + return expression_tree_mutator(node, strip_noop_phvs_mutator, + context); +} diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 59233b647302d..7c4be1748699d 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -4,7 +4,7 @@ * routines for accessing the system catalogs * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -42,6 +42,7 @@ #include "parser/parse_relation.h" #include "parser/parsetree.h" #include "partitioning/partdesc.h" +#include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "statistics/statistics.h" #include "storage/bufmgr.h" @@ -56,8 +57,11 @@ /* GUC parameter */ int constraint_exclusion = CONSTRAINT_EXCLUSION_PARTITION; -/* Hook for plugins to get control in get_relation_info() */ -get_relation_info_hook_type get_relation_info_hook = NULL; +typedef struct NotnullHashEntry +{ + Oid relid; /* OID of the relation */ + Bitmapset *notnullattnums; /* attnums of NOT NULL columns */ +} NotnullHashEntry; static void get_relation_foreign_keys(PlannerInfo *root, RelOptInfo *rel, @@ -71,7 +75,8 @@ static List *get_relation_constraints(PlannerInfo *root, bool include_partition); static List *build_index_tlist(PlannerInfo *root, IndexOptInfo *index, Relation heapRelation); -static List *get_relation_statistics(RelOptInfo *rel, Relation relation); +static List *get_relation_statistics(PlannerInfo *root, RelOptInfo *rel, + Relation relation); static void set_relation_partition_info(PlannerInfo *root, RelOptInfo *rel, Relation relation); static PartitionScheme find_partition_scheme(PlannerInfo *root, @@ -172,27 +177,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, * RangeTblEntry does get populated. */ if (!inhparent || relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - for (int i = 0; i < relation->rd_att->natts; i++) - { - CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i); - - Assert(attr->attnullability != ATTNULLABLE_UNKNOWN); - - if (attr->attnullability == ATTNULLABLE_VALID) - { - rel->notnullattnums = bms_add_member(rel->notnullattnums, - i + 1); - - /* - * Per RemoveAttributeById(), dropped columns will have their - * attnotnull unset, so we needn't check for dropped columns - * in the above condition. - */ - Assert(!attr->attisdropped); - } - } - } + rel->notnullattnums = find_relation_notnullatts(root, relationObjectId); /* * Estimate relation size --- unless it's an inheritance parent, in which @@ -243,7 +228,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, Oid indexoid = lfirst_oid(l); Relation indexRelation; Form_pg_index index; - IndexAmRoutine *amroutine = NULL; + const IndexAmRoutine *amroutine = NULL; IndexOptInfo *info; int ncolumns, nkeycolumns; @@ -291,11 +276,11 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, info->ncolumns = ncolumns = index->indnatts; info->nkeycolumns = nkeycolumns = index->indnkeyatts; - info->indexkeys = (int *) palloc(sizeof(int) * ncolumns); - info->indexcollations = (Oid *) palloc(sizeof(Oid) * nkeycolumns); - info->opfamily = (Oid *) palloc(sizeof(Oid) * nkeycolumns); - info->opcintype = (Oid *) palloc(sizeof(Oid) * nkeycolumns); - info->canreturn = (bool *) palloc(sizeof(bool) * ncolumns); + info->indexkeys = palloc_array(int, ncolumns); + info->indexcollations = palloc_array(Oid, nkeycolumns); + info->opfamily = palloc_array(Oid, nkeycolumns); + info->opcintype = palloc_array(Oid, nkeycolumns); + info->canreturn = palloc_array(bool, ncolumns); for (i = 0; i < ncolumns; i++) { @@ -348,8 +333,8 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, Assert(amroutine->amcanorder); info->sortopfamily = info->opfamily; - info->reverse_sort = (bool *) palloc(sizeof(bool) * nkeycolumns); - info->nulls_first = (bool *) palloc(sizeof(bool) * nkeycolumns); + info->reverse_sort = palloc_array(bool, nkeycolumns); + info->nulls_first = palloc_array(bool, nkeycolumns); for (i = 0; i < nkeycolumns; i++) { @@ -372,9 +357,9 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, * corresponding btree opfamily for each opfamily of the * other index type. */ - info->sortopfamily = (Oid *) palloc(sizeof(Oid) * nkeycolumns); - info->reverse_sort = (bool *) palloc(sizeof(bool) * nkeycolumns); - info->nulls_first = (bool *) palloc(sizeof(bool) * nkeycolumns); + info->sortopfamily = palloc_array(Oid, nkeycolumns); + info->reverse_sort = palloc_array(bool, nkeycolumns); + info->nulls_first = palloc_array(bool, nkeycolumns); for (i = 0; i < nkeycolumns; i++) { @@ -441,13 +426,32 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, * modify the copies we obtain from the relcache to have the * correct varno for the parent relation, so that they match up * correctly against qual clauses. + * + * After fixing the varnos, we need to run the index expressions + * and predicate through const-simplification again, using a valid + * "root". This ensures that NullTest quals for Vars can be + * properly reduced. */ info->indexprs = RelationGetIndexExpressions(indexRelation); info->indpred = RelationGetIndexPredicate(indexRelation); - if (info->indexprs && varno != 1) - ChangeVarNodes((Node *) info->indexprs, 1, varno, 0); - if (info->indpred && varno != 1) - ChangeVarNodes((Node *) info->indpred, 1, varno, 0); + if (info->indexprs) + { + if (varno != 1) + ChangeVarNodes((Node *) info->indexprs, 1, varno, 0); + + info->indexprs = (List *) + eval_const_expressions(root, (Node *) info->indexprs); + } + if (info->indpred) + { + if (varno != 1) + ChangeVarNodes((Node *) info->indpred, 1, varno, 0); + + info->indpred = (List *) + eval_const_expressions(root, + (Node *) make_ands_explicit(info->indpred)); + info->indpred = make_ands_implicit((Expr *) info->indpred); + } /* Build targetlist using the completed indexprs data */ info->indextlist = build_index_tlist(root, info, relation); @@ -522,7 +526,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, rel->indexlist = indexinfos; - rel->statlist = get_relation_statistics(rel, relation); + rel->statlist = get_relation_statistics(root, rel, relation); /* Grab foreign-table info using the relcache, while we have it */ if (relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE) @@ -564,14 +568,6 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, set_relation_partition_info(root, rel, relation); table_close(relation, NoLock); - - /* - * Allow a plugin to editorialize on the info we obtained from the - * catalogs. Actions might include altering the assumed relation size, - * removing an index, or adding a hypothetical index to the indexlist. - */ - if (get_relation_info_hook) - (*get_relation_info_hook) (root, relationObjectId, inhparent, rel); } /* @@ -683,6 +679,105 @@ get_relation_foreign_keys(PlannerInfo *root, RelOptInfo *rel, } } +/* + * get_relation_notnullatts - + * Retrieves column not-null constraint information for a given relation. + * + * We do this while we have the relcache entry open, and store the column + * not-null constraint information in a hash table based on the relation OID. + */ +void +get_relation_notnullatts(PlannerInfo *root, Relation relation) +{ + Oid relid = RelationGetRelid(relation); + NotnullHashEntry *hentry; + bool found; + Bitmapset *notnullattnums = NULL; + + /* bail out if the relation has no not-null constraints */ + if (relation->rd_att->constr == NULL || + !relation->rd_att->constr->has_not_null) + return; + + /* create the hash table if it hasn't been created yet */ + if (root->glob->rel_notnullatts_hash == NULL) + { + HTAB *hashtab; + HASHCTL hash_ctl; + + hash_ctl.keysize = sizeof(Oid); + hash_ctl.entrysize = sizeof(NotnullHashEntry); + hash_ctl.hcxt = CurrentMemoryContext; + + hashtab = hash_create("Relation NOT NULL attnums", + 64L, /* arbitrary initial size */ + &hash_ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + root->glob->rel_notnullatts_hash = hashtab; + } + + /* + * Create a hash entry for this relation OID, if we don't have one + * already. + */ + hentry = (NotnullHashEntry *) hash_search(root->glob->rel_notnullatts_hash, + &relid, + HASH_ENTER, + &found); + + /* bail out if a hash entry already exists for this relation OID */ + if (found) + return; + + /* collect the column not-null constraint information for this relation */ + for (int i = 0; i < relation->rd_att->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i); + + Assert(attr->attnullability != ATTNULLABLE_UNKNOWN); + + if (attr->attnullability == ATTNULLABLE_VALID) + { + notnullattnums = bms_add_member(notnullattnums, i + 1); + + /* + * Per RemoveAttributeById(), dropped columns will have their + * attnotnull unset, so we needn't check for dropped columns in + * the above condition. + */ + Assert(!attr->attisdropped); + } + } + + /* ... and initialize the new hash entry */ + hentry->notnullattnums = notnullattnums; +} + +/* + * find_relation_notnullatts - + * Searches the hash table and returns the column not-null constraint + * information for a given relation. + */ +Bitmapset * +find_relation_notnullatts(PlannerInfo *root, Oid relid) +{ + NotnullHashEntry *hentry; + bool found; + + if (root->glob->rel_notnullatts_hash == NULL) + return NULL; + + hentry = (NotnullHashEntry *) hash_search(root->glob->rel_notnullatts_hash, + &relid, + HASH_FIND, + &found); + if (!found) + return NULL; + + return hentry->notnullattnums; +} + /* * infer_arbiter_indexes - * Determine the unique indexes used to arbitrate speculative insertion. @@ -714,20 +809,27 @@ infer_arbiter_indexes(PlannerInfo *root) Relation relation; Oid indexOidFromConstraint = InvalidOid; List *indexList; - ListCell *l; + List *indexRelList = NIL; - /* Normalized inference attributes and inference expressions: */ + /* + * Required attributes and expressions used to match indexes to the clause + * given by the user. In the ON CONFLICT ON CONSTRAINT case, we compute + * these from that constraint's index to match all other indexes, to + * account for the case where that index is being concurrently reindexed. + */ + List *inferIndexExprs = (List *) onconflict->arbiterWhere; Bitmapset *inferAttrs = NULL; List *inferElems = NIL; /* Results */ List *results = NIL; + bool foundValid = false; /* * Quickly return NIL for ON CONFLICT DO NOTHING without an inference - * specification or named constraint. ON CONFLICT DO UPDATE statements - * must always provide one or the other (but parser ought to have caught - * that already). + * specification or named constraint. ON CONFLICT DO SELECT/UPDATE + * statements must always provide one or the other (but parser ought to + * have caught that already). */ if (onconflict->arbiterElems == NIL && onconflict->constraint == InvalidOid) @@ -748,12 +850,14 @@ infer_arbiter_indexes(PlannerInfo *root) * well as a separate list of expression items. This simplifies matching * the cataloged definition of indexes. */ - foreach(l, onconflict->arbiterElems) + foreach_ptr(InferenceElem, elem, onconflict->arbiterElems) { - InferenceElem *elem = (InferenceElem *) lfirst(l); Var *var; int attno; + /* we cannot also have a constraint name, per grammar */ + Assert(!OidIsValid(onconflict->constraint)); + if (!IsA(elem->expr, Var)) { /* If not a plain Var, just shove it in inferElems for now */ @@ -774,49 +878,124 @@ infer_arbiter_indexes(PlannerInfo *root) } /* - * Lookup named constraint's index. This is not immediately returned - * because some additional sanity checks are required. + * Next, open all the indexes. We need this list for two things: first, + * if an ON CONSTRAINT clause was given, and that constraint's index is + * undergoing REINDEX CONCURRENTLY, then we need to consider all matches + * for that index. Second, if an attribute list was specified in the ON + * CONFLICT clause, we use the list to find the indexes whose attributes + * match that list. + */ + indexList = RelationGetIndexList(relation); + foreach_oid(indexoid, indexList) + { + Relation idxRel; + + /* obtain the same lock type that the executor will ultimately use */ + idxRel = index_open(indexoid, rte->rellockmode); + indexRelList = lappend(indexRelList, idxRel); + } + + /* + * If a constraint was named in the command, look up its index. We don't + * return it immediately because we need some additional sanity checks, + * and also because we need to include other indexes as arbiters to + * account for REINDEX CONCURRENTLY processing it. */ if (onconflict->constraint != InvalidOid) { - indexOidFromConstraint = get_constraint_index(onconflict->constraint); + /* we cannot also have an explicit list of elements, per grammar */ + Assert(onconflict->arbiterElems == NIL); + indexOidFromConstraint = get_constraint_index(onconflict->constraint); if (indexOidFromConstraint == InvalidOid) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("constraint in ON CONFLICT clause has no associated index"))); + + /* + * Find the named constraint index to extract its attributes and + * predicates. + */ + foreach_ptr(RelationData, idxRel, indexRelList) + { + Form_pg_index idxForm = idxRel->rd_index; + + if (indexOidFromConstraint == idxForm->indexrelid) + { + /* Found it. */ + Assert(idxForm->indisready); + + /* + * Set up inferElems and inferIndexExprs to match the + * constraint index, so that we can match them in the loop + * below. + */ + for (int natt = 0; natt < idxForm->indnkeyatts; natt++) + { + int attno; + + attno = idxRel->rd_index->indkey.values[natt]; + if (attno != InvalidAttrNumber) + inferAttrs = + bms_add_member(inferAttrs, + attno - FirstLowInvalidHeapAttributeNumber); + } + + inferElems = RelationGetIndexExpressions(idxRel); + inferIndexExprs = RelationGetIndexPredicate(idxRel); + break; + } + } } /* * Using that representation, iterate through the list of indexes on the - * target relation to try and find a match + * target relation to find matches. */ - indexList = RelationGetIndexList(relation); - - foreach(l, indexList) + foreach_ptr(RelationData, idxRel, indexRelList) { - Oid indexoid = lfirst_oid(l); - Relation idxRel; Form_pg_index idxForm; Bitmapset *indexedAttrs; List *idxExprs; List *predExprs; AttrNumber natt; - ListCell *el; + bool match; /* - * Extract info from the relation descriptor for the index. Obtain - * the same lock type that the executor will ultimately use. + * Extract info from the relation descriptor for the index. * * Let executor complain about !indimmediate case directly, because * enforcement needs to occur there anyway when an inference clause is * omitted. */ - idxRel = index_open(indexoid, rte->rellockmode); idxForm = idxRel->rd_index; - if (!idxForm->indisvalid) - goto next; + /* + * Ignore indexes that aren't indisready, because we cannot trust + * their catalog structure yet. However, if any indexes are marked + * indisready but not yet indisvalid, we still consider them, because + * they might turn valid while we're running. Doing it this way + * allows a concurrent transaction with a slightly later catalog + * snapshot infer the same set of indexes, which is critical to + * prevent spurious 'duplicate key' errors. + * + * However, another critical aspect is that a unique index that isn't + * yet marked indisvalid=true might not be complete yet, meaning it + * wouldn't detect possible duplicate rows. In order to prevent false + * negatives, we require that we include in the set of inferred + * indexes at least one index that is marked valid. + */ + if (!idxForm->indisready) + continue; + + /* + * Ignore invalid indexes for partitioned tables. It's possible that + * some partitions don't have the index (yet), and then we would not + * find a match during ExecInitPartitionInfo. + */ + if (relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE && + !idxForm->indisvalid) + continue; /* * Note that we do not perform a check against indcheckxmin (like e.g. @@ -826,42 +1005,56 @@ infer_arbiter_indexes(PlannerInfo *root) */ /* - * Look for match on "ON constraint_name" variant, which may not be + * Look for match for "ON constraint_name" variant, which may not be a * unique constraint. This can only be a constraint name. */ if (indexOidFromConstraint == idxForm->indexrelid) { - if (idxForm->indisexclusion && onconflict->action == ONCONFLICT_UPDATE) + /* + * ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT are not + * supported with exclusion constraints. + */ + if (idxForm->indisexclusion && + (onconflict->action == ONCONFLICT_UPDATE || + onconflict->action == ONCONFLICT_SELECT)) ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("ON CONFLICT DO UPDATE not supported with exclusion constraints"))); + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("ON CONFLICT DO %s not supported with exclusion constraints", + onconflict->action == ONCONFLICT_UPDATE ? "UPDATE" : "SELECT")); + /* Consider this one a match already */ results = lappend_oid(results, idxForm->indexrelid); - list_free(indexList); - index_close(idxRel, NoLock); - table_close(relation, NoLock); - return results; + foundValid |= idxForm->indisvalid; + continue; } else if (indexOidFromConstraint != InvalidOid) { - /* No point in further work for index in named constraint case */ - goto next; + /* + * In the case of "ON constraint_name DO SELECT/UPDATE" we need to + * skip non-unique candidates. + */ + if (!idxForm->indisunique && + (onconflict->action == ONCONFLICT_UPDATE || + onconflict->action == ONCONFLICT_SELECT)) + continue; + } + else + { + /* + * Only considering conventional inference at this point (not + * named constraints), so index under consideration can be + * immediately skipped if it's not unique. + */ + if (!idxForm->indisunique) + continue; } - - /* - * Only considering conventional inference at this point (not named - * constraints), so index under consideration can be immediately - * skipped if it's not unique - */ - if (!idxForm->indisunique) - goto next; /* * So-called unique constraints with WITHOUT OVERLAPS are really * exclusion constraints, so skip those too. */ if (idxForm->indisexclusion) - goto next; + continue; /* Build BMS representation of plain (non expression) index attrs */ indexedAttrs = NULL; @@ -876,17 +1069,25 @@ infer_arbiter_indexes(PlannerInfo *root) /* Non-expression attributes (if any) must match */ if (!bms_equal(indexedAttrs, inferAttrs)) - goto next; + continue; /* Expression attributes (if any) must match */ idxExprs = RelationGetIndexExpressions(idxRel); - if (idxExprs && varno != 1) - ChangeVarNodes((Node *) idxExprs, 1, varno, 0); - - foreach(el, onconflict->arbiterElems) + if (idxExprs) { - InferenceElem *elem = (InferenceElem *) lfirst(el); + if (varno != 1) + ChangeVarNodes((Node *) idxExprs, 1, varno, 0); + idxExprs = (List *) eval_const_expressions(root, (Node *) idxExprs); + } + + /* + * If arbiterElems are present, check them. (Note that if a + * constraint name was given in the command line, this list is NIL.) + */ + match = true; + foreach_ptr(InferenceElem, elem, onconflict->arbiterElems) + { /* * Ensure that collation/opclass aspects of inference expression * element match. Even though this loop is primarily concerned @@ -895,7 +1096,10 @@ infer_arbiter_indexes(PlannerInfo *root) * attributes appearing as inference elements. */ if (!infer_collation_opclass_match(elem, idxRel, idxExprs)) - goto next; + { + match = false; + break; + } /* * Plain Vars don't factor into count of expression elements, and @@ -916,39 +1120,71 @@ infer_arbiter_indexes(PlannerInfo *root) list_member(idxExprs, elem->expr)) continue; - goto next; + match = false; + break; } + if (!match) + continue; /* - * Now that all inference elements were matched, ensure that the + * In case of inference from an attribute list, ensure that the * expression elements from inference clause are not missing any * cataloged expressions. This does the right thing when unique * indexes redundantly repeat the same attribute, or if attributes * redundantly appear multiple times within an inference clause. + * + * In case a constraint was named, ensure the candidate has an equal + * set of expressions as the named constraint's index. */ if (list_difference(idxExprs, inferElems) != NIL) - goto next; + continue; - /* - * If it's a partial index, its predicate must be implied by the ON - * CONFLICT's WHERE clause. - */ predExprs = RelationGetIndexPredicate(idxRel); - if (predExprs && varno != 1) - ChangeVarNodes((Node *) predExprs, 1, varno, 0); + if (predExprs) + { + if (varno != 1) + ChangeVarNodes((Node *) predExprs, 1, varno, 0); - if (!predicate_implied_by(predExprs, (List *) onconflict->arbiterWhere, false)) - goto next; + predExprs = (List *) + eval_const_expressions(root, + (Node *) make_ands_explicit(predExprs)); + predExprs = make_ands_implicit((Expr *) predExprs); + } + /* + * Partial indexes affect each form of ON CONFLICT differently: if a + * constraint was named, then the predicates must be identical. In + * conventional inference, the index's predicate must be implied by + * the WHERE clause. + */ + if (OidIsValid(indexOidFromConstraint)) + { + if (list_difference(predExprs, inferIndexExprs) != NIL) + continue; + } + else + { + if (!predicate_implied_by(predExprs, inferIndexExprs, false)) + continue; + } + + /* All good -- consider this index a match */ results = lappend_oid(results, idxForm->indexrelid); -next: + foundValid |= idxForm->indisvalid; + } + + /* Close all indexes */ + foreach_ptr(RelationData, idxRel, indexRelList) + { index_close(idxRel, NoLock); } list_free(indexList); + list_free(indexRelList); table_close(relation, NoLock); - if (results == NIL) + /* We require at least one indisvalid index */ + if (results == NIL || !foundValid) ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), errmsg("there is no unique or exclusion constraint matching the ON CONFLICT specification"))); @@ -1321,6 +1557,14 @@ get_relation_constraints(PlannerInfo *root, cexpr = stringToNode(constr->check[i].ccbin); + /* + * Fix Vars to have the desired varno. This must be done before + * const-simplification because eval_const_expressions reduces + * NullTest for Vars based on varno. + */ + if (varno != 1) + ChangeVarNodes(cexpr, 1, varno, 0); + /* * Run each expression through const-simplification and * canonicalization. This is not just an optimization, but is @@ -1335,10 +1579,6 @@ get_relation_constraints(PlannerInfo *root, cexpr = (Node *) canonicalize_qual((Expr *) cexpr, true); - /* Fix Vars to have the desired varno */ - if (varno != 1) - ChangeVarNodes(cexpr, 1, varno, 0); - /* * Finally, convert to implicit-AND format (that is, a List) and * append the resulting item(s) to our output list. @@ -1392,6 +1632,14 @@ get_relation_constraints(PlannerInfo *root, result = list_concat(result, rel->partition_qual); } + /* + * Expand virtual generated columns in the constraint expressions. + */ + if (result) + result = (List *) expand_generated_columns_in_expr((Node *) result, + relation, + varno); + table_close(relation, NoLock); return result; @@ -1487,7 +1735,8 @@ get_relation_statistics_worker(List **stainfos, RelOptInfo *rel, * just the identifying metadata. Only stats actually built are considered. */ static List * -get_relation_statistics(RelOptInfo *rel, Relation relation) +get_relation_statistics(PlannerInfo *root, RelOptInfo *rel, + Relation relation) { Index varno = rel->relid; List *statoidlist; @@ -1519,8 +1768,8 @@ get_relation_statistics(RelOptInfo *rel, Relation relation) keys = bms_add_member(keys, staForm->stxkeys.values[i]); /* - * Preprocess expressions (if any). We read the expressions, run them - * through eval_const_expressions, and fix the varnos. + * Preprocess expressions (if any). We read the expressions, fix the + * varnos, and run them through eval_const_expressions. * * XXX We don't know yet if there are any data for this stats object, * with either stxdinherit value. But it's reasonable to assume there @@ -1543,6 +1792,21 @@ get_relation_statistics(RelOptInfo *rel, Relation relation) exprs = (List *) stringToNode(exprsString); pfree(exprsString); + /* Expand virtual generated columns in the expressions */ + exprs = (List *) expand_generated_columns_in_expr((Node *) exprs, relation, 1); + + /* + * Modify the copies we obtain from the relcache to have the + * correct varno for the parent relation, so that they match + * up correctly against qual clauses. + * + * This must be done before const-simplification because + * eval_const_expressions reduces NullTest for Vars based on + * varno. + */ + if (varno != 1) + ChangeVarNodes((Node *) exprs, 1, varno, 0); + /* * Run the expressions through eval_const_expressions. This is * not just an optimization, but is necessary, because the @@ -1551,18 +1815,10 @@ get_relation_statistics(RelOptInfo *rel, Relation relation) * We must not use canonicalize_qual, however, since these * aren't qual expressions. */ - exprs = (List *) eval_const_expressions(NULL, (Node *) exprs); + exprs = (List *) eval_const_expressions(root, (Node *) exprs); /* May as well fix opfuncids too */ fix_opfuncids((Node *) exprs); - - /* - * Modify the copies we obtain from the relcache to have the - * correct varno for the parent relation, so that they match - * up correctly against qual clauses. - */ - if (varno != 1) - ChangeVarNodes((Node *) exprs, 1, varno, 0); } } @@ -2039,9 +2295,8 @@ join_selectivity(PlannerInfo *root, /* * function_selectivity * - * Returns the selectivity of a specified boolean function clause. - * This code executes registered procedures stored in the - * pg_proc relation, by calling the function manager. + * Attempt to estimate the selectivity of a specified boolean function clause + * by asking its support function. If the function lacks support, return -1. * * See clause_selectivity() for the meaning of the additional parameters. */ @@ -2059,15 +2314,8 @@ function_selectivity(PlannerInfo *root, SupportRequestSelectivity req; SupportRequestSelectivity *sresult; - /* - * If no support function is provided, use our historical default - * estimate, 0.3333333. This seems a pretty unprincipled choice, but - * Postgres has been using that estimate for function calls since 1992. - * The hoariness of this behavior suggests that we should not be in too - * much hurry to use another value. - */ if (!prosupport) - return (Selectivity) 0.3333333; + return (Selectivity) -1; /* no support function */ req.type = T_SupportRequestSelectivity; req.root = root; @@ -2084,9 +2332,8 @@ function_selectivity(PlannerInfo *root, DatumGetPointer(OidFunctionCall1(prosupport, PointerGetDatum(&req))); - /* If support function fails, use default */ if (sresult != &req) - return (Selectivity) 0.3333333; + return (Selectivity) -1; /* function did not honor request */ if (req.selectivity < 0.0 || req.selectivity > 1.0) elog(ERROR, "invalid function selectivity: %f", req.selectivity); @@ -2303,6 +2550,60 @@ has_row_triggers(PlannerInfo *root, Index rti, CmdType event) return result; } +/* + * has_transition_tables + * + * Detect whether the specified relation has any transition tables for event. + */ +bool +has_transition_tables(PlannerInfo *root, Index rti, CmdType event) +{ + RangeTblEntry *rte = planner_rt_fetch(rti, root); + Relation relation; + TriggerDesc *trigDesc; + bool result = false; + + Assert(rte->rtekind == RTE_RELATION); + + /* Currently foreign tables cannot have transition tables */ + if (rte->relkind == RELKIND_FOREIGN_TABLE) + return result; + + /* Assume we already have adequate lock */ + relation = table_open(rte->relid, NoLock); + + trigDesc = relation->trigdesc; + switch (event) + { + case CMD_INSERT: + if (trigDesc && + trigDesc->trig_insert_new_table) + result = true; + break; + case CMD_UPDATE: + if (trigDesc && + (trigDesc->trig_update_old_table || + trigDesc->trig_update_new_table)) + result = true; + break; + case CMD_DELETE: + if (trigDesc && + trigDesc->trig_delete_old_table) + result = true; + break; + /* There is no separate event for MERGE, only INSERT/UPDATE/DELETE */ + case CMD_MERGE: + result = false; + break; + default: + elog(ERROR, "unrecognized CmdType: %d", (int) event); + break; + } + + table_close(relation, NoLock); + return result; +} + /* * has_stored_generated_columns * @@ -2360,7 +2661,7 @@ get_dependent_generated_columns(PlannerInfo *root, Index rti, Bitmapset *attrs_used = NULL; /* skip if not generated column */ - if (!TupleDescAttr(tupdesc, defval->adnum - 1)->attgenerated) + if (!TupleDescCompactAttr(tupdesc, defval->adnum - 1)->attgenerated) continue; /* identify columns this generated column depends on */ @@ -2478,32 +2779,31 @@ find_partition_scheme(PlannerInfo *root, Relation relation) * array since the relcache entry may not survive after we have closed the * relation. */ - part_scheme = (PartitionScheme) palloc0(sizeof(PartitionSchemeData)); + part_scheme = palloc0_object(PartitionSchemeData); part_scheme->strategy = partkey->strategy; part_scheme->partnatts = partkey->partnatts; - part_scheme->partopfamily = (Oid *) palloc(sizeof(Oid) * partnatts); + part_scheme->partopfamily = palloc_array(Oid, partnatts); memcpy(part_scheme->partopfamily, partkey->partopfamily, sizeof(Oid) * partnatts); - part_scheme->partopcintype = (Oid *) palloc(sizeof(Oid) * partnatts); + part_scheme->partopcintype = palloc_array(Oid, partnatts); memcpy(part_scheme->partopcintype, partkey->partopcintype, sizeof(Oid) * partnatts); - part_scheme->partcollation = (Oid *) palloc(sizeof(Oid) * partnatts); + part_scheme->partcollation = palloc_array(Oid, partnatts); memcpy(part_scheme->partcollation, partkey->partcollation, sizeof(Oid) * partnatts); - part_scheme->parttyplen = (int16 *) palloc(sizeof(int16) * partnatts); + part_scheme->parttyplen = palloc_array(int16, partnatts); memcpy(part_scheme->parttyplen, partkey->parttyplen, sizeof(int16) * partnatts); - part_scheme->parttypbyval = (bool *) palloc(sizeof(bool) * partnatts); + part_scheme->parttypbyval = palloc_array(bool, partnatts); memcpy(part_scheme->parttypbyval, partkey->parttypbyval, sizeof(bool) * partnatts); - part_scheme->partsupfunc = (FmgrInfo *) - palloc(sizeof(FmgrInfo) * partnatts); + part_scheme->partsupfunc = palloc_array(FmgrInfo, partnatts); for (i = 0; i < partnatts; i++) fmgr_info_copy(&part_scheme->partsupfunc[i], &partkey->partsupfunc[i], CurrentMemoryContext); @@ -2537,7 +2837,7 @@ set_baserel_partition_key_exprs(Relation relation, Assert(partkey != NULL); partnatts = partkey->partnatts; - partexprs = (List **) palloc(sizeof(List *) * partnatts); + partexprs = palloc_array(List *, partnatts); lc = list_head(partkey->partexprs); for (cnt = 0; cnt < partnatts; cnt++) @@ -2578,7 +2878,7 @@ set_baserel_partition_key_exprs(Relation relation, * expression lists to keep partition key expression handling code simple. * See build_joinrel_partition_info() and match_expr_to_partition_keys(). */ - rel->nullable_partexprs = (List **) palloc0(sizeof(List *) * partnatts); + rel->nullable_partexprs = palloc0_array(List *, partnatts); } /* diff --git a/src/backend/optimizer/util/predtest.c b/src/backend/optimizer/util/predtest.c index ac28573cd0a5a..690a23d619aab 100644 --- a/src/backend/optimizer/util/predtest.c +++ b/src/backend/optimizer/util/predtest.c @@ -4,7 +4,7 @@ * Routines to attempt to prove logical implications between predicate * expressions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "nodes/pathnodes.h" #include "optimizer/optimizer.h" #include "utils/array.h" +#include "utils/hsearch.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/syscache.h" @@ -109,7 +110,8 @@ static bool operator_same_subexprs_proof(Oid pred_op, Oid clause_op, static bool operator_same_subexprs_lookup(Oid pred_op, Oid clause_op, bool refute_it); static Oid get_btree_test_op(Oid pred_op, Oid clause_op, bool refute_it); -static void InvalidateOprProofCacheCallBack(Datum arg, int cacheid, uint32 hashvalue); +static void InvalidateOprProofCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); /* @@ -967,7 +969,7 @@ arrayconst_startup_fn(Node *clause, PredIterInfo info) char elmalign; /* Create working state struct */ - state = (ArrayConstIterState *) palloc(sizeof(ArrayConstIterState)); + state = palloc_object(ArrayConstIterState); info->state = state; /* Deconstruct the array literal */ @@ -1046,7 +1048,7 @@ arrayexpr_startup_fn(Node *clause, PredIterInfo info) ArrayExpr *arrayexpr; /* Create working state struct */ - state = (ArrayExprIterState *) palloc(sizeof(ArrayExprIterState)); + state = palloc_object(ArrayExprIterState); info->state = state; /* Set up a dummy OpExpr to return as the per-item node */ @@ -2343,7 +2345,8 @@ get_btree_test_op(Oid pred_op, Oid clause_op, bool refute_it) * Callback for pg_amop inval events */ static void -InvalidateOprProofCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) +InvalidateOprProofCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { HASH_SEQ_STATUS status; OprProofCacheEntry *hentry; diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index ff507331a061a..3fc2c2f71d008 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -3,7 +3,7 @@ * relnode.c * Relation-node lookup/construction routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,8 @@ #include +#include "access/nbtree.h" +#include "catalog/pg_constraint.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/appendinfo.h" @@ -27,12 +29,16 @@ #include "optimizer/paths.h" #include "optimizer/placeholder.h" #include "optimizer/plancat.h" +#include "optimizer/planner.h" #include "optimizer/restrictinfo.h" #include "optimizer/tlist.h" +#include "parser/parse_oper.h" #include "parser/parse_relation.h" #include "rewrite/rewriteManip.h" #include "utils/hsearch.h" #include "utils/lsyscache.h" +#include "utils/selfuncs.h" +#include "utils/typcache.h" typedef struct JoinHashEntry @@ -41,6 +47,12 @@ typedef struct JoinHashEntry RelOptInfo *join_rel; } JoinHashEntry; +/* Hook for plugins to get control in build_simple_rel() */ +build_simple_rel_hook_type build_simple_rel_hook = NULL; + +/* Hook for plugins to get control during joinrel setup */ +joinrel_setup_hook_type joinrel_setup_hook = NULL; + static void build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *input_rel, SpecialJoinInfo *sjinfo, @@ -83,6 +95,14 @@ static void build_child_join_reltarget(PlannerInfo *root, RelOptInfo *childrel, int nappinfos, AppendRelInfo **appinfos); +static bool eager_aggregation_possible_for_relation(PlannerInfo *root, + RelOptInfo *rel); +static bool init_grouping_targets(PlannerInfo *root, RelOptInfo *rel, + PathTarget *target, PathTarget *agg_input, + List **group_clauses, List **group_exprs); +static bool is_var_in_aggref_only(PlannerInfo *root, Var *var); +static bool is_var_needed_by_join(PlannerInfo *root, Var *var, RelOptInfo *rel); +static Index get_expression_sortgroupref(PlannerInfo *root, Expr *expr); /* @@ -106,11 +126,11 @@ setup_simple_rel_arrays(PlannerInfo *root) * exist yet. It'll be filled by later calls to build_simple_rel(). */ root->simple_rel_array = (RelOptInfo **) - palloc0(size * sizeof(RelOptInfo *)); + palloc0_array(RelOptInfo *, size); /* simple_rte_array is an array equivalent of the rtable list */ root->simple_rte_array = (RangeTblEntry **) - palloc0(size * sizeof(RangeTblEntry *)); + palloc0_array(RangeTblEntry *, size); rti = 1; foreach(lc, root->parse->rtable) { @@ -127,7 +147,7 @@ setup_simple_rel_arrays(PlannerInfo *root) } root->append_rel_array = (AppendRelInfo **) - palloc0(size * sizeof(AppendRelInfo *)); + palloc0_array(AppendRelInfo *, size); /* * append_rel_array is filled with any already-existing AppendRelInfos, @@ -211,13 +231,13 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) rel->consider_startup = (root->tuple_fraction > 0); rel->consider_param_startup = false; /* might get changed later */ rel->consider_parallel = false; /* might get changed later */ + rel->pgs_mask = root->glob->default_pgs_mask; rel->reltarget = create_empty_pathtarget(); rel->pathlist = NIL; rel->ppilist = NIL; rel->partial_pathlist = NIL; rel->cheapest_startup_path = NULL; rel->cheapest_total_path = NULL; - rel->cheapest_unique_path = NULL; rel->cheapest_parameterized_paths = NIL; rel->relid = relid; rel->rtekind = rte->rtekind; @@ -269,6 +289,9 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) rel->fdw_private = NULL; rel->unique_for_rels = NIL; rel->non_unique_for_rels = NIL; + rel->unique_rel = NULL; + rel->unique_pathkeys = NIL; + rel->unique_groupclause = NIL; rel->baserestrictinfo = NIL; rel->baserestrictcost.startup = 0; rel->baserestrictcost.per_tuple = 0; @@ -276,6 +299,8 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) rel->joininfo = NIL; rel->has_eclass_joins = false; rel->consider_partitionwise_join = false; /* might get changed later */ + rel->agg_info = NULL; + rel->grouped_rel = NULL; rel->part_scheme = NULL; rel->nparts = -1; rel->boundinfo = NULL; @@ -355,9 +380,9 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) rel->min_attr = 0; rel->max_attr = list_length(rte->eref->colnames); rel->attr_needed = (Relids *) - palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(Relids)); + palloc0_array(Relids, rel->max_attr - rel->min_attr + 1); rel->attr_widths = (int32 *) - palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(int32)); + palloc0_array(int32, rel->max_attr - rel->min_attr + 1); break; case RTE_RESULT: /* RTE_RESULT has no columns, nor could it have whole-row Var */ @@ -373,20 +398,24 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) } /* - * We must apply the partially filled in RelOptInfo before calling - * apply_child_basequals due to some transformations within that function - * which require the RelOptInfo to be available in the simple_rel_array. + * Allow a plugin to editorialize on the new RelOptInfo. This could + * involve editorializing on the information which get_relation_info + * obtained from the catalogs, such as altering the assumed relation size, + * removing an index, or adding a hypothetical index to the indexlist. + * + * An extension can also modify rel->pgs_mask here to control path + * generation. */ - root->simple_rel_array[relid] = rel; + if (build_simple_rel_hook) + (*build_simple_rel_hook) (root, rel, rte); /* * Apply the parent's quals to the child, with appropriate substitution of - * variables. If the resulting clause is constant-FALSE or NULL after - * applying transformations, apply_child_basequals returns false to - * indicate that scanning this relation won't yield any rows. In this - * case, we mark the child as dummy right away. (We must do this - * immediately so that pruning works correctly when recursing in - * expand_partitioned_rtentry.) + * variables. If any resulting clause is reduced to constant FALSE or + * NULL, apply_child_basequals returns false to indicate that scanning + * this relation won't yield any rows. In this case, we mark the child as + * dummy right away. (We must do this immediately so that pruning works + * correctly when recursing in expand_partitioned_rtentry.) */ if (parent) { @@ -396,16 +425,117 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent) if (!apply_child_basequals(root, parent, rel, rte, appinfo)) { /* - * Restriction clause reduced to constant FALSE or NULL. Mark as - * dummy so we won't scan this relation. + * A restriction clause reduced to constant FALSE or NULL after + * substitution. Mark the child as dummy so that it need not be + * scanned. */ mark_dummy_rel(rel); } } + /* Save the finished struct in the query's simple_rel_array */ + root->simple_rel_array[relid] = rel; + return rel; } +/* + * build_simple_grouped_rel + * Construct a new RelOptInfo representing a grouped version of the input + * simple relation. + */ +RelOptInfo * +build_simple_grouped_rel(PlannerInfo *root, RelOptInfo *rel) +{ + RelOptInfo *grouped_rel; + RelAggInfo *agg_info; + + /* + * We should have available aggregate expressions and grouping + * expressions, otherwise we cannot reach here. + */ + Assert(root->agg_clause_list != NIL); + Assert(root->group_expr_list != NIL); + + /* nothing to do for dummy rel */ + if (IS_DUMMY_REL(rel)) + return NULL; + + /* + * Prepare the information needed to create grouped paths for this simple + * relation. + */ + agg_info = create_rel_agg_info(root, rel, true); + if (agg_info == NULL) + return NULL; + + /* + * If grouped paths for the given simple relation are not considered + * useful, skip building the grouped relation. + */ + if (!agg_info->agg_useful) + return NULL; + + /* Track the set of relids at which partial aggregation is applied */ + agg_info->apply_agg_at = bms_copy(rel->relids); + + /* build the grouped relation */ + grouped_rel = build_grouped_rel(root, rel); + grouped_rel->reltarget = agg_info->target; + grouped_rel->rows = agg_info->grouped_rows; + grouped_rel->agg_info = agg_info; + + rel->grouped_rel = grouped_rel; + + return grouped_rel; +} + +/* + * build_grouped_rel + * Build a grouped relation by flat copying the input relation and resetting + * the necessary fields. + */ +RelOptInfo * +build_grouped_rel(PlannerInfo *root, RelOptInfo *rel) +{ + RelOptInfo *grouped_rel; + + grouped_rel = makeNode(RelOptInfo); + memcpy(grouped_rel, rel, sizeof(RelOptInfo)); + + /* + * clear path info + */ + grouped_rel->pathlist = NIL; + grouped_rel->ppilist = NIL; + grouped_rel->partial_pathlist = NIL; + grouped_rel->cheapest_startup_path = NULL; + grouped_rel->cheapest_total_path = NULL; + grouped_rel->cheapest_parameterized_paths = NIL; + + /* + * clear partition info + */ + grouped_rel->part_scheme = NULL; + grouped_rel->nparts = -1; + grouped_rel->boundinfo = NULL; + grouped_rel->partbounds_merged = false; + grouped_rel->partition_qual = NIL; + grouped_rel->part_rels = NULL; + grouped_rel->live_parts = NULL; + grouped_rel->all_partrels = NULL; + grouped_rel->partexprs = NULL; + grouped_rel->nullable_partexprs = NULL; + grouped_rel->consider_partitionwise_join = false; + + /* + * clear size estimates + */ + grouped_rel->rows = 0; + + return grouped_rel; +} + /* * find_base_rel * Find a base or otherrel relation entry, which must already exist. @@ -707,13 +837,13 @@ build_join_rel(PlannerInfo *root, joinrel->consider_startup = (root->tuple_fraction > 0); joinrel->consider_param_startup = false; joinrel->consider_parallel = false; + joinrel->pgs_mask = root->glob->default_pgs_mask; joinrel->reltarget = create_empty_pathtarget(); joinrel->pathlist = NIL; joinrel->ppilist = NIL; joinrel->partial_pathlist = NIL; joinrel->cheapest_startup_path = NULL; joinrel->cheapest_total_path = NULL; - joinrel->cheapest_unique_path = NULL; joinrel->cheapest_parameterized_paths = NIL; /* init direct_lateral_relids from children; we'll finish it up below */ joinrel->direct_lateral_relids = @@ -748,6 +878,9 @@ build_join_rel(PlannerInfo *root, joinrel->fdw_private = NULL; joinrel->unique_for_rels = NIL; joinrel->non_unique_for_rels = NIL; + joinrel->unique_rel = NULL; + joinrel->unique_pathkeys = NIL; + joinrel->unique_groupclause = NIL; joinrel->baserestrictinfo = NIL; joinrel->baserestrictcost.startup = 0; joinrel->baserestrictcost.per_tuple = 0; @@ -755,6 +888,8 @@ build_join_rel(PlannerInfo *root, joinrel->joininfo = NIL; joinrel->has_eclass_joins = false; joinrel->consider_partitionwise_join = false; /* might get changed later */ + joinrel->agg_info = NULL; + joinrel->grouped_rel = NULL; joinrel->parent = NULL; joinrel->top_parent = NULL; joinrel->top_parent_relids = NULL; @@ -815,10 +950,6 @@ build_join_rel(PlannerInfo *root, */ joinrel->has_eclass_joins = has_relevant_eclass_joinclause(root, joinrel); - /* Store the partition information. */ - build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo, - restrictlist); - /* * Set estimates of the joinrel's size. */ @@ -844,6 +975,19 @@ build_join_rel(PlannerInfo *root, is_parallel_safe(root, (Node *) joinrel->reltarget->exprs)) joinrel->consider_parallel = true; + /* + * Allow a plugin to editorialize on the new joinrel's properties. Actions + * might include altering the size estimate, clearing consider_parallel, + * or adjusting pgs_mask. + */ + if (joinrel_setup_hook) + (*joinrel_setup_hook) (root, joinrel, outer_rel, inner_rel, sjinfo, + restrictlist); + + /* Store the partition information. */ + build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo, + restrictlist); + /* Add the joinrel to the PlannerInfo. */ add_join_rel(root, joinrel); @@ -900,13 +1044,13 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, joinrel->consider_startup = (root->tuple_fraction > 0); joinrel->consider_param_startup = false; joinrel->consider_parallel = false; + joinrel->pgs_mask = root->glob->default_pgs_mask; joinrel->reltarget = create_empty_pathtarget(); joinrel->pathlist = NIL; joinrel->ppilist = NIL; joinrel->partial_pathlist = NIL; joinrel->cheapest_startup_path = NULL; joinrel->cheapest_total_path = NULL; - joinrel->cheapest_unique_path = NULL; joinrel->cheapest_parameterized_paths = NIL; joinrel->direct_lateral_relids = NULL; joinrel->lateral_relids = NULL; @@ -933,12 +1077,17 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, joinrel->useridiscurrent = false; joinrel->fdwroutine = NULL; joinrel->fdw_private = NULL; + joinrel->unique_rel = NULL; + joinrel->unique_pathkeys = NIL; + joinrel->unique_groupclause = NIL; joinrel->baserestrictinfo = NIL; joinrel->baserestrictcost.startup = 0; joinrel->baserestrictcost.per_tuple = 0; joinrel->joininfo = NIL; joinrel->has_eclass_joins = false; joinrel->consider_partitionwise_join = false; /* might get changed later */ + joinrel->agg_info = NULL; + joinrel->grouped_rel = NULL; joinrel->parent = parent_joinrel; joinrel->top_parent = parent_joinrel->top_parent ? parent_joinrel->top_parent : parent_joinrel; joinrel->top_parent_relids = joinrel->top_parent->relids; @@ -979,10 +1128,6 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, */ joinrel->has_eclass_joins = parent_joinrel->has_eclass_joins; - /* Is the join between partitions itself partitioned? */ - build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo, - restrictlist); - /* Child joinrel is parallel safe if parent is parallel safe. */ joinrel->consider_parallel = parent_joinrel->consider_parallel; @@ -990,6 +1135,20 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel, set_joinrel_size_estimates(root, joinrel, outer_rel, inner_rel, sjinfo, restrictlist); + /* + * Allow a plugin to editorialize on the new joinrel's properties. Actions + * might include altering the size estimate, clearing consider_parallel, + * or adjusting pgs_mask. (However, note that clearing consider_parallel + * would be better done in the parent joinrel rather than here.) + */ + if (joinrel_setup_hook) + (*joinrel_setup_hook) (root, joinrel, outer_rel, inner_rel, sjinfo, + restrictlist); + + /* Is the join between partitions itself partitioned? */ + build_joinrel_partition_info(root, joinrel, outer_rel, inner_rel, sjinfo, + restrictlist); + /* We build the join only once. */ Assert(!find_join_rel(root, joinrel->relids)); @@ -1479,6 +1638,7 @@ fetch_upper_rel(PlannerInfo *root, UpperRelationKind kind, Relids relids) upperrel = makeNode(RelOptInfo); upperrel->reloptkind = RELOPT_UPPER_REL; upperrel->relids = bms_copy(relids); + upperrel->pgs_mask = root->glob->default_pgs_mask; /* cheap startup cost is interesting iff not all tuples to be retrieved */ upperrel->consider_startup = (root->tuple_fraction > 0); @@ -1488,7 +1648,6 @@ fetch_upper_rel(PlannerInfo *root, UpperRelationKind kind, Relids relids) upperrel->pathlist = NIL; upperrel->cheapest_startup_path = NULL; upperrel->cheapest_total_path = NULL; - upperrel->cheapest_unique_path = NULL; upperrel->cheapest_parameterized_paths = NIL; root->upper_rels[kind] = lappend(root->upper_rels[kind], upperrel); @@ -1996,7 +2155,7 @@ build_joinrel_partition_info(PlannerInfo *root, PartitionScheme part_scheme; /* Nothing to do if partitionwise join technique is disabled. */ - if (!enable_partitionwise_join) + if ((joinrel->pgs_mask & PGS_CONSIDER_PARTITIONWISE) == 0) { Assert(!IS_PARTITIONED_REL(joinrel)); return; @@ -2364,9 +2523,8 @@ set_joinrel_partition_key_exprs(RelOptInfo *joinrel, PartitionScheme part_scheme = joinrel->part_scheme; int partnatts = part_scheme->partnatts; - joinrel->partexprs = (List **) palloc0(sizeof(List *) * partnatts); - joinrel->nullable_partexprs = - (List **) palloc0(sizeof(List *) * partnatts); + joinrel->partexprs = palloc0_array(List *, partnatts); + joinrel->nullable_partexprs = palloc0_array(List *, partnatts); /* * The joinrel's partition expressions are the same as those of the input @@ -2518,3 +2676,544 @@ build_child_join_reltarget(PlannerInfo *root, childrel->reltarget->cost.per_tuple = parentrel->reltarget->cost.per_tuple; childrel->reltarget->width = parentrel->reltarget->width; } + +/* + * create_rel_agg_info + * Create the RelAggInfo structure for the given relation if it can produce + * grouped paths. The given relation is the non-grouped one which has the + * reltarget already constructed. + * + * calculate_grouped_rows: if true, calculate the estimated number of grouped + * rows for the relation. If false, skip the estimation to avoid unnecessary + * planning overhead. + */ +RelAggInfo * +create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel, + bool calculate_grouped_rows) +{ + ListCell *lc; + RelAggInfo *result; + PathTarget *agg_input; + PathTarget *target; + List *group_clauses = NIL; + List *group_exprs = NIL; + + /* + * The lists of aggregate expressions and grouping expressions should have + * been constructed. + */ + Assert(root->agg_clause_list != NIL); + Assert(root->group_expr_list != NIL); + + /* + * If this is a child rel, the grouped rel for its parent rel must have + * been created if it can. So we can just use parent's RelAggInfo if + * there is one, with appropriate variable substitutions. + */ + if (IS_OTHER_REL(rel)) + { + RelOptInfo *grouped_rel; + RelAggInfo *agg_info; + + grouped_rel = rel->top_parent->grouped_rel; + if (grouped_rel == NULL) + return NULL; + + Assert(IS_GROUPED_REL(grouped_rel)); + + /* Must do multi-level transformation */ + agg_info = (RelAggInfo *) + adjust_appendrel_attrs_multilevel(root, + (Node *) grouped_rel->agg_info, + rel, + rel->top_parent); + + agg_info->apply_agg_at = NULL; /* caller will change this later */ + + if (calculate_grouped_rows) + { + agg_info->grouped_rows = + estimate_num_groups(root, agg_info->group_exprs, + rel->rows, NULL, NULL); + + /* + * The grouped paths for the given relation are considered useful + * iff the average group size is no less than + * min_eager_agg_group_size. + */ + agg_info->agg_useful = + (rel->rows / agg_info->grouped_rows) >= min_eager_agg_group_size; + } + + return agg_info; + } + + /* Check if it's possible to produce grouped paths for this relation. */ + if (!eager_aggregation_possible_for_relation(root, rel)) + return NULL; + + /* + * Create targets for the grouped paths and for the input paths of the + * grouped paths. + */ + target = create_empty_pathtarget(); + agg_input = create_empty_pathtarget(); + + /* ... and initialize these targets */ + if (!init_grouping_targets(root, rel, target, agg_input, + &group_clauses, &group_exprs)) + return NULL; + + /* + * Eager aggregation is not applicable if there are no available grouping + * expressions. + */ + if (group_clauses == NIL) + return NULL; + + /* Add aggregates to the grouping target */ + foreach(lc, root->agg_clause_list) + { + AggClauseInfo *ac_info = lfirst_node(AggClauseInfo, lc); + Aggref *aggref; + + Assert(IsA(ac_info->aggref, Aggref)); + + aggref = (Aggref *) copyObject(ac_info->aggref); + mark_partial_aggref(aggref, AGGSPLIT_INITIAL_SERIAL); + + add_column_to_pathtarget(target, (Expr *) aggref, 0); + } + + /* Set the estimated eval cost and output width for both targets */ + set_pathtarget_cost_width(root, target); + set_pathtarget_cost_width(root, agg_input); + + /* build the RelAggInfo result */ + result = makeNode(RelAggInfo); + result->target = target; + result->agg_input = agg_input; + result->group_clauses = group_clauses; + result->group_exprs = group_exprs; + result->apply_agg_at = NULL; /* caller will change this later */ + + if (calculate_grouped_rows) + { + result->grouped_rows = estimate_num_groups(root, result->group_exprs, + rel->rows, NULL, NULL); + + /* + * The grouped paths for the given relation are considered useful iff + * the average group size is no less than min_eager_agg_group_size. + */ + result->agg_useful = + (rel->rows / result->grouped_rows) >= min_eager_agg_group_size; + } + + return result; +} + +/* + * eager_aggregation_possible_for_relation + * Check if it's possible to produce grouped paths for the given relation. + */ +static bool +eager_aggregation_possible_for_relation(PlannerInfo *root, RelOptInfo *rel) +{ + ListCell *lc; + int cur_relid; + + /* + * Check to see if the given relation is in the nullable side of an outer + * join. In this case, we cannot push a partial aggregation down to the + * relation, because the NULL-extended rows produced by the outer join + * would not be available when we perform the partial aggregation, while + * with a non-eager-aggregation plan these rows are available for the + * top-level aggregation. Doing so may result in the rows being grouped + * differently than expected, or produce incorrect values from the + * aggregate functions. + */ + cur_relid = -1; + while ((cur_relid = bms_next_member(rel->relids, cur_relid)) >= 0) + { + RelOptInfo *baserel = find_base_rel_ignore_join(root, cur_relid); + + if (baserel == NULL) + continue; /* ignore outer joins in rel->relids */ + + if (!bms_is_subset(baserel->nulling_relids, rel->relids)) + return false; + } + + /* + * For now we don't try to support PlaceHolderVars. + */ + foreach(lc, rel->reltarget->exprs) + { + Expr *expr = lfirst(lc); + + if (IsA(expr, PlaceHolderVar)) + return false; + } + + /* Caller should only pass base relations or joins. */ + Assert(rel->reloptkind == RELOPT_BASEREL || + rel->reloptkind == RELOPT_JOINREL); + + /* + * Check if all aggregate expressions can be evaluated on this relation + * level. + */ + foreach(lc, root->agg_clause_list) + { + AggClauseInfo *ac_info = lfirst_node(AggClauseInfo, lc); + + Assert(IsA(ac_info->aggref, Aggref)); + + /* + * Give up if any aggregate requires relations other than the current + * one. If the aggregate requires the current relation plus + * additional relations, grouping the current relation could make some + * input rows unavailable for the higher aggregate and may reduce the + * number of input rows it receives. If the aggregate does not + * require the current relation at all, it should not be grouped, as + * we do not support joining two grouped relations. + */ + if (!bms_is_subset(ac_info->agg_eval_at, rel->relids)) + return false; + } + + return true; +} + +/* + * init_grouping_targets + * Initialize the target for grouped paths (target) as well as the target + * for paths that generate input for the grouped paths (agg_input). + * + * We also construct the list of SortGroupClauses and the list of grouping + * expressions for the partial aggregation, and return them in *group_clause + * and *group_exprs. + * + * Return true if the targets could be initialized, false otherwise. + */ +static bool +init_grouping_targets(PlannerInfo *root, RelOptInfo *rel, + PathTarget *target, PathTarget *agg_input, + List **group_clauses, List **group_exprs) +{ + ListCell *lc; + List *possibly_dependent = NIL; + Index maxSortGroupRef; + + /* Identify the max sortgroupref */ + maxSortGroupRef = 0; + foreach(lc, root->processed_tlist) + { + Index ref = ((TargetEntry *) lfirst(lc))->ressortgroupref; + + if (ref > maxSortGroupRef) + maxSortGroupRef = ref; + } + + /* + * At this point, all Vars from this relation that are needed by upper + * joins or are required in the final targetlist should already be present + * in its reltarget. Therefore, we can safely iterate over this + * relation's reltarget->exprs to construct the PathTarget and grouping + * clauses for the grouped paths. + */ + foreach(lc, rel->reltarget->exprs) + { + Expr *expr = (Expr *) lfirst(lc); + Index sortgroupref; + + /* + * Given that PlaceHolderVar currently prevents us from doing eager + * aggregation, the source target cannot contain anything more complex + * than a Var. + */ + Assert(IsA(expr, Var)); + + /* + * Get the sortgroupref of the expr if it is found among, or can be + * deduced from, the original grouping expressions. + */ + sortgroupref = get_expression_sortgroupref(root, expr); + if (sortgroupref > 0) + { + SortGroupClause *sgc; + + /* Find the matching SortGroupClause */ + sgc = get_sortgroupref_clause(sortgroupref, root->processed_groupClause); + Assert(sgc->tleSortGroupRef <= maxSortGroupRef); + + /* + * If the target expression is to be used as a grouping key, it + * should be emitted by the grouped paths that have been pushed + * down to this relation level. + */ + add_column_to_pathtarget(target, expr, sortgroupref); + + /* + * ... and it also should be emitted by the input paths. + */ + add_column_to_pathtarget(agg_input, expr, sortgroupref); + + /* + * Record this SortGroupClause and grouping expression. Note that + * this SortGroupClause might have already been recorded. + */ + if (!list_member(*group_clauses, sgc)) + { + *group_clauses = lappend(*group_clauses, sgc); + *group_exprs = lappend(*group_exprs, expr); + } + } + else if (is_var_needed_by_join(root, (Var *) expr, rel)) + { + /* + * The expression is needed for an upper join but is neither in + * the GROUP BY clause nor derivable from it using EC (otherwise, + * it would have already been included in the targets above). We + * need to create a special SortGroupClause for this expression. + * + * It is important to include such expressions in the grouping + * keys. This is essential to ensure that an aggregated row from + * the partial aggregation matches the other side of the join if + * and only if each row in the partial group does. This ensures + * that all rows within the same partial group share the same + * 'destiny', which is crucial for maintaining correctness. + */ + SortGroupClause *sgc; + TypeCacheEntry *tce; + Oid equalimageproc; + + /* + * But first, check if equality implies image equality for this + * expression. If not, we cannot use it as a grouping key. See + * comments in create_grouping_expr_infos(). + */ + tce = lookup_type_cache(exprType((Node *) expr), + TYPECACHE_BTREE_OPFAMILY); + if (!OidIsValid(tce->btree_opf) || + !OidIsValid(tce->btree_opintype)) + return false; + + equalimageproc = get_opfamily_proc(tce->btree_opf, + tce->btree_opintype, + tce->btree_opintype, + BTEQUALIMAGE_PROC); + + /* + * If there is no BTEQUALIMAGE_PROC, eager aggregation is assumed + * to be unsafe. Otherwise, we call the procedure to check. We + * must be careful to pass the expression's actual collation, + * rather than the data type's default collation, to ensure that + * non-deterministic collations are correctly handled. + */ + if (!OidIsValid(equalimageproc) || + !DatumGetBool(OidFunctionCall1Coll(equalimageproc, + exprCollation((Node *) expr), + ObjectIdGetDatum(tce->btree_opintype)))) + return false; + + /* Create the SortGroupClause. */ + sgc = makeNode(SortGroupClause); + + /* Initialize the SortGroupClause. */ + sgc->tleSortGroupRef = ++maxSortGroupRef; + get_sort_group_operators(exprType((Node *) expr), + false, true, false, + &sgc->sortop, &sgc->eqop, NULL, + &sgc->hashable); + + /* This expression should be emitted by the grouped paths */ + add_column_to_pathtarget(target, expr, sgc->tleSortGroupRef); + + /* ... and it also should be emitted by the input paths. */ + add_column_to_pathtarget(agg_input, expr, sgc->tleSortGroupRef); + + /* Record this SortGroupClause and grouping expression */ + *group_clauses = lappend(*group_clauses, sgc); + *group_exprs = lappend(*group_exprs, expr); + } + else if (is_var_in_aggref_only(root, (Var *) expr)) + { + /* + * The expression is referenced by an aggregate function pushed + * down to this relation and does not appear elsewhere in the + * targetlist or havingQual. Add it to 'agg_input' but not to + * 'target'. + */ + add_new_column_to_pathtarget(agg_input, expr); + } + else + { + /* + * The expression may be functionally dependent on other + * expressions in the target, but we cannot verify this until all + * target expressions have been constructed. + */ + possibly_dependent = lappend(possibly_dependent, expr); + } + } + + /* + * Now we can verify whether an expression is functionally dependent on + * others. + */ + foreach(lc, possibly_dependent) + { + Var *tvar; + List *deps = NIL; + RangeTblEntry *rte; + + tvar = lfirst_node(Var, lc); + rte = root->simple_rte_array[tvar->varno]; + + if (check_functional_grouping(rte->relid, tvar->varno, + tvar->varlevelsup, + target->exprs, &deps)) + { + /* + * The expression is functionally dependent on other target + * expressions, so it can be included in the targets. Since it + * will not be used as a grouping key, a sortgroupref is not + * needed for it. + */ + add_new_column_to_pathtarget(target, (Expr *) tvar); + add_new_column_to_pathtarget(agg_input, (Expr *) tvar); + } + else + { + /* + * We may arrive here with a grouping expression that is proven + * redundant by EquivalenceClass processing, such as 't1.a' in the + * query below. + * + * select max(t1.c) from t t1, t t2 where t1.a = 1 group by t1.a, + * t1.b; + * + * For now we just give up in this case. + */ + return false; + } + } + + return true; +} + +/* + * is_var_in_aggref_only + * Check whether the given Var appears in aggregate expressions and not + * elsewhere in the targetlist or havingQual. + */ +static bool +is_var_in_aggref_only(PlannerInfo *root, Var *var) +{ + ListCell *lc; + + /* + * Search the list of aggregate expressions for the Var. + */ + foreach(lc, root->agg_clause_list) + { + AggClauseInfo *ac_info = lfirst_node(AggClauseInfo, lc); + List *vars; + + Assert(IsA(ac_info->aggref, Aggref)); + + if (!bms_is_member(var->varno, ac_info->agg_eval_at)) + continue; + + vars = pull_var_clause((Node *) ac_info->aggref, + PVC_RECURSE_AGGREGATES | + PVC_RECURSE_WINDOWFUNCS | + PVC_RECURSE_PLACEHOLDERS); + + if (list_member(vars, var)) + { + list_free(vars); + break; + } + + list_free(vars); + } + + return (lc != NULL && !list_member(root->tlist_vars, var)); +} + +/* + * is_var_needed_by_join + * Check if the given Var is needed by joins above the current rel. + */ +static bool +is_var_needed_by_join(PlannerInfo *root, Var *var, RelOptInfo *rel) +{ + Relids relids; + int attno; + RelOptInfo *baserel; + + /* + * Note that when checking if the Var is needed by joins above, we want to + * exclude cases where the Var is only needed in the final targetlist. So + * include "relation 0" in the check. + */ + relids = bms_copy(rel->relids); + relids = bms_add_member(relids, 0); + + baserel = find_base_rel(root, var->varno); + attno = var->varattno - baserel->min_attr; + + return bms_nonempty_difference(baserel->attr_needed[attno], relids); +} + +/* + * get_expression_sortgroupref + * Return the sortgroupref of the given "expr" if it is found among the + * original grouping expressions, or is known equal to any of the original + * grouping expressions due to equivalence relationships. Return 0 if no + * match is found. + */ +static Index +get_expression_sortgroupref(PlannerInfo *root, Expr *expr) +{ + ListCell *lc; + + Assert(IsA(expr, Var)); + + foreach(lc, root->group_expr_list) + { + GroupingExprInfo *ge_info = lfirst_node(GroupingExprInfo, lc); + ListCell *lc1; + + Assert(IsA(ge_info->expr, Var)); + Assert(ge_info->sortgroupref > 0); + + if (equal(expr, ge_info->expr)) + return ge_info->sortgroupref; + + if (ge_info->ec == NULL || + !bms_is_member(((Var *) expr)->varno, ge_info->ec->ec_relids)) + continue; + + /* + * Scan the EquivalenceClass, looking for a match to the given + * expression. We ignore child members here. + */ + foreach(lc1, ge_info->ec->ec_members) + { + EquivalenceMember *em = (EquivalenceMember *) lfirst(lc1); + + /* Child members should not exist in ec_members */ + Assert(!em->em_is_child); + + if (equal(expr, em->em_expr)) + return ge_info->sortgroupref; + } + } + + /* no match is found */ + return 0; +} diff --git a/src/backend/optimizer/util/restrictinfo.c b/src/backend/optimizer/util/restrictinfo.c index a80083d23232b..713518b856497 100644 --- a/src/backend/optimizer/util/restrictinfo.c +++ b/src/backend/optimizer/util/restrictinfo.c @@ -3,7 +3,7 @@ * restrictinfo.c * RestrictInfo node manipulation routines. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/optimizer/util/tlist.c b/src/backend/optimizer/util/tlist.c index d2b4ecc5e5131..752ea9222f612 100644 --- a/src/backend/optimizer/util/tlist.c +++ b/src/backend/optimizer/util/tlist.c @@ -3,7 +3,7 @@ * tlist.c * Target list manipulation routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,6 +19,7 @@ #include "optimizer/cost.h" #include "optimizer/optimizer.h" #include "optimizer/tlist.h" +#include "rewrite/rewriteManip.h" /* @@ -45,6 +46,8 @@ typedef struct typedef struct { + PlannerInfo *root; + bool is_grouping_target; /* true if processing grouping target */ /* This is a List of bare expressions: */ List *input_target_exprs; /* exprs available from input */ /* These are Lists of Lists of split_pathtarget_items: */ @@ -59,6 +62,12 @@ typedef struct Index current_sgref; /* current subexpr's sortgroupref, or 0 */ } split_pathtarget_context; +static void split_pathtarget_at_srfs_extended(PlannerInfo *root, + PathTarget *target, + PathTarget *input_target, + List **targets, + List **targets_contain_srfs, + bool is_grouping_target); static bool split_pathtarget_walker(Node *node, split_pathtarget_context *context); static void add_sp_item_to_pathtarget(PathTarget *target, @@ -467,7 +476,7 @@ extract_grouping_ops(List *groupClause) Oid *groupOperators; ListCell *glitem; - groupOperators = (Oid *) palloc(sizeof(Oid) * numCols); + groupOperators = palloc_array(Oid, numCols); foreach(glitem, groupClause) { @@ -493,7 +502,7 @@ extract_grouping_collations(List *groupClause, List *tlist) Oid *grpCollations; ListCell *glitem; - grpCollations = (Oid *) palloc(sizeof(Oid) * numCols); + grpCollations = palloc_array(Oid, numCols); foreach(glitem, groupClause) { @@ -518,7 +527,7 @@ extract_grouping_cols(List *groupClause, List *tlist) int colno = 0; ListCell *glitem; - grpColIdx = (AttrNumber *) palloc(sizeof(AttrNumber) * numCols); + grpColIdx = palloc_array(AttrNumber, numCols); foreach(glitem, groupClause) { @@ -822,6 +831,51 @@ apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target) /* * split_pathtarget_at_srfs + * Split given PathTarget into multiple levels to position SRFs safely, + * performing exact matching against input_target. + * + * This is a wrapper for split_pathtarget_at_srfs_extended() that is used when + * both targets are on the same side of the grouping boundary (i.e., both are + * pre-grouping or both are post-grouping). In this case, no special handling + * for the grouping nulling bit is required. + * + * See split_pathtarget_at_srfs_extended() for more details. + */ +void +split_pathtarget_at_srfs(PlannerInfo *root, + PathTarget *target, PathTarget *input_target, + List **targets, List **targets_contain_srfs) +{ + split_pathtarget_at_srfs_extended(root, target, input_target, + targets, targets_contain_srfs, + false); +} + +/* + * split_pathtarget_at_srfs_grouping + * Split given PathTarget into multiple levels to position SRFs safely, + * ignoring the grouping nulling bit when matching against input_target. + * + * This variant is used when the targets cross the grouping boundary (i.e., + * target is post-grouping while input_target is pre-grouping). In this case, + * we need to ignore the grouping nulling bit when checking for expression + * availability to avoid incorrectly re-evaluating SRFs that have already been + * computed in input_target. + * + * See split_pathtarget_at_srfs_extended() for more details. + */ +void +split_pathtarget_at_srfs_grouping(PlannerInfo *root, + PathTarget *target, PathTarget *input_target, + List **targets, List **targets_contain_srfs) +{ + split_pathtarget_at_srfs_extended(root, target, input_target, + targets, targets_contain_srfs, + true); +} + +/* + * split_pathtarget_at_srfs_extended * Split given PathTarget into multiple levels to position SRFs safely * * The executor can only handle set-returning functions that appear at the @@ -860,6 +914,13 @@ apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target) * already meant as a reference to a lower subexpression). So, don't expand * any tlist expressions that appear in input_target, if that's not NULL. * + * This check requires extra care when processing the grouping target + * (indicated by the is_grouping_target flag). In this case input_target is + * pre-grouping while target is post-grouping, so the latter may carry + * nullingrels bits from the grouping step that are absent in the former. We + * must ignore those bits to correctly recognize that the tlist expressions are + * available in input_target. + * * It's also important that we preserve any sortgroupref annotation appearing * in the given target, especially on expressions matching input_target items. * @@ -877,10 +938,11 @@ apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target) * are only a few possible patterns for which levels contain SRFs. * But this representation decouples callers from that knowledge. */ -void -split_pathtarget_at_srfs(PlannerInfo *root, - PathTarget *target, PathTarget *input_target, - List **targets, List **targets_contain_srfs) +static void +split_pathtarget_at_srfs_extended(PlannerInfo *root, + PathTarget *target, PathTarget *input_target, + List **targets, List **targets_contain_srfs, + bool is_grouping_target) { split_pathtarget_context context; int max_depth; @@ -905,7 +967,12 @@ split_pathtarget_at_srfs(PlannerInfo *root, return; } - /* Pass any input_target exprs down to split_pathtarget_walker() */ + /* + * Pass 'root', the is_grouping_target flag, and any input_target exprs + * down to split_pathtarget_walker(). + */ + context.root = root; + context.is_grouping_target = is_grouping_target; context.input_target_exprs = input_target ? input_target->exprs : NIL; /* @@ -1076,9 +1143,27 @@ split_pathtarget_at_srfs(PlannerInfo *root, static bool split_pathtarget_walker(Node *node, split_pathtarget_context *context) { + Node *sanitized_node = node; + if (node == NULL) return false; + /* + * If we are crossing the grouping boundary (post-grouping target vs + * pre-grouping input_target), we must ignore the grouping nulling bit to + * correctly check if the subexpression is available in input_target. This + * aligns with the matching logic in set_upper_references(). + */ + if (context->is_grouping_target && + context->root->parse->hasGroupRTE && + context->root->parse->groupingSets != NIL) + { + sanitized_node = + remove_nulling_relids(node, + bms_make_singleton(context->root->group_rtindex), + NULL); + } + /* * A subexpression that matches an expression already computed in * input_target can be treated like a Var (which indeed it will be after @@ -1087,9 +1172,9 @@ split_pathtarget_walker(Node *node, split_pathtarget_context *context) * substructure. (Note in particular that this preserves the identity of * any expressions that appear as sortgrouprefs in input_target.) */ - if (list_member(context->input_target_exprs, node)) + if (list_member(context->input_target_exprs, sanitized_node)) { - split_pathtarget_item *item = palloc(sizeof(split_pathtarget_item)); + split_pathtarget_item *item = palloc_object(split_pathtarget_item); item->expr = node; item->sortgroupref = context->current_sgref; @@ -1109,7 +1194,7 @@ split_pathtarget_walker(Node *node, split_pathtarget_context *context) IsA(node, GroupingFunc) || IsA(node, WindowFunc)) { - split_pathtarget_item *item = palloc(sizeof(split_pathtarget_item)); + split_pathtarget_item *item = palloc_object(split_pathtarget_item); item->expr = node; item->sortgroupref = context->current_sgref; @@ -1124,7 +1209,7 @@ split_pathtarget_walker(Node *node, split_pathtarget_context *context) */ if (IS_SRF_CALL(node)) { - split_pathtarget_item *item = palloc(sizeof(split_pathtarget_item)); + split_pathtarget_item *item = palloc_object(split_pathtarget_item); List *save_input_vars = context->current_input_vars; List *save_input_srfs = context->current_input_srfs; int save_current_depth = context->current_depth; diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c index 8065237a1895f..907a255c36faa 100644 --- a/src/backend/optimizer/util/var.c +++ b/src/backend/optimizer/util/var.c @@ -9,7 +9,7 @@ * contains variables. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -776,14 +776,6 @@ pull_var_clause_walker(Node *node, pull_var_clause_context *context) * PlaceHolderVar or constructed from those, we can just add the * varnullingrels bits to the existing nullingrels field(s); otherwise * we have to add a PlaceHolderVar wrapper. - * - * NOTE: this is also used by the parser, to expand join alias Vars before - * checking GROUP BY validity. For that use-case, root will be NULL, which - * is why we have to pass the Query separately. We need the root itself only - * for making PlaceHolderVars. We can avoid making PlaceHolderVars in the - * parser's usage because it won't be dealing with arbitrary expressions: - * so long as adjust_standard_join_alias_expression can handle everything - * the parser would make as a join alias expression, we're OK. */ Node * flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node) @@ -808,6 +800,44 @@ flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node) return flatten_join_alias_vars_mutator(node, &context); } +/* + * flatten_join_alias_for_parser + * + * This variant of flatten_join_alias_vars is used by the parser, to expand + * join alias Vars before checking GROUP BY validity. In that case we lack + * a root structure. Fortunately, we'd only need the root for making + * PlaceHolderVars. We can avoid making PlaceHolderVars in the parser's + * usage because it won't be dealing with arbitrary expressions: so long as + * adjust_standard_join_alias_expression can handle everything the parser + * would make as a join alias expression, we're OK. + * + * The "node" might be part of a sub-query of the Query whose join alias + * Vars are to be expanded. "sublevels_up" indicates how far below the + * given query we are starting. + */ +Node * +flatten_join_alias_for_parser(Query *query, Node *node, int sublevels_up) +{ + flatten_join_alias_vars_context context; + + /* + * We do not expect this to be applied to the whole Query, only to + * expressions or LATERAL subqueries. Hence, if the top node is a Query, + * it's okay to immediately increment sublevels_up. + */ + Assert(node != (Node *) query); + + context.root = NULL; + context.query = query; + context.sublevels_up = sublevels_up; + /* flag whether join aliases could possibly contain SubLinks */ + context.possible_sublink = query->hasSubLinks; + /* if hasSubLinks is already true, no need to work hard */ + context.inserted_sublink = query->hasSubLinks; + + return flatten_join_alias_vars_mutator(node, &context); +} + static Node * flatten_join_alias_vars_mutator(Node *node, flatten_join_alias_vars_context *context) @@ -958,11 +988,12 @@ flatten_join_alias_vars_mutator(Node *node, * existing nullingrels field(s); otherwise we have to add a PlaceHolderVar * wrapper. * - * NOTE: this is also used by ruleutils.c, to deparse one query parsetree back - * to source text. For that use-case, root will be NULL, which is why we have - * to pass the Query separately. We need the root itself only for preserving - * varnullingrels. We can avoid preserving varnullingrels in the ruleutils.c's - * usage because it does not make any difference to the deparsed source text. + * NOTE: root may be passed as NULL, which is why we have to pass the Query + * separately. We need the root itself only for preserving varnullingrels. + * Callers can safely pass NULL if preserving varnullingrels is unnecessary for + * their specific use case (e.g., deparsing source text, or scanning for + * volatile functions), or if it is already guaranteed that the query cannot + * contain grouping sets. */ Node * flatten_group_exprs(PlannerInfo *root, Query *query, Node *node) diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 8c0fe28d63f59..8b5a4af6bf2a3 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_graphtable.o \ parse_jsontable.o \ parse_merge.o \ parse_node.o \ diff --git a/src/backend/parser/README b/src/backend/parser/README index e0c986a41efea..e26eb437a9f35 100644 --- a/src/backend/parser/README +++ b/src/backend/parser/README @@ -20,6 +20,7 @@ parse_cte.c handle Common Table Expressions (WITH clauses) parse_expr.c handle expressions like col, col + 3, x = 3 or x = 4 parse_enr.c handle ephemeral named rels (trigger transition tables, ...) parse_func.c handle functions, table.column and column identifiers +parse_jsontable.c handle JSON_TABLE parse_merge.c handle MERGE parse_node.c create nodes for various structures parse_oper.c handle operators in expressions diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index a16fdd65601d5..ffcf25a6be73f 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -14,7 +14,7 @@ * contain optimizable statements, which we should transform. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/parser/analyze.c @@ -24,7 +24,11 @@ #include "postgres.h" +#include "access/stratnum.h" #include "access/sysattr.h" +#include "catalog/dependency.h" +#include "catalog/pg_am.h" +#include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/defrem.h" @@ -50,11 +54,22 @@ #include "parser/parsetree.h" #include "utils/backend_status.h" #include "utils/builtins.h" +#include "utils/fmgroids.h" #include "utils/guc.h" +#include "utils/lsyscache.h" +#include "utils/rangetypes.h" #include "utils/rel.h" #include "utils/syscache.h" +/* Passthrough data for transformPLAssignStmtTarget */ +typedef struct SelectStmtPassthrough +{ + PLAssignStmt *stmt; /* the assignment statement */ + Node *target; /* node representing the target variable */ + List *indirection; /* indirection yet to be applied to target */ +} SelectStmtPassthrough; + /* Hook for plugins to get control at end of parse analysis */ post_parse_analyze_hook_type post_parse_analyze_hook = NULL; @@ -63,8 +78,13 @@ static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt); static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt); static OnConflictExpr *transformOnConflictClause(ParseState *pstate, OnConflictClause *onConflictClause); +static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate, + int rtindex, + const ForPortionOfClause *forPortionOf, + bool isUpdate); static int count_rowexpr_columns(ParseState *pstate, Node *expr); -static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt); +static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt, + SelectStmtPassthrough *passthru); static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt); static Query *transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt); static Node *transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, @@ -75,6 +95,8 @@ static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt); static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt); static Query *transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt); +static List *transformPLAssignStmtTarget(ParseState *pstate, List *tlist, + SelectStmtPassthrough *passthru); static Query *transformDeclareCursorStmt(ParseState *pstate, DeclareCursorStmt *stmt); static Query *transformExplainStmt(ParseState *pstate, @@ -238,103 +260,24 @@ parse_sub_analyze(Node *parseTree, ParseState *parentParseState, return query; } -/* - * setQueryLocationAndLength - * Set query's location and length from statement and ParseState - * - * Some statements, like PreparableStmt, can be located within parentheses. - * For example "(SELECT 1)" or "COPY (UPDATE ...) to x;". For those, we - * cannot use the whole string from the statement's location or the SQL - * string would yield incorrectly. The parser will set stmt_len, reflecting - * the size of the statement within the parentheses. Thus, when stmt_len is - * available, we need to use it for the Query's stmt_len. - * - * For other cases, the parser can't provide the length of individual - * statements. However, we have the statement's location plus the length - * (p_stmt_len) and location (p_stmt_location) of the top level RawStmt, - * stored in pstate. Thus, the statement's length is the RawStmt's length - * minus how much we've advanced in the RawStmt's string. If p_stmt_len - * is 0, the SQL string is used up to its end. - */ -static void -setQueryLocationAndLength(ParseState *pstate, Query *qry, Node *parseTree) -{ - ParseLoc stmt_len = 0; - - switch (nodeTag(parseTree)) - { - case T_InsertStmt: - qry->stmt_location = ((InsertStmt *) parseTree)->stmt_location; - stmt_len = ((InsertStmt *) parseTree)->stmt_len; - break; - - case T_DeleteStmt: - qry->stmt_location = ((DeleteStmt *) parseTree)->stmt_location; - stmt_len = ((DeleteStmt *) parseTree)->stmt_len; - break; - - case T_UpdateStmt: - qry->stmt_location = ((UpdateStmt *) parseTree)->stmt_location; - stmt_len = ((UpdateStmt *) parseTree)->stmt_len; - break; - - case T_MergeStmt: - qry->stmt_location = ((MergeStmt *) parseTree)->stmt_location; - stmt_len = ((MergeStmt *) parseTree)->stmt_len; - break; - - case T_SelectStmt: - qry->stmt_location = ((SelectStmt *) parseTree)->stmt_location; - stmt_len = ((SelectStmt *) parseTree)->stmt_len; - break; - - case T_PLAssignStmt: - qry->stmt_location = ((PLAssignStmt *) parseTree)->location; - break; - - default: - qry->stmt_location = pstate->p_stmt_location; - break; - } - - if (stmt_len > 0) - { - /* Statement's length is known, use it */ - qry->stmt_len = stmt_len; - } - else if (pstate->p_stmt_len > 0) - { - /* - * The top RawStmt's length is known, so calculate the statement's - * length from the statement's location and the RawStmt's length and - * location. - */ - qry->stmt_len = pstate->p_stmt_len - (qry->stmt_location - pstate->p_stmt_location); - } - - /* The calculated statement length should be calculated as positive. */ - Assert(qry->stmt_len >= 0); -} - /* * transformTopLevelStmt - * transform a Parse tree into a Query tree. * - * This function is just responsible for storing location data - * from the RawStmt into the ParseState. + * This function is just responsible for transferring statement location data + * from the RawStmt into the finished Query. */ Query * transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree) { Query *result; - /* Store RawStmt's length and location in pstate */ - pstate->p_stmt_len = parseTree->stmt_len; - pstate->p_stmt_location = parseTree->stmt_location; - /* We're at top level, so allow SELECT INTO */ result = transformOptionalSelectInto(pstate, parseTree->stmt); + result->stmt_location = parseTree->stmt_location; + result->stmt_len = parseTree->stmt_len; + return result; } @@ -450,7 +393,7 @@ transformStmt(ParseState *pstate, Node *parseTree) if (n->valuesLists) result = transformValuesClause(pstate, n); else if (n->op == SETOP_NONE) - result = transformSelectStmt(pstate, n); + result = transformSelectStmt(pstate, n, NULL); else result = transformSetOperationStmt(pstate, n); } @@ -496,14 +439,13 @@ transformStmt(ParseState *pstate, Node *parseTree) */ result = makeNode(Query); result->commandType = CMD_UTILITY; - result->utilityStmt = (Node *) parseTree; + result->utilityStmt = parseTree; break; } /* Mark as original query until we learn differently */ result->querySource = QSRC_ORIGINAL; result->canSetTag = true; - setQueryLocationAndLength(pstate, result, parseTree); return result; } @@ -653,6 +595,14 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) ACL_DELETE); nsitem = pstate->p_target_nsitem; + /* disallow DELETE ... WHERE CURRENT OF on a view */ + if (stmt->whereClause && + IsA(stmt->whereClause, CurrentOfExpr) && + pstate->p_target_relation->rd_rel->relkind == RELKIND_VIEW) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WHERE CURRENT OF on a view is not implemented")); + /* there's no DISTINCT in DELETE */ qry->distinctClause = NIL; @@ -672,6 +622,12 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) nsitem->p_lateral_only = false; nsitem->p_lateral_ok = true; + if (stmt->forPortionOf) + qry->forPortionOf = transformForPortionOfClause(pstate, + qry->resultRelation, + stmt->forPortionOf, + false); + qual = transformWhereClause(pstate, stmt->whereClause, EXPR_KIND_WHERE, "WHERE"); @@ -718,14 +674,13 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) ListCell *icols; ListCell *attnos; ListCell *lc; - bool isOnConflictUpdate; + bool requiresUpdatePerm; AclMode targetPerms; /* There can't be any outer WITH to worry about */ Assert(pstate->p_ctenamespace == NIL); qry->commandType = CMD_INSERT; - pstate->p_is_insert = true; /* process the WITH clause independently of all else */ if (stmt->withClause) @@ -737,8 +692,14 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) qry->override = stmt->override; - isOnConflictUpdate = (stmt->onConflictClause && - stmt->onConflictClause->action == ONCONFLICT_UPDATE); + /* + * ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT FOR UPDATE/SHARE + * require UPDATE permission on the target relation. + */ + requiresUpdatePerm = (stmt->onConflictClause && + (stmt->onConflictClause->action == ONCONFLICT_UPDATE || + (stmt->onConflictClause->action == ONCONFLICT_SELECT && + stmt->onConflictClause->lockStrength != LCS_NONE))); /* * We have three cases to deal with: DEFAULT VALUES (selectStmt == NULL), @@ -788,7 +749,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) * to the joinlist or namespace. */ targetPerms = ACL_INSERT; - if (isOnConflictUpdate) + if (requiresUpdatePerm) targetPerms |= ACL_UPDATE; qry->resultRelation = setTargetTable(pstate, stmt->relation, false, false, targetPerms); @@ -857,7 +818,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) */ nsitem = addRangeTableEntryForSubquery(pstate, selectQuery, - makeAlias("*SELECT*", NIL), + NULL, false, false); addNSItemToQuery(pstate, nsitem, true, false, false); @@ -1095,6 +1056,15 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt) false, true, true); } + /* ON CONFLICT DO SELECT requires a RETURNING clause */ + if (stmt->onConflictClause && + stmt->onConflictClause->action == ONCONFLICT_SELECT && + !stmt->returningClause) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("ON CONFLICT DO SELECT requires a RETURNING clause"), + parser_errposition(pstate, stmt->onConflictClause->location)); + /* Process ON CONFLICT, if any. */ if (stmt->onConflictClause) qry->onConflict = transformOnConflictClause(pstate, @@ -1253,12 +1223,13 @@ transformOnConflictClause(ParseState *pstate, OnConflictExpr *result; /* - * If this is ON CONFLICT ... UPDATE, first create the range table entry - * for the EXCLUDED pseudo relation, so that that will be present while - * processing arbiter expressions. (You can't actually reference it from - * there, but this provides a useful error message if you try.) + * If this is ON CONFLICT DO SELECT/UPDATE, first create the range table + * entry for the EXCLUDED pseudo relation, so that that will be present + * while processing arbiter expressions. (You can't actually reference it + * from there, but this provides a useful error message if you try.) */ - if (onConflictClause->action == ONCONFLICT_UPDATE) + if (onConflictClause->action == ONCONFLICT_UPDATE || + onConflictClause->action == ONCONFLICT_SELECT) { Relation targetrel = pstate->p_target_relation; RangeTblEntry *exclRte; @@ -1287,28 +1258,22 @@ transformOnConflictClause(ParseState *pstate, transformOnConflictArbiter(pstate, onConflictClause, &arbiterElems, &arbiterWhere, &arbiterConstraint); - /* Process DO UPDATE */ - if (onConflictClause->action == ONCONFLICT_UPDATE) + /* Process DO SELECT/UPDATE */ + if (onConflictClause->action == ONCONFLICT_UPDATE || + onConflictClause->action == ONCONFLICT_SELECT) { - /* - * Expressions in the UPDATE targetlist need to be handled like UPDATE - * not INSERT. We don't need to save/restore this because all INSERT - * expressions have been parsed already. - */ - pstate->p_is_insert = false; - /* * Add the EXCLUDED pseudo relation to the query namespace, making it - * available in the UPDATE subexpressions. + * available in SET and WHERE subexpressions. */ addNSItemToQuery(pstate, exclNSItem, false, true, true); - /* - * Now transform the UPDATE subexpressions. - */ - onConflictSet = - transformUpdateTargetList(pstate, onConflictClause->targetList); + /* Process the UPDATE SET clause */ + if (onConflictClause->action == ONCONFLICT_UPDATE) + onConflictSet = + transformUpdateTargetList(pstate, onConflictClause->targetList, NULL); + /* Process the SELECT/UPDATE WHERE clause */ onConflictWhere = transformWhereClause(pstate, onConflictClause->whereClause, EXPR_KIND_WHERE, "WHERE"); @@ -1322,13 +1287,14 @@ transformOnConflictClause(ParseState *pstate, pstate->p_namespace = list_delete_last(pstate->p_namespace); } - /* Finally, build ON CONFLICT DO [NOTHING | UPDATE] expression */ + /* Finally, build ON CONFLICT DO [NOTHING | SELECT | UPDATE] expression */ result = makeNode(OnConflictExpr); result->action = onConflictClause->action; result->arbiterElems = arbiterElems; result->arbiterWhere = arbiterWhere; result->constraint = arbiterConstraint; + result->lockStrength = onConflictClause->lockStrength; result->onConflictSet = onConflictSet; result->onConflictWhere = onConflictWhere; result->exclRelIndex = exclRelIndex; @@ -1337,6 +1303,319 @@ transformOnConflictClause(ParseState *pstate, return result; } +/* + * transformForPortionOfClause + * + * Transforms a ForPortionOfClause in an UPDATE/DELETE statement. + * + * - Look up the range/period requested. + * - Build a compatible range value from the FROM and TO expressions. + * - Build an "overlaps" expression for filtering, used later by the + * rewriter. + * - For UPDATEs, build an "intersects" expression the rewriter can add + * to the targetList to change the temporal bounds. + */ +static ForPortionOfExpr * +transformForPortionOfClause(ParseState *pstate, + int rtindex, + const ForPortionOfClause *forPortionOf, + bool isUpdate) +{ + Relation targetrel = pstate->p_target_relation; + int range_attno = InvalidAttrNumber; + Form_pg_attribute attr; + Oid attbasetype; + Oid opclass; + Oid opfamily; + Oid opcintype; + Oid funcid = InvalidOid; + StrategyNumber strat; + Oid opid; + OpExpr *op; + ForPortionOfExpr *result; + Var *rangeVar; + + /* We don't support FOR PORTION OF FDW queries. */ + if (targetrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("foreign tables don't support FOR PORTION OF"))); + + result = makeNode(ForPortionOfExpr); + + /* Look up the FOR PORTION OF name requested. */ + range_attno = attnameAttNum(targetrel, forPortionOf->range_name, false); + if (range_attno == InvalidAttrNumber) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + forPortionOf->range_name, + RelationGetRelationName(targetrel)), + parser_errposition(pstate, forPortionOf->location))); + attr = TupleDescAttr(targetrel->rd_att, range_attno - 1); + + attbasetype = getBaseType(attr->atttypid); + + rangeVar = makeVar(rtindex, + range_attno, + attr->atttypid, + attr->atttypmod, + attr->attcollation, + 0); + rangeVar->location = forPortionOf->location; + result->rangeVar = rangeVar; + + /* Require SELECT privilege on the application-time column. */ + markVarForSelectPriv(pstate, rangeVar); + + /* + * Use the basetype for the target, which shouldn't be required to follow + * domain rules. The table's column type is in the Var if we need it. + */ + result->rangeType = attbasetype; + result->isDomain = attbasetype != attr->atttypid; + + if (forPortionOf->target) + { + Oid declared_target_type = attbasetype; + Oid actual_target_type; + + /* + * We were already given an expression for the target, so we don't + * have to build anything. We still have to make sure we got the right + * type. NULL will be caught be the executor. + */ + + result->targetRange = transformExpr(pstate, + forPortionOf->target, + EXPR_KIND_FOR_PORTION); + + actual_target_type = exprType(result->targetRange); + + if (!can_coerce_type(1, &actual_target_type, &declared_target_type, COERCION_IMPLICIT)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("could not coerce FOR PORTION OF target from %s to %s", + format_type_be(actual_target_type), + format_type_be(declared_target_type)), + parser_errposition(pstate, exprLocation(forPortionOf->target)))); + + result->targetRange = coerce_type(pstate, + result->targetRange, + actual_target_type, + declared_target_type, + -1, + COERCION_IMPLICIT, + COERCE_IMPLICIT_CAST, + exprLocation(forPortionOf->target)); + + /* + * XXX: For now we only support ranges and multiranges, so we fail on + * anything else. + */ + if (!type_is_range(attbasetype) && !type_is_multirange(attbasetype)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("column \"%s\" of relation \"%s\" is not a range or multirange type", + forPortionOf->range_name, + RelationGetRelationName(targetrel)), + parser_errposition(pstate, forPortionOf->location))); + + } + else + { + Oid rngsubtype; + Oid declared_arg_types[2]; + Oid actual_arg_types[2]; + List *args; + + /* + * Make sure it's a range column. XXX: We could support this syntax on + * multirange columns too, if we just built a one-range multirange + * from the FROM/TO phrases. + */ + if (!type_is_range(attbasetype)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("column \"%s\" of relation \"%s\" is not a range type", + forPortionOf->range_name, + RelationGetRelationName(targetrel)), + parser_errposition(pstate, forPortionOf->location))); + + rngsubtype = get_range_subtype(attbasetype); + declared_arg_types[0] = rngsubtype; + declared_arg_types[1] = rngsubtype; + + /* + * Build a range from the FROM ... TO ... bounds. This should give a + * constant result, so we accept functions like NOW() but not column + * references, subqueries, etc. + */ + result->targetFrom = transformExpr(pstate, + forPortionOf->target_start, + EXPR_KIND_FOR_PORTION); + result->targetTo = transformExpr(pstate, + forPortionOf->target_end, + EXPR_KIND_FOR_PORTION); + actual_arg_types[0] = exprType(result->targetFrom); + actual_arg_types[1] = exprType(result->targetTo); + args = list_make2(copyObject(result->targetFrom), + copyObject(result->targetTo)); + + /* + * Check the bound types separately, for better error message and + * location + */ + if (!can_coerce_type(1, actual_arg_types, declared_arg_types, COERCION_IMPLICIT)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("could not coerce FOR PORTION OF %s bound from %s to %s", + "FROM", + format_type_be(actual_arg_types[0]), + format_type_be(declared_arg_types[0])), + parser_errposition(pstate, exprLocation(forPortionOf->target_start)))); + if (!can_coerce_type(1, &actual_arg_types[1], &declared_arg_types[1], COERCION_IMPLICIT)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("could not coerce FOR PORTION OF %s bound from %s to %s", + "TO", + format_type_be(actual_arg_types[1]), + format_type_be(declared_arg_types[1])), + parser_errposition(pstate, exprLocation(forPortionOf->target_end)))); + + make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types); + result->targetRange = (Node *) makeFuncExpr(get_range_constructor2(attbasetype), + attbasetype, + args, + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); + } + if (contain_volatile_functions_after_planning((Expr *) result->targetRange)) + ereport(ERROR, + (errmsg("FOR PORTION OF bounds cannot contain volatile functions"))); + + /* + * Build overlapsExpr to use as an extra qual. This means we only hit rows + * matching the FROM & TO bounds. We must look up the overlaps operator + * (usually "&&"). + */ + opclass = GetDefaultOpClass(attr->atttypid, GIST_AM_OID); + if (!OidIsValid(opclass)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("data type %s has no default operator class for access method \"%s\"", + format_type_be(attr->atttypid), "gist"), + errhint("You must define a default operator class for the data type."))); + + /* Look up the operators and functions we need. */ + GetOperatorFromCompareType(opclass, InvalidOid, COMPARE_OVERLAP, &opid, &strat); + op = makeNode(OpExpr); + op->opno = opid; + op->opfuncid = get_opcode(opid); + op->opresulttype = BOOLOID; + op->args = list_make2(copyObject(rangeVar), copyObject(result->targetRange)); + result->overlapsExpr = (Node *) op; + + /* + * Look up the without_portion func. This computes the bounds of temporal + * leftovers. + * + * XXX: Find a more extensible way to look up the function, permitting + * user-defined types. An opclass support function doesn't make sense, + * since there is no index involved. Perhaps a type support function. + */ + if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype)) + switch (opcintype) + { + case ANYRANGEOID: + result->withoutPortionProc = F_RANGE_MINUS_MULTI; + break; + case ANYMULTIRANGEOID: + result->withoutPortionProc = F_MULTIRANGE_MINUS_MULTI; + break; + default: + elog(ERROR, "unexpected opcintype: %u", opcintype); + } + else + elog(ERROR, "unexpected opclass: %u", opclass); + + if (isUpdate) + { + /* + * Now make sure we update the start/end time of the record. For a + * range col (r) this is `r = r * targetRange` (where * is the + * intersect operator). + */ + Oid intersectoperoid; + List *funcArgs; + Node *rangeTLEExpr; + TargetEntry *tle; + + /* + * Whatever operator is used for intersect by temporal foreign keys, + * we can use its backing procedure for intersects in FOR PORTION OF. + * XXX: Share code with FindFKPeriodOpers? + */ + switch (opcintype) + { + case ANYRANGEOID: + intersectoperoid = OID_RANGE_INTERSECT_RANGE_OP; + break; + case ANYMULTIRANGEOID: + intersectoperoid = OID_MULTIRANGE_INTERSECT_MULTIRANGE_OP; + break; + default: + elog(ERROR, "unexpected opcintype: %u", opcintype); + } + funcid = get_opcode(intersectoperoid); + if (!OidIsValid(funcid)) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not identify an intersect function for type %s", + format_type_be(opcintype))); + + funcArgs = list_make2(copyObject(rangeVar), + copyObject(result->targetRange)); + rangeTLEExpr = (Node *) makeFuncExpr(funcid, attbasetype, funcArgs, + InvalidOid, InvalidOid, + COERCE_EXPLICIT_CALL); + + /* + * Coerce to domain if necessary. If we skip this, we will allow + * updating to forbidden values. + */ + rangeTLEExpr = coerce_type(pstate, + rangeTLEExpr, + attbasetype, + attr->atttypid, + -1, + COERCION_IMPLICIT, + COERCE_IMPLICIT_CAST, + exprLocation(forPortionOf->target)); + + /* Make a TLE to set the range column */ + result->rangeTargetList = NIL; + tle = makeTargetEntry((Expr *) rangeTLEExpr, range_attno, + forPortionOf->range_name, false); + result->rangeTargetList = lappend(result->rangeTargetList, tle); + + /* + * The range column will change, but you don't need UPDATE permission + * on it, so we don't add to updatedCols here. XXX: If + * https://www.postgresql.org/message-id/CACJufxEtY1hdLcx%3DFhnqp-ERcV1PhbvELG5COy_CZjoEW76ZPQ%40mail.gmail.com + * is merged (only validate CHECK constraints if they depend on one of + * the columns being UPDATEd), we need to make sure that code knows + * that we are updating the application-time column. + */ + } + else + result->rangeTargetList = NIL; + + result->range_name = forPortionOf->range_name; + result->location = forPortionOf->location; + result->targetLocation = forPortionOf->target_location; + + return result; +} /* * BuildOnConflictExcludedTargetlist @@ -1454,11 +1733,19 @@ count_rowexpr_columns(ParseState *pstate, Node *expr) * transformSelectStmt - * transforms a Select Statement * + * This function is also used to transform the source expression of a + * PLAssignStmt. In that usage, passthru is non-NULL and we need to + * call transformPLAssignStmtTarget after the initial transformation of the + * SELECT's targetlist. (We could generalize this into an arbitrary callback + * function, but for now that would just be more notation with no benefit.) + * All the rest is the same as a regular SelectStmt. + * * Note: this covers only cases with no set operations and no VALUES lists; * see below for the other cases. */ static Query * -transformSelectStmt(ParseState *pstate, SelectStmt *stmt) +transformSelectStmt(ParseState *pstate, SelectStmt *stmt, + SelectStmtPassthrough *passthru) { Query *qry = makeNode(Query); Node *qual; @@ -1495,8 +1782,16 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->targetList = transformTargetList(pstate, stmt->targetList, EXPR_KIND_SELECT_TARGET); - /* mark column origins */ - markTargetListOrigins(pstate, qry->targetList); + /* + * If we're within a PLAssignStmt, do further transformation of the + * targetlist; that has to happen before we consider sorting or grouping. + * Otherwise, mark column origins (which are useless in a PLAssignStmt). + */ + if (passthru) + qry->targetList = transformPLAssignStmtTarget(pstate, qry->targetList, + passthru); + else + markTargetListOrigins(pstate, qry->targetList); /* transform WHERE */ qual = transformWhereClause(pstate, stmt->whereClause, @@ -1520,12 +1815,14 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt) qry->groupClause = transformGroupClause(pstate, stmt->groupClause, + stmt->groupByAll, &qry->groupingSets, &qry->targetList, qry->sortClause, EXPR_KIND_GROUP_BY, false /* allow SQL92 rules */ ); qry->groupDistinct = stmt->groupDistinct; + qry->groupByAll = stmt->groupByAll; if (stmt->distinctClause == NIL) { @@ -2180,10 +2477,8 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, { /* Process leaf SELECT */ Query *selectQuery; - char selectName[32]; ParseNamespaceItem *nsitem; RangeTblRef *rtr; - ListCell *tl; /* * Transform SelectStmt into a Query. @@ -2223,6 +2518,8 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, */ if (targetlist) { + ListCell *tl; + *targetlist = NIL; foreach(tl, selectQuery->targetList) { @@ -2236,11 +2533,9 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, /* * Make the leaf query be a subquery in the top-level rangetable. */ - snprintf(selectName, sizeof(selectName), "*SELECT* %d", - list_length(pstate->p_rtable) + 1); nsitem = addRangeTableEntryForSubquery(pstate, selectQuery, - makeAlias(selectName, NIL), + NULL, false, false); @@ -2257,8 +2552,6 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, SetOperationStmt *op = makeNode(SetOperationStmt); List *ltargetlist; List *rtargetlist; - ListCell *ltl; - ListCell *rtl; const char *context; bool recursive = (pstate->p_parent_cte && pstate->p_parent_cte->cterecursive); @@ -2293,161 +2586,182 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt, false, &rtargetlist); - /* - * Verify that the two children have the same number of non-junk - * columns, and determine the types of the merged output columns. - */ - if (list_length(ltargetlist) != list_length(rtargetlist)) - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("each %s query must have the same number of columns", - context), - parser_errposition(pstate, - exprLocation((Node *) rtargetlist)))); + constructSetOpTargetlist(pstate, op, ltargetlist, rtargetlist, targetlist, + context, recursive); - if (targetlist) - *targetlist = NIL; - op->colTypes = NIL; - op->colTypmods = NIL; - op->colCollations = NIL; - op->groupClauses = NIL; - forboth(ltl, ltargetlist, rtl, rtargetlist) - { - TargetEntry *ltle = (TargetEntry *) lfirst(ltl); - TargetEntry *rtle = (TargetEntry *) lfirst(rtl); - Node *lcolnode = (Node *) ltle->expr; - Node *rcolnode = (Node *) rtle->expr; - Oid lcoltype = exprType(lcolnode); - Oid rcoltype = exprType(rcolnode); - Node *bestexpr; - int bestlocation; - Oid rescoltype; - int32 rescoltypmod; - Oid rescolcoll; - - /* select common type, same as CASE et al */ - rescoltype = select_common_type(pstate, - list_make2(lcolnode, rcolnode), - context, - &bestexpr); - bestlocation = exprLocation(bestexpr); + return (Node *) op; + } +} - /* - * Verify the coercions are actually possible. If not, we'd fail - * later anyway, but we want to fail now while we have sufficient - * context to produce an error cursor position. - * - * For all non-UNKNOWN-type cases, we verify coercibility but we - * don't modify the child's expression, for fear of changing the - * child query's semantics. - * - * If a child expression is an UNKNOWN-type Const or Param, we - * want to replace it with the coerced expression. This can only - * happen when the child is a leaf set-op node. It's safe to - * replace the expression because if the child query's semantics - * depended on the type of this output column, it'd have already - * coerced the UNKNOWN to something else. We want to do this - * because (a) we want to verify that a Const is valid for the - * target type, or resolve the actual type of an UNKNOWN Param, - * and (b) we want to avoid unnecessary discrepancies between the - * output type of the child query and the resolved target type. - * Such a discrepancy would disable optimization in the planner. - * - * If it's some other UNKNOWN-type node, eg a Var, we do nothing - * (knowing that coerce_to_common_type would fail). The planner - * is sometimes able to fold an UNKNOWN Var to a constant before - * it has to coerce the type, so failing now would just break - * cases that might work. - */ - if (lcoltype != UNKNOWNOID) - lcolnode = coerce_to_common_type(pstate, lcolnode, - rescoltype, context); - else if (IsA(lcolnode, Const) || - IsA(lcolnode, Param)) - { - lcolnode = coerce_to_common_type(pstate, lcolnode, - rescoltype, context); - ltle->expr = (Expr *) lcolnode; - } +/* + * constructSetOpTargetlist + * Compute the types, typmods and collations of the columns in the target + * list of the given set operation. + * + * For every pair of columns in the targetlists of the children, compute the + * common type, typmod, and collation representing the output (UNION) column. + * If targetlist is not NULL, also build the dummy output targetlist + * containing non-resjunk output columns. The values are stored into the + * given SetOperationStmt node. context is a string for error messages + * ("UNION" etc.). recursive is true if it is a recursive union. + */ +void +constructSetOpTargetlist(ParseState *pstate, SetOperationStmt *op, + const List *ltargetlist, const List *rtargetlist, + List **targetlist, const char *context, bool recursive) +{ + ListCell *ltl; + ListCell *rtl; - if (rcoltype != UNKNOWNOID) - rcolnode = coerce_to_common_type(pstate, rcolnode, - rescoltype, context); - else if (IsA(rcolnode, Const) || - IsA(rcolnode, Param)) - { - rcolnode = coerce_to_common_type(pstate, rcolnode, - rescoltype, context); - rtle->expr = (Expr *) rcolnode; - } + /* + * Verify that the two children have the same number of non-junk columns, + * and determine the types of the merged output columns. + */ + if (list_length(ltargetlist) != list_length(rtargetlist)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("each %s query must have the same number of columns", + context), + parser_errposition(pstate, + exprLocation((const Node *) rtargetlist)))); - rescoltypmod = select_common_typmod(pstate, - list_make2(lcolnode, rcolnode), - rescoltype); + if (targetlist) + *targetlist = NIL; + op->colTypes = NIL; + op->colTypmods = NIL; + op->colCollations = NIL; + op->groupClauses = NIL; - /* - * Select common collation. A common collation is required for - * all set operators except UNION ALL; see SQL:2008 7.13 Syntax Rule 15c. (If we fail to identify a common - * collation for a UNION ALL column, the colCollations element - * will be set to InvalidOid, which may result in a runtime error - * if something at a higher query level wants to use the column's - * collation.) - */ - rescolcoll = select_common_collation(pstate, - list_make2(lcolnode, rcolnode), - (op->op == SETOP_UNION && op->all)); + forboth(ltl, ltargetlist, rtl, rtargetlist) + { + TargetEntry *ltle = (TargetEntry *) lfirst(ltl); + TargetEntry *rtle = (TargetEntry *) lfirst(rtl); + Node *lcolnode = (Node *) ltle->expr; + Node *rcolnode = (Node *) rtle->expr; + Oid lcoltype = exprType(lcolnode); + Oid rcoltype = exprType(rcolnode); + Node *bestexpr; + int bestlocation; + Oid rescoltype; + int32 rescoltypmod; + Oid rescolcoll; + + /* select common type, same as CASE et al */ + rescoltype = select_common_type(pstate, + list_make2(lcolnode, rcolnode), + context, + &bestexpr); + bestlocation = exprLocation(bestexpr); + + /* + * Verify the coercions are actually possible. If not, we'd fail + * later anyway, but we want to fail now while we have sufficient + * context to produce an error cursor position. + * + * For all non-UNKNOWN-type cases, we verify coercibility but we don't + * modify the child's expression, for fear of changing the child + * query's semantics. + * + * If a child expression is an UNKNOWN-type Const or Param, we want to + * replace it with the coerced expression. This can only happen when + * the child is a leaf set-op node. It's safe to replace the + * expression because if the child query's semantics depended on the + * type of this output column, it'd have already coerced the UNKNOWN + * to something else. We want to do this because (a) we want to + * verify that a Const is valid for the target type, or resolve the + * actual type of an UNKNOWN Param, and (b) we want to avoid + * unnecessary discrepancies between the output type of the child + * query and the resolved target type. Such a discrepancy would + * disable optimization in the planner. + * + * If it's some other UNKNOWN-type node, eg a Var, we do nothing + * (knowing that coerce_to_common_type would fail). The planner is + * sometimes able to fold an UNKNOWN Var to a constant before it has + * to coerce the type, so failing now would just break cases that + * might work. + */ + if (lcoltype != UNKNOWNOID) + lcolnode = coerce_to_common_type(pstate, lcolnode, + rescoltype, context); + else if (IsA(lcolnode, Const) || + IsA(lcolnode, Param)) + { + lcolnode = coerce_to_common_type(pstate, lcolnode, + rescoltype, context); + ltle->expr = (Expr *) lcolnode; + } - /* emit results */ - op->colTypes = lappend_oid(op->colTypes, rescoltype); - op->colTypmods = lappend_int(op->colTypmods, rescoltypmod); - op->colCollations = lappend_oid(op->colCollations, rescolcoll); + if (rcoltype != UNKNOWNOID) + rcolnode = coerce_to_common_type(pstate, rcolnode, + rescoltype, context); + else if (IsA(rcolnode, Const) || + IsA(rcolnode, Param)) + { + rcolnode = coerce_to_common_type(pstate, rcolnode, + rescoltype, context); + rtle->expr = (Expr *) rcolnode; + } - /* - * For all cases except UNION ALL, identify the grouping operators - * (and, if available, sorting operators) that will be used to - * eliminate duplicates. - */ - if (op->op != SETOP_UNION || !op->all) - { - ParseCallbackState pcbstate; + rescoltypmod = select_common_typmod(pstate, + list_make2(lcolnode, rcolnode), + rescoltype); - setup_parser_errposition_callback(&pcbstate, pstate, - bestlocation); + /* + * Select common collation. A common collation is required for all + * set operators except UNION ALL; see SQL:2008 7.13 Syntax Rule 15c. (If we fail to identify a common + * collation for a UNION ALL column, the colCollations element will be + * set to InvalidOid, which may result in a runtime error if something + * at a higher query level wants to use the column's collation.) + */ + rescolcoll = select_common_collation(pstate, + list_make2(lcolnode, rcolnode), + (op->op == SETOP_UNION && op->all)); - /* - * If it's a recursive union, we need to require hashing - * support. - */ - op->groupClauses = lappend(op->groupClauses, - makeSortGroupClauseForSetOp(rescoltype, recursive)); + /* emit results */ + op->colTypes = lappend_oid(op->colTypes, rescoltype); + op->colTypmods = lappend_int(op->colTypmods, rescoltypmod); + op->colCollations = lappend_oid(op->colCollations, rescolcoll); - cancel_parser_errposition_callback(&pcbstate); - } + /* + * For all cases except UNION ALL, identify the grouping operators + * (and, if available, sorting operators) that will be used to + * eliminate duplicates. + */ + if (op->op != SETOP_UNION || !op->all) + { + ParseCallbackState pcbstate; - /* - * Construct a dummy tlist entry to return. We use a SetToDefault - * node for the expression, since it carries exactly the fields - * needed, but any other expression node type would do as well. - */ - if (targetlist) - { - SetToDefault *rescolnode = makeNode(SetToDefault); - TargetEntry *restle; - - rescolnode->typeId = rescoltype; - rescolnode->typeMod = rescoltypmod; - rescolnode->collation = rescolcoll; - rescolnode->location = bestlocation; - restle = makeTargetEntry((Expr *) rescolnode, - 0, /* no need to set resno */ - NULL, - false); - *targetlist = lappend(*targetlist, restle); - } + setup_parser_errposition_callback(&pcbstate, pstate, + bestlocation); + + /* If it's a recursive union, we need to require hashing support. */ + op->groupClauses = lappend(op->groupClauses, + makeSortGroupClauseForSetOp(rescoltype, recursive)); + + cancel_parser_errposition_callback(&pcbstate); } - return (Node *) op; + /* + * Construct a dummy tlist entry to return. We use a SetToDefault + * node for the expression, since it carries exactly the fields + * needed, but any other expression node type would do as well. + */ + if (targetlist) + { + SetToDefault *rescolnode = makeNode(SetToDefault); + TargetEntry *restle; + + rescolnode->typeId = rescoltype; + rescolnode->typeMod = rescoltypmod; + rescolnode->collation = rescolcoll; + rescolnode->location = bestlocation; + restle = makeTargetEntry((Expr *) rescolnode, + 0, /* no need to set resno */ + NULL, + false); + *targetlist = lappend(*targetlist, restle); + } } } @@ -2548,7 +2862,6 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) Node *qual; qry->commandType = CMD_UPDATE; - pstate->p_is_insert = false; /* process the WITH clause independently of all else */ if (stmt->withClause) @@ -2562,6 +2875,21 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) stmt->relation->inh, true, ACL_UPDATE); + + /* disallow UPDATE ... WHERE CURRENT OF on a view */ + if (stmt->whereClause && + IsA(stmt->whereClause, CurrentOfExpr) && + pstate->p_target_relation->rd_rel->relkind == RELKIND_VIEW) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("WHERE CURRENT OF on a view is not implemented")); + + if (stmt->forPortionOf) + qry->forPortionOf = transformForPortionOfClause(pstate, + qry->resultRelation, + stmt->forPortionOf, + true); + nsitem = pstate->p_target_nsitem; /* subqueries in FROM cannot access the result relation */ @@ -2588,7 +2916,8 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) * Now we are done with SELECT-like processing, and can get on with * transforming the target list to match the UPDATE target columns. */ - qry->targetList = transformUpdateTargetList(pstate, stmt->targetList); + qry->targetList = transformUpdateTargetList(pstate, stmt->targetList, + qry->forPortionOf); qry->rtable = pstate->p_rtable; qry->rteperminfos = pstate->p_rteperminfos; @@ -2607,7 +2936,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) * handle SET clause in UPDATE/MERGE/INSERT ... ON CONFLICT UPDATE */ List * -transformUpdateTargetList(ParseState *pstate, List *origTlist) +transformUpdateTargetList(ParseState *pstate, List *origTlist, ForPortionOfExpr *forPortionOf) { List *tlist = NIL; RTEPermissionInfo *target_perminfo; @@ -2660,6 +2989,20 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist) errhint("SET target columns cannot be qualified with the relation name.") : 0, parser_errposition(pstate, origTarget->location))); + /* + * If this is a FOR PORTION OF update, forbid directly setting the + * range column, since that would conflict with the implicit updates. + */ + if (forPortionOf != NULL) + { + if (attrno == forPortionOf->rangeVar->varattno) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot update column \"%s\" because it is used in FOR PORTION OF", + origTarget->name), + parser_errposition(pstate, origTarget->location))); + } + updateTargetListEntry(pstate, tle, origTarget->name, attrno, origTarget->indirection, @@ -2694,8 +3037,7 @@ addNSItemForReturning(ParseState *pstate, const char *aliasname, colnames = pstate->p_target_nsitem->p_rte->eref->colnames; numattrs = list_length(colnames); - nscolumns = (ParseNamespaceColumn *) - palloc(numattrs * sizeof(ParseNamespaceColumn)); + nscolumns = palloc_array(ParseNamespaceColumn, numattrs); memcpy(nscolumns, pstate->p_target_nsitem->p_nscolumns, numattrs * sizeof(ParseNamespaceColumn)); @@ -2705,7 +3047,7 @@ addNSItemForReturning(ParseState *pstate, const char *aliasname, nscolumns[i].p_varreturningtype = returning_type; /* build the nsitem, copying most fields from the target relation */ - nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem)); + nsitem = palloc_object(ParseNamespaceItem); nsitem->p_names = makeAlias(aliasname, colnames); nsitem->p_rte = pstate->p_target_nsitem->p_rte; nsitem->p_rtindex = pstate->p_target_nsitem->p_rtindex; @@ -2846,20 +3188,13 @@ transformReturningClause(ParseState *pstate, Query *qry, static Query * transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) { - Query *qry = makeNode(Query); + Query *qry; ColumnRef *cref = makeNode(ColumnRef); List *indirection = stmt->indirection; int nnames = stmt->nnames; - SelectStmt *sstmt = stmt->val; Node *target; - Oid targettype; - int32 targettypmod; - Oid targetcollation; - List *tlist; - TargetEntry *tle; - Oid type_id; - Node *qual; - ListCell *l; + SelectStmtPassthrough passthru; + bool save_resolve_unknowns; /* * First, construct a ColumnRef for the target variable. If the target @@ -2885,33 +3220,62 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) /* * Transform the target reference. Typically we will get back a Param - * node, but there's no reason to be too picky about its type. + * node, but there's no reason to be too picky about its type. (Note that + * we must do this before calling transformSelectStmt. It's tempting to + * do it inside transformPLAssignStmtTarget, but we need to do it before + * adding any FROM tables to the pstate's namespace, else we might wrongly + * resolve the target as a table column.) */ target = transformExpr(pstate, (Node *) cref, EXPR_KIND_UPDATE_TARGET); - targettype = exprType(target); - targettypmod = exprTypmod(target); - targetcollation = exprCollation(target); + + /* Set up passthrough data for transformPLAssignStmtTarget */ + passthru.stmt = stmt; + passthru.target = target; + passthru.indirection = indirection; /* - * The rest mostly matches transformSelectStmt, except that we needn't - * consider WITH or INTO, and we build a targetlist our own way. + * To avoid duplicating a lot of code, we use transformSelectStmt to do + * almost all of the work. However, we need to do additional processing + * on the SELECT's targetlist after it's been transformed, but before + * possible addition of targetlist items for ORDER BY or GROUP BY. + * transformSelectStmt knows it should call transformPLAssignStmtTarget if + * it's passed a passthru argument. + * + * Also, disable resolution of unknown-type tlist items; PL/pgSQL wants to + * deal with that itself. */ - qry->commandType = CMD_SELECT; - pstate->p_is_insert = false; + save_resolve_unknowns = pstate->p_resolve_unknowns; + pstate->p_resolve_unknowns = false; + qry = transformSelectStmt(pstate, stmt->val, &passthru); + pstate->p_resolve_unknowns = save_resolve_unknowns; - /* make FOR UPDATE/FOR SHARE info available to addRangeTableEntry */ - pstate->p_locking_clause = sstmt->lockingClause; - - /* make WINDOW info available for window functions, too */ - pstate->p_windowdefs = sstmt->windowClause; + return qry; +} - /* process the FROM clause */ - transformFromClause(pstate, sstmt->fromClause); +/* + * Callback function to adjust a SELECT's tlist to make the output suitable + * for assignment to a PLAssignStmt's target variable. + * + * Note: we actually modify the tle->expr in-place, but the function's API + * is set up to not presume that. + */ +static List * +transformPLAssignStmtTarget(ParseState *pstate, List *tlist, + SelectStmtPassthrough *passthru) +{ + PLAssignStmt *stmt = passthru->stmt; + Node *target = passthru->target; + List *indirection = passthru->indirection; + Oid targettype; + int32 targettypmod; + Oid targetcollation; + TargetEntry *tle; + Oid type_id; - /* initially transform the targetlist as if in SELECT */ - tlist = transformTargetList(pstate, sstmt->targetList, - EXPR_KIND_SELECT_TARGET); + targettype = exprType(target); + targettypmod = exprTypmod(target); + targetcollation = exprCollation(target); /* we should have exactly one targetlist item */ if (list_length(tlist) != 1) @@ -2989,96 +3353,7 @@ transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt) pstate->p_expr_kind = EXPR_KIND_NONE; - qry->targetList = list_make1(tle); - - /* transform WHERE */ - qual = transformWhereClause(pstate, sstmt->whereClause, - EXPR_KIND_WHERE, "WHERE"); - - /* initial processing of HAVING clause is much like WHERE clause */ - qry->havingQual = transformWhereClause(pstate, sstmt->havingClause, - EXPR_KIND_HAVING, "HAVING"); - - /* - * Transform sorting/grouping stuff. Do ORDER BY first because both - * transformGroupClause and transformDistinctClause need the results. Note - * that these functions can also change the targetList, so it's passed to - * them by reference. - */ - qry->sortClause = transformSortClause(pstate, - sstmt->sortClause, - &qry->targetList, - EXPR_KIND_ORDER_BY, - false /* allow SQL92 rules */ ); - - qry->groupClause = transformGroupClause(pstate, - sstmt->groupClause, - &qry->groupingSets, - &qry->targetList, - qry->sortClause, - EXPR_KIND_GROUP_BY, - false /* allow SQL92 rules */ ); - - if (sstmt->distinctClause == NIL) - { - qry->distinctClause = NIL; - qry->hasDistinctOn = false; - } - else if (linitial(sstmt->distinctClause) == NULL) - { - /* We had SELECT DISTINCT */ - qry->distinctClause = transformDistinctClause(pstate, - &qry->targetList, - qry->sortClause, - false); - qry->hasDistinctOn = false; - } - else - { - /* We had SELECT DISTINCT ON */ - qry->distinctClause = transformDistinctOnClause(pstate, - sstmt->distinctClause, - &qry->targetList, - qry->sortClause); - qry->hasDistinctOn = true; - } - - /* transform LIMIT */ - qry->limitOffset = transformLimitClause(pstate, sstmt->limitOffset, - EXPR_KIND_OFFSET, "OFFSET", - sstmt->limitOption); - qry->limitCount = transformLimitClause(pstate, sstmt->limitCount, - EXPR_KIND_LIMIT, "LIMIT", - sstmt->limitOption); - qry->limitOption = sstmt->limitOption; - - /* transform window clauses after we have seen all window functions */ - qry->windowClause = transformWindowDefinitions(pstate, - pstate->p_windowdefs, - &qry->targetList); - - qry->rtable = pstate->p_rtable; - qry->rteperminfos = pstate->p_rteperminfos; - qry->jointree = makeFromExpr(pstate->p_joinlist, qual); - - qry->hasSubLinks = pstate->p_hasSubLinks; - qry->hasWindowFuncs = pstate->p_hasWindowFuncs; - qry->hasTargetSRFs = pstate->p_hasTargetSRFs; - qry->hasAggs = pstate->p_hasAggs; - - foreach(l, sstmt->lockingClause) - { - transformLockingClause(pstate, qry, - (LockingClause *) lfirst(l), false); - } - - assign_query_collations(pstate, qry); - - /* this must be done after collations, for reliable comparison of exprs */ - if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual) - parseCheckAggregates(pstate, qry); - - return qry; + return list_make1(tle); } @@ -3250,6 +3525,8 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt) /* additional work needed for CREATE MATERIALIZED VIEW */ if (stmt->objtype == OBJECT_MATVIEW) { + ObjectAddress temp_object; + /* * Prohibit a data-modifying CTE in the query used to create a * materialized view. It's not sufficiently clear what the user would @@ -3265,10 +3542,12 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt) * creation query. It would be hard to refresh data or incrementally * maintain it if a source disappeared. */ - if (isQueryUsingTempRelation(query)) + if (query_uses_temp_object(query, &temp_object)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("materialized views must not use temporary tables or views"))); + errmsg("materialized views must not use temporary objects"), + errdetail("This view depends on temporary %s.", + getObjectDescription(&temp_object, false)))); /* * A materialized view would either need to save parameters for use in diff --git a/src/backend/parser/check_keywords.pl b/src/backend/parser/check_keywords.pl index 2f25b2a1071ec..9a0b6242325ce 100644 --- a/src/backend/parser/check_keywords.pl +++ b/src/backend/parser/check_keywords.pl @@ -4,7 +4,7 @@ # Usage: check_keywords.pl gram.y kwlist.h # src/backend/parser/check_keywords.pl -# Copyright (c) 2009-2025, PostgreSQL Global Development Group +# Copyright (c) 2009-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0b5652071d119..ff4e1388c5500 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -6,7 +6,7 @@ * gram.y * POSTGRESQL BISON rules/actions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -120,6 +120,7 @@ typedef struct SelectLimit typedef struct GroupClause { bool distinct; + bool all; List *list; } GroupClause; @@ -154,7 +155,6 @@ static void base_yyerror(YYLTYPE *yylloc, core_yyscan_t yyscanner, const char *msg); static RawStmt *makeRawStmt(Node *stmt, int stmt_location); static void updateRawStmtEnd(RawStmt *rs, int end_location); -static void updatePreparableStmtEnd(Node *n, int end_location); static Node *makeColumnRef(char *colname, List *indirection, int location, core_yyscan_t yyscanner); static Node *makeTypeCast(Node *arg, TypeName *typename, int location); @@ -178,13 +178,13 @@ static void insertSelectOptions(SelectStmt *stmt, SelectLimit *limitClause, WithClause *withClause, core_yyscan_t yyscanner); -static Node *makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg, int location); +static Node *makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg); static Node *doNegate(Node *n, int location); static void doNegateFloat(Float *v); static Node *makeAndExpr(Node *lexpr, Node *rexpr, int location); static Node *makeOrExpr(Node *lexpr, Node *rexpr, int location); static Node *makeNotExpr(Node *expr, int location); -static Node *makeAArrayExpr(List *elements, int location); +static Node *makeAArrayExpr(List *elements, int location, int location_end); static Node *makeSQLValueFunction(SQLValueFunctionOp op, int32 typmod, int location); static Node *makeXmlExpr(XmlExprOp op, char *name, List *named_args, @@ -202,6 +202,11 @@ static void processCASbits(int cas_bits, int location, const char *constrType, bool *not_valid, bool *no_inherit, core_yyscan_t yyscanner); static PartitionStrategy parsePartitionStrategy(char *strategy, int location, core_yyscan_t yyscanner); +static void preprocess_pub_all_objtype_list(List *all_objects_list, + List **pubobjects, + bool *all_tables, + bool *all_sequences, + core_yyscan_t yyscanner); static void preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner); static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); @@ -258,8 +263,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); PartitionElem *partelem; PartitionSpec *partspec; PartitionBoundSpec *partboundspec; + SinglePartitionSpec *singlepartspec; RoleSpec *rolespec; PublicationObjSpec *publicationobjectspec; + PublicationAllObjSpec *publicationallobjectspec; struct SelectLimit *selectlimit; SetQuantifier setquantifier; struct GroupClause *groupclause; @@ -281,13 +288,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); AlterCompositeTypeStmt AlterUserMappingStmt AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt AlterStatsStmt AlterDefaultPrivilegesStmt DefACLAction - AnalyzeStmt CallStmt ClosePortalStmt ClusterStmt CommentStmt + AnalyzeStmt CallStmt ClosePortalStmt CommentStmt ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt CreateDomainStmt CreateExtensionStmt CreateGroupStmt CreateOpClassStmt CreateOpFamilyStmt AlterOpFamilyStmt CreatePLangStmt CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt + CreatePropGraphStmt AlterPropGraphStmt CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt DropOpClassStmt DropOpFamilyStmt DropStmt @@ -298,12 +306,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt ListenStmt LoadStmt LockStmt MergeStmt NotifyStmt ExplainableStmt PreparableStmt CreateFunctionStmt AlterFunctionStmt ReindexStmt RemoveAggrStmt - RemoveFuncStmt RemoveOperStmt RenameStmt ReturnStmt RevokeStmt RevokeRoleStmt + RemoveFuncStmt RemoveOperStmt RenameStmt RepackStmt ReturnStmt RevokeStmt RevokeRoleStmt RuleActionStmt RuleActionStmtOrEmpty RuleStmt SecLabelStmt SelectStmt TransactionStmt TransactionStmtLegacy TruncateStmt UnlistenStmt UpdateStmt VacuumStmt VariableResetStmt VariableSetStmt VariableShowStmt - ViewStmt CheckPointStmt CreateConversionStmt + ViewStmt WaitStmt CheckPointStmt CreateConversionStmt DeallocateStmt PrepareStmt ExecuteStmt DropOwnedStmt ReassignOwnedStmt AlterTSConfigurationStmt AlterTSDictionaryStmt @@ -317,8 +325,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_single_name %type opt_qualified_name -%type opt_concurrently +%type opt_concurrently opt_usingindex %type opt_drop_behavior +%type opt_utility_option_list +%type opt_wait_with_clause +%type utility_option_list +%type utility_option_elem +%type utility_option_name +%type utility_option_arg %type alter_column_default opclass_item opclass_drop alter_using %type add_drop opt_asc_desc opt_nulls_order @@ -339,10 +353,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); create_extension_opt_item alter_extension_opt_item %type opt_lock lock_type cast_context -%type utility_option_name -%type utility_option_elem -%type utility_option_list -%type utility_option_arg %type drop_option %type opt_or_replace opt_no opt_grant_grant_option @@ -446,7 +456,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); transform_element_list transform_type_list TriggerTransitions TriggerReferencing vacuum_relation_list opt_vacuum_relation_list - drop_option_list pub_obj_list + drop_option_list pub_obj_list pub_all_obj_type_list + pub_except_obj_list opt_pub_except_clause %type returning_clause %type returning_option @@ -473,7 +484,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type OptNoLog %type OnCommitOption -%type for_locking_strength +%type for_locking_strength opt_for_locking_strength %type for_locking_item %type for_locking_clause opt_for_locking_clause for_locking_items %type locked_rels_list @@ -523,7 +534,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type def_elem reloption_elem old_aggr_elem operator_def_elem %type def_arg columnElem where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound - columnref in_expr having_clause func_table xmltable array_expr + columnref having_clause func_table xmltable array_expr OptWhereClause operator_def_arg %type opt_column_and_period_list %type rowsfrom_item rowsfrom_list opt_col_def_list @@ -548,6 +559,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type relation_expr %type extended_relation_expr %type relation_expr_opt_alias +%type for_portion_of_opt_alias +%type for_portion_of_clause %type tablesample_clause opt_repeatable_clause %type target_el set_target insert_column_item @@ -557,7 +570,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type generic_option_list alter_generic_option_list %type reindex_target_relation reindex_target_all -%type opt_reindex_option_list %type copy_generic_opt_arg copy_generic_opt_arg_list_item %type copy_generic_opt_elem @@ -585,6 +597,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type var_value zone_value %type auth_ident RoleSpec opt_granted_by %type PublicationObjSpec +%type PublicationExceptObjSpec +%type PublicationAllObjSpec %type unreserved_keyword type_func_name_keyword %type col_name_keyword reserved_keyword @@ -609,8 +623,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_provider security_label -%type xml_attribute_el -%type xml_attribute_list xml_attributes +%type labeled_expr +%type labeled_expr_list xml_attributes %type xml_root_version opt_xml_root_standalone %type xmlexists_argument %type document_or_content @@ -632,7 +646,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type window_clause window_definition_list opt_partition_clause %type window_definition over_clause window_specification opt_frame_clause frame_extent frame_bound -%type opt_window_exclusion_clause +%type null_treatment opt_window_exclusion_clause %type opt_existing_window_name %type opt_if_not_exists %type opt_unique_null_treatment @@ -641,6 +655,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type part_elem %type part_params %type PartitionBoundSpec +%type SinglePartitionSpec +%type partitions_list %type hash_partbound %type hash_partbound_elem @@ -672,6 +688,35 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type vertex_tables_clause edge_tables_clause + opt_vertex_tables_clause opt_edge_tables_clause + vertex_table_list + opt_graph_table_key_clause + edge_table_list + source_vertex_table destination_vertex_table + opt_element_table_label_and_properties + label_and_properties_list + add_label_list +%type vertex_table_definition edge_table_definition +%type opt_propgraph_table_alias +%type element_table_label_clause +%type label_and_properties element_table_properties + add_label +%type vertex_or_edge + +%type opt_graph_pattern_quantifier + path_pattern_list + path_pattern + path_pattern_expression + path_term +%type graph_pattern + path_factor + path_primary + opt_is_label_expression + label_expression + label_disjunction + label_term +%type opt_colid /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -715,22 +760,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS - DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC + DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC DESTINATION DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP - EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P ERROR_P - ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN - EXPRESSION EXTENSION EXTERNAL EXTRACT + EACH EDGE ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENFORCED ENUM_P + ERROR_P ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS + EXPLAIN EXPRESSION EXTENSION EXTERNAL EXTRACT FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS - GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING GROUPS + GENERATED GLOBAL GRANT GRANTED GRAPH GRAPH_TABLE GREATEST GROUP_P GROUPING GROUPS HANDLER HAVING HEADER_P HOLD HOUR_P - IDENTITY_P IF_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE + IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION @@ -742,12 +787,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); LABEL LANGUAGE LARGE_P LAST_P LATERAL_P LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL - LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED + LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED LSN_P MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO NODE NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -756,22 +801,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PATH - PERIOD PLACING PLAN PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH + PERIOD PLACING PLAN PLANS POLICY PORTION POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY - PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION + PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PROPERTIES PROPERTY PUBLICATION QUOTE QUOTES RANGE READ REAL REASSIGN RECURSIVE REF_P REFERENCES REFERENCING - REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA - RESET RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP + REFRESH REINDEX RELATIONSHIP RELATIVE_P RELEASE RENAME REPACK REPEATABLE REPLACE REPLICA + RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROUTINE ROUTINES ROW ROWS RULE SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW - SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SOURCE SQL_P STABLE STANDALONE_P + SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SOURCE SQL_P STABLE STANDALONE_P START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRING_P STRIP_P SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER @@ -784,9 +829,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); UNLISTEN UNLOGGED UNTIL UPDATE USER USING VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING - VERBOSE VERSION_P VIEW VIEWS VIRTUAL VOLATILE + VERBOSE VERSION_P VERTEX VIEW VIEWS VIRTUAL VOLATILE - WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE + WAIT WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE XML_P XMLATTRIBUTES XMLCONCAT XMLELEMENT XMLEXISTS XMLFOREST XMLNAMESPACES XMLPARSE XMLPI XMLROOT XMLSERIALIZE XMLTABLE @@ -876,13 +921,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * json_predicate_type_constraint and json_key_uniqueness_constraint_opt * productions (see comments there). * + * TO is assigned the same precedence as IDENT, to support the opt_interval + * production (see comment there). + * * Like the UNBOUNDED PRECEDING/FOLLOWING case, NESTED is assigned a lower * precedence than PATH to fix ambiguity in the json_table production. */ %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP - SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH -%left Op OPERATOR /* multi-character ops and user-defined operators */ + SET KEYS OBJECT_P SCALAR TO USING VALUE_P WITH WITHOUT PATH +%left Op OPERATOR RIGHT_ARROW '|' /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' %left '^' @@ -1009,6 +1057,7 @@ stmt: | AlterOperatorStmt | AlterTypeStmt | AlterPolicyStmt + | AlterPropGraphStmt | AlterSeqStmt | AlterSystemStmt | AlterTableStmt @@ -1026,7 +1075,6 @@ stmt: | CallStmt | CheckPointStmt | ClosePortalStmt - | ClusterStmt | CommentStmt | ConstraintsSetStmt | CopyStmt @@ -1049,6 +1097,7 @@ stmt: | AlterOpFamilyStmt | CreatePolicyStmt | CreatePLangStmt + | CreatePropGraphStmt | CreateSchemaStmt | CreateSeqStmt | CreateStmt @@ -1100,6 +1149,7 @@ stmt: | RemoveFuncStmt | RemoveOperStmt | RenameStmt + | RepackStmt | RevokeStmt | RevokeRoleStmt | RuleStmt @@ -1114,6 +1164,7 @@ stmt: | VariableSetStmt | VariableShowStmt | ViewStmt + | WaitStmt | /*EMPTY*/ { $$ = NULL; } ; @@ -1136,12 +1187,52 @@ opt_concurrently: | /*EMPTY*/ { $$ = false; } ; +opt_usingindex: + USING INDEX { $$ = true; } + | /* EMPTY */ { $$ = false; } + ; + opt_drop_behavior: CASCADE { $$ = DROP_CASCADE; } | RESTRICT { $$ = DROP_RESTRICT; } | /* EMPTY */ { $$ = DROP_RESTRICT; /* default */ } ; +opt_utility_option_list: + '(' utility_option_list ')' { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + +utility_option_list: + utility_option_elem + { + $$ = list_make1($1); + } + | utility_option_list ',' utility_option_elem + { + $$ = lappend($1, $3); + } + ; + +utility_option_elem: + utility_option_name utility_option_arg + { + $$ = makeDefElem($1, $2, @1); + } + ; + +utility_option_name: + NonReservedWord { $$ = $1; } + | analyze_keyword { $$ = "analyze"; } + | FORMAT_LA { $$ = "format"; } + ; + +utility_option_arg: + opt_boolean_or_string { $$ = (Node *) makeString($1); } + | NumericOnly { $$ = (Node *) $1; } + | /* EMPTY */ { $$ = NULL; } + ; + /***************************************************************************** * * CALL statement @@ -1588,8 +1679,11 @@ OptSchemaEltList: schema_stmt: CreateStmt | IndexStmt + | CreateDomainStmt + | CreateFunctionStmt | CreateSeqStmt | CreateTrigStmt + | DefineStmt | GrantStmt | ViewStmt ; @@ -1675,6 +1769,26 @@ generic_set: n->location = @3; $$ = n; } + | var_name TO NULL_P + { + VariableSetStmt *n = makeNode(VariableSetStmt); + + n->kind = VAR_SET_VALUE; + n->name = $1; + n->args = list_make1(makeNullAConst(@3)); + n->location = @3; + $$ = n; + } + | var_name '=' NULL_P + { + VariableSetStmt *n = makeNode(VariableSetStmt); + + n->kind = VAR_SET_VALUE; + n->name = $1; + n->args = list_make1(makeNullAConst(@3)); + n->location = @3; + $$ = n; + } | var_name TO DEFAULT { VariableSetStmt *n = makeNode(VariableSetStmt); @@ -2029,11 +2143,12 @@ constraints_set_mode: * Checkpoint statement */ CheckPointStmt: - CHECKPOINT + CHECKPOINT opt_utility_option_list { CheckPointStmt *n = makeNode(CheckPointStmt); $$ = (Node *) n; + n->options = $2; } ; @@ -2322,6 +2437,23 @@ alter_table_cmds: | alter_table_cmds ',' alter_table_cmd { $$ = lappend($1, $3); } ; +partitions_list: + SinglePartitionSpec { $$ = list_make1($1); } + | partitions_list ',' SinglePartitionSpec { $$ = lappend($1, $3); } + ; + +SinglePartitionSpec: + PARTITION qualified_name PartitionBoundSpec + { + SinglePartitionSpec *n = makeNode(SinglePartitionSpec); + + n->name = $2; + n->bound = $3; + + $$ = n; + } + ; + partition_cmd: /* ALTER TABLE ATTACH PARTITION FOR VALUES */ ATTACH PARTITION qualified_name PartitionBoundSpec @@ -2332,6 +2464,7 @@ partition_cmd: n->subtype = AT_AttachPartition; cmd->name = $3; cmd->bound = $4; + cmd->partlist = NIL; cmd->concurrent = false; n->def = (Node *) cmd; @@ -2346,6 +2479,7 @@ partition_cmd: n->subtype = AT_DetachPartition; cmd->name = $3; cmd->bound = NULL; + cmd->partlist = NIL; cmd->concurrent = $4; n->def = (Node *) cmd; @@ -2359,6 +2493,35 @@ partition_cmd: n->subtype = AT_DetachPartitionFinalize; cmd->name = $3; cmd->bound = NULL; + cmd->partlist = NIL; + cmd->concurrent = false; + n->def = (Node *) cmd; + $$ = (Node *) n; + } + /* ALTER TABLE SPLIT PARTITION INTO () */ + | SPLIT PARTITION qualified_name INTO '(' partitions_list ')' + { + AlterTableCmd *n = makeNode(AlterTableCmd); + PartitionCmd *cmd = makeNode(PartitionCmd); + + n->subtype = AT_SplitPartition; + cmd->name = $3; + cmd->bound = NULL; + cmd->partlist = $6; + cmd->concurrent = false; + n->def = (Node *) cmd; + $$ = (Node *) n; + } + /* ALTER TABLE MERGE PARTITIONS () INTO */ + | MERGE PARTITIONS '(' qualified_name_list ')' INTO qualified_name + { + AlterTableCmd *n = makeNode(AlterTableCmd); + PartitionCmd *cmd = makeNode(PartitionCmd); + + n->subtype = AT_MergePartitions; + cmd->name = $7; + cmd->bound = NULL; + cmd->partlist = $4; cmd->concurrent = false; n->def = (Node *) cmd; $$ = (Node *) n; @@ -2375,6 +2538,7 @@ index_partition_cmd: n->subtype = AT_AttachPartition; cmd->name = $3; cmd->bound = NULL; + cmd->partlist = NIL; cmd->concurrent = false; n->def = (Node *) cmd; @@ -2669,6 +2833,12 @@ alter_table_cmd: c->alterDeferrability = true; if ($4 & CAS_NO_INHERIT) c->alterInheritability = true; + /* handle unsupported case with specific error message */ + if ($4 & CAS_NOT_VALID) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("constraints cannot be altered to be NOT VALID"), + parser_errposition(@4)); processCASbits($4, @4, "FOREIGN KEY", &c->deferrable, &c->initdeferred, @@ -3360,7 +3530,7 @@ ClosePortalStmt: * COPY ( query ) TO file [WITH] [(options)] * * where 'query' can be one of: - * { SELECT | UPDATE | INSERT | DELETE } + * { SELECT | UPDATE | INSERT | DELETE | MERGE } * * and 'file' can be one of: * { PROGRAM 'command' | STDIN | STDOUT | 'filename' } @@ -3401,6 +3571,7 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("WHERE clause not allowed with COPY TO"), + errhint("Try the COPY (SELECT ... WHERE ...) TO variant."), parser_errposition(@11))); n->options = NIL; @@ -3417,7 +3588,6 @@ CopyStmt: COPY opt_binary qualified_name opt_column_list { CopyStmt *n = makeNode(CopyStmt); - updatePreparableStmtEnd($3, @4); n->relation = NULL; n->query = $3; n->attlist = NIL; @@ -3488,6 +3658,10 @@ copy_opt_item: { $$ = makeDefElem("format", (Node *) makeString("csv"), @1); } + | JSON + { + $$ = makeDefElem("format", (Node *) makeString("json"), @1); + } | HEADER_P { $$ = makeDefElem("header", (Node *) makeBoolean(true), @1); @@ -3570,6 +3744,10 @@ copy_generic_opt_elem: { $$ = makeDefElem($1, $2, @1); } + | FORMAT_LA copy_generic_opt_arg + { + $$ = makeDefElem("format", $2, @1); + } ; copy_generic_opt_arg: @@ -4486,19 +4664,19 @@ OptWhereClause: key_actions: key_update { - KeyActions *n = palloc(sizeof(KeyActions)); + KeyActions *n = palloc_object(KeyActions); n->updateAction = $1; - n->deleteAction = palloc(sizeof(KeyAction)); + n->deleteAction = palloc_object(KeyAction); n->deleteAction->action = FKCONSTR_ACTION_NOACTION; n->deleteAction->cols = NIL; $$ = n; } | key_delete { - KeyActions *n = palloc(sizeof(KeyActions)); + KeyActions *n = palloc_object(KeyActions); - n->updateAction = palloc(sizeof(KeyAction)); + n->updateAction = palloc_object(KeyAction); n->updateAction->action = FKCONSTR_ACTION_NOACTION; n->updateAction->cols = NIL; n->deleteAction = $1; @@ -4506,7 +4684,7 @@ key_actions: } | key_update key_delete { - KeyActions *n = palloc(sizeof(KeyActions)); + KeyActions *n = palloc_object(KeyActions); n->updateAction = $1; n->deleteAction = $2; @@ -4514,7 +4692,7 @@ key_actions: } | key_delete key_update { - KeyActions *n = palloc(sizeof(KeyActions)); + KeyActions *n = palloc_object(KeyActions); n->updateAction = $2; n->deleteAction = $1; @@ -4522,12 +4700,12 @@ key_actions: } | /*EMPTY*/ { - KeyActions *n = palloc(sizeof(KeyActions)); + KeyActions *n = palloc_object(KeyActions); - n->updateAction = palloc(sizeof(KeyAction)); + n->updateAction = palloc_object(KeyAction); n->updateAction->action = FKCONSTR_ACTION_NOACTION; n->updateAction->cols = NIL; - n->deleteAction = palloc(sizeof(KeyAction)); + n->deleteAction = palloc_object(KeyAction); n->deleteAction->action = FKCONSTR_ACTION_NOACTION; n->deleteAction->cols = NIL; $$ = n; @@ -4555,7 +4733,7 @@ key_delete: ON DELETE_P key_action key_action: NO ACTION { - KeyAction *n = palloc(sizeof(KeyAction)); + KeyAction *n = palloc_object(KeyAction); n->action = FKCONSTR_ACTION_NOACTION; n->cols = NIL; @@ -4563,7 +4741,7 @@ key_action: } | RESTRICT { - KeyAction *n = palloc(sizeof(KeyAction)); + KeyAction *n = palloc_object(KeyAction); n->action = FKCONSTR_ACTION_RESTRICT; n->cols = NIL; @@ -4571,7 +4749,7 @@ key_action: } | CASCADE { - KeyAction *n = palloc(sizeof(KeyAction)); + KeyAction *n = palloc_object(KeyAction); n->action = FKCONSTR_ACTION_CASCADE; n->cols = NIL; @@ -4579,7 +4757,7 @@ key_action: } | SET NULL_P opt_column_list { - KeyAction *n = palloc(sizeof(KeyAction)); + KeyAction *n = palloc_object(KeyAction); n->action = FKCONSTR_ACTION_SETNULL; n->cols = $3; @@ -4587,7 +4765,7 @@ key_action: } | SET DEFAULT opt_column_list { - KeyAction *n = palloc(sizeof(KeyAction)); + KeyAction *n = palloc_object(KeyAction); n->action = FKCONSTR_ACTION_SETDEFAULT; n->cols = $3; @@ -5459,6 +5637,8 @@ fdw_option: | NO HANDLER { $$ = makeDefElem("handler", NULL, @1); } | VALIDATOR handler_name { $$ = makeDefElem("validator", (Node *) $2, @1); } | NO VALIDATOR { $$ = makeDefElem("validator", NULL, @1); } + | CONNECTION handler_name { $$ = makeDefElem("connection", (Node *) $2, @1); } + | NO CONNECTION { $$ = makeDefElem("connection", NULL, @1); } ; fdw_options: @@ -5784,7 +5964,7 @@ import_qualification_type: import_qualification: import_qualification_type '(' relation_expr_list ')' { - ImportQual *n = (ImportQual *) palloc(sizeof(ImportQual)); + ImportQual *n = palloc_object(ImportQual); n->type = $1; n->table_names = $3; @@ -5792,7 +5972,7 @@ import_qualification: } | /*EMPTY*/ { - ImportQual *n = (ImportQual *) palloc(sizeof(ImportQual)); + ImportQual *n = palloc_object(ImportQual); n->type = FDW_IMPORT_SCHEMA_ALL; n->table_names = NIL; $$ = n; @@ -6037,6 +6217,26 @@ CreateTrigStmt: EXECUTE FUNCTION_or_PROCEDURE func_name '(' TriggerFuncArgs ')' { CreateTrigStmt *n = makeNode(CreateTrigStmt); + bool dummy; + + if (($11 & CAS_NOT_VALID) != 0) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("constraint triggers cannot be marked %s", + "NOT VALID"), + parser_errposition(@11)); + if (($11 & CAS_NO_INHERIT) != 0) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("constraint triggers cannot be marked %s", + "NO INHERIT"), + parser_errposition(@11)); + if (($11 & CAS_NOT_ENFORCED) != 0) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("constraint triggers cannot be marked %s", + "NOT ENFORCED"), + parser_errposition(@11)); n->replace = $2; if (n->replace) /* not supported, see CreateTrigger */ @@ -6056,7 +6256,7 @@ CreateTrigStmt: n->whenClause = $15; n->transitionRels = NIL; processCASbits($11, @11, "TRIGGER", - &n->deferrable, &n->initdeferred, NULL, + &n->deferrable, &n->initdeferred, &dummy, NULL, NULL, yyscanner); n->constrrel = $10; $$ = (Node *) n; @@ -7051,6 +7251,7 @@ object_type_any_name: | MATERIALIZED VIEW { $$ = OBJECT_MATVIEW; } | INDEX { $$ = OBJECT_INDEX; } | FOREIGN TABLE { $$ = OBJECT_FOREIGN_TABLE; } + | PROPERTY GRAPH { $$ = OBJECT_PROPGRAPH; } | COLLATION { $$ = OBJECT_COLLATION; } | CONVERSION_P { $$ = OBJECT_CONVERSION; } | STATISTICS { $$ = OBJECT_STATISTIC_EXT; } @@ -7479,6 +7680,8 @@ fetch_args: cursor_name n->portalname = $1; n->direction = FETCH_FORWARD; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_NONE; $$ = (Node *) n; } | from_in cursor_name @@ -7488,6 +7691,19 @@ fetch_args: cursor_name n->portalname = $2; n->direction = FETCH_FORWARD; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_NONE; + $$ = (Node *) n; + } + | SignedIconst opt_from_in cursor_name + { + FetchStmt *n = makeNode(FetchStmt); + + n->portalname = $3; + n->direction = FETCH_FORWARD; + n->howMany = $1; + n->location = @1; + n->direction_keyword = FETCH_KEYWORD_NONE; $$ = (Node *) n; } | NEXT opt_from_in cursor_name @@ -7497,6 +7713,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_FORWARD; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_NEXT; $$ = (Node *) n; } | PRIOR opt_from_in cursor_name @@ -7506,6 +7724,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_BACKWARD; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_PRIOR; $$ = (Node *) n; } | FIRST_P opt_from_in cursor_name @@ -7515,6 +7735,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_ABSOLUTE; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_FIRST; $$ = (Node *) n; } | LAST_P opt_from_in cursor_name @@ -7524,6 +7746,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_ABSOLUTE; n->howMany = -1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_LAST; $$ = (Node *) n; } | ABSOLUTE_P SignedIconst opt_from_in cursor_name @@ -7533,6 +7757,8 @@ fetch_args: cursor_name n->portalname = $4; n->direction = FETCH_ABSOLUTE; n->howMany = $2; + n->location = @2; + n->direction_keyword = FETCH_KEYWORD_ABSOLUTE; $$ = (Node *) n; } | RELATIVE_P SignedIconst opt_from_in cursor_name @@ -7542,15 +7768,8 @@ fetch_args: cursor_name n->portalname = $4; n->direction = FETCH_RELATIVE; n->howMany = $2; - $$ = (Node *) n; - } - | SignedIconst opt_from_in cursor_name - { - FetchStmt *n = makeNode(FetchStmt); - - n->portalname = $3; - n->direction = FETCH_FORWARD; - n->howMany = $1; + n->location = @2; + n->direction_keyword = FETCH_KEYWORD_RELATIVE; $$ = (Node *) n; } | ALL opt_from_in cursor_name @@ -7560,6 +7779,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_FORWARD; n->howMany = FETCH_ALL; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_ALL; $$ = (Node *) n; } | FORWARD opt_from_in cursor_name @@ -7569,6 +7790,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_FORWARD; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_FORWARD; $$ = (Node *) n; } | FORWARD SignedIconst opt_from_in cursor_name @@ -7578,6 +7801,8 @@ fetch_args: cursor_name n->portalname = $4; n->direction = FETCH_FORWARD; n->howMany = $2; + n->location = @2; + n->direction_keyword = FETCH_KEYWORD_FORWARD; $$ = (Node *) n; } | FORWARD ALL opt_from_in cursor_name @@ -7587,6 +7812,8 @@ fetch_args: cursor_name n->portalname = $4; n->direction = FETCH_FORWARD; n->howMany = FETCH_ALL; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_FORWARD_ALL; $$ = (Node *) n; } | BACKWARD opt_from_in cursor_name @@ -7596,6 +7823,8 @@ fetch_args: cursor_name n->portalname = $3; n->direction = FETCH_BACKWARD; n->howMany = 1; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_BACKWARD; $$ = (Node *) n; } | BACKWARD SignedIconst opt_from_in cursor_name @@ -7605,6 +7834,8 @@ fetch_args: cursor_name n->portalname = $4; n->direction = FETCH_BACKWARD; n->howMany = $2; + n->location = @2; + n->direction_keyword = FETCH_KEYWORD_BACKWARD; $$ = (Node *) n; } | BACKWARD ALL opt_from_in cursor_name @@ -7614,6 +7845,8 @@ fetch_args: cursor_name n->portalname = $4; n->direction = FETCH_BACKWARD; n->howMany = FETCH_ALL; + n->location = -1; + n->direction_keyword = FETCH_KEYWORD_BACKWARD_ALL; $$ = (Node *) n; } ; @@ -7793,7 +8026,7 @@ parameter_name: privilege_target: qualified_name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_TABLE; @@ -7802,7 +8035,7 @@ privilege_target: } | TABLE qualified_name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_TABLE; @@ -7811,7 +8044,7 @@ privilege_target: } | SEQUENCE qualified_name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_SEQUENCE; @@ -7820,7 +8053,7 @@ privilege_target: } | FOREIGN DATA_P WRAPPER name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_FDW; @@ -7829,7 +8062,7 @@ privilege_target: } | FOREIGN SERVER name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_FOREIGN_SERVER; @@ -7838,7 +8071,7 @@ privilege_target: } | FUNCTION function_with_argtypes_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_FUNCTION; @@ -7847,7 +8080,7 @@ privilege_target: } | PROCEDURE function_with_argtypes_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_PROCEDURE; @@ -7856,7 +8089,7 @@ privilege_target: } | ROUTINE function_with_argtypes_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_ROUTINE; @@ -7865,7 +8098,7 @@ privilege_target: } | DATABASE name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_DATABASE; @@ -7874,7 +8107,7 @@ privilege_target: } | DOMAIN_P any_name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_DOMAIN; @@ -7883,7 +8116,7 @@ privilege_target: } | LANGUAGE name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_LANGUAGE; @@ -7892,7 +8125,7 @@ privilege_target: } | LARGE_P OBJECT_P NumericOnly_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_LARGEOBJECT; @@ -7901,15 +8134,24 @@ privilege_target: } | PARAMETER parameter_name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_PARAMETER_ACL; n->objs = $2; $$ = n; } + | PROPERTY GRAPH qualified_name_list + { + PrivTarget *n = palloc_object(PrivTarget); + + n->targtype = ACL_TARGET_OBJECT; + n->objtype = OBJECT_PROPGRAPH; + n->objs = $3; + $$ = n; + } | SCHEMA name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_SCHEMA; @@ -7918,7 +8160,7 @@ privilege_target: } | TABLESPACE name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_TABLESPACE; @@ -7927,7 +8169,7 @@ privilege_target: } | TYPE_P any_name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_OBJECT; n->objtype = OBJECT_TYPE; @@ -7936,7 +8178,7 @@ privilege_target: } | ALL TABLES IN_P SCHEMA name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_ALL_IN_SCHEMA; n->objtype = OBJECT_TABLE; @@ -7945,7 +8187,7 @@ privilege_target: } | ALL SEQUENCES IN_P SCHEMA name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_ALL_IN_SCHEMA; n->objtype = OBJECT_SEQUENCE; @@ -7954,7 +8196,7 @@ privilege_target: } | ALL FUNCTIONS IN_P SCHEMA name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_ALL_IN_SCHEMA; n->objtype = OBJECT_FUNCTION; @@ -7963,7 +8205,7 @@ privilege_target: } | ALL PROCEDURES IN_P SCHEMA name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_ALL_IN_SCHEMA; n->objtype = OBJECT_PROCEDURE; @@ -7972,7 +8214,7 @@ privilege_target: } | ALL ROUTINES IN_P SCHEMA name_list { - PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget)); + PrivTarget *n = palloc_object(PrivTarget); n->targtype = ACL_TARGET_ALL_IN_SCHEMA; n->objtype = OBJECT_ROUTINE; @@ -8281,6 +8523,7 @@ index_elem_options: $$->opclassopts = NIL; $$->ordering = $3; $$->nulls_ordering = $4; + /* location will be filled in index_elem production */ } | opt_collate any_name reloptions opt_asc_desc opt_nulls_order { @@ -8293,6 +8536,7 @@ index_elem_options: $$->opclassopts = $3; $$->ordering = $4; $$->nulls_ordering = $5; + /* location will be filled in index_elem production */ } ; @@ -8305,16 +8549,19 @@ index_elem: ColId index_elem_options { $$ = $2; $$->name = $1; + $$->location = @1; } | func_expr_windowless index_elem_options { $$ = $2; $$->expr = $1; + $$->location = @1; } | '(' a_expr ')' index_elem_options { $$ = $4; $$->expr = $2; + $$->location = @1; } ; @@ -9233,100 +9480,464 @@ opt_if_exists: IF_P EXISTS { $$ = true; } /***************************************************************************** * - * CREATE TRANSFORM / DROP TRANSFORM + * CREATE PROPERTY GRAPH + * ALTER PROPERTY GRAPH * *****************************************************************************/ -CreateTransformStmt: CREATE opt_or_replace TRANSFORM FOR Typename LANGUAGE name '(' transform_element_list ')' +CreatePropGraphStmt: CREATE OptTemp PROPERTY GRAPH qualified_name opt_vertex_tables_clause opt_edge_tables_clause { - CreateTransformStmt *n = makeNode(CreateTransformStmt); + CreatePropGraphStmt *n = makeNode(CreatePropGraphStmt); + + n->pgname = $5; + n->pgname->relpersistence = $2; + n->vertex_tables = $6; + n->edge_tables = $7; + + $$ = (Node *)n; + } + ; + +opt_vertex_tables_clause: + vertex_tables_clause { $$ = $1; } + | /*EMPTY*/ { $$ = NIL; } + ; + +vertex_tables_clause: + vertex_synonym TABLES '(' vertex_table_list ')' { $$ = $4; } + ; + +vertex_synonym: NODE | VERTEX + ; + +vertex_table_list: vertex_table_definition { $$ = list_make1($1); } + | vertex_table_list ',' vertex_table_definition { $$ = lappend($1, $3); } + ; + +vertex_table_definition: qualified_name opt_propgraph_table_alias opt_graph_table_key_clause + opt_element_table_label_and_properties + { + PropGraphVertex *n = makeNode(PropGraphVertex); + + $1->alias = $2; + n->vtable = $1; + n->vkey = $3; + n->labels = $4; + n->location = @1; - n->replace = $2; - n->type_name = $5; - n->lang = $7; - n->fromsql = linitial($9); - n->tosql = lsecond($9); $$ = (Node *) n; } ; -transform_element_list: FROM SQL_P WITH FUNCTION function_with_argtypes ',' TO SQL_P WITH FUNCTION function_with_argtypes +opt_propgraph_table_alias: + AS name { - $$ = list_make2($5, $11); + $$ = makeNode(Alias); + $$->aliasname = $2; } - | TO SQL_P WITH FUNCTION function_with_argtypes ',' FROM SQL_P WITH FUNCTION function_with_argtypes + | /*EMPTY*/ { $$ = NULL; } + ; + +opt_graph_table_key_clause: + KEY '(' columnList ')' { $$ = $3; } + | /*EMPTY*/ { $$ = NIL; } + ; + +opt_edge_tables_clause: + edge_tables_clause { $$ = $1; } + | /*EMPTY*/ { $$ = NIL; } + ; + +edge_tables_clause: + edge_synonym TABLES '(' edge_table_list ')' { $$ = $4; } + ; + +edge_synonym: EDGE | RELATIONSHIP + ; + +edge_table_list: edge_table_definition { $$ = list_make1($1); } + | edge_table_list ',' edge_table_definition { $$ = lappend($1, $3); } + ; + +edge_table_definition: qualified_name opt_propgraph_table_alias opt_graph_table_key_clause + source_vertex_table destination_vertex_table opt_element_table_label_and_properties { - $$ = list_make2($11, $5); + PropGraphEdge *n = makeNode(PropGraphEdge); + + $1->alias = $2; + n->etable = $1; + n->ekey = $3; + n->esrckey = linitial($4); + n->esrcvertex = lsecond($4); + n->esrcvertexcols = lthird($4); + n->edestkey = linitial($5); + n->edestvertex = lsecond($5); + n->edestvertexcols = lthird($5); + n->labels = $6; + n->location = @1; + + $$ = (Node *) n; } - | FROM SQL_P WITH FUNCTION function_with_argtypes + ; + +source_vertex_table: SOURCE name { - $$ = list_make2($5, NULL); + $$ = list_make3(NULL, $2, NULL); } - | TO SQL_P WITH FUNCTION function_with_argtypes + | SOURCE KEY '(' columnList ')' REFERENCES name '(' columnList ')' { - $$ = list_make2(NULL, $5); + $$ = list_make3($4, $7, $9); } ; +destination_vertex_table: DESTINATION name + { + $$ = list_make3(NULL, $2, NULL); + } + | DESTINATION KEY '(' columnList ')' REFERENCES name '(' columnList ')' + { + $$ = list_make3($4, $7, $9); + } + ; -DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_drop_behavior +opt_element_table_label_and_properties: + element_table_properties { - DropStmt *n = makeNode(DropStmt); + PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties); - n->removeType = OBJECT_TRANSFORM; - n->objects = list_make1(list_make2($5, makeString($7))); - n->behavior = $8; - n->missing_ok = $3; - $$ = (Node *) n; + lp->properties = (PropGraphProperties *) $1; + lp->location = @1; + + $$ = list_make1(lp); } - ; + | label_and_properties_list + { + $$ = $1; + } + | /*EMPTY*/ + { + PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties); + PropGraphProperties *pr = makeNode(PropGraphProperties); + pr->all = true; + pr->location = -1; + lp->properties = pr; + lp->location = -1; -/***************************************************************************** - * - * QUERY: - * - * REINDEX [ (options) ] {INDEX | TABLE | SCHEMA} [CONCURRENTLY] - * REINDEX [ (options) ] {DATABASE | SYSTEM} [CONCURRENTLY] [] - *****************************************************************************/ + $$ = list_make1(lp); + } + ; -ReindexStmt: - REINDEX opt_reindex_option_list reindex_target_relation opt_concurrently qualified_name +element_table_properties: + NO PROPERTIES { - ReindexStmt *n = makeNode(ReindexStmt); + PropGraphProperties *pr = makeNode(PropGraphProperties); - n->kind = $3; - n->relation = $5; - n->name = NULL; - n->params = $2; - if ($4) - n->params = lappend(n->params, - makeDefElem("concurrently", NULL, @4)); - $$ = (Node *) n; + pr->properties = NIL; + pr->location = @1; + + $$ = (Node *) pr; } - | REINDEX opt_reindex_option_list SCHEMA opt_concurrently name + | PROPERTIES ALL COLUMNS + /* + * SQL standard also allows "PROPERTIES ARE ALL COLUMNS", but that + * would require making ARE a keyword, which seems a bit much for + * such a marginal use. Could be added later if needed. + */ { - ReindexStmt *n = makeNode(ReindexStmt); + PropGraphProperties *pr = makeNode(PropGraphProperties); - n->kind = REINDEX_OBJECT_SCHEMA; - n->relation = NULL; - n->name = $5; - n->params = $2; - if ($4) - n->params = lappend(n->params, - makeDefElem("concurrently", NULL, @4)); - $$ = (Node *) n; + pr->all = true; + pr->location = @1; + + $$ = (Node *) pr; } - | REINDEX opt_reindex_option_list reindex_target_all opt_concurrently opt_single_name + | PROPERTIES '(' labeled_expr_list ')' { - ReindexStmt *n = makeNode(ReindexStmt); + PropGraphProperties *pr = makeNode(PropGraphProperties); - n->kind = $3; - n->relation = NULL; - n->name = $5; - n->params = $2; - if ($4) - n->params = lappend(n->params, + pr->properties = $3; + pr->location = @1; + + $$ = (Node *) pr; + } + ; + +label_and_properties_list: + label_and_properties + { + $$ = list_make1($1); + } + | label_and_properties_list label_and_properties + { + $$ = lappend($1, $2); + } + ; + +label_and_properties: + element_table_label_clause + { + PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties); + PropGraphProperties *pr = makeNode(PropGraphProperties); + + pr->all = true; + pr->location = -1; + + lp->label = $1; + lp->properties = pr; + lp->location = @1; + + $$ = (Node *) lp; + } + | element_table_label_clause element_table_properties + { + PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties); + + lp->label = $1; + lp->properties = (PropGraphProperties *) $2; + lp->location = @1; + + $$ = (Node *) lp; + } + ; + +element_table_label_clause: + LABEL name + { + $$ = $2; + } + | DEFAULT LABEL + { + $$ = NULL; + } + ; + +AlterPropGraphStmt: + ALTER PROPERTY GRAPH qualified_name ADD_P vertex_tables_clause + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->add_vertex_tables = $6; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name ADD_P vertex_tables_clause ADD_P edge_tables_clause + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->add_vertex_tables = $6; + n->add_edge_tables = $8; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name ADD_P edge_tables_clause + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->add_edge_tables = $6; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name DROP vertex_synonym TABLES '(' name_list ')' opt_drop_behavior + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->drop_vertex_tables = $9; + n->drop_behavior = $11; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name DROP edge_synonym TABLES '(' name_list ')' opt_drop_behavior + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->drop_edge_tables = $9; + n->drop_behavior = $11; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name + add_label_list + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->element_kind = $6; + n->element_alias = $8; + n->add_labels = $9; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name + DROP LABEL name opt_drop_behavior + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->element_kind = $6; + n->element_alias = $8; + n->drop_label = $11; + n->drop_behavior = $12; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name + ALTER LABEL name ADD_P PROPERTIES '(' labeled_expr_list ')' + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + PropGraphProperties *pr = makeNode(PropGraphProperties); + + n->pgname = $4; + n->element_kind = $6; + n->element_alias = $8; + n->alter_label = $11; + + pr->properties = $15; + pr->location = @13; + n->add_properties = pr; + + $$ = (Node *) n; + } + | ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name + ALTER LABEL name DROP PROPERTIES '(' name_list ')' opt_drop_behavior + { + AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt); + + n->pgname = $4; + n->element_kind = $6; + n->element_alias = $8; + n->alter_label = $11; + n->drop_properties = $15; + n->drop_behavior = $17; + + $$ = (Node *) n; + } + ; + +vertex_or_edge: + vertex_synonym { $$ = PROPGRAPH_ELEMENT_KIND_VERTEX; } + | edge_synonym { $$ = PROPGRAPH_ELEMENT_KIND_EDGE; } + ; + +add_label_list: + add_label { $$ = list_make1($1); } + | add_label_list add_label { $$ = lappend($1, $2); } + ; + +add_label: ADD_P LABEL name element_table_properties + { + PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties); + + lp->label = $3; + lp->properties = (PropGraphProperties *) $4; + lp->location = @1; + + $$ = (Node *) lp; + } + ; + + +/***************************************************************************** + * + * CREATE TRANSFORM / DROP TRANSFORM + * + *****************************************************************************/ + +CreateTransformStmt: CREATE opt_or_replace TRANSFORM FOR Typename LANGUAGE name '(' transform_element_list ')' + { + CreateTransformStmt *n = makeNode(CreateTransformStmt); + + n->replace = $2; + n->type_name = $5; + n->lang = $7; + n->fromsql = linitial($9); + n->tosql = lsecond($9); + $$ = (Node *) n; + } + ; + +transform_element_list: FROM SQL_P WITH FUNCTION function_with_argtypes ',' TO SQL_P WITH FUNCTION function_with_argtypes + { + $$ = list_make2($5, $11); + } + | TO SQL_P WITH FUNCTION function_with_argtypes ',' FROM SQL_P WITH FUNCTION function_with_argtypes + { + $$ = list_make2($11, $5); + } + | FROM SQL_P WITH FUNCTION function_with_argtypes + { + $$ = list_make2($5, NULL); + } + | TO SQL_P WITH FUNCTION function_with_argtypes + { + $$ = list_make2(NULL, $5); + } + ; + + +DropTransformStmt: DROP TRANSFORM opt_if_exists FOR Typename LANGUAGE name opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + + n->removeType = OBJECT_TRANSFORM; + n->objects = list_make1(list_make2($5, makeString($7))); + n->behavior = $8; + n->missing_ok = $3; + $$ = (Node *) n; + } + ; + + +/***************************************************************************** + * + * QUERY: + * + * REINDEX [ (options) ] {INDEX | TABLE | SCHEMA} [CONCURRENTLY] + * REINDEX [ (options) ] {DATABASE | SYSTEM} [CONCURRENTLY] [] + *****************************************************************************/ + +ReindexStmt: + REINDEX opt_utility_option_list reindex_target_relation opt_concurrently qualified_name + { + ReindexStmt *n = makeNode(ReindexStmt); + + n->kind = $3; + n->relation = $5; + n->name = NULL; + n->params = $2; + if ($4) + n->params = lappend(n->params, + makeDefElem("concurrently", NULL, @4)); + $$ = (Node *) n; + } + | REINDEX opt_utility_option_list SCHEMA opt_concurrently name + { + ReindexStmt *n = makeNode(ReindexStmt); + + n->kind = REINDEX_OBJECT_SCHEMA; + n->relation = NULL; + n->name = $5; + n->params = $2; + if ($4) + n->params = lappend(n->params, + makeDefElem("concurrently", NULL, @4)); + $$ = (Node *) n; + } + | REINDEX opt_utility_option_list reindex_target_all opt_concurrently opt_single_name + { + ReindexStmt *n = makeNode(ReindexStmt); + + n->kind = $3; + n->relation = NULL; + n->name = $5; + n->params = $2; + if ($4) + n->params = lappend(n->params, makeDefElem("concurrently", NULL, @4)); $$ = (Node *) n; } @@ -9339,10 +9950,6 @@ reindex_target_all: SYSTEM_P { $$ = REINDEX_OBJECT_SYSTEM; } | DATABASE { $$ = REINDEX_OBJECT_DATABASE; } ; -opt_reindex_option_list: - '(' utility_option_list ')' { $$ = $2; } - | /* EMPTY */ { $$ = NULL; } - ; /***************************************************************************** * @@ -9531,6 +10138,16 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name n->missing_ok = false; $$ = (Node *) n; } + | ALTER PROPERTY GRAPH qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + + n->renameType = OBJECT_PROPGRAPH; + n->relation = $4; + n->newname = $7; + n->missing_ok = false; + $$ = (Node *)n; + } | ALTER PUBLICATION name RENAME TO name { RenameStmt *n = makeNode(RenameStmt); @@ -10156,6 +10773,26 @@ AlterObjectSchemaStmt: n->missing_ok = false; $$ = (Node *) n; } + | ALTER PROPERTY GRAPH qualified_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + + n->objectType = OBJECT_PROPGRAPH; + n->relation = $4; + n->newschema = $7; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER PROPERTY GRAPH IF_P EXISTS qualified_name SET SCHEMA name + { + AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); + + n->objectType = OBJECT_PROPGRAPH; + n->relation = $6; + n->newschema = $9; + n->missing_ok = true; + $$ = (Node *)n; + } | ALTER ROUTINE function_with_argtypes SET SCHEMA name { AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt); @@ -10499,6 +11136,15 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec n->newowner = $6; $$ = (Node *) n; } + | ALTER PROPERTY GRAPH qualified_name OWNER TO RoleSpec + { + AlterOwnerStmt *n = makeNode(AlterOwnerStmt); + + n->objectType = OBJECT_PROPGRAPH; + n->relation = $4; + n->newowner = $7; + $$ = (Node *) n; + } | ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec { AlterOwnerStmt *n = makeNode(AlterOwnerStmt); @@ -10614,7 +11260,12 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec * * CREATE PUBLICATION name [WITH options] * - * CREATE PUBLICATION FOR ALL TABLES [WITH options] + * CREATE PUBLICATION FOR ALL pub_all_obj_type [, ...] [WITH options] + * + * pub_all_obj_type is one of: + * + * TABLES [EXCEPT (TABLE table [, ...] )] + * SEQUENCES * * CREATE PUBLICATION FOR pub_obj [, ...] [WITH options] * @@ -10634,13 +11285,16 @@ CreatePublicationStmt: n->options = $4; $$ = (Node *) n; } - | CREATE PUBLICATION name FOR ALL TABLES opt_definition + | CREATE PUBLICATION name FOR pub_all_obj_type_list opt_definition { CreatePublicationStmt *n = makeNode(CreatePublicationStmt); n->pubname = $3; - n->options = $7; - n->for_all_tables = true; + preprocess_pub_all_objtype_list($5, &n->pubobjects, + &n->for_all_tables, + &n->for_all_sequences, + yyscanner); + n->options = $6; $$ = (Node *) n; } | CREATE PUBLICATION name FOR pub_obj_list opt_definition @@ -10752,6 +11406,51 @@ pub_obj_list: PublicationObjSpec { $$ = lappend($1, $3); } ; +opt_pub_except_clause: + EXCEPT '(' TABLE pub_except_obj_list ')' { $$ = $4; } + | /*EMPTY*/ { $$ = NIL; } + ; + +PublicationAllObjSpec: + ALL TABLES opt_pub_except_clause + { + $$ = makeNode(PublicationAllObjSpec); + $$->pubobjtype = PUBLICATION_ALL_TABLES; + $$->except_tables = $3; + $$->location = @1; + } + | ALL SEQUENCES + { + $$ = makeNode(PublicationAllObjSpec); + $$->pubobjtype = PUBLICATION_ALL_SEQUENCES; + $$->location = @1; + } + ; + +pub_all_obj_type_list: PublicationAllObjSpec + { $$ = list_make1($1); } + | pub_all_obj_type_list ',' PublicationAllObjSpec + { $$ = lappend($1, $3); } + ; + +PublicationExceptObjSpec: + relation_expr + { + $$ = makeNode(PublicationObjSpec); + $$->pubobjtype = PUBLICATIONOBJ_EXCEPT_TABLE; + $$->pubtable = makeNode(PublicationTable); + $$->pubtable->except = true; + $$->pubtable->relation = $1; + $$->location = @1; + } + ; + +pub_except_obj_list: PublicationExceptObjSpec + { $$ = list_make1($1); } + | pub_except_obj_list ',' opt_table PublicationExceptObjSpec + { $$ = lappend($1, $4); } + ; + /***************************************************************************** * * ALTER PUBLICATION name SET ( options ) @@ -10762,11 +11461,18 @@ pub_obj_list: PublicationObjSpec * * ALTER PUBLICATION name SET pub_obj [, ...] * + * ALTER PUBLICATION name SET pub_all_obj_type [, ...] + * * pub_obj is one of: * * TABLE table_name [, ...] * TABLES IN SCHEMA schema_name [, ...] * + * pub_all_obj_type is one of: + * + * ALL TABLES [ EXCEPT ( TABLE table_name [, ...] ) ] + * ALL SEQUENCES + * *****************************************************************************/ AlterPublicationStmt: @@ -10776,6 +11482,7 @@ AlterPublicationStmt: n->pubname = $3; n->options = $5; + n->for_all_tables = false; $$ = (Node *) n; } | ALTER PUBLICATION name ADD_P pub_obj_list @@ -10786,6 +11493,7 @@ AlterPublicationStmt: n->pubobjects = $5; preprocess_pubobj_list(n->pubobjects, yyscanner); n->action = AP_AddObjects; + n->for_all_tables = false; $$ = (Node *) n; } | ALTER PUBLICATION name SET pub_obj_list @@ -10796,6 +11504,19 @@ AlterPublicationStmt: n->pubobjects = $5; preprocess_pubobj_list(n->pubobjects, yyscanner); n->action = AP_SetObjects; + n->for_all_tables = false; + $$ = (Node *) n; + } + | ALTER PUBLICATION name SET pub_all_obj_type_list + { + AlterPublicationStmt *n = makeNode(AlterPublicationStmt); + + n->pubname = $3; + n->action = AP_SetObjects; + preprocess_pub_all_objtype_list($5, &n->pubobjects, + &n->for_all_tables, + &n->for_all_sequences, + yyscanner); $$ = (Node *) n; } | ALTER PUBLICATION name DROP pub_obj_list @@ -10806,6 +11527,7 @@ AlterPublicationStmt: n->pubobjects = $5; preprocess_pubobj_list(n->pubobjects, yyscanner); n->action = AP_DropObjects; + n->for_all_tables = false; $$ = (Node *) n; } ; @@ -10827,6 +11549,16 @@ CreateSubscriptionStmt: n->options = $8; $$ = (Node *) n; } + | CREATE SUBSCRIPTION name SERVER name PUBLICATION name_list opt_definition + { + CreateSubscriptionStmt *n = + makeNode(CreateSubscriptionStmt); + n->subname = $3; + n->servername = $5; + n->publication = $7; + n->options = $8; + $$ = (Node *) n; + } ; /***************************************************************************** @@ -10856,16 +11588,35 @@ AlterSubscriptionStmt: n->conninfo = $5; $$ = (Node *) n; } + | ALTER SUBSCRIPTION name SERVER name + { + AlterSubscriptionStmt *n = + makeNode(AlterSubscriptionStmt); + + n->kind = ALTER_SUBSCRIPTION_SERVER; + n->subname = $3; + n->servername = $5; + $$ = (Node *) n; + } | ALTER SUBSCRIPTION name REFRESH PUBLICATION opt_definition { AlterSubscriptionStmt *n = makeNode(AlterSubscriptionStmt); - n->kind = ALTER_SUBSCRIPTION_REFRESH; + n->kind = ALTER_SUBSCRIPTION_REFRESH_PUBLICATION; n->subname = $3; n->options = $6; $$ = (Node *) n; } + | ALTER SUBSCRIPTION name REFRESH SEQUENCES + { + AlterSubscriptionStmt *n = + makeNode(AlterSubscriptionStmt); + + n->kind = ALTER_SUBSCRIPTION_REFRESH_SEQUENCES; + n->subname = $3; + $$ = (Node *) n; + } | ALTER SUBSCRIPTION name ADD_P PUBLICATION name_list opt_definition { AlterSubscriptionStmt *n = @@ -11629,7 +12380,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'T'; + n->subtype = AD_AlterDefault; n->typeName = $3; n->def = $4; $$ = (Node *) n; @@ -11639,7 +12390,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'N'; + n->subtype = AD_DropNotNull; n->typeName = $3; $$ = (Node *) n; } @@ -11648,7 +12399,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'O'; + n->subtype = AD_SetNotNull; n->typeName = $3; $$ = (Node *) n; } @@ -11657,7 +12408,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'C'; + n->subtype = AD_AddConstraint; n->typeName = $3; n->def = $5; $$ = (Node *) n; @@ -11667,7 +12418,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'X'; + n->subtype = AD_DropConstraint; n->typeName = $3; n->name = $6; n->behavior = $7; @@ -11679,7 +12430,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'X'; + n->subtype = AD_DropConstraint; n->typeName = $3; n->name = $8; n->behavior = $9; @@ -11691,7 +12442,7 @@ AlterDomainStmt: { AlterDomainStmt *n = makeNode(AlterDomainStmt); - n->subtype = 'V'; + n->subtype = AD_ValidateConstraint; n->typeName = $3; n->name = $6; $$ = (Node *) n; @@ -11824,65 +12575,110 @@ CreateConversionStmt: /***************************************************************************** * * QUERY: + * REPACK [ (options) ] [ [ ] [ USING INDEX ] ] + * + * obsolete variants: * CLUSTER (options) [ [ USING ] ] * CLUSTER [VERBOSE] [ [ USING ] ] * CLUSTER [VERBOSE] ON (for pre-8.3) * *****************************************************************************/ -ClusterStmt: - CLUSTER '(' utility_option_list ')' qualified_name cluster_index_specification +RepackStmt: + REPACK opt_utility_option_list vacuum_relation USING INDEX name { - ClusterStmt *n = makeNode(ClusterStmt); + RepackStmt *n = makeNode(RepackStmt); - n->relation = $5; + n->command = REPACK_COMMAND_REPACK; + n->relation = (VacuumRelation *) $3; n->indexname = $6; - n->params = $3; + n->usingindex = true; + n->params = $2; + $$ = (Node *) n; + } + | REPACK opt_utility_option_list vacuum_relation opt_usingindex + { + RepackStmt *n = makeNode(RepackStmt); + + n->command = REPACK_COMMAND_REPACK; + n->relation = (VacuumRelation *) $3; + n->indexname = NULL; + n->usingindex = $4; + n->params = $2; $$ = (Node *) n; } - | CLUSTER '(' utility_option_list ')' + | REPACK opt_utility_option_list opt_usingindex { - ClusterStmt *n = makeNode(ClusterStmt); + RepackStmt *n = makeNode(RepackStmt); + n->command = REPACK_COMMAND_REPACK; n->relation = NULL; n->indexname = NULL; + n->usingindex = $3; + n->params = $2; + $$ = (Node *) n; + } + | CLUSTER '(' utility_option_list ')' qualified_name cluster_index_specification + { + RepackStmt *n = makeNode(RepackStmt); + + n->command = REPACK_COMMAND_CLUSTER; + n->relation = makeNode(VacuumRelation); + n->relation->relation = $5; + n->indexname = $6; + n->usingindex = true; n->params = $3; $$ = (Node *) n; } + | CLUSTER opt_utility_option_list + { + RepackStmt *n = makeNode(RepackStmt); + + n->command = REPACK_COMMAND_CLUSTER; + n->relation = NULL; + n->indexname = NULL; + n->usingindex = true; + n->params = $2; + $$ = (Node *) n; + } /* unparenthesized VERBOSE kept for pre-14 compatibility */ | CLUSTER opt_verbose qualified_name cluster_index_specification { - ClusterStmt *n = makeNode(ClusterStmt); + RepackStmt *n = makeNode(RepackStmt); - n->relation = $3; + n->command = REPACK_COMMAND_CLUSTER; + n->relation = makeNode(VacuumRelation); + n->relation->relation = $3; n->indexname = $4; - n->params = NIL; + n->usingindex = true; if ($2) - n->params = lappend(n->params, makeDefElem("verbose", NULL, @2)); + n->params = list_make1(makeDefElem("verbose", NULL, @2)); $$ = (Node *) n; } /* unparenthesized VERBOSE kept for pre-17 compatibility */ - | CLUSTER opt_verbose + | CLUSTER VERBOSE { - ClusterStmt *n = makeNode(ClusterStmt); + RepackStmt *n = makeNode(RepackStmt); + n->command = REPACK_COMMAND_CLUSTER; n->relation = NULL; n->indexname = NULL; - n->params = NIL; - if ($2) - n->params = lappend(n->params, makeDefElem("verbose", NULL, @2)); + n->usingindex = true; + n->params = list_make1(makeDefElem("verbose", NULL, @2)); $$ = (Node *) n; } /* kept for pre-8.3 compatibility */ | CLUSTER opt_verbose name ON qualified_name { - ClusterStmt *n = makeNode(ClusterStmt); + RepackStmt *n = makeNode(RepackStmt); - n->relation = $5; + n->command = REPACK_COMMAND_CLUSTER; + n->relation = makeNode(VacuumRelation); + n->relation->relation = $5; n->indexname = $3; - n->params = NIL; + n->usingindex = true; if ($2) - n->params = lappend(n->params, makeDefElem("verbose", NULL, @2)); + n->params = list_make1(makeDefElem("verbose", NULL, @2)); $$ = (Node *) n; } ; @@ -11933,64 +12729,31 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose opt_analyze opt_vacuum_relati } ; -AnalyzeStmt: analyze_keyword opt_verbose opt_vacuum_relation_list +AnalyzeStmt: analyze_keyword opt_utility_option_list opt_vacuum_relation_list { VacuumStmt *n = makeNode(VacuumStmt); - n->options = NIL; - if ($2) - n->options = lappend(n->options, - makeDefElem("verbose", NULL, @2)); + n->options = $2; n->rels = $3; n->is_vacuumcmd = false; $$ = (Node *) n; } - | analyze_keyword '(' utility_option_list ')' opt_vacuum_relation_list + | analyze_keyword VERBOSE opt_vacuum_relation_list { VacuumStmt *n = makeNode(VacuumStmt); - n->options = $3; - n->rels = $5; + n->options = list_make1(makeDefElem("verbose", NULL, @2)); + n->rels = $3; n->is_vacuumcmd = false; $$ = (Node *) n; } ; -utility_option_list: - utility_option_elem - { - $$ = list_make1($1); - } - | utility_option_list ',' utility_option_elem - { - $$ = lappend($1, $3); - } - ; - analyze_keyword: ANALYZE | ANALYSE /* British */ ; -utility_option_elem: - utility_option_name utility_option_arg - { - $$ = makeDefElem($1, $2, @1); - } - ; - -utility_option_name: - NonReservedWord { $$ = $1; } - | analyze_keyword { $$ = "analyze"; } - | FORMAT_LA { $$ = "format"; } - ; - -utility_option_arg: - opt_boolean_or_string { $$ = (Node *) makeString($1); } - | NumericOnly { $$ = (Node *) $1; } - | /* EMPTY */ { $$ = NULL; } - ; - opt_analyze: analyze_keyword { $$ = true; } | /*EMPTY*/ { $$ = false; } @@ -12240,7 +13003,6 @@ InsertStmt: $5->onConflictClause = $6; $5->returningClause = $7; $5->withClause = $1; - $5->stmt_location = @$; $$ = (Node *) $5; } ; @@ -12322,12 +13084,24 @@ insert_column_item: ; opt_on_conflict: + ON CONFLICT opt_conf_expr DO SELECT opt_for_locking_strength where_clause + { + $$ = makeNode(OnConflictClause); + $$->action = ONCONFLICT_SELECT; + $$->infer = $3; + $$->targetList = NIL; + $$->lockStrength = $6; + $$->whereClause = $7; + $$->location = @1; + } + | ON CONFLICT opt_conf_expr DO UPDATE SET set_clause_list where_clause { $$ = makeNode(OnConflictClause); $$->action = ONCONFLICT_UPDATE; $$->infer = $3; $$->targetList = $7; + $$->lockStrength = LCS_NONE; $$->whereClause = $8; $$->location = @1; } @@ -12338,6 +13112,7 @@ opt_on_conflict: $$->action = ONCONFLICT_NOTHING; $$->infer = $3; $$->targetList = NIL; + $$->lockStrength = LCS_NONE; $$->whereClause = NULL; $$->location = @1; } @@ -12427,11 +13202,25 @@ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias DeleteStmt *n = makeNode(DeleteStmt); n->relation = $4; - n->usingClause = $5; - n->whereClause = $6; - n->returningClause = $7; + n->usingClause = $5; + n->whereClause = $6; + n->returningClause = $7; + n->withClause = $1; + $$ = (Node *) n; + } + | opt_with_clause DELETE_P FROM relation_expr + for_portion_of_clause for_portion_of_opt_alias + using_clause where_or_current_clause returning_clause + { + DeleteStmt *n = makeNode(DeleteStmt); + + n->relation = $4; + n->forPortionOf = (ForPortionOfClause *) $5; + n->relation->alias = $6; + n->usingClause = $7; + n->whereClause = $8; + n->returningClause = $9; n->withClause = $1; - n->stmt_location = @$; $$ = (Node *) n; } ; @@ -12506,7 +13295,25 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias n->whereClause = $7; n->returningClause = $8; n->withClause = $1; - n->stmt_location = @$; + $$ = (Node *) n; + } + | opt_with_clause UPDATE relation_expr + for_portion_of_clause for_portion_of_opt_alias + SET set_clause_list + from_clause + where_or_current_clause + returning_clause + { + UpdateStmt *n = makeNode(UpdateStmt); + + n->relation = $3; + n->forPortionOf = (ForPortionOfClause *) $4; + n->relation->alias = $5; + n->targetList = $7; + n->fromClause = $8; + n->whereClause = $9; + n->returningClause = $10; + n->withClause = $1; $$ = (Node *) n; } ; @@ -12584,7 +13391,6 @@ MergeStmt: m->joinCondition = $8; m->mergeWhenClauses = $9; m->returningClause = $10; - m->stmt_location = @$; $$ = (Node *) m; } @@ -12825,20 +13631,7 @@ SelectStmt: select_no_parens %prec UMINUS ; select_with_parens: - '(' select_no_parens ')' - { - SelectStmt *n = (SelectStmt *) $2; - - /* - * As SelectStmt's location starts at the SELECT keyword, - * we need to track the length of the SelectStmt within - * parentheses to be able to extract the relevant part - * of the query. Without this, the RawStmt's length would - * be used and would include the closing parenthesis. - */ - n->stmt_len = @3 - @2; - $$ = $2; - } + '(' select_no_parens ')' { $$ = $2; } | '(' select_with_parens ')' { $$ = $2; } ; @@ -12958,9 +13751,9 @@ simple_select: n->whereClause = $6; n->groupClause = ($7)->list; n->groupDistinct = ($7)->distinct; + n->groupByAll = ($7)->all; n->havingClause = $8; n->windowClause = $9; - n->stmt_location = @1; $$ = (Node *) n; } | SELECT distinct_clause target_list @@ -12976,9 +13769,9 @@ simple_select: n->whereClause = $6; n->groupClause = ($7)->list; n->groupDistinct = ($7)->distinct; + n->groupByAll = ($7)->all; n->havingClause = $8; n->windowClause = $9; - n->stmt_location = @1; $$ = (Node *) n; } | values_clause { $$ = $1; } @@ -12999,20 +13792,19 @@ simple_select: n->targetList = list_make1(rt); n->fromClause = list_make1($2); - n->stmt_location = @1; $$ = (Node *) n; } | select_clause UNION set_quantifier select_clause { - $$ = makeSetOp(SETOP_UNION, $3 == SET_QUANTIFIER_ALL, $1, $4, @1); + $$ = makeSetOp(SETOP_UNION, $3 == SET_QUANTIFIER_ALL, $1, $4); } | select_clause INTERSECT set_quantifier select_clause { - $$ = makeSetOp(SETOP_INTERSECT, $3 == SET_QUANTIFIER_ALL, $1, $4, @1); + $$ = makeSetOp(SETOP_INTERSECT, $3 == SET_QUANTIFIER_ALL, $1, $4); } | select_clause EXCEPT set_quantifier select_clause { - $$ = makeSetOp(SETOP_EXCEPT, $3 == SET_QUANTIFIER_ALL, $1, $4, @1); + $$ = makeSetOp(SETOP_EXCEPT, $3 == SET_QUANTIFIER_ALL, $1, $4); } ; @@ -13293,7 +14085,7 @@ select_limit: } | offset_clause { - SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + SelectLimit *n = palloc_object(SelectLimit); n->limitOffset = $1; n->limitCount = NULL; @@ -13313,7 +14105,7 @@ opt_select_limit: limit_clause: LIMIT select_limit_value { - SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + SelectLimit *n = palloc_object(SelectLimit); n->limitOffset = NULL; n->limitCount = $2; @@ -13341,7 +14133,7 @@ limit_clause: */ | FETCH first_or_next select_fetch_first_value row_or_rows ONLY { - SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + SelectLimit *n = palloc_object(SelectLimit); n->limitOffset = NULL; n->limitCount = $3; @@ -13353,7 +14145,7 @@ limit_clause: } | FETCH first_or_next select_fetch_first_value row_or_rows WITH TIES { - SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + SelectLimit *n = palloc_object(SelectLimit); n->limitOffset = NULL; n->limitCount = $3; @@ -13365,7 +14157,7 @@ limit_clause: } | FETCH first_or_next row_or_rows ONLY { - SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + SelectLimit *n = palloc_object(SelectLimit); n->limitOffset = NULL; n->limitCount = makeIntConst(1, -1); @@ -13377,7 +14169,7 @@ limit_clause: } | FETCH first_or_next row_or_rows WITH TIES { - SelectLimit *n = (SelectLimit *) palloc(sizeof(SelectLimit)); + SelectLimit *n = palloc_object(SelectLimit); n->limitOffset = NULL; n->limitCount = makeIntConst(1, -1); @@ -13472,17 +14264,27 @@ first_or_next: FIRST_P { $$ = 0; } group_clause: GROUP_P BY set_quantifier group_by_list { - GroupClause *n = (GroupClause *) palloc(sizeof(GroupClause)); + GroupClause *n = palloc_object(GroupClause); n->distinct = $3 == SET_QUANTIFIER_DISTINCT; + n->all = false; n->list = $4; $$ = n; } + | GROUP_P BY ALL + { + GroupClause *n = palloc_object(GroupClause); + n->distinct = false; + n->all = true; + n->list = NIL; + $$ = n; + } | /*EMPTY*/ { - GroupClause *n = (GroupClause *) palloc(sizeof(GroupClause)); + GroupClause *n = palloc_object(GroupClause); n->distinct = false; + n->all = false; n->list = NIL; $$ = n; } @@ -13574,6 +14376,11 @@ for_locking_strength: | FOR KEY SHARE { $$ = LCS_FORKEYSHARE; } ; +opt_for_locking_strength: + for_locking_strength { $$ = $1; } + | /* EMPTY */ { $$ = LCS_NONE; } + ; + locked_rels_list: OF qualified_name_list { $$ = $2; } | /* EMPTY */ { $$ = NIL; } @@ -13590,7 +14397,6 @@ values_clause: { SelectStmt *n = makeNode(SelectStmt); - n->stmt_location = @1; n->valuesLists = list_make1($3); $$ = (Node *) n; } @@ -13671,6 +14477,17 @@ table_ref: relation_expr opt_alias_clause n->alias = $3; $$ = (Node *) n; } + | GRAPH_TABLE '(' qualified_name MATCH graph_pattern COLUMNS '(' labeled_expr_list ')' ')' opt_alias_clause + { + RangeGraphTable *n = makeNode(RangeGraphTable); + + n->graph_name = $3; + n->graph_pattern = castNode(GraphPattern, $5); + n->columns = $8; + n->alias = $11; + n->location = @1; + $$ = (Node *) n; + } | select_with_parens opt_alias_clause { RangeSubselect *n = makeNode(RangeSubselect); @@ -14012,6 +14829,55 @@ relation_expr_opt_alias: relation_expr %prec UMINUS } ; +/* + * If an UPDATE/DELETE has FOR PORTION OF, then the relation_expr is separated + * from its potential alias by the for_portion_of_clause. So this production + * handles the potential alias in those cases. We need to solve the same + * problems as relation_expr_opt_alias, in particular resolving a shift/reduce + * conflict where "set set" could be an alias plus the SET keyword, or the SET + * keyword then a column name. As above, we force the latter interpretation by + * giving the non-alias choice a higher precedence. + */ +for_portion_of_opt_alias: + AS ColId + { + Alias *alias = makeNode(Alias); + + alias->aliasname = $2; + $$ = alias; + } + | BareColLabel + { + Alias *alias = makeNode(Alias); + + alias->aliasname = $1; + $$ = alias; + } + | /* empty */ %prec UMINUS { $$ = NULL; } + ; + +for_portion_of_clause: + FOR PORTION OF ColId '(' a_expr ')' + { + ForPortionOfClause *n = makeNode(ForPortionOfClause); + n->range_name = $4; + n->location = @4; + n->target = $6; + n->target_location = @6; + $$ = (Node *) n; + } + | FOR PORTION OF ColId FROM a_expr TO a_expr + { + ForPortionOfClause *n = makeNode(ForPortionOfClause); + n->range_name = $4; + n->location = @4; + n->target_start = $6; + n->target_end = $8; + n->target_location = @5; + $$ = (Node *) n; + } + ; + /* * TABLESAMPLE decoration in a FROM item */ @@ -14852,16 +15718,25 @@ opt_timezone: | /*EMPTY*/ { $$ = false; } ; +/* + * We need to handle this shift/reduce conflict: + * FOR PORTION OF valid_at FROM t + INTERVAL '1' YEAR TO MONTH. + * We don't see far enough ahead to know if there is another TO coming. + * We prefer to interpret this as FROM (t + INTERVAL '1' YEAR TO MONTH), + * i.e. to shift. + * That gives the user the option of adding parentheses to get the other meaning. + * If we reduced, intervals could never have a TO. + */ opt_interval: - YEAR_P + YEAR_P %prec IS { $$ = list_make1(makeIntConst(INTERVAL_MASK(YEAR), @1)); } | MONTH_P { $$ = list_make1(makeIntConst(INTERVAL_MASK(MONTH), @1)); } - | DAY_P + | DAY_P %prec IS { $$ = list_make1(makeIntConst(INTERVAL_MASK(DAY), @1)); } - | HOUR_P + | HOUR_P %prec IS { $$ = list_make1(makeIntConst(INTERVAL_MASK(HOUR), @1)); } - | MINUTE_P + | MINUTE_P %prec IS { $$ = list_make1(makeIntConst(INTERVAL_MASK(MINUTE), @1)); } | interval_second { $$ = $1; } @@ -15022,6 +15897,10 @@ a_expr: c_expr { $$ = $1; } { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2); } | a_expr NOT_EQUALS a_expr { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2); } + | a_expr RIGHT_ARROW a_expr + { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "->", $1, $3, @2); } + | a_expr '|' a_expr + { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "|", $1, $3, @2); } | a_expr qual_Op a_expr %prec Op { $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); } @@ -15287,49 +16166,50 @@ a_expr: c_expr { $$ = $1; } (Node *) list_make2($5, $7), @2); } - | a_expr IN_P in_expr + | a_expr IN_P select_with_parens { - /* in_expr returns a SubLink or a list of a_exprs */ - if (IsA($3, SubLink)) - { - /* generate foo = ANY (subquery) */ - SubLink *n = (SubLink *) $3; + /* generate foo = ANY (subquery) */ + SubLink *n = makeNode(SubLink); - n->subLinkType = ANY_SUBLINK; - n->subLinkId = 0; - n->testexpr = $1; - n->operName = NIL; /* show it's IN not = ANY */ - n->location = @2; - $$ = (Node *) n; - } - else - { - /* generate scalar IN expression */ - $$ = (Node *) makeSimpleA_Expr(AEXPR_IN, "=", $1, $3, @2); - } + n->subselect = $3; + n->subLinkType = ANY_SUBLINK; + n->subLinkId = 0; + n->testexpr = $1; + n->operName = NIL; /* show it's IN not = ANY */ + n->location = @2; + $$ = (Node *) n; } - | a_expr NOT_LA IN_P in_expr %prec NOT_LA + | a_expr IN_P '(' expr_list ')' { - /* in_expr returns a SubLink or a list of a_exprs */ - if (IsA($4, SubLink)) - { - /* generate NOT (foo = ANY (subquery)) */ - /* Make an = ANY node */ - SubLink *n = (SubLink *) $4; - - n->subLinkType = ANY_SUBLINK; - n->subLinkId = 0; - n->testexpr = $1; - n->operName = NIL; /* show it's IN not = ANY */ - n->location = @2; - /* Stick a NOT on top; must have same parse location */ - $$ = makeNotExpr((Node *) n, @2); - } - else - { - /* generate scalar NOT IN expression */ - $$ = (Node *) makeSimpleA_Expr(AEXPR_IN, "<>", $1, $4, @2); - } + /* generate scalar IN expression */ + A_Expr *n = makeSimpleA_Expr(AEXPR_IN, "=", $1, (Node *) $4, @2); + + n->rexpr_list_start = @3; + n->rexpr_list_end = @5; + $$ = (Node *) n; + } + | a_expr NOT_LA IN_P select_with_parens %prec NOT_LA + { + /* generate NOT (foo = ANY (subquery)) */ + SubLink *n = makeNode(SubLink); + + n->subselect = $4; + n->subLinkType = ANY_SUBLINK; + n->subLinkId = 0; + n->testexpr = $1; + n->operName = NIL; /* show it's IN not = ANY */ + n->location = @2; + /* Stick a NOT on top; must have same parse location */ + $$ = makeNotExpr((Node *) n, @2); + } + | a_expr NOT_LA IN_P '(' expr_list ')' + { + /* generate scalar NOT IN expression */ + A_Expr *n = makeSimpleA_Expr(AEXPR_IN, "<>", $1, (Node *) $5, @2); + + n->rexpr_list_start = @4; + n->rexpr_list_end = @6; + $$ = (Node *) n; } | a_expr subquery_Op sub_type select_with_parens %prec Op { @@ -15412,7 +16292,7 @@ a_expr: c_expr { $$ = $1; } { JsonFormat *format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); - $$ = makeJsonIsPredicate($1, format, $3, $4, @1); + $$ = makeJsonIsPredicate($1, format, $3, $4, InvalidOid, @1); } /* * Required by SQL/JSON, but there are conflicts @@ -15421,7 +16301,7 @@ a_expr: c_expr { $$ = $1; } IS json_predicate_type_constraint json_key_uniqueness_constraint_opt %prec IS { - $$ = makeJsonIsPredicate($1, $2, $4, $5, @1); + $$ = makeJsonIsPredicate($1, $2, $4, $5, InvalidOid, @1); } */ | a_expr IS NOT @@ -15430,7 +16310,7 @@ a_expr: c_expr { $$ = $1; } { JsonFormat *format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); - $$ = makeNotExpr(makeJsonIsPredicate($1, format, $4, $5, @1), @1); + $$ = makeNotExpr(makeJsonIsPredicate($1, format, $4, $5, InvalidOid, @1), @1); } /* * Required by SQL/JSON, but there are conflicts @@ -15440,7 +16320,7 @@ a_expr: c_expr { $$ = $1; } json_predicate_type_constraint json_key_uniqueness_constraint_opt %prec IS { - $$ = makeNotExpr(makeJsonIsPredicate($1, $2, $5, $6, @1), @1); + $$ = makeNotExpr(makeJsonIsPredicate($1, $2, $5, $6, InvalidOid, @1), @1); } */ | DEFAULT @@ -15501,6 +16381,10 @@ b_expr: c_expr { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2); } | b_expr NOT_EQUALS b_expr { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2); } + | b_expr RIGHT_ARROW b_expr + { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "->", $1, $3, @2); } + | b_expr '|' b_expr + { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "|", $1, $3, @2); } | b_expr qual_Op b_expr %prec Op { $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); } | qual_Op b_expr %prec Op @@ -15760,7 +16644,7 @@ func_application: func_name '(' ')' * (Note that many of the special SQL functions wouldn't actually make any * sense as functional index entries, but we ignore that consideration here.) */ -func_expr: func_application within_group_clause filter_clause over_clause +func_expr: func_application within_group_clause filter_clause null_treatment over_clause { FuncCall *n = (FuncCall *) $1; @@ -15793,7 +16677,8 @@ func_expr: func_application within_group_clause filter_clause over_clause n->agg_within_group = true; } n->agg_filter = $3; - n->over = $4; + n->ignore_nulls = $4; + n->over = $5; $$ = (Node *) n; } | json_aggregate_func filter_clause over_clause @@ -16082,7 +16967,7 @@ func_expr_common_subexpr: COERCE_SQL_SYNTAX, @1); } - | XMLFOREST '(' xml_attribute_list ')' + | XMLFOREST '(' labeled_expr_list ')' { $$ = makeXmlExpr(IS_XMLFOREST, NULL, $3, NIL, @1); } @@ -16307,14 +17192,14 @@ opt_xml_root_standalone: ',' STANDALONE_P YES_P { $$ = makeIntConst(XML_STANDALONE_OMITTED, -1); } ; -xml_attributes: XMLATTRIBUTES '(' xml_attribute_list ')' { $$ = $3; } +xml_attributes: XMLATTRIBUTES '(' labeled_expr_list ')' { $$ = $3; } ; -xml_attribute_list: xml_attribute_el { $$ = list_make1($1); } - | xml_attribute_list ',' xml_attribute_el { $$ = lappend($1, $3); } +labeled_expr_list: labeled_expr { $$ = list_make1($1); } + | labeled_expr_list ',' labeled_expr { $$ = lappend($1, $3); } ; -xml_attribute_el: a_expr AS ColLabel +labeled_expr: a_expr AS ColLabel { $$ = makeNode(ResTarget); $$->name = $3; @@ -16371,6 +17256,26 @@ xml_passing_mech: | BY VALUE_P ; +/***************************************************************************** + * + * WAIT FOR LSN + * + *****************************************************************************/ + +WaitStmt: + WAIT FOR LSN_P Sconst opt_wait_with_clause + { + WaitStmt *n = makeNode(WaitStmt); + n->lsn_literal = $4; + n->options = $5; + $$ = (Node *) n; + } + ; + +opt_wait_with_clause: + WITH '(' utility_option_list ')' { $$ = $3; } + | /*EMPTY*/ { $$ = NIL; } + ; /* * Aggregate decoration clauses @@ -16389,6 +17294,12 @@ filter_clause: /* * Window Definitions */ +null_treatment: + IGNORE_P NULLS_P { $$ = PARSER_IGNORE_NULLS; } + | RESPECT_P NULLS_P { $$ = PARSER_RESPECT_NULLS; } + | /*EMPTY*/ { $$ = NO_NULLTREATMENT; } + ; + window_clause: WINDOW window_definition_list { $$ = $2; } | /*EMPTY*/ { $$ = NIL; } @@ -16669,6 +17580,8 @@ MathOp: '+' { $$ = "+"; } | LESS_EQUALS { $$ = "<="; } | GREATER_EQUALS { $$ = ">="; } | NOT_EQUALS { $$ = "<>"; } + | RIGHT_ARROW { $$ = "->"; } + | '|' { $$ = "|"; } ; qual_Op: Op @@ -16764,15 +17677,15 @@ type_list: Typename { $$ = list_make1($1); } array_expr: '[' expr_list ']' { - $$ = makeAArrayExpr($2, @1); + $$ = makeAArrayExpr($2, @1, @3); } | '[' array_expr_list ']' { - $$ = makeAArrayExpr($2, @1); + $$ = makeAArrayExpr($2, @1, @3); } | '[' ']' { - $$ = makeAArrayExpr(NIL, @1); + $$ = makeAArrayExpr(NIL, @1, @2); } ; @@ -16894,17 +17807,6 @@ trim_list: a_expr FROM expr_list { $$ = lappend($3, $1); } | expr_list { $$ = $1; } ; -in_expr: select_with_parens - { - SubLink *n = makeNode(SubLink); - - n->subselect = $1; - /* other fields will be filled later */ - $$ = (Node *) n; - } - | '(' expr_list ')' { $$ = (Node *) $2; } - ; - /* * Define SQL-style CASE clause. * - Full specification @@ -17260,6 +18162,214 @@ json_array_aggregate_order_by_clause_opt: | /* EMPTY */ { $$ = NIL; } ; + +/***************************************************************************** + * + * graph patterns + * + *****************************************************************************/ + +graph_pattern: + path_pattern_list where_clause + { + GraphPattern *gp = makeNode(GraphPattern); + + gp->path_pattern_list = $1; + gp->whereClause = $2; + $$ = (Node *) gp; + } + ; + +path_pattern_list: + path_pattern { $$ = list_make1($1); } + | path_pattern_list ',' path_pattern { $$ = lappend($1, $3); } + ; + +path_pattern: + path_pattern_expression { $$ = $1; } + ; + +/* + * path pattern expression + */ + +path_pattern_expression: + path_term { $$ = $1; } + /* | path_multiset_alternation */ + /* | path_pattern_union */ + ; + +path_term: + path_factor { $$ = list_make1($1); } + | path_term path_factor { $$ = lappend($1, $2); } + ; + +path_factor: + path_primary opt_graph_pattern_quantifier + { + GraphElementPattern *gep = (GraphElementPattern *) $1; + + gep->quantifier = $2; + + $$ = (Node *) gep; + } + ; + +path_primary: + '(' opt_colid opt_is_label_expression where_clause ')' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = VERTEX_PATTERN; + gep->variable = $2; + gep->labelexpr = $3; + gep->whereClause = $4; + gep->location = @1; + + $$ = (Node *) gep; + } + /* full edge pointing left: <-[ xxx ]- */ + | '<' '-' '[' opt_colid opt_is_label_expression where_clause ']' '-' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_LEFT; + gep->variable = $4; + gep->labelexpr = $5; + gep->whereClause = $6; + gep->location = @1; + + $$ = (Node *) gep; + } + /* full edge pointing right: -[ xxx ]-> */ + | '-' '[' opt_colid opt_is_label_expression where_clause ']' '-' '>' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_RIGHT; + gep->variable = $3; + gep->labelexpr = $4; + gep->whereClause = $5; + gep->location = @1; + + $$ = (Node *) gep; + } + | '-' '[' opt_colid opt_is_label_expression where_clause ']' RIGHT_ARROW + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_RIGHT; + gep->variable = $3; + gep->labelexpr = $4; + gep->whereClause = $5; + gep->location = @1; + + $$ = (Node *) gep; + } + /* full edge any direction: -[ xxx ]- */ + | '-' '[' opt_colid opt_is_label_expression where_clause ']' '-' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_ANY; + gep->variable = $3; + gep->labelexpr = $4; + gep->whereClause = $5; + gep->location = @1; + + $$ = (Node *) gep; + } + /* abbreviated edge patterns */ + | '<' '-' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_LEFT; + gep->location = @1; + + $$ = (Node *) gep; + } + | '-' '>' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_RIGHT; + gep->location = @1; + + $$ = (Node *) gep; + } + | RIGHT_ARROW + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_RIGHT; + gep->location = @1; + + $$ = (Node *) gep; + } + | '-' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = EDGE_PATTERN_ANY; + gep->location = @1; + + $$ = (Node *) gep; + } + | '(' path_pattern_expression where_clause ')' + { + GraphElementPattern *gep = makeNode(GraphElementPattern); + + gep->kind = PAREN_EXPR; + gep->subexpr = $2; + gep->whereClause = $3; + gep->location = @1; + + $$ = (Node *) gep; + } + ; + +opt_colid: + ColId { $$ = $1; } + | /*EMPTY*/ { $$ = NULL; } + ; + +opt_is_label_expression: + IS label_expression { $$ = $2; } + | /*EMPTY*/ { $$ = NULL; } + ; + +/* + * graph pattern quantifier + */ + +opt_graph_pattern_quantifier: + '{' Iconst '}' { $$ = list_make2_int($2, $2); } + | '{' ',' Iconst '}' { $$ = list_make2_int(0, $3); } + | '{' Iconst ',' Iconst '}' { $$ = list_make2_int($2, $4); } + | /*EMPTY*/ { $$ = NULL; } + ; + +/* + * label expression + */ + +label_expression: + label_term + | label_disjunction + ; + +label_disjunction: + label_expression '|' label_term + { $$ = makeOrExpr($1, $3, @2); } + ; + +label_term: + name + { $$ = makeColumnRef($1, NIL, @1, yyscanner); } + ; + + /***************************************************************************** * * target list for SELECT @@ -17597,6 +18707,7 @@ PLpgSQL_Expr: opt_distinct_clause opt_target_list n->whereClause = $4; n->groupClause = ($5)->list; n->groupDistinct = ($5)->distinct; + n->groupByAll = ($5)->all; n->havingClause = $6; n->windowClause = $7; n->sortClause = $8; @@ -17780,6 +18891,7 @@ unreserved_keyword: | DELIMITERS | DEPENDS | DEPTH + | DESTINATION | DETACH | DICTIONARY | DISABLE_P @@ -17789,6 +18901,7 @@ unreserved_keyword: | DOUBLE_P | DROP | EACH + | EDGE | EMPTY_P | ENABLE_P | ENCODING @@ -17819,6 +18932,7 @@ unreserved_keyword: | GENERATED | GLOBAL | GRANTED + | GRAPH | GROUPS | HANDLER | HEADER_P @@ -17826,6 +18940,7 @@ unreserved_keyword: | HOUR_P | IDENTITY_P | IF_P + | IGNORE_P | IMMEDIATE | IMMUTABLE | IMPLICIT_P @@ -17861,6 +18976,7 @@ unreserved_keyword: | LOCK_P | LOCKED | LOGGED + | LSN_P | MAPPING | MATCH | MATCHED @@ -17883,6 +18999,7 @@ unreserved_keyword: | NFKC | NFKD | NO + | NODE | NORMALIZED | NOTHING | NOTIFY @@ -17909,6 +19026,7 @@ unreserved_keyword: | PARSER | PARTIAL | PARTITION + | PARTITIONS | PASSING | PASSWORD | PATH @@ -17916,6 +19034,7 @@ unreserved_keyword: | PLAN | PLANS | POLICY + | PORTION | PRECEDING | PREPARE | PREPARED @@ -17926,6 +19045,8 @@ unreserved_keyword: | PROCEDURE | PROCEDURES | PROGRAM + | PROPERTIES + | PROPERTY | PUBLICATION | QUOTE | QUOTES @@ -17937,13 +19058,16 @@ unreserved_keyword: | REFERENCING | REFRESH | REINDEX + | RELATIONSHIP | RELATIVE_P | RELEASE | RENAME + | REPACK | REPEATABLE | REPLACE | REPLICA | RESET + | RESPECT_P | RESTART | RESTRICT | RETURN @@ -17977,6 +19101,7 @@ unreserved_keyword: | SKIP | SNAPSHOT | SOURCE + | SPLIT | SQL_P | STABLE | STANDALONE_P @@ -18026,10 +19151,12 @@ unreserved_keyword: | VALUE_P | VARYING | VERSION_P + | VERTEX | VIEW | VIEWS | VIRTUAL | VOLATILE + | WAIT | WHITESPACE_P | WITHIN | WITHOUT @@ -18065,6 +19192,7 @@ col_name_keyword: | EXISTS | EXTRACT | FLOAT_P + | GRAPH_TABLE | GREATEST | GROUPING | INOUT @@ -18356,6 +19484,7 @@ bare_label_keyword: | DEPENDS | DEPTH | DESC + | DESTINATION | DETACH | DICTIONARY | DISABLE_P @@ -18367,6 +19496,7 @@ bare_label_keyword: | DOUBLE_P | DROP | EACH + | EDGE | ELSE | EMPTY_P | ENABLE_P @@ -18405,6 +19535,8 @@ bare_label_keyword: | GENERATED | GLOBAL | GRANTED + | GRAPH + | GRAPH_TABLE | GREATEST | GROUPING | GROUPS @@ -18476,6 +19608,7 @@ bare_label_keyword: | LOCK_P | LOCKED | LOGGED + | LSN_P | MAPPING | MATCH | MATCHED @@ -18500,6 +19633,7 @@ bare_label_keyword: | NFKC | NFKD | NO + | NODE | NONE | NORMALIZE | NORMALIZED @@ -18536,6 +19670,7 @@ bare_label_keyword: | PARSER | PARTIAL | PARTITION + | PARTITIONS | PASSING | PASSWORD | PATH @@ -18544,6 +19679,7 @@ bare_label_keyword: | PLAN | PLANS | POLICY + | PORTION | POSITION | PRECEDING | PREPARE @@ -18556,6 +19692,8 @@ bare_label_keyword: | PROCEDURE | PROCEDURES | PROGRAM + | PROPERTIES + | PROPERTY | PUBLICATION | QUOTE | QUOTES @@ -18569,9 +19707,11 @@ bare_label_keyword: | REFERENCING | REFRESH | REINDEX + | RELATIONSHIP | RELATIVE_P | RELEASE | RENAME + | REPACK | REPEATABLE | REPLACE | REPLICA @@ -18616,6 +19756,7 @@ bare_label_keyword: | SNAPSHOT | SOME | SOURCE + | SPLIT | SQL_P | STABLE | STANDALONE_P @@ -18683,10 +19824,12 @@ bare_label_keyword: | VARIADIC | VERBOSE | VERSION_P + | VERTEX | VIEW | VIEWS | VIRTUAL | VOLATILE + | WAIT | WHEN | WHITESPACE_P | WORK @@ -18748,47 +19891,6 @@ updateRawStmtEnd(RawStmt *rs, int end_location) rs->stmt_len = end_location - rs->stmt_location; } -/* - * Adjust a PreparableStmt to reflect that it doesn't run to the end of the - * string. - */ -static void -updatePreparableStmtEnd(Node *n, int end_location) -{ - if (IsA(n, SelectStmt)) - { - SelectStmt *stmt = (SelectStmt *) n; - - stmt->stmt_len = end_location - stmt->stmt_location; - } - else if (IsA(n, InsertStmt)) - { - InsertStmt *stmt = (InsertStmt *) n; - - stmt->stmt_len = end_location - stmt->stmt_location; - } - else if (IsA(n, UpdateStmt)) - { - UpdateStmt *stmt = (UpdateStmt *) n; - - stmt->stmt_len = end_location - stmt->stmt_location; - } - else if (IsA(n, DeleteStmt)) - { - DeleteStmt *stmt = (DeleteStmt *) n; - - stmt->stmt_len = end_location - stmt->stmt_location; - } - else if (IsA(n, MergeStmt)) - { - MergeStmt *stmt = (MergeStmt *) n; - - stmt->stmt_len = end_location - stmt->stmt_location; - } - else - elog(ERROR, "unexpected node type %d", (int) n->type); -} - static Node * makeColumnRef(char *colname, List *indirection, int location, core_yyscan_t yyscanner) @@ -19167,14 +20269,11 @@ insertSelectOptions(SelectStmt *stmt, errmsg("multiple WITH clauses not allowed"), parser_errposition(exprLocation((Node *) withClause)))); stmt->withClause = withClause; - - /* Update SelectStmt's location to the start of the WITH clause */ - stmt->stmt_location = withClause->location; } } static Node * -makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg, int location) +makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg) { SelectStmt *n = makeNode(SelectStmt); @@ -19182,7 +20281,6 @@ makeSetOp(SetOperation op, bool all, Node *larg, Node *rarg, int location) n->all = all; n->larg = (SelectStmt *) larg; n->rarg = (SelectStmt *) rarg; - n->stmt_location = location; return (Node *) n; } @@ -19300,12 +20398,14 @@ makeNotExpr(Node *expr, int location) } static Node * -makeAArrayExpr(List *elements, int location) +makeAArrayExpr(List *elements, int location, int location_end) { A_ArrayExpr *n = makeNode(A_ArrayExpr); n->elements = elements; n->location = location; + n->list_start = location; + n->list_end = location_end; return (Node *) n; } @@ -19638,6 +20738,49 @@ parsePartitionStrategy(char *strategy, int location, core_yyscan_t yyscanner) } +/* + * Process all_objects_list to set all_tables and/or all_sequences. + * Also, checks if the pub_object_type has been specified more than once. + */ +static void +preprocess_pub_all_objtype_list(List *all_objects_list, List **pubobjects, + bool *all_tables, bool *all_sequences, + core_yyscan_t yyscanner) +{ + if (!all_objects_list) + return; + + *all_tables = false; + *all_sequences = false; + + foreach_ptr(PublicationAllObjSpec, obj, all_objects_list) + { + if (obj->pubobjtype == PUBLICATION_ALL_TABLES) + { + if (*all_tables) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid publication object list"), + errdetail("ALL TABLES can be specified only once."), + parser_errposition(obj->location)); + + *all_tables = true; + *pubobjects = list_concat(*pubobjects, obj->except_tables); + } + else if (obj->pubobjtype == PUBLICATION_ALL_SEQUENCES) + { + if (*all_sequences) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid publication object list"), + errdetail("ALL SEQUENCES can be specified only once."), + parser_errposition(obj->location)); + + *all_sequences = true; + } + } +} + /* * Process pubobjspec_list to check for errors in any of the objects and * convert PUBLICATIONOBJ_CONTINUATION into appropriate PublicationObjSpecType. diff --git a/src/backend/parser/gramparse.h b/src/backend/parser/gramparse.h index a06a918c6541e..ecd2dbb96b1aa 100644 --- a/src/backend/parser/gramparse.h +++ b/src/backend/parser/gramparse.h @@ -8,7 +8,7 @@ * Definitions that are needed outside the core parser should be in parser.h. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/parser/gramparse.h diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 874aa749aa69a..86c09b29ec2c7 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'analyze.c', @@ -10,6 +10,7 @@ backend_sources += files( 'parse_enr.c', 'parse_expr.c', 'parse_func.c', + 'parse_graphtable.c', 'parse_jsontable.c', 'parse_merge.c', 'parse_node.c', diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 0ac8966e30ff3..acb933392deb7 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -3,7 +3,7 @@ * parse_agg.c * handle aggregates and window functions in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -38,6 +38,8 @@ typedef struct ParseState *pstate; int min_varlevel; int min_agglevel; + int min_ctelevel; + RangeTblEntry *min_cte; int sublevels_up; } check_agg_arguments_context; @@ -46,9 +48,10 @@ typedef struct ParseState *pstate; Query *qry; bool hasJoinRTEs; - List *groupClauses; - List *groupClauseCommonVars; - List *gset_common; + List *groupClauses; /* list of TargetEntry */ + List *groupClauseCommonVars; /* list of Vars */ + List *groupClauseSubLevels; /* list of lists of TargetEntry */ + List *gset_common; /* integer list of sortgrouprefs */ bool have_non_var_grouping; List **func_grouped_rels; int sublevels_up; @@ -58,7 +61,8 @@ typedef struct static int check_agg_arguments(ParseState *pstate, List *directargs, List *args, - Expr *filter); + Expr *filter, + int agglocation); static bool check_agg_arguments_walker(Node *node, check_agg_arguments_context *context); static Node *substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry, @@ -339,7 +343,8 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) min_varlevel = check_agg_arguments(pstate, directargs, args, - filter); + filter, + location); *p_levelsup = min_varlevel; @@ -579,6 +584,21 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_FOR_PORTION: + if (isAgg) + err = _("aggregate functions are not allowed in FOR PORTION OF expressions"); + else + err = _("grouping operations are not allowed in FOR PORTION OF expressions"); + + break; + + case EXPR_KIND_PROPGRAPH_PROPERTY: + if (isAgg) + err = _("aggregate functions are not allowed in property definition expressions"); + else + err = _("grouping operations are not allowed in property definition expressions"); + + break; /* * There is intentionally no default: case here, so that the @@ -641,7 +661,8 @@ static int check_agg_arguments(ParseState *pstate, List *directargs, List *args, - Expr *filter) + Expr *filter, + int agglocation) { int agglevel; check_agg_arguments_context context; @@ -649,6 +670,8 @@ check_agg_arguments(ParseState *pstate, context.pstate = pstate; context.min_varlevel = -1; /* signifies nothing found yet */ context.min_agglevel = -1; + context.min_ctelevel = -1; + context.min_cte = NULL; context.sublevels_up = 0; (void) check_agg_arguments_walker((Node *) args, &context); @@ -686,6 +709,20 @@ check_agg_arguments(ParseState *pstate, parser_errposition(pstate, aggloc))); } + /* + * If there's a non-local CTE that's below the aggregate's semantic level, + * complain. It's not quite clear what we should do to fix up such a case + * (treating the CTE reference like a Var seems wrong), and it's also + * unclear whether there is a real-world use for such cases. + */ + if (context.min_ctelevel >= 0 && context.min_ctelevel < agglevel) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("outer-level aggregate cannot use a nested CTE"), + errdetail("CTE \"%s\" is below the aggregate's semantic level.", + context.min_cte->eref->aliasname), + parser_errposition(pstate, agglocation))); + /* * Now check for vars/aggs in the direct arguments, and throw error if * needed. Note that we allow a Var of the agg's semantic level, but not @@ -699,6 +736,7 @@ check_agg_arguments(ParseState *pstate, { context.min_varlevel = -1; context.min_agglevel = -1; + context.min_ctelevel = -1; (void) check_agg_arguments_walker((Node *) directargs, &context); if (context.min_varlevel >= 0 && context.min_varlevel < agglevel) ereport(ERROR, @@ -714,6 +752,13 @@ check_agg_arguments(ParseState *pstate, parser_errposition(pstate, locate_agg_of_level((Node *) directargs, context.min_agglevel)))); + if (context.min_ctelevel >= 0 && context.min_ctelevel < agglevel) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("outer-level aggregate cannot use a nested CTE"), + errdetail("CTE \"%s\" is below the aggregate's semantic level.", + context.min_cte->eref->aliasname), + parser_errposition(pstate, agglocation))); } return agglevel; } @@ -791,6 +836,30 @@ check_agg_arguments_walker(Node *node, parser_errposition(context->pstate, ((WindowFunc *) node)->location))); } + + if (IsA(node, RangeTblEntry)) + { + RangeTblEntry *rte = (RangeTblEntry *) node; + + if (rte->rtekind == RTE_CTE) + { + int ctelevelsup = rte->ctelevelsup; + + /* convert levelsup to frame of reference of original query */ + ctelevelsup -= context->sublevels_up; + /* ignore local CTEs of subqueries */ + if (ctelevelsup >= 0) + { + if (context->min_ctelevel < 0 || + context->min_ctelevel > ctelevelsup) + { + context->min_ctelevel = ctelevelsup; + context->min_cte = rte; + } + } + } + return false; /* allow range_table_walker to continue */ + } if (IsA(node, Query)) { /* Recurse into subselects */ @@ -800,7 +869,7 @@ check_agg_arguments_walker(Node *node, result = query_tree_walker((Query *) node, check_agg_arguments_walker, context, - 0); + QTW_EXAMINE_RTES_BEFORE); context->sublevels_up--; return result; } @@ -970,6 +1039,12 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_PROPGRAPH_PROPERTY: + err = _("window functions are not allowed in property definition expressions"); + break; + case EXPR_KIND_FOR_PORTION: + err = _("window functions are not allowed in FOR PORTION OF expressions"); + break; /* * There is intentionally no default: case here, so that the @@ -1160,8 +1235,8 @@ parseCheckAggregates(ParseState *pstate, Query *qry) } /* - * Build a list of the acceptable GROUP BY expressions for use by - * substitute_grouped_columns(). + * Build a list of the acceptable GROUP BY expressions to save in the + * RTE_GROUP RTE, and for use by substitute_grouped_columns(). * * We get the TLE, not just the expr, because GROUPING wants to know the * sortgroupref. @@ -1178,6 +1253,23 @@ parseCheckAggregates(ParseState *pstate, Query *qry) groupClauses = lappend(groupClauses, expr); } + /* + * If there are any acceptable GROUP BY expressions, build an RTE and + * nsitem for the result of the grouping step. (It's important to do this + * before flattening join alias vars in groupClauses, because the RTE + * should preserve any alias vars that were in the input.) + */ + if (groupClauses) + { + pstate->p_grouping_nsitem = + addRangeTableEntryForGroup(pstate, groupClauses); + + /* Set qry->rtable again in case it was previously NIL */ + qry->rtable = pstate->p_rtable; + /* Mark the Query as having RTE_GROUP RTE */ + qry->hasGroupRTE = true; + } + /* * If there are join alias vars involved, we have to flatten them to the * underlying vars, so that aliased and unaliased vars will be correctly @@ -1185,8 +1277,8 @@ parseCheckAggregates(ParseState *pstate, Query *qry) * entries are RTE_JOIN kind. */ if (hasJoinRTEs) - groupClauses = (List *) flatten_join_alias_vars(NULL, qry, - (Node *) groupClauses); + groupClauses = (List *) + flatten_join_alias_for_parser(qry, (Node *) groupClauses, 0); /* * Detect whether any of the grouping expressions aren't simple Vars; if @@ -1213,21 +1305,6 @@ parseCheckAggregates(ParseState *pstate, Query *qry) } } - /* - * If there are any acceptable GROUP BY expressions, build an RTE and - * nsitem for the result of the grouping step. - */ - if (groupClauses) - { - pstate->p_grouping_nsitem = - addRangeTableEntryForGroup(pstate, groupClauses); - - /* Set qry->rtable again in case it was previously NIL */ - qry->rtable = pstate->p_rtable; - /* Mark the Query as having RTE_GROUP RTE */ - qry->hasGroupRTE = true; - } - /* * Replace grouped variables in the targetlist and HAVING clause with Vars * that reference the RTE_GROUP RTE. Emit an error message if we find any @@ -1246,7 +1323,7 @@ parseCheckAggregates(ParseState *pstate, Query *qry) groupClauses, hasJoinRTEs, have_non_var_grouping); if (hasJoinRTEs) - clause = flatten_join_alias_vars(NULL, qry, clause); + clause = flatten_join_alias_for_parser(qry, clause, 0); qry->targetList = (List *) substitute_grouped_columns(clause, pstate, qry, groupClauses, groupClauseCommonVars, @@ -1259,7 +1336,7 @@ parseCheckAggregates(ParseState *pstate, Query *qry) groupClauses, hasJoinRTEs, have_non_var_grouping); if (hasJoinRTEs) - clause = flatten_join_alias_vars(NULL, qry, clause); + clause = flatten_join_alias_for_parser(qry, clause, 0); qry->havingQual = substitute_grouped_columns(clause, pstate, qry, groupClauses, groupClauseCommonVars, @@ -1289,17 +1366,6 @@ parseCheckAggregates(ParseState *pstate, Query *qry) * * NOTE: we assume that the given clause has been transformed suitably for * parser output. This means we can use expression_tree_mutator. - * - * NOTE: we recognize grouping expressions in the main query, but only - * grouping Vars in subqueries. For example, this will be rejected, - * although it could be allowed: - * SELECT - * (SELECT x FROM bar where y = (foo.a + foo.b)) - * FROM foo - * GROUP BY a + b; - * The difficulty is the need to account for different sublevels_up. - * This appears to require a whole custom version of equal(), which is - * way more pain than the feature seems worth. */ static Node * substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry, @@ -1315,6 +1381,7 @@ substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry, context.hasJoinRTEs = false; /* assume caller flattened join Vars */ context.groupClauses = groupClauses; context.groupClauseCommonVars = groupClauseCommonVars; + context.groupClauseSubLevels = NIL; context.gset_common = gset_common; context.have_non_var_grouping = have_non_var_grouping; context.func_grouped_rels = func_grouped_rels; @@ -1382,14 +1449,22 @@ substitute_grouped_columns_mutator(Node *node, * If we have any GROUP BY items that are not simple Vars, check to see if * subexpression as a whole matches any GROUP BY item. We need to do this * at every recursion level so that we recognize GROUPed-BY expressions - * before reaching variables within them. But this only works at the outer - * query level, as noted above. + * before reaching variables within them. (Since this approach is pretty + * expensive, we don't do it this way if the items are all simple Vars.) */ - if (context->have_non_var_grouping && context->sublevels_up == 0) + if (context->have_non_var_grouping) { + List *groupClauses; int attnum = 0; - foreach(gl, context->groupClauses) + /* Within a subquery, we need a mutated version of the groupClauses */ + if (context->sublevels_up == 0) + groupClauses = context->groupClauses; + else + groupClauses = list_nth(context->groupClauseSubLevels, + context->sublevels_up - 1); + + foreach(gl, groupClauses) { TargetEntry *tle = (TargetEntry *) lfirst(gl); @@ -1432,7 +1507,7 @@ substitute_grouped_columns_mutator(Node *node, /* * Check for a match, if we didn't do it above. */ - if (!context->have_non_var_grouping || context->sublevels_up != 0) + if (!context->have_non_var_grouping) { int attnum = 0; @@ -1515,6 +1590,24 @@ substitute_grouped_columns_mutator(Node *node, Query *newnode; context->sublevels_up++; + + /* + * If we have non-Var grouping expressions, we'll need a copy of the + * groupClauses list that's mutated to match this sublevels_up depth. + * Build one if we've not yet visited a subquery at this depth. + */ + if (context->have_non_var_grouping && + context->sublevels_up > list_length(context->groupClauseSubLevels)) + { + List *subGroupClauses = copyObject(context->groupClauses); + + IncrementVarSublevelsUp((Node *) subGroupClauses, + context->sublevels_up, 0); + context->groupClauseSubLevels = + lappend(context->groupClauseSubLevels, subGroupClauses); + Assert(context->sublevels_up == list_length(context->groupClauseSubLevels)); + } + newnode = query_tree_mutator((Query *) node, substitute_grouped_columns_mutator, context, @@ -1549,6 +1642,7 @@ finalize_grouping_exprs(Node *node, ParseState *pstate, Query *qry, context.hasJoinRTEs = hasJoinRTEs; context.groupClauses = groupClauses; context.groupClauseCommonVars = NIL; + context.groupClauseSubLevels = NIL; context.gset_common = NIL; context.have_non_var_grouping = have_non_var_grouping; context.func_grouped_rels = NULL; @@ -1621,7 +1715,9 @@ finalize_grouping_exprs_walker(Node *node, Index ref = 0; if (context->hasJoinRTEs) - expr = flatten_join_alias_vars(NULL, context->qry, expr); + expr = flatten_join_alias_for_parser(context->qry, + expr, + context->sublevels_up); /* * Each expression must match a grouping entry at the current @@ -1651,10 +1747,21 @@ finalize_grouping_exprs_walker(Node *node, } } } - else if (context->have_non_var_grouping && - context->sublevels_up == 0) + else if (context->have_non_var_grouping) { - foreach(gl, context->groupClauses) + List *groupClauses; + + /* + * Within a subquery, we need a mutated version of the + * groupClauses + */ + if (context->sublevels_up == 0) + groupClauses = context->groupClauses; + else + groupClauses = list_nth(context->groupClauseSubLevels, + context->sublevels_up - 1); + + foreach(gl, groupClauses) { TargetEntry *tle = lfirst(gl); @@ -1689,6 +1796,24 @@ finalize_grouping_exprs_walker(Node *node, bool result; context->sublevels_up++; + + /* + * If we have non-Var grouping expressions, we'll need a copy of the + * groupClauses list that's mutated to match this sublevels_up depth. + * Build one if we've not yet visited a subquery at this depth. + */ + if (context->have_non_var_grouping && + context->sublevels_up > list_length(context->groupClauseSubLevels)) + { + List *subGroupClauses = copyObject(context->groupClauses); + + IncrementVarSublevelsUp((Node *) subGroupClauses, + context->sublevels_up, 0); + context->groupClauseSubLevels = + lappend(context->groupClauseSubLevels, subGroupClauses); + Assert(context->sublevels_up == list_length(context->groupClauseSubLevels)); + } + result = query_tree_walker((Query *) node, finalize_grouping_exprs_walker, context, diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index 9f20a70ce13cf..967eea44f1c7c 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -3,7 +3,7 @@ * parse_clause.c * handle clauses in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include "access/htup_details.h" #include "access/nbtree.h" +#include "access/relation.h" #include "access/table.h" #include "access/tsmapi.h" #include "catalog/catalog.h" @@ -35,6 +36,7 @@ #include "parser/parse_collate.h" #include "parser/parse_expr.h" #include "parser/parse_func.h" +#include "parser/parse_graphtable.h" #include "parser/parse_oper.h" #include "parser/parse_relation.h" #include "parser/parse_target.h" @@ -65,6 +67,8 @@ static ParseNamespaceItem *transformRangeFunction(ParseState *pstate, RangeFunction *r); static ParseNamespaceItem *transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf); +static ParseNamespaceItem *transformRangeGraphTable(ParseState *pstate, + RangeGraphTable *rgt); static TableSampleClause *transformRangeTableSample(ParseState *pstate, RangeTableSample *rts); static ParseNamespaceItem *getNSItemForSpecialRelationTypes(ParseState *pstate, @@ -733,7 +737,7 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) tf->ordinalitycol = -1; /* Process column specs */ - names = palloc(sizeof(char *) * list_length(rtf->columns)); + names = palloc_array(char *, list_length(rtf->columns)); colno = 0; foreach(col, rtf->columns) @@ -898,6 +902,126 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) tf, rtf->alias, is_lateral, true); } +/* + * Similar to parserOpenTable() but for property graphs. + */ +static Relation +parserOpenPropGraph(ParseState *pstate, const RangeVar *relation, LOCKMODE lockmode) +{ + Relation rel; + ParseCallbackState pcbstate; + + setup_parser_errposition_callback(&pcbstate, pstate, relation->location); + + rel = relation_openrv(relation, lockmode); + + /* + * In parserOpenTable(), the relkind check is done inside table_openrv*. + * We do it here since we don't have anything like propgraph_open. + */ + if (rel->rd_rel->relkind != RELKIND_PROPGRAPH) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a property graph", + RelationGetRelationName(rel))); + + cancel_parser_errposition_callback(&pcbstate); + return rel; +} + +/* + * transformRangeGraphTable -- transform a GRAPH_TABLE clause + */ +static ParseNamespaceItem * +transformRangeGraphTable(ParseState *pstate, RangeGraphTable *rgt) +{ + Relation rel; + Oid graphid; + GraphTableParseState *gpstate = palloc0_object(GraphTableParseState); + Node *gp; + List *columns = NIL; + List *colnames = NIL; + ListCell *lc; + int resno = 0; + bool saved_hasSublinks; + + rel = parserOpenPropGraph(pstate, rgt->graph_name, AccessShareLock); + + graphid = RelationGetRelid(rel); + + gpstate->graphid = graphid; + + /* + * The syntax does not allow nested GRAPH_TABLE and this function + * prohibits subquery within GRAPH_TABLE. There should be only one + * GRAPH_TABLE being transformed at a time. + */ + Assert(!pstate->p_graph_table_pstate); + pstate->p_graph_table_pstate = gpstate; + + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + saved_hasSublinks = pstate->p_hasSubLinks; + pstate->p_hasSubLinks = false; + + gp = transformGraphPattern(pstate, rgt->graph_pattern); + + /* + * Construct a targetlist representing the COLUMNS specified in the + * GRAPH_TABLE. This uses previously constructed list of element pattern + * variables in the GraphTableParseState. + */ + foreach(lc, rgt->columns) + { + ResTarget *rt = lfirst_node(ResTarget, lc); + Node *colexpr; + TargetEntry *te; + char *colname; + + colexpr = transformExpr(pstate, rt->val, EXPR_KIND_SELECT_TARGET); + + if (rt->name) + colname = rt->name; + else + { + if (IsA(colexpr, GraphPropertyRef)) + colname = get_propgraph_property_name(castNode(GraphPropertyRef, colexpr)->propid); + else + { + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("complex graph table column must specify an explicit column name"), + parser_errposition(pstate, rt->location)); + colname = NULL; + } + } + + colnames = lappend(colnames, makeString(colname)); + + te = makeTargetEntry((Expr *) colexpr, ++resno, colname, false); + columns = lappend(columns, te); + } + + table_close(rel, NoLock); + + pstate->p_graph_table_pstate = NULL; + pstate->p_lateral_active = false; + + /* + * If we support subqueries within GRAPH_TABLE, those need to be + * propagated to the queries resulting from rewriting graph table RTE. We + * don't do that right now, hence prohibit it for now. + */ + if (pstate->p_hasSubLinks) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("subqueries within GRAPH_TABLE reference are not supported"))); + pstate->p_hasSubLinks = saved_hasSublinks; + + return addRangeTableEntryForGraphTable(pstate, graphid, castNode(GraphPattern, gp), columns, colnames, rgt->alias, false, true); +} + /* * transformRangeTableSample --- transform a TABLESAMPLE clause * @@ -1121,6 +1245,18 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } + else if (IsA(n, RangeGraphTable)) + { + RangeTblRef *rtr; + ParseNamespaceItem *nsitem; + + nsitem = transformRangeGraphTable(pstate, (RangeGraphTable *) n); + *top_nsitem = nsitem; + *namespace = list_make1(nsitem); + rtr = makeNode(RangeTblRef); + rtr->rtindex = nsitem->p_rtindex; + return (Node *) rtr; + } else if (IsA(n, RangeTableSample)) { /* TABLESAMPLE clause (wrapping some other valid FROM node) */ @@ -1573,7 +1709,7 @@ transformFromClauseItem(ParseState *pstate, Node *n, { ParseNamespaceItem *jnsitem; - jnsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem)); + jnsitem = palloc_object(ParseNamespaceItem); jnsitem->p_names = j->join_using_alias; jnsitem->p_rte = nsitem->p_rte; jnsitem->p_rtindex = nsitem->p_rtindex; @@ -2598,6 +2734,9 @@ transformGroupingSet(List **flatresult, * GROUP BY items will be added to the targetlist (as resjunk columns) * if not already present, so the targetlist must be passed by reference. * + * If GROUP BY ALL is specified, the groupClause will be inferred to be all + * non-aggregate, non-window expressions in the targetlist. + * * This is also used for window PARTITION BY clauses (which act almost the * same, but are always interpreted per SQL99 rules). * @@ -2622,6 +2761,7 @@ transformGroupingSet(List **flatresult, * * pstate ParseState * grouplist clause to transform + * groupByAll is this a GROUP BY ALL statement? * groupingSets reference to list to contain the grouping set tree * targetlist reference to TargetEntry list * sortClause ORDER BY clause (SortGroupClause nodes) @@ -2629,7 +2769,8 @@ transformGroupingSet(List **flatresult, * useSQL99 SQL99 rather than SQL92 syntax */ List * -transformGroupClause(ParseState *pstate, List *grouplist, List **groupingSets, +transformGroupClause(ParseState *pstate, List *grouplist, bool groupByAll, + List **groupingSets, List **targetlist, List *sortClause, ParseExprKind exprKind, bool useSQL99) { @@ -2640,6 +2781,63 @@ transformGroupClause(ParseState *pstate, List *grouplist, List **groupingSets, bool hasGroupingSets = false; Bitmapset *seen_local = NULL; + /* Handle GROUP BY ALL */ + if (groupByAll) + { + /* There cannot have been any explicit grouplist items */ + Assert(grouplist == NIL); + + /* Iterate over targets, adding acceptable ones to the result list */ + foreach_ptr(TargetEntry, tle, *targetlist) + { + /* Ignore junk TLEs */ + if (tle->resjunk) + continue; + + /* + * TLEs containing aggregates are not okay to add to GROUP BY + * (compare checkTargetlistEntrySQL92). But the SQL standard + * directs us to skip them, so it's fine. + */ + if (pstate->p_hasAggs && + contain_aggs_of_level((Node *) tle->expr, 0)) + continue; + + /* + * Likewise, TLEs containing window functions are not okay to add + * to GROUP BY. At this writing, the SQL standard is silent on + * what to do with them, but by analogy to aggregates we'll just + * skip them. + */ + if (pstate->p_hasWindowFuncs && + contain_windowfuncs((Node *) tle->expr)) + continue; + + /* + * Otherwise, add the TLE to the result using default sort/group + * semantics. We specify the parse location as the TLE's + * location, despite the comment for addTargetToGroupList + * discouraging that. The only other thing we could point to is + * the ALL keyword, which seems unhelpful when there are multiple + * TLEs. + */ + result = addTargetToGroupList(pstate, tle, + result, *targetlist, + exprLocation((Node *) tle->expr)); + } + + /* If we found any acceptable targets, we're done */ + if (result != NIL) + return result; + + /* + * Otherwise, the SQL standard says to treat it like "GROUP BY ()". + * Build a representation of that, and let the rest of this function + * handle it. + */ + grouplist = list_make1(makeGroupingSet(GROUPING_SET_EMPTY, NIL, -1)); + } + /* * Recursively flatten implicit RowExprs. (Technically this is only needed * for GROUP BY, per the syntax rules for grouping sets, but we do it @@ -2818,6 +3016,7 @@ transformWindowDefinitions(ParseState *pstate, true /* force SQL99 rules */ ); partitionClause = transformGroupClause(pstate, windef->partitionClause, + false /* not GROUP BY ALL */ , NULL, targetlist, orderClause, @@ -3214,24 +3413,29 @@ resolve_unique_index_expr(ParseState *pstate, InferClause *infer, * Raw grammar re-uses CREATE INDEX infrastructure for unique index * inference clause, and so will accept opclasses by name and so on. * - * Make no attempt to match ASC or DESC ordering or NULLS FIRST/NULLS - * LAST ordering, since those are not significant for inference - * purposes (any unique index matching the inference specification in - * other regards is accepted indifferently). Actively reject this as - * wrong-headed. + * Make no attempt to match ASC or DESC ordering, NULLS FIRST/NULLS + * LAST ordering or opclass options, since those are not significant + * for inference purposes (any unique index matching the inference + * specification in other regards is accepted indifferently). Actively + * reject this as wrong-headed. */ if (ielem->ordering != SORTBY_DEFAULT) ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), - errmsg("ASC/DESC is not allowed in ON CONFLICT clause"), - parser_errposition(pstate, - exprLocation((Node *) infer)))); + errmsg("%s is not allowed in ON CONFLICT clause", + "ASC/DESC"), + parser_errposition(pstate, ielem->location))); if (ielem->nulls_ordering != SORTBY_NULLS_DEFAULT) ereport(ERROR, (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), - errmsg("NULLS FIRST/LAST is not allowed in ON CONFLICT clause"), - parser_errposition(pstate, - exprLocation((Node *) infer)))); + errmsg("%s is not allowed in ON CONFLICT clause", + "NULLS FIRST/LAST"), + parser_errposition(pstate, ielem->location))); + if (ielem->opclassopts) + ereport(ERROR, + errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("operator class options are not allowed in ON CONFLICT clause"), + parser_errposition(pstate, ielem->location)); if (!ielem->expr) { @@ -3271,7 +3475,7 @@ resolve_unique_index_expr(ParseState *pstate, InferClause *infer, pInfer->infercollid = InvalidOid; else pInfer->infercollid = LookupCollation(pstate, ielem->collation, - exprLocation(pInfer->expr)); + ielem->location); if (!ielem->opclass) pInfer->inferopclass = InvalidOid; @@ -3305,13 +3509,15 @@ transformOnConflictArbiter(ParseState *pstate, *arbiterWhere = NULL; *constraint = InvalidOid; - if (onConflictClause->action == ONCONFLICT_UPDATE && !infer) + if ((onConflictClause->action == ONCONFLICT_UPDATE || + onConflictClause->action == ONCONFLICT_SELECT) && !infer) ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("ON CONFLICT DO UPDATE requires inference specification or constraint name"), - errhint("For example, ON CONFLICT (column_name)."), - parser_errposition(pstate, - exprLocation((Node *) onConflictClause)))); + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("ON CONFLICT DO %s requires inference specification or constraint name", + onConflictClause->action == ONCONFLICT_UPDATE ? "UPDATE" : "SELECT"), + errhint("For example, ON CONFLICT (column_name)."), + parser_errposition(pstate, + exprLocation((Node *) onConflictClause))); /* * To simplify certain aspects of its design, speculative insertion into diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c index 0b5b81c7f27ee..913ca53666fa0 100644 --- a/src/backend/parser/parse_coerce.c +++ b/src/backend/parser/parse_coerce.c @@ -3,7 +3,7 @@ * parse_coerce.c * handle type coercions/conversions for parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_cast.h" #include "catalog/pg_class.h" #include "catalog/pg_inherits.h" diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index d2e218353f310..022b8cac122b8 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -29,7 +29,7 @@ * at runtime. If we knew exactly which functions require collation * information, we could throw those errors at parse time instead. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -484,6 +484,7 @@ assign_collations_walker(Node *node, assign_collations_context *context) case T_JoinExpr: case T_FromExpr: case T_OnConflictExpr: + case T_ForPortionOfExpr: case T_SortGroupClause: case T_MergeAction: (void) expression_tree_walker(node, @@ -546,6 +547,7 @@ assign_collations_walker(Node *node, assign_collations_context *context) case T_CaseTestExpr: case T_SetToDefault: case T_CurrentOfExpr: + case T_GraphPropertyRef: /* * General case for childless expression nodes. These should diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c index 366fd901d9d5e..ccde199319afc 100644 --- a/src/backend/parser/parse_cte.c +++ b/src/backend/parser/parse_cte.c @@ -3,7 +3,7 @@ * parse_cte.c * handle CTEs (common table expressions) in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/parser/parse_enr.c b/src/backend/parser/parse_enr.c index 119911598b90d..9b47b7f65fa28 100644 --- a/src/backend/parser/parse_enr.c +++ b/src/backend/parser/parse_enr.c @@ -3,7 +3,7 @@ * parse_enr.c * parser support routines dealing with ephemeral named relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 1f8e2d54673dd..f535f3b9351dd 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3,7 +3,7 @@ * parse_expr.c * handle expressions in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -15,9 +15,9 @@ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -29,6 +29,7 @@ #include "parser/parse_collate.h" #include "parser/parse_expr.h" #include "parser/parse_func.h" +#include "parser/parse_graphtable.h" #include "parser/parse_oper.h" #include "parser/parse_relation.h" #include "parser/parse_target.h" @@ -38,6 +39,7 @@ #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/timestamp.h" +#include "utils/typcache.h" #include "utils/xml.h" /* GUC parameters */ @@ -94,7 +96,8 @@ static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func); static void transformJsonPassingArgs(ParseState *pstate, const char *constructName, JsonFormatType format, List *args, List **passing_values, List **passing_names); -static JsonBehavior *transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, +static JsonBehavior *transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, + JsonBehavior *behavior, JsonBehaviorType default_behavior, JsonReturning *returning); static Node *GetJsonBehaviorConst(JsonBehaviorType btype, int location); @@ -326,7 +329,7 @@ transformExprRecurse(ParseState *pstate, Node *expr) case T_CaseTestExpr: case T_Var: { - result = (Node *) expr; + result = expr; break; } @@ -575,6 +578,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_PROPGRAPH_PROPERTY: /* okay */ break; @@ -584,6 +588,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_PARTITION_BOUND: err = _("cannot use column reference in partition bound expression"); break; + case EXPR_KIND_FOR_PORTION: + err = _("cannot use column reference in FOR PORTION OF expression"); + break; /* * There is intentionally no default: case here, so that the @@ -610,6 +617,16 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) return node; } + /* + * Element pattern variables in a GRAPH_TABLE clause form the innermost + * namespace since we do not allow subqueries in GRAPH_TABLE patterns. Try + * to resolve the column reference as a graph table property reference + * before trying to resolve it as a regular column reference. + */ + node = transformGraphTablePropertyRef(pstate, cref); + if (node != NULL) + return node; + /*---------- * The allowed syntaxes are: * @@ -1130,6 +1147,7 @@ transformAExprIn(ParseState *pstate, A_Expr *a) List *rnonvars; bool useOr; ListCell *l; + bool has_rvars = false; /* * If the operator is <>, combine with AND not OR. @@ -1158,7 +1176,10 @@ transformAExprIn(ParseState *pstate, A_Expr *a) rexprs = lappend(rexprs, rexpr); if (contain_vars_of_level(rexpr, 0)) + { rvars = lappend(rvars, rexpr); + has_rvars = true; + } else rnonvars = lappend(rnonvars, rexpr); } @@ -1225,6 +1246,13 @@ transformAExprIn(ParseState *pstate, A_Expr *a) newa->multidims = false; newa->location = -1; + /* + * If the IN expression contains Vars, disable query jumbling + * squashing. Vars cannot be safely jumbled. + */ + newa->list_start = has_rvars ? -1 : a->rexpr_list_start; + newa->list_end = has_rvars ? -1 : a->rexpr_list_end; + result = (Node *) make_scalar_array_op(pstate, a->name, useOr, @@ -1858,6 +1886,12 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_PROPGRAPH_PROPERTY: + err = _("cannot use subquery in property definition expression"); + break; + case EXPR_KIND_FOR_PORTION: + err = _("cannot use subquery in FOR PORTION OF expression"); + break; /* * There is intentionally no default: case here, so that the @@ -2165,6 +2199,8 @@ transformArrayExpr(ParseState *pstate, A_ArrayExpr *a, /* array_collid will be set by parse_collate.c */ newa->element_typeid = element_type; newa->elements = newcoercedelems; + newa->list_start = a->list_start; + newa->list_end = a->list_end; newa->location = a->location; return (Node *) newa; @@ -2901,7 +2937,7 @@ make_row_comparison_op(ParseState *pstate, List *opname, * operators, and see which interpretations (cmptypes) exist for each * operator. */ - opinfo_lists = (List **) palloc(nopers * sizeof(List *)); + opinfo_lists = palloc_array(List *, nopers); cmptypes = NULL; i = 0; foreach(l, opexprs) @@ -3215,6 +3251,10 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_PROPGRAPH_PROPERTY: + return "property definition expression"; + case EXPR_KIND_FOR_PORTION: + return "FOR PORTION OF"; /* * There is intentionally no default: case here, so that the @@ -3236,7 +3276,7 @@ getJsonEncodingConst(JsonFormat *format) { JsonEncoding encoding; const char *enc; - Name encname = palloc(sizeof(NameData)); + Name encname = palloc_object(NameData); if (!format || format->format_type == JS_FORMAT_DEFAULT || @@ -4051,7 +4091,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, Node *raw_expr = transformExprRecurse(pstate, jsexpr); Node *expr = raw_expr; - *exprtype = exprType(expr); + *exprtype = getBaseType(exprType(expr)); /* prepare input document */ if (*exprtype == BYTEAOID) @@ -4074,7 +4114,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format, if (*exprtype == UNKNOWNOID || typcategory == TYPCATEGORY_STRING) { - expr = coerce_to_target_type(pstate, (Node *) expr, *exprtype, + expr = coerce_to_target_type(pstate, expr, *exprtype, TEXTOID, -1, COERCION_IMPLICIT, COERCE_IMPLICIT_CAST, -1); @@ -4104,13 +4144,14 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred) /* make resulting expression */ if (exprtype != TEXTOID && exprtype != JSONOID && exprtype != JSONBOID) ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("cannot use type %s in IS JSON predicate", - format_type_be(exprtype)))); + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("cannot use type %s in IS JSON predicate", + format_type_be(exprType(expr))), + parser_errposition(pstate, exprLocation(expr))); /* This intentionally(?) drops the format clause. */ return makeJsonIsPredicate(expr, NULL, pred->item_type, - pred->unique_keys, pred->location); + pred->unique_keys, exprtype, pred->location); } /* @@ -4280,6 +4321,9 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) { JsonExpr *jsexpr; Node *path_spec; + Oid pathspec_type; + int pathspec_loc; + Node *coerced_path_spec; const char *func_name = NULL; JsonFormatType default_format; @@ -4495,17 +4539,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->format = func->context_item->format; path_spec = transformExprRecurse(pstate, func->pathspec); - path_spec = coerce_to_target_type(pstate, path_spec, exprType(path_spec), - JSONPATHOID, -1, - COERCION_EXPLICIT, COERCE_IMPLICIT_CAST, - exprLocation(path_spec)); - if (path_spec == NULL) + pathspec_type = exprType(path_spec); + pathspec_loc = exprLocation(path_spec); + coerced_path_spec = coerce_to_target_type(pstate, path_spec, + pathspec_type, + JSONPATHOID, -1, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + pathspec_loc); + if (coerced_path_spec == NULL) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("JSON path expression must be of type %s, not of type %s", - "jsonpath", format_type_be(exprType(path_spec))), - parser_errposition(pstate, exprLocation(path_spec)))); - jsexpr->path_spec = path_spec; + "jsonpath", format_type_be(pathspec_type)), + parser_errposition(pstate, pathspec_loc))); + jsexpr->path_spec = coerced_path_spec; /* Transform and coerce the PASSING arguments to jsonb. */ transformJsonPassingArgs(pstate, func_name, @@ -4525,13 +4573,16 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) { jsexpr->returning->typid = BOOLOID; jsexpr->returning->typmod = -1; + jsexpr->collation = InvalidOid; } /* JSON_TABLE() COLUMNS can specify a non-boolean type. */ if (jsexpr->returning->typid != BOOLOID) jsexpr->use_json_coercion = true; - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + jsexpr->on_error = transformJsonBehavior(pstate, + jsexpr, + func->on_error, JSON_BEHAVIOR_FALSE, jsexpr->returning); break; @@ -4546,6 +4597,8 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) ret->typmod = -1; } + jsexpr->collation = get_typcollation(jsexpr->returning->typid); + /* * Keep quotes on scalar strings by default, omitting them only if * OMIT QUOTES is specified. @@ -4562,11 +4615,15 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->use_json_coercion = true; /* Assume NULL ON EMPTY when ON EMPTY is not specified. */ - jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + jsexpr->on_empty = transformJsonBehavior(pstate, + jsexpr, + func->on_empty, JSON_BEHAVIOR_NULL, jsexpr->returning); /* Assume NULL ON ERROR when ON ERROR is not specified. */ - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + jsexpr->on_error = transformJsonBehavior(pstate, + jsexpr, + func->on_error, JSON_BEHAVIOR_NULL, jsexpr->returning); break; @@ -4578,6 +4635,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->returning->typid = TEXTOID; jsexpr->returning->typmod = -1; } + jsexpr->collation = get_typcollation(jsexpr->returning->typid); /* * Override whatever transformJsonOutput() set these to, which @@ -4596,18 +4654,22 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) if (jsexpr->returning->typid != TEXTOID) { if (get_typtype(jsexpr->returning->typid) == TYPTYPE_DOMAIN && - DomainHasConstraints(jsexpr->returning->typid)) + DomainHasConstraints(jsexpr->returning->typid, NULL)) jsexpr->use_json_coercion = true; else jsexpr->use_io_coercion = true; } /* Assume NULL ON EMPTY when ON EMPTY is not specified. */ - jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, + jsexpr->on_empty = transformJsonBehavior(pstate, + jsexpr, + func->on_empty, JSON_BEHAVIOR_NULL, jsexpr->returning); /* Assume NULL ON ERROR when ON ERROR is not specified. */ - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + jsexpr->on_error = transformJsonBehavior(pstate, + jsexpr, + func->on_error, JSON_BEHAVIOR_NULL, jsexpr->returning); break; @@ -4618,6 +4680,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->returning->typid = exprType(jsexpr->formatted_expr); jsexpr->returning->typmod = -1; } + jsexpr->collation = get_typcollation(jsexpr->returning->typid); /* * Assume EMPTY ARRAY ON ERROR when ON ERROR is not specified. @@ -4625,7 +4688,9 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) * ON EMPTY cannot be specified at the top level but it can be for * the individual columns. */ - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + jsexpr->on_error = transformJsonBehavior(pstate, + jsexpr, + func->on_error, JSON_BEHAVIOR_EMPTY_ARRAY, jsexpr->returning); break; @@ -4701,7 +4766,8 @@ ValidJsonBehaviorDefaultExpr(Node *expr, void *context) * Transform a JSON BEHAVIOR clause. */ static JsonBehavior * -transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, +transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr, + JsonBehavior *behavior, JsonBehaviorType default_behavior, JsonReturning *returning) { @@ -4716,7 +4782,11 @@ transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, location = behavior->location; if (btype == JSON_BEHAVIOR_DEFAULT) { + Oid targetcoll = jsexpr->collation; + Oid exprcoll; + expr = transformExprRecurse(pstate, behavior->expr); + if (!ValidJsonBehaviorDefaultExpr(expr, NULL)) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), @@ -4732,6 +4802,24 @@ transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("DEFAULT expression must not return a set"), parser_errposition(pstate, exprLocation(expr)))); + + /* + * Reject a DEFAULT expression whose collation differs from the + * enclosing JSON expression's result collation + * (jsexpr->collation), as chosen by the RETURNING clause. + */ + exprcoll = exprCollation(expr); + if (!OidIsValid(exprcoll)) + exprcoll = get_typcollation(exprType(expr)); + if (OidIsValid(targetcoll) && OidIsValid(exprcoll) && + targetcoll != exprcoll) + ereport(ERROR, + errcode(ERRCODE_COLLATION_MISMATCH), + errmsg("collation of DEFAULT expression conflicts with RETURNING clause"), + errdetail("\"%s\" versus \"%s\"", + get_collation_name(exprcoll), + get_collation_name(targetcoll)), + parser_errposition(pstate, exprLocation(expr))); } } diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 583bbbf232f04..35ff642714799 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -3,7 +3,7 @@ * parse_func.c * handle function calls in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -42,6 +42,8 @@ typedef enum FUNCLOOKUP_AMBIGUOUS, } FuncLookupError; +static int func_lookup_failure_details(int fgc_flags, List *argnames, + bool proc_call); static void unify_hypothetical_args(ParseState *pstate, List *fargs, int numAggregatedArgs, Oid *actual_arg_types, Oid *declared_arg_types); @@ -98,6 +100,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, bool agg_star = (fn ? fn->agg_star : false); bool agg_distinct = (fn ? fn->agg_distinct : false); bool func_variadic = (fn ? fn->func_variadic : false); + int ignore_nulls = (fn ? fn->ignore_nulls : NO_NULLTREATMENT); CoercionForm funcformat = (fn ? fn->funcformat : COERCE_EXPLICIT_CALL); bool could_be_projection; Oid rettype; @@ -115,6 +118,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, int nvargs; Oid vatype; FuncDetailCode fdresult; + int fgc_flags; char aggkind = 0; ParseCallbackState pcbstate; @@ -266,6 +270,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, fdresult = func_get_detail(funcname, fargs, argnames, nargs, actual_arg_types, !func_variadic, true, proc_call, + &fgc_flags, &funcid, &rettype, &retset, &nvargs, &vatype, &declared_arg_types, &argdefaults); @@ -514,6 +519,13 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("%s is not an ordered-set aggregate, so it cannot have WITHIN GROUP", NameListToString(funcname)), parser_errposition(pstate, location))); + + /* It also can't treat nulls as a window function */ + if (ignore_nulls != NO_NULLTREATMENT) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("aggregate functions do not accept RESPECT/IGNORE NULLS"), + parser_errposition(pstate, location))); } } else if (fdresult == FUNCDETAIL_WINDOWFUNC) @@ -563,8 +575,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("procedure %s is not unique", func_signature_string(funcname, nargs, argnames, actual_arg_types)), - errhint("Could not choose a best candidate procedure. " - "You might need to add explicit type casts."), + errdetail("Could not choose a best candidate procedure."), + errhint("You might need to add explicit type casts."), parser_errposition(pstate, location))); else ereport(ERROR, @@ -572,8 +584,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("function %s is not unique", func_signature_string(funcname, nargs, argnames, actual_arg_types)), - errhint("Could not choose a best candidate function. " - "You might need to add explicit type casts."), + errdetail("Could not choose a best candidate function."), + errhint("You might need to add explicit type casts."), parser_errposition(pstate, location))); } else @@ -601,7 +613,9 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, /* * No function, and no column either. Since we're dealing with - * function notation, report "function does not exist". + * function notation, report "function/procedure does not exist". + * Depending on what was returned in fgc_flags, we can add some color + * to that with detail or hint messages. */ if (list_length(agg_order) > 1 && !agg_within_group) { @@ -611,8 +625,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("function %s does not exist", func_signature_string(funcname, nargs, argnames, actual_arg_types)), - errhint("No aggregate function matches the given name and argument types. " - "Perhaps you misplaced ORDER BY; ORDER BY must appear " + errdetail("No aggregate function matches the given name and argument types."), + errhint("Perhaps you misplaced ORDER BY; ORDER BY must appear " "after all regular arguments of the aggregate."), parser_errposition(pstate, location))); } @@ -622,8 +636,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("procedure %s does not exist", func_signature_string(funcname, nargs, argnames, actual_arg_types)), - errhint("No procedure matches the given name and argument types. " - "You might need to add explicit type casts."), + func_lookup_failure_details(fgc_flags, argnames, + proc_call), parser_errposition(pstate, location))); else ereport(ERROR, @@ -631,8 +645,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, errmsg("function %s does not exist", func_signature_string(funcname, nargs, argnames, actual_arg_types)), - errhint("No function matches the given name and argument types. " - "You might need to add explicit type casts."), + func_lookup_failure_details(fgc_flags, argnames, + proc_call), parser_errposition(pstate, location))); } @@ -834,6 +848,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, wfunc->winstar = agg_star; wfunc->winagg = (fdresult == FUNCDETAIL_AGGREGATE); wfunc->aggfilter = agg_filter; + wfunc->ignore_nulls = ignore_nulls; wfunc->runCondition = NIL; wfunc->location = location; @@ -905,6 +920,104 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, return retval; } +/* + * Interpret the fgc_flags and issue a suitable detail or hint message. + * + * Helper function to reduce code duplication while throwing a + * function-not-found error. + */ +static int +func_lookup_failure_details(int fgc_flags, List *argnames, bool proc_call) +{ + /* + * If not FGC_NAME_VISIBLE, we shouldn't raise the question of whether the + * arguments are wrong. If the function name was not schema-qualified, + * it's helpful to distinguish between doesn't-exist-anywhere and + * not-in-search-path; but if it was, there's really nothing to add to the + * basic "function/procedure %s does not exist" message. + * + * Note: we passed missing_ok = false to FuncnameGetCandidates, so there's + * no need to consider FGC_SCHEMA_EXISTS here: we'd have already thrown an + * error if an explicitly-given schema doesn't exist. + */ + if (!(fgc_flags & FGC_NAME_VISIBLE)) + { + if (fgc_flags & FGC_SCHEMA_GIVEN) + return 0; /* schema-qualified name */ + else if (!(fgc_flags & FGC_NAME_EXISTS)) + { + if (proc_call) + return errdetail("There is no procedure of that name."); + else + return errdetail("There is no function of that name."); + } + else + { + if (proc_call) + return errdetail("A procedure of that name exists, but it is not in the search_path."); + else + return errdetail("A function of that name exists, but it is not in the search_path."); + } + } + + /* + * Next, complain if nothing had the right number of arguments. (This + * takes precedence over wrong-argnames cases because we won't even look + * at the argnames unless there's a workable number of arguments.) + */ + if (!(fgc_flags & FGC_ARGCOUNT_MATCH)) + { + if (proc_call) + return errdetail("No procedure of that name accepts the given number of arguments."); + else + return errdetail("No function of that name accepts the given number of arguments."); + } + + /* + * If there are argnames, and we failed to match them, again we should + * mention that and not bring up the argument types. + */ + if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_MATCH)) + { + if (proc_call) + return errdetail("No procedure of that name accepts the given argument names."); + else + return errdetail("No function of that name accepts the given argument names."); + } + + /* + * We could have matched all the given argnames and still not have had a + * valid call, either because of improper use of mixed notation, or + * because of missing arguments, or because the user misused VARIADIC. The + * rules about named-argument matching are finicky enough that it's worth + * trying to be specific about the problem. (The messages here are chosen + * with full knowledge of the steps that namespace.c uses while checking a + * potential match.) + */ + if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_NONDUP)) + return errdetail("In the closest available match, " + "an argument was specified both positionally and by name."); + + if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_ALL)) + return errdetail("In the closest available match, " + "not all required arguments were supplied."); + + if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_VALID)) + return errhint("This call would be correct if the variadic array were labeled VARIADIC and placed last."); + + if (fgc_flags & FGC_VARIADIC_FAIL) + return errhint("The VARIADIC parameter must be placed last, even when using argument names."); + + /* + * Otherwise, the problem must be incorrect argument types. + */ + if (proc_call) + (void) errdetail("No procedure of that name accepts the given argument types."); + else + (void) errdetail("No function of that name accepts the given argument types."); + return errhint("You might need to add explicit type casts."); +} + /* func_match_argtypes() * @@ -1372,9 +1485,14 @@ func_select_candidate(int nargs, * 1) check for possible interpretation as a type coercion request * 2) apply the ambiguous-function resolution rules * - * Return values *funcid through *true_typeids receive info about the function. - * If argdefaults isn't NULL, *argdefaults receives a list of any default - * argument expressions that need to be added to the given arguments. + * If there is no match at all, we return FUNCDETAIL_NOTFOUND, and *fgc_flags + * is filled with some flags that may be useful for issuing an on-point error + * message (see FuncnameGetCandidates). + * + * On success, return values *funcid through *true_typeids receive info about + * the function. If argdefaults isn't NULL, *argdefaults receives a list of + * any default argument expressions that need to be added to the given + * arguments. * * When processing a named- or mixed-notation call (ie, fargnames isn't NIL), * the returned true_typeids and argdefaults are ordered according to the @@ -1400,6 +1518,7 @@ func_get_detail(List *funcname, bool expand_variadic, bool expand_defaults, bool include_out_arguments, + int *fgc_flags, /* return value */ Oid *funcid, /* return value */ Oid *rettype, /* return value */ bool *retset, /* return value */ @@ -1424,7 +1543,8 @@ func_get_detail(List *funcname, /* Get list of possible candidates from namespace search */ raw_candidates = FuncnameGetCandidates(funcname, nargs, fargnames, expand_variadic, expand_defaults, - include_out_arguments, false); + include_out_arguments, false, + fgc_flags); /* * Quickly check if there is an exact match to the input datatypes (there @@ -1594,7 +1714,10 @@ func_get_detail(List *funcname, */ if (fargnames != NIL && !expand_variadic && nargs > 0 && best_candidate->argnumbers[nargs - 1] != nargs - 1) + { + *fgc_flags |= FGC_VARIADIC_FAIL; return FUNCDETAIL_NOTFOUND; + } *funcid = best_candidate->oid; *nvargs = best_candidate->nvargs; @@ -2053,6 +2176,7 @@ LookupFuncNameInternal(ObjectType objtype, List *funcname, { Oid result = InvalidOid; FuncCandidateList clist; + int fgc_flags; /* NULL argtypes allowed for nullary functions only */ Assert(argtypes != NULL || nargs == 0); @@ -2062,7 +2186,8 @@ LookupFuncNameInternal(ObjectType objtype, List *funcname, /* Get list of candidate objects */ clist = FuncnameGetCandidates(funcname, nargs, NIL, false, false, - include_out_arguments, missing_ok); + include_out_arguments, missing_ok, + &fgc_flags); /* Scan list for a match to the arg types (if specified) and the objtype */ for (; clist != NULL; clist = clist->next) @@ -2658,6 +2783,12 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_PROPGRAPH_PROPERTY: + err = _("set-returning functions are not allowed in property definition expressions"); + break; + case EXPR_KIND_FOR_PORTION: + err = _("set-returning functions are not allowed in FOR PORTION OF expressions"); + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c new file mode 100644 index 0000000000000..f889c8df4e314 --- /dev/null +++ b/src/backend/parser/parse_graphtable.c @@ -0,0 +1,384 @@ +/*------------------------------------------------------------------------- + * + * parse_graphtable.c + * parsing of GRAPH_TABLE + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_graphtable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/genam.h" +#include "access/htup_details.h" +#include "access/table.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_property.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_graphtable.h" +#include "parser/parse_node.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/relcache.h" +#include "utils/syscache.h" + + +/* + * Return human-readable name of the type of graph element pattern in + * GRAPH_TABLE clause, usually for error message purpose. + */ +static const char * +get_gep_kind_name(GraphElementPatternKind gepkind) +{ + switch (gepkind) + { + case VERTEX_PATTERN: + return "vertex"; + case EDGE_PATTERN_LEFT: + return "edge pointing left"; + case EDGE_PATTERN_RIGHT: + return "edge pointing right"; + case EDGE_PATTERN_ANY: + return "edge pointing any direction"; + case PAREN_EXPR: + return "nested path pattern"; + } + + /* + * When a GraphElementPattern is constructed by the parser, it will set a + * value from the GraphElementPatternKind enum. But we may get here if the + * GraphElementPatternKind value stored in a catalog is corrupted. + */ + return "unknown"; +} + +/* + * Transform a property reference. + * + * A property reference is parsed as a ColumnRef of the form: + * .. If is one of the variables bound to an + * element pattern in the graph pattern and can be resolved as a + * property of the property graph, then we return a GraphPropertyRef node + * representing the property reference. If the exists in the graph + * pattern but does not exist in the property graph, we raise an + * error. However, if does not exist in the graph pattern, we return + * NULL to let the caller handle it as some other kind of ColumnRef. The + * variables bound to the element patterns in the graph pattern are expected to + * be collected in the GraphTableParseState. + */ +Node * +transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref) +{ + GraphTableParseState *gpstate = pstate->p_graph_table_pstate; + + if (!gpstate) + return NULL; + + if (list_length(cref->fields) == 2) + { + Node *field1 = linitial(cref->fields); + Node *field2 = lsecond(cref->fields); + char *elvarname; + char *propname; + + if (IsA(field1, A_Star) || IsA(field2, A_Star)) + { + if (pstate->p_expr_kind == EXPR_KIND_SELECT_TARGET) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("\"*\" is not supported here"), + parser_errposition(pstate, cref->location)); + else + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("\"*\" not allowed here"), + parser_errposition(pstate, cref->location)); + } + + elvarname = strVal(field1); + propname = strVal(field2); + + if (list_member(gpstate->variables, field1)) + { + GraphPropertyRef *gpr; + HeapTuple pgptup; + Form_pg_propgraph_property pgpform; + + /* + * If we are transforming expression in an element pattern, + * property references containing only that variable are allowed. + */ + if (gpstate->cur_gep) + { + if (!gpstate->cur_gep->variable || + strcmp(elvarname, gpstate->cur_gep->variable) != 0) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("non-local element variable reference is not supported"), + parser_errposition(pstate, cref->location)); + } + + gpr = makeNode(GraphPropertyRef); + pgptup = SearchSysCache2(PROPGRAPHPROPNAME, ObjectIdGetDatum(gpstate->graphid), CStringGetDatum(propname)); + if (!HeapTupleIsValid(pgptup)) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("property \"%s\" does not exist", propname)); + pgpform = (Form_pg_propgraph_property) GETSTRUCT(pgptup); + + gpr->location = cref->location; + gpr->elvarname = elvarname; + gpr->propid = pgpform->oid; + gpr->typeId = pgpform->pgptypid; + gpr->typmod = pgpform->pgptypmod; + gpr->collation = pgpform->pgpcollation; + + ReleaseSysCache(pgptup); + + return (Node *) gpr; + } + } + + return NULL; +} + +/* + * Transform a label expression. + * + * A label expression is parsed as either a ColumnRef with a single field or a + * label expression like label disjunction. The single field in the ColumnRef is + * treated as a label name and transformed to a GraphLabelRef node. The label + * expression is recursively transformed into an expression tree containing + * GraphLabelRef nodes corresponding to the names of the labels appearing in the + * expression. If any label name cannot be resolved to a label in the property + * graph, an error is raised. + */ +static Node * +transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr) +{ + Node *result; + + if (labelexpr == NULL) + return NULL; + + check_stack_depth(); + + switch (nodeTag(labelexpr)) + { + case T_ColumnRef: + { + ColumnRef *cref = (ColumnRef *) labelexpr; + const char *labelname; + Oid labelid; + GraphLabelRef *lref; + + Assert(list_length(cref->fields) == 1); + labelname = strVal(linitial(cref->fields)); + + labelid = GetSysCacheOid2(PROPGRAPHLABELNAME, Anum_pg_propgraph_label_oid, ObjectIdGetDatum(gpstate->graphid), CStringGetDatum(labelname)); + if (!labelid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("label \"%s\" does not exist in property graph \"%s\"", labelname, get_rel_name(gpstate->graphid))); + + lref = makeNode(GraphLabelRef); + lref->labelid = labelid; + lref->location = cref->location; + + result = (Node *) lref; + break; + } + + case T_BoolExpr: + { + BoolExpr *be = (BoolExpr *) labelexpr; + ListCell *lc; + List *args = NIL; + + foreach(lc, be->args) + { + Node *arg = (Node *) lfirst(lc); + + arg = transformLabelExpr(gpstate, arg); + args = lappend(args, arg); + } + + result = (Node *) makeBoolExpr(be->boolop, args, be->location); + break; + } + + default: + /* should not reach here */ + elog(ERROR, "unsupported label expression node: %d", (int) nodeTag(labelexpr)); + result = NULL; /* keep compiler quiet */ + break; + } + + return result; +} + +/* + * Transform a GraphElementPattern. + * + * Transform the label expression and the where clause in the element pattern + * given by GraphElementPattern. The variable name in the GraphElementPattern is + * added to the list of variables in the GraphTableParseState which is used to + * resolve property references in this element pattern or elsewhere in the + * GRAPH_TABLE. + */ +static Node * +transformGraphElementPattern(ParseState *pstate, GraphElementPattern *gep) +{ + GraphTableParseState *gpstate = pstate->p_graph_table_pstate; + + if (gep->quantifier) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("element pattern quantifier is not supported"))); + + Assert(!gpstate->cur_gep); + + gpstate->cur_gep = gep; + + gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr); + + gep->whereClause = transformExpr(pstate, gep->whereClause, EXPR_KIND_WHERE); + assign_expr_collations(pstate, gep->whereClause); + + gpstate->cur_gep = NULL; + + return (Node *) gep; +} + +/* + * Transform a path term (list of GraphElementPattern's). + */ +static Node * +transformPathTerm(ParseState *pstate, List *path_term) +{ + List *result = NIL; + GraphElementPattern *prev_gep = NULL; + + foreach_node(GraphElementPattern, gep, path_term) + { + if (gep->kind != VERTEX_PATTERN && !IS_EDGE_PATTERN(gep->kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("unsupported element pattern kind: \"%s\"", get_gep_kind_name(gep->kind)), + parser_errposition(pstate, gep->location))); + + if (IS_EDGE_PATTERN(gep->kind)) + { + if (!prev_gep) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("path pattern cannot start with an edge pattern"), + parser_errposition(pstate, gep->location))); + else if (prev_gep->kind != VERTEX_PATTERN) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("edge pattern must be preceded by a vertex pattern"), + parser_errposition(pstate, gep->location))); + } + else + { + if (prev_gep && !IS_EDGE_PATTERN(prev_gep->kind)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("adjacent vertex patterns are not supported"), + parser_errposition(pstate, gep->location))); + } + + result = lappend(result, + transformGraphElementPattern(pstate, gep)); + prev_gep = gep; + } + + /* Path pattern should have at least one element pattern. */ + Assert(prev_gep); + + if (IS_EDGE_PATTERN(prev_gep->kind)) + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("path pattern cannot end with an edge pattern"), + parser_errposition(pstate, prev_gep->location))); + } + + return (Node *) result; +} + +/* + * Transform a path pattern list (list of path terms). + */ +static Node * +transformPathPatternList(ParseState *pstate, List *path_pattern) +{ + List *result = NIL; + GraphTableParseState *gpstate = pstate->p_graph_table_pstate; + + Assert(gpstate); + + /* Grammar doesn't allow empty path pattern list */ + Assert(list_length(path_pattern) > 0); + + /* + * We do not support multiple path patterns in one GRAPH_TABLE clause + * right now. But we may do so in future. + */ + if (list_length(path_pattern) != 1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("multiple path patterns in one GRAPH_TABLE clause not supported"))); + + /* + * Collect all the variables in the path pattern into the + * GraphTableParseState so that we can detect any non-local element + * variable references. We need to do this before transforming the path + * pattern so as to detect forward references to element variables in the + * WHERE clause of an element pattern. + */ + foreach_node(List, path_term, path_pattern) + { + foreach_node(GraphElementPattern, gep, path_term) + { + if (gep->variable) + gpstate->variables = list_append_unique(gpstate->variables, makeString(pstrdup(gep->variable))); + } + } + + foreach_node(List, path_term, path_pattern) + result = lappend(result, transformPathTerm(pstate, path_term)); + + return (Node *) result; +} + +/* + * Transform a GraphPattern. + * + * A GraphPattern consists of a list of one or more path patterns and an + * optional where clause. Transform them. We use the previously constructure + * list of variables in the GraphTableParseState to resolve property references + * in the WHERE clause. + */ +Node * +transformGraphPattern(ParseState *pstate, GraphPattern *graph_pattern) +{ + List *path_pattern_list = castNode(List, + transformPathPatternList(pstate, graph_pattern->path_pattern_list)); + + graph_pattern->path_pattern_list = path_pattern_list; + graph_pattern->whereClause = transformExpr(pstate, graph_pattern->whereClause, EXPR_KIND_WHERE); + assign_expr_collations(pstate, graph_pattern->whereClause); + + return (Node *) graph_pattern; +} diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c index 13d533b83f37a..32a1e8629b209 100644 --- a/src/backend/parser/parse_jsontable.c +++ b/src/backend/parser/parse_jsontable.c @@ -3,7 +3,7 @@ * parse_jsontable.c * parsing of JSON_TABLE * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -312,7 +312,7 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, rawc->wrapper != JSW_UNSPEC) rawc->coltype = JTC_FORMATTED; - /* FALLTHROUGH */ + pg_fallthrough; case JTC_FORMATTED: case JTC_EXISTS: { diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 51d7703eff7e0..2e6dd166c9860 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -3,7 +3,7 @@ * parse_merge.c * handle merge-statement in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -307,8 +307,6 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) List *icolumns; List *attrnos; - pstate->p_is_insert = true; - icolumns = checkInsertTargets(pstate, mergeWhenClause->targetList, &attrnos); @@ -381,12 +379,9 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) } break; case CMD_UPDATE: - { - pstate->p_is_insert = false; - action->targetList = - transformUpdateTargetList(pstate, - mergeWhenClause->targetList); - } + action->targetList = + transformUpdateTargetList(pstate, + mergeWhenClause->targetList, NULL); break; case CMD_DELETE: break; diff --git a/src/backend/parser/parse_node.c b/src/backend/parser/parse_node.c index d6feb16aef375..dacd851cffd15 100644 --- a/src/backend/parser/parse_node.c +++ b/src/backend/parser/parse_node.c @@ -3,7 +3,7 @@ * parse_node.c * various routines that make nodes for querytrees * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -40,7 +40,7 @@ make_parsestate(ParseState *parentParseState) { ParseState *pstate; - pstate = palloc0(sizeof(ParseState)); + pstate = palloc0_object(ParseState); pstate->parentParseState = parentParseState; @@ -408,7 +408,7 @@ make_const(ParseState *pstate, A_Const *aconst) typeid = INT8OID; typelen = sizeof(int64); - typebyval = FLOAT8PASSBYVAL; /* int8 and float8 alike */ + typebyval = true; } } else diff --git a/src/backend/parser/parse_oper.c b/src/backend/parser/parse_oper.c index 0c4337563cf35..2f218c1ab8b97 100644 --- a/src/backend/parser/parse_oper.c +++ b/src/backend/parser/parse_oper.c @@ -3,7 +3,7 @@ * parse_oper.c * handle operator things for parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "parser/parse_oper.h" #include "parser/parse_type.h" #include "utils/builtins.h" +#include "utils/hsearch.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/syscache.h" @@ -72,13 +73,15 @@ static FuncDetailCode oper_select_candidate(int nargs, Oid *operOid); static void op_error(ParseState *pstate, List *op, Oid arg1, Oid arg2, - FuncDetailCode fdresult, int location); + FuncDetailCode fdresult, int fgc_flags, int location); +static int oper_lookup_failure_details(int fgc_flags, bool is_unary_op); static bool make_oper_cache_key(ParseState *pstate, OprCacheKey *key, List *opname, Oid ltypeId, Oid rtypeId, int location); static Oid find_oper_cache_entry(OprCacheKey *key); static void make_oper_cache_entry(OprCacheKey *key, Oid opr_oid); -static void InvalidateOprCacheCallBack(Datum arg, int cacheid, uint32 hashvalue); +static void InvalidateOprCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); /* @@ -373,6 +376,7 @@ oper(ParseState *pstate, List *opname, Oid ltypeId, Oid rtypeId, Oid operOid; OprCacheKey key; bool key_ok; + int fgc_flags = 0; FuncDetailCode fdresult = FUNCDETAIL_NOTFOUND; HeapTuple tup = NULL; @@ -404,7 +408,7 @@ oper(ParseState *pstate, List *opname, Oid ltypeId, Oid rtypeId, FuncCandidateList clist; /* Get binary operators of given name */ - clist = OpernameGetCandidates(opname, 'b', false); + clist = OpernameGetCandidates(opname, 'b', false, &fgc_flags); /* No operators found? Then fail... */ if (clist != NULL) @@ -434,7 +438,8 @@ oper(ParseState *pstate, List *opname, Oid ltypeId, Oid rtypeId, make_oper_cache_entry(&key, operOid); } else if (!noError) - op_error(pstate, opname, ltypeId, rtypeId, fdresult, location); + op_error(pstate, opname, ltypeId, rtypeId, + fdresult, fgc_flags, location); return (Operator) tup; } @@ -520,6 +525,7 @@ left_oper(ParseState *pstate, List *op, Oid arg, bool noError, int location) Oid operOid; OprCacheKey key; bool key_ok; + int fgc_flags = 0; FuncDetailCode fdresult = FUNCDETAIL_NOTFOUND; HeapTuple tup = NULL; @@ -551,7 +557,7 @@ left_oper(ParseState *pstate, List *op, Oid arg, bool noError, int location) FuncCandidateList clist; /* Get prefix operators of given name */ - clist = OpernameGetCandidates(op, 'l', false); + clist = OpernameGetCandidates(op, 'l', false, &fgc_flags); /* No operators found? Then fail... */ if (clist != NULL) @@ -585,7 +591,8 @@ left_oper(ParseState *pstate, List *op, Oid arg, bool noError, int location) make_oper_cache_entry(&key, operOid); } else if (!noError) - op_error(pstate, op, InvalidOid, arg, fdresult, location); + op_error(pstate, op, InvalidOid, arg, + fdresult, fgc_flags, location); return (Operator) tup; } @@ -621,29 +628,67 @@ op_signature_string(List *op, Oid arg1, Oid arg2) static void op_error(ParseState *pstate, List *op, Oid arg1, Oid arg2, - FuncDetailCode fdresult, int location) + FuncDetailCode fdresult, int fgc_flags, int location) { if (fdresult == FUNCDETAIL_MULTIPLE) ereport(ERROR, (errcode(ERRCODE_AMBIGUOUS_FUNCTION), errmsg("operator is not unique: %s", op_signature_string(op, arg1, arg2)), - errhint("Could not choose a best candidate operator. " - "You might need to add explicit type casts."), + errdetail("Could not choose a best candidate operator."), + errhint("You might need to add explicit type casts."), parser_errposition(pstate, location))); else ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("operator does not exist: %s", op_signature_string(op, arg1, arg2)), - (!arg1 || !arg2) ? - errhint("No operator matches the given name and argument type. " - "You might need to add an explicit type cast.") : - errhint("No operator matches the given name and argument types. " - "You might need to add explicit type casts."), + oper_lookup_failure_details(fgc_flags, (!arg1 || !arg2)), parser_errposition(pstate, location))); } +/* + * Interpret the fgc_flags and issue a suitable detail or hint message. + */ +static int +oper_lookup_failure_details(int fgc_flags, bool is_unary_op) +{ + /* + * If not FGC_NAME_VISIBLE, we shouldn't raise the question of whether the + * arguments are wrong. If the operator name was not schema-qualified, + * it's helpful to distinguish between doesn't-exist-anywhere and + * not-in-search-path; but if it was, there's really nothing to add to the + * basic "operator does not exist" message. + * + * Note: we passed missing_ok = false to OpernameGetCandidates, so there's + * no need to consider FGC_SCHEMA_EXISTS here: we'd have already thrown an + * error if an explicitly-given schema doesn't exist. + */ + if (!(fgc_flags & FGC_NAME_VISIBLE)) + { + if (fgc_flags & FGC_SCHEMA_GIVEN) + return 0; /* schema-qualified name */ + else if (!(fgc_flags & FGC_NAME_EXISTS)) + return errdetail("There is no operator of that name."); + else + return errdetail("An operator of that name exists, but it is not in the search_path."); + } + + /* + * Otherwise, the problem must be incorrect argument type(s). + */ + if (is_unary_op) + { + (void) errdetail("No operator of that name accepts the given argument type."); + return errhint("You might need to add an explicit type cast."); + } + else + { + (void) errdetail("No operator of that name accepts the given argument types."); + return errhint("You might need to add explicit type casts."); + } +} + /* * make_op() * Operator expression construction. @@ -1033,7 +1078,8 @@ make_oper_cache_entry(OprCacheKey *key, Oid opr_oid) * Callback for pg_operator and pg_cast inval events */ static void -InvalidateOprCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) +InvalidateOprCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { HASH_SEQ_STATUS status; OprCacheEntry *hentry; diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c index 930921626b6d5..946950dbbda22 100644 --- a/src/backend/parser/parse_param.c +++ b/src/backend/parser/parse_param.c @@ -12,7 +12,7 @@ * Note that other approaches to parameters are possible using the parser * hooks defined in ParseState. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -68,7 +68,7 @@ void setup_parse_fixed_parameters(ParseState *pstate, const Oid *paramTypes, int numParams) { - FixedParamState *parstate = palloc(sizeof(FixedParamState)); + FixedParamState *parstate = palloc_object(FixedParamState); parstate->paramTypes = paramTypes; parstate->numParams = numParams; @@ -84,7 +84,7 @@ void setup_parse_variable_parameters(ParseState *pstate, Oid **paramTypes, int *numParams) { - VarParamState *parstate = palloc(sizeof(VarParamState)); + VarParamState *parstate = palloc_object(VarParamState); parstate->paramTypes = paramTypes; parstate->numParams = numParams; diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 04ecf64b1fc25..ffd1fdab7a096 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -3,7 +3,7 @@ * parse_relation.c * parser support routines dealing with relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,11 +18,9 @@ #include "access/htup_details.h" #include "access/relation.h" -#include "access/sysattr.h" #include "access/table.h" #include "catalog/heap.h" #include "catalog/namespace.h" -#include "catalog/pg_type.h" #include "funcapi.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -33,7 +31,6 @@ #include "storage/lmgr.h" #include "utils/builtins.h" #include "utils/lsyscache.h" -#include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" @@ -103,7 +100,6 @@ static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, static int specialAttNum(const char *attname); static bool rte_visible_if_lateral(ParseState *pstate, RangeTblEntry *rte); static bool rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte); -static bool isQueryUsingTempRelation_walker(Node *node, void *context); /* @@ -968,7 +964,7 @@ searchRangeTableForCol(ParseState *pstate, const char *alias, const char *colnam int location) { ParseState *orig_pstate = pstate; - FuzzyAttrMatchState *fuzzystate = palloc(sizeof(FuzzyAttrMatchState)); + FuzzyAttrMatchState *fuzzystate = palloc_object(FuzzyAttrMatchState); fuzzystate->distance = MAX_FUZZY_DISTANCE + 1; fuzzystate->rfirst = NULL; @@ -1340,7 +1336,7 @@ buildNSItemFromTupleDesc(RangeTblEntry *rte, Index rtindex, } /* ... and build the nsitem */ - nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem)); + nsitem = palloc_object(ParseNamespaceItem); nsitem->p_names = rte->eref; nsitem->p_rte = rte; nsitem->p_rtindex = rtindex; @@ -1404,7 +1400,7 @@ buildNSItemFromLists(RangeTblEntry *rte, Index rtindex, } /* ... and build the nsitem */ - nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem)); + nsitem = palloc_object(ParseNamespaceItem); nsitem->p_names = rte->eref; nsitem->p_rte = rte; nsitem->p_rtindex = rtindex; @@ -1426,13 +1422,9 @@ buildNSItemFromLists(RangeTblEntry *rte, Index rtindex, * This is essentially just the same as table_openrv(), except that it caters * to some parser-specific error reporting needs, notably that it arranges * to include the RangeVar's parse location in any resulting error. - * - * Note: properly, lockmode should be declared LOCKMODE not int, but that - * would require importing storage/lock.h into parse_relation.h. Since - * LOCKMODE is typedef'd as int anyway, that seems like overkill. */ Relation -parserOpenTable(ParseState *pstate, const RangeVar *relation, int lockmode) +parserOpenTable(ParseState *pstate, const RangeVar *relation, LOCKMODE lockmode) { Relation rel; ParseCallbackState pcbstate; @@ -1575,15 +1567,11 @@ addRangeTableEntry(ParseState *pstate, * of AccessShareLock, RowShareLock, or RowExclusiveLock depending on the * RTE's role within the query. The caller must hold that lock mode * or a stronger one. - * - * Note: properly, lockmode should be declared LOCKMODE not int, but that - * would require importing storage/lock.h into parse_relation.h. Since - * LOCKMODE is typedef'd as int anyway, that seems like overkill. */ ParseNamespaceItem * addRangeTableEntryForRelation(ParseState *pstate, Relation rel, - int lockmode, + LOCKMODE lockmode, Alias *alias, bool inh, bool inFromCl) @@ -1795,7 +1783,7 @@ addRangeTableEntryForFunction(ParseState *pstate, rte->eref = eref; /* Process each function ... */ - functupdescs = (TupleDesc *) palloc(nfuncs * sizeof(TupleDesc)); + functupdescs = palloc_array(TupleDesc, nfuncs); totalatts = 0; funcno = 0; @@ -1895,6 +1883,7 @@ addRangeTableEntryForFunction(ParseState *pstate, TupleDescInitEntryCollation(tupdesc, (AttrNumber) 1, exprCollation(funcexpr)); + TupleDescFinalize(tupdesc); } else if (functypclass == TYPEFUNC_RECORD) { @@ -1952,6 +1941,7 @@ addRangeTableEntryForFunction(ParseState *pstate, i++; } + TupleDescFinalize(tupdesc); /* * Ensure that the coldeflist defines a legal set of names (no @@ -2020,7 +2010,7 @@ addRangeTableEntryForFunction(ParseState *pstate, 0); /* no need to set collation */ } - + TupleDescFinalize(tupdesc); Assert(natts == totalatts); } else @@ -2141,6 +2131,99 @@ addRangeTableEntryForTableFunc(ParseState *pstate, rte->colcollations); } +ParseNamespaceItem * +addRangeTableEntryForGraphTable(ParseState *pstate, + Oid graphid, + GraphPattern *graph_pattern, + List *columns, + List *colnames, + Alias *alias, + bool lateral, + bool inFromCl) +{ + RangeTblEntry *rte = makeNode(RangeTblEntry); + char *refname = alias ? alias->aliasname : pstrdup("graph_table"); + Alias *eref; + int numaliases; + int varattno; + ListCell *lc; + List *coltypes = NIL; + List *coltypmods = NIL; + List *colcollations = NIL; + RTEPermissionInfo *perminfo; + ParseNamespaceItem *nsitem; + + Assert(pstate != NULL); + + rte->rtekind = RTE_GRAPH_TABLE; + rte->relid = graphid; + rte->relkind = RELKIND_PROPGRAPH; + rte->graph_pattern = graph_pattern; + rte->graph_table_columns = columns; + rte->alias = alias; + rte->rellockmode = AccessShareLock; + + eref = alias ? copyObject(alias) : makeAlias(refname, NIL); + + if (!eref->colnames) + eref->colnames = colnames; + + numaliases = list_length(eref->colnames); + + /* fill in any unspecified alias columns */ + varattno = 0; + foreach(lc, colnames) + { + varattno++; + if (varattno > numaliases) + eref->colnames = lappend(eref->colnames, lfirst(lc)); + } + if (varattno < numaliases) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("GRAPH_TABLE \"%s\" has %d columns available but %d columns specified", + refname, varattno, numaliases))); + + rte->eref = eref; + + foreach(lc, columns) + { + TargetEntry *te = lfirst_node(TargetEntry, lc); + Node *colexpr = (Node *) te->expr; + + coltypes = lappend_oid(coltypes, exprType(colexpr)); + coltypmods = lappend_int(coltypmods, exprTypmod(colexpr)); + colcollations = lappend_oid(colcollations, exprCollation(colexpr)); + } + + /* + * Set flags and access permissions. + */ + rte->lateral = lateral; + rte->inFromCl = inFromCl; + + perminfo = addRTEPermissionInfo(&pstate->p_rteperminfos, rte); + perminfo->requiredPerms = ACL_SELECT; + + /* + * Add completed RTE to pstate's range table list, so that we know its + * index. But we don't add it to the join list --- caller must do that if + * appropriate. + */ + pstate->p_rtable = lappend(pstate->p_rtable, rte); + + /* + * Build a ParseNamespaceItem, but don't add it to the pstate's namespace + * list --- caller must do that if appropriate. + */ + nsitem = buildNSItemFromLists(rte, list_length(pstate->p_rtable), + coltypes, coltypmods, colcollations); + + nsitem->p_perminfo = perminfo; + + return nsitem; +} + /* * Add an entry for a VALUES list to the pstate's range table (p_rtable). * Then, construct and return a ParseNamespaceItem for the new RTE. @@ -2306,7 +2389,7 @@ addRangeTableEntryForJoin(ParseState *pstate, * Build a ParseNamespaceItem, but don't add it to the pstate's namespace * list --- caller must do that if appropriate. */ - nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem)); + nsitem = palloc_object(ParseNamespaceItem); nsitem->p_names = rte->eref; nsitem->p_rte = rte; nsitem->p_perminfo = NULL; @@ -3039,6 +3122,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, case RTE_VALUES: case RTE_CTE: case RTE_NAMEDTUPLESTORE: + case RTE_GRAPH_TABLE: { /* Tablefunc, Values, CTE, or ENR RTE */ ListCell *aliasp_item = list_head(rte->eref->colnames); @@ -3423,10 +3507,11 @@ get_rte_attribute_is_dropped(RangeTblEntry *rte, AttrNumber attnum) case RTE_VALUES: case RTE_CTE: case RTE_GROUP: + case RTE_GRAPH_TABLE: /* - * Subselect, Table Functions, Values, CTE, GROUP RTEs never have - * dropped columns + * Subselect, Table Functions, Values, CTE, GROUP RTEs, Property + * graph references never have dropped columns */ result = false; break; @@ -3489,13 +3574,13 @@ get_rte_attribute_is_dropped(RangeTblEntry *rte, AttrNumber attnum) if (tupdesc) { /* Composite data type, e.g. a table's row type */ - Form_pg_attribute att_tup; + CompactAttribute *att; Assert(tupdesc); Assert(attnum - atts_done <= tupdesc->natts); - att_tup = TupleDescAttr(tupdesc, - attnum - atts_done - 1); - return att_tup->attisdropped; + att = TupleDescCompactAttr(tupdesc, + attnum - atts_done - 1); + return att->attisdropped; } /* Otherwise, it can't have any dropped columns */ return false; @@ -3922,53 +4007,6 @@ rte_visible_if_qualified(ParseState *pstate, RangeTblEntry *rte) } -/* - * Examine a fully-parsed query, and return true iff any relation underlying - * the query is a temporary relation (table, view, or materialized view). - */ -bool -isQueryUsingTempRelation(Query *query) -{ - return isQueryUsingTempRelation_walker((Node *) query, NULL); -} - -static bool -isQueryUsingTempRelation_walker(Node *node, void *context) -{ - if (node == NULL) - return false; - - if (IsA(node, Query)) - { - Query *query = (Query *) node; - ListCell *rtable; - - foreach(rtable, query->rtable) - { - RangeTblEntry *rte = lfirst(rtable); - - if (rte->rtekind == RTE_RELATION) - { - Relation rel = table_open(rte->relid, AccessShareLock); - char relpersistence = rel->rd_rel->relpersistence; - - table_close(rel, AccessShareLock); - if (relpersistence == RELPERSISTENCE_TEMP) - return true; - } - } - - return query_tree_walker(query, - isQueryUsingTempRelation_walker, - context, - QTW_IGNORE_JOINALIASES); - } - - return expression_tree_walker(node, - isQueryUsingTempRelation_walker, - context); -} - /* * addRTEPermissionInfo * Creates RTEPermissionInfo for a given RTE and adds it into the diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 4aba0d9d4d5cc..541fef5f18385 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -3,7 +3,7 @@ * parse_target.c * handle target lists * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,7 +16,6 @@ #include "catalog/namespace.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "funcapi.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -360,6 +359,10 @@ markTargetListOrigin(ParseState *pstate, TargetEntry *tle, tle->resorigtbl = rte->relid; tle->resorigcol = attnum; break; + case RTE_GRAPH_TABLE: + tle->resorigtbl = rte->relid; + tle->resorigcol = InvalidAttrNumber; + break; case RTE_SUBQUERY: /* Subselect-in-FROM: copy up from the subselect */ if (attnum != InvalidAttrNumber) @@ -439,6 +442,7 @@ markTargetListOrigin(ParseState *pstate, TargetEntry *tle, * pstate parse state * expr expression to be modified * exprKind indicates which type of statement we're dealing with + * (EXPR_KIND_INSERT_TARGET or EXPR_KIND_UPDATE_TARGET) * colname target column name (ie, name of attribute to be assigned to) * attrno target attribute number * indirection subscripts/field names for target column, if any @@ -472,7 +476,8 @@ transformAssignedExpr(ParseState *pstate, * set p_expr_kind here because we can parse subscripts without going * through transformExpr(). */ - Assert(exprKind != EXPR_KIND_NONE); + Assert(exprKind == EXPR_KIND_INSERT_TARGET || + exprKind == EXPR_KIND_UPDATE_TARGET); sv_expr_kind = pstate->p_expr_kind; pstate->p_expr_kind = exprKind; @@ -531,7 +536,7 @@ transformAssignedExpr(ParseState *pstate, { Node *colVar; - if (pstate->p_is_insert) + if (exprKind == EXPR_KIND_INSERT_TARGET) { /* * The command is INSERT INTO table (col.something) ... so there @@ -1571,6 +1576,8 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup) } Assert(lname == NULL && lvar == NULL); /* lists same length? */ + TupleDescFinalize(tupleDesc); + return tupleDesc; } @@ -1581,6 +1588,7 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup) case RTE_RELATION: case RTE_VALUES: case RTE_NAMEDTUPLESTORE: + case RTE_GRAPH_TABLE: case RTE_RESULT: /* @@ -1610,10 +1618,9 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup) * subselect must have that outer level as parent. */ ParseState mypstate = {0}; - Index levelsup; /* this loop must work, since GetRTEByRangeTablePosn did */ - for (levelsup = 0; levelsup < netlevelsup; levelsup++) + for (Index level = 0; level < netlevelsup; level++) pstate = pstate->parentParseState; mypstate.parentParseState = pstate; mypstate.p_rtable = rte->subquery->rtable; @@ -1668,12 +1675,11 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup) * could be an outer CTE (compare SUBQUERY case above). */ ParseState mypstate = {0}; - Index levelsup; /* this loop must work, since GetCTEForRTE did */ - for (levelsup = 0; - levelsup < rte->ctelevelsup + netlevelsup; - levelsup++) + for (Index level = 0; + level < rte->ctelevelsup + netlevelsup; + level++) pstate = pstate->parentParseState; mypstate.parentParseState = pstate; mypstate.p_rtable = ((Query *) cte->ctequery)->rtable; diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c index 7713bdc6af0a9..bb7eccde9fdd3 100644 --- a/src/backend/parser/parse_type.c +++ b/src/backend/parser/parse_type.c @@ -3,7 +3,7 @@ * parse_type.c * handle type operations for parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -369,7 +369,7 @@ typenameTypeMod(ParseState *pstate, const TypeName *typeName, Type typ) * Currently, we allow simple numeric constants, string literals, and * identifiers; possibly this list could be extended. */ - datums = (Datum *) palloc(list_length(typeName->typmods) * sizeof(Datum)); + datums = palloc_array(Datum, list_length(typeName->typmods)); n = 0; foreach(l, typeName->typmods) { @@ -382,7 +382,7 @@ typenameTypeMod(ParseState *pstate, const TypeName *typeName, Type typ) if (IsA(&ac->val, Integer)) { - cstr = psprintf("%ld", (long) intVal(&ac->val)); + cstr = psprintf("%d", intVal(&ac->val)); } else if (IsA(&ac->val, Float)) { diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 62015431fdf1a..37071502a9f6e 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -12,7 +12,7 @@ * respective utility commands. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/parser/parse_utilcmd.c @@ -23,6 +23,7 @@ #include "postgres.h" #include "access/amapi.h" +#include "access/attmap.h" #include "access/htup_details.h" #include "access/relation.h" #include "access/reloptions.h" @@ -32,6 +33,7 @@ #include "catalog/heap.h" #include "catalog/index.h" #include "catalog/namespace.h" +#include "catalog/partition.h" #include "catalog/pg_am.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" @@ -58,6 +60,8 @@ #include "parser/parse_type.h" #include "parser/parse_utilcmd.h" #include "parser/parser.h" +#include "partitioning/partbounds.h" +#include "partitioning/partdesc.h" #include "rewrite/rewriteManip.h" #include "utils/acl.h" #include "utils/builtins.h" @@ -95,18 +99,6 @@ typedef struct bool ofType; /* true if statement contains OF typename */ } CreateStmtContext; -/* State shared by transformCreateSchemaStmtElements and its subroutines */ -typedef struct -{ - const char *schemaname; /* name of schema */ - List *sequences; /* CREATE SEQUENCE items */ - List *tables; /* CREATE TABLE items */ - List *views; /* CREATE VIEW items */ - List *indexes; /* CREATE INDEX items */ - List *triggers; /* CREATE TRIGGER items */ - List *grants; /* GRANT items */ -} CreateSchemaStmtContext; - static void transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column); @@ -130,11 +122,17 @@ static void transformFKConstraints(CreateStmtContext *cxt, bool isAddConstraint); static void transformCheckConstraints(CreateStmtContext *cxt, bool skipValidation); -static void transformConstraintAttrs(CreateStmtContext *cxt, +static void transformConstraintAttrs(ParseState *pstate, List *constraintList); static void transformColumnType(CreateStmtContext *cxt, ColumnDef *column); -static void setSchemaName(const char *context_schema, char **stmt_schema_name); -static void transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd); +static void checkSchemaNameRV(ParseState *pstate, const char *context_schema, + RangeVar *relation); +static void checkSchemaNameList(const char *context_schema, + List *qualified_name); +static CreateStmt *transformCreateSchemaCreateTable(ParseState *pstate, + CreateStmt *stmt, + List **fk_elements); +static void transformPartitionCmd(CreateStmtContext *cxt, PartitionBoundSpec *bound); static List *transformPartitionRangeBounds(ParseState *pstate, List *blist, Relation parent); static void validateInfiniteBounds(ParseState *pstate, List *blist); @@ -700,7 +698,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) } /* Process column constraints, if any... */ - transformConstraintAttrs(cxt, column->constraints); + transformConstraintAttrs(cxt->pstate, column->constraints); /* * First, scan the column's constraints to see if a not-null constraint @@ -915,7 +913,7 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) errmsg("primary key constraints are not supported on foreign tables"), parser_errposition(cxt->pstate, constraint->location))); - /* FALL THRU */ + pg_fallthrough; case CONSTR_UNIQUE: if (cxt->isforeign) @@ -1279,6 +1277,28 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false, true); cxt->nnconstraints = list_concat(cxt->nnconstraints, lst); + + /* Copy comments on not-null constraints */ + if (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS) + { + foreach_node(Constraint, nnconstr, lst) + { + if ((comment = GetComment(get_relation_constraint_oid(RelationGetRelid(relation), + nnconstr->conname, false), + ConstraintRelationId, + 0)) != NULL) + { + CommentStmt *stmt = makeNode(CommentStmt); + + stmt->objtype = OBJECT_TABCONSTRAINT; + stmt->object = (Node *) list_make3(makeString(cxt->relation->schemaname), + makeString(cxt->relation->relname), + makeString(nnconstr->conname)); + stmt->comment = comment; + cxt->alist = lappend(cxt->alist, stmt); + } + } + } } /* @@ -1439,7 +1459,6 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause) char *ccname = constr->check[ccnum].ccname; char *ccbin = constr->check[ccnum].ccbin; bool ccenforced = constr->check[ccnum].ccenforced; - bool ccvalid = constr->check[ccnum].ccvalid; bool ccnoinherit = constr->check[ccnum].ccnoinherit; Node *ccbin_node; bool found_whole_row; @@ -1470,7 +1489,7 @@ expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause) n->conname = pstrdup(ccname); n->location = -1; n->is_enforced = ccenforced; - n->initially_valid = ccvalid; + n->initially_valid = ccenforced; /* sic */ n->is_no_inherit = ccnoinherit; n->raw_expr = NULL; n->cooked_expr = nodeToString(ccbin_node); @@ -1938,6 +1957,8 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, } } + iparam->location = -1; + index->indexParams = lappend(index->indexParams, iparam); } @@ -1969,6 +1990,8 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, /* Copy the original index column name */ iparam->indexcolname = pstrdup(NameStr(attr->attname)); + iparam->location = -1; + index->indexIncludingParams = lappend(index->indexIncludingParams, iparam); } /* Copy reloptions if any */ @@ -2548,7 +2571,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) } /* Close the index relation but keep the lock */ - relation_close(index_rel, NoLock); + index_close(index_rel, NoLock); index->indexOid = index_oid; } @@ -2737,7 +2760,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) /* * The WITHOUT OVERLAPS part (if any) must be a range or - * multirange type. + * multirange type, or a domain over such a type. */ if (constraint->without_overlaps && lc == list_last_cell(constraint->keys)) { @@ -2755,8 +2778,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) const char *attname; if (attr->attisdropped) - break; - + continue; attname = NameStr(attr->attname); if (strcmp(attname, key) == 0) { @@ -2768,10 +2790,16 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) } if (found) { + /* Look up column type if we didn't already */ if (!OidIsValid(typid) && column) - typid = typenameTypeId(NULL, column->typeName); - - if (!OidIsValid(typid) || !(type_is_range(typid) || type_is_multirange(typid))) + typid = typenameTypeId(cxt->pstate, + column->typeName); + /* Look through any domain */ + if (OidIsValid(typid)) + typid = getBaseType(typid); + /* Complain if not range/multirange */ + if (!OidIsValid(typid) || + !(type_is_range(typid) || type_is_multirange(typid))) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range or multirange type", key), @@ -2789,6 +2817,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) iparam->opclassopts = NIL; iparam->ordering = SORTBY_DEFAULT; iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + iparam->location = -1; index->indexParams = lappend(index->indexParams, iparam); } @@ -2905,6 +2934,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) iparam->collation = NIL; iparam->opclass = NIL; iparam->opclassopts = NIL; + iparam->location = -1; index->indexIncludingParams = lappend(index->indexIncludingParams, iparam); } @@ -3488,6 +3518,288 @@ transformRuleStmt(RuleStmt *stmt, const char *queryString, } +/* + * checkPartition + * Check whether partRelOid is a leaf partition of the parent table (rel). + * isMerge: true indicates the operation is "ALTER TABLE ... MERGE PARTITIONS"; + * false indicates the operation is "ALTER TABLE ... SPLIT PARTITION". + */ +static void +checkPartition(Relation rel, Oid partRelOid, bool isMerge) +{ + Relation partRel; + + partRel = table_open(partRelOid, NoLock); + + if (partRel->rd_rel->relkind != RELKIND_RELATION) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table", RelationGetRelationName(partRel)), + isMerge + ? errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions.") + : errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions.")); + + if (!partRel->rd_rel->relispartition) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a partition of partitioned table \"%s\"", + RelationGetRelationName(partRel), RelationGetRelationName(rel)), + isMerge + ? errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions.") + : errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions.")); + + if (get_partition_parent(partRelOid, false) != RelationGetRelid(rel)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("relation \"%s\" is not a partition of relation \"%s\"", + RelationGetRelationName(partRel), RelationGetRelationName(rel)), + isMerge + ? errhint("ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions.") + : errhint("ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions.")); + + table_close(partRel, NoLock); +} + +/* + * transformPartitionCmdForSplit - + * analyze the ALTER TABLE ... SPLIT PARTITION command + * + * For each new partition, sps->bound is set to the transformed value of bound. + * Does checks for bounds of new partitions. + */ +static void +transformPartitionCmdForSplit(CreateStmtContext *cxt, PartitionCmd *partcmd) +{ + Relation parent = cxt->rel; + PartitionKey key; + char strategy; + Oid splitPartOid; + Oid defaultPartOid; + int default_index = -1; + bool isSplitPartDefault; + ListCell *listptr, + *listptr2; + List *splitlist; + + splitlist = partcmd->partlist; + key = RelationGetPartitionKey(parent); + strategy = get_partition_strategy(key); + defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true)); + + /* Transform partition bounds for all partitions in the list: */ + foreach_node(SinglePartitionSpec, sps, splitlist) + { + cxt->partbound = NULL; + transformPartitionCmd(cxt, sps->bound); + /* Assign the transformed value of the partition bound. */ + sps->bound = cxt->partbound; + } + + /* + * Open and lock the partition, check ownership along the way. We need to + * use AccessExclusiveLock here because this split partition will be + * detached, then dropped in ATExecSplitPartition. + */ + splitPartOid = RangeVarGetRelidExtended(partcmd->name, AccessExclusiveLock, + 0, RangeVarCallbackOwnsRelation, + NULL); + + checkPartition(parent, splitPartOid, false); + + switch (strategy) + { + case PARTITION_STRATEGY_LIST: + case PARTITION_STRATEGY_RANGE: + { + foreach_node(SinglePartitionSpec, sps, splitlist) + { + if (sps->bound->is_default) + { + if (default_index != -1) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("DEFAULT partition should be one"), + parser_errposition(cxt->pstate, sps->name->location)); + + default_index = foreach_current_index(sps); + } + } + } + break; + + case PARTITION_STRATEGY_HASH: + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partition of hash-partitioned table cannot be split")); + break; + + default: + elog(ERROR, "unexpected partition strategy: %d", + (int) key->strategy); + break; + } + + /* isSplitPartDefault: is the being split partition a DEFAULT partition? */ + isSplitPartDefault = (defaultPartOid == splitPartOid); + + if (isSplitPartDefault && default_index == -1) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("can not split DEFAULT partition \"%s\"", + get_rel_name(splitPartOid)), + errhint("To split DEFAULT partition one of the new partition must be DEFAULT."), + parser_errposition(cxt->pstate, ((SinglePartitionSpec *) linitial(splitlist))->name->location)); + + /* + * If the partition being split is not the DEFAULT partition, but the + * DEFAULT partition exists, then none of the resulting split partitions + * can be the DEFAULT. + */ + if (!isSplitPartDefault && (default_index != -1) && OidIsValid(defaultPartOid)) + { + SinglePartitionSpec *spsDef = + (SinglePartitionSpec *) list_nth(splitlist, default_index); + + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("can not split non-DEFAULT partition \"%s\"", + get_rel_name(splitPartOid)), + errmsg("new partition cannot be DEFAULT because DEFAULT partition \"%s\" already exists", + get_rel_name(defaultPartOid)), + parser_errposition(cxt->pstate, spsDef->name->location)); + } + + foreach(listptr, splitlist) + { + Oid nspid; + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + RangeVar *name = sps->name; + + nspid = RangeVarGetCreationNamespace(sps->name); + + /* Partitions in the list should have different names. */ + for_each_cell(listptr2, splitlist, lnext(splitlist, listptr)) + { + Oid nspid2; + SinglePartitionSpec *sps2 = (SinglePartitionSpec *) lfirst(listptr2); + RangeVar *name2 = sps2->name; + + if (equal(name, name2)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("partition with name \"%s\" is already used", name->relname), + parser_errposition(cxt->pstate, name2->location)); + + nspid2 = RangeVarGetCreationNamespace(sps2->name); + + if (nspid2 == nspid && strcmp(name->relname, name2->relname) == 0) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("partition with name \"%s\" is already used", name->relname), + parser_errposition(cxt->pstate, name2->location)); + } + } + + /* Then we should check partitions with transformed bounds. */ + check_partitions_for_split(parent, splitPartOid, splitlist, cxt->pstate); +} + + +/* + * transformPartitionCmdForMerge - + * analyze the ALTER TABLE ... MERGE PARTITIONS command + * + * Does simple checks for merged partitions. Calculates bound of the resulting + * partition. + */ +static void +transformPartitionCmdForMerge(CreateStmtContext *cxt, PartitionCmd *partcmd) +{ + Oid defaultPartOid; + Oid partOid; + Relation parent = cxt->rel; + PartitionKey key; + char strategy; + ListCell *listptr, + *listptr2; + bool isDefaultPart = false; + List *partOids = NIL; + + key = RelationGetPartitionKey(parent); + strategy = get_partition_strategy(key); + + if (strategy == PARTITION_STRATEGY_HASH) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partition of hash-partitioned table cannot be merged")); + + /* Does the partitioned table (parent) have a default partition? */ + defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true)); + + foreach(listptr, partcmd->partlist) + { + RangeVar *name = (RangeVar *) lfirst(listptr); + + /* Partitions in the list should have different names. */ + for_each_cell(listptr2, partcmd->partlist, lnext(partcmd->partlist, listptr)) + { + RangeVar *name2 = (RangeVar *) lfirst(listptr2); + + if (equal(name, name2)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("partition with name \"%s\" is already used", name->relname), + parser_errposition(cxt->pstate, name2->location)); + } + + /* + * Search the DEFAULT partition in the list. Open and lock partitions + * before calculating the boundary for resulting partition, we also + * check for ownership along the way. We need to use + * AccessExclusiveLock here, because these merged partitions will be + * detached and then dropped in ATExecMergePartitions. + */ + partOid = RangeVarGetRelidExtended(name, AccessExclusiveLock, 0, + RangeVarCallbackOwnsRelation, + NULL); + /* Is the current partition a DEFAULT partition? */ + if (partOid == defaultPartOid) + isDefaultPart = true; + + /* + * Extended check because the same partition can have different names + * (for example, "part_name" and "public.part_name"). + */ + foreach(listptr2, partOids) + { + Oid curOid = lfirst_oid(listptr2); + + if (curOid == partOid) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("partition with name \"%s\" is already used", name->relname), + parser_errposition(cxt->pstate, name->location)); + } + + checkPartition(parent, partOid, true); + + partOids = lappend_oid(partOids, partOid); + } + + /* Allocate the bound of the resulting partition. */ + Assert(partcmd->bound == NULL); + partcmd->bound = makeNode(PartitionBoundSpec); + + /* Fill the partition bound. */ + partcmd->bound->strategy = strategy; + partcmd->bound->location = -1; + partcmd->bound->is_default = isDefaultPart; + if (!isDefaultPart) + calculate_partition_bound_for_merge(parent, partcmd->partlist, + partOids, partcmd->bound, + cxt->pstate); +} + /* * transformAlterTableStmt - * parse analysis for ALTER TABLE @@ -3757,20 +4069,48 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, { PartitionCmd *partcmd = (PartitionCmd *) cmd->def; - transformPartitionCmd(&cxt, partcmd); - /* assign transformed value of the partition bound */ + transformPartitionCmd(&cxt, partcmd->bound); + /* assign the transformed value of the partition bound */ partcmd->bound = cxt.partbound; } newcmds = lappend(newcmds, cmd); break; + case AT_MergePartitions: + { + PartitionCmd *partcmd = (PartitionCmd *) cmd->def; + + if (list_length(partcmd->partlist) < 2) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("list of partitions to be merged should include at least two partitions")); + + transformPartitionCmdForMerge(&cxt, partcmd); + newcmds = lappend(newcmds, cmd); + break; + } + + case AT_SplitPartition: + { + PartitionCmd *partcmd = (PartitionCmd *) cmd->def; + + if (list_length(partcmd->partlist) < 2) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("list of new partitions should contain at least two partitions")); + + transformPartitionCmdForSplit(&cxt, partcmd); + newcmds = lappend(newcmds, cmd); + break; + } + default: /* - * Currently, we shouldn't actually get here for subcommand - * types that don't require transformation; but if we do, just - * emit them unchanged. + * Currently, we shouldn't actually get here for the + * subcommand types that don't require transformation; but if + * we do, just emit them unchanged. */ newcmds = lappend(newcmds, cmd); break; @@ -3864,9 +4204,12 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, * NOTE: currently, attributes are only supported for FOREIGN KEY, UNIQUE, * EXCLUSION, and PRIMARY KEY constraints, but someday they ought to be * supported for other constraint types. + * + * NOTE: this must be idempotent in non-error cases; see + * transformCreateSchemaCreateTable. */ static void -transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) +transformConstraintAttrs(ParseState *pstate, List *constraintList) { Constraint *lastprimarycon = NULL; bool saw_deferrability = false; @@ -3895,12 +4238,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced DEFERRABLE clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_deferrability) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple DEFERRABLE/NOT DEFERRABLE clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_deferrability = true; lastprimarycon->deferrable = true; break; @@ -3910,12 +4253,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced NOT DEFERRABLE clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_deferrability) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple DEFERRABLE/NOT DEFERRABLE clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_deferrability = true; lastprimarycon->deferrable = false; if (saw_initially && @@ -3923,7 +4266,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("constraint declared INITIALLY DEFERRED must be DEFERRABLE"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); break; case CONSTR_ATTR_DEFERRED: @@ -3931,12 +4274,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced INITIALLY DEFERRED clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_initially) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple INITIALLY IMMEDIATE/DEFERRED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_initially = true; lastprimarycon->initdeferred = true; @@ -3949,7 +4292,7 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("constraint declared INITIALLY DEFERRED must be DEFERRABLE"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); break; case CONSTR_ATTR_IMMEDIATE: @@ -3957,12 +4300,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced INITIALLY IMMEDIATE clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_initially) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple INITIALLY IMMEDIATE/DEFERRED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_initially = true; lastprimarycon->initdeferred = false; break; @@ -3974,12 +4317,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced ENFORCED clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_enforced) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_enforced = true; lastprimarycon->is_enforced = true; break; @@ -3991,12 +4334,12 @@ transformConstraintAttrs(CreateStmtContext *cxt, List *constraintList) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("misplaced NOT ENFORCED clause"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); if (saw_enforced) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("multiple ENFORCED/NOT ENFORCED clauses not allowed"), - parser_errposition(cxt->pstate, con->location))); + parser_errposition(pstate, con->location))); saw_enforced = true; lastprimarycon->is_enforced = false; @@ -4054,51 +4397,45 @@ transformColumnType(CreateStmtContext *cxt, ColumnDef *column) * transformCreateSchemaStmtElements - * analyzes the elements of a CREATE SCHEMA statement * - * Split the schema element list from a CREATE SCHEMA statement into - * individual commands and place them in the result list in an order - * such that there are no forward references (e.g. GRANT to a table - * created later in the list). Note that the logic we use for determining - * forward references is presently quite incomplete. + * This presently has two responsibilities. We verify that no subcommands are + * trying to create objects outside the new schema. We also pull out any + * foreign-key constraint clauses embedded in CREATE TABLE subcommands, and + * convert them to ALTER TABLE ADD CONSTRAINT commands appended to the list. + * This supports forward references in foreign keys, which is required by the + * SQL standard. + * + * We used to try to re-order the commands in a way that would work even if + * the user-written order would not, but that's too hard (perhaps impossible) + * to do correctly with not-yet-parse-analyzed commands. Now we'll just + * execute the elements in the order given, except for foreign keys. * * "schemaName" is the name of the schema that will be used for the creation - * of the objects listed, that may be compiled from the schema name defined + * of the objects listed. It may be obtained from the schema name defined * in the statement or a role specification. * - * SQL also allows constraints to make forward references, so thumb through - * the table columns and move forward references to a posterior alter-table - * command. - * * The result is a list of parse nodes that still need to be analyzed --- * but we can't analyze the later commands until we've executed the earlier * ones, because of possible inter-object references. * - * Note: this breaks the rules a little bit by modifying schema-name fields - * within passed-in structs. However, the transformation would be the same - * if done over, so it should be all right to scribble on the input to this - * extent. + * Note it's important that we not modify the input data structure. We create + * a new result List, and we copy any CREATE TABLE subcommands that we might + * modify. */ List * -transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) +transformCreateSchemaStmtElements(ParseState *pstate, List *schemaElts, + const char *schemaName) { - CreateSchemaStmtContext cxt; - List *result; - ListCell *elements; - - cxt.schemaname = schemaName; - cxt.sequences = NIL; - cxt.tables = NIL; - cxt.views = NIL; - cxt.indexes = NIL; - cxt.triggers = NIL; - cxt.grants = NIL; + List *elements = NIL; + List *fk_elements = NIL; + ListCell *lc; /* - * Run through each schema element in the schema element list. Separate - * statements by type, and do preliminary analysis. + * Run through each schema element in the schema element list. Check + * target schema names, and collect the list of actions to be done. */ - foreach(elements, schemaElts) + foreach(lc, schemaElts) { - Node *element = lfirst(elements); + Node *element = lfirst(lc); switch (nodeTag(element)) { @@ -4106,8 +4443,8 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) { CreateSeqStmt *elp = (CreateSeqStmt *) element; - setSchemaName(cxt.schemaname, &elp->sequence->schemaname); - cxt.sequences = lappend(cxt.sequences, element); + checkSchemaNameRV(pstate, schemaName, elp->sequence); + elements = lappend(elements, element); } break; @@ -4115,12 +4452,12 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) { CreateStmt *elp = (CreateStmt *) element; - setSchemaName(cxt.schemaname, &elp->relation->schemaname); - - /* - * XXX todo: deal with constraints - */ - cxt.tables = lappend(cxt.tables, element); + checkSchemaNameRV(pstate, schemaName, elp->relation); + /* Pull out any foreign key clauses, add to fk_elements */ + elp = transformCreateSchemaCreateTable(pstate, + elp, + &fk_elements); + elements = lappend(elements, elp); } break; @@ -4128,12 +4465,8 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) { ViewStmt *elp = (ViewStmt *) element; - setSchemaName(cxt.schemaname, &elp->view->schemaname); - - /* - * XXX todo: deal with references between views - */ - cxt.views = lappend(cxt.views, element); + checkSchemaNameRV(pstate, schemaName, elp->view); + elements = lappend(elements, element); } break; @@ -4141,8 +4474,8 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) { IndexStmt *elp = (IndexStmt *) element; - setSchemaName(cxt.schemaname, &elp->relation->schemaname); - cxt.indexes = lappend(cxt.indexes, element); + checkSchemaNameRV(pstate, schemaName, elp->relation); + elements = lappend(elements, element); } break; @@ -4150,13 +4483,75 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) { CreateTrigStmt *elp = (CreateTrigStmt *) element; - setSchemaName(cxt.schemaname, &elp->relation->schemaname); - cxt.triggers = lappend(cxt.triggers, element); + checkSchemaNameRV(pstate, schemaName, elp->relation); + elements = lappend(elements, element); + } + break; + + case T_CreateDomainStmt: + { + CreateDomainStmt *elp = (CreateDomainStmt *) element; + + checkSchemaNameList(schemaName, elp->domainname); + elements = lappend(elements, element); + } + break; + + case T_CreateFunctionStmt: + { + CreateFunctionStmt *elp = (CreateFunctionStmt *) element; + + checkSchemaNameList(schemaName, elp->funcname); + elements = lappend(elements, element); + } + break; + + /* + * CREATE TYPE can produce a DefineStmt, but also + * CreateEnumStmt, CreateRangeStmt, and CompositeTypeStmt. + * Allowing DefineStmt also provides support for several other + * commands: currently, CREATE AGGREGATE, CREATE COLLATION, + * CREATE OPERATOR, and text search objects. + */ + + case T_DefineStmt: + { + DefineStmt *elp = (DefineStmt *) element; + + checkSchemaNameList(schemaName, elp->defnames); + elements = lappend(elements, element); + } + break; + + case T_CreateEnumStmt: + { + CreateEnumStmt *elp = (CreateEnumStmt *) element; + + checkSchemaNameList(schemaName, elp->typeName); + elements = lappend(elements, element); + } + break; + + case T_CreateRangeStmt: + { + CreateRangeStmt *elp = (CreateRangeStmt *) element; + + checkSchemaNameList(schemaName, elp->typeName); + elements = lappend(elements, element); + } + break; + + case T_CompositeTypeStmt: + { + CompositeTypeStmt *elp = (CompositeTypeStmt *) element; + + checkSchemaNameRV(pstate, schemaName, elp->typevar); + elements = lappend(elements, element); } break; case T_GrantStmt: - cxt.grants = lappend(cxt.grants, element); + elements = lappend(elements, element); break; default: @@ -4165,43 +4560,231 @@ transformCreateSchemaStmtElements(List *schemaElts, const char *schemaName) } } - result = NIL; - result = list_concat(result, cxt.sequences); - result = list_concat(result, cxt.tables); - result = list_concat(result, cxt.views); - result = list_concat(result, cxt.indexes); - result = list_concat(result, cxt.triggers); - result = list_concat(result, cxt.grants); + return list_concat(elements, fk_elements); +} - return result; +/* + * checkSchemaNameRV + * Check schema name in an element of a CREATE SCHEMA command, + * where the element's name is given by a RangeVar + * + * It's okay if the command doesn't specify a target schema name, because + * CreateSchemaCommand will set up the default creation schema to be the + * new schema. But if a target schema name is given, it had better match. + * We also have to check that the command doesn't say CREATE TEMP, since + * that would likewise put the object into the wrong schema. + */ +static void +checkSchemaNameRV(ParseState *pstate, const char *context_schema, + RangeVar *relation) +{ + if (relation->schemaname != NULL && + strcmp(context_schema, relation->schemaname) != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_SCHEMA_DEFINITION), + errmsg("CREATE specifies a schema (%s) " + "different from the one being created (%s)", + relation->schemaname, context_schema), + parser_errposition(pstate, relation->location))); + + if (relation->relpersistence == RELPERSISTENCE_TEMP) + { + /* spell this error the same as in RangeVarAdjustRelationPersistence */ + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("cannot create temporary relation in non-temporary schema"), + parser_errposition(pstate, relation->location))); + } } /* - * setSchemaName - * Set or check schema name in an element of a CREATE SCHEMA command + * checkSchemaNameList + * Check schema name in an element of a CREATE SCHEMA command, + * where the element's name is given by a List + * + * Much as above, but we don't have to worry about TEMP. + * Sadly, this also means we don't have a parse location to report. */ static void -setSchemaName(const char *context_schema, char **stmt_schema_name) +checkSchemaNameList(const char *context_schema, List *qualified_name) { - if (*stmt_schema_name == NULL) - *stmt_schema_name = unconstify(char *, context_schema); - else if (strcmp(context_schema, *stmt_schema_name) != 0) + char *obj_schema; + char *obj_name; + + DeconstructQualifiedName(qualified_name, &obj_schema, &obj_name); + if (obj_schema != NULL && + strcmp(context_schema, obj_schema) != 0) ereport(ERROR, (errcode(ERRCODE_INVALID_SCHEMA_DEFINITION), errmsg("CREATE specifies a schema (%s) " "different from the one being created (%s)", - *stmt_schema_name, context_schema))); + obj_schema, context_schema))); +} + +/* + * transformCreateSchemaCreateTable + * Process one CreateStmt for transformCreateSchemaStmtElements. + * + * We remove any foreign-key clauses in the statement and convert them into + * ALTER TABLE commands, which we append to *fk_elements. + */ +static CreateStmt * +transformCreateSchemaCreateTable(ParseState *pstate, + CreateStmt *stmt, + List **fk_elements) +{ + CreateStmt *newstmt; + List *newElts = NIL; + ListCell *lc; + + /* + * Flat-copy the CreateStmt node, allowing us to replace its tableElts + * list without damaging the input data structure. Most sub-nodes will be + * shared with the input, though. + */ + newstmt = makeNode(CreateStmt); + memcpy(newstmt, stmt, sizeof(CreateStmt)); + + /* Scan for foreign-key constraints */ + foreach(lc, stmt->tableElts) + { + Node *element = lfirst(lc); + AlterTableStmt *alterstmt; + AlterTableCmd *altercmd; + + if (IsA(element, Constraint)) + { + Constraint *constr = (Constraint *) element; + + if (constr->contype != CONSTR_FOREIGN) + { + /* Other constraint types pass through unchanged */ + newElts = lappend(newElts, constr); + continue; + } + + /* Make it into an ALTER TABLE ADD CONSTRAINT command */ + altercmd = makeNode(AlterTableCmd); + altercmd->subtype = AT_AddConstraint; + altercmd->name = NULL; + altercmd->def = (Node *) copyObject(constr); + + alterstmt = makeNode(AlterTableStmt); + alterstmt->relation = copyObject(stmt->relation); + alterstmt->cmds = list_make1(altercmd); + alterstmt->objtype = OBJECT_TABLE; + + *fk_elements = lappend(*fk_elements, alterstmt); + } + else if (IsA(element, ColumnDef)) + { + ColumnDef *entry = (ColumnDef *) element; + ColumnDef *newentry; + List *entryconstraints; + bool afterFK = false; + + /* + * We must preprocess the list of column constraints to attach + * attributes such as DEFERRED to the appropriate constraint node. + * Do this on a copy. (But execution of the CreateStmt will run + * transformConstraintAttrs on the copy, so we are nonetheless + * relying on transformConstraintAttrs to be idempotent.) + */ + entryconstraints = copyObject(entry->constraints); + transformConstraintAttrs(pstate, entryconstraints); + + /* Scan the column constraints ... */ + foreach_node(Constraint, colconstr, entryconstraints) + { + switch (colconstr->contype) + { + case CONSTR_FOREIGN: + /* colconstr is already a copy, OK to modify */ + colconstr->fk_attrs = list_make1(makeString(entry->colname)); + + /* Make it into an ALTER TABLE ADD CONSTRAINT command */ + altercmd = makeNode(AlterTableCmd); + altercmd->subtype = AT_AddConstraint; + altercmd->name = NULL; + altercmd->def = (Node *) colconstr; + + alterstmt = makeNode(AlterTableStmt); + alterstmt->relation = copyObject(stmt->relation); + alterstmt->cmds = list_make1(altercmd); + alterstmt->objtype = OBJECT_TABLE; + + *fk_elements = lappend(*fk_elements, alterstmt); + + /* Remove the Constraint node from entryconstraints */ + entryconstraints = + foreach_delete_current(entryconstraints, colconstr); + + /* + * Immediately-following attribute constraints should + * be dropped, too. + */ + afterFK = true; + break; + + /* + * Column constraint lists separate a Constraint node + * from its attributes (e.g. NOT ENFORCED); so a + * column-level foreign key constraint may be + * represented by multiple Constraint nodes. After + * transformConstraintAttrs, the foreign key + * Constraint node contains all required information, + * making it okay to put into *fk_elements as a + * stand-alone Constraint. But since we removed the + * foreign key Constraint node from entryconstraints, + * we must remove any dependent attribute nodes too, + * else the later re-execution of + * transformConstraintAttrs will misbehave. + */ + case CONSTR_ATTR_DEFERRABLE: + case CONSTR_ATTR_NOT_DEFERRABLE: + case CONSTR_ATTR_DEFERRED: + case CONSTR_ATTR_IMMEDIATE: + case CONSTR_ATTR_ENFORCED: + case CONSTR_ATTR_NOT_ENFORCED: + if (afterFK) + entryconstraints = + foreach_delete_current(entryconstraints, + colconstr); + break; + + default: + /* Any following constraint attributes are unrelated */ + afterFK = false; + break; + } + } + + /* Now make a modified ColumnDef to put into newElts */ + newentry = makeNode(ColumnDef); + memcpy(newentry, entry, sizeof(ColumnDef)); + newentry->constraints = entryconstraints; + newElts = lappend(newElts, newentry); + } + else + { + /* Other node types pass through unchanged */ + newElts = lappend(newElts, element); + } + } + + newstmt->tableElts = newElts; + return newstmt; } /* * transformPartitionCmd - * Analyze the ATTACH/DETACH PARTITION command + * Analyze the ATTACH/DETACH/SPLIT PARTITION command * - * In case of the ATTACH PARTITION command, cxt->partbound is set to the - * transformed value of cmd->bound. + * In case of the ATTACH/SPLIT PARTITION command, cxt->partbound is set to the + * transformed value of bound. */ static void -transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd) +transformPartitionCmd(CreateStmtContext *cxt, PartitionBoundSpec *bound) { Relation parentRel = cxt->rel; @@ -4210,9 +4793,9 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd) case RELKIND_PARTITIONED_TABLE: /* transform the partition bound, if any */ Assert(RelationGetPartitionKey(parentRel) != NULL); - if (cmd->bound != NULL) + if (bound != NULL) cxt->partbound = transformPartitionBound(cxt->pstate, parentRel, - cmd->bound); + bound); break; case RELKIND_PARTITIONED_INDEX: @@ -4220,7 +4803,7 @@ transformPartitionCmd(CreateStmtContext *cxt, PartitionCmd *cmd) * A partitioned index cannot have a partition bound set. ALTER * INDEX prevents that with its grammar, but not ALTER TABLE. */ - if (cmd->bound != NULL) + if (bound != NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), errmsg("\"%s\" is not a partitioned table", @@ -4418,12 +5001,14 @@ transformPartitionRangeBounds(ParseState *pstate, List *blist, int i, j; - i = j = 0; + j = 0; foreach(lc, blist) { Node *expr = lfirst(lc); PartitionRangeDatum *prd = NULL; + i = foreach_current_index(lc); + /* * Infinite range bounds -- "minvalue" and "maxvalue" -- get passed in * as ColumnRefs. @@ -4501,7 +5086,6 @@ transformPartitionRangeBounds(ParseState *pstate, List *blist, prd = makeNode(PartitionRangeDatum); prd->kind = PARTITION_RANGE_DATUM_VALUE; prd->value = (Node *) value; - ++i; } prd->location = exprLocation(expr); diff --git a/src/backend/parser/parser.c b/src/backend/parser/parser.c index 33a040506b47f..b5c285239712b 100644 --- a/src/backend/parser/parser.c +++ b/src/backend/parser/parser.c @@ -10,7 +10,7 @@ * analyze.c and related files. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -339,7 +339,7 @@ hexval(unsigned char c) /* is Unicode code point acceptable? */ static void -check_unicode_value(pg_wchar c) +check_unicode_value(char32_t c) { if (!is_valid_unicode_codepoint(c)) ereport(ERROR, @@ -376,7 +376,7 @@ str_udeescape(const char *str, char escape, char *new, *out; size_t new_len; - pg_wchar pair_first = 0; + char16_t pair_first = 0; ScannerCallbackState scbstate; /* @@ -420,7 +420,7 @@ str_udeescape(const char *str, char escape, isxdigit((unsigned char) in[3]) && isxdigit((unsigned char) in[4])) { - pg_wchar unicode; + char32_t unicode; unicode = (hexval(in[1]) << 12) + (hexval(in[2]) << 8) + @@ -457,7 +457,7 @@ str_udeescape(const char *str, char escape, isxdigit((unsigned char) in[6]) && isxdigit((unsigned char) in[7])) { - pg_wchar unicode; + char32_t unicode; unicode = (hexval(in[2]) << 20) + (hexval(in[3]) << 16) + diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l index 08990831fe81a..ee6c34cc14b01 100644 --- a/src/backend/parser/scan.l +++ b/src/backend/parser/scan.l @@ -22,7 +22,7 @@ * Postgres 9.2, this check is made automatically by the Makefile.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -60,14 +60,13 @@ fprintf_to_ereport(const char *fmt, const char *msg) } /* - * GUC variables. This is a DIRECT violation of the warning given at the + * GUC variable. This is a DIRECT violation of the warning given at the * head of gram.y, ie flex/bison code must not depend on any GUC variables; - * as such, changing their values can induce very unintuitive behavior. - * But we shall have to live with it until we can remove these variables. + * as such, changing its value can induce very unintuitive behavior. + * In practice, backslash_quote is not too awful since it only controls + * whether to throw an error: it cannot change non-error results. */ int backslash_quote = BACKSLASH_QUOTE_SAFE_ENCODING; -bool escape_string_warning = true; -bool standard_conforming_strings = true; /* * Constant data exported from this file. This array maps from the @@ -121,15 +120,12 @@ static void addlitchar(unsigned char ychar, core_yyscan_t yyscanner); static char *litbufdup(core_yyscan_t yyscanner); static unsigned char unescape_single_char(unsigned char c, core_yyscan_t yyscanner); static int process_integer_literal(const char *token, YYSTYPE *lval, int base); -static void addunicode(pg_wchar c, yyscan_t yyscanner); +static void addunicode(char32_t c, yyscan_t yyscanner); #define yyerror(msg) scanner_yyerror(msg, yyscanner) #define lexer_errposition() scanner_errposition(*(yylloc), yyscanner) -static void check_string_escape_warning(unsigned char ychar, core_yyscan_t yyscanner); -static void check_escape_warning(core_yyscan_t yyscanner); - %} %option reentrant @@ -352,6 +348,8 @@ less_equals "<=" greater_equals ">=" less_greater "<>" not_equals "!=" +/* Note there is no need for left_arrow, since "<-" is not a single operator. */ +right_arrow "->" /* * "self" is the set of chars that should be returned as single-character @@ -363,7 +361,7 @@ not_equals "!=" * If you change either set, adjust the character lists appearing in the * rule for "operator"! */ -self [,()\[\].;\:\+\-\*\/\%\^\<\>\=] +self [,()\[\].;\:\|\+\-\*\/\%\^\<\>\=] op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=] operator {op_chars}+ @@ -543,17 +541,12 @@ other . } {xqstart} { - yyextra->warn_on_first_escape = true; yyextra->saw_non_ascii = false; SET_YYLLOC(); - if (yyextra->standard_conforming_strings) - BEGIN(xq); - else - BEGIN(xe); + BEGIN(xq); startlit(); } {xestart} { - yyextra->warn_on_first_escape = false; yyextra->saw_non_ascii = false; SET_YYLLOC(); BEGIN(xe); @@ -561,12 +554,6 @@ other . } {xusstart} { SET_YYLLOC(); - if (!yyextra->standard_conforming_strings) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("unsafe use of string constant with Unicode escapes"), - errdetail("String constants with Unicode escapes cannot be used when \"standard_conforming_strings\" is off."), - lexer_errposition())); BEGIN(xus); startlit(); } @@ -640,14 +627,7 @@ other . addlit(yytext, yyleng, yyscanner); } {xeunicode} { - pg_wchar c = strtoul(yytext + 2, NULL, 16); - - /* - * For consistency with other productions, issue any - * escape warning with cursor pointing to start of string. - * We might want to change that, someday. - */ - check_escape_warning(yyscanner); + char32_t c = strtoul(yytext + 2, NULL, 16); /* Remember start of overall string token ... */ PUSH_YYLLOC(); @@ -668,7 +648,7 @@ other . POP_YYLLOC(); } {xeunicode} { - pg_wchar c = strtoul(yytext + 2, NULL, 16); + char32_t c = strtoul(yytext + 2, NULL, 16); /* Remember start of overall string token ... */ PUSH_YYLLOC(); @@ -715,14 +695,12 @@ other . errhint("Use '' to write quotes in strings. \\' is insecure in client-only encodings."), lexer_errposition())); } - check_string_escape_warning(yytext[1], yyscanner); addlitchar(unescape_single_char(yytext[1], yyscanner), yyscanner); } {xeoctesc} { unsigned char c = strtoul(yytext + 1, NULL, 8); - check_escape_warning(yyscanner); addlitchar(c, yyscanner); if (c == '\0' || IS_HIGHBIT_SET(c)) yyextra->saw_non_ascii = true; @@ -730,7 +708,6 @@ other . {xehexesc} { unsigned char c = strtoul(yytext + 2, NULL, 16); - check_escape_warning(yyscanner); addlitchar(c, yyscanner); if (c == '\0' || IS_HIGHBIT_SET(c)) yyextra->saw_non_ascii = true; @@ -878,6 +855,11 @@ other . return NOT_EQUALS; } +{right_arrow} { + SET_YYLLOC(); + return RIGHT_ARROW; + } + {self} { SET_YYLLOC(); return yytext[0]; @@ -955,7 +937,7 @@ other . * that the "self" rule would have. */ if (nchars == 1 && - strchr(",()[].;:+-*/%^<>=", yytext[0])) + strchr(",()[].;:|+-*/%^<>=", yytext[0])) return yytext[0]; /* * Likewise, if what we have left is two chars, and @@ -975,6 +957,8 @@ other . return NOT_EQUALS; if (yytext[0] == '!' && yytext[1] == '=') return NOT_EQUALS; + if (yytext[0] == '-' && yytext[1] == '>') + return RIGHT_ARROW; } } @@ -1263,8 +1247,6 @@ scanner_init(const char *str, yyext->keyword_tokens = keyword_tokens; yyext->backslash_quote = backslash_quote; - yyext->escape_string_warning = escape_string_warning; - yyext->standard_conforming_strings = standard_conforming_strings; /* * Make a scan buffer with special termination needed by flex. @@ -1376,7 +1358,7 @@ process_integer_literal(const char *token, YYSTYPE *lval, int base) } static void -addunicode(pg_wchar c, core_yyscan_t yyscanner) +addunicode(char32_t c, core_yyscan_t yyscanner) { ScannerCallbackState scbstate; char buf[MAX_UNICODE_EQUIVALENT_STRING + 1]; @@ -1420,45 +1402,6 @@ unescape_single_char(unsigned char c, core_yyscan_t yyscanner) } } -static void -check_string_escape_warning(unsigned char ychar, core_yyscan_t yyscanner) -{ - if (ychar == '\'') - { - if (yyextra->warn_on_first_escape && yyextra->escape_string_warning) - ereport(WARNING, - (errcode(ERRCODE_NONSTANDARD_USE_OF_ESCAPE_CHARACTER), - errmsg("nonstandard use of \\' in a string literal"), - errhint("Use '' to write quotes in strings, or use the escape string syntax (E'...')."), - lexer_errposition())); - yyextra->warn_on_first_escape = false; /* warn only once per string */ - } - else if (ychar == '\\') - { - if (yyextra->warn_on_first_escape && yyextra->escape_string_warning) - ereport(WARNING, - (errcode(ERRCODE_NONSTANDARD_USE_OF_ESCAPE_CHARACTER), - errmsg("nonstandard use of \\\\ in a string literal"), - errhint("Use the escape string syntax for backslashes, e.g., E'\\\\'."), - lexer_errposition())); - yyextra->warn_on_first_escape = false; /* warn only once per string */ - } - else - check_escape_warning(yyscanner); -} - -static void -check_escape_warning(core_yyscan_t yyscanner) -{ - if (yyextra->warn_on_first_escape && yyextra->escape_string_warning) - ereport(WARNING, - (errcode(ERRCODE_NONSTANDARD_USE_OF_ESCAPE_CHARACTER), - errmsg("nonstandard use of escape in a string literal"), - errhint("Use the escape string syntax for escapes, e.g., E'\\r\\n'."), - lexer_errposition())); - yyextra->warn_on_first_escape = false; /* warn only once per string */ -} - /* * Interface functions to make flex use palloc() instead of malloc(). * It'd be better to make these static, but flex insists otherwise. diff --git a/src/backend/parser/scansup.c b/src/backend/parser/scansup.c index 2feb2b6cf5a96..cd9594226d616 100644 --- a/src/backend/parser/scansup.c +++ b/src/backend/parser/scansup.c @@ -3,7 +3,7 @@ * scansup.c * scanner support routines used by the core lexer * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,6 +18,7 @@ #include "mb/pg_wchar.h" #include "parser/scansup.h" +#include "utils/pg_locale.h" /* @@ -46,35 +47,22 @@ char * downcase_identifier(const char *ident, int len, bool warn, bool truncate) { char *result; - int i; - bool enc_is_single_byte; - - result = palloc(len + 1); - enc_is_single_byte = pg_database_encoding_max_length() == 1; + size_t needed pg_attribute_unused(); /* - * SQL99 specifies Unicode-aware case normalization, which we don't yet - * have the infrastructure for. Instead we use tolower() to provide a - * locale-aware translation. However, there are some locales where this - * is not right either (eg, Turkish may do strange things with 'i' and - * 'I'). Our current compromise is to use tolower() for characters with - * the high bit set, as long as they aren't part of a multi-byte - * character, and use an ASCII-only downcasing for 7-bit characters. + * Preserves string length. + * + * NB: if we decide to support Unicode-aware identifier case folding, then + * we need to account for a change in string length. */ - for (i = 0; i < len; i++) - { - unsigned char ch = (unsigned char) ident[i]; + result = palloc(len + 1); - if (ch >= 'A' && ch <= 'Z') - ch += 'a' - 'A'; - else if (enc_is_single_byte && IS_HIGHBIT_SET(ch) && isupper(ch)) - ch = tolower(ch); - result[i] = (char) ch; - } - result[i] = '\0'; + needed = pg_downcase_ident(result, len + 1, ident, len); + Assert(needed == len); + Assert(result[len] == '\0'); - if (i >= NAMEDATALEN && truncate) - truncate_identifier(result, i, warn); + if (len >= NAMEDATALEN && truncate) + truncate_identifier(result, len, warn); return result; } diff --git a/src/backend/partitioning/meson.build b/src/backend/partitioning/meson.build index 126655ef58fde..891e379da2b81 100644 --- a/src/backend/partitioning/meson.build +++ b/src/backend/partitioning/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'partbounds.c', diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c index 4bdc2941efb21..05250eda472a1 100644 --- a/src/backend/partitioning/partbounds.c +++ b/src/backend/partitioning/partbounds.c @@ -3,7 +3,7 @@ * partbounds.c * Support routines for manipulating partition bounds * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -17,6 +17,7 @@ #include "access/relation.h" #include "access/table.h" #include "access/tableam.h" +#include "catalog/namespace.h" #include "catalog/partition.h" #include "catalog/pg_inherits.h" #include "catalog/pg_type.h" @@ -319,7 +320,7 @@ partition_bounds_create(PartitionBoundSpec **boundspecs, int nparts, * Initialize mapping array with invalid values, this is filled within * each sub-routine below depending on the bound type. */ - *mapping = (int *) palloc(sizeof(int) * nparts); + *mapping = palloc_array(int, nparts); for (i = 0; i < nparts; i++) (*mapping)[i] = -1; @@ -353,15 +354,13 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts, int greatest_modulus; Datum *boundDatums; - boundinfo = (PartitionBoundInfoData *) - palloc0(sizeof(PartitionBoundInfoData)); + boundinfo = palloc0_object(PartitionBoundInfoData); boundinfo->strategy = key->strategy; /* No special hash partitions. */ boundinfo->null_index = -1; boundinfo->default_index = -1; - hbounds = (PartitionHashBound *) - palloc(nparts * sizeof(PartitionHashBound)); + hbounds = palloc_array(PartitionHashBound, nparts); /* Convert from node to the internal representation */ for (i = 0; i < nparts; i++) @@ -384,7 +383,7 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts, greatest_modulus = hbounds[nparts - 1].modulus; boundinfo->ndatums = nparts; - boundinfo->datums = (Datum **) palloc0(nparts * sizeof(Datum *)); + boundinfo->datums = palloc0_array(Datum *, nparts); boundinfo->kind = NULL; boundinfo->interleaved_parts = NULL; boundinfo->nindexes = greatest_modulus; @@ -472,8 +471,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts, int null_index = -1; Datum *boundDatums; - boundinfo = (PartitionBoundInfoData *) - palloc0(sizeof(PartitionBoundInfoData)); + boundinfo = palloc0_object(PartitionBoundInfoData); boundinfo->strategy = key->strategy; /* Will be set correctly below. */ boundinfo->null_index = -1; @@ -533,7 +531,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts, qsort_partition_list_value_cmp, key); boundinfo->ndatums = ndatums; - boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *)); + boundinfo->datums = palloc0_array(Datum *, ndatums); boundinfo->kind = NULL; boundinfo->interleaved_parts = NULL; boundinfo->nindexes = ndatums; @@ -690,16 +688,14 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts, Datum *boundDatums; PartitionRangeDatumKind *boundKinds; - boundinfo = (PartitionBoundInfoData *) - palloc0(sizeof(PartitionBoundInfoData)); + boundinfo = palloc0_object(PartitionBoundInfoData); boundinfo->strategy = key->strategy; /* There is no special null-accepting range partition. */ boundinfo->null_index = -1; /* Will be set correctly below. */ boundinfo->default_index = -1; - all_bounds = (PartitionRangeBound **) - palloc0(2 * nparts * sizeof(PartitionRangeBound *)); + all_bounds = palloc0_array(PartitionRangeBound *, 2 * nparts); /* Create a unified list of range bounds across all the partitions. */ ndatums = 0; @@ -803,10 +799,8 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts, * bound. */ boundinfo->ndatums = ndatums; - boundinfo->datums = (Datum **) palloc0(ndatums * sizeof(Datum *)); - boundinfo->kind = (PartitionRangeDatumKind **) - palloc(ndatums * - sizeof(PartitionRangeDatumKind *)); + boundinfo->datums = palloc0_array(Datum *, ndatums); + boundinfo->kind = palloc0_array(PartitionRangeDatumKind *, ndatums); boundinfo->interleaved_parts = NULL; /* @@ -814,7 +808,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts, * element of the indexes[] array. */ boundinfo->nindexes = ndatums + 1; - boundinfo->indexes = (int *) palloc((ndatums + 1) * sizeof(int)); + boundinfo->indexes = palloc_array(int, (ndatums + 1)); /* * In the loop below, to save from allocating a series of small arrays, @@ -824,8 +818,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts, */ partnatts = key->partnatts; boundDatums = (Datum *) palloc(ndatums * partnatts * sizeof(Datum)); - boundKinds = (PartitionRangeDatumKind *) palloc(ndatums * partnatts * - sizeof(PartitionRangeDatumKind)); + boundKinds = palloc_array(PartitionRangeDatumKind, ndatums * partnatts); for (i = 0; i < ndatums; i++) { @@ -1007,11 +1000,8 @@ partition_bounds_copy(PartitionBoundInfo src, int ndatums; int nindexes; int partnatts; - bool hash_part; - int natts; - Datum *boundDatums; - dest = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData)); + dest = (PartitionBoundInfo) palloc_object(PartitionBoundInfoData); dest->strategy = src->strategy; ndatums = dest->ndatums = src->ndatums; @@ -1021,9 +1011,9 @@ partition_bounds_copy(PartitionBoundInfo src, /* List partitioned tables have only a single partition key. */ Assert(key->strategy != PARTITION_STRATEGY_LIST || partnatts == 1); - dest->datums = (Datum **) palloc(sizeof(Datum *) * ndatums); + dest->datums = palloc_array(Datum *, ndatums); - if (src->kind != NULL) + if (src->kind != NULL && ndatums > 0) { PartitionRangeDatumKind *boundKinds; @@ -1058,40 +1048,44 @@ partition_bounds_copy(PartitionBoundInfo src, * For hash partitioning, datums array will have two elements - modulus * and remainder. */ - hash_part = (key->strategy == PARTITION_STRATEGY_HASH); - natts = hash_part ? 2 : partnatts; - boundDatums = palloc(ndatums * natts * sizeof(Datum)); - - for (i = 0; i < ndatums; i++) + if (ndatums > 0) { - int j; - - dest->datums[i] = &boundDatums[i * natts]; + bool hash_part = (key->strategy == PARTITION_STRATEGY_HASH); + int natts = hash_part ? 2 : partnatts; + Datum *boundDatums = palloc(ndatums * natts * sizeof(Datum)); - for (j = 0; j < natts; j++) + for (i = 0; i < ndatums; i++) { - bool byval; - int typlen; + int j; - if (hash_part) - { - typlen = sizeof(int32); /* Always int4 */ - byval = true; /* int4 is pass-by-value */ - } - else + dest->datums[i] = &boundDatums[i * natts]; + + for (j = 0; j < natts; j++) { - byval = key->parttypbyval[j]; - typlen = key->parttyplen[j]; - } + if (dest->kind == NULL || + dest->kind[i][j] == PARTITION_RANGE_DATUM_VALUE) + { + bool byval; + int typlen; - if (dest->kind == NULL || - dest->kind[i][j] == PARTITION_RANGE_DATUM_VALUE) - dest->datums[i][j] = datumCopy(src->datums[i][j], - byval, typlen); + if (hash_part) + { + typlen = sizeof(int32); /* Always int4 */ + byval = true; /* int4 is pass-by-value */ + } + else + { + byval = key->parttypbyval[j]; + typlen = key->parttyplen[j]; + } + dest->datums[i][j] = datumCopy(src->datums[i][j], + byval, typlen); + } + } } } - dest->indexes = (int *) palloc(sizeof(int) * nindexes); + dest->indexes = palloc_array(int, nindexes); memcpy(dest->indexes, src->indexes, sizeof(int) * nindexes); dest->null_index = src->null_index; @@ -1814,10 +1808,10 @@ init_partition_map(RelOptInfo *rel, PartitionMap *map) int i; map->nparts = nparts; - map->merged_indexes = (int *) palloc(sizeof(int) * nparts); - map->merged = (bool *) palloc(sizeof(bool) * nparts); + map->merged_indexes = palloc_array(int, nparts); + map->merged = palloc_array(bool, nparts); map->did_remapping = false; - map->old_indexes = (int *) palloc(sizeof(int) * nparts); + map->old_indexes = palloc_array(int, nparts); for (i = 0; i < nparts; i++) { map->merged_indexes[i] = map->old_indexes[i] = -1; @@ -2392,7 +2386,7 @@ fix_merged_indexes(PartitionMap *outer_map, PartitionMap *inner_map, Assert(nmerged > 0); - new_indexes = (int *) palloc(sizeof(int) * nmerged); + new_indexes = palloc_array(int, nmerged); for (i = 0; i < nmerged; i++) new_indexes[i] = -1; @@ -2452,8 +2446,8 @@ generate_matching_part_pairs(RelOptInfo *outer_rel, RelOptInfo *inner_rel, Assert(*outer_parts == NIL); Assert(*inner_parts == NIL); - outer_indexes = (int *) palloc(sizeof(int) * nmerged); - inner_indexes = (int *) palloc(sizeof(int) * nmerged); + outer_indexes = palloc_array(int, nmerged); + inner_indexes = palloc_array(int, nmerged); for (i = 0; i < nmerged; i++) outer_indexes[i] = inner_indexes[i] = -1; @@ -2524,11 +2518,11 @@ build_merged_partition_bounds(char strategy, List *merged_datums, int pos; ListCell *lc; - merged_bounds = (PartitionBoundInfo) palloc(sizeof(PartitionBoundInfoData)); + merged_bounds = palloc_object(PartitionBoundInfoData); merged_bounds->strategy = strategy; merged_bounds->ndatums = ndatums; - merged_bounds->datums = (Datum **) palloc(sizeof(Datum *) * ndatums); + merged_bounds->datums = palloc_array(Datum *, ndatums); pos = 0; foreach(lc, merged_datums) merged_bounds->datums[pos++] = (Datum *) lfirst(lc); @@ -2536,8 +2530,7 @@ build_merged_partition_bounds(char strategy, List *merged_datums, if (strategy == PARTITION_STRATEGY_RANGE) { Assert(list_length(merged_kinds) == ndatums); - merged_bounds->kind = (PartitionRangeDatumKind **) - palloc(sizeof(PartitionRangeDatumKind *) * ndatums); + merged_bounds->kind = palloc_array(PartitionRangeDatumKind *, ndatums); pos = 0; foreach(lc, merged_kinds) merged_bounds->kind[pos++] = (PartitionRangeDatumKind *) lfirst(lc); @@ -2558,7 +2551,7 @@ build_merged_partition_bounds(char strategy, List *merged_datums, Assert(list_length(merged_indexes) == ndatums); merged_bounds->nindexes = ndatums; - merged_bounds->indexes = (int *) palloc(sizeof(int) * ndatums); + merged_bounds->indexes = palloc_array(int, ndatums); pos = 0; foreach(lc, merged_indexes) merged_bounds->indexes[pos++] = lfirst_int(lc); @@ -3369,7 +3362,8 @@ check_default_partition_contents(Relation parent, Relation default_rel, econtext = GetPerTupleExprContext(estate); snapshot = RegisterSnapshot(GetLatestSnapshot()); tupslot = table_slot_create(part_rel, &estate->es_tupleTable); - scan = table_beginscan(part_rel, snapshot, 0, NULL); + scan = table_beginscan(part_rel, snapshot, 0, NULL, + SO_NONE); /* * Switch to per-tuple memory context and reset it for each tuple @@ -3433,11 +3427,10 @@ make_one_partition_rbound(PartitionKey key, int index, List *datums, bool lower) Assert(datums != NIL); - bound = (PartitionRangeBound *) palloc0(sizeof(PartitionRangeBound)); + bound = palloc0_object(PartitionRangeBound); bound->index = index; - bound->datums = (Datum *) palloc0(key->partnatts * sizeof(Datum)); - bound->kind = (PartitionRangeDatumKind *) palloc0(key->partnatts * - sizeof(PartitionRangeDatumKind)); + bound->datums = palloc0_array(Datum, key->partnatts); + bound->kind = palloc0_array(PartitionRangeDatumKind, key->partnatts); bound->lower = lower; i = 0; @@ -3554,8 +3547,8 @@ partition_rbound_cmp(int partnatts, FmgrInfo *partsupfunc, */ int32 partition_rbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation, - Datum *rb_datums, PartitionRangeDatumKind *rb_kind, - Datum *tuple_datums, int n_tuple_datums) + const Datum *rb_datums, PartitionRangeDatumKind *rb_kind, + const Datum *tuple_datums, int n_tuple_datums) { int i; int32 cmpval = -1; @@ -3694,7 +3687,7 @@ partition_range_bsearch(int partnatts, FmgrInfo *partsupfunc, int partition_range_datum_bsearch(FmgrInfo *partsupfunc, Oid *partcollation, PartitionBoundInfo boundinfo, - int nvalues, Datum *values, bool *is_equal) + int nvalues, const Datum *values, bool *is_equal) { int lo, hi, @@ -4977,3 +4970,907 @@ satisfies_hash_partition(PG_FUNCTION_ARGS) PG_RETURN_BOOL(rowHash % modulus == remainder); } + +/* + * check_two_partitions_bounds_range + * + * (function for BY RANGE partitioning) + * + * This is a helper function for check_partitions_for_split() and + * calculate_partition_bound_for_merge(). This function compares the upper + * bound of first_bound and the lower bound of second_bound. These bounds + * should be equal except when "defaultPart == true" (this means that one of + * the split partitions is DEFAULT). In this case, the upper bound of + * first_bound can be less than the lower bound of second_bound because + * the space between these bounds will be included in the DEFAULT partition. + * + * parent: partitioned table + * first_name: name of the first partition + * first_bound: bound of the first partition + * second_name: name of the second partition + * second_bound: bound of the second partition + * defaultPart: true if one of the new partitions is DEFAULT + * is_merge: true indicates the operation is MERGE PARTITIONS; + * false indicates the operation is SPLIT PARTITION. + * pstate: pointer to ParseState struct for determining error position + */ +static void +check_two_partitions_bounds_range(Relation parent, + RangeVar *first_name, + PartitionBoundSpec *first_bound, + RangeVar *second_name, + PartitionBoundSpec *second_bound, + bool defaultPart, + bool is_merge, + ParseState *pstate) +{ + PartitionKey key = RelationGetPartitionKey(parent); + PartitionRangeBound *first_upper; + PartitionRangeBound *second_lower; + int cmpval; + + Assert(key->strategy == PARTITION_STRATEGY_RANGE); + + first_upper = make_one_partition_rbound(key, -1, first_bound->upperdatums, false); + second_lower = make_one_partition_rbound(key, -1, second_bound->lowerdatums, true); + + /* + * lower1 argument of partition_rbound_cmp() is set to false for the + * correct comparison result of the lower and upper bounds. + */ + cmpval = partition_rbound_cmp(key->partnatts, + key->partsupfunc, + key->partcollation, + second_lower->datums, second_lower->kind, + false, first_upper); + if ((!defaultPart && cmpval) || (defaultPart && cmpval < 0)) + { + PartitionRangeDatum *datum = linitial(second_bound->lowerdatums); + + if (is_merge) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("can not merge partition \"%s\" together with partition \"%s\"", + second_name->relname, first_name->relname), + errdetail("lower bound of partition \"%s\" is not equal to the upper bound of partition \"%s\"", + second_name->relname, first_name->relname), + errhint("ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent."), + parser_errposition(pstate, datum->location)); + else + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("can not split to partition \"%s\" together with partition \"%s\"", + second_name->relname, first_name->relname), + errdetail("lower bound of partition \"%s\" is not equal to the upper bound of partition \"%s\"", + second_name->relname, first_name->relname), + errhint("ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent."), + parser_errposition(pstate, datum->location)); + } +} + +/* + * get_partition_bound_spec + * + * Returns the PartitionBoundSpec for the partition with the given OID partOid. + */ +static PartitionBoundSpec * +get_partition_bound_spec(Oid partOid) +{ + HeapTuple tuple; + Datum datum; + bool isnull; + PartitionBoundSpec *boundspec = NULL; + + /* Try fetching the tuple from the catcache, for speed. */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(partOid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", partOid); + + datum = SysCacheGetAttr(RELOID, tuple, + Anum_pg_class_relpartbound, + &isnull); + if (isnull) + elog(ERROR, "partition bound for relation %u is null", + partOid); + + boundspec = stringToNode(TextDatumGetCString(datum)); + + if (!IsA(boundspec, PartitionBoundSpec)) + elog(ERROR, "expected PartitionBoundSpec for relation %u", + partOid); + + ReleaseSysCache(tuple); + return boundspec; +} + +/* + * calculate_partition_bound_for_merge + * + * Calculates the bound of the merged partition "spec" by using the bounds of + * the partitions to be merged. + * + * parent: partitioned table + * partNames: names of partitions to be merged + * partOids: Oids of partitions to be merged + * spec (out): bounds specification of the merged partition + * pstate: pointer to ParseState struct to determine error position + */ +void +calculate_partition_bound_for_merge(Relation parent, + List *partNames, + List *partOids, + PartitionBoundSpec *spec, + ParseState *pstate) +{ + PartitionKey key = RelationGetPartitionKey(parent); + PartitionBoundSpec *bound; + + Assert(!spec->is_default); + + switch (key->strategy) + { + case PARTITION_STRATEGY_RANGE: + { + int i; + PartitionRangeBound **lower_bounds; + int nparts = list_length(partOids); + List *bounds = NIL; + + lower_bounds = palloc0_array(PartitionRangeBound *, nparts); + + /* + * Create an array of lower bounds and a list of + * PartitionBoundSpec. + */ + foreach_oid(partoid, partOids) + { + bound = get_partition_bound_spec(partoid); + i = foreach_current_index(partoid); + + lower_bounds[i] = make_one_partition_rbound(key, i, bound->lowerdatums, true); + bounds = lappend(bounds, bound); + } + + /* Sort the array of lower bounds. */ + qsort_arg(lower_bounds, nparts, sizeof(PartitionRangeBound *), + qsort_partition_rbound_cmp, key); + + /* Ranges of partitions should be adjacent. */ + for (i = 1; i < nparts; i++) + { + int index = lower_bounds[i]->index; + int prev_index = lower_bounds[i - 1]->index; + + check_two_partitions_bounds_range(parent, + (RangeVar *) list_nth(partNames, prev_index), + (PartitionBoundSpec *) list_nth(bounds, prev_index), + (RangeVar *) list_nth(partNames, index), + (PartitionBoundSpec *) list_nth(bounds, index), + false, + true, + pstate); + } + + /* + * The lower bound of the first partition is the lower bound + * of the merged partition. + */ + spec->lowerdatums = + ((PartitionBoundSpec *) list_nth(bounds, lower_bounds[0]->index))->lowerdatums; + + /* + * The upper bound of the last partition is the upper bound of + * the merged partition. + */ + spec->upperdatums = + ((PartitionBoundSpec *) list_nth(bounds, lower_bounds[nparts - 1]->index))->upperdatums; + + pfree(lower_bounds); + list_free(bounds); + break; + } + + case PARTITION_STRATEGY_LIST: + { + /* Consolidate bounds for all partitions in the list. */ + foreach_oid(partoid, partOids) + { + bound = get_partition_bound_spec(partoid); + spec->listdatums = list_concat(spec->listdatums, bound->listdatums); + } + break; + } + + default: + elog(ERROR, "unexpected partition strategy: %d", + (int) key->strategy); + } +} + +/* + * partitions_listdatum_intersection + * + * (function for BY LIST partitioning) + * + * Function compares lists of values for different partitions. + * Return a list that contains *one* cell that is present in both list1 and + * list2. The returned list is freshly allocated via palloc(), but the + * cells themselves point to the same objects as the cells of the + * input lists. + * + * Currently, there is no need to collect all common partition datums from the + * two lists. + */ +static List * +partitions_listdatum_intersection(FmgrInfo *partsupfunc, Oid *partcollation, + const List *list1, const List *list2) +{ + List *result = NIL; + + if (list1 == NIL || list2 == NIL) + return result; + + foreach_node(Const, val1, list1) + { + bool isnull1 = val1->constisnull; + + foreach_node(Const, val2, list2) + { + if (val2->constisnull) + { + if (isnull1) + { + result = lappend(result, val1); + return result; + } + continue; + } + else if (isnull1) + continue; + + /* Compare two datum values. */ + if (DatumGetInt32(FunctionCall2Coll(&partsupfunc[0], + partcollation[0], + val1->constvalue, + val2->constvalue)) == 0) + { + result = lappend(result, val1); + return result; + } + } + } + + return result; +} + +/* + * check_partitions_not_overlap_list + * + * (function for BY LIST partitioning) + * + * This is a helper function for check_partitions_for_split(). + * Checks that the values of the new partitions do not overlap. + * + * parent: partitioned table + * parts: array of SinglePartitionSpec structs with info about split partitions + * nparts: size of array "parts" + */ +static void +check_partitions_not_overlap_list(Relation parent, + SinglePartitionSpec **parts, + int nparts, + ParseState *pstate) +{ + PartitionKey key PG_USED_FOR_ASSERTS_ONLY = RelationGetPartitionKey(parent); + int i, + j; + SinglePartitionSpec *sps1, + *sps2; + List *overlap; + + Assert(key->strategy == PARTITION_STRATEGY_LIST); + + for (i = 0; i < nparts; i++) + { + sps1 = parts[i]; + + for (j = i + 1; j < nparts; j++) + { + sps2 = parts[j]; + + overlap = partitions_listdatum_intersection(&key->partsupfunc[0], + key->partcollation, + sps1->bound->listdatums, + sps2->bound->listdatums); + if (list_length(overlap) > 0) + { + Const *val = (Const *) linitial_node(Const, overlap); + + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("new partition \"%s\" would overlap with another new partition \"%s\"", + sps1->name->relname, sps2->name->relname), + parser_errposition(pstate, exprLocation((Node *) val))); + } + } + } +} + +/* + * check_partition_bounds_for_split_range + * + * (function for BY RANGE partitioning) + * + * Checks that bounds of new partition "spec" are inside bounds of split + * partition (with Oid splitPartOid). If first=true (this means that "spec" is + * the first of the new partitions), then the lower bound of "spec" should be + * equal (or greater than or equal in case defaultPart=true) to the lower + * bound of the split partition. If last=true (this means that "spec" is the + * last of the new partitions), then the upper bound of "spec" should be + * equal (or less than or equal in case defaultPart=true) to the upper bound + * of the split partition. + * + * parent: partitioned table + * relname: name of the new partition + * spec: bounds specification of the new partition + * splitPartOid: split partition Oid + * first: true iff the new partition "spec" is the first of the + * new partitions + * last: true iff the new partition "spec" is the last of the + * new partitions + * defaultPart: true iff new partitions contain the DEFAULT partition + * pstate: pointer to ParseState struct to determine error position + */ +static void +check_partition_bounds_for_split_range(Relation parent, + char *relname, + PartitionBoundSpec *spec, + Oid splitPartOid, + bool first, + bool last, + bool defaultPart, + ParseState *pstate) +{ + PartitionKey key = RelationGetPartitionKey(parent); + PartitionRangeBound *lower, + *upper; + int cmpval; + + Assert(key->strategy == PARTITION_STRATEGY_RANGE); + Assert(spec->strategy == PARTITION_STRATEGY_RANGE); + + lower = make_one_partition_rbound(key, -1, spec->lowerdatums, true); + upper = make_one_partition_rbound(key, -1, spec->upperdatums, false); + + /* + * First, check if the resulting range would be empty with the specified + * lower and upper bounds. partition_rbound_cmp cannot return zero here, + * since the lower-bound flags are different. + */ + cmpval = partition_rbound_cmp(key->partnatts, + key->partsupfunc, + key->partcollation, + lower->datums, lower->kind, + true, upper); + Assert(cmpval != 0); + if (cmpval > 0) + { + /* Point to the problematic key in the lower datums list. */ + PartitionRangeDatum *datum = list_nth(spec->lowerdatums, cmpval - 1); + + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("empty range bound specified for partition \"%s\"", + relname), + errdetail("Specified lower bound %s is greater than or equal to upper bound %s.", + get_range_partbound_string(spec->lowerdatums), + get_range_partbound_string(spec->upperdatums)), + parser_errposition(pstate, exprLocation((Node *) datum))); + } + + /* + * Need to check first and last partitions (from the set of new + * partitions) + */ + if (first || last) + { + PartitionBoundSpec *split_spec = get_partition_bound_spec(splitPartOid); + PartitionRangeDatum *datum; + + if (first) + { + PartitionRangeBound *split_lower; + + split_lower = make_one_partition_rbound(key, -1, split_spec->lowerdatums, true); + + cmpval = partition_rbound_cmp(key->partnatts, + key->partsupfunc, + key->partcollation, + lower->datums, lower->kind, + true, split_lower); + if (cmpval != 0) + datum = list_nth(spec->lowerdatums, abs(cmpval) - 1); + + /* + * The lower bound of "spec" must equal the lower bound of the + * split partition. However, if one of the new partitions is + * DEFAULT, then it is ok for the new partition's lower bound to + * be greater than that of the split partition. + */ + if (!defaultPart) + { + if (cmpval != 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("lower bound of partition \"%s\" is not equal to lower bound of split partition \"%s\"", + relname, + get_rel_name(splitPartOid)), + errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", + "ALTER TABLE ... SPLIT PARTITION"), + parser_errposition(pstate, exprLocation((Node *) datum))); + } + else if (cmpval < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("lower bound of partition \"%s\" is less than lower bound of split partition \"%s\"", + relname, + get_rel_name(splitPartOid)), + errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", + "ALTER TABLE ... SPLIT PARTITION"), + parser_errposition(pstate, exprLocation((Node *) datum))); + } + else + { + PartitionRangeBound *split_upper; + + split_upper = make_one_partition_rbound(key, -1, split_spec->upperdatums, false); + + cmpval = partition_rbound_cmp(key->partnatts, + key->partsupfunc, + key->partcollation, + upper->datums, upper->kind, + false, split_upper); + if (cmpval != 0) + datum = list_nth(spec->upperdatums, abs(cmpval) - 1); + + /* + * The upper bound of "spec" must equal the upper bound of the + * split partition. However, if one of the new partitions is + * DEFAULT, then it is ok for the new partition's upper bound to + * be less than that of the split partition. + */ + if (!defaultPart) + { + if (cmpval != 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("upper bound of partition \"%s\" is not equal to upper bound of split partition \"%s\"", + relname, + get_rel_name(splitPartOid)), + errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", + "ALTER TABLE ... SPLIT PARTITION"), + parser_errposition(pstate, exprLocation((Node *) datum))); + } + else if (cmpval > 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("upper bound of partition \"%s\" is greater than upper bound of split partition \"%s\"", + relname, + get_rel_name(splitPartOid)), + errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", + "ALTER TABLE ... SPLIT PARTITION"), + parser_errposition(pstate, exprLocation((Node *) datum))); + } + } +} + +/* + * check_partition_bounds_for_split_list + * + * (function for BY LIST partitioning) + * + * Checks that the bounds of the new partition are inside the bounds of the + * split partition (with Oid splitPartOid). + * + * parent: partitioned table + * relname: name of the new partition + * spec: bounds specification of the new partition + * splitPartOid: split partition Oid + * pstate: pointer to ParseState struct to determine error position + */ +static void +check_partition_bounds_for_split_list(Relation parent, char *relname, + PartitionBoundSpec *spec, + Oid splitPartOid, + ParseState *pstate) +{ + PartitionKey key = RelationGetPartitionKey(parent); + PartitionDesc partdesc = RelationGetPartitionDesc(parent, false); + PartitionBoundInfo boundinfo = partdesc->boundinfo; + int with = -1; + bool overlap = false; + int overlap_location = -1; + + Assert(key->strategy == PARTITION_STRATEGY_LIST); + Assert(spec->strategy == PARTITION_STRATEGY_LIST); + Assert(boundinfo && boundinfo->strategy == PARTITION_STRATEGY_LIST); + + /* + * Search each value of the new partition "spec" in the existing + * partitions. All of them should be in the split partition (with Oid + * splitPartOid). + */ + foreach_node(Const, val, spec->listdatums) + { + overlap_location = exprLocation((Node *) val); + if (!val->constisnull) + { + int offset; + bool equal; + + offset = partition_list_bsearch(&key->partsupfunc[0], + key->partcollation, + boundinfo, + val->constvalue, + &equal); + if (offset >= 0 && equal) + { + with = boundinfo->indexes[offset]; + if (partdesc->oids[with] != splitPartOid) + { + overlap = true; + break; + } + } + else + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("new partition \"%s\" cannot have this value because split partition \"%s\" does not have", + relname, + get_rel_name(splitPartOid)), + parser_errposition(pstate, overlap_location)); + } + else if (partition_bound_accepts_nulls(boundinfo)) + { + with = boundinfo->null_index; + if (partdesc->oids[with] != splitPartOid) + { + overlap = true; + break; + } + } + else + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("new partition \"%s\" cannot have NULL value because split partition \"%s\" does not have", + relname, + get_rel_name(splitPartOid)), + parser_errposition(pstate, overlap_location)); + } + + if (overlap) + { + Assert(with >= 0); + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("new partition \"%s\" would overlap with another (not split) partition \"%s\"", + relname, get_rel_name(partdesc->oids[with])), + parser_errposition(pstate, overlap_location)); + } +} + +/* + * find_value_in_new_partitions_list + * + * (function for BY LIST partitioning) + * + * Function returns true iff any of the new partitions contains the value + * "value". + * + * partsupfunc: information about the comparison function associated with + * the partition key + * partcollation: partitioning collation + * parts: pointer to an array with new partition descriptions + * nparts: number of new partitions + * value: the value that we are looking for + * isnull: true if the value that we are looking for is NULL + */ +static bool +find_value_in_new_partitions_list(FmgrInfo *partsupfunc, + Oid *partcollation, + SinglePartitionSpec **parts, + int nparts, + Datum value, + bool isnull) +{ + for (int i = 0; i < nparts; i++) + { + SinglePartitionSpec *sps = parts[i]; + + foreach_node(Const, val, sps->bound->listdatums) + { + if (isnull && val->constisnull) + return true; + + if (!isnull && !val->constisnull) + { + if (DatumGetInt32(FunctionCall2Coll(&partsupfunc[0], + partcollation[0], + val->constvalue, + value)) == 0) + return true; + } + } + } + return false; +} + +/* + * check_parent_values_in_new_partitions + * + * (function for BY LIST partitioning) + * + * Checks that all values of split partition (with Oid partOid) are contained + * in new partitions. + * + * parent: partitioned table + * partOid: split partition Oid + * parts: pointer to an array with new partition descriptions + * nparts: number of new partitions + * pstate: pointer to ParseState struct to determine error position + */ +static void +check_parent_values_in_new_partitions(Relation parent, + Oid partOid, + SinglePartitionSpec **parts, + int nparts, + ParseState *pstate) +{ + PartitionKey key = RelationGetPartitionKey(parent); + PartitionDesc partdesc = RelationGetPartitionDesc(parent, false); + PartitionBoundInfo boundinfo = partdesc->boundinfo; + int i; + bool found = true; + Datum datum = PointerGetDatum(NULL); + + Assert(key->strategy == PARTITION_STRATEGY_LIST); + + /* + * Special processing for NULL value. Search for a NULL value if the split + * partition (partOid) contains it. + */ + if (partition_bound_accepts_nulls(boundinfo) && + partdesc->oids[boundinfo->null_index] == partOid) + { + if (!find_value_in_new_partitions_list(&key->partsupfunc[0], + key->partcollation, parts, nparts, datum, true)) + found = false; + } + + if (!found) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("new partitions combined partition bounds do not contain value (%s) but split partition \"%s\" does", + "NULL", + get_rel_name(partOid)), + errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", + "ALTER TABLE ... SPLIT PARTITION")); + + /* + * Search all values of split partition with partOid in the PartitionDesc + * of partitioned table. + */ + for (i = 0; i < boundinfo->ndatums; i++) + { + if (partdesc->oids[boundinfo->indexes[i]] == partOid) + { + /* We found the value that the split partition contains. */ + datum = boundinfo->datums[i][0]; + if (!find_value_in_new_partitions_list(&key->partsupfunc[0], + key->partcollation, parts, nparts, datum, false)) + { + found = false; + break; + } + } + } + + if (!found) + { + Const *notFoundVal; + + /* + * Make a Const for getting the string representation of the missing + * value. + */ + notFoundVal = makeConst(key->parttypid[0], + key->parttypmod[0], + key->parttypcoll[0], + key->parttyplen[0], + datum, + false, /* isnull */ + key->parttypbyval[0]); + + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("new partitions combined partition bounds do not contain value (%s) but split partition \"%s\" does", + deparse_expression((Node *) notFoundVal, NIL, false, false), + get_rel_name(partOid)), + errhint("%s require combined bounds of new partitions must exactly match the bound of the split partition.", + "ALTER TABLE ... SPLIT PARTITION")); + } +} + +/* + * check_partitions_for_split + * + * Checks new partitions for the SPLIT PARTITION command: + * 1. Bounds of new partitions should not overlap with new and existing + * partitions. + * 2. In the case when new or existing partitions contain the DEFAULT + * partition, new partitions can have any bounds inside the split partition + * bound (can be spaces between partition bounds). + * 3. In case new partitions don't contain the DEFAULT partition and the + * partitioned table does not have the DEFAULT partition, the following + * should be true: the sum of the bounds of new partitions should be equal + & to the bound of the split partition. + * + * parent: partitioned table + * splitPartOid: split partition Oid + * partlist: list of new partitions after partition split + * pstate: pointer to ParseState struct for determine error position + */ +void +check_partitions_for_split(Relation parent, + Oid splitPartOid, + List *partlist, + ParseState *pstate) +{ + PartitionKey key; + char strategy; + Oid defaultPartOid; + bool isSplitPartDefault; + bool createDefaultPart = false; + int default_index = -1; + int i; + SinglePartitionSpec **new_parts; + SinglePartitionSpec *spsPrev = NULL; + + /* + * nparts counts the number of split partitions, but it exclude the + * default partition. + */ + int nparts = 0; + + key = RelationGetPartitionKey(parent); + strategy = get_partition_strategy(key); + + defaultPartOid = + get_default_oid_from_partdesc(RelationGetPartitionDesc(parent, true)); + + Assert(strategy == PARTITION_STRATEGY_RANGE || + strategy == PARTITION_STRATEGY_LIST); + + /* + * Make an array new_parts with new partitions except the DEFAULT + * partition. + */ + new_parts = palloc0_array(SinglePartitionSpec *, list_length(partlist)); + + /* isSplitPartDefault flag: is split partition a DEFAULT partition? */ + isSplitPartDefault = (defaultPartOid == splitPartOid); + + foreach_node(SinglePartitionSpec, sps, partlist) + { + if (sps->bound->is_default) + default_index = foreach_current_index(sps); + else + new_parts[nparts++] = sps; + } + + /* An indicator that the DEFAULT partition will be created. */ + if (default_index != -1) + { + createDefaultPart = true; + Assert(nparts == list_length(partlist) - 1); + } + + if (strategy == PARTITION_STRATEGY_RANGE) + { + PartitionRangeBound **lower_bounds; + SinglePartitionSpec **tmp_new_parts; + + /* + * To simplify the check for ranges of new partitions, we need to sort + * all partitions in ascending order of their bounds (we compare the + * lower bound only). + */ + lower_bounds = palloc0_array(PartitionRangeBound *, nparts); + + /* Create an array of lower bounds. */ + for (i = 0; i < nparts; i++) + { + lower_bounds[i] = make_one_partition_rbound(key, i, + new_parts[i]->bound->lowerdatums, true); + } + + /* Sort the array of lower bounds. */ + qsort_arg(lower_bounds, nparts, sizeof(PartitionRangeBound *), + qsort_partition_rbound_cmp, (void *) key); + + /* Reorder the array of partitions. */ + tmp_new_parts = new_parts; + new_parts = palloc0_array(SinglePartitionSpec *, nparts); + for (i = 0; i < nparts; i++) + new_parts[i] = tmp_new_parts[lower_bounds[i]->index]; + + pfree(tmp_new_parts); + pfree(lower_bounds); + } + + for (i = 0; i < nparts; i++) + { + SinglePartitionSpec *sps = new_parts[i]; + + if (isSplitPartDefault) + { + /* + * When the split partition is the DEFAULT partition, we can use + * any free ranges - as when creating a new partition. + */ + check_new_partition_bound(sps->name->relname, parent, sps->bound, + pstate); + } + else + { + /* + * Checks that the bounds of the current partition are inside the + * bounds of the split partition. For range partitioning: checks + * that the upper bound of the previous partition is equal to the + * lower bound of the current partition. For list partitioning: + * checks that the split partition contains all values of the + * current partition. + */ + if (strategy == PARTITION_STRATEGY_RANGE) + { + bool first = (i == 0); + bool last = (i == (nparts - 1)); + + check_partition_bounds_for_split_range(parent, sps->name->relname, sps->bound, + splitPartOid, first, last, + createDefaultPart, pstate); + } + else + check_partition_bounds_for_split_list(parent, sps->name->relname, + sps->bound, splitPartOid, pstate); + } + + /* Ranges of new partitions should not overlap. */ + if (strategy == PARTITION_STRATEGY_RANGE && spsPrev) + check_two_partitions_bounds_range(parent, spsPrev->name, spsPrev->bound, + sps->name, sps->bound, + createDefaultPart, + false, + pstate); + + spsPrev = sps; + } + + if (strategy == PARTITION_STRATEGY_LIST) + { + /* Values of new partitions should not overlap. */ + check_partitions_not_overlap_list(parent, new_parts, nparts, + pstate); + + /* + * Need to check that all values of the split partition are contained + * in the new partitions. Skip this check if the DEFAULT partition + * exists. + */ + if (!createDefaultPart) + check_parent_values_in_new_partitions(parent, splitPartOid, + new_parts, nparts, pstate); + } + + pfree(new_parts); +} diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c index 328b4d450e451..c3d275f8726be 100644 --- a/src/backend/partitioning/partdesc.c +++ b/src/backend/partitioning/partdesc.c @@ -3,7 +3,7 @@ * partdesc.c * Support routines for manipulating partition descriptors * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -37,7 +37,7 @@ typedef struct PartitionDirectoryData MemoryContext pdir_mcxt; HTAB *pdir_hash; bool omit_detached; -} PartitionDirectoryData; +} PartitionDirectoryData; typedef struct PartitionDirectoryEntry { @@ -426,7 +426,7 @@ CreatePartitionDirectory(MemoryContext mcxt, bool omit_detached) PartitionDirectory pdir; HASHCTL ctl; - pdir = palloc(sizeof(PartitionDirectoryData)); + pdir = palloc_object(PartitionDirectoryData); pdir->pdir_mcxt = mcxt; ctl.keysize = sizeof(Oid); diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c index 48a35f763e906..e7c318bbcacc9 100644 --- a/src/backend/partitioning/partprune.c +++ b/src/backend/partitioning/partprune.c @@ -25,7 +25,7 @@ * * See gen_partprune_steps_internal() for more details on step generation. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -49,6 +49,7 @@ #include "optimizer/cost.h" #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" +#include "optimizer/placeholder.h" #include "parser/parsetree.h" #include "partitioning/partbounds.h" #include "partitioning/partprune.h" @@ -158,7 +159,7 @@ static PartitionPruneStep *gen_prune_step_combine(GeneratePruningStepsContext *c static List *gen_prune_steps_from_opexps(GeneratePruningStepsContext *context, List **keyclauses, Bitmapset *nullkeys); static PartClauseMatchStatus match_clause_to_partition_key(GeneratePruningStepsContext *context, - Expr *clause, Expr *partkey, int partkeyidx, + Expr *clause, const Expr *partkey, int partkeyidx, bool *clause_is_not_null, PartClauseInfo **pc, List **clause_steps); static List *get_steps_using_prefix(GeneratePruningStepsContext *context, @@ -179,13 +180,13 @@ static List *get_steps_using_prefix_recurse(GeneratePruningStepsContext *context List *step_exprs, List *step_cmpfns); static PruneStepResult *get_matching_hash_bounds(PartitionPruneContext *context, - StrategyNumber opstrategy, Datum *values, int nvalues, + StrategyNumber opstrategy, const Datum *values, int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys); static PruneStepResult *get_matching_list_bounds(PartitionPruneContext *context, StrategyNumber opstrategy, Datum value, int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys); static PruneStepResult *get_matching_range_bounds(PartitionPruneContext *context, - StrategyNumber opstrategy, Datum *values, int nvalues, + StrategyNumber opstrategy, const Datum *values, int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys); static Bitmapset *pull_exec_paramids(Expr *expr); static bool pull_exec_paramids_walker(Node *node, Bitmapset **context); @@ -197,7 +198,7 @@ static PruneStepResult *perform_pruning_combine_step(PartitionPruneContext *cont PruneStepResult **step_results); static PartClauseMatchStatus match_boolean_partition_clause(Oid partopfamily, Expr *clause, - Expr *partkey, + const Expr *partkey, Expr **outconst, bool *notclause); static void partkey_datum_from_expr(PartitionPruneContext *context, @@ -246,7 +247,7 @@ make_partition_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel, * that zero can represent an un-filled array entry. */ allpartrelids = NIL; - relid_subplan_map = palloc0(sizeof(int) * root->simple_rel_array_size); + relid_subplan_map = palloc0_array(int, root->simple_rel_array_size); i = 1; foreach(lc, subpaths) @@ -465,7 +466,7 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel, * In this phase we discover whether runtime pruning is needed at all; if * not, we can avoid doing further work. */ - relid_subpart_map = palloc0(sizeof(int) * root->simple_rel_array_size); + relid_subpart_map = palloc0_array(int, root->simple_rel_array_size); i = 1; rti = -1; @@ -818,9 +819,8 @@ prune_append_rel_partitions(RelOptInfo *rel) context.boundinfo = rel->boundinfo; context.partcollation = rel->part_scheme->partcollation; context.partsupfunc = rel->part_scheme->partsupfunc; - context.stepcmpfuncs = (FmgrInfo *) palloc0(sizeof(FmgrInfo) * - context.partnatts * - list_length(pruning_steps)); + context.stepcmpfuncs = palloc0_array(FmgrInfo, + context.partnatts * list_length(pruning_steps)); context.ppccontext = CurrentMemoryContext; /* These are not valid when being called from the planner */ @@ -1814,10 +1814,19 @@ gen_prune_steps_from_opexps(GeneratePruningStepsContext *context, * and couldn't possibly match any other one either, due to its form or * properties (such as containing a volatile function). * Output arguments: none set. + * + * Note that when pulling up a subquery, the clause operands may get wrapped + * in PlaceHolderVars to enforce separate identity or as a result of outer + * joins. We must strip such no-op PlaceHolderVars before comparing operands + * to the partition key, otherwise the equal() checks will fail to recognize + * valid matches. This is safe because the clauses here are always + * relation-scan-level expressions, where a PlaceHolderVar with empty + * phnullingrels is effectively a no-op. Stripping may also bring separate + * RelabelType nodes into adjacency, so we must loop when peeling those. */ static PartClauseMatchStatus match_clause_to_partition_key(GeneratePruningStepsContext *context, - Expr *clause, Expr *partkey, int partkeyidx, + Expr *clause, const Expr *partkey, int partkeyidx, bool *clause_is_not_null, PartClauseInfo **pc, List **clause_steps) { @@ -1890,7 +1899,7 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, return PARTCLAUSE_MATCH_STEPS; } - partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo)); + partclause = palloc_object(PartClauseInfo); partclause->keyno = partkeyidx; /* Do pruning with the Boolean equality operator. */ partclause->opno = BooleanEqualOperator; @@ -1929,10 +1938,12 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, PartClauseInfo *partclause; leftop = (Expr *) get_leftop(clause); - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; rightop = (Expr *) get_rightop(clause); - if (IsA(rightop, RelabelType)) + rightop = (Expr *) strip_noop_phvs((Node *) rightop); + while (IsA(rightop, RelabelType)) rightop = ((RelabelType *) rightop)->arg; opno = opclause->opno; @@ -2147,7 +2158,7 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, /* * Build the clause, passing the negator if applicable. */ - partclause = (PartClauseInfo *) palloc(sizeof(PartClauseInfo)); + partclause = palloc_object(PartClauseInfo); partclause->keyno = partkeyidx; if (is_opne_listp) { @@ -2180,7 +2191,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, *elem_clauses; ListCell *lc1; - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; /* check if the LHS matches this partition key */ @@ -2406,7 +2418,8 @@ match_clause_to_partition_key(GeneratePruningStepsContext *context, NullTest *nulltest = (NullTest *) clause; Expr *arg = nulltest->arg; - if (IsA(arg, RelabelType)) + arg = (Expr *) strip_noop_phvs((Node *) arg); + while (IsA(arg, RelabelType)) arg = ((RelabelType *) arg)->arg; /* Does arg match with this partition key column? */ @@ -2690,10 +2703,10 @@ get_steps_using_prefix_recurse(GeneratePruningStepsContext *context, */ static PruneStepResult * get_matching_hash_bounds(PartitionPruneContext *context, - StrategyNumber opstrategy, Datum *values, int nvalues, + StrategyNumber opstrategy, const Datum *values, int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys) { - PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult)); + PruneStepResult *result = palloc0_object(PruneStepResult); PartitionBoundInfo boundinfo = context->boundinfo; int *partindices = boundinfo->indexes; int partnatts = context->partnatts; @@ -2770,7 +2783,7 @@ get_matching_list_bounds(PartitionPruneContext *context, StrategyNumber opstrategy, Datum value, int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys) { - PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult)); + PruneStepResult *result = palloc0_object(PruneStepResult); PartitionBoundInfo boundinfo = context->boundinfo; int off, minoff, @@ -2881,7 +2894,7 @@ get_matching_list_bounds(PartitionPruneContext *context, case BTGreaterEqualStrategyNumber: inclusive = true; - /* fall through */ + pg_fallthrough; case BTGreaterStrategyNumber: off = partition_list_bsearch(partsupfunc, partcollation, @@ -2916,7 +2929,7 @@ get_matching_list_bounds(PartitionPruneContext *context, case BTLessEqualStrategyNumber: inclusive = true; - /* fall through */ + pg_fallthrough; case BTLessStrategyNumber: off = partition_list_bsearch(partsupfunc, partcollation, @@ -2978,10 +2991,10 @@ get_matching_list_bounds(PartitionPruneContext *context, */ static PruneStepResult * get_matching_range_bounds(PartitionPruneContext *context, - StrategyNumber opstrategy, Datum *values, int nvalues, + StrategyNumber opstrategy, const Datum *values, int nvalues, FmgrInfo *partsupfunc, Bitmapset *nullkeys) { - PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult)); + PruneStepResult *result = palloc0_object(PruneStepResult); PartitionBoundInfo boundinfo = context->boundinfo; Oid *partcollation = context->partcollation; int partnatts = context->partnatts; @@ -3163,7 +3176,7 @@ get_matching_range_bounds(PartitionPruneContext *context, case BTGreaterEqualStrategyNumber: inclusive = true; - /* fall through */ + pg_fallthrough; case BTGreaterStrategyNumber: /* @@ -3244,7 +3257,7 @@ get_matching_range_bounds(PartitionPruneContext *context, case BTLessEqualStrategyNumber: inclusive = true; - /* fall through */ + pg_fallthrough; case BTLessStrategyNumber: /* @@ -3504,7 +3517,7 @@ perform_pruning_base_step(PartitionPruneContext *context, { PruneStepResult *result; - result = (PruneStepResult *) palloc(sizeof(PruneStepResult)); + result = palloc_object(PruneStepResult); result->bound_offsets = NULL; result->scan_default = false; result->scan_null = false; @@ -3593,7 +3606,7 @@ perform_pruning_combine_step(PartitionPruneContext *context, PartitionPruneStepCombine *cstep, PruneStepResult **step_results) { - PruneStepResult *result = (PruneStepResult *) palloc0(sizeof(PruneStepResult)); + PruneStepResult *result = palloc0_object(PruneStepResult); bool firststep; ListCell *lc1; @@ -3698,7 +3711,7 @@ perform_pruning_combine_step(PartitionPruneContext *context, * 'partkey'. */ static PartClauseMatchStatus -match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey, +match_boolean_partition_clause(Oid partopfamily, Expr *clause, const Expr *partkey, Expr **outconst, bool *notclause) { Expr *leftop; @@ -3718,7 +3731,8 @@ match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey, BooleanTest *btest = (BooleanTest *) clause; leftop = btest->arg; - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; if (equal(leftop, partkey)) @@ -3727,19 +3741,19 @@ match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey, { case IS_NOT_TRUE: *notclause = true; - /* fall through */ + pg_fallthrough; case IS_TRUE: *outconst = (Expr *) makeBoolConst(true, false); return PARTCLAUSE_MATCH_CLAUSE; case IS_NOT_FALSE: *notclause = true; - /* fall through */ + pg_fallthrough; case IS_FALSE: *outconst = (Expr *) makeBoolConst(false, false); return PARTCLAUSE_MATCH_CLAUSE; case IS_NOT_UNKNOWN: *notclause = true; - /* fall through */ + pg_fallthrough; case IS_UNKNOWN: return PARTCLAUSE_MATCH_NULLNESS; default: @@ -3755,7 +3769,8 @@ match_boolean_partition_clause(Oid partopfamily, Expr *clause, Expr *partkey, leftop = is_not_clause ? get_notclausearg(clause) : clause; - if (IsA(leftop, RelabelType)) + leftop = (Expr *) strip_noop_phvs((Node *) leftop); + while (IsA(leftop, RelabelType)) leftop = ((RelabelType *) leftop)->arg; /* Compare to the partition key, and make up a clause ... */ diff --git a/src/backend/po/meson.build b/src/backend/po/meson.build index 94ee268564ddd..f30cb10368a0a 100644 --- a/src/backend/po/meson.build +++ b/src/backend/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('postgres-' + pg_version_major.to_string())] diff --git a/src/backend/port/Makefile b/src/backend/port/Makefile index 47338d9922957..8613ac01aff6d 100644 --- a/src/backend/port/Makefile +++ b/src/backend/port/Makefile @@ -22,7 +22,6 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global OBJS = \ - $(TAS) \ atomics.o \ pg_sema.o \ pg_shmem.o @@ -33,16 +32,5 @@ endif include $(top_srcdir)/src/backend/common.mk -tas.o: tas.s -ifeq ($(SUN_STUDIO_CC), yes) -# preprocess assembler file with cpp - $(CC) $(CFLAGS) -c -P $< - mv $*.i $*_cpp.s - $(CC) $(CFLAGS) -c $*_cpp.s -o $@ -else - $(CC) $(CFLAGS) -c $< -endif - clean: - rm -f tas_cpp.s $(MAKE) -C win32 clean diff --git a/src/backend/port/aix/mkldexport.sh b/src/backend/port/aix/mkldexport.sh new file mode 100755 index 0000000000000..9d016e7afd555 --- /dev/null +++ b/src/backend/port/aix/mkldexport.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# +# mkldexport +# create an AIX exports file from an object file +# +# src/backend/port/aix/mkldexport.sh +# +# Usage: +# mkldexport objectfile [location] +# where +# objectfile is the current location of the object file. +# location is the eventual (installed) location of the +# object file (if different from the current +# working directory). +# +# On AIX, executables do not automatically expose their symbols to shared +# modules. Extensions therefore cannot call functions in the main Postgres +# binary unless those symbols are explicitly exported. Unlike other platforms, +# AIX executables are not default symbol providers; each shared module must +# link against an export list that defines which symbols it can use. +# +# The mkldexport.sh script fixes AIX's symbol export issue by generating an +# explicit export list. It uses nm to gather all symbols from the Postgres +# object files, then writes them into the export file. When invoked with ".", +# it outputs #! ., which tells AIX the list applies to the main executable. +# This way, extension modules can link against that list and resolve their +# undefined symbols directly from the Postgres binary. +# + +# Search for the nm command binary. +if [ -x /usr/ucb/nm ] +then NM=/usr/ucb/nm +elif [ -x /usr/bin/nm ] +then NM=/usr/bin/nm +elif [ -x /usr/ccs/bin/nm ] +then NM=/usr/ccs/bin/nm +elif [ -x /usr/usg/bin/nm ] +then NM=/usr/usg/bin/nm +else echo "Fatal error: cannot find `nm' ... please check your installation." + exit 1 +fi + +# instruct nm to process 64-bit objects +export OBJECT_MODE=64 + +CMDNAME=`basename $0` +if [ -z "$1" ]; then + echo "Usage: $CMDNAME object [location]" + exit 1 +fi +OBJNAME=`basename $1` +if [ "`basename $OBJNAME`" != "`basename $OBJNAME .o`" ]; then + OBJNAME=`basename $OBJNAME .o`.so +fi +if [ -z "$2" ]; then + echo '#!' +else + if [ "$2" = "." ]; then + # for the base executable (AIX 4.2 and up) + echo '#! .' + else + echo '#!' $2 + fi +fi +$NM -BCg $1 | \ + egrep ' [TDB] ' | \ + sed -e 's/.* //' | \ + egrep -v '\$' | \ + sed -e 's/^[.]//' | \ + sort | \ + uniq diff --git a/src/backend/port/atomics.c b/src/backend/port/atomics.c index 1a96c6f5799a1..7a300a5c16dab 100644 --- a/src/backend/port/atomics.c +++ b/src/backend/port/atomics.c @@ -3,7 +3,7 @@ * atomics.c * Non-Inline parts of the atomics implementation * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/port/meson.build b/src/backend/port/meson.build index 09d54e01d1338..e8b7da8d281c7 100644 --- a/src/backend/port/meson.build +++ b/src/backend/port/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'atomics.c', diff --git a/src/backend/port/posix_sema.c b/src/backend/port/posix_sema.c index 269c7460817ec..53e4a7a5c38f9 100644 --- a/src/backend/port/posix_sema.c +++ b/src/backend/port/posix_sema.c @@ -15,7 +15,7 @@ * forked backends, but they could not be accessed by exec'd backends. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -159,22 +159,24 @@ PosixSemaphoreKill(sem_t *sem) /* - * Report amount of shared memory needed for semaphores + * Request shared memory needed for semaphores */ -Size -PGSemaphoreShmemSize(int maxSemas) +void +PGSemaphoreShmemRequest(int maxSemas) { #ifdef USE_NAMED_POSIX_SEMAPHORES /* No shared memory needed in this case */ - return 0; #else /* Need a PGSemaphoreData per semaphore */ - return mul_size(maxSemas, sizeof(PGSemaphoreData)); + ShmemRequestStruct(.name = "Semaphores", + .size = mul_size(maxSemas, sizeof(PGSemaphoreData)), + .ptr = (void **) &sharedSemas, + ); #endif } /* - * PGReserveSemaphores --- initialize semaphore support + * PGSemaphoreInit --- initialize semaphore support * * This is called during postmaster start or shared memory reinitialization. * It should do whatever is needed to be able to support up to maxSemas @@ -193,7 +195,7 @@ PGSemaphoreShmemSize(int maxSemas) * we don't have to expose the counters to other processes.) */ void -PGReserveSemaphores(int maxSemas) +PGSemaphoreInit(int maxSemas) { struct stat statbuf; @@ -213,14 +215,6 @@ PGReserveSemaphores(int maxSemas) mySemPointers = (sem_t **) malloc(maxSemas * sizeof(sem_t *)); if (mySemPointers == NULL) elog(PANIC, "out of memory"); -#else - - /* - * We must use ShmemAllocUnlocked(), since the spinlock protecting - * ShmemAlloc() won't be ready yet. - */ - sharedSemas = (PGSemaphore) - ShmemAllocUnlocked(PGSemaphoreShmemSize(maxSemas)); #endif numSems = 0; diff --git a/src/backend/port/sysv_sema.c b/src/backend/port/sysv_sema.c index 423b2b4f9d6d1..98d99515043b6 100644 --- a/src/backend/port/sysv_sema.c +++ b/src/backend/port/sysv_sema.c @@ -4,7 +4,7 @@ * Implement PGSemaphores using SysV semaphore facilities * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -69,7 +69,7 @@ static int nextSemaNumber; /* next free sem num in last sema set */ static IpcSemaphoreId InternalIpcSemaphoreCreate(IpcSemaphoreKey semKey, - int numSems); + int numSems, bool retry_ok); static void IpcSemaphoreInitialize(IpcSemaphoreId semId, int semNum, int value); static void IpcSemaphoreKill(IpcSemaphoreId semId); @@ -88,9 +88,13 @@ static void ReleaseSemaphores(int status, Datum arg); * If we fail with a failure code other than collision-with-existing-set, * print out an error and abort. Other types of errors suggest nonrecoverable * problems. + * + * Unfortunately, it's sometimes hard to tell whether errors are + * nonrecoverable. Our caller keeps track of whether continuing to retry + * is sane or not; if not, we abort on failure regardless of the errno. */ static IpcSemaphoreId -InternalIpcSemaphoreCreate(IpcSemaphoreKey semKey, int numSems) +InternalIpcSemaphoreCreate(IpcSemaphoreKey semKey, int numSems, bool retry_ok) { int semId; @@ -101,16 +105,27 @@ InternalIpcSemaphoreCreate(IpcSemaphoreKey semKey, int numSems) int saved_errno = errno; /* - * Fail quietly if error indicates a collision with existing set. One - * would expect EEXIST, given that we said IPC_EXCL, but perhaps we - * could get a permission violation instead? Also, EIDRM might occur - * if an old set is slated for destruction but not gone yet. + * Fail quietly if error suggests a collision with an existing set and + * our caller has not lost patience. + * + * One would expect EEXIST, given that we said IPC_EXCL, but perhaps + * we could get a permission violation instead. On some platforms + * EINVAL will be reported if the existing set has too few semaphores. + * Also, EIDRM might occur if an old set is slated for destruction but + * not gone yet. + * + * EINVAL is the key reason why we need the caller-level loop limit, + * as it can also mean that the platform's SEMMSL is less than + * numSems, and that condition can't be fixed by trying another key. */ - if (saved_errno == EEXIST || saved_errno == EACCES + if (retry_ok && + (saved_errno == EEXIST + || saved_errno == EACCES + || saved_errno == EINVAL #ifdef EIDRM - || saved_errno == EIDRM + || saved_errno == EIDRM #endif - ) + )) return -1; /* @@ -207,17 +222,22 @@ IpcSemaphoreGetLastPID(IpcSemaphoreId semId, int semNum) static IpcSemaphoreId IpcSemaphoreCreate(int numSems) { + int num_tries = 0; IpcSemaphoreId semId; union semun semun; PGSemaphoreData mysema; /* Loop till we find a free IPC key */ - for (nextSemaKey++;; nextSemaKey++) + for (nextSemaKey++;; nextSemaKey++, num_tries++) { pid_t creatorPID; - /* Try to create new semaphore set */ - semId = InternalIpcSemaphoreCreate(nextSemaKey, numSems + 1); + /* + * Try to create new semaphore set. Give up after trying 1000 + * distinct IPC keys. + */ + semId = InternalIpcSemaphoreCreate(nextSemaKey, numSems + 1, + num_tries < 1000); if (semId >= 0) break; /* successful create */ @@ -254,7 +274,7 @@ IpcSemaphoreCreate(int numSems) /* * Now try again to create the sema set. */ - semId = InternalIpcSemaphoreCreate(nextSemaKey, numSems + 1); + semId = InternalIpcSemaphoreCreate(nextSemaKey, numSems + 1, true); if (semId >= 0) break; /* successful create */ @@ -281,16 +301,20 @@ IpcSemaphoreCreate(int numSems) /* - * Report amount of shared memory needed for semaphores + * Request shared memory needed for semaphores */ -Size -PGSemaphoreShmemSize(int maxSemas) +void +PGSemaphoreShmemRequest(int maxSemas) { - return mul_size(maxSemas, sizeof(PGSemaphoreData)); + /* Need a PGSemaphoreData per semaphore */ + ShmemRequestStruct(.name = "Semaphores", + .size = mul_size(maxSemas, sizeof(PGSemaphoreData)), + .ptr = (void **) &sharedSemas, + ); } /* - * PGReserveSemaphores --- initialize semaphore support + * PGSemaphoreInit --- initialize semaphore support * * This is called during postmaster start or shared memory reinitialization. * It should do whatever is needed to be able to support up to maxSemas @@ -307,7 +331,7 @@ PGSemaphoreShmemSize(int maxSemas) * have clobbered.) */ void -PGReserveSemaphores(int maxSemas) +PGSemaphoreInit(int maxSemas) { struct stat statbuf; @@ -323,12 +347,6 @@ PGReserveSemaphores(int maxSemas) errmsg("could not stat data directory \"%s\": %m", DataDir))); - /* - * We must use ShmemAllocUnlocked(), since the spinlock protecting - * ShmemAlloc() won't be ready yet. - */ - sharedSemas = (PGSemaphore) - ShmemAllocUnlocked(PGSemaphoreShmemSize(maxSemas)); numSharedSemas = 0; maxSharedSemas = maxSemas; diff --git a/src/backend/port/sysv_shmem.c b/src/backend/port/sysv_shmem.c index 197926d44f6bc..2e3886cf9fe49 100644 --- a/src/backend/port/sysv_shmem.c +++ b/src/backend/port/sysv_shmem.c @@ -9,7 +9,7 @@ * exist, though, because mmap'd shmem provides no way to find out how * many processes are attached, which we need for interlocking purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -34,6 +34,7 @@ #include "storage/fd.h" #include "storage/ipc.h" #include "storage/pg_shmem.h" +#include "storage/shmem.h" #include "utils/guc.h" #include "utils/guc_hooks.h" #include "utils/pidfile.h" @@ -206,7 +207,7 @@ InternalIpcMemoryCreate(IpcMemoryKey memKey, Size size) */ if (shmctl(shmid, IPC_RMID, NULL) < 0) elog(LOG, "shmctl(%d, %d, 0) failed: %m", - (int) shmid, IPC_RMID); + shmid, IPC_RMID); } } @@ -601,6 +602,7 @@ CreateAnonymousSegment(Size *size) Size allocsize = *size; void *ptr = MAP_FAILED; int mmap_errno = 0; + int mmap_flags = MAP_SHARED | MAP_ANONYMOUS | MAP_HASSEMAPHORE; #ifndef MAP_HUGETLB /* PGSharedMemoryCreate should have dealt with this case */ @@ -612,15 +614,15 @@ CreateAnonymousSegment(Size *size) * Round up the request size to a suitable large value. */ Size hugepagesize; - int mmap_flags; + int huge_mmap_flags; - GetHugePageSize(&hugepagesize, &mmap_flags); + GetHugePageSize(&hugepagesize, &huge_mmap_flags); if (allocsize % hugepagesize != 0) - allocsize += hugepagesize - (allocsize % hugepagesize); + allocsize = add_size(allocsize, hugepagesize - (allocsize % hugepagesize)); ptr = mmap(NULL, allocsize, PROT_READ | PROT_WRITE, - PG_MMAP_FLAGS | mmap_flags, -1, 0); + mmap_flags | huge_mmap_flags, -1, 0); mmap_errno = errno; if (huge_pages == HUGE_PAGES_TRY && ptr == MAP_FAILED) elog(DEBUG1, "mmap(%zu) with MAP_HUGETLB failed, huge pages disabled: %m", @@ -644,7 +646,7 @@ CreateAnonymousSegment(Size *size) */ allocsize = *size; ptr = mmap(NULL, allocsize, PROT_READ | PROT_WRITE, - PG_MMAP_FLAGS, -1, 0); + mmap_flags, -1, 0); mmap_errno = errno; } @@ -853,7 +855,7 @@ PGSharedMemoryCreate(Size size, * Initialize space allocation status for segment. */ hdr->totalsize = size; - hdr->freeoffset = MAXALIGN(sizeof(PGShmemHeader)); + hdr->content_offset = MAXALIGN(sizeof(PGShmemHeader)); *shim = hdr; /* Save info for possible future use */ diff --git a/src/backend/port/tas/sunstudio_sparc.s b/src/backend/port/tas/sunstudio_sparc.s deleted file mode 100644 index 8e0a0965b64ea..0000000000000 --- a/src/backend/port/tas/sunstudio_sparc.s +++ /dev/null @@ -1,53 +0,0 @@ -!------------------------------------------------------------------------- -! -! sunstudio_sparc.s -! compare and swap for Sun Studio on Sparc -! -! Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group -! Portions Copyright (c) 1994, Regents of the University of California -! -! IDENTIFICATION -! src/backend/port/tas/sunstudio_sparc.s -! -!------------------------------------------------------------------------- - -! Fortunately the Sun compiler can process cpp conditionals with -P - -! '/' is the comment for x86, while '!' is the comment for Sparc - -#if defined(__sparcv9) || defined(__sparc) - - .section ".text" - .align 8 - .skip 24 - .align 4 - - .global pg_atomic_cas -pg_atomic_cas: - - ! "cas" only works on sparcv9 and sparcv8plus chips, and - ! requires a compiler targeting these CPUs. It will fail - ! on a compiler targeting sparcv8, and of course will not - ! be understood by a sparcv8 CPU. gcc continues to use - ! "ldstub" because it targets sparcv7. - ! - ! There is actually a trick for embedding "cas" in a - ! sparcv8-targeted compiler, but it can only be run - ! on a sparcv8plus/v9 cpus: - ! - ! http://cvs.opensolaris.org/source/xref/on/usr/src/lib/libc/sparc/threads/sparc.il - ! - ! NB: We're assuming we're running on a TSO system here - solaris - ! userland luckily always has done so. - -#if defined(__sparcv9) || defined(__sparcv8plus) - cas [%o0],%o2,%o1 -#else - ldstub [%o0],%o1 -#endif - mov %o1,%o0 - retl - nop - .type pg_atomic_cas,2 - .size pg_atomic_cas,(.-pg_atomic_cas) -#endif diff --git a/src/backend/port/tas/sunstudio_x86.s b/src/backend/port/tas/sunstudio_x86.s deleted file mode 100644 index 0111ffde45c29..0000000000000 --- a/src/backend/port/tas/sunstudio_x86.s +++ /dev/null @@ -1,43 +0,0 @@ -/------------------------------------------------------------------------- -/ -/ sunstudio_x86.s -/ compare and swap for Sun Studio on x86 -/ -/ Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group -/ Portions Copyright (c) 1994, Regents of the University of California -/ -/ IDENTIFICATION -/ src/backend/port/tas/sunstudio_x86.s -/ -/------------------------------------------------------------------------- - -/ Fortunately the Sun compiler can process cpp conditionals with -P - -/ '/' is the comment for x86, while '!' is the comment for Sparc - - .file "tas.s" - -#if defined(__amd64) - .code64 -#endif - - .globl pg_atomic_cas - .type pg_atomic_cas, @function - - .section .text, "ax" - .align 16 - -pg_atomic_cas: -#if defined(__amd64) - movl %edx,%eax - lock - cmpxchgl %esi,(%rdi) -#else - movl 4(%esp), %edx - movl 8(%esp), %ecx - movl 12(%esp), %eax - lock - cmpxchgl %ecx, (%edx) -#endif - ret - .size pg_atomic_cas, . - pg_atomic_cas diff --git a/src/backend/port/win32/crashdump.c b/src/backend/port/win32/crashdump.c index 5a8048be025a3..0efeb9a923605 100644 --- a/src/backend/port/win32/crashdump.c +++ b/src/backend/port/win32/crashdump.c @@ -28,7 +28,7 @@ * be added, though at the cost of a greater chance of the crash dump failing. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/port/win32/crashdump.c @@ -38,21 +38,7 @@ #include "postgres.h" -/* - * Some versions of the MS SDK contain "typedef enum { ... } ;" which the MS - * compiler quite sanely complains about. Well done, Microsoft. - * This pragma disables the warning just while we include the header. - * The pragma is known to work with all (as at the time of writing) supported - * versions of MSVC. - */ -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable : 4091) -#endif #include -#ifdef _MSC_VER -#pragma warning(pop) -#endif /* * Much of the following code is based on CodeProject and MSDN examples, diff --git a/src/backend/port/win32/meson.build b/src/backend/port/win32/meson.build index af80071387932..5199202df33b7 100644 --- a/src/backend/port/win32/meson.build +++ b/src/backend/port/win32/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'crashdump.c', diff --git a/src/backend/port/win32/signal.c b/src/backend/port/win32/signal.c index d051b15c0ddb3..f002542803739 100644 --- a/src/backend/port/win32/signal.c +++ b/src/backend/port/win32/signal.c @@ -3,7 +3,7 @@ * signal.c * Microsoft Windows Win32 Signal Emulation Functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/port/win32/signal.c @@ -88,7 +88,7 @@ pgwin32_signal_initialize(void) pg_signal_array[i].sa_handler = SIG_DFL; pg_signal_array[i].sa_mask = 0; pg_signal_array[i].sa_flags = 0; - pg_signal_defaults[i] = SIG_IGN; + pg_signal_defaults[i] = PG_SIG_IGN; } pg_signal_mask = 0; pg_signal_queue = 0; @@ -134,15 +134,19 @@ pgwin32_dispatch_queued_signals(void) { /* Execute this signal */ struct sigaction *act = &pg_signal_array[i]; - pqsigfunc sig = act->sa_handler; + pqsigfunc sig = (pqsigfunc) (pg_funcptr_t) act->sa_handler; - if (sig == SIG_DFL) + if (sig == PG_SIG_DFL) sig = pg_signal_defaults[i]; pg_signal_queue &= ~sigmask(i); - if (sig != SIG_ERR && sig != SIG_IGN && sig != SIG_DFL) + if (sig != (pqsigfunc) (pg_funcptr_t) SIG_ERR && sig != PG_SIG_IGN && sig != PG_SIG_DFL) { sigset_t block_mask; sigset_t save_mask; + struct pg_signal_info nodata; + + nodata.pid = 0; + nodata.uid = 0; LeaveCriticalSection(&pg_signal_crit_sec); @@ -151,7 +155,7 @@ pgwin32_dispatch_queued_signals(void) block_mask |= sigmask(i); sigprocmask(SIG_BLOCK, &block_mask, &save_mask); - sig(i); + sig(i, &nodata); sigprocmask(SIG_SETMASK, &save_mask, NULL); EnterCriticalSection(&pg_signal_crit_sec); diff --git a/src/backend/port/win32/socket.c b/src/backend/port/win32/socket.c index a8538afe68446..3aaf971e97342 100644 --- a/src/backend/port/win32/socket.c +++ b/src/backend/port/win32/socket.c @@ -3,7 +3,7 @@ * socket.c * Microsoft Windows Win32 Socket Functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/port/win32/socket.c diff --git a/src/backend/port/win32/timer.c b/src/backend/port/win32/timer.c index 4843e8bd498c6..751327ef03ec3 100644 --- a/src/backend/port/win32/timer.c +++ b/src/backend/port/win32/timer.c @@ -8,7 +8,7 @@ * - Does not support interval timer (value->it_interval) * - Only supports ITIMER_REAL * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/port/win32/timer.c diff --git a/src/backend/port/win32_sema.c b/src/backend/port/win32_sema.c index 5854ad1f54d3d..a320255476970 100644 --- a/src/backend/port/win32_sema.c +++ b/src/backend/port/win32_sema.c @@ -3,7 +3,7 @@ * win32_sema.c * Microsoft Windows Win32 Semaphores Emulation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/port/win32_sema.c @@ -25,17 +25,16 @@ static void ReleaseSemaphores(int code, Datum arg); /* - * Report amount of shared memory needed for semaphores + * Request shared memory needed for semaphores */ -Size -PGSemaphoreShmemSize(int maxSemas) +void +PGSemaphoreShmemRequest(int maxSemas) { /* No shared memory needed on Windows */ - return 0; } /* - * PGReserveSemaphores --- initialize semaphore support + * PGSemaphoreInit --- initialize semaphore support * * In the Win32 implementation, we acquire semaphores on-demand; the * maxSemas parameter is just used to size the array that keeps track of @@ -44,7 +43,7 @@ PGSemaphoreShmemSize(int maxSemas) * process exits. */ void -PGReserveSemaphores(int maxSemas) +PGSemaphoreInit(int maxSemas) { mySemSet = (HANDLE *) malloc(maxSemas * sizeof(HANDLE)); if (mySemSet == NULL) diff --git a/src/backend/port/win32_shmem.c b/src/backend/port/win32_shmem.c index 4dee856d6bd69..794e4fcb2ad43 100644 --- a/src/backend/port/win32_shmem.c +++ b/src/backend/port/win32_shmem.c @@ -3,7 +3,7 @@ * win32_shmem.c * Implement shared memory using win32 facilities * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/port/win32_shmem.c @@ -389,7 +389,7 @@ PGSharedMemoryCreate(Size size, * Initialize space allocation status for segment. */ hdr->totalsize = size; - hdr->freeoffset = MAXALIGN(sizeof(PGShmemHeader)); + hdr->content_offset = MAXALIGN(sizeof(PGShmemHeader)); hdr->dsm_control = 0; /* Save info for possible future use */ diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile index 0f4435d2d97c7..55044b2bc6f71 100644 --- a/src/backend/postmaster/Makefile +++ b/src/backend/postmaster/Makefile @@ -18,6 +18,7 @@ OBJS = \ bgworker.o \ bgwriter.o \ checkpointer.o \ + datachecksum_state.o \ fork_process.o \ interrupt.o \ launch_backend.o \ diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 981be42e3afc8..a5a8db2ff8879 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -51,7 +51,7 @@ * holding the relation lock) during which a worker may choose a table that was * already vacuumed; this is a bug in the current design. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -62,6 +62,7 @@ */ #include "postgres.h" +#include #include #include #include @@ -77,9 +78,9 @@ #include "catalog/namespace.h" #include "catalog/pg_database.h" #include "catalog/pg_namespace.h" -#include "commands/dbcommands.h" #include "commands/vacuum.h" #include "common/int.h" +#include "funcapi.h" #include "lib/ilist.h" #include "libpq/pqsignal.h" #include "miscadmin.h" @@ -91,12 +92,14 @@ #include "storage/aio_subsys.h" #include "storage/bufmgr.h" #include "storage/ipc.h" +#include "storage/fd.h" #include "storage/latch.h" #include "storage/lmgr.h" #include "storage/pmsignal.h" #include "storage/proc.h" #include "storage/procsignal.h" #include "storage/smgr.h" +#include "storage/subsystems.h" #include "tcop/tcopprot.h" #include "utils/fmgroids.h" #include "utils/fmgrprotos.h" @@ -110,6 +113,8 @@ #include "utils/syscache.h" #include "utils/timeout.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" +#include "utils/wait_event.h" /* @@ -129,11 +134,16 @@ int autovacuum_anl_thresh; double autovacuum_anl_scale; int autovacuum_freeze_max_age; int autovacuum_multixact_freeze_max_age; - +double autovacuum_freeze_score_weight = 1.0; +double autovacuum_multixact_freeze_score_weight = 1.0; +double autovacuum_vacuum_score_weight = 1.0; +double autovacuum_vacuum_insert_score_weight = 1.0; +double autovacuum_analyze_score_weight = 1.0; double autovacuum_vac_cost_delay; int autovacuum_vac_cost_limit; int Log_autovacuum_min_duration = 600000; +int Log_autoanalyze_min_duration = 600000; /* the minimum allowed time between two awakenings of the launcher */ #define MIN_AUTOVAC_SLEEPTIME 100.0 /* milliseconds */ @@ -204,7 +214,6 @@ typedef struct autovac_table double at_storage_param_vac_cost_delay; int at_storage_param_vac_cost_limit; bool at_dobalance; - bool at_sharedrel; char *at_relname; char *at_nspname; char *at_datname; @@ -303,6 +312,14 @@ typedef struct static AutoVacuumShmemStruct *AutoVacuumShmem; +static void AutoVacuumShmemRequest(void *arg); +static void AutoVacuumShmemInit(void *arg); + +const ShmemCallbacks AutoVacuumShmemCallbacks = { + .request_fn = AutoVacuumShmemRequest, + .init_fn = AutoVacuumShmemInit, +}; + /* * the database list (of avl_dbase elements) in the launcher, and the context * that contains it @@ -310,12 +327,43 @@ static AutoVacuumShmemStruct *AutoVacuumShmem; static dlist_head DatabaseList = DLIST_STATIC_INIT(DatabaseList); static MemoryContext DatabaseListCxt = NULL; +/* + * This struct is used by relation_needs_vacanalyze() to return the table's + * score (i.e., the maximum of the component scores) as well as the component + * scores themselves. + */ +typedef struct +{ + double max; /* maximum of all values below */ + double xid; /* transaction ID component */ + double mxid; /* multixact ID component */ + double vac; /* vacuum component */ + double vac_ins; /* vacuum insert component */ + double anl; /* analyze component */ +} AutoVacuumScores; + +/* + * This struct is used to track and sort the list of tables to process. + */ +typedef struct +{ + Oid oid; + double score; +} TableToProcess; + +/* + * Dummy pointer to persuade Valgrind that we've not leaked the array of + * avl_dbase structs. Make it global to ensure the compiler doesn't + * optimize it away. + */ +#ifdef USE_VALGRIND +extern avl_dbase *avl_dbase_array; +avl_dbase *avl_dbase_array; +#endif + /* Pointer to my own WorkerInfo, valid on each worker */ static WorkerInfo MyWorkerInfo = NULL; -/* PID of launcher, valid only in worker while shutting down */ -int AutovacuumLauncherPid = 0; - static Oid do_start_worker(void); static void ProcessAutoVacLauncherInterrupts(void); pg_noreturn static void AutoVacLauncherShutdown(void); @@ -333,15 +381,12 @@ static void FreeWorkerInfo(int code, Datum arg); static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map, TupleDesc pg_class_desc, int effective_multixact_freeze_max_age); -static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts, - Form_pg_class classForm, - int effective_multixact_freeze_max_age, - bool *dovacuum, bool *doanalyze, bool *wraparound); static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts, Form_pg_class classForm, - PgStat_StatTabEntry *tabentry, int effective_multixact_freeze_max_age, - bool *dovacuum, bool *doanalyze, bool *wraparound); + int elevel, + bool *dovacuum, bool *doanalyze, bool *wraparound, + AutoVacuumScores *scores); static void autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy); @@ -378,7 +423,6 @@ AutoVacLauncherMain(const void *startup_data, size_t startup_data_len) PostmasterContext = NULL; } - MyBackendType = B_AUTOVAC_LAUNCHER; init_ps_display(NULL); ereport(DEBUG1, @@ -401,11 +445,11 @@ AutoVacLauncherMain(const void *startup_data, size_t startup_data_len) InitializeTimeouts(); /* establishes SIGALRM handler */ - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, avl_sigusr2_handler); pqsignal(SIGFPE, FloatExceptionHandler); - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * Create a per-backend PGPROC struct in shared memory. We must do this @@ -562,10 +606,10 @@ AutoVacLauncherMain(const void *startup_data, size_t startup_data_len) /* * Create the initial database list. The invariant we want this list to - * keep is that it's ordered by decreasing next_time. As soon as an entry - * is updated to a higher time, it will be moved to the front (which is - * correct because the only operation is to add autovacuum_naptime to the - * entry, and time always increases). + * keep is that it's ordered by decreasing next_worker. As soon as an + * entry is updated to a higher time, it will be moved to the front (which + * is correct because the only operation is to add autovacuum_naptime to + * the entry, and time always increases). */ rebuild_database_list(InvalidOid); @@ -1020,6 +1064,10 @@ rebuild_database_list(Oid newdb) /* put all the hash elements into an array */ dbary = palloc(nelems * sizeof(avl_dbase)); + /* keep Valgrind quiet */ +#ifdef USE_VALGRIND + avl_dbase_array = dbary; +#endif i = 0; hash_seq_init(&seq, dbhash); @@ -1387,7 +1435,6 @@ AutoVacWorkerMain(const void *startup_data, size_t startup_data_len) PostmasterContext = NULL; } - MyBackendType = B_AUTOVAC_WORKER; init_ps_display(NULL); Assert(GetProcessingMode() == InitProcessing); @@ -1409,11 +1456,11 @@ AutoVacWorkerMain(const void *startup_data, size_t startup_data_len) InitializeTimeouts(); /* establishes SIGALRM handler */ - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); + pqsignal(SIGUSR2, PG_SIG_IGN); pqsignal(SIGFPE, FloatExceptionHandler); - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * Create a per-backend PGPROC struct in shared memory. We must do this @@ -1590,11 +1637,6 @@ AutoVacWorkerMain(const void *startup_data, size_t startup_data_len) do_autovacuum(); } - /* - * The launcher will be notified of my death in ProcKill, *if* we managed - * to get a worker slot at all - */ - /* All done, go away */ proc_exit(0); } @@ -1609,20 +1651,6 @@ FreeWorkerInfo(int code, Datum arg) { LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE); - /* - * Wake the launcher up so that he can launch a new worker immediately - * if required. We only save the launcher's PID in local memory here; - * the actual signal will be sent when the PGPROC is recycled. Note - * that we always do this, so that the launcher can rebalance the cost - * limit setting of the remaining workers. - * - * We somewhat ignore the risk that the launcher changes its PID - * between us reading it and the actual kill; we expect ProcKill to be - * called shortly after us, and we assume that PIDs are not reused too - * quickly after a process exits. - */ - AutovacuumLauncherPid = AutoVacuumShmem->av_launcherpid; - dlist_delete(&MyWorkerInfo->wi_links); MyWorkerInfo->wi_dboid = InvalidOid; MyWorkerInfo->wi_tableoid = InvalidOid; @@ -1667,7 +1695,7 @@ VacuumUpdateCosts(void) } else { - /* Must be explicit VACUUM or ANALYZE */ + /* Must be explicit VACUUM or ANALYZE or parallel autovacuum worker */ vacuum_cost_delay = VacuumCostDelay; vacuum_cost_limit = VacuumCostLimit; } @@ -1851,7 +1879,7 @@ get_database_list(void) */ oldcxt = MemoryContextSwitchTo(resultcxt); - avdb = (avw_dbase *) palloc(sizeof(avw_dbase)); + avdb = palloc_object(avw_dbase); avdb->adw_datid = pgdatabase->oid; avdb->adw_name = pstrdup(NameStr(pgdatabase->datname)); @@ -1875,6 +1903,19 @@ get_database_list(void) return dblist; } +/* + * List comparator for TableToProcess. Note that this sorts the tables based + * on their scores in descending order. + */ +static int +TableToProcessComparator(const ListCell *a, const ListCell *b) +{ + TableToProcess *t1 = (TableToProcess *) lfirst(a); + TableToProcess *t2 = (TableToProcess *) lfirst(b); + + return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0; +} + /* * Process a database table-by-table * @@ -1888,7 +1929,7 @@ do_autovacuum(void) HeapTuple tuple; TableScanDesc relScan; Form_pg_database dbForm; - List *table_oids = NIL; + List *tables_to_process = NIL; List *orphan_oids = NIL; HASHCTL ctl; HTAB *table_toast_map; @@ -1922,8 +1963,8 @@ do_autovacuum(void) /* * Compute the multixact age for which freezing is urgent. This is - * normally autovacuum_multixact_freeze_max_age, but may be less if we are - * short of multixact member space. + * normally autovacuum_multixact_freeze_max_age, but may be less if + * multixact members are bloated. */ effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold(); @@ -1994,12 +2035,12 @@ do_autovacuum(void) while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL) { Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple); - PgStat_StatTabEntry *tabentry; AutoVacOpts *relopts; Oid relid; bool dovacuum; bool doanalyze; bool wraparound; + AutoVacuumScores scores; if (classForm->relkind != RELKIND_RELATION && classForm->relkind != RELKIND_MATVIEW) @@ -2034,17 +2075,23 @@ do_autovacuum(void) /* Fetch reloptions and the pgstat entry for this table */ relopts = extract_autovac_opts(tuple, pg_class_desc); - tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared, - relid); /* Check if it needs vacuum or analyze */ - relation_needs_vacanalyze(relid, relopts, classForm, tabentry, + relation_needs_vacanalyze(relid, relopts, classForm, effective_multixact_freeze_max_age, - &dovacuum, &doanalyze, &wraparound); + DEBUG3, + &dovacuum, &doanalyze, &wraparound, + &scores); - /* Relations that need work are added to table_oids */ + /* Relations that need work are added to tables_to_process */ if (dovacuum || doanalyze) - table_oids = lappend_oid(table_oids, relid); + { + TableToProcess *table = palloc_object(TableToProcess); + + table->oid = relid; + table->score = scores.max; + tables_to_process = lappend(tables_to_process, table); + } /* * Remember TOAST associations for the second pass. Note: we must do @@ -2077,8 +2124,6 @@ do_autovacuum(void) /* Release stuff to avoid per-relation leakage */ if (relopts) pfree(relopts); - if (tabentry) - pfree(tabentry); } table_endscan(relScan); @@ -2093,13 +2138,13 @@ do_autovacuum(void) while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL) { Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple); - PgStat_StatTabEntry *tabentry; Oid relid; AutoVacOpts *relopts; bool free_relopts = false; bool dovacuum; bool doanalyze; bool wraparound; + AutoVacuumScores scores; /* * We cannot safely process other backends' temp tables, so skip 'em. @@ -2126,23 +2171,25 @@ do_autovacuum(void) relopts = &hentry->ar_reloptions; } - /* Fetch the pgstat entry for this table */ - tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared, - relid); - - relation_needs_vacanalyze(relid, relopts, classForm, tabentry, + relation_needs_vacanalyze(relid, relopts, classForm, effective_multixact_freeze_max_age, - &dovacuum, &doanalyze, &wraparound); + DEBUG3, + &dovacuum, &doanalyze, &wraparound, + &scores); /* ignore analyze for toast tables */ if (dovacuum) - table_oids = lappend_oid(table_oids, relid); + { + TableToProcess *table = palloc_object(TableToProcess); + + table->oid = relid; + table->score = scores.max; + tables_to_process = lappend(tables_to_process, table); + } /* Release stuff to avoid leakage */ if (free_relopts) pfree(relopts); - if (tabentry) - pfree(tabentry); } table_endscan(relScan); @@ -2234,6 +2281,12 @@ do_autovacuum(void) get_namespace_name(classForm->relnamespace), NameStr(classForm->relname)))); + /* + * Deletion might involve TOAST table access, so ensure we have a + * valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + object.classId = RelationRelationId; object.objectId = relid; object.objectSubId = 0; @@ -2246,6 +2299,7 @@ do_autovacuum(void) * To commit the deletion, end current transaction and start a new * one. Note this also releases the locks we took. */ + PopActiveSnapshot(); CommitTransactionCommand(); StartTransactionCommand(); @@ -2253,6 +2307,19 @@ do_autovacuum(void) MemoryContextSwitchTo(AutovacMemCxt); } + /* + * In case list_sort() would modify the list even when all the scores are + * 0.0, skip sorting if all the weight parameters are set to 0.0. This is + * probably not necessary, but we want to ensure folks have a guaranteed + * escape hatch from the scoring system. + */ + if (autovacuum_freeze_score_weight != 0.0 || + autovacuum_multixact_freeze_score_weight != 0.0 || + autovacuum_vacuum_score_weight != 0.0 || + autovacuum_vacuum_insert_score_weight != 0.0 || + autovacuum_analyze_score_weight != 0.0) + list_sort(tables_to_process, TableToProcessComparator); + /* * Optionally, create a buffer access strategy object for VACUUM to use. * We use the same BufferAccessStrategy object for all tables VACUUMed by @@ -2281,9 +2348,9 @@ do_autovacuum(void) /* * Perform operations on collected tables. */ - foreach(cell, table_oids) + foreach_ptr(TableToProcess, table, tables_to_process) { - Oid relid = lfirst_oid(cell); + Oid relid = table->oid; HeapTuple classTup; autovac_table *tab; bool isshared; @@ -2514,7 +2581,7 @@ do_autovacuum(void) pg_atomic_test_set_flag(&MyWorkerInfo->wi_dobalance); } - list_free(table_oids); + list_free_deep(tables_to_process); /* * Perform additional work items, as requested by backends. @@ -2535,7 +2602,10 @@ do_autovacuum(void) workitem->avw_active = true; LWLockRelease(AutovacuumLock); + PushActiveSnapshot(GetTransactionSnapshot()); perform_work_item(workitem); + if (ActiveSnapshotSet()) /* transaction could have aborted */ + PopActiveSnapshot(); /* * Check for config changes before acquiring lock for further jobs. @@ -2558,8 +2628,18 @@ do_autovacuum(void) /* * We leak table_toast_map here (among other things), but since we're - * going away soon, it's not a problem. + * going away soon, it's not a problem normally. But when using Valgrind, + * release some stuff to reduce complaints about leaked storage. */ +#ifdef USE_VALGRIND + hash_destroy(table_toast_map); + FreeTupleDesc(pg_class_desc); + if (bstrategy) + pfree(bstrategy); +#endif + + /* Run the rest in xact context, mainly to avoid Valgrind leak warnings */ + MemoryContextSwitchTo(TopTransactionContext); /* * Update pg_database.datfrozenxid, and truncate pg_xact if possible. We @@ -2719,7 +2799,7 @@ extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc) if (relopts == NULL) return NULL; - av = palloc(sizeof(AutoVacOpts)); + av = palloc_object(AutoVacOpts); memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts)); pfree(relopts); @@ -2748,6 +2828,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, bool wraparound; AutoVacOpts *avopts; bool free_avopts = false; + AutoVacuumScores scores; /* fetch the relation's relcache entry */ classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); @@ -2773,9 +2854,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, avopts = &hentry->ar_reloptions; } - recheck_relation_needs_vacanalyze(relid, avopts, classForm, - effective_multixact_freeze_max_age, - &dovacuum, &doanalyze, &wraparound); + relation_needs_vacanalyze(relid, avopts, classForm, + effective_multixact_freeze_max_age, + DEBUG3, + &dovacuum, &doanalyze, &wraparound, + &scores); /* OK, it needs something done */ if (doanalyze || dovacuum) @@ -2784,7 +2867,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, int freeze_table_age; int multixact_freeze_min_age; int multixact_freeze_table_age; - int log_min_duration; + int log_vacuum_min_duration; + int log_analyze_min_duration; /* * Calculate the vacuum cost parameters and the freeze ages. If there @@ -2794,10 +2878,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, */ /* -1 in autovac setting means use log_autovacuum_min_duration */ - log_min_duration = (avopts && avopts->log_min_duration >= 0) - ? avopts->log_min_duration + log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0) + ? avopts->log_vacuum_min_duration : Log_autovacuum_min_duration; + /* -1 in autovac setting means use log_autoanalyze_min_duration */ + log_analyze_min_duration = (avopts && avopts->log_analyze_min_duration >= 0) + ? avopts->log_analyze_min_duration + : Log_autoanalyze_min_duration; + /* these do not have autovacuum-specific settings */ freeze_min_age = (avopts && avopts->freeze_min_age >= 0) ? avopts->freeze_min_age @@ -2817,9 +2906,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, ? avopts->multixact_freeze_table_age : default_multixact_freeze_table_age; - tab = palloc(sizeof(autovac_table)); + tab = palloc_object(autovac_table); tab->at_relid = relid; - tab->at_sharedrel = classForm->relisshared; /* * Select VACUUM options. Note we don't say VACOPT_PROCESS_TOAST, so @@ -2840,16 +2928,36 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, */ tab->at_params.index_cleanup = VACOPTVALUE_UNSPECIFIED; tab->at_params.truncate = VACOPTVALUE_UNSPECIFIED; - /* As of now, we don't support parallel vacuum for autovacuum */ - tab->at_params.nworkers = -1; tab->at_params.freeze_min_age = freeze_min_age; tab->at_params.freeze_table_age = freeze_table_age; tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age; tab->at_params.multixact_freeze_table_age = multixact_freeze_table_age; tab->at_params.is_wraparound = wraparound; - tab->at_params.log_min_duration = log_min_duration; + tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration; + tab->at_params.log_analyze_min_duration = log_analyze_min_duration; tab->at_params.toast_parent = InvalidOid; + /* Determine the number of parallel vacuum workers to use */ + tab->at_params.nworkers = 0; + if (avopts) + { + if (avopts->autovacuum_parallel_workers == 0) + { + /* + * Disable parallel vacuum, if the reloption sets the parallel + * degree as zero. + */ + tab->at_params.nworkers = -1; + } + else if (avopts->autovacuum_parallel_workers > 0) + tab->at_params.nworkers = avopts->autovacuum_parallel_workers; + + /* + * autovacuum_parallel_workers == -1 falls through, keep + * nworkers=0 + */ + } + /* * Later, in vacuum_rel(), we check reloptions for any * vacuum_max_eager_freeze_failure_rate override. @@ -2878,42 +2986,6 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, return tab; } -/* - * recheck_relation_needs_vacanalyze - * - * Subroutine for table_recheck_autovac. - * - * Fetch the pgstat of a relation and recheck whether a relation - * needs to be vacuumed or analyzed. - */ -static void -recheck_relation_needs_vacanalyze(Oid relid, - AutoVacOpts *avopts, - Form_pg_class classForm, - int effective_multixact_freeze_max_age, - bool *dovacuum, - bool *doanalyze, - bool *wraparound) -{ - PgStat_StatTabEntry *tabentry; - - /* fetch the pgstat table entry */ - tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared, - relid); - - relation_needs_vacanalyze(relid, avopts, classForm, tabentry, - effective_multixact_freeze_max_age, - dovacuum, doanalyze, wraparound); - - /* Release tabentry to avoid leakage */ - if (tabentry) - pfree(tabentry); - - /* ignore ANALYZE for toast tables */ - if (classForm->relkind == RELKIND_TOASTVALUE) - *doanalyze = false; -} - /* * relation_needs_vacanalyze * @@ -2923,8 +2995,7 @@ recheck_relation_needs_vacanalyze(Oid relid, * * relopts is a pointer to the AutoVacOpts options (either for itself in the * case of a plain table, or for either itself or its parent table in the case - * of a TOAST table), NULL if none; tabentry is the pgstats entry, which can be - * NULL. + * of a TOAST table), NULL if none. * * A table needs to be vacuumed if the number of dead tuples exceeds a * threshold. This threshold is calculated as @@ -2952,20 +3023,64 @@ recheck_relation_needs_vacanalyze(Oid relid, * autovacuum_vacuum_threshold GUC variable. Similarly, a vac_scale_factor * value < 0 is substituted with the value of * autovacuum_vacuum_scale_factor GUC variable. Ditto for analyze. + * + * This function also returns scores that can be used to sort the list of + * tables to process. The idea is to have autovacuum prioritize tables that + * are furthest beyond their thresholds (e.g., a table nearing transaction ID + * wraparound should be vacuumed first). This prioritization scheme is + * certainly far from perfect; there are simply too many possibilities for any + * scoring technique to work across all workloads, and the situation might + * change significantly between the time we calculate the score and the time + * that autovacuum processes it. However, we have attempted to develop + * something that is expected to work for a large portion of workloads with + * reasonable parameter settings. + * + * The autovacuum table score is calculated as the maximum of the ratios of + * each of the table's relevant values to its threshold. For example, if the + * number of inserted tuples is 100, and the insert threshold for the table is + * 80, the insert score is 1.25. If all other scores are below that value, the + * returned score will be 1.25. The other criteria considered for the score + * are the table ages (both relfrozenxid and relminmxid) compared to the + * corresponding freeze-max-age setting, the number of updated/deleted tuples + * compared to the vacuum threshold, and the number of inserted/updated/deleted + * tuples compared to the analyze threshold. + * + * One exception to the previous paragraph is for tables nearing wraparound, + * i.e., those that have surpassed the effective failsafe ages. In that case, + * the relfrozenxid/relminmxid-based score is scaled aggressively so that the + * table has a decent chance of sorting to the front of the list. + * + * To adjust how strongly each component contributes to the score, the + * following parameters can be adjusted from their default of 1.0 to anywhere + * between 0.0 and 10.0 (inclusive). Setting all of these to 0.0 restores + * pre-v19 prioritization behavior: + * + * autovacuum_freeze_score_weight + * autovacuum_multixact_freeze_score_weight + * autovacuum_vacuum_score_weight + * autovacuum_vacuum_insert_score_weight + * autovacuum_analyze_score_weight + * + * The autovacuum table score is returned in scores->max. The component scores + * are also returned in the "scores" argument via the other members of the + * AutoVacuumScores struct. */ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts, Form_pg_class classForm, - PgStat_StatTabEntry *tabentry, int effective_multixact_freeze_max_age, + int elevel, /* output params below */ bool *dovacuum, bool *doanalyze, - bool *wraparound) + bool *wraparound, + AutoVacuumScores *scores) { + PgStat_StatTabEntry *tabentry; bool force_vacuum; bool av_enabled; + bool may_free = false; /* constants from reloptions or GUC variables */ int vac_base_thresh, @@ -2991,11 +3106,25 @@ relation_needs_vacanalyze(Oid relid, int multixact_freeze_max_age; TransactionId xidForceLimit; TransactionId relfrozenxid; + MultiXactId relminmxid; MultiXactId multiForceLimit; + uint32 xid_age; + uint32 mxid_age; + int effective_xid_failsafe_age; + int effective_mxid_failsafe_age; + + float4 pcnt_unfrozen = 1; + float4 reltuples = classForm->reltuples; + int32 relpages = classForm->relpages; + int32 relallfrozen = classForm->relallfrozen; Assert(classForm != NULL); Assert(OidIsValid(relid)); + memset(scores, 0, sizeof(AutoVacuumScores)); + *dovacuum = false; + *doanalyze = false; + /* * Determine vacuum/analyze equation parameters. We have two possible * sources: the passed reloptions (which could be a main table or a toast @@ -3042,18 +3171,19 @@ relation_needs_vacanalyze(Oid relid, : effective_multixact_freeze_max_age; av_enabled = (relopts ? relopts->enabled : true); + av_enabled &= AutoVacuumingActive(); + + relfrozenxid = classForm->relfrozenxid; + relminmxid = classForm->relminmxid; /* Force vacuum if table is at risk of wraparound */ xidForceLimit = recentXid - freeze_max_age; if (xidForceLimit < FirstNormalTransactionId) xidForceLimit -= FirstNormalTransactionId; - relfrozenxid = classForm->relfrozenxid; force_vacuum = (TransactionIdIsNormal(relfrozenxid) && TransactionIdPrecedes(relfrozenxid, xidForceLimit)); if (!force_vacuum) { - MultiXactId relminmxid = classForm->relminmxid; - multiForceLimit = recentMulti - multixact_freeze_max_age; if (multiForceLimit < FirstMultiXactId) multiForceLimit -= FirstMultiXactId; @@ -3062,13 +3192,52 @@ relation_needs_vacanalyze(Oid relid, } *wraparound = force_vacuum; - /* User disabled it in pg_class.reloptions? (But ignore if at risk) */ - if (!av_enabled && !force_vacuum) - { - *doanalyze = false; - *dovacuum = false; - return; - } + /* + * To calculate the (M)XID age portion of the score, divide the age by its + * respective *_freeze_max_age parameter. + */ + xid_age = TransactionIdIsNormal(relfrozenxid) ? recentXid - relfrozenxid : 0; + mxid_age = MultiXactIdIsValid(relminmxid) ? recentMulti - relminmxid : 0; + + scores->xid = (double) xid_age / freeze_max_age; + scores->mxid = (double) mxid_age / multixact_freeze_max_age; + + /* + * To ensure tables are given increased priority once they begin + * approaching wraparound, we scale the score aggressively if the ages + * surpass vacuum_failsafe_age or vacuum_multixact_failsafe_age. + * + * As in vacuum_xid_failsafe_check(), the effective failsafe age is no + * less than 105% the value of the respective *_freeze_max_age parameter. + * Note that per-table settings could result in a low score even if the + * table surpasses the failsafe settings. However, this is a strange + * enough corner case that we don't bother trying to handle it. + * + * We further adjust the effective failsafe ages with the weight + * parameters so that increasing them lowers the ages at which we begin + * scaling aggressively. + */ + effective_xid_failsafe_age = Max(vacuum_failsafe_age, + autovacuum_freeze_max_age * 1.05); + effective_mxid_failsafe_age = Max(vacuum_multixact_failsafe_age, + autovacuum_multixact_freeze_max_age * 1.05); + + if (autovacuum_freeze_score_weight > 1.0) + effective_xid_failsafe_age /= autovacuum_freeze_score_weight; + if (autovacuum_multixact_freeze_score_weight > 1.0) + effective_mxid_failsafe_age /= autovacuum_multixact_freeze_score_weight; + + if (xid_age >= effective_xid_failsafe_age) + scores->xid = pow(scores->xid, Max(1.0, (double) xid_age / 100000000)); + if (mxid_age >= effective_mxid_failsafe_age) + scores->mxid = pow(scores->mxid, Max(1.0, (double) mxid_age / 100000000)); + + scores->xid *= autovacuum_freeze_score_weight; + scores->mxid *= autovacuum_multixact_freeze_score_weight; + + scores->max = Max(scores->xid, scores->mxid); + if (force_vacuum) + *dovacuum = true; /* * If we found stats for the table, and autovacuum is currently enabled, @@ -3077,79 +3246,91 @@ relation_needs_vacanalyze(Oid relid, * vacuuming only, so don't vacuum (or analyze) anything that's not being * forced. */ - if (PointerIsValid(tabentry) && AutoVacuumingActive()) - { - float4 pcnt_unfrozen = 1; - float4 reltuples = classForm->reltuples; - int32 relpages = classForm->relpages; - int32 relallfrozen = classForm->relallfrozen; + tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared, + relid, &may_free); + if (!tabentry) + return; - vactuples = tabentry->dead_tuples; - instuples = tabentry->ins_since_vacuum; - anltuples = tabentry->mod_since_analyze; + vactuples = tabentry->dead_tuples; + instuples = tabentry->ins_since_vacuum; + anltuples = tabentry->mod_since_analyze; - /* If the table hasn't yet been vacuumed, take reltuples as zero */ - if (reltuples < 0) - reltuples = 0; + /* If the table hasn't yet been vacuumed, take reltuples as zero */ + if (reltuples < 0) + reltuples = 0; + /* + * If we have data for relallfrozen, calculate the unfrozen percentage of + * the table to modify insert scale factor. This helps us decide whether + * or not to vacuum an insert-heavy table based on the number of inserts + * to the more "active" part of the table. + */ + if (relpages > 0 && relallfrozen > 0) + { /* - * If we have data for relallfrozen, calculate the unfrozen percentage - * of the table to modify insert scale factor. This helps us decide - * whether or not to vacuum an insert-heavy table based on the number - * of inserts to the more "active" part of the table. + * It could be the stats were updated manually and relallfrozen > + * relpages. Clamp relallfrozen to relpages to avoid nonsensical + * calculations. */ - if (relpages > 0 && relallfrozen > 0) - { - /* - * It could be the stats were updated manually and relallfrozen > - * relpages. Clamp relallfrozen to relpages to avoid nonsensical - * calculations. - */ - relallfrozen = Min(relallfrozen, relpages); - pcnt_unfrozen = 1 - ((float4) relallfrozen / relpages); - } + relallfrozen = Min(relallfrozen, relpages); + pcnt_unfrozen = 1 - ((float4) relallfrozen / relpages); + } - vacthresh = (float4) vac_base_thresh + vac_scale_factor * reltuples; - if (vac_max_thresh >= 0 && vacthresh > (float4) vac_max_thresh) - vacthresh = (float4) vac_max_thresh; + vacthresh = (float4) vac_base_thresh + vac_scale_factor * reltuples; + if (vac_max_thresh >= 0 && vacthresh > (float4) vac_max_thresh) + vacthresh = (float4) vac_max_thresh; - vacinsthresh = (float4) vac_ins_base_thresh + - vac_ins_scale_factor * reltuples * pcnt_unfrozen; - anlthresh = (float4) anl_base_thresh + anl_scale_factor * reltuples; + vacinsthresh = (float4) vac_ins_base_thresh + + vac_ins_scale_factor * reltuples * pcnt_unfrozen; + anlthresh = (float4) anl_base_thresh + anl_scale_factor * reltuples; - /* - * Note that we don't need to take special consideration for stat - * reset, because if that happens, the last vacuum and analyze counts - * will be reset too. - */ - if (vac_ins_base_thresh >= 0) - elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: %.0f (threshold %.0f), anl: %.0f (threshold %.0f)", - NameStr(classForm->relname), - vactuples, vacthresh, instuples, vacinsthresh, anltuples, anlthresh); - else - elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: (disabled), anl: %.0f (threshold %.0f)", - NameStr(classForm->relname), - vactuples, vacthresh, anltuples, anlthresh); - - /* Determine if this table needs vacuum or analyze. */ - *dovacuum = force_vacuum || (vactuples > vacthresh) || - (vac_ins_base_thresh >= 0 && instuples > vacinsthresh); - *doanalyze = (anltuples > anlthresh); + /* Determine if this table needs vacuum, and update the score. */ + scores->vac = (double) vactuples / Max(vacthresh, 1); + scores->vac *= autovacuum_vacuum_score_weight; + scores->max = Max(scores->max, scores->vac); + if (av_enabled && vactuples > vacthresh) + *dovacuum = true; + + if (vac_ins_base_thresh >= 0) + { + scores->vac_ins = (double) instuples / Max(vacinsthresh, 1); + scores->vac_ins *= autovacuum_vacuum_insert_score_weight; + scores->max = Max(scores->max, scores->vac_ins); + if (av_enabled && instuples > vacinsthresh) + *dovacuum = true; } - else + + /* + * Determine if this table needs analyze, and update the score. Note that + * we don't analyze TOAST tables and pg_statistic. + */ + if (relid != StatisticRelationId && + classForm->relkind != RELKIND_TOASTVALUE) { - /* - * Skip a table not found in stat hash, unless we have to force vacuum - * for anti-wrap purposes. If it's not acted upon, there's no need to - * vacuum it. - */ - *dovacuum = force_vacuum; - *doanalyze = false; + scores->anl = (double) anltuples / Max(anlthresh, 1); + scores->anl *= autovacuum_analyze_score_weight; + scores->max = Max(scores->max, scores->anl); + if (av_enabled && anltuples > anlthresh) + *doanalyze = true; } - /* ANALYZE refuses to work with pg_statistic */ - if (relid == StatisticRelationId) - *doanalyze = false; + if (vac_ins_base_thresh >= 0) + elog(elevel, "%s: vac: %.0f (thresh %.0f, score %.2f), ins: %.0f (thresh %.0f, score %.2f), anl: %.0f (thresh %.0f, score %.2f), xid score: %.2f, mxid score: %.2f", + NameStr(classForm->relname), + vactuples, vacthresh, scores->vac, + instuples, vacinsthresh, scores->vac_ins, + anltuples, anlthresh, scores->anl, + scores->xid, scores->mxid); + else + elog(elevel, "%s: vac: %.0f (thresh %.0f, score %.2f), ins: (disabled), anl: %.0f (thresh %.0f, score %.2f), xid score: %.2f, mxid score: %.2f", + NameStr(classForm->relname), + vactuples, vacthresh, scores->vac, + anltuples, anlthresh, scores->anl, + scores->xid, scores->mxid); + + /* Avoid leaking pgstat entries until the end of autovacuum. */ + if (may_free) + pfree(tabentry); } /* @@ -3342,11 +3523,11 @@ autovac_init(void) } /* - * AutoVacuumShmemSize - * Compute space needed for autovacuum-related shared memory + * AutoVacuumShmemRequest + * Register shared memory space needed for autovacuum */ -Size -AutoVacuumShmemSize(void) +static void +AutoVacuumShmemRequest(void *arg) { Size size; @@ -3357,53 +3538,41 @@ AutoVacuumShmemSize(void) size = MAXALIGN(size); size = add_size(size, mul_size(autovacuum_worker_slots, sizeof(WorkerInfoData))); - return size; + + ShmemRequestStruct(.name = "AutoVacuum Data", + .size = size, + .ptr = (void **) &AutoVacuumShmem, + ); } /* * AutoVacuumShmemInit - * Allocate and initialize autovacuum-related shared memory + * Initialize autovacuum-related shared memory */ -void -AutoVacuumShmemInit(void) +static void +AutoVacuumShmemInit(void *arg) { - bool found; - - AutoVacuumShmem = (AutoVacuumShmemStruct *) - ShmemInitStruct("AutoVacuum Data", - AutoVacuumShmemSize(), - &found); + WorkerInfo worker; - if (!IsUnderPostmaster) - { - WorkerInfo worker; - int i; - - Assert(!found); - - AutoVacuumShmem->av_launcherpid = 0; - dclist_init(&AutoVacuumShmem->av_freeWorkers); - dlist_init(&AutoVacuumShmem->av_runningWorkers); - AutoVacuumShmem->av_startingWorker = NULL; - memset(AutoVacuumShmem->av_workItems, 0, - sizeof(AutoVacuumWorkItem) * NUM_WORKITEMS); - - worker = (WorkerInfo) ((char *) AutoVacuumShmem + - MAXALIGN(sizeof(AutoVacuumShmemStruct))); - - /* initialize the WorkerInfo free list */ - for (i = 0; i < autovacuum_worker_slots; i++) - { - dclist_push_head(&AutoVacuumShmem->av_freeWorkers, - &worker[i].wi_links); - pg_atomic_init_flag(&worker[i].wi_dobalance); - } + AutoVacuumShmem->av_launcherpid = 0; + dclist_init(&AutoVacuumShmem->av_freeWorkers); + dlist_init(&AutoVacuumShmem->av_runningWorkers); + AutoVacuumShmem->av_startingWorker = NULL; + memset(AutoVacuumShmem->av_workItems, 0, + sizeof(AutoVacuumWorkItem) * NUM_WORKITEMS); - pg_atomic_init_u32(&AutoVacuumShmem->av_nworkersForBalance, 0); + worker = (WorkerInfo) ((char *) AutoVacuumShmem + + MAXALIGN(sizeof(AutoVacuumShmemStruct))); + /* initialize the WorkerInfo free list */ + for (int i = 0; i < autovacuum_worker_slots; i++) + { + dclist_push_head(&AutoVacuumShmem->av_freeWorkers, + &worker[i].wi_links); + pg_atomic_init_flag(&worker[i].wi_dobalance); } - else - Assert(found); + + pg_atomic_init_u32(&AutoVacuumShmem->av_nworkersForBalance, 0); } /* @@ -3463,3 +3632,75 @@ check_av_worker_gucs(void) errdetail("The server will only start up to \"autovacuum_worker_slots\" (%d) autovacuum workers at a given time.", autovacuum_worker_slots))); } + +/* + * pg_stat_get_autovacuum_scores + * + * Returns current autovacuum scores for all relevant tables in the current + * database. + */ +Datum +pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS) +{ + int effective_multixact_freeze_max_age; + Relation rel; + TableScanDesc scan; + HeapTuple tup; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + InitMaterializedSRF(fcinfo, 0); + + /* some prerequisite initialization */ + effective_multixact_freeze_max_age = MultiXactMemberFreezeThreshold(); + recentXid = ReadNextTransactionId(); + recentMulti = ReadNextMultiXactId(); + + /* scan pg_class */ + rel = table_open(RelationRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 0, NULL); + while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL) + { + Form_pg_class form = (Form_pg_class) GETSTRUCT(tup); + AutoVacOpts *avopts; + bool dovacuum; + bool doanalyze; + bool wraparound; + AutoVacuumScores scores; + Datum vals[10]; + bool nulls[10] = {false}; + + /* skip ineligible entries */ + if (form->relkind != RELKIND_RELATION && + form->relkind != RELKIND_MATVIEW && + form->relkind != RELKIND_TOASTVALUE) + continue; + if (form->relpersistence == RELPERSISTENCE_TEMP) + continue; + + avopts = extract_autovac_opts(tup, RelationGetDescr(rel)); + relation_needs_vacanalyze(form->oid, avopts, form, + effective_multixact_freeze_max_age, + LOG_NEVER, + &dovacuum, &doanalyze, &wraparound, + &scores); + if (avopts) + pfree(avopts); + + vals[0] = ObjectIdGetDatum(form->oid); + vals[1] = Float8GetDatum(scores.max); + vals[2] = Float8GetDatum(scores.xid); + vals[3] = Float8GetDatum(scores.mxid); + vals[4] = Float8GetDatum(scores.vac); + vals[5] = Float8GetDatum(scores.vac_ins); + vals[6] = Float8GetDatum(scores.anl); + vals[7] = BoolGetDatum(dovacuum); + vals[8] = BoolGetDatum(doanalyze); + vals[9] = BoolGetDatum(wraparound); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, vals, nulls); + } + table_endscan(scan); + table_close(rel, AccessShareLock); + + return (Datum) 0; +} diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c index a6d3630398f4d..8fdc518b3a1e6 100644 --- a/src/backend/postmaster/auxprocess.c +++ b/src/backend/postmaster/auxprocess.c @@ -3,7 +3,7 @@ * functions related to auxiliary processes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -15,6 +15,7 @@ #include #include +#include "access/xlog.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/auxprocess.h" @@ -24,6 +25,7 @@ #include "storage/procsignal.h" #include "utils/memutils.h" #include "utils/ps_status.h" +#include "utils/wait_event.h" static void ShutdownAuxiliaryProcess(int code, Datum arg); @@ -68,6 +70,24 @@ AuxiliaryProcessMainCommon(void) ProcSignalInit(NULL, 0); + /* + * Initialize a local cache of the data_checksum_version, to be updated by + * the procsignal-based barriers. + * + * This intentionally happens after initializing the procsignal, otherwise + * we might miss a state change. This means we can get a barrier for the + * state we've just initialized - but it can happen only once. + * + * The postmaster (which is what gets forked into the new child process) + * does not handle barriers, therefore it may not have the current value + * of LocalDataChecksumVersion value (it'll have the value read from the + * control file, which may be arbitrarily old). + * + * NB: Even if the postmaster handled barriers, the value might still be + * stale, as it might have changed after this process forked. + */ + InitLocalDataChecksumState(); + /* * Auxiliary processes don't run transactions, but they may need a * resource owner anyway to manage buffer pins acquired outside diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c index 116ddf7b835f1..2e4acad4f005c 100644 --- a/src/backend/postmaster/bgworker.c +++ b/src/backend/postmaster/bgworker.c @@ -2,7 +2,7 @@ * bgworker.c * POSTGRES pluggable background workers implementation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/postmaster/bgworker.c @@ -13,11 +13,13 @@ #include "postgres.h" #include "access/parallel.h" +#include "commands/repack.h" #include "libpq/pqsignal.h" #include "miscadmin.h" #include "pgstat.h" #include "port/atomics.h" #include "postmaster/bgworker_internals.h" +#include "postmaster/datachecksum_state.h" #include "postmaster/postmaster.h" #include "replication/logicallauncher.h" #include "replication/logicalworker.h" @@ -26,13 +28,16 @@ #include "storage/lwlock.h" #include "storage/pmsignal.h" #include "storage/proc.h" +#include "storage/procarray.h" #include "storage/procsignal.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "tcop/tcopprot.h" #include "utils/ascii.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/timeout.h" +#include "utils/wait_event.h" /* * The postmaster's list of registered background workers, in private memory. @@ -107,6 +112,14 @@ struct BackgroundWorkerHandle static BackgroundWorkerArray *BackgroundWorkerData; +static void BackgroundWorkerShmemRequest(void *arg); +static void BackgroundWorkerShmemInit(void *arg); + +const ShmemCallbacks BackgroundWorkerShmemCallbacks = { + .request_fn = BackgroundWorkerShmemRequest, + .init_fn = BackgroundWorkerShmemInit, +}; + /* * List of internal background worker entry points. We need this for * reasons explained in LookupBackgroundWorkerFunction(), below. @@ -119,19 +132,40 @@ static const struct { { - "ParallelWorkerMain", ParallelWorkerMain + .fn_name = "ApplyLauncherMain", + .fn_addr = ApplyLauncherMain + }, + { + .fn_name = "ApplyWorkerMain", + .fn_addr = ApplyWorkerMain + }, + { + .fn_name = "ParallelApplyWorkerMain", + .fn_addr = ParallelApplyWorkerMain + }, + { + .fn_name = "ParallelWorkerMain", + .fn_addr = ParallelWorkerMain + }, + { + .fn_name = "RepackWorkerMain", + .fn_addr = RepackWorkerMain }, { - "ApplyLauncherMain", ApplyLauncherMain + .fn_name = "SequenceSyncWorkerMain", + .fn_addr = SequenceSyncWorkerMain }, { - "ApplyWorkerMain", ApplyWorkerMain + .fn_name = "TableSyncWorkerMain", + .fn_addr = TableSyncWorkerMain }, { - "ParallelApplyWorkerMain", ParallelApplyWorkerMain + .fn_name = "DataChecksumsWorkerLauncherMain", + .fn_addr = DataChecksumsWorkerLauncherMain }, { - "TablesyncWorkerMain", TablesyncWorkerMain + .fn_name = "DataChecksumsWorkerMain", + .fn_addr = DataChecksumsWorkerMain } }; @@ -140,10 +174,10 @@ static bgworker_main_type LookupBackgroundWorkerFunction(const char *libraryname /* - * Calculate shared memory needed. + * Register shared memory needed for background workers. */ -Size -BackgroundWorkerShmemSize(void) +static void +BackgroundWorkerShmemRequest(void *arg) { Size size; @@ -151,66 +185,58 @@ BackgroundWorkerShmemSize(void) size = offsetof(BackgroundWorkerArray, slot); size = add_size(size, mul_size(max_worker_processes, sizeof(BackgroundWorkerSlot))); - - return size; + ShmemRequestStruct(.name = "Background Worker Data", + .size = size, + .ptr = (void **) &BackgroundWorkerData, + ); } /* - * Initialize shared memory. + * Initialize shared memory for background workers. */ -void -BackgroundWorkerShmemInit(void) +static void +BackgroundWorkerShmemInit(void *arg) { - bool found; - - BackgroundWorkerData = ShmemInitStruct("Background Worker Data", - BackgroundWorkerShmemSize(), - &found); - if (!IsUnderPostmaster) - { - dlist_iter iter; - int slotno = 0; + dlist_iter iter; + int slotno = 0; - BackgroundWorkerData->total_slots = max_worker_processes; - BackgroundWorkerData->parallel_register_count = 0; - BackgroundWorkerData->parallel_terminate_count = 0; + BackgroundWorkerData->total_slots = max_worker_processes; + BackgroundWorkerData->parallel_register_count = 0; + BackgroundWorkerData->parallel_terminate_count = 0; - /* - * Copy contents of worker list into shared memory. Record the shared - * memory slot assigned to each worker. This ensures a 1-to-1 - * correspondence between the postmaster's private list and the array - * in shared memory. - */ - dlist_foreach(iter, &BackgroundWorkerList) - { - BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno]; - RegisteredBgWorker *rw; + /* + * Copy contents of worker list into shared memory. Record the shared + * memory slot assigned to each worker. This ensures a 1-to-1 + * correspondence between the postmaster's private list and the array in + * shared memory. + */ + dlist_foreach(iter, &BackgroundWorkerList) + { + BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno]; + RegisteredBgWorker *rw; - rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur); - Assert(slotno < max_worker_processes); - slot->in_use = true; - slot->terminate = false; - slot->pid = InvalidPid; - slot->generation = 0; - rw->rw_shmem_slot = slotno; - rw->rw_worker.bgw_notify_pid = 0; /* might be reinit after crash */ - memcpy(&slot->worker, &rw->rw_worker, sizeof(BackgroundWorker)); - ++slotno; - } + rw = dlist_container(RegisteredBgWorker, rw_lnode, iter.cur); + Assert(slotno < max_worker_processes); + slot->in_use = true; + slot->terminate = false; + slot->pid = InvalidPid; + slot->generation = 0; + rw->rw_shmem_slot = slotno; + rw->rw_worker.bgw_notify_pid = 0; /* might be reinit after crash */ + memcpy(&slot->worker, &rw->rw_worker, sizeof(BackgroundWorker)); + ++slotno; + } - /* - * Mark any remaining slots as not in use. - */ - while (slotno < max_worker_processes) - { - BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno]; + /* + * Mark any remaining slots as not in use. + */ + while (slotno < max_worker_processes) + { + BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno]; - slot->in_use = false; - ++slotno; - } + slot->in_use = false; + ++slotno; } - else - Assert(found); } /* @@ -613,6 +639,7 @@ ResetBackgroundWorkerCrashTimes(void) * resetting. */ rw->rw_crashed_at = 0; + rw->rw_pid = 0; /* * If there was anyone waiting for it, they're history. @@ -661,6 +688,17 @@ SanityCheckBackgroundWorker(BackgroundWorker *worker, int elevel) /* XXX other checks? */ } + /* Interruptible workers require a database connection */ + if ((worker->bgw_flags & BGWORKER_INTERRUPTIBLE) && + !(worker->bgw_flags & BGWORKER_BACKEND_DATABASE_CONNECTION)) + { + ereport(elevel, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("background worker \"%s\": cannot make background workers interruptible without database access", + worker->bgw_name))); + return false; + } + if ((worker->bgw_restart_time < 0 && worker->bgw_restart_time != BGW_NEVER_RESTART) || (worker->bgw_restart_time > USECS_PER_DAY / 1000)) @@ -696,20 +734,6 @@ SanityCheckBackgroundWorker(BackgroundWorker *worker, int elevel) return true; } -/* - * Standard SIGTERM handler for background workers - */ -static void -bgworker_die(SIGNAL_ARGS) -{ - sigprocmask(SIG_SETMASK, &BlockSig, NULL); - - ereport(FATAL, - (errcode(ERRCODE_ADMIN_SHUTDOWN), - errmsg("terminating background worker \"%s\" due to administrator command", - MyBgworkerEntry->bgw_type))); -} - /* * Main entry point for background worker processes. */ @@ -737,7 +761,6 @@ BackgroundWorkerMain(const void *startup_data, size_t startup_data_len) } MyBgworkerEntry = worker; - MyBackendType = B_BG_WORKER; init_ps_display(worker->bgw_name); Assert(GetProcessingMode() == InitProcessing); @@ -762,19 +785,19 @@ BackgroundWorkerMain(const void *startup_data, size_t startup_data_len) } else { - pqsignal(SIGINT, SIG_IGN); - pqsignal(SIGUSR1, SIG_IGN); - pqsignal(SIGFPE, SIG_IGN); + pqsignal(SIGINT, PG_SIG_IGN); + pqsignal(SIGUSR1, PG_SIG_IGN); + pqsignal(SIGFPE, PG_SIG_IGN); } - pqsignal(SIGTERM, bgworker_die); + pqsignal(SIGTERM, die); /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGHUP, SIG_IGN); + pqsignal(SIGHUP, PG_SIG_IGN); InitializeTimeouts(); /* establishes SIGALRM handler */ - pqsignal(SIGPIPE, SIG_IGN); - pqsignal(SIGUSR2, SIG_IGN); - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGPIPE, PG_SIG_IGN); + pqsignal(SIGUSR2, PG_SIG_IGN); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * If an exception is encountered, processing resumes here. @@ -852,7 +875,7 @@ void BackgroundWorkerInitializeConnection(const char *dbname, const char *username, uint32 flags) { BackgroundWorker *worker = MyBgworkerEntry; - bits32 init_flags = 0; /* never honor session_preload_libraries */ + uint32 init_flags = 0; /* never honor session_preload_libraries */ /* ignore datallowconn and ACL_CONNECT? */ if (flags & BGWORKER_BYPASS_ALLOWCONN) @@ -886,7 +909,7 @@ void BackgroundWorkerInitializeConnectionByOid(Oid dboid, Oid useroid, uint32 flags) { BackgroundWorker *worker = MyBgworkerEntry; - bits32 init_flags = 0; /* never honor session_preload_libraries */ + uint32 init_flags = 0; /* never honor session_preload_libraries */ /* ignore datallowconn and ACL_CONNECT? */ if (flags & BGWORKER_BYPASS_ALLOWCONN) @@ -1128,7 +1151,7 @@ RegisterDynamicBackgroundWorker(BackgroundWorker *worker, */ if (success && handle) { - *handle = palloc(sizeof(BackgroundWorkerHandle)); + *handle = palloc_object(BackgroundWorkerHandle); (*handle)->slot = slotno; (*handle)->generation = generation; } @@ -1395,3 +1418,48 @@ GetBackgroundWorkerTypeByPid(pid_t pid) return result; } + +/* + * Terminate all background workers connected to the given database, if the + * workers can be interrupted. + */ +void +TerminateBackgroundWorkersForDatabase(Oid databaseId) +{ + bool signal_postmaster = false; + + elog(DEBUG1, "attempting worker termination for database %u", + databaseId); + + LWLockAcquire(BackgroundWorkerLock, LW_EXCLUSIVE); + + /* + * Iterate through slots, looking for workers connected to the given + * database. + */ + for (int slotno = 0; slotno < BackgroundWorkerData->total_slots; slotno++) + { + BackgroundWorkerSlot *slot = &BackgroundWorkerData->slot[slotno]; + + if (slot->in_use && + (slot->worker.bgw_flags & BGWORKER_INTERRUPTIBLE)) + { + PGPROC *proc = BackendPidGetProc(slot->pid); + + if (proc && proc->databaseId == databaseId) + { + slot->terminate = true; + signal_postmaster = true; + + elog(DEBUG1, "termination requested for worker (PID %d) on database %u", + (int) slot->pid, databaseId); + } + } + } + + LWLockRelease(BackgroundWorkerLock); + + /* Make sure the postmaster notices the change to shared memory. */ + if (signal_postmaster) + SendPostmasterSignal(PMSIGNAL_BACKGROUND_WORKER_CHANGE); +} diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c index 72f5acceec78d..cd1bf9d919c1e 100644 --- a/src/backend/postmaster/bgwriter.c +++ b/src/backend/postmaster/bgwriter.c @@ -21,7 +21,7 @@ * should be killed by SIGQUIT and then a recovery cycle started. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -51,6 +51,7 @@ #include "utils/memutils.h" #include "utils/resowner.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* * GUC parameters @@ -94,25 +95,24 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len) Assert(startup_data_len == 0); - MyBackendType = B_BG_WRITER; AuxiliaryProcessMainCommon(); /* * Properly accept or ignore signals that might be sent to us. */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGINT, SIG_IGN); + pqsignal(SIGINT, PG_SIG_IGN); pqsignal(SIGTERM, SignalHandlerForShutdownRequest); /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); + pqsignal(SIGUSR2, PG_SIG_IGN); /* * Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * We just started, assume there has been either a shutdown or @@ -289,7 +289,7 @@ BackgroundWriterMain(const void *startup_data, size_t startup_data_len) if (now >= timeout && last_snapshot_lsn <= GetLastImportantRecPtr()) { - last_snapshot_lsn = LogStandbySnapshot(); + last_snapshot_lsn = LogStandbySnapshot(InvalidOid); last_snapshot_ts = now; } } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1ce2d..087120db0909d 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -26,7 +26,7 @@ * restart needs to be forced.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -42,6 +42,8 @@ #include "access/xlog.h" #include "access/xlog_internal.h" #include "access/xlogrecovery.h" +#include "catalog/pg_authid.h" +#include "commands/defrem.h" #include "libpq/pqsignal.h" #include "miscadmin.h" #include "pgstat.h" @@ -61,9 +63,12 @@ #include "storage/shmem.h" #include "storage/smgr.h" #include "storage/spin.h" +#include "storage/subsystems.h" +#include "utils/acl.h" #include "utils/guc.h" #include "utils/memutils.h" #include "utils/resowner.h" +#include "utils/wait_event.h" /*---------- @@ -127,14 +132,35 @@ typedef struct int num_requests; /* current # of requests */ int max_requests; /* allocated array size */ + + int head; /* Index of the first request in the ring + * buffer */ + int tail; /* Index of the last request in the ring + * buffer */ + + /* The ring buffer of pending checkpointer requests */ CheckpointerRequest requests[FLEXIBLE_ARRAY_MEMBER]; } CheckpointerShmemStruct; static CheckpointerShmemStruct *CheckpointerShmem; +static void CheckpointerShmemRequest(void *arg); +static void CheckpointerShmemInit(void *arg); + +const ShmemCallbacks CheckpointerShmemCallbacks = { + .request_fn = CheckpointerShmemRequest, + .init_fn = CheckpointerShmemInit, +}; + /* interval for calling AbsorbSyncRequests in CheckpointWriteDelay */ #define WRITES_PER_ABSORB 1000 +/* Maximum number of checkpointer requests to process in one batch */ +#define CKPT_REQ_BATCH_SIZE 10000 + +/* Max number of requests the checkpointer request queue can hold */ +#define MAX_CHECKPOINT_REQUESTS 10000000 + /* * GUC parameters */ @@ -161,7 +187,7 @@ static pg_time_t last_xlog_switch_time; static void ProcessCheckpointerInterrupts(void); static void CheckArchiveTimeout(void); static bool IsCheckpointOnSchedule(double progress); -static bool ImmediateCheckpointRequested(void); +static bool FastCheckpointRequested(void); static bool CompactCheckpointerRequestQueue(void); static void UpdateSharedMemoryConfig(void); @@ -183,7 +209,6 @@ CheckpointerMain(const void *startup_data, size_t startup_data_len) Assert(startup_data_len == 0); - MyBackendType = B_CHECKPOINTER; AuxiliaryProcessMainCommon(); CheckpointerShmem->checkpointer_pid = MyProcPid; @@ -198,17 +223,17 @@ CheckpointerMain(const void *startup_data, size_t startup_data_len) */ pqsignal(SIGHUP, SignalHandlerForConfigReload); pqsignal(SIGINT, ReqShutdownXLOG); - pqsignal(SIGTERM, SIG_IGN); /* ignore SIGTERM */ + pqsignal(SIGTERM, PG_SIG_IGN); /* ignore SIGTERM */ /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, SignalHandlerForShutdownRequest); /* * Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * Initialize so that first time-driven event happens at the correct time. @@ -543,6 +568,12 @@ CheckpointerMain(const void *startup_data, size_t startup_data_len) break; } + /* + * Disable logical decoding if someone requested it. See comments atop + * logicalctl.c. + */ + DisableLogicalDecodingIfNecessary(); + /* Check for archive_timeout and switch xlog files if necessary. */ CheckArchiveTimeout(); @@ -734,12 +765,12 @@ CheckArchiveTimeout(void) } /* - * Returns true if an immediate checkpoint request is pending. (Note that - * this does not check the *current* checkpoint's IMMEDIATE flag, but whether - * there is one pending behind it.) + * Returns true if a fast checkpoint request is pending. (Note that this does + * not check the *current* checkpoint's FAST flag, but whether there is one + * pending behind it.) */ static bool -ImmediateCheckpointRequested(void) +FastCheckpointRequested(void) { volatile CheckpointerShmemStruct *cps = CheckpointerShmem; @@ -747,7 +778,7 @@ ImmediateCheckpointRequested(void) * We don't need to acquire the ckpt_lck in this case because we're only * looking at a single flag bit. */ - if (cps->ckpt_flags & CHECKPOINT_IMMEDIATE) + if (cps->ckpt_flags & CHECKPOINT_FAST) return true; return false; } @@ -760,7 +791,7 @@ ImmediateCheckpointRequested(void) * checkpoint_completion_target. * * The checkpoint request flags should be passed in; currently the only one - * examined is CHECKPOINT_IMMEDIATE, which disables delays between writes. + * examined is CHECKPOINT_FAST, which disables delays between writes. * * 'progress' is an estimate of how much of the work has been done, as a * fraction between 0.0 meaning none, and 1.0 meaning all done. @@ -778,10 +809,10 @@ CheckpointWriteDelay(int flags, double progress) * Perform the usual duties and take a nap, unless we're behind schedule, * in which case we just try to catch up as quickly as possible. */ - if (!(flags & CHECKPOINT_IMMEDIATE) && + if (!(flags & CHECKPOINT_FAST) && !ShutdownXLOGPending && !ShutdownRequestPending && - !ImmediateCheckpointRequested() && + !FastCheckpointRequested() && IsCheckpointOnSchedule(progress)) { if (ConfigReloadPending) @@ -928,52 +959,93 @@ ReqShutdownXLOG(SIGNAL_ARGS) */ /* - * CheckpointerShmemSize - * Compute space needed for checkpointer-related shared memory + * CheckpointerShmemRequest + * Register shared memory space needed for checkpointer */ -Size -CheckpointerShmemSize(void) +static void +CheckpointerShmemRequest(void *arg) { Size size; /* - * Currently, the size of the requests[] array is arbitrarily set equal to - * NBuffers. This may prove too large or small ... + * The size of the requests[] array is arbitrarily set equal to NBuffers. + * But there is a cap of MAX_CHECKPOINT_REQUESTS to prevent accumulating + * too many checkpoint requests in the ring buffer. */ size = offsetof(CheckpointerShmemStruct, requests); - size = add_size(size, mul_size(NBuffers, sizeof(CheckpointerRequest))); - - return size; + size = add_size(size, mul_size(Min(NBuffers, + MAX_CHECKPOINT_REQUESTS), + sizeof(CheckpointerRequest))); + ShmemRequestStruct(.name = "Checkpointer Data", + .size = size, + .ptr = (void **) &CheckpointerShmem, + ); } /* * CheckpointerShmemInit - * Allocate and initialize checkpointer-related shared memory + * Initialize checkpointer-related shared memory */ -void -CheckpointerShmemInit(void) +static void +CheckpointerShmemInit(void *arg) { - Size size = CheckpointerShmemSize(); - bool found; + SpinLockInit(&CheckpointerShmem->ckpt_lck); + CheckpointerShmem->max_requests = Min(NBuffers, MAX_CHECKPOINT_REQUESTS); + CheckpointerShmem->head = CheckpointerShmem->tail = 0; + ConditionVariableInit(&CheckpointerShmem->start_cv); + ConditionVariableInit(&CheckpointerShmem->done_cv); +} - CheckpointerShmem = (CheckpointerShmemStruct *) - ShmemInitStruct("Checkpointer Data", - size, - &found); +/* + * ExecCheckpoint + * Primary entry point for manual CHECKPOINT commands + * + * This is mainly a wrapper for RequestCheckpoint(). + */ +void +ExecCheckpoint(ParseState *pstate, CheckPointStmt *stmt) +{ + bool fast = true; + bool unlogged = false; - if (!found) + foreach_ptr(DefElem, opt, stmt->options) { - /* - * First time through, so initialize. Note that we zero the whole - * requests array; this is so that CompactCheckpointerRequestQueue can - * assume that any pad bytes in the request structs are zeroes. - */ - MemSet(CheckpointerShmem, 0, size); - SpinLockInit(&CheckpointerShmem->ckpt_lck); - CheckpointerShmem->max_requests = NBuffers; - ConditionVariableInit(&CheckpointerShmem->start_cv); - ConditionVariableInit(&CheckpointerShmem->done_cv); + if (strcmp(opt->defname, "mode") == 0) + { + char *mode = defGetString(opt); + + if (strcmp(mode, "spread") == 0) + fast = false; + else if (strcmp(mode, "fast") != 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized value for %s option \"%s\": \"%s\"", + "CHECKPOINT", "mode", mode), + parser_errposition(pstate, opt->location))); + } + else if (strcmp(opt->defname, "flush_unlogged") == 0) + unlogged = defGetBoolean(opt); + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized %s option \"%s\"", + "CHECKPOINT", opt->defname), + parser_errposition(pstate, opt->location))); } + + if (!has_privs_of_role(GetUserId(), ROLE_PG_CHECKPOINT)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + /* translator: %s is name of an SQL command (e.g., CHECKPOINT) */ + errmsg("permission denied to execute %s command", + "CHECKPOINT"), + errdetail("Only roles with privileges of the \"%s\" role may execute this command.", + "pg_checkpoint"))); + + RequestCheckpoint(CHECKPOINT_WAIT | + (fast ? CHECKPOINT_FAST : 0) | + (unlogged ? CHECKPOINT_FLUSH_UNLOGGED : 0) | + (RecoveryInProgress() ? 0 : CHECKPOINT_FORCE)); } /* @@ -983,11 +1055,11 @@ CheckpointerShmemInit(void) * flags is a bitwise OR of the following: * CHECKPOINT_IS_SHUTDOWN: checkpoint is for database shutdown. * CHECKPOINT_END_OF_RECOVERY: checkpoint is for end of WAL recovery. - * CHECKPOINT_IMMEDIATE: finish the checkpoint ASAP, + * CHECKPOINT_FAST: finish the checkpoint ASAP, * ignoring checkpoint_completion_target parameter. * CHECKPOINT_FORCE: force a checkpoint even if no XLOG activity has occurred * since the last one (implied by CHECKPOINT_IS_SHUTDOWN or - * CHECKPOINT_END_OF_RECOVERY). + * CHECKPOINT_END_OF_RECOVERY, and the CHECKPOINT command). * CHECKPOINT_WAIT: wait for completion before returning (otherwise, * just signal checkpointer to do it, and return). * CHECKPOINT_CAUSE_XLOG: checkpoint is requested due to xlog filling. @@ -1009,7 +1081,7 @@ RequestCheckpoint(int flags) * There's no point in doing slow checkpoints in a standalone backend, * because there's no other backends the checkpoint could disrupt. */ - CreateCheckPoint(flags | CHECKPOINT_IMMEDIATE); + CreateCheckPoint(flags | CHECKPOINT_FAST); /* Free all smgr objects, as CheckpointerMain() normally would. */ smgrdestroyall(); @@ -1148,6 +1220,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type) { CheckpointerRequest *request; bool too_full; + int insert_pos; if (!IsUnderPostmaster) return false; /* probably shouldn't even get here */ @@ -1171,10 +1244,14 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type) } /* OK, insert request */ - request = &CheckpointerShmem->requests[CheckpointerShmem->num_requests++]; + insert_pos = CheckpointerShmem->tail; + request = &CheckpointerShmem->requests[insert_pos]; request->ftag = *ftag; request->type = type; + CheckpointerShmem->tail = (CheckpointerShmem->tail + 1) % CheckpointerShmem->max_requests; + CheckpointerShmem->num_requests++; + /* If queue is more than half full, nudge the checkpointer to empty it */ too_full = (CheckpointerShmem->num_requests >= CheckpointerShmem->max_requests / 2); @@ -1216,12 +1293,16 @@ CompactCheckpointerRequestQueue(void) struct CheckpointerSlotMapping { CheckpointerRequest request; - int slot; + int ring_idx; }; - int n, - preserve_count; + int n; int num_skipped = 0; + int head; + int max_requests; + int num_requests; + int read_idx, + write_idx; HASHCTL ctl; HTAB *htab; bool *skip_slot; @@ -1233,8 +1314,13 @@ CompactCheckpointerRequestQueue(void) if (CritSectionCount > 0) return false; + max_requests = CheckpointerShmem->max_requests; + num_requests = CheckpointerShmem->num_requests; + /* Initialize skip_slot array */ - skip_slot = palloc0(sizeof(bool) * CheckpointerShmem->num_requests); + skip_slot = palloc0_array(bool, max_requests); + + head = CheckpointerShmem->head; /* Initialize temporary hash table */ ctl.keysize = sizeof(CheckpointerRequest); @@ -1258,7 +1344,8 @@ CompactCheckpointerRequestQueue(void) * away preceding entries that would end up being canceled anyhow), but * it's not clear that the extra complexity would buy us anything. */ - for (n = 0; n < CheckpointerShmem->num_requests; n++) + read_idx = head; + for (n = 0; n < num_requests; n++) { CheckpointerRequest *request; struct CheckpointerSlotMapping *slotmap; @@ -1271,16 +1358,19 @@ CompactCheckpointerRequestQueue(void) * CheckpointerShmemInit. Note also that RelFileLocator had better * contain no pad bytes. */ - request = &CheckpointerShmem->requests[n]; + request = &CheckpointerShmem->requests[read_idx]; slotmap = hash_search(htab, request, HASH_ENTER, &found); if (found) { /* Duplicate, so mark the previous occurrence as skippable */ - skip_slot[slotmap->slot] = true; + skip_slot[slotmap->ring_idx] = true; num_skipped++; } /* Remember slot containing latest occurrence of this request value */ - slotmap->slot = n; + slotmap->ring_idx = read_idx; + + /* Move to the next request in the ring buffer */ + read_idx = (read_idx + 1) % max_requests; } /* Done with the hash table. */ @@ -1294,17 +1384,34 @@ CompactCheckpointerRequestQueue(void) } /* We found some duplicates; remove them. */ - preserve_count = 0; - for (n = 0; n < CheckpointerShmem->num_requests; n++) + read_idx = write_idx = head; + for (n = 0; n < num_requests; n++) { - if (skip_slot[n]) - continue; - CheckpointerShmem->requests[preserve_count++] = CheckpointerShmem->requests[n]; + /* If this slot is NOT skipped, keep it */ + if (!skip_slot[read_idx]) + { + /* If the read and write positions are different, copy the request */ + if (write_idx != read_idx) + CheckpointerShmem->requests[write_idx] = + CheckpointerShmem->requests[read_idx]; + + /* Advance the write position */ + write_idx = (write_idx + 1) % max_requests; + } + + read_idx = (read_idx + 1) % max_requests; } + + /* + * Update ring buffer state: head remains the same, tail moves, count + * decreases + */ + CheckpointerShmem->tail = write_idx; + CheckpointerShmem->num_requests -= num_skipped; + ereport(DEBUG1, (errmsg_internal("compacted fsync request queue from %d entries to %d entries", - CheckpointerShmem->num_requests, preserve_count))); - CheckpointerShmem->num_requests = preserve_count; + num_requests, CheckpointerShmem->num_requests))); /* Cleanup. */ pfree(skip_slot); @@ -1325,40 +1432,64 @@ AbsorbSyncRequests(void) { CheckpointerRequest *requests = NULL; CheckpointerRequest *request; - int n; + int n, + i; + bool loop; if (!AmCheckpointerProcess()) return; - LWLockAcquire(CheckpointerCommLock, LW_EXCLUSIVE); - - /* - * We try to avoid holding the lock for a long time by copying the request - * array, and processing the requests after releasing the lock. - * - * Once we have cleared the requests from shared memory, we have to PANIC - * if we then fail to absorb them (eg, because our hashtable runs out of - * memory). This is because the system cannot run safely if we are unable - * to fsync what we have been told to fsync. Fortunately, the hashtable - * is so small that the problem is quite unlikely to arise in practice. - */ - n = CheckpointerShmem->num_requests; - if (n > 0) + do { - requests = (CheckpointerRequest *) palloc(n * sizeof(CheckpointerRequest)); - memcpy(requests, CheckpointerShmem->requests, n * sizeof(CheckpointerRequest)); - } + LWLockAcquire(CheckpointerCommLock, LW_EXCLUSIVE); + + /*--- + * We try to avoid holding the lock for a long time by: + * 1. Copying the request array and processing the requests after + * releasing the lock; + * 2. Processing not the whole queue, but only batches of + * CKPT_REQ_BATCH_SIZE at once. + * + * Once we have cleared the requests from shared memory, we must + * PANIC if we then fail to absorb them (e.g., because our hashtable + * runs out of memory). This is because the system cannot run safely + * if we are unable to fsync what we have been told to fsync. + * Fortunately, the hashtable is so small that the problem is quite + * unlikely to arise in practice. + * + * Note: The maximum possible size of a ring buffer is + * MAX_CHECKPOINT_REQUESTS entries, which fit into a maximum palloc + * allocation size of 1Gb. Our maximum batch size, + * CKPT_REQ_BATCH_SIZE, is even smaller. + */ + n = Min(CheckpointerShmem->num_requests, CKPT_REQ_BATCH_SIZE); + if (n > 0) + { + if (!requests) + requests = (CheckpointerRequest *) palloc(n * sizeof(CheckpointerRequest)); - START_CRIT_SECTION(); + for (i = 0; i < n; i++) + { + requests[i] = CheckpointerShmem->requests[CheckpointerShmem->head]; + CheckpointerShmem->head = (CheckpointerShmem->head + 1) % CheckpointerShmem->max_requests; + } - CheckpointerShmem->num_requests = 0; + CheckpointerShmem->num_requests -= n; - LWLockRelease(CheckpointerCommLock); + } + + START_CRIT_SECTION(); - for (request = requests; n > 0; request++, n--) - RememberSyncRequest(&request->ftag, request->type); + /* Are there any requests in the queue? If so, keep going. */ + loop = CheckpointerShmem->num_requests != 0; + + LWLockRelease(CheckpointerCommLock); - END_CRIT_SECTION(); + for (request = requests; n > 0; request++, n--) + RememberSyncRequest(&request->ftag, request->type); + + END_CRIT_SECTION(); + } while (loop); if (requests) pfree(requests); @@ -1404,3 +1535,16 @@ FirstCallSinceLastCheckpoint(void) return FirstCall; } + +/* + * Wake up the checkpointer process. + */ +void +WakeupCheckpointer(void) +{ + volatile PROC_HDR *procglobal = ProcGlobal; + ProcNumber checkpointerProc = procglobal->checkpointerProc; + + if (checkpointerProc != INVALID_PROC_NUMBER) + SetLatch(&GetPGProcByNumber(checkpointerProc)->procLatch); +} diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c new file mode 100644 index 0000000000000..b3e991706699d --- /dev/null +++ b/src/backend/postmaster/datachecksum_state.c @@ -0,0 +1,1636 @@ +/*------------------------------------------------------------------------- + * + * datachecksum_state.c + * Background worker for enabling or disabling data checksums online as + * well as functionality for manipulating data checksum state + * + * When enabling data checksums on a cluster at initdb time or when shut down + * with pg_checksums, no extra process is required as each page is checksummed, + * and verified, when accessed. When enabling checksums on an already running + * cluster, this worker will ensure that all pages are checksummed before + * verification of the checksums is turned on. In the case of disabling + * checksums, the state transition is performed only in the control file, no + * changes are performed on the data pages. + * + * Checksums can be either enabled or disabled cluster-wide, with on/off being + * the end state for data_checksums. + * + * 1. Enabling checksums + * --------------------- + * When enabling checksums in an online cluster, data_checksums will be set to + * "inprogress-on" which signals that write operations MUST compute and write + * the checksum on the data page, but during reading the checksum SHALL NOT be + * verified. This ensures that all objects created during when checksums are + * being enabled will have checksums set, but reads won't fail due to missing or + * invalid checksums. Invalid checksums can be present in case the cluster had + * checksums enabled, then disabled them and updated the page while they were + * disabled. + * + * The DataChecksumsWorker will compile a list of all databases at the start, + * any databases created concurrently will see the in-progress state and will + * be checksummed automatically. All databases from the original list MUST BE + * successfully processed in order for data checksums to be enabled, the only + * exception are databases which are dropped before having been processed. + * + * For each database, all relations which have storage are read and every data + * page is marked dirty to force a write with the checksum. This will generate + * a lot of WAL as the entire database is read and written. + * + * If the processing is interrupted by a cluster crash or restart, it needs to + * be restarted from the beginning again as state isn't persisted. + * + * 2. Disabling checksums + * ---------------------- + * When disabling checksums, data_checksums will be set to "inprogress-off" + * which signals that checksums are written but no longer need to be verified. + * This ensures that backends which have not yet transitioned to the + * "inprogress-off" state will still see valid checksums on pages. + * + * 3. Synchronization and Correctness + * ---------------------------------- + * The processes involved in enabling or disabling data checksums in an + * online cluster must be properly synchronized with the normal backends + * serving concurrent queries to ensure correctness. Correctness is defined + * as the following: + * + * - Backends SHALL NOT violate the data_checksums state they have agreed to + * by acknowledging the procsignalbarrier: This means that all backends + * MUST calculate and write data checksums during all states except off; + * MUST validate checksums only in the 'on' state. + * - Data checksums SHALL NOT be considered enabled cluster-wide until all + * currently connected backends have state "on": This means that all + * backends must wait on the procsignalbarrier to be acknowledged by all + * before proceeding to validate data checksums. + * + * There are two steps of synchronization required for changing data_checksums + * in an online cluster: (i) changing state in the active backends ("on", + * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no + * incompatible objects and processes are left in a database when workers end. + * The former deals with cluster-wide agreement on data checksum state and the + * latter with ensuring that any concurrent activity cannot break the data + * checksum contract during processing. + * + * Synchronizing the state change is done with procsignal barriers. Before + * updating the data_checksums state in the control file, all other backends must absorb the + * barrier. Barrier absorption will happen during interrupt processing, which + * means that connected backends will change state at different times. If + * waiting for a barrier is done during startup, for example during replay, it + * is important to realize that any locks held by the startup process might + * cause deadlocks if backends end up waiting for those locks while startup + * is waiting for a procsignalbarrier. + * + * 3.1 When Enabling Data Checksums + * -------------------------------- + * A process which fails to observe data checksums being enabled can induce two + * types of errors: failing to write the checksum when modifying the page and + * failing to validate the data checksum on the page when reading it. + * + * When processing starts all backends belong to one of the below sets, with + * one if Bd and Bi being empty: + * + * Bg: Backend updating the global state and emitting the procsignalbarrier + * Bd: Backends in "off" state + * Bi: Backends in "inprogress-on" state + * + * If processing is started in an online cluster then all backends are in Bd. + * If processing was halted by the cluster shutting down (due to a crash or + * intentional restart), the controlfile state "inprogress-on" will be observed + * on system startup and all backends will be placed in Bd. The controlfile + * state will also be set to "off". + * + * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the + * DataChecksumsWorkerLauncherMain. When all backends have acknowledged the + * barrier then Bd will be empty and the next phase can begin: calculating and + * writing data checksums with DataChecksumsWorkers. When the + * DataChecksumsWorker processes have finished writing checksums on all pages, + * data checksums are enabled cluster-wide via another procsignalbarrier. + * There are four sets of backends where Bd shall be an empty set: + * + * Bg: Backend updating the global state and emitting the procsignalbarrier + * Bd: Backends in "off" state + * Be: Backends in "on" state + * Bi: Backends in "inprogress-on" state + * + * Backends in Bi and Be will write checksums when modifying a page, but only + * backends in Be will verify the checksum during reading. The Bg backend is + * blocked waiting for all backends in Bi to process interrupts and move to + * Be. Any backend starting while Bg is waiting on the procsignalbarrier will + * observe the global state being "on" and will thus automatically belong to + * Be. Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be + * are compatible sets while still operating based on their local state as + * both write data checksums. + * + * 3.2 When Disabling Data Checksums + * --------------------------------- + * A process which fails to observe that data checksums have been disabled + * can induce two types of errors: writing the checksum when modifying the + * page and validating a data checksum which is no longer correct due to + * modifications to the page. The former is not an error per se as data + * integrity is maintained, but it is wasteful. The latter will cause errors + * in user operations. Assuming the following sets of backends: + * + * Bg: Backend updating the global state and emitting the procsignalbarrier + * Bd: Backends in "off" state + * Be: Backends in "on" state + * Bo: Backends in "inprogress-off" state + * Bi: Backends in "inprogress-on" state + * + * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd. From + * all other states, the transition can be straight to Bd. + * + * The goal is to transition all backends to Bd making the others empty sets. + * Backends in Bo write data checksums, but don't validate them, such that + * backends still in Be can continue to validate pages until the barrier has + * been absorbed such that they are in Bo. Once all backends are in Bo, the + * barrier to transition to "off" can be raised and all backends can safely + * stop writing data checksums as no backend is enforcing data checksum + * validation any longer. + * + * 4. Future opportunities for optimizations + * ----------------------------------------- + * Below are some potential optimizations and improvements which were brought + * up during reviews of this feature, but which weren't implemented in the + * initial version. These are ideas listed without any validation on their + * feasibility or potential payoff. More discussion on (most of) these can be + * found on the -hackers threads linked to in the commit message of this + * feature. + * + * * Launching datachecksumsworker for resuming operation from the startup + * process: Currently users have to restart processing manually after a + * restart since dynamic background worker cannot be started from the + * postmaster. Changing the startup process could make restarting the + * processing automatic on cluster restart. + * * Avoid dirtying the page when checksums already match: Iff the checksum + * on the page happens to already match we still dirty the page. It should + * be enough to only do the log_newpage_buffer() call in that case. + * * Teach pg_checksums to avoid checksummed pages when pg_checksums is used + * to enable checksums on a cluster which is in inprogress-on state and + * may have checksummed pages (make pg_checksums be able to resume an + * online operation). This should only be attempted for wal_level minimal. + * * Restartability (not necessarily with page granularity). + * * Avoid processing databases which were created during inprogress-on. + * Right now all databases are processed regardless to be safe. + * * Teach CREATE DATABASE to calculate checksums for databases created + * during inprogress-on with a template database which has yet to be + * processed. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/postmaster/datachecksum_state.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/xact.h" +#include "access/xlog.h" +#include "access/xloginsert.h" +#include "catalog/indexing.h" +#include "catalog/pg_class.h" +#include "catalog/pg_database.h" +#include "commands/progress.h" +#include "commands/vacuum.h" +#include "common/relpath.h" +#include "miscadmin.h" +#include "pgstat.h" +#include "postmaster/bgworker.h" +#include "postmaster/bgwriter.h" +#include "postmaster/datachecksum_state.h" +#include "storage/bufmgr.h" +#include "storage/checksum.h" +#include "storage/ipc.h" +#include "storage/latch.h" +#include "storage/lmgr.h" +#include "storage/lwlock.h" +#include "storage/procarray.h" +#include "storage/smgr.h" +#include "storage/subsystems.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/injection_point.h" +#include "utils/lsyscache.h" +#include "utils/ps_status.h" +#include "utils/syscache.h" +#include "utils/wait_event.h" + +/* + * Configuration of conditions which must match when absorbing a procsignal + * barrier during data checksum enable/disable operations. A single function + * is used for absorbing all barriers, and the current and target states must + * be defined as a from/to tuple in the checksum_barriers struct. + */ +typedef struct ChecksumBarrierCondition +{ + /* Current state of data checksums */ + int from; + /* Target state for data checksums */ + int to; +} ChecksumBarrierCondition; + +static const ChecksumBarrierCondition checksum_barriers[6] = +{ + /* + * Disabling checksums: If checksums are currently enabled, disabling must + * go through the 'inprogress-off' state. + */ + {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF}, + {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF}, + + /* + * If checksums are in the process of being enabled, but are not yet being + * verified, we can abort by going back to 'off' state. + */ + {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF}, + + /* + * Enabling checksums must normally go through the 'inprogress-on' state. + */ + {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON}, + {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION}, + + /* + * If checksums are being disabled but all backends are still computing + * checksums, we can go straight back to 'on' + */ + {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION}, +}; + +/* + * Signaling between backends calling pg_enable/disable_data_checksums, the + * checksums launcher process, and the checksums worker process. + * + * This struct is protected by DataChecksumsWorkerLock + */ +typedef struct DataChecksumsStateStruct +{ + /* + * These are set by pg_{enable|disable}_data_checksums, to tell the + * launcher what the target state is. + */ + DataChecksumsWorkerOperation launch_operation; + int launch_cost_delay; + int launch_cost_limit; + + /* + * Is a launcher process is currently running? This is set by the main + * launcher process, after it has read the above launch_* parameters. + */ + bool launcher_running; + + /* + * Is a worker process currently running? This is set by the worker + * launcher when it starts waiting for a worker process to finish. + */ + int worker_pid; + + /* + * These fields indicate the target state that the launcher is currently + * working towards. They can be different from the corresponding launch_* + * fields, if a new pg_enable/disable_data_checksums() call was made while + * the launcher/worker was already running. + * + * The below members are set when the launcher starts, and are only + * accessed read-only by the single worker. Thus, we can access these + * without a lock. If multiple workers, or dynamic cost parameters, are + * supported at some point then this would need to be revisited. + */ + DataChecksumsWorkerOperation operation; + int cost_delay; + int cost_limit; + + /* + * Signaling between the launcher and the worker process. + * + * As there is only a single worker, and the launcher won't read these + * until the worker exits, they can be accessed without the need for a + * lock. If multiple workers are supported then this will have to be + * revisited. + */ + + /* result, set by worker before exiting */ + DataChecksumsWorkerResult success; + + /* + * tells the worker process whether it should also process the shared + * catalogs + */ + bool process_shared_catalogs; +} DataChecksumsStateStruct; + +/* Shared memory segment for datachecksumsworker */ +static DataChecksumsStateStruct *DataChecksumState; + +typedef struct DataChecksumsWorkerDatabase +{ + Oid dboid; + char *dbname; +} DataChecksumsWorkerDatabase; + +/* Flag set by the interrupt handler */ +static volatile sig_atomic_t abort_requested = false; + +/* + * Have we set the DataChecksumsStateStruct->launcher_running flag? + * If we have, we need to clear it before exiting! + */ +static volatile sig_atomic_t launcher_running = false; + +/* Are we enabling data checksums, or disabling them? */ +static DataChecksumsWorkerOperation operation; + +/* Prototypes */ +static void DataChecksumsShmemRequest(void *arg); +static bool DatabaseExists(Oid dboid); +static List *BuildDatabaseList(void); +static List *BuildRelationList(bool temp_relations, bool include_shared); +static void FreeDatabaseList(List *dblist); +static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db); +static bool ProcessAllDatabases(void); +static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy); +static void launcher_cancel_handler(SIGNAL_ARGS); +static void WaitForAllTransactionsToFinish(void); + +const ShmemCallbacks DataChecksumsShmemCallbacks = { + .request_fn = DataChecksumsShmemRequest, +}; + +/***************************************************************************** + * Functionality for manipulating the data checksum state in the cluster + */ + +void +EmitAndWaitDataChecksumsBarrier(uint32 state) +{ + uint64 barrier; + + switch (state) + { + case PG_DATA_CHECKSUM_INPROGRESS_ON: + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON); + WaitForProcSignalBarrier(barrier); + break; + + case PG_DATA_CHECKSUM_INPROGRESS_OFF: + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF); + WaitForProcSignalBarrier(barrier); + break; + + case PG_DATA_CHECKSUM_VERSION: + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON); + WaitForProcSignalBarrier(barrier); + break; + + case PG_DATA_CHECKSUM_OFF: + barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF); + WaitForProcSignalBarrier(barrier); + break; + + default: + Assert(false); + } +} + +/* + * AbsorbDataChecksumsBarrier + * Generic function for absorbing data checksum state changes + * + * All procsignalbarriers regarding data checksum state changes are absorbed + * with this function. The set of conditions required for the state change to + * be accepted are listed in the checksum_barriers struct, target_state is + * used to look up the relevant entry. + */ +bool +AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier) +{ + uint32 target_state; + int current = data_checksums; + bool found = false; + + /* + * Translate the barrier condition to the target state, doing it here + * instead of in the procsignal code saves the latter from knowing about + * checksum states. + */ + switch (barrier) + { + case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON: + target_state = PG_DATA_CHECKSUM_INPROGRESS_ON; + break; + case PROCSIGNAL_BARRIER_CHECKSUM_ON: + target_state = PG_DATA_CHECKSUM_VERSION; + break; + case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF: + target_state = PG_DATA_CHECKSUM_INPROGRESS_OFF; + break; + case PROCSIGNAL_BARRIER_CHECKSUM_OFF: + target_state = PG_DATA_CHECKSUM_OFF; + break; + default: + elog(ERROR, "incorrect barrier \"%i\" received", barrier); + } + + /* + * If the target state matches the current state then the barrier has been + * repeated. + */ + if (current == target_state) + return true; + + /* + * If the cluster is in recovery we skip the validation of current state + * since the replay is trusted. + */ + if (RecoveryInProgress()) + { + SetLocalDataChecksumState(target_state); + return true; + } + + /* + * Find the barrier condition definition for the target state. Not finding + * a condition would be a grave programmer error as the states are a + * discrete set. + */ + for (int i = 0; i < lengthof(checksum_barriers) && !found; i++) + { + if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state) + found = true; + } + + /* + * If the relevant state criteria aren't satisfied, throw an error which + * will be caught by the procsignal machinery for a later retry. + */ + if (!found) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("incorrect data checksum state %i for target state %i", + current, target_state)); + + SetLocalDataChecksumState(target_state); + return true; +} + + +/* + * Disables data checksums for the cluster, if applicable. Starts a background + * worker which turns off the data checksums. + */ +Datum +disable_data_checksums(PG_FUNCTION_ARGS) +{ + if (!superuser()) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to change data checksum state")); + + StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0); + PG_RETURN_VOID(); +} + +/* + * Enables data checksums for the cluster, if applicable. Supports vacuum- + * like cost based throttling to limit system load. Starts a background worker + * which updates data checksums on existing data. + */ +Datum +enable_data_checksums(PG_FUNCTION_ARGS) +{ + int cost_delay = PG_GETARG_INT32(0); + int cost_limit = PG_GETARG_INT32(1); + + if (!superuser()) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to change data checksum state")); + + if (cost_delay < 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cost delay cannot be a negative value")); + + if (cost_limit <= 0) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cost limit must be greater than zero")); + + StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit); + + PG_RETURN_VOID(); +} + + +/***************************************************************************** + * Functionality for running the datachecksumsworker and associated launcher + */ + +/* + * StartDataChecksumsWorkerLauncher + * Main entry point for datachecksumsworker launcher process + * + * The main entrypoint for starting data checksums processing for enabling as + * well as disabling. + */ +void +StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op, + int cost_delay, + int cost_limit) +{ + BackgroundWorker bgw; + BackgroundWorkerHandle *bgw_handle; + bool launcher_running; + DataChecksumsWorkerOperation launcher_running_op; + +#ifdef USE_ASSERT_CHECKING + /* The cost delay settings have no effect when disabling */ + if (op == DISABLE_DATACHECKSUMS) + Assert(cost_delay == 0 && cost_limit == 0); +#endif + + INJECTION_POINT("datachecksumsworker-startup-delay", NULL); + + /* Store the desired state in shared memory */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + + DataChecksumState->launch_operation = op; + DataChecksumState->launch_cost_delay = cost_delay; + DataChecksumState->launch_cost_limit = cost_limit; + + /* Is the launcher already running? If so, what is it doing? */ + launcher_running = DataChecksumState->launcher_running; + if (launcher_running) + launcher_running_op = DataChecksumState->operation; + + LWLockRelease(DataChecksumsWorkerLock); + + /* + * Launch a new launcher process, if it's not running already. + * + * If the launcher is currently busy enabling the checksums, and we want + * them disabled (or vice versa), the launcher will notice that at latest + * when it's about to exit, and will loop back process the new request. So + * if the launcher is already running, we don't need to do anything more + * here to abort it. + * + * If you call pg_enable/disable_data_checksums() twice in a row, before + * the launcher has had a chance to start up, we still end up launching it + * twice. That's OK, the second invocation will see that a launcher is + * already running and exit quickly. + * + * TODO: We could optimize here and skip launching the launcher, if we are + * already in the desired state, i.e. if the checksums are already enabled + * and you call pg_enable_data_checksums(). + */ + if (!launcher_running) + { + /* + * Prepare the BackgroundWorker and launch it. + */ + memset(&bgw, 0, sizeof(bgw)); + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION; + bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; + snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher"); + snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher"); + bgw.bgw_restart_time = BGW_NEVER_RESTART; + bgw.bgw_notify_pid = MyProcPid; + bgw.bgw_main_arg = (Datum) 0; + + if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle)) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("failed to start background worker to process data checksums")); + } + else + { + if (launcher_running_op == op) + ereport(ERROR, + errmsg("data checksum processing already running")); + } +} + +/* + * ProcessSingleRelationFork + * Enable data checksums in a single relation/fork. + * + * Returns true if successful, and false if *aborted*. On error, an actual + * error is raised in the lower levels. + */ +static bool +ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy) +{ + BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum); + char activity[NAMEDATALEN * 2 + 128]; + char *relns; + + relns = get_namespace_name(RelationGetNamespace(reln)); + + /* Report the current relation to pg_stat_activity */ + snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %u blocks)", + (relns ? relns : ""), RelationGetRelationName(reln), forkNames[forkNum], numblocks); + pgstat_report_activity(STATE_RUNNING, activity); + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks); + if (relns) + pfree(relns); + + /* + * We are looping over the blocks which existed at the time of process + * start, which is safe since new blocks are created with checksums set + * already due to the state being "inprogress-on". + */ + for (BlockNumber blknum = 0; blknum < numblocks; blknum++) + { + Buffer buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy); + + /* Need to get an exclusive lock to mark the buffer as dirty */ + LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); + + /* + * Mark the buffer as dirty and force a full page write. We have to + * re-write the page to WAL even if the checksum hasn't changed, + * because if there is a replica it might have a slightly different + * version of the page with an invalid checksum, caused by unlogged + * changes (e.g. hint bits) on the primary happening while checksums + * were off. This can happen if there was a valid checksum on the page + * at one point in the past, so only when checksums are first on, then + * off, and then turned on again. TODO: investigate if this could be + * avoided if the checksum is calculated to be correct and wal_level + * is set to "minimal", + */ + START_CRIT_SECTION(); + MarkBufferDirty(buf); + log_newpage_buffer(buf, false); + END_CRIT_SECTION(); + + UnlockReleaseBuffer(buf); + + /* + * This is the only place where we check if we are asked to abort, the + * abortion will bubble up from here. + */ + Assert(operation == ENABLE_DATACHECKSUMS); + LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); + if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS) + abort_requested = true; + LWLockRelease(DataChecksumsWorkerLock); + + if (abort_requested) + return false; + + /* update the block counter */ + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE, + (blknum + 1)); + + /* + * Processing is re-using the vacuum cost delay for process + * throttling, hence why we call vacuum APIs here. + */ + vacuum_delay_point(false); + } + + return true; +} + +/* + * ProcessSingleRelationByOid + * Process a single relation based on oid. + * + * Returns true if successful, and false if *aborted*. On error, an actual + * error is raised in the lower levels. + */ +static bool +ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy) +{ + Relation rel; + bool aborted = false; + + StartTransactionCommand(); + + rel = try_relation_open(relationId, AccessShareLock); + if (rel == NULL) + { + /* + * Relation no longer exists. We don't consider this an error since + * there are no pages in it that need data checksums, and thus return + * true. The worker operates off a list of relations generated at the + * start of processing, so relations being dropped in the meantime is + * to be expected. + */ + CommitTransactionCommand(); + pgstat_report_activity(STATE_IDLE, NULL); + return true; + } + RelationGetSmgr(rel); + + for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++) + { + if (smgrexists(rel->rd_smgr, fnum)) + { + if (!ProcessSingleRelationFork(rel, fnum, strategy)) + { + aborted = true; + break; + } + } + } + relation_close(rel, AccessShareLock); + + CommitTransactionCommand(); + pgstat_report_activity(STATE_IDLE, NULL); + + return !aborted; +} + +/* + * ProcessDatabase + * Enable data checksums in a single database. + * + * We do this by launching a dynamic background worker into this database, and + * waiting for it to finish. We have to do this in a separate worker, since + * each process can only be connected to one database during its lifetime. + */ +static DataChecksumsWorkerResult +ProcessDatabase(DataChecksumsWorkerDatabase *db) +{ + BackgroundWorker bgw; + BackgroundWorkerHandle *bgw_handle; + BgwHandleStatus status; + pid_t pid; + char activity[NAMEDATALEN + 64]; + + DataChecksumState->success = DATACHECKSUMSWORKER_FAILED; + + memset(&bgw, 0, sizeof(bgw)); + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION; + bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; + snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker"); + snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker"); + bgw.bgw_restart_time = BGW_NEVER_RESTART; + bgw.bgw_notify_pid = MyProcPid; + bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid); + + /* + * If there are no worker slots available, there is little we can do. If + * we retry in a bit it's still unlikely that the user has managed to + * reconfigure in the meantime and we'd be run through retries fast. + */ + if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle)) + { + ereport(WARNING, + errmsg("could not start background worker for enabling data checksums in database \"%s\"", + db->dbname), + errhint("The \"%s\" setting might be too low.", "max_worker_processes")); + return DATACHECKSUMSWORKER_FAILED; + } + + status = WaitForBackgroundWorkerStartup(bgw_handle, &pid); + if (status == BGWH_STOPPED) + { + /* + * If the worker managed to start, and stop, before we got to waiting + * for it we can se a STOPPED status here without it being a failure. + */ + if (DataChecksumState->success == DATACHECKSUMSWORKER_SUCCESSFUL) + { + pgstat_report_activity(STATE_IDLE, NULL); + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->worker_pid = InvalidPid; + LWLockRelease(DataChecksumsWorkerLock); + return DataChecksumState->success; + } + + ereport(WARNING, + errmsg("could not start background worker for enabling data checksums in database \"%s\"", + db->dbname), + errhint("More details on the error might be found in the server log.")); + + /* + * Heuristic to see if the database was dropped, and if it was we can + * treat it as not an error, else treat as fatal and error out. TODO: + * this could probably be improved with a tighter check. + */ + if (DatabaseExists(db->dboid)) + return DATACHECKSUMSWORKER_FAILED; + else + return DATACHECKSUMSWORKER_DROPDB; + } + + /* + * If the postmaster crashed we cannot end up with a processed database so + * we have no alternative other than exiting. When enabling checksums we + * won't at this time have changed the data checksums state in pg_control + * to enabled so when the cluster comes back up processing will have to be + * restarted. + */ + if (status == BGWH_POSTMASTER_DIED) + ereport(FATAL, + errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("cannot enable data checksums without the postmaster process"), + errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums().")); + + Assert(status == BGWH_STARTED); + ereport(LOG, + errmsg("initiating data checksum processing in database \"%s\"", + db->dbname)); + + /* Save the pid of the worker so we can signal it later */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->worker_pid = pid; + LWLockRelease(DataChecksumsWorkerLock); + + snprintf(activity, sizeof(activity) - 1, + "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid); + pgstat_report_activity(STATE_RUNNING, activity); + + status = WaitForBackgroundWorkerShutdown(bgw_handle); + if (status == BGWH_POSTMASTER_DIED) + ereport(FATAL, + errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("postmaster exited during data checksum processing in \"%s\"", + db->dbname), + errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums().")); + + if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED) + ereport(LOG, + errmsg("data checksums processing was aborted in database \"%s\"", + db->dbname)); + + pgstat_report_activity(STATE_IDLE, NULL); + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->worker_pid = InvalidPid; + LWLockRelease(DataChecksumsWorkerLock); + + return DataChecksumState->success; +} + +/* + * launcher_exit + * + * Internal routine for cleaning up state when the launcher process exits. We + * need to clean up the abort flag to ensure that processing started again if + * it was previously aborted (note: started again, *not* restarted from where + * it left off). + */ +static void +launcher_exit(int code, Datum arg) +{ + abort_requested = false; + + if (launcher_running) + { + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + if (DataChecksumState->worker_pid != InvalidPid) + { + ereport(LOG, + errmsg("data checksums launcher exiting while worker is still running, signalling worker")); + kill(DataChecksumState->worker_pid, SIGTERM); + } + LWLockRelease(DataChecksumsWorkerLock); + } + + /* + * If the launcher is exiting before data checksums are enabled then set + * the state to off since processing cannot be resumed. + */ + if (DataChecksumsInProgressOn()) + SetDataChecksumsOff(); + + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + launcher_running = false; + DataChecksumState->launcher_running = false; + LWLockRelease(DataChecksumsWorkerLock); +} + +/* + * launcher_cancel_handler + * + * Internal routine for reacting to SIGINT and flagging the worker to abort. + * The worker won't be interrupted immediately but will check for abort flag + * between each block in a relation. + */ +static void +launcher_cancel_handler(SIGNAL_ARGS) +{ + int save_errno = errno; + + abort_requested = true; + + /* + * There is no sleeping in the main loop, the flag will be checked + * periodically in ProcessSingleRelationFork. The worker does however + * sleep when waiting for concurrent transactions to end so we still need + * to set the latch. + */ + SetLatch(MyLatch); + + errno = save_errno; +} + +/* + * WaitForAllTransactionsToFinish + * Blocks awaiting all current transactions to finish + * + * Returns when all transactions which are active at the call of the function + * have ended, or if the postmaster dies while waiting. If the postmaster dies + * the abort flag will be set to indicate that the caller of this shouldn't + * proceed. + * + * NB: this will return early, if aborted by SIGINT or if the target state + * is changed while we're running. + */ +static void +WaitForAllTransactionsToFinish(void) +{ + TransactionId waitforxid; + + LWLockAcquire(XidGenLock, LW_SHARED); + waitforxid = XidFromFullTransactionId(TransamVariables->nextXid); + LWLockRelease(XidGenLock); + + while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid)) + { + char activity[64]; + int rc; + + /* Oldest running xid is older than us, so wait */ + snprintf(activity, + sizeof(activity), + "Waiting for current transactions to finish (waiting for %u)", + waitforxid); + pgstat_report_activity(STATE_RUNNING, activity); + + /* Retry every 3 seconds */ + ResetLatch(MyLatch); + rc = WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH, + 3000, + WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION); + + /* + * If the postmaster died we won't be able to enable checksums + * cluster-wide so abort and hope to continue when restarted. + */ + if (rc & WL_POSTMASTER_DEATH) + ereport(FATAL, + errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("postmaster exited during data checksums processing"), + errhint("Data checksums processing must be restarted manually after cluster restart.")); + + CHECK_FOR_INTERRUPTS(); + + LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED); + if (DataChecksumState->launch_operation != operation) + abort_requested = true; + LWLockRelease(DataChecksumsWorkerLock); + if (abort_requested) + break; + } + + pgstat_report_activity(STATE_IDLE, NULL); + return; +} + +/* + * DataChecksumsWorkerLauncherMain + * + * Main function for launching dynamic background workers for processing data + * checksums in databases. This function has the bgworker management, with + * ProcessAllDatabases being responsible for looping over the databases and + * initiating processing. + */ +void +DataChecksumsWorkerLauncherMain(Datum arg) +{ + on_shmem_exit(launcher_exit, 0); + + ereport(DEBUG1, + errmsg("background worker \"datachecksums launcher\" started")); + + pqsignal(SIGTERM, die); + pqsignal(SIGINT, launcher_cancel_handler); + pqsignal(SIGUSR1, procsignal_sigusr1_handler); + pqsignal(SIGUSR2, PG_SIG_IGN); + + BackgroundWorkerUnblockSignals(); + + MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER; + init_ps_display(NULL); + + INJECTION_POINT("datachecksumsworker-launcher-delay", NULL); + + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + + if (DataChecksumState->launcher_running) + { + ereport(LOG, + errmsg("background worker \"datachecksums launcher\" already running, exiting")); + /* Launcher was already running, let it finish */ + LWLockRelease(DataChecksumsWorkerLock); + return; + } + + launcher_running = true; + + /* Initialize a connection to shared catalogs only */ + BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0); + + operation = DataChecksumState->launch_operation; + DataChecksumState->launcher_running = true; + DataChecksumState->operation = operation; + DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay; + DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit; + LWLockRelease(DataChecksumsWorkerLock); + + /* + * The target state can change while we are busy enabling/disabling + * checksums, if the user calls pg_disable/enable_data_checksums() before + * we are finished with the previous request. In that case, we will loop + * back here, to process the new request. + */ +again: + + pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS, + InvalidOid); + + if (operation == ENABLE_DATACHECKSUMS) + { + /* + * If we are asked to enable checksums in a cluster which already has + * checksums enabled, exit immediately as there is nothing more to do. + */ + if (DataChecksumsNeedVerify()) + goto done; + + ereport(LOG, + errmsg("enabling data checksums requested, starting data checksum calculation")); + + /* + * Set the state to inprogress-on and wait on the procsignal barrier. + */ + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE, + PROGRESS_DATACHECKSUMS_PHASE_ENABLING); + SetDataChecksumsOnInProgress(); + + /* + * All backends are now in inprogress-on state and are writing data + * checksums. Start processing all data at rest. + */ + if (!ProcessAllDatabases()) + { + /* + * If the target state changed during processing then it's not a + * failure, so restart processing instead. + */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + if (DataChecksumState->launch_operation != operation) + { + LWLockRelease(DataChecksumsWorkerLock); + goto done; + } + LWLockRelease(DataChecksumsWorkerLock); + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("unable to enable data checksums in cluster")); + } + + /* + * Data checksums have been set on all pages, set the state to on in + * order to instruct backends to validate checksums on reading. + */ + SetDataChecksumsOn(); + + ereport(LOG, + errmsg("data checksums are now enabled")); + } + else if (operation == DISABLE_DATACHECKSUMS) + { + ereport(LOG, + errmsg("disabling data checksums requested")); + + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE, + PROGRESS_DATACHECKSUMS_PHASE_DISABLING); + SetDataChecksumsOff(); + ereport(LOG, + errmsg("data checksums are now disabled")); + } + else + Assert(false); + +done: + + /* + * This state will only be displayed for a fleeting moment, but for the + * sake of correctness it is still added before ending the command. + */ + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE, + PROGRESS_DATACHECKSUMS_PHASE_DONE); + + /* + * All done. But before we exit, check if the target state was changed + * while we were running. In that case we will have to start all over + * again. + */ + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + if (DataChecksumState->launch_operation != operation) + { + DataChecksumState->operation = DataChecksumState->launch_operation; + operation = DataChecksumState->launch_operation; + DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay; + DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit; + LWLockRelease(DataChecksumsWorkerLock); + goto again; + } + + /* Shut down progress reporting as we are done */ + pgstat_progress_end_command(); + + launcher_running = false; + DataChecksumState->launcher_running = false; + LWLockRelease(DataChecksumsWorkerLock); +} + +/* + * ProcessAllDatabases + * Compute the list of all databases and process checksums in each + * + * This will generate a list of databases to process for enabling checksums. + * If a database encounters a failure then processing will end immediately and + * return an error. + */ +static bool +ProcessAllDatabases(void) +{ + List *DatabaseList; + int cumulative_total = 0; + + /* Set up so first run processes shared catalogs, not once in every db */ + DataChecksumState->process_shared_catalogs = true; + + /* Get a list of all databases to process */ + WaitForAllTransactionsToFinish(); + DatabaseList = BuildDatabaseList(); + + /* + * Update progress reporting with the total number of databases we need to + * process. This number should not be changed during processing, the + * columns for processed databases is instead increased such that it can + * be compared against the total. + */ + { + const int index[] = { + PROGRESS_DATACHECKSUMS_DBS_TOTAL, + PROGRESS_DATACHECKSUMS_DBS_DONE, + PROGRESS_DATACHECKSUMS_RELS_TOTAL, + PROGRESS_DATACHECKSUMS_RELS_DONE, + PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, + PROGRESS_DATACHECKSUMS_BLOCKS_DONE, + }; + + int64 vals[6]; + + vals[0] = list_length(DatabaseList); + vals[1] = 0; + /* translated to NULL */ + vals[2] = -1; + vals[3] = -1; + vals[4] = -1; + vals[5] = -1; + + pgstat_progress_update_multi_param(6, index, vals); + } + + foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList) + { + DataChecksumsWorkerResult result; + + result = ProcessDatabase(db); + +#ifdef USE_INJECTION_POINTS + /* Allow a test process to alter the result of the operation */ + if (IS_INJECTION_POINT_ATTACHED("datachecksumsworker-fail-db-result")) + { + result = DATACHECKSUMSWORKER_FAILED; + INJECTION_POINT_CACHED("datachecksumsworker-fail-db-result", + db->dbname); + } +#endif + + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE, + ++cumulative_total); + + if (result == DATACHECKSUMSWORKER_FAILED) + { + /* + * Disable checksums on cluster, because we failed one of the + * databases and this is an all or nothing process. + */ + SetDataChecksumsOff(); + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_RESOURCES), + errmsg("data checksums failed to get enabled in all databases, aborting"), + errhint("The server log might have more information on the cause of the error.")); + } + else if (result == DATACHECKSUMSWORKER_ABORTED || abort_requested) + { + /* Abort flag set, so exit the whole process */ + return false; + } + + /* + * When one database has completed, it will have done shared catalogs + * so we don't have to process them again. + */ + DataChecksumState->process_shared_catalogs = false; + } + + FreeDatabaseList(DatabaseList); + + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE, + PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER); + return true; +} + +/* + * DataChecksumsShmemRequest + * Request datachecksumsworker-related shared memory + */ +static void +DataChecksumsShmemRequest(void *arg) +{ + ShmemRequestStruct(.name = "DataChecksumsWorker Data", + .size = sizeof(DataChecksumsStateStruct), + .ptr = (void **) &DataChecksumState, + ); +} + +/* + * DatabaseExists + * + * Scans the system catalog to check if a database with the given Oid exist + * and returns true if it is found, else false. + */ +static bool +DatabaseExists(Oid dboid) +{ + Relation rel; + ScanKeyData skey; + SysScanDesc scan; + bool found; + HeapTuple tuple; + + StartTransactionCommand(); + + rel = table_open(DatabaseRelationId, AccessShareLock); + ScanKeyInit(&skey, + Anum_pg_database_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(dboid)); + scan = systable_beginscan(rel, DatabaseOidIndexId, true, SnapshotSelf, + 1, &skey); + tuple = systable_getnext(scan); + found = HeapTupleIsValid(tuple); + + systable_endscan(scan); + table_close(rel, AccessShareLock); + + CommitTransactionCommand(); + + return found; +} + +/* + * BuildDatabaseList + * Compile a list of all currently available databases in the cluster + * + * This creates the list of databases for the datachecksumsworker workers to + * add checksums to. If the caller wants to ensure that no concurrently + * running CREATE DATABASE calls exist, this needs to be preceded by a call + * to WaitForAllTransactionsToFinish(). + */ +static List * +BuildDatabaseList(void) +{ + List *DatabaseList = NIL; + Relation rel; + TableScanDesc scan; + HeapTuple tup; + MemoryContext ctx = CurrentMemoryContext; + MemoryContext oldctx; + + StartTransactionCommand(); + + rel = table_open(DatabaseRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 0, NULL); + + while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection))) + { + Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup); + DataChecksumsWorkerDatabase *db; + + oldctx = MemoryContextSwitchTo(ctx); + + db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase)); + + db->dboid = pgdb->oid; + db->dbname = pstrdup(NameStr(pgdb->datname)); + + DatabaseList = lappend(DatabaseList, db); + + MemoryContextSwitchTo(oldctx); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + + CommitTransactionCommand(); + + return DatabaseList; +} + +static void +FreeDatabaseList(List *dblist) +{ + if (!dblist) + return; + + foreach_ptr(DataChecksumsWorkerDatabase, db, dblist) + { + if (db->dbname != NULL) + pfree(db->dbname); + } + + list_free_deep(dblist); +} + +/* + * BuildRelationList + * Compile a list of relations in the database + * + * Returns a list of OIDs for the request relation types. If temp_relations + * is True then only temporary relations are returned. If temp_relations is + * False then non-temporary relations which have data checksums are returned. + * If include_shared is True then shared relations are included as well in a + * non-temporary list. include_shared has no relevance when building a list of + * temporary relations. + */ +static List * +BuildRelationList(bool temp_relations, bool include_shared) +{ + List *RelationList = NIL; + Relation rel; + TableScanDesc scan; + HeapTuple tup; + MemoryContext ctx = CurrentMemoryContext; + MemoryContext oldctx; + + StartTransactionCommand(); + + rel = table_open(RelationRelationId, AccessShareLock); + scan = table_beginscan_catalog(rel, 0, NULL); + + while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection))) + { + Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup); + + /* Only include temporary relations when explicitly asked to */ + if (pgc->relpersistence == RELPERSISTENCE_TEMP) + { + if (!temp_relations) + continue; + } + else + { + /* + * If we are only interested in temp relations then continue + * immediately as the current relation isn't a temp relation. + */ + if (temp_relations) + continue; + + if (!RELKIND_HAS_STORAGE(pgc->relkind)) + continue; + + if (pgc->relisshared && !include_shared) + continue; + } + + oldctx = MemoryContextSwitchTo(ctx); + RelationList = lappend_oid(RelationList, pgc->oid); + MemoryContextSwitchTo(oldctx); + } + + table_endscan(scan); + table_close(rel, AccessShareLock); + + CommitTransactionCommand(); + + return RelationList; +} + +/* + * DataChecksumsWorkerMain + * + * Main function for enabling checksums in a single database, This is the + * function set as the bgw_function_name in the dynamic background worker + * process initiated for each database by the worker launcher. After enabling + * data checksums in each applicable relation in the database, it will wait for + * all temporary relations that were present when the function started to + * disappear before returning. This is required since we cannot rewrite + * existing temporary relations with data checksums. + */ +void +DataChecksumsWorkerMain(Datum arg) +{ + Oid dboid = DatumGetObjectId(arg); + List *RelationList = NIL; + List *InitialTempTableList = NIL; + BufferAccessStrategy strategy; + bool aborted = false; + int64 rels_done; +#ifdef USE_INJECTION_POINTS + bool retried = false; +#endif + + operation = ENABLE_DATACHECKSUMS; + + pqsignal(SIGTERM, die); + pqsignal(SIGUSR1, procsignal_sigusr1_handler); + + BackgroundWorkerUnblockSignals(); + + MyBackendType = B_DATACHECKSUMSWORKER_WORKER; + init_ps_display(NULL); + + BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid, + BGWORKER_BYPASS_ALLOWCONN); + + /* worker will have a separate entry in pg_stat_progress_data_checksums */ + pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS, + InvalidOid); + + /* + * Get a list of all temp tables present as we start in this database. We + * need to wait until they are all gone until we are done, since we cannot + * access these relations and modify them. + */ + InitialTempTableList = BuildRelationList(true, false); + + /* + * Enable vacuum cost delay, if any. While this process isn't doing any + * vacuuming, we are re-using the infrastructure that vacuum cost delay + * provides rather than inventing something bespoke. This is an internal + * implementation detail and care should be taken to avoid it bleeding + * through to the user to avoid confusion. + */ + Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS); + VacuumCostDelay = DataChecksumState->cost_delay; + VacuumCostLimit = DataChecksumState->cost_limit; + VacuumCostActive = (VacuumCostDelay > 0); + VacuumCostBalance = 0; + VacuumCostPageHit = 0; + VacuumCostPageMiss = 0; + VacuumCostPageDirty = 0; + + /* + * Create and set the vacuum strategy as our buffer strategy. + */ + strategy = GetAccessStrategy(BAS_VACUUM); + + RelationList = BuildRelationList(false, + DataChecksumState->process_shared_catalogs); + + /* Update the total number of relations to be processed in this DB. */ + { + const int index[] = { + PROGRESS_DATACHECKSUMS_RELS_TOTAL, + PROGRESS_DATACHECKSUMS_RELS_DONE + }; + + int64 vals[2]; + + vals[0] = list_length(RelationList); + vals[1] = 0; + + pgstat_progress_update_multi_param(2, index, vals); + } + + /* Process the relations */ + rels_done = 0; + foreach_oid(reloid, RelationList) + { + CHECK_FOR_INTERRUPTS(); + + if (!ProcessSingleRelationByOid(reloid, strategy)) + { + aborted = true; + break; + } + + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE, + ++rels_done); + } + list_free(RelationList); + + if (aborted) + { + DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED; + ereport(DEBUG1, + errmsg("data checksum processing aborted in database OID %u", + dboid)); + return; + } + + /* The worker is about to wait for temporary tables to go away. */ + pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE, + PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL); + + /* + * Wait for all temp tables that existed when we started to go away. This + * is necessary since we cannot "reach" them to enable checksums. Any temp + * tables created after we started will already have checksums in them + * (due to the "inprogress-on" state), so no need to wait for those. + */ + for (;;) + { + List *CurrentTempTables; + int numleft; + char activity[64]; + + CurrentTempTables = BuildRelationList(true, false); + numleft = 0; + foreach_oid(tmptbloid, InitialTempTableList) + { + if (list_member_oid(CurrentTempTables, tmptbloid)) + numleft++; + } + list_free(CurrentTempTables); + +#ifdef USE_INJECTION_POINTS + if (IS_INJECTION_POINT_ATTACHED("datachecksumsworker-fake-temptable-wait")) + { + /* Make sure to just cause one retry */ + if (!retried && numleft == 0) + { + numleft = 1; + retried = true; + + INJECTION_POINT_CACHED("datachecksumsworker-fake-temptable-wait", NULL); + } + } +#endif + + if (numleft == 0) + break; + + /* + * At least one temp table is left to wait for, indicate in pgstat + * activity and progress reporting. + */ + snprintf(activity, + sizeof(activity), + "Waiting for %d temp tables to be removed", numleft); + pgstat_report_activity(STATE_RUNNING, activity); + + /* Retry every 3 seconds */ + ResetLatch(MyLatch); + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + 3000, + WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT); + + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + aborted = DataChecksumState->launch_operation != operation; + LWLockRelease(DataChecksumsWorkerLock); + + CHECK_FOR_INTERRUPTS(); + + if (aborted || abort_requested) + { + DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED; + ereport(LOG, + errmsg("data checksum processing aborted in database OID %u", + dboid)); + return; + } + } + + list_free(InitialTempTableList); + + /* worker done */ + pgstat_progress_end_command(); + + LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE); + DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL; + LWLockRelease(DataChecksumsWorkerLock); +} diff --git a/src/backend/postmaster/fork_process.c b/src/backend/postmaster/fork_process.c index a64eeb71b879a..855c1a9abda35 100644 --- a/src/backend/postmaster/fork_process.c +++ b/src/backend/postmaster/fork_process.c @@ -4,7 +4,7 @@ * EXEC_BACKEND case; it might be extended to do so, but it would be * considerably more complex. * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/postmaster/fork_process.c diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec18..a2c0ff012c515 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -3,7 +3,7 @@ * interrupt.c * Interrupt handling routines. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -94,9 +94,8 @@ SignalHandlerForCrashExit(SIGNAL_ARGS) * shut down and exit. * * Typically, this handler would be used for SIGTERM, but some processes use - * other signals. In particular, the checkpointer exits on SIGUSR2, and the WAL - * writer and the logical replication parallel apply worker exits on either - * SIGINT or SIGTERM. + * other signals. In particular, the checkpointer and parallel apply worker + * exit on SIGUSR2, and the WAL writer exits on either SIGINT or SIGTERM. * * ShutdownRequestPending should be checked at a convenient place within the * main loop, or else the main loop should call ProcessMainLoopInterrupts. diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c index bf6b55ee83048..8f3cfea880c3c 100644 --- a/src/backend/postmaster/launch_backend.c +++ b/src/backend/postmaster/launch_backend.c @@ -20,7 +20,7 @@ * same state as after fork() on a Unix system. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -49,12 +49,15 @@ #include "replication/walreceiver.h" #include "storage/dsm.h" #include "storage/io_worker.h" +#include "storage/ipc.h" #include "storage/pg_shmem.h" +#include "storage/shmem_internal.h" #include "tcop/backend_startup.h" #include "utils/memutils.h" #ifdef EXEC_BACKEND #include "nodes/queryjumble.h" +#include "portability/instr_time.h" #include "storage/pg_shmem.h" #include "storage/spin.h" #endif @@ -96,14 +99,9 @@ typedef struct HANDLE UsedShmemSegID; #endif void *UsedShmemSegAddr; - slock_t *ShmemLock; #ifdef USE_INJECTION_POINTS struct InjectionPointsCtl *ActiveInjectionPoints; #endif - int NamedLWLockTrancheRequests; - NamedLWLockTranche *NamedLWLockTrancheArray; - LWLockPadded *MainLWLockArray; - slock_t *ProcStructLock; PROC_HDR *ProcGlobal; PGPROC *AuxiliaryProcs; PGPROC *PreparedXactProcs; @@ -132,6 +130,8 @@ typedef struct int MyPMChildSlot; + int32 timing_tsc_frequency_khz; + /* * These are only used by backend processes, but are here because passing * a socket needs some special handling on Windows. 'client_sock' is an @@ -154,15 +154,15 @@ static void read_backend_variables(char *id, void **startup_data, size_t *startu static void restore_backend_variables(BackendParameters *param); static bool save_backend_variables(BackendParameters *param, int child_slot, - ClientSocket *client_sock, + const ClientSocket *client_sock, #ifdef WIN32 HANDLE childProcess, pid_t childPid, #endif const void *startup_data, size_t startup_data_len); -static pid_t internal_forkexec(const char *child_kind, int child_slot, +static pid_t internal_forkexec(BackendType child_kind, int child_slot, const void *startup_data, size_t startup_data_len, - ClientSocket *client_sock); + const ClientSocket *client_sock); #endif /* EXEC_BACKEND */ @@ -177,34 +177,10 @@ typedef struct } child_process_kind; static child_process_kind child_process_kinds[] = { - [B_INVALID] = {"invalid", NULL, false}, - - [B_BACKEND] = {"backend", BackendMain, true}, - [B_DEAD_END_BACKEND] = {"dead-end backend", BackendMain, true}, - [B_AUTOVAC_LAUNCHER] = {"autovacuum launcher", AutoVacLauncherMain, true}, - [B_AUTOVAC_WORKER] = {"autovacuum worker", AutoVacWorkerMain, true}, - [B_BG_WORKER] = {"bgworker", BackgroundWorkerMain, true}, - - /* - * WAL senders start their life as regular backend processes, and change - * their type after authenticating the client for replication. We list it - * here for PostmasterChildName() but cannot launch them directly. - */ - [B_WAL_SENDER] = {"wal sender", NULL, true}, - [B_SLOTSYNC_WORKER] = {"slot sync worker", ReplSlotSyncWorkerMain, true}, - - [B_STANDALONE_BACKEND] = {"standalone backend", NULL, false}, - - [B_ARCHIVER] = {"archiver", PgArchiverMain, true}, - [B_BG_WRITER] = {"bgwriter", BackgroundWriterMain, true}, - [B_CHECKPOINTER] = {"checkpointer", CheckpointerMain, true}, - [B_IO_WORKER] = {"io_worker", IoWorkerMain, true}, - [B_STARTUP] = {"startup", StartupProcessMain, true}, - [B_WAL_RECEIVER] = {"wal_receiver", WalReceiverMain, true}, - [B_WAL_SUMMARIZER] = {"wal_summarizer", WalSummarizerMain, true}, - [B_WAL_WRITER] = {"wal_writer", WalWriterMain, true}, - - [B_LOGGER] = {"syslogger", SysLoggerMain, false}, +#define PG_PROCTYPE(bktype, bkcategory, description, main_func, shmem_attach) \ + [bktype] = {description, main_func, shmem_attach}, +#include "postmaster/proctypelist.h" +#undef PG_PROCTYPE }; const char * @@ -227,8 +203,8 @@ PostmasterChildName(BackendType child_type) */ pid_t postmaster_child_launch(BackendType child_type, int child_slot, - const void *startup_data, size_t startup_data_len, - ClientSocket *client_sock) + void *startup_data, size_t startup_data_len, + const ClientSocket *client_sock) { pid_t pid; @@ -239,13 +215,15 @@ postmaster_child_launch(BackendType child_type, int child_slot, ((BackendStartupData *) startup_data)->fork_started = GetCurrentTimestamp(); #ifdef EXEC_BACKEND - pid = internal_forkexec(child_process_kinds[child_type].name, child_slot, + pid = internal_forkexec(child_type, child_slot, startup_data, startup_data_len, client_sock); /* the child process will arrive in SubPostmasterMain */ #else /* !EXEC_BACKEND */ pid = fork_process(); if (pid == 0) /* child */ { + MyBackendType = child_type; + /* Capture and transfer timings that may be needed for logging */ if (IsExternalConnectionBackend(child_type)) { @@ -280,7 +258,7 @@ postmaster_child_launch(BackendType child_type, int child_slot, MyPMChildSlot = child_slot; if (client_sock) { - MyClientSocket = palloc(sizeof(ClientSocket)); + MyClientSocket = palloc_object(ClientSocket); memcpy(MyClientSocket, client_sock, sizeof(ClientSocket)); } @@ -304,8 +282,8 @@ postmaster_child_launch(BackendType child_type, int child_slot, * - fork():s, and then exec():s the child process */ static pid_t -internal_forkexec(const char *child_kind, int child_slot, - const void *startup_data, size_t startup_data_len, ClientSocket *client_sock) +internal_forkexec(BackendType child_kind, int child_slot, + const void *startup_data, size_t startup_data_len, const ClientSocket *client_sock) { static unsigned long tmpBackendFileNum = 0; pid_t pid; @@ -380,7 +358,7 @@ internal_forkexec(const char *child_kind, int child_slot, /* set up argv properly */ argv[0] = "postgres"; - snprintf(forkav, MAXPGPATH, "--forkchild=%s", child_kind); + snprintf(forkav, MAXPGPATH, "--forkchild=%d", (int) child_kind); argv[1] = forkav; /* Insert temp file name after --forkchild argument */ argv[2] = tmpfilename; @@ -414,8 +392,8 @@ internal_forkexec(const char *child_kind, int child_slot, * file is complete. */ static pid_t -internal_forkexec(const char *child_kind, int child_slot, - const void *startup_data, size_t startup_data_len, ClientSocket *client_sock) +internal_forkexec(BackendType child_kind, int child_slot, + const void *startup_data, size_t startup_data_len, const ClientSocket *client_sock) { int retry_count = 0; STARTUPINFO si; @@ -466,8 +444,8 @@ internal_forkexec(const char *child_kind, int child_slot, #else sprintf(paramHandleStr, "%lu", (DWORD) paramHandle); #endif - l = snprintf(cmdLine, sizeof(cmdLine) - 1, "\"%s\" --forkchild=\"%s\" %s", - postgres_exec_path, child_kind, paramHandleStr); + l = snprintf(cmdLine, sizeof(cmdLine) - 1, "\"%s\" --forkchild=%d %s", + postgres_exec_path, (int) child_kind, paramHandleStr); if (l >= sizeof(cmdLine)) { ereport(LOG, @@ -589,8 +567,8 @@ internal_forkexec(const char *child_kind, int child_slot, * to what it would be if we'd simply forked on Unix, and then * dispatch to the appropriate place. * - * The first two command line arguments are expected to be "--forkchild=", - * where indicates which postmaster child we are to become, and + * The first two command line arguments are expected to be "--forkchild=", + * where indicates which process type we are to become, and * the name of a variables file that we can read to load data that would * have been inherited by fork() on Unix. */ @@ -601,7 +579,6 @@ SubPostmasterMain(int argc, char *argv[]) size_t startup_data_len; char *child_kind; BackendType child_type; - bool found = false; TimestampTz fork_end; /* In EXEC_BACKEND case we will not have inherited these settings */ @@ -621,22 +598,17 @@ SubPostmasterMain(int argc, char *argv[]) if (argc != 3) elog(FATAL, "invalid subpostmaster invocation"); - /* Find the entry in child_process_kinds */ + /* + * Parse the --forkchild argument to find our process type. We rely with + * malice aforethought on atoi returning 0 (B_INVALID) on error. + */ if (strncmp(argv[1], "--forkchild=", 12) != 0) elog(FATAL, "invalid subpostmaster invocation (--forkchild argument missing)"); child_kind = argv[1] + 12; - found = false; - for (int idx = 0; idx < lengthof(child_process_kinds); idx++) - { - if (strcmp(child_process_kinds[idx].name, child_kind) == 0) - { - child_type = (BackendType) idx; - found = true; - break; - } - } - if (!found) + child_type = (BackendType) atoi(child_kind); + if (child_type <= B_INVALID || child_type > BACKEND_NUM_TYPES - 1) elog(ERROR, "unknown child kind %s", child_kind); + MyBackendType = child_type; /* Read in the variables file */ read_backend_variables(argv[2], &startup_data, &startup_data_len); @@ -695,6 +667,8 @@ SubPostmasterMain(int argc, char *argv[]) */ LocalProcessControlFile(false); + RegisterBuiltinShmemCallbacks(); + /* * Reload any libraries that were preloaded by the postmaster. Since we * exec'd this process, those libraries didn't come along with us; but we @@ -705,7 +679,10 @@ SubPostmasterMain(int argc, char *argv[]) /* Restore basic shared memory pointers */ if (UsedShmemSegAddr != NULL) - InitShmemAccess(UsedShmemSegAddr); + { + InitShmemAllocator(UsedShmemSegAddr); + ShmemCallRequestCallbacks(); + } /* * Run the appropriate Main function @@ -728,7 +705,7 @@ static void read_inheritable_socket(SOCKET *dest, InheritableSocket *src); /* Save critical backend variables into the BackendParameters struct */ static bool save_backend_variables(BackendParameters *param, - int child_slot, ClientSocket *client_sock, + int child_slot, const ClientSocket *client_sock, #ifdef WIN32 HANDLE childProcess, pid_t childPid, #endif @@ -753,16 +730,10 @@ save_backend_variables(BackendParameters *param, param->UsedShmemSegID = UsedShmemSegID; param->UsedShmemSegAddr = UsedShmemSegAddr; - param->ShmemLock = ShmemLock; - #ifdef USE_INJECTION_POINTS param->ActiveInjectionPoints = ActiveInjectionPoints; #endif - param->NamedLWLockTrancheRequests = NamedLWLockTrancheRequests; - param->NamedLWLockTrancheArray = NamedLWLockTrancheArray; - param->MainLWLockArray = MainLWLockArray; - param->ProcStructLock = ProcStructLock; param->ProcGlobal = ProcGlobal; param->AuxiliaryProcs = AuxiliaryProcs; param->PreparedXactProcs = PreparedXactProcs; @@ -782,6 +753,8 @@ save_backend_variables(BackendParameters *param, param->MaxBackends = MaxBackends; param->num_pmchild_slots = num_pmchild_slots; + param->timing_tsc_frequency_khz = timing_tsc_frequency_khz; + #ifdef WIN32 param->PostmasterHandle = PostmasterHandle; if (!write_duplicated_handle(¶m->initial_signal_pipe, @@ -1013,16 +986,10 @@ restore_backend_variables(BackendParameters *param) UsedShmemSegID = param->UsedShmemSegID; UsedShmemSegAddr = param->UsedShmemSegAddr; - ShmemLock = param->ShmemLock; - #ifdef USE_INJECTION_POINTS ActiveInjectionPoints = param->ActiveInjectionPoints; #endif - NamedLWLockTrancheRequests = param->NamedLWLockTrancheRequests; - NamedLWLockTrancheArray = param->NamedLWLockTrancheArray; - MainLWLockArray = param->MainLWLockArray; - ProcStructLock = param->ProcStructLock; ProcGlobal = param->ProcGlobal; AuxiliaryProcs = param->AuxiliaryProcs; PreparedXactProcs = param->PreparedXactProcs; @@ -1042,6 +1009,12 @@ restore_backend_variables(BackendParameters *param) MaxBackends = param->MaxBackends; num_pmchild_slots = param->num_pmchild_slots; + timing_tsc_frequency_khz = param->timing_tsc_frequency_khz; + + /* Re-run logic usually done by assign_timing_clock_source */ + pg_initialize_timing(); + pg_set_timing_clock_source(timing_clock_source); + #ifdef WIN32 PostmasterHandle = param->PostmasterHandle; pgwin32_initial_signal_pipe = param->initial_signal_pipe; diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build index 0008603cfee99..6cba23bbeef72 100644 --- a/src/backend/postmaster/meson.build +++ b/src/backend/postmaster/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'autovacuum.c', @@ -6,6 +6,7 @@ backend_sources += files( 'bgworker.c', 'bgwriter.c', 'checkpointer.c', + 'datachecksum_state.c', 'fork_process.c', 'interrupt.c', 'launch_backend.c', diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd2a7..0f207ac035674 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -14,7 +14,7 @@ * * Initial author: Simon Riggs simon@2ndquadrant.com * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -48,11 +48,13 @@ #include "storage/proc.h" #include "storage/procsignal.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/guc.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/resowner.h" #include "utils/timeout.h" +#include "utils/wait_event.h" /* ---------- @@ -153,40 +155,38 @@ static int ready_file_comparator(Datum a, Datum b, void *arg); static void LoadArchiveLibrary(void); static void pgarch_call_module_shutdown_cb(int code, Datum arg); -/* Report shared memory space needed by PgArchShmemInit */ -Size -PgArchShmemSize(void) -{ - Size size = 0; +static void PgArchShmemRequest(void *arg); +static void PgArchShmemInit(void *arg); - size = add_size(size, sizeof(PgArchData)); +const ShmemCallbacks PgArchShmemCallbacks = { + .request_fn = PgArchShmemRequest, + .init_fn = PgArchShmemInit, +}; - return size; +/* Register shared memory space needed by the archiver */ +static void +PgArchShmemRequest(void *arg) +{ + ShmemRequestStruct(.name = "Archiver Data", + .size = sizeof(PgArchData), + .ptr = (void **) &PgArch, + ); } -/* Allocate and initialize archiver-related shared memory */ -void -PgArchShmemInit(void) +/* Initialize archiver-related shared memory */ +static void +PgArchShmemInit(void *arg) { - bool found; - - PgArch = (PgArchData *) - ShmemInitStruct("Archiver Data", PgArchShmemSize(), &found); - - if (!found) - { - /* First time through, so initialize */ - MemSet(PgArch, 0, PgArchShmemSize()); - PgArch->pgprocno = INVALID_PROC_NUMBER; - pg_atomic_init_u32(&PgArch->force_dir_scan, 0); - } + MemSet(PgArch, 0, sizeof(PgArchData)); + PgArch->pgprocno = INVALID_PROC_NUMBER; + pg_atomic_init_u32(&PgArch->force_dir_scan, 0); } /* * PgArchCanRestart * - * Return true and archiver is allowed to restart if enough time has - * passed since it was launched last to reach PGARCH_RESTART_INTERVAL. + * Return true, indicating archiver is allowed to restart, if enough time has + * passed since it was last launched to reach PGARCH_RESTART_INTERVAL. * Otherwise return false. * * This is a safety valve to protect against continuous respawn attempts if the @@ -201,15 +201,18 @@ PgArchCanRestart(void) time_t curtime = time(NULL); /* - * Return false and don't restart archiver if too soon since last archiver - * start. + * If first time through, or time somehow went backwards, always update + * last_pgarch_start_time to match the current clock and allow archiver + * start. Otherwise allow it only once enough time has elapsed. */ - if ((unsigned int) (curtime - last_pgarch_start_time) < - (unsigned int) PGARCH_RESTART_INTERVAL) - return false; - - last_pgarch_start_time = curtime; - return true; + if (last_pgarch_start_time == 0 || + curtime < last_pgarch_start_time || + curtime - last_pgarch_start_time >= PGARCH_RESTART_INTERVAL) + { + last_pgarch_start_time = curtime; + return true; + } + return false; } @@ -219,7 +222,6 @@ PgArchiverMain(const void *startup_data, size_t startup_data_len) { Assert(startup_data_len == 0); - MyBackendType = B_ARCHIVER; AuxiliaryProcessMainCommon(); /* @@ -227,16 +229,16 @@ PgArchiverMain(const void *startup_data, size_t startup_data_len) * except for SIGHUP, SIGTERM, SIGUSR1, SIGUSR2, and SIGQUIT. */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGINT, SIG_IGN); + pqsignal(SIGINT, PG_SIG_IGN); pqsignal(SIGTERM, SignalHandlerForShutdownRequest); /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, pgarch_waken_stop); /* Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* Unblock signals (they were blocked when the postmaster forked us) */ sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); @@ -254,7 +256,7 @@ PgArchiverMain(const void *startup_data, size_t startup_data_len) PgArch->pgprocno = MyProcNumber; /* Create workspace for pgarch_readyXlog() */ - arch_files = palloc(sizeof(struct arch_files_state)); + arch_files = palloc_object(struct arch_files_state); arch_files->arch_files_size = 0; /* Initialize our max-heap for prioritizing files to archive. */ @@ -289,7 +291,7 @@ PgArchWakeup(void) * be relaunched shortly and will start archiving. */ if (arch_pgprocno != INVALID_PROC_NUMBER) - SetLatch(&ProcGlobal->allProcs[arch_pgprocno].procLatch); + SetLatch(&GetPGProcByNumber(arch_pgprocno)->procLatch); } @@ -332,7 +334,8 @@ pgarch_MainLoop(void) * SIGUSR2 arrives. However, that means a random SIGTERM would * disable archiving indefinitely, which doesn't seem like a good * idea. If more than 60 seconds pass since SIGTERM, exit anyway, so - * that the postmaster can start a new archiver if needed. + * that the postmaster can start a new archiver if needed. Also exit + * if time unexpectedly goes backward. */ if (ShutdownRequestPending) { @@ -340,8 +343,8 @@ pgarch_MainLoop(void) if (last_sigterm_time == 0) last_sigterm_time = curtime; - else if ((unsigned int) (curtime - last_sigterm_time) >= - (unsigned int) 60) + else if (curtime < last_sigterm_time || + curtime - last_sigterm_time >= 60) break; } @@ -718,15 +721,15 @@ pgarch_readyXlog(char *xlog) /* * Store the file in our max-heap if it has a high enough priority. */ - if (arch_files->arch_heap->bh_size < NUM_FILES_PER_DIRECTORY_SCAN) + if (binaryheap_size(arch_files->arch_heap) < NUM_FILES_PER_DIRECTORY_SCAN) { /* If the heap isn't full yet, quickly add it. */ - arch_file = arch_files->arch_filenames[arch_files->arch_heap->bh_size]; + arch_file = arch_files->arch_filenames[binaryheap_size(arch_files->arch_heap)]; strcpy(arch_file, basename); binaryheap_add_unordered(arch_files->arch_heap, CStringGetDatum(arch_file)); /* If we just filled the heap, make it a valid one. */ - if (arch_files->arch_heap->bh_size == NUM_FILES_PER_DIRECTORY_SCAN) + if (binaryheap_size(arch_files->arch_heap) == NUM_FILES_PER_DIRECTORY_SCAN) binaryheap_build(arch_files->arch_heap); } else if (ready_file_comparator(binaryheap_first(arch_files->arch_heap), @@ -744,21 +747,21 @@ pgarch_readyXlog(char *xlog) FreeDir(rldir); /* If no files were found, simply return. */ - if (arch_files->arch_heap->bh_size == 0) + if (binaryheap_empty(arch_files->arch_heap)) return false; /* * If we didn't fill the heap, we didn't make it a valid one. Do that * now. */ - if (arch_files->arch_heap->bh_size < NUM_FILES_PER_DIRECTORY_SCAN) + if (binaryheap_size(arch_files->arch_heap) < NUM_FILES_PER_DIRECTORY_SCAN) binaryheap_build(arch_files->arch_heap); /* * Fill arch_files array with the files to archive in ascending order of * priority. */ - arch_files->arch_files_size = arch_files->arch_heap->bh_size; + arch_files->arch_files_size = binaryheap_size(arch_files->arch_heap); for (int i = 0; i < arch_files->arch_files_size; i++) arch_files->arch_files[i] = DatumGetCString(binaryheap_remove_first(arch_files->arch_heap)); @@ -941,7 +944,7 @@ LoadArchiveLibrary(void) ereport(ERROR, (errmsg("archive modules must register an archive callback"))); - archive_module_state = (ArchiveModuleState *) palloc0(sizeof(ArchiveModuleState)); + archive_module_state = palloc0_object(ArchiveModuleState); if (ArchiveCallbacks->startup_cb != NULL) ArchiveCallbacks->startup_cb(archive_module_state); diff --git a/src/backend/postmaster/pmchild.c b/src/backend/postmaster/pmchild.c index cde1d23a4ca8b..5c4c66fe76ff7 100644 --- a/src/backend/postmaster/pmchild.c +++ b/src/backend/postmaster/pmchild.c @@ -20,7 +20,7 @@ * pmsignal.c, that mirrors this. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -59,6 +59,17 @@ NON_EXEC_STATIC int num_pmchild_slots = 0; */ dlist_head ActiveChildList; +/* + * Dummy pointer to persuade Valgrind that we've not leaked the array of + * PMChild structs. Make it global to ensure the compiler doesn't + * optimize it away. + */ +#ifdef USE_VALGRIND +extern PMChild *pmchild_array; +PMChild *pmchild_array; +#endif + + /* * MaxLivePostmasterChildren * @@ -125,8 +136,13 @@ InitPostmasterChildSlots(void) for (int i = 0; i < BACKEND_NUM_TYPES; i++) num_pmchild_slots += pmchild_pools[i].size; - /* Initialize them */ + /* Allocate enough slots, and make sure Valgrind doesn't complain */ slots = palloc(num_pmchild_slots * sizeof(PMChild)); +#ifdef USE_VALGRIND + pmchild_array = slots; +#endif + + /* Initialize them */ slotno = 0; for (int btype = 0; btype < BACKEND_NUM_TYPES; btype++) { diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index 490f7ce36645b..90c7c4528e872 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -32,7 +32,7 @@ * clients. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -97,9 +97,9 @@ #include "lib/ilist.h" #include "libpq/libpq.h" #include "libpq/pqsignal.h" -#include "pg_getopt.h" #include "pgstat.h" #include "port/pg_bswap.h" +#include "port/pg_getopt_ctx.h" #include "postmaster/autovacuum.h" #include "postmaster/bgworker_internals.h" #include "postmaster/pgarch.h" @@ -115,6 +115,7 @@ #include "storage/ipc.h" #include "storage/pmsignal.h" #include "storage/proc.h" +#include "storage/shmem_internal.h" #include "tcop/backend_startup.h" #include "tcop/tcopprot.h" #include "utils/datetime.h" @@ -303,12 +304,13 @@ static bool FatalError = false; /* T if recovering from backend crash */ * * When the startup process is ready to start archive recovery, it signals the * postmaster, and we switch to PM_RECOVERY state. The background writer and - * checkpointer are launched, while the startup process continues applying WAL. - * If Hot Standby is enabled, then, after reaching a consistent point in WAL - * redo, startup process signals us again, and we switch to PM_HOT_STANDBY - * state and begin accepting connections to perform read-only queries. When - * archive recovery is finished, the startup process exits with exit code 0 - * and we switch to PM_RUN state. + * checkpointer are already running (as these are launched during PM_STARTUP), + * and the startup process continues applying WAL. If Hot Standby is enabled, + * then, after reaching a consistent point in WAL redo, startup process + * signals us again, and we switch to PM_HOT_STANDBY state and begin accepting + * connections to perform read-only queries. When archive recovery is + * finished, the startup process exits with exit code 0 and we switch to + * PM_RUN state. * * Normal child backends can only be launched when we are in PM_RUN or * PM_HOT_STANDBY state. (connsAllowed can also restrict launching.) @@ -408,6 +410,7 @@ static DNSServiceRef bonjour_sdref = NULL; #endif /* State for IO worker management. */ +static TimestampTz io_worker_launch_next_time = 0; static int io_worker_count = 0; static PMChild *io_worker_children[MAX_IO_WORKERS]; @@ -446,7 +449,8 @@ static int CountChildren(BackendTypeMask targetMask); static void LaunchMissingBackgroundProcesses(void); static void maybe_start_bgworkers(void); static bool maybe_reap_io_worker(int pid); -static void maybe_adjust_io_workers(void); +static void maybe_start_io_workers(void); +static TimestampTz maybe_start_io_workers_scheduled_at(void); static bool CreateOptsFile(int argc, char *argv[], char *fullprogname); static PMChild *StartChildProcess(BackendType type); static void StartSysLogger(void); @@ -492,6 +496,7 @@ HANDLE PostmasterHandle; void PostmasterMain(int argc, char *argv[]) { + pg_getopt_ctx optctx; int opt; int status; char *userDoption = NULL; @@ -551,8 +556,8 @@ PostmasterMain(int argc, char *argv[]) pqsignal(SIGINT, handle_pm_shutdown_request_signal); pqsignal(SIGQUIT, handle_pm_shutdown_request_signal); pqsignal(SIGTERM, handle_pm_shutdown_request_signal); - pqsignal(SIGALRM, SIG_IGN); /* ignored */ - pqsignal(SIGPIPE, SIG_IGN); /* ignored */ + pqsignal(SIGALRM, PG_SIG_IGN); /* ignored */ + pqsignal(SIGPIPE, PG_SIG_IGN); /* ignored */ pqsignal(SIGUSR1, handle_pm_pmsignal_signal); pqsignal(SIGUSR2, dummy_handler); /* unused, reserve for children */ pqsignal(SIGCHLD, handle_pm_child_exit_signal); @@ -569,15 +574,15 @@ PostmasterMain(int argc, char *argv[]) * child processes should just allow the inherited settings to stand. */ #ifdef SIGTTIN - pqsignal(SIGTTIN, SIG_IGN); /* ignored */ + pqsignal(SIGTTIN, PG_SIG_IGN); /* ignored */ #endif #ifdef SIGTTOU - pqsignal(SIGTTOU, SIG_IGN); /* ignored */ + pqsignal(SIGTTOU, PG_SIG_IGN); /* ignored */ #endif /* ignore SIGXFSZ, so that ulimit violations work like disk full */ #ifdef SIGXFSZ - pqsignal(SIGXFSZ, SIG_IGN); /* ignored */ + pqsignal(SIGXFSZ, PG_SIG_IGN); /* ignored */ #endif /* Begin accepting signals. */ @@ -588,19 +593,19 @@ PostmasterMain(int argc, char *argv[]) */ InitializeGUCOptions(); - opterr = 1; - /* * Parse command-line options. CAUTION: keep this in sync with * tcop/postgres.c (the option sets should not conflict) and with the * common help() function in main/main.c. */ - while ((opt = getopt(argc, argv, "B:bC:c:D:d:EeFf:h:ijk:lN:OPp:r:S:sTt:W:-:")) != -1) + pg_getopt_start(&optctx, argc, argv, "B:bC:c:D:d:EeFf:h:ijk:lN:OPp:r:S:sTt:W:-:"); + optctx.opterr = 1; + while ((opt = pg_getopt_next(&optctx)) != -1) { switch (opt) { case 'B': - SetConfigOption("shared_buffers", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("shared_buffers", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case 'b': @@ -609,7 +614,7 @@ PostmasterMain(int argc, char *argv[]) break; case 'C': - output_config_variable = strdup(optarg); + output_config_variable = strdup(optctx.optarg); break; case '-': @@ -620,30 +625,30 @@ PostmasterMain(int argc, char *argv[]) * returns DISPATCH_POSTMASTER if it doesn't find a match, so * error for anything else. */ - if (parse_dispatch_option(optarg) != DISPATCH_POSTMASTER) + if (parse_dispatch_option(optctx.optarg) != DISPATCH_POSTMASTER) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("--%s must be first argument", optarg))); + errmsg("--%s must be first argument", optctx.optarg))); - /* FALLTHROUGH */ + pg_fallthrough; case 'c': { char *name, *value; - ParseLongOption(optarg, &name, &value); + ParseLongOption(optctx.optarg, &name, &value); if (!value) { if (opt == '-') ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("--%s requires a value", - optarg))); + optctx.optarg))); else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("-c %s requires a value", - optarg))); + optctx.optarg))); } SetConfigOption(name, value, PGC_POSTMASTER, PGC_S_ARGV); @@ -653,11 +658,11 @@ PostmasterMain(int argc, char *argv[]) } case 'D': - userDoption = strdup(optarg); + userDoption = strdup(optctx.optarg); break; case 'd': - set_debug_options(atoi(optarg), PGC_POSTMASTER, PGC_S_ARGV); + set_debug_options(atoi(optctx.optarg), PGC_POSTMASTER, PGC_S_ARGV); break; case 'E': @@ -673,16 +678,16 @@ PostmasterMain(int argc, char *argv[]) break; case 'f': - if (!set_plan_disabling_options(optarg, PGC_POSTMASTER, PGC_S_ARGV)) + if (!set_plan_disabling_options(optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV)) { write_stderr("%s: invalid argument for option -f: \"%s\"\n", - progname, optarg); + progname, optctx.optarg); ExitPostmaster(1); } break; case 'h': - SetConfigOption("listen_addresses", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("listen_addresses", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case 'i': @@ -694,7 +699,7 @@ PostmasterMain(int argc, char *argv[]) break; case 'k': - SetConfigOption("unix_socket_directories", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("unix_socket_directories", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case 'l': @@ -702,7 +707,7 @@ PostmasterMain(int argc, char *argv[]) break; case 'N': - SetConfigOption("max_connections", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("max_connections", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case 'O': @@ -714,7 +719,7 @@ PostmasterMain(int argc, char *argv[]) break; case 'p': - SetConfigOption("port", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("port", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case 'r': @@ -722,7 +727,7 @@ PostmasterMain(int argc, char *argv[]) break; case 'S': - SetConfigOption("work_mem", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("work_mem", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; case 's': @@ -740,7 +745,7 @@ PostmasterMain(int argc, char *argv[]) case 't': { - const char *tmp = get_stats_option_name(optarg); + const char *tmp = get_stats_option_name(optctx.optarg); if (tmp) { @@ -749,14 +754,14 @@ PostmasterMain(int argc, char *argv[]) else { write_stderr("%s: invalid argument for option -t: \"%s\"\n", - progname, optarg); + progname, optctx.optarg); ExitPostmaster(1); } break; } case 'W': - SetConfigOption("post_auth_delay", optarg, PGC_POSTMASTER, PGC_S_ARGV); + SetConfigOption("post_auth_delay", optctx.optarg, PGC_POSTMASTER, PGC_S_ARGV); break; default: @@ -769,10 +774,10 @@ PostmasterMain(int argc, char *argv[]) /* * Postmaster accepts no non-option switch arguments. */ - if (optind < argc) + if (optctx.optind < argc) { write_stderr("%s: invalid argument: \"%s\"\n", - progname, argv[optind]); + progname, argv[optctx.optind]); write_stderr("Try \"%s --help\" for more information.\n", progname); ExitPostmaster(1); @@ -854,6 +859,9 @@ PostmasterMain(int argc, char *argv[]) if (summarize_wal && wal_level == WAL_LEVEL_MINIMAL) ereport(ERROR, (errmsg("WAL cannot be summarized when \"wal_level\" is \"minimal\""))); + if (sync_replication_slots && wal_level == WAL_LEVEL_MINIMAL) + ereport(ERROR, + (errmsg("replication slot synchronization (\"sync_replication_slots\" = on) requires \"wal_level\" to be \"replica\" or \"logical\""))); /* * Other one-time internal sanity checks can go here, if they are fast. @@ -865,19 +873,10 @@ PostmasterMain(int argc, char *argv[]) ExitPostmaster(1); } - /* - * Now that we are done processing the postmaster arguments, reset - * getopt(3) library so that it will work correctly in subprocesses. - */ - optind = 1; -#ifdef HAVE_INT_OPTRESET - optreset = 1; /* some systems need this too */ -#endif - /* For debugging: display postmaster environment */ if (message_level_is_interesting(DEBUG3)) { -#if !defined(WIN32) || defined(_MSC_VER) +#if !defined(WIN32) extern char **environ; #endif char **p; @@ -926,6 +925,11 @@ PostmasterMain(int argc, char *argv[]) */ ApplyLauncherRegister(); + /* + * Register the shared memory needs of all core subsystems. + */ + RegisterBuiltinShmemCallbacks(); + /* * process any libraries that should be preloaded at postmaster start */ @@ -956,10 +960,21 @@ PostmasterMain(int argc, char *argv[]) InitializeFastPathLocks(); /* - * Give preloaded libraries a chance to request additional shared memory. + * Also call any legacy shmem request hooks that might've been installed + * by preloaded libraries. + * + * Note: this must be done before ShmemCallRequestCallbacks(), because the + * hooks may request LWLocks with RequestNamedLWLockTranche(), which in + * turn affects the size of the LWLock array calculated in lwlock.c. */ process_shmem_requests(); + /* + * Ask all subsystems, including preloaded libraries, to register their + * shared memory needs. + */ + ShmemCallRequestCallbacks(); + /* * Now that loadable modules have had their chance to request additional * shared memory, determine the value of any runtime-computed GUCs that @@ -1379,7 +1394,7 @@ PostmasterMain(int argc, char *argv[]) UpdatePMState(PM_STARTUP); /* Make sure we can perform I/O while starting up. */ - maybe_adjust_io_workers(); + maybe_start_io_workers(); /* Start bgwriter and checkpointer so they can help with recovery */ if (CheckpointerPMChild == NULL) @@ -1543,33 +1558,44 @@ checkControlFile(void) static int DetermineSleepTime(void) { - TimestampTz next_wakeup = 0; + TimestampTz next_wakeup; /* - * Normal case: either there are no background workers at all, or we're in - * a shutdown sequence (during which we ignore bgworkers altogether). + * If in ImmediateShutdown with a SIGKILL timeout, ignore everything else + * and wait for that. + * + * XXX Shouldn't this also test FatalError? */ - if (Shutdown > NoShutdown || - (!StartWorkerNeeded && !HaveCrashedWorker)) + if (Shutdown >= ImmediateShutdown) { if (AbortStartTime != 0) { + time_t curtime = time(NULL); int seconds; - /* time left to abort; clamp to 0 in case it already expired */ - seconds = SIGKILL_CHILDREN_AFTER_SECS - - (time(NULL) - AbortStartTime); + /* + * time left to abort; clamp to 0 if it already expired, or if + * time goes backwards + */ + if (curtime < AbortStartTime || + curtime - AbortStartTime >= SIGKILL_CHILDREN_AFTER_SECS) + seconds = 0; + else + seconds = SIGKILL_CHILDREN_AFTER_SECS - + (curtime - AbortStartTime); - return Max(seconds * 1000, 0); + return seconds * 1000; } - else - return 60 * 1000; } - if (StartWorkerNeeded) + /* Time of next maybe_start_io_workers() call, or 0 for none. */ + next_wakeup = maybe_start_io_workers_scheduled_at(); + + /* Ignore bgworkers during shutdown. */ + if (StartWorkerNeeded && Shutdown == NoShutdown) return 0; - if (HaveCrashedWorker) + if (HaveCrashedWorker && Shutdown == NoShutdown) { dlist_mutable_iter iter; @@ -1934,6 +1960,9 @@ InitProcessGlobals(void) MyStartTimestamp = GetCurrentTimestamp(); MyStartTime = timestamptz_to_time_t(MyStartTimestamp); + /* initialize timing infrastructure (required for INSTR_* calls) */ + pg_initialize_timing(); + /* * Set a different global seed in every process. We want something * unpredictable, so if possible, use high-quality random bits for the @@ -2277,29 +2306,13 @@ process_pm_child_exit(void) } /* - * Unexpected exit of startup process (including FATAL exit) - * during PM_STARTUP is treated as catastrophic. There are no - * other processes running yet, so we can just exit. - */ - if (pmState == PM_STARTUP && - StartupStatus != STARTUP_SIGNALED && - !EXIT_STATUS_0(exitstatus)) - { - LogChildExit(LOG, _("startup process"), - pid, exitstatus); - ereport(LOG, - (errmsg("aborting startup due to startup process failure"))); - ExitPostmaster(1); - } - - /* - * After PM_STARTUP, any unexpected exit (including FATAL exit) of - * the startup process is catastrophic, so kill other children, - * and set StartupStatus so we don't try to reinitialize after - * they're gone. Exception: if StartupStatus is STARTUP_SIGNALED, - * then we previously sent the startup process a SIGQUIT; so - * that's probably the reason it died, and we do want to try to - * restart in that case. + * Any unexpected exit (including FATAL exit) of the startup + * process is catastrophic, so kill other children, and set + * StartupStatus so we don't try to reinitialize after they're + * gone. Exception: if StartupStatus is STARTUP_SIGNALED, then we + * previously sent the startup process a SIGQUIT; so that's + * probably the reason it died, and we do want to try to restart + * in that case. * * This stanza also handles the case where we sent a SIGQUIT * during PM_STARTUP due to some dead-end child crashing: in that @@ -2522,7 +2535,17 @@ process_pm_child_exit(void) if (!EXIT_STATUS_0(exitstatus) && !EXIT_STATUS_1(exitstatus)) HandleChildCrash(pid, exitstatus, _("io worker")); - maybe_adjust_io_workers(); + /* + * A worker that exited with an error might have brought the pool + * size below io_min_workers, or allowed the queue to grow to the + * point where another worker called for growth. + * + * In the common case that a worker timed out due to idleness, no + * replacement needs to be started. maybe_start_io_workers() will + * figure that out. + */ + maybe_start_io_workers(); + continue; } @@ -2630,6 +2653,13 @@ CleanupBackend(PMChild *bp, } bp = NULL; + /* + * In a crash case, exit immediately without resetting background worker + * state. However, if restart_after_crash is enabled, the background + * worker state (e.g., rw_pid) still needs be reset so the worker can + * restart after crash recovery. This reset is handled in + * ResetBackgroundWorkerCrashTimes(), not here. + */ if (crashed) { HandleChildCrash(bp_pid, exitstatus, procname); @@ -2646,6 +2676,14 @@ CleanupBackend(PMChild *bp, if (bp_bgworker_notify) BackgroundWorkerStopNotifications(bp_pid); + /* + * If it was an autovacuum worker, wake up the launcher so that it can + * immediately launch a new worker or rebalance to cost limit setting of + * the remaining workers. + */ + if (bp_bkend_type == B_AUTOVAC_WORKER && AutoVacLauncherPMChild != NULL) + signal_child(AutoVacLauncherPMChild, SIGUSR2); + /* * If it was a background worker, also update its RegisteredBgWorker * entry. @@ -2727,12 +2765,9 @@ HandleFatalError(QuitSignalReason reason, bool consider_sigabrt) /* shouldn't have any children */ Assert(false); break; - case PM_STARTUP: - /* should have been handled in process_pm_child_exit */ - Assert(false); - break; /* wait for children to die */ + case PM_STARTUP: case PM_RECOVERY: case PM_HOT_STANDBY: case PM_RUN: @@ -2973,6 +3008,11 @@ PostmasterStateMachine(void) B_INVALID, B_STANDALONE_BACKEND); + /* also add data checksums processes */ + remainMask = btmask_add(remainMask, + B_DATACHECKSUMSWORKER_LAUNCHER, + B_DATACHECKSUMSWORKER_WORKER); + /* All types should be included in targetMask or remainMask */ Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask); } @@ -3209,13 +3249,20 @@ PostmasterStateMachine(void) /* re-read control file into local memory */ LocalProcessControlFile(true); - /* re-create shared memory and semaphores */ + /* + * Re-initialize shared memory and semaphores. Note: We don't call + * RegisterBuiltinShmemCallbacks(), we keep the old registrations. In + * order to re-register structs in extensions, we'd need to reload + * shared preload libraries, and we don't want to do that. + */ + ResetShmemAllocator(); + ShmemCallRequestCallbacks(); CreateSharedMemoryAndSemaphores(); UpdatePMState(PM_STARTUP); /* Make sure we can perform I/O while starting up. */ - maybe_adjust_io_workers(); + maybe_start_io_workers(); StartupPMChild = StartChildProcess(B_STARTUP); Assert(StartupPMChild != NULL); @@ -3289,7 +3336,7 @@ LaunchMissingBackgroundProcesses(void) * A config file change will always lead to this function being called, so * we always will process the config change in a timely manner. */ - maybe_adjust_io_workers(); + maybe_start_io_workers(); /* * The checkpointer and the background writer are active from the start, @@ -3373,7 +3420,7 @@ LaunchMissingBackgroundProcesses(void) Shutdown <= SmartShutdown) { WalReceiverPMChild = StartChildProcess(B_WAL_RECEIVER); - if (WalReceiverPMChild != 0) + if (WalReceiverPMChild != NULL) WalReceiverRequested = false; /* else leave the flag set, so we'll try again later */ } @@ -3750,6 +3797,16 @@ process_pm_pmsignal(void) StartWorkerNeeded = true; } + /* Process IO worker start requests. */ + if (CheckPostmasterSignal(PMSIGNAL_IO_WORKER_GROW)) + { + /* + * No local flag, as the state is exposed through pgaio_worker_*() + * functions. This signal is received on potentially actionable level + * changes, so that maybe_start_io_workers() will run. + */ + } + /* Process background worker state changes. */ if (CheckPostmasterSignal(PMSIGNAL_BACKGROUND_WORKER_CHANGE)) { @@ -3892,7 +3949,7 @@ process_pm_pmsignal(void) * Dummy signal handler * * We use this for signals that we don't actually use in the postmaster, - * but we do use in backends. If we were to SIG_IGN such signals in the + * but we do use in backends. If we were to PG_SIG_IGN such signals in the * postmaster, then a newly started backend might drop a signal that arrives * before it's able to reconfigure its signal processing. (See notes in * tcop/postgres.c.) @@ -4191,19 +4248,18 @@ bgworker_should_start_now(BgWorkerStartTime start_time) case PM_RUN: if (start_time == BgWorkerStart_RecoveryFinished) return true; - /* fall through */ + pg_fallthrough; case PM_HOT_STANDBY: if (start_time == BgWorkerStart_ConsistentState) return true; - /* fall through */ + pg_fallthrough; case PM_RECOVERY: case PM_STARTUP: case PM_INIT: if (start_time == BgWorkerStart_PostmasterStart) return true; - /* fall through */ } return false; @@ -4337,15 +4393,15 @@ maybe_start_bgworkers(void) static bool maybe_reap_io_worker(int pid) { - for (int id = 0; id < MAX_IO_WORKERS; ++id) + for (int i = 0; i < MAX_IO_WORKERS; ++i) { - if (io_worker_children[id] && - io_worker_children[id]->pid == pid) + if (io_worker_children[i] && + io_worker_children[i]->pid == pid) { - ReleasePostmasterChildSlot(io_worker_children[id]); + ReleasePostmasterChildSlot(io_worker_children[i]); --io_worker_count; - io_worker_children[id] = NULL; + io_worker_children[i] = NULL; return true; } } @@ -4353,77 +4409,145 @@ maybe_reap_io_worker(int pid) } /* - * Start or stop IO workers, to close the gap between the number of running - * workers and the number of configured workers. Used to respond to change of - * the io_workers GUC (by increasing and decreasing the number of workers), as - * well as workers terminating in response to errors (by starting - * "replacement" workers). + * Returns the next time at which maybe_start_io_workers() would start one or + * more I/O workers. Any time in the past means ASAP, and 0 means no worker + * is currently scheduled. + * + * This is called by DetermineSleepTime() and also maybe_start_io_workers() + * itself, to make sure that they agree. */ -static void -maybe_adjust_io_workers(void) +static TimestampTz +maybe_start_io_workers_scheduled_at(void) { if (!pgaio_workers_enabled()) - return; + return 0; /* * If we're in final shutting down state, then we're just waiting for all * processes to exit. */ if (pmState >= PM_WAIT_IO_WORKERS) - return; + return 0; /* Don't start new workers during an immediate shutdown either. */ if (Shutdown >= ImmediateShutdown) - return; + return 0; /* * Don't start new workers if we're in the shutdown phase of a crash * restart. But we *do* need to start if we're already starting up again. */ if (FatalError && pmState >= PM_STOP_BACKENDS) - return; + return 0; + + /* + * Don't start a worker if we're at or above the maximum. (Excess workers + * exit when the GUC is lowered, but the count can be temporarily too high + * until they are reaped.) + */ + if (io_worker_count >= io_max_workers) + return 0; - Assert(pmState < PM_WAIT_IO_WORKERS); + /* If we're under the minimum, start a worker as soon as possible. */ + if (io_worker_count < io_min_workers) + return TIMESTAMP_MINUS_INFINITY; /* start worker ASAP */ - /* Not enough running? */ - while (io_worker_count < io_workers) + /* Only proceed if a "grow" signal has been received from a worker. */ + if (!pgaio_worker_pm_test_grow_signal_sent()) + return 0; + + /* + * maybe_start_io_workers() should start a new I/O worker after this time, + * or as soon as possible if is already in the past. + */ + return io_worker_launch_next_time; +} + +/* + * Start I/O workers if required. Used at startup, to respond to change of + * the io_min_workers GUC, when asked to start a new one due to submission + * queue backlog, and after workers terminate in response to errors (by + * starting "replacement" workers). + */ +static void +maybe_start_io_workers(void) +{ + TimestampTz scheduled_at; + + while ((scheduled_at = maybe_start_io_workers_scheduled_at()) != 0) { + TimestampTz now = GetCurrentTimestamp(); PMChild *child; - int id; + int i; + + Assert(pmState < PM_WAIT_IO_WORKERS); + + /* Still waiting for the scheduled time? */ + if (scheduled_at > now) + break; + + /* + * Compute next launch time relative to the previous value, so that + * time spent on the postmaster's other duties don't result in an + * inaccurate launch interval. + */ + io_worker_launch_next_time = + TimestampTzPlusMilliseconds(io_worker_launch_next_time, + io_worker_launch_interval); + + /* + * If that's already in the past, the interval is either impossibly + * short or we received no requests for new workers for a period. + * Compute a new future time relative to now instead. + */ + if (io_worker_launch_next_time <= now) + io_worker_launch_next_time = + TimestampTzPlusMilliseconds(now, io_worker_launch_interval); + + /* + * Check if a grow signal has been received, but the grow request has + * been canceled since then because work ran out. We've still + * advanced the next launch time, to suppress repeat signals from + * workers until then. + */ + if (io_worker_count >= io_min_workers && !pgaio_worker_pm_test_grow()) + { + pgaio_worker_pm_clear_grow_signal_sent(); + break; + } /* find unused entry in io_worker_children array */ - for (id = 0; id < MAX_IO_WORKERS; ++id) + for (i = 0; i < MAX_IO_WORKERS; ++i) { - if (io_worker_children[id] == NULL) + if (io_worker_children[i] == NULL) break; } - if (id == MAX_IO_WORKERS) - elog(ERROR, "could not find a free IO worker ID"); + if (i == MAX_IO_WORKERS) + elog(ERROR, "could not find a free IO worker slot"); /* Try to launch one. */ child = StartChildProcess(B_IO_WORKER); if (child != NULL) { - io_worker_children[id] = child; + io_worker_children[i] = child; ++io_worker_count; } else - break; /* try again next time */ - } - - /* Too many running? */ - if (io_worker_count > io_workers) - { - /* ask the IO worker in the highest slot to exit */ - for (int id = MAX_IO_WORKERS - 1; id >= 0; --id) { - if (io_worker_children[id] != NULL) - { - kill(io_worker_children[id]->pid, SIGUSR2); - break; - } + /* + * Fork failure: we'll try again after the launch interval + * expires, or be called again without delay if we don't yet have + * io_min_workers. Don't loop here though, the postmaster has + * other duties. + */ + break; } } + + /* + * Workers decide when to shut down by themselves, according to the + * io_max_workers and io_worker_idle_timeout GUCs. + */ } @@ -4544,7 +4668,7 @@ pgwin32_register_deadchild_callback(HANDLE procHandle, DWORD procId) { win32_deadchild_waitinfo *childinfo; - childinfo = palloc(sizeof(win32_deadchild_waitinfo)); + childinfo = palloc_object(win32_deadchild_waitinfo); childinfo->procHandle = procHandle; childinfo->procId = procId; diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f6f..b46bac681fef5 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -9,7 +9,7 @@ * though.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -217,7 +217,6 @@ StartupProcessMain(const void *startup_data, size_t startup_data_len) { Assert(startup_data_len == 0); - MyBackendType = B_STARTUP; AuxiliaryProcessMainCommon(); /* Arrange to clean up at startup process exit */ @@ -227,18 +226,18 @@ StartupProcessMain(const void *startup_data, size_t startup_data_len) * Properly accept or ignore signals the postmaster might send us. */ pqsignal(SIGHUP, StartupProcSigHupHandler); /* reload config file */ - pqsignal(SIGINT, SIG_IGN); /* ignore query cancel */ + pqsignal(SIGINT, PG_SIG_IGN); /* ignore query cancel */ pqsignal(SIGTERM, StartupProcShutdownHandler); /* request shutdown */ /* SIGQUIT handler was already set up by InitPostmasterChild */ InitializeTimeouts(); /* establishes SIGALRM handler */ - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, StartupProcTriggerHandler); /* * Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * Register timeouts needed for standby mode diff --git a/src/backend/postmaster/syslogger.c b/src/backend/postmaster/syslogger.c index 50c2edec1f611..acfe0a01715e6 100644 --- a/src/backend/postmaster/syslogger.c +++ b/src/backend/postmaster/syslogger.c @@ -13,7 +13,7 @@ * * Author: Andreas Pflug * - * Copyright (c) 2004-2025, PostgreSQL Global Development Group + * Copyright (c) 2004-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -51,6 +51,7 @@ #include "utils/guc.h" #include "utils/memutils.h" #include "utils/ps_status.h" +#include "utils/wait_event.h" /* * We read() into a temp buffer twice as big as a chunk, so that any fragment @@ -206,7 +207,6 @@ SysLoggerMain(const void *startup_data, size_t startup_data_len) now = MyStartTime; - MyBackendType = B_LOGGER; init_ps_display(NULL); /* @@ -276,18 +276,18 @@ SysLoggerMain(const void *startup_data, size_t startup_data_len) pqsignal(SIGHUP, SignalHandlerForConfigReload); /* set flag to read config * file */ - pqsignal(SIGINT, SIG_IGN); - pqsignal(SIGTERM, SIG_IGN); - pqsignal(SIGQUIT, SIG_IGN); - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGINT, PG_SIG_IGN); + pqsignal(SIGTERM, PG_SIG_IGN); + pqsignal(SIGQUIT, PG_SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, sigUsr1Handler); /* request log rotation */ - pqsignal(SIGUSR2, SIG_IGN); + pqsignal(SIGUSR2, PG_SIG_IGN); /* * Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); @@ -887,7 +887,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer) { PipeProtoHeader p; int chunklen; - bits8 dest_flags; + uint8 dest_flags; /* Do we have a valid header? */ memcpy(&p, cursor, offsetof(PipeProtoHeader, data)); @@ -960,7 +960,7 @@ process_pipe_input(char *logbuffer, int *bytes_in_logbuffer) * Need a free slot, but there isn't one in the list, * so create a new one and extend the list with it. */ - free_slot = palloc(sizeof(save_buffer)); + free_slot = palloc_object(save_buffer); buffer_list = lappend(buffer_list, free_slot); buffer_lists[p.pid % NBUFFER_LISTS] = buffer_list; } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f871ce..4f12eaf2c8527 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -13,7 +13,7 @@ * summary files when the file timestamp is older than a configurable * threshold (but only if the WAL has been removed first). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/postmaster/walsummarizer.c @@ -23,6 +23,7 @@ #include "postgres.h" #include "access/timeline.h" +#include "access/visibilitymap.h" #include "access/xlog.h" #include "access/xlog_internal.h" #include "access/xlogrecovery.h" @@ -46,6 +47,7 @@ #include "storage/proc.h" #include "storage/procsignal.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/guc.h" #include "utils/memutils.h" #include "utils/wait_event.h" @@ -108,6 +110,14 @@ typedef struct /* Pointer to shared memory state. */ static WalSummarizerData *WalSummarizerCtl; +static void WalSummarizerShmemRequest(void *arg); +static void WalSummarizerShmemInit(void *arg); + +const ShmemCallbacks WalSummarizerShmemCallbacks = { + .request_fn = WalSummarizerShmemRequest, + .init_fn = WalSummarizerShmemInit, +}; + /* * When we reach end of WAL and need to read more, we sleep for a number of * milliseconds that is an integer multiple of MS_PER_SLEEP_QUANTUM. This is @@ -167,43 +177,34 @@ static void summarizer_wait_for_wal(void); static void MaybeRemoveOldWalSummaries(void); /* - * Amount of shared memory required for this module. + * Register shared memory space needed by this module. */ -Size -WalSummarizerShmemSize(void) +static void +WalSummarizerShmemRequest(void *arg) { - return sizeof(WalSummarizerData); + ShmemRequestStruct(.name = "Wal Summarizer Ctl", + .size = sizeof(WalSummarizerData), + .ptr = (void **) &WalSummarizerCtl, + ); } /* - * Create or attach to shared memory segment for this module. + * Initialize shared memory for this module. */ -void -WalSummarizerShmemInit(void) +static void +WalSummarizerShmemInit(void *arg) { - bool found; - - WalSummarizerCtl = (WalSummarizerData *) - ShmemInitStruct("Wal Summarizer Ctl", WalSummarizerShmemSize(), - &found); - - if (!found) - { - /* - * First time through, so initialize. - * - * We're just filling in dummy values here -- the real initialization - * will happen when GetOldestUnsummarizedLSN() is called for the first - * time. - */ - WalSummarizerCtl->initialized = false; - WalSummarizerCtl->summarized_tli = 0; - WalSummarizerCtl->summarized_lsn = InvalidXLogRecPtr; - WalSummarizerCtl->lsn_is_exact = false; - WalSummarizerCtl->summarizer_pgprocno = INVALID_PROC_NUMBER; - WalSummarizerCtl->pending_lsn = InvalidXLogRecPtr; - ConditionVariableInit(&WalSummarizerCtl->summary_file_cv); - } + /* + * We're just filling in dummy values here -- the real initialization will + * happen when GetOldestUnsummarizedLSN() is called for the first time. + */ + WalSummarizerCtl->initialized = false; + WalSummarizerCtl->summarized_tli = 0; + WalSummarizerCtl->summarized_lsn = InvalidXLogRecPtr; + WalSummarizerCtl->lsn_is_exact = false; + WalSummarizerCtl->summarizer_pgprocno = INVALID_PROC_NUMBER; + WalSummarizerCtl->pending_lsn = InvalidXLogRecPtr; + ConditionVariableInit(&WalSummarizerCtl->summary_file_cv); } /* @@ -234,7 +235,6 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) Assert(startup_data_len == 0); - MyBackendType = B_WAL_SUMMARIZER; AuxiliaryProcessMainCommon(); ereport(DEBUG1, @@ -242,18 +242,15 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) /* * Properly accept or ignore signals the postmaster might send us - * - * We have no particular use for SIGINT at the moment, but seems - * reasonable to treat like SIGTERM. */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGINT, SignalHandlerForShutdownRequest); + pqsignal(SIGINT, PG_SIG_IGN); /* no query to cancel */ pqsignal(SIGTERM, SignalHandlerForShutdownRequest); /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); /* not used */ + pqsignal(SIGUSR2, PG_SIG_IGN); /* not used */ /* Advertise ourselves. */ on_shmem_exit(WalSummarizerShutdown, (Datum) 0); @@ -270,7 +267,7 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) /* * Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * If an exception is encountered, processing resumes here. @@ -342,7 +339,7 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) * If we discover that WAL summarization is not enabled, just exit. */ current_lsn = GetOldestUnsummarizedLSN(¤t_tli, &exact); - if (XLogRecPtrIsInvalid(current_lsn)) + if (!XLogRecPtrIsValid(current_lsn)) proc_exit(0); /* @@ -379,13 +376,13 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) * only have to do this once per timeline switch, we probably wouldn't * save any significant amount of work in practice. */ - if (current_tli != latest_tli && XLogRecPtrIsInvalid(switch_lsn)) + if (current_tli != latest_tli && !XLogRecPtrIsValid(switch_lsn)) { List *tles = readTimeLineHistory(latest_tli); switch_lsn = tliSwitchPoint(current_tli, tles, &switch_tli); ereport(DEBUG1, - errmsg_internal("switch point from TLI %u to TLI %u is at %X/%X", + errmsg_internal("switch point from TLI %u to TLI %u is at %X/%08X", current_tli, switch_tli, LSN_FORMAT_ARGS(switch_lsn))); } @@ -394,7 +391,7 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) * on this timeline. Switch to the next timeline and go around again, * backing up to the exact switch point if we passed it. */ - if (!XLogRecPtrIsInvalid(switch_lsn) && current_lsn >= switch_lsn) + if (XLogRecPtrIsValid(switch_lsn) && current_lsn >= switch_lsn) { /* Restart summarization from switch point. */ current_tli = switch_tli; @@ -419,7 +416,7 @@ WalSummarizerMain(const void *startup_data, size_t startup_data_len) end_of_summary_lsn = SummarizeWAL(current_tli, current_lsn, exact, switch_lsn, latest_lsn); - Assert(!XLogRecPtrIsInvalid(end_of_summary_lsn)); + Assert(XLogRecPtrIsValid(end_of_summary_lsn)); Assert(end_of_summary_lsn >= current_lsn); /* @@ -644,12 +641,12 @@ WakeupWalSummarizer(void) if (WalSummarizerCtl == NULL) return; - LWLockAcquire(WALSummarizerLock, LW_EXCLUSIVE); + LWLockAcquire(WALSummarizerLock, LW_SHARED); pgprocno = WalSummarizerCtl->summarizer_pgprocno; LWLockRelease(WALSummarizerLock); if (pgprocno != INVALID_PROC_NUMBER) - SetLatch(&ProcGlobal->allProcs[pgprocno].procLatch); + SetLatch(&GetPGProcByNumber(pgprocno)->procLatch); } /* @@ -685,7 +682,7 @@ WaitForWalSummarization(XLogRecPtr lsn) /* * If the LSN summarized on disk has reached the target value, stop. */ - LWLockAcquire(WALSummarizerLock, LW_EXCLUSIVE); + LWLockAcquire(WALSummarizerLock, LW_SHARED); summarized_lsn = WalSummarizerCtl->summarized_lsn; pending_lsn = WalSummarizerCtl->pending_lsn; LWLockRelease(WALSummarizerLock); @@ -741,7 +738,7 @@ WaitForWalSummarization(XLogRecPtr lsn) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("WAL summarization is not progressing"), - errdetail("Summarization is needed through %X/%X, but is stuck at %X/%X on disk and %X/%X in memory.", + errdetail("Summarization is needed through %X/%08X, but is stuck at %X/%08X on disk and %X/%08X in memory.", LSN_FORMAT_ARGS(lsn), LSN_FORMAT_ARGS(summarized_lsn), LSN_FORMAT_ARGS(pending_lsn)))); @@ -755,12 +752,12 @@ WaitForWalSummarization(XLogRecPtr lsn) current_time) / 1000; ereport(WARNING, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg_plural("still waiting for WAL summarization through %X/%X after %ld second", - "still waiting for WAL summarization through %X/%X after %ld seconds", + errmsg_plural("still waiting for WAL summarization through %X/%08X after %ld second", + "still waiting for WAL summarization through %X/%08X after %ld seconds", elapsed_seconds, LSN_FORMAT_ARGS(lsn), elapsed_seconds), - errdetail("Summarization has reached %X/%X on disk and %X/%X in memory.", + errdetail("Summarization has reached %X/%08X on disk and %X/%08X in memory.", LSN_FORMAT_ARGS(summarized_lsn), LSN_FORMAT_ARGS(pending_lsn)))); } @@ -918,12 +915,12 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, WalSummaryIO io; BlockRefTable *brtab = CreateEmptyBlockRefTable(); bool fast_forward = true; + char *errormsg; /* Initialize private data for xlogreader. */ - private_data = (SummarizerReadLocalXLogPrivate *) - palloc0(sizeof(SummarizerReadLocalXLogPrivate)); + private_data = palloc0_object(SummarizerReadLocalXLogPrivate); private_data->tli = tli; - private_data->historic = !XLogRecPtrIsInvalid(switch_lsn); + private_data->historic = XLogRecPtrIsValid(switch_lsn); private_data->read_upto = maximum_lsn; /* Create xlogreader. */ @@ -970,8 +967,8 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, } else { - summary_start_lsn = XLogFindNextRecord(xlogreader, start_lsn); - if (XLogRecPtrIsInvalid(summary_start_lsn)) + summary_start_lsn = XLogFindNextRecord(xlogreader, start_lsn, &errormsg); + if (!XLogRecPtrIsValid(summary_start_lsn)) { /* * If we hit end-of-WAL while trying to find the next valid @@ -981,7 +978,7 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, if (private_data->end_of_wal) { ereport(DEBUG1, - errmsg_internal("could not read WAL from timeline %u at %X/%X: end of WAL at %X/%X", + errmsg_internal("could not read WAL from timeline %u at %X/%08X: end of WAL at %X/%08X", tli, LSN_FORMAT_ARGS(start_lsn), LSN_FORMAT_ARGS(private_data->read_upto))); @@ -999,9 +996,16 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, switch_lsn = xlogreader->EndRecPtr; } else - ereport(ERROR, - (errmsg("could not find a valid record after %X/%X", - LSN_FORMAT_ARGS(start_lsn)))); + { + if (errormsg) + ereport(ERROR, + errmsg("could not find a valid record after %X/%08X: %s", + LSN_FORMAT_ARGS(start_lsn), errormsg)); + else + ereport(ERROR, + errmsg("could not find a valid record after %X/%08X", + LSN_FORMAT_ARGS(start_lsn))); + } } /* We shouldn't go backward. */ @@ -1014,7 +1018,6 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, while (1) { int block_id; - char *errormsg; XLogRecord *record; uint8 rmid; @@ -1034,7 +1037,7 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, * able to read a complete record. */ ereport(DEBUG1, - errmsg_internal("could not read WAL from timeline %u at %X/%X: end of WAL at %X/%X", + errmsg_internal("could not read WAL from timeline %u at %X/%08X: end of WAL at %X/%08X", tli, LSN_FORMAT_ARGS(xlogreader->EndRecPtr), LSN_FORMAT_ARGS(private_data->read_upto))); @@ -1045,20 +1048,20 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, if (errormsg) ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read WAL from timeline %u at %X/%X: %s", + errmsg("could not read WAL from timeline %u at %X/%08X: %s", tli, LSN_FORMAT_ARGS(xlogreader->EndRecPtr), errormsg))); else ereport(ERROR, (errcode_for_file_access(), - errmsg("could not read WAL from timeline %u at %X/%X", + errmsg("could not read WAL from timeline %u at %X/%08X", tli, LSN_FORMAT_ARGS(xlogreader->EndRecPtr)))); } /* We shouldn't go backward. */ Assert(summary_start_lsn <= xlogreader->EndRecPtr); - if (!XLogRecPtrIsInvalid(switch_lsn) && + if (XLogRecPtrIsValid(switch_lsn) && xlogreader->ReadRecPtr >= switch_lsn) { /* @@ -1180,7 +1183,7 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, * If we have a switch LSN and have reached it, stop before reading * the next record. */ - if (!XLogRecPtrIsInvalid(switch_lsn) && + if (XLogRecPtrIsValid(switch_lsn) && xlogreader->EndRecPtr >= switch_lsn) break; } @@ -1222,7 +1225,7 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, /* Tell the user what we did. */ ereport(DEBUG1, - errmsg_internal("summarized WAL on TLI %u from %X/%X to %X/%X", + errmsg_internal("summarized WAL on TLI %u from %X/%08X to %X/%08X", tli, LSN_FORMAT_ARGS(summary_start_lsn), LSN_FORMAT_ARGS(summary_end_lsn))); @@ -1234,7 +1237,7 @@ SummarizeWAL(TimeLineID tli, XLogRecPtr start_lsn, bool exact, /* If we skipped a non-zero amount of WAL, log a debug message. */ if (summary_end_lsn > summary_start_lsn && fast_forward) ereport(DEBUG1, - errmsg_internal("skipped summarizing WAL on TLI %u from %X/%X to %X/%X", + errmsg_internal("skipped summarizing WAL on TLI %u from %X/%08X to %X/%08X", tli, LSN_FORMAT_ARGS(summary_start_lsn), LSN_FORMAT_ARGS(summary_end_lsn))); @@ -1356,7 +1359,8 @@ SummarizeSmgrRecord(XLogReaderState *xlogreader, BlockRefTable *brtab) MAIN_FORKNUM, xlrec->blkno); if ((xlrec->flags & SMGR_TRUNCATE_VM) != 0) BlockRefTableSetLimitBlock(brtab, &xlrec->rlocator, - VISIBILITYMAP_FORKNUM, xlrec->blkno); + VISIBILITYMAP_FORKNUM, + visibilitymap_truncation_length(xlrec->blkno)); } } @@ -1431,8 +1435,11 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward) if (info == XLOG_CHECKPOINT_REDO) { + xl_checkpoint_redo xlrec; + /* Payload is wal_level at the time record was written. */ - memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int)); + memcpy(&xlrec, XLogRecGetData(xlogreader), sizeof(xl_checkpoint_redo)); + record_wal_level = xlrec.wal_level; } else if (info == XLOG_CHECKPOINT_SHUTDOWN) { @@ -1580,7 +1587,7 @@ summarizer_read_local_xlog_page(XLogReaderState *state, /* Debugging output. */ ereport(DEBUG1, - errmsg_internal("timeline %u became historic, can read up to %X/%X", + errmsg_internal("timeline %u became historic, can read up to %X/%08X", private_data->tli, LSN_FORMAT_ARGS(private_data->read_upto))); } @@ -1723,7 +1730,7 @@ MaybeRemoveOldWalSummaries(void) * If the WAL doesn't exist any more, we can remove it if the file * modification time is old enough. */ - if (XLogRecPtrIsInvalid(oldest_lsn) || ws->end_lsn <= oldest_lsn) + if (!XLogRecPtrIsValid(oldest_lsn) || ws->end_lsn <= oldest_lsn) RemoveWalSummaryIfOlderThan(ws, cutoff_time); /* diff --git a/src/backend/postmaster/walwriter.c b/src/backend/postmaster/walwriter.c index fd92c8b7a33e1..af24d05c542f8 100644 --- a/src/backend/postmaster/walwriter.c +++ b/src/backend/postmaster/walwriter.c @@ -31,7 +31,7 @@ * should be killed by SIGQUIT and then a recovery cycle started. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -62,6 +62,7 @@ #include "utils/hsearch.h" #include "utils/memutils.h" #include "utils/resowner.h" +#include "utils/wait_event.h" /* @@ -94,28 +95,24 @@ WalWriterMain(const void *startup_data, size_t startup_data_len) Assert(startup_data_len == 0); - MyBackendType = B_WAL_WRITER; AuxiliaryProcessMainCommon(); /* * Properly accept or ignore signals the postmaster might send us - * - * We have no particular use for SIGINT at the moment, but seems - * reasonable to treat like SIGTERM. */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGINT, SignalHandlerForShutdownRequest); + pqsignal(SIGINT, PG_SIG_IGN); /* no query to cancel */ pqsignal(SIGTERM, SignalHandlerForShutdownRequest); /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); /* not used */ + pqsignal(SIGUSR2, PG_SIG_IGN); /* not used */ /* * Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* * Create a memory context that we will do all our work in. We do this so diff --git a/src/backend/regex/meson.build b/src/backend/regex/meson.build index 8bcd195b42abd..640930e9cb2a0 100644 --- a/src/backend/regex/meson.build +++ b/src/backend/regex/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'regcomp.c', diff --git a/src/backend/regex/regc_lex.c b/src/backend/regex/regc_lex.c index 9087ef95af3e9..55df64f9adeb3 100644 --- a/src/backend/regex/regc_lex.c +++ b/src/backend/regex/regc_lex.c @@ -743,7 +743,7 @@ lexescape(struct vars *v) /* oops, doesn't look like it's a backref after all... */ v->now = save; /* and fall through into octal number */ - /* FALLTHROUGH */ + pg_fallthrough; case CHR('0'): NOTE(REG_UUNPORT); v->now--; /* put first digit back */ diff --git a/src/backend/regex/regc_locale.c b/src/backend/regex/regc_locale.c index 77d1ce28168b2..847abcc35b321 100644 --- a/src/backend/regex/regc_locale.c +++ b/src/backend/regex/regc_locale.c @@ -453,7 +453,7 @@ range(struct vars *v, /* context */ for (c = a; c <= b; c++) { - cc = pg_wc_tolower(c); + cc = regc_wc_tolower(c); if (cc != c && (before(cc, a) || before(b, cc))) { @@ -464,7 +464,7 @@ range(struct vars *v, /* context */ } addchr(cv, cc); } - cc = pg_wc_toupper(c); + cc = regc_wc_toupper(c); if (cc != c && (before(cc, a) || before(b, cc))) { @@ -562,7 +562,7 @@ lookupcclass(struct vars *v, /* context (for returning errors) */ * Must include case counterparts if "cases" is true. * * The returned cvec might be either a transient cvec gotten from getcvec(), - * or a permanently cached one from pg_ctype_get_cache(). This is okay + * or a permanently cached one from regc_ctype_get_cache(). This is okay * because callers are not supposed to explicitly free the result either way. */ static struct cvec * @@ -584,7 +584,7 @@ cclasscvec(struct vars *v, /* context */ /* * Now compute the character class contents. For classes that are based * on the behavior of a or function, we use - * pg_ctype_get_cache so that we can cache the results. Other classes + * regc_ctype_get_cache so that we can cache the results. Other classes * have definitions that are hard-wired here, and for those we just * construct a transient cvec on the fly. * @@ -594,16 +594,16 @@ cclasscvec(struct vars *v, /* context */ switch (cclasscode) { case CC_PRINT: - cv = pg_ctype_get_cache(pg_wc_isprint, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isprint, cclasscode); break; case CC_ALNUM: - cv = pg_ctype_get_cache(pg_wc_isalnum, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isalnum, cclasscode); break; case CC_ALPHA: - cv = pg_ctype_get_cache(pg_wc_isalpha, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isalpha, cclasscode); break; case CC_WORD: - cv = pg_ctype_get_cache(pg_wc_isword, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isword, cclasscode); break; case CC_ASCII: /* hard-wired meaning */ @@ -624,10 +624,10 @@ cclasscvec(struct vars *v, /* context */ addrange(cv, 0x7f, 0x9f); break; case CC_DIGIT: - cv = pg_ctype_get_cache(pg_wc_isdigit, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isdigit, cclasscode); break; case CC_PUNCT: - cv = pg_ctype_get_cache(pg_wc_ispunct, cclasscode); + cv = regc_ctype_get_cache(regc_wc_ispunct, cclasscode); break; case CC_XDIGIT: @@ -645,16 +645,16 @@ cclasscvec(struct vars *v, /* context */ } break; case CC_SPACE: - cv = pg_ctype_get_cache(pg_wc_isspace, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isspace, cclasscode); break; case CC_LOWER: - cv = pg_ctype_get_cache(pg_wc_islower, cclasscode); + cv = regc_ctype_get_cache(regc_wc_islower, cclasscode); break; case CC_UPPER: - cv = pg_ctype_get_cache(pg_wc_isupper, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isupper, cclasscode); break; case CC_GRAPH: - cv = pg_ctype_get_cache(pg_wc_isgraph, cclasscode); + cv = regc_ctype_get_cache(regc_wc_isgraph, cclasscode); break; } @@ -679,29 +679,29 @@ cclass_column_index(struct colormap *cm, chr c) * Note: we should not see requests to consider cclasses that are not * treated as locale-specific by cclasscvec(), above. */ - if (cm->classbits[CC_PRINT] && pg_wc_isprint(c)) + if (cm->classbits[CC_PRINT] && regc_wc_isprint(c)) colnum |= cm->classbits[CC_PRINT]; - if (cm->classbits[CC_ALNUM] && pg_wc_isalnum(c)) + if (cm->classbits[CC_ALNUM] && regc_wc_isalnum(c)) colnum |= cm->classbits[CC_ALNUM]; - if (cm->classbits[CC_ALPHA] && pg_wc_isalpha(c)) + if (cm->classbits[CC_ALPHA] && regc_wc_isalpha(c)) colnum |= cm->classbits[CC_ALPHA]; - if (cm->classbits[CC_WORD] && pg_wc_isword(c)) + if (cm->classbits[CC_WORD] && regc_wc_isword(c)) colnum |= cm->classbits[CC_WORD]; assert(cm->classbits[CC_ASCII] == 0); assert(cm->classbits[CC_BLANK] == 0); assert(cm->classbits[CC_CNTRL] == 0); - if (cm->classbits[CC_DIGIT] && pg_wc_isdigit(c)) + if (cm->classbits[CC_DIGIT] && regc_wc_isdigit(c)) colnum |= cm->classbits[CC_DIGIT]; - if (cm->classbits[CC_PUNCT] && pg_wc_ispunct(c)) + if (cm->classbits[CC_PUNCT] && regc_wc_ispunct(c)) colnum |= cm->classbits[CC_PUNCT]; assert(cm->classbits[CC_XDIGIT] == 0); - if (cm->classbits[CC_SPACE] && pg_wc_isspace(c)) + if (cm->classbits[CC_SPACE] && regc_wc_isspace(c)) colnum |= cm->classbits[CC_SPACE]; - if (cm->classbits[CC_LOWER] && pg_wc_islower(c)) + if (cm->classbits[CC_LOWER] && regc_wc_islower(c)) colnum |= cm->classbits[CC_LOWER]; - if (cm->classbits[CC_UPPER] && pg_wc_isupper(c)) + if (cm->classbits[CC_UPPER] && regc_wc_isupper(c)) colnum |= cm->classbits[CC_UPPER]; - if (cm->classbits[CC_GRAPH] && pg_wc_isgraph(c)) + if (cm->classbits[CC_GRAPH] && regc_wc_isgraph(c)) colnum |= cm->classbits[CC_GRAPH]; return colnum; @@ -721,8 +721,8 @@ allcases(struct vars *v, /* context */ chr lc, uc; - lc = pg_wc_tolower(c); - uc = pg_wc_toupper(c); + lc = regc_wc_tolower(c); + uc = regc_wc_toupper(c); cv = getcvec(v, 2, 0); addchr(cv, lc); @@ -760,7 +760,7 @@ casecmp(const chr *x, const chr *y, /* strings to compare */ { for (; len > 0; len--, x++, y++) { - if ((*x != *y) && (pg_wc_tolower(*x) != pg_wc_tolower(*y))) + if ((*x != *y) && (regc_wc_tolower(*x) != regc_wc_tolower(*y))) return 1; } return 0; diff --git a/src/backend/regex/regc_pg_locale.c b/src/backend/regex/regc_pg_locale.c index 78193cfb964e5..83b7200651807 100644 --- a/src/backend/regex/regc_pg_locale.c +++ b/src/backend/regex/regc_pg_locale.c @@ -6,7 +6,7 @@ * * This file is #included by regcomp.c; it's not meant to compile standalone. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -19,203 +19,10 @@ #include "common/unicode_case.h" #include "common/unicode_category.h" #include "utils/pg_locale.h" +#include "utils/pg_locale_c.h" -/* - * For the libc provider, to provide as much functionality as possible on a - * variety of platforms without going so far as to implement everything from - * scratch, we use several implementation strategies depending on the - * situation: - * - * 1. In C/POSIX collations, we use hard-wired code. We can't depend on - * the functions since those will obey LC_CTYPE. Note that these - * collations don't give a fig about multibyte characters. - * - * 2. When working in UTF8 encoding, we use the functions. - * This assumes that every platform uses Unicode codepoints directly - * as the wchar_t representation of Unicode. (XXX: ICU makes this assumption - * even for non-UTF8 encodings, which may be a problem.) On some platforms - * wchar_t is only 16 bits wide, so we have to punt for codepoints > 0xFFFF. - * - * 3. In all other encodings, we use the functions for pg_wchar - * values up to 255, and punt for values above that. This is 100% correct - * only in single-byte encodings such as LATINn. However, non-Unicode - * multibyte encodings are mostly Far Eastern character sets for which the - * properties being tested here aren't very relevant for higher code values - * anyway. The difficulty with using the functions with - * non-Unicode multibyte encodings is that we can have no certainty that - * the platform's wchar_t representation matches what we do in pg_wchar - * conversions. - * - * As a special case, in the "default" collation, (2) and (3) force ASCII - * letters to follow ASCII upcase/downcase rules, while in a non-default - * collation we just let the library functions do what they will. The case - * where this matters is treatment of I/i in Turkish, and the behavior is - * meant to match the upper()/lower() SQL functions. - * - * We store the active collation setting in static variables. In principle - * it could be passed down to here via the regex library's "struct vars" data - * structure; but that would require somewhat invasive changes in the regex - * library, and right now there's no real benefit to be gained from that. - * - * NB: the coding here assumes pg_wchar is an unsigned type. - */ - -typedef enum -{ - PG_REGEX_STRATEGY_C, /* C locale (encoding independent) */ - PG_REGEX_STRATEGY_BUILTIN, /* built-in Unicode semantics */ - PG_REGEX_STRATEGY_LIBC_WIDE, /* Use locale_t functions */ - PG_REGEX_STRATEGY_LIBC_1BYTE, /* Use locale_t functions */ - PG_REGEX_STRATEGY_ICU, /* Use ICU uchar.h functions */ -} PG_Locale_Strategy; - -static PG_Locale_Strategy pg_regex_strategy; static pg_locale_t pg_regex_locale; -/* - * Hard-wired character properties for C locale - */ -#define PG_ISDIGIT 0x01 -#define PG_ISALPHA 0x02 -#define PG_ISALNUM (PG_ISDIGIT | PG_ISALPHA) -#define PG_ISUPPER 0x04 -#define PG_ISLOWER 0x08 -#define PG_ISGRAPH 0x10 -#define PG_ISPRINT 0x20 -#define PG_ISPUNCT 0x40 -#define PG_ISSPACE 0x80 - -static const unsigned char pg_char_properties[128] = { - /* NUL */ 0, - /* ^A */ 0, - /* ^B */ 0, - /* ^C */ 0, - /* ^D */ 0, - /* ^E */ 0, - /* ^F */ 0, - /* ^G */ 0, - /* ^H */ 0, - /* ^I */ PG_ISSPACE, - /* ^J */ PG_ISSPACE, - /* ^K */ PG_ISSPACE, - /* ^L */ PG_ISSPACE, - /* ^M */ PG_ISSPACE, - /* ^N */ 0, - /* ^O */ 0, - /* ^P */ 0, - /* ^Q */ 0, - /* ^R */ 0, - /* ^S */ 0, - /* ^T */ 0, - /* ^U */ 0, - /* ^V */ 0, - /* ^W */ 0, - /* ^X */ 0, - /* ^Y */ 0, - /* ^Z */ 0, - /* ^[ */ 0, - /* ^\ */ 0, - /* ^] */ 0, - /* ^^ */ 0, - /* ^_ */ 0, - /* */ PG_ISPRINT | PG_ISSPACE, - /* ! */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* " */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* # */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* $ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* % */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* & */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ' */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ( */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ) */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* * */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* + */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* , */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* - */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* . */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* / */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* 0 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 1 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 2 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 3 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 4 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 5 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 6 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 7 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 8 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* 9 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, - /* : */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ; */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* < */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* = */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* > */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ? */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* @ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* A */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* B */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* C */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* D */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* E */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* F */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* G */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* H */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* I */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* J */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* K */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* L */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* M */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* N */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* O */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* P */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* Q */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* R */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* S */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* T */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* U */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* V */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* W */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* X */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* Y */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* Z */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, - /* [ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* \ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ] */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ^ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* _ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ` */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* a */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* b */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* c */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* d */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* e */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* f */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* g */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* h */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* i */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* j */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* k */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* l */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* m */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* n */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* o */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* p */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* q */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* r */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* s */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* t */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* u */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* v */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* w */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* x */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* y */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* z */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, - /* { */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* | */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* } */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* ~ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, - /* DEL */ 0 -}; - /* * pg_set_regex_collation: set collation for these functions to obey @@ -228,7 +35,6 @@ void pg_set_regex_collation(Oid collation) { pg_locale_t locale = 0; - PG_Locale_Strategy strategy; if (!OidIsValid(collation)) { @@ -242,377 +48,144 @@ pg_set_regex_collation(Oid collation) errhint("Use the COLLATE clause to set the collation explicitly."))); } - if (collation == C_COLLATION_OID) - { - /* - * Some callers expect regexes to work for C_COLLATION_OID before - * catalog access is available, so we can't call - * pg_newlocale_from_collation(). - */ - strategy = PG_REGEX_STRATEGY_C; - locale = 0; - } - else - { - locale = pg_newlocale_from_collation(collation); - - if (!locale->deterministic) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("nondeterministic collations are not supported for regular expressions"))); + locale = pg_newlocale_from_collation(collation); - if (locale->ctype_is_c) - { - /* - * C/POSIX collations use this path regardless of database - * encoding - */ - strategy = PG_REGEX_STRATEGY_C; - locale = 0; - } - else if (locale->provider == COLLPROVIDER_BUILTIN) - { - Assert(GetDatabaseEncoding() == PG_UTF8); - strategy = PG_REGEX_STRATEGY_BUILTIN; - } -#ifdef USE_ICU - else if (locale->provider == COLLPROVIDER_ICU) - { - strategy = PG_REGEX_STRATEGY_ICU; - } -#endif - else - { - Assert(locale->provider == COLLPROVIDER_LIBC); - if (GetDatabaseEncoding() == PG_UTF8) - strategy = PG_REGEX_STRATEGY_LIBC_WIDE; - else - strategy = PG_REGEX_STRATEGY_LIBC_1BYTE; - } - } + if (!locale->deterministic) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("nondeterministic collations are not supported for regular expressions"))); - pg_regex_strategy = strategy; pg_regex_locale = locale; } +/* + * The following functions overlap with those defined in pg_locale.c. XXX: + * consider refactor. + */ + static int -pg_wc_isdigit(pg_wchar c) +regc_wc_isdigit(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISDIGIT)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isdigit(c, !pg_regex_locale->info.builtin.casemap_full); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswdigit_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isdigit_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isdigit(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISDIGIT)); + else + return pg_regex_locale->ctype->wc_isdigit(c, pg_regex_locale); } static int -pg_wc_isalpha(pg_wchar c) +regc_wc_isalpha(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISALPHA)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isalpha(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswalpha_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isalpha_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isalpha(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISALPHA)); + else + return pg_regex_locale->ctype->wc_isalpha(c, pg_regex_locale); } static int -pg_wc_isalnum(pg_wchar c) +regc_wc_isalnum(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISALNUM)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isalnum(c, !pg_regex_locale->info.builtin.casemap_full); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswalnum_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isalnum_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isalnum(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISALNUM)); + else + return pg_regex_locale->ctype->wc_isalnum(c, pg_regex_locale); } static int -pg_wc_isword(pg_wchar c) +regc_wc_isword(pg_wchar c) { /* We define word characters as alnum class plus underscore */ if (c == CHR('_')) return 1; - return pg_wc_isalnum(c); + return regc_wc_isalnum(c); } static int -pg_wc_isupper(pg_wchar c) +regc_wc_isupper(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISUPPER)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isupper(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswupper_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isupper_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isupper(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISUPPER)); + else + return pg_regex_locale->ctype->wc_isupper(c, pg_regex_locale); } static int -pg_wc_islower(pg_wchar c) +regc_wc_islower(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISLOWER)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_islower(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswlower_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - islower_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_islower(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISLOWER)); + else + return pg_regex_locale->ctype->wc_islower(c, pg_regex_locale); } static int -pg_wc_isgraph(pg_wchar c) +regc_wc_isgraph(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISGRAPH)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isgraph(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswgraph_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isgraph_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isgraph(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISGRAPH)); + else + return pg_regex_locale->ctype->wc_isgraph(c, pg_regex_locale); } static int -pg_wc_isprint(pg_wchar c) +regc_wc_isprint(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISPRINT)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isprint(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswprint_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isprint_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isprint(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISPRINT)); + else + return pg_regex_locale->ctype->wc_isprint(c, pg_regex_locale); } static int -pg_wc_ispunct(pg_wchar c) +regc_wc_ispunct(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISPUNCT)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_ispunct(c, !pg_regex_locale->info.builtin.casemap_full); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswpunct_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - ispunct_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_ispunct(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISPUNCT)); + else + return pg_regex_locale->ctype->wc_ispunct(c, pg_regex_locale); } static int -pg_wc_isspace(pg_wchar c) +regc_wc_isspace(pg_wchar c) { - switch (pg_regex_strategy) - { - case PG_REGEX_STRATEGY_C: - return (c <= (pg_wchar) 127 && - (pg_char_properties[c] & PG_ISSPACE)); - case PG_REGEX_STRATEGY_BUILTIN: - return pg_u_isspace(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return iswspace_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - return (c <= (pg_wchar) UCHAR_MAX && - isspace_l((unsigned char) c, pg_regex_locale->info.lt)); - break; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_isspace(c); -#endif - break; - } - return 0; /* can't get here, but keep compiler quiet */ + if (pg_regex_locale->ctype_is_c) + return (c <= (pg_wchar) 127 && + (pg_char_properties[c] & PG_ISSPACE)); + else + return pg_regex_locale->ctype->wc_isspace(c, pg_regex_locale); } static pg_wchar -pg_wc_toupper(pg_wchar c) +regc_wc_toupper(pg_wchar c) { - switch (pg_regex_strategy) + if (pg_regex_locale->ctype_is_c) { - case PG_REGEX_STRATEGY_C: - if (c <= (pg_wchar) 127) - return pg_ascii_toupper((unsigned char) c); - return c; - case PG_REGEX_STRATEGY_BUILTIN: - return unicode_uppercase_simple(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - /* force C behavior for ASCII characters, per comments above */ - if (pg_regex_locale->is_default && c <= (pg_wchar) 127) - return pg_ascii_toupper((unsigned char) c); - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return towupper_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - /* force C behavior for ASCII characters, per comments above */ - if (pg_regex_locale->is_default && c <= (pg_wchar) 127) - return pg_ascii_toupper((unsigned char) c); - if (c <= (pg_wchar) UCHAR_MAX) - return toupper_l((unsigned char) c, pg_regex_locale->info.lt); - return c; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_toupper(c); -#endif - break; + if (c <= (pg_wchar) 127) + return pg_ascii_toupper((unsigned char) c); + return c; } - return 0; /* can't get here, but keep compiler quiet */ + else + return pg_regex_locale->ctype->wc_toupper(c, pg_regex_locale); } static pg_wchar -pg_wc_tolower(pg_wchar c) +regc_wc_tolower(pg_wchar c) { - switch (pg_regex_strategy) + if (pg_regex_locale->ctype_is_c) { - case PG_REGEX_STRATEGY_C: - if (c <= (pg_wchar) 127) - return pg_ascii_tolower((unsigned char) c); - return c; - case PG_REGEX_STRATEGY_BUILTIN: - return unicode_lowercase_simple(c); - case PG_REGEX_STRATEGY_LIBC_WIDE: - /* force C behavior for ASCII characters, per comments above */ - if (pg_regex_locale->is_default && c <= (pg_wchar) 127) - return pg_ascii_tolower((unsigned char) c); - if (sizeof(wchar_t) >= 4 || c <= (pg_wchar) 0xFFFF) - return towlower_l((wint_t) c, pg_regex_locale->info.lt); - /* FALL THRU */ - case PG_REGEX_STRATEGY_LIBC_1BYTE: - /* force C behavior for ASCII characters, per comments above */ - if (pg_regex_locale->is_default && c <= (pg_wchar) 127) - return pg_ascii_tolower((unsigned char) c); - if (c <= (pg_wchar) UCHAR_MAX) - return tolower_l((unsigned char) c, pg_regex_locale->info.lt); - return c; - case PG_REGEX_STRATEGY_ICU: -#ifdef USE_ICU - return u_tolower(c); -#endif - break; + if (c <= (pg_wchar) 127) + return pg_ascii_tolower((unsigned char) c); + return c; } - return 0; /* can't get here, but keep compiler quiet */ + else + return pg_regex_locale->ctype->wc_tolower(c, pg_regex_locale); } @@ -629,11 +202,11 @@ pg_wc_tolower(pg_wchar c) * the main regex code expects us to return a failure indication instead. */ -typedef int (*pg_wc_probefunc) (pg_wchar c); +typedef int (*regc_wc_probefunc) (pg_wchar c); typedef struct pg_ctype_cache { - pg_wc_probefunc probefunc; /* pg_wc_isalpha or a sibling */ + regc_wc_probefunc probefunc; /* regc_wc_isalpha or a sibling */ pg_locale_t locale; /* locale this entry is for */ struct cvec cv; /* cache entry contents */ struct pg_ctype_cache *next; /* chain link */ @@ -682,14 +255,14 @@ store_match(pg_ctype_cache *pcc, pg_wchar chr1, int nchrs) } /* - * Given a probe function (e.g., pg_wc_isalpha) get a struct cvec for all + * Given a probe function (e.g., regc_wc_isalpha) get a struct cvec for all * chrs satisfying the probe function. The active collation is the one * previously set by pg_set_regex_collation. Return NULL if out of memory. * * Note that the result must not be freed or modified by caller. */ static struct cvec * -pg_ctype_get_cache(pg_wc_probefunc probefunc, int cclasscode) +regc_ctype_get_cache(regc_wc_probefunc probefunc, int cclasscode) { pg_ctype_cache *pcc; pg_wchar max_chr; @@ -738,37 +311,27 @@ pg_ctype_get_cache(pg_wc_probefunc probefunc, int cclasscode) * would always be true for production values of MAX_SIMPLE_CHR, but it's * useful to allow it to be small for testing purposes.) */ - switch (pg_regex_strategy) + if (pg_regex_locale->ctype_is_c) { - case PG_REGEX_STRATEGY_C: #if MAX_SIMPLE_CHR >= 127 - max_chr = (pg_wchar) 127; - pcc->cv.cclasscode = -1; + max_chr = (pg_wchar) 127; + pcc->cv.cclasscode = -1; #else - max_chr = (pg_wchar) MAX_SIMPLE_CHR; + max_chr = (pg_wchar) MAX_SIMPLE_CHR; #endif - break; - case PG_REGEX_STRATEGY_BUILTIN: - max_chr = (pg_wchar) MAX_SIMPLE_CHR; - break; - case PG_REGEX_STRATEGY_LIBC_WIDE: - max_chr = (pg_wchar) MAX_SIMPLE_CHR; - break; - case PG_REGEX_STRATEGY_LIBC_1BYTE: + } + else if (GetDatabaseEncoding() == PG_UTF8) + { + max_chr = (pg_wchar) MAX_SIMPLE_CHR; + } + else + { #if MAX_SIMPLE_CHR >= UCHAR_MAX - max_chr = (pg_wchar) UCHAR_MAX; - pcc->cv.cclasscode = -1; + max_chr = (pg_wchar) UCHAR_MAX; + pcc->cv.cclasscode = -1; #else - max_chr = (pg_wchar) MAX_SIMPLE_CHR; + max_chr = (pg_wchar) MAX_SIMPLE_CHR; #endif - break; - case PG_REGEX_STRATEGY_ICU: - max_chr = (pg_wchar) MAX_SIMPLE_CHR; - break; - default: - Assert(false); - max_chr = 0; /* can't get here, but keep compiler quiet */ - break; } /* diff --git a/src/backend/regex/regcomp.c b/src/backend/regex/regcomp.c index 15b264e50f1a7..820995332bac0 100644 --- a/src/backend/regex/regcomp.c +++ b/src/backend/regex/regcomp.c @@ -249,18 +249,18 @@ static struct cvec *getcvec(struct vars *v, int nchrs, int nranges); static void freecvec(struct cvec *cv); /* === regc_pg_locale.c === */ -static int pg_wc_isdigit(pg_wchar c); -static int pg_wc_isalpha(pg_wchar c); -static int pg_wc_isalnum(pg_wchar c); -static int pg_wc_isword(pg_wchar c); -static int pg_wc_isupper(pg_wchar c); -static int pg_wc_islower(pg_wchar c); -static int pg_wc_isgraph(pg_wchar c); -static int pg_wc_isprint(pg_wchar c); -static int pg_wc_ispunct(pg_wchar c); -static int pg_wc_isspace(pg_wchar c); -static pg_wchar pg_wc_toupper(pg_wchar c); -static pg_wchar pg_wc_tolower(pg_wchar c); +static int regc_wc_isdigit(pg_wchar c); +static int regc_wc_isalpha(pg_wchar c); +static int regc_wc_isalnum(pg_wchar c); +static int regc_wc_isword(pg_wchar c); +static int regc_wc_isupper(pg_wchar c); +static int regc_wc_islower(pg_wchar c); +static int regc_wc_isgraph(pg_wchar c); +static int regc_wc_isprint(pg_wchar c); +static int regc_wc_ispunct(pg_wchar c); +static int regc_wc_isspace(pg_wchar c); +static pg_wchar regc_wc_toupper(pg_wchar c); +static pg_wchar regc_wc_tolower(pg_wchar c); /* === regc_locale.c === */ static chr element(struct vars *v, const chr *startp, const chr *endp); @@ -975,7 +975,7 @@ parseqatom(struct vars *v, /* legal in EREs due to specification botch */ NOTE(REG_UPBOTCH); /* fall through into case PLAIN */ - /* FALLTHROUGH */ + pg_fallthrough; case PLAIN: onechr(v, v->nextvalue, lp, rp); okcolors(v->nfa, v->cm); diff --git a/src/backend/regex/regexport.c b/src/backend/regex/regexport.c index 3866471c2a913..177a34cb93380 100644 --- a/src/backend/regex/regexport.c +++ b/src/backend/regex/regexport.c @@ -15,7 +15,7 @@ * allows the caller to decide how big is too big to bother with. * * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1998, 1999 Henry Spencer * * IDENTIFICATION diff --git a/src/backend/regex/regprefix.c b/src/backend/regex/regprefix.c index 8c07e963b8919..7957317311692 100644 --- a/src/backend/regex/regprefix.c +++ b/src/backend/regex/regprefix.c @@ -4,7 +4,7 @@ * Extract a common prefix, if any, from a compiled regex. * * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1998, 1999 Henry Spencer * * IDENTIFICATION diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c index 7b4ddf7a8f52f..9f04c9ed25da6 100644 --- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c +++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c @@ -9,7 +9,7 @@ * Apart from walreceiver, the libpq-specific routines are now being used by * logical replication workers and slot synchronization. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -65,6 +65,8 @@ static void libpqrcv_get_senderinfo(WalReceiverConn *conn, static char *libpqrcv_identify_system(WalReceiverConn *conn, TimeLineID *primary_tli); static char *libpqrcv_get_dbname_from_conninfo(const char *connInfo); +static char *libpqrcv_get_option_from_conninfo(const char *connInfo, + const char *keyword); static int libpqrcv_server_version(WalReceiverConn *conn); static void libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn, TimeLineID tli, char **filename, @@ -150,6 +152,7 @@ libpqrcv_connect(const char *conninfo, bool replication, bool logical, const char *keys[6]; const char *vals[6]; int i = 0; + char *options_val = NULL; /* * Re-validate connection string. The validation already happened at DDL @@ -177,6 +180,8 @@ libpqrcv_connect(const char *conninfo, bool replication, bool logical, if (logical) { + char *opt = NULL; + /* Tell the publisher to translate to our encoding */ keys[++i] = "client_encoding"; vals[i] = GetDatabaseEncodingName(); @@ -189,8 +194,13 @@ libpqrcv_connect(const char *conninfo, bool replication, bool logical, * running in the subscriber, such as triggers.) This should * match what pg_dump does. */ + opt = libpqrcv_get_option_from_conninfo(conninfo, "options"); + options_val = psprintf("%s -c datestyle=ISO -c intervalstyle=postgres -c extra_float_digits=3", + (opt == NULL) ? "" : opt); keys[++i] = "options"; - vals[i] = "-c datestyle=ISO -c intervalstyle=postgres -c extra_float_digits=3"; + vals[i] = options_val; + if (opt != NULL) + pfree(opt); } else { @@ -211,12 +221,15 @@ libpqrcv_connect(const char *conninfo, bool replication, bool logical, Assert(i < lengthof(keys)); - conn = palloc0(sizeof(WalReceiverConn)); + conn = palloc0_object(WalReceiverConn); conn->streamConn = libpqsrv_connect_params(keys, vals, /* expand_dbname = */ true, WAIT_EVENT_LIBPQWALRECEIVER_CONNECT); + if (options_val != NULL) + pfree(options_val); + if (PQstatus(conn->streamConn) != CONNECTION_OK) goto bad_connection_errmsg; @@ -232,6 +245,9 @@ libpqrcv_connect(const char *conninfo, bool replication, bool logical, errhint("Target server's authentication method must be changed, or set password_required=false in the subscription parameters."))); } + PQsetNoticeReceiver(conn->streamConn, libpqsrv_notice_receiver, + "received message via replication"); + /* * Set always-secure search path for the cases where the connection is * used to run SQL queries, so malicious users can't get control. @@ -418,31 +434,22 @@ libpqrcv_identify_system(WalReceiverConn *conn, TimeLineID *primary_tli) "IDENTIFY_SYSTEM", WAIT_EVENT_LIBPQWALRECEIVER_RECEIVE); if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("could not receive database system identifier and timeline ID from " "the primary server: %s", pchomp(PQerrorMessage(conn->streamConn))))); - } /* * IDENTIFY_SYSTEM returns 3 columns in 9.3 and earlier, and 4 columns in * 9.4 and onwards. */ if (PQnfields(res) < 3 || PQntuples(res) != 1) - { - int ntuples = PQntuples(res); - int nfields = PQnfields(res); - - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("invalid response from primary server"), errdetail("Could not identify system: got %d rows and %d fields, expected %d rows and %d or more fields.", - ntuples, nfields, 1, 3))); - } + PQntuples(res), PQnfields(res), 1, 3))); primary_sysid = pstrdup(PQgetvalue(res, 0, 0)); *primary_tli = pg_strtoint32(PQgetvalue(res, 0, 1)); PQclear(res); @@ -466,9 +473,21 @@ libpqrcv_server_version(WalReceiverConn *conn) */ static char * libpqrcv_get_dbname_from_conninfo(const char *connInfo) +{ + return libpqrcv_get_option_from_conninfo(connInfo, "dbname"); +} + +/* + * Get the value of the option with the given keyword from the primary + * server's conninfo. + * + * If the option is not found in connInfo, return NULL value. + */ +static char * +libpqrcv_get_option_from_conninfo(const char *connInfo, const char *keyword) { PQconninfoOption *opts; - char *dbname = NULL; + char *option = NULL; char *err = NULL; opts = PQconninfoParse(connInfo, &err); @@ -486,21 +505,21 @@ libpqrcv_get_dbname_from_conninfo(const char *connInfo) for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt) { /* - * If multiple dbnames are specified, then the last one will be - * returned + * If the same option appears multiple times, then the last one will + * be returned */ - if (strcmp(opt->keyword, "dbname") == 0 && opt->val && + if (strcmp(opt->keyword, keyword) == 0 && opt->val && *opt->val) { - if (dbname) - pfree(dbname); + if (option) + pfree(option); - dbname = pstrdup(opt->val); + option = pstrdup(opt->val); } } PQconninfoFree(opts); - return dbname; + return option; } /* @@ -534,7 +553,7 @@ libpqrcv_startstreaming(WalReceiverConn *conn, if (options->logical) appendStringInfoString(&cmd, " LOGICAL"); - appendStringInfo(&cmd, " %X/%X", LSN_FORMAT_ARGS(options->startpoint)); + appendStringInfo(&cmd, " %X/%08X", LSN_FORMAT_ARGS(options->startpoint)); /* * Additional options are different depending on if we are doing logical @@ -604,13 +623,10 @@ libpqrcv_startstreaming(WalReceiverConn *conn, return false; } else if (PQresultStatus(res) != PGRES_COPY_BOTH) - { - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("could not start WAL streaming: %s", pchomp(PQerrorMessage(conn->streamConn))))); - } PQclear(res); return true; } @@ -718,26 +734,17 @@ libpqrcv_readtimelinehistoryfile(WalReceiverConn *conn, cmd, WAIT_EVENT_LIBPQWALRECEIVER_RECEIVE); if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("could not receive timeline history file from " "the primary server: %s", pchomp(PQerrorMessage(conn->streamConn))))); - } if (PQnfields(res) != 2 || PQntuples(res) != 1) - { - int ntuples = PQntuples(res); - int nfields = PQnfields(res); - - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("invalid response from primary server"), errdetail("Expected 1 tuple with 2 fields, got %d tuples with %d fields.", - ntuples, nfields))); - } + PQntuples(res), PQnfields(res)))); *filename = pstrdup(PQgetvalue(res, 0, 0)); *len = PQgetlength(res, 0, 1); @@ -841,13 +848,10 @@ libpqrcv_receive(WalReceiverConn *conn, char **buffer, return -1; } else - { - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("could not receive data from WAL stream: %s", pchomp(PQerrorMessage(conn->streamConn))))); - } } if (rawlen < -1) ereport(ERROR, @@ -971,13 +975,10 @@ libpqrcv_create_slot(WalReceiverConn *conn, const char *slotname, pfree(cmd.data); if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - PQclear(res); ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("could not create replication slot \"%s\": %s", slotname, pchomp(PQerrorMessage(conn->streamConn))))); - } if (lsn) *lsn = DatumGetLSN(DirectFunctionCall1Coll(pg_lsn_in, InvalidOid, @@ -1072,6 +1073,7 @@ libpqrcv_processTuples(PGresult *pgres, WalRcvExecResult *walres, for (coln = 0; coln < nRetTypes; coln++) TupleDescInitEntry(walres->tupledesc, (AttrNumber) coln + 1, PQfname(pgres, coln), retTypes[coln], -1, 0); + TupleDescFinalize(walres->tupledesc); attinmeta = TupleDescGetAttInMetadata(walres->tupledesc); /* No point in doing more here if there were no tuples returned. */ @@ -1126,7 +1128,7 @@ libpqrcv_exec(WalReceiverConn *conn, const char *query, const int nRetTypes, const Oid *retTypes) { PGresult *pgres = NULL; - WalRcvExecResult *walres = palloc0(sizeof(WalRcvExecResult)); + WalRcvExecResult *walres = palloc0_object(WalRcvExecResult); char *diag_sqlstate; if (MyDatabaseId == InvalidOid) diff --git a/src/backend/replication/libpqwalreceiver/meson.build b/src/backend/replication/libpqwalreceiver/meson.build index 2150f31cfa3df..d2f2a4b791f9d 100644 --- a/src/backend/replication/libpqwalreceiver/meson.build +++ b/src/backend/replication/libpqwalreceiver/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group libpqwalreceiver_sources = files( 'libpqwalreceiver.c', diff --git a/src/backend/replication/logical/Makefile b/src/backend/replication/logical/Makefile index 1e08bbbd4eb15..455768a57f0f3 100644 --- a/src/backend/replication/logical/Makefile +++ b/src/backend/replication/logical/Makefile @@ -20,14 +20,17 @@ OBJS = \ decode.o \ launcher.o \ logical.o \ + logicalctl.o \ logicalfuncs.o \ message.o \ origin.o \ proto.o \ relation.o \ reorderbuffer.o \ + sequencesync.o \ slotsync.o \ snapbuild.o \ + syncutils.o \ tablesync.o \ worker.o diff --git a/src/backend/replication/logical/applyparallelworker.c b/src/backend/replication/logical/applyparallelworker.c index d25085d351535..e10f653fde798 100644 --- a/src/backend/replication/logical/applyparallelworker.c +++ b/src/backend/replication/logical/applyparallelworker.c @@ -2,7 +2,7 @@ * applyparallelworker.c * Support routines for applying xact by parallel apply worker * - * Copyright (c) 2023-2025, PostgreSQL Global Development Group + * Copyright (c) 2023-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/applyparallelworker.c @@ -166,11 +166,14 @@ #include "replication/origin.h" #include "replication/worker_internal.h" #include "storage/ipc.h" +#include "storage/latch.h" #include "storage/lmgr.h" +#include "storage/proc.h" #include "tcop/tcopprot.h" #include "utils/inval.h" #include "utils/memutils.h" #include "utils/syscache.h" +#include "utils/wait_event.h" #define PG_LOGICAL_APPLY_SHM_MAGIC 0x787ca067 @@ -299,7 +302,7 @@ pa_can_start(void) * STREAM START message, and it doesn't seem worth sending the extra eight * bytes with the STREAM START to enable parallelism for this case. */ - if (!XLogRecPtrIsInvalid(MySubscription->skiplsn)) + if (XLogRecPtrIsValid(MySubscription->skiplsn)) return false; /* @@ -425,7 +428,7 @@ pa_launch_parallel_worker(void) */ oldcontext = MemoryContextSwitchTo(ApplyContext); - winfo = (ParallelApplyWorkerInfo *) palloc0(sizeof(ParallelApplyWorkerInfo)); + winfo = palloc0_object(ParallelApplyWorkerInfo); /* Setup shared memory. */ if (!pa_setup_dsm(winfo)) @@ -441,7 +444,8 @@ pa_launch_parallel_worker(void) MySubscription->name, MyLogicalRepWorker->userid, InvalidOid, - dsm_segment_handle(winfo->dsm_seg)); + dsm_segment_handle(winfo->dsm_seg), + false); if (launched) { @@ -639,7 +643,7 @@ pa_detach_all_error_mq(void) * Check if there are any pending spooled messages. */ static bool -pa_has_spooled_message_pending() +pa_has_spooled_message_pending(void) { PartialFileSetState fileset_state; @@ -777,10 +781,10 @@ LogicalParallelApplyLoop(shm_mq_handle *mqh) /* * The first byte of messages sent from leader apply worker to - * parallel apply workers can only be 'w'. + * parallel apply workers can only be PqReplMsg_WALData. */ c = pq_getmsgbyte(&s); - if (c != 'w') + if (c != PqReplMsg_WALData) elog(ERROR, "unexpected message \"%c\"", c); /* @@ -811,6 +815,15 @@ LogicalParallelApplyLoop(shm_mq_handle *mqh) if (rc & WL_LATCH_SET) ResetLatch(MyLatch); + + /* + * Force stats reporting to avoid long delays. There can be + * long idle gaps before the leader assigns the next + * transaction, and the only opportunity to report stats + * during such gaps is here. + */ + if ((rc & WL_TIMEOUT) && !IsTransactionState()) + pgstat_report_stat(true); } } else @@ -863,16 +876,22 @@ ParallelApplyWorkerMain(Datum main_arg) shm_mq *mq; shm_mq_handle *mqh; shm_mq_handle *error_mqh; - RepOriginId originid; + ReplOriginId originid; int worker_slot = DatumGetInt32(main_arg); char originname[NAMEDATALEN]; InitializingApplyWorker = true; - /* Setup signal handling. */ + /* + * Setup signal handling. + * + * Note: We intentionally used SIGUSR2 to trigger a graceful shutdown + * initiated by the leader apply worker. This helps to differentiate it + * from the case where we abort the current transaction and exit on + * receiving SIGTERM. + */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGINT, SignalHandlerForShutdownRequest); - pqsignal(SIGTERM, die); + pqsignal(SIGUSR2, SignalHandlerForShutdownRequest); BackgroundWorkerUnblockSignals(); /* @@ -954,7 +973,7 @@ ParallelApplyWorkerMain(Datum main_arg) * origin which was already acquired by its leader process. */ replorigin_session_setup(originid, MyLogicalRepWorker->leader_pid); - replorigin_session_origin = originid; + replorigin_xact_state.origin = originid; CommitTransactionCommand(); /* @@ -962,7 +981,7 @@ ParallelApplyWorkerMain(Datum main_arg) * the subscription relation state. */ CacheRegisterSyscacheCallback(SUBSCRIPTIONRELMAP, - invalidate_syncing_table_states, + InvalidateSyncingRelStates, (Datum) 0); set_apply_error_context_origin(originname); @@ -971,9 +990,9 @@ ParallelApplyWorkerMain(Datum main_arg) /* * The parallel apply worker must not get here because the parallel apply - * worker will only stop when it receives a SIGTERM or SIGINT from the - * leader, or when there is an error. None of these cases will allow the - * code to reach here. + * worker will only stop when it receives a SIGTERM or SIGUSR2 from the + * leader, or SIGINT from itself, or when there is an error. None of these + * cases will allow the code to reach here. */ Assert(false); } @@ -990,7 +1009,7 @@ HandleParallelApplyMessageInterrupt(void) { InterruptPending = true; ParallelApplyMessagePending = true; - SetLatch(MyLatch); + /* latch will be set by procsignal_sigusr1_handler */ } /* @@ -1006,7 +1025,7 @@ ProcessParallelApplyMessage(StringInfo msg) switch (msgtype) { - case 'E': /* ErrorResponse */ + case PqMsg_ErrorResponse: { ErrorData edata; @@ -1043,11 +1062,11 @@ ProcessParallelApplyMessage(StringInfo msg) /* * Don't need to do anything about NoticeResponse and - * NotifyResponse as the logical replication worker doesn't need - * to send messages to the client. + * NotificationResponse as the logical replication worker doesn't + * need to send messages to the client. */ - case 'N': - case 'A': + case PqMsg_NoticeResponse: + case PqMsg_NotificationResponse: break; default: @@ -1422,8 +1441,8 @@ pa_stream_abort(LogicalRepStreamAbortData *abort_data) * Update origin state so we can restart streaming from correct position * in case of crash. */ - replorigin_session_origin_lsn = abort_data->abort_lsn; - replorigin_session_origin_timestamp = abort_data->abort_time; + replorigin_xact_state.origin_lsn = abort_data->abort_lsn; + replorigin_xact_state.origin_timestamp = abort_data->abort_time; /* * If the two XIDs are the same, it's in fact abort of toplevel xact, so @@ -1632,7 +1651,7 @@ pa_xact_finish(ParallelApplyWorkerInfo *winfo, XLogRecPtr remote_lsn) */ pa_wait_for_xact_finish(winfo); - if (!XLogRecPtrIsInvalid(remote_lsn)) + if (XLogRecPtrIsValid(remote_lsn)) store_flush_position(remote_lsn, winfo->shared->last_commit_end); pa_free_worker(winfo); diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index 97c4e26b58654..48ef84ee924ed 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -2,7 +2,7 @@ * conflict.c * Support routines for logging conflicts. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/conflict.c @@ -15,6 +15,7 @@ #include "postgres.h" #include "access/commit_ts.h" +#include "access/genam.h" #include "access/tableam.h" #include "executor/executor.h" #include "pgstat.h" @@ -29,6 +30,7 @@ static const char *const ConflictTypeNames[] = { [CT_UPDATE_EXISTS] = "update_exists", [CT_UPDATE_MISSING] = "update_missing", [CT_DELETE_ORIGIN_DIFFERS] = "delete_origin_differs", + [CT_UPDATE_DELETED] = "update_deleted", [CT_DELETE_MISSING] = "delete_missing", [CT_MULTIPLE_UNIQUE_CONFLICTS] = "multiple_unique_conflicts" }; @@ -41,26 +43,26 @@ static void errdetail_apply_conflict(EState *estate, TupleTableSlot *localslot, TupleTableSlot *remoteslot, Oid indexoid, TransactionId localxmin, - RepOriginId localorigin, + ReplOriginId localorigin, TimestampTz localts, StringInfo err_msg); -static char *build_tuple_value_details(EState *estate, ResultRelInfo *relinfo, - ConflictType type, - TupleTableSlot *searchslot, - TupleTableSlot *localslot, - TupleTableSlot *remoteslot, - Oid indexoid); +static void get_tuple_desc(EState *estate, ResultRelInfo *relinfo, + ConflictType type, char **key_desc, + TupleTableSlot *localslot, char **local_desc, + TupleTableSlot *remoteslot, char **remote_desc, + TupleTableSlot *searchslot, char **search_desc, + Oid indexoid); static char *build_index_value_desc(EState *estate, Relation localrel, TupleTableSlot *slot, Oid indexoid); /* * Get the xmin and commit timestamp data (origin and timestamp) associated - * with the provided local tuple. + * with the provided local row. * * Return true if the commit timestamp data was found, false otherwise. */ bool GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, - RepOriginId *localorigin, TimestampTz *localts) + ReplOriginId *localorigin, TimestampTz *localts) { Datum xminDatum; bool isnull; @@ -76,7 +78,7 @@ GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, */ if (!track_commit_timestamp) { - *localorigin = InvalidRepOriginId; + *localorigin = InvalidReplOriginId; *localts = 0; return false; } @@ -88,12 +90,12 @@ GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, * This function is used to report a conflict while applying replication * changes. * - * 'searchslot' should contain the tuple used to search the local tuple to be + * 'searchslot' should contain the tuple used to search the local row to be * updated or deleted. * * 'remoteslot' should contain the remote new tuple, if any. * - * conflicttuples is a list of local tuples that caused the conflict and the + * conflicttuples is a list of local rows that caused the conflict and the * conflict related information. See ConflictTupleInfo. * * The caller must ensure that all the indexes passed in ConflictTupleInfo are @@ -176,6 +178,7 @@ errcode_apply_conflict(ConflictType type) case CT_UPDATE_ORIGIN_DIFFERS: case CT_UPDATE_MISSING: case CT_DELETE_ORIGIN_DIFFERS: + case CT_UPDATE_DELETED: case CT_DELETE_MISSING: return errcode(ERRCODE_T_R_SERIALIZATION_FAILURE); } @@ -184,14 +187,66 @@ errcode_apply_conflict(ConflictType type) return 0; /* silence compiler warning */ } +/* + * Helper function to build the additional details for conflicting key, + * local row, remote row, and replica identity columns. + */ +static void +append_tuple_value_detail(StringInfo buf, List *tuple_values, + bool need_newline) +{ + bool first = true; + + Assert(buf != NULL && tuple_values != NIL); + + foreach_ptr(char, tuple_value, tuple_values) + { + /* + * Skip if the value is NULL. This means the current user does not + * have enough permissions to see all columns in the table. See + * get_tuple_desc(). + */ + if (!tuple_value) + continue; + + if (first) + { + /* + * translator: The colon is used as a separator in conflict + * messages. The first part, built in the caller, describes what + * happened locally; the second part lists the conflicting keys + * and tuple data. + */ + appendStringInfoString(buf, _(": ")); + } + else + { + /* + * translator: This is a separator in a list of conflicting keys + * and tuple data. + */ + appendStringInfoString(buf, _(", ")); + } + + appendStringInfoString(buf, tuple_value); + first = false; + } + + /* translator: This is the terminator of a conflict message */ + appendStringInfoString(buf, _(".")); + + if (need_newline) + appendStringInfoChar(buf, '\n'); +} + /* * Add an errdetail() line showing conflict detail. * * The DETAIL line comprises of two parts: * 1. Explanation of the conflict type, including the origin and commit - * timestamp of the existing local tuple. - * 2. Display of conflicting key, existing local tuple, remote new tuple, and - * replica identity columns, if any. The remote old tuple is excluded as its + * timestamp of the local row. + * 2. Display of conflicting key, local row, remote new row, and replica + * identity columns, if any. The remote old row is excluded as its * information is covered in the replica identity columns. */ static void @@ -199,16 +254,26 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, ConflictType type, TupleTableSlot *searchslot, TupleTableSlot *localslot, TupleTableSlot *remoteslot, Oid indexoid, TransactionId localxmin, - RepOriginId localorigin, TimestampTz localts, + ReplOriginId localorigin, TimestampTz localts, StringInfo err_msg) { StringInfoData err_detail; - char *val_desc; char *origin_name; + char *key_desc = NULL; + char *local_desc = NULL; + char *remote_desc = NULL; + char *search_desc = NULL; + + /* Get key, replica identity, remote, and local value data */ + get_tuple_desc(estate, relinfo, type, &key_desc, + localslot, &local_desc, + remoteslot, &remote_desc, + searchslot, &search_desc, + indexoid); initStringInfo(&err_detail); - /* First, construct a detailed message describing the type of conflict */ + /* Construct a detailed message describing the type of conflict */ switch (type) { case CT_INSERT_EXISTS: @@ -217,14 +282,23 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, Assert(OidIsValid(indexoid) && CheckRelationOidLockedByMe(indexoid, RowExclusiveLock, true)); + if (err_msg->len == 0) + { + appendStringInfoString(&err_detail, _("Could not apply remote change")); + + append_tuple_value_detail(&err_detail, + list_make2(remote_desc, search_desc), + true); + } + if (localts) { - if (localorigin == InvalidRepOriginId) - appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s."), + if (localorigin == InvalidReplOriginId) + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s"), get_rel_name(indexoid), localxmin, timestamptz_to_str(localts)); else if (replorigin_by_oid(localorigin, true, &origin_name)) - appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s."), + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s"), get_rel_name(indexoid), origin_name, localxmin, timestamptz_to_str(localts)); @@ -236,67 +310,103 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, * manually dropped by the user. */ else - appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s."), + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s"), get_rel_name(indexoid), localxmin, timestamptz_to_str(localts)); } else - appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u."), + appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u"), get_rel_name(indexoid), localxmin); + append_tuple_value_detail(&err_detail, + list_make2(key_desc, local_desc), false); + break; case CT_UPDATE_ORIGIN_DIFFERS: - if (localorigin == InvalidRepOriginId) - appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s."), + if (localorigin == InvalidReplOriginId) + appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s"), localxmin, timestamptz_to_str(localts)); else if (replorigin_by_oid(localorigin, true, &origin_name)) - appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s."), + appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s"), origin_name, localxmin, timestamptz_to_str(localts)); /* The origin that modified this row has been removed. */ else - appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s."), + appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s"), localxmin, timestamptz_to_str(localts)); + append_tuple_value_detail(&err_detail, + list_make3(local_desc, remote_desc, + search_desc), false); + + break; + + case CT_UPDATE_DELETED: + appendStringInfoString(&err_detail, _("Could not find the row to be updated")); + + append_tuple_value_detail(&err_detail, + list_make2(remote_desc, search_desc), + true); + + if (localts) + { + if (localorigin == InvalidReplOriginId) + appendStringInfo(&err_detail, _("The row to be updated was deleted locally in transaction %u at %s"), + localxmin, timestamptz_to_str(localts)); + else if (replorigin_by_oid(localorigin, true, &origin_name)) + appendStringInfo(&err_detail, _("The row to be updated was deleted by a different origin \"%s\" in transaction %u at %s"), + origin_name, localxmin, timestamptz_to_str(localts)); + + /* The origin that modified this row has been removed. */ + else + appendStringInfo(&err_detail, _("The row to be updated was deleted by a non-existent origin in transaction %u at %s"), + localxmin, timestamptz_to_str(localts)); + } + else + appendStringInfoString(&err_detail, _("The row to be updated was deleted")); + break; case CT_UPDATE_MISSING: - appendStringInfoString(&err_detail, _("Could not find the row to be updated.")); + appendStringInfoString(&err_detail, _("Could not find the row to be updated")); + + append_tuple_value_detail(&err_detail, + list_make2(remote_desc, search_desc), + false); + break; case CT_DELETE_ORIGIN_DIFFERS: - if (localorigin == InvalidRepOriginId) - appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s."), + if (localorigin == InvalidReplOriginId) + appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s"), localxmin, timestamptz_to_str(localts)); else if (replorigin_by_oid(localorigin, true, &origin_name)) - appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s."), + appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s"), origin_name, localxmin, timestamptz_to_str(localts)); /* The origin that modified this row has been removed. */ else - appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s."), + appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s"), localxmin, timestamptz_to_str(localts)); + append_tuple_value_detail(&err_detail, + list_make3(local_desc, remote_desc, + search_desc), false); + break; case CT_DELETE_MISSING: - appendStringInfoString(&err_detail, _("Could not find the row to be deleted.")); + appendStringInfoString(&err_detail, _("Could not find the row to be deleted")); + + append_tuple_value_detail(&err_detail, + list_make1(search_desc), false); + break; } Assert(err_detail.len > 0); - val_desc = build_tuple_value_details(estate, relinfo, type, searchslot, - localslot, remoteslot, indexoid); - - /* - * Next, append the key values, existing local tuple, remote tuple and - * replica identity columns after the message. - */ - if (val_desc) - appendStringInfo(&err_detail, "\n%s", val_desc); - /* * Insert a blank line to visually separate the new detail line from the * existing ones. @@ -308,29 +418,27 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, } /* - * Helper function to build the additional details for conflicting key, - * existing local tuple, remote tuple, and replica identity columns. + * Extract conflicting key, local row, remote row, and replica identity + * columns. Results are set at xxx_desc. * - * If the return value is NULL, it indicates that the current user lacks - * permissions to view the columns involved. + * If the output is NULL, it indicates that the current user lacks permissions + * to view the columns involved. */ -static char * -build_tuple_value_details(EState *estate, ResultRelInfo *relinfo, - ConflictType type, - TupleTableSlot *searchslot, - TupleTableSlot *localslot, - TupleTableSlot *remoteslot, - Oid indexoid) +static void +get_tuple_desc(EState *estate, ResultRelInfo *relinfo, ConflictType type, + char **key_desc, + TupleTableSlot *localslot, char **local_desc, + TupleTableSlot *remoteslot, char **remote_desc, + TupleTableSlot *searchslot, char **search_desc, + Oid indexoid) { Relation localrel = relinfo->ri_RelationDesc; Oid relid = RelationGetRelid(localrel); TupleDesc tupdesc = RelationGetDescr(localrel); - StringInfoData tuple_value; char *desc = NULL; - Assert(searchslot || localslot || remoteslot); - - initStringInfo(&tuple_value); + Assert((localslot && local_desc) || (remoteslot && remote_desc) || + (searchslot && search_desc)); /* * Report the conflicting key values in the case of a unique constraint @@ -341,35 +449,24 @@ build_tuple_value_details(EState *estate, ResultRelInfo *relinfo, { Assert(OidIsValid(indexoid) && localslot); - desc = build_index_value_desc(estate, localrel, localslot, indexoid); + desc = build_index_value_desc(estate, localrel, localslot, + indexoid); if (desc) - appendStringInfo(&tuple_value, _("Key %s"), desc); + *key_desc = psprintf(_("key %s"), desc); } if (localslot) { /* * The 'modifiedCols' only applies to the new tuple, hence we pass - * NULL for the existing local tuple. + * NULL for the local row. */ desc = ExecBuildSlotValueDescription(relid, localslot, tupdesc, NULL, 64); if (desc) - { - if (tuple_value.len > 0) - { - appendStringInfoString(&tuple_value, "; "); - appendStringInfo(&tuple_value, _("existing local tuple %s"), - desc); - } - else - { - appendStringInfo(&tuple_value, _("Existing local tuple %s"), - desc); - } - } + *local_desc = psprintf(_("local row %s"), desc); } if (remoteslot) @@ -385,21 +482,12 @@ build_tuple_value_details(EState *estate, ResultRelInfo *relinfo, */ modifiedCols = bms_union(ExecGetInsertedCols(relinfo, estate), ExecGetUpdatedCols(relinfo, estate)); - desc = ExecBuildSlotValueDescription(relid, remoteslot, tupdesc, - modifiedCols, 64); + desc = ExecBuildSlotValueDescription(relid, remoteslot, + tupdesc, modifiedCols, + 64); if (desc) - { - if (tuple_value.len > 0) - { - appendStringInfoString(&tuple_value, "; "); - appendStringInfo(&tuple_value, _("remote tuple %s"), desc); - } - else - { - appendStringInfo(&tuple_value, _("Remote tuple %s"), desc); - } - } + *remote_desc = psprintf(_("remote row %s"), desc); } if (searchslot) @@ -427,27 +515,12 @@ build_tuple_value_details(EState *estate, ResultRelInfo *relinfo, if (desc) { - if (tuple_value.len > 0) - { - appendStringInfoString(&tuple_value, "; "); - appendStringInfo(&tuple_value, OidIsValid(replica_index) - ? _("replica identity %s") - : _("replica identity full %s"), desc); - } + if (OidIsValid(replica_index)) + *search_desc = psprintf(_("replica identity %s"), desc); else - { - appendStringInfo(&tuple_value, OidIsValid(replica_index) - ? _("Replica identity %s") - : _("Replica identity full %s"), desc); - } + *search_desc = psprintf(_("replica identity full %s"), desc); } } - - if (tuple_value.len == 0) - return NULL; - - appendStringInfoChar(&tuple_value, '.'); - return tuple_value.data; } /* diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c index cc03f0706e9c8..38c5a4f554070 100644 --- a/src/backend/replication/logical/decode.c +++ b/src/backend/replication/logical/decode.c @@ -16,7 +16,7 @@ * contents of records in here except turning them into a more usable * format. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -33,6 +33,7 @@ #include "access/xlogreader.h" #include "access/xlogrecord.h" #include "catalog/pg_control.h" +#include "commands/repack.h" #include "replication/decode.h" #include "replication/logical.h" #include "replication/message.h" @@ -66,7 +67,7 @@ static inline bool FilterPrepare(LogicalDecodingContext *ctx, TransactionId xid, const char *gid); static bool DecodeTXNNeedSkip(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, Oid txn_dbid, - RepOriginId origin_id); + ReplOriginId origin_id); /* * Take every XLogReadRecord()ed record and perform the actions required to @@ -149,39 +150,34 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) * can restart from there. */ break; - case XLOG_PARAMETER_CHANGE: + case XLOG_LOGICAL_DECODING_STATUS_CHANGE: { - xl_parameter_change *xlrec = - (xl_parameter_change *) XLogRecGetData(buf->record); + bool logical_decoding; + + memcpy(&logical_decoding, XLogRecGetData(buf->record), sizeof(bool)); /* - * If wal_level on the primary is reduced to less than - * logical, we want to prevent existing logical slots from - * being used. Existing logical slots on the standby get - * invalidated when this WAL record is replayed; and further, - * slot creation fails when wal_level is not sufficient; but - * all these operations are not synchronized, so a logical - * slot may creep in while the wal_level is being reduced. - * Hence this extra check. + * Error out as we should not decode this WAL record. + * + * Logical decoding is disabled, and existing logical slots on + * the standby are invalidated when this WAL record is + * replayed. No logical decoder can process this WAL record + * until replay completes, and by then the slots are already + * invalidated. Furthermore, no new logical slots can be + * created while logical decoding is disabled. This cannot + * occur even on primary either, since it will not restart + * with wal_level < replica if any logical slots exist. */ - if (xlrec->wal_level < WAL_LEVEL_LOGICAL) - { - /* - * This can occur only on a standby, as a primary would - * not allow to restart after changing wal_level < logical - * if there is pre-existing logical slot. - */ - Assert(RecoveryInProgress()); - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("logical decoding on standby requires \"wal_level\" >= \"logical\" on the primary"))); - } + elog(ERROR, "unexpected logical decoding status change %d", + logical_decoding); + break; } case XLOG_NOOP: case XLOG_NEXTOID: case XLOG_SWITCH: case XLOG_BACKUP_END: + case XLOG_PARAMETER_CHANGE: case XLOG_RESTORE_POINT: case XLOG_FPW_CHANGE: case XLOG_FPI_FOR_HINT: @@ -194,6 +190,22 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) } } +void +xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) +{ + uint8 info = XLogRecGetInfo(buf->record) & ~XLR_INFO_MASK; + + ReorderBufferProcessXid(ctx->reorder, XLogRecGetXid(buf->record), buf->origptr); + + switch (info) + { + case XLOG2_CHECKSUMS: + break; + default: + elog(ERROR, "unexpected RM_XLOG2_ID record type: %u", info); + } +} + /* * Handle rmgr XACT_ID records for LogicalDecodingProcessRecord(). */ @@ -370,7 +382,16 @@ standby_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { xl_running_xacts *running = (xl_running_xacts *) XLogRecGetData(r); - SnapBuildProcessRunningXacts(builder, buf->origptr, running); + /* + * Update this decoder's idea of transactions currently + * running. In doing so we will determine whether we have + * reached consistent status. + * + * If the output plugin doesn't need access to shared + * catalogs, we can ignore transactions in other databases. + */ + SnapBuildProcessRunningXacts(builder, buf->origptr, running, + !ctx->options.need_shared_catalogs); /* * Abort all transactions that we keep track of, that are @@ -380,8 +401,12 @@ standby_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) * all running transactions which includes prepared ones, * while shutdown checkpoints just know that no non-prepared * transactions are in progress. + * + * The database-specific records might work here too, but it's + * not their purpose. */ - ReorderBufferAbortOld(ctx->reorder, running->oldestRunningXid); + if (!OidIsValid(running->dbid)) + ReorderBufferAbortOld(ctx->reorder, running->oldestRunningXid); } break; case XLOG_STANDBY_LOCK: @@ -425,7 +450,8 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { case XLOG_HEAP2_MULTI_INSERT: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeMultiInsert(ctx, buf); break; case XLOG_HEAP2_NEW_CID: @@ -435,9 +461,8 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xlrec = (xl_heap_new_cid *) XLogRecGetData(buf->record); SnapBuildProcessNewCid(builder, xid, buf->origptr, xlrec); - - break; } + break; case XLOG_HEAP2_REWRITE: /* @@ -454,7 +479,6 @@ heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) case XLOG_HEAP2_PRUNE_ON_ACCESS: case XLOG_HEAP2_PRUNE_VACUUM_SCAN: case XLOG_HEAP2_PRUNE_VACUUM_CLEANUP: - case XLOG_HEAP2_VISIBLE: case XLOG_HEAP2_LOCK_UPDATED: break; default: @@ -489,7 +513,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) { case XLOG_HEAP_INSERT: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeInsert(ctx, buf); break; @@ -501,19 +526,22 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) case XLOG_HEAP_HOT_UPDATE: case XLOG_HEAP_UPDATE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeUpdate(ctx, buf); break; case XLOG_HEAP_DELETE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeDelete(ctx, buf); break; case XLOG_HEAP_TRUNCATE: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeTruncate(ctx, buf); break; @@ -521,24 +549,16 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) /* * Inplace updates are only ever performed on catalog tuples and - * can, per definition, not change tuple visibility. Inplace - * updates don't affect storage or interpretation of table rows, - * so they don't affect logicalrep_write_tuple() outcomes. Hence, - * we don't process invalidations from the original operation. If - * inplace updates did affect those things, invalidations wouldn't - * make it work, since there are no snapshot-specific versions of - * inplace-updated values. Since we also don't decode catalog - * tuples, we're not interested in the record's contents. - * - * WAL contains likely-unnecessary commit-time invals from the - * CacheInvalidateHeapTuple() call in - * heap_inplace_update_and_unlock(). Excess invalidation is safe. + * can, per definition, not change tuple visibility. Since we + * also don't decode catalog tuples, we're not interested in the + * record's contents. */ break; case XLOG_HEAP_CONFIRM: if (SnapBuildProcessChange(builder, xid, buf->origptr) && - !ctx->fast_forward) + !ctx->fast_forward && + !change_useless_for_repack(buf)) DecodeSpecConfirm(ctx, buf); break; @@ -580,7 +600,7 @@ FilterPrepare(LogicalDecodingContext *ctx, TransactionId xid, } static inline bool -FilterByOrigin(LogicalDecodingContext *ctx, RepOriginId origin_id) +FilterByOrigin(LogicalDecodingContext *ctx, ReplOriginId origin_id) { if (ctx->callbacks.filter_by_origin_cb == NULL) return false; @@ -598,7 +618,7 @@ logicalmsg_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) XLogReaderState *r = buf->record; TransactionId xid = XLogRecGetXid(r); uint8 info = XLogRecGetInfo(r) & ~XLR_INFO_MASK; - RepOriginId origin_id = XLogRecGetOrigin(r); + ReplOriginId origin_id = XLogRecGetOrigin(r); Snapshot snapshot = NULL; xl_logical_message *message; @@ -679,7 +699,7 @@ DecodeCommit(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, { XLogRecPtr origin_lsn = InvalidXLogRecPtr; TimestampTz commit_time = parsed->xact_time; - RepOriginId origin_id = XLogRecGetOrigin(buf->record); + ReplOriginId origin_id = XLogRecGetOrigin(buf->record); int i; if (parsed->xinfo & XACT_XINFO_HAS_ORIGIN) @@ -775,7 +795,7 @@ DecodePrepare(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, SnapBuild *builder = ctx->snapshot_builder; XLogRecPtr origin_lsn = parsed->origin_lsn; TimestampTz prepare_time = parsed->xact_time; - RepOriginId origin_id = XLogRecGetOrigin(buf->record); + ReplOriginId origin_id = XLogRecGetOrigin(buf->record); int i; TransactionId xid = parsed->twophase_xid; @@ -851,7 +871,7 @@ DecodeAbort(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, int i; XLogRecPtr origin_lsn = InvalidXLogRecPtr; TimestampTz abort_time = parsed->xact_time; - RepOriginId origin_id = XLogRecGetOrigin(buf->record); + ReplOriginId origin_id = XLogRecGetOrigin(buf->record); bool skip_xact; if (parsed->xinfo & XACT_XINFO_HAS_ORIGIN) @@ -1035,6 +1055,15 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf) xlrec = (xl_heap_delete *) XLogRecGetData(r); + /* + * Skip changes that were marked as ignorable at origin. + * + * (This is used for changes that affect relations not visible to other + * transactions, such as the transient table during concurrent repack.) + */ + if (xlrec->flags & XLH_DELETE_NO_LOGICAL) + return; + /* only interested in our database */ XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL); if (target_locator.dbOid != ctx->slot->data.database) @@ -1303,7 +1332,7 @@ DecodeXLogTuple(char *data, Size len, HeapTuple tuple) */ static bool DecodeTXNNeedSkip(LogicalDecodingContext *ctx, XLogRecordBuffer *buf, - Oid txn_dbid, RepOriginId origin_id) + Oid txn_dbid, ReplOriginId origin_id) { if (SnapBuildXactNeedsSkip(ctx->snapshot_builder, buf->origptr) || (txn_dbid != InvalidOid && txn_dbid != ctx->slot->data.database) || diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index 10677da56b2b6..7adf4dbe0d143 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -2,7 +2,7 @@ * launcher.c * PostgreSQL logical replication worker launcher process * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/launcher.c @@ -32,16 +32,20 @@ #include "postmaster/interrupt.h" #include "replication/logicallauncher.h" #include "replication/origin.h" +#include "replication/slot.h" #include "replication/walreceiver.h" #include "replication/worker_internal.h" #include "storage/ipc.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/subsystems.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" #include "utils/memutils.h" #include "utils/pg_lsn.h" #include "utils/snapmgr.h" +#include "utils/syscache.h" +#include "utils/wait_event.h" /* max sleep time between cycles (3min) */ #define DEFAULT_NAPTIME_PER_CYCLE 180000L @@ -68,6 +72,14 @@ typedef struct LogicalRepCtxStruct static LogicalRepCtxStruct *LogicalRepCtx; +static void ApplyLauncherShmemRequest(void *arg); +static void ApplyLauncherShmemInit(void *arg); + +const ShmemCallbacks ApplyLauncherShmemCallbacks = { + .request_fn = ApplyLauncherShmemRequest, + .init_fn = ApplyLauncherShmemInit, +}; + /* an entry in the last-start-times shared hash table */ typedef struct LauncherLastStartTimesEntry { @@ -91,7 +103,6 @@ static dshash_table *last_start_times = NULL; static bool on_commit_launcher_wakeup = false; -static void ApplyLauncherWakeup(void); static void logicalrep_launcher_onexit(int code, Datum arg); static void logicalrep_worker_onexit(int code, Datum arg); static void logicalrep_worker_detach(void); @@ -100,6 +111,10 @@ static int logicalrep_pa_worker_count(Oid subid); static void logicalrep_launcher_attach_dshmem(void); static void ApplyLauncherSetWorkerStartTime(Oid subid, TimestampTz start_time); static TimestampTz ApplyLauncherGetWorkerStartTime(Oid subid); +static void compute_min_nonremovable_xid(LogicalRepWorker *worker, TransactionId *xmin); +static bool acquire_conflict_slot_if_exists(void); +static void update_conflict_slot_xmin(TransactionId new_xmin); +static void init_conflict_slot_xmin(void); /* @@ -142,12 +157,14 @@ get_subscription_list(void) */ oldcxt = MemoryContextSwitchTo(resultcxt); - sub = (Subscription *) palloc0(sizeof(Subscription)); + sub = palloc0_object(Subscription); sub->oid = subform->oid; sub->dbid = subform->subdbid; sub->owner = subform->subowner; sub->enabled = subform->subenabled; sub->name = pstrdup(NameStr(subform->subname)); + sub->retaindeadtuples = subform->subretaindeadtuples; + sub->retentionactive = subform->subretentionactive; /* We don't fill fields we are not interested in. */ res = lappend(res, sub); @@ -175,12 +192,14 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker, uint16 generation, BackgroundWorkerHandle *handle) { - BgwHandleStatus status; - int rc; + bool result = false; + bool dropped_latch = false; for (;;) { + BgwHandleStatus status; pid_t pid; + int rc; CHECK_FOR_INTERRUPTS(); @@ -189,8 +208,9 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker, /* Worker either died or has started. Return false if died. */ if (!worker->in_use || worker->proc) { + result = worker->in_use; LWLockRelease(LogicalRepWorkerLock); - return worker->in_use; + break; } LWLockRelease(LogicalRepWorkerLock); @@ -205,7 +225,7 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker, if (generation == worker->generation) logicalrep_worker_cleanup(worker); LWLockRelease(LogicalRepWorkerLock); - return false; + break; /* result is already false */ } /* @@ -220,25 +240,42 @@ WaitForReplicationWorkerAttach(LogicalRepWorker *worker, { ResetLatch(MyLatch); CHECK_FOR_INTERRUPTS(); + dropped_latch = true; } } + + /* + * If we had to clear a latch event in order to wait, be sure to restore + * it before exiting. Otherwise caller may miss events. + */ + if (dropped_latch) + SetLatch(MyLatch); + + return result; } /* - * Walks the workers array and searches for one that matches given - * subscription id and relid. + * Walks the workers array and searches for one that matches given worker type, + * subscription id, and relation id. * - * We are only interested in the leader apply worker or table sync worker. + * For both apply workers and sequencesync workers, the relid should be set to + * InvalidOid, as these workers handle changes across all tables and sequences + * respectively, rather than targeting a specific relation. For tablesync + * workers, the relid should be set to the OID of the relation being + * synchronized. */ LogicalRepWorker * -logicalrep_worker_find(Oid subid, Oid relid, bool only_running) +logicalrep_worker_find(LogicalRepWorkerType wtype, Oid subid, Oid relid, + bool only_running) { int i; LogicalRepWorker *res = NULL; + /* relid must be valid only for table sync workers */ + Assert((wtype == WORKERTYPE_TABLESYNC) == OidIsValid(relid)); Assert(LWLockHeldByMe(LogicalRepWorkerLock)); - /* Search for attached worker for a given subscription id. */ + /* Search for an attached worker that matches the specified criteria. */ for (i = 0; i < max_logical_replication_workers; i++) { LogicalRepWorker *w = &LogicalRepCtx->workers[i]; @@ -248,7 +285,7 @@ logicalrep_worker_find(Oid subid, Oid relid, bool only_running) continue; if (w->in_use && w->subid == subid && w->relid == relid && - (!only_running || w->proc)) + w->type == wtype && (!only_running || w->proc)) { res = w; break; @@ -296,7 +333,8 @@ logicalrep_workers_find(Oid subid, bool only_running, bool acquire_lock) bool logicalrep_worker_launch(LogicalRepWorkerType wtype, Oid dbid, Oid subid, const char *subname, Oid userid, - Oid relid, dsm_handle subworker_dsm) + Oid relid, dsm_handle subworker_dsm, + bool retain_dead_tuples) { BackgroundWorker bgw; BackgroundWorkerHandle *bgw_handle; @@ -308,6 +346,7 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype, int nparallelapplyworkers; TimestampTz now; bool is_tablesync_worker = (wtype == WORKERTYPE_TABLESYNC); + bool is_sequencesync_worker = (wtype == WORKERTYPE_SEQUENCESYNC); bool is_parallel_apply_worker = (wtype == WORKERTYPE_PARALLEL_APPLY); /*---------- @@ -315,10 +354,13 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype, * - must be valid worker type * - tablesync workers are only ones to have relid * - parallel apply worker is the only kind of subworker + * - The replication slot used in conflict detection is created when + * retain_dead_tuples is enabled */ Assert(wtype != WORKERTYPE_UNKNOWN); Assert(is_tablesync_worker == OidIsValid(relid)); Assert(is_parallel_apply_worker == (subworker_dsm != DSM_HANDLE_INVALID)); + Assert(!retain_dead_tuples || MyReplicationSlot); ereport(DEBUG1, (errmsg_internal("starting logical replication worker for subscription \"%s\"", @@ -328,7 +370,7 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype, if (max_active_replication_origins == 0) ereport(ERROR, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), - errmsg("cannot start logical replication workers when \"max_active_replication_origins\"=0"))); + errmsg("cannot start logical replication workers when \"max_active_replication_origins\" is 0"))); /* * We need to do the modification of the shared memory under lock so that @@ -393,7 +435,8 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype, * sync worker limit per subscription. So, just return silently as we * might get here because of an otherwise harmless race condition. */ - if (is_tablesync_worker && nsyncworkers >= max_sync_workers_per_subscription) + if ((is_tablesync_worker || is_sequencesync_worker) && + nsyncworkers >= max_sync_workers_per_subscription) { LWLockRelease(LogicalRepWorkerLock); return false; @@ -441,11 +484,15 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype, worker->stream_fileset = NULL; worker->leader_pid = is_parallel_apply_worker ? MyProcPid : InvalidPid; worker->parallel_apply = is_parallel_apply_worker; + worker->oldest_nonremovable_xid = retain_dead_tuples + ? MyReplicationSlot->data.xmin + : InvalidTransactionId; worker->last_lsn = InvalidXLogRecPtr; TIMESTAMP_NOBEGIN(worker->last_send_time); TIMESTAMP_NOBEGIN(worker->last_recv_time); worker->reply_lsn = InvalidXLogRecPtr; TIMESTAMP_NOBEGIN(worker->reply_time); + worker->last_seqsync_start_time = 0; /* Before releasing lock, remember generation for future identification. */ generation = worker->generation; @@ -479,8 +526,16 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype, memcpy(bgw.bgw_extra, &subworker_dsm, sizeof(dsm_handle)); break; + case WORKERTYPE_SEQUENCESYNC: + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "SequenceSyncWorkerMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, + "logical replication sequencesync worker for subscription %u", + subid); + snprintf(bgw.bgw_type, BGW_MAXLEN, "logical replication sequencesync worker"); + break; + case WORKERTYPE_TABLESYNC: - snprintf(bgw.bgw_function_name, BGW_MAXLEN, "TablesyncWorkerMain"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "TableSyncWorkerMain"); snprintf(bgw.bgw_name, BGW_MAXLEN, "logical replication tablesync worker for subscription %u sync %u", subid, @@ -600,16 +655,20 @@ logicalrep_worker_stop_internal(LogicalRepWorker *worker, int signo) } /* - * Stop the logical replication worker for subid/relid, if any. + * Stop the logical replication worker that matches the specified worker type, + * subscription id, and relation id. */ void -logicalrep_worker_stop(Oid subid, Oid relid) +logicalrep_worker_stop(LogicalRepWorkerType wtype, Oid subid, Oid relid) { LogicalRepWorker *worker; + /* relid must be valid only for table sync workers */ + Assert((wtype == WORKERTYPE_TABLESYNC) == OidIsValid(relid)); + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); - worker = logicalrep_worker_find(subid, relid, false); + worker = logicalrep_worker_find(wtype, subid, relid, false); if (worker) { @@ -623,7 +682,7 @@ logicalrep_worker_stop(Oid subid, Oid relid) /* * Stop the given logical replication parallel apply worker. * - * Node that the function sends SIGINT instead of SIGTERM to the parallel apply + * Node that the function sends SIGUSR2 instead of SIGTERM to the parallel apply * worker so that the worker exits cleanly. */ void @@ -661,22 +720,26 @@ logicalrep_pa_worker_stop(ParallelApplyWorkerInfo *winfo) * Only stop the worker if the generation matches and the worker is alive. */ if (worker->generation == generation && worker->proc) - logicalrep_worker_stop_internal(worker, SIGINT); + logicalrep_worker_stop_internal(worker, SIGUSR2); LWLockRelease(LogicalRepWorkerLock); } /* - * Wake up (using latch) any logical replication worker for specified sub/rel. + * Wake up (using latch) any logical replication worker that matches the + * specified worker type, subscription id, and relation id. */ void -logicalrep_worker_wakeup(Oid subid, Oid relid) +logicalrep_worker_wakeup(LogicalRepWorkerType wtype, Oid subid, Oid relid) { LogicalRepWorker *worker; + /* relid must be valid only for table sync workers */ + Assert((wtype == WORKERTYPE_TABLESYNC) == OidIsValid(relid)); + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); - worker = logicalrep_worker_find(subid, relid, true); + worker = logicalrep_worker_find(wtype, subid, relid, true); if (worker) logicalrep_worker_wakeup_ptr(worker); @@ -766,6 +829,8 @@ logicalrep_worker_detach(void) } LWLockRelease(LogicalRepWorkerLock); + + list_free(workers); } /* Block concurrent access. */ @@ -806,6 +871,33 @@ logicalrep_launcher_onexit(int code, Datum arg) LogicalRepCtx->launcher_pid = 0; } +/* + * Reset the last_seqsync_start_time of the sequencesync worker in the + * subscription's apply worker. + * + * Note that this value is not stored in the sequencesync worker, because that + * has finished already and is about to exit. + */ +void +logicalrep_reset_seqsync_start_time(void) +{ + LogicalRepWorker *worker; + + /* + * The apply worker can't access last_seqsync_start_time concurrently, so + * it is okay to use SHARED lock here. See ProcessSequencesForSync(). + */ + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + + worker = logicalrep_worker_find(WORKERTYPE_APPLY, + MyLogicalRepWorker->subid, InvalidOid, + true); + if (worker) + worker->last_seqsync_start_time = 0; + + LWLockRelease(LogicalRepWorkerLock); +} + /* * Cleanup function. * @@ -854,7 +946,7 @@ logicalrep_sync_worker_count(Oid subid) { LogicalRepWorker *w = &LogicalRepCtx->workers[i]; - if (isTablesyncWorker(w) && w->subid == subid) + if (w->subid == subid && (isTableSyncWorker(w) || isSequenceSyncWorker(w))) res++; } @@ -889,11 +981,11 @@ logicalrep_pa_worker_count(Oid subid) } /* - * ApplyLauncherShmemSize - * Compute space needed for replication launcher shared memory + * ApplyLauncherShmemRequest + * Register shared memory space needed for replication launcher */ -Size -ApplyLauncherShmemSize(void) +static void +ApplyLauncherShmemRequest(void *arg) { Size size; @@ -904,7 +996,10 @@ ApplyLauncherShmemSize(void) size = MAXALIGN(size); size = add_size(size, mul_size(max_logical_replication_workers, sizeof(LogicalRepWorker))); - return size; + ShmemRequestStruct(.name = "Logical Replication Launcher Data", + .size = size, + .ptr = (void **) &LogicalRepCtx, + ); } /* @@ -945,35 +1040,23 @@ ApplyLauncherRegister(void) /* * ApplyLauncherShmemInit - * Allocate and initialize replication launcher shared memory + * Initialize replication launcher shared memory */ -void -ApplyLauncherShmemInit(void) +static void +ApplyLauncherShmemInit(void *arg) { - bool found; + int slot; - LogicalRepCtx = (LogicalRepCtxStruct *) - ShmemInitStruct("Logical Replication Launcher Data", - ApplyLauncherShmemSize(), - &found); + LogicalRepCtx->last_start_dsa = DSA_HANDLE_INVALID; + LogicalRepCtx->last_start_dsh = DSHASH_HANDLE_INVALID; - if (!found) + /* Initialize memory and spin locks for each worker slot. */ + for (slot = 0; slot < max_logical_replication_workers; slot++) { - int slot; - - memset(LogicalRepCtx, 0, ApplyLauncherShmemSize()); + LogicalRepWorker *worker = &LogicalRepCtx->workers[slot]; - LogicalRepCtx->last_start_dsa = DSA_HANDLE_INVALID; - LogicalRepCtx->last_start_dsh = DSHASH_HANDLE_INVALID; - - /* Initialize memory and spin locks for each worker slot. */ - for (slot = 0; slot < max_logical_replication_workers; slot++) - { - LogicalRepWorker *worker = &LogicalRepCtx->workers[slot]; - - memset(worker, 0, sizeof(LogicalRepWorker)); - SpinLockInit(&worker->relmutex); - } + memset(worker, 0, sizeof(LogicalRepWorker)); + SpinLockInit(&worker->relmutex); } } @@ -1016,7 +1099,7 @@ logicalrep_launcher_attach_dshmem(void) last_start_times_dsa = dsa_attach(LogicalRepCtx->last_start_dsa); dsa_pin_mapping(last_start_times_dsa); last_start_times = dshash_attach(last_start_times_dsa, &dsh_params, - LogicalRepCtx->last_start_dsh, 0); + LogicalRepCtx->last_start_dsh, NULL); } MemoryContextSwitchTo(oldcontext); @@ -1105,7 +1188,10 @@ ApplyLauncherWakeupAtCommit(void) on_commit_launcher_wakeup = true; } -static void +/* + * Wakeup the launcher immediately. + */ +void ApplyLauncherWakeup(void) { if (LogicalRepCtx->launcher_pid != 0) @@ -1128,7 +1214,6 @@ ApplyLauncherMain(Datum main_arg) /* Establish signal handlers. */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGTERM, die); BackgroundWorkerUnblockSignals(); /* @@ -1137,6 +1222,12 @@ ApplyLauncherMain(Datum main_arg) */ BackgroundWorkerInitializeConnection(NULL, NULL, 0); + /* + * Acquire the conflict detection slot at startup to ensure it can be + * dropped if no longer needed after a restart. + */ + acquire_conflict_slot_if_exists(); + /* Enter main loop */ for (;;) { @@ -1146,6 +1237,9 @@ ApplyLauncherMain(Datum main_arg) MemoryContext subctx; MemoryContext oldctx; long wait_time = DEFAULT_NAPTIME_PER_CYCLE; + bool can_update_xmin = true; + bool retain_dead_tuples = false; + TransactionId xmin = InvalidTransactionId; CHECK_FOR_INTERRUPTS(); @@ -1155,7 +1249,14 @@ ApplyLauncherMain(Datum main_arg) ALLOCSET_DEFAULT_SIZES); oldctx = MemoryContextSwitchTo(subctx); - /* Start any missing workers for enabled subscriptions. */ + /* + * Start any missing workers for enabled subscriptions. + * + * Also, during the iteration through all subscriptions, we compute + * the minimum XID required to protect deleted tuples for conflict + * detection if one of the subscription enables retain_dead_tuples + * option. + */ sublist = get_subscription_list(); foreach(lc, sublist) { @@ -1165,15 +1266,87 @@ ApplyLauncherMain(Datum main_arg) TimestampTz now; long elapsed; + if (sub->retaindeadtuples) + { + retain_dead_tuples = true; + + /* + * Create a replication slot to retain information necessary + * for conflict detection such as dead tuples, commit + * timestamps, and origins. + * + * The slot is created before starting the apply worker to + * prevent it from unnecessarily maintaining its + * oldest_nonremovable_xid. + * + * The slot is created even for a disabled subscription to + * ensure that conflict-related information is available when + * applying remote changes that occurred before the + * subscription was enabled. + */ + CreateConflictDetectionSlot(); + + if (sub->retentionactive) + { + /* + * Can't advance xmin of the slot unless all the + * subscriptions actively retaining dead tuples are + * enabled. This is required to ensure that we don't + * advance the xmin of CONFLICT_DETECTION_SLOT if one of + * the subscriptions is not enabled. Otherwise, we won't + * be able to detect conflicts reliably for such a + * subscription even though it has set the + * retain_dead_tuples option. + */ + can_update_xmin &= sub->enabled; + + /* + * Initialize the slot once the subscription activates + * retention. + */ + if (!TransactionIdIsValid(MyReplicationSlot->data.xmin)) + init_conflict_slot_xmin(); + } + } + if (!sub->enabled) continue; LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); - w = logicalrep_worker_find(sub->oid, InvalidOid, false); - LWLockRelease(LogicalRepWorkerLock); + w = logicalrep_worker_find(WORKERTYPE_APPLY, sub->oid, InvalidOid, + false); if (w != NULL) - continue; /* worker is running already */ + { + /* + * Compute the minimum xmin required to protect dead tuples + * required for conflict detection among all running apply + * workers. This computation is performed while holding + * LogicalRepWorkerLock to prevent accessing invalid worker + * data, in scenarios where a worker might exit and reset its + * state concurrently. + */ + if (sub->retaindeadtuples && + sub->retentionactive && + can_update_xmin) + compute_min_nonremovable_xid(w, &xmin); + + LWLockRelease(LogicalRepWorkerLock); + + /* worker is running already */ + continue; + } + + LWLockRelease(LogicalRepWorkerLock); + + /* + * Can't advance xmin of the slot unless all the workers + * corresponding to subscriptions actively retaining dead tuples + * are running, disabling the further computation of the minimum + * nonremovable xid. + */ + if (sub->retaindeadtuples && sub->retentionactive) + can_update_xmin = false; /* * If the worker is eligible to start now, launch it. Otherwise, @@ -1194,10 +1367,23 @@ ApplyLauncherMain(Datum main_arg) (elapsed = TimestampDifferenceMilliseconds(last_start, now)) >= wal_retrieve_retry_interval) { ApplyLauncherSetWorkerStartTime(sub->oid, now); - logicalrep_worker_launch(WORKERTYPE_APPLY, - sub->dbid, sub->oid, sub->name, - sub->owner, InvalidOid, - DSM_HANDLE_INVALID); + if (!logicalrep_worker_launch(WORKERTYPE_APPLY, + sub->dbid, sub->oid, sub->name, + sub->owner, InvalidOid, + DSM_HANDLE_INVALID, + sub->retaindeadtuples && + sub->retentionactive)) + { + /* + * We get here either if we failed to launch a worker + * (perhaps for resource-exhaustion reasons) or if we + * launched one but it immediately quit. Either way, it + * seems appropriate to try again after + * wal_retrieve_retry_interval. + */ + wait_time = Min(wait_time, + wal_retrieve_retry_interval); + } } else { @@ -1206,6 +1392,25 @@ ApplyLauncherMain(Datum main_arg) } } + /* + * Drop the CONFLICT_DETECTION_SLOT slot if there is no subscription + * that requires us to retain dead tuples. Otherwise, if required, + * advance the slot's xmin to protect dead tuples required for the + * conflict detection. + * + * Additionally, if all apply workers for subscriptions with + * retain_dead_tuples enabled have requested to stop retention, the + * slot's xmin will be set to InvalidTransactionId allowing the + * removal of dead tuples. + */ + if (MyReplicationSlot) + { + if (!retain_dead_tuples) + ReplicationSlotDropAcquired(); + else if (can_update_xmin) + update_conflict_slot_xmin(xmin); + } + /* Switch back to original memory context. */ MemoryContextSwitchTo(oldctx); /* Clean the temporary memory. */ @@ -1233,6 +1438,148 @@ ApplyLauncherMain(Datum main_arg) /* Not reachable */ } +/* + * Determine the minimum non-removable transaction ID across all apply workers + * for subscriptions that have retain_dead_tuples enabled. Store the result + * in *xmin. + */ +static void +compute_min_nonremovable_xid(LogicalRepWorker *worker, TransactionId *xmin) +{ + TransactionId nonremovable_xid; + + Assert(worker != NULL); + + /* + * The replication slot for conflict detection must be created before the + * worker starts. + */ + Assert(MyReplicationSlot); + + SpinLockAcquire(&worker->relmutex); + nonremovable_xid = worker->oldest_nonremovable_xid; + SpinLockRelease(&worker->relmutex); + + /* + * Return if the apply worker has stopped retention concurrently. + * + * Although this function is invoked only when retentionactive is true, + * the apply worker might stop retention after the launcher fetches the + * retentionactive flag. + */ + if (!TransactionIdIsValid(nonremovable_xid)) + return; + + if (!TransactionIdIsValid(*xmin) || + TransactionIdPrecedes(nonremovable_xid, *xmin)) + *xmin = nonremovable_xid; +} + +/* + * Acquire the replication slot used to retain information for conflict + * detection, if it exists. + * + * Return true if successfully acquired, otherwise return false. + */ +static bool +acquire_conflict_slot_if_exists(void) +{ + if (!SearchNamedReplicationSlot(CONFLICT_DETECTION_SLOT, true)) + return false; + + ReplicationSlotAcquire(CONFLICT_DETECTION_SLOT, true, false); + return true; +} + +/* + * Update the xmin the replication slot used to retain information required + * for conflict detection. + */ +static void +update_conflict_slot_xmin(TransactionId new_xmin) +{ + Assert(MyReplicationSlot); + Assert(!TransactionIdIsValid(new_xmin) || + TransactionIdPrecedesOrEquals(MyReplicationSlot->data.xmin, new_xmin)); + + /* Return if the xmin value of the slot cannot be updated */ + if (TransactionIdEquals(MyReplicationSlot->data.xmin, new_xmin)) + return; + + SpinLockAcquire(&MyReplicationSlot->mutex); + MyReplicationSlot->effective_xmin = new_xmin; + MyReplicationSlot->data.xmin = new_xmin; + SpinLockRelease(&MyReplicationSlot->mutex); + + elog(DEBUG1, "updated xmin: %u", MyReplicationSlot->data.xmin); + + ReplicationSlotMarkDirty(); + ReplicationSlotsComputeRequiredXmin(false); + + /* + * Like PhysicalConfirmReceivedLocation(), do not save slot information + * each time. This is acceptable because all concurrent transactions on + * the publisher that require the data preceding the slot's xmin should + * have already been applied and flushed on the subscriber before the xmin + * is advanced. So, even if the slot's xmin regresses after a restart, it + * will be advanced again in the next cycle. Therefore, no data required + * for conflict detection will be prematurely removed. + */ + return; +} + +/* + * Initialize the xmin for the conflict detection slot. + */ +static void +init_conflict_slot_xmin(void) +{ + TransactionId xmin_horizon; + + /* Replication slot must exist but shouldn't be initialized. */ + Assert(MyReplicationSlot && + !TransactionIdIsValid(MyReplicationSlot->data.xmin)); + + LWLockAcquire(ReplicationSlotControlLock, LW_EXCLUSIVE); + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + + xmin_horizon = GetOldestSafeDecodingTransactionId(false); + + SpinLockAcquire(&MyReplicationSlot->mutex); + MyReplicationSlot->effective_xmin = xmin_horizon; + MyReplicationSlot->data.xmin = xmin_horizon; + SpinLockRelease(&MyReplicationSlot->mutex); + + ReplicationSlotsComputeRequiredXmin(true); + + LWLockRelease(ProcArrayLock); + LWLockRelease(ReplicationSlotControlLock); + + /* Write this slot to disk */ + ReplicationSlotMarkDirty(); + ReplicationSlotSave(); +} + +/* + * Create and acquire the replication slot used to retain information for + * conflict detection, if not yet. + */ +void +CreateConflictDetectionSlot(void) +{ + /* Exit early, if the replication slot is already created and acquired */ + if (MyReplicationSlot) + return; + + ereport(LOG, + errmsg("creating replication conflict detection slot")); + + ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false, + false, false, false); + + init_conflict_slot_xmin(); +} + /* * Is current process the logical replication launcher? */ @@ -1305,7 +1652,7 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS) worker_pid = worker.proc->pid; values[0] = ObjectIdGetDatum(worker.subid); - if (isTablesyncWorker(&worker)) + if (isTableSyncWorker(&worker)) values[1] = ObjectIdGetDatum(worker.relid); else nulls[1] = true; @@ -1316,7 +1663,7 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS) else nulls[3] = true; - if (XLogRecPtrIsInvalid(worker.last_lsn)) + if (!XLogRecPtrIsValid(worker.last_lsn)) nulls[4] = true; else values[4] = LSNGetDatum(worker.last_lsn); @@ -1328,7 +1675,7 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS) nulls[6] = true; else values[6] = TimestampTzGetDatum(worker.last_recv_time); - if (XLogRecPtrIsInvalid(worker.reply_lsn)) + if (!XLogRecPtrIsValid(worker.reply_lsn)) nulls[7] = true; else values[7] = LSNGetDatum(worker.reply_lsn); @@ -1345,6 +1692,9 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS) case WORKERTYPE_PARALLEL_APPLY: values[9] = CStringGetTextDatum("parallel apply"); break; + case WORKERTYPE_SEQUENCESYNC: + values[9] = CStringGetTextDatum("sequence synchronization"); + break; case WORKERTYPE_TABLESYNC: values[9] = CStringGetTextDatum("table synchronization"); break; diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index 1d56d0c4ef314..a33a685dcc6d5 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -2,7 +2,7 @@ * logical.c * PostgreSQL logical decoding coordination * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/logical.c @@ -29,6 +29,7 @@ #include "postgres.h" #include "access/xact.h" +#include "access/xlog_internal.h" #include "access/xlogutils.h" #include "fmgr.h" #include "miscadmin.h" @@ -41,6 +42,7 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/inval.h" #include "utils/memutils.h" @@ -106,40 +108,29 @@ static void LoadOutputPlugin(OutputPluginCallbacks *callbacks, const char *plugi * decoding. */ void -CheckLogicalDecodingRequirements(void) +CheckLogicalDecodingRequirements(bool repack) { - CheckSlotRequirements(); + CheckSlotRequirements(repack); /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() * needs the same check. */ - if (wal_level < WAL_LEVEL_LOGICAL) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("logical decoding requires \"wal_level\" >= \"logical\""))); - if (MyDatabaseId == InvalidOid) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("logical decoding requires a database connection"))); - if (RecoveryInProgress()) - { - /* - * This check may have race conditions, but whenever - * XLOG_PARAMETER_CHANGE indicates that wal_level has changed, we - * verify that there are no existing logical replication slots. And to - * avoid races around creating a new slot, - * CheckLogicalDecodingRequirements() is called once before creating - * the slot, and once when logical decoding is initially starting up. - */ - if (GetActiveWalLevelOnStandby() < WAL_LEVEL_LOGICAL) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("logical decoding on standby requires \"wal_level\" >= \"logical\" on the primary"))); - } + /* CheckSlotRequirements() has already checked if wal_level >= 'replica' */ + Assert(wal_level >= WAL_LEVEL_REPLICA); + + /* Check if logical decoding is available on standby */ + if (RecoveryInProgress() && !IsLogicalDecodingEnabled()) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("logical decoding on standby requires \"effective_wal_level\" >= \"logical\" on the primary"), + errhint("Set \"wal_level\" >= \"logical\" or create at least one logical slot when \"wal_level\" = \"replica\"."))); } /* @@ -170,7 +161,7 @@ StartupDecodingContext(List *output_plugin_options, "Logical decoding context", ALLOCSET_DEFAULT_SIZES); old_context = MemoryContextSwitchTo(context); - ctx = palloc0(sizeof(LogicalDecodingContext)); + ctx = palloc0_object(LogicalDecodingContext); ctx->context = context; @@ -294,6 +285,9 @@ StartupDecodingContext(List *output_plugin_options, ctx->write = do_write; ctx->update_progress = update_progress; + /* Assume shared catalog access. The startup callback can change it. */ + ctx->options.need_shared_catalogs = true; + ctx->output_plugin_options = output_plugin_options; ctx->fast_forward = fast_forward; @@ -310,6 +304,7 @@ StartupDecodingContext(List *output_plugin_options, * output_plugin_options -- contains options passed to the output plugin * need_full_snapshot -- if true, must obtain a snapshot able to read all * tables; if false, one that can read only catalogs is acceptable. + * for_repack -- if true, we're going to be decoding for REPACK. * restart_lsn -- if given as invalid, it's this routine's responsibility to * mark WAL as reserved by setting a convenient restart_lsn for the slot. * Otherwise, we set for decoding to start from the given LSN without @@ -330,6 +325,7 @@ LogicalDecodingContext * CreateInitDecodingContext(const char *plugin, List *output_plugin_options, bool need_full_snapshot, + bool for_repack, XLogRecPtr restart_lsn, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, @@ -346,7 +342,7 @@ CreateInitDecodingContext(const char *plugin, * On a standby, this check is also required while creating the slot. * Check the comments in the function. */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(for_repack); /* shorter lines... */ slot = MyReplicationSlot; @@ -386,7 +382,7 @@ CreateInitDecodingContext(const char *plugin, slot->data.plugin = plugin_name; SpinLockRelease(&slot->mutex); - if (XLogRecPtrIsInvalid(restart_lsn)) + if (!XLogRecPtrIsValid(restart_lsn)) ReplicationSlotReserveWal(); else { @@ -403,11 +399,11 @@ CreateInitDecodingContext(const char *plugin, * without further interlock its return value might immediately be out of * date. * - * So we have to acquire the ProcArrayLock to prevent computation of new - * xmin horizons by other backends, get the safe decoding xid, and inform - * the slot machinery about the new limit. Once that's done the - * ProcArrayLock can be released as the slot machinery now is - * protecting against vacuum. + * So we have to acquire both the ReplicationSlotControlLock and the + * ProcArrayLock to prevent concurrent computation and update of new xmin + * horizons by other backends, get the safe decoding xid, and inform the + * slot machinery about the new limit. Once that's done both locks can be + * released as the slot machinery now is protecting against vacuum. * * Note that, temporarily, the data, not just the catalog, xmin has to be * reserved if a data snapshot is to be exported. Otherwise the initial @@ -420,6 +416,7 @@ CreateInitDecodingContext(const char *plugin, * * ---- */ + LWLockAcquire(ReplicationSlotControlLock, LW_EXCLUSIVE); LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); xmin_horizon = GetOldestSafeDecodingTransactionId(!need_full_snapshot); @@ -434,6 +431,7 @@ CreateInitDecodingContext(const char *plugin, ReplicationSlotsComputeRequiredXmin(true); LWLockRelease(ProcArrayLock); + LWLockRelease(ReplicationSlotControlLock); ReplicationSlotMarkDirty(); ReplicationSlotSave(); @@ -544,9 +542,9 @@ CreateDecodingContext(XLogRecPtr start_lsn, /* slot must be valid to allow decoding */ Assert(slot->data.invalidated == RS_INVAL_NONE); - Assert(slot->data.restart_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(slot->data.restart_lsn)); - if (start_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(start_lsn)) { /* continue from last position */ start_lsn = slot->data.confirmed_flush; @@ -565,7 +563,7 @@ CreateDecodingContext(XLogRecPtr start_lsn, * kinds of client errors; so the client may wish to check that * confirmed_flush_lsn matches its expectations. */ - elog(LOG, "%X/%X has been already streamed, forwarding to %X/%X", + elog(LOG, "%X/%08X has been already streamed, forwarding to %X/%08X", LSN_FORMAT_ARGS(start_lsn), LSN_FORMAT_ARGS(slot->data.confirmed_flush)); @@ -605,10 +603,10 @@ CreateDecodingContext(XLogRecPtr start_lsn, ctx->reorder->output_rewrites = ctx->options.receive_rewrites; - ereport(LOG, + ereport(LogicalDecodingLogLevel(), (errmsg("starting logical decoding for slot \"%s\"", NameStr(slot->data.name)), - errdetail("Streaming transactions committing after %X/%X, reading WAL from %X/%X.", + errdetail("Streaming transactions committing after %X/%08X, reading WAL from %X/%08X.", LSN_FORMAT_ARGS(slot->data.confirmed_flush), LSN_FORMAT_ARGS(slot->data.restart_lsn)))); @@ -635,7 +633,7 @@ DecodingContextFindStartpoint(LogicalDecodingContext *ctx) /* Initialize from where to start reading WAL. */ XLogBeginRead(ctx->reader, slot->data.restart_lsn); - elog(DEBUG1, "searching for logical decoding starting point, starting at %X/%X", + elog(DEBUG1, "searching for logical decoding starting point, starting at %X/%08X", LSN_FORMAT_ARGS(slot->data.restart_lsn)); /* Wait for a consistent starting point */ @@ -755,8 +753,8 @@ output_plugin_error_callback(void *arg) LogicalErrorCallbackState *state = (LogicalErrorCallbackState *) arg; /* not all callbacks have an associated LSN */ - if (state->report_location != InvalidXLogRecPtr) - errcontext("slot \"%s\", output plugin \"%s\", in the %s callback, associated LSN %X/%X", + if (XLogRecPtrIsValid(state->report_location)) + errcontext("slot \"%s\", output plugin \"%s\", in the %s callback, associated LSN %X/%08X", NameStr(state->ctx->slot->data.name), NameStr(state->ctx->slot->data.plugin), state->callback_name, @@ -1194,7 +1192,7 @@ filter_prepare_cb_wrapper(LogicalDecodingContext *ctx, TransactionId xid, } bool -filter_by_origin_cb_wrapper(LogicalDecodingContext *ctx, RepOriginId origin_id) +filter_by_origin_cb_wrapper(LogicalDecodingContext *ctx, ReplOriginId origin_id) { LogicalErrorCallbackState state; ErrorContextCallback errcallback; @@ -1709,7 +1707,7 @@ LogicalIncreaseXminForSlot(XLogRecPtr current_lsn, TransactionId xmin) * Only increase if the previous values have been applied, otherwise we * might never end up updating if the receiver acks too slowly. */ - else if (slot->candidate_xmin_lsn == InvalidXLogRecPtr) + else if (!XLogRecPtrIsValid(slot->candidate_xmin_lsn)) { slot->candidate_catalog_xmin = xmin; slot->candidate_xmin_lsn = current_lsn; @@ -1723,7 +1721,7 @@ LogicalIncreaseXminForSlot(XLogRecPtr current_lsn, TransactionId xmin) SpinLockRelease(&slot->mutex); if (got_new_xmin) - elog(DEBUG1, "got new catalog xmin %u at %X/%X", xmin, + elog(DEBUG1, "got new catalog xmin %u at %X/%08X", xmin, LSN_FORMAT_ARGS(current_lsn)); /* candidate already valid with the current flush position, apply */ @@ -1747,8 +1745,8 @@ LogicalIncreaseRestartDecodingForSlot(XLogRecPtr current_lsn, XLogRecPtr restart slot = MyReplicationSlot; Assert(slot != NULL); - Assert(restart_lsn != InvalidXLogRecPtr); - Assert(current_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(restart_lsn)); + Assert(XLogRecPtrIsValid(current_lsn)); SpinLockAcquire(&slot->mutex); @@ -1777,13 +1775,13 @@ LogicalIncreaseRestartDecodingForSlot(XLogRecPtr current_lsn, XLogRecPtr restart * might never end up updating if the receiver acks too slowly. A missed * value here will just cause some extra effort after reconnecting. */ - else if (slot->candidate_restart_valid == InvalidXLogRecPtr) + else if (!XLogRecPtrIsValid(slot->candidate_restart_valid)) { slot->candidate_restart_valid = current_lsn; slot->candidate_restart_lsn = restart_lsn; SpinLockRelease(&slot->mutex); - elog(DEBUG1, "got new restart lsn %X/%X at %X/%X", + elog(DEBUG1, "got new restart lsn %X/%08X at %X/%08X", LSN_FORMAT_ARGS(restart_lsn), LSN_FORMAT_ARGS(current_lsn)); } @@ -1798,7 +1796,7 @@ LogicalIncreaseRestartDecodingForSlot(XLogRecPtr current_lsn, XLogRecPtr restart confirmed_flush = slot->data.confirmed_flush; SpinLockRelease(&slot->mutex); - elog(DEBUG1, "failed to increase restart lsn: proposed %X/%X, after %X/%X, current candidate %X/%X, current after %X/%X, flushed up to %X/%X", + elog(DEBUG1, "failed to increase restart lsn: proposed %X/%08X, after %X/%08X, current candidate %X/%08X, current after %X/%08X, flushed up to %X/%08X", LSN_FORMAT_ARGS(restart_lsn), LSN_FORMAT_ARGS(current_lsn), LSN_FORMAT_ARGS(candidate_restart_lsn), @@ -1817,17 +1815,21 @@ LogicalIncreaseRestartDecodingForSlot(XLogRecPtr current_lsn, XLogRecPtr restart void LogicalConfirmReceivedLocation(XLogRecPtr lsn) { - Assert(lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(lsn)); /* Do an unlocked check for candidate_lsn first. */ - if (MyReplicationSlot->candidate_xmin_lsn != InvalidXLogRecPtr || - MyReplicationSlot->candidate_restart_valid != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(MyReplicationSlot->candidate_xmin_lsn) || + XLogRecPtrIsValid(MyReplicationSlot->candidate_restart_valid)) { bool updated_xmin = false; bool updated_restart = false; + XLogRecPtr restart_lsn pg_attribute_unused(); SpinLockAcquire(&MyReplicationSlot->mutex); + /* remember the old restart lsn */ + restart_lsn = MyReplicationSlot->data.restart_lsn; + /* * Prevent moving the confirmed_flush backwards, as this could lead to * data duplication issues caused by replicating already replicated @@ -1843,7 +1845,7 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn) MyReplicationSlot->data.confirmed_flush = lsn; /* if we're past the location required for bumping xmin, do so */ - if (MyReplicationSlot->candidate_xmin_lsn != InvalidXLogRecPtr && + if (XLogRecPtrIsValid(MyReplicationSlot->candidate_xmin_lsn) && MyReplicationSlot->candidate_xmin_lsn <= lsn) { /* @@ -1865,10 +1867,10 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn) } } - if (MyReplicationSlot->candidate_restart_valid != InvalidXLogRecPtr && + if (XLogRecPtrIsValid(MyReplicationSlot->candidate_restart_valid) && MyReplicationSlot->candidate_restart_valid <= lsn) { - Assert(MyReplicationSlot->candidate_restart_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(MyReplicationSlot->candidate_restart_lsn)); MyReplicationSlot->data.restart_lsn = MyReplicationSlot->candidate_restart_lsn; MyReplicationSlot->candidate_restart_lsn = InvalidXLogRecPtr; @@ -1881,6 +1883,18 @@ LogicalConfirmReceivedLocation(XLogRecPtr lsn) /* first write new xmin to disk, so we know what's up after a crash */ if (updated_xmin || updated_restart) { +#ifdef USE_INJECTION_POINTS + XLogSegNo seg1, + seg2; + + XLByteToSeg(restart_lsn, seg1, wal_segment_size); + XLByteToSeg(MyReplicationSlot->data.restart_lsn, seg2, wal_segment_size); + + /* trigger injection point, but only if segment changes */ + if (seg1 != seg2) + INJECTION_POINT("logical-replication-slot-advance-segment", NULL); +#endif + ReplicationSlotMarkDirty(); ReplicationSlotSave(); elog(DEBUG1, "updated xmin: %u restart: %u", updated_xmin, updated_restart); @@ -1937,10 +1951,11 @@ UpdateDecodingStats(LogicalDecodingContext *ctx) PgStat_StatReplSlotEntry repSlotStat; /* Nothing to do if we don't have any replication stats to be sent. */ - if (rb->spillBytes <= 0 && rb->streamBytes <= 0 && rb->totalBytes <= 0) + if (rb->spillBytes <= 0 && rb->streamBytes <= 0 && rb->totalBytes <= 0 && + rb->memExceededCount <= 0) return; - elog(DEBUG2, "UpdateDecodingStats: updating stats %p %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64, + elog(DEBUG2, "UpdateDecodingStats: updating stats %p %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64 " %" PRId64, rb, rb->spillTxns, rb->spillCount, @@ -1948,6 +1963,7 @@ UpdateDecodingStats(LogicalDecodingContext *ctx) rb->streamTxns, rb->streamCount, rb->streamBytes, + rb->memExceededCount, rb->totalTxns, rb->totalBytes); @@ -1957,6 +1973,7 @@ UpdateDecodingStats(LogicalDecodingContext *ctx) repSlotStat.stream_txns = rb->streamTxns; repSlotStat.stream_count = rb->streamCount; repSlotStat.stream_bytes = rb->streamBytes; + repSlotStat.mem_exceeded_count = rb->memExceededCount; repSlotStat.total_txns = rb->totalTxns; repSlotStat.total_bytes = rb->totalBytes; @@ -1968,21 +1985,28 @@ UpdateDecodingStats(LogicalDecodingContext *ctx) rb->streamTxns = 0; rb->streamCount = 0; rb->streamBytes = 0; + rb->memExceededCount = 0; rb->totalTxns = 0; rb->totalBytes = 0; } /* - * Read up to the end of WAL starting from the decoding slot's restart_lsn. - * Return true if any meaningful/decodable WAL records are encountered, - * otherwise false. + * Read up to the end of WAL starting from the decoding slot's restart_lsn + * to end_of_wal in order to check if any meaningful/decodable WAL records + * are encountered. scan_cutoff_lsn is the LSN, where we can terminate the + * WAL scan early if we find a decodable WAL record after this LSN. + * + * Returns the last LSN decodable WAL record's LSN if found, otherwise + * returns InvalidXLogRecPtr. */ -bool -LogicalReplicationSlotHasPendingWal(XLogRecPtr end_of_wal) +XLogRecPtr +LogicalReplicationSlotCheckPendingWal(XLogRecPtr end_of_wal, + XLogRecPtr scan_cutoff_lsn) { - bool has_pending_wal = false; + XLogRecPtr last_pending_wal = InvalidXLogRecPtr; Assert(MyReplicationSlot); + Assert(end_of_wal >= scan_cutoff_lsn); PG_TRY(); { @@ -2010,8 +2034,7 @@ LogicalReplicationSlotHasPendingWal(XLogRecPtr end_of_wal) /* Invalidate non-timetravel entries */ InvalidateSystemCaches(); - /* Loop until the end of WAL or some changes are processed */ - while (!has_pending_wal && ctx->reader->EndRecPtr < end_of_wal) + while (ctx->reader->EndRecPtr < end_of_wal) { XLogRecord *record; char *errm = NULL; @@ -2024,7 +2047,20 @@ LogicalReplicationSlotHasPendingWal(XLogRecPtr end_of_wal) if (record != NULL) LogicalDecodingProcessRecord(ctx, ctx->reader); - has_pending_wal = ctx->processing_required; + if (ctx->processing_required) + { + last_pending_wal = ctx->reader->ReadRecPtr; + + /* + * If we find a decodable WAL after the scan_cutoff_lsn point, + * we can terminate the scan early. + */ + if (last_pending_wal >= scan_cutoff_lsn) + break; + + /* Reset the flag and continue checking */ + ctx->processing_required = false; + } CHECK_FOR_INTERRUPTS(); } @@ -2042,7 +2078,7 @@ LogicalReplicationSlotHasPendingWal(XLogRecPtr end_of_wal) } PG_END_TRY(); - return has_pending_wal; + return last_pending_wal; } /* @@ -2064,10 +2100,10 @@ LogicalSlotAdvanceAndCheckSnapState(XLogRecPtr moveto, bool *found_consistent_snapshot) { LogicalDecodingContext *ctx; - ResourceOwner old_resowner = CurrentResourceOwner; + ResourceOwner old_resowner PG_USED_FOR_ASSERTS_ONLY = CurrentResourceOwner; XLogRecPtr retlsn; - Assert(moveto != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(moveto)); if (found_consistent_snapshot) *found_consistent_snapshot = false; @@ -2123,22 +2159,25 @@ LogicalSlotAdvanceAndCheckSnapState(XLogRecPtr moveto, * might still have critical updates to do. */ if (record) + { LogicalDecodingProcessRecord(ctx, ctx->reader); + /* + * We used to have bugs where logical decoding would fail to + * preserve the resource owner. That's important here, so + * verify that that doesn't happen anymore. XXX this could be + * removed once it's been battle-tested. + */ + Assert(CurrentResourceOwner == old_resowner); + } + CHECK_FOR_INTERRUPTS(); } if (found_consistent_snapshot && DecodingContextReady(ctx)) *found_consistent_snapshot = true; - /* - * Logical decoding could have clobbered CurrentResourceOwner during - * transaction management, so restore the executor's value. (This is - * a kluge, but it's not worth cleaning up right now.) - */ - CurrentResourceOwner = old_resowner; - - if (ctx->reader->EndRecPtr != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(ctx->reader->EndRecPtr)) { LogicalConfirmReceivedLocation(moveto); diff --git a/src/backend/replication/logical/logicalctl.c b/src/backend/replication/logical/logicalctl.c new file mode 100644 index 0000000000000..72f68ec58ef7e --- /dev/null +++ b/src/backend/replication/logical/logicalctl.c @@ -0,0 +1,637 @@ +/*------------------------------------------------------------------------- + * logicalctl.c + * Functionality to control logical decoding status online. + * + * This module enables dynamic control of logical decoding availability. + * Logical decoding becomes active under two conditions: when the wal_level + * parameter is set to 'logical', or when at least one valid logical replication + * slot exists with wal_level set to 'replica'. The system disables logical + * decoding when neither condition is met. Therefore, the dynamic control + * of logical decoding availability is required only when wal_level is set + * to 'replica'. Logical decoding is always enabled when wal_level='logical' + * and always disabled when wal_level='minimal'. + * + * The core concept of dynamically enabling and disabling logical decoding + * is to separately control two aspects: writing information required for + * logical decoding to WAL records, and using logical decoding itself. During + * activation, we first enable logical WAL writing while keeping logical + * decoding disabled. This change is reflected in the read-only + * effective_wal_level GUC parameter. Once we ensure that all processes have + * updated to the latest effective_wal_level value, we then enable logical + * decoding. Deactivation follows a similar careful, multi-step process + * in reverse order. + * + * While activation occurs synchronously right after creating the first + * logical slot, deactivation happens asynchronously through the checkpointer + * process. This design avoids a race condition at the end of recovery; see + * the comments in UpdateLogicalDecodingStatusEndOfRecovery() for details. + * Asynchronous deactivation also avoids excessive toggling of the logical + * decoding status in workloads that repeatedly create and drop a single + * logical slot. On the other hand, this lazy approach can delay changes + * to effective_wal_level and the disabling logical decoding, especially + * when the checkpointer is busy with other tasks. We chose this lazy approach + * in all deactivation paths to keep the implementation simple, even though + * laziness is strictly required only for end-of-recovery cases. Future work + * might address this limitation either by using a dedicated worker instead + * of the checkpointer, or by implementing synchronous waiting during slot + * drops if workloads are significantly affected by the lazy deactivation + * of logical decoding. + * + * Standby servers use the primary server's effective_wal_level and logical + * decoding status. Unlike normal activation and deactivation, these + * are updated simultaneously without status change coordination, solely by + * replaying XLOG_LOGICAL_DECODING_STATUS_CHANGE records. The local wal_level + * setting has no effect during this time. Upon promotion, we update the + * logical decoding status based on local conditions: the wal_level value and + * the presence of logical slots. + * + * In the future, we could extend support to include automatic transitions + * of effective_wal_level between 'minimal' and 'logical' WAL levels. However, + * this enhancement would require additional coordination mechanisms and + * careful implementation of operations such as terminating walsenders and + * archiver processes while carefully considering the sequence of operations + * to ensure system stability during these transitions. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/replication/logical/logicalctl.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/xloginsert.h" +#include "catalog/pg_control.h" +#include "miscadmin.h" +#include "replication/slot.h" +#include "storage/ipc.h" +#include "storage/lmgr.h" +#include "storage/proc.h" +#include "storage/procarray.h" +#include "storage/procsignal.h" +#include "storage/subsystems.h" +#include "utils/injection_point.h" + +/* + * Struct for controlling the logical decoding status. + * + * This struct is protected by LogicalDecodingControlLock. + */ +typedef struct LogicalDecodingCtlData +{ + /* + * This is the authoritative value used by all processes to determine + * whether to write additional information required by logical decoding to + * WAL. Since this information could be checked frequently, each process + * caches this value in XLogLogicalInfo for better performance. + */ + bool xlog_logical_info; + + /* True if logical decoding is available in the system */ + bool logical_decoding_enabled; + + /* True if logical decoding might need to be disabled */ + bool pending_disable; +} LogicalDecodingCtlData; + +static LogicalDecodingCtlData *LogicalDecodingCtl = NULL; + +static void LogicalDecodingCtlShmemRequest(void *arg); + +const ShmemCallbacks LogicalDecodingCtlShmemCallbacks = { + .request_fn = LogicalDecodingCtlShmemRequest, +}; + +/* + * A process-local cache of LogicalDecodingCtl->xlog_logical_info. This is + * initialized at process startup, and updated when processing the process + * barrier signal in ProcessBarrierUpdateXLogLogicalInfo(). If the process + * is in an XID-assigned transaction, the cache update is delayed until the + * transaction ends. See the comments for XLogLogicalInfoUpdatePending for details. + */ +bool XLogLogicalInfo = false; + +/* + * When receiving the PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO signal, if + * an XID is assigned to the current transaction, the process sets this flag and + * delays the XLogLogicalInfo update until the transaction ends. This ensures + * that the XLogLogicalInfo value (typically accessed via XLogLogicalInfoActive) + * remains consistent throughout the transaction. + */ +static bool XLogLogicalInfoUpdatePending = false; + +static void update_xlog_logical_info(void); +static void abort_logical_decoding_activation(int code, Datum arg); +static void write_logical_decoding_status_update_record(bool status); + +static void +LogicalDecodingCtlShmemRequest(void *arg) +{ + ShmemRequestStruct(.name = "Logical decoding control", + .size = sizeof(LogicalDecodingCtlData), + .ptr = (void **) &LogicalDecodingCtl, + ); +} + +/* + * Initialize the logical decoding status in shmem at server startup. This + * must be called ONCE during postmaster or standalone-backend startup. + */ +void +StartupLogicalDecodingStatus(bool last_status) +{ + /* Logical decoding is always disabled when 'minimal' WAL level */ + if (wal_level == WAL_LEVEL_MINIMAL) + return; + + /* + * Set the initial logical decoding status based on the last status. If + * logical decoding was enabled before the last shutdown, it remains + * enabled as we might have set wal_level='logical' or have at least one + * logical slot. + */ + LogicalDecodingCtl->xlog_logical_info = last_status; + LogicalDecodingCtl->logical_decoding_enabled = last_status; +} + +/* + * Update the XLogLogicalInfo cache. + */ +static inline void +update_xlog_logical_info(void) +{ + XLogLogicalInfo = IsXLogLogicalInfoEnabled(); +} + +/* + * Initialize XLogLogicalInfo backend-private cache. This routine is called + * during process initialization. + */ +void +InitializeProcessXLogLogicalInfo(void) +{ + update_xlog_logical_info(); +} + +/* + * This routine is called when we are told to update XLogLogicalInfo + * by a ProcSignalBarrier. + */ +bool +ProcessBarrierUpdateXLogLogicalInfo(void) +{ + if (GetTopTransactionIdIfAny() != InvalidTransactionId) + { + /* Delay updating XLogLogicalInfo until the transaction end */ + XLogLogicalInfoUpdatePending = true; + } + else + update_xlog_logical_info(); + + return true; +} + +/* + * Check the shared memory state and return true if logical decoding is + * enabled on the system. + */ +bool +IsLogicalDecodingEnabled(void) +{ + bool enabled; + + LWLockAcquire(LogicalDecodingControlLock, LW_SHARED); + enabled = LogicalDecodingCtl->logical_decoding_enabled; + LWLockRelease(LogicalDecodingControlLock); + + return enabled; +} + +/* + * Returns true if logical WAL logging is enabled based on the shared memory + * status. + */ +bool +IsXLogLogicalInfoEnabled(void) +{ + bool xlog_logical_info; + + LWLockAcquire(LogicalDecodingControlLock, LW_SHARED); + xlog_logical_info = LogicalDecodingCtl->xlog_logical_info; + LWLockRelease(LogicalDecodingControlLock); + + return xlog_logical_info; +} + +/* + * Reset the local cache at end of the transaction. + */ +void +AtEOXact_LogicalCtl(void) +{ + /* Update the local cache if there is a pending update */ + if (XLogLogicalInfoUpdatePending) + { + update_xlog_logical_info(); + XLogLogicalInfoUpdatePending = false; + } +} + +/* + * Writes an XLOG_LOGICAL_DECODING_STATUS_CHANGE WAL record with the given + * status. + */ +static void +write_logical_decoding_status_update_record(bool status) +{ + XLogRecPtr recptr; + + XLogBeginInsert(); + XLogRegisterData(&status, sizeof(bool)); + recptr = XLogInsert(RM_XLOG_ID, XLOG_LOGICAL_DECODING_STATUS_CHANGE); + XLogFlush(recptr); +} + +/* + * A PG_ENSURE_ERROR_CLEANUP callback for activating logical decoding, resetting + * the shared flags to revert the logical decoding activation process. + */ +static void +abort_logical_decoding_activation(int code, Datum arg) +{ + Assert(MyReplicationSlot); + Assert(!LogicalDecodingCtl->logical_decoding_enabled); + + elog(DEBUG1, "aborting logical decoding activation process"); + + /* + * Abort the change to xlog_logical_info. We don't need to check + * CheckLogicalSlotExists() as we're still holding a logical slot. + */ + LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE); + LogicalDecodingCtl->xlog_logical_info = false; + LWLockRelease(LogicalDecodingControlLock); + + /* + * Some processes might have already started logical info WAL logging, so + * tell all running processes to update their caches. We don't need to + * wait for all processes to disable xlog_logical_info locally as it's + * always safe to write logical information to WAL records, even when not + * strictly required. + */ + EmitProcSignalBarrier(PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO); +} + +/* + * Enable logical decoding if disabled. + * + * If this function is called during recovery, it simply returns without + * action since the logical decoding status change is not allowed during + * this time. The logical decoding status depends on the status on the primary. + * The caller should use CheckLogicalDecodingRequirements() before calling this + * function to make sure that the logical decoding status can be modified. + * + * Note that there is no interlock between logical decoding activation + * and slot creation. To ensure enabling logical decoding, the caller + * needs to call this function after creating a logical slot before + * initializing the logical decoding context. + */ +void +EnsureLogicalDecodingEnabled(void) +{ + Assert(MyReplicationSlot); + Assert(wal_level >= WAL_LEVEL_REPLICA); + + /* Logical decoding is always enabled */ + if (wal_level >= WAL_LEVEL_LOGICAL) + return; + + if (RecoveryInProgress()) + { + /* + * CheckLogicalDecodingRequirements() must have already errored out if + * logical decoding is not enabled since we cannot enable the logical + * decoding status during recovery. + */ + Assert(IsLogicalDecodingEnabled()); + return; + } + + /* + * Ensure to abort the activation process in cases where there in an + * interruption during the wait. + */ + PG_ENSURE_ERROR_CLEANUP(abort_logical_decoding_activation, (Datum) 0); + { + EnableLogicalDecoding(); + } + PG_END_ENSURE_ERROR_CLEANUP(abort_logical_decoding_activation, (Datum) 0); +} + +/* + * A workhorse function to enable logical decoding. + */ +void +EnableLogicalDecoding(void) +{ + bool in_recovery; + + LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE); + + /* Return if it is already enabled */ + if (LogicalDecodingCtl->logical_decoding_enabled) + { + LogicalDecodingCtl->pending_disable = false; + LWLockRelease(LogicalDecodingControlLock); + return; + } + + /* + * Set logical info WAL logging in shmem. All process starts after this + * point will include the information required by logical decoding to WAL + * records. + */ + LogicalDecodingCtl->xlog_logical_info = true; + + LWLockRelease(LogicalDecodingControlLock); + + /* + * Tell all running processes to reflect the xlog_logical_info update, and + * wait. This ensures that all running processes have enabled logical + * information WAL logging. + */ + WaitForProcSignalBarrier( + EmitProcSignalBarrier(PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO)); + + INJECTION_POINT("logical-decoding-activation", NULL); + + in_recovery = RecoveryInProgress(); + + /* + * There could be some transactions that might have started with the old + * status, but we don't need to wait for these transactions to complete as + * long as they have valid XIDs. These transactions will appear in the + * xl_running_xacts record and therefore the snapshot builder will not try + * to decode the transaction during the logical decoding initialization. + * + * There is a theoretical case where a transaction decides whether to + * include logical-info to WAL records before getting an XID. In this + * case, the transaction won't appear in xl_running_xacts. + * + * For operations that do not require an XID assignment, the process + * starts including logical-info immediately upon receiving the signal + * (barrier). If such an operation checks the effective_wal_level multiple + * times within a single execution, the resulting WAL records might be + * inconsistent (i.e., logical-info is included in some records but not in + * others). However, this is harmless because logical decoding generally + * ignores WAL records that are not associated with an assigned XID. + * + * One might think we need to wait for all running transactions, including + * those without XIDs and read-only transactions, to finish before + * enabling logical decoding. However, such a requirement would force the + * slot creation to wait for a potentially very long time due to + * long-running read queries, which is practically unacceptable. + */ + + START_CRIT_SECTION(); + + /* + * We enable logical decoding first, followed by writing the WAL record. + * This sequence ensures logical decoding becomes available on the primary + * first. + */ + LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE); + + LogicalDecodingCtl->logical_decoding_enabled = true; + + if (!in_recovery) + write_logical_decoding_status_update_record(true); + + LogicalDecodingCtl->pending_disable = false; + + LWLockRelease(LogicalDecodingControlLock); + + END_CRIT_SECTION(); + + if (!in_recovery) + ereport(LOG, + errmsg("logical decoding is enabled upon creating a new logical replication slot")); +} + +/* + * Initiate a request for disabling logical decoding. + * + * Note that this function does not verify whether logical slots exist. The + * checkpointer will verify if logical decoding should actually be disabled. + */ +void +RequestDisableLogicalDecoding(void) +{ + if (wal_level != WAL_LEVEL_REPLICA) + return; + + /* + * It's possible that we might not actually need to disable logical + * decoding if someone creates a new logical slot concurrently. We set the + * flag anyway and the checkpointer will check it and disable logical + * decoding if necessary. + */ + LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE); + LogicalDecodingCtl->pending_disable = true; + LWLockRelease(LogicalDecodingControlLock); + + WakeupCheckpointer(); + + elog(DEBUG1, "requested disabling logical decoding"); +} + +/* + * Disable logical decoding if necessary. + * + * This function disables logical decoding upon a request initiated by + * RequestDisableLogicalDecoding(). Otherwise, it performs no action. + */ +void +DisableLogicalDecodingIfNecessary(void) +{ + bool pending_disable; + + if (wal_level != WAL_LEVEL_REPLICA) + return; + + /* + * Sanity check as we cannot disable logical decoding while holding a + * logical slot. + */ + Assert(!MyReplicationSlot); + + if (RecoveryInProgress()) + return; + + LWLockAcquire(LogicalDecodingControlLock, LW_SHARED); + pending_disable = LogicalDecodingCtl->pending_disable; + LWLockRelease(LogicalDecodingControlLock); + + /* Quick return if no pending disable request */ + if (!pending_disable) + return; + + DisableLogicalDecoding(); +} + +/* + * A workhorse function to disable logical decoding. + */ +void +DisableLogicalDecoding(void) +{ + bool in_recovery = RecoveryInProgress(); + + LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE); + + /* + * Check if we can disable logical decoding. + * + * Skip CheckLogicalSlotExists() check during recovery because the + * existing slots will be invalidated after disabling logical decoding. + */ + if (!LogicalDecodingCtl->logical_decoding_enabled || + (!in_recovery && CheckLogicalSlotExists())) + { + LogicalDecodingCtl->pending_disable = false; + LWLockRelease(LogicalDecodingControlLock); + return; + } + + START_CRIT_SECTION(); + + /* + * We need to disable logical decoding first and then disable logical + * information WAL logging in order to ensure that no logical decoding + * processes WAL records with insufficient information. + */ + LogicalDecodingCtl->logical_decoding_enabled = false; + + /* Write the WAL to disable logical decoding on standbys too */ + if (!in_recovery) + write_logical_decoding_status_update_record(false); + + /* Now disable logical information WAL logging */ + LogicalDecodingCtl->xlog_logical_info = false; + LogicalDecodingCtl->pending_disable = false; + + END_CRIT_SECTION(); + + if (!in_recovery) + ereport(LOG, + errmsg("logical decoding is disabled because there are no valid logical replication slots")); + + LWLockRelease(LogicalDecodingControlLock); + + /* + * Tell all running processes to reflect the xlog_logical_info update. + * Unlike when enabling logical decoding, we don't need to wait for all + * processes to complete it in this case. We already disabled logical + * decoding and it's always safe to write logical information to WAL + * records, even when not strictly required. Therefore, we don't need to + * wait for all running transactions to finish either. + */ + EmitProcSignalBarrier(PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO); +} + +/* + * Updates the logical decoding status at end of recovery, and ensures that + * all running processes have the updated XLogLogicalInfo status. This + * function must be called before accepting writes. + */ +void +UpdateLogicalDecodingStatusEndOfRecovery(void) +{ + bool new_status = false; + + Assert(RecoveryInProgress()); + + /* + * With 'minimal' WAL level, there are no logical replication slots during + * recovery. Logical decoding is always disabled, so there is no need to + * synchronize XLogLogicalInfo. + */ + if (wal_level == WAL_LEVEL_MINIMAL) + { + Assert(!IsXLogLogicalInfoEnabled() && !IsLogicalDecodingEnabled()); + return; + } + + LWLockAcquire(LogicalDecodingControlLock, LW_EXCLUSIVE); + + if (wal_level == WAL_LEVEL_LOGICAL || CheckLogicalSlotExists()) + new_status = true; + + /* + * When recovery ends, we need to either enable or disable logical + * decoding based on the wal_level setting and the presence of logical + * slots. We need to note that concurrent slot creation and deletion could + * happen but WAL writes are still not permitted until recovery fully + * completes. Here's how we handle concurrent toggling of logical + * decoding: + * + * For 'enable' case, if there's a concurrent disable request before + * recovery fully completes, the checkpointer will handle it after + * recovery is done. This means there might be a brief period after + * recovery where logical decoding remains enabled even with no logical + * replication slots present. This temporary state is not new - it can + * already occur due to the checkpointer's asynchronous deactivation + * process. + * + * For 'disable' case, backend cannot create logical replication slots + * during recovery (see checks in CheckLogicalDecodingRequirements()), + * which prevents a race condition between disabling logical decoding and + * concurrent slot creation. + */ + if (new_status != LogicalDecodingCtl->logical_decoding_enabled) + { + /* + * Update both the logical decoding status and logical WAL logging + * status. Unlike toggling these status during non-recovery, we don't + * need to worry about the operation order as WAL writes are still not + * permitted. + */ + LogicalDecodingCtl->xlog_logical_info = new_status; + LogicalDecodingCtl->logical_decoding_enabled = new_status; + + elog(DEBUG1, + "update logical decoding status to %d at the end of recovery", + new_status); + + /* + * Now that we updated the logical decoding status, clear the pending + * disable flag. It's possible that a concurrent process drops the + * last logical slot and initiates the pending disable again. The + * checkpointer process will check it. + */ + LogicalDecodingCtl->pending_disable = false; + + LWLockRelease(LogicalDecodingControlLock); + + write_logical_decoding_status_update_record(new_status); + } + else + LWLockRelease(LogicalDecodingControlLock); + + /* + * Ensure all running processes have the updated status. We don't need to + * wait for running transactions to finish as we don't accept any writes + * yet. On the other hand, we need to wait for synchronizing + * XLogLogicalInfo even if we've not updated the status above as the + * status have been turned on and off during recovery, having running + * processes have different status on their local caches. + */ + if (IsUnderPostmaster) + WaitForProcSignalBarrier( + EmitProcSignalBarrier(PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO)); + + INJECTION_POINT("startup-logical-decoding-status-change-end-of-recovery", NULL); +} diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c index ca53caac2f2f5..512013b0ef0d1 100644 --- a/src/backend/replication/logical/logicalfuncs.c +++ b/src/backend/replication/logical/logicalfuncs.c @@ -6,7 +6,7 @@ * logical replication slots via SQL. * * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/logicalfuncs.c @@ -107,7 +107,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin XLogRecPtr end_of_wal; XLogRecPtr wait_for_wal_lsn; LogicalDecodingContext *ctx; - ResourceOwner old_resowner = CurrentResourceOwner; + ResourceOwner old_resowner PG_USED_FOR_ASSERTS_ONLY = CurrentResourceOwner; ArrayType *arr; Size ndim; List *options = NIL; @@ -115,7 +115,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); if (PG_ARGISNULL(0)) ereport(ERROR, @@ -129,7 +129,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin upto_lsn = PG_GETARG_LSN(1); if (PG_ARGISNULL(2)) - upto_nchanges = InvalidXLogRecPtr; + upto_nchanges = 0; else upto_nchanges = PG_GETARG_INT32(2); @@ -140,7 +140,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin arr = PG_GETARG_ARRAYTYPE_P(3); /* state to write output to */ - p = palloc0(sizeof(DecodingOutputState)); + p = palloc0_object(DecodingOutputState); p->binary_output = binary; @@ -229,7 +229,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin * Wait for specified streaming replication standby servers (if any) * to confirm receipt of WAL up to wait_for_wal_lsn. */ - if (XLogRecPtrIsInvalid(upto_lsn)) + if (!XLogRecPtrIsValid(upto_lsn)) wait_for_wal_lsn = end_of_wal; else wait_for_wal_lsn = Min(upto_lsn, end_of_wal); @@ -263,10 +263,20 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin * store the description into our tuplestore. */ if (record != NULL) + { LogicalDecodingProcessRecord(ctx, ctx->reader); + /* + * We used to have bugs where logical decoding would fail to + * preserve the resource owner. Verify that that doesn't + * happen anymore. XXX this could be removed once it's been + * battle-tested. + */ + Assert(CurrentResourceOwner == old_resowner); + } + /* check limits */ - if (upto_lsn != InvalidXLogRecPtr && + if (XLogRecPtrIsValid(upto_lsn) && upto_lsn <= ctx->reader->EndRecPtr) break; if (upto_nchanges != 0 && @@ -275,18 +285,11 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin CHECK_FOR_INTERRUPTS(); } - /* - * Logical decoding could have clobbered CurrentResourceOwner during - * transaction management, so restore the executor's value. (This is - * a kluge, but it's not worth cleaning up right now.) - */ - CurrentResourceOwner = old_resowner; - /* * Next time, start where we left off. (Hunting things, the family * business..) */ - if (ctx->reader->EndRecPtr != InvalidXLogRecPtr && confirm) + if (XLogRecPtrIsValid(ctx->reader->EndRecPtr) && confirm) { LogicalConfirmReceivedLocation(ctx->reader->EndRecPtr); diff --git a/src/backend/replication/logical/meson.build b/src/backend/replication/logical/meson.build index 6f19614c79d8f..47a68b660d28f 100644 --- a/src/backend/replication/logical/meson.build +++ b/src/backend/replication/logical/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'applyparallelworker.c', @@ -6,14 +6,17 @@ backend_sources += files( 'decode.c', 'launcher.c', 'logical.c', + 'logicalctl.c', 'logicalfuncs.c', 'message.c', 'origin.c', 'proto.c', 'relation.c', 'reorderbuffer.c', + 'sequencesync.c', 'slotsync.c', 'snapbuild.c', + 'syncutils.c', 'tablesync.c', 'worker.c', ) diff --git a/src/backend/replication/logical/message.c b/src/backend/replication/logical/message.c index ebc8454bad926..06825d66e7f64 100644 --- a/src/backend/replication/logical/message.c +++ b/src/backend/replication/logical/message.c @@ -3,7 +3,7 @@ * message.c * Generic logical messages. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/message.c diff --git a/src/backend/replication/logical/origin.c b/src/backend/replication/logical/origin.c index a17bacf88e7f3..c9dfb094c2b16 100644 --- a/src/backend/replication/logical/origin.c +++ b/src/backend/replication/logical/origin.c @@ -3,7 +3,7 @@ * origin.c * Logical replication progress tracking support. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/origin.c @@ -15,7 +15,7 @@ * * A facility to efficiently store and persist replication progress in an * efficient and durable manner. * - * Replication origin consist out of a descriptive, user defined, external + * Replication origin consists of a descriptive, user defined, external * name and a short, thus space efficient, internal 2 byte one. This split * exists because replication origin have to be stored in WAL and shared * memory and long descriptors would be inefficient. For now only use 2 bytes @@ -88,6 +88,7 @@ #include "storage/fd.h" #include "storage/ipc.h" #include "storage/lmgr.h" +#include "storage/subsystems.h" #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/guc.h" @@ -95,6 +96,7 @@ #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/wait_event.h" /* paths for replication origin checkpoint files */ #define PG_REPLORIGIN_CHECKPOINT_FILENAME PG_LOGICAL_DIR "/replorigin_checkpoint" @@ -111,7 +113,7 @@ typedef struct ReplicationState /* * Local identifier for the remote node. */ - RepOriginId roident; + ReplOriginId roident; /* * Location of the latest commit from the remote side. @@ -130,6 +132,9 @@ typedef struct ReplicationState */ int acquired_by; + /* Count of processes that are currently using this origin. */ + int refcount; + /* * Condition variable that's signaled when acquired_by changes. */ @@ -146,7 +151,7 @@ typedef struct ReplicationState */ typedef struct ReplicationStateOnDisk { - RepOriginId roident; + ReplOriginId roident; XLogRecPtr remote_lsn; } ReplicationStateOnDisk; @@ -159,10 +164,12 @@ typedef struct ReplicationStateCtl ReplicationState states[FLEXIBLE_ARRAY_MEMBER]; } ReplicationStateCtl; -/* external variables */ -RepOriginId replorigin_session_origin = InvalidRepOriginId; /* assumed identity */ -XLogRecPtr replorigin_session_origin_lsn = InvalidXLogRecPtr; -TimestampTz replorigin_session_origin_timestamp = 0; +/* Global variable for per-transaction replication origin state */ +ReplOriginXactState replorigin_xact_state = { + .origin = InvalidReplOriginId, /* assumed identity */ + .origin_lsn = InvalidXLogRecPtr, + .origin_timestamp = 0 +}; /* * Base address into a shared memory array of replication states of size @@ -170,6 +177,16 @@ TimestampTz replorigin_session_origin_timestamp = 0; */ static ReplicationState *replication_states; +static void ReplicationOriginShmemRequest(void *arg); +static void ReplicationOriginShmemInit(void *arg); +static void ReplicationOriginShmemAttach(void *arg); + +const ShmemCallbacks ReplicationOriginShmemCallbacks = { + .request_fn = ReplicationOriginShmemRequest, + .init_fn = ReplicationOriginShmemInit, + .attach_fn = ReplicationOriginShmemAttach, +}; + /* * Actual shared memory block (replication_states[] is now part of this). */ @@ -222,7 +239,7 @@ IsReservedOriginName(const char *name) * * Returns InvalidOid if the node isn't known yet and missing_ok is true. */ -RepOriginId +ReplOriginId replorigin_by_name(const char *roname, bool missing_ok) { Form_pg_replication_origin ident; @@ -253,7 +270,7 @@ replorigin_by_name(const char *roname, bool missing_ok) * * Needs to be called in a transaction. */ -RepOriginId +ReplOriginId replorigin_create(const char *roname) { Oid roident; @@ -366,7 +383,7 @@ replorigin_create(const char *roname) * Helper function to drop a replication origin. */ static void -replorigin_state_clear(RepOriginId roident, bool nowait) +replorigin_state_clear(ReplOriginId roident, bool nowait) { int i; @@ -383,16 +400,19 @@ replorigin_state_clear(RepOriginId roident, bool nowait) if (state->roident == roident) { /* found our slot, is it busy? */ - if (state->acquired_by != 0) + if (state->refcount > 0) { ConditionVariable *cv; if (nowait) ereport(ERROR, (errcode(ERRCODE_OBJECT_IN_USE), - errmsg("could not drop replication origin with ID %d, in use by PID %d", - state->roident, - state->acquired_by))); + (state->acquired_by != 0) + ? errmsg("could not drop replication origin with ID %d, in use by PID %d", + state->roident, + state->acquired_by) + : errmsg("could not drop replication origin with ID %d, in use by another process", + state->roident))); /* * We must wait and then retry. Since we don't know which CV @@ -420,7 +440,7 @@ replorigin_state_clear(RepOriginId roident, bool nowait) } /* then clear the in-memory slot */ - state->roident = InvalidRepOriginId; + state->roident = InvalidReplOriginId; state->remote_lsn = InvalidXLogRecPtr; state->local_lsn = InvalidXLogRecPtr; break; @@ -438,7 +458,7 @@ replorigin_state_clear(RepOriginId roident, bool nowait) void replorigin_drop_by_name(const char *name, bool missing_ok, bool nowait) { - RepOriginId roident; + ReplOriginId roident; Relation rel; HeapTuple tuple; @@ -490,13 +510,13 @@ replorigin_drop_by_name(const char *name, bool missing_ok, bool nowait) * Returns true if the origin is known, false otherwise. */ bool -replorigin_by_oid(RepOriginId roident, bool missing_ok, char **roname) +replorigin_by_oid(ReplOriginId roident, bool missing_ok, char **roname) { HeapTuple tuple; Form_pg_replication_origin ric; Assert(OidIsValid((Oid) roident)); - Assert(roident != InvalidRepOriginId); + Assert(roident != InvalidReplOriginId); Assert(roident != DoNotReplicateId); tuple = SearchSysCache1(REPLORIGIDENT, @@ -530,50 +550,48 @@ replorigin_by_oid(RepOriginId roident, bool missing_ok, char **roname) * --------------------------------------------------------------------------- */ -Size -ReplicationOriginShmemSize(void) +static void +ReplicationOriginShmemRequest(void *arg) { Size size = 0; if (max_active_replication_origins == 0) - return size; + return; size = add_size(size, offsetof(ReplicationStateCtl, states)); - size = add_size(size, mul_size(max_active_replication_origins, sizeof(ReplicationState))); - return size; + ShmemRequestStruct(.name = "ReplicationOriginState", + .size = size, + .ptr = (void **) &replication_states_ctl, + ); } -void -ReplicationOriginShmemInit(void) +static void +ReplicationOriginShmemInit(void *arg) { - bool found; - if (max_active_replication_origins == 0) return; - replication_states_ctl = (ReplicationStateCtl *) - ShmemInitStruct("ReplicationOriginState", - ReplicationOriginShmemSize(), - &found); replication_states = replication_states_ctl->states; - if (!found) - { - int i; + replication_states_ctl->tranche_id = LWTRANCHE_REPLICATION_ORIGIN_STATE; - MemSet(replication_states_ctl, 0, ReplicationOriginShmemSize()); + for (int i = 0; i < max_active_replication_origins; i++) + { + LWLockInitialize(&replication_states[i].lock, + replication_states_ctl->tranche_id); + ConditionVariableInit(&replication_states[i].origin_cv); + } +} - replication_states_ctl->tranche_id = LWTRANCHE_REPLICATION_ORIGIN_STATE; +static void +ReplicationOriginShmemAttach(void *arg) +{ + if (max_active_replication_origins == 0) + return; - for (i = 0; i < max_active_replication_origins; i++) - { - LWLockInitialize(&replication_states[i].lock, - replication_states_ctl->tranche_id); - ConditionVariableInit(&replication_states[i].origin_cv); - } - } + replication_states = replication_states_ctl->states; } /* --------------------------------------------------------------------------- @@ -650,7 +668,7 @@ CheckPointReplicationOrigin(void) ReplicationState *curstate = &replication_states[i]; XLogRecPtr local_lsn; - if (curstate->roident == InvalidRepOriginId) + if (curstate->roident == InvalidReplOriginId) continue; /* zero, to avoid uninitialized padding bytes */ @@ -789,14 +807,6 @@ StartupReplicationOrigin(void) readBytes = read(fd, &disk_state, sizeof(disk_state)); - /* no further data */ - if (readBytes == sizeof(crc)) - { - /* not pretty, but simple ... */ - file_crc = *(pg_crc32c *) &disk_state; - break; - } - if (readBytes < 0) { ereport(PANIC, @@ -805,6 +815,13 @@ StartupReplicationOrigin(void) path))); } + /* no further data */ + if (readBytes == sizeof(crc)) + { + memcpy(&file_crc, &disk_state, sizeof(file_crc)); + break; + } + if (readBytes != sizeof(disk_state)) { ereport(PANIC, @@ -826,9 +843,9 @@ StartupReplicationOrigin(void) last_state++; ereport(LOG, - (errmsg("recovered replication state of node %d to %X/%X", - disk_state.roident, - LSN_FORMAT_ARGS(disk_state.remote_lsn)))); + errmsg("recovered replication state of node %d to %X/%08X", + disk_state.roident, + LSN_FORMAT_ARGS(disk_state.remote_lsn))); } /* now check checksum */ @@ -879,7 +896,7 @@ replorigin_redo(XLogReaderState *record) if (state->roident == xlrec->node_id) { /* reset entry */ - state->roident = InvalidRepOriginId; + state->roident = InvalidReplOriginId; state->remote_lsn = InvalidXLogRecPtr; state->local_lsn = InvalidXLogRecPtr; break; @@ -897,7 +914,7 @@ replorigin_redo(XLogReaderState *record) * Tell the replication origin progress machinery that a commit from 'node' * that originated at the LSN remote_commit on the remote node was replayed * successfully and that we don't need to do so again. In combination with - * setting up replorigin_session_origin_lsn and replorigin_session_origin + * setting up replorigin_xact_state {.origin_lsn, .origin_timestamp} * that ensures we won't lose knowledge about that after a crash if the * transaction had a persistent effect (think of asynchronous commits). * @@ -908,7 +925,7 @@ replorigin_redo(XLogReaderState *record) * unless running in recovery. */ void -replorigin_advance(RepOriginId node, +replorigin_advance(ReplOriginId node, XLogRecPtr remote_commit, XLogRecPtr local_commit, bool go_backward, bool wal_log) { @@ -916,7 +933,7 @@ replorigin_advance(RepOriginId node, ReplicationState *replication_state = NULL; ReplicationState *free_state = NULL; - Assert(node != InvalidRepOriginId); + Assert(node != InvalidReplOriginId); /* we don't track DoNotReplicateId */ if (node == DoNotReplicateId) @@ -941,7 +958,7 @@ replorigin_advance(RepOriginId node, ReplicationState *curstate = &replication_states[i]; /* remember where to insert if necessary */ - if (curstate->roident == InvalidRepOriginId && + if (curstate->roident == InvalidReplOriginId && free_state == NULL) { free_state = curstate; @@ -960,13 +977,16 @@ replorigin_advance(RepOriginId node, LWLockAcquire(&replication_state->lock, LW_EXCLUSIVE); /* Make sure it's not used by somebody else */ - if (replication_state->acquired_by != 0) + if (replication_state->refcount > 0) { ereport(ERROR, (errcode(ERRCODE_OBJECT_IN_USE), - errmsg("replication origin with ID %d is already active for PID %d", - replication_state->roident, - replication_state->acquired_by))); + (replication_state->acquired_by != 0) + ? errmsg("replication origin with ID %d is already active for PID %d", + replication_state->roident, + replication_state->acquired_by) + : errmsg("replication origin with ID %d is already active in another process", + replication_state->roident))); } break; @@ -984,12 +1004,12 @@ replorigin_advance(RepOriginId node, /* initialize new slot */ LWLockAcquire(&free_state->lock, LW_EXCLUSIVE); replication_state = free_state; - Assert(replication_state->remote_lsn == InvalidXLogRecPtr); - Assert(replication_state->local_lsn == InvalidXLogRecPtr); + Assert(!XLogRecPtrIsValid(replication_state->remote_lsn)); + Assert(!XLogRecPtrIsValid(replication_state->local_lsn)); replication_state->roident = node; } - Assert(replication_state->roident != InvalidRepOriginId); + Assert(replication_state->roident != InvalidReplOriginId); /* * If somebody "forcefully" sets this slot, WAL log it, so it's durable @@ -1020,7 +1040,7 @@ replorigin_advance(RepOriginId node, */ if (go_backward || replication_state->remote_lsn < remote_commit) replication_state->remote_lsn = remote_commit; - if (local_commit != InvalidXLogRecPtr && + if (XLogRecPtrIsValid(local_commit) && (go_backward || replication_state->local_lsn < local_commit)) replication_state->local_lsn = local_commit; LWLockRelease(&replication_state->lock); @@ -1034,7 +1054,7 @@ replorigin_advance(RepOriginId node, XLogRecPtr -replorigin_get_progress(RepOriginId node, bool flush) +replorigin_get_progress(ReplOriginId node, bool flush) { int i; XLogRecPtr local_lsn = InvalidXLogRecPtr; @@ -1064,38 +1084,54 @@ replorigin_get_progress(RepOriginId node, bool flush) LWLockRelease(ReplicationOriginLock); - if (flush && local_lsn != InvalidXLogRecPtr) + if (flush && XLogRecPtrIsValid(local_lsn)) XLogFlush(local_lsn); return remote_lsn; } -/* - * Tear down a (possibly) configured session replication origin during process - * exit. - */ +/* Helper function to reset the session replication origin */ static void -ReplicationOriginExitCleanup(int code, Datum arg) +replorigin_session_reset_internal(void) { - ConditionVariable *cv = NULL; + ConditionVariable *cv; - if (session_replication_state == NULL) - return; + Assert(session_replication_state != NULL); LWLockAcquire(ReplicationOriginLock, LW_EXCLUSIVE); - if (session_replication_state->acquired_by == MyProcPid) - { - cv = &session_replication_state->origin_cv; + /* The origin must be held by at least one process at this point. */ + Assert(session_replication_state->refcount > 0); + /* + * Reset the PID only if the current session is the first to set up this + * origin. This avoids clearing the first process's PID when any other + * session releases the origin. + */ + if (session_replication_state->acquired_by == MyProcPid) session_replication_state->acquired_by = 0; - session_replication_state = NULL; - } + + session_replication_state->refcount--; + + cv = &session_replication_state->origin_cv; + session_replication_state = NULL; LWLockRelease(ReplicationOriginLock); - if (cv) - ConditionVariableBroadcast(cv); + ConditionVariableBroadcast(cv); +} + +/* + * Tear down a (possibly) configured session replication origin during process + * exit. + */ +static void +ReplicationOriginExitCleanup(int code, Datum arg) +{ + if (session_replication_state == NULL) + return; + + replorigin_session_reset_internal(); } /* @@ -1117,7 +1153,7 @@ ReplicationOriginExitCleanup(int code, Datum arg) * acquired_by = PID of the first process. */ void -replorigin_session_setup(RepOriginId node, int acquired_by) +replorigin_session_setup(ReplOriginId node, int acquired_by) { static bool registered_cleanup; int i; @@ -1148,7 +1184,7 @@ replorigin_session_setup(RepOriginId node, int acquired_by) ReplicationState *curstate = &replication_states[i]; /* remember where to insert if necessary */ - if (curstate->roident == InvalidRepOriginId && + if (curstate->roident == InvalidReplOriginId && free_slot == -1) { free_slot = i; @@ -1159,12 +1195,47 @@ replorigin_session_setup(RepOriginId node, int acquired_by) if (curstate->roident != node) continue; - else if (curstate->acquired_by != 0 && acquired_by == 0) + if (acquired_by == 0) { - ereport(ERROR, - (errcode(ERRCODE_OBJECT_IN_USE), - errmsg("replication origin with ID %d is already active for PID %d", - curstate->roident, curstate->acquired_by))); + /* With acquired_by == 0, we need the origin to be free */ + if (curstate->acquired_by != 0) + { + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("replication origin with ID %d is already active for PID %d", + curstate->roident, curstate->acquired_by))); + } + else if (curstate->refcount > 0) + { + /* + * The origin is in use, but PID is not recorded. This can + * happen if the process that originally acquired the origin + * exited without releasing it. To ensure correctness, other + * processes cannot acquire the origin until all processes + * currently using it have released it. + */ + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("replication origin with ID %d is already active in another process", + curstate->roident))); + } + } + else + { + /* + * With acquired_by != 0, we need the origin to be active by the + * given PID + */ + if (curstate->acquired_by != acquired_by) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_IN_USE), + errmsg("replication origin with ID %d is not active for PID %d", + curstate->roident, acquired_by))); + + /* + * Here, it is okay to have refcount > 0 as more than one process + * can safely re-use the origin. + */ } /* ok, found slot */ @@ -1172,30 +1243,47 @@ replorigin_session_setup(RepOriginId node, int acquired_by) break; } - - if (session_replication_state == NULL && free_slot == -1) - ereport(ERROR, - (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), - errmsg("could not find free replication state slot for replication origin with ID %d", - node), - errhint("Increase \"max_active_replication_origins\" and try again."))); - else if (session_replication_state == NULL) + if (session_replication_state == NULL) { + if (acquired_by != 0) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot use PID %d for inactive replication origin with ID %d", + acquired_by, node))); + /* initialize new slot */ + if (free_slot == -1) + ereport(ERROR, + (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), + errmsg("could not find free replication state slot for replication origin with ID %d", + node), + errhint("Increase \"max_active_replication_origins\" and try again."))); + session_replication_state = &replication_states[free_slot]; - Assert(session_replication_state->remote_lsn == InvalidXLogRecPtr); - Assert(session_replication_state->local_lsn == InvalidXLogRecPtr); + Assert(!XLogRecPtrIsValid(session_replication_state->remote_lsn)); + Assert(!XLogRecPtrIsValid(session_replication_state->local_lsn)); session_replication_state->roident = node; } - Assert(session_replication_state->roident != InvalidRepOriginId); + Assert(session_replication_state->roident != InvalidReplOriginId); if (acquired_by == 0) + { session_replication_state->acquired_by = MyProcPid; - else if (session_replication_state->acquired_by != acquired_by) - elog(ERROR, "could not find replication state slot for replication origin with OID %u which was acquired by %d", - node, acquired_by); + Assert(session_replication_state->refcount == 0); + } + else + { + /* + * Sanity check: the origin must already be acquired by the process + * passed as input, and at least one process must be using it. + */ + Assert(session_replication_state->acquired_by == acquired_by); + Assert(session_replication_state->refcount > 0); + } + + session_replication_state->refcount++; LWLockRelease(ReplicationOriginLock); @@ -1212,8 +1300,6 @@ replorigin_session_setup(RepOriginId node, int acquired_by) void replorigin_session_reset(void) { - ConditionVariable *cv; - Assert(max_active_replication_origins != 0); if (session_replication_state == NULL) @@ -1221,15 +1307,22 @@ replorigin_session_reset(void) (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("no replication origin is configured"))); - LWLockAcquire(ReplicationOriginLock, LW_EXCLUSIVE); - - session_replication_state->acquired_by = 0; - cv = &session_replication_state->origin_cv; - session_replication_state = NULL; - - LWLockRelease(ReplicationOriginLock); + /* + * Restrict explicit resetting of the replication origin if it was first + * acquired by this process and others are still using it. While the + * system handles this safely (as happens if the first session exits + * without calling reset), it is best to avoid doing so. + */ + if (session_replication_state->acquired_by == MyProcPid && + session_replication_state->refcount > 1) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot reset replication origin with ID %d because it is still in use by other processes", + session_replication_state->roident), + errdetail("This session is the first process for this replication origin, and other processes are currently sharing it."), + errhint("Reset the replication origin in all other processes before retrying."))); - ConditionVariableBroadcast(cv); + replorigin_session_reset_internal(); } /* @@ -1242,7 +1335,7 @@ void replorigin_session_advance(XLogRecPtr remote_commit, XLogRecPtr local_commit) { Assert(session_replication_state != NULL); - Assert(session_replication_state->roident != InvalidRepOriginId); + Assert(session_replication_state->roident != InvalidReplOriginId); LWLockAcquire(&session_replication_state->lock, LW_EXCLUSIVE); if (session_replication_state->local_lsn < local_commit) @@ -1269,12 +1362,25 @@ replorigin_session_get_progress(bool flush) local_lsn = session_replication_state->local_lsn; LWLockRelease(&session_replication_state->lock); - if (flush && local_lsn != InvalidXLogRecPtr) + if (flush && XLogRecPtrIsValid(local_lsn)) XLogFlush(local_lsn); return remote_lsn; } +/* + * Clear the per-transaction replication origin state. + * + * replorigin_xact_state.origin is also cleared if clear_origin is set. + */ +void +replorigin_xact_clear(bool clear_origin) +{ + replorigin_xact_state.origin_lsn = InvalidXLogRecPtr; + replorigin_xact_state.origin_timestamp = 0; + if (clear_origin) + replorigin_xact_state.origin = InvalidReplOriginId; +} /* --------------------------------------------------------------------------- @@ -1292,7 +1398,7 @@ Datum pg_replication_origin_create(PG_FUNCTION_ARGS) { char *name; - RepOriginId roident; + ReplOriginId roident; replorigin_check_prerequisites(false, false); @@ -1352,7 +1458,7 @@ Datum pg_replication_origin_oid(PG_FUNCTION_ARGS) { char *name; - RepOriginId roident; + ReplOriginId roident; replorigin_check_prerequisites(false, false); @@ -1373,15 +1479,17 @@ Datum pg_replication_origin_session_setup(PG_FUNCTION_ARGS) { char *name; - RepOriginId origin; + ReplOriginId origin; + int pid; replorigin_check_prerequisites(true, false); name = text_to_cstring((text *) DatumGetPointer(PG_GETARG_DATUM(0))); origin = replorigin_by_name(name, false); - replorigin_session_setup(origin, 0); + pid = PG_GETARG_INT32(1); + replorigin_session_setup(origin, pid); - replorigin_session_origin = origin; + replorigin_xact_state.origin = origin; pfree(name); @@ -1398,9 +1506,7 @@ pg_replication_origin_session_reset(PG_FUNCTION_ARGS) replorigin_session_reset(); - replorigin_session_origin = InvalidRepOriginId; - replorigin_session_origin_lsn = InvalidXLogRecPtr; - replorigin_session_origin_timestamp = 0; + replorigin_xact_clear(true); PG_RETURN_VOID(); } @@ -1413,7 +1519,7 @@ pg_replication_origin_session_is_setup(PG_FUNCTION_ARGS) { replorigin_check_prerequisites(false, false); - PG_RETURN_BOOL(replorigin_session_origin != InvalidRepOriginId); + PG_RETURN_BOOL(replorigin_xact_state.origin != InvalidReplOriginId); } @@ -1439,7 +1545,7 @@ pg_replication_origin_session_progress(PG_FUNCTION_ARGS) remote_lsn = replorigin_session_get_progress(flush); - if (remote_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(remote_lsn)) PG_RETURN_NULL(); PG_RETURN_LSN(remote_lsn); @@ -1457,8 +1563,8 @@ pg_replication_origin_xact_setup(PG_FUNCTION_ARGS) (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("no replication origin is configured"))); - replorigin_session_origin_lsn = location; - replorigin_session_origin_timestamp = PG_GETARG_TIMESTAMPTZ(1); + replorigin_xact_state.origin_lsn = location; + replorigin_xact_state.origin_timestamp = PG_GETARG_TIMESTAMPTZ(1); PG_RETURN_VOID(); } @@ -1468,8 +1574,8 @@ pg_replication_origin_xact_reset(PG_FUNCTION_ARGS) { replorigin_check_prerequisites(true, false); - replorigin_session_origin_lsn = InvalidXLogRecPtr; - replorigin_session_origin_timestamp = 0; + /* Do not clear the session origin */ + replorigin_xact_clear(false); PG_RETURN_VOID(); } @@ -1480,7 +1586,7 @@ pg_replication_origin_advance(PG_FUNCTION_ARGS) { text *name = PG_GETARG_TEXT_PP(0); XLogRecPtr remote_commit = PG_GETARG_LSN(1); - RepOriginId node; + ReplOriginId node; replorigin_check_prerequisites(true, false); @@ -1515,7 +1621,7 @@ pg_replication_origin_progress(PG_FUNCTION_ARGS) { char *name; bool flush; - RepOriginId roident; + ReplOriginId roident; XLogRecPtr remote_lsn = InvalidXLogRecPtr; replorigin_check_prerequisites(true, true); @@ -1528,7 +1634,7 @@ pg_replication_origin_progress(PG_FUNCTION_ARGS) remote_lsn = replorigin_get_progress(roident, flush); - if (remote_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(remote_lsn)) PG_RETURN_NULL(); PG_RETURN_LSN(remote_lsn); @@ -1565,7 +1671,7 @@ pg_show_replication_origin_status(PG_FUNCTION_ARGS) state = &replication_states[i]; /* unused slot, nothing to display */ - if (state->roident == InvalidRepOriginId) + if (state->roident == InvalidReplOriginId) continue; memset(values, 0, sizeof(values)); diff --git a/src/backend/replication/logical/proto.c b/src/backend/replication/logical/proto.c index 1a352b542dc56..86ad97cd937b3 100644 --- a/src/backend/replication/logical/proto.c +++ b/src/backend/replication/logical/proto.c @@ -3,7 +3,7 @@ * proto.c * logical replication protocol functions * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/proto.c @@ -52,7 +52,7 @@ logicalrep_write_begin(StringInfo out, ReorderBufferTXN *txn) /* fixed fields */ pq_sendint64(out, txn->final_lsn); - pq_sendint64(out, txn->xact_time.commit_time); + pq_sendint64(out, txn->commit_time); pq_sendint32(out, txn->xid); } @@ -64,7 +64,7 @@ logicalrep_read_begin(StringInfo in, LogicalRepBeginData *begin_data) { /* read fields */ begin_data->final_lsn = pq_getmsgint64(in); - if (begin_data->final_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(begin_data->final_lsn)) elog(ERROR, "final_lsn not set in begin message"); begin_data->committime = pq_getmsgint64(in); begin_data->xid = pq_getmsgint(in, 4); @@ -88,7 +88,7 @@ logicalrep_write_commit(StringInfo out, ReorderBufferTXN *txn, /* send fields */ pq_sendint64(out, commit_lsn); pq_sendint64(out, txn->end_lsn); - pq_sendint64(out, txn->xact_time.commit_time); + pq_sendint64(out, txn->commit_time); } /* @@ -120,7 +120,7 @@ logicalrep_write_begin_prepare(StringInfo out, ReorderBufferTXN *txn) /* fixed fields */ pq_sendint64(out, txn->final_lsn); pq_sendint64(out, txn->end_lsn); - pq_sendint64(out, txn->xact_time.prepare_time); + pq_sendint64(out, txn->prepare_time); pq_sendint32(out, txn->xid); /* send gid */ @@ -135,10 +135,10 @@ logicalrep_read_begin_prepare(StringInfo in, LogicalRepPreparedTxnData *begin_da { /* read fields */ begin_data->prepare_lsn = pq_getmsgint64(in); - if (begin_data->prepare_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(begin_data->prepare_lsn)) elog(ERROR, "prepare_lsn not set in begin prepare message"); begin_data->end_lsn = pq_getmsgint64(in); - if (begin_data->end_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(begin_data->end_lsn)) elog(ERROR, "end_lsn not set in begin prepare message"); begin_data->prepare_time = pq_getmsgint64(in); begin_data->xid = pq_getmsgint(in, 4); @@ -173,7 +173,7 @@ logicalrep_write_prepare_common(StringInfo out, LogicalRepMsgType type, /* send fields */ pq_sendint64(out, prepare_lsn); pq_sendint64(out, txn->end_lsn); - pq_sendint64(out, txn->xact_time.prepare_time); + pq_sendint64(out, txn->prepare_time); pq_sendint32(out, txn->xid); /* send gid */ @@ -207,10 +207,10 @@ logicalrep_read_prepare_common(StringInfo in, char *msgtype, /* read fields */ prepare_data->prepare_lsn = pq_getmsgint64(in); - if (prepare_data->prepare_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(prepare_data->prepare_lsn)) elog(ERROR, "prepare_lsn is not set in %s message", msgtype); prepare_data->end_lsn = pq_getmsgint64(in); - if (prepare_data->end_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(prepare_data->end_lsn)) elog(ERROR, "end_lsn is not set in %s message", msgtype); prepare_data->prepare_time = pq_getmsgint64(in); prepare_data->xid = pq_getmsgint(in, 4); @@ -253,7 +253,7 @@ logicalrep_write_commit_prepared(StringInfo out, ReorderBufferTXN *txn, /* send fields */ pq_sendint64(out, commit_lsn); pq_sendint64(out, txn->end_lsn); - pq_sendint64(out, txn->xact_time.commit_time); + pq_sendint64(out, txn->commit_time); pq_sendint32(out, txn->xid); /* send gid */ @@ -274,10 +274,10 @@ logicalrep_read_commit_prepared(StringInfo in, LogicalRepCommitPreparedTxnData * /* read fields */ prepare_data->commit_lsn = pq_getmsgint64(in); - if (prepare_data->commit_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(prepare_data->commit_lsn)) elog(ERROR, "commit_lsn is not set in commit prepared message"); prepare_data->end_lsn = pq_getmsgint64(in); - if (prepare_data->end_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(prepare_data->end_lsn)) elog(ERROR, "end_lsn is not set in commit prepared message"); prepare_data->commit_time = pq_getmsgint64(in); prepare_data->xid = pq_getmsgint(in, 4); @@ -311,7 +311,7 @@ logicalrep_write_rollback_prepared(StringInfo out, ReorderBufferTXN *txn, pq_sendint64(out, prepare_end_lsn); pq_sendint64(out, txn->end_lsn); pq_sendint64(out, prepare_time); - pq_sendint64(out, txn->xact_time.commit_time); + pq_sendint64(out, txn->commit_time); pq_sendint32(out, txn->xid); /* send gid */ @@ -333,10 +333,10 @@ logicalrep_read_rollback_prepared(StringInfo in, /* read fields */ rollback_data->prepare_end_lsn = pq_getmsgint64(in); - if (rollback_data->prepare_end_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(rollback_data->prepare_end_lsn)) elog(ERROR, "prepare_end_lsn is not set in rollback prepared message"); rollback_data->rollback_end_lsn = pq_getmsgint64(in); - if (rollback_data->rollback_end_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(rollback_data->rollback_end_lsn)) elog(ERROR, "rollback_end_lsn is not set in rollback prepared message"); rollback_data->prepare_time = pq_getmsgint64(in); rollback_data->rollback_time = pq_getmsgint64(in); @@ -697,7 +697,7 @@ logicalrep_write_rel(StringInfo out, TransactionId xid, Relation rel, LogicalRepRelation * logicalrep_read_rel(StringInfo in) { - LogicalRepRelation *rel = palloc(sizeof(LogicalRepRelation)); + LogicalRepRelation *rel = palloc_object(LogicalRepRelation); rel->remoteid = pq_getmsgint(in, 4); @@ -708,6 +708,9 @@ logicalrep_read_rel(StringInfo in) /* Read the replica identity. */ rel->replident = pq_getmsgbyte(in); + /* relkind is not sent */ + rel->relkind = 0; + /* Get attribute description */ logicalrep_read_attrs(in, rel); @@ -809,7 +812,7 @@ logicalrep_write_tuple(StringInfo out, Relation rel, TupleTableSlot *slot, continue; } - if (att->attlen == -1 && VARATT_IS_EXTERNAL_ONDISK(values[i])) + if (att->attlen == -1 && VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(values[i]))) { /* * Unchanged toasted datum. (Note that we don't promise to detect @@ -867,8 +870,8 @@ logicalrep_read_tuple(StringInfo in, LogicalRepTupleData *tuple) natts = pq_getmsgint(in, 2); /* Allocate space for per-column values; zero out unused StringInfoDatas */ - tuple->colvalues = (StringInfoData *) palloc0(natts * sizeof(StringInfoData)); - tuple->colstatus = (char *) palloc(natts * sizeof(char)); + tuple->colvalues = palloc0_array(StringInfoData, natts); + tuple->colstatus = palloc_array(char, natts); tuple->ncols = natts; /* Read the data */ @@ -991,8 +994,8 @@ logicalrep_read_attrs(StringInfo in, LogicalRepRelation *rel) Bitmapset *attkeys = NULL; natts = pq_getmsgint(in, 2); - attnames = palloc(natts * sizeof(char *)); - atttyps = palloc(natts * sizeof(Oid)); + attnames = palloc_array(char *, natts); + atttyps = palloc_array(Oid, natts); /* read the attributes */ for (i = 0; i < natts; i++) @@ -1119,7 +1122,7 @@ logicalrep_write_stream_commit(StringInfo out, ReorderBufferTXN *txn, /* send fields */ pq_sendint64(out, commit_lsn); pq_sendint64(out, txn->end_lsn); - pq_sendint64(out, txn->xact_time.commit_time); + pq_sendint64(out, txn->commit_time); } /* diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c index f59046ad620da..0b1d80b5b0fd2 100644 --- a/src/backend/replication/logical/relation.c +++ b/src/backend/replication/logical/relation.c @@ -2,7 +2,7 @@ * relation.c * PostgreSQL logical replication relation mapping cache * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/relation.c @@ -29,6 +29,7 @@ #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/syscache.h" +#include "utils/typcache.h" static MemoryContext LogicalRepRelMapContext = NULL; @@ -188,14 +189,25 @@ logicalrep_relmap_update(LogicalRepRelation *remoterel) entry->remoterel.nspname = pstrdup(remoterel->nspname); entry->remoterel.relname = pstrdup(remoterel->relname); entry->remoterel.natts = remoterel->natts; - entry->remoterel.attnames = palloc(remoterel->natts * sizeof(char *)); - entry->remoterel.atttyps = palloc(remoterel->natts * sizeof(Oid)); + entry->remoterel.attnames = palloc_array(char *, remoterel->natts); + entry->remoterel.atttyps = palloc_array(Oid, remoterel->natts); for (i = 0; i < remoterel->natts; i++) { entry->remoterel.attnames[i] = pstrdup(remoterel->attnames[i]); entry->remoterel.atttyps[i] = remoterel->atttyps[i]; } entry->remoterel.replident = remoterel->replident; + + /* + * XXX The walsender currently does not transmit the relkind of the remote + * relation when replicating changes. Since we support replicating only + * table changes at present, we default to initializing relkind as + * RELKIND_RELATION. This is needed in CheckSubscriptionRelkind() to check + * if the publisher and subscriber relation kinds are compatible. + */ + entry->remoterel.relkind = + (remoterel->relkind == 0) ? RELKIND_RELATION : remoterel->relkind; + entry->remoterel.attkeys = bms_copy(remoterel->attkeys); MemoryContextSwitchTo(oldctx); } @@ -238,6 +250,7 @@ logicalrep_get_attrs_str(LogicalRepRelation *remoterel, Bitmapset *atts) { attcnt++; if (attcnt > 1) + /* translator: This is a separator in a list of entity names. */ appendStringInfoString(&attsbuf, _(", ")); appendStringInfo(&attsbuf, _("\"%s\""), remoterel->attnames[i]); @@ -425,6 +438,7 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode) /* Check for supported relkind. */ CheckSubscriptionRelkind(entry->localrel->rd_rel->relkind, + remoterel->relkind, remoterel->nspname, remoterel->relname); /* @@ -691,8 +705,8 @@ logicalrep_partition_open(LogicalRepRelMapEntry *root, entry->remoterel.nspname = pstrdup(remoterel->nspname); entry->remoterel.relname = pstrdup(remoterel->relname); entry->remoterel.natts = remoterel->natts; - entry->remoterel.attnames = palloc(remoterel->natts * sizeof(char *)); - entry->remoterel.atttyps = palloc(remoterel->natts * sizeof(Oid)); + entry->remoterel.attnames = palloc_array(char *, remoterel->natts); + entry->remoterel.atttyps = palloc_array(Oid, remoterel->natts); for (i = 0; i < remoterel->natts; i++) { entry->remoterel.attnames[i] = pstrdup(remoterel->attnames[i]); diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c index 676551118753d..682d13c9f22f0 100644 --- a/src/backend/replication/logical/reorderbuffer.c +++ b/src/backend/replication/logical/reorderbuffer.c @@ -4,7 +4,7 @@ * PostgreSQL logical replay/reorder buffer management * * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -109,9 +109,22 @@ #include "storage/procarray.h" #include "storage/sinval.h" #include "utils/builtins.h" +#include "utils/inval.h" #include "utils/memutils.h" #include "utils/rel.h" #include "utils/relfilenumbermap.h" +#include "utils/wait_event.h" + +/* + * Each transaction has an 8MB limit for invalidation messages distributed from + * other transactions. This limit is set considering scenarios with many + * concurrent logical decoding operations. When the distributed invalidation + * messages reach this threshold, the transaction is marked as + * RBTXN_DISTR_INVAL_OVERFLOWED to invalidate the complete cache as we have lost + * some inval messages and hence don't know what needs to be invalidated. + */ +#define MAX_DISTR_INVAL_MSG_PER_TXN \ + ((8 * 1024 * 1024) / sizeof(SharedInvalidationMessage)) /* entry for a hash table we use to map from xid to our transaction state */ typedef struct ReorderBufferTXNByIdEnt @@ -170,8 +183,8 @@ typedef struct ReorderBufferToastEnt Size num_chunks; /* number of chunks we've already seen */ Size size; /* combined size of chunks seen */ dlist_head chunks; /* linked list of chunks */ - struct varlena *reconstructed; /* reconstructed varlena now pointed to in - * main tup */ + varlena *reconstructed; /* reconstructed varlena now pointed to in + * main tup */ } ReorderBufferToastEnt; /* Disk serialization support datastructures */ @@ -378,6 +391,7 @@ ReorderBufferAllocate(void) buffer->streamTxns = 0; buffer->streamCount = 0; buffer->streamBytes = 0; + buffer->memExceededCount = 0; buffer->totalTxns = 0; buffer->totalBytes = 0; @@ -472,6 +486,12 @@ ReorderBufferFreeTXN(ReorderBuffer *rb, ReorderBufferTXN *txn) txn->invalidations = NULL; } + if (txn->invalidations_distributed) + { + pfree(txn->invalidations_distributed); + txn->invalidations_distributed = NULL; + } + /* Reset the toast hash */ ReorderBufferToastReset(rb, txn); @@ -682,7 +702,7 @@ ReorderBufferTXNByXid(ReorderBuffer *rb, TransactionId xid, bool create, { /* initialize the new entry, if creation was requested */ Assert(ent != NULL); - Assert(lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(lsn)); ent->txn = ReorderBufferAllocTXN(rb); ent->txn->xid = xid; @@ -830,7 +850,7 @@ ReorderBufferQueueChange(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn, change->lsn = lsn; change->txn = txn; - Assert(InvalidXLogRecPtr != lsn); + Assert(XLogRecPtrIsValid(lsn)); dlist_push_tail(&txn->changes, &change->node); txn->nentries++; txn->nentries_mem++; @@ -947,14 +967,14 @@ AssertTXNLsnOrder(ReorderBuffer *rb) iter.cur); /* start LSN must be set */ - Assert(cur_txn->first_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(cur_txn->first_lsn)); /* If there is an end LSN, it must be higher than start LSN */ - if (cur_txn->end_lsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(cur_txn->end_lsn)) Assert(cur_txn->first_lsn <= cur_txn->end_lsn); /* Current initial LSN must be strictly higher than previous */ - if (prev_first_lsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(prev_first_lsn)) Assert(prev_first_lsn < cur_txn->first_lsn); /* known-as-subtxn txns must not be listed */ @@ -971,10 +991,10 @@ AssertTXNLsnOrder(ReorderBuffer *rb) /* base snapshot (and its LSN) must be set */ Assert(cur_txn->base_snapshot != NULL); - Assert(cur_txn->base_snapshot_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(cur_txn->base_snapshot_lsn)); /* current LSN must be strictly higher than previous */ - if (prev_base_snap_lsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(prev_base_snap_lsn)) Assert(prev_base_snap_lsn < cur_txn->base_snapshot_lsn); /* known-as-subtxn txns must not be listed */ @@ -1003,11 +1023,11 @@ AssertChangeLsnOrder(ReorderBufferTXN *txn) cur_change = dlist_container(ReorderBufferChange, node, iter.cur); - Assert(txn->first_lsn != InvalidXLogRecPtr); - Assert(cur_change->lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(txn->first_lsn)); + Assert(XLogRecPtrIsValid(cur_change->lsn)); Assert(txn->first_lsn <= cur_change->lsn); - if (txn->end_lsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(txn->end_lsn)) Assert(cur_change->lsn <= txn->end_lsn); Assert(prev_lsn <= cur_change->lsn); @@ -1034,7 +1054,7 @@ ReorderBufferGetOldestTXN(ReorderBuffer *rb) txn = dlist_head_element(ReorderBufferTXN, node, &rb->toplevel_by_lsn); Assert(!rbtxn_is_known_subxact(txn)); - Assert(txn->first_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(txn->first_lsn)); return txn; } @@ -1397,7 +1417,7 @@ ReorderBufferIterTXNNext(ReorderBuffer *rb, ReorderBufferIterTXNState *state) int32 off; /* nothing there anymore */ - if (state->heap->bh_size == 0) + if (binaryheap_empty(state->heap)) return NULL; off = DatumGetInt32(binaryheap_first(state->heap)); @@ -2197,6 +2217,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, { bool using_subtxn; MemoryContext ccxt = CurrentMemoryContext; + ResourceOwner cowner = CurrentResourceOwner; ReorderBufferIterTXNState *volatile iterstate = NULL; volatile XLogRecPtr prev_lsn = InvalidXLogRecPtr; ReorderBufferChange *volatile specinsert = NULL; @@ -2256,7 +2277,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, * We can't call start stream callback before processing first * change. */ - if (prev_lsn == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(prev_lsn)) { if (streaming) { @@ -2271,7 +2292,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, * subtransactions. The changes may have the same LSN due to * MULTI_INSERT xlog records. */ - Assert(prev_lsn == InvalidXLogRecPtr || prev_lsn <= change->lsn); + Assert(!XLogRecPtrIsValid(prev_lsn) || prev_lsn <= change->lsn); prev_lsn = change->lsn; @@ -2302,6 +2323,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, change->action = REORDER_BUFFER_CHANGE_INSERT; /* intentionally fall through */ + pg_fallthrough; case REORDER_BUFFER_CHANGE_INSERT: case REORDER_BUFFER_CHANGE_UPDATE: case REORDER_BUFFER_CHANGE_DELETE: @@ -2425,12 +2447,8 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, * CheckTableNotInUse() and locking. */ - /* clear out a pending (and thus failed) speculation */ - if (specinsert != NULL) - { - ReorderBufferFreeChange(rb, specinsert, true); - specinsert = NULL; - } + /* Previous speculative insertion must be aborted */ + Assert(specinsert == NULL); /* and memorize the pending insertion */ dlist_delete(&change->node); @@ -2470,7 +2488,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, int nrelations = 0; Relation *relations; - relations = palloc0(nrelids * sizeof(Relation)); + relations = palloc0_array(Relation, nrelids); for (i = 0; i < nrelids; i++) { Oid relid = change->data.truncate.relids[i]; @@ -2581,7 +2599,7 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, if (++changes_count >= CHANGES_THRESHOLD) { - rb->update_progress_txn(rb, txn, change->lsn); + rb->update_progress_txn(rb, txn, prev_lsn); changes_count = 0; } } @@ -2661,10 +2679,24 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, AbortCurrentTransaction(); /* make sure there's no cache pollution */ - ReorderBufferExecuteInvalidations(txn->ninvalidations, txn->invalidations); + if (rbtxn_distr_inval_overflowed(txn)) + { + Assert(txn->ninvalidations_distributed == 0); + InvalidateSystemCaches(); + } + else + { + ReorderBufferExecuteInvalidations(txn->ninvalidations, txn->invalidations); + ReorderBufferExecuteInvalidations(txn->ninvalidations_distributed, + txn->invalidations_distributed); + } if (using_subtxn) + { RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(ccxt); + CurrentResourceOwner = cowner; + } /* * We are here due to one of the four reasons: 1. Decoding an @@ -2710,11 +2742,24 @@ ReorderBufferProcessTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, AbortCurrentTransaction(); /* make sure there's no cache pollution */ - ReorderBufferExecuteInvalidations(txn->ninvalidations, - txn->invalidations); + if (rbtxn_distr_inval_overflowed(txn)) + { + Assert(txn->ninvalidations_distributed == 0); + InvalidateSystemCaches(); + } + else + { + ReorderBufferExecuteInvalidations(txn->ninvalidations, txn->invalidations); + ReorderBufferExecuteInvalidations(txn->ninvalidations_distributed, + txn->invalidations_distributed); + } if (using_subtxn) + { RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(ccxt); + CurrentResourceOwner = cowner; + } /* * The error code ERRCODE_TRANSACTION_ROLLBACK indicates a concurrent @@ -2777,14 +2822,14 @@ ReorderBufferReplay(ReorderBufferTXN *txn, ReorderBuffer *rb, TransactionId xid, XLogRecPtr commit_lsn, XLogRecPtr end_lsn, TimestampTz commit_time, - RepOriginId origin_id, XLogRecPtr origin_lsn) + ReplOriginId origin_id, XLogRecPtr origin_lsn) { Snapshot snapshot_now; CommandId command_id = FirstCommandId; txn->final_lsn = commit_lsn; txn->end_lsn = end_lsn; - txn->xact_time.commit_time = commit_time; + txn->commit_time = commit_time; txn->origin_id = origin_id; txn->origin_lsn = origin_lsn; @@ -2837,7 +2882,7 @@ void ReorderBufferCommit(ReorderBuffer *rb, TransactionId xid, XLogRecPtr commit_lsn, XLogRecPtr end_lsn, TimestampTz commit_time, - RepOriginId origin_id, XLogRecPtr origin_lsn) + ReplOriginId origin_id, XLogRecPtr origin_lsn) { ReorderBufferTXN *txn; @@ -2860,7 +2905,7 @@ bool ReorderBufferRememberPrepareInfo(ReorderBuffer *rb, TransactionId xid, XLogRecPtr prepare_lsn, XLogRecPtr end_lsn, TimestampTz prepare_time, - RepOriginId origin_id, XLogRecPtr origin_lsn) + ReplOriginId origin_id, XLogRecPtr origin_lsn) { ReorderBufferTXN *txn; @@ -2876,7 +2921,7 @@ ReorderBufferRememberPrepareInfo(ReorderBuffer *rb, TransactionId xid, */ txn->final_lsn = prepare_lsn; txn->end_lsn = end_lsn; - txn->xact_time.prepare_time = prepare_time; + txn->prepare_time = prepare_time; txn->origin_id = origin_id; txn->origin_lsn = origin_lsn; @@ -2928,12 +2973,12 @@ ReorderBufferPrepare(ReorderBuffer *rb, TransactionId xid, * have been updated in it by now. */ Assert((txn->txn_flags & RBTXN_PREPARE_STATUS_MASK) == RBTXN_IS_PREPARED); - Assert(txn->final_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(txn->final_lsn)); txn->gid = pstrdup(gid); ReorderBufferReplay(txn, rb, xid, txn->final_lsn, txn->end_lsn, - txn->xact_time.prepare_time, txn->origin_id, txn->origin_lsn); + txn->prepare_time, txn->origin_id, txn->origin_lsn); /* * Send a prepare if not already done so. This might occur if we have @@ -2954,7 +2999,7 @@ void ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, XLogRecPtr commit_lsn, XLogRecPtr end_lsn, XLogRecPtr two_phase_at, - TimestampTz commit_time, RepOriginId origin_id, + TimestampTz commit_time, ReplOriginId origin_id, XLogRecPtr origin_lsn, char *gid, bool is_commit) { ReorderBufferTXN *txn; @@ -2972,7 +3017,7 @@ ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, * be later used for rollback. */ prepare_end_lsn = txn->end_lsn; - prepare_time = txn->xact_time.prepare_time; + prepare_time = txn->prepare_time; /* add the gid in the txn */ txn->gid = pstrdup(gid); @@ -2994,7 +3039,7 @@ ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, */ Assert((txn->txn_flags & RBTXN_PREPARE_STATUS_MASK) == (RBTXN_IS_PREPARED | RBTXN_SKIPPED_PREPARE)); - Assert(txn->final_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(txn->final_lsn)); /* * By this time the txn has the prepare record information and it is @@ -3004,12 +3049,12 @@ ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, * prepared after the restart. */ ReorderBufferReplay(txn, rb, xid, txn->final_lsn, txn->end_lsn, - txn->xact_time.prepare_time, txn->origin_id, txn->origin_lsn); + txn->prepare_time, txn->origin_id, txn->origin_lsn); } txn->final_lsn = commit_lsn; txn->end_lsn = end_lsn; - txn->xact_time.commit_time = commit_time; + txn->commit_time = commit_time; txn->origin_id = origin_id; txn->origin_lsn = origin_lsn; @@ -3049,7 +3094,7 @@ ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn, if (txn == NULL) return; - txn->xact_time.abort_time = abort_time; + txn->abort_time = abort_time; /* For streamed transactions notify the remote node about the abort. */ if (rbtxn_is_streamed(txn)) @@ -3060,7 +3105,8 @@ ReorderBufferAbort(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn, * We might have decoded changes for this transaction that could load * the cache as per the current transaction's view (consider DDL's * happened in this transaction). We don't want the decoding of future - * transactions to use those cache entries so execute invalidations. + * transactions to use those cache entries so execute only the inval + * messages in this transaction. */ if (txn->ninvalidations > 0) ReorderBufferImmediateInvalidation(rb, txn->ninvalidations, @@ -3147,9 +3193,10 @@ ReorderBufferForget(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn) txn->final_lsn = lsn; /* - * Process cache invalidation messages if there are any. Even if we're not - * interested in the transaction's contents, it could have manipulated the - * catalog and we need to update the caches according to that. + * Process only cache invalidation messages in this transaction if there + * are any. Even if we're not interested in the transaction's contents, it + * could have manipulated the catalog and we need to update the caches + * according to that. */ if (txn->base_snapshot != NULL && txn->ninvalidations > 0) ReorderBufferImmediateInvalidation(rb, txn->ninvalidations, @@ -3205,6 +3252,8 @@ ReorderBufferImmediateInvalidation(ReorderBuffer *rb, uint32 ninvalidations, SharedInvalidationMessage *invalidations) { bool use_subtxn = IsTransactionOrTransactionBlock(); + MemoryContext ccxt = CurrentMemoryContext; + ResourceOwner cowner = CurrentResourceOwner; int i; if (use_subtxn) @@ -3223,7 +3272,11 @@ ReorderBufferImmediateInvalidation(ReorderBuffer *rb, uint32 ninvalidations, LocalExecuteInvalidationMessage(&invalidations[i]); if (use_subtxn) + { RollbackAndReleaseCurrentSubTransaction(); + MemoryContextSwitchTo(ccxt); + CurrentResourceOwner = cowner; + } } /* @@ -3421,6 +3474,55 @@ ReorderBufferAddNewTupleCids(ReorderBuffer *rb, TransactionId xid, txn->ntuplecids++; } +/* + * Add new invalidation messages to the reorder buffer queue. + */ +static void +ReorderBufferQueueInvalidations(ReorderBuffer *rb, TransactionId xid, + XLogRecPtr lsn, Size nmsgs, + SharedInvalidationMessage *msgs) +{ + ReorderBufferChange *change; + + change = ReorderBufferAllocChange(rb); + change->action = REORDER_BUFFER_CHANGE_INVALIDATION; + change->data.inval.ninvalidations = nmsgs; + change->data.inval.invalidations = palloc_array(SharedInvalidationMessage, nmsgs); + memcpy(change->data.inval.invalidations, msgs, + sizeof(SharedInvalidationMessage) * nmsgs); + + ReorderBufferQueueChange(rb, xid, lsn, change, false); +} + +/* + * A helper function for ReorderBufferAddInvalidations() and + * ReorderBufferAddDistributedInvalidations() to accumulate the invalidation + * messages to the **invals_out. + */ +static void +ReorderBufferAccumulateInvalidations(SharedInvalidationMessage **invals_out, + uint32 *ninvals_out, + SharedInvalidationMessage *msgs_new, + Size nmsgs_new) +{ + if (*ninvals_out == 0) + { + *ninvals_out = nmsgs_new; + *invals_out = palloc_array(SharedInvalidationMessage, nmsgs_new); + memcpy(*invals_out, msgs_new, sizeof(SharedInvalidationMessage) * nmsgs_new); + } + else + { + /* Enlarge the array of inval messages */ + *invals_out = + repalloc_array(*invals_out, SharedInvalidationMessage, + (*ninvals_out + nmsgs_new)); + memcpy(*invals_out + *ninvals_out, msgs_new, + nmsgs_new * sizeof(SharedInvalidationMessage)); + *ninvals_out += nmsgs_new; + } +} + /* * Accumulate the invalidations for executing them later. * @@ -3441,7 +3543,6 @@ ReorderBufferAddInvalidations(ReorderBuffer *rb, TransactionId xid, { ReorderBufferTXN *txn; MemoryContext oldcontext; - ReorderBufferChange *change; txn = ReorderBufferTXNByXid(rb, xid, true, NULL, lsn, true); @@ -3456,35 +3557,76 @@ ReorderBufferAddInvalidations(ReorderBuffer *rb, TransactionId xid, Assert(nmsgs > 0); - /* Accumulate invalidations. */ - if (txn->ninvalidations == 0) - { - txn->ninvalidations = nmsgs; - txn->invalidations = (SharedInvalidationMessage *) - palloc(sizeof(SharedInvalidationMessage) * nmsgs); - memcpy(txn->invalidations, msgs, - sizeof(SharedInvalidationMessage) * nmsgs); - } - else + ReorderBufferAccumulateInvalidations(&txn->invalidations, + &txn->ninvalidations, + msgs, nmsgs); + + ReorderBufferQueueInvalidations(rb, xid, lsn, nmsgs, msgs); + + MemoryContextSwitchTo(oldcontext); +} + +/* + * Accumulate the invalidations distributed by other committed transactions + * for executing them later. + * + * This function is similar to ReorderBufferAddInvalidations() but stores + * the given inval messages to the txn->invalidations_distributed with the + * overflow check. + * + * This needs to be called by committed transactions to distribute their + * inval messages to in-progress transactions. + */ +void +ReorderBufferAddDistributedInvalidations(ReorderBuffer *rb, TransactionId xid, + XLogRecPtr lsn, Size nmsgs, + SharedInvalidationMessage *msgs) +{ + ReorderBufferTXN *txn; + MemoryContext oldcontext; + + txn = ReorderBufferTXNByXid(rb, xid, true, NULL, lsn, true); + + oldcontext = MemoryContextSwitchTo(rb->context); + + /* + * Collect all the invalidations under the top transaction, if available, + * so that we can execute them all together. See comments + * ReorderBufferAddInvalidations. + */ + txn = rbtxn_get_toptxn(txn); + + Assert(nmsgs > 0); + + if (!rbtxn_distr_inval_overflowed(txn)) { - txn->invalidations = (SharedInvalidationMessage *) - repalloc(txn->invalidations, sizeof(SharedInvalidationMessage) * - (txn->ninvalidations + nmsgs)); + /* + * Check the transaction has enough space for storing distributed + * invalidation messages. + */ + if (txn->ninvalidations_distributed + nmsgs >= MAX_DISTR_INVAL_MSG_PER_TXN) + { + /* + * Mark the invalidation message as overflowed and free up the + * messages accumulated so far. + */ + txn->txn_flags |= RBTXN_DISTR_INVAL_OVERFLOWED; - memcpy(txn->invalidations + txn->ninvalidations, msgs, - nmsgs * sizeof(SharedInvalidationMessage)); - txn->ninvalidations += nmsgs; + if (txn->invalidations_distributed) + { + pfree(txn->invalidations_distributed); + txn->invalidations_distributed = NULL; + txn->ninvalidations_distributed = 0; + } + } + else + ReorderBufferAccumulateInvalidations(&txn->invalidations_distributed, + &txn->ninvalidations_distributed, + msgs, nmsgs); } - change = ReorderBufferAllocChange(rb); - change->action = REORDER_BUFFER_CHANGE_INVALIDATION; - change->data.inval.ninvalidations = nmsgs; - change->data.inval.invalidations = (SharedInvalidationMessage *) - palloc(sizeof(SharedInvalidationMessage) * nmsgs); - memcpy(change->data.inval.invalidations, msgs, - sizeof(SharedInvalidationMessage) * nmsgs); - - ReorderBufferQueueChange(rb, xid, lsn, change, false); + /* Queue the invalidation messages into the transaction */ + ReorderBufferQueueInvalidations(rb, xid, lsn, nmsgs, msgs); MemoryContextSwitchTo(oldcontext); } @@ -3555,8 +3697,7 @@ ReorderBufferGetCatalogChangesXacts(ReorderBuffer *rb) return NULL; /* Initialize XID array */ - xids = (TransactionId *) palloc(sizeof(TransactionId) * - dclist_count(&rb->catchange_txns)); + xids = palloc_array(TransactionId, dclist_count(&rb->catchange_txns)); dclist_foreach(iter, &rb->catchange_txns) { ReorderBufferTXN *txn = dclist_container(ReorderBufferTXN, @@ -3753,14 +3894,26 @@ static void ReorderBufferCheckMemoryLimit(ReorderBuffer *rb) { ReorderBufferTXN *txn; + bool update_stats = true; - /* - * Bail out if debug_logical_replication_streaming is buffered and we - * haven't exceeded the memory limit. - */ - if (debug_logical_replication_streaming == DEBUG_LOGICAL_REP_STREAMING_BUFFERED && - rb->size < logical_decoding_work_mem * (Size) 1024) + if (rb->size >= logical_decoding_work_mem * (Size) 1024) + { + /* + * Update the statistics as the memory usage has reached the limit. We + * report the statistics update later in this function since we can + * update the slot statistics altogether while streaming or + * serializing transactions in most cases. + */ + rb->memExceededCount += 1; + } + else if (debug_logical_replication_streaming == DEBUG_LOGICAL_REP_STREAMING_BUFFERED) + { + /* + * Bail out if debug_logical_replication_streaming is buffered and we + * haven't exceeded the memory limit. + */ return; + } /* * If debug_logical_replication_streaming is immediate, loop until there's @@ -3820,8 +3973,17 @@ ReorderBufferCheckMemoryLimit(ReorderBuffer *rb) */ Assert(txn->size == 0); Assert(txn->nentries_mem == 0); + + /* + * We've reported the memExceededCount update while streaming or + * serializing the transaction. + */ + update_stats = false; } + if (update_stats) + UpdateDecodingStats((LogicalDecodingContext *) rb->private_data); + /* We must be under the memory limit now. */ Assert(rb->size < logical_decoding_work_mem * (Size) 1024); } @@ -4385,8 +4547,8 @@ ReorderBufferRestoreChanges(ReorderBuffer *rb, ReorderBufferTXN *txn, dlist_mutable_iter cleanup_iter; File *fd = &file->vfd; - Assert(txn->first_lsn != InvalidXLogRecPtr); - Assert(txn->final_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(txn->first_lsn)); + Assert(XLogRecPtrIsValid(txn->final_lsn)); /* free current entries, so we have memory for more */ dlist_foreach_modify(cleanup_iter, &txn->changes) @@ -4693,8 +4855,8 @@ ReorderBufferRestoreCleanup(ReorderBuffer *rb, ReorderBufferTXN *txn) XLogSegNo cur; XLogSegNo last; - Assert(txn->first_lsn != InvalidXLogRecPtr); - Assert(txn->final_lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(txn->first_lsn)); + Assert(XLogRecPtrIsValid(txn->final_lsn)); XLByteToSeg(txn->first_lsn, first, wal_segment_size); XLByteToSeg(txn->final_lsn, last, wal_segment_size); @@ -4787,7 +4949,7 @@ StartupReorderBuffer(void) continue; /* if it cannot be a slot, skip the directory */ - if (!ReplicationSlotValidateName(logical_de->d_name, DEBUG2)) + if (!ReplicationSlotValidateName(logical_de->d_name, true, DEBUG2)) continue; /* @@ -4957,9 +5119,9 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn, toast_desc = RelationGetDescr(toast_rel); /* should we allocate from stack instead? */ - attrs = palloc0(sizeof(Datum) * desc->natts); - isnull = palloc0(sizeof(bool) * desc->natts); - free = palloc0(sizeof(bool) * desc->natts); + attrs = palloc0_array(Datum, desc->natts); + isnull = palloc0_array(bool, desc->natts); + free = palloc0_array(bool, desc->natts); newtup = change->data.tp.newtuple; @@ -4967,22 +5129,18 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn, for (natt = 0; natt < desc->natts; natt++) { - Form_pg_attribute attr = TupleDescAttr(desc, natt); + CompactAttribute *attr = TupleDescCompactAttr(desc, natt); ReorderBufferToastEnt *ent; - struct varlena *varlena; + varlena *varlena_pointer; /* va_rawsize is the size of the original datum -- including header */ - struct varatt_external toast_pointer; - struct varatt_indirect redirect_pointer; - struct varlena *new_datum = NULL; - struct varlena *reconstructed; + varatt_external toast_pointer; + varatt_indirect redirect_pointer; + varlena *new_datum = NULL; + varlena *reconstructed; dlist_iter it; Size data_done = 0; - /* system columns aren't toasted */ - if (attr->attnum < 0) - continue; - if (attr->attisdropped) continue; @@ -4995,13 +5153,13 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn, continue; /* ok, we know we have a toast datum */ - varlena = (struct varlena *) DatumGetPointer(attrs[natt]); + varlena_pointer = (varlena *) DatumGetPointer(attrs[natt]); /* no need to do anything if the tuple isn't external */ - if (!VARATT_IS_EXTERNAL(varlena)) + if (!VARATT_IS_EXTERNAL(varlena_pointer)) continue; - VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena); + VARATT_EXTERNAL_GET_POINTER(toast_pointer, varlena_pointer); /* * Check whether the toast tuple changed, replace if so. @@ -5015,7 +5173,7 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn, continue; new_datum = - (struct varlena *) palloc0(INDIRECT_POINTER_SIZE); + (varlena *) palloc0(INDIRECT_POINTER_SIZE); free[natt] = true; @@ -5201,7 +5359,7 @@ DisplayMapping(HTAB *tuplecid_data) * transaction c) applied in LSN order. */ static void -ApplyLogicalMappingFile(HTAB *tuplecid_data, Oid relid, const char *fname) +ApplyLogicalMappingFile(HTAB *tuplecid_data, const char *fname) { char path[MAXPGPATH]; int fd; @@ -5368,7 +5526,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot) continue; /* ok, relevant, queue for apply */ - f = palloc(sizeof(RewriteMappingFile)); + f = palloc_object(RewriteMappingFile); f->lsn = f_lsn; strcpy(f->fname, mapping_de->d_name); files = lappend(files, f); @@ -5384,7 +5542,7 @@ UpdateLogicalMappings(HTAB *tuplecid_data, Oid relid, Snapshot snapshot) elog(DEBUG1, "applying mapping: \"%s\" in %u", f->fname, snapshot->subxip[0]); - ApplyLogicalMappingFile(tuplecid_data, relid, f->fname); + ApplyLogicalMappingFile(tuplecid_data, f->fname); pfree(f); } } diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c new file mode 100644 index 0000000000000..ec7e76abf934f --- /dev/null +++ b/src/backend/replication/logical/sequencesync.c @@ -0,0 +1,776 @@ +/*------------------------------------------------------------------------- + * sequencesync.c + * PostgreSQL logical replication: sequence synchronization + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/logical/sequencesync.c + * + * NOTES + * This file contains code for sequence synchronization for + * logical replication. + * + * Sequences requiring synchronization are tracked in the pg_subscription_rel + * catalog. + * + * Sequences to be synchronized will be added with state INIT when either of + * the following commands is executed: + * CREATE SUBSCRIPTION + * ALTER SUBSCRIPTION ... REFRESH PUBLICATION + * + * Executing the following command resets all sequences in the subscription to + * state INIT, triggering re-synchronization: + * ALTER SUBSCRIPTION ... REFRESH SEQUENCES + * + * The apply worker periodically scans pg_subscription_rel for sequences in + * INIT state. When such sequences are found, it spawns a sequencesync worker + * to handle synchronization. + * + * A single sequencesync worker is responsible for synchronizing all sequences. + * It begins by retrieving the list of sequences that are flagged for + * synchronization, i.e., those in the INIT state. These sequences are then + * processed in batches, allowing multiple entries to be synchronized within a + * single transaction. The worker fetches the current sequence values and page + * LSNs from the remote publisher, updates the corresponding sequences on the + * local subscriber, and finally marks each sequence as READY upon successful + * synchronization. + * + * Sequence state transitions follow this pattern: + * INIT -> READY + * + * To avoid creating too many transactions, up to MAX_SEQUENCES_SYNC_PER_BATCH + * sequences are synchronized per transaction. The locks on the sequence + * relation will be periodically released at each transaction commit. + * + * XXX: We didn't choose launcher process to maintain the launch of sequencesync + * worker as it didn't have database connection to access the sequences from the + * pg_subscription_rel system catalog that need to be synchronized. + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/genam.h" +#include "access/table.h" +#include "catalog/pg_sequence.h" +#include "catalog/pg_subscription_rel.h" +#include "commands/sequence.h" +#include "pgstat.h" +#include "postmaster/interrupt.h" +#include "replication/logicalworker.h" +#include "replication/worker_internal.h" +#include "storage/lwlock.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/guc.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/pg_lsn.h" +#include "utils/syscache.h" +#include "utils/usercontext.h" + +#define REMOTE_SEQ_COL_COUNT 10 + +typedef enum CopySeqResult +{ + COPYSEQ_SUCCESS, + COPYSEQ_MISMATCH, + COPYSEQ_INSUFFICIENT_PERM, + COPYSEQ_SKIPPED +} CopySeqResult; + +static List *seqinfos = NIL; + +/* + * Apply worker determines if sequence synchronization is needed. + * + * Start a sequencesync worker if one is not already running. The active + * sequencesync worker will handle all pending sequence synchronization. If any + * sequences remain unsynchronized after it exits, a new worker can be started + * in the next iteration. + */ +void +ProcessSequencesForSync(void) +{ + LogicalRepWorker *sequencesync_worker; + int nsyncworkers; + bool has_pending_sequences; + bool started_tx; + + FetchRelationStates(NULL, &has_pending_sequences, &started_tx); + + if (started_tx) + { + CommitTransactionCommand(); + pgstat_report_stat(true); + } + + if (!has_pending_sequences) + return; + + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + + /* Check if there is a sequencesync worker already running? */ + sequencesync_worker = logicalrep_worker_find(WORKERTYPE_SEQUENCESYNC, + MyLogicalRepWorker->subid, + InvalidOid, true); + if (sequencesync_worker) + { + LWLockRelease(LogicalRepWorkerLock); + return; + } + + /* + * Count running sync workers for this subscription, while we have the + * lock. + */ + nsyncworkers = logicalrep_sync_worker_count(MyLogicalRepWorker->subid); + LWLockRelease(LogicalRepWorkerLock); + + /* + * It is okay to read/update last_seqsync_start_time here in apply worker + * as we have already ensured that sync worker doesn't exist. + */ + launch_sync_worker(WORKERTYPE_SEQUENCESYNC, nsyncworkers, InvalidOid, + &MyLogicalRepWorker->last_seqsync_start_time); +} + +/* + * get_sequences_string + * + * Build a comma-separated string of schema-qualified sequence names + * for the given list of sequence indexes. + */ +static void +get_sequences_string(List *seqindexes, StringInfo buf) +{ + resetStringInfo(buf); + foreach_int(seqidx, seqindexes) + { + LogicalRepSequenceInfo *seqinfo = + (LogicalRepSequenceInfo *) list_nth(seqinfos, seqidx); + + if (buf->len > 0) + appendStringInfoString(buf, ", "); + + appendStringInfo(buf, "\"%s.%s\"", seqinfo->nspname, seqinfo->seqname); + } +} + +/* + * report_sequence_errors + * + * Report discrepancies found during sequence synchronization between + * the publisher and subscriber. Emits warnings for: + * a) mismatched definitions or concurrent rename + * b) insufficient privileges + * c) missing sequences on the subscriber + * Then raises an ERROR to indicate synchronization failure. + */ +static void +report_sequence_errors(List *mismatched_seqs_idx, List *insuffperm_seqs_idx, + List *missing_seqs_idx) +{ + StringInfoData seqstr; + + /* Quick exit if there are no errors to report */ + if (!mismatched_seqs_idx && !insuffperm_seqs_idx && !missing_seqs_idx) + return; + + initStringInfo(&seqstr); + + if (mismatched_seqs_idx) + { + get_sequences_string(mismatched_seqs_idx, &seqstr); + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg_plural("mismatched or renamed sequence on subscriber (%s)", + "mismatched or renamed sequences on subscriber (%s)", + list_length(mismatched_seqs_idx), + seqstr.data)); + } + + if (insuffperm_seqs_idx) + { + get_sequences_string(insuffperm_seqs_idx, &seqstr); + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg_plural("insufficient privileges on sequence (%s)", + "insufficient privileges on sequences (%s)", + list_length(insuffperm_seqs_idx), + seqstr.data)); + } + + if (missing_seqs_idx) + { + get_sequences_string(missing_seqs_idx, &seqstr); + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg_plural("missing sequence on publisher (%s)", + "missing sequences on publisher (%s)", + list_length(missing_seqs_idx), + seqstr.data)); + } + + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("logical replication sequence synchronization failed for subscription \"%s\"", + MySubscription->name)); +} + +/* + * get_and_validate_seq_info + * + * Extracts remote sequence information from the tuple slot received from the + * publisher, and validates it against the corresponding local sequence + * definition. + */ +static CopySeqResult +get_and_validate_seq_info(TupleTableSlot *slot, Relation *sequence_rel, + LogicalRepSequenceInfo **seqinfo, int *seqidx) +{ + bool isnull; + int col = 0; + Datum datum; + Oid remote_typid; + int64 remote_start; + int64 remote_increment; + int64 remote_min; + int64 remote_max; + bool remote_cycle; + CopySeqResult result = COPYSEQ_SUCCESS; + HeapTuple tup; + Form_pg_sequence local_seq; + LogicalRepSequenceInfo *seqinfo_local; + + *seqidx = DatumGetInt32(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + /* Identify the corresponding local sequence for the given index. */ + *seqinfo = seqinfo_local = + (LogicalRepSequenceInfo *) list_nth(seqinfos, *seqidx); + + /* + * last_value can be NULL if the sequence was dropped concurrently (see + * pg_get_sequence_data()). + */ + datum = slot_getattr(slot, ++col, &isnull); + if (isnull) + return COPYSEQ_SKIPPED; + seqinfo_local->last_value = DatumGetInt64(datum); + + seqinfo_local->is_called = DatumGetBool(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + seqinfo_local->page_lsn = DatumGetLSN(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + remote_typid = DatumGetObjectId(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + remote_start = DatumGetInt64(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + remote_increment = DatumGetInt64(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + remote_min = DatumGetInt64(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + remote_max = DatumGetInt64(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + remote_cycle = DatumGetBool(slot_getattr(slot, ++col, &isnull)); + Assert(!isnull); + + /* Sanity check */ + Assert(col == REMOTE_SEQ_COL_COUNT); + + seqinfo_local->found_on_pub = true; + + *sequence_rel = try_table_open(seqinfo_local->localrelid, RowExclusiveLock); + + /* Sequence was concurrently dropped? */ + if (!*sequence_rel) + return COPYSEQ_SKIPPED; + + tup = SearchSysCache1(SEQRELID, ObjectIdGetDatum(seqinfo_local->localrelid)); + + /* Sequence was concurrently dropped? */ + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for sequence %u", + seqinfo_local->localrelid); + + local_seq = (Form_pg_sequence) GETSTRUCT(tup); + + /* Sequence parameters for remote/local are the same? */ + if (local_seq->seqtypid != remote_typid || + local_seq->seqstart != remote_start || + local_seq->seqincrement != remote_increment || + local_seq->seqmin != remote_min || + local_seq->seqmax != remote_max || + local_seq->seqcycle != remote_cycle) + result = COPYSEQ_MISMATCH; + + /* Sequence was concurrently renamed? */ + if (strcmp(seqinfo_local->nspname, + get_namespace_name(RelationGetNamespace(*sequence_rel))) || + strcmp(seqinfo_local->seqname, RelationGetRelationName(*sequence_rel))) + result = COPYSEQ_MISMATCH; + + ReleaseSysCache(tup); + return result; +} + +/* + * Apply remote sequence state to local sequence and mark it as + * synchronized (READY). + */ +static CopySeqResult +copy_sequence(LogicalRepSequenceInfo *seqinfo, Oid seqowner) +{ + UserContext ucxt; + AclResult aclresult; + bool run_as_owner = MySubscription->runasowner; + Oid seqoid = seqinfo->localrelid; + + /* + * If the user did not opt to run as the owner of the subscription + * ('run_as_owner'), then copy the sequence as the owner of the sequence. + */ + if (!run_as_owner) + SwitchToUntrustedUser(seqowner, &ucxt); + + aclresult = pg_class_aclcheck(seqoid, GetUserId(), ACL_UPDATE); + + if (aclresult != ACLCHECK_OK) + { + if (!run_as_owner) + RestoreUserContext(&ucxt); + + return COPYSEQ_INSUFFICIENT_PERM; + } + + /* + * The log counter (log_cnt) tracks how many sequence values are still + * unused locally. It is only relevant to the local node and managed + * internally by nextval() when allocating new ranges. Since log_cnt does + * not affect the visible sequence state (like last_value or is_called) + * and is only used for local caching, it need not be copied to the + * subscriber during synchronization. + */ + SetSequence(seqoid, seqinfo->last_value, seqinfo->is_called); + + if (!run_as_owner) + RestoreUserContext(&ucxt); + + /* + * Record the remote sequence's LSN in pg_subscription_rel and mark the + * sequence as READY. + */ + UpdateSubscriptionRelState(MySubscription->oid, seqoid, SUBREL_STATE_READY, + seqinfo->page_lsn, false); + + return COPYSEQ_SUCCESS; +} + +/* + * Copy existing data of sequences from the publisher. + */ +static void +copy_sequences(WalReceiverConn *conn) +{ + int cur_batch_base_index = 0; + int n_seqinfos = list_length(seqinfos); + List *mismatched_seqs_idx = NIL; + List *missing_seqs_idx = NIL; + List *insuffperm_seqs_idx = NIL; + StringInfoData seqstr; + StringInfoData cmd; + MemoryContext oldctx; + + initStringInfo(&seqstr); + initStringInfo(&cmd); + +#define MAX_SEQUENCES_SYNC_PER_BATCH 100 + + elog(DEBUG1, + "logical replication sequence synchronization for subscription \"%s\" - total unsynchronized: %d", + MySubscription->name, n_seqinfos); + + while (cur_batch_base_index < n_seqinfos) + { + Oid seqRow[REMOTE_SEQ_COL_COUNT] = {INT8OID, INT8OID, + BOOLOID, LSNOID, OIDOID, INT8OID, INT8OID, INT8OID, INT8OID, BOOLOID}; + int batch_size = 0; + int batch_succeeded_count = 0; + int batch_mismatched_count = 0; + int batch_skipped_count = 0; + int batch_insuffperm_count = 0; + int batch_missing_count; + Relation sequence_rel = NULL; + + WalRcvExecResult *res; + TupleTableSlot *slot; + + StartTransactionCommand(); + + for (int idx = cur_batch_base_index; idx < n_seqinfos; idx++) + { + char *nspname_literal; + char *seqname_literal; + + LogicalRepSequenceInfo *seqinfo = + (LogicalRepSequenceInfo *) list_nth(seqinfos, idx); + + if (seqstr.len > 0) + appendStringInfoString(&seqstr, ", "); + + nspname_literal = quote_literal_cstr(seqinfo->nspname); + seqname_literal = quote_literal_cstr(seqinfo->seqname); + + appendStringInfo(&seqstr, "(%s, %s, %d)", + nspname_literal, seqname_literal, idx); + + if (++batch_size == MAX_SEQUENCES_SYNC_PER_BATCH) + break; + } + + /* + * We deliberately avoid acquiring a local lock on the sequence before + * querying the publisher to prevent potential distributed deadlocks + * in bi-directional replication setups. + * + * Example scenario: + * + * - On each node, a background worker acquires a lock on a sequence + * as part of a sync operation. + * + * - Concurrently, a user transaction attempts to alter the same + * sequence, waiting on the background worker's lock. + * + * - Meanwhile, a query from the other node tries to access metadata + * that depends on the completion of the alter operation. + * + * - This creates a circular wait across nodes: + * + * Node-1: Query -> waits on Alter -> waits on Sync Worker + * + * Node-2: Query -> waits on Alter -> waits on Sync Worker + * + * Since each node only sees part of the wait graph, the deadlock may + * go undetected, leading to indefinite blocking. + * + * Note: Each entry in VALUES includes an index 'seqidx' that + * represents the sequence's position in the local 'seqinfos' list. + * This index is propagated to the query results and later used to + * directly map the fetched publisher sequence rows back to their + * corresponding local entries without relying on result order or name + * matching. + */ + appendStringInfo(&cmd, + "SELECT s.seqidx, ps.*, seq.seqtypid,\n" + " seq.seqstart, seq.seqincrement, seq.seqmin,\n" + " seq.seqmax, seq.seqcycle\n" + "FROM ( VALUES %s ) AS s (schname, seqname, seqidx)\n" + "JOIN pg_namespace n ON n.nspname = s.schname\n" + "JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = s.seqname\n" + "JOIN pg_sequence seq ON seq.seqrelid = c.oid\n" + "JOIN LATERAL pg_get_sequence_data(seq.seqrelid) AS ps ON true\n", + seqstr.data); + + res = walrcv_exec(conn, cmd.data, lengthof(seqRow), seqRow); + if (res->status != WALRCV_OK_TUPLES) + ereport(ERROR, + errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("could not fetch sequence information from the publisher: %s", + res->err)); + + slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple); + while (tuplestore_gettupleslot(res->tuplestore, true, false, slot)) + { + CopySeqResult sync_status; + LogicalRepSequenceInfo *seqinfo; + int seqidx; + + CHECK_FOR_INTERRUPTS(); + + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + } + + sync_status = get_and_validate_seq_info(slot, &sequence_rel, + &seqinfo, &seqidx); + if (sync_status == COPYSEQ_SUCCESS) + sync_status = copy_sequence(seqinfo, + sequence_rel->rd_rel->relowner); + + switch (sync_status) + { + case COPYSEQ_SUCCESS: + elog(DEBUG1, + "logical replication synchronization for subscription \"%s\", sequence \"%s.%s\" has finished", + MySubscription->name, seqinfo->nspname, + seqinfo->seqname); + batch_succeeded_count++; + break; + case COPYSEQ_MISMATCH: + + /* + * Remember mismatched sequences in a long-lived memory + * context since these will be used after the transaction + * is committed. + */ + oldctx = MemoryContextSwitchTo(ApplyContext); + mismatched_seqs_idx = lappend_int(mismatched_seqs_idx, + seqidx); + MemoryContextSwitchTo(oldctx); + batch_mismatched_count++; + break; + case COPYSEQ_INSUFFICIENT_PERM: + + /* + * Remember sequences with insufficient privileges in a + * long-lived memory context since these will be used + * after the transaction is committed. + */ + oldctx = MemoryContextSwitchTo(ApplyContext); + insuffperm_seqs_idx = lappend_int(insuffperm_seqs_idx, + seqidx); + MemoryContextSwitchTo(oldctx); + batch_insuffperm_count++; + break; + case COPYSEQ_SKIPPED: + + /* + * Concurrent removal of a sequence on the subscriber is + * treated as success, since the only viable action is to + * skip the corresponding sequence data. Missing sequences + * on the publisher are treated as ERROR. + */ + if (seqinfo->found_on_pub) + { + ereport(LOG, + errmsg("skip synchronization of sequence \"%s.%s\" because it has been dropped concurrently", + seqinfo->nspname, + seqinfo->seqname)); + batch_skipped_count++; + } + break; + } + + if (sequence_rel) + table_close(sequence_rel, NoLock); + } + + ExecDropSingleTupleTableSlot(slot); + walrcv_clear_result(res); + resetStringInfo(&seqstr); + resetStringInfo(&cmd); + + batch_missing_count = batch_size - (batch_succeeded_count + + batch_mismatched_count + + batch_insuffperm_count + + batch_skipped_count); + + elog(DEBUG1, + "logical replication sequence synchronization for subscription \"%s\" - batch #%d = %d attempted, %d succeeded, %d mismatched, %d insufficient permission, %d missing from publisher, %d skipped", + MySubscription->name, + (cur_batch_base_index / MAX_SEQUENCES_SYNC_PER_BATCH) + 1, + batch_size, batch_succeeded_count, batch_mismatched_count, + batch_insuffperm_count, batch_missing_count, batch_skipped_count); + + /* Commit this batch, and prepare for next batch */ + CommitTransactionCommand(); + + if (batch_missing_count) + { + for (int idx = cur_batch_base_index; idx < cur_batch_base_index + batch_size; idx++) + { + LogicalRepSequenceInfo *seqinfo = + (LogicalRepSequenceInfo *) list_nth(seqinfos, idx); + + /* If the sequence was not found on publisher, record it */ + if (!seqinfo->found_on_pub) + missing_seqs_idx = lappend_int(missing_seqs_idx, idx); + } + } + + /* + * cur_batch_base_index is not incremented sequentially because some + * sequences may be missing, and the number of fetched rows may not + * match the batch size. + */ + cur_batch_base_index += batch_size; + } + + /* Report mismatches, permission issues, or missing sequences */ + report_sequence_errors(mismatched_seqs_idx, insuffperm_seqs_idx, + missing_seqs_idx); +} + +/* + * Identifies sequences that require synchronization and initiates the + * synchronization process. + */ +static void +LogicalRepSyncSequences(void) +{ + char *err; + bool must_use_password; + Relation rel; + HeapTuple tup; + ScanKeyData skey[2]; + SysScanDesc scan; + Oid subid = MyLogicalRepWorker->subid; + StringInfoData app_name; + + StartTransactionCommand(); + + rel = table_open(SubscriptionRelRelationId, AccessShareLock); + + ScanKeyInit(&skey[0], + Anum_pg_subscription_rel_srsubid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(subid)); + + ScanKeyInit(&skey[1], + Anum_pg_subscription_rel_srsubstate, + BTEqualStrategyNumber, F_CHAREQ, + CharGetDatum(SUBREL_STATE_INIT)); + + scan = systable_beginscan(rel, InvalidOid, false, + NULL, 2, skey); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_subscription_rel subrel; + LogicalRepSequenceInfo *seq; + Relation sequence_rel; + MemoryContext oldctx; + + CHECK_FOR_INTERRUPTS(); + + subrel = (Form_pg_subscription_rel) GETSTRUCT(tup); + + sequence_rel = try_table_open(subrel->srrelid, RowExclusiveLock); + + /* Skip if sequence was dropped concurrently */ + if (!sequence_rel) + continue; + + /* Skip if the relation is not a sequence */ + if (sequence_rel->rd_rel->relkind != RELKIND_SEQUENCE) + { + table_close(sequence_rel, NoLock); + continue; + } + + /* + * Worker needs to process sequences across transaction boundary, so + * allocate them under long-lived context. + */ + oldctx = MemoryContextSwitchTo(ApplyContext); + + seq = palloc0_object(LogicalRepSequenceInfo); + seq->localrelid = subrel->srrelid; + seq->nspname = get_namespace_name(RelationGetNamespace(sequence_rel)); + seq->seqname = pstrdup(RelationGetRelationName(sequence_rel)); + seqinfos = lappend(seqinfos, seq); + + MemoryContextSwitchTo(oldctx); + + table_close(sequence_rel, NoLock); + } + + /* Cleanup */ + systable_endscan(scan); + table_close(rel, AccessShareLock); + + CommitTransactionCommand(); + + /* + * Exit early if no catalog entries found, likely due to concurrent drops. + */ + if (!seqinfos) + return; + + /* Is the use of a password mandatory? */ + must_use_password = MySubscription->passwordrequired && + !MySubscription->ownersuperuser; + + initStringInfo(&app_name); + appendStringInfo(&app_name, "pg_%u_sequence_sync_" UINT64_FORMAT, + MySubscription->oid, GetSystemIdentifier()); + + /* + * Establish the connection to the publisher for sequence synchronization. + */ + LogRepWorkerWalRcvConn = + walrcv_connect(MySubscription->conninfo, true, true, + must_use_password, + app_name.data, &err); + if (LogRepWorkerWalRcvConn == NULL) + ereport(ERROR, + errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("sequencesync worker for subscription \"%s\" could not connect to the publisher: %s", + MySubscription->name, err)); + + pfree(app_name.data); + + copy_sequences(LogRepWorkerWalRcvConn); +} + +/* + * Execute the initial sync with error handling. Disable the subscription, + * if required. + * + * Note that we don't handle FATAL errors which are probably because of system + * resource error and are not repeatable. + */ +static void +start_sequence_sync(void) +{ + Assert(am_sequencesync_worker()); + + PG_TRY(); + { + /* Call initial sync. */ + LogicalRepSyncSequences(); + } + PG_CATCH(); + { + if (MySubscription->disableonerr) + DisableSubscriptionAndExit(); + else + { + /* + * Report the worker failed during sequence synchronization. Abort + * the current transaction so that the stats message is sent in an + * idle state. + */ + AbortOutOfAnyTransaction(); + pgstat_report_subscription_error(MySubscription->oid); + + PG_RE_THROW(); + } + } + PG_END_TRY(); +} + +/* Logical Replication sequencesync worker entry point */ +void +SequenceSyncWorkerMain(Datum main_arg) +{ + int worker_slot = DatumGetInt32(main_arg); + + SetupApplyOrSyncWorker(worker_slot); + + start_sequence_sync(); + + FinishSyncWorker(); +} diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index 656e66e0ae0a1..ad3747e598c77 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -3,7 +3,7 @@ * Functionality for synchronizing slots to a standby server from the * primary server. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/slotsync.c @@ -34,11 +34,22 @@ * RS_TEMPORARY. Once the decoding from corresponding LSNs can reach a * consistent point, they will be marked as RS_PERSISTENT. * + * If the WAL prior to the remote slot's confirmed_flush_lsn has not been + * flushed on the standby, the slot is marked as RS_TEMPORARY. Once the standby + * catches up and flushes that WAL, the slot will be marked as RS_PERSISTENT. + * * The slot sync worker waits for some time before the next synchronization, * with the duration varying based on whether any slots were updated during * the last cycle. Refer to the comments above wait_for_slot_activity() for * more details. * + * If the SQL function pg_sync_replication_slots() is used to sync the slots, + * and if the slots are not ready to be synced and are marked as RS_TEMPORARY + * because of any of the reasons mentioned above, then the SQL function also + * waits and retries until the slots are marked as RS_PERSISTENT (which means + * sync-ready). Refer to the comments in SyncReplicationSlots() for more + * details. + * * Any standby synchronized slots will be dropped if they no longer need * to be synchronized. See comment atop drop_local_obsolete_slots() for more * details. @@ -52,7 +63,6 @@ #include "access/xlog_internal.h" #include "access/xlogrecovery.h" #include "catalog/pg_database.h" -#include "commands/dbcommands.h" #include "libpq/pqsignal.h" #include "pgstat.h" #include "postmaster/interrupt.h" @@ -63,25 +73,32 @@ #include "storage/lmgr.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/subsystems.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" +#include "utils/memutils.h" #include "utils/pg_lsn.h" #include "utils/ps_status.h" #include "utils/timeout.h" +#include "utils/wait_event.h" /* * Struct for sharing information to control slot synchronization. * - * The slot sync worker's pid is needed by the startup process to shut it - * down during promotion. The startup process shuts down the slot sync worker - * and also sets stopSignaled=true to handle the race condition when the - * postmaster has not noticed the promotion yet and thus may end up restarting - * the slot sync worker. If stopSignaled is set, the worker will exit in such a - * case. The SQL function pg_sync_replication_slots() will also error out if - * this flag is set. Note that we don't need to reset this variable as after - * promotion the slot sync worker won't be restarted because the pmState - * changes to PM_RUN from PM_HOT_STANDBY and we don't support demoting - * primary without restarting the server. See LaunchMissingBackgroundProcesses. + * The 'pid' is either the slot sync worker's pid or the backend's pid running + * the SQL function pg_sync_replication_slots(). On promotion, the startup + * process sets 'stopSignaled' and uses this 'pid' to signal the synchronizing + * process with PROCSIG_SLOTSYNC_MESSAGE and also to wake it up so that the + * process can immediately stop its synchronizing work. + * Setting 'stopSignaled' on the other hand is used to handle the race + * condition when the postmaster has not noticed the promotion yet and thus may + * end up restarting the slot sync worker. If 'stopSignaled' is set, the worker + * will exit in such a case. The SQL function pg_sync_replication_slots() will + * also error out if this flag is set. Note that we don't need to reset this + * variable as after promotion the slot sync worker won't be restarted because + * the pmState changes to PM_RUN from PM_HOT_STANDBY and we don't support + * demoting primary without restarting the server. + * See LaunchMissingBackgroundProcesses. * * The 'syncing' flag is needed to prevent concurrent slot syncs to avoid slot * overwrites. @@ -103,6 +120,14 @@ typedef struct SlotSyncCtxStruct static SlotSyncCtxStruct *SlotSyncCtx = NULL; +static void SlotSyncShmemRequest(void *arg); +static void SlotSyncShmemInit(void *arg); + +const ShmemCallbacks SlotSyncShmemCallbacks = { + .request_fn = SlotSyncShmemRequest, + .init_fn = SlotSyncShmemInit, +}; + /* GUC variable */ bool sync_replication_slots = false; @@ -126,6 +151,13 @@ static long sleep_ms = MIN_SLOTSYNC_WORKER_NAPTIME_MS; */ static bool syncing_slots = false; +/* + * Interrupt flag set when PROCSIG_SLOTSYNC_MESSAGE is received, asking the + * slotsync worker or pg_sync_replication_slots() to stop because + * standby promotion has been triggered. + */ +volatile sig_atomic_t SlotSyncShutdownPending = false; + /* * Structure to hold information fetched from the primary server about a logical * replication slot. @@ -149,36 +181,75 @@ typedef struct RemoteSlot static void slotsync_failure_callback(int code, Datum arg); static void update_synced_slots_inactive_since(void); +/* + * Update slot sync skip stats. This function requires the caller to acquire + * the slot. + */ +static void +update_slotsync_skip_stats(SlotSyncSkipReason skip_reason) +{ + ReplicationSlot *slot; + + Assert(MyReplicationSlot); + + slot = MyReplicationSlot; + + /* + * Update the slot sync related stats in pg_stat_replication_slots when a + * slot sync is skipped + */ + if (skip_reason != SS_SKIP_NONE) + pgstat_report_replslotsync(slot); + + /* Update the slot sync skip reason */ + if (slot->slotsync_skip_reason != skip_reason) + { + SpinLockAcquire(&slot->mutex); + slot->slotsync_skip_reason = skip_reason; + SpinLockRelease(&slot->mutex); + } +} + /* * If necessary, update the local synced slot's metadata based on the data * from the remote slot. * * If no update was needed (the data of the remote slot is the same as the * local slot) return false, otherwise true. - * - * *found_consistent_snapshot will be true iff the remote slot's LSN or xmin is - * modified, and decoding from the corresponding LSN's can reach a - * consistent snapshot. - * - * *remote_slot_precedes will be true if the remote slot's LSN or xmin - * precedes locally reserved position. */ static bool -update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid, - bool *found_consistent_snapshot, - bool *remote_slot_precedes) +update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid) { ReplicationSlot *slot = MyReplicationSlot; bool updated_xmin_or_lsn = false; bool updated_config = false; + SlotSyncSkipReason skip_reason = SS_SKIP_NONE; + XLogRecPtr latestFlushPtr = GetStandbyFlushRecPtr(NULL); Assert(slot->data.invalidated == RS_INVAL_NONE); - if (found_consistent_snapshot) - *found_consistent_snapshot = false; + /* + * Make sure that concerned WAL is received and flushed before syncing + * slot to target lsn received from the primary server. + */ + if (remote_slot->confirmed_lsn > latestFlushPtr) + { + update_slotsync_skip_stats(SS_SKIP_WAL_NOT_FLUSHED); + + /* + * Can get here only if GUC 'synchronized_standby_slots' on the + * primary server was not configured correctly. + */ + ereport(LOG, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("skipping slot synchronization because the received slot sync" + " LSN %X/%08X for slot \"%s\" is ahead of the standby position %X/%08X", + LSN_FORMAT_ARGS(remote_slot->confirmed_lsn), + remote_slot->name, + LSN_FORMAT_ARGS(latestFlushPtr))); - if (remote_slot_precedes) - *remote_slot_precedes = false; + return false; + } /* * Don't overwrite if we already have a newer catalog_xmin and @@ -188,6 +259,9 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid, TransactionIdPrecedes(remote_slot->catalog_xmin, slot->data.catalog_xmin)) { + /* Update slot sync skip stats */ + update_slotsync_skip_stats(SS_SKIP_WAL_OR_ROWS_REMOVED); + /* * This can happen in following situations: * @@ -211,17 +285,14 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid, * impact the users, so we used DEBUG1 level to log the message. */ ereport(slot->data.persistency == RS_TEMPORARY ? LOG : DEBUG1, - errmsg("could not synchronize replication slot \"%s\" because remote slot precedes local slot", + errmsg("could not synchronize replication slot \"%s\"", remote_slot->name), - errdetail("The remote slot has LSN %X/%X and catalog xmin %u, but the local slot has LSN %X/%X and catalog xmin %u.", + errdetail("Synchronization could lead to data loss, because the remote slot needs WAL at LSN %X/%08X and catalog xmin %u, but the standby has LSN %X/%08X and catalog xmin %u.", LSN_FORMAT_ARGS(remote_slot->restart_lsn), remote_slot->catalog_xmin, LSN_FORMAT_ARGS(slot->data.restart_lsn), slot->data.catalog_xmin)); - if (remote_slot_precedes) - *remote_slot_precedes = true; - /* * Skip updating the configuration. This is required to avoid syncing * two_phase_at without syncing confirmed_lsn. Otherwise, the prepared @@ -262,27 +333,58 @@ update_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid, slot->data.catalog_xmin = remote_slot->catalog_xmin; SpinLockRelease(&slot->mutex); - if (found_consistent_snapshot) - *found_consistent_snapshot = true; + updated_xmin_or_lsn = true; } else { + bool found_consistent_snapshot; + XLogRecPtr old_confirmed_lsn = slot->data.confirmed_flush; + XLogRecPtr old_restart_lsn = slot->data.restart_lsn; + XLogRecPtr old_catalog_xmin = slot->data.catalog_xmin; + LogicalSlotAdvanceAndCheckSnapState(remote_slot->confirmed_lsn, - found_consistent_snapshot); + &found_consistent_snapshot); /* Sanity check */ if (slot->data.confirmed_flush != remote_slot->confirmed_lsn) ereport(ERROR, errmsg_internal("synchronized confirmed_flush for slot \"%s\" differs from remote slot", remote_slot->name), - errdetail_internal("Remote slot has LSN %X/%X but local slot has LSN %X/%X.", + errdetail_internal("Remote slot has LSN %X/%08X but local slot has LSN %X/%08X.", LSN_FORMAT_ARGS(remote_slot->confirmed_lsn), LSN_FORMAT_ARGS(slot->data.confirmed_flush))); - } - updated_xmin_or_lsn = true; + /* + * If we can't reach a consistent snapshot, the slot won't be + * persisted. See update_and_persist_local_synced_slot(). + */ + if (!found_consistent_snapshot) + { + Assert(MyReplicationSlot->data.persistency == RS_TEMPORARY); + + ereport(LOG, + errmsg("could not synchronize replication slot \"%s\"", + remote_slot->name), + errdetail("Synchronization could lead to data loss, because the standby could not build a consistent snapshot to decode WALs at LSN %X/%08X.", + LSN_FORMAT_ARGS(slot->data.restart_lsn))); + + skip_reason = SS_SKIP_NO_CONSISTENT_SNAPSHOT; + } + + /* + * It is possible that the slot's xmin or LSNs are not updated, + * when the synced slot has reached consistent snapshot state or + * cannot build one at all. + */ + updated_xmin_or_lsn = (old_confirmed_lsn != slot->data.confirmed_flush || + old_restart_lsn != slot->data.restart_lsn || + old_catalog_xmin != slot->data.catalog_xmin); + } } + /* Update slot sync skip stats */ + update_slotsync_skip_stats(skip_reason); + if (remote_dbid != slot->data.database || remote_slot->two_phase != slot->data.two_phase || remote_slot->failover != slot->data.failover || @@ -352,7 +454,7 @@ get_local_synced_slots(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -483,70 +585,71 @@ drop_local_obsolete_slots(List *remote_slot_list) * Reserve WAL for the currently active local slot using the specified WAL * location (restart_lsn). * - * If the given WAL location has been removed, reserve WAL using the oldest - * existing WAL segment. + * If the given WAL location has been removed or is at risk of removal, + * reserve WAL using the oldest segment that is non-removable. */ static void reserve_wal_for_local_slot(XLogRecPtr restart_lsn) { - XLogSegNo oldest_segno; + XLogRecPtr slot_min_lsn; + XLogRecPtr min_safe_lsn; XLogSegNo segno; ReplicationSlot *slot = MyReplicationSlot; Assert(slot != NULL); - Assert(XLogRecPtrIsInvalid(slot->data.restart_lsn)); - - while (true) - { - SpinLockAcquire(&slot->mutex); - slot->data.restart_lsn = restart_lsn; - SpinLockRelease(&slot->mutex); - - /* Prevent WAL removal as fast as possible */ - ReplicationSlotsComputeRequiredLSN(); + Assert(!XLogRecPtrIsValid(slot->data.restart_lsn)); - XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size); + /* + * Acquire an exclusive lock to prevent the checkpoint process from + * concurrently calculating the minimum slot LSN (see + * CheckPointReplicationSlots), ensuring that if WAL reservation occurs + * first, the checkpoint must wait for the restart_lsn update before + * calculating the minimum LSN. + * + * Note: Unlike ReplicationSlotReserveWal(), this lock does not protect a + * newly synced slot from being invalidated if a concurrent checkpoint has + * invoked CheckPointReplicationSlots() before the WAL reservation here. + * This can happen because the initial restart_lsn received from the + * remote server can precede the redo pointer. Therefore, when selecting + * the initial restart_lsn, we consider using the redo pointer or the + * minimum slot LSN (if those values are greater than the remote + * restart_lsn) instead of relying solely on the remote value. + */ + LWLockAcquire(ReplicationSlotAllocationLock, LW_EXCLUSIVE); - /* - * Find the oldest existing WAL segment file. - * - * Normally, we can determine it by using the last removed segment - * number. However, if no WAL segment files have been removed by a - * checkpoint since startup, we need to search for the oldest segment - * file from the current timeline existing in XLOGDIR. - * - * XXX: Currently, we are searching for the oldest segment in the - * current timeline as there is less chance of the slot's restart_lsn - * from being some prior timeline, and even if it happens, in the - * worst case, we will wait to sync till the slot's restart_lsn moved - * to the current timeline. - */ - oldest_segno = XLogGetLastRemovedSegno() + 1; + /* + * Determine the minimum non-removable LSN by comparing the redo pointer + * with the minimum slot LSN. + * + * The minimum slot LSN is considered because the redo pointer advances at + * every checkpoint, even when replication slots are present on the + * standby. In such scenarios, the redo pointer can exceed the remote + * restart_lsn, while WALs preceding the remote restart_lsn remain + * protected by a local replication slot. + */ + min_safe_lsn = GetRedoRecPtr(); + slot_min_lsn = XLogGetReplicationSlotMinimumLSN(); - if (oldest_segno == 1) - { - TimeLineID cur_timeline; + if (XLogRecPtrIsValid(slot_min_lsn) && min_safe_lsn > slot_min_lsn) + min_safe_lsn = slot_min_lsn; - GetWalRcvFlushRecPtr(NULL, &cur_timeline); - oldest_segno = XLogGetOldestSegno(cur_timeline); - } + /* + * If the minimum safe LSN is greater than the given restart_lsn, use it + * as the initial restart_lsn for the newly synced slot. Otherwise, use + * the given remote restart_lsn. + */ + SpinLockAcquire(&slot->mutex); + slot->data.restart_lsn = Max(restart_lsn, min_safe_lsn); + SpinLockRelease(&slot->mutex); - elog(DEBUG1, "segno: " UINT64_FORMAT " of purposed restart_lsn for the synced slot, oldest_segno: " UINT64_FORMAT " available", - segno, oldest_segno); + ReplicationSlotsComputeRequiredLSN(); - /* - * If all required WAL is still there, great, otherwise retry. The - * slot should prevent further removal of WAL, unless there's a - * concurrent ReplicationSlotsComputeRequiredLSN() after we've written - * the new restart_lsn above, so normally we should never need to loop - * more than twice. - */ - if (segno >= oldest_segno) - break; + XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size); + if (XLogGetLastRemovedSegno() >= segno) + elog(ERROR, "WAL required by replication slot %s has been removed concurrently", + NameStr(slot->data.name)); - /* Retry using the location of the oldest wal segment */ - XLogSegNoOffsetToRecPtr(oldest_segno, 0, wal_segment_size, restart_lsn); - } + LWLockRelease(ReplicationSlotAllocationLock); } /* @@ -554,47 +657,44 @@ reserve_wal_for_local_slot(XLogRecPtr restart_lsn) * local ones, then update the LSNs and persist the local synced slot for * future synchronization; otherwise, do nothing. * + * *slot_persistence_pending is set to true if any of the slots fail to + * persist. + * * Return true if the slot is marked as RS_PERSISTENT (sync-ready), otherwise * false. */ static bool -update_and_persist_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid) +update_and_persist_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid, + bool *slot_persistence_pending) { ReplicationSlot *slot = MyReplicationSlot; - bool found_consistent_snapshot = false; - bool remote_slot_precedes = false; - (void) update_local_synced_slot(remote_slot, remote_dbid, - &found_consistent_snapshot, - &remote_slot_precedes); + /* Slotsync skip stats are handled in function update_local_synced_slot() */ + (void) update_local_synced_slot(remote_slot, remote_dbid); /* - * Check if the primary server has caught up. Refer to the comment atop - * the file for details on this check. + * Check if the slot cannot be synchronized. Refer to the comment atop the + * file for details on this check. */ - if (remote_slot_precedes) + if (slot->slotsync_skip_reason != SS_SKIP_NONE) { /* - * The remote slot didn't catch up to locally reserved position. + * We reach this point when the remote slot didn't catch up to locally + * reserved position, or it cannot reach the consistent point from the + * restart_lsn, or the WAL prior to the remote confirmed flush LSN has + * not been received and flushed. + * + * We do not drop the slot because the restart_lsn and confirmed_lsn + * can be ahead of the current location when recreating the slot in + * the next cycle. It may take more time to create such a slot or + * reach the consistent point. Therefore, we keep this slot and + * attempt the synchronization in the next cycle. * - * We do not drop the slot because the restart_lsn can be ahead of the - * current location when recreating the slot in the next cycle. It may - * take more time to create such a slot. Therefore, we keep this slot - * and attempt the synchronization in the next cycle. + * We also update the slot_persistence_pending parameter, so the SQL + * function can retry. */ - return false; - } - - /* - * Don't persist the slot if it cannot reach the consistent point from the - * restart_lsn. See comments atop this file. - */ - if (!found_consistent_snapshot) - { - ereport(LOG, - errmsg("could not synchronize replication slot \"%s\"", remote_slot->name), - errdetail("Logical decoding could not find consistent point from local slot's LSN %X/%X.", - LSN_FORMAT_ARGS(slot->data.restart_lsn))); + if (slot_persistence_pending) + *slot_persistence_pending = true; return false; } @@ -619,37 +719,18 @@ update_and_persist_local_synced_slot(RemoteSlot *remote_slot, Oid remote_dbid) * updated. The slot is then persisted and is considered as sync-ready for * periodic syncs. * + * *slot_persistence_pending is set to true if any of the slots fail to + * persist. + * * Returns TRUE if the local slot is updated. */ static bool -synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) +synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid, + bool *slot_persistence_pending) { ReplicationSlot *slot; - XLogRecPtr latestFlushPtr; bool slot_updated = false; - /* - * Make sure that concerned WAL is received and flushed before syncing - * slot to target lsn received from the primary server. - */ - latestFlushPtr = GetStandbyFlushRecPtr(NULL); - if (remote_slot->confirmed_lsn > latestFlushPtr) - { - /* - * Can get here only if GUC 'synchronized_standby_slots' on the - * primary server was not configured correctly. - */ - ereport(AmLogicalSlotSyncWorkerProcess() ? LOG : ERROR, - errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("skipping slot synchronization because the received slot sync" - " LSN %X/%X for slot \"%s\" is ahead of the standby position %X/%X", - LSN_FORMAT_ARGS(remote_slot->confirmed_lsn), - remote_slot->name, - LSN_FORMAT_ARGS(latestFlushPtr))); - - return false; - } - /* Search for the named slot */ if ((slot = SearchNamedReplicationSlot(remote_slot->name, true))) { @@ -708,6 +789,8 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) /* Skip the sync of an invalidated slot */ if (slot->data.invalidated != RS_INVAL_NONE) { + update_slotsync_skip_stats(SS_SKIP_INVALID); + ReplicationSlotRelease(); return slot_updated; } @@ -716,7 +799,8 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) if (slot->data.persistency == RS_TEMPORARY) { slot_updated = update_and_persist_local_synced_slot(remote_slot, - remote_dbid); + remote_dbid, + slot_persistence_pending); } /* Slot ready for sync, so sync it. */ @@ -733,12 +817,11 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) ereport(ERROR, errmsg_internal("cannot synchronize local slot \"%s\"", remote_slot->name), - errdetail_internal("Local slot's start streaming location LSN(%X/%X) is ahead of remote slot's LSN(%X/%X).", + errdetail_internal("Local slot's start streaming location LSN(%X/%08X) is ahead of remote slot's LSN(%X/%08X).", LSN_FORMAT_ARGS(slot->data.confirmed_flush), LSN_FORMAT_ARGS(remote_slot->confirmed_lsn))); - slot_updated = update_local_synced_slot(remote_slot, remote_dbid, - NULL, NULL); + slot_updated = update_local_synced_slot(remote_slot, remote_dbid); } } /* Otherwise create the slot first. */ @@ -760,6 +843,7 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) */ ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY, remote_slot->two_phase, + false, remote_slot->failover, true); @@ -776,6 +860,7 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) reserve_wal_for_local_slot(remote_slot->restart_lsn); + LWLockAcquire(ReplicationSlotControlLock, LW_EXCLUSIVE); LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); xmin_horizon = GetOldestSafeDecodingTransactionId(true); SpinLockAcquire(&slot->mutex); @@ -784,8 +869,10 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) SpinLockRelease(&slot->mutex); ReplicationSlotsComputeRequiredXmin(true); LWLockRelease(ProcArrayLock); + LWLockRelease(ReplicationSlotControlLock); - update_and_persist_local_synced_slot(remote_slot, remote_dbid); + update_and_persist_local_synced_slot(remote_slot, remote_dbid, + slot_persistence_pending); slot_updated = true; } @@ -796,15 +883,16 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid) } /* - * Synchronize slots. + * Fetch remote slots. * - * Gets the failover logical slots info from the primary server and updates - * the slots locally. Creates the slots if not present on the standby. + * If slot_names is NIL, fetches all failover logical slots from the + * primary server, otherwise fetches only the ones with names in slot_names. * - * Returns TRUE if any of the slots gets updated in this sync-cycle. + * Returns a list of remote slot information structures, or NIL if none + * are found. */ -static bool -synchronize_slots(WalReceiverConn *wrconn) +static List * +fetch_remote_slots(WalReceiverConn *wrconn, List *slot_names) { #define SLOTSYNC_COLUMN_COUNT 10 Oid slotRow[SLOTSYNC_COLUMN_COUNT] = {TEXTOID, TEXTOID, LSNOID, @@ -813,34 +901,50 @@ synchronize_slots(WalReceiverConn *wrconn) WalRcvExecResult *res; TupleTableSlot *tupslot; List *remote_slot_list = NIL; - bool some_slot_updated = false; - bool started_tx = false; - const char *query = "SELECT slot_name, plugin, confirmed_flush_lsn," - " restart_lsn, catalog_xmin, two_phase, two_phase_at, failover," - " database, invalidation_reason" - " FROM pg_catalog.pg_replication_slots" - " WHERE failover and NOT temporary"; - - /* The syscache access in walrcv_exec() needs a transaction env. */ - if (!IsTransactionState()) + StringInfoData query; + + initStringInfo(&query); + appendStringInfoString(&query, + "SELECT slot_name, plugin, confirmed_flush_lsn," + " restart_lsn, catalog_xmin, two_phase," + " two_phase_at, failover," + " database, invalidation_reason" + " FROM pg_catalog.pg_replication_slots" + " WHERE failover and NOT temporary"); + + if (slot_names != NIL) { - StartTransactionCommand(); - started_tx = true; + bool first_slot = true; + + /* + * Construct the query to fetch only the specified slots + */ + appendStringInfoString(&query, " AND slot_name IN ("); + + foreach_ptr(char, slot_name, slot_names) + { + if (!first_slot) + appendStringInfoString(&query, ", "); + + appendStringInfoString(&query, quote_literal_cstr(slot_name)); + first_slot = false; + } + appendStringInfoChar(&query, ')'); } /* Execute the query */ - res = walrcv_exec(wrconn, query, SLOTSYNC_COLUMN_COUNT, slotRow); + res = walrcv_exec(wrconn, query.data, SLOTSYNC_COLUMN_COUNT, slotRow); + pfree(query.data); if (res->status != WALRCV_OK_TUPLES) ereport(ERROR, errmsg("could not fetch failover logical slots info from the primary server: %s", res->err)); - /* Construct the remote_slot tuple and synchronize each slot locally */ tupslot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple); while (tuplestore_gettupleslot(res->tuplestore, true, false, tupslot)) { bool isnull; - RemoteSlot *remote_slot = palloc0(sizeof(RemoteSlot)); + RemoteSlot *remote_slot = palloc0_object(RemoteSlot); Datum d; int col = 0; @@ -900,8 +1004,8 @@ synchronize_slots(WalReceiverConn *wrconn) * pg_replication_slots view, then we can avoid fetching RS_EPHEMERAL * slots in the first place. */ - if ((XLogRecPtrIsInvalid(remote_slot->restart_lsn) || - XLogRecPtrIsInvalid(remote_slot->confirmed_lsn) || + if ((!XLogRecPtrIsValid(remote_slot->restart_lsn) || + !XLogRecPtrIsValid(remote_slot->confirmed_lsn) || !TransactionIdIsValid(remote_slot->catalog_xmin)) && remote_slot->invalidated == RS_INVAL_NONE) pfree(remote_slot); @@ -912,6 +1016,29 @@ synchronize_slots(WalReceiverConn *wrconn) ExecClearTuple(tupslot); } + walrcv_clear_result(res); + + return remote_slot_list; +} + +/* + * Synchronize slots. + * + * This function takes a list of remote slots and synchronizes them locally. It + * creates the slots if not present on the standby and updates existing ones. + * + * If slot_persistence_pending is not NULL, it will be set to true if one or + * more slots could not be persisted. This allows callers such as + * SyncReplicationSlots() to retry those slots. + * + * Returns TRUE if any of the slots gets updated in this sync-cycle. + */ +static bool +synchronize_slots(WalReceiverConn *wrconn, List *remote_slot_list, + bool *slot_persistence_pending) +{ + bool some_slot_updated = false; + /* Drop local slots that no longer need to be synced. */ drop_local_obsolete_slots(remote_slot_list); @@ -927,19 +1054,12 @@ synchronize_slots(WalReceiverConn *wrconn) */ LockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock); - some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid); + some_slot_updated |= synchronize_one_slot(remote_slot, remote_dbid, + slot_persistence_pending); UnlockSharedObject(DatabaseRelationId, remote_dbid, 0, AccessShareLock); } - /* We are done, free remote_slot_list elements */ - list_free_deep(remote_slot_list); - - walrcv_clear_result(res); - - if (started_tx) - CommitTransactionCommand(); - return some_slot_updated; } @@ -1058,15 +1178,17 @@ bool ValidateSlotSyncParams(int elevel) { /* - * Logical slot sync/creation requires wal_level >= logical. - * - * Since altering the wal_level requires a server restart, so error out in - * this case regardless of elevel provided by caller. + * Logical slot sync/creation requires logical decoding to be enabled. */ - if (wal_level < WAL_LEVEL_LOGICAL) - ereport(ERROR, + if (!IsLogicalDecodingEnabled()) + { + ereport(elevel, errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("replication slot synchronization requires \"wal_level\" >= \"logical\"")); + errmsg("replication slot synchronization requires \"effective_wal_level\" >= \"logical\" on the primary"), + errhint("To enable logical decoding on primary, set \"wal_level\" >= \"logical\" or create at least one logical slot when \"wal_level\" = \"replica\".")); + + return false; + } /* * A physical replication slot(primary_slot_name) is required on the @@ -1116,10 +1238,10 @@ ValidateSlotSyncParams(int elevel) } /* - * Re-read the config file. + * Re-read the config file for slot synchronization. * - * Exit if any of the slot sync GUCs have changed. The postmaster will - * restart it. + * Exit or throw error if relevant GUCs have changed depending on whether + * called from slot sync worker or from the SQL function pg_sync_replication_slots() */ static void slotsync_reread_config(void) @@ -1130,8 +1252,11 @@ slotsync_reread_config(void) bool old_hot_standby_feedback = hot_standby_feedback; bool conninfo_changed; bool primary_slotname_changed; + bool is_slotsync_worker = AmLogicalSlotSyncWorkerProcess(); + bool parameter_changed = false; - Assert(sync_replication_slots); + if (is_slotsync_worker) + Assert(sync_replication_slots); ConfigReloadPending = false; ProcessConfigFile(PGC_SIGHUP); @@ -1143,48 +1268,105 @@ slotsync_reread_config(void) if (old_sync_replication_slots != sync_replication_slots) { - ereport(LOG, - /* translator: %s is a GUC variable name */ - errmsg("replication slot synchronization worker will shut down because \"%s\" is disabled", "sync_replication_slots")); - proc_exit(0); - } + if (is_slotsync_worker) + { + ereport(LOG, + /* translator: %s is a GUC variable name */ + errmsg("replication slot synchronization worker will stop because \"%s\" is disabled", + "sync_replication_slots")); + + proc_exit(0); + } - if (conninfo_changed || - primary_slotname_changed || - (old_hot_standby_feedback != hot_standby_feedback)) + parameter_changed = true; + } + else { - ereport(LOG, - errmsg("replication slot synchronization worker will restart because of a parameter change")); + if (conninfo_changed || + primary_slotname_changed || + (old_hot_standby_feedback != hot_standby_feedback)) + { - /* - * Reset the last-start time for this worker so that the postmaster - * can restart it without waiting for SLOTSYNC_RESTART_INTERVAL_SEC. - */ - SlotSyncCtx->last_start_time = 0; + if (is_slotsync_worker) + { + ereport(LOG, + errmsg("replication slot synchronization worker will restart because of a parameter change")); - proc_exit(0); + /* + * Reset the last-start time for this worker so that the + * postmaster can restart it without waiting for + * SLOTSYNC_RESTART_INTERVAL_SEC. + */ + SlotSyncCtx->last_start_time = 0; + + proc_exit(0); + } + + parameter_changed = true; + } + } + + /* + * If we have reached here with a parameter change, we must be running in + * SQL function, emit error in such a case. + */ + if (parameter_changed) + { + Assert(!is_slotsync_worker); + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("replication slot synchronization will stop because of a parameter change")); } } /* - * Interrupt handler for main loop of slot sync worker. + * Handle receipt of an interrupt indicating a slotsync shutdown message. + * + * This is called within the SIGUSR1 handler. All we do here is set a flag + * that will cause the next CHECK_FOR_INTERRUPTS() to invoke + * ProcessSlotSyncMessage(). */ -static void -ProcessSlotSyncInterrupts(WalReceiverConn *wrconn) +void +HandleSlotSyncMessageInterrupt(void) +{ + InterruptPending = true; + SlotSyncShutdownPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * Handle a PROCSIG_SLOTSYNC_MESSAGE signal, called from ProcessInterrupts(). + * + * If the current process is the slotsync background worker, log a message + * and exit cleanly. If it is a backend executing pg_sync_replication_slots(), + * raise an error, unless the sync has already finished, in which case there + * is no need to interrupt the caller. + */ +void +ProcessSlotSyncMessage(void) { - CHECK_FOR_INTERRUPTS(); + SlotSyncShutdownPending = false; - if (ShutdownRequestPending) + if (AmLogicalSlotSyncWorkerProcess()) { ereport(LOG, - errmsg("replication slot synchronization worker is shutting down on receiving SIGINT")); - + errmsg("replication slot synchronization worker will stop because promotion is triggered")); proc_exit(0); } + else + { + /* + * If sync has already completed, there is no need to interrupt the + * caller with an error. + */ + if (!IsSyncingReplicationSlots()) + return; - if (ConfigReloadPending) - slotsync_reread_config(); + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("replication slot synchronization will stop because promotion is triggered")); + } } /* @@ -1283,27 +1465,40 @@ wait_for_slot_activity(bool some_slot_updated) } /* - * Emit an error if a promotion or a concurrent sync call is in progress. + * Emit an error if a concurrent sync call is in progress. * Otherwise, advertise that a sync is in progress. */ static void -check_and_set_sync_info(pid_t worker_pid) +check_and_set_sync_info(pid_t sync_process_pid) { SpinLockAcquire(&SlotSyncCtx->mutex); - /* The worker pid must not be already assigned in SlotSyncCtx */ - Assert(worker_pid == InvalidPid || SlotSyncCtx->pid == InvalidPid); - /* - * Emit an error if startup process signaled the slot sync machinery to - * stop. See comments atop SlotSyncCtxStruct. + * Exit immediately if promotion has been triggered. This guards against + * a new worker (or a call to pg_sync_replication_slots()) that starts + * after the old worker was stopped by ShutDownSlotSync(). */ if (SlotSyncCtx->stopSignaled) { SpinLockRelease(&SlotSyncCtx->mutex); - ereport(ERROR, - errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot synchronize replication slots when standby promotion is ongoing")); + + if (AmLogicalSlotSyncWorkerProcess()) + { + ereport(DEBUG1, + errmsg("replication slot synchronization worker will not start because promotion was triggered")); + + proc_exit(0); + } + else + { + /* + * For the backend executing SQL function + * pg_sync_replication_slots(). + */ + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("replication slot synchronization will not start because promotion was triggered")); + } } if (SlotSyncCtx->syncing) @@ -1314,13 +1509,16 @@ check_and_set_sync_info(pid_t worker_pid) errmsg("cannot synchronize replication slots concurrently")); } + /* The pid must not be already assigned in SlotSyncCtx */ + Assert(SlotSyncCtx->pid == InvalidPid); + SlotSyncCtx->syncing = true; /* * Advertise the required PID so that the startup process can kill the - * slot sync worker on promotion. + * slot sync process on promotion. */ - SlotSyncCtx->pid = worker_pid; + SlotSyncCtx->pid = sync_process_pid; SpinLockRelease(&SlotSyncCtx->mutex); @@ -1331,20 +1529,24 @@ check_and_set_sync_info(pid_t worker_pid) * Reset syncing flag. */ static void -reset_syncing_flag() +reset_syncing_flag(void) { SpinLockAcquire(&SlotSyncCtx->mutex); SlotSyncCtx->syncing = false; + SlotSyncCtx->pid = InvalidPid; SpinLockRelease(&SlotSyncCtx->mutex); syncing_slots = false; -}; +} /* * The main loop of our worker process. * * It connects to the primary server, fetches logical failover slots * information periodically in order to create and sync the slots. + * + * Note: If any changes are made here, check if the corresponding SQL + * function logic in SyncReplicationSlots() also needs to be changed. */ void ReplSlotSyncWorkerMain(const void *startup_data, size_t startup_data_len) @@ -1357,7 +1559,12 @@ ReplSlotSyncWorkerMain(const void *startup_data, size_t startup_data_len) Assert(startup_data_len == 0); - MyBackendType = B_SLOTSYNC_WORKER; + /* Release postmaster's working memory context */ + if (PostmasterContext) + { + MemoryContextDelete(PostmasterContext); + PostmasterContext = NULL; + } init_ps_display(NULL); @@ -1409,13 +1616,13 @@ ReplSlotSyncWorkerMain(const void *startup_data, size_t startup_data_len) /* Setup signal handling */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGINT, SignalHandlerForShutdownRequest); + pqsignal(SIGINT, StatementCancelHandler); pqsignal(SIGTERM, die); pqsignal(SIGFPE, FloatExceptionHandler); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGUSR2, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); + pqsignal(SIGCHLD, PG_SIG_DFL); check_and_set_sync_info(MyProcPid); @@ -1477,7 +1684,6 @@ ReplSlotSyncWorkerMain(const void *startup_data, size_t startup_data_len) */ wrconn = walrcv_connect(PrimaryConnInfo, false, false, false, app_name.data, &err); - pfree(app_name.data); if (!wrconn) ereport(ERROR, @@ -1485,6 +1691,8 @@ ReplSlotSyncWorkerMain(const void *startup_data, size_t startup_data_len) errmsg("synchronization worker \"%s\" could not connect to the primary server: %s", app_name.data, err)); + pfree(app_name.data); + /* * Register the disconnection callback. * @@ -1505,17 +1713,38 @@ ReplSlotSyncWorkerMain(const void *startup_data, size_t startup_data_len) for (;;) { bool some_slot_updated = false; + bool started_tx = false; + List *remote_slots; + + CHECK_FOR_INTERRUPTS(); + + if (ConfigReloadPending) + slotsync_reread_config(); + + /* + * The syscache access in fetch_remote_slots() needs a transaction + * env. + */ + if (!IsTransactionState()) + { + StartTransactionCommand(); + started_tx = true; + } - ProcessSlotSyncInterrupts(wrconn); + remote_slots = fetch_remote_slots(wrconn, NIL); + some_slot_updated = synchronize_slots(wrconn, remote_slots, NULL); + list_free_deep(remote_slots); - some_slot_updated = synchronize_slots(wrconn); + if (started_tx) + CommitTransactionCommand(); wait_for_slot_activity(some_slot_updated); } /* * The slot sync worker can't get here because it will only stop when it - * receives a SIGINT from the startup process, or when there is an error. + * receives a stop request from the startup process, or when there is an + * error. */ Assert(false); } @@ -1541,12 +1770,12 @@ update_synced_slots_inactive_since(void) if (!StandbyMode) return; - /* The slot sync worker or SQL function mustn't be running by now */ + /* The slot sync worker or the SQL function mustn't be running by now */ Assert((SlotSyncCtx->pid == InvalidPid) && !SlotSyncCtx->syncing); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -1556,7 +1785,7 @@ update_synced_slots_inactive_since(void) Assert(SlotIsLogical(s)); /* The slot must not be acquired by any process */ - Assert(s->active_pid == 0); + Assert(s->active_proc == INVALID_PROC_NUMBER); /* Use the same inactive_since time for all the slots. */ if (now == 0) @@ -1570,16 +1799,18 @@ update_synced_slots_inactive_since(void) } /* - * Shut down the slot sync worker. + * Shut down slot synchronization. * - * This function sends signal to shutdown slot sync worker, if required. It - * also waits till the slot sync worker has exited or + * This function sets stopSignaled=true and wakes up the slot sync process + * (either worker or backend running the SQL function pg_sync_replication_slots()) + * so that worker can exit or the SQL function pg_sync_replication_slots() can + * finish. It also waits till the slot sync worker has exited or * pg_sync_replication_slots() has finished. */ void ShutDownSlotSync(void) { - pid_t worker_pid; + pid_t sync_process_pid; SpinLockAcquire(&SlotSyncCtx->mutex); @@ -1596,12 +1827,16 @@ ShutDownSlotSync(void) return; } - worker_pid = SlotSyncCtx->pid; + sync_process_pid = SlotSyncCtx->pid; SpinLockRelease(&SlotSyncCtx->mutex); - if (worker_pid != InvalidPid) - kill(worker_pid, SIGINT); + /* + * Signal process doing slotsync, if any, asking it to stop. + */ + if (sync_process_pid != InvalidPid) + SendProcSignal(sync_process_pid, PROCSIG_SLOTSYNC_MESSAGE, + INVALID_PROC_NUMBER); /* Wait for slot sync to end */ for (;;) @@ -1636,8 +1871,9 @@ ShutDownSlotSync(void) /* * SlotSyncWorkerCanRestart * - * Returns true if enough time (SLOTSYNC_RESTART_INTERVAL_SEC) has passed - * since it was launched last. Otherwise returns false. + * Return true, indicating worker is allowed to restart, if enough time has + * passed since it was last launched to reach SLOTSYNC_RESTART_INTERVAL_SEC. + * Otherwise return false. * * This is a safety valve to protect against continuous respawn attempts if the * worker is dying immediately at launch. Note that since we will retry to @@ -1649,14 +1885,19 @@ SlotSyncWorkerCanRestart(void) { time_t curtime = time(NULL); - /* Return false if too soon since last start. */ - if ((unsigned int) (curtime - SlotSyncCtx->last_start_time) < - (unsigned int) SLOTSYNC_RESTART_INTERVAL_SEC) - return false; - - SlotSyncCtx->last_start_time = curtime; - - return true; + /* + * If first time through, or time somehow went backwards, always update + * last_start_time to match the current clock and allow worker start. + * Otherwise allow it only once enough time has elapsed. + */ + if (SlotSyncCtx->last_start_time == 0 || + curtime < SlotSyncCtx->last_start_time || + curtime - SlotSyncCtx->last_start_time >= SLOTSYNC_RESTART_INTERVAL_SEC) + { + SlotSyncCtx->last_start_time = curtime; + return true; + } + return false; } /* @@ -1671,32 +1912,26 @@ IsSyncingReplicationSlots(void) } /* - * Amount of shared memory required for slot synchronization. + * Register shared memory space needed for slot synchronization. */ -Size -SlotSyncShmemSize(void) +static void +SlotSyncShmemRequest(void *arg) { - return sizeof(SlotSyncCtxStruct); + ShmemRequestStruct(.name = "Slot Sync Data", + .size = sizeof(SlotSyncCtxStruct), + .ptr = (void **) &SlotSyncCtx, + ); } /* - * Allocate and initialize the shared memory of slot synchronization. + * Initialize shared memory for slot synchronization. */ -void -SlotSyncShmemInit(void) +static void +SlotSyncShmemInit(void *arg) { - Size size = SlotSyncShmemSize(); - bool found; - - SlotSyncCtx = (SlotSyncCtxStruct *) - ShmemInitStruct("Slot Sync Data", size, &found); - - if (!found) - { - memset(SlotSyncCtx, 0, size); - SlotSyncCtx->pid = InvalidPid; - SpinLockInit(&SlotSyncCtx->mutex); - } + memset(SlotSyncCtx, 0, sizeof(SlotSyncCtxStruct)); + SlotSyncCtx->pid = InvalidPid; + SpinLockInit(&SlotSyncCtx->mutex); } /* @@ -1735,20 +1970,98 @@ slotsync_failure_callback(int code, Datum arg) walrcv_disconnect(wrconn); } +/* + * Helper function to extract slot names from a list of remote slots + */ +static List * +extract_slot_names(List *remote_slots) +{ + List *slot_names = NIL; + + foreach_ptr(RemoteSlot, remote_slot, remote_slots) + { + char *slot_name; + + slot_name = pstrdup(remote_slot->name); + slot_names = lappend(slot_names, slot_name); + } + + return slot_names; +} + /* * Synchronize the failover enabled replication slots using the specified * primary server connection. + * + * Repeatedly fetches and updates replication slot information from the + * primary until all slots are at least "sync ready". + * + * Exits early if promotion is triggered or certain critical + * configuration parameters have changed. */ void SyncReplicationSlots(WalReceiverConn *wrconn) { PG_ENSURE_ERROR_CLEANUP(slotsync_failure_callback, PointerGetDatum(wrconn)); { - check_and_set_sync_info(InvalidPid); + List *remote_slots = NIL; + List *slot_names = NIL; /* List of slot names to track */ + + check_and_set_sync_info(MyProcPid); validate_remote_info(wrconn); - synchronize_slots(wrconn); + /* Retry until all the slots are sync-ready */ + for (;;) + { + bool slot_persistence_pending = false; + bool some_slot_updated = false; + + /* Check for interrupts and config changes */ + CHECK_FOR_INTERRUPTS(); + + if (ConfigReloadPending) + slotsync_reread_config(); + + /* We must be in a valid transaction state */ + Assert(IsTransactionState()); + + /* + * Fetch remote slot info for the given slot_names. If slot_names + * is NIL, fetch all failover-enabled slots. Note that we reuse + * slot_names from the first iteration; re-fetching all failover + * slots each time could cause an endless loop. Instead of + * reprocessing only the pending slots in each iteration, it's + * better to process all the slots received in the first + * iteration. This ensures that by the time we're done, all slots + * reflect the latest values. + */ + remote_slots = fetch_remote_slots(wrconn, slot_names); + + /* Attempt to synchronize slots */ + some_slot_updated = synchronize_slots(wrconn, remote_slots, + &slot_persistence_pending); + + /* + * If slot_persistence_pending is true, extract slot names for + * future iterations (only needed if we haven't done it yet) + */ + if (slot_names == NIL && slot_persistence_pending) + slot_names = extract_slot_names(remote_slots); + + /* Free the current remote_slots list */ + list_free_deep(remote_slots); + + /* Done if all slots are persisted i.e are sync-ready */ + if (!slot_persistence_pending) + break; + + /* wait before retrying again */ + wait_for_slot_activity(some_slot_updated); + } + + if (slot_names) + list_free_deep(slot_names); /* Cleanup the synced temporary slots */ ReplicationSlotCleanup(true); diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c index 0d7bddbe4ed4e..c8309b96ed45c 100644 --- a/src/backend/replication/logical/snapbuild.c +++ b/src/backend/replication/logical/snapbuild.c @@ -112,7 +112,7 @@ * is a convenient point to initialize replication from, which is why we * export a snapshot at that point, which *can* be used to read normal data. * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/snapbuild.c @@ -144,6 +144,9 @@ #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/snapshot.h" +#include "utils/wait_event.h" + + /* * Starting a transaction -- which we need to do while exporting a snapshot -- * removes knowledge about the previously used resowner, so we save it here. @@ -151,6 +154,14 @@ static ResourceOwner SavedResourceOwnerDuringExport = NULL; static bool ExportInProgress = false; +/* + * If a backend is going to do logical decoding and the output plugin does + * not need to access shared catalogs, setting this variable to false can make + * the decoding startup faster. In particular, the backend will not need to + * wait for completion of already running transactions in other databases. + */ +bool accessSharedCatalogsInDecoding = true; + /* ->committed and ->catchange manipulation */ static void SnapBuildPurgeOlderTxn(SnapBuild *builder); @@ -167,7 +178,8 @@ static inline bool SnapBuildXidHasCatalogChanges(SnapBuild *builder, Transaction uint32 xinfo); /* xlog reading helper functions for SnapBuildProcessRunningXacts */ -static bool SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *running); +static bool SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, + xl_running_xacts *running); static void SnapBuildWaitSnapshot(xl_running_xacts *running, TransactionId cutoff); /* serialization functions */ @@ -199,7 +211,7 @@ AllocateSnapshotBuilder(ReorderBuffer *reorder, ALLOCSET_DEFAULT_SIZES); oldcontext = MemoryContextSwitchTo(context); - builder = palloc0(sizeof(SnapBuild)); + builder = palloc0_object(SnapBuild); builder->state = SNAPBUILD_START; builder->context = context; @@ -209,7 +221,7 @@ AllocateSnapshotBuilder(ReorderBuffer *reorder, builder->committed.xcnt = 0; builder->committed.xcnt_space = 128; /* arbitrary number */ builder->committed.xip = - palloc0(builder->committed.xcnt_space * sizeof(TransactionId)); + palloc0_array(TransactionId, builder->committed.xcnt_space); builder->committed.includes_all_transactions = true; builder->catchange.xcnt = 0; @@ -223,6 +235,9 @@ AllocateSnapshotBuilder(ReorderBuffer *reorder, MemoryContextSwitchTo(oldcontext); + /* The default is that shared catalog are used. */ + accessSharedCatalogsInDecoding = true; + return builder; } @@ -241,6 +256,9 @@ FreeSnapshotBuilder(SnapBuild *builder) builder->snapshot = NULL; } + /* The default is that shared catalog are used. */ + accessSharedCatalogsInDecoding = true; + /* other resources are deallocated via memory context reset */ MemoryContextDelete(context); } @@ -486,8 +504,7 @@ SnapBuildInitialSnapshot(SnapBuild *builder) MyProc->xmin = snap->xmin; /* allocate in transaction context */ - newxip = (TransactionId *) - palloc(sizeof(TransactionId) * GetMaxSnapshotXidCount()); + newxip = palloc_array(TransactionId, GetMaxSnapshotXidCount()); /* * snapbuild.c builds transactions in an "inverted" manner, which means it @@ -774,7 +791,7 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact if (rbtxn_is_prepared(txn)) continue; - elog(DEBUG2, "adding a new snapshot and invalidations to %u at %X/%X", + elog(DEBUG2, "adding a new snapshot and invalidations to %u at %X/%08X", txn->xid, LSN_FORMAT_ARGS(lsn)); /* @@ -794,6 +811,13 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact * contents built by the current transaction even after its decoding, * which should have been invalidated due to concurrent catalog * changing transaction. + * + * Distribute only the invalidation messages generated by the current + * committed transaction. Invalidation messages received from other + * transactions would have already been propagated to the relevant + * in-progress transactions. This transaction would have processed + * those invalidations, ensuring that subsequent transactions observe + * a consistent cache state. */ if (txn->xid != xid) { @@ -807,8 +831,9 @@ SnapBuildDistributeSnapshotAndInval(SnapBuild *builder, XLogRecPtr lsn, Transact { Assert(msgs != NULL); - ReorderBufferAddInvalidations(builder->reorder, txn->xid, lsn, - ninvalidations, msgs); + ReorderBufferAddDistributedInvalidations(builder->reorder, + txn->xid, lsn, + ninvalidations, msgs); } } } @@ -829,8 +854,9 @@ SnapBuildAddCommittedTxn(SnapBuild *builder, TransactionId xid) elog(DEBUG1, "increasing space for committed transactions to %u", (uint32) builder->committed.xcnt_space); - builder->committed.xip = repalloc(builder->committed.xip, - builder->committed.xcnt_space * sizeof(TransactionId)); + builder->committed.xip = repalloc_array(builder->committed.xip, + TransactionId, + builder->committed.xcnt_space); } /* @@ -1125,7 +1151,8 @@ SnapBuildXidHasCatalogChanges(SnapBuild *builder, TransactionId xid, * anymore. */ void -SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *running) +SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *running, + bool db_specific) { ReorderBufferTXN *txn; TransactionId xmin; @@ -1137,6 +1164,33 @@ SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xact */ if (builder->state < SNAPBUILD_CONSISTENT) { + /* + * To reduce the potential for unnecessarily waiting for completion of + * unrelated transactions, the caller can declare that only + * transactions of the current database are relevant at this stage. + */ + if (db_specific) + { + /* + * If we must only keep track of transactions running in the + * current database, we need transaction info from exactly that + * database. + */ + if (running->dbid != MyDatabaseId) + { + LogStandbySnapshot(MyDatabaseId); + + return; + } + + /* + * We'd better be able to check during scan if the plugin does not + * lie. + */ + if (accessSharedCatalogsInDecoding) + accessSharedCatalogsInDecoding = false; + } + /* returns false if there's no point in performing cleanup just yet */ if (!SnapBuildFindSnapshot(builder, lsn, running)) return; @@ -1144,6 +1198,16 @@ SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xact else SnapBuildSerialize(builder, lsn); + /* + * Database specific transaction info may exist to reach CONSISTENT state + * faster, however the code below makes no use of it. Moreover, such + * record might cause problems because the following normal (cluster-wide) + * record can have lower value of oldestRunningXid. In that case, let's + * wait with the cleanup for the next regular cluster-wide record. + */ + if (OidIsValid(running->dbid)) + return; + /* * Update range of interesting xids based on the running xacts * information. We don't increase ->xmax using it, because once we are in @@ -1202,7 +1266,7 @@ SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xact * oldest ongoing txn might have started when we didn't yet serialize * anything because we hadn't reached a consistent state yet. */ - if (txn != NULL && txn->restart_decoding_lsn != InvalidXLogRecPtr) + if (txn != NULL && XLogRecPtrIsValid(txn->restart_decoding_lsn)) LogicalIncreaseRestartDecodingForSlot(lsn, txn->restart_decoding_lsn); /* @@ -1210,8 +1274,8 @@ SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, xl_running_xact * we have one. */ else if (txn == NULL && - builder->reorder->current_restart_decoding_lsn != InvalidXLogRecPtr && - builder->last_serialized_snapshot != InvalidXLogRecPtr) + XLogRecPtrIsValid(builder->reorder->current_restart_decoding_lsn) && + XLogRecPtrIsValid(builder->last_serialized_snapshot)) LogicalIncreaseRestartDecodingForSlot(lsn, builder->last_serialized_snapshot); } @@ -1263,10 +1327,10 @@ SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *runn builder->initial_xmin_horizon)) { ereport(DEBUG1, - (errmsg_internal("skipping snapshot at %X/%X while building logical decoding snapshot, xmin horizon too low", - LSN_FORMAT_ARGS(lsn)), - errdetail_internal("initial xmin horizon of %u vs the snapshot's %u", - builder->initial_xmin_horizon, running->oldestRunningXid))); + errmsg_internal("skipping snapshot at %X/%08X while building logical decoding snapshot, xmin horizon too low", + LSN_FORMAT_ARGS(lsn)), + errdetail_internal("initial xmin horizon of %u vs the snapshot's %u", + builder->initial_xmin_horizon, running->oldestRunningXid)); SnapBuildWaitSnapshot(running, builder->initial_xmin_horizon); @@ -1285,7 +1349,7 @@ SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *runn */ if (running->oldestRunningXid == running->nextXid) { - if (builder->start_decoding_at == InvalidXLogRecPtr || + if (!XLogRecPtrIsValid(builder->start_decoding_at) || builder->start_decoding_at <= lsn) /* can decode everything after this */ builder->start_decoding_at = lsn + 1; @@ -1301,10 +1365,10 @@ SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *runn builder->state = SNAPBUILD_CONSISTENT; builder->next_phase_at = InvalidTransactionId; - ereport(LOG, - (errmsg("logical decoding found consistent point at %X/%X", - LSN_FORMAT_ARGS(lsn)), - errdetail("There are no running transactions."))); + ereport(LogicalDecodingLogLevel(), + errmsg("logical decoding found consistent point at %X/%08X", + LSN_FORMAT_ARGS(lsn)), + errdetail("There are no running transactions.")); return false; } @@ -1351,10 +1415,10 @@ SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *runn Assert(TransactionIdIsNormal(builder->xmax)); ereport(LOG, - (errmsg("logical decoding found initial starting point at %X/%X", - LSN_FORMAT_ARGS(lsn)), - errdetail("Waiting for transactions (approximately %d) older than %u to end.", - running->xcnt, running->nextXid))); + errmsg("logical decoding found initial starting point at %X/%08X", + LSN_FORMAT_ARGS(lsn)), + errdetail("Waiting for transactions (approximately %d) older than %u to end.", + running->xcnt, running->nextXid)); SnapBuildWaitSnapshot(running, running->nextXid); } @@ -1375,10 +1439,10 @@ SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *runn builder->next_phase_at = running->nextXid; ereport(LOG, - (errmsg("logical decoding found initial consistent point at %X/%X", - LSN_FORMAT_ARGS(lsn)), - errdetail("Waiting for transactions (approximately %d) older than %u to end.", - running->xcnt, running->nextXid))); + errmsg("logical decoding found initial consistent point at %X/%08X", + LSN_FORMAT_ARGS(lsn)), + errdetail("Waiting for transactions (approximately %d) older than %u to end.", + running->xcnt, running->nextXid)); SnapBuildWaitSnapshot(running, running->nextXid); } @@ -1398,10 +1462,10 @@ SnapBuildFindSnapshot(SnapBuild *builder, XLogRecPtr lsn, xl_running_xacts *runn builder->state = SNAPBUILD_CONSISTENT; builder->next_phase_at = InvalidTransactionId; - ereport(LOG, - (errmsg("logical decoding found consistent point at %X/%X", - LSN_FORMAT_ARGS(lsn)), - errdetail("There are no old transactions anymore."))); + ereport(LogicalDecodingLogLevel(), + errmsg("logical decoding found consistent point at %X/%08X", + LSN_FORMAT_ARGS(lsn)), + errdetail("There are no old transactions anymore.")); } /* @@ -1454,7 +1518,11 @@ SnapBuildWaitSnapshot(xl_running_xacts *running, TransactionId cutoff) */ if (!RecoveryInProgress()) { - LogStandbySnapshot(); + /* + * If the last transaction info was about specific database, so needs + * to be the next one - at least until we're in the CONSISTENT state. + */ + LogStandbySnapshot(running->dbid); } } @@ -1501,8 +1569,8 @@ SnapBuildSerialize(SnapBuild *builder, XLogRecPtr lsn) struct stat stat_buf; Size sz; - Assert(lsn != InvalidXLogRecPtr); - Assert(builder->last_serialized_snapshot == InvalidXLogRecPtr || + Assert(XLogRecPtrIsValid(lsn)); + Assert(!XLogRecPtrIsValid(builder->last_serialized_snapshot) || builder->last_serialized_snapshot <= lsn); /* @@ -1904,10 +1972,10 @@ SnapBuildRestore(SnapBuild *builder, XLogRecPtr lsn) Assert(builder->state == SNAPBUILD_CONSISTENT); - ereport(LOG, - (errmsg("logical decoding found consistent point at %X/%X", - LSN_FORMAT_ARGS(lsn)), - errdetail("Logical decoding will begin using saved snapshot."))); + ereport(LogicalDecodingLogLevel(), + errmsg("logical decoding found consistent point at %X/%08X", + LSN_FORMAT_ARGS(lsn)), + errdetail("Logical decoding will begin using saved snapshot.")); return true; snapshot_not_interesting: @@ -2021,7 +2089,7 @@ CheckPointSnapBuild(void) lsn = ((uint64) hi) << 32 | lo; /* check whether we still need it */ - if (lsn < cutoff || cutoff == InvalidXLogRecPtr) + if (lsn < cutoff || !XLogRecPtrIsValid(cutoff)) { elog(DEBUG1, "removing snapbuild snapshot %s", path); diff --git a/src/backend/replication/logical/syncutils.c b/src/backend/replication/logical/syncutils.c new file mode 100644 index 0000000000000..ef61ca0437de5 --- /dev/null +++ b/src/backend/replication/logical/syncutils.c @@ -0,0 +1,281 @@ +/*------------------------------------------------------------------------- + * syncutils.c + * PostgreSQL logical replication: common synchronization code + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/logical/syncutils.c + * + * NOTES + * This file contains code common for synchronization workers. + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_subscription_rel.h" +#include "pgstat.h" +#include "replication/logicallauncher.h" +#include "replication/worker_internal.h" +#include "storage/ipc.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" + +/* + * Enum for phases of the subscription relations state. + * + * SYNC_RELATIONS_STATE_NEEDS_REBUILD indicates that the subscription relations + * state is no longer valid, and the subscription relations should be rebuilt. + * + * SYNC_RELATIONS_STATE_REBUILD_STARTED indicates that the subscription + * relations state is being rebuilt. + * + * SYNC_RELATIONS_STATE_VALID indicates that the subscription relation state is + * up-to-date and valid. + */ +typedef enum +{ + SYNC_RELATIONS_STATE_NEEDS_REBUILD, + SYNC_RELATIONS_STATE_REBUILD_STARTED, + SYNC_RELATIONS_STATE_VALID, +} SyncingRelationsState; + +static SyncingRelationsState relation_states_validity = SYNC_RELATIONS_STATE_NEEDS_REBUILD; + +/* + * Exit routine for synchronization worker. + */ +pg_noreturn void +FinishSyncWorker(void) +{ + Assert(am_sequencesync_worker() || am_tablesync_worker()); + + /* + * Commit any outstanding transaction. This is the usual case, unless + * there was nothing to do for the table. + */ + if (IsTransactionState()) + { + CommitTransactionCommand(); + pgstat_report_stat(true); + } + + /* And flush all writes. */ + XLogFlush(GetXLogWriteRecPtr()); + + if (am_sequencesync_worker()) + { + ereport(LOG, + errmsg("logical replication sequence synchronization worker for subscription \"%s\" has finished", + MySubscription->name)); + + /* + * Reset last_seqsync_start_time, so that next time a sequencesync + * worker is needed it can be started promptly. + */ + logicalrep_reset_seqsync_start_time(); + } + else + { + StartTransactionCommand(); + ereport(LOG, + errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has finished", + MySubscription->name, + get_rel_name(MyLogicalRepWorker->relid))); + CommitTransactionCommand(); + + /* Find the leader apply worker and signal it. */ + logicalrep_worker_wakeup(WORKERTYPE_APPLY, MyLogicalRepWorker->subid, + InvalidOid); + } + + /* Stop gracefully */ + proc_exit(0); +} + +/* + * Callback from syscache invalidation. + */ +void +InvalidateSyncingRelStates(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) +{ + relation_states_validity = SYNC_RELATIONS_STATE_NEEDS_REBUILD; +} + +/* + * Attempt to launch a sync worker for one or more sequences or a table, if + * a worker slot is available and the retry interval has elapsed. + * + * wtype: sync worker type. + * nsyncworkers: Number of currently running sync workers for the subscription. + * relid: InvalidOid for sequencesync worker, actual relid for tablesync + * worker. + * last_start_time: Pointer to the last start time of the worker. + */ +void +launch_sync_worker(LogicalRepWorkerType wtype, int nsyncworkers, Oid relid, + TimestampTz *last_start_time) +{ + TimestampTz now; + + Assert((wtype == WORKERTYPE_TABLESYNC && OidIsValid(relid)) || + (wtype == WORKERTYPE_SEQUENCESYNC && !OidIsValid(relid))); + + /* If there is a free sync worker slot, start a new sync worker */ + if (nsyncworkers >= max_sync_workers_per_subscription) + return; + + now = GetCurrentTimestamp(); + + if (!(*last_start_time) || + TimestampDifferenceExceeds(*last_start_time, now, + wal_retrieve_retry_interval)) + { + /* + * Set the last_start_time even if we fail to start the worker, so + * that we won't retry until wal_retrieve_retry_interval has elapsed. + */ + *last_start_time = now; + (void) logicalrep_worker_launch(wtype, + MyLogicalRepWorker->dbid, + MySubscription->oid, + MySubscription->name, + MyLogicalRepWorker->userid, + relid, DSM_HANDLE_INVALID, false); + } +} + +/* + * Process possible state change(s) of relations that are being synchronized + * and start new tablesync workers for the newly added tables. Also, start a + * new sequencesync worker for the newly added sequences. + */ +void +ProcessSyncingRelations(XLogRecPtr current_lsn) +{ + switch (MyLogicalRepWorker->type) + { + case WORKERTYPE_PARALLEL_APPLY: + + /* + * Skip for parallel apply workers because they only operate on + * tables that are in a READY state. See pa_can_start() and + * should_apply_changes_for_rel(). + */ + break; + + case WORKERTYPE_TABLESYNC: + ProcessSyncingTablesForSync(current_lsn); + break; + + case WORKERTYPE_APPLY: + ProcessSyncingTablesForApply(current_lsn); + ProcessSequencesForSync(); + break; + + case WORKERTYPE_SEQUENCESYNC: + /* Should never happen. */ + elog(ERROR, "sequence synchronization worker is not expected to process relations"); + break; + + case WORKERTYPE_UNKNOWN: + /* Should never happen. */ + elog(ERROR, "Unknown worker type"); + } +} + +/* + * Common code to fetch the up-to-date sync state info for tables and sequences. + * + * The pg_subscription_rel catalog is shared by tables and sequences. Changes + * to either sequences or tables can affect the validity of relation states, so + * we identify non-READY tables and non-READY sequences together to ensure + * consistency. + * + * has_pending_subtables: true if the subscription has one or more tables that + * are not in READY state, otherwise false. + * has_pending_subsequences: true if the subscription has one or more sequences + * that are not in READY state, otherwise false. + */ +void +FetchRelationStates(bool *has_pending_subtables, + bool *has_pending_subsequences, + bool *started_tx) +{ + /* + * has_subtables and has_subsequences_non_ready are declared as static, + * since the same value can be used until the system table is invalidated. + */ + static bool has_subtables = false; + static bool has_subsequences_non_ready = false; + + *started_tx = false; + + if (relation_states_validity != SYNC_RELATIONS_STATE_VALID) + { + MemoryContext oldctx; + List *rstates; + SubscriptionRelState *rstate; + + relation_states_validity = SYNC_RELATIONS_STATE_REBUILD_STARTED; + has_subsequences_non_ready = false; + + /* Clean the old lists. */ + list_free_deep(table_states_not_ready); + table_states_not_ready = NIL; + + if (!IsTransactionState()) + { + StartTransactionCommand(); + *started_tx = true; + } + + /* Fetch tables and sequences that are in non-READY state. */ + rstates = GetSubscriptionRelations(MySubscription->oid, true, true, + true); + + /* Allocate the tracking info in a permanent memory context. */ + oldctx = MemoryContextSwitchTo(CacheMemoryContext); + foreach_ptr(SubscriptionRelState, subrel, rstates) + { + if (get_rel_relkind(subrel->relid) == RELKIND_SEQUENCE) + has_subsequences_non_ready = true; + else + { + rstate = palloc_object(SubscriptionRelState); + memcpy(rstate, subrel, sizeof(SubscriptionRelState)); + table_states_not_ready = lappend(table_states_not_ready, + rstate); + } + } + MemoryContextSwitchTo(oldctx); + + /* + * Does the subscription have tables? + * + * If there were not-READY tables found then we know it does. But if + * table_states_not_ready was empty we still need to check again to + * see if there are 0 tables. + */ + has_subtables = (table_states_not_ready != NIL) || + HasSubscriptionTables(MySubscription->oid); + + /* + * If the subscription relation cache has been invalidated since we + * entered this routine, we still use and return the relations we just + * finished constructing, to avoid infinite loops, but we leave the + * table states marked as stale so that we'll rebuild it again on next + * access. Otherwise, we mark the table states as valid. + */ + if (relation_states_validity == SYNC_RELATIONS_STATE_REBUILD_STARTED) + relation_states_validity = SYNC_RELATIONS_STATE_VALID; + } + + if (has_pending_subtables) + *has_pending_subtables = has_subtables; + + if (has_pending_subsequences) + *has_pending_subsequences = has_subsequences_non_ready; +} diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c index 8e1e8762f6258..eb71811429700 100644 --- a/src/backend/replication/logical/tablesync.c +++ b/src/backend/replication/logical/tablesync.c @@ -2,7 +2,7 @@ * tablesync.c * PostgreSQL logical replication: initial table data synchronization * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/tablesync.c @@ -112,63 +112,22 @@ #include "replication/walreceiver.h" #include "replication/worker_internal.h" #include "storage/ipc.h" +#include "storage/latch.h" #include "storage/lmgr.h" #include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/lsyscache.h" -#include "utils/memutils.h" #include "utils/rls.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/usercontext.h" +#include "utils/wait_event.h" -typedef enum -{ - SYNC_TABLE_STATE_NEEDS_REBUILD, - SYNC_TABLE_STATE_REBUILD_STARTED, - SYNC_TABLE_STATE_VALID, -} SyncingTablesState; - -static SyncingTablesState table_states_validity = SYNC_TABLE_STATE_NEEDS_REBUILD; -static List *table_states_not_ready = NIL; -static bool FetchTableStates(bool *started_tx); +List *table_states_not_ready = NIL; static StringInfo copybuf = NULL; -/* - * Exit routine for synchronization worker. - */ -pg_noreturn static void -finish_sync_worker(void) -{ - /* - * Commit any outstanding transaction. This is the usual case, unless - * there was nothing to do for the table. - */ - if (IsTransactionState()) - { - CommitTransactionCommand(); - pgstat_report_stat(true); - } - - /* And flush all writes. */ - XLogFlush(GetXLogWriteRecPtr()); - - StartTransactionCommand(); - ereport(LOG, - (errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has finished", - MySubscription->name, - get_rel_name(MyLogicalRepWorker->relid)))); - CommitTransactionCommand(); - - /* Find the leader apply worker and signal it. */ - logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid); - - /* Stop gracefully */ - proc_exit(0); -} - /* * Wait until the relation sync state is set in the catalog to the expected * one; return true when it happens. @@ -180,7 +139,7 @@ finish_sync_worker(void) * CATCHUP state to SYNCDONE. */ static bool -wait_for_relation_state_change(Oid relid, char expected_state) +wait_for_table_state_change(Oid relid, char expected_state) { char state; @@ -203,7 +162,8 @@ wait_for_relation_state_change(Oid relid, char expected_state) /* Check if the sync worker is still running and bail if not. */ LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); - worker = logicalrep_worker_find(MyLogicalRepWorker->subid, relid, + worker = logicalrep_worker_find(WORKERTYPE_TABLESYNC, + MyLogicalRepWorker->subid, relid, false); LWLockRelease(LogicalRepWorkerLock); if (!worker) @@ -250,8 +210,9 @@ wait_for_worker_state_change(char expected_state) * waiting. */ LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); - worker = logicalrep_worker_find(MyLogicalRepWorker->subid, - InvalidOid, false); + worker = logicalrep_worker_find(WORKERTYPE_APPLY, + MyLogicalRepWorker->subid, InvalidOid, + false); if (worker && worker->proc) logicalrep_worker_wakeup_ptr(worker); LWLockRelease(LogicalRepWorkerLock); @@ -273,15 +234,6 @@ wait_for_worker_state_change(char expected_state) return false; } -/* - * Callback from syscache invalidation. - */ -void -invalidate_syncing_table_states(Datum arg, int cacheid, uint32 hashvalue) -{ - table_states_validity = SYNC_TABLE_STATE_NEEDS_REBUILD; -} - /* * Handle table synchronization cooperation from the synchronization * worker. @@ -290,8 +242,8 @@ invalidate_syncing_table_states(Datum arg, int cacheid, uint32 hashvalue) * predetermined synchronization point in the WAL stream, mark the table as * SYNCDONE and finish. */ -static void -process_syncing_tables_for_sync(XLogRecPtr current_lsn) +void +ProcessSyncingTablesForSync(XLogRecPtr current_lsn) { SpinLockAcquire(&MyLogicalRepWorker->relmutex); @@ -316,7 +268,8 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) UpdateSubscriptionRelState(MyLogicalRepWorker->subid, MyLogicalRepWorker->relid, MyLogicalRepWorker->relstate, - MyLogicalRepWorker->relstate_lsn); + MyLogicalRepWorker->relstate_lsn, + false); /* * End streaming so that LogRepWorkerWalRcvConn can be used to drop @@ -348,9 +301,9 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) /* * Start a new transaction to clean up the tablesync origin tracking. - * This transaction will be ended within the finish_sync_worker(). - * Now, even, if we fail to remove this here, the apply worker will - * ensure to clean it up afterward. + * This transaction will be ended within the FinishSyncWorker(). Now, + * even, if we fail to remove this here, the apply worker will ensure + * to clean it up afterward. * * We need to do this after the table state is set to SYNCDONE. * Otherwise, if an error occurs while performing the database @@ -372,9 +325,7 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) * This is needed to allow the origin to be dropped. */ replorigin_session_reset(); - replorigin_session_origin = InvalidRepOriginId; - replorigin_session_origin_lsn = InvalidXLogRecPtr; - replorigin_session_origin_timestamp = 0; + replorigin_xact_clear(true); /* * Drop the tablesync's origin tracking if exists. @@ -386,7 +337,7 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) */ replorigin_drop_by_name(originname, true, false); - finish_sync_worker(); + FinishSyncWorker(); } else SpinLockRelease(&MyLogicalRepWorker->relmutex); @@ -413,8 +364,8 @@ process_syncing_tables_for_sync(XLogRecPtr current_lsn) * If the synchronization position is reached (SYNCDONE), then the table can * be marked as READY and is no longer tracked. */ -static void -process_syncing_tables_for_apply(XLogRecPtr current_lsn) +void +ProcessSyncingTablesForApply(XLogRecPtr current_lsn) { struct tablesync_start_time_mapping { @@ -423,13 +374,14 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) }; static HTAB *last_start_times = NULL; ListCell *lc; - bool started_tx = false; + bool started_tx; bool should_exit = false; + Relation rel = NULL; Assert(!IsTransactionState()); /* We need up-to-date sync state info for subscription tables here. */ - FetchTableStates(&started_tx); + FetchRelationStates(NULL, NULL, &started_tx); /* * Prepare a hash table for tracking last start times of workers, to avoid @@ -463,6 +415,14 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) { SubscriptionRelState *rstate = (SubscriptionRelState *) lfirst(lc); + if (!started_tx) + { + StartTransactionCommand(); + started_tx = true; + } + + Assert(get_rel_relkind(rstate->relid) != RELKIND_SEQUENCE); + if (rstate->state == SUBREL_STATE_SYNCDONE) { /* @@ -476,11 +436,6 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) rstate->state = SUBREL_STATE_READY; rstate->lsn = current_lsn; - if (!started_tx) - { - StartTransactionCommand(); - started_tx = true; - } /* * Remove the tablesync origin tracking if exists. @@ -492,7 +447,17 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) * worker to remove the origin tracking as if there is any * error while dropping we won't restart it to drop the * origin. So passing missing_ok = true. + * + * Lock the subscription and origin in the same order as we + * are doing during DDL commands to avoid deadlocks. See + * AlterSubscription_refresh. */ + LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, + 0, AccessShareLock); + + if (!rel) + rel = table_open(SubscriptionRelRelationId, RowExclusiveLock); + ReplicationOriginNameForLogicalRep(MyLogicalRepWorker->subid, rstate->relid, originname, @@ -504,7 +469,7 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) */ UpdateSubscriptionRelState(MyLogicalRepWorker->subid, rstate->relid, rstate->state, - rstate->lsn); + rstate->lsn, true); } } else @@ -516,7 +481,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) */ LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); - syncworker = logicalrep_worker_find(MyLogicalRepWorker->subid, + syncworker = logicalrep_worker_find(WORKERTYPE_TABLESYNC, + MyLogicalRepWorker->subid, rstate->relid, false); if (syncworker) @@ -555,7 +521,14 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) * This is required to avoid any undetected deadlocks * due to any existing lock as deadlock detector won't * be able to detect the waits on the latch. + * + * Also close any tables prior to the commit. */ + if (rel) + { + table_close(rel, NoLock); + rel = NULL; + } CommitTransactionCommand(); pgstat_report_stat(false); } @@ -567,8 +540,8 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) StartTransactionCommand(); started_tx = true; - wait_for_relation_state_change(rstate->relid, - SUBREL_STATE_SYNCDONE); + wait_for_table_state_change(rstate->relid, + SUBREL_STATE_SYNCDONE); } else LWLockRelease(LogicalRepWorkerLock); @@ -582,41 +555,28 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) */ int nsyncworkers = logicalrep_sync_worker_count(MyLogicalRepWorker->subid); + struct tablesync_start_time_mapping *hentry; + bool found; /* Now safe to release the LWLock */ LWLockRelease(LogicalRepWorkerLock); - /* - * If there are free sync worker slot(s), start a new sync - * worker for the table. - */ - if (nsyncworkers < max_sync_workers_per_subscription) - { - TimestampTz now = GetCurrentTimestamp(); - struct tablesync_start_time_mapping *hentry; - bool found; - - hentry = hash_search(last_start_times, &rstate->relid, - HASH_ENTER, &found); + hentry = hash_search(last_start_times, &rstate->relid, + HASH_ENTER, &found); + if (!found) + hentry->last_start_time = 0; - if (!found || - TimestampDifferenceExceeds(hentry->last_start_time, now, - wal_retrieve_retry_interval)) - { - logicalrep_worker_launch(WORKERTYPE_TABLESYNC, - MyLogicalRepWorker->dbid, - MySubscription->oid, - MySubscription->name, - MyLogicalRepWorker->userid, - rstate->relid, - DSM_HANDLE_INVALID); - hentry->last_start_time = now; - } - } + launch_sync_worker(WORKERTYPE_TABLESYNC, nsyncworkers, + rstate->relid, &hentry->last_start_time); } } } + /* Close table if opened */ + if (rel) + table_close(rel, NoLock); + + if (started_tx) { /* @@ -659,37 +619,6 @@ process_syncing_tables_for_apply(XLogRecPtr current_lsn) } } -/* - * Process possible state change(s) of tables that are being synchronized. - */ -void -process_syncing_tables(XLogRecPtr current_lsn) -{ - switch (MyLogicalRepWorker->type) - { - case WORKERTYPE_PARALLEL_APPLY: - - /* - * Skip for parallel apply workers because they only operate on - * tables that are in a READY state. See pa_can_start() and - * should_apply_changes_for_rel(). - */ - break; - - case WORKERTYPE_TABLESYNC: - process_syncing_tables_for_sync(current_lsn); - break; - - case WORKERTYPE_APPLY: - process_syncing_tables_for_apply(current_lsn); - break; - - case WORKERTYPE_UNKNOWN: - /* Should never happen. */ - elog(ERROR, "Unknown worker type"); - } -} - /* * Create list of columns for COPY based on logical relation mapping. */ @@ -869,17 +798,35 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel, * publications). */ resetStringInfo(&cmd); - appendStringInfo(&cmd, - "SELECT DISTINCT" - " (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)" - " THEN NULL ELSE gpt.attrs END)" - " FROM pg_publication p," - " LATERAL pg_get_publication_tables(p.pubname) gpt," - " pg_class c" - " WHERE gpt.relid = %u AND c.oid = gpt.relid" - " AND p.pubname IN ( %s )", - lrel->remoteid, - pub_names->data); + + if (server_version >= 190000) + { + /* + * We can pass both publication names and relid to + * pg_get_publication_tables() since version 19. + */ + appendStringInfo(&cmd, + "SELECT DISTINCT" + " (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)" + " THEN NULL ELSE gpt.attrs END)" + " FROM pg_get_publication_tables(ARRAY[%s], %u) gpt," + " pg_class c" + " WHERE c.oid = gpt.relid", + pub_names->data, + lrel->remoteid); + } + else + appendStringInfo(&cmd, + "SELECT DISTINCT" + " (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)" + " THEN NULL ELSE gpt.attrs END)" + " FROM pg_publication p," + " LATERAL pg_get_publication_tables(p.pubname) gpt," + " pg_class c" + " WHERE gpt.relid = %u AND c.oid = gpt.relid" + " AND p.pubname IN ( %s )", + lrel->remoteid, + pub_names->data); pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, lengthof(attrsRow), attrsRow); @@ -893,7 +840,7 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel, /* * We don't support the case where the column list is different for * the same table when combining publications. See comments atop - * fetch_table_list. So there should be only one row returned. + * fetch_relation_list. So there should be only one row returned. * Although we already checked this when creating the subscription, we * still need to check here in case the column list was changed after * creating the subscription and before the sync worker is started. @@ -972,8 +919,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel, nspname, relname, res->err))); /* We don't know the number of rows coming, so allocate enough space. */ - lrel->attnames = palloc0(MaxTupleAttributeNumber * sizeof(char *)); - lrel->atttyps = palloc0(MaxTupleAttributeNumber * sizeof(Oid)); + lrel->attnames = palloc0_array(char *, MaxTupleAttributeNumber); + lrel->atttyps = palloc0_array(Oid, MaxTupleAttributeNumber); lrel->attkeys = NULL; /* @@ -1053,14 +1000,28 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel, /* Check for row filters. */ resetStringInfo(&cmd); - appendStringInfo(&cmd, - "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)" - " FROM pg_publication p," - " LATERAL pg_get_publication_tables(p.pubname) gpt" - " WHERE gpt.relid = %u" - " AND p.pubname IN ( %s )", - lrel->remoteid, - pub_names->data); + + if (server_version >= 190000) + { + /* + * We can pass both publication names and relid to + * pg_get_publication_tables() since version 19. + */ + appendStringInfo(&cmd, + "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)" + " FROM pg_get_publication_tables(ARRAY[%s], %u) gpt", + pub_names->data, + lrel->remoteid); + } + else + appendStringInfo(&cmd, + "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)" + " FROM pg_publication p," + " LATERAL pg_get_publication_tables(p.pubname) gpt" + " WHERE gpt.relid = %u" + " AND p.pubname IN ( %s )", + lrel->remoteid, + pub_names->data); res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow); @@ -1139,8 +1100,9 @@ copy_table(Relation rel) /* Start copy on the publisher. */ initStringInfo(&cmd); - /* Regular table with no row filter or generated columns */ - if (lrel.relkind == RELKIND_RELATION && qual == NIL && !gencol_published) + /* Regular or partitioned table with no row filter or generated columns */ + if ((lrel.relkind == RELKIND_RELATION || lrel.relkind == RELKIND_PARTITIONED_TABLE) + && qual == NIL && !gencol_published) { appendStringInfo(&cmd, "COPY %s", quote_qualified_identifier(lrel.nspname, lrel.relname)); @@ -1296,7 +1258,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) AclResult aclresult; WalRcvExecResult *res; char originname[NAMEDATALEN]; - RepOriginId originid; + ReplOriginId originid; UserContext ucxt; bool must_use_password; bool run_as_owner; @@ -1326,7 +1288,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) case SUBREL_STATE_SYNCDONE: case SUBREL_STATE_READY: case SUBREL_STATE_UNKNOWN: - finish_sync_worker(); /* doesn't return */ + FinishSyncWorker(); /* doesn't return */ } /* Calculate the name of the tablesync slot. */ @@ -1390,7 +1352,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) */ originid = replorigin_by_name(originname, false); replorigin_session_setup(originid, 0); - replorigin_session_origin = originid; + replorigin_xact_state.origin = originid; *origin_startpos = replorigin_session_get_progress(false); CommitTransactionCommand(); @@ -1403,12 +1365,27 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) MyLogicalRepWorker->relstate_lsn = InvalidXLogRecPtr; SpinLockRelease(&MyLogicalRepWorker->relmutex); - /* Update the state and make it visible to others. */ + /* + * Update the state, create the replication origin, and make them visible + * to others. + */ StartTransactionCommand(); UpdateSubscriptionRelState(MyLogicalRepWorker->subid, MyLogicalRepWorker->relid, MyLogicalRepWorker->relstate, - MyLogicalRepWorker->relstate_lsn); + MyLogicalRepWorker->relstate_lsn, + false); + + /* + * Create the replication origin in a separate transaction from the one + * that sets up the origin in shared memory. This prevents the risk that + * changes to the origin in shared memory cannot be rolled back if the + * transaction aborts. + */ + originid = replorigin_by_name(originname, true); + if (!OidIsValid(originid)) + originid = replorigin_create(originname); + CommitTransactionCommand(); pgstat_report_stat(true); @@ -1448,41 +1425,25 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) CRS_USE_SNAPSHOT, origin_startpos); /* - * Setup replication origin tracking. The purpose of doing this before the - * copy is to avoid doing the copy again due to any error in setting up - * origin tracking. + * Advance the origin to the LSN got from walrcv_create_slot and then set + * up the origin. The advancement is WAL logged for the purpose of + * recovery. Locks are to prevent the replication origin from vanishing + * while advancing. + * + * The purpose of doing these before the copy is to avoid doing the copy + * again due to any error in advancing or setting up origin tracking. */ - originid = replorigin_by_name(originname, true); - if (!OidIsValid(originid)) - { - /* - * Origin tracking does not exist, so create it now. - * - * Then advance to the LSN got from walrcv_create_slot. This is WAL - * logged for the purpose of recovery. Locks are to prevent the - * replication origin from vanishing while advancing. - */ - originid = replorigin_create(originname); + LockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); + replorigin_advance(originid, *origin_startpos, InvalidXLogRecPtr, + true /* go backward */ , true /* WAL log */ ); + UnlockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); - LockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); - replorigin_advance(originid, *origin_startpos, InvalidXLogRecPtr, - true /* go backward */ , true /* WAL log */ ); - UnlockRelationOid(ReplicationOriginRelationId, RowExclusiveLock); - - replorigin_session_setup(originid, 0); - replorigin_session_origin = originid; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_OBJECT), - errmsg("replication origin \"%s\" already exists", - originname))); - } + replorigin_session_setup(originid, 0); + replorigin_xact_state.origin = originid; /* - * Make sure that the copy command runs as the table owner, unless the - * user has opted out of that behaviour. + * If the user did not opt to run as the owner of the subscription + * ('run_as_owner'), then copy the table as the owner of the table. */ run_as_owner = MySubscription->runasowner; if (!run_as_owner) @@ -1541,14 +1502,15 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) UpdateSubscriptionRelState(MyLogicalRepWorker->subid, MyLogicalRepWorker->relid, SUBREL_STATE_FINISHEDCOPY, - MyLogicalRepWorker->relstate_lsn); + MyLogicalRepWorker->relstate_lsn, + false); CommitTransactionCommand(); copy_table_done: elog(DEBUG1, - "LogicalRepSyncTableStart: '%s' origin_startpos lsn %X/%X", + "LogicalRepSyncTableStart: '%s' origin_startpos lsn %X/%08X", originname, LSN_FORMAT_ARGS(*origin_startpos)); /* @@ -1567,77 +1529,6 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos) return slotname; } -/* - * Common code to fetch the up-to-date sync state info into the static lists. - * - * Returns true if subscription has 1 or more tables, else false. - * - * Note: If this function started the transaction (indicated by the parameter) - * then it is the caller's responsibility to commit it. - */ -static bool -FetchTableStates(bool *started_tx) -{ - static bool has_subrels = false; - - *started_tx = false; - - if (table_states_validity != SYNC_TABLE_STATE_VALID) - { - MemoryContext oldctx; - List *rstates; - ListCell *lc; - SubscriptionRelState *rstate; - - table_states_validity = SYNC_TABLE_STATE_REBUILD_STARTED; - - /* Clean the old lists. */ - list_free_deep(table_states_not_ready); - table_states_not_ready = NIL; - - if (!IsTransactionState()) - { - StartTransactionCommand(); - *started_tx = true; - } - - /* Fetch all non-ready tables. */ - rstates = GetSubscriptionRelations(MySubscription->oid, true); - - /* Allocate the tracking info in a permanent memory context. */ - oldctx = MemoryContextSwitchTo(CacheMemoryContext); - foreach(lc, rstates) - { - rstate = palloc(sizeof(SubscriptionRelState)); - memcpy(rstate, lfirst(lc), sizeof(SubscriptionRelState)); - table_states_not_ready = lappend(table_states_not_ready, rstate); - } - MemoryContextSwitchTo(oldctx); - - /* - * Does the subscription have tables? - * - * If there were not-READY relations found then we know it does. But - * if table_states_not_ready was empty we still need to check again to - * see if there are 0 tables. - */ - has_subrels = (table_states_not_ready != NIL) || - HasSubscriptionRelations(MySubscription->oid); - - /* - * If the subscription relation cache has been invalidated since we - * entered this routine, we still use and return the relations we just - * finished constructing, to avoid infinite loops, but we leave the - * table states marked as stale so that we'll rebuild it again on next - * access. Otherwise, we mark the table states as valid. - */ - if (table_states_validity == SYNC_TABLE_STATE_REBUILD_STARTED) - table_states_validity = SYNC_TABLE_STATE_VALID; - } - - return has_subrels; -} - /* * Execute the initial sync with error handling. Disable the subscription, * if it's required. @@ -1670,7 +1561,7 @@ start_table_sync(XLogRecPtr *origin_startpos, char **slotname) * idle state. */ AbortOutOfAnyTransaction(); - pgstat_report_subscription_error(MySubscription->oid, false); + pgstat_report_subscription_error(MySubscription->oid); PG_RE_THROW(); } @@ -1689,7 +1580,7 @@ start_table_sync(XLogRecPtr *origin_startpos, char **slotname) * and starts streaming to catchup with apply worker. */ static void -run_tablesync_worker() +run_tablesync_worker(void) { char originname[NAMEDATALEN]; XLogRecPtr origin_startpos = InvalidXLogRecPtr; @@ -1715,7 +1606,7 @@ run_tablesync_worker() /* Logical Replication Tablesync worker entry point */ void -TablesyncWorkerMain(Datum main_arg) +TableSyncWorkerMain(Datum main_arg) { int worker_slot = DatumGetInt32(main_arg); @@ -1723,7 +1614,7 @@ TablesyncWorkerMain(Datum main_arg) run_tablesync_worker(); - finish_sync_worker(); + FinishSyncWorker(); } /* @@ -1737,11 +1628,11 @@ TablesyncWorkerMain(Datum main_arg) bool AllTablesyncsReady(void) { - bool started_tx = false; - bool has_subrels = false; + bool started_tx; + bool has_tables; /* We need up-to-date sync state info for subscription tables here. */ - has_subrels = FetchTableStates(&started_tx); + FetchRelationStates(&has_tables, NULL, &started_tx); if (started_tx) { @@ -1753,7 +1644,33 @@ AllTablesyncsReady(void) * Return false when there are no tables in subscription or not all tables * are in ready state; true otherwise. */ - return has_subrels && (table_states_not_ready == NIL); + return has_tables && (table_states_not_ready == NIL); +} + +/* + * Return whether the subscription currently has any tables. + * + * Note: Unlike HasSubscriptionTables(), this function relies on cached + * information for subscription tables. Additionally, it should not be + * invoked outside of apply or tablesync workers, as MySubscription must be + * initialized first. + */ +bool +HasSubscriptionTablesCached(void) +{ + bool started_tx; + bool has_tables; + + /* We need up-to-date subscription tables info here */ + FetchRelationStates(&has_tables, NULL, &started_tx); + + if (started_tx) + { + CommitTransactionCommand(); + pgstat_report_stat(true); + } + + return has_tables; } /* diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 4151a4b2a96ba..9f2a16b17fa99 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -2,7 +2,7 @@ * worker.c * PostgreSQL logical replication worker (apply) * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/logical/worker.c @@ -91,7 +91,7 @@ * behave as if two_phase = off. When the apply worker detects that all * tablesyncs have become READY (while the tri-state was PENDING) it will * restart the apply worker process. This happens in - * process_syncing_tables_for_apply. + * ProcessSyncingTablesForApply. * * When the (re-started) apply worker finds that all tablesyncs are READY for a * two_phase tri-state of PENDING it start streaming messages with the @@ -109,13 +109,6 @@ * If ever a user needs to be aware of the tri-state value, they can fetch it * from the pg_subscription catalog (see column subtwophasestate). * - * We don't allow to toggle two_phase option of a subscription because it can - * lead to an inconsistent replica. Consider, initially, it was on and we have - * received some prepare then we turn it off, now at commit time the server - * will send the entire transaction data along with the commit. With some more - * analysis, we can allow changing this option from off to on but not sure if - * that alone would be useful. - * * Finally, to avoid problems mentioned in previous paragraphs from any * subsequent (not READY) tablesyncs (need to toggle two_phase option from 'on' * to 'off' and then again back to 'on') there is a restriction for @@ -139,6 +132,113 @@ * failover = true when creating the subscription. Enabling failover allows us * to smoothly transition to the promoted standby, ensuring that we can * subscribe to the new primary without losing any data. + * + * RETAIN DEAD TUPLES + * ---------------------- + * Each apply worker that enabled retain_dead_tuples option maintains a + * non-removable transaction ID (oldest_nonremovable_xid) in shared memory to + * prevent dead rows from being removed prematurely when the apply worker still + * needs them to detect update_deleted conflicts. Additionally, this helps to + * retain the required commit_ts module information, which further helps to + * detect update_origin_differs and delete_origin_differs conflicts reliably, as + * otherwise, vacuum freeze could remove the required information. + * + * The logical replication launcher manages an internal replication slot named + * "pg_conflict_detection". It asynchronously aggregates the non-removable + * transaction ID from all apply workers to determine the appropriate xmin for + * the slot, thereby retaining necessary tuples. + * + * The non-removable transaction ID in the apply worker is advanced to the + * oldest running transaction ID once all concurrent transactions on the + * publisher have been applied and flushed locally. The process involves: + * + * - RDT_GET_CANDIDATE_XID: + * Call GetOldestActiveTransactionId() to take oldestRunningXid as the + * candidate xid. + * + * - RDT_REQUEST_PUBLISHER_STATUS: + * Send a message to the walsender requesting the publisher status, which + * includes the latest WAL write position and information about transactions + * that are in the commit phase. + * + * - RDT_WAIT_FOR_PUBLISHER_STATUS: + * Wait for the status from the walsender. After receiving the first status, + * do not proceed if there are concurrent remote transactions that are still + * in the commit phase. These transactions might have been assigned an + * earlier commit timestamp but have not yet written the commit WAL record. + * Continue to request the publisher status (RDT_REQUEST_PUBLISHER_STATUS) + * until all these transactions have completed. + * + * - RDT_WAIT_FOR_LOCAL_FLUSH: + * Advance the non-removable transaction ID if the current flush location has + * reached or surpassed the last received WAL position. + * + * - RDT_STOP_CONFLICT_INFO_RETENTION: + * This phase is required only when max_retention_duration is defined. We + * enter this phase if the wait time in either the + * RDT_WAIT_FOR_PUBLISHER_STATUS or RDT_WAIT_FOR_LOCAL_FLUSH phase exceeds + * configured max_retention_duration. In this phase, + * pg_subscription.subretentionactive is updated to false within a new + * transaction, and oldest_nonremovable_xid is set to InvalidTransactionId. + * + * - RDT_RESUME_CONFLICT_INFO_RETENTION: + * This phase is required only when max_retention_duration is defined. We + * enter this phase if the retention was previously stopped, and the time + * required to advance the non-removable transaction ID in the + * RDT_WAIT_FOR_LOCAL_FLUSH phase has decreased to within acceptable limits + * (or if max_retention_duration is set to 0). During this phase, + * pg_subscription.subretentionactive is updated to true within a new + * transaction, and the worker will be restarted. + * + * The overall state progression is: GET_CANDIDATE_XID -> + * REQUEST_PUBLISHER_STATUS -> WAIT_FOR_PUBLISHER_STATUS -> (loop to + * REQUEST_PUBLISHER_STATUS till concurrent remote transactions end) -> + * WAIT_FOR_LOCAL_FLUSH -> loop back to GET_CANDIDATE_XID. + * + * Retaining the dead tuples for this period is sufficient for ensuring + * eventual consistency using last-update-wins strategy, as dead tuples are + * useful for detecting conflicts only during the application of concurrent + * transactions from remote nodes. After applying and flushing all remote + * transactions that occurred concurrently with the tuple DELETE, any + * subsequent UPDATE from a remote node should have a later timestamp. In such + * cases, it is acceptable to detect an update_missing scenario and convert the + * UPDATE to an INSERT when applying it. But, for concurrent remote + * transactions with earlier timestamps than the DELETE, detecting + * update_deleted is necessary, as the UPDATEs in remote transactions should be + * ignored if their timestamp is earlier than that of the dead tuples. + * + * Note that advancing the non-removable transaction ID is not supported if the + * publisher is also a physical standby. This is because the logical walsender + * on the standby can only get the WAL replay position but there may be more + * WALs that are being replicated from the primary and those WALs could have + * earlier commit timestamp. + * + * Similarly, when the publisher has subscribed to another publisher, + * information necessary for conflict detection cannot be retained for + * changes from origins other than the publisher. This is because publisher + * lacks the information on concurrent transactions of other publishers to + * which it subscribes. As the information on concurrent transactions is + * unavailable beyond subscriber's immediate publishers, the non-removable + * transaction ID might be advanced prematurely before changes from other + * origins have been fully applied. + * + * XXX Retaining information for changes from other origins might be possible + * by requesting the subscription on that origin to enable retain_dead_tuples + * and fetching the conflict detection slot.xmin along with the publisher's + * status. In the RDT_WAIT_FOR_PUBLISHER_STATUS phase, the apply worker could + * wait for the remote slot's xmin to reach the oldest active transaction ID, + * ensuring that all transactions from other origins have been applied on the + * publisher, thereby getting the latest WAL position that includes all + * concurrent changes. However, this approach may impact performance, so it + * might not worth the effort. + * + * XXX It seems feasible to get the latest commit's WAL location from the + * publisher and wait till that is applied. However, we can't do that + * because commit timestamps can regress as a commit with a later LSN is not + * guaranteed to have a later timestamp than those with earlier LSNs. Having + * said that, even if that is possible, it won't improve performance much as + * the apply always lag and moves slowly as compared with the transactions + * on the publisher. *------------------------------------------------------------------------- */ @@ -147,14 +247,18 @@ #include #include +#include "access/genam.h" +#include "access/commit_ts.h" #include "access/table.h" #include "access/tableam.h" +#include "access/tupconvert.h" #include "access/twophase.h" #include "access/xact.h" #include "catalog/indexing.h" #include "catalog/pg_inherits.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" +#include "commands/subscriptioncmds.h" #include "commands/tablecmds.h" #include "commands/trigger.h" #include "executor/executor.h" @@ -164,6 +268,7 @@ #include "optimizer/optimizer.h" #include "parser/parse_relation.h" #include "pgstat.h" +#include "port/pg_bitutils.h" #include "postmaster/bgworker.h" #include "postmaster/interrupt.h" #include "postmaster/walwriter.h" @@ -173,15 +278,17 @@ #include "replication/logicalrelation.h" #include "replication/logicalworker.h" #include "replication/origin.h" +#include "replication/slot.h" #include "replication/walreceiver.h" #include "replication/worker_internal.h" #include "rewrite/rewriteHandler.h" #include "storage/buffile.h" #include "storage/ipc.h" +#include "storage/latch.h" #include "storage/lmgr.h" +#include "storage/procarray.h" #include "tcop/tcopprot.h" #include "utils/acl.h" -#include "utils/dynahash.h" #include "utils/guc.h" #include "utils/inval.h" #include "utils/lsyscache.h" @@ -192,6 +299,7 @@ #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/usercontext.h" +#include "utils/wait_event.h" #define NAPTIME_PER_CYCLE 1000 /* max sleep time between cycles (1s) */ @@ -275,6 +383,83 @@ typedef enum TRANS_PARALLEL_APPLY, } TransApplyAction; +/* + * The phases involved in advancing the non-removable transaction ID. + * + * See comments atop worker.c for details of the transition between these + * phases. + */ +typedef enum +{ + RDT_GET_CANDIDATE_XID, + RDT_REQUEST_PUBLISHER_STATUS, + RDT_WAIT_FOR_PUBLISHER_STATUS, + RDT_WAIT_FOR_LOCAL_FLUSH, + RDT_STOP_CONFLICT_INFO_RETENTION, + RDT_RESUME_CONFLICT_INFO_RETENTION, +} RetainDeadTuplesPhase; + +/* + * Critical information for managing phase transitions within the + * RetainDeadTuplesPhase. + */ +typedef struct RetainDeadTuplesData +{ + RetainDeadTuplesPhase phase; /* current phase */ + XLogRecPtr remote_lsn; /* WAL write position on the publisher */ + + /* + * Oldest transaction ID that was in the commit phase on the publisher. + * Use FullTransactionId to prevent issues with transaction ID wraparound, + * where a new remote_oldestxid could falsely appear to originate from the + * past and block advancement. + */ + FullTransactionId remote_oldestxid; + + /* + * Next transaction ID to be assigned on the publisher. Use + * FullTransactionId for consistency and to allow straightforward + * comparisons with remote_oldestxid. + */ + FullTransactionId remote_nextxid; + + TimestampTz reply_time; /* when the publisher responds with status */ + + /* + * Publisher transaction ID that must be awaited to complete before + * entering the final phase (RDT_WAIT_FOR_LOCAL_FLUSH). Use + * FullTransactionId for the same reason as remote_nextxid. + */ + FullTransactionId remote_wait_for; + + TransactionId candidate_xid; /* candidate for the non-removable + * transaction ID */ + TimestampTz flushpos_update_time; /* when the remote flush position was + * updated in final phase + * (RDT_WAIT_FOR_LOCAL_FLUSH) */ + + long table_sync_wait_time; /* time spent waiting for table sync + * to finish */ + + /* + * The following fields are used to determine the timing for the next + * round of transaction ID advancement. + */ + TimestampTz last_recv_time; /* when the last message was received */ + TimestampTz candidate_xid_time; /* when the candidate_xid is decided */ + int xid_advance_interval; /* how much time (ms) to wait before + * attempting to advance the + * non-removable transaction ID */ +} RetainDeadTuplesData; + +/* + * The minimum (100ms) and maximum (3 minutes) intervals for advancing + * non-removable transaction IDs. The maximum interval is a bit arbitrary but + * is sufficient to not cause any undue network traffic. + */ +#define MIN_XID_ADVANCE_INTERVAL 100 +#define MAX_XID_ADVANCE_INTERVAL 180000 + /* errcontext tracker */ static ApplyErrorCallbackArg apply_error_callback_arg = { @@ -334,16 +519,23 @@ bool InitializingApplyWorker = false; * by the user. */ static XLogRecPtr skip_xact_finish_lsn = InvalidXLogRecPtr; -#define is_skipping_changes() (unlikely(!XLogRecPtrIsInvalid(skip_xact_finish_lsn))) +#define is_skipping_changes() (unlikely(XLogRecPtrIsValid(skip_xact_finish_lsn))) /* BufFile handle of the current streaming file */ static BufFile *stream_fd = NULL; +/* + * The remote WAL position that has been applied and flushed locally. We record + * and use this information both while sending feedback to the server and + * advancing oldest_nonremovable_xid. + */ +static XLogRecPtr last_flushpos = InvalidXLogRecPtr; + typedef struct SubXactInfo { TransactionId xid; /* XID of the subxact */ int fileno; /* file number in the buffile */ - off_t offset; /* offset in the file */ + pgoff_t offset; /* offset in the file */ } SubXactInfo; /* Sub-transaction data for the current streaming transaction */ @@ -379,6 +571,26 @@ static void stream_close_file(void); static void send_feedback(XLogRecPtr recvpos, bool force, bool requestReply); +static void maybe_advance_nonremovable_xid(RetainDeadTuplesData *rdt_data, + bool status_received); +static bool can_advance_nonremovable_xid(RetainDeadTuplesData *rdt_data); +static void process_rdt_phase_transition(RetainDeadTuplesData *rdt_data, + bool status_received); +static void get_candidate_xid(RetainDeadTuplesData *rdt_data); +static void request_publisher_status(RetainDeadTuplesData *rdt_data); +static void wait_for_publisher_status(RetainDeadTuplesData *rdt_data, + bool status_received); +static void wait_for_local_flush(RetainDeadTuplesData *rdt_data); +static bool should_stop_conflict_info_retention(RetainDeadTuplesData *rdt_data); +static void stop_conflict_info_retention(RetainDeadTuplesData *rdt_data); +static void resume_conflict_info_retention(RetainDeadTuplesData *rdt_data); +static bool update_retention_status(bool active); +static void reset_retention_data_fields(RetainDeadTuplesData *rdt_data); +static void adjust_xid_advance_interval(RetainDeadTuplesData *rdt_data, + bool new_xid_found); + +static void apply_worker_exit(void); + static void apply_handle_commit_internal(LogicalRepCommitData *commit_data); static void apply_handle_insert_internal(ApplyExecutionData *edata, ResultRelInfo *relinfo, @@ -397,6 +609,12 @@ static bool FindReplTupleInLocalRel(ApplyExecutionData *edata, Relation localrel Oid localidxoid, TupleTableSlot *remoteslot, TupleTableSlot **localslot); +static bool FindDeletedTupleInLocalRel(Relation localrel, + Oid localidxoid, + TupleTableSlot *remoteslot, + TransactionId *delete_xid, + ReplOriginId *delete_origin, + TimestampTz *delete_time); static void apply_handle_tuple_routing(ApplyExecutionData *edata, TupleTableSlot *remoteslot, LogicalRepTupleData *newtup, @@ -414,7 +632,9 @@ static inline void reset_apply_error_context_info(void); static TransApplyAction get_transaction_apply_action(TransactionId xid, ParallelApplyWorkerInfo **winfo); -static void replorigin_reset(int code, Datum arg); +static void set_wal_receiver_timeout(void); + +static void on_exit_clear_xact_state(int code, Datum arg); /* * Form the origin name for the subscription. @@ -489,6 +709,11 @@ should_apply_changes_for_rel(LogicalRepRelMapEntry *rel) (rel->state == SUBREL_STATE_SYNCDONE && rel->statelsn <= remote_final_lsn)); + case WORKERTYPE_SEQUENCESYNC: + /* Should never happen. */ + elog(ERROR, "sequence synchronization worker is not expected to apply changes"); + break; + case WORKERTYPE_UNKNOWN: /* Should never happen. */ elog(ERROR, "Unknown worker type"); @@ -621,7 +846,7 @@ handle_streamed_transaction(LogicalRepMsgType action, StringInfo s) */ pa_switch_to_partial_serialize(winfo, false); - /* fall through */ + pg_fallthrough; case TRANS_LEADER_PARTIAL_SERIALIZE: stream_write_change(action, &original_msg); @@ -657,7 +882,7 @@ create_edata_for_relation(LogicalRepRelMapEntry *rel) List *perminfos = NIL; ResultRelInfo *resultRelInfo; - edata = (ApplyExecutionData *) palloc0(sizeof(ApplyExecutionData)); + edata = palloc0_object(ApplyExecutionData); edata->targetRel = rel; edata->estate = estate = CreateExecutorState(); @@ -756,15 +981,16 @@ slot_fill_defaults(LogicalRepRelMapEntry *rel, EState *estate, if (num_phys_attrs == rel->remoterel.natts) return; - defmap = (int *) palloc(num_phys_attrs * sizeof(int)); - defexprs = (ExprState **) palloc(num_phys_attrs * sizeof(ExprState *)); + defmap = palloc_array(int, num_phys_attrs); + defexprs = palloc_array(ExprState *, num_phys_attrs); Assert(rel->attrmap->maplen == num_phys_attrs); for (attnum = 0; attnum < num_phys_attrs; attnum++) { + CompactAttribute *cattr = TupleDescCompactAttr(desc, attnum); Expr *defexpr; - if (TupleDescAttr(desc, attnum)->attisdropped || TupleDescAttr(desc, attnum)->attgenerated) + if (cattr->attisdropped || cattr->attgenerated) continue; if (rel->attrmap->attnums[attnum] >= 0) @@ -1023,14 +1249,17 @@ apply_handle_commit(StringInfo s) if (commit_data.commit_lsn != remote_final_lsn) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg_internal("incorrect commit LSN %X/%X in commit message (expected %X/%X)", + errmsg_internal("incorrect commit LSN %X/%08X in commit message (expected %X/%08X)", LSN_FORMAT_ARGS(commit_data.commit_lsn), LSN_FORMAT_ARGS(remote_final_lsn)))); apply_handle_commit_internal(&commit_data); - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(commit_data.end_lsn); + /* + * Process any tables that are being synchronized in parallel, as well as + * any newly added tables or sequences. + */ + ProcessSyncingRelations(commit_data.end_lsn); pgstat_report_activity(STATE_IDLE, NULL); reset_apply_error_context_info(); @@ -1096,8 +1325,8 @@ apply_handle_prepare_internal(LogicalRepPreparedTxnData *prepare_data) * Update origin state so we can restart streaming from correct position * in case of crash. */ - replorigin_session_origin_lsn = prepare_data->end_lsn; - replorigin_session_origin_timestamp = prepare_data->prepare_time; + replorigin_xact_state.origin_lsn = prepare_data->end_lsn; + replorigin_xact_state.origin_timestamp = prepare_data->prepare_time; PrepareTransactionBlock(gid); } @@ -1115,7 +1344,7 @@ apply_handle_prepare(StringInfo s) if (prepare_data.prepare_lsn != remote_final_lsn) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg_internal("incorrect prepare LSN %X/%X in prepare message (expected %X/%X)", + errmsg_internal("incorrect prepare LSN %X/%08X in prepare message (expected %X/%08X)", LSN_FORMAT_ARGS(prepare_data.prepare_lsn), LSN_FORMAT_ARGS(remote_final_lsn)))); @@ -1151,8 +1380,11 @@ apply_handle_prepare(StringInfo s) in_remote_transaction = false; - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(prepare_data.end_lsn); + /* + * Process any tables that are being synchronized in parallel, as well as + * any newly added tables or sequences. + */ + ProcessSyncingRelations(prepare_data.end_lsn); /* * Since we have already prepared the transaction, in a case where the @@ -1196,8 +1428,8 @@ apply_handle_commit_prepared(StringInfo s) * Update origin state so we can restart streaming from correct position * in case of crash. */ - replorigin_session_origin_lsn = prepare_data.end_lsn; - replorigin_session_origin_timestamp = prepare_data.commit_time; + replorigin_xact_state.origin_lsn = prepare_data.end_lsn; + replorigin_xact_state.origin_timestamp = prepare_data.commit_time; FinishPreparedTransaction(gid, true); end_replication_step(); @@ -1207,8 +1439,11 @@ apply_handle_commit_prepared(StringInfo s) store_flush_position(prepare_data.end_lsn, XactLastCommitEnd); in_remote_transaction = false; - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(prepare_data.end_lsn); + /* + * Process any tables that are being synchronized in parallel, as well as + * any newly added tables or sequences. + */ + ProcessSyncingRelations(prepare_data.end_lsn); clear_subscription_skip_lsn(prepare_data.end_lsn); @@ -1251,8 +1486,8 @@ apply_handle_rollback_prepared(StringInfo s) * Update origin state so we can restart streaming from correct * position in case of crash. */ - replorigin_session_origin_lsn = rollback_data.rollback_end_lsn; - replorigin_session_origin_timestamp = rollback_data.rollback_time; + replorigin_xact_state.origin_lsn = rollback_data.rollback_end_lsn; + replorigin_xact_state.origin_timestamp = rollback_data.rollback_time; /* There is no transaction when ABORT/ROLLBACK PREPARED is called */ begin_replication_step(); @@ -1273,8 +1508,11 @@ apply_handle_rollback_prepared(StringInfo s) store_flush_position(rollback_data.rollback_end_lsn, InvalidXLogRecPtr); in_remote_transaction = false; - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(rollback_data.rollback_end_lsn); + /* + * Process any tables that are being synchronized in parallel, as well as + * any newly added tables or sequences. + */ + ProcessSyncingRelations(rollback_data.rollback_end_lsn); pgstat_report_activity(STATE_IDLE, NULL); reset_apply_error_context_info(); @@ -1355,7 +1593,7 @@ apply_handle_stream_prepare(StringInfo s) */ pa_switch_to_partial_serialize(winfo, true); - /* fall through */ + pg_fallthrough; case TRANS_LEADER_PARTIAL_SERIALIZE: Assert(winfo); @@ -1408,8 +1646,11 @@ apply_handle_stream_prepare(StringInfo s) pgstat_report_stat(false); - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(prepare_data.end_lsn); + /* + * Process any tables that are being synchronized in parallel, as well as + * any newly added tables or sequences. + */ + ProcessSyncingRelations(prepare_data.end_lsn); /* * Similar to prepare case, the subskiplsn could be left in a case of @@ -1468,7 +1709,7 @@ stream_start_internal(TransactionId xid, bool first_segment) oldctx = MemoryContextSwitchTo(ApplyContext); - MyLogicalRepWorker->stream_fileset = palloc(sizeof(FileSet)); + MyLogicalRepWorker->stream_fileset = palloc_object(FileSet); FileSetInit(MyLogicalRepWorker->stream_fileset); MemoryContextSwitchTo(oldctx); @@ -1574,7 +1815,7 @@ apply_handle_stream_start(StringInfo s) */ pa_switch_to_partial_serialize(winfo, !first_segment); - /* fall through */ + pg_fallthrough; case TRANS_LEADER_PARTIAL_SERIALIZE: Assert(winfo); @@ -1603,7 +1844,8 @@ apply_handle_stream_start(StringInfo s) * Signal the leader apply worker, as it may be waiting for * us. */ - logicalrep_worker_wakeup(MyLogicalRepWorker->subid, InvalidOid); + logicalrep_worker_wakeup(WORKERTYPE_APPLY, + MyLogicalRepWorker->subid, InvalidOid); } parallel_stream_nchanges = 0; @@ -1688,7 +1930,7 @@ apply_handle_stream_stop(StringInfo s) */ pa_switch_to_partial_serialize(winfo, true); - /* fall through */ + pg_fallthrough; case TRANS_LEADER_PARTIAL_SERIALIZE: stream_write_change(LOGICAL_REP_MSG_STREAM_STOP, s); stream_stop_internal(stream_xid); @@ -1934,7 +2176,7 @@ apply_handle_stream_abort(StringInfo s) */ pa_switch_to_partial_serialize(winfo, true); - /* fall through */ + pg_fallthrough; case TRANS_LEADER_PARTIAL_SERIALIZE: Assert(winfo); @@ -1991,12 +2233,12 @@ apply_handle_stream_abort(StringInfo s) */ static void ensure_last_message(FileSet *stream_fileset, TransactionId xid, int fileno, - off_t offset) + pgoff_t offset) { char path[MAXPGPATH]; BufFile *fd; int last_fileno; - off_t last_offset; + pgoff_t last_offset; Assert(!IsTransactionState()); @@ -2031,7 +2273,7 @@ apply_spooled_messages(FileSet *stream_fileset, TransactionId xid, MemoryContext oldcxt; ResourceOwner oldowner; int fileno; - off_t offset; + pgoff_t offset; if (!am_parallel_apply_worker()) maybe_start_skipping_changes(lsn); @@ -2207,7 +2449,7 @@ apply_handle_stream_commit(StringInfo s) */ pa_switch_to_partial_serialize(winfo, true); - /* fall through */ + pg_fallthrough; case TRANS_LEADER_PARTIAL_SERIALIZE: Assert(winfo); @@ -2250,8 +2492,11 @@ apply_handle_stream_commit(StringInfo s) break; } - /* Process any tables that are being synchronized in parallel. */ - process_syncing_tables(commit_data.end_lsn); + /* + * Process any tables that are being synchronized in parallel, as well as + * any newly added tables or sequences. + */ + ProcessSyncingRelations(commit_data.end_lsn); pgstat_report_activity(STATE_IDLE, NULL); @@ -2288,8 +2533,8 @@ apply_handle_commit_internal(LogicalRepCommitData *commit_data) * Update origin state so we can restart streaming from correct * position in case of crash. */ - replorigin_session_origin_lsn = commit_data->end_lsn; - replorigin_session_origin_timestamp = commit_data->committime; + replorigin_xact_state.origin_lsn = commit_data->end_lsn; + replorigin_xact_state.origin_timestamp = commit_data->committime; CommitTransactionCommand(); @@ -2620,7 +2865,7 @@ apply_handle_update(StringInfo s) target_perminfo = list_nth(estate->es_rteperminfos, 0); for (int i = 0; i < remoteslot->tts_tupleDescriptor->natts; i++) { - Form_pg_attribute att = TupleDescAttr(remoteslot->tts_tupleDescriptor, i); + CompactAttribute *att = TupleDescCompactAttr(remoteslot->tts_tupleDescriptor, i); int remoteattnum = rel->attrmap->attnums[i]; if (!att->attisdropped && remoteattnum >= 0) @@ -2702,7 +2947,7 @@ apply_handle_update_internal(ApplyExecutionData *edata, */ if (GetTupleTransactionInfo(localslot, &conflicttuple.xmin, &conflicttuple.origin, &conflicttuple.ts) && - conflicttuple.origin != replorigin_session_origin) + conflicttuple.origin != replorigin_xact_state.origin) { TupleTableSlot *newslot; @@ -2733,17 +2978,31 @@ apply_handle_update_internal(ApplyExecutionData *edata, } else { + ConflictType type; TupleTableSlot *newslot = localslot; + /* + * Detecting whether the tuple was recently deleted or never existed + * is crucial to avoid misleading the user during conflict handling. + */ + if (FindDeletedTupleInLocalRel(localrel, localindexoid, remoteslot, + &conflicttuple.xmin, + &conflicttuple.origin, + &conflicttuple.ts) && + conflicttuple.origin != replorigin_xact_state.origin) + type = CT_UPDATE_DELETED; + else + type = CT_UPDATE_MISSING; + /* Store the new tuple for conflict reporting */ slot_store_data(newslot, relmapentry, newtup); /* - * The tuple to be updated could not be found. Do nothing except for - * emitting a log message. + * The tuple to be updated could not be found or was deleted. Do + * nothing except for emitting a log message. */ - ReportApplyConflict(estate, relinfo, LOG, CT_UPDATE_MISSING, - remoteslot, newslot, list_make1(&conflicttuple)); + ReportApplyConflict(estate, relinfo, LOG, type, remoteslot, newslot, + list_make1(&conflicttuple)); } /* Cleanup. */ @@ -2883,7 +3142,7 @@ apply_handle_delete_internal(ApplyExecutionData *edata, */ if (GetTupleTransactionInfo(localslot, &conflicttuple.xmin, &conflicttuple.origin, &conflicttuple.ts) && - conflicttuple.origin != replorigin_session_origin) + conflicttuple.origin != replorigin_xact_state.origin) { conflicttuple.slot = localslot; ReportApplyConflict(estate, relinfo, LOG, CT_DELETE_ORIGIN_DIFFERS, @@ -2963,6 +3222,135 @@ FindReplTupleInLocalRel(ApplyExecutionData *edata, Relation localrel, return found; } +/* + * Determine whether the index can reliably locate the deleted tuple in the + * local relation. + * + * An index may exclude deleted tuples if it was re-indexed or re-created during + * change application. Therefore, an index is considered usable only if the + * conflict detection slot.xmin (conflict_detection_xmin) is greater than the + * index tuple's xmin. This ensures that any tuples deleted prior to the index + * creation or re-indexing are not relevant for conflict detection in the + * current apply worker. + * + * Note that indexes may also be excluded if they were modified by other DDL + * operations, such as ALTER INDEX. However, this is acceptable, as the + * likelihood of such DDL changes coinciding with the need to scan dead + * tuples for the update_deleted is low. + */ +static bool +IsIndexUsableForFindingDeletedTuple(Oid localindexoid, + TransactionId conflict_detection_xmin) +{ + HeapTuple index_tuple; + TransactionId index_xmin; + + index_tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(localindexoid)); + + if (!HeapTupleIsValid(index_tuple)) /* should not happen */ + elog(ERROR, "cache lookup failed for index %u", localindexoid); + + /* + * No need to check for a frozen transaction ID, as + * TransactionIdPrecedes() manages it internally, treating it as falling + * behind the conflict_detection_xmin. + */ + index_xmin = HeapTupleHeaderGetXmin(index_tuple->t_data); + + ReleaseSysCache(index_tuple); + + return TransactionIdPrecedes(index_xmin, conflict_detection_xmin); +} + +/* + * Attempts to locate a deleted tuple in the local relation that matches the + * values of the tuple received from the publication side (in 'remoteslot'). + * The search is performed using either the replica identity index, primary + * key, other available index, or a sequential scan if necessary. + * + * Returns true if the deleted tuple is found. If found, the transaction ID, + * origin, and commit timestamp of the deletion are stored in '*delete_xid', + * '*delete_origin', and '*delete_time' respectively. + */ +static bool +FindDeletedTupleInLocalRel(Relation localrel, Oid localidxoid, + TupleTableSlot *remoteslot, + TransactionId *delete_xid, ReplOriginId *delete_origin, + TimestampTz *delete_time) +{ + TransactionId oldestxmin; + + /* + * Return false if either dead tuples are not retained or commit timestamp + * data is not available. + */ + if (!MySubscription->retaindeadtuples || !track_commit_timestamp) + return false; + + /* + * For conflict detection, we use the leader worker's + * oldest_nonremovable_xid value instead of invoking + * GetOldestNonRemovableTransactionId() or using the conflict detection + * slot's xmin. The oldest_nonremovable_xid acts as a threshold to + * identify tuples that were recently deleted. These deleted tuples are no + * longer visible to concurrent transactions. However, if a remote update + * matches such a tuple, we log an update_deleted conflict. + * + * While GetOldestNonRemovableTransactionId() and slot.xmin may return + * transaction IDs older than oldest_nonremovable_xid, for our current + * purpose, it is acceptable to treat tuples deleted by transactions prior + * to oldest_nonremovable_xid as update_missing conflicts. + */ + if (am_leader_apply_worker()) + { + oldestxmin = MyLogicalRepWorker->oldest_nonremovable_xid; + } + else + { + LogicalRepWorker *leader; + + /* + * Obtain the information from the leader apply worker as only the + * leader manages oldest_nonremovable_xid (see + * maybe_advance_nonremovable_xid() for details). + */ + LWLockAcquire(LogicalRepWorkerLock, LW_SHARED); + leader = logicalrep_worker_find(WORKERTYPE_APPLY, + MyLogicalRepWorker->subid, InvalidOid, + false); + if (!leader) + { + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not detect conflict as the leader apply worker has exited"))); + } + + SpinLockAcquire(&leader->relmutex); + oldestxmin = leader->oldest_nonremovable_xid; + SpinLockRelease(&leader->relmutex); + LWLockRelease(LogicalRepWorkerLock); + } + + /* + * Return false if the leader apply worker has stopped retaining + * information for detecting conflicts. This implies that update_deleted + * can no longer be reliably detected. + */ + if (!TransactionIdIsValid(oldestxmin)) + return false; + + if (OidIsValid(localidxoid) && + IsIndexUsableForFindingDeletedTuple(localidxoid, oldestxmin)) + return RelationFindDeletedTupleInfoByIndex(localrel, localidxoid, + remoteslot, oldestxmin, + delete_xid, delete_origin, + delete_time); + else + return RelationFindDeletedTupleInfoSeq(localrel, remoteslot, + oldestxmin, delete_xid, + delete_origin, delete_time); +} + /* * This handles insert, update, delete on a partitioned table. */ @@ -3012,6 +3400,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, * at CREATE/ALTER SUBSCRIPTION would be insufficient. */ CheckSubscriptionRelkind(partrel->rd_rel->relkind, + relmapentry->remoterel.relkind, get_namespace_name(RelationGetNamespace(partrel)), RelationGetRelationName(partrel)); @@ -3081,18 +3470,35 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, remoteslot_part, &localslot); if (!found) { + ConflictType type; TupleTableSlot *newslot = localslot; + /* + * Detecting whether the tuple was recently deleted or + * never existed is crucial to avoid misleading the user + * during conflict handling. + */ + if (FindDeletedTupleInLocalRel(partrel, + part_entry->localindexoid, + remoteslot_part, + &conflicttuple.xmin, + &conflicttuple.origin, + &conflicttuple.ts) && + conflicttuple.origin != replorigin_xact_state.origin) + type = CT_UPDATE_DELETED; + else + type = CT_UPDATE_MISSING; + /* Store the new tuple for conflict reporting */ slot_store_data(newslot, part_entry, newtup); /* - * The tuple to be updated could not be found. Do nothing - * except for emitting a log message. + * The tuple to be updated could not be found or was + * deleted. Do nothing except for emitting a log message. */ ReportApplyConflict(estate, partrelinfo, LOG, - CT_UPDATE_MISSING, remoteslot_part, - newslot, list_make1(&conflicttuple)); + type, remoteslot_part, newslot, + list_make1(&conflicttuple)); return; } @@ -3104,7 +3510,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, if (GetTupleTransactionInfo(localslot, &conflicttuple.xmin, &conflicttuple.origin, &conflicttuple.ts) && - conflicttuple.origin != replorigin_session_origin) + conflicttuple.origin != replorigin_xact_state.origin) { TupleTableSlot *newslot; @@ -3191,6 +3597,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, /* Check that new partition also has supported relkind. */ CheckSubscriptionRelkind(partrel_new->rd_rel->relkind, + relmapentry->remoterel.relkind, get_namespace_name(RelationGetNamespace(partrel_new)), RelationGetRelationName(partrel_new)); @@ -3551,7 +3958,7 @@ store_flush_position(XLogRecPtr remote_lsn, XLogRecPtr local_lsn) MemoryContextSwitchTo(ApplyContext); /* Track commit lsn */ - flushpos = (FlushPosition *) palloc(sizeof(FlushPosition)); + flushpos = palloc_object(FlushPosition); flushpos->local_end = local_lsn; flushpos->remote_end = remote_lsn; @@ -3584,6 +3991,7 @@ LogicalRepApplyLoop(XLogRecPtr last_received) bool ping_sent = false; TimeLineID tli; ErrorContextCallback errcallback; + RetainDeadTuplesData rdt_data = {0}; /* * Init the ApplyMessageContext which we clean up after each replication @@ -3662,6 +4070,8 @@ LogicalRepApplyLoop(XLogRecPtr last_received) last_recv_timestamp = GetCurrentTimestamp(); ping_sent = false; + rdt_data.last_recv_time = last_recv_timestamp; + /* Ensure we are reading the data into our memory context. */ MemoryContextSwitchTo(ApplyMessageContext); @@ -3669,7 +4079,7 @@ LogicalRepApplyLoop(XLogRecPtr last_received) c = pq_getmsgbyte(&s); - if (c == 'w') + if (c == PqReplMsg_WALData) { XLogRecPtr start_lsn; XLogRecPtr end_lsn; @@ -3688,8 +4098,10 @@ LogicalRepApplyLoop(XLogRecPtr last_received) UpdateWorkerStats(last_received, send_time, false); apply_dispatch(&s); + + maybe_advance_nonremovable_xid(&rdt_data, false); } - else if (c == 'k') + else if (c == PqReplMsg_Keepalive) { XLogRecPtr end_lsn; TimestampTz timestamp; @@ -3703,8 +4115,31 @@ LogicalRepApplyLoop(XLogRecPtr last_received) last_received = end_lsn; send_feedback(last_received, reply_requested, false); + + maybe_advance_nonremovable_xid(&rdt_data, false); + UpdateWorkerStats(last_received, timestamp, true); } + else if (c == PqReplMsg_PrimaryStatusUpdate) + { + rdt_data.remote_lsn = pq_getmsgint64(&s); + rdt_data.remote_oldestxid = FullTransactionIdFromU64((uint64) pq_getmsgint64(&s)); + rdt_data.remote_nextxid = FullTransactionIdFromU64((uint64) pq_getmsgint64(&s)); + rdt_data.reply_time = pq_getmsgint64(&s); + + /* + * This should never happen, see + * ProcessStandbyPSRequestMessage. But if it happens + * due to a bug, we don't want to proceed as it can + * incorrectly advance oldest_nonremovable_xid. + */ + if (!XLogRecPtrIsValid(rdt_data.remote_lsn)) + elog(ERROR, "cannot get the latest WAL position from the publisher"); + + maybe_advance_nonremovable_xid(&rdt_data, true); + + UpdateWorkerStats(last_received, rdt_data.reply_time, false); + } /* other message types are purposefully ignored */ MemoryContextReset(ApplyMessageContext); @@ -3717,6 +4152,11 @@ LogicalRepApplyLoop(XLogRecPtr last_received) /* confirm all writes so far */ send_feedback(last_received, false, false); + /* Reset the timestamp if no message was received */ + rdt_data.last_recv_time = 0; + + maybe_advance_nonremovable_xid(&rdt_data, false); + if (!in_remote_transaction && !in_streamed_transaction) { /* @@ -3727,8 +4167,11 @@ LogicalRepApplyLoop(XLogRecPtr last_received) AcceptInvalidationMessages(); maybe_reread_subscription(); - /* Process any table synchronization changes. */ - process_syncing_tables(last_received); + /* + * Process any relations that are being synchronized in parallel + * and any newly added tables or sequences. + */ + ProcessSyncingRelations(last_received); } /* Cleanup the memory. */ @@ -3751,6 +4194,20 @@ LogicalRepApplyLoop(XLogRecPtr last_received) else wait_time = NAPTIME_PER_CYCLE; + /* + * Ensure to wake up when it's possible to advance the non-removable + * transaction ID, or when the retention duration may have exceeded + * max_retention_duration. + */ + if (MySubscription->retentionactive) + { + if (rdt_data.phase == RDT_GET_CANDIDATE_XID && + rdt_data.xid_advance_interval) + wait_time = Min(wait_time, rdt_data.xid_advance_interval); + else if (MySubscription->maxretention > 0) + wait_time = Min(wait_time, MySubscription->maxretention); + } + rc = WaitLatchOrSocket(MyLatch, WL_SOCKET_READABLE | WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, @@ -3814,6 +4271,8 @@ LogicalRepApplyLoop(XLogRecPtr last_received) send_feedback(last_received, requestReply, requestReply); + maybe_advance_nonremovable_xid(&rdt_data, false); + /* * Force reporting to ensure long idle periods don't lead to * arbitrarily delayed stats. Stats can only be reported outside @@ -3849,7 +4308,6 @@ send_feedback(XLogRecPtr recvpos, bool force, bool requestReply) static XLogRecPtr last_recvpos = InvalidXLogRecPtr; static XLogRecPtr last_writepos = InvalidXLogRecPtr; - static XLogRecPtr last_flushpos = InvalidXLogRecPtr; XLogRecPtr writepos; XLogRecPtr flushpos; @@ -3903,14 +4361,14 @@ send_feedback(XLogRecPtr recvpos, bool force, bool requestReply) else resetStringInfo(reply_message); - pq_sendbyte(reply_message, 'r'); + pq_sendbyte(reply_message, PqReplMsg_StandbyStatusUpdate); pq_sendint64(reply_message, recvpos); /* write */ pq_sendint64(reply_message, flushpos); /* flush */ pq_sendint64(reply_message, writepos); /* apply */ pq_sendint64(reply_message, now); /* sendTime */ pq_sendbyte(reply_message, requestReply); /* replyRequested */ - elog(DEBUG2, "sending feedback (force %d) to recv %X/%X, write %X/%X, flush %X/%X", + elog(DEBUG2, "sending feedback (force %d) to recv %X/%08X, write %X/%08X, flush %X/%08X", force, LSN_FORMAT_ARGS(recvpos), LSN_FORMAT_ARGS(writepos), @@ -3928,102 +4386,721 @@ send_feedback(XLogRecPtr recvpos, bool force, bool requestReply) } /* - * Exit routine for apply workers due to subscription parameter changes. + * Attempt to advance the non-removable transaction ID. + * + * See comments atop worker.c for details. */ static void -apply_worker_exit(void) +maybe_advance_nonremovable_xid(RetainDeadTuplesData *rdt_data, + bool status_received) { - if (am_parallel_apply_worker()) - { - /* - * Don't stop the parallel apply worker as the leader will detect the - * subscription parameter change and restart logical replication later - * anyway. This also prevents the leader from reporting errors when - * trying to communicate with a stopped parallel apply worker, which - * would accidentally disable subscriptions if disable_on_error was - * set. - */ + if (!can_advance_nonremovable_xid(rdt_data)) return; - } + process_rdt_phase_transition(rdt_data, status_received); +} + +/* + * Preliminary check to determine if advancing the non-removable transaction ID + * is allowed. + */ +static bool +can_advance_nonremovable_xid(RetainDeadTuplesData *rdt_data) +{ /* - * Reset the last-start time for this apply worker so that the launcher - * will restart it without waiting for wal_retrieve_retry_interval if the - * subscription is still active, and so that we won't leak that hash table - * entry if it isn't. + * It is sufficient to manage non-removable transaction ID for a + * subscription by the main apply worker to detect update_deleted reliably + * even for table sync or parallel apply workers. */ - if (am_leader_apply_worker()) - ApplyLauncherForgetWorkerStartTime(MyLogicalRepWorker->subid); + if (!am_leader_apply_worker()) + return false; - proc_exit(0); + /* No need to advance if retaining dead tuples is not required */ + if (!MySubscription->retaindeadtuples) + return false; + + return true; } /* - * Reread subscription info if needed. - * - * For significant changes, we react by exiting the current process; a new - * one will be launched afterwards if needed. + * Process phase transitions during the non-removable transaction ID + * advancement. See comments atop worker.c for details of the transition. */ -void -maybe_reread_subscription(void) +static void +process_rdt_phase_transition(RetainDeadTuplesData *rdt_data, + bool status_received) { - MemoryContext oldctx; - Subscription *newsub; - bool started_tx = false; - - /* When cache state is valid there is nothing to do here. */ - if (MySubscriptionValid) - return; - - /* This function might be called inside or outside of transaction. */ - if (!IsTransactionState()) + switch (rdt_data->phase) { - StartTransactionCommand(); - started_tx = true; + case RDT_GET_CANDIDATE_XID: + get_candidate_xid(rdt_data); + break; + case RDT_REQUEST_PUBLISHER_STATUS: + request_publisher_status(rdt_data); + break; + case RDT_WAIT_FOR_PUBLISHER_STATUS: + wait_for_publisher_status(rdt_data, status_received); + break; + case RDT_WAIT_FOR_LOCAL_FLUSH: + wait_for_local_flush(rdt_data); + break; + case RDT_STOP_CONFLICT_INFO_RETENTION: + stop_conflict_info_retention(rdt_data); + break; + case RDT_RESUME_CONFLICT_INFO_RETENTION: + resume_conflict_info_retention(rdt_data); + break; } +} - /* Ensure allocations in permanent context. */ - oldctx = MemoryContextSwitchTo(ApplyContext); +/* + * Workhorse for the RDT_GET_CANDIDATE_XID phase. + */ +static void +get_candidate_xid(RetainDeadTuplesData *rdt_data) +{ + TransactionId oldest_running_xid; + TimestampTz now; - newsub = GetSubscription(MyLogicalRepWorker->subid, true); + /* + * Use last_recv_time when applying changes in the loop to avoid + * unnecessary system time retrieval. If last_recv_time is not available, + * obtain the current timestamp. + */ + now = rdt_data->last_recv_time ? rdt_data->last_recv_time : GetCurrentTimestamp(); /* - * Exit if the subscription was removed. This normally should not happen - * as the worker gets killed during DROP SUBSCRIPTION. + * Compute the candidate_xid and request the publisher status at most once + * per xid_advance_interval. Refer to adjust_xid_advance_interval() for + * details on how this value is dynamically adjusted. This is to avoid + * using CPU and network resources without making much progress. */ - if (!newsub) - { - ereport(LOG, - (errmsg("logical replication worker for subscription \"%s\" will stop because the subscription was removed", - MySubscription->name))); + if (!TimestampDifferenceExceeds(rdt_data->candidate_xid_time, now, + rdt_data->xid_advance_interval)) + return; - /* Ensure we remove no-longer-useful entry for worker's start time */ - if (am_leader_apply_worker()) - ApplyLauncherForgetWorkerStartTime(MyLogicalRepWorker->subid); + /* + * Immediately update the timer, even if the function returns later + * without setting candidate_xid due to inactivity on the subscriber. This + * avoids frequent calls to GetOldestActiveTransactionId. + */ + rdt_data->candidate_xid_time = now; - proc_exit(0); + /* + * Consider transactions in the current database, as only dead tuples from + * this database are required for conflict detection. + */ + oldest_running_xid = GetOldestActiveTransactionId(false, false); + + /* + * Oldest active transaction ID (oldest_running_xid) can't be behind any + * of its previously computed value. + */ + Assert(TransactionIdPrecedesOrEquals(MyLogicalRepWorker->oldest_nonremovable_xid, + oldest_running_xid)); + + /* Return if the oldest_nonremovable_xid cannot be advanced */ + if (TransactionIdEquals(MyLogicalRepWorker->oldest_nonremovable_xid, + oldest_running_xid)) + { + adjust_xid_advance_interval(rdt_data, false); + return; } - /* Exit if the subscription was disabled. */ - if (!newsub->enabled) + adjust_xid_advance_interval(rdt_data, true); + + rdt_data->candidate_xid = oldest_running_xid; + rdt_data->phase = RDT_REQUEST_PUBLISHER_STATUS; + + /* process the next phase */ + process_rdt_phase_transition(rdt_data, false); +} + +/* + * Workhorse for the RDT_REQUEST_PUBLISHER_STATUS phase. + */ +static void +request_publisher_status(RetainDeadTuplesData *rdt_data) +{ + static StringInfo request_message = NULL; + + if (!request_message) { - ereport(LOG, - (errmsg("logical replication worker for subscription \"%s\" will stop because the subscription was disabled", - MySubscription->name))); + MemoryContext oldctx = MemoryContextSwitchTo(ApplyContext); - apply_worker_exit(); + request_message = makeStringInfo(); + MemoryContextSwitchTo(oldctx); } + else + resetStringInfo(request_message); - /* !slotname should never happen when enabled is true. */ - Assert(newsub->slotname); + /* + * Send the current time to update the remote walsender's latest reply + * message received time. + */ + pq_sendbyte(request_message, PqReplMsg_PrimaryStatusRequest); + pq_sendint64(request_message, GetCurrentTimestamp()); - /* two-phase cannot be altered while the worker is running */ - Assert(newsub->twophasestate == MySubscription->twophasestate); + elog(DEBUG2, "sending publisher status request message"); + + /* Send a request for the publisher status */ + walrcv_send(LogRepWorkerWalRcvConn, + request_message->data, request_message->len); + + rdt_data->phase = RDT_WAIT_FOR_PUBLISHER_STATUS; /* - * Exit if any parameter that affects the remote connection was changed. - * The launcher will start a new worker but note that the parallel apply - * worker won't restart if the streaming option's value is changed from + * Skip calling maybe_advance_nonremovable_xid() since further transition + * is possible only once we receive the publisher status message. + */ +} + +/* + * Workhorse for the RDT_WAIT_FOR_PUBLISHER_STATUS phase. + */ +static void +wait_for_publisher_status(RetainDeadTuplesData *rdt_data, + bool status_received) +{ + /* + * Return if we have requested but not yet received the publisher status. + */ + if (!status_received) + return; + + /* + * We don't need to maintain oldest_nonremovable_xid if we decide to stop + * retaining conflict information for this worker. + */ + if (should_stop_conflict_info_retention(rdt_data)) + { + rdt_data->phase = RDT_STOP_CONFLICT_INFO_RETENTION; + return; + } + + if (!FullTransactionIdIsValid(rdt_data->remote_wait_for)) + rdt_data->remote_wait_for = rdt_data->remote_nextxid; + + /* + * Check if all remote concurrent transactions that were active at the + * first status request have now completed. If completed, proceed to the + * next phase; otherwise, continue checking the publisher status until + * these transactions finish. + * + * It's possible that transactions in the commit phase during the last + * cycle have now finished committing, but remote_oldestxid remains older + * than remote_wait_for. This can happen if some old transaction came in + * the commit phase when we requested status in this cycle. We do not + * handle this case explicitly as it's rare and the benefit doesn't + * justify the required complexity. Tracking would require either caching + * all xids at the publisher or sending them to subscribers. The condition + * will resolve naturally once the remaining transactions are finished. + * + * Directly advancing the non-removable transaction ID is possible if + * there are no activities on the publisher since the last advancement + * cycle. However, it requires maintaining two fields, last_remote_nextxid + * and last_remote_lsn, within the structure for comparison with the + * current cycle's values. Considering the minimal cost of continuing in + * RDT_WAIT_FOR_LOCAL_FLUSH without awaiting changes, we opted not to + * advance the transaction ID here. + */ + if (FullTransactionIdPrecedesOrEquals(rdt_data->remote_wait_for, + rdt_data->remote_oldestxid)) + rdt_data->phase = RDT_WAIT_FOR_LOCAL_FLUSH; + else + rdt_data->phase = RDT_REQUEST_PUBLISHER_STATUS; + + /* process the next phase */ + process_rdt_phase_transition(rdt_data, false); +} + +/* + * Workhorse for the RDT_WAIT_FOR_LOCAL_FLUSH phase. + */ +static void +wait_for_local_flush(RetainDeadTuplesData *rdt_data) +{ + Assert(XLogRecPtrIsValid(rdt_data->remote_lsn) && + TransactionIdIsValid(rdt_data->candidate_xid)); + + /* + * We expect the publisher and subscriber clocks to be in sync using time + * sync service like NTP. Otherwise, we will advance this worker's + * oldest_nonremovable_xid prematurely, leading to the removal of rows + * required to detect update_deleted reliably. This check primarily + * addresses scenarios where the publisher's clock falls behind; if the + * publisher's clock is ahead, subsequent transactions will naturally bear + * later commit timestamps, conforming to the design outlined atop + * worker.c. + * + * XXX Consider waiting for the publisher's clock to catch up with the + * subscriber's before proceeding to the next phase. + */ + if (TimestampDifferenceExceeds(rdt_data->reply_time, + rdt_data->candidate_xid_time, 0)) + ereport(ERROR, + errmsg_internal("oldest_nonremovable_xid transaction ID could be advanced prematurely"), + errdetail_internal("The clock on the publisher is behind that of the subscriber.")); + + /* + * Do not attempt to advance the non-removable transaction ID when table + * sync is in progress. During this time, changes from a single + * transaction may be applied by multiple table sync workers corresponding + * to the target tables. So, it's necessary for all table sync workers to + * apply and flush the corresponding changes before advancing the + * transaction ID, otherwise, dead tuples that are still needed for + * conflict detection in table sync workers could be removed prematurely. + * However, confirming the apply and flush progress across all table sync + * workers is complex and not worth the effort, so we simply return if not + * all tables are in the READY state. + * + * Advancing the transaction ID is necessary even when no tables are + * currently subscribed, to avoid retaining dead tuples unnecessarily. + * While it might seem safe to skip all phases and directly assign + * candidate_xid to oldest_nonremovable_xid during the + * RDT_GET_CANDIDATE_XID phase in such cases, this is unsafe. If users + * concurrently add tables to the subscription, the apply worker may not + * process invalidations in time. Consequently, + * HasSubscriptionTablesCached() might miss the new tables, leading to + * premature advancement of oldest_nonremovable_xid. + * + * Performing the check during RDT_WAIT_FOR_LOCAL_FLUSH is safe, as + * invalidations are guaranteed to be processed before applying changes + * from newly added tables while waiting for the local flush to reach + * remote_lsn. + * + * Additionally, even if we check for subscription tables during + * RDT_GET_CANDIDATE_XID, they might be dropped before reaching + * RDT_WAIT_FOR_LOCAL_FLUSH. Therefore, it's still necessary to verify + * subscription tables at this stage to prevent unnecessary tuple + * retention. + */ + if (HasSubscriptionTablesCached() && !AllTablesyncsReady()) + { + TimestampTz now; + + now = rdt_data->last_recv_time + ? rdt_data->last_recv_time : GetCurrentTimestamp(); + + /* + * Record the time spent waiting for table sync, it is needed for the + * timeout check in should_stop_conflict_info_retention(). + */ + rdt_data->table_sync_wait_time = + TimestampDifferenceMilliseconds(rdt_data->candidate_xid_time, now); + + return; + } + + /* + * We don't need to maintain oldest_nonremovable_xid if we decide to stop + * retaining conflict information for this worker. + */ + if (should_stop_conflict_info_retention(rdt_data)) + { + rdt_data->phase = RDT_STOP_CONFLICT_INFO_RETENTION; + return; + } + + /* + * Update and check the remote flush position if we are applying changes + * in a loop. This is done at most once per WalWriterDelay to avoid + * performing costly operations in get_flush_position() too frequently + * during change application. + */ + if (last_flushpos < rdt_data->remote_lsn && rdt_data->last_recv_time && + TimestampDifferenceExceeds(rdt_data->flushpos_update_time, + rdt_data->last_recv_time, WalWriterDelay)) + { + XLogRecPtr writepos; + XLogRecPtr flushpos; + bool have_pending_txes; + + /* Fetch the latest remote flush position */ + get_flush_position(&writepos, &flushpos, &have_pending_txes); + + if (flushpos > last_flushpos) + last_flushpos = flushpos; + + rdt_data->flushpos_update_time = rdt_data->last_recv_time; + } + + /* Return to wait for the changes to be applied */ + if (last_flushpos < rdt_data->remote_lsn) + return; + + /* + * Reaching this point implies should_stop_conflict_info_retention() + * returned false earlier, meaning that the most recent duration for + * advancing the non-removable transaction ID is within the + * max_retention_duration or max_retention_duration is set to 0. + * + * Therefore, if conflict info retention was previously stopped due to a + * timeout, it is now safe to resume retention. + */ + if (!MySubscription->retentionactive) + { + rdt_data->phase = RDT_RESUME_CONFLICT_INFO_RETENTION; + return; + } + + /* + * Reaching here means the remote WAL position has been received, and all + * transactions up to that position on the publisher have been applied and + * flushed locally. So, we can advance the non-removable transaction ID. + */ + SpinLockAcquire(&MyLogicalRepWorker->relmutex); + MyLogicalRepWorker->oldest_nonremovable_xid = rdt_data->candidate_xid; + SpinLockRelease(&MyLogicalRepWorker->relmutex); + + elog(DEBUG2, "confirmed flush up to remote lsn %X/%08X: new oldest_nonremovable_xid %u", + LSN_FORMAT_ARGS(rdt_data->remote_lsn), + rdt_data->candidate_xid); + + /* Notify launcher to update the xmin of the conflict slot */ + ApplyLauncherWakeup(); + + reset_retention_data_fields(rdt_data); + + /* process the next phase */ + process_rdt_phase_transition(rdt_data, false); +} + +/* + * Check whether conflict information retention should be stopped due to + * exceeding the maximum wait time (max_retention_duration). + * + * If retention should be stopped, return true. Otherwise, return false. + */ +static bool +should_stop_conflict_info_retention(RetainDeadTuplesData *rdt_data) +{ + TimestampTz now; + + Assert(TransactionIdIsValid(rdt_data->candidate_xid)); + Assert(rdt_data->phase == RDT_WAIT_FOR_PUBLISHER_STATUS || + rdt_data->phase == RDT_WAIT_FOR_LOCAL_FLUSH); + + if (!MySubscription->maxretention) + return false; + + /* + * Use last_recv_time when applying changes in the loop to avoid + * unnecessary system time retrieval. If last_recv_time is not available, + * obtain the current timestamp. + */ + now = rdt_data->last_recv_time ? rdt_data->last_recv_time : GetCurrentTimestamp(); + + /* + * Return early if the wait time has not exceeded the configured maximum + * (max_retention_duration). Time spent waiting for table synchronization + * is excluded from this calculation, as it occurs infrequently. + */ + if (!TimestampDifferenceExceeds(rdt_data->candidate_xid_time, now, + MySubscription->maxretention + + rdt_data->table_sync_wait_time)) + return false; + + return true; +} + +/* + * Workhorse for the RDT_STOP_CONFLICT_INFO_RETENTION phase. + */ +static void +stop_conflict_info_retention(RetainDeadTuplesData *rdt_data) +{ + /* Stop retention if not yet */ + if (MySubscription->retentionactive) + { + /* + * If the retention status cannot be updated (e.g., due to active + * transaction), skip further processing to avoid inconsistent + * retention behavior. + */ + if (!update_retention_status(false)) + return; + + SpinLockAcquire(&MyLogicalRepWorker->relmutex); + MyLogicalRepWorker->oldest_nonremovable_xid = InvalidTransactionId; + SpinLockRelease(&MyLogicalRepWorker->relmutex); + + ereport(LOG, + errmsg("logical replication worker for subscription \"%s\" has stopped retaining the information for detecting conflicts", + MySubscription->name), + errdetail("Retention is stopped because the apply process has not caught up with the publisher within the configured max_retention_duration.")); + } + + Assert(!TransactionIdIsValid(MyLogicalRepWorker->oldest_nonremovable_xid)); + + /* + * If retention has been stopped, reset to the initial phase to retry + * resuming retention. This reset is required to recalculate the current + * wait time and resume retention if the time falls within + * max_retention_duration. + */ + reset_retention_data_fields(rdt_data); +} + +/* + * Workhorse for the RDT_RESUME_CONFLICT_INFO_RETENTION phase. + */ +static void +resume_conflict_info_retention(RetainDeadTuplesData *rdt_data) +{ + /* We can't resume retention without updating retention status. */ + if (!update_retention_status(true)) + return; + + ereport(LOG, + errmsg("logical replication worker for subscription \"%s\" will resume retaining the information for detecting conflicts", + MySubscription->name), + MySubscription->maxretention + ? errdetail("Retention is re-enabled because the apply process has caught up with the publisher within the configured max_retention_duration.") + : errdetail("Retention is re-enabled because max_retention_duration has been set to unlimited.")); + + /* + * Restart the worker to let the launcher initialize + * oldest_nonremovable_xid at startup. + * + * While it's technically possible to derive this value on-the-fly using + * the conflict detection slot's xmin, doing so risks a race condition: + * the launcher might clean slot.xmin just after retention resumes. This + * would make oldest_nonremovable_xid unreliable, especially during xid + * wraparound. + * + * Although this can be prevented by introducing heavy weight locking, the + * complexity it will bring doesn't seem worthwhile given how rarely + * retention is resumed. + */ + apply_worker_exit(); +} + +/* + * Updates pg_subscription.subretentionactive to the given value within a + * new transaction. + * + * If already inside an active transaction, skips the update and returns + * false. + * + * Returns true if the update is successfully performed. + */ +static bool +update_retention_status(bool active) +{ + /* + * Do not update the catalog during an active transaction. The transaction + * may be started during change application, leading to a possible + * rollback of catalog updates if the application fails subsequently. + */ + if (IsTransactionState()) + return false; + + StartTransactionCommand(); + + /* + * Updating pg_subscription might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* Update pg_subscription.subretentionactive */ + UpdateDeadTupleRetentionStatus(MySubscription->oid, active); + + PopActiveSnapshot(); + CommitTransactionCommand(); + + /* Notify launcher to update the conflict slot */ + ApplyLauncherWakeup(); + + MySubscription->retentionactive = active; + + return true; +} + +/* + * Reset all data fields of RetainDeadTuplesData except those used to + * determine the timing for the next round of transaction ID advancement. We + * can even use flushpos_update_time in the next round to decide whether to get + * the latest flush position. + */ +static void +reset_retention_data_fields(RetainDeadTuplesData *rdt_data) +{ + rdt_data->phase = RDT_GET_CANDIDATE_XID; + rdt_data->remote_lsn = InvalidXLogRecPtr; + rdt_data->remote_oldestxid = InvalidFullTransactionId; + rdt_data->remote_nextxid = InvalidFullTransactionId; + rdt_data->reply_time = 0; + rdt_data->remote_wait_for = InvalidFullTransactionId; + rdt_data->candidate_xid = InvalidTransactionId; + rdt_data->table_sync_wait_time = 0; +} + +/* + * Adjust the interval for advancing non-removable transaction IDs. + * + * If there is no activity on the node or retention has been stopped, we + * progressively double the interval used to advance non-removable transaction + * ID. This helps conserve CPU and network resources when there's little benefit + * to frequent updates. + * + * The interval is capped by the lowest of the following: + * - wal_receiver_status_interval (if set and retention is active), + * - a default maximum of 3 minutes, + * - max_retention_duration (if retention is active). + * + * This ensures the interval never exceeds the retention boundary, even if other + * limits are higher. Once activity resumes on the node and the retention is + * active, the interval is reset to lesser of 100ms and max_retention_duration, + * allowing timely advancement of non-removable transaction ID. + * + * XXX The use of wal_receiver_status_interval is a bit arbitrary so we can + * consider the other interval or a separate GUC if the need arises. + */ +static void +adjust_xid_advance_interval(RetainDeadTuplesData *rdt_data, bool new_xid_found) +{ + if (rdt_data->xid_advance_interval && !new_xid_found) + { + int max_interval = wal_receiver_status_interval + ? wal_receiver_status_interval * 1000 + : MAX_XID_ADVANCE_INTERVAL; + + /* + * No new transaction ID has been assigned since the last check, so + * double the interval, but not beyond the maximum allowable value. + */ + rdt_data->xid_advance_interval = Min(rdt_data->xid_advance_interval * 2, + max_interval); + } + else if (rdt_data->xid_advance_interval && + !MySubscription->retentionactive) + { + /* + * Retention has been stopped, so double the interval-capped at a + * maximum of 3 minutes. The wal_receiver_status_interval is + * intentionally not used as an upper bound, since the likelihood of + * retention resuming is lower than that of general activity resuming. + */ + rdt_data->xid_advance_interval = Min(rdt_data->xid_advance_interval * 2, + MAX_XID_ADVANCE_INTERVAL); + } + else + { + /* + * A new transaction ID was found or the interval is not yet + * initialized, so set the interval to the minimum value. + */ + rdt_data->xid_advance_interval = MIN_XID_ADVANCE_INTERVAL; + } + + /* + * Ensure the wait time remains within the maximum retention time limit + * when retention is active. + */ + if (MySubscription->retentionactive) + rdt_data->xid_advance_interval = Min(rdt_data->xid_advance_interval, + MySubscription->maxretention); +} + +/* + * Exit routine for apply workers due to subscription parameter changes. + */ +static void +apply_worker_exit(void) +{ + if (am_parallel_apply_worker()) + { + /* + * Don't stop the parallel apply worker as the leader will detect the + * subscription parameter change and restart logical replication later + * anyway. This also prevents the leader from reporting errors when + * trying to communicate with a stopped parallel apply worker, which + * would accidentally disable subscriptions if disable_on_error was + * set. + */ + return; + } + + /* + * Reset the last-start time for this apply worker so that the launcher + * will restart it without waiting for wal_retrieve_retry_interval if the + * subscription is still active, and so that we won't leak that hash table + * entry if it isn't. + */ + if (am_leader_apply_worker()) + ApplyLauncherForgetWorkerStartTime(MyLogicalRepWorker->subid); + + proc_exit(0); +} + +/* + * Reread subscription info if needed. + * + * For significant changes, we react by exiting the current process; a new + * one will be launched afterwards if needed. + */ +void +maybe_reread_subscription(void) +{ + Subscription *newsub; + bool started_tx = false; + + /* When cache state is valid there is nothing to do here. */ + if (MySubscriptionValid) + return; + + /* This function might be called inside or outside of transaction. */ + if (!IsTransactionState()) + { + StartTransactionCommand(); + started_tx = true; + } + + newsub = GetSubscription(MyLogicalRepWorker->subid, true, true); + + if (newsub) + { + MemoryContextSetParent(newsub->cxt, ApplyContext); + } + else + { + /* + * Exit if the subscription was removed. This normally should not + * happen as the worker gets killed during DROP SUBSCRIPTION. + */ + ereport(LOG, + (errmsg("logical replication worker for subscription \"%s\" will stop because the subscription was removed", + MySubscription->name))); + + /* Ensure we remove no-longer-useful entry for worker's start time */ + if (am_leader_apply_worker()) + ApplyLauncherForgetWorkerStartTime(MyLogicalRepWorker->subid); + + proc_exit(0); + } + + /* Exit if the subscription was disabled. */ + if (!newsub->enabled) + { + ereport(LOG, + (errmsg("logical replication worker for subscription \"%s\" will stop because the subscription was disabled", + MySubscription->name))); + + apply_worker_exit(); + } + + /* !slotname should never happen when enabled is true. */ + Assert(newsub->slotname); + + /* two-phase cannot be altered while the worker is running */ + Assert(newsub->twophasestate == MySubscription->twophasestate); + + /* + * Exit if any parameter that affects the remote connection was changed. + * The launcher will start a new worker but note that the parallel apply + * worker won't restart if the streaming option's value is changed from * 'parallel' to any other value or the server decides not to stream the * in-progress transaction. */ @@ -4075,15 +5152,16 @@ maybe_reread_subscription(void) } /* Clean old subscription info and switch to new one. */ - FreeSubscription(MySubscription); + MemoryContextDelete(MySubscription->cxt); MySubscription = newsub; - MemoryContextSwitchTo(oldctx); - /* Change synchronous commit according to the user's wishes */ SetConfigOption("synchronous_commit", MySubscription->synccommit, PGC_BACKEND, PGC_S_OVERRIDE); + /* Change wal_receiver_timeout according to the user's wishes */ + set_wal_receiver_timeout(); + if (started_tx) CommitTransactionCommand(); @@ -4091,10 +5169,45 @@ maybe_reread_subscription(void) } /* - * Callback from subscription syscache invalidation. + * Change wal_receiver_timeout to MySubscription->walrcvtimeout. */ static void -subscription_change_cb(Datum arg, int cacheid, uint32 hashvalue) +set_wal_receiver_timeout(void) +{ + bool parsed; + int val; + int prev_timeout = wal_receiver_timeout; + + /* + * Set the wal_receiver_timeout GUC to MySubscription->walrcvtimeout, + * which comes from the subscription's wal_receiver_timeout option. If the + * value is -1, reset the GUC to its default, meaning it will inherit from + * the server config, command line, or role/database settings. + */ + parsed = parse_int(MySubscription->walrcvtimeout, &val, 0, NULL); + if (parsed && val == -1) + SetConfigOption("wal_receiver_timeout", NULL, + PGC_BACKEND, PGC_S_SESSION); + else + SetConfigOption("wal_receiver_timeout", MySubscription->walrcvtimeout, + PGC_BACKEND, PGC_S_SESSION); + + /* + * Log the wal_receiver_timeout setting (in milliseconds) as a debug + * message when it changes, to verify it was set correctly. + */ + if (prev_timeout != wal_receiver_timeout) + elog(DEBUG1, "logical replication worker for subscription \"%s\" wal_receiver_timeout: %d ms", + MySubscription->name, wal_receiver_timeout); +} + +/* + * Callback from subscription syscache invalidation. Also needed for server or + * user mapping invalidation, which can change the connection information for + * subscriptions that connect using a server object. + */ +static void +subscription_change_cb(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { MySubscriptionValid = false; } @@ -4185,7 +5298,7 @@ subxact_info_read(Oid subid, TransactionId xid) len = sizeof(SubXactInfo) * subxact_data.nsubxacts; /* we keep the maximum as a power of 2 */ - subxact_data.nsubxacts_max = 1 << my_log2(subxact_data.nsubxacts); + subxact_data.nsubxacts_max = 1 << pg_ceil_log2_32(subxact_data.nsubxacts); /* * Allocate subxact information in the logical streaming context. We need @@ -4194,8 +5307,8 @@ subxact_info_read(Oid subid, TransactionId xid) * to the subxact file and reset the logical streaming context. */ oldctx = MemoryContextSwitchTo(LogicalStreamingContext); - subxact_data.subxacts = palloc(subxact_data.nsubxacts_max * - sizeof(SubXactInfo)); + subxact_data.subxacts = palloc_array(SubXactInfo, + subxact_data.nsubxacts_max); MemoryContextSwitchTo(oldctx); if (len > 0) @@ -4261,14 +5374,14 @@ subxact_info_add(TransactionId xid) * subxact_info_read. */ oldctx = MemoryContextSwitchTo(LogicalStreamingContext); - subxacts = palloc(subxact_data.nsubxacts_max * sizeof(SubXactInfo)); + subxacts = palloc_array(SubXactInfo, subxact_data.nsubxacts_max); MemoryContextSwitchTo(oldctx); } else if (subxact_data.nsubxacts == subxact_data.nsubxacts_max) { subxact_data.nsubxacts_max *= 2; - subxacts = repalloc(subxacts, - subxact_data.nsubxacts_max * sizeof(SubXactInfo)); + subxacts = repalloc_array(subxacts, SubXactInfo, + subxact_data.nsubxacts_max); } subxacts[subxact_data.nsubxacts].xid = xid; @@ -4491,7 +5604,7 @@ set_stream_options(WalRcvStreamOptions *options, * Cleanup the memory for subxacts and reset the related variables. */ static inline void -cleanup_subxact_info() +cleanup_subxact_info(void) { if (subxact_data.subxacts) pfree(subxact_data.subxacts); @@ -4524,7 +5637,7 @@ start_apply(XLogRecPtr origin_startpos) * transaction loss as that transaction won't be sent again by the * server. */ - replorigin_reset(0, (Datum) 0); + replorigin_xact_clear(true); if (MySubscription->disableonerr) DisableSubscriptionAndExit(); @@ -4536,7 +5649,7 @@ start_apply(XLogRecPtr origin_startpos) * idle state. */ AbortOutOfAnyTransaction(); - pgstat_report_subscription_error(MySubscription->oid, !am_tablesync_worker()); + pgstat_report_subscription_error(MySubscription->oid); PG_RE_THROW(); } @@ -4550,13 +5663,13 @@ start_apply(XLogRecPtr origin_startpos) * It sets up replication origin, streaming options and then starts streaming. */ static void -run_apply_worker() +run_apply_worker(void) { char originname[NAMEDATALEN]; XLogRecPtr origin_startpos = InvalidXLogRecPtr; char *slotname = NULL; WalRcvStreamOptions options; - RepOriginId originid; + ReplOriginId originid; TimeLineID startpointTLI; char *err; bool must_use_password; @@ -4581,7 +5694,7 @@ run_apply_worker() if (!OidIsValid(originid)) originid = replorigin_create(originname); replorigin_session_setup(originid, 0); - replorigin_session_origin = originid; + replorigin_xact_state.origin = originid; origin_startpos = replorigin_session_get_progress(false); CommitTransactionCommand(); @@ -4626,8 +5739,16 @@ run_apply_worker() walrcv_startstreaming(LogRepWorkerWalRcvConn, &options); StartTransactionCommand(); + + /* + * Updating pg_subscription might involve TOAST table access, so + * ensure we have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + UpdateTwoPhaseState(MySubscription->oid, LOGICALREP_TWOPHASE_STATE_ENABLED); MySubscription->twophasestate = LOGICALREP_TWOPHASE_STATE_ENABLED; + PopActiveSnapshot(); CommitTransactionCommand(); } else @@ -4648,8 +5769,8 @@ run_apply_worker() } /* - * Common initialization for leader apply worker, parallel apply worker and - * tablesync worker. + * Common initialization for leader apply worker, parallel apply worker, + * tablesync worker and sequencesync worker. * * Initialize the database connection, in-memory subscription and necessary * config options. @@ -4657,8 +5778,6 @@ run_apply_worker() void InitializeLogRepWorker(void) { - MemoryContext oldctx; - /* Run as replica session replication role. */ SetConfigOption("session_replication_role", "replica", PGC_SUSET, PGC_S_OVERRIDE); @@ -4674,15 +5793,27 @@ InitializeLogRepWorker(void) */ SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE); - /* Load the subscription into persistent memory context. */ ApplyContext = AllocSetContextCreate(TopMemoryContext, "ApplyContext", ALLOCSET_DEFAULT_SIZES); + StartTransactionCommand(); - oldctx = MemoryContextSwitchTo(ApplyContext); - MySubscription = GetSubscription(MyLogicalRepWorker->subid, true); - if (!MySubscription) + /* + * Lock the subscription to prevent it from being concurrently dropped, + * then re-verify its existence. After the initialization, the worker will + * be terminated gracefully if the subscription is dropped. + */ + LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, 0, + AccessShareLock); + + MySubscription = GetSubscription(MyLogicalRepWorker->subid, true, true); + + if (MySubscription) + { + MemoryContextSetParent(MySubscription->cxt, ApplyContext); + } + else { ereport(LOG, (errmsg("logical replication worker for subscription %u will not start because the subscription was removed during startup", @@ -4696,7 +5827,6 @@ InitializeLogRepWorker(void) } MySubscriptionValid = true; - MemoryContextSwitchTo(oldctx); if (!MySubscription->enabled) { @@ -4707,10 +5837,38 @@ InitializeLogRepWorker(void) apply_worker_exit(); } + /* + * Restart the worker if retain_dead_tuples was enabled during startup. + * + * At this point, the replication slot used for conflict detection might + * not exist yet, or could be dropped soon if the launcher perceives + * retain_dead_tuples as disabled. To avoid unnecessary tracking of + * oldest_nonremovable_xid when the slot is absent or at risk of being + * dropped, a restart is initiated. + * + * The oldest_nonremovable_xid should be initialized only when the + * subscription's retention is active before launching the worker. See + * logicalrep_worker_launch. + */ + if (am_leader_apply_worker() && + MySubscription->retaindeadtuples && + MySubscription->retentionactive && + !TransactionIdIsValid(MyLogicalRepWorker->oldest_nonremovable_xid)) + { + ereport(LOG, + errmsg("logical replication worker for subscription \"%s\" will restart because the option %s was enabled during startup", + MySubscription->name, "retain_dead_tuples")); + + apply_worker_exit(); + } + /* Setup synchronous commit according to the user's wishes */ SetConfigOption("synchronous_commit", MySubscription->synccommit, PGC_BACKEND, PGC_S_OVERRIDE); + /* Change wal_receiver_timeout according to the user's wishes */ + set_wal_receiver_timeout(); + /* * Keep us informed about subscription or role changes. Note that the * role's superuser privilege can be revoked. @@ -4718,6 +5876,22 @@ InitializeLogRepWorker(void) CacheRegisterSyscacheCallback(SUBSCRIPTIONOID, subscription_change_cb, (Datum) 0); + /* Changes to foreign servers may affect subscriptions using SERVER. */ + CacheRegisterSyscacheCallback(FOREIGNSERVEROID, + subscription_change_cb, + (Datum) 0); + /* Changes to user mappings may affect subscriptions using SERVER. */ + CacheRegisterSyscacheCallback(USERMAPPINGOID, + subscription_change_cb, + (Datum) 0); + + /* + * Changes to FDW connection_function may affect subscriptions using + * SERVER. + */ + CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, + subscription_change_cb, + (Datum) 0); CacheRegisterSyscacheCallback(AUTHOID, subscription_change_cb, @@ -4725,40 +5899,60 @@ InitializeLogRepWorker(void) if (am_tablesync_worker()) ereport(LOG, - (errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has started", - MySubscription->name, - get_rel_name(MyLogicalRepWorker->relid)))); + errmsg("logical replication table synchronization worker for subscription \"%s\", table \"%s\" has started", + MySubscription->name, + get_rel_name(MyLogicalRepWorker->relid))); + else if (am_sequencesync_worker()) + ereport(LOG, + errmsg("logical replication sequence synchronization worker for subscription \"%s\" has started", + MySubscription->name)); else ereport(LOG, - (errmsg("logical replication apply worker for subscription \"%s\" has started", - MySubscription->name))); + errmsg("logical replication apply worker for subscription \"%s\" has started", + MySubscription->name)); CommitTransactionCommand(); + + /* + * Register a callback to reset the origin state before aborting any + * pending transaction during shutdown (see ShutdownPostgres()). This will + * avoid origin advancement for an incomplete transaction which could + * otherwise lead to its loss as such a transaction won't be sent by the + * server again. + * + * Note that even a LOG or DEBUG statement placed after setting the origin + * state may process a shutdown signal before committing the current apply + * operation. So, it is important to register such a callback here. + * + * Register this callback here to ensure that all types of logical + * replication workers that set up origins and apply remote transactions + * are protected. + */ + before_shmem_exit(on_exit_clear_xact_state, (Datum) 0); } /* - * Reset the origin state. + * Callback on exit to clear transaction-level replication origin state. */ static void -replorigin_reset(int code, Datum arg) +on_exit_clear_xact_state(int code, Datum arg) { - replorigin_session_origin = InvalidRepOriginId; - replorigin_session_origin_lsn = InvalidXLogRecPtr; - replorigin_session_origin_timestamp = 0; + replorigin_xact_clear(true); } -/* Common function to setup the leader apply or tablesync worker. */ +/* + * Common function to setup the leader apply, tablesync and sequencesync worker. + */ void SetupApplyOrSyncWorker(int worker_slot) { /* Attach to slot */ logicalrep_worker_attach(worker_slot); - Assert(am_tablesync_worker() || am_leader_apply_worker()); + Assert(am_tablesync_worker() || am_sequencesync_worker() || am_leader_apply_worker()); /* Setup signal handling */ pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGTERM, die); BackgroundWorkerUnblockSignals(); /* @@ -4775,19 +5969,6 @@ SetupApplyOrSyncWorker(int worker_slot) InitializeLogRepWorker(); - /* - * Register a callback to reset the origin state before aborting any - * pending transaction during shutdown (see ShutdownPostgres()). This will - * avoid origin advancement for an in-complete transaction which could - * otherwise lead to its loss as such a transaction won't be sent by the - * server again. - * - * Note that even a LOG or DEBUG statement placed after setting the origin - * state may process a shutdown signal before committing the current apply - * operation. So, it is important to register such a callback here. - */ - before_shmem_exit(replorigin_reset, (Datum) 0); - /* Connect to the origin and start the replication. */ elog(DEBUG1, "connecting to publisher using connection string \"%s\"", MySubscription->conninfo); @@ -4797,7 +5978,7 @@ SetupApplyOrSyncWorker(int worker_slot) * the subscription relation state. */ CacheRegisterSyscacheCallback(SUBSCRIPTIONRELMAP, - invalidate_syncing_table_states, + InvalidateSyncingRelStates, (Datum) 0); } @@ -4837,13 +6018,23 @@ DisableSubscriptionAndExit(void) RESUME_INTERRUPTS(); - /* Report the worker failed during either table synchronization or apply */ - pgstat_report_subscription_error(MyLogicalRepWorker->subid, - !am_tablesync_worker()); + /* + * Report the worker failed during sequence synchronization, table + * synchronization, or apply. + */ + pgstat_report_subscription_error(MyLogicalRepWorker->subid); /* Disable the subscription */ StartTransactionCommand(); + + /* + * Updating pg_subscription might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + DisableSubscription(MySubscription->oid); + PopActiveSnapshot(); CommitTransactionCommand(); /* Ensure we remove no-longer-useful entry for worker's start time */ @@ -4855,6 +6046,15 @@ DisableSubscriptionAndExit(void) errmsg("subscription \"%s\" has been disabled because of an error", MySubscription->name)); + /* + * Skip the track_commit_timestamp check when disabling the worker due to + * an error, as verifying commit timestamps is unnecessary in this + * context. + */ + CheckSubDeadTupleRetention(false, true, WARNING, + MySubscription->retaindeadtuples, + MySubscription->retentionactive, false); + proc_exit(0); } @@ -4892,7 +6092,7 @@ maybe_start_skipping_changes(XLogRecPtr finish_lsn) * function is called for every remote transaction and we assume that * skipping the transaction is not used often. */ - if (likely(XLogRecPtrIsInvalid(MySubscription->skiplsn) || + if (likely(!XLogRecPtrIsValid(MySubscription->skiplsn) || MySubscription->skiplsn != finish_lsn)) return; @@ -4900,7 +6100,7 @@ maybe_start_skipping_changes(XLogRecPtr finish_lsn) skip_xact_finish_lsn = finish_lsn; ereport(LOG, - errmsg("logical replication starts skipping transaction at LSN %X/%X", + errmsg("logical replication starts skipping transaction at LSN %X/%08X", LSN_FORMAT_ARGS(skip_xact_finish_lsn))); } @@ -4914,8 +6114,8 @@ stop_skipping_changes(void) return; ereport(LOG, - (errmsg("logical replication completed skipping transaction at LSN %X/%X", - LSN_FORMAT_ARGS(skip_xact_finish_lsn)))); + errmsg("logical replication completed skipping transaction at LSN %X/%08X", + LSN_FORMAT_ARGS(skip_xact_finish_lsn))); /* Stop skipping changes */ skip_xact_finish_lsn = InvalidXLogRecPtr; @@ -4938,7 +6138,7 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) XLogRecPtr myskiplsn = MySubscription->skiplsn; bool started_tx = false; - if (likely(XLogRecPtrIsInvalid(myskiplsn)) || am_parallel_apply_worker()) + if (likely(!XLogRecPtrIsValid(myskiplsn)) || am_parallel_apply_worker()) return; if (!IsTransactionState()) @@ -4947,6 +6147,12 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) started_tx = true; } + /* + * Updating pg_subscription might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + /* * Protect subskiplsn of pg_subscription from being concurrently updated * while clearing it. @@ -4997,7 +6203,7 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) if (myskiplsn != finish_lsn) ereport(WARNING, errmsg("skip-LSN of subscription \"%s\" cleared", MySubscription->name), - errdetail("Remote transaction's finish WAL location (LSN) %X/%X did not match skip-LSN %X/%X.", + errdetail("Remote transaction's finish WAL location (LSN) %X/%08X did not match skip-LSN %X/%08X.", LSN_FORMAT_ARGS(finish_lsn), LSN_FORMAT_ARGS(myskiplsn))); } @@ -5005,6 +6211,8 @@ clear_subscription_skip_lsn(XLogRecPtr finish_lsn) heap_freetuple(tup); table_close(rel, NoLock); + PopActiveSnapshot(); + if (started_tx) CommitTransactionCommand(); } @@ -5026,13 +6234,13 @@ apply_error_callback(void *arg) errcontext("processing remote data for replication origin \"%s\" during message type \"%s\"", errarg->origin_name, logicalrep_message_type(errarg->command)); - else if (XLogRecPtrIsInvalid(errarg->finish_lsn)) + else if (!XLogRecPtrIsValid(errarg->finish_lsn)) errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" in transaction %u", errarg->origin_name, logicalrep_message_type(errarg->command), errarg->remote_xid); else - errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" in transaction %u, finished at %X/%X", + errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" in transaction %u, finished at %X/%08X", errarg->origin_name, logicalrep_message_type(errarg->command), errarg->remote_xid, @@ -5042,7 +6250,7 @@ apply_error_callback(void *arg) { if (errarg->remote_attnum < 0) { - if (XLogRecPtrIsInvalid(errarg->finish_lsn)) + if (!XLogRecPtrIsValid(errarg->finish_lsn)) errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" for replication target relation \"%s.%s\" in transaction %u", errarg->origin_name, logicalrep_message_type(errarg->command), @@ -5050,7 +6258,7 @@ apply_error_callback(void *arg) errarg->rel->remoterel.relname, errarg->remote_xid); else - errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" for replication target relation \"%s.%s\" in transaction %u, finished at %X/%X", + errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" for replication target relation \"%s.%s\" in transaction %u, finished at %X/%08X", errarg->origin_name, logicalrep_message_type(errarg->command), errarg->rel->remoterel.nspname, @@ -5060,7 +6268,7 @@ apply_error_callback(void *arg) } else { - if (XLogRecPtrIsInvalid(errarg->finish_lsn)) + if (!XLogRecPtrIsValid(errarg->finish_lsn)) errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" for replication target relation \"%s.%s\" column \"%s\" in transaction %u", errarg->origin_name, logicalrep_message_type(errarg->command), @@ -5069,7 +6277,7 @@ apply_error_callback(void *arg) errarg->rel->remoterel.attnames[errarg->remote_attnum], errarg->remote_xid); else - errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" for replication target relation \"%s.%s\" column \"%s\" in transaction %u, finished at %X/%X", + errcontext("processing remote data for replication origin \"%s\" during message type \"%s\" for replication target relation \"%s.%s\" column \"%s\" in transaction %u, finished at %X/%08X", errarg->origin_name, logicalrep_message_type(errarg->command), errarg->rel->remoterel.nspname, diff --git a/src/backend/replication/meson.build b/src/backend/replication/meson.build index b0601498865b3..ce9be4117ad3f 100644 --- a/src/backend/replication/meson.build +++ b/src/backend/replication/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'slot.c', diff --git a/src/backend/replication/pgoutput/meson.build b/src/backend/replication/pgoutput/meson.build index 14e2f03ada018..eb9c918eaafda 100644 --- a/src/backend/replication/pgoutput/meson.build +++ b/src/backend/replication/pgoutput/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgoutput_sources = files( 'pgoutput.c', diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c index 693a766e6d75f..4ecfcbff7abef 100644 --- a/src/backend/replication/pgoutput/pgoutput.c +++ b/src/backend/replication/pgoutput/pgoutput.c @@ -3,7 +3,7 @@ * pgoutput.c * Logical Replication output plugin * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/pgoutput/pgoutput.c @@ -59,7 +59,7 @@ static void pgoutput_message(LogicalDecodingContext *ctx, bool transactional, const char *prefix, Size sz, const char *message); static bool pgoutput_origin_filter(LogicalDecodingContext *ctx, - RepOriginId origin_id); + ReplOriginId origin_id); static void pgoutput_begin_prepare_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn); static void pgoutput_prepare_txn(LogicalDecodingContext *ctx, @@ -86,10 +86,10 @@ static void pgoutput_stream_prepare_txn(LogicalDecodingContext *ctx, static bool publications_valid; static List *LoadPublications(List *pubnames); -static void publication_invalidation_cb(Datum arg, int cacheid, +static void publication_invalidation_cb(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue); static void send_repl_origin(LogicalDecodingContext *ctx, - RepOriginId origin_id, XLogRecPtr origin_lsn, + ReplOriginId origin_id, XLogRecPtr origin_lsn, bool send_origin); /* @@ -227,7 +227,7 @@ static void send_relation_and_attrs(Relation relation, TransactionId xid, LogicalDecodingContext *ctx, RelationSyncEntry *relentry); static void rel_sync_cache_relation_cb(Datum arg, Oid relid); -static void rel_sync_cache_publication_cb(Datum arg, int cacheid, +static void rel_sync_cache_publication_cb(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue); static void set_schema_sent_in_streamed_txn(RelationSyncEntry *entry, TransactionId xid); @@ -235,6 +235,7 @@ static bool get_schema_sent_in_streamed_txn(RelationSyncEntry *entry, TransactionId xid); static void init_tuple_slot(PGOutputData *data, Relation relation, RelationSyncEntry *entry); +static void pgoutput_memory_context_reset(void *arg); /* row filter routines */ static EState *create_estate_for_relation(Relation rel); @@ -297,10 +298,12 @@ parse_output_parameters(List *options, PGOutputData *data) bool two_phase_option_given = false; bool origin_option_given = false; + /* Initialize optional parameters to defaults */ data->binary = false; data->streaming = LOGICALREP_STREAM_OFF; data->messages = false; data->two_phase = false; + data->publish_no_origin = false; foreach(lc, options) { @@ -343,7 +346,11 @@ parse_output_parameters(List *options, PGOutputData *data) errmsg("conflicting or redundant options"))); publication_names_given = true; - if (!SplitIdentifierString(strVal(defel->arg), ',', + /* + * Pass a copy of the DefElem->arg since SplitIdentifierString + * modifies its input. + */ + if (!SplitIdentifierString(pstrdup(strVal(defel->arg)), ',', &data->publication_names)) ereport(ERROR, (errcode(ERRCODE_INVALID_NAME), @@ -424,6 +431,19 @@ parse_output_parameters(List *options, PGOutputData *data) errmsg("option \"%s\" missing", "publication_names")); } +/* + * Memory context reset callback of PGOutputData->context. + */ +static void +pgoutput_memory_context_reset(void *arg) +{ + if (RelationSyncCache) + { + hash_destroy(RelationSyncCache); + RelationSyncCache = NULL; + } +} + /* * Initialize this plugin */ @@ -431,8 +451,9 @@ static void pgoutput_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, bool is_init) { - PGOutputData *data = palloc0(sizeof(PGOutputData)); + PGOutputData *data = palloc0_object(PGOutputData); static bool publication_callback_registered = false; + MemoryContextCallback *mcallback; /* Create our memory context for private allocations. */ data->context = AllocSetContextCreate(ctx->context, @@ -447,6 +468,14 @@ pgoutput_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, "logical replication publication list context", ALLOCSET_SMALL_SIZES); + /* + * Ensure to cleanup RelationSyncCache even when logical decoding invoked + * via SQL interface ends up with an error. + */ + mcallback = palloc0_object(MemoryContextCallback); + mcallback->func = pgoutput_memory_context_reset; + MemoryContextRegisterResetCallback(ctx->context, mcallback); + ctx->output_plugin_private = data; /* This plugin uses binary protocol. */ @@ -580,7 +609,7 @@ pgoutput_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn) static void pgoutput_send_begin(LogicalDecodingContext *ctx, ReorderBufferTXN *txn) { - bool send_replication_origin = txn->origin_id != InvalidRepOriginId; + bool send_replication_origin = txn->origin_id != InvalidReplOriginId; PGOutputTxnData *txndata = (PGOutputTxnData *) txn->output_plugin_private; Assert(txndata); @@ -634,7 +663,7 @@ pgoutput_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, static void pgoutput_begin_prepare_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn) { - bool send_replication_origin = txn->origin_id != InvalidRepOriginId; + bool send_replication_origin = txn->origin_id != InvalidReplOriginId; OutputPluginPrepareWrite(ctx, !send_replication_origin); logicalrep_write_begin_prepare(ctx->out, txn); @@ -1044,7 +1073,7 @@ check_and_init_gencol(PGOutputData *data, List *publications, /* Check if there is any generated column present. */ for (int i = 0; i < desc->natts; i++) { - Form_pg_attribute att = TupleDescAttr(desc, i); + CompactAttribute *att = TupleDescCompactAttr(desc, i); if (att->attgenerated) { @@ -1112,9 +1141,9 @@ pgoutput_column_list_init(PGOutputData *data, List *publications, * * Note that we don't support the case where the column list is different * for the same table when combining publications. See comments atop - * fetch_table_list. But one can later change the publication so we still - * need to check all the given publication-table mappings and report an - * error if any publications have a different column list. + * fetch_relation_list. But one can later change the publication so we + * still need to check all the given publication-table mappings and report + * an error if any publications have a different column list. */ foreach(lc, publications) { @@ -1372,8 +1401,8 @@ pgoutput_row_filter(Relation relation, TupleTableSlot *old_slot, * VARTAG_INDIRECT. See ReorderBufferToastReplace. */ if (att->attlen == -1 && - VARATT_IS_EXTERNAL_ONDISK(new_slot->tts_values[i]) && - !VARATT_IS_EXTERNAL_ONDISK(old_slot->tts_values[i])) + VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(new_slot->tts_values[i])) && + !VARATT_IS_EXTERNAL_ONDISK(DatumGetPointer(old_slot->tts_values[i]))) { if (!tmp_new_slot) { @@ -1530,7 +1559,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (relentry->attrmap) { TupleTableSlot *slot = MakeTupleTableSlot(RelationGetDescr(targetrel), - &TTSOpsVirtual); + &TTSOpsVirtual, 0); old_slot = execute_attr_map_slot(relentry->attrmap, old_slot, slot); } @@ -1545,7 +1574,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, if (relentry->attrmap) { TupleTableSlot *slot = MakeTupleTableSlot(RelationGetDescr(targetrel), - &TTSOpsVirtual); + &TTSOpsVirtual, 0); new_slot = execute_attr_map_slot(relentry->attrmap, new_slot, slot); } @@ -1738,11 +1767,11 @@ pgoutput_message(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, */ static bool pgoutput_origin_filter(LogicalDecodingContext *ctx, - RepOriginId origin_id) + ReplOriginId origin_id) { PGOutputData *data = (PGOutputData *) ctx->output_plugin_private; - if (data->publish_no_origin && origin_id != InvalidRepOriginId) + if (data->publish_no_origin && origin_id != InvalidReplOriginId) return true; return false; @@ -1758,11 +1787,7 @@ pgoutput_origin_filter(LogicalDecodingContext *ctx, static void pgoutput_shutdown(LogicalDecodingContext *ctx) { - if (RelationSyncCache) - { - hash_destroy(RelationSyncCache); - RelationSyncCache = NULL; - } + pgoutput_memory_context_reset(NULL); } /* @@ -1789,7 +1814,7 @@ LoadPublications(List *pubnames) else ereport(WARNING, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("skipped loading publication: %s", pubname), + errmsg("skipped loading publication \"%s\"", pubname), errdetail("The publication does not exist at this point in the WAL."), errhint("Create the publication if it does not exist.")); } @@ -1803,7 +1828,8 @@ LoadPublications(List *pubnames) * Called for invalidations on pg_publication. */ static void -publication_invalidation_cb(Datum arg, int cacheid, uint32 hashvalue) +publication_invalidation_cb(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { publications_valid = false; } @@ -1816,7 +1842,7 @@ pgoutput_stream_start(struct LogicalDecodingContext *ctx, ReorderBufferTXN *txn) { PGOutputData *data = (PGOutputData *) ctx->output_plugin_private; - bool send_replication_origin = txn->origin_id != InvalidRepOriginId; + bool send_replication_origin = txn->origin_id != InvalidReplOriginId; /* we can't nest streaming of transactions */ Assert(!data->in_streaming); @@ -1886,7 +1912,7 @@ pgoutput_stream_abort(struct LogicalDecodingContext *ctx, OutputPluginPrepareWrite(ctx, true); logicalrep_write_stream_abort(ctx->out, toptxn->xid, txn->xid, abort_lsn, - txn->xact_time.abort_time, write_abort_info); + txn->abort_time, write_abort_info); OutputPluginWrite(ctx, true); @@ -2063,7 +2089,7 @@ get_rel_sync_entry(PGOutputData *data, Relation relation) if (!entry->replicate_valid) { Oid schemaId = get_rel_namespace(relid); - List *pubids = GetRelationPublications(relid); + List *pubids = GetRelationIncludedPublications(relid); /* * We don't acquire a lock on the namespace system table as we build @@ -2180,14 +2206,47 @@ get_rel_sync_entry(PGOutputData *data, Relation relation) */ if (pub->alltables) { - publish = true; - if (pub->pubviaroot && am_partition) + List *exceptpubids = NIL; + + if (am_partition) { List *ancestors = get_partition_ancestors(relid); + Oid last_ancestor_relid = llast_oid(ancestors); + + /* + * For a partition, changes are published via top-most + * ancestor when pubviaroot is true, so populate pub_relid + * accordingly. + */ + if (pub->pubviaroot) + { + pub_relid = last_ancestor_relid; + ancestor_level = list_length(ancestors); + } - pub_relid = llast_oid(ancestors); - ancestor_level = list_length(ancestors); + /* + * Only the top-most ancestor can appear in the EXCEPT + * clause. Therefore, for a partition, exclusion must be + * evaluated at the top-most ancestor. + */ + exceptpubids = GetRelationExcludedPublications(last_ancestor_relid); } + else + { + /* + * For a regular table or a root partitioned table, check + * exclusion on table itself. + */ + exceptpubids = GetRelationExcludedPublications(pub_relid); + } + + if (!list_member_oid(exceptpubids, pub->oid)) + publish = true; + + list_free(exceptpubids); + + if (!publish) + continue; } if (!publish) @@ -2406,7 +2465,8 @@ rel_sync_cache_relation_cb(Datum arg, Oid relid) * Called for invalidations on pg_namespace. */ static void -rel_sync_cache_publication_cb(Datum arg, int cacheid, uint32 hashvalue) +rel_sync_cache_publication_cb(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { HASH_SEQ_STATUS status; RelationSyncEntry *entry; @@ -2432,7 +2492,7 @@ rel_sync_cache_publication_cb(Datum arg, int cacheid, uint32 hashvalue) /* Send Replication origin */ static void -send_repl_origin(LogicalDecodingContext *ctx, RepOriginId origin_id, +send_repl_origin(LogicalDecodingContext *ctx, ReplOriginId origin_id, XLogRecPtr origin_lsn, bool send_origin) { if (send_origin) diff --git a/src/backend/replication/pgrepack/Makefile b/src/backend/replication/pgrepack/Makefile new file mode 100644 index 0000000000000..d3d31407a4978 --- /dev/null +++ b/src/backend/replication/pgrepack/Makefile @@ -0,0 +1,32 @@ +#------------------------------------------------------------------------- +# +# Makefile-- +# Makefile for src/backend/replication/pgrepack +# +# IDENTIFICATION +# src/backend/replication/pgrepack +# +#------------------------------------------------------------------------- + +subdir = src/backend/replication/pgrepack +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +OBJS = \ + $(WIN32RES) \ + pgrepack.o +PGFILEDESC = "pgrepack - logical replication output plugin for REPACK" +NAME = pgrepack + +all: all-shared-lib + +include $(top_srcdir)/src/Makefile.shlib + +install: all installdirs install-lib + +installdirs: installdirs-lib + +uninstall: uninstall-lib + +clean distclean: clean-lib + rm -f $(OBJS) diff --git a/src/backend/replication/pgrepack/meson.build b/src/backend/replication/pgrepack/meson.build new file mode 100644 index 0000000000000..2c7de963ffefa --- /dev/null +++ b/src/backend/replication/pgrepack/meson.build @@ -0,0 +1,18 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +pgrepack_sources = files( + 'pgrepack.c', +) + +if host_system == 'windows' + pgrepack_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'pgrepack', + '--FILEDESC', 'pgrepack - logical replication output plugin for REPACK',]) +endif + +pgrepack = shared_module('pgrepack', + pgrepack_sources, + kwargs: pg_mod_args, +) + +backend_targets += pgrepack diff --git a/src/backend/replication/pgrepack/pgrepack.c b/src/backend/replication/pgrepack/pgrepack.c new file mode 100644 index 0000000000000..eb9a883d7a9f3 --- /dev/null +++ b/src/backend/replication/pgrepack/pgrepack.c @@ -0,0 +1,294 @@ +/*------------------------------------------------------------------------- + * + * pgrepack.c + * Logical Replication output plugin for REPACK command + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/replication/pgrepack/pgrepack.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/detoast.h" +#include "commands/repack_internal.h" +#include "replication/snapbuild.h" +#include "utils/memutils.h" + +PG_MODULE_MAGIC; + +static void repack_startup(LogicalDecodingContext *ctx, + OutputPluginOptions *opt, bool is_init); +static void repack_shutdown(LogicalDecodingContext *ctx); +static void repack_begin_txn(LogicalDecodingContext *ctx, + ReorderBufferTXN *txn); +static void repack_commit_txn(LogicalDecodingContext *ctx, + ReorderBufferTXN *txn, XLogRecPtr commit_lsn); +static void repack_process_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + Relation relation, ReorderBufferChange *change); +static void repack_store_change(LogicalDecodingContext *ctx, Relation relation, + ConcurrentChangeKind kind, HeapTuple tuple); + +void +_PG_output_plugin_init(OutputPluginCallbacks *cb) +{ + cb->startup_cb = repack_startup; + cb->begin_cb = repack_begin_txn; + cb->change_cb = repack_process_change; + cb->commit_cb = repack_commit_txn; + cb->shutdown_cb = repack_shutdown; +} + + +/* initialize this plugin */ +static void +repack_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt, + bool is_init) +{ + ctx->output_plugin_private = NULL; + + /* Probably unnecessary, as we don't use the SQL interface ... */ + opt->output_type = OUTPUT_PLUGIN_BINARY_OUTPUT; + + /* + * REPACK doesn't need access to shared catalogs, so we can speed up the + * historic snapshot creation by setting this flag. We'll only have to + * wait for transactions in our database. + */ + opt->need_shared_catalogs = false; + + if (ctx->output_plugin_options != NIL) + { + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("this plugin does not expect any options")); + } +} + +static void +repack_shutdown(LogicalDecodingContext *ctx) +{ +} + +/* + * As we don't release the slot during processing of particular table, there's + * no room for SQL interface, even for debugging purposes. Therefore we need + * neither OutputPluginPrepareWrite() nor OutputPluginWrite() in the plugin + * callbacks. (Although we might want to write custom callbacks, this API + * seems to be unnecessarily generic for our purposes.) + */ + +/* BEGIN callback */ +static void +repack_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn) +{ +} + +/* COMMIT callback */ +static void +repack_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + XLogRecPtr commit_lsn) +{ +} + +/* + * Callback for individual changed tuples + */ +static void +repack_process_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn, + Relation relation, ReorderBufferChange *change) +{ + RepackDecodingState *private PG_USED_FOR_ASSERTS_ONLY = + (RepackDecodingState *) ctx->output_writer_private; + + /* Changes of other relation should not have been decoded. */ + Assert(RelationGetRelid(relation) == private->relid); + + /* Decode entry depending on its type */ + switch (change->action) + { + case REORDER_BUFFER_CHANGE_INSERT: + { + HeapTuple newtuple; + + newtuple = change->data.tp.newtuple; + + /* + * Identity checks in the main function should have made this + * impossible. + */ + if (newtuple == NULL) + elog(ERROR, "incomplete insert info"); + + repack_store_change(ctx, relation, CHANGE_INSERT, newtuple); + } + break; + case REORDER_BUFFER_CHANGE_UPDATE: + { + HeapTuple oldtuple, + newtuple; + + oldtuple = change->data.tp.oldtuple; + newtuple = change->data.tp.newtuple; + + if (newtuple == NULL) + elog(ERROR, "incomplete update info"); + + if (oldtuple != NULL) + repack_store_change(ctx, relation, CHANGE_UPDATE_OLD, oldtuple); + + repack_store_change(ctx, relation, CHANGE_UPDATE_NEW, newtuple); + } + break; + case REORDER_BUFFER_CHANGE_DELETE: + { + HeapTuple oldtuple; + + oldtuple = change->data.tp.oldtuple; + + if (oldtuple == NULL) + elog(ERROR, "incomplete delete info"); + + repack_store_change(ctx, relation, CHANGE_DELETE, oldtuple); + } + break; + default: + + /* + * Should not come here. This includes TRUNCATE of the table being + * processed. heap_decode() cannot check the file locator easily, + * but we assume that TRUNCATE uses AccessExclusiveLock on the + * table so it should not occur during REPACK (CONCURRENTLY). + */ + Assert(false); + break; + } +} + +/* + * Write the given tuple, with the given change kind, to the repack spill + * file. Later, the repack decoding worker can read these and replay + * the operations on the new copy of the table. + * + * For each change affecting the table being repacked, we store enough + * information about each tuple in it, so that it can be replayed in the + * new copy of the table. + */ +static void +repack_store_change(LogicalDecodingContext *ctx, Relation relation, + ConcurrentChangeKind kind, HeapTuple tuple) +{ + RepackDecodingState *dstate; + MemoryContext oldcxt; + BufFile *file; + List *attrs_ext = NIL; + int natt_ext; + + dstate = (RepackDecodingState *) ctx->output_writer_private; + file = dstate->file; + + /* Store the change kind. */ + BufFileWrite(file, &kind, 1); + + /* Use a frequently-reset context to avoid dealing with leaks manually */ + oldcxt = MemoryContextSwitchTo(dstate->change_cxt); + + /* + * If the tuple contains "external indirect" attributes, we need to write + * the contents to the file because we have no control over that memory. + */ + if (HeapTupleHasExternal(tuple)) + { + TupleDesc desc = RelationGetDescr(relation); + TupleTableSlot *slot; + + /* Initialize the slot, if not done already */ + if (dstate->slot == NULL) + { + ResourceOwner saveResourceOwner; + + MemoryContextSwitchTo(dstate->worker_cxt); + saveResourceOwner = CurrentResourceOwner; + CurrentResourceOwner = dstate->worker_resowner; + dstate->slot = MakeSingleTupleTableSlot(desc, &TTSOpsHeapTuple); + MemoryContextSwitchTo(dstate->change_cxt); + CurrentResourceOwner = saveResourceOwner; + } + + slot = dstate->slot; + ExecStoreHeapTuple(tuple, slot, false); + + /* + * Loop over all attributes, and find out which ones we need to spill + * separately, to wit: each one that's a non-null varlena and stored + * out of line. + */ + for (int i = 0; i < desc->natts; i++) + { + CompactAttribute *attr = TupleDescCompactAttr(desc, i); + varlena *varlen; + + if (attr->attisdropped || attr->attlen != -1 || + slot_attisnull(slot, i + 1)) + continue; + + slot_getsomeattrs(slot, i + 1); + + /* + * This is a non-null varlena datum, but we only care if it's + * out-of-line + */ + varlen = (varlena *) DatumGetPointer(slot->tts_values[i]); + if (!VARATT_IS_EXTERNAL(varlen)) + continue; + + /* + * We spill any indirect-external attributes separately from the + * heap tuple. Anything else is written as is. + */ + if (VARATT_IS_EXTERNAL_INDIRECT(varlen)) + attrs_ext = lappend(attrs_ext, varlen); + else + { + /* + * Logical decoding should not produce "external expanded" + * attributes (those actually should never appear on disk), so + * only TOASTed attribute can be seen here. + * + * We get here if the table has external values but only + * in-line values are being updated now. + */ + Assert(VARATT_IS_EXTERNAL_ONDISK(varlen)); + } + } + + ExecClearTuple(slot); + } + + /* + * First, write the original heap tuple, prefixed by its length. Note + * that the external-toast tag for each toasted attribute will be present + * in what we write, so that we know where to restore each one later. + */ + BufFileWrite(file, &tuple->t_len, sizeof(tuple->t_len)); + BufFileWrite(file, tuple->t_data, tuple->t_len); + + /* Then, write the number of external attributes we found. */ + natt_ext = list_length(attrs_ext); + BufFileWrite(file, &natt_ext, sizeof(natt_ext)); + + /* Finally, the attributes themselves, if any */ + foreach_ptr(varlena, attr_val, attrs_ext) + { + attr_val = detoast_external_attr(attr_val); + BufFileWrite(file, attr_val, VARSIZE_ANY(attr_val)); + /* These attributes could be large, so free them right away */ + pfree(attr_val); + } + + /* Cleanup. */ + MemoryContextSwitchTo(oldcxt); + MemoryContextReset(dstate->change_cxt); +} diff --git a/src/backend/replication/repl_gram.y b/src/backend/replication/repl_gram.y index 7440aae5a1a7e..aa8a96a3a603a 100644 --- a/src/backend/replication/repl_gram.y +++ b/src/backend/replication/repl_gram.y @@ -3,7 +3,7 @@ * * repl_gram.y - Parser for the replication commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -279,7 +279,7 @@ alter_replication_slot: ; /* - * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%X [TIMELINE %u] + * START_REPLICATION [SLOT slot] [PHYSICAL] %X/%08X [TIMELINE %u] */ start_replication: K_START_REPLICATION opt_slot opt_physical RECPTR opt_timeline @@ -295,7 +295,7 @@ start_replication: } ; -/* START_REPLICATION SLOT slot LOGICAL %X/%X options */ +/* START_REPLICATION SLOT slot LOGICAL %X/%08X options */ start_logical_replication: K_START_REPLICATION K_SLOT IDENT K_LOGICAL RECPTR plugin_options { diff --git a/src/backend/replication/repl_scanner.l b/src/backend/replication/repl_scanner.l index 014ea8d25c6b7..fcdeca04bf909 100644 --- a/src/backend/replication/repl_scanner.l +++ b/src/backend/replication/repl_scanner.l @@ -4,7 +4,7 @@ * repl_scanner.l * a lexical scanner for the replication commands * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -155,7 +155,7 @@ UPLOAD_MANIFEST { return K_UPLOAD_MANIFEST; } {hexdigit}+\/{hexdigit}+ { uint32 hi, lo; - if (sscanf(yytext, "%X/%X", &hi, &lo) != 2) + if (sscanf(yytext, "%X/%08X", &hi, &lo) != 2) replication_yyerror(NULL, yyscanner, "invalid streaming start location"); yylval->recptr = ((uint64) hi) << 32 | lo; return RECPTR; diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index 600b87fa9cb65..83fcde7471808 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -4,7 +4,7 @@ * Replication slot management. * * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -47,6 +47,7 @@ #include "miscadmin.h" #include "pgstat.h" #include "postmaster/interrupt.h" +#include "replication/logicallauncher.h" #include "replication/slotsync.h" #include "replication/slot.h" #include "replication/walsender_private.h" @@ -54,10 +55,12 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/subsystems.h" #include "utils/builtins.h" #include "utils/guc_hooks.h" #include "utils/injection_point.h" #include "utils/varlena.h" +#include "utils/wait_event.h" /* * Replication slot on-disk data structure. @@ -143,18 +146,28 @@ StaticAssertDecl(lengthof(SlotInvalidationCauses) == (RS_INVAL_MAX_CAUSES + 1), /* Control array for replication slot management */ ReplicationSlotCtlData *ReplicationSlotCtl = NULL; +static void ReplicationSlotsShmemRequest(void *arg); +static void ReplicationSlotsShmemInit(void *arg); + +const ShmemCallbacks ReplicationSlotsShmemCallbacks = { + .request_fn = ReplicationSlotsShmemRequest, + .init_fn = ReplicationSlotsShmemInit, +}; + /* My backend's replication slot in the shared memory array */ ReplicationSlot *MyReplicationSlot = NULL; /* GUC variables */ int max_replication_slots = 10; /* the maximum number of replication * slots */ +int max_repack_replication_slots = 5; /* the maximum number of slots + * for REPACK */ /* * Invalidate replication slots that have remained idle longer than this * duration; '0' disables it. */ -int idle_replication_slot_timeout_mins = 0; +int idle_replication_slot_timeout_secs = 0; /* * This GUC lists streaming replication standby server slot names that @@ -172,6 +185,7 @@ static SyncStandbySlotsConfigData *synchronized_standby_slots_config; static XLogRecPtr ss_oldest_flush_lsn = InvalidXLogRecPtr; static void ReplicationSlotShmemExit(int code, Datum arg); +static bool IsSlotForConflictCheck(const char *name); static void ReplicationSlotDropPtr(ReplicationSlot *slot); /* internal persistency functions */ @@ -180,55 +194,42 @@ static void CreateSlotOnDisk(ReplicationSlot *slot); static void SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel); /* - * Report shared-memory space needed by ReplicationSlotsShmemInit. + * Register shared memory space needed for replication slots. */ -Size -ReplicationSlotsShmemSize(void) +static void +ReplicationSlotsShmemRequest(void *arg) { - Size size = 0; + Size size; - if (max_replication_slots == 0) - return size; + if (max_replication_slots + max_repack_replication_slots == 0) + return; size = offsetof(ReplicationSlotCtlData, replication_slots); size = add_size(size, - mul_size(max_replication_slots, sizeof(ReplicationSlot))); - - return size; + mul_size(max_replication_slots + max_repack_replication_slots, + sizeof(ReplicationSlot))); + ShmemRequestStruct(.name = "ReplicationSlot Ctl", + .size = size, + .ptr = (void **) &ReplicationSlotCtl, + ); } /* - * Allocate and initialize shared memory for replication slots. + * Initialize shared memory for replication slots. */ -void -ReplicationSlotsShmemInit(void) +static void +ReplicationSlotsShmemInit(void *arg) { - bool found; - - if (max_replication_slots == 0) - return; - - ReplicationSlotCtl = (ReplicationSlotCtlData *) - ShmemInitStruct("ReplicationSlot Ctl", ReplicationSlotsShmemSize(), - &found); - - if (!found) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { - int i; - - /* First time through, so initialize */ - MemSet(ReplicationSlotCtl, 0, ReplicationSlotsShmemSize()); - - for (i = 0; i < max_replication_slots; i++) - { - ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[i]; - - /* everything else is zeroed by the memset above */ - SpinLockInit(&slot->mutex); - LWLockInitialize(&slot->io_in_progress_lock, - LWTRANCHE_REPLICATION_SLOT_IO); - ConditionVariableInit(&slot->active_cv); - } + ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[i]; + + /* everything else is zeroed by the memset above */ + slot->active_proc = INVALID_PROC_NUMBER; + SpinLockInit(&slot->mutex); + LWLockInitialize(&slot->io_in_progress_lock, + LWTRANCHE_REPLICATION_SLOT_IO); + ConditionVariableInit(&slot->active_cv); } } @@ -258,31 +259,72 @@ ReplicationSlotShmemExit(int code, Datum arg) /* * Check whether the passed slot name is valid and report errors at elevel. * + * See comments for ReplicationSlotValidateNameInternal(). + */ +bool +ReplicationSlotValidateName(const char *name, bool allow_reserved_name, + int elevel) +{ + int err_code; + char *err_msg = NULL; + char *err_hint = NULL; + + if (!ReplicationSlotValidateNameInternal(name, allow_reserved_name, + &err_code, &err_msg, &err_hint)) + { + /* + * Use errmsg_internal() and errhint_internal() instead of errmsg() + * and errhint(), since the messages from + * ReplicationSlotValidateNameInternal() are already translated. This + * avoids double translation. + */ + ereport(elevel, + errcode(err_code), + errmsg_internal("%s", err_msg), + (err_hint != NULL) ? errhint_internal("%s", err_hint) : 0); + + pfree(err_msg); + if (err_hint != NULL) + pfree(err_hint); + return false; + } + + return true; +} + +/* + * Check whether the passed slot name is valid. + * + * An error will be reported for a reserved replication slot name if + * allow_reserved_name is set to false. + * * Slot names may consist out of [a-z0-9_]{1,NAMEDATALEN-1} which should allow * the name to be used as a directory name on every supported OS. * - * Returns whether the directory name is valid or not if elevel < ERROR. + * Returns true if the slot name is valid. Otherwise, returns false and stores + * the error code, error message, and optional hint in err_code, err_msg, and + * err_hint, respectively. The caller is responsible for freeing err_msg and + * err_hint, which are palloc'd. */ bool -ReplicationSlotValidateName(const char *name, int elevel) +ReplicationSlotValidateNameInternal(const char *name, bool allow_reserved_name, + int *err_code, char **err_msg, char **err_hint) { const char *cp; if (strlen(name) == 0) { - ereport(elevel, - (errcode(ERRCODE_INVALID_NAME), - errmsg("replication slot name \"%s\" is too short", - name))); + *err_code = ERRCODE_INVALID_NAME; + *err_msg = psprintf(_("replication slot name \"%s\" is too short"), name); + *err_hint = NULL; return false; } if (strlen(name) >= NAMEDATALEN) { - ereport(elevel, - (errcode(ERRCODE_NAME_TOO_LONG), - errmsg("replication slot name \"%s\" is too long", - name))); + *err_code = ERRCODE_NAME_TOO_LONG; + *err_msg = psprintf(_("replication slot name \"%s\" is too long"), name); + *err_hint = NULL; return false; } @@ -292,30 +334,42 @@ ReplicationSlotValidateName(const char *name, int elevel) || (*cp >= '0' && *cp <= '9') || (*cp == '_'))) { - ereport(elevel, - (errcode(ERRCODE_INVALID_NAME), - errmsg("replication slot name \"%s\" contains invalid character", - name), - errhint("Replication slot names may only contain lower case letters, numbers, and the underscore character."))); + *err_code = ERRCODE_INVALID_NAME; + *err_msg = psprintf(_("replication slot name \"%s\" contains invalid character"), name); + *err_hint = psprintf(_("Replication slot names may only contain lower case letters, numbers, and the underscore character.")); return false; } } + + if (!allow_reserved_name && IsSlotForConflictCheck(name)) + { + *err_code = ERRCODE_RESERVED_NAME; + *err_msg = psprintf(_("replication slot name \"%s\" is reserved"), name); + *err_hint = psprintf(_("The name \"%s\" is reserved for the conflict detection slot."), + CONFLICT_DETECTION_SLOT); + return false; + } + return true; } +/* + * Return true if the replication slot name is "pg_conflict_detection". + */ +static bool +IsSlotForConflictCheck(const char *name) +{ + return (strcmp(name, CONFLICT_DETECTION_SLOT) == 0); +} + /* * Create a new replication slot and mark it as used by this backend. * * name: Name of the slot * db_specific: logical decoding is db specific; if the slot is going to * be used for that pass true, otherwise false. - * two_phase: Allows decoding of prepared transactions. We allow this option - * to be enabled only at the slot creation time. If we allow this option - * to be changed during decoding then it is quite possible that we skip - * prepare first time because this option was not enabled. Now next time - * during getting changes, if the two_phase option is enabled it can skip - * prepare because by that time start decoding point has been moved. So the - * user will only get commit prepared. + * two_phase: If enabled, allows decoding of prepared transactions. + * repack: If true, use a slot from the pool for REPACK. * failover: If enabled, allows the slot to be synced to standbys so * that logical replication can be resumed after failover. * synced: True if the slot is synchronized from the primary server. @@ -323,14 +377,20 @@ ReplicationSlotValidateName(const char *name, int elevel) void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, bool synced) + bool two_phase, bool repack, bool failover, bool synced) { ReplicationSlot *slot = NULL; - int i; + int startpoint, + endpoint; Assert(MyReplicationSlot == NULL); - ReplicationSlotValidateName(name, ERROR); + /* + * The logical launcher or pg_upgrade may create or migrate an internal + * slot, so using a reserved name is allowed in these cases. + */ + ReplicationSlotValidateName(name, IsBinaryUpgrade || IsLogicalLauncher(), + ERROR); if (failover) { @@ -370,12 +430,16 @@ ReplicationSlotCreate(const char *name, bool db_specific, LWLockAcquire(ReplicationSlotAllocationLock, LW_EXCLUSIVE); /* - * Check for name collision, and identify an allocatable slot. We need to - * hold ReplicationSlotControlLock in shared mode for this, so that nobody - * else can change the in_use flags while we're looking at them. + * Check for name collision (across the whole array), and identify an + * allocatable slot (in the array slice specific to our current use case: + * either general, or REPACK only). We need to hold + * ReplicationSlotControlLock in shared mode for this, so that nobody else + * can change the in_use flags while we're looking at them. */ LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + startpoint = !repack ? 0 : max_replication_slots; + endpoint = max_replication_slots + (repack ? max_repack_replication_slots : 0); + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -383,7 +447,9 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("replication slot \"%s\" already exists", name))); - if (!s->in_use && slot == NULL) + + if (i >= startpoint && i < endpoint && + !s->in_use && slot == NULL) slot = s; } LWLockRelease(ReplicationSlotControlLock); @@ -393,7 +459,8 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("all replication slots are in use"), - errhint("Free one or increase \"max_replication_slots\"."))); + errhint("Free one or increase \"%s\".", + repack ? "max_repack_replication_slots" : "max_replication_slots"))); /* * Since this slot is not in use, nobody should be looking at any part of @@ -402,7 +469,7 @@ ReplicationSlotCreate(const char *name, bool db_specific, * be doing that. So it's safe to initialize the slot. */ Assert(!slot->in_use); - Assert(slot->active_pid == 0); + Assert(slot->active_proc == INVALID_PROC_NUMBER); /* first initialize persistent data */ memset(&slot->data, 0, sizeof(ReplicationSlotPersistentData)); @@ -424,7 +491,9 @@ ReplicationSlotCreate(const char *name, bool db_specific, slot->candidate_restart_valid = InvalidXLogRecPtr; slot->candidate_restart_lsn = InvalidXLogRecPtr; slot->last_saved_confirmed_flush = InvalidXLogRecPtr; + slot->last_saved_restart_lsn = InvalidXLogRecPtr; slot->inactive_since = 0; + slot->slotsync_skip_reason = SS_SKIP_NONE; /* * Create the slot on disk. We haven't actually marked the slot allocated @@ -444,8 +513,8 @@ ReplicationSlotCreate(const char *name, bool db_specific, /* We can now mark the slot active, and that makes it our slot. */ SpinLockAcquire(&slot->mutex); - Assert(slot->active_pid == 0); - slot->active_pid = MyProcPid; + Assert(slot->active_proc == INVALID_PROC_NUMBER); + slot->active_proc = MyProcNumber; SpinLockRelease(&slot->mutex); MyReplicationSlot = slot; @@ -484,7 +553,7 @@ SearchNamedReplicationSlot(const char *name, bool need_lock) if (need_lock) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -512,7 +581,8 @@ int ReplicationSlotIndex(ReplicationSlot *slot) { Assert(slot >= ReplicationSlotCtl->replication_slots && - slot < ReplicationSlotCtl->replication_slots + max_replication_slots); + slot < ReplicationSlotCtl->replication_slots + + (max_replication_slots + max_repack_replication_slots)); return slot - ReplicationSlotCtl->replication_slots; } @@ -559,6 +629,7 @@ void ReplicationSlotAcquire(const char *name, bool nowait, bool error_if_invalid) { ReplicationSlot *s; + ProcNumber active_proc; int active_pid; Assert(name != NULL); @@ -568,7 +639,7 @@ ReplicationSlotAcquire(const char *name, bool nowait, bool error_if_invalid) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - /* Check if the slot exits with the given name. */ + /* Check if the slot exists with the given name. */ s = SearchNamedReplicationSlot(name, false); if (s == NULL || !s->in_use) { @@ -580,6 +651,17 @@ ReplicationSlotAcquire(const char *name, bool nowait, bool error_if_invalid) name))); } + /* + * Do not allow users to acquire the reserved slot. This scenario may + * occur if the launcher that owns the slot has terminated unexpectedly + * due to an error, and a backend process attempts to reuse the slot. + */ + if (!IsLogicalLauncher() && IsSlotForConflictCheck(name)) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("cannot acquire replication slot \"%s\"", name), + errdetail("The slot is reserved for conflict detection and can only be acquired by logical replication launcher.")); + /* * This is the slot we want; check if it's active under some other * process. In single user mode, we don't need this check. @@ -600,17 +682,18 @@ ReplicationSlotAcquire(const char *name, bool nowait, bool error_if_invalid) * to inactive_since in InvalidatePossiblyObsoleteSlot. */ SpinLockAcquire(&s->mutex); - if (s->active_pid == 0) - s->active_pid = MyProcPid; - active_pid = s->active_pid; + if (s->active_proc == INVALID_PROC_NUMBER) + s->active_proc = MyProcNumber; + active_proc = s->active_proc; ReplicationSlotSetInactiveSince(s, 0, false); SpinLockRelease(&s->mutex); } else { - active_pid = MyProcPid; + s->active_proc = active_proc = MyProcNumber; ReplicationSlotSetInactiveSince(s, 0, true); } + active_pid = GetPGProcByNumber(active_proc)->pid; LWLockRelease(ReplicationSlotControlLock); /* @@ -618,7 +701,7 @@ ReplicationSlotAcquire(const char *name, bool nowait, bool error_if_invalid) * wait until the owning process signals us that it's been released, or * error out. */ - if (active_pid != MyProcPid) + if (active_proc != MyProcNumber) { if (!nowait) { @@ -687,16 +770,15 @@ ReplicationSlotRelease(void) { ReplicationSlot *slot = MyReplicationSlot; char *slotname = NULL; /* keep compiler quiet */ - bool is_logical = false; /* keep compiler quiet */ + bool is_logical; TimestampTz now = 0; - Assert(slot != NULL && slot->active_pid != 0); + Assert(slot != NULL && slot->active_proc != INVALID_PROC_NUMBER); + + is_logical = SlotIsLogical(slot); if (am_walsender) - { slotname = pstrdup(NameStr(slot->data.name)); - is_logical = SlotIsLogical(slot); - } if (slot->data.persistency == RS_EPHEMERAL) { @@ -706,6 +788,14 @@ ReplicationSlotRelease(void) * data. */ ReplicationSlotDropAcquired(); + + /* + * Request to disable logical decoding, even though this slot may not + * have been the last logical slot. The checkpointer will verify if + * logical decoding should actually be disabled. + */ + if (is_logical) + RequestDisableLogicalDecoding(); } /* @@ -736,7 +826,7 @@ ReplicationSlotRelease(void) * disconnecting, but wake up others that may be waiting for it. */ SpinLockAcquire(&slot->mutex); - slot->active_pid = 0; + slot->active_proc = INVALID_PROC_NUMBER; ReplicationSlotSetInactiveSince(slot, now, false); SpinLockRelease(&slot->mutex); ConditionVariableBroadcast(&slot->active_cv); @@ -770,17 +860,23 @@ ReplicationSlotRelease(void) * * Cleanup only synced temporary slots if 'synced_only' is true, else * cleanup all temporary slots. + * + * If it drops the last logical slot in the cluster, requests to disable + * logical decoding. */ void ReplicationSlotCleanup(bool synced_only) { int i; + bool found_valid_logicalslot; + bool dropped_logical = false; Assert(MyReplicationSlot == NULL); restart: + found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -788,13 +884,20 @@ ReplicationSlotCleanup(bool synced_only) continue; SpinLockAcquire(&s->mutex); - if ((s->active_pid == MyProcPid && + + found_valid_logicalslot |= + (SlotIsLogical(s) && s->data.invalidated == RS_INVAL_NONE); + + if ((s->active_proc == MyProcNumber && (!synced_only || s->data.synced))) { Assert(s->data.persistency == RS_TEMPORARY); SpinLockRelease(&s->mutex); LWLockRelease(ReplicationSlotControlLock); /* avoid deadlock */ + if (SlotIsLogical(s)) + dropped_logical = true; + ReplicationSlotDropPtr(s); ConditionVariableBroadcast(&s->active_cv); @@ -805,6 +908,9 @@ ReplicationSlotCleanup(bool synced_only) } LWLockRelease(ReplicationSlotControlLock); + + if (dropped_logical && !found_valid_logicalslot) + RequestDisableLogicalDecoding(); } /* @@ -813,6 +919,8 @@ ReplicationSlotCleanup(bool synced_only) void ReplicationSlotDrop(const char *name, bool nowait) { + bool is_logical; + Assert(MyReplicationSlot == NULL); ReplicationSlotAcquire(name, nowait, false); @@ -827,11 +935,26 @@ ReplicationSlotDrop(const char *name, bool nowait) errmsg("cannot drop replication slot \"%s\"", name), errdetail("This replication slot is being synchronized from the primary server.")); + is_logical = SlotIsLogical(MyReplicationSlot); + ReplicationSlotDropAcquired(); + + if (is_logical) + RequestDisableLogicalDecoding(); } /* * Change the definition of the slot identified by the specified name. + * + * Altering the two_phase property of a slot requires caution on the + * client-side. Enabling it at any random point during decoding has the + * risk that transactions prepared before this change may be skipped by + * the decoder, leading to missing prepare records on the client. So, we + * enable it for subscription related slots only once the initial tablesync + * is finished. See comments atop worker.c. Disabling it is safe only when + * there are no pending prepared transaction, otherwise, the changes of + * already prepared transactions can be replicated again along with their + * corresponding commit leading to duplicate data or errors. */ void ReplicationSlotAlter(const char *name, const bool *failover, @@ -976,7 +1099,7 @@ ReplicationSlotDropPtr(ReplicationSlot *slot) bool fail_softly = slot->data.persistency != RS_PERSISTENT; SpinLockAcquire(&slot->mutex); - slot->active_pid = 0; + slot->active_proc = INVALID_PROC_NUMBER; SpinLockRelease(&slot->mutex); /* wake up anyone waiting on this slot */ @@ -998,7 +1121,7 @@ ReplicationSlotDropPtr(ReplicationSlot *slot) * Also wake up processes waiting for it. */ LWLockAcquire(ReplicationSlotControlLock, LW_EXCLUSIVE); - slot->active_pid = 0; + slot->active_proc = INVALID_PROC_NUMBER; slot->in_use = false; LWLockRelease(ReplicationSlotControlLock); ConditionVariableBroadcast(&slot->active_cv); @@ -1093,8 +1216,11 @@ ReplicationSlotPersist(void) /* * Compute the oldest xmin across all slots and store it in the ProcArray. * - * If already_locked is true, ProcArrayLock has already been acquired - * exclusively. + * If already_locked is true, both the ReplicationSlotControlLock and the + * ProcArrayLock have already been acquired exclusively. It is crucial that the + * caller first acquires the ReplicationSlotControlLock, followed by the + * ProcArrayLock, to prevent any undetectable deadlocks since this function + * acquires them in that order. */ void ReplicationSlotsComputeRequiredXmin(bool already_locked) @@ -1104,10 +1230,35 @@ ReplicationSlotsComputeRequiredXmin(bool already_locked) TransactionId agg_catalog_xmin = InvalidTransactionId; Assert(ReplicationSlotCtl != NULL); + Assert(!already_locked || + (LWLockHeldByMeInMode(ReplicationSlotControlLock, LW_EXCLUSIVE) && + LWLockHeldByMeInMode(ProcArrayLock, LW_EXCLUSIVE))); - LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); + /* + * Hold the ReplicationSlotControlLock until after updating the slot xmin + * values, so no backend updates the initial xmin for newly created slot + * concurrently. A shared lock is used here to minimize lock contention, + * especially when many slots exist and advancements occur frequently. + * This is safe since an exclusive lock is taken during initial slot xmin + * update in slot creation. + * + * One might think that we can hold the ProcArrayLock exclusively and + * update the slot xmin values, but it could increase lock contention on + * the ProcArrayLock, which is not great since this function can be called + * at non-negligible frequency. + * + * Concurrent invocation of this function may cause the computed slot xmin + * to regress. However, this is harmless because tuples prior to the most + * recent xmin are no longer useful once advancement occurs (see + * LogicalConfirmReceivedLocation where the slot's xmin value is flushed + * before updating the effective_xmin). Thus, such regression merely + * prevents VACUUM from prematurely removing tuples without causing the + * early deletion of required data. + */ + if (!already_locked) + LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; TransactionId effective_xmin; @@ -1140,9 +1291,10 @@ ReplicationSlotsComputeRequiredXmin(bool already_locked) agg_catalog_xmin = effective_catalog_xmin; } - LWLockRelease(ReplicationSlotControlLock); - ProcArraySetReplicationSlotXmin(agg_xmin, agg_catalog_xmin, already_locked); + + if (!already_locked) + LWLockRelease(ReplicationSlotControlLock); } /* @@ -1161,26 +1313,47 @@ ReplicationSlotsComputeRequiredLSN(void) Assert(ReplicationSlotCtl != NULL); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; XLogRecPtr restart_lsn; + XLogRecPtr last_saved_restart_lsn; bool invalidated; + ReplicationSlotPersistency persistency; if (!s->in_use) continue; SpinLockAcquire(&s->mutex); + persistency = s->data.persistency; restart_lsn = s->data.restart_lsn; invalidated = s->data.invalidated != RS_INVAL_NONE; + last_saved_restart_lsn = s->last_saved_restart_lsn; SpinLockRelease(&s->mutex); /* invalidated slots need not apply */ if (invalidated) continue; - if (restart_lsn != InvalidXLogRecPtr && - (min_required == InvalidXLogRecPtr || + /* + * For persistent slot use last_saved_restart_lsn to compute the + * oldest LSN for removal of WAL segments. The segments between + * last_saved_restart_lsn and restart_lsn might be needed by a + * persistent slot in the case of database crash. Non-persistent + * slots can't survive the database crash, so we don't care about + * last_saved_restart_lsn for them. + */ + if (persistency == RS_PERSISTENT) + { + if (XLogRecPtrIsValid(last_saved_restart_lsn) && + restart_lsn > last_saved_restart_lsn) + { + restart_lsn = last_saved_restart_lsn; + } + } + + if (XLogRecPtrIsValid(restart_lsn) && + (!XLogRecPtrIsValid(min_required) || restart_lsn < min_required)) min_required = restart_lsn; } @@ -1207,16 +1380,18 @@ ReplicationSlotsComputeLogicalRestartLSN(void) XLogRecPtr result = InvalidXLogRecPtr; int i; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return InvalidXLogRecPtr; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; XLogRecPtr restart_lsn; + XLogRecPtr last_saved_restart_lsn; bool invalidated; + ReplicationSlotPersistency persistency; s = &ReplicationSlotCtl->replication_slots[i]; @@ -1230,18 +1405,37 @@ ReplicationSlotsComputeLogicalRestartLSN(void) /* read once, it's ok if it increases while we're checking */ SpinLockAcquire(&s->mutex); + persistency = s->data.persistency; restart_lsn = s->data.restart_lsn; invalidated = s->data.invalidated != RS_INVAL_NONE; + last_saved_restart_lsn = s->last_saved_restart_lsn; SpinLockRelease(&s->mutex); /* invalidated slots need not apply */ if (invalidated) continue; - if (restart_lsn == InvalidXLogRecPtr) + /* + * For persistent slot use last_saved_restart_lsn to compute the + * oldest LSN for removal of WAL segments. The segments between + * last_saved_restart_lsn and restart_lsn might be needed by a + * persistent slot in the case of database crash. Non-persistent + * slots can't survive the database crash, so we don't care about + * last_saved_restart_lsn for them. + */ + if (persistency == RS_PERSISTENT) + { + if (XLogRecPtrIsValid(last_saved_restart_lsn) && + restart_lsn > last_saved_restart_lsn) + { + restart_lsn = last_saved_restart_lsn; + } + } + + if (!XLogRecPtrIsValid(restart_lsn)) continue; - if (result == InvalidXLogRecPtr || + if (!XLogRecPtrIsValid(result) || restart_lsn < result) result = restart_lsn; } @@ -1266,11 +1460,11 @@ ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive) *nslots = *nactive = 0; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; @@ -1293,7 +1487,7 @@ ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive) /* count slots with spinlock held */ SpinLockAcquire(&s->mutex); (*nslots)++; - if (s->active_pid != 0) + if (s->active_proc != INVALID_PROC_NUMBER) (*nactive)++; SpinLockRelease(&s->mutex); } @@ -1316,22 +1510,28 @@ ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive) * * This routine isn't as efficient as it could be - but we don't drop * databases often, especially databases with lots of slots. + * + * If it drops the last logical slot in the cluster, it requests to disable + * logical decoding. */ void ReplicationSlotsDropDBSlots(Oid dboid) { int i; + bool found_valid_logicalslot; + bool dropped = false; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return; restart: + found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; char *slotname; - int active_pid; + ProcNumber active_proc; s = &ReplicationSlotCtl->replication_slots[i]; @@ -1343,21 +1543,29 @@ ReplicationSlotsDropDBSlots(Oid dboid) if (!SlotIsLogical(s)) continue; + /* + * Check logical slots on other databases too so we can disable + * logical decoding only if no slots in the cluster. + */ + SpinLockAcquire(&s->mutex); + found_valid_logicalslot |= (s->data.invalidated == RS_INVAL_NONE); + SpinLockRelease(&s->mutex); + /* not our database, skip */ if (s->data.database != dboid) continue; - /* NB: intentionally including invalidated slots */ + /* NB: intentionally including invalidated slots to drop */ /* acquire slot, so ReplicationSlotDropAcquired can be reused */ SpinLockAcquire(&s->mutex); /* can't change while ReplicationSlotControlLock is held */ slotname = NameStr(s->data.name); - active_pid = s->active_pid; - if (active_pid == 0) + active_proc = s->active_proc; + if (active_proc == INVALID_PROC_NUMBER) { MyReplicationSlot = s; - s->active_pid = MyProcPid; + s->active_proc = MyProcNumber; } SpinLockRelease(&s->mutex); @@ -1382,11 +1590,11 @@ ReplicationSlotsDropDBSlots(Oid dboid) * XXX: We can consider shutting down the slot sync worker before * trying to drop synced temporary slots here. */ - if (active_pid) + if (active_proc != INVALID_PROC_NUMBER) ereport(ERROR, (errcode(ERRCODE_OBJECT_IN_USE), errmsg("replication slot \"%s\" is active for PID %d", - slotname, active_pid))); + slotname, GetPGProcByNumber(active_proc)->pid))); /* * To avoid duplicating ReplicationSlotDropAcquired() and to avoid @@ -1399,28 +1607,79 @@ ReplicationSlotsDropDBSlots(Oid dboid) */ LWLockRelease(ReplicationSlotControlLock); ReplicationSlotDropAcquired(); + dropped = true; goto restart; } LWLockRelease(ReplicationSlotControlLock); + + if (dropped && !found_valid_logicalslot) + RequestDisableLogicalDecoding(); } +/* + * Returns true if there is at least one in-use valid logical replication slot. + */ +bool +CheckLogicalSlotExists(void) +{ + bool found = false; + + if (max_replication_slots + max_repack_replication_slots <= 0) + return false; + + LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) + { + ReplicationSlot *s; + bool invalidated; + + s = &ReplicationSlotCtl->replication_slots[i]; + + /* cannot change while ReplicationSlotCtlLock is held */ + if (!s->in_use) + continue; + + if (SlotIsPhysical(s)) + continue; + + SpinLockAcquire(&s->mutex); + invalidated = s->data.invalidated != RS_INVAL_NONE; + SpinLockRelease(&s->mutex); + + if (invalidated) + continue; + + found = true; + break; + } + LWLockRelease(ReplicationSlotControlLock); + + return found; +} /* * Check whether the server's configuration supports using replication * slots. */ void -CheckSlotRequirements(void) +CheckSlotRequirements(bool repack) { /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() * needs the same check. */ - if (max_replication_slots == 0) + if (!repack && max_replication_slots == 0) ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("replication slots can only be used if \"max_replication_slots\" > 0"))); + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("replication slots can only be used if \"%s\" > 0", + "max_replication_slots")); + + if (repack && max_repack_replication_slots == 0) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("REPACK can only be used if \"%s\" > 0", + "max_repack_replication_slots")); if (wal_level < WAL_LEVEL_REPLICA) ereport(ERROR, @@ -1452,69 +1711,72 @@ void ReplicationSlotReserveWal(void) { ReplicationSlot *slot = MyReplicationSlot; + XLogSegNo segno; + XLogRecPtr restart_lsn; Assert(slot != NULL); - Assert(slot->data.restart_lsn == InvalidXLogRecPtr); + Assert(!XLogRecPtrIsValid(slot->data.restart_lsn)); + Assert(!XLogRecPtrIsValid(slot->last_saved_restart_lsn)); /* - * The replication slot mechanism is used to prevent removal of required - * WAL. As there is no interlock between this routine and checkpoints, WAL - * segments could concurrently be removed when a now stale return value of - * ReplicationSlotsComputeRequiredLSN() is used. In the unlikely case that - * this happens we'll just retry. + * The replication slot mechanism is used to prevent the removal of + * required WAL. + * + * Acquire an exclusive lock to prevent the checkpoint process from + * concurrently computing the minimum slot LSN (see + * CheckPointReplicationSlots). This ensures that the WAL reserved for + * replication cannot be removed during a checkpoint. + * + * The mechanism is reliable because if WAL reservation occurs first, the + * checkpoint must wait for the restart_lsn update before determining the + * minimum non-removable LSN. On the other hand, if the checkpoint happens + * first, subsequent WAL reservations will select positions at or beyond + * the redo pointer of that checkpoint. */ - while (true) - { - XLogSegNo segno; - XLogRecPtr restart_lsn; + LWLockAcquire(ReplicationSlotAllocationLock, LW_EXCLUSIVE); - /* - * For logical slots log a standby snapshot and start logical decoding - * at exactly that position. That allows the slot to start up more - * quickly. But on a standby we cannot do WAL writes, so just use the - * replay pointer; effectively, an attempt to create a logical slot on - * standby will cause it to wait for an xl_running_xact record to be - * logged independently on the primary, so that a snapshot can be - * built using the record. - * - * None of this is needed (or indeed helpful) for physical slots as - * they'll start replay at the last logged checkpoint anyway. Instead - * return the location of the last redo LSN. While that slightly - * increases the chance that we have to retry, it's where a base - * backup has to start replay at. - */ - if (SlotIsPhysical(slot)) - restart_lsn = GetRedoRecPtr(); - else if (RecoveryInProgress()) - restart_lsn = GetXLogReplayRecPtr(NULL); - else - restart_lsn = GetXLogInsertRecPtr(); + /* + * For logical slots log a standby snapshot and start logical decoding at + * exactly that position. That allows the slot to start up more quickly. + * But on a standby we cannot do WAL writes, so just use the replay + * pointer; effectively, an attempt to create a logical slot on standby + * will cause it to wait for an xl_running_xact record to be logged + * independently on the primary, so that a snapshot can be built using the + * record. + * + * None of this is needed (or indeed helpful) for physical slots as + * they'll start replay at the last logged checkpoint anyway. Instead, + * return the location of the last redo LSN, where a base backup has to + * start replay at. + */ + if (SlotIsPhysical(slot)) + restart_lsn = GetRedoRecPtr(); + else if (RecoveryInProgress()) + restart_lsn = GetXLogReplayRecPtr(NULL); + else + restart_lsn = GetXLogInsertRecPtr(); - SpinLockAcquire(&slot->mutex); - slot->data.restart_lsn = restart_lsn; - SpinLockRelease(&slot->mutex); + SpinLockAcquire(&slot->mutex); + slot->data.restart_lsn = restart_lsn; + SpinLockRelease(&slot->mutex); - /* prevent WAL removal as fast as possible */ - ReplicationSlotsComputeRequiredLSN(); + /* prevent WAL removal as fast as possible */ + ReplicationSlotsComputeRequiredLSN(); - /* - * If all required WAL is still there, great, otherwise retry. The - * slot should prevent further removal of WAL, unless there's a - * concurrent ReplicationSlotsComputeRequiredLSN() after we've written - * the new restart_lsn above, so normally we should never need to loop - * more than twice. - */ - XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size); - if (XLogGetLastRemovedSegno() < segno) - break; - } + /* Checkpoint shouldn't remove the required WAL. */ + XLByteToSeg(slot->data.restart_lsn, segno, wal_segment_size); + if (XLogGetLastRemovedSegno() >= segno) + elog(ERROR, "WAL required by replication slot %s has been removed concurrently", + NameStr(slot->data.name)); + + LWLockRelease(ReplicationSlotAllocationLock); if (!RecoveryInProgress() && SlotIsLogical(slot)) { XLogRecPtr flushptr; /* make sure we have enough information to start */ - flushptr = LogStandbySnapshot(); + flushptr = LogStandbySnapshot(InvalidOid); /* and make sure it's fsynced to disk */ XLogFlush(flushptr); @@ -1547,8 +1809,8 @@ ReportSlotInvalidation(ReplicationSlotInvalidationCause cause, uint64 ex = oldestLSN - restart_lsn; appendStringInfo(&err_detail, - ngettext("The slot's restart_lsn %X/%X exceeds the limit by %" PRIu64 " byte.", - "The slot's restart_lsn %X/%X exceeds the limit by %" PRIu64 " bytes.", + ngettext("The slot's restart_lsn %X/%08X exceeds the limit by %" PRIu64 " byte.", + "The slot's restart_lsn %X/%08X exceeds the limit by %" PRIu64 " bytes.", ex), LSN_FORMAT_ARGS(restart_lsn), ex); @@ -1563,18 +1825,15 @@ ReportSlotInvalidation(ReplicationSlotInvalidationCause cause, break; case RS_INVAL_WAL_LEVEL: - appendStringInfoString(&err_detail, _("Logical decoding on standby requires \"wal_level\" >= \"logical\" on the primary server.")); + appendStringInfoString(&err_detail, _("Logical decoding on standby requires the primary server to either set \"wal_level\" >= \"logical\" or have at least one logical slot when \"wal_level\" = \"replica\".")); break; case RS_INVAL_IDLE_TIMEOUT: { - int minutes = slot_idle_seconds / SECS_PER_MINUTE; - int secs = slot_idle_seconds % SECS_PER_MINUTE; - /* translator: %s is a GUC variable name */ - appendStringInfo(&err_detail, _("The slot's idle time of %dmin %02ds exceeds the configured \"%s\" duration of %dmin."), - minutes, secs, "idle_replication_slot_timeout", - idle_replication_slot_timeout_mins); + appendStringInfo(&err_detail, _("The slot's idle time of %lds exceeds the configured \"%s\" duration of %ds."), + slot_idle_seconds, "idle_replication_slot_timeout", + idle_replication_slot_timeout_secs); /* translator: %s is a GUC variable name */ appendStringInfo(&err_hint, _("You might need to increase \"%s\"."), "idle_replication_slot_timeout"); @@ -1612,8 +1871,8 @@ ReportSlotInvalidation(ReplicationSlotInvalidationCause cause, static inline bool CanInvalidateIdleSlot(ReplicationSlot *s) { - return (idle_replication_slot_timeout_mins != 0 && - !XLogRecPtrIsInvalid(s->data.restart_lsn) && + return (idle_replication_slot_timeout_secs != 0 && + XLogRecPtrIsValid(s->data.restart_lsn) && s->inactive_since > 0 && !(RecoveryInProgress() && s->data.synced)); } @@ -1629,17 +1888,16 @@ static ReplicationSlotInvalidationCause DetermineSlotInvalidationCause(uint32 possible_causes, ReplicationSlot *s, XLogRecPtr oldestLSN, Oid dboid, TransactionId snapshotConflictHorizon, - TransactionId initial_effective_xmin, - TransactionId initial_catalog_effective_xmin, - XLogRecPtr initial_restart_lsn, TimestampTz *inactive_since, TimestampTz now) { Assert(possible_causes != RS_INVAL_NONE); if (possible_causes & RS_INVAL_WAL_REMOVED) { - if (initial_restart_lsn != InvalidXLogRecPtr && - initial_restart_lsn < oldestLSN) + XLogRecPtr restart_lsn = s->data.restart_lsn; + + if (XLogRecPtrIsValid(restart_lsn) && + restart_lsn < oldestLSN) return RS_INVAL_WAL_REMOVED; } @@ -1649,12 +1907,15 @@ DetermineSlotInvalidationCause(uint32 possible_causes, ReplicationSlot *s, if (SlotIsLogical(s) && (dboid == InvalidOid || dboid == s->data.database)) { - if (TransactionIdIsValid(initial_effective_xmin) && - TransactionIdPrecedesOrEquals(initial_effective_xmin, + TransactionId effective_xmin = s->effective_xmin; + TransactionId catalog_effective_xmin = s->effective_catalog_xmin; + + if (TransactionIdIsValid(effective_xmin) && + TransactionIdPrecedesOrEquals(effective_xmin, snapshotConflictHorizon)) return RS_INVAL_HORIZON; - else if (TransactionIdIsValid(initial_catalog_effective_xmin) && - TransactionIdPrecedesOrEquals(initial_catalog_effective_xmin, + else if (TransactionIdIsValid(catalog_effective_xmin) && + TransactionIdPrecedesOrEquals(catalog_effective_xmin, snapshotConflictHorizon)) return RS_INVAL_HORIZON; } @@ -1673,9 +1934,9 @@ DetermineSlotInvalidationCause(uint32 possible_causes, ReplicationSlot *s, if (CanInvalidateIdleSlot(s)) { /* - * We simulate the invalidation due to idle_timeout as the minimum - * time idle time is one minute which makes tests take a long - * time. + * Simulate the invalidation due to idle_timeout to test the + * timeout behavior promptly, without waiting for it to trigger + * naturally. */ #ifdef USE_INJECTION_POINTS if (IS_INJECTION_POINT_ATTACHED("slot-timeout-inval")) @@ -1690,7 +1951,7 @@ DetermineSlotInvalidationCause(uint32 possible_causes, ReplicationSlot *s, * idle_replication_slot_timeout GUC. */ if (TimestampDifferenceExceedsSeconds(s->inactive_since, now, - idle_replication_slot_timeout_mins * SECS_PER_MINUTE)) + idle_replication_slot_timeout_secs)) { *inactive_since = s->inactive_since; return RS_INVAL_IDLE_TIMEOUT; @@ -1706,10 +1967,11 @@ DetermineSlotInvalidationCause(uint32 possible_causes, ReplicationSlot *s, * * Acquires the given slot and mark it invalid, if necessary and possible. * - * Returns whether ReplicationSlotControlLock was released in the interim (and - * in that case we're not holding the lock at return, otherwise we are). + * Returns true if the slot was invalidated. * - * Sets *invalidated true if the slot was invalidated. (Untouched otherwise.) + * Set *released_lock_out if ReplicationSlotControlLock was released in the + * interim (and in that case we're not holding the lock at return, otherwise + * we are). * * This is inherently racy, because we release the LWLock * for syscalls, so caller must restart if we return true. @@ -1719,21 +1981,18 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, ReplicationSlot *s, XLogRecPtr oldestLSN, Oid dboid, TransactionId snapshotConflictHorizon, - bool *invalidated) + bool *released_lock_out) { int last_signaled_pid = 0; bool released_lock = false; - bool terminated = false; - TransactionId initial_effective_xmin = InvalidTransactionId; - TransactionId initial_catalog_effective_xmin = InvalidTransactionId; - XLogRecPtr initial_restart_lsn = InvalidXLogRecPtr; - ReplicationSlotInvalidationCause invalidation_cause_prev PG_USED_FOR_ASSERTS_ONLY = RS_INVAL_NONE; + bool invalidated = false; TimestampTz inactive_since = 0; for (;;) { XLogRecPtr restart_lsn; NameData slotname; + ProcNumber active_proc; int active_pid = 0; ReplicationSlotInvalidationCause invalidation_cause = RS_INVAL_NONE; TimestampTz now = 0; @@ -1770,42 +2029,12 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, /* we do nothing if the slot is already invalid */ if (s->data.invalidated == RS_INVAL_NONE) - { - /* - * The slot's mutex will be released soon, and it is possible that - * those values change since the process holding the slot has been - * terminated (if any), so record them here to ensure that we - * would report the correct invalidation cause. - * - * Unlike other slot attributes, slot's inactive_since can't be - * changed until the acquired slot is released or the owning - * process is terminated. So, the inactive slot can only be - * invalidated immediately without being terminated. - */ - if (!terminated) - { - initial_restart_lsn = s->data.restart_lsn; - initial_effective_xmin = s->effective_xmin; - initial_catalog_effective_xmin = s->effective_catalog_xmin; - } - invalidation_cause = DetermineSlotInvalidationCause(possible_causes, s, oldestLSN, dboid, snapshotConflictHorizon, - initial_effective_xmin, - initial_catalog_effective_xmin, - initial_restart_lsn, &inactive_since, now); - } - - /* - * The invalidation cause recorded previously should not change while - * the process owning the slot (if any) has been terminated. - */ - Assert(!(invalidation_cause_prev != RS_INVAL_NONE && terminated && - invalidation_cause_prev != invalidation_cause)); /* if there's no invalidation, we're done */ if (invalidation_cause == RS_INVAL_NONE) @@ -1817,17 +2046,22 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, } slotname = s->data.name; - active_pid = s->active_pid; + active_proc = s->active_proc; /* * If the slot can be acquired, do so and mark it invalidated * immediately. Otherwise we'll signal the owning process, below, and * retry. + * + * Note: Unlike other slot attributes, slot's inactive_since can't be + * changed until the acquired slot is released or the owning process + * is terminated. So, the inactive slot can only be invalidated + * immediately without being terminated. */ - if (active_pid == 0) + if (active_proc == INVALID_PROC_NUMBER) { MyReplicationSlot = s; - s->active_pid = MyProcPid; + s->active_proc = MyProcNumber; s->data.invalidated = invalidation_cause; /* @@ -1835,23 +2069,22 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, * just rely on .invalidated. */ if (invalidation_cause == RS_INVAL_WAL_REMOVED) + { s->data.restart_lsn = InvalidXLogRecPtr; + s->last_saved_restart_lsn = InvalidXLogRecPtr; + } /* Let caller know */ - *invalidated = true; + invalidated = true; + } + else + { + active_pid = GetPGProcByNumber(active_proc)->pid; + Assert(active_pid != 0); } SpinLockRelease(&s->mutex); - /* - * The logical replication slots shouldn't be invalidated as GUC - * max_slot_wal_keep_size is set to -1 and - * idle_replication_slot_timeout is set to 0 during the binary - * upgrade. See check_old_cluster_for_valid_slots() where we ensure - * that no invalidated before the upgrade. - */ - Assert(!(*invalidated && SlotIsLogical(s) && IsBinaryUpgrade)); - /* * Calculate the idle time duration of the slot if slot is marked * invalidated with RS_INVAL_IDLE_TIMEOUT. @@ -1864,7 +2097,7 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, &slot_idle_usecs); } - if (active_pid != 0) + if (active_proc != INVALID_PROC_NUMBER) { /* * Prepare the sleep on the slot's condition variable before @@ -1896,15 +2129,13 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, slot_idle_secs); if (MyBackendType == B_STARTUP) - (void) SendProcSignal(active_pid, - PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT, - INVALID_PROC_NUMBER); + (void) SignalRecoveryConflict(GetPGProcByNumber(active_proc), + active_pid, + RECOVERY_CONFLICT_LOGICALSLOT); else (void) kill(active_pid, SIGTERM); last_signaled_pid = active_pid; - terminated = true; - invalidation_cause_prev = invalidation_cause; } /* Wait until the slot is released. */ @@ -1915,6 +2146,14 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, * Re-acquire lock and start over; we expect to invalidate the * slot next time (unless another process acquires the slot in the * meantime). + * + * Note: It is possible for a slot to advance its restart_lsn or + * xmin values sufficiently between when we release the mutex and + * when we recheck, moving from a conflicting state to a non + * conflicting state. This is intentional and safe: if the slot + * has caught up while we're busy here, the resources we were + * concerned about (WAL segments or tuples) have not yet been + * removed, and there's no reason to invalidate the slot. */ LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); continue; @@ -1949,7 +2188,8 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, Assert(released_lock == !LWLockHeldByMe(ReplicationSlotControlLock)); - return released_lock; + *released_lock_out = released_lock; + return invalidated; } /* @@ -1962,7 +2202,8 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, * - RS_INVAL_WAL_REMOVED: requires a LSN older than the given segment * - RS_INVAL_HORIZON: requires a snapshot <= the given horizon in the given * db; dboid may be InvalidOid for shared relations - * - RS_INVAL_WAL_LEVEL: is logical and wal_level is insufficient + * - RS_INVAL_WAL_LEVEL: is a logical slot and effective_wal_level is not + * logical. * - RS_INVAL_IDLE_TIMEOUT: has been idle longer than the configured * "idle_replication_slot_timeout" duration. * @@ -1970,6 +2211,9 @@ InvalidatePossiblyObsoleteSlot(uint32 possible_causes, * causes in a single pass, minimizing redundant iterations. The "cause" * parameter can be a MASK representing one or more of the defined causes. * + * If it invalidates the last logical slot in the cluster, it requests to + * disable logical decoding. + * * NB - this runs as part of checkpoint, so avoid raising errors if possible. */ bool @@ -1979,32 +2223,71 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, { XLogRecPtr oldestLSN; bool invalidated = false; + bool invalidated_logical = false; + bool found_valid_logicalslot; Assert(!(possible_causes & RS_INVAL_HORIZON) || TransactionIdIsValid(snapshotConflictHorizon)); Assert(!(possible_causes & RS_INVAL_WAL_REMOVED) || oldestSegno > 0); Assert(possible_causes != RS_INVAL_NONE); - if (max_replication_slots == 0) + if (max_replication_slots == 0 && max_repack_replication_slots == 0) return invalidated; XLogSegNoOffsetToRecPtr(oldestSegno, 0, wal_segment_size, oldestLSN); restart: + found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; + bool released_lock = false; if (!s->in_use) continue; - if (InvalidatePossiblyObsoleteSlot(possible_causes, s, oldestLSN, dboid, - snapshotConflictHorizon, - &invalidated)) + /* Prevent invalidation of logical slots during binary upgrade */ + if (SlotIsLogical(s) && IsBinaryUpgrade) { - /* if the lock was released, start from scratch */ - goto restart; + SpinLockAcquire(&s->mutex); + found_valid_logicalslot |= (s->data.invalidated == RS_INVAL_NONE); + SpinLockRelease(&s->mutex); + + continue; + } + + if (InvalidatePossiblyObsoleteSlot(possible_causes, s, oldestLSN, + dboid, snapshotConflictHorizon, + &released_lock)) + { + Assert(released_lock); + + /* Remember we have invalidated a physical or logical slot */ + invalidated = true; + + /* + * Additionally, remember we have invalidated a logical slot as we + * can request disabling logical decoding later. + */ + if (SlotIsLogical(s)) + invalidated_logical = true; + } + else + { + /* + * We need to check if the slot is invalidated here since + * InvalidatePossiblyObsoleteSlot() returns false also if the slot + * is already invalidated. + */ + SpinLockAcquire(&s->mutex); + found_valid_logicalslot |= + (SlotIsLogical(s) && (s->data.invalidated == RS_INVAL_NONE)); + SpinLockRelease(&s->mutex); } + + /* if the lock was released, start from scratch */ + if (released_lock) + goto restart; } LWLockRelease(ReplicationSlotControlLock); @@ -2017,6 +2300,15 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, ReplicationSlotsComputeRequiredLSN(); } + /* + * Request the checkpointer to disable logical decoding if no valid + * logical slots remain. If called by the checkpointer during a + * checkpoint, only the request is initiated; actual deactivation is + * deferred until after the checkpoint completes. + */ + if (invalidated_logical && !found_valid_logicalslot) + RequestDisableLogicalDecoding(); + return invalidated; } @@ -2032,6 +2324,7 @@ void CheckPointReplicationSlots(bool is_shutdown) { int i; + bool last_saved_restart_lsn_updated = false; elog(DEBUG1, "performing replication slot checkpoint"); @@ -2041,10 +2334,16 @@ CheckPointReplicationSlots(bool is_shutdown) * acquiring a slot we cannot take the control lock - but that's OK, * because holding ReplicationSlotAllocationLock is strictly stronger, and * enough to guarantee that nobody can change the in_use bits on us. + * + * Additionally, acquiring the Allocation lock is necessary to serialize + * the slot flush process with concurrent slot WAL reservation. This + * ensures that the WAL position being reserved is either flushed to disk + * or is beyond or equal to the redo pointer of the current checkpoint + * (See ReplicationSlotReserveWal for details). */ LWLockAcquire(ReplicationSlotAllocationLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; char path[MAXPGPATH]; @@ -2076,9 +2375,23 @@ CheckPointReplicationSlots(bool is_shutdown) SpinLockRelease(&s->mutex); } + /* + * Track if we're going to update slot's last_saved_restart_lsn. We + * need this to know if we need to recompute the required LSN. + */ + if (s->last_saved_restart_lsn != s->data.restart_lsn) + last_saved_restart_lsn_updated = true; + SaveSlotToPath(s, path, LOG); } LWLockRelease(ReplicationSlotAllocationLock); + + /* + * Recompute the required LSN if SaveSlotToPath() updated + * last_saved_restart_lsn for any slot. + */ + if (last_saved_restart_lsn_updated) + ReplicationSlotsComputeRequiredLSN(); } /* @@ -2131,7 +2444,7 @@ StartupReplicationSlots(void) FreeDir(replication_dir); /* currently no slots exist, we're done. */ - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return; /* Now that we have recovered all the data, compute replication xmin */ @@ -2278,6 +2591,7 @@ SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel) pgstat_report_wait_end(); CloseTransientFile(fd); + unlink(tmppath); LWLockRelease(&slot->io_in_progress_lock); /* if write didn't set errno, assume problem is no disk space */ @@ -2298,7 +2612,9 @@ SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel) pgstat_report_wait_end(); CloseTransientFile(fd); + unlink(tmppath); LWLockRelease(&slot->io_in_progress_lock); + errno = save_errno; ereport(elevel, (errcode_for_file_access(), @@ -2312,7 +2628,9 @@ SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel) { int save_errno = errno; + unlink(tmppath); LWLockRelease(&slot->io_in_progress_lock); + errno = save_errno; ereport(elevel, (errcode_for_file_access(), @@ -2326,7 +2644,9 @@ SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel) { int save_errno = errno; + unlink(tmppath); LWLockRelease(&slot->io_in_progress_lock); + errno = save_errno; ereport(elevel, (errcode_for_file_access(), @@ -2354,6 +2674,7 @@ SaveSlotToPath(ReplicationSlot *slot, const char *dir, int elevel) if (!slot->just_dirtied) slot->dirty = false; slot->last_saved_confirmed_flush = cp.slotdata.confirmed_flush; + slot->last_saved_restart_lsn = cp.slotdata.restart_lsn; SpinLockRelease(&slot->mutex); LWLockRelease(&slot->io_in_progress_lock); @@ -2523,19 +2844,20 @@ RestoreSlotFromDisk(const char *name) */ if (cp.slotdata.database != InvalidOid) { - if (wal_level < WAL_LEVEL_LOGICAL) + if (wal_level < WAL_LEVEL_REPLICA) ereport(FATAL, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("logical replication slot \"%s\" exists, but \"wal_level\" < \"logical\"", + errmsg("logical replication slot \"%s\" exists, but \"wal_level\" < \"replica\"", NameStr(cp.slotdata.name)), - errhint("Change \"wal_level\" to be \"logical\" or higher."))); + errhint("Change \"wal_level\" to be \"replica\" or higher."))); /* * In standby mode, the hot standby must be enabled. This check is * necessary to ensure logical slots are invalidated when they become * incompatible due to insufficient wal_level. Otherwise, if the - * primary reduces wal_level < logical while hot standby is disabled, - * logical slots would remain valid even after promotion. + * primary reduces effective_wal_level < logical while hot standby is + * disabled, primary disable logical decoding while hot standby is + * disabled, logical slots would remain valid even after promotion. */ if (StandbyMode && !EnableHotStandby) ereport(FATAL, @@ -2551,7 +2873,13 @@ RestoreSlotFromDisk(const char *name) NameStr(cp.slotdata.name)), errhint("Change \"wal_level\" to be \"replica\" or higher."))); - /* nothing can be active yet, don't lock anything */ + /* + * Nothing can be active yet, don't lock anything. Note we iterate up to + * max_replication_slots instead of adding max_repack_replication_slots as + * in all other places, because we must enforce the GUC value in case + * there were more slots before the shutdown than what it is set up to + * now. + */ for (i = 0; i < max_replication_slots; i++) { ReplicationSlot *slot; @@ -2569,6 +2897,7 @@ RestoreSlotFromDisk(const char *name) slot->effective_xmin = cp.slotdata.xmin; slot->effective_catalog_xmin = cp.slotdata.catalog_xmin; slot->last_saved_confirmed_flush = cp.slotdata.confirmed_flush; + slot->last_saved_restart_lsn = cp.slotdata.restart_lsn; slot->candidate_catalog_xmin = InvalidTransactionId; slot->candidate_xmin_lsn = InvalidXLogRecPtr; @@ -2576,7 +2905,7 @@ RestoreSlotFromDisk(const char *name) slot->candidate_restart_valid = InvalidXLogRecPtr; slot->in_use = true; - slot->active_pid = 0; + slot->active_proc = INVALID_PROC_NUMBER; /* * Set the time since the slot has become inactive after loading the @@ -2620,7 +2949,7 @@ GetSlotInvalidationCause(const char *cause_name) } /* - * Maps an ReplicationSlotInvalidationCause to the invalidation + * Maps a ReplicationSlotInvalidationCause to the invalidation * reason for a replication slot. */ const char * @@ -2645,53 +2974,32 @@ GetSlotInvalidationCauseName(ReplicationSlotInvalidationCause cause) static bool validate_sync_standby_slots(char *rawname, List **elemlist) { - bool ok; - /* Verify syntax and parse string into a list of identifiers */ - ok = SplitIdentifierString(rawname, ',', elemlist); - - if (!ok) + if (!SplitIdentifierString(rawname, ',', elemlist)) { GUC_check_errdetail("List syntax is invalid."); + return false; } - else if (MyProc) + + /* Iterate the list to validate each slot name */ + foreach_ptr(char, name, *elemlist) { - /* - * Check that each specified slot exist and is physical. - * - * Because we need an LWLock, we cannot do this on processes without a - * PGPROC, so we skip it there; but see comments in - * StandbySlotsHaveCaughtup() as to why that's not a problem. - */ - LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); + int err_code; + char *err_msg = NULL; + char *err_hint = NULL; - foreach_ptr(char, name, *elemlist) + if (!ReplicationSlotValidateNameInternal(name, false, &err_code, + &err_msg, &err_hint)) { - ReplicationSlot *slot; - - slot = SearchNamedReplicationSlot(name, false); - - if (!slot) - { - GUC_check_errdetail("Replication slot \"%s\" does not exist.", - name); - ok = false; - break; - } - - if (!SlotIsPhysical(slot)) - { - GUC_check_errdetail("\"%s\" is not a physical replication slot.", - name); - ok = false; - break; - } + GUC_check_errcode(err_code); + GUC_check_errdetail("%s", err_msg); + if (err_hint != NULL) + GUC_check_errhint("%s", err_hint); + return false; } - - LWLockRelease(ReplicationSlotControlLock); } - return ok; + return true; } /* @@ -2826,7 +3134,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel) * Don't need to wait for the standbys to catch up if they are already * beyond the specified WAL location. */ - if (!XLogRecPtrIsInvalid(ss_oldest_flush_lsn) && + if (XLogRecPtrIsValid(ss_oldest_flush_lsn) && ss_oldest_flush_lsn >= wait_for_lsn) return true; @@ -2849,12 +3157,6 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel) /* * If a slot name provided in synchronized_standby_slots does not * exist, report a message and exit the loop. - * - * Though validate_sync_standby_slots (the GUC check_hook) tries to - * avoid this, it can nonetheless happen because the user can specify - * a nonexistent slot name before server startup. That function cannot - * validate such a slot during startup, as ReplicationSlotCtl is not - * initialized by then. Also, the user might have dropped one slot. */ if (!slot) { @@ -2886,7 +3188,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel) SpinLockAcquire(&slot->mutex); restart_lsn = slot->data.restart_lsn; invalidated = slot->data.invalidated != RS_INVAL_NONE; - inactive = slot->active_pid == 0; + inactive = slot->active_proc == INVALID_PROC_NUMBER; SpinLockRelease(&slot->mutex); if (invalidated) @@ -2903,7 +3205,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel) break; } - if (XLogRecPtrIsInvalid(restart_lsn) || restart_lsn < wait_for_lsn) + if (!XLogRecPtrIsValid(restart_lsn) || restart_lsn < wait_for_lsn) { /* Log a message if no active_pid for this physical slot */ if (inactive) @@ -2922,7 +3224,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel) Assert(restart_lsn >= wait_for_lsn); - if (XLogRecPtrIsInvalid(min_restart_lsn) || + if (!XLogRecPtrIsValid(min_restart_lsn) || min_restart_lsn > restart_lsn) min_restart_lsn = restart_lsn; @@ -2941,7 +3243,7 @@ StandbySlotsHaveCaughtup(XLogRecPtr wait_for_lsn, int elevel) return false; /* The ss_oldest_flush_lsn must not retreat. */ - Assert(XLogRecPtrIsInvalid(ss_oldest_flush_lsn) || + Assert(!XLogRecPtrIsValid(ss_oldest_flush_lsn) || min_restart_lsn >= ss_oldest_flush_lsn); ss_oldest_flush_lsn = min_restart_lsn; @@ -2993,22 +3295,3 @@ WaitForStandbyConfirmation(XLogRecPtr wait_for_lsn) ConditionVariableCancelSleep(); } - -/* - * GUC check_hook for idle_replication_slot_timeout - * - * The value of idle_replication_slot_timeout must be set to 0 during - * a binary upgrade. See start_postmaster() in pg_upgrade for more details. - */ -bool -check_idle_replication_slot_timeout(int *newval, void **extra, GucSource source) -{ - if (IsBinaryUpgrade && *newval != 0) - { - GUC_check_errdetail("\"%s\" must be set to 0 during binary upgrade mode.", - "idle_replication_slot_timeout"); - return false; - } - - return true; -} diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c index 36cc2ed4e440f..16fbd38373593 100644 --- a/src/backend/replication/slotfuncs.c +++ b/src/backend/replication/slotfuncs.c @@ -3,7 +3,7 @@ * slotfuncs.c * Support functions for replication slots * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/slotfuncs.c @@ -20,10 +20,22 @@ #include "replication/logical.h" #include "replication/slot.h" #include "replication/slotsync.h" +#include "storage/proc.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/pg_lsn.h" +/* + * Map SlotSyncSkipReason enum values to human-readable names. + */ +static const char *SlotSyncSkipReasonNames[] = { + [SS_SKIP_NONE] = "none", + [SS_SKIP_WAL_NOT_FLUSHED] = "wal_not_flushed", + [SS_SKIP_WAL_OR_ROWS_REMOVED] = "wal_or_rows_removed", + [SS_SKIP_NO_CONSISTENT_SNAPSHOT] = "no_consistent_snapshot", + [SS_SKIP_INVALID] = "slot_invalidated" +}; + /* * Helper function for creating a new physical replication slot with * given arguments. Note that this function doesn't release the created @@ -41,12 +53,12 @@ create_physical_replication_slot(char *name, bool immediately_reserve, /* acquire replication slot, this will check for conflicting names */ ReplicationSlotCreate(name, false, temporary ? RS_TEMPORARY : RS_PERSISTENT, false, - false, false); + false, false, false); if (immediately_reserve) { /* Reserve WAL as the user asked for it */ - if (XLogRecPtrIsInvalid(restart_lsn)) + if (!XLogRecPtrIsValid(restart_lsn)) ReplicationSlotReserveWal(); else MyReplicationSlot->data.restart_lsn = restart_lsn; @@ -78,7 +90,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckSlotRequirements(); + CheckSlotRequirements(false); create_physical_replication_slot(NameStr(*name), immediately_reserve, @@ -134,7 +146,14 @@ create_logical_replication_slot(char *name, char *plugin, */ ReplicationSlotCreate(name, true, temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase, - failover, false); + false, failover, false); + + /* + * Ensure the logical decoding is enabled before initializing the logical + * decoding context. + */ + EnsureLogicalDecodingEnabled(); + Assert(IsLogicalDecodingEnabled()); /* * Create logical decoding context to find start point or, if we don't @@ -145,6 +164,7 @@ create_logical_replication_slot(char *name, char *plugin, */ ctx = CreateInitDecodingContext(plugin, NIL, false, /* just catalogs is OK */ + false, /* not repack */ restart_lsn, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, @@ -184,7 +204,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); create_logical_replication_slot(NameStr(*name), NameStr(*plugin), @@ -221,7 +241,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckSlotRequirements(); + CheckSlotRequirements(false); ReplicationSlotDrop(NameStr(*name), true); @@ -235,7 +255,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS) Datum pg_get_replication_slots(PG_FUNCTION_ARGS) { -#define PG_GET_REPLICATION_SLOTS_COLS 20 +#define PG_GET_REPLICATION_SLOTS_COLS 21 ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; XLogRecPtr currlsn; int slotno; @@ -251,7 +271,7 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) currlsn = GetXLogWriteRecPtr(); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (slotno = 0; slotno < max_replication_slots; slotno++) + for (slotno = 0; slotno < max_replication_slots + max_repack_replication_slots; slotno++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno]; ReplicationSlot slot_contents; @@ -291,10 +311,10 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) values[i++] = ObjectIdGetDatum(slot_contents.data.database); values[i++] = BoolGetDatum(slot_contents.data.persistency == RS_TEMPORARY); - values[i++] = BoolGetDatum(slot_contents.active_pid != 0); + values[i++] = BoolGetDatum(slot_contents.active_proc != INVALID_PROC_NUMBER); - if (slot_contents.active_pid != 0) - values[i++] = Int32GetDatum(slot_contents.active_pid); + if (slot_contents.active_proc != INVALID_PROC_NUMBER) + values[i++] = Int32GetDatum(GetPGProcByNumber(slot_contents.active_proc)->pid); else nulls[i++] = true; @@ -308,12 +328,12 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) else nulls[i++] = true; - if (slot_contents.data.restart_lsn != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(slot_contents.data.restart_lsn)) values[i++] = LSNGetDatum(slot_contents.data.restart_lsn); else nulls[i++] = true; - if (slot_contents.data.confirmed_flush != InvalidXLogRecPtr) + if (XLogRecPtrIsValid(slot_contents.data.confirmed_flush)) values[i++] = LSNGetDatum(slot_contents.data.confirmed_flush); else nulls[i++] = true; @@ -357,15 +377,15 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) * * If we do change it, save the state for safe_wal_size below. */ - if (!XLogRecPtrIsInvalid(slot_contents.data.restart_lsn)) + if (XLogRecPtrIsValid(slot_contents.data.restart_lsn)) { - int pid; + ProcNumber procno; SpinLockAcquire(&slot->mutex); - pid = slot->active_pid; + procno = slot->active_proc; slot_contents.data.restart_lsn = slot->data.restart_lsn; SpinLockRelease(&slot->mutex); - if (pid != 0) + if (procno != INVALID_PROC_NUMBER) { values[i++] = CStringGetTextDatum("unreserved"); walstate = WALAVAIL_UNRESERVED; @@ -407,7 +427,7 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) values[i++] = BoolGetDatum(slot_contents.data.two_phase); if (slot_contents.data.two_phase && - !XLogRecPtrIsInvalid(slot_contents.data.two_phase_at)) + XLogRecPtrIsValid(slot_contents.data.two_phase_at)) values[i++] = LSNGetDatum(slot_contents.data.two_phase_at); else nulls[i++] = true; @@ -443,6 +463,11 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) values[i++] = BoolGetDatum(slot_contents.data.synced); + if (slot_contents.slotsync_skip_reason == SS_SKIP_NONE) + nulls[i++] = true; + else + values[i++] = CStringGetTextDatum(SlotSyncSkipReasonNames[slot_contents.slotsync_skip_reason]); + Assert(i == PG_GET_REPLICATION_SLOTS_COLS); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, @@ -467,7 +492,7 @@ pg_physical_replication_slot_advance(XLogRecPtr moveto) XLogRecPtr startlsn = MyReplicationSlot->data.restart_lsn; XLogRecPtr retlsn = startlsn; - Assert(moveto != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(moveto)); if (startlsn < moveto) { @@ -523,7 +548,7 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS) CheckSlotPermissions(); - if (XLogRecPtrIsInvalid(moveto)) + if (!XLogRecPtrIsValid(moveto)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid target WAL LSN"))); @@ -545,7 +570,7 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS) ReplicationSlotAcquire(NameStr(*slotname), true, true); /* A slot whose restart_lsn has never been reserved cannot be advanced */ - if (XLogRecPtrIsInvalid(MyReplicationSlot->data.restart_lsn)) + if (!XLogRecPtrIsValid(MyReplicationSlot->data.restart_lsn)) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("replication slot \"%s\" cannot be advanced", @@ -566,7 +591,7 @@ pg_replication_slot_advance(PG_FUNCTION_ARGS) if (moveto < minlsn) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot advance replication slot to %X/%X, minimum is %X/%X", + errmsg("cannot advance replication slot to %X/%08X, minimum is %X/%08X", LSN_FORMAT_ARGS(moveto), LSN_FORMAT_ARGS(minlsn)))); /* Do the actual slot update, depending on the slot type */ @@ -624,9 +649,9 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) CheckSlotPermissions(); if (logical_slot) - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); else - CheckSlotRequirements(); + CheckSlotRequirements(false); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); @@ -641,7 +666,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) * managed to create the new slot, we advance the new slot's restart_lsn * to the source slot's updated restart_lsn the second time we lock it. */ - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -679,7 +704,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) NameStr(*src_name)))); /* Copying non-reserved slot doesn't make sense */ - if (XLogRecPtrIsInvalid(src_restart_lsn)) + if (!XLogRecPtrIsValid(src_restart_lsn)) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("cannot copy a replication slot that doesn't reserve WAL"))); @@ -785,7 +810,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) errdetail("The source replication slot was modified incompatibly during the copy operation."))); /* The source slot must have a consistent snapshot */ - if (src_islogical && XLogRecPtrIsInvalid(copy_confirmed_flush)) + if (src_islogical && !XLogRecPtrIsValid(copy_confirmed_flush)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot copy unfinished logical replication slot \"%s\"", @@ -840,7 +865,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) /* All done. Set up the return values */ values[0] = NameGetDatum(dst_name); nulls[0] = false; - if (!XLogRecPtrIsInvalid(MyReplicationSlot->data.confirmed_flush)) + if (XLogRecPtrIsValid(MyReplicationSlot->data.confirmed_flush)) { values[1] = LSNGetDatum(MyReplicationSlot->data.confirmed_flush); nulls[1] = false; @@ -921,7 +946,6 @@ pg_sync_replication_slots(PG_FUNCTION_ARGS) /* Connect to the primary server. */ wrconn = walrcv_connect(PrimaryConnInfo, false, false, false, app_name.data, &err); - pfree(app_name.data); if (!wrconn) ereport(ERROR, @@ -929,6 +953,8 @@ pg_sync_replication_slots(PG_FUNCTION_ARGS) errmsg("synchronization worker \"%s\" could not connect to the primary server: %s", app_name.data, err)); + pfree(app_name.data); + SyncReplicationSlots(wrconn); walrcv_disconnect(wrconn); diff --git a/src/backend/replication/syncrep.c b/src/backend/replication/syncrep.c index cc35984ad0085..73450fe437e22 100644 --- a/src/backend/replication/syncrep.c +++ b/src/backend/replication/syncrep.c @@ -63,7 +63,7 @@ * the standbys which are considered as synchronous at that moment * will release waiters from the queue. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/syncrep.c @@ -85,6 +85,7 @@ #include "tcop/tcopprot.h" #include "utils/guc_hooks.h" #include "utils/ps_status.h" +#include "utils/wait_event.h" /* User-settable parameters for sync rep */ char *SyncRepStandbyNames; @@ -258,7 +259,7 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit) { char buffer[32]; - sprintf(buffer, "waiting for %X/%X", LSN_FORMAT_ARGS(lsn)); + sprintf(buffer, "waiting for %X/%08X", LSN_FORMAT_ARGS(lsn)); set_ps_display_suffix(buffer); } @@ -299,10 +300,18 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit) */ if (ProcDiePending) { - ereport(WARNING, - (errcode(ERRCODE_ADMIN_SHUTDOWN), - errmsg("canceling the wait for synchronous replication and terminating connection due to administrator command"), - errdetail("The transaction has already committed locally, but might not have been replicated to the standby."))); + if (ProcDieSenderPid != 0) + ereport(WARNING, + (errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("canceling the wait for synchronous replication and terminating connection due to administrator command"), + errdetail("The transaction has already committed locally, but might not have been replicated to the standby. Signal sent by PID %d, UID %d.", + (int) ProcDieSenderPid, + (int) ProcDieSenderUid))); + else + ereport(WARNING, + (errcode(ERRCODE_ADMIN_SHUTDOWN), + errmsg("canceling the wait for synchronous replication and terminating connection due to administrator command"), + errdetail("The transaction has already committed locally, but might not have been replicated to the standby."))); whereToSendOutput = DestNone; SyncRepCancelWait(); break; @@ -355,7 +364,7 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit) pg_read_barrier(); Assert(dlist_node_is_detached(&MyProc->syncRepLinks)); MyProc->syncRepState = SYNC_REP_NOT_WAITING; - MyProc->waitLSN = 0; + MyProc->waitLSN = InvalidXLogRecPtr; /* reset ps display to remove the suffix */ if (update_process_title) @@ -493,7 +502,7 @@ SyncRepReleaseWaiters(void) if (MyWalSnd->sync_standby_priority == 0 || (MyWalSnd->state != WALSNDSTATE_STREAMING && MyWalSnd->state != WALSNDSTATE_STOPPING) || - XLogRecPtrIsInvalid(MyWalSnd->flush)) + !XLogRecPtrIsValid(MyWalSnd->flush)) { announce_next_takeover = true; return; @@ -566,7 +575,7 @@ SyncRepReleaseWaiters(void) LWLockRelease(SyncRepLock); - elog(DEBUG3, "released %d procs up to write %X/%X, %d procs up to flush %X/%X, %d procs up to apply %X/%X", + elog(DEBUG3, "released %d procs up to write %X/%08X, %d procs up to flush %X/%08X, %d procs up to apply %X/%08X", numwrite, LSN_FORMAT_ARGS(writePtr), numflush, LSN_FORMAT_ARGS(flushPtr), numapply, LSN_FORMAT_ARGS(applyPtr)); @@ -676,11 +685,11 @@ SyncRepGetOldestSyncRecPtr(XLogRecPtr *writePtr, XLogRecPtr flush = sync_standbys[i].flush; XLogRecPtr apply = sync_standbys[i].apply; - if (XLogRecPtrIsInvalid(*writePtr) || *writePtr > write) + if (!XLogRecPtrIsValid(*writePtr) || *writePtr > write) *writePtr = write; - if (XLogRecPtrIsInvalid(*flushPtr) || *flushPtr > flush) + if (!XLogRecPtrIsValid(*flushPtr) || *flushPtr > flush) *flushPtr = flush; - if (XLogRecPtrIsInvalid(*applyPtr) || *applyPtr > apply) + if (!XLogRecPtrIsValid(*applyPtr) || *applyPtr > apply) *applyPtr = apply; } } @@ -705,9 +714,9 @@ SyncRepGetNthLatestSyncRecPtr(XLogRecPtr *writePtr, /* Should have enough candidates, or somebody messed up */ Assert(nth > 0 && nth <= num_standbys); - write_array = (XLogRecPtr *) palloc(sizeof(XLogRecPtr) * num_standbys); - flush_array = (XLogRecPtr *) palloc(sizeof(XLogRecPtr) * num_standbys); - apply_array = (XLogRecPtr *) palloc(sizeof(XLogRecPtr) * num_standbys); + write_array = palloc_array(XLogRecPtr, num_standbys); + flush_array = palloc_array(XLogRecPtr, num_standbys); + apply_array = palloc_array(XLogRecPtr, num_standbys); for (i = 0; i < num_standbys; i++) { @@ -757,8 +766,7 @@ SyncRepGetCandidateStandbys(SyncRepStandbyData **standbys) int n; /* Create result array */ - *standbys = (SyncRepStandbyData *) - palloc(max_wal_senders * sizeof(SyncRepStandbyData)); + *standbys = palloc_array(SyncRepStandbyData, max_wal_senders); /* Quick exit if sync replication is not requested */ if (SyncRepConfig == NULL) @@ -799,7 +807,7 @@ SyncRepGetCandidateStandbys(SyncRepStandbyData **standbys) continue; /* Must have a valid flush position */ - if (XLogRecPtrIsInvalid(stby->flush)) + if (!XLogRecPtrIsValid(stby->flush)) continue; /* OK, it's a candidate */ @@ -1028,7 +1036,7 @@ SyncRepQueueIsOrderedByLSN(int mode) Assert(mode >= 0 && mode < NUM_SYNC_REP_WAIT_MODE); - lastLSN = 0; + lastLSN = InvalidXLogRecPtr; dlist_foreach(iter, &WalSndCtl->SyncRepQueue[mode]) { @@ -1078,6 +1086,7 @@ check_synchronous_standby_names(char **newval, void **extra, GucSource source) if (syncrep_parse_error_msg) GUC_check_errdetail("%s", syncrep_parse_error_msg); else + /* translator: %s is a GUC name */ GUC_check_errdetail("\"%s\" parser failed.", "synchronous_standby_names"); return false; diff --git a/src/backend/replication/syncrep_gram.y b/src/backend/replication/syncrep_gram.y index 22297bb151a70..1b9d7b2edc4b5 100644 --- a/src/backend/replication/syncrep_gram.y +++ b/src/backend/replication/syncrep_gram.y @@ -3,7 +3,7 @@ * * syncrep_gram.y - Parser for synchronous_standby_names * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/replication/syncrep_scanner.l b/src/backend/replication/syncrep_scanner.l index 7dec1f869c745..475a7c4014e5a 100644 --- a/src/backend/replication/syncrep_scanner.l +++ b/src/backend/replication/syncrep_scanner.l @@ -4,7 +4,7 @@ * syncrep_scanner.l * a lexical scanner for synchronous_standby_names * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -157,17 +157,16 @@ syncrep_yyerror(SyncRepConfigData **syncrep_parse_result_p, char **syncrep_parse { struct yyguts_t *yyg = (struct yyguts_t *) yyscanner; /* needed for yytext * macro */ - char *syncrep_parse_error_msg = *syncrep_parse_error_msg_p; /* report only the first error in a parse operation */ - if (syncrep_parse_error_msg) + if (*syncrep_parse_error_msg_p) return; if (yytext[0]) - syncrep_parse_error_msg = psprintf("%s at or near \"%s\"", - message, yytext); + *syncrep_parse_error_msg_p = psprintf("%s at or near \"%s\"", + message, yytext); else - syncrep_parse_error_msg = psprintf("%s at end of input", - message); + *syncrep_parse_error_msg_p = psprintf("%s at end of input", + message); } void diff --git a/src/backend/replication/walreceiver.c b/src/backend/replication/walreceiver.c index 8c4d0fd9aed2b..8185412a8103e 100644 --- a/src/backend/replication/walreceiver.c +++ b/src/backend/replication/walreceiver.c @@ -30,16 +30,16 @@ * a new one. * * Normal termination is by SIGTERM, which instructs the walreceiver to - * exit(0). Emergency termination is by SIGQUIT; like any postmaster child - * process, the walreceiver will simply abort and exit on SIGQUIT. A close - * of the connection and a FATAL error are treated not as a crash but as + * ereport(FATAL). Emergency termination is by SIGQUIT; like any postmaster + * child process, the walreceiver will simply abort and exit on SIGQUIT. A + * close of the connection and a FATAL error are treated not as a crash but as * normal operation. * * This file contains the server-facing parts of walreceiver. The libpq- * specific parts are in the libpqwalreceiver module. It's loaded * dynamically to avoid linking the server with libpq. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -57,6 +57,7 @@ #include "access/xlog_internal.h" #include "access/xlogarchive.h" #include "access/xlogrecovery.h" +#include "access/xlogwait.h" #include "catalog/pg_authid.h" #include "funcapi.h" #include "libpq/pqformat.h" @@ -78,6 +79,7 @@ #include "utils/pg_lsn.h" #include "utils/ps_status.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* @@ -141,7 +143,7 @@ static void XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli); static void XLogWalRcvFlush(bool dying, TimeLineID tli); static void XLogWalRcvClose(XLogRecPtr recptr, TimeLineID tli); -static void XLogWalRcvSendReply(bool force, bool requestReply); +static void XLogWalRcvSendReply(bool force, bool requestReply, bool checkApply); static void XLogWalRcvSendHSFeedback(bool immed); static void ProcessWalSndrMessage(XLogRecPtr walEnd, TimestampTz sendTime); static void WalRcvComputeNextWakeup(WalRcvWakeupReason reason, TimestampTz now); @@ -168,7 +170,6 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) Assert(startup_data_len == 0); - MyBackendType = B_WAL_RECEIVER; AuxiliaryProcessMainCommon(); /* @@ -192,7 +193,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) case WALRCV_STOPPING: /* If we've already been requested to stop, don't start up. */ walrcv->walRcvState = WALRCV_STOPPED; - /* fall through */ + pg_fallthrough; case WALRCV_STOPPED: SpinLockRelease(&walrcv->mutex); @@ -204,6 +205,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) /* The usual case */ break; + case WALRCV_CONNECTING: case WALRCV_WAITING: case WALRCV_STREAMING: case WALRCV_RESTARTING: @@ -214,7 +216,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) } /* Advertise our PID so that the startup process can kill us */ walrcv->pid = MyProcPid; - walrcv->walRcvState = WALRCV_STREAMING; + walrcv->walRcvState = WALRCV_CONNECTING; /* Fetch information required to start streaming */ walrcv->ready_to_display = false; @@ -240,24 +242,22 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) SpinLockRelease(&walrcv->mutex); - pg_atomic_write_u64(&WalRcv->writtenUpto, 0); - /* Arrange to clean up at walreceiver exit */ on_shmem_exit(WalRcvDie, PointerGetDatum(&startpointTLI)); /* Properly accept or ignore signals the postmaster might send us */ pqsignal(SIGHUP, SignalHandlerForConfigReload); /* set flag to read config * file */ - pqsignal(SIGINT, SIG_IGN); + pqsignal(SIGINT, PG_SIG_IGN); pqsignal(SIGTERM, die); /* request shutdown */ /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); + pqsignal(SIGUSR2, PG_SIG_IGN); /* Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); /* Load the libpq-specific functions */ load_file("libpqwalreceiver", false); @@ -386,14 +386,25 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) { if (first_stream) ereport(LOG, - (errmsg("started streaming WAL from primary at %X/%X on timeline %u", - LSN_FORMAT_ARGS(startpoint), startpointTLI))); + errmsg("started streaming WAL from primary at %X/%08X on timeline %u", + LSN_FORMAT_ARGS(startpoint), startpointTLI)); else ereport(LOG, - (errmsg("restarted WAL streaming at %X/%X on timeline %u", - LSN_FORMAT_ARGS(startpoint), startpointTLI))); + errmsg("restarted WAL streaming at %X/%08X on timeline %u", + LSN_FORMAT_ARGS(startpoint), startpointTLI)); first_stream = false; + /* + * Switch to STREAMING after a successful connection if current + * state is CONNECTING. This switch happens after an initial + * startup, or after a restart as determined by + * WalRcvWaitForStartPosition(). + */ + SpinLockAcquire(&walrcv->mutex); + if (walrcv->walRcvState == WALRCV_CONNECTING) + walrcv->walRcvState = WALRCV_STREAMING; + SpinLockRelease(&walrcv->mutex); + /* Initialize LogstreamResult and buffers for processing messages */ LogstreamResult.Write = LogstreamResult.Flush = GetXLogReplayRecPtr(NULL); initStringInfo(&reply_message); @@ -404,7 +415,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) WalRcvComputeNextWakeup(i, now); /* Send initial reply/feedback messages. */ - XLogWalRcvSendReply(true, false); + XLogWalRcvSendReply(true, false, false); XLogWalRcvSendHSFeedback(true); /* Loop until end-of-streaming or error */ @@ -470,7 +481,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) { ereport(LOG, (errmsg("replication terminated by primary server"), - errdetail("End of WAL reached on timeline %u at %X/%X.", + errdetail("End of WAL reached on timeline %u at %X/%08X.", startpointTLI, LSN_FORMAT_ARGS(LogstreamResult.Write)))); endofwal = true; @@ -480,7 +491,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) } /* Let the primary know that we received some data. */ - XLogWalRcvSendReply(false, false); + XLogWalRcvSendReply(false, false, false); /* * If we've written some records, flush them to disk and @@ -526,7 +537,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) ResetLatch(MyLatch); CHECK_FOR_INTERRUPTS(); - if (walrcv->force_reply) + if (walrcv->apply_reply_requested) { /* * The recovery process has asked us to send apply @@ -534,9 +545,9 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) * false in shared memory before sending the reply, so * we don't miss a new request for a reply. */ - walrcv->force_reply = false; + walrcv->apply_reply_requested = false; pg_memory_barrier(); - XLogWalRcvSendReply(true, false); + XLogWalRcvSendReply(false, false, true); } } if (rc & WL_TIMEOUT) @@ -582,7 +593,7 @@ WalReceiverMain(const void *startup_data, size_t startup_data_len) wakeup[WALRCV_WAKEUP_PING] = TIMESTAMP_INFINITY; } - XLogWalRcvSendReply(requestReply, requestReply); + XLogWalRcvSendReply(requestReply, requestReply, false); XLogWalRcvSendHSFeedback(false); } } @@ -649,7 +660,7 @@ WalRcvWaitForStartPosition(XLogRecPtr *startpoint, TimeLineID *startpointTLI) SpinLockAcquire(&walrcv->mutex); state = walrcv->walRcvState; - if (state != WALRCV_STREAMING) + if (state != WALRCV_STREAMING && state != WALRCV_CONNECTING) { SpinLockRelease(&walrcv->mutex); if (state == WALRCV_STOPPING) @@ -688,7 +699,7 @@ WalRcvWaitForStartPosition(XLogRecPtr *startpoint, TimeLineID *startpointTLI) */ *startpoint = walrcv->receiveStart; *startpointTLI = walrcv->receiveStartTLI; - walrcv->walRcvState = WALRCV_STREAMING; + walrcv->walRcvState = WALRCV_CONNECTING; SpinLockRelease(&walrcv->mutex); break; } @@ -699,7 +710,7 @@ WalRcvWaitForStartPosition(XLogRecPtr *startpoint, TimeLineID *startpointTLI) * to die, but might as well check it here too. */ SpinLockRelease(&walrcv->mutex); - exit(1); + proc_exit(1); } SpinLockRelease(&walrcv->mutex); @@ -711,7 +722,7 @@ WalRcvWaitForStartPosition(XLogRecPtr *startpoint, TimeLineID *startpointTLI) { char activitymsg[50]; - snprintf(activitymsg, sizeof(activitymsg), "restarting at %X/%X", + snprintf(activitymsg, sizeof(activitymsg), "restarting at %X/%08X", LSN_FORMAT_ARGS(*startpoint)); set_ps_display(activitymsg); } @@ -791,6 +802,7 @@ WalRcvDie(int code, Datum arg) /* Mark ourselves inactive in shared memory */ SpinLockAcquire(&walrcv->mutex); Assert(walrcv->walRcvState == WALRCV_STREAMING || + walrcv->walRcvState == WALRCV_CONNECTING || walrcv->walRcvState == WALRCV_RESTARTING || walrcv->walRcvState == WALRCV_STARTING || walrcv->walRcvState == WALRCV_WAITING || @@ -826,7 +838,7 @@ XLogWalRcvProcessMsg(unsigned char type, char *buf, Size len, TimeLineID tli) switch (type) { - case 'w': /* WAL records */ + case PqReplMsg_WALData: { StringInfoData incoming_message; @@ -850,7 +862,7 @@ XLogWalRcvProcessMsg(unsigned char type, char *buf, Size len, TimeLineID tli) XLogWalRcvWrite(buf, len, dataStart, tli); break; } - case 'k': /* Keepalive */ + case PqReplMsg_Keepalive: { StringInfoData incoming_message; @@ -872,7 +884,7 @@ XLogWalRcvProcessMsg(unsigned char type, char *buf, Size len, TimeLineID tli) /* If the primary requested a reply, send one immediately */ if (replyRequested) - XLogWalRcvSendReply(true, false); + XLogWalRcvSendReply(true, false, false); break; } default: @@ -928,7 +940,7 @@ XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) start = pgstat_prepare_io_time(track_wal_io_timing); pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE); - byteswritten = pg_pwrite(recvFile, buf, segbytes, (off_t) startoff); + byteswritten = pg_pwrite(recvFile, buf, segbytes, (pgoff_t) startoff); pgstat_report_wait_end(); pgstat_count_io_op_time(IOOBJECT_WAL, IOCONTEXT_NORMAL, @@ -949,8 +961,8 @@ XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) ereport(PANIC, (errcode_for_file_access(), errmsg("could not write to WAL segment %s " - "at offset %d, length %lu: %m", - xlogfname, startoff, (unsigned long) segbytes))); + "at offset %d, length %d: %m", + xlogfname, startoff, segbytes))); } /* Update state for write */ @@ -965,6 +977,14 @@ XLogWalRcvWrite(char *buf, Size nbytes, XLogRecPtr recptr, TimeLineID tli) /* Update shared-memory status */ pg_atomic_write_u64(&WalRcv->writtenUpto, LogstreamResult.Write); + /* + * If we wrote an LSN that someone was waiting for, notify the waiters. + */ + if (waitLSNState && + (LogstreamResult.Write >= + pg_atomic_read_u64(&waitLSNState->minWaitedLSN[WAIT_LSN_TYPE_STANDBY_WRITE]))) + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_WRITE, LogstreamResult.Write); + /* * Close the current segment if it's fully written up in the last cycle of * the loop, to create its archive notification file soon. Otherwise WAL @@ -1004,6 +1024,15 @@ XLogWalRcvFlush(bool dying, TimeLineID tli) } SpinLockRelease(&walrcv->mutex); + /* + * If we flushed an LSN that someone was waiting for, notify the + * waiters. + */ + if (waitLSNState && + (LogstreamResult.Flush >= + pg_atomic_read_u64(&waitLSNState->minWaitedLSN[WAIT_LSN_TYPE_STANDBY_FLUSH]))) + WaitLSNWakeup(WAIT_LSN_TYPE_STANDBY_FLUSH, LogstreamResult.Flush); + /* Signal the startup process and walsender that new WAL has arrived */ WakeupRecovery(); if (AllowCascadeReplication()) @@ -1014,7 +1043,7 @@ XLogWalRcvFlush(bool dying, TimeLineID tli) { char activitymsg[50]; - snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%X", + snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%08X", LSN_FORMAT_ARGS(LogstreamResult.Write)); set_ps_display(activitymsg); } @@ -1022,7 +1051,7 @@ XLogWalRcvFlush(bool dying, TimeLineID tli) /* Also let the primary know that we made some progress */ if (!dying) { - XLogWalRcvSendReply(false, false); + XLogWalRcvSendReply(false, false, false); XLogWalRcvSendHSFeedback(false); } } @@ -1076,24 +1105,35 @@ XLogWalRcvClose(XLogRecPtr recptr, TimeLineID tli) } /* - * Send reply message to primary, indicating our current WAL locations, oldest - * xmin and the current time. + * Send reply message to primary, indicating our current WAL locations and + * time. + * + * The message is sent if 'force' is set, if enough time has passed since the + * last update to reach wal_receiver_status_interval, or if WAL locations have + * advanced since the previous status update. If wal_receiver_status_interval + * is disabled and 'force' is false, this function does nothing. Set 'force' to + * send the message unconditionally. * - * If 'force' is not set, the message is only sent if enough time has - * passed since last status update to reach wal_receiver_status_interval. - * If wal_receiver_status_interval is disabled altogether and 'force' is - * false, this is a no-op. + * Whether WAL locations are considered "advanced" depends on 'checkApply'. + * If 'checkApply' is false, only the write and flush locations are checked. + * This should be used when the call is triggered by write/flush activity + * (e.g., after walreceiver writes or flushes WAL), and avoids the + * apply-location check, which requires a spinlock. If 'checkApply' is true, + * the apply location is also considered. This should be used when the apply + * location is expected to advance (e.g., when the startup process requests + * an apply notification). * * If 'requestReply' is true, requests the server to reply immediately upon * receiving this message. This is used for heartbeats, when approaching * wal_receiver_timeout. */ static void -XLogWalRcvSendReply(bool force, bool requestReply) +XLogWalRcvSendReply(bool force, bool requestReply, bool checkApply) { - static XLogRecPtr writePtr = 0; - static XLogRecPtr flushPtr = 0; - XLogRecPtr applyPtr; + static XLogRecPtr writePtr = InvalidXLogRecPtr; + static XLogRecPtr flushPtr = InvalidXLogRecPtr; + static XLogRecPtr applyPtr = InvalidXLogRecPtr; + XLogRecPtr latestApplyPtr = InvalidXLogRecPtr; TimestampTz now; /* @@ -1109,17 +1149,19 @@ XLogWalRcvSendReply(bool force, bool requestReply) /* * We can compare the write and flush positions to the last message we * sent without taking any lock, but the apply position requires a spin - * lock, so we don't check that unless something else has changed or 10 - * seconds have passed. This means that the apply WAL location will - * appear, from the primary's point of view, to lag slightly, but since - * this is only for reporting purposes and only on idle systems, that's - * probably OK. + * lock, so we don't check that unless it is expected to advance since the + * previous update, i.e., when 'checkApply' is true. */ - if (!force - && writePtr == LogstreamResult.Write - && flushPtr == LogstreamResult.Flush - && now < wakeup[WALRCV_WAKEUP_REPLY]) - return; + if (!force && now < wakeup[WALRCV_WAKEUP_REPLY]) + { + if (checkApply) + latestApplyPtr = GetXLogReplayRecPtr(NULL); + + if (writePtr == LogstreamResult.Write + && flushPtr == LogstreamResult.Flush + && (!checkApply || applyPtr == latestApplyPtr)) + return; + } /* Make sure we wake up when it's time to send another reply. */ WalRcvComputeNextWakeup(WALRCV_WAKEUP_REPLY, now); @@ -1127,10 +1169,11 @@ XLogWalRcvSendReply(bool force, bool requestReply) /* Construct a new message */ writePtr = LogstreamResult.Write; flushPtr = LogstreamResult.Flush; - applyPtr = GetXLogReplayRecPtr(NULL); + applyPtr = XLogRecPtrIsValid(latestApplyPtr) ? + latestApplyPtr : GetXLogReplayRecPtr(NULL); resetStringInfo(&reply_message); - pq_sendbyte(&reply_message, 'r'); + pq_sendbyte(&reply_message, PqReplMsg_StandbyStatusUpdate); pq_sendint64(&reply_message, writePtr); pq_sendint64(&reply_message, flushPtr); pq_sendint64(&reply_message, applyPtr); @@ -1138,7 +1181,7 @@ XLogWalRcvSendReply(bool force, bool requestReply) pq_sendbyte(&reply_message, requestReply ? 1 : 0); /* Send it */ - elog(DEBUG2, "sending write %X/%X flush %X/%X apply %X/%X%s", + elog(DEBUG2, "sending write %X/%08X flush %X/%08X apply %X/%08X%s", LSN_FORMAT_ARGS(writePtr), LSN_FORMAT_ARGS(flushPtr), LSN_FORMAT_ARGS(applyPtr), @@ -1234,7 +1277,7 @@ XLogWalRcvSendHSFeedback(bool immed) /* Construct the message and send it. */ resetStringInfo(&reply_message); - pq_sendbyte(&reply_message, 'h'); + pq_sendbyte(&reply_message, PqReplMsg_HotStandbyFeedback); pq_sendint64(&reply_message, GetCurrentTimestamp()); pq_sendint32(&reply_message, xmin); pq_sendint32(&reply_message, xmin_epoch); @@ -1347,11 +1390,11 @@ WalRcvComputeNextWakeup(WalRcvWakeupReason reason, TimestampTz now) * synchronous_commit = remote_apply. */ void -WalRcvForceReply(void) +WalRcvRequestApplyReply(void) { ProcNumber procno; - WalRcv->force_reply = true; + WalRcv->apply_reply_requested = true; /* fetching the proc number is probably atomic, but don't rely on it */ SpinLockAcquire(&WalRcv->mutex); procno = WalRcv->procno; @@ -1373,6 +1416,8 @@ WalRcvGetStateString(WalRcvState state) return "stopped"; case WALRCV_STARTING: return "starting"; + case WALRCV_CONNECTING: + return "connecting"; case WALRCV_STREAMING: return "streaming"; case WALRCV_WAITING: @@ -1450,8 +1495,8 @@ pg_stat_get_wal_receiver(PG_FUNCTION_ARGS) if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); - values = palloc0(sizeof(Datum) * tupdesc->natts); - nulls = palloc0(sizeof(bool) * tupdesc->natts); + values = palloc0_array(Datum, tupdesc->natts); + nulls = palloc0_array(bool, tupdesc->natts); /* Fetch values */ values[0] = Int32GetDatum(pid); @@ -1469,16 +1514,16 @@ pg_stat_get_wal_receiver(PG_FUNCTION_ARGS) { values[1] = CStringGetTextDatum(WalRcvGetStateString(state)); - if (XLogRecPtrIsInvalid(receive_start_lsn)) + if (!XLogRecPtrIsValid(receive_start_lsn)) nulls[2] = true; else values[2] = LSNGetDatum(receive_start_lsn); values[3] = Int32GetDatum(receive_start_tli); - if (XLogRecPtrIsInvalid(written_lsn)) + if (!XLogRecPtrIsValid(written_lsn)) nulls[4] = true; else values[4] = LSNGetDatum(written_lsn); - if (XLogRecPtrIsInvalid(flushed_lsn)) + if (!XLogRecPtrIsValid(flushed_lsn)) nulls[5] = true; else values[5] = LSNGetDatum(flushed_lsn); @@ -1491,7 +1536,7 @@ pg_stat_get_wal_receiver(PG_FUNCTION_ARGS) nulls[8] = true; else values[8] = TimestampTzGetDatum(last_receipt_time); - if (XLogRecPtrIsInvalid(latest_end_lsn)) + if (!XLogRecPtrIsValid(latest_end_lsn)) nulls[9] = true; else values[9] = LSNGetDatum(latest_end_lsn); diff --git a/src/backend/replication/walreceiverfuncs.c b/src/backend/replication/walreceiverfuncs.c index 8de2886ff0b59..bd5d47be964cd 100644 --- a/src/backend/replication/walreceiverfuncs.c +++ b/src/backend/replication/walreceiverfuncs.c @@ -6,7 +6,7 @@ * with the walreceiver process. Functions implementing walreceiver itself * are in walreceiver.c. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -29,46 +29,46 @@ #include "storage/pmsignal.h" #include "storage/proc.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" WalRcvData *WalRcv = NULL; +static void WalRcvShmemRequest(void *arg); +static void WalRcvShmemInit(void *arg); + +const ShmemCallbacks WalRcvShmemCallbacks = { + .request_fn = WalRcvShmemRequest, + .init_fn = WalRcvShmemInit, +}; + /* * How long to wait for walreceiver to start up after requesting * postmaster to launch it. In seconds. */ #define WALRCV_STARTUP_TIMEOUT 10 -/* Report shared memory space needed by WalRcvShmemInit */ -Size -WalRcvShmemSize(void) +/* Register shared memory space needed by walreceiver */ +static void +WalRcvShmemRequest(void *arg) { - Size size = 0; - - size = add_size(size, sizeof(WalRcvData)); - - return size; + ShmemRequestStruct(.name = "Wal Receiver Ctl", + .size = sizeof(WalRcvData), + .ptr = (void **) &WalRcv, + ); } -/* Allocate and initialize walreceiver-related shared memory */ -void -WalRcvShmemInit(void) +/* Initialize walreceiver-related shared memory */ +static void +WalRcvShmemInit(void *arg) { - bool found; - - WalRcv = (WalRcvData *) - ShmemInitStruct("Wal Receiver Ctl", WalRcvShmemSize(), &found); - - if (!found) - { - /* First time through, so initialize */ - MemSet(WalRcv, 0, WalRcvShmemSize()); - WalRcv->walRcvState = WALRCV_STOPPED; - ConditionVariableInit(&WalRcv->walRcvStoppedCV); - SpinLockInit(&WalRcv->mutex); - pg_atomic_init_u64(&WalRcv->writtenUpto, 0); - WalRcv->procno = INVALID_PROC_NUMBER; - } + MemSet(WalRcv, 0, sizeof(WalRcvData)); + WalRcv->walRcvState = WALRCV_STOPPED; + ConditionVariableInit(&WalRcv->walRcvStoppedCV); + SpinLockInit(&WalRcv->mutex); + pg_atomic_init_u64(&WalRcv->writtenUpto, 0); + WalRcv->procno = INVALID_PROC_NUMBER; } /* Is walreceiver running (or starting up)? */ @@ -119,6 +119,20 @@ WalRcvRunning(void) return false; } +/* Return the state of the walreceiver. */ +WalRcvState +WalRcvGetState(void) +{ + WalRcvData *walrcv = WalRcv; + WalRcvState state; + + SpinLockAcquire(&walrcv->mutex); + state = walrcv->walRcvState; + SpinLockRelease(&walrcv->mutex); + + return state; +} + /* * Is walreceiver running and streaming (or at least attempting to connect, * or starting up)? @@ -165,7 +179,7 @@ WalRcvStreaming(void) } if (state == WALRCV_STREAMING || state == WALRCV_STARTING || - state == WALRCV_RESTARTING) + state == WALRCV_CONNECTING || state == WALRCV_RESTARTING) return true; else return false; @@ -197,11 +211,12 @@ ShutdownWalRcv(void) stopped = true; break; + case WALRCV_CONNECTING: case WALRCV_STREAMING: case WALRCV_WAITING: case WALRCV_RESTARTING: walrcv->walRcvState = WALRCV_STOPPING; - /* fall through */ + pg_fallthrough; case WALRCV_STOPPING: walrcvpid = walrcv->pid; break; @@ -301,11 +316,12 @@ RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr, const char *conninfo, * If this is the first startup of walreceiver (on this timeline), * initialize flushedUpto and latestChunkStart to the starting point. */ - if (walrcv->receiveStart == 0 || walrcv->receivedTLI != tli) + if (!XLogRecPtrIsValid(walrcv->receiveStart) || walrcv->receivedTLI != tli) { walrcv->flushedUpto = recptr; walrcv->receivedTLI = tli; walrcv->latestChunkStart = recptr; + pg_atomic_write_u64(&walrcv->writtenUpto, recptr); } walrcv->receiveStart = recptr; walrcv->receiveStartTLI = tli; diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index 9fa8beb6103d3..3d4ab929f9165 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -35,9 +35,11 @@ * checkpoint finishes, the postmaster sends us SIGUSR2. This instructs * walsender to send any outstanding WAL, including the shutdown checkpoint * record, wait for it to be replicated to the standby, and then exit. + * This waiting time can be limited by the wal_sender_shutdown_timeout + * parameter. * * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/replication/walsender.c @@ -51,6 +53,7 @@ #include "access/timeline.h" #include "access/transam.h" +#include "access/twophase.h" #include "access/xact.h" #include "access/xlog_internal.h" #include "access/xlogreader.h" @@ -60,11 +63,11 @@ #include "backup/basebackup_incremental.h" #include "catalog/pg_authid.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "commands/defrem.h" #include "funcapi.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" +#include "libpq/protocol.h" #include "miscadmin.h" #include "nodes/replnodes.h" #include "pgstat.h" @@ -84,17 +87,21 @@ #include "storage/ipc.h" #include "storage/pmsignal.h" #include "storage/proc.h" +#include "storage/procarray.h" +#include "storage/subsystems.h" #include "tcop/dest.h" #include "tcop/tcopprot.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_lsn.h" #include "utils/pgstat_internal.h" #include "utils/ps_status.h" #include "utils/timeout.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* Minimum interval used by walsender for stats flushes, in ms */ #define WALSENDER_STATS_FLUSH_INTERVAL 1000 @@ -113,6 +120,14 @@ /* Array of WalSnds in shared memory */ WalSndCtlData *WalSndCtl = NULL; +static void WalSndShmemRequest(void *arg); +static void WalSndShmemInit(void *arg); + +const ShmemCallbacks WalSndShmemCallbacks = { + .request_fn = WalSndShmemRequest, + .init_fn = WalSndShmemInit, +}; + /* My slot in the shared memory array */ WalSnd *MyWalSnd = NULL; @@ -127,6 +142,11 @@ int max_wal_senders = 10; /* the maximum number of concurrent * walsenders */ int wal_sender_timeout = 60 * 1000; /* maximum time to send one WAL * data message */ + +int wal_sender_shutdown_timeout = -1; /* maximum time to wait during + * shutdown for WAL + * replication */ + bool log_replication_commands = false; /* @@ -186,6 +206,9 @@ static TimestampTz last_reply_timestamp = 0; /* Have we sent a heartbeat message asking for reply, since last reply? */ static bool waiting_for_ping_response = false; +/* Timestamp when walsender received the shutdown request */ +static TimestampTz shutdown_request_timestamp = 0; + /* * While streaming WAL in Copy mode, streamingDoneSending is set to true * after we have sent CopyDone. We should not send any more CopyData messages @@ -230,6 +253,20 @@ typedef struct int write_head; int read_heads[NUM_SYNC_REP_WAIT_MODE]; WalTimeSample last_read[NUM_SYNC_REP_WAIT_MODE]; + + /* + * Overflow entries for read heads that collide with the write head. + * + * When the cyclic buffer fills (write head is about to collide with a + * read head), we save that read head's current sample here and mark it as + * using overflow (read_heads[i] = -1). This allows the write head to + * continue advancing while the overflowed mode continues lag computation + * using the saved sample. + * + * Once the standby's reported LSN advances past the overflow entry's LSN, + * we transition back to normal buffer-based tracking. + */ + WalTimeSample overflowed[NUM_SYNC_REP_WAIT_MODE]; } LagTracker; static LagTracker *lag_tracker; @@ -245,6 +282,7 @@ static void WalSndKill(int code, Datum arg); pg_noreturn static void WalSndShutdown(void); static void XLogSendPhysical(void); static void XLogSendLogical(void); +pg_noreturn static void WalSndDoneImmediate(void); static void WalSndDone(WalSndSendDataCallback send_data); static void IdentifySystem(void); static void UploadManifest(void); @@ -258,11 +296,13 @@ static void StartLogicalReplication(StartReplicationCmd *cmd); static void ProcessStandbyMessage(void); static void ProcessStandbyReplyMessage(void); static void ProcessStandbyHSFeedbackMessage(void); +static void ProcessStandbyPSRequestMessage(void); static void ProcessRepliesIfAny(void); static void ProcessPendingWrites(void); static void WalSndKeepalive(bool requestReply, XLogRecPtr writePtr); static void WalSndKeepaliveIfNecessary(void); static void WalSndCheckTimeOut(void); +static void WalSndCheckShutdownTimeout(void); static long WalSndComputeSleeptime(TimestampTz now); static void WalSndWait(uint32 socket_events, long timeout, uint32 wait_event); static void WalSndPrepareWrite(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId xid, bool last_write); @@ -373,7 +413,6 @@ WalSndShutdown(void) whereToSendOutput = DestNone; proc_exit(0); - abort(); /* keep the compiler quiet */ } /* @@ -408,7 +447,7 @@ IdentifySystem(void) else logptr = GetFlushRecPtr(&currTLI); - snprintf(xloc, sizeof(xloc), "%X/%X", LSN_FORMAT_ARGS(logptr)); + snprintf(xloc, sizeof(xloc), "%X/%08X", LSN_FORMAT_ARGS(logptr)); if (MyDatabaseId != InvalidOid) { @@ -434,6 +473,7 @@ IdentifySystem(void) TEXTOID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "dbname", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); /* prepare for projection of tuples */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); @@ -479,6 +519,7 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd) /* TimeLineID is unsigned, so int4 is not wide enough. */ TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "restart_tli", INT8OID, -1, 0); + TupleDescFinalize(tupdesc); memset(nulls, true, READ_REPLICATION_SLOT_COLS * sizeof(bool)); @@ -511,11 +552,11 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd) i++; /* start LSN */ - if (!XLogRecPtrIsInvalid(slot_contents.data.restart_lsn)) + if (XLogRecPtrIsValid(slot_contents.data.restart_lsn)) { char xloc[64]; - snprintf(xloc, sizeof(xloc), "%X/%X", + snprintf(xloc, sizeof(xloc), "%X/%08X", LSN_FORMAT_ARGS(slot_contents.data.restart_lsn)); values[i] = CStringGetTextDatum(xloc); nulls[i] = false; @@ -523,7 +564,7 @@ ReadReplicationSlot(ReadReplicationSlotCmd *cmd) i++; /* timeline this WAL was produced on */ - if (!XLogRecPtrIsInvalid(slot_contents.data.restart_lsn)) + if (XLogRecPtrIsValid(slot_contents.data.restart_lsn)) { TimeLineID slots_position_timeline; TimeLineID current_timeline; @@ -581,6 +622,7 @@ SendTimeLineHistory(TimeLineHistoryCmd *cmd) tupdesc = CreateTemplateTupleDesc(2); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, "filename", TEXTOID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "content", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); TLHistoryFileName(histfname, cmd->timeline); TLHistoryFilePath(path, cmd->timeline); @@ -733,13 +775,13 @@ HandleUploadManifestPacket(StringInfo buf, off_t *offset, switch (mtype) { - case 'd': /* CopyData */ + case PqMsg_CopyData: maxmsglen = PQ_LARGE_MESSAGE_LIMIT; break; - case 'c': /* CopyDone */ - case 'f': /* CopyFail */ - case 'H': /* Flush */ - case 'S': /* Sync */ + case PqMsg_CopyDone: + case PqMsg_CopyFail: + case PqMsg_Flush: + case PqMsg_Sync: maxmsglen = PQ_SMALL_MESSAGE_LIMIT; break; default: @@ -761,19 +803,19 @@ HandleUploadManifestPacket(StringInfo buf, off_t *offset, /* Process the message */ switch (mtype) { - case 'd': /* CopyData */ + case PqMsg_CopyData: AppendIncrementalManifestData(ib, buf->data, buf->len); return true; - case 'c': /* CopyDone */ + case PqMsg_CopyDone: return false; - case 'H': /* Sync */ - case 'S': /* Flush */ + case PqMsg_Sync: + case PqMsg_Flush: /* Ignore these while in CopyOut mode as we do elsewhere. */ return true; - case 'f': + case PqMsg_CopyFail: ereport(ERROR, (errcode(ERRCODE_QUERY_CANCELED), errmsg("COPY from stdin failed: %s", @@ -888,16 +930,16 @@ StartReplication(StartReplicationCmd *cmd) * that's older than the switchpoint, if it's still in the same * WAL segment. */ - if (!XLogRecPtrIsInvalid(switchpoint) && + if (XLogRecPtrIsValid(switchpoint) && switchpoint < cmd->startpoint) { ereport(ERROR, - (errmsg("requested starting point %X/%X on timeline %u is not in this server's history", - LSN_FORMAT_ARGS(cmd->startpoint), - cmd->timeline), - errdetail("This server's history forked from timeline %u at %X/%X.", - cmd->timeline, - LSN_FORMAT_ARGS(switchpoint)))); + errmsg("requested starting point %X/%08X on timeline %u is not in this server's history", + LSN_FORMAT_ARGS(cmd->startpoint), + cmd->timeline), + errdetail("This server's history forked from timeline %u at %X/%08X.", + cmd->timeline, + LSN_FORMAT_ARGS(switchpoint))); } sendTimeLineValidUpto = switchpoint; } @@ -939,9 +981,9 @@ StartReplication(StartReplicationCmd *cmd) if (FlushPtr < cmd->startpoint) { ereport(ERROR, - (errmsg("requested starting point %X/%X is ahead of the WAL flush position of this server %X/%X", - LSN_FORMAT_ARGS(cmd->startpoint), - LSN_FORMAT_ARGS(FlushPtr)))); + errmsg("requested starting point %X/%08X is ahead of the WAL flush position of this server %X/%08X", + LSN_FORMAT_ARGS(cmd->startpoint), + LSN_FORMAT_ARGS(FlushPtr))); } /* Start streaming from the requested point */ @@ -983,7 +1025,7 @@ StartReplication(StartReplicationCmd *cmd) Datum values[2]; bool nulls[2] = {0}; - snprintf(startpos_str, sizeof(startpos_str), "%X/%X", + snprintf(startpos_str, sizeof(startpos_str), "%X/%08X", LSN_FORMAT_ARGS(sendTimeLineValidUpto)); dest = CreateDestReceiver(DestRemoteSimple); @@ -998,6 +1040,7 @@ StartReplication(StartReplicationCmd *cmd) INT8OID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 2, "next_tli_startpos", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); /* prepare for projection of tuple */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); @@ -1134,8 +1177,8 @@ parseCreateReplSlotOptions(CreateReplicationSlotCmd *cmd, else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized value for CREATE_REPLICATION_SLOT option \"%s\": \"%s\"", - defel->defname, action))); + errmsg("unrecognized value for %s option \"%s\": \"%s\"", + "CREATE_REPLICATION_SLOT", defel->defname, action))); } else if (strcmp(defel->defname, "reserve_wal") == 0) { @@ -1198,7 +1241,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) { ReplicationSlotCreate(cmd->slotname, false, cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT, - false, false, false); + false, false, false, false); if (reserve_wal) { @@ -1218,7 +1261,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) Assert(cmd->kind == REPLICATION_KIND_LOGICAL); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); /* * Initially create persistent slot as ephemeral - that allows us to @@ -1229,7 +1272,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) */ ReplicationSlotCreate(cmd->slotname, true, cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL, - two_phase, failover, false); + two_phase, false, failover, false); /* * Do options check early so that we can bail before calling the @@ -1279,7 +1322,15 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) need_full_snapshot = true; } + /* + * Ensure the logical decoding is enabled before initializing the + * logical decoding context. + */ + EnsureLogicalDecodingEnabled(); + Assert(IsLogicalDecodingEnabled()); + ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot, + false, InvalidXLogRecPtr, XL_ROUTINE(.page_read = logical_read_xlog_page, .segment_open = WalSndSegmentOpen, @@ -1324,7 +1375,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) ReplicationSlotPersist(); } - snprintf(xloc, sizeof(xloc), "%X/%X", + snprintf(xloc, sizeof(xloc), "%X/%08X", LSN_FORMAT_ARGS(MyReplicationSlot->data.confirmed_flush)); dest = CreateDestReceiver(DestRemoteSimple); @@ -1345,6 +1396,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) TEXTOID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 4, "output_plugin", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); /* prepare for projection of tuples */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); @@ -1436,7 +1488,7 @@ StartLogicalReplication(StartReplicationCmd *cmd) QueryCompletion qc; /* make sure that our requirements are still fulfilled */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); Assert(!MyReplicationSlot); @@ -1531,7 +1583,7 @@ WalSndPrepareWrite(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId xi resetStringInfo(ctx->out); - pq_sendbyte(ctx->out, 'w'); + pq_sendbyte(ctx->out, PqReplMsg_WALData); pq_sendint64(ctx->out, lsn); /* dataStart */ pq_sendint64(ctx->out, lsn); /* walEnd */ @@ -1567,7 +1619,7 @@ WalSndWriteData(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId xid, tmpbuf.data, sizeof(int64)); /* output previously gathered data in a CopyData packet */ - pq_putmessage_noblock('d', ctx->out->data, ctx->out->len); + pq_putmessage_noblock(PqMsg_CopyData, ctx->out->data, ctx->out->len); CHECK_FOR_INTERRUPTS(); @@ -1587,6 +1639,32 @@ WalSndWriteData(LogicalDecodingContext *ctx, XLogRecPtr lsn, TransactionId xid, ProcessPendingWrites(); } +/* + * Handle configuration reload. + * + * Process the pending configuration file reload and reinitializes synchronous + * replication settings. Also releases any waiters that may now be satisfied due + * to changes in synchronous replication requirements. + */ +static void +WalSndHandleConfigReload(void) +{ + if (!ConfigReloadPending) + return; + + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + SyncRepInitConfig(); + + /* + * Recheck and release any now-satisfied waiters after config reload + * changes synchronous replication requirements (e.g., reducing the number + * of sync standbys or changing the standby names). + */ + if (!am_cascading_walsender) + SyncRepReleaseWaiters(); +} + /* * Wait until there is no pending write. Also process replies from the other * side and check timeouts during that. @@ -1604,6 +1682,13 @@ ProcessPendingWrites(void) /* die if timeout was reached */ WalSndCheckTimeOut(); + /* + * During shutdown, die if the shutdown timeout expires. Call this + * before WalSndComputeSleeptime() so the timeout is considered when + * computing sleep time. + */ + WalSndCheckShutdownTimeout(); + /* Send keepalive if the time has come */ WalSndKeepaliveIfNecessary(); @@ -1622,12 +1707,7 @@ ProcessPendingWrites(void) CHECK_FOR_INTERRUPTS(); /* Process any requests or signals received recently */ - if (ConfigReloadPending) - { - ConfigReloadPending = false; - ProcessConfigFile(PGC_SIGHUP); - SyncRepInitConfig(); - } + WalSndHandleConfigReload(); /* Try to flush pending output to the client */ if (pq_flush_if_writable() != 0) @@ -1809,7 +1889,7 @@ WalSndWaitForWal(XLogRecPtr loc) * receipt of WAL up to RecentFlushPtr. This is particularly interesting * if we're far behind. */ - if (!XLogRecPtrIsInvalid(RecentFlushPtr) && + if (XLogRecPtrIsValid(RecentFlushPtr) && !NeedToWaitForWal(loc, RecentFlushPtr, &wait_event)) return RecentFlushPtr; @@ -1830,12 +1910,7 @@ WalSndWaitForWal(XLogRecPtr loc) CHECK_FOR_INTERRUPTS(); /* Process any requests or signals received recently */ - if (ConfigReloadPending) - { - ConfigReloadPending = false; - ProcessConfigFile(PGC_SIGHUP); - SyncRepInitConfig(); - } + WalSndHandleConfigReload(); /* Check for input from the client */ ProcessRepliesIfAny(); @@ -1844,9 +1919,15 @@ WalSndWaitForWal(XLogRecPtr loc) * If we're shutting down, trigger pending WAL to be written out, * otherwise we'd possibly end up waiting for WAL that never gets * written, because walwriter has shut down already. + * + * Note that GetXLogInsertEndRecPtr() is used to obtain the WAL flush + * request location instead of GetXLogInsertRecPtr(). Because if the + * last WAL record ends at a page boundary, GetXLogInsertRecPtr() can + * return an LSN pointing past the page header, which may cause + * XLogFlush() to report an error. */ - if (got_STOPPING) - XLogBackgroundFlush(); + if (got_STOPPING && !RecoveryInProgress()) + XLogFlush(GetXLogInsertEndRecPtr()); /* * To avoid the scenario where standbys need to catch up to a newer @@ -1923,6 +2004,13 @@ WalSndWaitForWal(XLogRecPtr loc) /* die if timeout was reached */ WalSndCheckTimeOut(); + /* + * During shutdown, die if the shutdown timeout expires. Call this + * before WalSndComputeSleeptime() so the timeout is considered when + * computing sleep time. + */ + WalSndCheckShutdownTimeout(); + /* Send keepalive if the time has come */ WalSndKeepaliveIfNecessary(); @@ -2289,7 +2377,8 @@ ProcessRepliesIfAny(void) switch (firstchar) { /* - * 'd' means a standby reply wrapped in a CopyData packet. + * PqMsg_CopyData means a standby reply wrapped in a CopyData + * packet. */ case PqMsg_CopyData: ProcessStandbyMessage(); @@ -2297,13 +2386,14 @@ ProcessRepliesIfAny(void) break; /* - * CopyDone means the standby requested to finish streaming. - * Reply with CopyDone, if we had not sent that already. + * PqMsg_CopyDone means the standby requested to finish + * streaming. Reply with CopyDone, if we had not sent that + * already. */ case PqMsg_CopyDone: if (!streamingDoneSending) { - pq_putmessage_noblock('c', NULL, 0); + pq_putmessage_noblock(PqMsg_CopyDone, NULL, 0); streamingDoneSending = true; } @@ -2312,7 +2402,8 @@ ProcessRepliesIfAny(void) break; /* - * 'X' means that the standby is closing down the socket. + * PqMsg_Terminate means that the standby is closing down the + * socket. */ case PqMsg_Terminate: proc_exit(0); @@ -2347,14 +2438,18 @@ ProcessStandbyMessage(void) switch (msgtype) { - case 'r': + case PqReplMsg_StandbyStatusUpdate: ProcessStandbyReplyMessage(); break; - case 'h': + case PqReplMsg_HotStandbyFeedback: ProcessStandbyHSFeedbackMessage(); break; + case PqReplMsg_PrimaryStatusRequest: + ProcessStandbyPSRequestMessage(); + break; + default: ereport(COMMERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), @@ -2372,7 +2467,7 @@ PhysicalConfirmReceivedLocation(XLogRecPtr lsn) bool changed = false; ReplicationSlot *slot = MyReplicationSlot; - Assert(lsn != InvalidXLogRecPtr); + Assert(XLogRecPtrIsValid(lsn)); SpinLockAcquire(&slot->mutex); if (slot->data.restart_lsn != lsn) { @@ -2413,7 +2508,9 @@ ProcessStandbyReplyMessage(void) TimestampTz now; TimestampTz replyTime; - static bool fullyAppliedLastTime = false; + static XLogRecPtr prevWritePtr = InvalidXLogRecPtr; + static XLogRecPtr prevFlushPtr = InvalidXLogRecPtr; + static XLogRecPtr prevApplyPtr = InvalidXLogRecPtr; /* the caller already consumed the msgtype byte */ writePtr = pq_getmsgint64(&reply_message); @@ -2429,7 +2526,7 @@ ProcessStandbyReplyMessage(void) /* Copy because timestamptz_to_str returns a static buffer */ replyTimeStr = pstrdup(timestamptz_to_str(replyTime)); - elog(DEBUG2, "write %X/%X flush %X/%X apply %X/%X%s reply_time %s", + elog(DEBUG2, "write %X/%08X flush %X/%08X apply %X/%08X%s reply_time %s", LSN_FORMAT_ARGS(writePtr), LSN_FORMAT_ARGS(flushPtr), LSN_FORMAT_ARGS(applyPtr), @@ -2446,22 +2543,23 @@ ProcessStandbyReplyMessage(void) applyLag = LagTrackerRead(SYNC_REP_WAIT_APPLY, applyPtr, now); /* - * If the standby reports that it has fully replayed the WAL in two - * consecutive reply messages, then the second such message must result - * from wal_receiver_status_interval expiring on the standby. This is a - * convenient time to forget the lag times measured when it last - * wrote/flushed/applied a WAL record, to avoid displaying stale lag data - * until more WAL traffic arrives. + * If the standby reports that it has fully replayed the WAL, and the + * write/flush/apply positions remain unchanged across two consecutive + * reply messages, forget the lag times measured when it last + * wrote/flushed/applied a WAL record. + * + * The second message with unchanged positions typically results from + * wal_receiver_status_interval expiring on the standby, so lag values are + * usually cleared after that interval when there is no activity. This + * avoids displaying stale lag data until more WAL traffic arrives. */ - clearLagTimes = false; - if (applyPtr == sentPtr) - { - if (fullyAppliedLastTime) - clearLagTimes = true; - fullyAppliedLastTime = true; - } - else - fullyAppliedLastTime = false; + clearLagTimes = (applyPtr == sentPtr && flushPtr == sentPtr && + writePtr == prevWritePtr && flushPtr == prevFlushPtr && + applyPtr == prevApplyPtr); + + prevWritePtr = writePtr; + prevFlushPtr = flushPtr; + prevApplyPtr = applyPtr; /* Send a reply if the standby requested one. */ if (replyRequested) @@ -2494,7 +2592,7 @@ ProcessStandbyReplyMessage(void) /* * Advance our local xmin horizon when the client confirmed a flush. */ - if (MyReplicationSlot && flushPtr != InvalidXLogRecPtr) + if (MyReplicationSlot && XLogRecPtrIsValid(flushPtr)) { if (SlotIsLogical(MyReplicationSlot)) LogicalConfirmReceivedLocation(flushPtr); @@ -2701,22 +2799,89 @@ ProcessStandbyHSFeedbackMessage(void) } } +/* + * Process the request for a primary status update message. + */ +static void +ProcessStandbyPSRequestMessage(void) +{ + XLogRecPtr lsn = InvalidXLogRecPtr; + TransactionId oldestXidInCommit; + TransactionId oldestGXidInCommit; + FullTransactionId nextFullXid; + FullTransactionId fullOldestXidInCommit; + WalSnd *walsnd = MyWalSnd; + TimestampTz replyTime; + + /* + * This shouldn't happen because we don't support getting primary status + * message from standby. + */ + if (RecoveryInProgress()) + elog(ERROR, "the primary status is unavailable during recovery"); + + replyTime = pq_getmsgint64(&reply_message); + + /* + * Update shared state for this WalSender process based on reply data from + * standby. + */ + SpinLockAcquire(&walsnd->mutex); + walsnd->replyTime = replyTime; + SpinLockRelease(&walsnd->mutex); + + /* + * Consider transactions in the current database, as only these are the + * ones replicated. + */ + oldestXidInCommit = GetOldestActiveTransactionId(true, false); + oldestGXidInCommit = TwoPhaseGetOldestXidInCommit(); + + /* + * Update the oldest xid for standby transmission if an older prepared + * transaction exists and is currently in commit phase. + */ + if (TransactionIdIsValid(oldestGXidInCommit) && + TransactionIdPrecedes(oldestGXidInCommit, oldestXidInCommit)) + oldestXidInCommit = oldestGXidInCommit; + + nextFullXid = ReadNextFullTransactionId(); + fullOldestXidInCommit = FullTransactionIdFromAllowableAt(nextFullXid, + oldestXidInCommit); + lsn = GetXLogWriteRecPtr(); + + elog(DEBUG2, "sending primary status"); + + /* construct the message... */ + resetStringInfo(&output_message); + pq_sendbyte(&output_message, PqReplMsg_PrimaryStatusUpdate); + pq_sendint64(&output_message, lsn); + pq_sendint64(&output_message, (int64) U64FromFullTransactionId(fullOldestXidInCommit)); + pq_sendint64(&output_message, (int64) U64FromFullTransactionId(nextFullXid)); + pq_sendint64(&output_message, GetCurrentTimestamp()); + + /* ... and send it wrapped in CopyData */ + pq_putmessage_noblock(PqMsg_CopyData, output_message.data, output_message.len); +} + /* * Compute how long send/receive loops should sleep. * * If wal_sender_timeout is enabled we want to wake up in time to send * keepalives and to abort the connection if wal_sender_timeout has been * reached. + * + * If wal_sender_shutdown_timeout is enabled, during shutdown, we want to + * wake up in time to exit when it expires. */ static long WalSndComputeSleeptime(TimestampTz now) { + TimestampTz wakeup_time; long sleeptime = 10000; /* 10 s */ if (wal_sender_timeout > 0 && last_reply_timestamp > 0) { - TimestampTz wakeup_time; - /* * At the latest stop sleeping once wal_sender_timeout has been * reached. @@ -2737,6 +2902,20 @@ WalSndComputeSleeptime(TimestampTz now) sleeptime = TimestampDifferenceMilliseconds(now, wakeup_time); } + if (shutdown_request_timestamp != 0 && wal_sender_shutdown_timeout > 0) + { + long shutdown_sleeptime; + + wakeup_time = TimestampTzPlusMilliseconds(shutdown_request_timestamp, + wal_sender_shutdown_timeout); + + shutdown_sleeptime = TimestampDifferenceMilliseconds(now, wakeup_time); + + /* Choose the earliest wakeup. */ + if (shutdown_sleeptime < sleeptime) + sleeptime = shutdown_sleeptime; + } + return sleeptime; } @@ -2778,6 +2957,45 @@ WalSndCheckTimeOut(void) } } +/* + * Check whether the walsender process should terminate due to the expiration + * of wal_sender_shutdown_timeout after the receipt of a shutdown request. + */ +static void +WalSndCheckShutdownTimeout(void) +{ + TimestampTz now; + + /* Do nothing if shutdown has not been requested yet */ + if (!(got_STOPPING || got_SIGUSR2)) + return; + + /* Terminate immediately if the timeout is set to 0 */ + if (wal_sender_shutdown_timeout == 0) + WalSndDoneImmediate(); + + /* + * Record the shutdown request timestamp even if + * wal_sender_shutdown_timeout is disabled (-1), since the setting may + * change during shutdown and the timestamp will be needed in that case. + */ + if (shutdown_request_timestamp == 0) + { + shutdown_request_timestamp = GetCurrentTimestamp(); + return; + } + + /* Do not check the timeout if it's disabled */ + if (wal_sender_shutdown_timeout == -1) + return; + + /* Terminate immediately if the timeout expires */ + now = GetCurrentTimestamp(); + if (TimestampDifferenceExceeds(shutdown_request_timestamp, now, + wal_sender_shutdown_timeout)) + WalSndDoneImmediate(); +} + /* Main loop of walsender process that streams the WAL over Copy messages. */ static void WalSndLoop(WalSndSendDataCallback send_data) @@ -2803,12 +3021,7 @@ WalSndLoop(WalSndSendDataCallback send_data) CHECK_FOR_INTERRUPTS(); /* Process any requests or signals received recently */ - if (ConfigReloadPending) - { - ConfigReloadPending = false; - ProcessConfigFile(PGC_SIGHUP); - SyncRepInitConfig(); - } + WalSndHandleConfigReload(); /* Check for input from the client */ ProcessRepliesIfAny(); @@ -2870,6 +3083,13 @@ WalSndLoop(WalSndSendDataCallback send_data) /* Check for replication timeout. */ WalSndCheckTimeOut(); + /* + * During shutdown, die if the shutdown timeout expires. Call this + * before WalSndComputeSleeptime() so the timeout is considered when + * computing sleep time. + */ + WalSndCheckShutdownTimeout(); + /* Send keepalive if the time has come */ WalSndKeepaliveIfNecessary(); @@ -2984,7 +3204,7 @@ InitWalSenderSlot(void) SpinLockRelease(&walsnd->mutex); /* don't need the lock anymore */ - MyWalSnd = (WalSnd *) walsnd; + MyWalSnd = walsnd; break; } @@ -3246,12 +3466,12 @@ XLogSendPhysical(void) wal_segment_close(xlogreader); /* Send CopyDone */ - pq_putmessage_noblock('c', NULL, 0); + pq_putmessage_noblock(PqMsg_CopyDone, NULL, 0); streamingDoneSending = true; WalSndCaughtUp = true; - elog(DEBUG1, "walsender reached end of timeline at %X/%X (sent up to %X/%X)", + elog(DEBUG1, "walsender reached end of timeline at %X/%08X (sent up to %X/%08X)", LSN_FORMAT_ARGS(sendTimeLineValidUpto), LSN_FORMAT_ARGS(sentPtr)); return; @@ -3303,7 +3523,7 @@ XLogSendPhysical(void) * OK to read and send the slice. */ resetStringInfo(&output_message); - pq_sendbyte(&output_message, 'w'); + pq_sendbyte(&output_message, PqReplMsg_WALData); pq_sendint64(&output_message, startptr); /* dataStart */ pq_sendint64(&output_message, SendRqstPtr); /* walEnd */ @@ -3374,7 +3594,7 @@ XLogSendPhysical(void) memcpy(&output_message.data[1 + sizeof(int64) + sizeof(int64)], tmpbuf.data, sizeof(int64)); - pq_putmessage_noblock('d', output_message.data, output_message.len); + pq_putmessage_noblock(PqMsg_CopyData, output_message.data, output_message.len); sentPtr = endptr; @@ -3392,7 +3612,7 @@ XLogSendPhysical(void) { char activitymsg[50]; - snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%X", + snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%08X", LSN_FORMAT_ARGS(sentPtr)); set_ps_display(activitymsg); } @@ -3446,11 +3666,19 @@ XLogSendLogical(void) * If first time through in this session, initialize flushPtr. Otherwise, * we only need to update flushPtr if EndRecPtr is past it. */ - if (flushPtr == InvalidXLogRecPtr || + if (!XLogRecPtrIsValid(flushPtr) || logical_decoding_ctx->reader->EndRecPtr >= flushPtr) { + /* + * For cascading logical WAL senders, we use the replay LSN instead of + * the flush LSN, since logical decoding on a standby only processes + * WAL that has been replayed. This distinction becomes particularly + * important during shutdown, as new WAL is no longer replayed and the + * last replayed LSN marks the furthest point up to which decoding can + * proceed. + */ if (am_cascading_walsender) - flushPtr = GetStandbyFlushRecPtr(NULL); + flushPtr = GetXLogReplayRecPtr(NULL); else flushPtr = GetFlushRecPtr(NULL); } @@ -3477,6 +3705,49 @@ XLogSendLogical(void) } } +/* + * Forced shutdown of walsender if wal_sender_shutdown_timeout has expired. + */ +static void +WalSndDoneImmediate(void) +{ + WalSndState state = MyWalSnd->state; + + if (state == WALSNDSTATE_CATCHUP || + state == WALSNDSTATE_STREAMING || + state == WALSNDSTATE_STOPPING) + { + QueryCompletion qc; + + /* Try to inform receiver that XLOG streaming is done */ + SetQueryCompletion(&qc, CMDTAG_COPY, 0); + EndCommand(&qc, DestRemote, false); + + /* + * Note that the output buffer may be full during the forced shutdown + * of walsender. If pq_flush() is called at that time, the walsender + * process will be stuck. Therefore, call pq_flush_if_writable() + * instead. Successful reception of the done message with the + * walsender forced into a shutdown is not guaranteed. + */ + pq_flush_if_writable(); + } + + /* + * Prevent ereport from attempting to send any more messages to the + * standby. Otherwise, it can cause the process to get stuck if the output + * buffers are full. + */ + if (whereToSendOutput == DestRemote) + whereToSendOutput = DestNone; + + ereport(WARNING, + (errmsg("terminating walsender process due to replication shutdown timeout"), + errdetail("Walsender process might have been terminated before all WAL data was replicated to the receiver."))); + + proc_exit(0); +} + /* * Shutdown if the sender is caught up. * @@ -3499,8 +3770,8 @@ WalSndDone(WalSndSendDataCallback send_data) * flush location if valid, write otherwise. Tools like pg_receivewal will * usually (unless in synchronous mode) return an invalid flush location. */ - replicatedPtr = XLogRecPtrIsInvalid(MyWalSnd->flush) ? - MyWalSnd->write : MyWalSnd->flush; + replicatedPtr = XLogRecPtrIsValid(MyWalSnd->flush) ? + MyWalSnd->flush : MyWalSnd->write; if (WalSndCaughtUp && sentPtr == replicatedPtr && !pq_is_send_pending()) @@ -3600,6 +3871,8 @@ HandleWalSndInitStopping(void) kill(MyProcPid, SIGTERM); else got_STOPPING = true; + + /* latch will be set by procsignal_sigusr1_handler */ } /* @@ -3624,56 +3897,46 @@ WalSndSignals(void) pqsignal(SIGTERM, die); /* request shutdown */ /* SIGQUIT handler was already set up by InitPostmasterChild */ InitializeTimeouts(); /* establishes SIGALRM handler */ - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, WalSndLastCycleHandler); /* request a last cycle and * shutdown */ /* Reset some signals that are accepted by postmaster but not here */ - pqsignal(SIGCHLD, SIG_DFL); + pqsignal(SIGCHLD, PG_SIG_DFL); } -/* Report shared-memory space needed by WalSndShmemInit */ -Size -WalSndShmemSize(void) +/* Register shared-memory space needed by walsender */ +static void +WalSndShmemRequest(void *arg) { - Size size = 0; + Size size; size = offsetof(WalSndCtlData, walsnds); size = add_size(size, mul_size(max_wal_senders, sizeof(WalSnd))); - - return size; + ShmemRequestStruct(.name = "Wal Sender Ctl", + .size = size, + .ptr = (void **) &WalSndCtl, + ); } -/* Allocate and initialize walsender-related shared memory */ -void -WalSndShmemInit(void) +/* Initialize walsender-related shared memory */ +static void +WalSndShmemInit(void *arg) { - bool found; - int i; - - WalSndCtl = (WalSndCtlData *) - ShmemInitStruct("Wal Sender Ctl", WalSndShmemSize(), &found); + for (int i = 0; i < NUM_SYNC_REP_WAIT_MODE; i++) + dlist_init(&(WalSndCtl->SyncRepQueue[i])); - if (!found) + for (int i = 0; i < max_wal_senders; i++) { - /* First time through, so initialize */ - MemSet(WalSndCtl, 0, WalSndShmemSize()); - - for (i = 0; i < NUM_SYNC_REP_WAIT_MODE; i++) - dlist_init(&(WalSndCtl->SyncRepQueue[i])); - - for (i = 0; i < max_wal_senders; i++) - { - WalSnd *walsnd = &WalSndCtl->walsnds[i]; - - SpinLockInit(&walsnd->mutex); - } + WalSnd *walsnd = &WalSndCtl->walsnds[i]; - ConditionVariableInit(&WalSndCtl->wal_flush_cv); - ConditionVariableInit(&WalSndCtl->wal_replay_cv); - ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv); + SpinLockInit(&walsnd->mutex); } + + ConditionVariableInit(&WalSndCtl->wal_flush_cv); + ConditionVariableInit(&WalSndCtl->wal_replay_cv); + ConditionVariableInit(&WalSndCtl->wal_confirm_rcv_cv); } /* @@ -3875,7 +4138,7 @@ WalSndGetStateString(WalSndState state) static Interval * offset_to_interval(TimeOffset offset) { - Interval *result = palloc(sizeof(Interval)); + Interval *result = palloc_object(Interval); result->month = 0; result->day = 0; @@ -3975,19 +4238,19 @@ pg_stat_get_wal_senders(PG_FUNCTION_ARGS) { values[1] = CStringGetTextDatum(WalSndGetStateString(state)); - if (XLogRecPtrIsInvalid(sent_ptr)) + if (!XLogRecPtrIsValid(sent_ptr)) nulls[2] = true; values[2] = LSNGetDatum(sent_ptr); - if (XLogRecPtrIsInvalid(write)) + if (!XLogRecPtrIsValid(write)) nulls[3] = true; values[3] = LSNGetDatum(write); - if (XLogRecPtrIsInvalid(flush)) + if (!XLogRecPtrIsValid(flush)) nulls[4] = true; values[4] = LSNGetDatum(flush); - if (XLogRecPtrIsInvalid(apply)) + if (!XLogRecPtrIsValid(apply)) nulls[5] = true; values[5] = LSNGetDatum(apply); @@ -3996,7 +4259,7 @@ pg_stat_get_wal_senders(PG_FUNCTION_ARGS) * which always returns an invalid flush location, as an * asynchronous standby. */ - priority = XLogRecPtrIsInvalid(flush) ? 0 : priority; + priority = XLogRecPtrIsValid(flush) ? priority : 0; if (writeLag < 0) nulls[6] = true; @@ -4066,13 +4329,13 @@ WalSndKeepalive(bool requestReply, XLogRecPtr writePtr) /* construct the message... */ resetStringInfo(&output_message); - pq_sendbyte(&output_message, 'k'); - pq_sendint64(&output_message, XLogRecPtrIsInvalid(writePtr) ? sentPtr : writePtr); + pq_sendbyte(&output_message, PqReplMsg_Keepalive); + pq_sendint64(&output_message, XLogRecPtrIsValid(writePtr) ? writePtr : sentPtr); pq_sendint64(&output_message, GetCurrentTimestamp()); pq_sendbyte(&output_message, requestReply ? 1 : 0); /* ... and send it wrapped in CopyData */ - pq_putmessage_noblock('d', output_message.data, output_message.len); + pq_putmessage_noblock(PqMsg_CopyData, output_message.data, output_message.len); /* Set local flag */ if (requestReply) @@ -4123,7 +4386,6 @@ WalSndKeepaliveIfNecessary(void) static void LagTrackerWrite(XLogRecPtr lsn, TimestampTz local_flush_time) { - bool buffer_full; int new_write_head; int i; @@ -4145,25 +4407,19 @@ LagTrackerWrite(XLogRecPtr lsn, TimestampTz local_flush_time) * of space. */ new_write_head = (lag_tracker->write_head + 1) % LAG_TRACKER_BUFFER_SIZE; - buffer_full = false; for (i = 0; i < NUM_SYNC_REP_WAIT_MODE; ++i) { + /* + * If the buffer is full, move the slowest reader to a separate + * overflow entry and free its space in the buffer so the write head + * can advance. + */ if (new_write_head == lag_tracker->read_heads[i]) - buffer_full = true; - } - - /* - * If the buffer is full, for now we just rewind by one slot and overwrite - * the last sample, as a simple (if somewhat uneven) way to lower the - * sampling rate. There may be better adaptive compaction algorithms. - */ - if (buffer_full) - { - new_write_head = lag_tracker->write_head; - if (lag_tracker->write_head > 0) - lag_tracker->write_head--; - else - lag_tracker->write_head = LAG_TRACKER_BUFFER_SIZE - 1; + { + lag_tracker->overflowed[i] = + lag_tracker->buffer[lag_tracker->read_heads[i]]; + lag_tracker->read_heads[i] = -1; + } } /* Store a sample at the current write head position. */ @@ -4190,6 +4446,28 @@ LagTrackerRead(int head, XLogRecPtr lsn, TimestampTz now) { TimestampTz time = 0; + /* + * If 'lsn' has not passed the WAL position stored in the overflow entry, + * return the elapsed time (in microseconds) since the saved local flush + * time. If the flush time is in the future (due to clock drift), return + * -1 to treat as no valid sample. + * + * Otherwise, switch back to using the buffer to control the read head and + * compute the elapsed time. The read head is then reset to point to the + * oldest entry in the buffer. + */ + if (lag_tracker->read_heads[head] == -1) + { + if (lag_tracker->overflowed[head].lsn > lsn) + return (now >= lag_tracker->overflowed[head].time) ? + now - lag_tracker->overflowed[head].time : -1; + + time = lag_tracker->overflowed[head].time; + lag_tracker->last_read[head] = lag_tracker->overflowed[head]; + lag_tracker->read_heads[head] = + (lag_tracker->write_head + 1) % LAG_TRACKER_BUFFER_SIZE; + } + /* Read all unread samples up to this LSN or end of buffer. */ while (lag_tracker->read_heads[head] != lag_tracker->write_head && lag_tracker->buffer[lag_tracker->read_heads[head]].lsn <= lsn) diff --git a/src/backend/rewrite/Makefile b/src/backend/rewrite/Makefile index 4680752e6a7f8..09070047b7e06 100644 --- a/src/backend/rewrite/Makefile +++ b/src/backend/rewrite/Makefile @@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global OBJS = \ rewriteDefine.o \ + rewriteGraphTable.o \ rewriteHandler.o \ rewriteManip.o \ rewriteRemove.o \ diff --git a/src/backend/rewrite/meson.build b/src/backend/rewrite/meson.build index 5615d085ba345..4387d80d93ca2 100644 --- a/src/backend/rewrite/meson.build +++ b/src/backend/rewrite/meson.build @@ -1,7 +1,8 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'rewriteDefine.c', + 'rewriteGraphTable.c', 'rewriteHandler.c', 'rewriteManip.c', 'rewriteRemove.c', diff --git a/src/backend/rewrite/rewriteDefine.c b/src/backend/rewrite/rewriteDefine.c index 8aa90b0d6fb75..6a223fbeaa4e3 100644 --- a/src/backend/rewrite/rewriteDefine.c +++ b/src/backend/rewrite/rewriteDefine.c @@ -3,7 +3,7 @@ * rewriteDefine.c * routines for defining a rewrite rule * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -725,10 +725,9 @@ EnableDisableRule(Relation rel, const char *rulename, /* * Change ev_enabled if it is different from the desired new state. */ - if (DatumGetChar(ruleform->ev_enabled) != - fires_when) + if (ruleform->ev_enabled != fires_when) { - ruleform->ev_enabled = CharGetDatum(fires_when); + ruleform->ev_enabled = fires_when; CatalogTupleUpdate(pg_rewrite_desc, &ruletup->t_self, ruletup); changed = true; diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c new file mode 100644 index 0000000000000..3519ea3beae38 --- /dev/null +++ b/src/backend/rewrite/rewriteGraphTable.c @@ -0,0 +1,1327 @@ +/*------------------------------------------------------------------------- + * + * rewriteGraphTable.c + * Support for rewriting GRAPH_TABLE clauses. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/rewrite/rewriteGraphTable.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/sysattr.h" +#include "access/table.h" +#include "access/htup_details.h" +#include "catalog/pg_operator.h" +#include "catalog/pg_propgraph_element.h" +#include "catalog/pg_propgraph_element_label.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_label_property.h" +#include "catalog/pg_propgraph_property.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/analyze.h" +#include "parser/parse_collate.h" +#include "parser/parse_func.h" +#include "parser/parse_node.h" +#include "parser/parse_oper.h" +#include "parser/parse_relation.h" +#include "parser/parsetree.h" +#include "parser/parse_graphtable.h" +#include "rewrite/rewriteGraphTable.h" +#include "rewrite/rewriteHandler.h" +#include "rewrite/rewriteManip.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/ruleutils.h" +#include "utils/syscache.h" + + +/* + * Represents one path factor in a path. + * + * In a non-cyclic path, one path factor corresponds to one element pattern. + * + * In a cyclic path, one path factor corresponds to all the element patterns with + * the same variable name. + */ +struct path_factor +{ + GraphElementPatternKind kind; + const char *variable; + Node *labelexpr; + Node *whereClause; + int factorpos; /* Position of this path factor in the list of + * path factors representing a given path + * pattern. */ + List *labeloids; /* OIDs of all the labels referenced in + * labelexpr. */ + /* Links to adjacent vertex path factors if this is an edge path factor. */ + struct path_factor *src_pf; + struct path_factor *dest_pf; +}; + +/* + * Represents one property graph element (vertex or edge) in the path. + * + * Label expression in an element pattern resolves into a set of elements. We + * create one path_element object for each of those elements. + */ +struct path_element +{ + /* Path factor from which this element is derived. */ + struct path_factor *path_factor; + Oid elemoid; + Oid reloid; + /* Source and destination vertex elements for an edge element. */ + Oid srcvertexid; + Oid destvertexid; + /* Source and destination conditions for an edge element. */ + List *src_quals; + List *dest_quals; +}; + +static Node *replace_property_refs(Oid propgraphid, Node *node, const List *mappings); +static List *build_edge_vertex_link_quals(HeapTuple edgetup, int edgerti, int refrti, Oid refid, AttrNumber catalog_key_attnum, AttrNumber catalog_ref_attnum, AttrNumber catalog_eqop_attnum); +static List *generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern); +static Query *generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path); +static Node *generate_setop_from_pathqueries(List *pathqueries, List **rtable, List **targetlist); +static List *generate_queries_for_path_pattern_recurse(RangeTblEntry *rte, List *pathqueries, List *cur_path, List *path_elem_lists, int elempos); +static Query *generate_query_for_empty_path_pattern(RangeTblEntry *rte); +static Query *generate_union_from_pathqueries(List **pathqueries); +static List *get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf); +static bool is_property_associated_with_label(Oid labeloid, Oid propoid); +static Node *get_element_property_expr(Oid elemoid, Oid propoid, int rtindex); + +/* + * Convert GRAPH_TABLE clause into a subquery using relational + * operators. + */ +Query * +rewriteGraphTable(Query *parsetree, int rt_index) +{ + RangeTblEntry *rte; + Query *graph_table_query; + List *path_pattern; + List *pathqueries = NIL; + + rte = rt_fetch(rt_index, parsetree->rtable); + + Assert(list_length(rte->graph_pattern->path_pattern_list) == 1); + + path_pattern = linitial(rte->graph_pattern->path_pattern_list); + pathqueries = generate_queries_for_path_pattern(rte, path_pattern); + graph_table_query = generate_union_from_pathqueries(&pathqueries); + + AcquireRewriteLocks(graph_table_query, true, false); + + rte->rtekind = RTE_SUBQUERY; + rte->subquery = graph_table_query; + rte->lateral = true; + + /* + * Reset no longer applicable fields, to appease + * WRITE_READ_PARSE_PLAN_TREES. + */ + rte->graph_pattern = NULL; + rte->graph_table_columns = NIL; + + return parsetree; +} + +/* + * Generate queries representing the given path pattern applied to the given + * property graph. + * + * A path pattern consists of one or more element patterns. Each of the element + * patterns may be satisfied by multiple elements. A path satisfying the given + * path pattern consists of one element from each element pattern. There can be + * as many paths as the number of combinations of the elements. A path pattern + * in itself is a K-partite graph where K = number of element patterns in the + * path pattern. The possible paths are computed by performing a DFS in this + * graph. The DFS is implemented as recursion. Each of these paths is converted + * into a query connecting all the elements in that path. Set of these queries is + * returned. + * + * Between every two vertex elements in the path there is an edge element that + * connects them. An edge connects two vertexes identified by the source and + * destination keys respectively. The connection between an edge and its + * adjacent vertex is naturally computed as an equi-join between edge and vertex + * table on their respective keys. Hence the query representing one path + * consists of JOINs between edge and vertex tables. + * + * generate_queries_for_path_pattern() starts the recursion but actual work is + * done by generate_queries_for_path_pattern_recurse(). + * generate_query_for_graph_path() constructs a query for a given path. + * + * A path pattern may result into no path if any of the element pattern yields no + * elements or edge patterns yield no edges connecting adjacent vertex patterns. + * In such a case a dummy query which returns no result is returned + * (generate_query_for_empty_path_pattern()). + * + * 'path_pattern' is given path pattern to be applied on the property graph in + * the GRAPH_TABLE clause represented by given 'rte'. + */ +static List * +generate_queries_for_path_pattern(RangeTblEntry *rte, List *path_pattern) +{ + List *pathqueries = NIL; + List *path_elem_lists = NIL; + int factorpos = 0; + List *path_factors = NIL; + struct path_factor *prev_pf = NULL; + + Assert(list_length(path_pattern) > 0); + + /* + * Create a list of path factors representing the given path pattern + * linking edge path factors to their adjacent vertex path factors. + * + * While doing that merge element patterns with the same variable name + * into a single path_factor. + */ + foreach_node(GraphElementPattern, gep, path_pattern) + { + struct path_factor *pf = NULL; + + /* + * Unsupported conditions should have been caught by the parser + * itself. We have corresponding Asserts here to document the + * assumptions in this code. + */ + Assert(gep->kind == VERTEX_PATTERN || IS_EDGE_PATTERN(gep->kind)); + Assert(!gep->quantifier); + + foreach_ptr(struct path_factor, other, path_factors) + { + if (gep->variable && other->variable && + strcmp(gep->variable, other->variable) == 0) + { + if (other->kind != gep->kind) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("element patterns with same variable name \"%s\" but different element pattern types", + gep->variable))); + + /* + * If both the element patterns have label expressions, they + * need to be conjuncted, which is not supported right now. + * + * However, an empty label expression means all labels. + * Conjunction of any label expression with all labels is the + * expression itself. Hence if only one of the two element + * patterns has a label expression use that expression. + */ + if (!other->labelexpr) + other->labelexpr = gep->labelexpr; + else if (gep->labelexpr && !equal(other->labelexpr, gep->labelexpr)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("element patterns with same variable name \"%s\" but different label expressions are not supported", + gep->variable))); + + /* + * If two element patterns have the same variable name, they + * represent the same set of graph elements and hence are + * constrained by conditions from both the element patterns. + */ + if (!other->whereClause) + other->whereClause = gep->whereClause; + else if (gep->whereClause) + other->whereClause = (Node *) makeBoolExpr(AND_EXPR, + list_make2(other->whereClause, gep->whereClause), + -1); + pf = other; + break; + } + } + + if (!pf) + { + pf = palloc0_object(struct path_factor); + pf->factorpos = factorpos++; + pf->kind = gep->kind; + pf->labelexpr = gep->labelexpr; + pf->variable = gep->variable; + pf->whereClause = gep->whereClause; + + path_factors = lappend(path_factors, pf); + } + + /* + * Setup links to the previous path factor in the path. + * + * If the previous path factor represents an edge, this path factor + * represents an adjacent vertex; the source vertex for an edge + * pointing left or the destination vertex for an edge pointing right. + * If this path factor represents an edge, the previous path factor + * represents an adjacent vertex; source vertex for an edge pointing + * right or the destination vertex for an edge pointing left. + * + * Edge pointing in any direction is treated similar to that pointing + * in right direction here. When constructing a query in + * generate_query_for_graph_path(), we will try links in both the + * directions. + * + * If multiple edge patterns share the same variable name, they + * constrain the adjacent vertex patterns since an edge can connect + * only one pair of vertexes. These adjacent vertex patterns need to + * be merged even though they have different variables. Such element + * patterns form a walk of graph where vertex and edges are repeated. + * For example, in (a)-[b]->(c)<-[b]-(d), (a) and (d) represent the + * same vertex element. This is slightly harder to implement and + * probably less useful. Hence not supported for now. + */ + if (prev_pf) + { + if (prev_pf->kind == EDGE_PATTERN_RIGHT || prev_pf->kind == EDGE_PATTERN_ANY) + { + Assert(!IS_EDGE_PATTERN(pf->kind)); + if (prev_pf->dest_pf && prev_pf->dest_pf != pf) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("an edge cannot connect more than two vertexes even in a cyclic pattern")); + prev_pf->dest_pf = pf; + } + else if (prev_pf->kind == EDGE_PATTERN_LEFT) + { + Assert(!IS_EDGE_PATTERN(pf->kind)); + if (prev_pf->src_pf && prev_pf->src_pf != pf) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("an edge cannot connect more than two vertexes even in a cyclic pattern")); + prev_pf->src_pf = pf; + } + else + { + Assert(prev_pf->kind == VERTEX_PATTERN); + Assert(IS_EDGE_PATTERN(pf->kind)); + } + + if (pf->kind == EDGE_PATTERN_RIGHT || pf->kind == EDGE_PATTERN_ANY) + { + Assert(!IS_EDGE_PATTERN(prev_pf->kind)); + if (pf->src_pf && pf->src_pf != prev_pf) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("an edge cannot connect more than two vertexes even in a cyclic pattern")); + pf->src_pf = prev_pf; + } + else if (pf->kind == EDGE_PATTERN_LEFT) + { + Assert(!IS_EDGE_PATTERN(prev_pf->kind)); + if (pf->dest_pf && pf->dest_pf != prev_pf) + ereport(ERROR, + errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("an edge cannot connect more than two vertexes even in a cyclic pattern")); + pf->dest_pf = prev_pf; + } + else + { + Assert(pf->kind == VERTEX_PATTERN); + Assert(IS_EDGE_PATTERN(prev_pf->kind)); + } + } + + prev_pf = pf; + } + + /* + * Collect list of elements for each path factor. Do this after all the + * edge links are setup correctly. + */ + foreach_ptr(struct path_factor, pf, path_factors) + path_elem_lists = lappend(path_elem_lists, + get_path_elements_for_path_factor(rte->relid, pf)); + + pathqueries = generate_queries_for_path_pattern_recurse(rte, pathqueries, + NIL, path_elem_lists, 0); + if (!pathqueries) + pathqueries = list_make1(generate_query_for_empty_path_pattern(rte)); + + return pathqueries; +} + +/* + * Recursive workhorse function of generate_queries_for_path_pattern(). + * + * `elempos` is the position of the next element being added in the path being + * built. + */ +static List * +generate_queries_for_path_pattern_recurse(RangeTblEntry *rte, List *pathqueries, List *cur_path, List *path_elem_lists, int elempos) +{ + List *path_elems = list_nth_node(List, path_elem_lists, elempos); + + foreach_ptr(struct path_element, pe, path_elems) + { + /* Update current path being built with current element. */ + cur_path = lappend(cur_path, pe); + + /* + * If this is the last element in the path, generate query for the + * completed path. Else recurse processing the next element. + */ + if (list_length(path_elem_lists) == list_length(cur_path)) + { + Query *pathquery = generate_query_for_graph_path(rte, cur_path); + + Assert(elempos == list_length(path_elem_lists) - 1); + if (pathquery) + pathqueries = lappend(pathqueries, pathquery); + } + else + pathqueries = generate_queries_for_path_pattern_recurse(rte, pathqueries, + cur_path, + path_elem_lists, + elempos + 1); + /* Make way for the next element at the same position. */ + cur_path = list_delete_last(cur_path); + } + + return pathqueries; +} + +/* + * Construct a query representing given graph path. + * + * The query contains: + * + * 1. targetlist corresponding to the COLUMNS clause of GRAPH_TABLE clause + * + * 2. quals corresponding to the WHERE clause of individual elements, WHERE + * clause in GRAPH_TABLE clause and quals representing edge-vertex links. + * + * 3. fromlist containing all elements in the path + * + * The collations of property expressions are obtained from the catalog. The + * collations of expressions in COLUMNS and WHERE clauses are assigned before + * rewriting the graph table. The collations of the edge-vertex link quals are + * assigned when crafting those quals. Thus everything in the query that requires + * collation assignment has been taken care of already. No separate collation + * assignment is required in this function. + * + * More details in the prologue of generate_queries_for_path_pattern(). + */ +static Query * +generate_query_for_graph_path(RangeTblEntry *rte, List *graph_path) +{ + Query *path_query = makeNode(Query); + List *fromlist = NIL; + List *qual_exprs = NIL; + List *vars; + + path_query->commandType = CMD_SELECT; + + foreach_ptr(struct path_element, pe, graph_path) + { + struct path_factor *pf = pe->path_factor; + RangeTblRef *rtr; + Relation rel; + ParseNamespaceItem *pni; + + Assert(pf->kind == VERTEX_PATTERN || IS_EDGE_PATTERN(pf->kind)); + + /* Add conditions representing edge connections. */ + if (IS_EDGE_PATTERN(pf->kind)) + { + struct path_element *src_pe; + struct path_element *dest_pe; + Expr *edge_qual = NULL; + + Assert(pf->src_pf && pf->dest_pf); + src_pe = list_nth(graph_path, pf->src_pf->factorpos); + dest_pe = list_nth(graph_path, pf->dest_pf->factorpos); + + /* Make sure that the links of adjacent vertices are correct. */ + Assert(pf->src_pf == src_pe->path_factor && + pf->dest_pf == dest_pe->path_factor); + + if (src_pe->elemoid == pe->srcvertexid && + dest_pe->elemoid == pe->destvertexid) + edge_qual = makeBoolExpr(AND_EXPR, + list_concat(copyObject(pe->src_quals), + copyObject(pe->dest_quals)), + -1); + + /* + * An edge pattern in any direction matches edges in both + * directions, try swapping source and destination. When the + * source and destination is the same vertex table, quals + * corresponding to either direction may get satisfied. Hence OR + * the quals corresponding to both the directions. + */ + if (pf->kind == EDGE_PATTERN_ANY && + dest_pe->elemoid == pe->srcvertexid && + src_pe->elemoid == pe->destvertexid) + { + List *src_quals = copyObject(pe->dest_quals); + List *dest_quals = copyObject(pe->src_quals); + Expr *rev_edge_qual; + + /* Swap the source and destination varnos in the quals. */ + ChangeVarNodes((Node *) dest_quals, pe->path_factor->src_pf->factorpos + 1, + pe->path_factor->dest_pf->factorpos + 1, 0); + ChangeVarNodes((Node *) src_quals, pe->path_factor->dest_pf->factorpos + 1, + pe->path_factor->src_pf->factorpos + 1, 0); + + rev_edge_qual = makeBoolExpr(AND_EXPR, list_concat(src_quals, dest_quals), -1); + if (edge_qual) + edge_qual = makeBoolExpr(OR_EXPR, list_make2(edge_qual, rev_edge_qual), -1); + else + edge_qual = rev_edge_qual; + } + + /* + * If the given edge element does not connect the adjacent vertex + * elements in this path, the path is broken. Abandon this path as + * it won't return any rows. + */ + if (edge_qual == NULL) + return NULL; + + qual_exprs = lappend(qual_exprs, edge_qual); + } + else + Assert(!pe->src_quals && !pe->dest_quals); + + /* + * Create RangeTblEntry for this element table. + * + * SQL/PGQ standard (Ref. Section 11.19, Access rule 2 and General + * rule 4) does not specify whose access privileges to use when + * accessing the element tables: property graph owner's or current + * user's. It is safer to use current user's privileges so as not to + * make property graphs as a hole for unpriviledged data access. This + * is inline with the views being security_invoker by default. + */ + rel = table_open(pe->reloid, AccessShareLock); + pni = addRangeTableEntryForRelation(make_parsestate(NULL), rel, AccessShareLock, + NULL, true, false); + table_close(rel, NoLock); + path_query->rtable = lappend(path_query->rtable, pni->p_rte); + path_query->rteperminfos = lappend(path_query->rteperminfos, pni->p_perminfo); + pni->p_rte->perminfoindex = list_length(path_query->rteperminfos); + rtr = makeNode(RangeTblRef); + rtr->rtindex = list_length(path_query->rtable); + fromlist = lappend(fromlist, rtr); + + /* + * Make sure that the assumption mentioned in create_pe_for_element() + * holds true; that the elements' RangeTblEntrys are added in the + * order in which their respective path factors appear in the list of + * path factors representing the path pattern. + */ + Assert(pf->factorpos + 1 == rtr->rtindex); + + if (pf->whereClause) + { + Node *tr; + + tr = replace_property_refs(rte->relid, pf->whereClause, list_make1(pe)); + + qual_exprs = lappend(qual_exprs, tr); + } + } + + if (rte->graph_pattern->whereClause) + { + Node *path_quals = replace_property_refs(rte->relid, + (Node *) rte->graph_pattern->whereClause, + graph_path); + + qual_exprs = lappend(qual_exprs, path_quals); + } + + path_query->jointree = makeFromExpr(fromlist, + qual_exprs ? (Node *) makeBoolExpr(AND_EXPR, qual_exprs, -1) : NULL); + + /* Construct query targetlist from COLUMNS specification of GRAPH_TABLE. */ + path_query->targetList = castNode(List, + replace_property_refs(rte->relid, + (Node *) rte->graph_table_columns, + graph_path)); + + /* + * Mark the columns being accessed in the path query as requiring SELECT + * privilege. Any lateral columns should have been handled when the + * corresponding ColumnRefs were transformed. Ignore those here. + */ + vars = pull_vars_of_level((Node *) list_make2(qual_exprs, path_query->targetList), 0); + foreach_node(Var, var, vars) + { + RTEPermissionInfo *perminfo = getRTEPermissionInfo(path_query->rteperminfos, + rt_fetch(var->varno, path_query->rtable)); + + /* Must offset the attnum to fit in a bitmapset */ + perminfo->selectedCols = bms_add_member(perminfo->selectedCols, + var->varattno - FirstLowInvalidHeapAttributeNumber); + } + + return path_query; +} + +/* + * Construct a query which would not return any rows. + * + * More details in the prologue of generate_queries_for_path_pattern(). + */ +static Query * +generate_query_for_empty_path_pattern(RangeTblEntry *rte) +{ + Query *query = makeNode(Query); + + query->commandType = CMD_SELECT; + query->rtable = NIL; + query->rteperminfos = NIL; + query->jointree = makeFromExpr(NIL, (Node *) makeBoolConst(false, false)); + + /* + * Even though no rows are returned, the result still projects the same + * columns as projected by GRAPH_TABLE clause. Do this by constructing a + * target list full of NULL values. + */ + foreach_node(TargetEntry, te, rte->graph_table_columns) + { + Node *nte = (Node *) te->expr; + + te->expr = (Expr *) makeNullConst(exprType(nte), exprTypmod(nte), exprCollation(nte)); + query->targetList = lappend(query->targetList, te); + } + + return query; +} + +/* + * Construct a query which is UNION of given path queries. + * + * The UNION query derives collations of its targetlist entries from the + * corresponding targetlist entries of the path queries. The targetlists of path + * queries being UNION'ed already have collations assigned. No separate + * collation assignment required in this function. + * + * The function destroys given pathqueries list while constructing + * SetOperationStmt recursively. Hence the function always returns with + * `pathqueries` set to NIL. + */ +static Query * +generate_union_from_pathqueries(List **pathqueries) +{ + List *rtable = NIL; + Query *sampleQuery = linitial_node(Query, *pathqueries); + SetOperationStmt *sostmt; + Query *union_query; + int resno; + ListCell *lctl, + *lct, + *lcm, + *lcc; + + Assert(list_length(*pathqueries) > 0); + + /* If there's only one pathquery, no need to construct a UNION query. */ + if (list_length(*pathqueries) == 1) + { + *pathqueries = NIL; + return sampleQuery; + } + + sostmt = castNode(SetOperationStmt, + generate_setop_from_pathqueries(*pathqueries, &rtable, NULL)); + + /* Encapsulate the set operation statement into a Query. */ + union_query = makeNode(Query); + union_query->commandType = CMD_SELECT; + union_query->rtable = rtable; + union_query->setOperations = (Node *) sostmt; + union_query->rteperminfos = NIL; + union_query->jointree = makeFromExpr(NIL, NULL); + + /* + * Generate dummy targetlist for outer query using column names from one + * of the queries and common datatypes/collations of topmost set + * operation. It shouldn't matter which query. Also it shouldn't matter + * which RT index is used as varno in the target list entries, as long as + * it corresponds to a real RT entry; else funny things may happen when + * the tree is mashed by rule rewriting. So we use 1 since there's always + * one RT entry at least. + */ + Assert(rt_fetch(1, rtable)); + union_query->targetList = NULL; + resno = 1; + forfour(lct, sostmt->colTypes, + lcm, sostmt->colTypmods, + lcc, sostmt->colCollations, + lctl, sampleQuery->targetList) + { + Oid colType = lfirst_oid(lct); + int32 colTypmod = lfirst_int(lcm); + Oid colCollation = lfirst_oid(lcc); + TargetEntry *sample_tle = (TargetEntry *) lfirst(lctl); + char *colName; + TargetEntry *tle; + Var *var; + + Assert(!sample_tle->resjunk); + colName = pstrdup(sample_tle->resname); + var = makeVar(1, sample_tle->resno, colType, colTypmod, colCollation, 0); + var->location = exprLocation((Node *) sample_tle->expr); + tle = makeTargetEntry((Expr *) var, (AttrNumber) resno++, colName, false); + union_query->targetList = lappend(union_query->targetList, tle); + } + + *pathqueries = NIL; + return union_query; +} + +/* + * Construct a query which is UNION of all the given path queries. + * + * The function destroys given pathqueries list while constructing + * SetOperationStmt recursively. + */ +static Node * +generate_setop_from_pathqueries(List *pathqueries, List **rtable, List **targetlist) +{ + SetOperationStmt *sostmt; + Query *lquery; + Node *rarg; + RangeTblRef *lrtr = makeNode(RangeTblRef); + List *rtargetlist; + ParseNamespaceItem *pni; + + /* Recursion termination condition. */ + if (list_length(pathqueries) == 0) + { + *targetlist = NIL; + return NULL; + } + + lquery = linitial_node(Query, pathqueries); + + pni = addRangeTableEntryForSubquery(make_parsestate(NULL), lquery, NULL, + false, false); + *rtable = lappend(*rtable, pni->p_rte); + lrtr->rtindex = list_length(*rtable); + rarg = generate_setop_from_pathqueries(list_delete_first(pathqueries), rtable, &rtargetlist); + if (rarg == NULL) + { + /* + * No further path queries in the list. Convert the last query into a + * RangeTblRef as expected by SetOperationStmt. Extract a list of the + * non-junk TLEs for upper-level processing. + */ + if (targetlist) + { + *targetlist = NIL; + foreach_node(TargetEntry, tle, lquery->targetList) + { + if (!tle->resjunk) + *targetlist = lappend(*targetlist, tle); + } + } + return (Node *) lrtr; + } + + sostmt = makeNode(SetOperationStmt); + sostmt->op = SETOP_UNION; + sostmt->all = true; + sostmt->larg = (Node *) lrtr; + sostmt->rarg = rarg; + constructSetOpTargetlist(NULL, sostmt, lquery->targetList, rtargetlist, targetlist, "UNION", false); + + return (Node *) sostmt; +} + +/* + * Construct a path_element object for the graph element given by `elemoid` + * satisfied by the path factor `pf`. + * + * If the type of graph element does not fit the element pattern kind, the + * function returns NULL. + */ +static struct path_element * +create_pe_for_element(struct path_factor *pf, Oid elemoid) +{ + HeapTuple eletup = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(elemoid)); + Form_pg_propgraph_element pgeform; + struct path_element *pe; + + if (!eletup) + elog(ERROR, "cache lookup failed for property graph element %u", elemoid); + pgeform = ((Form_pg_propgraph_element) GETSTRUCT(eletup)); + + if ((pgeform->pgekind == PGEKIND_VERTEX && pf->kind != VERTEX_PATTERN) || + (pgeform->pgekind == PGEKIND_EDGE && !IS_EDGE_PATTERN(pf->kind))) + { + ReleaseSysCache(eletup); + return NULL; + } + + pe = palloc0_object(struct path_element); + pe->path_factor = pf; + pe->elemoid = elemoid; + pe->reloid = pgeform->pgerelid; + + /* + * When a path is converted into a query + * (generate_query_for_graph_path()), a RangeTblEntry will be created for + * every element in the path. Fixing rtindexes of RangeTblEntrys here + * makes it possible to craft elements' qual expressions only once while + * we have access to the catalog entry. Otherwise they need to be crafted + * as many times as the number of paths a given element appears in, + * fetching catalog entry again each time. Hence we simply assume + * RangeTblEntrys will be created in the same order in which the + * corresponding path factors appear in the list of path factors + * representing a path pattern. That way their rtindexes will be same as + * path_factor::factorpos + 1. + */ + if (IS_EDGE_PATTERN(pf->kind)) + { + pe->srcvertexid = pgeform->pgesrcvertexid; + pe->destvertexid = pgeform->pgedestvertexid; + Assert(pf->src_pf && pf->dest_pf); + + pe->src_quals = build_edge_vertex_link_quals(eletup, pf->factorpos + 1, pf->src_pf->factorpos + 1, + pe->srcvertexid, + Anum_pg_propgraph_element_pgesrckey, + Anum_pg_propgraph_element_pgesrcref, + Anum_pg_propgraph_element_pgesrceqop); + pe->dest_quals = build_edge_vertex_link_quals(eletup, pf->factorpos + 1, pf->dest_pf->factorpos + 1, + pe->destvertexid, + Anum_pg_propgraph_element_pgedestkey, + Anum_pg_propgraph_element_pgedestref, + Anum_pg_propgraph_element_pgedesteqop); + } + + ReleaseSysCache(eletup); + + return pe; +} + +/* + * Returns the list of OIDs of graph labels which the given label expression + * resolves to in the given property graph. + */ +static List * +get_labels_for_expr(Oid propgraphid, Node *labelexpr) +{ + List *label_oids; + + if (!labelexpr) + { + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tup; + + /* + * According to section 9.2 "Contextual inference of a set of labels" + * subclause 2.a.ii of SQL/PGQ standard, element pattern which does + * not have a label expression is considered to have label expression + * equivalent to '%|!%' which is set of all labels. + */ + label_oids = NIL; + rel = table_open(PropgraphLabelRelationId, AccessShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_label_pglpgid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(propgraphid)); + scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId, + true, NULL, 1, key); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_propgraph_label label = (Form_pg_propgraph_label) GETSTRUCT(tup); + + label_oids = lappend_oid(label_oids, label->oid); + } + systable_endscan(scan); + table_close(rel, AccessShareLock); + } + else if (IsA(labelexpr, GraphLabelRef)) + { + GraphLabelRef *glr = castNode(GraphLabelRef, labelexpr); + + label_oids = list_make1_oid(glr->labelid); + } + else if (IsA(labelexpr, BoolExpr)) + { + BoolExpr *be = castNode(BoolExpr, labelexpr); + List *label_exprs = be->args; + + label_oids = NIL; + foreach_node(GraphLabelRef, glr, label_exprs) + label_oids = lappend_oid(label_oids, glr->labelid); + } + else + { + /* + * should not reach here since gram.y will not generate a label + * expression with other node types. + */ + elog(ERROR, "unsupported label expression node: %d", (int) nodeTag(labelexpr)); + } + + return label_oids; +} + +/* + * Return a list of all the graph elements that satisfy the graph element pattern + * represented by the given path_factor `pf`. + * + * First we find all the graph labels that satisfy the label expression in path + * factor. Each label is associated with one or more graph elements. A union of + * all such elements satisfies the element pattern. We create one path_element + * object representing every element whose graph element kind qualifies the + * element pattern kind. A list of all such path_element objects is returned. + * + * Note that we need to report an error for an explicitly specified label which + * is not associated with any graph element of the required kind. So we have to + * treat each label separately. Without that requirement we could have collected + * all the unique elements first and then created path_element objects for them + * to simplify the code. + */ +static List * +get_path_elements_for_path_factor(Oid propgraphid, struct path_factor *pf) +{ + List *label_oids = get_labels_for_expr(propgraphid, pf->labelexpr); + List *elem_oids_seen = NIL; + List *pf_elem_oids = NIL; + List *path_elements = NIL; + List *unresolved_labels = NIL; + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tup; + + /* + * A property graph element can be either a vertex or an edge. Other types + * of path factors like nested path pattern need to be handled separately + * when supported. + */ + Assert(pf->kind == VERTEX_PATTERN || IS_EDGE_PATTERN(pf->kind)); + + rel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + foreach_oid(labeloid, label_oids) + { + bool found = false; + + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_pgellabelid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(labeloid)); + scan = systable_beginscan(rel, PropgraphElementLabelLabelIndexId, true, + NULL, 1, key); + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_propgraph_element_label label_elem = (Form_pg_propgraph_element_label) GETSTRUCT(tup); + Oid elem_oid = label_elem->pgelelid; + + if (!list_member_oid(elem_oids_seen, elem_oid)) + { + /* + * Create path_element object if the new element qualifies the + * element pattern kind. + */ + struct path_element *pe = create_pe_for_element(pf, elem_oid); + + if (pe) + { + path_elements = lappend(path_elements, pe); + + /* Remember qualified elements. */ + pf_elem_oids = lappend_oid(pf_elem_oids, elem_oid); + found = true; + } + + /* + * Remember qualified and unqualified elements processed so + * far to avoid processing already processed elements again. + */ + elem_oids_seen = lappend_oid(elem_oids_seen, label_elem->pgelelid); + } + else if (list_member_oid(pf_elem_oids, elem_oid)) + { + /* + * The graph element is known to qualify the given element + * pattern. Flag that the current label has at least one + * qualified element associated with it. + */ + found = true; + } + } + + if (!found) + { + /* + * We did not find any qualified element associated with this + * label. The label or its properties can not be associated with + * the given element pattern. Throw an error if the label was + * explicitly specified in the element pattern. Otherwise remember + * it for later use. + */ + if (!pf->labelexpr) + unresolved_labels = lappend_oid(unresolved_labels, labeloid); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("no property graph element of type \"%s\" has label \"%s\" associated with it in property graph \"%s\"", + pf->kind == VERTEX_PATTERN ? "vertex" : "edge", + get_propgraph_label_name(labeloid), + get_rel_name(propgraphid)))); + } + + systable_endscan(scan); + } + table_close(rel, AccessShareLock); + + /* + * Remove the labels which were not explicitly mentioned in the label + * expression but do not have any qualified elements associated with them. + * Properties associated with such labels may not be referenced. See + * replace_property_refs_mutator() for more details. + */ + pf->labeloids = list_difference_oid(label_oids, unresolved_labels); + + return path_elements; +} + +/* + * Mutating property references into table variables + */ + +struct replace_property_refs_context +{ + Oid propgraphid; + const List *mappings; +}; + +static Node * +replace_property_refs_mutator(Node *node, struct replace_property_refs_context *context) +{ + if (node == NULL) + return NULL; + if (IsA(node, Var)) + { + Var *var = (Var *) node; + Var *newvar = copyObject(var); + + /* + * If it's already a Var, then it was a lateral reference. Since we + * are in a subquery after the rewrite, we have to increase the level + * by one. + */ + newvar->varlevelsup++; + + return (Node *) newvar; + } + else if (IsA(node, GraphPropertyRef)) + { + GraphPropertyRef *gpr = (GraphPropertyRef *) node; + Node *n = NULL; + struct path_element *found_mapping = NULL; + struct path_factor *mapping_factor = NULL; + List *unrelated_labels = NIL; + + foreach_ptr(struct path_element, m, context->mappings) + { + if (m->path_factor->variable && strcmp(gpr->elvarname, m->path_factor->variable) == 0) + { + found_mapping = m; + break; + } + } + + /* + * transformGraphTablePropertyRef() would not create a + * GraphPropertyRef for a variable which is not present in the graph + * path pattern. + */ + Assert(found_mapping); + + mapping_factor = found_mapping->path_factor; + + /* + * Find property definition for given element through any of the + * associated labels qualifying the given element pattern. + */ + foreach_oid(labeloid, mapping_factor->labeloids) + { + Oid elem_labelid = GetSysCacheOid2(PROPGRAPHELEMENTLABELELEMENTLABEL, + Anum_pg_propgraph_element_label_oid, + ObjectIdGetDatum(found_mapping->elemoid), + ObjectIdGetDatum(labeloid)); + + if (OidIsValid(elem_labelid)) + { + HeapTuple tup = SearchSysCache2(PROPGRAPHLABELPROP, ObjectIdGetDatum(elem_labelid), + ObjectIdGetDatum(gpr->propid)); + + if (!tup) + { + /* + * The label is associated with the given element but it + * is not associated with the required property. Check + * next label. + */ + continue; + } + + n = stringToNode(TextDatumGetCString(SysCacheGetAttrNotNull(PROPGRAPHLABELPROP, + tup, Anum_pg_propgraph_label_property_plpexpr))); + ChangeVarNodes(n, 1, mapping_factor->factorpos + 1, 0); + + ReleaseSysCache(tup); + } + else + { + /* + * Label is not associated with the element but it may be + * associated with the property through some other element. + * Save it for later use. + */ + unrelated_labels = lappend_oid(unrelated_labels, labeloid); + } + } + + /* See if we can resolve the property in some other way. */ + if (!n) + { + bool prop_associated = false; + + foreach_oid(loid, unrelated_labels) + { + if (is_property_associated_with_label(loid, gpr->propid)) + { + prop_associated = true; + break; + } + } + + if (prop_associated) + { + /* + * The property is associated with at least one of the labels + * that satisfy given element pattern. If it's associated with + * the given element (through some other label), use + * corresponding value expression. Otherwise NULL. Ref. + * SQL/PGQ standard section 6.5 Property Reference, General + * Rule 2.b. + */ + n = get_element_property_expr(found_mapping->elemoid, gpr->propid, + mapping_factor->factorpos + 1); + + if (!n) + n = (Node *) makeNullConst(gpr->typeId, gpr->typmod, gpr->collation); + } + + } + + if (!n) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("property \"%s\" for element variable \"%s\" not found", + get_propgraph_property_name(gpr->propid), mapping_factor->variable)); + + return n; + } + + return expression_tree_mutator(node, replace_property_refs_mutator, context); +} + +static Node * +replace_property_refs(Oid propgraphid, Node *node, const List *mappings) +{ + struct replace_property_refs_context context; + + context.mappings = mappings; + context.propgraphid = propgraphid; + + return expression_tree_mutator(node, replace_property_refs_mutator, &context); +} + +/* + * Build join qualification expressions between edge and vertex tables. + */ +static List * +build_edge_vertex_link_quals(HeapTuple edgetup, int edgerti, int refrti, Oid refid, AttrNumber catalog_key_attnum, AttrNumber catalog_ref_attnum, AttrNumber catalog_eqop_attnum) +{ + List *quals = NIL; + Form_pg_propgraph_element pgeform; + Datum datum; + Datum *d1, + *d2, + *d3; + int n1, + n2, + n3; + ParseState *pstate = make_parsestate(NULL); + Oid refrelid = GetSysCacheOid1(PROPGRAPHELOID, Anum_pg_propgraph_element_pgerelid, ObjectIdGetDatum(refid)); + + pgeform = (Form_pg_propgraph_element) GETSTRUCT(edgetup); + + datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, edgetup, catalog_key_attnum); + deconstruct_array_builtin(DatumGetArrayTypeP(datum), INT2OID, &d1, NULL, &n1); + + datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, edgetup, catalog_ref_attnum); + deconstruct_array_builtin(DatumGetArrayTypeP(datum), INT2OID, &d2, NULL, &n2); + + datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, edgetup, catalog_eqop_attnum); + deconstruct_array_builtin(DatumGetArrayTypeP(datum), OIDOID, &d3, NULL, &n3); + + if (n1 != n2) + elog(ERROR, "array size key (%d) vs ref (%d) mismatch for element ID %u", catalog_key_attnum, catalog_ref_attnum, pgeform->oid); + if (n1 != n3) + elog(ERROR, "array size key (%d) vs operator (%d) mismatch for element ID %u", catalog_key_attnum, catalog_eqop_attnum, pgeform->oid); + + for (int i = 0; i < n1; i++) + { + AttrNumber keyattn = DatumGetInt16(d1[i]); + AttrNumber refattn = DatumGetInt16(d2[i]); + Oid eqop = DatumGetObjectId(d3[i]); + Var *keyvar; + Var *refvar; + Oid atttypid; + int32 atttypmod; + Oid attcoll; + HeapTuple tup; + Form_pg_operator opform; + List *args; + Oid actual_arg_types[2]; + Oid declared_arg_types[2]; + OpExpr *linkqual; + + get_atttypetypmodcoll(pgeform->pgerelid, keyattn, &atttypid, &atttypmod, &attcoll); + keyvar = makeVar(edgerti, keyattn, atttypid, atttypmod, attcoll, 0); + get_atttypetypmodcoll(refrelid, refattn, &atttypid, &atttypmod, &attcoll); + refvar = makeVar(refrti, refattn, atttypid, atttypmod, attcoll, 0); + + tup = SearchSysCache1(OPEROID, ObjectIdGetDatum(eqop)); + if (!HeapTupleIsValid(tup)) + elog(ERROR, "cache lookup failed for operator %u", eqop); + opform = (Form_pg_operator) GETSTRUCT(tup); + /* An equality operator is a binary operator returning boolean result. */ + Assert(opform->oprkind == 'b' + && RegProcedureIsValid(opform->oprcode) + && opform->oprresult == BOOLOID + && !get_func_retset(opform->oprcode)); + + /* + * Prepare operands and cast them to the types required by the + * equality operator. Similar to PK/FK quals, referenced vertex key is + * used as left operand and referencing edge key is used as right + * operand. + */ + args = list_make2(refvar, keyvar); + actual_arg_types[0] = exprType((Node *) refvar); + actual_arg_types[1] = exprType((Node *) keyvar); + declared_arg_types[0] = opform->oprleft; + declared_arg_types[1] = opform->oprright; + make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types); + + linkqual = makeNode(OpExpr); + linkqual->opno = opform->oid; + linkqual->opfuncid = opform->oprcode; + linkqual->opresulttype = opform->oprresult; + linkqual->opretset = false; + /* opcollid and inputcollid will be set by parse_collate.c */ + linkqual->args = args; + linkqual->location = -1; + + ReleaseSysCache(tup); + quals = lappend(quals, linkqual); + } + + assign_expr_collations(pstate, (Node *) quals); + + return quals; +} + +/* + * Check if the given property is associated with the given label. + * + * A label projects the same set of properties through every element it is + * associated with. Find any of the elements and return true if that element is + * associated with the given property. False otherwise. + */ +static bool +is_property_associated_with_label(Oid labeloid, Oid propoid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple tup; + bool associated = false; + + rel = table_open(PropgraphElementLabelRelationId, RowShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_pgellabelid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(labeloid)); + scan = systable_beginscan(rel, PropgraphElementLabelLabelIndexId, + true, NULL, 1, key); + + if (HeapTupleIsValid(tup = systable_getnext(scan))) + { + Form_pg_propgraph_element_label ele_label = (Form_pg_propgraph_element_label) GETSTRUCT(tup); + + associated = SearchSysCacheExists2(PROPGRAPHLABELPROP, + ObjectIdGetDatum(ele_label->oid), ObjectIdGetDatum(propoid)); + } + systable_endscan(scan); + table_close(rel, RowShareLock); + + return associated; +} + +/* + * If given element has the given property associated with it, through any of + * the associated labels, return value expression of the property. Otherwise + * NULL. + */ +static Node * +get_element_property_expr(Oid elemoid, Oid propoid, int rtindex) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple labeltup; + Node *n = NULL; + + rel = table_open(PropgraphElementLabelRelationId, RowShareLock); + ScanKeyInit(&key[0], + Anum_pg_propgraph_element_label_pgelelid, + BTEqualStrategyNumber, + F_OIDEQ, ObjectIdGetDatum(elemoid)); + scan = systable_beginscan(rel, PropgraphElementLabelElementLabelIndexId, + true, NULL, 1, key); + + while (HeapTupleIsValid(labeltup = systable_getnext(scan))) + { + Form_pg_propgraph_element_label ele_label = (Form_pg_propgraph_element_label) GETSTRUCT(labeltup); + + HeapTuple proptup = SearchSysCache2(PROPGRAPHLABELPROP, + ObjectIdGetDatum(ele_label->oid), ObjectIdGetDatum(propoid)); + + if (!proptup) + continue; + n = stringToNode(TextDatumGetCString(SysCacheGetAttrNotNull(PROPGRAPHLABELPROP, + proptup, Anum_pg_propgraph_label_property_plpexpr))); + ChangeVarNodes(n, 1, rtindex, 0); + + ReleaseSysCache(proptup); + break; + } + systable_endscan(scan); + table_close(rel, RowShareLock); + + return n; +} diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index f0bce5f9ed957..77b2c9bc622c2 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -3,7 +3,7 @@ * rewriteHandler.c * Primary module of query rewriter. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -36,6 +36,7 @@ #include "parser/parse_relation.h" #include "parser/parsetree.h" #include "rewrite/rewriteDefine.h" +#include "rewrite/rewriteGraphTable.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "rewrite/rewriteSearchCycle.h" @@ -96,8 +97,7 @@ static List *matchLocks(CmdType event, Relation relation, int varno, Query *parsetree, bool *hasUpdate); static Query *fireRIRrules(Query *parsetree, List *activeRIRs); static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist); -static Node *expand_generated_columns_internal(Node *node, Relation rel, int rt_index, - RangeTblEntry *rte, int result_relation); +static List *get_generated_columns(Relation rel, int rt_index, bool include_stored); /* @@ -173,6 +173,7 @@ AcquireRewriteLocks(Query *parsetree, switch (rte->rtekind) { case RTE_RELATION: + case RTE_GRAPH_TABLE: /* * Grab the appropriate lock type for the relation, and do not @@ -195,7 +196,7 @@ AcquireRewriteLocks(Query *parsetree, else lockmode = rte->rellockmode; - rel = table_open(rte->relid, lockmode); + rel = relation_open(rte->relid, lockmode); /* * While we have the relation open, update the RTE's relkind, @@ -203,7 +204,7 @@ AcquireRewriteLocks(Query *parsetree, */ rte->relkind = rel->rd_rel->relkind; - table_close(rel, NoLock); + relation_close(rel, NoLock); break; case RTE_JOIN: @@ -592,7 +593,10 @@ rewriteRuleAction(Query *parsetree, } } - /* OK, it's safe to combine the CTE lists */ + /* + * OK, it's safe to combine the CTE lists. Beware that RewriteQuery + * knows we concatenate the lists in this order. + */ sub_action->cteList = list_concat(sub_action->cteList, copyObject(parsetree->cteList)); /* ... and don't forget about the associated flags */ @@ -637,12 +641,46 @@ rewriteRuleAction(Query *parsetree, if ((event == CMD_INSERT || event == CMD_UPDATE) && sub_action->commandType != CMD_UTILITY) { + RangeTblEntry *new_rte = rt_fetch(new_varno, sub_action->rtable); + Relation new_rel; + List *gen_cols; + + /* + * The target list does not contain entries for generated columns + * (they are removed by rewriteTargetListIU), so we must build entries + * for them here, so that new.gen_col can be rewritten correctly. + */ + new_rel = relation_open(new_rte->relid, NoLock); + gen_cols = get_generated_columns(new_rel, new_varno, true); + relation_close(new_rel, NoLock); + + /* + * The generated column expressions refer to new.attribute, so they + * must be rewritten before they can be used as replacements. + */ + gen_cols = (List *) + ReplaceVarsFromTargetList((Node *) gen_cols, + new_varno, + 0, + new_rte, + parsetree->targetList, + sub_action->resultRelation, + (event == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, + current_varno, + &sub_action->hasSubLinks); + + /* + * Now rewrite new.attribute in sub_action, using both the target list + * and the rewritten generated column expressions. + */ sub_action = (Query *) ReplaceVarsFromTargetList((Node *) sub_action, new_varno, 0, - rt_fetch(new_varno, sub_action->rtable), - parsetree->targetList, + new_rte, + list_concat(gen_cols, parsetree->targetList), sub_action->resultRelation, (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : @@ -655,6 +693,19 @@ rewriteRuleAction(Query *parsetree, rule_action = sub_action; } + /* + * If rule_action is INSERT .. ON CONFLICT DO SELECT, the parser should + * have verified that it has a RETURNING clause, but we must also check + * that the triggering query has a RETURNING clause. + */ + if (rule_action->onConflict && + rule_action->onConflict->action == ONCONFLICT_SELECT && + (!rule_action->returningList || !parsetree->returningList)) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("ON CONFLICT DO SELECT requires a RETURNING clause"), + errdetail("A rule action is INSERT ... ON CONFLICT DO SELECT, which requires a RETURNING clause.")); + /* * If rule_action has a RETURNING clause, then either throw it away if the * triggering query has no RETURNING clause, or rewrite it to emit what @@ -923,8 +974,9 @@ rewriteTargetListIU(List *targetList, apply_default = true; /* - * Can only insert DEFAULT into generated columns, regardless of - * any OVERRIDING clauses. + * Can only insert DEFAULT into generated columns. (The + * OVERRIDING clause does not apply to generated columns, so we + * don't consider it here.) */ if (att_tup->attgenerated && !apply_default) { @@ -2028,6 +2080,16 @@ fireRIRrules(Query *parsetree, List *activeRIRs) rte = rt_fetch(rt_index, parsetree->rtable); + /* + * Convert GRAPH_TABLE clause into a subquery using relational + * operators. (This will change the rtekind to subquery, so it must + * be done before the subquery handling below.) + */ + if (rte->rtekind == RTE_GRAPH_TABLE) + { + parsetree = rewriteGraphTable(parsetree, rt_index); + } + /* * A subquery RTE can't have associated rules, so there's nothing to * do to this level of the query, but we must recurse into the @@ -2099,7 +2161,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs) * We can use NoLock here since either the parser or * AcquireRewriteLocks should have locked the rel already. */ - rel = table_open(rte->relid, NoLock); + rel = relation_open(rte->relid, NoLock); /* * Collect the RIR rules that we must apply @@ -2209,7 +2271,7 @@ fireRIRrules(Query *parsetree, List *activeRIRs) rte->relkind != RELKIND_PARTITIONED_TABLE)) continue; - rel = table_open(rte->relid, NoLock); + rel = relation_open(rte->relid, NoLock); /* * Fetch any new security quals that must be applied to this RTE. @@ -2339,18 +2401,50 @@ CopyAndAddInvertedQual(Query *parsetree, ChangeVarNodes(new_qual, PRS2_OLD_VARNO, rt_index, 0); /* Fix references to NEW */ if (event == CMD_INSERT || event == CMD_UPDATE) + { + RangeTblEntry *rte = rt_fetch(rt_index, parsetree->rtable); + Relation rel; + List *gen_cols; + + /* + * As in rewriteRuleAction, build entries for generated columns so + * that new.gen_col in the rule qualification can be rewritten + * correctly. + */ + rel = relation_open(rte->relid, NoLock); + gen_cols = get_generated_columns(rel, PRS2_NEW_VARNO, true); + relation_close(rel, NoLock); + + /* + * The generated column expressions refer to new.attribute, so they + * must be rewritten before they can be used as replacements. + */ + gen_cols = (List *) + ReplaceVarsFromTargetList((Node *) gen_cols, + PRS2_NEW_VARNO, + 0, + rte, + parsetree->targetList, + parsetree->resultRelation, + (event == CMD_UPDATE) ? + REPLACEVARS_CHANGE_VARNO : + REPLACEVARS_SUBSTITUTE_NULL, + rt_index, + &parsetree->hasSubLinks); + new_qual = ReplaceVarsFromTargetList(new_qual, PRS2_NEW_VARNO, 0, - rt_fetch(rt_index, - parsetree->rtable), - parsetree->targetList, + rte, + list_concat(gen_cols, + parsetree->targetList), parsetree->resultRelation, (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : REPLACEVARS_SUBSTITUTE_NULL, rt_index, &parsetree->hasSubLinks); + } /* And attach the fixed qual */ AddInvertedQual(parsetree, new_qual); @@ -2616,7 +2710,7 @@ view_col_is_auto_updatable(RangeTblRef *rtr, TargetEntry *tle) * view_query_is_auto_updatable - test whether the specified view definition * represents an auto-updatable view. Returns NULL (if the view can be updated) * or a message string giving the reason that it cannot be. - + * * The returned string has not been translated; if it is shown as an error * message, the caller should apply _() to translate it. * @@ -3428,7 +3522,7 @@ rewriteTargetView(Query *parsetree, Relation view) * already have the right lock!) Since it will become the query target * relation, RowExclusiveLock is always the right thing. */ - base_rel = table_open(base_rte->relid, RowExclusiveLock); + base_rel = relation_open(base_rte->relid, RowExclusiveLock); /* * While we have the relation open, update the RTE's relkind, just in case @@ -3639,11 +3733,12 @@ rewriteTargetView(Query *parsetree, Relation view) } /* - * For INSERT .. ON CONFLICT .. DO UPDATE, we must also update assorted - * stuff in the onConflict data structure. + * For INSERT .. ON CONFLICT .. DO SELECT/UPDATE, we must also update + * assorted stuff in the onConflict data structure. */ if (parsetree->onConflict && - parsetree->onConflict->action == ONCONFLICT_UPDATE) + (parsetree->onConflict->action == ONCONFLICT_UPDATE || + parsetree->onConflict->action == ONCONFLICT_SELECT)) { Index old_exclRelIndex, new_exclRelIndex; @@ -3652,9 +3747,8 @@ rewriteTargetView(Query *parsetree, Relation view) List *tmp_tlist; /* - * Like the INSERT/UPDATE code above, update the resnos in the - * auxiliary UPDATE targetlist to refer to columns of the base - * relation. + * For ON CONFLICT DO UPDATE, update the resnos in the auxiliary + * UPDATE targetlist to refer to columns of the base relation. */ foreach(lc, parsetree->onConflict->onConflictSet) { @@ -3673,7 +3767,7 @@ rewriteTargetView(Query *parsetree, Relation view) } /* - * Also, create a new RTE for the EXCLUDED pseudo-relation, using the + * Create a new RTE for the EXCLUDED pseudo-relation, using the * query's new base rel (which may well have a different column list * from the view, hence we need a new column alias list). This should * match transformOnConflictClause. In particular, note that the @@ -3728,6 +3822,30 @@ rewriteTargetView(Query *parsetree, Relation view) &parsetree->hasSubLinks); } + if (parsetree->forPortionOf && parsetree->commandType == CMD_UPDATE) + { + /* + * Like the INSERT/UPDATE code above, update the resnos in the + * auxiliary UPDATE targetlist to refer to columns of the base + * relation. + */ + foreach(lc, parsetree->forPortionOf->rangeTargetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + TargetEntry *view_tle; + + if (tle->resjunk) + continue; + + view_tle = get_tle_by_resno(view_targetlist, tle->resno); + if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var)) + tle->resno = ((Var *) view_tle->expr)->varattno; + else + elog(ERROR, "attribute number %d not found in view targetlist", + tle->resno); + } + } + /* * For UPDATE/DELETE/MERGE, pull up any WHERE quals from the view. We * know that any Vars in the quals must reference the one base relation, @@ -3780,7 +3898,7 @@ rewriteTargetView(Query *parsetree, Relation view) parsetree->hasSubLinks = checkExprHasSubLink(viewqual); } else - AddQual(parsetree, (Node *) viewqual); + AddQual(parsetree, viewqual); } /* @@ -3871,9 +3989,13 @@ rewriteTargetView(Query *parsetree, Relation view) * orig_rt_length is the length of the originating query's rtable, for product * queries created by fireRules(), and 0 otherwise. This is used to skip any * already-processed VALUES RTEs from the original query. + * + * num_ctes_processed is the number of CTEs at the end of the query's cteList + * that have already been rewritten, and must not be rewritten again. */ static List * -RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) +RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length, + int num_ctes_processed) { CmdType event = parsetree->commandType; bool instead = false; @@ -3887,17 +4009,29 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) * First, recursively process any insert/update/delete/merge statements in * WITH clauses. (We have to do this first because the WITH clauses may * get copied into rule actions below.) + * + * Any new WITH clauses from rule actions are processed when we recurse + * into product queries below. However, when recursing, we must take care + * to avoid rewriting a CTE query more than once (because expanding + * generated columns in the targetlist more than once would fail). Since + * new CTEs from product queries are added to the start of the list (see + * rewriteRuleAction), we just skip the last num_ctes_processed items. */ foreach(lc1, parsetree->cteList) { CommonTableExpr *cte = lfirst_node(CommonTableExpr, lc1); Query *ctequery = castNode(Query, cte->ctequery); + int i = foreach_current_index(lc1); List *newstuff; + /* Skip already-processed CTEs at the end of the list */ + if (i >= list_length(parsetree->cteList) - num_ctes_processed) + break; + if (ctequery->commandType == CMD_SELECT) continue; - newstuff = RewriteQuery(ctequery, rewrite_events, 0); + newstuff = RewriteQuery(ctequery, rewrite_events, 0, 0); /* * Currently we can only handle unconditional, single-statement DO @@ -3957,6 +4091,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) errmsg("multi-statement DO INSTEAD rules are not supported for data-modifying statements in WITH"))); } } + num_ctes_processed = list_length(parsetree->cteList); /* * If the statement is an insert, update, delete, or merge, adjust its @@ -3987,7 +4122,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) * We can use NoLock here since either the parser or * AcquireRewriteLocks should have locked the rel already. */ - rt_entry_relation = table_open(rt_entry->relid, NoLock); + rt_entry_relation = relation_open(rt_entry->relid, NoLock); /* * Rewrite the targetlist as needed for the command type. @@ -4067,6 +4202,37 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) else if (event == CMD_UPDATE) { Assert(parsetree->override == OVERRIDING_NOT_SET); + + if (parsetree->forPortionOf) + { + /* + * Don't add FOR PORTION OF details until we're done rewriting + * a view update, so that we don't add the same qual and TLE + * on the recursion. + * + * Views don't need to do anything special here to remap Vars; + * that is handled by the tree walker. + */ + if (rt_entry_relation->rd_rel->relkind != RELKIND_VIEW) + { + ListCell *tl; + + /* + * Add qual: UPDATE FOR PORTION OF should be limited to + * rows that overlap the target range. + */ + AddQual(parsetree, parsetree->forPortionOf->overlapsExpr); + + /* Update FOR PORTION OF column(s) automatically. */ + foreach(tl, parsetree->forPortionOf->rangeTargetList) + { + TargetEntry *tle = (TargetEntry *) lfirst(tl); + + parsetree->targetList = lappend(parsetree->targetList, tle); + } + } + } + parsetree->targetList = rewriteTargetListIU(parsetree->targetList, parsetree->commandType, @@ -4112,7 +4278,25 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) } else if (event == CMD_DELETE) { - /* Nothing to do here */ + if (parsetree->forPortionOf) + { + /* + * Don't add FOR PORTION OF details until we're done rewriting + * a view delete, so that we don't add the same qual on the + * recursion. + * + * Views don't need to do anything special here to remap Vars; + * that is handled by the tree walker. + */ + if (rt_entry_relation->rd_rel->relkind != RELKIND_VIEW) + { + /* + * Add qual: DELETE FOR PORTION OF should be limited to + * rows that overlap the target range. + */ + AddQual(parsetree, parsetree->forPortionOf->overlapsExpr); + } + } } else elog(ERROR, "unrecognized commandType: %d", (int) event); @@ -4266,7 +4450,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) RelationGetRelationName(rt_entry_relation)))); } - rev = (rewrite_event *) palloc(sizeof(rewrite_event)); + rev = palloc_object(rewrite_event); rev->relation = RelationGetRelid(rt_entry_relation); rev->event = event; rewrite_events = lappend(rewrite_events, rev); @@ -4288,7 +4472,8 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) newstuff = RewriteQuery(pt, rewrite_events, pt == parsetree ? orig_rt_length : - product_orig_rt_length); + product_orig_rt_length, + num_ctes_processed); rewritten = list_concat(rewritten, newstuff); } @@ -4410,36 +4595,31 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) /* - * Expand virtual generated columns - * - * If the table contains virtual generated columns, build a target list - * containing the expanded expressions and use ReplaceVarsFromTargetList() to - * do the replacements. + * Get a table's generated columns * - * Vars matching rt_index at the current query level are replaced by the - * virtual generated column expressions from rel, if there are any. + * If include_stored is true, both stored and virtual generated columns are + * returned. Otherwise, only virtual generated columns are returned. * - * The caller must also provide rte, the RTE describing the target relation, - * in order to handle any whole-row Vars referencing the target, and - * result_relation, the index of the result relation, if this is part of an - * INSERT/UPDATE/DELETE/MERGE query. + * Returns a list of TargetEntry, one for each generated column, containing + * the attribute numbers and generation expressions. */ -static Node * -expand_generated_columns_internal(Node *node, Relation rel, int rt_index, - RangeTblEntry *rte, int result_relation) +static List * +get_generated_columns(Relation rel, int rt_index, bool include_stored) { + List *gen_cols = NIL; TupleDesc tupdesc; tupdesc = RelationGetDescr(rel); - if (tupdesc->constr && tupdesc->constr->has_generated_virtual) + if (tupdesc->constr && + (tupdesc->constr->has_generated_virtual || + (include_stored && tupdesc->constr->has_generated_stored))) { - List *tlist = NIL; - for (int i = 0; i < tupdesc->natts; i++) { Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL || + (include_stored && attr->attgenerated == ATTRIBUTE_GENERATED_STORED)) { Node *defexpr; TargetEntry *te; @@ -4448,19 +4628,12 @@ expand_generated_columns_internal(Node *node, Relation rel, int rt_index, ChangeVarNodes(defexpr, 1, rt_index, 0); te = makeTargetEntry((Expr *) defexpr, i + 1, 0, false); - tlist = lappend(tlist, te); + gen_cols = lappend(gen_cols, te); } } - - Assert(list_length(tlist) > 0); - - node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist, - result_relation, - REPLACEVARS_CHANGE_VARNO, rt_index, - NULL); } - return node; + return gen_cols; } /* @@ -4477,6 +4650,7 @@ expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index) if (tupdesc->constr && tupdesc->constr->has_generated_virtual) { RangeTblEntry *rte; + List *vcols; rte = makeNode(RangeTblEntry); /* eref needs to be set, but the actual name doesn't matter */ @@ -4484,14 +4658,26 @@ expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index) rte->rtekind = RTE_RELATION; rte->relid = RelationGetRelid(rel); - node = expand_generated_columns_internal(node, rel, rt_index, rte, 0); + vcols = get_generated_columns(rel, rt_index, false); + + if (vcols) + { + /* + * Passing NULL for outer_hasSubLinks is safe because generation + * expressions cannot contain SubLinks, so the replacement cannot + * introduce any. + */ + node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, vcols, 0, + REPLACEVARS_CHANGE_VARNO, rt_index, + NULL); + } } return node; } /* - * Build the generation expression for the virtual generated column. + * Build the generation expression for a generated column. * * Error out if there is no generation expression found for the given column. */ @@ -4503,8 +4689,11 @@ build_generation_expression(Relation rel, int attrno) Node *defexpr; Oid attcollid; - Assert(rd_att->constr && rd_att->constr->has_generated_virtual); - Assert(att_tup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL); + Assert(rd_att->constr && + (rd_att->constr->has_generated_virtual || + rd_att->constr->has_generated_stored)); + Assert(att_tup->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL || + att_tup->attgenerated == ATTRIBUTE_GENERATED_STORED); defexpr = build_column_default(rel, attrno); if (defexpr == NULL) @@ -4544,7 +4733,7 @@ build_generation_expression(Relation rel, int attrno) List * QueryRewrite(Query *parsetree) { - uint64 input_query_id = parsetree->queryId; + int64 input_query_id = parsetree->queryId; List *querylist; List *results; ListCell *l; @@ -4563,7 +4752,7 @@ QueryRewrite(Query *parsetree) * * Apply all non-SELECT rules possibly getting 0 or many queries */ - querylist = RewriteQuery(parsetree, NIL, 0); + querylist = RewriteQuery(parsetree, NIL, 0, 0); /* * Step 2 diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c index cd786aa4112b5..9aa7ef60475db 100644 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -2,7 +2,7 @@ * * rewriteManip.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -13,6 +13,7 @@ */ #include "postgres.h" +#include "access/attmap.h" #include "catalog/pg_type.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -542,8 +543,6 @@ offset_relid_set(Relids relids, int offset) * (identified by sublevels_up and rt_index), and change their varno fields * to 'new_index'. The varnosyn fields are changed too. Also, adjust other * nodes that contain rangetable indexes, such as RangeTblRef and JoinExpr. - * Specifying 'change_RangeTblRef' to false allows skipping RangeTblRef. - * See ChangeVarNodesExtended for details. * * NOTE: although this has the form of a walker, we cheat and modify the * nodes in-place. The given expression tree should have been copied @@ -664,17 +663,16 @@ ChangeVarNodes_walker(Node *node, ChangeVarNodes_context *context) } /* - * ChangeVarNodesExtended - similar to ChangeVarNodes, but with an additional + * ChangeVarNodesExtended - similar to ChangeVarNodes, but with an additional * 'callback' param * - * ChangeVarNodes changes a given node and all of its underlying nodes. - * This version of function additionally takes a callback, which has a - * chance to process a node before ChangeVarNodes_walker. A callback - * returns a boolean value indicating if given node should be skipped from - * further processing by ChangeVarNodes_walker. The callback is called - * only for expressions and other children nodes of a Query processed by - * a walker. Initial processing of the root Query doesn't involve the - * callback. + * ChangeVarNodes changes a given node and all of its underlying nodes. This + * version of function additionally takes a callback, which has a chance to + * process a node before ChangeVarNodes_walker. A callback returns a boolean + * value indicating if the given node should be skipped from further processing + * by ChangeVarNodes_walker. The callback is called only for expressions and + * other children nodes of a Query processed by a walker. Initial processing + * of the root Query node doesn't invoke the callback. */ void ChangeVarNodesExtended(Node *node, int rt_index, int new_index, @@ -739,16 +737,27 @@ ChangeVarNodes(Node *node, int rt_index, int new_index, int sublevels_up) } /* - * ChangeVarNodesWalkExpression - process expression within the custom - * callback provided to the - * ChangeVarNodesExtended. + * ChangeVarNodesWalkExpression - process subexpression within a callback + * function passed to ChangeVarNodesExtended. + * + * This is intended to be used by a callback that needs to recursively + * process subexpressions of some node being visited by an outer + * ChangeVarNodesExtended call, instead of relying on ChangeVarNodes_walker's + * default recursion. We invoke ChangeVarNodes_walker directly rather than + * via expression_tree_walker, because expression_tree_walker only visits + * child nodes and would fail to process the passed node itself -- + * for example, a bare Var node would not get its varno adjusted. + * + * Because this calls ChangeVarNodes_walker directly, if the passed node is + * a Query, it will be treated as a sub-Query: sublevels_up is incremented + * before recursing into it, and Query-level fields (resultRelation, + * mergeTargetRelation, rowMarks, etc.) will not be adjusted. Do not apply + * this to a top-level Query node; use ChangeVarNodesExtended for that. */ bool ChangeVarNodesWalkExpression(Node *node, ChangeVarNodes_context *context) { - return expression_tree_walker(node, - ChangeVarNodes_walker, - (void *) context); + return ChangeVarNodes_walker(node, context); } /* @@ -1505,25 +1514,6 @@ replace_rte_variables_mutator(Node *node, } /* otherwise fall through to copy the var normally */ } - else if (IsA(node, CurrentOfExpr)) - { - CurrentOfExpr *cexpr = (CurrentOfExpr *) node; - - if (cexpr->cvarno == context->target_varno && - context->sublevels_up == 0) - { - /* - * We get here if a WHERE CURRENT OF expression turns out to apply - * to a view. Someday we might be able to translate the - * expression to apply to an underlying table of the view, but - * right now it's not implemented. - */ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("WHERE CURRENT OF on a view is not implemented"))); - } - /* otherwise fall through to copy the expr normally */ - } else if (IsA(node, Query)) { /* Recurse into RTE subquery or not-yet-planned sublink subquery */ @@ -1593,7 +1583,7 @@ map_variable_attnos_mutator(Node *node, var->varlevelsup == context->sublevels_up) { /* Found a matching variable, make the substitution */ - Var *newvar = (Var *) palloc(sizeof(Var)); + Var *newvar = palloc_object(Var); int attno = var->varattno; *newvar = *var; /* initially copy all fields of the Var */ @@ -1664,7 +1654,7 @@ map_variable_attnos_mutator(Node *node, context->to_rowtype != var->vartype) { ConvertRowtypeExpr *newnode; - Var *newvar = (Var *) palloc(sizeof(Var)); + Var *newvar = palloc_object(Var); /* whole-row variable, warn caller */ *(context->found_whole_row) = true; @@ -1677,7 +1667,7 @@ map_variable_attnos_mutator(Node *node, /* Var itself is changed to the requested type. */ newvar->vartype = context->to_rowtype; - newnode = (ConvertRowtypeExpr *) palloc(sizeof(ConvertRowtypeExpr)); + newnode = palloc_object(ConvertRowtypeExpr); *newnode = *r; /* initially copy all fields of the CRE */ newnode->arg = (Expr *) newvar; @@ -1771,7 +1761,7 @@ typedef struct } ReplaceVarsFromTargetList_context; static Node * -ReplaceVarsFromTargetList_callback(Var *var, +ReplaceVarsFromTargetList_callback(const Var *var, replace_rte_variables_context *context) { ReplaceVarsFromTargetList_context *rcon = (ReplaceVarsFromTargetList_context *) context->callback_arg; @@ -1792,7 +1782,7 @@ ReplaceVarsFromTargetList_callback(Var *var, } Node * -ReplaceVarFromTargetList(Var *var, +ReplaceVarFromTargetList(const Var *var, RangeTblEntry *target_rte, List *targetlist, int result_relation, @@ -1878,11 +1868,14 @@ ReplaceVarFromTargetList(Var *var, break; case REPLACEVARS_CHANGE_VARNO: - var = copyObject(var); - var->varno = nomatch_varno; - var->varlevelsup = 0; - /* we leave the syntactic referent alone */ - return (Node *) var; + { + Var *newvar = copyObject(var); + + newvar->varno = nomatch_varno; + newvar->varlevelsup = 0; + /* we leave the syntactic referent alone */ + return (Node *) newvar; + } case REPLACEVARS_SUBSTITUTE_NULL: { diff --git a/src/backend/rewrite/rewriteRemove.c b/src/backend/rewrite/rewriteRemove.c index 292c6e52cfc57..215d27bdd1c4b 100644 --- a/src/backend/rewrite/rewriteRemove.c +++ b/src/backend/rewrite/rewriteRemove.c @@ -3,7 +3,7 @@ * rewriteRemove.c * routines for removing rewrite rules * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/rewrite/rewriteSearchCycle.c b/src/backend/rewrite/rewriteSearchCycle.c index 19b89dee0d096..7594307281748 100644 --- a/src/backend/rewrite/rewriteSearchCycle.c +++ b/src/backend/rewrite/rewriteSearchCycle.c @@ -3,7 +3,7 @@ * rewriteSearchCycle.c * Support for rewriting SEARCH and CYCLE clauses. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -282,8 +282,8 @@ rewriteSearchAndCycle(CommonTableExpr *cte) newrte = makeNode(RangeTblEntry); newrte->rtekind = RTE_SUBQUERY; - newrte->alias = makeAlias("*TLOCRN*", cte->ctecolnames); - newrte->eref = newrte->alias; + newrte->alias = NULL; + newrte->eref = makeAlias("*TLOCRN*", cte->ctecolnames); newsubquery = copyObject(rte1->subquery); IncrementVarSublevelsUp((Node *) newsubquery, 1, 1); newrte->subquery = newsubquery; @@ -320,7 +320,7 @@ rewriteSearchAndCycle(CommonTableExpr *cte) if (cte->search_clause->search_breadth_first) { search_col_rowexpr->args = lcons(makeConst(INT8OID, -1, InvalidOid, sizeof(int64), - Int64GetDatum(0), false, FLOAT8PASSBYVAL), + Int64GetDatum(0), false, true), search_col_rowexpr->args); search_col_rowexpr->colnames = lcons(makeString("*DEPTH*"), search_col_rowexpr->colnames); texpr = (Expr *) search_col_rowexpr; @@ -379,8 +379,8 @@ rewriteSearchAndCycle(CommonTableExpr *cte) ewcl = lappend(ewcl, makeString(cte->cycle_clause->cycle_mark_column)); ewcl = lappend(ewcl, makeString(cte->cycle_clause->cycle_path_column)); } - newrte->alias = makeAlias("*TROCRN*", ewcl); - newrte->eref = newrte->alias; + newrte->alias = NULL; + newrte->eref = makeAlias("*TROCRN*", ewcl); /* * Find the reference to the recursive CTE in the right UNION subquery's diff --git a/src/backend/rewrite/rewriteSupport.c b/src/backend/rewrite/rewriteSupport.c index e401c19594902..fffded88a6e69 100644 --- a/src/backend/rewrite/rewriteSupport.c +++ b/src/backend/rewrite/rewriteSupport.c @@ -3,7 +3,7 @@ * rewriteSupport.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c index 4dad384d04da3..e88a1bc1a89b4 100644 --- a/src/backend/rewrite/rowsecurity.c +++ b/src/backend/rewrite/rowsecurity.c @@ -29,7 +29,7 @@ * in the current environment, but that may change if the row_security GUC or * the current role changes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California */ #include "postgres.h" @@ -301,40 +301,48 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, } /* - * For INSERT ... ON CONFLICT DO UPDATE we need additional policy - * checks for the UPDATE which may be applied to the same RTE. + * For INSERT ... ON CONFLICT DO SELECT/UPDATE we need additional + * policy checks for the SELECT/UPDATE which may be applied to the + * same RTE. */ - if (commandType == CMD_INSERT && - root->onConflict && root->onConflict->action == ONCONFLICT_UPDATE) + if (commandType == CMD_INSERT && root->onConflict && + (root->onConflict->action == ONCONFLICT_UPDATE || + root->onConflict->action == ONCONFLICT_SELECT)) { - List *conflict_permissive_policies; - List *conflict_restrictive_policies; + List *conflict_permissive_policies = NIL; + List *conflict_restrictive_policies = NIL; List *conflict_select_permissive_policies = NIL; List *conflict_select_restrictive_policies = NIL; - /* Get the policies that apply to the auxiliary UPDATE */ - get_policies_for_relation(rel, CMD_UPDATE, user_id, - &conflict_permissive_policies, - &conflict_restrictive_policies); - - /* - * Enforce the USING clauses of the UPDATE policies using WCOs - * rather than security quals. This ensures that an error is - * raised if the conflicting row cannot be updated due to RLS, - * rather than the change being silently dropped. - */ - add_with_check_options(rel, rt_index, - WCO_RLS_CONFLICT_CHECK, - conflict_permissive_policies, - conflict_restrictive_policies, - withCheckOptions, - hasSubLinks, - true); + if (perminfo->requiredPerms & ACL_UPDATE) + { + /* + * Get the policies that apply to the auxiliary UPDATE or + * SELECT FOR UPDATE/SHARE. + */ + get_policies_for_relation(rel, CMD_UPDATE, user_id, + &conflict_permissive_policies, + &conflict_restrictive_policies); + + /* + * Enforce the USING clauses of the UPDATE policies using WCOs + * rather than security quals. This ensures that an error is + * raised if the conflicting row cannot be updated/locked due + * to RLS, rather than the change being silently dropped. + */ + add_with_check_options(rel, rt_index, + WCO_RLS_CONFLICT_CHECK, + conflict_permissive_policies, + conflict_restrictive_policies, + withCheckOptions, + hasSubLinks, + true); + } /* * Get and add ALL/SELECT policies, as WCO_RLS_CONFLICT_CHECK WCOs - * to ensure they are considered when taking the UPDATE path of an - * INSERT .. ON CONFLICT DO UPDATE, if SELECT rights are required + * to ensure they are considered when taking the SELECT/UPDATE + * path of an INSERT .. ON CONFLICT, if SELECT rights are required * for this relation, also as WCO policies, again, to avoid * silently dropping data. See above. */ @@ -352,29 +360,36 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, true); } - /* Enforce the WITH CHECK clauses of the UPDATE policies */ - add_with_check_options(rel, rt_index, - WCO_RLS_UPDATE_CHECK, - conflict_permissive_policies, - conflict_restrictive_policies, - withCheckOptions, - hasSubLinks, - false); - /* - * Add ALL/SELECT policies as WCO_RLS_UPDATE_CHECK WCOs, to ensure - * that the final updated row is visible when taking the UPDATE - * path of an INSERT .. ON CONFLICT DO UPDATE, if SELECT rights - * are required for this relation. + * For INSERT .. ON CONFLICT DO UPDATE, add additional policies to + * be checked when the auxiliary UPDATE is executed. */ - if (perminfo->requiredPerms & ACL_SELECT) + if (root->onConflict->action == ONCONFLICT_UPDATE) + { + /* Enforce the WITH CHECK clauses of the UPDATE policies */ add_with_check_options(rel, rt_index, WCO_RLS_UPDATE_CHECK, - conflict_select_permissive_policies, - conflict_select_restrictive_policies, + conflict_permissive_policies, + conflict_restrictive_policies, withCheckOptions, hasSubLinks, - true); + false); + + /* + * Add ALL/SELECT policies as WCO_RLS_UPDATE_CHECK WCOs, to + * ensure that the final updated row is visible when taking + * the UPDATE path of an INSERT .. ON CONFLICT, if SELECT + * rights are required for this relation. + */ + if (perminfo->requiredPerms & ACL_SELECT) + add_with_check_options(rel, rt_index, + WCO_RLS_UPDATE_CHECK, + conflict_select_permissive_policies, + conflict_select_restrictive_policies, + withCheckOptions, + hasSubLinks, + true); + } } } @@ -398,8 +413,8 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index, * XXX We are setting up USING quals as WITH CHECK. If RLS prohibits * UPDATE/DELETE on the target row, we shall throw an error instead of * silently ignoring the row. This is different than how normal - * UPDATE/DELETE works and more in line with INSERT ON CONFLICT DO UPDATE - * handling. + * UPDATE/DELETE works and more in line with INSERT ON CONFLICT DO + * SELECT/UPDATE handling. */ if (commandType == CMD_MERGE) { @@ -784,9 +799,9 @@ add_security_quals(int rt_index, * added by an INSERT or UPDATE are consistent with the specified RLS * policies. Normally new data must satisfy the WITH CHECK clauses from the * policies. If a policy has no explicit WITH CHECK clause, its USING clause - * is used instead. In the special case of an UPDATE arising from an - * INSERT ... ON CONFLICT DO UPDATE, existing records are first checked using - * a WCO_RLS_CONFLICT_CHECK WithCheckOption, which always uses the USING + * is used instead. In the special case of a SELECT or UPDATE arising from an + * INSERT ... ON CONFLICT DO SELECT/UPDATE, existing records are first checked + * using a WCO_RLS_CONFLICT_CHECK WithCheckOption, which always uses the USING * clauses from RLS policies. * * New WCOs are added to withCheckOptions, and hasSubLinks is set to true if diff --git a/src/backend/snowball/Makefile b/src/backend/snowball/Makefile index 0398c9bb14ceb..ecfae121565bb 100644 --- a/src/backend/snowball/Makefile +++ b/src/backend/snowball/Makefile @@ -27,6 +27,7 @@ OBJS += \ stem_ISO_8859_1_catalan.o \ stem_ISO_8859_1_danish.o \ stem_ISO_8859_1_dutch.o \ + stem_ISO_8859_1_dutch_porter.o \ stem_ISO_8859_1_english.o \ stem_ISO_8859_1_finnish.o \ stem_ISO_8859_1_french.o \ @@ -40,6 +41,7 @@ OBJS += \ stem_ISO_8859_1_spanish.o \ stem_ISO_8859_1_swedish.o \ stem_ISO_8859_2_hungarian.o \ + stem_ISO_8859_2_polish.o \ stem_KOI8_R_russian.o \ stem_UTF_8_arabic.o \ stem_UTF_8_armenian.o \ @@ -47,7 +49,9 @@ OBJS += \ stem_UTF_8_catalan.o \ stem_UTF_8_danish.o \ stem_UTF_8_dutch.o \ + stem_UTF_8_dutch_porter.o \ stem_UTF_8_english.o \ + stem_UTF_8_esperanto.o \ stem_UTF_8_estonian.o \ stem_UTF_8_finnish.o \ stem_UTF_8_french.o \ @@ -61,6 +65,7 @@ OBJS += \ stem_UTF_8_lithuanian.o \ stem_UTF_8_nepali.o \ stem_UTF_8_norwegian.o \ + stem_UTF_8_polish.o \ stem_UTF_8_porter.o \ stem_UTF_8_portuguese.o \ stem_UTF_8_romanian.o \ diff --git a/src/backend/snowball/README b/src/backend/snowball/README index 2e41bee11423e..d4ababe9b38ed 100644 --- a/src/backend/snowball/README +++ b/src/backend/snowball/README @@ -29,27 +29,28 @@ We choose to include the derived files in the PostgreSQL distribution because most installations will not have the Snowball compiler available. We are currently synced with the Snowball git commit -d19326ac6c1b9a417fc872f7c2f845265a5e9ece -of 2025-02-19. +2d2e312df56f2ede014a4ffb3e91e6dea43c24be +of 2025-12-15. To update the PostgreSQL sources from a new Snowball version: 0. If you didn't do it already, "make -C snowball". 1. Copy the *.c files in snowball/src_c/ to src/backend/snowball/libstemmer -with replacement of "../runtime/header.h" by "header.h", for example +with replacement of "../runtime/snowball_runtime.h" by "snowball_runtime.h", +for example for f in .../snowball/src_c/*.c do - sed 's|\.\./runtime/header\.h|header.h|' $f >libstemmer/`basename $f` + sed 's|\.\./runtime/snowball_runtime\.h|snowball_runtime.h|' $f >libstemmer/`basename $f` done Do not copy stemmers that are listed in their libstemmer/modules.txt as -nonstandard, such as "kraaij_pohlmann" or "lovins". +nonstandard, such as "lovins". -2. Copy the *.c files in snowball/runtime/ to -src/backend/snowball/libstemmer, and edit them to remove direct inclusions -of system headers such as --- they should only include "header.h". +2. Copy the *.c files in snowball/runtime/ to src/backend/snowball/libstemmer, +and edit them to remove direct inclusions of system headers such as +--- they should only include "snowball_runtime.h". (This removal avoids portability problems on some platforms where is sensitive to largefile compilation options.) @@ -61,9 +62,12 @@ stemmers. 4. Check whether any stemmer modules have been added or removed. If so, edit the OBJS list in Makefile, the dict_snowball_sources list in meson.build, the list of #include's and the stemmer_modules[] table in dict_snowball.c, +the list of valid language parameters in the documentation in textsearch.sgml, and the sample \dFd output in the documentation in textsearch.sgml. You might also need to change the @languages array in snowball_create.pl -and the tsearch_config_languages[] table in initdb.c. +and the tsearch_config_languages[] table in initdb.c. Typically, the latter +two should be extended when a stemmer for an entirely new language is added, +but alternative stemmers for existing languages are not represented in them. 5. The various stopword files in stopwords/ must be downloaded individually from pages on the snowballstem.org website. diff --git a/src/backend/snowball/dict_snowball.c b/src/backend/snowball/dict_snowball.c index e2b811a3806ec..182bd156995e1 100644 --- a/src/backend/snowball/dict_snowball.c +++ b/src/backend/snowball/dict_snowball.c @@ -3,7 +3,7 @@ * dict_snowball.c * Snowball dictionary * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/snowball/dict_snowball.c @@ -26,12 +26,13 @@ #undef MININT #endif -/* Now we can include the original Snowball header.h */ -#include "snowball/libstemmer/header.h" +/* Now we can include the original Snowball snowball_runtime.h */ +#include "snowball/libstemmer/snowball_runtime.h" #include "snowball/libstemmer/stem_ISO_8859_1_basque.h" #include "snowball/libstemmer/stem_ISO_8859_1_catalan.h" #include "snowball/libstemmer/stem_ISO_8859_1_danish.h" #include "snowball/libstemmer/stem_ISO_8859_1_dutch.h" +#include "snowball/libstemmer/stem_ISO_8859_1_dutch_porter.h" #include "snowball/libstemmer/stem_ISO_8859_1_english.h" #include "snowball/libstemmer/stem_ISO_8859_1_finnish.h" #include "snowball/libstemmer/stem_ISO_8859_1_french.h" @@ -45,6 +46,7 @@ #include "snowball/libstemmer/stem_ISO_8859_1_spanish.h" #include "snowball/libstemmer/stem_ISO_8859_1_swedish.h" #include "snowball/libstemmer/stem_ISO_8859_2_hungarian.h" +#include "snowball/libstemmer/stem_ISO_8859_2_polish.h" #include "snowball/libstemmer/stem_KOI8_R_russian.h" #include "snowball/libstemmer/stem_UTF_8_arabic.h" #include "snowball/libstemmer/stem_UTF_8_armenian.h" @@ -52,7 +54,9 @@ #include "snowball/libstemmer/stem_UTF_8_catalan.h" #include "snowball/libstemmer/stem_UTF_8_danish.h" #include "snowball/libstemmer/stem_UTF_8_dutch.h" +#include "snowball/libstemmer/stem_UTF_8_dutch_porter.h" #include "snowball/libstemmer/stem_UTF_8_english.h" +#include "snowball/libstemmer/stem_UTF_8_esperanto.h" #include "snowball/libstemmer/stem_UTF_8_estonian.h" #include "snowball/libstemmer/stem_UTF_8_finnish.h" #include "snowball/libstemmer/stem_UTF_8_french.h" @@ -66,6 +70,7 @@ #include "snowball/libstemmer/stem_UTF_8_lithuanian.h" #include "snowball/libstemmer/stem_UTF_8_nepali.h" #include "snowball/libstemmer/stem_UTF_8_norwegian.h" +#include "snowball/libstemmer/stem_UTF_8_polish.h" #include "snowball/libstemmer/stem_UTF_8_porter.h" #include "snowball/libstemmer/stem_UTF_8_portuguese.h" #include "snowball/libstemmer/stem_UTF_8_romanian.h" @@ -109,6 +114,7 @@ static const stemmer_module stemmer_modules[] = STEMMER_MODULE(catalan, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(danish, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(dutch, PG_LATIN1, ISO_8859_1), + STEMMER_MODULE(dutch_porter, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(english, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(finnish, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(french, PG_LATIN1, ISO_8859_1), @@ -122,6 +128,7 @@ static const stemmer_module stemmer_modules[] = STEMMER_MODULE(spanish, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(swedish, PG_LATIN1, ISO_8859_1), STEMMER_MODULE(hungarian, PG_LATIN2, ISO_8859_2), + STEMMER_MODULE(polish, PG_LATIN2, ISO_8859_2), STEMMER_MODULE(russian, PG_KOI8R, KOI8_R), STEMMER_MODULE(arabic, PG_UTF8, UTF_8), STEMMER_MODULE(armenian, PG_UTF8, UTF_8), @@ -129,7 +136,9 @@ static const stemmer_module stemmer_modules[] = STEMMER_MODULE(catalan, PG_UTF8, UTF_8), STEMMER_MODULE(danish, PG_UTF8, UTF_8), STEMMER_MODULE(dutch, PG_UTF8, UTF_8), + STEMMER_MODULE(dutch_porter, PG_UTF8, UTF_8), STEMMER_MODULE(english, PG_UTF8, UTF_8), + STEMMER_MODULE(esperanto, PG_UTF8, UTF_8), STEMMER_MODULE(estonian, PG_UTF8, UTF_8), STEMMER_MODULE(finnish, PG_UTF8, UTF_8), STEMMER_MODULE(french, PG_UTF8, UTF_8), @@ -144,6 +153,7 @@ static const stemmer_module stemmer_modules[] = STEMMER_MODULE(nepali, PG_UTF8, UTF_8), STEMMER_MODULE(norwegian, PG_UTF8, UTF_8), STEMMER_MODULE(porter, PG_UTF8, UTF_8), + STEMMER_MODULE(polish, PG_UTF8, UTF_8), STEMMER_MODULE(portuguese, PG_UTF8, UTF_8), STEMMER_MODULE(romanian, PG_UTF8, UTF_8), STEMMER_MODULE(russian, PG_UTF8, UTF_8), @@ -229,7 +239,7 @@ dsnowball_init(PG_FUNCTION_ARGS) bool stoploaded = false; ListCell *l; - d = (DictSnowball *) palloc0(sizeof(DictSnowball)); + d = palloc0_object(DictSnowball); foreach(l, dictoptions) { @@ -278,7 +288,7 @@ dsnowball_lexize(PG_FUNCTION_ARGS) char *in = (char *) PG_GETARG_POINTER(1); int32 len = PG_GETARG_INT32(2); char *txt = str_tolower(in, len, DEFAULT_COLLATION_OID); - TSLexeme *res = palloc0(sizeof(TSLexeme) * 2); + TSLexeme *res = palloc0_array(TSLexeme, 2); /* * Do not pass strings exceeding 1000 bytes to the stemmer, as they're diff --git a/src/backend/snowball/libstemmer/api.c b/src/backend/snowball/libstemmer/api.c index 358f5633b28fe..9890f6664f945 100644 --- a/src/backend/snowball/libstemmer/api.c +++ b/src/backend/snowball/libstemmer/api.c @@ -1,56 +1,30 @@ -#include "header.h" +#include "snowball_runtime.h" -extern struct SN_env * SN_create_env(int S_size, int I_size) +static const struct SN_env default_SN_env; + +extern struct SN_env * SN_new_env(int alloc_size) { - struct SN_env * z = (struct SN_env *) calloc(1, sizeof(struct SN_env)); + struct SN_env * z = (struct SN_env *) malloc(alloc_size); if (z == NULL) return NULL; + *z = default_SN_env; z->p = create_s(); - if (z->p == NULL) goto error; - if (S_size) - { - int i; - z->S = (symbol * *) calloc(S_size, sizeof(symbol *)); - if (z->S == NULL) goto error; - - for (i = 0; i < S_size; i++) - { - z->S[i] = create_s(); - if (z->S[i] == NULL) goto error; - } - } - - if (I_size) - { - z->I = (int *) calloc(I_size, sizeof(int)); - if (z->I == NULL) goto error; + if (z->p == NULL) { + SN_delete_env(z); + return NULL; } - return z; -error: - SN_close_env(z, S_size); - return NULL; } -extern void SN_close_env(struct SN_env * z, int S_size) +extern void SN_delete_env(struct SN_env * z) { if (z == NULL) return; - if (z->S) - { - int i; - for (i = 0; i < S_size; i++) - { - lose_s(z->S[i]); - } - free(z->S); - } - free(z->I); if (z->p) lose_s(z->p); free(z); } extern int SN_set_current(struct SN_env * z, int size, const symbol * s) { - int err = replace_s(z, 0, z->l, size, s, NULL); + int err = replace_s(z, 0, z->l, size, s); z->c = 0; return err; } diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_basque.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_basque.c index ec2d4e7482ecc..865793837c141 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_basque.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_basque.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from basque.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_basque.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int basque_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_R1(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_RV(struct SN_env * z); @@ -16,18 +30,12 @@ static int r_mark_regions(struct SN_env * z); static int r_adjetiboak(struct SN_env * z); static int r_izenak(struct SN_env * z); static int r_aditzak(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - -extern struct SN_env * basque_ISO_8859_1_create_env(void); -extern void basque_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'j', 'o', 'k' }; +static const symbol s_1[] = { 't', 'r', 'a' }; +static const symbol s_2[] = { 'm', 'i', 'n', 'u', 't', 'u' }; +static const symbol s_3[] = { 'z' }; - -#ifdef __cplusplus -} -#endif static const symbol s_0_0[4] = { 'i', 'd', 'e', 'a' }; static const symbol s_0_1[5] = { 'b', 'i', 'd', 'e', 'a' }; static const symbol s_0_2[5] = { 'k', 'i', 'd', 'e', 'a' }; @@ -137,118 +145,116 @@ static const symbol s_0_105[5] = { 'e', 'r', 'r', 'e', 'z' }; static const symbol s_0_106[4] = { 't', 'z', 'e', 'z' }; static const symbol s_0_107[5] = { 'g', 'a', 'i', 't', 'z' }; static const symbol s_0_108[5] = { 'k', 'a', 'i', 't', 'z' }; - -static const struct among a_0[109] = -{ -{ 4, s_0_0, -1, 1, 0}, -{ 5, s_0_1, 0, 1, 0}, -{ 5, s_0_2, 0, 1, 0}, -{ 5, s_0_3, 0, 1, 0}, -{ 6, s_0_4, -1, 1, 0}, -{ 5, s_0_5, -1, 1, 0}, -{ 6, s_0_6, -1, 1, 0}, -{ 7, s_0_7, -1, 1, 0}, -{ 5, s_0_8, -1, 1, 0}, -{ 5, s_0_9, -1, 1, 0}, -{ 5, s_0_10, -1, 1, 0}, -{ 4, s_0_11, -1, 1, 0}, -{ 5, s_0_12, -1, 1, 0}, -{ 6, s_0_13, 12, 1, 0}, -{ 5, s_0_14, -1, 1, 0}, -{ 6, s_0_15, -1, 2, 0}, -{ 6, s_0_16, -1, 1, 0}, -{ 2, s_0_17, -1, 1, 0}, -{ 5, s_0_18, 17, 1, 0}, -{ 2, s_0_19, -1, 1, 0}, -{ 4, s_0_20, -1, 1, 0}, -{ 4, s_0_21, -1, 1, 0}, -{ 4, s_0_22, -1, 1, 0}, -{ 5, s_0_23, -1, 1, 0}, -{ 6, s_0_24, 23, 1, 0}, -{ 4, s_0_25, -1, 1, 0}, -{ 4, s_0_26, -1, 1, 0}, -{ 6, s_0_27, -1, 1, 0}, -{ 3, s_0_28, -1, 1, 0}, -{ 4, s_0_29, 28, 1, 0}, -{ 7, s_0_30, 29, 4, 0}, -{ 4, s_0_31, 28, 1, 0}, -{ 4, s_0_32, 28, 1, 0}, -{ 4, s_0_33, -1, 1, 0}, -{ 5, s_0_34, 33, 1, 0}, -{ 4, s_0_35, -1, 1, 0}, -{ 4, s_0_36, -1, 1, 0}, -{ 4, s_0_37, -1, 1, 0}, -{ 4, s_0_38, -1, 1, 0}, -{ 3, s_0_39, -1, 1, 0}, -{ 4, s_0_40, 39, 1, 0}, -{ 6, s_0_41, -1, 1, 0}, -{ 3, s_0_42, -1, 1, 0}, -{ 6, s_0_43, 42, 1, 0}, -{ 3, s_0_44, -1, 2, 0}, -{ 6, s_0_45, 44, 1, 0}, -{ 6, s_0_46, 44, 1, 0}, -{ 6, s_0_47, 44, 1, 0}, -{ 3, s_0_48, -1, 1, 0}, -{ 4, s_0_49, 48, 1, 0}, -{ 4, s_0_50, 48, 1, 0}, -{ 4, s_0_51, 48, 1, 0}, -{ 5, s_0_52, -1, 1, 0}, -{ 5, s_0_53, -1, 1, 0}, -{ 5, s_0_54, -1, 1, 0}, -{ 2, s_0_55, -1, 1, 0}, -{ 4, s_0_56, 55, 1, 0}, -{ 5, s_0_57, 55, 1, 0}, -{ 6, s_0_58, 55, 1, 0}, -{ 4, s_0_59, -1, 1, 0}, -{ 4, s_0_60, -1, 1, 0}, -{ 3, s_0_61, -1, 1, 0}, -{ 4, s_0_62, 61, 1, 0}, -{ 3, s_0_63, -1, 1, 0}, -{ 4, s_0_64, -1, 1, 0}, -{ 5, s_0_65, 64, 1, 0}, -{ 2, s_0_66, -1, 1, 0}, -{ 3, s_0_67, -1, 1, 0}, -{ 4, s_0_68, 67, 1, 0}, -{ 4, s_0_69, 67, 1, 0}, -{ 4, s_0_70, 67, 1, 0}, -{ 5, s_0_71, 70, 1, 0}, -{ 5, s_0_72, -1, 2, 0}, -{ 5, s_0_73, -1, 1, 0}, -{ 5, s_0_74, -1, 1, 0}, -{ 6, s_0_75, 74, 1, 0}, -{ 2, s_0_76, -1, 1, 0}, -{ 3, s_0_77, 76, 1, 0}, -{ 4, s_0_78, 77, 1, 0}, -{ 3, s_0_79, 76, 1, 0}, -{ 4, s_0_80, 76, 1, 0}, -{ 7, s_0_81, -1, 3, 0}, -{ 3, s_0_82, -1, 1, 0}, -{ 3, s_0_83, -1, 1, 0}, -{ 3, s_0_84, -1, 1, 0}, -{ 5, s_0_85, 84, 1, 0}, -{ 4, s_0_86, -1, 1, 0}, -{ 5, s_0_87, 86, 1, 0}, -{ 3, s_0_88, -1, 1, 0}, -{ 5, s_0_89, -1, 1, 0}, -{ 2, s_0_90, -1, 1, 0}, -{ 3, s_0_91, 90, 1, 0}, -{ 3, s_0_92, -1, 1, 0}, -{ 4, s_0_93, -1, 1, 0}, -{ 2, s_0_94, -1, 1, 0}, -{ 3, s_0_95, 94, 1, 0}, -{ 4, s_0_96, -1, 1, 0}, -{ 2, s_0_97, -1, 1, 0}, -{ 5, s_0_98, -1, 1, 0}, -{ 2, s_0_99, -1, 1, 0}, -{ 3, s_0_100, 99, 1, 0}, -{ 6, s_0_101, 100, 1, 0}, -{ 4, s_0_102, 100, 1, 0}, -{ 6, s_0_103, 99, 5, 0}, -{ 2, s_0_104, -1, 1, 0}, -{ 5, s_0_105, 104, 1, 0}, -{ 4, s_0_106, 104, 1, 0}, -{ 5, s_0_107, -1, 1, 0}, -{ 5, s_0_108, -1, 1, 0} +static const struct among a_0[109] = { +{ 4, s_0_0, 0, 1, 0}, +{ 5, s_0_1, -1, 1, 0}, +{ 5, s_0_2, -2, 1, 0}, +{ 5, s_0_3, -3, 1, 0}, +{ 6, s_0_4, 0, 1, 0}, +{ 5, s_0_5, 0, 1, 0}, +{ 6, s_0_6, 0, 1, 0}, +{ 7, s_0_7, 0, 1, 0}, +{ 5, s_0_8, 0, 1, 0}, +{ 5, s_0_9, 0, 1, 0}, +{ 5, s_0_10, 0, 1, 0}, +{ 4, s_0_11, 0, 1, 0}, +{ 5, s_0_12, 0, 1, 0}, +{ 6, s_0_13, -1, 1, 0}, +{ 5, s_0_14, 0, 1, 0}, +{ 6, s_0_15, 0, 2, 0}, +{ 6, s_0_16, 0, 1, 0}, +{ 2, s_0_17, 0, 1, 0}, +{ 5, s_0_18, -1, 1, 0}, +{ 2, s_0_19, 0, 1, 0}, +{ 4, s_0_20, 0, 1, 0}, +{ 4, s_0_21, 0, 1, 0}, +{ 4, s_0_22, 0, 1, 0}, +{ 5, s_0_23, 0, 1, 0}, +{ 6, s_0_24, -1, 1, 0}, +{ 4, s_0_25, 0, 1, 0}, +{ 4, s_0_26, 0, 1, 0}, +{ 6, s_0_27, 0, 1, 0}, +{ 3, s_0_28, 0, 1, 0}, +{ 4, s_0_29, -1, 1, 0}, +{ 7, s_0_30, -1, -1, 0}, +{ 4, s_0_31, -3, 1, 0}, +{ 4, s_0_32, -4, 1, 0}, +{ 4, s_0_33, 0, 1, 0}, +{ 5, s_0_34, -1, 1, 0}, +{ 4, s_0_35, 0, 1, 0}, +{ 4, s_0_36, 0, 1, 0}, +{ 4, s_0_37, 0, 1, 0}, +{ 4, s_0_38, 0, 1, 0}, +{ 3, s_0_39, 0, 1, 0}, +{ 4, s_0_40, -1, 1, 0}, +{ 6, s_0_41, 0, 1, 0}, +{ 3, s_0_42, 0, 1, 0}, +{ 6, s_0_43, -1, 1, 0}, +{ 3, s_0_44, 0, 2, 0}, +{ 6, s_0_45, -1, 1, 0}, +{ 6, s_0_46, -2, 1, 0}, +{ 6, s_0_47, -3, 1, 0}, +{ 3, s_0_48, 0, 1, 0}, +{ 4, s_0_49, -1, 1, 0}, +{ 4, s_0_50, -2, 1, 0}, +{ 4, s_0_51, -3, 1, 0}, +{ 5, s_0_52, 0, 1, 0}, +{ 5, s_0_53, 0, 1, 0}, +{ 5, s_0_54, 0, 1, 0}, +{ 2, s_0_55, 0, 1, 0}, +{ 4, s_0_56, -1, 1, 0}, +{ 5, s_0_57, -2, 1, 0}, +{ 6, s_0_58, -3, 1, 0}, +{ 4, s_0_59, 0, 1, 0}, +{ 4, s_0_60, 0, 1, 0}, +{ 3, s_0_61, 0, 1, 0}, +{ 4, s_0_62, -1, 1, 0}, +{ 3, s_0_63, 0, 1, 0}, +{ 4, s_0_64, 0, 1, 0}, +{ 5, s_0_65, -1, 1, 0}, +{ 2, s_0_66, 0, 1, 0}, +{ 3, s_0_67, 0, 1, 0}, +{ 4, s_0_68, -1, 1, 0}, +{ 4, s_0_69, -2, 1, 0}, +{ 4, s_0_70, -3, 1, 0}, +{ 5, s_0_71, -1, 1, 0}, +{ 5, s_0_72, 0, 2, 0}, +{ 5, s_0_73, 0, 1, 0}, +{ 5, s_0_74, 0, 1, 0}, +{ 6, s_0_75, -1, 1, 0}, +{ 2, s_0_76, 0, 1, 0}, +{ 3, s_0_77, -1, 1, 0}, +{ 4, s_0_78, -1, 1, 0}, +{ 3, s_0_79, -3, 1, 0}, +{ 4, s_0_80, -4, 1, 0}, +{ 7, s_0_81, 0, -1, 0}, +{ 3, s_0_82, 0, 1, 0}, +{ 3, s_0_83, 0, 1, 0}, +{ 3, s_0_84, 0, 1, 0}, +{ 5, s_0_85, -1, 1, 0}, +{ 4, s_0_86, 0, 1, 0}, +{ 5, s_0_87, -1, 1, 0}, +{ 3, s_0_88, 0, 1, 0}, +{ 5, s_0_89, 0, 1, 0}, +{ 2, s_0_90, 0, 1, 0}, +{ 3, s_0_91, -1, 1, 0}, +{ 3, s_0_92, 0, 1, 0}, +{ 4, s_0_93, 0, 1, 0}, +{ 2, s_0_94, 0, 1, 0}, +{ 3, s_0_95, -1, 1, 0}, +{ 4, s_0_96, 0, 1, 0}, +{ 2, s_0_97, 0, 1, 0}, +{ 5, s_0_98, 0, 1, 0}, +{ 2, s_0_99, 0, 1, 0}, +{ 3, s_0_100, -1, 1, 0}, +{ 6, s_0_101, -1, 1, 0}, +{ 4, s_0_102, -2, 1, 0}, +{ 6, s_0_103, -4, -1, 0}, +{ 2, s_0_104, 0, 1, 0}, +{ 5, s_0_105, -1, 1, 0}, +{ 4, s_0_106, -2, 1, 0}, +{ 5, s_0_107, 0, 1, 0}, +{ 5, s_0_108, 0, 1, 0} }; static const symbol s_1_0[3] = { 'a', 'd', 'a' }; @@ -546,304 +552,302 @@ static const symbol s_1_291[2] = { 'e', 'z' }; static const symbol s_1_292[4] = { 'e', 'r', 'o', 'z' }; static const symbol s_1_293[2] = { 't', 'z' }; static const symbol s_1_294[5] = { 'k', 'o', 'i', 't', 'z' }; - -static const struct among a_1[295] = -{ -{ 3, s_1_0, -1, 1, 0}, -{ 4, s_1_1, 0, 1, 0}, -{ 4, s_1_2, -1, 1, 0}, -{ 5, s_1_3, -1, 1, 0}, -{ 5, s_1_4, -1, 1, 0}, -{ 5, s_1_5, -1, 1, 0}, -{ 5, s_1_6, -1, 1, 0}, -{ 6, s_1_7, 6, 1, 0}, -{ 6, s_1_8, 6, 1, 0}, -{ 5, s_1_9, -1, 1, 0}, -{ 5, s_1_10, -1, 1, 0}, -{ 6, s_1_11, 10, 1, 0}, -{ 5, s_1_12, -1, 1, 0}, -{ 4, s_1_13, -1, 1, 0}, -{ 5, s_1_14, -1, 1, 0}, -{ 3, s_1_15, -1, 1, 0}, -{ 4, s_1_16, 15, 1, 0}, -{ 6, s_1_17, 15, 1, 0}, -{ 4, s_1_18, 15, 1, 0}, -{ 5, s_1_19, 18, 1, 0}, -{ 3, s_1_20, -1, 1, 0}, -{ 6, s_1_21, -1, 1, 0}, -{ 3, s_1_22, -1, 1, 0}, -{ 5, s_1_23, 22, 1, 0}, -{ 5, s_1_24, 22, 1, 0}, -{ 5, s_1_25, 22, 1, 0}, -{ 5, s_1_26, -1, 1, 0}, -{ 2, s_1_27, -1, 1, 0}, -{ 4, s_1_28, 27, 1, 0}, -{ 4, s_1_29, -1, 1, 0}, -{ 5, s_1_30, -1, 1, 0}, -{ 6, s_1_31, 30, 1, 0}, -{ 6, s_1_32, -1, 1, 0}, -{ 6, s_1_33, -1, 1, 0}, -{ 4, s_1_34, -1, 1, 0}, -{ 4, s_1_35, -1, 1, 0}, -{ 5, s_1_36, 35, 1, 0}, -{ 5, s_1_37, 35, 1, 0}, -{ 5, s_1_38, -1, 1, 0}, -{ 4, s_1_39, -1, 1, 0}, -{ 3, s_1_40, -1, 1, 0}, -{ 5, s_1_41, 40, 1, 0}, -{ 3, s_1_42, -1, 1, 0}, -{ 4, s_1_43, 42, 1, 0}, -{ 4, s_1_44, -1, 1, 0}, -{ 5, s_1_45, 44, 1, 0}, -{ 5, s_1_46, 44, 1, 0}, -{ 5, s_1_47, 44, 1, 0}, -{ 4, s_1_48, -1, 1, 0}, -{ 5, s_1_49, 48, 1, 0}, -{ 5, s_1_50, 48, 1, 0}, -{ 6, s_1_51, -1, 2, 0}, -{ 6, s_1_52, -1, 1, 0}, -{ 6, s_1_53, -1, 1, 0}, -{ 5, s_1_54, -1, 1, 0}, -{ 4, s_1_55, -1, 1, 0}, -{ 3, s_1_56, -1, 1, 0}, -{ 4, s_1_57, -1, 1, 0}, -{ 5, s_1_58, -1, 1, 0}, -{ 6, s_1_59, -1, 1, 0}, -{ 2, s_1_60, -1, 1, 0}, -{ 4, s_1_61, 60, 3, 0}, -{ 5, s_1_62, 60, 10, 0}, -{ 3, s_1_63, 60, 1, 0}, -{ 3, s_1_64, 60, 1, 0}, -{ 3, s_1_65, 60, 1, 0}, -{ 6, s_1_66, -1, 1, 0}, -{ 4, s_1_67, -1, 1, 0}, -{ 5, s_1_68, -1, 1, 0}, -{ 5, s_1_69, -1, 1, 0}, -{ 4, s_1_70, -1, 1, 0}, -{ 3, s_1_71, -1, 1, 0}, -{ 2, s_1_72, -1, 1, 0}, -{ 4, s_1_73, 72, 1, 0}, -{ 3, s_1_74, 72, 1, 0}, -{ 7, s_1_75, 74, 1, 0}, -{ 7, s_1_76, 74, 1, 0}, -{ 6, s_1_77, 74, 1, 0}, -{ 5, s_1_78, 72, 1, 0}, -{ 6, s_1_79, 78, 1, 0}, -{ 4, s_1_80, 72, 1, 0}, -{ 4, s_1_81, 72, 1, 0}, -{ 5, s_1_82, 72, 1, 0}, -{ 3, s_1_83, 72, 1, 0}, -{ 4, s_1_84, 83, 1, 0}, -{ 5, s_1_85, 83, 1, 0}, -{ 6, s_1_86, 85, 1, 0}, -{ 5, s_1_87, -1, 1, 0}, -{ 6, s_1_88, 87, 1, 0}, -{ 4, s_1_89, -1, 1, 0}, -{ 4, s_1_90, -1, 1, 0}, -{ 3, s_1_91, -1, 1, 0}, -{ 5, s_1_92, 91, 1, 0}, -{ 4, s_1_93, 91, 1, 0}, -{ 3, s_1_94, -1, 1, 0}, -{ 5, s_1_95, 94, 1, 0}, -{ 4, s_1_96, -1, 1, 0}, -{ 5, s_1_97, 96, 1, 0}, -{ 5, s_1_98, 96, 1, 0}, -{ 4, s_1_99, -1, 1, 0}, -{ 4, s_1_100, -1, 1, 0}, -{ 4, s_1_101, -1, 1, 0}, -{ 3, s_1_102, -1, 1, 0}, -{ 4, s_1_103, 102, 1, 0}, -{ 4, s_1_104, 102, 1, 0}, -{ 4, s_1_105, -1, 1, 0}, -{ 4, s_1_106, -1, 1, 0}, -{ 3, s_1_107, -1, 1, 0}, -{ 2, s_1_108, -1, 1, 0}, -{ 3, s_1_109, 108, 1, 0}, -{ 4, s_1_110, 109, 1, 0}, -{ 5, s_1_111, 109, 1, 0}, -{ 5, s_1_112, 109, 1, 0}, -{ 4, s_1_113, 109, 1, 0}, -{ 5, s_1_114, 113, 1, 0}, -{ 5, s_1_115, 109, 1, 0}, -{ 4, s_1_116, 108, 1, 0}, -{ 4, s_1_117, 108, 1, 0}, -{ 4, s_1_118, 108, 1, 0}, -{ 3, s_1_119, 108, 2, 0}, -{ 6, s_1_120, 108, 1, 0}, -{ 5, s_1_121, 108, 1, 0}, -{ 3, s_1_122, 108, 1, 0}, -{ 2, s_1_123, -1, 1, 0}, -{ 3, s_1_124, 123, 1, 0}, -{ 2, s_1_125, -1, 1, 0}, -{ 3, s_1_126, 125, 1, 0}, -{ 4, s_1_127, 126, 1, 0}, -{ 3, s_1_128, 125, 1, 0}, -{ 3, s_1_129, -1, 1, 0}, -{ 6, s_1_130, 129, 1, 0}, -{ 5, s_1_131, 129, 1, 0}, -{ 5, s_1_132, -1, 1, 0}, -{ 5, s_1_133, -1, 1, 0}, -{ 5, s_1_134, -1, 1, 0}, -{ 4, s_1_135, -1, 1, 0}, -{ 3, s_1_136, -1, 1, 0}, -{ 6, s_1_137, 136, 1, 0}, -{ 5, s_1_138, 136, 1, 0}, -{ 4, s_1_139, -1, 1, 0}, -{ 3, s_1_140, -1, 1, 0}, -{ 4, s_1_141, 140, 1, 0}, -{ 2, s_1_142, -1, 1, 0}, -{ 3, s_1_143, 142, 1, 0}, -{ 5, s_1_144, 142, 1, 0}, -{ 3, s_1_145, 142, 2, 0}, -{ 6, s_1_146, 145, 1, 0}, -{ 5, s_1_147, 145, 1, 0}, -{ 6, s_1_148, 145, 1, 0}, -{ 6, s_1_149, 145, 1, 0}, -{ 6, s_1_150, 145, 1, 0}, -{ 4, s_1_151, -1, 1, 0}, -{ 4, s_1_152, -1, 1, 0}, -{ 4, s_1_153, -1, 1, 0}, -{ 4, s_1_154, -1, 1, 0}, -{ 5, s_1_155, 154, 1, 0}, -{ 5, s_1_156, 154, 1, 0}, -{ 4, s_1_157, -1, 1, 0}, -{ 2, s_1_158, -1, 1, 0}, -{ 4, s_1_159, -1, 1, 0}, -{ 5, s_1_160, 159, 1, 0}, -{ 4, s_1_161, -1, 1, 0}, -{ 3, s_1_162, -1, 1, 0}, -{ 4, s_1_163, -1, 1, 0}, -{ 2, s_1_164, -1, 1, 0}, -{ 5, s_1_165, 164, 1, 0}, -{ 3, s_1_166, 164, 1, 0}, -{ 4, s_1_167, 166, 1, 0}, -{ 2, s_1_168, -1, 1, 0}, -{ 5, s_1_169, -1, 1, 0}, -{ 2, s_1_170, -1, 1, 0}, -{ 4, s_1_171, 170, 1, 0}, -{ 4, s_1_172, 170, 1, 0}, -{ 4, s_1_173, 170, 1, 0}, -{ 4, s_1_174, -1, 1, 0}, -{ 3, s_1_175, -1, 1, 0}, -{ 2, s_1_176, -1, 1, 0}, -{ 4, s_1_177, 176, 1, 0}, -{ 5, s_1_178, 177, 1, 0}, -{ 5, s_1_179, 176, 8, 0}, -{ 5, s_1_180, 176, 1, 0}, -{ 5, s_1_181, 176, 1, 0}, -{ 3, s_1_182, -1, 1, 0}, -{ 3, s_1_183, -1, 1, 0}, -{ 4, s_1_184, 183, 1, 0}, -{ 4, s_1_185, 183, 1, 0}, -{ 4, s_1_186, -1, 1, 0}, -{ 3, s_1_187, -1, 1, 0}, -{ 2, s_1_188, -1, 1, 0}, -{ 4, s_1_189, 188, 1, 0}, -{ 2, s_1_190, -1, 1, 0}, -{ 3, s_1_191, 190, 1, 0}, -{ 3, s_1_192, 190, 1, 0}, -{ 3, s_1_193, -1, 1, 0}, -{ 4, s_1_194, 193, 1, 0}, -{ 4, s_1_195, 193, 1, 0}, -{ 4, s_1_196, 193, 1, 0}, -{ 5, s_1_197, -1, 2, 0}, -{ 5, s_1_198, -1, 1, 0}, -{ 5, s_1_199, -1, 1, 0}, -{ 4, s_1_200, -1, 1, 0}, -{ 3, s_1_201, -1, 1, 0}, -{ 2, s_1_202, -1, 1, 0}, -{ 5, s_1_203, -1, 1, 0}, -{ 2, s_1_204, -1, 1, 0}, -{ 2, s_1_205, -1, 1, 0}, -{ 2, s_1_206, -1, 1, 0}, -{ 5, s_1_207, -1, 1, 0}, -{ 5, s_1_208, -1, 1, 0}, -{ 3, s_1_209, -1, 1, 0}, -{ 4, s_1_210, 209, 1, 0}, -{ 3, s_1_211, -1, 1, 0}, -{ 3, s_1_212, -1, 1, 0}, -{ 4, s_1_213, 212, 1, 0}, -{ 2, s_1_214, -1, 4, 0}, -{ 3, s_1_215, 214, 2, 0}, -{ 6, s_1_216, 215, 1, 0}, -{ 6, s_1_217, 215, 1, 0}, -{ 5, s_1_218, 215, 1, 0}, -{ 3, s_1_219, 214, 4, 0}, -{ 4, s_1_220, 214, 4, 0}, -{ 4, s_1_221, -1, 1, 0}, -{ 5, s_1_222, 221, 1, 0}, -{ 3, s_1_223, -1, 1, 0}, -{ 3, s_1_224, -1, 1, 0}, -{ 3, s_1_225, -1, 1, 0}, -{ 4, s_1_226, -1, 1, 0}, -{ 5, s_1_227, 226, 1, 0}, -{ 5, s_1_228, -1, 1, 0}, -{ 4, s_1_229, -1, 1, 0}, -{ 5, s_1_230, 229, 1, 0}, -{ 2, s_1_231, -1, 1, 0}, -{ 3, s_1_232, 231, 1, 0}, -{ 3, s_1_233, -1, 1, 0}, -{ 2, s_1_234, -1, 1, 0}, -{ 5, s_1_235, 234, 5, 0}, -{ 4, s_1_236, 234, 1, 0}, -{ 5, s_1_237, 236, 1, 0}, -{ 3, s_1_238, 234, 1, 0}, -{ 6, s_1_239, 234, 1, 0}, -{ 3, s_1_240, 234, 1, 0}, -{ 4, s_1_241, 234, 1, 0}, -{ 8, s_1_242, 241, 6, 0}, -{ 3, s_1_243, 234, 1, 0}, -{ 2, s_1_244, -1, 1, 0}, -{ 4, s_1_245, 244, 1, 0}, -{ 2, s_1_246, -1, 1, 0}, -{ 3, s_1_247, 246, 1, 0}, -{ 5, s_1_248, 247, 9, 0}, -{ 4, s_1_249, 247, 1, 0}, -{ 4, s_1_250, 247, 1, 0}, -{ 3, s_1_251, 246, 1, 0}, -{ 4, s_1_252, 246, 1, 0}, -{ 3, s_1_253, 246, 1, 0}, -{ 3, s_1_254, -1, 1, 0}, -{ 2, s_1_255, -1, 1, 0}, -{ 3, s_1_256, 255, 1, 0}, -{ 3, s_1_257, 255, 1, 0}, -{ 3, s_1_258, -1, 1, 0}, -{ 3, s_1_259, -1, 1, 0}, -{ 6, s_1_260, 259, 1, 0}, -{ 2, s_1_261, -1, 1, 0}, -{ 2, s_1_262, -1, 1, 0}, -{ 2, s_1_263, -1, 1, 0}, -{ 3, s_1_264, 263, 1, 0}, -{ 5, s_1_265, 263, 1, 0}, -{ 5, s_1_266, 263, 7, 0}, -{ 4, s_1_267, 263, 1, 0}, -{ 4, s_1_268, 263, 1, 0}, -{ 3, s_1_269, 263, 1, 0}, -{ 4, s_1_270, 263, 1, 0}, -{ 2, s_1_271, -1, 2, 0}, -{ 3, s_1_272, 271, 1, 0}, -{ 2, s_1_273, -1, 1, 0}, -{ 3, s_1_274, -1, 1, 0}, -{ 2, s_1_275, -1, 1, 0}, -{ 5, s_1_276, 275, 1, 0}, -{ 4, s_1_277, 275, 1, 0}, -{ 4, s_1_278, -1, 1, 0}, -{ 4, s_1_279, -1, 2, 0}, -{ 4, s_1_280, -1, 1, 0}, -{ 3, s_1_281, -1, 1, 0}, -{ 2, s_1_282, -1, 1, 0}, -{ 4, s_1_283, 282, 4, 0}, -{ 5, s_1_284, 282, 1, 0}, -{ 4, s_1_285, 282, 1, 0}, -{ 3, s_1_286, -1, 1, 0}, -{ 2, s_1_287, -1, 1, 0}, -{ 3, s_1_288, 287, 1, 0}, -{ 6, s_1_289, 288, 1, 0}, -{ 1, s_1_290, -1, 1, 0}, -{ 2, s_1_291, 290, 1, 0}, -{ 4, s_1_292, 290, 1, 0}, -{ 2, s_1_293, 290, 1, 0}, -{ 5, s_1_294, 293, 1, 0} +static const struct among a_1[295] = { +{ 3, s_1_0, 0, 1, 0}, +{ 4, s_1_1, -1, 1, 0}, +{ 4, s_1_2, 0, 1, 0}, +{ 5, s_1_3, 0, 1, 0}, +{ 5, s_1_4, 0, 1, 0}, +{ 5, s_1_5, 0, 1, 0}, +{ 5, s_1_6, 0, 1, 0}, +{ 6, s_1_7, -1, 1, 0}, +{ 6, s_1_8, -2, 1, 0}, +{ 5, s_1_9, 0, 1, 0}, +{ 5, s_1_10, 0, 1, 0}, +{ 6, s_1_11, -1, 1, 0}, +{ 5, s_1_12, 0, 1, 0}, +{ 4, s_1_13, 0, 1, 0}, +{ 5, s_1_14, 0, 1, 0}, +{ 3, s_1_15, 0, 1, 0}, +{ 4, s_1_16, -1, 1, 0}, +{ 6, s_1_17, -2, 1, 0}, +{ 4, s_1_18, -3, 1, 0}, +{ 5, s_1_19, -1, 1, 0}, +{ 3, s_1_20, 0, 1, 0}, +{ 6, s_1_21, 0, 1, 0}, +{ 3, s_1_22, 0, 1, 0}, +{ 5, s_1_23, -1, 1, 0}, +{ 5, s_1_24, -2, 1, 0}, +{ 5, s_1_25, -3, 1, 0}, +{ 5, s_1_26, 0, 1, 0}, +{ 2, s_1_27, 0, 1, 0}, +{ 4, s_1_28, -1, 1, 0}, +{ 4, s_1_29, 0, 1, 0}, +{ 5, s_1_30, 0, 1, 0}, +{ 6, s_1_31, -1, 1, 0}, +{ 6, s_1_32, 0, 1, 0}, +{ 6, s_1_33, 0, 1, 0}, +{ 4, s_1_34, 0, 1, 0}, +{ 4, s_1_35, 0, 1, 0}, +{ 5, s_1_36, -1, 1, 0}, +{ 5, s_1_37, -2, 1, 0}, +{ 5, s_1_38, 0, 1, 0}, +{ 4, s_1_39, 0, 1, 0}, +{ 3, s_1_40, 0, 1, 0}, +{ 5, s_1_41, -1, 1, 0}, +{ 3, s_1_42, 0, 1, 0}, +{ 4, s_1_43, -1, 1, 0}, +{ 4, s_1_44, 0, 1, 0}, +{ 5, s_1_45, -1, 1, 0}, +{ 5, s_1_46, -2, 1, 0}, +{ 5, s_1_47, -3, 1, 0}, +{ 4, s_1_48, 0, 1, 0}, +{ 5, s_1_49, -1, 1, 0}, +{ 5, s_1_50, -2, 1, 0}, +{ 6, s_1_51, 0, 2, 0}, +{ 6, s_1_52, 0, 1, 0}, +{ 6, s_1_53, 0, 1, 0}, +{ 5, s_1_54, 0, 1, 0}, +{ 4, s_1_55, 0, 1, 0}, +{ 3, s_1_56, 0, 1, 0}, +{ 4, s_1_57, 0, 1, 0}, +{ 5, s_1_58, 0, 1, 0}, +{ 6, s_1_59, 0, 1, 0}, +{ 2, s_1_60, 0, 1, 0}, +{ 4, s_1_61, -1, 3, 0}, +{ 5, s_1_62, -2, -1, 0}, +{ 3, s_1_63, -3, 1, 0}, +{ 3, s_1_64, -4, 1, 0}, +{ 3, s_1_65, -5, 1, 0}, +{ 6, s_1_66, 0, 1, 0}, +{ 4, s_1_67, 0, 1, 0}, +{ 5, s_1_68, 0, 1, 0}, +{ 5, s_1_69, 0, 1, 0}, +{ 4, s_1_70, 0, 1, 0}, +{ 3, s_1_71, 0, 1, 0}, +{ 2, s_1_72, 0, 1, 0}, +{ 4, s_1_73, -1, 1, 0}, +{ 3, s_1_74, -2, 1, 0}, +{ 7, s_1_75, -1, 1, 0}, +{ 7, s_1_76, -2, 1, 0}, +{ 6, s_1_77, -3, 1, 0}, +{ 5, s_1_78, -6, 1, 0}, +{ 6, s_1_79, -1, 1, 0}, +{ 4, s_1_80, -8, 1, 0}, +{ 4, s_1_81, -9, 1, 0}, +{ 5, s_1_82, -10, 1, 0}, +{ 3, s_1_83, -11, 1, 0}, +{ 4, s_1_84, -1, 1, 0}, +{ 5, s_1_85, -2, 1, 0}, +{ 6, s_1_86, -1, 1, 0}, +{ 5, s_1_87, 0, 1, 0}, +{ 6, s_1_88, -1, 1, 0}, +{ 4, s_1_89, 0, 1, 0}, +{ 4, s_1_90, 0, 1, 0}, +{ 3, s_1_91, 0, 1, 0}, +{ 5, s_1_92, -1, 1, 0}, +{ 4, s_1_93, -2, 1, 0}, +{ 3, s_1_94, 0, 1, 0}, +{ 5, s_1_95, -1, 1, 0}, +{ 4, s_1_96, 0, 1, 0}, +{ 5, s_1_97, -1, 1, 0}, +{ 5, s_1_98, -2, 1, 0}, +{ 4, s_1_99, 0, 1, 0}, +{ 4, s_1_100, 0, 1, 0}, +{ 4, s_1_101, 0, 1, 0}, +{ 3, s_1_102, 0, 1, 0}, +{ 4, s_1_103, -1, 1, 0}, +{ 4, s_1_104, -2, 1, 0}, +{ 4, s_1_105, 0, 1, 0}, +{ 4, s_1_106, 0, 1, 0}, +{ 3, s_1_107, 0, 1, 0}, +{ 2, s_1_108, 0, 1, 0}, +{ 3, s_1_109, -1, 1, 0}, +{ 4, s_1_110, -1, 1, 0}, +{ 5, s_1_111, -2, 1, 0}, +{ 5, s_1_112, -3, 1, 0}, +{ 4, s_1_113, -4, 1, 0}, +{ 5, s_1_114, -1, 1, 0}, +{ 5, s_1_115, -6, 1, 0}, +{ 4, s_1_116, -8, 1, 0}, +{ 4, s_1_117, -9, 1, 0}, +{ 4, s_1_118, -10, 1, 0}, +{ 3, s_1_119, -11, 2, 0}, +{ 6, s_1_120, -12, 1, 0}, +{ 5, s_1_121, -13, 1, 0}, +{ 3, s_1_122, -14, 1, 0}, +{ 2, s_1_123, 0, 1, 0}, +{ 3, s_1_124, -1, 1, 0}, +{ 2, s_1_125, 0, 1, 0}, +{ 3, s_1_126, -1, 1, 0}, +{ 4, s_1_127, -1, 1, 0}, +{ 3, s_1_128, -3, 1, 0}, +{ 3, s_1_129, 0, 1, 0}, +{ 6, s_1_130, -1, 1, 0}, +{ 5, s_1_131, -2, 1, 0}, +{ 5, s_1_132, 0, 1, 0}, +{ 5, s_1_133, 0, 1, 0}, +{ 5, s_1_134, 0, 1, 0}, +{ 4, s_1_135, 0, 1, 0}, +{ 3, s_1_136, 0, 1, 0}, +{ 6, s_1_137, -1, 1, 0}, +{ 5, s_1_138, -2, 1, 0}, +{ 4, s_1_139, 0, 1, 0}, +{ 3, s_1_140, 0, 1, 0}, +{ 4, s_1_141, -1, 1, 0}, +{ 2, s_1_142, 0, 1, 0}, +{ 3, s_1_143, -1, 1, 0}, +{ 5, s_1_144, -2, 1, 0}, +{ 3, s_1_145, -3, 2, 0}, +{ 6, s_1_146, -1, 1, 0}, +{ 5, s_1_147, -2, 1, 0}, +{ 6, s_1_148, -3, 1, 0}, +{ 6, s_1_149, -4, 1, 0}, +{ 6, s_1_150, -5, 1, 0}, +{ 4, s_1_151, 0, 1, 0}, +{ 4, s_1_152, 0, 1, 0}, +{ 4, s_1_153, 0, 1, 0}, +{ 4, s_1_154, 0, 1, 0}, +{ 5, s_1_155, -1, 1, 0}, +{ 5, s_1_156, -2, 1, 0}, +{ 4, s_1_157, 0, 1, 0}, +{ 2, s_1_158, 0, 1, 0}, +{ 4, s_1_159, 0, 1, 0}, +{ 5, s_1_160, -1, 1, 0}, +{ 4, s_1_161, 0, 1, 0}, +{ 3, s_1_162, 0, 1, 0}, +{ 4, s_1_163, 0, 1, 0}, +{ 2, s_1_164, 0, 1, 0}, +{ 5, s_1_165, -1, 1, 0}, +{ 3, s_1_166, -2, 1, 0}, +{ 4, s_1_167, -1, 1, 0}, +{ 2, s_1_168, 0, 1, 0}, +{ 5, s_1_169, 0, 1, 0}, +{ 2, s_1_170, 0, 1, 0}, +{ 4, s_1_171, -1, 1, 0}, +{ 4, s_1_172, -2, 1, 0}, +{ 4, s_1_173, -3, 1, 0}, +{ 4, s_1_174, 0, 1, 0}, +{ 3, s_1_175, 0, 1, 0}, +{ 2, s_1_176, 0, 1, 0}, +{ 4, s_1_177, -1, 1, 0}, +{ 5, s_1_178, -1, 1, 0}, +{ 5, s_1_179, -3, -1, 0}, +{ 5, s_1_180, -4, 1, 0}, +{ 5, s_1_181, -5, 1, 0}, +{ 3, s_1_182, 0, 1, 0}, +{ 3, s_1_183, 0, 1, 0}, +{ 4, s_1_184, -1, 1, 0}, +{ 4, s_1_185, -2, 1, 0}, +{ 4, s_1_186, 0, 1, 0}, +{ 3, s_1_187, 0, 1, 0}, +{ 2, s_1_188, 0, 1, 0}, +{ 4, s_1_189, -1, 1, 0}, +{ 2, s_1_190, 0, 1, 0}, +{ 3, s_1_191, -1, 1, 0}, +{ 3, s_1_192, -2, 1, 0}, +{ 3, s_1_193, 0, 1, 0}, +{ 4, s_1_194, -1, 1, 0}, +{ 4, s_1_195, -2, 1, 0}, +{ 4, s_1_196, -3, 1, 0}, +{ 5, s_1_197, 0, 2, 0}, +{ 5, s_1_198, 0, 1, 0}, +{ 5, s_1_199, 0, 1, 0}, +{ 4, s_1_200, 0, 1, 0}, +{ 3, s_1_201, 0, 1, 0}, +{ 2, s_1_202, 0, 1, 0}, +{ 5, s_1_203, 0, 1, 0}, +{ 2, s_1_204, 0, 1, 0}, +{ 2, s_1_205, 0, 1, 0}, +{ 2, s_1_206, 0, 1, 0}, +{ 5, s_1_207, 0, 1, 0}, +{ 5, s_1_208, 0, 1, 0}, +{ 3, s_1_209, 0, 1, 0}, +{ 4, s_1_210, -1, 1, 0}, +{ 3, s_1_211, 0, 1, 0}, +{ 3, s_1_212, 0, 1, 0}, +{ 4, s_1_213, -1, 1, 0}, +{ 2, s_1_214, 0, 4, 0}, +{ 3, s_1_215, -1, 2, 0}, +{ 6, s_1_216, -1, 1, 0}, +{ 6, s_1_217, -2, 1, 0}, +{ 5, s_1_218, -3, 1, 0}, +{ 3, s_1_219, -5, 4, 0}, +{ 4, s_1_220, -6, 4, 0}, +{ 4, s_1_221, 0, 1, 0}, +{ 5, s_1_222, -1, 1, 0}, +{ 3, s_1_223, 0, 1, 0}, +{ 3, s_1_224, 0, 1, 0}, +{ 3, s_1_225, 0, 1, 0}, +{ 4, s_1_226, 0, 1, 0}, +{ 5, s_1_227, -1, 1, 0}, +{ 5, s_1_228, 0, 1, 0}, +{ 4, s_1_229, 0, 1, 0}, +{ 5, s_1_230, -1, 1, 0}, +{ 2, s_1_231, 0, 1, 0}, +{ 3, s_1_232, -1, 1, 0}, +{ 3, s_1_233, 0, 1, 0}, +{ 2, s_1_234, 0, 1, 0}, +{ 5, s_1_235, -1, 5, 0}, +{ 4, s_1_236, -2, 1, 0}, +{ 5, s_1_237, -1, 1, 0}, +{ 3, s_1_238, -4, 1, 0}, +{ 6, s_1_239, -5, 1, 0}, +{ 3, s_1_240, -6, 1, 0}, +{ 4, s_1_241, -7, 1, 0}, +{ 8, s_1_242, -1, 6, 0}, +{ 3, s_1_243, -9, 1, 0}, +{ 2, s_1_244, 0, 1, 0}, +{ 4, s_1_245, -1, 1, 0}, +{ 2, s_1_246, 0, 1, 0}, +{ 3, s_1_247, -1, 1, 0}, +{ 5, s_1_248, -1, -1, 0}, +{ 4, s_1_249, -2, 1, 0}, +{ 4, s_1_250, -3, 1, 0}, +{ 3, s_1_251, -5, 1, 0}, +{ 4, s_1_252, -6, 1, 0}, +{ 3, s_1_253, -7, 1, 0}, +{ 3, s_1_254, 0, 1, 0}, +{ 2, s_1_255, 0, 1, 0}, +{ 3, s_1_256, -1, 1, 0}, +{ 3, s_1_257, -2, 1, 0}, +{ 3, s_1_258, 0, 1, 0}, +{ 3, s_1_259, 0, 1, 0}, +{ 6, s_1_260, -1, 1, 0}, +{ 2, s_1_261, 0, 1, 0}, +{ 2, s_1_262, 0, 1, 0}, +{ 2, s_1_263, 0, 1, 0}, +{ 3, s_1_264, -1, 1, 0}, +{ 5, s_1_265, -2, 1, 0}, +{ 5, s_1_266, -3, -1, 0}, +{ 4, s_1_267, -4, 1, 0}, +{ 4, s_1_268, -5, 1, 0}, +{ 3, s_1_269, -6, 1, 0}, +{ 4, s_1_270, -7, 1, 0}, +{ 2, s_1_271, 0, 2, 0}, +{ 3, s_1_272, -1, 1, 0}, +{ 2, s_1_273, 0, 1, 0}, +{ 3, s_1_274, 0, 1, 0}, +{ 2, s_1_275, 0, 1, 0}, +{ 5, s_1_276, -1, 1, 0}, +{ 4, s_1_277, -2, 1, 0}, +{ 4, s_1_278, 0, 1, 0}, +{ 4, s_1_279, 0, 2, 0}, +{ 4, s_1_280, 0, 1, 0}, +{ 3, s_1_281, 0, 1, 0}, +{ 2, s_1_282, 0, 1, 0}, +{ 4, s_1_283, -1, 4, 0}, +{ 5, s_1_284, -2, 1, 0}, +{ 4, s_1_285, -3, 1, 0}, +{ 3, s_1_286, 0, 1, 0}, +{ 2, s_1_287, 0, 1, 0}, +{ 3, s_1_288, -1, 1, 0}, +{ 6, s_1_289, -1, 1, 0}, +{ 1, s_1_290, 0, 1, 0}, +{ 2, s_1_291, -1, 1, 0}, +{ 4, s_1_292, -2, 1, 0}, +{ 2, s_1_293, -3, 1, 0}, +{ 5, s_1_294, -1, 1, 0} }; static const symbol s_2_0[4] = { 'z', 'l', 'e', 'a' }; @@ -865,179 +869,148 @@ static const symbol s_2_15[2] = { 'g', 'o' }; static const symbol s_2_16[2] = { 'r', 'o' }; static const symbol s_2_17[3] = { 'e', 'r', 'o' }; static const symbol s_2_18[2] = { 't', 'o' }; - -static const struct among a_2[19] = -{ -{ 4, s_2_0, -1, 2, 0}, -{ 5, s_2_1, -1, 1, 0}, -{ 2, s_2_2, -1, 1, 0}, -{ 3, s_2_3, -1, 1, 0}, -{ 4, s_2_4, -1, 1, 0}, -{ 4, s_2_5, -1, 1, 0}, -{ 4, s_2_6, -1, 1, 0}, -{ 4, s_2_7, -1, 1, 0}, -{ 2, s_2_8, -1, 1, 0}, -{ 2, s_2_9, -1, 1, 0}, -{ 2, s_2_10, -1, 1, 0}, -{ 5, s_2_11, 10, 1, 0}, -{ 3, s_2_12, 10, 1, 0}, -{ 5, s_2_13, 12, 1, 0}, -{ 4, s_2_14, 10, 1, 0}, -{ 2, s_2_15, -1, 1, 0}, -{ 2, s_2_16, -1, 1, 0}, -{ 3, s_2_17, 16, 1, 0}, -{ 2, s_2_18, -1, 1, 0} +static const struct among a_2[19] = { +{ 4, s_2_0, 0, 2, 0}, +{ 5, s_2_1, 0, 1, 0}, +{ 2, s_2_2, 0, 1, 0}, +{ 3, s_2_3, 0, 1, 0}, +{ 4, s_2_4, 0, 1, 0}, +{ 4, s_2_5, 0, 1, 0}, +{ 4, s_2_6, 0, 1, 0}, +{ 4, s_2_7, 0, 1, 0}, +{ 2, s_2_8, 0, 1, 0}, +{ 2, s_2_9, 0, 1, 0}, +{ 2, s_2_10, 0, 1, 0}, +{ 5, s_2_11, -1, 1, 0}, +{ 3, s_2_12, -2, 1, 0}, +{ 5, s_2_13, -1, 1, 0}, +{ 4, s_2_14, -4, 1, 0}, +{ 2, s_2_15, 0, 1, 0}, +{ 2, s_2_16, 0, 1, 0}, +{ 3, s_2_17, -1, 1, 0}, +{ 2, s_2_18, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16 }; -static const symbol s_0[] = { 'a', 't', 's', 'e', 'd', 'e', 'n' }; -static const symbol s_1[] = { 'a', 'r', 'a', 'b', 'e', 'r', 'a' }; -static const symbol s_2[] = { 'b', 'a', 'd', 'i', 't', 'u' }; -static const symbol s_3[] = { 'j', 'o', 'k' }; -static const symbol s_4[] = { 't', 'r', 'a' }; -static const symbol s_5[] = { 'm', 'i', 'n', 'u', 't', 'u' }; -static const symbol s_6[] = { 'z', 'e', 'h', 'a', 'r' }; -static const symbol s_7[] = { 'g', 'e', 'l', 'd', 'i' }; -static const symbol s_8[] = { 'i', 'g', 'a', 'r', 'o' }; -static const symbol s_9[] = { 'a', 'u', 'r', 'k', 'a' }; -static const symbol s_10[] = { 'z' }; - static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping(z, g_v, 97, 117, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping(z, g_v, 97, 117, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping(z, g_v, 97, 117, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping(z, g_v, 97, 117, 0)) goto lab2; { int ret = out_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping(z, g_v, 97, 117, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping(z, g_v, 97, 117, 0)) goto lab1; { int ret = in_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping(z, g_v, 97, 117, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping(z, g_v, 97, 117, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping(z, g_v, 97, 117, 0)) goto lab3; { int ret = out_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping(z, g_v, 97, 117, 0)) goto lab0; if (z->c >= z->l) goto lab0; z->c++; - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_aditzak(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((70566434 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_0, 109); + among_var = find_among_b(z, a_0, 109, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = slice_from_s(z, 7, s_0); - if (ret < 0) return ret; - } - break; - case 4: - { int ret = slice_from_s(z, 7, s_1); - if (ret < 0) return ret; - } - break; - case 5: - { int ret = slice_from_s(z, 6, s_2); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1049,66 +1022,55 @@ static int r_izenak(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((71162402 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_1, 295); + among_var = find_among_b(z, a_1, 295, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_3); + { + int ret = slice_from_s(z, 3, s_0); if (ret < 0) return ret; } break; case 4: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_4); + { + int ret = slice_from_s(z, 3, s_1); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_5); - if (ret < 0) return ret; - } - break; - case 7: - { int ret = slice_from_s(z, 5, s_6); - if (ret < 0) return ret; - } - break; - case 8: - { int ret = slice_from_s(z, 5, s_7); - if (ret < 0) return ret; - } - break; - case 9: - { int ret = slice_from_s(z, 5, s_8); - if (ret < 0) return ret; - } - break; - case 10: - { int ret = slice_from_s(z, 5, s_9); + { + int ret = slice_from_s(z, 6, s_2); if (ret < 0) return ret; } break; @@ -1120,20 +1082,23 @@ static int r_adjetiboak(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((35362 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_2, 19); + among_var = find_among_b(z, a_2, 19, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; @@ -1142,45 +1107,58 @@ static int r_adjetiboak(struct SN_env * z) { } extern int basque_ISO_8859_1_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - while(1) { - int m1 = z->l - z->c; (void)m1; - { int ret = r_aditzak(z); + while (1) { + int v_1 = z->l - z->c; + { + int ret = r_aditzak(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } continue; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; break; } - while(1) { - int m2 = z->l - z->c; (void)m2; - { int ret = r_izenak(z); + while (1) { + int v_2 = z->l - z->c; + { + int ret = r_izenak(z); if (ret == 0) goto lab1; if (ret < 0) return ret; } continue; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; break; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_adjetiboak(z); + { + int v_3 = z->l - z->c; + { + int ret = r_adjetiboak(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } z->c = z->lb; return 1; } -extern struct SN_env * basque_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * basque_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void basque_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void basque_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_catalan.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_catalan.c index f2710534d6274..ef82a92548e61 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_catalan.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_catalan.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from catalan.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_catalan.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int catalan_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_residual_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_standard_suffix(struct SN_env * z); @@ -17,18 +30,18 @@ static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_cleaning(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * catalan_ISO_8859_1_create_env(void); -extern void catalan_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'i' }; +static const symbol s_3[] = { 'o' }; +static const symbol s_4[] = { 'u' }; +static const symbol s_5[] = { '.' }; +static const symbol s_6[] = { 'l', 'o', 'g' }; +static const symbol s_7[] = { 'i', 'c' }; +static const symbol s_8[] = { 'c' }; +static const symbol s_9[] = { 'i', 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[1] = { 0xB7 }; static const symbol s_0_2[1] = { 0xE0 }; static const symbol s_0_3[1] = { 0xE1 }; @@ -41,22 +54,20 @@ static const symbol s_0_9[1] = { 0xF2 }; static const symbol s_0_10[1] = { 0xF3 }; static const symbol s_0_11[1] = { 0xFA }; static const symbol s_0_12[1] = { 0xFC }; - -static const struct among a_0[13] = -{ -{ 0, 0, -1, 7, 0}, -{ 1, s_0_1, 0, 6, 0}, -{ 1, s_0_2, 0, 1, 0}, -{ 1, s_0_3, 0, 1, 0}, -{ 1, s_0_4, 0, 2, 0}, -{ 1, s_0_5, 0, 2, 0}, -{ 1, s_0_6, 0, 3, 0}, -{ 1, s_0_7, 0, 3, 0}, -{ 1, s_0_8, 0, 3, 0}, -{ 1, s_0_9, 0, 4, 0}, -{ 1, s_0_10, 0, 4, 0}, -{ 1, s_0_11, 0, 5, 0}, -{ 1, s_0_12, 0, 5, 0} +static const struct among a_0[13] = { +{ 0, 0, 0, 7, 0}, +{ 1, s_0_1, -1, 6, 0}, +{ 1, s_0_2, -2, 1, 0}, +{ 1, s_0_3, -3, 1, 0}, +{ 1, s_0_4, -4, 2, 0}, +{ 1, s_0_5, -5, 2, 0}, +{ 1, s_0_6, -6, 3, 0}, +{ 1, s_0_7, -7, 3, 0}, +{ 1, s_0_8, -8, 3, 0}, +{ 1, s_0_9, -9, 4, 0}, +{ 1, s_0_10, -10, 4, 0}, +{ 1, s_0_11, -11, 5, 0}, +{ 1, s_0_12, -12, 5, 0} }; static const symbol s_1_0[2] = { 'l', 'a' }; @@ -98,48 +109,46 @@ static const symbol s_1_35[3] = { 'v', 'o', 's' }; static const symbol s_1_36[2] = { 'u', 's' }; static const symbol s_1_37[3] = { '-', 'u', 's' }; static const symbol s_1_38[2] = { '\'', 't' }; - -static const struct among a_1[39] = -{ -{ 2, s_1_0, -1, 1, 0}, -{ 3, s_1_1, 0, 1, 0}, -{ 4, s_1_2, 0, 1, 0}, -{ 2, s_1_3, -1, 1, 0}, -{ 2, s_1_4, -1, 1, 0}, -{ 3, s_1_5, 4, 1, 0}, -{ 2, s_1_6, -1, 1, 0}, -{ 3, s_1_7, -1, 1, 0}, -{ 2, s_1_8, -1, 1, 0}, -{ 3, s_1_9, 8, 1, 0}, -{ 2, s_1_10, -1, 1, 0}, -{ 3, s_1_11, 10, 1, 0}, -{ 2, s_1_12, -1, 1, 0}, -{ 2, s_1_13, -1, 1, 0}, -{ 2, s_1_14, -1, 1, 0}, -{ 2, s_1_15, -1, 1, 0}, -{ 2, s_1_16, -1, 1, 0}, -{ 2, s_1_17, -1, 1, 0}, -{ 3, s_1_18, 17, 1, 0}, -{ 2, s_1_19, -1, 1, 0}, -{ 4, s_1_20, 19, 1, 0}, -{ 2, s_1_21, -1, 1, 0}, -{ 3, s_1_22, -1, 1, 0}, -{ 5, s_1_23, 22, 1, 0}, -{ 3, s_1_24, -1, 1, 0}, -{ 4, s_1_25, 24, 1, 0}, -{ 3, s_1_26, -1, 1, 0}, -{ 3, s_1_27, -1, 1, 0}, -{ 3, s_1_28, -1, 1, 0}, -{ 3, s_1_29, -1, 1, 0}, -{ 3, s_1_30, -1, 1, 0}, -{ 3, s_1_31, -1, 1, 0}, -{ 5, s_1_32, 31, 1, 0}, -{ 3, s_1_33, -1, 1, 0}, -{ 4, s_1_34, 33, 1, 0}, -{ 3, s_1_35, -1, 1, 0}, -{ 2, s_1_36, -1, 1, 0}, -{ 3, s_1_37, 36, 1, 0}, -{ 2, s_1_38, -1, 1, 0} +static const struct among a_1[39] = { +{ 2, s_1_0, 0, 1, 0}, +{ 3, s_1_1, -1, 1, 0}, +{ 4, s_1_2, -2, 1, 0}, +{ 2, s_1_3, 0, 1, 0}, +{ 2, s_1_4, 0, 1, 0}, +{ 3, s_1_5, -1, 1, 0}, +{ 2, s_1_6, 0, 1, 0}, +{ 3, s_1_7, 0, 1, 0}, +{ 2, s_1_8, 0, 1, 0}, +{ 3, s_1_9, -1, 1, 0}, +{ 2, s_1_10, 0, 1, 0}, +{ 3, s_1_11, -1, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 2, s_1_13, 0, 1, 0}, +{ 2, s_1_14, 0, 1, 0}, +{ 2, s_1_15, 0, 1, 0}, +{ 2, s_1_16, 0, 1, 0}, +{ 2, s_1_17, 0, 1, 0}, +{ 3, s_1_18, -1, 1, 0}, +{ 2, s_1_19, 0, 1, 0}, +{ 4, s_1_20, -1, 1, 0}, +{ 2, s_1_21, 0, 1, 0}, +{ 3, s_1_22, 0, 1, 0}, +{ 5, s_1_23, -1, 1, 0}, +{ 3, s_1_24, 0, 1, 0}, +{ 4, s_1_25, -1, 1, 0}, +{ 3, s_1_26, 0, 1, 0}, +{ 3, s_1_27, 0, 1, 0}, +{ 3, s_1_28, 0, 1, 0}, +{ 3, s_1_29, 0, 1, 0}, +{ 3, s_1_30, 0, 1, 0}, +{ 3, s_1_31, 0, 1, 0}, +{ 5, s_1_32, -1, 1, 0}, +{ 3, s_1_33, 0, 1, 0}, +{ 4, s_1_34, -1, 1, 0}, +{ 3, s_1_35, 0, 1, 0}, +{ 2, s_1_36, 0, 1, 0}, +{ 3, s_1_37, -1, 1, 0}, +{ 2, s_1_38, 0, 1, 0} }; static const symbol s_2_0[3] = { 'i', 'c', 'a' }; @@ -342,209 +351,207 @@ static const symbol s_2_196[1] = { 0xF3 }; static const symbol s_2_197[2] = { 'i', 0xF3 }; static const symbol s_2_198[3] = { 'c', 'i', 0xF3 }; static const symbol s_2_199[4] = { 'a', 'c', 'i', 0xF3 }; - -static const struct among a_2[200] = -{ -{ 3, s_2_0, -1, 4, 0}, -{ 6, s_2_1, 0, 3, 0}, -{ 4, s_2_2, -1, 1, 0}, -{ 3, s_2_3, -1, 2, 0}, -{ 5, s_2_4, -1, 1, 0}, -{ 5, s_2_5, -1, 1, 0}, -{ 5, s_2_6, -1, 1, 0}, -{ 4, s_2_7, -1, 1, 0}, -{ 5, s_2_8, -1, 3, 0}, -{ 4, s_2_9, -1, 1, 0}, -{ 5, s_2_10, 9, 1, 0}, -{ 4, s_2_11, -1, 1, 0}, -{ 4, s_2_12, -1, 1, 0}, -{ 6, s_2_13, -1, 1, 0}, -{ 4, s_2_14, -1, 1, 0}, -{ 4, s_2_15, -1, 1, 0}, -{ 5, s_2_16, -1, 1, 0}, -{ 3, s_2_17, -1, 1, 0}, -{ 6, s_2_18, 17, 1, 0}, -{ 8, s_2_19, 18, 5, 0}, -{ 3, s_2_20, -1, 1, 0}, -{ 3, s_2_21, -1, 1, 0}, -{ 3, s_2_22, -1, 1, 0}, -{ 5, s_2_23, 22, 1, 0}, -{ 3, s_2_24, -1, 1, 0}, -{ 4, s_2_25, 24, 1, 0}, -{ 5, s_2_26, 25, 1, 0}, -{ 5, s_2_27, -1, 1, 0}, -{ 3, s_2_28, -1, 1, 0}, -{ 3, s_2_29, -1, 1, 0}, -{ 4, s_2_30, -1, 1, 0}, -{ 4, s_2_31, -1, 1, 0}, -{ 4, s_2_32, -1, 1, 0}, -{ 3, s_2_33, -1, 1, 0}, -{ 3, s_2_34, -1, 1, 0}, -{ 3, s_2_35, -1, 1, 0}, -{ 4, s_2_36, -1, 1, 0}, -{ 7, s_2_37, 36, 1, 0}, -{ 7, s_2_38, 36, 1, 0}, -{ 3, s_2_39, -1, 1, 0}, -{ 5, s_2_40, 39, 1, 0}, -{ 3, s_2_41, -1, 1, 0}, -{ 5, s_2_42, -1, 3, 0}, -{ 2, s_2_43, -1, 4, 0}, -{ 5, s_2_44, 43, 1, 0}, -{ 3, s_2_45, -1, 1, 0}, -{ 3, s_2_46, -1, 1, 0}, -{ 2, s_2_47, -1, 1, 0}, -{ 4, s_2_48, -1, 1, 0}, -{ 3, s_2_49, -1, 1, 0}, -{ 4, s_2_50, 49, 1, 0}, -{ 4, s_2_51, 49, 1, 0}, -{ 4, s_2_52, -1, 1, 0}, -{ 7, s_2_53, 52, 1, 0}, -{ 7, s_2_54, 52, 1, 0}, -{ 6, s_2_55, 52, 1, 0}, -{ 4, s_2_56, -1, 1, 0}, -{ 4, s_2_57, -1, 1, 0}, -{ 4, s_2_58, -1, 1, 0}, -{ 3, s_2_59, -1, 1, 0}, -{ 3, s_2_60, -1, 1, 0}, -{ 4, s_2_61, -1, 3, 0}, -{ 3, s_2_62, -1, 1, 0}, -{ 4, s_2_63, -1, 1, 0}, -{ 2, s_2_64, -1, 1, 0}, -{ 2, s_2_65, -1, 1, 0}, -{ 3, s_2_66, -1, 1, 0}, -{ 3, s_2_67, -1, 1, 0}, -{ 4, s_2_68, -1, 1, 0}, -{ 4, s_2_69, -1, 1, 0}, -{ 5, s_2_70, -1, 1, 0}, -{ 5, s_2_71, -1, 1, 0}, -{ 5, s_2_72, -1, 1, 0}, -{ 5, s_2_73, -1, 1, 0}, -{ 7, s_2_74, 73, 5, 0}, -{ 4, s_2_75, -1, 1, 0}, -{ 5, s_2_76, -1, 1, 0}, -{ 2, s_2_77, -1, 1, 0}, -{ 6, s_2_78, 77, 1, 0}, -{ 4, s_2_79, 77, 1, 0}, -{ 4, s_2_80, 77, 1, 0}, -{ 4, s_2_81, 77, 1, 0}, -{ 5, s_2_82, 77, 1, 0}, -{ 3, s_2_83, -1, 1, 0}, -{ 2, s_2_84, -1, 1, 0}, -{ 3, s_2_85, 84, 1, 0}, -{ 3, s_2_86, -1, 1, 0}, -{ 5, s_2_87, -1, 1, 0}, -{ 3, s_2_88, -1, 4, 0}, -{ 6, s_2_89, 88, 3, 0}, -{ 3, s_2_90, -1, 1, 0}, -{ 4, s_2_91, -1, 1, 0}, -{ 4, s_2_92, -1, 2, 0}, -{ 6, s_2_93, -1, 1, 0}, -{ 6, s_2_94, -1, 1, 0}, -{ 6, s_2_95, -1, 1, 0}, -{ 5, s_2_96, -1, 1, 0}, -{ 6, s_2_97, -1, 3, 0}, -{ 5, s_2_98, -1, 1, 0}, -{ 5, s_2_99, -1, 1, 0}, -{ 5, s_2_100, -1, 1, 0}, -{ 5, s_2_101, -1, 1, 0}, -{ 7, s_2_102, -1, 1, 0}, -{ 4, s_2_103, -1, 1, 0}, -{ 5, s_2_104, 103, 1, 0}, -{ 5, s_2_105, 103, 1, 0}, -{ 4, s_2_106, -1, 1, 0}, -{ 7, s_2_107, 106, 1, 0}, -{ 9, s_2_108, 107, 5, 0}, -{ 6, s_2_109, -1, 1, 0}, -{ 5, s_2_110, -1, 1, 0}, -{ 8, s_2_111, 110, 1, 0}, -{ 4, s_2_112, -1, 1, 0}, -{ 4, s_2_113, -1, 1, 0}, -{ 4, s_2_114, -1, 1, 0}, -{ 5, s_2_115, 114, 1, 0}, -{ 6, s_2_116, 115, 1, 0}, -{ 5, s_2_117, -1, 1, 0}, -{ 4, s_2_118, -1, 1, 0}, -{ 4, s_2_119, -1, 1, 0}, -{ 5, s_2_120, -1, 1, 0}, -{ 5, s_2_121, -1, 1, 0}, -{ 4, s_2_122, -1, 1, 0}, -{ 4, s_2_123, -1, 1, 0}, -{ 5, s_2_124, -1, 1, 0}, -{ 8, s_2_125, 124, 1, 0}, -{ 8, s_2_126, 124, 1, 0}, -{ 5, s_2_127, -1, 4, 0}, -{ 8, s_2_128, 127, 3, 0}, -{ 4, s_2_129, -1, 1, 0}, -{ 6, s_2_130, 129, 1, 0}, -{ 6, s_2_131, -1, 3, 0}, -{ 9, s_2_132, -1, 1, 0}, -{ 4, s_2_133, -1, 1, 0}, -{ 4, s_2_134, -1, 1, 0}, -{ 5, s_2_135, -1, 3, 0}, -{ 4, s_2_136, -1, 1, 0}, -{ 5, s_2_137, -1, 1, 0}, -{ 2, s_2_138, -1, 1, 0}, -{ 3, s_2_139, 138, 1, 0}, -{ 4, s_2_140, 138, 1, 0}, -{ 3, s_2_141, -1, 1, 0}, -{ 6, s_2_142, 141, 1, 0}, -{ 8, s_2_143, 142, 5, 0}, -{ 4, s_2_144, -1, 1, 0}, -{ 5, s_2_145, 144, 1, 0}, -{ 6, s_2_146, 145, 2, 0}, -{ 4, s_2_147, -1, 1, 0}, -{ 4, s_2_148, -1, 1, 0}, -{ 5, s_2_149, -1, 1, 0}, -{ 5, s_2_150, -1, 1, 0}, -{ 3, s_2_151, -1, 1, 0}, -{ 3, s_2_152, -1, 1, 0}, -{ 4, s_2_153, 152, 1, 0}, -{ 5, s_2_154, 153, 1, 0}, -{ 5, s_2_155, 153, 1, 0}, -{ 3, s_2_156, -1, 1, 0}, -{ 5, s_2_157, 156, 1, 0}, -{ 8, s_2_158, 157, 1, 0}, -{ 7, s_2_159, 157, 1, 0}, -{ 9, s_2_160, 159, 1, 0}, -{ 5, s_2_161, 156, 1, 0}, -{ 3, s_2_162, -1, 1, 0}, -{ 4, s_2_163, -1, 1, 0}, -{ 4, s_2_164, -1, 1, 0}, -{ 5, s_2_165, 164, 1, 0}, -{ 6, s_2_166, 165, 1, 0}, -{ 3, s_2_167, -1, 1, 0}, -{ 3, s_2_168, -1, 1, 0}, -{ 3, s_2_169, -1, 1, 0}, -{ 5, s_2_170, 169, 1, 0}, -{ 5, s_2_171, 169, 1, 0}, -{ 2, s_2_172, -1, 1, 0}, -{ 2, s_2_173, -1, 1, 0}, -{ 2, s_2_174, -1, 1, 0}, -{ 3, s_2_175, 174, 1, 0}, -{ 2, s_2_176, -1, 1, 0}, -{ 4, s_2_177, -1, 1, 0}, -{ 7, s_2_178, 177, 1, 0}, -{ 6, s_2_179, 177, 1, 0}, -{ 8, s_2_180, 179, 1, 0}, -{ 4, s_2_181, -1, 1, 0}, -{ 2, s_2_182, -1, 1, 0}, -{ 3, s_2_183, -1, 1, 0}, -{ 3, s_2_184, -1, 1, 0}, -{ 4, s_2_185, 184, 1, 0}, -{ 4, s_2_186, 184, 1, 0}, -{ 5, s_2_187, 186, 1, 0}, -{ 7, s_2_188, 187, 1, 0}, -{ 2, s_2_189, -1, 1, 0}, -{ 5, s_2_190, -1, 1, 0}, -{ 5, s_2_191, -1, 1, 0}, -{ 5, s_2_192, -1, 1, 0}, -{ 4, s_2_193, -1, 1, 0}, -{ 5, s_2_194, -1, 1, 0}, -{ 4, s_2_195, -1, 1, 0}, -{ 1, s_2_196, -1, 1, 0}, -{ 2, s_2_197, 196, 1, 0}, -{ 3, s_2_198, 197, 1, 0}, -{ 4, s_2_199, 198, 1, 0} +static const struct among a_2[200] = { +{ 3, s_2_0, 0, 4, 0}, +{ 6, s_2_1, -1, 3, 0}, +{ 4, s_2_2, 0, 1, 0}, +{ 3, s_2_3, 0, 2, 0}, +{ 5, s_2_4, 0, 1, 0}, +{ 5, s_2_5, 0, 1, 0}, +{ 5, s_2_6, 0, 1, 0}, +{ 4, s_2_7, 0, 1, 0}, +{ 5, s_2_8, 0, 3, 0}, +{ 4, s_2_9, 0, 1, 0}, +{ 5, s_2_10, -1, 1, 0}, +{ 4, s_2_11, 0, 1, 0}, +{ 4, s_2_12, 0, 1, 0}, +{ 6, s_2_13, 0, 1, 0}, +{ 4, s_2_14, 0, 1, 0}, +{ 4, s_2_15, 0, 1, 0}, +{ 5, s_2_16, 0, 1, 0}, +{ 3, s_2_17, 0, 1, 0}, +{ 6, s_2_18, -1, 1, 0}, +{ 8, s_2_19, -1, 5, 0}, +{ 3, s_2_20, 0, 1, 0}, +{ 3, s_2_21, 0, 1, 0}, +{ 3, s_2_22, 0, 1, 0}, +{ 5, s_2_23, -1, 1, 0}, +{ 3, s_2_24, 0, 1, 0}, +{ 4, s_2_25, -1, 1, 0}, +{ 5, s_2_26, -1, 1, 0}, +{ 5, s_2_27, 0, 1, 0}, +{ 3, s_2_28, 0, 1, 0}, +{ 3, s_2_29, 0, 1, 0}, +{ 4, s_2_30, 0, 1, 0}, +{ 4, s_2_31, 0, 1, 0}, +{ 4, s_2_32, 0, 1, 0}, +{ 3, s_2_33, 0, 1, 0}, +{ 3, s_2_34, 0, 1, 0}, +{ 3, s_2_35, 0, 1, 0}, +{ 4, s_2_36, 0, 1, 0}, +{ 7, s_2_37, -1, 1, 0}, +{ 7, s_2_38, -2, 1, 0}, +{ 3, s_2_39, 0, 1, 0}, +{ 5, s_2_40, -1, 1, 0}, +{ 3, s_2_41, 0, 1, 0}, +{ 5, s_2_42, 0, 3, 0}, +{ 2, s_2_43, 0, 4, 0}, +{ 5, s_2_44, -1, 1, 0}, +{ 3, s_2_45, 0, 1, 0}, +{ 3, s_2_46, 0, 1, 0}, +{ 2, s_2_47, 0, 1, 0}, +{ 4, s_2_48, 0, 1, 0}, +{ 3, s_2_49, 0, 1, 0}, +{ 4, s_2_50, -1, 1, 0}, +{ 4, s_2_51, -2, 1, 0}, +{ 4, s_2_52, 0, 1, 0}, +{ 7, s_2_53, -1, 1, 0}, +{ 7, s_2_54, -2, 1, 0}, +{ 6, s_2_55, -3, 1, 0}, +{ 4, s_2_56, 0, 1, 0}, +{ 4, s_2_57, 0, 1, 0}, +{ 4, s_2_58, 0, 1, 0}, +{ 3, s_2_59, 0, 1, 0}, +{ 3, s_2_60, 0, 1, 0}, +{ 4, s_2_61, 0, 3, 0}, +{ 3, s_2_62, 0, 1, 0}, +{ 4, s_2_63, 0, 1, 0}, +{ 2, s_2_64, 0, 1, 0}, +{ 2, s_2_65, 0, 1, 0}, +{ 3, s_2_66, 0, 1, 0}, +{ 3, s_2_67, 0, 1, 0}, +{ 4, s_2_68, 0, 1, 0}, +{ 4, s_2_69, 0, 1, 0}, +{ 5, s_2_70, 0, 1, 0}, +{ 5, s_2_71, 0, 1, 0}, +{ 5, s_2_72, 0, 1, 0}, +{ 5, s_2_73, 0, 1, 0}, +{ 7, s_2_74, -1, 5, 0}, +{ 4, s_2_75, 0, 1, 0}, +{ 5, s_2_76, 0, 1, 0}, +{ 2, s_2_77, 0, 1, 0}, +{ 6, s_2_78, -1, 1, 0}, +{ 4, s_2_79, -2, 1, 0}, +{ 4, s_2_80, -3, 1, 0}, +{ 4, s_2_81, -4, 1, 0}, +{ 5, s_2_82, -5, 1, 0}, +{ 3, s_2_83, 0, 1, 0}, +{ 2, s_2_84, 0, 1, 0}, +{ 3, s_2_85, -1, 1, 0}, +{ 3, s_2_86, 0, 1, 0}, +{ 5, s_2_87, 0, 1, 0}, +{ 3, s_2_88, 0, 4, 0}, +{ 6, s_2_89, -1, 3, 0}, +{ 3, s_2_90, 0, 1, 0}, +{ 4, s_2_91, 0, 1, 0}, +{ 4, s_2_92, 0, 2, 0}, +{ 6, s_2_93, 0, 1, 0}, +{ 6, s_2_94, 0, 1, 0}, +{ 6, s_2_95, 0, 1, 0}, +{ 5, s_2_96, 0, 1, 0}, +{ 6, s_2_97, 0, 3, 0}, +{ 5, s_2_98, 0, 1, 0}, +{ 5, s_2_99, 0, 1, 0}, +{ 5, s_2_100, 0, 1, 0}, +{ 5, s_2_101, 0, 1, 0}, +{ 7, s_2_102, 0, 1, 0}, +{ 4, s_2_103, 0, 1, 0}, +{ 5, s_2_104, -1, 1, 0}, +{ 5, s_2_105, -2, 1, 0}, +{ 4, s_2_106, 0, 1, 0}, +{ 7, s_2_107, -1, 1, 0}, +{ 9, s_2_108, -1, 5, 0}, +{ 6, s_2_109, 0, 1, 0}, +{ 5, s_2_110, 0, 1, 0}, +{ 8, s_2_111, -1, 1, 0}, +{ 4, s_2_112, 0, 1, 0}, +{ 4, s_2_113, 0, 1, 0}, +{ 4, s_2_114, 0, 1, 0}, +{ 5, s_2_115, -1, 1, 0}, +{ 6, s_2_116, -1, 1, 0}, +{ 5, s_2_117, 0, 1, 0}, +{ 4, s_2_118, 0, 1, 0}, +{ 4, s_2_119, 0, 1, 0}, +{ 5, s_2_120, 0, 1, 0}, +{ 5, s_2_121, 0, 1, 0}, +{ 4, s_2_122, 0, 1, 0}, +{ 4, s_2_123, 0, 1, 0}, +{ 5, s_2_124, 0, 1, 0}, +{ 8, s_2_125, -1, 1, 0}, +{ 8, s_2_126, -2, 1, 0}, +{ 5, s_2_127, 0, 4, 0}, +{ 8, s_2_128, -1, 3, 0}, +{ 4, s_2_129, 0, 1, 0}, +{ 6, s_2_130, -1, 1, 0}, +{ 6, s_2_131, 0, 3, 0}, +{ 9, s_2_132, 0, 1, 0}, +{ 4, s_2_133, 0, 1, 0}, +{ 4, s_2_134, 0, 1, 0}, +{ 5, s_2_135, 0, 3, 0}, +{ 4, s_2_136, 0, 1, 0}, +{ 5, s_2_137, 0, 1, 0}, +{ 2, s_2_138, 0, 1, 0}, +{ 3, s_2_139, -1, 1, 0}, +{ 4, s_2_140, -2, 1, 0}, +{ 3, s_2_141, 0, 1, 0}, +{ 6, s_2_142, -1, 1, 0}, +{ 8, s_2_143, -1, 5, 0}, +{ 4, s_2_144, 0, 1, 0}, +{ 5, s_2_145, -1, 1, 0}, +{ 6, s_2_146, -1, 2, 0}, +{ 4, s_2_147, 0, 1, 0}, +{ 4, s_2_148, 0, 1, 0}, +{ 5, s_2_149, 0, 1, 0}, +{ 5, s_2_150, 0, 1, 0}, +{ 3, s_2_151, 0, 1, 0}, +{ 3, s_2_152, 0, 1, 0}, +{ 4, s_2_153, -1, 1, 0}, +{ 5, s_2_154, -1, 1, 0}, +{ 5, s_2_155, -2, 1, 0}, +{ 3, s_2_156, 0, 1, 0}, +{ 5, s_2_157, -1, 1, 0}, +{ 8, s_2_158, -1, 1, 0}, +{ 7, s_2_159, -2, 1, 0}, +{ 9, s_2_160, -1, 1, 0}, +{ 5, s_2_161, -5, 1, 0}, +{ 3, s_2_162, 0, 1, 0}, +{ 4, s_2_163, 0, 1, 0}, +{ 4, s_2_164, 0, 1, 0}, +{ 5, s_2_165, -1, 1, 0}, +{ 6, s_2_166, -1, 1, 0}, +{ 3, s_2_167, 0, 1, 0}, +{ 3, s_2_168, 0, 1, 0}, +{ 3, s_2_169, 0, 1, 0}, +{ 5, s_2_170, -1, 1, 0}, +{ 5, s_2_171, -2, 1, 0}, +{ 2, s_2_172, 0, 1, 0}, +{ 2, s_2_173, 0, 1, 0}, +{ 2, s_2_174, 0, 1, 0}, +{ 3, s_2_175, -1, 1, 0}, +{ 2, s_2_176, 0, 1, 0}, +{ 4, s_2_177, 0, 1, 0}, +{ 7, s_2_178, -1, 1, 0}, +{ 6, s_2_179, -2, 1, 0}, +{ 8, s_2_180, -1, 1, 0}, +{ 4, s_2_181, 0, 1, 0}, +{ 2, s_2_182, 0, 1, 0}, +{ 3, s_2_183, 0, 1, 0}, +{ 3, s_2_184, 0, 1, 0}, +{ 4, s_2_185, -1, 1, 0}, +{ 4, s_2_186, -2, 1, 0}, +{ 5, s_2_187, -1, 1, 0}, +{ 7, s_2_188, -1, 1, 0}, +{ 2, s_2_189, 0, 1, 0}, +{ 5, s_2_190, 0, 1, 0}, +{ 5, s_2_191, 0, 1, 0}, +{ 5, s_2_192, 0, 1, 0}, +{ 4, s_2_193, 0, 1, 0}, +{ 5, s_2_194, 0, 1, 0}, +{ 4, s_2_195, 0, 1, 0}, +{ 1, s_2_196, 0, 1, 0}, +{ 2, s_2_197, -1, 1, 0}, +{ 3, s_2_198, -1, 1, 0}, +{ 4, s_2_199, -1, 1, 0} }; static const symbol s_3_0[3] = { 'a', 'b', 'a' }; @@ -830,292 +837,290 @@ static const symbol s_3_279[3] = { 'i', 'r', 0xE9 }; static const symbol s_3_280[1] = { 0xED }; static const symbol s_3_281[2] = { 'i', 0xEF }; static const symbol s_3_282[2] = { 'i', 0xF3 }; - -static const struct among a_3[283] = -{ -{ 3, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 4, s_3_2, -1, 1, 0}, -{ 4, s_3_3, -1, 1, 0}, -{ 3, s_3_4, -1, 1, 0}, -{ 3, s_3_5, -1, 1, 0}, -{ 3, s_3_6, -1, 1, 0}, -{ 3, s_3_7, -1, 1, 0}, -{ 2, s_3_8, -1, 1, 0}, -{ 4, s_3_9, 8, 1, 0}, -{ 4, s_3_10, 8, 1, 0}, -{ 3, s_3_11, -1, 1, 0}, -{ 4, s_3_12, -1, 1, 0}, -{ 3, s_3_13, -1, 1, 0}, -{ 5, s_3_14, -1, 1, 0}, -{ 3, s_3_15, -1, 1, 0}, -{ 3, s_3_16, -1, 1, 0}, -{ 3, s_3_17, -1, 1, 0}, -{ 4, s_3_18, -1, 1, 0}, -{ 2, s_3_19, -1, 1, 0}, -{ 4, s_3_20, 19, 1, 0}, -{ 4, s_3_21, 19, 1, 0}, -{ 4, s_3_22, 19, 1, 0}, -{ 2, s_3_23, -1, 1, 0}, -{ 3, s_3_24, -1, 1, 0}, -{ 3, s_3_25, -1, 1, 0}, -{ 2, s_3_26, -1, 1, 0}, -{ 2, s_3_27, -1, 1, 0}, -{ 2, s_3_28, -1, 1, 0}, -{ 2, s_3_29, -1, 1, 0}, -{ 2, s_3_30, -1, 1, 0}, -{ 3, s_3_31, 30, 1, 0}, -{ 3, s_3_32, -1, 1, 0}, -{ 4, s_3_33, -1, 1, 0}, -{ 4, s_3_34, -1, 1, 0}, -{ 4, s_3_35, -1, 1, 0}, -{ 2, s_3_36, -1, 1, 0}, -{ 3, s_3_37, -1, 1, 0}, -{ 5, s_3_38, -1, 1, 0}, -{ 4, s_3_39, -1, 1, 0}, -{ 4, s_3_40, -1, 1, 0}, -{ 2, s_3_41, -1, 1, 0}, -{ 2, s_3_42, -1, 1, 0}, -{ 4, s_3_43, 42, 1, 0}, -{ 4, s_3_44, 42, 1, 0}, -{ 4, s_3_45, 42, 1, 0}, -{ 4, s_3_46, 42, 1, 0}, -{ 5, s_3_47, 42, 1, 0}, -{ 5, s_3_48, 42, 1, 0}, -{ 5, s_3_49, 42, 1, 0}, -{ 5, s_3_50, 42, 1, 0}, -{ 4, s_3_51, 42, 1, 0}, -{ 4, s_3_52, 42, 1, 0}, -{ 4, s_3_53, 42, 1, 0}, -{ 5, s_3_54, 42, 1, 0}, -{ 3, s_3_55, 42, 1, 0}, -{ 5, s_3_56, 55, 1, 0}, -{ 5, s_3_57, 55, 1, 0}, -{ 5, s_3_58, -1, 1, 0}, -{ 5, s_3_59, -1, 1, 0}, -{ 5, s_3_60, -1, 1, 0}, -{ 5, s_3_61, -1, 1, 0}, -{ 5, s_3_62, -1, 1, 0}, -{ 5, s_3_63, -1, 1, 0}, -{ 5, s_3_64, -1, 1, 0}, -{ 2, s_3_65, -1, 1, 0}, -{ 2, s_3_66, -1, 1, 0}, -{ 4, s_3_67, 66, 1, 0}, -{ 5, s_3_68, 66, 1, 0}, -{ 4, s_3_69, 66, 1, 0}, -{ 5, s_3_70, 66, 1, 0}, -{ 4, s_3_71, 66, 1, 0}, -{ 3, s_3_72, 66, 1, 0}, -{ 5, s_3_73, 72, 1, 0}, -{ 5, s_3_74, 72, 1, 0}, -{ 5, s_3_75, 72, 1, 0}, -{ 2, s_3_76, -1, 1, 0}, -{ 3, s_3_77, 76, 1, 0}, -{ 5, s_3_78, 77, 1, 0}, -{ 5, s_3_79, 77, 1, 0}, -{ 4, s_3_80, 76, 1, 0}, -{ 4, s_3_81, 76, 1, 0}, -{ 4, s_3_82, 76, 1, 0}, -{ 4, s_3_83, 76, 1, 0}, -{ 4, s_3_84, 76, 1, 0}, -{ 4, s_3_85, 76, 1, 0}, -{ 5, s_3_86, 76, 1, 0}, -{ 5, s_3_87, 76, 1, 0}, -{ 5, s_3_88, 76, 1, 0}, -{ 5, s_3_89, 76, 1, 0}, -{ 5, s_3_90, 76, 1, 0}, -{ 5, s_3_91, 76, 1, 0}, -{ 6, s_3_92, 76, 1, 0}, -{ 6, s_3_93, 76, 1, 0}, -{ 6, s_3_94, 76, 1, 0}, -{ 4, s_3_95, 76, 1, 0}, -{ 4, s_3_96, 76, 1, 0}, -{ 5, s_3_97, 96, 1, 0}, -{ 4, s_3_98, 76, 1, 0}, -{ 3, s_3_99, 76, 1, 0}, -{ 2, s_3_100, -1, 1, 0}, -{ 4, s_3_101, 100, 1, 0}, -{ 3, s_3_102, 100, 1, 0}, -{ 4, s_3_103, 102, 1, 0}, -{ 5, s_3_104, 102, 1, 0}, -{ 5, s_3_105, 102, 1, 0}, -{ 5, s_3_106, 102, 1, 0}, -{ 5, s_3_107, 102, 1, 0}, -{ 6, s_3_108, 100, 1, 0}, -{ 5, s_3_109, 100, 1, 0}, -{ 4, s_3_110, -1, 1, 0}, -{ 5, s_3_111, -1, 1, 0}, -{ 4, s_3_112, -1, 1, 0}, -{ 4, s_3_113, -1, 1, 0}, -{ 4, s_3_114, -1, 1, 0}, -{ 3, s_3_115, -1, 1, 0}, -{ 3, s_3_116, -1, 1, 0}, -{ 3, s_3_117, -1, 1, 0}, -{ 4, s_3_118, -1, 2, 0}, -{ 5, s_3_119, -1, 1, 0}, -{ 2, s_3_120, -1, 1, 0}, -{ 3, s_3_121, -1, 1, 0}, -{ 4, s_3_122, 121, 1, 0}, -{ 3, s_3_123, -1, 1, 0}, -{ 4, s_3_124, -1, 1, 0}, -{ 2, s_3_125, -1, 1, 0}, -{ 4, s_3_126, 125, 1, 0}, -{ 2, s_3_127, -1, 1, 0}, -{ 5, s_3_128, 127, 1, 0}, -{ 2, s_3_129, -1, 1, 0}, -{ 4, s_3_130, -1, 1, 0}, -{ 2, s_3_131, -1, 1, 0}, -{ 4, s_3_132, 131, 1, 0}, -{ 4, s_3_133, 131, 1, 0}, -{ 4, s_3_134, 131, 1, 0}, -{ 4, s_3_135, 131, 1, 0}, -{ 5, s_3_136, 131, 1, 0}, -{ 3, s_3_137, 131, 1, 0}, -{ 5, s_3_138, 137, 1, 0}, -{ 5, s_3_139, 137, 1, 0}, -{ 5, s_3_140, 137, 1, 0}, -{ 3, s_3_141, -1, 1, 0}, -{ 2, s_3_142, -1, 1, 0}, -{ 4, s_3_143, 142, 1, 0}, -{ 4, s_3_144, 142, 1, 0}, -{ 4, s_3_145, 142, 1, 0}, -{ 4, s_3_146, 142, 1, 0}, -{ 5, s_3_147, 142, 1, 0}, -{ 3, s_3_148, 142, 1, 0}, -{ 5, s_3_149, 148, 1, 0}, -{ 5, s_3_150, 148, 1, 0}, -{ 4, s_3_151, 142, 1, 0}, -{ 4, s_3_152, 142, 1, 0}, -{ 6, s_3_153, 142, 1, 0}, -{ 4, s_3_154, 142, 1, 0}, -{ 4, s_3_155, 142, 1, 0}, -{ 5, s_3_156, 142, 1, 0}, -{ 5, s_3_157, 142, 1, 0}, -{ 5, s_3_158, 142, 1, 0}, -{ 5, s_3_159, 142, 1, 0}, -{ 5, s_3_160, 142, 1, 0}, -{ 4, s_3_161, 142, 1, 0}, -{ 6, s_3_162, 161, 1, 0}, -{ 6, s_3_163, 161, 1, 0}, -{ 4, s_3_164, 142, 1, 0}, -{ 4, s_3_165, 142, 1, 0}, -{ 5, s_3_166, 165, 1, 0}, -{ 4, s_3_167, 142, 1, 0}, -{ 3, s_3_168, 142, 1, 0}, -{ 5, s_3_169, -1, 1, 0}, -{ 5, s_3_170, -1, 1, 0}, -{ 6, s_3_171, -1, 1, 0}, -{ 4, s_3_172, -1, 1, 0}, -{ 6, s_3_173, 172, 1, 0}, -{ 6, s_3_174, 172, 1, 0}, -{ 6, s_3_175, 172, 1, 0}, -{ 5, s_3_176, -1, 1, 0}, -{ 6, s_3_177, -1, 1, 0}, -{ 6, s_3_178, -1, 1, 0}, -{ 6, s_3_179, -1, 1, 0}, -{ 4, s_3_180, -1, 1, 0}, -{ 3, s_3_181, -1, 1, 0}, -{ 4, s_3_182, 181, 1, 0}, -{ 5, s_3_183, 181, 1, 0}, -{ 5, s_3_184, 181, 1, 0}, -{ 5, s_3_185, 181, 1, 0}, -{ 5, s_3_186, 181, 1, 0}, -{ 6, s_3_187, -1, 1, 0}, -{ 5, s_3_188, -1, 1, 0}, -{ 5, s_3_189, -1, 1, 0}, -{ 3, s_3_190, -1, 1, 0}, -{ 5, s_3_191, -1, 1, 0}, -{ 5, s_3_192, -1, 1, 0}, -{ 5, s_3_193, -1, 1, 0}, -{ 3, s_3_194, -1, 1, 0}, -{ 4, s_3_195, -1, 1, 0}, -{ 4, s_3_196, -1, 1, 0}, -{ 4, s_3_197, -1, 1, 0}, -{ 6, s_3_198, 197, 1, 0}, -{ 6, s_3_199, 197, 1, 0}, -{ 7, s_3_200, 197, 1, 0}, -{ 5, s_3_201, 197, 1, 0}, -{ 7, s_3_202, 201, 1, 0}, -{ 7, s_3_203, 201, 1, 0}, -{ 7, s_3_204, 201, 1, 0}, -{ 6, s_3_205, -1, 1, 0}, -{ 6, s_3_206, -1, 1, 0}, -{ 6, s_3_207, -1, 1, 0}, -{ 6, s_3_208, -1, 1, 0}, -{ 7, s_3_209, -1, 1, 0}, -{ 4, s_3_210, -1, 1, 0}, -{ 5, s_3_211, -1, 1, 0}, -{ 3, s_3_212, -1, 1, 0}, -{ 5, s_3_213, 212, 1, 0}, -{ 3, s_3_214, -1, 1, 0}, -{ 3, s_3_215, -1, 1, 0}, -{ 3, s_3_216, -1, 1, 0}, -{ 4, s_3_217, -1, 1, 0}, -{ 2, s_3_218, -1, 1, 0}, -{ 4, s_3_219, 218, 1, 0}, -{ 4, s_3_220, 218, 1, 0}, -{ 4, s_3_221, -1, 1, 0}, -{ 4, s_3_222, -1, 1, 0}, -{ 4, s_3_223, -1, 1, 0}, -{ 2, s_3_224, -1, 1, 0}, -{ 4, s_3_225, 224, 1, 0}, -{ 2, s_3_226, -1, 1, 0}, -{ 3, s_3_227, -1, 1, 0}, -{ 2, s_3_228, -1, 1, 0}, -{ 2, s_3_229, -1, 1, 0}, -{ 3, s_3_230, -1, 1, 0}, -{ 3, s_3_231, -1, 1, 0}, -{ 3, s_3_232, -1, 1, 0}, -{ 2, s_3_233, -1, 1, 0}, -{ 2, s_3_234, -1, 1, 0}, -{ 2, s_3_235, -1, 1, 0}, -{ 4, s_3_236, 235, 1, 0}, -{ 3, s_3_237, -1, 1, 0}, -{ 4, s_3_238, -1, 1, 0}, -{ 4, s_3_239, -1, 1, 0}, -{ 4, s_3_240, -1, 1, 0}, -{ 4, s_3_241, -1, 1, 0}, -{ 4, s_3_242, -1, 1, 0}, -{ 5, s_3_243, -1, 1, 0}, -{ 5, s_3_244, -1, 1, 0}, -{ 7, s_3_245, 244, 1, 0}, -{ 5, s_3_246, -1, 1, 0}, -{ 5, s_3_247, -1, 1, 0}, -{ 5, s_3_248, -1, 1, 0}, -{ 5, s_3_249, -1, 1, 0}, -{ 4, s_3_250, -1, 1, 0}, -{ 4, s_3_251, -1, 1, 0}, -{ 5, s_3_252, -1, 1, 0}, -{ 3, s_3_253, -1, 1, 0}, -{ 5, s_3_254, 253, 1, 0}, -{ 3, s_3_255, -1, 1, 0}, -{ 5, s_3_256, 255, 1, 0}, -{ 5, s_3_257, 255, 1, 0}, -{ 5, s_3_258, -1, 1, 0}, -{ 5, s_3_259, -1, 1, 0}, -{ 5, s_3_260, -1, 1, 0}, -{ 5, s_3_261, -1, 1, 0}, -{ 5, s_3_262, -1, 1, 0}, -{ 5, s_3_263, -1, 1, 0}, -{ 2, s_3_264, -1, 1, 0}, -{ 2, s_3_265, -1, 1, 0}, -{ 3, s_3_266, 265, 1, 0}, -{ 2, s_3_267, -1, 1, 0}, -{ 3, s_3_268, -1, 1, 0}, -{ 2, s_3_269, -1, 1, 0}, -{ 3, s_3_270, -1, 1, 0}, -{ 3, s_3_271, -1, 1, 0}, -{ 4, s_3_272, -1, 1, 0}, -{ 3, s_3_273, -1, 1, 0}, -{ 3, s_3_274, -1, 1, 0}, -{ 3, s_3_275, -1, 1, 0}, -{ 3, s_3_276, -1, 1, 0}, -{ 3, s_3_277, -1, 1, 0}, -{ 3, s_3_278, -1, 1, 0}, -{ 3, s_3_279, -1, 1, 0}, -{ 1, s_3_280, -1, 1, 0}, -{ 2, s_3_281, -1, 1, 0}, -{ 2, s_3_282, -1, 1, 0} +static const struct among a_3[283] = { +{ 3, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 4, s_3_2, 0, 1, 0}, +{ 4, s_3_3, 0, 1, 0}, +{ 3, s_3_4, 0, 1, 0}, +{ 3, s_3_5, 0, 1, 0}, +{ 3, s_3_6, 0, 1, 0}, +{ 3, s_3_7, 0, 1, 0}, +{ 2, s_3_8, 0, 1, 0}, +{ 4, s_3_9, -1, 1, 0}, +{ 4, s_3_10, -2, 1, 0}, +{ 3, s_3_11, 0, 1, 0}, +{ 4, s_3_12, 0, 1, 0}, +{ 3, s_3_13, 0, 1, 0}, +{ 5, s_3_14, 0, 1, 0}, +{ 3, s_3_15, 0, 1, 0}, +{ 3, s_3_16, 0, 1, 0}, +{ 3, s_3_17, 0, 1, 0}, +{ 4, s_3_18, 0, 1, 0}, +{ 2, s_3_19, 0, 1, 0}, +{ 4, s_3_20, -1, 1, 0}, +{ 4, s_3_21, -2, 1, 0}, +{ 4, s_3_22, -3, 1, 0}, +{ 2, s_3_23, 0, 1, 0}, +{ 3, s_3_24, 0, 1, 0}, +{ 3, s_3_25, 0, 1, 0}, +{ 2, s_3_26, 0, 1, 0}, +{ 2, s_3_27, 0, 1, 0}, +{ 2, s_3_28, 0, 1, 0}, +{ 2, s_3_29, 0, 1, 0}, +{ 2, s_3_30, 0, 1, 0}, +{ 3, s_3_31, -1, 1, 0}, +{ 3, s_3_32, 0, 1, 0}, +{ 4, s_3_33, 0, 1, 0}, +{ 4, s_3_34, 0, 1, 0}, +{ 4, s_3_35, 0, 1, 0}, +{ 2, s_3_36, 0, 1, 0}, +{ 3, s_3_37, 0, 1, 0}, +{ 5, s_3_38, 0, 1, 0}, +{ 4, s_3_39, 0, 1, 0}, +{ 4, s_3_40, 0, 1, 0}, +{ 2, s_3_41, 0, 1, 0}, +{ 2, s_3_42, 0, 1, 0}, +{ 4, s_3_43, -1, 1, 0}, +{ 4, s_3_44, -2, 1, 0}, +{ 4, s_3_45, -3, 1, 0}, +{ 4, s_3_46, -4, 1, 0}, +{ 5, s_3_47, -5, 1, 0}, +{ 5, s_3_48, -6, 1, 0}, +{ 5, s_3_49, -7, 1, 0}, +{ 5, s_3_50, -8, 1, 0}, +{ 4, s_3_51, -9, 1, 0}, +{ 4, s_3_52, -10, 1, 0}, +{ 4, s_3_53, -11, 1, 0}, +{ 5, s_3_54, -12, 1, 0}, +{ 3, s_3_55, -13, 1, 0}, +{ 5, s_3_56, -1, 1, 0}, +{ 5, s_3_57, -2, 1, 0}, +{ 5, s_3_58, 0, 1, 0}, +{ 5, s_3_59, 0, 1, 0}, +{ 5, s_3_60, 0, 1, 0}, +{ 5, s_3_61, 0, 1, 0}, +{ 5, s_3_62, 0, 1, 0}, +{ 5, s_3_63, 0, 1, 0}, +{ 5, s_3_64, 0, 1, 0}, +{ 2, s_3_65, 0, 1, 0}, +{ 2, s_3_66, 0, 1, 0}, +{ 4, s_3_67, -1, 1, 0}, +{ 5, s_3_68, -2, 1, 0}, +{ 4, s_3_69, -3, 1, 0}, +{ 5, s_3_70, -4, 1, 0}, +{ 4, s_3_71, -5, 1, 0}, +{ 3, s_3_72, -6, 1, 0}, +{ 5, s_3_73, -1, 1, 0}, +{ 5, s_3_74, -2, 1, 0}, +{ 5, s_3_75, -3, 1, 0}, +{ 2, s_3_76, 0, 1, 0}, +{ 3, s_3_77, -1, 1, 0}, +{ 5, s_3_78, -1, 1, 0}, +{ 5, s_3_79, -2, 1, 0}, +{ 4, s_3_80, -4, 1, 0}, +{ 4, s_3_81, -5, 1, 0}, +{ 4, s_3_82, -6, 1, 0}, +{ 4, s_3_83, -7, 1, 0}, +{ 4, s_3_84, -8, 1, 0}, +{ 4, s_3_85, -9, 1, 0}, +{ 5, s_3_86, -10, 1, 0}, +{ 5, s_3_87, -11, 1, 0}, +{ 5, s_3_88, -12, 1, 0}, +{ 5, s_3_89, -13, 1, 0}, +{ 5, s_3_90, -14, 1, 0}, +{ 5, s_3_91, -15, 1, 0}, +{ 6, s_3_92, -16, 1, 0}, +{ 6, s_3_93, -17, 1, 0}, +{ 6, s_3_94, -18, 1, 0}, +{ 4, s_3_95, -19, 1, 0}, +{ 4, s_3_96, -20, 1, 0}, +{ 5, s_3_97, -1, 1, 0}, +{ 4, s_3_98, -22, 1, 0}, +{ 3, s_3_99, -23, 1, 0}, +{ 2, s_3_100, 0, 1, 0}, +{ 4, s_3_101, -1, 1, 0}, +{ 3, s_3_102, -2, 1, 0}, +{ 4, s_3_103, -1, 1, 0}, +{ 5, s_3_104, -2, 1, 0}, +{ 5, s_3_105, -3, 1, 0}, +{ 5, s_3_106, -4, 1, 0}, +{ 5, s_3_107, -5, 1, 0}, +{ 6, s_3_108, -8, 1, 0}, +{ 5, s_3_109, -9, 1, 0}, +{ 4, s_3_110, 0, 1, 0}, +{ 5, s_3_111, 0, 1, 0}, +{ 4, s_3_112, 0, 1, 0}, +{ 4, s_3_113, 0, 1, 0}, +{ 4, s_3_114, 0, 1, 0}, +{ 3, s_3_115, 0, 1, 0}, +{ 3, s_3_116, 0, 1, 0}, +{ 3, s_3_117, 0, 1, 0}, +{ 4, s_3_118, 0, 2, 0}, +{ 5, s_3_119, 0, 1, 0}, +{ 2, s_3_120, 0, 1, 0}, +{ 3, s_3_121, 0, 1, 0}, +{ 4, s_3_122, -1, 1, 0}, +{ 3, s_3_123, 0, 1, 0}, +{ 4, s_3_124, 0, 1, 0}, +{ 2, s_3_125, 0, 1, 0}, +{ 4, s_3_126, -1, 1, 0}, +{ 2, s_3_127, 0, 1, 0}, +{ 5, s_3_128, -1, 1, 0}, +{ 2, s_3_129, 0, 1, 0}, +{ 4, s_3_130, 0, 1, 0}, +{ 2, s_3_131, 0, 1, 0}, +{ 4, s_3_132, -1, 1, 0}, +{ 4, s_3_133, -2, 1, 0}, +{ 4, s_3_134, -3, 1, 0}, +{ 4, s_3_135, -4, 1, 0}, +{ 5, s_3_136, -5, 1, 0}, +{ 3, s_3_137, -6, 1, 0}, +{ 5, s_3_138, -1, 1, 0}, +{ 5, s_3_139, -2, 1, 0}, +{ 5, s_3_140, -3, 1, 0}, +{ 3, s_3_141, 0, 1, 0}, +{ 2, s_3_142, 0, 1, 0}, +{ 4, s_3_143, -1, 1, 0}, +{ 4, s_3_144, -2, 1, 0}, +{ 4, s_3_145, -3, 1, 0}, +{ 4, s_3_146, -4, 1, 0}, +{ 5, s_3_147, -5, 1, 0}, +{ 3, s_3_148, -6, 1, 0}, +{ 5, s_3_149, -1, 1, 0}, +{ 5, s_3_150, -2, 1, 0}, +{ 4, s_3_151, -9, 1, 0}, +{ 4, s_3_152, -10, 1, 0}, +{ 6, s_3_153, -11, 1, 0}, +{ 4, s_3_154, -12, 1, 0}, +{ 4, s_3_155, -13, 1, 0}, +{ 5, s_3_156, -14, 1, 0}, +{ 5, s_3_157, -15, 1, 0}, +{ 5, s_3_158, -16, 1, 0}, +{ 5, s_3_159, -17, 1, 0}, +{ 5, s_3_160, -18, 1, 0}, +{ 4, s_3_161, -19, 1, 0}, +{ 6, s_3_162, -1, 1, 0}, +{ 6, s_3_163, -2, 1, 0}, +{ 4, s_3_164, -22, 1, 0}, +{ 4, s_3_165, -23, 1, 0}, +{ 5, s_3_166, -1, 1, 0}, +{ 4, s_3_167, -25, 1, 0}, +{ 3, s_3_168, -26, 1, 0}, +{ 5, s_3_169, 0, 1, 0}, +{ 5, s_3_170, 0, 1, 0}, +{ 6, s_3_171, 0, 1, 0}, +{ 4, s_3_172, 0, 1, 0}, +{ 6, s_3_173, -1, 1, 0}, +{ 6, s_3_174, -2, 1, 0}, +{ 6, s_3_175, -3, 1, 0}, +{ 5, s_3_176, 0, 1, 0}, +{ 6, s_3_177, 0, 1, 0}, +{ 6, s_3_178, 0, 1, 0}, +{ 6, s_3_179, 0, 1, 0}, +{ 4, s_3_180, 0, 1, 0}, +{ 3, s_3_181, 0, 1, 0}, +{ 4, s_3_182, -1, 1, 0}, +{ 5, s_3_183, -2, 1, 0}, +{ 5, s_3_184, -3, 1, 0}, +{ 5, s_3_185, -4, 1, 0}, +{ 5, s_3_186, -5, 1, 0}, +{ 6, s_3_187, 0, 1, 0}, +{ 5, s_3_188, 0, 1, 0}, +{ 5, s_3_189, 0, 1, 0}, +{ 3, s_3_190, 0, 1, 0}, +{ 5, s_3_191, 0, 1, 0}, +{ 5, s_3_192, 0, 1, 0}, +{ 5, s_3_193, 0, 1, 0}, +{ 3, s_3_194, 0, 1, 0}, +{ 4, s_3_195, 0, 1, 0}, +{ 4, s_3_196, 0, 1, 0}, +{ 4, s_3_197, 0, 1, 0}, +{ 6, s_3_198, -1, 1, 0}, +{ 6, s_3_199, -2, 1, 0}, +{ 7, s_3_200, -3, 1, 0}, +{ 5, s_3_201, -4, 1, 0}, +{ 7, s_3_202, -1, 1, 0}, +{ 7, s_3_203, -2, 1, 0}, +{ 7, s_3_204, -3, 1, 0}, +{ 6, s_3_205, 0, 1, 0}, +{ 6, s_3_206, 0, 1, 0}, +{ 6, s_3_207, 0, 1, 0}, +{ 6, s_3_208, 0, 1, 0}, +{ 7, s_3_209, 0, 1, 0}, +{ 4, s_3_210, 0, 1, 0}, +{ 5, s_3_211, 0, 1, 0}, +{ 3, s_3_212, 0, 1, 0}, +{ 5, s_3_213, -1, 1, 0}, +{ 3, s_3_214, 0, 1, 0}, +{ 3, s_3_215, 0, 1, 0}, +{ 3, s_3_216, 0, 1, 0}, +{ 4, s_3_217, 0, 1, 0}, +{ 2, s_3_218, 0, 1, 0}, +{ 4, s_3_219, -1, 1, 0}, +{ 4, s_3_220, -2, 1, 0}, +{ 4, s_3_221, 0, 1, 0}, +{ 4, s_3_222, 0, 1, 0}, +{ 4, s_3_223, 0, 1, 0}, +{ 2, s_3_224, 0, 1, 0}, +{ 4, s_3_225, -1, 1, 0}, +{ 2, s_3_226, 0, 1, 0}, +{ 3, s_3_227, 0, 1, 0}, +{ 2, s_3_228, 0, 1, 0}, +{ 2, s_3_229, 0, 1, 0}, +{ 3, s_3_230, 0, 1, 0}, +{ 3, s_3_231, 0, 1, 0}, +{ 3, s_3_232, 0, 1, 0}, +{ 2, s_3_233, 0, 1, 0}, +{ 2, s_3_234, 0, 1, 0}, +{ 2, s_3_235, 0, 1, 0}, +{ 4, s_3_236, -1, 1, 0}, +{ 3, s_3_237, 0, 1, 0}, +{ 4, s_3_238, 0, 1, 0}, +{ 4, s_3_239, 0, 1, 0}, +{ 4, s_3_240, 0, 1, 0}, +{ 4, s_3_241, 0, 1, 0}, +{ 4, s_3_242, 0, 1, 0}, +{ 5, s_3_243, 0, 1, 0}, +{ 5, s_3_244, 0, 1, 0}, +{ 7, s_3_245, -1, 1, 0}, +{ 5, s_3_246, 0, 1, 0}, +{ 5, s_3_247, 0, 1, 0}, +{ 5, s_3_248, 0, 1, 0}, +{ 5, s_3_249, 0, 1, 0}, +{ 4, s_3_250, 0, 1, 0}, +{ 4, s_3_251, 0, 1, 0}, +{ 5, s_3_252, 0, 1, 0}, +{ 3, s_3_253, 0, 1, 0}, +{ 5, s_3_254, -1, 1, 0}, +{ 3, s_3_255, 0, 1, 0}, +{ 5, s_3_256, -1, 1, 0}, +{ 5, s_3_257, -2, 1, 0}, +{ 5, s_3_258, 0, 1, 0}, +{ 5, s_3_259, 0, 1, 0}, +{ 5, s_3_260, 0, 1, 0}, +{ 5, s_3_261, 0, 1, 0}, +{ 5, s_3_262, 0, 1, 0}, +{ 5, s_3_263, 0, 1, 0}, +{ 2, s_3_264, 0, 1, 0}, +{ 2, s_3_265, 0, 1, 0}, +{ 3, s_3_266, -1, 1, 0}, +{ 2, s_3_267, 0, 1, 0}, +{ 3, s_3_268, 0, 1, 0}, +{ 2, s_3_269, 0, 1, 0}, +{ 3, s_3_270, 0, 1, 0}, +{ 3, s_3_271, 0, 1, 0}, +{ 4, s_3_272, 0, 1, 0}, +{ 3, s_3_273, 0, 1, 0}, +{ 3, s_3_274, 0, 1, 0}, +{ 3, s_3_275, 0, 1, 0}, +{ 3, s_3_276, 0, 1, 0}, +{ 3, s_3_277, 0, 1, 0}, +{ 3, s_3_278, 0, 1, 0}, +{ 3, s_3_279, 0, 1, 0}, +{ 1, s_3_280, 0, 1, 0}, +{ 2, s_3_281, 0, 1, 0}, +{ 2, s_3_282, 0, 1, 0} }; static const symbol s_4_0[1] = { 'a' }; @@ -1140,117 +1145,107 @@ static const symbol s_4_18[1] = { 0xEC }; static const symbol s_4_19[1] = { 0xED }; static const symbol s_4_20[1] = { 0xEF }; static const symbol s_4_21[1] = { 0xF3 }; - -static const struct among a_4[22] = -{ -{ 1, s_4_0, -1, 1, 0}, -{ 1, s_4_1, -1, 1, 0}, -{ 1, s_4_2, -1, 1, 0}, -{ 2, s_4_3, -1, 1, 0}, -{ 1, s_4_4, -1, 1, 0}, -{ 2, s_4_5, -1, 1, 0}, -{ 1, s_4_6, -1, 1, 0}, -{ 2, s_4_7, 6, 1, 0}, -{ 2, s_4_8, 6, 1, 0}, -{ 2, s_4_9, 6, 1, 0}, -{ 2, s_4_10, -1, 1, 0}, -{ 2, s_4_11, -1, 1, 0}, -{ 2, s_4_12, -1, 1, 0}, -{ 3, s_4_13, -1, 2, 0}, -{ 3, s_4_14, -1, 1, 0}, -{ 1, s_4_15, -1, 1, 0}, -{ 1, s_4_16, -1, 1, 0}, -{ 1, s_4_17, -1, 1, 0}, -{ 1, s_4_18, -1, 1, 0}, -{ 1, s_4_19, -1, 1, 0}, -{ 1, s_4_20, -1, 1, 0}, -{ 1, s_4_21, -1, 1, 0} +static const struct among a_4[22] = { +{ 1, s_4_0, 0, 1, 0}, +{ 1, s_4_1, 0, 1, 0}, +{ 1, s_4_2, 0, 1, 0}, +{ 2, s_4_3, 0, 1, 0}, +{ 1, s_4_4, 0, 1, 0}, +{ 2, s_4_5, 0, 1, 0}, +{ 1, s_4_6, 0, 1, 0}, +{ 2, s_4_7, -1, 1, 0}, +{ 2, s_4_8, -2, 1, 0}, +{ 2, s_4_9, -3, 1, 0}, +{ 2, s_4_10, 0, 1, 0}, +{ 2, s_4_11, 0, 1, 0}, +{ 2, s_4_12, 0, 1, 0}, +{ 3, s_4_13, 0, 2, 0}, +{ 3, s_4_14, 0, 1, 0}, +{ 1, s_4_15, 0, 1, 0}, +{ 1, s_4_16, 0, 1, 0}, +{ 1, s_4_17, 0, 1, 0}, +{ 1, s_4_18, 0, 1, 0}, +{ 1, s_4_19, 0, 1, 0}, +{ 1, s_4_20, 0, 1, 0}, +{ 1, s_4_21, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 129, 81, 6, 10 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'i' }; -static const symbol s_3[] = { 'o' }; -static const symbol s_4[] = { 'u' }; -static const symbol s_5[] = { '.' }; -static const symbol s_6[] = { 'l', 'o', 'g' }; -static const symbol s_7[] = { 'i', 'c' }; -static const symbol s_8[] = { 'c' }; -static const symbol s_9[] = { 'i', 'c' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_cleaning(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 13); + among_var = find_among(z, a_0, 13, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; @@ -1261,29 +1256,31 @@ static int r_cleaning(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_attached_pronoun(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1634850 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_1, 39)) return 0; + if (!find_among_b(z, a_1, 39, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1292,47 +1289,57 @@ static int r_attached_pronoun(struct SN_env * z) { static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_2, 200); + among_var = find_among_b(z, a_2, 200, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_6); + { + int ret = slice_from_s(z, 3, s_6); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 5: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; @@ -1343,23 +1350,27 @@ static int r_standard_suffix(struct SN_env * z) { static int r_verb_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_3, 283); + among_var = find_among_b(z, a_3, 283, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1370,23 +1381,27 @@ static int r_verb_suffix(struct SN_env * z) { static int r_residual_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_4, 22); + among_var = find_among_b(z, a_4, 22, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; @@ -1395,53 +1410,70 @@ static int r_residual_suffix(struct SN_env * z) { } extern int catalan_ISO_8859_1_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_attached_pronoun(z); + { + int v_1 = z->l - z->c; + { + int ret = r_attached_pronoun(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_verb_suffix(z); + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_verb_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_residual_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_residual_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; - { int c5 = z->c; - { int ret = r_cleaning(z); + { + int v_5 = z->c; + { + int ret = r_cleaning(z); if (ret < 0) return ret; } - z->c = c5; + z->c = v_5; } return 1; } -extern struct SN_env * catalan_ISO_8859_1_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * catalan_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void catalan_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void catalan_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_danish.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_danish.c index 372902d495c7b..30bfe059e8b1a 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_danish.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_danish.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from danish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_danish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; + symbol * s_ch; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,23 +21,17 @@ extern int danish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_undouble(struct SN_env * z); static int r_other_suffix(struct SN_env * z); static int r_consonant_pair(struct SN_env * z); static int r_main_suffix(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * danish_ISO_8859_1_create_env(void); -extern void danish_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 's', 't' }; +static const symbol s_1[] = { 'i', 'g' }; +static const symbol s_2[] = { 'l', 0xF8, 's' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 'h', 'e', 'd' }; static const symbol s_0_1[5] = { 'e', 't', 'h', 'e', 'd' }; static const symbol s_0_2[4] = { 'e', 'r', 'e', 'd' }; @@ -58,54 +64,50 @@ static const symbol s_0_28[3] = { 'e', 't', 's' }; static const symbol s_0_29[5] = { 'e', 'r', 'e', 't', 's' }; static const symbol s_0_30[2] = { 'e', 't' }; static const symbol s_0_31[4] = { 'e', 'r', 'e', 't' }; - -static const struct among a_0[32] = -{ -{ 3, s_0_0, -1, 1, 0}, -{ 5, s_0_1, 0, 1, 0}, -{ 4, s_0_2, -1, 1, 0}, -{ 1, s_0_3, -1, 1, 0}, -{ 5, s_0_4, 3, 1, 0}, -{ 4, s_0_5, 3, 1, 0}, -{ 6, s_0_6, 5, 1, 0}, -{ 3, s_0_7, 3, 1, 0}, -{ 4, s_0_8, 3, 1, 0}, -{ 3, s_0_9, 3, 1, 0}, -{ 2, s_0_10, -1, 1, 0}, -{ 5, s_0_11, 10, 1, 0}, -{ 4, s_0_12, 10, 1, 0}, -{ 2, s_0_13, -1, 1, 0}, -{ 5, s_0_14, 13, 1, 0}, -{ 4, s_0_15, 13, 1, 0}, -{ 1, s_0_16, -1, 2, 0}, -{ 4, s_0_17, 16, 1, 0}, -{ 2, s_0_18, 16, 1, 0}, -{ 5, s_0_19, 18, 1, 0}, -{ 7, s_0_20, 19, 1, 0}, -{ 4, s_0_21, 18, 1, 0}, -{ 5, s_0_22, 18, 1, 0}, -{ 4, s_0_23, 18, 1, 0}, -{ 3, s_0_24, 16, 1, 0}, -{ 6, s_0_25, 24, 1, 0}, -{ 5, s_0_26, 24, 1, 0}, -{ 3, s_0_27, 16, 1, 0}, -{ 3, s_0_28, 16, 1, 0}, -{ 5, s_0_29, 28, 1, 0}, -{ 2, s_0_30, -1, 1, 0}, -{ 4, s_0_31, 30, 1, 0} +static const struct among a_0[32] = { +{ 3, s_0_0, 0, 1, 0}, +{ 5, s_0_1, -1, 1, 0}, +{ 4, s_0_2, 0, 1, 0}, +{ 1, s_0_3, 0, 1, 0}, +{ 5, s_0_4, -1, 1, 0}, +{ 4, s_0_5, -2, 1, 0}, +{ 6, s_0_6, -1, 1, 0}, +{ 3, s_0_7, -4, 1, 0}, +{ 4, s_0_8, -5, 1, 0}, +{ 3, s_0_9, -6, 1, 0}, +{ 2, s_0_10, 0, 1, 0}, +{ 5, s_0_11, -1, 1, 0}, +{ 4, s_0_12, -2, 1, 0}, +{ 2, s_0_13, 0, 1, 0}, +{ 5, s_0_14, -1, 1, 0}, +{ 4, s_0_15, -2, 1, 0}, +{ 1, s_0_16, 0, 2, 0}, +{ 4, s_0_17, -1, 1, 0}, +{ 2, s_0_18, -2, 1, 0}, +{ 5, s_0_19, -1, 1, 0}, +{ 7, s_0_20, -1, 1, 0}, +{ 4, s_0_21, -3, 1, 0}, +{ 5, s_0_22, -4, 1, 0}, +{ 4, s_0_23, -5, 1, 0}, +{ 3, s_0_24, -8, 1, 0}, +{ 6, s_0_25, -1, 1, 0}, +{ 5, s_0_26, -2, 1, 0}, +{ 3, s_0_27, -11, 1, 0}, +{ 3, s_0_28, -12, 1, 0}, +{ 5, s_0_29, -1, 1, 0}, +{ 2, s_0_30, 0, 1, 0}, +{ 4, s_0_31, -1, 1, 0} }; static const symbol s_1_0[2] = { 'g', 'd' }; static const symbol s_1_1[2] = { 'd', 't' }; static const symbol s_1_2[2] = { 'g', 't' }; static const symbol s_1_3[2] = { 'k', 't' }; - -static const struct among a_1[4] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 2, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0} +static const struct among a_1[4] = { +{ 2, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 2, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0} }; static const symbol s_2_0[2] = { 'i', 'g' }; @@ -113,14 +115,12 @@ static const symbol s_2_1[3] = { 'l', 'i', 'g' }; static const symbol s_2_2[4] = { 'e', 'l', 'i', 'g' }; static const symbol s_2_3[3] = { 'e', 'l', 's' }; static const symbol s_2_4[4] = { 'l', 0xF8, 's', 't' }; - -static const struct among a_2[5] = -{ -{ 2, s_2_0, -1, 1, 0}, -{ 3, s_2_1, 0, 1, 0}, -{ 4, s_2_2, 1, 1, 0}, -{ 3, s_2_3, -1, 1, 0}, -{ 4, s_2_4, -1, 2, 0} +static const struct among a_2[5] = { +{ 2, s_2_0, 0, 1, 0}, +{ 3, s_2_1, -1, 1, 0}, +{ 4, s_2_2, -1, 1, 0}, +{ 3, s_2_3, 0, 1, 0}, +{ 4, s_2_4, 0, 2, 0} }; static const unsigned char g_c[] = { 119, 223, 119, 1 }; @@ -129,56 +129,57 @@ static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 static const unsigned char g_s_ending[] = { 239, 254, 42, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 }; -static const symbol s_0[] = { 's', 't' }; -static const symbol s_1[] = { 'i', 'g' }; -static const symbol s_2[] = { 'l', 0xF8, 's' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c_test1 = z->c; -z->c = z->c + 3; - if (z->c > z->l) return 0; - z->I[0] = z->c; - z->c = c_test1; + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping(z, g_v, 97, 248, 1); + if (ret < 0) return 0; + z->c += ret; } - - if (out_grouping(z, g_v, 97, 248, 1) < 0) return 0; - { int ret = in_grouping(z, g_v, 97, 248, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab0; - z->I[1] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: return 1; } static int r_main_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851440 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 32); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851440 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_0, 32, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b(z, g_s_ending, 97, 229, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -187,23 +188,25 @@ static int r_main_suffix(struct SN_env * z) { } static int r_consonant_pair(struct SN_env * z) { - { int m_test1 = z->l - z->c; - - { int mlimit2; - if (z->c < z->I[1]) return 0; - mlimit2 = z->lb; z->lb = z->I[1]; + { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 116)) { z->lb = mlimit2; return 0; } - if (!find_among_b(z, a_1, 4)) { z->lb = mlimit2; return 0; } + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 116)) { z->lb = v_2; return 0; } + if (!find_among_b(z, a_1, 4, 0)) { z->lb = v_2; return 0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } - z->c = z->l - m_test1; + z->c = z->l - v_1; } if (z->c <= z->lb) return 0; z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -211,42 +214,48 @@ static int r_consonant_pair(struct SN_env * z) { static int r_other_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; if (!(eq_s_b(z, 2, s_0))) goto lab0; z->bra = z->c; if (!(eq_s_b(z, 2, s_1))) goto lab0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - - { int mlimit2; - if (z->c < z->I[1]) return 0; - mlimit2 = z->lb; z->lb = z->I[1]; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit2; return 0; } - among_var = find_among_b(z, a_2, 5); - if (!among_var) { z->lb = mlimit2; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_2; return 0; } + among_var = find_among_b(z, a_2, 5, 0); + if (!among_var) { z->lb = v_2; return 0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } break; case 2: - { int ret = slice_from_s(z, 3, s_2); + { + int ret = slice_from_s(z, 3, s_2); if (ret < 0) return ret; } break; @@ -255,62 +264,91 @@ static int r_other_suffix(struct SN_env * z) { } static int r_undouble(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (in_grouping_b(z, g_c, 98, 122, 0)) { z->lb = mlimit1; return 0; } + if (in_grouping_b(z, g_c, 98, 122, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->S[0] = slice_to(z, z->S[0]); - if (z->S[0] == 0) return -1; - z->lb = mlimit1; + { + int ret = slice_to(z, &((SN_local *)z)->s_ch); + if (ret < 0) return ret; + } + z->lb = v_1; } - if (!(eq_v_b(z, z->S[0]))) return 0; - { int ret = slice_del(z); + if (!(eq_v_b(z, ((SN_local *)z)->s_ch))) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int danish_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_main_suffix(z); + { + int v_2 = z->l - z->c; + { + int ret = r_main_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_other_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_other_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_undouble(z); + { + int v_5 = z->l - z->c; + { + int ret = r_undouble(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } z->c = z->lb; return 1; } -extern struct SN_env * danish_ISO_8859_1_create_env(void) { return SN_create_env(1, 2); } +extern struct SN_env * danish_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->s_ch = NULL; -extern void danish_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 1); } + if ((((SN_local *)z)->s_ch = create_s()) == NULL) { + danish_ISO_8859_1_close_env(z); + return NULL; + } + } + return z; +} + +extern void danish_ISO_8859_1_close_env(struct SN_env * z) { + if (z) { + lose_s(((SN_local *)z)->s_ch); + } + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch.c index d2bc1baebef10..424bff8258cfb 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch.c @@ -1,6 +1,20 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from dutch.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_dutch.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_GE_removed; + symbol * s_ch; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,601 +23,2003 @@ extern int dutch_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif -static int r_standard_suffix(struct SN_env * z); -static int r_undouble(struct SN_env * z); + +static int r_measure(struct SN_env * z); +static int r_Lose_infix(struct SN_env * z); +static int r_Lose_prefix(struct SN_env * z); +static int r_Step_1c(struct SN_env * z); +static int r_Step_6(struct SN_env * z); +static int r_Step_7(struct SN_env * z); +static int r_Step_4(struct SN_env * z); +static int r_Step_3(struct SN_env * z); +static int r_Step_2(struct SN_env * z); +static int r_Step_1(struct SN_env * z); +static int r_lengthen_V(struct SN_env * z); +static int r_VX(struct SN_env * z); +static int r_V(struct SN_env * z); +static int r_C(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); -static int r_mark_regions(struct SN_env * z); -static int r_en_ending(struct SN_env * z); -static int r_e_ending(struct SN_env * z); -static int r_postlude(struct SN_env * z); -static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * dutch_ISO_8859_1_create_env(void); -extern void dutch_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'i', 'j' }; +static const symbol s_1[] = { 'i', 'j' }; +static const symbol s_2[] = { 'i', 'j' }; +static const symbol s_3[] = { 'e', 0xEB, 'e' }; +static const symbol s_4[] = { 'i', 'e', 'e' }; +static const symbol s_5[] = { 'i', 'e' }; +static const symbol s_6[] = { 'a', 'r' }; +static const symbol s_7[] = { 'e', 'r' }; +static const symbol s_8[] = { 'e' }; +static const symbol s_9[] = { 0xE9 }; +static const symbol s_10[] = { 'a', 'u' }; +static const symbol s_11[] = { 'h', 'e', 'd' }; +static const symbol s_12[] = { 'h', 'e', 'i', 'd' }; +static const symbol s_13[] = { 'n', 'd' }; +static const symbol s_14[] = { 'n', 'd' }; +static const symbol s_15[] = { '\'', 't' }; +static const symbol s_16[] = { 'e', 't' }; +static const symbol s_17[] = { 'r', 'n', 't' }; +static const symbol s_18[] = { 'r', 'n' }; +static const symbol s_19[] = { 'i', 'n', 'k' }; +static const symbol s_20[] = { 'i', 'n', 'g' }; +static const symbol s_21[] = { 'm', 'p' }; +static const symbol s_22[] = { 'm' }; +static const symbol s_23[] = { 'g' }; +static const symbol s_24[] = { 'l', 'i', 'j', 'k' }; +static const symbol s_25[] = { 'i', 's', 'c', 'h' }; +static const symbol s_26[] = { 't' }; +static const symbol s_27[] = { 's' }; +static const symbol s_28[] = { 'r' }; +static const symbol s_29[] = { 'l' }; +static const symbol s_30[] = { 'e', 'n' }; +static const symbol s_31[] = { 'i', 'e', 'f' }; +static const symbol s_32[] = { 'e', 'e', 'r' }; +static const symbol s_33[] = { 'r' }; +static const symbol s_34[] = { 'i', 'l', 'd' }; +static const symbol s_35[] = { 'e', 'r' }; +static const symbol s_36[] = { 'a', 'a', 'r' }; +static const symbol s_37[] = { 'f' }; +static const symbol s_38[] = { 'g' }; +static const symbol s_39[] = { 't' }; +static const symbol s_40[] = { 'd' }; +static const symbol s_41[] = { 'i', 'e' }; +static const symbol s_42[] = { 'e', 'e', 'r' }; +static const symbol s_43[] = { 'n' }; +static const symbol s_44[] = { 'l' }; +static const symbol s_45[] = { 'r' }; +static const symbol s_46[] = { 't', 'e', 'e', 'r' }; +static const symbol s_47[] = { 'l', 'i', 'j', 'k' }; +static const symbol s_48[] = { 'i', 'n', 'n' }; +static const symbol s_49[] = { 'k' }; +static const symbol s_50[] = { 'f' }; +static const symbol s_51[] = { 'p' }; +static const symbol s_52[] = { 'b' }; +static const symbol s_53[] = { 'c' }; +static const symbol s_54[] = { 'd' }; +static const symbol s_55[] = { 'f' }; +static const symbol s_56[] = { 'g' }; +static const symbol s_57[] = { 'h' }; +static const symbol s_58[] = { 'j' }; +static const symbol s_59[] = { 'k' }; +static const symbol s_60[] = { 'l' }; +static const symbol s_61[] = { 'm' }; +static const symbol s_62[] = { 'n' }; +static const symbol s_63[] = { 'p' }; +static const symbol s_64[] = { 'q' }; +static const symbol s_65[] = { 'r' }; +static const symbol s_66[] = { 's' }; +static const symbol s_67[] = { 't' }; +static const symbol s_68[] = { 'v' }; +static const symbol s_69[] = { 'w' }; +static const symbol s_70[] = { 'x' }; +static const symbol s_71[] = { 'z' }; +static const symbol s_72[] = { 'i', 'n' }; +static const symbol s_73[] = { 'n' }; +static const symbol s_74[] = { 'e', 'n' }; +static const symbol s_75[] = { 'g', 'e' }; +static const symbol s_76[] = { 'i', 'j' }; +static const symbol s_77[] = { 'i', 'j' }; +static const symbol s_78[] = { 'e' }; +static const symbol s_79[] = { 'i' }; +static const symbol s_80[] = { 'g', 'e' }; +static const symbol s_81[] = { 'i', 'j' }; +static const symbol s_82[] = { 'i', 'j' }; +static const symbol s_83[] = { 'e' }; +static const symbol s_84[] = { 'i' }; +static const symbol s_85[] = { 'i', 'j' }; +static const symbol s_86[] = { 'i', 'j' }; -#ifdef __cplusplus -} -#endif -static const symbol s_0_1[1] = { 0xE1 }; -static const symbol s_0_2[1] = { 0xE4 }; -static const symbol s_0_3[1] = { 0xE9 }; -static const symbol s_0_4[1] = { 0xEB }; -static const symbol s_0_5[1] = { 0xED }; -static const symbol s_0_6[1] = { 0xEF }; -static const symbol s_0_7[1] = { 0xF3 }; -static const symbol s_0_8[1] = { 0xF6 }; -static const symbol s_0_9[1] = { 0xFA }; -static const symbol s_0_10[1] = { 0xFC }; - -static const struct among a_0[11] = -{ -{ 0, 0, -1, 6, 0}, -{ 1, s_0_1, 0, 1, 0}, +static const symbol s_0_0[1] = { 'a' }; +static const symbol s_0_1[1] = { 'e' }; +static const symbol s_0_2[1] = { 'o' }; +static const symbol s_0_3[1] = { 'u' }; +static const symbol s_0_4[1] = { 0xE0 }; +static const symbol s_0_5[1] = { 0xE1 }; +static const symbol s_0_6[1] = { 0xE2 }; +static const symbol s_0_7[1] = { 0xE4 }; +static const symbol s_0_8[1] = { 0xE8 }; +static const symbol s_0_9[1] = { 0xE9 }; +static const symbol s_0_10[1] = { 0xEA }; +static const symbol s_0_11[2] = { 'e', 0xEB }; +static const symbol s_0_12[2] = { 'i', 0xEB }; +static const symbol s_0_13[1] = { 0xF2 }; +static const symbol s_0_14[1] = { 0xF3 }; +static const symbol s_0_15[1] = { 0xF4 }; +static const symbol s_0_16[1] = { 0xF6 }; +static const symbol s_0_17[1] = { 0xF9 }; +static const symbol s_0_18[1] = { 0xFA }; +static const symbol s_0_19[1] = { 0xFB }; +static const symbol s_0_20[1] = { 0xFC }; +static const struct among a_0[21] = { +{ 1, s_0_0, 0, 1, 0}, +{ 1, s_0_1, 0, 2, 0}, { 1, s_0_2, 0, 1, 0}, -{ 1, s_0_3, 0, 2, 0}, -{ 1, s_0_4, 0, 2, 0}, -{ 1, s_0_5, 0, 3, 0}, -{ 1, s_0_6, 0, 3, 0}, -{ 1, s_0_7, 0, 4, 0}, -{ 1, s_0_8, 0, 4, 0}, -{ 1, s_0_9, 0, 5, 0}, -{ 1, s_0_10, 0, 5, 0} +{ 1, s_0_3, 0, 1, 0}, +{ 1, s_0_4, 0, 1, 0}, +{ 1, s_0_5, 0, 1, 0}, +{ 1, s_0_6, 0, 1, 0}, +{ 1, s_0_7, 0, 1, 0}, +{ 1, s_0_8, 0, 2, 0}, +{ 1, s_0_9, 0, 2, 0}, +{ 1, s_0_10, 0, 2, 0}, +{ 2, s_0_11, 0, 3, 0}, +{ 2, s_0_12, 0, 4, 0}, +{ 1, s_0_13, 0, 1, 0}, +{ 1, s_0_14, 0, 1, 0}, +{ 1, s_0_15, 0, 1, 0}, +{ 1, s_0_16, 0, 1, 0}, +{ 1, s_0_17, 0, 1, 0}, +{ 1, s_0_18, 0, 1, 0}, +{ 1, s_0_19, 0, 1, 0}, +{ 1, s_0_20, 0, 1, 0} }; -static const symbol s_1_1[1] = { 'I' }; -static const symbol s_1_2[1] = { 'Y' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 1, s_1_1, 0, 2, 0}, -{ 1, s_1_2, 0, 1, 0} +static const symbol s_1_0[3] = { 'n', 'd', 'e' }; +static const symbol s_1_1[2] = { 'e', 'n' }; +static const symbol s_1_2[1] = { 's' }; +static const symbol s_1_3[2] = { '\'', 's' }; +static const symbol s_1_4[2] = { 'e', 's' }; +static const symbol s_1_5[3] = { 'i', 'e', 's' }; +static const symbol s_1_6[3] = { 'a', 'u', 's' }; +static const symbol s_1_7[2] = { 0xE9, 's' }; +static const struct among a_1[8] = { +{ 3, s_1_0, 0, 8, 0}, +{ 2, s_1_1, 0, 7, 0}, +{ 1, s_1_2, 0, 2, 0}, +{ 2, s_1_3, -1, 1, 0}, +{ 2, s_1_4, -2, 4, 0}, +{ 3, s_1_5, -1, 3, 0}, +{ 3, s_1_6, -4, 6, 0}, +{ 2, s_1_7, -5, 5, 0} }; -static const symbol s_2_0[2] = { 'd', 'd' }; -static const symbol s_2_1[2] = { 'k', 'k' }; -static const symbol s_2_2[2] = { 't', 't' }; - -static const struct among a_2[3] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0} +static const symbol s_2_0[2] = { 'd', 'e' }; +static const symbol s_2_1[2] = { 'g', 'e' }; +static const symbol s_2_2[5] = { 'i', 's', 'c', 'h', 'e' }; +static const symbol s_2_3[2] = { 'j', 'e' }; +static const symbol s_2_4[5] = { 'l', 'i', 'j', 'k', 'e' }; +static const symbol s_2_5[2] = { 'l', 'e' }; +static const symbol s_2_6[3] = { 'e', 'n', 'e' }; +static const symbol s_2_7[2] = { 'r', 'e' }; +static const symbol s_2_8[2] = { 's', 'e' }; +static const symbol s_2_9[2] = { 't', 'e' }; +static const symbol s_2_10[4] = { 'i', 'e', 'v', 'e' }; +static const struct among a_2[11] = { +{ 2, s_2_0, 0, 5, 0}, +{ 2, s_2_1, 0, 2, 0}, +{ 5, s_2_2, 0, 4, 0}, +{ 2, s_2_3, 0, 1, 0}, +{ 5, s_2_4, 0, 3, 0}, +{ 2, s_2_5, 0, 9, 0}, +{ 3, s_2_6, 0, 10, 0}, +{ 2, s_2_7, 0, 8, 0}, +{ 2, s_2_8, 0, 7, 0}, +{ 2, s_2_9, 0, 6, 0}, +{ 4, s_2_10, 0, 11, 0} }; -static const symbol s_3_0[3] = { 'e', 'n', 'e' }; -static const symbol s_3_1[2] = { 's', 'e' }; -static const symbol s_3_2[2] = { 'e', 'n' }; -static const symbol s_3_3[5] = { 'h', 'e', 'd', 'e', 'n' }; -static const symbol s_3_4[1] = { 's' }; - -static const struct among a_3[5] = -{ -{ 3, s_3_0, -1, 2, 0}, -{ 2, s_3_1, -1, 3, 0}, -{ 2, s_3_2, -1, 2, 0}, -{ 5, s_3_3, 2, 1, 0}, -{ 1, s_3_4, -1, 3, 0} +static const symbol s_3_0[4] = { 'h', 'e', 'i', 'd' }; +static const symbol s_3_1[3] = { 'f', 'i', 'e' }; +static const symbol s_3_2[3] = { 'g', 'i', 'e' }; +static const symbol s_3_3[4] = { 'a', 't', 'i', 'e' }; +static const symbol s_3_4[4] = { 'i', 's', 'm', 'e' }; +static const symbol s_3_5[3] = { 'i', 'n', 'g' }; +static const symbol s_3_6[4] = { 'a', 'r', 'i', 'j' }; +static const symbol s_3_7[4] = { 'e', 'r', 'i', 'j' }; +static const symbol s_3_8[3] = { 's', 'e', 'l' }; +static const symbol s_3_9[4] = { 'r', 'd', 'e', 'r' }; +static const symbol s_3_10[4] = { 's', 't', 'e', 'r' }; +static const symbol s_3_11[5] = { 'i', 't', 'e', 'i', 't' }; +static const symbol s_3_12[3] = { 'd', 's', 't' }; +static const symbol s_3_13[3] = { 't', 's', 't' }; +static const struct among a_3[14] = { +{ 4, s_3_0, 0, 3, 0}, +{ 3, s_3_1, 0, 7, 0}, +{ 3, s_3_2, 0, 8, 0}, +{ 4, s_3_3, 0, 1, 0}, +{ 4, s_3_4, 0, 5, 0}, +{ 3, s_3_5, 0, 5, 0}, +{ 4, s_3_6, 0, 6, 0}, +{ 4, s_3_7, 0, 5, 0}, +{ 3, s_3_8, 0, 3, 0}, +{ 4, s_3_9, 0, 4, 0}, +{ 4, s_3_10, 0, 3, 0}, +{ 5, s_3_11, 0, 2, 0}, +{ 3, s_3_12, 0, 10, 0}, +{ 3, s_3_13, 0, 9, 0} }; static const symbol s_4_0[3] = { 'e', 'n', 'd' }; -static const symbol s_4_1[2] = { 'i', 'g' }; -static const symbol s_4_2[3] = { 'i', 'n', 'g' }; -static const symbol s_4_3[4] = { 'l', 'i', 'j', 'k' }; -static const symbol s_4_4[4] = { 'b', 'a', 'a', 'r' }; -static const symbol s_4_5[3] = { 'b', 'a', 'r' }; - -static const struct among a_4[6] = -{ -{ 3, s_4_0, -1, 1, 0}, -{ 2, s_4_1, -1, 2, 0}, -{ 3, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 3, 0}, -{ 4, s_4_4, -1, 4, 0}, -{ 3, s_4_5, -1, 5, 0} +static const symbol s_4_1[5] = { 'a', 't', 'i', 'e', 'f' }; +static const symbol s_4_2[4] = { 'e', 'r', 'i', 'g' }; +static const symbol s_4_3[6] = { 'a', 'c', 'h', 't', 'i', 'g' }; +static const symbol s_4_4[6] = { 'i', 'o', 'n', 'e', 'e', 'l' }; +static const symbol s_4_5[4] = { 'b', 'a', 'a', 'r' }; +static const symbol s_4_6[4] = { 'l', 'a', 'a', 'r' }; +static const symbol s_4_7[4] = { 'n', 'a', 'a', 'r' }; +static const symbol s_4_8[4] = { 'r', 'a', 'a', 'r' }; +static const symbol s_4_9[6] = { 'e', 'r', 'i', 'g', 'e', 'r' }; +static const symbol s_4_10[8] = { 'a', 'c', 'h', 't', 'i', 'g', 'e', 'r' }; +static const symbol s_4_11[6] = { 'l', 'i', 'j', 'k', 'e', 'r' }; +static const symbol s_4_12[4] = { 't', 'a', 'n', 't' }; +static const symbol s_4_13[6] = { 'e', 'r', 'i', 'g', 's', 't' }; +static const symbol s_4_14[8] = { 'a', 'c', 'h', 't', 'i', 'g', 's', 't' }; +static const symbol s_4_15[6] = { 'l', 'i', 'j', 'k', 's', 't' }; +static const struct among a_4[16] = { +{ 3, s_4_0, 0, 9, 0}, +{ 5, s_4_1, 0, 2, 0}, +{ 4, s_4_2, 0, 9, 0}, +{ 6, s_4_3, 0, 3, 0}, +{ 6, s_4_4, 0, 1, 0}, +{ 4, s_4_5, 0, 3, 0}, +{ 4, s_4_6, 0, 5, 0}, +{ 4, s_4_7, 0, 4, 0}, +{ 4, s_4_8, 0, 6, 0}, +{ 6, s_4_9, 0, 9, 0}, +{ 8, s_4_10, 0, 3, 0}, +{ 6, s_4_11, 0, 8, 0}, +{ 4, s_4_12, 0, 7, 0}, +{ 6, s_4_13, 0, 9, 0}, +{ 8, s_4_14, 0, 3, 0}, +{ 6, s_4_15, 0, 8, 0} }; -static const symbol s_5_0[2] = { 'a', 'a' }; -static const symbol s_5_1[2] = { 'e', 'e' }; -static const symbol s_5_2[2] = { 'o', 'o' }; -static const symbol s_5_3[2] = { 'u', 'u' }; - -static const struct among a_5[4] = -{ -{ 2, s_5_0, -1, -1, 0}, -{ 2, s_5_1, -1, -1, 0}, -{ 2, s_5_2, -1, -1, 0}, -{ 2, s_5_3, -1, -1, 0} +static const symbol s_5_0[2] = { 'i', 'g' }; +static const symbol s_5_1[4] = { 'i', 'g', 'e', 'r' }; +static const symbol s_5_2[4] = { 'i', 'g', 's', 't' }; +static const struct among a_5[3] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 4, s_5_2, 0, 1, 0} }; -static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; - -static const unsigned char g_v_I[] = { 1, 0, 0, 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; +static const symbol s_6_0[2] = { 'f', 't' }; +static const symbol s_6_1[2] = { 'k', 't' }; +static const symbol s_6_2[2] = { 'p', 't' }; +static const struct among a_6[3] = { +{ 2, s_6_0, 0, 2, 0}, +{ 2, s_6_1, 0, 1, 0}, +{ 2, s_6_2, 0, 3, 0} +}; -static const unsigned char g_v_j[] = { 17, 67, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; +static const symbol s_7_0[2] = { 'b', 'b' }; +static const symbol s_7_1[2] = { 'c', 'c' }; +static const symbol s_7_2[2] = { 'd', 'd' }; +static const symbol s_7_3[2] = { 'f', 'f' }; +static const symbol s_7_4[2] = { 'g', 'g' }; +static const symbol s_7_5[2] = { 'h', 'h' }; +static const symbol s_7_6[2] = { 'j', 'j' }; +static const symbol s_7_7[2] = { 'k', 'k' }; +static const symbol s_7_8[2] = { 'l', 'l' }; +static const symbol s_7_9[2] = { 'm', 'm' }; +static const symbol s_7_10[2] = { 'n', 'n' }; +static const symbol s_7_11[2] = { 'p', 'p' }; +static const symbol s_7_12[2] = { 'q', 'q' }; +static const symbol s_7_13[2] = { 'r', 'r' }; +static const symbol s_7_14[2] = { 's', 's' }; +static const symbol s_7_15[2] = { 't', 't' }; +static const symbol s_7_16[1] = { 'v' }; +static const symbol s_7_17[2] = { 'v', 'v' }; +static const symbol s_7_18[2] = { 'w', 'w' }; +static const symbol s_7_19[2] = { 'x', 'x' }; +static const symbol s_7_20[1] = { 'z' }; +static const symbol s_7_21[2] = { 'z', 'z' }; +static const struct among a_7[22] = { +{ 2, s_7_0, 0, 1, 0}, +{ 2, s_7_1, 0, 2, 0}, +{ 2, s_7_2, 0, 3, 0}, +{ 2, s_7_3, 0, 4, 0}, +{ 2, s_7_4, 0, 5, 0}, +{ 2, s_7_5, 0, 6, 0}, +{ 2, s_7_6, 0, 7, 0}, +{ 2, s_7_7, 0, 8, 0}, +{ 2, s_7_8, 0, 9, 0}, +{ 2, s_7_9, 0, 10, 0}, +{ 2, s_7_10, 0, 11, 0}, +{ 2, s_7_11, 0, 12, 0}, +{ 2, s_7_12, 0, 13, 0}, +{ 2, s_7_13, 0, 14, 0}, +{ 2, s_7_14, 0, 15, 0}, +{ 2, s_7_15, 0, 16, 0}, +{ 1, s_7_16, 0, 4, 0}, +{ 2, s_7_17, -1, 17, 0}, +{ 2, s_7_18, 0, 18, 0}, +{ 2, s_7_19, 0, 19, 0}, +{ 1, s_7_20, 0, 15, 0}, +{ 2, s_7_21, -1, 20, 0} +}; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'i' }; -static const symbol s_3[] = { 'o' }; -static const symbol s_4[] = { 'u' }; -static const symbol s_5[] = { 'Y' }; -static const symbol s_6[] = { 'I' }; -static const symbol s_7[] = { 'Y' }; -static const symbol s_8[] = { 'y' }; -static const symbol s_9[] = { 'i' }; -static const symbol s_10[] = { 'g', 'e', 'm' }; -static const symbol s_11[] = { 'h', 'e', 'i', 'd' }; -static const symbol s_12[] = { 'h', 'e', 'i', 'd' }; -static const symbol s_13[] = { 'e', 'n' }; -static const symbol s_14[] = { 'i', 'g' }; +static const symbol s_8_0[1] = { 'd' }; +static const symbol s_8_1[1] = { 't' }; +static const struct among a_8[2] = { +{ 1, s_8_0, 0, 1, 0}, +{ 1, s_8_1, 0, 2, 0} +}; -static int r_prelude(struct SN_env * z) { - int among_var; - { int c_test1 = z->c; - while(1) { - int c2 = z->c; - z->bra = z->c; - if (z->c >= z->l || z->p[z->c + 0] >> 5 != 7 || !((340306450 >> (z->p[z->c + 0] & 0x1f)) & 1)) among_var = 6; else - among_var = find_among(z, a_0, 11); - z->ket = z->c; - switch (among_var) { - case 1: - { int ret = slice_from_s(z, 1, s_0); - if (ret < 0) return ret; - } - break; - case 2: - { int ret = slice_from_s(z, 1, s_1); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = slice_from_s(z, 1, s_2); - if (ret < 0) return ret; - } - break; - case 4: - { int ret = slice_from_s(z, 1, s_3); - if (ret < 0) return ret; - } - break; - case 5: - { int ret = slice_from_s(z, 1, s_4); - if (ret < 0) return ret; - } - break; - case 6: - if (z->c >= z->l) goto lab0; - z->c++; - break; - } - continue; - lab0: - z->c = c2; - break; - } - z->c = c_test1; - } - { int c3 = z->c; - z->bra = z->c; - if (z->c == z->l || z->p[z->c] != 'y') { z->c = c3; goto lab1; } - z->c++; - z->ket = z->c; - { int ret = slice_from_s(z, 1, s_5); - if (ret < 0) return ret; - } - lab1: - ; - } - while(1) { - int c4 = z->c; - while(1) { - int c5 = z->c; - if (in_grouping(z, g_v, 97, 232, 0)) goto lab3; - z->bra = z->c; - { int c6 = z->c; - if (z->c == z->l || z->p[z->c] != 'i') goto lab5; - z->c++; - z->ket = z->c; - if (in_grouping(z, g_v, 97, 232, 0)) goto lab5; - { int ret = slice_from_s(z, 1, s_6); - if (ret < 0) return ret; - } - goto lab4; - lab5: - z->c = c6; - if (z->c == z->l || z->p[z->c] != 'y') goto lab3; - z->c++; - z->ket = z->c; - { int ret = slice_from_s(z, 1, s_7); - if (ret < 0) return ret; - } - } - lab4: - z->c = c5; - break; - lab3: - z->c = c5; - if (z->c >= z->l) goto lab2; - z->c++; - } - continue; - lab2: - z->c = c4; - break; - } - return 1; -} +static const symbol s_9_1[3] = { 'e', 'f', 't' }; +static const symbol s_9_2[3] = { 'v', 'a', 'a' }; +static const symbol s_9_3[3] = { 'v', 'a', 'l' }; +static const symbol s_9_4[4] = { 'v', 'a', 'l', 'i' }; +static const symbol s_9_5[4] = { 'v', 'a', 'r', 'e' }; +static const struct among a_9[6] = { +{ 0, 0, 0, -1, 0}, +{ 3, s_9_1, -1, 1, 0}, +{ 3, s_9_2, -2, 1, 0}, +{ 3, s_9_3, -3, 1, 0}, +{ 4, s_9_4, -1, -1, 0}, +{ 4, s_9_5, -5, 1, 0} +}; -static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - { int c_test1 = z->c; -z->c = z->c + 3; - if (z->c > z->l) return 0; - z->I[0] = z->c; - z->c = c_test1; - } +static const symbol s_10_0[1] = { 0xEB }; +static const symbol s_10_1[1] = { 0xEF }; +static const struct among a_10[2] = { +{ 1, s_10_0, 0, 1, 0}, +{ 1, s_10_1, 0, 2, 0} +}; - { - int ret = out_grouping(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } +static const symbol s_11_0[1] = { 0xEB }; +static const symbol s_11_1[1] = { 0xEF }; +static const struct among a_11[2] = { +{ 1, s_11_0, 0, 1, 0}, +{ 1, s_11_1, 0, 2, 0} +}; - { - int ret = in_grouping(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } - z->I[2] = z->c; +static const unsigned char g_E[] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120 }; - if (z->I[2] >= z->I[0]) goto lab0; - z->I[2] = z->I[0]; -lab0: +static const unsigned char g_AIOU[] = { 1, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 11, 120, 46, 15 }; - { - int ret = out_grouping(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } +static const unsigned char g_AEIOU[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 139, 127, 46, 15 }; - { - int ret = in_grouping(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } - z->I[1] = z->c; - return 1; -} +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 139, 127, 46, 15 }; -static int r_postlude(struct SN_env * z) { - int among_var; - while(1) { - int c1 = z->c; - z->bra = z->c; - if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 89)) among_var = 3; else - among_var = find_among(z, a_1, 3); - z->ket = z->c; - switch (among_var) { - case 1: - { int ret = slice_from_s(z, 1, s_8); - if (ret < 0) return ret; - } - break; - case 2: - { int ret = slice_from_s(z, 1, s_9); - if (ret < 0) return ret; - } - break; - case 3: - if (z->c >= z->l) goto lab0; - z->c++; - break; - } - continue; - lab0: - z->c = c1; - break; - } - return 1; -} +static const unsigned char g_v_WX[] = { 17, 65, 208, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 139, 127, 46, 15 }; static int r_R1(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } -static int r_undouble(struct SN_env * z) { - { int m_test1 = z->l - z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1050640 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_2, 3)) return 0; - z->c = z->l - m_test1; - } - z->ket = z->c; - if (z->c <= z->lb) return 0; - z->c--; - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; +static int r_V(struct SN_env * z) { + { + int v_1 = z->l - z->c; + do { + int v_2 = z->l - z->c; + if (in_grouping_b(z, g_v, 97, 252, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_0))) return 0; + } while (0); + z->c = z->l - v_1; } return 1; } -static int r_e_ending(struct SN_env * z) { - z->I[3] = 0; - z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; - z->c--; - z->bra = z->c; - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m_test1 = z->l - z->c; - if (out_grouping_b(z, g_v, 97, 232, 0)) return 0; - z->c = z->l - m_test1; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[3] = 1; - { int ret = r_undouble(z); - if (ret <= 0) return ret; +static int r_VX(struct SN_env * z) { + { + int v_1 = z->l - z->c; + if (z->c <= z->lb) return 0; + z->c--; + do { + int v_2 = z->l - z->c; + if (in_grouping_b(z, g_v, 97, 252, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_1))) return 0; + } while (0); + z->c = z->l - v_1; } return 1; } -static int r_en_ending(struct SN_env * z) { - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m1 = z->l - z->c; (void)m1; - if (out_grouping_b(z, g_v, 97, 232, 0)) return 0; - z->c = z->l - m1; - { int m2 = z->l - z->c; (void)m2; - if (!(eq_s_b(z, 3, s_10))) goto lab0; +static int r_C(struct SN_env * z) { + { + int v_1 = z->l - z->c; + { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_2))) goto lab0; return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - { int ret = r_undouble(z); - if (ret <= 0) return ret; + if (out_grouping_b(z, g_v, 97, 252, 0)) return 0; + z->c = z->l - v_1; } return 1; } -static int r_standard_suffix(struct SN_env * z) { +static int r_lengthen_V(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; + if (out_grouping_b(z, g_v_WX, 97, 252, 0)) goto lab0; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540704 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; - among_var = find_among_b(z, a_3, 5); + among_var = find_among_b(z, a_0, 21, 0); if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); - if (ret == 0) goto lab0; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + if (out_grouping_b(z, g_AEIOU, 97, 252, 0)) goto lab1; + break; + lab1: + z->c = z->l - v_3; + if (z->c > z->lb) goto lab0; + } while (0); + z->c = z->l - v_2; + } + { + int ret = slice_to(z, &((SN_local *)z)->s_ch); if (ret < 0) return ret; } - { int ret = slice_from_s(z, 4, s_11); + { + int saved_c = z->c; + int ret = insert_v(z, z->c, z->c, ((SN_local *)z)->s_ch); + z->c = saved_c; if (ret < 0) return ret; } break; case 2: - { int ret = r_en_ending(z); - if (ret == 0) goto lab0; + { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + if (out_grouping_b(z, g_AEIOU, 97, 252, 0)) goto lab2; + break; + lab2: + z->c = z->l - v_5; + if (z->c > z->lb) goto lab0; + } while (0); + { + int v_6 = z->l - z->c; + do { + int v_7 = z->l - z->c; + if (in_grouping_b(z, g_AIOU, 97, 252, 0)) goto lab4; + break; + lab4: + z->c = z->l - v_7; + if (in_grouping_b(z, g_E, 101, 235, 0)) goto lab3; + if (z->c > z->lb) goto lab3; + } while (0); + goto lab0; + lab3: + z->c = z->l - v_6; + } + { + int v_8 = z->l - z->c; + if (z->c <= z->lb) goto lab5; + z->c--; + if (in_grouping_b(z, g_AIOU, 97, 252, 0)) goto lab5; + if (out_grouping_b(z, g_AEIOU, 97, 252, 0)) goto lab5; + goto lab0; + lab5: + z->c = z->l - v_8; + } + z->c = z->l - v_4; + } + { + int ret = slice_to(z, &((SN_local *)z)->s_ch); + if (ret < 0) return ret; + } + { + int saved_c = z->c; + int ret = insert_v(z, z->c, z->c, ((SN_local *)z)->s_ch); + z->c = saved_c; if (ret < 0) return ret; } break; case 3: - { int ret = r_R1(z); - if (ret == 0) goto lab0; + { + int ret = slice_from_s(z, 3, s_3); if (ret < 0) return ret; } - if (out_grouping_b(z, g_v_j, 97, 232, 0)) goto lab0; - { int ret = slice_del(z); + break; + case 4: + { + int ret = slice_from_s(z, 3, s_4); if (ret < 0) return ret; } break; } lab0: - z->c = z->l - m1; - } - { int m2 = z->l - z->c; (void)m2; - { int ret = r_e_ending(z); - if (ret < 0) return ret; - } - z->c = z->l - m2; - } - { int m3 = z->l - z->c; (void)m3; - z->ket = z->c; - if (!(eq_s_b(z, 4, s_12))) goto lab1; - z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - { int m4 = z->l - z->c; (void)m4; - if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab2; - z->c--; - goto lab1; - lab2: - z->c = z->l - m4; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->ket = z->c; - if (!(eq_s_b(z, 2, s_13))) goto lab1; - z->bra = z->c; - { int ret = r_en_ending(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - lab1: - z->c = z->l - m3; + z->c = z->l - v_1; } - { int m5 = z->l - z->c; (void)m5; - z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((264336 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab3; - among_var = find_among_b(z, a_4, 6); - if (!among_var) goto lab3; - z->bra = z->c; - switch (among_var) { - case 1: - { int ret = r_R2(z); - if (ret == 0) goto lab3; - if (ret < 0) return ret; - } - { int ret = slice_del(z); + return 1; +} + +static int r_Step_1(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540704 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_1, 8, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 't') goto lab0; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; - z->ket = z->c; - if (!(eq_s_b(z, 2, s_14))) goto lab5; - z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) goto lab5; + return 0; + lab0: + z->c = z->l - v_1; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_5); + if (ret < 0) return ret; + } + break; + case 4: + do { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 2, s_6))) goto lab1; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - { int m7 = z->l - z->c; (void)m7; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab6; - z->c--; - goto lab5; - lab6: - z->c = z->l - m7; + { + int ret = r_C(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; } - { int ret = slice_del(z); + z->c = z->l - v_3; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret < 0) return ret; + } + break; + lab1: + z->c = z->l - v_2; + { + int v_4 = z->l - z->c; + if (!(eq_s_b(z, 2, s_7))) goto lab2; + { + int ret = r_R1(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m6; - { int ret = r_undouble(z); - if (ret == 0) goto lab3; + { + int ret = r_C(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } + z->c = z->l - v_4; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; } - lab4: break; - case 2: - { int ret = r_R2(z); + lab2: + z->c = z->l - v_2; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_8); + if (ret < 0) return ret; + } + } while (0); + break; + case 5: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_9); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_V(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_10); + if (ret < 0) return ret; + } + break; + case 7: + do { + int v_5 = z->l - z->c; + if (!(eq_s_b(z, 3, s_11))) goto lab3; + { + int ret = r_R1(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab7; + z->bra = z->c; + { + int ret = slice_from_s(z, 4, s_12); + if (ret < 0) return ret; + } + break; + lab3: + z->c = z->l - v_5; + if (!(eq_s_b(z, 2, s_13))) goto lab4; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab4: + z->c = z->l - v_5; + if (z->c <= z->lb || z->p[z->c - 1] != 'd') goto lab5; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab5; + if (ret < 0) return ret; + } + { + int ret = r_C(z); + if (ret == 0) goto lab5; + if (ret < 0) return ret; + } + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab5: + z->c = z->l - v_5; + do { + int v_6 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab7; z->c--; - goto lab3; + break; lab7: - z->c = z->l - m8; + z->c = z->l - v_6; + if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab6; + z->c--; + } while (0); + { + int ret = r_V(z); + if (ret == 0) goto lab6; + if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 3: - { int ret = r_R2(z); - if (ret == 0) goto lab3; + lab6: + z->c = z->l - v_5; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + } while (0); + break; + case 8: + { + int ret = slice_from_s(z, 2, s_14); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_2(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 101) return 0; + among_var = find_among_b(z, a_2, 11, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 2, s_15))) goto lab0; + z->bra = z->c; + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_e_ending(z); - if (ret == 0) goto lab3; + break; + lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 2, s_16))) goto lab1; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + { + int ret = r_C(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 4: - { int ret = r_R2(z); + lab1: + z->c = z->l - v_1; + if (!(eq_s_b(z, 3, s_17))) goto lab2; + z->bra = z->c; + { + int ret = slice_from_s(z, 2, s_18); + if (ret < 0) return ret; + } + break; + lab2: + z->c = z->l - v_1; + if (z->c <= z->lb || z->p[z->c - 1] != 't') goto lab3; + z->c--; + z->bra = z->c; + { + int ret = r_R1(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = r_VX(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 5: - { int ret = r_R2(z); - if (ret == 0) goto lab3; + lab3: + z->c = z->l - v_1; + if (!(eq_s_b(z, 3, s_19))) goto lab4; + z->bra = z->c; + { + int ret = slice_from_s(z, 3, s_20); if (ret < 0) return ret; } - if (!(z->I[3])) goto lab3; - { int ret = slice_del(z); + break; + lab4: + z->c = z->l - v_1; + if (!(eq_s_b(z, 2, s_21))) goto lab5; + z->bra = z->c; + { + int ret = slice_from_s(z, 1, s_22); + if (ret < 0) return ret; + } + break; + lab5: + z->c = z->l - v_1; + if (z->c <= z->lb || z->p[z->c - 1] != '\'') goto lab6; + z->c--; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab6; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab6: + z->c = z->l - v_1; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } while (0); + break; + case 2: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_23); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_24); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_25); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_26); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_27); + if (ret < 0) return ret; + } + break; + case 8: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_28); + if (ret < 0) return ret; + } + break; + case 9: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 1, s_29); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 10: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 2, s_30); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 11: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_31); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_3(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1316016 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_3, 14, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_32); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_33); + if (ret < 0) return ret; + } + break; + case 5: + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 3, s_34))) goto lab0; + { + int ret = slice_from_s(z, 2, s_35); + if (ret < 0) return ret; + } + break; + lab0: + z->c = z->l - v_1; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + } while (0); + break; + case 6: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_36); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = r_R2(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 1, s_37); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 8: + { + int ret = r_R2(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 1, s_38); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 9: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_39); + if (ret < 0) return ret; + } + break; + case 10: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_40); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_4(struct SN_env * z) { + int among_var; + do { + int v_1 = z->l - z->c; + z->ket = z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1315024 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_4, 16, 0); + if (!among_var) goto lab0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_41); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_42); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_V(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_43); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_V(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_44); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_V(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_45); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_46); + if (ret < 0) return ret; + } + break; + case 8: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_47); + if (ret < 0) return ret; + } + break; + case 9: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_C(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); if (ret < 0) return ret; } break; } - lab3: - z->c = z->l - m5; - } - { int m9 = z->l - z->c; (void)m9; - if (out_grouping_b(z, g_v_I, 73, 232, 0)) goto lab8; - { int m_test10 = z->l - z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2129954 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab8; - if (!find_among_b(z, a_5, 4)) goto lab8; - if (out_grouping_b(z, g_v, 97, 232, 0)) goto lab8; - z->c = z->l - m_test10; - } + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; - if (z->c <= z->lb) goto lab8; - z->c--; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1310848 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + if (!find_among_b(z, a_5, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 3, s_48))) goto lab1; + if (z->c > z->lb) goto lab1; + return 0; + lab1: + z->c = z->l - v_2; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab8: - z->c = z->l - m9; + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + } while (0); + return 1; +} + +static int r_Step_7(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 116) return 0; + among_var = find_among_b(z, a_6, 3, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_49); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_50); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_51); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_6(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((98532828 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_7, 22, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_52); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_53); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_54); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_55); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_from_s(z, 1, s_56); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = slice_from_s(z, 1, s_57); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = slice_from_s(z, 1, s_58); + if (ret < 0) return ret; + } + break; + case 8: + { + int ret = slice_from_s(z, 1, s_59); + if (ret < 0) return ret; + } + break; + case 9: + { + int ret = slice_from_s(z, 1, s_60); + if (ret < 0) return ret; + } + break; + case 10: + { + int ret = slice_from_s(z, 1, s_61); + if (ret < 0) return ret; + } + break; + case 11: + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab0; + z->c--; + if (z->c > z->lb) goto lab0; + return 0; + lab0: + z->c = z->l - v_1; + } + { + int ret = slice_from_s(z, 1, s_62); + if (ret < 0) return ret; + } + break; + case 12: + { + int ret = slice_from_s(z, 1, s_63); + if (ret < 0) return ret; + } + break; + case 13: + { + int ret = slice_from_s(z, 1, s_64); + if (ret < 0) return ret; + } + break; + case 14: + { + int ret = slice_from_s(z, 1, s_65); + if (ret < 0) return ret; + } + break; + case 15: + { + int ret = slice_from_s(z, 1, s_66); + if (ret < 0) return ret; + } + break; + case 16: + { + int ret = slice_from_s(z, 1, s_67); + if (ret < 0) return ret; + } + break; + case 17: + { + int ret = slice_from_s(z, 1, s_68); + if (ret < 0) return ret; + } + break; + case 18: + { + int ret = slice_from_s(z, 1, s_69); + if (ret < 0) return ret; + } + break; + case 19: + { + int ret = slice_from_s(z, 1, s_70); + if (ret < 0) return ret; + } + break; + case 20: + { + int ret = slice_from_s(z, 1, s_71); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_1c(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 116)) return 0; + among_var = find_among_b(z, a_8, 2, 0); + if (!among_var) return 0; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + switch (among_var) { + case 1: + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') goto lab0; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + return 0; + lab0: + z->c = z->l - v_1; + } + do { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_72))) goto lab1; + if (z->c > z->lb) goto lab1; + { + int ret = slice_from_s(z, 1, s_73); + if (ret < 0) return ret; + } + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } while (0); + break; + case 2: + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'h') goto lab2; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab2; + if (ret < 0) return ret; + } + return 0; + lab2: + z->c = z->l - v_3; + } + { + int v_4 = z->l - z->c; + if (!(eq_s_b(z, 2, s_74))) goto lab3; + if (z->c > z->lb) goto lab3; + return 0; + lab3: + z->c = z->l - v_4; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Lose_prefix(struct SN_env * z) { + int among_var; + z->bra = z->c; + if (!(eq_s(z, 2, s_75))) return 0; + z->ket = z->c; + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + z->c = v_1; + } + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; + if (!(eq_s(z, 2, s_76))) goto lab1; + break; + lab1: + z->c = v_4; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab0; + } while (0); + break; + lab0: + z->c = v_3; + if (z->c >= z->l) return 0; + z->c++; + } + while (1) { + int v_5 = z->c; + do { + int v_6 = z->c; + if (!(eq_s(z, 2, s_77))) goto lab3; + break; + lab3: + z->c = v_6; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab2; + } while (0); + continue; + lab2: + z->c = v_5; + break; + } + if (z->c < z->l) goto lab4; + return 0; + lab4: + z->c = v_2; + } + if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((1314818 >> (z->p[z->c + 2] & 0x1f)) & 1)) among_var = -1; else + among_var = find_among(z, a_9, 6, 0); + switch (among_var) { + case 1: + return 0; + break; + } + ((SN_local *)z)->b_GE_removed = 1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int v_7 = z->c; + z->bra = z->c; + if (z->c >= z->l || (z->p[z->c + 0] != 235 && z->p[z->c + 0] != 239)) goto lab5; + among_var = find_among(z, a_10, 2, 0); + if (!among_var) goto lab5; + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_78); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_79); + if (ret < 0) return ret; + } + break; + } + lab5: + z->c = v_7; + } + return 1; +} + +static int r_Lose_infix(struct SN_env * z) { + int among_var; + if (z->c >= z->l) return 0; + z->c++; + while (1) { + z->bra = z->c; + if (!(eq_s(z, 2, s_80))) goto lab0; + z->ket = z->c; + break; + lab0: + if (z->c >= z->l) return 0; + z->c++; + } + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + z->c = v_1; + } + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; + if (!(eq_s(z, 2, s_81))) goto lab2; + break; + lab2: + z->c = v_4; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab1; + } while (0); + break; + lab1: + z->c = v_3; + if (z->c >= z->l) return 0; + z->c++; + } + while (1) { + int v_5 = z->c; + do { + int v_6 = z->c; + if (!(eq_s(z, 2, s_82))) goto lab4; + break; + lab4: + z->c = v_6; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab3; + } while (0); + continue; + lab3: + z->c = v_5; + break; + } + if (z->c < z->l) goto lab5; + return 0; + lab5: + z->c = v_2; + } + ((SN_local *)z)->b_GE_removed = 1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int v_7 = z->c; + z->bra = z->c; + if (z->c >= z->l || (z->p[z->c + 0] != 235 && z->p[z->c + 0] != 239)) goto lab6; + among_var = find_among(z, a_11, 2, 0); + if (!among_var) goto lab6; + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_83); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_84); + if (ret < 0) return ret; + } + break; + } + lab6: + z->c = v_7; + } + return 1; +} + +static int r_measure(struct SN_env * z) { + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + while (1) { + if (out_grouping(z, g_v, 97, 252, 0)) goto lab1; + continue; + lab1: + break; + } + { + int v_2 = 1; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; + if (!(eq_s(z, 2, s_85))) goto lab3; + break; + lab3: + z->c = v_4; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab2; + } while (0); + v_2--; + continue; + lab2: + z->c = v_3; + break; + } + if (v_2 > 0) goto lab0; + } + if (out_grouping(z, g_v, 97, 252, 0)) goto lab0; + ((SN_local *)z)->i_p1 = z->c; + while (1) { + if (out_grouping(z, g_v, 97, 252, 0)) goto lab4; + continue; + lab4: + break; + } + { + int v_5 = 1; + while (1) { + int v_6 = z->c; + do { + int v_7 = z->c; + if (!(eq_s(z, 2, s_86))) goto lab6; + break; + lab6: + z->c = v_7; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab5; + } while (0); + v_5--; + continue; + lab5: + z->c = v_6; + break; + } + if (v_5 > 0) goto lab0; + } + if (out_grouping(z, g_v, 97, 252, 0)) goto lab0; + ((SN_local *)z)->i_p2 = z->c; + lab0: + z->c = v_1; } return 1; } extern int dutch_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_prelude(z); + int b_stemmed; + b_stemmed = 0; + { + int ret = r_measure(z); + if (ret <= 0) return ret; + } + z->lb = z->c; z->c = z->l; + { + int v_1 = z->l - z->c; + { + int ret = r_Step_1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab0: + z->c = z->l - v_1; + } + { + int v_2 = z->l - z->c; + { + int ret = r_Step_2(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - z->c = c1; + b_stemmed = 1; + lab1: + z->c = z->l - v_2; } - { int c2 = z->c; - { int ret = r_mark_regions(z); + { + int v_3 = z->l - z->c; + { + int ret = r_Step_3(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - z->c = c2; + b_stemmed = 1; + lab2: + z->c = z->l - v_3; + } + { + int v_4 = z->l - z->c; + { + int ret = r_Step_4(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab3: + z->c = z->l - v_4; + } + z->c = z->lb; + ((SN_local *)z)->b_GE_removed = 0; + { + int v_5 = z->c; + { + int v_6 = z->c; + { + int ret = r_Lose_prefix(z); + if (ret == 0) goto lab4; + if (ret < 0) return ret; + } + z->c = v_6; + { + int ret = r_measure(z); + if (ret < 0) return ret; + } + } + lab4: + z->c = v_5; } z->lb = z->c; z->c = z->l; - - - { int ret = r_standard_suffix(z); - if (ret < 0) return ret; + { + int v_7 = z->l - z->c; + if (!((SN_local *)z)->b_GE_removed) goto lab5; + b_stemmed = 1; + { + int ret = r_Step_1c(z); + if (ret == 0) goto lab5; + if (ret < 0) return ret; + } + lab5: + z->c = z->l - v_7; } z->c = z->lb; - { int c3 = z->c; - { int ret = r_postlude(z); + ((SN_local *)z)->b_GE_removed = 0; + { + int v_8 = z->c; + { + int v_9 = z->c; + { + int ret = r_Lose_infix(z); + if (ret == 0) goto lab6; + if (ret < 0) return ret; + } + z->c = v_9; + { + int ret = r_measure(z); + if (ret < 0) return ret; + } + } + lab6: + z->c = v_8; + } + z->lb = z->c; z->c = z->l; + { + int v_10 = z->l - z->c; + if (!((SN_local *)z)->b_GE_removed) goto lab7; + b_stemmed = 1; + { + int ret = r_Step_1c(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } - z->c = c3; + lab7: + z->c = z->l - v_10; } + z->c = z->lb; + z->lb = z->c; z->c = z->l; + { + int v_11 = z->l - z->c; + { + int ret = r_Step_7(z); + if (ret == 0) goto lab8; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab8: + z->c = z->l - v_11; + } + { + int v_12 = z->l - z->c; + if (!b_stemmed) goto lab9; + { + int ret = r_Step_6(z); + if (ret == 0) goto lab9; + if (ret < 0) return ret; + } + lab9: + z->c = z->l - v_12; + } + z->c = z->lb; return 1; } -extern struct SN_env * dutch_ISO_8859_1_create_env(void) { return SN_create_env(0, 4); } +extern struct SN_env * dutch_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_GE_removed = 0; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->s_ch = NULL; -extern void dutch_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } + if ((((SN_local *)z)->s_ch = create_s()) == NULL) { + dutch_ISO_8859_1_close_env(z); + return NULL; + } + } + return z; +} + +extern void dutch_ISO_8859_1_close_env(struct SN_env * z) { + if (z) { + lose_s(((SN_local *)z)->s_ch); + } + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch_porter.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch_porter.c new file mode 100644 index 0000000000000..64e8bbee16c64 --- /dev/null +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_dutch_porter.c @@ -0,0 +1,665 @@ +/* Generated from dutch_porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#include "stem_ISO_8859_1_dutch_porter.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_e_found; +}; + +typedef struct SN_local SN_local; + +#ifdef __cplusplus +extern "C" { +#endif +extern int dutch_porter_ISO_8859_1_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif + +static int r_standard_suffix(struct SN_env * z); +static int r_undouble(struct SN_env * z); +static int r_R2(struct SN_env * z); +static int r_R1(struct SN_env * z); +static int r_mark_regions(struct SN_env * z); +static int r_en_ending(struct SN_env * z); +static int r_e_ending(struct SN_env * z); +static int r_postlude(struct SN_env * z); +static int r_prelude(struct SN_env * z); + +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'i' }; +static const symbol s_3[] = { 'o' }; +static const symbol s_4[] = { 'u' }; +static const symbol s_5[] = { 'Y' }; +static const symbol s_6[] = { 'I' }; +static const symbol s_7[] = { 'Y' }; +static const symbol s_8[] = { 'y' }; +static const symbol s_9[] = { 'i' }; +static const symbol s_10[] = { 'g', 'e', 'm' }; +static const symbol s_11[] = { 'h', 'e', 'i', 'd' }; +static const symbol s_12[] = { 'h', 'e', 'i', 'd' }; +static const symbol s_13[] = { 'e', 'n' }; +static const symbol s_14[] = { 'i', 'g' }; + +static const symbol s_0_1[1] = { 0xE1 }; +static const symbol s_0_2[1] = { 0xE4 }; +static const symbol s_0_3[1] = { 0xE9 }; +static const symbol s_0_4[1] = { 0xEB }; +static const symbol s_0_5[1] = { 0xED }; +static const symbol s_0_6[1] = { 0xEF }; +static const symbol s_0_7[1] = { 0xF3 }; +static const symbol s_0_8[1] = { 0xF6 }; +static const symbol s_0_9[1] = { 0xFA }; +static const symbol s_0_10[1] = { 0xFC }; +static const struct among a_0[11] = { +{ 0, 0, 0, 6, 0}, +{ 1, s_0_1, -1, 1, 0}, +{ 1, s_0_2, -2, 1, 0}, +{ 1, s_0_3, -3, 2, 0}, +{ 1, s_0_4, -4, 2, 0}, +{ 1, s_0_5, -5, 3, 0}, +{ 1, s_0_6, -6, 3, 0}, +{ 1, s_0_7, -7, 4, 0}, +{ 1, s_0_8, -8, 4, 0}, +{ 1, s_0_9, -9, 5, 0}, +{ 1, s_0_10, -10, 5, 0} +}; + +static const symbol s_1_1[1] = { 'I' }; +static const symbol s_1_2[1] = { 'Y' }; +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 1, s_1_1, -1, 2, 0}, +{ 1, s_1_2, -2, 1, 0} +}; + +static const symbol s_2_0[2] = { 'd', 'd' }; +static const symbol s_2_1[2] = { 'k', 'k' }; +static const symbol s_2_2[2] = { 't', 't' }; +static const struct among a_2[3] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0} +}; + +static const symbol s_3_0[3] = { 'e', 'n', 'e' }; +static const symbol s_3_1[2] = { 's', 'e' }; +static const symbol s_3_2[2] = { 'e', 'n' }; +static const symbol s_3_3[5] = { 'h', 'e', 'd', 'e', 'n' }; +static const symbol s_3_4[1] = { 's' }; +static const struct among a_3[5] = { +{ 3, s_3_0, 0, 2, 0}, +{ 2, s_3_1, 0, 3, 0}, +{ 2, s_3_2, 0, 2, 0}, +{ 5, s_3_3, -1, 1, 0}, +{ 1, s_3_4, 0, 3, 0} +}; + +static const symbol s_4_0[3] = { 'e', 'n', 'd' }; +static const symbol s_4_1[2] = { 'i', 'g' }; +static const symbol s_4_2[3] = { 'i', 'n', 'g' }; +static const symbol s_4_3[4] = { 'l', 'i', 'j', 'k' }; +static const symbol s_4_4[4] = { 'b', 'a', 'a', 'r' }; +static const symbol s_4_5[3] = { 'b', 'a', 'r' }; +static const struct among a_4[6] = { +{ 3, s_4_0, 0, 1, 0}, +{ 2, s_4_1, 0, 2, 0}, +{ 3, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 3, 0}, +{ 4, s_4_4, 0, 4, 0}, +{ 3, s_4_5, 0, 5, 0} +}; + +static const symbol s_5_0[2] = { 'a', 'a' }; +static const symbol s_5_1[2] = { 'e', 'e' }; +static const symbol s_5_2[2] = { 'o', 'o' }; +static const symbol s_5_3[2] = { 'u', 'u' }; +static const struct among a_5[4] = { +{ 2, s_5_0, 0, -1, 0}, +{ 2, s_5_1, 0, -1, 0}, +{ 2, s_5_2, 0, -1, 0}, +{ 2, s_5_3, 0, -1, 0} +}; + +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + +static const unsigned char g_v_I[] = { 1, 0, 0, 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + +static const unsigned char g_v_j[] = { 17, 67, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + +static int r_prelude(struct SN_env * z) { + int among_var; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + z->bra = z->c; + if (z->c >= z->l || z->p[z->c + 0] >> 5 != 7 || !((340306450 >> (z->p[z->c + 0] & 0x1f)) & 1)) among_var = 6; else + among_var = find_among(z, a_0, 11, 0); + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_0); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_1); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_2); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_3); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_from_s(z, 1, s_4); + if (ret < 0) return ret; + } + break; + case 6: + if (z->c >= z->l) goto lab0; + z->c++; + break; + } + continue; + lab0: + z->c = v_2; + break; + } + z->c = v_1; + } + { + int v_3 = z->c; + z->bra = z->c; + if (z->c == z->l || z->p[z->c] != 'y') { z->c = v_3; goto lab1; } + z->c++; + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_5); + if (ret < 0) return ret; + } + lab1: + ; + } + while (1) { + int v_4 = z->c; + { + int ret = out_grouping(z, g_v, 97, 232, 1); + if (ret < 0) goto lab2; + z->c += ret; + } + { + int v_5 = z->c; + z->bra = z->c; + do { + int v_6 = z->c; + if (z->c == z->l || z->p[z->c] != 'i') goto lab4; + z->c++; + z->ket = z->c; + { + int v_7 = z->c; + if (in_grouping(z, g_v, 97, 232, 0)) goto lab5; + { + int ret = slice_from_s(z, 1, s_6); + if (ret < 0) return ret; + } + lab5: + z->c = v_7; + } + break; + lab4: + z->c = v_6; + if (z->c == z->l || z->p[z->c] != 'y') { z->c = v_5; goto lab3; } + z->c++; + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_7); + if (ret < 0) return ret; + } + } while (0); + lab3: + ; + } + continue; + lab2: + z->c = v_4; + break; + } + return 1; +} + +static int r_mark_regions(struct SN_env * z) { + int i_x; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + { + int ret = in_grouping(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; +lab0: + { + int ret = out_grouping(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + { + int ret = in_grouping(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + ((SN_local *)z)->i_p2 = z->c; + return 1; +} + +static int r_postlude(struct SN_env * z) { + int among_var; + while (1) { + int v_1 = z->c; + z->bra = z->c; + if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 89)) among_var = 3; else + among_var = find_among(z, a_1, 3, 0); + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_8); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_9); + if (ret < 0) return ret; + } + break; + case 3: + if (z->c >= z->l) goto lab0; + z->c++; + break; + } + continue; + lab0: + z->c = v_1; + break; + } + return 1; +} + +static int r_R1(struct SN_env * z) { + return ((SN_local *)z)->i_p1 <= z->c; +} + +static int r_R2(struct SN_env * z) { + return ((SN_local *)z)->i_p2 <= z->c; +} + +static int r_undouble(struct SN_env * z) { + { + int v_1 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1050640 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + if (!find_among_b(z, a_2, 3, 0)) return 0; + z->c = z->l - v_1; + } + z->ket = z->c; + if (z->c <= z->lb) return 0; + z->c--; + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return 1; +} + +static int r_e_ending(struct SN_env * z) { + ((SN_local *)z)->b_e_found = 0; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; + z->c--; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + if (out_grouping_b(z, g_v, 97, 232, 0)) return 0; + z->c = z->l - v_1; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + ((SN_local *)z)->b_e_found = 1; + return r_undouble(z); +} + +static int r_en_ending(struct SN_env * z) { + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + if (out_grouping_b(z, g_v, 97, 232, 0)) return 0; + z->c = z->l - v_1; + { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 3, s_10))) goto lab0; + return 0; + lab0: + z->c = z->l - v_2; + } + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return r_undouble(z); +} + +static int r_standard_suffix(struct SN_env * z) { + int among_var; + { + int v_1 = z->l - z->c; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540704 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_3, 5, 0); + if (!among_var) goto lab0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_11); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_en_ending(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + if (out_grouping_b(z, g_v_j, 97, 232, 0)) goto lab0; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + lab0: + z->c = z->l - v_1; + } + { + int v_2 = z->l - z->c; + { + int ret = r_e_ending(z); + if (ret < 0) return ret; + } + z->c = z->l - v_2; + } + { + int v_3 = z->l - z->c; + z->ket = z->c; + if (!(eq_s_b(z, 4, s_12))) goto lab1; + z->bra = z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + { + int v_4 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab2; + z->c--; + goto lab1; + lab2: + z->c = z->l - v_4; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + z->ket = z->c; + if (!(eq_s_b(z, 2, s_13))) goto lab1; + z->bra = z->c; + { + int ret = r_en_ending(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + lab1: + z->c = z->l - v_3; + } + { + int v_5 = z->l - z->c; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((264336 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab3; + among_var = find_among_b(z, a_4, 6, 0); + if (!among_var) goto lab3; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + do { + int v_6 = z->l - z->c; + z->ket = z->c; + if (!(eq_s_b(z, 2, s_14))) goto lab4; + z->bra = z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab4; + if (ret < 0) return ret; + } + { + int v_7 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab5; + z->c--; + goto lab4; + lab5: + z->c = z->l - v_7; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab4: + z->c = z->l - v_6; + { + int ret = r_undouble(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + } while (0); + break; + case 2: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int v_8 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab6; + z->c--; + goto lab3; + lab6: + z->c = z->l - v_8; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_e_ending(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + if (!((SN_local *)z)->b_e_found) goto lab3; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + lab3: + z->c = z->l - v_5; + } + { + int v_9 = z->l - z->c; + if (out_grouping_b(z, g_v_I, 73, 232, 0)) goto lab7; + { + int v_10 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2129954 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab7; + if (!find_among_b(z, a_5, 4, 0)) goto lab7; + if (out_grouping_b(z, g_v, 97, 232, 0)) goto lab7; + z->c = z->l - v_10; + } + z->ket = z->c; + if (z->c <= z->lb) goto lab7; + z->c--; + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + lab7: + z->c = z->l - v_9; + } + return 1; +} + +extern int dutch_porter_ISO_8859_1_stem(struct SN_env * z) { + { + int v_1 = z->c; + { + int ret = r_prelude(z); + if (ret < 0) return ret; + } + z->c = v_1; + } + { + int v_2 = z->c; + { + int ret = r_mark_regions(z); + if (ret < 0) return ret; + } + z->c = v_2; + } + z->lb = z->c; z->c = z->l; + { + int ret = r_standard_suffix(z); + if (ret < 0) return ret; + } + z->c = z->lb; + { + int v_3 = z->c; + { + int ret = r_postlude(z); + if (ret < 0) return ret; + } + z->c = v_3; + } + return 1; +} + +extern struct SN_env * dutch_porter_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->b_e_found = 0; + } + return z; +} + +extern void dutch_porter_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} + diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_english.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_english.c index ac36c163f9312..cb9ea3ae1c33a 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_english.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_english.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from english.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_english.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_Y_found; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,7 +22,7 @@ extern int english_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif -static int r_exception2(struct SN_env * z); + static int r_exception1(struct SN_env * z); static int r_Step_5(struct SN_env * z); static int r_Step_4(struct SN_env * z); @@ -24,38 +37,75 @@ static int r_shortv(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * english_ISO_8859_1_create_env(void); -extern void english_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'Y' }; +static const symbol s_1[] = { 'Y' }; +static const symbol s_2[] = { 'p', 'a', 's', 't' }; +static const symbol s_3[] = { 's', 's' }; +static const symbol s_4[] = { 'i' }; +static const symbol s_5[] = { 'i', 'e' }; +static const symbol s_6[] = { 'e', 'e' }; +static const symbol s_7[] = { 'i', 'e' }; +static const symbol s_8[] = { 'e' }; +static const symbol s_9[] = { 'e' }; +static const symbol s_10[] = { 'i' }; +static const symbol s_11[] = { 't', 'i', 'o', 'n' }; +static const symbol s_12[] = { 'e', 'n', 'c', 'e' }; +static const symbol s_13[] = { 'a', 'n', 'c', 'e' }; +static const symbol s_14[] = { 'a', 'b', 'l', 'e' }; +static const symbol s_15[] = { 'e', 'n', 't' }; +static const symbol s_16[] = { 'i', 'z', 'e' }; +static const symbol s_17[] = { 'a', 't', 'e' }; +static const symbol s_18[] = { 'a', 'l' }; +static const symbol s_19[] = { 'f', 'u', 'l' }; +static const symbol s_20[] = { 'o', 'u', 's' }; +static const symbol s_21[] = { 'i', 'v', 'e' }; +static const symbol s_22[] = { 'b', 'l', 'e' }; +static const symbol s_23[] = { 'o', 'g' }; +static const symbol s_24[] = { 'o', 'g' }; +static const symbol s_25[] = { 'l', 'e', 's', 's' }; +static const symbol s_26[] = { 't', 'i', 'o', 'n' }; +static const symbol s_27[] = { 'a', 't', 'e' }; +static const symbol s_28[] = { 'a', 'l' }; +static const symbol s_29[] = { 'i', 'c' }; +static const symbol s_30[] = { 's', 'k', 'i' }; +static const symbol s_31[] = { 's', 'k', 'y' }; +static const symbol s_32[] = { 'i', 'd', 'l' }; +static const symbol s_33[] = { 'g', 'e', 'n', 't', 'l' }; +static const symbol s_34[] = { 'u', 'g', 'l', 'i' }; +static const symbol s_35[] = { 'e', 'a', 'r', 'l', 'i' }; +static const symbol s_36[] = { 'o', 'n', 'l', 'i' }; +static const symbol s_37[] = { 's', 'i', 'n', 'g', 'l' }; +static const symbol s_38[] = { 'y' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[5] = { 'a', 'r', 's', 'e', 'n' }; static const symbol s_0_1[6] = { 'c', 'o', 'm', 'm', 'u', 'n' }; -static const symbol s_0_2[5] = { 'g', 'e', 'n', 'e', 'r' }; - -static const struct among a_0[3] = -{ -{ 5, s_0_0, -1, -1, 0}, -{ 6, s_0_1, -1, -1, 0}, -{ 5, s_0_2, -1, -1, 0} +static const symbol s_0_2[5] = { 'e', 'm', 'e', 'r', 'g' }; +static const symbol s_0_3[5] = { 'g', 'e', 'n', 'e', 'r' }; +static const symbol s_0_4[5] = { 'i', 'n', 't', 'e', 'r' }; +static const symbol s_0_5[5] = { 'l', 'a', 't', 'e', 'r' }; +static const symbol s_0_6[5] = { 'o', 'r', 'g', 'a', 'n' }; +static const symbol s_0_7[4] = { 'p', 'a', 's', 't' }; +static const symbol s_0_8[7] = { 'u', 'n', 'i', 'v', 'e', 'r', 's' }; +static const struct among a_0[9] = { +{ 5, s_0_0, 0, -1, 0}, +{ 6, s_0_1, 0, -1, 0}, +{ 5, s_0_2, 0, -1, 0}, +{ 5, s_0_3, 0, -1, 0}, +{ 5, s_0_4, 0, -1, 0}, +{ 5, s_0_5, 0, -1, 0}, +{ 5, s_0_6, 0, -1, 0}, +{ 4, s_0_7, 0, -1, 0}, +{ 7, s_0_8, 0, -1, 0} }; static const symbol s_1_0[1] = { '\'' }; static const symbol s_1_1[3] = { '\'', 's', '\'' }; static const symbol s_1_2[2] = { '\'', 's' }; - -static const struct among a_1[3] = -{ -{ 1, s_1_0, -1, 1, 0}, -{ 3, s_1_1, 0, 1, 0}, -{ 2, s_1_2, -1, 1, 0} +static const struct among a_1[3] = { +{ 1, s_1_0, 0, 1, 0}, +{ 3, s_1_1, -1, 1, 0}, +{ 2, s_1_2, 0, 1, 0} }; static const symbol s_2_0[3] = { 'i', 'e', 'd' }; @@ -64,250 +114,236 @@ static const symbol s_2_2[3] = { 'i', 'e', 's' }; static const symbol s_2_3[4] = { 's', 's', 'e', 's' }; static const symbol s_2_4[2] = { 's', 's' }; static const symbol s_2_5[2] = { 'u', 's' }; - -static const struct among a_2[6] = -{ -{ 3, s_2_0, -1, 2, 0}, -{ 1, s_2_1, -1, 3, 0}, -{ 3, s_2_2, 1, 2, 0}, -{ 4, s_2_3, 1, 1, 0}, -{ 2, s_2_4, 1, -1, 0}, -{ 2, s_2_5, 1, -1, 0} +static const struct among a_2[6] = { +{ 3, s_2_0, 0, 2, 0}, +{ 1, s_2_1, 0, 3, 0}, +{ 3, s_2_2, -1, 2, 0}, +{ 4, s_2_3, -2, 1, 0}, +{ 2, s_2_4, -3, -1, 0}, +{ 2, s_2_5, -4, -1, 0} }; -static const symbol s_3_1[2] = { 'b', 'b' }; -static const symbol s_3_2[2] = { 'd', 'd' }; -static const symbol s_3_3[2] = { 'f', 'f' }; -static const symbol s_3_4[2] = { 'g', 'g' }; -static const symbol s_3_5[2] = { 'b', 'l' }; -static const symbol s_3_6[2] = { 'm', 'm' }; -static const symbol s_3_7[2] = { 'n', 'n' }; -static const symbol s_3_8[2] = { 'p', 'p' }; -static const symbol s_3_9[2] = { 'r', 'r' }; -static const symbol s_3_10[2] = { 'a', 't' }; -static const symbol s_3_11[2] = { 't', 't' }; -static const symbol s_3_12[2] = { 'i', 'z' }; - -static const struct among a_3[13] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_3_1, 0, 2, 0}, -{ 2, s_3_2, 0, 2, 0}, -{ 2, s_3_3, 0, 2, 0}, -{ 2, s_3_4, 0, 2, 0}, -{ 2, s_3_5, 0, 1, 0}, -{ 2, s_3_6, 0, 2, 0}, -{ 2, s_3_7, 0, 2, 0}, -{ 2, s_3_8, 0, 2, 0}, -{ 2, s_3_9, 0, 2, 0}, -{ 2, s_3_10, 0, 1, 0}, -{ 2, s_3_11, 0, 2, 0}, -{ 2, s_3_12, 0, 1, 0} +static const symbol s_3_0[4] = { 's', 'u', 'c', 'c' }; +static const symbol s_3_1[4] = { 'p', 'r', 'o', 'c' }; +static const symbol s_3_2[3] = { 'e', 'x', 'c' }; +static const struct among a_3[3] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 3, s_3_2, 0, 1, 0} }; -static const symbol s_4_0[2] = { 'e', 'd' }; -static const symbol s_4_1[3] = { 'e', 'e', 'd' }; -static const symbol s_4_2[3] = { 'i', 'n', 'g' }; -static const symbol s_4_3[4] = { 'e', 'd', 'l', 'y' }; -static const symbol s_4_4[5] = { 'e', 'e', 'd', 'l', 'y' }; -static const symbol s_4_5[5] = { 'i', 'n', 'g', 'l', 'y' }; - -static const struct among a_4[6] = -{ -{ 2, s_4_0, -1, 2, 0}, -{ 3, s_4_1, 0, 1, 0}, -{ 3, s_4_2, -1, 2, 0}, -{ 4, s_4_3, -1, 2, 0}, -{ 5, s_4_4, 3, 1, 0}, -{ 5, s_4_5, -1, 2, 0} +static const symbol s_4_0[4] = { 'e', 'v', 'e', 'n' }; +static const symbol s_4_1[4] = { 'c', 'a', 'n', 'n' }; +static const symbol s_4_2[3] = { 'i', 'n', 'n' }; +static const symbol s_4_3[4] = { 'e', 'a', 'r', 'r' }; +static const symbol s_4_4[4] = { 'h', 'e', 'r', 'r' }; +static const symbol s_4_5[3] = { 'o', 'u', 't' }; +static const symbol s_4_6[1] = { 'y' }; +static const struct among a_4[7] = { +{ 4, s_4_0, 0, 2, 0}, +{ 4, s_4_1, 0, 2, 0}, +{ 3, s_4_2, 0, 2, 0}, +{ 4, s_4_3, 0, 2, 0}, +{ 4, s_4_4, 0, 2, 0}, +{ 3, s_4_5, 0, 2, 0}, +{ 1, s_4_6, 0, 1, 0} }; -static const symbol s_5_0[4] = { 'a', 'n', 'c', 'i' }; -static const symbol s_5_1[4] = { 'e', 'n', 'c', 'i' }; -static const symbol s_5_2[3] = { 'o', 'g', 'i' }; -static const symbol s_5_3[2] = { 'l', 'i' }; -static const symbol s_5_4[3] = { 'b', 'l', 'i' }; -static const symbol s_5_5[4] = { 'a', 'b', 'l', 'i' }; -static const symbol s_5_6[4] = { 'a', 'l', 'l', 'i' }; -static const symbol s_5_7[5] = { 'f', 'u', 'l', 'l', 'i' }; -static const symbol s_5_8[6] = { 'l', 'e', 's', 's', 'l', 'i' }; -static const symbol s_5_9[5] = { 'o', 'u', 's', 'l', 'i' }; -static const symbol s_5_10[5] = { 'e', 'n', 't', 'l', 'i' }; -static const symbol s_5_11[5] = { 'a', 'l', 'i', 't', 'i' }; -static const symbol s_5_12[6] = { 'b', 'i', 'l', 'i', 't', 'i' }; -static const symbol s_5_13[5] = { 'i', 'v', 'i', 't', 'i' }; -static const symbol s_5_14[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_5_15[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_5_16[5] = { 'a', 'l', 'i', 's', 'm' }; -static const symbol s_5_17[5] = { 'a', 't', 'i', 'o', 'n' }; -static const symbol s_5_18[7] = { 'i', 'z', 'a', 't', 'i', 'o', 'n' }; -static const symbol s_5_19[4] = { 'i', 'z', 'e', 'r' }; -static const symbol s_5_20[4] = { 'a', 't', 'o', 'r' }; -static const symbol s_5_21[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; -static const symbol s_5_22[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; -static const symbol s_5_23[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; - -static const struct among a_5[24] = -{ -{ 4, s_5_0, -1, 3, 0}, -{ 4, s_5_1, -1, 2, 0}, -{ 3, s_5_2, -1, 13, 0}, -{ 2, s_5_3, -1, 15, 0}, -{ 3, s_5_4, 3, 12, 0}, -{ 4, s_5_5, 4, 4, 0}, -{ 4, s_5_6, 3, 8, 0}, -{ 5, s_5_7, 3, 9, 0}, -{ 6, s_5_8, 3, 14, 0}, -{ 5, s_5_9, 3, 10, 0}, -{ 5, s_5_10, 3, 5, 0}, -{ 5, s_5_11, -1, 8, 0}, -{ 6, s_5_12, -1, 12, 0}, -{ 5, s_5_13, -1, 11, 0}, -{ 6, s_5_14, -1, 1, 0}, -{ 7, s_5_15, 14, 7, 0}, -{ 5, s_5_16, -1, 8, 0}, -{ 5, s_5_17, -1, 7, 0}, -{ 7, s_5_18, 17, 6, 0}, -{ 4, s_5_19, -1, 6, 0}, -{ 4, s_5_20, -1, 7, 0}, -{ 7, s_5_21, -1, 11, 0}, -{ 7, s_5_22, -1, 9, 0}, -{ 7, s_5_23, -1, 10, 0} +static const symbol s_5_1[2] = { 'e', 'd' }; +static const symbol s_5_2[3] = { 'e', 'e', 'd' }; +static const symbol s_5_3[3] = { 'i', 'n', 'g' }; +static const symbol s_5_4[4] = { 'e', 'd', 'l', 'y' }; +static const symbol s_5_5[5] = { 'e', 'e', 'd', 'l', 'y' }; +static const symbol s_5_6[5] = { 'i', 'n', 'g', 'l', 'y' }; +static const struct among a_5[7] = { +{ 0, 0, 0, -1, 0}, +{ 2, s_5_1, -1, 2, 0}, +{ 3, s_5_2, -1, 1, 0}, +{ 3, s_5_3, -3, 3, 0}, +{ 4, s_5_4, -4, 2, 0}, +{ 5, s_5_5, -1, 1, 0}, +{ 5, s_5_6, -6, 2, 0} }; -static const symbol s_6_0[5] = { 'i', 'c', 'a', 't', 'e' }; -static const symbol s_6_1[5] = { 'a', 't', 'i', 'v', 'e' }; -static const symbol s_6_2[5] = { 'a', 'l', 'i', 'z', 'e' }; -static const symbol s_6_3[5] = { 'i', 'c', 'i', 't', 'i' }; -static const symbol s_6_4[4] = { 'i', 'c', 'a', 'l' }; -static const symbol s_6_5[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_6_6[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_6_7[3] = { 'f', 'u', 'l' }; -static const symbol s_6_8[4] = { 'n', 'e', 's', 's' }; - -static const struct among a_6[9] = -{ -{ 5, s_6_0, -1, 4, 0}, -{ 5, s_6_1, -1, 6, 0}, -{ 5, s_6_2, -1, 3, 0}, -{ 5, s_6_3, -1, 4, 0}, -{ 4, s_6_4, -1, 4, 0}, -{ 6, s_6_5, -1, 1, 0}, -{ 7, s_6_6, 5, 2, 0}, -{ 3, s_6_7, -1, 5, 0}, -{ 4, s_6_8, -1, 5, 0} +static const symbol s_6_1[2] = { 'b', 'b' }; +static const symbol s_6_2[2] = { 'd', 'd' }; +static const symbol s_6_3[2] = { 'f', 'f' }; +static const symbol s_6_4[2] = { 'g', 'g' }; +static const symbol s_6_5[2] = { 'b', 'l' }; +static const symbol s_6_6[2] = { 'm', 'm' }; +static const symbol s_6_7[2] = { 'n', 'n' }; +static const symbol s_6_8[2] = { 'p', 'p' }; +static const symbol s_6_9[2] = { 'r', 'r' }; +static const symbol s_6_10[2] = { 'a', 't' }; +static const symbol s_6_11[2] = { 't', 't' }; +static const symbol s_6_12[2] = { 'i', 'z' }; +static const struct among a_6[13] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_6_1, -1, 2, 0}, +{ 2, s_6_2, -2, 2, 0}, +{ 2, s_6_3, -3, 2, 0}, +{ 2, s_6_4, -4, 2, 0}, +{ 2, s_6_5, -5, 1, 0}, +{ 2, s_6_6, -6, 2, 0}, +{ 2, s_6_7, -7, 2, 0}, +{ 2, s_6_8, -8, 2, 0}, +{ 2, s_6_9, -9, 2, 0}, +{ 2, s_6_10, -10, 1, 0}, +{ 2, s_6_11, -11, 2, 0}, +{ 2, s_6_12, -12, 1, 0} }; -static const symbol s_7_0[2] = { 'i', 'c' }; -static const symbol s_7_1[4] = { 'a', 'n', 'c', 'e' }; -static const symbol s_7_2[4] = { 'e', 'n', 'c', 'e' }; -static const symbol s_7_3[4] = { 'a', 'b', 'l', 'e' }; -static const symbol s_7_4[4] = { 'i', 'b', 'l', 'e' }; -static const symbol s_7_5[3] = { 'a', 't', 'e' }; -static const symbol s_7_6[3] = { 'i', 'v', 'e' }; -static const symbol s_7_7[3] = { 'i', 'z', 'e' }; -static const symbol s_7_8[3] = { 'i', 't', 'i' }; -static const symbol s_7_9[2] = { 'a', 'l' }; -static const symbol s_7_10[3] = { 'i', 's', 'm' }; -static const symbol s_7_11[3] = { 'i', 'o', 'n' }; -static const symbol s_7_12[2] = { 'e', 'r' }; -static const symbol s_7_13[3] = { 'o', 'u', 's' }; -static const symbol s_7_14[3] = { 'a', 'n', 't' }; -static const symbol s_7_15[3] = { 'e', 'n', 't' }; -static const symbol s_7_16[4] = { 'm', 'e', 'n', 't' }; -static const symbol s_7_17[5] = { 'e', 'm', 'e', 'n', 't' }; - -static const struct among a_7[18] = -{ -{ 2, s_7_0, -1, 1, 0}, -{ 4, s_7_1, -1, 1, 0}, -{ 4, s_7_2, -1, 1, 0}, -{ 4, s_7_3, -1, 1, 0}, -{ 4, s_7_4, -1, 1, 0}, -{ 3, s_7_5, -1, 1, 0}, -{ 3, s_7_6, -1, 1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 3, s_7_8, -1, 1, 0}, -{ 2, s_7_9, -1, 1, 0}, -{ 3, s_7_10, -1, 1, 0}, -{ 3, s_7_11, -1, 2, 0}, -{ 2, s_7_12, -1, 1, 0}, -{ 3, s_7_13, -1, 1, 0}, -{ 3, s_7_14, -1, 1, 0}, -{ 3, s_7_15, -1, 1, 0}, -{ 4, s_7_16, 15, 1, 0}, -{ 5, s_7_17, 16, 1, 0} +static const symbol s_7_0[4] = { 'a', 'n', 'c', 'i' }; +static const symbol s_7_1[4] = { 'e', 'n', 'c', 'i' }; +static const symbol s_7_2[3] = { 'o', 'g', 'i' }; +static const symbol s_7_3[2] = { 'l', 'i' }; +static const symbol s_7_4[3] = { 'b', 'l', 'i' }; +static const symbol s_7_5[4] = { 'a', 'b', 'l', 'i' }; +static const symbol s_7_6[4] = { 'a', 'l', 'l', 'i' }; +static const symbol s_7_7[5] = { 'f', 'u', 'l', 'l', 'i' }; +static const symbol s_7_8[6] = { 'l', 'e', 's', 's', 'l', 'i' }; +static const symbol s_7_9[5] = { 'o', 'u', 's', 'l', 'i' }; +static const symbol s_7_10[5] = { 'e', 'n', 't', 'l', 'i' }; +static const symbol s_7_11[5] = { 'a', 'l', 'i', 't', 'i' }; +static const symbol s_7_12[6] = { 'b', 'i', 'l', 'i', 't', 'i' }; +static const symbol s_7_13[5] = { 'i', 'v', 'i', 't', 'i' }; +static const symbol s_7_14[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_7_15[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_7_16[5] = { 'a', 'l', 'i', 's', 'm' }; +static const symbol s_7_17[5] = { 'a', 't', 'i', 'o', 'n' }; +static const symbol s_7_18[7] = { 'i', 'z', 'a', 't', 'i', 'o', 'n' }; +static const symbol s_7_19[4] = { 'i', 'z', 'e', 'r' }; +static const symbol s_7_20[4] = { 'a', 't', 'o', 'r' }; +static const symbol s_7_21[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; +static const symbol s_7_22[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; +static const symbol s_7_23[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; +static const symbol s_7_24[5] = { 'o', 'g', 'i', 's', 't' }; +static const struct among a_7[25] = { +{ 4, s_7_0, 0, 3, 0}, +{ 4, s_7_1, 0, 2, 0}, +{ 3, s_7_2, 0, 14, 0}, +{ 2, s_7_3, 0, 16, 0}, +{ 3, s_7_4, -1, 12, 0}, +{ 4, s_7_5, -1, 4, 0}, +{ 4, s_7_6, -3, 8, 0}, +{ 5, s_7_7, -4, 9, 0}, +{ 6, s_7_8, -5, 15, 0}, +{ 5, s_7_9, -6, 10, 0}, +{ 5, s_7_10, -7, 5, 0}, +{ 5, s_7_11, 0, 8, 0}, +{ 6, s_7_12, 0, 12, 0}, +{ 5, s_7_13, 0, 11, 0}, +{ 6, s_7_14, 0, 1, 0}, +{ 7, s_7_15, -1, 7, 0}, +{ 5, s_7_16, 0, 8, 0}, +{ 5, s_7_17, 0, 7, 0}, +{ 7, s_7_18, -1, 6, 0}, +{ 4, s_7_19, 0, 6, 0}, +{ 4, s_7_20, 0, 7, 0}, +{ 7, s_7_21, 0, 11, 0}, +{ 7, s_7_22, 0, 9, 0}, +{ 7, s_7_23, 0, 10, 0}, +{ 5, s_7_24, 0, 13, 0} }; -static const symbol s_8_0[1] = { 'e' }; -static const symbol s_8_1[1] = { 'l' }; - -static const struct among a_8[2] = -{ -{ 1, s_8_0, -1, 1, 0}, -{ 1, s_8_1, -1, 2, 0} +static const symbol s_8_0[5] = { 'i', 'c', 'a', 't', 'e' }; +static const symbol s_8_1[5] = { 'a', 't', 'i', 'v', 'e' }; +static const symbol s_8_2[5] = { 'a', 'l', 'i', 'z', 'e' }; +static const symbol s_8_3[5] = { 'i', 'c', 'i', 't', 'i' }; +static const symbol s_8_4[4] = { 'i', 'c', 'a', 'l' }; +static const symbol s_8_5[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_8_6[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_8_7[3] = { 'f', 'u', 'l' }; +static const symbol s_8_8[4] = { 'n', 'e', 's', 's' }; +static const struct among a_8[9] = { +{ 5, s_8_0, 0, 4, 0}, +{ 5, s_8_1, 0, 6, 0}, +{ 5, s_8_2, 0, 3, 0}, +{ 5, s_8_3, 0, 4, 0}, +{ 4, s_8_4, 0, 4, 0}, +{ 6, s_8_5, 0, 1, 0}, +{ 7, s_8_6, -1, 2, 0}, +{ 3, s_8_7, 0, 5, 0}, +{ 4, s_8_8, 0, 5, 0} }; -static const symbol s_9_0[7] = { 's', 'u', 'c', 'c', 'e', 'e', 'd' }; -static const symbol s_9_1[7] = { 'p', 'r', 'o', 'c', 'e', 'e', 'd' }; -static const symbol s_9_2[6] = { 'e', 'x', 'c', 'e', 'e', 'd' }; -static const symbol s_9_3[7] = { 'c', 'a', 'n', 'n', 'i', 'n', 'g' }; -static const symbol s_9_4[6] = { 'i', 'n', 'n', 'i', 'n', 'g' }; -static const symbol s_9_5[7] = { 'e', 'a', 'r', 'r', 'i', 'n', 'g' }; -static const symbol s_9_6[7] = { 'h', 'e', 'r', 'r', 'i', 'n', 'g' }; -static const symbol s_9_7[6] = { 'o', 'u', 't', 'i', 'n', 'g' }; - -static const struct among a_9[8] = -{ -{ 7, s_9_0, -1, -1, 0}, -{ 7, s_9_1, -1, -1, 0}, -{ 6, s_9_2, -1, -1, 0}, -{ 7, s_9_3, -1, -1, 0}, -{ 6, s_9_4, -1, -1, 0}, -{ 7, s_9_5, -1, -1, 0}, -{ 7, s_9_6, -1, -1, 0}, -{ 6, s_9_7, -1, -1, 0} +static const symbol s_9_0[2] = { 'i', 'c' }; +static const symbol s_9_1[4] = { 'a', 'n', 'c', 'e' }; +static const symbol s_9_2[4] = { 'e', 'n', 'c', 'e' }; +static const symbol s_9_3[4] = { 'a', 'b', 'l', 'e' }; +static const symbol s_9_4[4] = { 'i', 'b', 'l', 'e' }; +static const symbol s_9_5[3] = { 'a', 't', 'e' }; +static const symbol s_9_6[3] = { 'i', 'v', 'e' }; +static const symbol s_9_7[3] = { 'i', 'z', 'e' }; +static const symbol s_9_8[3] = { 'i', 't', 'i' }; +static const symbol s_9_9[2] = { 'a', 'l' }; +static const symbol s_9_10[3] = { 'i', 's', 'm' }; +static const symbol s_9_11[3] = { 'i', 'o', 'n' }; +static const symbol s_9_12[2] = { 'e', 'r' }; +static const symbol s_9_13[3] = { 'o', 'u', 's' }; +static const symbol s_9_14[3] = { 'a', 'n', 't' }; +static const symbol s_9_15[3] = { 'e', 'n', 't' }; +static const symbol s_9_16[4] = { 'm', 'e', 'n', 't' }; +static const symbol s_9_17[5] = { 'e', 'm', 'e', 'n', 't' }; +static const struct among a_9[18] = { +{ 2, s_9_0, 0, 1, 0}, +{ 4, s_9_1, 0, 1, 0}, +{ 4, s_9_2, 0, 1, 0}, +{ 4, s_9_3, 0, 1, 0}, +{ 4, s_9_4, 0, 1, 0}, +{ 3, s_9_5, 0, 1, 0}, +{ 3, s_9_6, 0, 1, 0}, +{ 3, s_9_7, 0, 1, 0}, +{ 3, s_9_8, 0, 1, 0}, +{ 2, s_9_9, 0, 1, 0}, +{ 3, s_9_10, 0, 1, 0}, +{ 3, s_9_11, 0, 2, 0}, +{ 2, s_9_12, 0, 1, 0}, +{ 3, s_9_13, 0, 1, 0}, +{ 3, s_9_14, 0, 1, 0}, +{ 3, s_9_15, 0, 1, 0}, +{ 4, s_9_16, -1, 1, 0}, +{ 5, s_9_17, -1, 1, 0} }; -static const symbol s_10_0[5] = { 'a', 'n', 'd', 'e', 's' }; -static const symbol s_10_1[5] = { 'a', 't', 'l', 'a', 's' }; -static const symbol s_10_2[4] = { 'b', 'i', 'a', 's' }; -static const symbol s_10_3[6] = { 'c', 'o', 's', 'm', 'o', 's' }; -static const symbol s_10_4[5] = { 'd', 'y', 'i', 'n', 'g' }; -static const symbol s_10_5[5] = { 'e', 'a', 'r', 'l', 'y' }; -static const symbol s_10_6[6] = { 'g', 'e', 'n', 't', 'l', 'y' }; -static const symbol s_10_7[4] = { 'h', 'o', 'w', 'e' }; -static const symbol s_10_8[4] = { 'i', 'd', 'l', 'y' }; -static const symbol s_10_9[5] = { 'l', 'y', 'i', 'n', 'g' }; -static const symbol s_10_10[4] = { 'n', 'e', 'w', 's' }; -static const symbol s_10_11[4] = { 'o', 'n', 'l', 'y' }; -static const symbol s_10_12[6] = { 's', 'i', 'n', 'g', 'l', 'y' }; -static const symbol s_10_13[5] = { 's', 'k', 'i', 'e', 's' }; -static const symbol s_10_14[4] = { 's', 'k', 'i', 's' }; -static const symbol s_10_15[3] = { 's', 'k', 'y' }; -static const symbol s_10_16[5] = { 't', 'y', 'i', 'n', 'g' }; -static const symbol s_10_17[4] = { 'u', 'g', 'l', 'y' }; +static const symbol s_10_0[1] = { 'e' }; +static const symbol s_10_1[1] = { 'l' }; +static const struct among a_10[2] = { +{ 1, s_10_0, 0, 1, 0}, +{ 1, s_10_1, 0, 2, 0} +}; -static const struct among a_10[18] = -{ -{ 5, s_10_0, -1, -1, 0}, -{ 5, s_10_1, -1, -1, 0}, -{ 4, s_10_2, -1, -1, 0}, -{ 6, s_10_3, -1, -1, 0}, -{ 5, s_10_4, -1, 3, 0}, -{ 5, s_10_5, -1, 9, 0}, -{ 6, s_10_6, -1, 7, 0}, -{ 4, s_10_7, -1, -1, 0}, -{ 4, s_10_8, -1, 6, 0}, -{ 5, s_10_9, -1, 4, 0}, -{ 4, s_10_10, -1, -1, 0}, -{ 4, s_10_11, -1, 10, 0}, -{ 6, s_10_12, -1, 11, 0}, -{ 5, s_10_13, -1, 2, 0}, -{ 4, s_10_14, -1, 1, 0}, -{ 3, s_10_15, -1, -1, 0}, -{ 5, s_10_16, -1, 5, 0}, -{ 4, s_10_17, -1, 8, 0} +static const symbol s_11_0[5] = { 'a', 'n', 'd', 'e', 's' }; +static const symbol s_11_1[5] = { 'a', 't', 'l', 'a', 's' }; +static const symbol s_11_2[4] = { 'b', 'i', 'a', 's' }; +static const symbol s_11_3[6] = { 'c', 'o', 's', 'm', 'o', 's' }; +static const symbol s_11_4[5] = { 'e', 'a', 'r', 'l', 'y' }; +static const symbol s_11_5[6] = { 'g', 'e', 'n', 't', 'l', 'y' }; +static const symbol s_11_6[4] = { 'h', 'o', 'w', 'e' }; +static const symbol s_11_7[4] = { 'i', 'd', 'l', 'y' }; +static const symbol s_11_8[4] = { 'n', 'e', 'w', 's' }; +static const symbol s_11_9[4] = { 'o', 'n', 'l', 'y' }; +static const symbol s_11_10[6] = { 's', 'i', 'n', 'g', 'l', 'y' }; +static const symbol s_11_11[5] = { 's', 'k', 'i', 'e', 's' }; +static const symbol s_11_12[4] = { 's', 'k', 'i', 's' }; +static const symbol s_11_13[3] = { 's', 'k', 'y' }; +static const symbol s_11_14[4] = { 'u', 'g', 'l', 'y' }; +static const struct among a_11[15] = { +{ 5, s_11_0, 0, -1, 0}, +{ 5, s_11_1, 0, -1, 0}, +{ 4, s_11_2, 0, -1, 0}, +{ 6, s_11_3, 0, -1, 0}, +{ 5, s_11_4, 0, 6, 0}, +{ 6, s_11_5, 0, 4, 0}, +{ 4, s_11_6, 0, -1, 0}, +{ 4, s_11_7, 0, 3, 0}, +{ 4, s_11_8, 0, -1, 0}, +{ 4, s_11_9, 0, 7, 0}, +{ 6, s_11_10, 0, 8, 0}, +{ 5, s_11_11, 0, 2, 0}, +{ 4, s_11_12, 0, 1, 0}, +{ 3, s_11_13, 0, -1, 0}, +{ 4, s_11_14, 0, 5, 0} }; static const unsigned char g_aeo[] = { 17, 64 }; @@ -318,178 +354,147 @@ static const unsigned char g_v_WXY[] = { 1, 17, 65, 208, 1 }; static const unsigned char g_valid_LI[] = { 55, 141, 2 }; -static const symbol s_0[] = { 'Y' }; -static const symbol s_1[] = { 'Y' }; -static const symbol s_2[] = { 's', 's' }; -static const symbol s_3[] = { 'i' }; -static const symbol s_4[] = { 'i', 'e' }; -static const symbol s_5[] = { 'e', 'e' }; -static const symbol s_6[] = { 'e' }; -static const symbol s_7[] = { 'e' }; -static const symbol s_8[] = { 'i' }; -static const symbol s_9[] = { 't', 'i', 'o', 'n' }; -static const symbol s_10[] = { 'e', 'n', 'c', 'e' }; -static const symbol s_11[] = { 'a', 'n', 'c', 'e' }; -static const symbol s_12[] = { 'a', 'b', 'l', 'e' }; -static const symbol s_13[] = { 'e', 'n', 't' }; -static const symbol s_14[] = { 'i', 'z', 'e' }; -static const symbol s_15[] = { 'a', 't', 'e' }; -static const symbol s_16[] = { 'a', 'l' }; -static const symbol s_17[] = { 'f', 'u', 'l' }; -static const symbol s_18[] = { 'o', 'u', 's' }; -static const symbol s_19[] = { 'i', 'v', 'e' }; -static const symbol s_20[] = { 'b', 'l', 'e' }; -static const symbol s_21[] = { 'o', 'g' }; -static const symbol s_22[] = { 'l', 'e', 's', 's' }; -static const symbol s_23[] = { 't', 'i', 'o', 'n' }; -static const symbol s_24[] = { 'a', 't', 'e' }; -static const symbol s_25[] = { 'a', 'l' }; -static const symbol s_26[] = { 'i', 'c' }; -static const symbol s_27[] = { 's', 'k', 'i' }; -static const symbol s_28[] = { 's', 'k', 'y' }; -static const symbol s_29[] = { 'd', 'i', 'e' }; -static const symbol s_30[] = { 'l', 'i', 'e' }; -static const symbol s_31[] = { 't', 'i', 'e' }; -static const symbol s_32[] = { 'i', 'd', 'l' }; -static const symbol s_33[] = { 'g', 'e', 'n', 't', 'l' }; -static const symbol s_34[] = { 'u', 'g', 'l', 'i' }; -static const symbol s_35[] = { 'e', 'a', 'r', 'l', 'i' }; -static const symbol s_36[] = { 'o', 'n', 'l', 'i' }; -static const symbol s_37[] = { 's', 'i', 'n', 'g', 'l' }; -static const symbol s_38[] = { 'y' }; - static int r_prelude(struct SN_env * z) { - z->I[2] = 0; - { int c1 = z->c; + ((SN_local *)z)->b_Y_found = 0; + { + int v_1 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != '\'') goto lab0; z->c++; z->ket = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: - z->c = c1; + z->c = v_1; } - { int c2 = z->c; + { + int v_2 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab1; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } - z->I[2] = 1; + ((SN_local *)z)->b_Y_found = 1; lab1: - z->c = c2; + z->c = v_2; } - { int c3 = z->c; - while(1) { - int c4 = z->c; - while(1) { - int c5 = z->c; + { + int v_3 = z->c; + while (1) { + int v_4 = z->c; + while (1) { + int v_5 = z->c; if (in_grouping(z, g_v, 97, 121, 0)) goto lab4; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab4; z->c++; z->ket = z->c; - z->c = c5; + z->c = v_5; break; lab4: - z->c = c5; + z->c = v_5; if (z->c >= z->l) goto lab3; z->c++; } - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } - z->I[2] = 1; + ((SN_local *)z)->b_Y_found = 1; continue; lab3: - z->c = c4; + z->c = v_4; break; } - z->c = c3; + z->c = v_3; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (z->c + 4 >= z->l || z->p[z->c + 4] >> 5 != 3 || !((2375680 >> (z->p[z->c + 4] & 0x1f)) & 1)) goto lab2; - if (!find_among(z, a_0, 3)) goto lab2; - goto lab1; - lab2: - z->c = c2; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (z->c + 3 >= z->l || z->p[z->c + 3] >> 5 != 3 || !((5513250 >> (z->p[z->c + 3] & 0x1f)) & 1)) goto lab1; + if (!find_among(z, a_0, 9, 0)) goto lab1; + break; + lab1: + z->c = v_2; { int ret = out_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - } - lab1: - z->I[1] = z->c; - + } while (0); + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_shortv(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - if (out_grouping_b(z, g_v_WXY, 89, 121, 0)) goto lab1; - if (in_grouping_b(z, g_v, 97, 121, 0)) goto lab1; + do { + int v_1 = z->l - z->c; + if (out_grouping_b(z, g_v_WXY, 89, 121, 0)) goto lab0; + if (in_grouping_b(z, g_v, 97, 121, 0)) goto lab0; + if (out_grouping_b(z, g_v, 97, 121, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_1; if (out_grouping_b(z, g_v, 97, 121, 0)) goto lab1; - goto lab0; + if (in_grouping_b(z, g_v, 97, 121, 0)) goto lab1; + if (z->c > z->lb) goto lab1; + break; lab1: - z->c = z->l - m1; - if (out_grouping_b(z, g_v, 97, 121, 0)) return 0; - if (in_grouping_b(z, g_v, 97, 121, 0)) return 0; - if (z->c > z->lb) return 0; - } -lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 4, s_2))) return 0; + } while (0); return 1; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_Step_1a(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || (z->p[z->c - 1] != 39 && z->p[z->c - 1] != 115)) { z->c = z->l - m1; goto lab0; } - if (!find_among_b(z, a_1, 3)) { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || (z->p[z->c - 1] != 39 && z->p[z->c - 1] != 115)) { z->c = z->l - v_1; goto lab0; } + if (!find_among_b(z, a_1, 3, 0)) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -497,41 +502,44 @@ static int r_Step_1a(struct SN_env * z) { } z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 115)) return 0; - among_var = find_among_b(z, a_2, 6); + among_var = find_among_b(z, a_2, 6, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 2: - { int m2 = z->l - z->c; (void)m2; -z->c = z->c - 2; - if (z->c < z->lb) goto lab2; - { int ret = slice_from_s(z, 1, s_3); + do { + int v_2 = z->l - z->c; + if (z->c - 2 < z->lb) goto lab1; + z->c -= 2; + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; - { int ret = slice_from_s(z, 2, s_4); + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } - } - lab1: + } while (0); break; case 3: if (z->c <= z->lb) return 0; z->c--; - { int ret = out_grouping_b(z, g_v, 97, 121, 1); if (ret < 0) return 0; z->c -= ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -542,100 +550,154 @@ z->c = z->c - 2; static int r_Step_1b(struct SN_env * z) { int among_var; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33554576 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_4, 6); - if (!among_var) return 0; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33554576 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = -1; else + among_var = find_among_b(z, a_5, 7, 0); z->bra = z->c; - switch (among_var) { - case 1: - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int ret = slice_from_s(z, 2, s_5); - if (ret < 0) return ret; - } - break; - case 2: - { int m_test1 = z->l - z->c; - + do { + int v_1 = z->l - z->c; + switch (among_var) { + case 1: { - int ret = out_grouping_b(z, g_v, 97, 121, 1); - if (ret < 0) return 0; - z->c -= ret; + int v_2 = z->l - z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + do { + int v_3 = z->l - z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] != 99) goto lab2; + if (!find_among_b(z, a_3, 3, 0)) goto lab2; + if (z->c > z->lb) goto lab2; + break; + lab2: + z->c = z->l - v_3; + { + int ret = slice_from_s(z, 2, s_6); + if (ret < 0) return ret; + } + } while (0); + lab1: + z->c = z->l - v_2; } - z->c = z->l - m_test1; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->ket = z->c; - z->bra = z->c; - { int m_test2 = z->l - z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else - among_var = find_among_b(z, a_3, 13); + break; + case 2: + goto lab0; + break; + case 3: + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((34881536 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_4, 7, 0); + if (!among_var) goto lab0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_6); - if (ret < 0) return ret; - } - return 0; - break; - case 2: - { int m3 = z->l - z->c; (void)m3; - if (in_grouping_b(z, g_aeo, 97, 111, 0)) goto lab0; + { + int v_4 = z->l - z->c; + if (out_grouping_b(z, g_v, 97, 121, 0)) goto lab0; if (z->c > z->lb) goto lab0; - return 0; - lab0: - z->c = z->l - m3; - } - break; - case 3: - if (z->c != z->I[1]) return 0; - { int m_test4 = z->l - z->c; - { int ret = r_shortv(z); - if (ret <= 0) return ret; - } - z->c = z->l - m_test4; + z->c = z->l - v_4; } - { int ret = slice_from_s(z, 1, s_7); + z->bra = z->c; + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } - return 0; + break; + case 2: + if (z->c > z->lb) goto lab0; break; } - z->c = z->l - m_test2; - } - z->ket = z->c; - if (z->c <= z->lb) return 0; - z->c--; - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; + break; + } + break; + lab0: + z->c = z->l - v_1; + { + int v_5 = z->l - z->c; + { + int ret = out_grouping_b(z, g_v, 97, 121, 1); + if (ret < 0) return 0; + z->c -= ret; } - break; - } + z->c = z->l - v_5; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + z->ket = z->c; + z->bra = z->c; + { + int v_6 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else + among_var = find_among_b(z, a_6, 13, 0); + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_8); + if (ret < 0) return ret; + } + return 0; + break; + case 2: + { + int v_7 = z->l - z->c; + if (in_grouping_b(z, g_aeo, 97, 111, 0)) goto lab3; + if (z->c > z->lb) goto lab3; + return 0; + lab3: + z->c = z->l - v_7; + } + break; + case 3: + if (z->c != ((SN_local *)z)->i_p1) return 0; + { + int v_8 = z->l - z->c; + { + int ret = r_shortv(z); + if (ret <= 0) return ret; + } + z->c = z->l - v_8; + } + { + int ret = slice_from_s(z, 1, s_9); + if (ret < 0) return ret; + } + return 0; + break; + } + z->c = z->l - v_6; + } + z->ket = z->c; + if (z->c <= z->lb) return 0; + z->c--; + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } while (0); return 1; } static int r_Step_1c(struct SN_env * z) { z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'Y') return 0; z->c--; - } -lab0: + } while (0); z->bra = z->c; if (out_grouping_b(z, g_v, 97, 121, 0)) return 0; - - if (z->c > z->lb) goto lab2; + if (z->c > z->lb) goto lab1; return 0; -lab2: - { int ret = slice_from_s(z, 1, s_8); +lab1: + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } return 1; @@ -644,89 +706,111 @@ static int r_Step_1c(struct SN_env * z) { static int r_Step_2(struct SN_env * z) { int among_var; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((815616 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_5, 24); + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1864192 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_7, 25, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_9); + { + int ret = slice_from_s(z, 4, s_11); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_10); + { + int ret = slice_from_s(z, 4, s_12); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_11); + { + int ret = slice_from_s(z, 4, s_13); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_12); + { + int ret = slice_from_s(z, 4, s_14); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_13); + { + int ret = slice_from_s(z, 3, s_15); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 3, s_14); + { + int ret = slice_from_s(z, 3, s_16); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_15); + { + int ret = slice_from_s(z, 3, s_17); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 2, s_16); + { + int ret = slice_from_s(z, 2, s_18); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 3, s_17); + { + int ret = slice_from_s(z, 3, s_19); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 3, s_18); + { + int ret = slice_from_s(z, 3, s_20); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 3, s_19); + { + int ret = slice_from_s(z, 3, s_21); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 3, s_20); + { + int ret = slice_from_s(z, 3, s_22); if (ret < 0) return ret; } break; case 13: - if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; - z->c--; - { int ret = slice_from_s(z, 2, s_21); + { + int ret = slice_from_s(z, 2, s_23); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 4, s_22); + if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; + z->c--; + { + int ret = slice_from_s(z, 2, s_24); if (ret < 0) return ret; } break; case 15: + { + int ret = slice_from_s(z, 4, s_25); + if (ret < 0) return ret; + } + break; + case 16: if (in_grouping_b(z, g_valid_LI, 99, 116, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -738,43 +822,51 @@ static int r_Step_3(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((528928 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_6, 9); + among_var = find_among_b(z, a_8, 9, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_23); + { + int ret = slice_from_s(z, 4, s_26); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_24); + { + int ret = slice_from_s(z, 3, s_27); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_25); + { + int ret = slice_from_s(z, 2, s_28); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_26); + { + int ret = slice_from_s(z, 2, s_29); if (ret < 0) return ret; } break; case 5: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 6: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -786,30 +878,33 @@ static int r_Step_4(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1864232 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_7, 18); + among_var = find_among_b(z, a_9, 18, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 't') return 0; z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -821,42 +916,49 @@ static int r_Step_5(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) return 0; - among_var = find_among_b(z, a_8, 2); + among_var = find_among_b(z, a_10, 2, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - - { int ret = r_R2(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - goto lab0; - lab1: - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m1 = z->l - z->c; (void)m1; - { int ret = r_shortv(z); - if (ret == 0) goto lab2; + do { + { + int ret = r_R2(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - return 0; - lab2: - z->c = z->l - m1; - } - lab0: - { int ret = slice_del(z); + break; + lab0: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + { + int ret = r_shortv(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + return 0; + lab1: + z->c = z->l - v_1; + } + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -864,76 +966,60 @@ static int r_Step_5(struct SN_env * z) { return 1; } -static int r_exception2(struct SN_env * z) { - z->ket = z->c; - if (z->c - 5 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 103)) return 0; - if (!find_among_b(z, a_9, 8)) return 0; - z->bra = z->c; - if (z->c > z->lb) return 0; - return 1; -} - static int r_exception1(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((42750482 >> (z->p[z->c + 2] & 0x1f)) & 1)) return 0; - among_var = find_among(z, a_10, 18); + among_var = find_among(z, a_11, 15, 0); if (!among_var) return 0; z->ket = z->c; if (z->c < z->l) return 0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 3, s_27); + { + int ret = slice_from_s(z, 3, s_30); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_28); + { + int ret = slice_from_s(z, 3, s_31); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_29); + { + int ret = slice_from_s(z, 3, s_32); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 3, s_30); + { + int ret = slice_from_s(z, 5, s_33); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_31); + { + int ret = slice_from_s(z, 4, s_34); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 3, s_32); + { + int ret = slice_from_s(z, 5, s_35); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 5, s_33); + { + int ret = slice_from_s(z, 4, s_36); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 4, s_34); - if (ret < 0) return ret; - } - break; - case 9: - { int ret = slice_from_s(z, 5, s_35); - if (ret < 0) return ret; - } - break; - case 10: - { int ret = slice_from_s(z, 4, s_36); - if (ret < 0) return ret; - } - break; - case 11: - { int ret = slice_from_s(z, 5, s_37); + { + int ret = slice_from_s(z, 5, s_37); if (ret < 0) return ret; } break; @@ -942,127 +1028,145 @@ static int r_exception1(struct SN_env * z) { } static int r_postlude(struct SN_env * z) { - if (!(z->I[2])) return 0; - while(1) { - int c1 = z->c; - while(1) { - int c2 = z->c; + if (!((SN_local *)z)->b_Y_found) return 0; + while (1) { + int v_1 = z->c; + while (1) { + int v_2 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'Y') goto lab1; z->c++; z->ket = z->c; - z->c = c2; + z->c = v_2; break; lab1: - z->c = c2; + z->c = v_2; if (z->c >= z->l) goto lab0; z->c++; } - { int ret = slice_from_s(z, 1, s_38); + { + int ret = slice_from_s(z, 1, s_38); if (ret < 0) return ret; } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } extern int english_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_exception1(z); - if (ret == 0) goto lab1; + do { + int v_1 = z->c; + { + int ret = r_exception1(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = c1; - { int c2 = z->c; -z->c = z->c + 3; - if (z->c > z->l) goto lab3; - goto lab2; - lab3: - z->c = c2; + break; + lab0: + z->c = v_1; + { + int v_2 = z->c; + if (z->c + 3 > z->l) goto lab2; + z->c += 3; + goto lab1; + lab2: + z->c = v_2; } - goto lab0; - lab2: - z->c = c1; - - { int ret = r_prelude(z); + break; + lab1: + z->c = v_1; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_Step_1a(z); + { + int v_3 = z->l - z->c; + { + int ret = r_Step_1a(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_exception2(z); - if (ret == 0) goto lab5; + { + int v_4 = z->l - z->c; + { + int ret = r_Step_1b(z); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_Step_1b(z); - if (ret < 0) return ret; - } - z->c = z->l - m5; - } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_Step_1c(z); - if (ret < 0) return ret; - } - z->c = z->l - m6; + z->c = z->l - v_4; + } + { + int v_5 = z->l - z->c; + { + int ret = r_Step_1c(z); + if (ret < 0) return ret; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_Step_2(z); - if (ret < 0) return ret; - } - z->c = z->l - m7; + z->c = z->l - v_5; + } + { + int v_6 = z->l - z->c; + { + int ret = r_Step_2(z); + if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_Step_3(z); - if (ret < 0) return ret; - } - z->c = z->l - m8; + z->c = z->l - v_6; + } + { + int v_7 = z->l - z->c; + { + int ret = r_Step_3(z); + if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_Step_4(z); - if (ret < 0) return ret; - } - z->c = z->l - m9; + z->c = z->l - v_7; + } + { + int v_8 = z->l - z->c; + { + int ret = r_Step_4(z); + if (ret < 0) return ret; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_Step_5(z); - if (ret < 0) return ret; - } - z->c = z->l - m10; + z->c = z->l - v_8; + } + { + int v_9 = z->l - z->c; + { + int ret = r_Step_5(z); + if (ret < 0) return ret; } + z->c = z->l - v_9; } - lab4: z->c = z->lb; - { int c11 = z->c; - { int ret = r_postlude(z); + { + int v_10 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c11; + z->c = v_10; } - } -lab0: + } while (0); return 1; } -extern struct SN_env * english_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * english_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_Y_found = 0; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void english_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void english_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_finnish.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_finnish.c index 978ecb0227890..c4318056c5d87 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_finnish.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_finnish.c @@ -1,6 +1,20 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from finnish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_finnish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_ending_removed; + symbol * s_x; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +23,7 @@ extern int finnish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_tidy(struct SN_env * z); static int r_other_endings(struct SN_env * z); static int r_t_plural(struct SN_env * z); @@ -20,18 +35,13 @@ static int r_possessive(struct SN_env * z); static int r_particle_etc(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * finnish_ISO_8859_1_create_env(void); -extern void finnish_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'k', 's', 'e' }; +static const symbol s_1[] = { 'k', 's', 'i' }; +static const symbol s_2[] = { 'i', 'e' }; +static const symbol s_3[] = { 'p', 'o' }; +static const symbol s_4[] = { 'p', 'o' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 'p', 'a' }; static const symbol s_0_1[3] = { 's', 't', 'i' }; static const symbol s_0_2[4] = { 'k', 'a', 'a', 'n' }; @@ -42,19 +52,17 @@ static const symbol s_0_6[4] = { 'k', 0xE4, 0xE4, 'n' }; static const symbol s_0_7[2] = { 'k', 'o' }; static const symbol s_0_8[2] = { 'p', 0xE4 }; static const symbol s_0_9[2] = { 'k', 0xF6 }; - -static const struct among a_0[10] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 3, s_0_1, -1, 2, 0}, -{ 4, s_0_2, -1, 1, 0}, -{ 3, s_0_3, -1, 1, 0}, -{ 3, s_0_4, -1, 1, 0}, -{ 3, s_0_5, -1, 1, 0}, -{ 4, s_0_6, -1, 1, 0}, -{ 2, s_0_7, -1, 1, 0}, -{ 2, s_0_8, -1, 1, 0}, -{ 2, s_0_9, -1, 1, 0} +static const struct among a_0[10] = { +{ 2, s_0_0, 0, 1, 0}, +{ 3, s_0_1, 0, 2, 0}, +{ 4, s_0_2, 0, 1, 0}, +{ 3, s_0_3, 0, 1, 0}, +{ 3, s_0_4, 0, 1, 0}, +{ 3, s_0_5, 0, 1, 0}, +{ 4, s_0_6, 0, 1, 0}, +{ 2, s_0_7, 0, 1, 0}, +{ 2, s_0_8, 0, 1, 0}, +{ 2, s_0_9, 0, 1, 0} }; static const symbol s_1_0[3] = { 'l', 'l', 'a' }; @@ -63,15 +71,13 @@ static const symbol s_1_2[3] = { 's', 's', 'a' }; static const symbol s_1_3[2] = { 't', 'a' }; static const symbol s_1_4[3] = { 'l', 't', 'a' }; static const symbol s_1_5[3] = { 's', 't', 'a' }; - -static const struct among a_1[6] = -{ -{ 3, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 3, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0}, -{ 3, s_1_4, 3, -1, 0}, -{ 3, s_1_5, 3, -1, 0} +static const struct among a_1[6] = { +{ 3, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 3, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 3, s_1_4, -1, -1, 0}, +{ 3, s_1_5, -2, -1, 0} }; static const symbol s_2_0[3] = { 'l', 'l', 0xE4 }; @@ -80,24 +86,20 @@ static const symbol s_2_2[3] = { 's', 's', 0xE4 }; static const symbol s_2_3[2] = { 't', 0xE4 }; static const symbol s_2_4[3] = { 'l', 't', 0xE4 }; static const symbol s_2_5[3] = { 's', 't', 0xE4 }; - -static const struct among a_2[6] = -{ -{ 3, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 3, s_2_2, -1, -1, 0}, -{ 2, s_2_3, -1, -1, 0}, -{ 3, s_2_4, 3, -1, 0}, -{ 3, s_2_5, 3, -1, 0} +static const struct among a_2[6] = { +{ 3, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 3, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, -1, 0}, +{ 3, s_2_4, -1, -1, 0}, +{ 3, s_2_5, -2, -1, 0} }; static const symbol s_3_0[3] = { 'l', 'l', 'e' }; static const symbol s_3_1[3] = { 'i', 'n', 'e' }; - -static const struct among a_3[2] = -{ -{ 3, s_3_0, -1, -1, 0}, -{ 3, s_3_1, -1, -1, 0} +static const struct among a_3[2] = { +{ 3, s_3_0, 0, -1, 0}, +{ 3, s_3_1, 0, -1, 0} }; static const symbol s_4_0[3] = { 'n', 's', 'a' }; @@ -109,18 +111,16 @@ static const symbol s_4_5[2] = { 'a', 'n' }; static const symbol s_4_6[2] = { 'e', 'n' }; static const symbol s_4_7[2] = { 0xE4, 'n' }; static const symbol s_4_8[3] = { 'n', 's', 0xE4 }; - -static const struct among a_4[9] = -{ -{ 3, s_4_0, -1, 3, 0}, -{ 3, s_4_1, -1, 3, 0}, -{ 3, s_4_2, -1, 3, 0}, -{ 2, s_4_3, -1, 2, 0}, -{ 2, s_4_4, -1, 1, 0}, -{ 2, s_4_5, -1, 4, 0}, -{ 2, s_4_6, -1, 6, 0}, -{ 2, s_4_7, -1, 5, 0}, -{ 3, s_4_8, -1, 3, 0} +static const struct among a_4[9] = { +{ 3, s_4_0, 0, 3, 0}, +{ 3, s_4_1, 0, 3, 0}, +{ 3, s_4_2, 0, 3, 0}, +{ 2, s_4_3, 0, 2, 0}, +{ 2, s_4_4, 0, 1, 0}, +{ 2, s_4_5, 0, 4, 0}, +{ 2, s_4_6, 0, 6, 0}, +{ 2, s_4_7, 0, 5, 0}, +{ 3, s_4_8, 0, 3, 0} }; static const symbol s_5_0[2] = { 'a', 'a' }; @@ -130,16 +130,14 @@ static const symbol s_5_3[2] = { 'o', 'o' }; static const symbol s_5_4[2] = { 'u', 'u' }; static const symbol s_5_5[2] = { 0xE4, 0xE4 }; static const symbol s_5_6[2] = { 0xF6, 0xF6 }; - -static const struct among a_5[7] = -{ -{ 2, s_5_0, -1, -1, 0}, -{ 2, s_5_1, -1, -1, 0}, -{ 2, s_5_2, -1, -1, 0}, -{ 2, s_5_3, -1, -1, 0}, -{ 2, s_5_4, -1, -1, 0}, -{ 2, s_5_5, -1, -1, 0}, -{ 2, s_5_6, -1, -1, 0} +static const struct among a_5[7] = { +{ 2, s_5_0, 0, -1, 0}, +{ 2, s_5_1, 0, -1, 0}, +{ 2, s_5_2, 0, -1, 0}, +{ 2, s_5_3, 0, -1, 0}, +{ 2, s_5_4, 0, -1, 0}, +{ 2, s_5_5, 0, -1, 0}, +{ 2, s_5_6, 0, -1, 0} }; static const symbol s_6_0[1] = { 'a' }; @@ -172,41 +170,47 @@ static const symbol s_6_26[2] = { 't', 0xE4 }; static const symbol s_6_27[3] = { 'l', 't', 0xE4 }; static const symbol s_6_28[3] = { 's', 't', 0xE4 }; static const symbol s_6_29[3] = { 't', 't', 0xE4 }; - -static const struct among a_6[30] = -{ -{ 1, s_6_0, -1, 8, 0}, -{ 3, s_6_1, 0, -1, 0}, -{ 2, s_6_2, 0, -1, 0}, -{ 3, s_6_3, 0, -1, 0}, -{ 2, s_6_4, 0, -1, 0}, -{ 3, s_6_5, 4, -1, 0}, -{ 3, s_6_6, 4, -1, 0}, -{ 3, s_6_7, 4, 2, 0}, -{ 3, s_6_8, -1, -1, 0}, -{ 3, s_6_9, -1, -1, 0}, -{ 3, s_6_10, -1, -1, 0}, -{ 1, s_6_11, -1, 7, 0}, -{ 3, s_6_12, 11, 1, 0}, -{ 3, s_6_13, 11, -1, r_VI}, -{ 4, s_6_14, 11, -1, r_LONG}, -{ 3, s_6_15, 11, 2, 0}, -{ 4, s_6_16, 11, -1, r_VI}, -{ 3, s_6_17, 11, 3, 0}, -{ 4, s_6_18, 11, -1, r_VI}, -{ 3, s_6_19, 11, 4, 0}, -{ 3, s_6_20, 11, 5, 0}, -{ 3, s_6_21, 11, 6, 0}, -{ 1, s_6_22, -1, 8, 0}, -{ 3, s_6_23, 22, -1, 0}, -{ 2, s_6_24, 22, -1, 0}, -{ 3, s_6_25, 22, -1, 0}, -{ 2, s_6_26, 22, -1, 0}, -{ 3, s_6_27, 26, -1, 0}, -{ 3, s_6_28, 26, -1, 0}, -{ 3, s_6_29, 26, 2, 0} +static const struct among a_6[30] = { +{ 1, s_6_0, 0, 8, 0}, +{ 3, s_6_1, -1, -1, 0}, +{ 2, s_6_2, -2, -1, 0}, +{ 3, s_6_3, -3, -1, 0}, +{ 2, s_6_4, -4, -1, 0}, +{ 3, s_6_5, -1, -1, 0}, +{ 3, s_6_6, -2, -1, 0}, +{ 3, s_6_7, -3, 2, 0}, +{ 3, s_6_8, 0, -1, 0}, +{ 3, s_6_9, 0, -1, 0}, +{ 3, s_6_10, 0, -1, 0}, +{ 1, s_6_11, 0, 7, 0}, +{ 3, s_6_12, -1, 1, 0}, +{ 3, s_6_13, -2, -1, 1}, +{ 4, s_6_14, -3, -1, 2}, +{ 3, s_6_15, -4, 2, 0}, +{ 4, s_6_16, -5, -1, 1}, +{ 3, s_6_17, -6, 3, 0}, +{ 4, s_6_18, -7, -1, 1}, +{ 3, s_6_19, -8, 4, 0}, +{ 3, s_6_20, -9, 5, 0}, +{ 3, s_6_21, -10, 6, 0}, +{ 1, s_6_22, 0, 8, 0}, +{ 3, s_6_23, -1, -1, 0}, +{ 2, s_6_24, -2, -1, 0}, +{ 3, s_6_25, -3, -1, 0}, +{ 2, s_6_26, -4, -1, 0}, +{ 3, s_6_27, -1, -1, 0}, +{ 3, s_6_28, -2, -1, 0}, +{ 3, s_6_29, -3, 2, 0} }; +static int af_6(struct SN_env * z) { + switch (z->af) { + case 1: return r_VI(z); + case 2: return r_LONG(z); + } + return -1; +} + static const symbol s_7_0[3] = { 'e', 'j', 'a' }; static const symbol s_7_1[3] = { 'm', 'm', 'a' }; static const symbol s_7_2[4] = { 'i', 'm', 'm', 'a' }; @@ -221,41 +225,28 @@ static const symbol s_7_10[3] = { 'm', 'm', 0xE4 }; static const symbol s_7_11[4] = { 'i', 'm', 'm', 0xE4 }; static const symbol s_7_12[3] = { 'm', 'p', 0xE4 }; static const symbol s_7_13[4] = { 'i', 'm', 'p', 0xE4 }; - -static const struct among a_7[14] = -{ -{ 3, s_7_0, -1, -1, 0}, -{ 3, s_7_1, -1, 1, 0}, -{ 4, s_7_2, 1, -1, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 4, s_7_4, 3, -1, 0}, -{ 3, s_7_5, -1, 1, 0}, -{ 4, s_7_6, 5, -1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 4, s_7_8, 7, -1, 0}, -{ 3, s_7_9, -1, -1, 0}, -{ 3, s_7_10, -1, 1, 0}, -{ 4, s_7_11, 10, -1, 0}, -{ 3, s_7_12, -1, 1, 0}, -{ 4, s_7_13, 12, -1, 0} -}; - -static const symbol s_8_0[1] = { 'i' }; -static const symbol s_8_1[1] = { 'j' }; - -static const struct among a_8[2] = -{ -{ 1, s_8_0, -1, -1, 0}, -{ 1, s_8_1, -1, -1, 0} +static const struct among a_7[14] = { +{ 3, s_7_0, 0, -1, 0}, +{ 3, s_7_1, 0, 1, 0}, +{ 4, s_7_2, -1, -1, 0}, +{ 3, s_7_3, 0, 1, 0}, +{ 4, s_7_4, -1, -1, 0}, +{ 3, s_7_5, 0, 1, 0}, +{ 4, s_7_6, -1, -1, 0}, +{ 3, s_7_7, 0, 1, 0}, +{ 4, s_7_8, -1, -1, 0}, +{ 3, s_7_9, 0, -1, 0}, +{ 3, s_7_10, 0, 1, 0}, +{ 4, s_7_11, -1, -1, 0}, +{ 3, s_7_12, 0, 1, 0}, +{ 4, s_7_13, -1, -1, 0} }; static const symbol s_9_0[3] = { 'm', 'm', 'a' }; static const symbol s_9_1[4] = { 'i', 'm', 'm', 'a' }; - -static const struct among a_9[2] = -{ -{ 3, s_9_0, -1, 1, 0}, -{ 4, s_9_1, 0, -1, 0} +static const struct among a_9[2] = { +{ 3, s_9_0, 0, 1, 0}, +{ 4, s_9_1, -1, -1, 0} }; static const unsigned char g_AEI[] = { 17, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8 }; @@ -268,63 +259,63 @@ static const unsigned char g_V2[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, static const unsigned char g_particle_end[] = { 17, 97, 24, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 32 }; -static const symbol s_0[] = { 'k', 's', 'e' }; -static const symbol s_1[] = { 'k', 's', 'i' }; -static const symbol s_2[] = { 'i', 'e' }; -static const symbol s_3[] = { 'p', 'o' }; -static const symbol s_4[] = { 'p', 'o' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - - if (out_grouping(z, g_V1, 97, 246, 1) < 0) return 0; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int ret = out_grouping(z, g_V1, 97, 246, 1); + if (ret < 0) return 0; + z->c += ret; + } { int ret = in_grouping(z, g_V1, 97, 246, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (out_grouping(z, g_V1, 97, 246, 1) < 0) return 0; - + ((SN_local *)z)->i_p1 = z->c; + { + int ret = out_grouping(z, g_V1, 97, 246, 1); + if (ret < 0) return 0; + z->c += ret; + } { int ret = in_grouping(z, g_V1, 97, 246, 1); if (ret < 0) return 0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; return 1; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_particle_etc(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - among_var = find_among_b(z, a_0, 10); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_0, 10, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: if (in_grouping_b(z, g_particle_end, 97, 246, 0)) return 0; break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -332,63 +323,71 @@ static int r_particle_etc(struct SN_env * z) { static int r_possessive(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - among_var = find_among_b(z, a_4, 9); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_4, 9, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'k') goto lab0; z->c--; return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; if (!(eq_s_b(z, 3, s_0))) return 0; z->bra = z->c; - { int ret = slice_from_s(z, 3, s_1); + { + int ret = slice_from_s(z, 3, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 4: if (z->c - 1 <= z->lb || z->p[z->c - 1] != 97) return 0; - if (!find_among_b(z, a_1, 6)) return 0; - { int ret = slice_del(z); + if (!find_among_b(z, a_1, 6, 0)) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 5: if (z->c - 1 <= z->lb || z->p[z->c - 1] != 228) return 0; - if (!find_among_b(z, a_2, 6)) return 0; - { int ret = slice_del(z); + if (!find_among_b(z, a_2, 6, 0)) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 6: if (z->c - 2 <= z->lb || z->p[z->c - 1] != 101) return 0; - if (!find_among_b(z, a_3, 2)) return 0; - { int ret = slice_del(z); + if (!find_among_b(z, a_3, 2, 0)) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -397,28 +396,26 @@ static int r_possessive(struct SN_env * z) { } static int r_LONG(struct SN_env * z) { - if (!find_among_b(z, a_5, 7)) return 0; - return 1; + return find_among_b(z, a_5, 7, 0) != 0; } static int r_VI(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] != 'i') return 0; z->c--; - if (in_grouping_b(z, g_V2, 97, 246, 0)) return 0; - return 1; + return !in_grouping_b(z, g_V2, 97, 246, 0); } static int r_case_ending(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - among_var = find_among_b(z, a_6, 30); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_6, 30, af_6); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: @@ -446,21 +443,24 @@ static int r_case_ending(struct SN_env * z) { z->c--; break; case 7: - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int ret = r_LONG(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; + { + int ret = r_LONG(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m4; - if (!(eq_s_b(z, 2, s_2))) { z->c = z->l - m2; goto lab0; } - } - lab1: - z->c = z->l - m3; - if (z->c <= z->lb) { z->c = z->l - m2; goto lab0; } + break; + lab1: + z->c = z->l - v_4; + if (!(eq_s_b(z, 2, s_2))) { z->c = z->l - v_2; goto lab0; } + } while (0); + z->c = z->l - v_3; + if (z->c <= z->lb) { z->c = z->l - v_2; goto lab0; } z->c--; } z->bra = z->c; @@ -473,53 +473,57 @@ static int r_case_ending(struct SN_env * z) { if (in_grouping_b(z, g_C, 98, 122, 0)) return 0; break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[2] = 1; + ((SN_local *)z)->b_ending_removed = 1; return 1; } static int r_other_endings(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p2) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p2; z->ket = z->c; - among_var = find_among_b(z, a_7, 14); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_7, 14, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (!(eq_s_b(z, 2, s_3))) goto lab0; return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_i_plural(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 106)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_8, 2)) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 106)) { z->lb = v_1; return 0; } + z->c--; z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -527,196 +531,246 @@ static int r_i_plural(struct SN_env * z) { static int r_t_plural(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = v_1; return 0; } z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - if (in_grouping_b(z, g_V1, 97, 246, 0)) { z->lb = mlimit1; return 0; } - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + if (in_grouping_b(z, g_V1, 97, 246, 0)) { z->lb = v_1; return 0; } + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } - - { int mlimit3; - if (z->c < z->I[0]) return 0; - mlimit3 = z->lb; z->lb = z->I[0]; + { + int v_3; + if (z->c < ((SN_local *)z)->i_p2) return 0; + v_3 = z->lb; z->lb = ((SN_local *)z)->i_p2; z->ket = z->c; - if (z->c - 2 <= z->lb || z->p[z->c - 1] != 97) { z->lb = mlimit3; return 0; } - among_var = find_among_b(z, a_9, 2); - if (!among_var) { z->lb = mlimit3; return 0; } + if (z->c - 2 <= z->lb || z->p[z->c - 1] != 97) { z->lb = v_3; return 0; } + among_var = find_among_b(z, a_9, 2, 0); + if (!among_var) { z->lb = v_3; return 0; } z->bra = z->c; - z->lb = mlimit3; + z->lb = v_3; } switch (among_var) { case 1: - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; if (!(eq_s_b(z, 2, s_4))) goto lab0; return 0; lab0: - z->c = z->l - m4; + z->c = z->l - v_4; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_tidy(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_LONG(z); + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; + { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + { + int ret = r_LONG(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; z->ket = z->c; if (z->c <= z->lb) goto lab0; z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } } lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; if (in_grouping_b(z, g_AEI, 97, 228, 0)) goto lab1; z->bra = z->c; if (in_grouping_b(z, g_C, 98, 122, 0)) goto lab1; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab1: - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab2; z->c--; z->bra = z->c; - { int m6 = z->l - z->c; (void)m6; - if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab4; + do { + int v_6 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab3; z->c--; - goto lab3; - lab4: - z->c = z->l - m6; + break; + lab3: + z->c = z->l - v_6; if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab2; z->c--; - } - lab3: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m7 = z->l - z->c; (void)m7; + { + int v_7 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab5; + if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab4; z->c--; z->bra = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab5; + if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab4; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab5: - z->c = z->l - m7; + lab4: + z->c = z->l - v_7; } - z->lb = mlimit1; + z->lb = v_1; } - if (in_grouping_b(z, g_V1, 97, 246, 1) < 0) return 0; z->ket = z->c; if (in_grouping_b(z, g_C, 98, 122, 0)) return 0; z->bra = z->c; - z->S[0] = slice_to(z, z->S[0]); - if (z->S[0] == 0) return -1; - if (!(eq_v_b(z, z->S[0]))) return 0; - { int ret = slice_del(z); + { + int ret = slice_to(z, &((SN_local *)z)->s_x); + if (ret < 0) return ret; + } + if (!(eq_v_b(z, ((SN_local *)z)->s_x))) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int finnish_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - z->I[2] = 0; + ((SN_local *)z)->b_ending_removed = 0; z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_particle_etc(z); + { + int v_2 = z->l - z->c; + { + int ret = r_particle_etc(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_possessive(z); + { + int v_3 = z->l - z->c; + { + int ret = r_possessive(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_case_ending(z); + { + int v_4 = z->l - z->c; + { + int ret = r_case_ending(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_other_endings(z); + { + int v_5 = z->l - z->c; + { + int ret = r_other_endings(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - - if (!(z->I[2])) goto lab1; - { int m6 = z->l - z->c; (void)m6; - { int ret = r_i_plural(z); - if (ret < 0) return ret; + do { + if (!((SN_local *)z)->b_ending_removed) goto lab0; + { + int v_6 = z->l - z->c; + { + int ret = r_i_plural(z); + if (ret < 0) return ret; + } + z->c = z->l - v_6; } - z->c = z->l - m6; - } - goto lab0; -lab1: - { int m7 = z->l - z->c; (void)m7; - { int ret = r_t_plural(z); - if (ret < 0) return ret; + break; + lab0: + { + int v_7 = z->l - z->c; + { + int ret = r_t_plural(z); + if (ret < 0) return ret; + } + z->c = z->l - v_7; } - z->c = z->l - m7; - } -lab0: - { int m8 = z->l - z->c; (void)m8; - { int ret = r_tidy(z); + } while (0); + { + int v_8 = z->l - z->c; + { + int ret = r_tidy(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } z->c = z->lb; return 1; } -extern struct SN_env * finnish_ISO_8859_1_create_env(void) { return SN_create_env(1, 3); } +extern struct SN_env * finnish_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_ending_removed = 0; + ((SN_local *)z)->s_x = NULL; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + + if ((((SN_local *)z)->s_x = create_s()) == NULL) { + finnish_ISO_8859_1_close_env(z); + return NULL; + } + } + return z; +} -extern void finnish_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 1); } +extern void finnish_ISO_8859_1_close_env(struct SN_env * z) { + if (z) { + lose_s(((SN_local *)z)->s_x); + } + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_french.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_french.c index 5706a5296bb9b..ba6fff32b00f3 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_french.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_french.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from french.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_french.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int french_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_un_accent(struct SN_env * z); static int r_un_double(struct SN_env * z); static int r_residual_suffix(struct SN_env * z); @@ -22,27 +36,54 @@ static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); static int r_elisions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * french_ISO_8859_1_create_env(void); -extern void french_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'q', 'u' }; +static const symbol s_1[] = { 'U' }; +static const symbol s_2[] = { 'I' }; +static const symbol s_3[] = { 'Y' }; +static const symbol s_4[] = { 'H', 'e' }; +static const symbol s_5[] = { 'H', 'i' }; +static const symbol s_6[] = { 'Y' }; +static const symbol s_7[] = { 'U' }; +static const symbol s_8[] = { 'i' }; +static const symbol s_9[] = { 'u' }; +static const symbol s_10[] = { 'y' }; +static const symbol s_11[] = { 0xEB }; +static const symbol s_12[] = { 0xEF }; +static const symbol s_13[] = { 'i', 'c' }; +static const symbol s_14[] = { 'i', 'q', 'U' }; +static const symbol s_15[] = { 'l', 'o', 'g' }; +static const symbol s_16[] = { 'u' }; +static const symbol s_17[] = { 'e', 'n', 't' }; +static const symbol s_18[] = { 'a', 't' }; +static const symbol s_19[] = { 'e', 'u', 'x' }; +static const symbol s_20[] = { 'i' }; +static const symbol s_21[] = { 'a', 'b', 'l' }; +static const symbol s_22[] = { 'i', 'q', 'U' }; +static const symbol s_23[] = { 'a', 't' }; +static const symbol s_24[] = { 'i', 'c' }; +static const symbol s_25[] = { 'i', 'q', 'U' }; +static const symbol s_26[] = { 'e', 'a', 'u' }; +static const symbol s_27[] = { 'a', 'l' }; +static const symbol s_28[] = { 'o', 'u' }; +static const symbol s_29[] = { 'e', 'u', 'x' }; +static const symbol s_30[] = { 'a', 'n', 't' }; +static const symbol s_31[] = { 'e', 'n', 't' }; +static const symbol s_32[] = { 'H', 'i' }; +static const symbol s_33[] = { 'i' }; +static const symbol s_34[] = { 'e' }; +static const symbol s_35[] = { 'i' }; +static const symbol s_36[] = { 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 'c', 'o', 'l' }; -static const symbol s_0_1[3] = { 'p', 'a', 'r' }; -static const symbol s_0_2[3] = { 't', 'a', 'p' }; - -static const struct among a_0[3] = -{ -{ 3, s_0_0, -1, -1, 0}, -{ 3, s_0_1, -1, -1, 0}, -{ 3, s_0_2, -1, -1, 0} +static const symbol s_0_1[2] = { 'n', 'i' }; +static const symbol s_0_2[3] = { 'p', 'a', 'r' }; +static const symbol s_0_3[3] = { 't', 'a', 'p' }; +static const struct among a_0[4] = { +{ 3, s_0_0, 0, -1, 0}, +{ 2, s_0_1, 0, 1, 0}, +{ 3, s_0_2, 0, -1, 0}, +{ 3, s_0_3, 0, -1, 0} }; static const symbol s_1_1[1] = { 'H' }; @@ -51,16 +92,14 @@ static const symbol s_1_3[2] = { 'H', 'i' }; static const symbol s_1_4[1] = { 'I' }; static const symbol s_1_5[1] = { 'U' }; static const symbol s_1_6[1] = { 'Y' }; - -static const struct among a_1[7] = -{ -{ 0, 0, -1, 7, 0}, -{ 1, s_1_1, 0, 6, 0}, -{ 2, s_1_2, 1, 4, 0}, -{ 2, s_1_3, 1, 5, 0}, -{ 1, s_1_4, 0, 1, 0}, -{ 1, s_1_5, 0, 2, 0}, -{ 1, s_1_6, 0, 3, 0} +static const struct among a_1[7] = { +{ 0, 0, 0, 7, 0}, +{ 1, s_1_1, -1, 6, 0}, +{ 2, s_1_2, -1, 4, 0}, +{ 2, s_1_3, -2, 5, 0}, +{ 1, s_1_4, -4, 1, 0}, +{ 1, s_1_5, -5, 2, 0}, +{ 1, s_1_6, -6, 3, 0} }; static const symbol s_2_0[3] = { 'i', 'q', 'U' }; @@ -69,26 +108,22 @@ static const symbol s_2_2[3] = { 'I', 0xE8, 'r' }; static const symbol s_2_3[3] = { 'i', 0xE8, 'r' }; static const symbol s_2_4[3] = { 'e', 'u', 's' }; static const symbol s_2_5[2] = { 'i', 'v' }; - -static const struct among a_2[6] = -{ -{ 3, s_2_0, -1, 3, 0}, -{ 3, s_2_1, -1, 3, 0}, -{ 3, s_2_2, -1, 4, 0}, -{ 3, s_2_3, -1, 4, 0}, -{ 3, s_2_4, -1, 2, 0}, -{ 2, s_2_5, -1, 1, 0} +static const struct among a_2[6] = { +{ 3, s_2_0, 0, 3, 0}, +{ 3, s_2_1, 0, 3, 0}, +{ 3, s_2_2, 0, 4, 0}, +{ 3, s_2_3, 0, 4, 0}, +{ 3, s_2_4, 0, 2, 0}, +{ 2, s_2_5, 0, 1, 0} }; static const symbol s_3_0[2] = { 'i', 'c' }; static const symbol s_3_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_3_2[2] = { 'i', 'v' }; - -static const struct among a_3[3] = -{ -{ 2, s_3_0, -1, 2, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 2, s_3_2, -1, 3, 0} +static const struct among a_3[3] = { +{ 2, s_3_0, 0, 2, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 2, s_3_2, 0, 3, 0} }; static const symbol s_4_0[4] = { 'i', 'q', 'U', 'e' }; @@ -133,53 +168,53 @@ static const symbol s_4_38[6] = { 'e', 'm', 'm', 'e', 'n', 't' }; static const symbol s_4_39[3] = { 'a', 'u', 'x' }; static const symbol s_4_40[4] = { 'e', 'a', 'u', 'x' }; static const symbol s_4_41[3] = { 'e', 'u', 'x' }; -static const symbol s_4_42[3] = { 'i', 't', 0xE9 }; - -static const struct among a_4[43] = -{ -{ 4, s_4_0, -1, 1, 0}, -{ 6, s_4_1, -1, 2, 0}, -{ 4, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 5, 0}, -{ 5, s_4_4, -1, 3, 0}, -{ 4, s_4_5, -1, 1, 0}, -{ 4, s_4_6, -1, 1, 0}, -{ 4, s_4_7, -1, 11, 0}, -{ 4, s_4_8, -1, 1, 0}, -{ 3, s_4_9, -1, 8, 0}, -{ 2, s_4_10, -1, 8, 0}, -{ 5, s_4_11, -1, 4, 0}, -{ 5, s_4_12, -1, 2, 0}, -{ 5, s_4_13, -1, 4, 0}, -{ 5, s_4_14, -1, 2, 0}, -{ 5, s_4_15, -1, 1, 0}, -{ 7, s_4_16, -1, 2, 0}, -{ 5, s_4_17, -1, 1, 0}, -{ 5, s_4_18, -1, 5, 0}, -{ 6, s_4_19, -1, 3, 0}, -{ 5, s_4_20, -1, 1, 0}, -{ 5, s_4_21, -1, 1, 0}, -{ 5, s_4_22, -1, 11, 0}, -{ 5, s_4_23, -1, 1, 0}, -{ 4, s_4_24, -1, 8, 0}, -{ 3, s_4_25, -1, 8, 0}, -{ 6, s_4_26, -1, 4, 0}, -{ 6, s_4_27, -1, 2, 0}, -{ 6, s_4_28, -1, 4, 0}, -{ 6, s_4_29, -1, 2, 0}, -{ 5, s_4_30, -1, 15, 0}, -{ 6, s_4_31, 30, 6, 0}, -{ 9, s_4_32, 31, 12, 0}, -{ 4, s_4_33, -1, 7, 0}, -{ 4, s_4_34, -1, 15, 0}, -{ 5, s_4_35, 34, 6, 0}, -{ 8, s_4_36, 35, 12, 0}, -{ 6, s_4_37, 34, 13, 0}, -{ 6, s_4_38, 34, 14, 0}, -{ 3, s_4_39, -1, 10, 0}, -{ 4, s_4_40, 39, 9, 0}, -{ 3, s_4_41, -1, 1, 0}, -{ 3, s_4_42, -1, 7, 0} +static const symbol s_4_42[3] = { 'o', 'u', 'x' }; +static const symbol s_4_43[3] = { 'i', 't', 0xE9 }; +static const struct among a_4[44] = { +{ 4, s_4_0, 0, 1, 0}, +{ 6, s_4_1, 0, 2, 0}, +{ 4, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 5, 0}, +{ 5, s_4_4, 0, 3, 0}, +{ 4, s_4_5, 0, 1, 0}, +{ 4, s_4_6, 0, 1, 0}, +{ 4, s_4_7, 0, 12, 0}, +{ 4, s_4_8, 0, 1, 0}, +{ 3, s_4_9, 0, 8, 0}, +{ 2, s_4_10, 0, 8, 0}, +{ 5, s_4_11, 0, 4, 0}, +{ 5, s_4_12, 0, 2, 0}, +{ 5, s_4_13, 0, 4, 0}, +{ 5, s_4_14, 0, 2, 0}, +{ 5, s_4_15, 0, 1, 0}, +{ 7, s_4_16, 0, 2, 0}, +{ 5, s_4_17, 0, 1, 0}, +{ 5, s_4_18, 0, 5, 0}, +{ 6, s_4_19, 0, 3, 0}, +{ 5, s_4_20, 0, 1, 0}, +{ 5, s_4_21, 0, 1, 0}, +{ 5, s_4_22, 0, 12, 0}, +{ 5, s_4_23, 0, 1, 0}, +{ 4, s_4_24, 0, 8, 0}, +{ 3, s_4_25, 0, 8, 0}, +{ 6, s_4_26, 0, 4, 0}, +{ 6, s_4_27, 0, 2, 0}, +{ 6, s_4_28, 0, 4, 0}, +{ 6, s_4_29, 0, 2, 0}, +{ 5, s_4_30, 0, 16, 0}, +{ 6, s_4_31, -1, 6, 0}, +{ 9, s_4_32, -1, 13, 0}, +{ 4, s_4_33, 0, 7, 0}, +{ 4, s_4_34, 0, 16, 0}, +{ 5, s_4_35, -1, 6, 0}, +{ 8, s_4_36, -1, 13, 0}, +{ 6, s_4_37, -3, 14, 0}, +{ 6, s_4_38, -4, 15, 0}, +{ 3, s_4_39, 0, 10, 0}, +{ 4, s_4_40, -1, 9, 0}, +{ 3, s_4_41, 0, 1, 0}, +{ 3, s_4_42, 0, 11, 0}, +{ 3, s_4_43, 0, 7, 0} }; static const symbol s_5_0[3] = { 'i', 'r', 'a' }; @@ -217,423 +252,412 @@ static const symbol s_5_31[5] = { 'i', 'r', 'i', 'e', 'z' }; static const symbol s_5_32[6] = { 'i', 's', 's', 'i', 'e', 'z' }; static const symbol s_5_33[4] = { 'i', 'r', 'e', 'z' }; static const symbol s_5_34[5] = { 'i', 's', 's', 'e', 'z' }; - -static const struct among a_5[35] = -{ -{ 3, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 1, 0}, -{ 4, s_5_2, -1, 1, 0}, -{ 7, s_5_3, -1, 1, 0}, -{ 1, s_5_4, -1, 1, 0}, -{ 4, s_5_5, 4, 1, 0}, -{ 2, s_5_6, -1, 1, 0}, -{ 4, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 1, 0}, -{ 4, s_5_9, -1, 1, 0}, -{ 5, s_5_10, -1, 1, 0}, -{ 8, s_5_11, -1, 1, 0}, -{ 4, s_5_12, -1, 1, 0}, -{ 2, s_5_13, -1, 1, 0}, -{ 5, s_5_14, 13, 1, 0}, -{ 6, s_5_15, 13, 1, 0}, -{ 6, s_5_16, -1, 1, 0}, -{ 7, s_5_17, -1, 1, 0}, -{ 5, s_5_18, -1, 1, 0}, -{ 6, s_5_19, -1, 1, 0}, -{ 7, s_5_20, -1, 1, 0}, -{ 2, s_5_21, -1, 1, 0}, -{ 5, s_5_22, 21, 1, 0}, -{ 6, s_5_23, 21, 1, 0}, -{ 6, s_5_24, -1, 1, 0}, -{ 7, s_5_25, -1, 1, 0}, -{ 8, s_5_26, -1, 1, 0}, -{ 5, s_5_27, -1, 1, 0}, -{ 6, s_5_28, -1, 1, 0}, -{ 5, s_5_29, -1, 1, 0}, -{ 2, s_5_30, -1, 1, 0}, -{ 5, s_5_31, -1, 1, 0}, -{ 6, s_5_32, -1, 1, 0}, -{ 4, s_5_33, -1, 1, 0}, -{ 5, s_5_34, -1, 1, 0} +static const struct among a_5[35] = { +{ 3, s_5_0, 0, 1, 0}, +{ 2, s_5_1, 0, 1, 0}, +{ 4, s_5_2, 0, 1, 0}, +{ 7, s_5_3, 0, 1, 0}, +{ 1, s_5_4, 0, 1, 0}, +{ 4, s_5_5, -1, 1, 0}, +{ 2, s_5_6, 0, 1, 0}, +{ 4, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 1, 0}, +{ 4, s_5_9, 0, 1, 0}, +{ 5, s_5_10, 0, 1, 0}, +{ 8, s_5_11, 0, 1, 0}, +{ 4, s_5_12, 0, 1, 0}, +{ 2, s_5_13, 0, 1, 0}, +{ 5, s_5_14, -1, 1, 0}, +{ 6, s_5_15, -2, 1, 0}, +{ 6, s_5_16, 0, 1, 0}, +{ 7, s_5_17, 0, 1, 0}, +{ 5, s_5_18, 0, 1, 0}, +{ 6, s_5_19, 0, 1, 0}, +{ 7, s_5_20, 0, 1, 0}, +{ 2, s_5_21, 0, 1, 0}, +{ 5, s_5_22, -1, 1, 0}, +{ 6, s_5_23, -2, 1, 0}, +{ 6, s_5_24, 0, 1, 0}, +{ 7, s_5_25, 0, 1, 0}, +{ 8, s_5_26, 0, 1, 0}, +{ 5, s_5_27, 0, 1, 0}, +{ 6, s_5_28, 0, 1, 0}, +{ 5, s_5_29, 0, 1, 0}, +{ 2, s_5_30, 0, 1, 0}, +{ 5, s_5_31, 0, 1, 0}, +{ 6, s_5_32, 0, 1, 0}, +{ 4, s_5_33, 0, 1, 0}, +{ 5, s_5_34, 0, 1, 0} }; -static const symbol s_6_0[1] = { 'a' }; -static const symbol s_6_1[3] = { 'e', 'r', 'a' }; -static const symbol s_6_2[4] = { 'a', 's', 's', 'e' }; -static const symbol s_6_3[4] = { 'a', 'n', 't', 'e' }; -static const symbol s_6_4[2] = { 0xE9, 'e' }; -static const symbol s_6_5[2] = { 'a', 'i' }; -static const symbol s_6_6[4] = { 'e', 'r', 'a', 'i' }; -static const symbol s_6_7[2] = { 'e', 'r' }; -static const symbol s_6_8[2] = { 'a', 's' }; -static const symbol s_6_9[4] = { 'e', 'r', 'a', 's' }; -static const symbol s_6_10[4] = { 0xE2, 'm', 'e', 's' }; -static const symbol s_6_11[5] = { 'a', 's', 's', 'e', 's' }; -static const symbol s_6_12[5] = { 'a', 'n', 't', 'e', 's' }; -static const symbol s_6_13[4] = { 0xE2, 't', 'e', 's' }; -static const symbol s_6_14[3] = { 0xE9, 'e', 's' }; -static const symbol s_6_15[3] = { 'a', 'i', 's' }; -static const symbol s_6_16[5] = { 'e', 'r', 'a', 'i', 's' }; -static const symbol s_6_17[4] = { 'i', 'o', 'n', 's' }; -static const symbol s_6_18[6] = { 'e', 'r', 'i', 'o', 'n', 's' }; -static const symbol s_6_19[7] = { 'a', 's', 's', 'i', 'o', 'n', 's' }; -static const symbol s_6_20[5] = { 'e', 'r', 'o', 'n', 's' }; -static const symbol s_6_21[4] = { 'a', 'n', 't', 's' }; -static const symbol s_6_22[2] = { 0xE9, 's' }; -static const symbol s_6_23[3] = { 'a', 'i', 't' }; -static const symbol s_6_24[5] = { 'e', 'r', 'a', 'i', 't' }; -static const symbol s_6_25[3] = { 'a', 'n', 't' }; -static const symbol s_6_26[5] = { 'a', 'I', 'e', 'n', 't' }; -static const symbol s_6_27[7] = { 'e', 'r', 'a', 'I', 'e', 'n', 't' }; -static const symbol s_6_28[5] = { 0xE8, 'r', 'e', 'n', 't' }; -static const symbol s_6_29[6] = { 'a', 's', 's', 'e', 'n', 't' }; -static const symbol s_6_30[5] = { 'e', 'r', 'o', 'n', 't' }; -static const symbol s_6_31[2] = { 0xE2, 't' }; -static const symbol s_6_32[2] = { 'e', 'z' }; -static const symbol s_6_33[3] = { 'i', 'e', 'z' }; -static const symbol s_6_34[5] = { 'e', 'r', 'i', 'e', 'z' }; -static const symbol s_6_35[6] = { 'a', 's', 's', 'i', 'e', 'z' }; -static const symbol s_6_36[4] = { 'e', 'r', 'e', 'z' }; -static const symbol s_6_37[1] = { 0xE9 }; - -static const struct among a_6[38] = -{ -{ 1, s_6_0, -1, 3, 0}, -{ 3, s_6_1, 0, 2, 0}, -{ 4, s_6_2, -1, 3, 0}, -{ 4, s_6_3, -1, 3, 0}, -{ 2, s_6_4, -1, 2, 0}, -{ 2, s_6_5, -1, 3, 0}, -{ 4, s_6_6, 5, 2, 0}, -{ 2, s_6_7, -1, 2, 0}, -{ 2, s_6_8, -1, 3, 0}, -{ 4, s_6_9, 8, 2, 0}, -{ 4, s_6_10, -1, 3, 0}, -{ 5, s_6_11, -1, 3, 0}, -{ 5, s_6_12, -1, 3, 0}, -{ 4, s_6_13, -1, 3, 0}, -{ 3, s_6_14, -1, 2, 0}, -{ 3, s_6_15, -1, 3, 0}, -{ 5, s_6_16, 15, 2, 0}, -{ 4, s_6_17, -1, 1, 0}, -{ 6, s_6_18, 17, 2, 0}, -{ 7, s_6_19, 17, 3, 0}, -{ 5, s_6_20, -1, 2, 0}, -{ 4, s_6_21, -1, 3, 0}, -{ 2, s_6_22, -1, 2, 0}, -{ 3, s_6_23, -1, 3, 0}, -{ 5, s_6_24, 23, 2, 0}, -{ 3, s_6_25, -1, 3, 0}, -{ 5, s_6_26, -1, 3, 0}, -{ 7, s_6_27, 26, 2, 0}, -{ 5, s_6_28, -1, 2, 0}, -{ 6, s_6_29, -1, 3, 0}, -{ 5, s_6_30, -1, 2, 0}, -{ 2, s_6_31, -1, 3, 0}, -{ 2, s_6_32, -1, 2, 0}, -{ 3, s_6_33, 32, 2, 0}, -{ 5, s_6_34, 33, 2, 0}, -{ 6, s_6_35, 33, 3, 0}, -{ 4, s_6_36, 32, 2, 0}, -{ 1, s_6_37, -1, 2, 0} +static const symbol s_6_0[2] = { 'a', 'l' }; +static const symbol s_6_1[3] = { 0xE9, 'p', 'l' }; +static const symbol s_6_2[3] = { 'a', 'u', 'v' }; +static const struct among a_6[3] = { +{ 2, s_6_0, 0, 1, 0}, +{ 3, s_6_1, 0, -1, 0}, +{ 3, s_6_2, 0, -1, 0} }; -static const symbol s_7_0[1] = { 'e' }; -static const symbol s_7_1[4] = { 'I', 0xE8, 'r', 'e' }; -static const symbol s_7_2[4] = { 'i', 0xE8, 'r', 'e' }; -static const symbol s_7_3[3] = { 'i', 'o', 'n' }; -static const symbol s_7_4[3] = { 'I', 'e', 'r' }; -static const symbol s_7_5[3] = { 'i', 'e', 'r' }; - -static const struct among a_7[6] = -{ -{ 1, s_7_0, -1, 3, 0}, -{ 4, s_7_1, 0, 2, 0}, -{ 4, s_7_2, 0, 2, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 3, s_7_4, -1, 2, 0}, -{ 3, s_7_5, -1, 2, 0} +static const symbol s_7_0[1] = { 'a' }; +static const symbol s_7_1[3] = { 'e', 'r', 'a' }; +static const symbol s_7_2[4] = { 'a', 'i', 's', 'e' }; +static const symbol s_7_3[4] = { 'a', 's', 's', 'e' }; +static const symbol s_7_4[4] = { 'a', 'n', 't', 'e' }; +static const symbol s_7_5[2] = { 0xE9, 'e' }; +static const symbol s_7_6[2] = { 'a', 'i' }; +static const symbol s_7_7[4] = { 'e', 'r', 'a', 'i' }; +static const symbol s_7_8[2] = { 'e', 'r' }; +static const symbol s_7_9[2] = { 'a', 's' }; +static const symbol s_7_10[4] = { 'e', 'r', 'a', 's' }; +static const symbol s_7_11[4] = { 0xE2, 'm', 'e', 's' }; +static const symbol s_7_12[5] = { 'a', 'i', 's', 'e', 's' }; +static const symbol s_7_13[5] = { 'a', 's', 's', 'e', 's' }; +static const symbol s_7_14[5] = { 'a', 'n', 't', 'e', 's' }; +static const symbol s_7_15[4] = { 0xE2, 't', 'e', 's' }; +static const symbol s_7_16[3] = { 0xE9, 'e', 's' }; +static const symbol s_7_17[3] = { 'a', 'i', 's' }; +static const symbol s_7_18[4] = { 'e', 'a', 'i', 's' }; +static const symbol s_7_19[5] = { 'e', 'r', 'a', 'i', 's' }; +static const symbol s_7_20[4] = { 'i', 'o', 'n', 's' }; +static const symbol s_7_21[6] = { 'e', 'r', 'i', 'o', 'n', 's' }; +static const symbol s_7_22[7] = { 'a', 's', 's', 'i', 'o', 'n', 's' }; +static const symbol s_7_23[5] = { 'e', 'r', 'o', 'n', 's' }; +static const symbol s_7_24[4] = { 'a', 'n', 't', 's' }; +static const symbol s_7_25[2] = { 0xE9, 's' }; +static const symbol s_7_26[3] = { 'a', 'i', 't' }; +static const symbol s_7_27[5] = { 'e', 'r', 'a', 'i', 't' }; +static const symbol s_7_28[3] = { 'a', 'n', 't' }; +static const symbol s_7_29[5] = { 'a', 'I', 'e', 'n', 't' }; +static const symbol s_7_30[7] = { 'e', 'r', 'a', 'I', 'e', 'n', 't' }; +static const symbol s_7_31[5] = { 0xE8, 'r', 'e', 'n', 't' }; +static const symbol s_7_32[6] = { 'a', 's', 's', 'e', 'n', 't' }; +static const symbol s_7_33[5] = { 'e', 'r', 'o', 'n', 't' }; +static const symbol s_7_34[2] = { 0xE2, 't' }; +static const symbol s_7_35[2] = { 'e', 'z' }; +static const symbol s_7_36[3] = { 'i', 'e', 'z' }; +static const symbol s_7_37[5] = { 'e', 'r', 'i', 'e', 'z' }; +static const symbol s_7_38[6] = { 'a', 's', 's', 'i', 'e', 'z' }; +static const symbol s_7_39[4] = { 'e', 'r', 'e', 'z' }; +static const symbol s_7_40[1] = { 0xE9 }; +static const struct among a_7[41] = { +{ 1, s_7_0, 0, 3, 0}, +{ 3, s_7_1, -1, 2, 0}, +{ 4, s_7_2, 0, 4, 0}, +{ 4, s_7_3, 0, 3, 0}, +{ 4, s_7_4, 0, 3, 0}, +{ 2, s_7_5, 0, 2, 0}, +{ 2, s_7_6, 0, 3, 0}, +{ 4, s_7_7, -1, 2, 0}, +{ 2, s_7_8, 0, 2, 0}, +{ 2, s_7_9, 0, 3, 0}, +{ 4, s_7_10, -1, 2, 0}, +{ 4, s_7_11, 0, 3, 0}, +{ 5, s_7_12, 0, 4, 0}, +{ 5, s_7_13, 0, 3, 0}, +{ 5, s_7_14, 0, 3, 0}, +{ 4, s_7_15, 0, 3, 0}, +{ 3, s_7_16, 0, 2, 0}, +{ 3, s_7_17, 0, 4, 0}, +{ 4, s_7_18, -1, 2, 0}, +{ 5, s_7_19, -2, 2, 0}, +{ 4, s_7_20, 0, 1, 0}, +{ 6, s_7_21, -1, 2, 0}, +{ 7, s_7_22, -2, 3, 0}, +{ 5, s_7_23, 0, 2, 0}, +{ 4, s_7_24, 0, 3, 0}, +{ 2, s_7_25, 0, 2, 0}, +{ 3, s_7_26, 0, 3, 0}, +{ 5, s_7_27, -1, 2, 0}, +{ 3, s_7_28, 0, 3, 0}, +{ 5, s_7_29, 0, 3, 0}, +{ 7, s_7_30, -1, 2, 0}, +{ 5, s_7_31, 0, 2, 0}, +{ 6, s_7_32, 0, 3, 0}, +{ 5, s_7_33, 0, 2, 0}, +{ 2, s_7_34, 0, 3, 0}, +{ 2, s_7_35, 0, 2, 0}, +{ 3, s_7_36, -1, 2, 0}, +{ 5, s_7_37, -1, 2, 0}, +{ 6, s_7_38, -2, 3, 0}, +{ 4, s_7_39, -4, 2, 0}, +{ 1, s_7_40, 0, 2, 0} }; -static const symbol s_8_0[3] = { 'e', 'l', 'l' }; -static const symbol s_8_1[4] = { 'e', 'i', 'l', 'l' }; -static const symbol s_8_2[3] = { 'e', 'n', 'n' }; -static const symbol s_8_3[3] = { 'o', 'n', 'n' }; -static const symbol s_8_4[3] = { 'e', 't', 't' }; +static const symbol s_8_0[1] = { 'e' }; +static const symbol s_8_1[4] = { 'I', 0xE8, 'r', 'e' }; +static const symbol s_8_2[4] = { 'i', 0xE8, 'r', 'e' }; +static const symbol s_8_3[3] = { 'i', 'o', 'n' }; +static const symbol s_8_4[3] = { 'I', 'e', 'r' }; +static const symbol s_8_5[3] = { 'i', 'e', 'r' }; +static const struct among a_8[6] = { +{ 1, s_8_0, 0, 3, 0}, +{ 4, s_8_1, -1, 2, 0}, +{ 4, s_8_2, -2, 2, 0}, +{ 3, s_8_3, 0, 1, 0}, +{ 3, s_8_4, 0, 2, 0}, +{ 3, s_8_5, 0, 2, 0} +}; -static const struct among a_8[5] = -{ -{ 3, s_8_0, -1, -1, 0}, -{ 4, s_8_1, -1, -1, 0}, -{ 3, s_8_2, -1, -1, 0}, -{ 3, s_8_3, -1, -1, 0}, -{ 3, s_8_4, -1, -1, 0} +static const symbol s_9_0[3] = { 'e', 'l', 'l' }; +static const symbol s_9_1[4] = { 'e', 'i', 'l', 'l' }; +static const symbol s_9_2[3] = { 'e', 'n', 'n' }; +static const symbol s_9_3[3] = { 'o', 'n', 'n' }; +static const symbol s_9_4[3] = { 'e', 't', 't' }; +static const struct among a_9[5] = { +{ 3, s_9_0, 0, -1, 0}, +{ 4, s_9_1, 0, -1, 0}, +{ 3, s_9_2, 0, -1, 0}, +{ 3, s_9_3, 0, -1, 0}, +{ 3, s_9_4, 0, -1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 130, 103, 8, 5 }; +static const unsigned char g_oux_ending[] = { 65, 85 }; + static const unsigned char g_elision_char[] = { 131, 14, 3 }; static const unsigned char g_keep_with_s[] = { 1, 65, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; -static const symbol s_0[] = { 'q', 'u' }; -static const symbol s_1[] = { 'U' }; -static const symbol s_2[] = { 'I' }; -static const symbol s_3[] = { 'Y' }; -static const symbol s_4[] = { 'H', 'e' }; -static const symbol s_5[] = { 'H', 'i' }; -static const symbol s_6[] = { 'Y' }; -static const symbol s_7[] = { 'U' }; -static const symbol s_8[] = { 'i' }; -static const symbol s_9[] = { 'u' }; -static const symbol s_10[] = { 'y' }; -static const symbol s_11[] = { 0xEB }; -static const symbol s_12[] = { 0xEF }; -static const symbol s_13[] = { 'i', 'c' }; -static const symbol s_14[] = { 'i', 'q', 'U' }; -static const symbol s_15[] = { 'l', 'o', 'g' }; -static const symbol s_16[] = { 'u' }; -static const symbol s_17[] = { 'e', 'n', 't' }; -static const symbol s_18[] = { 'a', 't' }; -static const symbol s_19[] = { 'e', 'u', 'x' }; -static const symbol s_20[] = { 'i' }; -static const symbol s_21[] = { 'a', 'b', 'l' }; -static const symbol s_22[] = { 'i', 'q', 'U' }; -static const symbol s_23[] = { 'a', 't' }; -static const symbol s_24[] = { 'i', 'c' }; -static const symbol s_25[] = { 'i', 'q', 'U' }; -static const symbol s_26[] = { 'e', 'a', 'u' }; -static const symbol s_27[] = { 'a', 'l' }; -static const symbol s_28[] = { 'e', 'u', 'x' }; -static const symbol s_29[] = { 'a', 'n', 't' }; -static const symbol s_30[] = { 'e', 'n', 't' }; -static const symbol s_31[] = { 'H', 'i' }; -static const symbol s_32[] = { 'i' }; -static const symbol s_33[] = { 'e' }; -static const symbol s_34[] = { 'i' }; -static const symbol s_35[] = { 'c' }; - static int r_elisions(struct SN_env * z) { z->bra = z->c; - { int c1 = z->c; - if (in_grouping(z, g_elision_char, 99, 116, 0)) goto lab1; - goto lab0; - lab1: - z->c = c1; + do { + int v_1 = z->c; + if (in_grouping(z, g_elision_char, 99, 116, 0)) goto lab0; + break; + lab0: + z->c = v_1; if (!(eq_s(z, 2, s_0))) return 0; - } -lab0: + } while (0); if (z->c == z->l || z->p[z->c] != '\'') return 0; z->c++; z->ket = z->c; - - if (z->c < z->l) goto lab2; + if (z->c < z->l) goto lab1; return 0; -lab2: - { int ret = slice_del(z); +lab1: + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_prelude(struct SN_env * z) { - while(1) { - int c1 = z->c; - while(1) { - int c2 = z->c; - { int c3 = z->c; - if (in_grouping(z, g_v, 97, 251, 0)) goto lab3; + while (1) { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + do { + int v_3 = z->c; + if (in_grouping(z, g_v, 97, 251, 0)) goto lab2; z->bra = z->c; - { int c4 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab5; + do { + int v_4 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab3; z->c++; z->ket = z->c; - if (in_grouping(z, g_v, 97, 251, 0)) goto lab5; - { int ret = slice_from_s(z, 1, s_1); + if (in_grouping(z, g_v, 97, 251, 0)) goto lab3; + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = c4; - if (z->c == z->l || z->p[z->c] != 'i') goto lab6; + break; + lab3: + z->c = v_4; + if (z->c == z->l || z->p[z->c] != 'i') goto lab4; z->c++; z->ket = z->c; - if (in_grouping(z, g_v, 97, 251, 0)) goto lab6; - { int ret = slice_from_s(z, 1, s_2); + if (in_grouping(z, g_v, 97, 251, 0)) goto lab4; + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } - goto lab4; - lab6: - z->c = c4; - if (z->c == z->l || z->p[z->c] != 'y') goto lab3; + break; + lab4: + z->c = v_4; + if (z->c == z->l || z->p[z->c] != 'y') goto lab2; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } - } - lab4: - goto lab2; - lab3: - z->c = c3; + } while (0); + break; + lab2: + z->c = v_3; z->bra = z->c; - if (z->c == z->l || z->p[z->c] != 0xEB) goto lab7; + if (z->c == z->l || z->p[z->c] != 0xEB) goto lab5; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 2, s_4); + { + int ret = slice_from_s(z, 2, s_4); if (ret < 0) return ret; } - goto lab2; - lab7: - z->c = c3; + break; + lab5: + z->c = v_3; z->bra = z->c; - if (z->c == z->l || z->p[z->c] != 0xEF) goto lab8; + if (z->c == z->l || z->p[z->c] != 0xEF) goto lab6; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } - goto lab2; - lab8: - z->c = c3; + break; + lab6: + z->c = v_3; z->bra = z->c; - if (z->c == z->l || z->p[z->c] != 'y') goto lab9; + if (z->c == z->l || z->p[z->c] != 'y') goto lab7; z->c++; z->ket = z->c; - if (in_grouping(z, g_v, 97, 251, 0)) goto lab9; - { int ret = slice_from_s(z, 1, s_6); + if (in_grouping(z, g_v, 97, 251, 0)) goto lab7; + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } - goto lab2; - lab9: - z->c = c3; + break; + lab7: + z->c = v_3; if (z->c == z->l || z->p[z->c] != 'q') goto lab1; z->c++; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'u') goto lab1; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } - } - lab2: - z->c = c2; + } while (0); + z->c = v_2; break; lab1: - z->c = c2; + z->c = v_2; if (z->c >= z->l) goto lab0; z->c++; } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping(z, g_v, 97, 251, 0)) goto lab2; - if (in_grouping(z, g_v, 97, 251, 0)) goto lab2; - if (z->c >= z->l) goto lab2; + int among_var; + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping(z, g_v, 97, 251, 0)) goto lab1; + if (in_grouping(z, g_v, 97, 251, 0)) goto lab1; + if (z->c >= z->l) goto lab1; z->c++; - goto lab1; + break; + lab1: + z->c = v_2; + if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 3 || !((33282 >> (z->p[z->c + 1] & 0x1f)) & 1)) goto lab2; + among_var = find_among(z, a_0, 4, 0); + if (!among_var) goto lab2; + switch (among_var) { + case 1: + if (in_grouping(z, g_v, 97, 251, 0)) goto lab2; + break; + } + break; lab2: - z->c = c2; - if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((331776 >> (z->p[z->c + 2] & 0x1f)) & 1)) goto lab3; - if (!find_among(z, a_0, 3)) goto lab3; - goto lab1; - lab3: - z->c = c2; + z->c = v_2; if (z->c >= z->l) goto lab0; z->c++; - { int ret = out_grouping(z, g_v, 97, 251, 1); if (ret < 0) goto lab0; z->c += ret; } - } - lab1: - z->I[2] = z->c; + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c3 = z->c; - + { + int v_3 = z->c; { int ret = out_grouping(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - z->I[0] = z->c; - lab4: - z->c = c3; + ((SN_local *)z)->i_p2 = z->c; + lab3: + z->c = v_3; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || z->p[z->c + 0] >> 5 != 2 || !((35652352 >> (z->p[z->c + 0] & 0x1f)) & 1)) among_var = 7; else - among_var = find_among(z, a_1, 7); + among_var = find_among(z, a_1, 7, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 6: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -644,346 +668,417 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_4, 43); + among_var = find_among_b(z, a_4, 44, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_13))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_13))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int m2 = z->l - z->c; (void)m2; - { int ret = r_R2(z); - if (ret == 0) goto lab2; + do { + int v_2 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; - { int ret = slice_from_s(z, 3, s_14); + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_from_s(z, 3, s_14); if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: ; } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_15); + { + int ret = slice_from_s(z, 3, s_15); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_16); + { + int ret = slice_from_s(z, 1, s_16); if (ret < 0) return ret; } break; case 5: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_17); + { + int ret = slice_from_s(z, 3, s_17); if (ret < 0) return ret; } break; case 6: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_2, 6); - if (!among_var) { z->c = z->l - m3; goto lab3; } + among_var = find_among_b(z, a_2, 6, 0); + if (!among_var) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (!(eq_s_b(z, 2, s_18))) { z->c = z->l - m3; goto lab3; } + if (!(eq_s_b(z, 2, s_18))) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m4 = z->l - z->c; (void)m4; - { int ret = r_R2(z); - if (ret == 0) goto lab5; + do { + int v_4 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m4; - { int ret = r_R1(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + break; + lab3: + z->c = z->l - v_4; + { + int ret = r_R1(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_from_s(z, 3, s_19); + { + int ret = slice_from_s(z, 3, s_19); if (ret < 0) return ret; } - } - lab4: + } while (0); break; case 3: - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 4: - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_from_s(z, 1, s_20); + { + int ret = slice_from_s(z, 1, s_20); if (ret < 0) return ret; } break; } - lab3: + lab2: ; } break; case 7: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m5; goto lab6; } - among_var = find_among_b(z, a_3, 3); - if (!among_var) { z->c = z->l - m5; goto lab6; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_5; goto lab4; } + among_var = find_among_b(z, a_3, 3, 0); + if (!among_var) { z->c = z->l - v_5; goto lab4; } z->bra = z->c; switch (among_var) { case 1: - { int m6 = z->l - z->c; (void)m6; - { int ret = r_R2(z); - if (ret == 0) goto lab8; + do { + int v_6 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m6; - { int ret = slice_from_s(z, 3, s_21); + break; + lab5: + z->c = z->l - v_6; + { + int ret = slice_from_s(z, 3, s_21); if (ret < 0) return ret; } - } - lab7: + } while (0); break; case 2: - { int m7 = z->l - z->c; (void)m7; - { int ret = r_R2(z); - if (ret == 0) goto lab10; + do { + int v_7 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab9; - lab10: - z->c = z->l - m7; - { int ret = slice_from_s(z, 3, s_22); + break; + lab6: + z->c = z->l - v_7; + { + int ret = slice_from_s(z, 3, s_22); if (ret < 0) return ret; } - } - lab9: + } while (0); break; case 3: - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m5; goto lab6; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_5; goto lab4; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - lab6: + lab4: ; } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; + { + int v_8 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_23))) { z->c = z->l - m8; goto lab11; } + if (!(eq_s_b(z, 2, s_23))) { z->c = z->l - v_8; goto lab7; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m8; goto lab11; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_8; goto lab7; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (!(eq_s_b(z, 2, s_24))) { z->c = z->l - m8; goto lab11; } + if (!(eq_s_b(z, 2, s_24))) { z->c = z->l - v_8; goto lab7; } z->bra = z->c; - { int m9 = z->l - z->c; (void)m9; - { int ret = r_R2(z); - if (ret == 0) goto lab13; + do { + int v_9 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab8; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab12; - lab13: - z->c = z->l - m9; - { int ret = slice_from_s(z, 3, s_25); + break; + lab8: + z->c = z->l - v_9; + { + int ret = slice_from_s(z, 3, s_25); if (ret < 0) return ret; } - } - lab12: - lab11: + } while (0); + lab7: ; } break; case 9: - { int ret = slice_from_s(z, 3, s_26); + { + int ret = slice_from_s(z, 3, s_26); if (ret < 0) return ret; } break; case 10: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_27); + { + int ret = slice_from_s(z, 2, s_27); if (ret < 0) return ret; } break; case 11: - { int m10 = z->l - z->c; (void)m10; - { int ret = r_R2(z); - if (ret == 0) goto lab15; + if (in_grouping_b(z, g_oux_ending, 98, 112, 0)) return 0; + { + int ret = slice_from_s(z, 2, s_28); + if (ret < 0) return ret; + } + break; + case 12: + do { + int v_10 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab14; - lab15: - z->c = z->l - m10; - { int ret = r_R1(z); + break; + lab9: + z->c = z->l - v_10; + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_28); + { + int ret = slice_from_s(z, 3, s_29); if (ret < 0) return ret; } - } - lab14: + } while (0); break; - case 12: - { int ret = r_R1(z); + case 13: + { + int ret = r_R1(z); if (ret <= 0) return ret; } if (out_grouping_b(z, g_v, 97, 251, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 13: - { int ret = r_RV(z); + case 14: + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_29); + { + int ret = slice_from_s(z, 3, s_30); if (ret < 0) return ret; } return 0; break; - case 14: - { int ret = r_RV(z); + case 15: + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_30); + { + int ret = slice_from_s(z, 3, s_31); if (ret < 0) return ret; } return 0; break; - case 15: - { int m_test11 = z->l - z->c; + case 16: + { + int v_11 = z->l - z->c; if (in_grouping_b(z, g_v, 97, 251, 0)) return 0; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - z->c = z->l - m_test11; + z->c = z->l - v_11; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 0; @@ -993,294 +1088,362 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_i_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68944418 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_5, 35)) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68944418 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_5, 35, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'H') goto lab0; z->c--; - { z->lb = mlimit1; return 0; } + { z->lb = v_1; return 0; } lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - if (out_grouping_b(z, g_v, 97, 251, 0)) { z->lb = mlimit1; return 0; } - { int ret = slice_del(z); + if (out_grouping_b(z, g_v, 97, 251, 0)) { z->lb = v_1; return 0; } + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_verb_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - among_var = find_among_b(z, a_6, 38); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_7, 41, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - switch (among_var) { - case 1: - { int ret = r_R2(z); - if (ret == 0) { z->lb = mlimit1; return 0; } - if (ret < 0) return ret; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - break; - case 2: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = slice_del(z); + z->lb = v_1; + } + switch (among_var) { + case 1: + { + int ret = r_R2(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') { z->c = z->l - v_2; goto lab0; } + z->c--; + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_2; goto lab0; } if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; - z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') { z->c = z->l - m2; goto lab0; } - z->c--; - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; - } - lab0: - ; + z->bra = z->c; + lab0: + ; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 4: + { + int v_3 = z->l - z->c; + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 108 && z->p[z->c - 1] != 118)) goto lab1; + among_var = find_among_b(z, a_6, 3, 0); + if (!among_var) goto lab1; + switch (among_var) { + case 1: + if (z->c <= z->lb) goto lab1; + z->c--; + if (z->c > z->lb) goto lab1; + break; } - break; - } - z->lb = mlimit1; + return 0; + lab1: + z->c = z->l - v_3; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; } return 1; } static int r_residual_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - v_1; goto lab0; } z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - { int m3 = z->l - z->c; (void)m3; - if (!(eq_s_b(z, 2, s_31))) goto lab2; - goto lab1; - lab2: - z->c = z->l - m3; - if (out_grouping_b(z, g_keep_with_s, 97, 232, 0)) { z->c = z->l - m1; goto lab0; } - } - lab1: - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 2, s_32))) goto lab1; + break; + lab1: + z->c = z->l - v_3; + if (out_grouping_b(z, g_keep_with_s, 97, 232, 0)) { z->c = z->l - v_1; goto lab0; } + } while (0); + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: ; } - - { int mlimit4; - if (z->c < z->I[2]) return 0; - mlimit4 = z->lb; z->lb = z->I[2]; + { + int v_4; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_4 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((278560 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit4; return 0; } - among_var = find_among_b(z, a_7, 6); - if (!among_var) { z->lb = mlimit4; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((278560 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_4; return 0; } + among_var = find_among_b(z, a_8, 6, 0); + if (!among_var) { z->lb = v_4; return 0; } z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); - if (ret == 0) { z->lb = mlimit4; return 0; } + { + int ret = r_R2(z); + if (ret == 0) { z->lb = v_4; return 0; } if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab4; + do { + int v_5 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab2; z->c--; - goto lab3; - lab4: - z->c = z->l - m5; - if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = mlimit4; return 0; } + break; + lab2: + z->c = z->l - v_5; + if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = v_4; return 0; } z->c--; - } - lab3: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_32); + { + int ret = slice_from_s(z, 1, s_33); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - z->lb = mlimit4; + z->lb = v_4; } return 1; } static int r_un_double(struct SN_env * z) { - { int m_test1 = z->l - z->c; + { + int v_1 = z->l - z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1069056 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_8, 5)) return 0; - z->c = z->l - m_test1; + if (!find_among_b(z, a_9, 5, 0)) return 0; + z->c = z->l - v_1; } z->ket = z->c; if (z->c <= z->lb) return 0; z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_un_accent(struct SN_env * z) { - { int i = 1; - while(1) { + { + int v_1 = 1; + while (1) { if (out_grouping_b(z, g_v, 97, 251, 0)) goto lab0; - i--; + v_1--; continue; lab0: break; } - if (i > 0) return 0; + if (v_1 > 0) return 0; } z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 0xE9) goto lab2; + do { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 0xE9) goto lab1; z->c--; - goto lab1; - lab2: - z->c = z->l - m1; + break; + lab1: + z->c = z->l - v_2; if (z->c <= z->lb || z->p[z->c - 1] != 0xE8) return 0; z->c--; - } -lab1: + } while (0); z->bra = z->c; - { int ret = slice_from_s(z, 1, s_33); + { + int ret = slice_from_s(z, 1, s_34); if (ret < 0) return ret; } return 1; } extern int french_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_elisions(z); + { + int v_1 = z->c; + { + int ret = r_elisions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - { int ret = r_prelude(z); + { + int v_2 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c2; + z->c = v_2; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int m6 = z->l - z->c; (void)m6; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab4; + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; + { + int v_5 = z->l - z->c; + do { + int v_6 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab3; - lab4: - z->c = z->l - m6; - { int ret = r_i_verb_suffix(z); - if (ret == 0) goto lab5; + break; + lab2: + z->c = z->l - v_6; + { + int ret = r_i_verb_suffix(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - goto lab3; - lab5: - z->c = z->l - m6; - { int ret = r_verb_suffix(z); - if (ret == 0) goto lab2; + break; + lab3: + z->c = z->l - v_6; + { + int ret = r_verb_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - } - lab3: - z->c = z->l - m5; - { int m7 = z->l - z->c; (void)m7; + } while (0); + z->c = z->l - v_5; + { + int v_7 = z->l - z->c; z->ket = z->c; - { int m8 = z->l - z->c; (void)m8; - if (z->c <= z->lb || z->p[z->c - 1] != 'Y') goto lab8; + do { + int v_8 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'Y') goto lab5; z->c--; z->bra = z->c; - { int ret = slice_from_s(z, 1, s_34); + { + int ret = slice_from_s(z, 1, s_35); if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m8; - if (z->c <= z->lb || z->p[z->c - 1] != 0xE7) { z->c = z->l - m7; goto lab6; } + break; + lab5: + z->c = z->l - v_8; + if (z->c <= z->lb || z->p[z->c - 1] != 0xE7) { z->c = z->l - v_7; goto lab4; } z->c--; z->bra = z->c; - { int ret = slice_from_s(z, 1, s_35); + { + int ret = slice_from_s(z, 1, s_36); if (ret < 0) return ret; } - } - lab7: - lab6: + } while (0); + lab4: ; } } - goto lab1; - lab2: - z->c = z->l - m4; - { int ret = r_residual_suffix(z); + break; + lab1: + z->c = z->l - v_4; + { + int ret = r_residual_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_un_double(z); + { + int v_9 = z->l - z->c; + { + int ret = r_un_double(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_un_accent(z); + { + int v_10 = z->l - z->c; + { + int ret = r_un_accent(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } z->c = z->lb; - { int c11 = z->c; - { int ret = r_postlude(z); + { + int v_11 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c11; + z->c = v_11; } return 1; } -extern struct SN_env * french_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * french_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void french_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void french_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_german.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_german.c index c053b201d5503..86c85c32ae627 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_german.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_german.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from german.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_german.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,38 +21,43 @@ extern int german_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_standard_suffix(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * german_ISO_8859_1_create_env(void); -extern void german_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'U' }; +static const symbol s_1[] = { 'Y' }; +static const symbol s_2[] = { 's', 's' }; +static const symbol s_3[] = { 0xE4 }; +static const symbol s_4[] = { 0xF6 }; +static const symbol s_5[] = { 0xFC }; +static const symbol s_6[] = { 'y' }; +static const symbol s_7[] = { 'u' }; +static const symbol s_8[] = { 'a' }; +static const symbol s_9[] = { 'o' }; +static const symbol s_10[] = { 's', 'y', 's', 't' }; +static const symbol s_11[] = { 'n', 'i', 's' }; +static const symbol s_12[] = { 'l' }; +static const symbol s_13[] = { 'i', 'g' }; +static const symbol s_14[] = { 'e', 'r' }; +static const symbol s_15[] = { 'e', 'n' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 'a', 'e' }; static const symbol s_0_2[2] = { 'o', 'e' }; static const symbol s_0_3[2] = { 'q', 'u' }; static const symbol s_0_4[2] = { 'u', 'e' }; static const symbol s_0_5[1] = { 0xDF }; - -static const struct among a_0[6] = -{ -{ 0, 0, -1, 5, 0}, -{ 2, s_0_1, 0, 2, 0}, -{ 2, s_0_2, 0, 3, 0}, -{ 2, s_0_3, 0, -1, 0}, -{ 2, s_0_4, 0, 4, 0}, -{ 1, s_0_5, 0, 1, 0} +static const struct among a_0[6] = { +{ 0, 0, 0, 5, 0}, +{ 2, s_0_1, -1, 2, 0}, +{ 2, s_0_2, -2, 3, 0}, +{ 2, s_0_3, -3, -1, 0}, +{ 2, s_0_4, -4, 4, 0}, +{ 1, s_0_5, -5, 1, 0} }; static const symbol s_1_1[1] = { 'U' }; @@ -48,15 +65,13 @@ static const symbol s_1_2[1] = { 'Y' }; static const symbol s_1_3[1] = { 0xE4 }; static const symbol s_1_4[1] = { 0xF6 }; static const symbol s_1_5[1] = { 0xFC }; - -static const struct among a_1[6] = -{ -{ 0, 0, -1, 5, 0}, -{ 1, s_1_1, 0, 2, 0}, -{ 1, s_1_2, 0, 1, 0}, -{ 1, s_1_3, 0, 3, 0}, -{ 1, s_1_4, 0, 4, 0}, -{ 1, s_1_5, 0, 2, 0} +static const struct among a_1[6] = { +{ 0, 0, 0, 5, 0}, +{ 1, s_1_1, -1, 2, 0}, +{ 1, s_1_2, -2, 1, 0}, +{ 1, s_1_3, -3, 3, 0}, +{ 1, s_1_4, -4, 4, 0}, +{ 1, s_1_5, -5, 2, 0} }; static const symbol s_2_0[1] = { 'e' }; @@ -70,237 +85,237 @@ static const symbol s_2_7[2] = { 'e', 'r' }; static const symbol s_2_8[1] = { 's' }; static const symbol s_2_9[2] = { 'e', 's' }; static const symbol s_2_10[3] = { 'l', 'n', 's' }; - -static const struct among a_2[11] = -{ -{ 1, s_2_0, -1, 3, 0}, -{ 2, s_2_1, -1, 1, 0}, -{ 2, s_2_2, -1, 3, 0}, -{ 7, s_2_3, 2, 2, 0}, -{ 4, s_2_4, -1, 2, 0}, -{ 2, s_2_5, -1, 5, 0}, -{ 3, s_2_6, -1, 2, 0}, -{ 2, s_2_7, -1, 2, 0}, -{ 1, s_2_8, -1, 4, 0}, -{ 2, s_2_9, 8, 3, 0}, -{ 3, s_2_10, 8, 5, 0} +static const struct among a_2[11] = { +{ 1, s_2_0, 0, 3, 0}, +{ 2, s_2_1, 0, 1, 0}, +{ 2, s_2_2, 0, 3, 0}, +{ 7, s_2_3, -1, 2, 0}, +{ 4, s_2_4, 0, 2, 0}, +{ 2, s_2_5, 0, 5, 0}, +{ 3, s_2_6, 0, 2, 0}, +{ 2, s_2_7, 0, 2, 0}, +{ 1, s_2_8, 0, 4, 0}, +{ 2, s_2_9, -1, 3, 0}, +{ 3, s_2_10, -2, 5, 0} }; -static const symbol s_3_0[2] = { 'e', 'n' }; -static const symbol s_3_1[2] = { 'e', 'r' }; -static const symbol s_3_2[2] = { 's', 't' }; -static const symbol s_3_3[3] = { 'e', 's', 't' }; - -static const struct among a_3[4] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 1, 0}, -{ 2, s_3_2, -1, 2, 0}, -{ 3, s_3_3, 2, 1, 0} +static const symbol s_3_0[4] = { 't', 'i', 'c', 'k' }; +static const symbol s_3_1[4] = { 'p', 'l', 'a', 'n' }; +static const symbol s_3_2[6] = { 'g', 'e', 'o', 'r', 'd', 'n' }; +static const symbol s_3_3[6] = { 'i', 'n', 't', 'e', 'r', 'n' }; +static const symbol s_3_4[2] = { 't', 'r' }; +static const struct among a_3[5] = { +{ 4, s_3_0, 0, -1, 0}, +{ 4, s_3_1, 0, -1, 0}, +{ 6, s_3_2, 0, -1, 0}, +{ 6, s_3_3, 0, -1, 0}, +{ 2, s_3_4, 0, -1, 0} }; -static const symbol s_4_0[2] = { 'i', 'g' }; -static const symbol s_4_1[4] = { 'l', 'i', 'c', 'h' }; - -static const struct among a_4[2] = -{ -{ 2, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 1, 0} +static const symbol s_4_0[2] = { 'e', 'n' }; +static const symbol s_4_1[2] = { 'e', 'r' }; +static const symbol s_4_2[2] = { 'e', 't' }; +static const symbol s_4_3[2] = { 's', 't' }; +static const symbol s_4_4[3] = { 'e', 's', 't' }; +static const struct among a_4[5] = { +{ 2, s_4_0, 0, 1, 0}, +{ 2, s_4_1, 0, 1, 0}, +{ 2, s_4_2, 0, 3, 0}, +{ 2, s_4_3, 0, 2, 0}, +{ 3, s_4_4, -1, 1, 0} }; -static const symbol s_5_0[3] = { 'e', 'n', 'd' }; -static const symbol s_5_1[2] = { 'i', 'g' }; -static const symbol s_5_2[3] = { 'u', 'n', 'g' }; -static const symbol s_5_3[4] = { 'l', 'i', 'c', 'h' }; -static const symbol s_5_4[4] = { 'i', 's', 'c', 'h' }; -static const symbol s_5_5[2] = { 'i', 'k' }; -static const symbol s_5_6[4] = { 'h', 'e', 'i', 't' }; -static const symbol s_5_7[4] = { 'k', 'e', 'i', 't' }; +static const symbol s_5_0[2] = { 'i', 'g' }; +static const symbol s_5_1[4] = { 'l', 'i', 'c', 'h' }; +static const struct among a_5[2] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0} +}; -static const struct among a_5[8] = -{ -{ 3, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 2, 0}, -{ 3, s_5_2, -1, 1, 0}, -{ 4, s_5_3, -1, 3, 0}, -{ 4, s_5_4, -1, 2, 0}, -{ 2, s_5_5, -1, 2, 0}, -{ 4, s_5_6, -1, 3, 0}, -{ 4, s_5_7, -1, 4, 0} +static const symbol s_6_0[3] = { 'e', 'n', 'd' }; +static const symbol s_6_1[2] = { 'i', 'g' }; +static const symbol s_6_2[3] = { 'u', 'n', 'g' }; +static const symbol s_6_3[4] = { 'l', 'i', 'c', 'h' }; +static const symbol s_6_4[4] = { 'i', 's', 'c', 'h' }; +static const symbol s_6_5[2] = { 'i', 'k' }; +static const symbol s_6_6[4] = { 'h', 'e', 'i', 't' }; +static const symbol s_6_7[4] = { 'k', 'e', 'i', 't' }; +static const struct among a_6[8] = { +{ 3, s_6_0, 0, 1, 0}, +{ 2, s_6_1, 0, 2, 0}, +{ 3, s_6_2, 0, 1, 0}, +{ 4, s_6_3, 0, 3, 0}, +{ 4, s_6_4, 0, 2, 0}, +{ 2, s_6_5, 0, 2, 0}, +{ 4, s_6_6, 0, 3, 0}, +{ 4, s_6_7, 0, 4, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 32, 8 }; +static const unsigned char g_et_ending[] = { 1, 128, 198, 227, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + static const unsigned char g_s_ending[] = { 117, 30, 5 }; static const unsigned char g_st_ending[] = { 117, 30, 4 }; -static const symbol s_0[] = { 'U' }; -static const symbol s_1[] = { 'Y' }; -static const symbol s_2[] = { 's', 's' }; -static const symbol s_3[] = { 0xE4 }; -static const symbol s_4[] = { 0xF6 }; -static const symbol s_5[] = { 0xFC }; -static const symbol s_6[] = { 'y' }; -static const symbol s_7[] = { 'u' }; -static const symbol s_8[] = { 'a' }; -static const symbol s_9[] = { 'o' }; -static const symbol s_10[] = { 's', 'y', 's', 't' }; -static const symbol s_11[] = { 'n', 'i', 's' }; -static const symbol s_12[] = { 'l' }; -static const symbol s_13[] = { 'i', 'g' }; -static const symbol s_14[] = { 'e', 'r' }; -static const symbol s_15[] = { 'e', 'n' }; - static int r_prelude(struct SN_env * z) { int among_var; - { int c_test1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; if (in_grouping(z, g_v, 97, 252, 0)) goto lab1; z->bra = z->c; - { int c4 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab3; + do { + int v_4 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab2; z->c++; z->ket = z->c; - if (in_grouping(z, g_v, 97, 252, 0)) goto lab3; - { int ret = slice_from_s(z, 1, s_0); + if (in_grouping(z, g_v, 97, 252, 0)) goto lab2; + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } - goto lab2; - lab3: - z->c = c4; + break; + lab2: + z->c = v_4; if (z->c == z->l || z->p[z->c] != 'y') goto lab1; z->c++; z->ket = z->c; if (in_grouping(z, g_v, 97, 252, 0)) goto lab1; - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } - } - lab2: - z->c = c3; + } while (0); + z->c = v_3; break; lab1: - z->c = c3; + z->c = v_3; if (z->c >= z->l) goto lab0; z->c++; } continue; lab0: - z->c = c2; + z->c = v_2; break; } - z->c = c_test1; + z->c = v_1; } - while(1) { - int c5 = z->c; + while (1) { + int v_5 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 6); + among_var = find_among(z, a_0, 6, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 5: - if (z->c >= z->l) goto lab4; + if (z->c >= z->l) goto lab3; z->c++; break; } continue; - lab4: - z->c = c5; + lab3: + z->c = v_5; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - { int c_test1 = z->c; -z->c = z->c + 3; - if (z->c > z->l) return 0; - z->I[0] = z->c; - z->c = c_test1; + int i_x; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + i_x = z->c; + z->c = v_1; } - { int ret = out_grouping(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - z->I[2] = z->c; - - if (z->I[2] >= z->I[0]) goto lab0; - z->I[2] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: - { int ret = out_grouping(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; + ((SN_local *)z)->i_p2 = z->c; return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; - among_var = find_among(z, a_1, 6); + among_var = find_among(z, a_1, 6, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; @@ -311,60 +326,68 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_R1(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_standard_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((811040 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; - among_var = find_among_b(z, a_2, 11); + among_var = find_among_b(z, a_2, 11, 0); if (!among_var) goto lab0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (!(eq_s_b(z, 4, s_10))) goto lab1; goto lab0; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - m3; goto lab2; } + if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - v_3; goto lab2; } z->c--; z->bra = z->c; - if (!(eq_s_b(z, 3, s_11))) { z->c = z->l - m3; goto lab2; } - { int ret = slice_del(z); + if (!(eq_s_b(z, 3, s_11))) { z->c = z->l - v_3; goto lab2; } + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -373,136 +396,178 @@ static int r_standard_suffix(struct SN_env * z) { break; case 4: if (in_grouping_b(z, g_s_ending, 98, 116, 0)) goto lab0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1327104 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab3; - among_var = find_among_b(z, a_3, 4); + among_var = find_among_b(z, a_4, 5, 0); if (!among_var) goto lab3; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b(z, g_st_ending, 98, 116, 0)) goto lab3; -z->c = z->c - 3; - if (z->c < z->lb) goto lab3; - { int ret = slice_del(z); + if (z->c - 3 < z->lb) goto lab3; + z->c -= 3; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int v_5 = z->l - z->c; + if (in_grouping_b(z, g_et_ending, 85, 228, 0)) goto lab3; + z->c = z->l - v_5; + } + { + int v_6 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((280576 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab4; + if (!find_among_b(z, a_3, 5, 0)) goto lab4; + goto lab3; + lab4: + z->c = z->l - v_6; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } lab3: - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; + { + int v_7 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1051024 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab4; - among_var = find_among_b(z, a_5, 8); - if (!among_var) goto lab4; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1051024 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab5; + among_var = find_among_b(z, a_6, 8, 0); + if (!among_var) goto lab5; z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) goto lab4; + { + int ret = r_R2(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; + { + int v_8 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_13))) { z->c = z->l - m6; goto lab5; } + if (!(eq_s_b(z, 2, s_13))) { z->c = z->l - v_8; goto lab6; } z->bra = z->c; - { int m7 = z->l - z->c; (void)m7; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab6; + { + int v_9 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab7; z->c--; - { z->c = z->l - m6; goto lab5; } - lab6: - z->c = z->l - m7; + { z->c = z->l - v_8; goto lab6; } + lab7: + z->c = z->l - v_9; } - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m6; goto lab5; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_8; goto lab6; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab5: + lab6: ; } break; case 2: - { int m8 = z->l - z->c; (void)m8; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab7; + { + int v_10 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab8; z->c--; - goto lab4; - lab7: - z->c = z->l - m8; + goto lab5; + lab8: + z->c = z->l - v_10; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; + { + int v_11 = z->l - z->c; z->ket = z->c; - { int m10 = z->l - z->c; (void)m10; + do { + int v_12 = z->l - z->c; if (!(eq_s_b(z, 2, s_14))) goto lab10; - goto lab9; + break; lab10: - z->c = z->l - m10; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m9; goto lab8; } - } - lab9: + z->c = z->l - v_12; + if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - v_11; goto lab9; } + } while (0); z->bra = z->c; - { int ret = r_R1(z); - if (ret == 0) { z->c = z->l - m9; goto lab8; } + { + int ret = r_R1(z); + if (ret == 0) { z->c = z->l - v_11; goto lab9; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab8: + lab9: ; } break; case 4: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m11 = z->l - z->c; (void)m11; + { + int v_13 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 103 && z->p[z->c - 1] != 104)) { z->c = z->l - m11; goto lab11; } - if (!find_among_b(z, a_4, 2)) { z->c = z->l - m11; goto lab11; } + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 103 && z->p[z->c - 1] != 104)) { z->c = z->l - v_13; goto lab11; } + if (!find_among_b(z, a_5, 2, 0)) { z->c = z->l - v_13; goto lab11; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m11; goto lab11; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_13; goto lab11; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab11: @@ -510,42 +575,56 @@ z->c = z->c - 3; } break; } - lab4: - z->c = z->l - m5; + lab5: + z->c = z->l - v_7; } return 1; } extern int german_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_prelude(z); + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - { int ret = r_mark_regions(z); + { + int v_2 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c2; + z->c = v_2; } z->lb = z->c; z->c = z->l; - - - { int ret = r_standard_suffix(z); + { + int ret = r_standard_suffix(z); if (ret < 0) return ret; } z->c = z->lb; - { int c3 = z->c; - { int ret = r_postlude(z); + { + int v_3 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c3; + z->c = v_3; } return 1; } -extern struct SN_env * german_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * german_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void german_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void german_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_indonesian.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_indonesian.c index 8624d0382ebde..986599f3dc0f9 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_indonesian.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_indonesian.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from indonesian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_indonesian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_prefix; + int i_measure; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,59 +21,44 @@ extern int indonesian_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif -static int r_VOWEL(struct SN_env * z); -static int r_SUFFIX_I_OK(struct SN_env * z); -static int r_SUFFIX_AN_OK(struct SN_env * z); -static int r_SUFFIX_KAN_OK(struct SN_env * z); -static int r_KER(struct SN_env * z); + static int r_remove_suffix(struct SN_env * z); static int r_remove_second_order_prefix(struct SN_env * z); static int r_remove_first_order_prefix(struct SN_env * z); static int r_remove_possessive_pronoun(struct SN_env * z); static int r_remove_particle(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * indonesian_ISO_8859_1_create_env(void); -extern void indonesian_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 's' }; +static const symbol s_1[] = { 's' }; +static const symbol s_2[] = { 'p' }; +static const symbol s_3[] = { 'p' }; +static const symbol s_4[] = { 'a', 'j', 'a', 'r' }; +static const symbol s_5[] = { 'a', 'j', 'a', 'r' }; +static const symbol s_6[] = { 'e', 'r' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 'k', 'a', 'h' }; static const symbol s_0_1[3] = { 'l', 'a', 'h' }; static const symbol s_0_2[3] = { 'p', 'u', 'n' }; - -static const struct among a_0[3] = -{ -{ 3, s_0_0, -1, 1, 0}, -{ 3, s_0_1, -1, 1, 0}, -{ 3, s_0_2, -1, 1, 0} +static const struct among a_0[3] = { +{ 3, s_0_0, 0, 1, 0}, +{ 3, s_0_1, 0, 1, 0}, +{ 3, s_0_2, 0, 1, 0} }; static const symbol s_1_0[3] = { 'n', 'y', 'a' }; static const symbol s_1_1[2] = { 'k', 'u' }; static const symbol s_1_2[2] = { 'm', 'u' }; - -static const struct among a_1[3] = -{ -{ 3, s_1_0, -1, 1, 0}, -{ 2, s_1_1, -1, 1, 0}, -{ 2, s_1_2, -1, 1, 0} +static const struct among a_1[3] = { +{ 3, s_1_0, 0, 1, 0}, +{ 2, s_1_1, 0, 1, 0}, +{ 2, s_1_2, 0, 1, 0} }; static const symbol s_2_0[1] = { 'i' }; static const symbol s_2_1[2] = { 'a', 'n' }; -static const symbol s_2_2[3] = { 'k', 'a', 'n' }; - -static const struct among a_2[3] = -{ -{ 1, s_2_0, -1, 1, r_SUFFIX_I_OK}, -{ 2, s_2_1, -1, 1, r_SUFFIX_AN_OK}, -{ 3, s_2_2, 1, 1, r_SUFFIX_KAN_OK} +static const struct among a_2[2] = { +{ 1, s_2_0, 0, 2, 0}, +{ 2, s_2_1, 0, 1, 0} }; static const symbol s_3_0[2] = { 'd', 'i' }; @@ -70,123 +67,97 @@ static const symbol s_3_2[2] = { 'm', 'e' }; static const symbol s_3_3[3] = { 'm', 'e', 'm' }; static const symbol s_3_4[3] = { 'm', 'e', 'n' }; static const symbol s_3_5[4] = { 'm', 'e', 'n', 'g' }; -static const symbol s_3_6[4] = { 'm', 'e', 'n', 'y' }; -static const symbol s_3_7[3] = { 'p', 'e', 'm' }; -static const symbol s_3_8[3] = { 'p', 'e', 'n' }; -static const symbol s_3_9[4] = { 'p', 'e', 'n', 'g' }; -static const symbol s_3_10[4] = { 'p', 'e', 'n', 'y' }; -static const symbol s_3_11[3] = { 't', 'e', 'r' }; - -static const struct among a_3[12] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 2, 0}, -{ 2, s_3_2, -1, 1, 0}, -{ 3, s_3_3, 2, 5, 0}, -{ 3, s_3_4, 2, 1, 0}, -{ 4, s_3_5, 4, 1, 0}, -{ 4, s_3_6, 4, 3, r_VOWEL}, -{ 3, s_3_7, -1, 6, 0}, -{ 3, s_3_8, -1, 2, 0}, -{ 4, s_3_9, 8, 2, 0}, -{ 4, s_3_10, 8, 4, r_VOWEL}, -{ 3, s_3_11, -1, 1, 0} +static const symbol s_3_6[3] = { 'p', 'e', 'm' }; +static const symbol s_3_7[3] = { 'p', 'e', 'n' }; +static const symbol s_3_8[4] = { 'p', 'e', 'n', 'g' }; +static const symbol s_3_9[3] = { 't', 'e', 'r' }; +static const struct among a_3[10] = { +{ 2, s_3_0, 0, 1, 0}, +{ 2, s_3_1, 0, 3, 0}, +{ 2, s_3_2, 0, 1, 0}, +{ 3, s_3_3, -1, 5, 0}, +{ 3, s_3_4, -2, 2, 0}, +{ 4, s_3_5, -1, 1, 0}, +{ 3, s_3_6, 0, 6, 0}, +{ 3, s_3_7, 0, 4, 0}, +{ 4, s_3_8, -1, 3, 0}, +{ 3, s_3_9, 0, 1, 0} }; static const symbol s_4_0[2] = { 'b', 'e' }; -static const symbol s_4_1[7] = { 'b', 'e', 'l', 'a', 'j', 'a', 'r' }; -static const symbol s_4_2[3] = { 'b', 'e', 'r' }; -static const symbol s_4_3[2] = { 'p', 'e' }; -static const symbol s_4_4[7] = { 'p', 'e', 'l', 'a', 'j', 'a', 'r' }; -static const symbol s_4_5[3] = { 'p', 'e', 'r' }; - -static const struct among a_4[6] = -{ -{ 2, s_4_0, -1, 3, r_KER}, -{ 7, s_4_1, 0, 4, 0}, -{ 3, s_4_2, 0, 3, 0}, -{ 2, s_4_3, -1, 1, 0}, -{ 7, s_4_4, 3, 2, 0}, -{ 3, s_4_5, 3, 1, 0} +static const symbol s_4_1[2] = { 'p', 'e' }; +static const struct among a_4[2] = { +{ 2, s_4_0, 0, 2, 0}, +{ 2, s_4_1, 0, 1, 0} }; static const unsigned char g_vowel[] = { 17, 65, 16 }; -static const symbol s_0[] = { 'e', 'r' }; -static const symbol s_1[] = { 's' }; -static const symbol s_2[] = { 's' }; -static const symbol s_3[] = { 'p' }; -static const symbol s_4[] = { 'p' }; -static const symbol s_5[] = { 'a', 'j', 'a', 'r' }; -static const symbol s_6[] = { 'a', 'j', 'a', 'r' }; - static int r_remove_particle(struct SN_env * z) { z->ket = z->c; if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 104 && z->p[z->c - 1] != 110)) return 0; - if (!find_among_b(z, a_0, 3)) return 0; + if (!find_among_b(z, a_0, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; + ((SN_local *)z)->i_measure -= 1; return 1; } static int r_remove_possessive_pronoun(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 117)) return 0; - if (!find_among_b(z, a_1, 3)) return 0; + if (!find_among_b(z, a_1, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; - return 1; -} - -static int r_SUFFIX_KAN_OK(struct SN_env * z) { - - if (z->I[0] == 3) return 0; - if (z->I[0] == 2) return 0; - return 1; -} - -static int r_SUFFIX_AN_OK(struct SN_env * z) { - return z->I[0] != 1; -} - -static int r_SUFFIX_I_OK(struct SN_env * z) { - if (z->I[0] > 2) return 0; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; - z->c--; - return 0; - lab0: - z->c = z->l - m1; - } + ((SN_local *)z)->i_measure -= 1; return 1; } static int r_remove_suffix(struct SN_env * z) { + int among_var; z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 110)) return 0; - if (!find_among_b(z, a_2, 3)) return 0; + among_var = find_among_b(z, a_2, 2, 0); + if (!among_var) return 0; z->bra = z->c; - { int ret = slice_del(z); + switch (among_var) { + case 1: + do { + int v_1 = z->l - z->c; + if (((SN_local *)z)->i_prefix == 3) goto lab0; + if (((SN_local *)z)->i_prefix == 2) goto lab0; + if (z->c <= z->lb || z->p[z->c - 1] != 'k') goto lab0; + z->c--; + z->bra = z->c; + break; + lab0: + z->c = z->l - v_1; + if (((SN_local *)z)->i_prefix == 1) return 0; + } while (0); + break; + case 2: + if (((SN_local *)z)->i_prefix > 2) return 0; + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + z->c--; + return 0; + lab1: + z->c = z->l - v_2; + } + break; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; - return 1; -} - -static int r_VOWEL(struct SN_env * z) { - if (in_grouping(z, g_vowel, 97, 117, 0)) return 0; - return 1; -} - -static int r_KER(struct SN_env * z) { - if (out_grouping(z, g_vowel, 97, 117, 0)) return 0; - if (!(eq_s(z, 2, s_0))) return 0; + ((SN_local *)z)->i_measure -= 1; return 1; } @@ -194,77 +165,127 @@ static int r_remove_first_order_prefix(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 1 >= z->l || (z->p[z->c + 1] != 105 && z->p[z->c + 1] != 101)) return 0; - among_var = find_among(z, a_3, 12); + among_var = find_among(z, a_3, 10, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 1; - z->I[1] -= 1; + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; break; case 2: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[0] = 3; - z->I[1] -= 1; + do { + int v_1 = z->c; + if (z->c == z->l || z->p[z->c] != 'y') goto lab0; + z->c++; + { + int v_2 = z->c; + if (in_grouping(z, g_vowel, 97, 117, 0)) goto lab0; + z->c = v_2; + } + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_0); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; + break; + lab0: + z->c = v_1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; + } while (0); break; case 3: - z->I[0] = 1; - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; break; case 4: - z->I[0] = 3; - { int ret = slice_from_s(z, 1, s_2); - if (ret < 0) return ret; - } - z->I[1] -= 1; + do { + int v_3 = z->c; + if (z->c == z->l || z->p[z->c] != 'y') goto lab1; + z->c++; + { + int v_4 = z->c; + if (in_grouping(z, g_vowel, 97, 117, 0)) goto lab1; + z->c = v_4; + } + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_1); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; + break; + lab1: + z->c = v_3; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; + } while (0); break; case 5: - z->I[0] = 1; - z->I[1] -= 1; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping(z, g_vowel, 97, 117, 0)) goto lab1; - z->c = c2; - { int ret = slice_from_s(z, 1, s_3); + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; + do { + int v_5 = z->c; + { + int v_6 = z->c; + if (in_grouping(z, g_vowel, 97, 117, 0)) goto lab2; + z->c = v_6; + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } } - goto lab0; - lab1: - z->c = c1; - { int ret = slice_del(z); + break; + lab2: + z->c = v_5; + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab0: + } while (0); break; case 6: - z->I[0] = 3; - z->I[1] -= 1; - { int c3 = z->c; - { int c4 = z->c; + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; + do { + int v_7 = z->c; + { + int v_8 = z->c; if (in_grouping(z, g_vowel, 97, 117, 0)) goto lab3; - z->c = c4; - { int ret = slice_from_s(z, 1, s_4); + z->c = v_8; + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } } - goto lab2; + break; lab3: - z->c = c3; - { int ret = slice_del(z); + z->c = v_7; + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab2: + } while (0); break; } return 1; @@ -274,134 +295,174 @@ static int r_remove_second_order_prefix(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 1 >= z->l || z->p[z->c + 1] != 101) return 0; - among_var = find_among(z, a_4, 6); + among_var = find_among(z, a_4, 2, 0); if (!among_var) return 0; - z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[0] = 2; - z->I[1] -= 1; + do { + int v_1 = z->c; + if (z->c == z->l || z->p[z->c] != 'r') goto lab0; + z->c++; + z->ket = z->c; + ((SN_local *)z)->i_prefix = 2; + break; + lab0: + z->c = v_1; + if (z->c == z->l || z->p[z->c] != 'l') goto lab1; + z->c++; + z->ket = z->c; + if (!(eq_s(z, 4, s_4))) goto lab1; + break; + lab1: + z->c = v_1; + z->ket = z->c; + ((SN_local *)z)->i_prefix = 2; + } while (0); break; case 2: - { int ret = slice_from_s(z, 4, s_5); - if (ret < 0) return ret; - } - z->I[1] -= 1; - break; - case 3: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[0] = 4; - z->I[1] -= 1; - break; - case 4: - { int ret = slice_from_s(z, 4, s_6); - if (ret < 0) return ret; - } - z->I[0] = 4; - z->I[1] -= 1; + do { + int v_2 = z->c; + if (z->c == z->l || z->p[z->c] != 'r') goto lab2; + z->c++; + z->ket = z->c; + break; + lab2: + z->c = v_2; + if (z->c == z->l || z->p[z->c] != 'l') goto lab3; + z->c++; + z->ket = z->c; + if (!(eq_s(z, 4, s_5))) goto lab3; + break; + lab3: + z->c = v_2; + z->ket = z->c; + if (out_grouping(z, g_vowel, 97, 117, 0)) return 0; + if (!(eq_s(z, 2, s_6))) return 0; + } while (0); + ((SN_local *)z)->i_prefix = 4; break; } + ((SN_local *)z)->i_measure -= 1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } return 1; } extern int indonesian_ISO_8859_1_stem(struct SN_env * z) { - z->I[1] = 0; - { int c1 = z->c; - while(1) { - int c2 = z->c; - + ((SN_local *)z)->i_measure = 0; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; { int ret = out_grouping(z, g_vowel, 97, 117, 1); if (ret < 0) goto lab1; z->c += ret; } - z->I[1] += 1; + ((SN_local *)z)->i_measure += 1; continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } - if (z->I[1] <= 2) return 0; - z->I[0] = 0; + if (((SN_local *)z)->i_measure <= 2) return 0; + ((SN_local *)z)->i_prefix = 0; z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_remove_particle(z); + { + int v_3 = z->l - z->c; + { + int ret = r_remove_particle(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - if (z->I[1] <= 2) return 0; - { int m4 = z->l - z->c; (void)m4; - { int ret = r_remove_possessive_pronoun(z); + if (((SN_local *)z)->i_measure <= 2) return 0; + { + int v_4 = z->l - z->c; + { + int ret = r_remove_possessive_pronoun(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; - if (z->I[1] <= 2) return 0; - { int c5 = z->c; - { int c_test6 = z->c; - { int ret = r_remove_first_order_prefix(z); - if (ret == 0) goto lab3; + if (((SN_local *)z)->i_measure <= 2) return 0; + do { + int v_5 = z->c; + { + int v_6 = z->c; + { + int ret = r_remove_first_order_prefix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - { int c7 = z->c; - { int c_test8 = z->c; - if (z->I[1] <= 2) goto lab4; + { + int v_7 = z->c; + { + int v_8 = z->c; + if (((SN_local *)z)->i_measure <= 2) goto lab3; z->lb = z->c; z->c = z->l; - - { int ret = r_remove_suffix(z); - if (ret == 0) goto lab4; + { + int ret = r_remove_suffix(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } z->c = z->lb; - z->c = c_test8; + z->c = v_8; } - if (z->I[1] <= 2) goto lab4; - { int ret = r_remove_second_order_prefix(z); - if (ret == 0) goto lab4; + if (((SN_local *)z)->i_measure <= 2) goto lab3; + { + int ret = r_remove_second_order_prefix(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - lab4: - z->c = c7; + lab3: + z->c = v_7; } - z->c = c_test6; + z->c = v_6; } - goto lab2; - lab3: - z->c = c5; - { int c9 = z->c; - { int ret = r_remove_second_order_prefix(z); + break; + lab2: + z->c = v_5; + { + int v_9 = z->c; + { + int ret = r_remove_second_order_prefix(z); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; } - { int c10 = z->c; - if (z->I[1] <= 2) goto lab5; + { + int v_10 = z->c; + if (((SN_local *)z)->i_measure <= 2) goto lab4; z->lb = z->c; z->c = z->l; - - { int ret = r_remove_suffix(z); - if (ret == 0) goto lab5; + { + int ret = r_remove_suffix(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } z->c = z->lb; - lab5: - z->c = c10; + lab4: + z->c = v_10; } - } -lab2: + } while (0); return 1; } -extern struct SN_env * indonesian_ISO_8859_1_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * indonesian_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_prefix = 0; + ((SN_local *)z)->i_measure = 0; + } + return z; +} -extern void indonesian_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void indonesian_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_irish.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_irish.c index a61591eab3f95..9a14b0d5d40d0 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_irish.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_irish.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from irish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_irish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int irish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_verb_sfx(struct SN_env * z); static int r_deriv(struct SN_env * z); static int r_noun_sfx(struct SN_env * z); @@ -17,18 +31,22 @@ static int r_initial_morph(struct SN_env * z); static int r_RV(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * irish_ISO_8859_1_create_env(void); -extern void irish_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'f' }; +static const symbol s_1[] = { 's' }; +static const symbol s_2[] = { 'b' }; +static const symbol s_3[] = { 'c' }; +static const symbol s_4[] = { 'd' }; +static const symbol s_5[] = { 'g' }; +static const symbol s_6[] = { 'p' }; +static const symbol s_7[] = { 't' }; +static const symbol s_8[] = { 'm' }; +static const symbol s_9[] = { 'a', 'r', 'c' }; +static const symbol s_10[] = { 'g', 'i', 'n' }; +static const symbol s_11[] = { 'g', 'r', 'a', 'f' }; +static const symbol s_12[] = { 'p', 'a', 'i', 't', 'e' }; +static const symbol s_13[] = { 0xF3, 'i', 'd' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 'b', '\'' }; static const symbol s_0_1[2] = { 'b', 'h' }; static const symbol s_0_2[3] = { 'b', 'h', 'f' }; @@ -53,33 +71,31 @@ static const symbol s_0_20[2] = { 's', 'h' }; static const symbol s_0_21[2] = { 't', '-' }; static const symbol s_0_22[2] = { 't', 'h' }; static const symbol s_0_23[2] = { 't', 's' }; - -static const struct among a_0[24] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 2, s_0_1, -1, 4, 0}, -{ 3, s_0_2, 1, 2, 0}, -{ 2, s_0_3, -1, 8, 0}, -{ 2, s_0_4, -1, 5, 0}, -{ 2, s_0_5, -1, 1, 0}, -{ 4, s_0_6, 5, 2, 0}, -{ 2, s_0_7, -1, 6, 0}, -{ 2, s_0_8, -1, 9, 0}, -{ 2, s_0_9, -1, 2, 0}, -{ 2, s_0_10, -1, 5, 0}, -{ 2, s_0_11, -1, 7, 0}, -{ 2, s_0_12, -1, 1, 0}, -{ 2, s_0_13, -1, 1, 0}, -{ 2, s_0_14, -1, 4, 0}, -{ 2, s_0_15, -1, 10, 0}, -{ 2, s_0_16, -1, 1, 0}, -{ 2, s_0_17, -1, 6, 0}, -{ 2, s_0_18, -1, 7, 0}, -{ 2, s_0_19, -1, 8, 0}, -{ 2, s_0_20, -1, 3, 0}, -{ 2, s_0_21, -1, 1, 0}, -{ 2, s_0_22, -1, 9, 0}, -{ 2, s_0_23, -1, 3, 0} +static const struct among a_0[24] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 4, 0}, +{ 3, s_0_2, -1, 2, 0}, +{ 2, s_0_3, 0, 8, 0}, +{ 2, s_0_4, 0, 5, 0}, +{ 2, s_0_5, 0, 1, 0}, +{ 4, s_0_6, -1, 2, 0}, +{ 2, s_0_7, 0, 6, 0}, +{ 2, s_0_8, 0, 9, 0}, +{ 2, s_0_9, 0, 2, 0}, +{ 2, s_0_10, 0, 5, 0}, +{ 2, s_0_11, 0, 7, 0}, +{ 2, s_0_12, 0, 1, 0}, +{ 2, s_0_13, 0, 1, 0}, +{ 2, s_0_14, 0, 4, 0}, +{ 2, s_0_15, 0, 10, 0}, +{ 2, s_0_16, 0, 1, 0}, +{ 2, s_0_17, 0, 6, 0}, +{ 2, s_0_18, 0, 7, 0}, +{ 2, s_0_19, 0, 8, 0}, +{ 2, s_0_20, 0, 3, 0}, +{ 2, s_0_21, 0, 1, 0}, +{ 2, s_0_22, 0, 9, 0}, +{ 2, s_0_23, 0, 3, 0} }; static const symbol s_1_0[6] = { 0xED, 'o', 'c', 'h', 't', 'a' }; @@ -98,25 +114,23 @@ static const symbol s_1_12[5] = { 0xED, 'o', 'c', 'h', 't' }; static const symbol s_1_13[6] = { 'a', 0xED, 'o', 'c', 'h', 't' }; static const symbol s_1_14[3] = { 'i', 'r', 0xED }; static const symbol s_1_15[4] = { 'a', 'i', 'r', 0xED }; - -static const struct among a_1[16] = -{ -{ 6, s_1_0, -1, 1, 0}, -{ 7, s_1_1, 0, 1, 0}, -{ 3, s_1_2, -1, 2, 0}, -{ 4, s_1_3, 2, 2, 0}, -{ 3, s_1_4, -1, 1, 0}, -{ 4, s_1_5, 4, 1, 0}, -{ 3, s_1_6, -1, 1, 0}, -{ 4, s_1_7, 6, 1, 0}, -{ 3, s_1_8, -1, 1, 0}, -{ 4, s_1_9, 8, 1, 0}, -{ 3, s_1_10, -1, 1, 0}, -{ 4, s_1_11, 10, 1, 0}, -{ 5, s_1_12, -1, 1, 0}, -{ 6, s_1_13, 12, 1, 0}, -{ 3, s_1_14, -1, 2, 0}, -{ 4, s_1_15, 14, 2, 0} +static const struct among a_1[16] = { +{ 6, s_1_0, 0, 1, 0}, +{ 7, s_1_1, -1, 1, 0}, +{ 3, s_1_2, 0, 2, 0}, +{ 4, s_1_3, -1, 2, 0}, +{ 3, s_1_4, 0, 1, 0}, +{ 4, s_1_5, -1, 1, 0}, +{ 3, s_1_6, 0, 1, 0}, +{ 4, s_1_7, -1, 1, 0}, +{ 3, s_1_8, 0, 1, 0}, +{ 4, s_1_9, -1, 1, 0}, +{ 3, s_1_10, 0, 1, 0}, +{ 4, s_1_11, -1, 1, 0}, +{ 5, s_1_12, 0, 1, 0}, +{ 6, s_1_13, -1, 1, 0}, +{ 3, s_1_14, 0, 2, 0}, +{ 4, s_1_15, -1, 2, 0} }; static const symbol s_2_0[8] = { 0xF3, 'i', 'd', 'e', 'a', 'c', 'h', 'a' }; @@ -144,34 +158,32 @@ static const symbol s_2_21[5] = { 'e', 'a', 'c', 'h', 't' }; static const symbol s_2_22[10] = { 'g', 'r', 'a', 'f', 'a', 0xED, 'o', 'c', 'h', 't' }; static const symbol s_2_23[9] = { 'a', 'r', 'c', 'a', 'c', 'h', 't', 'a', 0xED }; static const symbol s_2_24[12] = { 'g', 'r', 'a', 'f', 'a', 0xED, 'o', 'c', 'h', 't', 'a', 0xED }; - -static const struct among a_2[25] = -{ -{ 8, s_2_0, -1, 6, 0}, -{ 7, s_2_1, -1, 5, 0}, -{ 5, s_2_2, -1, 1, 0}, -{ 8, s_2_3, 2, 2, 0}, -{ 6, s_2_4, 2, 1, 0}, -{ 11, s_2_5, -1, 4, 0}, -{ 5, s_2_6, -1, 5, 0}, -{ 3, s_2_7, -1, 1, 0}, -{ 4, s_2_8, 7, 1, 0}, -{ 7, s_2_9, 8, 6, 0}, -{ 7, s_2_10, 8, 3, 0}, -{ 6, s_2_11, 7, 5, 0}, -{ 9, s_2_12, -1, 4, 0}, -{ 7, s_2_13, -1, 5, 0}, -{ 6, s_2_14, -1, 6, 0}, -{ 7, s_2_15, -1, 1, 0}, -{ 8, s_2_16, 15, 1, 0}, -{ 6, s_2_17, -1, 3, 0}, -{ 5, s_2_18, -1, 3, 0}, -{ 4, s_2_19, -1, 1, 0}, -{ 7, s_2_20, 19, 2, 0}, -{ 5, s_2_21, 19, 1, 0}, -{ 10, s_2_22, -1, 4, 0}, -{ 9, s_2_23, -1, 2, 0}, -{ 12, s_2_24, -1, 4, 0} +static const struct among a_2[25] = { +{ 8, s_2_0, 0, 6, 0}, +{ 7, s_2_1, 0, 5, 0}, +{ 5, s_2_2, 0, 1, 0}, +{ 8, s_2_3, -1, 2, 0}, +{ 6, s_2_4, -2, 1, 0}, +{ 11, s_2_5, 0, 4, 0}, +{ 5, s_2_6, 0, 5, 0}, +{ 3, s_2_7, 0, 1, 0}, +{ 4, s_2_8, -1, 1, 0}, +{ 7, s_2_9, -1, 6, 0}, +{ 7, s_2_10, -2, 3, 0}, +{ 6, s_2_11, -4, 5, 0}, +{ 9, s_2_12, 0, 4, 0}, +{ 7, s_2_13, 0, 5, 0}, +{ 6, s_2_14, 0, 6, 0}, +{ 7, s_2_15, 0, 1, 0}, +{ 8, s_2_16, -1, 1, 0}, +{ 6, s_2_17, 0, 3, 0}, +{ 5, s_2_18, 0, 3, 0}, +{ 4, s_2_19, 0, 1, 0}, +{ 7, s_2_20, -1, 2, 0}, +{ 5, s_2_21, -2, 1, 0}, +{ 10, s_2_22, 0, 4, 0}, +{ 9, s_2_23, 0, 2, 0}, +{ 12, s_2_24, 0, 4, 0} }; static const symbol s_3_0[4] = { 'i', 'm', 'i', 'd' }; @@ -186,74 +198,54 @@ static const symbol s_3_8[3] = { 0xE1, 'i', 'l' }; static const symbol s_3_9[3] = { 'a', 'i', 'n' }; static const symbol s_3_10[4] = { 't', 'e', 'a', 'r' }; static const symbol s_3_11[3] = { 't', 'a', 'r' }; - -static const struct among a_3[12] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 5, s_3_1, 0, 1, 0}, -{ 4, s_3_2, -1, 1, 0}, -{ 5, s_3_3, 2, 1, 0}, -{ 3, s_3_4, -1, 2, 0}, -{ 4, s_3_5, 4, 2, 0}, -{ 5, s_3_6, -1, 1, 0}, -{ 4, s_3_7, -1, 1, 0}, -{ 3, s_3_8, -1, 2, 0}, -{ 3, s_3_9, -1, 2, 0}, -{ 4, s_3_10, -1, 2, 0}, -{ 3, s_3_11, -1, 2, 0} +static const struct among a_3[12] = { +{ 4, s_3_0, 0, 1, 0}, +{ 5, s_3_1, -1, 1, 0}, +{ 4, s_3_2, 0, 1, 0}, +{ 5, s_3_3, -1, 1, 0}, +{ 3, s_3_4, 0, 2, 0}, +{ 4, s_3_5, -1, 2, 0}, +{ 5, s_3_6, 0, 1, 0}, +{ 4, s_3_7, 0, 1, 0}, +{ 3, s_3_8, 0, 2, 0}, +{ 3, s_3_9, 0, 2, 0}, +{ 4, s_3_10, 0, 2, 0}, +{ 3, s_3_11, 0, 2, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 17, 4, 2 }; -static const symbol s_0[] = { 'f' }; -static const symbol s_1[] = { 's' }; -static const symbol s_2[] = { 'b' }; -static const symbol s_3[] = { 'c' }; -static const symbol s_4[] = { 'd' }; -static const symbol s_5[] = { 'g' }; -static const symbol s_6[] = { 'p' }; -static const symbol s_7[] = { 't' }; -static const symbol s_8[] = { 'm' }; -static const symbol s_9[] = { 'a', 'r', 'c' }; -static const symbol s_10[] = { 'g', 'i', 'n' }; -static const symbol s_11[] = { 'g', 'r', 'a', 'f' }; -static const symbol s_12[] = { 'p', 'a', 'i', 't', 'e' }; -static const symbol s_13[] = { 0xF3, 'i', 'd' }; - static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[2] = z->c; - + ((SN_local *)z)->i_pV = z->c; { int ret = in_grouping(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } @@ -261,57 +253,67 @@ static int r_mark_regions(struct SN_env * z) { static int r_initial_morph(struct SN_env * z) { int among_var; z->bra = z->c; - among_var = find_among(z, a_0, 24); + among_var = find_among(z, a_0, 24, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; @@ -320,37 +322,41 @@ static int r_initial_morph(struct SN_env * z) { } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_noun_sfx(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_1, 16); + among_var = find_among_b(z, a_1, 16, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -361,40 +367,47 @@ static int r_noun_sfx(struct SN_env * z) { static int r_deriv(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_2, 25); + among_var = find_among_b(z, a_2, 25, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_9); + { + int ret = slice_from_s(z, 3, s_9); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_10); + { + int ret = slice_from_s(z, 3, s_10); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_11); + { + int ret = slice_from_s(z, 4, s_11); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 5, s_12); + { + int ret = slice_from_s(z, 5, s_12); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 3, s_13); + { + int ret = slice_from_s(z, 3, s_13); if (ret < 0) return ret; } break; @@ -406,23 +419,27 @@ static int r_verb_sfx(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((282896 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_3, 12); + among_var = find_among_b(z, a_3, 12, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -431,41 +448,58 @@ static int r_verb_sfx(struct SN_env * z) { } extern int irish_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_initial_morph(z); + { + int v_1 = z->c; + { + int ret = r_initial_morph(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_noun_sfx(z); + { + int v_2 = z->l - z->c; + { + int ret = r_noun_sfx(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_deriv(z); + { + int v_3 = z->l - z->c; + { + int ret = r_deriv(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_verb_sfx(z); + { + int v_4 = z->l - z->c; + { + int ret = r_verb_sfx(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; return 1; } -extern struct SN_env * irish_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * irish_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void irish_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void irish_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_italian.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_italian.c index 122449e90a9a1..323378171d8d9 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_italian.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_italian.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from italian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_italian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int italian_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_vowel_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_standard_suffix(struct SN_env * z); @@ -19,45 +33,49 @@ static int r_RV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -static int r_exceptions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * italian_ISO_8859_1_create_env(void); -extern void italian_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xE0 }; +static const symbol s_1[] = { 0xE8 }; +static const symbol s_2[] = { 0xEC }; +static const symbol s_3[] = { 0xF2 }; +static const symbol s_4[] = { 0xF9 }; +static const symbol s_5[] = { 'q', 'U' }; +static const symbol s_6[] = { 'U' }; +static const symbol s_7[] = { 'I' }; +static const symbol s_8[] = { 'd', 'i', 'v', 'a', 'n' }; +static const symbol s_9[] = { 'i' }; +static const symbol s_10[] = { 'u' }; +static const symbol s_11[] = { 'e' }; +static const symbol s_12[] = { 'i', 'c' }; +static const symbol s_13[] = { 'l', 'o', 'g' }; +static const symbol s_14[] = { 'u' }; +static const symbol s_15[] = { 'e', 'n', 't', 'e' }; +static const symbol s_16[] = { 'a', 't' }; +static const symbol s_17[] = { 'a', 't' }; +static const symbol s_18[] = { 'i', 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 'q', 'u' }; static const symbol s_0_2[1] = { 0xE1 }; static const symbol s_0_3[1] = { 0xE9 }; static const symbol s_0_4[1] = { 0xED }; static const symbol s_0_5[1] = { 0xF3 }; static const symbol s_0_6[1] = { 0xFA }; - -static const struct among a_0[7] = -{ -{ 0, 0, -1, 7, 0}, -{ 2, s_0_1, 0, 6, 0}, -{ 1, s_0_2, 0, 1, 0}, -{ 1, s_0_3, 0, 2, 0}, -{ 1, s_0_4, 0, 3, 0}, -{ 1, s_0_5, 0, 4, 0}, -{ 1, s_0_6, 0, 5, 0} +static const struct among a_0[7] = { +{ 0, 0, 0, 7, 0}, +{ 2, s_0_1, -1, 6, 0}, +{ 1, s_0_2, -2, 1, 0}, +{ 1, s_0_3, -3, 2, 0}, +{ 1, s_0_4, -4, 3, 0}, +{ 1, s_0_5, -5, 4, 0}, +{ 1, s_0_6, -6, 5, 0} }; static const symbol s_1_1[1] = { 'I' }; static const symbol s_1_2[1] = { 'U' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 1, s_1_1, 0, 1, 0}, -{ 1, s_1_2, 0, 2, 0} +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 1, s_1_1, -1, 1, 0}, +{ 1, s_1_2, -2, 2, 0} }; static const symbol s_2_0[2] = { 'l', 'a' }; @@ -97,46 +115,44 @@ static const symbol s_2_33[6] = { 'g', 'l', 'i', 'e', 'l', 'o' }; static const symbol s_2_34[4] = { 'm', 'e', 'l', 'o' }; static const symbol s_2_35[4] = { 't', 'e', 'l', 'o' }; static const symbol s_2_36[4] = { 'v', 'e', 'l', 'o' }; - -static const struct among a_2[37] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 4, s_2_1, 0, -1, 0}, -{ 6, s_2_2, 0, -1, 0}, -{ 4, s_2_3, 0, -1, 0}, -{ 4, s_2_4, 0, -1, 0}, -{ 4, s_2_5, 0, -1, 0}, -{ 2, s_2_6, -1, -1, 0}, -{ 4, s_2_7, 6, -1, 0}, -{ 6, s_2_8, 6, -1, 0}, -{ 4, s_2_9, 6, -1, 0}, -{ 4, s_2_10, 6, -1, 0}, -{ 4, s_2_11, 6, -1, 0}, -{ 2, s_2_12, -1, -1, 0}, -{ 4, s_2_13, 12, -1, 0}, -{ 6, s_2_14, 12, -1, 0}, -{ 4, s_2_15, 12, -1, 0}, -{ 4, s_2_16, 12, -1, 0}, -{ 4, s_2_17, 12, -1, 0}, -{ 4, s_2_18, 12, -1, 0}, -{ 2, s_2_19, -1, -1, 0}, -{ 2, s_2_20, -1, -1, 0}, -{ 4, s_2_21, 20, -1, 0}, -{ 6, s_2_22, 20, -1, 0}, -{ 4, s_2_23, 20, -1, 0}, -{ 4, s_2_24, 20, -1, 0}, -{ 4, s_2_25, 20, -1, 0}, -{ 3, s_2_26, 20, -1, 0}, -{ 2, s_2_27, -1, -1, 0}, -{ 2, s_2_28, -1, -1, 0}, -{ 2, s_2_29, -1, -1, 0}, -{ 2, s_2_30, -1, -1, 0}, -{ 2, s_2_31, -1, -1, 0}, -{ 4, s_2_32, 31, -1, 0}, -{ 6, s_2_33, 31, -1, 0}, -{ 4, s_2_34, 31, -1, 0}, -{ 4, s_2_35, 31, -1, 0}, -{ 4, s_2_36, 31, -1, 0} +static const struct among a_2[37] = { +{ 2, s_2_0, 0, -1, 0}, +{ 4, s_2_1, -1, -1, 0}, +{ 6, s_2_2, -2, -1, 0}, +{ 4, s_2_3, -3, -1, 0}, +{ 4, s_2_4, -4, -1, 0}, +{ 4, s_2_5, -5, -1, 0}, +{ 2, s_2_6, 0, -1, 0}, +{ 4, s_2_7, -1, -1, 0}, +{ 6, s_2_8, -2, -1, 0}, +{ 4, s_2_9, -3, -1, 0}, +{ 4, s_2_10, -4, -1, 0}, +{ 4, s_2_11, -5, -1, 0}, +{ 2, s_2_12, 0, -1, 0}, +{ 4, s_2_13, -1, -1, 0}, +{ 6, s_2_14, -2, -1, 0}, +{ 4, s_2_15, -3, -1, 0}, +{ 4, s_2_16, -4, -1, 0}, +{ 4, s_2_17, -5, -1, 0}, +{ 4, s_2_18, -6, -1, 0}, +{ 2, s_2_19, 0, -1, 0}, +{ 2, s_2_20, 0, -1, 0}, +{ 4, s_2_21, -1, -1, 0}, +{ 6, s_2_22, -2, -1, 0}, +{ 4, s_2_23, -3, -1, 0}, +{ 4, s_2_24, -4, -1, 0}, +{ 4, s_2_25, -5, -1, 0}, +{ 3, s_2_26, -6, -1, 0}, +{ 2, s_2_27, 0, -1, 0}, +{ 2, s_2_28, 0, -1, 0}, +{ 2, s_2_29, 0, -1, 0}, +{ 2, s_2_30, 0, -1, 0}, +{ 2, s_2_31, 0, -1, 0}, +{ 4, s_2_32, -1, -1, 0}, +{ 6, s_2_33, -2, -1, 0}, +{ 4, s_2_34, -3, -1, 0}, +{ 4, s_2_35, -4, -1, 0}, +{ 4, s_2_36, -5, -1, 0} }; static const symbol s_3_0[4] = { 'a', 'n', 'd', 'o' }; @@ -144,38 +160,32 @@ static const symbol s_3_1[4] = { 'e', 'n', 'd', 'o' }; static const symbol s_3_2[2] = { 'a', 'r' }; static const symbol s_3_3[2] = { 'e', 'r' }; static const symbol s_3_4[2] = { 'i', 'r' }; - -static const struct among a_3[5] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 2, s_3_2, -1, 2, 0}, -{ 2, s_3_3, -1, 2, 0}, -{ 2, s_3_4, -1, 2, 0} +static const struct among a_3[5] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 2, s_3_2, 0, 2, 0}, +{ 2, s_3_3, 0, 2, 0}, +{ 2, s_3_4, 0, 2, 0} }; static const symbol s_4_0[2] = { 'i', 'c' }; static const symbol s_4_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_4_2[2] = { 'o', 's' }; static const symbol s_4_3[2] = { 'i', 'v' }; - -static const struct among a_4[4] = -{ -{ 2, s_4_0, -1, -1, 0}, -{ 4, s_4_1, -1, -1, 0}, -{ 2, s_4_2, -1, -1, 0}, -{ 2, s_4_3, -1, 1, 0} +static const struct among a_4[4] = { +{ 2, s_4_0, 0, -1, 0}, +{ 4, s_4_1, 0, -1, 0}, +{ 2, s_4_2, 0, -1, 0}, +{ 2, s_4_3, 0, 1, 0} }; static const symbol s_5_0[2] = { 'i', 'c' }; static const symbol s_5_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_5_2[2] = { 'i', 'v' }; - -static const struct among a_5[3] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 2, s_5_2, -1, 1, 0} +static const struct among a_5[3] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 2, s_5_2, 0, 1, 0} }; static const symbol s_6_0[3] = { 'i', 'c', 'a' }; @@ -229,60 +239,58 @@ static const symbol s_6_47[3] = { 'i', 't', 0xE0 }; static const symbol s_6_48[4] = { 'i', 's', 't', 0xE0 }; static const symbol s_6_49[4] = { 'i', 's', 't', 0xE8 }; static const symbol s_6_50[4] = { 'i', 's', 't', 0xEC }; - -static const struct among a_6[51] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 5, s_6_1, -1, 3, 0}, -{ 3, s_6_2, -1, 1, 0}, -{ 4, s_6_3, -1, 1, 0}, -{ 3, s_6_4, -1, 9, 0}, -{ 4, s_6_5, -1, 1, 0}, -{ 4, s_6_6, -1, 5, 0}, -{ 3, s_6_7, -1, 1, 0}, -{ 6, s_6_8, 7, 1, 0}, -{ 4, s_6_9, -1, 1, 0}, -{ 5, s_6_10, -1, 3, 0}, -{ 5, s_6_11, -1, 1, 0}, -{ 5, s_6_12, -1, 1, 0}, -{ 6, s_6_13, -1, 4, 0}, -{ 6, s_6_14, -1, 2, 0}, -{ 6, s_6_15, -1, 4, 0}, -{ 5, s_6_16, -1, 2, 0}, -{ 3, s_6_17, -1, 1, 0}, -{ 4, s_6_18, -1, 1, 0}, -{ 5, s_6_19, -1, 1, 0}, -{ 6, s_6_20, 19, 7, 0}, -{ 4, s_6_21, -1, 1, 0}, -{ 3, s_6_22, -1, 9, 0}, -{ 4, s_6_23, -1, 1, 0}, -{ 4, s_6_24, -1, 5, 0}, -{ 3, s_6_25, -1, 1, 0}, -{ 6, s_6_26, 25, 1, 0}, -{ 4, s_6_27, -1, 1, 0}, -{ 5, s_6_28, -1, 1, 0}, -{ 5, s_6_29, -1, 1, 0}, -{ 4, s_6_30, -1, 1, 0}, -{ 6, s_6_31, -1, 4, 0}, -{ 6, s_6_32, -1, 2, 0}, -{ 6, s_6_33, -1, 4, 0}, -{ 5, s_6_34, -1, 2, 0}, -{ 3, s_6_35, -1, 1, 0}, -{ 4, s_6_36, -1, 1, 0}, -{ 6, s_6_37, -1, 6, 0}, -{ 6, s_6_38, -1, 6, 0}, -{ 4, s_6_39, -1, 1, 0}, -{ 3, s_6_40, -1, 9, 0}, -{ 3, s_6_41, -1, 1, 0}, -{ 4, s_6_42, -1, 1, 0}, -{ 3, s_6_43, -1, 1, 0}, -{ 6, s_6_44, -1, 6, 0}, -{ 6, s_6_45, -1, 6, 0}, -{ 3, s_6_46, -1, 9, 0}, -{ 3, s_6_47, -1, 8, 0}, -{ 4, s_6_48, -1, 1, 0}, -{ 4, s_6_49, -1, 1, 0}, -{ 4, s_6_50, -1, 1, 0} +static const struct among a_6[51] = { +{ 3, s_6_0, 0, 1, 0}, +{ 5, s_6_1, 0, 3, 0}, +{ 3, s_6_2, 0, 1, 0}, +{ 4, s_6_3, 0, 1, 0}, +{ 3, s_6_4, 0, 9, 0}, +{ 4, s_6_5, 0, 1, 0}, +{ 4, s_6_6, 0, 5, 0}, +{ 3, s_6_7, 0, 1, 0}, +{ 6, s_6_8, -1, 1, 0}, +{ 4, s_6_9, 0, 1, 0}, +{ 5, s_6_10, 0, 3, 0}, +{ 5, s_6_11, 0, 1, 0}, +{ 5, s_6_12, 0, 1, 0}, +{ 6, s_6_13, 0, 4, 0}, +{ 6, s_6_14, 0, 2, 0}, +{ 6, s_6_15, 0, 4, 0}, +{ 5, s_6_16, 0, 2, 0}, +{ 3, s_6_17, 0, 1, 0}, +{ 4, s_6_18, 0, 1, 0}, +{ 5, s_6_19, 0, 1, 0}, +{ 6, s_6_20, -1, 7, 0}, +{ 4, s_6_21, 0, 1, 0}, +{ 3, s_6_22, 0, 9, 0}, +{ 4, s_6_23, 0, 1, 0}, +{ 4, s_6_24, 0, 5, 0}, +{ 3, s_6_25, 0, 1, 0}, +{ 6, s_6_26, -1, 1, 0}, +{ 4, s_6_27, 0, 1, 0}, +{ 5, s_6_28, 0, 1, 0}, +{ 5, s_6_29, 0, 1, 0}, +{ 4, s_6_30, 0, 1, 0}, +{ 6, s_6_31, 0, 4, 0}, +{ 6, s_6_32, 0, 2, 0}, +{ 6, s_6_33, 0, 4, 0}, +{ 5, s_6_34, 0, 2, 0}, +{ 3, s_6_35, 0, 1, 0}, +{ 4, s_6_36, 0, 1, 0}, +{ 6, s_6_37, 0, 6, 0}, +{ 6, s_6_38, 0, 6, 0}, +{ 4, s_6_39, 0, 1, 0}, +{ 3, s_6_40, 0, 9, 0}, +{ 3, s_6_41, 0, 1, 0}, +{ 4, s_6_42, 0, 1, 0}, +{ 3, s_6_43, 0, 1, 0}, +{ 6, s_6_44, 0, 6, 0}, +{ 6, s_6_45, 0, 6, 0}, +{ 3, s_6_46, 0, 9, 0}, +{ 3, s_6_47, 0, 8, 0}, +{ 4, s_6_48, 0, 1, 0}, +{ 4, s_6_49, 0, 1, 0}, +{ 4, s_6_50, 0, 1, 0} }; static const symbol s_7_0[4] = { 'i', 's', 'c', 'a' }; @@ -372,96 +380,94 @@ static const symbol s_7_83[3] = { 'e', 'r', 0xE0 }; static const symbol s_7_84[3] = { 'i', 'r', 0xE0 }; static const symbol s_7_85[3] = { 'e', 'r', 0xF2 }; static const symbol s_7_86[3] = { 'i', 'r', 0xF2 }; - -static const struct among a_7[87] = -{ -{ 4, s_7_0, -1, 1, 0}, -{ 4, s_7_1, -1, 1, 0}, -{ 3, s_7_2, -1, 1, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 3, s_7_4, -1, 1, 0}, -{ 3, s_7_5, -1, 1, 0}, -{ 3, s_7_6, -1, 1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 6, s_7_8, -1, 1, 0}, -{ 6, s_7_9, -1, 1, 0}, -{ 4, s_7_10, -1, 1, 0}, -{ 4, s_7_11, -1, 1, 0}, -{ 3, s_7_12, -1, 1, 0}, -{ 3, s_7_13, -1, 1, 0}, -{ 3, s_7_14, -1, 1, 0}, -{ 4, s_7_15, -1, 1, 0}, -{ 3, s_7_16, -1, 1, 0}, -{ 5, s_7_17, 16, 1, 0}, -{ 5, s_7_18, 16, 1, 0}, -{ 5, s_7_19, 16, 1, 0}, -{ 3, s_7_20, -1, 1, 0}, -{ 5, s_7_21, 20, 1, 0}, -{ 5, s_7_22, 20, 1, 0}, -{ 3, s_7_23, -1, 1, 0}, -{ 6, s_7_24, -1, 1, 0}, -{ 6, s_7_25, -1, 1, 0}, -{ 3, s_7_26, -1, 1, 0}, -{ 4, s_7_27, -1, 1, 0}, -{ 4, s_7_28, -1, 1, 0}, -{ 4, s_7_29, -1, 1, 0}, -{ 4, s_7_30, -1, 1, 0}, -{ 4, s_7_31, -1, 1, 0}, -{ 4, s_7_32, -1, 1, 0}, -{ 4, s_7_33, -1, 1, 0}, -{ 3, s_7_34, -1, 1, 0}, -{ 3, s_7_35, -1, 1, 0}, -{ 6, s_7_36, -1, 1, 0}, -{ 6, s_7_37, -1, 1, 0}, -{ 3, s_7_38, -1, 1, 0}, -{ 3, s_7_39, -1, 1, 0}, -{ 3, s_7_40, -1, 1, 0}, -{ 3, s_7_41, -1, 1, 0}, -{ 4, s_7_42, -1, 1, 0}, -{ 4, s_7_43, -1, 1, 0}, -{ 4, s_7_44, -1, 1, 0}, -{ 4, s_7_45, -1, 1, 0}, -{ 4, s_7_46, -1, 1, 0}, -{ 5, s_7_47, -1, 1, 0}, -{ 5, s_7_48, -1, 1, 0}, -{ 5, s_7_49, -1, 1, 0}, -{ 5, s_7_50, -1, 1, 0}, -{ 5, s_7_51, -1, 1, 0}, -{ 6, s_7_52, -1, 1, 0}, -{ 4, s_7_53, -1, 1, 0}, -{ 4, s_7_54, -1, 1, 0}, -{ 6, s_7_55, 54, 1, 0}, -{ 6, s_7_56, 54, 1, 0}, -{ 4, s_7_57, -1, 1, 0}, -{ 3, s_7_58, -1, 1, 0}, -{ 6, s_7_59, 58, 1, 0}, -{ 5, s_7_60, 58, 1, 0}, -{ 5, s_7_61, 58, 1, 0}, -{ 5, s_7_62, 58, 1, 0}, -{ 6, s_7_63, -1, 1, 0}, -{ 6, s_7_64, -1, 1, 0}, -{ 3, s_7_65, -1, 1, 0}, -{ 6, s_7_66, 65, 1, 0}, -{ 5, s_7_67, 65, 1, 0}, -{ 5, s_7_68, 65, 1, 0}, -{ 5, s_7_69, 65, 1, 0}, -{ 8, s_7_70, -1, 1, 0}, -{ 8, s_7_71, -1, 1, 0}, -{ 6, s_7_72, -1, 1, 0}, -{ 6, s_7_73, -1, 1, 0}, -{ 6, s_7_74, -1, 1, 0}, -{ 3, s_7_75, -1, 1, 0}, -{ 3, s_7_76, -1, 1, 0}, -{ 3, s_7_77, -1, 1, 0}, -{ 3, s_7_78, -1, 1, 0}, -{ 3, s_7_79, -1, 1, 0}, -{ 3, s_7_80, -1, 1, 0}, -{ 2, s_7_81, -1, 1, 0}, -{ 2, s_7_82, -1, 1, 0}, -{ 3, s_7_83, -1, 1, 0}, -{ 3, s_7_84, -1, 1, 0}, -{ 3, s_7_85, -1, 1, 0}, -{ 3, s_7_86, -1, 1, 0} +static const struct among a_7[87] = { +{ 4, s_7_0, 0, 1, 0}, +{ 4, s_7_1, 0, 1, 0}, +{ 3, s_7_2, 0, 1, 0}, +{ 3, s_7_3, 0, 1, 0}, +{ 3, s_7_4, 0, 1, 0}, +{ 3, s_7_5, 0, 1, 0}, +{ 3, s_7_6, 0, 1, 0}, +{ 3, s_7_7, 0, 1, 0}, +{ 6, s_7_8, 0, 1, 0}, +{ 6, s_7_9, 0, 1, 0}, +{ 4, s_7_10, 0, 1, 0}, +{ 4, s_7_11, 0, 1, 0}, +{ 3, s_7_12, 0, 1, 0}, +{ 3, s_7_13, 0, 1, 0}, +{ 3, s_7_14, 0, 1, 0}, +{ 4, s_7_15, 0, 1, 0}, +{ 3, s_7_16, 0, 1, 0}, +{ 5, s_7_17, -1, 1, 0}, +{ 5, s_7_18, -2, 1, 0}, +{ 5, s_7_19, -3, 1, 0}, +{ 3, s_7_20, 0, 1, 0}, +{ 5, s_7_21, -1, 1, 0}, +{ 5, s_7_22, -2, 1, 0}, +{ 3, s_7_23, 0, 1, 0}, +{ 6, s_7_24, 0, 1, 0}, +{ 6, s_7_25, 0, 1, 0}, +{ 3, s_7_26, 0, 1, 0}, +{ 4, s_7_27, 0, 1, 0}, +{ 4, s_7_28, 0, 1, 0}, +{ 4, s_7_29, 0, 1, 0}, +{ 4, s_7_30, 0, 1, 0}, +{ 4, s_7_31, 0, 1, 0}, +{ 4, s_7_32, 0, 1, 0}, +{ 4, s_7_33, 0, 1, 0}, +{ 3, s_7_34, 0, 1, 0}, +{ 3, s_7_35, 0, 1, 0}, +{ 6, s_7_36, 0, 1, 0}, +{ 6, s_7_37, 0, 1, 0}, +{ 3, s_7_38, 0, 1, 0}, +{ 3, s_7_39, 0, 1, 0}, +{ 3, s_7_40, 0, 1, 0}, +{ 3, s_7_41, 0, 1, 0}, +{ 4, s_7_42, 0, 1, 0}, +{ 4, s_7_43, 0, 1, 0}, +{ 4, s_7_44, 0, 1, 0}, +{ 4, s_7_45, 0, 1, 0}, +{ 4, s_7_46, 0, 1, 0}, +{ 5, s_7_47, 0, 1, 0}, +{ 5, s_7_48, 0, 1, 0}, +{ 5, s_7_49, 0, 1, 0}, +{ 5, s_7_50, 0, 1, 0}, +{ 5, s_7_51, 0, 1, 0}, +{ 6, s_7_52, 0, 1, 0}, +{ 4, s_7_53, 0, 1, 0}, +{ 4, s_7_54, 0, 1, 0}, +{ 6, s_7_55, -1, 1, 0}, +{ 6, s_7_56, -2, 1, 0}, +{ 4, s_7_57, 0, 1, 0}, +{ 3, s_7_58, 0, 1, 0}, +{ 6, s_7_59, -1, 1, 0}, +{ 5, s_7_60, -2, 1, 0}, +{ 5, s_7_61, -3, 1, 0}, +{ 5, s_7_62, -4, 1, 0}, +{ 6, s_7_63, 0, 1, 0}, +{ 6, s_7_64, 0, 1, 0}, +{ 3, s_7_65, 0, 1, 0}, +{ 6, s_7_66, -1, 1, 0}, +{ 5, s_7_67, -2, 1, 0}, +{ 5, s_7_68, -3, 1, 0}, +{ 5, s_7_69, -4, 1, 0}, +{ 8, s_7_70, 0, 1, 0}, +{ 8, s_7_71, 0, 1, 0}, +{ 6, s_7_72, 0, 1, 0}, +{ 6, s_7_73, 0, 1, 0}, +{ 6, s_7_74, 0, 1, 0}, +{ 3, s_7_75, 0, 1, 0}, +{ 3, s_7_76, 0, 1, 0}, +{ 3, s_7_77, 0, 1, 0}, +{ 3, s_7_78, 0, 1, 0}, +{ 3, s_7_79, 0, 1, 0}, +{ 3, s_7_80, 0, 1, 0}, +{ 2, s_7_81, 0, 1, 0}, +{ 2, s_7_82, 0, 1, 0}, +{ 3, s_7_83, 0, 1, 0}, +{ 3, s_7_84, 0, 1, 0}, +{ 3, s_7_85, 0, 1, 0}, +{ 3, s_7_86, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 8, 2, 1 }; @@ -470,63 +476,49 @@ static const unsigned char g_AEIO[] = { 17, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, static const unsigned char g_CG[] = { 17 }; -static const symbol s_0[] = { 0xE0 }; -static const symbol s_1[] = { 0xE8 }; -static const symbol s_2[] = { 0xEC }; -static const symbol s_3[] = { 0xF2 }; -static const symbol s_4[] = { 0xF9 }; -static const symbol s_5[] = { 'q', 'U' }; -static const symbol s_6[] = { 'U' }; -static const symbol s_7[] = { 'I' }; -static const symbol s_8[] = { 'i' }; -static const symbol s_9[] = { 'u' }; -static const symbol s_10[] = { 'e' }; -static const symbol s_11[] = { 'i', 'c' }; -static const symbol s_12[] = { 'l', 'o', 'g' }; -static const symbol s_13[] = { 'u' }; -static const symbol s_14[] = { 'e', 'n', 't', 'e' }; -static const symbol s_15[] = { 'a', 't' }; -static const symbol s_16[] = { 'a', 't' }; -static const symbol s_17[] = { 'i', 'c' }; -static const symbol s_18[] = { 'd', 'i', 'v', 'a', 'n', 'o' }; -static const symbol s_19[] = { 'd', 'i', 'v', 'a', 'n' }; - static int r_prelude(struct SN_env * z) { int among_var; - { int c_test1 = z->c; - while(1) { - int c2 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 7); + among_var = find_among(z, a_0, 7, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } break; @@ -537,155 +529,157 @@ static int r_prelude(struct SN_env * z) { } continue; lab0: - z->c = c2; + z->c = v_2; break; } - z->c = c_test1; + z->c = v_1; } - while(1) { - int c3 = z->c; - while(1) { - int c4 = z->c; + while (1) { + int v_3 = z->c; + while (1) { + int v_4 = z->c; if (in_grouping(z, g_v, 97, 249, 0)) goto lab2; z->bra = z->c; - { int c5 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab4; + do { + int v_5 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab3; z->c++; z->ket = z->c; - if (in_grouping(z, g_v, 97, 249, 0)) goto lab4; - { int ret = slice_from_s(z, 1, s_6); + if (in_grouping(z, g_v, 97, 249, 0)) goto lab3; + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } - goto lab3; - lab4: - z->c = c5; + break; + lab3: + z->c = v_5; if (z->c == z->l || z->p[z->c] != 'i') goto lab2; z->c++; z->ket = z->c; if (in_grouping(z, g_v, 97, 249, 0)) goto lab2; - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } - } - lab3: - z->c = c4; + } while (0); + z->c = v_4; break; lab2: - z->c = c4; + z->c = v_4; if (z->c >= z->l) goto lab1; z->c++; } continue; lab1: - z->c = c3; + z->c = v_3; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping(z, g_v, 97, 249, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping(z, g_v, 97, 249, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping(z, g_v, 97, 249, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping(z, g_v, 97, 249, 0)) goto lab2; { int ret = out_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping(z, g_v, 97, 249, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping(z, g_v, 97, 249, 0)) goto lab1; { int ret = in_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } + } while (0); + break; + lab1: + z->c = v_2; + if (!(eq_s(z, 5, s_8))) goto lab3; + break; lab3: - goto lab1; - lab2: - z->c = c2; + z->c = v_2; if (out_grouping(z, g_v, 97, 249, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping(z, g_v, 97, 249, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping(z, g_v, 97, 249, 0)) goto lab4; { int ret = out_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab4; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab4: + z->c = v_4; if (in_grouping(z, g_v, 97, 249, 0)) goto lab0; if (z->c >= z->l) goto lab0; z->c++; - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab5: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 85)) among_var = 3; else - among_var = find_among(z, a_1, 3); + among_var = find_among(z, a_1, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; @@ -696,44 +690,47 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_attached_pronoun(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33314 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_2, 37)) return 0; + if (!find_among_b(z, a_2, 37, 0)) return 0; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 111 && z->p[z->c - 1] != 114)) return 0; - among_var = find_among_b(z, a_3, 5); + among_var = find_among_b(z, a_3, 5, 0); if (!among_var) return 0; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; @@ -744,34 +741,41 @@ static int r_attached_pronoun(struct SN_env * z) { static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_6, 51); + among_var = find_among_b(z, a_6, 51, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_11))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_12))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -779,67 +783,82 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_12); + { + int ret = slice_from_s(z, 3, s_13); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_13); + { + int ret = slice_from_s(z, 1, s_14); if (ret < 0) return ret; } break; case 5: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 4, s_14); + { + int ret = slice_from_s(z, 4, s_15); if (ret < 0) return ret; } break; case 6: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 7: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4722696 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m2; goto lab1; } - among_var = find_among_b(z, a_4, 4); - if (!among_var) { z->c = z->l - m2; goto lab1; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4722696 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_2; goto lab1; } + among_var = find_among_b(z, a_4, 4, 0); + if (!among_var) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } switch (among_var) { case 1: z->ket = z->c; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m2; goto lab1; } + if (!(eq_s_b(z, 2, s_16))) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -849,22 +868,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m3; goto lab2; } - if (!find_among_b(z, a_5, 3)) { z->c = z->l - m3; goto lab2; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_3; goto lab2; } + if (!find_among_b(z, a_5, 3, 0)) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab2; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -872,31 +896,38 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 9: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_16))) { z->c = z->l - m4; goto lab3; } + if (!(eq_s_b(z, 2, s_17))) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (!(eq_s_b(z, 2, s_17))) { z->c = z->l - m4; goto lab3; } + if (!(eq_s_b(z, 2, s_18))) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab3: @@ -908,58 +939,67 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (!find_among_b(z, a_7, 87)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_7, 87, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_vowel_suffix(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (in_grouping_b(z, g_AEIO, 97, 242, 0)) { z->c = z->l - m1; goto lab0; } + if (in_grouping_b(z, g_AEIO, 97, 242, 0)) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'i') { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] != 'i') { z->c = z->l - v_1; goto lab0; } z->c--; z->bra = z->c; - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: ; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'h') { z->c = z->l - m2; goto lab1; } + if (z->c <= z->lb || z->p[z->c - 1] != 'h') { z->c = z->l - v_2; goto lab1; } z->c--; z->bra = z->c; - if (in_grouping_b(z, g_CG, 99, 103, 0)) { z->c = z->l - m2; goto lab1; } - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + if (in_grouping_b(z, g_CG, 99, 103, 0)) { z->c = z->l - v_2; goto lab1; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab1: @@ -968,81 +1008,80 @@ static int r_vowel_suffix(struct SN_env * z) { return 1; } -static int r_exceptions(struct SN_env * z) { - z->bra = z->c; - if (!(eq_s(z, 6, s_18))) return 0; - if (z->c < z->l) return 0; - z->ket = z->c; - { int ret = slice_from_s(z, 5, s_19); - if (ret < 0) return ret; - } - return 1; -} - extern int italian_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_exceptions(z); - if (ret == 0) goto lab1; + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = c1; - { int c2 = z->c; - { int ret = r_prelude(z); - if (ret < 0) return ret; - } - z->c = c2; - } - - { int ret = r_mark_regions(z); + z->c = v_1; + } + { + int ret = r_mark_regions(z); + if (ret < 0) return ret; + } + z->lb = z->c; z->c = z->l; + { + int v_2 = z->l - z->c; + { + int ret = r_attached_pronoun(z); if (ret < 0) return ret; } - z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_attached_pronoun(z); + z->c = z->l - v_2; + } + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - z->c = z->l - m3; - } - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab4; - if (ret < 0) return ret; - } - goto lab3; - lab4: - z->c = z->l - m5; - { int ret = r_verb_suffix(z); - if (ret == 0) goto lab2; - if (ret < 0) return ret; - } - } - lab3: - lab2: - z->c = z->l - m4; - } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_vowel_suffix(z); + break; + lab1: + z->c = z->l - v_4; + { + int ret = r_verb_suffix(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - z->c = z->l - m6; + } while (0); + lab0: + z->c = z->l - v_3; + } + { + int v_5 = z->l - z->c; + { + int ret = r_vowel_suffix(z); + if (ret < 0) return ret; } - z->c = z->lb; - { int c7 = z->c; - { int ret = r_postlude(z); - if (ret < 0) return ret; - } - z->c = c7; + z->c = z->l - v_5; + } + z->c = z->lb; + { + int v_6 = z->c; + { + int ret = r_postlude(z); + if (ret < 0) return ret; } + z->c = v_6; } -lab0: return 1; } -extern struct SN_env * italian_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * italian_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void italian_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void italian_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_norwegian.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_norwegian.c index c10c0d761f1f5..66978be21d82d 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_norwegian.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_norwegian.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from norwegian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_norwegian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,187 +20,234 @@ extern int norwegian_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_other_suffix(struct SN_env * z); static int r_consonant_pair(struct SN_env * z); static int r_main_suffix(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - -extern struct SN_env * norwegian_ISO_8859_1_create_env(void); -extern void norwegian_ISO_8859_1_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[1] = { 'a' }; -static const symbol s_0_1[1] = { 'e' }; -static const symbol s_0_2[3] = { 'e', 'd', 'e' }; -static const symbol s_0_3[4] = { 'a', 'n', 'd', 'e' }; -static const symbol s_0_4[4] = { 'e', 'n', 'd', 'e' }; -static const symbol s_0_5[3] = { 'a', 'n', 'e' }; -static const symbol s_0_6[3] = { 'e', 'n', 'e' }; -static const symbol s_0_7[6] = { 'h', 'e', 't', 'e', 'n', 'e' }; -static const symbol s_0_8[4] = { 'e', 'r', 't', 'e' }; -static const symbol s_0_9[2] = { 'e', 'n' }; -static const symbol s_0_10[5] = { 'h', 'e', 't', 'e', 'n' }; -static const symbol s_0_11[2] = { 'a', 'r' }; -static const symbol s_0_12[2] = { 'e', 'r' }; -static const symbol s_0_13[5] = { 'h', 'e', 't', 'e', 'r' }; -static const symbol s_0_14[1] = { 's' }; -static const symbol s_0_15[2] = { 'a', 's' }; -static const symbol s_0_16[2] = { 'e', 's' }; -static const symbol s_0_17[4] = { 'e', 'd', 'e', 's' }; -static const symbol s_0_18[5] = { 'e', 'n', 'd', 'e', 's' }; -static const symbol s_0_19[4] = { 'e', 'n', 'e', 's' }; -static const symbol s_0_20[7] = { 'h', 'e', 't', 'e', 'n', 'e', 's' }; -static const symbol s_0_21[3] = { 'e', 'n', 's' }; -static const symbol s_0_22[6] = { 'h', 'e', 't', 'e', 'n', 's' }; -static const symbol s_0_23[3] = { 'e', 'r', 's' }; -static const symbol s_0_24[3] = { 'e', 't', 's' }; -static const symbol s_0_25[2] = { 'e', 't' }; -static const symbol s_0_26[3] = { 'h', 'e', 't' }; -static const symbol s_0_27[3] = { 'e', 'r', 't' }; -static const symbol s_0_28[3] = { 'a', 's', 't' }; +static const symbol s_0[] = { 'e', 'r' }; -static const struct among a_0[29] = -{ -{ 1, s_0_0, -1, 1, 0}, -{ 1, s_0_1, -1, 1, 0}, -{ 3, s_0_2, 1, 1, 0}, -{ 4, s_0_3, 1, 1, 0}, -{ 4, s_0_4, 1, 1, 0}, -{ 3, s_0_5, 1, 1, 0}, -{ 3, s_0_6, 1, 1, 0}, -{ 6, s_0_7, 6, 1, 0}, -{ 4, s_0_8, 1, 3, 0}, -{ 2, s_0_9, -1, 1, 0}, -{ 5, s_0_10, 9, 1, 0}, -{ 2, s_0_11, -1, 1, 0}, -{ 2, s_0_12, -1, 1, 0}, -{ 5, s_0_13, 12, 1, 0}, -{ 1, s_0_14, -1, 2, 0}, -{ 2, s_0_15, 14, 1, 0}, -{ 2, s_0_16, 14, 1, 0}, -{ 4, s_0_17, 16, 1, 0}, -{ 5, s_0_18, 16, 1, 0}, -{ 4, s_0_19, 16, 1, 0}, -{ 7, s_0_20, 19, 1, 0}, -{ 3, s_0_21, 14, 1, 0}, -{ 6, s_0_22, 21, 1, 0}, -{ 3, s_0_23, 14, 1, 0}, -{ 3, s_0_24, 14, 1, 0}, -{ 2, s_0_25, -1, 1, 0}, -{ 3, s_0_26, 25, 1, 0}, -{ 3, s_0_27, -1, 3, 0}, -{ 3, s_0_28, -1, 1, 0} +static const symbol s_0_1[3] = { 'i', 'n', 'd' }; +static const symbol s_0_2[2] = { 'k', 'k' }; +static const symbol s_0_3[2] = { 'n', 'k' }; +static const symbol s_0_4[3] = { 'a', 'm', 'm' }; +static const symbol s_0_5[3] = { 'o', 'm', 'm' }; +static const symbol s_0_6[3] = { 'k', 'a', 'p' }; +static const symbol s_0_7[4] = { 's', 'k', 'a', 'p' }; +static const symbol s_0_8[2] = { 'p', 'p' }; +static const symbol s_0_9[2] = { 'l', 't' }; +static const symbol s_0_10[3] = { 'a', 's', 't' }; +static const symbol s_0_11[3] = { 0xF8, 's', 't' }; +static const symbol s_0_12[1] = { 'v' }; +static const symbol s_0_13[3] = { 'h', 'a', 'v' }; +static const symbol s_0_14[3] = { 'g', 'i', 'v' }; +static const struct among a_0[15] = { +{ 0, 0, 0, 1, 0}, +{ 3, s_0_1, -1, -1, 0}, +{ 2, s_0_2, -2, -1, 0}, +{ 2, s_0_3, -3, -1, 0}, +{ 3, s_0_4, -4, -1, 0}, +{ 3, s_0_5, -5, -1, 0}, +{ 3, s_0_6, -6, -1, 0}, +{ 4, s_0_7, -1, 1, 0}, +{ 2, s_0_8, -8, -1, 0}, +{ 2, s_0_9, -9, -1, 0}, +{ 3, s_0_10, -10, -1, 0}, +{ 3, s_0_11, -11, -1, 0}, +{ 1, s_0_12, -12, -1, 0}, +{ 3, s_0_13, -1, 1, 0}, +{ 3, s_0_14, -2, 1, 0} }; -static const symbol s_1_0[2] = { 'd', 't' }; -static const symbol s_1_1[2] = { 'v', 't' }; - -static const struct among a_1[2] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0} +static const symbol s_1_0[1] = { 'a' }; +static const symbol s_1_1[1] = { 'e' }; +static const symbol s_1_2[3] = { 'e', 'd', 'e' }; +static const symbol s_1_3[4] = { 'a', 'n', 'd', 'e' }; +static const symbol s_1_4[4] = { 'e', 'n', 'd', 'e' }; +static const symbol s_1_5[3] = { 'a', 'n', 'e' }; +static const symbol s_1_6[3] = { 'e', 'n', 'e' }; +static const symbol s_1_7[6] = { 'h', 'e', 't', 'e', 'n', 'e' }; +static const symbol s_1_8[4] = { 'e', 'r', 't', 'e' }; +static const symbol s_1_9[2] = { 'e', 'n' }; +static const symbol s_1_10[5] = { 'h', 'e', 't', 'e', 'n' }; +static const symbol s_1_11[2] = { 'a', 'r' }; +static const symbol s_1_12[2] = { 'e', 'r' }; +static const symbol s_1_13[5] = { 'h', 'e', 't', 'e', 'r' }; +static const symbol s_1_14[1] = { 's' }; +static const symbol s_1_15[2] = { 'a', 's' }; +static const symbol s_1_16[2] = { 'e', 's' }; +static const symbol s_1_17[4] = { 'e', 'd', 'e', 's' }; +static const symbol s_1_18[5] = { 'e', 'n', 'd', 'e', 's' }; +static const symbol s_1_19[4] = { 'e', 'n', 'e', 's' }; +static const symbol s_1_20[7] = { 'h', 'e', 't', 'e', 'n', 'e', 's' }; +static const symbol s_1_21[3] = { 'e', 'n', 's' }; +static const symbol s_1_22[6] = { 'h', 'e', 't', 'e', 'n', 's' }; +static const symbol s_1_23[3] = { 'e', 'r', 's' }; +static const symbol s_1_24[3] = { 'e', 't', 's' }; +static const symbol s_1_25[2] = { 'e', 't' }; +static const symbol s_1_26[3] = { 'h', 'e', 't' }; +static const symbol s_1_27[3] = { 'e', 'r', 't' }; +static const symbol s_1_28[3] = { 'a', 's', 't' }; +static const struct among a_1[29] = { +{ 1, s_1_0, 0, 1, 0}, +{ 1, s_1_1, 0, 1, 0}, +{ 3, s_1_2, -1, 1, 0}, +{ 4, s_1_3, -2, 1, 0}, +{ 4, s_1_4, -3, 1, 0}, +{ 3, s_1_5, -4, 1, 0}, +{ 3, s_1_6, -5, 1, 0}, +{ 6, s_1_7, -1, 1, 0}, +{ 4, s_1_8, -7, 4, 0}, +{ 2, s_1_9, 0, 1, 0}, +{ 5, s_1_10, -1, 1, 0}, +{ 2, s_1_11, 0, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 5, s_1_13, -1, 1, 0}, +{ 1, s_1_14, 0, 3, 0}, +{ 2, s_1_15, -1, 1, 0}, +{ 2, s_1_16, -2, 1, 0}, +{ 4, s_1_17, -1, 1, 0}, +{ 5, s_1_18, -2, 1, 0}, +{ 4, s_1_19, -3, 1, 0}, +{ 7, s_1_20, -1, 1, 0}, +{ 3, s_1_21, -7, 1, 0}, +{ 6, s_1_22, -1, 1, 0}, +{ 3, s_1_23, -9, 2, 0}, +{ 3, s_1_24, -10, 1, 0}, +{ 2, s_1_25, 0, 1, 0}, +{ 3, s_1_26, -1, 1, 0}, +{ 3, s_1_27, 0, 4, 0}, +{ 3, s_1_28, 0, 1, 0} }; -static const symbol s_2_0[3] = { 'l', 'e', 'g' }; -static const symbol s_2_1[4] = { 'e', 'l', 'e', 'g' }; -static const symbol s_2_2[2] = { 'i', 'g' }; -static const symbol s_2_3[3] = { 'e', 'i', 'g' }; -static const symbol s_2_4[3] = { 'l', 'i', 'g' }; -static const symbol s_2_5[4] = { 'e', 'l', 'i', 'g' }; -static const symbol s_2_6[3] = { 'e', 'l', 's' }; -static const symbol s_2_7[3] = { 'l', 'o', 'v' }; -static const symbol s_2_8[4] = { 'e', 'l', 'o', 'v' }; -static const symbol s_2_9[4] = { 's', 'l', 'o', 'v' }; -static const symbol s_2_10[7] = { 'h', 'e', 't', 's', 'l', 'o', 'v' }; - -static const struct among a_2[11] = -{ -{ 3, s_2_0, -1, 1, 0}, -{ 4, s_2_1, 0, 1, 0}, -{ 2, s_2_2, -1, 1, 0}, -{ 3, s_2_3, 2, 1, 0}, -{ 3, s_2_4, 2, 1, 0}, -{ 4, s_2_5, 4, 1, 0}, -{ 3, s_2_6, -1, 1, 0}, -{ 3, s_2_7, -1, 1, 0}, -{ 4, s_2_8, 7, 1, 0}, -{ 4, s_2_9, 7, 1, 0}, -{ 7, s_2_10, 9, 1, 0} +static const symbol s_2_0[2] = { 'd', 't' }; +static const symbol s_2_1[2] = { 'v', 't' }; +static const struct among a_2[2] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0} }; -static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 128 }; +static const symbol s_3_0[3] = { 'l', 'e', 'g' }; +static const symbol s_3_1[4] = { 'e', 'l', 'e', 'g' }; +static const symbol s_3_2[2] = { 'i', 'g' }; +static const symbol s_3_3[3] = { 'e', 'i', 'g' }; +static const symbol s_3_4[3] = { 'l', 'i', 'g' }; +static const symbol s_3_5[4] = { 'e', 'l', 'i', 'g' }; +static const symbol s_3_6[3] = { 'e', 'l', 's' }; +static const symbol s_3_7[3] = { 'l', 'o', 'v' }; +static const symbol s_3_8[4] = { 'e', 'l', 'o', 'v' }; +static const symbol s_3_9[4] = { 's', 'l', 'o', 'v' }; +static const symbol s_3_10[7] = { 'h', 'e', 't', 's', 'l', 'o', 'v' }; +static const struct among a_3[11] = { +{ 3, s_3_0, 0, 1, 0}, +{ 4, s_3_1, -1, 1, 0}, +{ 2, s_3_2, 0, 1, 0}, +{ 3, s_3_3, -1, 1, 0}, +{ 3, s_3_4, -2, 1, 0}, +{ 4, s_3_5, -1, 1, 0}, +{ 3, s_3_6, 0, 1, 0}, +{ 3, s_3_7, 0, 1, 0}, +{ 4, s_3_8, -1, 1, 0}, +{ 4, s_3_9, -2, 1, 0}, +{ 7, s_3_10, -1, 1, 0} +}; -static const unsigned char g_s_ending[] = { 119, 125, 149, 1 }; +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 2, 142 }; -static const symbol s_0[] = { 'e', 'r' }; +static const unsigned char g_s_ending[] = { 119, 125, 148, 1 }; static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c_test1 = z->c; -z->c = z->c + 3; - if (z->c > z->l) return 0; - z->I[0] = z->c; - z->c = c_test1; + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping(z, g_v, 97, 248, 1); + if (ret < 0) return 0; + z->c += ret; } - - if (out_grouping(z, g_v, 97, 248, 1) < 0) return 0; - { int ret = in_grouping(z, g_v, 97, 248, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab0; - z->I[1] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: return 1; } static int r_main_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851426 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 29); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851426 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_1, 29, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m2 = z->l - z->c; (void)m2; - if (in_grouping_b(z, g_s_ending, 98, 122, 0)) goto lab1; - goto lab0; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((5318672 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 1; else + among_var = find_among_b(z, a_0, 15, 0); + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + break; + case 3: + do { + int v_2 = z->l - z->c; + if (in_grouping_b(z, g_s_ending, 98, 122, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'r') goto lab1; + z->c--; + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab2; + z->c--; + goto lab1; + lab2: + z->c = z->l - v_3; + } + break; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; if (z->c <= z->lb || z->p[z->c - 1] != 'k') return 0; z->c--; if (out_grouping_b(z, g_v, 97, 248, 0)) return 0; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 3: - { int ret = slice_from_s(z, 2, s_0); + case 4: + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; @@ -198,77 +256,95 @@ static int r_main_suffix(struct SN_env * z) { } static int r_consonant_pair(struct SN_env * z) { - { int m_test1 = z->l - z->c; - - { int mlimit2; - if (z->c < z->I[1]) return 0; - mlimit2 = z->lb; z->lb = z->I[1]; + { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 116) { z->lb = mlimit2; return 0; } - if (!find_among_b(z, a_1, 2)) { z->lb = mlimit2; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 116) { z->lb = v_2; return 0; } + if (!find_among_b(z, a_2, 2, 0)) { z->lb = v_2; return 0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } - z->c = z->l - m_test1; + z->c = z->l - v_1; } if (z->c <= z->lb) return 0; z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_other_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718720 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_2, 11)) { z->lb = mlimit1; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718720 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_3, 11, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int norwegian_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_main_suffix(z); + { + int v_2 = z->l - z->c; + { + int ret = r_main_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_other_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_other_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; return 1; } -extern struct SN_env * norwegian_ISO_8859_1_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * norwegian_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void norwegian_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void norwegian_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_porter.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_porter.c index b8be0a26e4f0a..e7f9be15fc5b2 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_porter.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_porter.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_porter.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int porter_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_Step_5b(struct SN_env * z); static int r_Step_5a(struct SN_env * z); static int r_Step_4(struct SN_env * z); @@ -20,29 +33,41 @@ static int r_Step_1a(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_shortv(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * porter_ISO_8859_1_create_env(void); -extern void porter_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 's', 's' }; +static const symbol s_1[] = { 'i' }; +static const symbol s_2[] = { 'e', 'e' }; +static const symbol s_3[] = { 'e' }; +static const symbol s_4[] = { 'e' }; +static const symbol s_5[] = { 'i' }; +static const symbol s_6[] = { 't', 'i', 'o', 'n' }; +static const symbol s_7[] = { 'e', 'n', 'c', 'e' }; +static const symbol s_8[] = { 'a', 'n', 'c', 'e' }; +static const symbol s_9[] = { 'a', 'b', 'l', 'e' }; +static const symbol s_10[] = { 'e', 'n', 't' }; +static const symbol s_11[] = { 'e' }; +static const symbol s_12[] = { 'i', 'z', 'e' }; +static const symbol s_13[] = { 'a', 't', 'e' }; +static const symbol s_14[] = { 'a', 'l' }; +static const symbol s_15[] = { 'f', 'u', 'l' }; +static const symbol s_16[] = { 'o', 'u', 's' }; +static const symbol s_17[] = { 'i', 'v', 'e' }; +static const symbol s_18[] = { 'b', 'l', 'e' }; +static const symbol s_19[] = { 'a', 'l' }; +static const symbol s_20[] = { 'i', 'c' }; +static const symbol s_21[] = { 'Y' }; +static const symbol s_22[] = { 'Y' }; +static const symbol s_23[] = { 'y' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[1] = { 's' }; static const symbol s_0_1[3] = { 'i', 'e', 's' }; static const symbol s_0_2[4] = { 's', 's', 'e', 's' }; static const symbol s_0_3[2] = { 's', 's' }; - -static const struct among a_0[4] = -{ -{ 1, s_0_0, -1, 3, 0}, -{ 3, s_0_1, 0, 2, 0}, -{ 4, s_0_2, 0, 1, 0}, -{ 2, s_0_3, 0, -1, 0} +static const struct among a_0[4] = { +{ 1, s_0_0, 0, 3, 0}, +{ 3, s_0_1, -1, 2, 0}, +{ 4, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, -1, 0} }; static const symbol s_1_1[2] = { 'b', 'b' }; @@ -57,33 +82,29 @@ static const symbol s_1_9[2] = { 'r', 'r' }; static const symbol s_1_10[2] = { 'a', 't' }; static const symbol s_1_11[2] = { 't', 't' }; static const symbol s_1_12[2] = { 'i', 'z' }; - -static const struct among a_1[13] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_1_1, 0, 2, 0}, -{ 2, s_1_2, 0, 2, 0}, -{ 2, s_1_3, 0, 2, 0}, -{ 2, s_1_4, 0, 2, 0}, -{ 2, s_1_5, 0, 1, 0}, -{ 2, s_1_6, 0, 2, 0}, -{ 2, s_1_7, 0, 2, 0}, -{ 2, s_1_8, 0, 2, 0}, -{ 2, s_1_9, 0, 2, 0}, -{ 2, s_1_10, 0, 1, 0}, -{ 2, s_1_11, 0, 2, 0}, -{ 2, s_1_12, 0, 1, 0} +static const struct among a_1[13] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_1_1, -1, 2, 0}, +{ 2, s_1_2, -2, 2, 0}, +{ 2, s_1_3, -3, 2, 0}, +{ 2, s_1_4, -4, 2, 0}, +{ 2, s_1_5, -5, 1, 0}, +{ 2, s_1_6, -6, 2, 0}, +{ 2, s_1_7, -7, 2, 0}, +{ 2, s_1_8, -8, 2, 0}, +{ 2, s_1_9, -9, 2, 0}, +{ 2, s_1_10, -10, 1, 0}, +{ 2, s_1_11, -11, 2, 0}, +{ 2, s_1_12, -12, 1, 0} }; static const symbol s_2_0[2] = { 'e', 'd' }; static const symbol s_2_1[3] = { 'e', 'e', 'd' }; static const symbol s_2_2[3] = { 'i', 'n', 'g' }; - -static const struct among a_2[3] = -{ -{ 2, s_2_0, -1, 2, 0}, -{ 3, s_2_1, 0, 1, 0}, -{ 3, s_2_2, -1, 2, 0} +static const struct among a_2[3] = { +{ 2, s_2_0, 0, 2, 0}, +{ 3, s_2_1, -1, 1, 0}, +{ 3, s_2_2, 0, 2, 0} }; static const symbol s_3_0[4] = { 'a', 'n', 'c', 'i' }; @@ -106,29 +127,27 @@ static const symbol s_3_16[4] = { 'a', 't', 'o', 'r' }; static const symbol s_3_17[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; static const symbol s_3_18[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; static const symbol s_3_19[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; - -static const struct among a_3[20] = -{ -{ 4, s_3_0, -1, 3, 0}, -{ 4, s_3_1, -1, 2, 0}, -{ 4, s_3_2, -1, 4, 0}, -{ 3, s_3_3, -1, 6, 0}, -{ 4, s_3_4, -1, 9, 0}, -{ 5, s_3_5, -1, 11, 0}, -{ 5, s_3_6, -1, 5, 0}, -{ 5, s_3_7, -1, 9, 0}, -{ 6, s_3_8, -1, 13, 0}, -{ 5, s_3_9, -1, 12, 0}, -{ 6, s_3_10, -1, 1, 0}, -{ 7, s_3_11, 10, 8, 0}, -{ 5, s_3_12, -1, 9, 0}, -{ 5, s_3_13, -1, 8, 0}, -{ 7, s_3_14, 13, 7, 0}, -{ 4, s_3_15, -1, 7, 0}, -{ 4, s_3_16, -1, 8, 0}, -{ 7, s_3_17, -1, 12, 0}, -{ 7, s_3_18, -1, 10, 0}, -{ 7, s_3_19, -1, 11, 0} +static const struct among a_3[20] = { +{ 4, s_3_0, 0, 3, 0}, +{ 4, s_3_1, 0, 2, 0}, +{ 4, s_3_2, 0, 4, 0}, +{ 3, s_3_3, 0, 6, 0}, +{ 4, s_3_4, 0, 9, 0}, +{ 5, s_3_5, 0, 11, 0}, +{ 5, s_3_6, 0, 5, 0}, +{ 5, s_3_7, 0, 9, 0}, +{ 6, s_3_8, 0, 13, 0}, +{ 5, s_3_9, 0, 12, 0}, +{ 6, s_3_10, 0, 1, 0}, +{ 7, s_3_11, -1, 8, 0}, +{ 5, s_3_12, 0, 9, 0}, +{ 5, s_3_13, 0, 8, 0}, +{ 7, s_3_14, -1, 7, 0}, +{ 4, s_3_15, 0, 7, 0}, +{ 4, s_3_16, 0, 8, 0}, +{ 7, s_3_17, 0, 12, 0}, +{ 7, s_3_18, 0, 10, 0}, +{ 7, s_3_19, 0, 11, 0} }; static const symbol s_4_0[5] = { 'i', 'c', 'a', 't', 'e' }; @@ -138,16 +157,14 @@ static const symbol s_4_3[5] = { 'i', 'c', 'i', 't', 'i' }; static const symbol s_4_4[4] = { 'i', 'c', 'a', 'l' }; static const symbol s_4_5[3] = { 'f', 'u', 'l' }; static const symbol s_4_6[4] = { 'n', 'e', 's', 's' }; - -static const struct among a_4[7] = -{ -{ 5, s_4_0, -1, 2, 0}, -{ 5, s_4_1, -1, 3, 0}, -{ 5, s_4_2, -1, 1, 0}, -{ 5, s_4_3, -1, 2, 0}, -{ 4, s_4_4, -1, 2, 0}, -{ 3, s_4_5, -1, 3, 0}, -{ 4, s_4_6, -1, 3, 0} +static const struct among a_4[7] = { +{ 5, s_4_0, 0, 2, 0}, +{ 5, s_4_1, 0, 3, 0}, +{ 5, s_4_2, 0, 1, 0}, +{ 5, s_4_3, 0, 2, 0}, +{ 4, s_4_4, 0, 2, 0}, +{ 3, s_4_5, 0, 3, 0}, +{ 4, s_4_6, 0, 3, 0} }; static const symbol s_5_0[2] = { 'i', 'c' }; @@ -169,94 +186,69 @@ static const symbol s_5_15[3] = { 'e', 'n', 't' }; static const symbol s_5_16[4] = { 'm', 'e', 'n', 't' }; static const symbol s_5_17[5] = { 'e', 'm', 'e', 'n', 't' }; static const symbol s_5_18[2] = { 'o', 'u' }; - -static const struct among a_5[19] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 4, s_5_2, -1, 1, 0}, -{ 4, s_5_3, -1, 1, 0}, -{ 4, s_5_4, -1, 1, 0}, -{ 3, s_5_5, -1, 1, 0}, -{ 3, s_5_6, -1, 1, 0}, -{ 3, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 1, 0}, -{ 2, s_5_9, -1, 1, 0}, -{ 3, s_5_10, -1, 1, 0}, -{ 3, s_5_11, -1, 2, 0}, -{ 2, s_5_12, -1, 1, 0}, -{ 3, s_5_13, -1, 1, 0}, -{ 3, s_5_14, -1, 1, 0}, -{ 3, s_5_15, -1, 1, 0}, -{ 4, s_5_16, 15, 1, 0}, -{ 5, s_5_17, 16, 1, 0}, -{ 2, s_5_18, -1, 1, 0} +static const struct among a_5[19] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 4, s_5_2, 0, 1, 0}, +{ 4, s_5_3, 0, 1, 0}, +{ 4, s_5_4, 0, 1, 0}, +{ 3, s_5_5, 0, 1, 0}, +{ 3, s_5_6, 0, 1, 0}, +{ 3, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 1, 0}, +{ 2, s_5_9, 0, 1, 0}, +{ 3, s_5_10, 0, 1, 0}, +{ 3, s_5_11, 0, 2, 0}, +{ 2, s_5_12, 0, 1, 0}, +{ 3, s_5_13, 0, 1, 0}, +{ 3, s_5_14, 0, 1, 0}, +{ 3, s_5_15, 0, 1, 0}, +{ 4, s_5_16, -1, 1, 0}, +{ 5, s_5_17, -1, 1, 0}, +{ 2, s_5_18, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1 }; static const unsigned char g_v_WXY[] = { 1, 17, 65, 208, 1 }; -static const symbol s_0[] = { 's', 's' }; -static const symbol s_1[] = { 'i' }; -static const symbol s_2[] = { 'e', 'e' }; -static const symbol s_3[] = { 'e' }; -static const symbol s_4[] = { 'e' }; -static const symbol s_5[] = { 'i' }; -static const symbol s_6[] = { 't', 'i', 'o', 'n' }; -static const symbol s_7[] = { 'e', 'n', 'c', 'e' }; -static const symbol s_8[] = { 'a', 'n', 'c', 'e' }; -static const symbol s_9[] = { 'a', 'b', 'l', 'e' }; -static const symbol s_10[] = { 'e', 'n', 't' }; -static const symbol s_11[] = { 'e' }; -static const symbol s_12[] = { 'i', 'z', 'e' }; -static const symbol s_13[] = { 'a', 't', 'e' }; -static const symbol s_14[] = { 'a', 'l' }; -static const symbol s_15[] = { 'f', 'u', 'l' }; -static const symbol s_16[] = { 'o', 'u', 's' }; -static const symbol s_17[] = { 'i', 'v', 'e' }; -static const symbol s_18[] = { 'b', 'l', 'e' }; -static const symbol s_19[] = { 'a', 'l' }; -static const symbol s_20[] = { 'i', 'c' }; -static const symbol s_21[] = { 'Y' }; -static const symbol s_22[] = { 'Y' }; -static const symbol s_23[] = { 'y' }; - static int r_shortv(struct SN_env * z) { if (out_grouping_b(z, g_v_WXY, 89, 121, 0)) return 0; if (in_grouping_b(z, g_v, 97, 121, 0)) return 0; - if (out_grouping_b(z, g_v, 97, 121, 0)) return 0; - return 1; + return !out_grouping_b(z, g_v, 97, 121, 0); } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_Step_1a(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 115) return 0; - among_var = find_among_b(z, a_0, 4); + among_var = find_among_b(z, a_0, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -268,43 +260,46 @@ static int r_Step_1b(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 103)) return 0; - among_var = find_among_b(z, a_2, 3); + among_var = find_among_b(z, a_2, 3, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 2: - { int m_test1 = z->l - z->c; - + { + int v_1 = z->l - z->c; { int ret = out_grouping_b(z, g_v, 97, 121, 1); if (ret < 0) return 0; z->c -= ret; } - z->c = z->l - m_test1; + z->c = z->l - v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m_test2 = z->l - z->c; + { + int v_2 = z->l - z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else - among_var = find_among_b(z, a_1, 13); - z->c = z->l - m_test2; + among_var = find_among_b(z, a_1, 13, 0); + z->c = z->l - v_2; } switch (among_var) { case 1: - { int ret; - { int saved_c = z->c; - ret = insert_s(z, z->c, z->c, 1, s_3); - z->c = saved_c; - } + { + int saved_c = z->c; + int ret = insert_s(z, z->c, z->c, 1, s_3); + z->c = saved_c; if (ret < 0) return ret; } break; @@ -313,23 +308,25 @@ static int r_Step_1b(struct SN_env * z) { if (z->c <= z->lb) return 0; z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - if (z->c != z->I[1]) return 0; - { int m_test3 = z->l - z->c; - { int ret = r_shortv(z); + if (z->c != ((SN_local *)z)->i_p1) return 0; + { + int v_3 = z->l - z->c; + { + int ret = r_shortv(z); if (ret <= 0) return ret; } - z->c = z->l - m_test3; + z->c = z->l - v_3; } - { int ret; - { int saved_c = z->c; - ret = insert_s(z, z->c, z->c, 1, s_4); - z->c = saved_c; - } + { + int saved_c = z->c; + int ret = insert_s(z, z->c, z->c, 1, s_4); + z->c = saved_c; if (ret < 0) return ret; } break; @@ -341,24 +338,24 @@ static int r_Step_1b(struct SN_env * z) { static int r_Step_1c(struct SN_env * z) { z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'Y') return 0; z->c--; - } -lab0: + } while (0); z->bra = z->c; - { int ret = out_grouping_b(z, g_v, 97, 121, 1); if (ret < 0) return 0; z->c -= ret; } - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } return 1; @@ -368,75 +365,89 @@ static int r_Step_2(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((815616 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_3, 20); + among_var = find_among_b(z, a_3, 20, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_6); + { + int ret = slice_from_s(z, 4, s_6); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_7); + { + int ret = slice_from_s(z, 4, s_7); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_8); + { + int ret = slice_from_s(z, 4, s_8); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_9); + { + int ret = slice_from_s(z, 4, s_9); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_10); + { + int ret = slice_from_s(z, 3, s_10); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_12); + { + int ret = slice_from_s(z, 3, s_12); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 3, s_13); + { + int ret = slice_from_s(z, 3, s_13); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 2, s_14); + { + int ret = slice_from_s(z, 2, s_14); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 3, s_15); + { + int ret = slice_from_s(z, 3, s_15); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 3, s_16); + { + int ret = slice_from_s(z, 3, s_16); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 3, s_17); + { + int ret = slice_from_s(z, 3, s_17); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 3, s_18); + { + int ret = slice_from_s(z, 3, s_18); if (ret < 0) return ret; } break; @@ -448,25 +459,29 @@ static int r_Step_3(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((528928 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_4, 7); + among_var = find_among_b(z, a_4, 7, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_19); + { + int ret = slice_from_s(z, 2, s_19); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_20); + { + int ret = slice_from_s(z, 2, s_20); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -478,30 +493,33 @@ static int r_Step_4(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((3961384 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_5, 19); + among_var = find_among_b(z, a_5, 19, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 't') return 0; z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -514,27 +532,32 @@ static int r_Step_5a(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; z->c--; z->bra = z->c; - - { int ret = r_R2(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - goto lab0; -lab1: - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m1 = z->l - z->c; (void)m1; - { int ret = r_shortv(z); - if (ret == 0) goto lab2; + do { + { + int ret = r_R2(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - return 0; - lab2: - z->c = z->l - m1; - } -lab0: - { int ret = slice_del(z); + break; + lab0: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + { + int ret = r_shortv(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + return 0; + lab1: + z->c = z->l - v_1; + } + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -545,174 +568,204 @@ static int r_Step_5b(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; z->c--; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int porter_ISO_8859_1_stem(struct SN_env * z) { - z->I[2] = 0; - { int c1 = z->c; + int b_Y_found; + b_Y_found = 0; + { + int v_1 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab0; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_21); + { + int ret = slice_from_s(z, 1, s_21); if (ret < 0) return ret; } - z->I[2] = 1; + b_Y_found = 1; lab0: - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - while(1) { - int c3 = z->c; - while(1) { - int c4 = z->c; + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + while (1) { + int v_4 = z->c; if (in_grouping(z, g_v, 97, 121, 0)) goto lab3; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab3; z->c++; z->ket = z->c; - z->c = c4; + z->c = v_4; break; lab3: - z->c = c4; + z->c = v_4; if (z->c >= z->l) goto lab2; z->c++; } - { int ret = slice_from_s(z, 1, s_22); + { + int ret = slice_from_s(z, 1, s_22); if (ret < 0) return ret; } - z->I[2] = 1; + b_Y_found = 1; continue; lab2: - z->c = c3; + z->c = v_3; break; } - z->c = c2; + z->c = v_2; } - z->I[1] = z->l; - z->I[0] = z->l; - { int c5 = z->c; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_5 = z->c; { int ret = out_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab4: - z->c = c5; + z->c = v_5; } z->lb = z->c; z->c = z->l; - - { int m6 = z->l - z->c; (void)m6; - { int ret = r_Step_1a(z); + { + int v_6 = z->l - z->c; + { + int ret = r_Step_1a(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_Step_1b(z); + { + int v_7 = z->l - z->c; + { + int ret = r_Step_1b(z); if (ret < 0) return ret; } - z->c = z->l - m7; + z->c = z->l - v_7; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_Step_1c(z); + { + int v_8 = z->l - z->c; + { + int ret = r_Step_1c(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_Step_2(z); + { + int v_9 = z->l - z->c; + { + int ret = r_Step_2(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_Step_3(z); + { + int v_10 = z->l - z->c; + { + int ret = r_Step_3(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } - { int m11 = z->l - z->c; (void)m11; - { int ret = r_Step_4(z); + { + int v_11 = z->l - z->c; + { + int ret = r_Step_4(z); if (ret < 0) return ret; } - z->c = z->l - m11; + z->c = z->l - v_11; } - { int m12 = z->l - z->c; (void)m12; - { int ret = r_Step_5a(z); + { + int v_12 = z->l - z->c; + { + int ret = r_Step_5a(z); if (ret < 0) return ret; } - z->c = z->l - m12; + z->c = z->l - v_12; } - { int m13 = z->l - z->c; (void)m13; - { int ret = r_Step_5b(z); + { + int v_13 = z->l - z->c; + { + int ret = r_Step_5b(z); if (ret < 0) return ret; } - z->c = z->l - m13; + z->c = z->l - v_13; } z->c = z->lb; - { int c14 = z->c; - if (!(z->I[2])) goto lab5; - while(1) { - int c15 = z->c; - while(1) { - int c16 = z->c; + { + int v_14 = z->c; + if (!b_Y_found) goto lab5; + while (1) { + int v_15 = z->c; + while (1) { + int v_16 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'Y') goto lab7; z->c++; z->ket = z->c; - z->c = c16; + z->c = v_16; break; lab7: - z->c = c16; + z->c = v_16; if (z->c >= z->l) goto lab6; z->c++; } - { int ret = slice_from_s(z, 1, s_23); + { + int ret = slice_from_s(z, 1, s_23); if (ret < 0) return ret; } continue; lab6: - z->c = c15; + z->c = v_15; break; } lab5: - z->c = c14; + z->c = v_14; } return 1; } -extern struct SN_env * porter_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * porter_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void porter_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void porter_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_portuguese.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_portuguese.c index 33aae89f7322b..39a36a97b32c7 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_portuguese.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_portuguese.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from portuguese.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_portuguese.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int portuguese_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_residual_form(struct SN_env * z); static int r_residual_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); @@ -19,71 +33,62 @@ static int r_RV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * portuguese_ISO_8859_1_create_env(void); -extern void portuguese_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a', '~' }; +static const symbol s_1[] = { 'o', '~' }; +static const symbol s_2[] = { 0xE3 }; +static const symbol s_3[] = { 0xF5 }; +static const symbol s_4[] = { 'l', 'o', 'g' }; +static const symbol s_5[] = { 'u' }; +static const symbol s_6[] = { 'e', 'n', 't', 'e' }; +static const symbol s_7[] = { 'a', 't' }; +static const symbol s_8[] = { 'a', 't' }; +static const symbol s_9[] = { 'i', 'r' }; +static const symbol s_10[] = { 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[1] = { 0xE3 }; static const symbol s_0_2[1] = { 0xF5 }; - -static const struct among a_0[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 1, s_0_1, 0, 1, 0}, -{ 1, s_0_2, 0, 2, 0} +static const struct among a_0[3] = { +{ 0, 0, 0, 3, 0}, +{ 1, s_0_1, -1, 1, 0}, +{ 1, s_0_2, -2, 2, 0} }; static const symbol s_1_1[2] = { 'a', '~' }; static const symbol s_1_2[2] = { 'o', '~' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_1_1, 0, 1, 0}, -{ 2, s_1_2, 0, 2, 0} +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_1_1, -1, 1, 0}, +{ 2, s_1_2, -2, 2, 0} }; static const symbol s_2_0[2] = { 'i', 'c' }; static const symbol s_2_1[2] = { 'a', 'd' }; static const symbol s_2_2[2] = { 'o', 's' }; static const symbol s_2_3[2] = { 'i', 'v' }; - -static const struct among a_2[4] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0}, -{ 2, s_2_3, -1, 1, 0} +static const struct among a_2[4] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, 1, 0} }; static const symbol s_3_0[4] = { 'a', 'n', 't', 'e' }; static const symbol s_3_1[4] = { 'a', 'v', 'e', 'l' }; static const symbol s_3_2[4] = { 0xED, 'v', 'e', 'l' }; - -static const struct among a_3[3] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 4, s_3_2, -1, 1, 0} +static const struct among a_3[3] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 4, s_3_2, 0, 1, 0} }; static const symbol s_4_0[2] = { 'i', 'c' }; static const symbol s_4_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_4_2[2] = { 'i', 'v' }; - -static const struct among a_4[3] = -{ -{ 2, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 1, 0}, -{ 2, s_4_2, -1, 1, 0} +static const struct among a_4[3] = { +{ 2, s_4_0, 0, 1, 0}, +{ 4, s_4_1, 0, 1, 0}, +{ 2, s_4_2, 0, 1, 0} }; static const symbol s_5_0[3] = { 'i', 'c', 'a' }; @@ -131,54 +136,52 @@ static const symbol s_5_41[4] = { 'o', 's', 'o', 's' }; static const symbol s_5_42[7] = { 'a', 'm', 'e', 'n', 't', 'o', 's' }; static const symbol s_5_43[7] = { 'i', 'm', 'e', 'n', 't', 'o', 's' }; static const symbol s_5_44[4] = { 'i', 'v', 'o', 's' }; - -static const struct among a_5[45] = -{ -{ 3, s_5_0, -1, 1, 0}, -{ 5, s_5_1, -1, 1, 0}, -{ 5, s_5_2, -1, 4, 0}, -{ 5, s_5_3, -1, 2, 0}, -{ 3, s_5_4, -1, 9, 0}, -{ 5, s_5_5, -1, 1, 0}, -{ 3, s_5_6, -1, 1, 0}, -{ 4, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 8, 0}, -{ 3, s_5_9, -1, 1, 0}, -{ 5, s_5_10, -1, 7, 0}, -{ 4, s_5_11, -1, 1, 0}, -{ 5, s_5_12, -1, 6, 0}, -{ 6, s_5_13, 12, 5, 0}, -{ 4, s_5_14, -1, 1, 0}, -{ 4, s_5_15, -1, 1, 0}, -{ 3, s_5_16, -1, 1, 0}, -{ 4, s_5_17, -1, 1, 0}, -{ 3, s_5_18, -1, 1, 0}, -{ 6, s_5_19, -1, 1, 0}, -{ 6, s_5_20, -1, 1, 0}, -{ 3, s_5_21, -1, 8, 0}, -{ 5, s_5_22, -1, 1, 0}, -{ 5, s_5_23, -1, 3, 0}, -{ 4, s_5_24, -1, 1, 0}, -{ 4, s_5_25, -1, 1, 0}, -{ 6, s_5_26, -1, 4, 0}, -{ 6, s_5_27, -1, 2, 0}, -{ 4, s_5_28, -1, 9, 0}, -{ 6, s_5_29, -1, 1, 0}, -{ 4, s_5_30, -1, 1, 0}, -{ 5, s_5_31, -1, 1, 0}, -{ 4, s_5_32, -1, 8, 0}, -{ 4, s_5_33, -1, 1, 0}, -{ 6, s_5_34, -1, 7, 0}, -{ 6, s_5_35, -1, 1, 0}, -{ 5, s_5_36, -1, 1, 0}, -{ 6, s_5_37, -1, 1, 0}, -{ 6, s_5_38, -1, 3, 0}, -{ 4, s_5_39, -1, 1, 0}, -{ 5, s_5_40, -1, 1, 0}, -{ 4, s_5_41, -1, 1, 0}, -{ 7, s_5_42, -1, 1, 0}, -{ 7, s_5_43, -1, 1, 0}, -{ 4, s_5_44, -1, 8, 0} +static const struct among a_5[45] = { +{ 3, s_5_0, 0, 1, 0}, +{ 5, s_5_1, 0, 1, 0}, +{ 5, s_5_2, 0, 4, 0}, +{ 5, s_5_3, 0, 2, 0}, +{ 3, s_5_4, 0, 9, 0}, +{ 5, s_5_5, 0, 1, 0}, +{ 3, s_5_6, 0, 1, 0}, +{ 4, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 8, 0}, +{ 3, s_5_9, 0, 1, 0}, +{ 5, s_5_10, 0, 7, 0}, +{ 4, s_5_11, 0, 1, 0}, +{ 5, s_5_12, 0, 6, 0}, +{ 6, s_5_13, -1, 5, 0}, +{ 4, s_5_14, 0, 1, 0}, +{ 4, s_5_15, 0, 1, 0}, +{ 3, s_5_16, 0, 1, 0}, +{ 4, s_5_17, 0, 1, 0}, +{ 3, s_5_18, 0, 1, 0}, +{ 6, s_5_19, 0, 1, 0}, +{ 6, s_5_20, 0, 1, 0}, +{ 3, s_5_21, 0, 8, 0}, +{ 5, s_5_22, 0, 1, 0}, +{ 5, s_5_23, 0, 3, 0}, +{ 4, s_5_24, 0, 1, 0}, +{ 4, s_5_25, 0, 1, 0}, +{ 6, s_5_26, 0, 4, 0}, +{ 6, s_5_27, 0, 2, 0}, +{ 4, s_5_28, 0, 9, 0}, +{ 6, s_5_29, 0, 1, 0}, +{ 4, s_5_30, 0, 1, 0}, +{ 5, s_5_31, 0, 1, 0}, +{ 4, s_5_32, 0, 8, 0}, +{ 4, s_5_33, 0, 1, 0}, +{ 6, s_5_34, 0, 7, 0}, +{ 6, s_5_35, 0, 1, 0}, +{ 5, s_5_36, 0, 1, 0}, +{ 6, s_5_37, 0, 1, 0}, +{ 6, s_5_38, 0, 3, 0}, +{ 4, s_5_39, 0, 1, 0}, +{ 5, s_5_40, 0, 1, 0}, +{ 4, s_5_41, 0, 1, 0}, +{ 7, s_5_42, 0, 1, 0}, +{ 7, s_5_43, 0, 1, 0}, +{ 4, s_5_44, 0, 8, 0} }; static const symbol s_6_0[3] = { 'a', 'd', 'a' }; @@ -301,129 +304,127 @@ static const symbol s_6_116[2] = { 'o', 'u' }; static const symbol s_6_117[3] = { 'a', 'r', 0xE1 }; static const symbol s_6_118[3] = { 'e', 'r', 0xE1 }; static const symbol s_6_119[3] = { 'i', 'r', 0xE1 }; - -static const struct among a_6[120] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 3, s_6_1, -1, 1, 0}, -{ 2, s_6_2, -1, 1, 0}, -{ 4, s_6_3, 2, 1, 0}, -{ 4, s_6_4, 2, 1, 0}, -{ 4, s_6_5, 2, 1, 0}, -{ 3, s_6_6, -1, 1, 0}, -{ 3, s_6_7, -1, 1, 0}, -{ 3, s_6_8, -1, 1, 0}, -{ 3, s_6_9, -1, 1, 0}, -{ 4, s_6_10, -1, 1, 0}, -{ 4, s_6_11, -1, 1, 0}, -{ 4, s_6_12, -1, 1, 0}, -{ 4, s_6_13, -1, 1, 0}, -{ 4, s_6_14, -1, 1, 0}, -{ 4, s_6_15, -1, 1, 0}, -{ 2, s_6_16, -1, 1, 0}, -{ 4, s_6_17, 16, 1, 0}, -{ 4, s_6_18, 16, 1, 0}, -{ 4, s_6_19, 16, 1, 0}, -{ 2, s_6_20, -1, 1, 0}, -{ 3, s_6_21, 20, 1, 0}, -{ 5, s_6_22, 21, 1, 0}, -{ 5, s_6_23, 21, 1, 0}, -{ 5, s_6_24, 21, 1, 0}, -{ 4, s_6_25, 20, 1, 0}, -{ 4, s_6_26, 20, 1, 0}, -{ 4, s_6_27, 20, 1, 0}, -{ 4, s_6_28, 20, 1, 0}, -{ 2, s_6_29, -1, 1, 0}, -{ 4, s_6_30, 29, 1, 0}, -{ 4, s_6_31, 29, 1, 0}, -{ 4, s_6_32, 29, 1, 0}, -{ 5, s_6_33, 29, 1, 0}, -{ 5, s_6_34, 29, 1, 0}, -{ 5, s_6_35, 29, 1, 0}, -{ 3, s_6_36, -1, 1, 0}, -{ 3, s_6_37, -1, 1, 0}, -{ 4, s_6_38, -1, 1, 0}, -{ 4, s_6_39, -1, 1, 0}, -{ 4, s_6_40, -1, 1, 0}, -{ 5, s_6_41, -1, 1, 0}, -{ 5, s_6_42, -1, 1, 0}, -{ 5, s_6_43, -1, 1, 0}, -{ 2, s_6_44, -1, 1, 0}, -{ 2, s_6_45, -1, 1, 0}, -{ 2, s_6_46, -1, 1, 0}, -{ 2, s_6_47, -1, 1, 0}, -{ 4, s_6_48, 47, 1, 0}, -{ 4, s_6_49, 47, 1, 0}, -{ 3, s_6_50, 47, 1, 0}, -{ 5, s_6_51, 50, 1, 0}, -{ 5, s_6_52, 50, 1, 0}, -{ 5, s_6_53, 50, 1, 0}, -{ 4, s_6_54, 47, 1, 0}, -{ 4, s_6_55, 47, 1, 0}, -{ 4, s_6_56, 47, 1, 0}, -{ 4, s_6_57, 47, 1, 0}, -{ 2, s_6_58, -1, 1, 0}, -{ 5, s_6_59, 58, 1, 0}, -{ 5, s_6_60, 58, 1, 0}, -{ 5, s_6_61, 58, 1, 0}, -{ 4, s_6_62, 58, 1, 0}, -{ 4, s_6_63, 58, 1, 0}, -{ 4, s_6_64, 58, 1, 0}, -{ 5, s_6_65, 58, 1, 0}, -{ 5, s_6_66, 58, 1, 0}, -{ 5, s_6_67, 58, 1, 0}, -{ 5, s_6_68, 58, 1, 0}, -{ 5, s_6_69, 58, 1, 0}, -{ 5, s_6_70, 58, 1, 0}, -{ 2, s_6_71, -1, 1, 0}, -{ 3, s_6_72, 71, 1, 0}, -{ 3, s_6_73, 71, 1, 0}, -{ 5, s_6_74, 73, 1, 0}, -{ 5, s_6_75, 73, 1, 0}, -{ 5, s_6_76, 73, 1, 0}, -{ 5, s_6_77, 73, 1, 0}, -{ 5, s_6_78, 73, 1, 0}, -{ 5, s_6_79, 73, 1, 0}, -{ 6, s_6_80, 73, 1, 0}, -{ 6, s_6_81, 73, 1, 0}, -{ 6, s_6_82, 73, 1, 0}, -{ 5, s_6_83, 73, 1, 0}, -{ 4, s_6_84, 73, 1, 0}, -{ 6, s_6_85, 84, 1, 0}, -{ 6, s_6_86, 84, 1, 0}, -{ 6, s_6_87, 84, 1, 0}, -{ 4, s_6_88, -1, 1, 0}, -{ 4, s_6_89, -1, 1, 0}, -{ 4, s_6_90, -1, 1, 0}, -{ 6, s_6_91, 90, 1, 0}, -{ 6, s_6_92, 90, 1, 0}, -{ 6, s_6_93, 90, 1, 0}, -{ 6, s_6_94, 90, 1, 0}, -{ 5, s_6_95, 90, 1, 0}, -{ 7, s_6_96, 95, 1, 0}, -{ 7, s_6_97, 95, 1, 0}, -{ 7, s_6_98, 95, 1, 0}, -{ 4, s_6_99, -1, 1, 0}, -{ 6, s_6_100, 99, 1, 0}, -{ 6, s_6_101, 99, 1, 0}, -{ 6, s_6_102, 99, 1, 0}, -{ 7, s_6_103, 99, 1, 0}, -{ 7, s_6_104, 99, 1, 0}, -{ 7, s_6_105, 99, 1, 0}, -{ 4, s_6_106, -1, 1, 0}, -{ 5, s_6_107, -1, 1, 0}, -{ 5, s_6_108, -1, 1, 0}, -{ 5, s_6_109, -1, 1, 0}, -{ 4, s_6_110, -1, 1, 0}, -{ 4, s_6_111, -1, 1, 0}, -{ 4, s_6_112, -1, 1, 0}, -{ 4, s_6_113, -1, 1, 0}, -{ 2, s_6_114, -1, 1, 0}, -{ 2, s_6_115, -1, 1, 0}, -{ 2, s_6_116, -1, 1, 0}, -{ 3, s_6_117, -1, 1, 0}, -{ 3, s_6_118, -1, 1, 0}, -{ 3, s_6_119, -1, 1, 0} +static const struct among a_6[120] = { +{ 3, s_6_0, 0, 1, 0}, +{ 3, s_6_1, 0, 1, 0}, +{ 2, s_6_2, 0, 1, 0}, +{ 4, s_6_3, -1, 1, 0}, +{ 4, s_6_4, -2, 1, 0}, +{ 4, s_6_5, -3, 1, 0}, +{ 3, s_6_6, 0, 1, 0}, +{ 3, s_6_7, 0, 1, 0}, +{ 3, s_6_8, 0, 1, 0}, +{ 3, s_6_9, 0, 1, 0}, +{ 4, s_6_10, 0, 1, 0}, +{ 4, s_6_11, 0, 1, 0}, +{ 4, s_6_12, 0, 1, 0}, +{ 4, s_6_13, 0, 1, 0}, +{ 4, s_6_14, 0, 1, 0}, +{ 4, s_6_15, 0, 1, 0}, +{ 2, s_6_16, 0, 1, 0}, +{ 4, s_6_17, -1, 1, 0}, +{ 4, s_6_18, -2, 1, 0}, +{ 4, s_6_19, -3, 1, 0}, +{ 2, s_6_20, 0, 1, 0}, +{ 3, s_6_21, -1, 1, 0}, +{ 5, s_6_22, -1, 1, 0}, +{ 5, s_6_23, -2, 1, 0}, +{ 5, s_6_24, -3, 1, 0}, +{ 4, s_6_25, -5, 1, 0}, +{ 4, s_6_26, -6, 1, 0}, +{ 4, s_6_27, -7, 1, 0}, +{ 4, s_6_28, -8, 1, 0}, +{ 2, s_6_29, 0, 1, 0}, +{ 4, s_6_30, -1, 1, 0}, +{ 4, s_6_31, -2, 1, 0}, +{ 4, s_6_32, -3, 1, 0}, +{ 5, s_6_33, -4, 1, 0}, +{ 5, s_6_34, -5, 1, 0}, +{ 5, s_6_35, -6, 1, 0}, +{ 3, s_6_36, 0, 1, 0}, +{ 3, s_6_37, 0, 1, 0}, +{ 4, s_6_38, 0, 1, 0}, +{ 4, s_6_39, 0, 1, 0}, +{ 4, s_6_40, 0, 1, 0}, +{ 5, s_6_41, 0, 1, 0}, +{ 5, s_6_42, 0, 1, 0}, +{ 5, s_6_43, 0, 1, 0}, +{ 2, s_6_44, 0, 1, 0}, +{ 2, s_6_45, 0, 1, 0}, +{ 2, s_6_46, 0, 1, 0}, +{ 2, s_6_47, 0, 1, 0}, +{ 4, s_6_48, -1, 1, 0}, +{ 4, s_6_49, -2, 1, 0}, +{ 3, s_6_50, -3, 1, 0}, +{ 5, s_6_51, -1, 1, 0}, +{ 5, s_6_52, -2, 1, 0}, +{ 5, s_6_53, -3, 1, 0}, +{ 4, s_6_54, -7, 1, 0}, +{ 4, s_6_55, -8, 1, 0}, +{ 4, s_6_56, -9, 1, 0}, +{ 4, s_6_57, -10, 1, 0}, +{ 2, s_6_58, 0, 1, 0}, +{ 5, s_6_59, -1, 1, 0}, +{ 5, s_6_60, -2, 1, 0}, +{ 5, s_6_61, -3, 1, 0}, +{ 4, s_6_62, -4, 1, 0}, +{ 4, s_6_63, -5, 1, 0}, +{ 4, s_6_64, -6, 1, 0}, +{ 5, s_6_65, -7, 1, 0}, +{ 5, s_6_66, -8, 1, 0}, +{ 5, s_6_67, -9, 1, 0}, +{ 5, s_6_68, -10, 1, 0}, +{ 5, s_6_69, -11, 1, 0}, +{ 5, s_6_70, -12, 1, 0}, +{ 2, s_6_71, 0, 1, 0}, +{ 3, s_6_72, -1, 1, 0}, +{ 3, s_6_73, -2, 1, 0}, +{ 5, s_6_74, -1, 1, 0}, +{ 5, s_6_75, -2, 1, 0}, +{ 5, s_6_76, -3, 1, 0}, +{ 5, s_6_77, -4, 1, 0}, +{ 5, s_6_78, -5, 1, 0}, +{ 5, s_6_79, -6, 1, 0}, +{ 6, s_6_80, -7, 1, 0}, +{ 6, s_6_81, -8, 1, 0}, +{ 6, s_6_82, -9, 1, 0}, +{ 5, s_6_83, -10, 1, 0}, +{ 4, s_6_84, -11, 1, 0}, +{ 6, s_6_85, -1, 1, 0}, +{ 6, s_6_86, -2, 1, 0}, +{ 6, s_6_87, -3, 1, 0}, +{ 4, s_6_88, 0, 1, 0}, +{ 4, s_6_89, 0, 1, 0}, +{ 4, s_6_90, 0, 1, 0}, +{ 6, s_6_91, -1, 1, 0}, +{ 6, s_6_92, -2, 1, 0}, +{ 6, s_6_93, -3, 1, 0}, +{ 6, s_6_94, -4, 1, 0}, +{ 5, s_6_95, -5, 1, 0}, +{ 7, s_6_96, -1, 1, 0}, +{ 7, s_6_97, -2, 1, 0}, +{ 7, s_6_98, -3, 1, 0}, +{ 4, s_6_99, 0, 1, 0}, +{ 6, s_6_100, -1, 1, 0}, +{ 6, s_6_101, -2, 1, 0}, +{ 6, s_6_102, -3, 1, 0}, +{ 7, s_6_103, -4, 1, 0}, +{ 7, s_6_104, -5, 1, 0}, +{ 7, s_6_105, -6, 1, 0}, +{ 4, s_6_106, 0, 1, 0}, +{ 5, s_6_107, 0, 1, 0}, +{ 5, s_6_108, 0, 1, 0}, +{ 5, s_6_109, 0, 1, 0}, +{ 4, s_6_110, 0, 1, 0}, +{ 4, s_6_111, 0, 1, 0}, +{ 4, s_6_112, 0, 1, 0}, +{ 4, s_6_113, 0, 1, 0}, +{ 2, s_6_114, 0, 1, 0}, +{ 2, s_6_115, 0, 1, 0}, +{ 2, s_6_116, 0, 1, 0}, +{ 3, s_6_117, 0, 1, 0}, +{ 3, s_6_118, 0, 1, 0}, +{ 3, s_6_119, 0, 1, 0} }; static const symbol s_7_0[1] = { 'a' }; @@ -433,61 +434,47 @@ static const symbol s_7_3[2] = { 'o', 's' }; static const symbol s_7_4[1] = { 0xE1 }; static const symbol s_7_5[1] = { 0xED }; static const symbol s_7_6[1] = { 0xF3 }; - -static const struct among a_7[7] = -{ -{ 1, s_7_0, -1, 1, 0}, -{ 1, s_7_1, -1, 1, 0}, -{ 1, s_7_2, -1, 1, 0}, -{ 2, s_7_3, -1, 1, 0}, -{ 1, s_7_4, -1, 1, 0}, -{ 1, s_7_5, -1, 1, 0}, -{ 1, s_7_6, -1, 1, 0} +static const struct among a_7[7] = { +{ 1, s_7_0, 0, 1, 0}, +{ 1, s_7_1, 0, 1, 0}, +{ 1, s_7_2, 0, 1, 0}, +{ 2, s_7_3, 0, 1, 0}, +{ 1, s_7_4, 0, 1, 0}, +{ 1, s_7_5, 0, 1, 0}, +{ 1, s_7_6, 0, 1, 0} }; static const symbol s_8_0[1] = { 'e' }; static const symbol s_8_1[1] = { 0xE7 }; static const symbol s_8_2[1] = { 0xE9 }; static const symbol s_8_3[1] = { 0xEA }; - -static const struct among a_8[4] = -{ -{ 1, s_8_0, -1, 1, 0}, -{ 1, s_8_1, -1, 2, 0}, -{ 1, s_8_2, -1, 1, 0}, -{ 1, s_8_3, -1, 1, 0} +static const struct among a_8[4] = { +{ 1, s_8_0, 0, 1, 0}, +{ 1, s_8_1, 0, 2, 0}, +{ 1, s_8_2, 0, 1, 0}, +{ 1, s_8_3, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 19, 12, 2 }; -static const symbol s_0[] = { 'a', '~' }; -static const symbol s_1[] = { 'o', '~' }; -static const symbol s_2[] = { 0xE3 }; -static const symbol s_3[] = { 0xF5 }; -static const symbol s_4[] = { 'l', 'o', 'g' }; -static const symbol s_5[] = { 'u' }; -static const symbol s_6[] = { 'e', 'n', 't', 'e' }; -static const symbol s_7[] = { 'a', 't' }; -static const symbol s_8[] = { 'a', 't' }; -static const symbol s_9[] = { 'i', 'r' }; -static const symbol s_10[] = { 'c' }; - static int r_prelude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || (z->p[z->c + 0] != 227 && z->p[z->c + 0] != 245)) among_var = 3; else - among_var = find_among(z, a_0, 3); + among_var = find_among(z, a_0, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_1); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; @@ -498,115 +485,111 @@ static int r_prelude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping(z, g_v, 97, 250, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping(z, g_v, 97, 250, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping(z, g_v, 97, 250, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping(z, g_v, 97, 250, 0)) goto lab2; { int ret = out_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping(z, g_v, 97, 250, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping(z, g_v, 97, 250, 0)) goto lab1; { int ret = in_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping(z, g_v, 97, 250, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping(z, g_v, 97, 250, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping(z, g_v, 97, 250, 0)) goto lab3; { int ret = out_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping(z, g_v, 97, 250, 0)) goto lab0; if (z->c >= z->l) goto lab0; z->c++; - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c + 1 >= z->l || z->p[z->c + 1] != 126) among_var = 3; else - among_var = find_among(z, a_1, 3); + among_var = find_among(z, a_1, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; @@ -617,94 +600,109 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((823330 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_5, 45); + among_var = find_among_b(z, a_5, 45, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_4); + { + int ret = slice_from_s(z, 3, s_4); if (ret < 0) return ret; } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 4, s_6); + { + int ret = slice_from_s(z, 4, s_6); if (ret < 0) return ret; } break; case 5: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m1; goto lab0; } - among_var = find_among_b(z, a_2, 4); - if (!among_var) { z->c = z->l - m1; goto lab0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_1; goto lab0; } + among_var = find_among_b(z, a_2, 4, 0); + if (!among_var) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } switch (among_var) { case 1: z->ket = z->c; - if (!(eq_s_b(z, 2, s_7))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_7))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -714,22 +712,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 6: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) { z->c = z->l - m2; goto lab1; } - if (!find_among_b(z, a_3, 3)) { z->c = z->l - m2; goto lab1; } + if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) { z->c = z->l - v_2; goto lab1; } + if (!find_among_b(z, a_3, 3, 0)) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab1: @@ -737,22 +740,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 7: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m3; goto lab2; } - if (!find_among_b(z, a_4, 3)) { z->c = z->l - m3; goto lab2; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_3; goto lab2; } + if (!find_among_b(z, a_4, 3, 0)) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab2; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -760,21 +768,26 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_8))) { z->c = z->l - m4; goto lab3; } + if (!(eq_s_b(z, 2, s_8))) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab3: @@ -782,12 +795,14 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 9: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; z->c--; - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; @@ -796,29 +811,32 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (!find_among_b(z, a_6, 120)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_6, 120, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_residual_suffix(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_7, 7)) return 0; + if (!find_among_b(z, a_7, 7, 0)) return 0; z->bra = z->c; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -827,49 +845,56 @@ static int r_residual_suffix(struct SN_env * z) { static int r_residual_form(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_8, 4); + among_var = find_among_b(z, a_8, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab0; z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'g') goto lab1; + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'g') goto lab0; z->c--; - z->c = z->l - m_test2; + z->c = z->l - v_2; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'i') return 0; z->c--; z->bra = z->c; - { int m_test3 = z->l - z->c; + { + int v_3 = z->l - z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'c') return 0; z->c--; - z->c = z->l - m_test3; + z->c = z->l - v_3; } - } - lab0: - { int ret = r_RV(z); + } while (0); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; @@ -878,86 +903,110 @@ static int r_residual_form(struct SN_env * z) { } extern int portuguese_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_prelude(z); + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab4; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab3; - lab4: - z->c = z->l - m5; - { int ret = r_verb_suffix(z); - if (ret == 0) goto lab2; + break; + lab2: + z->c = z->l - v_5; + { + int ret = r_verb_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - } - lab3: - z->c = z->l - m4; - { int m6 = z->l - z->c; (void)m6; + } while (0); + z->c = z->l - v_4; + { + int v_6 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab5; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab3; z->c--; z->bra = z->c; - { int m_test7 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab5; + { + int v_7 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab3; z->c--; - z->c = z->l - m_test7; + z->c = z->l - v_7; } - { int ret = r_RV(z); - if (ret == 0) goto lab5; + { + int ret = r_RV(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab5: - z->c = z->l - m6; + lab3: + z->c = z->l - v_6; } } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_residual_suffix(z); + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_residual_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_residual_form(z); + { + int v_8 = z->l - z->c; + { + int ret = r_residual_form(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } z->c = z->lb; - { int c9 = z->c; - { int ret = r_postlude(z); + { + int v_9 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; } return 1; } -extern struct SN_env * portuguese_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * portuguese_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void portuguese_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void portuguese_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_spanish.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_spanish.c index 252bb28cd3c43..01f10f847e30f 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_spanish.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_spanish.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from spanish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_spanish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int spanish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_residual_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_y_verb_suffix(struct SN_env * z); @@ -19,32 +33,36 @@ static int r_R1(struct SN_env * z); static int r_RV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * spanish_ISO_8859_1_create_env(void); -extern void spanish_ISO_8859_1_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'i' }; +static const symbol s_3[] = { 'o' }; +static const symbol s_4[] = { 'u' }; +static const symbol s_5[] = { 'i', 'e', 'n', 'd', 'o' }; +static const symbol s_6[] = { 'a', 'n', 'd', 'o' }; +static const symbol s_7[] = { 'a', 'r' }; +static const symbol s_8[] = { 'e', 'r' }; +static const symbol s_9[] = { 'i', 'r' }; +static const symbol s_10[] = { 'i', 'c' }; +static const symbol s_11[] = { 'l', 'o', 'g' }; +static const symbol s_12[] = { 'u' }; +static const symbol s_13[] = { 'e', 'n', 't', 'e' }; +static const symbol s_14[] = { 'a', 't' }; +static const symbol s_15[] = { 'a', 't' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[1] = { 0xE1 }; static const symbol s_0_2[1] = { 0xE9 }; static const symbol s_0_3[1] = { 0xED }; static const symbol s_0_4[1] = { 0xF3 }; static const symbol s_0_5[1] = { 0xFA }; - -static const struct among a_0[6] = -{ -{ 0, 0, -1, 6, 0}, -{ 1, s_0_1, 0, 1, 0}, -{ 1, s_0_2, 0, 2, 0}, -{ 1, s_0_3, 0, 3, 0}, -{ 1, s_0_4, 0, 4, 0}, -{ 1, s_0_5, 0, 5, 0} +static const struct among a_0[6] = { +{ 0, 0, 0, 6, 0}, +{ 1, s_0_1, -1, 1, 0}, +{ 1, s_0_2, -2, 2, 0}, +{ 1, s_0_3, -3, 3, 0}, +{ 1, s_0_4, -4, 4, 0}, +{ 1, s_0_5, -5, 5, 0} }; static const symbol s_1_0[2] = { 'l', 'a' }; @@ -60,22 +78,20 @@ static const symbol s_1_9[3] = { 'l', 'e', 's' }; static const symbol s_1_10[3] = { 'l', 'o', 's' }; static const symbol s_1_11[5] = { 's', 'e', 'l', 'o', 's' }; static const symbol s_1_12[3] = { 'n', 'o', 's' }; - -static const struct among a_1[13] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 4, s_1_1, 0, -1, 0}, -{ 2, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0}, -{ 2, s_1_4, -1, -1, 0}, -{ 2, s_1_5, -1, -1, 0}, -{ 4, s_1_6, 5, -1, 0}, -{ 3, s_1_7, -1, -1, 0}, -{ 5, s_1_8, 7, -1, 0}, -{ 3, s_1_9, -1, -1, 0}, -{ 3, s_1_10, -1, -1, 0}, -{ 5, s_1_11, 10, -1, 0}, -{ 3, s_1_12, -1, -1, 0} +static const struct among a_1[13] = { +{ 2, s_1_0, 0, -1, 0}, +{ 4, s_1_1, -1, -1, 0}, +{ 2, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 2, s_1_4, 0, -1, 0}, +{ 2, s_1_5, 0, -1, 0}, +{ 4, s_1_6, -1, -1, 0}, +{ 3, s_1_7, 0, -1, 0}, +{ 5, s_1_8, -1, -1, 0}, +{ 3, s_1_9, 0, -1, 0}, +{ 3, s_1_10, 0, -1, 0}, +{ 5, s_1_11, -1, -1, 0}, +{ 3, s_1_12, 0, -1, 0} }; static const symbol s_2_0[4] = { 'a', 'n', 'd', 'o' }; @@ -89,55 +105,47 @@ static const symbol s_2_7[2] = { 'i', 'r' }; static const symbol s_2_8[2] = { 0xE1, 'r' }; static const symbol s_2_9[2] = { 0xE9, 'r' }; static const symbol s_2_10[2] = { 0xED, 'r' }; - -static const struct among a_2[11] = -{ -{ 4, s_2_0, -1, 6, 0}, -{ 5, s_2_1, -1, 6, 0}, -{ 5, s_2_2, -1, 7, 0}, -{ 4, s_2_3, -1, 2, 0}, -{ 5, s_2_4, -1, 1, 0}, -{ 2, s_2_5, -1, 6, 0}, -{ 2, s_2_6, -1, 6, 0}, -{ 2, s_2_7, -1, 6, 0}, -{ 2, s_2_8, -1, 3, 0}, -{ 2, s_2_9, -1, 4, 0}, -{ 2, s_2_10, -1, 5, 0} +static const struct among a_2[11] = { +{ 4, s_2_0, 0, 6, 0}, +{ 5, s_2_1, 0, 6, 0}, +{ 5, s_2_2, 0, 7, 0}, +{ 4, s_2_3, 0, 2, 0}, +{ 5, s_2_4, 0, 1, 0}, +{ 2, s_2_5, 0, 6, 0}, +{ 2, s_2_6, 0, 6, 0}, +{ 2, s_2_7, 0, 6, 0}, +{ 2, s_2_8, 0, 3, 0}, +{ 2, s_2_9, 0, 4, 0}, +{ 2, s_2_10, 0, 5, 0} }; static const symbol s_3_0[2] = { 'i', 'c' }; static const symbol s_3_1[2] = { 'a', 'd' }; static const symbol s_3_2[2] = { 'o', 's' }; static const symbol s_3_3[2] = { 'i', 'v' }; - -static const struct among a_3[4] = -{ -{ 2, s_3_0, -1, -1, 0}, -{ 2, s_3_1, -1, -1, 0}, -{ 2, s_3_2, -1, -1, 0}, -{ 2, s_3_3, -1, 1, 0} +static const struct among a_3[4] = { +{ 2, s_3_0, 0, -1, 0}, +{ 2, s_3_1, 0, -1, 0}, +{ 2, s_3_2, 0, -1, 0}, +{ 2, s_3_3, 0, 1, 0} }; static const symbol s_4_0[4] = { 'a', 'b', 'l', 'e' }; static const symbol s_4_1[4] = { 'i', 'b', 'l', 'e' }; static const symbol s_4_2[4] = { 'a', 'n', 't', 'e' }; - -static const struct among a_4[3] = -{ -{ 4, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 1, 0}, -{ 4, s_4_2, -1, 1, 0} +static const struct among a_4[3] = { +{ 4, s_4_0, 0, 1, 0}, +{ 4, s_4_1, 0, 1, 0}, +{ 4, s_4_2, 0, 1, 0} }; static const symbol s_5_0[2] = { 'i', 'c' }; static const symbol s_5_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_5_2[2] = { 'i', 'v' }; - -static const struct among a_5[3] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 2, s_5_2, -1, 1, 0} +static const struct among a_5[3] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 2, s_5_2, 0, 1, 0} }; static const symbol s_6_0[3] = { 'i', 'c', 'a' }; @@ -155,86 +163,88 @@ static const symbol s_6_11[4] = { 'i', 'b', 'l', 'e' }; static const symbol s_6_12[4] = { 'a', 'n', 't', 'e' }; static const symbol s_6_13[5] = { 'm', 'e', 'n', 't', 'e' }; static const symbol s_6_14[6] = { 'a', 'm', 'e', 'n', 't', 'e' }; -static const symbol s_6_15[5] = { 'a', 'c', 'i', 0xF3, 'n' }; -static const symbol s_6_16[5] = { 'u', 'c', 'i', 0xF3, 'n' }; -static const symbol s_6_17[3] = { 'i', 'c', 'o' }; -static const symbol s_6_18[4] = { 'i', 's', 'm', 'o' }; -static const symbol s_6_19[3] = { 'o', 's', 'o' }; -static const symbol s_6_20[7] = { 'a', 'm', 'i', 'e', 'n', 't', 'o' }; -static const symbol s_6_21[7] = { 'i', 'm', 'i', 'e', 'n', 't', 'o' }; -static const symbol s_6_22[3] = { 'i', 'v', 'o' }; -static const symbol s_6_23[4] = { 'a', 'd', 'o', 'r' }; -static const symbol s_6_24[4] = { 'i', 'c', 'a', 's' }; -static const symbol s_6_25[6] = { 'a', 'n', 'c', 'i', 'a', 's' }; -static const symbol s_6_26[6] = { 'e', 'n', 'c', 'i', 'a', 's' }; -static const symbol s_6_27[6] = { 'a', 'd', 'o', 'r', 'a', 's' }; -static const symbol s_6_28[4] = { 'o', 's', 'a', 's' }; -static const symbol s_6_29[5] = { 'i', 's', 't', 'a', 's' }; -static const symbol s_6_30[4] = { 'i', 'v', 'a', 's' }; -static const symbol s_6_31[5] = { 'a', 'n', 'z', 'a', 's' }; -static const symbol s_6_32[6] = { 'l', 'o', 'g', 0xED, 'a', 's' }; -static const symbol s_6_33[6] = { 'i', 'd', 'a', 'd', 'e', 's' }; -static const symbol s_6_34[5] = { 'a', 'b', 'l', 'e', 's' }; -static const symbol s_6_35[5] = { 'i', 'b', 'l', 'e', 's' }; -static const symbol s_6_36[7] = { 'a', 'c', 'i', 'o', 'n', 'e', 's' }; -static const symbol s_6_37[7] = { 'u', 'c', 'i', 'o', 'n', 'e', 's' }; -static const symbol s_6_38[6] = { 'a', 'd', 'o', 'r', 'e', 's' }; -static const symbol s_6_39[5] = { 'a', 'n', 't', 'e', 's' }; -static const symbol s_6_40[4] = { 'i', 'c', 'o', 's' }; -static const symbol s_6_41[5] = { 'i', 's', 'm', 'o', 's' }; -static const symbol s_6_42[4] = { 'o', 's', 'o', 's' }; -static const symbol s_6_43[8] = { 'a', 'm', 'i', 'e', 'n', 't', 'o', 's' }; -static const symbol s_6_44[8] = { 'i', 'm', 'i', 'e', 'n', 't', 'o', 's' }; -static const symbol s_6_45[4] = { 'i', 'v', 'o', 's' }; - -static const struct among a_6[46] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 5, s_6_1, -1, 2, 0}, -{ 5, s_6_2, -1, 5, 0}, -{ 5, s_6_3, -1, 2, 0}, -{ 3, s_6_4, -1, 1, 0}, -{ 4, s_6_5, -1, 1, 0}, -{ 3, s_6_6, -1, 9, 0}, -{ 4, s_6_7, -1, 1, 0}, -{ 5, s_6_8, -1, 3, 0}, -{ 4, s_6_9, -1, 8, 0}, -{ 4, s_6_10, -1, 1, 0}, -{ 4, s_6_11, -1, 1, 0}, -{ 4, s_6_12, -1, 2, 0}, -{ 5, s_6_13, -1, 7, 0}, -{ 6, s_6_14, 13, 6, 0}, -{ 5, s_6_15, -1, 2, 0}, -{ 5, s_6_16, -1, 4, 0}, -{ 3, s_6_17, -1, 1, 0}, -{ 4, s_6_18, -1, 1, 0}, -{ 3, s_6_19, -1, 1, 0}, -{ 7, s_6_20, -1, 1, 0}, -{ 7, s_6_21, -1, 1, 0}, -{ 3, s_6_22, -1, 9, 0}, -{ 4, s_6_23, -1, 2, 0}, -{ 4, s_6_24, -1, 1, 0}, -{ 6, s_6_25, -1, 2, 0}, -{ 6, s_6_26, -1, 5, 0}, -{ 6, s_6_27, -1, 2, 0}, -{ 4, s_6_28, -1, 1, 0}, -{ 5, s_6_29, -1, 1, 0}, -{ 4, s_6_30, -1, 9, 0}, -{ 5, s_6_31, -1, 1, 0}, -{ 6, s_6_32, -1, 3, 0}, -{ 6, s_6_33, -1, 8, 0}, -{ 5, s_6_34, -1, 1, 0}, -{ 5, s_6_35, -1, 1, 0}, -{ 7, s_6_36, -1, 2, 0}, -{ 7, s_6_37, -1, 4, 0}, -{ 6, s_6_38, -1, 2, 0}, -{ 5, s_6_39, -1, 2, 0}, -{ 4, s_6_40, -1, 1, 0}, -{ 5, s_6_41, -1, 1, 0}, -{ 4, s_6_42, -1, 1, 0}, -{ 8, s_6_43, -1, 1, 0}, -{ 8, s_6_44, -1, 1, 0}, -{ 4, s_6_45, -1, 9, 0} +static const symbol s_6_15[5] = { 'a', 'c', 'i', 'o', 'n' }; +static const symbol s_6_16[5] = { 'u', 'c', 'i', 'o', 'n' }; +static const symbol s_6_17[5] = { 'a', 'c', 'i', 0xF3, 'n' }; +static const symbol s_6_18[5] = { 'u', 'c', 'i', 0xF3, 'n' }; +static const symbol s_6_19[3] = { 'i', 'c', 'o' }; +static const symbol s_6_20[4] = { 'i', 's', 'm', 'o' }; +static const symbol s_6_21[3] = { 'o', 's', 'o' }; +static const symbol s_6_22[7] = { 'a', 'm', 'i', 'e', 'n', 't', 'o' }; +static const symbol s_6_23[7] = { 'i', 'm', 'i', 'e', 'n', 't', 'o' }; +static const symbol s_6_24[3] = { 'i', 'v', 'o' }; +static const symbol s_6_25[4] = { 'a', 'd', 'o', 'r' }; +static const symbol s_6_26[4] = { 'i', 'c', 'a', 's' }; +static const symbol s_6_27[6] = { 'a', 'n', 'c', 'i', 'a', 's' }; +static const symbol s_6_28[6] = { 'e', 'n', 'c', 'i', 'a', 's' }; +static const symbol s_6_29[6] = { 'a', 'd', 'o', 'r', 'a', 's' }; +static const symbol s_6_30[4] = { 'o', 's', 'a', 's' }; +static const symbol s_6_31[5] = { 'i', 's', 't', 'a', 's' }; +static const symbol s_6_32[4] = { 'i', 'v', 'a', 's' }; +static const symbol s_6_33[5] = { 'a', 'n', 'z', 'a', 's' }; +static const symbol s_6_34[6] = { 'l', 'o', 'g', 0xED, 'a', 's' }; +static const symbol s_6_35[6] = { 'i', 'd', 'a', 'd', 'e', 's' }; +static const symbol s_6_36[5] = { 'a', 'b', 'l', 'e', 's' }; +static const symbol s_6_37[5] = { 'i', 'b', 'l', 'e', 's' }; +static const symbol s_6_38[7] = { 'a', 'c', 'i', 'o', 'n', 'e', 's' }; +static const symbol s_6_39[7] = { 'u', 'c', 'i', 'o', 'n', 'e', 's' }; +static const symbol s_6_40[6] = { 'a', 'd', 'o', 'r', 'e', 's' }; +static const symbol s_6_41[5] = { 'a', 'n', 't', 'e', 's' }; +static const symbol s_6_42[4] = { 'i', 'c', 'o', 's' }; +static const symbol s_6_43[5] = { 'i', 's', 'm', 'o', 's' }; +static const symbol s_6_44[4] = { 'o', 's', 'o', 's' }; +static const symbol s_6_45[8] = { 'a', 'm', 'i', 'e', 'n', 't', 'o', 's' }; +static const symbol s_6_46[8] = { 'i', 'm', 'i', 'e', 'n', 't', 'o', 's' }; +static const symbol s_6_47[4] = { 'i', 'v', 'o', 's' }; +static const struct among a_6[48] = { +{ 3, s_6_0, 0, 1, 0}, +{ 5, s_6_1, 0, 2, 0}, +{ 5, s_6_2, 0, 5, 0}, +{ 5, s_6_3, 0, 2, 0}, +{ 3, s_6_4, 0, 1, 0}, +{ 4, s_6_5, 0, 1, 0}, +{ 3, s_6_6, 0, 9, 0}, +{ 4, s_6_7, 0, 1, 0}, +{ 5, s_6_8, 0, 3, 0}, +{ 4, s_6_9, 0, 8, 0}, +{ 4, s_6_10, 0, 1, 0}, +{ 4, s_6_11, 0, 1, 0}, +{ 4, s_6_12, 0, 2, 0}, +{ 5, s_6_13, 0, 7, 0}, +{ 6, s_6_14, -1, 6, 0}, +{ 5, s_6_15, 0, 2, 0}, +{ 5, s_6_16, 0, 4, 0}, +{ 5, s_6_17, 0, 2, 0}, +{ 5, s_6_18, 0, 4, 0}, +{ 3, s_6_19, 0, 1, 0}, +{ 4, s_6_20, 0, 1, 0}, +{ 3, s_6_21, 0, 1, 0}, +{ 7, s_6_22, 0, 1, 0}, +{ 7, s_6_23, 0, 1, 0}, +{ 3, s_6_24, 0, 9, 0}, +{ 4, s_6_25, 0, 2, 0}, +{ 4, s_6_26, 0, 1, 0}, +{ 6, s_6_27, 0, 2, 0}, +{ 6, s_6_28, 0, 5, 0}, +{ 6, s_6_29, 0, 2, 0}, +{ 4, s_6_30, 0, 1, 0}, +{ 5, s_6_31, 0, 1, 0}, +{ 4, s_6_32, 0, 9, 0}, +{ 5, s_6_33, 0, 1, 0}, +{ 6, s_6_34, 0, 3, 0}, +{ 6, s_6_35, 0, 8, 0}, +{ 5, s_6_36, 0, 1, 0}, +{ 5, s_6_37, 0, 1, 0}, +{ 7, s_6_38, 0, 2, 0}, +{ 7, s_6_39, 0, 4, 0}, +{ 6, s_6_40, 0, 2, 0}, +{ 5, s_6_41, 0, 2, 0}, +{ 4, s_6_42, 0, 1, 0}, +{ 5, s_6_43, 0, 1, 0}, +{ 4, s_6_44, 0, 1, 0}, +{ 8, s_6_45, 0, 1, 0}, +{ 8, s_6_46, 0, 1, 0}, +{ 4, s_6_47, 0, 9, 0} }; static const symbol s_7_0[2] = { 'y', 'a' }; @@ -249,21 +259,19 @@ static const symbol s_7_8[3] = { 'y', 'e', 's' }; static const symbol s_7_9[4] = { 'y', 'a', 'i', 's' }; static const symbol s_7_10[5] = { 'y', 'a', 'm', 'o', 's' }; static const symbol s_7_11[2] = { 'y', 0xF3 }; - -static const struct among a_7[12] = -{ -{ 2, s_7_0, -1, 1, 0}, -{ 2, s_7_1, -1, 1, 0}, -{ 3, s_7_2, -1, 1, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 5, s_7_4, -1, 1, 0}, -{ 5, s_7_5, -1, 1, 0}, -{ 2, s_7_6, -1, 1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 3, s_7_8, -1, 1, 0}, -{ 4, s_7_9, -1, 1, 0}, -{ 5, s_7_10, -1, 1, 0}, -{ 2, s_7_11, -1, 1, 0} +static const struct among a_7[12] = { +{ 2, s_7_0, 0, 1, 0}, +{ 2, s_7_1, 0, 1, 0}, +{ 3, s_7_2, 0, 1, 0}, +{ 3, s_7_3, 0, 1, 0}, +{ 5, s_7_4, 0, 1, 0}, +{ 5, s_7_5, 0, 1, 0}, +{ 2, s_7_6, 0, 1, 0}, +{ 3, s_7_7, 0, 1, 0}, +{ 3, s_7_8, 0, 1, 0}, +{ 4, s_7_9, 0, 1, 0}, +{ 5, s_7_10, 0, 1, 0}, +{ 2, s_7_11, 0, 1, 0} }; static const symbol s_8_0[3] = { 'a', 'b', 'a' }; @@ -362,105 +370,103 @@ static const symbol s_8_92[3] = { 'a', 'r', 0xE9 }; static const symbol s_8_93[3] = { 'e', 'r', 0xE9 }; static const symbol s_8_94[3] = { 'i', 'r', 0xE9 }; static const symbol s_8_95[2] = { 'i', 0xF3 }; - -static const struct among a_8[96] = -{ -{ 3, s_8_0, -1, 2, 0}, -{ 3, s_8_1, -1, 2, 0}, -{ 3, s_8_2, -1, 2, 0}, -{ 3, s_8_3, -1, 2, 0}, -{ 4, s_8_4, -1, 2, 0}, -{ 2, s_8_5, -1, 2, 0}, -{ 4, s_8_6, 5, 2, 0}, -{ 4, s_8_7, 5, 2, 0}, -{ 4, s_8_8, 5, 2, 0}, -{ 2, s_8_9, -1, 2, 0}, -{ 2, s_8_10, -1, 2, 0}, -{ 2, s_8_11, -1, 2, 0}, -{ 3, s_8_12, -1, 2, 0}, -{ 4, s_8_13, -1, 2, 0}, -{ 4, s_8_14, -1, 2, 0}, -{ 4, s_8_15, -1, 2, 0}, -{ 2, s_8_16, -1, 2, 0}, -{ 4, s_8_17, 16, 2, 0}, -{ 4, s_8_18, 16, 2, 0}, -{ 5, s_8_19, 16, 2, 0}, -{ 3, s_8_20, 16, 2, 0}, -{ 5, s_8_21, 20, 2, 0}, -{ 5, s_8_22, 20, 2, 0}, -{ 5, s_8_23, 20, 2, 0}, -{ 2, s_8_24, -1, 1, 0}, -{ 4, s_8_25, 24, 2, 0}, -{ 5, s_8_26, 24, 2, 0}, -{ 4, s_8_27, -1, 2, 0}, -{ 5, s_8_28, -1, 2, 0}, -{ 4, s_8_29, -1, 2, 0}, -{ 4, s_8_30, -1, 2, 0}, -{ 4, s_8_31, -1, 2, 0}, -{ 3, s_8_32, -1, 2, 0}, -{ 3, s_8_33, -1, 2, 0}, -{ 4, s_8_34, -1, 2, 0}, -{ 5, s_8_35, -1, 2, 0}, -{ 2, s_8_36, -1, 2, 0}, -{ 2, s_8_37, -1, 2, 0}, -{ 2, s_8_38, -1, 2, 0}, -{ 2, s_8_39, -1, 2, 0}, -{ 4, s_8_40, 39, 2, 0}, -{ 4, s_8_41, 39, 2, 0}, -{ 4, s_8_42, 39, 2, 0}, -{ 4, s_8_43, 39, 2, 0}, -{ 5, s_8_44, 39, 2, 0}, -{ 3, s_8_45, 39, 2, 0}, -{ 5, s_8_46, 45, 2, 0}, -{ 5, s_8_47, 45, 2, 0}, -{ 5, s_8_48, 45, 2, 0}, -{ 2, s_8_49, -1, 1, 0}, -{ 4, s_8_50, 49, 2, 0}, -{ 5, s_8_51, 49, 2, 0}, -{ 5, s_8_52, -1, 2, 0}, -{ 5, s_8_53, -1, 2, 0}, -{ 6, s_8_54, -1, 2, 0}, -{ 4, s_8_55, -1, 2, 0}, -{ 6, s_8_56, 55, 2, 0}, -{ 6, s_8_57, 55, 2, 0}, -{ 6, s_8_58, 55, 2, 0}, -{ 5, s_8_59, -1, 2, 0}, -{ 6, s_8_60, -1, 2, 0}, -{ 6, s_8_61, -1, 2, 0}, -{ 6, s_8_62, -1, 2, 0}, -{ 3, s_8_63, -1, 2, 0}, -{ 3, s_8_64, -1, 1, 0}, -{ 5, s_8_65, 64, 2, 0}, -{ 5, s_8_66, 64, 2, 0}, -{ 5, s_8_67, 64, 2, 0}, -{ 4, s_8_68, -1, 2, 0}, -{ 4, s_8_69, -1, 2, 0}, -{ 4, s_8_70, -1, 2, 0}, -{ 6, s_8_71, 70, 2, 0}, -{ 6, s_8_72, 70, 2, 0}, -{ 7, s_8_73, 70, 2, 0}, -{ 5, s_8_74, 70, 2, 0}, -{ 7, s_8_75, 74, 2, 0}, -{ 7, s_8_76, 74, 2, 0}, -{ 7, s_8_77, 74, 2, 0}, -{ 4, s_8_78, -1, 1, 0}, -{ 6, s_8_79, 78, 2, 0}, -{ 6, s_8_80, 78, 2, 0}, -{ 6, s_8_81, 78, 2, 0}, -{ 6, s_8_82, 78, 2, 0}, -{ 7, s_8_83, 78, 2, 0}, -{ 4, s_8_84, -1, 2, 0}, -{ 4, s_8_85, -1, 2, 0}, -{ 4, s_8_86, -1, 2, 0}, -{ 4, s_8_87, -1, 2, 0}, -{ 2, s_8_88, -1, 2, 0}, -{ 3, s_8_89, -1, 2, 0}, -{ 3, s_8_90, -1, 2, 0}, -{ 3, s_8_91, -1, 2, 0}, -{ 3, s_8_92, -1, 2, 0}, -{ 3, s_8_93, -1, 2, 0}, -{ 3, s_8_94, -1, 2, 0}, -{ 2, s_8_95, -1, 2, 0} +static const struct among a_8[96] = { +{ 3, s_8_0, 0, 2, 0}, +{ 3, s_8_1, 0, 2, 0}, +{ 3, s_8_2, 0, 2, 0}, +{ 3, s_8_3, 0, 2, 0}, +{ 4, s_8_4, 0, 2, 0}, +{ 2, s_8_5, 0, 2, 0}, +{ 4, s_8_6, -1, 2, 0}, +{ 4, s_8_7, -2, 2, 0}, +{ 4, s_8_8, -3, 2, 0}, +{ 2, s_8_9, 0, 2, 0}, +{ 2, s_8_10, 0, 2, 0}, +{ 2, s_8_11, 0, 2, 0}, +{ 3, s_8_12, 0, 2, 0}, +{ 4, s_8_13, 0, 2, 0}, +{ 4, s_8_14, 0, 2, 0}, +{ 4, s_8_15, 0, 2, 0}, +{ 2, s_8_16, 0, 2, 0}, +{ 4, s_8_17, -1, 2, 0}, +{ 4, s_8_18, -2, 2, 0}, +{ 5, s_8_19, -3, 2, 0}, +{ 3, s_8_20, -4, 2, 0}, +{ 5, s_8_21, -1, 2, 0}, +{ 5, s_8_22, -2, 2, 0}, +{ 5, s_8_23, -3, 2, 0}, +{ 2, s_8_24, 0, 1, 0}, +{ 4, s_8_25, -1, 2, 0}, +{ 5, s_8_26, -2, 2, 0}, +{ 4, s_8_27, 0, 2, 0}, +{ 5, s_8_28, 0, 2, 0}, +{ 4, s_8_29, 0, 2, 0}, +{ 4, s_8_30, 0, 2, 0}, +{ 4, s_8_31, 0, 2, 0}, +{ 3, s_8_32, 0, 2, 0}, +{ 3, s_8_33, 0, 2, 0}, +{ 4, s_8_34, 0, 2, 0}, +{ 5, s_8_35, 0, 2, 0}, +{ 2, s_8_36, 0, 2, 0}, +{ 2, s_8_37, 0, 2, 0}, +{ 2, s_8_38, 0, 2, 0}, +{ 2, s_8_39, 0, 2, 0}, +{ 4, s_8_40, -1, 2, 0}, +{ 4, s_8_41, -2, 2, 0}, +{ 4, s_8_42, -3, 2, 0}, +{ 4, s_8_43, -4, 2, 0}, +{ 5, s_8_44, -5, 2, 0}, +{ 3, s_8_45, -6, 2, 0}, +{ 5, s_8_46, -1, 2, 0}, +{ 5, s_8_47, -2, 2, 0}, +{ 5, s_8_48, -3, 2, 0}, +{ 2, s_8_49, 0, 1, 0}, +{ 4, s_8_50, -1, 2, 0}, +{ 5, s_8_51, -2, 2, 0}, +{ 5, s_8_52, 0, 2, 0}, +{ 5, s_8_53, 0, 2, 0}, +{ 6, s_8_54, 0, 2, 0}, +{ 4, s_8_55, 0, 2, 0}, +{ 6, s_8_56, -1, 2, 0}, +{ 6, s_8_57, -2, 2, 0}, +{ 6, s_8_58, -3, 2, 0}, +{ 5, s_8_59, 0, 2, 0}, +{ 6, s_8_60, 0, 2, 0}, +{ 6, s_8_61, 0, 2, 0}, +{ 6, s_8_62, 0, 2, 0}, +{ 3, s_8_63, 0, 2, 0}, +{ 3, s_8_64, 0, 1, 0}, +{ 5, s_8_65, -1, 2, 0}, +{ 5, s_8_66, -2, 2, 0}, +{ 5, s_8_67, -3, 2, 0}, +{ 4, s_8_68, 0, 2, 0}, +{ 4, s_8_69, 0, 2, 0}, +{ 4, s_8_70, 0, 2, 0}, +{ 6, s_8_71, -1, 2, 0}, +{ 6, s_8_72, -2, 2, 0}, +{ 7, s_8_73, -3, 2, 0}, +{ 5, s_8_74, -4, 2, 0}, +{ 7, s_8_75, -1, 2, 0}, +{ 7, s_8_76, -2, 2, 0}, +{ 7, s_8_77, -3, 2, 0}, +{ 4, s_8_78, 0, 1, 0}, +{ 6, s_8_79, -1, 2, 0}, +{ 6, s_8_80, -2, 2, 0}, +{ 6, s_8_81, -3, 2, 0}, +{ 6, s_8_82, -4, 2, 0}, +{ 7, s_8_83, -5, 2, 0}, +{ 4, s_8_84, 0, 2, 0}, +{ 4, s_8_85, 0, 2, 0}, +{ 4, s_8_86, 0, 2, 0}, +{ 4, s_8_87, 0, 2, 0}, +{ 2, s_8_88, 0, 2, 0}, +{ 3, s_8_89, 0, 2, 0}, +{ 3, s_8_90, 0, 2, 0}, +{ 3, s_8_91, 0, 2, 0}, +{ 3, s_8_92, 0, 2, 0}, +{ 3, s_8_93, 0, 2, 0}, +{ 3, s_8_94, 0, 2, 0}, +{ 2, s_8_95, 0, 2, 0} }; static const symbol s_9_0[1] = { 'a' }; @@ -471,156 +477,136 @@ static const symbol s_9_4[1] = { 0xE1 }; static const symbol s_9_5[1] = { 0xE9 }; static const symbol s_9_6[1] = { 0xED }; static const symbol s_9_7[1] = { 0xF3 }; - -static const struct among a_9[8] = -{ -{ 1, s_9_0, -1, 1, 0}, -{ 1, s_9_1, -1, 2, 0}, -{ 1, s_9_2, -1, 1, 0}, -{ 2, s_9_3, -1, 1, 0}, -{ 1, s_9_4, -1, 1, 0}, -{ 1, s_9_5, -1, 2, 0}, -{ 1, s_9_6, -1, 1, 0}, -{ 1, s_9_7, -1, 1, 0} +static const struct among a_9[8] = { +{ 1, s_9_0, 0, 1, 0}, +{ 1, s_9_1, 0, 2, 0}, +{ 1, s_9_2, 0, 1, 0}, +{ 2, s_9_3, 0, 1, 0}, +{ 1, s_9_4, 0, 1, 0}, +{ 1, s_9_5, 0, 2, 0}, +{ 1, s_9_6, 0, 1, 0}, +{ 1, s_9_7, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 17, 4, 10 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'i' }; -static const symbol s_3[] = { 'o' }; -static const symbol s_4[] = { 'u' }; -static const symbol s_5[] = { 'i', 'e', 'n', 'd', 'o' }; -static const symbol s_6[] = { 'a', 'n', 'd', 'o' }; -static const symbol s_7[] = { 'a', 'r' }; -static const symbol s_8[] = { 'e', 'r' }; -static const symbol s_9[] = { 'i', 'r' }; -static const symbol s_10[] = { 'i', 'c' }; -static const symbol s_11[] = { 'l', 'o', 'g' }; -static const symbol s_12[] = { 'u' }; -static const symbol s_13[] = { 'e', 'n', 't', 'e' }; -static const symbol s_14[] = { 'a', 't' }; -static const symbol s_15[] = { 'a', 't' }; - static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping(z, g_v, 97, 252, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping(z, g_v, 97, 252, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping(z, g_v, 97, 252, 0)) goto lab2; { int ret = out_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping(z, g_v, 97, 252, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab1; { int ret = in_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping(z, g_v, 97, 252, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping(z, g_v, 97, 252, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping(z, g_v, 97, 252, 0)) goto lab3; { int ret = out_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping(z, g_v, 97, 252, 0)) goto lab0; if (z->c >= z->l) goto lab0; z->c++; - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || z->p[z->c + 0] >> 5 != 7 || !((67641858 >> (z->p[z->c + 0] & 0x1f)) & 1)) among_var = 6; else - among_var = find_among(z, a_0, 6); + among_var = find_among(z, a_0, 6, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; @@ -631,76 +617,84 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_attached_pronoun(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((557090 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_1, 13)) return 0; + if (!find_among_b(z, a_1, 13, 0)) return 0; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 111 && z->p[z->c - 1] != 114)) return 0; - among_var = find_among_b(z, a_2, 11); + among_var = find_among_b(z, a_2, 11, 0); if (!among_var) return 0; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } switch (among_var) { case 1: z->bra = z->c; - { int ret = slice_from_s(z, 5, s_5); + { + int ret = slice_from_s(z, 5, s_5); if (ret < 0) return ret; } break; case 2: z->bra = z->c; - { int ret = slice_from_s(z, 4, s_6); + { + int ret = slice_from_s(z, 4, s_6); if (ret < 0) return ret; } break; case 3: z->bra = z->c; - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 4: z->bra = z->c; - { int ret = slice_from_s(z, 2, s_8); + { + int ret = slice_from_s(z, 2, s_8); if (ret < 0) return ret; } break; case 5: z->bra = z->c; - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; case 6: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 7: if (z->c <= z->lb || z->p[z->c - 1] != 'u') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -712,34 +706,41 @@ static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((835634 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_6, 46); + among_var = find_among_b(z, a_6, 48, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_10))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_10))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -747,59 +748,72 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_11); + { + int ret = slice_from_s(z, 3, s_11); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 5: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 4, s_13); + { + int ret = slice_from_s(z, 4, s_13); if (ret < 0) return ret; } break; case 6: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m2; goto lab1; } - among_var = find_among_b(z, a_3, 4); - if (!among_var) { z->c = z->l - m2; goto lab1; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_2; goto lab1; } + among_var = find_among_b(z, a_3, 4, 0); + if (!among_var) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } switch (among_var) { case 1: z->ket = z->c; - if (!(eq_s_b(z, 2, s_14))) { z->c = z->l - m2; goto lab1; } + if (!(eq_s_b(z, 2, s_14))) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -809,22 +823,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 7: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c - 3 <= z->lb || z->p[z->c - 1] != 101) { z->c = z->l - m3; goto lab2; } - if (!find_among_b(z, a_4, 3)) { z->c = z->l - m3; goto lab2; } + if (z->c - 3 <= z->lb || z->p[z->c - 1] != 101) { z->c = z->l - v_3; goto lab2; } + if (!find_among_b(z, a_4, 3, 0)) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab2; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -832,22 +851,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m4; goto lab3; } - if (!find_among_b(z, a_5, 3)) { z->c = z->l - m4; goto lab3; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_4; goto lab3; } + if (!find_among_b(z, a_5, 3, 0)) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab3: @@ -855,21 +879,26 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 9: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m5; goto lab4; } + if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - v_5; goto lab4; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m5; goto lab4; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_5; goto lab4; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab4: @@ -881,18 +910,19 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_y_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (!find_among_b(z, a_7, 12)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_7, 12, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } if (z->c <= z->lb || z->p[z->c - 1] != 'u') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -900,36 +930,40 @@ static int r_y_verb_suffix(struct SN_env * z) { static int r_verb_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - among_var = find_among_b(z, a_8, 96); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_8, 96, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - m2; goto lab0; } + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - v_2; goto lab0; } z->c--; - { int m_test3 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - m2; goto lab0; } + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - v_2; goto lab0; } z->c--; - z->c = z->l - m_test3; + z->c = z->l - v_3; } lab0: ; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -940,40 +974,48 @@ static int r_verb_suffix(struct SN_env * z) { static int r_residual_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_9, 8); + among_var = find_among_b(z, a_9, 8, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - v_1; goto lab0; } z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - m1; goto lab0; } + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - v_1; goto lab0; } z->c--; - z->c = z->l - m_test2; + z->c = z->l - v_2; } - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -985,60 +1027,79 @@ static int r_residual_suffix(struct SN_env * z) { } extern int spanish_ISO_8859_1_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_attached_pronoun(z); + { + int v_1 = z->l - z->c; + { + int ret = r_attached_pronoun(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_y_verb_suffix(z); - if (ret == 0) goto lab3; + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_y_verb_suffix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab1; - lab3: - z->c = z->l - m3; - { int ret = r_verb_suffix(z); + break; + lab2: + z->c = z->l - v_3; + { + int ret = r_verb_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_residual_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_residual_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; - { int c5 = z->c; - { int ret = r_postlude(z); + { + int v_5 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c5; + z->c = v_5; } return 1; } -extern struct SN_env * spanish_ISO_8859_1_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * spanish_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void spanish_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void spanish_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_1_swedish.c b/src/backend/snowball/libstemmer/stem_ISO_8859_1_swedish.c index 6cf715d14c4d7..28552cf74ce61 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_1_swedish.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_1_swedish.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from swedish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_1_swedish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,133 +20,169 @@ extern int swedish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_other_suffix(struct SN_env * z); static int r_consonant_pair(struct SN_env * z); static int r_main_suffix(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - +static int r_et_condition(struct SN_env * z); -extern struct SN_env * swedish_ISO_8859_1_create_env(void); -extern void swedish_ISO_8859_1_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[1] = { 'a' }; -static const symbol s_0_1[4] = { 'a', 'r', 'n', 'a' }; -static const symbol s_0_2[4] = { 'e', 'r', 'n', 'a' }; -static const symbol s_0_3[7] = { 'h', 'e', 't', 'e', 'r', 'n', 'a' }; -static const symbol s_0_4[4] = { 'o', 'r', 'n', 'a' }; -static const symbol s_0_5[2] = { 'a', 'd' }; -static const symbol s_0_6[1] = { 'e' }; -static const symbol s_0_7[3] = { 'a', 'd', 'e' }; -static const symbol s_0_8[4] = { 'a', 'n', 'd', 'e' }; -static const symbol s_0_9[4] = { 'a', 'r', 'n', 'e' }; -static const symbol s_0_10[3] = { 'a', 'r', 'e' }; -static const symbol s_0_11[4] = { 'a', 's', 't', 'e' }; -static const symbol s_0_12[2] = { 'e', 'n' }; -static const symbol s_0_13[5] = { 'a', 'n', 'd', 'e', 'n' }; -static const symbol s_0_14[4] = { 'a', 'r', 'e', 'n' }; -static const symbol s_0_15[5] = { 'h', 'e', 't', 'e', 'n' }; -static const symbol s_0_16[3] = { 'e', 'r', 'n' }; -static const symbol s_0_17[2] = { 'a', 'r' }; -static const symbol s_0_18[2] = { 'e', 'r' }; -static const symbol s_0_19[5] = { 'h', 'e', 't', 'e', 'r' }; -static const symbol s_0_20[2] = { 'o', 'r' }; -static const symbol s_0_21[1] = { 's' }; -static const symbol s_0_22[2] = { 'a', 's' }; -static const symbol s_0_23[5] = { 'a', 'r', 'n', 'a', 's' }; -static const symbol s_0_24[5] = { 'e', 'r', 'n', 'a', 's' }; -static const symbol s_0_25[5] = { 'o', 'r', 'n', 'a', 's' }; -static const symbol s_0_26[2] = { 'e', 's' }; -static const symbol s_0_27[4] = { 'a', 'd', 'e', 's' }; -static const symbol s_0_28[5] = { 'a', 'n', 'd', 'e', 's' }; -static const symbol s_0_29[3] = { 'e', 'n', 's' }; -static const symbol s_0_30[5] = { 'a', 'r', 'e', 'n', 's' }; -static const symbol s_0_31[6] = { 'h', 'e', 't', 'e', 'n', 's' }; -static const symbol s_0_32[4] = { 'e', 'r', 'n', 's' }; -static const symbol s_0_33[2] = { 'a', 't' }; -static const symbol s_0_34[5] = { 'a', 'n', 'd', 'e', 't' }; -static const symbol s_0_35[3] = { 'h', 'e', 't' }; -static const symbol s_0_36[3] = { 'a', 's', 't' }; +static const symbol s_0[] = { 'e', 't' }; +static const symbol s_1[] = { 0xF6, 's' }; +static const symbol s_2[] = { 'f', 'u', 'l', 'l' }; -static const struct among a_0[37] = -{ -{ 1, s_0_0, -1, 1, 0}, -{ 4, s_0_1, 0, 1, 0}, -{ 4, s_0_2, 0, 1, 0}, -{ 7, s_0_3, 2, 1, 0}, -{ 4, s_0_4, 0, 1, 0}, -{ 2, s_0_5, -1, 1, 0}, -{ 1, s_0_6, -1, 1, 0}, -{ 3, s_0_7, 6, 1, 0}, -{ 4, s_0_8, 6, 1, 0}, -{ 4, s_0_9, 6, 1, 0}, -{ 3, s_0_10, 6, 1, 0}, -{ 4, s_0_11, 6, 1, 0}, -{ 2, s_0_12, -1, 1, 0}, -{ 5, s_0_13, 12, 1, 0}, -{ 4, s_0_14, 12, 1, 0}, -{ 5, s_0_15, 12, 1, 0}, -{ 3, s_0_16, -1, 1, 0}, -{ 2, s_0_17, -1, 1, 0}, -{ 2, s_0_18, -1, 1, 0}, -{ 5, s_0_19, 18, 1, 0}, -{ 2, s_0_20, -1, 1, 0}, -{ 1, s_0_21, -1, 2, 0}, -{ 2, s_0_22, 21, 1, 0}, -{ 5, s_0_23, 22, 1, 0}, -{ 5, s_0_24, 22, 1, 0}, -{ 5, s_0_25, 22, 1, 0}, -{ 2, s_0_26, 21, 1, 0}, -{ 4, s_0_27, 26, 1, 0}, -{ 5, s_0_28, 26, 1, 0}, -{ 3, s_0_29, 21, 1, 0}, -{ 5, s_0_30, 29, 1, 0}, -{ 6, s_0_31, 29, 1, 0}, -{ 4, s_0_32, 21, 1, 0}, -{ 2, s_0_33, -1, 1, 0}, -{ 5, s_0_34, -1, 1, 0}, -{ 3, s_0_35, -1, 1, 0}, -{ 3, s_0_36, -1, 1, 0} +static const symbol s_0_0[3] = { 'f', 'a', 'b' }; +static const symbol s_0_1[1] = { 'h' }; +static const symbol s_0_2[3] = { 'p', 'a', 'k' }; +static const symbol s_0_3[3] = { 'r', 'a', 'k' }; +static const symbol s_0_4[4] = { 's', 't', 'a', 'k' }; +static const symbol s_0_5[3] = { 'k', 'o', 'm' }; +static const symbol s_0_6[3] = { 'i', 'e', 't' }; +static const symbol s_0_7[3] = { 'c', 'i', 't' }; +static const symbol s_0_8[3] = { 'd', 'i', 't' }; +static const symbol s_0_9[4] = { 'a', 'l', 'i', 't' }; +static const symbol s_0_10[4] = { 'i', 'l', 'i', 't' }; +static const symbol s_0_11[3] = { 'm', 'i', 't' }; +static const symbol s_0_12[3] = { 'n', 'i', 't' }; +static const symbol s_0_13[3] = { 'p', 'i', 't' }; +static const symbol s_0_14[3] = { 'r', 'i', 't' }; +static const symbol s_0_15[3] = { 's', 'i', 't' }; +static const symbol s_0_16[3] = { 't', 'i', 't' }; +static const symbol s_0_17[3] = { 'u', 'i', 't' }; +static const symbol s_0_18[4] = { 'i', 'v', 'i', 't' }; +static const symbol s_0_19[4] = { 'k', 'v', 'i', 't' }; +static const symbol s_0_20[3] = { 'x', 'i', 't' }; +static const struct among a_0[21] = { +{ 3, s_0_0, 0, -1, 0}, +{ 1, s_0_1, 0, -1, 0}, +{ 3, s_0_2, 0, -1, 0}, +{ 3, s_0_3, 0, -1, 0}, +{ 4, s_0_4, 0, -1, 0}, +{ 3, s_0_5, 0, -1, 0}, +{ 3, s_0_6, 0, -1, 0}, +{ 3, s_0_7, 0, -1, 0}, +{ 3, s_0_8, 0, -1, 0}, +{ 4, s_0_9, 0, -1, 0}, +{ 4, s_0_10, 0, -1, 0}, +{ 3, s_0_11, 0, -1, 0}, +{ 3, s_0_12, 0, -1, 0}, +{ 3, s_0_13, 0, -1, 0}, +{ 3, s_0_14, 0, -1, 0}, +{ 3, s_0_15, 0, -1, 0}, +{ 3, s_0_16, 0, -1, 0}, +{ 3, s_0_17, 0, -1, 0}, +{ 4, s_0_18, 0, -1, 0}, +{ 4, s_0_19, 0, -1, 0}, +{ 3, s_0_20, 0, -1, 0} }; -static const symbol s_1_0[2] = { 'd', 'd' }; -static const symbol s_1_1[2] = { 'g', 'd' }; -static const symbol s_1_2[2] = { 'n', 'n' }; -static const symbol s_1_3[2] = { 'd', 't' }; -static const symbol s_1_4[2] = { 'g', 't' }; -static const symbol s_1_5[2] = { 'k', 't' }; -static const symbol s_1_6[2] = { 't', 't' }; - -static const struct among a_1[7] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 2, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0}, -{ 2, s_1_4, -1, -1, 0}, -{ 2, s_1_5, -1, -1, 0}, -{ 2, s_1_6, -1, -1, 0} +static const symbol s_1_0[1] = { 'a' }; +static const symbol s_1_1[4] = { 'a', 'r', 'n', 'a' }; +static const symbol s_1_2[4] = { 'e', 'r', 'n', 'a' }; +static const symbol s_1_3[7] = { 'h', 'e', 't', 'e', 'r', 'n', 'a' }; +static const symbol s_1_4[4] = { 'o', 'r', 'n', 'a' }; +static const symbol s_1_5[2] = { 'a', 'd' }; +static const symbol s_1_6[1] = { 'e' }; +static const symbol s_1_7[3] = { 'a', 'd', 'e' }; +static const symbol s_1_8[4] = { 'a', 'n', 'd', 'e' }; +static const symbol s_1_9[4] = { 'a', 'r', 'n', 'e' }; +static const symbol s_1_10[3] = { 'a', 'r', 'e' }; +static const symbol s_1_11[4] = { 'a', 's', 't', 'e' }; +static const symbol s_1_12[2] = { 'e', 'n' }; +static const symbol s_1_13[5] = { 'a', 'n', 'd', 'e', 'n' }; +static const symbol s_1_14[4] = { 'a', 'r', 'e', 'n' }; +static const symbol s_1_15[5] = { 'h', 'e', 't', 'e', 'n' }; +static const symbol s_1_16[3] = { 'e', 'r', 'n' }; +static const symbol s_1_17[2] = { 'a', 'r' }; +static const symbol s_1_18[2] = { 'e', 'r' }; +static const symbol s_1_19[5] = { 'h', 'e', 't', 'e', 'r' }; +static const symbol s_1_20[2] = { 'o', 'r' }; +static const symbol s_1_21[1] = { 's' }; +static const symbol s_1_22[2] = { 'a', 's' }; +static const symbol s_1_23[5] = { 'a', 'r', 'n', 'a', 's' }; +static const symbol s_1_24[5] = { 'e', 'r', 'n', 'a', 's' }; +static const symbol s_1_25[5] = { 'o', 'r', 'n', 'a', 's' }; +static const symbol s_1_26[2] = { 'e', 's' }; +static const symbol s_1_27[4] = { 'a', 'd', 'e', 's' }; +static const symbol s_1_28[5] = { 'a', 'n', 'd', 'e', 's' }; +static const symbol s_1_29[3] = { 'e', 'n', 's' }; +static const symbol s_1_30[5] = { 'a', 'r', 'e', 'n', 's' }; +static const symbol s_1_31[6] = { 'h', 'e', 't', 'e', 'n', 's' }; +static const symbol s_1_32[4] = { 'e', 'r', 'n', 's' }; +static const symbol s_1_33[2] = { 'a', 't' }; +static const symbol s_1_34[2] = { 'e', 't' }; +static const symbol s_1_35[5] = { 'a', 'n', 'd', 'e', 't' }; +static const symbol s_1_36[3] = { 'h', 'e', 't' }; +static const symbol s_1_37[3] = { 'a', 's', 't' }; +static const struct among a_1[38] = { +{ 1, s_1_0, 0, 1, 0}, +{ 4, s_1_1, -1, 1, 0}, +{ 4, s_1_2, -2, 1, 0}, +{ 7, s_1_3, -1, 1, 0}, +{ 4, s_1_4, -4, 1, 0}, +{ 2, s_1_5, 0, 1, 0}, +{ 1, s_1_6, 0, 1, 0}, +{ 3, s_1_7, -1, 1, 0}, +{ 4, s_1_8, -2, 1, 0}, +{ 4, s_1_9, -3, 1, 0}, +{ 3, s_1_10, -4, 1, 0}, +{ 4, s_1_11, -5, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 5, s_1_13, -1, 1, 0}, +{ 4, s_1_14, -2, 1, 0}, +{ 5, s_1_15, -3, 1, 0}, +{ 3, s_1_16, 0, 1, 0}, +{ 2, s_1_17, 0, 1, 0}, +{ 2, s_1_18, 0, 1, 0}, +{ 5, s_1_19, -1, 1, 0}, +{ 2, s_1_20, 0, 1, 0}, +{ 1, s_1_21, 0, 2, 0}, +{ 2, s_1_22, -1, 1, 0}, +{ 5, s_1_23, -1, 1, 0}, +{ 5, s_1_24, -2, 1, 0}, +{ 5, s_1_25, -3, 1, 0}, +{ 2, s_1_26, -5, 1, 0}, +{ 4, s_1_27, -1, 1, 0}, +{ 5, s_1_28, -2, 1, 0}, +{ 3, s_1_29, -8, 1, 0}, +{ 5, s_1_30, -1, 1, 0}, +{ 6, s_1_31, -2, 1, 0}, +{ 4, s_1_32, -11, 1, 0}, +{ 2, s_1_33, 0, 1, 0}, +{ 2, s_1_34, 0, 3, 0}, +{ 5, s_1_35, -1, 1, 0}, +{ 3, s_1_36, -2, 1, 0}, +{ 3, s_1_37, 0, 1, 0} }; -static const symbol s_2_0[2] = { 'i', 'g' }; -static const symbol s_2_1[3] = { 'l', 'i', 'g' }; -static const symbol s_2_2[3] = { 'e', 'l', 's' }; -static const symbol s_2_3[5] = { 'f', 'u', 'l', 'l', 't' }; -static const symbol s_2_4[3] = { 0xF6, 's', 't' }; +static const symbol s_2_0[2] = { 'd', 'd' }; +static const symbol s_2_1[2] = { 'g', 'd' }; +static const symbol s_2_2[2] = { 'n', 'n' }; +static const symbol s_2_3[2] = { 'd', 't' }; +static const symbol s_2_4[2] = { 'g', 't' }; +static const symbol s_2_5[2] = { 'k', 't' }; +static const symbol s_2_6[2] = { 't', 't' }; +static const struct among a_2[7] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, -1, 0}, +{ 2, s_2_4, 0, -1, 0}, +{ 2, s_2_5, 0, -1, 0}, +{ 2, s_2_6, 0, -1, 0} +}; -static const struct among a_2[5] = -{ -{ 2, s_2_0, -1, 1, 0}, -{ 3, s_2_1, 0, 1, 0}, -{ 3, s_2_2, -1, 1, 0}, -{ 5, s_2_3, -1, 3, 0}, -{ 3, s_2_4, -1, 2, 0} +static const symbol s_3_0[2] = { 'i', 'g' }; +static const symbol s_3_1[3] = { 'l', 'i', 'g' }; +static const symbol s_3_2[3] = { 'e', 'l', 's' }; +static const symbol s_3_3[5] = { 'f', 'u', 'l', 'l', 't' }; +static const symbol s_3_4[3] = { 0xF6, 's', 't' }; +static const struct among a_3[5] = { +{ 2, s_3_0, 0, 1, 0}, +{ 3, s_3_1, -1, 1, 0}, +{ 3, s_3_2, 0, 1, 0}, +{ 5, s_3_3, 0, 3, 0}, +{ 3, s_3_4, 0, 2, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 32 }; @@ -144,55 +191,101 @@ static const unsigned char g_s_ending[] = { 119, 127, 149 }; static const unsigned char g_ost_ending[] = { 173, 58 }; -static const symbol s_0[] = { 0xF6, 's' }; -static const symbol s_1[] = { 'f', 'u', 'l', 'l' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c_test1 = z->c; -z->c = z->c + 3; - if (z->c > z->l) return 0; - z->I[0] = z->c; - z->c = c_test1; + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + if (z->c + 3 > z->l) return 0; + z->c += 3; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping(z, g_v, 97, 246, 1); + if (ret < 0) return 0; + z->c += ret; } - - if (out_grouping(z, g_v, 97, 246, 1) < 0) return 0; - { int ret = in_grouping(z, g_v, 97, 246, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab0; - z->I[1] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: return 1; } +static int r_et_condition(struct SN_env * z) { + { + int v_1 = z->l - z->c; + if (out_grouping_b(z, g_v, 97, 246, 0)) return 0; + if (in_grouping_b(z, g_v, 97, 246, 0)) return 0; + if (z->c > z->lb) goto lab0; + return 0; + lab0: + z->c = z->l - v_1; + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1059076 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab1; + if (!find_among_b(z, a_0, 21, 0)) goto lab1; + return 0; + lab1: + z->c = z->l - v_2; + } + } + return 1; +} + static int r_main_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851442 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 37); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851442 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_1, 38, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - if (in_grouping_b(z, g_s_ending, 98, 121, 0)) return 0; - { int ret = slice_del(z); + do { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_0))) goto lab0; + { + int ret = r_et_condition(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + z->bra = z->c; + break; + lab0: + z->c = z->l - v_2; + if (in_grouping_b(z, g_s_ending, 98, 121, 0)) return 0; + } while (0); + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_et_condition(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -201,54 +294,59 @@ static int r_main_suffix(struct SN_env * z) { } static int r_consonant_pair(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; - { int m2 = z->l - z->c; (void)m2; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1064976 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_1, 7)) { z->lb = mlimit1; return 0; } - z->c = z->l - m2; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; + { + int v_2 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1064976 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_2, 7, 0)) { z->lb = v_1; return 0; } + z->c = z->l - v_2; z->ket = z->c; - if (z->c <= z->lb) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb) { z->lb = v_1; return 0; } z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_other_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_2, 5); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_3, 5, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b(z, g_ost_ending, 105, 118, 0)) return 0; - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_1); + { + int ret = slice_from_s(z, 4, s_2); if (ret < 0) return ret; } break; @@ -257,37 +355,52 @@ static int r_other_suffix(struct SN_env * z) { } extern int swedish_ISO_8859_1_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_main_suffix(z); + { + int v_2 = z->l - z->c; + { + int ret = r_main_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_other_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_other_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; return 1; } -extern struct SN_env * swedish_ISO_8859_1_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * swedish_ISO_8859_1_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void swedish_ISO_8859_1_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void swedish_ISO_8859_1_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_2_hungarian.c b/src/backend/snowball/libstemmer/stem_ISO_8859_2_hungarian.c index 7308963105d3f..2d4ffac098c68 100644 --- a/src/backend/snowball/libstemmer/stem_ISO_8859_2_hungarian.c +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_2_hungarian.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from hungarian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_ISO_8859_2_hungarian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +20,7 @@ extern int hungarian_ISO_8859_2_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_double(struct SN_env * z); static int r_undouble(struct SN_env * z); static int r_factive(struct SN_env * z); @@ -23,514 +35,452 @@ static int r_case(struct SN_env * z); static int r_v_ending(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * hungarian_ISO_8859_2_create_env(void); -extern void hungarian_ISO_8859_2_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'e' }; +static const symbol s_3[] = { 'a' }; +static const symbol s_4[] = { 'a' }; +static const symbol s_5[] = { 'e' }; +static const symbol s_6[] = { 'a' }; +static const symbol s_7[] = { 'e' }; +static const symbol s_8[] = { 'e' }; +static const symbol s_9[] = { 'a' }; +static const symbol s_10[] = { 'a' }; +static const symbol s_11[] = { 'e' }; +static const symbol s_12[] = { 'a' }; +static const symbol s_13[] = { 'e' }; -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[2] = { 'c', 's' }; -static const symbol s_0_1[3] = { 'd', 'z', 's' }; -static const symbol s_0_2[2] = { 'g', 'y' }; -static const symbol s_0_3[2] = { 'l', 'y' }; -static const symbol s_0_4[2] = { 'n', 'y' }; -static const symbol s_0_5[2] = { 's', 'z' }; -static const symbol s_0_6[2] = { 't', 'y' }; -static const symbol s_0_7[2] = { 'z', 's' }; - -static const struct among a_0[8] = -{ -{ 2, s_0_0, -1, -1, 0}, -{ 3, s_0_1, -1, -1, 0}, -{ 2, s_0_2, -1, -1, 0}, -{ 2, s_0_3, -1, -1, 0}, -{ 2, s_0_4, -1, -1, 0}, -{ 2, s_0_5, -1, -1, 0}, -{ 2, s_0_6, -1, -1, 0}, -{ 2, s_0_7, -1, -1, 0} -}; - -static const symbol s_1_0[1] = { 0xE1 }; -static const symbol s_1_1[1] = { 0xE9 }; - -static const struct among a_1[2] = -{ -{ 1, s_1_0, -1, 1, 0}, -{ 1, s_1_1, -1, 2, 0} -}; - -static const symbol s_2_0[2] = { 'b', 'b' }; -static const symbol s_2_1[2] = { 'c', 'c' }; -static const symbol s_2_2[2] = { 'd', 'd' }; -static const symbol s_2_3[2] = { 'f', 'f' }; -static const symbol s_2_4[2] = { 'g', 'g' }; -static const symbol s_2_5[2] = { 'j', 'j' }; -static const symbol s_2_6[2] = { 'k', 'k' }; -static const symbol s_2_7[2] = { 'l', 'l' }; -static const symbol s_2_8[2] = { 'm', 'm' }; -static const symbol s_2_9[2] = { 'n', 'n' }; -static const symbol s_2_10[2] = { 'p', 'p' }; -static const symbol s_2_11[2] = { 'r', 'r' }; -static const symbol s_2_12[3] = { 'c', 'c', 's' }; -static const symbol s_2_13[2] = { 's', 's' }; -static const symbol s_2_14[3] = { 'z', 'z', 's' }; -static const symbol s_2_15[2] = { 't', 't' }; -static const symbol s_2_16[2] = { 'v', 'v' }; -static const symbol s_2_17[3] = { 'g', 'g', 'y' }; -static const symbol s_2_18[3] = { 'l', 'l', 'y' }; -static const symbol s_2_19[3] = { 'n', 'n', 'y' }; -static const symbol s_2_20[3] = { 't', 't', 'y' }; -static const symbol s_2_21[3] = { 's', 's', 'z' }; -static const symbol s_2_22[2] = { 'z', 'z' }; - -static const struct among a_2[23] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0}, -{ 2, s_2_3, -1, -1, 0}, -{ 2, s_2_4, -1, -1, 0}, -{ 2, s_2_5, -1, -1, 0}, -{ 2, s_2_6, -1, -1, 0}, -{ 2, s_2_7, -1, -1, 0}, -{ 2, s_2_8, -1, -1, 0}, -{ 2, s_2_9, -1, -1, 0}, -{ 2, s_2_10, -1, -1, 0}, -{ 2, s_2_11, -1, -1, 0}, -{ 3, s_2_12, -1, -1, 0}, -{ 2, s_2_13, -1, -1, 0}, -{ 3, s_2_14, -1, -1, 0}, -{ 2, s_2_15, -1, -1, 0}, -{ 2, s_2_16, -1, -1, 0}, -{ 3, s_2_17, -1, -1, 0}, -{ 3, s_2_18, -1, -1, 0}, -{ 3, s_2_19, -1, -1, 0}, -{ 3, s_2_20, -1, -1, 0}, -{ 3, s_2_21, -1, -1, 0}, -{ 2, s_2_22, -1, -1, 0} +static const symbol s_0_0[1] = { 0xE1 }; +static const symbol s_0_1[1] = { 0xE9 }; +static const struct among a_0[2] = { +{ 1, s_0_0, 0, 1, 0}, +{ 1, s_0_1, 0, 2, 0} }; -static const symbol s_3_0[2] = { 'a', 'l' }; -static const symbol s_3_1[2] = { 'e', 'l' }; - -static const struct among a_3[2] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 1, 0} +static const symbol s_1_0[2] = { 'b', 'b' }; +static const symbol s_1_1[2] = { 'c', 'c' }; +static const symbol s_1_2[2] = { 'd', 'd' }; +static const symbol s_1_3[2] = { 'f', 'f' }; +static const symbol s_1_4[2] = { 'g', 'g' }; +static const symbol s_1_5[2] = { 'j', 'j' }; +static const symbol s_1_6[2] = { 'k', 'k' }; +static const symbol s_1_7[2] = { 'l', 'l' }; +static const symbol s_1_8[2] = { 'm', 'm' }; +static const symbol s_1_9[2] = { 'n', 'n' }; +static const symbol s_1_10[2] = { 'p', 'p' }; +static const symbol s_1_11[2] = { 'r', 'r' }; +static const symbol s_1_12[3] = { 'c', 'c', 's' }; +static const symbol s_1_13[2] = { 's', 's' }; +static const symbol s_1_14[3] = { 'z', 'z', 's' }; +static const symbol s_1_15[2] = { 't', 't' }; +static const symbol s_1_16[2] = { 'v', 'v' }; +static const symbol s_1_17[3] = { 'g', 'g', 'y' }; +static const symbol s_1_18[3] = { 'l', 'l', 'y' }; +static const symbol s_1_19[3] = { 'n', 'n', 'y' }; +static const symbol s_1_20[3] = { 't', 't', 'y' }; +static const symbol s_1_21[3] = { 's', 's', 'z' }; +static const symbol s_1_22[2] = { 'z', 'z' }; +static const struct among a_1[23] = { +{ 2, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 2, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 2, s_1_4, 0, -1, 0}, +{ 2, s_1_5, 0, -1, 0}, +{ 2, s_1_6, 0, -1, 0}, +{ 2, s_1_7, 0, -1, 0}, +{ 2, s_1_8, 0, -1, 0}, +{ 2, s_1_9, 0, -1, 0}, +{ 2, s_1_10, 0, -1, 0}, +{ 2, s_1_11, 0, -1, 0}, +{ 3, s_1_12, 0, -1, 0}, +{ 2, s_1_13, 0, -1, 0}, +{ 3, s_1_14, 0, -1, 0}, +{ 2, s_1_15, 0, -1, 0}, +{ 2, s_1_16, 0, -1, 0}, +{ 3, s_1_17, 0, -1, 0}, +{ 3, s_1_18, 0, -1, 0}, +{ 3, s_1_19, 0, -1, 0}, +{ 3, s_1_20, 0, -1, 0}, +{ 3, s_1_21, 0, -1, 0}, +{ 2, s_1_22, 0, -1, 0} }; -static const symbol s_4_0[2] = { 'b', 'a' }; -static const symbol s_4_1[2] = { 'r', 'a' }; -static const symbol s_4_2[2] = { 'b', 'e' }; -static const symbol s_4_3[2] = { 'r', 'e' }; -static const symbol s_4_4[2] = { 'i', 'g' }; -static const symbol s_4_5[3] = { 'n', 'a', 'k' }; -static const symbol s_4_6[3] = { 'n', 'e', 'k' }; -static const symbol s_4_7[3] = { 'v', 'a', 'l' }; -static const symbol s_4_8[3] = { 'v', 'e', 'l' }; -static const symbol s_4_9[2] = { 'u', 'l' }; -static const symbol s_4_10[3] = { 'n', 0xE1, 'l' }; -static const symbol s_4_11[3] = { 'n', 0xE9, 'l' }; -static const symbol s_4_12[3] = { 'b', 0xF3, 'l' }; -static const symbol s_4_13[3] = { 'r', 0xF3, 'l' }; -static const symbol s_4_14[3] = { 't', 0xF3, 'l' }; -static const symbol s_4_15[3] = { 'b', 0xF5, 'l' }; -static const symbol s_4_16[3] = { 'r', 0xF5, 'l' }; -static const symbol s_4_17[3] = { 't', 0xF5, 'l' }; -static const symbol s_4_18[2] = { 0xFC, 'l' }; -static const symbol s_4_19[1] = { 'n' }; -static const symbol s_4_20[2] = { 'a', 'n' }; -static const symbol s_4_21[3] = { 'b', 'a', 'n' }; -static const symbol s_4_22[2] = { 'e', 'n' }; -static const symbol s_4_23[3] = { 'b', 'e', 'n' }; -static const symbol s_4_24[6] = { 'k', 0xE9, 'p', 'p', 'e', 'n' }; -static const symbol s_4_25[2] = { 'o', 'n' }; -static const symbol s_4_26[2] = { 0xF6, 'n' }; -static const symbol s_4_27[4] = { 'k', 0xE9, 'p', 'p' }; -static const symbol s_4_28[3] = { 'k', 'o', 'r' }; -static const symbol s_4_29[1] = { 't' }; -static const symbol s_4_30[2] = { 'a', 't' }; -static const symbol s_4_31[2] = { 'e', 't' }; -static const symbol s_4_32[4] = { 'k', 0xE9, 'n', 't' }; -static const symbol s_4_33[6] = { 'a', 'n', 'k', 0xE9, 'n', 't' }; -static const symbol s_4_34[6] = { 'e', 'n', 'k', 0xE9, 'n', 't' }; -static const symbol s_4_35[6] = { 'o', 'n', 'k', 0xE9, 'n', 't' }; -static const symbol s_4_36[2] = { 'o', 't' }; -static const symbol s_4_37[3] = { 0xE9, 'r', 't' }; -static const symbol s_4_38[2] = { 0xF6, 't' }; -static const symbol s_4_39[3] = { 'h', 'e', 'z' }; -static const symbol s_4_40[3] = { 'h', 'o', 'z' }; -static const symbol s_4_41[3] = { 'h', 0xF6, 'z' }; -static const symbol s_4_42[2] = { 'v', 0xE1 }; -static const symbol s_4_43[2] = { 'v', 0xE9 }; - -static const struct among a_4[44] = -{ -{ 2, s_4_0, -1, -1, 0}, -{ 2, s_4_1, -1, -1, 0}, -{ 2, s_4_2, -1, -1, 0}, -{ 2, s_4_3, -1, -1, 0}, -{ 2, s_4_4, -1, -1, 0}, -{ 3, s_4_5, -1, -1, 0}, -{ 3, s_4_6, -1, -1, 0}, -{ 3, s_4_7, -1, -1, 0}, -{ 3, s_4_8, -1, -1, 0}, -{ 2, s_4_9, -1, -1, 0}, -{ 3, s_4_10, -1, -1, 0}, -{ 3, s_4_11, -1, -1, 0}, -{ 3, s_4_12, -1, -1, 0}, -{ 3, s_4_13, -1, -1, 0}, -{ 3, s_4_14, -1, -1, 0}, -{ 3, s_4_15, -1, -1, 0}, -{ 3, s_4_16, -1, -1, 0}, -{ 3, s_4_17, -1, -1, 0}, -{ 2, s_4_18, -1, -1, 0}, -{ 1, s_4_19, -1, -1, 0}, -{ 2, s_4_20, 19, -1, 0}, -{ 3, s_4_21, 20, -1, 0}, -{ 2, s_4_22, 19, -1, 0}, -{ 3, s_4_23, 22, -1, 0}, -{ 6, s_4_24, 22, -1, 0}, -{ 2, s_4_25, 19, -1, 0}, -{ 2, s_4_26, 19, -1, 0}, -{ 4, s_4_27, -1, -1, 0}, -{ 3, s_4_28, -1, -1, 0}, -{ 1, s_4_29, -1, -1, 0}, -{ 2, s_4_30, 29, -1, 0}, -{ 2, s_4_31, 29, -1, 0}, -{ 4, s_4_32, 29, -1, 0}, -{ 6, s_4_33, 32, -1, 0}, -{ 6, s_4_34, 32, -1, 0}, -{ 6, s_4_35, 32, -1, 0}, -{ 2, s_4_36, 29, -1, 0}, -{ 3, s_4_37, 29, -1, 0}, -{ 2, s_4_38, 29, -1, 0}, -{ 3, s_4_39, -1, -1, 0}, -{ 3, s_4_40, -1, -1, 0}, -{ 3, s_4_41, -1, -1, 0}, -{ 2, s_4_42, -1, -1, 0}, -{ 2, s_4_43, -1, -1, 0} +static const symbol s_2_0[2] = { 'a', 'l' }; +static const symbol s_2_1[2] = { 'e', 'l' }; +static const struct among a_2[2] = { +{ 2, s_2_0, 0, 1, 0}, +{ 2, s_2_1, 0, 1, 0} }; -static const symbol s_5_0[2] = { 0xE1, 'n' }; -static const symbol s_5_1[2] = { 0xE9, 'n' }; -static const symbol s_5_2[6] = { 0xE1, 'n', 'k', 0xE9, 'n', 't' }; - -static const struct among a_5[3] = -{ -{ 2, s_5_0, -1, 2, 0}, -{ 2, s_5_1, -1, 1, 0}, -{ 6, s_5_2, -1, 2, 0} +static const symbol s_3_0[2] = { 'b', 'a' }; +static const symbol s_3_1[2] = { 'r', 'a' }; +static const symbol s_3_2[2] = { 'b', 'e' }; +static const symbol s_3_3[2] = { 'r', 'e' }; +static const symbol s_3_4[2] = { 'i', 'g' }; +static const symbol s_3_5[3] = { 'n', 'a', 'k' }; +static const symbol s_3_6[3] = { 'n', 'e', 'k' }; +static const symbol s_3_7[3] = { 'v', 'a', 'l' }; +static const symbol s_3_8[3] = { 'v', 'e', 'l' }; +static const symbol s_3_9[2] = { 'u', 'l' }; +static const symbol s_3_10[3] = { 'n', 0xE1, 'l' }; +static const symbol s_3_11[3] = { 'n', 0xE9, 'l' }; +static const symbol s_3_12[3] = { 'b', 0xF3, 'l' }; +static const symbol s_3_13[3] = { 'r', 0xF3, 'l' }; +static const symbol s_3_14[3] = { 't', 0xF3, 'l' }; +static const symbol s_3_15[3] = { 'b', 0xF5, 'l' }; +static const symbol s_3_16[3] = { 'r', 0xF5, 'l' }; +static const symbol s_3_17[3] = { 't', 0xF5, 'l' }; +static const symbol s_3_18[2] = { 0xFC, 'l' }; +static const symbol s_3_19[1] = { 'n' }; +static const symbol s_3_20[2] = { 'a', 'n' }; +static const symbol s_3_21[3] = { 'b', 'a', 'n' }; +static const symbol s_3_22[2] = { 'e', 'n' }; +static const symbol s_3_23[3] = { 'b', 'e', 'n' }; +static const symbol s_3_24[6] = { 'k', 0xE9, 'p', 'p', 'e', 'n' }; +static const symbol s_3_25[2] = { 'o', 'n' }; +static const symbol s_3_26[2] = { 0xF6, 'n' }; +static const symbol s_3_27[4] = { 'k', 0xE9, 'p', 'p' }; +static const symbol s_3_28[3] = { 'k', 'o', 'r' }; +static const symbol s_3_29[1] = { 't' }; +static const symbol s_3_30[2] = { 'a', 't' }; +static const symbol s_3_31[2] = { 'e', 't' }; +static const symbol s_3_32[4] = { 'k', 0xE9, 'n', 't' }; +static const symbol s_3_33[6] = { 'a', 'n', 'k', 0xE9, 'n', 't' }; +static const symbol s_3_34[6] = { 'e', 'n', 'k', 0xE9, 'n', 't' }; +static const symbol s_3_35[6] = { 'o', 'n', 'k', 0xE9, 'n', 't' }; +static const symbol s_3_36[2] = { 'o', 't' }; +static const symbol s_3_37[3] = { 0xE9, 'r', 't' }; +static const symbol s_3_38[2] = { 0xF6, 't' }; +static const symbol s_3_39[3] = { 'h', 'e', 'z' }; +static const symbol s_3_40[3] = { 'h', 'o', 'z' }; +static const symbol s_3_41[3] = { 'h', 0xF6, 'z' }; +static const symbol s_3_42[2] = { 'v', 0xE1 }; +static const symbol s_3_43[2] = { 'v', 0xE9 }; +static const struct among a_3[44] = { +{ 2, s_3_0, 0, -1, 0}, +{ 2, s_3_1, 0, -1, 0}, +{ 2, s_3_2, 0, -1, 0}, +{ 2, s_3_3, 0, -1, 0}, +{ 2, s_3_4, 0, -1, 0}, +{ 3, s_3_5, 0, -1, 0}, +{ 3, s_3_6, 0, -1, 0}, +{ 3, s_3_7, 0, -1, 0}, +{ 3, s_3_8, 0, -1, 0}, +{ 2, s_3_9, 0, -1, 0}, +{ 3, s_3_10, 0, -1, 0}, +{ 3, s_3_11, 0, -1, 0}, +{ 3, s_3_12, 0, -1, 0}, +{ 3, s_3_13, 0, -1, 0}, +{ 3, s_3_14, 0, -1, 0}, +{ 3, s_3_15, 0, -1, 0}, +{ 3, s_3_16, 0, -1, 0}, +{ 3, s_3_17, 0, -1, 0}, +{ 2, s_3_18, 0, -1, 0}, +{ 1, s_3_19, 0, -1, 0}, +{ 2, s_3_20, -1, -1, 0}, +{ 3, s_3_21, -1, -1, 0}, +{ 2, s_3_22, -3, -1, 0}, +{ 3, s_3_23, -1, -1, 0}, +{ 6, s_3_24, -2, -1, 0}, +{ 2, s_3_25, -6, -1, 0}, +{ 2, s_3_26, -7, -1, 0}, +{ 4, s_3_27, 0, -1, 0}, +{ 3, s_3_28, 0, -1, 0}, +{ 1, s_3_29, 0, -1, 0}, +{ 2, s_3_30, -1, -1, 0}, +{ 2, s_3_31, -2, -1, 0}, +{ 4, s_3_32, -3, -1, 0}, +{ 6, s_3_33, -1, -1, 0}, +{ 6, s_3_34, -2, -1, 0}, +{ 6, s_3_35, -3, -1, 0}, +{ 2, s_3_36, -7, -1, 0}, +{ 3, s_3_37, -8, -1, 0}, +{ 2, s_3_38, -9, -1, 0}, +{ 3, s_3_39, 0, -1, 0}, +{ 3, s_3_40, 0, -1, 0}, +{ 3, s_3_41, 0, -1, 0}, +{ 2, s_3_42, 0, -1, 0}, +{ 2, s_3_43, 0, -1, 0} }; -static const symbol s_6_0[4] = { 's', 't', 'u', 'l' }; -static const symbol s_6_1[5] = { 'a', 's', 't', 'u', 'l' }; -static const symbol s_6_2[5] = { 0xE1, 's', 't', 'u', 'l' }; -static const symbol s_6_3[4] = { 's', 't', 0xFC, 'l' }; -static const symbol s_6_4[5] = { 'e', 's', 't', 0xFC, 'l' }; -static const symbol s_6_5[5] = { 0xE9, 's', 't', 0xFC, 'l' }; - -static const struct among a_6[6] = -{ -{ 4, s_6_0, -1, 1, 0}, -{ 5, s_6_1, 0, 1, 0}, -{ 5, s_6_2, 0, 2, 0}, -{ 4, s_6_3, -1, 1, 0}, -{ 5, s_6_4, 3, 1, 0}, -{ 5, s_6_5, 3, 3, 0} +static const symbol s_4_0[2] = { 0xE1, 'n' }; +static const symbol s_4_1[2] = { 0xE9, 'n' }; +static const symbol s_4_2[6] = { 0xE1, 'n', 'k', 0xE9, 'n', 't' }; +static const struct among a_4[3] = { +{ 2, s_4_0, 0, 2, 0}, +{ 2, s_4_1, 0, 1, 0}, +{ 6, s_4_2, 0, 2, 0} }; -static const symbol s_7_0[1] = { 0xE1 }; -static const symbol s_7_1[1] = { 0xE9 }; - -static const struct among a_7[2] = -{ -{ 1, s_7_0, -1, 1, 0}, -{ 1, s_7_1, -1, 1, 0} +static const symbol s_5_0[4] = { 's', 't', 'u', 'l' }; +static const symbol s_5_1[5] = { 'a', 's', 't', 'u', 'l' }; +static const symbol s_5_2[5] = { 0xE1, 's', 't', 'u', 'l' }; +static const symbol s_5_3[4] = { 's', 't', 0xFC, 'l' }; +static const symbol s_5_4[5] = { 'e', 's', 't', 0xFC, 'l' }; +static const symbol s_5_5[5] = { 0xE9, 's', 't', 0xFC, 'l' }; +static const struct among a_5[6] = { +{ 4, s_5_0, 0, 1, 0}, +{ 5, s_5_1, -1, 1, 0}, +{ 5, s_5_2, -2, 2, 0}, +{ 4, s_5_3, 0, 1, 0}, +{ 5, s_5_4, -1, 1, 0}, +{ 5, s_5_5, -2, 3, 0} }; -static const symbol s_8_0[1] = { 'k' }; -static const symbol s_8_1[2] = { 'a', 'k' }; -static const symbol s_8_2[2] = { 'e', 'k' }; -static const symbol s_8_3[2] = { 'o', 'k' }; -static const symbol s_8_4[2] = { 0xE1, 'k' }; -static const symbol s_8_5[2] = { 0xE9, 'k' }; -static const symbol s_8_6[2] = { 0xF6, 'k' }; - -static const struct among a_8[7] = -{ -{ 1, s_8_0, -1, 3, 0}, -{ 2, s_8_1, 0, 3, 0}, -{ 2, s_8_2, 0, 3, 0}, -{ 2, s_8_3, 0, 3, 0}, -{ 2, s_8_4, 0, 1, 0}, -{ 2, s_8_5, 0, 2, 0}, -{ 2, s_8_6, 0, 3, 0} +static const symbol s_7_0[1] = { 'k' }; +static const symbol s_7_1[2] = { 'a', 'k' }; +static const symbol s_7_2[2] = { 'e', 'k' }; +static const symbol s_7_3[2] = { 'o', 'k' }; +static const symbol s_7_4[2] = { 0xE1, 'k' }; +static const symbol s_7_5[2] = { 0xE9, 'k' }; +static const symbol s_7_6[2] = { 0xF6, 'k' }; +static const struct among a_7[7] = { +{ 1, s_7_0, 0, 3, 0}, +{ 2, s_7_1, -1, 3, 0}, +{ 2, s_7_2, -2, 3, 0}, +{ 2, s_7_3, -3, 3, 0}, +{ 2, s_7_4, -4, 1, 0}, +{ 2, s_7_5, -5, 2, 0}, +{ 2, s_7_6, -6, 3, 0} }; -static const symbol s_9_0[2] = { 0xE9, 'i' }; -static const symbol s_9_1[3] = { 0xE1, 0xE9, 'i' }; -static const symbol s_9_2[3] = { 0xE9, 0xE9, 'i' }; -static const symbol s_9_3[1] = { 0xE9 }; -static const symbol s_9_4[2] = { 'k', 0xE9 }; -static const symbol s_9_5[3] = { 'a', 'k', 0xE9 }; -static const symbol s_9_6[3] = { 'e', 'k', 0xE9 }; -static const symbol s_9_7[3] = { 'o', 'k', 0xE9 }; -static const symbol s_9_8[3] = { 0xE1, 'k', 0xE9 }; -static const symbol s_9_9[3] = { 0xE9, 'k', 0xE9 }; -static const symbol s_9_10[3] = { 0xF6, 'k', 0xE9 }; -static const symbol s_9_11[2] = { 0xE9, 0xE9 }; - -static const struct among a_9[12] = -{ -{ 2, s_9_0, -1, 1, 0}, -{ 3, s_9_1, 0, 3, 0}, -{ 3, s_9_2, 0, 2, 0}, -{ 1, s_9_3, -1, 1, 0}, -{ 2, s_9_4, 3, 1, 0}, -{ 3, s_9_5, 4, 1, 0}, -{ 3, s_9_6, 4, 1, 0}, -{ 3, s_9_7, 4, 1, 0}, -{ 3, s_9_8, 4, 3, 0}, -{ 3, s_9_9, 4, 2, 0}, -{ 3, s_9_10, 4, 1, 0}, -{ 2, s_9_11, 3, 2, 0} +static const symbol s_8_0[2] = { 0xE9, 'i' }; +static const symbol s_8_1[3] = { 0xE1, 0xE9, 'i' }; +static const symbol s_8_2[3] = { 0xE9, 0xE9, 'i' }; +static const symbol s_8_3[1] = { 0xE9 }; +static const symbol s_8_4[2] = { 'k', 0xE9 }; +static const symbol s_8_5[3] = { 'a', 'k', 0xE9 }; +static const symbol s_8_6[3] = { 'e', 'k', 0xE9 }; +static const symbol s_8_7[3] = { 'o', 'k', 0xE9 }; +static const symbol s_8_8[3] = { 0xE1, 'k', 0xE9 }; +static const symbol s_8_9[3] = { 0xE9, 'k', 0xE9 }; +static const symbol s_8_10[3] = { 0xF6, 'k', 0xE9 }; +static const symbol s_8_11[2] = { 0xE9, 0xE9 }; +static const struct among a_8[12] = { +{ 2, s_8_0, 0, 1, 0}, +{ 3, s_8_1, -1, 3, 0}, +{ 3, s_8_2, -2, 2, 0}, +{ 1, s_8_3, 0, 1, 0}, +{ 2, s_8_4, -1, 1, 0}, +{ 3, s_8_5, -1, 1, 0}, +{ 3, s_8_6, -2, 1, 0}, +{ 3, s_8_7, -3, 1, 0}, +{ 3, s_8_8, -4, 3, 0}, +{ 3, s_8_9, -5, 2, 0}, +{ 3, s_8_10, -6, 1, 0}, +{ 2, s_8_11, -8, 2, 0} }; -static const symbol s_10_0[1] = { 'a' }; -static const symbol s_10_1[2] = { 'j', 'a' }; -static const symbol s_10_2[1] = { 'd' }; -static const symbol s_10_3[2] = { 'a', 'd' }; -static const symbol s_10_4[2] = { 'e', 'd' }; -static const symbol s_10_5[2] = { 'o', 'd' }; -static const symbol s_10_6[2] = { 0xE1, 'd' }; -static const symbol s_10_7[2] = { 0xE9, 'd' }; -static const symbol s_10_8[2] = { 0xF6, 'd' }; -static const symbol s_10_9[1] = { 'e' }; -static const symbol s_10_10[2] = { 'j', 'e' }; -static const symbol s_10_11[2] = { 'n', 'k' }; -static const symbol s_10_12[3] = { 'u', 'n', 'k' }; -static const symbol s_10_13[3] = { 0xE1, 'n', 'k' }; -static const symbol s_10_14[3] = { 0xE9, 'n', 'k' }; -static const symbol s_10_15[3] = { 0xFC, 'n', 'k' }; -static const symbol s_10_16[2] = { 'u', 'k' }; -static const symbol s_10_17[3] = { 'j', 'u', 'k' }; -static const symbol s_10_18[4] = { 0xE1, 'j', 'u', 'k' }; -static const symbol s_10_19[2] = { 0xFC, 'k' }; -static const symbol s_10_20[3] = { 'j', 0xFC, 'k' }; -static const symbol s_10_21[4] = { 0xE9, 'j', 0xFC, 'k' }; -static const symbol s_10_22[1] = { 'm' }; -static const symbol s_10_23[2] = { 'a', 'm' }; -static const symbol s_10_24[2] = { 'e', 'm' }; -static const symbol s_10_25[2] = { 'o', 'm' }; -static const symbol s_10_26[2] = { 0xE1, 'm' }; -static const symbol s_10_27[2] = { 0xE9, 'm' }; -static const symbol s_10_28[1] = { 'o' }; -static const symbol s_10_29[1] = { 0xE1 }; -static const symbol s_10_30[1] = { 0xE9 }; - -static const struct among a_10[31] = -{ -{ 1, s_10_0, -1, 1, 0}, -{ 2, s_10_1, 0, 1, 0}, -{ 1, s_10_2, -1, 1, 0}, -{ 2, s_10_3, 2, 1, 0}, -{ 2, s_10_4, 2, 1, 0}, -{ 2, s_10_5, 2, 1, 0}, -{ 2, s_10_6, 2, 2, 0}, -{ 2, s_10_7, 2, 3, 0}, -{ 2, s_10_8, 2, 1, 0}, -{ 1, s_10_9, -1, 1, 0}, -{ 2, s_10_10, 9, 1, 0}, -{ 2, s_10_11, -1, 1, 0}, -{ 3, s_10_12, 11, 1, 0}, -{ 3, s_10_13, 11, 2, 0}, -{ 3, s_10_14, 11, 3, 0}, -{ 3, s_10_15, 11, 1, 0}, -{ 2, s_10_16, -1, 1, 0}, -{ 3, s_10_17, 16, 1, 0}, -{ 4, s_10_18, 17, 2, 0}, -{ 2, s_10_19, -1, 1, 0}, -{ 3, s_10_20, 19, 1, 0}, -{ 4, s_10_21, 20, 3, 0}, -{ 1, s_10_22, -1, 1, 0}, -{ 2, s_10_23, 22, 1, 0}, -{ 2, s_10_24, 22, 1, 0}, -{ 2, s_10_25, 22, 1, 0}, -{ 2, s_10_26, 22, 2, 0}, -{ 2, s_10_27, 22, 3, 0}, -{ 1, s_10_28, -1, 1, 0}, -{ 1, s_10_29, -1, 2, 0}, -{ 1, s_10_30, -1, 3, 0} +static const symbol s_9_0[1] = { 'a' }; +static const symbol s_9_1[2] = { 'j', 'a' }; +static const symbol s_9_2[1] = { 'd' }; +static const symbol s_9_3[2] = { 'a', 'd' }; +static const symbol s_9_4[2] = { 'e', 'd' }; +static const symbol s_9_5[2] = { 'o', 'd' }; +static const symbol s_9_6[2] = { 0xE1, 'd' }; +static const symbol s_9_7[2] = { 0xE9, 'd' }; +static const symbol s_9_8[2] = { 0xF6, 'd' }; +static const symbol s_9_9[1] = { 'e' }; +static const symbol s_9_10[2] = { 'j', 'e' }; +static const symbol s_9_11[2] = { 'n', 'k' }; +static const symbol s_9_12[3] = { 'u', 'n', 'k' }; +static const symbol s_9_13[3] = { 0xE1, 'n', 'k' }; +static const symbol s_9_14[3] = { 0xE9, 'n', 'k' }; +static const symbol s_9_15[3] = { 0xFC, 'n', 'k' }; +static const symbol s_9_16[2] = { 'u', 'k' }; +static const symbol s_9_17[3] = { 'j', 'u', 'k' }; +static const symbol s_9_18[4] = { 0xE1, 'j', 'u', 'k' }; +static const symbol s_9_19[2] = { 0xFC, 'k' }; +static const symbol s_9_20[3] = { 'j', 0xFC, 'k' }; +static const symbol s_9_21[4] = { 0xE9, 'j', 0xFC, 'k' }; +static const symbol s_9_22[1] = { 'm' }; +static const symbol s_9_23[2] = { 'a', 'm' }; +static const symbol s_9_24[2] = { 'e', 'm' }; +static const symbol s_9_25[2] = { 'o', 'm' }; +static const symbol s_9_26[2] = { 0xE1, 'm' }; +static const symbol s_9_27[2] = { 0xE9, 'm' }; +static const symbol s_9_28[1] = { 'o' }; +static const symbol s_9_29[1] = { 0xE1 }; +static const symbol s_9_30[1] = { 0xE9 }; +static const struct among a_9[31] = { +{ 1, s_9_0, 0, 1, 0}, +{ 2, s_9_1, -1, 1, 0}, +{ 1, s_9_2, 0, 1, 0}, +{ 2, s_9_3, -1, 1, 0}, +{ 2, s_9_4, -2, 1, 0}, +{ 2, s_9_5, -3, 1, 0}, +{ 2, s_9_6, -4, 2, 0}, +{ 2, s_9_7, -5, 3, 0}, +{ 2, s_9_8, -6, 1, 0}, +{ 1, s_9_9, 0, 1, 0}, +{ 2, s_9_10, -1, 1, 0}, +{ 2, s_9_11, 0, 1, 0}, +{ 3, s_9_12, -1, 1, 0}, +{ 3, s_9_13, -2, 2, 0}, +{ 3, s_9_14, -3, 3, 0}, +{ 3, s_9_15, -4, 1, 0}, +{ 2, s_9_16, 0, 1, 0}, +{ 3, s_9_17, -1, 1, 0}, +{ 4, s_9_18, -1, 2, 0}, +{ 2, s_9_19, 0, 1, 0}, +{ 3, s_9_20, -1, 1, 0}, +{ 4, s_9_21, -1, 3, 0}, +{ 1, s_9_22, 0, 1, 0}, +{ 2, s_9_23, -1, 1, 0}, +{ 2, s_9_24, -2, 1, 0}, +{ 2, s_9_25, -3, 1, 0}, +{ 2, s_9_26, -4, 2, 0}, +{ 2, s_9_27, -5, 3, 0}, +{ 1, s_9_28, 0, 1, 0}, +{ 1, s_9_29, 0, 2, 0}, +{ 1, s_9_30, 0, 3, 0} }; -static const symbol s_11_0[2] = { 'i', 'd' }; -static const symbol s_11_1[3] = { 'a', 'i', 'd' }; -static const symbol s_11_2[4] = { 'j', 'a', 'i', 'd' }; -static const symbol s_11_3[3] = { 'e', 'i', 'd' }; -static const symbol s_11_4[4] = { 'j', 'e', 'i', 'd' }; -static const symbol s_11_5[3] = { 0xE1, 'i', 'd' }; -static const symbol s_11_6[3] = { 0xE9, 'i', 'd' }; -static const symbol s_11_7[1] = { 'i' }; -static const symbol s_11_8[2] = { 'a', 'i' }; -static const symbol s_11_9[3] = { 'j', 'a', 'i' }; -static const symbol s_11_10[2] = { 'e', 'i' }; -static const symbol s_11_11[3] = { 'j', 'e', 'i' }; -static const symbol s_11_12[2] = { 0xE1, 'i' }; -static const symbol s_11_13[2] = { 0xE9, 'i' }; -static const symbol s_11_14[4] = { 'i', 't', 'e', 'k' }; -static const symbol s_11_15[5] = { 'e', 'i', 't', 'e', 'k' }; -static const symbol s_11_16[6] = { 'j', 'e', 'i', 't', 'e', 'k' }; -static const symbol s_11_17[5] = { 0xE9, 'i', 't', 'e', 'k' }; -static const symbol s_11_18[2] = { 'i', 'k' }; -static const symbol s_11_19[3] = { 'a', 'i', 'k' }; -static const symbol s_11_20[4] = { 'j', 'a', 'i', 'k' }; -static const symbol s_11_21[3] = { 'e', 'i', 'k' }; -static const symbol s_11_22[4] = { 'j', 'e', 'i', 'k' }; -static const symbol s_11_23[3] = { 0xE1, 'i', 'k' }; -static const symbol s_11_24[3] = { 0xE9, 'i', 'k' }; -static const symbol s_11_25[3] = { 'i', 'n', 'k' }; -static const symbol s_11_26[4] = { 'a', 'i', 'n', 'k' }; -static const symbol s_11_27[5] = { 'j', 'a', 'i', 'n', 'k' }; -static const symbol s_11_28[4] = { 'e', 'i', 'n', 'k' }; -static const symbol s_11_29[5] = { 'j', 'e', 'i', 'n', 'k' }; -static const symbol s_11_30[4] = { 0xE1, 'i', 'n', 'k' }; -static const symbol s_11_31[4] = { 0xE9, 'i', 'n', 'k' }; -static const symbol s_11_32[5] = { 'a', 'i', 't', 'o', 'k' }; -static const symbol s_11_33[6] = { 'j', 'a', 'i', 't', 'o', 'k' }; -static const symbol s_11_34[5] = { 0xE1, 'i', 't', 'o', 'k' }; -static const symbol s_11_35[2] = { 'i', 'm' }; -static const symbol s_11_36[3] = { 'a', 'i', 'm' }; -static const symbol s_11_37[4] = { 'j', 'a', 'i', 'm' }; -static const symbol s_11_38[3] = { 'e', 'i', 'm' }; -static const symbol s_11_39[4] = { 'j', 'e', 'i', 'm' }; -static const symbol s_11_40[3] = { 0xE1, 'i', 'm' }; -static const symbol s_11_41[3] = { 0xE9, 'i', 'm' }; - -static const struct among a_11[42] = -{ -{ 2, s_11_0, -1, 1, 0}, -{ 3, s_11_1, 0, 1, 0}, -{ 4, s_11_2, 1, 1, 0}, -{ 3, s_11_3, 0, 1, 0}, -{ 4, s_11_4, 3, 1, 0}, -{ 3, s_11_5, 0, 2, 0}, -{ 3, s_11_6, 0, 3, 0}, -{ 1, s_11_7, -1, 1, 0}, -{ 2, s_11_8, 7, 1, 0}, -{ 3, s_11_9, 8, 1, 0}, -{ 2, s_11_10, 7, 1, 0}, -{ 3, s_11_11, 10, 1, 0}, -{ 2, s_11_12, 7, 2, 0}, -{ 2, s_11_13, 7, 3, 0}, -{ 4, s_11_14, -1, 1, 0}, -{ 5, s_11_15, 14, 1, 0}, -{ 6, s_11_16, 15, 1, 0}, -{ 5, s_11_17, 14, 3, 0}, -{ 2, s_11_18, -1, 1, 0}, -{ 3, s_11_19, 18, 1, 0}, -{ 4, s_11_20, 19, 1, 0}, -{ 3, s_11_21, 18, 1, 0}, -{ 4, s_11_22, 21, 1, 0}, -{ 3, s_11_23, 18, 2, 0}, -{ 3, s_11_24, 18, 3, 0}, -{ 3, s_11_25, -1, 1, 0}, -{ 4, s_11_26, 25, 1, 0}, -{ 5, s_11_27, 26, 1, 0}, -{ 4, s_11_28, 25, 1, 0}, -{ 5, s_11_29, 28, 1, 0}, -{ 4, s_11_30, 25, 2, 0}, -{ 4, s_11_31, 25, 3, 0}, -{ 5, s_11_32, -1, 1, 0}, -{ 6, s_11_33, 32, 1, 0}, -{ 5, s_11_34, -1, 2, 0}, -{ 2, s_11_35, -1, 1, 0}, -{ 3, s_11_36, 35, 1, 0}, -{ 4, s_11_37, 36, 1, 0}, -{ 3, s_11_38, 35, 1, 0}, -{ 4, s_11_39, 38, 1, 0}, -{ 3, s_11_40, 35, 2, 0}, -{ 3, s_11_41, 35, 3, 0} +static const symbol s_10_0[2] = { 'i', 'd' }; +static const symbol s_10_1[3] = { 'a', 'i', 'd' }; +static const symbol s_10_2[4] = { 'j', 'a', 'i', 'd' }; +static const symbol s_10_3[3] = { 'e', 'i', 'd' }; +static const symbol s_10_4[4] = { 'j', 'e', 'i', 'd' }; +static const symbol s_10_5[3] = { 0xE1, 'i', 'd' }; +static const symbol s_10_6[3] = { 0xE9, 'i', 'd' }; +static const symbol s_10_7[1] = { 'i' }; +static const symbol s_10_8[2] = { 'a', 'i' }; +static const symbol s_10_9[3] = { 'j', 'a', 'i' }; +static const symbol s_10_10[2] = { 'e', 'i' }; +static const symbol s_10_11[3] = { 'j', 'e', 'i' }; +static const symbol s_10_12[2] = { 0xE1, 'i' }; +static const symbol s_10_13[2] = { 0xE9, 'i' }; +static const symbol s_10_14[4] = { 'i', 't', 'e', 'k' }; +static const symbol s_10_15[5] = { 'e', 'i', 't', 'e', 'k' }; +static const symbol s_10_16[6] = { 'j', 'e', 'i', 't', 'e', 'k' }; +static const symbol s_10_17[5] = { 0xE9, 'i', 't', 'e', 'k' }; +static const symbol s_10_18[2] = { 'i', 'k' }; +static const symbol s_10_19[3] = { 'a', 'i', 'k' }; +static const symbol s_10_20[4] = { 'j', 'a', 'i', 'k' }; +static const symbol s_10_21[3] = { 'e', 'i', 'k' }; +static const symbol s_10_22[4] = { 'j', 'e', 'i', 'k' }; +static const symbol s_10_23[3] = { 0xE1, 'i', 'k' }; +static const symbol s_10_24[3] = { 0xE9, 'i', 'k' }; +static const symbol s_10_25[3] = { 'i', 'n', 'k' }; +static const symbol s_10_26[4] = { 'a', 'i', 'n', 'k' }; +static const symbol s_10_27[5] = { 'j', 'a', 'i', 'n', 'k' }; +static const symbol s_10_28[4] = { 'e', 'i', 'n', 'k' }; +static const symbol s_10_29[5] = { 'j', 'e', 'i', 'n', 'k' }; +static const symbol s_10_30[4] = { 0xE1, 'i', 'n', 'k' }; +static const symbol s_10_31[4] = { 0xE9, 'i', 'n', 'k' }; +static const symbol s_10_32[5] = { 'a', 'i', 't', 'o', 'k' }; +static const symbol s_10_33[6] = { 'j', 'a', 'i', 't', 'o', 'k' }; +static const symbol s_10_34[5] = { 0xE1, 'i', 't', 'o', 'k' }; +static const symbol s_10_35[2] = { 'i', 'm' }; +static const symbol s_10_36[3] = { 'a', 'i', 'm' }; +static const symbol s_10_37[4] = { 'j', 'a', 'i', 'm' }; +static const symbol s_10_38[3] = { 'e', 'i', 'm' }; +static const symbol s_10_39[4] = { 'j', 'e', 'i', 'm' }; +static const symbol s_10_40[3] = { 0xE1, 'i', 'm' }; +static const symbol s_10_41[3] = { 0xE9, 'i', 'm' }; +static const struct among a_10[42] = { +{ 2, s_10_0, 0, 1, 0}, +{ 3, s_10_1, -1, 1, 0}, +{ 4, s_10_2, -1, 1, 0}, +{ 3, s_10_3, -3, 1, 0}, +{ 4, s_10_4, -1, 1, 0}, +{ 3, s_10_5, -5, 2, 0}, +{ 3, s_10_6, -6, 3, 0}, +{ 1, s_10_7, 0, 1, 0}, +{ 2, s_10_8, -1, 1, 0}, +{ 3, s_10_9, -1, 1, 0}, +{ 2, s_10_10, -3, 1, 0}, +{ 3, s_10_11, -1, 1, 0}, +{ 2, s_10_12, -5, 2, 0}, +{ 2, s_10_13, -6, 3, 0}, +{ 4, s_10_14, 0, 1, 0}, +{ 5, s_10_15, -1, 1, 0}, +{ 6, s_10_16, -1, 1, 0}, +{ 5, s_10_17, -3, 3, 0}, +{ 2, s_10_18, 0, 1, 0}, +{ 3, s_10_19, -1, 1, 0}, +{ 4, s_10_20, -1, 1, 0}, +{ 3, s_10_21, -3, 1, 0}, +{ 4, s_10_22, -1, 1, 0}, +{ 3, s_10_23, -5, 2, 0}, +{ 3, s_10_24, -6, 3, 0}, +{ 3, s_10_25, 0, 1, 0}, +{ 4, s_10_26, -1, 1, 0}, +{ 5, s_10_27, -1, 1, 0}, +{ 4, s_10_28, -3, 1, 0}, +{ 5, s_10_29, -1, 1, 0}, +{ 4, s_10_30, -5, 2, 0}, +{ 4, s_10_31, -6, 3, 0}, +{ 5, s_10_32, 0, 1, 0}, +{ 6, s_10_33, -1, 1, 0}, +{ 5, s_10_34, 0, 2, 0}, +{ 2, s_10_35, 0, 1, 0}, +{ 3, s_10_36, -1, 1, 0}, +{ 4, s_10_37, -1, 1, 0}, +{ 3, s_10_38, -3, 1, 0}, +{ 4, s_10_39, -1, 1, 0}, +{ 3, s_10_40, -5, 2, 0}, +{ 3, s_10_41, -6, 3, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 17, 52, 14 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'e' }; -static const symbol s_3[] = { 'a' }; -static const symbol s_4[] = { 'a' }; -static const symbol s_5[] = { 'e' }; -static const symbol s_6[] = { 'a' }; -static const symbol s_7[] = { 'e' }; -static const symbol s_8[] = { 'e' }; -static const symbol s_9[] = { 'a' }; -static const symbol s_10[] = { 'a' }; -static const symbol s_11[] = { 'e' }; -static const symbol s_12[] = { 'a' }; -static const symbol s_13[] = { 'e' }; - static int r_mark_regions(struct SN_env * z) { - z->I[0] = z->l; - { int c1 = z->c; - if (in_grouping(z, g_v, 97, 252, 0)) goto lab1; - - if (in_grouping(z, g_v, 97, 252, 1) < 0) goto lab1; - { int c2 = z->c; - if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 3 || !((101187584 >> (z->p[z->c + 1] & 0x1f)) & 1)) goto lab3; - if (!find_among(z, a_0, 8)) goto lab3; - goto lab2; - lab3: - z->c = c2; - if (z->c >= z->l) goto lab1; - z->c++; + ((SN_local *)z)->i_p1 = z->l; + do { + int v_1 = z->c; + if (in_grouping(z, g_v, 97, 252, 0)) goto lab0; + { + int v_2 = z->c; + { + int ret = in_grouping(z, g_v, 97, 252, 1); + if (ret < 0) goto lab1; + z->c += ret; + } + ((SN_local *)z)->i_p1 = z->c; + lab1: + z->c = v_2; } - lab2: - z->I[0] = z->c; - goto lab0; - lab1: - z->c = c1; - if (out_grouping(z, g_v, 97, 252, 0)) return 0; - + break; + lab0: + z->c = v_1; { int ret = out_grouping(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - z->I[0] = z->c; - } -lab0: + ((SN_local *)z)->i_p1 = z->c; + } while (0); return 1; } static int r_R1(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_v_ending(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 225 && z->p[z->c - 1] != 233)) return 0; - among_var = find_among_b(z, a_1, 2); + among_var = find_among_b(z, a_0, 2, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; @@ -539,10 +489,11 @@ static int r_v_ending(struct SN_env * z) { } static int r_double(struct SN_env * z) { - { int m_test1 = z->l - z->c; + { + int v_1 = z->l - z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((106790108 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_2, 23)) return 0; - z->c = z->l - m_test1; + if (!find_among_b(z, a_1, 23, 0)) return 0; + z->c = z->l - v_1; } return 1; } @@ -551,10 +502,11 @@ static int r_undouble(struct SN_env * z) { if (z->c <= z->lb) return 0; z->c--; z->ket = z->c; -z->c = z->c - 1; - if (z->c < z->lb) return 0; + if (z->c <= z->lb) return 0; + z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -563,57 +515,59 @@ z->c = z->c - 1; static int r_instrum(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] != 108) return 0; - if (!find_among_b(z, a_3, 2)) return 0; + if (!find_among_b(z, a_2, 2, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = r_double(z); + { + int ret = r_double(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_undouble(z); - if (ret <= 0) return ret; - } - return 1; + return r_undouble(z); } static int r_case(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_4, 44)) return 0; + if (!find_among_b(z, a_3, 44, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_v_ending(z); - if (ret <= 0) return ret; - } - return 1; + return r_v_ending(z); } static int r_case_special(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 110 && z->p[z->c - 1] != 116)) return 0; - among_var = find_among_b(z, a_5, 3); + among_var = find_among_b(z, a_4, 3, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; @@ -625,25 +579,29 @@ static int r_case_other(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 3 <= z->lb || z->p[z->c - 1] != 108) return 0; - among_var = find_among_b(z, a_6, 6); + among_var = find_among_b(z, a_5, 6, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; @@ -654,46 +612,50 @@ static int r_case_other(struct SN_env * z) { static int r_factive(struct SN_env * z) { z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 225 && z->p[z->c - 1] != 233)) return 0; - if (!find_among_b(z, a_7, 2)) return 0; + z->c--; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = r_double(z); + { + int ret = r_double(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_undouble(z); - if (ret <= 0) return ret; - } - return 1; + return r_undouble(z); } static int r_plural(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 107) return 0; - among_var = find_among_b(z, a_8, 7); + among_var = find_among_b(z, a_7, 7, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -705,25 +667,29 @@ static int r_owned(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 233)) return 0; - among_var = find_among_b(z, a_9, 12); + among_var = find_among_b(z, a_8, 12, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; @@ -734,25 +700,29 @@ static int r_owned(struct SN_env * z) { static int r_sing_owner(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_10, 31); + among_var = find_among_b(z, a_9, 31, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; @@ -764,25 +734,29 @@ static int r_plur_owner(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((10768 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_11, 42); + among_var = find_among_b(z, a_10, 42, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_13); + { + int ret = slice_from_s(z, 1, s_13); if (ret < 0) return ret; } break; @@ -791,73 +765,100 @@ static int r_plur_owner(struct SN_env * z) { } extern int hungarian_ISO_8859_2_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_instrum(z); + { + int v_2 = z->l - z->c; + { + int ret = r_instrum(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_case(z); + { + int v_3 = z->l - z->c; + { + int ret = r_case(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_case_special(z); + { + int v_4 = z->l - z->c; + { + int ret = r_case_special(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_case_other(z); + { + int v_5 = z->l - z->c; + { + int ret = r_case_other(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_factive(z); + { + int v_6 = z->l - z->c; + { + int ret = r_factive(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_owned(z); + { + int v_7 = z->l - z->c; + { + int ret = r_owned(z); if (ret < 0) return ret; } - z->c = z->l - m7; + z->c = z->l - v_7; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_sing_owner(z); + { + int v_8 = z->l - z->c; + { + int ret = r_sing_owner(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_plur_owner(z); + { + int v_9 = z->l - z->c; + { + int ret = r_plur_owner(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_plural(z); + { + int v_10 = z->l - z->c; + { + int ret = r_plural(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } z->c = z->lb; return 1; } -extern struct SN_env * hungarian_ISO_8859_2_create_env(void) { return SN_create_env(0, 1); } +extern struct SN_env * hungarian_ISO_8859_2_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void hungarian_ISO_8859_2_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void hungarian_ISO_8859_2_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_ISO_8859_2_polish.c b/src/backend/snowball/libstemmer/stem_ISO_8859_2_polish.c new file mode 100644 index 0000000000000..893d0ab3ffa6c --- /dev/null +++ b/src/backend/snowball/libstemmer/stem_ISO_8859_2_polish.c @@ -0,0 +1,520 @@ +/* Generated from polish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#include "stem_ISO_8859_2_polish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; + +#ifdef __cplusplus +extern "C" { +#endif +extern int polish_ISO_8859_2_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif + +static int r_R1(struct SN_env * z); +static int r_normalize_consonant(struct SN_env * z); +static int r_remove_endings(struct SN_env * z); +static int r_mark_regions(struct SN_env * z); + +static const symbol s_0[] = { 's' }; +static const symbol s_1[] = { 's' }; +static const symbol s_2[] = { 0xB3 }; +static const symbol s_3[] = { 's' }; +static const symbol s_4[] = { 'c' }; +static const symbol s_5[] = { 'n' }; +static const symbol s_6[] = { 's' }; +static const symbol s_7[] = { 'z' }; + +static const symbol s_0_0[6] = { 'b', 'y', 0xB6, 'c', 'i', 'e' }; +static const symbol s_0_1[3] = { 'b', 'y', 'm' }; +static const symbol s_0_2[2] = { 'b', 'y' }; +static const symbol s_0_3[5] = { 'b', 'y', 0xB6, 'm', 'y' }; +static const symbol s_0_4[3] = { 'b', 'y', 0xB6 }; +static const struct among a_0[5] = { +{ 6, s_0_0, 0, 1, 0}, +{ 3, s_0_1, 0, 1, 0}, +{ 2, s_0_2, 0, 1, 0}, +{ 5, s_0_3, 0, 1, 0}, +{ 3, s_0_4, 0, 1, 0} +}; + +static const symbol s_1_0[2] = { 0xB1, 'c' }; +static const symbol s_1_1[4] = { 'a', 'j', 0xB1, 'c' }; +static const symbol s_1_2[4] = { 's', 'z', 0xB1, 'c' }; +static const symbol s_1_3[2] = { 's', 'z' }; +static const symbol s_1_4[5] = { 'i', 'e', 'j', 's', 'z' }; +static const struct among a_1[5] = { +{ 2, s_1_0, 0, 1, 0}, +{ 4, s_1_1, -1, 1, 0}, +{ 4, s_1_2, -2, 2, 0}, +{ 2, s_1_3, 0, 1, 0}, +{ 5, s_1_4, -1, 1, 0} +}; + +static const symbol s_2_0[1] = { 'a' }; +static const symbol s_2_1[3] = { 0xB1, 'c', 'a' }; +static const symbol s_2_2[5] = { 'a', 'j', 0xB1, 'c', 'a' }; +static const symbol s_2_3[5] = { 's', 'z', 0xB1, 'c', 'a' }; +static const symbol s_2_4[2] = { 'i', 'a' }; +static const symbol s_2_5[3] = { 's', 'z', 'a' }; +static const symbol s_2_6[6] = { 'i', 'e', 'j', 's', 'z', 'a' }; +static const symbol s_2_7[3] = { 'a', 0xB3, 'a' }; +static const symbol s_2_8[4] = { 'i', 'a', 0xB3, 'a' }; +static const symbol s_2_9[3] = { 'i', 0xB3, 'a' }; +static const symbol s_2_10[2] = { 0xB1, 'c' }; +static const symbol s_2_11[4] = { 'a', 'j', 0xB1, 'c' }; +static const symbol s_2_12[1] = { 'e' }; +static const symbol s_2_13[3] = { 0xB1, 'c', 'e' }; +static const symbol s_2_14[5] = { 'a', 'j', 0xB1, 'c', 'e' }; +static const symbol s_2_15[5] = { 's', 'z', 0xB1, 'c', 'e' }; +static const symbol s_2_16[2] = { 'i', 'e' }; +static const symbol s_2_17[3] = { 'c', 'i', 'e' }; +static const symbol s_2_18[4] = { 'a', 'c', 'i', 'e' }; +static const symbol s_2_19[4] = { 'e', 'c', 'i', 'e' }; +static const symbol s_2_20[4] = { 'i', 'c', 'i', 'e' }; +static const symbol s_2_21[5] = { 'a', 'j', 'c', 'i', 'e' }; +static const symbol s_2_22[6] = { 'l', 'i', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_23[7] = { 'a', 'l', 'i', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_24[8] = { 'i', 'e', 'l', 'i', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_25[7] = { 'i', 'l', 'i', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_26[6] = { 0xB3, 'y', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_27[7] = { 'a', 0xB3, 'y', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_28[8] = { 'i', 'a', 0xB3, 'y', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_29[7] = { 'i', 0xB3, 'y', 0xB6, 'c', 'i', 'e' }; +static const symbol s_2_30[3] = { 's', 'z', 'e' }; +static const symbol s_2_31[6] = { 'i', 'e', 'j', 's', 'z', 'e' }; +static const symbol s_2_32[3] = { 'a', 'c', 'h' }; +static const symbol s_2_33[4] = { 'i', 'a', 'c', 'h' }; +static const symbol s_2_34[3] = { 'i', 'c', 'h' }; +static const symbol s_2_35[3] = { 'y', 'c', 'h' }; +static const symbol s_2_36[1] = { 'i' }; +static const symbol s_2_37[3] = { 'a', 'l', 'i' }; +static const symbol s_2_38[4] = { 'i', 'e', 'l', 'i' }; +static const symbol s_2_39[3] = { 'i', 'l', 'i' }; +static const symbol s_2_40[3] = { 'a', 'm', 'i' }; +static const symbol s_2_41[4] = { 'i', 'a', 'm', 'i' }; +static const symbol s_2_42[3] = { 'i', 'm', 'i' }; +static const symbol s_2_43[3] = { 'y', 'm', 'i' }; +static const symbol s_2_44[3] = { 'o', 'w', 'i' }; +static const symbol s_2_45[4] = { 'i', 'o', 'w', 'i' }; +static const symbol s_2_46[2] = { 'a', 'j' }; +static const symbol s_2_47[2] = { 'e', 'j' }; +static const symbol s_2_48[3] = { 'i', 'e', 'j' }; +static const symbol s_2_49[2] = { 'a', 'm' }; +static const symbol s_2_50[4] = { 'a', 0xB3, 'a', 'm' }; +static const symbol s_2_51[5] = { 'i', 'a', 0xB3, 'a', 'm' }; +static const symbol s_2_52[4] = { 'i', 0xB3, 'a', 'm' }; +static const symbol s_2_53[2] = { 'e', 'm' }; +static const symbol s_2_54[3] = { 'i', 'e', 'm' }; +static const symbol s_2_55[4] = { 'a', 0xB3, 'e', 'm' }; +static const symbol s_2_56[5] = { 'i', 'a', 0xB3, 'e', 'm' }; +static const symbol s_2_57[4] = { 'i', 0xB3, 'e', 'm' }; +static const symbol s_2_58[2] = { 'i', 'm' }; +static const symbol s_2_59[2] = { 'o', 'm' }; +static const symbol s_2_60[3] = { 'i', 'o', 'm' }; +static const symbol s_2_61[2] = { 'y', 'm' }; +static const symbol s_2_62[1] = { 'o' }; +static const symbol s_2_63[3] = { 'e', 'g', 'o' }; +static const symbol s_2_64[4] = { 'i', 'e', 'g', 'o' }; +static const symbol s_2_65[3] = { 'a', 0xB3, 'o' }; +static const symbol s_2_66[4] = { 'i', 'a', 0xB3, 'o' }; +static const symbol s_2_67[3] = { 'i', 0xB3, 'o' }; +static const symbol s_2_68[1] = { 'u' }; +static const symbol s_2_69[2] = { 'i', 'u' }; +static const symbol s_2_70[3] = { 'e', 'm', 'u' }; +static const symbol s_2_71[4] = { 'i', 'e', 'm', 'u' }; +static const symbol s_2_72[2] = { 0xF3, 'w' }; +static const symbol s_2_73[1] = { 'y' }; +static const symbol s_2_74[3] = { 'a', 'm', 'y' }; +static const symbol s_2_75[3] = { 'e', 'm', 'y' }; +static const symbol s_2_76[3] = { 'i', 'm', 'y' }; +static const symbol s_2_77[5] = { 'l', 'i', 0xB6, 'm', 'y' }; +static const symbol s_2_78[6] = { 'a', 'l', 'i', 0xB6, 'm', 'y' }; +static const symbol s_2_79[7] = { 'i', 'e', 'l', 'i', 0xB6, 'm', 'y' }; +static const symbol s_2_80[6] = { 'i', 'l', 'i', 0xB6, 'm', 'y' }; +static const symbol s_2_81[5] = { 0xB3, 'y', 0xB6, 'm', 'y' }; +static const symbol s_2_82[6] = { 'a', 0xB3, 'y', 0xB6, 'm', 'y' }; +static const symbol s_2_83[7] = { 'i', 'a', 0xB3, 'y', 0xB6, 'm', 'y' }; +static const symbol s_2_84[6] = { 'i', 0xB3, 'y', 0xB6, 'm', 'y' }; +static const symbol s_2_85[3] = { 'a', 0xB3, 'y' }; +static const symbol s_2_86[4] = { 'i', 'a', 0xB3, 'y' }; +static const symbol s_2_87[3] = { 'i', 0xB3, 'y' }; +static const symbol s_2_88[3] = { 'a', 's', 'z' }; +static const symbol s_2_89[3] = { 'e', 's', 'z' }; +static const symbol s_2_90[3] = { 'i', 's', 'z' }; +static const symbol s_2_91[1] = { 0xB1 }; +static const symbol s_2_92[3] = { 0xB1, 'c', 0xB1 }; +static const symbol s_2_93[5] = { 'a', 'j', 0xB1, 'c', 0xB1 }; +static const symbol s_2_94[5] = { 's', 'z', 0xB1, 'c', 0xB1 }; +static const symbol s_2_95[2] = { 'i', 0xB1 }; +static const symbol s_2_96[3] = { 'a', 'j', 0xB1 }; +static const symbol s_2_97[3] = { 's', 'z', 0xB1 }; +static const symbol s_2_98[6] = { 'i', 'e', 'j', 's', 'z', 0xB1 }; +static const symbol s_2_99[2] = { 'a', 0xB3 }; +static const symbol s_2_100[3] = { 'i', 'a', 0xB3 }; +static const symbol s_2_101[2] = { 'i', 0xB3 }; +static const symbol s_2_102[3] = { 0xB3, 'a', 0xB6 }; +static const symbol s_2_103[4] = { 'a', 0xB3, 'a', 0xB6 }; +static const symbol s_2_104[5] = { 'i', 'a', 0xB3, 'a', 0xB6 }; +static const symbol s_2_105[4] = { 'i', 0xB3, 'a', 0xB6 }; +static const symbol s_2_106[3] = { 0xB3, 'e', 0xB6 }; +static const symbol s_2_107[4] = { 'a', 0xB3, 'e', 0xB6 }; +static const symbol s_2_108[5] = { 'i', 'a', 0xB3, 'e', 0xB6 }; +static const symbol s_2_109[4] = { 'i', 0xB3, 'e', 0xB6 }; +static const symbol s_2_110[2] = { 'a', 0xE6 }; +static const symbol s_2_111[3] = { 'i', 'e', 0xE6 }; +static const symbol s_2_112[2] = { 'i', 0xE6 }; +static const symbol s_2_113[2] = { 0xB1, 0xE6 }; +static const symbol s_2_114[3] = { 'a', 0xB6, 0xE6 }; +static const symbol s_2_115[3] = { 'e', 0xB6, 0xE6 }; +static const symbol s_2_116[1] = { 0xEA }; +static const symbol s_2_117[3] = { 's', 'z', 0xEA }; +static const struct among a_2[118] = { +{ 1, s_2_0, 0, 1, 1}, +{ 3, s_2_1, -1, 1, 0}, +{ 5, s_2_2, -1, 1, 0}, +{ 5, s_2_3, -2, 2, 0}, +{ 2, s_2_4, -4, 1, 1}, +{ 3, s_2_5, -5, 1, 0}, +{ 6, s_2_6, -1, 1, 0}, +{ 3, s_2_7, -7, 1, 0}, +{ 4, s_2_8, -1, 1, 0}, +{ 3, s_2_9, -9, 1, 0}, +{ 2, s_2_10, 0, 1, 0}, +{ 4, s_2_11, -1, 1, 0}, +{ 1, s_2_12, 0, 1, 1}, +{ 3, s_2_13, -1, 1, 0}, +{ 5, s_2_14, -1, 1, 0}, +{ 5, s_2_15, -2, 2, 0}, +{ 2, s_2_16, -4, 1, 1}, +{ 3, s_2_17, -1, 1, 0}, +{ 4, s_2_18, -1, 1, 0}, +{ 4, s_2_19, -2, 1, 0}, +{ 4, s_2_20, -3, 1, 0}, +{ 5, s_2_21, -4, 1, 0}, +{ 6, s_2_22, -5, 4, 0}, +{ 7, s_2_23, -1, 1, 0}, +{ 8, s_2_24, -2, 1, 0}, +{ 7, s_2_25, -3, 1, 0}, +{ 6, s_2_26, -9, 4, 0}, +{ 7, s_2_27, -1, 1, 0}, +{ 8, s_2_28, -1, 1, 0}, +{ 7, s_2_29, -3, 1, 0}, +{ 3, s_2_30, -18, 1, 0}, +{ 6, s_2_31, -1, 1, 0}, +{ 3, s_2_32, 0, 1, 1}, +{ 4, s_2_33, -1, 1, 1}, +{ 3, s_2_34, 0, 5, 0}, +{ 3, s_2_35, 0, 5, 0}, +{ 1, s_2_36, 0, 1, 1}, +{ 3, s_2_37, -1, 1, 0}, +{ 4, s_2_38, -2, 1, 0}, +{ 3, s_2_39, -3, 1, 0}, +{ 3, s_2_40, -4, 1, 1}, +{ 4, s_2_41, -1, 1, 1}, +{ 3, s_2_42, -6, 5, 0}, +{ 3, s_2_43, -7, 5, 0}, +{ 3, s_2_44, -8, 1, 1}, +{ 4, s_2_45, -1, 1, 1}, +{ 2, s_2_46, 0, 1, 0}, +{ 2, s_2_47, 0, 5, 0}, +{ 3, s_2_48, -1, 5, 0}, +{ 2, s_2_49, 0, 1, 0}, +{ 4, s_2_50, -1, 1, 0}, +{ 5, s_2_51, -1, 1, 0}, +{ 4, s_2_52, -3, 1, 0}, +{ 2, s_2_53, 0, 1, 1}, +{ 3, s_2_54, -1, 1, 1}, +{ 4, s_2_55, -2, 1, 0}, +{ 5, s_2_56, -1, 1, 0}, +{ 4, s_2_57, -4, 1, 0}, +{ 2, s_2_58, 0, 5, 0}, +{ 2, s_2_59, 0, 1, 1}, +{ 3, s_2_60, -1, 1, 1}, +{ 2, s_2_61, 0, 5, 0}, +{ 1, s_2_62, 0, 1, 1}, +{ 3, s_2_63, -1, 5, 0}, +{ 4, s_2_64, -1, 5, 0}, +{ 3, s_2_65, -3, 1, 0}, +{ 4, s_2_66, -1, 1, 0}, +{ 3, s_2_67, -5, 1, 0}, +{ 1, s_2_68, 0, 1, 1}, +{ 2, s_2_69, -1, 1, 1}, +{ 3, s_2_70, -2, 5, 0}, +{ 4, s_2_71, -1, 5, 0}, +{ 2, s_2_72, 0, 1, 1}, +{ 1, s_2_73, 0, 5, 0}, +{ 3, s_2_74, -1, 1, 0}, +{ 3, s_2_75, -2, 1, 0}, +{ 3, s_2_76, -3, 1, 0}, +{ 5, s_2_77, -4, 4, 0}, +{ 6, s_2_78, -1, 1, 0}, +{ 7, s_2_79, -2, 1, 0}, +{ 6, s_2_80, -3, 1, 0}, +{ 5, s_2_81, -8, 4, 0}, +{ 6, s_2_82, -1, 1, 0}, +{ 7, s_2_83, -1, 1, 0}, +{ 6, s_2_84, -3, 1, 0}, +{ 3, s_2_85, -12, 1, 0}, +{ 4, s_2_86, -1, 1, 0}, +{ 3, s_2_87, -14, 1, 0}, +{ 3, s_2_88, 0, 1, 0}, +{ 3, s_2_89, 0, 1, 0}, +{ 3, s_2_90, 0, 1, 0}, +{ 1, s_2_91, 0, 1, 1}, +{ 3, s_2_92, -1, 1, 0}, +{ 5, s_2_93, -1, 1, 0}, +{ 5, s_2_94, -2, 2, 0}, +{ 2, s_2_95, -4, 1, 1}, +{ 3, s_2_96, -5, 1, 0}, +{ 3, s_2_97, -6, 3, 0}, +{ 6, s_2_98, -1, 1, 0}, +{ 2, s_2_99, 0, 1, 0}, +{ 3, s_2_100, -1, 1, 0}, +{ 2, s_2_101, 0, 1, 0}, +{ 3, s_2_102, 0, 4, 0}, +{ 4, s_2_103, -1, 1, 0}, +{ 5, s_2_104, -1, 1, 0}, +{ 4, s_2_105, -3, 1, 0}, +{ 3, s_2_106, 0, 4, 0}, +{ 4, s_2_107, -1, 1, 0}, +{ 5, s_2_108, -1, 1, 0}, +{ 4, s_2_109, -3, 1, 0}, +{ 2, s_2_110, 0, 1, 0}, +{ 3, s_2_111, 0, 1, 0}, +{ 2, s_2_112, 0, 1, 0}, +{ 2, s_2_113, 0, 1, 0}, +{ 3, s_2_114, 0, 1, 0}, +{ 3, s_2_115, 0, 1, 0}, +{ 1, s_2_116, 0, 1, 0}, +{ 3, s_2_117, -1, 2, 0} +}; + +static const symbol s_3_0[1] = { 0xB6 }; +static const symbol s_3_1[1] = { 0xBC }; +static const symbol s_3_2[1] = { 0xE6 }; +static const symbol s_3_3[1] = { 0xF1 }; +static const struct among a_3[4] = { +{ 1, s_3_0, 0, 3, 0}, +{ 1, s_3_1, 0, 4, 0}, +{ 1, s_3_2, 0, 1, 0}, +{ 1, s_3_3, 0, 2, 0} +}; + +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 4 }; + +static int r_mark_regions(struct SN_env * z) { + ((SN_local *)z)->i_p1 = z->l; + { + int ret = out_grouping(z, g_v, 97, 243, 1); + if (ret < 0) return 0; + z->c += ret; + } + { + int ret = in_grouping(z, g_v, 97, 243, 1); + if (ret < 0) return 0; + z->c += ret; + } + ((SN_local *)z)->i_p1 = z->c; + return 1; +} + +static int r_R1(struct SN_env * z) { + return ((SN_local *)z)->i_p1 <= z->c; +} + +static int r_remove_endings(struct SN_env * z) { + int among_var; + { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) goto lab0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; + z->ket = z->c; + if (!find_among_b(z, a_0, 5, 0)) { z->lb = v_2; goto lab0; } + z->bra = z->c; + z->lb = v_2; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + lab0: + z->c = z->l - v_1; + } + z->ket = z->c; + among_var = find_among_b(z, a_2, 118, r_R1); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_0); + if (ret < 0) return ret; + } + break; + case 3: + do { + int v_3 = z->l - z->c; + { + int v_4 = z->l - z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + z->c = z->l - v_4; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } + break; + lab1: + z->c = z->l - v_3; + { + int ret = slice_from_s(z, 1, s_1); + if (ret < 0) return ret; + } + } while (0); + break; + case 4: + { + int ret = slice_from_s(z, 1, s_2); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int v_5 = z->l - z->c; + z->ket = z->c; + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 99 && z->p[z->c - 1] != 122)) { z->c = z->l - v_5; goto lab2; } + among_var = find_among_b(z, a_1, 5, 0); + if (!among_var) { z->c = z->l - v_5; goto lab2; } + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_3); + if (ret < 0) return ret; + } + break; + } + lab2: + ; + } + break; + } + return 1; +} + +static int r_normalize_consonant(struct SN_env * z) { + int among_var; + z->ket = z->c; + among_var = find_among_b(z, a_3, 4, 0); + if (!among_var) return 0; + z->bra = z->c; + if (z->c > z->lb) goto lab0; + return 0; +lab0: + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_4); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_5); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_6); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_7); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +extern int polish_ISO_8859_2_stem(struct SN_env * z) { + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); + if (ret < 0) return ret; + } + z->c = v_1; + } + do { + int v_2 = z->c; + if (z->c + 2 > z->l) goto lab0; + z->c += 2; + z->lb = z->c; z->c = z->l; + { + int ret = r_remove_endings(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + z->c = z->lb; + break; + lab0: + z->c = v_2; + z->lb = z->c; z->c = z->l; + { + int ret = r_normalize_consonant(z); + if (ret <= 0) return ret; + } + z->c = z->lb; + } while (0); + return 1; +} + +extern struct SN_env * polish_ISO_8859_2_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} + +extern void polish_ISO_8859_2_close_env(struct SN_env * z) { + SN_delete_env(z); +} + diff --git a/src/backend/snowball/libstemmer/stem_KOI8_R_russian.c b/src/backend/snowball/libstemmer/stem_KOI8_R_russian.c index f9f0973b3935e..d44e3db4429fa 100644 --- a/src/backend/snowball/libstemmer/stem_KOI8_R_russian.c +++ b/src/backend/snowball/libstemmer/stem_KOI8_R_russian.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from russian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_KOI8_R_russian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int russian_KOI8_R_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_tidy_up(struct SN_env * z); static int r_derivational(struct SN_env * z); static int r_noun(struct SN_env * z); @@ -19,18 +32,9 @@ static int r_adjective(struct SN_env * z); static int r_perfective_gerund(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * russian_KOI8_R_create_env(void); -extern void russian_KOI8_R_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xC5 }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 0xD7, 0xDB, 0xC9 }; static const symbol s_0_1[4] = { 0xC9, 0xD7, 0xDB, 0xC9 }; static const symbol s_0_2[4] = { 0xD9, 0xD7, 0xDB, 0xC9 }; @@ -40,18 +44,16 @@ static const symbol s_0_5[2] = { 0xD9, 0xD7 }; static const symbol s_0_6[5] = { 0xD7, 0xDB, 0xC9, 0xD3, 0xD8 }; static const symbol s_0_7[6] = { 0xC9, 0xD7, 0xDB, 0xC9, 0xD3, 0xD8 }; static const symbol s_0_8[6] = { 0xD9, 0xD7, 0xDB, 0xC9, 0xD3, 0xD8 }; - -static const struct among a_0[9] = -{ -{ 3, s_0_0, -1, 1, 0}, -{ 4, s_0_1, 0, 2, 0}, -{ 4, s_0_2, 0, 2, 0}, -{ 1, s_0_3, -1, 1, 0}, -{ 2, s_0_4, 3, 2, 0}, -{ 2, s_0_5, 3, 2, 0}, -{ 5, s_0_6, -1, 1, 0}, -{ 6, s_0_7, 6, 2, 0}, -{ 6, s_0_8, 6, 2, 0} +static const struct among a_0[9] = { +{ 3, s_0_0, 0, 1, 0}, +{ 4, s_0_1, -1, 2, 0}, +{ 4, s_0_2, -2, 2, 0}, +{ 1, s_0_3, 0, 1, 0}, +{ 2, s_0_4, -1, 2, 0}, +{ 2, s_0_5, -2, 2, 0}, +{ 5, s_0_6, 0, 1, 0}, +{ 6, s_0_7, -1, 2, 0}, +{ 6, s_0_8, -2, 2, 0} }; static const symbol s_1_0[2] = { 0xC0, 0xC0 }; @@ -80,35 +82,33 @@ static const symbol s_1_22[2] = { 0xC1, 0xD1 }; static const symbol s_1_23[2] = { 0xD1, 0xD1 }; static const symbol s_1_24[3] = { 0xC5, 0xCD, 0xD5 }; static const symbol s_1_25[3] = { 0xCF, 0xCD, 0xD5 }; - -static const struct among a_1[26] = -{ -{ 2, s_1_0, -1, 1, 0}, -{ 2, s_1_1, -1, 1, 0}, -{ 2, s_1_2, -1, 1, 0}, -{ 2, s_1_3, -1, 1, 0}, -{ 2, s_1_4, -1, 1, 0}, -{ 2, s_1_5, -1, 1, 0}, -{ 2, s_1_6, -1, 1, 0}, -{ 2, s_1_7, -1, 1, 0}, -{ 2, s_1_8, -1, 1, 0}, -{ 2, s_1_9, -1, 1, 0}, -{ 3, s_1_10, -1, 1, 0}, -{ 3, s_1_11, -1, 1, 0}, -{ 2, s_1_12, -1, 1, 0}, -{ 2, s_1_13, -1, 1, 0}, -{ 2, s_1_14, -1, 1, 0}, -{ 2, s_1_15, -1, 1, 0}, -{ 2, s_1_16, -1, 1, 0}, -{ 2, s_1_17, -1, 1, 0}, -{ 2, s_1_18, -1, 1, 0}, -{ 2, s_1_19, -1, 1, 0}, -{ 3, s_1_20, -1, 1, 0}, -{ 3, s_1_21, -1, 1, 0}, -{ 2, s_1_22, -1, 1, 0}, -{ 2, s_1_23, -1, 1, 0}, -{ 3, s_1_24, -1, 1, 0}, -{ 3, s_1_25, -1, 1, 0} +static const struct among a_1[26] = { +{ 2, s_1_0, 0, 1, 0}, +{ 2, s_1_1, 0, 1, 0}, +{ 2, s_1_2, 0, 1, 0}, +{ 2, s_1_3, 0, 1, 0}, +{ 2, s_1_4, 0, 1, 0}, +{ 2, s_1_5, 0, 1, 0}, +{ 2, s_1_6, 0, 1, 0}, +{ 2, s_1_7, 0, 1, 0}, +{ 2, s_1_8, 0, 1, 0}, +{ 2, s_1_9, 0, 1, 0}, +{ 3, s_1_10, 0, 1, 0}, +{ 3, s_1_11, 0, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 2, s_1_13, 0, 1, 0}, +{ 2, s_1_14, 0, 1, 0}, +{ 2, s_1_15, 0, 1, 0}, +{ 2, s_1_16, 0, 1, 0}, +{ 2, s_1_17, 0, 1, 0}, +{ 2, s_1_18, 0, 1, 0}, +{ 2, s_1_19, 0, 1, 0}, +{ 3, s_1_20, 0, 1, 0}, +{ 3, s_1_21, 0, 1, 0}, +{ 2, s_1_22, 0, 1, 0}, +{ 2, s_1_23, 0, 1, 0}, +{ 3, s_1_24, 0, 1, 0}, +{ 3, s_1_25, 0, 1, 0} }; static const symbol s_2_0[2] = { 0xC5, 0xCD }; @@ -119,26 +119,22 @@ static const symbol s_2_4[3] = { 0xD9, 0xD7, 0xDB }; static const symbol s_2_5[1] = { 0xDD }; static const symbol s_2_6[2] = { 0xC0, 0xDD }; static const symbol s_2_7[3] = { 0xD5, 0xC0, 0xDD }; - -static const struct among a_2[8] = -{ -{ 2, s_2_0, -1, 1, 0}, -{ 2, s_2_1, -1, 1, 0}, -{ 2, s_2_2, -1, 1, 0}, -{ 3, s_2_3, 2, 2, 0}, -{ 3, s_2_4, 2, 2, 0}, -{ 1, s_2_5, -1, 1, 0}, -{ 2, s_2_6, 5, 1, 0}, -{ 3, s_2_7, 6, 2, 0} +static const struct among a_2[8] = { +{ 2, s_2_0, 0, 1, 0}, +{ 2, s_2_1, 0, 1, 0}, +{ 2, s_2_2, 0, 1, 0}, +{ 3, s_2_3, -1, 2, 0}, +{ 3, s_2_4, -2, 2, 0}, +{ 1, s_2_5, 0, 1, 0}, +{ 2, s_2_6, -1, 1, 0}, +{ 3, s_2_7, -1, 2, 0} }; static const symbol s_3_0[2] = { 0xD3, 0xD1 }; static const symbol s_3_1[2] = { 0xD3, 0xD8 }; - -static const struct among a_3[2] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 1, 0} +static const struct among a_3[2] = { +{ 2, s_3_0, 0, 1, 0}, +{ 2, s_3_1, 0, 1, 0} }; static const symbol s_4_0[1] = { 0xC0 }; @@ -187,55 +183,53 @@ static const symbol s_4_42[3] = { 0xC5, 0xDB, 0xD8 }; static const symbol s_4_43[3] = { 0xC9, 0xDB, 0xD8 }; static const symbol s_4_44[2] = { 0xCE, 0xD9 }; static const symbol s_4_45[3] = { 0xC5, 0xCE, 0xD9 }; - -static const struct among a_4[46] = -{ -{ 1, s_4_0, -1, 2, 0}, -{ 2, s_4_1, 0, 2, 0}, -{ 2, s_4_2, -1, 1, 0}, -{ 3, s_4_3, 2, 2, 0}, -{ 3, s_4_4, 2, 2, 0}, -{ 2, s_4_5, -1, 1, 0}, -{ 3, s_4_6, 5, 2, 0}, -{ 3, s_4_7, -1, 1, 0}, -{ 3, s_4_8, -1, 2, 0}, -{ 3, s_4_9, -1, 1, 0}, -{ 4, s_4_10, 9, 2, 0}, -{ 4, s_4_11, 9, 2, 0}, -{ 2, s_4_12, -1, 1, 0}, -{ 3, s_4_13, 12, 2, 0}, -{ 3, s_4_14, 12, 2, 0}, -{ 1, s_4_15, -1, 1, 0}, -{ 2, s_4_16, 15, 2, 0}, -{ 2, s_4_17, 15, 2, 0}, -{ 1, s_4_18, -1, 1, 0}, -{ 2, s_4_19, 18, 2, 0}, -{ 2, s_4_20, 18, 2, 0}, -{ 2, s_4_21, -1, 1, 0}, -{ 2, s_4_22, -1, 2, 0}, -{ 2, s_4_23, -1, 2, 0}, -{ 1, s_4_24, -1, 1, 0}, -{ 2, s_4_25, 24, 2, 0}, -{ 2, s_4_26, -1, 1, 0}, -{ 3, s_4_27, 26, 2, 0}, -{ 3, s_4_28, 26, 2, 0}, -{ 2, s_4_29, -1, 1, 0}, -{ 3, s_4_30, 29, 2, 0}, -{ 3, s_4_31, 29, 1, 0}, -{ 2, s_4_32, -1, 1, 0}, -{ 3, s_4_33, 32, 2, 0}, -{ 2, s_4_34, -1, 1, 0}, -{ 3, s_4_35, 34, 2, 0}, -{ 2, s_4_36, -1, 2, 0}, -{ 2, s_4_37, -1, 2, 0}, -{ 2, s_4_38, -1, 2, 0}, -{ 2, s_4_39, -1, 1, 0}, -{ 3, s_4_40, 39, 2, 0}, -{ 3, s_4_41, 39, 2, 0}, -{ 3, s_4_42, -1, 1, 0}, -{ 3, s_4_43, -1, 2, 0}, -{ 2, s_4_44, -1, 1, 0}, -{ 3, s_4_45, 44, 2, 0} +static const struct among a_4[46] = { +{ 1, s_4_0, 0, 2, 0}, +{ 2, s_4_1, -1, 2, 0}, +{ 2, s_4_2, 0, 1, 0}, +{ 3, s_4_3, -1, 2, 0}, +{ 3, s_4_4, -2, 2, 0}, +{ 2, s_4_5, 0, 1, 0}, +{ 3, s_4_6, -1, 2, 0}, +{ 3, s_4_7, 0, 1, 0}, +{ 3, s_4_8, 0, 2, 0}, +{ 3, s_4_9, 0, 1, 0}, +{ 4, s_4_10, -1, 2, 0}, +{ 4, s_4_11, -2, 2, 0}, +{ 2, s_4_12, 0, 1, 0}, +{ 3, s_4_13, -1, 2, 0}, +{ 3, s_4_14, -2, 2, 0}, +{ 1, s_4_15, 0, 1, 0}, +{ 2, s_4_16, -1, 2, 0}, +{ 2, s_4_17, -2, 2, 0}, +{ 1, s_4_18, 0, 1, 0}, +{ 2, s_4_19, -1, 2, 0}, +{ 2, s_4_20, -2, 2, 0}, +{ 2, s_4_21, 0, 1, 0}, +{ 2, s_4_22, 0, 2, 0}, +{ 2, s_4_23, 0, 2, 0}, +{ 1, s_4_24, 0, 1, 0}, +{ 2, s_4_25, -1, 2, 0}, +{ 2, s_4_26, 0, 1, 0}, +{ 3, s_4_27, -1, 2, 0}, +{ 3, s_4_28, -2, 2, 0}, +{ 2, s_4_29, 0, 1, 0}, +{ 3, s_4_30, -1, 2, 0}, +{ 3, s_4_31, -2, 1, 0}, +{ 2, s_4_32, 0, 1, 0}, +{ 3, s_4_33, -1, 2, 0}, +{ 2, s_4_34, 0, 1, 0}, +{ 3, s_4_35, -1, 2, 0}, +{ 2, s_4_36, 0, 2, 0}, +{ 2, s_4_37, 0, 2, 0}, +{ 2, s_4_38, 0, 2, 0}, +{ 2, s_4_39, 0, 1, 0}, +{ 3, s_4_40, -1, 2, 0}, +{ 3, s_4_41, -2, 2, 0}, +{ 3, s_4_42, 0, 1, 0}, +{ 3, s_4_43, 0, 2, 0}, +{ 2, s_4_44, 0, 1, 0}, +{ 3, s_4_45, -1, 2, 0} }; static const symbol s_5_0[1] = { 0xC0 }; @@ -274,138 +268,129 @@ static const symbol s_5_32[2] = { 0xC5, 0xD7 }; static const symbol s_5_33[2] = { 0xCF, 0xD7 }; static const symbol s_5_34[1] = { 0xD8 }; static const symbol s_5_35[1] = { 0xD9 }; - -static const struct among a_5[36] = -{ -{ 1, s_5_0, -1, 1, 0}, -{ 2, s_5_1, 0, 1, 0}, -{ 2, s_5_2, 0, 1, 0}, -{ 1, s_5_3, -1, 1, 0}, -{ 1, s_5_4, -1, 1, 0}, -{ 2, s_5_5, 4, 1, 0}, -{ 2, s_5_6, 4, 1, 0}, -{ 2, s_5_7, -1, 1, 0}, -{ 2, s_5_8, -1, 1, 0}, -{ 3, s_5_9, 8, 1, 0}, -{ 1, s_5_10, -1, 1, 0}, -{ 2, s_5_11, 10, 1, 0}, -{ 2, s_5_12, 10, 1, 0}, -{ 3, s_5_13, 10, 1, 0}, -{ 3, s_5_14, 10, 1, 0}, -{ 4, s_5_15, 14, 1, 0}, -{ 1, s_5_16, -1, 1, 0}, -{ 2, s_5_17, 16, 1, 0}, -{ 3, s_5_18, 17, 1, 0}, -{ 2, s_5_19, 16, 1, 0}, -{ 2, s_5_20, 16, 1, 0}, -{ 2, s_5_21, -1, 1, 0}, -{ 2, s_5_22, -1, 1, 0}, -{ 3, s_5_23, 22, 1, 0}, -{ 2, s_5_24, -1, 1, 0}, -{ 2, s_5_25, -1, 1, 0}, -{ 3, s_5_26, 25, 1, 0}, -{ 1, s_5_27, -1, 1, 0}, -{ 1, s_5_28, -1, 1, 0}, -{ 2, s_5_29, 28, 1, 0}, -{ 2, s_5_30, 28, 1, 0}, -{ 1, s_5_31, -1, 1, 0}, -{ 2, s_5_32, -1, 1, 0}, -{ 2, s_5_33, -1, 1, 0}, -{ 1, s_5_34, -1, 1, 0}, -{ 1, s_5_35, -1, 1, 0} +static const struct among a_5[36] = { +{ 1, s_5_0, 0, 1, 0}, +{ 2, s_5_1, -1, 1, 0}, +{ 2, s_5_2, -2, 1, 0}, +{ 1, s_5_3, 0, 1, 0}, +{ 1, s_5_4, 0, 1, 0}, +{ 2, s_5_5, -1, 1, 0}, +{ 2, s_5_6, -2, 1, 0}, +{ 2, s_5_7, 0, 1, 0}, +{ 2, s_5_8, 0, 1, 0}, +{ 3, s_5_9, -1, 1, 0}, +{ 1, s_5_10, 0, 1, 0}, +{ 2, s_5_11, -1, 1, 0}, +{ 2, s_5_12, -2, 1, 0}, +{ 3, s_5_13, -3, 1, 0}, +{ 3, s_5_14, -4, 1, 0}, +{ 4, s_5_15, -1, 1, 0}, +{ 1, s_5_16, 0, 1, 0}, +{ 2, s_5_17, -1, 1, 0}, +{ 3, s_5_18, -1, 1, 0}, +{ 2, s_5_19, -3, 1, 0}, +{ 2, s_5_20, -4, 1, 0}, +{ 2, s_5_21, 0, 1, 0}, +{ 2, s_5_22, 0, 1, 0}, +{ 3, s_5_23, -1, 1, 0}, +{ 2, s_5_24, 0, 1, 0}, +{ 2, s_5_25, 0, 1, 0}, +{ 3, s_5_26, -1, 1, 0}, +{ 1, s_5_27, 0, 1, 0}, +{ 1, s_5_28, 0, 1, 0}, +{ 2, s_5_29, -1, 1, 0}, +{ 2, s_5_30, -2, 1, 0}, +{ 1, s_5_31, 0, 1, 0}, +{ 2, s_5_32, 0, 1, 0}, +{ 2, s_5_33, 0, 1, 0}, +{ 1, s_5_34, 0, 1, 0}, +{ 1, s_5_35, 0, 1, 0} }; static const symbol s_6_0[3] = { 0xCF, 0xD3, 0xD4 }; static const symbol s_6_1[4] = { 0xCF, 0xD3, 0xD4, 0xD8 }; - -static const struct among a_6[2] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 4, s_6_1, -1, 1, 0} +static const struct among a_6[2] = { +{ 3, s_6_0, 0, 1, 0}, +{ 4, s_6_1, 0, 1, 0} }; static const symbol s_7_0[4] = { 0xC5, 0xCA, 0xDB, 0xC5 }; static const symbol s_7_1[1] = { 0xCE }; static const symbol s_7_2[1] = { 0xD8 }; static const symbol s_7_3[3] = { 0xC5, 0xCA, 0xDB }; - -static const struct among a_7[4] = -{ -{ 4, s_7_0, -1, 1, 0}, -{ 1, s_7_1, -1, 2, 0}, -{ 1, s_7_2, -1, 3, 0}, -{ 3, s_7_3, -1, 1, 0} +static const struct among a_7[4] = { +{ 4, s_7_0, 0, 1, 0}, +{ 1, s_7_1, 0, 2, 0}, +{ 1, s_7_2, 0, 3, 0}, +{ 3, s_7_3, 0, 1, 0} }; static const unsigned char g_v[] = { 35, 130, 34, 18 }; -static const symbol s_0[] = { 0xC5 }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping(z, g_v, 192, 220, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_pV = z->c; { int ret = in_grouping(z, g_v, 192, 220, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = out_grouping(z, g_v, 192, 220, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping(z, g_v, 192, 220, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_perfective_gerund(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((25166336 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_0, 9); + among_var = find_among_b(z, a_0, 9, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 0xC1) goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 0xC1) goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 0xD1) return 0; z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -416,9 +401,10 @@ static int r_perfective_gerund(struct SN_env * z) { static int r_adjective(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((2271009 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_1, 26)) return 0; + if (!find_among_b(z, a_1, 26, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -426,33 +412,37 @@ static int r_adjective(struct SN_env * z) { static int r_adjectival(struct SN_env * z) { int among_var; - { int ret = r_adjective(z); + { + int ret = r_adjective(z); if (ret <= 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((671113216 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m1; goto lab0; } - among_var = find_among_b(z, a_2, 8); - if (!among_var) { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((671113216 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_1; goto lab0; } + among_var = find_among_b(z, a_2, 8, 0); + if (!among_var) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; - if (z->c <= z->lb || z->p[z->c - 1] != 0xC1) goto lab2; + do { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 0xC1) goto lab1; z->c--; - goto lab1; - lab2: - z->c = z->l - m2; - if (z->c <= z->lb || z->p[z->c - 1] != 0xD1) { z->c = z->l - m1; goto lab0; } + break; + lab1: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 0xD1) { z->c = z->l - v_1; goto lab0; } z->c--; - } - lab1: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -466,9 +456,10 @@ static int r_adjectival(struct SN_env * z) { static int r_reflexive(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 209 && z->p[z->c - 1] != 216)) return 0; - if (!find_among_b(z, a_3, 2)) return 0; + if (!find_among_b(z, a_3, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -478,27 +469,29 @@ static int r_verb(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((51443235 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_4, 46); + among_var = find_among_b(z, a_4, 46, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 0xC1) goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 0xC1) goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 0xD1) return 0; z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -509,9 +502,10 @@ static int r_verb(struct SN_env * z) { static int r_noun(struct SN_env * z) { z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((60991267 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_5, 36)) return 0; + if (!find_among_b(z, a_5, 36, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -520,12 +514,14 @@ static int r_noun(struct SN_env * z) { static int r_derivational(struct SN_env * z) { z->ket = z->c; if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 212 && z->p[z->c - 1] != 216)) return 0; - if (!find_among_b(z, a_6, 2)) return 0; + if (!find_among_b(z, a_6, 2, 0)) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -535,12 +531,13 @@ static int r_tidy_up(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 6 || !((151011360 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_7, 4); + among_var = find_among_b(z, a_7, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; @@ -549,19 +546,22 @@ static int r_tidy_up(struct SN_env * z) { z->bra = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 0xCE) return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (z->c <= z->lb || z->p[z->c - 1] != 0xCE) return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -570,116 +570,138 @@ static int r_tidy_up(struct SN_env * z) { } extern int russian_KOI8_R_stem(struct SN_env * z) { - { int c1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 0xA3) goto lab2; z->c++; z->ket = z->c; - z->c = c3; + z->c = v_3; break; lab2: - z->c = c3; + z->c = v_3; if (z->c >= z->l) goto lab1; z->c++; } - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - - { int mlimit4; - if (z->c < z->I[1]) return 0; - mlimit4 = z->lb; z->lb = z->I[1]; - { int m5 = z->l - z->c; (void)m5; - { int m6 = z->l - z->c; (void)m6; - { int ret = r_perfective_gerund(z); - if (ret == 0) goto lab5; + { + int v_4; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_4 = z->lb; z->lb = ((SN_local *)z)->i_pV; + { + int v_5 = z->l - z->c; + do { + int v_6 = z->l - z->c; + { + int ret = r_perfective_gerund(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m6; - { int m7 = z->l - z->c; (void)m7; - { int ret = r_reflexive(z); - if (ret == 0) { z->c = z->l - m7; goto lab6; } + break; + lab4: + z->c = z->l - v_6; + { + int v_7 = z->l - z->c; + { + int ret = r_reflexive(z); + if (ret == 0) { z->c = z->l - v_7; goto lab5; } if (ret < 0) return ret; } - lab6: + lab5: ; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_adjectival(z); - if (ret == 0) goto lab8; + do { + int v_8 = z->l - z->c; + { + int ret = r_adjectival(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m8; - { int ret = r_verb(z); - if (ret == 0) goto lab9; + break; + lab6: + z->c = z->l - v_8; + { + int ret = r_verb(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } - goto lab7; - lab9: - z->c = z->l - m8; - { int ret = r_noun(z); + break; + lab7: + z->c = z->l - v_8; + { + int ret = r_noun(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } - } - lab7: - ; - } - lab4: + } while (0); + } while (0); lab3: - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m9 = z->l - z->c; (void)m9; + { + int v_9 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 0xC9) { z->c = z->l - m9; goto lab10; } + if (z->c <= z->lb || z->p[z->c - 1] != 0xC9) { z->c = z->l - v_9; goto lab8; } z->c--; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab10: + lab8: ; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_derivational(z); + { + int v_10 = z->l - z->c; + { + int ret = r_derivational(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } - { int m11 = z->l - z->c; (void)m11; - { int ret = r_tidy_up(z); + { + int v_11 = z->l - z->c; + { + int ret = r_tidy_up(z); if (ret < 0) return ret; } - z->c = z->l - m11; + z->c = z->l - v_11; } - z->lb = mlimit4; + z->lb = v_4; } z->c = z->lb; return 1; } -extern struct SN_env * russian_KOI8_R_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * russian_KOI8_R_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void russian_KOI8_R_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void russian_KOI8_R_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_arabic.c b/src/backend/snowball/libstemmer/stem_UTF_8_arabic.c index 1f00dd95c89e5..bd039b5747454 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_arabic.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_arabic.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from arabic.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_arabic.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + unsigned char b_is_defined; + unsigned char b_is_verb; + unsigned char b_is_noun; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int arabic_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_Checks1(struct SN_env * z); static int r_Normalize_pre(struct SN_env * z); static int r_Normalize_post(struct SN_env * z); @@ -30,18 +44,81 @@ static int r_Prefix_Step3b_Noun(struct SN_env * z); static int r_Prefix_Step3a_Noun(struct SN_env * z); static int r_Prefix_Step2(struct SN_env * z); static int r_Prefix_Step1(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * arabic_UTF_8_create_env(void); -extern void arabic_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { '0' }; +static const symbol s_1[] = { '1' }; +static const symbol s_2[] = { '2' }; +static const symbol s_3[] = { '3' }; +static const symbol s_4[] = { '4' }; +static const symbol s_5[] = { '5' }; +static const symbol s_6[] = { '6' }; +static const symbol s_7[] = { '7' }; +static const symbol s_8[] = { '8' }; +static const symbol s_9[] = { '9' }; +static const symbol s_10[] = { 0xD8, 0xA1 }; +static const symbol s_11[] = { 0xD8, 0xA3 }; +static const symbol s_12[] = { 0xD8, 0xA5 }; +static const symbol s_13[] = { 0xD8, 0xA6 }; +static const symbol s_14[] = { 0xD8, 0xA2 }; +static const symbol s_15[] = { 0xD8, 0xA4 }; +static const symbol s_16[] = { 0xD8, 0xA7 }; +static const symbol s_17[] = { 0xD8, 0xA8 }; +static const symbol s_18[] = { 0xD8, 0xA9 }; +static const symbol s_19[] = { 0xD8, 0xAA }; +static const symbol s_20[] = { 0xD8, 0xAB }; +static const symbol s_21[] = { 0xD8, 0xAC }; +static const symbol s_22[] = { 0xD8, 0xAD }; +static const symbol s_23[] = { 0xD8, 0xAE }; +static const symbol s_24[] = { 0xD8, 0xAF }; +static const symbol s_25[] = { 0xD8, 0xB0 }; +static const symbol s_26[] = { 0xD8, 0xB1 }; +static const symbol s_27[] = { 0xD8, 0xB2 }; +static const symbol s_28[] = { 0xD8, 0xB3 }; +static const symbol s_29[] = { 0xD8, 0xB4 }; +static const symbol s_30[] = { 0xD8, 0xB5 }; +static const symbol s_31[] = { 0xD8, 0xB6 }; +static const symbol s_32[] = { 0xD8, 0xB7 }; +static const symbol s_33[] = { 0xD8, 0xB8 }; +static const symbol s_34[] = { 0xD8, 0xB9 }; +static const symbol s_35[] = { 0xD8, 0xBA }; +static const symbol s_36[] = { 0xD9, 0x81 }; +static const symbol s_37[] = { 0xD9, 0x82 }; +static const symbol s_38[] = { 0xD9, 0x83 }; +static const symbol s_39[] = { 0xD9, 0x84 }; +static const symbol s_40[] = { 0xD9, 0x85 }; +static const symbol s_41[] = { 0xD9, 0x86 }; +static const symbol s_42[] = { 0xD9, 0x87 }; +static const symbol s_43[] = { 0xD9, 0x88 }; +static const symbol s_44[] = { 0xD9, 0x89 }; +static const symbol s_45[] = { 0xD9, 0x8A }; +static const symbol s_46[] = { 0xD9, 0x84, 0xD8, 0xA7 }; +static const symbol s_47[] = { 0xD9, 0x84, 0xD8, 0xA3 }; +static const symbol s_48[] = { 0xD9, 0x84, 0xD8, 0xA5 }; +static const symbol s_49[] = { 0xD9, 0x84, 0xD8, 0xA2 }; +static const symbol s_50[] = { 0xD8, 0xA1 }; +static const symbol s_51[] = { 0xD8, 0xA7 }; +static const symbol s_52[] = { 0xD9, 0x88 }; +static const symbol s_53[] = { 0xD9, 0x8A }; +static const symbol s_54[] = { 0xD8, 0xA3 }; +static const symbol s_55[] = { 0xD8, 0xA2 }; +static const symbol s_56[] = { 0xD8, 0xA7 }; +static const symbol s_57[] = { 0xD8, 0xA5 }; +static const symbol s_58[] = { 0xD8, 0xA7 }; +static const symbol s_59[] = { 0xD8, 0xA8 }; +static const symbol s_60[] = { 0xD9, 0x83 }; +static const symbol s_61[] = { 0xD9, 0x8A }; +static const symbol s_62[] = { 0xD8, 0xAA }; +static const symbol s_63[] = { 0xD9, 0x86 }; +static const symbol s_64[] = { 0xD8, 0xA3 }; +static const symbol s_65[] = { 0xD8, 0xA7, 0xD8, 0xB3, 0xD8, 0xAA }; +static const symbol s_66[] = { 0xD9, 0x86 }; +static const symbol s_67[] = { 0xD8, 0xA7, 0xD8, 0xAA }; +static const symbol s_68[] = { 0xD8, 0xAA }; +static const symbol s_69[] = { 0xD8, 0xA9 }; +static const symbol s_70[] = { 0xD9, 0x8A }; +static const symbol s_71[] = { 0xD9, 0x89 }; +static const symbol s_72[] = { 0xD9, 0x8A }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 0xD9, 0x80 }; static const symbol s_0_1[2] = { 0xD9, 0x8B }; static const symbol s_0_2[2] = { 0xD9, 0x8C }; @@ -186,153 +263,151 @@ static const symbol s_0_140[3] = { 0xEF, 0xBB, 0xB9 }; static const symbol s_0_141[3] = { 0xEF, 0xBB, 0xBA }; static const symbol s_0_142[3] = { 0xEF, 0xBB, 0xBB }; static const symbol s_0_143[3] = { 0xEF, 0xBB, 0xBC }; - -static const struct among a_0[144] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 2, s_0_1, -1, 1, 0}, -{ 2, s_0_2, -1, 1, 0}, -{ 2, s_0_3, -1, 1, 0}, -{ 2, s_0_4, -1, 1, 0}, -{ 2, s_0_5, -1, 1, 0}, -{ 2, s_0_6, -1, 1, 0}, -{ 2, s_0_7, -1, 1, 0}, -{ 2, s_0_8, -1, 1, 0}, -{ 2, s_0_9, -1, 2, 0}, -{ 2, s_0_10, -1, 3, 0}, -{ 2, s_0_11, -1, 4, 0}, -{ 2, s_0_12, -1, 5, 0}, -{ 2, s_0_13, -1, 6, 0}, -{ 2, s_0_14, -1, 7, 0}, -{ 2, s_0_15, -1, 8, 0}, -{ 2, s_0_16, -1, 9, 0}, -{ 2, s_0_17, -1, 10, 0}, -{ 2, s_0_18, -1, 11, 0}, -{ 3, s_0_19, -1, 12, 0}, -{ 3, s_0_20, -1, 16, 0}, -{ 3, s_0_21, -1, 16, 0}, -{ 3, s_0_22, -1, 13, 0}, -{ 3, s_0_23, -1, 13, 0}, -{ 3, s_0_24, -1, 17, 0}, -{ 3, s_0_25, -1, 17, 0}, -{ 3, s_0_26, -1, 14, 0}, -{ 3, s_0_27, -1, 14, 0}, -{ 3, s_0_28, -1, 15, 0}, -{ 3, s_0_29, -1, 15, 0}, -{ 3, s_0_30, -1, 15, 0}, -{ 3, s_0_31, -1, 15, 0}, -{ 3, s_0_32, -1, 18, 0}, -{ 3, s_0_33, -1, 18, 0}, -{ 3, s_0_34, -1, 19, 0}, -{ 3, s_0_35, -1, 19, 0}, -{ 3, s_0_36, -1, 19, 0}, -{ 3, s_0_37, -1, 19, 0}, -{ 3, s_0_38, -1, 20, 0}, -{ 3, s_0_39, -1, 20, 0}, -{ 3, s_0_40, -1, 21, 0}, -{ 3, s_0_41, -1, 21, 0}, -{ 3, s_0_42, -1, 21, 0}, -{ 3, s_0_43, -1, 21, 0}, -{ 3, s_0_44, -1, 22, 0}, -{ 3, s_0_45, -1, 22, 0}, -{ 3, s_0_46, -1, 22, 0}, -{ 3, s_0_47, -1, 22, 0}, -{ 3, s_0_48, -1, 23, 0}, -{ 3, s_0_49, -1, 23, 0}, -{ 3, s_0_50, -1, 23, 0}, -{ 3, s_0_51, -1, 23, 0}, -{ 3, s_0_52, -1, 24, 0}, -{ 3, s_0_53, -1, 24, 0}, -{ 3, s_0_54, -1, 24, 0}, -{ 3, s_0_55, -1, 24, 0}, -{ 3, s_0_56, -1, 25, 0}, -{ 3, s_0_57, -1, 25, 0}, -{ 3, s_0_58, -1, 25, 0}, -{ 3, s_0_59, -1, 25, 0}, -{ 3, s_0_60, -1, 26, 0}, -{ 3, s_0_61, -1, 26, 0}, -{ 3, s_0_62, -1, 27, 0}, -{ 3, s_0_63, -1, 27, 0}, -{ 3, s_0_64, -1, 28, 0}, -{ 3, s_0_65, -1, 28, 0}, -{ 3, s_0_66, -1, 29, 0}, -{ 3, s_0_67, -1, 29, 0}, -{ 3, s_0_68, -1, 30, 0}, -{ 3, s_0_69, -1, 30, 0}, -{ 3, s_0_70, -1, 30, 0}, -{ 3, s_0_71, -1, 30, 0}, -{ 3, s_0_72, -1, 31, 0}, -{ 3, s_0_73, -1, 31, 0}, -{ 3, s_0_74, -1, 31, 0}, -{ 3, s_0_75, -1, 31, 0}, -{ 3, s_0_76, -1, 32, 0}, -{ 3, s_0_77, -1, 32, 0}, -{ 3, s_0_78, -1, 32, 0}, -{ 3, s_0_79, -1, 32, 0}, -{ 3, s_0_80, -1, 33, 0}, -{ 3, s_0_81, -1, 33, 0}, -{ 3, s_0_82, -1, 33, 0}, -{ 3, s_0_83, -1, 33, 0}, -{ 3, s_0_84, -1, 34, 0}, -{ 3, s_0_85, -1, 34, 0}, -{ 3, s_0_86, -1, 34, 0}, -{ 3, s_0_87, -1, 34, 0}, -{ 3, s_0_88, -1, 35, 0}, -{ 3, s_0_89, -1, 35, 0}, -{ 3, s_0_90, -1, 35, 0}, -{ 3, s_0_91, -1, 35, 0}, -{ 3, s_0_92, -1, 36, 0}, -{ 3, s_0_93, -1, 36, 0}, -{ 3, s_0_94, -1, 36, 0}, -{ 3, s_0_95, -1, 36, 0}, -{ 3, s_0_96, -1, 37, 0}, -{ 3, s_0_97, -1, 37, 0}, -{ 3, s_0_98, -1, 37, 0}, -{ 3, s_0_99, -1, 37, 0}, -{ 3, s_0_100, -1, 38, 0}, -{ 3, s_0_101, -1, 38, 0}, -{ 3, s_0_102, -1, 38, 0}, -{ 3, s_0_103, -1, 38, 0}, -{ 3, s_0_104, -1, 39, 0}, -{ 3, s_0_105, -1, 39, 0}, -{ 3, s_0_106, -1, 39, 0}, -{ 3, s_0_107, -1, 39, 0}, -{ 3, s_0_108, -1, 40, 0}, -{ 3, s_0_109, -1, 40, 0}, -{ 3, s_0_110, -1, 40, 0}, -{ 3, s_0_111, -1, 40, 0}, -{ 3, s_0_112, -1, 41, 0}, -{ 3, s_0_113, -1, 41, 0}, -{ 3, s_0_114, -1, 41, 0}, -{ 3, s_0_115, -1, 41, 0}, -{ 3, s_0_116, -1, 42, 0}, -{ 3, s_0_117, -1, 42, 0}, -{ 3, s_0_118, -1, 42, 0}, -{ 3, s_0_119, -1, 42, 0}, -{ 3, s_0_120, -1, 43, 0}, -{ 3, s_0_121, -1, 43, 0}, -{ 3, s_0_122, -1, 43, 0}, -{ 3, s_0_123, -1, 43, 0}, -{ 3, s_0_124, -1, 44, 0}, -{ 3, s_0_125, -1, 44, 0}, -{ 3, s_0_126, -1, 44, 0}, -{ 3, s_0_127, -1, 44, 0}, -{ 3, s_0_128, -1, 45, 0}, -{ 3, s_0_129, -1, 45, 0}, -{ 3, s_0_130, -1, 46, 0}, -{ 3, s_0_131, -1, 46, 0}, -{ 3, s_0_132, -1, 47, 0}, -{ 3, s_0_133, -1, 47, 0}, -{ 3, s_0_134, -1, 47, 0}, -{ 3, s_0_135, -1, 47, 0}, -{ 3, s_0_136, -1, 51, 0}, -{ 3, s_0_137, -1, 51, 0}, -{ 3, s_0_138, -1, 49, 0}, -{ 3, s_0_139, -1, 49, 0}, -{ 3, s_0_140, -1, 50, 0}, -{ 3, s_0_141, -1, 50, 0}, -{ 3, s_0_142, -1, 48, 0}, -{ 3, s_0_143, -1, 48, 0} +static const struct among a_0[144] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 1, 0}, +{ 2, s_0_2, 0, 1, 0}, +{ 2, s_0_3, 0, 1, 0}, +{ 2, s_0_4, 0, 1, 0}, +{ 2, s_0_5, 0, 1, 0}, +{ 2, s_0_6, 0, 1, 0}, +{ 2, s_0_7, 0, 1, 0}, +{ 2, s_0_8, 0, 1, 0}, +{ 2, s_0_9, 0, 2, 0}, +{ 2, s_0_10, 0, 3, 0}, +{ 2, s_0_11, 0, 4, 0}, +{ 2, s_0_12, 0, 5, 0}, +{ 2, s_0_13, 0, 6, 0}, +{ 2, s_0_14, 0, 7, 0}, +{ 2, s_0_15, 0, 8, 0}, +{ 2, s_0_16, 0, 9, 0}, +{ 2, s_0_17, 0, 10, 0}, +{ 2, s_0_18, 0, 11, 0}, +{ 3, s_0_19, 0, 12, 0}, +{ 3, s_0_20, 0, 16, 0}, +{ 3, s_0_21, 0, 16, 0}, +{ 3, s_0_22, 0, 13, 0}, +{ 3, s_0_23, 0, 13, 0}, +{ 3, s_0_24, 0, 17, 0}, +{ 3, s_0_25, 0, 17, 0}, +{ 3, s_0_26, 0, 14, 0}, +{ 3, s_0_27, 0, 14, 0}, +{ 3, s_0_28, 0, 15, 0}, +{ 3, s_0_29, 0, 15, 0}, +{ 3, s_0_30, 0, 15, 0}, +{ 3, s_0_31, 0, 15, 0}, +{ 3, s_0_32, 0, 18, 0}, +{ 3, s_0_33, 0, 18, 0}, +{ 3, s_0_34, 0, 19, 0}, +{ 3, s_0_35, 0, 19, 0}, +{ 3, s_0_36, 0, 19, 0}, +{ 3, s_0_37, 0, 19, 0}, +{ 3, s_0_38, 0, 20, 0}, +{ 3, s_0_39, 0, 20, 0}, +{ 3, s_0_40, 0, 21, 0}, +{ 3, s_0_41, 0, 21, 0}, +{ 3, s_0_42, 0, 21, 0}, +{ 3, s_0_43, 0, 21, 0}, +{ 3, s_0_44, 0, 22, 0}, +{ 3, s_0_45, 0, 22, 0}, +{ 3, s_0_46, 0, 22, 0}, +{ 3, s_0_47, 0, 22, 0}, +{ 3, s_0_48, 0, 23, 0}, +{ 3, s_0_49, 0, 23, 0}, +{ 3, s_0_50, 0, 23, 0}, +{ 3, s_0_51, 0, 23, 0}, +{ 3, s_0_52, 0, 24, 0}, +{ 3, s_0_53, 0, 24, 0}, +{ 3, s_0_54, 0, 24, 0}, +{ 3, s_0_55, 0, 24, 0}, +{ 3, s_0_56, 0, 25, 0}, +{ 3, s_0_57, 0, 25, 0}, +{ 3, s_0_58, 0, 25, 0}, +{ 3, s_0_59, 0, 25, 0}, +{ 3, s_0_60, 0, 26, 0}, +{ 3, s_0_61, 0, 26, 0}, +{ 3, s_0_62, 0, 27, 0}, +{ 3, s_0_63, 0, 27, 0}, +{ 3, s_0_64, 0, 28, 0}, +{ 3, s_0_65, 0, 28, 0}, +{ 3, s_0_66, 0, 29, 0}, +{ 3, s_0_67, 0, 29, 0}, +{ 3, s_0_68, 0, 30, 0}, +{ 3, s_0_69, 0, 30, 0}, +{ 3, s_0_70, 0, 30, 0}, +{ 3, s_0_71, 0, 30, 0}, +{ 3, s_0_72, 0, 31, 0}, +{ 3, s_0_73, 0, 31, 0}, +{ 3, s_0_74, 0, 31, 0}, +{ 3, s_0_75, 0, 31, 0}, +{ 3, s_0_76, 0, 32, 0}, +{ 3, s_0_77, 0, 32, 0}, +{ 3, s_0_78, 0, 32, 0}, +{ 3, s_0_79, 0, 32, 0}, +{ 3, s_0_80, 0, 33, 0}, +{ 3, s_0_81, 0, 33, 0}, +{ 3, s_0_82, 0, 33, 0}, +{ 3, s_0_83, 0, 33, 0}, +{ 3, s_0_84, 0, 34, 0}, +{ 3, s_0_85, 0, 34, 0}, +{ 3, s_0_86, 0, 34, 0}, +{ 3, s_0_87, 0, 34, 0}, +{ 3, s_0_88, 0, 35, 0}, +{ 3, s_0_89, 0, 35, 0}, +{ 3, s_0_90, 0, 35, 0}, +{ 3, s_0_91, 0, 35, 0}, +{ 3, s_0_92, 0, 36, 0}, +{ 3, s_0_93, 0, 36, 0}, +{ 3, s_0_94, 0, 36, 0}, +{ 3, s_0_95, 0, 36, 0}, +{ 3, s_0_96, 0, 37, 0}, +{ 3, s_0_97, 0, 37, 0}, +{ 3, s_0_98, 0, 37, 0}, +{ 3, s_0_99, 0, 37, 0}, +{ 3, s_0_100, 0, 38, 0}, +{ 3, s_0_101, 0, 38, 0}, +{ 3, s_0_102, 0, 38, 0}, +{ 3, s_0_103, 0, 38, 0}, +{ 3, s_0_104, 0, 39, 0}, +{ 3, s_0_105, 0, 39, 0}, +{ 3, s_0_106, 0, 39, 0}, +{ 3, s_0_107, 0, 39, 0}, +{ 3, s_0_108, 0, 40, 0}, +{ 3, s_0_109, 0, 40, 0}, +{ 3, s_0_110, 0, 40, 0}, +{ 3, s_0_111, 0, 40, 0}, +{ 3, s_0_112, 0, 41, 0}, +{ 3, s_0_113, 0, 41, 0}, +{ 3, s_0_114, 0, 41, 0}, +{ 3, s_0_115, 0, 41, 0}, +{ 3, s_0_116, 0, 42, 0}, +{ 3, s_0_117, 0, 42, 0}, +{ 3, s_0_118, 0, 42, 0}, +{ 3, s_0_119, 0, 42, 0}, +{ 3, s_0_120, 0, 43, 0}, +{ 3, s_0_121, 0, 43, 0}, +{ 3, s_0_122, 0, 43, 0}, +{ 3, s_0_123, 0, 43, 0}, +{ 3, s_0_124, 0, 44, 0}, +{ 3, s_0_125, 0, 44, 0}, +{ 3, s_0_126, 0, 44, 0}, +{ 3, s_0_127, 0, 44, 0}, +{ 3, s_0_128, 0, 45, 0}, +{ 3, s_0_129, 0, 45, 0}, +{ 3, s_0_130, 0, 46, 0}, +{ 3, s_0_131, 0, 46, 0}, +{ 3, s_0_132, 0, 47, 0}, +{ 3, s_0_133, 0, 47, 0}, +{ 3, s_0_134, 0, 47, 0}, +{ 3, s_0_135, 0, 47, 0}, +{ 3, s_0_136, 0, 51, 0}, +{ 3, s_0_137, 0, 51, 0}, +{ 3, s_0_138, 0, 49, 0}, +{ 3, s_0_139, 0, 49, 0}, +{ 3, s_0_140, 0, 50, 0}, +{ 3, s_0_141, 0, 50, 0}, +{ 3, s_0_142, 0, 48, 0}, +{ 3, s_0_143, 0, 48, 0} }; static const symbol s_1_0[2] = { 0xD8, 0xA2 }; @@ -340,14 +415,12 @@ static const symbol s_1_1[2] = { 0xD8, 0xA3 }; static const symbol s_1_2[2] = { 0xD8, 0xA4 }; static const symbol s_1_3[2] = { 0xD8, 0xA5 }; static const symbol s_1_4[2] = { 0xD8, 0xA6 }; - -static const struct among a_1[5] = -{ -{ 2, s_1_0, -1, 1, 0}, -{ 2, s_1_1, -1, 1, 0}, -{ 2, s_1_2, -1, 1, 0}, -{ 2, s_1_3, -1, 1, 0}, -{ 2, s_1_4, -1, 1, 0} +static const struct among a_1[5] = { +{ 2, s_1_0, 0, 1, 0}, +{ 2, s_1_1, 0, 1, 0}, +{ 2, s_1_2, 0, 1, 0}, +{ 2, s_1_3, 0, 1, 0}, +{ 2, s_1_4, 0, 1, 0} }; static const symbol s_2_0[2] = { 0xD8, 0xA2 }; @@ -355,27 +428,23 @@ static const symbol s_2_1[2] = { 0xD8, 0xA3 }; static const symbol s_2_2[2] = { 0xD8, 0xA4 }; static const symbol s_2_3[2] = { 0xD8, 0xA5 }; static const symbol s_2_4[2] = { 0xD8, 0xA6 }; - -static const struct among a_2[5] = -{ -{ 2, s_2_0, -1, 1, 0}, -{ 2, s_2_1, -1, 1, 0}, -{ 2, s_2_2, -1, 2, 0}, -{ 2, s_2_3, -1, 1, 0}, -{ 2, s_2_4, -1, 3, 0} +static const struct among a_2[5] = { +{ 2, s_2_0, 0, 1, 0}, +{ 2, s_2_1, 0, 1, 0}, +{ 2, s_2_2, 0, 2, 0}, +{ 2, s_2_3, 0, 1, 0}, +{ 2, s_2_4, 0, 3, 0} }; static const symbol s_3_0[4] = { 0xD8, 0xA7, 0xD9, 0x84 }; static const symbol s_3_1[6] = { 0xD8, 0xA8, 0xD8, 0xA7, 0xD9, 0x84 }; static const symbol s_3_2[6] = { 0xD9, 0x83, 0xD8, 0xA7, 0xD9, 0x84 }; static const symbol s_3_3[4] = { 0xD9, 0x84, 0xD9, 0x84 }; - -static const struct among a_3[4] = -{ -{ 4, s_3_0, -1, 2, 0}, -{ 6, s_3_1, -1, 1, 0}, -{ 6, s_3_2, -1, 1, 0}, -{ 4, s_3_3, -1, 2, 0} +static const struct among a_3[4] = { +{ 4, s_3_0, 0, 2, 0}, +{ 6, s_3_1, 0, 1, 0}, +{ 6, s_3_2, 0, 1, 0}, +{ 4, s_3_3, 0, 2, 0} }; static const symbol s_4_0[4] = { 0xD8, 0xA3, 0xD8, 0xA2 }; @@ -383,73 +452,61 @@ static const symbol s_4_1[4] = { 0xD8, 0xA3, 0xD8, 0xA3 }; static const symbol s_4_2[4] = { 0xD8, 0xA3, 0xD8, 0xA4 }; static const symbol s_4_3[4] = { 0xD8, 0xA3, 0xD8, 0xA5 }; static const symbol s_4_4[4] = { 0xD8, 0xA3, 0xD8, 0xA7 }; - -static const struct among a_4[5] = -{ -{ 4, s_4_0, -1, 2, 0}, -{ 4, s_4_1, -1, 1, 0}, -{ 4, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 4, 0}, -{ 4, s_4_4, -1, 3, 0} +static const struct among a_4[5] = { +{ 4, s_4_0, 0, 2, 0}, +{ 4, s_4_1, 0, 1, 0}, +{ 4, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 4, 0}, +{ 4, s_4_4, 0, 3, 0} }; static const symbol s_5_0[2] = { 0xD9, 0x81 }; static const symbol s_5_1[2] = { 0xD9, 0x88 }; - -static const struct among a_5[2] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 1, 0} +static const struct among a_5[2] = { +{ 2, s_5_0, 0, 1, 0}, +{ 2, s_5_1, 0, 1, 0} }; static const symbol s_6_0[4] = { 0xD8, 0xA7, 0xD9, 0x84 }; static const symbol s_6_1[6] = { 0xD8, 0xA8, 0xD8, 0xA7, 0xD9, 0x84 }; static const symbol s_6_2[6] = { 0xD9, 0x83, 0xD8, 0xA7, 0xD9, 0x84 }; static const symbol s_6_3[4] = { 0xD9, 0x84, 0xD9, 0x84 }; - -static const struct among a_6[4] = -{ -{ 4, s_6_0, -1, 2, 0}, -{ 6, s_6_1, -1, 1, 0}, -{ 6, s_6_2, -1, 1, 0}, -{ 4, s_6_3, -1, 2, 0} +static const struct among a_6[4] = { +{ 4, s_6_0, 0, 2, 0}, +{ 6, s_6_1, 0, 1, 0}, +{ 6, s_6_2, 0, 1, 0}, +{ 4, s_6_3, 0, 2, 0} }; static const symbol s_7_0[2] = { 0xD8, 0xA8 }; static const symbol s_7_1[4] = { 0xD8, 0xA8, 0xD8, 0xA7 }; static const symbol s_7_2[4] = { 0xD8, 0xA8, 0xD8, 0xA8 }; static const symbol s_7_3[4] = { 0xD9, 0x83, 0xD9, 0x83 }; - -static const struct among a_7[4] = -{ -{ 2, s_7_0, -1, 1, 0}, -{ 4, s_7_1, 0, -1, 0}, -{ 4, s_7_2, 0, 2, 0}, -{ 4, s_7_3, -1, 3, 0} +static const struct among a_7[4] = { +{ 2, s_7_0, 0, 1, 0}, +{ 4, s_7_1, -1, -1, 0}, +{ 4, s_7_2, -2, 2, 0}, +{ 4, s_7_3, 0, 3, 0} }; static const symbol s_8_0[4] = { 0xD8, 0xB3, 0xD8, 0xA3 }; static const symbol s_8_1[4] = { 0xD8, 0xB3, 0xD8, 0xAA }; static const symbol s_8_2[4] = { 0xD8, 0xB3, 0xD9, 0x86 }; static const symbol s_8_3[4] = { 0xD8, 0xB3, 0xD9, 0x8A }; - -static const struct among a_8[4] = -{ -{ 4, s_8_0, -1, 4, 0}, -{ 4, s_8_1, -1, 2, 0}, -{ 4, s_8_2, -1, 3, 0}, -{ 4, s_8_3, -1, 1, 0} +static const struct among a_8[4] = { +{ 4, s_8_0, 0, 4, 0}, +{ 4, s_8_1, 0, 2, 0}, +{ 4, s_8_2, 0, 3, 0}, +{ 4, s_8_3, 0, 1, 0} }; static const symbol s_9_0[6] = { 0xD8, 0xAA, 0xD8, 0xB3, 0xD8, 0xAA }; static const symbol s_9_1[6] = { 0xD9, 0x86, 0xD8, 0xB3, 0xD8, 0xAA }; static const symbol s_9_2[6] = { 0xD9, 0x8A, 0xD8, 0xB3, 0xD8, 0xAA }; - -static const struct among a_9[3] = -{ -{ 6, s_9_0, -1, 1, 0}, -{ 6, s_9_1, -1, 1, 0}, -{ 6, s_9_2, -1, 1, 0} +static const struct among a_9[3] = { +{ 6, s_9_0, 0, 1, 0}, +{ 6, s_9_1, 0, 1, 0}, +{ 6, s_9_2, 0, 1, 0} }; static const symbol s_10_0[2] = { 0xD9, 0x83 }; @@ -462,559 +519,496 @@ static const symbol s_10_6[6] = { 0xD9, 0x83, 0xD9, 0x85, 0xD8, 0xA7 }; static const symbol s_10_7[6] = { 0xD9, 0x87, 0xD9, 0x85, 0xD8, 0xA7 }; static const symbol s_10_8[4] = { 0xD9, 0x86, 0xD8, 0xA7 }; static const symbol s_10_9[4] = { 0xD9, 0x87, 0xD8, 0xA7 }; - -static const struct among a_10[10] = -{ -{ 2, s_10_0, -1, 1, 0}, -{ 4, s_10_1, -1, 2, 0}, -{ 4, s_10_2, -1, 2, 0}, -{ 4, s_10_3, -1, 2, 0}, -{ 2, s_10_4, -1, 1, 0}, -{ 2, s_10_5, -1, 1, 0}, -{ 6, s_10_6, -1, 3, 0}, -{ 6, s_10_7, -1, 3, 0}, -{ 4, s_10_8, -1, 2, 0}, -{ 4, s_10_9, -1, 2, 0} +static const struct among a_10[10] = { +{ 2, s_10_0, 0, 1, 0}, +{ 4, s_10_1, 0, 2, 0}, +{ 4, s_10_2, 0, 2, 0}, +{ 4, s_10_3, 0, 2, 0}, +{ 2, s_10_4, 0, 1, 0}, +{ 2, s_10_5, 0, 1, 0}, +{ 6, s_10_6, 0, 3, 0}, +{ 6, s_10_7, 0, 3, 0}, +{ 4, s_10_8, 0, 2, 0}, +{ 4, s_10_9, 0, 2, 0} }; -static const symbol s_11_0[2] = { 0xD9, 0x86 }; - -static const struct among a_11[1] = -{ -{ 2, s_11_0, -1, 1, 0} +static const symbol s_11_0[2] = { 0xD9, 0x88 }; +static const symbol s_11_1[2] = { 0xD9, 0x8A }; +static const symbol s_11_2[2] = { 0xD8, 0xA7 }; +static const struct among a_11[3] = { +{ 2, s_11_0, 0, 1, 0}, +{ 2, s_11_1, 0, 1, 0}, +{ 2, s_11_2, 0, 1, 0} }; -static const symbol s_12_0[2] = { 0xD9, 0x88 }; -static const symbol s_12_1[2] = { 0xD9, 0x8A }; -static const symbol s_12_2[2] = { 0xD8, 0xA7 }; - -static const struct among a_12[3] = -{ -{ 2, s_12_0, -1, 1, 0}, -{ 2, s_12_1, -1, 1, 0}, -{ 2, s_12_2, -1, 1, 0} +static const symbol s_12_0[2] = { 0xD9, 0x83 }; +static const symbol s_12_1[4] = { 0xD9, 0x83, 0xD9, 0x85 }; +static const symbol s_12_2[4] = { 0xD9, 0x87, 0xD9, 0x85 }; +static const symbol s_12_3[4] = { 0xD9, 0x83, 0xD9, 0x86 }; +static const symbol s_12_4[4] = { 0xD9, 0x87, 0xD9, 0x86 }; +static const symbol s_12_5[2] = { 0xD9, 0x87 }; +static const symbol s_12_6[6] = { 0xD9, 0x83, 0xD9, 0x85, 0xD9, 0x88 }; +static const symbol s_12_7[4] = { 0xD9, 0x86, 0xD9, 0x8A }; +static const symbol s_12_8[6] = { 0xD9, 0x83, 0xD9, 0x85, 0xD8, 0xA7 }; +static const symbol s_12_9[6] = { 0xD9, 0x87, 0xD9, 0x85, 0xD8, 0xA7 }; +static const symbol s_12_10[4] = { 0xD9, 0x86, 0xD8, 0xA7 }; +static const symbol s_12_11[4] = { 0xD9, 0x87, 0xD8, 0xA7 }; +static const struct among a_12[12] = { +{ 2, s_12_0, 0, 1, 0}, +{ 4, s_12_1, 0, 2, 0}, +{ 4, s_12_2, 0, 2, 0}, +{ 4, s_12_3, 0, 2, 0}, +{ 4, s_12_4, 0, 2, 0}, +{ 2, s_12_5, 0, 1, 0}, +{ 6, s_12_6, 0, 3, 0}, +{ 4, s_12_7, 0, 2, 0}, +{ 6, s_12_8, 0, 3, 0}, +{ 6, s_12_9, 0, 3, 0}, +{ 4, s_12_10, 0, 2, 0}, +{ 4, s_12_11, 0, 2, 0} }; -static const symbol s_13_0[4] = { 0xD8, 0xA7, 0xD8, 0xAA }; - -static const struct among a_13[1] = -{ -{ 4, s_13_0, -1, 1, 0} +static const symbol s_13_0[2] = { 0xD9, 0x86 }; +static const symbol s_13_1[4] = { 0xD9, 0x88, 0xD9, 0x86 }; +static const symbol s_13_2[4] = { 0xD9, 0x8A, 0xD9, 0x86 }; +static const symbol s_13_3[4] = { 0xD8, 0xA7, 0xD9, 0x86 }; +static const symbol s_13_4[4] = { 0xD8, 0xAA, 0xD9, 0x86 }; +static const symbol s_13_5[2] = { 0xD9, 0x8A }; +static const symbol s_13_6[2] = { 0xD8, 0xA7 }; +static const symbol s_13_7[6] = { 0xD8, 0xAA, 0xD9, 0x85, 0xD8, 0xA7 }; +static const symbol s_13_8[4] = { 0xD9, 0x86, 0xD8, 0xA7 }; +static const symbol s_13_9[4] = { 0xD8, 0xAA, 0xD8, 0xA7 }; +static const symbol s_13_10[2] = { 0xD8, 0xAA }; +static const struct among a_13[11] = { +{ 2, s_13_0, 0, 1, 0}, +{ 4, s_13_1, -1, 3, 0}, +{ 4, s_13_2, -2, 3, 0}, +{ 4, s_13_3, -3, 3, 0}, +{ 4, s_13_4, -4, 2, 0}, +{ 2, s_13_5, 0, 1, 0}, +{ 2, s_13_6, 0, 1, 0}, +{ 6, s_13_7, -1, 4, 0}, +{ 4, s_13_8, -2, 2, 0}, +{ 4, s_13_9, -3, 2, 0}, +{ 2, s_13_10, 0, 1, 0} }; -static const symbol s_14_0[2] = { 0xD8, 0xAA }; - -static const struct among a_14[1] = -{ -{ 2, s_14_0, -1, 1, 0} -}; - -static const symbol s_15_0[2] = { 0xD8, 0xA9 }; - -static const struct among a_15[1] = -{ -{ 2, s_15_0, -1, 1, 0} +static const symbol s_14_0[4] = { 0xD8, 0xAA, 0xD9, 0x85 }; +static const symbol s_14_1[4] = { 0xD9, 0x88, 0xD8, 0xA7 }; +static const struct among a_14[2] = { +{ 4, s_14_0, 0, 1, 0}, +{ 4, s_14_1, 0, 1, 0} }; -static const symbol s_16_0[2] = { 0xD9, 0x8A }; - -static const struct among a_16[1] = -{ -{ 2, s_16_0, -1, 1, 0} -}; - -static const symbol s_17_0[2] = { 0xD9, 0x83 }; -static const symbol s_17_1[4] = { 0xD9, 0x83, 0xD9, 0x85 }; -static const symbol s_17_2[4] = { 0xD9, 0x87, 0xD9, 0x85 }; -static const symbol s_17_3[4] = { 0xD9, 0x83, 0xD9, 0x86 }; -static const symbol s_17_4[4] = { 0xD9, 0x87, 0xD9, 0x86 }; -static const symbol s_17_5[2] = { 0xD9, 0x87 }; -static const symbol s_17_6[6] = { 0xD9, 0x83, 0xD9, 0x85, 0xD9, 0x88 }; -static const symbol s_17_7[4] = { 0xD9, 0x86, 0xD9, 0x8A }; -static const symbol s_17_8[6] = { 0xD9, 0x83, 0xD9, 0x85, 0xD8, 0xA7 }; -static const symbol s_17_9[6] = { 0xD9, 0x87, 0xD9, 0x85, 0xD8, 0xA7 }; -static const symbol s_17_10[4] = { 0xD9, 0x86, 0xD8, 0xA7 }; -static const symbol s_17_11[4] = { 0xD9, 0x87, 0xD8, 0xA7 }; - -static const struct among a_17[12] = -{ -{ 2, s_17_0, -1, 1, 0}, -{ 4, s_17_1, -1, 2, 0}, -{ 4, s_17_2, -1, 2, 0}, -{ 4, s_17_3, -1, 2, 0}, -{ 4, s_17_4, -1, 2, 0}, -{ 2, s_17_5, -1, 1, 0}, -{ 6, s_17_6, -1, 3, 0}, -{ 4, s_17_7, -1, 2, 0}, -{ 6, s_17_8, -1, 3, 0}, -{ 6, s_17_9, -1, 3, 0}, -{ 4, s_17_10, -1, 2, 0}, -{ 4, s_17_11, -1, 2, 0} -}; - -static const symbol s_18_0[2] = { 0xD9, 0x86 }; -static const symbol s_18_1[4] = { 0xD9, 0x88, 0xD9, 0x86 }; -static const symbol s_18_2[4] = { 0xD9, 0x8A, 0xD9, 0x86 }; -static const symbol s_18_3[4] = { 0xD8, 0xA7, 0xD9, 0x86 }; -static const symbol s_18_4[4] = { 0xD8, 0xAA, 0xD9, 0x86 }; -static const symbol s_18_5[2] = { 0xD9, 0x8A }; -static const symbol s_18_6[2] = { 0xD8, 0xA7 }; -static const symbol s_18_7[6] = { 0xD8, 0xAA, 0xD9, 0x85, 0xD8, 0xA7 }; -static const symbol s_18_8[4] = { 0xD9, 0x86, 0xD8, 0xA7 }; -static const symbol s_18_9[4] = { 0xD8, 0xAA, 0xD8, 0xA7 }; -static const symbol s_18_10[2] = { 0xD8, 0xAA }; - -static const struct among a_18[11] = -{ -{ 2, s_18_0, -1, 1, 0}, -{ 4, s_18_1, 0, 3, 0}, -{ 4, s_18_2, 0, 3, 0}, -{ 4, s_18_3, 0, 3, 0}, -{ 4, s_18_4, 0, 2, 0}, -{ 2, s_18_5, -1, 1, 0}, -{ 2, s_18_6, -1, 1, 0}, -{ 6, s_18_7, 6, 4, 0}, -{ 4, s_18_8, 6, 2, 0}, -{ 4, s_18_9, 6, 2, 0}, -{ 2, s_18_10, -1, 1, 0} -}; - -static const symbol s_19_0[4] = { 0xD8, 0xAA, 0xD9, 0x85 }; -static const symbol s_19_1[4] = { 0xD9, 0x88, 0xD8, 0xA7 }; - -static const struct among a_19[2] = -{ -{ 4, s_19_0, -1, 1, 0}, -{ 4, s_19_1, -1, 1, 0} -}; - -static const symbol s_20_0[2] = { 0xD9, 0x88 }; -static const symbol s_20_1[6] = { 0xD8, 0xAA, 0xD9, 0x85, 0xD9, 0x88 }; - -static const struct among a_20[2] = -{ -{ 2, s_20_0, -1, 1, 0}, -{ 6, s_20_1, 0, 2, 0} +static const symbol s_15_0[2] = { 0xD9, 0x88 }; +static const symbol s_15_1[6] = { 0xD8, 0xAA, 0xD9, 0x85, 0xD9, 0x88 }; +static const struct among a_15[2] = { +{ 2, s_15_0, 0, 1, 0}, +{ 6, s_15_1, -1, 2, 0} }; -static const symbol s_21_0[2] = { 0xD9, 0x89 }; - -static const struct among a_21[1] = -{ -{ 2, s_21_0, -1, 1, 0} -}; - -static const symbol s_0[] = { '0' }; -static const symbol s_1[] = { '1' }; -static const symbol s_2[] = { '2' }; -static const symbol s_3[] = { '3' }; -static const symbol s_4[] = { '4' }; -static const symbol s_5[] = { '5' }; -static const symbol s_6[] = { '6' }; -static const symbol s_7[] = { '7' }; -static const symbol s_8[] = { '8' }; -static const symbol s_9[] = { '9' }; -static const symbol s_10[] = { 0xD8, 0xA1 }; -static const symbol s_11[] = { 0xD8, 0xA3 }; -static const symbol s_12[] = { 0xD8, 0xA5 }; -static const symbol s_13[] = { 0xD8, 0xA6 }; -static const symbol s_14[] = { 0xD8, 0xA2 }; -static const symbol s_15[] = { 0xD8, 0xA4 }; -static const symbol s_16[] = { 0xD8, 0xA7 }; -static const symbol s_17[] = { 0xD8, 0xA8 }; -static const symbol s_18[] = { 0xD8, 0xA9 }; -static const symbol s_19[] = { 0xD8, 0xAA }; -static const symbol s_20[] = { 0xD8, 0xAB }; -static const symbol s_21[] = { 0xD8, 0xAC }; -static const symbol s_22[] = { 0xD8, 0xAD }; -static const symbol s_23[] = { 0xD8, 0xAE }; -static const symbol s_24[] = { 0xD8, 0xAF }; -static const symbol s_25[] = { 0xD8, 0xB0 }; -static const symbol s_26[] = { 0xD8, 0xB1 }; -static const symbol s_27[] = { 0xD8, 0xB2 }; -static const symbol s_28[] = { 0xD8, 0xB3 }; -static const symbol s_29[] = { 0xD8, 0xB4 }; -static const symbol s_30[] = { 0xD8, 0xB5 }; -static const symbol s_31[] = { 0xD8, 0xB6 }; -static const symbol s_32[] = { 0xD8, 0xB7 }; -static const symbol s_33[] = { 0xD8, 0xB8 }; -static const symbol s_34[] = { 0xD8, 0xB9 }; -static const symbol s_35[] = { 0xD8, 0xBA }; -static const symbol s_36[] = { 0xD9, 0x81 }; -static const symbol s_37[] = { 0xD9, 0x82 }; -static const symbol s_38[] = { 0xD9, 0x83 }; -static const symbol s_39[] = { 0xD9, 0x84 }; -static const symbol s_40[] = { 0xD9, 0x85 }; -static const symbol s_41[] = { 0xD9, 0x86 }; -static const symbol s_42[] = { 0xD9, 0x87 }; -static const symbol s_43[] = { 0xD9, 0x88 }; -static const symbol s_44[] = { 0xD9, 0x89 }; -static const symbol s_45[] = { 0xD9, 0x8A }; -static const symbol s_46[] = { 0xD9, 0x84, 0xD8, 0xA7 }; -static const symbol s_47[] = { 0xD9, 0x84, 0xD8, 0xA3 }; -static const symbol s_48[] = { 0xD9, 0x84, 0xD8, 0xA5 }; -static const symbol s_49[] = { 0xD9, 0x84, 0xD8, 0xA2 }; -static const symbol s_50[] = { 0xD8, 0xA1 }; -static const symbol s_51[] = { 0xD8, 0xA7 }; -static const symbol s_52[] = { 0xD9, 0x88 }; -static const symbol s_53[] = { 0xD9, 0x8A }; -static const symbol s_54[] = { 0xD8, 0xA3 }; -static const symbol s_55[] = { 0xD8, 0xA2 }; -static const symbol s_56[] = { 0xD8, 0xA7 }; -static const symbol s_57[] = { 0xD8, 0xA5 }; -static const symbol s_58[] = { 0xD8, 0xA7 }; -static const symbol s_59[] = { 0xD8, 0xA8 }; -static const symbol s_60[] = { 0xD9, 0x83 }; -static const symbol s_61[] = { 0xD9, 0x8A }; -static const symbol s_62[] = { 0xD8, 0xAA }; -static const symbol s_63[] = { 0xD9, 0x86 }; -static const symbol s_64[] = { 0xD8, 0xA3 }; -static const symbol s_65[] = { 0xD8, 0xA7, 0xD8, 0xB3, 0xD8, 0xAA }; -static const symbol s_66[] = { 0xD9, 0x8A }; - static int r_Normalize_pre(struct SN_env * z) { int among_var; - { int c1 = z->c; - while(1) { - int c2 = z->c; - { int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + do { + int v_3 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 144); - if (!among_var) goto lab3; + among_var = find_among(z, a_0, 144, 0); + if (!among_var) goto lab2; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 2, s_10); + { + int ret = slice_from_s(z, 2, s_10); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 2, s_11); + { + int ret = slice_from_s(z, 2, s_11); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 2, s_12); + { + int ret = slice_from_s(z, 2, s_12); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 2, s_13); + { + int ret = slice_from_s(z, 2, s_13); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 2, s_14); + { + int ret = slice_from_s(z, 2, s_14); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 2, s_15); + { + int ret = slice_from_s(z, 2, s_15); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 2, s_16); + { + int ret = slice_from_s(z, 2, s_16); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 2, s_17); + { + int ret = slice_from_s(z, 2, s_17); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 2, s_18); + { + int ret = slice_from_s(z, 2, s_18); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 2, s_19); + { + int ret = slice_from_s(z, 2, s_19); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 2, s_20); + { + int ret = slice_from_s(z, 2, s_20); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 2, s_21); + { + int ret = slice_from_s(z, 2, s_21); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 2, s_22); + { + int ret = slice_from_s(z, 2, s_22); if (ret < 0) return ret; } break; case 25: - { int ret = slice_from_s(z, 2, s_23); + { + int ret = slice_from_s(z, 2, s_23); if (ret < 0) return ret; } break; case 26: - { int ret = slice_from_s(z, 2, s_24); + { + int ret = slice_from_s(z, 2, s_24); if (ret < 0) return ret; } break; case 27: - { int ret = slice_from_s(z, 2, s_25); + { + int ret = slice_from_s(z, 2, s_25); if (ret < 0) return ret; } break; case 28: - { int ret = slice_from_s(z, 2, s_26); + { + int ret = slice_from_s(z, 2, s_26); if (ret < 0) return ret; } break; case 29: - { int ret = slice_from_s(z, 2, s_27); + { + int ret = slice_from_s(z, 2, s_27); if (ret < 0) return ret; } break; case 30: - { int ret = slice_from_s(z, 2, s_28); + { + int ret = slice_from_s(z, 2, s_28); if (ret < 0) return ret; } break; case 31: - { int ret = slice_from_s(z, 2, s_29); + { + int ret = slice_from_s(z, 2, s_29); if (ret < 0) return ret; } break; case 32: - { int ret = slice_from_s(z, 2, s_30); + { + int ret = slice_from_s(z, 2, s_30); if (ret < 0) return ret; } break; case 33: - { int ret = slice_from_s(z, 2, s_31); + { + int ret = slice_from_s(z, 2, s_31); if (ret < 0) return ret; } break; case 34: - { int ret = slice_from_s(z, 2, s_32); + { + int ret = slice_from_s(z, 2, s_32); if (ret < 0) return ret; } break; case 35: - { int ret = slice_from_s(z, 2, s_33); + { + int ret = slice_from_s(z, 2, s_33); if (ret < 0) return ret; } break; case 36: - { int ret = slice_from_s(z, 2, s_34); + { + int ret = slice_from_s(z, 2, s_34); if (ret < 0) return ret; } break; case 37: - { int ret = slice_from_s(z, 2, s_35); + { + int ret = slice_from_s(z, 2, s_35); if (ret < 0) return ret; } break; case 38: - { int ret = slice_from_s(z, 2, s_36); + { + int ret = slice_from_s(z, 2, s_36); if (ret < 0) return ret; } break; case 39: - { int ret = slice_from_s(z, 2, s_37); + { + int ret = slice_from_s(z, 2, s_37); if (ret < 0) return ret; } break; case 40: - { int ret = slice_from_s(z, 2, s_38); + { + int ret = slice_from_s(z, 2, s_38); if (ret < 0) return ret; } break; case 41: - { int ret = slice_from_s(z, 2, s_39); + { + int ret = slice_from_s(z, 2, s_39); if (ret < 0) return ret; } break; case 42: - { int ret = slice_from_s(z, 2, s_40); + { + int ret = slice_from_s(z, 2, s_40); if (ret < 0) return ret; } break; case 43: - { int ret = slice_from_s(z, 2, s_41); + { + int ret = slice_from_s(z, 2, s_41); if (ret < 0) return ret; } break; case 44: - { int ret = slice_from_s(z, 2, s_42); + { + int ret = slice_from_s(z, 2, s_42); if (ret < 0) return ret; } break; case 45: - { int ret = slice_from_s(z, 2, s_43); + { + int ret = slice_from_s(z, 2, s_43); if (ret < 0) return ret; } break; case 46: - { int ret = slice_from_s(z, 2, s_44); + { + int ret = slice_from_s(z, 2, s_44); if (ret < 0) return ret; } break; case 47: - { int ret = slice_from_s(z, 2, s_45); + { + int ret = slice_from_s(z, 2, s_45); if (ret < 0) return ret; } break; case 48: - { int ret = slice_from_s(z, 4, s_46); + { + int ret = slice_from_s(z, 4, s_46); if (ret < 0) return ret; } break; case 49: - { int ret = slice_from_s(z, 4, s_47); + { + int ret = slice_from_s(z, 4, s_47); if (ret < 0) return ret; } break; case 50: - { int ret = slice_from_s(z, 4, s_48); + { + int ret = slice_from_s(z, 4, s_48); if (ret < 0) return ret; } break; case 51: - { int ret = slice_from_s(z, 4, s_49); + { + int ret = slice_from_s(z, 4, s_49); if (ret < 0) return ret; } break; } - goto lab2; - lab3: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + break; + lab2: + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } - } - lab2: + } while (0); continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } return 1; } static int r_Normalize_post(struct SN_env * z) { int among_var; - { int c1 = z->c; + { + int v_1 = z->c; z->lb = z->c; z->c = z->l; - z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 5 || !((124 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; - if (!find_among_b(z, a_1, 5)) goto lab0; + if (!find_among_b(z, a_1, 5, 0)) goto lab0; z->bra = z->c; - { int ret = slice_from_s(z, 2, s_50); + { + int ret = slice_from_s(z, 2, s_50); if (ret < 0) return ret; } z->c = z->lb; lab0: - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - while(1) { - int c3 = z->c; - { int c4 = z->c; + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; z->bra = z->c; - if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 5 || !((124 >> (z->p[z->c + 1] & 0x1f)) & 1)) goto lab4; - among_var = find_among(z, a_2, 5); - if (!among_var) goto lab4; + if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 5 || !((124 >> (z->p[z->c + 1] & 0x1f)) & 1)) goto lab3; + among_var = find_among(z, a_2, 5, 0); + if (!among_var) goto lab3; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_51); + { + int ret = slice_from_s(z, 2, s_51); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_52); + { + int ret = slice_from_s(z, 2, s_52); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_53); + { + int ret = slice_from_s(z, 2, s_53); if (ret < 0) return ret; } break; } - goto lab3; - lab4: - z->c = c4; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + break; + lab3: + z->c = v_4; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab2; z->c = ret; } - } - lab3: + } while (0); continue; lab2: - z->c = c3; + z->c = v_3; break; } - z->c = c2; + z->c = v_2; } return 1; } @@ -1023,21 +1017,21 @@ static int r_Checks1(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 3 >= z->l || (z->p[z->c + 3] != 132 && z->p[z->c + 3] != 167)) return 0; - among_var = find_among(z, a_3, 4); + among_var = find_among(z, a_3, 4, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: if (len_utf8(z->p) <= 4) return 0; - z->I[2] = 1; - z->I[1] = 0; - z->I[0] = 1; + ((SN_local *)z)->b_is_noun = 1; + ((SN_local *)z)->b_is_verb = 0; + ((SN_local *)z)->b_is_defined = 1; break; case 2: if (len_utf8(z->p) <= 3) return 0; - z->I[2] = 1; - z->I[1] = 0; - z->I[0] = 1; + ((SN_local *)z)->b_is_noun = 1; + ((SN_local *)z)->b_is_verb = 0; + ((SN_local *)z)->b_is_defined = 1; break; } return 1; @@ -1047,31 +1041,35 @@ static int r_Prefix_Step1(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 3 >= z->l || z->p[z->c + 3] >> 5 != 5 || !((188 >> (z->p[z->c + 3] & 0x1f)) & 1)) return 0; - among_var = find_among(z, a_4, 5); + among_var = find_among(z, a_4, 5, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_from_s(z, 2, s_54); + { + int ret = slice_from_s(z, 2, s_54); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_from_s(z, 2, s_55); + { + int ret = slice_from_s(z, 2, s_55); if (ret < 0) return ret; } break; case 3: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_from_s(z, 2, s_56); + { + int ret = slice_from_s(z, 2, s_56); if (ret < 0) return ret; } break; case 4: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_from_s(z, 2, s_57); + { + int ret = slice_from_s(z, 2, s_57); if (ret < 0) return ret; } break; @@ -1082,16 +1080,18 @@ static int r_Prefix_Step1(struct SN_env * z) { static int r_Prefix_Step2(struct SN_env * z) { z->bra = z->c; if (z->c + 1 >= z->l || (z->p[z->c + 1] != 129 && z->p[z->c + 1] != 136)) return 0; - if (!find_among(z, a_5, 2)) return 0; + if (!find_among(z, a_5, 2, 0)) return 0; z->ket = z->c; if (len_utf8(z->p) <= 3) return 0; - { int c1 = z->c; + { + int v_1 = z->c; if (!(eq_s(z, 2, s_58))) goto lab0; return 0; lab0: - z->c = c1; + z->c = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1101,19 +1101,21 @@ static int r_Prefix_Step3a_Noun(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 3 >= z->l || (z->p[z->c + 3] != 132 && z->p[z->c + 3] != 167)) return 0; - among_var = find_among(z, a_6, 4); + among_var = find_among(z, a_6, 4, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: if (len_utf8(z->p) <= 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) <= 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1125,25 +1127,28 @@ static int r_Prefix_Step3b_Noun(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 1 >= z->l || (z->p[z->c + 1] != 168 && z->p[z->c + 1] != 131)) return 0; - among_var = find_among(z, a_7, 4); + among_var = find_among(z, a_7, 4, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_from_s(z, 2, s_59); + { + int ret = slice_from_s(z, 2, s_59); if (ret < 0) return ret; } break; case 3: if (len_utf8(z->p) <= 3) return 0; - { int ret = slice_from_s(z, 2, s_60); + { + int ret = slice_from_s(z, 2, s_60); if (ret < 0) return ret; } break; @@ -1154,31 +1159,35 @@ static int r_Prefix_Step3b_Noun(struct SN_env * z) { static int r_Prefix_Step3_Verb(struct SN_env * z) { int among_var; z->bra = z->c; - among_var = find_among(z, a_8, 4); + among_var = find_among(z, a_8, 4, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: if (len_utf8(z->p) <= 4) return 0; - { int ret = slice_from_s(z, 2, s_61); + { + int ret = slice_from_s(z, 2, s_61); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) <= 4) return 0; - { int ret = slice_from_s(z, 2, s_62); + { + int ret = slice_from_s(z, 2, s_62); if (ret < 0) return ret; } break; case 3: if (len_utf8(z->p) <= 4) return 0; - { int ret = slice_from_s(z, 2, s_63); + { + int ret = slice_from_s(z, 2, s_63); if (ret < 0) return ret; } break; case 4: if (len_utf8(z->p) <= 4) return 0; - { int ret = slice_from_s(z, 2, s_64); + { + int ret = slice_from_s(z, 2, s_64); if (ret < 0) return ret; } break; @@ -1189,12 +1198,13 @@ static int r_Prefix_Step3_Verb(struct SN_env * z) { static int r_Prefix_Step4_Verb(struct SN_env * z) { z->bra = z->c; if (z->c + 5 >= z->l || z->p[z->c + 5] != 170) return 0; - if (!find_among(z, a_9, 3)) return 0; + if (!find_among(z, a_9, 3, 0)) return 0; z->ket = z->c; if (len_utf8(z->p) <= 4) return 0; - z->I[1] = 1; - z->I[2] = 0; - { int ret = slice_from_s(z, 6, s_65); + ((SN_local *)z)->b_is_verb = 1; + ((SN_local *)z)->b_is_noun = 0; + { + int ret = slice_from_s(z, 6, s_65); if (ret < 0) return ret; } return 1; @@ -1203,25 +1213,28 @@ static int r_Prefix_Step4_Verb(struct SN_env * z) { static int r_Suffix_Noun_Step1a(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_10, 10); + among_var = find_among_b(z, a_10, 10, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: if (len_utf8(z->p) < 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) < 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: if (len_utf8(z->p) < 6) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1231,11 +1244,11 @@ static int r_Suffix_Noun_Step1a(struct SN_env * z) { static int r_Suffix_Noun_Step1b(struct SN_env * z) { z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 134) return 0; - if (!find_among_b(z, a_11, 1)) return 0; + if (!(eq_s_b(z, 2, s_66))) return 0; z->bra = z->c; if (len_utf8(z->p) <= 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1243,10 +1256,11 @@ static int r_Suffix_Noun_Step1b(struct SN_env * z) { static int r_Suffix_Noun_Step2a(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_12, 3)) return 0; + if (!find_among_b(z, a_11, 3, 0)) return 0; z->bra = z->c; if (len_utf8(z->p) <= 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1254,11 +1268,11 @@ static int r_Suffix_Noun_Step2a(struct SN_env * z) { static int r_Suffix_Noun_Step2b(struct SN_env * z) { z->ket = z->c; - if (z->c - 3 <= z->lb || z->p[z->c - 1] != 170) return 0; - if (!find_among_b(z, a_13, 1)) return 0; + if (!(eq_s_b(z, 4, s_67))) return 0; z->bra = z->c; if (len_utf8(z->p) < 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1266,11 +1280,11 @@ static int r_Suffix_Noun_Step2b(struct SN_env * z) { static int r_Suffix_Noun_Step2c1(struct SN_env * z) { z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 170) return 0; - if (!find_among_b(z, a_14, 1)) return 0; + if (!(eq_s_b(z, 2, s_68))) return 0; z->bra = z->c; if (len_utf8(z->p) < 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1278,11 +1292,11 @@ static int r_Suffix_Noun_Step2c1(struct SN_env * z) { static int r_Suffix_Noun_Step2c2(struct SN_env * z) { z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 169) return 0; - if (!find_among_b(z, a_15, 1)) return 0; + if (!(eq_s_b(z, 2, s_69))) return 0; z->bra = z->c; if (len_utf8(z->p) < 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1290,11 +1304,11 @@ static int r_Suffix_Noun_Step2c2(struct SN_env * z) { static int r_Suffix_Noun_Step3(struct SN_env * z) { z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 138) return 0; - if (!find_among_b(z, a_16, 1)) return 0; + if (!(eq_s_b(z, 2, s_70))) return 0; z->bra = z->c; if (len_utf8(z->p) < 3) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1303,25 +1317,28 @@ static int r_Suffix_Noun_Step3(struct SN_env * z) { static int r_Suffix_Verb_Step1(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_17, 12); + among_var = find_among_b(z, a_12, 12, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: if (len_utf8(z->p) < 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) < 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: if (len_utf8(z->p) < 6) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1332,31 +1349,35 @@ static int r_Suffix_Verb_Step1(struct SN_env * z) { static int r_Suffix_Verb_Step2a(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_18, 11); + among_var = find_among_b(z, a_13, 11, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: if (len_utf8(z->p) < 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) < 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: if (len_utf8(z->p) <= 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 4: if (len_utf8(z->p) < 6) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1367,10 +1388,11 @@ static int r_Suffix_Verb_Step2a(struct SN_env * z) { static int r_Suffix_Verb_Step2b(struct SN_env * z) { z->ket = z->c; if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 133 && z->p[z->c - 1] != 167)) return 0; - if (!find_among_b(z, a_19, 2)) return 0; + if (!find_among_b(z, a_14, 2, 0)) return 0; z->bra = z->c; if (len_utf8(z->p) < 5) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1380,19 +1402,21 @@ static int r_Suffix_Verb_Step2c(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] != 136) return 0; - among_var = find_among_b(z, a_20, 2); + among_var = find_among_b(z, a_15, 2, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: if (len_utf8(z->p) < 4) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (len_utf8(z->p) < 6) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1402,263 +1426,301 @@ static int r_Suffix_Verb_Step2c(struct SN_env * z) { static int r_Suffix_All_alef_maqsura(struct SN_env * z) { z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 137) return 0; - if (!find_among_b(z, a_21, 1)) return 0; + if (!(eq_s_b(z, 2, s_71))) return 0; z->bra = z->c; - { int ret = slice_from_s(z, 2, s_66); + { + int ret = slice_from_s(z, 2, s_72); if (ret < 0) return ret; } return 1; } extern int arabic_UTF_8_stem(struct SN_env * z) { - z->I[2] = 1; - z->I[1] = 1; - z->I[0] = 0; - { int c1 = z->c; - { int ret = r_Checks1(z); + ((SN_local *)z)->b_is_noun = 1; + ((SN_local *)z)->b_is_verb = 1; + ((SN_local *)z)->b_is_defined = 0; + { + int v_1 = z->c; + { + int ret = r_Checks1(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - - { int ret = r_Normalize_pre(z); + { + int ret = r_Normalize_pre(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - if (!(z->I[1])) goto lab2; - { int m4 = z->l - z->c; (void)m4; - { int i = 1; - while(1) { - int m5 = z->l - z->c; (void)m5; - { int ret = r_Suffix_Verb_Step1(z); - if (ret == 0) goto lab5; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + if (!((SN_local *)z)->b_is_verb) goto lab1; + do { + int v_4 = z->l - z->c; + { + int v_5 = 1; + while (1) { + int v_6 = z->l - z->c; + { + int ret = r_Suffix_Verb_Step1(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - i--; + v_5--; continue; - lab5: - z->c = z->l - m5; + lab3: + z->c = z->l - v_6; break; } - if (i > 0) goto lab4; + if (v_5 > 0) goto lab2; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_Suffix_Verb_Step2a(z); - if (ret == 0) goto lab7; + do { + int v_7 = z->l - z->c; + { + int ret = r_Suffix_Verb_Step2a(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } - goto lab6; - lab7: - z->c = z->l - m6; - { int ret = r_Suffix_Verb_Step2c(z); - if (ret == 0) goto lab8; + break; + lab4: + z->c = z->l - v_7; + { + int ret = r_Suffix_Verb_Step2c(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } - goto lab6; - lab8: - z->c = z->l - m6; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) goto lab4; + break; + lab5: + z->c = z->l - v_7; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab2; z->c = ret; } - } - lab6: - goto lab3; - lab4: - z->c = z->l - m4; - { int ret = r_Suffix_Verb_Step2b(z); - if (ret == 0) goto lab9; + } while (0); + break; + lab2: + z->c = z->l - v_4; + { + int ret = r_Suffix_Verb_Step2b(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - goto lab3; - lab9: - z->c = z->l - m4; - { int ret = r_Suffix_Verb_Step2a(z); - if (ret == 0) goto lab2; + break; + lab6: + z->c = z->l - v_4; + { + int ret = r_Suffix_Verb_Step2a(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - } - lab3: - goto lab1; - lab2: - z->c = z->l - m3; - if (!(z->I[2])) goto lab10; - { int m7 = z->l - z->c; (void)m7; - { int m8 = z->l - z->c; (void)m8; - { int ret = r_Suffix_Noun_Step2c2(z); - if (ret == 0) goto lab13; + } while (0); + break; + lab1: + z->c = z->l - v_3; + if (!((SN_local *)z)->b_is_noun) goto lab7; + { + int v_8 = z->l - z->c; + do { + int v_9 = z->l - z->c; + { + int ret = r_Suffix_Noun_Step2c2(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - goto lab12; - lab13: - z->c = z->l - m8; - - if (!(z->I[0])) goto lab15; - goto lab14; - lab15: - { int ret = r_Suffix_Noun_Step1a(z); - if (ret == 0) goto lab14; + break; + lab9: + z->c = z->l - v_9; + if (((SN_local *)z)->b_is_defined) goto lab10; + { + int ret = r_Suffix_Noun_Step1a(z); + if (ret == 0) goto lab10; if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_Suffix_Noun_Step2a(z); - if (ret == 0) goto lab17; + do { + int v_10 = z->l - z->c; + { + int ret = r_Suffix_Noun_Step2a(z); + if (ret == 0) goto lab11; if (ret < 0) return ret; } - goto lab16; - lab17: - z->c = z->l - m9; - { int ret = r_Suffix_Noun_Step2b(z); - if (ret == 0) goto lab18; + break; + lab11: + z->c = z->l - v_10; + { + int ret = r_Suffix_Noun_Step2b(z); + if (ret == 0) goto lab12; if (ret < 0) return ret; } - goto lab16; - lab18: - z->c = z->l - m9; - { int ret = r_Suffix_Noun_Step2c1(z); - if (ret == 0) goto lab19; + break; + lab12: + z->c = z->l - v_10; + { + int ret = r_Suffix_Noun_Step2c1(z); + if (ret == 0) goto lab13; if (ret < 0) return ret; } - goto lab16; - lab19: - z->c = z->l - m9; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) goto lab14; + break; + lab13: + z->c = z->l - v_10; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab10; z->c = ret; } - } - lab16: - goto lab12; - lab14: - z->c = z->l - m8; - { int ret = r_Suffix_Noun_Step1b(z); - if (ret == 0) goto lab20; + } while (0); + break; + lab10: + z->c = z->l - v_9; + { + int ret = r_Suffix_Noun_Step1b(z); + if (ret == 0) goto lab14; if (ret < 0) return ret; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_Suffix_Noun_Step2a(z); - if (ret == 0) goto lab22; + do { + int v_11 = z->l - z->c; + { + int ret = r_Suffix_Noun_Step2a(z); + if (ret == 0) goto lab15; if (ret < 0) return ret; } - goto lab21; - lab22: - z->c = z->l - m10; - { int ret = r_Suffix_Noun_Step2b(z); - if (ret == 0) goto lab23; + break; + lab15: + z->c = z->l - v_11; + { + int ret = r_Suffix_Noun_Step2b(z); + if (ret == 0) goto lab16; if (ret < 0) return ret; } - goto lab21; - lab23: - z->c = z->l - m10; - { int ret = r_Suffix_Noun_Step2c1(z); - if (ret == 0) goto lab20; + break; + lab16: + z->c = z->l - v_11; + { + int ret = r_Suffix_Noun_Step2c1(z); + if (ret == 0) goto lab14; if (ret < 0) return ret; } - } - lab21: - goto lab12; - lab20: - z->c = z->l - m8; - - if (!(z->I[0])) goto lab25; - goto lab24; - lab25: - { int ret = r_Suffix_Noun_Step2a(z); - if (ret == 0) goto lab24; + } while (0); + break; + lab14: + z->c = z->l - v_9; + if (((SN_local *)z)->b_is_defined) goto lab17; + { + int ret = r_Suffix_Noun_Step2a(z); + if (ret == 0) goto lab17; if (ret < 0) return ret; } - goto lab12; - lab24: - z->c = z->l - m8; - { int ret = r_Suffix_Noun_Step2b(z); - if (ret == 0) { z->c = z->l - m7; goto lab11; } + break; + lab17: + z->c = z->l - v_9; + { + int ret = r_Suffix_Noun_Step2b(z); + if (ret == 0) { z->c = z->l - v_8; goto lab8; } if (ret < 0) return ret; } - } - lab12: - lab11: + } while (0); + lab8: ; } - { int ret = r_Suffix_Noun_Step3(z); - if (ret == 0) goto lab10; + { + int ret = r_Suffix_Noun_Step3(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } - goto lab1; - lab10: - z->c = z->l - m3; - { int ret = r_Suffix_All_alef_maqsura(z); + break; + lab7: + z->c = z->l - v_3; + { + int ret = r_Suffix_All_alef_maqsura(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } z->c = z->lb; - { int c11 = z->c; - { int c12 = z->c; - { int ret = r_Prefix_Step1(z); - if (ret == 0) { z->c = c12; goto lab27; } + { + int v_12 = z->c; + { + int v_13 = z->c; + { + int ret = r_Prefix_Step1(z); + if (ret == 0) { z->c = v_13; goto lab19; } if (ret < 0) return ret; } - lab27: + lab19: ; } - { int c13 = z->c; - { int ret = r_Prefix_Step2(z); - if (ret == 0) { z->c = c13; goto lab28; } + { + int v_14 = z->c; + { + int ret = r_Prefix_Step2(z); + if (ret == 0) { z->c = v_14; goto lab20; } if (ret < 0) return ret; } - lab28: + lab20: ; } - { int c14 = z->c; - { int ret = r_Prefix_Step3a_Noun(z); - if (ret == 0) goto lab30; + do { + int v_15 = z->c; + { + int ret = r_Prefix_Step3a_Noun(z); + if (ret == 0) goto lab21; if (ret < 0) return ret; } - goto lab29; - lab30: - z->c = c14; - if (!(z->I[2])) goto lab31; - { int ret = r_Prefix_Step3b_Noun(z); - if (ret == 0) goto lab31; + break; + lab21: + z->c = v_15; + if (!((SN_local *)z)->b_is_noun) goto lab22; + { + int ret = r_Prefix_Step3b_Noun(z); + if (ret == 0) goto lab22; if (ret < 0) return ret; } - goto lab29; - lab31: - z->c = c14; - if (!(z->I[1])) goto lab26; - { int c15 = z->c; - { int ret = r_Prefix_Step3_Verb(z); - if (ret == 0) { z->c = c15; goto lab32; } + break; + lab22: + z->c = v_15; + if (!((SN_local *)z)->b_is_verb) goto lab18; + { + int v_16 = z->c; + { + int ret = r_Prefix_Step3_Verb(z); + if (ret == 0) { z->c = v_16; goto lab23; } if (ret < 0) return ret; } - lab32: + lab23: ; } - { int ret = r_Prefix_Step4_Verb(z); - if (ret == 0) goto lab26; + { + int ret = r_Prefix_Step4_Verb(z); + if (ret == 0) goto lab18; if (ret < 0) return ret; } - } - lab29: - lab26: - z->c = c11; + } while (0); + lab18: + z->c = v_12; } - - { int ret = r_Normalize_post(z); + { + int ret = r_Normalize_post(z); if (ret < 0) return ret; } return 1; } -extern struct SN_env * arabic_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * arabic_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_is_defined = 0; + ((SN_local *)z)->b_is_verb = 0; + ((SN_local *)z)->b_is_noun = 0; + } + return z; +} -extern void arabic_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void arabic_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_armenian.c b/src/backend/snowball/libstemmer/stem_UTF_8_armenian.c index 81ec52fef2b2e..0ec4250bdc312 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_armenian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_armenian.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from armenian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_armenian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,24 +21,15 @@ extern int armenian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_ending(struct SN_env * z); static int r_noun(struct SN_env * z); static int r_verb(struct SN_env * z); static int r_adjective(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif -extern struct SN_env * armenian_UTF_8_create_env(void); -extern void armenian_UTF_8_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif static const symbol s_0_0[6] = { 0xD5, 0xA2, 0xD5, 0xA1, 0xD6, 0x80 }; static const symbol s_0_1[8] = { 0xD6, 0x80, 0xD5, 0xB8, 0xD6, 0x80, 0xD5, 0xA4 }; static const symbol s_0_2[10] = { 0xD5, 0xA5, 0xD6, 0x80, 0xD5, 0xB8, 0xD6, 0x80, 0xD5, 0xA4 }; @@ -50,32 +53,30 @@ static const symbol s_0_19[4] = { 0xD5, 0xAB, 0xD5, 0xBE }; static const symbol s_0_20[4] = { 0xD5, 0xA1, 0xD5, 0xBF }; static const symbol s_0_21[8] = { 0xD5, 0xA1, 0xD5, 0xBE, 0xD5, 0xA5, 0xD5, 0xBF }; static const symbol s_0_22[6] = { 0xD5, 0xAF, 0xD5, 0xB8, 0xD5, 0xBF }; - -static const struct among a_0[23] = -{ -{ 6, s_0_0, -1, 1, 0}, -{ 8, s_0_1, -1, 1, 0}, -{ 10, s_0_2, 1, 1, 0}, -{ 6, s_0_3, -1, 1, 0}, -{ 6, s_0_4, -1, 1, 0}, -{ 8, s_0_5, -1, 1, 0}, -{ 4, s_0_6, -1, 1, 0}, -{ 8, s_0_7, -1, 1, 0}, -{ 8, s_0_8, -1, 1, 0}, -{ 8, s_0_9, -1, 1, 0}, -{ 4, s_0_10, -1, 1, 0}, -{ 8, s_0_11, 10, 1, 0}, -{ 8, s_0_12, 10, 1, 0}, -{ 8, s_0_13, -1, 1, 0}, -{ 4, s_0_14, -1, 1, 0}, -{ 6, s_0_15, 14, 1, 0}, -{ 8, s_0_16, 14, 1, 0}, -{ 8, s_0_17, -1, 1, 0}, -{ 6, s_0_18, -1, 1, 0}, -{ 4, s_0_19, -1, 1, 0}, -{ 4, s_0_20, -1, 1, 0}, -{ 8, s_0_21, -1, 1, 0}, -{ 6, s_0_22, -1, 1, 0} +static const struct among a_0[23] = { +{ 6, s_0_0, 0, 1, 0}, +{ 8, s_0_1, 0, 1, 0}, +{ 10, s_0_2, -1, 1, 0}, +{ 6, s_0_3, 0, 1, 0}, +{ 6, s_0_4, 0, 1, 0}, +{ 8, s_0_5, 0, 1, 0}, +{ 4, s_0_6, 0, 1, 0}, +{ 8, s_0_7, 0, 1, 0}, +{ 8, s_0_8, 0, 1, 0}, +{ 8, s_0_9, 0, 1, 0}, +{ 4, s_0_10, 0, 1, 0}, +{ 8, s_0_11, -1, 1, 0}, +{ 8, s_0_12, -2, 1, 0}, +{ 8, s_0_13, 0, 1, 0}, +{ 4, s_0_14, 0, 1, 0}, +{ 6, s_0_15, -1, 1, 0}, +{ 8, s_0_16, -2, 1, 0}, +{ 8, s_0_17, 0, 1, 0}, +{ 6, s_0_18, 0, 1, 0}, +{ 4, s_0_19, 0, 1, 0}, +{ 4, s_0_20, 0, 1, 0}, +{ 8, s_0_21, 0, 1, 0}, +{ 6, s_0_22, 0, 1, 0} }; static const symbol s_1_0[4] = { 0xD5, 0xA1, 0xD6, 0x80 }; @@ -149,80 +150,78 @@ static const symbol s_1_67[8] = { 0xD5, 0xA1, 0xD6, 0x81, 0xD5, 0xA1, 0xD5, 0xBE static const symbol s_1_68[8] = { 0xD5, 0xA5, 0xD6, 0x81, 0xD5, 0xA1, 0xD5, 0xBE }; static const symbol s_1_69[8] = { 0xD5, 0xA1, 0xD5, 0xAC, 0xD5, 0xB8, 0xD5, 0xBE }; static const symbol s_1_70[8] = { 0xD5, 0xA5, 0xD5, 0xAC, 0xD5, 0xB8, 0xD5, 0xBE }; - -static const struct among a_1[71] = -{ -{ 4, s_1_0, -1, 1, 0}, -{ 8, s_1_1, 0, 1, 0}, -{ 8, s_1_2, 0, 1, 0}, -{ 10, s_1_3, -1, 1, 0}, -{ 8, s_1_4, -1, 1, 0}, -{ 8, s_1_5, -1, 1, 0}, -{ 10, s_1_6, 5, 1, 0}, -{ 10, s_1_7, -1, 1, 0}, -{ 10, s_1_8, -1, 1, 0}, -{ 4, s_1_9, -1, 1, 0}, -{ 4, s_1_10, -1, 1, 0}, -{ 10, s_1_11, 10, 1, 0}, -{ 8, s_1_12, -1, 1, 0}, -{ 8, s_1_13, -1, 1, 0}, -{ 4, s_1_14, -1, 1, 0}, -{ 6, s_1_15, 14, 1, 0}, -{ 8, s_1_16, 15, 1, 0}, -{ 10, s_1_17, -1, 1, 0}, -{ 8, s_1_18, -1, 1, 0}, -{ 8, s_1_19, -1, 1, 0}, -{ 10, s_1_20, 19, 1, 0}, -{ 6, s_1_21, -1, 1, 0}, -{ 8, s_1_22, 21, 1, 0}, -{ 10, s_1_23, 22, 1, 0}, -{ 12, s_1_24, -1, 1, 0}, -{ 10, s_1_25, -1, 1, 0}, -{ 10, s_1_26, -1, 1, 0}, -{ 12, s_1_27, 26, 1, 0}, -{ 2, s_1_28, -1, 1, 0}, -{ 6, s_1_29, 28, 1, 0}, -{ 6, s_1_30, 28, 1, 0}, -{ 4, s_1_31, -1, 1, 0}, -{ 8, s_1_32, -1, 1, 0}, -{ 6, s_1_33, -1, 1, 0}, -{ 6, s_1_34, -1, 1, 0}, -{ 8, s_1_35, 34, 1, 0}, -{ 4, s_1_36, -1, 1, 0}, -{ 6, s_1_37, 36, 1, 0}, -{ 10, s_1_38, 36, 1, 0}, -{ 8, s_1_39, 36, 1, 0}, -{ 8, s_1_40, 36, 1, 0}, -{ 4, s_1_41, -1, 1, 0}, -{ 6, s_1_42, 41, 1, 0}, -{ 6, s_1_43, 41, 1, 0}, -{ 8, s_1_44, 43, 1, 0}, -{ 10, s_1_45, 44, 1, 0}, -{ 6, s_1_46, 41, 1, 0}, -{ 6, s_1_47, 41, 1, 0}, -{ 10, s_1_48, 47, 1, 0}, -{ 10, s_1_49, 47, 1, 0}, -{ 6, s_1_50, 41, 1, 0}, -{ 8, s_1_51, 50, 1, 0}, -{ 8, s_1_52, 50, 1, 0}, -{ 10, s_1_53, 52, 1, 0}, -{ 6, s_1_54, -1, 1, 0}, -{ 6, s_1_55, -1, 1, 0}, -{ 8, s_1_56, 55, 1, 0}, -{ 4, s_1_57, -1, 1, 0}, -{ 6, s_1_58, 57, 1, 0}, -{ 8, s_1_59, 58, 1, 0}, -{ 10, s_1_60, -1, 1, 0}, -{ 8, s_1_61, -1, 1, 0}, -{ 8, s_1_62, -1, 1, 0}, -{ 10, s_1_63, 62, 1, 0}, -{ 8, s_1_64, -1, 1, 0}, -{ 8, s_1_65, -1, 1, 0}, -{ 4, s_1_66, -1, 1, 0}, -{ 8, s_1_67, 66, 1, 0}, -{ 8, s_1_68, 66, 1, 0}, -{ 8, s_1_69, -1, 1, 0}, -{ 8, s_1_70, -1, 1, 0} +static const struct among a_1[71] = { +{ 4, s_1_0, 0, 1, 0}, +{ 8, s_1_1, -1, 1, 0}, +{ 8, s_1_2, -2, 1, 0}, +{ 10, s_1_3, 0, 1, 0}, +{ 8, s_1_4, 0, 1, 0}, +{ 8, s_1_5, 0, 1, 0}, +{ 10, s_1_6, -1, 1, 0}, +{ 10, s_1_7, 0, 1, 0}, +{ 10, s_1_8, 0, 1, 0}, +{ 4, s_1_9, 0, 1, 0}, +{ 4, s_1_10, 0, 1, 0}, +{ 10, s_1_11, -1, 1, 0}, +{ 8, s_1_12, 0, 1, 0}, +{ 8, s_1_13, 0, 1, 0}, +{ 4, s_1_14, 0, 1, 0}, +{ 6, s_1_15, -1, 1, 0}, +{ 8, s_1_16, -1, 1, 0}, +{ 10, s_1_17, 0, 1, 0}, +{ 8, s_1_18, 0, 1, 0}, +{ 8, s_1_19, 0, 1, 0}, +{ 10, s_1_20, -1, 1, 0}, +{ 6, s_1_21, 0, 1, 0}, +{ 8, s_1_22, -1, 1, 0}, +{ 10, s_1_23, -1, 1, 0}, +{ 12, s_1_24, 0, 1, 0}, +{ 10, s_1_25, 0, 1, 0}, +{ 10, s_1_26, 0, 1, 0}, +{ 12, s_1_27, -1, 1, 0}, +{ 2, s_1_28, 0, 1, 0}, +{ 6, s_1_29, -1, 1, 0}, +{ 6, s_1_30, -2, 1, 0}, +{ 4, s_1_31, 0, 1, 0}, +{ 8, s_1_32, 0, 1, 0}, +{ 6, s_1_33, 0, 1, 0}, +{ 6, s_1_34, 0, 1, 0}, +{ 8, s_1_35, -1, 1, 0}, +{ 4, s_1_36, 0, 1, 0}, +{ 6, s_1_37, -1, 1, 0}, +{ 10, s_1_38, -2, 1, 0}, +{ 8, s_1_39, -3, 1, 0}, +{ 8, s_1_40, -4, 1, 0}, +{ 4, s_1_41, 0, 1, 0}, +{ 6, s_1_42, -1, 1, 0}, +{ 6, s_1_43, -2, 1, 0}, +{ 8, s_1_44, -1, 1, 0}, +{ 10, s_1_45, -1, 1, 0}, +{ 6, s_1_46, -5, 1, 0}, +{ 6, s_1_47, -6, 1, 0}, +{ 10, s_1_48, -1, 1, 0}, +{ 10, s_1_49, -2, 1, 0}, +{ 6, s_1_50, -9, 1, 0}, +{ 8, s_1_51, -1, 1, 0}, +{ 8, s_1_52, -2, 1, 0}, +{ 10, s_1_53, -1, 1, 0}, +{ 6, s_1_54, 0, 1, 0}, +{ 6, s_1_55, 0, 1, 0}, +{ 8, s_1_56, -1, 1, 0}, +{ 4, s_1_57, 0, 1, 0}, +{ 6, s_1_58, -1, 1, 0}, +{ 8, s_1_59, -1, 1, 0}, +{ 10, s_1_60, 0, 1, 0}, +{ 8, s_1_61, 0, 1, 0}, +{ 8, s_1_62, 0, 1, 0}, +{ 10, s_1_63, -1, 1, 0}, +{ 8, s_1_64, 0, 1, 0}, +{ 8, s_1_65, 0, 1, 0}, +{ 4, s_1_66, 0, 1, 0}, +{ 8, s_1_67, -1, 1, 0}, +{ 8, s_1_68, -2, 1, 0}, +{ 8, s_1_69, 0, 1, 0}, +{ 8, s_1_70, 0, 1, 0} }; static const symbol s_2_0[6] = { 0xD5, 0xA3, 0xD5, 0xA1, 0xD6, 0x80 }; @@ -265,49 +264,47 @@ static const symbol s_2_36[6] = { 0xD5, 0xA1, 0xD5, 0xAE, 0xD5, 0xB8 }; static const symbol s_2_37[4] = { 0xD5, 0xAB, 0xD5, 0xB9 }; static const symbol s_2_38[6] = { 0xD5, 0xB8, 0xD6, 0x82, 0xD5, 0xBD }; static const symbol s_2_39[8] = { 0xD5, 0xB8, 0xD6, 0x82, 0xD5, 0xBD, 0xD5, 0xBF }; - -static const struct among a_2[40] = -{ -{ 6, s_2_0, -1, 1, 0}, -{ 6, s_2_1, -1, 1, 0}, -{ 8, s_2_2, 1, 1, 0}, -{ 8, s_2_3, -1, 1, 0}, -{ 4, s_2_4, -1, 1, 0}, -{ 4, s_2_5, -1, 1, 0}, -{ 2, s_2_6, -1, 1, 0}, -{ 6, s_2_7, 6, 1, 0}, -{ 6, s_2_8, 6, 1, 0}, -{ 4, s_2_9, 6, 1, 0}, -{ 8, s_2_10, 9, 1, 0}, -{ 8, s_2_11, 9, 1, 0}, -{ 8, s_2_12, 6, 1, 0}, -{ 8, s_2_13, 6, 1, 0}, -{ 8, s_2_14, 6, 1, 0}, -{ 10, s_2_15, 14, 1, 0}, -{ 6, s_2_16, 6, 1, 0}, -{ 6, s_2_17, 6, 1, 0}, -{ 6, s_2_18, 6, 1, 0}, -{ 6, s_2_19, -1, 1, 0}, -{ 8, s_2_20, -1, 1, 0}, -{ 4, s_2_21, -1, 1, 0}, -{ 8, s_2_22, -1, 1, 0}, -{ 4, s_2_23, -1, 1, 0}, -{ 6, s_2_24, -1, 1, 0}, -{ 4, s_2_25, -1, 1, 0}, -{ 6, s_2_26, 25, 1, 0}, -{ 8, s_2_27, 25, 1, 0}, -{ 4, s_2_28, -1, 1, 0}, -{ 8, s_2_29, -1, 1, 0}, -{ 14, s_2_30, 29, 1, 0}, -{ 4, s_2_31, -1, 1, 0}, -{ 8, s_2_32, 31, 1, 0}, -{ 6, s_2_33, 31, 1, 0}, -{ 8, s_2_34, 31, 1, 0}, -{ 8, s_2_35, -1, 1, 0}, -{ 6, s_2_36, -1, 1, 0}, -{ 4, s_2_37, -1, 1, 0}, -{ 6, s_2_38, -1, 1, 0}, -{ 8, s_2_39, -1, 1, 0} +static const struct among a_2[40] = { +{ 6, s_2_0, 0, 1, 0}, +{ 6, s_2_1, 0, 1, 0}, +{ 8, s_2_2, -1, 1, 0}, +{ 8, s_2_3, 0, 1, 0}, +{ 4, s_2_4, 0, 1, 0}, +{ 4, s_2_5, 0, 1, 0}, +{ 2, s_2_6, 0, 1, 0}, +{ 6, s_2_7, -1, 1, 0}, +{ 6, s_2_8, -2, 1, 0}, +{ 4, s_2_9, -3, 1, 0}, +{ 8, s_2_10, -1, 1, 0}, +{ 8, s_2_11, -2, 1, 0}, +{ 8, s_2_12, -6, 1, 0}, +{ 8, s_2_13, -7, 1, 0}, +{ 8, s_2_14, -8, 1, 0}, +{ 10, s_2_15, -1, 1, 0}, +{ 6, s_2_16, -10, 1, 0}, +{ 6, s_2_17, -11, 1, 0}, +{ 6, s_2_18, -12, 1, 0}, +{ 6, s_2_19, 0, 1, 0}, +{ 8, s_2_20, 0, 1, 0}, +{ 4, s_2_21, 0, 1, 0}, +{ 8, s_2_22, 0, 1, 0}, +{ 4, s_2_23, 0, 1, 0}, +{ 6, s_2_24, 0, 1, 0}, +{ 4, s_2_25, 0, 1, 0}, +{ 6, s_2_26, -1, 1, 0}, +{ 8, s_2_27, -2, 1, 0}, +{ 4, s_2_28, 0, 1, 0}, +{ 8, s_2_29, 0, 1, 0}, +{ 14, s_2_30, -1, 1, 0}, +{ 4, s_2_31, 0, 1, 0}, +{ 8, s_2_32, -1, 1, 0}, +{ 6, s_2_33, -2, 1, 0}, +{ 8, s_2_34, -3, 1, 0}, +{ 8, s_2_35, 0, 1, 0}, +{ 6, s_2_36, 0, 1, 0}, +{ 4, s_2_37, 0, 1, 0}, +{ 6, s_2_38, 0, 1, 0}, +{ 8, s_2_39, 0, 1, 0} }; static const symbol s_3_0[4] = { 0xD5, 0xA5, 0xD6, 0x80 }; @@ -367,116 +364,111 @@ static const symbol s_3_53[8] = { 0xD5, 0xA5, 0xD6, 0x80, 0xD5, 0xB8, 0xD5, 0xBE static const symbol s_3_54[10] = { 0xD5, 0xB6, 0xD5, 0xA5, 0xD6, 0x80, 0xD5, 0xB8, 0xD5, 0xBE }; static const symbol s_3_55[8] = { 0xD5, 0xA1, 0xD5, 0xB6, 0xD5, 0xB8, 0xD5, 0xBE }; static const symbol s_3_56[6] = { 0xD5, 0xBE, 0xD5, 0xB8, 0xD5, 0xBE }; - -static const struct among a_3[57] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 6, s_3_1, 0, 1, 0}, -{ 2, s_3_2, -1, 1, 0}, -{ 6, s_3_3, 2, 1, 0}, -{ 4, s_3_4, 2, 1, 0}, -{ 8, s_3_5, 4, 1, 0}, -{ 10, s_3_6, 5, 1, 0}, -{ 6, s_3_7, 4, 1, 0}, -{ 10, s_3_8, 4, 1, 0}, -{ 8, s_3_9, 4, 1, 0}, -{ 6, s_3_10, 4, 1, 0}, -{ 4, s_3_11, 2, 1, 0}, -{ 4, s_3_12, -1, 1, 0}, -{ 4, s_3_13, -1, 1, 0}, -{ 6, s_3_14, -1, 1, 0}, -{ 2, s_3_15, -1, 1, 0}, -{ 6, s_3_16, 15, 1, 0}, -{ 8, s_3_17, 16, 1, 0}, -{ 6, s_3_18, 15, 1, 0}, -{ 6, s_3_19, 15, 1, 0}, -{ 14, s_3_20, 19, 1, 0}, -{ 8, s_3_21, 19, 1, 0}, -{ 6, s_3_22, 15, 1, 0}, -{ 2, s_3_23, -1, 1, 0}, -{ 6, s_3_24, 23, 1, 0}, -{ 8, s_3_25, 24, 1, 0}, -{ 6, s_3_26, 23, 1, 0}, -{ 14, s_3_27, 26, 1, 0}, -{ 8, s_3_28, 26, 1, 0}, -{ 6, s_3_29, 23, 1, 0}, -{ 2, s_3_30, -1, 1, 0}, -{ 6, s_3_31, 30, 1, 0}, -{ 8, s_3_32, 31, 1, 0}, -{ 4, s_3_33, 30, 1, 0}, -{ 10, s_3_34, -1, 1, 0}, -{ 12, s_3_35, 34, 1, 0}, -{ 10, s_3_36, -1, 1, 0}, -{ 2, s_3_37, -1, 1, 0}, -{ 6, s_3_38, 37, 1, 0}, -{ 8, s_3_39, 38, 1, 0}, -{ 6, s_3_40, 37, 1, 0}, -{ 4, s_3_41, 37, 1, 0}, -{ 12, s_3_42, 41, 1, 0}, -{ 6, s_3_43, 41, 1, 0}, -{ 4, s_3_44, 37, 1, 0}, -{ 8, s_3_45, 44, 1, 0}, -{ 10, s_3_46, 45, 1, 0}, -{ 14, s_3_47, 37, 1, 0}, -{ 4, s_3_48, -1, 1, 0}, -{ 14, s_3_49, -1, 1, 0}, -{ 8, s_3_50, -1, 1, 0}, -{ 6, s_3_51, -1, 1, 0}, -{ 4, s_3_52, -1, 1, 0}, -{ 8, s_3_53, 52, 1, 0}, -{ 10, s_3_54, 53, 1, 0}, -{ 8, s_3_55, 52, 1, 0}, -{ 6, s_3_56, 52, 1, 0} +static const struct among a_3[57] = { +{ 4, s_3_0, 0, 1, 0}, +{ 6, s_3_1, -1, 1, 0}, +{ 2, s_3_2, 0, 1, 0}, +{ 6, s_3_3, -1, 1, 0}, +{ 4, s_3_4, -2, 1, 0}, +{ 8, s_3_5, -1, 1, 0}, +{ 10, s_3_6, -1, 1, 0}, +{ 6, s_3_7, -3, 1, 0}, +{ 10, s_3_8, -4, 1, 0}, +{ 8, s_3_9, -5, 1, 0}, +{ 6, s_3_10, -6, 1, 0}, +{ 4, s_3_11, -9, 1, 0}, +{ 4, s_3_12, 0, 1, 0}, +{ 4, s_3_13, 0, 1, 0}, +{ 6, s_3_14, 0, 1, 0}, +{ 2, s_3_15, 0, 1, 0}, +{ 6, s_3_16, -1, 1, 0}, +{ 8, s_3_17, -1, 1, 0}, +{ 6, s_3_18, -3, 1, 0}, +{ 6, s_3_19, -4, 1, 0}, +{ 14, s_3_20, -1, 1, 0}, +{ 8, s_3_21, -2, 1, 0}, +{ 6, s_3_22, -7, 1, 0}, +{ 2, s_3_23, 0, 1, 0}, +{ 6, s_3_24, -1, 1, 0}, +{ 8, s_3_25, -1, 1, 0}, +{ 6, s_3_26, -3, 1, 0}, +{ 14, s_3_27, -1, 1, 0}, +{ 8, s_3_28, -2, 1, 0}, +{ 6, s_3_29, -6, 1, 0}, +{ 2, s_3_30, 0, 1, 0}, +{ 6, s_3_31, -1, 1, 0}, +{ 8, s_3_32, -1, 1, 0}, +{ 4, s_3_33, -3, 1, 0}, +{ 10, s_3_34, 0, 1, 0}, +{ 12, s_3_35, -1, 1, 0}, +{ 10, s_3_36, 0, 1, 0}, +{ 2, s_3_37, 0, 1, 0}, +{ 6, s_3_38, -1, 1, 0}, +{ 8, s_3_39, -1, 1, 0}, +{ 6, s_3_40, -3, 1, 0}, +{ 4, s_3_41, -4, 1, 0}, +{ 12, s_3_42, -1, 1, 0}, +{ 6, s_3_43, -2, 1, 0}, +{ 4, s_3_44, -7, 1, 0}, +{ 8, s_3_45, -1, 1, 0}, +{ 10, s_3_46, -1, 1, 0}, +{ 14, s_3_47, -10, 1, 0}, +{ 4, s_3_48, 0, 1, 0}, +{ 14, s_3_49, 0, 1, 0}, +{ 8, s_3_50, 0, 1, 0}, +{ 6, s_3_51, 0, 1, 0}, +{ 4, s_3_52, 0, 1, 0}, +{ 8, s_3_53, -1, 1, 0}, +{ 10, s_3_54, -1, 1, 0}, +{ 8, s_3_55, -3, 1, 0}, +{ 6, s_3_56, -4, 1, 0} }; static const unsigned char g_v[] = { 209, 4, 128, 0, 18 }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping_U(z, g_v, 1377, 1413, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_pV = z->c; { int ret = in_grouping_U(z, g_v, 1377, 1413, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = out_grouping_U(z, g_v, 1377, 1413, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 1377, 1413, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_adjective(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_0, 23)) return 0; + if (!find_among_b(z, a_0, 23, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -484,9 +476,10 @@ static int r_adjective(struct SN_env * z) { static int r_verb(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_1, 71)) return 0; + if (!find_among_b(z, a_1, 71, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -494,9 +487,10 @@ static int r_verb(struct SN_env * z) { static int r_noun(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_2, 40)) return 0; + if (!find_among_b(z, a_2, 40, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -504,59 +498,77 @@ static int r_noun(struct SN_env * z) { static int r_ending(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_3, 57)) return 0; + if (!find_among_b(z, a_3, 57, 0)) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int armenian_UTF_8_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; - { int m2 = z->l - z->c; (void)m2; - { int ret = r_ending(z); + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; + { + int v_2 = z->l - z->c; + { + int ret = r_ending(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_verb(z); + { + int v_3 = z->l - z->c; + { + int ret = r_verb(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_adjective(z); + { + int v_4 = z->l - z->c; + { + int ret = r_adjective(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_noun(z); + { + int v_5 = z->l - z->c; + { + int ret = r_noun(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - z->lb = mlimit1; + z->lb = v_1; } z->c = z->lb; return 1; } -extern struct SN_env * armenian_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * armenian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void armenian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void armenian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_basque.c b/src/backend/snowball/libstemmer/stem_UTF_8_basque.c index 67b5b4835b872..9f29692f4282a 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_basque.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_basque.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from basque.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_basque.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int basque_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_R1(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_RV(struct SN_env * z); @@ -16,18 +30,12 @@ static int r_mark_regions(struct SN_env * z); static int r_adjetiboak(struct SN_env * z); static int r_izenak(struct SN_env * z); static int r_aditzak(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif +static const symbol s_0[] = { 'j', 'o', 'k' }; +static const symbol s_1[] = { 't', 'r', 'a' }; +static const symbol s_2[] = { 'm', 'i', 'n', 'u', 't', 'u' }; +static const symbol s_3[] = { 'z' }; -extern struct SN_env * basque_UTF_8_create_env(void); -extern void basque_UTF_8_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif static const symbol s_0_0[4] = { 'i', 'd', 'e', 'a' }; static const symbol s_0_1[5] = { 'b', 'i', 'd', 'e', 'a' }; static const symbol s_0_2[5] = { 'k', 'i', 'd', 'e', 'a' }; @@ -137,118 +145,116 @@ static const symbol s_0_105[5] = { 'e', 'r', 'r', 'e', 'z' }; static const symbol s_0_106[4] = { 't', 'z', 'e', 'z' }; static const symbol s_0_107[5] = { 'g', 'a', 'i', 't', 'z' }; static const symbol s_0_108[5] = { 'k', 'a', 'i', 't', 'z' }; - -static const struct among a_0[109] = -{ -{ 4, s_0_0, -1, 1, 0}, -{ 5, s_0_1, 0, 1, 0}, -{ 5, s_0_2, 0, 1, 0}, -{ 5, s_0_3, 0, 1, 0}, -{ 6, s_0_4, -1, 1, 0}, -{ 5, s_0_5, -1, 1, 0}, -{ 6, s_0_6, -1, 1, 0}, -{ 7, s_0_7, -1, 1, 0}, -{ 5, s_0_8, -1, 1, 0}, -{ 5, s_0_9, -1, 1, 0}, -{ 5, s_0_10, -1, 1, 0}, -{ 4, s_0_11, -1, 1, 0}, -{ 5, s_0_12, -1, 1, 0}, -{ 6, s_0_13, 12, 1, 0}, -{ 5, s_0_14, -1, 1, 0}, -{ 6, s_0_15, -1, 2, 0}, -{ 6, s_0_16, -1, 1, 0}, -{ 2, s_0_17, -1, 1, 0}, -{ 5, s_0_18, 17, 1, 0}, -{ 2, s_0_19, -1, 1, 0}, -{ 4, s_0_20, -1, 1, 0}, -{ 4, s_0_21, -1, 1, 0}, -{ 4, s_0_22, -1, 1, 0}, -{ 5, s_0_23, -1, 1, 0}, -{ 6, s_0_24, 23, 1, 0}, -{ 4, s_0_25, -1, 1, 0}, -{ 4, s_0_26, -1, 1, 0}, -{ 6, s_0_27, -1, 1, 0}, -{ 3, s_0_28, -1, 1, 0}, -{ 4, s_0_29, 28, 1, 0}, -{ 7, s_0_30, 29, 4, 0}, -{ 4, s_0_31, 28, 1, 0}, -{ 4, s_0_32, 28, 1, 0}, -{ 4, s_0_33, -1, 1, 0}, -{ 5, s_0_34, 33, 1, 0}, -{ 4, s_0_35, -1, 1, 0}, -{ 4, s_0_36, -1, 1, 0}, -{ 4, s_0_37, -1, 1, 0}, -{ 4, s_0_38, -1, 1, 0}, -{ 3, s_0_39, -1, 1, 0}, -{ 4, s_0_40, 39, 1, 0}, -{ 6, s_0_41, -1, 1, 0}, -{ 3, s_0_42, -1, 1, 0}, -{ 6, s_0_43, 42, 1, 0}, -{ 3, s_0_44, -1, 2, 0}, -{ 6, s_0_45, 44, 1, 0}, -{ 6, s_0_46, 44, 1, 0}, -{ 6, s_0_47, 44, 1, 0}, -{ 3, s_0_48, -1, 1, 0}, -{ 4, s_0_49, 48, 1, 0}, -{ 4, s_0_50, 48, 1, 0}, -{ 4, s_0_51, 48, 1, 0}, -{ 5, s_0_52, -1, 1, 0}, -{ 5, s_0_53, -1, 1, 0}, -{ 5, s_0_54, -1, 1, 0}, -{ 2, s_0_55, -1, 1, 0}, -{ 4, s_0_56, 55, 1, 0}, -{ 5, s_0_57, 55, 1, 0}, -{ 6, s_0_58, 55, 1, 0}, -{ 4, s_0_59, -1, 1, 0}, -{ 4, s_0_60, -1, 1, 0}, -{ 3, s_0_61, -1, 1, 0}, -{ 4, s_0_62, 61, 1, 0}, -{ 3, s_0_63, -1, 1, 0}, -{ 4, s_0_64, -1, 1, 0}, -{ 5, s_0_65, 64, 1, 0}, -{ 2, s_0_66, -1, 1, 0}, -{ 3, s_0_67, -1, 1, 0}, -{ 4, s_0_68, 67, 1, 0}, -{ 4, s_0_69, 67, 1, 0}, -{ 4, s_0_70, 67, 1, 0}, -{ 5, s_0_71, 70, 1, 0}, -{ 5, s_0_72, -1, 2, 0}, -{ 5, s_0_73, -1, 1, 0}, -{ 5, s_0_74, -1, 1, 0}, -{ 6, s_0_75, 74, 1, 0}, -{ 2, s_0_76, -1, 1, 0}, -{ 3, s_0_77, 76, 1, 0}, -{ 4, s_0_78, 77, 1, 0}, -{ 3, s_0_79, 76, 1, 0}, -{ 4, s_0_80, 76, 1, 0}, -{ 7, s_0_81, -1, 3, 0}, -{ 3, s_0_82, -1, 1, 0}, -{ 3, s_0_83, -1, 1, 0}, -{ 3, s_0_84, -1, 1, 0}, -{ 5, s_0_85, 84, 1, 0}, -{ 4, s_0_86, -1, 1, 0}, -{ 5, s_0_87, 86, 1, 0}, -{ 3, s_0_88, -1, 1, 0}, -{ 5, s_0_89, -1, 1, 0}, -{ 2, s_0_90, -1, 1, 0}, -{ 3, s_0_91, 90, 1, 0}, -{ 3, s_0_92, -1, 1, 0}, -{ 4, s_0_93, -1, 1, 0}, -{ 2, s_0_94, -1, 1, 0}, -{ 3, s_0_95, 94, 1, 0}, -{ 4, s_0_96, -1, 1, 0}, -{ 2, s_0_97, -1, 1, 0}, -{ 5, s_0_98, -1, 1, 0}, -{ 2, s_0_99, -1, 1, 0}, -{ 3, s_0_100, 99, 1, 0}, -{ 6, s_0_101, 100, 1, 0}, -{ 4, s_0_102, 100, 1, 0}, -{ 6, s_0_103, 99, 5, 0}, -{ 2, s_0_104, -1, 1, 0}, -{ 5, s_0_105, 104, 1, 0}, -{ 4, s_0_106, 104, 1, 0}, -{ 5, s_0_107, -1, 1, 0}, -{ 5, s_0_108, -1, 1, 0} +static const struct among a_0[109] = { +{ 4, s_0_0, 0, 1, 0}, +{ 5, s_0_1, -1, 1, 0}, +{ 5, s_0_2, -2, 1, 0}, +{ 5, s_0_3, -3, 1, 0}, +{ 6, s_0_4, 0, 1, 0}, +{ 5, s_0_5, 0, 1, 0}, +{ 6, s_0_6, 0, 1, 0}, +{ 7, s_0_7, 0, 1, 0}, +{ 5, s_0_8, 0, 1, 0}, +{ 5, s_0_9, 0, 1, 0}, +{ 5, s_0_10, 0, 1, 0}, +{ 4, s_0_11, 0, 1, 0}, +{ 5, s_0_12, 0, 1, 0}, +{ 6, s_0_13, -1, 1, 0}, +{ 5, s_0_14, 0, 1, 0}, +{ 6, s_0_15, 0, 2, 0}, +{ 6, s_0_16, 0, 1, 0}, +{ 2, s_0_17, 0, 1, 0}, +{ 5, s_0_18, -1, 1, 0}, +{ 2, s_0_19, 0, 1, 0}, +{ 4, s_0_20, 0, 1, 0}, +{ 4, s_0_21, 0, 1, 0}, +{ 4, s_0_22, 0, 1, 0}, +{ 5, s_0_23, 0, 1, 0}, +{ 6, s_0_24, -1, 1, 0}, +{ 4, s_0_25, 0, 1, 0}, +{ 4, s_0_26, 0, 1, 0}, +{ 6, s_0_27, 0, 1, 0}, +{ 3, s_0_28, 0, 1, 0}, +{ 4, s_0_29, -1, 1, 0}, +{ 7, s_0_30, -1, -1, 0}, +{ 4, s_0_31, -3, 1, 0}, +{ 4, s_0_32, -4, 1, 0}, +{ 4, s_0_33, 0, 1, 0}, +{ 5, s_0_34, -1, 1, 0}, +{ 4, s_0_35, 0, 1, 0}, +{ 4, s_0_36, 0, 1, 0}, +{ 4, s_0_37, 0, 1, 0}, +{ 4, s_0_38, 0, 1, 0}, +{ 3, s_0_39, 0, 1, 0}, +{ 4, s_0_40, -1, 1, 0}, +{ 6, s_0_41, 0, 1, 0}, +{ 3, s_0_42, 0, 1, 0}, +{ 6, s_0_43, -1, 1, 0}, +{ 3, s_0_44, 0, 2, 0}, +{ 6, s_0_45, -1, 1, 0}, +{ 6, s_0_46, -2, 1, 0}, +{ 6, s_0_47, -3, 1, 0}, +{ 3, s_0_48, 0, 1, 0}, +{ 4, s_0_49, -1, 1, 0}, +{ 4, s_0_50, -2, 1, 0}, +{ 4, s_0_51, -3, 1, 0}, +{ 5, s_0_52, 0, 1, 0}, +{ 5, s_0_53, 0, 1, 0}, +{ 5, s_0_54, 0, 1, 0}, +{ 2, s_0_55, 0, 1, 0}, +{ 4, s_0_56, -1, 1, 0}, +{ 5, s_0_57, -2, 1, 0}, +{ 6, s_0_58, -3, 1, 0}, +{ 4, s_0_59, 0, 1, 0}, +{ 4, s_0_60, 0, 1, 0}, +{ 3, s_0_61, 0, 1, 0}, +{ 4, s_0_62, -1, 1, 0}, +{ 3, s_0_63, 0, 1, 0}, +{ 4, s_0_64, 0, 1, 0}, +{ 5, s_0_65, -1, 1, 0}, +{ 2, s_0_66, 0, 1, 0}, +{ 3, s_0_67, 0, 1, 0}, +{ 4, s_0_68, -1, 1, 0}, +{ 4, s_0_69, -2, 1, 0}, +{ 4, s_0_70, -3, 1, 0}, +{ 5, s_0_71, -1, 1, 0}, +{ 5, s_0_72, 0, 2, 0}, +{ 5, s_0_73, 0, 1, 0}, +{ 5, s_0_74, 0, 1, 0}, +{ 6, s_0_75, -1, 1, 0}, +{ 2, s_0_76, 0, 1, 0}, +{ 3, s_0_77, -1, 1, 0}, +{ 4, s_0_78, -1, 1, 0}, +{ 3, s_0_79, -3, 1, 0}, +{ 4, s_0_80, -4, 1, 0}, +{ 7, s_0_81, 0, -1, 0}, +{ 3, s_0_82, 0, 1, 0}, +{ 3, s_0_83, 0, 1, 0}, +{ 3, s_0_84, 0, 1, 0}, +{ 5, s_0_85, -1, 1, 0}, +{ 4, s_0_86, 0, 1, 0}, +{ 5, s_0_87, -1, 1, 0}, +{ 3, s_0_88, 0, 1, 0}, +{ 5, s_0_89, 0, 1, 0}, +{ 2, s_0_90, 0, 1, 0}, +{ 3, s_0_91, -1, 1, 0}, +{ 3, s_0_92, 0, 1, 0}, +{ 4, s_0_93, 0, 1, 0}, +{ 2, s_0_94, 0, 1, 0}, +{ 3, s_0_95, -1, 1, 0}, +{ 4, s_0_96, 0, 1, 0}, +{ 2, s_0_97, 0, 1, 0}, +{ 5, s_0_98, 0, 1, 0}, +{ 2, s_0_99, 0, 1, 0}, +{ 3, s_0_100, -1, 1, 0}, +{ 6, s_0_101, -1, 1, 0}, +{ 4, s_0_102, -2, 1, 0}, +{ 6, s_0_103, -4, -1, 0}, +{ 2, s_0_104, 0, 1, 0}, +{ 5, s_0_105, -1, 1, 0}, +{ 4, s_0_106, -2, 1, 0}, +{ 5, s_0_107, 0, 1, 0}, +{ 5, s_0_108, 0, 1, 0} }; static const symbol s_1_0[3] = { 'a', 'd', 'a' }; @@ -546,304 +552,302 @@ static const symbol s_1_291[2] = { 'e', 'z' }; static const symbol s_1_292[4] = { 'e', 'r', 'o', 'z' }; static const symbol s_1_293[2] = { 't', 'z' }; static const symbol s_1_294[5] = { 'k', 'o', 'i', 't', 'z' }; - -static const struct among a_1[295] = -{ -{ 3, s_1_0, -1, 1, 0}, -{ 4, s_1_1, 0, 1, 0}, -{ 4, s_1_2, -1, 1, 0}, -{ 5, s_1_3, -1, 1, 0}, -{ 5, s_1_4, -1, 1, 0}, -{ 5, s_1_5, -1, 1, 0}, -{ 5, s_1_6, -1, 1, 0}, -{ 6, s_1_7, 6, 1, 0}, -{ 6, s_1_8, 6, 1, 0}, -{ 5, s_1_9, -1, 1, 0}, -{ 5, s_1_10, -1, 1, 0}, -{ 6, s_1_11, 10, 1, 0}, -{ 5, s_1_12, -1, 1, 0}, -{ 4, s_1_13, -1, 1, 0}, -{ 5, s_1_14, -1, 1, 0}, -{ 3, s_1_15, -1, 1, 0}, -{ 4, s_1_16, 15, 1, 0}, -{ 6, s_1_17, 15, 1, 0}, -{ 4, s_1_18, 15, 1, 0}, -{ 5, s_1_19, 18, 1, 0}, -{ 3, s_1_20, -1, 1, 0}, -{ 6, s_1_21, -1, 1, 0}, -{ 3, s_1_22, -1, 1, 0}, -{ 5, s_1_23, 22, 1, 0}, -{ 5, s_1_24, 22, 1, 0}, -{ 5, s_1_25, 22, 1, 0}, -{ 5, s_1_26, -1, 1, 0}, -{ 2, s_1_27, -1, 1, 0}, -{ 4, s_1_28, 27, 1, 0}, -{ 4, s_1_29, -1, 1, 0}, -{ 5, s_1_30, -1, 1, 0}, -{ 6, s_1_31, 30, 1, 0}, -{ 6, s_1_32, -1, 1, 0}, -{ 6, s_1_33, -1, 1, 0}, -{ 4, s_1_34, -1, 1, 0}, -{ 4, s_1_35, -1, 1, 0}, -{ 5, s_1_36, 35, 1, 0}, -{ 5, s_1_37, 35, 1, 0}, -{ 5, s_1_38, -1, 1, 0}, -{ 4, s_1_39, -1, 1, 0}, -{ 3, s_1_40, -1, 1, 0}, -{ 5, s_1_41, 40, 1, 0}, -{ 3, s_1_42, -1, 1, 0}, -{ 4, s_1_43, 42, 1, 0}, -{ 4, s_1_44, -1, 1, 0}, -{ 5, s_1_45, 44, 1, 0}, -{ 5, s_1_46, 44, 1, 0}, -{ 5, s_1_47, 44, 1, 0}, -{ 4, s_1_48, -1, 1, 0}, -{ 5, s_1_49, 48, 1, 0}, -{ 5, s_1_50, 48, 1, 0}, -{ 6, s_1_51, -1, 2, 0}, -{ 6, s_1_52, -1, 1, 0}, -{ 6, s_1_53, -1, 1, 0}, -{ 5, s_1_54, -1, 1, 0}, -{ 4, s_1_55, -1, 1, 0}, -{ 3, s_1_56, -1, 1, 0}, -{ 4, s_1_57, -1, 1, 0}, -{ 5, s_1_58, -1, 1, 0}, -{ 6, s_1_59, -1, 1, 0}, -{ 2, s_1_60, -1, 1, 0}, -{ 4, s_1_61, 60, 3, 0}, -{ 5, s_1_62, 60, 10, 0}, -{ 3, s_1_63, 60, 1, 0}, -{ 3, s_1_64, 60, 1, 0}, -{ 3, s_1_65, 60, 1, 0}, -{ 6, s_1_66, -1, 1, 0}, -{ 4, s_1_67, -1, 1, 0}, -{ 5, s_1_68, -1, 1, 0}, -{ 5, s_1_69, -1, 1, 0}, -{ 4, s_1_70, -1, 1, 0}, -{ 3, s_1_71, -1, 1, 0}, -{ 2, s_1_72, -1, 1, 0}, -{ 4, s_1_73, 72, 1, 0}, -{ 3, s_1_74, 72, 1, 0}, -{ 7, s_1_75, 74, 1, 0}, -{ 7, s_1_76, 74, 1, 0}, -{ 6, s_1_77, 74, 1, 0}, -{ 5, s_1_78, 72, 1, 0}, -{ 6, s_1_79, 78, 1, 0}, -{ 4, s_1_80, 72, 1, 0}, -{ 4, s_1_81, 72, 1, 0}, -{ 5, s_1_82, 72, 1, 0}, -{ 3, s_1_83, 72, 1, 0}, -{ 4, s_1_84, 83, 1, 0}, -{ 5, s_1_85, 83, 1, 0}, -{ 6, s_1_86, 85, 1, 0}, -{ 5, s_1_87, -1, 1, 0}, -{ 6, s_1_88, 87, 1, 0}, -{ 4, s_1_89, -1, 1, 0}, -{ 4, s_1_90, -1, 1, 0}, -{ 3, s_1_91, -1, 1, 0}, -{ 5, s_1_92, 91, 1, 0}, -{ 4, s_1_93, 91, 1, 0}, -{ 3, s_1_94, -1, 1, 0}, -{ 5, s_1_95, 94, 1, 0}, -{ 4, s_1_96, -1, 1, 0}, -{ 5, s_1_97, 96, 1, 0}, -{ 5, s_1_98, 96, 1, 0}, -{ 4, s_1_99, -1, 1, 0}, -{ 4, s_1_100, -1, 1, 0}, -{ 4, s_1_101, -1, 1, 0}, -{ 3, s_1_102, -1, 1, 0}, -{ 4, s_1_103, 102, 1, 0}, -{ 4, s_1_104, 102, 1, 0}, -{ 4, s_1_105, -1, 1, 0}, -{ 4, s_1_106, -1, 1, 0}, -{ 4, s_1_107, -1, 1, 0}, -{ 2, s_1_108, -1, 1, 0}, -{ 3, s_1_109, 108, 1, 0}, -{ 4, s_1_110, 109, 1, 0}, -{ 5, s_1_111, 109, 1, 0}, -{ 5, s_1_112, 109, 1, 0}, -{ 4, s_1_113, 109, 1, 0}, -{ 5, s_1_114, 113, 1, 0}, -{ 5, s_1_115, 109, 1, 0}, -{ 4, s_1_116, 108, 1, 0}, -{ 4, s_1_117, 108, 1, 0}, -{ 4, s_1_118, 108, 1, 0}, -{ 3, s_1_119, 108, 2, 0}, -{ 6, s_1_120, 108, 1, 0}, -{ 5, s_1_121, 108, 1, 0}, -{ 3, s_1_122, 108, 1, 0}, -{ 2, s_1_123, -1, 1, 0}, -{ 3, s_1_124, 123, 1, 0}, -{ 2, s_1_125, -1, 1, 0}, -{ 3, s_1_126, 125, 1, 0}, -{ 4, s_1_127, 126, 1, 0}, -{ 3, s_1_128, 125, 1, 0}, -{ 3, s_1_129, -1, 1, 0}, -{ 6, s_1_130, 129, 1, 0}, -{ 5, s_1_131, 129, 1, 0}, -{ 5, s_1_132, -1, 1, 0}, -{ 5, s_1_133, -1, 1, 0}, -{ 5, s_1_134, -1, 1, 0}, -{ 4, s_1_135, -1, 1, 0}, -{ 3, s_1_136, -1, 1, 0}, -{ 6, s_1_137, 136, 1, 0}, -{ 5, s_1_138, 136, 1, 0}, -{ 4, s_1_139, -1, 1, 0}, -{ 3, s_1_140, -1, 1, 0}, -{ 4, s_1_141, 140, 1, 0}, -{ 2, s_1_142, -1, 1, 0}, -{ 3, s_1_143, 142, 1, 0}, -{ 5, s_1_144, 142, 1, 0}, -{ 3, s_1_145, 142, 2, 0}, -{ 6, s_1_146, 145, 1, 0}, -{ 5, s_1_147, 145, 1, 0}, -{ 6, s_1_148, 145, 1, 0}, -{ 6, s_1_149, 145, 1, 0}, -{ 6, s_1_150, 145, 1, 0}, -{ 4, s_1_151, -1, 1, 0}, -{ 4, s_1_152, -1, 1, 0}, -{ 4, s_1_153, -1, 1, 0}, -{ 4, s_1_154, -1, 1, 0}, -{ 5, s_1_155, 154, 1, 0}, -{ 5, s_1_156, 154, 1, 0}, -{ 4, s_1_157, -1, 1, 0}, -{ 2, s_1_158, -1, 1, 0}, -{ 4, s_1_159, -1, 1, 0}, -{ 5, s_1_160, 159, 1, 0}, -{ 4, s_1_161, -1, 1, 0}, -{ 3, s_1_162, -1, 1, 0}, -{ 4, s_1_163, -1, 1, 0}, -{ 2, s_1_164, -1, 1, 0}, -{ 5, s_1_165, 164, 1, 0}, -{ 3, s_1_166, 164, 1, 0}, -{ 4, s_1_167, 166, 1, 0}, -{ 2, s_1_168, -1, 1, 0}, -{ 5, s_1_169, -1, 1, 0}, -{ 2, s_1_170, -1, 1, 0}, -{ 4, s_1_171, 170, 1, 0}, -{ 4, s_1_172, 170, 1, 0}, -{ 4, s_1_173, 170, 1, 0}, -{ 4, s_1_174, -1, 1, 0}, -{ 3, s_1_175, -1, 1, 0}, -{ 2, s_1_176, -1, 1, 0}, -{ 4, s_1_177, 176, 1, 0}, -{ 5, s_1_178, 177, 1, 0}, -{ 5, s_1_179, 176, 8, 0}, -{ 5, s_1_180, 176, 1, 0}, -{ 5, s_1_181, 176, 1, 0}, -{ 3, s_1_182, -1, 1, 0}, -{ 3, s_1_183, -1, 1, 0}, -{ 4, s_1_184, 183, 1, 0}, -{ 4, s_1_185, 183, 1, 0}, -{ 4, s_1_186, -1, 1, 0}, -{ 3, s_1_187, -1, 1, 0}, -{ 2, s_1_188, -1, 1, 0}, -{ 4, s_1_189, 188, 1, 0}, -{ 2, s_1_190, -1, 1, 0}, -{ 3, s_1_191, 190, 1, 0}, -{ 3, s_1_192, 190, 1, 0}, -{ 3, s_1_193, -1, 1, 0}, -{ 4, s_1_194, 193, 1, 0}, -{ 4, s_1_195, 193, 1, 0}, -{ 4, s_1_196, 193, 1, 0}, -{ 5, s_1_197, -1, 2, 0}, -{ 5, s_1_198, -1, 1, 0}, -{ 5, s_1_199, -1, 1, 0}, -{ 4, s_1_200, -1, 1, 0}, -{ 3, s_1_201, -1, 1, 0}, -{ 2, s_1_202, -1, 1, 0}, -{ 5, s_1_203, -1, 1, 0}, -{ 3, s_1_204, -1, 1, 0}, -{ 2, s_1_205, -1, 1, 0}, -{ 2, s_1_206, -1, 1, 0}, -{ 5, s_1_207, -1, 1, 0}, -{ 5, s_1_208, -1, 1, 0}, -{ 3, s_1_209, -1, 1, 0}, -{ 4, s_1_210, 209, 1, 0}, -{ 3, s_1_211, -1, 1, 0}, -{ 3, s_1_212, -1, 1, 0}, -{ 4, s_1_213, 212, 1, 0}, -{ 2, s_1_214, -1, 4, 0}, -{ 3, s_1_215, 214, 2, 0}, -{ 6, s_1_216, 215, 1, 0}, -{ 6, s_1_217, 215, 1, 0}, -{ 5, s_1_218, 215, 1, 0}, -{ 3, s_1_219, 214, 4, 0}, -{ 4, s_1_220, 214, 4, 0}, -{ 4, s_1_221, -1, 1, 0}, -{ 5, s_1_222, 221, 1, 0}, -{ 3, s_1_223, -1, 1, 0}, -{ 3, s_1_224, -1, 1, 0}, -{ 3, s_1_225, -1, 1, 0}, -{ 4, s_1_226, -1, 1, 0}, -{ 5, s_1_227, 226, 1, 0}, -{ 5, s_1_228, -1, 1, 0}, -{ 4, s_1_229, -1, 1, 0}, -{ 5, s_1_230, 229, 1, 0}, -{ 2, s_1_231, -1, 1, 0}, -{ 3, s_1_232, 231, 1, 0}, -{ 3, s_1_233, -1, 1, 0}, -{ 2, s_1_234, -1, 1, 0}, -{ 5, s_1_235, 234, 5, 0}, -{ 4, s_1_236, 234, 1, 0}, -{ 5, s_1_237, 236, 1, 0}, -{ 3, s_1_238, 234, 1, 0}, -{ 6, s_1_239, 234, 1, 0}, -{ 3, s_1_240, 234, 1, 0}, -{ 4, s_1_241, 234, 1, 0}, -{ 8, s_1_242, 241, 6, 0}, -{ 3, s_1_243, 234, 1, 0}, -{ 2, s_1_244, -1, 1, 0}, -{ 4, s_1_245, 244, 1, 0}, -{ 2, s_1_246, -1, 1, 0}, -{ 3, s_1_247, 246, 1, 0}, -{ 5, s_1_248, 247, 9, 0}, -{ 4, s_1_249, 247, 1, 0}, -{ 4, s_1_250, 247, 1, 0}, -{ 3, s_1_251, 246, 1, 0}, -{ 4, s_1_252, 246, 1, 0}, -{ 3, s_1_253, 246, 1, 0}, -{ 3, s_1_254, -1, 1, 0}, -{ 2, s_1_255, -1, 1, 0}, -{ 3, s_1_256, 255, 1, 0}, -{ 3, s_1_257, 255, 1, 0}, -{ 3, s_1_258, -1, 1, 0}, -{ 3, s_1_259, -1, 1, 0}, -{ 6, s_1_260, 259, 1, 0}, -{ 3, s_1_261, -1, 1, 0}, -{ 2, s_1_262, -1, 1, 0}, -{ 2, s_1_263, -1, 1, 0}, -{ 3, s_1_264, 263, 1, 0}, -{ 5, s_1_265, 263, 1, 0}, -{ 5, s_1_266, 263, 7, 0}, -{ 4, s_1_267, 263, 1, 0}, -{ 4, s_1_268, 263, 1, 0}, -{ 3, s_1_269, 263, 1, 0}, -{ 4, s_1_270, 263, 1, 0}, -{ 2, s_1_271, -1, 2, 0}, -{ 3, s_1_272, 271, 1, 0}, -{ 2, s_1_273, -1, 1, 0}, -{ 3, s_1_274, -1, 1, 0}, -{ 2, s_1_275, -1, 1, 0}, -{ 5, s_1_276, 275, 1, 0}, -{ 4, s_1_277, 275, 1, 0}, -{ 4, s_1_278, -1, 1, 0}, -{ 4, s_1_279, -1, 2, 0}, -{ 4, s_1_280, -1, 1, 0}, -{ 3, s_1_281, -1, 1, 0}, -{ 2, s_1_282, -1, 1, 0}, -{ 4, s_1_283, 282, 4, 0}, -{ 5, s_1_284, 282, 1, 0}, -{ 4, s_1_285, 282, 1, 0}, -{ 3, s_1_286, -1, 1, 0}, -{ 2, s_1_287, -1, 1, 0}, -{ 3, s_1_288, 287, 1, 0}, -{ 6, s_1_289, 288, 1, 0}, -{ 1, s_1_290, -1, 1, 0}, -{ 2, s_1_291, 290, 1, 0}, -{ 4, s_1_292, 290, 1, 0}, -{ 2, s_1_293, 290, 1, 0}, -{ 5, s_1_294, 293, 1, 0} +static const struct among a_1[295] = { +{ 3, s_1_0, 0, 1, 0}, +{ 4, s_1_1, -1, 1, 0}, +{ 4, s_1_2, 0, 1, 0}, +{ 5, s_1_3, 0, 1, 0}, +{ 5, s_1_4, 0, 1, 0}, +{ 5, s_1_5, 0, 1, 0}, +{ 5, s_1_6, 0, 1, 0}, +{ 6, s_1_7, -1, 1, 0}, +{ 6, s_1_8, -2, 1, 0}, +{ 5, s_1_9, 0, 1, 0}, +{ 5, s_1_10, 0, 1, 0}, +{ 6, s_1_11, -1, 1, 0}, +{ 5, s_1_12, 0, 1, 0}, +{ 4, s_1_13, 0, 1, 0}, +{ 5, s_1_14, 0, 1, 0}, +{ 3, s_1_15, 0, 1, 0}, +{ 4, s_1_16, -1, 1, 0}, +{ 6, s_1_17, -2, 1, 0}, +{ 4, s_1_18, -3, 1, 0}, +{ 5, s_1_19, -1, 1, 0}, +{ 3, s_1_20, 0, 1, 0}, +{ 6, s_1_21, 0, 1, 0}, +{ 3, s_1_22, 0, 1, 0}, +{ 5, s_1_23, -1, 1, 0}, +{ 5, s_1_24, -2, 1, 0}, +{ 5, s_1_25, -3, 1, 0}, +{ 5, s_1_26, 0, 1, 0}, +{ 2, s_1_27, 0, 1, 0}, +{ 4, s_1_28, -1, 1, 0}, +{ 4, s_1_29, 0, 1, 0}, +{ 5, s_1_30, 0, 1, 0}, +{ 6, s_1_31, -1, 1, 0}, +{ 6, s_1_32, 0, 1, 0}, +{ 6, s_1_33, 0, 1, 0}, +{ 4, s_1_34, 0, 1, 0}, +{ 4, s_1_35, 0, 1, 0}, +{ 5, s_1_36, -1, 1, 0}, +{ 5, s_1_37, -2, 1, 0}, +{ 5, s_1_38, 0, 1, 0}, +{ 4, s_1_39, 0, 1, 0}, +{ 3, s_1_40, 0, 1, 0}, +{ 5, s_1_41, -1, 1, 0}, +{ 3, s_1_42, 0, 1, 0}, +{ 4, s_1_43, -1, 1, 0}, +{ 4, s_1_44, 0, 1, 0}, +{ 5, s_1_45, -1, 1, 0}, +{ 5, s_1_46, -2, 1, 0}, +{ 5, s_1_47, -3, 1, 0}, +{ 4, s_1_48, 0, 1, 0}, +{ 5, s_1_49, -1, 1, 0}, +{ 5, s_1_50, -2, 1, 0}, +{ 6, s_1_51, 0, 2, 0}, +{ 6, s_1_52, 0, 1, 0}, +{ 6, s_1_53, 0, 1, 0}, +{ 5, s_1_54, 0, 1, 0}, +{ 4, s_1_55, 0, 1, 0}, +{ 3, s_1_56, 0, 1, 0}, +{ 4, s_1_57, 0, 1, 0}, +{ 5, s_1_58, 0, 1, 0}, +{ 6, s_1_59, 0, 1, 0}, +{ 2, s_1_60, 0, 1, 0}, +{ 4, s_1_61, -1, 3, 0}, +{ 5, s_1_62, -2, -1, 0}, +{ 3, s_1_63, -3, 1, 0}, +{ 3, s_1_64, -4, 1, 0}, +{ 3, s_1_65, -5, 1, 0}, +{ 6, s_1_66, 0, 1, 0}, +{ 4, s_1_67, 0, 1, 0}, +{ 5, s_1_68, 0, 1, 0}, +{ 5, s_1_69, 0, 1, 0}, +{ 4, s_1_70, 0, 1, 0}, +{ 3, s_1_71, 0, 1, 0}, +{ 2, s_1_72, 0, 1, 0}, +{ 4, s_1_73, -1, 1, 0}, +{ 3, s_1_74, -2, 1, 0}, +{ 7, s_1_75, -1, 1, 0}, +{ 7, s_1_76, -2, 1, 0}, +{ 6, s_1_77, -3, 1, 0}, +{ 5, s_1_78, -6, 1, 0}, +{ 6, s_1_79, -1, 1, 0}, +{ 4, s_1_80, -8, 1, 0}, +{ 4, s_1_81, -9, 1, 0}, +{ 5, s_1_82, -10, 1, 0}, +{ 3, s_1_83, -11, 1, 0}, +{ 4, s_1_84, -1, 1, 0}, +{ 5, s_1_85, -2, 1, 0}, +{ 6, s_1_86, -1, 1, 0}, +{ 5, s_1_87, 0, 1, 0}, +{ 6, s_1_88, -1, 1, 0}, +{ 4, s_1_89, 0, 1, 0}, +{ 4, s_1_90, 0, 1, 0}, +{ 3, s_1_91, 0, 1, 0}, +{ 5, s_1_92, -1, 1, 0}, +{ 4, s_1_93, -2, 1, 0}, +{ 3, s_1_94, 0, 1, 0}, +{ 5, s_1_95, -1, 1, 0}, +{ 4, s_1_96, 0, 1, 0}, +{ 5, s_1_97, -1, 1, 0}, +{ 5, s_1_98, -2, 1, 0}, +{ 4, s_1_99, 0, 1, 0}, +{ 4, s_1_100, 0, 1, 0}, +{ 4, s_1_101, 0, 1, 0}, +{ 3, s_1_102, 0, 1, 0}, +{ 4, s_1_103, -1, 1, 0}, +{ 4, s_1_104, -2, 1, 0}, +{ 4, s_1_105, 0, 1, 0}, +{ 4, s_1_106, 0, 1, 0}, +{ 4, s_1_107, 0, 1, 0}, +{ 2, s_1_108, 0, 1, 0}, +{ 3, s_1_109, -1, 1, 0}, +{ 4, s_1_110, -1, 1, 0}, +{ 5, s_1_111, -2, 1, 0}, +{ 5, s_1_112, -3, 1, 0}, +{ 4, s_1_113, -4, 1, 0}, +{ 5, s_1_114, -1, 1, 0}, +{ 5, s_1_115, -6, 1, 0}, +{ 4, s_1_116, -8, 1, 0}, +{ 4, s_1_117, -9, 1, 0}, +{ 4, s_1_118, -10, 1, 0}, +{ 3, s_1_119, -11, 2, 0}, +{ 6, s_1_120, -12, 1, 0}, +{ 5, s_1_121, -13, 1, 0}, +{ 3, s_1_122, -14, 1, 0}, +{ 2, s_1_123, 0, 1, 0}, +{ 3, s_1_124, -1, 1, 0}, +{ 2, s_1_125, 0, 1, 0}, +{ 3, s_1_126, -1, 1, 0}, +{ 4, s_1_127, -1, 1, 0}, +{ 3, s_1_128, -3, 1, 0}, +{ 3, s_1_129, 0, 1, 0}, +{ 6, s_1_130, -1, 1, 0}, +{ 5, s_1_131, -2, 1, 0}, +{ 5, s_1_132, 0, 1, 0}, +{ 5, s_1_133, 0, 1, 0}, +{ 5, s_1_134, 0, 1, 0}, +{ 4, s_1_135, 0, 1, 0}, +{ 3, s_1_136, 0, 1, 0}, +{ 6, s_1_137, -1, 1, 0}, +{ 5, s_1_138, -2, 1, 0}, +{ 4, s_1_139, 0, 1, 0}, +{ 3, s_1_140, 0, 1, 0}, +{ 4, s_1_141, -1, 1, 0}, +{ 2, s_1_142, 0, 1, 0}, +{ 3, s_1_143, -1, 1, 0}, +{ 5, s_1_144, -2, 1, 0}, +{ 3, s_1_145, -3, 2, 0}, +{ 6, s_1_146, -1, 1, 0}, +{ 5, s_1_147, -2, 1, 0}, +{ 6, s_1_148, -3, 1, 0}, +{ 6, s_1_149, -4, 1, 0}, +{ 6, s_1_150, -5, 1, 0}, +{ 4, s_1_151, 0, 1, 0}, +{ 4, s_1_152, 0, 1, 0}, +{ 4, s_1_153, 0, 1, 0}, +{ 4, s_1_154, 0, 1, 0}, +{ 5, s_1_155, -1, 1, 0}, +{ 5, s_1_156, -2, 1, 0}, +{ 4, s_1_157, 0, 1, 0}, +{ 2, s_1_158, 0, 1, 0}, +{ 4, s_1_159, 0, 1, 0}, +{ 5, s_1_160, -1, 1, 0}, +{ 4, s_1_161, 0, 1, 0}, +{ 3, s_1_162, 0, 1, 0}, +{ 4, s_1_163, 0, 1, 0}, +{ 2, s_1_164, 0, 1, 0}, +{ 5, s_1_165, -1, 1, 0}, +{ 3, s_1_166, -2, 1, 0}, +{ 4, s_1_167, -1, 1, 0}, +{ 2, s_1_168, 0, 1, 0}, +{ 5, s_1_169, 0, 1, 0}, +{ 2, s_1_170, 0, 1, 0}, +{ 4, s_1_171, -1, 1, 0}, +{ 4, s_1_172, -2, 1, 0}, +{ 4, s_1_173, -3, 1, 0}, +{ 4, s_1_174, 0, 1, 0}, +{ 3, s_1_175, 0, 1, 0}, +{ 2, s_1_176, 0, 1, 0}, +{ 4, s_1_177, -1, 1, 0}, +{ 5, s_1_178, -1, 1, 0}, +{ 5, s_1_179, -3, -1, 0}, +{ 5, s_1_180, -4, 1, 0}, +{ 5, s_1_181, -5, 1, 0}, +{ 3, s_1_182, 0, 1, 0}, +{ 3, s_1_183, 0, 1, 0}, +{ 4, s_1_184, -1, 1, 0}, +{ 4, s_1_185, -2, 1, 0}, +{ 4, s_1_186, 0, 1, 0}, +{ 3, s_1_187, 0, 1, 0}, +{ 2, s_1_188, 0, 1, 0}, +{ 4, s_1_189, -1, 1, 0}, +{ 2, s_1_190, 0, 1, 0}, +{ 3, s_1_191, -1, 1, 0}, +{ 3, s_1_192, -2, 1, 0}, +{ 3, s_1_193, 0, 1, 0}, +{ 4, s_1_194, -1, 1, 0}, +{ 4, s_1_195, -2, 1, 0}, +{ 4, s_1_196, -3, 1, 0}, +{ 5, s_1_197, 0, 2, 0}, +{ 5, s_1_198, 0, 1, 0}, +{ 5, s_1_199, 0, 1, 0}, +{ 4, s_1_200, 0, 1, 0}, +{ 3, s_1_201, 0, 1, 0}, +{ 2, s_1_202, 0, 1, 0}, +{ 5, s_1_203, 0, 1, 0}, +{ 3, s_1_204, 0, 1, 0}, +{ 2, s_1_205, 0, 1, 0}, +{ 2, s_1_206, 0, 1, 0}, +{ 5, s_1_207, 0, 1, 0}, +{ 5, s_1_208, 0, 1, 0}, +{ 3, s_1_209, 0, 1, 0}, +{ 4, s_1_210, -1, 1, 0}, +{ 3, s_1_211, 0, 1, 0}, +{ 3, s_1_212, 0, 1, 0}, +{ 4, s_1_213, -1, 1, 0}, +{ 2, s_1_214, 0, 4, 0}, +{ 3, s_1_215, -1, 2, 0}, +{ 6, s_1_216, -1, 1, 0}, +{ 6, s_1_217, -2, 1, 0}, +{ 5, s_1_218, -3, 1, 0}, +{ 3, s_1_219, -5, 4, 0}, +{ 4, s_1_220, -6, 4, 0}, +{ 4, s_1_221, 0, 1, 0}, +{ 5, s_1_222, -1, 1, 0}, +{ 3, s_1_223, 0, 1, 0}, +{ 3, s_1_224, 0, 1, 0}, +{ 3, s_1_225, 0, 1, 0}, +{ 4, s_1_226, 0, 1, 0}, +{ 5, s_1_227, -1, 1, 0}, +{ 5, s_1_228, 0, 1, 0}, +{ 4, s_1_229, 0, 1, 0}, +{ 5, s_1_230, -1, 1, 0}, +{ 2, s_1_231, 0, 1, 0}, +{ 3, s_1_232, -1, 1, 0}, +{ 3, s_1_233, 0, 1, 0}, +{ 2, s_1_234, 0, 1, 0}, +{ 5, s_1_235, -1, 5, 0}, +{ 4, s_1_236, -2, 1, 0}, +{ 5, s_1_237, -1, 1, 0}, +{ 3, s_1_238, -4, 1, 0}, +{ 6, s_1_239, -5, 1, 0}, +{ 3, s_1_240, -6, 1, 0}, +{ 4, s_1_241, -7, 1, 0}, +{ 8, s_1_242, -1, 6, 0}, +{ 3, s_1_243, -9, 1, 0}, +{ 2, s_1_244, 0, 1, 0}, +{ 4, s_1_245, -1, 1, 0}, +{ 2, s_1_246, 0, 1, 0}, +{ 3, s_1_247, -1, 1, 0}, +{ 5, s_1_248, -1, -1, 0}, +{ 4, s_1_249, -2, 1, 0}, +{ 4, s_1_250, -3, 1, 0}, +{ 3, s_1_251, -5, 1, 0}, +{ 4, s_1_252, -6, 1, 0}, +{ 3, s_1_253, -7, 1, 0}, +{ 3, s_1_254, 0, 1, 0}, +{ 2, s_1_255, 0, 1, 0}, +{ 3, s_1_256, -1, 1, 0}, +{ 3, s_1_257, -2, 1, 0}, +{ 3, s_1_258, 0, 1, 0}, +{ 3, s_1_259, 0, 1, 0}, +{ 6, s_1_260, -1, 1, 0}, +{ 3, s_1_261, 0, 1, 0}, +{ 2, s_1_262, 0, 1, 0}, +{ 2, s_1_263, 0, 1, 0}, +{ 3, s_1_264, -1, 1, 0}, +{ 5, s_1_265, -2, 1, 0}, +{ 5, s_1_266, -3, -1, 0}, +{ 4, s_1_267, -4, 1, 0}, +{ 4, s_1_268, -5, 1, 0}, +{ 3, s_1_269, -6, 1, 0}, +{ 4, s_1_270, -7, 1, 0}, +{ 2, s_1_271, 0, 2, 0}, +{ 3, s_1_272, -1, 1, 0}, +{ 2, s_1_273, 0, 1, 0}, +{ 3, s_1_274, 0, 1, 0}, +{ 2, s_1_275, 0, 1, 0}, +{ 5, s_1_276, -1, 1, 0}, +{ 4, s_1_277, -2, 1, 0}, +{ 4, s_1_278, 0, 1, 0}, +{ 4, s_1_279, 0, 2, 0}, +{ 4, s_1_280, 0, 1, 0}, +{ 3, s_1_281, 0, 1, 0}, +{ 2, s_1_282, 0, 1, 0}, +{ 4, s_1_283, -1, 4, 0}, +{ 5, s_1_284, -2, 1, 0}, +{ 4, s_1_285, -3, 1, 0}, +{ 3, s_1_286, 0, 1, 0}, +{ 2, s_1_287, 0, 1, 0}, +{ 3, s_1_288, -1, 1, 0}, +{ 6, s_1_289, -1, 1, 0}, +{ 1, s_1_290, 0, 1, 0}, +{ 2, s_1_291, -1, 1, 0}, +{ 4, s_1_292, -2, 1, 0}, +{ 2, s_1_293, -3, 1, 0}, +{ 5, s_1_294, -1, 1, 0} }; static const symbol s_2_0[4] = { 'z', 'l', 'e', 'a' }; @@ -865,181 +869,151 @@ static const symbol s_2_15[2] = { 'g', 'o' }; static const symbol s_2_16[2] = { 'r', 'o' }; static const symbol s_2_17[3] = { 'e', 'r', 'o' }; static const symbol s_2_18[2] = { 't', 'o' }; - -static const struct among a_2[19] = -{ -{ 4, s_2_0, -1, 2, 0}, -{ 5, s_2_1, -1, 1, 0}, -{ 2, s_2_2, -1, 1, 0}, -{ 3, s_2_3, -1, 1, 0}, -{ 4, s_2_4, -1, 1, 0}, -{ 4, s_2_5, -1, 1, 0}, -{ 4, s_2_6, -1, 1, 0}, -{ 4, s_2_7, -1, 1, 0}, -{ 2, s_2_8, -1, 1, 0}, -{ 2, s_2_9, -1, 1, 0}, -{ 2, s_2_10, -1, 1, 0}, -{ 5, s_2_11, 10, 1, 0}, -{ 3, s_2_12, 10, 1, 0}, -{ 5, s_2_13, 12, 1, 0}, -{ 4, s_2_14, 10, 1, 0}, -{ 2, s_2_15, -1, 1, 0}, -{ 2, s_2_16, -1, 1, 0}, -{ 3, s_2_17, 16, 1, 0}, -{ 2, s_2_18, -1, 1, 0} +static const struct among a_2[19] = { +{ 4, s_2_0, 0, 2, 0}, +{ 5, s_2_1, 0, 1, 0}, +{ 2, s_2_2, 0, 1, 0}, +{ 3, s_2_3, 0, 1, 0}, +{ 4, s_2_4, 0, 1, 0}, +{ 4, s_2_5, 0, 1, 0}, +{ 4, s_2_6, 0, 1, 0}, +{ 4, s_2_7, 0, 1, 0}, +{ 2, s_2_8, 0, 1, 0}, +{ 2, s_2_9, 0, 1, 0}, +{ 2, s_2_10, 0, 1, 0}, +{ 5, s_2_11, -1, 1, 0}, +{ 3, s_2_12, -2, 1, 0}, +{ 5, s_2_13, -1, 1, 0}, +{ 4, s_2_14, -4, 1, 0}, +{ 2, s_2_15, 0, 1, 0}, +{ 2, s_2_16, 0, 1, 0}, +{ 3, s_2_17, -1, 1, 0}, +{ 2, s_2_18, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16 }; -static const symbol s_0[] = { 'a', 't', 's', 'e', 'd', 'e', 'n' }; -static const symbol s_1[] = { 'a', 'r', 'a', 'b', 'e', 'r', 'a' }; -static const symbol s_2[] = { 'b', 'a', 'd', 'i', 't', 'u' }; -static const symbol s_3[] = { 'j', 'o', 'k' }; -static const symbol s_4[] = { 't', 'r', 'a' }; -static const symbol s_5[] = { 'm', 'i', 'n', 'u', 't', 'u' }; -static const symbol s_6[] = { 'z', 'e', 'h', 'a', 'r' }; -static const symbol s_7[] = { 'g', 'e', 'l', 'd', 'i' }; -static const symbol s_8[] = { 'i', 'g', 'a', 'r', 'o' }; -static const symbol s_9[] = { 'a', 'u', 'r', 'k', 'a' }; -static const symbol s_10[] = { 'z' }; - static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_v, 97, 117, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping_U(z, g_v, 97, 117, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping_U(z, g_v, 97, 117, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping_U(z, g_v, 97, 117, 0)) goto lab2; { int ret = out_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping_U(z, g_v, 97, 117, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping_U(z, g_v, 97, 117, 0)) goto lab1; { int ret = in_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping_U(z, g_v, 97, 117, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping_U(z, g_v, 97, 117, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping_U(z, g_v, 97, 117, 0)) goto lab3; { int ret = out_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping_U(z, g_v, 97, 117, 0)) goto lab0; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 117, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_aditzak(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((70566434 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_0, 109); + among_var = find_among_b(z, a_0, 109, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = slice_from_s(z, 7, s_0); - if (ret < 0) return ret; - } - break; - case 4: - { int ret = slice_from_s(z, 7, s_1); - if (ret < 0) return ret; - } - break; - case 5: - { int ret = slice_from_s(z, 6, s_2); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1051,66 +1025,55 @@ static int r_izenak(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((71162402 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_1, 295); + among_var = find_among_b(z, a_1, 295, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_3); + { + int ret = slice_from_s(z, 3, s_0); if (ret < 0) return ret; } break; case 4: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_4); + { + int ret = slice_from_s(z, 3, s_1); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_5); - if (ret < 0) return ret; - } - break; - case 7: - { int ret = slice_from_s(z, 5, s_6); - if (ret < 0) return ret; - } - break; - case 8: - { int ret = slice_from_s(z, 5, s_7); - if (ret < 0) return ret; - } - break; - case 9: - { int ret = slice_from_s(z, 5, s_8); - if (ret < 0) return ret; - } - break; - case 10: - { int ret = slice_from_s(z, 5, s_9); + { + int ret = slice_from_s(z, 6, s_2); if (ret < 0) return ret; } break; @@ -1122,20 +1085,23 @@ static int r_adjetiboak(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((35362 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_2, 19); + among_var = find_among_b(z, a_2, 19, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; @@ -1144,45 +1110,58 @@ static int r_adjetiboak(struct SN_env * z) { } extern int basque_UTF_8_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - while(1) { - int m1 = z->l - z->c; (void)m1; - { int ret = r_aditzak(z); + while (1) { + int v_1 = z->l - z->c; + { + int ret = r_aditzak(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } continue; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; break; } - while(1) { - int m2 = z->l - z->c; (void)m2; - { int ret = r_izenak(z); + while (1) { + int v_2 = z->l - z->c; + { + int ret = r_izenak(z); if (ret == 0) goto lab1; if (ret < 0) return ret; } continue; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; break; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_adjetiboak(z); + { + int v_3 = z->l - z->c; + { + int ret = r_adjetiboak(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } z->c = z->lb; return 1; } -extern struct SN_env * basque_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * basque_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void basque_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void basque_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_catalan.c b/src/backend/snowball/libstemmer/stem_UTF_8_catalan.c index d261407883b15..8b363e3ea01d9 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_catalan.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_catalan.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from catalan.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_catalan.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int catalan_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_residual_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_standard_suffix(struct SN_env * z); @@ -17,18 +30,18 @@ static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_cleaning(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * catalan_UTF_8_create_env(void); -extern void catalan_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'i' }; +static const symbol s_3[] = { 'o' }; +static const symbol s_4[] = { 'u' }; +static const symbol s_5[] = { '.' }; +static const symbol s_6[] = { 'l', 'o', 'g' }; +static const symbol s_7[] = { 'i', 'c' }; +static const symbol s_8[] = { 'c' }; +static const symbol s_9[] = { 'i', 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 0xC2, 0xB7 }; static const symbol s_0_2[2] = { 0xC3, 0xA0 }; static const symbol s_0_3[2] = { 0xC3, 0xA1 }; @@ -41,22 +54,20 @@ static const symbol s_0_9[2] = { 0xC3, 0xB2 }; static const symbol s_0_10[2] = { 0xC3, 0xB3 }; static const symbol s_0_11[2] = { 0xC3, 0xBA }; static const symbol s_0_12[2] = { 0xC3, 0xBC }; - -static const struct among a_0[13] = -{ -{ 0, 0, -1, 7, 0}, -{ 2, s_0_1, 0, 6, 0}, -{ 2, s_0_2, 0, 1, 0}, -{ 2, s_0_3, 0, 1, 0}, -{ 2, s_0_4, 0, 2, 0}, -{ 2, s_0_5, 0, 2, 0}, -{ 2, s_0_6, 0, 3, 0}, -{ 2, s_0_7, 0, 3, 0}, -{ 2, s_0_8, 0, 3, 0}, -{ 2, s_0_9, 0, 4, 0}, -{ 2, s_0_10, 0, 4, 0}, -{ 2, s_0_11, 0, 5, 0}, -{ 2, s_0_12, 0, 5, 0} +static const struct among a_0[13] = { +{ 0, 0, 0, 7, 0}, +{ 2, s_0_1, -1, 6, 0}, +{ 2, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, 1, 0}, +{ 2, s_0_4, -4, 2, 0}, +{ 2, s_0_5, -5, 2, 0}, +{ 2, s_0_6, -6, 3, 0}, +{ 2, s_0_7, -7, 3, 0}, +{ 2, s_0_8, -8, 3, 0}, +{ 2, s_0_9, -9, 4, 0}, +{ 2, s_0_10, -10, 4, 0}, +{ 2, s_0_11, -11, 5, 0}, +{ 2, s_0_12, -12, 5, 0} }; static const symbol s_1_0[2] = { 'l', 'a' }; @@ -98,48 +109,46 @@ static const symbol s_1_35[3] = { 'v', 'o', 's' }; static const symbol s_1_36[2] = { 'u', 's' }; static const symbol s_1_37[3] = { '-', 'u', 's' }; static const symbol s_1_38[2] = { '\'', 't' }; - -static const struct among a_1[39] = -{ -{ 2, s_1_0, -1, 1, 0}, -{ 3, s_1_1, 0, 1, 0}, -{ 4, s_1_2, 0, 1, 0}, -{ 2, s_1_3, -1, 1, 0}, -{ 2, s_1_4, -1, 1, 0}, -{ 3, s_1_5, 4, 1, 0}, -{ 2, s_1_6, -1, 1, 0}, -{ 3, s_1_7, -1, 1, 0}, -{ 2, s_1_8, -1, 1, 0}, -{ 3, s_1_9, 8, 1, 0}, -{ 2, s_1_10, -1, 1, 0}, -{ 3, s_1_11, 10, 1, 0}, -{ 2, s_1_12, -1, 1, 0}, -{ 2, s_1_13, -1, 1, 0}, -{ 2, s_1_14, -1, 1, 0}, -{ 2, s_1_15, -1, 1, 0}, -{ 2, s_1_16, -1, 1, 0}, -{ 2, s_1_17, -1, 1, 0}, -{ 3, s_1_18, 17, 1, 0}, -{ 2, s_1_19, -1, 1, 0}, -{ 4, s_1_20, 19, 1, 0}, -{ 2, s_1_21, -1, 1, 0}, -{ 3, s_1_22, -1, 1, 0}, -{ 5, s_1_23, 22, 1, 0}, -{ 3, s_1_24, -1, 1, 0}, -{ 4, s_1_25, 24, 1, 0}, -{ 3, s_1_26, -1, 1, 0}, -{ 3, s_1_27, -1, 1, 0}, -{ 3, s_1_28, -1, 1, 0}, -{ 3, s_1_29, -1, 1, 0}, -{ 3, s_1_30, -1, 1, 0}, -{ 3, s_1_31, -1, 1, 0}, -{ 5, s_1_32, 31, 1, 0}, -{ 3, s_1_33, -1, 1, 0}, -{ 4, s_1_34, 33, 1, 0}, -{ 3, s_1_35, -1, 1, 0}, -{ 2, s_1_36, -1, 1, 0}, -{ 3, s_1_37, 36, 1, 0}, -{ 2, s_1_38, -1, 1, 0} +static const struct among a_1[39] = { +{ 2, s_1_0, 0, 1, 0}, +{ 3, s_1_1, -1, 1, 0}, +{ 4, s_1_2, -2, 1, 0}, +{ 2, s_1_3, 0, 1, 0}, +{ 2, s_1_4, 0, 1, 0}, +{ 3, s_1_5, -1, 1, 0}, +{ 2, s_1_6, 0, 1, 0}, +{ 3, s_1_7, 0, 1, 0}, +{ 2, s_1_8, 0, 1, 0}, +{ 3, s_1_9, -1, 1, 0}, +{ 2, s_1_10, 0, 1, 0}, +{ 3, s_1_11, -1, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 2, s_1_13, 0, 1, 0}, +{ 2, s_1_14, 0, 1, 0}, +{ 2, s_1_15, 0, 1, 0}, +{ 2, s_1_16, 0, 1, 0}, +{ 2, s_1_17, 0, 1, 0}, +{ 3, s_1_18, -1, 1, 0}, +{ 2, s_1_19, 0, 1, 0}, +{ 4, s_1_20, -1, 1, 0}, +{ 2, s_1_21, 0, 1, 0}, +{ 3, s_1_22, 0, 1, 0}, +{ 5, s_1_23, -1, 1, 0}, +{ 3, s_1_24, 0, 1, 0}, +{ 4, s_1_25, -1, 1, 0}, +{ 3, s_1_26, 0, 1, 0}, +{ 3, s_1_27, 0, 1, 0}, +{ 3, s_1_28, 0, 1, 0}, +{ 3, s_1_29, 0, 1, 0}, +{ 3, s_1_30, 0, 1, 0}, +{ 3, s_1_31, 0, 1, 0}, +{ 5, s_1_32, -1, 1, 0}, +{ 3, s_1_33, 0, 1, 0}, +{ 4, s_1_34, -1, 1, 0}, +{ 3, s_1_35, 0, 1, 0}, +{ 2, s_1_36, 0, 1, 0}, +{ 3, s_1_37, -1, 1, 0}, +{ 2, s_1_38, 0, 1, 0} }; static const symbol s_2_0[3] = { 'i', 'c', 'a' }; @@ -342,209 +351,207 @@ static const symbol s_2_196[2] = { 0xC3, 0xB3 }; static const symbol s_2_197[3] = { 'i', 0xC3, 0xB3 }; static const symbol s_2_198[4] = { 'c', 'i', 0xC3, 0xB3 }; static const symbol s_2_199[5] = { 'a', 'c', 'i', 0xC3, 0xB3 }; - -static const struct among a_2[200] = -{ -{ 3, s_2_0, -1, 4, 0}, -{ 7, s_2_1, 0, 3, 0}, -{ 4, s_2_2, -1, 1, 0}, -{ 3, s_2_3, -1, 2, 0}, -{ 5, s_2_4, -1, 1, 0}, -{ 5, s_2_5, -1, 1, 0}, -{ 6, s_2_6, -1, 1, 0}, -{ 5, s_2_7, -1, 1, 0}, -{ 5, s_2_8, -1, 3, 0}, -{ 4, s_2_9, -1, 1, 0}, -{ 6, s_2_10, 9, 1, 0}, -{ 4, s_2_11, -1, 1, 0}, -{ 5, s_2_12, -1, 1, 0}, -{ 7, s_2_13, -1, 1, 0}, -{ 4, s_2_14, -1, 1, 0}, -{ 4, s_2_15, -1, 1, 0}, -{ 6, s_2_16, -1, 1, 0}, -{ 3, s_2_17, -1, 1, 0}, -{ 7, s_2_18, 17, 1, 0}, -{ 9, s_2_19, 18, 5, 0}, -{ 3, s_2_20, -1, 1, 0}, -{ 3, s_2_21, -1, 1, 0}, -{ 3, s_2_22, -1, 1, 0}, -{ 5, s_2_23, 22, 1, 0}, -{ 3, s_2_24, -1, 1, 0}, -{ 4, s_2_25, 24, 1, 0}, -{ 5, s_2_26, 25, 1, 0}, -{ 5, s_2_27, -1, 1, 0}, -{ 3, s_2_28, -1, 1, 0}, -{ 3, s_2_29, -1, 1, 0}, -{ 4, s_2_30, -1, 1, 0}, -{ 4, s_2_31, -1, 1, 0}, -{ 4, s_2_32, -1, 1, 0}, -{ 3, s_2_33, -1, 1, 0}, -{ 3, s_2_34, -1, 1, 0}, -{ 3, s_2_35, -1, 1, 0}, -{ 4, s_2_36, -1, 1, 0}, -{ 7, s_2_37, 36, 1, 0}, -{ 7, s_2_38, 36, 1, 0}, -{ 3, s_2_39, -1, 1, 0}, -{ 5, s_2_40, 39, 1, 0}, -{ 4, s_2_41, -1, 1, 0}, -{ 6, s_2_42, -1, 3, 0}, -{ 2, s_2_43, -1, 4, 0}, -{ 6, s_2_44, 43, 1, 0}, -{ 3, s_2_45, -1, 1, 0}, -{ 3, s_2_46, -1, 1, 0}, -{ 2, s_2_47, -1, 1, 0}, -{ 4, s_2_48, -1, 1, 0}, -{ 3, s_2_49, -1, 1, 0}, -{ 4, s_2_50, 49, 1, 0}, -{ 4, s_2_51, 49, 1, 0}, -{ 4, s_2_52, -1, 1, 0}, -{ 7, s_2_53, 52, 1, 0}, -{ 7, s_2_54, 52, 1, 0}, -{ 6, s_2_55, 52, 1, 0}, -{ 4, s_2_56, -1, 1, 0}, -{ 4, s_2_57, -1, 1, 0}, -{ 4, s_2_58, -1, 1, 0}, -{ 3, s_2_59, -1, 1, 0}, -{ 4, s_2_60, -1, 1, 0}, -{ 4, s_2_61, -1, 3, 0}, -{ 3, s_2_62, -1, 1, 0}, -{ 4, s_2_63, -1, 1, 0}, -{ 2, s_2_64, -1, 1, 0}, -{ 2, s_2_65, -1, 1, 0}, -{ 3, s_2_66, -1, 1, 0}, -{ 3, s_2_67, -1, 1, 0}, -{ 5, s_2_68, -1, 1, 0}, -{ 4, s_2_69, -1, 1, 0}, -{ 5, s_2_70, -1, 1, 0}, -{ 6, s_2_71, -1, 1, 0}, -{ 6, s_2_72, -1, 1, 0}, -{ 6, s_2_73, -1, 1, 0}, -{ 8, s_2_74, 73, 5, 0}, -{ 4, s_2_75, -1, 1, 0}, -{ 6, s_2_76, -1, 1, 0}, -{ 2, s_2_77, -1, 1, 0}, -{ 6, s_2_78, 77, 1, 0}, -{ 4, s_2_79, 77, 1, 0}, -{ 4, s_2_80, 77, 1, 0}, -{ 4, s_2_81, 77, 1, 0}, -{ 5, s_2_82, 77, 1, 0}, -{ 3, s_2_83, -1, 1, 0}, -{ 2, s_2_84, -1, 1, 0}, -{ 3, s_2_85, 84, 1, 0}, -{ 3, s_2_86, -1, 1, 0}, -{ 5, s_2_87, -1, 1, 0}, -{ 3, s_2_88, -1, 4, 0}, -{ 7, s_2_89, 88, 3, 0}, -{ 3, s_2_90, -1, 1, 0}, -{ 4, s_2_91, -1, 1, 0}, -{ 4, s_2_92, -1, 2, 0}, -{ 6, s_2_93, -1, 1, 0}, -{ 6, s_2_94, -1, 1, 0}, -{ 7, s_2_95, -1, 1, 0}, -{ 6, s_2_96, -1, 1, 0}, -{ 6, s_2_97, -1, 3, 0}, -{ 5, s_2_98, -1, 1, 0}, -{ 6, s_2_99, -1, 1, 0}, -{ 5, s_2_100, -1, 1, 0}, -{ 6, s_2_101, -1, 1, 0}, -{ 8, s_2_102, -1, 1, 0}, -{ 4, s_2_103, -1, 1, 0}, -{ 5, s_2_104, 103, 1, 0}, -{ 5, s_2_105, 103, 1, 0}, -{ 4, s_2_106, -1, 1, 0}, -{ 8, s_2_107, 106, 1, 0}, -{ 10, s_2_108, 107, 5, 0}, -{ 6, s_2_109, -1, 1, 0}, -{ 5, s_2_110, -1, 1, 0}, -{ 8, s_2_111, 110, 1, 0}, -{ 4, s_2_112, -1, 1, 0}, -{ 4, s_2_113, -1, 1, 0}, -{ 4, s_2_114, -1, 1, 0}, -{ 5, s_2_115, 114, 1, 0}, -{ 6, s_2_116, 115, 1, 0}, -{ 5, s_2_117, -1, 1, 0}, -{ 4, s_2_118, -1, 1, 0}, -{ 4, s_2_119, -1, 1, 0}, -{ 5, s_2_120, -1, 1, 0}, -{ 5, s_2_121, -1, 1, 0}, -{ 4, s_2_122, -1, 1, 0}, -{ 4, s_2_123, -1, 1, 0}, -{ 5, s_2_124, -1, 1, 0}, -{ 8, s_2_125, 124, 1, 0}, -{ 8, s_2_126, 124, 1, 0}, -{ 5, s_2_127, -1, 4, 0}, -{ 9, s_2_128, 127, 3, 0}, -{ 4, s_2_129, -1, 1, 0}, -{ 6, s_2_130, 129, 1, 0}, -{ 7, s_2_131, -1, 3, 0}, -{ 10, s_2_132, -1, 1, 0}, -{ 4, s_2_133, -1, 1, 0}, -{ 5, s_2_134, -1, 1, 0}, -{ 5, s_2_135, -1, 3, 0}, -{ 4, s_2_136, -1, 1, 0}, -{ 5, s_2_137, -1, 1, 0}, -{ 2, s_2_138, -1, 1, 0}, -{ 3, s_2_139, 138, 1, 0}, -{ 4, s_2_140, 138, 1, 0}, -{ 3, s_2_141, -1, 1, 0}, -{ 7, s_2_142, 141, 1, 0}, -{ 9, s_2_143, 142, 5, 0}, -{ 4, s_2_144, -1, 1, 0}, -{ 5, s_2_145, 144, 1, 0}, -{ 6, s_2_146, 145, 2, 0}, -{ 4, s_2_147, -1, 1, 0}, -{ 4, s_2_148, -1, 1, 0}, -{ 5, s_2_149, -1, 1, 0}, -{ 5, s_2_150, -1, 1, 0}, -{ 3, s_2_151, -1, 1, 0}, -{ 3, s_2_152, -1, 1, 0}, -{ 4, s_2_153, 152, 1, 0}, -{ 5, s_2_154, 153, 1, 0}, -{ 5, s_2_155, 153, 1, 0}, -{ 3, s_2_156, -1, 1, 0}, -{ 5, s_2_157, 156, 1, 0}, -{ 8, s_2_158, 157, 1, 0}, -{ 7, s_2_159, 157, 1, 0}, -{ 9, s_2_160, 159, 1, 0}, -{ 6, s_2_161, 156, 1, 0}, -{ 3, s_2_162, -1, 1, 0}, -{ 4, s_2_163, -1, 1, 0}, -{ 4, s_2_164, -1, 1, 0}, -{ 5, s_2_165, 164, 1, 0}, -{ 6, s_2_166, 165, 1, 0}, -{ 3, s_2_167, -1, 1, 0}, -{ 3, s_2_168, -1, 1, 0}, -{ 3, s_2_169, -1, 1, 0}, -{ 5, s_2_170, 169, 1, 0}, -{ 5, s_2_171, 169, 1, 0}, -{ 3, s_2_172, -1, 1, 0}, -{ 3, s_2_173, -1, 1, 0}, -{ 3, s_2_174, -1, 1, 0}, -{ 4, s_2_175, 174, 1, 0}, -{ 3, s_2_176, -1, 1, 0}, -{ 4, s_2_177, -1, 1, 0}, -{ 7, s_2_178, 177, 1, 0}, -{ 6, s_2_179, 177, 1, 0}, -{ 8, s_2_180, 179, 1, 0}, -{ 5, s_2_181, -1, 1, 0}, -{ 2, s_2_182, -1, 1, 0}, -{ 3, s_2_183, -1, 1, 0}, -{ 3, s_2_184, -1, 1, 0}, -{ 4, s_2_185, 184, 1, 0}, -{ 4, s_2_186, 184, 1, 0}, -{ 5, s_2_187, 186, 1, 0}, -{ 7, s_2_188, 187, 1, 0}, -{ 2, s_2_189, -1, 1, 0}, -{ 5, s_2_190, -1, 1, 0}, -{ 6, s_2_191, -1, 1, 0}, -{ 6, s_2_192, -1, 1, 0}, -{ 4, s_2_193, -1, 1, 0}, -{ 6, s_2_194, -1, 1, 0}, -{ 4, s_2_195, -1, 1, 0}, -{ 2, s_2_196, -1, 1, 0}, -{ 3, s_2_197, 196, 1, 0}, -{ 4, s_2_198, 197, 1, 0}, -{ 5, s_2_199, 198, 1, 0} +static const struct among a_2[200] = { +{ 3, s_2_0, 0, 4, 0}, +{ 7, s_2_1, -1, 3, 0}, +{ 4, s_2_2, 0, 1, 0}, +{ 3, s_2_3, 0, 2, 0}, +{ 5, s_2_4, 0, 1, 0}, +{ 5, s_2_5, 0, 1, 0}, +{ 6, s_2_6, 0, 1, 0}, +{ 5, s_2_7, 0, 1, 0}, +{ 5, s_2_8, 0, 3, 0}, +{ 4, s_2_9, 0, 1, 0}, +{ 6, s_2_10, -1, 1, 0}, +{ 4, s_2_11, 0, 1, 0}, +{ 5, s_2_12, 0, 1, 0}, +{ 7, s_2_13, 0, 1, 0}, +{ 4, s_2_14, 0, 1, 0}, +{ 4, s_2_15, 0, 1, 0}, +{ 6, s_2_16, 0, 1, 0}, +{ 3, s_2_17, 0, 1, 0}, +{ 7, s_2_18, -1, 1, 0}, +{ 9, s_2_19, -1, 5, 0}, +{ 3, s_2_20, 0, 1, 0}, +{ 3, s_2_21, 0, 1, 0}, +{ 3, s_2_22, 0, 1, 0}, +{ 5, s_2_23, -1, 1, 0}, +{ 3, s_2_24, 0, 1, 0}, +{ 4, s_2_25, -1, 1, 0}, +{ 5, s_2_26, -1, 1, 0}, +{ 5, s_2_27, 0, 1, 0}, +{ 3, s_2_28, 0, 1, 0}, +{ 3, s_2_29, 0, 1, 0}, +{ 4, s_2_30, 0, 1, 0}, +{ 4, s_2_31, 0, 1, 0}, +{ 4, s_2_32, 0, 1, 0}, +{ 3, s_2_33, 0, 1, 0}, +{ 3, s_2_34, 0, 1, 0}, +{ 3, s_2_35, 0, 1, 0}, +{ 4, s_2_36, 0, 1, 0}, +{ 7, s_2_37, -1, 1, 0}, +{ 7, s_2_38, -2, 1, 0}, +{ 3, s_2_39, 0, 1, 0}, +{ 5, s_2_40, -1, 1, 0}, +{ 4, s_2_41, 0, 1, 0}, +{ 6, s_2_42, 0, 3, 0}, +{ 2, s_2_43, 0, 4, 0}, +{ 6, s_2_44, -1, 1, 0}, +{ 3, s_2_45, 0, 1, 0}, +{ 3, s_2_46, 0, 1, 0}, +{ 2, s_2_47, 0, 1, 0}, +{ 4, s_2_48, 0, 1, 0}, +{ 3, s_2_49, 0, 1, 0}, +{ 4, s_2_50, -1, 1, 0}, +{ 4, s_2_51, -2, 1, 0}, +{ 4, s_2_52, 0, 1, 0}, +{ 7, s_2_53, -1, 1, 0}, +{ 7, s_2_54, -2, 1, 0}, +{ 6, s_2_55, -3, 1, 0}, +{ 4, s_2_56, 0, 1, 0}, +{ 4, s_2_57, 0, 1, 0}, +{ 4, s_2_58, 0, 1, 0}, +{ 3, s_2_59, 0, 1, 0}, +{ 4, s_2_60, 0, 1, 0}, +{ 4, s_2_61, 0, 3, 0}, +{ 3, s_2_62, 0, 1, 0}, +{ 4, s_2_63, 0, 1, 0}, +{ 2, s_2_64, 0, 1, 0}, +{ 2, s_2_65, 0, 1, 0}, +{ 3, s_2_66, 0, 1, 0}, +{ 3, s_2_67, 0, 1, 0}, +{ 5, s_2_68, 0, 1, 0}, +{ 4, s_2_69, 0, 1, 0}, +{ 5, s_2_70, 0, 1, 0}, +{ 6, s_2_71, 0, 1, 0}, +{ 6, s_2_72, 0, 1, 0}, +{ 6, s_2_73, 0, 1, 0}, +{ 8, s_2_74, -1, 5, 0}, +{ 4, s_2_75, 0, 1, 0}, +{ 6, s_2_76, 0, 1, 0}, +{ 2, s_2_77, 0, 1, 0}, +{ 6, s_2_78, -1, 1, 0}, +{ 4, s_2_79, -2, 1, 0}, +{ 4, s_2_80, -3, 1, 0}, +{ 4, s_2_81, -4, 1, 0}, +{ 5, s_2_82, -5, 1, 0}, +{ 3, s_2_83, 0, 1, 0}, +{ 2, s_2_84, 0, 1, 0}, +{ 3, s_2_85, -1, 1, 0}, +{ 3, s_2_86, 0, 1, 0}, +{ 5, s_2_87, 0, 1, 0}, +{ 3, s_2_88, 0, 4, 0}, +{ 7, s_2_89, -1, 3, 0}, +{ 3, s_2_90, 0, 1, 0}, +{ 4, s_2_91, 0, 1, 0}, +{ 4, s_2_92, 0, 2, 0}, +{ 6, s_2_93, 0, 1, 0}, +{ 6, s_2_94, 0, 1, 0}, +{ 7, s_2_95, 0, 1, 0}, +{ 6, s_2_96, 0, 1, 0}, +{ 6, s_2_97, 0, 3, 0}, +{ 5, s_2_98, 0, 1, 0}, +{ 6, s_2_99, 0, 1, 0}, +{ 5, s_2_100, 0, 1, 0}, +{ 6, s_2_101, 0, 1, 0}, +{ 8, s_2_102, 0, 1, 0}, +{ 4, s_2_103, 0, 1, 0}, +{ 5, s_2_104, -1, 1, 0}, +{ 5, s_2_105, -2, 1, 0}, +{ 4, s_2_106, 0, 1, 0}, +{ 8, s_2_107, -1, 1, 0}, +{ 10, s_2_108, -1, 5, 0}, +{ 6, s_2_109, 0, 1, 0}, +{ 5, s_2_110, 0, 1, 0}, +{ 8, s_2_111, -1, 1, 0}, +{ 4, s_2_112, 0, 1, 0}, +{ 4, s_2_113, 0, 1, 0}, +{ 4, s_2_114, 0, 1, 0}, +{ 5, s_2_115, -1, 1, 0}, +{ 6, s_2_116, -1, 1, 0}, +{ 5, s_2_117, 0, 1, 0}, +{ 4, s_2_118, 0, 1, 0}, +{ 4, s_2_119, 0, 1, 0}, +{ 5, s_2_120, 0, 1, 0}, +{ 5, s_2_121, 0, 1, 0}, +{ 4, s_2_122, 0, 1, 0}, +{ 4, s_2_123, 0, 1, 0}, +{ 5, s_2_124, 0, 1, 0}, +{ 8, s_2_125, -1, 1, 0}, +{ 8, s_2_126, -2, 1, 0}, +{ 5, s_2_127, 0, 4, 0}, +{ 9, s_2_128, -1, 3, 0}, +{ 4, s_2_129, 0, 1, 0}, +{ 6, s_2_130, -1, 1, 0}, +{ 7, s_2_131, 0, 3, 0}, +{ 10, s_2_132, 0, 1, 0}, +{ 4, s_2_133, 0, 1, 0}, +{ 5, s_2_134, 0, 1, 0}, +{ 5, s_2_135, 0, 3, 0}, +{ 4, s_2_136, 0, 1, 0}, +{ 5, s_2_137, 0, 1, 0}, +{ 2, s_2_138, 0, 1, 0}, +{ 3, s_2_139, -1, 1, 0}, +{ 4, s_2_140, -2, 1, 0}, +{ 3, s_2_141, 0, 1, 0}, +{ 7, s_2_142, -1, 1, 0}, +{ 9, s_2_143, -1, 5, 0}, +{ 4, s_2_144, 0, 1, 0}, +{ 5, s_2_145, -1, 1, 0}, +{ 6, s_2_146, -1, 2, 0}, +{ 4, s_2_147, 0, 1, 0}, +{ 4, s_2_148, 0, 1, 0}, +{ 5, s_2_149, 0, 1, 0}, +{ 5, s_2_150, 0, 1, 0}, +{ 3, s_2_151, 0, 1, 0}, +{ 3, s_2_152, 0, 1, 0}, +{ 4, s_2_153, -1, 1, 0}, +{ 5, s_2_154, -1, 1, 0}, +{ 5, s_2_155, -2, 1, 0}, +{ 3, s_2_156, 0, 1, 0}, +{ 5, s_2_157, -1, 1, 0}, +{ 8, s_2_158, -1, 1, 0}, +{ 7, s_2_159, -2, 1, 0}, +{ 9, s_2_160, -1, 1, 0}, +{ 6, s_2_161, -5, 1, 0}, +{ 3, s_2_162, 0, 1, 0}, +{ 4, s_2_163, 0, 1, 0}, +{ 4, s_2_164, 0, 1, 0}, +{ 5, s_2_165, -1, 1, 0}, +{ 6, s_2_166, -1, 1, 0}, +{ 3, s_2_167, 0, 1, 0}, +{ 3, s_2_168, 0, 1, 0}, +{ 3, s_2_169, 0, 1, 0}, +{ 5, s_2_170, -1, 1, 0}, +{ 5, s_2_171, -2, 1, 0}, +{ 3, s_2_172, 0, 1, 0}, +{ 3, s_2_173, 0, 1, 0}, +{ 3, s_2_174, 0, 1, 0}, +{ 4, s_2_175, -1, 1, 0}, +{ 3, s_2_176, 0, 1, 0}, +{ 4, s_2_177, 0, 1, 0}, +{ 7, s_2_178, -1, 1, 0}, +{ 6, s_2_179, -2, 1, 0}, +{ 8, s_2_180, -1, 1, 0}, +{ 5, s_2_181, 0, 1, 0}, +{ 2, s_2_182, 0, 1, 0}, +{ 3, s_2_183, 0, 1, 0}, +{ 3, s_2_184, 0, 1, 0}, +{ 4, s_2_185, -1, 1, 0}, +{ 4, s_2_186, -2, 1, 0}, +{ 5, s_2_187, -1, 1, 0}, +{ 7, s_2_188, -1, 1, 0}, +{ 2, s_2_189, 0, 1, 0}, +{ 5, s_2_190, 0, 1, 0}, +{ 6, s_2_191, 0, 1, 0}, +{ 6, s_2_192, 0, 1, 0}, +{ 4, s_2_193, 0, 1, 0}, +{ 6, s_2_194, 0, 1, 0}, +{ 4, s_2_195, 0, 1, 0}, +{ 2, s_2_196, 0, 1, 0}, +{ 3, s_2_197, -1, 1, 0}, +{ 4, s_2_198, -1, 1, 0}, +{ 5, s_2_199, -1, 1, 0} }; static const symbol s_3_0[3] = { 'a', 'b', 'a' }; @@ -830,292 +837,290 @@ static const symbol s_3_279[4] = { 'i', 'r', 0xC3, 0xA9 }; static const symbol s_3_280[2] = { 0xC3, 0xAD }; static const symbol s_3_281[3] = { 'i', 0xC3, 0xAF }; static const symbol s_3_282[3] = { 'i', 0xC3, 0xB3 }; - -static const struct among a_3[283] = -{ -{ 3, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 4, s_3_2, -1, 1, 0}, -{ 5, s_3_3, -1, 1, 0}, -{ 3, s_3_4, -1, 1, 0}, -{ 3, s_3_5, -1, 1, 0}, -{ 3, s_3_6, -1, 1, 0}, -{ 4, s_3_7, -1, 1, 0}, -{ 2, s_3_8, -1, 1, 0}, -{ 4, s_3_9, 8, 1, 0}, -{ 4, s_3_10, 8, 1, 0}, -{ 3, s_3_11, -1, 1, 0}, -{ 4, s_3_12, -1, 1, 0}, -{ 3, s_3_13, -1, 1, 0}, -{ 5, s_3_14, -1, 1, 0}, -{ 4, s_3_15, -1, 1, 0}, -{ 3, s_3_16, -1, 1, 0}, -{ 3, s_3_17, -1, 1, 0}, -{ 4, s_3_18, -1, 1, 0}, -{ 3, s_3_19, -1, 1, 0}, -{ 5, s_3_20, 19, 1, 0}, -{ 5, s_3_21, 19, 1, 0}, -{ 5, s_3_22, 19, 1, 0}, -{ 3, s_3_23, -1, 1, 0}, -{ 3, s_3_24, -1, 1, 0}, -{ 4, s_3_25, -1, 1, 0}, -{ 2, s_3_26, -1, 1, 0}, -{ 2, s_3_27, -1, 1, 0}, -{ 2, s_3_28, -1, 1, 0}, -{ 2, s_3_29, -1, 1, 0}, -{ 2, s_3_30, -1, 1, 0}, -{ 3, s_3_31, 30, 1, 0}, -{ 3, s_3_32, -1, 1, 0}, -{ 4, s_3_33, -1, 1, 0}, -{ 4, s_3_34, -1, 1, 0}, -{ 4, s_3_35, -1, 1, 0}, -{ 2, s_3_36, -1, 1, 0}, -{ 3, s_3_37, -1, 1, 0}, -{ 5, s_3_38, -1, 1, 0}, -{ 4, s_3_39, -1, 1, 0}, -{ 4, s_3_40, -1, 1, 0}, -{ 2, s_3_41, -1, 1, 0}, -{ 2, s_3_42, -1, 1, 0}, -{ 4, s_3_43, 42, 1, 0}, -{ 4, s_3_44, 42, 1, 0}, -{ 5, s_3_45, 42, 1, 0}, -{ 5, s_3_46, 42, 1, 0}, -{ 6, s_3_47, 42, 1, 0}, -{ 6, s_3_48, 42, 1, 0}, -{ 5, s_3_49, 42, 1, 0}, -{ 6, s_3_50, 42, 1, 0}, -{ 4, s_3_51, 42, 1, 0}, -{ 5, s_3_52, 42, 1, 0}, -{ 5, s_3_53, 42, 1, 0}, -{ 6, s_3_54, 42, 1, 0}, -{ 4, s_3_55, 42, 1, 0}, -{ 6, s_3_56, 55, 1, 0}, -{ 6, s_3_57, 55, 1, 0}, -{ 5, s_3_58, -1, 1, 0}, -{ 5, s_3_59, -1, 1, 0}, -{ 5, s_3_60, -1, 1, 0}, -{ 6, s_3_61, -1, 1, 0}, -{ 6, s_3_62, -1, 1, 0}, -{ 6, s_3_63, -1, 1, 0}, -{ 6, s_3_64, -1, 1, 0}, -{ 3, s_3_65, -1, 1, 0}, -{ 2, s_3_66, -1, 1, 0}, -{ 4, s_3_67, 66, 1, 0}, -{ 5, s_3_68, 66, 1, 0}, -{ 4, s_3_69, 66, 1, 0}, -{ 5, s_3_70, 66, 1, 0}, -{ 4, s_3_71, 66, 1, 0}, -{ 4, s_3_72, 66, 1, 0}, -{ 6, s_3_73, 72, 1, 0}, -{ 6, s_3_74, 72, 1, 0}, -{ 6, s_3_75, 72, 1, 0}, -{ 2, s_3_76, -1, 1, 0}, -{ 3, s_3_77, 76, 1, 0}, -{ 5, s_3_78, 77, 1, 0}, -{ 5, s_3_79, 77, 1, 0}, -{ 4, s_3_80, 76, 1, 0}, -{ 4, s_3_81, 76, 1, 0}, -{ 4, s_3_82, 76, 1, 0}, -{ 5, s_3_83, 76, 1, 0}, -{ 5, s_3_84, 76, 1, 0}, -{ 4, s_3_85, 76, 1, 0}, -{ 5, s_3_86, 76, 1, 0}, -{ 5, s_3_87, 76, 1, 0}, -{ 5, s_3_88, 76, 1, 0}, -{ 5, s_3_89, 76, 1, 0}, -{ 6, s_3_90, 76, 1, 0}, -{ 6, s_3_91, 76, 1, 0}, -{ 6, s_3_92, 76, 1, 0}, -{ 6, s_3_93, 76, 1, 0}, -{ 7, s_3_94, 76, 1, 0}, -{ 4, s_3_95, 76, 1, 0}, -{ 4, s_3_96, 76, 1, 0}, -{ 5, s_3_97, 96, 1, 0}, -{ 5, s_3_98, 76, 1, 0}, -{ 4, s_3_99, 76, 1, 0}, -{ 2, s_3_100, -1, 1, 0}, -{ 4, s_3_101, 100, 1, 0}, -{ 3, s_3_102, 100, 1, 0}, -{ 4, s_3_103, 102, 1, 0}, -{ 5, s_3_104, 102, 1, 0}, -{ 5, s_3_105, 102, 1, 0}, -{ 5, s_3_106, 102, 1, 0}, -{ 6, s_3_107, 102, 1, 0}, -{ 6, s_3_108, 100, 1, 0}, -{ 5, s_3_109, 100, 1, 0}, -{ 4, s_3_110, -1, 1, 0}, -{ 5, s_3_111, -1, 1, 0}, -{ 5, s_3_112, -1, 1, 0}, -{ 5, s_3_113, -1, 1, 0}, -{ 5, s_3_114, -1, 1, 0}, -{ 4, s_3_115, -1, 1, 0}, -{ 3, s_3_116, -1, 1, 0}, -{ 3, s_3_117, -1, 1, 0}, -{ 4, s_3_118, -1, 2, 0}, -{ 5, s_3_119, -1, 1, 0}, -{ 2, s_3_120, -1, 1, 0}, -{ 3, s_3_121, -1, 1, 0}, -{ 4, s_3_122, 121, 1, 0}, -{ 4, s_3_123, -1, 1, 0}, -{ 4, s_3_124, -1, 1, 0}, -{ 2, s_3_125, -1, 1, 0}, -{ 4, s_3_126, 125, 1, 0}, -{ 2, s_3_127, -1, 1, 0}, -{ 5, s_3_128, 127, 1, 0}, -{ 2, s_3_129, -1, 1, 0}, -{ 4, s_3_130, -1, 1, 0}, -{ 2, s_3_131, -1, 1, 0}, -{ 4, s_3_132, 131, 1, 0}, -{ 4, s_3_133, 131, 1, 0}, -{ 4, s_3_134, 131, 1, 0}, -{ 4, s_3_135, 131, 1, 0}, -{ 5, s_3_136, 131, 1, 0}, -{ 4, s_3_137, 131, 1, 0}, -{ 6, s_3_138, 137, 1, 0}, -{ 6, s_3_139, 137, 1, 0}, -{ 6, s_3_140, 137, 1, 0}, -{ 3, s_3_141, -1, 1, 0}, -{ 2, s_3_142, -1, 1, 0}, -{ 4, s_3_143, 142, 1, 0}, -{ 4, s_3_144, 142, 1, 0}, -{ 4, s_3_145, 142, 1, 0}, -{ 5, s_3_146, 142, 1, 0}, -{ 5, s_3_147, 142, 1, 0}, -{ 3, s_3_148, 142, 1, 0}, -{ 5, s_3_149, 148, 1, 0}, -{ 5, s_3_150, 148, 1, 0}, -{ 4, s_3_151, 142, 1, 0}, -{ 4, s_3_152, 142, 1, 0}, -{ 6, s_3_153, 142, 1, 0}, -{ 5, s_3_154, 142, 1, 0}, -{ 4, s_3_155, 142, 1, 0}, -{ 5, s_3_156, 142, 1, 0}, -{ 5, s_3_157, 142, 1, 0}, -{ 5, s_3_158, 142, 1, 0}, -{ 5, s_3_159, 142, 1, 0}, -{ 6, s_3_160, 142, 1, 0}, -{ 4, s_3_161, 142, 1, 0}, -{ 6, s_3_162, 161, 1, 0}, -{ 7, s_3_163, 161, 1, 0}, -{ 4, s_3_164, 142, 1, 0}, -{ 4, s_3_165, 142, 1, 0}, -{ 5, s_3_166, 165, 1, 0}, -{ 5, s_3_167, 142, 1, 0}, -{ 4, s_3_168, 142, 1, 0}, -{ 5, s_3_169, -1, 1, 0}, -{ 5, s_3_170, -1, 1, 0}, -{ 6, s_3_171, -1, 1, 0}, -{ 5, s_3_172, -1, 1, 0}, -{ 7, s_3_173, 172, 1, 0}, -{ 7, s_3_174, 172, 1, 0}, -{ 7, s_3_175, 172, 1, 0}, -{ 5, s_3_176, -1, 1, 0}, -{ 6, s_3_177, -1, 1, 0}, -{ 6, s_3_178, -1, 1, 0}, -{ 6, s_3_179, -1, 1, 0}, -{ 4, s_3_180, -1, 1, 0}, -{ 3, s_3_181, -1, 1, 0}, -{ 4, s_3_182, 181, 1, 0}, -{ 5, s_3_183, 181, 1, 0}, -{ 5, s_3_184, 181, 1, 0}, -{ 5, s_3_185, 181, 1, 0}, -{ 6, s_3_186, 181, 1, 0}, -{ 6, s_3_187, -1, 1, 0}, -{ 5, s_3_188, -1, 1, 0}, -{ 5, s_3_189, -1, 1, 0}, -{ 4, s_3_190, -1, 1, 0}, -{ 6, s_3_191, -1, 1, 0}, -{ 6, s_3_192, -1, 1, 0}, -{ 6, s_3_193, -1, 1, 0}, -{ 3, s_3_194, -1, 1, 0}, -{ 4, s_3_195, -1, 1, 0}, -{ 4, s_3_196, -1, 1, 0}, -{ 4, s_3_197, -1, 1, 0}, -{ 7, s_3_198, 197, 1, 0}, -{ 7, s_3_199, 197, 1, 0}, -{ 8, s_3_200, 197, 1, 0}, -{ 6, s_3_201, 197, 1, 0}, -{ 8, s_3_202, 201, 1, 0}, -{ 8, s_3_203, 201, 1, 0}, -{ 8, s_3_204, 201, 1, 0}, -{ 6, s_3_205, -1, 1, 0}, -{ 6, s_3_206, -1, 1, 0}, -{ 6, s_3_207, -1, 1, 0}, -{ 7, s_3_208, -1, 1, 0}, -{ 8, s_3_209, -1, 1, 0}, -{ 4, s_3_210, -1, 1, 0}, -{ 5, s_3_211, -1, 1, 0}, -{ 3, s_3_212, -1, 1, 0}, -{ 5, s_3_213, 212, 1, 0}, -{ 3, s_3_214, -1, 1, 0}, -{ 3, s_3_215, -1, 1, 0}, -{ 3, s_3_216, -1, 1, 0}, -{ 4, s_3_217, -1, 1, 0}, -{ 3, s_3_218, -1, 1, 0}, -{ 5, s_3_219, 218, 1, 0}, -{ 5, s_3_220, 218, 1, 0}, -{ 5, s_3_221, -1, 1, 0}, -{ 5, s_3_222, -1, 1, 0}, -{ 5, s_3_223, -1, 1, 0}, -{ 3, s_3_224, -1, 1, 0}, -{ 5, s_3_225, 224, 1, 0}, -{ 3, s_3_226, -1, 1, 0}, -{ 4, s_3_227, -1, 1, 0}, -{ 2, s_3_228, -1, 1, 0}, -{ 2, s_3_229, -1, 1, 0}, -{ 3, s_3_230, -1, 1, 0}, -{ 3, s_3_231, -1, 1, 0}, -{ 3, s_3_232, -1, 1, 0}, -{ 2, s_3_233, -1, 1, 0}, -{ 3, s_3_234, -1, 1, 0}, -{ 2, s_3_235, -1, 1, 0}, -{ 4, s_3_236, 235, 1, 0}, -{ 3, s_3_237, -1, 1, 0}, -{ 4, s_3_238, -1, 1, 0}, -{ 4, s_3_239, -1, 1, 0}, -{ 4, s_3_240, -1, 1, 0}, -{ 5, s_3_241, -1, 1, 0}, -{ 5, s_3_242, -1, 1, 0}, -{ 5, s_3_243, -1, 1, 0}, -{ 5, s_3_244, -1, 1, 0}, -{ 7, s_3_245, 244, 1, 0}, -{ 6, s_3_246, -1, 1, 0}, -{ 6, s_3_247, -1, 1, 0}, -{ 5, s_3_248, -1, 1, 0}, -{ 6, s_3_249, -1, 1, 0}, -{ 5, s_3_250, -1, 1, 0}, -{ 5, s_3_251, -1, 1, 0}, -{ 5, s_3_252, -1, 1, 0}, -{ 4, s_3_253, -1, 1, 0}, -{ 6, s_3_254, 253, 1, 0}, -{ 4, s_3_255, -1, 1, 0}, -{ 6, s_3_256, 255, 1, 0}, -{ 6, s_3_257, 255, 1, 0}, -{ 5, s_3_258, -1, 1, 0}, -{ 5, s_3_259, -1, 1, 0}, -{ 6, s_3_260, -1, 1, 0}, -{ 6, s_3_261, -1, 1, 0}, -{ 6, s_3_262, -1, 1, 0}, -{ 6, s_3_263, -1, 1, 0}, -{ 3, s_3_264, -1, 1, 0}, -{ 2, s_3_265, -1, 1, 0}, -{ 3, s_3_266, 265, 1, 0}, -{ 3, s_3_267, -1, 1, 0}, -{ 3, s_3_268, -1, 1, 0}, -{ 3, s_3_269, -1, 1, 0}, -{ 4, s_3_270, -1, 1, 0}, -{ 4, s_3_271, -1, 1, 0}, -{ 5, s_3_272, -1, 1, 0}, -{ 4, s_3_273, -1, 1, 0}, -{ 4, s_3_274, -1, 1, 0}, -{ 4, s_3_275, -1, 1, 0}, -{ 4, s_3_276, -1, 1, 0}, -{ 4, s_3_277, -1, 1, 0}, -{ 4, s_3_278, -1, 1, 0}, -{ 4, s_3_279, -1, 1, 0}, -{ 2, s_3_280, -1, 1, 0}, -{ 3, s_3_281, -1, 1, 0}, -{ 3, s_3_282, -1, 1, 0} +static const struct among a_3[283] = { +{ 3, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 4, s_3_2, 0, 1, 0}, +{ 5, s_3_3, 0, 1, 0}, +{ 3, s_3_4, 0, 1, 0}, +{ 3, s_3_5, 0, 1, 0}, +{ 3, s_3_6, 0, 1, 0}, +{ 4, s_3_7, 0, 1, 0}, +{ 2, s_3_8, 0, 1, 0}, +{ 4, s_3_9, -1, 1, 0}, +{ 4, s_3_10, -2, 1, 0}, +{ 3, s_3_11, 0, 1, 0}, +{ 4, s_3_12, 0, 1, 0}, +{ 3, s_3_13, 0, 1, 0}, +{ 5, s_3_14, 0, 1, 0}, +{ 4, s_3_15, 0, 1, 0}, +{ 3, s_3_16, 0, 1, 0}, +{ 3, s_3_17, 0, 1, 0}, +{ 4, s_3_18, 0, 1, 0}, +{ 3, s_3_19, 0, 1, 0}, +{ 5, s_3_20, -1, 1, 0}, +{ 5, s_3_21, -2, 1, 0}, +{ 5, s_3_22, -3, 1, 0}, +{ 3, s_3_23, 0, 1, 0}, +{ 3, s_3_24, 0, 1, 0}, +{ 4, s_3_25, 0, 1, 0}, +{ 2, s_3_26, 0, 1, 0}, +{ 2, s_3_27, 0, 1, 0}, +{ 2, s_3_28, 0, 1, 0}, +{ 2, s_3_29, 0, 1, 0}, +{ 2, s_3_30, 0, 1, 0}, +{ 3, s_3_31, -1, 1, 0}, +{ 3, s_3_32, 0, 1, 0}, +{ 4, s_3_33, 0, 1, 0}, +{ 4, s_3_34, 0, 1, 0}, +{ 4, s_3_35, 0, 1, 0}, +{ 2, s_3_36, 0, 1, 0}, +{ 3, s_3_37, 0, 1, 0}, +{ 5, s_3_38, 0, 1, 0}, +{ 4, s_3_39, 0, 1, 0}, +{ 4, s_3_40, 0, 1, 0}, +{ 2, s_3_41, 0, 1, 0}, +{ 2, s_3_42, 0, 1, 0}, +{ 4, s_3_43, -1, 1, 0}, +{ 4, s_3_44, -2, 1, 0}, +{ 5, s_3_45, -3, 1, 0}, +{ 5, s_3_46, -4, 1, 0}, +{ 6, s_3_47, -5, 1, 0}, +{ 6, s_3_48, -6, 1, 0}, +{ 5, s_3_49, -7, 1, 0}, +{ 6, s_3_50, -8, 1, 0}, +{ 4, s_3_51, -9, 1, 0}, +{ 5, s_3_52, -10, 1, 0}, +{ 5, s_3_53, -11, 1, 0}, +{ 6, s_3_54, -12, 1, 0}, +{ 4, s_3_55, -13, 1, 0}, +{ 6, s_3_56, -1, 1, 0}, +{ 6, s_3_57, -2, 1, 0}, +{ 5, s_3_58, 0, 1, 0}, +{ 5, s_3_59, 0, 1, 0}, +{ 5, s_3_60, 0, 1, 0}, +{ 6, s_3_61, 0, 1, 0}, +{ 6, s_3_62, 0, 1, 0}, +{ 6, s_3_63, 0, 1, 0}, +{ 6, s_3_64, 0, 1, 0}, +{ 3, s_3_65, 0, 1, 0}, +{ 2, s_3_66, 0, 1, 0}, +{ 4, s_3_67, -1, 1, 0}, +{ 5, s_3_68, -2, 1, 0}, +{ 4, s_3_69, -3, 1, 0}, +{ 5, s_3_70, -4, 1, 0}, +{ 4, s_3_71, -5, 1, 0}, +{ 4, s_3_72, -6, 1, 0}, +{ 6, s_3_73, -1, 1, 0}, +{ 6, s_3_74, -2, 1, 0}, +{ 6, s_3_75, -3, 1, 0}, +{ 2, s_3_76, 0, 1, 0}, +{ 3, s_3_77, -1, 1, 0}, +{ 5, s_3_78, -1, 1, 0}, +{ 5, s_3_79, -2, 1, 0}, +{ 4, s_3_80, -4, 1, 0}, +{ 4, s_3_81, -5, 1, 0}, +{ 4, s_3_82, -6, 1, 0}, +{ 5, s_3_83, -7, 1, 0}, +{ 5, s_3_84, -8, 1, 0}, +{ 4, s_3_85, -9, 1, 0}, +{ 5, s_3_86, -10, 1, 0}, +{ 5, s_3_87, -11, 1, 0}, +{ 5, s_3_88, -12, 1, 0}, +{ 5, s_3_89, -13, 1, 0}, +{ 6, s_3_90, -14, 1, 0}, +{ 6, s_3_91, -15, 1, 0}, +{ 6, s_3_92, -16, 1, 0}, +{ 6, s_3_93, -17, 1, 0}, +{ 7, s_3_94, -18, 1, 0}, +{ 4, s_3_95, -19, 1, 0}, +{ 4, s_3_96, -20, 1, 0}, +{ 5, s_3_97, -1, 1, 0}, +{ 5, s_3_98, -22, 1, 0}, +{ 4, s_3_99, -23, 1, 0}, +{ 2, s_3_100, 0, 1, 0}, +{ 4, s_3_101, -1, 1, 0}, +{ 3, s_3_102, -2, 1, 0}, +{ 4, s_3_103, -1, 1, 0}, +{ 5, s_3_104, -2, 1, 0}, +{ 5, s_3_105, -3, 1, 0}, +{ 5, s_3_106, -4, 1, 0}, +{ 6, s_3_107, -5, 1, 0}, +{ 6, s_3_108, -8, 1, 0}, +{ 5, s_3_109, -9, 1, 0}, +{ 4, s_3_110, 0, 1, 0}, +{ 5, s_3_111, 0, 1, 0}, +{ 5, s_3_112, 0, 1, 0}, +{ 5, s_3_113, 0, 1, 0}, +{ 5, s_3_114, 0, 1, 0}, +{ 4, s_3_115, 0, 1, 0}, +{ 3, s_3_116, 0, 1, 0}, +{ 3, s_3_117, 0, 1, 0}, +{ 4, s_3_118, 0, 2, 0}, +{ 5, s_3_119, 0, 1, 0}, +{ 2, s_3_120, 0, 1, 0}, +{ 3, s_3_121, 0, 1, 0}, +{ 4, s_3_122, -1, 1, 0}, +{ 4, s_3_123, 0, 1, 0}, +{ 4, s_3_124, 0, 1, 0}, +{ 2, s_3_125, 0, 1, 0}, +{ 4, s_3_126, -1, 1, 0}, +{ 2, s_3_127, 0, 1, 0}, +{ 5, s_3_128, -1, 1, 0}, +{ 2, s_3_129, 0, 1, 0}, +{ 4, s_3_130, 0, 1, 0}, +{ 2, s_3_131, 0, 1, 0}, +{ 4, s_3_132, -1, 1, 0}, +{ 4, s_3_133, -2, 1, 0}, +{ 4, s_3_134, -3, 1, 0}, +{ 4, s_3_135, -4, 1, 0}, +{ 5, s_3_136, -5, 1, 0}, +{ 4, s_3_137, -6, 1, 0}, +{ 6, s_3_138, -1, 1, 0}, +{ 6, s_3_139, -2, 1, 0}, +{ 6, s_3_140, -3, 1, 0}, +{ 3, s_3_141, 0, 1, 0}, +{ 2, s_3_142, 0, 1, 0}, +{ 4, s_3_143, -1, 1, 0}, +{ 4, s_3_144, -2, 1, 0}, +{ 4, s_3_145, -3, 1, 0}, +{ 5, s_3_146, -4, 1, 0}, +{ 5, s_3_147, -5, 1, 0}, +{ 3, s_3_148, -6, 1, 0}, +{ 5, s_3_149, -1, 1, 0}, +{ 5, s_3_150, -2, 1, 0}, +{ 4, s_3_151, -9, 1, 0}, +{ 4, s_3_152, -10, 1, 0}, +{ 6, s_3_153, -11, 1, 0}, +{ 5, s_3_154, -12, 1, 0}, +{ 4, s_3_155, -13, 1, 0}, +{ 5, s_3_156, -14, 1, 0}, +{ 5, s_3_157, -15, 1, 0}, +{ 5, s_3_158, -16, 1, 0}, +{ 5, s_3_159, -17, 1, 0}, +{ 6, s_3_160, -18, 1, 0}, +{ 4, s_3_161, -19, 1, 0}, +{ 6, s_3_162, -1, 1, 0}, +{ 7, s_3_163, -2, 1, 0}, +{ 4, s_3_164, -22, 1, 0}, +{ 4, s_3_165, -23, 1, 0}, +{ 5, s_3_166, -1, 1, 0}, +{ 5, s_3_167, -25, 1, 0}, +{ 4, s_3_168, -26, 1, 0}, +{ 5, s_3_169, 0, 1, 0}, +{ 5, s_3_170, 0, 1, 0}, +{ 6, s_3_171, 0, 1, 0}, +{ 5, s_3_172, 0, 1, 0}, +{ 7, s_3_173, -1, 1, 0}, +{ 7, s_3_174, -2, 1, 0}, +{ 7, s_3_175, -3, 1, 0}, +{ 5, s_3_176, 0, 1, 0}, +{ 6, s_3_177, 0, 1, 0}, +{ 6, s_3_178, 0, 1, 0}, +{ 6, s_3_179, 0, 1, 0}, +{ 4, s_3_180, 0, 1, 0}, +{ 3, s_3_181, 0, 1, 0}, +{ 4, s_3_182, -1, 1, 0}, +{ 5, s_3_183, -2, 1, 0}, +{ 5, s_3_184, -3, 1, 0}, +{ 5, s_3_185, -4, 1, 0}, +{ 6, s_3_186, -5, 1, 0}, +{ 6, s_3_187, 0, 1, 0}, +{ 5, s_3_188, 0, 1, 0}, +{ 5, s_3_189, 0, 1, 0}, +{ 4, s_3_190, 0, 1, 0}, +{ 6, s_3_191, 0, 1, 0}, +{ 6, s_3_192, 0, 1, 0}, +{ 6, s_3_193, 0, 1, 0}, +{ 3, s_3_194, 0, 1, 0}, +{ 4, s_3_195, 0, 1, 0}, +{ 4, s_3_196, 0, 1, 0}, +{ 4, s_3_197, 0, 1, 0}, +{ 7, s_3_198, -1, 1, 0}, +{ 7, s_3_199, -2, 1, 0}, +{ 8, s_3_200, -3, 1, 0}, +{ 6, s_3_201, -4, 1, 0}, +{ 8, s_3_202, -1, 1, 0}, +{ 8, s_3_203, -2, 1, 0}, +{ 8, s_3_204, -3, 1, 0}, +{ 6, s_3_205, 0, 1, 0}, +{ 6, s_3_206, 0, 1, 0}, +{ 6, s_3_207, 0, 1, 0}, +{ 7, s_3_208, 0, 1, 0}, +{ 8, s_3_209, 0, 1, 0}, +{ 4, s_3_210, 0, 1, 0}, +{ 5, s_3_211, 0, 1, 0}, +{ 3, s_3_212, 0, 1, 0}, +{ 5, s_3_213, -1, 1, 0}, +{ 3, s_3_214, 0, 1, 0}, +{ 3, s_3_215, 0, 1, 0}, +{ 3, s_3_216, 0, 1, 0}, +{ 4, s_3_217, 0, 1, 0}, +{ 3, s_3_218, 0, 1, 0}, +{ 5, s_3_219, -1, 1, 0}, +{ 5, s_3_220, -2, 1, 0}, +{ 5, s_3_221, 0, 1, 0}, +{ 5, s_3_222, 0, 1, 0}, +{ 5, s_3_223, 0, 1, 0}, +{ 3, s_3_224, 0, 1, 0}, +{ 5, s_3_225, -1, 1, 0}, +{ 3, s_3_226, 0, 1, 0}, +{ 4, s_3_227, 0, 1, 0}, +{ 2, s_3_228, 0, 1, 0}, +{ 2, s_3_229, 0, 1, 0}, +{ 3, s_3_230, 0, 1, 0}, +{ 3, s_3_231, 0, 1, 0}, +{ 3, s_3_232, 0, 1, 0}, +{ 2, s_3_233, 0, 1, 0}, +{ 3, s_3_234, 0, 1, 0}, +{ 2, s_3_235, 0, 1, 0}, +{ 4, s_3_236, -1, 1, 0}, +{ 3, s_3_237, 0, 1, 0}, +{ 4, s_3_238, 0, 1, 0}, +{ 4, s_3_239, 0, 1, 0}, +{ 4, s_3_240, 0, 1, 0}, +{ 5, s_3_241, 0, 1, 0}, +{ 5, s_3_242, 0, 1, 0}, +{ 5, s_3_243, 0, 1, 0}, +{ 5, s_3_244, 0, 1, 0}, +{ 7, s_3_245, -1, 1, 0}, +{ 6, s_3_246, 0, 1, 0}, +{ 6, s_3_247, 0, 1, 0}, +{ 5, s_3_248, 0, 1, 0}, +{ 6, s_3_249, 0, 1, 0}, +{ 5, s_3_250, 0, 1, 0}, +{ 5, s_3_251, 0, 1, 0}, +{ 5, s_3_252, 0, 1, 0}, +{ 4, s_3_253, 0, 1, 0}, +{ 6, s_3_254, -1, 1, 0}, +{ 4, s_3_255, 0, 1, 0}, +{ 6, s_3_256, -1, 1, 0}, +{ 6, s_3_257, -2, 1, 0}, +{ 5, s_3_258, 0, 1, 0}, +{ 5, s_3_259, 0, 1, 0}, +{ 6, s_3_260, 0, 1, 0}, +{ 6, s_3_261, 0, 1, 0}, +{ 6, s_3_262, 0, 1, 0}, +{ 6, s_3_263, 0, 1, 0}, +{ 3, s_3_264, 0, 1, 0}, +{ 2, s_3_265, 0, 1, 0}, +{ 3, s_3_266, -1, 1, 0}, +{ 3, s_3_267, 0, 1, 0}, +{ 3, s_3_268, 0, 1, 0}, +{ 3, s_3_269, 0, 1, 0}, +{ 4, s_3_270, 0, 1, 0}, +{ 4, s_3_271, 0, 1, 0}, +{ 5, s_3_272, 0, 1, 0}, +{ 4, s_3_273, 0, 1, 0}, +{ 4, s_3_274, 0, 1, 0}, +{ 4, s_3_275, 0, 1, 0}, +{ 4, s_3_276, 0, 1, 0}, +{ 4, s_3_277, 0, 1, 0}, +{ 4, s_3_278, 0, 1, 0}, +{ 4, s_3_279, 0, 1, 0}, +{ 2, s_3_280, 0, 1, 0}, +{ 3, s_3_281, 0, 1, 0}, +{ 3, s_3_282, 0, 1, 0} }; static const symbol s_4_0[1] = { 'a' }; @@ -1140,123 +1145,114 @@ static const symbol s_4_18[2] = { 0xC3, 0xAC }; static const symbol s_4_19[2] = { 0xC3, 0xAD }; static const symbol s_4_20[2] = { 0xC3, 0xAF }; static const symbol s_4_21[2] = { 0xC3, 0xB3 }; - -static const struct among a_4[22] = -{ -{ 1, s_4_0, -1, 1, 0}, -{ 1, s_4_1, -1, 1, 0}, -{ 1, s_4_2, -1, 1, 0}, -{ 3, s_4_3, -1, 1, 0}, -{ 1, s_4_4, -1, 1, 0}, -{ 2, s_4_5, -1, 1, 0}, -{ 1, s_4_6, -1, 1, 0}, -{ 2, s_4_7, 6, 1, 0}, -{ 2, s_4_8, 6, 1, 0}, -{ 3, s_4_9, 6, 1, 0}, -{ 2, s_4_10, -1, 1, 0}, -{ 2, s_4_11, -1, 1, 0}, -{ 2, s_4_12, -1, 1, 0}, -{ 3, s_4_13, -1, 2, 0}, -{ 3, s_4_14, -1, 1, 0}, -{ 2, s_4_15, -1, 1, 0}, -{ 2, s_4_16, -1, 1, 0}, -{ 2, s_4_17, -1, 1, 0}, -{ 2, s_4_18, -1, 1, 0}, -{ 2, s_4_19, -1, 1, 0}, -{ 2, s_4_20, -1, 1, 0}, -{ 2, s_4_21, -1, 1, 0} +static const struct among a_4[22] = { +{ 1, s_4_0, 0, 1, 0}, +{ 1, s_4_1, 0, 1, 0}, +{ 1, s_4_2, 0, 1, 0}, +{ 3, s_4_3, 0, 1, 0}, +{ 1, s_4_4, 0, 1, 0}, +{ 2, s_4_5, 0, 1, 0}, +{ 1, s_4_6, 0, 1, 0}, +{ 2, s_4_7, -1, 1, 0}, +{ 2, s_4_8, -2, 1, 0}, +{ 3, s_4_9, -3, 1, 0}, +{ 2, s_4_10, 0, 1, 0}, +{ 2, s_4_11, 0, 1, 0}, +{ 2, s_4_12, 0, 1, 0}, +{ 3, s_4_13, 0, 2, 0}, +{ 3, s_4_14, 0, 1, 0}, +{ 2, s_4_15, 0, 1, 0}, +{ 2, s_4_16, 0, 1, 0}, +{ 2, s_4_17, 0, 1, 0}, +{ 2, s_4_18, 0, 1, 0}, +{ 2, s_4_19, 0, 1, 0}, +{ 2, s_4_20, 0, 1, 0}, +{ 2, s_4_21, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 129, 81, 6, 10 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'i' }; -static const symbol s_3[] = { 'o' }; -static const symbol s_4[] = { 'u' }; -static const symbol s_5[] = { '.' }; -static const symbol s_6[] = { 'l', 'o', 'g' }; -static const symbol s_7[] = { 'i', 'c' }; -static const symbol s_8[] = { 'c' }; -static const symbol s_9[] = { 'i', 'c' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_cleaning(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 5 || !((344765187 >> (z->p[z->c + 1] & 0x1f)) & 1)) among_var = 7; else - among_var = find_among(z, a_0, 13); + among_var = find_among(z, a_0, 13, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 7: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -1264,29 +1260,31 @@ static int r_cleaning(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_attached_pronoun(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1634850 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_1, 39)) return 0; + if (!find_among_b(z, a_1, 39, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1295,47 +1293,57 @@ static int r_attached_pronoun(struct SN_env * z) { static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_2, 200); + among_var = find_among_b(z, a_2, 200, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_6); + { + int ret = slice_from_s(z, 3, s_6); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 5: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; @@ -1346,23 +1354,27 @@ static int r_standard_suffix(struct SN_env * z) { static int r_verb_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_3, 283); + among_var = find_among_b(z, a_3, 283, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1373,23 +1385,27 @@ static int r_verb_suffix(struct SN_env * z) { static int r_residual_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_4, 22); + among_var = find_among_b(z, a_4, 22, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; @@ -1398,53 +1414,70 @@ static int r_residual_suffix(struct SN_env * z) { } extern int catalan_UTF_8_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_attached_pronoun(z); + { + int v_1 = z->l - z->c; + { + int ret = r_attached_pronoun(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_verb_suffix(z); + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_verb_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_residual_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_residual_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; - { int c5 = z->c; - { int ret = r_cleaning(z); + { + int v_5 = z->c; + { + int ret = r_cleaning(z); if (ret < 0) return ret; } - z->c = c5; + z->c = v_5; } return 1; } -extern struct SN_env * catalan_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * catalan_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void catalan_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void catalan_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_danish.c b/src/backend/snowball/libstemmer/stem_UTF_8_danish.c index b16c6e67423ea..cc8d8d2aefb1c 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_danish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_danish.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from danish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_danish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; + symbol * s_ch; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,23 +21,17 @@ extern int danish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_undouble(struct SN_env * z); static int r_other_suffix(struct SN_env * z); static int r_consonant_pair(struct SN_env * z); static int r_main_suffix(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * danish_UTF_8_create_env(void); -extern void danish_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 's', 't' }; +static const symbol s_1[] = { 'i', 'g' }; +static const symbol s_2[] = { 'l', 0xC3, 0xB8, 's' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 'h', 'e', 'd' }; static const symbol s_0_1[5] = { 'e', 't', 'h', 'e', 'd' }; static const symbol s_0_2[4] = { 'e', 'r', 'e', 'd' }; @@ -58,54 +64,50 @@ static const symbol s_0_28[3] = { 'e', 't', 's' }; static const symbol s_0_29[5] = { 'e', 'r', 'e', 't', 's' }; static const symbol s_0_30[2] = { 'e', 't' }; static const symbol s_0_31[4] = { 'e', 'r', 'e', 't' }; - -static const struct among a_0[32] = -{ -{ 3, s_0_0, -1, 1, 0}, -{ 5, s_0_1, 0, 1, 0}, -{ 4, s_0_2, -1, 1, 0}, -{ 1, s_0_3, -1, 1, 0}, -{ 5, s_0_4, 3, 1, 0}, -{ 4, s_0_5, 3, 1, 0}, -{ 6, s_0_6, 5, 1, 0}, -{ 3, s_0_7, 3, 1, 0}, -{ 4, s_0_8, 3, 1, 0}, -{ 3, s_0_9, 3, 1, 0}, -{ 2, s_0_10, -1, 1, 0}, -{ 5, s_0_11, 10, 1, 0}, -{ 4, s_0_12, 10, 1, 0}, -{ 2, s_0_13, -1, 1, 0}, -{ 5, s_0_14, 13, 1, 0}, -{ 4, s_0_15, 13, 1, 0}, -{ 1, s_0_16, -1, 2, 0}, -{ 4, s_0_17, 16, 1, 0}, -{ 2, s_0_18, 16, 1, 0}, -{ 5, s_0_19, 18, 1, 0}, -{ 7, s_0_20, 19, 1, 0}, -{ 4, s_0_21, 18, 1, 0}, -{ 5, s_0_22, 18, 1, 0}, -{ 4, s_0_23, 18, 1, 0}, -{ 3, s_0_24, 16, 1, 0}, -{ 6, s_0_25, 24, 1, 0}, -{ 5, s_0_26, 24, 1, 0}, -{ 3, s_0_27, 16, 1, 0}, -{ 3, s_0_28, 16, 1, 0}, -{ 5, s_0_29, 28, 1, 0}, -{ 2, s_0_30, -1, 1, 0}, -{ 4, s_0_31, 30, 1, 0} +static const struct among a_0[32] = { +{ 3, s_0_0, 0, 1, 0}, +{ 5, s_0_1, -1, 1, 0}, +{ 4, s_0_2, 0, 1, 0}, +{ 1, s_0_3, 0, 1, 0}, +{ 5, s_0_4, -1, 1, 0}, +{ 4, s_0_5, -2, 1, 0}, +{ 6, s_0_6, -1, 1, 0}, +{ 3, s_0_7, -4, 1, 0}, +{ 4, s_0_8, -5, 1, 0}, +{ 3, s_0_9, -6, 1, 0}, +{ 2, s_0_10, 0, 1, 0}, +{ 5, s_0_11, -1, 1, 0}, +{ 4, s_0_12, -2, 1, 0}, +{ 2, s_0_13, 0, 1, 0}, +{ 5, s_0_14, -1, 1, 0}, +{ 4, s_0_15, -2, 1, 0}, +{ 1, s_0_16, 0, 2, 0}, +{ 4, s_0_17, -1, 1, 0}, +{ 2, s_0_18, -2, 1, 0}, +{ 5, s_0_19, -1, 1, 0}, +{ 7, s_0_20, -1, 1, 0}, +{ 4, s_0_21, -3, 1, 0}, +{ 5, s_0_22, -4, 1, 0}, +{ 4, s_0_23, -5, 1, 0}, +{ 3, s_0_24, -8, 1, 0}, +{ 6, s_0_25, -1, 1, 0}, +{ 5, s_0_26, -2, 1, 0}, +{ 3, s_0_27, -11, 1, 0}, +{ 3, s_0_28, -12, 1, 0}, +{ 5, s_0_29, -1, 1, 0}, +{ 2, s_0_30, 0, 1, 0}, +{ 4, s_0_31, -1, 1, 0} }; static const symbol s_1_0[2] = { 'g', 'd' }; static const symbol s_1_1[2] = { 'd', 't' }; static const symbol s_1_2[2] = { 'g', 't' }; static const symbol s_1_3[2] = { 'k', 't' }; - -static const struct among a_1[4] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 2, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0} +static const struct among a_1[4] = { +{ 2, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 2, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0} }; static const symbol s_2_0[2] = { 'i', 'g' }; @@ -113,14 +115,12 @@ static const symbol s_2_1[3] = { 'l', 'i', 'g' }; static const symbol s_2_2[4] = { 'e', 'l', 'i', 'g' }; static const symbol s_2_3[3] = { 'e', 'l', 's' }; static const symbol s_2_4[5] = { 'l', 0xC3, 0xB8, 's', 't' }; - -static const struct among a_2[5] = -{ -{ 2, s_2_0, -1, 1, 0}, -{ 3, s_2_1, 0, 1, 0}, -{ 4, s_2_2, 1, 1, 0}, -{ 3, s_2_3, -1, 1, 0}, -{ 5, s_2_4, -1, 2, 0} +static const struct among a_2[5] = { +{ 2, s_2_0, 0, 1, 0}, +{ 3, s_2_1, -1, 1, 0}, +{ 4, s_2_2, -1, 1, 0}, +{ 3, s_2_3, 0, 1, 0}, +{ 5, s_2_4, 0, 2, 0} }; static const unsigned char g_c[] = { 119, 223, 119, 1 }; @@ -129,58 +129,60 @@ static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 static const unsigned char g_s_ending[] = { 239, 254, 42, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16 }; -static const symbol s_0[] = { 's', 't' }; -static const symbol s_1[] = { 'i', 'g' }; -static const symbol s_2[] = { 'l', 0xC3, 0xB8, 's' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c_test1 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); if (ret < 0) return 0; z->c = ret; } - z->I[0] = z->c; - z->c = c_test1; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping_U(z, g_v, 97, 248, 1); + if (ret < 0) return 0; + z->c += ret; } - - if (out_grouping_U(z, g_v, 97, 248, 1) < 0) return 0; - { int ret = in_grouping_U(z, g_v, 97, 248, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab0; - z->I[1] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: return 1; } static int r_main_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851440 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 32); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851440 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_0, 32, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b_U(z, g_s_ending, 97, 229, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -189,25 +191,28 @@ static int r_main_suffix(struct SN_env * z) { } static int r_consonant_pair(struct SN_env * z) { - { int m_test1 = z->l - z->c; - - { int mlimit2; - if (z->c < z->I[1]) return 0; - mlimit2 = z->lb; z->lb = z->I[1]; + { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 116)) { z->lb = mlimit2; return 0; } - if (!find_among_b(z, a_1, 4)) { z->lb = mlimit2; return 0; } + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 116)) { z->lb = v_2; return 0; } + if (!find_among_b(z, a_1, 4, 0)) { z->lb = v_2; return 0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } - z->c = z->l - m_test1; + z->c = z->l - v_1; } - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -215,42 +220,48 @@ static int r_consonant_pair(struct SN_env * z) { static int r_other_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; if (!(eq_s_b(z, 2, s_0))) goto lab0; z->bra = z->c; if (!(eq_s_b(z, 2, s_1))) goto lab0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - - { int mlimit2; - if (z->c < z->I[1]) return 0; - mlimit2 = z->lb; z->lb = z->I[1]; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit2; return 0; } - among_var = find_among_b(z, a_2, 5); - if (!among_var) { z->lb = mlimit2; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_2; return 0; } + among_var = find_among_b(z, a_2, 5, 0); + if (!among_var) { z->lb = v_2; return 0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } break; case 2: - { int ret = slice_from_s(z, 4, s_2); + { + int ret = slice_from_s(z, 4, s_2); if (ret < 0) return ret; } break; @@ -259,62 +270,91 @@ static int r_other_suffix(struct SN_env * z) { } static int r_undouble(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (in_grouping_b_U(z, g_c, 98, 122, 0)) { z->lb = mlimit1; return 0; } + if (in_grouping_b_U(z, g_c, 98, 122, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->S[0] = slice_to(z, z->S[0]); - if (z->S[0] == 0) return -1; - z->lb = mlimit1; + { + int ret = slice_to(z, &((SN_local *)z)->s_ch); + if (ret < 0) return ret; + } + z->lb = v_1; } - if (!(eq_v_b(z, z->S[0]))) return 0; - { int ret = slice_del(z); + if (!(eq_v_b(z, ((SN_local *)z)->s_ch))) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int danish_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_main_suffix(z); + { + int v_2 = z->l - z->c; + { + int ret = r_main_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_other_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_other_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_undouble(z); + { + int v_5 = z->l - z->c; + { + int ret = r_undouble(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } z->c = z->lb; return 1; } -extern struct SN_env * danish_UTF_8_create_env(void) { return SN_create_env(1, 2); } +extern struct SN_env * danish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->s_ch = NULL; -extern void danish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 1); } + if ((((SN_local *)z)->s_ch = create_s()) == NULL) { + danish_UTF_8_close_env(z); + return NULL; + } + } + return z; +} + +extern void danish_UTF_8_close_env(struct SN_env * z) { + if (z) { + lose_s(((SN_local *)z)->s_ch); + } + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_dutch.c b/src/backend/snowball/libstemmer/stem_UTF_8_dutch.c index 8fde5390f4dce..bc5e883251200 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_dutch.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_dutch.c @@ -1,6 +1,20 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from dutch.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_dutch.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_GE_removed; + symbol * s_ch; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,613 +23,2027 @@ extern int dutch_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif -static int r_standard_suffix(struct SN_env * z); -static int r_undouble(struct SN_env * z); + +static int r_measure(struct SN_env * z); +static int r_Lose_infix(struct SN_env * z); +static int r_Lose_prefix(struct SN_env * z); +static int r_Step_1c(struct SN_env * z); +static int r_Step_6(struct SN_env * z); +static int r_Step_7(struct SN_env * z); +static int r_Step_4(struct SN_env * z); +static int r_Step_3(struct SN_env * z); +static int r_Step_2(struct SN_env * z); +static int r_Step_1(struct SN_env * z); +static int r_lengthen_V(struct SN_env * z); +static int r_VX(struct SN_env * z); +static int r_V(struct SN_env * z); +static int r_C(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); -static int r_mark_regions(struct SN_env * z); -static int r_en_ending(struct SN_env * z); -static int r_e_ending(struct SN_env * z); -static int r_postlude(struct SN_env * z); -static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * dutch_UTF_8_create_env(void); -extern void dutch_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'i', 'j' }; +static const symbol s_1[] = { 'i', 'j' }; +static const symbol s_2[] = { 'i', 'j' }; +static const symbol s_3[] = { 'e', 0xC3, 0xAB, 'e' }; +static const symbol s_4[] = { 'i', 'e', 'e' }; +static const symbol s_5[] = { 'i', 'e' }; +static const symbol s_6[] = { 'a', 'r' }; +static const symbol s_7[] = { 'e', 'r' }; +static const symbol s_8[] = { 'e' }; +static const symbol s_9[] = { 0xC3, 0xA9 }; +static const symbol s_10[] = { 'a', 'u' }; +static const symbol s_11[] = { 'h', 'e', 'd' }; +static const symbol s_12[] = { 'h', 'e', 'i', 'd' }; +static const symbol s_13[] = { 'n', 'd' }; +static const symbol s_14[] = { 'n', 'd' }; +static const symbol s_15[] = { '\'', 't' }; +static const symbol s_16[] = { 'e', 't' }; +static const symbol s_17[] = { 'r', 'n', 't' }; +static const symbol s_18[] = { 'r', 'n' }; +static const symbol s_19[] = { 'i', 'n', 'k' }; +static const symbol s_20[] = { 'i', 'n', 'g' }; +static const symbol s_21[] = { 'm', 'p' }; +static const symbol s_22[] = { 'm' }; +static const symbol s_23[] = { 'g' }; +static const symbol s_24[] = { 'l', 'i', 'j', 'k' }; +static const symbol s_25[] = { 'i', 's', 'c', 'h' }; +static const symbol s_26[] = { 't' }; +static const symbol s_27[] = { 's' }; +static const symbol s_28[] = { 'r' }; +static const symbol s_29[] = { 'l' }; +static const symbol s_30[] = { 'e', 'n' }; +static const symbol s_31[] = { 'i', 'e', 'f' }; +static const symbol s_32[] = { 'e', 'e', 'r' }; +static const symbol s_33[] = { 'r' }; +static const symbol s_34[] = { 'i', 'l', 'd' }; +static const symbol s_35[] = { 'e', 'r' }; +static const symbol s_36[] = { 'a', 'a', 'r' }; +static const symbol s_37[] = { 'f' }; +static const symbol s_38[] = { 'g' }; +static const symbol s_39[] = { 't' }; +static const symbol s_40[] = { 'd' }; +static const symbol s_41[] = { 'i', 'e' }; +static const symbol s_42[] = { 'e', 'e', 'r' }; +static const symbol s_43[] = { 'n' }; +static const symbol s_44[] = { 'l' }; +static const symbol s_45[] = { 'r' }; +static const symbol s_46[] = { 't', 'e', 'e', 'r' }; +static const symbol s_47[] = { 'l', 'i', 'j', 'k' }; +static const symbol s_48[] = { 'i', 'n', 'n' }; +static const symbol s_49[] = { 'k' }; +static const symbol s_50[] = { 'f' }; +static const symbol s_51[] = { 'p' }; +static const symbol s_52[] = { 'b' }; +static const symbol s_53[] = { 'c' }; +static const symbol s_54[] = { 'd' }; +static const symbol s_55[] = { 'f' }; +static const symbol s_56[] = { 'g' }; +static const symbol s_57[] = { 'h' }; +static const symbol s_58[] = { 'j' }; +static const symbol s_59[] = { 'k' }; +static const symbol s_60[] = { 'l' }; +static const symbol s_61[] = { 'm' }; +static const symbol s_62[] = { 'n' }; +static const symbol s_63[] = { 'p' }; +static const symbol s_64[] = { 'q' }; +static const symbol s_65[] = { 'r' }; +static const symbol s_66[] = { 's' }; +static const symbol s_67[] = { 't' }; +static const symbol s_68[] = { 'v' }; +static const symbol s_69[] = { 'w' }; +static const symbol s_70[] = { 'x' }; +static const symbol s_71[] = { 'z' }; +static const symbol s_72[] = { 'i', 'n' }; +static const symbol s_73[] = { 'n' }; +static const symbol s_74[] = { 'e', 'n' }; +static const symbol s_75[] = { 'g', 'e' }; +static const symbol s_76[] = { 'i', 'j' }; +static const symbol s_77[] = { 'i', 'j' }; +static const symbol s_78[] = { 'e' }; +static const symbol s_79[] = { 'i' }; +static const symbol s_80[] = { 'g', 'e' }; +static const symbol s_81[] = { 'i', 'j' }; +static const symbol s_82[] = { 'i', 'j' }; +static const symbol s_83[] = { 'e' }; +static const symbol s_84[] = { 'i' }; +static const symbol s_85[] = { 'i', 'j' }; +static const symbol s_86[] = { 'i', 'j' }; -#ifdef __cplusplus -} -#endif -static const symbol s_0_1[2] = { 0xC3, 0xA1 }; -static const symbol s_0_2[2] = { 0xC3, 0xA4 }; -static const symbol s_0_3[2] = { 0xC3, 0xA9 }; -static const symbol s_0_4[2] = { 0xC3, 0xAB }; -static const symbol s_0_5[2] = { 0xC3, 0xAD }; -static const symbol s_0_6[2] = { 0xC3, 0xAF }; -static const symbol s_0_7[2] = { 0xC3, 0xB3 }; -static const symbol s_0_8[2] = { 0xC3, 0xB6 }; -static const symbol s_0_9[2] = { 0xC3, 0xBA }; -static const symbol s_0_10[2] = { 0xC3, 0xBC }; - -static const struct among a_0[11] = -{ -{ 0, 0, -1, 6, 0}, -{ 2, s_0_1, 0, 1, 0}, -{ 2, s_0_2, 0, 1, 0}, -{ 2, s_0_3, 0, 2, 0}, -{ 2, s_0_4, 0, 2, 0}, -{ 2, s_0_5, 0, 3, 0}, -{ 2, s_0_6, 0, 3, 0}, -{ 2, s_0_7, 0, 4, 0}, -{ 2, s_0_8, 0, 4, 0}, -{ 2, s_0_9, 0, 5, 0}, -{ 2, s_0_10, 0, 5, 0} +static const symbol s_0_0[1] = { 'a' }; +static const symbol s_0_1[1] = { 'e' }; +static const symbol s_0_2[1] = { 'o' }; +static const symbol s_0_3[1] = { 'u' }; +static const symbol s_0_4[2] = { 0xC3, 0xA0 }; +static const symbol s_0_5[2] = { 0xC3, 0xA1 }; +static const symbol s_0_6[2] = { 0xC3, 0xA2 }; +static const symbol s_0_7[2] = { 0xC3, 0xA4 }; +static const symbol s_0_8[2] = { 0xC3, 0xA8 }; +static const symbol s_0_9[2] = { 0xC3, 0xA9 }; +static const symbol s_0_10[2] = { 0xC3, 0xAA }; +static const symbol s_0_11[3] = { 'e', 0xC3, 0xAB }; +static const symbol s_0_12[3] = { 'i', 0xC3, 0xAB }; +static const symbol s_0_13[2] = { 0xC3, 0xB2 }; +static const symbol s_0_14[2] = { 0xC3, 0xB3 }; +static const symbol s_0_15[2] = { 0xC3, 0xB4 }; +static const symbol s_0_16[2] = { 0xC3, 0xB6 }; +static const symbol s_0_17[2] = { 0xC3, 0xB9 }; +static const symbol s_0_18[2] = { 0xC3, 0xBA }; +static const symbol s_0_19[2] = { 0xC3, 0xBB }; +static const symbol s_0_20[2] = { 0xC3, 0xBC }; +static const struct among a_0[21] = { +{ 1, s_0_0, 0, 1, 0}, +{ 1, s_0_1, 0, 2, 0}, +{ 1, s_0_2, 0, 1, 0}, +{ 1, s_0_3, 0, 1, 0}, +{ 2, s_0_4, 0, 1, 0}, +{ 2, s_0_5, 0, 1, 0}, +{ 2, s_0_6, 0, 1, 0}, +{ 2, s_0_7, 0, 1, 0}, +{ 2, s_0_8, 0, 2, 0}, +{ 2, s_0_9, 0, 2, 0}, +{ 2, s_0_10, 0, 2, 0}, +{ 3, s_0_11, 0, 3, 0}, +{ 3, s_0_12, 0, 4, 0}, +{ 2, s_0_13, 0, 1, 0}, +{ 2, s_0_14, 0, 1, 0}, +{ 2, s_0_15, 0, 1, 0}, +{ 2, s_0_16, 0, 1, 0}, +{ 2, s_0_17, 0, 1, 0}, +{ 2, s_0_18, 0, 1, 0}, +{ 2, s_0_19, 0, 1, 0}, +{ 2, s_0_20, 0, 1, 0} }; -static const symbol s_1_1[1] = { 'I' }; -static const symbol s_1_2[1] = { 'Y' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 1, s_1_1, 0, 2, 0}, -{ 1, s_1_2, 0, 1, 0} +static const symbol s_1_0[3] = { 'n', 'd', 'e' }; +static const symbol s_1_1[2] = { 'e', 'n' }; +static const symbol s_1_2[1] = { 's' }; +static const symbol s_1_3[2] = { '\'', 's' }; +static const symbol s_1_4[2] = { 'e', 's' }; +static const symbol s_1_5[3] = { 'i', 'e', 's' }; +static const symbol s_1_6[3] = { 'a', 'u', 's' }; +static const symbol s_1_7[3] = { 0xC3, 0xA9, 's' }; +static const struct among a_1[8] = { +{ 3, s_1_0, 0, 8, 0}, +{ 2, s_1_1, 0, 7, 0}, +{ 1, s_1_2, 0, 2, 0}, +{ 2, s_1_3, -1, 1, 0}, +{ 2, s_1_4, -2, 4, 0}, +{ 3, s_1_5, -1, 3, 0}, +{ 3, s_1_6, -4, 6, 0}, +{ 3, s_1_7, -5, 5, 0} }; -static const symbol s_2_0[2] = { 'd', 'd' }; -static const symbol s_2_1[2] = { 'k', 'k' }; -static const symbol s_2_2[2] = { 't', 't' }; - -static const struct among a_2[3] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0} +static const symbol s_2_0[2] = { 'd', 'e' }; +static const symbol s_2_1[2] = { 'g', 'e' }; +static const symbol s_2_2[5] = { 'i', 's', 'c', 'h', 'e' }; +static const symbol s_2_3[2] = { 'j', 'e' }; +static const symbol s_2_4[5] = { 'l', 'i', 'j', 'k', 'e' }; +static const symbol s_2_5[2] = { 'l', 'e' }; +static const symbol s_2_6[3] = { 'e', 'n', 'e' }; +static const symbol s_2_7[2] = { 'r', 'e' }; +static const symbol s_2_8[2] = { 's', 'e' }; +static const symbol s_2_9[2] = { 't', 'e' }; +static const symbol s_2_10[4] = { 'i', 'e', 'v', 'e' }; +static const struct among a_2[11] = { +{ 2, s_2_0, 0, 5, 0}, +{ 2, s_2_1, 0, 2, 0}, +{ 5, s_2_2, 0, 4, 0}, +{ 2, s_2_3, 0, 1, 0}, +{ 5, s_2_4, 0, 3, 0}, +{ 2, s_2_5, 0, 9, 0}, +{ 3, s_2_6, 0, 10, 0}, +{ 2, s_2_7, 0, 8, 0}, +{ 2, s_2_8, 0, 7, 0}, +{ 2, s_2_9, 0, 6, 0}, +{ 4, s_2_10, 0, 11, 0} }; -static const symbol s_3_0[3] = { 'e', 'n', 'e' }; -static const symbol s_3_1[2] = { 's', 'e' }; -static const symbol s_3_2[2] = { 'e', 'n' }; -static const symbol s_3_3[5] = { 'h', 'e', 'd', 'e', 'n' }; -static const symbol s_3_4[1] = { 's' }; - -static const struct among a_3[5] = -{ -{ 3, s_3_0, -1, 2, 0}, -{ 2, s_3_1, -1, 3, 0}, -{ 2, s_3_2, -1, 2, 0}, -{ 5, s_3_3, 2, 1, 0}, -{ 1, s_3_4, -1, 3, 0} +static const symbol s_3_0[4] = { 'h', 'e', 'i', 'd' }; +static const symbol s_3_1[3] = { 'f', 'i', 'e' }; +static const symbol s_3_2[3] = { 'g', 'i', 'e' }; +static const symbol s_3_3[4] = { 'a', 't', 'i', 'e' }; +static const symbol s_3_4[4] = { 'i', 's', 'm', 'e' }; +static const symbol s_3_5[3] = { 'i', 'n', 'g' }; +static const symbol s_3_6[4] = { 'a', 'r', 'i', 'j' }; +static const symbol s_3_7[4] = { 'e', 'r', 'i', 'j' }; +static const symbol s_3_8[3] = { 's', 'e', 'l' }; +static const symbol s_3_9[4] = { 'r', 'd', 'e', 'r' }; +static const symbol s_3_10[4] = { 's', 't', 'e', 'r' }; +static const symbol s_3_11[5] = { 'i', 't', 'e', 'i', 't' }; +static const symbol s_3_12[3] = { 'd', 's', 't' }; +static const symbol s_3_13[3] = { 't', 's', 't' }; +static const struct among a_3[14] = { +{ 4, s_3_0, 0, 3, 0}, +{ 3, s_3_1, 0, 7, 0}, +{ 3, s_3_2, 0, 8, 0}, +{ 4, s_3_3, 0, 1, 0}, +{ 4, s_3_4, 0, 5, 0}, +{ 3, s_3_5, 0, 5, 0}, +{ 4, s_3_6, 0, 6, 0}, +{ 4, s_3_7, 0, 5, 0}, +{ 3, s_3_8, 0, 3, 0}, +{ 4, s_3_9, 0, 4, 0}, +{ 4, s_3_10, 0, 3, 0}, +{ 5, s_3_11, 0, 2, 0}, +{ 3, s_3_12, 0, 10, 0}, +{ 3, s_3_13, 0, 9, 0} }; static const symbol s_4_0[3] = { 'e', 'n', 'd' }; -static const symbol s_4_1[2] = { 'i', 'g' }; -static const symbol s_4_2[3] = { 'i', 'n', 'g' }; -static const symbol s_4_3[4] = { 'l', 'i', 'j', 'k' }; -static const symbol s_4_4[4] = { 'b', 'a', 'a', 'r' }; -static const symbol s_4_5[3] = { 'b', 'a', 'r' }; - -static const struct among a_4[6] = -{ -{ 3, s_4_0, -1, 1, 0}, -{ 2, s_4_1, -1, 2, 0}, -{ 3, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 3, 0}, -{ 4, s_4_4, -1, 4, 0}, -{ 3, s_4_5, -1, 5, 0} +static const symbol s_4_1[5] = { 'a', 't', 'i', 'e', 'f' }; +static const symbol s_4_2[4] = { 'e', 'r', 'i', 'g' }; +static const symbol s_4_3[6] = { 'a', 'c', 'h', 't', 'i', 'g' }; +static const symbol s_4_4[6] = { 'i', 'o', 'n', 'e', 'e', 'l' }; +static const symbol s_4_5[4] = { 'b', 'a', 'a', 'r' }; +static const symbol s_4_6[4] = { 'l', 'a', 'a', 'r' }; +static const symbol s_4_7[4] = { 'n', 'a', 'a', 'r' }; +static const symbol s_4_8[4] = { 'r', 'a', 'a', 'r' }; +static const symbol s_4_9[6] = { 'e', 'r', 'i', 'g', 'e', 'r' }; +static const symbol s_4_10[8] = { 'a', 'c', 'h', 't', 'i', 'g', 'e', 'r' }; +static const symbol s_4_11[6] = { 'l', 'i', 'j', 'k', 'e', 'r' }; +static const symbol s_4_12[4] = { 't', 'a', 'n', 't' }; +static const symbol s_4_13[6] = { 'e', 'r', 'i', 'g', 's', 't' }; +static const symbol s_4_14[8] = { 'a', 'c', 'h', 't', 'i', 'g', 's', 't' }; +static const symbol s_4_15[6] = { 'l', 'i', 'j', 'k', 's', 't' }; +static const struct among a_4[16] = { +{ 3, s_4_0, 0, 9, 0}, +{ 5, s_4_1, 0, 2, 0}, +{ 4, s_4_2, 0, 9, 0}, +{ 6, s_4_3, 0, 3, 0}, +{ 6, s_4_4, 0, 1, 0}, +{ 4, s_4_5, 0, 3, 0}, +{ 4, s_4_6, 0, 5, 0}, +{ 4, s_4_7, 0, 4, 0}, +{ 4, s_4_8, 0, 6, 0}, +{ 6, s_4_9, 0, 9, 0}, +{ 8, s_4_10, 0, 3, 0}, +{ 6, s_4_11, 0, 8, 0}, +{ 4, s_4_12, 0, 7, 0}, +{ 6, s_4_13, 0, 9, 0}, +{ 8, s_4_14, 0, 3, 0}, +{ 6, s_4_15, 0, 8, 0} }; -static const symbol s_5_0[2] = { 'a', 'a' }; -static const symbol s_5_1[2] = { 'e', 'e' }; -static const symbol s_5_2[2] = { 'o', 'o' }; -static const symbol s_5_3[2] = { 'u', 'u' }; - -static const struct among a_5[4] = -{ -{ 2, s_5_0, -1, -1, 0}, -{ 2, s_5_1, -1, -1, 0}, -{ 2, s_5_2, -1, -1, 0}, -{ 2, s_5_3, -1, -1, 0} +static const symbol s_5_0[2] = { 'i', 'g' }; +static const symbol s_5_1[4] = { 'i', 'g', 'e', 'r' }; +static const symbol s_5_2[4] = { 'i', 'g', 's', 't' }; +static const struct among a_5[3] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 4, s_5_2, 0, 1, 0} }; -static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; - -static const unsigned char g_v_I[] = { 1, 0, 0, 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; +static const symbol s_6_0[2] = { 'f', 't' }; +static const symbol s_6_1[2] = { 'k', 't' }; +static const symbol s_6_2[2] = { 'p', 't' }; +static const struct among a_6[3] = { +{ 2, s_6_0, 0, 2, 0}, +{ 2, s_6_1, 0, 1, 0}, +{ 2, s_6_2, 0, 3, 0} +}; -static const unsigned char g_v_j[] = { 17, 67, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; +static const symbol s_7_0[2] = { 'b', 'b' }; +static const symbol s_7_1[2] = { 'c', 'c' }; +static const symbol s_7_2[2] = { 'd', 'd' }; +static const symbol s_7_3[2] = { 'f', 'f' }; +static const symbol s_7_4[2] = { 'g', 'g' }; +static const symbol s_7_5[2] = { 'h', 'h' }; +static const symbol s_7_6[2] = { 'j', 'j' }; +static const symbol s_7_7[2] = { 'k', 'k' }; +static const symbol s_7_8[2] = { 'l', 'l' }; +static const symbol s_7_9[2] = { 'm', 'm' }; +static const symbol s_7_10[2] = { 'n', 'n' }; +static const symbol s_7_11[2] = { 'p', 'p' }; +static const symbol s_7_12[2] = { 'q', 'q' }; +static const symbol s_7_13[2] = { 'r', 'r' }; +static const symbol s_7_14[2] = { 's', 's' }; +static const symbol s_7_15[2] = { 't', 't' }; +static const symbol s_7_16[1] = { 'v' }; +static const symbol s_7_17[2] = { 'v', 'v' }; +static const symbol s_7_18[2] = { 'w', 'w' }; +static const symbol s_7_19[2] = { 'x', 'x' }; +static const symbol s_7_20[1] = { 'z' }; +static const symbol s_7_21[2] = { 'z', 'z' }; +static const struct among a_7[22] = { +{ 2, s_7_0, 0, 1, 0}, +{ 2, s_7_1, 0, 2, 0}, +{ 2, s_7_2, 0, 3, 0}, +{ 2, s_7_3, 0, 4, 0}, +{ 2, s_7_4, 0, 5, 0}, +{ 2, s_7_5, 0, 6, 0}, +{ 2, s_7_6, 0, 7, 0}, +{ 2, s_7_7, 0, 8, 0}, +{ 2, s_7_8, 0, 9, 0}, +{ 2, s_7_9, 0, 10, 0}, +{ 2, s_7_10, 0, 11, 0}, +{ 2, s_7_11, 0, 12, 0}, +{ 2, s_7_12, 0, 13, 0}, +{ 2, s_7_13, 0, 14, 0}, +{ 2, s_7_14, 0, 15, 0}, +{ 2, s_7_15, 0, 16, 0}, +{ 1, s_7_16, 0, 4, 0}, +{ 2, s_7_17, -1, 17, 0}, +{ 2, s_7_18, 0, 18, 0}, +{ 2, s_7_19, 0, 19, 0}, +{ 1, s_7_20, 0, 15, 0}, +{ 2, s_7_21, -1, 20, 0} +}; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'i' }; -static const symbol s_3[] = { 'o' }; -static const symbol s_4[] = { 'u' }; -static const symbol s_5[] = { 'Y' }; -static const symbol s_6[] = { 'I' }; -static const symbol s_7[] = { 'Y' }; -static const symbol s_8[] = { 'y' }; -static const symbol s_9[] = { 'i' }; -static const symbol s_10[] = { 'g', 'e', 'm' }; -static const symbol s_11[] = { 'h', 'e', 'i', 'd' }; -static const symbol s_12[] = { 'h', 'e', 'i', 'd' }; -static const symbol s_13[] = { 'e', 'n' }; -static const symbol s_14[] = { 'i', 'g' }; +static const symbol s_8_0[1] = { 'd' }; +static const symbol s_8_1[1] = { 't' }; +static const struct among a_8[2] = { +{ 1, s_8_0, 0, 1, 0}, +{ 1, s_8_1, 0, 2, 0} +}; -static int r_prelude(struct SN_env * z) { - int among_var; - { int c_test1 = z->c; - while(1) { - int c2 = z->c; - z->bra = z->c; - if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 5 || !((340306450 >> (z->p[z->c + 1] & 0x1f)) & 1)) among_var = 6; else - among_var = find_among(z, a_0, 11); - z->ket = z->c; - switch (among_var) { - case 1: - { int ret = slice_from_s(z, 1, s_0); - if (ret < 0) return ret; - } - break; - case 2: - { int ret = slice_from_s(z, 1, s_1); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = slice_from_s(z, 1, s_2); - if (ret < 0) return ret; - } - break; - case 4: - { int ret = slice_from_s(z, 1, s_3); - if (ret < 0) return ret; - } - break; - case 5: - { int ret = slice_from_s(z, 1, s_4); - if (ret < 0) return ret; - } - break; - case 6: - { int ret = skip_utf8(z->p, z->c, z->l, 1); - if (ret < 0) goto lab0; - z->c = ret; - } - break; - } - continue; - lab0: - z->c = c2; - break; - } - z->c = c_test1; - } - { int c3 = z->c; - z->bra = z->c; - if (z->c == z->l || z->p[z->c] != 'y') { z->c = c3; goto lab1; } - z->c++; - z->ket = z->c; - { int ret = slice_from_s(z, 1, s_5); - if (ret < 0) return ret; - } - lab1: - ; - } - while(1) { - int c4 = z->c; - while(1) { - int c5 = z->c; - if (in_grouping_U(z, g_v, 97, 232, 0)) goto lab3; - z->bra = z->c; - { int c6 = z->c; - if (z->c == z->l || z->p[z->c] != 'i') goto lab5; - z->c++; - z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 232, 0)) goto lab5; - { int ret = slice_from_s(z, 1, s_6); - if (ret < 0) return ret; - } - goto lab4; - lab5: - z->c = c6; - if (z->c == z->l || z->p[z->c] != 'y') goto lab3; - z->c++; - z->ket = z->c; - { int ret = slice_from_s(z, 1, s_7); - if (ret < 0) return ret; - } - } - lab4: - z->c = c5; - break; - lab3: - z->c = c5; - { int ret = skip_utf8(z->p, z->c, z->l, 1); - if (ret < 0) goto lab2; - z->c = ret; - } - } - continue; - lab2: - z->c = c4; - break; - } - return 1; -} +static const symbol s_9_1[3] = { 'e', 'f', 't' }; +static const symbol s_9_2[3] = { 'v', 'a', 'a' }; +static const symbol s_9_3[3] = { 'v', 'a', 'l' }; +static const symbol s_9_4[4] = { 'v', 'a', 'l', 'i' }; +static const symbol s_9_5[4] = { 'v', 'a', 'r', 'e' }; +static const struct among a_9[6] = { +{ 0, 0, 0, -1, 0}, +{ 3, s_9_1, -1, 1, 0}, +{ 3, s_9_2, -2, 1, 0}, +{ 3, s_9_3, -3, 1, 0}, +{ 4, s_9_4, -1, -1, 0}, +{ 4, s_9_5, -5, 1, 0} +}; -static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - { int c_test1 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); - if (ret < 0) return 0; - z->c = ret; - } - z->I[0] = z->c; - z->c = c_test1; - } +static const symbol s_10_0[2] = { 0xC3, 0xAB }; +static const symbol s_10_1[2] = { 0xC3, 0xAF }; +static const struct among a_10[2] = { +{ 2, s_10_0, 0, 1, 0}, +{ 2, s_10_1, 0, 2, 0} +}; - { - int ret = out_grouping_U(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } +static const symbol s_11_0[2] = { 0xC3, 0xAB }; +static const symbol s_11_1[2] = { 0xC3, 0xAF }; +static const struct among a_11[2] = { +{ 2, s_11_0, 0, 1, 0}, +{ 2, s_11_1, 0, 2, 0} +}; - { - int ret = in_grouping_U(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } - z->I[2] = z->c; +static const unsigned char g_E[] = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120 }; - if (z->I[2] >= z->I[0]) goto lab0; - z->I[2] = z->I[0]; -lab0: +static const unsigned char g_AIOU[] = { 1, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 11, 120, 46, 15 }; - { - int ret = out_grouping_U(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } +static const unsigned char g_AEIOU[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 139, 127, 46, 15 }; - { - int ret = in_grouping_U(z, g_v, 97, 232, 1); - if (ret < 0) return 0; - z->c += ret; - } - z->I[1] = z->c; - return 1; -} +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 139, 127, 46, 15 }; -static int r_postlude(struct SN_env * z) { - int among_var; - while(1) { - int c1 = z->c; - z->bra = z->c; - if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 89)) among_var = 3; else - among_var = find_among(z, a_1, 3); - z->ket = z->c; - switch (among_var) { - case 1: - { int ret = slice_from_s(z, 1, s_8); - if (ret < 0) return ret; - } - break; - case 2: - { int ret = slice_from_s(z, 1, s_9); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = skip_utf8(z->p, z->c, z->l, 1); - if (ret < 0) goto lab0; - z->c = ret; - } - break; - } - continue; - lab0: - z->c = c1; - break; - } - return 1; -} +static const unsigned char g_v_WX[] = { 17, 65, 208, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 139, 127, 46, 15 }; static int r_R1(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } -static int r_undouble(struct SN_env * z) { - { int m_test1 = z->l - z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1050640 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_2, 3)) return 0; - z->c = z->l - m_test1; - } - z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) return 0; - z->c = ret; - } - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; +static int r_V(struct SN_env * z) { + { + int v_1 = z->l - z->c; + do { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_v, 97, 252, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_0))) return 0; + } while (0); + z->c = z->l - v_1; } return 1; } -static int r_e_ending(struct SN_env * z) { - z->I[3] = 0; - z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; - z->c--; - z->bra = z->c; - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m_test1 = z->l - z->c; - if (out_grouping_b_U(z, g_v, 97, 232, 0)) return 0; - z->c = z->l - m_test1; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[3] = 1; - { int ret = r_undouble(z); - if (ret <= 0) return ret; +static int r_VX(struct SN_env * z) { + { + int v_1 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) return 0; + z->c = ret; + } + do { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_v, 97, 252, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_1))) return 0; + } while (0); + z->c = z->l - v_1; } return 1; } -static int r_en_ending(struct SN_env * z) { - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m1 = z->l - z->c; (void)m1; - if (out_grouping_b_U(z, g_v, 97, 232, 0)) return 0; - z->c = z->l - m1; - { int m2 = z->l - z->c; (void)m2; - if (!(eq_s_b(z, 3, s_10))) goto lab0; +static int r_C(struct SN_env * z) { + { + int v_1 = z->l - z->c; + { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_2))) goto lab0; return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - { int ret = r_undouble(z); - if (ret <= 0) return ret; + if (out_grouping_b_U(z, g_v, 97, 252, 0)) return 0; + z->c = z->l - v_1; } return 1; } -static int r_standard_suffix(struct SN_env * z) { +static int r_lengthen_V(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; + if (out_grouping_b_U(z, g_v_WX, 97, 252, 0)) goto lab0; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540704 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; - among_var = find_among_b(z, a_3, 5); + among_var = find_among_b(z, a_0, 21, 0); if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); - if (ret == 0) goto lab0; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + if (out_grouping_b_U(z, g_AEIOU, 97, 252, 0)) goto lab1; + break; + lab1: + z->c = z->l - v_3; + if (z->c > z->lb) goto lab0; + } while (0); + z->c = z->l - v_2; + } + { + int ret = slice_to(z, &((SN_local *)z)->s_ch); if (ret < 0) return ret; } - { int ret = slice_from_s(z, 4, s_11); + { + int saved_c = z->c; + int ret = insert_v(z, z->c, z->c, ((SN_local *)z)->s_ch); + z->c = saved_c; if (ret < 0) return ret; } break; case 2: - { int ret = r_en_ending(z); - if (ret == 0) goto lab0; + { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + if (out_grouping_b_U(z, g_AEIOU, 97, 252, 0)) goto lab2; + break; + lab2: + z->c = z->l - v_5; + if (z->c > z->lb) goto lab0; + } while (0); + { + int v_6 = z->l - z->c; + do { + int v_7 = z->l - z->c; + if (in_grouping_b_U(z, g_AIOU, 97, 252, 0)) goto lab4; + break; + lab4: + z->c = z->l - v_7; + if (in_grouping_b_U(z, g_E, 101, 235, 0)) goto lab3; + if (z->c > z->lb) goto lab3; + } while (0); + goto lab0; + lab3: + z->c = z->l - v_6; + } + { + int v_8 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab5; + z->c = ret; + } + if (in_grouping_b_U(z, g_AIOU, 97, 252, 0)) goto lab5; + if (out_grouping_b_U(z, g_AEIOU, 97, 252, 0)) goto lab5; + goto lab0; + lab5: + z->c = z->l - v_8; + } + z->c = z->l - v_4; + } + { + int ret = slice_to(z, &((SN_local *)z)->s_ch); + if (ret < 0) return ret; + } + { + int saved_c = z->c; + int ret = insert_v(z, z->c, z->c, ((SN_local *)z)->s_ch); + z->c = saved_c; if (ret < 0) return ret; } break; case 3: - { int ret = r_R1(z); - if (ret == 0) goto lab0; + { + int ret = slice_from_s(z, 4, s_3); if (ret < 0) return ret; } - if (out_grouping_b_U(z, g_v_j, 97, 232, 0)) goto lab0; - { int ret = slice_del(z); + break; + case 4: + { + int ret = slice_from_s(z, 3, s_4); if (ret < 0) return ret; } break; } lab0: - z->c = z->l - m1; - } - { int m2 = z->l - z->c; (void)m2; - { int ret = r_e_ending(z); - if (ret < 0) return ret; - } - z->c = z->l - m2; - } - { int m3 = z->l - z->c; (void)m3; - z->ket = z->c; - if (!(eq_s_b(z, 4, s_12))) goto lab1; - z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - { int m4 = z->l - z->c; (void)m4; - if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab2; - z->c--; - goto lab1; - lab2: - z->c = z->l - m4; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->ket = z->c; - if (!(eq_s_b(z, 2, s_13))) goto lab1; - z->bra = z->c; - { int ret = r_en_ending(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - lab1: - z->c = z->l - m3; + z->c = z->l - v_1; } - { int m5 = z->l - z->c; (void)m5; - z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((264336 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab3; - among_var = find_among_b(z, a_4, 6); - if (!among_var) goto lab3; - z->bra = z->c; - switch (among_var) { - case 1: - { int ret = r_R2(z); - if (ret == 0) goto lab3; - if (ret < 0) return ret; - } - { int ret = slice_del(z); + return 1; +} + +static int r_Step_1(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540704 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_1, 8, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 't') goto lab0; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; - z->ket = z->c; - if (!(eq_s_b(z, 2, s_14))) goto lab5; - z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) goto lab5; + return 0; + lab0: + z->c = z->l - v_1; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_5); + if (ret < 0) return ret; + } + break; + case 4: + do { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 2, s_6))) goto lab1; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - { int m7 = z->l - z->c; (void)m7; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab6; - z->c--; - goto lab5; - lab6: - z->c = z->l - m7; + { + int ret = r_C(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; } - { int ret = slice_del(z); + z->c = z->l - v_3; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret < 0) return ret; + } + break; + lab1: + z->c = z->l - v_2; + { + int v_4 = z->l - z->c; + if (!(eq_s_b(z, 2, s_7))) goto lab2; + { + int ret = r_R1(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m6; - { int ret = r_undouble(z); - if (ret == 0) goto lab3; + { + int ret = r_C(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } + z->c = z->l - v_4; } - lab4: - break; - case 2: - { int ret = r_R2(z); - if (ret == 0) goto lab3; + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab7; - z->c--; - goto lab3; - lab7: - z->c = z->l - m8; + break; + lab2: + z->c = z->l - v_2; + { + int ret = r_R1(z); + if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } - break; - case 3: - { int ret = r_R2(z); + } while (0); + break; + case 5: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_9); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_V(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_10); + if (ret < 0) return ret; + } + break; + case 7: + do { + int v_5 = z->l - z->c; + if (!(eq_s_b(z, 3, s_11))) goto lab3; + { + int ret = r_R1(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int ret = slice_del(z); + z->bra = z->c; + { + int ret = slice_from_s(z, 4, s_12); if (ret < 0) return ret; } - { int ret = r_e_ending(z); - if (ret == 0) goto lab3; + break; + lab3: + z->c = z->l - v_5; + if (!(eq_s_b(z, 2, s_13))) goto lab4; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 4: - { int ret = r_R2(z); - if (ret == 0) goto lab3; + lab4: + z->c = z->l - v_5; + if (z->c <= z->lb || z->p[z->c - 1] != 'd') goto lab5; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = r_C(z); + if (ret == 0) goto lab5; + if (ret < 0) return ret; + } + z->bra = z->c; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 5: - { int ret = r_R2(z); - if (ret == 0) goto lab3; + lab5: + z->c = z->l - v_5; + do { + int v_6 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab7; + z->c--; + break; + lab7: + z->c = z->l - v_6; + if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab6; + z->c--; + } while (0); + { + int ret = r_V(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - if (!(z->I[3])) goto lab3; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - } - lab3: - z->c = z->l - m5; - } - { int m9 = z->l - z->c; (void)m9; - if (out_grouping_b_U(z, g_v_I, 73, 232, 0)) goto lab8; - { int m_test10 = z->l - z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2129954 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab8; - if (!find_among_b(z, a_5, 4)) goto lab8; - if (out_grouping_b_U(z, g_v, 97, 232, 0)) goto lab8; - z->c = z->l - m_test10; - } - z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) goto lab8; - z->c = ret; - } - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; - } - lab8: - z->c = z->l - m9; + lab6: + z->c = z->l - v_5; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + } while (0); + break; + case 8: + { + int ret = slice_from_s(z, 2, s_14); + if (ret < 0) return ret; + } + break; } return 1; } -extern int dutch_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_prelude(z); - if (ret < 0) return ret; - } - z->c = c1; - } - { int c2 = z->c; - { int ret = r_mark_regions(z); +static int r_Step_2(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 101) return 0; + among_var = find_among_b(z, a_2, 11, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 2, s_15))) goto lab0; + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 2, s_16))) goto lab1; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + { + int ret = r_C(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab1: + z->c = z->l - v_1; + if (!(eq_s_b(z, 3, s_17))) goto lab2; + z->bra = z->c; + { + int ret = slice_from_s(z, 2, s_18); + if (ret < 0) return ret; + } + break; + lab2: + z->c = z->l - v_1; + if (z->c <= z->lb || z->p[z->c - 1] != 't') goto lab3; + z->c--; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = r_VX(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab3: + z->c = z->l - v_1; + if (!(eq_s_b(z, 3, s_19))) goto lab4; + z->bra = z->c; + { + int ret = slice_from_s(z, 3, s_20); + if (ret < 0) return ret; + } + break; + lab4: + z->c = z->l - v_1; + if (!(eq_s_b(z, 2, s_21))) goto lab5; + z->bra = z->c; + { + int ret = slice_from_s(z, 1, s_22); + if (ret < 0) return ret; + } + break; + lab5: + z->c = z->l - v_1; + if (z->c <= z->lb || z->p[z->c - 1] != '\'') goto lab6; + z->c--; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab6; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab6: + z->c = z->l - v_1; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } while (0); + break; + case 2: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_23); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_24); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_25); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_26); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_27); + if (ret < 0) return ret; + } + break; + case 8: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_28); + if (ret < 0) return ret; + } + break; + case 9: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 1, s_29); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 10: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 2, s_30); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 11: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_31); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_3(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1316016 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_3, 14, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_32); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_33); + if (ret < 0) return ret; + } + break; + case 5: + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 3, s_34))) goto lab0; + { + int ret = slice_from_s(z, 2, s_35); + if (ret < 0) return ret; + } + break; + lab0: + z->c = z->l - v_1; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + } while (0); + break; + case 6: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_36); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = r_R2(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 1, s_37); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 8: + { + int ret = r_R2(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = insert_s(z, z->c, z->c, 1, s_38); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + break; + case 9: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_39); + if (ret < 0) return ret; + } + break; + case 10: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_40); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_4(struct SN_env * z) { + int among_var; + do { + int v_1 = z->l - z->c; + z->ket = z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1315024 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_4, 16, 0); + if (!among_var) goto lab0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 2, s_41); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 3, s_42); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_V(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_43); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_V(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_44); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_V(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 1, s_45); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_46); + if (ret < 0) return ret; + } + break; + case 8: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_47); + if (ret < 0) return ret; + } + break; + case 9: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = r_C(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_lengthen_V(z); + if (ret < 0) return ret; + } + break; + } + break; + lab0: + z->c = z->l - v_1; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1310848 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + if (!find_among_b(z, a_5, 3, 0)) return 0; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 3, s_48))) goto lab1; + if (z->c > z->lb) goto lab1; + return 0; + lab1: + z->c = z->l - v_2; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->c = c2; + { + int ret = r_lengthen_V(z); + if (ret <= 0) return ret; + } + } while (0); + return 1; +} + +static int r_Step_7(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 116) return 0; + among_var = find_among_b(z, a_6, 3, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_49); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_50); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_51); + if (ret < 0) return ret; + } + break; } - z->lb = z->c; z->c = z->l; + return 1; +} +static int r_Step_6(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((98532828 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_7, 22, 0); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_52); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_53); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_54); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_55); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_from_s(z, 1, s_56); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = slice_from_s(z, 1, s_57); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = slice_from_s(z, 1, s_58); + if (ret < 0) return ret; + } + break; + case 8: + { + int ret = slice_from_s(z, 1, s_59); + if (ret < 0) return ret; + } + break; + case 9: + { + int ret = slice_from_s(z, 1, s_60); + if (ret < 0) return ret; + } + break; + case 10: + { + int ret = slice_from_s(z, 1, s_61); + if (ret < 0) return ret; + } + break; + case 11: + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab0; + z->c--; + if (z->c > z->lb) goto lab0; + return 0; + lab0: + z->c = z->l - v_1; + } + { + int ret = slice_from_s(z, 1, s_62); + if (ret < 0) return ret; + } + break; + case 12: + { + int ret = slice_from_s(z, 1, s_63); + if (ret < 0) return ret; + } + break; + case 13: + { + int ret = slice_from_s(z, 1, s_64); + if (ret < 0) return ret; + } + break; + case 14: + { + int ret = slice_from_s(z, 1, s_65); + if (ret < 0) return ret; + } + break; + case 15: + { + int ret = slice_from_s(z, 1, s_66); + if (ret < 0) return ret; + } + break; + case 16: + { + int ret = slice_from_s(z, 1, s_67); + if (ret < 0) return ret; + } + break; + case 17: + { + int ret = slice_from_s(z, 1, s_68); + if (ret < 0) return ret; + } + break; + case 18: + { + int ret = slice_from_s(z, 1, s_69); + if (ret < 0) return ret; + } + break; + case 19: + { + int ret = slice_from_s(z, 1, s_70); + if (ret < 0) return ret; + } + break; + case 20: + { + int ret = slice_from_s(z, 1, s_71); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +static int r_Step_1c(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 116)) return 0; + among_var = find_among_b(z, a_8, 2, 0); + if (!among_var) return 0; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int ret = r_C(z); + if (ret <= 0) return ret; + } + switch (among_var) { + case 1: + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') goto lab0; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + return 0; + lab0: + z->c = z->l - v_1; + } + do { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_72))) goto lab1; + if (z->c > z->lb) goto lab1; + { + int ret = slice_from_s(z, 1, s_73); + if (ret < 0) return ret; + } + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } while (0); + break; + case 2: + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'h') goto lab2; + z->c--; + { + int ret = r_R1(z); + if (ret == 0) goto lab2; + if (ret < 0) return ret; + } + return 0; + lab2: + z->c = z->l - v_3; + } + { + int v_4 = z->l - z->c; + if (!(eq_s_b(z, 2, s_74))) goto lab3; + if (z->c > z->lb) goto lab3; + return 0; + lab3: + z->c = z->l - v_4; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + return 1; +} - { int ret = r_standard_suffix(z); +static int r_Lose_prefix(struct SN_env * z) { + int among_var; + z->bra = z->c; + if (!(eq_s(z, 2, s_75))) return 0; + z->ket = z->c; + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); + if (ret < 0) return 0; + z->c = ret; + } + z->c = v_1; + } + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; + if (!(eq_s(z, 2, s_76))) goto lab1; + break; + lab1: + z->c = v_4; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab0; + } while (0); + break; + lab0: + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) return 0; + z->c = ret; + } + } + while (1) { + int v_5 = z->c; + do { + int v_6 = z->c; + if (!(eq_s(z, 2, s_77))) goto lab3; + break; + lab3: + z->c = v_6; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab2; + } while (0); + continue; + lab2: + z->c = v_5; + break; + } + if (z->c < z->l) goto lab4; + return 0; + lab4: + z->c = v_2; + } + if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((1314818 >> (z->p[z->c + 2] & 0x1f)) & 1)) among_var = -1; else + among_var = find_among(z, a_9, 6, 0); + switch (among_var) { + case 1: + return 0; + break; + } + ((SN_local *)z)->b_GE_removed = 1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int v_7 = z->c; + z->bra = z->c; + if (z->c + 1 >= z->l || (z->p[z->c + 1] != 171 && z->p[z->c + 1] != 175)) goto lab5; + among_var = find_among(z, a_10, 2, 0); + if (!among_var) goto lab5; + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_78); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_79); + if (ret < 0) return ret; + } + break; + } + lab5: + z->c = v_7; + } + return 1; +} + +static int r_Lose_infix(struct SN_env * z) { + int among_var; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) return 0; + z->c = ret; + } + while (1) { + z->bra = z->c; + if (!(eq_s(z, 2, s_80))) goto lab0; + z->ket = z->c; + break; + lab0: + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) return 0; + z->c = ret; + } + } + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); + if (ret < 0) return 0; + z->c = ret; + } + z->c = v_1; + } + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; + if (!(eq_s(z, 2, s_81))) goto lab2; + break; + lab2: + z->c = v_4; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab1; + } while (0); + break; + lab1: + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) return 0; + z->c = ret; + } + } + while (1) { + int v_5 = z->c; + do { + int v_6 = z->c; + if (!(eq_s(z, 2, s_82))) goto lab4; + break; + lab4: + z->c = v_6; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab3; + } while (0); + continue; + lab3: + z->c = v_5; + break; + } + if (z->c < z->l) goto lab5; + return 0; + lab5: + z->c = v_2; + } + ((SN_local *)z)->b_GE_removed = 1; + { + int ret = slice_del(z); if (ret < 0) return ret; } + { + int v_7 = z->c; + z->bra = z->c; + if (z->c + 1 >= z->l || (z->p[z->c + 1] != 171 && z->p[z->c + 1] != 175)) goto lab6; + among_var = find_among(z, a_11, 2, 0); + if (!among_var) goto lab6; + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_83); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_84); + if (ret < 0) return ret; + } + break; + } + lab6: + z->c = v_7; + } + return 1; +} + +static int r_measure(struct SN_env * z) { + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + while (1) { + if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab1; + continue; + lab1: + break; + } + { + int v_2 = 1; + while (1) { + int v_3 = z->c; + do { + int v_4 = z->c; + if (!(eq_s(z, 2, s_85))) goto lab3; + break; + lab3: + z->c = v_4; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab2; + } while (0); + v_2--; + continue; + lab2: + z->c = v_3; + break; + } + if (v_2 > 0) goto lab0; + } + if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab0; + ((SN_local *)z)->i_p1 = z->c; + while (1) { + if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab4; + continue; + lab4: + break; + } + { + int v_5 = 1; + while (1) { + int v_6 = z->c; + do { + int v_7 = z->c; + if (!(eq_s(z, 2, s_86))) goto lab6; + break; + lab6: + z->c = v_7; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab5; + } while (0); + v_5--; + continue; + lab5: + z->c = v_6; + break; + } + if (v_5 > 0) goto lab0; + } + if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab0; + ((SN_local *)z)->i_p2 = z->c; + lab0: + z->c = v_1; + } + return 1; +} + +extern int dutch_UTF_8_stem(struct SN_env * z) { + int b_stemmed; + b_stemmed = 0; + { + int ret = r_measure(z); + if (ret <= 0) return ret; + } + z->lb = z->c; z->c = z->l; + { + int v_1 = z->l - z->c; + { + int ret = r_Step_1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab0: + z->c = z->l - v_1; + } + { + int v_2 = z->l - z->c; + { + int ret = r_Step_2(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab1: + z->c = z->l - v_2; + } + { + int v_3 = z->l - z->c; + { + int ret = r_Step_3(z); + if (ret == 0) goto lab2; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab2: + z->c = z->l - v_3; + } + { + int v_4 = z->l - z->c; + { + int ret = r_Step_4(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab3: + z->c = z->l - v_4; + } z->c = z->lb; - { int c3 = z->c; - { int ret = r_postlude(z); + ((SN_local *)z)->b_GE_removed = 0; + { + int v_5 = z->c; + { + int v_6 = z->c; + { + int ret = r_Lose_prefix(z); + if (ret == 0) goto lab4; + if (ret < 0) return ret; + } + z->c = v_6; + { + int ret = r_measure(z); + if (ret < 0) return ret; + } + } + lab4: + z->c = v_5; + } + z->lb = z->c; z->c = z->l; + { + int v_7 = z->l - z->c; + if (!((SN_local *)z)->b_GE_removed) goto lab5; + b_stemmed = 1; + { + int ret = r_Step_1c(z); + if (ret == 0) goto lab5; + if (ret < 0) return ret; + } + lab5: + z->c = z->l - v_7; + } + z->c = z->lb; + ((SN_local *)z)->b_GE_removed = 0; + { + int v_8 = z->c; + { + int v_9 = z->c; + { + int ret = r_Lose_infix(z); + if (ret == 0) goto lab6; + if (ret < 0) return ret; + } + z->c = v_9; + { + int ret = r_measure(z); + if (ret < 0) return ret; + } + } + lab6: + z->c = v_8; + } + z->lb = z->c; z->c = z->l; + { + int v_10 = z->l - z->c; + if (!((SN_local *)z)->b_GE_removed) goto lab7; + b_stemmed = 1; + { + int ret = r_Step_1c(z); + if (ret == 0) goto lab7; + if (ret < 0) return ret; + } + lab7: + z->c = z->l - v_10; + } + z->c = z->lb; + z->lb = z->c; z->c = z->l; + { + int v_11 = z->l - z->c; + { + int ret = r_Step_7(z); + if (ret == 0) goto lab8; + if (ret < 0) return ret; + } + b_stemmed = 1; + lab8: + z->c = z->l - v_11; + } + { + int v_12 = z->l - z->c; + if (!b_stemmed) goto lab9; + { + int ret = r_Step_6(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - z->c = c3; + lab9: + z->c = z->l - v_12; } + z->c = z->lb; return 1; } -extern struct SN_env * dutch_UTF_8_create_env(void) { return SN_create_env(0, 4); } +extern struct SN_env * dutch_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_GE_removed = 0; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->s_ch = NULL; -extern void dutch_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } + if ((((SN_local *)z)->s_ch = create_s()) == NULL) { + dutch_UTF_8_close_env(z); + return NULL; + } + } + return z; +} + +extern void dutch_UTF_8_close_env(struct SN_env * z) { + if (z) { + lose_s(((SN_local *)z)->s_ch); + } + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_dutch_porter.c b/src/backend/snowball/libstemmer/stem_UTF_8_dutch_porter.c new file mode 100644 index 0000000000000..0726754effe22 --- /dev/null +++ b/src/backend/snowball/libstemmer/stem_UTF_8_dutch_porter.c @@ -0,0 +1,680 @@ +/* Generated from dutch_porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#include "stem_UTF_8_dutch_porter.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_e_found; +}; + +typedef struct SN_local SN_local; + +#ifdef __cplusplus +extern "C" { +#endif +extern int dutch_porter_UTF_8_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif + +static int r_standard_suffix(struct SN_env * z); +static int r_undouble(struct SN_env * z); +static int r_R2(struct SN_env * z); +static int r_R1(struct SN_env * z); +static int r_mark_regions(struct SN_env * z); +static int r_en_ending(struct SN_env * z); +static int r_e_ending(struct SN_env * z); +static int r_postlude(struct SN_env * z); +static int r_prelude(struct SN_env * z); + +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'i' }; +static const symbol s_3[] = { 'o' }; +static const symbol s_4[] = { 'u' }; +static const symbol s_5[] = { 'Y' }; +static const symbol s_6[] = { 'I' }; +static const symbol s_7[] = { 'Y' }; +static const symbol s_8[] = { 'y' }; +static const symbol s_9[] = { 'i' }; +static const symbol s_10[] = { 'g', 'e', 'm' }; +static const symbol s_11[] = { 'h', 'e', 'i', 'd' }; +static const symbol s_12[] = { 'h', 'e', 'i', 'd' }; +static const symbol s_13[] = { 'e', 'n' }; +static const symbol s_14[] = { 'i', 'g' }; + +static const symbol s_0_1[2] = { 0xC3, 0xA1 }; +static const symbol s_0_2[2] = { 0xC3, 0xA4 }; +static const symbol s_0_3[2] = { 0xC3, 0xA9 }; +static const symbol s_0_4[2] = { 0xC3, 0xAB }; +static const symbol s_0_5[2] = { 0xC3, 0xAD }; +static const symbol s_0_6[2] = { 0xC3, 0xAF }; +static const symbol s_0_7[2] = { 0xC3, 0xB3 }; +static const symbol s_0_8[2] = { 0xC3, 0xB6 }; +static const symbol s_0_9[2] = { 0xC3, 0xBA }; +static const symbol s_0_10[2] = { 0xC3, 0xBC }; +static const struct among a_0[11] = { +{ 0, 0, 0, 6, 0}, +{ 2, s_0_1, -1, 1, 0}, +{ 2, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, 2, 0}, +{ 2, s_0_4, -4, 2, 0}, +{ 2, s_0_5, -5, 3, 0}, +{ 2, s_0_6, -6, 3, 0}, +{ 2, s_0_7, -7, 4, 0}, +{ 2, s_0_8, -8, 4, 0}, +{ 2, s_0_9, -9, 5, 0}, +{ 2, s_0_10, -10, 5, 0} +}; + +static const symbol s_1_1[1] = { 'I' }; +static const symbol s_1_2[1] = { 'Y' }; +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 1, s_1_1, -1, 2, 0}, +{ 1, s_1_2, -2, 1, 0} +}; + +static const symbol s_2_0[2] = { 'd', 'd' }; +static const symbol s_2_1[2] = { 'k', 'k' }; +static const symbol s_2_2[2] = { 't', 't' }; +static const struct among a_2[3] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0} +}; + +static const symbol s_3_0[3] = { 'e', 'n', 'e' }; +static const symbol s_3_1[2] = { 's', 'e' }; +static const symbol s_3_2[2] = { 'e', 'n' }; +static const symbol s_3_3[5] = { 'h', 'e', 'd', 'e', 'n' }; +static const symbol s_3_4[1] = { 's' }; +static const struct among a_3[5] = { +{ 3, s_3_0, 0, 2, 0}, +{ 2, s_3_1, 0, 3, 0}, +{ 2, s_3_2, 0, 2, 0}, +{ 5, s_3_3, -1, 1, 0}, +{ 1, s_3_4, 0, 3, 0} +}; + +static const symbol s_4_0[3] = { 'e', 'n', 'd' }; +static const symbol s_4_1[2] = { 'i', 'g' }; +static const symbol s_4_2[3] = { 'i', 'n', 'g' }; +static const symbol s_4_3[4] = { 'l', 'i', 'j', 'k' }; +static const symbol s_4_4[4] = { 'b', 'a', 'a', 'r' }; +static const symbol s_4_5[3] = { 'b', 'a', 'r' }; +static const struct among a_4[6] = { +{ 3, s_4_0, 0, 1, 0}, +{ 2, s_4_1, 0, 2, 0}, +{ 3, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 3, 0}, +{ 4, s_4_4, 0, 4, 0}, +{ 3, s_4_5, 0, 5, 0} +}; + +static const symbol s_5_0[2] = { 'a', 'a' }; +static const symbol s_5_1[2] = { 'e', 'e' }; +static const symbol s_5_2[2] = { 'o', 'o' }; +static const symbol s_5_3[2] = { 'u', 'u' }; +static const struct among a_5[4] = { +{ 2, s_5_0, 0, -1, 0}, +{ 2, s_5_1, 0, -1, 0}, +{ 2, s_5_2, 0, -1, 0}, +{ 2, s_5_3, 0, -1, 0} +}; + +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + +static const unsigned char g_v_I[] = { 1, 0, 0, 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + +static const unsigned char g_v_j[] = { 17, 67, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + +static int r_prelude(struct SN_env * z) { + int among_var; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + z->bra = z->c; + if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 5 || !((340306450 >> (z->p[z->c + 1] & 0x1f)) & 1)) among_var = 6; else + among_var = find_among(z, a_0, 11, 0); + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_0); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_1); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_2); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_3); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_from_s(z, 1, s_4); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) goto lab0; + z->c = ret; + } + break; + } + continue; + lab0: + z->c = v_2; + break; + } + z->c = v_1; + } + { + int v_3 = z->c; + z->bra = z->c; + if (z->c == z->l || z->p[z->c] != 'y') { z->c = v_3; goto lab1; } + z->c++; + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_5); + if (ret < 0) return ret; + } + lab1: + ; + } + while (1) { + int v_4 = z->c; + { + int ret = out_grouping_U(z, g_v, 97, 232, 1); + if (ret < 0) goto lab2; + z->c += ret; + } + { + int v_5 = z->c; + z->bra = z->c; + do { + int v_6 = z->c; + if (z->c == z->l || z->p[z->c] != 'i') goto lab4; + z->c++; + z->ket = z->c; + { + int v_7 = z->c; + if (in_grouping_U(z, g_v, 97, 232, 0)) goto lab5; + { + int ret = slice_from_s(z, 1, s_6); + if (ret < 0) return ret; + } + lab5: + z->c = v_7; + } + break; + lab4: + z->c = v_6; + if (z->c == z->l || z->p[z->c] != 'y') { z->c = v_5; goto lab3; } + z->c++; + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_7); + if (ret < 0) return ret; + } + } while (0); + lab3: + ; + } + continue; + lab2: + z->c = v_4; + break; + } + return 1; +} + +static int r_mark_regions(struct SN_env * z) { + int i_x; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); + if (ret < 0) return 0; + z->c = ret; + } + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping_U(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + { + int ret = in_grouping_U(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; +lab0: + { + int ret = out_grouping_U(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + { + int ret = in_grouping_U(z, g_v, 97, 232, 1); + if (ret < 0) return 0; + z->c += ret; + } + ((SN_local *)z)->i_p2 = z->c; + return 1; +} + +static int r_postlude(struct SN_env * z) { + int among_var; + while (1) { + int v_1 = z->c; + z->bra = z->c; + if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 89)) among_var = 3; else + among_var = find_among(z, a_1, 3, 0); + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_8); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_9); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) goto lab0; + z->c = ret; + } + break; + } + continue; + lab0: + z->c = v_1; + break; + } + return 1; +} + +static int r_R1(struct SN_env * z) { + return ((SN_local *)z)->i_p1 <= z->c; +} + +static int r_R2(struct SN_env * z) { + return ((SN_local *)z)->i_p2 <= z->c; +} + +static int r_undouble(struct SN_env * z) { + { + int v_1 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1050640 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + if (!find_among_b(z, a_2, 3, 0)) return 0; + z->c = z->l - v_1; + } + z->ket = z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) return 0; + z->c = ret; + } + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return 1; +} + +static int r_e_ending(struct SN_env * z) { + ((SN_local *)z)->b_e_found = 0; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; + z->c--; + z->bra = z->c; + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + if (out_grouping_b_U(z, g_v, 97, 232, 0)) return 0; + z->c = z->l - v_1; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + ((SN_local *)z)->b_e_found = 1; + return r_undouble(z); +} + +static int r_en_ending(struct SN_env * z) { + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + if (out_grouping_b_U(z, g_v, 97, 232, 0)) return 0; + z->c = z->l - v_1; + { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 3, s_10))) goto lab0; + return 0; + lab0: + z->c = z->l - v_2; + } + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return r_undouble(z); +} + +static int r_standard_suffix(struct SN_env * z) { + int among_var; + { + int v_1 = z->l - z->c; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540704 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_3, 5, 0); + if (!among_var) goto lab0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + { + int ret = slice_from_s(z, 4, s_11); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = r_en_ending(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R1(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + if (out_grouping_b_U(z, g_v_j, 97, 232, 0)) goto lab0; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + lab0: + z->c = z->l - v_1; + } + { + int v_2 = z->l - z->c; + { + int ret = r_e_ending(z); + if (ret < 0) return ret; + } + z->c = z->l - v_2; + } + { + int v_3 = z->l - z->c; + z->ket = z->c; + if (!(eq_s_b(z, 4, s_12))) goto lab1; + z->bra = z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + { + int v_4 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab2; + z->c--; + goto lab1; + lab2: + z->c = z->l - v_4; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + z->ket = z->c; + if (!(eq_s_b(z, 2, s_13))) goto lab1; + z->bra = z->c; + { + int ret = r_en_ending(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + lab1: + z->c = z->l - v_3; + } + { + int v_5 = z->l - z->c; + z->ket = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((264336 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab3; + among_var = find_among_b(z, a_4, 6, 0); + if (!among_var) goto lab3; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + do { + int v_6 = z->l - z->c; + z->ket = z->c; + if (!(eq_s_b(z, 2, s_14))) goto lab4; + z->bra = z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab4; + if (ret < 0) return ret; + } + { + int v_7 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab5; + z->c--; + goto lab4; + lab5: + z->c = z->l - v_7; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + lab4: + z->c = z->l - v_6; + { + int ret = r_undouble(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + } while (0); + break; + case 2: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int v_8 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab6; + z->c--; + goto lab3; + lab6: + z->c = z->l - v_8; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int ret = r_e_ending(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = r_R2(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + if (!((SN_local *)z)->b_e_found) goto lab3; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + lab3: + z->c = z->l - v_5; + } + { + int v_9 = z->l - z->c; + if (out_grouping_b_U(z, g_v_I, 73, 232, 0)) goto lab7; + { + int v_10 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2129954 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab7; + if (!find_among_b(z, a_5, 4, 0)) goto lab7; + if (out_grouping_b_U(z, g_v, 97, 232, 0)) goto lab7; + z->c = z->l - v_10; + } + z->ket = z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab7; + z->c = ret; + } + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + lab7: + z->c = z->l - v_9; + } + return 1; +} + +extern int dutch_porter_UTF_8_stem(struct SN_env * z) { + { + int v_1 = z->c; + { + int ret = r_prelude(z); + if (ret < 0) return ret; + } + z->c = v_1; + } + { + int v_2 = z->c; + { + int ret = r_mark_regions(z); + if (ret < 0) return ret; + } + z->c = v_2; + } + z->lb = z->c; z->c = z->l; + { + int ret = r_standard_suffix(z); + if (ret < 0) return ret; + } + z->c = z->lb; + { + int v_3 = z->c; + { + int ret = r_postlude(z); + if (ret < 0) return ret; + } + z->c = v_3; + } + return 1; +} + +extern struct SN_env * dutch_porter_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->b_e_found = 0; + } + return z; +} + +extern void dutch_porter_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} + diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_english.c b/src/backend/snowball/libstemmer/stem_UTF_8_english.c index 25144ad24afd2..b0a20b52818ff 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_english.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_english.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from english.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_english.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_Y_found; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,7 +22,7 @@ extern int english_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif -static int r_exception2(struct SN_env * z); + static int r_exception1(struct SN_env * z); static int r_Step_5(struct SN_env * z); static int r_Step_4(struct SN_env * z); @@ -24,38 +37,75 @@ static int r_shortv(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * english_UTF_8_create_env(void); -extern void english_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'Y' }; +static const symbol s_1[] = { 'Y' }; +static const symbol s_2[] = { 'p', 'a', 's', 't' }; +static const symbol s_3[] = { 's', 's' }; +static const symbol s_4[] = { 'i' }; +static const symbol s_5[] = { 'i', 'e' }; +static const symbol s_6[] = { 'e', 'e' }; +static const symbol s_7[] = { 'i', 'e' }; +static const symbol s_8[] = { 'e' }; +static const symbol s_9[] = { 'e' }; +static const symbol s_10[] = { 'i' }; +static const symbol s_11[] = { 't', 'i', 'o', 'n' }; +static const symbol s_12[] = { 'e', 'n', 'c', 'e' }; +static const symbol s_13[] = { 'a', 'n', 'c', 'e' }; +static const symbol s_14[] = { 'a', 'b', 'l', 'e' }; +static const symbol s_15[] = { 'e', 'n', 't' }; +static const symbol s_16[] = { 'i', 'z', 'e' }; +static const symbol s_17[] = { 'a', 't', 'e' }; +static const symbol s_18[] = { 'a', 'l' }; +static const symbol s_19[] = { 'f', 'u', 'l' }; +static const symbol s_20[] = { 'o', 'u', 's' }; +static const symbol s_21[] = { 'i', 'v', 'e' }; +static const symbol s_22[] = { 'b', 'l', 'e' }; +static const symbol s_23[] = { 'o', 'g' }; +static const symbol s_24[] = { 'o', 'g' }; +static const symbol s_25[] = { 'l', 'e', 's', 's' }; +static const symbol s_26[] = { 't', 'i', 'o', 'n' }; +static const symbol s_27[] = { 'a', 't', 'e' }; +static const symbol s_28[] = { 'a', 'l' }; +static const symbol s_29[] = { 'i', 'c' }; +static const symbol s_30[] = { 's', 'k', 'i' }; +static const symbol s_31[] = { 's', 'k', 'y' }; +static const symbol s_32[] = { 'i', 'd', 'l' }; +static const symbol s_33[] = { 'g', 'e', 'n', 't', 'l' }; +static const symbol s_34[] = { 'u', 'g', 'l', 'i' }; +static const symbol s_35[] = { 'e', 'a', 'r', 'l', 'i' }; +static const symbol s_36[] = { 'o', 'n', 'l', 'i' }; +static const symbol s_37[] = { 's', 'i', 'n', 'g', 'l' }; +static const symbol s_38[] = { 'y' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[5] = { 'a', 'r', 's', 'e', 'n' }; static const symbol s_0_1[6] = { 'c', 'o', 'm', 'm', 'u', 'n' }; -static const symbol s_0_2[5] = { 'g', 'e', 'n', 'e', 'r' }; - -static const struct among a_0[3] = -{ -{ 5, s_0_0, -1, -1, 0}, -{ 6, s_0_1, -1, -1, 0}, -{ 5, s_0_2, -1, -1, 0} +static const symbol s_0_2[5] = { 'e', 'm', 'e', 'r', 'g' }; +static const symbol s_0_3[5] = { 'g', 'e', 'n', 'e', 'r' }; +static const symbol s_0_4[5] = { 'i', 'n', 't', 'e', 'r' }; +static const symbol s_0_5[5] = { 'l', 'a', 't', 'e', 'r' }; +static const symbol s_0_6[5] = { 'o', 'r', 'g', 'a', 'n' }; +static const symbol s_0_7[4] = { 'p', 'a', 's', 't' }; +static const symbol s_0_8[7] = { 'u', 'n', 'i', 'v', 'e', 'r', 's' }; +static const struct among a_0[9] = { +{ 5, s_0_0, 0, -1, 0}, +{ 6, s_0_1, 0, -1, 0}, +{ 5, s_0_2, 0, -1, 0}, +{ 5, s_0_3, 0, -1, 0}, +{ 5, s_0_4, 0, -1, 0}, +{ 5, s_0_5, 0, -1, 0}, +{ 5, s_0_6, 0, -1, 0}, +{ 4, s_0_7, 0, -1, 0}, +{ 7, s_0_8, 0, -1, 0} }; static const symbol s_1_0[1] = { '\'' }; static const symbol s_1_1[3] = { '\'', 's', '\'' }; static const symbol s_1_2[2] = { '\'', 's' }; - -static const struct among a_1[3] = -{ -{ 1, s_1_0, -1, 1, 0}, -{ 3, s_1_1, 0, 1, 0}, -{ 2, s_1_2, -1, 1, 0} +static const struct among a_1[3] = { +{ 1, s_1_0, 0, 1, 0}, +{ 3, s_1_1, -1, 1, 0}, +{ 2, s_1_2, 0, 1, 0} }; static const symbol s_2_0[3] = { 'i', 'e', 'd' }; @@ -64,250 +114,236 @@ static const symbol s_2_2[3] = { 'i', 'e', 's' }; static const symbol s_2_3[4] = { 's', 's', 'e', 's' }; static const symbol s_2_4[2] = { 's', 's' }; static const symbol s_2_5[2] = { 'u', 's' }; - -static const struct among a_2[6] = -{ -{ 3, s_2_0, -1, 2, 0}, -{ 1, s_2_1, -1, 3, 0}, -{ 3, s_2_2, 1, 2, 0}, -{ 4, s_2_3, 1, 1, 0}, -{ 2, s_2_4, 1, -1, 0}, -{ 2, s_2_5, 1, -1, 0} +static const struct among a_2[6] = { +{ 3, s_2_0, 0, 2, 0}, +{ 1, s_2_1, 0, 3, 0}, +{ 3, s_2_2, -1, 2, 0}, +{ 4, s_2_3, -2, 1, 0}, +{ 2, s_2_4, -3, -1, 0}, +{ 2, s_2_5, -4, -1, 0} }; -static const symbol s_3_1[2] = { 'b', 'b' }; -static const symbol s_3_2[2] = { 'd', 'd' }; -static const symbol s_3_3[2] = { 'f', 'f' }; -static const symbol s_3_4[2] = { 'g', 'g' }; -static const symbol s_3_5[2] = { 'b', 'l' }; -static const symbol s_3_6[2] = { 'm', 'm' }; -static const symbol s_3_7[2] = { 'n', 'n' }; -static const symbol s_3_8[2] = { 'p', 'p' }; -static const symbol s_3_9[2] = { 'r', 'r' }; -static const symbol s_3_10[2] = { 'a', 't' }; -static const symbol s_3_11[2] = { 't', 't' }; -static const symbol s_3_12[2] = { 'i', 'z' }; - -static const struct among a_3[13] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_3_1, 0, 2, 0}, -{ 2, s_3_2, 0, 2, 0}, -{ 2, s_3_3, 0, 2, 0}, -{ 2, s_3_4, 0, 2, 0}, -{ 2, s_3_5, 0, 1, 0}, -{ 2, s_3_6, 0, 2, 0}, -{ 2, s_3_7, 0, 2, 0}, -{ 2, s_3_8, 0, 2, 0}, -{ 2, s_3_9, 0, 2, 0}, -{ 2, s_3_10, 0, 1, 0}, -{ 2, s_3_11, 0, 2, 0}, -{ 2, s_3_12, 0, 1, 0} +static const symbol s_3_0[4] = { 's', 'u', 'c', 'c' }; +static const symbol s_3_1[4] = { 'p', 'r', 'o', 'c' }; +static const symbol s_3_2[3] = { 'e', 'x', 'c' }; +static const struct among a_3[3] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 3, s_3_2, 0, 1, 0} }; -static const symbol s_4_0[2] = { 'e', 'd' }; -static const symbol s_4_1[3] = { 'e', 'e', 'd' }; -static const symbol s_4_2[3] = { 'i', 'n', 'g' }; -static const symbol s_4_3[4] = { 'e', 'd', 'l', 'y' }; -static const symbol s_4_4[5] = { 'e', 'e', 'd', 'l', 'y' }; -static const symbol s_4_5[5] = { 'i', 'n', 'g', 'l', 'y' }; - -static const struct among a_4[6] = -{ -{ 2, s_4_0, -1, 2, 0}, -{ 3, s_4_1, 0, 1, 0}, -{ 3, s_4_2, -1, 2, 0}, -{ 4, s_4_3, -1, 2, 0}, -{ 5, s_4_4, 3, 1, 0}, -{ 5, s_4_5, -1, 2, 0} +static const symbol s_4_0[4] = { 'e', 'v', 'e', 'n' }; +static const symbol s_4_1[4] = { 'c', 'a', 'n', 'n' }; +static const symbol s_4_2[3] = { 'i', 'n', 'n' }; +static const symbol s_4_3[4] = { 'e', 'a', 'r', 'r' }; +static const symbol s_4_4[4] = { 'h', 'e', 'r', 'r' }; +static const symbol s_4_5[3] = { 'o', 'u', 't' }; +static const symbol s_4_6[1] = { 'y' }; +static const struct among a_4[7] = { +{ 4, s_4_0, 0, 2, 0}, +{ 4, s_4_1, 0, 2, 0}, +{ 3, s_4_2, 0, 2, 0}, +{ 4, s_4_3, 0, 2, 0}, +{ 4, s_4_4, 0, 2, 0}, +{ 3, s_4_5, 0, 2, 0}, +{ 1, s_4_6, 0, 1, 0} }; -static const symbol s_5_0[4] = { 'a', 'n', 'c', 'i' }; -static const symbol s_5_1[4] = { 'e', 'n', 'c', 'i' }; -static const symbol s_5_2[3] = { 'o', 'g', 'i' }; -static const symbol s_5_3[2] = { 'l', 'i' }; -static const symbol s_5_4[3] = { 'b', 'l', 'i' }; -static const symbol s_5_5[4] = { 'a', 'b', 'l', 'i' }; -static const symbol s_5_6[4] = { 'a', 'l', 'l', 'i' }; -static const symbol s_5_7[5] = { 'f', 'u', 'l', 'l', 'i' }; -static const symbol s_5_8[6] = { 'l', 'e', 's', 's', 'l', 'i' }; -static const symbol s_5_9[5] = { 'o', 'u', 's', 'l', 'i' }; -static const symbol s_5_10[5] = { 'e', 'n', 't', 'l', 'i' }; -static const symbol s_5_11[5] = { 'a', 'l', 'i', 't', 'i' }; -static const symbol s_5_12[6] = { 'b', 'i', 'l', 'i', 't', 'i' }; -static const symbol s_5_13[5] = { 'i', 'v', 'i', 't', 'i' }; -static const symbol s_5_14[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_5_15[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_5_16[5] = { 'a', 'l', 'i', 's', 'm' }; -static const symbol s_5_17[5] = { 'a', 't', 'i', 'o', 'n' }; -static const symbol s_5_18[7] = { 'i', 'z', 'a', 't', 'i', 'o', 'n' }; -static const symbol s_5_19[4] = { 'i', 'z', 'e', 'r' }; -static const symbol s_5_20[4] = { 'a', 't', 'o', 'r' }; -static const symbol s_5_21[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; -static const symbol s_5_22[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; -static const symbol s_5_23[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; - -static const struct among a_5[24] = -{ -{ 4, s_5_0, -1, 3, 0}, -{ 4, s_5_1, -1, 2, 0}, -{ 3, s_5_2, -1, 13, 0}, -{ 2, s_5_3, -1, 15, 0}, -{ 3, s_5_4, 3, 12, 0}, -{ 4, s_5_5, 4, 4, 0}, -{ 4, s_5_6, 3, 8, 0}, -{ 5, s_5_7, 3, 9, 0}, -{ 6, s_5_8, 3, 14, 0}, -{ 5, s_5_9, 3, 10, 0}, -{ 5, s_5_10, 3, 5, 0}, -{ 5, s_5_11, -1, 8, 0}, -{ 6, s_5_12, -1, 12, 0}, -{ 5, s_5_13, -1, 11, 0}, -{ 6, s_5_14, -1, 1, 0}, -{ 7, s_5_15, 14, 7, 0}, -{ 5, s_5_16, -1, 8, 0}, -{ 5, s_5_17, -1, 7, 0}, -{ 7, s_5_18, 17, 6, 0}, -{ 4, s_5_19, -1, 6, 0}, -{ 4, s_5_20, -1, 7, 0}, -{ 7, s_5_21, -1, 11, 0}, -{ 7, s_5_22, -1, 9, 0}, -{ 7, s_5_23, -1, 10, 0} +static const symbol s_5_1[2] = { 'e', 'd' }; +static const symbol s_5_2[3] = { 'e', 'e', 'd' }; +static const symbol s_5_3[3] = { 'i', 'n', 'g' }; +static const symbol s_5_4[4] = { 'e', 'd', 'l', 'y' }; +static const symbol s_5_5[5] = { 'e', 'e', 'd', 'l', 'y' }; +static const symbol s_5_6[5] = { 'i', 'n', 'g', 'l', 'y' }; +static const struct among a_5[7] = { +{ 0, 0, 0, -1, 0}, +{ 2, s_5_1, -1, 2, 0}, +{ 3, s_5_2, -1, 1, 0}, +{ 3, s_5_3, -3, 3, 0}, +{ 4, s_5_4, -4, 2, 0}, +{ 5, s_5_5, -1, 1, 0}, +{ 5, s_5_6, -6, 2, 0} }; -static const symbol s_6_0[5] = { 'i', 'c', 'a', 't', 'e' }; -static const symbol s_6_1[5] = { 'a', 't', 'i', 'v', 'e' }; -static const symbol s_6_2[5] = { 'a', 'l', 'i', 'z', 'e' }; -static const symbol s_6_3[5] = { 'i', 'c', 'i', 't', 'i' }; -static const symbol s_6_4[4] = { 'i', 'c', 'a', 'l' }; -static const symbol s_6_5[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_6_6[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; -static const symbol s_6_7[3] = { 'f', 'u', 'l' }; -static const symbol s_6_8[4] = { 'n', 'e', 's', 's' }; - -static const struct among a_6[9] = -{ -{ 5, s_6_0, -1, 4, 0}, -{ 5, s_6_1, -1, 6, 0}, -{ 5, s_6_2, -1, 3, 0}, -{ 5, s_6_3, -1, 4, 0}, -{ 4, s_6_4, -1, 4, 0}, -{ 6, s_6_5, -1, 1, 0}, -{ 7, s_6_6, 5, 2, 0}, -{ 3, s_6_7, -1, 5, 0}, -{ 4, s_6_8, -1, 5, 0} +static const symbol s_6_1[2] = { 'b', 'b' }; +static const symbol s_6_2[2] = { 'd', 'd' }; +static const symbol s_6_3[2] = { 'f', 'f' }; +static const symbol s_6_4[2] = { 'g', 'g' }; +static const symbol s_6_5[2] = { 'b', 'l' }; +static const symbol s_6_6[2] = { 'm', 'm' }; +static const symbol s_6_7[2] = { 'n', 'n' }; +static const symbol s_6_8[2] = { 'p', 'p' }; +static const symbol s_6_9[2] = { 'r', 'r' }; +static const symbol s_6_10[2] = { 'a', 't' }; +static const symbol s_6_11[2] = { 't', 't' }; +static const symbol s_6_12[2] = { 'i', 'z' }; +static const struct among a_6[13] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_6_1, -1, 2, 0}, +{ 2, s_6_2, -2, 2, 0}, +{ 2, s_6_3, -3, 2, 0}, +{ 2, s_6_4, -4, 2, 0}, +{ 2, s_6_5, -5, 1, 0}, +{ 2, s_6_6, -6, 2, 0}, +{ 2, s_6_7, -7, 2, 0}, +{ 2, s_6_8, -8, 2, 0}, +{ 2, s_6_9, -9, 2, 0}, +{ 2, s_6_10, -10, 1, 0}, +{ 2, s_6_11, -11, 2, 0}, +{ 2, s_6_12, -12, 1, 0} }; -static const symbol s_7_0[2] = { 'i', 'c' }; -static const symbol s_7_1[4] = { 'a', 'n', 'c', 'e' }; -static const symbol s_7_2[4] = { 'e', 'n', 'c', 'e' }; -static const symbol s_7_3[4] = { 'a', 'b', 'l', 'e' }; -static const symbol s_7_4[4] = { 'i', 'b', 'l', 'e' }; -static const symbol s_7_5[3] = { 'a', 't', 'e' }; -static const symbol s_7_6[3] = { 'i', 'v', 'e' }; -static const symbol s_7_7[3] = { 'i', 'z', 'e' }; -static const symbol s_7_8[3] = { 'i', 't', 'i' }; -static const symbol s_7_9[2] = { 'a', 'l' }; -static const symbol s_7_10[3] = { 'i', 's', 'm' }; -static const symbol s_7_11[3] = { 'i', 'o', 'n' }; -static const symbol s_7_12[2] = { 'e', 'r' }; -static const symbol s_7_13[3] = { 'o', 'u', 's' }; -static const symbol s_7_14[3] = { 'a', 'n', 't' }; -static const symbol s_7_15[3] = { 'e', 'n', 't' }; -static const symbol s_7_16[4] = { 'm', 'e', 'n', 't' }; -static const symbol s_7_17[5] = { 'e', 'm', 'e', 'n', 't' }; - -static const struct among a_7[18] = -{ -{ 2, s_7_0, -1, 1, 0}, -{ 4, s_7_1, -1, 1, 0}, -{ 4, s_7_2, -1, 1, 0}, -{ 4, s_7_3, -1, 1, 0}, -{ 4, s_7_4, -1, 1, 0}, -{ 3, s_7_5, -1, 1, 0}, -{ 3, s_7_6, -1, 1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 3, s_7_8, -1, 1, 0}, -{ 2, s_7_9, -1, 1, 0}, -{ 3, s_7_10, -1, 1, 0}, -{ 3, s_7_11, -1, 2, 0}, -{ 2, s_7_12, -1, 1, 0}, -{ 3, s_7_13, -1, 1, 0}, -{ 3, s_7_14, -1, 1, 0}, -{ 3, s_7_15, -1, 1, 0}, -{ 4, s_7_16, 15, 1, 0}, -{ 5, s_7_17, 16, 1, 0} +static const symbol s_7_0[4] = { 'a', 'n', 'c', 'i' }; +static const symbol s_7_1[4] = { 'e', 'n', 'c', 'i' }; +static const symbol s_7_2[3] = { 'o', 'g', 'i' }; +static const symbol s_7_3[2] = { 'l', 'i' }; +static const symbol s_7_4[3] = { 'b', 'l', 'i' }; +static const symbol s_7_5[4] = { 'a', 'b', 'l', 'i' }; +static const symbol s_7_6[4] = { 'a', 'l', 'l', 'i' }; +static const symbol s_7_7[5] = { 'f', 'u', 'l', 'l', 'i' }; +static const symbol s_7_8[6] = { 'l', 'e', 's', 's', 'l', 'i' }; +static const symbol s_7_9[5] = { 'o', 'u', 's', 'l', 'i' }; +static const symbol s_7_10[5] = { 'e', 'n', 't', 'l', 'i' }; +static const symbol s_7_11[5] = { 'a', 'l', 'i', 't', 'i' }; +static const symbol s_7_12[6] = { 'b', 'i', 'l', 'i', 't', 'i' }; +static const symbol s_7_13[5] = { 'i', 'v', 'i', 't', 'i' }; +static const symbol s_7_14[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_7_15[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_7_16[5] = { 'a', 'l', 'i', 's', 'm' }; +static const symbol s_7_17[5] = { 'a', 't', 'i', 'o', 'n' }; +static const symbol s_7_18[7] = { 'i', 'z', 'a', 't', 'i', 'o', 'n' }; +static const symbol s_7_19[4] = { 'i', 'z', 'e', 'r' }; +static const symbol s_7_20[4] = { 'a', 't', 'o', 'r' }; +static const symbol s_7_21[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; +static const symbol s_7_22[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; +static const symbol s_7_23[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; +static const symbol s_7_24[5] = { 'o', 'g', 'i', 's', 't' }; +static const struct among a_7[25] = { +{ 4, s_7_0, 0, 3, 0}, +{ 4, s_7_1, 0, 2, 0}, +{ 3, s_7_2, 0, 14, 0}, +{ 2, s_7_3, 0, 16, 0}, +{ 3, s_7_4, -1, 12, 0}, +{ 4, s_7_5, -1, 4, 0}, +{ 4, s_7_6, -3, 8, 0}, +{ 5, s_7_7, -4, 9, 0}, +{ 6, s_7_8, -5, 15, 0}, +{ 5, s_7_9, -6, 10, 0}, +{ 5, s_7_10, -7, 5, 0}, +{ 5, s_7_11, 0, 8, 0}, +{ 6, s_7_12, 0, 12, 0}, +{ 5, s_7_13, 0, 11, 0}, +{ 6, s_7_14, 0, 1, 0}, +{ 7, s_7_15, -1, 7, 0}, +{ 5, s_7_16, 0, 8, 0}, +{ 5, s_7_17, 0, 7, 0}, +{ 7, s_7_18, -1, 6, 0}, +{ 4, s_7_19, 0, 6, 0}, +{ 4, s_7_20, 0, 7, 0}, +{ 7, s_7_21, 0, 11, 0}, +{ 7, s_7_22, 0, 9, 0}, +{ 7, s_7_23, 0, 10, 0}, +{ 5, s_7_24, 0, 13, 0} }; -static const symbol s_8_0[1] = { 'e' }; -static const symbol s_8_1[1] = { 'l' }; - -static const struct among a_8[2] = -{ -{ 1, s_8_0, -1, 1, 0}, -{ 1, s_8_1, -1, 2, 0} +static const symbol s_8_0[5] = { 'i', 'c', 'a', 't', 'e' }; +static const symbol s_8_1[5] = { 'a', 't', 'i', 'v', 'e' }; +static const symbol s_8_2[5] = { 'a', 'l', 'i', 'z', 'e' }; +static const symbol s_8_3[5] = { 'i', 'c', 'i', 't', 'i' }; +static const symbol s_8_4[4] = { 'i', 'c', 'a', 'l' }; +static const symbol s_8_5[6] = { 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_8_6[7] = { 'a', 't', 'i', 'o', 'n', 'a', 'l' }; +static const symbol s_8_7[3] = { 'f', 'u', 'l' }; +static const symbol s_8_8[4] = { 'n', 'e', 's', 's' }; +static const struct among a_8[9] = { +{ 5, s_8_0, 0, 4, 0}, +{ 5, s_8_1, 0, 6, 0}, +{ 5, s_8_2, 0, 3, 0}, +{ 5, s_8_3, 0, 4, 0}, +{ 4, s_8_4, 0, 4, 0}, +{ 6, s_8_5, 0, 1, 0}, +{ 7, s_8_6, -1, 2, 0}, +{ 3, s_8_7, 0, 5, 0}, +{ 4, s_8_8, 0, 5, 0} }; -static const symbol s_9_0[7] = { 's', 'u', 'c', 'c', 'e', 'e', 'd' }; -static const symbol s_9_1[7] = { 'p', 'r', 'o', 'c', 'e', 'e', 'd' }; -static const symbol s_9_2[6] = { 'e', 'x', 'c', 'e', 'e', 'd' }; -static const symbol s_9_3[7] = { 'c', 'a', 'n', 'n', 'i', 'n', 'g' }; -static const symbol s_9_4[6] = { 'i', 'n', 'n', 'i', 'n', 'g' }; -static const symbol s_9_5[7] = { 'e', 'a', 'r', 'r', 'i', 'n', 'g' }; -static const symbol s_9_6[7] = { 'h', 'e', 'r', 'r', 'i', 'n', 'g' }; -static const symbol s_9_7[6] = { 'o', 'u', 't', 'i', 'n', 'g' }; - -static const struct among a_9[8] = -{ -{ 7, s_9_0, -1, -1, 0}, -{ 7, s_9_1, -1, -1, 0}, -{ 6, s_9_2, -1, -1, 0}, -{ 7, s_9_3, -1, -1, 0}, -{ 6, s_9_4, -1, -1, 0}, -{ 7, s_9_5, -1, -1, 0}, -{ 7, s_9_6, -1, -1, 0}, -{ 6, s_9_7, -1, -1, 0} +static const symbol s_9_0[2] = { 'i', 'c' }; +static const symbol s_9_1[4] = { 'a', 'n', 'c', 'e' }; +static const symbol s_9_2[4] = { 'e', 'n', 'c', 'e' }; +static const symbol s_9_3[4] = { 'a', 'b', 'l', 'e' }; +static const symbol s_9_4[4] = { 'i', 'b', 'l', 'e' }; +static const symbol s_9_5[3] = { 'a', 't', 'e' }; +static const symbol s_9_6[3] = { 'i', 'v', 'e' }; +static const symbol s_9_7[3] = { 'i', 'z', 'e' }; +static const symbol s_9_8[3] = { 'i', 't', 'i' }; +static const symbol s_9_9[2] = { 'a', 'l' }; +static const symbol s_9_10[3] = { 'i', 's', 'm' }; +static const symbol s_9_11[3] = { 'i', 'o', 'n' }; +static const symbol s_9_12[2] = { 'e', 'r' }; +static const symbol s_9_13[3] = { 'o', 'u', 's' }; +static const symbol s_9_14[3] = { 'a', 'n', 't' }; +static const symbol s_9_15[3] = { 'e', 'n', 't' }; +static const symbol s_9_16[4] = { 'm', 'e', 'n', 't' }; +static const symbol s_9_17[5] = { 'e', 'm', 'e', 'n', 't' }; +static const struct among a_9[18] = { +{ 2, s_9_0, 0, 1, 0}, +{ 4, s_9_1, 0, 1, 0}, +{ 4, s_9_2, 0, 1, 0}, +{ 4, s_9_3, 0, 1, 0}, +{ 4, s_9_4, 0, 1, 0}, +{ 3, s_9_5, 0, 1, 0}, +{ 3, s_9_6, 0, 1, 0}, +{ 3, s_9_7, 0, 1, 0}, +{ 3, s_9_8, 0, 1, 0}, +{ 2, s_9_9, 0, 1, 0}, +{ 3, s_9_10, 0, 1, 0}, +{ 3, s_9_11, 0, 2, 0}, +{ 2, s_9_12, 0, 1, 0}, +{ 3, s_9_13, 0, 1, 0}, +{ 3, s_9_14, 0, 1, 0}, +{ 3, s_9_15, 0, 1, 0}, +{ 4, s_9_16, -1, 1, 0}, +{ 5, s_9_17, -1, 1, 0} }; -static const symbol s_10_0[5] = { 'a', 'n', 'd', 'e', 's' }; -static const symbol s_10_1[5] = { 'a', 't', 'l', 'a', 's' }; -static const symbol s_10_2[4] = { 'b', 'i', 'a', 's' }; -static const symbol s_10_3[6] = { 'c', 'o', 's', 'm', 'o', 's' }; -static const symbol s_10_4[5] = { 'd', 'y', 'i', 'n', 'g' }; -static const symbol s_10_5[5] = { 'e', 'a', 'r', 'l', 'y' }; -static const symbol s_10_6[6] = { 'g', 'e', 'n', 't', 'l', 'y' }; -static const symbol s_10_7[4] = { 'h', 'o', 'w', 'e' }; -static const symbol s_10_8[4] = { 'i', 'd', 'l', 'y' }; -static const symbol s_10_9[5] = { 'l', 'y', 'i', 'n', 'g' }; -static const symbol s_10_10[4] = { 'n', 'e', 'w', 's' }; -static const symbol s_10_11[4] = { 'o', 'n', 'l', 'y' }; -static const symbol s_10_12[6] = { 's', 'i', 'n', 'g', 'l', 'y' }; -static const symbol s_10_13[5] = { 's', 'k', 'i', 'e', 's' }; -static const symbol s_10_14[4] = { 's', 'k', 'i', 's' }; -static const symbol s_10_15[3] = { 's', 'k', 'y' }; -static const symbol s_10_16[5] = { 't', 'y', 'i', 'n', 'g' }; -static const symbol s_10_17[4] = { 'u', 'g', 'l', 'y' }; +static const symbol s_10_0[1] = { 'e' }; +static const symbol s_10_1[1] = { 'l' }; +static const struct among a_10[2] = { +{ 1, s_10_0, 0, 1, 0}, +{ 1, s_10_1, 0, 2, 0} +}; -static const struct among a_10[18] = -{ -{ 5, s_10_0, -1, -1, 0}, -{ 5, s_10_1, -1, -1, 0}, -{ 4, s_10_2, -1, -1, 0}, -{ 6, s_10_3, -1, -1, 0}, -{ 5, s_10_4, -1, 3, 0}, -{ 5, s_10_5, -1, 9, 0}, -{ 6, s_10_6, -1, 7, 0}, -{ 4, s_10_7, -1, -1, 0}, -{ 4, s_10_8, -1, 6, 0}, -{ 5, s_10_9, -1, 4, 0}, -{ 4, s_10_10, -1, -1, 0}, -{ 4, s_10_11, -1, 10, 0}, -{ 6, s_10_12, -1, 11, 0}, -{ 5, s_10_13, -1, 2, 0}, -{ 4, s_10_14, -1, 1, 0}, -{ 3, s_10_15, -1, -1, 0}, -{ 5, s_10_16, -1, 5, 0}, -{ 4, s_10_17, -1, 8, 0} +static const symbol s_11_0[5] = { 'a', 'n', 'd', 'e', 's' }; +static const symbol s_11_1[5] = { 'a', 't', 'l', 'a', 's' }; +static const symbol s_11_2[4] = { 'b', 'i', 'a', 's' }; +static const symbol s_11_3[6] = { 'c', 'o', 's', 'm', 'o', 's' }; +static const symbol s_11_4[5] = { 'e', 'a', 'r', 'l', 'y' }; +static const symbol s_11_5[6] = { 'g', 'e', 'n', 't', 'l', 'y' }; +static const symbol s_11_6[4] = { 'h', 'o', 'w', 'e' }; +static const symbol s_11_7[4] = { 'i', 'd', 'l', 'y' }; +static const symbol s_11_8[4] = { 'n', 'e', 'w', 's' }; +static const symbol s_11_9[4] = { 'o', 'n', 'l', 'y' }; +static const symbol s_11_10[6] = { 's', 'i', 'n', 'g', 'l', 'y' }; +static const symbol s_11_11[5] = { 's', 'k', 'i', 'e', 's' }; +static const symbol s_11_12[4] = { 's', 'k', 'i', 's' }; +static const symbol s_11_13[3] = { 's', 'k', 'y' }; +static const symbol s_11_14[4] = { 'u', 'g', 'l', 'y' }; +static const struct among a_11[15] = { +{ 5, s_11_0, 0, -1, 0}, +{ 5, s_11_1, 0, -1, 0}, +{ 4, s_11_2, 0, -1, 0}, +{ 6, s_11_3, 0, -1, 0}, +{ 5, s_11_4, 0, 6, 0}, +{ 6, s_11_5, 0, 4, 0}, +{ 4, s_11_6, 0, -1, 0}, +{ 4, s_11_7, 0, 3, 0}, +{ 4, s_11_8, 0, -1, 0}, +{ 4, s_11_9, 0, 7, 0}, +{ 6, s_11_10, 0, 8, 0}, +{ 5, s_11_11, 0, 2, 0}, +{ 4, s_11_12, 0, 1, 0}, +{ 3, s_11_13, 0, -1, 0}, +{ 4, s_11_14, 0, 5, 0} }; static const unsigned char g_aeo[] = { 17, 64 }; @@ -318,180 +354,150 @@ static const unsigned char g_v_WXY[] = { 1, 17, 65, 208, 1 }; static const unsigned char g_valid_LI[] = { 55, 141, 2 }; -static const symbol s_0[] = { 'Y' }; -static const symbol s_1[] = { 'Y' }; -static const symbol s_2[] = { 's', 's' }; -static const symbol s_3[] = { 'i' }; -static const symbol s_4[] = { 'i', 'e' }; -static const symbol s_5[] = { 'e', 'e' }; -static const symbol s_6[] = { 'e' }; -static const symbol s_7[] = { 'e' }; -static const symbol s_8[] = { 'i' }; -static const symbol s_9[] = { 't', 'i', 'o', 'n' }; -static const symbol s_10[] = { 'e', 'n', 'c', 'e' }; -static const symbol s_11[] = { 'a', 'n', 'c', 'e' }; -static const symbol s_12[] = { 'a', 'b', 'l', 'e' }; -static const symbol s_13[] = { 'e', 'n', 't' }; -static const symbol s_14[] = { 'i', 'z', 'e' }; -static const symbol s_15[] = { 'a', 't', 'e' }; -static const symbol s_16[] = { 'a', 'l' }; -static const symbol s_17[] = { 'f', 'u', 'l' }; -static const symbol s_18[] = { 'o', 'u', 's' }; -static const symbol s_19[] = { 'i', 'v', 'e' }; -static const symbol s_20[] = { 'b', 'l', 'e' }; -static const symbol s_21[] = { 'o', 'g' }; -static const symbol s_22[] = { 'l', 'e', 's', 's' }; -static const symbol s_23[] = { 't', 'i', 'o', 'n' }; -static const symbol s_24[] = { 'a', 't', 'e' }; -static const symbol s_25[] = { 'a', 'l' }; -static const symbol s_26[] = { 'i', 'c' }; -static const symbol s_27[] = { 's', 'k', 'i' }; -static const symbol s_28[] = { 's', 'k', 'y' }; -static const symbol s_29[] = { 'd', 'i', 'e' }; -static const symbol s_30[] = { 'l', 'i', 'e' }; -static const symbol s_31[] = { 't', 'i', 'e' }; -static const symbol s_32[] = { 'i', 'd', 'l' }; -static const symbol s_33[] = { 'g', 'e', 'n', 't', 'l' }; -static const symbol s_34[] = { 'u', 'g', 'l', 'i' }; -static const symbol s_35[] = { 'e', 'a', 'r', 'l', 'i' }; -static const symbol s_36[] = { 'o', 'n', 'l', 'i' }; -static const symbol s_37[] = { 's', 'i', 'n', 'g', 'l' }; -static const symbol s_38[] = { 'y' }; - static int r_prelude(struct SN_env * z) { - z->I[2] = 0; - { int c1 = z->c; + ((SN_local *)z)->b_Y_found = 0; + { + int v_1 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != '\'') goto lab0; z->c++; z->ket = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: - z->c = c1; + z->c = v_1; } - { int c2 = z->c; + { + int v_2 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab1; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } - z->I[2] = 1; + ((SN_local *)z)->b_Y_found = 1; lab1: - z->c = c2; + z->c = v_2; } - { int c3 = z->c; - while(1) { - int c4 = z->c; - while(1) { - int c5 = z->c; + { + int v_3 = z->c; + while (1) { + int v_4 = z->c; + while (1) { + int v_5 = z->c; if (in_grouping_U(z, g_v, 97, 121, 0)) goto lab4; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab4; z->c++; z->ket = z->c; - z->c = c5; + z->c = v_5; break; lab4: - z->c = c5; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_5; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab3; z->c = ret; } } - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } - z->I[2] = 1; + ((SN_local *)z)->b_Y_found = 1; continue; lab3: - z->c = c4; + z->c = v_4; break; } - z->c = c3; + z->c = v_3; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (z->c + 4 >= z->l || z->p[z->c + 4] >> 5 != 3 || !((2375680 >> (z->p[z->c + 4] & 0x1f)) & 1)) goto lab2; - if (!find_among(z, a_0, 3)) goto lab2; - goto lab1; - lab2: - z->c = c2; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (z->c + 3 >= z->l || z->p[z->c + 3] >> 5 != 3 || !((5513250 >> (z->p[z->c + 3] & 0x1f)) & 1)) goto lab1; + if (!find_among(z, a_0, 9, 0)) goto lab1; + break; + lab1: + z->c = v_2; { int ret = out_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - } - lab1: - z->I[1] = z->c; - + } while (0); + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_shortv(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - if (out_grouping_b_U(z, g_v_WXY, 89, 121, 0)) goto lab1; - if (in_grouping_b_U(z, g_v, 97, 121, 0)) goto lab1; + do { + int v_1 = z->l - z->c; + if (out_grouping_b_U(z, g_v_WXY, 89, 121, 0)) goto lab0; + if (in_grouping_b_U(z, g_v, 97, 121, 0)) goto lab0; + if (out_grouping_b_U(z, g_v, 97, 121, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_1; if (out_grouping_b_U(z, g_v, 97, 121, 0)) goto lab1; - goto lab0; + if (in_grouping_b_U(z, g_v, 97, 121, 0)) goto lab1; + if (z->c > z->lb) goto lab1; + break; lab1: - z->c = z->l - m1; - if (out_grouping_b_U(z, g_v, 97, 121, 0)) return 0; - if (in_grouping_b_U(z, g_v, 97, 121, 0)) return 0; - if (z->c > z->lb) return 0; - } -lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 4, s_2))) return 0; + } while (0); return 1; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_Step_1a(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || (z->p[z->c - 1] != 39 && z->p[z->c - 1] != 115)) { z->c = z->l - m1; goto lab0; } - if (!find_among_b(z, a_1, 3)) { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || (z->p[z->c - 1] != 39 && z->p[z->c - 1] != 115)) { z->c = z->l - v_1; goto lab0; } + if (!find_among_b(z, a_1, 3, 0)) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -499,45 +505,50 @@ static int r_Step_1a(struct SN_env * z) { } z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 115)) return 0; - among_var = find_among_b(z, a_2, 6); + among_var = find_among_b(z, a_2, 6, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 2: - { int m2 = z->l - z->c; (void)m2; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 2); - if (ret < 0) goto lab2; + do { + int v_2 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 2); + if (ret < 0) goto lab1; z->c = ret; } - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; - { int ret = slice_from_s(z, 2, s_4); + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } - } - lab1: + } while (0); break; case 3: - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } - { int ret = out_grouping_b_U(z, g_v, 97, 121, 1); if (ret < 0) return 0; z->c -= ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -548,102 +559,157 @@ static int r_Step_1a(struct SN_env * z) { static int r_Step_1b(struct SN_env * z) { int among_var; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33554576 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_4, 6); - if (!among_var) return 0; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33554576 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = -1; else + among_var = find_among_b(z, a_5, 7, 0); z->bra = z->c; - switch (among_var) { - case 1: - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int ret = slice_from_s(z, 2, s_5); - if (ret < 0) return ret; - } - break; - case 2: - { int m_test1 = z->l - z->c; - + do { + int v_1 = z->l - z->c; + switch (among_var) { + case 1: { - int ret = out_grouping_b_U(z, g_v, 97, 121, 1); - if (ret < 0) return 0; - z->c -= ret; + int v_2 = z->l - z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + do { + int v_3 = z->l - z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] != 99) goto lab2; + if (!find_among_b(z, a_3, 3, 0)) goto lab2; + if (z->c > z->lb) goto lab2; + break; + lab2: + z->c = z->l - v_3; + { + int ret = slice_from_s(z, 2, s_6); + if (ret < 0) return ret; + } + } while (0); + lab1: + z->c = z->l - v_2; } - z->c = z->l - m_test1; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->ket = z->c; - z->bra = z->c; - { int m_test2 = z->l - z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else - among_var = find_among_b(z, a_3, 13); + break; + case 2: + goto lab0; + break; + case 3: + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((34881536 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_4, 7, 0); + if (!among_var) goto lab0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_6); - if (ret < 0) return ret; - } - return 0; - break; - case 2: - { int m3 = z->l - z->c; (void)m3; - if (in_grouping_b_U(z, g_aeo, 97, 111, 0)) goto lab0; + { + int v_4 = z->l - z->c; + if (out_grouping_b_U(z, g_v, 97, 121, 0)) goto lab0; if (z->c > z->lb) goto lab0; - return 0; - lab0: - z->c = z->l - m3; + z->c = z->l - v_4; } - break; - case 3: - if (z->c != z->I[1]) return 0; - { int m_test4 = z->l - z->c; - { int ret = r_shortv(z); - if (ret <= 0) return ret; - } - z->c = z->l - m_test4; - } - { int ret = slice_from_s(z, 1, s_7); + z->bra = z->c; + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } - return 0; + break; + case 2: + if (z->c > z->lb) goto lab0; break; } - z->c = z->l - m_test2; - } - z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + break; + } + break; + lab0: + z->c = z->l - v_1; + { + int v_5 = z->l - z->c; + { + int ret = out_grouping_b_U(z, g_v, 97, 121, 1); if (ret < 0) return 0; - z->c = ret; - } - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; + z->c -= ret; } - break; - } + z->c = z->l - v_5; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + z->ket = z->c; + z->bra = z->c; + { + int v_6 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else + among_var = find_among_b(z, a_6, 13, 0); + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_8); + if (ret < 0) return ret; + } + return 0; + break; + case 2: + { + int v_7 = z->l - z->c; + if (in_grouping_b_U(z, g_aeo, 97, 111, 0)) goto lab3; + if (z->c > z->lb) goto lab3; + return 0; + lab3: + z->c = z->l - v_7; + } + break; + case 3: + if (z->c != ((SN_local *)z)->i_p1) return 0; + { + int v_8 = z->l - z->c; + { + int ret = r_shortv(z); + if (ret <= 0) return ret; + } + z->c = z->l - v_8; + } + { + int ret = slice_from_s(z, 1, s_9); + if (ret < 0) return ret; + } + return 0; + break; + } + z->c = z->l - v_6; + } + z->ket = z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) return 0; + z->c = ret; + } + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } while (0); return 1; } static int r_Step_1c(struct SN_env * z) { z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'Y') return 0; z->c--; - } -lab0: + } while (0); z->bra = z->c; if (out_grouping_b_U(z, g_v, 97, 121, 0)) return 0; - - if (z->c > z->lb) goto lab2; + if (z->c > z->lb) goto lab1; return 0; -lab2: - { int ret = slice_from_s(z, 1, s_8); +lab1: + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } return 1; @@ -652,89 +718,111 @@ static int r_Step_1c(struct SN_env * z) { static int r_Step_2(struct SN_env * z) { int among_var; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((815616 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_5, 24); + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1864192 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_7, 25, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_9); + { + int ret = slice_from_s(z, 4, s_11); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_10); + { + int ret = slice_from_s(z, 4, s_12); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_11); + { + int ret = slice_from_s(z, 4, s_13); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_12); + { + int ret = slice_from_s(z, 4, s_14); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_13); + { + int ret = slice_from_s(z, 3, s_15); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 3, s_14); + { + int ret = slice_from_s(z, 3, s_16); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_15); + { + int ret = slice_from_s(z, 3, s_17); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 2, s_16); + { + int ret = slice_from_s(z, 2, s_18); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 3, s_17); + { + int ret = slice_from_s(z, 3, s_19); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 3, s_18); + { + int ret = slice_from_s(z, 3, s_20); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 3, s_19); + { + int ret = slice_from_s(z, 3, s_21); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 3, s_20); + { + int ret = slice_from_s(z, 3, s_22); if (ret < 0) return ret; } break; case 13: - if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; - z->c--; - { int ret = slice_from_s(z, 2, s_21); + { + int ret = slice_from_s(z, 2, s_23); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 4, s_22); + if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; + z->c--; + { + int ret = slice_from_s(z, 2, s_24); if (ret < 0) return ret; } break; case 15: + { + int ret = slice_from_s(z, 4, s_25); + if (ret < 0) return ret; + } + break; + case 16: if (in_grouping_b_U(z, g_valid_LI, 99, 116, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -746,43 +834,51 @@ static int r_Step_3(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((528928 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_6, 9); + among_var = find_among_b(z, a_8, 9, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_23); + { + int ret = slice_from_s(z, 4, s_26); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_24); + { + int ret = slice_from_s(z, 3, s_27); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_25); + { + int ret = slice_from_s(z, 2, s_28); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_26); + { + int ret = slice_from_s(z, 2, s_29); if (ret < 0) return ret; } break; case 5: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 6: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -794,30 +890,33 @@ static int r_Step_4(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1864232 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_7, 18); + among_var = find_among_b(z, a_9, 18, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 't') return 0; z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -829,42 +928,49 @@ static int r_Step_5(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) return 0; - among_var = find_among_b(z, a_8, 2); + among_var = find_among_b(z, a_10, 2, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - - { int ret = r_R2(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - goto lab0; - lab1: - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m1 = z->l - z->c; (void)m1; - { int ret = r_shortv(z); - if (ret == 0) goto lab2; + do { + { + int ret = r_R2(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - return 0; - lab2: - z->c = z->l - m1; - } - lab0: - { int ret = slice_del(z); + break; + lab0: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + { + int ret = r_shortv(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + return 0; + lab1: + z->c = z->l - v_1; + } + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -872,76 +978,60 @@ static int r_Step_5(struct SN_env * z) { return 1; } -static int r_exception2(struct SN_env * z) { - z->ket = z->c; - if (z->c - 5 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 103)) return 0; - if (!find_among_b(z, a_9, 8)) return 0; - z->bra = z->c; - if (z->c > z->lb) return 0; - return 1; -} - static int r_exception1(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((42750482 >> (z->p[z->c + 2] & 0x1f)) & 1)) return 0; - among_var = find_among(z, a_10, 18); + among_var = find_among(z, a_11, 15, 0); if (!among_var) return 0; z->ket = z->c; if (z->c < z->l) return 0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 3, s_27); + { + int ret = slice_from_s(z, 3, s_30); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_28); + { + int ret = slice_from_s(z, 3, s_31); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_29); + { + int ret = slice_from_s(z, 3, s_32); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 3, s_30); + { + int ret = slice_from_s(z, 5, s_33); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_31); + { + int ret = slice_from_s(z, 4, s_34); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 3, s_32); + { + int ret = slice_from_s(z, 5, s_35); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 5, s_33); + { + int ret = slice_from_s(z, 4, s_36); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 4, s_34); - if (ret < 0) return ret; - } - break; - case 9: - { int ret = slice_from_s(z, 5, s_35); - if (ret < 0) return ret; - } - break; - case 10: - { int ret = slice_from_s(z, 4, s_36); - if (ret < 0) return ret; - } - break; - case 11: - { int ret = slice_from_s(z, 5, s_37); + { + int ret = slice_from_s(z, 5, s_37); if (ret < 0) return ret; } break; @@ -950,131 +1040,151 @@ static int r_exception1(struct SN_env * z) { } static int r_postlude(struct SN_env * z) { - if (!(z->I[2])) return 0; - while(1) { - int c1 = z->c; - while(1) { - int c2 = z->c; + if (!((SN_local *)z)->b_Y_found) return 0; + while (1) { + int v_1 = z->c; + while (1) { + int v_2 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'Y') goto lab1; z->c++; z->ket = z->c; - z->c = c2; + z->c = v_2; break; lab1: - z->c = c2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_2; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } } - { int ret = slice_from_s(z, 1, s_38); + { + int ret = slice_from_s(z, 1, s_38); if (ret < 0) return ret; } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } extern int english_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_exception1(z); - if (ret == 0) goto lab1; + do { + int v_1 = z->c; + { + int ret = r_exception1(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = c1; - { int c2 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); - if (ret < 0) goto lab3; + break; + lab0: + z->c = v_1; + { + int v_2 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); + if (ret < 0) goto lab2; z->c = ret; } - goto lab2; - lab3: - z->c = c2; + goto lab1; + lab2: + z->c = v_2; } - goto lab0; - lab2: - z->c = c1; - - { int ret = r_prelude(z); + break; + lab1: + z->c = v_1; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_Step_1a(z); + { + int v_3 = z->l - z->c; + { + int ret = r_Step_1a(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_exception2(z); - if (ret == 0) goto lab5; + { + int v_4 = z->l - z->c; + { + int ret = r_Step_1b(z); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_Step_1b(z); - if (ret < 0) return ret; - } - z->c = z->l - m5; - } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_Step_1c(z); - if (ret < 0) return ret; - } - z->c = z->l - m6; + z->c = z->l - v_4; + } + { + int v_5 = z->l - z->c; + { + int ret = r_Step_1c(z); + if (ret < 0) return ret; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_Step_2(z); - if (ret < 0) return ret; - } - z->c = z->l - m7; + z->c = z->l - v_5; + } + { + int v_6 = z->l - z->c; + { + int ret = r_Step_2(z); + if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_Step_3(z); - if (ret < 0) return ret; - } - z->c = z->l - m8; + z->c = z->l - v_6; + } + { + int v_7 = z->l - z->c; + { + int ret = r_Step_3(z); + if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_Step_4(z); - if (ret < 0) return ret; - } - z->c = z->l - m9; + z->c = z->l - v_7; + } + { + int v_8 = z->l - z->c; + { + int ret = r_Step_4(z); + if (ret < 0) return ret; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_Step_5(z); - if (ret < 0) return ret; - } - z->c = z->l - m10; + z->c = z->l - v_8; + } + { + int v_9 = z->l - z->c; + { + int ret = r_Step_5(z); + if (ret < 0) return ret; } + z->c = z->l - v_9; } - lab4: z->c = z->lb; - { int c11 = z->c; - { int ret = r_postlude(z); + { + int v_10 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c11; + z->c = v_10; } - } -lab0: + } while (0); return 1; } -extern struct SN_env * english_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * english_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_Y_found = 0; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void english_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void english_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_esperanto.c b/src/backend/snowball/libstemmer/stem_UTF_8_esperanto.c new file mode 100644 index 0000000000000..895b2f7cb40f0 --- /dev/null +++ b/src/backend/snowball/libstemmer/stem_UTF_8_esperanto.c @@ -0,0 +1,820 @@ +/* Generated from esperanto.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#include "stem_UTF_8_esperanto.h" + +#include + +#include "snowball_runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif +extern int esperanto_UTF_8_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif + +static int r_uninflected(struct SN_env * z); +static int r_ujn_suffix(struct SN_env * z); +static int r_standard_suffix(struct SN_env * z); +static int r_pronoun(struct SN_env * z); +static int r_merged_numeral(struct SN_env * z); +static int r_long_word(struct SN_env * z); +static int r_initial_apostrophe(struct SN_env * z); +static int r_final_apostrophe(struct SN_env * z); +static int r_correlative(struct SN_env * z); +static int r_canonical_form(struct SN_env * z); + +static const symbol s_0[] = { 0xC4, 0x89 }; +static const symbol s_1[] = { 0xC4, 0x9D }; +static const symbol s_2[] = { 0xC4, 0xA5 }; +static const symbol s_3[] = { 0xC4, 0xB5 }; +static const symbol s_4[] = { 0xC5, 0x9D }; +static const symbol s_5[] = { 0xC5, 0xAD }; +static const symbol s_6[] = { 'a' }; +static const symbol s_7[] = { 'e' }; +static const symbol s_8[] = { 'i' }; +static const symbol s_9[] = { 'o' }; +static const symbol s_10[] = { 'u' }; +static const symbol s_11[] = { 's', 't' }; +static const symbol s_12[] = { 'e' }; +static const symbol s_13[] = { 'a' }; +static const symbol s_14[] = { 'u', 'n' }; +static const symbol s_15[] = { 'u' }; +static const symbol s_16[] = { 'a', 0xC5, 0xAD }; +static const symbol s_17[] = { 'o' }; + +static const symbol s_0_1[1] = { '-' }; +static const symbol s_0_2[2] = { 'c', 'x' }; +static const symbol s_0_3[2] = { 'g', 'x' }; +static const symbol s_0_4[2] = { 'h', 'x' }; +static const symbol s_0_5[2] = { 'j', 'x' }; +static const symbol s_0_6[1] = { 'q' }; +static const symbol s_0_7[2] = { 's', 'x' }; +static const symbol s_0_8[2] = { 'u', 'x' }; +static const symbol s_0_9[1] = { 'w' }; +static const symbol s_0_10[1] = { 'x' }; +static const symbol s_0_11[1] = { 'y' }; +static const symbol s_0_12[2] = { 0xC3, 0xA1 }; +static const symbol s_0_13[2] = { 0xC3, 0xA9 }; +static const symbol s_0_14[2] = { 0xC3, 0xAD }; +static const symbol s_0_15[2] = { 0xC3, 0xB3 }; +static const symbol s_0_16[2] = { 0xC3, 0xBA }; +static const struct among a_0[17] = { +{ 0, 0, 0, 14, 0}, +{ 1, s_0_1, -1, 13, 0}, +{ 2, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, 2, 0}, +{ 2, s_0_4, -4, 3, 0}, +{ 2, s_0_5, -5, 4, 0}, +{ 1, s_0_6, -6, 12, 0}, +{ 2, s_0_7, -7, 5, 0}, +{ 2, s_0_8, -8, 6, 0}, +{ 1, s_0_9, -9, 12, 0}, +{ 1, s_0_10, -10, 12, 0}, +{ 1, s_0_11, -11, 12, 0}, +{ 2, s_0_12, -12, 7, 0}, +{ 2, s_0_13, -13, 8, 0}, +{ 2, s_0_14, -14, 9, 0}, +{ 2, s_0_15, -15, 10, 0}, +{ 2, s_0_16, -16, 11, 0} +}; + +static const symbol s_1_0[2] = { 'a', 's' }; +static const symbol s_1_1[1] = { 'i' }; +static const symbol s_1_2[2] = { 'i', 's' }; +static const symbol s_1_3[2] = { 'o', 's' }; +static const symbol s_1_4[1] = { 'u' }; +static const symbol s_1_5[2] = { 'u', 's' }; +static const struct among a_1[6] = { +{ 2, s_1_0, 0, -1, 0}, +{ 1, s_1_1, 0, -1, 0}, +{ 2, s_1_2, -1, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 1, s_1_4, 0, -1, 0}, +{ 2, s_1_5, -1, -1, 0} +}; + +static const symbol s_2_0[2] = { 'c', 'i' }; +static const symbol s_2_1[2] = { 'g', 'i' }; +static const symbol s_2_2[2] = { 'h', 'i' }; +static const symbol s_2_3[2] = { 'l', 'i' }; +static const symbol s_2_4[3] = { 'i', 'l', 'i' }; +static const symbol s_2_5[4] = { 0xC5, 0x9D, 'l', 'i' }; +static const symbol s_2_6[2] = { 'm', 'i' }; +static const symbol s_2_7[2] = { 'n', 'i' }; +static const symbol s_2_8[3] = { 'o', 'n', 'i' }; +static const symbol s_2_9[2] = { 'r', 'i' }; +static const symbol s_2_10[2] = { 's', 'i' }; +static const symbol s_2_11[2] = { 'v', 'i' }; +static const symbol s_2_12[3] = { 'i', 'v', 'i' }; +static const symbol s_2_13[3] = { 0xC4, 0x9D, 'i' }; +static const symbol s_2_14[3] = { 0xC5, 0x9D, 'i' }; +static const symbol s_2_15[4] = { 'i', 0xC5, 0x9D, 'i' }; +static const symbol s_2_16[6] = { 'm', 'a', 'l', 0xC5, 0x9D, 'i' }; +static const struct among a_2[17] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, -1, 0}, +{ 3, s_2_4, -1, -1, 0}, +{ 4, s_2_5, -2, -1, 0}, +{ 2, s_2_6, 0, -1, 0}, +{ 2, s_2_7, 0, -1, 0}, +{ 3, s_2_8, -1, -1, 0}, +{ 2, s_2_9, 0, -1, 0}, +{ 2, s_2_10, 0, -1, 0}, +{ 2, s_2_11, 0, -1, 0}, +{ 3, s_2_12, -1, -1, 0}, +{ 3, s_2_13, 0, -1, 0}, +{ 3, s_2_14, 0, -1, 0}, +{ 4, s_2_15, -1, -1, 0}, +{ 6, s_2_16, -2, -1, 0} +}; + +static const symbol s_3_0[3] = { 'a', 'm', 'b' }; +static const symbol s_3_1[4] = { 'b', 'a', 'l', 'd' }; +static const symbol s_3_2[7] = { 'm', 'a', 'l', 'b', 'a', 'l', 'd' }; +static const symbol s_3_3[4] = { 'm', 'o', 'r', 'g' }; +static const symbol s_3_4[8] = { 'p', 'o', 's', 't', 'm', 'o', 'r', 'g' }; +static const symbol s_3_5[3] = { 'a', 'd', 'i' }; +static const symbol s_3_6[4] = { 'h', 'o', 'd', 'i' }; +static const symbol s_3_7[3] = { 'a', 'n', 'k' }; +static const symbol s_3_8[5] = { 0xC4, 0x89, 'i', 'r', 'k' }; +static const symbol s_3_9[8] = { 't', 'u', 't', 0xC4, 0x89, 'i', 'r', 'k' }; +static const symbol s_3_10[5] = { 'p', 'r', 'e', 's', 'k' }; +static const symbol s_3_11[5] = { 'a', 'l', 'm', 'e', 'n' }; +static const symbol s_3_12[4] = { 'a', 'p', 'e', 'n' }; +static const symbol s_3_13[4] = { 'h', 'i', 'e', 'r' }; +static const symbol s_3_14[10] = { 'a', 'n', 't', 'a', 0xC5, 0xAD, 'h', 'i', 'e', 'r' }; +static const symbol s_3_15[5] = { 'm', 'a', 'l', 'g', 'r' }; +static const symbol s_3_16[5] = { 'a', 'n', 'k', 'o', 'r' }; +static const symbol s_3_17[5] = { 'k', 'o', 'n', 't', 'r' }; +static const symbol s_3_18[6] = { 'a', 'n', 's', 't', 'a', 't' }; +static const symbol s_3_19[4] = { 'k', 'v', 'a', 'z' }; +static const struct among a_3[20] = { +{ 3, s_3_0, 0, -1, 0}, +{ 4, s_3_1, 0, -1, 0}, +{ 7, s_3_2, -1, -1, 0}, +{ 4, s_3_3, 0, -1, 0}, +{ 8, s_3_4, -1, -1, 0}, +{ 3, s_3_5, 0, -1, 0}, +{ 4, s_3_6, 0, -1, 0}, +{ 3, s_3_7, 0, -1, 0}, +{ 5, s_3_8, 0, -1, 0}, +{ 8, s_3_9, -1, -1, 0}, +{ 5, s_3_10, 0, -1, 0}, +{ 5, s_3_11, 0, -1, 0}, +{ 4, s_3_12, 0, -1, 0}, +{ 4, s_3_13, 0, -1, 0}, +{ 10, s_3_14, -1, -1, 0}, +{ 5, s_3_15, 0, -1, 0}, +{ 5, s_3_16, 0, -1, 0}, +{ 5, s_3_17, 0, -1, 0}, +{ 6, s_3_18, 0, -1, 0}, +{ 4, s_3_19, 0, -1, 0} +}; + +static const symbol s_4_0[4] = { 'a', 'l', 'i', 'u' }; +static const symbol s_4_1[3] = { 'u', 'n', 'u' }; +static const struct among a_4[2] = { +{ 4, s_4_0, 0, -1, 0}, +{ 3, s_4_1, 0, -1, 0} +}; + +static const symbol s_5_0[3] = { 'a', 'h', 'a' }; +static const symbol s_5_1[4] = { 'h', 'a', 'h', 'a' }; +static const symbol s_5_2[8] = { 'h', 'a', 'l', 'e', 'l', 'u', 'j', 'a' }; +static const symbol s_5_3[4] = { 'h', 'o', 'l', 'a' }; +static const symbol s_5_4[6] = { 'h', 'o', 's', 'a', 'n', 'a' }; +static const symbol s_5_5[6] = { 'm', 'a', 'l', 't', 'r', 'a' }; +static const symbol s_5_6[4] = { 'h', 'u', 'r', 'a' }; +static const symbol s_5_7[6] = { 0xC4, 0xA5, 'a', 0xC4, 0xA5, 'a' }; +static const symbol s_5_8[4] = { 'e', 'k', 'd', 'e' }; +static const symbol s_5_9[4] = { 'e', 'l', 'd', 'e' }; +static const symbol s_5_10[5] = { 'd', 'i', 's', 'd', 'e' }; +static const symbol s_5_11[3] = { 'e', 'h', 'e' }; +static const symbol s_5_12[6] = { 'm', 'a', 'l', 't', 'r', 'e' }; +static const symbol s_5_13[9] = { 'd', 'i', 'r', 'l', 'i', 'd', 'i', 'd', 'i' }; +static const symbol s_5_14[6] = { 'm', 'a', 'l', 'p', 'l', 'i' }; +static const symbol s_5_15[6] = { 'm', 'a', 'l', 0xC4, 0x89, 'i' }; +static const symbol s_5_16[6] = { 'm', 'a', 'l', 'k', 'a', 'j' }; +static const symbol s_5_17[4] = { 'a', 'm', 'e', 'n' }; +static const symbol s_5_18[5] = { 't', 'a', 'm', 'e', 'n' }; +static const symbol s_5_19[3] = { 'o', 'h', 'o' }; +static const symbol s_5_20[6] = { 'm', 'a', 'l', 't', 'r', 'o' }; +static const symbol s_5_21[5] = { 'm', 'i', 'n', 'u', 's' }; +static const symbol s_5_22[3] = { 'u', 'h', 'u' }; +static const symbol s_5_23[3] = { 'm', 'u', 'u' }; +static const struct among a_5[24] = { +{ 3, s_5_0, 0, -1, 0}, +{ 4, s_5_1, -1, -1, 0}, +{ 8, s_5_2, 0, -1, 0}, +{ 4, s_5_3, 0, -1, 0}, +{ 6, s_5_4, 0, -1, 0}, +{ 6, s_5_5, 0, -1, 0}, +{ 4, s_5_6, 0, -1, 0}, +{ 6, s_5_7, 0, -1, 0}, +{ 4, s_5_8, 0, -1, 0}, +{ 4, s_5_9, 0, -1, 0}, +{ 5, s_5_10, 0, -1, 0}, +{ 3, s_5_11, 0, -1, 0}, +{ 6, s_5_12, 0, -1, 0}, +{ 9, s_5_13, 0, -1, 0}, +{ 6, s_5_14, 0, -1, 0}, +{ 6, s_5_15, 0, -1, 0}, +{ 6, s_5_16, 0, -1, 0}, +{ 4, s_5_17, 0, -1, 0}, +{ 5, s_5_18, -1, -1, 0}, +{ 3, s_5_19, 0, -1, 0}, +{ 6, s_5_20, 0, -1, 0}, +{ 5, s_5_21, 0, -1, 0}, +{ 3, s_5_22, 0, -1, 0}, +{ 3, s_5_23, 0, -1, 0} +}; + +static const symbol s_6_0[3] = { 't', 'r', 'i' }; +static const symbol s_6_1[2] = { 'd', 'u' }; +static const symbol s_6_2[3] = { 'u', 'n', 'u' }; +static const struct among a_6[3] = { +{ 3, s_6_0, 0, -1, 0}, +{ 2, s_6_1, 0, -1, 0}, +{ 3, s_6_2, 0, -1, 0} +}; + +static const symbol s_7_0[3] = { 'd', 'e', 'k' }; +static const symbol s_7_1[4] = { 'c', 'e', 'n', 't' }; +static const struct among a_7[2] = { +{ 3, s_7_0, 0, -1, 0}, +{ 4, s_7_1, 0, -1, 0} +}; + +static const symbol s_8_0[1] = { 'k' }; +static const symbol s_8_1[4] = { 'k', 'e', 'l', 'k' }; +static const symbol s_8_2[3] = { 'n', 'e', 'n' }; +static const symbol s_8_3[1] = { 't' }; +static const symbol s_8_4[4] = { 'm', 'u', 'l', 't' }; +static const symbol s_8_5[4] = { 's', 'a', 'm', 't' }; +static const symbol s_8_6[2] = { 0xC4, 0x89 }; +static const struct among a_8[7] = { +{ 1, s_8_0, 0, -1, 0}, +{ 4, s_8_1, -1, -1, 0}, +{ 3, s_8_2, 0, -1, 0}, +{ 1, s_8_3, 0, -1, 0}, +{ 4, s_8_4, -1, -1, 0}, +{ 4, s_8_5, -2, -1, 0}, +{ 2, s_8_6, 0, -1, 0} +}; + +static const symbol s_9_0[1] = { 'a' }; +static const symbol s_9_1[1] = { 'e' }; +static const symbol s_9_2[1] = { 'i' }; +static const symbol s_9_3[1] = { 'j' }; +static const symbol s_9_4[2] = { 'a', 'j' }; +static const symbol s_9_5[2] = { 'o', 'j' }; +static const symbol s_9_6[1] = { 'n' }; +static const symbol s_9_7[2] = { 'a', 'n' }; +static const symbol s_9_8[2] = { 'e', 'n' }; +static const symbol s_9_9[2] = { 'j', 'n' }; +static const symbol s_9_10[3] = { 'a', 'j', 'n' }; +static const symbol s_9_11[3] = { 'o', 'j', 'n' }; +static const symbol s_9_12[2] = { 'o', 'n' }; +static const symbol s_9_13[1] = { 'o' }; +static const symbol s_9_14[2] = { 'a', 's' }; +static const symbol s_9_15[2] = { 'i', 's' }; +static const symbol s_9_16[2] = { 'o', 's' }; +static const symbol s_9_17[2] = { 'u', 's' }; +static const symbol s_9_18[1] = { 'u' }; +static const struct among a_9[19] = { +{ 1, s_9_0, 0, -1, 0}, +{ 1, s_9_1, 0, -1, 0}, +{ 1, s_9_2, 0, -1, 0}, +{ 1, s_9_3, 0, 1, 0}, +{ 2, s_9_4, -1, -1, 0}, +{ 2, s_9_5, -2, -1, 0}, +{ 1, s_9_6, 0, 1, 0}, +{ 2, s_9_7, -1, -1, 0}, +{ 2, s_9_8, -2, -1, 0}, +{ 2, s_9_9, -3, 1, 0}, +{ 3, s_9_10, -1, -1, 0}, +{ 3, s_9_11, -2, -1, 0}, +{ 2, s_9_12, -6, -1, 0}, +{ 1, s_9_13, 0, -1, 0}, +{ 2, s_9_14, 0, -1, 0}, +{ 2, s_9_15, 0, -1, 0}, +{ 2, s_9_16, 0, -1, 0}, +{ 2, s_9_17, 0, -1, 0}, +{ 1, s_9_18, 0, -1, 0} +}; + +static const unsigned char g_vowel[] = { 17, 65, 16 }; + +static const unsigned char g_aou[] = { 1, 64, 16 }; + +static const unsigned char g_digit[] = { 255, 3 }; + +static int r_canonical_form(struct SN_env * z) { + int among_var; + int b_foreign; + b_foreign = 0; + while (1) { + int v_1 = z->c; + z->bra = z->c; + among_var = find_among(z, a_0, 17, 0); + z->ket = z->c; + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 2, s_0); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 2, s_1); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 2, s_2); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 2, s_3); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_from_s(z, 2, s_4); + if (ret < 0) return ret; + } + break; + case 6: + { + int ret = slice_from_s(z, 2, s_5); + if (ret < 0) return ret; + } + break; + case 7: + { + int ret = slice_from_s(z, 1, s_6); + if (ret < 0) return ret; + } + b_foreign = 1; + break; + case 8: + { + int ret = slice_from_s(z, 1, s_7); + if (ret < 0) return ret; + } + b_foreign = 1; + break; + case 9: + { + int ret = slice_from_s(z, 1, s_8); + if (ret < 0) return ret; + } + b_foreign = 1; + break; + case 10: + { + int ret = slice_from_s(z, 1, s_9); + if (ret < 0) return ret; + } + b_foreign = 1; + break; + case 11: + { + int ret = slice_from_s(z, 1, s_10); + if (ret < 0) return ret; + } + b_foreign = 1; + break; + case 12: + b_foreign = 1; + break; + case 13: + b_foreign = 0; + break; + case 14: + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) goto lab0; + z->c = ret; + } + break; + } + continue; + lab0: + z->c = v_1; + break; + } + return !b_foreign; +} + +static int r_initial_apostrophe(struct SN_env * z) { + z->bra = z->c; + if (z->c == z->l || z->p[z->c] != '\'') return 0; + z->c++; + z->ket = z->c; + if (!(eq_s(z, 2, s_11))) return 0; + if (z->c >= z->l || z->p[z->c + 0] >> 5 != 3 || !((2130434 >> (z->p[z->c + 0] & 0x1f)) & 1)) return 0; + if (!find_among(z, a_1, 6, 0)) return 0; + if (z->c < z->l) return 0; + { + int ret = slice_from_s(z, 1, s_12); + if (ret < 0) return ret; + } + return 1; +} + +static int r_pronoun(struct SN_env * z) { + z->ket = z->c; + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') { z->c = z->l - v_1; goto lab0; } + z->c--; + lab0: + ; + } + z->bra = z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 105) return 0; + if (!find_among_b(z, a_2, 17, 0)) return 0; + do { + int v_2 = z->l - z->c; + if (z->c > z->lb) goto lab1; + break; + lab1: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != '-') return 0; + z->c--; + } while (0); + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return 1; +} + +static int r_final_apostrophe(struct SN_env * z) { + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] != '\'') return 0; + z->c--; + z->bra = z->c; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'l') goto lab0; + z->c--; + if (z->c > z->lb) goto lab0; + { + int ret = slice_from_s(z, 1, s_13); + if (ret < 0) return ret; + } + break; + lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 2, s_14))) goto lab1; + if (z->c > z->lb) goto lab1; + { + int ret = slice_from_s(z, 1, s_15); + if (ret < 0) return ret; + } + break; + lab1: + z->c = z->l - v_1; + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68438676 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab2; + if (!find_among_b(z, a_3, 20, 0)) goto lab2; + do { + int v_2 = z->l - z->c; + if (z->c > z->lb) goto lab3; + break; + lab3: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != '-') goto lab2; + z->c--; + } while (0); + { + int ret = slice_from_s(z, 3, s_16); + if (ret < 0) return ret; + } + break; + lab2: + z->c = z->l - v_1; + { + int ret = slice_from_s(z, 1, s_17); + if (ret < 0) return ret; + } + } while (0); + return 1; +} + +static int r_ujn_suffix(struct SN_env * z) { + z->ket = z->c; + { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') { z->c = z->l - v_1; goto lab0; } + z->c--; + lab0: + ; + } + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'j') { z->c = z->l - v_2; goto lab1; } + z->c--; + lab1: + ; + } + z->bra = z->c; + if (z->c - 2 <= z->lb || z->p[z->c - 1] != 117) return 0; + if (!find_among_b(z, a_4, 2, 0)) return 0; + do { + int v_3 = z->l - z->c; + if (z->c > z->lb) goto lab2; + break; + lab2: + z->c = z->l - v_3; + if (z->c <= z->lb || z->p[z->c - 1] != '-') return 0; + z->c--; + } while (0); + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return 1; +} + +static int r_uninflected(struct SN_env * z) { + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2672162 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + if (!find_among_b(z, a_5, 24, 0)) return 0; + do { + int v_1 = z->l - z->c; + if (z->c > z->lb) goto lab0; + break; + lab0: + z->c = z->l - v_1; + if (z->c <= z->lb || z->p[z->c - 1] != '-') return 0; + z->c--; + } while (0); + return 1; +} + +static int r_merged_numeral(struct SN_env * z) { + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 117)) return 0; + if (!find_among_b(z, a_6, 3, 0)) return 0; + if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 107 && z->p[z->c - 1] != 116)) return 0; + return find_among_b(z, a_7, 2, 0) != 0; +} + +static int r_correlative(struct SN_env * z) { + z->ket = z->c; + z->bra = z->c; + { + int v_1 = z->l - z->c; + do { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') { z->c = z->l - v_3; goto lab1; } + z->c--; + lab1: + ; + } + z->bra = z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab0; + z->c--; + break; + lab0: + z->c = z->l - v_2; + { + int v_4 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') { z->c = z->l - v_4; goto lab2; } + z->c--; + lab2: + ; + } + { + int v_5 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'j') { z->c = z->l - v_5; goto lab3; } + z->c--; + lab3: + ; + } + z->bra = z->c; + if (in_grouping_b_U(z, g_aou, 97, 117, 0)) return 0; + } while (0); + if (z->c <= z->lb || z->p[z->c - 1] != 'i') return 0; + z->c--; + { + int v_6 = z->l - z->c; + if (!find_among_b(z, a_8, 7, 0)) { z->c = z->l - v_6; goto lab4; } + lab4: + ; + } + do { + int v_7 = z->l - z->c; + if (z->c > z->lb) goto lab5; + break; + lab5: + z->c = z->l - v_7; + if (z->c <= z->lb || z->p[z->c - 1] != '-') return 0; + z->c--; + } while (0); + z->c = z->l - v_1; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return 1; +} + +static int r_long_word(struct SN_env * z) { + do { + int v_1 = z->l - z->c; + { + int i; for (i = 2; i > 0; i--) { + { + int ret = out_grouping_b_U(z, g_vowel, 97, 117, 1); + if (ret < 0) goto lab0; + z->c -= ret; + } + } + } + break; + lab0: + z->c = z->l - v_1; + while (1) { + if (z->c <= z->lb || z->p[z->c - 1] != '-') goto lab2; + z->c--; + break; + lab2: + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab1; + z->c = ret; + } + } + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab1; + z->c = ret; + } + break; + lab1: + z->c = z->l - v_1; + { + int ret = out_grouping_b_U(z, g_digit, 48, 57, 1); + if (ret < 0) return 0; + z->c -= ret; + } + } while (0); + return 1; +} + +static int r_standard_suffix(struct SN_env * z) { + int among_var; + z->ket = z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2672162 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; + among_var = find_among_b(z, a_9, 19, 0); + if (!among_var) return 0; + switch (among_var) { + case 1: + { + int v_1 = z->l - z->c; + do { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != '-') goto lab0; + z->c--; + break; + lab0: + z->c = z->l - v_2; + if (in_grouping_b_U(z, g_digit, 48, 57, 0)) return 0; + } while (0); + z->c = z->l - v_1; + } + break; + } + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != '-') { z->c = z->l - v_3; goto lab1; } + z->c--; + lab1: + ; + } + z->bra = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + return 1; +} + +extern int esperanto_UTF_8_stem(struct SN_env * z) { + { + int v_1 = z->c; + { + int ret = r_canonical_form(z); + if (ret <= 0) return ret; + } + z->c = v_1; + } + { + int v_2 = z->c; + { + int ret = r_initial_apostrophe(z); + if (ret < 0) return ret; + } + z->c = v_2; + } + z->lb = z->c; z->c = z->l; + { + int v_3 = z->l - z->c; + { + int ret = r_pronoun(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + return 0; + lab0: + z->c = z->l - v_3; + } + { + int v_4 = z->l - z->c; + { + int ret = r_final_apostrophe(z); + if (ret < 0) return ret; + } + z->c = z->l - v_4; + } + { + int v_5 = z->l - z->c; + { + int ret = r_correlative(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + return 0; + lab1: + z->c = z->l - v_5; + } + { + int v_6 = z->l - z->c; + { + int ret = r_uninflected(z); + if (ret == 0) goto lab2; + if (ret < 0) return ret; + } + return 0; + lab2: + z->c = z->l - v_6; + } + { + int v_7 = z->l - z->c; + { + int ret = r_merged_numeral(z); + if (ret == 0) goto lab3; + if (ret < 0) return ret; + } + return 0; + lab3: + z->c = z->l - v_7; + } + { + int v_8 = z->l - z->c; + { + int ret = r_ujn_suffix(z); + if (ret == 0) goto lab4; + if (ret < 0) return ret; + } + return 0; + lab4: + z->c = z->l - v_8; + } + { + int v_9 = z->l - z->c; + { + int ret = r_long_word(z); + if (ret <= 0) return ret; + } + z->c = z->l - v_9; + } + { + int ret = r_standard_suffix(z); + if (ret <= 0) return ret; + } + z->c = z->lb; + return 1; +} + +extern struct SN_env * esperanto_UTF_8_create_env(void) { + return SN_new_env(sizeof(struct SN_env)); +} + +extern void esperanto_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} + diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_estonian.c b/src/backend/snowball/libstemmer/stem_UTF_8_estonian.c index 168239919184f..1f6c18fa8e87e 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_estonian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_estonian.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from estonian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_estonian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +20,7 @@ extern int estonian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_nu(struct SN_env * z); static int r_verb(struct SN_env * z); static int r_verb_exceptions(struct SN_env * z); @@ -22,25 +34,41 @@ static int r_case_ending(struct SN_env * z); static int r_special_noun_endings(struct SN_env * z); static int r_LONGV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * estonian_UTF_8_create_env(void); -extern void estonian_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'l', 'a', 's', 'e' }; +static const symbol s_2[] = { 'm', 'i', 's', 'e' }; +static const symbol s_3[] = { 'l', 'i', 's', 'e' }; +static const symbol s_4[] = { 'i', 'k', 'u' }; +static const symbol s_5[] = { 'e' }; +static const symbol s_6[] = { 't' }; +static const symbol s_7[] = { 'k' }; +static const symbol s_8[] = { 'p' }; +static const symbol s_9[] = { 't' }; +static const symbol s_10[] = { 'j', 'o', 'o' }; +static const symbol s_11[] = { 's', 'a', 'a' }; +static const symbol s_12[] = { 'v', 'i', 'i', 'm', 'a' }; +static const symbol s_13[] = { 'k', 'e', 'e', 's', 'i' }; +static const symbol s_14[] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6 }; +static const symbol s_15[] = { 'l', 0xC3, 0xB5, 'i' }; +static const symbol s_16[] = { 'l', 'o', 'o' }; +static const symbol s_17[] = { 'k', 0xC3, 0xA4, 'i', 's', 'i' }; +static const symbol s_18[] = { 's', 0xC3, 0xB6, 0xC3, 0xB6 }; +static const symbol s_19[] = { 't', 'o', 'o' }; +static const symbol s_20[] = { 'v', 0xC3, 0xB5, 'i', 's', 'i' }; +static const symbol s_21[] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'a' }; +static const symbol s_22[] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 's', 'i' }; +static const symbol s_23[] = { 'l', 'u', 'g', 'e' }; +static const symbol s_24[] = { 'p', 0xC3, 0xB5, 'd', 'e' }; +static const symbol s_25[] = { 'l', 'a', 'd', 'u' }; +static const symbol s_26[] = { 't', 'e', 'g', 'i' }; +static const symbol s_27[] = { 'n', 0xC3, 0xA4, 'g', 'i' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 'g', 'i' }; static const symbol s_0_1[2] = { 'k', 'i' }; - -static const struct among a_0[2] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 2, s_0_1, -1, 2, 0} +static const struct among a_0[2] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 2, 0} }; static const symbol s_1_0[2] = { 'd', 'a' }; @@ -64,30 +92,28 @@ static const symbol s_1_17[4] = { 'k', 's', 'i', 'n' }; static const symbol s_1_18[6] = { 'n', 'u', 'k', 's', 'i', 'n' }; static const symbol s_1_19[4] = { 'd', 'a', 'k', 's' }; static const symbol s_1_20[4] = { 't', 'a', 'k', 's' }; - -static const struct among a_1[21] = -{ -{ 2, s_1_0, -1, 3, 0}, -{ 4, s_1_1, -1, 1, 0}, -{ 1, s_1_2, -1, 3, 0}, -{ 4, s_1_3, -1, 1, 0}, -{ 6, s_1_4, 3, 1, 0}, -{ 2, s_1_5, -1, 3, 0}, -{ 4, s_1_6, 5, 1, 0}, -{ 5, s_1_7, 6, 1, 0}, -{ 7, s_1_8, 7, 1, 0}, -{ 4, s_1_9, -1, 2, 0}, -{ 5, s_1_10, 9, 1, 0}, -{ 5, s_1_11, 9, 1, 0}, -{ 4, s_1_12, -1, 1, 0}, -{ 5, s_1_13, 12, 1, 0}, -{ 7, s_1_14, 13, 1, 0}, -{ 1, s_1_15, -1, 3, 0}, -{ 3, s_1_16, 15, 1, 0}, -{ 4, s_1_17, 16, 1, 0}, -{ 6, s_1_18, 17, 1, 0}, -{ 4, s_1_19, -1, 1, 0}, -{ 4, s_1_20, -1, 1, 0} +static const struct among a_1[21] = { +{ 2, s_1_0, 0, 3, 0}, +{ 4, s_1_1, 0, 1, 0}, +{ 1, s_1_2, 0, 3, 0}, +{ 4, s_1_3, 0, 1, 0}, +{ 6, s_1_4, -1, 1, 0}, +{ 2, s_1_5, 0, 3, 0}, +{ 4, s_1_6, -1, 1, 0}, +{ 5, s_1_7, -1, 1, 0}, +{ 7, s_1_8, -1, 1, 0}, +{ 4, s_1_9, 0, 2, 0}, +{ 5, s_1_10, -1, 1, 0}, +{ 5, s_1_11, -2, 1, 0}, +{ 4, s_1_12, 0, 1, 0}, +{ 5, s_1_13, -1, 1, 0}, +{ 7, s_1_14, -1, 1, 0}, +{ 1, s_1_15, 0, 3, 0}, +{ 3, s_1_16, -1, 1, 0}, +{ 4, s_1_17, -1, 1, 0}, +{ 6, s_1_18, -1, 1, 0}, +{ 4, s_1_19, 0, 1, 0}, +{ 4, s_1_20, 0, 1, 0} }; static const symbol s_2_0[2] = { 'a', 'a' }; @@ -99,732 +125,707 @@ static const symbol s_2_5[4] = { 0xC3, 0xA4, 0xC3, 0xA4 }; static const symbol s_2_6[4] = { 0xC3, 0xB5, 0xC3, 0xB5 }; static const symbol s_2_7[4] = { 0xC3, 0xB6, 0xC3, 0xB6 }; static const symbol s_2_8[4] = { 0xC3, 0xBC, 0xC3, 0xBC }; - -static const struct among a_2[9] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0}, -{ 2, s_2_3, -1, -1, 0}, -{ 2, s_2_4, -1, -1, 0}, -{ 4, s_2_5, -1, -1, 0}, -{ 4, s_2_6, -1, -1, 0}, -{ 4, s_2_7, -1, -1, 0}, -{ 4, s_2_8, -1, -1, 0} -}; - -static const symbol s_3_0[1] = { 'i' }; - -static const struct among a_3[1] = -{ -{ 1, s_3_0, -1, 1, 0} +static const struct among a_2[9] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, -1, 0}, +{ 2, s_2_4, 0, -1, 0}, +{ 4, s_2_5, 0, -1, 0}, +{ 4, s_2_6, 0, -1, 0}, +{ 4, s_2_7, 0, -1, 0}, +{ 4, s_2_8, 0, -1, 0} }; -static const symbol s_4_0[4] = { 'l', 'a', 'n', 'e' }; -static const symbol s_4_1[4] = { 'l', 'i', 'n', 'e' }; -static const symbol s_4_2[4] = { 'm', 'i', 'n', 'e' }; -static const symbol s_4_3[5] = { 'l', 'a', 's', 's', 'e' }; -static const symbol s_4_4[5] = { 'l', 'i', 's', 's', 'e' }; -static const symbol s_4_5[5] = { 'm', 'i', 's', 's', 'e' }; -static const symbol s_4_6[4] = { 'l', 'a', 's', 'i' }; -static const symbol s_4_7[4] = { 'l', 'i', 's', 'i' }; -static const symbol s_4_8[4] = { 'm', 'i', 's', 'i' }; -static const symbol s_4_9[4] = { 'l', 'a', 's', 't' }; -static const symbol s_4_10[4] = { 'l', 'i', 's', 't' }; -static const symbol s_4_11[4] = { 'm', 'i', 's', 't' }; - -static const struct among a_4[12] = -{ -{ 4, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 3, 0}, -{ 4, s_4_2, -1, 2, 0}, -{ 5, s_4_3, -1, 1, 0}, -{ 5, s_4_4, -1, 3, 0}, -{ 5, s_4_5, -1, 2, 0}, -{ 4, s_4_6, -1, 1, 0}, -{ 4, s_4_7, -1, 3, 0}, -{ 4, s_4_8, -1, 2, 0}, -{ 4, s_4_9, -1, 1, 0}, -{ 4, s_4_10, -1, 3, 0}, -{ 4, s_4_11, -1, 2, 0} +static const symbol s_3_0[4] = { 'l', 'a', 'n', 'e' }; +static const symbol s_3_1[4] = { 'l', 'i', 'n', 'e' }; +static const symbol s_3_2[4] = { 'm', 'i', 'n', 'e' }; +static const symbol s_3_3[5] = { 'l', 'a', 's', 's', 'e' }; +static const symbol s_3_4[5] = { 'l', 'i', 's', 's', 'e' }; +static const symbol s_3_5[5] = { 'm', 'i', 's', 's', 'e' }; +static const symbol s_3_6[4] = { 'l', 'a', 's', 'i' }; +static const symbol s_3_7[4] = { 'l', 'i', 's', 'i' }; +static const symbol s_3_8[4] = { 'm', 'i', 's', 'i' }; +static const symbol s_3_9[4] = { 'l', 'a', 's', 't' }; +static const symbol s_3_10[4] = { 'l', 'i', 's', 't' }; +static const symbol s_3_11[4] = { 'm', 'i', 's', 't' }; +static const struct among a_3[12] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 3, 0}, +{ 4, s_3_2, 0, 2, 0}, +{ 5, s_3_3, 0, 1, 0}, +{ 5, s_3_4, 0, 3, 0}, +{ 5, s_3_5, 0, 2, 0}, +{ 4, s_3_6, 0, 1, 0}, +{ 4, s_3_7, 0, 3, 0}, +{ 4, s_3_8, 0, 2, 0}, +{ 4, s_3_9, 0, 1, 0}, +{ 4, s_3_10, 0, 3, 0}, +{ 4, s_3_11, 0, 2, 0} }; -static const symbol s_5_0[2] = { 'g', 'a' }; -static const symbol s_5_1[2] = { 't', 'a' }; -static const symbol s_5_2[2] = { 'l', 'e' }; -static const symbol s_5_3[3] = { 's', 's', 'e' }; -static const symbol s_5_4[1] = { 'l' }; -static const symbol s_5_5[1] = { 's' }; -static const symbol s_5_6[2] = { 'k', 's' }; -static const symbol s_5_7[1] = { 't' }; -static const symbol s_5_8[2] = { 'l', 't' }; -static const symbol s_5_9[2] = { 's', 't' }; - -static const struct among a_5[10] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 1, 0}, -{ 2, s_5_2, -1, 1, 0}, -{ 3, s_5_3, -1, 1, 0}, -{ 1, s_5_4, -1, 1, 0}, -{ 1, s_5_5, -1, 1, 0}, -{ 2, s_5_6, 5, 1, 0}, -{ 1, s_5_7, -1, 2, 0}, -{ 2, s_5_8, 7, 1, 0}, -{ 2, s_5_9, 7, 1, 0} +static const symbol s_4_0[2] = { 'g', 'a' }; +static const symbol s_4_1[2] = { 't', 'a' }; +static const symbol s_4_2[2] = { 'l', 'e' }; +static const symbol s_4_3[3] = { 's', 's', 'e' }; +static const symbol s_4_4[1] = { 'l' }; +static const symbol s_4_5[1] = { 's' }; +static const symbol s_4_6[2] = { 'k', 's' }; +static const symbol s_4_7[1] = { 't' }; +static const symbol s_4_8[2] = { 'l', 't' }; +static const symbol s_4_9[2] = { 's', 't' }; +static const struct among a_4[10] = { +{ 2, s_4_0, 0, 1, 0}, +{ 2, s_4_1, 0, 1, 0}, +{ 2, s_4_2, 0, 1, 0}, +{ 3, s_4_3, 0, 1, 0}, +{ 1, s_4_4, 0, 1, 0}, +{ 1, s_4_5, 0, 1, 0}, +{ 2, s_4_6, -1, 1, 0}, +{ 1, s_4_7, 0, 2, 0}, +{ 2, s_4_8, -1, 1, 0}, +{ 2, s_4_9, -2, 1, 0} }; -static const symbol s_6_1[3] = { 'l', 'a', 's' }; -static const symbol s_6_2[3] = { 'l', 'i', 's' }; -static const symbol s_6_3[3] = { 'm', 'i', 's' }; -static const symbol s_6_4[1] = { 't' }; - -static const struct among a_6[5] = -{ -{ 0, 0, -1, 2, 0}, -{ 3, s_6_1, 0, 1, 0}, -{ 3, s_6_2, 0, 1, 0}, -{ 3, s_6_3, 0, 1, 0}, -{ 1, s_6_4, 0, -1, 0} +static const symbol s_5_1[3] = { 'l', 'a', 's' }; +static const symbol s_5_2[3] = { 'l', 'i', 's' }; +static const symbol s_5_3[3] = { 'm', 'i', 's' }; +static const symbol s_5_4[1] = { 't' }; +static const struct among a_5[5] = { +{ 0, 0, 0, 2, 0}, +{ 3, s_5_1, -1, 1, 0}, +{ 3, s_5_2, -2, 1, 0}, +{ 3, s_5_3, -3, 1, 0}, +{ 1, s_5_4, -4, -1, 0} }; -static const symbol s_7_0[1] = { 'd' }; -static const symbol s_7_1[3] = { 's', 'i', 'd' }; -static const symbol s_7_2[2] = { 'd', 'e' }; -static const symbol s_7_3[6] = { 'i', 'k', 'k', 'u', 'd', 'e' }; -static const symbol s_7_4[3] = { 'i', 'k', 'e' }; -static const symbol s_7_5[4] = { 'i', 'k', 'k', 'e' }; -static const symbol s_7_6[2] = { 't', 'e' }; - -static const struct among a_7[7] = -{ -{ 1, s_7_0, -1, 4, 0}, -{ 3, s_7_1, 0, 2, 0}, -{ 2, s_7_2, -1, 4, 0}, -{ 6, s_7_3, 2, 1, 0}, -{ 3, s_7_4, -1, 1, 0}, -{ 4, s_7_5, -1, 1, 0}, -{ 2, s_7_6, -1, 3, 0} +static const symbol s_6_0[1] = { 'd' }; +static const symbol s_6_1[3] = { 's', 'i', 'd' }; +static const symbol s_6_2[2] = { 'd', 'e' }; +static const symbol s_6_3[6] = { 'i', 'k', 'k', 'u', 'd', 'e' }; +static const symbol s_6_4[3] = { 'i', 'k', 'e' }; +static const symbol s_6_5[4] = { 'i', 'k', 'k', 'e' }; +static const symbol s_6_6[2] = { 't', 'e' }; +static const struct among a_6[7] = { +{ 1, s_6_0, 0, 4, 0}, +{ 3, s_6_1, -1, 2, 0}, +{ 2, s_6_2, 0, 4, 0}, +{ 6, s_6_3, -1, 1, 0}, +{ 3, s_6_4, 0, 1, 0}, +{ 4, s_6_5, 0, 1, 0}, +{ 2, s_6_6, 0, 3, 0} }; -static const symbol s_8_0[2] = { 'v', 'a' }; -static const symbol s_8_1[2] = { 'd', 'u' }; -static const symbol s_8_2[2] = { 'n', 'u' }; -static const symbol s_8_3[2] = { 't', 'u' }; - -static const struct among a_8[4] = -{ -{ 2, s_8_0, -1, -1, 0}, -{ 2, s_8_1, -1, -1, 0}, -{ 2, s_8_2, -1, -1, 0}, -{ 2, s_8_3, -1, -1, 0} +static const symbol s_7_0[2] = { 'v', 'a' }; +static const symbol s_7_1[2] = { 'd', 'u' }; +static const symbol s_7_2[2] = { 'n', 'u' }; +static const symbol s_7_3[2] = { 't', 'u' }; +static const struct among a_7[4] = { +{ 2, s_7_0, 0, -1, 0}, +{ 2, s_7_1, 0, -1, 0}, +{ 2, s_7_2, 0, -1, 0}, +{ 2, s_7_3, 0, -1, 0} }; -static const symbol s_9_0[2] = { 'k', 'k' }; -static const symbol s_9_1[2] = { 'p', 'p' }; -static const symbol s_9_2[2] = { 't', 't' }; - -static const struct among a_9[3] = -{ -{ 2, s_9_0, -1, 1, 0}, -{ 2, s_9_1, -1, 2, 0}, -{ 2, s_9_2, -1, 3, 0} +static const symbol s_8_0[2] = { 'k', 'k' }; +static const symbol s_8_1[2] = { 'p', 'p' }; +static const symbol s_8_2[2] = { 't', 't' }; +static const struct among a_8[3] = { +{ 2, s_8_0, 0, 1, 0}, +{ 2, s_8_1, 0, 2, 0}, +{ 2, s_8_2, 0, 3, 0} }; -static const symbol s_10_0[2] = { 'm', 'a' }; -static const symbol s_10_1[3] = { 'm', 'a', 'i' }; -static const symbol s_10_2[1] = { 'm' }; - -static const struct among a_10[3] = -{ -{ 2, s_10_0, -1, 2, 0}, -{ 3, s_10_1, -1, 1, 0}, -{ 1, s_10_2, -1, 1, 0} +static const symbol s_9_0[2] = { 'm', 'a' }; +static const symbol s_9_1[3] = { 'm', 'a', 'i' }; +static const symbol s_9_2[1] = { 'm' }; +static const struct among a_9[3] = { +{ 2, s_9_0, 0, 2, 0}, +{ 3, s_9_1, 0, 1, 0}, +{ 1, s_9_2, 0, 1, 0} }; -static const symbol s_11_0[4] = { 'j', 'o', 'o', 'b' }; -static const symbol s_11_1[4] = { 'j', 'o', 'o', 'd' }; -static const symbol s_11_2[8] = { 'j', 'o', 'o', 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_3[5] = { 'j', 'o', 'o', 'm', 'a' }; -static const symbol s_11_4[7] = { 'j', 'o', 'o', 'm', 'a', 't', 'a' }; -static const symbol s_11_5[5] = { 'j', 'o', 'o', 'm', 'e' }; -static const symbol s_11_6[4] = { 'j', 'o', 'o', 'n' }; -static const symbol s_11_7[5] = { 'j', 'o', 'o', 't', 'e' }; -static const symbol s_11_8[6] = { 'j', 'o', 'o', 'v', 'a', 'd' }; -static const symbol s_11_9[4] = { 'j', 'u', 'u', 'a' }; -static const symbol s_11_10[7] = { 'j', 'u', 'u', 'a', 'k', 's', 'e' }; -static const symbol s_11_11[4] = { 'j', 0xC3, 0xA4, 'i' }; -static const symbol s_11_12[5] = { 'j', 0xC3, 0xA4, 'i', 'd' }; -static const symbol s_11_13[6] = { 'j', 0xC3, 0xA4, 'i', 'm', 'e' }; -static const symbol s_11_14[5] = { 'j', 0xC3, 0xA4, 'i', 'n' }; -static const symbol s_11_15[6] = { 'j', 0xC3, 0xA4, 'i', 't', 'e' }; -static const symbol s_11_16[6] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'b' }; -static const symbol s_11_17[6] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd' }; -static const symbol s_11_18[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd', 'a' }; -static const symbol s_11_19[10] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_20[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd', 'i' }; -static const symbol s_11_21[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's' }; -static const symbol s_11_22[9] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 'd' }; -static const symbol s_11_23[10] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_24[9] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 'n' }; -static const symbol s_11_25[10] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_26[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'a' }; -static const symbol s_11_27[9] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'a', 't', 'a' }; -static const symbol s_11_28[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'e' }; -static const symbol s_11_29[6] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'n' }; -static const symbol s_11_30[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 't', 'e' }; -static const symbol s_11_31[8] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'v', 'a', 'd' }; -static const symbol s_11_32[4] = { 'j', 0xC3, 0xB5, 'i' }; -static const symbol s_11_33[5] = { 'j', 0xC3, 0xB5, 'i', 'd' }; -static const symbol s_11_34[6] = { 'j', 0xC3, 0xB5, 'i', 'm', 'e' }; -static const symbol s_11_35[5] = { 'j', 0xC3, 0xB5, 'i', 'n' }; -static const symbol s_11_36[6] = { 'j', 0xC3, 0xB5, 'i', 't', 'e' }; -static const symbol s_11_37[4] = { 'k', 'e', 'e', 'b' }; -static const symbol s_11_38[4] = { 'k', 'e', 'e', 'd' }; -static const symbol s_11_39[8] = { 'k', 'e', 'e', 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_40[5] = { 'k', 'e', 'e', 'k', 's' }; -static const symbol s_11_41[7] = { 'k', 'e', 'e', 'k', 's', 'i', 'd' }; -static const symbol s_11_42[8] = { 'k', 'e', 'e', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_43[7] = { 'k', 'e', 'e', 'k', 's', 'i', 'n' }; -static const symbol s_11_44[8] = { 'k', 'e', 'e', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_45[5] = { 'k', 'e', 'e', 'm', 'a' }; -static const symbol s_11_46[7] = { 'k', 'e', 'e', 'm', 'a', 't', 'a' }; -static const symbol s_11_47[5] = { 'k', 'e', 'e', 'm', 'e' }; -static const symbol s_11_48[4] = { 'k', 'e', 'e', 'n' }; -static const symbol s_11_49[4] = { 'k', 'e', 'e', 's' }; -static const symbol s_11_50[5] = { 'k', 'e', 'e', 't', 'a' }; -static const symbol s_11_51[5] = { 'k', 'e', 'e', 't', 'e' }; -static const symbol s_11_52[6] = { 'k', 'e', 'e', 'v', 'a', 'd' }; -static const symbol s_11_53[5] = { 'k', 0xC3, 0xA4, 'i', 'a' }; -static const symbol s_11_54[8] = { 'k', 0xC3, 0xA4, 'i', 'a', 'k', 's', 'e' }; -static const symbol s_11_55[5] = { 'k', 0xC3, 0xA4, 'i', 'b' }; -static const symbol s_11_56[5] = { 'k', 0xC3, 0xA4, 'i', 'd' }; -static const symbol s_11_57[6] = { 'k', 0xC3, 0xA4, 'i', 'd', 'i' }; -static const symbol s_11_58[6] = { 'k', 0xC3, 0xA4, 'i', 'k', 's' }; -static const symbol s_11_59[8] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 'd' }; -static const symbol s_11_60[9] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_61[8] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 'n' }; -static const symbol s_11_62[9] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_63[6] = { 'k', 0xC3, 0xA4, 'i', 'm', 'a' }; -static const symbol s_11_64[8] = { 'k', 0xC3, 0xA4, 'i', 'm', 'a', 't', 'a' }; -static const symbol s_11_65[6] = { 'k', 0xC3, 0xA4, 'i', 'm', 'e' }; -static const symbol s_11_66[5] = { 'k', 0xC3, 0xA4, 'i', 'n' }; -static const symbol s_11_67[5] = { 'k', 0xC3, 0xA4, 'i', 's' }; -static const symbol s_11_68[6] = { 'k', 0xC3, 0xA4, 'i', 't', 'e' }; -static const symbol s_11_69[7] = { 'k', 0xC3, 0xA4, 'i', 'v', 'a', 'd' }; -static const symbol s_11_70[4] = { 'l', 'a', 'o', 'b' }; -static const symbol s_11_71[4] = { 'l', 'a', 'o', 'd' }; -static const symbol s_11_72[5] = { 'l', 'a', 'o', 'k', 's' }; -static const symbol s_11_73[7] = { 'l', 'a', 'o', 'k', 's', 'i', 'd' }; -static const symbol s_11_74[8] = { 'l', 'a', 'o', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_75[7] = { 'l', 'a', 'o', 'k', 's', 'i', 'n' }; -static const symbol s_11_76[8] = { 'l', 'a', 'o', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_77[5] = { 'l', 'a', 'o', 'm', 'e' }; -static const symbol s_11_78[4] = { 'l', 'a', 'o', 'n' }; -static const symbol s_11_79[5] = { 'l', 'a', 'o', 't', 'e' }; -static const symbol s_11_80[6] = { 'l', 'a', 'o', 'v', 'a', 'd' }; -static const symbol s_11_81[4] = { 'l', 'o', 'e', 'b' }; -static const symbol s_11_82[4] = { 'l', 'o', 'e', 'd' }; -static const symbol s_11_83[5] = { 'l', 'o', 'e', 'k', 's' }; -static const symbol s_11_84[7] = { 'l', 'o', 'e', 'k', 's', 'i', 'd' }; -static const symbol s_11_85[8] = { 'l', 'o', 'e', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_86[7] = { 'l', 'o', 'e', 'k', 's', 'i', 'n' }; -static const symbol s_11_87[8] = { 'l', 'o', 'e', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_88[5] = { 'l', 'o', 'e', 'm', 'e' }; -static const symbol s_11_89[4] = { 'l', 'o', 'e', 'n' }; -static const symbol s_11_90[5] = { 'l', 'o', 'e', 't', 'e' }; -static const symbol s_11_91[6] = { 'l', 'o', 'e', 'v', 'a', 'd' }; -static const symbol s_11_92[4] = { 'l', 'o', 'o', 'b' }; -static const symbol s_11_93[4] = { 'l', 'o', 'o', 'd' }; -static const symbol s_11_94[5] = { 'l', 'o', 'o', 'd', 'i' }; -static const symbol s_11_95[5] = { 'l', 'o', 'o', 'k', 's' }; -static const symbol s_11_96[7] = { 'l', 'o', 'o', 'k', 's', 'i', 'd' }; -static const symbol s_11_97[8] = { 'l', 'o', 'o', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_98[7] = { 'l', 'o', 'o', 'k', 's', 'i', 'n' }; -static const symbol s_11_99[8] = { 'l', 'o', 'o', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_100[5] = { 'l', 'o', 'o', 'm', 'a' }; -static const symbol s_11_101[7] = { 'l', 'o', 'o', 'm', 'a', 't', 'a' }; -static const symbol s_11_102[5] = { 'l', 'o', 'o', 'm', 'e' }; -static const symbol s_11_103[4] = { 'l', 'o', 'o', 'n' }; -static const symbol s_11_104[5] = { 'l', 'o', 'o', 't', 'e' }; -static const symbol s_11_105[6] = { 'l', 'o', 'o', 'v', 'a', 'd' }; -static const symbol s_11_106[4] = { 'l', 'u', 'u', 'a' }; -static const symbol s_11_107[7] = { 'l', 'u', 'u', 'a', 'k', 's', 'e' }; -static const symbol s_11_108[4] = { 'l', 0xC3, 0xB5, 'i' }; -static const symbol s_11_109[5] = { 'l', 0xC3, 0xB5, 'i', 'd' }; -static const symbol s_11_110[6] = { 'l', 0xC3, 0xB5, 'i', 'm', 'e' }; -static const symbol s_11_111[5] = { 'l', 0xC3, 0xB5, 'i', 'n' }; -static const symbol s_11_112[6] = { 'l', 0xC3, 0xB5, 'i', 't', 'e' }; -static const symbol s_11_113[6] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'b' }; -static const symbol s_11_114[6] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'd' }; -static const symbol s_11_115[10] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_116[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'i' }; -static const symbol s_11_117[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's' }; -static const symbol s_11_118[9] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'd' }; -static const symbol s_11_119[10] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_120[9] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'n' }; -static const symbol s_11_121[10] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_122[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a' }; -static const symbol s_11_123[9] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a', 't', 'a' }; -static const symbol s_11_124[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'e' }; -static const symbol s_11_125[6] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'n' }; -static const symbol s_11_126[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 't', 'e' }; -static const symbol s_11_127[8] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'v', 'a', 'd' }; -static const symbol s_11_128[6] = { 'l', 0xC3, 0xBC, 0xC3, 0xBC, 'a' }; -static const symbol s_11_129[9] = { 'l', 0xC3, 0xBC, 0xC3, 0xBC, 'a', 'k', 's', 'e' }; -static const symbol s_11_130[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'a' }; -static const symbol s_11_131[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'a', 'k', 's', 'e' }; -static const symbol s_11_132[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'b' }; -static const symbol s_11_133[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'd' }; -static const symbol s_11_134[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'd', 'i' }; -static const symbol s_11_135[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's' }; -static const symbol s_11_136[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 'd' }; -static const symbol s_11_137[10] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_138[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 'n' }; -static const symbol s_11_139[10] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_140[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'm', 'a' }; -static const symbol s_11_141[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'm', 'a', 't', 'a' }; -static const symbol s_11_142[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'm', 'e' }; -static const symbol s_11_143[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'n' }; -static const symbol s_11_144[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 's' }; -static const symbol s_11_145[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 't', 'e' }; -static const symbol s_11_146[8] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'v', 'a', 'd' }; -static const symbol s_11_147[5] = { 'n', 0xC3, 0xA4, 'e', 'b' }; -static const symbol s_11_148[5] = { 'n', 0xC3, 0xA4, 'e', 'd' }; -static const symbol s_11_149[6] = { 'n', 0xC3, 0xA4, 'e', 'k', 's' }; -static const symbol s_11_150[8] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 'd' }; -static const symbol s_11_151[9] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_152[8] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 'n' }; -static const symbol s_11_153[9] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_154[6] = { 'n', 0xC3, 0xA4, 'e', 'm', 'e' }; -static const symbol s_11_155[5] = { 'n', 0xC3, 0xA4, 'e', 'n' }; -static const symbol s_11_156[6] = { 'n', 0xC3, 0xA4, 'e', 't', 'e' }; -static const symbol s_11_157[7] = { 'n', 0xC3, 0xA4, 'e', 'v', 'a', 'd' }; -static const symbol s_11_158[7] = { 'n', 0xC3, 0xA4, 'g', 'e', 'm', 'a' }; -static const symbol s_11_159[9] = { 'n', 0xC3, 0xA4, 'g', 'e', 'm', 'a', 't', 'a' }; -static const symbol s_11_160[5] = { 'n', 0xC3, 0xA4, 'h', 'a' }; -static const symbol s_11_161[8] = { 'n', 0xC3, 0xA4, 'h', 'a', 'k', 's', 'e' }; -static const symbol s_11_162[6] = { 'n', 0xC3, 0xA4, 'h', 't', 'i' }; -static const symbol s_11_163[5] = { 'p', 0xC3, 0xB5, 'e', 'b' }; -static const symbol s_11_164[5] = { 'p', 0xC3, 0xB5, 'e', 'd' }; -static const symbol s_11_165[6] = { 'p', 0xC3, 0xB5, 'e', 'k', 's' }; -static const symbol s_11_166[8] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 'd' }; -static const symbol s_11_167[9] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_168[8] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 'n' }; -static const symbol s_11_169[9] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_170[6] = { 'p', 0xC3, 0xB5, 'e', 'm', 'e' }; -static const symbol s_11_171[5] = { 'p', 0xC3, 0xB5, 'e', 'n' }; -static const symbol s_11_172[6] = { 'p', 0xC3, 0xB5, 'e', 't', 'e' }; -static const symbol s_11_173[7] = { 'p', 0xC3, 0xB5, 'e', 'v', 'a', 'd' }; -static const symbol s_11_174[4] = { 's', 'a', 'a', 'b' }; -static const symbol s_11_175[4] = { 's', 'a', 'a', 'd' }; -static const symbol s_11_176[5] = { 's', 'a', 'a', 'd', 'a' }; -static const symbol s_11_177[8] = { 's', 'a', 'a', 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_178[5] = { 's', 'a', 'a', 'd', 'i' }; -static const symbol s_11_179[5] = { 's', 'a', 'a', 'k', 's' }; -static const symbol s_11_180[7] = { 's', 'a', 'a', 'k', 's', 'i', 'd' }; -static const symbol s_11_181[8] = { 's', 'a', 'a', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_182[7] = { 's', 'a', 'a', 'k', 's', 'i', 'n' }; -static const symbol s_11_183[8] = { 's', 'a', 'a', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_184[5] = { 's', 'a', 'a', 'm', 'a' }; -static const symbol s_11_185[7] = { 's', 'a', 'a', 'm', 'a', 't', 'a' }; -static const symbol s_11_186[5] = { 's', 'a', 'a', 'm', 'e' }; -static const symbol s_11_187[4] = { 's', 'a', 'a', 'n' }; -static const symbol s_11_188[5] = { 's', 'a', 'a', 't', 'e' }; -static const symbol s_11_189[6] = { 's', 'a', 'a', 'v', 'a', 'd' }; -static const symbol s_11_190[3] = { 's', 'a', 'i' }; -static const symbol s_11_191[4] = { 's', 'a', 'i', 'd' }; -static const symbol s_11_192[5] = { 's', 'a', 'i', 'm', 'e' }; -static const symbol s_11_193[4] = { 's', 'a', 'i', 'n' }; -static const symbol s_11_194[5] = { 's', 'a', 'i', 't', 'e' }; -static const symbol s_11_195[4] = { 's', 0xC3, 0xB5, 'i' }; -static const symbol s_11_196[5] = { 's', 0xC3, 0xB5, 'i', 'd' }; -static const symbol s_11_197[6] = { 's', 0xC3, 0xB5, 'i', 'm', 'e' }; -static const symbol s_11_198[5] = { 's', 0xC3, 0xB5, 'i', 'n' }; -static const symbol s_11_199[6] = { 's', 0xC3, 0xB5, 'i', 't', 'e' }; -static const symbol s_11_200[6] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'b' }; -static const symbol s_11_201[6] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'd' }; -static const symbol s_11_202[10] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_203[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'i' }; -static const symbol s_11_204[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's' }; -static const symbol s_11_205[9] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'd' }; -static const symbol s_11_206[10] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_207[9] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'n' }; -static const symbol s_11_208[10] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_209[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a' }; -static const symbol s_11_210[9] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a', 't', 'a' }; -static const symbol s_11_211[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'e' }; -static const symbol s_11_212[6] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'n' }; -static const symbol s_11_213[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 't', 'e' }; -static const symbol s_11_214[8] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'v', 'a', 'd' }; -static const symbol s_11_215[6] = { 's', 0xC3, 0xBC, 0xC3, 0xBC, 'a' }; -static const symbol s_11_216[9] = { 's', 0xC3, 0xBC, 0xC3, 0xBC, 'a', 'k', 's', 'e' }; -static const symbol s_11_217[4] = { 't', 'e', 'e', 'b' }; -static const symbol s_11_218[4] = { 't', 'e', 'e', 'd' }; -static const symbol s_11_219[5] = { 't', 'e', 'e', 'k', 's' }; -static const symbol s_11_220[7] = { 't', 'e', 'e', 'k', 's', 'i', 'd' }; -static const symbol s_11_221[8] = { 't', 'e', 'e', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_222[7] = { 't', 'e', 'e', 'k', 's', 'i', 'n' }; -static const symbol s_11_223[8] = { 't', 'e', 'e', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_224[5] = { 't', 'e', 'e', 'm', 'e' }; -static const symbol s_11_225[4] = { 't', 'e', 'e', 'n' }; -static const symbol s_11_226[5] = { 't', 'e', 'e', 't', 'e' }; -static const symbol s_11_227[6] = { 't', 'e', 'e', 'v', 'a', 'd' }; -static const symbol s_11_228[6] = { 't', 'e', 'g', 'e', 'm', 'a' }; -static const symbol s_11_229[8] = { 't', 'e', 'g', 'e', 'm', 'a', 't', 'a' }; -static const symbol s_11_230[4] = { 't', 'e', 'h', 'a' }; -static const symbol s_11_231[7] = { 't', 'e', 'h', 'a', 'k', 's', 'e' }; -static const symbol s_11_232[5] = { 't', 'e', 'h', 't', 'i' }; -static const symbol s_11_233[4] = { 't', 'o', 'o', 'b' }; -static const symbol s_11_234[4] = { 't', 'o', 'o', 'd' }; -static const symbol s_11_235[5] = { 't', 'o', 'o', 'd', 'i' }; -static const symbol s_11_236[5] = { 't', 'o', 'o', 'k', 's' }; -static const symbol s_11_237[7] = { 't', 'o', 'o', 'k', 's', 'i', 'd' }; -static const symbol s_11_238[8] = { 't', 'o', 'o', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_239[7] = { 't', 'o', 'o', 'k', 's', 'i', 'n' }; -static const symbol s_11_240[8] = { 't', 'o', 'o', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_241[5] = { 't', 'o', 'o', 'm', 'a' }; -static const symbol s_11_242[7] = { 't', 'o', 'o', 'm', 'a', 't', 'a' }; -static const symbol s_11_243[5] = { 't', 'o', 'o', 'm', 'e' }; -static const symbol s_11_244[4] = { 't', 'o', 'o', 'n' }; -static const symbol s_11_245[5] = { 't', 'o', 'o', 't', 'e' }; -static const symbol s_11_246[6] = { 't', 'o', 'o', 'v', 'a', 'd' }; -static const symbol s_11_247[4] = { 't', 'u', 'u', 'a' }; -static const symbol s_11_248[7] = { 't', 'u', 'u', 'a', 'k', 's', 'e' }; -static const symbol s_11_249[4] = { 't', 0xC3, 0xB5, 'i' }; -static const symbol s_11_250[5] = { 't', 0xC3, 0xB5, 'i', 'd' }; -static const symbol s_11_251[6] = { 't', 0xC3, 0xB5, 'i', 'm', 'e' }; -static const symbol s_11_252[5] = { 't', 0xC3, 0xB5, 'i', 'n' }; -static const symbol s_11_253[6] = { 't', 0xC3, 0xB5, 'i', 't', 'e' }; -static const symbol s_11_254[4] = { 'v', 'i', 'i', 'a' }; -static const symbol s_11_255[7] = { 'v', 'i', 'i', 'a', 'k', 's', 'e' }; -static const symbol s_11_256[4] = { 'v', 'i', 'i', 'b' }; -static const symbol s_11_257[4] = { 'v', 'i', 'i', 'd' }; -static const symbol s_11_258[5] = { 'v', 'i', 'i', 'd', 'i' }; -static const symbol s_11_259[5] = { 'v', 'i', 'i', 'k', 's' }; -static const symbol s_11_260[7] = { 'v', 'i', 'i', 'k', 's', 'i', 'd' }; -static const symbol s_11_261[8] = { 'v', 'i', 'i', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_262[7] = { 'v', 'i', 'i', 'k', 's', 'i', 'n' }; -static const symbol s_11_263[8] = { 'v', 'i', 'i', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_264[5] = { 'v', 'i', 'i', 'm', 'a' }; -static const symbol s_11_265[7] = { 'v', 'i', 'i', 'm', 'a', 't', 'a' }; -static const symbol s_11_266[5] = { 'v', 'i', 'i', 'm', 'e' }; -static const symbol s_11_267[4] = { 'v', 'i', 'i', 'n' }; -static const symbol s_11_268[7] = { 'v', 'i', 'i', 's', 'i', 'm', 'e' }; -static const symbol s_11_269[6] = { 'v', 'i', 'i', 's', 'i', 'n' }; -static const symbol s_11_270[7] = { 'v', 'i', 'i', 's', 'i', 't', 'e' }; -static const symbol s_11_271[5] = { 'v', 'i', 'i', 't', 'e' }; -static const symbol s_11_272[6] = { 'v', 'i', 'i', 'v', 'a', 'd' }; -static const symbol s_11_273[5] = { 'v', 0xC3, 0xB5, 'i', 'b' }; -static const symbol s_11_274[5] = { 'v', 0xC3, 0xB5, 'i', 'd' }; -static const symbol s_11_275[6] = { 'v', 0xC3, 0xB5, 'i', 'd', 'a' }; -static const symbol s_11_276[9] = { 'v', 0xC3, 0xB5, 'i', 'd', 'a', 'k', 's', 'e' }; -static const symbol s_11_277[6] = { 'v', 0xC3, 0xB5, 'i', 'd', 'i' }; -static const symbol s_11_278[6] = { 'v', 0xC3, 0xB5, 'i', 'k', 's' }; -static const symbol s_11_279[8] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 'd' }; -static const symbol s_11_280[9] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 'm', 'e' }; -static const symbol s_11_281[8] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 'n' }; -static const symbol s_11_282[9] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 't', 'e' }; -static const symbol s_11_283[6] = { 'v', 0xC3, 0xB5, 'i', 'm', 'a' }; -static const symbol s_11_284[8] = { 'v', 0xC3, 0xB5, 'i', 'm', 'a', 't', 'a' }; -static const symbol s_11_285[6] = { 'v', 0xC3, 0xB5, 'i', 'm', 'e' }; -static const symbol s_11_286[5] = { 'v', 0xC3, 0xB5, 'i', 'n' }; -static const symbol s_11_287[5] = { 'v', 0xC3, 0xB5, 'i', 's' }; -static const symbol s_11_288[6] = { 'v', 0xC3, 0xB5, 'i', 't', 'e' }; -static const symbol s_11_289[7] = { 'v', 0xC3, 0xB5, 'i', 'v', 'a', 'd' }; - -static const struct among a_11[290] = -{ -{ 4, s_11_0, -1, 1, 0}, -{ 4, s_11_1, -1, 1, 0}, -{ 8, s_11_2, 1, 1, 0}, -{ 5, s_11_3, -1, 1, 0}, -{ 7, s_11_4, 3, 1, 0}, -{ 5, s_11_5, -1, 1, 0}, -{ 4, s_11_6, -1, 1, 0}, -{ 5, s_11_7, -1, 1, 0}, -{ 6, s_11_8, -1, 1, 0}, -{ 4, s_11_9, -1, 1, 0}, -{ 7, s_11_10, 9, 1, 0}, -{ 4, s_11_11, -1, 12, 0}, -{ 5, s_11_12, 11, 12, 0}, -{ 6, s_11_13, 11, 12, 0}, -{ 5, s_11_14, 11, 12, 0}, -{ 6, s_11_15, 11, 12, 0}, -{ 6, s_11_16, -1, 12, 0}, -{ 6, s_11_17, -1, 12, 0}, -{ 7, s_11_18, 17, 12, 0}, -{ 10, s_11_19, 18, 12, 0}, -{ 7, s_11_20, 17, 12, 0}, -{ 7, s_11_21, -1, 12, 0}, -{ 9, s_11_22, 21, 12, 0}, -{ 10, s_11_23, 21, 12, 0}, -{ 9, s_11_24, 21, 12, 0}, -{ 10, s_11_25, 21, 12, 0}, -{ 7, s_11_26, -1, 12, 0}, -{ 9, s_11_27, 26, 12, 0}, -{ 7, s_11_28, -1, 12, 0}, -{ 6, s_11_29, -1, 12, 0}, -{ 7, s_11_30, -1, 12, 0}, -{ 8, s_11_31, -1, 12, 0}, -{ 4, s_11_32, -1, 1, 0}, -{ 5, s_11_33, 32, 1, 0}, -{ 6, s_11_34, 32, 1, 0}, -{ 5, s_11_35, 32, 1, 0}, -{ 6, s_11_36, 32, 1, 0}, -{ 4, s_11_37, -1, 4, 0}, -{ 4, s_11_38, -1, 4, 0}, -{ 8, s_11_39, 38, 4, 0}, -{ 5, s_11_40, -1, 4, 0}, -{ 7, s_11_41, 40, 4, 0}, -{ 8, s_11_42, 40, 4, 0}, -{ 7, s_11_43, 40, 4, 0}, -{ 8, s_11_44, 40, 4, 0}, -{ 5, s_11_45, -1, 4, 0}, -{ 7, s_11_46, 45, 4, 0}, -{ 5, s_11_47, -1, 4, 0}, -{ 4, s_11_48, -1, 4, 0}, -{ 4, s_11_49, -1, 4, 0}, -{ 5, s_11_50, -1, 4, 0}, -{ 5, s_11_51, -1, 4, 0}, -{ 6, s_11_52, -1, 4, 0}, -{ 5, s_11_53, -1, 8, 0}, -{ 8, s_11_54, 53, 8, 0}, -{ 5, s_11_55, -1, 8, 0}, -{ 5, s_11_56, -1, 8, 0}, -{ 6, s_11_57, 56, 8, 0}, -{ 6, s_11_58, -1, 8, 0}, -{ 8, s_11_59, 58, 8, 0}, -{ 9, s_11_60, 58, 8, 0}, -{ 8, s_11_61, 58, 8, 0}, -{ 9, s_11_62, 58, 8, 0}, -{ 6, s_11_63, -1, 8, 0}, -{ 8, s_11_64, 63, 8, 0}, -{ 6, s_11_65, -1, 8, 0}, -{ 5, s_11_66, -1, 8, 0}, -{ 5, s_11_67, -1, 8, 0}, -{ 6, s_11_68, -1, 8, 0}, -{ 7, s_11_69, -1, 8, 0}, -{ 4, s_11_70, -1, 16, 0}, -{ 4, s_11_71, -1, 16, 0}, -{ 5, s_11_72, -1, 16, 0}, -{ 7, s_11_73, 72, 16, 0}, -{ 8, s_11_74, 72, 16, 0}, -{ 7, s_11_75, 72, 16, 0}, -{ 8, s_11_76, 72, 16, 0}, -{ 5, s_11_77, -1, 16, 0}, -{ 4, s_11_78, -1, 16, 0}, -{ 5, s_11_79, -1, 16, 0}, -{ 6, s_11_80, -1, 16, 0}, -{ 4, s_11_81, -1, 14, 0}, -{ 4, s_11_82, -1, 14, 0}, -{ 5, s_11_83, -1, 14, 0}, -{ 7, s_11_84, 83, 14, 0}, -{ 8, s_11_85, 83, 14, 0}, -{ 7, s_11_86, 83, 14, 0}, -{ 8, s_11_87, 83, 14, 0}, -{ 5, s_11_88, -1, 14, 0}, -{ 4, s_11_89, -1, 14, 0}, -{ 5, s_11_90, -1, 14, 0}, -{ 6, s_11_91, -1, 14, 0}, -{ 4, s_11_92, -1, 7, 0}, -{ 4, s_11_93, -1, 7, 0}, -{ 5, s_11_94, 93, 7, 0}, -{ 5, s_11_95, -1, 7, 0}, -{ 7, s_11_96, 95, 7, 0}, -{ 8, s_11_97, 95, 7, 0}, -{ 7, s_11_98, 95, 7, 0}, -{ 8, s_11_99, 95, 7, 0}, -{ 5, s_11_100, -1, 7, 0}, -{ 7, s_11_101, 100, 7, 0}, -{ 5, s_11_102, -1, 7, 0}, -{ 4, s_11_103, -1, 7, 0}, -{ 5, s_11_104, -1, 7, 0}, -{ 6, s_11_105, -1, 7, 0}, -{ 4, s_11_106, -1, 7, 0}, -{ 7, s_11_107, 106, 7, 0}, -{ 4, s_11_108, -1, 6, 0}, -{ 5, s_11_109, 108, 6, 0}, -{ 6, s_11_110, 108, 6, 0}, -{ 5, s_11_111, 108, 6, 0}, -{ 6, s_11_112, 108, 6, 0}, -{ 6, s_11_113, -1, 5, 0}, -{ 6, s_11_114, -1, 5, 0}, -{ 10, s_11_115, 114, 5, 0}, -{ 7, s_11_116, 114, 5, 0}, -{ 7, s_11_117, -1, 5, 0}, -{ 9, s_11_118, 117, 5, 0}, -{ 10, s_11_119, 117, 5, 0}, -{ 9, s_11_120, 117, 5, 0}, -{ 10, s_11_121, 117, 5, 0}, -{ 7, s_11_122, -1, 5, 0}, -{ 9, s_11_123, 122, 5, 0}, -{ 7, s_11_124, -1, 5, 0}, -{ 6, s_11_125, -1, 5, 0}, -{ 7, s_11_126, -1, 5, 0}, -{ 8, s_11_127, -1, 5, 0}, -{ 6, s_11_128, -1, 5, 0}, -{ 9, s_11_129, 128, 5, 0}, -{ 6, s_11_130, -1, 13, 0}, -{ 9, s_11_131, 130, 13, 0}, -{ 6, s_11_132, -1, 13, 0}, -{ 6, s_11_133, -1, 13, 0}, -{ 7, s_11_134, 133, 13, 0}, -{ 7, s_11_135, -1, 13, 0}, -{ 9, s_11_136, 135, 13, 0}, -{ 10, s_11_137, 135, 13, 0}, -{ 9, s_11_138, 135, 13, 0}, -{ 10, s_11_139, 135, 13, 0}, -{ 7, s_11_140, -1, 13, 0}, -{ 9, s_11_141, 140, 13, 0}, -{ 7, s_11_142, -1, 13, 0}, -{ 6, s_11_143, -1, 13, 0}, -{ 6, s_11_144, -1, 13, 0}, -{ 7, s_11_145, -1, 13, 0}, -{ 8, s_11_146, -1, 13, 0}, -{ 5, s_11_147, -1, 18, 0}, -{ 5, s_11_148, -1, 18, 0}, -{ 6, s_11_149, -1, 18, 0}, -{ 8, s_11_150, 149, 18, 0}, -{ 9, s_11_151, 149, 18, 0}, -{ 8, s_11_152, 149, 18, 0}, -{ 9, s_11_153, 149, 18, 0}, -{ 6, s_11_154, -1, 18, 0}, -{ 5, s_11_155, -1, 18, 0}, -{ 6, s_11_156, -1, 18, 0}, -{ 7, s_11_157, -1, 18, 0}, -{ 7, s_11_158, -1, 18, 0}, -{ 9, s_11_159, 158, 18, 0}, -{ 5, s_11_160, -1, 18, 0}, -{ 8, s_11_161, 160, 18, 0}, -{ 6, s_11_162, -1, 18, 0}, -{ 5, s_11_163, -1, 15, 0}, -{ 5, s_11_164, -1, 15, 0}, -{ 6, s_11_165, -1, 15, 0}, -{ 8, s_11_166, 165, 15, 0}, -{ 9, s_11_167, 165, 15, 0}, -{ 8, s_11_168, 165, 15, 0}, -{ 9, s_11_169, 165, 15, 0}, -{ 6, s_11_170, -1, 15, 0}, -{ 5, s_11_171, -1, 15, 0}, -{ 6, s_11_172, -1, 15, 0}, -{ 7, s_11_173, -1, 15, 0}, -{ 4, s_11_174, -1, 2, 0}, -{ 4, s_11_175, -1, 2, 0}, -{ 5, s_11_176, 175, 2, 0}, -{ 8, s_11_177, 176, 2, 0}, -{ 5, s_11_178, 175, 2, 0}, -{ 5, s_11_179, -1, 2, 0}, -{ 7, s_11_180, 179, 2, 0}, -{ 8, s_11_181, 179, 2, 0}, -{ 7, s_11_182, 179, 2, 0}, -{ 8, s_11_183, 179, 2, 0}, -{ 5, s_11_184, -1, 2, 0}, -{ 7, s_11_185, 184, 2, 0}, -{ 5, s_11_186, -1, 2, 0}, -{ 4, s_11_187, -1, 2, 0}, -{ 5, s_11_188, -1, 2, 0}, -{ 6, s_11_189, -1, 2, 0}, -{ 3, s_11_190, -1, 2, 0}, -{ 4, s_11_191, 190, 2, 0}, -{ 5, s_11_192, 190, 2, 0}, -{ 4, s_11_193, 190, 2, 0}, -{ 5, s_11_194, 190, 2, 0}, -{ 4, s_11_195, -1, 9, 0}, -{ 5, s_11_196, 195, 9, 0}, -{ 6, s_11_197, 195, 9, 0}, -{ 5, s_11_198, 195, 9, 0}, -{ 6, s_11_199, 195, 9, 0}, -{ 6, s_11_200, -1, 9, 0}, -{ 6, s_11_201, -1, 9, 0}, -{ 10, s_11_202, 201, 9, 0}, -{ 7, s_11_203, 201, 9, 0}, -{ 7, s_11_204, -1, 9, 0}, -{ 9, s_11_205, 204, 9, 0}, -{ 10, s_11_206, 204, 9, 0}, -{ 9, s_11_207, 204, 9, 0}, -{ 10, s_11_208, 204, 9, 0}, -{ 7, s_11_209, -1, 9, 0}, -{ 9, s_11_210, 209, 9, 0}, -{ 7, s_11_211, -1, 9, 0}, -{ 6, s_11_212, -1, 9, 0}, -{ 7, s_11_213, -1, 9, 0}, -{ 8, s_11_214, -1, 9, 0}, -{ 6, s_11_215, -1, 9, 0}, -{ 9, s_11_216, 215, 9, 0}, -{ 4, s_11_217, -1, 17, 0}, -{ 4, s_11_218, -1, 17, 0}, -{ 5, s_11_219, -1, 17, 0}, -{ 7, s_11_220, 219, 17, 0}, -{ 8, s_11_221, 219, 17, 0}, -{ 7, s_11_222, 219, 17, 0}, -{ 8, s_11_223, 219, 17, 0}, -{ 5, s_11_224, -1, 17, 0}, -{ 4, s_11_225, -1, 17, 0}, -{ 5, s_11_226, -1, 17, 0}, -{ 6, s_11_227, -1, 17, 0}, -{ 6, s_11_228, -1, 17, 0}, -{ 8, s_11_229, 228, 17, 0}, -{ 4, s_11_230, -1, 17, 0}, -{ 7, s_11_231, 230, 17, 0}, -{ 5, s_11_232, -1, 17, 0}, -{ 4, s_11_233, -1, 10, 0}, -{ 4, s_11_234, -1, 10, 0}, -{ 5, s_11_235, 234, 10, 0}, -{ 5, s_11_236, -1, 10, 0}, -{ 7, s_11_237, 236, 10, 0}, -{ 8, s_11_238, 236, 10, 0}, -{ 7, s_11_239, 236, 10, 0}, -{ 8, s_11_240, 236, 10, 0}, -{ 5, s_11_241, -1, 10, 0}, -{ 7, s_11_242, 241, 10, 0}, -{ 5, s_11_243, -1, 10, 0}, -{ 4, s_11_244, -1, 10, 0}, -{ 5, s_11_245, -1, 10, 0}, -{ 6, s_11_246, -1, 10, 0}, -{ 4, s_11_247, -1, 10, 0}, -{ 7, s_11_248, 247, 10, 0}, -{ 4, s_11_249, -1, 10, 0}, -{ 5, s_11_250, 249, 10, 0}, -{ 6, s_11_251, 249, 10, 0}, -{ 5, s_11_252, 249, 10, 0}, -{ 6, s_11_253, 249, 10, 0}, -{ 4, s_11_254, -1, 3, 0}, -{ 7, s_11_255, 254, 3, 0}, -{ 4, s_11_256, -1, 3, 0}, -{ 4, s_11_257, -1, 3, 0}, -{ 5, s_11_258, 257, 3, 0}, -{ 5, s_11_259, -1, 3, 0}, -{ 7, s_11_260, 259, 3, 0}, -{ 8, s_11_261, 259, 3, 0}, -{ 7, s_11_262, 259, 3, 0}, -{ 8, s_11_263, 259, 3, 0}, -{ 5, s_11_264, -1, 3, 0}, -{ 7, s_11_265, 264, 3, 0}, -{ 5, s_11_266, -1, 3, 0}, -{ 4, s_11_267, -1, 3, 0}, -{ 7, s_11_268, -1, 3, 0}, -{ 6, s_11_269, -1, 3, 0}, -{ 7, s_11_270, -1, 3, 0}, -{ 5, s_11_271, -1, 3, 0}, -{ 6, s_11_272, -1, 3, 0}, -{ 5, s_11_273, -1, 11, 0}, -{ 5, s_11_274, -1, 11, 0}, -{ 6, s_11_275, 274, 11, 0}, -{ 9, s_11_276, 275, 11, 0}, -{ 6, s_11_277, 274, 11, 0}, -{ 6, s_11_278, -1, 11, 0}, -{ 8, s_11_279, 278, 11, 0}, -{ 9, s_11_280, 278, 11, 0}, -{ 8, s_11_281, 278, 11, 0}, -{ 9, s_11_282, 278, 11, 0}, -{ 6, s_11_283, -1, 11, 0}, -{ 8, s_11_284, 283, 11, 0}, -{ 6, s_11_285, -1, 11, 0}, -{ 5, s_11_286, -1, 11, 0}, -{ 5, s_11_287, -1, 11, 0}, -{ 6, s_11_288, -1, 11, 0}, -{ 7, s_11_289, -1, 11, 0} +static const symbol s_10_0[4] = { 'j', 'o', 'o', 'b' }; +static const symbol s_10_1[4] = { 'j', 'o', 'o', 'd' }; +static const symbol s_10_2[8] = { 'j', 'o', 'o', 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_3[5] = { 'j', 'o', 'o', 'm', 'a' }; +static const symbol s_10_4[7] = { 'j', 'o', 'o', 'm', 'a', 't', 'a' }; +static const symbol s_10_5[5] = { 'j', 'o', 'o', 'm', 'e' }; +static const symbol s_10_6[4] = { 'j', 'o', 'o', 'n' }; +static const symbol s_10_7[5] = { 'j', 'o', 'o', 't', 'e' }; +static const symbol s_10_8[6] = { 'j', 'o', 'o', 'v', 'a', 'd' }; +static const symbol s_10_9[4] = { 'j', 'u', 'u', 'a' }; +static const symbol s_10_10[7] = { 'j', 'u', 'u', 'a', 'k', 's', 'e' }; +static const symbol s_10_11[4] = { 'j', 0xC3, 0xA4, 'i' }; +static const symbol s_10_12[5] = { 'j', 0xC3, 0xA4, 'i', 'd' }; +static const symbol s_10_13[6] = { 'j', 0xC3, 0xA4, 'i', 'm', 'e' }; +static const symbol s_10_14[5] = { 'j', 0xC3, 0xA4, 'i', 'n' }; +static const symbol s_10_15[6] = { 'j', 0xC3, 0xA4, 'i', 't', 'e' }; +static const symbol s_10_16[6] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'b' }; +static const symbol s_10_17[6] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd' }; +static const symbol s_10_18[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd', 'a' }; +static const symbol s_10_19[10] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_20[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'd', 'i' }; +static const symbol s_10_21[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's' }; +static const symbol s_10_22[9] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 'd' }; +static const symbol s_10_23[10] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_24[9] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 'n' }; +static const symbol s_10_25[10] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_26[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'a' }; +static const symbol s_10_27[9] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'a', 't', 'a' }; +static const symbol s_10_28[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'e' }; +static const symbol s_10_29[6] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'n' }; +static const symbol s_10_30[7] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 't', 'e' }; +static const symbol s_10_31[8] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'v', 'a', 'd' }; +static const symbol s_10_32[4] = { 'j', 0xC3, 0xB5, 'i' }; +static const symbol s_10_33[5] = { 'j', 0xC3, 0xB5, 'i', 'd' }; +static const symbol s_10_34[6] = { 'j', 0xC3, 0xB5, 'i', 'm', 'e' }; +static const symbol s_10_35[5] = { 'j', 0xC3, 0xB5, 'i', 'n' }; +static const symbol s_10_36[6] = { 'j', 0xC3, 0xB5, 'i', 't', 'e' }; +static const symbol s_10_37[4] = { 'k', 'e', 'e', 'b' }; +static const symbol s_10_38[4] = { 'k', 'e', 'e', 'd' }; +static const symbol s_10_39[8] = { 'k', 'e', 'e', 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_40[5] = { 'k', 'e', 'e', 'k', 's' }; +static const symbol s_10_41[7] = { 'k', 'e', 'e', 'k', 's', 'i', 'd' }; +static const symbol s_10_42[8] = { 'k', 'e', 'e', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_43[7] = { 'k', 'e', 'e', 'k', 's', 'i', 'n' }; +static const symbol s_10_44[8] = { 'k', 'e', 'e', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_45[5] = { 'k', 'e', 'e', 'm', 'a' }; +static const symbol s_10_46[7] = { 'k', 'e', 'e', 'm', 'a', 't', 'a' }; +static const symbol s_10_47[5] = { 'k', 'e', 'e', 'm', 'e' }; +static const symbol s_10_48[4] = { 'k', 'e', 'e', 'n' }; +static const symbol s_10_49[4] = { 'k', 'e', 'e', 's' }; +static const symbol s_10_50[5] = { 'k', 'e', 'e', 't', 'a' }; +static const symbol s_10_51[5] = { 'k', 'e', 'e', 't', 'e' }; +static const symbol s_10_52[6] = { 'k', 'e', 'e', 'v', 'a', 'd' }; +static const symbol s_10_53[5] = { 'k', 0xC3, 0xA4, 'i', 'a' }; +static const symbol s_10_54[8] = { 'k', 0xC3, 0xA4, 'i', 'a', 'k', 's', 'e' }; +static const symbol s_10_55[5] = { 'k', 0xC3, 0xA4, 'i', 'b' }; +static const symbol s_10_56[5] = { 'k', 0xC3, 0xA4, 'i', 'd' }; +static const symbol s_10_57[6] = { 'k', 0xC3, 0xA4, 'i', 'd', 'i' }; +static const symbol s_10_58[6] = { 'k', 0xC3, 0xA4, 'i', 'k', 's' }; +static const symbol s_10_59[8] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 'd' }; +static const symbol s_10_60[9] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_61[8] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 'n' }; +static const symbol s_10_62[9] = { 'k', 0xC3, 0xA4, 'i', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_63[6] = { 'k', 0xC3, 0xA4, 'i', 'm', 'a' }; +static const symbol s_10_64[8] = { 'k', 0xC3, 0xA4, 'i', 'm', 'a', 't', 'a' }; +static const symbol s_10_65[6] = { 'k', 0xC3, 0xA4, 'i', 'm', 'e' }; +static const symbol s_10_66[5] = { 'k', 0xC3, 0xA4, 'i', 'n' }; +static const symbol s_10_67[5] = { 'k', 0xC3, 0xA4, 'i', 's' }; +static const symbol s_10_68[6] = { 'k', 0xC3, 0xA4, 'i', 't', 'e' }; +static const symbol s_10_69[7] = { 'k', 0xC3, 0xA4, 'i', 'v', 'a', 'd' }; +static const symbol s_10_70[4] = { 'l', 'a', 'o', 'b' }; +static const symbol s_10_71[4] = { 'l', 'a', 'o', 'd' }; +static const symbol s_10_72[5] = { 'l', 'a', 'o', 'k', 's' }; +static const symbol s_10_73[7] = { 'l', 'a', 'o', 'k', 's', 'i', 'd' }; +static const symbol s_10_74[8] = { 'l', 'a', 'o', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_75[7] = { 'l', 'a', 'o', 'k', 's', 'i', 'n' }; +static const symbol s_10_76[8] = { 'l', 'a', 'o', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_77[5] = { 'l', 'a', 'o', 'm', 'e' }; +static const symbol s_10_78[4] = { 'l', 'a', 'o', 'n' }; +static const symbol s_10_79[5] = { 'l', 'a', 'o', 't', 'e' }; +static const symbol s_10_80[6] = { 'l', 'a', 'o', 'v', 'a', 'd' }; +static const symbol s_10_81[4] = { 'l', 'o', 'e', 'b' }; +static const symbol s_10_82[4] = { 'l', 'o', 'e', 'd' }; +static const symbol s_10_83[5] = { 'l', 'o', 'e', 'k', 's' }; +static const symbol s_10_84[7] = { 'l', 'o', 'e', 'k', 's', 'i', 'd' }; +static const symbol s_10_85[8] = { 'l', 'o', 'e', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_86[7] = { 'l', 'o', 'e', 'k', 's', 'i', 'n' }; +static const symbol s_10_87[8] = { 'l', 'o', 'e', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_88[5] = { 'l', 'o', 'e', 'm', 'e' }; +static const symbol s_10_89[4] = { 'l', 'o', 'e', 'n' }; +static const symbol s_10_90[5] = { 'l', 'o', 'e', 't', 'e' }; +static const symbol s_10_91[6] = { 'l', 'o', 'e', 'v', 'a', 'd' }; +static const symbol s_10_92[4] = { 'l', 'o', 'o', 'b' }; +static const symbol s_10_93[4] = { 'l', 'o', 'o', 'd' }; +static const symbol s_10_94[5] = { 'l', 'o', 'o', 'd', 'i' }; +static const symbol s_10_95[5] = { 'l', 'o', 'o', 'k', 's' }; +static const symbol s_10_96[7] = { 'l', 'o', 'o', 'k', 's', 'i', 'd' }; +static const symbol s_10_97[8] = { 'l', 'o', 'o', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_98[7] = { 'l', 'o', 'o', 'k', 's', 'i', 'n' }; +static const symbol s_10_99[8] = { 'l', 'o', 'o', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_100[5] = { 'l', 'o', 'o', 'm', 'a' }; +static const symbol s_10_101[7] = { 'l', 'o', 'o', 'm', 'a', 't', 'a' }; +static const symbol s_10_102[5] = { 'l', 'o', 'o', 'm', 'e' }; +static const symbol s_10_103[4] = { 'l', 'o', 'o', 'n' }; +static const symbol s_10_104[5] = { 'l', 'o', 'o', 't', 'e' }; +static const symbol s_10_105[6] = { 'l', 'o', 'o', 'v', 'a', 'd' }; +static const symbol s_10_106[4] = { 'l', 'u', 'u', 'a' }; +static const symbol s_10_107[7] = { 'l', 'u', 'u', 'a', 'k', 's', 'e' }; +static const symbol s_10_108[4] = { 'l', 0xC3, 0xB5, 'i' }; +static const symbol s_10_109[5] = { 'l', 0xC3, 0xB5, 'i', 'd' }; +static const symbol s_10_110[6] = { 'l', 0xC3, 0xB5, 'i', 'm', 'e' }; +static const symbol s_10_111[5] = { 'l', 0xC3, 0xB5, 'i', 'n' }; +static const symbol s_10_112[6] = { 'l', 0xC3, 0xB5, 'i', 't', 'e' }; +static const symbol s_10_113[6] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'b' }; +static const symbol s_10_114[6] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'd' }; +static const symbol s_10_115[10] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_116[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'i' }; +static const symbol s_10_117[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's' }; +static const symbol s_10_118[9] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'd' }; +static const symbol s_10_119[10] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_120[9] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'n' }; +static const symbol s_10_121[10] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_122[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a' }; +static const symbol s_10_123[9] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a', 't', 'a' }; +static const symbol s_10_124[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'e' }; +static const symbol s_10_125[6] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'n' }; +static const symbol s_10_126[7] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 't', 'e' }; +static const symbol s_10_127[8] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6, 'v', 'a', 'd' }; +static const symbol s_10_128[6] = { 'l', 0xC3, 0xBC, 0xC3, 0xBC, 'a' }; +static const symbol s_10_129[9] = { 'l', 0xC3, 0xBC, 0xC3, 0xBC, 'a', 'k', 's', 'e' }; +static const symbol s_10_130[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'a' }; +static const symbol s_10_131[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'a', 'k', 's', 'e' }; +static const symbol s_10_132[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'b' }; +static const symbol s_10_133[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'd' }; +static const symbol s_10_134[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'd', 'i' }; +static const symbol s_10_135[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's' }; +static const symbol s_10_136[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 'd' }; +static const symbol s_10_137[10] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_138[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 'n' }; +static const symbol s_10_139[10] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_140[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'm', 'a' }; +static const symbol s_10_141[9] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'm', 'a', 't', 'a' }; +static const symbol s_10_142[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'm', 'e' }; +static const symbol s_10_143[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'n' }; +static const symbol s_10_144[6] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 's' }; +static const symbol s_10_145[7] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 't', 'e' }; +static const symbol s_10_146[8] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 'v', 'a', 'd' }; +static const symbol s_10_147[5] = { 'n', 0xC3, 0xA4, 'e', 'b' }; +static const symbol s_10_148[5] = { 'n', 0xC3, 0xA4, 'e', 'd' }; +static const symbol s_10_149[6] = { 'n', 0xC3, 0xA4, 'e', 'k', 's' }; +static const symbol s_10_150[8] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 'd' }; +static const symbol s_10_151[9] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_152[8] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 'n' }; +static const symbol s_10_153[9] = { 'n', 0xC3, 0xA4, 'e', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_154[6] = { 'n', 0xC3, 0xA4, 'e', 'm', 'e' }; +static const symbol s_10_155[5] = { 'n', 0xC3, 0xA4, 'e', 'n' }; +static const symbol s_10_156[6] = { 'n', 0xC3, 0xA4, 'e', 't', 'e' }; +static const symbol s_10_157[7] = { 'n', 0xC3, 0xA4, 'e', 'v', 'a', 'd' }; +static const symbol s_10_158[7] = { 'n', 0xC3, 0xA4, 'g', 'e', 'm', 'a' }; +static const symbol s_10_159[9] = { 'n', 0xC3, 0xA4, 'g', 'e', 'm', 'a', 't', 'a' }; +static const symbol s_10_160[5] = { 'n', 0xC3, 0xA4, 'h', 'a' }; +static const symbol s_10_161[8] = { 'n', 0xC3, 0xA4, 'h', 'a', 'k', 's', 'e' }; +static const symbol s_10_162[6] = { 'n', 0xC3, 0xA4, 'h', 't', 'i' }; +static const symbol s_10_163[5] = { 'p', 0xC3, 0xB5, 'e', 'b' }; +static const symbol s_10_164[5] = { 'p', 0xC3, 0xB5, 'e', 'd' }; +static const symbol s_10_165[6] = { 'p', 0xC3, 0xB5, 'e', 'k', 's' }; +static const symbol s_10_166[8] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 'd' }; +static const symbol s_10_167[9] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_168[8] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 'n' }; +static const symbol s_10_169[9] = { 'p', 0xC3, 0xB5, 'e', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_170[6] = { 'p', 0xC3, 0xB5, 'e', 'm', 'e' }; +static const symbol s_10_171[5] = { 'p', 0xC3, 0xB5, 'e', 'n' }; +static const symbol s_10_172[6] = { 'p', 0xC3, 0xB5, 'e', 't', 'e' }; +static const symbol s_10_173[7] = { 'p', 0xC3, 0xB5, 'e', 'v', 'a', 'd' }; +static const symbol s_10_174[4] = { 's', 'a', 'a', 'b' }; +static const symbol s_10_175[4] = { 's', 'a', 'a', 'd' }; +static const symbol s_10_176[5] = { 's', 'a', 'a', 'd', 'a' }; +static const symbol s_10_177[8] = { 's', 'a', 'a', 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_178[5] = { 's', 'a', 'a', 'd', 'i' }; +static const symbol s_10_179[5] = { 's', 'a', 'a', 'k', 's' }; +static const symbol s_10_180[7] = { 's', 'a', 'a', 'k', 's', 'i', 'd' }; +static const symbol s_10_181[8] = { 's', 'a', 'a', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_182[7] = { 's', 'a', 'a', 'k', 's', 'i', 'n' }; +static const symbol s_10_183[8] = { 's', 'a', 'a', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_184[5] = { 's', 'a', 'a', 'm', 'a' }; +static const symbol s_10_185[7] = { 's', 'a', 'a', 'm', 'a', 't', 'a' }; +static const symbol s_10_186[5] = { 's', 'a', 'a', 'm', 'e' }; +static const symbol s_10_187[4] = { 's', 'a', 'a', 'n' }; +static const symbol s_10_188[5] = { 's', 'a', 'a', 't', 'e' }; +static const symbol s_10_189[6] = { 's', 'a', 'a', 'v', 'a', 'd' }; +static const symbol s_10_190[3] = { 's', 'a', 'i' }; +static const symbol s_10_191[4] = { 's', 'a', 'i', 'd' }; +static const symbol s_10_192[5] = { 's', 'a', 'i', 'm', 'e' }; +static const symbol s_10_193[4] = { 's', 'a', 'i', 'n' }; +static const symbol s_10_194[5] = { 's', 'a', 'i', 't', 'e' }; +static const symbol s_10_195[4] = { 's', 0xC3, 0xB5, 'i' }; +static const symbol s_10_196[5] = { 's', 0xC3, 0xB5, 'i', 'd' }; +static const symbol s_10_197[6] = { 's', 0xC3, 0xB5, 'i', 'm', 'e' }; +static const symbol s_10_198[5] = { 's', 0xC3, 0xB5, 'i', 'n' }; +static const symbol s_10_199[6] = { 's', 0xC3, 0xB5, 'i', 't', 'e' }; +static const symbol s_10_200[6] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'b' }; +static const symbol s_10_201[6] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'd' }; +static const symbol s_10_202[10] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_203[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'd', 'i' }; +static const symbol s_10_204[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's' }; +static const symbol s_10_205[9] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'd' }; +static const symbol s_10_206[10] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_207[9] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 'n' }; +static const symbol s_10_208[10] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_209[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a' }; +static const symbol s_10_210[9] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'a', 't', 'a' }; +static const symbol s_10_211[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'm', 'e' }; +static const symbol s_10_212[6] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'n' }; +static const symbol s_10_213[7] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 't', 'e' }; +static const symbol s_10_214[8] = { 's', 0xC3, 0xB6, 0xC3, 0xB6, 'v', 'a', 'd' }; +static const symbol s_10_215[6] = { 's', 0xC3, 0xBC, 0xC3, 0xBC, 'a' }; +static const symbol s_10_216[9] = { 's', 0xC3, 0xBC, 0xC3, 0xBC, 'a', 'k', 's', 'e' }; +static const symbol s_10_217[4] = { 't', 'e', 'e', 'b' }; +static const symbol s_10_218[4] = { 't', 'e', 'e', 'd' }; +static const symbol s_10_219[5] = { 't', 'e', 'e', 'k', 's' }; +static const symbol s_10_220[7] = { 't', 'e', 'e', 'k', 's', 'i', 'd' }; +static const symbol s_10_221[8] = { 't', 'e', 'e', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_222[7] = { 't', 'e', 'e', 'k', 's', 'i', 'n' }; +static const symbol s_10_223[8] = { 't', 'e', 'e', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_224[5] = { 't', 'e', 'e', 'm', 'e' }; +static const symbol s_10_225[4] = { 't', 'e', 'e', 'n' }; +static const symbol s_10_226[5] = { 't', 'e', 'e', 't', 'e' }; +static const symbol s_10_227[6] = { 't', 'e', 'e', 'v', 'a', 'd' }; +static const symbol s_10_228[6] = { 't', 'e', 'g', 'e', 'm', 'a' }; +static const symbol s_10_229[8] = { 't', 'e', 'g', 'e', 'm', 'a', 't', 'a' }; +static const symbol s_10_230[4] = { 't', 'e', 'h', 'a' }; +static const symbol s_10_231[7] = { 't', 'e', 'h', 'a', 'k', 's', 'e' }; +static const symbol s_10_232[5] = { 't', 'e', 'h', 't', 'i' }; +static const symbol s_10_233[4] = { 't', 'o', 'o', 'b' }; +static const symbol s_10_234[4] = { 't', 'o', 'o', 'd' }; +static const symbol s_10_235[5] = { 't', 'o', 'o', 'd', 'i' }; +static const symbol s_10_236[5] = { 't', 'o', 'o', 'k', 's' }; +static const symbol s_10_237[7] = { 't', 'o', 'o', 'k', 's', 'i', 'd' }; +static const symbol s_10_238[8] = { 't', 'o', 'o', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_239[7] = { 't', 'o', 'o', 'k', 's', 'i', 'n' }; +static const symbol s_10_240[8] = { 't', 'o', 'o', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_241[5] = { 't', 'o', 'o', 'm', 'a' }; +static const symbol s_10_242[7] = { 't', 'o', 'o', 'm', 'a', 't', 'a' }; +static const symbol s_10_243[5] = { 't', 'o', 'o', 'm', 'e' }; +static const symbol s_10_244[4] = { 't', 'o', 'o', 'n' }; +static const symbol s_10_245[5] = { 't', 'o', 'o', 't', 'e' }; +static const symbol s_10_246[6] = { 't', 'o', 'o', 'v', 'a', 'd' }; +static const symbol s_10_247[4] = { 't', 'u', 'u', 'a' }; +static const symbol s_10_248[7] = { 't', 'u', 'u', 'a', 'k', 's', 'e' }; +static const symbol s_10_249[4] = { 't', 0xC3, 0xB5, 'i' }; +static const symbol s_10_250[5] = { 't', 0xC3, 0xB5, 'i', 'd' }; +static const symbol s_10_251[6] = { 't', 0xC3, 0xB5, 'i', 'm', 'e' }; +static const symbol s_10_252[5] = { 't', 0xC3, 0xB5, 'i', 'n' }; +static const symbol s_10_253[6] = { 't', 0xC3, 0xB5, 'i', 't', 'e' }; +static const symbol s_10_254[4] = { 'v', 'i', 'i', 'a' }; +static const symbol s_10_255[7] = { 'v', 'i', 'i', 'a', 'k', 's', 'e' }; +static const symbol s_10_256[4] = { 'v', 'i', 'i', 'b' }; +static const symbol s_10_257[4] = { 'v', 'i', 'i', 'd' }; +static const symbol s_10_258[5] = { 'v', 'i', 'i', 'd', 'i' }; +static const symbol s_10_259[5] = { 'v', 'i', 'i', 'k', 's' }; +static const symbol s_10_260[7] = { 'v', 'i', 'i', 'k', 's', 'i', 'd' }; +static const symbol s_10_261[8] = { 'v', 'i', 'i', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_262[7] = { 'v', 'i', 'i', 'k', 's', 'i', 'n' }; +static const symbol s_10_263[8] = { 'v', 'i', 'i', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_264[5] = { 'v', 'i', 'i', 'm', 'a' }; +static const symbol s_10_265[7] = { 'v', 'i', 'i', 'm', 'a', 't', 'a' }; +static const symbol s_10_266[5] = { 'v', 'i', 'i', 'm', 'e' }; +static const symbol s_10_267[4] = { 'v', 'i', 'i', 'n' }; +static const symbol s_10_268[7] = { 'v', 'i', 'i', 's', 'i', 'm', 'e' }; +static const symbol s_10_269[6] = { 'v', 'i', 'i', 's', 'i', 'n' }; +static const symbol s_10_270[7] = { 'v', 'i', 'i', 's', 'i', 't', 'e' }; +static const symbol s_10_271[5] = { 'v', 'i', 'i', 't', 'e' }; +static const symbol s_10_272[6] = { 'v', 'i', 'i', 'v', 'a', 'd' }; +static const symbol s_10_273[5] = { 'v', 0xC3, 0xB5, 'i', 'b' }; +static const symbol s_10_274[5] = { 'v', 0xC3, 0xB5, 'i', 'd' }; +static const symbol s_10_275[6] = { 'v', 0xC3, 0xB5, 'i', 'd', 'a' }; +static const symbol s_10_276[9] = { 'v', 0xC3, 0xB5, 'i', 'd', 'a', 'k', 's', 'e' }; +static const symbol s_10_277[6] = { 'v', 0xC3, 0xB5, 'i', 'd', 'i' }; +static const symbol s_10_278[6] = { 'v', 0xC3, 0xB5, 'i', 'k', 's' }; +static const symbol s_10_279[8] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 'd' }; +static const symbol s_10_280[9] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 'm', 'e' }; +static const symbol s_10_281[8] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 'n' }; +static const symbol s_10_282[9] = { 'v', 0xC3, 0xB5, 'i', 'k', 's', 'i', 't', 'e' }; +static const symbol s_10_283[6] = { 'v', 0xC3, 0xB5, 'i', 'm', 'a' }; +static const symbol s_10_284[8] = { 'v', 0xC3, 0xB5, 'i', 'm', 'a', 't', 'a' }; +static const symbol s_10_285[6] = { 'v', 0xC3, 0xB5, 'i', 'm', 'e' }; +static const symbol s_10_286[5] = { 'v', 0xC3, 0xB5, 'i', 'n' }; +static const symbol s_10_287[5] = { 'v', 0xC3, 0xB5, 'i', 's' }; +static const symbol s_10_288[6] = { 'v', 0xC3, 0xB5, 'i', 't', 'e' }; +static const symbol s_10_289[7] = { 'v', 0xC3, 0xB5, 'i', 'v', 'a', 'd' }; +static const struct among a_10[290] = { +{ 4, s_10_0, 0, 1, 0}, +{ 4, s_10_1, 0, 1, 0}, +{ 8, s_10_2, -1, 1, 0}, +{ 5, s_10_3, 0, 1, 0}, +{ 7, s_10_4, -1, 1, 0}, +{ 5, s_10_5, 0, 1, 0}, +{ 4, s_10_6, 0, 1, 0}, +{ 5, s_10_7, 0, 1, 0}, +{ 6, s_10_8, 0, 1, 0}, +{ 4, s_10_9, 0, 1, 0}, +{ 7, s_10_10, -1, 1, 0}, +{ 4, s_10_11, 0, 12, 0}, +{ 5, s_10_12, -1, 12, 0}, +{ 6, s_10_13, -2, 12, 0}, +{ 5, s_10_14, -3, 12, 0}, +{ 6, s_10_15, -4, 12, 0}, +{ 6, s_10_16, 0, 12, 0}, +{ 6, s_10_17, 0, 12, 0}, +{ 7, s_10_18, -1, 12, 0}, +{ 10, s_10_19, -1, 12, 0}, +{ 7, s_10_20, -3, 12, 0}, +{ 7, s_10_21, 0, 12, 0}, +{ 9, s_10_22, -1, 12, 0}, +{ 10, s_10_23, -2, 12, 0}, +{ 9, s_10_24, -3, 12, 0}, +{ 10, s_10_25, -4, 12, 0}, +{ 7, s_10_26, 0, 12, 0}, +{ 9, s_10_27, -1, 12, 0}, +{ 7, s_10_28, 0, 12, 0}, +{ 6, s_10_29, 0, 12, 0}, +{ 7, s_10_30, 0, 12, 0}, +{ 8, s_10_31, 0, 12, 0}, +{ 4, s_10_32, 0, 1, 0}, +{ 5, s_10_33, -1, 1, 0}, +{ 6, s_10_34, -2, 1, 0}, +{ 5, s_10_35, -3, 1, 0}, +{ 6, s_10_36, -4, 1, 0}, +{ 4, s_10_37, 0, 4, 0}, +{ 4, s_10_38, 0, 4, 0}, +{ 8, s_10_39, -1, 4, 0}, +{ 5, s_10_40, 0, 4, 0}, +{ 7, s_10_41, -1, 4, 0}, +{ 8, s_10_42, -2, 4, 0}, +{ 7, s_10_43, -3, 4, 0}, +{ 8, s_10_44, -4, 4, 0}, +{ 5, s_10_45, 0, 4, 0}, +{ 7, s_10_46, -1, 4, 0}, +{ 5, s_10_47, 0, 4, 0}, +{ 4, s_10_48, 0, 4, 0}, +{ 4, s_10_49, 0, 4, 0}, +{ 5, s_10_50, 0, 4, 0}, +{ 5, s_10_51, 0, 4, 0}, +{ 6, s_10_52, 0, 4, 0}, +{ 5, s_10_53, 0, 8, 0}, +{ 8, s_10_54, -1, 8, 0}, +{ 5, s_10_55, 0, 8, 0}, +{ 5, s_10_56, 0, 8, 0}, +{ 6, s_10_57, -1, 8, 0}, +{ 6, s_10_58, 0, 8, 0}, +{ 8, s_10_59, -1, 8, 0}, +{ 9, s_10_60, -2, 8, 0}, +{ 8, s_10_61, -3, 8, 0}, +{ 9, s_10_62, -4, 8, 0}, +{ 6, s_10_63, 0, 8, 0}, +{ 8, s_10_64, -1, 8, 0}, +{ 6, s_10_65, 0, 8, 0}, +{ 5, s_10_66, 0, 8, 0}, +{ 5, s_10_67, 0, 8, 0}, +{ 6, s_10_68, 0, 8, 0}, +{ 7, s_10_69, 0, 8, 0}, +{ 4, s_10_70, 0, 16, 0}, +{ 4, s_10_71, 0, 16, 0}, +{ 5, s_10_72, 0, 16, 0}, +{ 7, s_10_73, -1, 16, 0}, +{ 8, s_10_74, -2, 16, 0}, +{ 7, s_10_75, -3, 16, 0}, +{ 8, s_10_76, -4, 16, 0}, +{ 5, s_10_77, 0, 16, 0}, +{ 4, s_10_78, 0, 16, 0}, +{ 5, s_10_79, 0, 16, 0}, +{ 6, s_10_80, 0, 16, 0}, +{ 4, s_10_81, 0, 14, 0}, +{ 4, s_10_82, 0, 14, 0}, +{ 5, s_10_83, 0, 14, 0}, +{ 7, s_10_84, -1, 14, 0}, +{ 8, s_10_85, -2, 14, 0}, +{ 7, s_10_86, -3, 14, 0}, +{ 8, s_10_87, -4, 14, 0}, +{ 5, s_10_88, 0, 14, 0}, +{ 4, s_10_89, 0, 14, 0}, +{ 5, s_10_90, 0, 14, 0}, +{ 6, s_10_91, 0, 14, 0}, +{ 4, s_10_92, 0, 7, 0}, +{ 4, s_10_93, 0, 7, 0}, +{ 5, s_10_94, -1, 7, 0}, +{ 5, s_10_95, 0, 7, 0}, +{ 7, s_10_96, -1, 7, 0}, +{ 8, s_10_97, -2, 7, 0}, +{ 7, s_10_98, -3, 7, 0}, +{ 8, s_10_99, -4, 7, 0}, +{ 5, s_10_100, 0, 7, 0}, +{ 7, s_10_101, -1, 7, 0}, +{ 5, s_10_102, 0, 7, 0}, +{ 4, s_10_103, 0, 7, 0}, +{ 5, s_10_104, 0, 7, 0}, +{ 6, s_10_105, 0, 7, 0}, +{ 4, s_10_106, 0, 7, 0}, +{ 7, s_10_107, -1, 7, 0}, +{ 4, s_10_108, 0, 6, 0}, +{ 5, s_10_109, -1, 6, 0}, +{ 6, s_10_110, -2, 6, 0}, +{ 5, s_10_111, -3, 6, 0}, +{ 6, s_10_112, -4, 6, 0}, +{ 6, s_10_113, 0, 5, 0}, +{ 6, s_10_114, 0, 5, 0}, +{ 10, s_10_115, -1, 5, 0}, +{ 7, s_10_116, -2, 5, 0}, +{ 7, s_10_117, 0, 5, 0}, +{ 9, s_10_118, -1, 5, 0}, +{ 10, s_10_119, -2, 5, 0}, +{ 9, s_10_120, -3, 5, 0}, +{ 10, s_10_121, -4, 5, 0}, +{ 7, s_10_122, 0, 5, 0}, +{ 9, s_10_123, -1, 5, 0}, +{ 7, s_10_124, 0, 5, 0}, +{ 6, s_10_125, 0, 5, 0}, +{ 7, s_10_126, 0, 5, 0}, +{ 8, s_10_127, 0, 5, 0}, +{ 6, s_10_128, 0, 5, 0}, +{ 9, s_10_129, -1, 5, 0}, +{ 6, s_10_130, 0, 13, 0}, +{ 9, s_10_131, -1, 13, 0}, +{ 6, s_10_132, 0, 13, 0}, +{ 6, s_10_133, 0, 13, 0}, +{ 7, s_10_134, -1, 13, 0}, +{ 7, s_10_135, 0, 13, 0}, +{ 9, s_10_136, -1, 13, 0}, +{ 10, s_10_137, -2, 13, 0}, +{ 9, s_10_138, -3, 13, 0}, +{ 10, s_10_139, -4, 13, 0}, +{ 7, s_10_140, 0, 13, 0}, +{ 9, s_10_141, -1, 13, 0}, +{ 7, s_10_142, 0, 13, 0}, +{ 6, s_10_143, 0, 13, 0}, +{ 6, s_10_144, 0, 13, 0}, +{ 7, s_10_145, 0, 13, 0}, +{ 8, s_10_146, 0, 13, 0}, +{ 5, s_10_147, 0, 18, 0}, +{ 5, s_10_148, 0, 18, 0}, +{ 6, s_10_149, 0, 18, 0}, +{ 8, s_10_150, -1, 18, 0}, +{ 9, s_10_151, -2, 18, 0}, +{ 8, s_10_152, -3, 18, 0}, +{ 9, s_10_153, -4, 18, 0}, +{ 6, s_10_154, 0, 18, 0}, +{ 5, s_10_155, 0, 18, 0}, +{ 6, s_10_156, 0, 18, 0}, +{ 7, s_10_157, 0, 18, 0}, +{ 7, s_10_158, 0, 18, 0}, +{ 9, s_10_159, -1, 18, 0}, +{ 5, s_10_160, 0, 18, 0}, +{ 8, s_10_161, -1, 18, 0}, +{ 6, s_10_162, 0, 18, 0}, +{ 5, s_10_163, 0, 15, 0}, +{ 5, s_10_164, 0, 15, 0}, +{ 6, s_10_165, 0, 15, 0}, +{ 8, s_10_166, -1, 15, 0}, +{ 9, s_10_167, -2, 15, 0}, +{ 8, s_10_168, -3, 15, 0}, +{ 9, s_10_169, -4, 15, 0}, +{ 6, s_10_170, 0, 15, 0}, +{ 5, s_10_171, 0, 15, 0}, +{ 6, s_10_172, 0, 15, 0}, +{ 7, s_10_173, 0, 15, 0}, +{ 4, s_10_174, 0, 2, 0}, +{ 4, s_10_175, 0, 2, 0}, +{ 5, s_10_176, -1, 2, 0}, +{ 8, s_10_177, -1, 2, 0}, +{ 5, s_10_178, -3, 2, 0}, +{ 5, s_10_179, 0, 2, 0}, +{ 7, s_10_180, -1, 2, 0}, +{ 8, s_10_181, -2, 2, 0}, +{ 7, s_10_182, -3, 2, 0}, +{ 8, s_10_183, -4, 2, 0}, +{ 5, s_10_184, 0, 2, 0}, +{ 7, s_10_185, -1, 2, 0}, +{ 5, s_10_186, 0, 2, 0}, +{ 4, s_10_187, 0, 2, 0}, +{ 5, s_10_188, 0, 2, 0}, +{ 6, s_10_189, 0, 2, 0}, +{ 3, s_10_190, 0, 2, 0}, +{ 4, s_10_191, -1, 2, 0}, +{ 5, s_10_192, -2, 2, 0}, +{ 4, s_10_193, -3, 2, 0}, +{ 5, s_10_194, -4, 2, 0}, +{ 4, s_10_195, 0, 9, 0}, +{ 5, s_10_196, -1, 9, 0}, +{ 6, s_10_197, -2, 9, 0}, +{ 5, s_10_198, -3, 9, 0}, +{ 6, s_10_199, -4, 9, 0}, +{ 6, s_10_200, 0, 9, 0}, +{ 6, s_10_201, 0, 9, 0}, +{ 10, s_10_202, -1, 9, 0}, +{ 7, s_10_203, -2, 9, 0}, +{ 7, s_10_204, 0, 9, 0}, +{ 9, s_10_205, -1, 9, 0}, +{ 10, s_10_206, -2, 9, 0}, +{ 9, s_10_207, -3, 9, 0}, +{ 10, s_10_208, -4, 9, 0}, +{ 7, s_10_209, 0, 9, 0}, +{ 9, s_10_210, -1, 9, 0}, +{ 7, s_10_211, 0, 9, 0}, +{ 6, s_10_212, 0, 9, 0}, +{ 7, s_10_213, 0, 9, 0}, +{ 8, s_10_214, 0, 9, 0}, +{ 6, s_10_215, 0, 9, 0}, +{ 9, s_10_216, -1, 9, 0}, +{ 4, s_10_217, 0, 17, 0}, +{ 4, s_10_218, 0, 17, 0}, +{ 5, s_10_219, 0, 17, 0}, +{ 7, s_10_220, -1, 17, 0}, +{ 8, s_10_221, -2, 17, 0}, +{ 7, s_10_222, -3, 17, 0}, +{ 8, s_10_223, -4, 17, 0}, +{ 5, s_10_224, 0, 17, 0}, +{ 4, s_10_225, 0, 17, 0}, +{ 5, s_10_226, 0, 17, 0}, +{ 6, s_10_227, 0, 17, 0}, +{ 6, s_10_228, 0, 17, 0}, +{ 8, s_10_229, -1, 17, 0}, +{ 4, s_10_230, 0, 17, 0}, +{ 7, s_10_231, -1, 17, 0}, +{ 5, s_10_232, 0, 17, 0}, +{ 4, s_10_233, 0, 10, 0}, +{ 4, s_10_234, 0, 10, 0}, +{ 5, s_10_235, -1, 10, 0}, +{ 5, s_10_236, 0, 10, 0}, +{ 7, s_10_237, -1, 10, 0}, +{ 8, s_10_238, -2, 10, 0}, +{ 7, s_10_239, -3, 10, 0}, +{ 8, s_10_240, -4, 10, 0}, +{ 5, s_10_241, 0, 10, 0}, +{ 7, s_10_242, -1, 10, 0}, +{ 5, s_10_243, 0, 10, 0}, +{ 4, s_10_244, 0, 10, 0}, +{ 5, s_10_245, 0, 10, 0}, +{ 6, s_10_246, 0, 10, 0}, +{ 4, s_10_247, 0, 10, 0}, +{ 7, s_10_248, -1, 10, 0}, +{ 4, s_10_249, 0, 10, 0}, +{ 5, s_10_250, -1, 10, 0}, +{ 6, s_10_251, -2, 10, 0}, +{ 5, s_10_252, -3, 10, 0}, +{ 6, s_10_253, -4, 10, 0}, +{ 4, s_10_254, 0, 3, 0}, +{ 7, s_10_255, -1, 3, 0}, +{ 4, s_10_256, 0, 3, 0}, +{ 4, s_10_257, 0, 3, 0}, +{ 5, s_10_258, -1, 3, 0}, +{ 5, s_10_259, 0, 3, 0}, +{ 7, s_10_260, -1, 3, 0}, +{ 8, s_10_261, -2, 3, 0}, +{ 7, s_10_262, -3, 3, 0}, +{ 8, s_10_263, -4, 3, 0}, +{ 5, s_10_264, 0, 3, 0}, +{ 7, s_10_265, -1, 3, 0}, +{ 5, s_10_266, 0, 3, 0}, +{ 4, s_10_267, 0, 3, 0}, +{ 7, s_10_268, 0, 3, 0}, +{ 6, s_10_269, 0, 3, 0}, +{ 7, s_10_270, 0, 3, 0}, +{ 5, s_10_271, 0, 3, 0}, +{ 6, s_10_272, 0, 3, 0}, +{ 5, s_10_273, 0, 11, 0}, +{ 5, s_10_274, 0, 11, 0}, +{ 6, s_10_275, -1, 11, 0}, +{ 9, s_10_276, -1, 11, 0}, +{ 6, s_10_277, -3, 11, 0}, +{ 6, s_10_278, 0, 11, 0}, +{ 8, s_10_279, -1, 11, 0}, +{ 9, s_10_280, -2, 11, 0}, +{ 8, s_10_281, -3, 11, 0}, +{ 9, s_10_282, -4, 11, 0}, +{ 6, s_10_283, 0, 11, 0}, +{ 8, s_10_284, -1, 11, 0}, +{ 6, s_10_285, 0, 11, 0}, +{ 5, s_10_286, 0, 11, 0}, +{ 5, s_10_287, 0, 11, 0}, +{ 6, s_10_288, 0, 11, 0}, +{ 7, s_10_289, 0, 11, 0} }; static const unsigned char g_V1[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 48, 8 }; @@ -835,91 +836,71 @@ static const unsigned char g_KI[] = { 117, 66, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, static const unsigned char g_GI[] = { 21, 123, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 48, 8 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'l', 'a', 's', 'e' }; -static const symbol s_2[] = { 'm', 'i', 's', 'e' }; -static const symbol s_3[] = { 'l', 'i', 's', 'e' }; -static const symbol s_4[] = { 'i', 'k', 'u' }; -static const symbol s_5[] = { 'e' }; -static const symbol s_6[] = { 't' }; -static const symbol s_7[] = { 'k' }; -static const symbol s_8[] = { 'p' }; -static const symbol s_9[] = { 't' }; -static const symbol s_10[] = { 'j', 'o', 'o' }; -static const symbol s_11[] = { 's', 'a', 'a' }; -static const symbol s_12[] = { 'v', 'i', 'i', 'm', 'a' }; -static const symbol s_13[] = { 'k', 'e', 'e', 's', 'i' }; -static const symbol s_14[] = { 'l', 0xC3, 0xB6, 0xC3, 0xB6 }; -static const symbol s_15[] = { 'l', 0xC3, 0xB5, 'i' }; -static const symbol s_16[] = { 'l', 'o', 'o' }; -static const symbol s_17[] = { 'k', 0xC3, 0xA4, 'i', 's', 'i' }; -static const symbol s_18[] = { 's', 0xC3, 0xB6, 0xC3, 0xB6 }; -static const symbol s_19[] = { 't', 'o', 'o' }; -static const symbol s_20[] = { 'v', 0xC3, 0xB5, 'i', 's', 'i' }; -static const symbol s_21[] = { 'j', 0xC3, 0xA4, 0xC3, 0xA4, 'm', 'a' }; -static const symbol s_22[] = { 'm', 0xC3, 0xBC, 0xC3, 0xBC, 's', 'i' }; -static const symbol s_23[] = { 'l', 'u', 'g', 'e' }; -static const symbol s_24[] = { 'p', 0xC3, 0xB5, 'd', 'e' }; -static const symbol s_25[] = { 'l', 'a', 'd', 'u' }; -static const symbol s_26[] = { 't', 'e', 'g', 'i' }; -static const symbol s_27[] = { 'n', 0xC3, 0xA4, 'g', 'i' }; - static int r_mark_regions(struct SN_env * z) { - z->I[0] = z->l; - - if (out_grouping_U(z, g_V1, 97, 252, 1) < 0) return 0; - + ((SN_local *)z)->i_p1 = z->l; + { + int ret = out_grouping_U(z, g_V1, 97, 252, 1); + if (ret < 0) return 0; + z->c += ret; + } { int ret = in_grouping_U(z, g_V1, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p1 = z->c; return 1; } static int r_emphasis(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 105) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 2); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 105) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_0, 2, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int m_test2 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 4); + { + int v_2 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 4); if (ret < 0) return 0; z->c = ret; } - z->c = z->l - m_test2; + z->c = z->l - v_2; } switch (among_var) { case 1: - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; if (in_grouping_b_U(z, g_GI, 97, 252, 0)) return 0; - z->c = z->l - m3; - { int m4 = z->l - z->c; (void)m4; - { int ret = r_LONGV(z); + z->c = z->l - v_3; + { + int v_4 = z->l - z->c; + { + int ret = r_LONGV(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } return 0; lab0: - z->c = z->l - m4; + z->c = z->l - v_4; } } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b_U(z, g_KI, 98, 382, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -929,31 +910,34 @@ static int r_emphasis(struct SN_env * z) { static int r_verb(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540726 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_1, 21); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((540726 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_1, 21, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 3: if (in_grouping_b_U(z, g_V1, 97, 252, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -962,23 +946,23 @@ static int r_verb(struct SN_env * z) { } static int r_LONGV(struct SN_env * z) { - if (!find_among_b(z, a_2, 9)) return 0; - return 1; + return find_among_b(z, a_2, 9, 0) != 0; } static int r_i_plural(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 105) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_3, 1)) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] != 'i') { z->lb = v_1; return 0; } + z->c--; z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } if (in_grouping_b_U(z, g_RV, 97, 117, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -986,30 +970,33 @@ static int r_i_plural(struct SN_env * z) { static int r_special_noun_endings(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 3 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1049120 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_4, 12); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c - 3 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1049120 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_3, 12, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_1); + { + int ret = slice_from_s(z, 4, s_1); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_2); + { + int ret = slice_from_s(z, 4, s_2); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_3); + { + int ret = slice_from_s(z, 4, s_3); if (ret < 0) return ret; } break; @@ -1019,41 +1006,45 @@ static int r_special_noun_endings(struct SN_env * z) { static int r_case_ending(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1576994 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_5, 10); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1576994 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_4, 10, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; - if (in_grouping_b_U(z, g_RV, 97, 117, 0)) goto lab1; - goto lab0; - lab1: - z->c = z->l - m2; - { int ret = r_LONGV(z); + do { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_RV, 97, 117, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + { + int ret = r_LONGV(z); if (ret <= 0) return ret; } - } - lab0: + } while (0); break; case 2: - { int m_test3 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 4); + { + int v_3 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 4); if (ret < 0) return 0; z->c = ret; } - z->c = z->l - m_test3; + z->c = z->l - v_3; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1061,81 +1052,92 @@ static int r_case_ending(struct SN_env * z) { static int r_plural_three_first_cases(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 101)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_7, 7); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 101)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_6, 7, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 3, s_4); + { + int ret = slice_from_s(z, 3, s_4); if (ret < 0) return ret; } break; case 2: - { int m2 = z->l - z->c; (void)m2; - { int ret = r_LONGV(z); + { + int v_2 = z->l - z->c; + { + int ret = r_LONGV(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int m3 = z->l - z->c; (void)m3; - { int m_test4 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 4); - if (ret < 0) goto lab2; + do { + int v_3 = z->l - z->c; + { + int v_4 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 4); + if (ret < 0) goto lab1; z->c = ret; } - z->c = z->l - m_test4; + z->c = z->l - v_4; } if (z->c <= z->lb || (z->p[z->c - 1] != 115 && z->p[z->c - 1] != 116)) among_var = 2; else - among_var = find_among_b(z, a_6, 5); + among_var = find_among_b(z, a_5, 5, 0); switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = slice_from_s(z, 1, s_6); + break; + lab1: + z->c = z->l - v_3; + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } - } - lab1: + } while (0); break; case 4: - { int m5 = z->l - z->c; (void)m5; - if (in_grouping_b_U(z, g_RV, 97, 117, 0)) goto lab4; - goto lab3; - lab4: - z->c = z->l - m5; - { int ret = r_LONGV(z); + do { + int v_5 = z->l - z->c; + if (in_grouping_b_U(z, g_RV, 97, 117, 0)) goto lab2; + break; + lab2: + z->c = z->l - v_5; + { + int ret = r_LONGV(z); if (ret <= 0) return ret; } - } - lab3: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1144,17 +1146,18 @@ static int r_plural_three_first_cases(struct SN_env * z) { } static int r_nu(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 117)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_8, 4)) { z->lb = mlimit1; return 0; } + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 117)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_7, 4, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1163,25 +1166,28 @@ static int r_nu(struct SN_env * z) { static int r_undouble_kpt(struct SN_env * z) { int among_var; if (in_grouping_b_U(z, g_V1, 97, 252, 0)) return 0; - if (z->I[0] > z->c) return 0; + if (((SN_local *)z)->i_p1 > z->c) return 0; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1116160 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_9, 3); + among_var = find_among_b(z, a_8, 3, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; @@ -1191,26 +1197,28 @@ static int r_undouble_kpt(struct SN_env * z) { static int r_degrees(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((8706 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_10, 3); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((8706 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_9, 3, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: if (in_grouping_b_U(z, g_RV, 97, 117, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1219,41 +1227,53 @@ static int r_degrees(struct SN_env * z) { } static int r_substantive(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - { int ret = r_special_noun_endings(z); + { + int v_1 = z->l - z->c; + { + int ret = r_special_noun_endings(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int ret = r_case_ending(z); + { + int v_2 = z->l - z->c; + { + int ret = r_case_ending(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_plural_three_first_cases(z); + { + int v_3 = z->l - z->c; + { + int ret = r_plural_three_first_cases(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_degrees(z); + { + int v_4 = z->l - z->c; + { + int ret = r_degrees(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_i_plural(z); + { + int v_5 = z->l - z->c; + { + int ret = r_i_plural(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_nu(z); + { + int v_6 = z->l - z->c; + { + int ret = r_nu(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } return 1; } @@ -1261,98 +1281,116 @@ static int r_substantive(struct SN_env * z) { static int r_verb_exceptions(struct SN_env * z) { int among_var; z->bra = z->c; - among_var = find_among(z, a_11, 290); + among_var = find_among(z, a_10, 290, 0); if (!among_var) return 0; z->ket = z->c; if (z->c < z->l) return 0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 3, s_10); + { + int ret = slice_from_s(z, 3, s_10); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_11); + { + int ret = slice_from_s(z, 3, s_11); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 5, s_12); + { + int ret = slice_from_s(z, 5, s_12); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 5, s_13); + { + int ret = slice_from_s(z, 5, s_13); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 5, s_14); + { + int ret = slice_from_s(z, 5, s_14); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 4, s_15); + { + int ret = slice_from_s(z, 4, s_15); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_16); + { + int ret = slice_from_s(z, 3, s_16); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 6, s_17); + { + int ret = slice_from_s(z, 6, s_17); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 5, s_18); + { + int ret = slice_from_s(z, 5, s_18); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 3, s_19); + { + int ret = slice_from_s(z, 3, s_19); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 6, s_20); + { + int ret = slice_from_s(z, 6, s_20); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 7, s_21); + { + int ret = slice_from_s(z, 7, s_21); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 7, s_22); + { + int ret = slice_from_s(z, 7, s_22); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 4, s_23); + { + int ret = slice_from_s(z, 4, s_23); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 5, s_24); + { + int ret = slice_from_s(z, 5, s_24); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 4, s_25); + { + int ret = slice_from_s(z, 4, s_25); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 4, s_26); + { + int ret = slice_from_s(z, 4, s_26); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 5, s_27); + { + int ret = slice_from_s(z, 5, s_27); if (ret < 0) return ret; } break; @@ -1361,56 +1399,74 @@ static int r_verb_exceptions(struct SN_env * z) { } extern int estonian_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_verb_exceptions(z); + { + int v_1 = z->c; + { + int ret = r_verb_exceptions(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } return 0; lab0: - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - { int ret = r_mark_regions(z); + { + int v_2 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c2; + z->c = v_2; } z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_emphasis(z); + { + int v_3 = z->l - z->c; + { + int ret = r_emphasis(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_verb(z); - if (ret == 0) goto lab3; + { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + { + int ret = r_verb(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab2; - lab3: - z->c = z->l - m5; - { int ret = r_substantive(z); + break; + lab2: + z->c = z->l - v_5; + { + int ret = r_substantive(z); if (ret < 0) return ret; } - } - lab2: - z->c = z->l - m4; + } while (0); + z->c = z->l - v_4; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_undouble_kpt(z); + { + int v_6 = z->l - z->c; + { + int ret = r_undouble_kpt(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } z->c = z->lb; return 1; } -extern struct SN_env * estonian_UTF_8_create_env(void) { return SN_create_env(0, 1); } +extern struct SN_env * estonian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void estonian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void estonian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_finnish.c b/src/backend/snowball/libstemmer/stem_UTF_8_finnish.c index bd8f9520fa847..8590d30f38c4d 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_finnish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_finnish.c @@ -1,6 +1,20 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from finnish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_finnish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + unsigned char b_ending_removed; + symbol * s_x; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +23,7 @@ extern int finnish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_tidy(struct SN_env * z); static int r_other_endings(struct SN_env * z); static int r_t_plural(struct SN_env * z); @@ -20,18 +35,15 @@ static int r_possessive(struct SN_env * z); static int r_particle_etc(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * finnish_UTF_8_create_env(void); -extern void finnish_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'k', 's', 'e' }; +static const symbol s_1[] = { 'k', 's', 'i' }; +static const symbol s_2[] = { 0xC3, 0xA4 }; +static const symbol s_3[] = { 0xC3, 0xB6 }; +static const symbol s_4[] = { 'i', 'e' }; +static const symbol s_5[] = { 'p', 'o' }; +static const symbol s_6[] = { 'p', 'o' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 'p', 'a' }; static const symbol s_0_1[3] = { 's', 't', 'i' }; static const symbol s_0_2[4] = { 'k', 'a', 'a', 'n' }; @@ -42,19 +54,17 @@ static const symbol s_0_6[6] = { 'k', 0xC3, 0xA4, 0xC3, 0xA4, 'n' }; static const symbol s_0_7[2] = { 'k', 'o' }; static const symbol s_0_8[3] = { 'p', 0xC3, 0xA4 }; static const symbol s_0_9[3] = { 'k', 0xC3, 0xB6 }; - -static const struct among a_0[10] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 3, s_0_1, -1, 2, 0}, -{ 4, s_0_2, -1, 1, 0}, -{ 3, s_0_3, -1, 1, 0}, -{ 3, s_0_4, -1, 1, 0}, -{ 4, s_0_5, -1, 1, 0}, -{ 6, s_0_6, -1, 1, 0}, -{ 2, s_0_7, -1, 1, 0}, -{ 3, s_0_8, -1, 1, 0}, -{ 3, s_0_9, -1, 1, 0} +static const struct among a_0[10] = { +{ 2, s_0_0, 0, 1, 0}, +{ 3, s_0_1, 0, 2, 0}, +{ 4, s_0_2, 0, 1, 0}, +{ 3, s_0_3, 0, 1, 0}, +{ 3, s_0_4, 0, 1, 0}, +{ 4, s_0_5, 0, 1, 0}, +{ 6, s_0_6, 0, 1, 0}, +{ 2, s_0_7, 0, 1, 0}, +{ 3, s_0_8, 0, 1, 0}, +{ 3, s_0_9, 0, 1, 0} }; static const symbol s_1_0[3] = { 'l', 'l', 'a' }; @@ -63,15 +73,13 @@ static const symbol s_1_2[3] = { 's', 's', 'a' }; static const symbol s_1_3[2] = { 't', 'a' }; static const symbol s_1_4[3] = { 'l', 't', 'a' }; static const symbol s_1_5[3] = { 's', 't', 'a' }; - -static const struct among a_1[6] = -{ -{ 3, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 3, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0}, -{ 3, s_1_4, 3, -1, 0}, -{ 3, s_1_5, 3, -1, 0} +static const struct among a_1[6] = { +{ 3, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 3, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 3, s_1_4, -1, -1, 0}, +{ 3, s_1_5, -2, -1, 0} }; static const symbol s_2_0[4] = { 'l', 'l', 0xC3, 0xA4 }; @@ -80,24 +88,20 @@ static const symbol s_2_2[4] = { 's', 's', 0xC3, 0xA4 }; static const symbol s_2_3[3] = { 't', 0xC3, 0xA4 }; static const symbol s_2_4[4] = { 'l', 't', 0xC3, 0xA4 }; static const symbol s_2_5[4] = { 's', 't', 0xC3, 0xA4 }; - -static const struct among a_2[6] = -{ -{ 4, s_2_0, -1, -1, 0}, -{ 3, s_2_1, -1, -1, 0}, -{ 4, s_2_2, -1, -1, 0}, -{ 3, s_2_3, -1, -1, 0}, -{ 4, s_2_4, 3, -1, 0}, -{ 4, s_2_5, 3, -1, 0} +static const struct among a_2[6] = { +{ 4, s_2_0, 0, -1, 0}, +{ 3, s_2_1, 0, -1, 0}, +{ 4, s_2_2, 0, -1, 0}, +{ 3, s_2_3, 0, -1, 0}, +{ 4, s_2_4, -1, -1, 0}, +{ 4, s_2_5, -2, -1, 0} }; static const symbol s_3_0[3] = { 'l', 'l', 'e' }; static const symbol s_3_1[3] = { 'i', 'n', 'e' }; - -static const struct among a_3[2] = -{ -{ 3, s_3_0, -1, -1, 0}, -{ 3, s_3_1, -1, -1, 0} +static const struct among a_3[2] = { +{ 3, s_3_0, 0, -1, 0}, +{ 3, s_3_1, 0, -1, 0} }; static const symbol s_4_0[3] = { 'n', 's', 'a' }; @@ -109,18 +113,16 @@ static const symbol s_4_5[2] = { 'a', 'n' }; static const symbol s_4_6[2] = { 'e', 'n' }; static const symbol s_4_7[3] = { 0xC3, 0xA4, 'n' }; static const symbol s_4_8[4] = { 'n', 's', 0xC3, 0xA4 }; - -static const struct among a_4[9] = -{ -{ 3, s_4_0, -1, 3, 0}, -{ 3, s_4_1, -1, 3, 0}, -{ 3, s_4_2, -1, 3, 0}, -{ 2, s_4_3, -1, 2, 0}, -{ 2, s_4_4, -1, 1, 0}, -{ 2, s_4_5, -1, 4, 0}, -{ 2, s_4_6, -1, 6, 0}, -{ 3, s_4_7, -1, 5, 0}, -{ 4, s_4_8, -1, 3, 0} +static const struct among a_4[9] = { +{ 3, s_4_0, 0, 3, 0}, +{ 3, s_4_1, 0, 3, 0}, +{ 3, s_4_2, 0, 3, 0}, +{ 2, s_4_3, 0, 2, 0}, +{ 2, s_4_4, 0, 1, 0}, +{ 2, s_4_5, 0, 4, 0}, +{ 2, s_4_6, 0, 6, 0}, +{ 3, s_4_7, 0, 5, 0}, +{ 4, s_4_8, 0, 3, 0} }; static const symbol s_5_0[2] = { 'a', 'a' }; @@ -130,16 +132,14 @@ static const symbol s_5_3[2] = { 'o', 'o' }; static const symbol s_5_4[2] = { 'u', 'u' }; static const symbol s_5_5[4] = { 0xC3, 0xA4, 0xC3, 0xA4 }; static const symbol s_5_6[4] = { 0xC3, 0xB6, 0xC3, 0xB6 }; - -static const struct among a_5[7] = -{ -{ 2, s_5_0, -1, -1, 0}, -{ 2, s_5_1, -1, -1, 0}, -{ 2, s_5_2, -1, -1, 0}, -{ 2, s_5_3, -1, -1, 0}, -{ 2, s_5_4, -1, -1, 0}, -{ 4, s_5_5, -1, -1, 0}, -{ 4, s_5_6, -1, -1, 0} +static const struct among a_5[7] = { +{ 2, s_5_0, 0, -1, 0}, +{ 2, s_5_1, 0, -1, 0}, +{ 2, s_5_2, 0, -1, 0}, +{ 2, s_5_3, 0, -1, 0}, +{ 2, s_5_4, 0, -1, 0}, +{ 4, s_5_5, 0, -1, 0}, +{ 4, s_5_6, 0, -1, 0} }; static const symbol s_6_0[1] = { 'a' }; @@ -172,41 +172,47 @@ static const symbol s_6_26[3] = { 't', 0xC3, 0xA4 }; static const symbol s_6_27[4] = { 'l', 't', 0xC3, 0xA4 }; static const symbol s_6_28[4] = { 's', 't', 0xC3, 0xA4 }; static const symbol s_6_29[4] = { 't', 't', 0xC3, 0xA4 }; - -static const struct among a_6[30] = -{ -{ 1, s_6_0, -1, 8, 0}, -{ 3, s_6_1, 0, -1, 0}, -{ 2, s_6_2, 0, -1, 0}, -{ 3, s_6_3, 0, -1, 0}, -{ 2, s_6_4, 0, -1, 0}, -{ 3, s_6_5, 4, -1, 0}, -{ 3, s_6_6, 4, -1, 0}, -{ 3, s_6_7, 4, 2, 0}, -{ 3, s_6_8, -1, -1, 0}, -{ 3, s_6_9, -1, -1, 0}, -{ 3, s_6_10, -1, -1, 0}, -{ 1, s_6_11, -1, 7, 0}, -{ 3, s_6_12, 11, 1, 0}, -{ 3, s_6_13, 11, -1, r_VI}, -{ 4, s_6_14, 11, -1, r_LONG}, -{ 3, s_6_15, 11, 2, 0}, -{ 4, s_6_16, 11, -1, r_VI}, -{ 3, s_6_17, 11, 3, 0}, -{ 4, s_6_18, 11, -1, r_VI}, -{ 3, s_6_19, 11, 4, 0}, -{ 4, s_6_20, 11, 5, 0}, -{ 4, s_6_21, 11, 6, 0}, -{ 2, s_6_22, -1, 8, 0}, -{ 4, s_6_23, 22, -1, 0}, -{ 3, s_6_24, 22, -1, 0}, -{ 4, s_6_25, 22, -1, 0}, -{ 3, s_6_26, 22, -1, 0}, -{ 4, s_6_27, 26, -1, 0}, -{ 4, s_6_28, 26, -1, 0}, -{ 4, s_6_29, 26, 2, 0} +static const struct among a_6[30] = { +{ 1, s_6_0, 0, 8, 0}, +{ 3, s_6_1, -1, -1, 0}, +{ 2, s_6_2, -2, -1, 0}, +{ 3, s_6_3, -3, -1, 0}, +{ 2, s_6_4, -4, -1, 0}, +{ 3, s_6_5, -1, -1, 0}, +{ 3, s_6_6, -2, -1, 0}, +{ 3, s_6_7, -3, 2, 0}, +{ 3, s_6_8, 0, -1, 0}, +{ 3, s_6_9, 0, -1, 0}, +{ 3, s_6_10, 0, -1, 0}, +{ 1, s_6_11, 0, 7, 0}, +{ 3, s_6_12, -1, 1, 0}, +{ 3, s_6_13, -2, -1, 1}, +{ 4, s_6_14, -3, -1, 2}, +{ 3, s_6_15, -4, 2, 0}, +{ 4, s_6_16, -5, -1, 1}, +{ 3, s_6_17, -6, 3, 0}, +{ 4, s_6_18, -7, -1, 1}, +{ 3, s_6_19, -8, 4, 0}, +{ 4, s_6_20, -9, 5, 0}, +{ 4, s_6_21, -10, 6, 0}, +{ 2, s_6_22, 0, 8, 0}, +{ 4, s_6_23, -1, -1, 0}, +{ 3, s_6_24, -2, -1, 0}, +{ 4, s_6_25, -3, -1, 0}, +{ 3, s_6_26, -4, -1, 0}, +{ 4, s_6_27, -1, -1, 0}, +{ 4, s_6_28, -2, -1, 0}, +{ 4, s_6_29, -3, 2, 0} }; +static int af_6(struct SN_env * z) { + switch (z->af) { + case 1: return r_VI(z); + case 2: return r_LONG(z); + } + return -1; +} + static const symbol s_7_0[3] = { 'e', 'j', 'a' }; static const symbol s_7_1[3] = { 'm', 'm', 'a' }; static const symbol s_7_2[4] = { 'i', 'm', 'm', 'a' }; @@ -221,41 +227,28 @@ static const symbol s_7_10[4] = { 'm', 'm', 0xC3, 0xA4 }; static const symbol s_7_11[5] = { 'i', 'm', 'm', 0xC3, 0xA4 }; static const symbol s_7_12[4] = { 'm', 'p', 0xC3, 0xA4 }; static const symbol s_7_13[5] = { 'i', 'm', 'p', 0xC3, 0xA4 }; - -static const struct among a_7[14] = -{ -{ 3, s_7_0, -1, -1, 0}, -{ 3, s_7_1, -1, 1, 0}, -{ 4, s_7_2, 1, -1, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 4, s_7_4, 3, -1, 0}, -{ 3, s_7_5, -1, 1, 0}, -{ 4, s_7_6, 5, -1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 4, s_7_8, 7, -1, 0}, -{ 4, s_7_9, -1, -1, 0}, -{ 4, s_7_10, -1, 1, 0}, -{ 5, s_7_11, 10, -1, 0}, -{ 4, s_7_12, -1, 1, 0}, -{ 5, s_7_13, 12, -1, 0} -}; - -static const symbol s_8_0[1] = { 'i' }; -static const symbol s_8_1[1] = { 'j' }; - -static const struct among a_8[2] = -{ -{ 1, s_8_0, -1, -1, 0}, -{ 1, s_8_1, -1, -1, 0} +static const struct among a_7[14] = { +{ 3, s_7_0, 0, -1, 0}, +{ 3, s_7_1, 0, 1, 0}, +{ 4, s_7_2, -1, -1, 0}, +{ 3, s_7_3, 0, 1, 0}, +{ 4, s_7_4, -1, -1, 0}, +{ 3, s_7_5, 0, 1, 0}, +{ 4, s_7_6, -1, -1, 0}, +{ 3, s_7_7, 0, 1, 0}, +{ 4, s_7_8, -1, -1, 0}, +{ 4, s_7_9, 0, -1, 0}, +{ 4, s_7_10, 0, 1, 0}, +{ 5, s_7_11, -1, -1, 0}, +{ 4, s_7_12, 0, 1, 0}, +{ 5, s_7_13, -1, -1, 0} }; static const symbol s_9_0[3] = { 'm', 'm', 'a' }; static const symbol s_9_1[4] = { 'i', 'm', 'm', 'a' }; - -static const struct among a_9[2] = -{ -{ 3, s_9_0, -1, 1, 0}, -{ 4, s_9_1, 0, -1, 0} +static const struct among a_9[2] = { +{ 3, s_9_0, 0, 1, 0}, +{ 4, s_9_1, -1, -1, 0} }; static const unsigned char g_AEI[] = { 17, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8 }; @@ -268,65 +261,63 @@ static const unsigned char g_V2[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, static const unsigned char g_particle_end[] = { 17, 97, 24, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 32 }; -static const symbol s_0[] = { 'k', 's', 'e' }; -static const symbol s_1[] = { 'k', 's', 'i' }; -static const symbol s_2[] = { 0xC3, 0xA4 }; -static const symbol s_3[] = { 0xC3, 0xB6 }; -static const symbol s_4[] = { 'i', 'e' }; -static const symbol s_5[] = { 'p', 'o' }; -static const symbol s_6[] = { 'p', 'o' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - - if (out_grouping_U(z, g_V1, 97, 246, 1) < 0) return 0; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int ret = out_grouping_U(z, g_V1, 97, 246, 1); + if (ret < 0) return 0; + z->c += ret; + } { int ret = in_grouping_U(z, g_V1, 97, 246, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (out_grouping_U(z, g_V1, 97, 246, 1) < 0) return 0; - + ((SN_local *)z)->i_p1 = z->c; + { + int ret = out_grouping_U(z, g_V1, 97, 246, 1); + if (ret < 0) return 0; + z->c += ret; + } { int ret = in_grouping_U(z, g_V1, 97, 246, 1); if (ret < 0) return 0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; return 1; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_particle_etc(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - among_var = find_among_b(z, a_0, 10); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_0, 10, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: if (in_grouping_b_U(z, g_particle_end, 97, 246, 0)) return 0; break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -334,63 +325,71 @@ static int r_particle_etc(struct SN_env * z) { static int r_possessive(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - among_var = find_among_b(z, a_4, 9); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_4, 9, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'k') goto lab0; z->c--; return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; if (!(eq_s_b(z, 3, s_0))) return 0; z->bra = z->c; - { int ret = slice_from_s(z, 3, s_1); + { + int ret = slice_from_s(z, 3, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 4: if (z->c - 1 <= z->lb || z->p[z->c - 1] != 97) return 0; - if (!find_among_b(z, a_1, 6)) return 0; - { int ret = slice_del(z); + if (!find_among_b(z, a_1, 6, 0)) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 5: if (z->c - 2 <= z->lb || z->p[z->c - 1] != 164) return 0; - if (!find_among_b(z, a_2, 6)) return 0; - { int ret = slice_del(z); + if (!find_among_b(z, a_2, 6, 0)) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 6: if (z->c - 2 <= z->lb || z->p[z->c - 1] != 101) return 0; - if (!find_among_b(z, a_3, 2)) return 0; - { int ret = slice_del(z); + if (!find_among_b(z, a_3, 2, 0)) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -399,28 +398,26 @@ static int r_possessive(struct SN_env * z) { } static int r_LONG(struct SN_env * z) { - if (!find_among_b(z, a_5, 7)) return 0; - return 1; + return find_among_b(z, a_5, 7, 0) != 0; } static int r_VI(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] != 'i') return 0; z->c--; - if (in_grouping_b_U(z, g_V2, 97, 246, 0)) return 0; - return 1; + return !in_grouping_b_U(z, g_V2, 97, 246, 0); } static int r_case_ending(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - among_var = find_among_b(z, a_6, 30); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_6, 30, af_6); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: @@ -446,22 +443,26 @@ static int r_case_ending(struct SN_env * z) { if (!(eq_s_b(z, 2, s_3))) return 0; break; case 7: - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int ret = r_LONG(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; + { + int ret = r_LONG(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m4; - if (!(eq_s_b(z, 2, s_4))) { z->c = z->l - m2; goto lab0; } - } - lab1: - z->c = z->l - m3; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) { z->c = z->l - m2; goto lab0; } + break; + lab1: + z->c = z->l - v_4; + if (!(eq_s_b(z, 2, s_4))) { z->c = z->l - v_2; goto lab0; } + } while (0); + z->c = z->l - v_3; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) { z->c = z->l - v_2; goto lab0; } z->c = ret; } } @@ -475,53 +476,57 @@ static int r_case_ending(struct SN_env * z) { if (in_grouping_b_U(z, g_C, 98, 122, 0)) return 0; break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[2] = 1; + ((SN_local *)z)->b_ending_removed = 1; return 1; } static int r_other_endings(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p2) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p2; z->ket = z->c; - among_var = find_among_b(z, a_7, 14); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_7, 14, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (!(eq_s_b(z, 2, s_5))) goto lab0; return 0; lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_i_plural(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 106)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_8, 2)) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 106)) { z->lb = v_1; return 0; } + z->c--; z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -529,198 +534,249 @@ static int r_i_plural(struct SN_env * z) { static int r_t_plural(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = v_1; return 0; } z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - if (in_grouping_b_U(z, g_V1, 97, 246, 0)) { z->lb = mlimit1; return 0; } - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_V1, 97, 246, 0)) { z->lb = v_1; return 0; } + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } - - { int mlimit3; - if (z->c < z->I[0]) return 0; - mlimit3 = z->lb; z->lb = z->I[0]; + { + int v_3; + if (z->c < ((SN_local *)z)->i_p2) return 0; + v_3 = z->lb; z->lb = ((SN_local *)z)->i_p2; z->ket = z->c; - if (z->c - 2 <= z->lb || z->p[z->c - 1] != 97) { z->lb = mlimit3; return 0; } - among_var = find_among_b(z, a_9, 2); - if (!among_var) { z->lb = mlimit3; return 0; } + if (z->c - 2 <= z->lb || z->p[z->c - 1] != 97) { z->lb = v_3; return 0; } + among_var = find_among_b(z, a_9, 2, 0); + if (!among_var) { z->lb = v_3; return 0; } z->bra = z->c; - z->lb = mlimit3; + z->lb = v_3; } switch (among_var) { case 1: - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; if (!(eq_s_b(z, 2, s_6))) goto lab0; return 0; lab0: - z->c = z->l - m4; + z->c = z->l - v_4; } break; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_tidy(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_LONG(z); + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; + { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + { + int ret = r_LONG(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) goto lab0; z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } } lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; if (in_grouping_b_U(z, g_AEI, 97, 228, 0)) goto lab1; z->bra = z->c; if (in_grouping_b_U(z, g_C, 98, 122, 0)) goto lab1; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab1: - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab2; z->c--; z->bra = z->c; - { int m6 = z->l - z->c; (void)m6; - if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab4; + do { + int v_6 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab3; z->c--; - goto lab3; - lab4: - z->c = z->l - m6; + break; + lab3: + z->c = z->l - v_6; if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab2; z->c--; - } - lab3: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m7 = z->l - z->c; (void)m7; + { + int v_7 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab5; + if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab4; z->c--; z->bra = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab5; + if (z->c <= z->lb || z->p[z->c - 1] != 'j') goto lab4; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab5: - z->c = z->l - m7; + lab4: + z->c = z->l - v_7; } - z->lb = mlimit1; + z->lb = v_1; } - if (in_grouping_b_U(z, g_V1, 97, 246, 1) < 0) return 0; z->ket = z->c; if (in_grouping_b_U(z, g_C, 98, 122, 0)) return 0; z->bra = z->c; - z->S[0] = slice_to(z, z->S[0]); - if (z->S[0] == 0) return -1; - if (!(eq_v_b(z, z->S[0]))) return 0; - { int ret = slice_del(z); + { + int ret = slice_to(z, &((SN_local *)z)->s_x); + if (ret < 0) return ret; + } + if (!(eq_v_b(z, ((SN_local *)z)->s_x))) return 0; + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int finnish_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - z->I[2] = 0; + ((SN_local *)z)->b_ending_removed = 0; z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_particle_etc(z); + { + int v_2 = z->l - z->c; + { + int ret = r_particle_etc(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_possessive(z); + { + int v_3 = z->l - z->c; + { + int ret = r_possessive(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_case_ending(z); + { + int v_4 = z->l - z->c; + { + int ret = r_case_ending(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_other_endings(z); + { + int v_5 = z->l - z->c; + { + int ret = r_other_endings(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - - if (!(z->I[2])) goto lab1; - { int m6 = z->l - z->c; (void)m6; - { int ret = r_i_plural(z); - if (ret < 0) return ret; + do { + if (!((SN_local *)z)->b_ending_removed) goto lab0; + { + int v_6 = z->l - z->c; + { + int ret = r_i_plural(z); + if (ret < 0) return ret; + } + z->c = z->l - v_6; } - z->c = z->l - m6; - } - goto lab0; -lab1: - { int m7 = z->l - z->c; (void)m7; - { int ret = r_t_plural(z); - if (ret < 0) return ret; + break; + lab0: + { + int v_7 = z->l - z->c; + { + int ret = r_t_plural(z); + if (ret < 0) return ret; + } + z->c = z->l - v_7; } - z->c = z->l - m7; - } -lab0: - { int m8 = z->l - z->c; (void)m8; - { int ret = r_tidy(z); + } while (0); + { + int v_8 = z->l - z->c; + { + int ret = r_tidy(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } z->c = z->lb; return 1; } -extern struct SN_env * finnish_UTF_8_create_env(void) { return SN_create_env(1, 3); } +extern struct SN_env * finnish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_ending_removed = 0; + ((SN_local *)z)->s_x = NULL; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + + if ((((SN_local *)z)->s_x = create_s()) == NULL) { + finnish_UTF_8_close_env(z); + return NULL; + } + } + return z; +} -extern void finnish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 1); } +extern void finnish_UTF_8_close_env(struct SN_env * z) { + if (z) { + lose_s(((SN_local *)z)->s_x); + } + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_french.c b/src/backend/snowball/libstemmer/stem_UTF_8_french.c index de8685977ad40..53f9909525fcd 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_french.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_french.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from french.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_french.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int french_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_un_accent(struct SN_env * z); static int r_un_double(struct SN_env * z); static int r_residual_suffix(struct SN_env * z); @@ -22,27 +36,59 @@ static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); static int r_elisions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * french_UTF_8_create_env(void); -extern void french_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'q', 'u' }; +static const symbol s_1[] = { 'U' }; +static const symbol s_2[] = { 'I' }; +static const symbol s_3[] = { 'Y' }; +static const symbol s_4[] = { 0xC3, 0xAB }; +static const symbol s_5[] = { 'H', 'e' }; +static const symbol s_6[] = { 0xC3, 0xAF }; +static const symbol s_7[] = { 'H', 'i' }; +static const symbol s_8[] = { 'Y' }; +static const symbol s_9[] = { 'U' }; +static const symbol s_10[] = { 'i' }; +static const symbol s_11[] = { 'u' }; +static const symbol s_12[] = { 'y' }; +static const symbol s_13[] = { 0xC3, 0xAB }; +static const symbol s_14[] = { 0xC3, 0xAF }; +static const symbol s_15[] = { 'i', 'c' }; +static const symbol s_16[] = { 'i', 'q', 'U' }; +static const symbol s_17[] = { 'l', 'o', 'g' }; +static const symbol s_18[] = { 'u' }; +static const symbol s_19[] = { 'e', 'n', 't' }; +static const symbol s_20[] = { 'a', 't' }; +static const symbol s_21[] = { 'e', 'u', 'x' }; +static const symbol s_22[] = { 'i' }; +static const symbol s_23[] = { 'a', 'b', 'l' }; +static const symbol s_24[] = { 'i', 'q', 'U' }; +static const symbol s_25[] = { 'a', 't' }; +static const symbol s_26[] = { 'i', 'c' }; +static const symbol s_27[] = { 'i', 'q', 'U' }; +static const symbol s_28[] = { 'e', 'a', 'u' }; +static const symbol s_29[] = { 'a', 'l' }; +static const symbol s_30[] = { 'o', 'u' }; +static const symbol s_31[] = { 'e', 'u', 'x' }; +static const symbol s_32[] = { 'a', 'n', 't' }; +static const symbol s_33[] = { 'e', 'n', 't' }; +static const symbol s_34[] = { 'H', 'i' }; +static const symbol s_35[] = { 'i' }; +static const symbol s_36[] = { 0xC3, 0xA9 }; +static const symbol s_37[] = { 0xC3, 0xA8 }; +static const symbol s_38[] = { 'e' }; +static const symbol s_39[] = { 'i' }; +static const symbol s_40[] = { 0xC3, 0xA7 }; +static const symbol s_41[] = { 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 'c', 'o', 'l' }; -static const symbol s_0_1[3] = { 'p', 'a', 'r' }; -static const symbol s_0_2[3] = { 't', 'a', 'p' }; - -static const struct among a_0[3] = -{ -{ 3, s_0_0, -1, -1, 0}, -{ 3, s_0_1, -1, -1, 0}, -{ 3, s_0_2, -1, -1, 0} +static const symbol s_0_1[2] = { 'n', 'i' }; +static const symbol s_0_2[3] = { 'p', 'a', 'r' }; +static const symbol s_0_3[3] = { 't', 'a', 'p' }; +static const struct among a_0[4] = { +{ 3, s_0_0, 0, -1, 0}, +{ 2, s_0_1, 0, 1, 0}, +{ 3, s_0_2, 0, -1, 0}, +{ 3, s_0_3, 0, -1, 0} }; static const symbol s_1_1[1] = { 'H' }; @@ -51,16 +97,14 @@ static const symbol s_1_3[2] = { 'H', 'i' }; static const symbol s_1_4[1] = { 'I' }; static const symbol s_1_5[1] = { 'U' }; static const symbol s_1_6[1] = { 'Y' }; - -static const struct among a_1[7] = -{ -{ 0, 0, -1, 7, 0}, -{ 1, s_1_1, 0, 6, 0}, -{ 2, s_1_2, 1, 4, 0}, -{ 2, s_1_3, 1, 5, 0}, -{ 1, s_1_4, 0, 1, 0}, -{ 1, s_1_5, 0, 2, 0}, -{ 1, s_1_6, 0, 3, 0} +static const struct among a_1[7] = { +{ 0, 0, 0, 7, 0}, +{ 1, s_1_1, -1, 6, 0}, +{ 2, s_1_2, -1, 4, 0}, +{ 2, s_1_3, -2, 5, 0}, +{ 1, s_1_4, -4, 1, 0}, +{ 1, s_1_5, -5, 2, 0}, +{ 1, s_1_6, -6, 3, 0} }; static const symbol s_2_0[3] = { 'i', 'q', 'U' }; @@ -69,26 +113,22 @@ static const symbol s_2_2[4] = { 'I', 0xC3, 0xA8, 'r' }; static const symbol s_2_3[4] = { 'i', 0xC3, 0xA8, 'r' }; static const symbol s_2_4[3] = { 'e', 'u', 's' }; static const symbol s_2_5[2] = { 'i', 'v' }; - -static const struct among a_2[6] = -{ -{ 3, s_2_0, -1, 3, 0}, -{ 3, s_2_1, -1, 3, 0}, -{ 4, s_2_2, -1, 4, 0}, -{ 4, s_2_3, -1, 4, 0}, -{ 3, s_2_4, -1, 2, 0}, -{ 2, s_2_5, -1, 1, 0} +static const struct among a_2[6] = { +{ 3, s_2_0, 0, 3, 0}, +{ 3, s_2_1, 0, 3, 0}, +{ 4, s_2_2, 0, 4, 0}, +{ 4, s_2_3, 0, 4, 0}, +{ 3, s_2_4, 0, 2, 0}, +{ 2, s_2_5, 0, 1, 0} }; static const symbol s_3_0[2] = { 'i', 'c' }; static const symbol s_3_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_3_2[2] = { 'i', 'v' }; - -static const struct among a_3[3] = -{ -{ 2, s_3_0, -1, 2, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 2, s_3_2, -1, 3, 0} +static const struct among a_3[3] = { +{ 2, s_3_0, 0, 2, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 2, s_3_2, 0, 3, 0} }; static const symbol s_4_0[4] = { 'i', 'q', 'U', 'e' }; @@ -133,53 +173,53 @@ static const symbol s_4_38[6] = { 'e', 'm', 'm', 'e', 'n', 't' }; static const symbol s_4_39[3] = { 'a', 'u', 'x' }; static const symbol s_4_40[4] = { 'e', 'a', 'u', 'x' }; static const symbol s_4_41[3] = { 'e', 'u', 'x' }; -static const symbol s_4_42[4] = { 'i', 't', 0xC3, 0xA9 }; - -static const struct among a_4[43] = -{ -{ 4, s_4_0, -1, 1, 0}, -{ 6, s_4_1, -1, 2, 0}, -{ 4, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 5, 0}, -{ 5, s_4_4, -1, 3, 0}, -{ 4, s_4_5, -1, 1, 0}, -{ 4, s_4_6, -1, 1, 0}, -{ 4, s_4_7, -1, 11, 0}, -{ 4, s_4_8, -1, 1, 0}, -{ 3, s_4_9, -1, 8, 0}, -{ 2, s_4_10, -1, 8, 0}, -{ 5, s_4_11, -1, 4, 0}, -{ 5, s_4_12, -1, 2, 0}, -{ 5, s_4_13, -1, 4, 0}, -{ 5, s_4_14, -1, 2, 0}, -{ 5, s_4_15, -1, 1, 0}, -{ 7, s_4_16, -1, 2, 0}, -{ 5, s_4_17, -1, 1, 0}, -{ 5, s_4_18, -1, 5, 0}, -{ 6, s_4_19, -1, 3, 0}, -{ 5, s_4_20, -1, 1, 0}, -{ 5, s_4_21, -1, 1, 0}, -{ 5, s_4_22, -1, 11, 0}, -{ 5, s_4_23, -1, 1, 0}, -{ 4, s_4_24, -1, 8, 0}, -{ 3, s_4_25, -1, 8, 0}, -{ 6, s_4_26, -1, 4, 0}, -{ 6, s_4_27, -1, 2, 0}, -{ 6, s_4_28, -1, 4, 0}, -{ 6, s_4_29, -1, 2, 0}, -{ 5, s_4_30, -1, 15, 0}, -{ 6, s_4_31, 30, 6, 0}, -{ 9, s_4_32, 31, 12, 0}, -{ 5, s_4_33, -1, 7, 0}, -{ 4, s_4_34, -1, 15, 0}, -{ 5, s_4_35, 34, 6, 0}, -{ 8, s_4_36, 35, 12, 0}, -{ 6, s_4_37, 34, 13, 0}, -{ 6, s_4_38, 34, 14, 0}, -{ 3, s_4_39, -1, 10, 0}, -{ 4, s_4_40, 39, 9, 0}, -{ 3, s_4_41, -1, 1, 0}, -{ 4, s_4_42, -1, 7, 0} +static const symbol s_4_42[3] = { 'o', 'u', 'x' }; +static const symbol s_4_43[4] = { 'i', 't', 0xC3, 0xA9 }; +static const struct among a_4[44] = { +{ 4, s_4_0, 0, 1, 0}, +{ 6, s_4_1, 0, 2, 0}, +{ 4, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 5, 0}, +{ 5, s_4_4, 0, 3, 0}, +{ 4, s_4_5, 0, 1, 0}, +{ 4, s_4_6, 0, 1, 0}, +{ 4, s_4_7, 0, 12, 0}, +{ 4, s_4_8, 0, 1, 0}, +{ 3, s_4_9, 0, 8, 0}, +{ 2, s_4_10, 0, 8, 0}, +{ 5, s_4_11, 0, 4, 0}, +{ 5, s_4_12, 0, 2, 0}, +{ 5, s_4_13, 0, 4, 0}, +{ 5, s_4_14, 0, 2, 0}, +{ 5, s_4_15, 0, 1, 0}, +{ 7, s_4_16, 0, 2, 0}, +{ 5, s_4_17, 0, 1, 0}, +{ 5, s_4_18, 0, 5, 0}, +{ 6, s_4_19, 0, 3, 0}, +{ 5, s_4_20, 0, 1, 0}, +{ 5, s_4_21, 0, 1, 0}, +{ 5, s_4_22, 0, 12, 0}, +{ 5, s_4_23, 0, 1, 0}, +{ 4, s_4_24, 0, 8, 0}, +{ 3, s_4_25, 0, 8, 0}, +{ 6, s_4_26, 0, 4, 0}, +{ 6, s_4_27, 0, 2, 0}, +{ 6, s_4_28, 0, 4, 0}, +{ 6, s_4_29, 0, 2, 0}, +{ 5, s_4_30, 0, 16, 0}, +{ 6, s_4_31, -1, 6, 0}, +{ 9, s_4_32, -1, 13, 0}, +{ 5, s_4_33, 0, 7, 0}, +{ 4, s_4_34, 0, 16, 0}, +{ 5, s_4_35, -1, 6, 0}, +{ 8, s_4_36, -1, 13, 0}, +{ 6, s_4_37, -3, 14, 0}, +{ 6, s_4_38, -4, 15, 0}, +{ 3, s_4_39, 0, 10, 0}, +{ 4, s_4_40, -1, 9, 0}, +{ 3, s_4_41, 0, 1, 0}, +{ 3, s_4_42, 0, 11, 0}, +{ 4, s_4_43, 0, 7, 0} }; static const symbol s_5_0[3] = { 'i', 'r', 'a' }; @@ -217,437 +257,425 @@ static const symbol s_5_31[5] = { 'i', 'r', 'i', 'e', 'z' }; static const symbol s_5_32[6] = { 'i', 's', 's', 'i', 'e', 'z' }; static const symbol s_5_33[4] = { 'i', 'r', 'e', 'z' }; static const symbol s_5_34[5] = { 'i', 's', 's', 'e', 'z' }; - -static const struct among a_5[35] = -{ -{ 3, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 1, 0}, -{ 4, s_5_2, -1, 1, 0}, -{ 7, s_5_3, -1, 1, 0}, -{ 1, s_5_4, -1, 1, 0}, -{ 4, s_5_5, 4, 1, 0}, -{ 2, s_5_6, -1, 1, 0}, -{ 4, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 1, 0}, -{ 5, s_5_9, -1, 1, 0}, -{ 5, s_5_10, -1, 1, 0}, -{ 8, s_5_11, -1, 1, 0}, -{ 5, s_5_12, -1, 1, 0}, -{ 2, s_5_13, -1, 1, 0}, -{ 5, s_5_14, 13, 1, 0}, -{ 6, s_5_15, 13, 1, 0}, -{ 6, s_5_16, -1, 1, 0}, -{ 7, s_5_17, -1, 1, 0}, -{ 5, s_5_18, -1, 1, 0}, -{ 6, s_5_19, -1, 1, 0}, -{ 7, s_5_20, -1, 1, 0}, -{ 2, s_5_21, -1, 1, 0}, -{ 5, s_5_22, 21, 1, 0}, -{ 6, s_5_23, 21, 1, 0}, -{ 6, s_5_24, -1, 1, 0}, -{ 7, s_5_25, -1, 1, 0}, -{ 8, s_5_26, -1, 1, 0}, -{ 5, s_5_27, -1, 1, 0}, -{ 6, s_5_28, -1, 1, 0}, -{ 5, s_5_29, -1, 1, 0}, -{ 3, s_5_30, -1, 1, 0}, -{ 5, s_5_31, -1, 1, 0}, -{ 6, s_5_32, -1, 1, 0}, -{ 4, s_5_33, -1, 1, 0}, -{ 5, s_5_34, -1, 1, 0} +static const struct among a_5[35] = { +{ 3, s_5_0, 0, 1, 0}, +{ 2, s_5_1, 0, 1, 0}, +{ 4, s_5_2, 0, 1, 0}, +{ 7, s_5_3, 0, 1, 0}, +{ 1, s_5_4, 0, 1, 0}, +{ 4, s_5_5, -1, 1, 0}, +{ 2, s_5_6, 0, 1, 0}, +{ 4, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 1, 0}, +{ 5, s_5_9, 0, 1, 0}, +{ 5, s_5_10, 0, 1, 0}, +{ 8, s_5_11, 0, 1, 0}, +{ 5, s_5_12, 0, 1, 0}, +{ 2, s_5_13, 0, 1, 0}, +{ 5, s_5_14, -1, 1, 0}, +{ 6, s_5_15, -2, 1, 0}, +{ 6, s_5_16, 0, 1, 0}, +{ 7, s_5_17, 0, 1, 0}, +{ 5, s_5_18, 0, 1, 0}, +{ 6, s_5_19, 0, 1, 0}, +{ 7, s_5_20, 0, 1, 0}, +{ 2, s_5_21, 0, 1, 0}, +{ 5, s_5_22, -1, 1, 0}, +{ 6, s_5_23, -2, 1, 0}, +{ 6, s_5_24, 0, 1, 0}, +{ 7, s_5_25, 0, 1, 0}, +{ 8, s_5_26, 0, 1, 0}, +{ 5, s_5_27, 0, 1, 0}, +{ 6, s_5_28, 0, 1, 0}, +{ 5, s_5_29, 0, 1, 0}, +{ 3, s_5_30, 0, 1, 0}, +{ 5, s_5_31, 0, 1, 0}, +{ 6, s_5_32, 0, 1, 0}, +{ 4, s_5_33, 0, 1, 0}, +{ 5, s_5_34, 0, 1, 0} }; -static const symbol s_6_0[1] = { 'a' }; -static const symbol s_6_1[3] = { 'e', 'r', 'a' }; -static const symbol s_6_2[4] = { 'a', 's', 's', 'e' }; -static const symbol s_6_3[4] = { 'a', 'n', 't', 'e' }; -static const symbol s_6_4[3] = { 0xC3, 0xA9, 'e' }; -static const symbol s_6_5[2] = { 'a', 'i' }; -static const symbol s_6_6[4] = { 'e', 'r', 'a', 'i' }; -static const symbol s_6_7[2] = { 'e', 'r' }; -static const symbol s_6_8[2] = { 'a', 's' }; -static const symbol s_6_9[4] = { 'e', 'r', 'a', 's' }; -static const symbol s_6_10[5] = { 0xC3, 0xA2, 'm', 'e', 's' }; -static const symbol s_6_11[5] = { 'a', 's', 's', 'e', 's' }; -static const symbol s_6_12[5] = { 'a', 'n', 't', 'e', 's' }; -static const symbol s_6_13[5] = { 0xC3, 0xA2, 't', 'e', 's' }; -static const symbol s_6_14[4] = { 0xC3, 0xA9, 'e', 's' }; -static const symbol s_6_15[3] = { 'a', 'i', 's' }; -static const symbol s_6_16[5] = { 'e', 'r', 'a', 'i', 's' }; -static const symbol s_6_17[4] = { 'i', 'o', 'n', 's' }; -static const symbol s_6_18[6] = { 'e', 'r', 'i', 'o', 'n', 's' }; -static const symbol s_6_19[7] = { 'a', 's', 's', 'i', 'o', 'n', 's' }; -static const symbol s_6_20[5] = { 'e', 'r', 'o', 'n', 's' }; -static const symbol s_6_21[4] = { 'a', 'n', 't', 's' }; -static const symbol s_6_22[3] = { 0xC3, 0xA9, 's' }; -static const symbol s_6_23[3] = { 'a', 'i', 't' }; -static const symbol s_6_24[5] = { 'e', 'r', 'a', 'i', 't' }; -static const symbol s_6_25[3] = { 'a', 'n', 't' }; -static const symbol s_6_26[5] = { 'a', 'I', 'e', 'n', 't' }; -static const symbol s_6_27[7] = { 'e', 'r', 'a', 'I', 'e', 'n', 't' }; -static const symbol s_6_28[6] = { 0xC3, 0xA8, 'r', 'e', 'n', 't' }; -static const symbol s_6_29[6] = { 'a', 's', 's', 'e', 'n', 't' }; -static const symbol s_6_30[5] = { 'e', 'r', 'o', 'n', 't' }; -static const symbol s_6_31[3] = { 0xC3, 0xA2, 't' }; -static const symbol s_6_32[2] = { 'e', 'z' }; -static const symbol s_6_33[3] = { 'i', 'e', 'z' }; -static const symbol s_6_34[5] = { 'e', 'r', 'i', 'e', 'z' }; -static const symbol s_6_35[6] = { 'a', 's', 's', 'i', 'e', 'z' }; -static const symbol s_6_36[4] = { 'e', 'r', 'e', 'z' }; -static const symbol s_6_37[2] = { 0xC3, 0xA9 }; - -static const struct among a_6[38] = -{ -{ 1, s_6_0, -1, 3, 0}, -{ 3, s_6_1, 0, 2, 0}, -{ 4, s_6_2, -1, 3, 0}, -{ 4, s_6_3, -1, 3, 0}, -{ 3, s_6_4, -1, 2, 0}, -{ 2, s_6_5, -1, 3, 0}, -{ 4, s_6_6, 5, 2, 0}, -{ 2, s_6_7, -1, 2, 0}, -{ 2, s_6_8, -1, 3, 0}, -{ 4, s_6_9, 8, 2, 0}, -{ 5, s_6_10, -1, 3, 0}, -{ 5, s_6_11, -1, 3, 0}, -{ 5, s_6_12, -1, 3, 0}, -{ 5, s_6_13, -1, 3, 0}, -{ 4, s_6_14, -1, 2, 0}, -{ 3, s_6_15, -1, 3, 0}, -{ 5, s_6_16, 15, 2, 0}, -{ 4, s_6_17, -1, 1, 0}, -{ 6, s_6_18, 17, 2, 0}, -{ 7, s_6_19, 17, 3, 0}, -{ 5, s_6_20, -1, 2, 0}, -{ 4, s_6_21, -1, 3, 0}, -{ 3, s_6_22, -1, 2, 0}, -{ 3, s_6_23, -1, 3, 0}, -{ 5, s_6_24, 23, 2, 0}, -{ 3, s_6_25, -1, 3, 0}, -{ 5, s_6_26, -1, 3, 0}, -{ 7, s_6_27, 26, 2, 0}, -{ 6, s_6_28, -1, 2, 0}, -{ 6, s_6_29, -1, 3, 0}, -{ 5, s_6_30, -1, 2, 0}, -{ 3, s_6_31, -1, 3, 0}, -{ 2, s_6_32, -1, 2, 0}, -{ 3, s_6_33, 32, 2, 0}, -{ 5, s_6_34, 33, 2, 0}, -{ 6, s_6_35, 33, 3, 0}, -{ 4, s_6_36, 32, 2, 0}, -{ 2, s_6_37, -1, 2, 0} +static const symbol s_6_0[2] = { 'a', 'l' }; +static const symbol s_6_1[4] = { 0xC3, 0xA9, 'p', 'l' }; +static const symbol s_6_2[3] = { 'a', 'u', 'v' }; +static const struct among a_6[3] = { +{ 2, s_6_0, 0, 1, 0}, +{ 4, s_6_1, 0, -1, 0}, +{ 3, s_6_2, 0, -1, 0} }; -static const symbol s_7_0[1] = { 'e' }; -static const symbol s_7_1[5] = { 'I', 0xC3, 0xA8, 'r', 'e' }; -static const symbol s_7_2[5] = { 'i', 0xC3, 0xA8, 'r', 'e' }; -static const symbol s_7_3[3] = { 'i', 'o', 'n' }; -static const symbol s_7_4[3] = { 'I', 'e', 'r' }; -static const symbol s_7_5[3] = { 'i', 'e', 'r' }; - -static const struct among a_7[6] = -{ -{ 1, s_7_0, -1, 3, 0}, -{ 5, s_7_1, 0, 2, 0}, -{ 5, s_7_2, 0, 2, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 3, s_7_4, -1, 2, 0}, -{ 3, s_7_5, -1, 2, 0} +static const symbol s_7_0[1] = { 'a' }; +static const symbol s_7_1[3] = { 'e', 'r', 'a' }; +static const symbol s_7_2[4] = { 'a', 'i', 's', 'e' }; +static const symbol s_7_3[4] = { 'a', 's', 's', 'e' }; +static const symbol s_7_4[4] = { 'a', 'n', 't', 'e' }; +static const symbol s_7_5[3] = { 0xC3, 0xA9, 'e' }; +static const symbol s_7_6[2] = { 'a', 'i' }; +static const symbol s_7_7[4] = { 'e', 'r', 'a', 'i' }; +static const symbol s_7_8[2] = { 'e', 'r' }; +static const symbol s_7_9[2] = { 'a', 's' }; +static const symbol s_7_10[4] = { 'e', 'r', 'a', 's' }; +static const symbol s_7_11[5] = { 0xC3, 0xA2, 'm', 'e', 's' }; +static const symbol s_7_12[5] = { 'a', 'i', 's', 'e', 's' }; +static const symbol s_7_13[5] = { 'a', 's', 's', 'e', 's' }; +static const symbol s_7_14[5] = { 'a', 'n', 't', 'e', 's' }; +static const symbol s_7_15[5] = { 0xC3, 0xA2, 't', 'e', 's' }; +static const symbol s_7_16[4] = { 0xC3, 0xA9, 'e', 's' }; +static const symbol s_7_17[3] = { 'a', 'i', 's' }; +static const symbol s_7_18[4] = { 'e', 'a', 'i', 's' }; +static const symbol s_7_19[5] = { 'e', 'r', 'a', 'i', 's' }; +static const symbol s_7_20[4] = { 'i', 'o', 'n', 's' }; +static const symbol s_7_21[6] = { 'e', 'r', 'i', 'o', 'n', 's' }; +static const symbol s_7_22[7] = { 'a', 's', 's', 'i', 'o', 'n', 's' }; +static const symbol s_7_23[5] = { 'e', 'r', 'o', 'n', 's' }; +static const symbol s_7_24[4] = { 'a', 'n', 't', 's' }; +static const symbol s_7_25[3] = { 0xC3, 0xA9, 's' }; +static const symbol s_7_26[3] = { 'a', 'i', 't' }; +static const symbol s_7_27[5] = { 'e', 'r', 'a', 'i', 't' }; +static const symbol s_7_28[3] = { 'a', 'n', 't' }; +static const symbol s_7_29[5] = { 'a', 'I', 'e', 'n', 't' }; +static const symbol s_7_30[7] = { 'e', 'r', 'a', 'I', 'e', 'n', 't' }; +static const symbol s_7_31[6] = { 0xC3, 0xA8, 'r', 'e', 'n', 't' }; +static const symbol s_7_32[6] = { 'a', 's', 's', 'e', 'n', 't' }; +static const symbol s_7_33[5] = { 'e', 'r', 'o', 'n', 't' }; +static const symbol s_7_34[3] = { 0xC3, 0xA2, 't' }; +static const symbol s_7_35[2] = { 'e', 'z' }; +static const symbol s_7_36[3] = { 'i', 'e', 'z' }; +static const symbol s_7_37[5] = { 'e', 'r', 'i', 'e', 'z' }; +static const symbol s_7_38[6] = { 'a', 's', 's', 'i', 'e', 'z' }; +static const symbol s_7_39[4] = { 'e', 'r', 'e', 'z' }; +static const symbol s_7_40[2] = { 0xC3, 0xA9 }; +static const struct among a_7[41] = { +{ 1, s_7_0, 0, 3, 0}, +{ 3, s_7_1, -1, 2, 0}, +{ 4, s_7_2, 0, 4, 0}, +{ 4, s_7_3, 0, 3, 0}, +{ 4, s_7_4, 0, 3, 0}, +{ 3, s_7_5, 0, 2, 0}, +{ 2, s_7_6, 0, 3, 0}, +{ 4, s_7_7, -1, 2, 0}, +{ 2, s_7_8, 0, 2, 0}, +{ 2, s_7_9, 0, 3, 0}, +{ 4, s_7_10, -1, 2, 0}, +{ 5, s_7_11, 0, 3, 0}, +{ 5, s_7_12, 0, 4, 0}, +{ 5, s_7_13, 0, 3, 0}, +{ 5, s_7_14, 0, 3, 0}, +{ 5, s_7_15, 0, 3, 0}, +{ 4, s_7_16, 0, 2, 0}, +{ 3, s_7_17, 0, 4, 0}, +{ 4, s_7_18, -1, 2, 0}, +{ 5, s_7_19, -2, 2, 0}, +{ 4, s_7_20, 0, 1, 0}, +{ 6, s_7_21, -1, 2, 0}, +{ 7, s_7_22, -2, 3, 0}, +{ 5, s_7_23, 0, 2, 0}, +{ 4, s_7_24, 0, 3, 0}, +{ 3, s_7_25, 0, 2, 0}, +{ 3, s_7_26, 0, 3, 0}, +{ 5, s_7_27, -1, 2, 0}, +{ 3, s_7_28, 0, 3, 0}, +{ 5, s_7_29, 0, 3, 0}, +{ 7, s_7_30, -1, 2, 0}, +{ 6, s_7_31, 0, 2, 0}, +{ 6, s_7_32, 0, 3, 0}, +{ 5, s_7_33, 0, 2, 0}, +{ 3, s_7_34, 0, 3, 0}, +{ 2, s_7_35, 0, 2, 0}, +{ 3, s_7_36, -1, 2, 0}, +{ 5, s_7_37, -1, 2, 0}, +{ 6, s_7_38, -2, 3, 0}, +{ 4, s_7_39, -4, 2, 0}, +{ 2, s_7_40, 0, 2, 0} }; -static const symbol s_8_0[3] = { 'e', 'l', 'l' }; -static const symbol s_8_1[4] = { 'e', 'i', 'l', 'l' }; -static const symbol s_8_2[3] = { 'e', 'n', 'n' }; -static const symbol s_8_3[3] = { 'o', 'n', 'n' }; -static const symbol s_8_4[3] = { 'e', 't', 't' }; +static const symbol s_8_0[1] = { 'e' }; +static const symbol s_8_1[5] = { 'I', 0xC3, 0xA8, 'r', 'e' }; +static const symbol s_8_2[5] = { 'i', 0xC3, 0xA8, 'r', 'e' }; +static const symbol s_8_3[3] = { 'i', 'o', 'n' }; +static const symbol s_8_4[3] = { 'I', 'e', 'r' }; +static const symbol s_8_5[3] = { 'i', 'e', 'r' }; +static const struct among a_8[6] = { +{ 1, s_8_0, 0, 3, 0}, +{ 5, s_8_1, -1, 2, 0}, +{ 5, s_8_2, -2, 2, 0}, +{ 3, s_8_3, 0, 1, 0}, +{ 3, s_8_4, 0, 2, 0}, +{ 3, s_8_5, 0, 2, 0} +}; -static const struct among a_8[5] = -{ -{ 3, s_8_0, -1, -1, 0}, -{ 4, s_8_1, -1, -1, 0}, -{ 3, s_8_2, -1, -1, 0}, -{ 3, s_8_3, -1, -1, 0}, -{ 3, s_8_4, -1, -1, 0} +static const symbol s_9_0[3] = { 'e', 'l', 'l' }; +static const symbol s_9_1[4] = { 'e', 'i', 'l', 'l' }; +static const symbol s_9_2[3] = { 'e', 'n', 'n' }; +static const symbol s_9_3[3] = { 'o', 'n', 'n' }; +static const symbol s_9_4[3] = { 'e', 't', 't' }; +static const struct among a_9[5] = { +{ 3, s_9_0, 0, -1, 0}, +{ 4, s_9_1, 0, -1, 0}, +{ 3, s_9_2, 0, -1, 0}, +{ 3, s_9_3, 0, -1, 0}, +{ 3, s_9_4, 0, -1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 130, 103, 8, 5 }; +static const unsigned char g_oux_ending[] = { 65, 85 }; + static const unsigned char g_elision_char[] = { 131, 14, 3 }; static const unsigned char g_keep_with_s[] = { 1, 65, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; -static const symbol s_0[] = { 'q', 'u' }; -static const symbol s_1[] = { 'U' }; -static const symbol s_2[] = { 'I' }; -static const symbol s_3[] = { 'Y' }; -static const symbol s_4[] = { 0xC3, 0xAB }; -static const symbol s_5[] = { 'H', 'e' }; -static const symbol s_6[] = { 0xC3, 0xAF }; -static const symbol s_7[] = { 'H', 'i' }; -static const symbol s_8[] = { 'Y' }; -static const symbol s_9[] = { 'U' }; -static const symbol s_10[] = { 'i' }; -static const symbol s_11[] = { 'u' }; -static const symbol s_12[] = { 'y' }; -static const symbol s_13[] = { 0xC3, 0xAB }; -static const symbol s_14[] = { 0xC3, 0xAF }; -static const symbol s_15[] = { 'i', 'c' }; -static const symbol s_16[] = { 'i', 'q', 'U' }; -static const symbol s_17[] = { 'l', 'o', 'g' }; -static const symbol s_18[] = { 'u' }; -static const symbol s_19[] = { 'e', 'n', 't' }; -static const symbol s_20[] = { 'a', 't' }; -static const symbol s_21[] = { 'e', 'u', 'x' }; -static const symbol s_22[] = { 'i' }; -static const symbol s_23[] = { 'a', 'b', 'l' }; -static const symbol s_24[] = { 'i', 'q', 'U' }; -static const symbol s_25[] = { 'a', 't' }; -static const symbol s_26[] = { 'i', 'c' }; -static const symbol s_27[] = { 'i', 'q', 'U' }; -static const symbol s_28[] = { 'e', 'a', 'u' }; -static const symbol s_29[] = { 'a', 'l' }; -static const symbol s_30[] = { 'e', 'u', 'x' }; -static const symbol s_31[] = { 'a', 'n', 't' }; -static const symbol s_32[] = { 'e', 'n', 't' }; -static const symbol s_33[] = { 'H', 'i' }; -static const symbol s_34[] = { 'i' }; -static const symbol s_35[] = { 0xC3, 0xA9 }; -static const symbol s_36[] = { 0xC3, 0xA8 }; -static const symbol s_37[] = { 'e' }; -static const symbol s_38[] = { 'i' }; -static const symbol s_39[] = { 0xC3, 0xA7 }; -static const symbol s_40[] = { 'c' }; - static int r_elisions(struct SN_env * z) { z->bra = z->c; - { int c1 = z->c; - if (in_grouping_U(z, g_elision_char, 99, 116, 0)) goto lab1; - goto lab0; - lab1: - z->c = c1; + do { + int v_1 = z->c; + if (in_grouping_U(z, g_elision_char, 99, 116, 0)) goto lab0; + break; + lab0: + z->c = v_1; if (!(eq_s(z, 2, s_0))) return 0; - } -lab0: + } while (0); if (z->c == z->l || z->p[z->c] != '\'') return 0; z->c++; z->ket = z->c; - - if (z->c < z->l) goto lab2; + if (z->c < z->l) goto lab1; return 0; -lab2: - { int ret = slice_del(z); +lab1: + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_prelude(struct SN_env * z) { - while(1) { - int c1 = z->c; - while(1) { - int c2 = z->c; - { int c3 = z->c; - if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab3; + while (1) { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + do { + int v_3 = z->c; + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab2; z->bra = z->c; - { int c4 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab5; + do { + int v_4 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab3; z->c++; z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab5; - { int ret = slice_from_s(z, 1, s_1); + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab3; + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = c4; - if (z->c == z->l || z->p[z->c] != 'i') goto lab6; + break; + lab3: + z->c = v_4; + if (z->c == z->l || z->p[z->c] != 'i') goto lab4; z->c++; z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab6; - { int ret = slice_from_s(z, 1, s_2); + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab4; + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } - goto lab4; - lab6: - z->c = c4; - if (z->c == z->l || z->p[z->c] != 'y') goto lab3; + break; + lab4: + z->c = v_4; + if (z->c == z->l || z->p[z->c] != 'y') goto lab2; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } - } - lab4: - goto lab2; - lab3: - z->c = c3; + } while (0); + break; + lab2: + z->c = v_3; z->bra = z->c; - if (!(eq_s(z, 2, s_4))) goto lab7; + if (!(eq_s(z, 2, s_4))) goto lab5; z->ket = z->c; - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } - goto lab2; - lab7: - z->c = c3; + break; + lab5: + z->c = v_3; z->bra = z->c; - if (!(eq_s(z, 2, s_6))) goto lab8; + if (!(eq_s(z, 2, s_6))) goto lab6; z->ket = z->c; - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } - goto lab2; - lab8: - z->c = c3; + break; + lab6: + z->c = v_3; z->bra = z->c; - if (z->c == z->l || z->p[z->c] != 'y') goto lab9; + if (z->c == z->l || z->p[z->c] != 'y') goto lab7; z->c++; z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab9; - { int ret = slice_from_s(z, 1, s_8); + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab7; + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } - goto lab2; - lab9: - z->c = c3; + break; + lab7: + z->c = v_3; if (z->c == z->l || z->p[z->c] != 'q') goto lab1; z->c++; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'u') goto lab1; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } - } - lab2: - z->c = c2; + } while (0); + z->c = v_2; break; lab1: - z->c = c2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_2; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab2; - if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); - if (ret < 0) goto lab2; + int among_var; + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab1; + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab1; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) goto lab1; z->c = ret; } - goto lab1; + break; + lab1: + z->c = v_2; + if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 3 || !((33282 >> (z->p[z->c + 1] & 0x1f)) & 1)) goto lab2; + among_var = find_among(z, a_0, 4, 0); + if (!among_var) goto lab2; + switch (among_var) { + case 1: + if (in_grouping_U(z, g_v, 97, 251, 0)) goto lab2; + break; + } + break; lab2: - z->c = c2; - if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 3 || !((331776 >> (z->p[z->c + 2] & 0x1f)) & 1)) goto lab3; - if (!find_among(z, a_0, 3)) goto lab3; - goto lab1; - lab3: - z->c = c2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_2; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } - { int ret = out_grouping_U(z, g_v, 97, 251, 1); if (ret < 0) goto lab0; z->c += ret; } - } - lab1: - z->I[2] = z->c; + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c3 = z->c; - + { + int v_3 = z->c; { int ret = out_grouping_U(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 251, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab3; z->c += ret; } - z->I[0] = z->c; - lab4: - z->c = c3; + ((SN_local *)z)->i_p2 = z->c; + lab3: + z->c = v_3; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || z->p[z->c + 0] >> 5 != 2 || !((35652352 >> (z->p[z->c + 0] & 0x1f)) & 1)) among_var = 7; else - among_var = find_among(z, a_1, 7); + among_var = find_among(z, a_1, 7, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_13); + { + int ret = slice_from_s(z, 2, s_13); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 2, s_14); + { + int ret = slice_from_s(z, 2, s_14); if (ret < 0) return ret; } break; case 6: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 7: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -655,346 +683,417 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_4, 43); + among_var = find_among_b(z, a_4, 44, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int m2 = z->l - z->c; (void)m2; - { int ret = r_R2(z); - if (ret == 0) goto lab2; + do { + int v_2 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; - { int ret = slice_from_s(z, 3, s_16); + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_from_s(z, 3, s_16); if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: ; } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_17); + { + int ret = slice_from_s(z, 3, s_17); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_18); + { + int ret = slice_from_s(z, 1, s_18); if (ret < 0) return ret; } break; case 5: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_19); + { + int ret = slice_from_s(z, 3, s_19); if (ret < 0) return ret; } break; case 6: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_2, 6); - if (!among_var) { z->c = z->l - m3; goto lab3; } + among_var = find_among_b(z, a_2, 6, 0); + if (!among_var) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (!(eq_s_b(z, 2, s_20))) { z->c = z->l - m3; goto lab3; } + if (!(eq_s_b(z, 2, s_20))) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m4 = z->l - z->c; (void)m4; - { int ret = r_R2(z); - if (ret == 0) goto lab5; + do { + int v_4 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m4; - { int ret = r_R1(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + break; + lab3: + z->c = z->l - v_4; + { + int ret = r_R1(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_from_s(z, 3, s_21); + { + int ret = slice_from_s(z, 3, s_21); if (ret < 0) return ret; } - } - lab4: + } while (0); break; case 3: - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 4: - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m3; goto lab3; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_from_s(z, 1, s_22); + { + int ret = slice_from_s(z, 1, s_22); if (ret < 0) return ret; } break; } - lab3: + lab2: ; } break; case 7: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m5; goto lab6; } - among_var = find_among_b(z, a_3, 3); - if (!among_var) { z->c = z->l - m5; goto lab6; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_5; goto lab4; } + among_var = find_among_b(z, a_3, 3, 0); + if (!among_var) { z->c = z->l - v_5; goto lab4; } z->bra = z->c; switch (among_var) { case 1: - { int m6 = z->l - z->c; (void)m6; - { int ret = r_R2(z); - if (ret == 0) goto lab8; + do { + int v_6 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m6; - { int ret = slice_from_s(z, 3, s_23); + break; + lab5: + z->c = z->l - v_6; + { + int ret = slice_from_s(z, 3, s_23); if (ret < 0) return ret; } - } - lab7: + } while (0); break; case 2: - { int m7 = z->l - z->c; (void)m7; - { int ret = r_R2(z); - if (ret == 0) goto lab10; + do { + int v_7 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab9; - lab10: - z->c = z->l - m7; - { int ret = slice_from_s(z, 3, s_24); + break; + lab6: + z->c = z->l - v_7; + { + int ret = slice_from_s(z, 3, s_24); if (ret < 0) return ret; } - } - lab9: + } while (0); break; case 3: - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m5; goto lab6; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_5; goto lab4; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - lab6: + lab4: ; } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; + { + int v_8 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_25))) { z->c = z->l - m8; goto lab11; } + if (!(eq_s_b(z, 2, s_25))) { z->c = z->l - v_8; goto lab7; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m8; goto lab11; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_8; goto lab7; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (!(eq_s_b(z, 2, s_26))) { z->c = z->l - m8; goto lab11; } + if (!(eq_s_b(z, 2, s_26))) { z->c = z->l - v_8; goto lab7; } z->bra = z->c; - { int m9 = z->l - z->c; (void)m9; - { int ret = r_R2(z); - if (ret == 0) goto lab13; + do { + int v_9 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab8; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab12; - lab13: - z->c = z->l - m9; - { int ret = slice_from_s(z, 3, s_27); + break; + lab8: + z->c = z->l - v_9; + { + int ret = slice_from_s(z, 3, s_27); if (ret < 0) return ret; } - } - lab12: - lab11: + } while (0); + lab7: ; } break; case 9: - { int ret = slice_from_s(z, 3, s_28); + { + int ret = slice_from_s(z, 3, s_28); if (ret < 0) return ret; } break; case 10: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_29); + { + int ret = slice_from_s(z, 2, s_29); if (ret < 0) return ret; } break; case 11: - { int m10 = z->l - z->c; (void)m10; - { int ret = r_R2(z); - if (ret == 0) goto lab15; + if (in_grouping_b_U(z, g_oux_ending, 98, 112, 0)) return 0; + { + int ret = slice_from_s(z, 2, s_30); + if (ret < 0) return ret; + } + break; + case 12: + do { + int v_10 = z->l - z->c; + { + int ret = r_R2(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab14; - lab15: - z->c = z->l - m10; - { int ret = r_R1(z); + break; + lab9: + z->c = z->l - v_10; + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_30); + { + int ret = slice_from_s(z, 3, s_31); if (ret < 0) return ret; } - } - lab14: + } while (0); break; - case 12: - { int ret = r_R1(z); + case 13: + { + int ret = r_R1(z); if (ret <= 0) return ret; } if (out_grouping_b_U(z, g_v, 97, 251, 0)) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 13: - { int ret = r_RV(z); + case 14: + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_31); + { + int ret = slice_from_s(z, 3, s_32); if (ret < 0) return ret; } return 0; break; - case 14: - { int ret = r_RV(z); + case 15: + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_32); + { + int ret = slice_from_s(z, 3, s_33); if (ret < 0) return ret; } return 0; break; - case 15: - { int m_test11 = z->l - z->c; + case 16: + { + int v_11 = z->l - z->c; if (in_grouping_b_U(z, g_v, 97, 251, 0)) return 0; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - z->c = z->l - m_test11; + z->c = z->l - v_11; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 0; @@ -1004,293 +1103,365 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_i_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68944418 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_5, 35)) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68944418 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_5, 35, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'H') goto lab0; z->c--; - { z->lb = mlimit1; return 0; } + { z->lb = v_1; return 0; } lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - if (out_grouping_b_U(z, g_v, 97, 251, 0)) { z->lb = mlimit1; return 0; } - { int ret = slice_del(z); + if (out_grouping_b_U(z, g_v, 97, 251, 0)) { z->lb = v_1; return 0; } + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_verb_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - among_var = find_among_b(z, a_6, 38); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_7, 41, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - switch (among_var) { - case 1: - { int ret = r_R2(z); - if (ret == 0) { z->lb = mlimit1; return 0; } - if (ret < 0) return ret; - } - { int ret = slice_del(z); - if (ret < 0) return ret; - } - break; - case 2: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - break; - case 3: - { int ret = slice_del(z); + z->lb = v_1; + } + switch (among_var) { + case 1: + { + int ret = r_R2(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') { z->c = z->l - v_2; goto lab0; } + z->c--; + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_2; goto lab0; } if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; - z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') { z->c = z->l - m2; goto lab0; } - z->c--; - z->bra = z->c; - { int ret = slice_del(z); - if (ret < 0) return ret; - } - lab0: - ; + z->bra = z->c; + lab0: + ; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 4: + { + int v_3 = z->l - z->c; + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 108 && z->p[z->c - 1] != 118)) goto lab1; + among_var = find_among_b(z, a_6, 3, 0); + if (!among_var) goto lab1; + switch (among_var) { + case 1: + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab1; + z->c = ret; + } + if (z->c > z->lb) goto lab1; + break; } - break; - } - z->lb = mlimit1; + return 0; + lab1: + z->c = z->l - v_3; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; } return 1; } static int r_residual_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - v_1; goto lab0; } z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - { int m3 = z->l - z->c; (void)m3; - if (!(eq_s_b(z, 2, s_33))) goto lab2; - goto lab1; - lab2: - z->c = z->l - m3; - if (out_grouping_b_U(z, g_keep_with_s, 97, 232, 0)) { z->c = z->l - m1; goto lab0; } - } - lab1: - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 2, s_34))) goto lab1; + break; + lab1: + z->c = z->l - v_3; + if (out_grouping_b_U(z, g_keep_with_s, 97, 232, 0)) { z->c = z->l - v_1; goto lab0; } + } while (0); + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: ; } - - { int mlimit4; - if (z->c < z->I[2]) return 0; - mlimit4 = z->lb; z->lb = z->I[2]; + { + int v_4; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_4 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((278560 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit4; return 0; } - among_var = find_among_b(z, a_7, 6); - if (!among_var) { z->lb = mlimit4; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((278560 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_4; return 0; } + among_var = find_among_b(z, a_8, 6, 0); + if (!among_var) { z->lb = v_4; return 0; } z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); - if (ret == 0) { z->lb = mlimit4; return 0; } + { + int ret = r_R2(z); + if (ret == 0) { z->lb = v_4; return 0; } if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab4; + do { + int v_5 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab2; z->c--; - goto lab3; - lab4: - z->c = z->l - m5; - if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = mlimit4; return 0; } + break; + lab2: + z->c = z->l - v_5; + if (z->c <= z->lb || z->p[z->c - 1] != 't') { z->lb = v_4; return 0; } z->c--; - } - lab3: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_34); + { + int ret = slice_from_s(z, 1, s_35); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - z->lb = mlimit4; + z->lb = v_4; } return 1; } static int r_un_double(struct SN_env * z) { - { int m_test1 = z->l - z->c; + { + int v_1 = z->l - z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1069056 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_8, 5)) return 0; - z->c = z->l - m_test1; + if (!find_among_b(z, a_9, 5, 0)) return 0; + z->c = z->l - v_1; } z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_un_accent(struct SN_env * z) { - { int i = 1; - while(1) { + { + int v_1 = 1; + while (1) { if (out_grouping_b_U(z, g_v, 97, 251, 0)) goto lab0; - i--; + v_1--; continue; lab0: break; } - if (i > 0) return 0; + if (v_1 > 0) return 0; } z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (!(eq_s_b(z, 2, s_35))) goto lab2; - goto lab1; - lab2: - z->c = z->l - m1; - if (!(eq_s_b(z, 2, s_36))) return 0; - } -lab1: + do { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_36))) goto lab1; + break; + lab1: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_37))) return 0; + } while (0); z->bra = z->c; - { int ret = slice_from_s(z, 1, s_37); + { + int ret = slice_from_s(z, 1, s_38); if (ret < 0) return ret; } return 1; } extern int french_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_elisions(z); + { + int v_1 = z->c; + { + int ret = r_elisions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - { int ret = r_prelude(z); + { + int v_2 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c2; + z->c = v_2; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int m6 = z->l - z->c; (void)m6; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab4; + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; + { + int v_5 = z->l - z->c; + do { + int v_6 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab3; - lab4: - z->c = z->l - m6; - { int ret = r_i_verb_suffix(z); - if (ret == 0) goto lab5; + break; + lab2: + z->c = z->l - v_6; + { + int ret = r_i_verb_suffix(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - goto lab3; - lab5: - z->c = z->l - m6; - { int ret = r_verb_suffix(z); - if (ret == 0) goto lab2; + break; + lab3: + z->c = z->l - v_6; + { + int ret = r_verb_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - } - lab3: - z->c = z->l - m5; - { int m7 = z->l - z->c; (void)m7; + } while (0); + z->c = z->l - v_5; + { + int v_7 = z->l - z->c; z->ket = z->c; - { int m8 = z->l - z->c; (void)m8; - if (z->c <= z->lb || z->p[z->c - 1] != 'Y') goto lab8; + do { + int v_8 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'Y') goto lab5; z->c--; z->bra = z->c; - { int ret = slice_from_s(z, 1, s_38); + { + int ret = slice_from_s(z, 1, s_39); if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m8; - if (!(eq_s_b(z, 2, s_39))) { z->c = z->l - m7; goto lab6; } + break; + lab5: + z->c = z->l - v_8; + if (!(eq_s_b(z, 2, s_40))) { z->c = z->l - v_7; goto lab4; } z->bra = z->c; - { int ret = slice_from_s(z, 1, s_40); + { + int ret = slice_from_s(z, 1, s_41); if (ret < 0) return ret; } - } - lab7: - lab6: + } while (0); + lab4: ; } } - goto lab1; - lab2: - z->c = z->l - m4; - { int ret = r_residual_suffix(z); + break; + lab1: + z->c = z->l - v_4; + { + int ret = r_residual_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_un_double(z); + { + int v_9 = z->l - z->c; + { + int ret = r_un_double(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_un_accent(z); + { + int v_10 = z->l - z->c; + { + int ret = r_un_accent(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } z->c = z->lb; - { int c11 = z->c; - { int ret = r_postlude(z); + { + int v_11 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c11; + z->c = v_11; } return 1; } -extern struct SN_env * french_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * french_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void french_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void french_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_german.c b/src/backend/snowball/libstemmer/stem_UTF_8_german.c index 6bb0ac050b431..f47855aba1d3b 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_german.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_german.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from german.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_german.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,38 +21,43 @@ extern int german_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_standard_suffix(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * german_UTF_8_create_env(void); -extern void german_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'U' }; +static const symbol s_1[] = { 'Y' }; +static const symbol s_2[] = { 's', 's' }; +static const symbol s_3[] = { 0xC3, 0xA4 }; +static const symbol s_4[] = { 0xC3, 0xB6 }; +static const symbol s_5[] = { 0xC3, 0xBC }; +static const symbol s_6[] = { 'y' }; +static const symbol s_7[] = { 'u' }; +static const symbol s_8[] = { 'a' }; +static const symbol s_9[] = { 'o' }; +static const symbol s_10[] = { 's', 'y', 's', 't' }; +static const symbol s_11[] = { 'n', 'i', 's' }; +static const symbol s_12[] = { 'l' }; +static const symbol s_13[] = { 'i', 'g' }; +static const symbol s_14[] = { 'e', 'r' }; +static const symbol s_15[] = { 'e', 'n' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 'a', 'e' }; static const symbol s_0_2[2] = { 'o', 'e' }; static const symbol s_0_3[2] = { 'q', 'u' }; static const symbol s_0_4[2] = { 'u', 'e' }; static const symbol s_0_5[2] = { 0xC3, 0x9F }; - -static const struct among a_0[6] = -{ -{ 0, 0, -1, 5, 0}, -{ 2, s_0_1, 0, 2, 0}, -{ 2, s_0_2, 0, 3, 0}, -{ 2, s_0_3, 0, -1, 0}, -{ 2, s_0_4, 0, 4, 0}, -{ 2, s_0_5, 0, 1, 0} +static const struct among a_0[6] = { +{ 0, 0, 0, 5, 0}, +{ 2, s_0_1, -1, 2, 0}, +{ 2, s_0_2, -2, 3, 0}, +{ 2, s_0_3, -3, -1, 0}, +{ 2, s_0_4, -4, 4, 0}, +{ 2, s_0_5, -5, 1, 0} }; static const symbol s_1_1[1] = { 'U' }; @@ -48,15 +65,13 @@ static const symbol s_1_2[1] = { 'Y' }; static const symbol s_1_3[2] = { 0xC3, 0xA4 }; static const symbol s_1_4[2] = { 0xC3, 0xB6 }; static const symbol s_1_5[2] = { 0xC3, 0xBC }; - -static const struct among a_1[6] = -{ -{ 0, 0, -1, 5, 0}, -{ 1, s_1_1, 0, 2, 0}, -{ 1, s_1_2, 0, 1, 0}, -{ 2, s_1_3, 0, 3, 0}, -{ 2, s_1_4, 0, 4, 0}, -{ 2, s_1_5, 0, 2, 0} +static const struct among a_1[6] = { +{ 0, 0, 0, 5, 0}, +{ 1, s_1_1, -1, 2, 0}, +{ 1, s_1_2, -2, 1, 0}, +{ 2, s_1_3, -3, 3, 0}, +{ 2, s_1_4, -4, 4, 0}, +{ 2, s_1_5, -5, 2, 0} }; static const symbol s_2_0[1] = { 'e' }; @@ -70,248 +85,252 @@ static const symbol s_2_7[2] = { 'e', 'r' }; static const symbol s_2_8[1] = { 's' }; static const symbol s_2_9[2] = { 'e', 's' }; static const symbol s_2_10[3] = { 'l', 'n', 's' }; - -static const struct among a_2[11] = -{ -{ 1, s_2_0, -1, 3, 0}, -{ 2, s_2_1, -1, 1, 0}, -{ 2, s_2_2, -1, 3, 0}, -{ 7, s_2_3, 2, 2, 0}, -{ 4, s_2_4, -1, 2, 0}, -{ 2, s_2_5, -1, 5, 0}, -{ 3, s_2_6, -1, 2, 0}, -{ 2, s_2_7, -1, 2, 0}, -{ 1, s_2_8, -1, 4, 0}, -{ 2, s_2_9, 8, 3, 0}, -{ 3, s_2_10, 8, 5, 0} +static const struct among a_2[11] = { +{ 1, s_2_0, 0, 3, 0}, +{ 2, s_2_1, 0, 1, 0}, +{ 2, s_2_2, 0, 3, 0}, +{ 7, s_2_3, -1, 2, 0}, +{ 4, s_2_4, 0, 2, 0}, +{ 2, s_2_5, 0, 5, 0}, +{ 3, s_2_6, 0, 2, 0}, +{ 2, s_2_7, 0, 2, 0}, +{ 1, s_2_8, 0, 4, 0}, +{ 2, s_2_9, -1, 3, 0}, +{ 3, s_2_10, -2, 5, 0} }; -static const symbol s_3_0[2] = { 'e', 'n' }; -static const symbol s_3_1[2] = { 'e', 'r' }; -static const symbol s_3_2[2] = { 's', 't' }; -static const symbol s_3_3[3] = { 'e', 's', 't' }; - -static const struct among a_3[4] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 1, 0}, -{ 2, s_3_2, -1, 2, 0}, -{ 3, s_3_3, 2, 1, 0} +static const symbol s_3_0[4] = { 't', 'i', 'c', 'k' }; +static const symbol s_3_1[4] = { 'p', 'l', 'a', 'n' }; +static const symbol s_3_2[6] = { 'g', 'e', 'o', 'r', 'd', 'n' }; +static const symbol s_3_3[6] = { 'i', 'n', 't', 'e', 'r', 'n' }; +static const symbol s_3_4[2] = { 't', 'r' }; +static const struct among a_3[5] = { +{ 4, s_3_0, 0, -1, 0}, +{ 4, s_3_1, 0, -1, 0}, +{ 6, s_3_2, 0, -1, 0}, +{ 6, s_3_3, 0, -1, 0}, +{ 2, s_3_4, 0, -1, 0} }; -static const symbol s_4_0[2] = { 'i', 'g' }; -static const symbol s_4_1[4] = { 'l', 'i', 'c', 'h' }; - -static const struct among a_4[2] = -{ -{ 2, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 1, 0} +static const symbol s_4_0[2] = { 'e', 'n' }; +static const symbol s_4_1[2] = { 'e', 'r' }; +static const symbol s_4_2[2] = { 'e', 't' }; +static const symbol s_4_3[2] = { 's', 't' }; +static const symbol s_4_4[3] = { 'e', 's', 't' }; +static const struct among a_4[5] = { +{ 2, s_4_0, 0, 1, 0}, +{ 2, s_4_1, 0, 1, 0}, +{ 2, s_4_2, 0, 3, 0}, +{ 2, s_4_3, 0, 2, 0}, +{ 3, s_4_4, -1, 1, 0} }; -static const symbol s_5_0[3] = { 'e', 'n', 'd' }; -static const symbol s_5_1[2] = { 'i', 'g' }; -static const symbol s_5_2[3] = { 'u', 'n', 'g' }; -static const symbol s_5_3[4] = { 'l', 'i', 'c', 'h' }; -static const symbol s_5_4[4] = { 'i', 's', 'c', 'h' }; -static const symbol s_5_5[2] = { 'i', 'k' }; -static const symbol s_5_6[4] = { 'h', 'e', 'i', 't' }; -static const symbol s_5_7[4] = { 'k', 'e', 'i', 't' }; +static const symbol s_5_0[2] = { 'i', 'g' }; +static const symbol s_5_1[4] = { 'l', 'i', 'c', 'h' }; +static const struct among a_5[2] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0} +}; -static const struct among a_5[8] = -{ -{ 3, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 2, 0}, -{ 3, s_5_2, -1, 1, 0}, -{ 4, s_5_3, -1, 3, 0}, -{ 4, s_5_4, -1, 2, 0}, -{ 2, s_5_5, -1, 2, 0}, -{ 4, s_5_6, -1, 3, 0}, -{ 4, s_5_7, -1, 4, 0} +static const symbol s_6_0[3] = { 'e', 'n', 'd' }; +static const symbol s_6_1[2] = { 'i', 'g' }; +static const symbol s_6_2[3] = { 'u', 'n', 'g' }; +static const symbol s_6_3[4] = { 'l', 'i', 'c', 'h' }; +static const symbol s_6_4[4] = { 'i', 's', 'c', 'h' }; +static const symbol s_6_5[2] = { 'i', 'k' }; +static const symbol s_6_6[4] = { 'h', 'e', 'i', 't' }; +static const symbol s_6_7[4] = { 'k', 'e', 'i', 't' }; +static const struct among a_6[8] = { +{ 3, s_6_0, 0, 1, 0}, +{ 2, s_6_1, 0, 2, 0}, +{ 3, s_6_2, 0, 1, 0}, +{ 4, s_6_3, 0, 3, 0}, +{ 4, s_6_4, 0, 2, 0}, +{ 2, s_6_5, 0, 2, 0}, +{ 4, s_6_6, 0, 3, 0}, +{ 4, s_6_7, 0, 4, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 32, 8 }; +static const unsigned char g_et_ending[] = { 1, 128, 198, 227, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128 }; + static const unsigned char g_s_ending[] = { 117, 30, 5 }; static const unsigned char g_st_ending[] = { 117, 30, 4 }; -static const symbol s_0[] = { 'U' }; -static const symbol s_1[] = { 'Y' }; -static const symbol s_2[] = { 's', 's' }; -static const symbol s_3[] = { 0xC3, 0xA4 }; -static const symbol s_4[] = { 0xC3, 0xB6 }; -static const symbol s_5[] = { 0xC3, 0xBC }; -static const symbol s_6[] = { 'y' }; -static const symbol s_7[] = { 'u' }; -static const symbol s_8[] = { 'a' }; -static const symbol s_9[] = { 'o' }; -static const symbol s_10[] = { 's', 'y', 's', 't' }; -static const symbol s_11[] = { 'n', 'i', 's' }; -static const symbol s_12[] = { 'l' }; -static const symbol s_13[] = { 'i', 'g' }; -static const symbol s_14[] = { 'e', 'r' }; -static const symbol s_15[] = { 'e', 'n' }; - static int r_prelude(struct SN_env * z) { int among_var; - { int c_test1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab1; z->bra = z->c; - { int c4 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab3; + do { + int v_4 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab2; z->c++; z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab3; - { int ret = slice_from_s(z, 1, s_0); + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab2; + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } - goto lab2; - lab3: - z->c = c4; + break; + lab2: + z->c = v_4; if (z->c == z->l || z->p[z->c] != 'y') goto lab1; z->c++; z->ket = z->c; if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab1; - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } - } - lab2: - z->c = c3; + } while (0); + z->c = v_3; break; lab1: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } } continue; lab0: - z->c = c2; + z->c = v_2; break; } - z->c = c_test1; + z->c = v_1; } - while(1) { - int c5 = z->c; + while (1) { + int v_5 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 6); + among_var = find_among(z, a_0, 6, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_3); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_4); + { + int ret = slice_from_s(z, 2, s_4); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } break; case 5: - { int ret = skip_utf8(z->p, z->c, z->l, 1); - if (ret < 0) goto lab4; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) goto lab3; z->c = ret; } break; } continue; - lab4: - z->c = c5; + lab3: + z->c = v_5; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - { int c_test1 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); + int i_x; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); if (ret < 0) return 0; z->c = ret; } - z->I[0] = z->c; - z->c = c_test1; + i_x = z->c; + z->c = v_1; } - { int ret = out_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - z->I[2] = z->c; - - if (z->I[2] >= z->I[0]) goto lab0; - z->I[2] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: - { int ret = out_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 252, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; + ((SN_local *)z)->i_p2 = z->c; return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; - among_var = find_among(z, a_1, 6); + among_var = find_among(z, a_1, 6, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; case 5: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -319,60 +338,68 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_R1(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_standard_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((811040 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; - among_var = find_among_b(z, a_2, 11); + among_var = find_among_b(z, a_2, 11, 0); if (!among_var) goto lab0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; if (!(eq_s_b(z, 4, s_10))) goto lab1; goto lab0; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - m3; goto lab2; } + if (z->c <= z->lb || z->p[z->c - 1] != 's') { z->c = z->l - v_3; goto lab2; } z->c--; z->bra = z->c; - if (!(eq_s_b(z, 3, s_11))) { z->c = z->l - m3; goto lab2; } - { int ret = slice_del(z); + if (!(eq_s_b(z, 3, s_11))) { z->c = z->l - v_3; goto lab2; } + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -381,138 +408,181 @@ static int r_standard_suffix(struct SN_env * z) { break; case 4: if (in_grouping_b_U(z, g_s_ending, 98, 116, 0)) goto lab0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1327104 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab3; - among_var = find_among_b(z, a_3, 4); + among_var = find_among_b(z, a_4, 5, 0); if (!among_var) goto lab3; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b_U(z, g_st_ending, 98, 116, 0)) goto lab3; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 3); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 3); if (ret < 0) goto lab3; z->c = ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int v_5 = z->l - z->c; + if (in_grouping_b_U(z, g_et_ending, 85, 228, 0)) goto lab3; + z->c = z->l - v_5; + } + { + int v_6 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((280576 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab4; + if (!find_among_b(z, a_3, 5, 0)) goto lab4; + goto lab3; + lab4: + z->c = z->l - v_6; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } lab3: - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; + { + int v_7 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1051024 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab4; - among_var = find_among_b(z, a_5, 8); - if (!among_var) goto lab4; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1051024 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab5; + among_var = find_among_b(z, a_6, 8, 0); + if (!among_var) goto lab5; z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) goto lab4; + { + int ret = r_R2(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; + { + int v_8 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_13))) { z->c = z->l - m6; goto lab5; } + if (!(eq_s_b(z, 2, s_13))) { z->c = z->l - v_8; goto lab6; } z->bra = z->c; - { int m7 = z->l - z->c; (void)m7; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab6; + { + int v_9 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab7; z->c--; - { z->c = z->l - m6; goto lab5; } - lab6: - z->c = z->l - m7; + { z->c = z->l - v_8; goto lab6; } + lab7: + z->c = z->l - v_9; } - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m6; goto lab5; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_8; goto lab6; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab5: + lab6: ; } break; case 2: - { int m8 = z->l - z->c; (void)m8; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab7; + { + int v_10 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab8; z->c--; - goto lab4; - lab7: - z->c = z->l - m8; + goto lab5; + lab8: + z->c = z->l - v_10; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; + { + int v_11 = z->l - z->c; z->ket = z->c; - { int m10 = z->l - z->c; (void)m10; + do { + int v_12 = z->l - z->c; if (!(eq_s_b(z, 2, s_14))) goto lab10; - goto lab9; + break; lab10: - z->c = z->l - m10; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m9; goto lab8; } - } - lab9: + z->c = z->l - v_12; + if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - v_11; goto lab9; } + } while (0); z->bra = z->c; - { int ret = r_R1(z); - if (ret == 0) { z->c = z->l - m9; goto lab8; } + { + int ret = r_R1(z); + if (ret == 0) { z->c = z->l - v_11; goto lab9; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab8: + lab9: ; } break; case 4: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m11 = z->l - z->c; (void)m11; + { + int v_13 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 103 && z->p[z->c - 1] != 104)) { z->c = z->l - m11; goto lab11; } - if (!find_among_b(z, a_4, 2)) { z->c = z->l - m11; goto lab11; } + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 103 && z->p[z->c - 1] != 104)) { z->c = z->l - v_13; goto lab11; } + if (!find_among_b(z, a_5, 2, 0)) { z->c = z->l - v_13; goto lab11; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m11; goto lab11; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_13; goto lab11; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab11: @@ -520,42 +590,56 @@ static int r_standard_suffix(struct SN_env * z) { } break; } - lab4: - z->c = z->l - m5; + lab5: + z->c = z->l - v_7; } return 1; } extern int german_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_prelude(z); + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - { int ret = r_mark_regions(z); + { + int v_2 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c2; + z->c = v_2; } z->lb = z->c; z->c = z->l; - - - { int ret = r_standard_suffix(z); + { + int ret = r_standard_suffix(z); if (ret < 0) return ret; } z->c = z->lb; - { int c3 = z->c; - { int ret = r_postlude(z); + { + int v_3 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c3; + z->c = v_3; } return 1; } -extern struct SN_env * german_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * german_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void german_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void german_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_greek.c b/src/backend/snowball/libstemmer/stem_UTF_8_greek.c index 33e5465267ca4..fe62148a536ee 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_greek.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_greek.c @@ -1,6 +1,25 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from greek.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_greek.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + unsigned char b_test1; +}; + +typedef struct SN_local SN_local; + +#ifdef __cplusplus +extern "C" { +#endif +extern int greek_UTF_8_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif static int r_step_7(struct SN_env * z); static int r_step_6(struct SN_env * z); @@ -36,25 +55,117 @@ static int r_step_s2(struct SN_env * z); static int r_step_s1(struct SN_env * z); static int r_has_min_length(struct SN_env * z); static int r_tolower(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif -extern int greek_UTF_8_stem(struct SN_env * z); -#ifdef __cplusplus -} -#endif -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * greek_UTF_8_create_env(void); -extern void greek_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xCE, 0xB1 }; +static const symbol s_1[] = { 0xCE, 0xB2 }; +static const symbol s_2[] = { 0xCE, 0xB3 }; +static const symbol s_3[] = { 0xCE, 0xB4 }; +static const symbol s_4[] = { 0xCE, 0xB5 }; +static const symbol s_5[] = { 0xCE, 0xB6 }; +static const symbol s_6[] = { 0xCE, 0xB7 }; +static const symbol s_7[] = { 0xCE, 0xB8 }; +static const symbol s_8[] = { 0xCE, 0xB9 }; +static const symbol s_9[] = { 0xCE, 0xBA }; +static const symbol s_10[] = { 0xCE, 0xBB }; +static const symbol s_11[] = { 0xCE, 0xBC }; +static const symbol s_12[] = { 0xCE, 0xBD }; +static const symbol s_13[] = { 0xCE, 0xBE }; +static const symbol s_14[] = { 0xCE, 0xBF }; +static const symbol s_15[] = { 0xCF, 0x80 }; +static const symbol s_16[] = { 0xCF, 0x81 }; +static const symbol s_17[] = { 0xCF, 0x83 }; +static const symbol s_18[] = { 0xCF, 0x84 }; +static const symbol s_19[] = { 0xCF, 0x85 }; +static const symbol s_20[] = { 0xCF, 0x86 }; +static const symbol s_21[] = { 0xCF, 0x87 }; +static const symbol s_22[] = { 0xCF, 0x88 }; +static const symbol s_23[] = { 0xCF, 0x89 }; +static const symbol s_24[] = { 0xCF, 0x86, 0xCE, 0xB1 }; +static const symbol s_25[] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB1 }; +static const symbol s_26[] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF }; +static const symbol s_27[] = { 0xCF, 0x83, 0xCE, 0xBF }; +static const symbol s_28[] = { 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF }; +static const symbol s_29[] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xB5 }; +static const symbol s_30[] = { 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_31[] = { 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_32[] = { 0xCF, 0x86, 0xCF, 0x89 }; +static const symbol s_33[] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_34[] = { 0xCE, 0xB3, 0xCE, 0xB5, 0xCE, 0xB3, 0xCE, 0xBF, 0xCE, 0xBD }; +static const symbol s_35[] = { 0xCE, 0xB9 }; +static const symbol s_36[] = { 0xCE, 0xB9, 0xCE, 0xB6 }; +static const symbol s_37[] = { 0xCF, 0x89, 0xCE, 0xBD }; +static const symbol s_38[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB1 }; +static const symbol s_39[] = { 0xCE, 0xB9, 0xCF, 0x83 }; +static const symbol s_40[] = { 0xCE, 0xB9 }; +static const symbol s_41[] = { 0xCE, 0xB9, 0xCF, 0x83 }; +static const symbol s_42[] = { 0xCE, 0xB9 }; +static const symbol s_43[] = { 0xCE, 0xB9 }; +static const symbol s_44[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_45[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC }; +static const symbol s_46[] = { 0xCE, 0xB9 }; +static const symbol s_47[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xBD, 0xCF, 0x89, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_48[] = { 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF, 0xCE, 0xBC }; +static const symbol s_49[] = { 0xCE, 0xB3, 0xCE, 0xBD, 0xCF, 0x89, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_50[] = { 0xCE, 0xB5, 0xCE, 0xB8, 0xCE, 0xBD }; +static const symbol s_51[] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xBB, 0xCE, 0xB5, 0xCE, 0xBA, 0xCF, 0x84 }; +static const symbol s_52[] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x80, 0xCF, 0x84 }; +static const symbol s_53[] = { 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54[] = { 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xB5, 0xCE, 0xBE, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB4, 0xCF, 0x81 }; +static const symbol s_55[] = { 0xCE, 0xB2, 0xCF, 0x85, 0xCE, 0xB6, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84 }; +static const symbol s_56[] = { 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB1, 0xCF, 0x84, 0xCF, 0x81 }; +static const symbol s_57[] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA }; +static const symbol s_58[] = { 0xCE, 0xB1, 0xCE, 0xBA }; +static const symbol s_59[] = { 0xCE, 0xB9, 0xCF, 0x84, 0xCF, 0x83 }; +static const symbol s_60[] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x81 }; +static const symbol s_61[] = { 0xCE, 0xB9, 0xCF, 0x84, 0xCF, 0x83 }; +static const symbol s_62[] = { 0xCE, 0xB9, 0xCE, 0xB4 }; +static const symbol s_63[] = { 0xCE, 0xB9, 0xCE, 0xB4 }; +static const symbol s_64[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBA }; +static const symbol s_65[] = { 0xCE, 0xB1, 0xCE, 0xB4 }; +static const symbol s_66[] = { 0xCE, 0xB5, 0xCE, 0xB4 }; +static const symbol s_67[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xB4 }; +static const symbol s_68[] = { 0xCE, 0xB5 }; +static const symbol s_69[] = { 0xCE, 0xB9 }; +static const symbol s_70[] = { 0xCE, 0xB9, 0xCE, 0xBA }; +static const symbol s_71[] = { 0xCE, 0xB9, 0xCE, 0xBA }; +static const symbol s_72[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; +static const symbol s_73[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBC }; +static const symbol s_74[] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; +static const symbol s_75[] = { 0xCE, 0xB1, 0xCE, 0xBC }; +static const symbol s_76[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_77[] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; +static const symbol s_78[] = { 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_79[] = { 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_80[] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_81[] = { 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_82[] = { 0xCE, 0xB5, 0xCF, 0x84 }; +static const symbol s_83[] = { 0xCE, 0xB5, 0xCF, 0x84 }; +static const symbol s_84[] = { 0xCE, 0xB5, 0xCF, 0x84 }; +static const symbol s_85[] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x87 }; +static const symbol s_86[] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84 }; +static const symbol s_87[] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xB5 }; +static const symbol s_88[] = { 0xCF, 0x89, 0xCE, 0xBD, 0xCF, 0x84 }; +static const symbol s_89[] = { 0xCE, 0xBF, 0xCE, 0xBD }; +static const symbol s_90[] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_91[] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_92[] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_93[] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_94[] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_95[] = { 0xCE, 0xB7, 0xCE, 0xBA }; +static const symbol s_96[] = { 0xCE, 0xB7, 0xCE, 0xBA }; +static const symbol s_97[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_98[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_99[] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBB }; +static const symbol s_100[] = { 0xCE, 0xB1, 0xCE, 0xB3 }; +static const symbol s_101[] = { 0xCE, 0xB1, 0xCE, 0xB3 }; +static const symbol s_102[] = { 0xCE, 0xB1, 0xCE, 0xB3 }; +static const symbol s_103[] = { 0xCE, 0xB7, 0xCF, 0x83 }; +static const symbol s_104[] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_105[] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_106[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_107[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC }; +static const symbol s_108[] = { 0xCE, 0xBC, 0xCE, 0xB1 }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 0xCF, 0x82 }; static const symbol s_0_2[2] = { 0xCE, 0x86 }; static const symbol s_0_3[2] = { 0xCE, 0x88 }; @@ -100,55 +211,53 @@ static const symbol s_0_42[2] = { 0xCE, 0xAD }; static const symbol s_0_43[2] = { 0xCE, 0xAE }; static const symbol s_0_44[2] = { 0xCE, 0xAF }; static const symbol s_0_45[2] = { 0xCE, 0xB0 }; - -static const struct among a_0[46] = -{ -{ 0, 0, -1, 25, 0}, -{ 2, s_0_1, 0, 18, 0}, -{ 2, s_0_2, 0, 1, 0}, -{ 2, s_0_3, 0, 5, 0}, -{ 2, s_0_4, 0, 7, 0}, -{ 2, s_0_5, 0, 9, 0}, -{ 2, s_0_6, 0, 7, 0}, -{ 2, s_0_7, 0, 20, 0}, -{ 2, s_0_8, 0, 15, 0}, -{ 2, s_0_9, 0, 15, 0}, -{ 2, s_0_10, 0, 20, 0}, -{ 2, s_0_11, 0, 20, 0}, -{ 2, s_0_12, 0, 24, 0}, -{ 2, s_0_13, 0, 24, 0}, -{ 2, s_0_14, 0, 7, 0}, -{ 2, s_0_15, 0, 1, 0}, -{ 2, s_0_16, 0, 2, 0}, -{ 2, s_0_17, 0, 3, 0}, -{ 2, s_0_18, 0, 4, 0}, -{ 2, s_0_19, 0, 5, 0}, -{ 2, s_0_20, 0, 6, 0}, -{ 2, s_0_21, 0, 7, 0}, -{ 2, s_0_22, 0, 8, 0}, -{ 2, s_0_23, 0, 9, 0}, -{ 2, s_0_24, 0, 10, 0}, -{ 2, s_0_25, 0, 11, 0}, -{ 2, s_0_26, 0, 12, 0}, -{ 2, s_0_27, 0, 13, 0}, -{ 2, s_0_28, 0, 14, 0}, -{ 2, s_0_29, 0, 15, 0}, -{ 2, s_0_30, 0, 16, 0}, -{ 2, s_0_31, 0, 17, 0}, -{ 2, s_0_32, 0, 18, 0}, -{ 2, s_0_33, 0, 19, 0}, -{ 2, s_0_34, 0, 20, 0}, -{ 2, s_0_35, 0, 21, 0}, -{ 2, s_0_36, 0, 22, 0}, -{ 2, s_0_37, 0, 23, 0}, -{ 2, s_0_38, 0, 24, 0}, -{ 2, s_0_39, 0, 9, 0}, -{ 2, s_0_40, 0, 20, 0}, -{ 2, s_0_41, 0, 1, 0}, -{ 2, s_0_42, 0, 5, 0}, -{ 2, s_0_43, 0, 7, 0}, -{ 2, s_0_44, 0, 9, 0}, -{ 2, s_0_45, 0, 20, 0} +static const struct among a_0[46] = { +{ 0, 0, 0, 25, 0}, +{ 2, s_0_1, -1, 18, 0}, +{ 2, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, 5, 0}, +{ 2, s_0_4, -4, 7, 0}, +{ 2, s_0_5, -5, 9, 0}, +{ 2, s_0_6, -6, 7, 0}, +{ 2, s_0_7, -7, 20, 0}, +{ 2, s_0_8, -8, 15, 0}, +{ 2, s_0_9, -9, 15, 0}, +{ 2, s_0_10, -10, 20, 0}, +{ 2, s_0_11, -11, 20, 0}, +{ 2, s_0_12, -12, 24, 0}, +{ 2, s_0_13, -13, 24, 0}, +{ 2, s_0_14, -14, 7, 0}, +{ 2, s_0_15, -15, 1, 0}, +{ 2, s_0_16, -16, 2, 0}, +{ 2, s_0_17, -17, 3, 0}, +{ 2, s_0_18, -18, 4, 0}, +{ 2, s_0_19, -19, 5, 0}, +{ 2, s_0_20, -20, 6, 0}, +{ 2, s_0_21, -21, 7, 0}, +{ 2, s_0_22, -22, 8, 0}, +{ 2, s_0_23, -23, 9, 0}, +{ 2, s_0_24, -24, 10, 0}, +{ 2, s_0_25, -25, 11, 0}, +{ 2, s_0_26, -26, 12, 0}, +{ 2, s_0_27, -27, 13, 0}, +{ 2, s_0_28, -28, 14, 0}, +{ 2, s_0_29, -29, 15, 0}, +{ 2, s_0_30, -30, 16, 0}, +{ 2, s_0_31, -31, 17, 0}, +{ 2, s_0_32, -32, 18, 0}, +{ 2, s_0_33, -33, 19, 0}, +{ 2, s_0_34, -34, 20, 0}, +{ 2, s_0_35, -35, 21, 0}, +{ 2, s_0_36, -36, 22, 0}, +{ 2, s_0_37, -37, 23, 0}, +{ 2, s_0_38, -38, 24, 0}, +{ 2, s_0_39, -39, 9, 0}, +{ 2, s_0_40, -40, 20, 0}, +{ 2, s_0_41, -41, 1, 0}, +{ 2, s_0_42, -42, 5, 0}, +{ 2, s_0_43, -43, 7, 0}, +{ 2, s_0_44, -44, 9, 0}, +{ 2, s_0_45, -45, 20, 0} }; static const symbol s_1_0[16] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCF, 0x89, 0xCF, 0x83 }; @@ -191,49 +300,47 @@ static const symbol s_1_36[14] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB static const symbol s_1_37[12] = { 0xCF, 0x83, 0xCE, 0xBF, 0xCE, 0xB3, 0xCE, 0xB9, 0xCF, 0x89, 0xCE, 0xBD }; static const symbol s_1_38[16] = { 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF, 0xCE, 0xB3, 0xCE, 0xB9, 0xCF, 0x89, 0xCE, 0xBD }; static const symbol s_1_39[14] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF, 0xCE, 0xB3, 0xCE, 0xB9, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_1[40] = -{ -{ 16, s_1_0, -1, 10, 0}, -{ 6, s_1_1, -1, 9, 0}, -{ 10, s_1_2, -1, 7, 0}, -{ 10, s_1_3, -1, 8, 0}, -{ 10, s_1_4, -1, 6, 0}, -{ 20, s_1_5, -1, 10, 0}, -{ 10, s_1_6, -1, 9, 0}, -{ 14, s_1_7, -1, 7, 0}, -{ 14, s_1_8, -1, 8, 0}, -{ 14, s_1_9, -1, 6, 0}, -{ 18, s_1_10, -1, 11, 0}, -{ 14, s_1_11, -1, 11, 0}, -{ 12, s_1_12, -1, 1, 0}, -{ 14, s_1_13, -1, 2, 0}, -{ 12, s_1_14, -1, 4, 0}, -{ 16, s_1_15, -1, 5, 0}, -{ 14, s_1_16, -1, 3, 0}, -{ 18, s_1_17, -1, 10, 0}, -{ 8, s_1_18, -1, 9, 0}, -{ 12, s_1_19, -1, 7, 0}, -{ 12, s_1_20, -1, 8, 0}, -{ 12, s_1_21, -1, 6, 0}, -{ 16, s_1_22, -1, 11, 0}, -{ 10, s_1_23, -1, 1, 0}, -{ 12, s_1_24, -1, 2, 0}, -{ 10, s_1_25, -1, 4, 0}, -{ 14, s_1_26, -1, 5, 0}, -{ 12, s_1_27, -1, 3, 0}, -{ 12, s_1_28, -1, 7, 0}, -{ 20, s_1_29, -1, 10, 0}, -{ 10, s_1_30, -1, 9, 0}, -{ 14, s_1_31, -1, 7, 0}, -{ 14, s_1_32, -1, 8, 0}, -{ 14, s_1_33, -1, 6, 0}, -{ 18, s_1_34, -1, 11, 0}, -{ 12, s_1_35, -1, 1, 0}, -{ 14, s_1_36, -1, 2, 0}, -{ 12, s_1_37, -1, 4, 0}, -{ 16, s_1_38, -1, 5, 0}, -{ 14, s_1_39, -1, 3, 0} +static const struct among a_1[40] = { +{ 16, s_1_0, 0, 10, 0}, +{ 6, s_1_1, 0, 9, 0}, +{ 10, s_1_2, 0, 7, 0}, +{ 10, s_1_3, 0, 8, 0}, +{ 10, s_1_4, 0, 6, 0}, +{ 20, s_1_5, 0, 10, 0}, +{ 10, s_1_6, 0, 9, 0}, +{ 14, s_1_7, 0, 7, 0}, +{ 14, s_1_8, 0, 8, 0}, +{ 14, s_1_9, 0, 6, 0}, +{ 18, s_1_10, 0, 11, 0}, +{ 14, s_1_11, 0, 11, 0}, +{ 12, s_1_12, 0, 1, 0}, +{ 14, s_1_13, 0, 2, 0}, +{ 12, s_1_14, 0, 4, 0}, +{ 16, s_1_15, 0, 5, 0}, +{ 14, s_1_16, 0, 3, 0}, +{ 18, s_1_17, 0, 10, 0}, +{ 8, s_1_18, 0, 9, 0}, +{ 12, s_1_19, 0, 7, 0}, +{ 12, s_1_20, 0, 8, 0}, +{ 12, s_1_21, 0, 6, 0}, +{ 16, s_1_22, 0, 11, 0}, +{ 10, s_1_23, 0, 1, 0}, +{ 12, s_1_24, 0, 2, 0}, +{ 10, s_1_25, 0, 4, 0}, +{ 14, s_1_26, 0, 5, 0}, +{ 12, s_1_27, 0, 3, 0}, +{ 12, s_1_28, 0, 7, 0}, +{ 20, s_1_29, 0, 10, 0}, +{ 10, s_1_30, 0, 9, 0}, +{ 14, s_1_31, 0, 7, 0}, +{ 14, s_1_32, 0, 8, 0}, +{ 14, s_1_33, 0, 6, 0}, +{ 18, s_1_34, 0, 11, 0}, +{ 12, s_1_35, 0, 1, 0}, +{ 14, s_1_36, 0, 2, 0}, +{ 12, s_1_37, 0, 4, 0}, +{ 16, s_1_38, 0, 5, 0}, +{ 14, s_1_39, 0, 3, 0} }; static const symbol s_2_0[2] = { 0xCF, 0x80 }; @@ -267,40 +374,38 @@ static const symbol s_2_27[2] = { 0xCE, 0xBC }; static const symbol s_2_28[8] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x81, 0xCE, 0xBD }; static const symbol s_2_29[8] = { 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; static const symbol s_2_30[14] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; - -static const struct among a_2[31] = -{ -{ 2, s_2_0, -1, 2, 0}, -{ 6, s_2_1, 0, 2, 0}, -{ 2, s_2_2, -1, 2, 0}, -{ 4, s_2_3, 2, 2, 0}, -{ 6, s_2_4, 3, 2, 0}, -{ 6, s_2_5, 2, 2, 0}, -{ 12, s_2_6, 2, 2, 0}, -{ 10, s_2_7, 2, 2, 0}, -{ 10, s_2_8, 2, 2, 0}, -{ 6, s_2_9, 2, 2, 0}, -{ 6, s_2_10, 2, 2, 0}, -{ 14, s_2_11, 2, 2, 0}, -{ 12, s_2_12, 2, 2, 0}, -{ 12, s_2_13, 2, 2, 0}, -{ 6, s_2_14, -1, 2, 0}, -{ 4, s_2_15, -1, 1, 0}, -{ 12, s_2_16, 15, 1, 0}, -{ 6, s_2_17, 15, 1, 0}, -{ 12, s_2_18, 15, 1, 0}, -{ 12, s_2_19, 15, 1, 0}, -{ 8, s_2_20, 15, 1, 0}, -{ 2, s_2_21, -1, 2, 0}, -{ 8, s_2_22, -1, 1, 0}, -{ 12, s_2_23, -1, 2, 0}, -{ 8, s_2_24, -1, 2, 0}, -{ 8, s_2_25, -1, 2, 0}, -{ 2, s_2_26, -1, 2, 0}, -{ 2, s_2_27, -1, 2, 0}, -{ 8, s_2_28, -1, 2, 0}, -{ 8, s_2_29, -1, 1, 0}, -{ 14, s_2_30, 29, 1, 0} +static const struct among a_2[31] = { +{ 2, s_2_0, 0, 2, 0}, +{ 6, s_2_1, -1, 2, 0}, +{ 2, s_2_2, 0, 2, 0}, +{ 4, s_2_3, -1, 2, 0}, +{ 6, s_2_4, -1, 2, 0}, +{ 6, s_2_5, -3, 2, 0}, +{ 12, s_2_6, -4, 2, 0}, +{ 10, s_2_7, -5, 2, 0}, +{ 10, s_2_8, -6, 2, 0}, +{ 6, s_2_9, -7, 2, 0}, +{ 6, s_2_10, -8, 2, 0}, +{ 14, s_2_11, -9, 2, 0}, +{ 12, s_2_12, -10, 2, 0}, +{ 12, s_2_13, -11, 2, 0}, +{ 6, s_2_14, 0, 2, 0}, +{ 4, s_2_15, 0, 1, 0}, +{ 12, s_2_16, -1, 1, 0}, +{ 6, s_2_17, -2, 1, 0}, +{ 12, s_2_18, -3, 1, 0}, +{ 12, s_2_19, -4, 1, 0}, +{ 8, s_2_20, -5, 1, 0}, +{ 2, s_2_21, 0, 2, 0}, +{ 8, s_2_22, 0, 1, 0}, +{ 12, s_2_23, 0, 2, 0}, +{ 8, s_2_24, 0, 2, 0}, +{ 8, s_2_25, 0, 2, 0}, +{ 2, s_2_26, 0, 2, 0}, +{ 2, s_2_27, 0, 2, 0}, +{ 8, s_2_28, 0, 2, 0}, +{ 8, s_2_29, 0, 1, 0}, +{ 14, s_2_30, -1, 1, 0} }; static const symbol s_3_0[8] = { 0xCE, 0xB9, 0xCE, 0xB6, 0xCE, 0xB5, 0xCF, 0x83 }; @@ -317,23 +422,21 @@ static const symbol s_3_10[10] = { 0xCE, 0xB9, 0xCE, 0xB6, 0xCE, 0xB1, 0xCE, 0xB static const symbol s_3_11[8] = { 0xCE, 0xB9, 0xCE, 0xB6, 0xCE, 0xB5, 0xCE, 0xB9 }; static const symbol s_3_12[10] = { 0xCE, 0xB9, 0xCE, 0xB6, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; static const symbol s_3_13[8] = { 0xCE, 0xB9, 0xCE, 0xB6, 0xCE, 0xB1, 0xCE, 0xBD }; - -static const struct among a_3[14] = -{ -{ 8, s_3_0, -1, 1, 0}, -{ 10, s_3_1, -1, 1, 0}, -{ 6, s_3_2, -1, 1, 0}, -{ 6, s_3_3, -1, 1, 0}, -{ 10, s_3_4, -1, 1, 0}, -{ 10, s_3_5, -1, 1, 0}, -{ 6, s_3_6, -1, 1, 0}, -{ 12, s_3_7, -1, 1, 0}, -{ 10, s_3_8, -1, 1, 0}, -{ 12, s_3_9, -1, 1, 0}, -{ 10, s_3_10, -1, 1, 0}, -{ 8, s_3_11, -1, 1, 0}, -{ 10, s_3_12, -1, 1, 0}, -{ 8, s_3_13, -1, 1, 0} +static const struct among a_3[14] = { +{ 8, s_3_0, 0, 1, 0}, +{ 10, s_3_1, 0, 1, 0}, +{ 6, s_3_2, 0, 1, 0}, +{ 6, s_3_3, 0, 1, 0}, +{ 10, s_3_4, 0, 1, 0}, +{ 10, s_3_5, 0, 1, 0}, +{ 6, s_3_6, 0, 1, 0}, +{ 12, s_3_7, 0, 1, 0}, +{ 10, s_3_8, 0, 1, 0}, +{ 12, s_3_9, 0, 1, 0}, +{ 10, s_3_10, 0, 1, 0}, +{ 8, s_3_11, 0, 1, 0}, +{ 10, s_3_12, 0, 1, 0}, +{ 8, s_3_13, 0, 1, 0} }; static const symbol s_4_0[2] = { 0xCF, 0x83 }; @@ -344,17 +447,15 @@ static const symbol s_4_4[4] = { 0xCE, 0xB2, 0xCE, 0xB9 }; static const symbol s_4_5[4] = { 0xCE, 0xBB, 0xCE, 0xB9 }; static const symbol s_4_6[4] = { 0xCE, 0xB1, 0xCE, 0xBB }; static const symbol s_4_7[4] = { 0xCE, 0xB5, 0xCE, 0xBD }; - -static const struct among a_4[8] = -{ -{ 2, s_4_0, -1, 1, 0}, -{ 2, s_4_1, -1, 1, 0}, -{ 4, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 1, 0}, -{ 4, s_4_4, -1, 1, 0}, -{ 4, s_4_5, -1, 1, 0}, -{ 4, s_4_6, -1, 1, 0}, -{ 4, s_4_7, -1, 1, 0} +static const struct among a_4[8] = { +{ 2, s_4_0, 0, 1, 0}, +{ 2, s_4_1, 0, 1, 0}, +{ 4, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 1, 0}, +{ 4, s_4_4, 0, 1, 0}, +{ 4, s_4_5, 0, 1, 0}, +{ 4, s_4_6, 0, 1, 0}, +{ 4, s_4_7, 0, 1, 0} }; static const symbol s_5_0[12] = { 0xCF, 0x89, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x83 }; @@ -364,16 +465,14 @@ static const symbol s_5_3[10] = { 0xCF, 0x89, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA static const symbol s_5_4[14] = { 0xCF, 0x89, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; static const symbol s_5_5[14] = { 0xCF, 0x89, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_5_6[12] = { 0xCF, 0x89, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; - -static const struct among a_5[7] = -{ -{ 12, s_5_0, -1, 1, 0}, -{ 10, s_5_1, -1, 1, 0}, -{ 14, s_5_2, -1, 1, 0}, -{ 10, s_5_3, -1, 1, 0}, -{ 14, s_5_4, -1, 1, 0}, -{ 14, s_5_5, -1, 1, 0}, -{ 12, s_5_6, -1, 1, 0} +static const struct among a_5[7] = { +{ 12, s_5_0, 0, 1, 0}, +{ 10, s_5_1, 0, 1, 0}, +{ 14, s_5_2, 0, 1, 0}, +{ 10, s_5_3, 0, 1, 0}, +{ 14, s_5_4, 0, 1, 0}, +{ 14, s_5_5, 0, 1, 0}, +{ 12, s_5_6, 0, 1, 0} }; static const symbol s_6_0[2] = { 0xCF, 0x80 }; @@ -408,41 +507,39 @@ static const symbol s_6_28[4] = { 0xCE, 0xB1, 0xCE, 0xBD }; static const symbol s_6_29[8] = { 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; static const symbol s_6_30[14] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; static const symbol s_6_31[6] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF }; - -static const struct among a_6[32] = -{ -{ 2, s_6_0, -1, 2, 0}, -{ 6, s_6_1, -1, 2, 0}, -{ 16, s_6_2, -1, 2, 0}, -{ 4, s_6_3, -1, 2, 0}, -{ 18, s_6_4, 3, 2, 0}, -{ 12, s_6_5, -1, 1, 0}, -{ 6, s_6_6, -1, 1, 0}, -{ 12, s_6_7, -1, 1, 0}, -{ 12, s_6_8, -1, 1, 0}, -{ 8, s_6_9, -1, 1, 0}, -{ 14, s_6_10, -1, 1, 0}, -{ 12, s_6_11, -1, 1, 0}, -{ 4, s_6_12, -1, 1, 0}, -{ 6, s_6_13, 12, 1, 0}, -{ 12, s_6_14, 13, 1, 0}, -{ 6, s_6_15, -1, 1, 0}, -{ 4, s_6_16, -1, 2, 0}, -{ 6, s_6_17, -1, 2, 0}, -{ 6, s_6_18, -1, 1, 0}, -{ 12, s_6_19, 18, 1, 0}, -{ 8, s_6_20, 18, 1, 0}, -{ 12, s_6_21, 20, 1, 0}, -{ 12, s_6_22, 18, 1, 0}, -{ 8, s_6_23, -1, 1, 0}, -{ 4, s_6_24, -1, 2, 0}, -{ 2, s_6_25, -1, 2, 0}, -{ 12, s_6_26, 25, 2, 0}, -{ 6, s_6_27, 25, 2, 0}, -{ 4, s_6_28, -1, 2, 0}, -{ 8, s_6_29, -1, 1, 0}, -{ 14, s_6_30, 29, 1, 0}, -{ 6, s_6_31, -1, 2, 0} +static const struct among a_6[32] = { +{ 2, s_6_0, 0, 2, 0}, +{ 6, s_6_1, 0, 2, 0}, +{ 16, s_6_2, 0, 2, 0}, +{ 4, s_6_3, 0, 2, 0}, +{ 18, s_6_4, -1, 2, 0}, +{ 12, s_6_5, 0, 1, 0}, +{ 6, s_6_6, 0, 1, 0}, +{ 12, s_6_7, 0, 1, 0}, +{ 12, s_6_8, 0, 1, 0}, +{ 8, s_6_9, 0, 1, 0}, +{ 14, s_6_10, 0, 1, 0}, +{ 12, s_6_11, 0, 1, 0}, +{ 4, s_6_12, 0, 1, 0}, +{ 6, s_6_13, -1, 1, 0}, +{ 12, s_6_14, -1, 1, 0}, +{ 6, s_6_15, 0, 1, 0}, +{ 4, s_6_16, 0, 2, 0}, +{ 6, s_6_17, 0, 2, 0}, +{ 6, s_6_18, 0, 1, 0}, +{ 12, s_6_19, -1, 1, 0}, +{ 8, s_6_20, -2, 1, 0}, +{ 12, s_6_21, -1, 1, 0}, +{ 12, s_6_22, -4, 1, 0}, +{ 8, s_6_23, 0, 1, 0}, +{ 4, s_6_24, 0, 2, 0}, +{ 2, s_6_25, 0, 2, 0}, +{ 12, s_6_26, -1, 2, 0}, +{ 6, s_6_27, -2, 2, 0}, +{ 4, s_6_28, 0, 2, 0}, +{ 8, s_6_29, 0, 1, 0}, +{ 14, s_6_30, -1, 1, 0}, +{ 6, s_6_31, 0, 2, 0} }; static const symbol s_7_0[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x83 }; @@ -452,16 +549,14 @@ static const symbol s_7_3[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x84 static const symbol s_7_4[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; static const symbol s_7_5[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_7_6[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; - -static const struct among a_7[7] = -{ -{ 8, s_7_0, -1, 1, 0}, -{ 6, s_7_1, -1, 1, 0}, -{ 6, s_7_2, -1, 1, 0}, -{ 10, s_7_3, -1, 1, 0}, -{ 10, s_7_4, -1, 1, 0}, -{ 10, s_7_5, -1, 1, 0}, -{ 8, s_7_6, -1, 1, 0} +static const struct among a_7[7] = { +{ 8, s_7_0, 0, 1, 0}, +{ 6, s_7_1, 0, 1, 0}, +{ 6, s_7_2, 0, 1, 0}, +{ 10, s_7_3, 0, 1, 0}, +{ 10, s_7_4, 0, 1, 0}, +{ 10, s_7_5, 0, 1, 0}, +{ 8, s_7_6, 0, 1, 0} }; static const symbol s_8_0[12] = { 0xCE, 0xBE, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xB1 }; @@ -483,28 +578,26 @@ static const symbol s_8_15[12] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xB static const symbol s_8_16[8] = { 0xCE, 0xB4, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_8_17[8] = { 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; static const symbol s_8_18[14] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; - -static const struct among a_8[19] = -{ -{ 12, s_8_0, -1, 1, 0}, -{ 6, s_8_1, -1, 1, 0}, -{ 12, s_8_2, -1, 1, 0}, -{ 12, s_8_3, -1, 1, 0}, -{ 8, s_8_4, -1, 1, 0}, -{ 14, s_8_5, -1, 1, 0}, -{ 12, s_8_6, -1, 1, 0}, -{ 4, s_8_7, -1, 1, 0}, -{ 6, s_8_8, 7, 1, 0}, -{ 12, s_8_9, 8, 1, 0}, -{ 6, s_8_10, -1, 1, 0}, -{ 6, s_8_11, -1, 1, 0}, -{ 12, s_8_12, 11, 1, 0}, -{ 8, s_8_13, 11, 1, 0}, -{ 12, s_8_14, 13, 1, 0}, -{ 12, s_8_15, 11, 1, 0}, -{ 8, s_8_16, -1, 1, 0}, -{ 8, s_8_17, -1, 1, 0}, -{ 14, s_8_18, 17, 1, 0} +static const struct among a_8[19] = { +{ 12, s_8_0, 0, 1, 0}, +{ 6, s_8_1, 0, 1, 0}, +{ 12, s_8_2, 0, 1, 0}, +{ 12, s_8_3, 0, 1, 0}, +{ 8, s_8_4, 0, 1, 0}, +{ 14, s_8_5, 0, 1, 0}, +{ 12, s_8_6, 0, 1, 0}, +{ 4, s_8_7, 0, 1, 0}, +{ 6, s_8_8, -1, 1, 0}, +{ 12, s_8_9, -1, 1, 0}, +{ 6, s_8_10, 0, 1, 0}, +{ 6, s_8_11, 0, 1, 0}, +{ 12, s_8_12, -1, 1, 0}, +{ 8, s_8_13, -2, 1, 0}, +{ 12, s_8_14, -1, 1, 0}, +{ 12, s_8_15, -4, 1, 0}, +{ 8, s_8_16, 0, 1, 0}, +{ 8, s_8_17, 0, 1, 0}, +{ 14, s_8_18, -1, 1, 0} }; static const symbol s_9_0[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x83 }; @@ -514,16 +607,14 @@ static const symbol s_9_3[12] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85 static const symbol s_9_4[12] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_9_5[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB5, 0xCE, 0xB9 }; static const symbol s_9_6[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; - -static const struct among a_9[7] = -{ -{ 10, s_9_0, -1, 1, 0}, -{ 6, s_9_1, -1, 1, 0}, -{ 10, s_9_2, -1, 1, 0}, -{ 12, s_9_3, -1, 1, 0}, -{ 12, s_9_4, -1, 1, 0}, -{ 8, s_9_5, -1, 1, 0}, -{ 10, s_9_6, -1, 1, 0} +static const struct among a_9[7] = { +{ 10, s_9_0, 0, 1, 0}, +{ 6, s_9_1, 0, 1, 0}, +{ 10, s_9_2, 0, 1, 0}, +{ 12, s_9_3, 0, 1, 0}, +{ 12, s_9_4, 0, 1, 0}, +{ 8, s_9_5, 0, 1, 0}, +{ 10, s_9_6, 0, 1, 0} }; static const symbol s_10_0[2] = { 0xCF, 0x80 }; @@ -566,49 +657,47 @@ static const symbol s_10_36[2] = { 0xCE, 0xBC }; static const symbol s_10_37[6] = { 0xCE, 0xB3, 0xCE, 0xB5, 0xCE, 0xBC }; static const symbol s_10_38[6] = { 0xCE, 0xB1, 0xCF, 0x87, 0xCE, 0xBD }; static const symbol s_10_39[14] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1, 0xCE, 0xB8, 0xCF, 0x81, 0xCE, 0xBF }; - -static const struct among a_10[40] = -{ -{ 2, s_10_0, -1, 2, 0}, -{ 6, s_10_1, 0, 2, 0}, -{ 4, s_10_2, 0, 2, 0}, -{ 6, s_10_3, 0, 2, 0}, -{ 6, s_10_4, -1, 2, 0}, -{ 4, s_10_5, -1, 2, 0}, -{ 6, s_10_6, -1, 2, 0}, -{ 4, s_10_7, -1, 2, 0}, -{ 6, s_10_8, -1, 2, 0}, -{ 4, s_10_9, -1, 2, 0}, -{ 6, s_10_10, 9, 2, 0}, -{ 4, s_10_11, -1, 2, 0}, -{ 6, s_10_12, 11, 2, 0}, -{ 4, s_10_13, -1, 2, 0}, -{ 6, s_10_14, 13, 2, 0}, -{ 6, s_10_15, -1, 2, 0}, -{ 4, s_10_16, -1, 2, 0}, -{ 6, s_10_17, -1, 2, 0}, -{ 4, s_10_18, -1, 2, 0}, -{ 6, s_10_19, 18, 2, 0}, -{ 6, s_10_20, -1, 2, 0}, -{ 6, s_10_21, -1, 2, 0}, -{ 4, s_10_22, -1, 2, 0}, -{ 4, s_10_23, -1, 1, 0}, -{ 6, s_10_24, 23, 1, 0}, -{ 6, s_10_25, -1, 1, 0}, -{ 6, s_10_26, -1, 1, 0}, -{ 12, s_10_27, 26, 1, 0}, -{ 8, s_10_28, -1, 1, 0}, -{ 6, s_10_29, -1, 2, 0}, -{ 6, s_10_30, -1, 2, 0}, -{ 4, s_10_31, -1, 2, 0}, -{ 6, s_10_32, -1, 2, 0}, -{ 6, s_10_33, -1, 2, 0}, -{ 6, s_10_34, -1, 2, 0}, -{ 6, s_10_35, -1, 2, 0}, -{ 2, s_10_36, -1, 2, 0}, -{ 6, s_10_37, 36, 2, 0}, -{ 6, s_10_38, -1, 2, 0}, -{ 14, s_10_39, -1, 1, 0} +static const struct among a_10[40] = { +{ 2, s_10_0, 0, 2, 0}, +{ 6, s_10_1, -1, 2, 0}, +{ 4, s_10_2, -2, 2, 0}, +{ 6, s_10_3, -3, 2, 0}, +{ 6, s_10_4, 0, 2, 0}, +{ 4, s_10_5, 0, 2, 0}, +{ 6, s_10_6, 0, 2, 0}, +{ 4, s_10_7, 0, 2, 0}, +{ 6, s_10_8, 0, 2, 0}, +{ 4, s_10_9, 0, 2, 0}, +{ 6, s_10_10, -1, 2, 0}, +{ 4, s_10_11, 0, 2, 0}, +{ 6, s_10_12, -1, 2, 0}, +{ 4, s_10_13, 0, 2, 0}, +{ 6, s_10_14, -1, 2, 0}, +{ 6, s_10_15, 0, 2, 0}, +{ 4, s_10_16, 0, 2, 0}, +{ 6, s_10_17, 0, 2, 0}, +{ 4, s_10_18, 0, 2, 0}, +{ 6, s_10_19, -1, 2, 0}, +{ 6, s_10_20, 0, 2, 0}, +{ 6, s_10_21, 0, 2, 0}, +{ 4, s_10_22, 0, 2, 0}, +{ 4, s_10_23, 0, 1, 0}, +{ 6, s_10_24, -1, 1, 0}, +{ 6, s_10_25, 0, 1, 0}, +{ 6, s_10_26, 0, 1, 0}, +{ 12, s_10_27, -1, 1, 0}, +{ 8, s_10_28, 0, 1, 0}, +{ 6, s_10_29, 0, 2, 0}, +{ 6, s_10_30, 0, 2, 0}, +{ 4, s_10_31, 0, 2, 0}, +{ 6, s_10_32, 0, 2, 0}, +{ 6, s_10_33, 0, 2, 0}, +{ 6, s_10_34, 0, 2, 0}, +{ 6, s_10_35, 0, 2, 0}, +{ 2, s_10_36, 0, 2, 0}, +{ 6, s_10_37, -1, 2, 0}, +{ 6, s_10_38, 0, 2, 0}, +{ 14, s_10_39, 0, 1, 0} }; static const symbol s_11_0[12] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; @@ -622,20 +711,18 @@ static const symbol s_11_7[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB7 static const symbol s_11_8[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xBF, 0xCE, 0xB9 }; static const symbol s_11_9[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84, 0xCF, 0x89, 0xCE, 0xBD }; static const symbol s_11_10[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xBF }; - -static const struct among a_11[11] = -{ -{ 12, s_11_0, -1, 1, 0}, -{ 10, s_11_1, -1, 1, 0}, -{ 10, s_11_2, -1, 1, 0}, -{ 10, s_11_3, -1, 1, 0}, -{ 10, s_11_4, -1, 1, 0}, -{ 8, s_11_5, -1, 1, 0}, -{ 8, s_11_6, -1, 1, 0}, -{ 8, s_11_7, -1, 1, 0}, -{ 10, s_11_8, -1, 1, 0}, -{ 10, s_11_9, -1, 1, 0}, -{ 8, s_11_10, -1, 1, 0} +static const struct among a_11[11] = { +{ 12, s_11_0, 0, 1, 0}, +{ 10, s_11_1, 0, 1, 0}, +{ 10, s_11_2, 0, 1, 0}, +{ 10, s_11_3, 0, 1, 0}, +{ 10, s_11_4, 0, 1, 0}, +{ 8, s_11_5, 0, 1, 0}, +{ 8, s_11_6, 0, 1, 0}, +{ 8, s_11_7, 0, 1, 0}, +{ 10, s_11_8, 0, 1, 0}, +{ 10, s_11_9, 0, 1, 0}, +{ 8, s_11_10, 0, 1, 0} }; static const symbol s_12_0[4] = { 0xCF, 0x83, 0xCE, 0xB5 }; @@ -645,16 +732,14 @@ static const symbol s_12_3[10] = { 0xCE, 0xB5, 0xCE, 0xB3, 0xCE, 0xBA, 0xCE, 0xB static const symbol s_12_4[12] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xBA, 0xCE, 0xBB, 0xCE, 0xB5 }; static const symbol s_12_5[8] = { 0xCE, 0xB4, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_12_6[16] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB9, 0xCE, 0xB4, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; - -static const struct among a_12[7] = -{ -{ 4, s_12_0, -1, 1, 0}, -{ 12, s_12_1, 0, 1, 0}, -{ 14, s_12_2, 0, 1, 0}, -{ 10, s_12_3, -1, 1, 0}, -{ 12, s_12_4, -1, 1, 0}, -{ 8, s_12_5, -1, 2, 0}, -{ 16, s_12_6, 5, 2, 0} +static const struct among a_12[7] = { +{ 4, s_12_0, 0, 1, 0}, +{ 12, s_12_1, -1, 1, 0}, +{ 14, s_12_2, -2, 1, 0}, +{ 10, s_12_3, 0, 1, 0}, +{ 12, s_12_4, 0, 1, 0}, +{ 8, s_12_5, 0, 2, 0}, +{ 16, s_12_6, -1, 2, 0} }; static const symbol s_13_0[10] = { 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x80, 0xCE, 0xB9, 0xCE, 0xBA }; @@ -667,19 +752,17 @@ static const symbol s_13_6[10] = { 0xCE, 0xB5, 0xCE, 0xB8, 0xCE, 0xBD, 0xCE, 0xB static const symbol s_13_7[14] = { 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB1, 0xCF, 0x84, 0xCF, 0x81, 0xCE, 0xB9, 0xCE, 0xBD }; static const symbol s_13_8[20] = { 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xB5, 0xCE, 0xBE, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB4, 0xCF, 0x81, 0xCE, 0xB9, 0xCE, 0xBD }; static const symbol s_13_9[16] = { 0xCE, 0xB2, 0xCF, 0x85, 0xCE, 0xB6, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB9, 0xCE, 0xBD }; - -static const struct among a_13[10] = -{ -{ 10, s_13_0, -1, 7, 0}, -{ 14, s_13_1, -1, 6, 0}, -{ 14, s_13_2, -1, 3, 0}, -{ 16, s_13_3, 2, 1, 0}, -{ 16, s_13_4, -1, 5, 0}, -{ 12, s_13_5, -1, 2, 0}, -{ 10, s_13_6, -1, 4, 0}, -{ 14, s_13_7, -1, 10, 0}, -{ 20, s_13_8, -1, 8, 0}, -{ 16, s_13_9, -1, 9, 0} +static const struct among a_13[10] = { +{ 10, s_13_0, 0, 7, 0}, +{ 14, s_13_1, 0, 6, 0}, +{ 14, s_13_2, 0, 3, 0}, +{ 16, s_13_3, -1, 1, 0}, +{ 16, s_13_4, 0, 5, 0}, +{ 12, s_13_5, 0, 2, 0}, +{ 10, s_13_6, 0, 4, 0}, +{ 14, s_13_7, 0, 10, 0}, +{ 20, s_13_8, 0, 8, 0}, +{ 16, s_13_9, 0, 9, 0} }; static const symbol s_14_0[12] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; @@ -688,37 +771,31 @@ static const symbol s_14_2[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xB static const symbol s_14_3[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xBF, 0xCE, 0xB9 }; static const symbol s_14_4[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCF, 0x89, 0xCE, 0xBD }; static const symbol s_14_5[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xBF }; - -static const struct among a_14[6] = -{ -{ 12, s_14_0, -1, 1, 0}, -{ 10, s_14_1, -1, 1, 0}, -{ 10, s_14_2, -1, 1, 0}, -{ 10, s_14_3, -1, 1, 0}, -{ 10, s_14_4, -1, 1, 0}, -{ 8, s_14_5, -1, 1, 0} +static const struct among a_14[6] = { +{ 12, s_14_0, 0, 1, 0}, +{ 10, s_14_1, 0, 1, 0}, +{ 10, s_14_2, 0, 1, 0}, +{ 10, s_14_3, 0, 1, 0}, +{ 10, s_14_4, 0, 1, 0}, +{ 8, s_14_5, 0, 1, 0} }; static const symbol s_15_0[2] = { 0xCF, 0x83 }; static const symbol s_15_1[2] = { 0xCF, 0x87 }; - -static const struct among a_15[2] = -{ -{ 2, s_15_0, -1, 1, 0}, -{ 2, s_15_1, -1, 1, 0} +static const struct among a_15[2] = { +{ 2, s_15_0, 0, 1, 0}, +{ 2, s_15_1, 0, 1, 0} }; static const symbol s_16_0[12] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB9, 0xCE, 0xB1 }; static const symbol s_16_1[14] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xB4, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB9, 0xCE, 0xB1 }; static const symbol s_16_2[10] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB9 }; static const symbol s_16_3[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xB4, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB9 }; - -static const struct among a_16[4] = -{ -{ 12, s_16_0, -1, 1, 0}, -{ 14, s_16_1, -1, 1, 0}, -{ 10, s_16_2, -1, 1, 0}, -{ 12, s_16_3, -1, 1, 0} +static const struct among a_16[4] = { +{ 12, s_16_0, 0, 1, 0}, +{ 14, s_16_1, 0, 1, 0}, +{ 10, s_16_2, 0, 1, 0}, +{ 12, s_16_3, 0, 1, 0} }; static const symbol s_17_0[2] = { 0xCF, 0x80 }; @@ -767,55 +844,53 @@ static const symbol s_17_42[8] = { 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xB static const symbol s_17_43[8] = { 0xCE, 0xB3, 0xCE, 0xB9, 0xCE, 0xB1, 0xCE, 0xBD }; static const symbol s_17_44[14] = { 0xCE, 0xB7, 0xCE, 0xB3, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5, 0xCE, 0xBD }; static const symbol s_17_45[6] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xBD }; - -static const struct among a_17[46] = -{ -{ 2, s_17_0, -1, 2, 0}, -{ 12, s_17_1, 0, 1, 0}, -{ 2, s_17_2, -1, 1, 0}, -{ 4, s_17_3, 2, 1, 0}, -{ 8, s_17_4, 3, 1, 0}, -{ 8, s_17_5, 3, 1, 0}, -{ 10, s_17_6, 2, 2, 0}, -{ 6, s_17_7, 2, 1, 0}, -{ 8, s_17_8, 2, 1, 0}, -{ 6, s_17_9, 2, 1, 0}, -{ 2, s_17_10, -1, 1, 0}, -{ 12, s_17_11, 10, 1, 0}, -{ 6, s_17_12, 10, 2, 0}, -{ 10, s_17_13, -1, 1, 0}, -{ 4, s_17_14, -1, 1, 0}, -{ 2, s_17_15, -1, 1, 0}, -{ 4, s_17_16, 15, 1, 0}, -{ 10, s_17_17, 16, 1, 0}, -{ 6, s_17_18, 15, 2, 0}, -{ 2, s_17_19, -1, 1, 0}, -{ 2, s_17_20, -1, 2, 0}, -{ 8, s_17_21, 20, 1, 0}, -{ 8, s_17_22, 20, 1, 0}, -{ 18, s_17_23, 22, 1, 0}, -{ 8, s_17_24, -1, 2, 0}, -{ 2, s_17_25, -1, 2, 0}, -{ 4, s_17_26, 25, 1, 0}, -{ 2, s_17_27, -1, 1, 0}, -{ 4, s_17_28, 27, 1, 0}, -{ 10, s_17_29, 27, 1, 0}, -{ 6, s_17_30, 27, 1, 0}, -{ 4, s_17_31, -1, 1, 0}, -{ 6, s_17_32, -1, 1, 0}, -{ 8, s_17_33, -1, 1, 0}, -{ 6, s_17_34, -1, 2, 0}, -{ 6, s_17_35, -1, 1, 0}, -{ 4, s_17_36, -1, 2, 0}, -{ 12, s_17_37, -1, 2, 0}, -{ 8, s_17_38, -1, 1, 0}, -{ 8, s_17_39, -1, 1, 0}, -{ 8, s_17_40, -1, 1, 0}, -{ 12, s_17_41, -1, 2, 0}, -{ 8, s_17_42, -1, 1, 0}, -{ 8, s_17_43, -1, 2, 0}, -{ 14, s_17_44, -1, 2, 0}, -{ 6, s_17_45, -1, 1, 0} +static const struct among a_17[46] = { +{ 2, s_17_0, 0, 2, 0}, +{ 12, s_17_1, -1, 1, 0}, +{ 2, s_17_2, 0, 1, 0}, +{ 4, s_17_3, -1, 1, 0}, +{ 8, s_17_4, -1, 1, 0}, +{ 8, s_17_5, -2, 1, 0}, +{ 10, s_17_6, -4, 2, 0}, +{ 6, s_17_7, -5, 1, 0}, +{ 8, s_17_8, -6, 1, 0}, +{ 6, s_17_9, -7, 1, 0}, +{ 2, s_17_10, 0, 1, 0}, +{ 12, s_17_11, -1, 1, 0}, +{ 6, s_17_12, -2, 2, 0}, +{ 10, s_17_13, 0, 1, 0}, +{ 4, s_17_14, 0, 1, 0}, +{ 2, s_17_15, 0, 1, 0}, +{ 4, s_17_16, -1, 1, 0}, +{ 10, s_17_17, -1, 1, 0}, +{ 6, s_17_18, -3, 2, 0}, +{ 2, s_17_19, 0, 1, 0}, +{ 2, s_17_20, 0, 2, 0}, +{ 8, s_17_21, -1, 1, 0}, +{ 8, s_17_22, -2, 1, 0}, +{ 18, s_17_23, -1, 1, 0}, +{ 8, s_17_24, 0, 2, 0}, +{ 2, s_17_25, 0, 2, 0}, +{ 4, s_17_26, -1, 1, 0}, +{ 2, s_17_27, 0, 1, 0}, +{ 4, s_17_28, -1, 1, 0}, +{ 10, s_17_29, -2, 1, 0}, +{ 6, s_17_30, -3, 1, 0}, +{ 4, s_17_31, 0, 1, 0}, +{ 6, s_17_32, 0, 1, 0}, +{ 8, s_17_33, 0, 1, 0}, +{ 6, s_17_34, 0, 2, 0}, +{ 6, s_17_35, 0, 1, 0}, +{ 4, s_17_36, 0, 2, 0}, +{ 12, s_17_37, 0, 2, 0}, +{ 8, s_17_38, 0, 1, 0}, +{ 8, s_17_39, 0, 1, 0}, +{ 8, s_17_40, 0, 1, 0}, +{ 12, s_17_41, 0, 2, 0}, +{ 8, s_17_42, 0, 1, 0}, +{ 8, s_17_43, 0, 2, 0}, +{ 14, s_17_44, 0, 2, 0}, +{ 6, s_17_45, 0, 1, 0} }; static const symbol s_18_0[10] = { 0xCE, 0xB9, 0xCF, 0x84, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83 }; @@ -826,50 +901,42 @@ static const symbol s_18_4[12] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xB static const symbol s_18_5[6] = { 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB9 }; static const symbol s_18_6[10] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB9 }; static const symbol s_18_7[10] = { 0xCE, 0xB9, 0xCF, 0x84, 0xCF, 0x83, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_18[8] = -{ -{ 10, s_18_0, -1, 1, 0}, -{ 10, s_18_1, -1, 1, 0}, -{ 8, s_18_2, -1, 1, 0}, -{ 8, s_18_3, -1, 1, 0}, -{ 12, s_18_4, 3, 1, 0}, -{ 6, s_18_5, -1, 1, 0}, -{ 10, s_18_6, 5, 1, 0}, -{ 10, s_18_7, -1, 1, 0} +static const struct among a_18[8] = { +{ 10, s_18_0, 0, 1, 0}, +{ 10, s_18_1, 0, 1, 0}, +{ 8, s_18_2, 0, 1, 0}, +{ 8, s_18_3, 0, 1, 0}, +{ 12, s_18_4, -1, 1, 0}, +{ 6, s_18_5, 0, 1, 0}, +{ 10, s_18_6, -1, 1, 0}, +{ 10, s_18_7, 0, 1, 0} }; static const symbol s_19_0[4] = { 0xCE, 0xB9, 0xCF, 0x81 }; static const symbol s_19_1[6] = { 0xCF, 0x88, 0xCE, 0xB1, 0xCE, 0xBB }; static const symbol s_19_2[8] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCF, 0x86, 0xCE, 0xBD }; static const symbol s_19_3[6] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF }; - -static const struct among a_19[4] = -{ -{ 4, s_19_0, -1, 1, 0}, -{ 6, s_19_1, -1, 1, 0}, -{ 8, s_19_2, -1, 1, 0}, -{ 6, s_19_3, -1, 1, 0} +static const struct among a_19[4] = { +{ 4, s_19_0, 0, 1, 0}, +{ 6, s_19_1, 0, 1, 0}, +{ 8, s_19_2, 0, 1, 0}, +{ 6, s_19_3, 0, 1, 0} }; static const symbol s_20_0[2] = { 0xCE, 0xB5 }; static const symbol s_20_1[10] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xB9, 0xCF, 0x87, 0xCE, 0xBD }; - -static const struct among a_20[2] = -{ -{ 2, s_20_0, -1, 1, 0}, -{ 10, s_20_1, -1, 1, 0} +static const struct among a_20[2] = { +{ 2, s_20_0, 0, 1, 0}, +{ 10, s_20_1, 0, 1, 0} }; static const symbol s_21_0[8] = { 0xCE, 0xB9, 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1 }; static const symbol s_21_1[10] = { 0xCE, 0xB9, 0xCE, 0xB4, 0xCE, 0xB9, 0xCF, 0x89, 0xCE, 0xBD }; static const symbol s_21_2[8] = { 0xCE, 0xB9, 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xBF }; - -static const struct among a_21[3] = -{ -{ 8, s_21_0, -1, 1, 0}, -{ 10, s_21_1, -1, 1, 0}, -{ 8, s_21_2, -1, 1, 0} +static const struct among a_21[3] = { +{ 8, s_21_0, 0, 1, 0}, +{ 10, s_21_1, 0, 1, 0}, +{ 8, s_21_2, 0, 1, 0} }; static const symbol s_22_0[2] = { 0xCF, 0x81 }; @@ -879,38 +946,32 @@ static const symbol s_22_3[6] = { 0xCE, 0xBB, 0xCF, 0x85, 0xCE, 0xBA }; static const symbol s_22_4[10] = { 0xCF, 0x86, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xBA }; static const symbol s_22_5[8] = { 0xCE, 0xBF, 0xCE, 0xB2, 0xCE, 0xB5, 0xCE, 0xBB }; static const symbol s_22_6[6] = { 0xCE, 0xBC, 0xCE, 0xB7, 0xCE, 0xBD }; - -static const struct among a_22[7] = -{ -{ 2, s_22_0, -1, 1, 0}, -{ 4, s_22_1, -1, 1, 0}, -{ 2, s_22_2, -1, 1, 0}, -{ 6, s_22_3, -1, 1, 0}, -{ 10, s_22_4, -1, 1, 0}, -{ 8, s_22_5, -1, 1, 0}, -{ 6, s_22_6, -1, 1, 0} +static const struct among a_22[7] = { +{ 2, s_22_0, 0, 1, 0}, +{ 4, s_22_1, 0, 1, 0}, +{ 2, s_22_2, 0, 1, 0}, +{ 6, s_22_3, 0, 1, 0}, +{ 10, s_22_4, 0, 1, 0}, +{ 8, s_22_5, 0, 1, 0}, +{ 6, s_22_6, 0, 1, 0} }; static const symbol s_23_0[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x83 }; static const symbol s_23_1[10] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x85 }; static const symbol s_23_2[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB5 }; static const symbol s_23_3[8] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xBF }; - -static const struct among a_23[4] = -{ -{ 10, s_23_0, -1, 1, 0}, -{ 10, s_23_1, -1, 1, 0}, -{ 8, s_23_2, -1, 1, 0}, -{ 8, s_23_3, -1, 1, 0} +static const struct among a_23[4] = { +{ 10, s_23_0, 0, 1, 0}, +{ 10, s_23_1, 0, 1, 0}, +{ 8, s_23_2, 0, 1, 0}, +{ 8, s_23_3, 0, 1, 0} }; static const symbol s_24_0[8] = { 0xCE, 0xB1, 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83 }; static const symbol s_24_1[8] = { 0xCE, 0xB1, 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_24[2] = -{ -{ 8, s_24_0, -1, 1, 0}, -{ 8, s_24_1, -1, 1, 0} +static const struct among a_24[2] = { +{ 8, s_24_0, 0, 1, 0}, +{ 8, s_24_1, 0, 1, 0} }; static const symbol s_25_0[10] = { 0xCE, 0xBC, 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x80 }; @@ -923,28 +984,24 @@ static const symbol s_25_6[6] = { 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9 }; static const symbol s_25_7[4] = { 0xCE, 0xBF, 0xCE, 0xBA }; static const symbol s_25_8[6] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBC }; static const symbol s_25_9[6] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBD }; - -static const struct among a_25[10] = -{ -{ 10, s_25_0, -1, -1, 0}, -{ 6, s_25_1, -1, -1, 0}, -{ 10, s_25_2, -1, -1, 0}, -{ 10, s_25_3, -1, -1, 0}, -{ 10, s_25_4, -1, -1, 0}, -{ 10, s_25_5, -1, -1, 0}, -{ 6, s_25_6, -1, -1, 0}, -{ 4, s_25_7, -1, -1, 0}, -{ 6, s_25_8, -1, -1, 0}, -{ 6, s_25_9, -1, -1, 0} +static const struct among a_25[10] = { +{ 10, s_25_0, 0, -1, 0}, +{ 6, s_25_1, 0, -1, 0}, +{ 10, s_25_2, 0, -1, 0}, +{ 10, s_25_3, 0, -1, 0}, +{ 10, s_25_4, 0, -1, 0}, +{ 10, s_25_5, 0, -1, 0}, +{ 6, s_25_6, 0, -1, 0}, +{ 4, s_25_7, 0, -1, 0}, +{ 6, s_25_8, 0, -1, 0}, +{ 6, s_25_9, 0, -1, 0} }; static const symbol s_26_0[8] = { 0xCE, 0xB5, 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83 }; static const symbol s_26_1[8] = { 0xCE, 0xB5, 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_26[2] = -{ -{ 8, s_26_0, -1, 1, 0}, -{ 8, s_26_1, -1, 1, 0} +static const struct among a_26[2] = { +{ 8, s_26_0, 0, 1, 0}, +{ 8, s_26_1, 0, 1, 0} }; static const symbol s_27_0[10] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x80 }; @@ -955,26 +1012,22 @@ static const symbol s_27_4[4] = { 0xCE, 0xB9, 0xCF, 0x80 }; static const symbol s_27_5[6] = { 0xCE, 0xB5, 0xCE, 0xBC, 0xCF, 0x80 }; static const symbol s_27_6[4] = { 0xCE, 0xBF, 0xCF, 0x80 }; static const symbol s_27_7[6] = { 0xCE, 0xBC, 0xCE, 0xB9, 0xCE, 0xBB }; - -static const struct among a_27[8] = -{ -{ 10, s_27_0, -1, 1, 0}, -{ 4, s_27_1, -1, 1, 0}, -{ 6, s_27_2, -1, 1, 0}, -{ 6, s_27_3, -1, 1, 0}, -{ 4, s_27_4, -1, 1, 0}, -{ 6, s_27_5, -1, 1, 0}, -{ 4, s_27_6, -1, 1, 0}, -{ 6, s_27_7, -1, 1, 0} +static const struct among a_27[8] = { +{ 10, s_27_0, 0, 1, 0}, +{ 4, s_27_1, 0, 1, 0}, +{ 6, s_27_2, 0, 1, 0}, +{ 6, s_27_3, 0, 1, 0}, +{ 4, s_27_4, 0, 1, 0}, +{ 6, s_27_5, 0, 1, 0}, +{ 4, s_27_6, 0, 1, 0}, +{ 6, s_27_7, 0, 1, 0} }; static const symbol s_28_0[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83 }; static const symbol s_28_1[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_28[2] = -{ -{ 10, s_28_0, -1, 1, 0}, -{ 10, s_28_1, -1, 1, 0} +static const struct among a_28[2] = { +{ 10, s_28_0, 0, 1, 0}, +{ 10, s_28_1, 0, 1, 0} }; static const symbol s_29_0[4] = { 0xCF, 0x83, 0xCF, 0x80 }; @@ -992,33 +1045,29 @@ static const symbol s_29_11[10] = { 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0x static const symbol s_29_12[6] = { 0xCE, 0xB2, 0xCE, 0xB5, 0xCE, 0xBB }; static const symbol s_29_13[4] = { 0xCF, 0x87, 0xCE, 0xBD }; static const symbol s_29_14[8] = { 0xCF, 0x80, 0xCE, 0xBB, 0xCE, 0xB5, 0xCE, 0xBE }; - -static const struct among a_29[15] = -{ -{ 4, s_29_0, -1, 1, 0}, -{ 4, s_29_1, -1, 1, 0}, -{ 2, s_29_2, -1, 1, 0}, -{ 6, s_29_3, -1, 1, 0}, -{ 8, s_29_4, -1, 1, 0}, -{ 4, s_29_5, -1, 1, 0}, -{ 6, s_29_6, -1, 1, 0}, -{ 4, s_29_7, -1, 1, 0}, -{ 12, s_29_8, -1, 1, 0}, -{ 8, s_29_9, -1, 1, 0}, -{ 4, s_29_10, -1, 1, 0}, -{ 10, s_29_11, -1, 1, 0}, -{ 6, s_29_12, -1, 1, 0}, -{ 4, s_29_13, -1, 1, 0}, -{ 8, s_29_14, -1, 1, 0} +static const struct among a_29[15] = { +{ 4, s_29_0, 0, 1, 0}, +{ 4, s_29_1, 0, 1, 0}, +{ 2, s_29_2, 0, 1, 0}, +{ 6, s_29_3, 0, 1, 0}, +{ 8, s_29_4, 0, 1, 0}, +{ 4, s_29_5, 0, 1, 0}, +{ 6, s_29_6, 0, 1, 0}, +{ 4, s_29_7, 0, 1, 0}, +{ 12, s_29_8, 0, 1, 0}, +{ 8, s_29_9, 0, 1, 0}, +{ 4, s_29_10, 0, 1, 0}, +{ 10, s_29_11, 0, 1, 0}, +{ 6, s_29_12, 0, 1, 0}, +{ 4, s_29_13, 0, 1, 0}, +{ 8, s_29_14, 0, 1, 0} }; static const symbol s_30_0[6] = { 0xCE, 0xB5, 0xCF, 0x89, 0xCF, 0x83 }; static const symbol s_30_1[6] = { 0xCE, 0xB5, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_30[2] = -{ -{ 6, s_30_0, -1, 1, 0}, -{ 6, s_30_1, -1, 1, 0} +static const struct among a_30[2] = { +{ 6, s_30_0, 0, 1, 0}, +{ 6, s_30_1, 0, 1, 0} }; static const symbol s_31_0[2] = { 0xCF, 0x80 }; @@ -1029,41 +1078,35 @@ static const symbol s_31_4[2] = { 0xCE, 0xB8 }; static const symbol s_31_5[6] = { 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBB }; static const symbol s_31_6[4] = { 0xCE, 0xB5, 0xCE, 0xBB }; static const symbol s_31_7[2] = { 0xCE, 0xBD }; - -static const struct among a_31[8] = -{ -{ 2, s_31_0, -1, 1, 0}, -{ 6, s_31_1, -1, 1, 0}, -{ 2, s_31_2, -1, 1, 0}, -{ 4, s_31_3, 2, 1, 0}, -{ 2, s_31_4, -1, 1, 0}, -{ 6, s_31_5, -1, 1, 0}, -{ 4, s_31_6, -1, 1, 0}, -{ 2, s_31_7, -1, 1, 0} +static const struct among a_31[8] = { +{ 2, s_31_0, 0, 1, 0}, +{ 6, s_31_1, 0, 1, 0}, +{ 2, s_31_2, 0, 1, 0}, +{ 4, s_31_3, -1, 1, 0}, +{ 2, s_31_4, 0, 1, 0}, +{ 6, s_31_5, 0, 1, 0}, +{ 4, s_31_6, 0, 1, 0}, +{ 2, s_31_7, 0, 1, 0} }; static const symbol s_32_0[6] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85 }; static const symbol s_32_1[4] = { 0xCE, 0xB9, 0xCE, 0xB1 }; static const symbol s_32_2[6] = { 0xCE, 0xB9, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_32[3] = -{ -{ 6, s_32_0, -1, 1, 0}, -{ 4, s_32_1, -1, 1, 0}, -{ 6, s_32_2, -1, 1, 0} +static const struct among a_32[3] = { +{ 6, s_32_0, 0, 1, 0}, +{ 4, s_32_1, 0, 1, 0}, +{ 6, s_32_2, 0, 1, 0} }; static const symbol s_33_0[8] = { 0xCE, 0xB9, 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x85 }; static const symbol s_33_1[6] = { 0xCE, 0xB9, 0xCE, 0xBA, 0xCE, 0xB1 }; static const symbol s_33_2[8] = { 0xCE, 0xB9, 0xCE, 0xBA, 0xCF, 0x89, 0xCE, 0xBD }; static const symbol s_33_3[6] = { 0xCE, 0xB9, 0xCE, 0xBA, 0xCE, 0xBF }; - -static const struct among a_33[4] = -{ -{ 8, s_33_0, -1, 1, 0}, -{ 6, s_33_1, -1, 1, 0}, -{ 8, s_33_2, -1, 1, 0}, -{ 6, s_33_3, -1, 1, 0} +static const struct among a_33[4] = { +{ 8, s_33_0, 0, 1, 0}, +{ 6, s_33_1, 0, 1, 0}, +{ 8, s_33_2, 0, 1, 0}, +{ 6, s_33_3, 0, 1, 0} }; static const symbol s_34_0[8] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB, 0xCF, 0x80 }; @@ -1102,45 +1145,43 @@ static const symbol s_34_32[8] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB static const symbol s_34_33[12] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB, 0xCE, 0xB9, 0xCE, 0xBD }; static const symbol s_34_34[14] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5, 0xCE, 0xBB, 0xCE, 0xBD }; static const symbol s_34_35[10] = { 0xCF, 0x86, 0xCE, 0xB9, 0xCE, 0xBB, 0xCE, 0xBF, 0xCE, 0xBD }; - -static const struct among a_34[36] = -{ -{ 8, s_34_0, -1, 1, 0}, -{ 6, s_34_1, -1, 1, 0}, -{ 12, s_34_2, -1, 1, 0}, -{ 8, s_34_3, -1, 1, 0}, -{ 8, s_34_4, -1, 1, 0}, -{ 6, s_34_5, -1, 1, 0}, -{ 6, s_34_6, -1, 1, 0}, -{ 8, s_34_7, -1, 1, 0}, -{ 8, s_34_8, -1, 1, 0}, -{ 14, s_34_9, -1, 1, 0}, -{ 6, s_34_10, -1, 1, 0}, -{ 12, s_34_11, -1, 1, 0}, -{ 8, s_34_12, -1, 1, 0}, -{ 4, s_34_13, -1, 1, 0}, -{ 10, s_34_14, 13, 1, 0}, -{ 10, s_34_15, 13, 1, 0}, -{ 10, s_34_16, -1, 1, 0}, -{ 6, s_34_17, -1, 1, 0}, -{ 8, s_34_18, -1, 1, 0}, -{ 12, s_34_19, -1, 1, 0}, -{ 10, s_34_20, -1, 1, 0}, -{ 4, s_34_21, -1, 1, 0}, -{ 8, s_34_22, 21, 1, 0}, -{ 6, s_34_23, -1, 1, 0}, -{ 8, s_34_24, -1, 1, 0}, -{ 4, s_34_25, -1, 1, 0}, -{ 14, s_34_26, 25, 1, 0}, -{ 14, s_34_27, -1, 1, 0}, -{ 8, s_34_28, -1, 1, 0}, -{ 8, s_34_29, -1, 1, 0}, -{ 8, s_34_30, -1, 1, 0}, -{ 8, s_34_31, -1, 1, 0}, -{ 8, s_34_32, -1, 1, 0}, -{ 12, s_34_33, -1, 1, 0}, -{ 14, s_34_34, -1, 1, 0}, -{ 10, s_34_35, -1, 1, 0} +static const struct among a_34[36] = { +{ 8, s_34_0, 0, 1, 0}, +{ 6, s_34_1, 0, 1, 0}, +{ 12, s_34_2, 0, 1, 0}, +{ 8, s_34_3, 0, 1, 0}, +{ 8, s_34_4, 0, 1, 0}, +{ 6, s_34_5, 0, 1, 0}, +{ 6, s_34_6, 0, 1, 0}, +{ 8, s_34_7, 0, 1, 0}, +{ 8, s_34_8, 0, 1, 0}, +{ 14, s_34_9, 0, 1, 0}, +{ 6, s_34_10, 0, 1, 0}, +{ 12, s_34_11, 0, 1, 0}, +{ 8, s_34_12, 0, 1, 0}, +{ 4, s_34_13, 0, 1, 0}, +{ 10, s_34_14, -1, 1, 0}, +{ 10, s_34_15, -2, 1, 0}, +{ 10, s_34_16, 0, 1, 0}, +{ 6, s_34_17, 0, 1, 0}, +{ 8, s_34_18, 0, 1, 0}, +{ 12, s_34_19, 0, 1, 0}, +{ 10, s_34_20, 0, 1, 0}, +{ 4, s_34_21, 0, 1, 0}, +{ 8, s_34_22, -1, 1, 0}, +{ 6, s_34_23, 0, 1, 0}, +{ 8, s_34_24, 0, 1, 0}, +{ 4, s_34_25, 0, 1, 0}, +{ 14, s_34_26, -1, 1, 0}, +{ 14, s_34_27, 0, 1, 0}, +{ 8, s_34_28, 0, 1, 0}, +{ 8, s_34_29, 0, 1, 0}, +{ 8, s_34_30, 0, 1, 0}, +{ 8, s_34_31, 0, 1, 0}, +{ 8, s_34_32, 0, 1, 0}, +{ 12, s_34_33, 0, 1, 0}, +{ 14, s_34_34, 0, 1, 0}, +{ 10, s_34_35, 0, 1, 0} }; static const symbol s_35_0[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; @@ -1148,14 +1189,12 @@ static const symbol s_35_1[10] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB static const symbol s_35_2[10] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; static const symbol s_35_3[10] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; static const symbol s_35_4[14] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; - -static const struct among a_35[5] = -{ -{ 12, s_35_0, -1, 1, 0}, -{ 10, s_35_1, -1, 1, 0}, -{ 10, s_35_2, -1, 1, 0}, -{ 10, s_35_3, -1, 1, 0}, -{ 14, s_35_4, 3, 1, 0} +static const struct among a_35[5] = { +{ 12, s_35_0, 0, 1, 0}, +{ 10, s_35_1, 0, 1, 0}, +{ 10, s_35_2, 0, 1, 0}, +{ 10, s_35_3, 0, 1, 0}, +{ 14, s_35_4, -1, 1, 0} }; static const symbol s_36_0[8] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x80 }; @@ -1170,30 +1209,26 @@ static const symbol s_36_8[6] = { 0xCE, 0xBE, 0xCE, 0xB5, 0xCE, 0xB8 }; static const symbol s_36_9[8] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xB8 }; static const symbol s_36_10[8] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xBA }; static const symbol s_36_11[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBB }; - -static const struct among a_36[12] = -{ -{ 8, s_36_0, -1, 1, 0}, -{ 8, s_36_1, -1, 1, 0}, -{ 10, s_36_2, -1, 1, 0}, -{ 6, s_36_3, -1, 1, 0}, -{ 2, s_36_4, -1, 1, 0}, -{ 6, s_36_5, 4, 1, 0}, -{ 8, s_36_6, -1, 1, 0}, -{ 6, s_36_7, -1, 1, 0}, -{ 6, s_36_8, -1, 1, 0}, -{ 8, s_36_9, -1, 1, 0}, -{ 8, s_36_10, -1, 1, 0}, -{ 6, s_36_11, -1, 1, 0} +static const struct among a_36[12] = { +{ 8, s_36_0, 0, 1, 0}, +{ 8, s_36_1, 0, 1, 0}, +{ 10, s_36_2, 0, 1, 0}, +{ 6, s_36_3, 0, 1, 0}, +{ 2, s_36_4, 0, 1, 0}, +{ 6, s_36_5, -1, 1, 0}, +{ 8, s_36_6, 0, 1, 0}, +{ 6, s_36_7, 0, 1, 0}, +{ 6, s_36_8, 0, 1, 0}, +{ 8, s_36_9, 0, 1, 0}, +{ 8, s_36_10, 0, 1, 0}, +{ 6, s_36_11, 0, 1, 0} }; static const symbol s_37_0[4] = { 0xCF, 0x84, 0xCF, 0x81 }; static const symbol s_37_1[4] = { 0xCF, 0x84, 0xCF, 0x83 }; - -static const struct among a_37[2] = -{ -{ 4, s_37_0, -1, 1, 0}, -{ 4, s_37_1, -1, 1, 0} +static const struct among a_37[2] = { +{ 4, s_37_0, 0, 1, 0}, +{ 4, s_37_1, 0, 1, 0} }; static const symbol s_38_0[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; @@ -1207,20 +1242,18 @@ static const symbol s_38_7[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB static const symbol s_38_8[10] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_38_9[10] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; static const symbol s_38_10[14] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; - -static const struct among a_38[11] = -{ -{ 12, s_38_0, -1, 1, 0}, -{ 10, s_38_1, -1, 1, 0}, -{ 14, s_38_2, -1, 1, 0}, -{ 16, s_38_3, 2, 1, 0}, -{ 12, s_38_4, -1, 1, 0}, -{ 14, s_38_5, 4, 1, 0}, -{ 10, s_38_6, -1, 1, 0}, -{ 12, s_38_7, 6, 1, 0}, -{ 10, s_38_8, -1, 1, 0}, -{ 10, s_38_9, -1, 1, 0}, -{ 14, s_38_10, 9, 1, 0} +static const struct among a_38[11] = { +{ 12, s_38_0, 0, 1, 0}, +{ 10, s_38_1, 0, 1, 0}, +{ 14, s_38_2, 0, 1, 0}, +{ 16, s_38_3, -1, 1, 0}, +{ 12, s_38_4, 0, 1, 0}, +{ 14, s_38_5, -1, 1, 0}, +{ 10, s_38_6, 0, 1, 0}, +{ 12, s_38_7, -1, 1, 0}, +{ 10, s_38_8, 0, 1, 0}, +{ 10, s_38_9, 0, 1, 0}, +{ 14, s_38_10, -1, 1, 0} }; static const symbol s_39_0[2] = { 0xCF, 0x80 }; @@ -1318,1144 +1351,993 @@ static const symbol s_39_91[16] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xB9, 0xCE, 0x static const symbol s_39_92[16] = { 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBB, 0xCE, 0xBC }; static const symbol s_39_93[2] = { 0xCE, 0xBD }; static const symbol s_39_94[16] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB9, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; - -static const struct among a_39[95] = -{ -{ 2, s_39_0, -1, 1, 0}, -{ 4, s_39_1, 0, 1, 0}, -{ 14, s_39_2, 0, 1, 0}, -{ 8, s_39_3, 0, 1, 0}, -{ 18, s_39_4, 0, 1, 0}, -{ 8, s_39_5, 0, 1, 0}, -{ 6, s_39_6, 0, 1, 0}, -{ 12, s_39_7, 6, 1, 0}, -{ 12, s_39_8, -1, 1, 0}, -{ 6, s_39_9, -1, 1, 0}, -{ 4, s_39_10, -1, 1, 0}, -{ 10, s_39_11, 10, 1, 0}, -{ 6, s_39_12, 10, 1, 0}, -{ 12, s_39_13, -1, 1, 0}, -{ 12, s_39_14, -1, 1, 0}, -{ 2, s_39_15, -1, 1, 0}, -{ 16, s_39_16, 15, 1, 0}, -{ 6, s_39_17, 15, 1, 0}, -{ 6, s_39_18, 15, 1, 0}, -{ 10, s_39_19, 15, 1, 0}, -{ 8, s_39_20, -1, 1, 0}, -{ 8, s_39_21, -1, 1, 0}, -{ 8, s_39_22, -1, 1, 0}, -{ 14, s_39_23, -1, 1, 0}, -{ 6, s_39_24, -1, 1, 0}, -{ 12, s_39_25, -1, 1, 0}, -{ 10, s_39_26, -1, 1, 0}, -{ 8, s_39_27, -1, 1, 0}, -{ 10, s_39_28, -1, 1, 0}, -{ 2, s_39_29, -1, 1, 0}, -{ 14, s_39_30, 29, 1, 0}, -{ 14, s_39_31, 29, 1, 0}, -{ 6, s_39_32, 29, 1, 0}, -{ 8, s_39_33, 29, 1, 0}, -{ 8, s_39_34, 29, 1, 0}, -{ 16, s_39_35, 34, 1, 0}, -{ 10, s_39_36, 29, 1, 0}, -{ 12, s_39_37, 36, 1, 0}, -{ 2, s_39_38, -1, 1, 0}, -{ 14, s_39_39, 38, 1, 0}, -{ 8, s_39_40, 38, 1, 0}, -{ 12, s_39_41, 38, 1, 0}, -{ 22, s_39_42, 41, 1, 0}, -{ 22, s_39_43, 41, 1, 0}, -{ 22, s_39_44, 41, 1, 0}, -{ 6, s_39_45, 38, 1, 0}, -{ 6, s_39_46, -1, 1, 0}, -{ 8, s_39_47, 46, 1, 0}, -{ 14, s_39_48, 46, 1, 0}, -{ 6, s_39_49, -1, 1, 0}, -{ 8, s_39_50, 49, 1, 0}, -{ 16, s_39_51, 50, 1, 0}, -{ 2, s_39_52, -1, 1, 0}, -{ 10, s_39_53, 52, 1, 0}, -{ 10, s_39_54, 52, 1, 0}, -{ 4, s_39_55, 52, 1, 0}, -{ 8, s_39_56, 55, 1, 0}, -{ 8, s_39_57, 55, 1, 0}, -{ 10, s_39_58, 52, 1, 0}, -{ 12, s_39_59, 58, 1, 0}, -{ 10, s_39_60, 52, 1, 0}, -{ 8, s_39_61, 52, 1, 0}, -{ 8, s_39_62, 52, 1, 0}, -{ 6, s_39_63, 52, 1, 0}, -{ 14, s_39_64, -1, 1, 0}, -{ 2, s_39_65, -1, 1, 0}, -{ 12, s_39_66, 65, 1, 0}, -{ 6, s_39_67, 65, 1, 0}, -{ 8, s_39_68, 67, 1, 0}, -{ 8, s_39_69, -1, 1, 0}, -{ 12, s_39_70, -1, 1, 0}, -{ 6, s_39_71, -1, 1, 0}, -{ 10, s_39_72, -1, 1, 0}, -{ 4, s_39_73, -1, 1, 0}, -{ 8, s_39_74, 73, 1, 0}, -{ 10, s_39_75, -1, 1, 0}, -{ 4, s_39_76, -1, 1, 0}, -{ 8, s_39_77, 76, 1, 0}, -{ 12, s_39_78, 76, 1, 0}, -{ 10, s_39_79, 76, 1, 0}, -{ 6, s_39_80, -1, 1, 0}, -{ 6, s_39_81, -1, 1, 0}, -{ 14, s_39_82, 81, 1, 0}, -{ 14, s_39_83, 81, 1, 0}, -{ 12, s_39_84, 81, 1, 0}, -{ 12, s_39_85, -1, 1, 0}, -{ 6, s_39_86, -1, 1, 0}, -{ 12, s_39_87, -1, 1, 0}, -{ 2, s_39_88, -1, 1, 0}, -{ 14, s_39_89, 88, 1, 0}, -{ 10, s_39_90, 88, 1, 0}, -{ 16, s_39_91, 88, 1, 0}, -{ 16, s_39_92, 88, 1, 0}, -{ 2, s_39_93, -1, 1, 0}, -{ 16, s_39_94, 93, 1, 0} -}; - -static const symbol s_40_0[10] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB5 }; - -static const struct among a_40[1] = -{ -{ 10, s_40_0, -1, 1, 0} -}; - -static const symbol s_41_0[6] = { 0xCF, 0x80, 0xCF, 0x85, 0xCF, 0x81 }; -static const symbol s_41_1[6] = { 0xCE, 0xB5, 0xCF, 0x85, 0xCF, 0x81 }; -static const symbol s_41_2[6] = { 0xCF, 0x87, 0xCF, 0x89, 0xCF, 0x81 }; -static const symbol s_41_3[6] = { 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_41_4[4] = { 0xCE, 0xB2, 0xCF, 0x81 }; -static const symbol s_41_5[6] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCF, 0x81 }; -static const symbol s_41_6[6] = { 0xCF, 0x86, 0xCE, 0xBF, 0xCF, 0x81 }; -static const symbol s_41_7[6] = { 0xCE, 0xBD, 0xCE, 0xB5, 0xCF, 0x84 }; -static const symbol s_41_8[4] = { 0xCF, 0x83, 0xCF, 0x87 }; -static const symbol s_41_9[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB4 }; -static const symbol s_41_10[6] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB4 }; -static const symbol s_41_11[4] = { 0xCE, 0xBF, 0xCE, 0xB4 }; -static const symbol s_41_12[10] = { 0xCF, 0x85, 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB8 }; -static const symbol s_41_13[4] = { 0xCF, 0x83, 0xCE, 0xB8 }; -static const symbol s_41_14[6] = { 0xCE, 0xB5, 0xCF, 0x85, 0xCE, 0xB8 }; -static const symbol s_41_15[6] = { 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_41_16[6] = { 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_41_17[8] = { 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_41_18[6] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_41_19[6] = { 0xCF, 0x84, 0xCE, 0xB9, 0xCE, 0xB8 }; -static const symbol s_41_20[6] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xB8 }; -static const symbol s_41_21[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB8 }; -static const symbol s_41_22[6] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB8 }; -static const symbol s_41_23[6] = { 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xB8 }; -static const symbol s_41_24[6] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xBA }; -static const symbol s_41_25[8] = { 0xCF, 0x89, 0xCF, 0x86, 0xCE, 0xB5, 0xCE, 0xBB }; -static const symbol s_41_26[6] = { 0xCE, 0xB2, 0xCE, 0xBF, 0xCE, 0xBB }; -static const symbol s_41_27[6] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_41_28[6] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCE, 0xBD }; -static const symbol s_41_29[6] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xBD }; -static const symbol s_41_30[6] = { 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBD }; - -static const struct among a_41[31] = -{ -{ 6, s_41_0, -1, 1, 0}, -{ 6, s_41_1, -1, 1, 0}, -{ 6, s_41_2, -1, 1, 0}, -{ 6, s_41_3, -1, 1, 0}, -{ 4, s_41_4, -1, 1, 0}, -{ 6, s_41_5, -1, 1, 0}, -{ 6, s_41_6, -1, 1, 0}, -{ 6, s_41_7, -1, 1, 0}, -{ 4, s_41_8, -1, 1, 0}, -{ 8, s_41_9, -1, 1, 0}, -{ 6, s_41_10, -1, 1, 0}, -{ 4, s_41_11, -1, 1, 0}, -{ 10, s_41_12, -1, 1, 0}, +static const struct among a_39[95] = { +{ 2, s_39_0, 0, 1, 0}, +{ 4, s_39_1, -1, 1, 0}, +{ 14, s_39_2, -2, 1, 0}, +{ 8, s_39_3, -3, 1, 0}, +{ 18, s_39_4, -4, 1, 0}, +{ 8, s_39_5, -5, 1, 0}, +{ 6, s_39_6, -6, 1, 0}, +{ 12, s_39_7, -1, 1, 0}, +{ 12, s_39_8, 0, 1, 0}, +{ 6, s_39_9, 0, 1, 0}, +{ 4, s_39_10, 0, 1, 0}, +{ 10, s_39_11, -1, 1, 0}, +{ 6, s_39_12, -2, 1, 0}, +{ 12, s_39_13, 0, 1, 0}, +{ 12, s_39_14, 0, 1, 0}, +{ 2, s_39_15, 0, 1, 0}, +{ 16, s_39_16, -1, 1, 0}, +{ 6, s_39_17, -2, 1, 0}, +{ 6, s_39_18, -3, 1, 0}, +{ 10, s_39_19, -4, 1, 0}, +{ 8, s_39_20, 0, 1, 0}, +{ 8, s_39_21, 0, 1, 0}, +{ 8, s_39_22, 0, 1, 0}, +{ 14, s_39_23, 0, 1, 0}, +{ 6, s_39_24, 0, 1, 0}, +{ 12, s_39_25, 0, 1, 0}, +{ 10, s_39_26, 0, 1, 0}, +{ 8, s_39_27, 0, 1, 0}, +{ 10, s_39_28, 0, 1, 0}, +{ 2, s_39_29, 0, 1, 0}, +{ 14, s_39_30, -1, 1, 0}, +{ 14, s_39_31, -2, 1, 0}, +{ 6, s_39_32, -3, 1, 0}, +{ 8, s_39_33, -4, 1, 0}, +{ 8, s_39_34, -5, 1, 0}, +{ 16, s_39_35, -1, 1, 0}, +{ 10, s_39_36, -7, 1, 0}, +{ 12, s_39_37, -1, 1, 0}, +{ 2, s_39_38, 0, 1, 0}, +{ 14, s_39_39, -1, 1, 0}, +{ 8, s_39_40, -2, 1, 0}, +{ 12, s_39_41, -3, 1, 0}, +{ 22, s_39_42, -1, 1, 0}, +{ 22, s_39_43, -2, 1, 0}, +{ 22, s_39_44, -3, 1, 0}, +{ 6, s_39_45, -7, 1, 0}, +{ 6, s_39_46, 0, 1, 0}, +{ 8, s_39_47, -1, 1, 0}, +{ 14, s_39_48, -2, 1, 0}, +{ 6, s_39_49, 0, 1, 0}, +{ 8, s_39_50, -1, 1, 0}, +{ 16, s_39_51, -1, 1, 0}, +{ 2, s_39_52, 0, 1, 0}, +{ 10, s_39_53, -1, 1, 0}, +{ 10, s_39_54, -2, 1, 0}, +{ 4, s_39_55, -3, 1, 0}, +{ 8, s_39_56, -1, 1, 0}, +{ 8, s_39_57, -2, 1, 0}, +{ 10, s_39_58, -6, 1, 0}, +{ 12, s_39_59, -1, 1, 0}, +{ 10, s_39_60, -8, 1, 0}, +{ 8, s_39_61, -9, 1, 0}, +{ 8, s_39_62, -10, 1, 0}, +{ 6, s_39_63, -11, 1, 0}, +{ 14, s_39_64, 0, 1, 0}, +{ 2, s_39_65, 0, 1, 0}, +{ 12, s_39_66, -1, 1, 0}, +{ 6, s_39_67, -2, 1, 0}, +{ 8, s_39_68, -1, 1, 0}, +{ 8, s_39_69, 0, 1, 0}, +{ 12, s_39_70, 0, 1, 0}, +{ 6, s_39_71, 0, 1, 0}, +{ 10, s_39_72, 0, 1, 0}, +{ 4, s_39_73, 0, 1, 0}, +{ 8, s_39_74, -1, 1, 0}, +{ 10, s_39_75, 0, 1, 0}, +{ 4, s_39_76, 0, 1, 0}, +{ 8, s_39_77, -1, 1, 0}, +{ 12, s_39_78, -2, 1, 0}, +{ 10, s_39_79, -3, 1, 0}, +{ 6, s_39_80, 0, 1, 0}, +{ 6, s_39_81, 0, 1, 0}, +{ 14, s_39_82, -1, 1, 0}, +{ 14, s_39_83, -2, 1, 0}, +{ 12, s_39_84, -3, 1, 0}, +{ 12, s_39_85, 0, 1, 0}, +{ 6, s_39_86, 0, 1, 0}, +{ 12, s_39_87, 0, 1, 0}, +{ 2, s_39_88, 0, 1, 0}, +{ 14, s_39_89, -1, 1, 0}, +{ 10, s_39_90, -2, 1, 0}, +{ 16, s_39_91, -3, 1, 0}, +{ 16, s_39_92, -4, 1, 0}, +{ 2, s_39_93, 0, 1, 0}, +{ 16, s_39_94, -1, 1, 0} +}; + +static const symbol s_40_0[6] = { 0xCF, 0x80, 0xCF, 0x85, 0xCF, 0x81 }; +static const symbol s_40_1[6] = { 0xCE, 0xB5, 0xCF, 0x85, 0xCF, 0x81 }; +static const symbol s_40_2[6] = { 0xCF, 0x87, 0xCF, 0x89, 0xCF, 0x81 }; +static const symbol s_40_3[6] = { 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_40_4[4] = { 0xCE, 0xB2, 0xCF, 0x81 }; +static const symbol s_40_5[6] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCF, 0x81 }; +static const symbol s_40_6[6] = { 0xCF, 0x86, 0xCE, 0xBF, 0xCF, 0x81 }; +static const symbol s_40_7[6] = { 0xCE, 0xBD, 0xCE, 0xB5, 0xCF, 0x84 }; +static const symbol s_40_8[4] = { 0xCF, 0x83, 0xCF, 0x87 }; +static const symbol s_40_9[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB4 }; +static const symbol s_40_10[6] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB4 }; +static const symbol s_40_11[4] = { 0xCE, 0xBF, 0xCE, 0xB4 }; +static const symbol s_40_12[10] = { 0xCF, 0x85, 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB8 }; +static const symbol s_40_13[4] = { 0xCF, 0x83, 0xCE, 0xB8 }; +static const symbol s_40_14[6] = { 0xCE, 0xB5, 0xCF, 0x85, 0xCE, 0xB8 }; +static const symbol s_40_15[6] = { 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_40_16[6] = { 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_40_17[8] = { 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_40_18[6] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_40_19[6] = { 0xCF, 0x84, 0xCE, 0xB9, 0xCE, 0xB8 }; +static const symbol s_40_20[6] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xB8 }; +static const symbol s_40_21[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB8 }; +static const symbol s_40_22[6] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB8 }; +static const symbol s_40_23[6] = { 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xB8 }; +static const symbol s_40_24[6] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xBA }; +static const symbol s_40_25[8] = { 0xCF, 0x89, 0xCF, 0x86, 0xCE, 0xB5, 0xCE, 0xBB }; +static const symbol s_40_26[6] = { 0xCE, 0xB2, 0xCE, 0xBF, 0xCE, 0xBB }; +static const symbol s_40_27[6] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_40_28[6] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCE, 0xBD }; +static const symbol s_40_29[6] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xBD }; +static const symbol s_40_30[6] = { 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBD }; +static const struct among a_40[31] = { +{ 6, s_40_0, 0, 1, 0}, +{ 6, s_40_1, 0, 1, 0}, +{ 6, s_40_2, 0, 1, 0}, +{ 6, s_40_3, 0, 1, 0}, +{ 4, s_40_4, 0, 1, 0}, +{ 6, s_40_5, 0, 1, 0}, +{ 6, s_40_6, 0, 1, 0}, +{ 6, s_40_7, 0, 1, 0}, +{ 4, s_40_8, 0, 1, 0}, +{ 8, s_40_9, 0, 1, 0}, +{ 6, s_40_10, 0, 1, 0}, +{ 4, s_40_11, 0, 1, 0}, +{ 10, s_40_12, 0, 1, 0}, +{ 4, s_40_13, 0, 1, 0}, +{ 6, s_40_14, 0, 1, 0}, +{ 6, s_40_15, 0, 1, 0}, +{ 6, s_40_16, 0, 1, 0}, +{ 8, s_40_17, 0, 1, 0}, +{ 6, s_40_18, 0, 1, 0}, +{ 6, s_40_19, 0, 1, 0}, +{ 6, s_40_20, 0, 1, 0}, +{ 8, s_40_21, 0, 1, 0}, +{ 6, s_40_22, 0, 1, 0}, +{ 6, s_40_23, 0, 1, 0}, +{ 6, s_40_24, 0, 1, 0}, +{ 8, s_40_25, 0, 1, 0}, +{ 6, s_40_26, 0, 1, 0}, +{ 6, s_40_27, 0, 1, 0}, +{ 6, s_40_28, 0, 1, 0}, +{ 6, s_40_29, 0, 1, 0}, +{ 6, s_40_30, 0, 1, 0} +}; + +static const symbol s_41_0[8] = { 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x81, 0xCF, 0x80 }; +static const symbol s_41_1[6] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_41_2[8] = { 0xCE, 0xB8, 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x81 }; +static const symbol s_41_3[6] = { 0xCE, 0xBD, 0xCF, 0x84, 0xCF, 0x81 }; +static const symbol s_41_4[8] = { 0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_41_5[8] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_41_6[6] = { 0xCE, 0xB1, 0xCE, 0xB2, 0xCF, 0x81 }; +static const symbol s_41_7[8] = { 0xCE, 0xBC, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x81 }; +static const symbol s_41_8[2] = { 0xCF, 0x85 }; +static const symbol s_41_9[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCF, 0x81, 0xCF, 0x86 }; +static const symbol s_41_10[6] = { 0xCE, 0xBD, 0xCE, 0xB9, 0xCF, 0x86 }; +static const symbol s_41_11[6] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xB3 }; +static const symbol s_41_12[2] = { 0xCE, 0xB4 }; +static const symbol s_41_13[4] = { 0xCE, 0xB1, 0xCE, 0xB4 }; +static const symbol s_41_14[2] = { 0xCE, 0xB8 }; +static const symbol s_41_15[4] = { 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_41_16[4] = { 0xCF, 0x83, 0xCE, 0xBA }; +static const symbol s_41_17[6] = { 0xCF, 0x84, 0xCE, 0xBF, 0xCE, 0xBA }; +static const symbol s_41_18[6] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBB }; +static const symbol s_41_19[14] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB }; +static const symbol s_41_20[8] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB5, 0xCE, 0xBB }; +static const symbol s_41_21[4] = { 0xCE, 0xB5, 0xCE, 0xBC }; +static const symbol s_41_22[4] = { 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_41_23[6] = { 0xCE, 0xB2, 0xCE, 0xB5, 0xCE, 0xBD }; +static const symbol s_41_24[10] = { 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBD }; +static const struct among a_41[25] = { +{ 8, s_41_0, 0, 1, 0}, +{ 6, s_41_1, 0, 1, 0}, +{ 8, s_41_2, 0, 1, 0}, +{ 6, s_41_3, 0, 1, 0}, +{ 8, s_41_4, 0, 1, 0}, +{ 8, s_41_5, 0, 1, 0}, +{ 6, s_41_6, 0, 1, 0}, +{ 8, s_41_7, 0, 1, 0}, +{ 2, s_41_8, 0, 1, 0}, +{ 8, s_41_9, 0, 1, 0}, +{ 6, s_41_10, 0, 1, 0}, +{ 6, s_41_11, 0, 1, 0}, +{ 2, s_41_12, 0, 1, 0}, { 4, s_41_13, -1, 1, 0}, -{ 6, s_41_14, -1, 1, 0}, -{ 6, s_41_15, -1, 1, 0}, -{ 6, s_41_16, -1, 1, 0}, -{ 8, s_41_17, -1, 1, 0}, -{ 6, s_41_18, -1, 1, 0}, -{ 6, s_41_19, -1, 1, 0}, -{ 6, s_41_20, -1, 1, 0}, -{ 8, s_41_21, -1, 1, 0}, -{ 6, s_41_22, -1, 1, 0}, -{ 6, s_41_23, -1, 1, 0}, -{ 6, s_41_24, -1, 1, 0}, -{ 8, s_41_25, -1, 1, 0}, -{ 6, s_41_26, -1, 1, 0}, -{ 6, s_41_27, -1, 1, 0}, -{ 6, s_41_28, -1, 1, 0}, -{ 6, s_41_29, -1, 1, 0}, -{ 6, s_41_30, -1, 1, 0} -}; - -static const symbol s_42_0[8] = { 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x81, 0xCF, 0x80 }; -static const symbol s_42_1[6] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_42_2[8] = { 0xCE, 0xB8, 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x81 }; -static const symbol s_42_3[6] = { 0xCE, 0xBD, 0xCF, 0x84, 0xCF, 0x81 }; -static const symbol s_42_4[8] = { 0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_42_5[8] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_42_6[6] = { 0xCE, 0xB1, 0xCE, 0xB2, 0xCF, 0x81 }; -static const symbol s_42_7[8] = { 0xCE, 0xBC, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x81 }; -static const symbol s_42_8[2] = { 0xCF, 0x85 }; -static const symbol s_42_9[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCF, 0x81, 0xCF, 0x86 }; -static const symbol s_42_10[6] = { 0xCE, 0xBD, 0xCE, 0xB9, 0xCF, 0x86 }; -static const symbol s_42_11[6] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xB3 }; -static const symbol s_42_12[2] = { 0xCE, 0xB4 }; -static const symbol s_42_13[4] = { 0xCE, 0xB1, 0xCE, 0xB4 }; -static const symbol s_42_14[2] = { 0xCE, 0xB8 }; -static const symbol s_42_15[4] = { 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_42_16[4] = { 0xCF, 0x83, 0xCE, 0xBA }; -static const symbol s_42_17[6] = { 0xCF, 0x84, 0xCE, 0xBF, 0xCE, 0xBA }; -static const symbol s_42_18[6] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBB }; -static const symbol s_42_19[14] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB }; -static const symbol s_42_20[8] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB5, 0xCE, 0xBB }; -static const symbol s_42_21[4] = { 0xCE, 0xB5, 0xCE, 0xBC }; -static const symbol s_42_22[4] = { 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_42_23[6] = { 0xCE, 0xB2, 0xCE, 0xB5, 0xCE, 0xBD }; -static const symbol s_42_24[10] = { 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBD }; - -static const struct among a_42[25] = -{ -{ 8, s_42_0, -1, 1, 0}, -{ 6, s_42_1, -1, 1, 0}, -{ 8, s_42_2, -1, 1, 0}, -{ 6, s_42_3, -1, 1, 0}, -{ 8, s_42_4, -1, 1, 0}, -{ 8, s_42_5, -1, 1, 0}, -{ 6, s_42_6, -1, 1, 0}, -{ 8, s_42_7, -1, 1, 0}, -{ 2, s_42_8, -1, 1, 0}, -{ 8, s_42_9, -1, 1, 0}, -{ 6, s_42_10, -1, 1, 0}, -{ 6, s_42_11, -1, 1, 0}, -{ 2, s_42_12, -1, 1, 0}, -{ 4, s_42_13, 12, 1, 0}, -{ 2, s_42_14, -1, 1, 0}, -{ 4, s_42_15, 14, 1, 0}, -{ 4, s_42_16, -1, 1, 0}, -{ 6, s_42_17, -1, 1, 0}, -{ 6, s_42_18, -1, 1, 0}, -{ 14, s_42_19, -1, 1, 0}, -{ 8, s_42_20, -1, 1, 0}, -{ 4, s_42_21, -1, 1, 0}, -{ 4, s_42_22, -1, 1, 0}, -{ 6, s_42_23, -1, 1, 0}, -{ 10, s_42_24, -1, 1, 0} -}; - -static const symbol s_43_0[10] = { 0xCF, 0x89, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x83 }; -static const symbol s_43_1[10] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x83 }; - -static const struct among a_43[2] = -{ -{ 10, s_43_0, -1, 1, 0}, -{ 10, s_43_1, -1, 1, 0} -}; - -static const symbol s_44_0[12] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_44_1[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; - -static const struct among a_44[2] = -{ -{ 12, s_44_0, -1, 1, 0}, -{ 14, s_44_1, 0, 1, 0} -}; - -static const symbol s_45_0[2] = { 0xCF, 0x80 }; -static const symbol s_45_1[4] = { 0xCE, 0xB1, 0xCF, 0x80 }; -static const symbol s_45_2[12] = { 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x80 }; -static const symbol s_45_3[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBC, 0xCF, 0x80 }; -static const symbol s_45_4[10] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBC, 0xCF, 0x80 }; -static const symbol s_45_5[14] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x86 }; - -static const struct among a_45[6] = -{ -{ 2, s_45_0, -1, 1, 0}, -{ 4, s_45_1, 0, 1, 0}, -{ 12, s_45_2, 1, 1, 0}, -{ 8, s_45_3, 0, 1, 0}, -{ 10, s_45_4, 3, 1, 0}, -{ 14, s_45_5, -1, 1, 0} -}; - -static const symbol s_46_0[4] = { 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_46_1[6] = { 0xCE, 0xBD, 0xCE, 0xB9, 0xCF, 0x83 }; -static const symbol s_46_2[2] = { 0xCE, 0xB6 }; -static const symbol s_46_3[4] = { 0xCE, 0xB1, 0xCE, 0xBB }; -static const symbol s_46_4[14] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB }; -static const symbol s_46_5[10] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCF, 0x84, 0xCE, 0xB5, 0xCE, 0xBB }; -static const symbol s_46_6[2] = { 0xCE, 0xBC }; -static const symbol s_46_7[2] = { 0xCE, 0xBE }; -static const symbol s_46_8[6] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF }; - -static const struct among a_46[9] = -{ -{ 4, s_46_0, -1, 1, 0}, -{ 6, s_46_1, -1, 1, 0}, -{ 2, s_46_2, -1, 1, 0}, -{ 4, s_46_3, -1, 1, 0}, -{ 14, s_46_4, 3, 1, 0}, -{ 10, s_46_5, -1, 1, 0}, -{ 2, s_46_6, -1, 1, 0}, -{ 2, s_46_7, -1, 1, 0}, -{ 6, s_46_8, -1, 1, 0} -}; - -static const symbol s_47_0[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_47_1[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1 }; -static const symbol s_47_2[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5 }; - -static const struct among a_47[3] = -{ -{ 12, s_47_0, -1, 1, 0}, -{ 10, s_47_1, -1, 1, 0}, -{ 10, s_47_2, -1, 1, 0} -}; - -static const symbol s_48_0[4] = { 0xCF, 0x83, 0xCF, 0x86 }; -static const symbol s_48_1[8] = { 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB8 }; -static const symbol s_48_2[6] = { 0xCF, 0x80, 0xCE, 0xB9, 0xCE, 0xB8 }; -static const symbol s_48_3[4] = { 0xCE, 0xBF, 0xCE, 0xB8 }; -static const symbol s_48_4[10] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBB }; -static const symbol s_48_5[8] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCF, 0x89, 0xCE, 0xBB }; - -static const struct among a_48[6] = -{ -{ 4, s_48_0, -1, 1, 0}, -{ 8, s_48_1, -1, 1, 0}, -{ 6, s_48_2, -1, 1, 0}, -{ 4, s_48_3, -1, 1, 0}, -{ 10, s_48_4, -1, 1, 0}, -{ 8, s_48_5, -1, 1, 0} -}; - -static const symbol s_49_0[2] = { 0xCE, 0xB8 }; -static const symbol s_49_1[10] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB8 }; -static const symbol s_49_2[18] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_49_3[8] = { 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCE, 0xB8 }; -static const symbol s_49_4[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB8 }; - -static const struct among a_49[5] = -{ -{ 2, s_49_0, -1, 1, 0}, -{ 10, s_49_1, 0, 1, 0}, -{ 18, s_49_2, 0, 1, 0}, -{ 8, s_49_3, 0, 1, 0}, -{ 8, s_49_4, 0, 1, 0} -}; - -static const symbol s_50_0[8] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_50_1[6] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1 }; -static const symbol s_50_2[6] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5 }; - -static const struct among a_50[3] = -{ -{ 8, s_50_0, -1, 1, 0}, -{ 6, s_50_1, -1, 1, 0}, -{ 6, s_50_2, -1, 1, 0} -}; - -static const symbol s_51_0[8] = { 0xCE, 0xB2, 0xCE, 0xBB, 0xCE, 0xB5, 0xCF, 0x80 }; -static const symbol s_51_1[10] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xB4, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_51_2[8] = { 0xCF, 0x80, 0xCF, 0x81, 0xCF, 0x89, 0xCF, 0x84 }; -static const symbol s_51_3[10] = { 0xCE, 0xBA, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84 }; -static const symbol s_51_4[12] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x87 }; -static const symbol s_51_5[6] = { 0xCE, 0xBB, 0xCE, 0xB1, 0xCF, 0x87 }; -static const symbol s_51_6[6] = { 0xCF, 0x86, 0xCE, 0xB1, 0xCE, 0xB3 }; -static const symbol s_51_7[6] = { 0xCE, 0xBB, 0xCE, 0xB7, 0xCE, 0xB3 }; -static const symbol s_51_8[8] = { 0xCF, 0x86, 0xCF, 0x81, 0xCF, 0x85, 0xCE, 0xB4 }; -static const symbol s_51_9[12] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB9, 0xCE, 0xBB }; -static const symbol s_51_10[8] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB }; -static const symbol s_51_11[4] = { 0xCE, 0xBF, 0xCE, 0xBC }; - -static const struct among a_51[12] = -{ -{ 8, s_51_0, -1, 1, 0}, -{ 10, s_51_1, -1, 1, 0}, -{ 8, s_51_2, -1, 1, 0}, -{ 10, s_51_3, -1, 1, 0}, -{ 12, s_51_4, -1, 1, 0}, -{ 6, s_51_5, -1, 1, 0}, -{ 6, s_51_6, -1, 1, 0}, -{ 6, s_51_7, -1, 1, 0}, -{ 8, s_51_8, -1, 1, 0}, -{ 12, s_51_9, -1, 1, 0}, -{ 8, s_51_10, -1, 1, 0}, -{ 4, s_51_11, -1, 1, 0} -}; - -static const symbol s_52_0[10] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xBB, 0xCE, 0xB9, 0xCF, 0x80 }; -static const symbol s_52_1[2] = { 0xCF, 0x81 }; -static const symbol s_52_2[10] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x81 }; -static const symbol s_52_3[16] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCF, 0x86, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_52_4[6] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x84 }; -static const symbol s_52_5[14] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB5, 0xCF, 0x85 }; -static const symbol s_52_6[16] = { 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x85, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB5, 0xCF, 0x85 }; -static const symbol s_52_7[6] = { 0xCE, 0xBB, 0xCE, 0xB5, 0xCF, 0x87 }; -static const symbol s_52_8[6] = { 0xCF, 0x84, 0xCF, 0x83, 0xCE, 0xB1 }; -static const symbol s_52_9[6] = { 0xCF, 0x87, 0xCE, 0xB1, 0xCE, 0xB4 }; -static const symbol s_52_10[6] = { 0xCE, 0xBC, 0xCE, 0xB5, 0xCE, 0xB4 }; -static const symbol s_52_11[12] = { 0xCE, 0xBB, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x80, 0xCE, 0xB9, 0xCE, 0xB4 }; -static const symbol s_52_12[4] = { 0xCE, 0xB4, 0xCE, 0xB5 }; -static const symbol s_52_13[6] = { 0xCF, 0x80, 0xCE, 0xBB, 0xCE, 0xB5 }; -static const symbol s_52_14[10] = { 0xCE, 0xBC, 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB6 }; -static const symbol s_52_15[12] = { 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xB6 }; -static const symbol s_52_16[6] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCE, 0xB8 }; -static const symbol s_52_17[12] = { 0xCF, 0x86, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBA }; -static const symbol s_52_18[6] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xBA }; -static const symbol s_52_19[8] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB7, 0xCE, 0xBA }; -static const symbol s_52_20[2] = { 0xCE, 0xBB }; -static const symbol s_52_21[2] = { 0xCE, 0xBC }; -static const symbol s_52_22[4] = { 0xCE, 0xB1, 0xCE, 0xBC }; -static const symbol s_52_23[8] = { 0xCE, 0xB2, 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBC }; -static const symbol s_52_24[14] = { 0xCF, 0x85, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB5, 0xCE, 0xB9, 0xCE, 0xBD }; - -static const struct among a_52[25] = -{ -{ 10, s_52_0, -1, 1, 0}, -{ 2, s_52_1, -1, 1, 0}, -{ 10, s_52_2, 1, 1, 0}, -{ 16, s_52_3, 1, 1, 0}, -{ 6, s_52_4, -1, 1, 0}, -{ 14, s_52_5, -1, 1, 0}, -{ 16, s_52_6, -1, 1, 0}, -{ 6, s_52_7, -1, 1, 0}, -{ 6, s_52_8, -1, 1, 0}, -{ 6, s_52_9, -1, 1, 0}, -{ 6, s_52_10, -1, 1, 0}, -{ 12, s_52_11, -1, 1, 0}, -{ 4, s_52_12, -1, 1, 0}, -{ 6, s_52_13, -1, 1, 0}, -{ 10, s_52_14, -1, 1, 0}, -{ 12, s_52_15, -1, 1, 0}, -{ 6, s_52_16, -1, 1, 0}, -{ 12, s_52_17, -1, 1, 0}, -{ 6, s_52_18, -1, 1, 0}, -{ 8, s_52_19, -1, 1, 0}, -{ 2, s_52_20, -1, 1, 0}, -{ 2, s_52_21, -1, 1, 0}, -{ 4, s_52_22, 21, 1, 0}, -{ 8, s_52_23, 21, 1, 0}, -{ 14, s_52_24, -1, 1, 0} -}; - -static const symbol s_53_0[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_53_1[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1 }; -static const symbol s_53_2[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB5 }; - -static const struct among a_53[3] = -{ -{ 10, s_53_0, -1, 1, 0}, -{ 8, s_53_1, -1, 1, 0}, -{ 8, s_53_2, -1, 1, 0} -}; - -static const symbol s_54_0[4] = { 0xCF, 0x81, 0xCF, 0x80 }; -static const symbol s_54_1[4] = { 0xCF, 0x80, 0xCF, 0x81 }; -static const symbol s_54_2[4] = { 0xCF, 0x86, 0xCF, 0x81 }; -static const symbol s_54_3[8] = { 0xCF, 0x87, 0xCE, 0xBF, 0xCF, 0x81, 0xCF, 0x84 }; -static const symbol s_54_4[4] = { 0xCF, 0x83, 0xCF, 0x86 }; -static const symbol s_54_5[4] = { 0xCE, 0xBF, 0xCF, 0x86 }; -static const symbol s_54_6[6] = { 0xCF, 0x88, 0xCE, 0xBF, 0xCF, 0x86 }; -static const symbol s_54_7[6] = { 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x87 }; -static const symbol s_54_8[12] = { 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x85, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x87 }; -static const symbol s_54_9[6] = { 0xCF, 0x80, 0xCE, 0xB5, 0xCE, 0xBB }; -static const symbol s_54_10[4] = { 0xCE, 0xBB, 0xCE, 0xBB }; -static const symbol s_54_11[8] = { 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xB7, 0xCE, 0xBD }; - -static const struct among a_54[12] = -{ -{ 4, s_54_0, -1, 1, 0}, -{ 4, s_54_1, -1, 1, 0}, -{ 4, s_54_2, -1, 1, 0}, -{ 8, s_54_3, -1, 1, 0}, -{ 4, s_54_4, -1, 1, 0}, -{ 4, s_54_5, -1, 1, 0}, -{ 6, s_54_6, 5, -1, 0}, -{ 6, s_54_7, -1, 1, 0}, -{ 12, s_54_8, 7, -1, 0}, -{ 6, s_54_9, -1, 1, 0}, -{ 4, s_54_10, -1, 1, 0}, -{ 8, s_54_11, -1, 1, 0} -}; - -static const symbol s_55_0[2] = { 0xCF, 0x80 }; -static const symbol s_55_1[6] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x80 }; -static const symbol s_55_2[8] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x85, 0xCF, 0x80 }; -static const symbol s_55_3[10] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x84, 0xCE, 0xB9, 0xCF, 0x80 }; -static const symbol s_55_4[8] = { 0xCE, 0xB1, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x80 }; -static const symbol s_55_5[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBC, 0xCF, 0x80 }; -static const symbol s_55_6[16] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x89, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_55_7[14] = { 0xCF, 0x83, 0xCE, 0xB9, 0xCE, 0xB4, 0xCE, 0xB7, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_55_8[12] = { 0xCE, 0xB4, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_55_9[8] = { 0xCE, 0xBD, 0xCE, 0xB5, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_55_10[16] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_55_11[8] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_55_12[2] = { 0xCF, 0x81 }; -static const symbol s_55_13[4] = { 0xCF, 0x84, 0xCF, 0x81 }; -static const symbol s_55_14[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x81 }; -static const symbol s_55_15[10] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_55_16[6] = { 0xCF, 0x87, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_55_17[8] = { 0xCE, 0xB1, 0xCF, 0x87, 0xCE, 0xB1, 0xCF, 0x81 }; -static const symbol s_55_18[8] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_55_19[2] = { 0xCF, 0x84 }; -static const symbol s_55_20[10] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x85, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_55_21[10] = { 0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_55_22[10] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_55_23[12] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_55_24[8] = { 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCF, 0x84 }; -static const symbol s_55_25[8] = { 0xCE, 0xB5, 0xCF, 0x80, 0xCE, 0xB9, 0xCF, 0x84 }; -static const symbol s_55_26[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84 }; -static const symbol s_55_27[8] = { 0xCF, 0x85, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x84 }; -static const symbol s_55_28[8] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x84 }; -static const symbol s_55_29[8] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x84 }; -static const symbol s_55_30[10] = { 0xCE, 0xBD, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x84 }; -static const symbol s_55_31[6] = { 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x85 }; -static const symbol s_55_32[10] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xBB, 0xCF, 0x85, 0xCF, 0x86 }; -static const symbol s_55_33[4] = { 0xCE, 0xB1, 0xCF, 0x86 }; -static const symbol s_55_34[6] = { 0xCE, 0xBE, 0xCE, 0xB5, 0xCF, 0x86 }; -static const symbol s_55_35[8] = { 0xCE, 0xB1, 0xCE, 0xB4, 0xCE, 0xB7, 0xCF, 0x86 }; -static const symbol s_55_36[8] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x86 }; -static const symbol s_55_37[12] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB, 0xCE, 0xB9 }; -static const symbol s_55_38[2] = { 0xCE, 0xBB }; -static const symbol s_55_39[8] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBB }; -static const symbol s_55_40[2] = { 0xCE, 0xBC }; -static const symbol s_55_41[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBB, 0xCE, 0xB1, 0xCE, 0xBC }; -static const symbol s_55_42[4] = { 0xCE, 0xB5, 0xCE, 0xBD }; -static const symbol s_55_43[12] = { 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB2, 0xCE, 0xB5, 0xCE, 0xBD }; - -static const struct among a_55[44] = -{ -{ 2, s_55_0, -1, 1, 0}, +{ 2, s_41_14, 0, 1, 0}, +{ 4, s_41_15, -1, 1, 0}, +{ 4, s_41_16, 0, 1, 0}, +{ 6, s_41_17, 0, 1, 0}, +{ 6, s_41_18, 0, 1, 0}, +{ 14, s_41_19, 0, 1, 0}, +{ 8, s_41_20, 0, 1, 0}, +{ 4, s_41_21, 0, 1, 0}, +{ 4, s_41_22, 0, 1, 0}, +{ 6, s_41_23, 0, 1, 0}, +{ 10, s_41_24, 0, 1, 0} +}; + +static const symbol s_42_0[10] = { 0xCF, 0x89, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x83 }; +static const symbol s_42_1[10] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x83 }; +static const struct among a_42[2] = { +{ 10, s_42_0, 0, 1, 0}, +{ 10, s_42_1, 0, 1, 0} +}; + +static const symbol s_43_0[12] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_43_1[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const struct among a_43[2] = { +{ 12, s_43_0, 0, 1, 0}, +{ 14, s_43_1, -1, 1, 0} +}; + +static const symbol s_44_0[2] = { 0xCF, 0x80 }; +static const symbol s_44_1[4] = { 0xCE, 0xB1, 0xCF, 0x80 }; +static const symbol s_44_2[12] = { 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x80 }; +static const symbol s_44_3[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBC, 0xCF, 0x80 }; +static const symbol s_44_4[10] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBC, 0xCF, 0x80 }; +static const symbol s_44_5[14] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x86 }; +static const struct among a_44[6] = { +{ 2, s_44_0, 0, 1, 0}, +{ 4, s_44_1, -1, 1, 0}, +{ 12, s_44_2, -1, 1, 0}, +{ 8, s_44_3, -3, 1, 0}, +{ 10, s_44_4, -1, 1, 0}, +{ 14, s_44_5, 0, 1, 0} +}; + +static const symbol s_45_0[4] = { 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_45_1[6] = { 0xCE, 0xBD, 0xCE, 0xB9, 0xCF, 0x83 }; +static const symbol s_45_2[2] = { 0xCE, 0xB6 }; +static const symbol s_45_3[4] = { 0xCE, 0xB1, 0xCE, 0xBB }; +static const symbol s_45_4[14] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB }; +static const symbol s_45_5[10] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCF, 0x84, 0xCE, 0xB5, 0xCE, 0xBB }; +static const symbol s_45_6[2] = { 0xCE, 0xBC }; +static const symbol s_45_7[2] = { 0xCE, 0xBE }; +static const symbol s_45_8[6] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF }; +static const struct among a_45[9] = { +{ 4, s_45_0, 0, 1, 0}, +{ 6, s_45_1, 0, 1, 0}, +{ 2, s_45_2, 0, 1, 0}, +{ 4, s_45_3, 0, 1, 0}, +{ 14, s_45_4, -1, 1, 0}, +{ 10, s_45_5, 0, 1, 0}, +{ 2, s_45_6, 0, 1, 0}, +{ 2, s_45_7, 0, 1, 0}, +{ 6, s_45_8, 0, 1, 0} +}; + +static const symbol s_46_0[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_46_1[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1 }; +static const symbol s_46_2[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5 }; +static const struct among a_46[3] = { +{ 12, s_46_0, 0, 1, 0}, +{ 10, s_46_1, 0, 1, 0}, +{ 10, s_46_2, 0, 1, 0} +}; + +static const symbol s_47_0[4] = { 0xCF, 0x83, 0xCF, 0x86 }; +static const symbol s_47_1[8] = { 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB8 }; +static const symbol s_47_2[6] = { 0xCF, 0x80, 0xCE, 0xB9, 0xCE, 0xB8 }; +static const symbol s_47_3[4] = { 0xCE, 0xBF, 0xCE, 0xB8 }; +static const symbol s_47_4[10] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBB }; +static const symbol s_47_5[8] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCF, 0x89, 0xCE, 0xBB }; +static const struct among a_47[6] = { +{ 4, s_47_0, 0, 1, 0}, +{ 8, s_47_1, 0, 1, 0}, +{ 6, s_47_2, 0, 1, 0}, +{ 4, s_47_3, 0, 1, 0}, +{ 10, s_47_4, 0, 1, 0}, +{ 8, s_47_5, 0, 1, 0} +}; + +static const symbol s_48_0[2] = { 0xCE, 0xB8 }; +static const symbol s_48_1[10] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB8 }; +static const symbol s_48_2[18] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_48_3[8] = { 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCE, 0xB8 }; +static const symbol s_48_4[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB8 }; +static const struct among a_48[5] = { +{ 2, s_48_0, 0, 1, 0}, +{ 10, s_48_1, -1, 1, 0}, +{ 18, s_48_2, -2, 1, 0}, +{ 8, s_48_3, -3, 1, 0}, +{ 8, s_48_4, -4, 1, 0} +}; + +static const symbol s_49_0[8] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_49_1[6] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1 }; +static const symbol s_49_2[6] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB5 }; +static const struct among a_49[3] = { +{ 8, s_49_0, 0, 1, 0}, +{ 6, s_49_1, 0, 1, 0}, +{ 6, s_49_2, 0, 1, 0} +}; + +static const symbol s_50_0[8] = { 0xCE, 0xB2, 0xCE, 0xBB, 0xCE, 0xB5, 0xCF, 0x80 }; +static const symbol s_50_1[10] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xB4, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_50_2[8] = { 0xCF, 0x80, 0xCF, 0x81, 0xCF, 0x89, 0xCF, 0x84 }; +static const symbol s_50_3[10] = { 0xCE, 0xBA, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84 }; +static const symbol s_50_4[12] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x87 }; +static const symbol s_50_5[6] = { 0xCE, 0xBB, 0xCE, 0xB1, 0xCF, 0x87 }; +static const symbol s_50_6[6] = { 0xCF, 0x86, 0xCE, 0xB1, 0xCE, 0xB3 }; +static const symbol s_50_7[6] = { 0xCE, 0xBB, 0xCE, 0xB7, 0xCE, 0xB3 }; +static const symbol s_50_8[8] = { 0xCF, 0x86, 0xCF, 0x81, 0xCF, 0x85, 0xCE, 0xB4 }; +static const symbol s_50_9[12] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB9, 0xCE, 0xBB }; +static const symbol s_50_10[8] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB }; +static const symbol s_50_11[4] = { 0xCE, 0xBF, 0xCE, 0xBC }; +static const struct among a_50[12] = { +{ 8, s_50_0, 0, 1, 0}, +{ 10, s_50_1, 0, 1, 0}, +{ 8, s_50_2, 0, 1, 0}, +{ 10, s_50_3, 0, 1, 0}, +{ 12, s_50_4, 0, 1, 0}, +{ 6, s_50_5, 0, 1, 0}, +{ 6, s_50_6, 0, 1, 0}, +{ 6, s_50_7, 0, 1, 0}, +{ 8, s_50_8, 0, 1, 0}, +{ 12, s_50_9, 0, 1, 0}, +{ 8, s_50_10, 0, 1, 0}, +{ 4, s_50_11, 0, 1, 0} +}; + +static const symbol s_51_0[10] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xBB, 0xCE, 0xB9, 0xCF, 0x80 }; +static const symbol s_51_1[2] = { 0xCF, 0x81 }; +static const symbol s_51_2[10] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x81 }; +static const symbol s_51_3[16] = { 0xCE, 0xB5, 0xCE, 0xBD, 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCF, 0x86, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_51_4[6] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x84 }; +static const symbol s_51_5[14] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB5, 0xCF, 0x85 }; +static const symbol s_51_6[16] = { 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x85, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB5, 0xCF, 0x85 }; +static const symbol s_51_7[6] = { 0xCE, 0xBB, 0xCE, 0xB5, 0xCF, 0x87 }; +static const symbol s_51_8[6] = { 0xCF, 0x84, 0xCF, 0x83, 0xCE, 0xB1 }; +static const symbol s_51_9[6] = { 0xCF, 0x87, 0xCE, 0xB1, 0xCE, 0xB4 }; +static const symbol s_51_10[6] = { 0xCE, 0xBC, 0xCE, 0xB5, 0xCE, 0xB4 }; +static const symbol s_51_11[12] = { 0xCE, 0xBB, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x80, 0xCE, 0xB9, 0xCE, 0xB4 }; +static const symbol s_51_12[4] = { 0xCE, 0xB4, 0xCE, 0xB5 }; +static const symbol s_51_13[6] = { 0xCF, 0x80, 0xCE, 0xBB, 0xCE, 0xB5 }; +static const symbol s_51_14[10] = { 0xCE, 0xBC, 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB6 }; +static const symbol s_51_15[12] = { 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xB6 }; +static const symbol s_51_16[6] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCE, 0xB8 }; +static const symbol s_51_17[12] = { 0xCF, 0x86, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBA }; +static const symbol s_51_18[6] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xBA }; +static const symbol s_51_19[8] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB7, 0xCE, 0xBA }; +static const symbol s_51_20[2] = { 0xCE, 0xBB }; +static const symbol s_51_21[2] = { 0xCE, 0xBC }; +static const symbol s_51_22[4] = { 0xCE, 0xB1, 0xCE, 0xBC }; +static const symbol s_51_23[8] = { 0xCE, 0xB2, 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBC }; +static const symbol s_51_24[14] = { 0xCF, 0x85, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB5, 0xCE, 0xB9, 0xCE, 0xBD }; +static const struct among a_51[25] = { +{ 10, s_51_0, 0, 1, 0}, +{ 2, s_51_1, 0, 1, 0}, +{ 10, s_51_2, -1, 1, 0}, +{ 16, s_51_3, -2, 1, 0}, +{ 6, s_51_4, 0, 1, 0}, +{ 14, s_51_5, 0, 1, 0}, +{ 16, s_51_6, 0, 1, 0}, +{ 6, s_51_7, 0, 1, 0}, +{ 6, s_51_8, 0, 1, 0}, +{ 6, s_51_9, 0, 1, 0}, +{ 6, s_51_10, 0, 1, 0}, +{ 12, s_51_11, 0, 1, 0}, +{ 4, s_51_12, 0, 1, 0}, +{ 6, s_51_13, 0, 1, 0}, +{ 10, s_51_14, 0, 1, 0}, +{ 12, s_51_15, 0, 1, 0}, +{ 6, s_51_16, 0, 1, 0}, +{ 12, s_51_17, 0, 1, 0}, +{ 6, s_51_18, 0, 1, 0}, +{ 8, s_51_19, 0, 1, 0}, +{ 2, s_51_20, 0, 1, 0}, +{ 2, s_51_21, 0, 1, 0}, +{ 4, s_51_22, -1, 1, 0}, +{ 8, s_51_23, -2, 1, 0}, +{ 14, s_51_24, 0, 1, 0} +}; + +static const symbol s_52_0[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_52_1[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1 }; +static const symbol s_52_2[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB5 }; +static const struct among a_52[3] = { +{ 10, s_52_0, 0, 1, 0}, +{ 8, s_52_1, 0, 1, 0}, +{ 8, s_52_2, 0, 1, 0} +}; + +static const symbol s_53_0[4] = { 0xCF, 0x81, 0xCF, 0x80 }; +static const symbol s_53_1[4] = { 0xCF, 0x80, 0xCF, 0x81 }; +static const symbol s_53_2[4] = { 0xCF, 0x86, 0xCF, 0x81 }; +static const symbol s_53_3[8] = { 0xCF, 0x87, 0xCE, 0xBF, 0xCF, 0x81, 0xCF, 0x84 }; +static const symbol s_53_4[4] = { 0xCF, 0x83, 0xCF, 0x86 }; +static const symbol s_53_5[4] = { 0xCE, 0xBF, 0xCF, 0x86 }; +static const symbol s_53_6[6] = { 0xCF, 0x88, 0xCE, 0xBF, 0xCF, 0x86 }; +static const symbol s_53_7[6] = { 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x87 }; +static const symbol s_53_8[12] = { 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x85, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x87 }; +static const symbol s_53_9[6] = { 0xCF, 0x80, 0xCE, 0xB5, 0xCE, 0xBB }; +static const symbol s_53_10[4] = { 0xCE, 0xBB, 0xCE, 0xBB }; +static const symbol s_53_11[8] = { 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xB7, 0xCE, 0xBD }; +static const struct among a_53[12] = { +{ 4, s_53_0, 0, 1, 0}, +{ 4, s_53_1, 0, 1, 0}, +{ 4, s_53_2, 0, 1, 0}, +{ 8, s_53_3, 0, 1, 0}, +{ 4, s_53_4, 0, 1, 0}, +{ 4, s_53_5, 0, 1, 0}, +{ 6, s_53_6, -1, -1, 0}, +{ 6, s_53_7, 0, 1, 0}, +{ 12, s_53_8, -1, -1, 0}, +{ 6, s_53_9, 0, 1, 0}, +{ 4, s_53_10, 0, 1, 0}, +{ 8, s_53_11, 0, 1, 0} +}; + +static const symbol s_54_0[2] = { 0xCF, 0x80 }; +static const symbol s_54_1[6] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x80 }; +static const symbol s_54_2[8] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x85, 0xCF, 0x80 }; +static const symbol s_54_3[10] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x84, 0xCE, 0xB9, 0xCF, 0x80 }; +static const symbol s_54_4[8] = { 0xCE, 0xB1, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x80 }; +static const symbol s_54_5[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBC, 0xCF, 0x80 }; +static const symbol s_54_6[16] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x89, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54_7[14] = { 0xCF, 0x83, 0xCE, 0xB9, 0xCE, 0xB4, 0xCE, 0xB7, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54_8[12] = { 0xCE, 0xB4, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54_9[8] = { 0xCE, 0xBD, 0xCE, 0xB5, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54_10[16] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xBF, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54_11[8] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x80 }; +static const symbol s_54_12[2] = { 0xCF, 0x81 }; +static const symbol s_54_13[4] = { 0xCF, 0x84, 0xCF, 0x81 }; +static const symbol s_54_14[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x81 }; +static const symbol s_54_15[10] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_54_16[6] = { 0xCF, 0x87, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_54_17[8] = { 0xCE, 0xB1, 0xCF, 0x87, 0xCE, 0xB1, 0xCF, 0x81 }; +static const symbol s_54_18[8] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_54_19[2] = { 0xCF, 0x84 }; +static const symbol s_54_20[10] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x85, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_54_21[10] = { 0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_54_22[10] = { 0xCF, 0x80, 0xCF, 0x81, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_54_23[12] = { 0xCE, 0xB1, 0xCE, 0xB9, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x83, 0xCF, 0x84 }; +static const symbol s_54_24[8] = { 0xCE, 0xB4, 0xCE, 0xB9, 0xCE, 0xB1, 0xCF, 0x84 }; +static const symbol s_54_25[8] = { 0xCE, 0xB5, 0xCF, 0x80, 0xCE, 0xB9, 0xCF, 0x84 }; +static const symbol s_54_26[8] = { 0xCF, 0x83, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84 }; +static const symbol s_54_27[8] = { 0xCF, 0x85, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x84 }; +static const symbol s_54_28[8] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBF, 0xCF, 0x84 }; +static const symbol s_54_29[8] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x84 }; +static const symbol s_54_30[10] = { 0xCE, 0xBD, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x84 }; +static const symbol s_54_31[6] = { 0xCE, 0xBD, 0xCE, 0xB1, 0xCF, 0x85 }; +static const symbol s_54_32[10] = { 0xCF, 0x80, 0xCE, 0xBF, 0xCE, 0xBB, 0xCF, 0x85, 0xCF, 0x86 }; +static const symbol s_54_33[4] = { 0xCE, 0xB1, 0xCF, 0x86 }; +static const symbol s_54_34[6] = { 0xCE, 0xBE, 0xCE, 0xB5, 0xCF, 0x86 }; +static const symbol s_54_35[8] = { 0xCE, 0xB1, 0xCE, 0xB4, 0xCE, 0xB7, 0xCF, 0x86 }; +static const symbol s_54_36[8] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBC, 0xCF, 0x86 }; +static const symbol s_54_37[12] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB, 0xCE, 0xB9 }; +static const symbol s_54_38[2] = { 0xCE, 0xBB }; +static const symbol s_54_39[8] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xBB }; +static const symbol s_54_40[2] = { 0xCE, 0xBC }; +static const symbol s_54_41[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBB, 0xCE, 0xB1, 0xCE, 0xBC }; +static const symbol s_54_42[4] = { 0xCE, 0xB5, 0xCE, 0xBD }; +static const symbol s_54_43[12] = { 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB2, 0xCE, 0xB5, 0xCE, 0xBD }; +static const struct among a_54[44] = { +{ 2, s_54_0, 0, 1, 0}, +{ 6, s_54_1, -1, 1, 0}, +{ 8, s_54_2, -2, 1, 0}, +{ 10, s_54_3, -3, 1, 0}, +{ 8, s_54_4, -4, 1, 0}, +{ 8, s_54_5, -5, 1, 0}, +{ 16, s_54_6, -6, 1, 0}, +{ 14, s_54_7, -7, 1, 0}, +{ 12, s_54_8, -8, 1, 0}, +{ 8, s_54_9, -9, 1, 0}, +{ 16, s_54_10, -10, 1, 0}, +{ 8, s_54_11, -11, 1, 0}, +{ 2, s_54_12, 0, 1, 0}, +{ 4, s_54_13, -1, 1, 0}, +{ 6, s_54_14, -2, 1, 0}, +{ 10, s_54_15, -3, 1, 0}, +{ 6, s_54_16, -4, 1, 0}, +{ 8, s_54_17, -1, 1, 0}, +{ 8, s_54_18, -6, 1, 0}, +{ 2, s_54_19, 0, 1, 0}, +{ 10, s_54_20, -1, 1, 0}, +{ 10, s_54_21, -2, 1, 0}, +{ 10, s_54_22, -3, 1, 0}, +{ 12, s_54_23, -4, 1, 0}, +{ 8, s_54_24, -5, 1, 0}, +{ 8, s_54_25, -6, 1, 0}, +{ 8, s_54_26, -7, 1, 0}, +{ 8, s_54_27, -8, 1, 0}, +{ 8, s_54_28, -9, 1, 0}, +{ 8, s_54_29, -10, 1, 0}, +{ 10, s_54_30, -1, 1, 0}, +{ 6, s_54_31, 0, 1, 0}, +{ 10, s_54_32, 0, 1, 0}, +{ 4, s_54_33, 0, 1, 0}, +{ 6, s_54_34, 0, 1, 0}, +{ 8, s_54_35, 0, 1, 0}, +{ 8, s_54_36, 0, 1, 0}, +{ 12, s_54_37, 0, 1, 0}, +{ 2, s_54_38, 0, 1, 0}, +{ 8, s_54_39, -1, 1, 0}, +{ 2, s_54_40, 0, 1, 0}, +{ 10, s_54_41, -1, 1, 0}, +{ 4, s_54_42, 0, 1, 0}, +{ 12, s_54_43, -1, 1, 0} +}; + +static const symbol s_55_0[8] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_55_1[6] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1 }; +static const symbol s_55_2[6] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB5 }; +static const struct among a_55[3] = { +{ 8, s_55_0, 0, 1, 0}, { 6, s_55_1, 0, 1, 0}, -{ 8, s_55_2, 0, 1, 0}, -{ 10, s_55_3, 0, 1, 0}, -{ 8, s_55_4, 0, 1, 0}, -{ 8, s_55_5, 0, 1, 0}, -{ 16, s_55_6, 0, 1, 0}, -{ 14, s_55_7, 0, 1, 0}, -{ 12, s_55_8, 0, 1, 0}, -{ 8, s_55_9, 0, 1, 0}, -{ 16, s_55_10, 0, 1, 0}, -{ 8, s_55_11, 0, 1, 0}, -{ 2, s_55_12, -1, 1, 0}, -{ 4, s_55_13, 12, 1, 0}, -{ 6, s_55_14, 12, 1, 0}, -{ 10, s_55_15, 12, 1, 0}, -{ 6, s_55_16, 12, 1, 0}, -{ 8, s_55_17, 16, 1, 0}, -{ 8, s_55_18, 12, 1, 0}, -{ 2, s_55_19, -1, 1, 0}, -{ 10, s_55_20, 19, 1, 0}, -{ 10, s_55_21, 19, 1, 0}, -{ 10, s_55_22, 19, 1, 0}, -{ 12, s_55_23, 19, 1, 0}, -{ 8, s_55_24, 19, 1, 0}, -{ 8, s_55_25, 19, 1, 0}, -{ 8, s_55_26, 19, 1, 0}, -{ 8, s_55_27, 19, 1, 0}, -{ 8, s_55_28, 19, 1, 0}, -{ 8, s_55_29, 19, 1, 0}, -{ 10, s_55_30, 29, 1, 0}, -{ 6, s_55_31, -1, 1, 0}, -{ 10, s_55_32, -1, 1, 0}, -{ 4, s_55_33, -1, 1, 0}, -{ 6, s_55_34, -1, 1, 0}, -{ 8, s_55_35, -1, 1, 0}, -{ 8, s_55_36, -1, 1, 0}, -{ 12, s_55_37, -1, 1, 0}, -{ 2, s_55_38, -1, 1, 0}, -{ 8, s_55_39, 38, 1, 0}, -{ 2, s_55_40, -1, 1, 0}, -{ 10, s_55_41, 40, 1, 0}, -{ 4, s_55_42, -1, 1, 0}, -{ 12, s_55_43, 42, 1, 0} -}; - -static const symbol s_56_0[8] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_56_1[6] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1 }; -static const symbol s_56_2[6] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB5 }; - -static const struct among a_56[3] = -{ -{ 8, s_56_0, -1, 1, 0}, -{ 6, s_56_1, -1, 1, 0}, -{ 6, s_56_2, -1, 1, 0} -}; - -static const symbol s_57_0[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85 }; -static const symbol s_57_1[6] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1 }; -static const symbol s_57_2[6] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5 }; - -static const struct among a_57[3] = -{ -{ 8, s_57_0, -1, 1, 0}, -{ 6, s_57_1, -1, 1, 0}, -{ 6, s_57_2, -1, 1, 0} -}; - -static const symbol s_58_0[2] = { 0xCE, 0xBD }; -static const symbol s_58_1[10] = { 0xCE, 0xB5, 0xCF, 0x80, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_58_2[14] = { 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xB4, 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_58_3[12] = { 0xCF, 0x87, 0xCE, 0xB5, 0xCF, 0x81, 0xCF, 0x83, 0xCE, 0xBF, 0xCE, 0xBD }; -static const symbol s_58_4[14] = { 0xCE, 0xBC, 0xCE, 0xB5, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBF, 0xCE, 0xBD }; -static const symbol s_58_5[12] = { 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB7, 0xCE, 0xBC, 0xCE, 0xBF, 0xCE, 0xBD }; - -static const struct among a_58[6] = -{ -{ 2, s_58_0, -1, 1, 0}, -{ 10, s_58_1, 0, 1, 0}, -{ 14, s_58_2, 0, 1, 0}, -{ 12, s_58_3, 0, 1, 0}, -{ 14, s_58_4, 0, 1, 0}, -{ 12, s_58_5, 0, 1, 0} -}; - -static const symbol s_59_0[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; - -static const struct among a_59[1] = -{ -{ 8, s_59_0, -1, 1, 0} -}; - -static const symbol s_60_0[4] = { 0xCF, 0x87, 0xCF, 0x81 }; -static const symbol s_60_1[10] = { 0xCE, 0xB4, 0xCF, 0x85, 0xCF, 0x83, 0xCF, 0x87, 0xCF, 0x81 }; -static const symbol s_60_2[8] = { 0xCE, 0xB5, 0xCF, 0x85, 0xCF, 0x87, 0xCF, 0x81 }; -static const symbol s_60_3[6] = { 0xCE, 0xB1, 0xCF, 0x87, 0xCF, 0x81 }; -static const symbol s_60_4[14] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xB9, 0xCE, 0xBD, 0xCE, 0xBF, 0xCF, 0x87, 0xCF, 0x81 }; -static const symbol s_60_5[12] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xB9, 0xCE, 0xBC, 0xCF, 0x88 }; -static const symbol s_60_6[4] = { 0xCF, 0x83, 0xCE, 0xB2 }; -static const symbol s_60_7[6] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xB2 }; -static const symbol s_60_8[6] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBB }; -static const symbol s_60_9[10] = { 0xCE, 0xB1, 0xCE, 0xB5, 0xCE, 0xB9, 0xCE, 0xBC, 0xCE, 0xBD }; - -static const struct among a_60[10] = -{ -{ 4, s_60_0, -1, 1, 0}, -{ 10, s_60_1, 0, 1, 0}, -{ 8, s_60_2, 0, 1, 0}, +{ 6, s_55_2, 0, 1, 0} +}; + +static const symbol s_56_0[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85 }; +static const symbol s_56_1[6] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1 }; +static const symbol s_56_2[6] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5 }; +static const struct among a_56[3] = { +{ 8, s_56_0, 0, 1, 0}, +{ 6, s_56_1, 0, 1, 0}, +{ 6, s_56_2, 0, 1, 0} +}; + +static const symbol s_57_0[2] = { 0xCE, 0xBD }; +static const symbol s_57_1[10] = { 0xCE, 0xB5, 0xCF, 0x80, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_57_2[14] = { 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xB4, 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_57_3[12] = { 0xCF, 0x87, 0xCE, 0xB5, 0xCF, 0x81, 0xCF, 0x83, 0xCE, 0xBF, 0xCE, 0xBD }; +static const symbol s_57_4[14] = { 0xCE, 0xBC, 0xCE, 0xB5, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBF, 0xCE, 0xBD }; +static const symbol s_57_5[12] = { 0xCE, 0xB5, 0xCF, 0x81, 0xCE, 0xB7, 0xCE, 0xBC, 0xCE, 0xBF, 0xCE, 0xBD }; +static const struct among a_57[6] = { +{ 2, s_57_0, 0, 1, 0}, +{ 10, s_57_1, -1, 1, 0}, +{ 14, s_57_2, -2, 1, 0}, +{ 12, s_57_3, -3, 1, 0}, +{ 14, s_57_4, -4, 1, 0}, +{ 12, s_57_5, -5, 1, 0} +}; + +static const symbol s_58_0[4] = { 0xCF, 0x87, 0xCF, 0x81 }; +static const symbol s_58_1[10] = { 0xCE, 0xB4, 0xCF, 0x85, 0xCF, 0x83, 0xCF, 0x87, 0xCF, 0x81 }; +static const symbol s_58_2[8] = { 0xCE, 0xB5, 0xCF, 0x85, 0xCF, 0x87, 0xCF, 0x81 }; +static const symbol s_58_3[6] = { 0xCE, 0xB1, 0xCF, 0x87, 0xCF, 0x81 }; +static const symbol s_58_4[14] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xB9, 0xCE, 0xBD, 0xCE, 0xBF, 0xCF, 0x87, 0xCF, 0x81 }; +static const symbol s_58_5[12] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xB9, 0xCE, 0xBC, 0xCF, 0x88 }; +static const symbol s_58_6[4] = { 0xCF, 0x83, 0xCE, 0xB2 }; +static const symbol s_58_7[6] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xB2 }; +static const symbol s_58_8[6] = { 0xCE, 0xB1, 0xCF, 0x80, 0xCE, 0xBB }; +static const symbol s_58_9[10] = { 0xCE, 0xB1, 0xCE, 0xB5, 0xCE, 0xB9, 0xCE, 0xBC, 0xCE, 0xBD }; +static const struct among a_58[10] = { +{ 4, s_58_0, 0, 1, 0}, +{ 10, s_58_1, -1, 1, 0}, +{ 8, s_58_2, -2, 1, 0}, +{ 6, s_58_3, -3, 1, 0}, +{ 14, s_58_4, -4, 1, 0}, +{ 12, s_58_5, 0, 1, 0}, +{ 4, s_58_6, 0, 1, 0}, +{ 6, s_58_7, -1, 1, 0}, +{ 6, s_58_8, 0, 1, 0}, +{ 10, s_58_9, 0, 1, 0} +}; + +static const symbol s_59_0[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; +static const symbol s_59_1[12] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; +static const symbol s_59_2[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; +static const struct among a_59[3] = { +{ 8, s_59_0, 0, 1, 0}, +{ 12, s_59_1, -1, 1, 0}, +{ 12, s_59_2, -2, 1, 0} +}; + +static const symbol s_60_0[2] = { 0xCF, 0x81 }; +static const symbol s_60_1[22] = { 0xCF, 0x83, 0xCF, 0x84, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x84, 0xCF, 0x83 }; +static const symbol s_60_2[18] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x84, 0xCF, 0x83 }; +static const symbol s_60_3[6] = { 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xB9 }; +static const symbol s_60_4[2] = { 0xCE, 0xBD }; +static const symbol s_60_5[8] = { 0xCE, 0xB5, 0xCE, 0xBE, 0xCF, 0x89, 0xCE, 0xBD }; +static const struct among a_60[6] = { +{ 2, s_60_0, 0, 1, 0}, +{ 22, s_60_1, 0, 1, 0}, +{ 18, s_60_2, 0, 1, 0}, { 6, s_60_3, 0, 1, 0}, -{ 14, s_60_4, 0, 1, 0}, -{ 12, s_60_5, -1, 1, 0}, -{ 4, s_60_6, -1, 1, 0}, -{ 6, s_60_7, 6, 1, 0}, -{ 6, s_60_8, -1, 1, 0}, -{ 10, s_60_9, -1, 1, 0} -}; - -static const symbol s_61_0[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; -static const symbol s_61_1[12] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; -static const symbol s_61_2[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB5 }; - -static const struct among a_61[3] = -{ -{ 8, s_61_0, -1, 1, 0}, -{ 12, s_61_1, 0, 1, 0}, -{ 12, s_61_2, 0, 1, 0} -}; - -static const symbol s_62_0[2] = { 0xCF, 0x81 }; -static const symbol s_62_1[22] = { 0xCF, 0x83, 0xCF, 0x84, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xB2, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x84, 0xCF, 0x83 }; -static const symbol s_62_2[18] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x84, 0xCF, 0x83 }; -static const symbol s_62_3[6] = { 0xCF, 0x83, 0xCF, 0x80, 0xCE, 0xB9 }; -static const symbol s_62_4[2] = { 0xCE, 0xBD }; -static const symbol s_62_5[8] = { 0xCE, 0xB5, 0xCE, 0xBE, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_62[6] = -{ -{ 2, s_62_0, -1, 1, 0}, -{ 22, s_62_1, -1, 1, 0}, -{ 18, s_62_2, -1, 1, 0}, -{ 6, s_62_3, -1, 1, 0}, -{ 2, s_62_4, -1, 1, 0}, -{ 8, s_62_5, 4, 1, 0} -}; - -static const symbol s_63_0[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5 }; -static const symbol s_63_1[12] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5 }; -static const symbol s_63_2[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5 }; - -static const struct among a_63[3] = -{ -{ 8, s_63_0, -1, 1, 0}, -{ 12, s_63_1, 0, 1, 0}, -{ 12, s_63_2, 0, 1, 0} -}; - -static const symbol s_64_0[10] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_64_1[16] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_64_2[16] = { 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_64_3[2] = { 0xCF, 0x86 }; -static const symbol s_64_4[2] = { 0xCF, 0x87 }; -static const symbol s_64_5[4] = { 0xCE, 0xB1, 0xCE, 0xB6 }; -static const symbol s_64_6[12] = { 0xCF, 0x89, 0xCF, 0x81, 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x80, 0xCE, 0xBB }; - -static const struct among a_64[7] = -{ -{ 10, s_64_0, -1, 1, 0}, -{ 16, s_64_1, 0, 1, 0}, -{ 16, s_64_2, -1, 1, 0}, -{ 2, s_64_3, -1, 1, 0}, -{ 2, s_64_4, -1, 1, 0}, -{ 4, s_64_5, -1, 1, 0}, -{ 12, s_64_6, -1, 1, 0} -}; - -static const symbol s_65_0[10] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x83 }; -static const symbol s_65_1[8] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1 }; -static const symbol s_65_2[10] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84, 0xCF, 0x89, 0xCE, 0xBD }; - -static const struct among a_65[3] = -{ -{ 10, s_65_0, -1, 1, 0}, -{ 8, s_65_1, -1, 1, 0}, -{ 10, s_65_2, -1, 1, 0} -}; - -static const symbol s_66_0[4] = { 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_66_1[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_66_2[4] = { 0xCE, 0xB1, 0xCF, 0x83 }; -static const symbol s_66_3[4] = { 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_66_4[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_66_5[8] = { 0xCE, 0xB7, 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83 }; -static const symbol s_66_6[4] = { 0xCE, 0xB7, 0xCF, 0x83 }; -static const symbol s_66_7[6] = { 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x83 }; -static const symbol s_66_8[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x83 }; -static const symbol s_66_9[4] = { 0xCE, 0xBF, 0xCF, 0x83 }; -static const symbol s_66_10[2] = { 0xCF, 0x85 }; -static const symbol s_66_11[4] = { 0xCE, 0xBF, 0xCF, 0x85 }; -static const symbol s_66_12[2] = { 0xCF, 0x89 }; -static const symbol s_66_13[6] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCF, 0x89 }; -static const symbol s_66_14[4] = { 0xCE, 0xB1, 0xCF, 0x89 }; -static const symbol s_66_15[6] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCF, 0x89 }; -static const symbol s_66_16[2] = { 0xCE, 0xB1 }; -static const symbol s_66_17[10] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1 }; -static const symbol s_66_18[12] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; -static const symbol s_66_19[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; -static const symbol s_66_20[12] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; -static const symbol s_66_21[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; -static const symbol s_66_22[2] = { 0xCE, 0xB5 }; -static const symbol s_66_23[14] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_24[12] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_25[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_26[14] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_27[16] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_28[14] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_29[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_30[10] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_31[10] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_32[10] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_33[14] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_34[8] = { 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_35[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_66_36[2] = { 0xCE, 0xB7 }; -static const symbol s_66_37[2] = { 0xCE, 0xB9 }; -static const symbol s_66_38[8] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_39[8] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_40[10] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_41[8] = { 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_42[8] = { 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_43[10] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_44[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_45[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_46[10] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_47[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_48[8] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_49[10] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_50[8] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; -static const symbol s_66_51[4] = { 0xCE, 0xB5, 0xCE, 0xB9 }; -static const symbol s_66_52[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5, 0xCE, 0xB9 }; -static const symbol s_66_53[6] = { 0xCE, 0xB1, 0xCE, 0xB5, 0xCE, 0xB9 }; -static const symbol s_66_54[8] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9 }; -static const symbol s_66_55[4] = { 0xCE, 0xBF, 0xCE, 0xB9 }; -static const symbol s_66_56[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_57[10] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_58[10] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_59[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_60[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_61[10] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_62[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_66_63[4] = { 0xCF, 0x89, 0xCE, 0xBD }; -static const symbol s_66_64[8] = { 0xCE, 0xB7, 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xBD }; -static const symbol s_66_65[4] = { 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_66[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_67[16] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_68[18] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_69[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_70[14] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_71[16] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_72[14] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_73[16] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_74[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_75[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_76[10] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_77[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_78[8] = { 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_79[10] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_80[8] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_81[8] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_82[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_66_83[2] = { 0xCE, 0xBF }; - -static const struct among a_66[84] = -{ -{ 4, s_66_0, -1, 1, 0}, -{ 6, s_66_1, 0, 1, 0}, -{ 4, s_66_2, -1, 1, 0}, -{ 4, s_66_3, -1, 1, 0}, -{ 8, s_66_4, 3, 1, 0}, -{ 8, s_66_5, 3, 1, 0}, -{ 4, s_66_6, -1, 1, 0}, -{ 6, s_66_7, -1, 1, 0}, -{ 10, s_66_8, 7, 1, 0}, -{ 4, s_66_9, -1, 1, 0}, -{ 2, s_66_10, -1, 1, 0}, -{ 4, s_66_11, 10, 1, 0}, -{ 2, s_66_12, -1, 1, 0}, -{ 6, s_66_13, 12, 1, 0}, -{ 4, s_66_14, 12, 1, 0}, -{ 6, s_66_15, 12, 1, 0}, -{ 2, s_66_16, -1, 1, 0}, -{ 10, s_66_17, 16, 1, 0}, -{ 12, s_66_18, 16, 1, 0}, -{ 14, s_66_19, 18, 1, 0}, -{ 12, s_66_20, 16, 1, 0}, -{ 14, s_66_21, 20, 1, 0}, -{ 2, s_66_22, -1, 1, 0}, -{ 14, s_66_23, 22, 1, 0}, -{ 12, s_66_24, 22, 1, 0}, -{ 14, s_66_25, 24, 1, 0}, -{ 14, s_66_26, 22, 1, 0}, -{ 16, s_66_27, 26, 1, 0}, -{ 14, s_66_28, 22, 1, 0}, -{ 12, s_66_29, 22, 1, 0}, -{ 10, s_66_30, 22, 1, 0}, -{ 10, s_66_31, 22, 1, 0}, -{ 10, s_66_32, 22, 1, 0}, -{ 14, s_66_33, 32, 1, 0}, -{ 8, s_66_34, 22, 1, 0}, -{ 12, s_66_35, 34, 1, 0}, -{ 2, s_66_36, -1, 1, 0}, -{ 2, s_66_37, -1, 1, 0}, -{ 8, s_66_38, 37, 1, 0}, -{ 8, s_66_39, 37, 1, 0}, -{ 10, s_66_40, 39, 1, 0}, -{ 8, s_66_41, 37, 1, 0}, -{ 8, s_66_42, 37, 1, 0}, -{ 10, s_66_43, 42, 1, 0}, -{ 12, s_66_44, 37, 1, 0}, -{ 14, s_66_45, 44, 1, 0}, -{ 10, s_66_46, 37, 1, 0}, -{ 10, s_66_47, 37, 1, 0}, -{ 8, s_66_48, 37, 1, 0}, -{ 10, s_66_49, 37, 1, 0}, -{ 8, s_66_50, 37, 1, 0}, -{ 4, s_66_51, 37, 1, 0}, -{ 8, s_66_52, 51, 1, 0}, -{ 6, s_66_53, 51, 1, 0}, -{ 8, s_66_54, 51, 1, 0}, -{ 4, s_66_55, 37, 1, 0}, -{ 6, s_66_56, -1, 1, 0}, -{ 10, s_66_57, 56, 1, 0}, -{ 10, s_66_58, 56, 1, 0}, -{ 12, s_66_59, 58, 1, 0}, -{ 10, s_66_60, 56, 1, 0}, -{ 10, s_66_61, 56, 1, 0}, -{ 12, s_66_62, 61, 1, 0}, -{ 4, s_66_63, -1, 1, 0}, -{ 8, s_66_64, 63, 1, 0}, -{ 4, s_66_65, -1, 1, 0}, -{ 10, s_66_66, 65, 1, 0}, -{ 16, s_66_67, 66, 1, 0}, -{ 18, s_66_68, 67, 1, 0}, -{ 8, s_66_69, 65, 1, 0}, -{ 14, s_66_70, 65, 1, 0}, -{ 16, s_66_71, 70, 1, 0}, -{ 14, s_66_72, 65, 1, 0}, -{ 16, s_66_73, 72, 1, 0}, -{ 12, s_66_74, 65, 1, 0}, -{ 14, s_66_75, 74, 1, 0}, -{ 10, s_66_76, 65, 1, 0}, -{ 12, s_66_77, 76, 1, 0}, -{ 8, s_66_78, 65, 1, 0}, -{ 10, s_66_79, 78, 1, 0}, -{ 8, s_66_80, 65, 1, 0}, -{ 8, s_66_81, 65, 1, 0}, -{ 12, s_66_82, 81, 1, 0}, -{ 2, s_66_83, -1, 1, 0} -}; - -static const symbol s_67_0[10] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_67_1[8] = { 0xCF, 0x85, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_67_2[8] = { 0xCF, 0x89, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_67_3[8] = { 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_67_4[10] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; -static const symbol s_67_5[8] = { 0xCF, 0x85, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; -static const symbol s_67_6[8] = { 0xCF, 0x89, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; -static const symbol s_67_7[8] = { 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; - -static const struct among a_67[8] = -{ -{ 10, s_67_0, -1, 1, 0}, -{ 8, s_67_1, -1, 1, 0}, -{ 8, s_67_2, -1, 1, 0}, -{ 8, s_67_3, -1, 1, 0}, -{ 10, s_67_4, -1, 1, 0}, -{ 8, s_67_5, -1, 1, 0}, -{ 8, s_67_6, -1, 1, 0}, -{ 8, s_67_7, -1, 1, 0} +{ 2, s_60_4, 0, 1, 0}, +{ 8, s_60_5, -1, 1, 0} +}; + +static const symbol s_61_0[8] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5 }; +static const symbol s_61_1[12] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5 }; +static const symbol s_61_2[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB5 }; +static const struct among a_61[3] = { +{ 8, s_61_0, 0, 1, 0}, +{ 12, s_61_1, -1, 1, 0}, +{ 12, s_61_2, -2, 1, 0} +}; + +static const symbol s_62_0[10] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_62_1[16] = { 0xCF, 0x80, 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_62_2[16] = { 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xBB, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_62_3[2] = { 0xCF, 0x86 }; +static const symbol s_62_4[2] = { 0xCF, 0x87 }; +static const symbol s_62_5[4] = { 0xCE, 0xB1, 0xCE, 0xB6 }; +static const symbol s_62_6[12] = { 0xCF, 0x89, 0xCF, 0x81, 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x80, 0xCE, 0xBB }; +static const struct among a_62[7] = { +{ 10, s_62_0, 0, 1, 0}, +{ 16, s_62_1, -1, 1, 0}, +{ 16, s_62_2, 0, 1, 0}, +{ 2, s_62_3, 0, 1, 0}, +{ 2, s_62_4, 0, 1, 0}, +{ 4, s_62_5, 0, 1, 0}, +{ 12, s_62_6, 0, 1, 0} +}; + +static const symbol s_63_0[10] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x83 }; +static const symbol s_63_1[8] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1 }; +static const symbol s_63_2[10] = { 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x84, 0xCF, 0x89, 0xCE, 0xBD }; +static const struct among a_63[3] = { +{ 10, s_63_0, 0, 1, 0}, +{ 8, s_63_1, 0, 1, 0}, +{ 10, s_63_2, 0, 1, 0} +}; + +static const symbol s_64_0[4] = { 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_64_1[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; +static const symbol s_64_2[4] = { 0xCE, 0xB1, 0xCF, 0x83 }; +static const symbol s_64_3[4] = { 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_64_4[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_64_5[8] = { 0xCE, 0xB7, 0xCE, 0xB4, 0xCE, 0xB5, 0xCF, 0x83 }; +static const symbol s_64_6[4] = { 0xCE, 0xB7, 0xCF, 0x83 }; +static const symbol s_64_7[6] = { 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x83 }; +static const symbol s_64_8[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x83 }; +static const symbol s_64_9[4] = { 0xCE, 0xBF, 0xCF, 0x83 }; +static const symbol s_64_10[2] = { 0xCF, 0x85 }; +static const symbol s_64_11[4] = { 0xCE, 0xBF, 0xCF, 0x85 }; +static const symbol s_64_12[2] = { 0xCF, 0x89 }; +static const symbol s_64_13[6] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCF, 0x89 }; +static const symbol s_64_14[4] = { 0xCE, 0xB1, 0xCF, 0x89 }; +static const symbol s_64_15[6] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCF, 0x89 }; +static const symbol s_64_16[2] = { 0xCE, 0xB1 }; +static const symbol s_64_17[10] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1 }; +static const symbol s_64_18[12] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; +static const symbol s_64_19[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; +static const symbol s_64_20[12] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; +static const symbol s_64_21[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCE, 0xB1 }; +static const symbol s_64_22[2] = { 0xCE, 0xB5 }; +static const symbol s_64_23[14] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_24[12] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_25[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_26[14] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_27[16] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_28[14] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_29[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_30[10] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_31[10] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_32[10] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_33[14] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_34[8] = { 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_35[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9, 0xCF, 0x84, 0xCE, 0xB5 }; +static const symbol s_64_36[2] = { 0xCE, 0xB7 }; +static const symbol s_64_37[2] = { 0xCE, 0xB9 }; +static const symbol s_64_38[8] = { 0xCE, 0xB1, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_39[8] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_40[10] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_41[8] = { 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_42[8] = { 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_43[10] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_44[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_45[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_46[10] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_47[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_48[8] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_49[10] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_50[8] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCE, 0xB9 }; +static const symbol s_64_51[4] = { 0xCE, 0xB5, 0xCE, 0xB9 }; +static const symbol s_64_52[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB5, 0xCE, 0xB9 }; +static const symbol s_64_53[6] = { 0xCE, 0xB1, 0xCE, 0xB5, 0xCE, 0xB9 }; +static const symbol s_64_54[8] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB9 }; +static const symbol s_64_55[4] = { 0xCE, 0xBF, 0xCE, 0xB9 }; +static const symbol s_64_56[6] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_57[10] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_58[10] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_59[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_60[10] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_61[10] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_62[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; +static const symbol s_64_63[4] = { 0xCF, 0x89, 0xCE, 0xBD }; +static const symbol s_64_64[8] = { 0xCE, 0xB7, 0xCE, 0xB4, 0xCF, 0x89, 0xCE, 0xBD }; +static const symbol s_64_65[4] = { 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_66[10] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_67[16] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_68[18] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_69[8] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_70[14] = { 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_71[16] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x83, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_72[14] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_73[16] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_74[12] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_75[14] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_76[10] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_77[12] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_78[8] = { 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_79[10] = { 0xCE, 0xB9, 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_80[8] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_81[8] = { 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_82[12] = { 0xCE, 0xB7, 0xCE, 0xB8, 0xCE, 0xB7, 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xBD }; +static const symbol s_64_83[2] = { 0xCE, 0xBF }; +static const struct among a_64[84] = { +{ 4, s_64_0, 0, 1, 0}, +{ 6, s_64_1, -1, 1, 0}, +{ 4, s_64_2, 0, 1, 0}, +{ 4, s_64_3, 0, 1, 0}, +{ 8, s_64_4, -1, 1, 0}, +{ 8, s_64_5, -2, 1, 0}, +{ 4, s_64_6, 0, 1, 0}, +{ 6, s_64_7, 0, 1, 0}, +{ 10, s_64_8, -1, 1, 0}, +{ 4, s_64_9, 0, 1, 0}, +{ 2, s_64_10, 0, 1, 0}, +{ 4, s_64_11, -1, 1, 0}, +{ 2, s_64_12, 0, 1, 0}, +{ 6, s_64_13, -1, 1, 0}, +{ 4, s_64_14, -2, 1, 0}, +{ 6, s_64_15, -3, 1, 0}, +{ 2, s_64_16, 0, 1, 0}, +{ 10, s_64_17, -1, 1, 0}, +{ 12, s_64_18, -2, 1, 0}, +{ 14, s_64_19, -1, 1, 0}, +{ 12, s_64_20, -4, 1, 0}, +{ 14, s_64_21, -1, 1, 0}, +{ 2, s_64_22, 0, 1, 0}, +{ 14, s_64_23, -1, 1, 0}, +{ 12, s_64_24, -2, 1, 0}, +{ 14, s_64_25, -1, 1, 0}, +{ 14, s_64_26, -4, 1, 0}, +{ 16, s_64_27, -1, 1, 0}, +{ 14, s_64_28, -6, 1, 0}, +{ 12, s_64_29, -7, 1, 0}, +{ 10, s_64_30, -8, 1, 0}, +{ 10, s_64_31, -9, 1, 0}, +{ 10, s_64_32, -10, 1, 0}, +{ 14, s_64_33, -1, 1, 0}, +{ 8, s_64_34, -12, 1, 0}, +{ 12, s_64_35, -1, 1, 0}, +{ 2, s_64_36, 0, 1, 0}, +{ 2, s_64_37, 0, 1, 0}, +{ 8, s_64_38, -1, 1, 0}, +{ 8, s_64_39, -2, 1, 0}, +{ 10, s_64_40, -1, 1, 0}, +{ 8, s_64_41, -4, 1, 0}, +{ 8, s_64_42, -5, 1, 0}, +{ 10, s_64_43, -1, 1, 0}, +{ 12, s_64_44, -7, 1, 0}, +{ 14, s_64_45, -1, 1, 0}, +{ 10, s_64_46, -9, 1, 0}, +{ 10, s_64_47, -10, 1, 0}, +{ 8, s_64_48, -11, 1, 0}, +{ 10, s_64_49, -12, 1, 0}, +{ 8, s_64_50, -13, 1, 0}, +{ 4, s_64_51, -14, 1, 0}, +{ 8, s_64_52, -1, 1, 0}, +{ 6, s_64_53, -2, 1, 0}, +{ 8, s_64_54, -3, 1, 0}, +{ 4, s_64_55, -18, 1, 0}, +{ 6, s_64_56, 0, 1, 0}, +{ 10, s_64_57, -1, 1, 0}, +{ 10, s_64_58, -2, 1, 0}, +{ 12, s_64_59, -1, 1, 0}, +{ 10, s_64_60, -4, 1, 0}, +{ 10, s_64_61, -5, 1, 0}, +{ 12, s_64_62, -1, 1, 0}, +{ 4, s_64_63, 0, 1, 0}, +{ 8, s_64_64, -1, 1, 0}, +{ 4, s_64_65, 0, 1, 0}, +{ 10, s_64_66, -1, 1, 0}, +{ 16, s_64_67, -1, 1, 0}, +{ 18, s_64_68, -1, 1, 0}, +{ 8, s_64_69, -4, 1, 0}, +{ 14, s_64_70, -5, 1, 0}, +{ 16, s_64_71, -1, 1, 0}, +{ 14, s_64_72, -7, 1, 0}, +{ 16, s_64_73, -1, 1, 0}, +{ 12, s_64_74, -9, 1, 0}, +{ 14, s_64_75, -1, 1, 0}, +{ 10, s_64_76, -11, 1, 0}, +{ 12, s_64_77, -1, 1, 0}, +{ 8, s_64_78, -13, 1, 0}, +{ 10, s_64_79, -1, 1, 0}, +{ 8, s_64_80, -15, 1, 0}, +{ 8, s_64_81, -16, 1, 0}, +{ 12, s_64_82, -1, 1, 0}, +{ 2, s_64_83, 0, 1, 0} +}; + +static const symbol s_65_0[10] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_65_1[8] = { 0xCF, 0x85, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_65_2[8] = { 0xCF, 0x89, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_65_3[8] = { 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; +static const symbol s_65_4[10] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; +static const symbol s_65_5[8] = { 0xCF, 0x85, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; +static const symbol s_65_6[8] = { 0xCF, 0x89, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; +static const symbol s_65_7[8] = { 0xCE, 0xBF, 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84 }; +static const struct among a_65[8] = { +{ 10, s_65_0, 0, 1, 0}, +{ 8, s_65_1, 0, 1, 0}, +{ 8, s_65_2, 0, 1, 0}, +{ 8, s_65_3, 0, 1, 0}, +{ 10, s_65_4, 0, 1, 0}, +{ 8, s_65_5, 0, 1, 0}, +{ 8, s_65_6, 0, 1, 0}, +{ 8, s_65_7, 0, 1, 0} }; static const unsigned char g_v[] = { 81, 65, 16, 1 }; static const unsigned char g_v2[] = { 81, 65, 0, 1 }; -static const symbol s_0[] = { 0xCE, 0xB1 }; -static const symbol s_1[] = { 0xCE, 0xB2 }; -static const symbol s_2[] = { 0xCE, 0xB3 }; -static const symbol s_3[] = { 0xCE, 0xB4 }; -static const symbol s_4[] = { 0xCE, 0xB5 }; -static const symbol s_5[] = { 0xCE, 0xB6 }; -static const symbol s_6[] = { 0xCE, 0xB7 }; -static const symbol s_7[] = { 0xCE, 0xB8 }; -static const symbol s_8[] = { 0xCE, 0xB9 }; -static const symbol s_9[] = { 0xCE, 0xBA }; -static const symbol s_10[] = { 0xCE, 0xBB }; -static const symbol s_11[] = { 0xCE, 0xBC }; -static const symbol s_12[] = { 0xCE, 0xBD }; -static const symbol s_13[] = { 0xCE, 0xBE }; -static const symbol s_14[] = { 0xCE, 0xBF }; -static const symbol s_15[] = { 0xCF, 0x80 }; -static const symbol s_16[] = { 0xCF, 0x81 }; -static const symbol s_17[] = { 0xCF, 0x83 }; -static const symbol s_18[] = { 0xCF, 0x84 }; -static const symbol s_19[] = { 0xCF, 0x85 }; -static const symbol s_20[] = { 0xCF, 0x86 }; -static const symbol s_21[] = { 0xCF, 0x87 }; -static const symbol s_22[] = { 0xCF, 0x88 }; -static const symbol s_23[] = { 0xCF, 0x89 }; -static const symbol s_24[] = { 0xCF, 0x86, 0xCE, 0xB1 }; -static const symbol s_25[] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB1 }; -static const symbol s_26[] = { 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBF }; -static const symbol s_27[] = { 0xCF, 0x83, 0xCE, 0xBF }; -static const symbol s_28[] = { 0xCF, 0x84, 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF }; -static const symbol s_29[] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xB5 }; -static const symbol s_30[] = { 0xCF, 0x80, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_31[] = { 0xCF, 0x84, 0xCE, 0xB5, 0xCF, 0x81 }; -static const symbol s_32[] = { 0xCF, 0x86, 0xCF, 0x89 }; -static const symbol s_33[] = { 0xCE, 0xBA, 0xCE, 0xB1, 0xCE, 0xB8, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_34[] = { 0xCE, 0xB3, 0xCE, 0xB5, 0xCE, 0xB3, 0xCE, 0xBF, 0xCE, 0xBD }; -static const symbol s_35[] = { 0xCE, 0xB9 }; -static const symbol s_36[] = { 0xCE, 0xB9, 0xCE, 0xB6 }; -static const symbol s_37[] = { 0xCF, 0x89, 0xCE, 0xBD }; -static const symbol s_38[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xB1 }; -static const symbol s_39[] = { 0xCE, 0xB9, 0xCF, 0x83 }; -static const symbol s_40[] = { 0xCE, 0xB9 }; -static const symbol s_41[] = { 0xCE, 0xB9, 0xCF, 0x83 }; -static const symbol s_42[] = { 0xCE, 0xB9 }; -static const symbol s_43[] = { 0xCE, 0xB9 }; -static const symbol s_44[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_45[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBC }; -static const symbol s_46[] = { 0xCE, 0xB9 }; -static const symbol s_47[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xBD, 0xCF, 0x89, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_48[] = { 0xCE, 0xB1, 0xCF, 0x84, 0xCE, 0xBF, 0xCE, 0xBC }; -static const symbol s_49[] = { 0xCE, 0xB3, 0xCE, 0xBD, 0xCF, 0x89, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_50[] = { 0xCE, 0xB5, 0xCE, 0xB8, 0xCE, 0xBD }; -static const symbol s_51[] = { 0xCE, 0xB5, 0xCE, 0xBA, 0xCE, 0xBB, 0xCE, 0xB5, 0xCE, 0xBA, 0xCF, 0x84 }; -static const symbol s_52[] = { 0xCF, 0x83, 0xCE, 0xBA, 0xCE, 0xB5, 0xCF, 0x80, 0xCF, 0x84 }; -static const symbol s_53[] = { 0xCF, 0x84, 0xCE, 0xBF, 0xCF, 0x80 }; -static const symbol s_54[] = { 0xCE, 0xB1, 0xCE, 0xBB, 0xCE, 0xB5, 0xCE, 0xBE, 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB4, 0xCF, 0x81 }; -static const symbol s_55[] = { 0xCE, 0xB2, 0xCF, 0x85, 0xCE, 0xB6, 0xCE, 0xB1, 0xCE, 0xBD, 0xCF, 0x84 }; -static const symbol s_56[] = { 0xCE, 0xB8, 0xCE, 0xB5, 0xCE, 0xB1, 0xCF, 0x84, 0xCF, 0x81 }; -static const symbol s_57[] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCE, 0xB1, 0xCE, 0xBA }; -static const symbol s_58[] = { 0xCE, 0xB1, 0xCE, 0xBA }; -static const symbol s_59[] = { 0xCE, 0xB9, 0xCF, 0x84, 0xCF, 0x83 }; -static const symbol s_60[] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCF, 0x81 }; -static const symbol s_61[] = { 0xCE, 0xB9, 0xCF, 0x84, 0xCF, 0x83 }; -static const symbol s_62[] = { 0xCE, 0xB9, 0xCE, 0xB4 }; -static const symbol s_63[] = { 0xCE, 0xB9, 0xCE, 0xB4 }; -static const symbol s_64[] = { 0xCE, 0xB9, 0xCF, 0x83, 0xCE, 0xBA }; -static const symbol s_65[] = { 0xCE, 0xB1, 0xCE, 0xB4 }; -static const symbol s_66[] = { 0xCE, 0xB5, 0xCE, 0xB4 }; -static const symbol s_67[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xB4 }; -static const symbol s_68[] = { 0xCE, 0xB5 }; -static const symbol s_69[] = { 0xCE, 0xB9 }; -static const symbol s_70[] = { 0xCE, 0xB9, 0xCE, 0xBA }; -static const symbol s_71[] = { 0xCE, 0xB9, 0xCE, 0xBA }; -static const symbol s_72[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; -static const symbol s_73[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBC }; -static const symbol s_74[] = { 0xCE, 0xB1, 0xCE, 0xBC, 0xCE, 0xB5 }; -static const symbol s_75[] = { 0xCE, 0xB1, 0xCE, 0xBC }; -static const symbol s_76[] = { 0xCE, 0xB1, 0xCE, 0xB3, 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_77[] = { 0xCE, 0xB1, 0xCE, 0xBD, 0xCE, 0xB5 }; -static const symbol s_78[] = { 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_79[] = { 0xCE, 0xB1, 0xCE, 0xBD }; -static const symbol s_80[] = { 0xCE, 0xB5, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_81[] = { 0xCE, 0xB5, 0xCF, 0x84 }; -static const symbol s_82[] = { 0xCE, 0xB5, 0xCF, 0x84 }; -static const symbol s_83[] = { 0xCE, 0xB5, 0xCF, 0x84 }; -static const symbol s_84[] = { 0xCE, 0xB1, 0xCF, 0x81, 0xCF, 0x87 }; -static const symbol s_85[] = { 0xCE, 0xBF, 0xCE, 0xBD, 0xCF, 0x84 }; -static const symbol s_86[] = { 0xCE, 0xBA, 0xCF, 0x81, 0xCE, 0xB5 }; -static const symbol s_87[] = { 0xCF, 0x89, 0xCE, 0xBD, 0xCF, 0x84 }; -static const symbol s_88[] = { 0xCE, 0xBF, 0xCE, 0xBD }; -static const symbol s_89[] = { 0xCE, 0xBF, 0xCE, 0xBC, 0xCE, 0xB1, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_90[] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_91[] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_92[] = { 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84, 0xCE, 0xB5 }; -static const symbol s_93[] = { 0xCE, 0xB9, 0xCE, 0xB5, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_94[] = { 0xCE, 0xB7, 0xCE, 0xBA }; -static const symbol s_95[] = { 0xCE, 0xB7, 0xCE, 0xBA }; -static const symbol s_96[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_97[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCF, 0x83 }; -static const symbol s_98[] = { 0xCE, 0xBA, 0xCE, 0xBF, 0xCE, 0xBB, 0xCE, 0xBB }; -static const symbol s_99[] = { 0xCE, 0xB1, 0xCE, 0xB3 }; -static const symbol s_100[] = { 0xCE, 0xB1, 0xCE, 0xB3 }; -static const symbol s_101[] = { 0xCE, 0xB1, 0xCE, 0xB3 }; -static const symbol s_102[] = { 0xCE, 0xB7, 0xCF, 0x83 }; -static const symbol s_103[] = { 0xCE, 0xB7, 0xCF, 0x83, 0xCF, 0x84 }; -static const symbol s_104[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBD }; -static const symbol s_105[] = { 0xCE, 0xBF, 0xCF, 0x85, 0xCE, 0xBC }; -static const symbol s_106[] = { 0xCE, 0xBC, 0xCE, 0xB1 }; - static int r_has_min_length(struct SN_env * z) { return len_utf8(z->p) >= 3; } static int r_tolower(struct SN_env * z) { int among_var; - while(1) { - int m1 = z->l - z->c; (void)m1; + while (1) { + int v_1 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_0, 46); + among_var = find_among_b(z, a_0, 46, 0); z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_1); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_3); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 2, s_4); + { + int ret = slice_from_s(z, 2, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 2, s_6); + { + int ret = slice_from_s(z, 2, s_6); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 2, s_8); + { + int ret = slice_from_s(z, 2, s_8); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 2, s_10); + { + int ret = slice_from_s(z, 2, s_10); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 2, s_11); + { + int ret = slice_from_s(z, 2, s_11); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 2, s_12); + { + int ret = slice_from_s(z, 2, s_12); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 2, s_13); + { + int ret = slice_from_s(z, 2, s_13); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 2, s_14); + { + int ret = slice_from_s(z, 2, s_14); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 2, s_15); + { + int ret = slice_from_s(z, 2, s_15); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 2, s_16); + { + int ret = slice_from_s(z, 2, s_16); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 2, s_17); + { + int ret = slice_from_s(z, 2, s_17); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 2, s_18); + { + int ret = slice_from_s(z, 2, s_18); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 2, s_19); + { + int ret = slice_from_s(z, 2, s_19); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 2, s_20); + { + int ret = slice_from_s(z, 2, s_20); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 2, s_21); + { + int ret = slice_from_s(z, 2, s_21); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 2, s_22); + { + int ret = slice_from_s(z, 2, s_22); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 2, s_23); + { + int ret = slice_from_s(z, 2, s_23); if (ret < 0) return ret; } break; case 25: - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -2463,7 +2345,7 @@ static int r_tolower(struct SN_env * z) { } continue; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; break; } return 1; @@ -2472,92 +2354,106 @@ static int r_tolower(struct SN_env * z) { static int r_step_1(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_1, 40); + among_var = find_among_b(z, a_1, 40, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_24); + { + int ret = slice_from_s(z, 4, s_24); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 6, s_25); + { + int ret = slice_from_s(z, 6, s_25); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 6, s_26); + { + int ret = slice_from_s(z, 6, s_26); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_27); + { + int ret = slice_from_s(z, 4, s_27); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 8, s_28); + { + int ret = slice_from_s(z, 8, s_28); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_29); + { + int ret = slice_from_s(z, 6, s_29); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 6, s_30); + { + int ret = slice_from_s(z, 6, s_30); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 6, s_31); + { + int ret = slice_from_s(z, 6, s_31); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 4, s_32); + { + int ret = slice_from_s(z, 4, s_32); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 12, s_33); + { + int ret = slice_from_s(z, 12, s_33); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 10, s_34); + { + int ret = slice_from_s(z, 10, s_34); if (ret < 0) return ret; } break; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; return 1; } static int r_step_s1(struct SN_env * z) { int among_var; z->ket = z->c; - if (!find_among_b(z, a_3, 14)) return 0; + if (!find_among_b(z, a_3, 14, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - among_var = find_among_b(z, a_2, 31); + among_var = find_among_b(z, a_2, 31, 0); if (!among_var) return 0; if (z->c > z->lb) return 0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_35); + { + int ret = slice_from_s(z, 2, s_35); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_36); + { + int ret = slice_from_s(z, 4, s_36); if (ret < 0) return ret; } break; @@ -2567,17 +2463,19 @@ static int r_step_s1(struct SN_env * z) { static int r_step_s2(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_5, 7)) return 0; + if (!find_among_b(z, a_5, 7, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_4, 8)) return 0; + if (!find_among_b(z, a_4, 8, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_37); + { + int ret = slice_from_s(z, 4, s_37); if (ret < 0) return ret; } return 1; @@ -2585,39 +2483,43 @@ static int r_step_s2(struct SN_env * z) { static int r_step_s3(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + do { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 6, s_38))) goto lab1; + if (!(eq_s_b(z, 6, s_38))) goto lab0; z->bra = z->c; - if (z->c > z->lb) goto lab1; - { int ret = slice_from_s(z, 4, s_39); + if (z->c > z->lb) goto lab0; + { + int ret = slice_from_s(z, 4, s_39); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; - } -lab0: - if (!find_among_b(z, a_7, 7)) return 0; + } while (0); + if (!find_among_b(z, a_7, 7, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - among_var = find_among_b(z, a_6, 32); + among_var = find_among_b(z, a_6, 32, 0); if (!among_var) return 0; if (z->c > z->lb) return 0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_40); + { + int ret = slice_from_s(z, 2, s_40); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_41); + { + int ret = slice_from_s(z, 4, s_41); if (ret < 0) return ret; } break; @@ -2627,18 +2529,20 @@ static int r_step_s3(struct SN_env * z) { static int r_step_s4(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_9, 7)) return 0; + if (!find_among_b(z, a_9, 7, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; if (z->c - 3 <= z->lb || z->p[z->c - 1] >> 5 != 5 || !((-2145255424 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_8, 19)) return 0; + if (!find_among_b(z, a_8, 19, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 2, s_42); + { + int ret = slice_from_s(z, 2, s_42); if (ret < 0) return ret; } return 1; @@ -2647,25 +2551,28 @@ static int r_step_s4(struct SN_env * z) { static int r_step_s5(struct SN_env * z) { int among_var; z->ket = z->c; - if (!find_among_b(z, a_11, 11)) return 0; + if (!find_among_b(z, a_11, 11, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - among_var = find_among_b(z, a_10, 40); + among_var = find_among_b(z, a_10, 40, 0); if (!among_var) return 0; if (z->c > z->lb) return 0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_43); + { + int ret = slice_from_s(z, 2, s_43); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 6, s_44); + { + int ret = slice_from_s(z, 6, s_44); if (ret < 0) return ret; } break; @@ -2676,111 +2583,126 @@ static int r_step_s5(struct SN_env * z) { static int r_step_s6(struct SN_env * z) { int among_var; z->ket = z->c; - if (!find_among_b(z, a_14, 6)) return 0; + if (!find_among_b(z, a_14, 6, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (z->c - 3 <= z->lb || z->p[z->c - 1] != 181) goto lab1; - among_var = find_among_b(z, a_12, 7); - if (!among_var) goto lab1; - if (z->c > z->lb) goto lab1; + if (z->c - 3 <= z->lb || z->p[z->c - 1] != 181) goto lab0; + among_var = find_among_b(z, a_12, 7, 0); + if (!among_var) goto lab0; + if (z->c > z->lb) goto lab0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 6, s_45); + { + int ret = slice_from_s(z, 6, s_45); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_46); + { + int ret = slice_from_s(z, 2, s_46); if (ret < 0) return ret; } break; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; if (z->c - 9 <= z->lb || (z->p[z->c - 1] != 186 && z->p[z->c - 1] != 189)) return 0; - among_var = find_among_b(z, a_13, 10); + among_var = find_among_b(z, a_13, 10, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 12, s_47); + { + int ret = slice_from_s(z, 12, s_47); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 8, s_48); + { + int ret = slice_from_s(z, 8, s_48); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 10, s_49); + { + int ret = slice_from_s(z, 10, s_49); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 6, s_50); + { + int ret = slice_from_s(z, 6, s_50); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 12, s_51); + { + int ret = slice_from_s(z, 12, s_51); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 10, s_52); + { + int ret = slice_from_s(z, 10, s_52); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 6, s_53); + { + int ret = slice_from_s(z, 6, s_53); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 16, s_54); + { + int ret = slice_from_s(z, 16, s_54); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 12, s_55); + { + int ret = slice_from_s(z, 12, s_55); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 10, s_56); + { + int ret = slice_from_s(z, 10, s_56); if (ret < 0) return ret; } break; } - } -lab0: + } while (0); return 1; } static int r_step_s7(struct SN_env * z) { z->ket = z->c; if (z->c - 9 <= z->lb || (z->p[z->c - 1] != 177 && z->p[z->c - 1] != 185)) return 0; - if (!find_among_b(z, a_16, 4)) return 0; + if (!find_among_b(z, a_16, 4, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 131 && z->p[z->c - 1] != 135)) return 0; - if (!find_among_b(z, a_15, 2)) return 0; + if (!find_among_b(z, a_15, 2, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 8, s_57); + { + int ret = slice_from_s(z, 8, s_57); if (ret < 0) return ret; } return 1; @@ -2789,89 +2711,98 @@ static int r_step_s7(struct SN_env * z) { static int r_step_s8(struct SN_env * z) { int among_var; z->ket = z->c; - if (!find_among_b(z, a_18, 8)) return 0; + if (!find_among_b(z, a_18, 8, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - among_var = find_among_b(z, a_17, 46); - if (!among_var) goto lab1; - if (z->c > z->lb) goto lab1; + among_var = find_among_b(z, a_17, 46, 0); + if (!among_var) goto lab0; + if (z->c > z->lb) goto lab0; switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_58); + { + int ret = slice_from_s(z, 4, s_58); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 6, s_59); + { + int ret = slice_from_s(z, 6, s_59); if (ret < 0) return ret; } break; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; z->bra = z->c; if (!(eq_s_b(z, 6, s_60))) return 0; - { int ret = slice_from_s(z, 6, s_61); + { + int ret = slice_from_s(z, 6, s_61); if (ret < 0) return ret; } - } -lab0: + } while (0); return 1; } static int r_step_s9(struct SN_env * z) { z->ket = z->c; if (z->c - 7 <= z->lb || z->p[z->c - 1] >> 5 != 5 || !((-1610481664 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_21, 3)) return 0; + if (!find_among_b(z, a_21, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_19, 4)) goto lab1; - if (z->c > z->lb) goto lab1; - { int ret = slice_from_s(z, 4, s_62); + if (!find_among_b(z, a_19, 4, 0)) goto lab0; + if (z->c > z->lb) goto lab0; + { + int ret = slice_from_s(z, 4, s_62); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 181 && z->p[z->c - 1] != 189)) return 0; - if (!find_among_b(z, a_20, 2)) return 0; - { int ret = slice_from_s(z, 4, s_63); + if (!find_among_b(z, a_20, 2, 0)) return 0; + { + int ret = slice_from_s(z, 4, s_63); if (ret < 0) return ret; } - } -lab0: + } while (0); return 1; } static int r_step_s10(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_23, 4)) return 0; + if (!find_among_b(z, a_23, 4, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_22, 7)) return 0; + if (!find_among_b(z, a_22, 7, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 6, s_64); + { + int ret = slice_from_s(z, 6, s_64); if (ret < 0) return ret; } return 1; @@ -2880,22 +2811,23 @@ static int r_step_s10(struct SN_env * z) { static int r_step_2a(struct SN_env * z) { z->ket = z->c; if (z->c - 7 <= z->lb || (z->p[z->c - 1] != 131 && z->p[z->c - 1] != 189)) return 0; - if (!find_among_b(z, a_24, 2)) return 0; + if (!find_among_b(z, a_24, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; - if (!find_among_b(z, a_25, 10)) goto lab0; + { + int v_1 = z->l - z->c; + if (!find_among_b(z, a_25, 10, 0)) goto lab0; return 0; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int ret; - { int saved_c = z->c; - ret = insert_s(z, z->c, z->c, 4, s_65); - z->c = saved_c; - } + { + int saved_c = z->c; + int ret = insert_s(z, z->c, z->c, 4, s_65); + z->c = saved_c; if (ret < 0) return ret; } return 1; @@ -2904,16 +2836,18 @@ static int r_step_2a(struct SN_env * z) { static int r_step_2b(struct SN_env * z) { z->ket = z->c; if (z->c - 7 <= z->lb || (z->p[z->c - 1] != 131 && z->p[z->c - 1] != 189)) return 0; - if (!find_among_b(z, a_26, 2)) return 0; + if (!find_among_b(z, a_26, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; z->bra = z->c; if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 128 && z->p[z->c - 1] != 187)) return 0; - if (!find_among_b(z, a_27, 8)) return 0; - { int ret = slice_from_s(z, 4, s_66); + if (!find_among_b(z, a_27, 8, 0)) return 0; + { + int ret = slice_from_s(z, 4, s_66); if (ret < 0) return ret; } return 1; @@ -2922,15 +2856,17 @@ static int r_step_2b(struct SN_env * z) { static int r_step_2c(struct SN_env * z) { z->ket = z->c; if (z->c - 9 <= z->lb || (z->p[z->c - 1] != 131 && z->p[z->c - 1] != 189)) return 0; - if (!find_among_b(z, a_28, 2)) return 0; + if (!find_among_b(z, a_28, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_29, 15)) return 0; - { int ret = slice_from_s(z, 6, s_67); + if (!find_among_b(z, a_29, 15, 0)) return 0; + { + int ret = slice_from_s(z, 6, s_67); if (ret < 0) return ret; } return 1; @@ -2939,17 +2875,19 @@ static int r_step_2c(struct SN_env * z) { static int r_step_2d(struct SN_env * z) { z->ket = z->c; if (z->c - 5 <= z->lb || (z->p[z->c - 1] != 131 && z->p[z->c - 1] != 189)) return 0; - if (!find_among_b(z, a_30, 2)) return 0; + if (!find_among_b(z, a_30, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_31, 8)) return 0; + if (!find_among_b(z, a_31, 8, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 2, s_68); + { + int ret = slice_from_s(z, 2, s_68); if (ret < 0) return ret; } return 1; @@ -2957,16 +2895,18 @@ static int r_step_2d(struct SN_env * z) { static int r_step_3(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_32, 3)) return 0; + if (!find_among_b(z, a_32, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; if (in_grouping_b_U(z, g_v, 945, 969, 0)) return 0; - { int ret = slice_from_s(z, 2, s_69); + { + int ret = slice_from_s(z, 2, s_69); if (ret < 0) return ret; } return 1; @@ -2974,171 +2914,191 @@ static int r_step_3(struct SN_env * z) { static int r_step_4(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_33, 4)) return 0; + if (!find_among_b(z, a_33, 4, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (in_grouping_b_U(z, g_v, 945, 969, 0)) goto lab1; - { int ret = slice_from_s(z, 4, s_70); + if (in_grouping_b_U(z, g_v, 945, 969, 0)) goto lab0; + { + int ret = slice_from_s(z, 4, s_70); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; - } -lab0: + } while (0); z->bra = z->c; - if (!find_among_b(z, a_34, 36)) return 0; + if (!find_among_b(z, a_34, 36, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_71); + { + int ret = slice_from_s(z, 4, s_71); if (ret < 0) return ret; } return 1; } static int r_step_5a(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; if (!(eq_s_b(z, 10, s_72))) goto lab0; z->bra = z->c; if (z->c > z->lb) goto lab0; - { int ret = slice_from_s(z, 8, s_73); + { + int ret = slice_from_s(z, 8, s_73); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; if (z->c - 9 <= z->lb || z->p[z->c - 1] != 181) goto lab1; - if (!find_among_b(z, a_35, 5)) goto lab1; + if (!find_among_b(z, a_35, 5, 0)) goto lab1; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; } z->ket = z->c; if (!(eq_s_b(z, 6, s_74))) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_36, 12)) return 0; + if (!find_among_b(z, a_36, 12, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_75); + { + int ret = slice_from_s(z, 4, s_75); if (ret < 0) return ret; } return 1; } static int r_step_5b(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; if (z->c - 9 <= z->lb || z->p[z->c - 1] != 181) goto lab0; - if (!find_among_b(z, a_38, 11)) goto lab0; + if (!find_among_b(z, a_38, 11, 0)) goto lab0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 129 && z->p[z->c - 1] != 131)) goto lab0; - if (!find_among_b(z, a_37, 2)) goto lab0; + if (!find_among_b(z, a_37, 2, 0)) goto lab0; if (z->c > z->lb) goto lab0; - { int ret = slice_from_s(z, 8, s_76); + { + int ret = slice_from_s(z, 8, s_76); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } z->ket = z->c; if (!(eq_s_b(z, 6, s_77))) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m2 = z->l - z->c; (void)m2; + ((SN_local *)z)->b_test1 = 0; + do { + int v_2 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (in_grouping_b_U(z, g_v2, 945, 969, 0)) goto lab2; - { int ret = slice_from_s(z, 4, s_78); + if (in_grouping_b_U(z, g_v2, 945, 969, 0)) goto lab1; + { + int ret = slice_from_s(z, 4, s_78); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; + break; + lab1: + z->c = z->l - v_2; z->ket = z->c; - } -lab1: + } while (0); z->bra = z->c; - if (!find_among_b(z, a_39, 95)) return 0; + if (!find_among_b(z, a_39, 95, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_79); + { + int ret = slice_from_s(z, 4, s_79); if (ret < 0) return ret; } return 1; } static int r_step_5c(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c - 9 <= z->lb || z->p[z->c - 1] != 181) goto lab0; - if (!find_among_b(z, a_40, 1)) goto lab0; + if (!(eq_s_b(z, 10, s_80))) goto lab0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } z->ket = z->c; - if (!(eq_s_b(z, 6, s_80))) return 0; + if (!(eq_s_b(z, 6, s_81))) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m2 = z->l - z->c; (void)m2; + ((SN_local *)z)->b_test1 = 0; + do { + int v_2 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (in_grouping_b_U(z, g_v2, 945, 969, 0)) goto lab2; - { int ret = slice_from_s(z, 4, s_81); + if (in_grouping_b_U(z, g_v2, 945, 969, 0)) goto lab1; + { + int ret = slice_from_s(z, 4, s_82); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; + break; + lab1: + z->c = z->l - v_2; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_41, 31)) goto lab3; - { int ret = slice_from_s(z, 4, s_82); + if (!find_among_b(z, a_40, 31, 0)) goto lab2; + { + int ret = slice_from_s(z, 4, s_83); if (ret < 0) return ret; } - goto lab1; - lab3: - z->c = z->l - m2; + break; + lab2: + z->c = z->l - v_2; z->ket = z->c; - } -lab1: + } while (0); z->bra = z->c; - if (!find_among_b(z, a_42, 25)) return 0; + if (!find_among_b(z, a_41, 25, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_83); + { + int ret = slice_from_s(z, 4, s_84); if (ret < 0) return ret; } return 1; @@ -3147,225 +3107,248 @@ static int r_step_5c(struct SN_env * z) { static int r_step_5d(struct SN_env * z) { z->ket = z->c; if (z->c - 9 <= z->lb || z->p[z->c - 1] != 131) return 0; - if (!find_among_b(z, a_43, 2)) return 0; + if (!find_among_b(z, a_42, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (!(eq_s_b(z, 6, s_84))) goto lab1; - if (z->c > z->lb) goto lab1; - { int ret = slice_from_s(z, 6, s_85); + if (!(eq_s_b(z, 6, s_85))) goto lab0; + if (z->c > z->lb) goto lab0; + { + int ret = slice_from_s(z, 6, s_86); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; z->bra = z->c; - if (!(eq_s_b(z, 6, s_86))) return 0; - { int ret = slice_from_s(z, 6, s_87); + if (!(eq_s_b(z, 6, s_87))) return 0; + { + int ret = slice_from_s(z, 6, s_88); if (ret < 0) return ret; } - } -lab0: + } while (0); return 1; } static int r_step_5e(struct SN_env * z) { z->ket = z->c; if (z->c - 11 <= z->lb || z->p[z->c - 1] != 181) return 0; - if (!find_among_b(z, a_44, 2)) return 0; + if (!find_among_b(z, a_43, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!(eq_s_b(z, 4, s_88))) return 0; + if (!(eq_s_b(z, 4, s_89))) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 10, s_89); + { + int ret = slice_from_s(z, 10, s_90); if (ret < 0) return ret; } return 1; } static int r_step_5f(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 10, s_90))) goto lab0; + if (!(eq_s_b(z, 10, s_91))) goto lab0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 128 && z->p[z->c - 1] != 134)) goto lab0; - if (!find_among_b(z, a_45, 6)) goto lab0; + if (!find_among_b(z, a_44, 6, 0)) goto lab0; if (z->c > z->lb) goto lab0; - { int ret = slice_from_s(z, 8, s_91); + { + int ret = slice_from_s(z, 8, s_92); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } z->ket = z->c; - if (!(eq_s_b(z, 8, s_92))) return 0; + if (!(eq_s_b(z, 8, s_93))) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_46, 9)) return 0; + if (!find_among_b(z, a_45, 9, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 8, s_93); + { + int ret = slice_from_s(z, 8, s_94); if (ret < 0) return ret; } return 1; } static int r_step_5g(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!find_among_b(z, a_47, 3)) goto lab0; + if (!find_among_b(z, a_46, 3, 0)) goto lab0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } z->ket = z->c; - if (!find_among_b(z, a_50, 3)) return 0; + if (!find_among_b(z, a_49, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m2 = z->l - z->c; (void)m2; + ((SN_local *)z)->b_test1 = 0; + do { + int v_2 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_48, 6)) goto lab2; - { int ret = slice_from_s(z, 4, s_94); + if (!find_among_b(z, a_47, 6, 0)) goto lab1; + { + int ret = slice_from_s(z, 4, s_95); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; + break; + lab1: + z->c = z->l - v_2; z->ket = z->c; z->bra = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] != 184) return 0; - if (!find_among_b(z, a_49, 5)) return 0; + if (!find_among_b(z, a_48, 5, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_95); + { + int ret = slice_from_s(z, 4, s_96); if (ret < 0) return ret; } - } -lab1: + } while (0); return 1; } static int r_step_5h(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_53, 3)) return 0; + if (!find_among_b(z, a_52, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_51, 12)) goto lab1; - { int ret = slice_from_s(z, 6, s_96); + if (!find_among_b(z, a_50, 12, 0)) goto lab0; + { + int ret = slice_from_s(z, 6, s_97); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_52, 25)) return 0; + if (!find_among_b(z, a_51, 25, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 6, s_97); + { + int ret = slice_from_s(z, 6, s_98); if (ret < 0) return ret; } - } -lab0: + } while (0); return 1; } static int r_step_5i(struct SN_env * z) { int among_var; z->ket = z->c; - if (!find_among_b(z, a_56, 3)) return 0; + if (!find_among_b(z, a_55, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; - { int m1 = z->l - z->c; (void)m1; + ((SN_local *)z)->b_test1 = 0; + do { + int v_1 = z->l - z->c; z->ket = z->c; z->bra = z->c; - if (!(eq_s_b(z, 8, s_98))) goto lab1; - { int ret = slice_from_s(z, 4, s_99); + if (!(eq_s_b(z, 8, s_99))) goto lab0; + { + int ret = slice_from_s(z, 4, s_100); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; - { int m2 = z->l - z->c; (void)m2; + break; + lab0: + z->c = z->l - v_1; + do { + int v_2 = z->l - z->c; z->ket = z->c; z->bra = z->c; - among_var = find_among_b(z, a_54, 12); - if (!among_var) goto lab3; + among_var = find_among_b(z, a_53, 12, 0); + if (!among_var) goto lab1; switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_100); + { + int ret = slice_from_s(z, 4, s_101); if (ret < 0) return ret; } break; } - goto lab2; - lab3: - z->c = z->l - m2; + break; + lab1: + z->c = z->l - v_2; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_55, 44)) return 0; + if (!find_among_b(z, a_54, 44, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_101); + { + int ret = slice_from_s(z, 4, s_102); if (ret < 0) return ret; } - } - lab2: - ; - } -lab0: + } while (0); + } while (0); return 1; } static int r_step_5j(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_57, 3)) return 0; + if (!find_among_b(z, a_56, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] != 189) return 0; - if (!find_among_b(z, a_58, 6)) return 0; + if (!find_among_b(z, a_57, 6, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 4, s_102); + { + int ret = slice_from_s(z, 4, s_103); if (ret < 0) return ret; } return 1; @@ -3373,18 +3356,19 @@ static int r_step_5j(struct SN_env * z) { static int r_step_5k(struct SN_env * z) { z->ket = z->c; - if (z->c - 7 <= z->lb || z->p[z->c - 1] != 181) return 0; - if (!find_among_b(z, a_59, 1)) return 0; + if (!(eq_s_b(z, 8, s_104))) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_60, 10)) return 0; + if (!find_among_b(z, a_58, 10, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 6, s_103); + { + int ret = slice_from_s(z, 6, s_105); if (ret < 0) return ret; } return 1; @@ -3393,17 +3377,19 @@ static int r_step_5k(struct SN_env * z) { static int r_step_5l(struct SN_env * z) { z->ket = z->c; if (z->c - 7 <= z->lb || z->p[z->c - 1] != 181) return 0; - if (!find_among_b(z, a_61, 3)) return 0; + if (!find_among_b(z, a_59, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_62, 6)) return 0; + if (!find_among_b(z, a_60, 6, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 6, s_104); + { + int ret = slice_from_s(z, 6, s_106); if (ret < 0) return ret; } return 1; @@ -3412,38 +3398,43 @@ static int r_step_5l(struct SN_env * z) { static int r_step_5m(struct SN_env * z) { z->ket = z->c; if (z->c - 7 <= z->lb || z->p[z->c - 1] != 181) return 0; - if (!find_among_b(z, a_63, 3)) return 0; + if (!find_among_b(z, a_61, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 0; + ((SN_local *)z)->b_test1 = 0; z->ket = z->c; z->bra = z->c; - if (!find_among_b(z, a_64, 7)) return 0; + if (!find_among_b(z, a_62, 7, 0)) return 0; if (z->c > z->lb) return 0; - { int ret = slice_from_s(z, 6, s_105); + { + int ret = slice_from_s(z, 6, s_107); if (ret < 0) return ret; } return 1; } static int r_step_6(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!find_among_b(z, a_65, 3)) goto lab0; + if (!find_among_b(z, a_63, 3, 0)) goto lab0; z->bra = z->c; - { int ret = slice_from_s(z, 4, s_106); + { + int ret = slice_from_s(z, 4, s_108); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - if (!(z->I[0])) return 0; + if (!((SN_local *)z)->b_test1) return 0; z->ket = z->c; - if (!find_among_b(z, a_66, 84)) return 0; + if (!find_among_b(z, a_64, 84, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -3452,9 +3443,10 @@ static int r_step_6(struct SN_env * z) { static int r_step_7(struct SN_env * z) { z->ket = z->c; if (z->c - 7 <= z->lb || (z->p[z->c - 1] != 129 && z->p[z->c - 1] != 132)) return 0; - if (!find_among_b(z, a_67, 8)) return 0; + if (!find_among_b(z, a_65, 8, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -3462,214 +3454,288 @@ static int r_step_7(struct SN_env * z) { extern int greek_UTF_8_stem(struct SN_env * z) { z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_tolower(z); + { + int v_1 = z->l - z->c; + { + int ret = r_tolower(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int ret = r_has_min_length(z); + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } - z->I[0] = 1; - { int m2 = z->l - z->c; (void)m2; - { int ret = r_step_1(z); + ((SN_local *)z)->b_test1 = 1; + { + int v_2 = z->l - z->c; + { + int ret = r_step_1(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_step_s1(z); + { + int v_3 = z->l - z->c; + { + int ret = r_step_s1(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_step_s2(z); + { + int v_4 = z->l - z->c; + { + int ret = r_step_s2(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_step_s3(z); + { + int v_5 = z->l - z->c; + { + int ret = r_step_s3(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_step_s4(z); + { + int v_6 = z->l - z->c; + { + int ret = r_step_s4(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_step_s5(z); + { + int v_7 = z->l - z->c; + { + int ret = r_step_s5(z); if (ret < 0) return ret; } - z->c = z->l - m7; + z->c = z->l - v_7; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_step_s6(z); + { + int v_8 = z->l - z->c; + { + int ret = r_step_s6(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_step_s7(z); + { + int v_9 = z->l - z->c; + { + int ret = r_step_s7(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_step_s8(z); + { + int v_10 = z->l - z->c; + { + int ret = r_step_s8(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } - { int m11 = z->l - z->c; (void)m11; - { int ret = r_step_s9(z); + { + int v_11 = z->l - z->c; + { + int ret = r_step_s9(z); if (ret < 0) return ret; } - z->c = z->l - m11; + z->c = z->l - v_11; } - { int m12 = z->l - z->c; (void)m12; - { int ret = r_step_s10(z); + { + int v_12 = z->l - z->c; + { + int ret = r_step_s10(z); if (ret < 0) return ret; } - z->c = z->l - m12; + z->c = z->l - v_12; } - { int m13 = z->l - z->c; (void)m13; - { int ret = r_step_2a(z); + { + int v_13 = z->l - z->c; + { + int ret = r_step_2a(z); if (ret < 0) return ret; } - z->c = z->l - m13; + z->c = z->l - v_13; } - { int m14 = z->l - z->c; (void)m14; - { int ret = r_step_2b(z); + { + int v_14 = z->l - z->c; + { + int ret = r_step_2b(z); if (ret < 0) return ret; } - z->c = z->l - m14; + z->c = z->l - v_14; } - { int m15 = z->l - z->c; (void)m15; - { int ret = r_step_2c(z); + { + int v_15 = z->l - z->c; + { + int ret = r_step_2c(z); if (ret < 0) return ret; } - z->c = z->l - m15; + z->c = z->l - v_15; } - { int m16 = z->l - z->c; (void)m16; - { int ret = r_step_2d(z); + { + int v_16 = z->l - z->c; + { + int ret = r_step_2d(z); if (ret < 0) return ret; } - z->c = z->l - m16; + z->c = z->l - v_16; } - { int m17 = z->l - z->c; (void)m17; - { int ret = r_step_3(z); + { + int v_17 = z->l - z->c; + { + int ret = r_step_3(z); if (ret < 0) return ret; } - z->c = z->l - m17; + z->c = z->l - v_17; } - { int m18 = z->l - z->c; (void)m18; - { int ret = r_step_4(z); + { + int v_18 = z->l - z->c; + { + int ret = r_step_4(z); if (ret < 0) return ret; } - z->c = z->l - m18; + z->c = z->l - v_18; } - { int m19 = z->l - z->c; (void)m19; - { int ret = r_step_5a(z); + { + int v_19 = z->l - z->c; + { + int ret = r_step_5a(z); if (ret < 0) return ret; } - z->c = z->l - m19; + z->c = z->l - v_19; } - { int m20 = z->l - z->c; (void)m20; - { int ret = r_step_5b(z); + { + int v_20 = z->l - z->c; + { + int ret = r_step_5b(z); if (ret < 0) return ret; } - z->c = z->l - m20; + z->c = z->l - v_20; } - { int m21 = z->l - z->c; (void)m21; - { int ret = r_step_5c(z); + { + int v_21 = z->l - z->c; + { + int ret = r_step_5c(z); if (ret < 0) return ret; } - z->c = z->l - m21; + z->c = z->l - v_21; } - { int m22 = z->l - z->c; (void)m22; - { int ret = r_step_5d(z); + { + int v_22 = z->l - z->c; + { + int ret = r_step_5d(z); if (ret < 0) return ret; } - z->c = z->l - m22; + z->c = z->l - v_22; } - { int m23 = z->l - z->c; (void)m23; - { int ret = r_step_5e(z); + { + int v_23 = z->l - z->c; + { + int ret = r_step_5e(z); if (ret < 0) return ret; } - z->c = z->l - m23; + z->c = z->l - v_23; } - { int m24 = z->l - z->c; (void)m24; - { int ret = r_step_5f(z); + { + int v_24 = z->l - z->c; + { + int ret = r_step_5f(z); if (ret < 0) return ret; } - z->c = z->l - m24; + z->c = z->l - v_24; } - { int m25 = z->l - z->c; (void)m25; - { int ret = r_step_5g(z); + { + int v_25 = z->l - z->c; + { + int ret = r_step_5g(z); if (ret < 0) return ret; } - z->c = z->l - m25; + z->c = z->l - v_25; } - { int m26 = z->l - z->c; (void)m26; - { int ret = r_step_5h(z); + { + int v_26 = z->l - z->c; + { + int ret = r_step_5h(z); if (ret < 0) return ret; } - z->c = z->l - m26; + z->c = z->l - v_26; } - { int m27 = z->l - z->c; (void)m27; - { int ret = r_step_5j(z); + { + int v_27 = z->l - z->c; + { + int ret = r_step_5j(z); if (ret < 0) return ret; } - z->c = z->l - m27; + z->c = z->l - v_27; } - { int m28 = z->l - z->c; (void)m28; - { int ret = r_step_5i(z); + { + int v_28 = z->l - z->c; + { + int ret = r_step_5i(z); if (ret < 0) return ret; } - z->c = z->l - m28; + z->c = z->l - v_28; } - { int m29 = z->l - z->c; (void)m29; - { int ret = r_step_5k(z); + { + int v_29 = z->l - z->c; + { + int ret = r_step_5k(z); if (ret < 0) return ret; } - z->c = z->l - m29; + z->c = z->l - v_29; } - { int m30 = z->l - z->c; (void)m30; - { int ret = r_step_5l(z); + { + int v_30 = z->l - z->c; + { + int ret = r_step_5l(z); if (ret < 0) return ret; } - z->c = z->l - m30; + z->c = z->l - v_30; } - { int m31 = z->l - z->c; (void)m31; - { int ret = r_step_5m(z); + { + int v_31 = z->l - z->c; + { + int ret = r_step_5m(z); if (ret < 0) return ret; } - z->c = z->l - m31; + z->c = z->l - v_31; } - { int m32 = z->l - z->c; (void)m32; - { int ret = r_step_6(z); + { + int v_32 = z->l - z->c; + { + int ret = r_step_6(z); if (ret < 0) return ret; } - z->c = z->l - m32; + z->c = z->l - v_32; } - { int m33 = z->l - z->c; (void)m33; - { int ret = r_step_7(z); + { + int v_33 = z->l - z->c; + { + int ret = r_step_7(z); if (ret < 0) return ret; } - z->c = z->l - m33; + z->c = z->l - v_33; } z->c = z->lb; return 1; } -extern struct SN_env * greek_UTF_8_create_env(void) { return SN_create_env(0, 1); } +extern struct SN_env * greek_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_test1 = 0; + } + return z; +} -extern void greek_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void greek_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_hindi.c b/src/backend/snowball/libstemmer/stem_UTF_8_hindi.c index a2f2ec7f20fee..ef7feb5736991 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_hindi.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_hindi.c @@ -1,8 +1,11 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from hindi.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_hindi.h" + +#include + +#include "snowball_runtime.h" -static int r_CONSONANT(struct SN_env * z); #ifdef __cplusplus extern "C" { #endif @@ -10,18 +13,10 @@ extern int hindi_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif -#ifdef __cplusplus -extern "C" { -#endif - -extern struct SN_env * hindi_UTF_8_create_env(void); -extern void hindi_UTF_8_close_env(struct SN_env * z); +static int r_CONSONANT(struct SN_env * z); -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 0xE0, 0xA5, 0x80 }; static const symbol s_0_1[12] = { 0xE0, 0xA5, 0x82, 0xE0, 0xA4, 0x82, 0xE0, 0xA4, 0x97, 0xE0, 0xA5, 0x80 }; static const symbol s_0_2[12] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x82, 0xE0, 0xA4, 0x97, 0xE0, 0xA5, 0x80 }; @@ -154,169 +149,170 @@ static const symbol s_0_128[9] = { 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xA8, 0xE0, 0xA static const symbol s_0_129[9] = { 0xE0, 0xA4, 0x86, 0xE0, 0xA4, 0xAF, 0xE0, 0xA4, 0xBE }; static const symbol s_0_130[9] = { 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xAF, 0xE0, 0xA4, 0xBE }; static const symbol s_0_131[3] = { 0xE0, 0xA4, 0xBF }; - -static const struct among a_0[132] = -{ -{ 3, s_0_0, -1, -1, 0}, -{ 12, s_0_1, 0, -1, 0}, -{ 12, s_0_2, 0, -1, 0}, -{ 12, s_0_3, 0, -1, 0}, -{ 15, s_0_4, 3, -1, 0}, -{ 15, s_0_5, 3, -1, 0}, -{ 12, s_0_6, 0, -1, 0}, -{ 15, s_0_7, 6, -1, 0}, -{ 15, s_0_8, 6, -1, 0}, -{ 9, s_0_9, 0, -1, 0}, -{ 9, s_0_10, 0, -1, 0}, -{ 9, s_0_11, 0, -1, 0}, -{ 12, s_0_12, 11, -1, 0}, -{ 12, s_0_13, 11, -1, 0}, -{ 9, s_0_14, 0, -1, 0}, -{ 12, s_0_15, 14, -1, 0}, -{ 12, s_0_16, 14, -1, 0}, -{ 6, s_0_17, 0, -1, r_CONSONANT}, -{ 9, s_0_18, 17, -1, 0}, -{ 9, s_0_19, 17, -1, 0}, -{ 9, s_0_20, 17, -1, 0}, -{ 6, s_0_21, 0, -1, r_CONSONANT}, -{ 9, s_0_22, 21, -1, 0}, -{ 6, s_0_23, -1, -1, 0}, -{ 6, s_0_24, -1, -1, 0}, -{ 12, s_0_25, 24, -1, 0}, -{ 15, s_0_26, 25, -1, 0}, -{ 15, s_0_27, 25, -1, 0}, -{ 12, s_0_28, 24, -1, 0}, -{ 3, s_0_29, -1, -1, 0}, -{ 6, s_0_30, -1, -1, 0}, -{ 9, s_0_31, 30, -1, r_CONSONANT}, -{ 12, s_0_32, 31, -1, 0}, -{ 12, s_0_33, 31, -1, 0}, -{ 12, s_0_34, 31, -1, 0}, -{ 6, s_0_35, -1, -1, 0}, -{ 9, s_0_36, 35, -1, 0}, -{ 9, s_0_37, 35, -1, 0}, -{ 6, s_0_38, -1, -1, 0}, -{ 6, s_0_39, -1, -1, 0}, -{ 9, s_0_40, 39, -1, 0}, -{ 9, s_0_41, 39, -1, 0}, -{ 6, s_0_42, -1, -1, 0}, -{ 12, s_0_43, 42, -1, 0}, -{ 15, s_0_44, 43, -1, 0}, -{ 15, s_0_45, 43, -1, 0}, -{ 12, s_0_46, 42, -1, 0}, -{ 6, s_0_47, -1, -1, 0}, -{ 9, s_0_48, 47, -1, 0}, -{ 9, s_0_49, 47, -1, 0}, -{ 9, s_0_50, 47, -1, 0}, -{ 9, s_0_51, 47, -1, 0}, -{ 12, s_0_52, 51, -1, r_CONSONANT}, -{ 15, s_0_53, 52, -1, 0}, -{ 12, s_0_54, 51, -1, r_CONSONANT}, -{ 15, s_0_55, 54, -1, 0}, -{ 6, s_0_56, -1, -1, 0}, -{ 9, s_0_57, 56, -1, 0}, -{ 9, s_0_58, 56, -1, 0}, -{ 9, s_0_59, 56, -1, 0}, -{ 9, s_0_60, 56, -1, 0}, -{ 12, s_0_61, 60, -1, r_CONSONANT}, -{ 15, s_0_62, 61, -1, 0}, -{ 12, s_0_63, 60, -1, r_CONSONANT}, -{ 15, s_0_64, 63, -1, 0}, -{ 6, s_0_65, -1, -1, 0}, -{ 12, s_0_66, 65, -1, 0}, -{ 15, s_0_67, 66, -1, 0}, -{ 15, s_0_68, 66, -1, 0}, -{ 12, s_0_69, 65, -1, 0}, -{ 3, s_0_70, -1, -1, 0}, -{ 3, s_0_71, -1, -1, 0}, -{ 3, s_0_72, -1, -1, 0}, -{ 3, s_0_73, -1, -1, 0}, -{ 3, s_0_74, -1, -1, 0}, -{ 12, s_0_75, 74, -1, 0}, -{ 12, s_0_76, 74, -1, 0}, -{ 15, s_0_77, 76, -1, 0}, -{ 15, s_0_78, 76, -1, 0}, -{ 9, s_0_79, 74, -1, 0}, -{ 9, s_0_80, 74, -1, 0}, -{ 12, s_0_81, 80, -1, 0}, -{ 12, s_0_82, 80, -1, 0}, -{ 6, s_0_83, 74, -1, r_CONSONANT}, -{ 9, s_0_84, 83, -1, 0}, -{ 9, s_0_85, 83, -1, 0}, -{ 9, s_0_86, 83, -1, 0}, -{ 6, s_0_87, 74, -1, r_CONSONANT}, -{ 9, s_0_88, 87, -1, 0}, -{ 9, s_0_89, 87, -1, 0}, -{ 9, s_0_90, 87, -1, 0}, -{ 3, s_0_91, -1, -1, 0}, -{ 6, s_0_92, 91, -1, 0}, -{ 6, s_0_93, 91, -1, 0}, -{ 3, s_0_94, -1, -1, 0}, -{ 3, s_0_95, -1, -1, 0}, -{ 3, s_0_96, -1, -1, 0}, -{ 3, s_0_97, -1, -1, 0}, -{ 3, s_0_98, -1, -1, 0}, -{ 6, s_0_99, 98, -1, 0}, -{ 6, s_0_100, 98, -1, 0}, -{ 9, s_0_101, 100, -1, 0}, -{ 9, s_0_102, 100, -1, 0}, -{ 6, s_0_103, 98, -1, 0}, -{ 6, s_0_104, 98, -1, 0}, -{ 3, s_0_105, -1, -1, 0}, -{ 6, s_0_106, 105, -1, 0}, -{ 6, s_0_107, 105, -1, 0}, -{ 6, s_0_108, -1, -1, r_CONSONANT}, -{ 9, s_0_109, 108, -1, 0}, -{ 9, s_0_110, 108, -1, 0}, -{ 9, s_0_111, 108, -1, 0}, -{ 3, s_0_112, -1, -1, 0}, -{ 12, s_0_113, 112, -1, 0}, -{ 12, s_0_114, 112, -1, 0}, -{ 15, s_0_115, 114, -1, 0}, -{ 15, s_0_116, 114, -1, 0}, -{ 9, s_0_117, 112, -1, 0}, -{ 9, s_0_118, 112, -1, 0}, -{ 12, s_0_119, 118, -1, 0}, -{ 12, s_0_120, 118, -1, 0}, -{ 6, s_0_121, 112, -1, r_CONSONANT}, -{ 9, s_0_122, 121, -1, 0}, -{ 9, s_0_123, 121, -1, 0}, -{ 9, s_0_124, 121, -1, 0}, -{ 6, s_0_125, 112, -1, r_CONSONANT}, -{ 9, s_0_126, 125, -1, 0}, -{ 9, s_0_127, 125, -1, 0}, -{ 9, s_0_128, 125, -1, 0}, -{ 9, s_0_129, 112, -1, 0}, -{ 9, s_0_130, 112, -1, 0}, -{ 3, s_0_131, -1, -1, 0} +static const struct among a_0[132] = { +{ 3, s_0_0, 0, -1, 0}, +{ 12, s_0_1, -1, -1, 0}, +{ 12, s_0_2, -2, -1, 0}, +{ 12, s_0_3, -3, -1, 0}, +{ 15, s_0_4, -1, -1, 0}, +{ 15, s_0_5, -2, -1, 0}, +{ 12, s_0_6, -6, -1, 0}, +{ 15, s_0_7, -1, -1, 0}, +{ 15, s_0_8, -2, -1, 0}, +{ 9, s_0_9, -9, -1, 0}, +{ 9, s_0_10, -10, -1, 0}, +{ 9, s_0_11, -11, -1, 0}, +{ 12, s_0_12, -1, -1, 0}, +{ 12, s_0_13, -2, -1, 0}, +{ 9, s_0_14, -14, -1, 0}, +{ 12, s_0_15, -1, -1, 0}, +{ 12, s_0_16, -2, -1, 0}, +{ 6, s_0_17, -17, -1, 1}, +{ 9, s_0_18, -1, -1, 0}, +{ 9, s_0_19, -2, -1, 0}, +{ 9, s_0_20, -3, -1, 0}, +{ 6, s_0_21, -21, -1, 1}, +{ 9, s_0_22, -1, -1, 0}, +{ 6, s_0_23, 0, -1, 0}, +{ 6, s_0_24, 0, -1, 0}, +{ 12, s_0_25, -1, -1, 0}, +{ 15, s_0_26, -1, -1, 0}, +{ 15, s_0_27, -2, -1, 0}, +{ 12, s_0_28, -4, -1, 0}, +{ 3, s_0_29, 0, -1, 0}, +{ 6, s_0_30, 0, -1, 0}, +{ 9, s_0_31, -1, -1, 1}, +{ 12, s_0_32, -1, -1, 0}, +{ 12, s_0_33, -2, -1, 0}, +{ 12, s_0_34, -3, -1, 0}, +{ 6, s_0_35, 0, -1, 0}, +{ 9, s_0_36, -1, -1, 0}, +{ 9, s_0_37, -2, -1, 0}, +{ 6, s_0_38, 0, -1, 0}, +{ 6, s_0_39, 0, -1, 0}, +{ 9, s_0_40, -1, -1, 0}, +{ 9, s_0_41, -2, -1, 0}, +{ 6, s_0_42, 0, -1, 0}, +{ 12, s_0_43, -1, -1, 0}, +{ 15, s_0_44, -1, -1, 0}, +{ 15, s_0_45, -2, -1, 0}, +{ 12, s_0_46, -4, -1, 0}, +{ 6, s_0_47, 0, -1, 0}, +{ 9, s_0_48, -1, -1, 0}, +{ 9, s_0_49, -2, -1, 0}, +{ 9, s_0_50, -3, -1, 0}, +{ 9, s_0_51, -4, -1, 0}, +{ 12, s_0_52, -1, -1, 1}, +{ 15, s_0_53, -1, -1, 0}, +{ 12, s_0_54, -3, -1, 1}, +{ 15, s_0_55, -1, -1, 0}, +{ 6, s_0_56, 0, -1, 0}, +{ 9, s_0_57, -1, -1, 0}, +{ 9, s_0_58, -2, -1, 0}, +{ 9, s_0_59, -3, -1, 0}, +{ 9, s_0_60, -4, -1, 0}, +{ 12, s_0_61, -1, -1, 1}, +{ 15, s_0_62, -1, -1, 0}, +{ 12, s_0_63, -3, -1, 1}, +{ 15, s_0_64, -1, -1, 0}, +{ 6, s_0_65, 0, -1, 0}, +{ 12, s_0_66, -1, -1, 0}, +{ 15, s_0_67, -1, -1, 0}, +{ 15, s_0_68, -2, -1, 0}, +{ 12, s_0_69, -4, -1, 0}, +{ 3, s_0_70, 0, -1, 0}, +{ 3, s_0_71, 0, -1, 0}, +{ 3, s_0_72, 0, -1, 0}, +{ 3, s_0_73, 0, -1, 0}, +{ 3, s_0_74, 0, -1, 0}, +{ 12, s_0_75, -1, -1, 0}, +{ 12, s_0_76, -2, -1, 0}, +{ 15, s_0_77, -1, -1, 0}, +{ 15, s_0_78, -2, -1, 0}, +{ 9, s_0_79, -5, -1, 0}, +{ 9, s_0_80, -6, -1, 0}, +{ 12, s_0_81, -1, -1, 0}, +{ 12, s_0_82, -2, -1, 0}, +{ 6, s_0_83, -9, -1, 1}, +{ 9, s_0_84, -1, -1, 0}, +{ 9, s_0_85, -2, -1, 0}, +{ 9, s_0_86, -3, -1, 0}, +{ 6, s_0_87, -13, -1, 1}, +{ 9, s_0_88, -1, -1, 0}, +{ 9, s_0_89, -2, -1, 0}, +{ 9, s_0_90, -3, -1, 0}, +{ 3, s_0_91, 0, -1, 0}, +{ 6, s_0_92, -1, -1, 0}, +{ 6, s_0_93, -2, -1, 0}, +{ 3, s_0_94, 0, -1, 0}, +{ 3, s_0_95, 0, -1, 0}, +{ 3, s_0_96, 0, -1, 0}, +{ 3, s_0_97, 0, -1, 0}, +{ 3, s_0_98, 0, -1, 0}, +{ 6, s_0_99, -1, -1, 0}, +{ 6, s_0_100, -2, -1, 0}, +{ 9, s_0_101, -1, -1, 0}, +{ 9, s_0_102, -2, -1, 0}, +{ 6, s_0_103, -5, -1, 0}, +{ 6, s_0_104, -6, -1, 0}, +{ 3, s_0_105, 0, -1, 0}, +{ 6, s_0_106, -1, -1, 0}, +{ 6, s_0_107, -2, -1, 0}, +{ 6, s_0_108, 0, -1, 1}, +{ 9, s_0_109, -1, -1, 0}, +{ 9, s_0_110, -2, -1, 0}, +{ 9, s_0_111, -3, -1, 0}, +{ 3, s_0_112, 0, -1, 0}, +{ 12, s_0_113, -1, -1, 0}, +{ 12, s_0_114, -2, -1, 0}, +{ 15, s_0_115, -1, -1, 0}, +{ 15, s_0_116, -2, -1, 0}, +{ 9, s_0_117, -5, -1, 0}, +{ 9, s_0_118, -6, -1, 0}, +{ 12, s_0_119, -1, -1, 0}, +{ 12, s_0_120, -2, -1, 0}, +{ 6, s_0_121, -9, -1, 1}, +{ 9, s_0_122, -1, -1, 0}, +{ 9, s_0_123, -2, -1, 0}, +{ 9, s_0_124, -3, -1, 0}, +{ 6, s_0_125, -13, -1, 1}, +{ 9, s_0_126, -1, -1, 0}, +{ 9, s_0_127, -2, -1, 0}, +{ 9, s_0_128, -3, -1, 0}, +{ 9, s_0_129, -17, -1, 0}, +{ 9, s_0_130, -18, -1, 0}, +{ 3, s_0_131, 0, -1, 0} }; static const unsigned char g_consonant[] = { 255, 255, 255, 255, 159, 0, 0, 0, 248, 7 }; - static int r_CONSONANT(struct SN_env * z) { - if (in_grouping_b_U(z, g_consonant, 2325, 2399, 0)) return 0; - return 1; + return !in_grouping_b_U(z, g_consonant, 2325, 2399, 0); } extern int hindi_UTF_8_stem(struct SN_env * z) { - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) return 0; z->c = ret; } z->lb = z->c; z->c = z->l; - z->ket = z->c; - if (!find_among_b(z, a_0, 132)) return 0; + if (!find_among_b(z, a_0, 132, r_CONSONANT)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->c = z->lb; return 1; } -extern struct SN_env * hindi_UTF_8_create_env(void) { return SN_create_env(0, 0); } +extern struct SN_env * hindi_UTF_8_create_env(void) { + return SN_new_env(sizeof(struct SN_env)); +} -extern void hindi_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void hindi_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_hungarian.c b/src/backend/snowball/libstemmer/stem_UTF_8_hungarian.c index ff193a4d336d6..6a6b3542b712a 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_hungarian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_hungarian.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from hungarian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_hungarian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +20,7 @@ extern int hungarian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_double(struct SN_env * z); static int r_undouble(struct SN_env * z); static int r_factive(struct SN_env * z); @@ -23,516 +35,459 @@ static int r_case(struct SN_env * z); static int r_v_ending(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * hungarian_UTF_8_create_env(void); -extern void hungarian_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'e' }; +static const symbol s_3[] = { 'a' }; +static const symbol s_4[] = { 'a' }; +static const symbol s_5[] = { 'e' }; +static const symbol s_6[] = { 'a' }; +static const symbol s_7[] = { 'e' }; +static const symbol s_8[] = { 'e' }; +static const symbol s_9[] = { 'a' }; +static const symbol s_10[] = { 'a' }; +static const symbol s_11[] = { 'e' }; +static const symbol s_12[] = { 'a' }; +static const symbol s_13[] = { 'e' }; -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[2] = { 'c', 's' }; -static const symbol s_0_1[3] = { 'd', 'z', 's' }; -static const symbol s_0_2[2] = { 'g', 'y' }; -static const symbol s_0_3[2] = { 'l', 'y' }; -static const symbol s_0_4[2] = { 'n', 'y' }; -static const symbol s_0_5[2] = { 's', 'z' }; -static const symbol s_0_6[2] = { 't', 'y' }; -static const symbol s_0_7[2] = { 'z', 's' }; - -static const struct among a_0[8] = -{ -{ 2, s_0_0, -1, -1, 0}, -{ 3, s_0_1, -1, -1, 0}, -{ 2, s_0_2, -1, -1, 0}, -{ 2, s_0_3, -1, -1, 0}, -{ 2, s_0_4, -1, -1, 0}, -{ 2, s_0_5, -1, -1, 0}, -{ 2, s_0_6, -1, -1, 0}, -{ 2, s_0_7, -1, -1, 0} +static const symbol s_0_0[2] = { 0xC3, 0xA1 }; +static const symbol s_0_1[2] = { 0xC3, 0xA9 }; +static const struct among a_0[2] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 2, 0} }; -static const symbol s_1_0[2] = { 0xC3, 0xA1 }; -static const symbol s_1_1[2] = { 0xC3, 0xA9 }; - -static const struct among a_1[2] = -{ -{ 2, s_1_0, -1, 1, 0}, -{ 2, s_1_1, -1, 2, 0} +static const symbol s_1_0[2] = { 'b', 'b' }; +static const symbol s_1_1[2] = { 'c', 'c' }; +static const symbol s_1_2[2] = { 'd', 'd' }; +static const symbol s_1_3[2] = { 'f', 'f' }; +static const symbol s_1_4[2] = { 'g', 'g' }; +static const symbol s_1_5[2] = { 'j', 'j' }; +static const symbol s_1_6[2] = { 'k', 'k' }; +static const symbol s_1_7[2] = { 'l', 'l' }; +static const symbol s_1_8[2] = { 'm', 'm' }; +static const symbol s_1_9[2] = { 'n', 'n' }; +static const symbol s_1_10[2] = { 'p', 'p' }; +static const symbol s_1_11[2] = { 'r', 'r' }; +static const symbol s_1_12[3] = { 'c', 'c', 's' }; +static const symbol s_1_13[2] = { 's', 's' }; +static const symbol s_1_14[3] = { 'z', 'z', 's' }; +static const symbol s_1_15[2] = { 't', 't' }; +static const symbol s_1_16[2] = { 'v', 'v' }; +static const symbol s_1_17[3] = { 'g', 'g', 'y' }; +static const symbol s_1_18[3] = { 'l', 'l', 'y' }; +static const symbol s_1_19[3] = { 'n', 'n', 'y' }; +static const symbol s_1_20[3] = { 't', 't', 'y' }; +static const symbol s_1_21[3] = { 's', 's', 'z' }; +static const symbol s_1_22[2] = { 'z', 'z' }; +static const struct among a_1[23] = { +{ 2, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 2, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 2, s_1_4, 0, -1, 0}, +{ 2, s_1_5, 0, -1, 0}, +{ 2, s_1_6, 0, -1, 0}, +{ 2, s_1_7, 0, -1, 0}, +{ 2, s_1_8, 0, -1, 0}, +{ 2, s_1_9, 0, -1, 0}, +{ 2, s_1_10, 0, -1, 0}, +{ 2, s_1_11, 0, -1, 0}, +{ 3, s_1_12, 0, -1, 0}, +{ 2, s_1_13, 0, -1, 0}, +{ 3, s_1_14, 0, -1, 0}, +{ 2, s_1_15, 0, -1, 0}, +{ 2, s_1_16, 0, -1, 0}, +{ 3, s_1_17, 0, -1, 0}, +{ 3, s_1_18, 0, -1, 0}, +{ 3, s_1_19, 0, -1, 0}, +{ 3, s_1_20, 0, -1, 0}, +{ 3, s_1_21, 0, -1, 0}, +{ 2, s_1_22, 0, -1, 0} }; -static const symbol s_2_0[2] = { 'b', 'b' }; -static const symbol s_2_1[2] = { 'c', 'c' }; -static const symbol s_2_2[2] = { 'd', 'd' }; -static const symbol s_2_3[2] = { 'f', 'f' }; -static const symbol s_2_4[2] = { 'g', 'g' }; -static const symbol s_2_5[2] = { 'j', 'j' }; -static const symbol s_2_6[2] = { 'k', 'k' }; -static const symbol s_2_7[2] = { 'l', 'l' }; -static const symbol s_2_8[2] = { 'm', 'm' }; -static const symbol s_2_9[2] = { 'n', 'n' }; -static const symbol s_2_10[2] = { 'p', 'p' }; -static const symbol s_2_11[2] = { 'r', 'r' }; -static const symbol s_2_12[3] = { 'c', 'c', 's' }; -static const symbol s_2_13[2] = { 's', 's' }; -static const symbol s_2_14[3] = { 'z', 'z', 's' }; -static const symbol s_2_15[2] = { 't', 't' }; -static const symbol s_2_16[2] = { 'v', 'v' }; -static const symbol s_2_17[3] = { 'g', 'g', 'y' }; -static const symbol s_2_18[3] = { 'l', 'l', 'y' }; -static const symbol s_2_19[3] = { 'n', 'n', 'y' }; -static const symbol s_2_20[3] = { 't', 't', 'y' }; -static const symbol s_2_21[3] = { 's', 's', 'z' }; -static const symbol s_2_22[2] = { 'z', 'z' }; - -static const struct among a_2[23] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0}, -{ 2, s_2_3, -1, -1, 0}, -{ 2, s_2_4, -1, -1, 0}, -{ 2, s_2_5, -1, -1, 0}, -{ 2, s_2_6, -1, -1, 0}, -{ 2, s_2_7, -1, -1, 0}, -{ 2, s_2_8, -1, -1, 0}, -{ 2, s_2_9, -1, -1, 0}, -{ 2, s_2_10, -1, -1, 0}, -{ 2, s_2_11, -1, -1, 0}, -{ 3, s_2_12, -1, -1, 0}, -{ 2, s_2_13, -1, -1, 0}, -{ 3, s_2_14, -1, -1, 0}, -{ 2, s_2_15, -1, -1, 0}, -{ 2, s_2_16, -1, -1, 0}, -{ 3, s_2_17, -1, -1, 0}, -{ 3, s_2_18, -1, -1, 0}, -{ 3, s_2_19, -1, -1, 0}, -{ 3, s_2_20, -1, -1, 0}, -{ 3, s_2_21, -1, -1, 0}, -{ 2, s_2_22, -1, -1, 0} +static const symbol s_2_0[2] = { 'a', 'l' }; +static const symbol s_2_1[2] = { 'e', 'l' }; +static const struct among a_2[2] = { +{ 2, s_2_0, 0, 1, 0}, +{ 2, s_2_1, 0, 1, 0} }; -static const symbol s_3_0[2] = { 'a', 'l' }; -static const symbol s_3_1[2] = { 'e', 'l' }; - -static const struct among a_3[2] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 1, 0} +static const symbol s_3_0[2] = { 'b', 'a' }; +static const symbol s_3_1[2] = { 'r', 'a' }; +static const symbol s_3_2[2] = { 'b', 'e' }; +static const symbol s_3_3[2] = { 'r', 'e' }; +static const symbol s_3_4[2] = { 'i', 'g' }; +static const symbol s_3_5[3] = { 'n', 'a', 'k' }; +static const symbol s_3_6[3] = { 'n', 'e', 'k' }; +static const symbol s_3_7[3] = { 'v', 'a', 'l' }; +static const symbol s_3_8[3] = { 'v', 'e', 'l' }; +static const symbol s_3_9[2] = { 'u', 'l' }; +static const symbol s_3_10[4] = { 'b', 0xC5, 0x91, 'l' }; +static const symbol s_3_11[4] = { 'r', 0xC5, 0x91, 'l' }; +static const symbol s_3_12[4] = { 't', 0xC5, 0x91, 'l' }; +static const symbol s_3_13[4] = { 'n', 0xC3, 0xA1, 'l' }; +static const symbol s_3_14[4] = { 'n', 0xC3, 0xA9, 'l' }; +static const symbol s_3_15[4] = { 'b', 0xC3, 0xB3, 'l' }; +static const symbol s_3_16[4] = { 'r', 0xC3, 0xB3, 'l' }; +static const symbol s_3_17[4] = { 't', 0xC3, 0xB3, 'l' }; +static const symbol s_3_18[3] = { 0xC3, 0xBC, 'l' }; +static const symbol s_3_19[1] = { 'n' }; +static const symbol s_3_20[2] = { 'a', 'n' }; +static const symbol s_3_21[3] = { 'b', 'a', 'n' }; +static const symbol s_3_22[2] = { 'e', 'n' }; +static const symbol s_3_23[3] = { 'b', 'e', 'n' }; +static const symbol s_3_24[7] = { 'k', 0xC3, 0xA9, 'p', 'p', 'e', 'n' }; +static const symbol s_3_25[2] = { 'o', 'n' }; +static const symbol s_3_26[3] = { 0xC3, 0xB6, 'n' }; +static const symbol s_3_27[5] = { 'k', 0xC3, 0xA9, 'p', 'p' }; +static const symbol s_3_28[3] = { 'k', 'o', 'r' }; +static const symbol s_3_29[1] = { 't' }; +static const symbol s_3_30[2] = { 'a', 't' }; +static const symbol s_3_31[2] = { 'e', 't' }; +static const symbol s_3_32[5] = { 'k', 0xC3, 0xA9, 'n', 't' }; +static const symbol s_3_33[7] = { 'a', 'n', 'k', 0xC3, 0xA9, 'n', 't' }; +static const symbol s_3_34[7] = { 'e', 'n', 'k', 0xC3, 0xA9, 'n', 't' }; +static const symbol s_3_35[7] = { 'o', 'n', 'k', 0xC3, 0xA9, 'n', 't' }; +static const symbol s_3_36[2] = { 'o', 't' }; +static const symbol s_3_37[4] = { 0xC3, 0xA9, 'r', 't' }; +static const symbol s_3_38[3] = { 0xC3, 0xB6, 't' }; +static const symbol s_3_39[3] = { 'h', 'e', 'z' }; +static const symbol s_3_40[3] = { 'h', 'o', 'z' }; +static const symbol s_3_41[4] = { 'h', 0xC3, 0xB6, 'z' }; +static const symbol s_3_42[3] = { 'v', 0xC3, 0xA1 }; +static const symbol s_3_43[3] = { 'v', 0xC3, 0xA9 }; +static const struct among a_3[44] = { +{ 2, s_3_0, 0, -1, 0}, +{ 2, s_3_1, 0, -1, 0}, +{ 2, s_3_2, 0, -1, 0}, +{ 2, s_3_3, 0, -1, 0}, +{ 2, s_3_4, 0, -1, 0}, +{ 3, s_3_5, 0, -1, 0}, +{ 3, s_3_6, 0, -1, 0}, +{ 3, s_3_7, 0, -1, 0}, +{ 3, s_3_8, 0, -1, 0}, +{ 2, s_3_9, 0, -1, 0}, +{ 4, s_3_10, 0, -1, 0}, +{ 4, s_3_11, 0, -1, 0}, +{ 4, s_3_12, 0, -1, 0}, +{ 4, s_3_13, 0, -1, 0}, +{ 4, s_3_14, 0, -1, 0}, +{ 4, s_3_15, 0, -1, 0}, +{ 4, s_3_16, 0, -1, 0}, +{ 4, s_3_17, 0, -1, 0}, +{ 3, s_3_18, 0, -1, 0}, +{ 1, s_3_19, 0, -1, 0}, +{ 2, s_3_20, -1, -1, 0}, +{ 3, s_3_21, -1, -1, 0}, +{ 2, s_3_22, -3, -1, 0}, +{ 3, s_3_23, -1, -1, 0}, +{ 7, s_3_24, -2, -1, 0}, +{ 2, s_3_25, -6, -1, 0}, +{ 3, s_3_26, -7, -1, 0}, +{ 5, s_3_27, 0, -1, 0}, +{ 3, s_3_28, 0, -1, 0}, +{ 1, s_3_29, 0, -1, 0}, +{ 2, s_3_30, -1, -1, 0}, +{ 2, s_3_31, -2, -1, 0}, +{ 5, s_3_32, -3, -1, 0}, +{ 7, s_3_33, -1, -1, 0}, +{ 7, s_3_34, -2, -1, 0}, +{ 7, s_3_35, -3, -1, 0}, +{ 2, s_3_36, -7, -1, 0}, +{ 4, s_3_37, -8, -1, 0}, +{ 3, s_3_38, -9, -1, 0}, +{ 3, s_3_39, 0, -1, 0}, +{ 3, s_3_40, 0, -1, 0}, +{ 4, s_3_41, 0, -1, 0}, +{ 3, s_3_42, 0, -1, 0}, +{ 3, s_3_43, 0, -1, 0} }; -static const symbol s_4_0[2] = { 'b', 'a' }; -static const symbol s_4_1[2] = { 'r', 'a' }; -static const symbol s_4_2[2] = { 'b', 'e' }; -static const symbol s_4_3[2] = { 'r', 'e' }; -static const symbol s_4_4[2] = { 'i', 'g' }; -static const symbol s_4_5[3] = { 'n', 'a', 'k' }; -static const symbol s_4_6[3] = { 'n', 'e', 'k' }; -static const symbol s_4_7[3] = { 'v', 'a', 'l' }; -static const symbol s_4_8[3] = { 'v', 'e', 'l' }; -static const symbol s_4_9[2] = { 'u', 'l' }; -static const symbol s_4_10[4] = { 'b', 0xC5, 0x91, 'l' }; -static const symbol s_4_11[4] = { 'r', 0xC5, 0x91, 'l' }; -static const symbol s_4_12[4] = { 't', 0xC5, 0x91, 'l' }; -static const symbol s_4_13[4] = { 'n', 0xC3, 0xA1, 'l' }; -static const symbol s_4_14[4] = { 'n', 0xC3, 0xA9, 'l' }; -static const symbol s_4_15[4] = { 'b', 0xC3, 0xB3, 'l' }; -static const symbol s_4_16[4] = { 'r', 0xC3, 0xB3, 'l' }; -static const symbol s_4_17[4] = { 't', 0xC3, 0xB3, 'l' }; -static const symbol s_4_18[3] = { 0xC3, 0xBC, 'l' }; -static const symbol s_4_19[1] = { 'n' }; -static const symbol s_4_20[2] = { 'a', 'n' }; -static const symbol s_4_21[3] = { 'b', 'a', 'n' }; -static const symbol s_4_22[2] = { 'e', 'n' }; -static const symbol s_4_23[3] = { 'b', 'e', 'n' }; -static const symbol s_4_24[7] = { 'k', 0xC3, 0xA9, 'p', 'p', 'e', 'n' }; -static const symbol s_4_25[2] = { 'o', 'n' }; -static const symbol s_4_26[3] = { 0xC3, 0xB6, 'n' }; -static const symbol s_4_27[5] = { 'k', 0xC3, 0xA9, 'p', 'p' }; -static const symbol s_4_28[3] = { 'k', 'o', 'r' }; -static const symbol s_4_29[1] = { 't' }; -static const symbol s_4_30[2] = { 'a', 't' }; -static const symbol s_4_31[2] = { 'e', 't' }; -static const symbol s_4_32[5] = { 'k', 0xC3, 0xA9, 'n', 't' }; -static const symbol s_4_33[7] = { 'a', 'n', 'k', 0xC3, 0xA9, 'n', 't' }; -static const symbol s_4_34[7] = { 'e', 'n', 'k', 0xC3, 0xA9, 'n', 't' }; -static const symbol s_4_35[7] = { 'o', 'n', 'k', 0xC3, 0xA9, 'n', 't' }; -static const symbol s_4_36[2] = { 'o', 't' }; -static const symbol s_4_37[4] = { 0xC3, 0xA9, 'r', 't' }; -static const symbol s_4_38[3] = { 0xC3, 0xB6, 't' }; -static const symbol s_4_39[3] = { 'h', 'e', 'z' }; -static const symbol s_4_40[3] = { 'h', 'o', 'z' }; -static const symbol s_4_41[4] = { 'h', 0xC3, 0xB6, 'z' }; -static const symbol s_4_42[3] = { 'v', 0xC3, 0xA1 }; -static const symbol s_4_43[3] = { 'v', 0xC3, 0xA9 }; - -static const struct among a_4[44] = -{ -{ 2, s_4_0, -1, -1, 0}, -{ 2, s_4_1, -1, -1, 0}, -{ 2, s_4_2, -1, -1, 0}, -{ 2, s_4_3, -1, -1, 0}, -{ 2, s_4_4, -1, -1, 0}, -{ 3, s_4_5, -1, -1, 0}, -{ 3, s_4_6, -1, -1, 0}, -{ 3, s_4_7, -1, -1, 0}, -{ 3, s_4_8, -1, -1, 0}, -{ 2, s_4_9, -1, -1, 0}, -{ 4, s_4_10, -1, -1, 0}, -{ 4, s_4_11, -1, -1, 0}, -{ 4, s_4_12, -1, -1, 0}, -{ 4, s_4_13, -1, -1, 0}, -{ 4, s_4_14, -1, -1, 0}, -{ 4, s_4_15, -1, -1, 0}, -{ 4, s_4_16, -1, -1, 0}, -{ 4, s_4_17, -1, -1, 0}, -{ 3, s_4_18, -1, -1, 0}, -{ 1, s_4_19, -1, -1, 0}, -{ 2, s_4_20, 19, -1, 0}, -{ 3, s_4_21, 20, -1, 0}, -{ 2, s_4_22, 19, -1, 0}, -{ 3, s_4_23, 22, -1, 0}, -{ 7, s_4_24, 22, -1, 0}, -{ 2, s_4_25, 19, -1, 0}, -{ 3, s_4_26, 19, -1, 0}, -{ 5, s_4_27, -1, -1, 0}, -{ 3, s_4_28, -1, -1, 0}, -{ 1, s_4_29, -1, -1, 0}, -{ 2, s_4_30, 29, -1, 0}, -{ 2, s_4_31, 29, -1, 0}, -{ 5, s_4_32, 29, -1, 0}, -{ 7, s_4_33, 32, -1, 0}, -{ 7, s_4_34, 32, -1, 0}, -{ 7, s_4_35, 32, -1, 0}, -{ 2, s_4_36, 29, -1, 0}, -{ 4, s_4_37, 29, -1, 0}, -{ 3, s_4_38, 29, -1, 0}, -{ 3, s_4_39, -1, -1, 0}, -{ 3, s_4_40, -1, -1, 0}, -{ 4, s_4_41, -1, -1, 0}, -{ 3, s_4_42, -1, -1, 0}, -{ 3, s_4_43, -1, -1, 0} +static const symbol s_4_0[3] = { 0xC3, 0xA1, 'n' }; +static const symbol s_4_1[3] = { 0xC3, 0xA9, 'n' }; +static const symbol s_4_2[8] = { 0xC3, 0xA1, 'n', 'k', 0xC3, 0xA9, 'n', 't' }; +static const struct among a_4[3] = { +{ 3, s_4_0, 0, 2, 0}, +{ 3, s_4_1, 0, 1, 0}, +{ 8, s_4_2, 0, 2, 0} }; -static const symbol s_5_0[3] = { 0xC3, 0xA1, 'n' }; -static const symbol s_5_1[3] = { 0xC3, 0xA9, 'n' }; -static const symbol s_5_2[8] = { 0xC3, 0xA1, 'n', 'k', 0xC3, 0xA9, 'n', 't' }; - -static const struct among a_5[3] = -{ -{ 3, s_5_0, -1, 2, 0}, -{ 3, s_5_1, -1, 1, 0}, -{ 8, s_5_2, -1, 2, 0} +static const symbol s_5_0[4] = { 's', 't', 'u', 'l' }; +static const symbol s_5_1[5] = { 'a', 's', 't', 'u', 'l' }; +static const symbol s_5_2[6] = { 0xC3, 0xA1, 's', 't', 'u', 'l' }; +static const symbol s_5_3[5] = { 's', 't', 0xC3, 0xBC, 'l' }; +static const symbol s_5_4[6] = { 'e', 's', 't', 0xC3, 0xBC, 'l' }; +static const symbol s_5_5[7] = { 0xC3, 0xA9, 's', 't', 0xC3, 0xBC, 'l' }; +static const struct among a_5[6] = { +{ 4, s_5_0, 0, 1, 0}, +{ 5, s_5_1, -1, 1, 0}, +{ 6, s_5_2, -2, 2, 0}, +{ 5, s_5_3, 0, 1, 0}, +{ 6, s_5_4, -1, 1, 0}, +{ 7, s_5_5, -2, 3, 0} }; -static const symbol s_6_0[4] = { 's', 't', 'u', 'l' }; -static const symbol s_6_1[5] = { 'a', 's', 't', 'u', 'l' }; -static const symbol s_6_2[6] = { 0xC3, 0xA1, 's', 't', 'u', 'l' }; -static const symbol s_6_3[5] = { 's', 't', 0xC3, 0xBC, 'l' }; -static const symbol s_6_4[6] = { 'e', 's', 't', 0xC3, 0xBC, 'l' }; -static const symbol s_6_5[7] = { 0xC3, 0xA9, 's', 't', 0xC3, 0xBC, 'l' }; - -static const struct among a_6[6] = -{ -{ 4, s_6_0, -1, 1, 0}, -{ 5, s_6_1, 0, 1, 0}, -{ 6, s_6_2, 0, 2, 0}, -{ 5, s_6_3, -1, 1, 0}, -{ 6, s_6_4, 3, 1, 0}, -{ 7, s_6_5, 3, 3, 0} +static const symbol s_6_0[2] = { 0xC3, 0xA1 }; +static const symbol s_6_1[2] = { 0xC3, 0xA9 }; +static const struct among a_6[2] = { +{ 2, s_6_0, 0, 1, 0}, +{ 2, s_6_1, 0, 1, 0} }; -static const symbol s_7_0[2] = { 0xC3, 0xA1 }; -static const symbol s_7_1[2] = { 0xC3, 0xA9 }; - -static const struct among a_7[2] = -{ -{ 2, s_7_0, -1, 1, 0}, -{ 2, s_7_1, -1, 1, 0} +static const symbol s_7_0[1] = { 'k' }; +static const symbol s_7_1[2] = { 'a', 'k' }; +static const symbol s_7_2[2] = { 'e', 'k' }; +static const symbol s_7_3[2] = { 'o', 'k' }; +static const symbol s_7_4[3] = { 0xC3, 0xA1, 'k' }; +static const symbol s_7_5[3] = { 0xC3, 0xA9, 'k' }; +static const symbol s_7_6[3] = { 0xC3, 0xB6, 'k' }; +static const struct among a_7[7] = { +{ 1, s_7_0, 0, 3, 0}, +{ 2, s_7_1, -1, 3, 0}, +{ 2, s_7_2, -2, 3, 0}, +{ 2, s_7_3, -3, 3, 0}, +{ 3, s_7_4, -4, 1, 0}, +{ 3, s_7_5, -5, 2, 0}, +{ 3, s_7_6, -6, 3, 0} }; -static const symbol s_8_0[1] = { 'k' }; -static const symbol s_8_1[2] = { 'a', 'k' }; -static const symbol s_8_2[2] = { 'e', 'k' }; -static const symbol s_8_3[2] = { 'o', 'k' }; -static const symbol s_8_4[3] = { 0xC3, 0xA1, 'k' }; -static const symbol s_8_5[3] = { 0xC3, 0xA9, 'k' }; -static const symbol s_8_6[3] = { 0xC3, 0xB6, 'k' }; - -static const struct among a_8[7] = -{ -{ 1, s_8_0, -1, 3, 0}, -{ 2, s_8_1, 0, 3, 0}, -{ 2, s_8_2, 0, 3, 0}, -{ 2, s_8_3, 0, 3, 0}, -{ 3, s_8_4, 0, 1, 0}, -{ 3, s_8_5, 0, 2, 0}, -{ 3, s_8_6, 0, 3, 0} +static const symbol s_8_0[3] = { 0xC3, 0xA9, 'i' }; +static const symbol s_8_1[5] = { 0xC3, 0xA1, 0xC3, 0xA9, 'i' }; +static const symbol s_8_2[5] = { 0xC3, 0xA9, 0xC3, 0xA9, 'i' }; +static const symbol s_8_3[2] = { 0xC3, 0xA9 }; +static const symbol s_8_4[3] = { 'k', 0xC3, 0xA9 }; +static const symbol s_8_5[4] = { 'a', 'k', 0xC3, 0xA9 }; +static const symbol s_8_6[4] = { 'e', 'k', 0xC3, 0xA9 }; +static const symbol s_8_7[4] = { 'o', 'k', 0xC3, 0xA9 }; +static const symbol s_8_8[5] = { 0xC3, 0xA1, 'k', 0xC3, 0xA9 }; +static const symbol s_8_9[5] = { 0xC3, 0xA9, 'k', 0xC3, 0xA9 }; +static const symbol s_8_10[5] = { 0xC3, 0xB6, 'k', 0xC3, 0xA9 }; +static const symbol s_8_11[4] = { 0xC3, 0xA9, 0xC3, 0xA9 }; +static const struct among a_8[12] = { +{ 3, s_8_0, 0, 1, 0}, +{ 5, s_8_1, -1, 3, 0}, +{ 5, s_8_2, -2, 2, 0}, +{ 2, s_8_3, 0, 1, 0}, +{ 3, s_8_4, -1, 1, 0}, +{ 4, s_8_5, -1, 1, 0}, +{ 4, s_8_6, -2, 1, 0}, +{ 4, s_8_7, -3, 1, 0}, +{ 5, s_8_8, -4, 3, 0}, +{ 5, s_8_9, -5, 2, 0}, +{ 5, s_8_10, -6, 1, 0}, +{ 4, s_8_11, -8, 2, 0} }; -static const symbol s_9_0[3] = { 0xC3, 0xA9, 'i' }; -static const symbol s_9_1[5] = { 0xC3, 0xA1, 0xC3, 0xA9, 'i' }; -static const symbol s_9_2[5] = { 0xC3, 0xA9, 0xC3, 0xA9, 'i' }; -static const symbol s_9_3[2] = { 0xC3, 0xA9 }; -static const symbol s_9_4[3] = { 'k', 0xC3, 0xA9 }; -static const symbol s_9_5[4] = { 'a', 'k', 0xC3, 0xA9 }; -static const symbol s_9_6[4] = { 'e', 'k', 0xC3, 0xA9 }; -static const symbol s_9_7[4] = { 'o', 'k', 0xC3, 0xA9 }; -static const symbol s_9_8[5] = { 0xC3, 0xA1, 'k', 0xC3, 0xA9 }; -static const symbol s_9_9[5] = { 0xC3, 0xA9, 'k', 0xC3, 0xA9 }; -static const symbol s_9_10[5] = { 0xC3, 0xB6, 'k', 0xC3, 0xA9 }; -static const symbol s_9_11[4] = { 0xC3, 0xA9, 0xC3, 0xA9 }; - -static const struct among a_9[12] = -{ -{ 3, s_9_0, -1, 1, 0}, -{ 5, s_9_1, 0, 3, 0}, -{ 5, s_9_2, 0, 2, 0}, +static const symbol s_9_0[1] = { 'a' }; +static const symbol s_9_1[2] = { 'j', 'a' }; +static const symbol s_9_2[1] = { 'd' }; +static const symbol s_9_3[2] = { 'a', 'd' }; +static const symbol s_9_4[2] = { 'e', 'd' }; +static const symbol s_9_5[2] = { 'o', 'd' }; +static const symbol s_9_6[3] = { 0xC3, 0xA1, 'd' }; +static const symbol s_9_7[3] = { 0xC3, 0xA9, 'd' }; +static const symbol s_9_8[3] = { 0xC3, 0xB6, 'd' }; +static const symbol s_9_9[1] = { 'e' }; +static const symbol s_9_10[2] = { 'j', 'e' }; +static const symbol s_9_11[2] = { 'n', 'k' }; +static const symbol s_9_12[3] = { 'u', 'n', 'k' }; +static const symbol s_9_13[4] = { 0xC3, 0xA1, 'n', 'k' }; +static const symbol s_9_14[4] = { 0xC3, 0xA9, 'n', 'k' }; +static const symbol s_9_15[4] = { 0xC3, 0xBC, 'n', 'k' }; +static const symbol s_9_16[2] = { 'u', 'k' }; +static const symbol s_9_17[3] = { 'j', 'u', 'k' }; +static const symbol s_9_18[5] = { 0xC3, 0xA1, 'j', 'u', 'k' }; +static const symbol s_9_19[3] = { 0xC3, 0xBC, 'k' }; +static const symbol s_9_20[4] = { 'j', 0xC3, 0xBC, 'k' }; +static const symbol s_9_21[6] = { 0xC3, 0xA9, 'j', 0xC3, 0xBC, 'k' }; +static const symbol s_9_22[1] = { 'm' }; +static const symbol s_9_23[2] = { 'a', 'm' }; +static const symbol s_9_24[2] = { 'e', 'm' }; +static const symbol s_9_25[2] = { 'o', 'm' }; +static const symbol s_9_26[3] = { 0xC3, 0xA1, 'm' }; +static const symbol s_9_27[3] = { 0xC3, 0xA9, 'm' }; +static const symbol s_9_28[1] = { 'o' }; +static const symbol s_9_29[2] = { 0xC3, 0xA1 }; +static const symbol s_9_30[2] = { 0xC3, 0xA9 }; +static const struct among a_9[31] = { +{ 1, s_9_0, 0, 1, 0}, +{ 2, s_9_1, -1, 1, 0}, +{ 1, s_9_2, 0, 1, 0}, { 2, s_9_3, -1, 1, 0}, -{ 3, s_9_4, 3, 1, 0}, -{ 4, s_9_5, 4, 1, 0}, -{ 4, s_9_6, 4, 1, 0}, -{ 4, s_9_7, 4, 1, 0}, -{ 5, s_9_8, 4, 3, 0}, -{ 5, s_9_9, 4, 2, 0}, -{ 5, s_9_10, 4, 1, 0}, -{ 4, s_9_11, 3, 2, 0} +{ 2, s_9_4, -2, 1, 0}, +{ 2, s_9_5, -3, 1, 0}, +{ 3, s_9_6, -4, 2, 0}, +{ 3, s_9_7, -5, 3, 0}, +{ 3, s_9_8, -6, 1, 0}, +{ 1, s_9_9, 0, 1, 0}, +{ 2, s_9_10, -1, 1, 0}, +{ 2, s_9_11, 0, 1, 0}, +{ 3, s_9_12, -1, 1, 0}, +{ 4, s_9_13, -2, 2, 0}, +{ 4, s_9_14, -3, 3, 0}, +{ 4, s_9_15, -4, 1, 0}, +{ 2, s_9_16, 0, 1, 0}, +{ 3, s_9_17, -1, 1, 0}, +{ 5, s_9_18, -1, 2, 0}, +{ 3, s_9_19, 0, 1, 0}, +{ 4, s_9_20, -1, 1, 0}, +{ 6, s_9_21, -1, 3, 0}, +{ 1, s_9_22, 0, 1, 0}, +{ 2, s_9_23, -1, 1, 0}, +{ 2, s_9_24, -2, 1, 0}, +{ 2, s_9_25, -3, 1, 0}, +{ 3, s_9_26, -4, 2, 0}, +{ 3, s_9_27, -5, 3, 0}, +{ 1, s_9_28, 0, 1, 0}, +{ 2, s_9_29, 0, 2, 0}, +{ 2, s_9_30, 0, 3, 0} }; -static const symbol s_10_0[1] = { 'a' }; -static const symbol s_10_1[2] = { 'j', 'a' }; -static const symbol s_10_2[1] = { 'd' }; -static const symbol s_10_3[2] = { 'a', 'd' }; -static const symbol s_10_4[2] = { 'e', 'd' }; -static const symbol s_10_5[2] = { 'o', 'd' }; -static const symbol s_10_6[3] = { 0xC3, 0xA1, 'd' }; -static const symbol s_10_7[3] = { 0xC3, 0xA9, 'd' }; -static const symbol s_10_8[3] = { 0xC3, 0xB6, 'd' }; -static const symbol s_10_9[1] = { 'e' }; -static const symbol s_10_10[2] = { 'j', 'e' }; -static const symbol s_10_11[2] = { 'n', 'k' }; -static const symbol s_10_12[3] = { 'u', 'n', 'k' }; -static const symbol s_10_13[4] = { 0xC3, 0xA1, 'n', 'k' }; -static const symbol s_10_14[4] = { 0xC3, 0xA9, 'n', 'k' }; -static const symbol s_10_15[4] = { 0xC3, 0xBC, 'n', 'k' }; -static const symbol s_10_16[2] = { 'u', 'k' }; -static const symbol s_10_17[3] = { 'j', 'u', 'k' }; -static const symbol s_10_18[5] = { 0xC3, 0xA1, 'j', 'u', 'k' }; -static const symbol s_10_19[3] = { 0xC3, 0xBC, 'k' }; -static const symbol s_10_20[4] = { 'j', 0xC3, 0xBC, 'k' }; -static const symbol s_10_21[6] = { 0xC3, 0xA9, 'j', 0xC3, 0xBC, 'k' }; -static const symbol s_10_22[1] = { 'm' }; -static const symbol s_10_23[2] = { 'a', 'm' }; -static const symbol s_10_24[2] = { 'e', 'm' }; -static const symbol s_10_25[2] = { 'o', 'm' }; -static const symbol s_10_26[3] = { 0xC3, 0xA1, 'm' }; -static const symbol s_10_27[3] = { 0xC3, 0xA9, 'm' }; -static const symbol s_10_28[1] = { 'o' }; -static const symbol s_10_29[2] = { 0xC3, 0xA1 }; -static const symbol s_10_30[2] = { 0xC3, 0xA9 }; - -static const struct among a_10[31] = -{ -{ 1, s_10_0, -1, 1, 0}, -{ 2, s_10_1, 0, 1, 0}, -{ 1, s_10_2, -1, 1, 0}, -{ 2, s_10_3, 2, 1, 0}, -{ 2, s_10_4, 2, 1, 0}, -{ 2, s_10_5, 2, 1, 0}, -{ 3, s_10_6, 2, 2, 0}, -{ 3, s_10_7, 2, 3, 0}, -{ 3, s_10_8, 2, 1, 0}, -{ 1, s_10_9, -1, 1, 0}, -{ 2, s_10_10, 9, 1, 0}, -{ 2, s_10_11, -1, 1, 0}, -{ 3, s_10_12, 11, 1, 0}, -{ 4, s_10_13, 11, 2, 0}, -{ 4, s_10_14, 11, 3, 0}, -{ 4, s_10_15, 11, 1, 0}, -{ 2, s_10_16, -1, 1, 0}, -{ 3, s_10_17, 16, 1, 0}, -{ 5, s_10_18, 17, 2, 0}, +static const symbol s_10_0[2] = { 'i', 'd' }; +static const symbol s_10_1[3] = { 'a', 'i', 'd' }; +static const symbol s_10_2[4] = { 'j', 'a', 'i', 'd' }; +static const symbol s_10_3[3] = { 'e', 'i', 'd' }; +static const symbol s_10_4[4] = { 'j', 'e', 'i', 'd' }; +static const symbol s_10_5[4] = { 0xC3, 0xA1, 'i', 'd' }; +static const symbol s_10_6[4] = { 0xC3, 0xA9, 'i', 'd' }; +static const symbol s_10_7[1] = { 'i' }; +static const symbol s_10_8[2] = { 'a', 'i' }; +static const symbol s_10_9[3] = { 'j', 'a', 'i' }; +static const symbol s_10_10[2] = { 'e', 'i' }; +static const symbol s_10_11[3] = { 'j', 'e', 'i' }; +static const symbol s_10_12[3] = { 0xC3, 0xA1, 'i' }; +static const symbol s_10_13[3] = { 0xC3, 0xA9, 'i' }; +static const symbol s_10_14[4] = { 'i', 't', 'e', 'k' }; +static const symbol s_10_15[5] = { 'e', 'i', 't', 'e', 'k' }; +static const symbol s_10_16[6] = { 'j', 'e', 'i', 't', 'e', 'k' }; +static const symbol s_10_17[6] = { 0xC3, 0xA9, 'i', 't', 'e', 'k' }; +static const symbol s_10_18[2] = { 'i', 'k' }; +static const symbol s_10_19[3] = { 'a', 'i', 'k' }; +static const symbol s_10_20[4] = { 'j', 'a', 'i', 'k' }; +static const symbol s_10_21[3] = { 'e', 'i', 'k' }; +static const symbol s_10_22[4] = { 'j', 'e', 'i', 'k' }; +static const symbol s_10_23[4] = { 0xC3, 0xA1, 'i', 'k' }; +static const symbol s_10_24[4] = { 0xC3, 0xA9, 'i', 'k' }; +static const symbol s_10_25[3] = { 'i', 'n', 'k' }; +static const symbol s_10_26[4] = { 'a', 'i', 'n', 'k' }; +static const symbol s_10_27[5] = { 'j', 'a', 'i', 'n', 'k' }; +static const symbol s_10_28[4] = { 'e', 'i', 'n', 'k' }; +static const symbol s_10_29[5] = { 'j', 'e', 'i', 'n', 'k' }; +static const symbol s_10_30[5] = { 0xC3, 0xA1, 'i', 'n', 'k' }; +static const symbol s_10_31[5] = { 0xC3, 0xA9, 'i', 'n', 'k' }; +static const symbol s_10_32[5] = { 'a', 'i', 't', 'o', 'k' }; +static const symbol s_10_33[6] = { 'j', 'a', 'i', 't', 'o', 'k' }; +static const symbol s_10_34[6] = { 0xC3, 0xA1, 'i', 't', 'o', 'k' }; +static const symbol s_10_35[2] = { 'i', 'm' }; +static const symbol s_10_36[3] = { 'a', 'i', 'm' }; +static const symbol s_10_37[4] = { 'j', 'a', 'i', 'm' }; +static const symbol s_10_38[3] = { 'e', 'i', 'm' }; +static const symbol s_10_39[4] = { 'j', 'e', 'i', 'm' }; +static const symbol s_10_40[4] = { 0xC3, 0xA1, 'i', 'm' }; +static const symbol s_10_41[4] = { 0xC3, 0xA9, 'i', 'm' }; +static const struct among a_10[42] = { +{ 2, s_10_0, 0, 1, 0}, +{ 3, s_10_1, -1, 1, 0}, +{ 4, s_10_2, -1, 1, 0}, +{ 3, s_10_3, -3, 1, 0}, +{ 4, s_10_4, -1, 1, 0}, +{ 4, s_10_5, -5, 2, 0}, +{ 4, s_10_6, -6, 3, 0}, +{ 1, s_10_7, 0, 1, 0}, +{ 2, s_10_8, -1, 1, 0}, +{ 3, s_10_9, -1, 1, 0}, +{ 2, s_10_10, -3, 1, 0}, +{ 3, s_10_11, -1, 1, 0}, +{ 3, s_10_12, -5, 2, 0}, +{ 3, s_10_13, -6, 3, 0}, +{ 4, s_10_14, 0, 1, 0}, +{ 5, s_10_15, -1, 1, 0}, +{ 6, s_10_16, -1, 1, 0}, +{ 6, s_10_17, -3, 3, 0}, +{ 2, s_10_18, 0, 1, 0}, { 3, s_10_19, -1, 1, 0}, -{ 4, s_10_20, 19, 1, 0}, -{ 6, s_10_21, 20, 3, 0}, -{ 1, s_10_22, -1, 1, 0}, -{ 2, s_10_23, 22, 1, 0}, -{ 2, s_10_24, 22, 1, 0}, -{ 2, s_10_25, 22, 1, 0}, -{ 3, s_10_26, 22, 2, 0}, -{ 3, s_10_27, 22, 3, 0}, -{ 1, s_10_28, -1, 1, 0}, -{ 2, s_10_29, -1, 2, 0}, -{ 2, s_10_30, -1, 3, 0} -}; - -static const symbol s_11_0[2] = { 'i', 'd' }; -static const symbol s_11_1[3] = { 'a', 'i', 'd' }; -static const symbol s_11_2[4] = { 'j', 'a', 'i', 'd' }; -static const symbol s_11_3[3] = { 'e', 'i', 'd' }; -static const symbol s_11_4[4] = { 'j', 'e', 'i', 'd' }; -static const symbol s_11_5[4] = { 0xC3, 0xA1, 'i', 'd' }; -static const symbol s_11_6[4] = { 0xC3, 0xA9, 'i', 'd' }; -static const symbol s_11_7[1] = { 'i' }; -static const symbol s_11_8[2] = { 'a', 'i' }; -static const symbol s_11_9[3] = { 'j', 'a', 'i' }; -static const symbol s_11_10[2] = { 'e', 'i' }; -static const symbol s_11_11[3] = { 'j', 'e', 'i' }; -static const symbol s_11_12[3] = { 0xC3, 0xA1, 'i' }; -static const symbol s_11_13[3] = { 0xC3, 0xA9, 'i' }; -static const symbol s_11_14[4] = { 'i', 't', 'e', 'k' }; -static const symbol s_11_15[5] = { 'e', 'i', 't', 'e', 'k' }; -static const symbol s_11_16[6] = { 'j', 'e', 'i', 't', 'e', 'k' }; -static const symbol s_11_17[6] = { 0xC3, 0xA9, 'i', 't', 'e', 'k' }; -static const symbol s_11_18[2] = { 'i', 'k' }; -static const symbol s_11_19[3] = { 'a', 'i', 'k' }; -static const symbol s_11_20[4] = { 'j', 'a', 'i', 'k' }; -static const symbol s_11_21[3] = { 'e', 'i', 'k' }; -static const symbol s_11_22[4] = { 'j', 'e', 'i', 'k' }; -static const symbol s_11_23[4] = { 0xC3, 0xA1, 'i', 'k' }; -static const symbol s_11_24[4] = { 0xC3, 0xA9, 'i', 'k' }; -static const symbol s_11_25[3] = { 'i', 'n', 'k' }; -static const symbol s_11_26[4] = { 'a', 'i', 'n', 'k' }; -static const symbol s_11_27[5] = { 'j', 'a', 'i', 'n', 'k' }; -static const symbol s_11_28[4] = { 'e', 'i', 'n', 'k' }; -static const symbol s_11_29[5] = { 'j', 'e', 'i', 'n', 'k' }; -static const symbol s_11_30[5] = { 0xC3, 0xA1, 'i', 'n', 'k' }; -static const symbol s_11_31[5] = { 0xC3, 0xA9, 'i', 'n', 'k' }; -static const symbol s_11_32[5] = { 'a', 'i', 't', 'o', 'k' }; -static const symbol s_11_33[6] = { 'j', 'a', 'i', 't', 'o', 'k' }; -static const symbol s_11_34[6] = { 0xC3, 0xA1, 'i', 't', 'o', 'k' }; -static const symbol s_11_35[2] = { 'i', 'm' }; -static const symbol s_11_36[3] = { 'a', 'i', 'm' }; -static const symbol s_11_37[4] = { 'j', 'a', 'i', 'm' }; -static const symbol s_11_38[3] = { 'e', 'i', 'm' }; -static const symbol s_11_39[4] = { 'j', 'e', 'i', 'm' }; -static const symbol s_11_40[4] = { 0xC3, 0xA1, 'i', 'm' }; -static const symbol s_11_41[4] = { 0xC3, 0xA9, 'i', 'm' }; - -static const struct among a_11[42] = -{ -{ 2, s_11_0, -1, 1, 0}, -{ 3, s_11_1, 0, 1, 0}, -{ 4, s_11_2, 1, 1, 0}, -{ 3, s_11_3, 0, 1, 0}, -{ 4, s_11_4, 3, 1, 0}, -{ 4, s_11_5, 0, 2, 0}, -{ 4, s_11_6, 0, 3, 0}, -{ 1, s_11_7, -1, 1, 0}, -{ 2, s_11_8, 7, 1, 0}, -{ 3, s_11_9, 8, 1, 0}, -{ 2, s_11_10, 7, 1, 0}, -{ 3, s_11_11, 10, 1, 0}, -{ 3, s_11_12, 7, 2, 0}, -{ 3, s_11_13, 7, 3, 0}, -{ 4, s_11_14, -1, 1, 0}, -{ 5, s_11_15, 14, 1, 0}, -{ 6, s_11_16, 15, 1, 0}, -{ 6, s_11_17, 14, 3, 0}, -{ 2, s_11_18, -1, 1, 0}, -{ 3, s_11_19, 18, 1, 0}, -{ 4, s_11_20, 19, 1, 0}, -{ 3, s_11_21, 18, 1, 0}, -{ 4, s_11_22, 21, 1, 0}, -{ 4, s_11_23, 18, 2, 0}, -{ 4, s_11_24, 18, 3, 0}, -{ 3, s_11_25, -1, 1, 0}, -{ 4, s_11_26, 25, 1, 0}, -{ 5, s_11_27, 26, 1, 0}, -{ 4, s_11_28, 25, 1, 0}, -{ 5, s_11_29, 28, 1, 0}, -{ 5, s_11_30, 25, 2, 0}, -{ 5, s_11_31, 25, 3, 0}, -{ 5, s_11_32, -1, 1, 0}, -{ 6, s_11_33, 32, 1, 0}, -{ 6, s_11_34, -1, 2, 0}, -{ 2, s_11_35, -1, 1, 0}, -{ 3, s_11_36, 35, 1, 0}, -{ 4, s_11_37, 36, 1, 0}, -{ 3, s_11_38, 35, 1, 0}, -{ 4, s_11_39, 38, 1, 0}, -{ 4, s_11_40, 35, 2, 0}, -{ 4, s_11_41, 35, 3, 0} +{ 4, s_10_20, -1, 1, 0}, +{ 3, s_10_21, -3, 1, 0}, +{ 4, s_10_22, -1, 1, 0}, +{ 4, s_10_23, -5, 2, 0}, +{ 4, s_10_24, -6, 3, 0}, +{ 3, s_10_25, 0, 1, 0}, +{ 4, s_10_26, -1, 1, 0}, +{ 5, s_10_27, -1, 1, 0}, +{ 4, s_10_28, -3, 1, 0}, +{ 5, s_10_29, -1, 1, 0}, +{ 5, s_10_30, -5, 2, 0}, +{ 5, s_10_31, -6, 3, 0}, +{ 5, s_10_32, 0, 1, 0}, +{ 6, s_10_33, -1, 1, 0}, +{ 6, s_10_34, 0, 2, 0}, +{ 2, s_10_35, 0, 1, 0}, +{ 3, s_10_36, -1, 1, 0}, +{ 4, s_10_37, -1, 1, 0}, +{ 3, s_10_38, -3, 1, 0}, +{ 4, s_10_39, -1, 1, 0}, +{ 4, s_10_40, -5, 2, 0}, +{ 4, s_10_41, -6, 3, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 17, 36, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'e' }; -static const symbol s_3[] = { 'a' }; -static const symbol s_4[] = { 'a' }; -static const symbol s_5[] = { 'e' }; -static const symbol s_6[] = { 'a' }; -static const symbol s_7[] = { 'e' }; -static const symbol s_8[] = { 'e' }; -static const symbol s_9[] = { 'a' }; -static const symbol s_10[] = { 'a' }; -static const symbol s_11[] = { 'e' }; -static const symbol s_12[] = { 'a' }; -static const symbol s_13[] = { 'e' }; - static int r_mark_regions(struct SN_env * z) { - z->I[0] = z->l; - { int c1 = z->c; - if (in_grouping_U(z, g_v, 97, 369, 0)) goto lab1; - - if (in_grouping_U(z, g_v, 97, 369, 1) < 0) goto lab1; - { int c2 = z->c; - if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 3 || !((101187584 >> (z->p[z->c + 1] & 0x1f)) & 1)) goto lab3; - if (!find_among(z, a_0, 8)) goto lab3; - goto lab2; - lab3: - z->c = c2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + ((SN_local *)z)->i_p1 = z->l; + do { + int v_1 = z->c; + if (in_grouping_U(z, g_v, 97, 369, 0)) goto lab0; + { + int v_2 = z->c; + { + int ret = in_grouping_U(z, g_v, 97, 369, 1); if (ret < 0) goto lab1; - z->c = ret; + z->c += ret; } + ((SN_local *)z)->i_p1 = z->c; + lab1: + z->c = v_2; } - lab2: - z->I[0] = z->c; - goto lab0; - lab1: - z->c = c1; - if (out_grouping_U(z, g_v, 97, 369, 0)) return 0; - + break; + lab0: + z->c = v_1; { int ret = out_grouping_U(z, g_v, 97, 369, 1); if (ret < 0) return 0; z->c += ret; } - z->I[0] = z->c; - } -lab0: + ((SN_local *)z)->i_p1 = z->c; + } while (0); return 1; } static int r_R1(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_v_ending(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 161 && z->p[z->c - 1] != 169)) return 0; - among_var = find_among_b(z, a_1, 2); + among_var = find_among_b(z, a_0, 2, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; @@ -541,26 +496,30 @@ static int r_v_ending(struct SN_env * z) { } static int r_double(struct SN_env * z) { - { int m_test1 = z->l - z->c; + { + int v_1 = z->l - z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((106790108 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_2, 23)) return 0; - z->c = z->l - m_test1; + if (!find_among_b(z, a_1, 23, 0)) return 0; + z->c = z->l - v_1; } return 1; } static int r_undouble(struct SN_env * z) { - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -569,57 +528,59 @@ static int r_undouble(struct SN_env * z) { static int r_instrum(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] != 108) return 0; - if (!find_among_b(z, a_3, 2)) return 0; + if (!find_among_b(z, a_2, 2, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = r_double(z); + { + int ret = r_double(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_undouble(z); - if (ret <= 0) return ret; - } - return 1; + return r_undouble(z); } static int r_case(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_4, 44)) return 0; + if (!find_among_b(z, a_3, 44, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_v_ending(z); - if (ret <= 0) return ret; - } - return 1; + return r_v_ending(z); } static int r_case_special(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 110 && z->p[z->c - 1] != 116)) return 0; - among_var = find_among_b(z, a_5, 3); + among_var = find_among_b(z, a_4, 3, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; @@ -631,25 +592,29 @@ static int r_case_other(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 3 <= z->lb || z->p[z->c - 1] != 108) return 0; - among_var = find_among_b(z, a_6, 6); + among_var = find_among_b(z, a_5, 6, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; @@ -660,46 +625,50 @@ static int r_case_other(struct SN_env * z) { static int r_factive(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 161 && z->p[z->c - 1] != 169)) return 0; - if (!find_among_b(z, a_7, 2)) return 0; + if (!find_among_b(z, a_6, 2, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = r_double(z); + { + int ret = r_double(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_undouble(z); - if (ret <= 0) return ret; - } - return 1; + return r_undouble(z); } static int r_plural(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 107) return 0; - among_var = find_among_b(z, a_8, 7); + among_var = find_among_b(z, a_7, 7, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -711,25 +680,29 @@ static int r_owned(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 169)) return 0; - among_var = find_among_b(z, a_9, 12); + among_var = find_among_b(z, a_8, 12, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; @@ -740,25 +713,29 @@ static int r_owned(struct SN_env * z) { static int r_sing_owner(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_10, 31); + among_var = find_among_b(z, a_9, 31, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; @@ -770,25 +747,29 @@ static int r_plur_owner(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((10768 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_11, 42); + among_var = find_among_b(z, a_10, 42, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_13); + { + int ret = slice_from_s(z, 1, s_13); if (ret < 0) return ret; } break; @@ -797,73 +778,100 @@ static int r_plur_owner(struct SN_env * z) { } extern int hungarian_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_instrum(z); + { + int v_2 = z->l - z->c; + { + int ret = r_instrum(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_case(z); + { + int v_3 = z->l - z->c; + { + int ret = r_case(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_case_special(z); + { + int v_4 = z->l - z->c; + { + int ret = r_case_special(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_case_other(z); + { + int v_5 = z->l - z->c; + { + int ret = r_case_other(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_factive(z); + { + int v_6 = z->l - z->c; + { + int ret = r_factive(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_owned(z); + { + int v_7 = z->l - z->c; + { + int ret = r_owned(z); if (ret < 0) return ret; } - z->c = z->l - m7; + z->c = z->l - v_7; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_sing_owner(z); + { + int v_8 = z->l - z->c; + { + int ret = r_sing_owner(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_plur_owner(z); + { + int v_9 = z->l - z->c; + { + int ret = r_plur_owner(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_plural(z); + { + int v_10 = z->l - z->c; + { + int ret = r_plural(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } z->c = z->lb; return 1; } -extern struct SN_env * hungarian_UTF_8_create_env(void) { return SN_create_env(0, 1); } +extern struct SN_env * hungarian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void hungarian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void hungarian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_indonesian.c b/src/backend/snowball/libstemmer/stem_UTF_8_indonesian.c index 2c0e5904951c6..0c679bfb1ab5a 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_indonesian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_indonesian.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from indonesian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_indonesian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_prefix; + int i_measure; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,59 +21,44 @@ extern int indonesian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif -static int r_VOWEL(struct SN_env * z); -static int r_SUFFIX_I_OK(struct SN_env * z); -static int r_SUFFIX_AN_OK(struct SN_env * z); -static int r_SUFFIX_KAN_OK(struct SN_env * z); -static int r_KER(struct SN_env * z); + static int r_remove_suffix(struct SN_env * z); static int r_remove_second_order_prefix(struct SN_env * z); static int r_remove_first_order_prefix(struct SN_env * z); static int r_remove_possessive_pronoun(struct SN_env * z); static int r_remove_particle(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * indonesian_UTF_8_create_env(void); -extern void indonesian_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 's' }; +static const symbol s_1[] = { 's' }; +static const symbol s_2[] = { 'p' }; +static const symbol s_3[] = { 'p' }; +static const symbol s_4[] = { 'a', 'j', 'a', 'r' }; +static const symbol s_5[] = { 'a', 'j', 'a', 'r' }; +static const symbol s_6[] = { 'e', 'r' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[3] = { 'k', 'a', 'h' }; static const symbol s_0_1[3] = { 'l', 'a', 'h' }; static const symbol s_0_2[3] = { 'p', 'u', 'n' }; - -static const struct among a_0[3] = -{ -{ 3, s_0_0, -1, 1, 0}, -{ 3, s_0_1, -1, 1, 0}, -{ 3, s_0_2, -1, 1, 0} +static const struct among a_0[3] = { +{ 3, s_0_0, 0, 1, 0}, +{ 3, s_0_1, 0, 1, 0}, +{ 3, s_0_2, 0, 1, 0} }; static const symbol s_1_0[3] = { 'n', 'y', 'a' }; static const symbol s_1_1[2] = { 'k', 'u' }; static const symbol s_1_2[2] = { 'm', 'u' }; - -static const struct among a_1[3] = -{ -{ 3, s_1_0, -1, 1, 0}, -{ 2, s_1_1, -1, 1, 0}, -{ 2, s_1_2, -1, 1, 0} +static const struct among a_1[3] = { +{ 3, s_1_0, 0, 1, 0}, +{ 2, s_1_1, 0, 1, 0}, +{ 2, s_1_2, 0, 1, 0} }; static const symbol s_2_0[1] = { 'i' }; static const symbol s_2_1[2] = { 'a', 'n' }; -static const symbol s_2_2[3] = { 'k', 'a', 'n' }; - -static const struct among a_2[3] = -{ -{ 1, s_2_0, -1, 1, r_SUFFIX_I_OK}, -{ 2, s_2_1, -1, 1, r_SUFFIX_AN_OK}, -{ 3, s_2_2, 1, 1, r_SUFFIX_KAN_OK} +static const struct among a_2[2] = { +{ 1, s_2_0, 0, 2, 0}, +{ 2, s_2_1, 0, 1, 0} }; static const symbol s_3_0[2] = { 'd', 'i' }; @@ -70,123 +67,97 @@ static const symbol s_3_2[2] = { 'm', 'e' }; static const symbol s_3_3[3] = { 'm', 'e', 'm' }; static const symbol s_3_4[3] = { 'm', 'e', 'n' }; static const symbol s_3_5[4] = { 'm', 'e', 'n', 'g' }; -static const symbol s_3_6[4] = { 'm', 'e', 'n', 'y' }; -static const symbol s_3_7[3] = { 'p', 'e', 'm' }; -static const symbol s_3_8[3] = { 'p', 'e', 'n' }; -static const symbol s_3_9[4] = { 'p', 'e', 'n', 'g' }; -static const symbol s_3_10[4] = { 'p', 'e', 'n', 'y' }; -static const symbol s_3_11[3] = { 't', 'e', 'r' }; - -static const struct among a_3[12] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 2, s_3_1, -1, 2, 0}, -{ 2, s_3_2, -1, 1, 0}, -{ 3, s_3_3, 2, 5, 0}, -{ 3, s_3_4, 2, 1, 0}, -{ 4, s_3_5, 4, 1, 0}, -{ 4, s_3_6, 4, 3, r_VOWEL}, -{ 3, s_3_7, -1, 6, 0}, -{ 3, s_3_8, -1, 2, 0}, -{ 4, s_3_9, 8, 2, 0}, -{ 4, s_3_10, 8, 4, r_VOWEL}, -{ 3, s_3_11, -1, 1, 0} +static const symbol s_3_6[3] = { 'p', 'e', 'm' }; +static const symbol s_3_7[3] = { 'p', 'e', 'n' }; +static const symbol s_3_8[4] = { 'p', 'e', 'n', 'g' }; +static const symbol s_3_9[3] = { 't', 'e', 'r' }; +static const struct among a_3[10] = { +{ 2, s_3_0, 0, 1, 0}, +{ 2, s_3_1, 0, 3, 0}, +{ 2, s_3_2, 0, 1, 0}, +{ 3, s_3_3, -1, 5, 0}, +{ 3, s_3_4, -2, 2, 0}, +{ 4, s_3_5, -1, 1, 0}, +{ 3, s_3_6, 0, 6, 0}, +{ 3, s_3_7, 0, 4, 0}, +{ 4, s_3_8, -1, 3, 0}, +{ 3, s_3_9, 0, 1, 0} }; static const symbol s_4_0[2] = { 'b', 'e' }; -static const symbol s_4_1[7] = { 'b', 'e', 'l', 'a', 'j', 'a', 'r' }; -static const symbol s_4_2[3] = { 'b', 'e', 'r' }; -static const symbol s_4_3[2] = { 'p', 'e' }; -static const symbol s_4_4[7] = { 'p', 'e', 'l', 'a', 'j', 'a', 'r' }; -static const symbol s_4_5[3] = { 'p', 'e', 'r' }; - -static const struct among a_4[6] = -{ -{ 2, s_4_0, -1, 3, r_KER}, -{ 7, s_4_1, 0, 4, 0}, -{ 3, s_4_2, 0, 3, 0}, -{ 2, s_4_3, -1, 1, 0}, -{ 7, s_4_4, 3, 2, 0}, -{ 3, s_4_5, 3, 1, 0} +static const symbol s_4_1[2] = { 'p', 'e' }; +static const struct among a_4[2] = { +{ 2, s_4_0, 0, 2, 0}, +{ 2, s_4_1, 0, 1, 0} }; static const unsigned char g_vowel[] = { 17, 65, 16 }; -static const symbol s_0[] = { 'e', 'r' }; -static const symbol s_1[] = { 's' }; -static const symbol s_2[] = { 's' }; -static const symbol s_3[] = { 'p' }; -static const symbol s_4[] = { 'p' }; -static const symbol s_5[] = { 'a', 'j', 'a', 'r' }; -static const symbol s_6[] = { 'a', 'j', 'a', 'r' }; - static int r_remove_particle(struct SN_env * z) { z->ket = z->c; if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 104 && z->p[z->c - 1] != 110)) return 0; - if (!find_among_b(z, a_0, 3)) return 0; + if (!find_among_b(z, a_0, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; + ((SN_local *)z)->i_measure -= 1; return 1; } static int r_remove_possessive_pronoun(struct SN_env * z) { z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 117)) return 0; - if (!find_among_b(z, a_1, 3)) return 0; + if (!find_among_b(z, a_1, 3, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; - return 1; -} - -static int r_SUFFIX_KAN_OK(struct SN_env * z) { - - if (z->I[0] == 3) return 0; - if (z->I[0] == 2) return 0; - return 1; -} - -static int r_SUFFIX_AN_OK(struct SN_env * z) { - return z->I[0] != 1; -} - -static int r_SUFFIX_I_OK(struct SN_env * z) { - if (z->I[0] > 2) return 0; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; - z->c--; - return 0; - lab0: - z->c = z->l - m1; - } + ((SN_local *)z)->i_measure -= 1; return 1; } static int r_remove_suffix(struct SN_env * z) { + int among_var; z->ket = z->c; if (z->c <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 110)) return 0; - if (!find_among_b(z, a_2, 3)) return 0; + among_var = find_among_b(z, a_2, 2, 0); + if (!among_var) return 0; z->bra = z->c; - { int ret = slice_del(z); + switch (among_var) { + case 1: + do { + int v_1 = z->l - z->c; + if (((SN_local *)z)->i_prefix == 3) goto lab0; + if (((SN_local *)z)->i_prefix == 2) goto lab0; + if (z->c <= z->lb || z->p[z->c - 1] != 'k') goto lab0; + z->c--; + z->bra = z->c; + break; + lab0: + z->c = z->l - v_1; + if (((SN_local *)z)->i_prefix == 1) return 0; + } while (0); + break; + case 2: + if (((SN_local *)z)->i_prefix > 2) return 0; + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + z->c--; + return 0; + lab1: + z->c = z->l - v_2; + } + break; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; - return 1; -} - -static int r_VOWEL(struct SN_env * z) { - if (in_grouping_U(z, g_vowel, 97, 117, 0)) return 0; - return 1; -} - -static int r_KER(struct SN_env * z) { - if (out_grouping_U(z, g_vowel, 97, 117, 0)) return 0; - if (!(eq_s(z, 2, s_0))) return 0; + ((SN_local *)z)->i_measure -= 1; return 1; } @@ -194,77 +165,127 @@ static int r_remove_first_order_prefix(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 1 >= z->l || (z->p[z->c + 1] != 105 && z->p[z->c + 1] != 101)) return 0; - among_var = find_among(z, a_3, 12); + among_var = find_among(z, a_3, 10, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[0] = 1; - z->I[1] -= 1; + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; break; case 2: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[0] = 3; - z->I[1] -= 1; + do { + int v_1 = z->c; + if (z->c == z->l || z->p[z->c] != 'y') goto lab0; + z->c++; + { + int v_2 = z->c; + if (in_grouping_U(z, g_vowel, 97, 117, 0)) goto lab0; + z->c = v_2; + } + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_0); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; + break; + lab0: + z->c = v_1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; + } while (0); break; case 3: - z->I[0] = 1; - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] -= 1; + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; break; case 4: - z->I[0] = 3; - { int ret = slice_from_s(z, 1, s_2); - if (ret < 0) return ret; - } - z->I[1] -= 1; + do { + int v_3 = z->c; + if (z->c == z->l || z->p[z->c] != 'y') goto lab1; + z->c++; + { + int v_4 = z->c; + if (in_grouping_U(z, g_vowel, 97, 117, 0)) goto lab1; + z->c = v_4; + } + z->ket = z->c; + { + int ret = slice_from_s(z, 1, s_1); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; + break; + lab1: + z->c = v_3; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; + } while (0); break; case 5: - z->I[0] = 1; - z->I[1] -= 1; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_vowel, 97, 117, 0)) goto lab1; - z->c = c2; - { int ret = slice_from_s(z, 1, s_3); + ((SN_local *)z)->i_prefix = 1; + ((SN_local *)z)->i_measure -= 1; + do { + int v_5 = z->c; + { + int v_6 = z->c; + if (in_grouping_U(z, g_vowel, 97, 117, 0)) goto lab2; + z->c = v_6; + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } } - goto lab0; - lab1: - z->c = c1; - { int ret = slice_del(z); + break; + lab2: + z->c = v_5; + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab0: + } while (0); break; case 6: - z->I[0] = 3; - z->I[1] -= 1; - { int c3 = z->c; - { int c4 = z->c; + ((SN_local *)z)->i_prefix = 3; + ((SN_local *)z)->i_measure -= 1; + do { + int v_7 = z->c; + { + int v_8 = z->c; if (in_grouping_U(z, g_vowel, 97, 117, 0)) goto lab3; - z->c = c4; - { int ret = slice_from_s(z, 1, s_4); + z->c = v_8; + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } } - goto lab2; + break; lab3: - z->c = c3; - { int ret = slice_del(z); + z->c = v_7; + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab2: + } while (0); break; } return 1; @@ -274,134 +295,174 @@ static int r_remove_second_order_prefix(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 1 >= z->l || z->p[z->c + 1] != 101) return 0; - among_var = find_among(z, a_4, 6); + among_var = find_among(z, a_4, 2, 0); if (!among_var) return 0; - z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[0] = 2; - z->I[1] -= 1; + do { + int v_1 = z->c; + if (z->c == z->l || z->p[z->c] != 'r') goto lab0; + z->c++; + z->ket = z->c; + ((SN_local *)z)->i_prefix = 2; + break; + lab0: + z->c = v_1; + if (z->c == z->l || z->p[z->c] != 'l') goto lab1; + z->c++; + z->ket = z->c; + if (!(eq_s(z, 4, s_4))) goto lab1; + break; + lab1: + z->c = v_1; + z->ket = z->c; + ((SN_local *)z)->i_prefix = 2; + } while (0); break; case 2: - { int ret = slice_from_s(z, 4, s_5); - if (ret < 0) return ret; - } - z->I[1] -= 1; - break; - case 3: - { int ret = slice_del(z); - if (ret < 0) return ret; - } - z->I[0] = 4; - z->I[1] -= 1; - break; - case 4: - { int ret = slice_from_s(z, 4, s_6); - if (ret < 0) return ret; - } - z->I[0] = 4; - z->I[1] -= 1; + do { + int v_2 = z->c; + if (z->c == z->l || z->p[z->c] != 'r') goto lab2; + z->c++; + z->ket = z->c; + break; + lab2: + z->c = v_2; + if (z->c == z->l || z->p[z->c] != 'l') goto lab3; + z->c++; + z->ket = z->c; + if (!(eq_s(z, 4, s_5))) goto lab3; + break; + lab3: + z->c = v_2; + z->ket = z->c; + if (out_grouping_U(z, g_vowel, 97, 117, 0)) return 0; + if (!(eq_s(z, 2, s_6))) return 0; + } while (0); + ((SN_local *)z)->i_prefix = 4; break; } + ((SN_local *)z)->i_measure -= 1; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } return 1; } extern int indonesian_UTF_8_stem(struct SN_env * z) { - z->I[1] = 0; - { int c1 = z->c; - while(1) { - int c2 = z->c; - + ((SN_local *)z)->i_measure = 0; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; { int ret = out_grouping_U(z, g_vowel, 97, 117, 1); if (ret < 0) goto lab1; z->c += ret; } - z->I[1] += 1; + ((SN_local *)z)->i_measure += 1; continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } - if (z->I[1] <= 2) return 0; - z->I[0] = 0; + if (((SN_local *)z)->i_measure <= 2) return 0; + ((SN_local *)z)->i_prefix = 0; z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_remove_particle(z); + { + int v_3 = z->l - z->c; + { + int ret = r_remove_particle(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - if (z->I[1] <= 2) return 0; - { int m4 = z->l - z->c; (void)m4; - { int ret = r_remove_possessive_pronoun(z); + if (((SN_local *)z)->i_measure <= 2) return 0; + { + int v_4 = z->l - z->c; + { + int ret = r_remove_possessive_pronoun(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; - if (z->I[1] <= 2) return 0; - { int c5 = z->c; - { int c_test6 = z->c; - { int ret = r_remove_first_order_prefix(z); - if (ret == 0) goto lab3; + if (((SN_local *)z)->i_measure <= 2) return 0; + do { + int v_5 = z->c; + { + int v_6 = z->c; + { + int ret = r_remove_first_order_prefix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - { int c7 = z->c; - { int c_test8 = z->c; - if (z->I[1] <= 2) goto lab4; + { + int v_7 = z->c; + { + int v_8 = z->c; + if (((SN_local *)z)->i_measure <= 2) goto lab3; z->lb = z->c; z->c = z->l; - - { int ret = r_remove_suffix(z); - if (ret == 0) goto lab4; + { + int ret = r_remove_suffix(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } z->c = z->lb; - z->c = c_test8; + z->c = v_8; } - if (z->I[1] <= 2) goto lab4; - { int ret = r_remove_second_order_prefix(z); - if (ret == 0) goto lab4; + if (((SN_local *)z)->i_measure <= 2) goto lab3; + { + int ret = r_remove_second_order_prefix(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - lab4: - z->c = c7; + lab3: + z->c = v_7; } - z->c = c_test6; + z->c = v_6; } - goto lab2; - lab3: - z->c = c5; - { int c9 = z->c; - { int ret = r_remove_second_order_prefix(z); + break; + lab2: + z->c = v_5; + { + int v_9 = z->c; + { + int ret = r_remove_second_order_prefix(z); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; } - { int c10 = z->c; - if (z->I[1] <= 2) goto lab5; + { + int v_10 = z->c; + if (((SN_local *)z)->i_measure <= 2) goto lab4; z->lb = z->c; z->c = z->l; - - { int ret = r_remove_suffix(z); - if (ret == 0) goto lab5; + { + int ret = r_remove_suffix(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } z->c = z->lb; - lab5: - z->c = c10; + lab4: + z->c = v_10; } - } -lab2: + } while (0); return 1; } -extern struct SN_env * indonesian_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * indonesian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_prefix = 0; + ((SN_local *)z)->i_measure = 0; + } + return z; +} -extern void indonesian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void indonesian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_irish.c b/src/backend/snowball/libstemmer/stem_UTF_8_irish.c index c79b9ee57e64a..f7f8e6ceade45 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_irish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_irish.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from irish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_irish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int irish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_verb_sfx(struct SN_env * z); static int r_deriv(struct SN_env * z); static int r_noun_sfx(struct SN_env * z); @@ -17,18 +31,22 @@ static int r_initial_morph(struct SN_env * z); static int r_RV(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * irish_UTF_8_create_env(void); -extern void irish_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'f' }; +static const symbol s_1[] = { 's' }; +static const symbol s_2[] = { 'b' }; +static const symbol s_3[] = { 'c' }; +static const symbol s_4[] = { 'd' }; +static const symbol s_5[] = { 'g' }; +static const symbol s_6[] = { 'p' }; +static const symbol s_7[] = { 't' }; +static const symbol s_8[] = { 'm' }; +static const symbol s_9[] = { 'a', 'r', 'c' }; +static const symbol s_10[] = { 'g', 'i', 'n' }; +static const symbol s_11[] = { 'g', 'r', 'a', 'f' }; +static const symbol s_12[] = { 'p', 'a', 'i', 't', 'e' }; +static const symbol s_13[] = { 0xC3, 0xB3, 'i', 'd' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 'b', '\'' }; static const symbol s_0_1[2] = { 'b', 'h' }; static const symbol s_0_2[3] = { 'b', 'h', 'f' }; @@ -53,33 +71,31 @@ static const symbol s_0_20[2] = { 's', 'h' }; static const symbol s_0_21[2] = { 't', '-' }; static const symbol s_0_22[2] = { 't', 'h' }; static const symbol s_0_23[2] = { 't', 's' }; - -static const struct among a_0[24] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 2, s_0_1, -1, 4, 0}, -{ 3, s_0_2, 1, 2, 0}, -{ 2, s_0_3, -1, 8, 0}, -{ 2, s_0_4, -1, 5, 0}, -{ 2, s_0_5, -1, 1, 0}, -{ 4, s_0_6, 5, 2, 0}, -{ 2, s_0_7, -1, 6, 0}, -{ 2, s_0_8, -1, 9, 0}, -{ 2, s_0_9, -1, 2, 0}, -{ 2, s_0_10, -1, 5, 0}, -{ 2, s_0_11, -1, 7, 0}, -{ 2, s_0_12, -1, 1, 0}, -{ 2, s_0_13, -1, 1, 0}, -{ 2, s_0_14, -1, 4, 0}, -{ 2, s_0_15, -1, 10, 0}, -{ 2, s_0_16, -1, 1, 0}, -{ 2, s_0_17, -1, 6, 0}, -{ 2, s_0_18, -1, 7, 0}, -{ 2, s_0_19, -1, 8, 0}, -{ 2, s_0_20, -1, 3, 0}, -{ 2, s_0_21, -1, 1, 0}, -{ 2, s_0_22, -1, 9, 0}, -{ 2, s_0_23, -1, 3, 0} +static const struct among a_0[24] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 4, 0}, +{ 3, s_0_2, -1, 2, 0}, +{ 2, s_0_3, 0, 8, 0}, +{ 2, s_0_4, 0, 5, 0}, +{ 2, s_0_5, 0, 1, 0}, +{ 4, s_0_6, -1, 2, 0}, +{ 2, s_0_7, 0, 6, 0}, +{ 2, s_0_8, 0, 9, 0}, +{ 2, s_0_9, 0, 2, 0}, +{ 2, s_0_10, 0, 5, 0}, +{ 2, s_0_11, 0, 7, 0}, +{ 2, s_0_12, 0, 1, 0}, +{ 2, s_0_13, 0, 1, 0}, +{ 2, s_0_14, 0, 4, 0}, +{ 2, s_0_15, 0, 10, 0}, +{ 2, s_0_16, 0, 1, 0}, +{ 2, s_0_17, 0, 6, 0}, +{ 2, s_0_18, 0, 7, 0}, +{ 2, s_0_19, 0, 8, 0}, +{ 2, s_0_20, 0, 3, 0}, +{ 2, s_0_21, 0, 1, 0}, +{ 2, s_0_22, 0, 9, 0}, +{ 2, s_0_23, 0, 3, 0} }; static const symbol s_1_0[7] = { 0xC3, 0xAD, 'o', 'c', 'h', 't', 'a' }; @@ -98,25 +114,23 @@ static const symbol s_1_12[6] = { 0xC3, 0xAD, 'o', 'c', 'h', 't' }; static const symbol s_1_13[7] = { 'a', 0xC3, 0xAD, 'o', 'c', 'h', 't' }; static const symbol s_1_14[4] = { 'i', 'r', 0xC3, 0xAD }; static const symbol s_1_15[5] = { 'a', 'i', 'r', 0xC3, 0xAD }; - -static const struct among a_1[16] = -{ -{ 7, s_1_0, -1, 1, 0}, -{ 8, s_1_1, 0, 1, 0}, -{ 3, s_1_2, -1, 2, 0}, -{ 4, s_1_3, 2, 2, 0}, -{ 3, s_1_4, -1, 1, 0}, -{ 4, s_1_5, 4, 1, 0}, -{ 3, s_1_6, -1, 1, 0}, -{ 4, s_1_7, 6, 1, 0}, -{ 3, s_1_8, -1, 1, 0}, -{ 4, s_1_9, 8, 1, 0}, -{ 3, s_1_10, -1, 1, 0}, -{ 4, s_1_11, 10, 1, 0}, -{ 6, s_1_12, -1, 1, 0}, -{ 7, s_1_13, 12, 1, 0}, -{ 4, s_1_14, -1, 2, 0}, -{ 5, s_1_15, 14, 2, 0} +static const struct among a_1[16] = { +{ 7, s_1_0, 0, 1, 0}, +{ 8, s_1_1, -1, 1, 0}, +{ 3, s_1_2, 0, 2, 0}, +{ 4, s_1_3, -1, 2, 0}, +{ 3, s_1_4, 0, 1, 0}, +{ 4, s_1_5, -1, 1, 0}, +{ 3, s_1_6, 0, 1, 0}, +{ 4, s_1_7, -1, 1, 0}, +{ 3, s_1_8, 0, 1, 0}, +{ 4, s_1_9, -1, 1, 0}, +{ 3, s_1_10, 0, 1, 0}, +{ 4, s_1_11, -1, 1, 0}, +{ 6, s_1_12, 0, 1, 0}, +{ 7, s_1_13, -1, 1, 0}, +{ 4, s_1_14, 0, 2, 0}, +{ 5, s_1_15, -1, 2, 0} }; static const symbol s_2_0[9] = { 0xC3, 0xB3, 'i', 'd', 'e', 'a', 'c', 'h', 'a' }; @@ -144,34 +158,32 @@ static const symbol s_2_21[5] = { 'e', 'a', 'c', 'h', 't' }; static const symbol s_2_22[11] = { 'g', 'r', 'a', 'f', 'a', 0xC3, 0xAD, 'o', 'c', 'h', 't' }; static const symbol s_2_23[10] = { 'a', 'r', 'c', 'a', 'c', 'h', 't', 'a', 0xC3, 0xAD }; static const symbol s_2_24[14] = { 'g', 'r', 'a', 'f', 'a', 0xC3, 0xAD, 'o', 'c', 'h', 't', 'a', 0xC3, 0xAD }; - -static const struct among a_2[25] = -{ -{ 9, s_2_0, -1, 6, 0}, -{ 7, s_2_1, -1, 5, 0}, -{ 5, s_2_2, -1, 1, 0}, -{ 8, s_2_3, 2, 2, 0}, -{ 6, s_2_4, 2, 1, 0}, -{ 12, s_2_5, -1, 4, 0}, -{ 5, s_2_6, -1, 5, 0}, -{ 3, s_2_7, -1, 1, 0}, -{ 4, s_2_8, 7, 1, 0}, -{ 8, s_2_9, 8, 6, 0}, -{ 7, s_2_10, 8, 3, 0}, -{ 6, s_2_11, 7, 5, 0}, -{ 10, s_2_12, -1, 4, 0}, -{ 7, s_2_13, -1, 5, 0}, -{ 7, s_2_14, -1, 6, 0}, -{ 8, s_2_15, -1, 1, 0}, -{ 9, s_2_16, 15, 1, 0}, -{ 6, s_2_17, -1, 3, 0}, -{ 5, s_2_18, -1, 3, 0}, -{ 4, s_2_19, -1, 1, 0}, -{ 7, s_2_20, 19, 2, 0}, -{ 5, s_2_21, 19, 1, 0}, -{ 11, s_2_22, -1, 4, 0}, -{ 10, s_2_23, -1, 2, 0}, -{ 14, s_2_24, -1, 4, 0} +static const struct among a_2[25] = { +{ 9, s_2_0, 0, 6, 0}, +{ 7, s_2_1, 0, 5, 0}, +{ 5, s_2_2, 0, 1, 0}, +{ 8, s_2_3, -1, 2, 0}, +{ 6, s_2_4, -2, 1, 0}, +{ 12, s_2_5, 0, 4, 0}, +{ 5, s_2_6, 0, 5, 0}, +{ 3, s_2_7, 0, 1, 0}, +{ 4, s_2_8, -1, 1, 0}, +{ 8, s_2_9, -1, 6, 0}, +{ 7, s_2_10, -2, 3, 0}, +{ 6, s_2_11, -4, 5, 0}, +{ 10, s_2_12, 0, 4, 0}, +{ 7, s_2_13, 0, 5, 0}, +{ 7, s_2_14, 0, 6, 0}, +{ 8, s_2_15, 0, 1, 0}, +{ 9, s_2_16, -1, 1, 0}, +{ 6, s_2_17, 0, 3, 0}, +{ 5, s_2_18, 0, 3, 0}, +{ 4, s_2_19, 0, 1, 0}, +{ 7, s_2_20, -1, 2, 0}, +{ 5, s_2_21, -2, 1, 0}, +{ 11, s_2_22, 0, 4, 0}, +{ 10, s_2_23, 0, 2, 0}, +{ 14, s_2_24, 0, 4, 0} }; static const symbol s_3_0[4] = { 'i', 'm', 'i', 'd' }; @@ -186,74 +198,54 @@ static const symbol s_3_8[4] = { 0xC3, 0xA1, 'i', 'l' }; static const symbol s_3_9[3] = { 'a', 'i', 'n' }; static const symbol s_3_10[4] = { 't', 'e', 'a', 'r' }; static const symbol s_3_11[3] = { 't', 'a', 'r' }; - -static const struct among a_3[12] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 5, s_3_1, 0, 1, 0}, -{ 5, s_3_2, -1, 1, 0}, -{ 6, s_3_3, 2, 1, 0}, -{ 3, s_3_4, -1, 2, 0}, -{ 4, s_3_5, 4, 2, 0}, -{ 5, s_3_6, -1, 1, 0}, -{ 4, s_3_7, -1, 1, 0}, -{ 4, s_3_8, -1, 2, 0}, -{ 3, s_3_9, -1, 2, 0}, -{ 4, s_3_10, -1, 2, 0}, -{ 3, s_3_11, -1, 2, 0} +static const struct among a_3[12] = { +{ 4, s_3_0, 0, 1, 0}, +{ 5, s_3_1, -1, 1, 0}, +{ 5, s_3_2, 0, 1, 0}, +{ 6, s_3_3, -1, 1, 0}, +{ 3, s_3_4, 0, 2, 0}, +{ 4, s_3_5, -1, 2, 0}, +{ 5, s_3_6, 0, 1, 0}, +{ 4, s_3_7, 0, 1, 0}, +{ 4, s_3_8, 0, 2, 0}, +{ 3, s_3_9, 0, 2, 0}, +{ 4, s_3_10, 0, 2, 0}, +{ 3, s_3_11, 0, 2, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 17, 4, 2 }; -static const symbol s_0[] = { 'f' }; -static const symbol s_1[] = { 's' }; -static const symbol s_2[] = { 'b' }; -static const symbol s_3[] = { 'c' }; -static const symbol s_4[] = { 'd' }; -static const symbol s_5[] = { 'g' }; -static const symbol s_6[] = { 'p' }; -static const symbol s_7[] = { 't' }; -static const symbol s_8[] = { 'm' }; -static const symbol s_9[] = { 'a', 'r', 'c' }; -static const symbol s_10[] = { 'g', 'i', 'n' }; -static const symbol s_11[] = { 'g', 'r', 'a', 'f' }; -static const symbol s_12[] = { 'p', 'a', 'i', 't', 'e' }; -static const symbol s_13[] = { 0xC3, 0xB3, 'i', 'd' }; - static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[2] = z->c; - + ((SN_local *)z)->i_pV = z->c; { int ret = in_grouping_U(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 250, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } @@ -261,57 +253,67 @@ static int r_mark_regions(struct SN_env * z) { static int r_initial_morph(struct SN_env * z) { int among_var; z->bra = z->c; - among_var = find_among(z, a_0, 24); + among_var = find_among(z, a_0, 24, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; @@ -320,37 +322,41 @@ static int r_initial_morph(struct SN_env * z) { } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_noun_sfx(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_1, 16); + among_var = find_among_b(z, a_1, 16, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -361,40 +367,47 @@ static int r_noun_sfx(struct SN_env * z) { static int r_deriv(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_2, 25); + among_var = find_among_b(z, a_2, 25, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_9); + { + int ret = slice_from_s(z, 3, s_9); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_10); + { + int ret = slice_from_s(z, 3, s_10); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_11); + { + int ret = slice_from_s(z, 4, s_11); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 5, s_12); + { + int ret = slice_from_s(z, 5, s_12); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 4, s_13); + { + int ret = slice_from_s(z, 4, s_13); if (ret < 0) return ret; } break; @@ -406,23 +419,27 @@ static int r_verb_sfx(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((282896 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_3, 12); + among_var = find_among_b(z, a_3, 12, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -431,41 +448,58 @@ static int r_verb_sfx(struct SN_env * z) { } extern int irish_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_initial_morph(z); + { + int v_1 = z->c; + { + int ret = r_initial_morph(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_noun_sfx(z); + { + int v_2 = z->l - z->c; + { + int ret = r_noun_sfx(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_deriv(z); + { + int v_3 = z->l - z->c; + { + int ret = r_deriv(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_verb_sfx(z); + { + int v_4 = z->l - z->c; + { + int ret = r_verb_sfx(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; return 1; } -extern struct SN_env * irish_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * irish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void irish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void irish_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_italian.c b/src/backend/snowball/libstemmer/stem_UTF_8_italian.c index cd4db27c84d3c..7d0312b45896d 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_italian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_italian.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from italian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_italian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int italian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_vowel_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_standard_suffix(struct SN_env * z); @@ -19,45 +33,49 @@ static int r_RV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -static int r_exceptions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * italian_UTF_8_create_env(void); -extern void italian_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xC3, 0xA0 }; +static const symbol s_1[] = { 0xC3, 0xA8 }; +static const symbol s_2[] = { 0xC3, 0xAC }; +static const symbol s_3[] = { 0xC3, 0xB2 }; +static const symbol s_4[] = { 0xC3, 0xB9 }; +static const symbol s_5[] = { 'q', 'U' }; +static const symbol s_6[] = { 'U' }; +static const symbol s_7[] = { 'I' }; +static const symbol s_8[] = { 'd', 'i', 'v', 'a', 'n' }; +static const symbol s_9[] = { 'i' }; +static const symbol s_10[] = { 'u' }; +static const symbol s_11[] = { 'e' }; +static const symbol s_12[] = { 'i', 'c' }; +static const symbol s_13[] = { 'l', 'o', 'g' }; +static const symbol s_14[] = { 'u' }; +static const symbol s_15[] = { 'e', 'n', 't', 'e' }; +static const symbol s_16[] = { 'a', 't' }; +static const symbol s_17[] = { 'a', 't' }; +static const symbol s_18[] = { 'i', 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 'q', 'u' }; static const symbol s_0_2[2] = { 0xC3, 0xA1 }; static const symbol s_0_3[2] = { 0xC3, 0xA9 }; static const symbol s_0_4[2] = { 0xC3, 0xAD }; static const symbol s_0_5[2] = { 0xC3, 0xB3 }; static const symbol s_0_6[2] = { 0xC3, 0xBA }; - -static const struct among a_0[7] = -{ -{ 0, 0, -1, 7, 0}, -{ 2, s_0_1, 0, 6, 0}, -{ 2, s_0_2, 0, 1, 0}, -{ 2, s_0_3, 0, 2, 0}, -{ 2, s_0_4, 0, 3, 0}, -{ 2, s_0_5, 0, 4, 0}, -{ 2, s_0_6, 0, 5, 0} +static const struct among a_0[7] = { +{ 0, 0, 0, 7, 0}, +{ 2, s_0_1, -1, 6, 0}, +{ 2, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, 2, 0}, +{ 2, s_0_4, -4, 3, 0}, +{ 2, s_0_5, -5, 4, 0}, +{ 2, s_0_6, -6, 5, 0} }; static const symbol s_1_1[1] = { 'I' }; static const symbol s_1_2[1] = { 'U' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 1, s_1_1, 0, 1, 0}, -{ 1, s_1_2, 0, 2, 0} +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 1, s_1_1, -1, 1, 0}, +{ 1, s_1_2, -2, 2, 0} }; static const symbol s_2_0[2] = { 'l', 'a' }; @@ -97,46 +115,44 @@ static const symbol s_2_33[6] = { 'g', 'l', 'i', 'e', 'l', 'o' }; static const symbol s_2_34[4] = { 'm', 'e', 'l', 'o' }; static const symbol s_2_35[4] = { 't', 'e', 'l', 'o' }; static const symbol s_2_36[4] = { 'v', 'e', 'l', 'o' }; - -static const struct among a_2[37] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 4, s_2_1, 0, -1, 0}, -{ 6, s_2_2, 0, -1, 0}, -{ 4, s_2_3, 0, -1, 0}, -{ 4, s_2_4, 0, -1, 0}, -{ 4, s_2_5, 0, -1, 0}, -{ 2, s_2_6, -1, -1, 0}, -{ 4, s_2_7, 6, -1, 0}, -{ 6, s_2_8, 6, -1, 0}, -{ 4, s_2_9, 6, -1, 0}, -{ 4, s_2_10, 6, -1, 0}, -{ 4, s_2_11, 6, -1, 0}, -{ 2, s_2_12, -1, -1, 0}, -{ 4, s_2_13, 12, -1, 0}, -{ 6, s_2_14, 12, -1, 0}, -{ 4, s_2_15, 12, -1, 0}, -{ 4, s_2_16, 12, -1, 0}, -{ 4, s_2_17, 12, -1, 0}, -{ 4, s_2_18, 12, -1, 0}, -{ 2, s_2_19, -1, -1, 0}, -{ 2, s_2_20, -1, -1, 0}, -{ 4, s_2_21, 20, -1, 0}, -{ 6, s_2_22, 20, -1, 0}, -{ 4, s_2_23, 20, -1, 0}, -{ 4, s_2_24, 20, -1, 0}, -{ 4, s_2_25, 20, -1, 0}, -{ 3, s_2_26, 20, -1, 0}, -{ 2, s_2_27, -1, -1, 0}, -{ 2, s_2_28, -1, -1, 0}, -{ 2, s_2_29, -1, -1, 0}, -{ 2, s_2_30, -1, -1, 0}, -{ 2, s_2_31, -1, -1, 0}, -{ 4, s_2_32, 31, -1, 0}, -{ 6, s_2_33, 31, -1, 0}, -{ 4, s_2_34, 31, -1, 0}, -{ 4, s_2_35, 31, -1, 0}, -{ 4, s_2_36, 31, -1, 0} +static const struct among a_2[37] = { +{ 2, s_2_0, 0, -1, 0}, +{ 4, s_2_1, -1, -1, 0}, +{ 6, s_2_2, -2, -1, 0}, +{ 4, s_2_3, -3, -1, 0}, +{ 4, s_2_4, -4, -1, 0}, +{ 4, s_2_5, -5, -1, 0}, +{ 2, s_2_6, 0, -1, 0}, +{ 4, s_2_7, -1, -1, 0}, +{ 6, s_2_8, -2, -1, 0}, +{ 4, s_2_9, -3, -1, 0}, +{ 4, s_2_10, -4, -1, 0}, +{ 4, s_2_11, -5, -1, 0}, +{ 2, s_2_12, 0, -1, 0}, +{ 4, s_2_13, -1, -1, 0}, +{ 6, s_2_14, -2, -1, 0}, +{ 4, s_2_15, -3, -1, 0}, +{ 4, s_2_16, -4, -1, 0}, +{ 4, s_2_17, -5, -1, 0}, +{ 4, s_2_18, -6, -1, 0}, +{ 2, s_2_19, 0, -1, 0}, +{ 2, s_2_20, 0, -1, 0}, +{ 4, s_2_21, -1, -1, 0}, +{ 6, s_2_22, -2, -1, 0}, +{ 4, s_2_23, -3, -1, 0}, +{ 4, s_2_24, -4, -1, 0}, +{ 4, s_2_25, -5, -1, 0}, +{ 3, s_2_26, -6, -1, 0}, +{ 2, s_2_27, 0, -1, 0}, +{ 2, s_2_28, 0, -1, 0}, +{ 2, s_2_29, 0, -1, 0}, +{ 2, s_2_30, 0, -1, 0}, +{ 2, s_2_31, 0, -1, 0}, +{ 4, s_2_32, -1, -1, 0}, +{ 6, s_2_33, -2, -1, 0}, +{ 4, s_2_34, -3, -1, 0}, +{ 4, s_2_35, -4, -1, 0}, +{ 4, s_2_36, -5, -1, 0} }; static const symbol s_3_0[4] = { 'a', 'n', 'd', 'o' }; @@ -144,38 +160,32 @@ static const symbol s_3_1[4] = { 'e', 'n', 'd', 'o' }; static const symbol s_3_2[2] = { 'a', 'r' }; static const symbol s_3_3[2] = { 'e', 'r' }; static const symbol s_3_4[2] = { 'i', 'r' }; - -static const struct among a_3[5] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 2, s_3_2, -1, 2, 0}, -{ 2, s_3_3, -1, 2, 0}, -{ 2, s_3_4, -1, 2, 0} +static const struct among a_3[5] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 2, s_3_2, 0, 2, 0}, +{ 2, s_3_3, 0, 2, 0}, +{ 2, s_3_4, 0, 2, 0} }; static const symbol s_4_0[2] = { 'i', 'c' }; static const symbol s_4_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_4_2[2] = { 'o', 's' }; static const symbol s_4_3[2] = { 'i', 'v' }; - -static const struct among a_4[4] = -{ -{ 2, s_4_0, -1, -1, 0}, -{ 4, s_4_1, -1, -1, 0}, -{ 2, s_4_2, -1, -1, 0}, -{ 2, s_4_3, -1, 1, 0} +static const struct among a_4[4] = { +{ 2, s_4_0, 0, -1, 0}, +{ 4, s_4_1, 0, -1, 0}, +{ 2, s_4_2, 0, -1, 0}, +{ 2, s_4_3, 0, 1, 0} }; static const symbol s_5_0[2] = { 'i', 'c' }; static const symbol s_5_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_5_2[2] = { 'i', 'v' }; - -static const struct among a_5[3] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 2, s_5_2, -1, 1, 0} +static const struct among a_5[3] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 2, s_5_2, 0, 1, 0} }; static const symbol s_6_0[3] = { 'i', 'c', 'a' }; @@ -229,60 +239,58 @@ static const symbol s_6_47[4] = { 'i', 't', 0xC3, 0xA0 }; static const symbol s_6_48[5] = { 'i', 's', 't', 0xC3, 0xA0 }; static const symbol s_6_49[5] = { 'i', 's', 't', 0xC3, 0xA8 }; static const symbol s_6_50[5] = { 'i', 's', 't', 0xC3, 0xAC }; - -static const struct among a_6[51] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 5, s_6_1, -1, 3, 0}, -{ 3, s_6_2, -1, 1, 0}, -{ 4, s_6_3, -1, 1, 0}, -{ 3, s_6_4, -1, 9, 0}, -{ 4, s_6_5, -1, 1, 0}, -{ 4, s_6_6, -1, 5, 0}, -{ 3, s_6_7, -1, 1, 0}, -{ 6, s_6_8, 7, 1, 0}, -{ 4, s_6_9, -1, 1, 0}, -{ 5, s_6_10, -1, 3, 0}, -{ 5, s_6_11, -1, 1, 0}, -{ 5, s_6_12, -1, 1, 0}, -{ 6, s_6_13, -1, 4, 0}, -{ 6, s_6_14, -1, 2, 0}, -{ 6, s_6_15, -1, 4, 0}, -{ 5, s_6_16, -1, 2, 0}, -{ 3, s_6_17, -1, 1, 0}, -{ 4, s_6_18, -1, 1, 0}, -{ 5, s_6_19, -1, 1, 0}, -{ 6, s_6_20, 19, 7, 0}, -{ 4, s_6_21, -1, 1, 0}, -{ 3, s_6_22, -1, 9, 0}, -{ 4, s_6_23, -1, 1, 0}, -{ 4, s_6_24, -1, 5, 0}, -{ 3, s_6_25, -1, 1, 0}, -{ 6, s_6_26, 25, 1, 0}, -{ 4, s_6_27, -1, 1, 0}, -{ 5, s_6_28, -1, 1, 0}, -{ 5, s_6_29, -1, 1, 0}, -{ 4, s_6_30, -1, 1, 0}, -{ 6, s_6_31, -1, 4, 0}, -{ 6, s_6_32, -1, 2, 0}, -{ 6, s_6_33, -1, 4, 0}, -{ 5, s_6_34, -1, 2, 0}, -{ 3, s_6_35, -1, 1, 0}, -{ 4, s_6_36, -1, 1, 0}, -{ 6, s_6_37, -1, 6, 0}, -{ 6, s_6_38, -1, 6, 0}, -{ 4, s_6_39, -1, 1, 0}, -{ 3, s_6_40, -1, 9, 0}, -{ 3, s_6_41, -1, 1, 0}, -{ 4, s_6_42, -1, 1, 0}, -{ 3, s_6_43, -1, 1, 0}, -{ 6, s_6_44, -1, 6, 0}, -{ 6, s_6_45, -1, 6, 0}, -{ 3, s_6_46, -1, 9, 0}, -{ 4, s_6_47, -1, 8, 0}, -{ 5, s_6_48, -1, 1, 0}, -{ 5, s_6_49, -1, 1, 0}, -{ 5, s_6_50, -1, 1, 0} +static const struct among a_6[51] = { +{ 3, s_6_0, 0, 1, 0}, +{ 5, s_6_1, 0, 3, 0}, +{ 3, s_6_2, 0, 1, 0}, +{ 4, s_6_3, 0, 1, 0}, +{ 3, s_6_4, 0, 9, 0}, +{ 4, s_6_5, 0, 1, 0}, +{ 4, s_6_6, 0, 5, 0}, +{ 3, s_6_7, 0, 1, 0}, +{ 6, s_6_8, -1, 1, 0}, +{ 4, s_6_9, 0, 1, 0}, +{ 5, s_6_10, 0, 3, 0}, +{ 5, s_6_11, 0, 1, 0}, +{ 5, s_6_12, 0, 1, 0}, +{ 6, s_6_13, 0, 4, 0}, +{ 6, s_6_14, 0, 2, 0}, +{ 6, s_6_15, 0, 4, 0}, +{ 5, s_6_16, 0, 2, 0}, +{ 3, s_6_17, 0, 1, 0}, +{ 4, s_6_18, 0, 1, 0}, +{ 5, s_6_19, 0, 1, 0}, +{ 6, s_6_20, -1, 7, 0}, +{ 4, s_6_21, 0, 1, 0}, +{ 3, s_6_22, 0, 9, 0}, +{ 4, s_6_23, 0, 1, 0}, +{ 4, s_6_24, 0, 5, 0}, +{ 3, s_6_25, 0, 1, 0}, +{ 6, s_6_26, -1, 1, 0}, +{ 4, s_6_27, 0, 1, 0}, +{ 5, s_6_28, 0, 1, 0}, +{ 5, s_6_29, 0, 1, 0}, +{ 4, s_6_30, 0, 1, 0}, +{ 6, s_6_31, 0, 4, 0}, +{ 6, s_6_32, 0, 2, 0}, +{ 6, s_6_33, 0, 4, 0}, +{ 5, s_6_34, 0, 2, 0}, +{ 3, s_6_35, 0, 1, 0}, +{ 4, s_6_36, 0, 1, 0}, +{ 6, s_6_37, 0, 6, 0}, +{ 6, s_6_38, 0, 6, 0}, +{ 4, s_6_39, 0, 1, 0}, +{ 3, s_6_40, 0, 9, 0}, +{ 3, s_6_41, 0, 1, 0}, +{ 4, s_6_42, 0, 1, 0}, +{ 3, s_6_43, 0, 1, 0}, +{ 6, s_6_44, 0, 6, 0}, +{ 6, s_6_45, 0, 6, 0}, +{ 3, s_6_46, 0, 9, 0}, +{ 4, s_6_47, 0, 8, 0}, +{ 5, s_6_48, 0, 1, 0}, +{ 5, s_6_49, 0, 1, 0}, +{ 5, s_6_50, 0, 1, 0} }; static const symbol s_7_0[4] = { 'i', 's', 'c', 'a' }; @@ -372,96 +380,94 @@ static const symbol s_7_83[4] = { 'e', 'r', 0xC3, 0xA0 }; static const symbol s_7_84[4] = { 'i', 'r', 0xC3, 0xA0 }; static const symbol s_7_85[4] = { 'e', 'r', 0xC3, 0xB2 }; static const symbol s_7_86[4] = { 'i', 'r', 0xC3, 0xB2 }; - -static const struct among a_7[87] = -{ -{ 4, s_7_0, -1, 1, 0}, -{ 4, s_7_1, -1, 1, 0}, -{ 3, s_7_2, -1, 1, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 3, s_7_4, -1, 1, 0}, -{ 3, s_7_5, -1, 1, 0}, -{ 3, s_7_6, -1, 1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 6, s_7_8, -1, 1, 0}, -{ 6, s_7_9, -1, 1, 0}, -{ 4, s_7_10, -1, 1, 0}, -{ 4, s_7_11, -1, 1, 0}, -{ 3, s_7_12, -1, 1, 0}, -{ 3, s_7_13, -1, 1, 0}, -{ 3, s_7_14, -1, 1, 0}, -{ 4, s_7_15, -1, 1, 0}, -{ 3, s_7_16, -1, 1, 0}, -{ 5, s_7_17, 16, 1, 0}, -{ 5, s_7_18, 16, 1, 0}, -{ 5, s_7_19, 16, 1, 0}, -{ 3, s_7_20, -1, 1, 0}, -{ 5, s_7_21, 20, 1, 0}, -{ 5, s_7_22, 20, 1, 0}, -{ 3, s_7_23, -1, 1, 0}, -{ 6, s_7_24, -1, 1, 0}, -{ 6, s_7_25, -1, 1, 0}, -{ 3, s_7_26, -1, 1, 0}, -{ 4, s_7_27, -1, 1, 0}, -{ 4, s_7_28, -1, 1, 0}, -{ 4, s_7_29, -1, 1, 0}, -{ 4, s_7_30, -1, 1, 0}, -{ 4, s_7_31, -1, 1, 0}, -{ 4, s_7_32, -1, 1, 0}, -{ 4, s_7_33, -1, 1, 0}, -{ 3, s_7_34, -1, 1, 0}, -{ 3, s_7_35, -1, 1, 0}, -{ 6, s_7_36, -1, 1, 0}, -{ 6, s_7_37, -1, 1, 0}, -{ 3, s_7_38, -1, 1, 0}, -{ 3, s_7_39, -1, 1, 0}, -{ 3, s_7_40, -1, 1, 0}, -{ 3, s_7_41, -1, 1, 0}, -{ 4, s_7_42, -1, 1, 0}, -{ 4, s_7_43, -1, 1, 0}, -{ 4, s_7_44, -1, 1, 0}, -{ 4, s_7_45, -1, 1, 0}, -{ 4, s_7_46, -1, 1, 0}, -{ 5, s_7_47, -1, 1, 0}, -{ 5, s_7_48, -1, 1, 0}, -{ 5, s_7_49, -1, 1, 0}, -{ 5, s_7_50, -1, 1, 0}, -{ 5, s_7_51, -1, 1, 0}, -{ 6, s_7_52, -1, 1, 0}, -{ 4, s_7_53, -1, 1, 0}, -{ 4, s_7_54, -1, 1, 0}, -{ 6, s_7_55, 54, 1, 0}, -{ 6, s_7_56, 54, 1, 0}, -{ 4, s_7_57, -1, 1, 0}, -{ 3, s_7_58, -1, 1, 0}, -{ 6, s_7_59, 58, 1, 0}, -{ 5, s_7_60, 58, 1, 0}, -{ 5, s_7_61, 58, 1, 0}, -{ 5, s_7_62, 58, 1, 0}, -{ 6, s_7_63, -1, 1, 0}, -{ 6, s_7_64, -1, 1, 0}, -{ 3, s_7_65, -1, 1, 0}, -{ 6, s_7_66, 65, 1, 0}, -{ 5, s_7_67, 65, 1, 0}, -{ 5, s_7_68, 65, 1, 0}, -{ 5, s_7_69, 65, 1, 0}, -{ 8, s_7_70, -1, 1, 0}, -{ 8, s_7_71, -1, 1, 0}, -{ 6, s_7_72, -1, 1, 0}, -{ 6, s_7_73, -1, 1, 0}, -{ 6, s_7_74, -1, 1, 0}, -{ 3, s_7_75, -1, 1, 0}, -{ 3, s_7_76, -1, 1, 0}, -{ 3, s_7_77, -1, 1, 0}, -{ 3, s_7_78, -1, 1, 0}, -{ 3, s_7_79, -1, 1, 0}, -{ 3, s_7_80, -1, 1, 0}, -{ 2, s_7_81, -1, 1, 0}, -{ 2, s_7_82, -1, 1, 0}, -{ 4, s_7_83, -1, 1, 0}, -{ 4, s_7_84, -1, 1, 0}, -{ 4, s_7_85, -1, 1, 0}, -{ 4, s_7_86, -1, 1, 0} +static const struct among a_7[87] = { +{ 4, s_7_0, 0, 1, 0}, +{ 4, s_7_1, 0, 1, 0}, +{ 3, s_7_2, 0, 1, 0}, +{ 3, s_7_3, 0, 1, 0}, +{ 3, s_7_4, 0, 1, 0}, +{ 3, s_7_5, 0, 1, 0}, +{ 3, s_7_6, 0, 1, 0}, +{ 3, s_7_7, 0, 1, 0}, +{ 6, s_7_8, 0, 1, 0}, +{ 6, s_7_9, 0, 1, 0}, +{ 4, s_7_10, 0, 1, 0}, +{ 4, s_7_11, 0, 1, 0}, +{ 3, s_7_12, 0, 1, 0}, +{ 3, s_7_13, 0, 1, 0}, +{ 3, s_7_14, 0, 1, 0}, +{ 4, s_7_15, 0, 1, 0}, +{ 3, s_7_16, 0, 1, 0}, +{ 5, s_7_17, -1, 1, 0}, +{ 5, s_7_18, -2, 1, 0}, +{ 5, s_7_19, -3, 1, 0}, +{ 3, s_7_20, 0, 1, 0}, +{ 5, s_7_21, -1, 1, 0}, +{ 5, s_7_22, -2, 1, 0}, +{ 3, s_7_23, 0, 1, 0}, +{ 6, s_7_24, 0, 1, 0}, +{ 6, s_7_25, 0, 1, 0}, +{ 3, s_7_26, 0, 1, 0}, +{ 4, s_7_27, 0, 1, 0}, +{ 4, s_7_28, 0, 1, 0}, +{ 4, s_7_29, 0, 1, 0}, +{ 4, s_7_30, 0, 1, 0}, +{ 4, s_7_31, 0, 1, 0}, +{ 4, s_7_32, 0, 1, 0}, +{ 4, s_7_33, 0, 1, 0}, +{ 3, s_7_34, 0, 1, 0}, +{ 3, s_7_35, 0, 1, 0}, +{ 6, s_7_36, 0, 1, 0}, +{ 6, s_7_37, 0, 1, 0}, +{ 3, s_7_38, 0, 1, 0}, +{ 3, s_7_39, 0, 1, 0}, +{ 3, s_7_40, 0, 1, 0}, +{ 3, s_7_41, 0, 1, 0}, +{ 4, s_7_42, 0, 1, 0}, +{ 4, s_7_43, 0, 1, 0}, +{ 4, s_7_44, 0, 1, 0}, +{ 4, s_7_45, 0, 1, 0}, +{ 4, s_7_46, 0, 1, 0}, +{ 5, s_7_47, 0, 1, 0}, +{ 5, s_7_48, 0, 1, 0}, +{ 5, s_7_49, 0, 1, 0}, +{ 5, s_7_50, 0, 1, 0}, +{ 5, s_7_51, 0, 1, 0}, +{ 6, s_7_52, 0, 1, 0}, +{ 4, s_7_53, 0, 1, 0}, +{ 4, s_7_54, 0, 1, 0}, +{ 6, s_7_55, -1, 1, 0}, +{ 6, s_7_56, -2, 1, 0}, +{ 4, s_7_57, 0, 1, 0}, +{ 3, s_7_58, 0, 1, 0}, +{ 6, s_7_59, -1, 1, 0}, +{ 5, s_7_60, -2, 1, 0}, +{ 5, s_7_61, -3, 1, 0}, +{ 5, s_7_62, -4, 1, 0}, +{ 6, s_7_63, 0, 1, 0}, +{ 6, s_7_64, 0, 1, 0}, +{ 3, s_7_65, 0, 1, 0}, +{ 6, s_7_66, -1, 1, 0}, +{ 5, s_7_67, -2, 1, 0}, +{ 5, s_7_68, -3, 1, 0}, +{ 5, s_7_69, -4, 1, 0}, +{ 8, s_7_70, 0, 1, 0}, +{ 8, s_7_71, 0, 1, 0}, +{ 6, s_7_72, 0, 1, 0}, +{ 6, s_7_73, 0, 1, 0}, +{ 6, s_7_74, 0, 1, 0}, +{ 3, s_7_75, 0, 1, 0}, +{ 3, s_7_76, 0, 1, 0}, +{ 3, s_7_77, 0, 1, 0}, +{ 3, s_7_78, 0, 1, 0}, +{ 3, s_7_79, 0, 1, 0}, +{ 3, s_7_80, 0, 1, 0}, +{ 2, s_7_81, 0, 1, 0}, +{ 2, s_7_82, 0, 1, 0}, +{ 4, s_7_83, 0, 1, 0}, +{ 4, s_7_84, 0, 1, 0}, +{ 4, s_7_85, 0, 1, 0}, +{ 4, s_7_86, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 8, 2, 1 }; @@ -470,68 +476,55 @@ static const unsigned char g_AEIO[] = { 17, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, static const unsigned char g_CG[] = { 17 }; -static const symbol s_0[] = { 0xC3, 0xA0 }; -static const symbol s_1[] = { 0xC3, 0xA8 }; -static const symbol s_2[] = { 0xC3, 0xAC }; -static const symbol s_3[] = { 0xC3, 0xB2 }; -static const symbol s_4[] = { 0xC3, 0xB9 }; -static const symbol s_5[] = { 'q', 'U' }; -static const symbol s_6[] = { 'U' }; -static const symbol s_7[] = { 'I' }; -static const symbol s_8[] = { 'i' }; -static const symbol s_9[] = { 'u' }; -static const symbol s_10[] = { 'e' }; -static const symbol s_11[] = { 'i', 'c' }; -static const symbol s_12[] = { 'l', 'o', 'g' }; -static const symbol s_13[] = { 'u' }; -static const symbol s_14[] = { 'e', 'n', 't', 'e' }; -static const symbol s_15[] = { 'a', 't' }; -static const symbol s_16[] = { 'a', 't' }; -static const symbol s_17[] = { 'i', 'c' }; -static const symbol s_18[] = { 'd', 'i', 'v', 'a', 'n', 'o' }; -static const symbol s_19[] = { 'd', 'i', 'v', 'a', 'n' }; - static int r_prelude(struct SN_env * z) { int among_var; - { int c_test1 = z->c; - while(1) { - int c2 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 7); + among_var = find_among(z, a_0, 7, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_1); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_3); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 2, s_4); + { + int ret = slice_from_s(z, 2, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } break; case 7: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -539,164 +532,169 @@ static int r_prelude(struct SN_env * z) { } continue; lab0: - z->c = c2; + z->c = v_2; break; } - z->c = c_test1; + z->c = v_1; } - while(1) { - int c3 = z->c; - while(1) { - int c4 = z->c; + while (1) { + int v_3 = z->c; + while (1) { + int v_4 = z->c; if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab2; z->bra = z->c; - { int c5 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab4; + do { + int v_5 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab3; z->c++; z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab4; - { int ret = slice_from_s(z, 1, s_6); + if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab3; + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } - goto lab3; - lab4: - z->c = c5; + break; + lab3: + z->c = v_5; if (z->c == z->l || z->p[z->c] != 'i') goto lab2; z->c++; z->ket = z->c; if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab2; - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } - } - lab3: - z->c = c4; + } while (0); + z->c = v_4; break; lab2: - z->c = c4; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_4; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } } continue; lab1: - z->c = c3; + z->c = v_3; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping_U(z, g_v, 97, 249, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping_U(z, g_v, 97, 249, 0)) goto lab2; { int ret = out_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab1; { int ret = in_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } + } while (0); + break; + lab1: + z->c = v_2; + if (!(eq_s(z, 5, s_8))) goto lab3; + break; lab3: - goto lab1; - lab2: - z->c = c2; + z->c = v_2; if (out_grouping_U(z, g_v, 97, 249, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping_U(z, g_v, 97, 249, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping_U(z, g_v, 97, 249, 0)) goto lab4; { int ret = out_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab4; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab4: + z->c = v_4; if (in_grouping_U(z, g_v, 97, 249, 0)) goto lab0; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 249, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab5; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab5: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 85)) among_var = 3; else - among_var = find_among(z, a_1, 3); + among_var = find_among(z, a_1, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 3: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -704,44 +702,47 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_attached_pronoun(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((33314 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_2, 37)) return 0; + if (!find_among_b(z, a_2, 37, 0)) return 0; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 111 && z->p[z->c - 1] != 114)) return 0; - among_var = find_among_b(z, a_3, 5); + among_var = find_among_b(z, a_3, 5, 0); if (!among_var) return 0; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; @@ -752,34 +753,41 @@ static int r_attached_pronoun(struct SN_env * z) { static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_6, 51); + among_var = find_among_b(z, a_6, 51, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_11))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_12))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -787,67 +795,82 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_12); + { + int ret = slice_from_s(z, 3, s_13); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_13); + { + int ret = slice_from_s(z, 1, s_14); if (ret < 0) return ret; } break; case 5: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 4, s_14); + { + int ret = slice_from_s(z, 4, s_15); if (ret < 0) return ret; } break; case 6: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 7: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4722696 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m2; goto lab1; } - among_var = find_among_b(z, a_4, 4); - if (!among_var) { z->c = z->l - m2; goto lab1; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4722696 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_2; goto lab1; } + among_var = find_among_b(z, a_4, 4, 0); + if (!among_var) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } switch (among_var) { case 1: z->ket = z->c; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m2; goto lab1; } + if (!(eq_s_b(z, 2, s_16))) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -857,22 +880,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m3; goto lab2; } - if (!find_among_b(z, a_5, 3)) { z->c = z->l - m3; goto lab2; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_3; goto lab2; } + if (!find_among_b(z, a_5, 3, 0)) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab2; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -880,31 +908,38 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 9: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_16))) { z->c = z->l - m4; goto lab3; } + if (!(eq_s_b(z, 2, s_17))) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (!(eq_s_b(z, 2, s_17))) { z->c = z->l - m4; goto lab3; } + if (!(eq_s_b(z, 2, s_18))) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab3: @@ -916,58 +951,67 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (!find_among_b(z, a_7, 87)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_7, 87, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_vowel_suffix(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (in_grouping_b_U(z, g_AEIO, 97, 242, 0)) { z->c = z->l - m1; goto lab0; } + if (in_grouping_b_U(z, g_AEIO, 97, 242, 0)) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'i') { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] != 'i') { z->c = z->l - v_1; goto lab0; } z->c--; z->bra = z->c; - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: ; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'h') { z->c = z->l - m2; goto lab1; } + if (z->c <= z->lb || z->p[z->c - 1] != 'h') { z->c = z->l - v_2; goto lab1; } z->c--; z->bra = z->c; - if (in_grouping_b_U(z, g_CG, 99, 103, 0)) { z->c = z->l - m2; goto lab1; } - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + if (in_grouping_b_U(z, g_CG, 99, 103, 0)) { z->c = z->l - v_2; goto lab1; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab1: @@ -976,81 +1020,80 @@ static int r_vowel_suffix(struct SN_env * z) { return 1; } -static int r_exceptions(struct SN_env * z) { - z->bra = z->c; - if (!(eq_s(z, 6, s_18))) return 0; - if (z->c < z->l) return 0; - z->ket = z->c; - { int ret = slice_from_s(z, 5, s_19); - if (ret < 0) return ret; - } - return 1; -} - extern int italian_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_exceptions(z); - if (ret == 0) goto lab1; + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = c1; - { int c2 = z->c; - { int ret = r_prelude(z); - if (ret < 0) return ret; - } - z->c = c2; - } - - { int ret = r_mark_regions(z); + z->c = v_1; + } + { + int ret = r_mark_regions(z); + if (ret < 0) return ret; + } + z->lb = z->c; z->c = z->l; + { + int v_2 = z->l - z->c; + { + int ret = r_attached_pronoun(z); if (ret < 0) return ret; } - z->lb = z->c; z->c = z->l; - - { int m3 = z->l - z->c; (void)m3; - { int ret = r_attached_pronoun(z); + z->c = z->l - v_2; + } + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - z->c = z->l - m3; - } - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab4; - if (ret < 0) return ret; - } - goto lab3; - lab4: - z->c = z->l - m5; - { int ret = r_verb_suffix(z); - if (ret == 0) goto lab2; - if (ret < 0) return ret; - } - } - lab3: - lab2: - z->c = z->l - m4; - } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_vowel_suffix(z); + break; + lab1: + z->c = z->l - v_4; + { + int ret = r_verb_suffix(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - z->c = z->l - m6; + } while (0); + lab0: + z->c = z->l - v_3; + } + { + int v_5 = z->l - z->c; + { + int ret = r_vowel_suffix(z); + if (ret < 0) return ret; } - z->c = z->lb; - { int c7 = z->c; - { int ret = r_postlude(z); - if (ret < 0) return ret; - } - z->c = c7; + z->c = z->l - v_5; + } + z->c = z->lb; + { + int v_6 = z->c; + { + int ret = r_postlude(z); + if (ret < 0) return ret; } + z->c = v_6; } -lab0: return 1; } -extern struct SN_env * italian_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * italian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void italian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void italian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_lithuanian.c b/src/backend/snowball/libstemmer/stem_UTF_8_lithuanian.c index 5dd8b038fd48d..a129c26c0514c 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_lithuanian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_lithuanian.c @@ -1,12 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from lithuanian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_lithuanian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; -static int r_fix_conflicts(struct SN_env * z); -static int r_fix_gd(struct SN_env * z); -static int r_fix_chdz(struct SN_env * z); -static int r_step1(struct SN_env * z); -static int r_step2(struct SN_env * z); #ifdef __cplusplus extern "C" { #endif @@ -14,429 +20,413 @@ extern int lithuanian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif -#ifdef __cplusplus -extern "C" { -#endif - -extern struct SN_env * lithuanian_UTF_8_create_env(void); -extern void lithuanian_UTF_8_close_env(struct SN_env * z); +static int r_fix_conflicts(struct SN_env * z); +static int r_fix_gd(struct SN_env * z); +static int r_fix_chdz(struct SN_env * z); +static int r_step1(struct SN_env * z); +static int r_step2(struct SN_env * z); +static const symbol s_0[] = { 'a', 'i', 't', 0xC4, 0x97 }; +static const symbol s_1[] = { 'u', 'o', 't', 0xC4, 0x97 }; +static const symbol s_2[] = { 0xC4, 0x97, 'j', 'i', 'm', 'a', 's' }; +static const symbol s_3[] = { 'e', 's', 'y', 's' }; +static const symbol s_4[] = { 'a', 's', 'y', 's' }; +static const symbol s_5[] = { 'a', 'v', 'i', 'm', 'a', 's' }; +static const symbol s_6[] = { 'o', 'j', 'i', 'm', 'a', 's' }; +static const symbol s_7[] = { 'o', 'k', 'a', 't', 0xC4, 0x97 }; +static const symbol s_8[] = { 't' }; +static const symbol s_9[] = { 'd' }; +static const symbol s_10[] = { 'g', 'd' }; +static const symbol s_11[] = { 'g' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[1] = { 'a' }; static const symbol s_0_1[2] = { 'i', 'a' }; -static const symbol s_0_2[4] = { 'e', 'r', 'i', 'a' }; -static const symbol s_0_3[4] = { 'o', 's', 'n', 'a' }; -static const symbol s_0_4[5] = { 'i', 'o', 's', 'n', 'a' }; -static const symbol s_0_5[5] = { 'u', 'o', 's', 'n', 'a' }; -static const symbol s_0_6[6] = { 'i', 'u', 'o', 's', 'n', 'a' }; -static const symbol s_0_7[4] = { 'y', 's', 'n', 'a' }; -static const symbol s_0_8[5] = { 0xC4, 0x97, 's', 'n', 'a' }; -static const symbol s_0_9[1] = { 'e' }; -static const symbol s_0_10[2] = { 'i', 'e' }; -static const symbol s_0_11[4] = { 'e', 'n', 'i', 'e' }; -static const symbol s_0_12[4] = { 'e', 'r', 'i', 'e' }; -static const symbol s_0_13[3] = { 'o', 'j', 'e' }; -static const symbol s_0_14[4] = { 'i', 'o', 'j', 'e' }; -static const symbol s_0_15[3] = { 'u', 'j', 'e' }; -static const symbol s_0_16[4] = { 'i', 'u', 'j', 'e' }; -static const symbol s_0_17[3] = { 'y', 'j', 'e' }; -static const symbol s_0_18[5] = { 'e', 'n', 'y', 'j', 'e' }; -static const symbol s_0_19[5] = { 'e', 'r', 'y', 'j', 'e' }; -static const symbol s_0_20[4] = { 0xC4, 0x97, 'j', 'e' }; -static const symbol s_0_21[3] = { 'a', 'm', 'e' }; -static const symbol s_0_22[4] = { 'i', 'a', 'm', 'e' }; -static const symbol s_0_23[4] = { 's', 'i', 'm', 'e' }; -static const symbol s_0_24[3] = { 'o', 'm', 'e' }; -static const symbol s_0_25[4] = { 0xC4, 0x97, 'm', 'e' }; -static const symbol s_0_26[7] = { 't', 'u', 'm', 0xC4, 0x97, 'm', 'e' }; -static const symbol s_0_27[3] = { 'o', 's', 'e' }; -static const symbol s_0_28[4] = { 'i', 'o', 's', 'e' }; -static const symbol s_0_29[4] = { 'u', 'o', 's', 'e' }; -static const symbol s_0_30[5] = { 'i', 'u', 'o', 's', 'e' }; -static const symbol s_0_31[3] = { 'y', 's', 'e' }; -static const symbol s_0_32[5] = { 'e', 'n', 'y', 's', 'e' }; -static const symbol s_0_33[5] = { 'e', 'r', 'y', 's', 'e' }; -static const symbol s_0_34[4] = { 0xC4, 0x97, 's', 'e' }; -static const symbol s_0_35[3] = { 'a', 't', 'e' }; -static const symbol s_0_36[4] = { 'i', 'a', 't', 'e' }; -static const symbol s_0_37[3] = { 'i', 't', 'e' }; -static const symbol s_0_38[4] = { 'k', 'i', 't', 'e' }; -static const symbol s_0_39[4] = { 's', 'i', 't', 'e' }; -static const symbol s_0_40[3] = { 'o', 't', 'e' }; -static const symbol s_0_41[4] = { 't', 'u', 't', 'e' }; -static const symbol s_0_42[4] = { 0xC4, 0x97, 't', 'e' }; -static const symbol s_0_43[7] = { 't', 'u', 'm', 0xC4, 0x97, 't', 'e' }; -static const symbol s_0_44[1] = { 'i' }; -static const symbol s_0_45[2] = { 'a', 'i' }; -static const symbol s_0_46[3] = { 'i', 'a', 'i' }; -static const symbol s_0_47[5] = { 'e', 'r', 'i', 'a', 'i' }; -static const symbol s_0_48[2] = { 'e', 'i' }; -static const symbol s_0_49[5] = { 't', 'u', 'm', 'e', 'i' }; -static const symbol s_0_50[2] = { 'k', 'i' }; -static const symbol s_0_51[3] = { 'i', 'm', 'i' }; -static const symbol s_0_52[5] = { 'e', 'r', 'i', 'm', 'i' }; -static const symbol s_0_53[3] = { 'u', 'm', 'i' }; -static const symbol s_0_54[4] = { 'i', 'u', 'm', 'i' }; -static const symbol s_0_55[2] = { 's', 'i' }; -static const symbol s_0_56[3] = { 'a', 's', 'i' }; -static const symbol s_0_57[4] = { 'i', 'a', 's', 'i' }; -static const symbol s_0_58[3] = { 'e', 's', 'i' }; -static const symbol s_0_59[4] = { 'i', 'e', 's', 'i' }; -static const symbol s_0_60[5] = { 's', 'i', 'e', 's', 'i' }; -static const symbol s_0_61[3] = { 'i', 's', 'i' }; -static const symbol s_0_62[4] = { 'a', 'i', 's', 'i' }; -static const symbol s_0_63[4] = { 'e', 'i', 's', 'i' }; -static const symbol s_0_64[7] = { 't', 'u', 'm', 'e', 'i', 's', 'i' }; -static const symbol s_0_65[4] = { 'u', 'i', 's', 'i' }; -static const symbol s_0_66[3] = { 'o', 's', 'i' }; -static const symbol s_0_67[6] = { 0xC4, 0x97, 'j', 'o', 's', 'i' }; -static const symbol s_0_68[4] = { 'u', 'o', 's', 'i' }; -static const symbol s_0_69[5] = { 'i', 'u', 'o', 's', 'i' }; -static const symbol s_0_70[6] = { 's', 'i', 'u', 'o', 's', 'i' }; -static const symbol s_0_71[3] = { 'u', 's', 'i' }; -static const symbol s_0_72[4] = { 'a', 'u', 's', 'i' }; -static const symbol s_0_73[7] = { 0xC4, 0x8D, 'i', 'a', 'u', 's', 'i' }; -static const symbol s_0_74[4] = { 0xC4, 0x85, 's', 'i' }; -static const symbol s_0_75[4] = { 0xC4, 0x97, 's', 'i' }; -static const symbol s_0_76[4] = { 0xC5, 0xB3, 's', 'i' }; -static const symbol s_0_77[5] = { 't', 0xC5, 0xB3, 's', 'i' }; -static const symbol s_0_78[2] = { 't', 'i' }; -static const symbol s_0_79[4] = { 'e', 'n', 't', 'i' }; -static const symbol s_0_80[4] = { 'i', 'n', 't', 'i' }; -static const symbol s_0_81[3] = { 'o', 't', 'i' }; -static const symbol s_0_82[4] = { 'i', 'o', 't', 'i' }; -static const symbol s_0_83[4] = { 'u', 'o', 't', 'i' }; -static const symbol s_0_84[5] = { 'i', 'u', 'o', 't', 'i' }; -static const symbol s_0_85[4] = { 'a', 'u', 't', 'i' }; -static const symbol s_0_86[5] = { 'i', 'a', 'u', 't', 'i' }; -static const symbol s_0_87[3] = { 'y', 't', 'i' }; -static const symbol s_0_88[4] = { 0xC4, 0x97, 't', 'i' }; -static const symbol s_0_89[7] = { 't', 'e', 'l', 0xC4, 0x97, 't', 'i' }; -static const symbol s_0_90[6] = { 'i', 'n', 0xC4, 0x97, 't', 'i' }; -static const symbol s_0_91[7] = { 't', 'e', 'r', 0xC4, 0x97, 't', 'i' }; -static const symbol s_0_92[2] = { 'u', 'i' }; -static const symbol s_0_93[3] = { 'i', 'u', 'i' }; -static const symbol s_0_94[5] = { 'e', 'n', 'i', 'u', 'i' }; -static const symbol s_0_95[2] = { 'o', 'j' }; -static const symbol s_0_96[3] = { 0xC4, 0x97, 'j' }; -static const symbol s_0_97[1] = { 'k' }; -static const symbol s_0_98[2] = { 'a', 'm' }; -static const symbol s_0_99[3] = { 'i', 'a', 'm' }; -static const symbol s_0_100[3] = { 'i', 'e', 'm' }; -static const symbol s_0_101[2] = { 'i', 'm' }; -static const symbol s_0_102[3] = { 's', 'i', 'm' }; -static const symbol s_0_103[2] = { 'o', 'm' }; -static const symbol s_0_104[3] = { 't', 'u', 'm' }; -static const symbol s_0_105[3] = { 0xC4, 0x97, 'm' }; -static const symbol s_0_106[6] = { 't', 'u', 'm', 0xC4, 0x97, 'm' }; -static const symbol s_0_107[2] = { 'a', 'n' }; -static const symbol s_0_108[2] = { 'o', 'n' }; -static const symbol s_0_109[3] = { 'i', 'o', 'n' }; -static const symbol s_0_110[2] = { 'u', 'n' }; -static const symbol s_0_111[3] = { 'i', 'u', 'n' }; -static const symbol s_0_112[3] = { 0xC4, 0x97, 'n' }; -static const symbol s_0_113[1] = { 'o' }; -static const symbol s_0_114[2] = { 'i', 'o' }; -static const symbol s_0_115[4] = { 'e', 'n', 'i', 'o' }; -static const symbol s_0_116[4] = { 0xC4, 0x97, 'j', 'o' }; -static const symbol s_0_117[2] = { 'u', 'o' }; -static const symbol s_0_118[1] = { 's' }; -static const symbol s_0_119[2] = { 'a', 's' }; -static const symbol s_0_120[3] = { 'i', 'a', 's' }; -static const symbol s_0_121[2] = { 'e', 's' }; -static const symbol s_0_122[3] = { 'i', 'e', 's' }; -static const symbol s_0_123[2] = { 'i', 's' }; -static const symbol s_0_124[3] = { 'a', 'i', 's' }; -static const symbol s_0_125[4] = { 'i', 'a', 'i', 's' }; -static const symbol s_0_126[6] = { 't', 'u', 'm', 'e', 'i', 's' }; -static const symbol s_0_127[4] = { 'i', 'm', 'i', 's' }; -static const symbol s_0_128[6] = { 'e', 'n', 'i', 'm', 'i', 's' }; -static const symbol s_0_129[4] = { 'o', 'm', 'i', 's' }; -static const symbol s_0_130[5] = { 'i', 'o', 'm', 'i', 's' }; -static const symbol s_0_131[4] = { 'u', 'm', 'i', 's' }; -static const symbol s_0_132[5] = { 0xC4, 0x97, 'm', 'i', 's' }; -static const symbol s_0_133[4] = { 'e', 'n', 'i', 's' }; -static const symbol s_0_134[4] = { 'a', 's', 'i', 's' }; -static const symbol s_0_135[4] = { 'y', 's', 'i', 's' }; -static const symbol s_0_136[3] = { 'a', 'm', 's' }; -static const symbol s_0_137[4] = { 'i', 'a', 'm', 's' }; -static const symbol s_0_138[4] = { 'i', 'e', 'm', 's' }; -static const symbol s_0_139[3] = { 'i', 'm', 's' }; -static const symbol s_0_140[5] = { 'e', 'n', 'i', 'm', 's' }; -static const symbol s_0_141[5] = { 'e', 'r', 'i', 'm', 's' }; -static const symbol s_0_142[3] = { 'o', 'm', 's' }; -static const symbol s_0_143[4] = { 'i', 'o', 'm', 's' }; -static const symbol s_0_144[3] = { 'u', 'm', 's' }; -static const symbol s_0_145[4] = { 0xC4, 0x97, 'm', 's' }; -static const symbol s_0_146[3] = { 'e', 'n', 's' }; -static const symbol s_0_147[2] = { 'o', 's' }; -static const symbol s_0_148[3] = { 'i', 'o', 's' }; -static const symbol s_0_149[3] = { 'u', 'o', 's' }; -static const symbol s_0_150[4] = { 'i', 'u', 'o', 's' }; -static const symbol s_0_151[3] = { 'e', 'r', 's' }; -static const symbol s_0_152[2] = { 'u', 's' }; -static const symbol s_0_153[3] = { 'a', 'u', 's' }; -static const symbol s_0_154[4] = { 'i', 'a', 'u', 's' }; -static const symbol s_0_155[3] = { 'i', 'u', 's' }; -static const symbol s_0_156[2] = { 'y', 's' }; -static const symbol s_0_157[4] = { 'e', 'n', 'y', 's' }; -static const symbol s_0_158[4] = { 'e', 'r', 'y', 's' }; -static const symbol s_0_159[3] = { 0xC4, 0x85, 's' }; -static const symbol s_0_160[4] = { 'i', 0xC4, 0x85, 's' }; -static const symbol s_0_161[3] = { 0xC4, 0x97, 's' }; -static const symbol s_0_162[5] = { 'a', 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_163[6] = { 'i', 'a', 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_164[5] = { 'i', 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_165[6] = { 'k', 'i', 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_166[6] = { 's', 'i', 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_167[5] = { 'o', 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_168[6] = { 0xC4, 0x97, 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_169[9] = { 't', 'u', 'm', 0xC4, 0x97, 'm', 0xC4, 0x97, 's' }; -static const symbol s_0_170[5] = { 'a', 't', 0xC4, 0x97, 's' }; -static const symbol s_0_171[6] = { 'i', 'a', 't', 0xC4, 0x97, 's' }; -static const symbol s_0_172[6] = { 's', 'i', 't', 0xC4, 0x97, 's' }; -static const symbol s_0_173[5] = { 'o', 't', 0xC4, 0x97, 's' }; -static const symbol s_0_174[6] = { 0xC4, 0x97, 't', 0xC4, 0x97, 's' }; -static const symbol s_0_175[9] = { 't', 'u', 'm', 0xC4, 0x97, 't', 0xC4, 0x97, 's' }; -static const symbol s_0_176[3] = { 0xC5, 0xAB, 's' }; -static const symbol s_0_177[3] = { 0xC4, 0xAF, 's' }; -static const symbol s_0_178[4] = { 't', 0xC5, 0xB3, 's' }; -static const symbol s_0_179[2] = { 'a', 't' }; -static const symbol s_0_180[3] = { 'i', 'a', 't' }; -static const symbol s_0_181[2] = { 'i', 't' }; -static const symbol s_0_182[3] = { 's', 'i', 't' }; -static const symbol s_0_183[2] = { 'o', 't' }; -static const symbol s_0_184[3] = { 0xC4, 0x97, 't' }; -static const symbol s_0_185[6] = { 't', 'u', 'm', 0xC4, 0x97, 't' }; -static const symbol s_0_186[1] = { 'u' }; -static const symbol s_0_187[2] = { 'a', 'u' }; -static const symbol s_0_188[3] = { 'i', 'a', 'u' }; -static const symbol s_0_189[5] = { 0xC4, 0x8D, 'i', 'a', 'u' }; -static const symbol s_0_190[2] = { 'i', 'u' }; -static const symbol s_0_191[4] = { 'e', 'n', 'i', 'u' }; -static const symbol s_0_192[3] = { 's', 'i', 'u' }; -static const symbol s_0_193[1] = { 'y' }; -static const symbol s_0_194[2] = { 0xC4, 0x85 }; -static const symbol s_0_195[3] = { 'i', 0xC4, 0x85 }; -static const symbol s_0_196[2] = { 0xC4, 0x97 }; -static const symbol s_0_197[2] = { 0xC4, 0x99 }; -static const symbol s_0_198[2] = { 0xC4, 0xAF }; -static const symbol s_0_199[4] = { 'e', 'n', 0xC4, 0xAF }; -static const symbol s_0_200[4] = { 'e', 'r', 0xC4, 0xAF }; -static const symbol s_0_201[2] = { 0xC5, 0xB3 }; -static const symbol s_0_202[3] = { 'i', 0xC5, 0xB3 }; -static const symbol s_0_203[4] = { 'e', 'r', 0xC5, 0xB3 }; - -static const struct among a_0[204] = -{ -{ 1, s_0_0, -1, -1, 0}, -{ 2, s_0_1, 0, -1, 0}, -{ 4, s_0_2, 1, -1, 0}, -{ 4, s_0_3, 0, -1, 0}, -{ 5, s_0_4, 3, -1, 0}, -{ 5, s_0_5, 3, -1, 0}, -{ 6, s_0_6, 5, -1, 0}, -{ 4, s_0_7, 0, -1, 0}, -{ 5, s_0_8, 0, -1, 0}, -{ 1, s_0_9, -1, -1, 0}, -{ 2, s_0_10, 9, -1, 0}, -{ 4, s_0_11, 10, -1, 0}, -{ 4, s_0_12, 10, -1, 0}, -{ 3, s_0_13, 9, -1, 0}, -{ 4, s_0_14, 13, -1, 0}, -{ 3, s_0_15, 9, -1, 0}, -{ 4, s_0_16, 15, -1, 0}, -{ 3, s_0_17, 9, -1, 0}, -{ 5, s_0_18, 17, -1, 0}, -{ 5, s_0_19, 17, -1, 0}, -{ 4, s_0_20, 9, -1, 0}, -{ 3, s_0_21, 9, -1, 0}, -{ 4, s_0_22, 21, -1, 0}, -{ 4, s_0_23, 9, -1, 0}, -{ 3, s_0_24, 9, -1, 0}, -{ 4, s_0_25, 9, -1, 0}, -{ 7, s_0_26, 25, -1, 0}, -{ 3, s_0_27, 9, -1, 0}, -{ 4, s_0_28, 27, -1, 0}, -{ 4, s_0_29, 27, -1, 0}, -{ 5, s_0_30, 29, -1, 0}, -{ 3, s_0_31, 9, -1, 0}, -{ 5, s_0_32, 31, -1, 0}, -{ 5, s_0_33, 31, -1, 0}, -{ 4, s_0_34, 9, -1, 0}, -{ 3, s_0_35, 9, -1, 0}, -{ 4, s_0_36, 35, -1, 0}, -{ 3, s_0_37, 9, -1, 0}, -{ 4, s_0_38, 37, -1, 0}, -{ 4, s_0_39, 37, -1, 0}, -{ 3, s_0_40, 9, -1, 0}, -{ 4, s_0_41, 9, -1, 0}, -{ 4, s_0_42, 9, -1, 0}, -{ 7, s_0_43, 42, -1, 0}, -{ 1, s_0_44, -1, -1, 0}, -{ 2, s_0_45, 44, -1, 0}, -{ 3, s_0_46, 45, -1, 0}, -{ 5, s_0_47, 46, -1, 0}, -{ 2, s_0_48, 44, -1, 0}, -{ 5, s_0_49, 48, -1, 0}, -{ 2, s_0_50, 44, -1, 0}, -{ 3, s_0_51, 44, -1, 0}, -{ 5, s_0_52, 51, -1, 0}, -{ 3, s_0_53, 44, -1, 0}, -{ 4, s_0_54, 53, -1, 0}, -{ 2, s_0_55, 44, -1, 0}, -{ 3, s_0_56, 55, -1, 0}, -{ 4, s_0_57, 56, -1, 0}, -{ 3, s_0_58, 55, -1, 0}, -{ 4, s_0_59, 58, -1, 0}, -{ 5, s_0_60, 59, -1, 0}, -{ 3, s_0_61, 55, -1, 0}, -{ 4, s_0_62, 61, -1, 0}, -{ 4, s_0_63, 61, -1, 0}, -{ 7, s_0_64, 63, -1, 0}, -{ 4, s_0_65, 61, -1, 0}, -{ 3, s_0_66, 55, -1, 0}, -{ 6, s_0_67, 66, -1, 0}, -{ 4, s_0_68, 66, -1, 0}, -{ 5, s_0_69, 68, -1, 0}, -{ 6, s_0_70, 69, -1, 0}, -{ 3, s_0_71, 55, -1, 0}, -{ 4, s_0_72, 71, -1, 0}, -{ 7, s_0_73, 72, -1, 0}, -{ 4, s_0_74, 55, -1, 0}, -{ 4, s_0_75, 55, -1, 0}, -{ 4, s_0_76, 55, -1, 0}, -{ 5, s_0_77, 76, -1, 0}, -{ 2, s_0_78, 44, -1, 0}, -{ 4, s_0_79, 78, -1, 0}, -{ 4, s_0_80, 78, -1, 0}, -{ 3, s_0_81, 78, -1, 0}, -{ 4, s_0_82, 81, -1, 0}, -{ 4, s_0_83, 81, -1, 0}, -{ 5, s_0_84, 83, -1, 0}, -{ 4, s_0_85, 78, -1, 0}, -{ 5, s_0_86, 85, -1, 0}, -{ 3, s_0_87, 78, -1, 0}, -{ 4, s_0_88, 78, -1, 0}, -{ 7, s_0_89, 88, -1, 0}, -{ 6, s_0_90, 88, -1, 0}, -{ 7, s_0_91, 88, -1, 0}, -{ 2, s_0_92, 44, -1, 0}, -{ 3, s_0_93, 92, -1, 0}, -{ 5, s_0_94, 93, -1, 0}, -{ 2, s_0_95, -1, -1, 0}, +static const symbol s_0_2[4] = { 'o', 's', 'n', 'a' }; +static const symbol s_0_3[5] = { 'i', 'o', 's', 'n', 'a' }; +static const symbol s_0_4[5] = { 'u', 'o', 's', 'n', 'a' }; +static const symbol s_0_5[6] = { 'i', 'u', 'o', 's', 'n', 'a' }; +static const symbol s_0_6[4] = { 'y', 's', 'n', 'a' }; +static const symbol s_0_7[5] = { 0xC4, 0x97, 's', 'n', 'a' }; +static const symbol s_0_8[1] = { 'e' }; +static const symbol s_0_9[2] = { 'i', 'e' }; +static const symbol s_0_10[4] = { 'e', 'n', 'i', 'e' }; +static const symbol s_0_11[3] = { 'o', 'j', 'e' }; +static const symbol s_0_12[4] = { 'i', 'o', 'j', 'e' }; +static const symbol s_0_13[3] = { 'u', 'j', 'e' }; +static const symbol s_0_14[4] = { 'i', 'u', 'j', 'e' }; +static const symbol s_0_15[3] = { 'y', 'j', 'e' }; +static const symbol s_0_16[5] = { 'e', 'n', 'y', 'j', 'e' }; +static const symbol s_0_17[4] = { 0xC4, 0x97, 'j', 'e' }; +static const symbol s_0_18[3] = { 'a', 'm', 'e' }; +static const symbol s_0_19[4] = { 'i', 'a', 'm', 'e' }; +static const symbol s_0_20[4] = { 's', 'i', 'm', 'e' }; +static const symbol s_0_21[3] = { 'o', 'm', 'e' }; +static const symbol s_0_22[4] = { 0xC4, 0x97, 'm', 'e' }; +static const symbol s_0_23[7] = { 't', 'u', 'm', 0xC4, 0x97, 'm', 'e' }; +static const symbol s_0_24[3] = { 'o', 's', 'e' }; +static const symbol s_0_25[4] = { 'i', 'o', 's', 'e' }; +static const symbol s_0_26[4] = { 'u', 'o', 's', 'e' }; +static const symbol s_0_27[5] = { 'i', 'u', 'o', 's', 'e' }; +static const symbol s_0_28[3] = { 'y', 's', 'e' }; +static const symbol s_0_29[5] = { 'e', 'n', 'y', 's', 'e' }; +static const symbol s_0_30[4] = { 0xC4, 0x97, 's', 'e' }; +static const symbol s_0_31[3] = { 'a', 't', 'e' }; +static const symbol s_0_32[4] = { 'i', 'a', 't', 'e' }; +static const symbol s_0_33[3] = { 'i', 't', 'e' }; +static const symbol s_0_34[4] = { 'k', 'i', 't', 'e' }; +static const symbol s_0_35[4] = { 's', 'i', 't', 'e' }; +static const symbol s_0_36[3] = { 'o', 't', 'e' }; +static const symbol s_0_37[4] = { 't', 'u', 't', 'e' }; +static const symbol s_0_38[4] = { 0xC4, 0x97, 't', 'e' }; +static const symbol s_0_39[7] = { 't', 'u', 'm', 0xC4, 0x97, 't', 'e' }; +static const symbol s_0_40[1] = { 'i' }; +static const symbol s_0_41[2] = { 'a', 'i' }; +static const symbol s_0_42[3] = { 'i', 'a', 'i' }; +static const symbol s_0_43[2] = { 'e', 'i' }; +static const symbol s_0_44[5] = { 't', 'u', 'm', 'e', 'i' }; +static const symbol s_0_45[2] = { 'k', 'i' }; +static const symbol s_0_46[3] = { 'i', 'm', 'i' }; +static const symbol s_0_47[3] = { 'u', 'm', 'i' }; +static const symbol s_0_48[4] = { 'i', 'u', 'm', 'i' }; +static const symbol s_0_49[2] = { 's', 'i' }; +static const symbol s_0_50[3] = { 'a', 's', 'i' }; +static const symbol s_0_51[4] = { 'i', 'a', 's', 'i' }; +static const symbol s_0_52[3] = { 'e', 's', 'i' }; +static const symbol s_0_53[4] = { 'i', 'e', 's', 'i' }; +static const symbol s_0_54[5] = { 's', 'i', 'e', 's', 'i' }; +static const symbol s_0_55[3] = { 'i', 's', 'i' }; +static const symbol s_0_56[4] = { 'a', 'i', 's', 'i' }; +static const symbol s_0_57[4] = { 'e', 'i', 's', 'i' }; +static const symbol s_0_58[7] = { 't', 'u', 'm', 'e', 'i', 's', 'i' }; +static const symbol s_0_59[4] = { 'u', 'i', 's', 'i' }; +static const symbol s_0_60[3] = { 'o', 's', 'i' }; +static const symbol s_0_61[6] = { 0xC4, 0x97, 'j', 'o', 's', 'i' }; +static const symbol s_0_62[4] = { 'u', 'o', 's', 'i' }; +static const symbol s_0_63[5] = { 'i', 'u', 'o', 's', 'i' }; +static const symbol s_0_64[6] = { 's', 'i', 'u', 'o', 's', 'i' }; +static const symbol s_0_65[3] = { 'u', 's', 'i' }; +static const symbol s_0_66[4] = { 'a', 'u', 's', 'i' }; +static const symbol s_0_67[7] = { 0xC4, 0x8D, 'i', 'a', 'u', 's', 'i' }; +static const symbol s_0_68[4] = { 0xC4, 0x85, 's', 'i' }; +static const symbol s_0_69[4] = { 0xC4, 0x97, 's', 'i' }; +static const symbol s_0_70[4] = { 0xC5, 0xB3, 's', 'i' }; +static const symbol s_0_71[5] = { 't', 0xC5, 0xB3, 's', 'i' }; +static const symbol s_0_72[2] = { 't', 'i' }; +static const symbol s_0_73[4] = { 'e', 'n', 't', 'i' }; +static const symbol s_0_74[4] = { 'i', 'n', 't', 'i' }; +static const symbol s_0_75[3] = { 'o', 't', 'i' }; +static const symbol s_0_76[4] = { 'i', 'o', 't', 'i' }; +static const symbol s_0_77[4] = { 'u', 'o', 't', 'i' }; +static const symbol s_0_78[5] = { 'i', 'u', 'o', 't', 'i' }; +static const symbol s_0_79[4] = { 'a', 'u', 't', 'i' }; +static const symbol s_0_80[5] = { 'i', 'a', 'u', 't', 'i' }; +static const symbol s_0_81[3] = { 'y', 't', 'i' }; +static const symbol s_0_82[4] = { 0xC4, 0x97, 't', 'i' }; +static const symbol s_0_83[7] = { 't', 'e', 'l', 0xC4, 0x97, 't', 'i' }; +static const symbol s_0_84[6] = { 'i', 'n', 0xC4, 0x97, 't', 'i' }; +static const symbol s_0_85[7] = { 't', 'e', 'r', 0xC4, 0x97, 't', 'i' }; +static const symbol s_0_86[2] = { 'u', 'i' }; +static const symbol s_0_87[3] = { 'i', 'u', 'i' }; +static const symbol s_0_88[5] = { 'e', 'n', 'i', 'u', 'i' }; +static const symbol s_0_89[2] = { 'o', 'j' }; +static const symbol s_0_90[3] = { 0xC4, 0x97, 'j' }; +static const symbol s_0_91[1] = { 'k' }; +static const symbol s_0_92[2] = { 'a', 'm' }; +static const symbol s_0_93[3] = { 'i', 'a', 'm' }; +static const symbol s_0_94[3] = { 'i', 'e', 'm' }; +static const symbol s_0_95[2] = { 'i', 'm' }; +static const symbol s_0_96[3] = { 's', 'i', 'm' }; +static const symbol s_0_97[2] = { 'o', 'm' }; +static const symbol s_0_98[3] = { 't', 'u', 'm' }; +static const symbol s_0_99[3] = { 0xC4, 0x97, 'm' }; +static const symbol s_0_100[6] = { 't', 'u', 'm', 0xC4, 0x97, 'm' }; +static const symbol s_0_101[2] = { 'a', 'n' }; +static const symbol s_0_102[2] = { 'o', 'n' }; +static const symbol s_0_103[3] = { 'i', 'o', 'n' }; +static const symbol s_0_104[2] = { 'u', 'n' }; +static const symbol s_0_105[3] = { 'i', 'u', 'n' }; +static const symbol s_0_106[3] = { 0xC4, 0x97, 'n' }; +static const symbol s_0_107[1] = { 'o' }; +static const symbol s_0_108[2] = { 'i', 'o' }; +static const symbol s_0_109[4] = { 'e', 'n', 'i', 'o' }; +static const symbol s_0_110[4] = { 0xC4, 0x97, 'j', 'o' }; +static const symbol s_0_111[2] = { 'u', 'o' }; +static const symbol s_0_112[1] = { 's' }; +static const symbol s_0_113[2] = { 'a', 's' }; +static const symbol s_0_114[3] = { 'i', 'a', 's' }; +static const symbol s_0_115[2] = { 'e', 's' }; +static const symbol s_0_116[3] = { 'i', 'e', 's' }; +static const symbol s_0_117[2] = { 'i', 's' }; +static const symbol s_0_118[3] = { 'a', 'i', 's' }; +static const symbol s_0_119[4] = { 'i', 'a', 'i', 's' }; +static const symbol s_0_120[6] = { 't', 'u', 'm', 'e', 'i', 's' }; +static const symbol s_0_121[4] = { 'i', 'm', 'i', 's' }; +static const symbol s_0_122[6] = { 'e', 'n', 'i', 'm', 'i', 's' }; +static const symbol s_0_123[4] = { 'o', 'm', 'i', 's' }; +static const symbol s_0_124[5] = { 'i', 'o', 'm', 'i', 's' }; +static const symbol s_0_125[4] = { 'u', 'm', 'i', 's' }; +static const symbol s_0_126[5] = { 0xC4, 0x97, 'm', 'i', 's' }; +static const symbol s_0_127[4] = { 'e', 'n', 'i', 's' }; +static const symbol s_0_128[4] = { 'a', 's', 'i', 's' }; +static const symbol s_0_129[4] = { 'y', 's', 'i', 's' }; +static const symbol s_0_130[3] = { 'a', 'm', 's' }; +static const symbol s_0_131[4] = { 'i', 'a', 'm', 's' }; +static const symbol s_0_132[4] = { 'i', 'e', 'm', 's' }; +static const symbol s_0_133[3] = { 'i', 'm', 's' }; +static const symbol s_0_134[5] = { 'e', 'n', 'i', 'm', 's' }; +static const symbol s_0_135[3] = { 'o', 'm', 's' }; +static const symbol s_0_136[4] = { 'i', 'o', 'm', 's' }; +static const symbol s_0_137[3] = { 'u', 'm', 's' }; +static const symbol s_0_138[4] = { 0xC4, 0x97, 'm', 's' }; +static const symbol s_0_139[3] = { 'e', 'n', 's' }; +static const symbol s_0_140[2] = { 'o', 's' }; +static const symbol s_0_141[3] = { 'i', 'o', 's' }; +static const symbol s_0_142[3] = { 'u', 'o', 's' }; +static const symbol s_0_143[4] = { 'i', 'u', 'o', 's' }; +static const symbol s_0_144[2] = { 'u', 's' }; +static const symbol s_0_145[3] = { 'a', 'u', 's' }; +static const symbol s_0_146[4] = { 'i', 'a', 'u', 's' }; +static const symbol s_0_147[3] = { 'i', 'u', 's' }; +static const symbol s_0_148[2] = { 'y', 's' }; +static const symbol s_0_149[4] = { 'e', 'n', 'y', 's' }; +static const symbol s_0_150[3] = { 0xC4, 0x85, 's' }; +static const symbol s_0_151[4] = { 'i', 0xC4, 0x85, 's' }; +static const symbol s_0_152[3] = { 0xC4, 0x97, 's' }; +static const symbol s_0_153[5] = { 'a', 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_154[6] = { 'i', 'a', 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_155[5] = { 'i', 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_156[6] = { 'k', 'i', 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_157[6] = { 's', 'i', 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_158[5] = { 'o', 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_159[6] = { 0xC4, 0x97, 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_160[9] = { 't', 'u', 'm', 0xC4, 0x97, 'm', 0xC4, 0x97, 's' }; +static const symbol s_0_161[5] = { 'a', 't', 0xC4, 0x97, 's' }; +static const symbol s_0_162[6] = { 'i', 'a', 't', 0xC4, 0x97, 's' }; +static const symbol s_0_163[6] = { 's', 'i', 't', 0xC4, 0x97, 's' }; +static const symbol s_0_164[5] = { 'o', 't', 0xC4, 0x97, 's' }; +static const symbol s_0_165[6] = { 0xC4, 0x97, 't', 0xC4, 0x97, 's' }; +static const symbol s_0_166[9] = { 't', 'u', 'm', 0xC4, 0x97, 't', 0xC4, 0x97, 's' }; +static const symbol s_0_167[3] = { 0xC5, 0xAB, 's' }; +static const symbol s_0_168[3] = { 0xC4, 0xAF, 's' }; +static const symbol s_0_169[4] = { 't', 0xC5, 0xB3, 's' }; +static const symbol s_0_170[2] = { 'a', 't' }; +static const symbol s_0_171[3] = { 'i', 'a', 't' }; +static const symbol s_0_172[2] = { 'i', 't' }; +static const symbol s_0_173[3] = { 's', 'i', 't' }; +static const symbol s_0_174[2] = { 'o', 't' }; +static const symbol s_0_175[3] = { 0xC4, 0x97, 't' }; +static const symbol s_0_176[6] = { 't', 'u', 'm', 0xC4, 0x97, 't' }; +static const symbol s_0_177[1] = { 'u' }; +static const symbol s_0_178[2] = { 'a', 'u' }; +static const symbol s_0_179[3] = { 'i', 'a', 'u' }; +static const symbol s_0_180[5] = { 0xC4, 0x8D, 'i', 'a', 'u' }; +static const symbol s_0_181[2] = { 'i', 'u' }; +static const symbol s_0_182[4] = { 'e', 'n', 'i', 'u' }; +static const symbol s_0_183[3] = { 's', 'i', 'u' }; +static const symbol s_0_184[1] = { 'y' }; +static const symbol s_0_185[2] = { 0xC4, 0x85 }; +static const symbol s_0_186[3] = { 'i', 0xC4, 0x85 }; +static const symbol s_0_187[2] = { 0xC4, 0x97 }; +static const symbol s_0_188[2] = { 0xC4, 0x99 }; +static const symbol s_0_189[2] = { 0xC4, 0xAF }; +static const symbol s_0_190[4] = { 'e', 'n', 0xC4, 0xAF }; +static const symbol s_0_191[2] = { 0xC5, 0xB3 }; +static const symbol s_0_192[3] = { 'i', 0xC5, 0xB3 }; +static const struct among a_0[193] = { +{ 1, s_0_0, 0, -1, 0}, +{ 2, s_0_1, -1, -1, 0}, +{ 4, s_0_2, -2, -1, 0}, +{ 5, s_0_3, -1, -1, 0}, +{ 5, s_0_4, -2, -1, 0}, +{ 6, s_0_5, -1, -1, 0}, +{ 4, s_0_6, -6, -1, 0}, +{ 5, s_0_7, -7, -1, 0}, +{ 1, s_0_8, 0, -1, 0}, +{ 2, s_0_9, -1, -1, 0}, +{ 4, s_0_10, -1, -1, 0}, +{ 3, s_0_11, -3, -1, 0}, +{ 4, s_0_12, -1, -1, 0}, +{ 3, s_0_13, -5, -1, 0}, +{ 4, s_0_14, -1, -1, 0}, +{ 3, s_0_15, -7, -1, 0}, +{ 5, s_0_16, -1, -1, 0}, +{ 4, s_0_17, -9, -1, 0}, +{ 3, s_0_18, -10, -1, 0}, +{ 4, s_0_19, -1, -1, 0}, +{ 4, s_0_20, -12, -1, 0}, +{ 3, s_0_21, -13, -1, 0}, +{ 4, s_0_22, -14, -1, 0}, +{ 7, s_0_23, -1, -1, 0}, +{ 3, s_0_24, -16, -1, 0}, +{ 4, s_0_25, -1, -1, 0}, +{ 4, s_0_26, -2, -1, 0}, +{ 5, s_0_27, -1, -1, 0}, +{ 3, s_0_28, -20, -1, 0}, +{ 5, s_0_29, -1, -1, 0}, +{ 4, s_0_30, -22, -1, 0}, +{ 3, s_0_31, -23, -1, 0}, +{ 4, s_0_32, -1, -1, 0}, +{ 3, s_0_33, -25, -1, 0}, +{ 4, s_0_34, -1, -1, 0}, +{ 4, s_0_35, -2, -1, 0}, +{ 3, s_0_36, -28, -1, 0}, +{ 4, s_0_37, -29, -1, 0}, +{ 4, s_0_38, -30, -1, 0}, +{ 7, s_0_39, -1, -1, 0}, +{ 1, s_0_40, 0, -1, 0}, +{ 2, s_0_41, -1, -1, 0}, +{ 3, s_0_42, -1, -1, 0}, +{ 2, s_0_43, -3, -1, 0}, +{ 5, s_0_44, -1, -1, 0}, +{ 2, s_0_45, -5, -1, 0}, +{ 3, s_0_46, -6, -1, 0}, +{ 3, s_0_47, -7, -1, 0}, +{ 4, s_0_48, -1, -1, 0}, +{ 2, s_0_49, -9, -1, 0}, +{ 3, s_0_50, -1, -1, 0}, +{ 4, s_0_51, -1, -1, 0}, +{ 3, s_0_52, -3, -1, 0}, +{ 4, s_0_53, -1, -1, 0}, +{ 5, s_0_54, -1, -1, 0}, +{ 3, s_0_55, -6, -1, 0}, +{ 4, s_0_56, -1, -1, 0}, +{ 4, s_0_57, -2, -1, 0}, +{ 7, s_0_58, -1, -1, 0}, +{ 4, s_0_59, -4, -1, 0}, +{ 3, s_0_60, -11, -1, 0}, +{ 6, s_0_61, -1, -1, 0}, +{ 4, s_0_62, -2, -1, 0}, +{ 5, s_0_63, -1, -1, 0}, +{ 6, s_0_64, -1, -1, 0}, +{ 3, s_0_65, -16, -1, 0}, +{ 4, s_0_66, -1, -1, 0}, +{ 7, s_0_67, -1, -1, 0}, +{ 4, s_0_68, -19, -1, 0}, +{ 4, s_0_69, -20, -1, 0}, +{ 4, s_0_70, -21, -1, 0}, +{ 5, s_0_71, -1, -1, 0}, +{ 2, s_0_72, -32, -1, 0}, +{ 4, s_0_73, -1, -1, 0}, +{ 4, s_0_74, -2, -1, 0}, +{ 3, s_0_75, -3, -1, 0}, +{ 4, s_0_76, -1, -1, 0}, +{ 4, s_0_77, -2, -1, 0}, +{ 5, s_0_78, -1, -1, 0}, +{ 4, s_0_79, -7, -1, 0}, +{ 5, s_0_80, -1, -1, 0}, +{ 3, s_0_81, -9, -1, 0}, +{ 4, s_0_82, -10, -1, 0}, +{ 7, s_0_83, -1, -1, 0}, +{ 6, s_0_84, -2, -1, 0}, +{ 7, s_0_85, -3, -1, 0}, +{ 2, s_0_86, -46, -1, 0}, +{ 3, s_0_87, -1, -1, 0}, +{ 5, s_0_88, -1, -1, 0}, +{ 2, s_0_89, 0, -1, 0}, +{ 3, s_0_90, 0, -1, 0}, +{ 1, s_0_91, 0, -1, 0}, +{ 2, s_0_92, 0, -1, 0}, +{ 3, s_0_93, -1, -1, 0}, +{ 3, s_0_94, 0, -1, 0}, +{ 2, s_0_95, 0, -1, 0}, { 3, s_0_96, -1, -1, 0}, -{ 1, s_0_97, -1, -1, 0}, -{ 2, s_0_98, -1, -1, 0}, -{ 3, s_0_99, 98, -1, 0}, -{ 3, s_0_100, -1, -1, 0}, -{ 2, s_0_101, -1, -1, 0}, -{ 3, s_0_102, 101, -1, 0}, -{ 2, s_0_103, -1, -1, 0}, -{ 3, s_0_104, -1, -1, 0}, +{ 2, s_0_97, 0, -1, 0}, +{ 3, s_0_98, 0, -1, 0}, +{ 3, s_0_99, 0, -1, 0}, +{ 6, s_0_100, -1, -1, 0}, +{ 2, s_0_101, 0, -1, 0}, +{ 2, s_0_102, 0, -1, 0}, +{ 3, s_0_103, -1, -1, 0}, +{ 2, s_0_104, 0, -1, 0}, { 3, s_0_105, -1, -1, 0}, -{ 6, s_0_106, 105, -1, 0}, -{ 2, s_0_107, -1, -1, 0}, +{ 3, s_0_106, 0, -1, 0}, +{ 1, s_0_107, 0, -1, 0}, { 2, s_0_108, -1, -1, 0}, -{ 3, s_0_109, 108, -1, 0}, -{ 2, s_0_110, -1, -1, 0}, -{ 3, s_0_111, 110, -1, 0}, -{ 3, s_0_112, -1, -1, 0}, -{ 1, s_0_113, -1, -1, 0}, -{ 2, s_0_114, 113, -1, 0}, -{ 4, s_0_115, 114, -1, 0}, -{ 4, s_0_116, 113, -1, 0}, -{ 2, s_0_117, 113, -1, 0}, -{ 1, s_0_118, -1, -1, 0}, -{ 2, s_0_119, 118, -1, 0}, -{ 3, s_0_120, 119, -1, 0}, -{ 2, s_0_121, 118, -1, 0}, -{ 3, s_0_122, 121, -1, 0}, -{ 2, s_0_123, 118, -1, 0}, -{ 3, s_0_124, 123, -1, 0}, -{ 4, s_0_125, 124, -1, 0}, -{ 6, s_0_126, 123, -1, 0}, -{ 4, s_0_127, 123, -1, 0}, -{ 6, s_0_128, 127, -1, 0}, -{ 4, s_0_129, 123, -1, 0}, -{ 5, s_0_130, 129, -1, 0}, -{ 4, s_0_131, 123, -1, 0}, -{ 5, s_0_132, 123, -1, 0}, -{ 4, s_0_133, 123, -1, 0}, -{ 4, s_0_134, 123, -1, 0}, -{ 4, s_0_135, 123, -1, 0}, -{ 3, s_0_136, 118, -1, 0}, -{ 4, s_0_137, 136, -1, 0}, -{ 4, s_0_138, 118, -1, 0}, -{ 3, s_0_139, 118, -1, 0}, -{ 5, s_0_140, 139, -1, 0}, -{ 5, s_0_141, 139, -1, 0}, -{ 3, s_0_142, 118, -1, 0}, -{ 4, s_0_143, 142, -1, 0}, -{ 3, s_0_144, 118, -1, 0}, -{ 4, s_0_145, 118, -1, 0}, -{ 3, s_0_146, 118, -1, 0}, -{ 2, s_0_147, 118, -1, 0}, -{ 3, s_0_148, 147, -1, 0}, -{ 3, s_0_149, 147, -1, 0}, -{ 4, s_0_150, 149, -1, 0}, -{ 3, s_0_151, 118, -1, 0}, -{ 2, s_0_152, 118, -1, 0}, -{ 3, s_0_153, 152, -1, 0}, -{ 4, s_0_154, 153, -1, 0}, -{ 3, s_0_155, 152, -1, 0}, -{ 2, s_0_156, 118, -1, 0}, -{ 4, s_0_157, 156, -1, 0}, -{ 4, s_0_158, 156, -1, 0}, -{ 3, s_0_159, 118, -1, 0}, -{ 4, s_0_160, 159, -1, 0}, -{ 3, s_0_161, 118, -1, 0}, -{ 5, s_0_162, 161, -1, 0}, -{ 6, s_0_163, 162, -1, 0}, -{ 5, s_0_164, 161, -1, 0}, -{ 6, s_0_165, 164, -1, 0}, -{ 6, s_0_166, 164, -1, 0}, -{ 5, s_0_167, 161, -1, 0}, -{ 6, s_0_168, 161, -1, 0}, -{ 9, s_0_169, 168, -1, 0}, -{ 5, s_0_170, 161, -1, 0}, -{ 6, s_0_171, 170, -1, 0}, -{ 6, s_0_172, 161, -1, 0}, -{ 5, s_0_173, 161, -1, 0}, -{ 6, s_0_174, 161, -1, 0}, -{ 9, s_0_175, 174, -1, 0}, -{ 3, s_0_176, 118, -1, 0}, -{ 3, s_0_177, 118, -1, 0}, -{ 4, s_0_178, 118, -1, 0}, -{ 2, s_0_179, -1, -1, 0}, -{ 3, s_0_180, 179, -1, 0}, -{ 2, s_0_181, -1, -1, 0}, -{ 3, s_0_182, 181, -1, 0}, -{ 2, s_0_183, -1, -1, 0}, -{ 3, s_0_184, -1, -1, 0}, -{ 6, s_0_185, 184, -1, 0}, -{ 1, s_0_186, -1, -1, 0}, -{ 2, s_0_187, 186, -1, 0}, -{ 3, s_0_188, 187, -1, 0}, -{ 5, s_0_189, 188, -1, 0}, -{ 2, s_0_190, 186, -1, 0}, -{ 4, s_0_191, 190, -1, 0}, -{ 3, s_0_192, 190, -1, 0}, -{ 1, s_0_193, -1, -1, 0}, -{ 2, s_0_194, -1, -1, 0}, -{ 3, s_0_195, 194, -1, 0}, -{ 2, s_0_196, -1, -1, 0}, -{ 2, s_0_197, -1, -1, 0}, -{ 2, s_0_198, -1, -1, 0}, -{ 4, s_0_199, 198, -1, 0}, -{ 4, s_0_200, 198, -1, 0}, -{ 2, s_0_201, -1, -1, 0}, -{ 3, s_0_202, 201, -1, 0}, -{ 4, s_0_203, 201, -1, 0} +{ 4, s_0_109, -1, -1, 0}, +{ 4, s_0_110, -3, -1, 0}, +{ 2, s_0_111, -4, -1, 0}, +{ 1, s_0_112, 0, -1, 0}, +{ 2, s_0_113, -1, -1, 0}, +{ 3, s_0_114, -1, -1, 0}, +{ 2, s_0_115, -3, -1, 0}, +{ 3, s_0_116, -1, -1, 0}, +{ 2, s_0_117, -5, -1, 0}, +{ 3, s_0_118, -1, -1, 0}, +{ 4, s_0_119, -1, -1, 0}, +{ 6, s_0_120, -3, -1, 0}, +{ 4, s_0_121, -4, -1, 0}, +{ 6, s_0_122, -1, -1, 0}, +{ 4, s_0_123, -6, -1, 0}, +{ 5, s_0_124, -1, -1, 0}, +{ 4, s_0_125, -8, -1, 0}, +{ 5, s_0_126, -9, -1, 0}, +{ 4, s_0_127, -10, -1, 0}, +{ 4, s_0_128, -11, -1, 0}, +{ 4, s_0_129, -12, -1, 0}, +{ 3, s_0_130, -18, -1, 0}, +{ 4, s_0_131, -1, -1, 0}, +{ 4, s_0_132, -20, -1, 0}, +{ 3, s_0_133, -21, -1, 0}, +{ 5, s_0_134, -1, -1, 0}, +{ 3, s_0_135, -23, -1, 0}, +{ 4, s_0_136, -1, -1, 0}, +{ 3, s_0_137, -25, -1, 0}, +{ 4, s_0_138, -26, -1, 0}, +{ 3, s_0_139, -27, -1, 0}, +{ 2, s_0_140, -28, -1, 0}, +{ 3, s_0_141, -1, -1, 0}, +{ 3, s_0_142, -2, -1, 0}, +{ 4, s_0_143, -1, -1, 0}, +{ 2, s_0_144, -32, -1, 0}, +{ 3, s_0_145, -1, -1, 0}, +{ 4, s_0_146, -1, -1, 0}, +{ 3, s_0_147, -3, -1, 0}, +{ 2, s_0_148, -36, -1, 0}, +{ 4, s_0_149, -1, -1, 0}, +{ 3, s_0_150, -38, -1, 0}, +{ 4, s_0_151, -1, -1, 0}, +{ 3, s_0_152, -40, -1, 0}, +{ 5, s_0_153, -1, -1, 0}, +{ 6, s_0_154, -1, -1, 0}, +{ 5, s_0_155, -3, -1, 0}, +{ 6, s_0_156, -1, -1, 0}, +{ 6, s_0_157, -2, -1, 0}, +{ 5, s_0_158, -6, -1, 0}, +{ 6, s_0_159, -7, -1, 0}, +{ 9, s_0_160, -1, -1, 0}, +{ 5, s_0_161, -9, -1, 0}, +{ 6, s_0_162, -1, -1, 0}, +{ 6, s_0_163, -11, -1, 0}, +{ 5, s_0_164, -12, -1, 0}, +{ 6, s_0_165, -13, -1, 0}, +{ 9, s_0_166, -1, -1, 0}, +{ 3, s_0_167, -55, -1, 0}, +{ 3, s_0_168, -56, -1, 0}, +{ 4, s_0_169, -57, -1, 0}, +{ 2, s_0_170, 0, -1, 0}, +{ 3, s_0_171, -1, -1, 0}, +{ 2, s_0_172, 0, -1, 0}, +{ 3, s_0_173, -1, -1, 0}, +{ 2, s_0_174, 0, -1, 0}, +{ 3, s_0_175, 0, -1, 0}, +{ 6, s_0_176, -1, -1, 0}, +{ 1, s_0_177, 0, -1, 0}, +{ 2, s_0_178, -1, -1, 0}, +{ 3, s_0_179, -1, -1, 0}, +{ 5, s_0_180, -1, -1, 0}, +{ 2, s_0_181, -4, -1, 0}, +{ 4, s_0_182, -1, -1, 0}, +{ 3, s_0_183, -2, -1, 0}, +{ 1, s_0_184, 0, -1, 0}, +{ 2, s_0_185, 0, -1, 0}, +{ 3, s_0_186, -1, -1, 0}, +{ 2, s_0_187, 0, -1, 0}, +{ 2, s_0_188, 0, -1, 0}, +{ 2, s_0_189, 0, -1, 0}, +{ 4, s_0_190, -1, -1, 0}, +{ 2, s_0_191, 0, -1, 0}, +{ 3, s_0_192, -1, -1, 0} }; static const symbol s_1_0[3] = { 'i', 'n', 'g' }; @@ -501,71 +491,69 @@ static const symbol s_1_58[3] = { 0xC5, 0xA1, 'v' }; static const symbol s_1_59[6] = { 'y', 'k', 0xC5, 0xA1, 0xC4, 0x8D }; static const symbol s_1_60[2] = { 0xC4, 0x99 }; static const symbol s_1_61[5] = { 0xC4, 0x97, 'j', 0xC4, 0x99 }; - -static const struct among a_1[62] = -{ -{ 3, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 3, s_1_2, 1, -1, 0}, -{ 3, s_1_3, -1, -1, 0}, -{ 2, s_1_4, -1, -1, 0}, -{ 3, s_1_5, 4, -1, 0}, -{ 3, s_1_6, 4, -1, 0}, -{ 4, s_1_7, 6, -1, 0}, -{ 3, s_1_8, -1, -1, 0}, -{ 3, s_1_9, -1, -1, 0}, -{ 4, s_1_10, 9, -1, 0}, -{ 3, s_1_11, -1, -1, 0}, -{ 3, s_1_12, -1, -1, 0}, -{ 4, s_1_13, 12, -1, 0}, -{ 2, s_1_14, -1, -1, 0}, -{ 3, s_1_15, 14, -1, 0}, -{ 3, s_1_16, -1, -1, 0}, -{ 5, s_1_17, 16, -1, 0}, -{ 6, s_1_18, 16, -1, 0}, -{ 4, s_1_19, -1, -1, 0}, -{ 3, s_1_20, -1, -1, 0}, -{ 2, s_1_21, -1, -1, 0}, -{ 3, s_1_22, -1, -1, 0}, -{ 2, s_1_23, -1, -1, 0}, -{ 3, s_1_24, 23, -1, 0}, -{ 3, s_1_25, 23, -1, 0}, -{ 4, s_1_26, -1, -1, 0}, -{ 3, s_1_27, -1, -1, 0}, -{ 3, s_1_28, -1, -1, 0}, -{ 2, s_1_29, -1, -1, 0}, -{ 3, s_1_30, 29, -1, 0}, -{ 3, s_1_31, -1, -1, 0}, -{ 3, s_1_32, -1, -1, 0}, -{ 3, s_1_33, -1, -1, 0}, -{ 4, s_1_34, 33, -1, 0}, -{ 2, s_1_35, -1, -1, 0}, -{ 3, s_1_36, 35, -1, 0}, -{ 3, s_1_37, 35, -1, 0}, -{ 4, s_1_38, 37, -1, 0}, -{ 3, s_1_39, -1, -1, 0}, -{ 4, s_1_40, 39, -1, 0}, -{ 3, s_1_41, -1, -1, 0}, -{ 4, s_1_42, 41, -1, 0}, -{ 3, s_1_43, -1, -1, 0}, -{ 7, s_1_44, -1, -1, 0}, -{ 3, s_1_45, -1, -1, 0}, -{ 4, s_1_46, 45, -1, 0}, -{ 5, s_1_47, 46, -1, 0}, -{ 3, s_1_48, -1, -1, 0}, -{ 2, s_1_49, -1, -1, 0}, -{ 3, s_1_50, 49, -1, 0}, -{ 4, s_1_51, 50, -1, 0}, -{ 2, s_1_52, -1, -1, 0}, -{ 3, s_1_53, -1, -1, 0}, -{ 5, s_1_54, -1, -1, 0}, -{ 3, s_1_55, -1, -1, 0}, -{ 3, s_1_56, -1, -1, 0}, -{ 2, s_1_57, -1, -1, 0}, -{ 3, s_1_58, -1, -1, 0}, -{ 6, s_1_59, -1, -1, 0}, -{ 2, s_1_60, -1, -1, 0}, -{ 5, s_1_61, 60, -1, 0} +static const struct among a_1[62] = { +{ 3, s_1_0, 0, -1, 0}, +{ 2, s_1_1, 0, -1, 0}, +{ 3, s_1_2, -1, -1, 0}, +{ 3, s_1_3, 0, -1, 0}, +{ 2, s_1_4, 0, -1, 0}, +{ 3, s_1_5, -1, -1, 0}, +{ 3, s_1_6, -2, -1, 0}, +{ 4, s_1_7, -1, -1, 0}, +{ 3, s_1_8, 0, -1, 0}, +{ 3, s_1_9, 0, -1, 0}, +{ 4, s_1_10, -1, -1, 0}, +{ 3, s_1_11, 0, -1, 0}, +{ 3, s_1_12, 0, -1, 0}, +{ 4, s_1_13, -1, -1, 0}, +{ 2, s_1_14, 0, -1, 0}, +{ 3, s_1_15, -1, -1, 0}, +{ 3, s_1_16, 0, -1, 0}, +{ 5, s_1_17, -1, -1, 0}, +{ 6, s_1_18, -2, -1, 0}, +{ 4, s_1_19, 0, -1, 0}, +{ 3, s_1_20, 0, -1, 0}, +{ 2, s_1_21, 0, -1, 0}, +{ 3, s_1_22, 0, -1, 0}, +{ 2, s_1_23, 0, -1, 0}, +{ 3, s_1_24, -1, -1, 0}, +{ 3, s_1_25, -2, -1, 0}, +{ 4, s_1_26, 0, -1, 0}, +{ 3, s_1_27, 0, -1, 0}, +{ 3, s_1_28, 0, -1, 0}, +{ 2, s_1_29, 0, -1, 0}, +{ 3, s_1_30, -1, -1, 0}, +{ 3, s_1_31, 0, -1, 0}, +{ 3, s_1_32, 0, -1, 0}, +{ 3, s_1_33, 0, -1, 0}, +{ 4, s_1_34, -1, -1, 0}, +{ 2, s_1_35, 0, -1, 0}, +{ 3, s_1_36, -1, -1, 0}, +{ 3, s_1_37, -2, -1, 0}, +{ 4, s_1_38, -1, -1, 0}, +{ 3, s_1_39, 0, -1, 0}, +{ 4, s_1_40, -1, -1, 0}, +{ 3, s_1_41, 0, -1, 0}, +{ 4, s_1_42, -1, -1, 0}, +{ 3, s_1_43, 0, -1, 0}, +{ 7, s_1_44, 0, -1, 0}, +{ 3, s_1_45, 0, -1, 0}, +{ 4, s_1_46, -1, -1, 0}, +{ 5, s_1_47, -1, -1, 0}, +{ 3, s_1_48, 0, -1, 0}, +{ 2, s_1_49, 0, -1, 0}, +{ 3, s_1_50, -1, -1, 0}, +{ 4, s_1_51, -1, -1, 0}, +{ 2, s_1_52, 0, -1, 0}, +{ 3, s_1_53, 0, -1, 0}, +{ 5, s_1_54, 0, -1, 0}, +{ 3, s_1_55, 0, -1, 0}, +{ 3, s_1_56, 0, -1, 0}, +{ 2, s_1_57, 0, -1, 0}, +{ 3, s_1_58, 0, -1, 0}, +{ 6, s_1_59, 0, -1, 0}, +{ 2, s_1_60, 0, -1, 0}, +{ 5, s_1_61, -1, -1, 0} }; static const symbol s_2_0[5] = { 'o', 'j', 'i', 'm', 'e' }; @@ -579,86 +567,65 @@ static const symbol s_2_7[7] = { 'o', 'k', 'a', 't', 0xC4, 0x97, 's' }; static const symbol s_2_8[6] = { 'a', 'i', 't', 0xC4, 0x97, 's' }; static const symbol s_2_9[6] = { 'u', 'o', 't', 0xC4, 0x97, 's' }; static const symbol s_2_10[4] = { 'e', 's', 'i', 'u' }; - -static const struct among a_2[11] = -{ -{ 5, s_2_0, -1, 7, 0}, -{ 6, s_2_1, -1, 3, 0}, -{ 5, s_2_2, -1, 6, 0}, -{ 5, s_2_3, -1, 8, 0}, -{ 4, s_2_4, -1, 1, 0}, -{ 4, s_2_5, -1, 2, 0}, -{ 5, s_2_6, -1, 5, 0}, -{ 7, s_2_7, -1, 8, 0}, -{ 6, s_2_8, -1, 1, 0}, -{ 6, s_2_9, -1, 2, 0}, -{ 4, s_2_10, -1, 4, 0} +static const struct among a_2[11] = { +{ 5, s_2_0, 0, 7, 0}, +{ 6, s_2_1, 0, 3, 0}, +{ 5, s_2_2, 0, 6, 0}, +{ 5, s_2_3, 0, 8, 0}, +{ 4, s_2_4, 0, 1, 0}, +{ 4, s_2_5, 0, 2, 0}, +{ 5, s_2_6, 0, 5, 0}, +{ 7, s_2_7, 0, 8, 0}, +{ 6, s_2_8, 0, 1, 0}, +{ 6, s_2_9, 0, 2, 0}, +{ 4, s_2_10, 0, 4, 0} }; static const symbol s_3_0[2] = { 0xC4, 0x8D }; static const symbol s_3_1[3] = { 'd', 0xC5, 0xBE }; - -static const struct among a_3[2] = -{ -{ 2, s_3_0, -1, 1, 0}, -{ 3, s_3_1, -1, 2, 0} -}; - -static const symbol s_4_0[2] = { 'g', 'd' }; - -static const struct among a_4[1] = -{ -{ 2, s_4_0, -1, 1, 0} +static const struct among a_3[2] = { +{ 2, s_3_0, 0, 1, 0}, +{ 3, s_3_1, 0, 2, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 64, 1, 0, 64, 0, 0, 0, 0, 0, 0, 0, 4, 4 }; -static const symbol s_0[] = { 'a', 'i', 't', 0xC4, 0x97 }; -static const symbol s_1[] = { 'u', 'o', 't', 0xC4, 0x97 }; -static const symbol s_2[] = { 0xC4, 0x97, 'j', 'i', 'm', 'a', 's' }; -static const symbol s_3[] = { 'e', 's', 'y', 's' }; -static const symbol s_4[] = { 'a', 's', 'y', 's' }; -static const symbol s_5[] = { 'a', 'v', 'i', 'm', 'a', 's' }; -static const symbol s_6[] = { 'o', 'j', 'i', 'm', 'a', 's' }; -static const symbol s_7[] = { 'o', 'k', 'a', 't', 0xC4, 0x97 }; -static const symbol s_8[] = { 't' }; -static const symbol s_9[] = { 'd' }; -static const symbol s_10[] = { 'g' }; - static int r_step1(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[0]) return 0; - mlimit1 = z->lb; z->lb = z->I[0]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (!find_among_b(z, a_0, 204)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_0, 193, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_step2(struct SN_env * z) { - while(1) { - int m1 = z->l - z->c; (void)m1; - - { int mlimit2; - if (z->c < z->I[0]) goto lab0; - mlimit2 = z->lb; z->lb = z->I[0]; + while (1) { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) goto lab0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (!find_among_b(z, a_1, 62)) { z->lb = mlimit2; goto lab0; } + if (!find_among_b(z, a_1, 62, 0)) { z->lb = v_2; goto lab0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } continue; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; break; } return 1; @@ -668,47 +635,55 @@ static int r_fix_conflicts(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 3 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((2621472 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_2, 11); + among_var = find_among_b(z, a_2, 11, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 5, s_0); + { + int ret = slice_from_s(z, 5, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 5, s_1); + { + int ret = slice_from_s(z, 5, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 7, s_2); + { + int ret = slice_from_s(z, 7, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_3); + { + int ret = slice_from_s(z, 4, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 4, s_4); + { + int ret = slice_from_s(z, 4, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_5); + { + int ret = slice_from_s(z, 6, s_5); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 6, s_6); + { + int ret = slice_from_s(z, 6, s_6); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 6, s_7); + { + int ret = slice_from_s(z, 6, s_7); if (ret < 0) return ret; } break; @@ -720,17 +695,19 @@ static int r_fix_chdz(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 141 && z->p[z->c - 1] != 190)) return 0; - among_var = find_among_b(z, a_3, 2); + among_var = find_among_b(z, a_3, 2, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; @@ -740,91 +717,103 @@ static int r_fix_chdz(struct SN_env * z) { static int r_fix_gd(struct SN_env * z) { z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 100) return 0; - if (!find_among_b(z, a_4, 1)) return 0; + if (!(eq_s_b(z, 2, s_10))) return 0; z->bra = z->c; - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } return 1; } extern int lithuanian_UTF_8_stem(struct SN_env * z) { - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - { int c_test3 = z->c; - if (z->c == z->l || z->p[z->c] != 'a') { z->c = c2; goto lab1; } - z->c++; - z->c = c_test3; - } - if (len_utf8(z->p) <= 6) { z->c = c2; goto lab1; } - { int ret = skip_utf8(z->p, z->c, z->l, 1); - if (ret < 0) { z->c = c2; goto lab1; } - z->c = ret; - } + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + { + int v_2 = z->c; + if (z->c == z->l || z->p[z->c] != 'a') { z->c = v_2; goto lab1; } + z->c++; + if (len_utf8(z->p) <= 6) { z->c = v_2; goto lab1; } lab1: ; } - { int ret = out_grouping_U(z, g_v, 97, 371, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 371, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p1 = z->c; lab0: - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m4 = z->l - z->c; (void)m4; - { int ret = r_fix_conflicts(z); + { + int v_3 = z->l - z->c; + { + int ret = r_fix_conflicts(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_3; } - { int m5 = z->l - z->c; (void)m5; - { int ret = r_step1(z); + { + int v_4 = z->l - z->c; + { + int ret = r_step1(z); if (ret < 0) return ret; } - z->c = z->l - m5; + z->c = z->l - v_4; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_fix_chdz(z); + { + int v_5 = z->l - z->c; + { + int ret = r_fix_chdz(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_5; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_step2(z); + { + int v_6 = z->l - z->c; + { + int ret = r_step2(z); if (ret < 0) return ret; } - z->c = z->l - m7; + z->c = z->l - v_6; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_fix_chdz(z); + { + int v_7 = z->l - z->c; + { + int ret = r_fix_chdz(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_7; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_fix_gd(z); + { + int v_8 = z->l - z->c; + { + int ret = r_fix_gd(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_8; } z->c = z->lb; return 1; } -extern struct SN_env * lithuanian_UTF_8_create_env(void) { return SN_create_env(0, 1); } +extern struct SN_env * lithuanian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void lithuanian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void lithuanian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_nepali.c b/src/backend/snowball/libstemmer/stem_UTF_8_nepali.c index 7cec8523cfa1f..8d148e7d85db1 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_nepali.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_nepali.c @@ -1,6 +1,10 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from nepali.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_nepali.h" + +#include + +#include "snowball_runtime.h" #ifdef __cplusplus extern "C" { @@ -9,22 +13,19 @@ extern int nepali_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_remove_category_3(struct SN_env * z); static int r_remove_category_2(struct SN_env * z); -static int r_check_category_2(struct SN_env * z); static int r_remove_category_1(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * nepali_UTF_8_create_env(void); -extern void nepali_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xE0, 0xA4, 0x8F }; +static const symbol s_1[] = { 0xE0, 0xA5, 0x87 }; +static const symbol s_2[] = { 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; +static const symbol s_3[] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_4[] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8C }; +static const symbol s_5[] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x87 }; +static const symbol s_6[] = { 0xE0, 0xA4, 0xA4, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xB0 }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[6] = { 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; static const symbol s_0_1[9] = { 0xE0, 0xA4, 0xB2, 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0x87 }; static const symbol s_0_2[6] = { 0xE0, 0xA4, 0xB2, 0xE0, 0xA5, 0x87 }; @@ -42,320 +43,288 @@ static const symbol s_0_13[6] = { 0xE0, 0xA4, 0xAE, 0xE0, 0xA4, 0xBE }; static const symbol s_0_14[18] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xB5, 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xB0, 0xE0, 0xA4, 0xBE }; static const symbol s_0_15[6] = { 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBF }; static const symbol s_0_16[9] = { 0xE0, 0xA4, 0xAA, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xBF }; - -static const struct among a_0[17] = -{ -{ 6, s_0_0, -1, 2, 0}, -{ 9, s_0_1, -1, 1, 0}, -{ 6, s_0_2, -1, 1, 0}, -{ 9, s_0_3, -1, 1, 0}, -{ 6, s_0_4, -1, 2, 0}, -{ 12, s_0_5, -1, 1, 0}, -{ 6, s_0_6, -1, 1, 0}, -{ 6, s_0_7, -1, 2, 0}, -{ 9, s_0_8, -1, 1, 0}, -{ 9, s_0_9, -1, 1, 0}, -{ 18, s_0_10, -1, 1, 0}, -{ 6, s_0_11, -1, 1, 0}, -{ 6, s_0_12, -1, 2, 0}, -{ 6, s_0_13, -1, 1, 0}, -{ 18, s_0_14, -1, 1, 0}, -{ 6, s_0_15, -1, 2, 0}, -{ 9, s_0_16, -1, 1, 0} +static const struct among a_0[17] = { +{ 6, s_0_0, 0, 2, 0}, +{ 9, s_0_1, 0, 1, 0}, +{ 6, s_0_2, 0, 1, 0}, +{ 9, s_0_3, 0, 1, 0}, +{ 6, s_0_4, 0, 2, 0}, +{ 12, s_0_5, 0, 1, 0}, +{ 6, s_0_6, 0, 1, 0}, +{ 6, s_0_7, 0, 2, 0}, +{ 9, s_0_8, 0, 1, 0}, +{ 9, s_0_9, 0, 1, 0}, +{ 18, s_0_10, 0, 1, 0}, +{ 6, s_0_11, 0, 1, 0}, +{ 6, s_0_12, 0, 2, 0}, +{ 6, s_0_13, 0, 1, 0}, +{ 18, s_0_14, 0, 1, 0}, +{ 6, s_0_15, 0, 2, 0}, +{ 9, s_0_16, 0, 1, 0} }; static const symbol s_1_0[3] = { 0xE0, 0xA4, 0x81 }; static const symbol s_1_1[3] = { 0xE0, 0xA4, 0x82 }; static const symbol s_1_2[3] = { 0xE0, 0xA5, 0x88 }; - -static const struct among a_1[3] = -{ -{ 3, s_1_0, -1, -1, 0}, -{ 3, s_1_1, -1, -1, 0}, -{ 3, s_1_2, -1, -1, 0} +static const struct among a_1[3] = { +{ 3, s_1_0, 0, 1, 0}, +{ 3, s_1_1, 0, 1, 0}, +{ 3, s_1_2, 0, 2, 0} }; -static const symbol s_2_0[3] = { 0xE0, 0xA4, 0x81 }; -static const symbol s_2_1[3] = { 0xE0, 0xA4, 0x82 }; -static const symbol s_2_2[3] = { 0xE0, 0xA5, 0x88 }; - -static const struct among a_2[3] = -{ -{ 3, s_2_0, -1, 1, 0}, -{ 3, s_2_1, -1, 1, 0}, -{ 3, s_2_2, -1, 2, 0} -}; - -static const symbol s_3_0[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_1[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_2[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_3[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_4[12] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x96, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_5[6] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_6[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x80 }; -static const symbol s_3_7[6] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; -static const symbol s_3_8[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; -static const symbol s_3_9[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; -static const symbol s_3_10[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; -static const symbol s_3_11[6] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x81 }; -static const symbol s_3_12[9] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA4, 0xB0, 0xE0, 0xA5, 0x81 }; -static const symbol s_3_13[9] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA4, 0xB0, 0xE0, 0xA5, 0x82 }; -static const symbol s_3_14[6] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x87 }; -static const symbol s_3_15[6] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x87 }; -static const symbol s_3_16[6] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87 }; -static const symbol s_3_17[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x88 }; -static const symbol s_3_18[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x88 }; -static const symbol s_3_19[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x88 }; -static const symbol s_3_20[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x88 }; -static const symbol s_3_21[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x88 }; -static const symbol s_3_22[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x88 }; -static const symbol s_3_23[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_24[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_25[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_26[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_27[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_28[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_29[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_30[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_31[6] = { 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_32[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_33[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_34[9] = { 0xE0, 0xA4, 0xAD, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_35[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_36[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_37[12] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; -static const symbol s_3_38[6] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_39[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_40[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_41[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_42[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_43[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_44[6] = { 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_45[12] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_46[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_47[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; -static const symbol s_3_48[9] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_49[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_50[12] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_51[15] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_52[12] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_53[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_54[12] = { 0xE0, 0xA4, 0xB2, 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_55[12] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_56[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_57[9] = { 0xE0, 0xA4, 0xAA, 0xE0, 0xA4, 0xB0, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_58[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_59[15] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_60[12] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_61[12] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA5, 0x8B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_62[9] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_63[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_64[12] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_65[15] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_66[12] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_67[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_68[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_69[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; -static const symbol s_3_70[9] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F }; -static const symbol s_3_71[3] = { 0xE0, 0xA4, 0x9B }; -static const symbol s_3_72[6] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_73[6] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_74[9] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_75[15] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA5, 0x81, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_76[15] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA5, 0x81, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_77[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_78[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_79[6] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_80[6] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B }; -static const symbol s_3_81[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_82[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_83[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_84[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_85[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_86[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_87[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_88[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBE }; -static const symbol s_3_89[12] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x96, 0xE0, 0xA4, 0xBF }; -static const symbol s_3_90[12] = { 0xE0, 0xA4, 0xAE, 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF }; - -static const struct among a_3[91] = -{ -{ 9, s_3_0, -1, 1, 0}, -{ 9, s_3_1, -1, 1, 0}, -{ 12, s_3_2, 1, 1, 0}, -{ 12, s_3_3, 1, 1, 0}, -{ 12, s_3_4, -1, 1, 0}, -{ 6, s_3_5, -1, 1, 0}, -{ 6, s_3_6, -1, 1, 0}, -{ 6, s_3_7, -1, 1, 0}, -{ 9, s_3_8, 7, 1, 0}, -{ 12, s_3_9, 8, 1, 0}, -{ 9, s_3_10, 7, 1, 0}, -{ 6, s_3_11, -1, 1, 0}, -{ 9, s_3_12, -1, 1, 0}, -{ 9, s_3_13, -1, 1, 0}, -{ 6, s_3_14, -1, 1, 0}, -{ 6, s_3_15, -1, 1, 0}, -{ 6, s_3_16, -1, 1, 0}, -{ 9, s_3_17, -1, 1, 0}, -{ 12, s_3_18, 17, 1, 0}, -{ 9, s_3_19, -1, 1, 0}, -{ 6, s_3_20, -1, 1, 0}, -{ 9, s_3_21, 20, 1, 0}, -{ 9, s_3_22, 20, 1, 0}, -{ 9, s_3_23, -1, 1, 0}, -{ 12, s_3_24, 23, 1, 0}, -{ 9, s_3_25, -1, 1, 0}, -{ 12, s_3_26, 25, 1, 0}, -{ 12, s_3_27, 25, 1, 0}, -{ 6, s_3_28, -1, 1, 0}, -{ 9, s_3_29, 28, 1, 0}, -{ 9, s_3_30, 28, 1, 0}, -{ 6, s_3_31, -1, 1, 0}, -{ 9, s_3_32, 31, 1, 0}, -{ 12, s_3_33, 31, 1, 0}, -{ 9, s_3_34, 31, 1, 0}, -{ 9, s_3_35, 31, 1, 0}, -{ 12, s_3_36, 35, 1, 0}, -{ 12, s_3_37, 35, 1, 0}, -{ 6, s_3_38, -1, 1, 0}, -{ 9, s_3_39, 38, 1, 0}, -{ 9, s_3_40, 38, 1, 0}, -{ 12, s_3_41, 40, 1, 0}, -{ 9, s_3_42, 38, 1, 0}, -{ 9, s_3_43, 38, 1, 0}, -{ 6, s_3_44, -1, 1, 0}, -{ 12, s_3_45, 44, 1, 0}, -{ 12, s_3_46, 44, 1, 0}, -{ 12, s_3_47, 44, 1, 0}, -{ 9, s_3_48, -1, 1, 0}, -{ 12, s_3_49, 48, 1, 0}, -{ 12, s_3_50, 48, 1, 0}, -{ 15, s_3_51, 50, 1, 0}, -{ 12, s_3_52, 48, 1, 0}, -{ 12, s_3_53, 48, 1, 0}, -{ 12, s_3_54, -1, 1, 0}, -{ 12, s_3_55, -1, 1, 0}, -{ 12, s_3_56, -1, 1, 0}, -{ 9, s_3_57, -1, 1, 0}, -{ 9, s_3_58, -1, 1, 0}, -{ 15, s_3_59, 58, 1, 0}, -{ 12, s_3_60, -1, 1, 0}, -{ 12, s_3_61, -1, 1, 0}, -{ 9, s_3_62, -1, 1, 0}, -{ 12, s_3_63, 62, 1, 0}, -{ 12, s_3_64, 62, 1, 0}, -{ 15, s_3_65, 64, 1, 0}, -{ 12, s_3_66, 62, 1, 0}, -{ 12, s_3_67, 62, 1, 0}, -{ 9, s_3_68, -1, 1, 0}, -{ 12, s_3_69, 68, 1, 0}, -{ 9, s_3_70, -1, 1, 0}, -{ 3, s_3_71, -1, 1, 0}, -{ 6, s_3_72, 71, 1, 0}, -{ 6, s_3_73, 71, 1, 0}, -{ 9, s_3_74, 73, 1, 0}, -{ 15, s_3_75, 74, 1, 0}, -{ 15, s_3_76, 71, 1, 0}, -{ 12, s_3_77, 71, 1, 0}, -{ 12, s_3_78, 71, 1, 0}, -{ 6, s_3_79, 71, 1, 0}, -{ 6, s_3_80, 71, 1, 0}, -{ 9, s_3_81, -1, 1, 0}, -{ 12, s_3_82, 81, 1, 0}, -{ 9, s_3_83, -1, 1, 0}, -{ 12, s_3_84, 83, 1, 0}, -{ 12, s_3_85, 83, 1, 0}, -{ 6, s_3_86, -1, 1, 0}, -{ 9, s_3_87, 86, 1, 0}, -{ 9, s_3_88, 86, 1, 0}, -{ 12, s_3_89, -1, 1, 0}, -{ 12, s_3_90, -1, 1, 0} +static const symbol s_2_0[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_1[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_2[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_3[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_4[12] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x96, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_5[6] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_6[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x80 }; +static const symbol s_2_7[6] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; +static const symbol s_2_8[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; +static const symbol s_2_9[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; +static const symbol s_2_10[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x81 }; +static const symbol s_2_11[6] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x81 }; +static const symbol s_2_12[9] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA4, 0xB0, 0xE0, 0xA5, 0x81 }; +static const symbol s_2_13[9] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA4, 0xB0, 0xE0, 0xA5, 0x82 }; +static const symbol s_2_14[6] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x87 }; +static const symbol s_2_15[6] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x87 }; +static const symbol s_2_16[6] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87 }; +static const symbol s_2_17[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x88 }; +static const symbol s_2_18[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x88 }; +static const symbol s_2_19[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x88 }; +static const symbol s_2_20[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x88 }; +static const symbol s_2_21[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x88 }; +static const symbol s_2_22[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x88 }; +static const symbol s_2_23[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_24[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_25[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_26[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_27[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_28[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_29[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_30[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_31[6] = { 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_32[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_33[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_34[9] = { 0xE0, 0xA4, 0xAD, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_35[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_36[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_37[12] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8B }; +static const symbol s_2_38[6] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_39[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_40[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_41[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_42[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_43[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_44[6] = { 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_45[12] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_46[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_47[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; +static const symbol s_2_48[9] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_49[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_50[12] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_51[15] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_52[12] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_53[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_54[12] = { 0xE0, 0xA4, 0xB2, 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_55[12] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_56[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_57[9] = { 0xE0, 0xA4, 0xAA, 0xE0, 0xA4, 0xB0, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_58[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_59[15] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_60[12] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_61[12] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA5, 0x8B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_62[9] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_63[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_64[12] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_65[15] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_66[12] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_67[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_68[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_69[12] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xB8, 0xE0, 0xA5, 0x8D }; +static const symbol s_2_70[9] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F }; +static const symbol s_2_71[3] = { 0xE0, 0xA4, 0x9B }; +static const symbol s_2_72[6] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_73[6] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_74[9] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_75[15] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA5, 0x81, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_76[15] = { 0xE0, 0xA4, 0xB9, 0xE0, 0xA5, 0x81, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_77[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_78[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_79[6] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_80[6] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x9B }; +static const symbol s_2_81[9] = { 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_82[12] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_83[9] = { 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_84[12] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_85[12] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0x8F, 0xE0, 0xA4, 0x95, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_86[6] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_87[9] = { 0xE0, 0xA4, 0x87, 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_88[9] = { 0xE0, 0xA4, 0xBF, 0xE0, 0xA4, 0xA6, 0xE0, 0xA4, 0xBE }; +static const symbol s_2_89[12] = { 0xE0, 0xA4, 0xA6, 0xE0, 0xA5, 0x87, 0xE0, 0xA4, 0x96, 0xE0, 0xA4, 0xBF }; +static const symbol s_2_90[12] = { 0xE0, 0xA4, 0xAE, 0xE0, 0xA4, 0xBE, 0xE0, 0xA4, 0xA5, 0xE0, 0xA4, 0xBF }; +static const struct among a_2[91] = { +{ 9, s_2_0, 0, 1, 0}, +{ 9, s_2_1, 0, 1, 0}, +{ 12, s_2_2, -1, 1, 0}, +{ 12, s_2_3, -2, 1, 0}, +{ 12, s_2_4, 0, 1, 0}, +{ 6, s_2_5, 0, 1, 0}, +{ 6, s_2_6, 0, 1, 0}, +{ 6, s_2_7, 0, 1, 0}, +{ 9, s_2_8, -1, 1, 0}, +{ 12, s_2_9, -1, 1, 0}, +{ 9, s_2_10, -3, 1, 0}, +{ 6, s_2_11, 0, 1, 0}, +{ 9, s_2_12, 0, 1, 0}, +{ 9, s_2_13, 0, 1, 0}, +{ 6, s_2_14, 0, 1, 0}, +{ 6, s_2_15, 0, 1, 0}, +{ 6, s_2_16, 0, 1, 0}, +{ 9, s_2_17, 0, 1, 0}, +{ 12, s_2_18, -1, 1, 0}, +{ 9, s_2_19, 0, 1, 0}, +{ 6, s_2_20, 0, 1, 0}, +{ 9, s_2_21, -1, 1, 0}, +{ 9, s_2_22, -2, 1, 0}, +{ 9, s_2_23, 0, 1, 0}, +{ 12, s_2_24, -1, 1, 0}, +{ 9, s_2_25, 0, 1, 0}, +{ 12, s_2_26, -1, 1, 0}, +{ 12, s_2_27, -2, 1, 0}, +{ 6, s_2_28, 0, 1, 0}, +{ 9, s_2_29, -1, 1, 0}, +{ 9, s_2_30, -2, 1, 0}, +{ 6, s_2_31, 0, 1, 0}, +{ 9, s_2_32, -1, 1, 0}, +{ 12, s_2_33, -2, 1, 0}, +{ 9, s_2_34, -3, 1, 0}, +{ 9, s_2_35, -4, 1, 0}, +{ 12, s_2_36, -1, 1, 0}, +{ 12, s_2_37, -2, 1, 0}, +{ 6, s_2_38, 0, 1, 0}, +{ 9, s_2_39, -1, 1, 0}, +{ 9, s_2_40, -2, 1, 0}, +{ 12, s_2_41, -1, 1, 0}, +{ 9, s_2_42, -4, 1, 0}, +{ 9, s_2_43, -5, 1, 0}, +{ 6, s_2_44, 0, 1, 0}, +{ 12, s_2_45, -1, 1, 0}, +{ 12, s_2_46, -2, 1, 0}, +{ 12, s_2_47, -3, 1, 0}, +{ 9, s_2_48, 0, 1, 0}, +{ 12, s_2_49, -1, 1, 0}, +{ 12, s_2_50, -2, 1, 0}, +{ 15, s_2_51, -1, 1, 0}, +{ 12, s_2_52, -4, 1, 0}, +{ 12, s_2_53, -5, 1, 0}, +{ 12, s_2_54, 0, 1, 0}, +{ 12, s_2_55, 0, 1, 0}, +{ 12, s_2_56, 0, 1, 0}, +{ 9, s_2_57, 0, 1, 0}, +{ 9, s_2_58, 0, 1, 0}, +{ 15, s_2_59, -1, 1, 0}, +{ 12, s_2_60, 0, 1, 0}, +{ 12, s_2_61, 0, 1, 0}, +{ 9, s_2_62, 0, 1, 0}, +{ 12, s_2_63, -1, 1, 0}, +{ 12, s_2_64, -2, 1, 0}, +{ 15, s_2_65, -1, 1, 0}, +{ 12, s_2_66, -4, 1, 0}, +{ 12, s_2_67, -5, 1, 0}, +{ 9, s_2_68, 0, 1, 0}, +{ 12, s_2_69, -1, 1, 0}, +{ 9, s_2_70, 0, 1, 0}, +{ 3, s_2_71, 0, 1, 0}, +{ 6, s_2_72, -1, 1, 0}, +{ 6, s_2_73, -2, 1, 0}, +{ 9, s_2_74, -1, 1, 0}, +{ 15, s_2_75, -1, 1, 0}, +{ 15, s_2_76, -5, 1, 0}, +{ 12, s_2_77, -6, 1, 0}, +{ 12, s_2_78, -7, 1, 0}, +{ 6, s_2_79, -8, 1, 0}, +{ 6, s_2_80, -9, 1, 0}, +{ 9, s_2_81, 0, 1, 0}, +{ 12, s_2_82, -1, 1, 0}, +{ 9, s_2_83, 0, 1, 0}, +{ 12, s_2_84, -1, 1, 0}, +{ 12, s_2_85, -2, 1, 0}, +{ 6, s_2_86, 0, 1, 0}, +{ 9, s_2_87, -1, 1, 0}, +{ 9, s_2_88, -2, 1, 0}, +{ 12, s_2_89, 0, 1, 0}, +{ 12, s_2_90, 0, 1, 0} }; -static const symbol s_0[] = { 0xE0, 0xA4, 0x8F }; -static const symbol s_1[] = { 0xE0, 0xA5, 0x87 }; -static const symbol s_2[] = { 0xE0, 0xA4, 0xAF, 0xE0, 0xA5, 0x8C }; -static const symbol s_3[] = { 0xE0, 0xA4, 0x9B, 0xE0, 0xA5, 0x8C }; -static const symbol s_4[] = { 0xE0, 0xA4, 0xA8, 0xE0, 0xA5, 0x8C }; -static const symbol s_5[] = { 0xE0, 0xA4, 0xA5, 0xE0, 0xA5, 0x87 }; -static const symbol s_6[] = { 0xE0, 0xA4, 0xA4, 0xE0, 0xA5, 0x8D, 0xE0, 0xA4, 0xB0 }; - static int r_remove_category_1(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_0, 17); + among_var = find_among_b(z, a_0, 17, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m1 = z->l - z->c; (void)m1; - { int m2 = z->l - z->c; (void)m2; - if (!(eq_s_b(z, 3, s_0))) goto lab3; - goto lab2; - lab3: - z->c = z->l - m2; - if (!(eq_s_b(z, 3, s_1))) goto lab1; - } - lab2: - goto lab0; + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 3, s_0))) goto lab0; + break; + lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 3, s_1))) goto lab1; + break; lab1: - z->c = z->l - m1; - { int ret = slice_del(z); + z->c = z->l - v_1; + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab0: + } while (0); break; } return 1; } -static int r_check_category_2(struct SN_env * z) { - z->ket = z->c; - if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((262 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_1, 3)) return 0; - z->bra = z->c; - return 1; -} - static int r_remove_category_2(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((262 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_2, 3); + among_var = find_among_b(z, a_1, 3, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int m1 = z->l - z->c; (void)m1; - if (!(eq_s_b(z, 6, s_2))) goto lab1; - goto lab0; + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 6, s_2))) goto lab0; + break; + lab0: + z->c = z->l - v_1; + if (!(eq_s_b(z, 6, s_3))) goto lab1; + break; lab1: - z->c = z->l - m1; - if (!(eq_s_b(z, 6, s_3))) goto lab2; - goto lab0; + z->c = z->l - v_1; + if (!(eq_s_b(z, 6, s_4))) goto lab2; + break; lab2: - z->c = z->l - m1; - if (!(eq_s_b(z, 6, s_4))) goto lab3; - goto lab0; - lab3: - z->c = z->l - m1; + z->c = z->l - v_1; if (!(eq_s_b(z, 6, s_5))) return 0; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (!(eq_s_b(z, 9, s_6))) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -365,9 +334,10 @@ static int r_remove_category_2(struct SN_env * z) { static int r_remove_category_3(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_3, 91)) return 0; + if (!find_among_b(z, a_2, 91, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -375,47 +345,43 @@ static int r_remove_category_3(struct SN_env * z) { extern int nepali_UTF_8_stem(struct SN_env * z) { z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_remove_category_1(z); + { + int v_1 = z->l - z->c; + { + int ret = r_remove_category_1(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - while(1) { - int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_check_category_2(z); - if (ret == 0) goto lab2; - if (ret < 0) return ret; - } - z->c = z->l - m5; - { int ret = r_remove_category_2(z); - if (ret == 0) goto lab2; - if (ret < 0) return ret; - } - } - lab2: - z->c = z->l - m4; - } - { int ret = r_remove_category_3(z); - if (ret == 0) goto lab1; + while (1) { + int v_2 = z->l - z->c; + { + int v_3 = z->l - z->c; + { + int ret = r_remove_category_2(z); if (ret < 0) return ret; } - continue; - lab1: - z->c = z->l - m3; - break; + z->c = z->l - v_3; + } + { + int ret = r_remove_category_3(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; } - z->c = z->l - m2; + continue; + lab0: + z->c = z->l - v_2; + break; } z->c = z->lb; return 1; } -extern struct SN_env * nepali_UTF_8_create_env(void) { return SN_create_env(0, 0); } +extern struct SN_env * nepali_UTF_8_create_env(void) { + return SN_new_env(sizeof(struct SN_env)); +} -extern void nepali_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void nepali_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_norwegian.c b/src/backend/snowball/libstemmer/stem_UTF_8_norwegian.c index 8d50961d50bf1..a85c76132605a 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_norwegian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_norwegian.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from norwegian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_norwegian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,189 +20,237 @@ extern int norwegian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_other_suffix(struct SN_env * z); static int r_consonant_pair(struct SN_env * z); static int r_main_suffix(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - -extern struct SN_env * norwegian_UTF_8_create_env(void); -extern void norwegian_UTF_8_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[1] = { 'a' }; -static const symbol s_0_1[1] = { 'e' }; -static const symbol s_0_2[3] = { 'e', 'd', 'e' }; -static const symbol s_0_3[4] = { 'a', 'n', 'd', 'e' }; -static const symbol s_0_4[4] = { 'e', 'n', 'd', 'e' }; -static const symbol s_0_5[3] = { 'a', 'n', 'e' }; -static const symbol s_0_6[3] = { 'e', 'n', 'e' }; -static const symbol s_0_7[6] = { 'h', 'e', 't', 'e', 'n', 'e' }; -static const symbol s_0_8[4] = { 'e', 'r', 't', 'e' }; -static const symbol s_0_9[2] = { 'e', 'n' }; -static const symbol s_0_10[5] = { 'h', 'e', 't', 'e', 'n' }; -static const symbol s_0_11[2] = { 'a', 'r' }; -static const symbol s_0_12[2] = { 'e', 'r' }; -static const symbol s_0_13[5] = { 'h', 'e', 't', 'e', 'r' }; -static const symbol s_0_14[1] = { 's' }; -static const symbol s_0_15[2] = { 'a', 's' }; -static const symbol s_0_16[2] = { 'e', 's' }; -static const symbol s_0_17[4] = { 'e', 'd', 'e', 's' }; -static const symbol s_0_18[5] = { 'e', 'n', 'd', 'e', 's' }; -static const symbol s_0_19[4] = { 'e', 'n', 'e', 's' }; -static const symbol s_0_20[7] = { 'h', 'e', 't', 'e', 'n', 'e', 's' }; -static const symbol s_0_21[3] = { 'e', 'n', 's' }; -static const symbol s_0_22[6] = { 'h', 'e', 't', 'e', 'n', 's' }; -static const symbol s_0_23[3] = { 'e', 'r', 's' }; -static const symbol s_0_24[3] = { 'e', 't', 's' }; -static const symbol s_0_25[2] = { 'e', 't' }; -static const symbol s_0_26[3] = { 'h', 'e', 't' }; -static const symbol s_0_27[3] = { 'e', 'r', 't' }; -static const symbol s_0_28[3] = { 'a', 's', 't' }; +static const symbol s_0[] = { 'e', 'r' }; -static const struct among a_0[29] = -{ -{ 1, s_0_0, -1, 1, 0}, -{ 1, s_0_1, -1, 1, 0}, -{ 3, s_0_2, 1, 1, 0}, -{ 4, s_0_3, 1, 1, 0}, -{ 4, s_0_4, 1, 1, 0}, -{ 3, s_0_5, 1, 1, 0}, -{ 3, s_0_6, 1, 1, 0}, -{ 6, s_0_7, 6, 1, 0}, -{ 4, s_0_8, 1, 3, 0}, -{ 2, s_0_9, -1, 1, 0}, -{ 5, s_0_10, 9, 1, 0}, -{ 2, s_0_11, -1, 1, 0}, -{ 2, s_0_12, -1, 1, 0}, -{ 5, s_0_13, 12, 1, 0}, -{ 1, s_0_14, -1, 2, 0}, -{ 2, s_0_15, 14, 1, 0}, -{ 2, s_0_16, 14, 1, 0}, -{ 4, s_0_17, 16, 1, 0}, -{ 5, s_0_18, 16, 1, 0}, -{ 4, s_0_19, 16, 1, 0}, -{ 7, s_0_20, 19, 1, 0}, -{ 3, s_0_21, 14, 1, 0}, -{ 6, s_0_22, 21, 1, 0}, -{ 3, s_0_23, 14, 1, 0}, -{ 3, s_0_24, 14, 1, 0}, -{ 2, s_0_25, -1, 1, 0}, -{ 3, s_0_26, 25, 1, 0}, -{ 3, s_0_27, -1, 3, 0}, -{ 3, s_0_28, -1, 1, 0} +static const symbol s_0_1[3] = { 'i', 'n', 'd' }; +static const symbol s_0_2[2] = { 'k', 'k' }; +static const symbol s_0_3[2] = { 'n', 'k' }; +static const symbol s_0_4[3] = { 'a', 'm', 'm' }; +static const symbol s_0_5[3] = { 'o', 'm', 'm' }; +static const symbol s_0_6[3] = { 'k', 'a', 'p' }; +static const symbol s_0_7[4] = { 's', 'k', 'a', 'p' }; +static const symbol s_0_8[2] = { 'p', 'p' }; +static const symbol s_0_9[2] = { 'l', 't' }; +static const symbol s_0_10[3] = { 'a', 's', 't' }; +static const symbol s_0_11[4] = { 0xC3, 0xB8, 's', 't' }; +static const symbol s_0_12[1] = { 'v' }; +static const symbol s_0_13[3] = { 'h', 'a', 'v' }; +static const symbol s_0_14[3] = { 'g', 'i', 'v' }; +static const struct among a_0[15] = { +{ 0, 0, 0, 1, 0}, +{ 3, s_0_1, -1, -1, 0}, +{ 2, s_0_2, -2, -1, 0}, +{ 2, s_0_3, -3, -1, 0}, +{ 3, s_0_4, -4, -1, 0}, +{ 3, s_0_5, -5, -1, 0}, +{ 3, s_0_6, -6, -1, 0}, +{ 4, s_0_7, -1, 1, 0}, +{ 2, s_0_8, -8, -1, 0}, +{ 2, s_0_9, -9, -1, 0}, +{ 3, s_0_10, -10, -1, 0}, +{ 4, s_0_11, -11, -1, 0}, +{ 1, s_0_12, -12, -1, 0}, +{ 3, s_0_13, -1, 1, 0}, +{ 3, s_0_14, -2, 1, 0} }; -static const symbol s_1_0[2] = { 'd', 't' }; -static const symbol s_1_1[2] = { 'v', 't' }; - -static const struct among a_1[2] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0} +static const symbol s_1_0[1] = { 'a' }; +static const symbol s_1_1[1] = { 'e' }; +static const symbol s_1_2[3] = { 'e', 'd', 'e' }; +static const symbol s_1_3[4] = { 'a', 'n', 'd', 'e' }; +static const symbol s_1_4[4] = { 'e', 'n', 'd', 'e' }; +static const symbol s_1_5[3] = { 'a', 'n', 'e' }; +static const symbol s_1_6[3] = { 'e', 'n', 'e' }; +static const symbol s_1_7[6] = { 'h', 'e', 't', 'e', 'n', 'e' }; +static const symbol s_1_8[4] = { 'e', 'r', 't', 'e' }; +static const symbol s_1_9[2] = { 'e', 'n' }; +static const symbol s_1_10[5] = { 'h', 'e', 't', 'e', 'n' }; +static const symbol s_1_11[2] = { 'a', 'r' }; +static const symbol s_1_12[2] = { 'e', 'r' }; +static const symbol s_1_13[5] = { 'h', 'e', 't', 'e', 'r' }; +static const symbol s_1_14[1] = { 's' }; +static const symbol s_1_15[2] = { 'a', 's' }; +static const symbol s_1_16[2] = { 'e', 's' }; +static const symbol s_1_17[4] = { 'e', 'd', 'e', 's' }; +static const symbol s_1_18[5] = { 'e', 'n', 'd', 'e', 's' }; +static const symbol s_1_19[4] = { 'e', 'n', 'e', 's' }; +static const symbol s_1_20[7] = { 'h', 'e', 't', 'e', 'n', 'e', 's' }; +static const symbol s_1_21[3] = { 'e', 'n', 's' }; +static const symbol s_1_22[6] = { 'h', 'e', 't', 'e', 'n', 's' }; +static const symbol s_1_23[3] = { 'e', 'r', 's' }; +static const symbol s_1_24[3] = { 'e', 't', 's' }; +static const symbol s_1_25[2] = { 'e', 't' }; +static const symbol s_1_26[3] = { 'h', 'e', 't' }; +static const symbol s_1_27[3] = { 'e', 'r', 't' }; +static const symbol s_1_28[3] = { 'a', 's', 't' }; +static const struct among a_1[29] = { +{ 1, s_1_0, 0, 1, 0}, +{ 1, s_1_1, 0, 1, 0}, +{ 3, s_1_2, -1, 1, 0}, +{ 4, s_1_3, -2, 1, 0}, +{ 4, s_1_4, -3, 1, 0}, +{ 3, s_1_5, -4, 1, 0}, +{ 3, s_1_6, -5, 1, 0}, +{ 6, s_1_7, -1, 1, 0}, +{ 4, s_1_8, -7, 4, 0}, +{ 2, s_1_9, 0, 1, 0}, +{ 5, s_1_10, -1, 1, 0}, +{ 2, s_1_11, 0, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 5, s_1_13, -1, 1, 0}, +{ 1, s_1_14, 0, 3, 0}, +{ 2, s_1_15, -1, 1, 0}, +{ 2, s_1_16, -2, 1, 0}, +{ 4, s_1_17, -1, 1, 0}, +{ 5, s_1_18, -2, 1, 0}, +{ 4, s_1_19, -3, 1, 0}, +{ 7, s_1_20, -1, 1, 0}, +{ 3, s_1_21, -7, 1, 0}, +{ 6, s_1_22, -1, 1, 0}, +{ 3, s_1_23, -9, 2, 0}, +{ 3, s_1_24, -10, 1, 0}, +{ 2, s_1_25, 0, 1, 0}, +{ 3, s_1_26, -1, 1, 0}, +{ 3, s_1_27, 0, 4, 0}, +{ 3, s_1_28, 0, 1, 0} }; -static const symbol s_2_0[3] = { 'l', 'e', 'g' }; -static const symbol s_2_1[4] = { 'e', 'l', 'e', 'g' }; -static const symbol s_2_2[2] = { 'i', 'g' }; -static const symbol s_2_3[3] = { 'e', 'i', 'g' }; -static const symbol s_2_4[3] = { 'l', 'i', 'g' }; -static const symbol s_2_5[4] = { 'e', 'l', 'i', 'g' }; -static const symbol s_2_6[3] = { 'e', 'l', 's' }; -static const symbol s_2_7[3] = { 'l', 'o', 'v' }; -static const symbol s_2_8[4] = { 'e', 'l', 'o', 'v' }; -static const symbol s_2_9[4] = { 's', 'l', 'o', 'v' }; -static const symbol s_2_10[7] = { 'h', 'e', 't', 's', 'l', 'o', 'v' }; - -static const struct among a_2[11] = -{ -{ 3, s_2_0, -1, 1, 0}, -{ 4, s_2_1, 0, 1, 0}, -{ 2, s_2_2, -1, 1, 0}, -{ 3, s_2_3, 2, 1, 0}, -{ 3, s_2_4, 2, 1, 0}, -{ 4, s_2_5, 4, 1, 0}, -{ 3, s_2_6, -1, 1, 0}, -{ 3, s_2_7, -1, 1, 0}, -{ 4, s_2_8, 7, 1, 0}, -{ 4, s_2_9, 7, 1, 0}, -{ 7, s_2_10, 9, 1, 0} +static const symbol s_2_0[2] = { 'd', 't' }; +static const symbol s_2_1[2] = { 'v', 't' }; +static const struct among a_2[2] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0} }; -static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 128 }; +static const symbol s_3_0[3] = { 'l', 'e', 'g' }; +static const symbol s_3_1[4] = { 'e', 'l', 'e', 'g' }; +static const symbol s_3_2[2] = { 'i', 'g' }; +static const symbol s_3_3[3] = { 'e', 'i', 'g' }; +static const symbol s_3_4[3] = { 'l', 'i', 'g' }; +static const symbol s_3_5[4] = { 'e', 'l', 'i', 'g' }; +static const symbol s_3_6[3] = { 'e', 'l', 's' }; +static const symbol s_3_7[3] = { 'l', 'o', 'v' }; +static const symbol s_3_8[4] = { 'e', 'l', 'o', 'v' }; +static const symbol s_3_9[4] = { 's', 'l', 'o', 'v' }; +static const symbol s_3_10[7] = { 'h', 'e', 't', 's', 'l', 'o', 'v' }; +static const struct among a_3[11] = { +{ 3, s_3_0, 0, 1, 0}, +{ 4, s_3_1, -1, 1, 0}, +{ 2, s_3_2, 0, 1, 0}, +{ 3, s_3_3, -1, 1, 0}, +{ 3, s_3_4, -2, 1, 0}, +{ 4, s_3_5, -1, 1, 0}, +{ 3, s_3_6, 0, 1, 0}, +{ 3, s_3_7, 0, 1, 0}, +{ 4, s_3_8, -1, 1, 0}, +{ 4, s_3_9, -2, 1, 0}, +{ 7, s_3_10, -1, 1, 0} +}; -static const unsigned char g_s_ending[] = { 119, 125, 149, 1 }; +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 2, 142 }; -static const symbol s_0[] = { 'e', 'r' }; +static const unsigned char g_s_ending[] = { 119, 125, 148, 1 }; static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c_test1 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); if (ret < 0) return 0; z->c = ret; } - z->I[0] = z->c; - z->c = c_test1; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping_U(z, g_v, 97, 248, 1); + if (ret < 0) return 0; + z->c += ret; } - - if (out_grouping_U(z, g_v, 97, 248, 1) < 0) return 0; - { int ret = in_grouping_U(z, g_v, 97, 248, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab0; - z->I[1] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: return 1; } static int r_main_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851426 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 29); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851426 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_1, 29, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m2 = z->l - z->c; (void)m2; - if (in_grouping_b_U(z, g_s_ending, 98, 122, 0)) goto lab1; - goto lab0; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((5318672 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 1; else + among_var = find_among_b(z, a_0, 15, 0); + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + } + break; + case 3: + do { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_s_ending, 98, 122, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'r') goto lab1; + z->c--; + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab2; + z->c--; + goto lab1; + lab2: + z->c = z->l - v_3; + } + break; lab1: - z->c = z->l - m2; + z->c = z->l - v_2; if (z->c <= z->lb || z->p[z->c - 1] != 'k') return 0; z->c--; if (out_grouping_b_U(z, g_v, 97, 248, 0)) return 0; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; - case 3: - { int ret = slice_from_s(z, 2, s_0); + case 4: + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; @@ -200,79 +259,98 @@ static int r_main_suffix(struct SN_env * z) { } static int r_consonant_pair(struct SN_env * z) { - { int m_test1 = z->l - z->c; - - { int mlimit2; - if (z->c < z->I[1]) return 0; - mlimit2 = z->lb; z->lb = z->I[1]; + { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] != 116) { z->lb = mlimit2; return 0; } - if (!find_among_b(z, a_1, 2)) { z->lb = mlimit2; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] != 116) { z->lb = v_2; return 0; } + if (!find_among_b(z, a_2, 2, 0)) { z->lb = v_2; return 0; } z->bra = z->c; - z->lb = mlimit2; + z->lb = v_2; } - z->c = z->l - m_test1; + z->c = z->l - v_1; } - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } static int r_other_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718720 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_2, 11)) { z->lb = mlimit1; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718720 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_3, 11, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int norwegian_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_main_suffix(z); + { + int v_2 = z->l - z->c; + { + int ret = r_main_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_other_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_other_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; return 1; } -extern struct SN_env * norwegian_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * norwegian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void norwegian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void norwegian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_polish.c b/src/backend/snowball/libstemmer/stem_UTF_8_polish.c new file mode 100644 index 0000000000000..a77433183da25 --- /dev/null +++ b/src/backend/snowball/libstemmer/stem_UTF_8_polish.c @@ -0,0 +1,523 @@ +/* Generated from polish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#include "stem_UTF_8_polish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; + +#ifdef __cplusplus +extern "C" { +#endif +extern int polish_UTF_8_stem(struct SN_env * z); +#ifdef __cplusplus +} +#endif + +static int r_R1(struct SN_env * z); +static int r_normalize_consonant(struct SN_env * z); +static int r_remove_endings(struct SN_env * z); +static int r_mark_regions(struct SN_env * z); + +static const symbol s_0[] = { 's' }; +static const symbol s_1[] = { 's' }; +static const symbol s_2[] = { 0xC5, 0x82 }; +static const symbol s_3[] = { 's' }; +static const symbol s_4[] = { 'c' }; +static const symbol s_5[] = { 'n' }; +static const symbol s_6[] = { 's' }; +static const symbol s_7[] = { 'z' }; + +static const symbol s_0_0[7] = { 'b', 'y', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_0_1[3] = { 'b', 'y', 'm' }; +static const symbol s_0_2[2] = { 'b', 'y' }; +static const symbol s_0_3[6] = { 'b', 'y', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_0_4[4] = { 'b', 'y', 0xC5, 0x9B }; +static const struct among a_0[5] = { +{ 7, s_0_0, 0, 1, 0}, +{ 3, s_0_1, 0, 1, 0}, +{ 2, s_0_2, 0, 1, 0}, +{ 6, s_0_3, 0, 1, 0}, +{ 4, s_0_4, 0, 1, 0} +}; + +static const symbol s_1_0[3] = { 0xC4, 0x85, 'c' }; +static const symbol s_1_1[5] = { 'a', 'j', 0xC4, 0x85, 'c' }; +static const symbol s_1_2[5] = { 's', 'z', 0xC4, 0x85, 'c' }; +static const symbol s_1_3[2] = { 's', 'z' }; +static const symbol s_1_4[5] = { 'i', 'e', 'j', 's', 'z' }; +static const struct among a_1[5] = { +{ 3, s_1_0, 0, 1, 0}, +{ 5, s_1_1, -1, 1, 0}, +{ 5, s_1_2, -2, 2, 0}, +{ 2, s_1_3, 0, 1, 0}, +{ 5, s_1_4, -1, 1, 0} +}; + +static const symbol s_2_0[1] = { 'a' }; +static const symbol s_2_1[4] = { 0xC4, 0x85, 'c', 'a' }; +static const symbol s_2_2[6] = { 'a', 'j', 0xC4, 0x85, 'c', 'a' }; +static const symbol s_2_3[6] = { 's', 'z', 0xC4, 0x85, 'c', 'a' }; +static const symbol s_2_4[2] = { 'i', 'a' }; +static const symbol s_2_5[3] = { 's', 'z', 'a' }; +static const symbol s_2_6[6] = { 'i', 'e', 'j', 's', 'z', 'a' }; +static const symbol s_2_7[4] = { 'a', 0xC5, 0x82, 'a' }; +static const symbol s_2_8[5] = { 'i', 'a', 0xC5, 0x82, 'a' }; +static const symbol s_2_9[4] = { 'i', 0xC5, 0x82, 'a' }; +static const symbol s_2_10[3] = { 0xC4, 0x85, 'c' }; +static const symbol s_2_11[5] = { 'a', 'j', 0xC4, 0x85, 'c' }; +static const symbol s_2_12[1] = { 'e' }; +static const symbol s_2_13[4] = { 0xC4, 0x85, 'c', 'e' }; +static const symbol s_2_14[6] = { 'a', 'j', 0xC4, 0x85, 'c', 'e' }; +static const symbol s_2_15[6] = { 's', 'z', 0xC4, 0x85, 'c', 'e' }; +static const symbol s_2_16[2] = { 'i', 'e' }; +static const symbol s_2_17[3] = { 'c', 'i', 'e' }; +static const symbol s_2_18[4] = { 'a', 'c', 'i', 'e' }; +static const symbol s_2_19[4] = { 'e', 'c', 'i', 'e' }; +static const symbol s_2_20[4] = { 'i', 'c', 'i', 'e' }; +static const symbol s_2_21[5] = { 'a', 'j', 'c', 'i', 'e' }; +static const symbol s_2_22[7] = { 'l', 'i', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_23[8] = { 'a', 'l', 'i', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_24[9] = { 'i', 'e', 'l', 'i', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_25[8] = { 'i', 'l', 'i', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_26[8] = { 0xC5, 0x82, 'y', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_27[9] = { 'a', 0xC5, 0x82, 'y', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_28[10] = { 'i', 'a', 0xC5, 0x82, 'y', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_29[9] = { 'i', 0xC5, 0x82, 'y', 0xC5, 0x9B, 'c', 'i', 'e' }; +static const symbol s_2_30[3] = { 's', 'z', 'e' }; +static const symbol s_2_31[6] = { 'i', 'e', 'j', 's', 'z', 'e' }; +static const symbol s_2_32[3] = { 'a', 'c', 'h' }; +static const symbol s_2_33[4] = { 'i', 'a', 'c', 'h' }; +static const symbol s_2_34[3] = { 'i', 'c', 'h' }; +static const symbol s_2_35[3] = { 'y', 'c', 'h' }; +static const symbol s_2_36[1] = { 'i' }; +static const symbol s_2_37[3] = { 'a', 'l', 'i' }; +static const symbol s_2_38[4] = { 'i', 'e', 'l', 'i' }; +static const symbol s_2_39[3] = { 'i', 'l', 'i' }; +static const symbol s_2_40[3] = { 'a', 'm', 'i' }; +static const symbol s_2_41[4] = { 'i', 'a', 'm', 'i' }; +static const symbol s_2_42[3] = { 'i', 'm', 'i' }; +static const symbol s_2_43[3] = { 'y', 'm', 'i' }; +static const symbol s_2_44[3] = { 'o', 'w', 'i' }; +static const symbol s_2_45[4] = { 'i', 'o', 'w', 'i' }; +static const symbol s_2_46[2] = { 'a', 'j' }; +static const symbol s_2_47[2] = { 'e', 'j' }; +static const symbol s_2_48[3] = { 'i', 'e', 'j' }; +static const symbol s_2_49[2] = { 'a', 'm' }; +static const symbol s_2_50[5] = { 'a', 0xC5, 0x82, 'a', 'm' }; +static const symbol s_2_51[6] = { 'i', 'a', 0xC5, 0x82, 'a', 'm' }; +static const symbol s_2_52[5] = { 'i', 0xC5, 0x82, 'a', 'm' }; +static const symbol s_2_53[2] = { 'e', 'm' }; +static const symbol s_2_54[3] = { 'i', 'e', 'm' }; +static const symbol s_2_55[5] = { 'a', 0xC5, 0x82, 'e', 'm' }; +static const symbol s_2_56[6] = { 'i', 'a', 0xC5, 0x82, 'e', 'm' }; +static const symbol s_2_57[5] = { 'i', 0xC5, 0x82, 'e', 'm' }; +static const symbol s_2_58[2] = { 'i', 'm' }; +static const symbol s_2_59[2] = { 'o', 'm' }; +static const symbol s_2_60[3] = { 'i', 'o', 'm' }; +static const symbol s_2_61[2] = { 'y', 'm' }; +static const symbol s_2_62[1] = { 'o' }; +static const symbol s_2_63[3] = { 'e', 'g', 'o' }; +static const symbol s_2_64[4] = { 'i', 'e', 'g', 'o' }; +static const symbol s_2_65[4] = { 'a', 0xC5, 0x82, 'o' }; +static const symbol s_2_66[5] = { 'i', 'a', 0xC5, 0x82, 'o' }; +static const symbol s_2_67[4] = { 'i', 0xC5, 0x82, 'o' }; +static const symbol s_2_68[1] = { 'u' }; +static const symbol s_2_69[2] = { 'i', 'u' }; +static const symbol s_2_70[3] = { 'e', 'm', 'u' }; +static const symbol s_2_71[4] = { 'i', 'e', 'm', 'u' }; +static const symbol s_2_72[3] = { 0xC3, 0xB3, 'w' }; +static const symbol s_2_73[1] = { 'y' }; +static const symbol s_2_74[3] = { 'a', 'm', 'y' }; +static const symbol s_2_75[3] = { 'e', 'm', 'y' }; +static const symbol s_2_76[3] = { 'i', 'm', 'y' }; +static const symbol s_2_77[6] = { 'l', 'i', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_78[7] = { 'a', 'l', 'i', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_79[8] = { 'i', 'e', 'l', 'i', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_80[7] = { 'i', 'l', 'i', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_81[7] = { 0xC5, 0x82, 'y', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_82[8] = { 'a', 0xC5, 0x82, 'y', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_83[9] = { 'i', 'a', 0xC5, 0x82, 'y', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_84[8] = { 'i', 0xC5, 0x82, 'y', 0xC5, 0x9B, 'm', 'y' }; +static const symbol s_2_85[4] = { 'a', 0xC5, 0x82, 'y' }; +static const symbol s_2_86[5] = { 'i', 'a', 0xC5, 0x82, 'y' }; +static const symbol s_2_87[4] = { 'i', 0xC5, 0x82, 'y' }; +static const symbol s_2_88[3] = { 'a', 's', 'z' }; +static const symbol s_2_89[3] = { 'e', 's', 'z' }; +static const symbol s_2_90[3] = { 'i', 's', 'z' }; +static const symbol s_2_91[3] = { 'a', 0xC5, 0x82 }; +static const symbol s_2_92[4] = { 'i', 'a', 0xC5, 0x82 }; +static const symbol s_2_93[3] = { 'i', 0xC5, 0x82 }; +static const symbol s_2_94[2] = { 0xC4, 0x85 }; +static const symbol s_2_95[5] = { 0xC4, 0x85, 'c', 0xC4, 0x85 }; +static const symbol s_2_96[7] = { 'a', 'j', 0xC4, 0x85, 'c', 0xC4, 0x85 }; +static const symbol s_2_97[7] = { 's', 'z', 0xC4, 0x85, 'c', 0xC4, 0x85 }; +static const symbol s_2_98[3] = { 'i', 0xC4, 0x85 }; +static const symbol s_2_99[4] = { 'a', 'j', 0xC4, 0x85 }; +static const symbol s_2_100[4] = { 's', 'z', 0xC4, 0x85 }; +static const symbol s_2_101[7] = { 'i', 'e', 'j', 's', 'z', 0xC4, 0x85 }; +static const symbol s_2_102[3] = { 'a', 0xC4, 0x87 }; +static const symbol s_2_103[4] = { 'i', 'e', 0xC4, 0x87 }; +static const symbol s_2_104[3] = { 'i', 0xC4, 0x87 }; +static const symbol s_2_105[4] = { 0xC4, 0x85, 0xC4, 0x87 }; +static const symbol s_2_106[5] = { 'a', 0xC5, 0x9B, 0xC4, 0x87 }; +static const symbol s_2_107[5] = { 'e', 0xC5, 0x9B, 0xC4, 0x87 }; +static const symbol s_2_108[2] = { 0xC4, 0x99 }; +static const symbol s_2_109[4] = { 's', 'z', 0xC4, 0x99 }; +static const symbol s_2_110[5] = { 0xC5, 0x82, 'a', 0xC5, 0x9B }; +static const symbol s_2_111[6] = { 'a', 0xC5, 0x82, 'a', 0xC5, 0x9B }; +static const symbol s_2_112[7] = { 'i', 'a', 0xC5, 0x82, 'a', 0xC5, 0x9B }; +static const symbol s_2_113[6] = { 'i', 0xC5, 0x82, 'a', 0xC5, 0x9B }; +static const symbol s_2_114[5] = { 0xC5, 0x82, 'e', 0xC5, 0x9B }; +static const symbol s_2_115[6] = { 'a', 0xC5, 0x82, 'e', 0xC5, 0x9B }; +static const symbol s_2_116[7] = { 'i', 'a', 0xC5, 0x82, 'e', 0xC5, 0x9B }; +static const symbol s_2_117[6] = { 'i', 0xC5, 0x82, 'e', 0xC5, 0x9B }; +static const struct among a_2[118] = { +{ 1, s_2_0, 0, 1, 1}, +{ 4, s_2_1, -1, 1, 0}, +{ 6, s_2_2, -1, 1, 0}, +{ 6, s_2_3, -2, 2, 0}, +{ 2, s_2_4, -4, 1, 1}, +{ 3, s_2_5, -5, 1, 0}, +{ 6, s_2_6, -1, 1, 0}, +{ 4, s_2_7, -7, 1, 0}, +{ 5, s_2_8, -1, 1, 0}, +{ 4, s_2_9, -9, 1, 0}, +{ 3, s_2_10, 0, 1, 0}, +{ 5, s_2_11, -1, 1, 0}, +{ 1, s_2_12, 0, 1, 1}, +{ 4, s_2_13, -1, 1, 0}, +{ 6, s_2_14, -1, 1, 0}, +{ 6, s_2_15, -2, 2, 0}, +{ 2, s_2_16, -4, 1, 1}, +{ 3, s_2_17, -1, 1, 0}, +{ 4, s_2_18, -1, 1, 0}, +{ 4, s_2_19, -2, 1, 0}, +{ 4, s_2_20, -3, 1, 0}, +{ 5, s_2_21, -4, 1, 0}, +{ 7, s_2_22, -5, 4, 0}, +{ 8, s_2_23, -1, 1, 0}, +{ 9, s_2_24, -2, 1, 0}, +{ 8, s_2_25, -3, 1, 0}, +{ 8, s_2_26, -9, 4, 0}, +{ 9, s_2_27, -1, 1, 0}, +{ 10, s_2_28, -1, 1, 0}, +{ 9, s_2_29, -3, 1, 0}, +{ 3, s_2_30, -18, 1, 0}, +{ 6, s_2_31, -1, 1, 0}, +{ 3, s_2_32, 0, 1, 1}, +{ 4, s_2_33, -1, 1, 1}, +{ 3, s_2_34, 0, 5, 0}, +{ 3, s_2_35, 0, 5, 0}, +{ 1, s_2_36, 0, 1, 1}, +{ 3, s_2_37, -1, 1, 0}, +{ 4, s_2_38, -2, 1, 0}, +{ 3, s_2_39, -3, 1, 0}, +{ 3, s_2_40, -4, 1, 1}, +{ 4, s_2_41, -1, 1, 1}, +{ 3, s_2_42, -6, 5, 0}, +{ 3, s_2_43, -7, 5, 0}, +{ 3, s_2_44, -8, 1, 1}, +{ 4, s_2_45, -1, 1, 1}, +{ 2, s_2_46, 0, 1, 0}, +{ 2, s_2_47, 0, 5, 0}, +{ 3, s_2_48, -1, 5, 0}, +{ 2, s_2_49, 0, 1, 0}, +{ 5, s_2_50, -1, 1, 0}, +{ 6, s_2_51, -1, 1, 0}, +{ 5, s_2_52, -3, 1, 0}, +{ 2, s_2_53, 0, 1, 1}, +{ 3, s_2_54, -1, 1, 1}, +{ 5, s_2_55, -2, 1, 0}, +{ 6, s_2_56, -1, 1, 0}, +{ 5, s_2_57, -4, 1, 0}, +{ 2, s_2_58, 0, 5, 0}, +{ 2, s_2_59, 0, 1, 1}, +{ 3, s_2_60, -1, 1, 1}, +{ 2, s_2_61, 0, 5, 0}, +{ 1, s_2_62, 0, 1, 1}, +{ 3, s_2_63, -1, 5, 0}, +{ 4, s_2_64, -1, 5, 0}, +{ 4, s_2_65, -3, 1, 0}, +{ 5, s_2_66, -1, 1, 0}, +{ 4, s_2_67, -5, 1, 0}, +{ 1, s_2_68, 0, 1, 1}, +{ 2, s_2_69, -1, 1, 1}, +{ 3, s_2_70, -2, 5, 0}, +{ 4, s_2_71, -1, 5, 0}, +{ 3, s_2_72, 0, 1, 1}, +{ 1, s_2_73, 0, 5, 0}, +{ 3, s_2_74, -1, 1, 0}, +{ 3, s_2_75, -2, 1, 0}, +{ 3, s_2_76, -3, 1, 0}, +{ 6, s_2_77, -4, 4, 0}, +{ 7, s_2_78, -1, 1, 0}, +{ 8, s_2_79, -2, 1, 0}, +{ 7, s_2_80, -3, 1, 0}, +{ 7, s_2_81, -8, 4, 0}, +{ 8, s_2_82, -1, 1, 0}, +{ 9, s_2_83, -1, 1, 0}, +{ 8, s_2_84, -3, 1, 0}, +{ 4, s_2_85, -12, 1, 0}, +{ 5, s_2_86, -1, 1, 0}, +{ 4, s_2_87, -14, 1, 0}, +{ 3, s_2_88, 0, 1, 0}, +{ 3, s_2_89, 0, 1, 0}, +{ 3, s_2_90, 0, 1, 0}, +{ 3, s_2_91, 0, 1, 0}, +{ 4, s_2_92, -1, 1, 0}, +{ 3, s_2_93, 0, 1, 0}, +{ 2, s_2_94, 0, 1, 1}, +{ 5, s_2_95, -1, 1, 0}, +{ 7, s_2_96, -1, 1, 0}, +{ 7, s_2_97, -2, 2, 0}, +{ 3, s_2_98, -4, 1, 1}, +{ 4, s_2_99, -5, 1, 0}, +{ 4, s_2_100, -6, 3, 0}, +{ 7, s_2_101, -1, 1, 0}, +{ 3, s_2_102, 0, 1, 0}, +{ 4, s_2_103, 0, 1, 0}, +{ 3, s_2_104, 0, 1, 0}, +{ 4, s_2_105, 0, 1, 0}, +{ 5, s_2_106, 0, 1, 0}, +{ 5, s_2_107, 0, 1, 0}, +{ 2, s_2_108, 0, 1, 0}, +{ 4, s_2_109, -1, 2, 0}, +{ 5, s_2_110, 0, 4, 0}, +{ 6, s_2_111, -1, 1, 0}, +{ 7, s_2_112, -1, 1, 0}, +{ 6, s_2_113, -3, 1, 0}, +{ 5, s_2_114, 0, 4, 0}, +{ 6, s_2_115, -1, 1, 0}, +{ 7, s_2_116, -1, 1, 0}, +{ 6, s_2_117, -3, 1, 0} +}; + +static const symbol s_3_0[2] = { 0xC5, 0x84 }; +static const symbol s_3_1[2] = { 0xC4, 0x87 }; +static const symbol s_3_2[2] = { 0xC5, 0x9B }; +static const symbol s_3_3[2] = { 0xC5, 0xBA }; +static const struct among a_3[4] = { +{ 2, s_3_0, 0, 2, 0}, +{ 2, s_3_1, 0, 1, 0}, +{ 2, s_3_2, 0, 3, 0}, +{ 2, s_3_3, 0, 4, 0} +}; + +static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 16, 0, 0, 1 }; + +static int r_mark_regions(struct SN_env * z) { + ((SN_local *)z)->i_p1 = z->l; + { + int ret = out_grouping_U(z, g_v, 97, 281, 1); + if (ret < 0) return 0; + z->c += ret; + } + { + int ret = in_grouping_U(z, g_v, 97, 281, 1); + if (ret < 0) return 0; + z->c += ret; + } + ((SN_local *)z)->i_p1 = z->c; + return 1; +} + +static int r_R1(struct SN_env * z) { + return ((SN_local *)z)->i_p1 <= z->c; +} + +static int r_remove_endings(struct SN_env * z) { + int among_var; + { + int v_1 = z->l - z->c; + { + int v_2; + if (z->c < ((SN_local *)z)->i_p1) goto lab0; + v_2 = z->lb; z->lb = ((SN_local *)z)->i_p1; + z->ket = z->c; + if (!find_among_b(z, a_0, 5, 0)) { z->lb = v_2; goto lab0; } + z->bra = z->c; + z->lb = v_2; + } + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + lab0: + z->c = z->l - v_1; + } + z->ket = z->c; + among_var = find_among_b(z, a_2, 118, r_R1); + if (!among_var) return 0; + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_0); + if (ret < 0) return ret; + } + break; + case 3: + do { + int v_3 = z->l - z->c; + { + int v_4 = z->l - z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + z->c = z->l - v_4; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + } + break; + lab1: + z->c = z->l - v_3; + { + int ret = slice_from_s(z, 1, s_1); + if (ret < 0) return ret; + } + } while (0); + break; + case 4: + { + int ret = slice_from_s(z, 2, s_2); + if (ret < 0) return ret; + } + break; + case 5: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + { + int v_5 = z->l - z->c; + z->ket = z->c; + if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 99 && z->p[z->c - 1] != 122)) { z->c = z->l - v_5; goto lab2; } + among_var = find_among_b(z, a_1, 5, 0); + if (!among_var) { z->c = z->l - v_5; goto lab2; } + z->bra = z->c; + switch (among_var) { + case 1: + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_3); + if (ret < 0) return ret; + } + break; + } + lab2: + ; + } + break; + } + return 1; +} + +static int r_normalize_consonant(struct SN_env * z) { + int among_var; + z->ket = z->c; + among_var = find_among_b(z, a_3, 4, 0); + if (!among_var) return 0; + z->bra = z->c; + if (z->c > z->lb) goto lab0; + return 0; +lab0: + switch (among_var) { + case 1: + { + int ret = slice_from_s(z, 1, s_4); + if (ret < 0) return ret; + } + break; + case 2: + { + int ret = slice_from_s(z, 1, s_5); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = slice_from_s(z, 1, s_6); + if (ret < 0) return ret; + } + break; + case 4: + { + int ret = slice_from_s(z, 1, s_7); + if (ret < 0) return ret; + } + break; + } + return 1; +} + +extern int polish_UTF_8_stem(struct SN_env * z) { + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); + if (ret < 0) return ret; + } + z->c = v_1; + } + do { + int v_2 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 2); + if (ret < 0) goto lab0; + z->c = ret; + } + z->lb = z->c; z->c = z->l; + { + int ret = r_remove_endings(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + z->c = z->lb; + break; + lab0: + z->c = v_2; + z->lb = z->c; z->c = z->l; + { + int ret = r_normalize_consonant(z); + if (ret <= 0) return ret; + } + z->c = z->lb; + } while (0); + return 1; +} + +extern struct SN_env * polish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} + +extern void polish_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} + diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_porter.c b/src/backend/snowball/libstemmer/stem_UTF_8_porter.c index 297ce1405009c..6d6ebf2f004ee 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_porter.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_porter.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_porter.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int porter_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_Step_5b(struct SN_env * z); static int r_Step_5a(struct SN_env * z); static int r_Step_4(struct SN_env * z); @@ -20,29 +33,41 @@ static int r_Step_1a(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_shortv(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * porter_UTF_8_create_env(void); -extern void porter_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 's', 's' }; +static const symbol s_1[] = { 'i' }; +static const symbol s_2[] = { 'e', 'e' }; +static const symbol s_3[] = { 'e' }; +static const symbol s_4[] = { 'e' }; +static const symbol s_5[] = { 'i' }; +static const symbol s_6[] = { 't', 'i', 'o', 'n' }; +static const symbol s_7[] = { 'e', 'n', 'c', 'e' }; +static const symbol s_8[] = { 'a', 'n', 'c', 'e' }; +static const symbol s_9[] = { 'a', 'b', 'l', 'e' }; +static const symbol s_10[] = { 'e', 'n', 't' }; +static const symbol s_11[] = { 'e' }; +static const symbol s_12[] = { 'i', 'z', 'e' }; +static const symbol s_13[] = { 'a', 't', 'e' }; +static const symbol s_14[] = { 'a', 'l' }; +static const symbol s_15[] = { 'f', 'u', 'l' }; +static const symbol s_16[] = { 'o', 'u', 's' }; +static const symbol s_17[] = { 'i', 'v', 'e' }; +static const symbol s_18[] = { 'b', 'l', 'e' }; +static const symbol s_19[] = { 'a', 'l' }; +static const symbol s_20[] = { 'i', 'c' }; +static const symbol s_21[] = { 'Y' }; +static const symbol s_22[] = { 'Y' }; +static const symbol s_23[] = { 'y' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[1] = { 's' }; static const symbol s_0_1[3] = { 'i', 'e', 's' }; static const symbol s_0_2[4] = { 's', 's', 'e', 's' }; static const symbol s_0_3[2] = { 's', 's' }; - -static const struct among a_0[4] = -{ -{ 1, s_0_0, -1, 3, 0}, -{ 3, s_0_1, 0, 2, 0}, -{ 4, s_0_2, 0, 1, 0}, -{ 2, s_0_3, 0, -1, 0} +static const struct among a_0[4] = { +{ 1, s_0_0, 0, 3, 0}, +{ 3, s_0_1, -1, 2, 0}, +{ 4, s_0_2, -2, 1, 0}, +{ 2, s_0_3, -3, -1, 0} }; static const symbol s_1_1[2] = { 'b', 'b' }; @@ -57,33 +82,29 @@ static const symbol s_1_9[2] = { 'r', 'r' }; static const symbol s_1_10[2] = { 'a', 't' }; static const symbol s_1_11[2] = { 't', 't' }; static const symbol s_1_12[2] = { 'i', 'z' }; - -static const struct among a_1[13] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_1_1, 0, 2, 0}, -{ 2, s_1_2, 0, 2, 0}, -{ 2, s_1_3, 0, 2, 0}, -{ 2, s_1_4, 0, 2, 0}, -{ 2, s_1_5, 0, 1, 0}, -{ 2, s_1_6, 0, 2, 0}, -{ 2, s_1_7, 0, 2, 0}, -{ 2, s_1_8, 0, 2, 0}, -{ 2, s_1_9, 0, 2, 0}, -{ 2, s_1_10, 0, 1, 0}, -{ 2, s_1_11, 0, 2, 0}, -{ 2, s_1_12, 0, 1, 0} +static const struct among a_1[13] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_1_1, -1, 2, 0}, +{ 2, s_1_2, -2, 2, 0}, +{ 2, s_1_3, -3, 2, 0}, +{ 2, s_1_4, -4, 2, 0}, +{ 2, s_1_5, -5, 1, 0}, +{ 2, s_1_6, -6, 2, 0}, +{ 2, s_1_7, -7, 2, 0}, +{ 2, s_1_8, -8, 2, 0}, +{ 2, s_1_9, -9, 2, 0}, +{ 2, s_1_10, -10, 1, 0}, +{ 2, s_1_11, -11, 2, 0}, +{ 2, s_1_12, -12, 1, 0} }; static const symbol s_2_0[2] = { 'e', 'd' }; static const symbol s_2_1[3] = { 'e', 'e', 'd' }; static const symbol s_2_2[3] = { 'i', 'n', 'g' }; - -static const struct among a_2[3] = -{ -{ 2, s_2_0, -1, 2, 0}, -{ 3, s_2_1, 0, 1, 0}, -{ 3, s_2_2, -1, 2, 0} +static const struct among a_2[3] = { +{ 2, s_2_0, 0, 2, 0}, +{ 3, s_2_1, -1, 1, 0}, +{ 3, s_2_2, 0, 2, 0} }; static const symbol s_3_0[4] = { 'a', 'n', 'c', 'i' }; @@ -106,29 +127,27 @@ static const symbol s_3_16[4] = { 'a', 't', 'o', 'r' }; static const symbol s_3_17[7] = { 'i', 'v', 'e', 'n', 'e', 's', 's' }; static const symbol s_3_18[7] = { 'f', 'u', 'l', 'n', 'e', 's', 's' }; static const symbol s_3_19[7] = { 'o', 'u', 's', 'n', 'e', 's', 's' }; - -static const struct among a_3[20] = -{ -{ 4, s_3_0, -1, 3, 0}, -{ 4, s_3_1, -1, 2, 0}, -{ 4, s_3_2, -1, 4, 0}, -{ 3, s_3_3, -1, 6, 0}, -{ 4, s_3_4, -1, 9, 0}, -{ 5, s_3_5, -1, 11, 0}, -{ 5, s_3_6, -1, 5, 0}, -{ 5, s_3_7, -1, 9, 0}, -{ 6, s_3_8, -1, 13, 0}, -{ 5, s_3_9, -1, 12, 0}, -{ 6, s_3_10, -1, 1, 0}, -{ 7, s_3_11, 10, 8, 0}, -{ 5, s_3_12, -1, 9, 0}, -{ 5, s_3_13, -1, 8, 0}, -{ 7, s_3_14, 13, 7, 0}, -{ 4, s_3_15, -1, 7, 0}, -{ 4, s_3_16, -1, 8, 0}, -{ 7, s_3_17, -1, 12, 0}, -{ 7, s_3_18, -1, 10, 0}, -{ 7, s_3_19, -1, 11, 0} +static const struct among a_3[20] = { +{ 4, s_3_0, 0, 3, 0}, +{ 4, s_3_1, 0, 2, 0}, +{ 4, s_3_2, 0, 4, 0}, +{ 3, s_3_3, 0, 6, 0}, +{ 4, s_3_4, 0, 9, 0}, +{ 5, s_3_5, 0, 11, 0}, +{ 5, s_3_6, 0, 5, 0}, +{ 5, s_3_7, 0, 9, 0}, +{ 6, s_3_8, 0, 13, 0}, +{ 5, s_3_9, 0, 12, 0}, +{ 6, s_3_10, 0, 1, 0}, +{ 7, s_3_11, -1, 8, 0}, +{ 5, s_3_12, 0, 9, 0}, +{ 5, s_3_13, 0, 8, 0}, +{ 7, s_3_14, -1, 7, 0}, +{ 4, s_3_15, 0, 7, 0}, +{ 4, s_3_16, 0, 8, 0}, +{ 7, s_3_17, 0, 12, 0}, +{ 7, s_3_18, 0, 10, 0}, +{ 7, s_3_19, 0, 11, 0} }; static const symbol s_4_0[5] = { 'i', 'c', 'a', 't', 'e' }; @@ -138,16 +157,14 @@ static const symbol s_4_3[5] = { 'i', 'c', 'i', 't', 'i' }; static const symbol s_4_4[4] = { 'i', 'c', 'a', 'l' }; static const symbol s_4_5[3] = { 'f', 'u', 'l' }; static const symbol s_4_6[4] = { 'n', 'e', 's', 's' }; - -static const struct among a_4[7] = -{ -{ 5, s_4_0, -1, 2, 0}, -{ 5, s_4_1, -1, 3, 0}, -{ 5, s_4_2, -1, 1, 0}, -{ 5, s_4_3, -1, 2, 0}, -{ 4, s_4_4, -1, 2, 0}, -{ 3, s_4_5, -1, 3, 0}, -{ 4, s_4_6, -1, 3, 0} +static const struct among a_4[7] = { +{ 5, s_4_0, 0, 2, 0}, +{ 5, s_4_1, 0, 3, 0}, +{ 5, s_4_2, 0, 1, 0}, +{ 5, s_4_3, 0, 2, 0}, +{ 4, s_4_4, 0, 2, 0}, +{ 3, s_4_5, 0, 3, 0}, +{ 4, s_4_6, 0, 3, 0} }; static const symbol s_5_0[2] = { 'i', 'c' }; @@ -169,94 +186,69 @@ static const symbol s_5_15[3] = { 'e', 'n', 't' }; static const symbol s_5_16[4] = { 'm', 'e', 'n', 't' }; static const symbol s_5_17[5] = { 'e', 'm', 'e', 'n', 't' }; static const symbol s_5_18[2] = { 'o', 'u' }; - -static const struct among a_5[19] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 4, s_5_2, -1, 1, 0}, -{ 4, s_5_3, -1, 1, 0}, -{ 4, s_5_4, -1, 1, 0}, -{ 3, s_5_5, -1, 1, 0}, -{ 3, s_5_6, -1, 1, 0}, -{ 3, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 1, 0}, -{ 2, s_5_9, -1, 1, 0}, -{ 3, s_5_10, -1, 1, 0}, -{ 3, s_5_11, -1, 2, 0}, -{ 2, s_5_12, -1, 1, 0}, -{ 3, s_5_13, -1, 1, 0}, -{ 3, s_5_14, -1, 1, 0}, -{ 3, s_5_15, -1, 1, 0}, -{ 4, s_5_16, 15, 1, 0}, -{ 5, s_5_17, 16, 1, 0}, -{ 2, s_5_18, -1, 1, 0} +static const struct among a_5[19] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 4, s_5_2, 0, 1, 0}, +{ 4, s_5_3, 0, 1, 0}, +{ 4, s_5_4, 0, 1, 0}, +{ 3, s_5_5, 0, 1, 0}, +{ 3, s_5_6, 0, 1, 0}, +{ 3, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 1, 0}, +{ 2, s_5_9, 0, 1, 0}, +{ 3, s_5_10, 0, 1, 0}, +{ 3, s_5_11, 0, 2, 0}, +{ 2, s_5_12, 0, 1, 0}, +{ 3, s_5_13, 0, 1, 0}, +{ 3, s_5_14, 0, 1, 0}, +{ 3, s_5_15, 0, 1, 0}, +{ 4, s_5_16, -1, 1, 0}, +{ 5, s_5_17, -1, 1, 0}, +{ 2, s_5_18, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1 }; static const unsigned char g_v_WXY[] = { 1, 17, 65, 208, 1 }; -static const symbol s_0[] = { 's', 's' }; -static const symbol s_1[] = { 'i' }; -static const symbol s_2[] = { 'e', 'e' }; -static const symbol s_3[] = { 'e' }; -static const symbol s_4[] = { 'e' }; -static const symbol s_5[] = { 'i' }; -static const symbol s_6[] = { 't', 'i', 'o', 'n' }; -static const symbol s_7[] = { 'e', 'n', 'c', 'e' }; -static const symbol s_8[] = { 'a', 'n', 'c', 'e' }; -static const symbol s_9[] = { 'a', 'b', 'l', 'e' }; -static const symbol s_10[] = { 'e', 'n', 't' }; -static const symbol s_11[] = { 'e' }; -static const symbol s_12[] = { 'i', 'z', 'e' }; -static const symbol s_13[] = { 'a', 't', 'e' }; -static const symbol s_14[] = { 'a', 'l' }; -static const symbol s_15[] = { 'f', 'u', 'l' }; -static const symbol s_16[] = { 'o', 'u', 's' }; -static const symbol s_17[] = { 'i', 'v', 'e' }; -static const symbol s_18[] = { 'b', 'l', 'e' }; -static const symbol s_19[] = { 'a', 'l' }; -static const symbol s_20[] = { 'i', 'c' }; -static const symbol s_21[] = { 'Y' }; -static const symbol s_22[] = { 'Y' }; -static const symbol s_23[] = { 'y' }; - static int r_shortv(struct SN_env * z) { if (out_grouping_b_U(z, g_v_WXY, 89, 121, 0)) return 0; if (in_grouping_b_U(z, g_v, 97, 121, 0)) return 0; - if (out_grouping_b_U(z, g_v, 97, 121, 0)) return 0; - return 1; + return !out_grouping_b_U(z, g_v, 97, 121, 0); } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_Step_1a(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] != 115) return 0; - among_var = find_among_b(z, a_0, 4); + among_var = find_among_b(z, a_0, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -268,70 +260,76 @@ static int r_Step_1b(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 100 && z->p[z->c - 1] != 103)) return 0; - among_var = find_among_b(z, a_2, 3); + among_var = find_among_b(z, a_2, 3, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 2: - { int m_test1 = z->l - z->c; - + { + int v_1 = z->l - z->c; { int ret = out_grouping_b_U(z, g_v, 97, 121, 1); if (ret < 0) return 0; z->c -= ret; } - z->c = z->l - m_test1; + z->c = z->l - v_1; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m_test2 = z->l - z->c; + { + int v_2 = z->l - z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((68514004 >> (z->p[z->c - 1] & 0x1f)) & 1)) among_var = 3; else - among_var = find_among_b(z, a_1, 13); - z->c = z->l - m_test2; + among_var = find_among_b(z, a_1, 13, 0); + z->c = z->l - v_2; } switch (among_var) { case 1: - { int ret; - { int saved_c = z->c; - ret = insert_s(z, z->c, z->c, 1, s_3); - z->c = saved_c; - } + { + int saved_c = z->c; + int ret = insert_s(z, z->c, z->c, 1, s_3); + z->c = saved_c; if (ret < 0) return ret; } break; case 2: z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - if (z->c != z->I[1]) return 0; - { int m_test3 = z->l - z->c; - { int ret = r_shortv(z); + if (z->c != ((SN_local *)z)->i_p1) return 0; + { + int v_3 = z->l - z->c; + { + int ret = r_shortv(z); if (ret <= 0) return ret; } - z->c = z->l - m_test3; + z->c = z->l - v_3; } - { int ret; - { int saved_c = z->c; - ret = insert_s(z, z->c, z->c, 1, s_4); - z->c = saved_c; - } + { + int saved_c = z->c; + int ret = insert_s(z, z->c, z->c, 1, s_4); + z->c = saved_c; if (ret < 0) return ret; } break; @@ -343,24 +341,24 @@ static int r_Step_1b(struct SN_env * z) { static int r_Step_1c(struct SN_env * z) { z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'Y') return 0; z->c--; - } -lab0: + } while (0); z->bra = z->c; - { int ret = out_grouping_b_U(z, g_v, 97, 121, 1); if (ret < 0) return 0; z->c -= ret; } - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } return 1; @@ -370,75 +368,89 @@ static int r_Step_2(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((815616 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_3, 20); + among_var = find_among_b(z, a_3, 20, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_6); + { + int ret = slice_from_s(z, 4, s_6); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_7); + { + int ret = slice_from_s(z, 4, s_7); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_8); + { + int ret = slice_from_s(z, 4, s_8); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_9); + { + int ret = slice_from_s(z, 4, s_9); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_10); + { + int ret = slice_from_s(z, 3, s_10); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_12); + { + int ret = slice_from_s(z, 3, s_12); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 3, s_13); + { + int ret = slice_from_s(z, 3, s_13); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 2, s_14); + { + int ret = slice_from_s(z, 2, s_14); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 3, s_15); + { + int ret = slice_from_s(z, 3, s_15); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 3, s_16); + { + int ret = slice_from_s(z, 3, s_16); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 3, s_17); + { + int ret = slice_from_s(z, 3, s_17); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 3, s_18); + { + int ret = slice_from_s(z, 3, s_18); if (ret < 0) return ret; } break; @@ -450,25 +462,29 @@ static int r_Step_3(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((528928 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_4, 7); + among_var = find_among_b(z, a_4, 7, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_19); + { + int ret = slice_from_s(z, 2, s_19); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_20); + { + int ret = slice_from_s(z, 2, s_20); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -480,30 +496,33 @@ static int r_Step_4(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((3961384 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_5, 19); + among_var = find_among_b(z, a_5, 19, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 't') return 0; z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -516,27 +535,32 @@ static int r_Step_5a(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; z->c--; z->bra = z->c; - - { int ret = r_R2(z); - if (ret == 0) goto lab1; - if (ret < 0) return ret; - } - goto lab0; -lab1: - { int ret = r_R1(z); - if (ret <= 0) return ret; - } - { int m1 = z->l - z->c; (void)m1; - { int ret = r_shortv(z); - if (ret == 0) goto lab2; + do { + { + int ret = r_R2(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - return 0; - lab2: - z->c = z->l - m1; - } -lab0: - { int ret = slice_del(z); + break; + lab0: + { + int ret = r_R1(z); + if (ret <= 0) return ret; + } + { + int v_1 = z->l - z->c; + { + int ret = r_shortv(z); + if (ret == 0) goto lab1; + if (ret < 0) return ret; + } + return 0; + lab1: + z->c = z->l - v_1; + } + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -547,178 +571,210 @@ static int r_Step_5b(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; z->c--; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } if (z->c <= z->lb || z->p[z->c - 1] != 'l') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int porter_UTF_8_stem(struct SN_env * z) { - z->I[2] = 0; - { int c1 = z->c; + int b_Y_found; + b_Y_found = 0; + { + int v_1 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab0; z->c++; z->ket = z->c; - { int ret = slice_from_s(z, 1, s_21); + { + int ret = slice_from_s(z, 1, s_21); if (ret < 0) return ret; } - z->I[2] = 1; + b_Y_found = 1; lab0: - z->c = c1; + z->c = v_1; } - { int c2 = z->c; - while(1) { - int c3 = z->c; - while(1) { - int c4 = z->c; + { + int v_2 = z->c; + while (1) { + int v_3 = z->c; + while (1) { + int v_4 = z->c; if (in_grouping_U(z, g_v, 97, 121, 0)) goto lab3; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'y') goto lab3; z->c++; z->ket = z->c; - z->c = c4; + z->c = v_4; break; lab3: - z->c = c4; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_4; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab2; z->c = ret; } } - { int ret = slice_from_s(z, 1, s_22); + { + int ret = slice_from_s(z, 1, s_22); if (ret < 0) return ret; } - z->I[2] = 1; + b_Y_found = 1; continue; lab2: - z->c = c3; + z->c = v_3; break; } - z->c = c2; + z->c = v_2; } - z->I[1] = z->l; - z->I[0] = z->l; - { int c5 = z->c; - + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_5 = z->c; { int ret = out_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 121, 1); if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab4: - z->c = c5; + z->c = v_5; } z->lb = z->c; z->c = z->l; - - { int m6 = z->l - z->c; (void)m6; - { int ret = r_Step_1a(z); + { + int v_6 = z->l - z->c; + { + int ret = r_Step_1a(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } - { int m7 = z->l - z->c; (void)m7; - { int ret = r_Step_1b(z); + { + int v_7 = z->l - z->c; + { + int ret = r_Step_1b(z); if (ret < 0) return ret; } - z->c = z->l - m7; + z->c = z->l - v_7; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_Step_1c(z); + { + int v_8 = z->l - z->c; + { + int ret = r_Step_1c(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } - { int m9 = z->l - z->c; (void)m9; - { int ret = r_Step_2(z); + { + int v_9 = z->l - z->c; + { + int ret = r_Step_2(z); if (ret < 0) return ret; } - z->c = z->l - m9; + z->c = z->l - v_9; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_Step_3(z); + { + int v_10 = z->l - z->c; + { + int ret = r_Step_3(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } - { int m11 = z->l - z->c; (void)m11; - { int ret = r_Step_4(z); + { + int v_11 = z->l - z->c; + { + int ret = r_Step_4(z); if (ret < 0) return ret; } - z->c = z->l - m11; + z->c = z->l - v_11; } - { int m12 = z->l - z->c; (void)m12; - { int ret = r_Step_5a(z); + { + int v_12 = z->l - z->c; + { + int ret = r_Step_5a(z); if (ret < 0) return ret; } - z->c = z->l - m12; + z->c = z->l - v_12; } - { int m13 = z->l - z->c; (void)m13; - { int ret = r_Step_5b(z); + { + int v_13 = z->l - z->c; + { + int ret = r_Step_5b(z); if (ret < 0) return ret; } - z->c = z->l - m13; + z->c = z->l - v_13; } z->c = z->lb; - { int c14 = z->c; - if (!(z->I[2])) goto lab5; - while(1) { - int c15 = z->c; - while(1) { - int c16 = z->c; + { + int v_14 = z->c; + if (!b_Y_found) goto lab5; + while (1) { + int v_15 = z->c; + while (1) { + int v_16 = z->c; z->bra = z->c; if (z->c == z->l || z->p[z->c] != 'Y') goto lab7; z->c++; z->ket = z->c; - z->c = c16; + z->c = v_16; break; lab7: - z->c = c16; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_16; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab6; z->c = ret; } } - { int ret = slice_from_s(z, 1, s_23); + { + int ret = slice_from_s(z, 1, s_23); if (ret < 0) return ret; } continue; lab6: - z->c = c15; + z->c = v_15; break; } lab5: - z->c = c14; + z->c = v_14; } return 1; } -extern struct SN_env * porter_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * porter_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void porter_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void porter_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_portuguese.c b/src/backend/snowball/libstemmer/stem_UTF_8_portuguese.c index 3ccf24cd735e9..b8074566212af 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_portuguese.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_portuguese.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from portuguese.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_portuguese.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int portuguese_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_residual_form(struct SN_env * z); static int r_residual_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); @@ -19,71 +33,62 @@ static int r_RV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * portuguese_UTF_8_create_env(void); -extern void portuguese_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a', '~' }; +static const symbol s_1[] = { 'o', '~' }; +static const symbol s_2[] = { 0xC3, 0xA3 }; +static const symbol s_3[] = { 0xC3, 0xB5 }; +static const symbol s_4[] = { 'l', 'o', 'g' }; +static const symbol s_5[] = { 'u' }; +static const symbol s_6[] = { 'e', 'n', 't', 'e' }; +static const symbol s_7[] = { 'a', 't' }; +static const symbol s_8[] = { 'a', 't' }; +static const symbol s_9[] = { 'i', 'r' }; +static const symbol s_10[] = { 'c' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 0xC3, 0xA3 }; static const symbol s_0_2[2] = { 0xC3, 0xB5 }; - -static const struct among a_0[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_0_1, 0, 1, 0}, -{ 2, s_0_2, 0, 2, 0} +static const struct among a_0[3] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_0_1, -1, 1, 0}, +{ 2, s_0_2, -2, 2, 0} }; static const symbol s_1_1[2] = { 'a', '~' }; static const symbol s_1_2[2] = { 'o', '~' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 2, s_1_1, 0, 1, 0}, -{ 2, s_1_2, 0, 2, 0} +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 2, s_1_1, -1, 1, 0}, +{ 2, s_1_2, -2, 2, 0} }; static const symbol s_2_0[2] = { 'i', 'c' }; static const symbol s_2_1[2] = { 'a', 'd' }; static const symbol s_2_2[2] = { 'o', 's' }; static const symbol s_2_3[2] = { 'i', 'v' }; - -static const struct among a_2[4] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 2, s_2_2, -1, -1, 0}, -{ 2, s_2_3, -1, 1, 0} +static const struct among a_2[4] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, 1, 0} }; static const symbol s_3_0[4] = { 'a', 'n', 't', 'e' }; static const symbol s_3_1[4] = { 'a', 'v', 'e', 'l' }; static const symbol s_3_2[5] = { 0xC3, 0xAD, 'v', 'e', 'l' }; - -static const struct among a_3[3] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0}, -{ 5, s_3_2, -1, 1, 0} +static const struct among a_3[3] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0}, +{ 5, s_3_2, 0, 1, 0} }; static const symbol s_4_0[2] = { 'i', 'c' }; static const symbol s_4_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_4_2[2] = { 'i', 'v' }; - -static const struct among a_4[3] = -{ -{ 2, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 1, 0}, -{ 2, s_4_2, -1, 1, 0} +static const struct among a_4[3] = { +{ 2, s_4_0, 0, 1, 0}, +{ 4, s_4_1, 0, 1, 0}, +{ 2, s_4_2, 0, 1, 0} }; static const symbol s_5_0[3] = { 'i', 'c', 'a' }; @@ -131,54 +136,52 @@ static const symbol s_5_41[4] = { 'o', 's', 'o', 's' }; static const symbol s_5_42[7] = { 'a', 'm', 'e', 'n', 't', 'o', 's' }; static const symbol s_5_43[7] = { 'i', 'm', 'e', 'n', 't', 'o', 's' }; static const symbol s_5_44[4] = { 'i', 'v', 'o', 's' }; - -static const struct among a_5[45] = -{ -{ 3, s_5_0, -1, 1, 0}, -{ 6, s_5_1, -1, 1, 0}, -{ 6, s_5_2, -1, 4, 0}, -{ 5, s_5_3, -1, 2, 0}, -{ 3, s_5_4, -1, 9, 0}, -{ 5, s_5_5, -1, 1, 0}, -{ 3, s_5_6, -1, 1, 0}, -{ 4, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 8, 0}, -{ 3, s_5_9, -1, 1, 0}, -{ 5, s_5_10, -1, 7, 0}, -{ 4, s_5_11, -1, 1, 0}, -{ 5, s_5_12, -1, 6, 0}, -{ 6, s_5_13, 12, 5, 0}, -{ 5, s_5_14, -1, 1, 0}, -{ 5, s_5_15, -1, 1, 0}, -{ 3, s_5_16, -1, 1, 0}, -{ 4, s_5_17, -1, 1, 0}, -{ 3, s_5_18, -1, 1, 0}, -{ 6, s_5_19, -1, 1, 0}, -{ 6, s_5_20, -1, 1, 0}, -{ 3, s_5_21, -1, 8, 0}, -{ 6, s_5_22, -1, 1, 0}, -{ 6, s_5_23, -1, 3, 0}, -{ 4, s_5_24, -1, 1, 0}, -{ 4, s_5_25, -1, 1, 0}, -{ 7, s_5_26, -1, 4, 0}, -{ 6, s_5_27, -1, 2, 0}, -{ 4, s_5_28, -1, 9, 0}, -{ 6, s_5_29, -1, 1, 0}, -{ 4, s_5_30, -1, 1, 0}, -{ 5, s_5_31, -1, 1, 0}, -{ 4, s_5_32, -1, 8, 0}, -{ 4, s_5_33, -1, 1, 0}, -{ 6, s_5_34, -1, 7, 0}, -{ 6, s_5_35, -1, 1, 0}, -{ 5, s_5_36, -1, 1, 0}, -{ 7, s_5_37, -1, 1, 0}, -{ 7, s_5_38, -1, 3, 0}, -{ 4, s_5_39, -1, 1, 0}, -{ 5, s_5_40, -1, 1, 0}, -{ 4, s_5_41, -1, 1, 0}, -{ 7, s_5_42, -1, 1, 0}, -{ 7, s_5_43, -1, 1, 0}, -{ 4, s_5_44, -1, 8, 0} +static const struct among a_5[45] = { +{ 3, s_5_0, 0, 1, 0}, +{ 6, s_5_1, 0, 1, 0}, +{ 6, s_5_2, 0, 4, 0}, +{ 5, s_5_3, 0, 2, 0}, +{ 3, s_5_4, 0, 9, 0}, +{ 5, s_5_5, 0, 1, 0}, +{ 3, s_5_6, 0, 1, 0}, +{ 4, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 8, 0}, +{ 3, s_5_9, 0, 1, 0}, +{ 5, s_5_10, 0, 7, 0}, +{ 4, s_5_11, 0, 1, 0}, +{ 5, s_5_12, 0, 6, 0}, +{ 6, s_5_13, -1, 5, 0}, +{ 5, s_5_14, 0, 1, 0}, +{ 5, s_5_15, 0, 1, 0}, +{ 3, s_5_16, 0, 1, 0}, +{ 4, s_5_17, 0, 1, 0}, +{ 3, s_5_18, 0, 1, 0}, +{ 6, s_5_19, 0, 1, 0}, +{ 6, s_5_20, 0, 1, 0}, +{ 3, s_5_21, 0, 8, 0}, +{ 6, s_5_22, 0, 1, 0}, +{ 6, s_5_23, 0, 3, 0}, +{ 4, s_5_24, 0, 1, 0}, +{ 4, s_5_25, 0, 1, 0}, +{ 7, s_5_26, 0, 4, 0}, +{ 6, s_5_27, 0, 2, 0}, +{ 4, s_5_28, 0, 9, 0}, +{ 6, s_5_29, 0, 1, 0}, +{ 4, s_5_30, 0, 1, 0}, +{ 5, s_5_31, 0, 1, 0}, +{ 4, s_5_32, 0, 8, 0}, +{ 4, s_5_33, 0, 1, 0}, +{ 6, s_5_34, 0, 7, 0}, +{ 6, s_5_35, 0, 1, 0}, +{ 5, s_5_36, 0, 1, 0}, +{ 7, s_5_37, 0, 1, 0}, +{ 7, s_5_38, 0, 3, 0}, +{ 4, s_5_39, 0, 1, 0}, +{ 5, s_5_40, 0, 1, 0}, +{ 4, s_5_41, 0, 1, 0}, +{ 7, s_5_42, 0, 1, 0}, +{ 7, s_5_43, 0, 1, 0}, +{ 4, s_5_44, 0, 8, 0} }; static const symbol s_6_0[3] = { 'a', 'd', 'a' }; @@ -301,129 +304,127 @@ static const symbol s_6_116[2] = { 'o', 'u' }; static const symbol s_6_117[4] = { 'a', 'r', 0xC3, 0xA1 }; static const symbol s_6_118[4] = { 'e', 'r', 0xC3, 0xA1 }; static const symbol s_6_119[4] = { 'i', 'r', 0xC3, 0xA1 }; - -static const struct among a_6[120] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 3, s_6_1, -1, 1, 0}, -{ 2, s_6_2, -1, 1, 0}, -{ 4, s_6_3, 2, 1, 0}, -{ 4, s_6_4, 2, 1, 0}, -{ 4, s_6_5, 2, 1, 0}, -{ 3, s_6_6, -1, 1, 0}, -{ 3, s_6_7, -1, 1, 0}, -{ 3, s_6_8, -1, 1, 0}, -{ 3, s_6_9, -1, 1, 0}, -{ 4, s_6_10, -1, 1, 0}, -{ 4, s_6_11, -1, 1, 0}, -{ 4, s_6_12, -1, 1, 0}, -{ 4, s_6_13, -1, 1, 0}, -{ 4, s_6_14, -1, 1, 0}, -{ 4, s_6_15, -1, 1, 0}, -{ 2, s_6_16, -1, 1, 0}, -{ 4, s_6_17, 16, 1, 0}, -{ 4, s_6_18, 16, 1, 0}, -{ 4, s_6_19, 16, 1, 0}, -{ 2, s_6_20, -1, 1, 0}, -{ 3, s_6_21, 20, 1, 0}, -{ 5, s_6_22, 21, 1, 0}, -{ 5, s_6_23, 21, 1, 0}, -{ 5, s_6_24, 21, 1, 0}, -{ 4, s_6_25, 20, 1, 0}, -{ 4, s_6_26, 20, 1, 0}, -{ 4, s_6_27, 20, 1, 0}, -{ 4, s_6_28, 20, 1, 0}, -{ 2, s_6_29, -1, 1, 0}, -{ 4, s_6_30, 29, 1, 0}, -{ 4, s_6_31, 29, 1, 0}, -{ 4, s_6_32, 29, 1, 0}, -{ 5, s_6_33, 29, 1, 0}, -{ 5, s_6_34, 29, 1, 0}, -{ 5, s_6_35, 29, 1, 0}, -{ 3, s_6_36, -1, 1, 0}, -{ 3, s_6_37, -1, 1, 0}, -{ 4, s_6_38, -1, 1, 0}, -{ 4, s_6_39, -1, 1, 0}, -{ 4, s_6_40, -1, 1, 0}, -{ 5, s_6_41, -1, 1, 0}, -{ 5, s_6_42, -1, 1, 0}, -{ 5, s_6_43, -1, 1, 0}, -{ 2, s_6_44, -1, 1, 0}, -{ 2, s_6_45, -1, 1, 0}, -{ 2, s_6_46, -1, 1, 0}, -{ 2, s_6_47, -1, 1, 0}, -{ 4, s_6_48, 47, 1, 0}, -{ 4, s_6_49, 47, 1, 0}, -{ 3, s_6_50, 47, 1, 0}, -{ 5, s_6_51, 50, 1, 0}, -{ 5, s_6_52, 50, 1, 0}, -{ 5, s_6_53, 50, 1, 0}, -{ 4, s_6_54, 47, 1, 0}, -{ 4, s_6_55, 47, 1, 0}, -{ 4, s_6_56, 47, 1, 0}, -{ 4, s_6_57, 47, 1, 0}, -{ 2, s_6_58, -1, 1, 0}, -{ 5, s_6_59, 58, 1, 0}, -{ 5, s_6_60, 58, 1, 0}, -{ 5, s_6_61, 58, 1, 0}, -{ 4, s_6_62, 58, 1, 0}, -{ 4, s_6_63, 58, 1, 0}, -{ 4, s_6_64, 58, 1, 0}, -{ 5, s_6_65, 58, 1, 0}, -{ 5, s_6_66, 58, 1, 0}, -{ 5, s_6_67, 58, 1, 0}, -{ 5, s_6_68, 58, 1, 0}, -{ 5, s_6_69, 58, 1, 0}, -{ 5, s_6_70, 58, 1, 0}, -{ 2, s_6_71, -1, 1, 0}, -{ 3, s_6_72, 71, 1, 0}, -{ 3, s_6_73, 71, 1, 0}, -{ 5, s_6_74, 73, 1, 0}, -{ 5, s_6_75, 73, 1, 0}, -{ 5, s_6_76, 73, 1, 0}, -{ 6, s_6_77, 73, 1, 0}, -{ 6, s_6_78, 73, 1, 0}, -{ 6, s_6_79, 73, 1, 0}, -{ 7, s_6_80, 73, 1, 0}, -{ 7, s_6_81, 73, 1, 0}, -{ 7, s_6_82, 73, 1, 0}, -{ 6, s_6_83, 73, 1, 0}, -{ 5, s_6_84, 73, 1, 0}, -{ 7, s_6_85, 84, 1, 0}, -{ 7, s_6_86, 84, 1, 0}, -{ 7, s_6_87, 84, 1, 0}, -{ 4, s_6_88, -1, 1, 0}, -{ 4, s_6_89, -1, 1, 0}, -{ 4, s_6_90, -1, 1, 0}, -{ 7, s_6_91, 90, 1, 0}, -{ 7, s_6_92, 90, 1, 0}, -{ 7, s_6_93, 90, 1, 0}, -{ 7, s_6_94, 90, 1, 0}, -{ 6, s_6_95, 90, 1, 0}, -{ 8, s_6_96, 95, 1, 0}, -{ 8, s_6_97, 95, 1, 0}, -{ 8, s_6_98, 95, 1, 0}, -{ 4, s_6_99, -1, 1, 0}, -{ 6, s_6_100, 99, 1, 0}, -{ 6, s_6_101, 99, 1, 0}, -{ 6, s_6_102, 99, 1, 0}, -{ 8, s_6_103, 99, 1, 0}, -{ 8, s_6_104, 99, 1, 0}, -{ 8, s_6_105, 99, 1, 0}, -{ 4, s_6_106, -1, 1, 0}, -{ 5, s_6_107, -1, 1, 0}, -{ 5, s_6_108, -1, 1, 0}, -{ 5, s_6_109, -1, 1, 0}, -{ 5, s_6_110, -1, 1, 0}, -{ 5, s_6_111, -1, 1, 0}, -{ 5, s_6_112, -1, 1, 0}, -{ 5, s_6_113, -1, 1, 0}, -{ 2, s_6_114, -1, 1, 0}, -{ 2, s_6_115, -1, 1, 0}, -{ 2, s_6_116, -1, 1, 0}, -{ 4, s_6_117, -1, 1, 0}, -{ 4, s_6_118, -1, 1, 0}, -{ 4, s_6_119, -1, 1, 0} +static const struct among a_6[120] = { +{ 3, s_6_0, 0, 1, 0}, +{ 3, s_6_1, 0, 1, 0}, +{ 2, s_6_2, 0, 1, 0}, +{ 4, s_6_3, -1, 1, 0}, +{ 4, s_6_4, -2, 1, 0}, +{ 4, s_6_5, -3, 1, 0}, +{ 3, s_6_6, 0, 1, 0}, +{ 3, s_6_7, 0, 1, 0}, +{ 3, s_6_8, 0, 1, 0}, +{ 3, s_6_9, 0, 1, 0}, +{ 4, s_6_10, 0, 1, 0}, +{ 4, s_6_11, 0, 1, 0}, +{ 4, s_6_12, 0, 1, 0}, +{ 4, s_6_13, 0, 1, 0}, +{ 4, s_6_14, 0, 1, 0}, +{ 4, s_6_15, 0, 1, 0}, +{ 2, s_6_16, 0, 1, 0}, +{ 4, s_6_17, -1, 1, 0}, +{ 4, s_6_18, -2, 1, 0}, +{ 4, s_6_19, -3, 1, 0}, +{ 2, s_6_20, 0, 1, 0}, +{ 3, s_6_21, -1, 1, 0}, +{ 5, s_6_22, -1, 1, 0}, +{ 5, s_6_23, -2, 1, 0}, +{ 5, s_6_24, -3, 1, 0}, +{ 4, s_6_25, -5, 1, 0}, +{ 4, s_6_26, -6, 1, 0}, +{ 4, s_6_27, -7, 1, 0}, +{ 4, s_6_28, -8, 1, 0}, +{ 2, s_6_29, 0, 1, 0}, +{ 4, s_6_30, -1, 1, 0}, +{ 4, s_6_31, -2, 1, 0}, +{ 4, s_6_32, -3, 1, 0}, +{ 5, s_6_33, -4, 1, 0}, +{ 5, s_6_34, -5, 1, 0}, +{ 5, s_6_35, -6, 1, 0}, +{ 3, s_6_36, 0, 1, 0}, +{ 3, s_6_37, 0, 1, 0}, +{ 4, s_6_38, 0, 1, 0}, +{ 4, s_6_39, 0, 1, 0}, +{ 4, s_6_40, 0, 1, 0}, +{ 5, s_6_41, 0, 1, 0}, +{ 5, s_6_42, 0, 1, 0}, +{ 5, s_6_43, 0, 1, 0}, +{ 2, s_6_44, 0, 1, 0}, +{ 2, s_6_45, 0, 1, 0}, +{ 2, s_6_46, 0, 1, 0}, +{ 2, s_6_47, 0, 1, 0}, +{ 4, s_6_48, -1, 1, 0}, +{ 4, s_6_49, -2, 1, 0}, +{ 3, s_6_50, -3, 1, 0}, +{ 5, s_6_51, -1, 1, 0}, +{ 5, s_6_52, -2, 1, 0}, +{ 5, s_6_53, -3, 1, 0}, +{ 4, s_6_54, -7, 1, 0}, +{ 4, s_6_55, -8, 1, 0}, +{ 4, s_6_56, -9, 1, 0}, +{ 4, s_6_57, -10, 1, 0}, +{ 2, s_6_58, 0, 1, 0}, +{ 5, s_6_59, -1, 1, 0}, +{ 5, s_6_60, -2, 1, 0}, +{ 5, s_6_61, -3, 1, 0}, +{ 4, s_6_62, -4, 1, 0}, +{ 4, s_6_63, -5, 1, 0}, +{ 4, s_6_64, -6, 1, 0}, +{ 5, s_6_65, -7, 1, 0}, +{ 5, s_6_66, -8, 1, 0}, +{ 5, s_6_67, -9, 1, 0}, +{ 5, s_6_68, -10, 1, 0}, +{ 5, s_6_69, -11, 1, 0}, +{ 5, s_6_70, -12, 1, 0}, +{ 2, s_6_71, 0, 1, 0}, +{ 3, s_6_72, -1, 1, 0}, +{ 3, s_6_73, -2, 1, 0}, +{ 5, s_6_74, -1, 1, 0}, +{ 5, s_6_75, -2, 1, 0}, +{ 5, s_6_76, -3, 1, 0}, +{ 6, s_6_77, -4, 1, 0}, +{ 6, s_6_78, -5, 1, 0}, +{ 6, s_6_79, -6, 1, 0}, +{ 7, s_6_80, -7, 1, 0}, +{ 7, s_6_81, -8, 1, 0}, +{ 7, s_6_82, -9, 1, 0}, +{ 6, s_6_83, -10, 1, 0}, +{ 5, s_6_84, -11, 1, 0}, +{ 7, s_6_85, -1, 1, 0}, +{ 7, s_6_86, -2, 1, 0}, +{ 7, s_6_87, -3, 1, 0}, +{ 4, s_6_88, 0, 1, 0}, +{ 4, s_6_89, 0, 1, 0}, +{ 4, s_6_90, 0, 1, 0}, +{ 7, s_6_91, -1, 1, 0}, +{ 7, s_6_92, -2, 1, 0}, +{ 7, s_6_93, -3, 1, 0}, +{ 7, s_6_94, -4, 1, 0}, +{ 6, s_6_95, -5, 1, 0}, +{ 8, s_6_96, -1, 1, 0}, +{ 8, s_6_97, -2, 1, 0}, +{ 8, s_6_98, -3, 1, 0}, +{ 4, s_6_99, 0, 1, 0}, +{ 6, s_6_100, -1, 1, 0}, +{ 6, s_6_101, -2, 1, 0}, +{ 6, s_6_102, -3, 1, 0}, +{ 8, s_6_103, -4, 1, 0}, +{ 8, s_6_104, -5, 1, 0}, +{ 8, s_6_105, -6, 1, 0}, +{ 4, s_6_106, 0, 1, 0}, +{ 5, s_6_107, 0, 1, 0}, +{ 5, s_6_108, 0, 1, 0}, +{ 5, s_6_109, 0, 1, 0}, +{ 5, s_6_110, 0, 1, 0}, +{ 5, s_6_111, 0, 1, 0}, +{ 5, s_6_112, 0, 1, 0}, +{ 5, s_6_113, 0, 1, 0}, +{ 2, s_6_114, 0, 1, 0}, +{ 2, s_6_115, 0, 1, 0}, +{ 2, s_6_116, 0, 1, 0}, +{ 4, s_6_117, 0, 1, 0}, +{ 4, s_6_118, 0, 1, 0}, +{ 4, s_6_119, 0, 1, 0} }; static const symbol s_7_0[1] = { 'a' }; @@ -433,66 +434,53 @@ static const symbol s_7_3[2] = { 'o', 's' }; static const symbol s_7_4[2] = { 0xC3, 0xA1 }; static const symbol s_7_5[2] = { 0xC3, 0xAD }; static const symbol s_7_6[2] = { 0xC3, 0xB3 }; - -static const struct among a_7[7] = -{ -{ 1, s_7_0, -1, 1, 0}, -{ 1, s_7_1, -1, 1, 0}, -{ 1, s_7_2, -1, 1, 0}, -{ 2, s_7_3, -1, 1, 0}, -{ 2, s_7_4, -1, 1, 0}, -{ 2, s_7_5, -1, 1, 0}, -{ 2, s_7_6, -1, 1, 0} +static const struct among a_7[7] = { +{ 1, s_7_0, 0, 1, 0}, +{ 1, s_7_1, 0, 1, 0}, +{ 1, s_7_2, 0, 1, 0}, +{ 2, s_7_3, 0, 1, 0}, +{ 2, s_7_4, 0, 1, 0}, +{ 2, s_7_5, 0, 1, 0}, +{ 2, s_7_6, 0, 1, 0} }; static const symbol s_8_0[1] = { 'e' }; static const symbol s_8_1[2] = { 0xC3, 0xA7 }; static const symbol s_8_2[2] = { 0xC3, 0xA9 }; static const symbol s_8_3[2] = { 0xC3, 0xAA }; - -static const struct among a_8[4] = -{ -{ 1, s_8_0, -1, 1, 0}, -{ 2, s_8_1, -1, 2, 0}, -{ 2, s_8_2, -1, 1, 0}, -{ 2, s_8_3, -1, 1, 0} +static const struct among a_8[4] = { +{ 1, s_8_0, 0, 1, 0}, +{ 2, s_8_1, 0, 2, 0}, +{ 2, s_8_2, 0, 1, 0}, +{ 2, s_8_3, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 19, 12, 2 }; -static const symbol s_0[] = { 'a', '~' }; -static const symbol s_1[] = { 'o', '~' }; -static const symbol s_2[] = { 0xC3, 0xA3 }; -static const symbol s_3[] = { 0xC3, 0xB5 }; -static const symbol s_4[] = { 'l', 'o', 'g' }; -static const symbol s_5[] = { 'u' }; -static const symbol s_6[] = { 'e', 'n', 't', 'e' }; -static const symbol s_7[] = { 'a', 't' }; -static const symbol s_8[] = { 'a', 't' }; -static const symbol s_9[] = { 'i', 'r' }; -static const symbol s_10[] = { 'c' }; - static int r_prelude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c + 1 >= z->l || (z->p[z->c + 1] != 163 && z->p[z->c + 1] != 181)) among_var = 3; else - among_var = find_among(z, a_0, 3); + among_var = find_among(z, a_0, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_1); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -500,122 +488,120 @@ static int r_prelude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_v, 97, 250, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping_U(z, g_v, 97, 250, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping_U(z, g_v, 97, 250, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping_U(z, g_v, 97, 250, 0)) goto lab2; { int ret = out_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping_U(z, g_v, 97, 250, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping_U(z, g_v, 97, 250, 0)) goto lab1; { int ret = in_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping_U(z, g_v, 97, 250, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping_U(z, g_v, 97, 250, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping_U(z, g_v, 97, 250, 0)) goto lab3; { int ret = out_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping_U(z, g_v, 97, 250, 0)) goto lab0; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 250, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c + 1 >= z->l || z->p[z->c + 1] != 126) among_var = 3; else - among_var = find_among(z, a_1, 3); + among_var = find_among(z, a_1, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_2); + { + int ret = slice_from_s(z, 2, s_2); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_3); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 3: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -623,94 +609,109 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((823330 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_5, 45); + among_var = find_among_b(z, a_5, 45, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_4); + { + int ret = slice_from_s(z, 3, s_4); if (ret < 0) return ret; } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 4, s_6); + { + int ret = slice_from_s(z, 4, s_6); if (ret < 0) return ret; } break; case 5: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m1; goto lab0; } - among_var = find_among_b(z, a_2, 4); - if (!among_var) { z->c = z->l - m1; goto lab0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_1; goto lab0; } + among_var = find_among_b(z, a_2, 4, 0); + if (!among_var) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } switch (among_var) { case 1: z->ket = z->c; - if (!(eq_s_b(z, 2, s_7))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_7))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -720,22 +721,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 6: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) { z->c = z->l - m2; goto lab1; } - if (!find_among_b(z, a_3, 3)) { z->c = z->l - m2; goto lab1; } + if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 101 && z->p[z->c - 1] != 108)) { z->c = z->l - v_2; goto lab1; } + if (!find_among_b(z, a_3, 3, 0)) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab1: @@ -743,22 +749,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 7: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m3; goto lab2; } - if (!find_among_b(z, a_4, 3)) { z->c = z->l - m3; goto lab2; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_3; goto lab2; } + if (!find_among_b(z, a_4, 3, 0)) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab2; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -766,21 +777,26 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_8))) { z->c = z->l - m4; goto lab3; } + if (!(eq_s_b(z, 2, s_8))) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab3: @@ -788,12 +804,14 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 9: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } if (z->c <= z->lb || z->p[z->c - 1] != 'e') return 0; z->c--; - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; @@ -802,29 +820,32 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (!find_among_b(z, a_6, 120)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_6, 120, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_residual_suffix(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_7, 7)) return 0; + if (!find_among_b(z, a_7, 7, 0)) return 0; z->bra = z->c; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -833,49 +854,56 @@ static int r_residual_suffix(struct SN_env * z) { static int r_residual_form(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_8, 4); + among_var = find_among_b(z, a_8, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab0; z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'g') goto lab1; + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'g') goto lab0; z->c--; - z->c = z->l - m_test2; + z->c = z->l - v_2; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'i') return 0; z->c--; z->bra = z->c; - { int m_test3 = z->l - z->c; + { + int v_3 = z->l - z->c; if (z->c <= z->lb || z->p[z->c - 1] != 'c') return 0; z->c--; - z->c = z->l - m_test3; + z->c = z->l - v_3; } - } - lab0: - { int ret = r_RV(z); + } while (0); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; @@ -884,86 +912,110 @@ static int r_residual_form(struct SN_env * z) { } extern int portuguese_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_prelude(z); + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab4; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab3; - lab4: - z->c = z->l - m5; - { int ret = r_verb_suffix(z); - if (ret == 0) goto lab2; + break; + lab2: + z->c = z->l - v_5; + { + int ret = r_verb_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - } - lab3: - z->c = z->l - m4; - { int m6 = z->l - z->c; (void)m6; + } while (0); + z->c = z->l - v_4; + { + int v_6 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab5; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab3; z->c--; z->bra = z->c; - { int m_test7 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab5; + { + int v_7 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'c') goto lab3; z->c--; - z->c = z->l - m_test7; + z->c = z->l - v_7; } - { int ret = r_RV(z); - if (ret == 0) goto lab5; + { + int ret = r_RV(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab5: - z->c = z->l - m6; + lab3: + z->c = z->l - v_6; } } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_residual_suffix(z); + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_residual_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_residual_form(z); + { + int v_8 = z->l - z->c; + { + int ret = r_residual_form(z); if (ret < 0) return ret; } - z->c = z->l - m8; + z->c = z->l - v_8; } z->c = z->lb; - { int c9 = z->c; - { int ret = r_postlude(z); + { + int v_9 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; } return 1; } -extern struct SN_env * portuguese_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * portuguese_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void portuguese_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void portuguese_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_romanian.c b/src/backend/snowball/libstemmer/stem_UTF_8_romanian.c index f0d688a9413f5..d938980f4f6ea 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_romanian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_romanian.c @@ -1,6 +1,20 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from romanian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_romanian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; + unsigned char b_standard_suffix_removed; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +23,7 @@ extern int romanian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_vowel_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_combo_suffix(struct SN_env * z); @@ -21,35 +36,43 @@ static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); static int r_prelude(struct SN_env * z); static int r_norm(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * romanian_UTF_8_create_env(void); -extern void romanian_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xC8, 0x99 }; +static const symbol s_1[] = { 0xC8, 0x9B }; +static const symbol s_2[] = { 'U' }; +static const symbol s_3[] = { 'I' }; +static const symbol s_4[] = { 'i' }; +static const symbol s_5[] = { 'u' }; +static const symbol s_6[] = { 'a' }; +static const symbol s_7[] = { 'e' }; +static const symbol s_8[] = { 'i' }; +static const symbol s_9[] = { 'a', 'b' }; +static const symbol s_10[] = { 'i' }; +static const symbol s_11[] = { 'a', 't' }; +static const symbol s_12[] = { 'a', 0xC8, 0x9B, 'i' }; +static const symbol s_13[] = { 'a', 'b', 'i', 'l' }; +static const symbol s_14[] = { 'i', 'b', 'i', 'l' }; +static const symbol s_15[] = { 'i', 'v' }; +static const symbol s_16[] = { 'i', 'c' }; +static const symbol s_17[] = { 'a', 't' }; +static const symbol s_18[] = { 'i', 't' }; +static const symbol s_19[] = { 0xC8, 0x9B }; +static const symbol s_20[] = { 't' }; +static const symbol s_21[] = { 'i', 's', 't' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[2] = { 0xC5, 0x9F }; static const symbol s_0_1[2] = { 0xC5, 0xA3 }; - -static const struct among a_0[2] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 2, s_0_1, -1, 2, 0} +static const struct among a_0[2] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 2, 0} }; static const symbol s_1_1[1] = { 'I' }; static const symbol s_1_2[1] = { 'U' }; - -static const struct among a_1[3] = -{ -{ 0, 0, -1, 3, 0}, -{ 1, s_1_1, 0, 1, 0}, -{ 1, s_1_2, 0, 2, 0} +static const struct among a_1[3] = { +{ 0, 0, 0, 3, 0}, +{ 1, s_1_1, -1, 1, 0}, +{ 1, s_1_2, -2, 2, 0} }; static const symbol s_2_0[2] = { 'e', 'a' }; @@ -68,25 +91,23 @@ static const symbol s_2_12[2] = { 'u', 'l' }; static const symbol s_2_13[4] = { 'e', 'l', 'o', 'r' }; static const symbol s_2_14[4] = { 'i', 'l', 'o', 'r' }; static const symbol s_2_15[5] = { 'i', 'i', 'l', 'o', 'r' }; - -static const struct among a_2[16] = -{ -{ 2, s_2_0, -1, 3, 0}, -{ 5, s_2_1, -1, 7, 0}, -{ 3, s_2_2, -1, 2, 0}, -{ 3, s_2_3, -1, 4, 0}, -{ 5, s_2_4, -1, 7, 0}, -{ 3, s_2_5, -1, 3, 0}, -{ 3, s_2_6, -1, 5, 0}, -{ 4, s_2_7, 6, 4, 0}, -{ 3, s_2_8, -1, 4, 0}, -{ 4, s_2_9, -1, 6, 0}, -{ 2, s_2_10, -1, 4, 0}, -{ 4, s_2_11, -1, 1, 0}, -{ 2, s_2_12, -1, 1, 0}, -{ 4, s_2_13, -1, 3, 0}, -{ 4, s_2_14, -1, 4, 0}, -{ 5, s_2_15, 14, 4, 0} +static const struct among a_2[16] = { +{ 2, s_2_0, 0, 3, 0}, +{ 5, s_2_1, 0, 7, 0}, +{ 3, s_2_2, 0, 2, 0}, +{ 3, s_2_3, 0, 4, 0}, +{ 5, s_2_4, 0, 7, 0}, +{ 3, s_2_5, 0, 3, 0}, +{ 3, s_2_6, 0, 5, 0}, +{ 4, s_2_7, -1, 4, 0}, +{ 3, s_2_8, 0, 4, 0}, +{ 4, s_2_9, 0, 6, 0}, +{ 2, s_2_10, 0, 4, 0}, +{ 4, s_2_11, 0, 1, 0}, +{ 2, s_2_12, 0, 1, 0}, +{ 4, s_2_13, 0, 3, 0}, +{ 4, s_2_14, 0, 4, 0}, +{ 5, s_2_15, -1, 4, 0} }; static const symbol s_3_0[5] = { 'i', 'c', 'a', 'l', 'a' }; @@ -135,55 +156,53 @@ static const symbol s_3_42[6] = { 'i', 'c', 'a', 'l', 0xC4, 0x83 }; static const symbol s_3_43[6] = { 'i', 'c', 'i', 'v', 0xC4, 0x83 }; static const symbol s_3_44[6] = { 'a', 't', 'i', 'v', 0xC4, 0x83 }; static const symbol s_3_45[6] = { 'i', 't', 'i', 'v', 0xC4, 0x83 }; - -static const struct among a_3[46] = -{ -{ 5, s_3_0, -1, 4, 0}, -{ 5, s_3_1, -1, 4, 0}, -{ 5, s_3_2, -1, 5, 0}, -{ 5, s_3_3, -1, 6, 0}, -{ 5, s_3_4, -1, 4, 0}, -{ 7, s_3_5, -1, 5, 0}, -{ 7, s_3_6, -1, 6, 0}, -{ 6, s_3_7, -1, 5, 0}, -{ 6, s_3_8, -1, 6, 0}, -{ 7, s_3_9, -1, 5, 0}, -{ 7, s_3_10, -1, 4, 0}, -{ 9, s_3_11, -1, 1, 0}, -{ 9, s_3_12, -1, 2, 0}, -{ 7, s_3_13, -1, 3, 0}, -{ 5, s_3_14, -1, 4, 0}, -{ 5, s_3_15, -1, 5, 0}, -{ 5, s_3_16, -1, 6, 0}, -{ 5, s_3_17, -1, 4, 0}, -{ 5, s_3_18, -1, 5, 0}, -{ 7, s_3_19, 18, 4, 0}, -{ 5, s_3_20, -1, 6, 0}, -{ 6, s_3_21, -1, 5, 0}, -{ 7, s_3_22, -1, 4, 0}, -{ 9, s_3_23, -1, 1, 0}, -{ 7, s_3_24, -1, 3, 0}, -{ 5, s_3_25, -1, 4, 0}, -{ 5, s_3_26, -1, 5, 0}, -{ 5, s_3_27, -1, 6, 0}, -{ 7, s_3_28, -1, 4, 0}, -{ 9, s_3_29, -1, 1, 0}, -{ 7, s_3_30, -1, 3, 0}, -{ 9, s_3_31, -1, 4, 0}, -{ 11, s_3_32, -1, 1, 0}, -{ 9, s_3_33, -1, 3, 0}, -{ 4, s_3_34, -1, 4, 0}, -{ 4, s_3_35, -1, 5, 0}, -{ 6, s_3_36, 35, 4, 0}, -{ 4, s_3_37, -1, 6, 0}, -{ 5, s_3_38, -1, 5, 0}, -{ 4, s_3_39, -1, 4, 0}, -{ 4, s_3_40, -1, 5, 0}, -{ 4, s_3_41, -1, 6, 0}, -{ 6, s_3_42, -1, 4, 0}, -{ 6, s_3_43, -1, 4, 0}, -{ 6, s_3_44, -1, 5, 0}, -{ 6, s_3_45, -1, 6, 0} +static const struct among a_3[46] = { +{ 5, s_3_0, 0, 4, 0}, +{ 5, s_3_1, 0, 4, 0}, +{ 5, s_3_2, 0, 5, 0}, +{ 5, s_3_3, 0, 6, 0}, +{ 5, s_3_4, 0, 4, 0}, +{ 7, s_3_5, 0, 5, 0}, +{ 7, s_3_6, 0, 6, 0}, +{ 6, s_3_7, 0, 5, 0}, +{ 6, s_3_8, 0, 6, 0}, +{ 7, s_3_9, 0, 5, 0}, +{ 7, s_3_10, 0, 4, 0}, +{ 9, s_3_11, 0, 1, 0}, +{ 9, s_3_12, 0, 2, 0}, +{ 7, s_3_13, 0, 3, 0}, +{ 5, s_3_14, 0, 4, 0}, +{ 5, s_3_15, 0, 5, 0}, +{ 5, s_3_16, 0, 6, 0}, +{ 5, s_3_17, 0, 4, 0}, +{ 5, s_3_18, 0, 5, 0}, +{ 7, s_3_19, -1, 4, 0}, +{ 5, s_3_20, 0, 6, 0}, +{ 6, s_3_21, 0, 5, 0}, +{ 7, s_3_22, 0, 4, 0}, +{ 9, s_3_23, 0, 1, 0}, +{ 7, s_3_24, 0, 3, 0}, +{ 5, s_3_25, 0, 4, 0}, +{ 5, s_3_26, 0, 5, 0}, +{ 5, s_3_27, 0, 6, 0}, +{ 7, s_3_28, 0, 4, 0}, +{ 9, s_3_29, 0, 1, 0}, +{ 7, s_3_30, 0, 3, 0}, +{ 9, s_3_31, 0, 4, 0}, +{ 11, s_3_32, 0, 1, 0}, +{ 9, s_3_33, 0, 3, 0}, +{ 4, s_3_34, 0, 4, 0}, +{ 4, s_3_35, 0, 5, 0}, +{ 6, s_3_36, -1, 4, 0}, +{ 4, s_3_37, 0, 6, 0}, +{ 5, s_3_38, 0, 5, 0}, +{ 4, s_3_39, 0, 4, 0}, +{ 4, s_3_40, 0, 5, 0}, +{ 4, s_3_41, 0, 6, 0}, +{ 6, s_3_42, 0, 4, 0}, +{ 6, s_3_43, 0, 4, 0}, +{ 6, s_3_44, 0, 5, 0}, +{ 6, s_3_45, 0, 6, 0} }; static const symbol s_4_0[3] = { 'i', 'c', 'a' }; @@ -248,71 +267,69 @@ static const symbol s_4_58[5] = { 'a', 'n', 't', 0xC4, 0x83 }; static const symbol s_4_59[5] = { 'i', 's', 't', 0xC4, 0x83 }; static const symbol s_4_60[4] = { 'u', 't', 0xC4, 0x83 }; static const symbol s_4_61[4] = { 'i', 'v', 0xC4, 0x83 }; - -static const struct among a_4[62] = -{ -{ 3, s_4_0, -1, 1, 0}, -{ 5, s_4_1, -1, 1, 0}, -{ 5, s_4_2, -1, 1, 0}, -{ 4, s_4_3, -1, 1, 0}, -{ 3, s_4_4, -1, 1, 0}, -{ 3, s_4_5, -1, 1, 0}, -{ 4, s_4_6, -1, 1, 0}, -{ 4, s_4_7, -1, 3, 0}, -{ 3, s_4_8, -1, 1, 0}, -{ 3, s_4_9, -1, 1, 0}, -{ 2, s_4_10, -1, 1, 0}, -{ 3, s_4_11, -1, 1, 0}, -{ 5, s_4_12, -1, 1, 0}, -{ 5, s_4_13, -1, 1, 0}, -{ 4, s_4_14, -1, 3, 0}, -{ 4, s_4_15, -1, 2, 0}, -{ 4, s_4_16, -1, 1, 0}, -{ 3, s_4_17, -1, 1, 0}, -{ 5, s_4_18, 17, 1, 0}, -{ 3, s_4_19, -1, 1, 0}, -{ 4, s_4_20, -1, 1, 0}, -{ 4, s_4_21, -1, 3, 0}, -{ 3, s_4_22, -1, 1, 0}, -{ 3, s_4_23, -1, 1, 0}, -{ 3, s_4_24, -1, 1, 0}, -{ 5, s_4_25, -1, 1, 0}, -{ 5, s_4_26, -1, 1, 0}, -{ 4, s_4_27, -1, 2, 0}, -{ 5, s_4_28, -1, 1, 0}, -{ 3, s_4_29, -1, 1, 0}, -{ 3, s_4_30, -1, 1, 0}, -{ 5, s_4_31, 30, 1, 0}, -{ 3, s_4_32, -1, 1, 0}, -{ 4, s_4_33, -1, 1, 0}, -{ 4, s_4_34, -1, 3, 0}, -{ 3, s_4_35, -1, 1, 0}, -{ 5, s_4_36, -1, 3, 0}, -{ 3, s_4_37, -1, 1, 0}, -{ 5, s_4_38, -1, 1, 0}, -{ 4, s_4_39, -1, 1, 0}, -{ 7, s_4_40, -1, 1, 0}, -{ 4, s_4_41, -1, 1, 0}, -{ 4, s_4_42, -1, 1, 0}, -{ 3, s_4_43, -1, 3, 0}, -{ 4, s_4_44, -1, 1, 0}, -{ 2, s_4_45, -1, 1, 0}, -{ 2, s_4_46, -1, 1, 0}, -{ 2, s_4_47, -1, 1, 0}, -{ 3, s_4_48, -1, 1, 0}, -{ 3, s_4_49, -1, 3, 0}, -{ 2, s_4_50, -1, 1, 0}, -{ 2, s_4_51, -1, 1, 0}, -{ 4, s_4_52, -1, 1, 0}, -{ 6, s_4_53, -1, 1, 0}, -{ 6, s_4_54, -1, 1, 0}, -{ 5, s_4_55, -1, 1, 0}, -{ 4, s_4_56, -1, 1, 0}, -{ 4, s_4_57, -1, 1, 0}, -{ 5, s_4_58, -1, 1, 0}, -{ 5, s_4_59, -1, 3, 0}, -{ 4, s_4_60, -1, 1, 0}, -{ 4, s_4_61, -1, 1, 0} +static const struct among a_4[62] = { +{ 3, s_4_0, 0, 1, 0}, +{ 5, s_4_1, 0, 1, 0}, +{ 5, s_4_2, 0, 1, 0}, +{ 4, s_4_3, 0, 1, 0}, +{ 3, s_4_4, 0, 1, 0}, +{ 3, s_4_5, 0, 1, 0}, +{ 4, s_4_6, 0, 1, 0}, +{ 4, s_4_7, 0, 3, 0}, +{ 3, s_4_8, 0, 1, 0}, +{ 3, s_4_9, 0, 1, 0}, +{ 2, s_4_10, 0, 1, 0}, +{ 3, s_4_11, 0, 1, 0}, +{ 5, s_4_12, 0, 1, 0}, +{ 5, s_4_13, 0, 1, 0}, +{ 4, s_4_14, 0, 3, 0}, +{ 4, s_4_15, 0, 2, 0}, +{ 4, s_4_16, 0, 1, 0}, +{ 3, s_4_17, 0, 1, 0}, +{ 5, s_4_18, -1, 1, 0}, +{ 3, s_4_19, 0, 1, 0}, +{ 4, s_4_20, 0, 1, 0}, +{ 4, s_4_21, 0, 3, 0}, +{ 3, s_4_22, 0, 1, 0}, +{ 3, s_4_23, 0, 1, 0}, +{ 3, s_4_24, 0, 1, 0}, +{ 5, s_4_25, 0, 1, 0}, +{ 5, s_4_26, 0, 1, 0}, +{ 4, s_4_27, 0, 2, 0}, +{ 5, s_4_28, 0, 1, 0}, +{ 3, s_4_29, 0, 1, 0}, +{ 3, s_4_30, 0, 1, 0}, +{ 5, s_4_31, -1, 1, 0}, +{ 3, s_4_32, 0, 1, 0}, +{ 4, s_4_33, 0, 1, 0}, +{ 4, s_4_34, 0, 3, 0}, +{ 3, s_4_35, 0, 1, 0}, +{ 5, s_4_36, 0, 3, 0}, +{ 3, s_4_37, 0, 1, 0}, +{ 5, s_4_38, 0, 1, 0}, +{ 4, s_4_39, 0, 1, 0}, +{ 7, s_4_40, 0, 1, 0}, +{ 4, s_4_41, 0, 1, 0}, +{ 4, s_4_42, 0, 1, 0}, +{ 3, s_4_43, 0, 3, 0}, +{ 4, s_4_44, 0, 1, 0}, +{ 2, s_4_45, 0, 1, 0}, +{ 2, s_4_46, 0, 1, 0}, +{ 2, s_4_47, 0, 1, 0}, +{ 3, s_4_48, 0, 1, 0}, +{ 3, s_4_49, 0, 3, 0}, +{ 2, s_4_50, 0, 1, 0}, +{ 2, s_4_51, 0, 1, 0}, +{ 4, s_4_52, 0, 1, 0}, +{ 6, s_4_53, 0, 1, 0}, +{ 6, s_4_54, 0, 1, 0}, +{ 5, s_4_55, 0, 1, 0}, +{ 4, s_4_56, 0, 1, 0}, +{ 4, s_4_57, 0, 1, 0}, +{ 5, s_4_58, 0, 1, 0}, +{ 5, s_4_59, 0, 3, 0}, +{ 4, s_4_60, 0, 1, 0}, +{ 4, s_4_61, 0, 1, 0} }; static const symbol s_5_0[2] = { 'e', 'a' }; @@ -409,103 +426,101 @@ static const symbol s_5_90[4] = { 'i', 'r', 0xC4, 0x83 }; static const symbol s_5_91[4] = { 'u', 'r', 0xC4, 0x83 }; static const symbol s_5_92[5] = { 0xC3, 0xA2, 'r', 0xC4, 0x83 }; static const symbol s_5_93[5] = { 'e', 'a', 'z', 0xC4, 0x83 }; - -static const struct among a_5[94] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 2, s_5_1, -1, 1, 0}, -{ 3, s_5_2, -1, 1, 0}, -{ 4, s_5_3, -1, 1, 0}, -{ 3, s_5_4, -1, 1, 0}, -{ 4, s_5_5, -1, 1, 0}, -{ 3, s_5_6, -1, 1, 0}, -{ 3, s_5_7, -1, 1, 0}, -{ 3, s_5_8, -1, 1, 0}, -{ 4, s_5_9, -1, 1, 0}, -{ 2, s_5_10, -1, 2, 0}, -{ 3, s_5_11, 10, 1, 0}, -{ 4, s_5_12, 10, 2, 0}, -{ 3, s_5_13, 10, 1, 0}, -{ 3, s_5_14, 10, 1, 0}, -{ 4, s_5_15, 10, 1, 0}, -{ 5, s_5_16, -1, 1, 0}, -{ 6, s_5_17, -1, 1, 0}, -{ 3, s_5_18, -1, 1, 0}, -{ 2, s_5_19, -1, 1, 0}, -{ 3, s_5_20, 19, 1, 0}, -{ 3, s_5_21, 19, 1, 0}, -{ 3, s_5_22, -1, 2, 0}, -{ 5, s_5_23, -1, 1, 0}, -{ 6, s_5_24, -1, 1, 0}, -{ 2, s_5_25, -1, 1, 0}, -{ 3, s_5_26, -1, 1, 0}, -{ 4, s_5_27, -1, 1, 0}, -{ 5, s_5_28, -1, 2, 0}, -{ 6, s_5_29, 28, 1, 0}, -{ 7, s_5_30, 28, 2, 0}, -{ 6, s_5_31, 28, 1, 0}, -{ 6, s_5_32, 28, 1, 0}, -{ 7, s_5_33, 28, 1, 0}, -{ 4, s_5_34, -1, 1, 0}, -{ 4, s_5_35, -1, 1, 0}, -{ 5, s_5_36, -1, 1, 0}, -{ 4, s_5_37, -1, 2, 0}, -{ 5, s_5_38, 37, 1, 0}, -{ 5, s_5_39, 37, 1, 0}, -{ 4, s_5_40, -1, 2, 0}, -{ 4, s_5_41, -1, 2, 0}, -{ 7, s_5_42, -1, 1, 0}, -{ 8, s_5_43, -1, 2, 0}, -{ 9, s_5_44, 43, 1, 0}, -{ 10, s_5_45, 43, 2, 0}, -{ 9, s_5_46, 43, 1, 0}, -{ 9, s_5_47, 43, 1, 0}, -{ 10, s_5_48, 43, 1, 0}, -{ 7, s_5_49, -1, 1, 0}, -{ 7, s_5_50, -1, 1, 0}, -{ 8, s_5_51, -1, 1, 0}, -{ 5, s_5_52, -1, 2, 0}, -{ 3, s_5_53, -1, 1, 0}, -{ 2, s_5_54, -1, 1, 0}, -{ 3, s_5_55, 54, 1, 0}, -{ 3, s_5_56, 54, 1, 0}, -{ 2, s_5_57, -1, 2, 0}, -{ 4, s_5_58, 57, 1, 0}, -{ 5, s_5_59, 57, 2, 0}, -{ 4, s_5_60, 57, 1, 0}, -{ 4, s_5_61, 57, 1, 0}, -{ 5, s_5_62, 57, 1, 0}, -{ 2, s_5_63, -1, 2, 0}, -{ 3, s_5_64, -1, 2, 0}, -{ 5, s_5_65, 64, 1, 0}, -{ 6, s_5_66, 64, 2, 0}, -{ 7, s_5_67, 66, 1, 0}, -{ 8, s_5_68, 66, 2, 0}, -{ 7, s_5_69, 66, 1, 0}, -{ 7, s_5_70, 66, 1, 0}, -{ 8, s_5_71, 66, 1, 0}, -{ 5, s_5_72, 64, 1, 0}, -{ 5, s_5_73, 64, 1, 0}, -{ 6, s_5_74, 64, 1, 0}, -{ 3, s_5_75, -1, 2, 0}, -{ 2, s_5_76, -1, 1, 0}, -{ 3, s_5_77, 76, 1, 0}, -{ 3, s_5_78, 76, 1, 0}, -{ 4, s_5_79, -1, 1, 0}, -{ 5, s_5_80, -1, 1, 0}, -{ 2, s_5_81, -1, 1, 0}, -{ 6, s_5_82, -1, 1, 0}, -{ 4, s_5_83, -1, 1, 0}, -{ 5, s_5_84, -1, 2, 0}, -{ 6, s_5_85, 84, 1, 0}, -{ 7, s_5_86, 84, 2, 0}, -{ 6, s_5_87, 84, 1, 0}, -{ 6, s_5_88, 84, 1, 0}, -{ 7, s_5_89, 84, 1, 0}, -{ 4, s_5_90, -1, 1, 0}, -{ 4, s_5_91, -1, 1, 0}, -{ 5, s_5_92, -1, 1, 0}, -{ 5, s_5_93, -1, 1, 0} +static const struct among a_5[94] = { +{ 2, s_5_0, 0, 1, 0}, +{ 2, s_5_1, 0, 1, 0}, +{ 3, s_5_2, 0, 1, 0}, +{ 4, s_5_3, 0, 1, 0}, +{ 3, s_5_4, 0, 1, 0}, +{ 4, s_5_5, 0, 1, 0}, +{ 3, s_5_6, 0, 1, 0}, +{ 3, s_5_7, 0, 1, 0}, +{ 3, s_5_8, 0, 1, 0}, +{ 4, s_5_9, 0, 1, 0}, +{ 2, s_5_10, 0, 2, 0}, +{ 3, s_5_11, -1, 1, 0}, +{ 4, s_5_12, -2, 2, 0}, +{ 3, s_5_13, -3, 1, 0}, +{ 3, s_5_14, -4, 1, 0}, +{ 4, s_5_15, -5, 1, 0}, +{ 5, s_5_16, 0, 1, 0}, +{ 6, s_5_17, 0, 1, 0}, +{ 3, s_5_18, 0, 1, 0}, +{ 2, s_5_19, 0, 1, 0}, +{ 3, s_5_20, -1, 1, 0}, +{ 3, s_5_21, -2, 1, 0}, +{ 3, s_5_22, 0, 2, 0}, +{ 5, s_5_23, 0, 1, 0}, +{ 6, s_5_24, 0, 1, 0}, +{ 2, s_5_25, 0, 1, 0}, +{ 3, s_5_26, 0, 1, 0}, +{ 4, s_5_27, 0, 1, 0}, +{ 5, s_5_28, 0, 2, 0}, +{ 6, s_5_29, -1, 1, 0}, +{ 7, s_5_30, -2, 2, 0}, +{ 6, s_5_31, -3, 1, 0}, +{ 6, s_5_32, -4, 1, 0}, +{ 7, s_5_33, -5, 1, 0}, +{ 4, s_5_34, 0, 1, 0}, +{ 4, s_5_35, 0, 1, 0}, +{ 5, s_5_36, 0, 1, 0}, +{ 4, s_5_37, 0, 2, 0}, +{ 5, s_5_38, -1, 1, 0}, +{ 5, s_5_39, -2, 1, 0}, +{ 4, s_5_40, 0, 2, 0}, +{ 4, s_5_41, 0, 2, 0}, +{ 7, s_5_42, 0, 1, 0}, +{ 8, s_5_43, 0, 2, 0}, +{ 9, s_5_44, -1, 1, 0}, +{ 10, s_5_45, -2, 2, 0}, +{ 9, s_5_46, -3, 1, 0}, +{ 9, s_5_47, -4, 1, 0}, +{ 10, s_5_48, -5, 1, 0}, +{ 7, s_5_49, 0, 1, 0}, +{ 7, s_5_50, 0, 1, 0}, +{ 8, s_5_51, 0, 1, 0}, +{ 5, s_5_52, 0, 2, 0}, +{ 3, s_5_53, 0, 1, 0}, +{ 2, s_5_54, 0, 1, 0}, +{ 3, s_5_55, -1, 1, 0}, +{ 3, s_5_56, -2, 1, 0}, +{ 2, s_5_57, 0, 2, 0}, +{ 4, s_5_58, -1, 1, 0}, +{ 5, s_5_59, -2, 2, 0}, +{ 4, s_5_60, -3, 1, 0}, +{ 4, s_5_61, -4, 1, 0}, +{ 5, s_5_62, -5, 1, 0}, +{ 2, s_5_63, 0, 2, 0}, +{ 3, s_5_64, 0, 2, 0}, +{ 5, s_5_65, -1, 1, 0}, +{ 6, s_5_66, -2, 2, 0}, +{ 7, s_5_67, -1, 1, 0}, +{ 8, s_5_68, -2, 2, 0}, +{ 7, s_5_69, -3, 1, 0}, +{ 7, s_5_70, -4, 1, 0}, +{ 8, s_5_71, -5, 1, 0}, +{ 5, s_5_72, -8, 1, 0}, +{ 5, s_5_73, -9, 1, 0}, +{ 6, s_5_74, -10, 1, 0}, +{ 3, s_5_75, 0, 2, 0}, +{ 2, s_5_76, 0, 1, 0}, +{ 3, s_5_77, -1, 1, 0}, +{ 3, s_5_78, -2, 1, 0}, +{ 4, s_5_79, 0, 1, 0}, +{ 5, s_5_80, 0, 1, 0}, +{ 2, s_5_81, 0, 1, 0}, +{ 6, s_5_82, 0, 1, 0}, +{ 4, s_5_83, 0, 1, 0}, +{ 5, s_5_84, 0, 2, 0}, +{ 6, s_5_85, -1, 1, 0}, +{ 7, s_5_86, -2, 2, 0}, +{ 6, s_5_87, -3, 1, 0}, +{ 6, s_5_88, -4, 1, 0}, +{ 7, s_5_89, -5, 1, 0}, +{ 4, s_5_90, 0, 1, 0}, +{ 4, s_5_91, 0, 1, 0}, +{ 5, s_5_92, 0, 1, 0}, +{ 5, s_5_93, 0, 1, 0} }; static const symbol s_6_0[1] = { 'a' }; @@ -513,238 +528,218 @@ static const symbol s_6_1[1] = { 'e' }; static const symbol s_6_2[2] = { 'i', 'e' }; static const symbol s_6_3[1] = { 'i' }; static const symbol s_6_4[2] = { 0xC4, 0x83 }; - -static const struct among a_6[5] = -{ -{ 1, s_6_0, -1, 1, 0}, -{ 1, s_6_1, -1, 1, 0}, -{ 2, s_6_2, 1, 1, 0}, -{ 1, s_6_3, -1, 1, 0}, -{ 2, s_6_4, -1, 1, 0} +static const struct among a_6[5] = { +{ 1, s_6_0, 0, 1, 0}, +{ 1, s_6_1, 0, 1, 0}, +{ 2, s_6_2, -1, 1, 0}, +{ 1, s_6_3, 0, 1, 0}, +{ 2, s_6_4, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 32, 0, 0, 4 }; -static const symbol s_0[] = { 0xC8, 0x99 }; -static const symbol s_1[] = { 0xC8, 0x9B }; -static const symbol s_2[] = { 'U' }; -static const symbol s_3[] = { 'I' }; -static const symbol s_4[] = { 'i' }; -static const symbol s_5[] = { 'u' }; -static const symbol s_6[] = { 'a' }; -static const symbol s_7[] = { 'e' }; -static const symbol s_8[] = { 'i' }; -static const symbol s_9[] = { 'a', 'b' }; -static const symbol s_10[] = { 'i' }; -static const symbol s_11[] = { 'a', 't' }; -static const symbol s_12[] = { 'a', 0xC8, 0x9B, 'i' }; -static const symbol s_13[] = { 'a', 'b', 'i', 'l' }; -static const symbol s_14[] = { 'i', 'b', 'i', 'l' }; -static const symbol s_15[] = { 'i', 'v' }; -static const symbol s_16[] = { 'i', 'c' }; -static const symbol s_17[] = { 'a', 't' }; -static const symbol s_18[] = { 'i', 't' }; -static const symbol s_19[] = { 0xC8, 0x9B }; -static const symbol s_20[] = { 't' }; -static const symbol s_21[] = { 'i', 's', 't' }; - static int r_norm(struct SN_env * z) { int among_var; - { int c1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; z->bra = z->c; if (z->c + 1 >= z->l || (z->p[z->c + 1] != 159 && z->p[z->c + 1] != 163)) goto lab2; - among_var = find_among(z, a_0, 2); + among_var = find_among(z, a_0, 2, 0); if (!among_var) goto lab2; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_0); + { + int ret = slice_from_s(z, 2, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_1); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; } - z->c = c3; + z->c = v_3; break; lab2: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } return 1; } static int r_prelude(struct SN_env * z) { - while(1) { - int c1 = z->c; - while(1) { - int c2 = z->c; + while (1) { + int v_1 = z->c; + while (1) { + int v_2 = z->c; if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab1; z->bra = z->c; - { int c3 = z->c; - if (z->c == z->l || z->p[z->c] != 'u') goto lab3; + do { + int v_3 = z->c; + if (z->c == z->l || z->p[z->c] != 'u') goto lab2; z->c++; z->ket = z->c; - if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab3; - { int ret = slice_from_s(z, 1, s_2); + if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab2; + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } - goto lab2; - lab3: - z->c = c3; + break; + lab2: + z->c = v_3; if (z->c == z->l || z->p[z->c] != 'i') goto lab1; z->c++; z->ket = z->c; if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab1; - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } - } - lab2: - z->c = c2; + } while (0); + z->c = v_2; break; lab1: - z->c = c2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_2; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping_U(z, g_v, 97, 259, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping_U(z, g_v, 97, 259, 0)) goto lab2; { int ret = out_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab1; { int ret = in_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping_U(z, g_v, 97, 259, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping_U(z, g_v, 97, 259, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping_U(z, g_v, 97, 259, 0)) goto lab3; { int ret = out_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping_U(z, g_v, 97, 259, 0)) goto lab0; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 259, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c >= z->l || (z->p[z->c + 0] != 73 && z->p[z->c + 0] != 85)) among_var = 3; else - among_var = find_among(z, a_1, 3); + among_var = find_among(z, a_1, 3, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 3: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -752,73 +747,82 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_step_0(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((266786 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_2, 16); + among_var = find_among_b(z, a_2, 16, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 5: - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; if (!(eq_s_b(z, 2, s_9))) goto lab0; return 0; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_11); + { + int ret = slice_from_s(z, 2, s_11); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 4, s_12); + { + int ret = slice_from_s(z, 4, s_12); if (ret < 0) return ret; } break; @@ -828,206 +832,245 @@ static int r_step_0(struct SN_env * z) { static int r_combo_suffix(struct SN_env * z) { int among_var; - { int m_test1 = z->l - z->c; + { + int v_1 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_3, 46); + among_var = find_among_b(z, a_3, 46, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_13); + { + int ret = slice_from_s(z, 4, s_13); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 4, s_14); + { + int ret = slice_from_s(z, 4, s_14); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 2, s_15); + { + int ret = slice_from_s(z, 2, s_15); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_16); + { + int ret = slice_from_s(z, 2, s_16); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 2, s_17); + { + int ret = slice_from_s(z, 2, s_17); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_18); + { + int ret = slice_from_s(z, 2, s_18); if (ret < 0) return ret; } break; } - z->I[3] = 1; - z->c = z->l - m_test1; + ((SN_local *)z)->b_standard_suffix_removed = 1; + z->c = z->l - v_1; } return 1; } static int r_standard_suffix(struct SN_env * z) { int among_var; - z->I[3] = 0; - while(1) { - int m1 = z->l - z->c; (void)m1; - { int ret = r_combo_suffix(z); + ((SN_local *)z)->b_standard_suffix_removed = 0; + while (1) { + int v_1 = z->l - z->c; + { + int ret = r_combo_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } continue; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; break; } z->ket = z->c; - among_var = find_among_b(z, a_4, 62); + among_var = find_among_b(z, a_4, 62, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (!(eq_s_b(z, 2, s_19))) return 0; z->bra = z->c; - { int ret = slice_from_s(z, 1, s_20); + { + int ret = slice_from_s(z, 1, s_20); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_21); + { + int ret = slice_from_s(z, 3, s_21); if (ret < 0) return ret; } break; } - z->I[3] = 1; + ((SN_local *)z)->b_standard_suffix_removed = 1; return 1; } static int r_verb_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - among_var = find_among_b(z, a_5, 94); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_5, 94, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; - if (out_grouping_b_U(z, g_v, 97, 259, 0)) goto lab1; - goto lab0; - lab1: - z->c = z->l - m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->lb = mlimit1; return 0; } + do { + int v_2 = z->l - z->c; + if (out_grouping_b_U(z, g_v, 97, 259, 0)) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->lb = v_1; return 0; } z->c--; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_vowel_suffix(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_6, 5)) return 0; + if (!find_among_b(z, a_6, 5, 0)) return 0; z->bra = z->c; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; } extern int romanian_UTF_8_stem(struct SN_env * z) { - - { int ret = r_norm(z); + { + int ret = r_norm(z); if (ret < 0) return ret; } - { int c1 = z->c; - { int ret = r_prelude(z); + { + int v_1 = z->c; + { + int ret = r_prelude(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_step_0(z); + { + int v_2 = z->l - z->c; + { + int ret = r_step_0(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_standard_suffix(z); + { + int v_3 = z->l - z->c; + { + int ret = r_standard_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - if (!(z->I[3])) goto lab2; - goto lab1; - lab2: - z->c = z->l - m5; - { int ret = r_verb_suffix(z); + { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + if (!((SN_local *)z)->b_standard_suffix_removed) goto lab1; + break; + lab1: + z->c = z->l - v_5; + { + int ret = r_verb_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m4; + z->c = z->l - v_4; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_vowel_suffix(z); + { + int v_6 = z->l - z->c; + { + int ret = r_vowel_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m6; + z->c = z->l - v_6; } z->c = z->lb; - { int c7 = z->c; - { int ret = r_postlude(z); + { + int v_7 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c7; + z->c = v_7; } return 1; } -extern struct SN_env * romanian_UTF_8_create_env(void) { return SN_create_env(0, 4); } +extern struct SN_env * romanian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_standard_suffix_removed = 0; + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void romanian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void romanian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_russian.c b/src/backend/snowball/libstemmer/stem_UTF_8_russian.c index 815af215e87a5..3133eb30c60d3 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_russian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_russian.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from russian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_russian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int russian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_tidy_up(struct SN_env * z); static int r_derivational(struct SN_env * z); static int r_noun(struct SN_env * z); @@ -19,18 +32,20 @@ static int r_adjective(struct SN_env * z); static int r_perfective_gerund(struct SN_env * z); static int r_R2(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * russian_UTF_8_create_env(void); -extern void russian_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xD0, 0xB0 }; +static const symbol s_1[] = { 0xD1, 0x8F }; +static const symbol s_2[] = { 0xD0, 0xB0 }; +static const symbol s_3[] = { 0xD1, 0x8F }; +static const symbol s_4[] = { 0xD0, 0xB0 }; +static const symbol s_5[] = { 0xD1, 0x8F }; +static const symbol s_6[] = { 0xD0, 0xBD }; +static const symbol s_7[] = { 0xD0, 0xBD }; +static const symbol s_8[] = { 0xD0, 0xBD }; +static const symbol s_9[] = { 0xD1, 0x91 }; +static const symbol s_10[] = { 0xD0, 0xB5 }; +static const symbol s_11[] = { 0xD0, 0xB8 }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[10] = { 0xD0, 0xB2, 0xD1, 0x88, 0xD0, 0xB8, 0xD1, 0x81, 0xD1, 0x8C }; static const symbol s_0_1[12] = { 0xD1, 0x8B, 0xD0, 0xB2, 0xD1, 0x88, 0xD0, 0xB8, 0xD1, 0x81, 0xD1, 0x8C }; static const symbol s_0_2[12] = { 0xD0, 0xB8, 0xD0, 0xB2, 0xD1, 0x88, 0xD0, 0xB8, 0xD1, 0x81, 0xD1, 0x8C }; @@ -40,18 +55,16 @@ static const symbol s_0_5[4] = { 0xD0, 0xB8, 0xD0, 0xB2 }; static const symbol s_0_6[6] = { 0xD0, 0xB2, 0xD1, 0x88, 0xD0, 0xB8 }; static const symbol s_0_7[8] = { 0xD1, 0x8B, 0xD0, 0xB2, 0xD1, 0x88, 0xD0, 0xB8 }; static const symbol s_0_8[8] = { 0xD0, 0xB8, 0xD0, 0xB2, 0xD1, 0x88, 0xD0, 0xB8 }; - -static const struct among a_0[9] = -{ -{ 10, s_0_0, -1, 1, 0}, -{ 12, s_0_1, 0, 2, 0}, -{ 12, s_0_2, 0, 2, 0}, -{ 2, s_0_3, -1, 1, 0}, -{ 4, s_0_4, 3, 2, 0}, -{ 4, s_0_5, 3, 2, 0}, -{ 6, s_0_6, -1, 1, 0}, -{ 8, s_0_7, 6, 2, 0}, -{ 8, s_0_8, 6, 2, 0} +static const struct among a_0[9] = { +{ 10, s_0_0, 0, 1, 0}, +{ 12, s_0_1, -1, 2, 0}, +{ 12, s_0_2, -2, 2, 0}, +{ 2, s_0_3, 0, 1, 0}, +{ 4, s_0_4, -1, 2, 0}, +{ 4, s_0_5, -2, 2, 0}, +{ 6, s_0_6, 0, 1, 0}, +{ 8, s_0_7, -1, 2, 0}, +{ 8, s_0_8, -2, 2, 0} }; static const symbol s_1_0[6] = { 0xD0, 0xB5, 0xD0, 0xBC, 0xD1, 0x83 }; @@ -80,35 +93,33 @@ static const symbol s_1_22[4] = { 0xD0, 0xB8, 0xD0, 0xBC }; static const symbol s_1_23[4] = { 0xD0, 0xBE, 0xD0, 0xBC }; static const symbol s_1_24[6] = { 0xD0, 0xB5, 0xD0, 0xB3, 0xD0, 0xBE }; static const symbol s_1_25[6] = { 0xD0, 0xBE, 0xD0, 0xB3, 0xD0, 0xBE }; - -static const struct among a_1[26] = -{ -{ 6, s_1_0, -1, 1, 0}, -{ 6, s_1_1, -1, 1, 0}, -{ 4, s_1_2, -1, 1, 0}, -{ 4, s_1_3, -1, 1, 0}, -{ 4, s_1_4, -1, 1, 0}, -{ 4, s_1_5, -1, 1, 0}, -{ 4, s_1_6, -1, 1, 0}, -{ 4, s_1_7, -1, 1, 0}, -{ 4, s_1_8, -1, 1, 0}, -{ 4, s_1_9, -1, 1, 0}, -{ 4, s_1_10, -1, 1, 0}, -{ 4, s_1_11, -1, 1, 0}, -{ 4, s_1_12, -1, 1, 0}, -{ 4, s_1_13, -1, 1, 0}, -{ 6, s_1_14, -1, 1, 0}, -{ 6, s_1_15, -1, 1, 0}, -{ 4, s_1_16, -1, 1, 0}, -{ 4, s_1_17, -1, 1, 0}, -{ 4, s_1_18, -1, 1, 0}, -{ 4, s_1_19, -1, 1, 0}, -{ 4, s_1_20, -1, 1, 0}, -{ 4, s_1_21, -1, 1, 0}, -{ 4, s_1_22, -1, 1, 0}, -{ 4, s_1_23, -1, 1, 0}, -{ 6, s_1_24, -1, 1, 0}, -{ 6, s_1_25, -1, 1, 0} +static const struct among a_1[26] = { +{ 6, s_1_0, 0, 1, 0}, +{ 6, s_1_1, 0, 1, 0}, +{ 4, s_1_2, 0, 1, 0}, +{ 4, s_1_3, 0, 1, 0}, +{ 4, s_1_4, 0, 1, 0}, +{ 4, s_1_5, 0, 1, 0}, +{ 4, s_1_6, 0, 1, 0}, +{ 4, s_1_7, 0, 1, 0}, +{ 4, s_1_8, 0, 1, 0}, +{ 4, s_1_9, 0, 1, 0}, +{ 4, s_1_10, 0, 1, 0}, +{ 4, s_1_11, 0, 1, 0}, +{ 4, s_1_12, 0, 1, 0}, +{ 4, s_1_13, 0, 1, 0}, +{ 6, s_1_14, 0, 1, 0}, +{ 6, s_1_15, 0, 1, 0}, +{ 4, s_1_16, 0, 1, 0}, +{ 4, s_1_17, 0, 1, 0}, +{ 4, s_1_18, 0, 1, 0}, +{ 4, s_1_19, 0, 1, 0}, +{ 4, s_1_20, 0, 1, 0}, +{ 4, s_1_21, 0, 1, 0}, +{ 4, s_1_22, 0, 1, 0}, +{ 4, s_1_23, 0, 1, 0}, +{ 6, s_1_24, 0, 1, 0}, +{ 6, s_1_25, 0, 1, 0} }; static const symbol s_2_0[4] = { 0xD0, 0xB2, 0xD1, 0x88 }; @@ -119,26 +130,22 @@ static const symbol s_2_4[4] = { 0xD1, 0x8E, 0xD1, 0x89 }; static const symbol s_2_5[6] = { 0xD1, 0x83, 0xD1, 0x8E, 0xD1, 0x89 }; static const symbol s_2_6[4] = { 0xD0, 0xB5, 0xD0, 0xBC }; static const symbol s_2_7[4] = { 0xD0, 0xBD, 0xD0, 0xBD }; - -static const struct among a_2[8] = -{ -{ 4, s_2_0, -1, 1, 0}, -{ 6, s_2_1, 0, 2, 0}, -{ 6, s_2_2, 0, 2, 0}, -{ 2, s_2_3, -1, 1, 0}, -{ 4, s_2_4, 3, 1, 0}, -{ 6, s_2_5, 4, 2, 0}, -{ 4, s_2_6, -1, 1, 0}, -{ 4, s_2_7, -1, 1, 0} +static const struct among a_2[8] = { +{ 4, s_2_0, 0, 1, 0}, +{ 6, s_2_1, -1, 2, 0}, +{ 6, s_2_2, -2, 2, 0}, +{ 2, s_2_3, 0, 1, 0}, +{ 4, s_2_4, -1, 1, 0}, +{ 6, s_2_5, -1, 2, 0}, +{ 4, s_2_6, 0, 1, 0}, +{ 4, s_2_7, 0, 1, 0} }; static const symbol s_3_0[4] = { 0xD1, 0x81, 0xD1, 0x8C }; static const symbol s_3_1[4] = { 0xD1, 0x81, 0xD1, 0x8F }; - -static const struct among a_3[2] = -{ -{ 4, s_3_0, -1, 1, 0}, -{ 4, s_3_1, -1, 1, 0} +static const struct among a_3[2] = { +{ 4, s_3_0, 0, 1, 0}, +{ 4, s_3_1, 0, 1, 0} }; static const symbol s_4_0[4] = { 0xD1, 0x8B, 0xD1, 0x82 }; @@ -187,55 +194,53 @@ static const symbol s_4_42[6] = { 0xD0, 0xB8, 0xD0, 0xBB, 0xD0, 0xBE }; static const symbol s_4_43[4] = { 0xD0, 0xBD, 0xD0, 0xBE }; static const symbol s_4_44[6] = { 0xD0, 0xB5, 0xD0, 0xBD, 0xD0, 0xBE }; static const symbol s_4_45[6] = { 0xD0, 0xBD, 0xD0, 0xBD, 0xD0, 0xBE }; - -static const struct among a_4[46] = -{ -{ 4, s_4_0, -1, 2, 0}, -{ 4, s_4_1, -1, 1, 0}, -{ 6, s_4_2, 1, 2, 0}, -{ 4, s_4_3, -1, 2, 0}, -{ 4, s_4_4, -1, 1, 0}, -{ 6, s_4_5, 4, 2, 0}, -{ 4, s_4_6, -1, 2, 0}, -{ 4, s_4_7, -1, 1, 0}, -{ 6, s_4_8, 7, 2, 0}, -{ 4, s_4_9, -1, 1, 0}, -{ 6, s_4_10, 9, 2, 0}, -{ 6, s_4_11, 9, 2, 0}, -{ 6, s_4_12, -1, 1, 0}, -{ 6, s_4_13, -1, 2, 0}, -{ 2, s_4_14, -1, 2, 0}, -{ 4, s_4_15, 14, 2, 0}, -{ 4, s_4_16, -1, 1, 0}, -{ 6, s_4_17, 16, 2, 0}, -{ 6, s_4_18, 16, 2, 0}, -{ 4, s_4_19, -1, 1, 0}, -{ 6, s_4_20, 19, 2, 0}, -{ 6, s_4_21, -1, 1, 0}, -{ 6, s_4_22, -1, 2, 0}, -{ 6, s_4_23, -1, 1, 0}, -{ 8, s_4_24, 23, 2, 0}, -{ 8, s_4_25, 23, 2, 0}, -{ 4, s_4_26, -1, 1, 0}, -{ 6, s_4_27, 26, 2, 0}, -{ 6, s_4_28, 26, 2, 0}, -{ 2, s_4_29, -1, 1, 0}, -{ 4, s_4_30, 29, 2, 0}, -{ 4, s_4_31, 29, 2, 0}, -{ 2, s_4_32, -1, 1, 0}, -{ 4, s_4_33, 32, 2, 0}, -{ 4, s_4_34, 32, 2, 0}, -{ 4, s_4_35, -1, 2, 0}, -{ 4, s_4_36, -1, 1, 0}, -{ 4, s_4_37, -1, 2, 0}, -{ 2, s_4_38, -1, 1, 0}, -{ 4, s_4_39, 38, 2, 0}, -{ 4, s_4_40, -1, 1, 0}, -{ 6, s_4_41, 40, 2, 0}, -{ 6, s_4_42, 40, 2, 0}, -{ 4, s_4_43, -1, 1, 0}, -{ 6, s_4_44, 43, 2, 0}, -{ 6, s_4_45, 43, 1, 0} +static const struct among a_4[46] = { +{ 4, s_4_0, 0, 2, 0}, +{ 4, s_4_1, 0, 1, 0}, +{ 6, s_4_2, -1, 2, 0}, +{ 4, s_4_3, 0, 2, 0}, +{ 4, s_4_4, 0, 1, 0}, +{ 6, s_4_5, -1, 2, 0}, +{ 4, s_4_6, 0, 2, 0}, +{ 4, s_4_7, 0, 1, 0}, +{ 6, s_4_8, -1, 2, 0}, +{ 4, s_4_9, 0, 1, 0}, +{ 6, s_4_10, -1, 2, 0}, +{ 6, s_4_11, -2, 2, 0}, +{ 6, s_4_12, 0, 1, 0}, +{ 6, s_4_13, 0, 2, 0}, +{ 2, s_4_14, 0, 2, 0}, +{ 4, s_4_15, -1, 2, 0}, +{ 4, s_4_16, 0, 1, 0}, +{ 6, s_4_17, -1, 2, 0}, +{ 6, s_4_18, -2, 2, 0}, +{ 4, s_4_19, 0, 1, 0}, +{ 6, s_4_20, -1, 2, 0}, +{ 6, s_4_21, 0, 1, 0}, +{ 6, s_4_22, 0, 2, 0}, +{ 6, s_4_23, 0, 1, 0}, +{ 8, s_4_24, -1, 2, 0}, +{ 8, s_4_25, -2, 2, 0}, +{ 4, s_4_26, 0, 1, 0}, +{ 6, s_4_27, -1, 2, 0}, +{ 6, s_4_28, -2, 2, 0}, +{ 2, s_4_29, 0, 1, 0}, +{ 4, s_4_30, -1, 2, 0}, +{ 4, s_4_31, -2, 2, 0}, +{ 2, s_4_32, 0, 1, 0}, +{ 4, s_4_33, -1, 2, 0}, +{ 4, s_4_34, -2, 2, 0}, +{ 4, s_4_35, 0, 2, 0}, +{ 4, s_4_36, 0, 1, 0}, +{ 4, s_4_37, 0, 2, 0}, +{ 2, s_4_38, 0, 1, 0}, +{ 4, s_4_39, -1, 2, 0}, +{ 4, s_4_40, 0, 1, 0}, +{ 6, s_4_41, -1, 2, 0}, +{ 6, s_4_42, -2, 2, 0}, +{ 4, s_4_43, 0, 1, 0}, +{ 6, s_4_44, -1, 2, 0}, +{ 6, s_4_45, -2, 1, 0} }; static const symbol s_5_0[2] = { 0xD1, 0x83 }; @@ -274,146 +279,126 @@ static const symbol s_5_32[4] = { 0xD0, 0xB5, 0xD0, 0xBC }; static const symbol s_5_33[6] = { 0xD0, 0xB8, 0xD0, 0xB5, 0xD0, 0xBC }; static const symbol s_5_34[4] = { 0xD0, 0xBE, 0xD0, 0xBC }; static const symbol s_5_35[2] = { 0xD0, 0xBE }; - -static const struct among a_5[36] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 6, s_5_2, 1, 1, 0}, -{ 4, s_5_3, -1, 1, 0}, -{ 2, s_5_4, -1, 1, 0}, -{ 2, s_5_5, -1, 1, 0}, -{ 2, s_5_6, -1, 1, 0}, -{ 4, s_5_7, 6, 1, 0}, -{ 4, s_5_8, 6, 1, 0}, -{ 2, s_5_9, -1, 1, 0}, -{ 4, s_5_10, 9, 1, 0}, -{ 4, s_5_11, 9, 1, 0}, -{ 2, s_5_12, -1, 1, 0}, -{ 4, s_5_13, -1, 1, 0}, -{ 4, s_5_14, -1, 1, 0}, -{ 2, s_5_15, -1, 1, 0}, -{ 4, s_5_16, 15, 1, 0}, -{ 4, s_5_17, 15, 1, 0}, -{ 2, s_5_18, -1, 1, 0}, -{ 4, s_5_19, 18, 1, 0}, -{ 4, s_5_20, 18, 1, 0}, -{ 6, s_5_21, 18, 1, 0}, -{ 8, s_5_22, 21, 1, 0}, -{ 6, s_5_23, 18, 1, 0}, -{ 2, s_5_24, -1, 1, 0}, -{ 4, s_5_25, 24, 1, 0}, -{ 6, s_5_26, 25, 1, 0}, -{ 4, s_5_27, 24, 1, 0}, -{ 4, s_5_28, 24, 1, 0}, -{ 4, s_5_29, -1, 1, 0}, -{ 6, s_5_30, 29, 1, 0}, -{ 4, s_5_31, -1, 1, 0}, -{ 4, s_5_32, -1, 1, 0}, -{ 6, s_5_33, 32, 1, 0}, -{ 4, s_5_34, -1, 1, 0}, -{ 2, s_5_35, -1, 1, 0} +static const struct among a_5[36] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 6, s_5_2, -1, 1, 0}, +{ 4, s_5_3, 0, 1, 0}, +{ 2, s_5_4, 0, 1, 0}, +{ 2, s_5_5, 0, 1, 0}, +{ 2, s_5_6, 0, 1, 0}, +{ 4, s_5_7, -1, 1, 0}, +{ 4, s_5_8, -2, 1, 0}, +{ 2, s_5_9, 0, 1, 0}, +{ 4, s_5_10, -1, 1, 0}, +{ 4, s_5_11, -2, 1, 0}, +{ 2, s_5_12, 0, 1, 0}, +{ 4, s_5_13, 0, 1, 0}, +{ 4, s_5_14, 0, 1, 0}, +{ 2, s_5_15, 0, 1, 0}, +{ 4, s_5_16, -1, 1, 0}, +{ 4, s_5_17, -2, 1, 0}, +{ 2, s_5_18, 0, 1, 0}, +{ 4, s_5_19, -1, 1, 0}, +{ 4, s_5_20, -2, 1, 0}, +{ 6, s_5_21, -3, 1, 0}, +{ 8, s_5_22, -1, 1, 0}, +{ 6, s_5_23, -5, 1, 0}, +{ 2, s_5_24, 0, 1, 0}, +{ 4, s_5_25, -1, 1, 0}, +{ 6, s_5_26, -1, 1, 0}, +{ 4, s_5_27, -3, 1, 0}, +{ 4, s_5_28, -4, 1, 0}, +{ 4, s_5_29, 0, 1, 0}, +{ 6, s_5_30, -1, 1, 0}, +{ 4, s_5_31, 0, 1, 0}, +{ 4, s_5_32, 0, 1, 0}, +{ 6, s_5_33, -1, 1, 0}, +{ 4, s_5_34, 0, 1, 0}, +{ 2, s_5_35, 0, 1, 0} }; static const symbol s_6_0[6] = { 0xD0, 0xBE, 0xD1, 0x81, 0xD1, 0x82 }; static const symbol s_6_1[8] = { 0xD0, 0xBE, 0xD1, 0x81, 0xD1, 0x82, 0xD1, 0x8C }; - -static const struct among a_6[2] = -{ -{ 6, s_6_0, -1, 1, 0}, -{ 8, s_6_1, -1, 1, 0} +static const struct among a_6[2] = { +{ 6, s_6_0, 0, 1, 0}, +{ 8, s_6_1, 0, 1, 0} }; static const symbol s_7_0[6] = { 0xD0, 0xB5, 0xD0, 0xB9, 0xD1, 0x88 }; static const symbol s_7_1[2] = { 0xD1, 0x8C }; static const symbol s_7_2[8] = { 0xD0, 0xB5, 0xD0, 0xB9, 0xD1, 0x88, 0xD0, 0xB5 }; static const symbol s_7_3[2] = { 0xD0, 0xBD }; - -static const struct among a_7[4] = -{ -{ 6, s_7_0, -1, 1, 0}, -{ 2, s_7_1, -1, 3, 0}, -{ 8, s_7_2, -1, 1, 0}, -{ 2, s_7_3, -1, 2, 0} +static const struct among a_7[4] = { +{ 6, s_7_0, 0, 1, 0}, +{ 2, s_7_1, 0, 3, 0}, +{ 8, s_7_2, 0, 1, 0}, +{ 2, s_7_3, 0, 2, 0} }; static const unsigned char g_v[] = { 33, 65, 8, 232 }; -static const symbol s_0[] = { 0xD0, 0xB0 }; -static const symbol s_1[] = { 0xD1, 0x8F }; -static const symbol s_2[] = { 0xD0, 0xB0 }; -static const symbol s_3[] = { 0xD1, 0x8F }; -static const symbol s_4[] = { 0xD0, 0xB0 }; -static const symbol s_5[] = { 0xD1, 0x8F }; -static const symbol s_6[] = { 0xD0, 0xBD }; -static const symbol s_7[] = { 0xD0, 0xBD }; -static const symbol s_8[] = { 0xD0, 0xBD }; -static const symbol s_9[] = { 0xD1, 0x91 }; -static const symbol s_10[] = { 0xD0, 0xB5 }; -static const symbol s_11[] = { 0xD0, 0xB8 }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; { int ret = out_grouping_U(z, g_v, 1072, 1103, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_pV = z->c; { int ret = in_grouping_U(z, g_v, 1072, 1103, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = out_grouping_U(z, g_v, 1072, 1103, 1); if (ret < 0) goto lab0; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 1072, 1103, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p2 = z->c; lab0: - z->c = c1; + z->c = v_1; } return 1; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_perfective_gerund(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_0, 9); + among_var = find_among_b(z, a_0, 9, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int m1 = z->l - z->c; (void)m1; - if (!(eq_s_b(z, 2, s_0))) goto lab1; - goto lab0; - lab1: - z->c = z->l - m1; + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 2, s_0))) goto lab0; + break; + lab0: + z->c = z->l - v_1; if (!(eq_s_b(z, 2, s_1))) return 0; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -423,9 +408,10 @@ static int r_perfective_gerund(struct SN_env * z) { static int r_adjective(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_1, 26)) return 0; + if (!find_among_b(z, a_1, 26, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -433,30 +419,34 @@ static int r_adjective(struct SN_env * z) { static int r_adjectival(struct SN_env * z) { int among_var; - { int ret = r_adjective(z); + { + int ret = r_adjective(z); if (ret <= 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_2, 8); - if (!among_var) { z->c = z->l - m1; goto lab0; } + among_var = find_among_b(z, a_2, 8, 0); + if (!among_var) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; - if (!(eq_s_b(z, 2, s_2))) goto lab2; - goto lab1; - lab2: - z->c = z->l - m2; - if (!(eq_s_b(z, 2, s_3))) { z->c = z->l - m1; goto lab0; } - } - lab1: - { int ret = slice_del(z); + do { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_2))) goto lab1; + break; + lab1: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_3))) { z->c = z->l - v_1; goto lab0; } + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -470,9 +460,10 @@ static int r_adjectival(struct SN_env * z) { static int r_reflexive(struct SN_env * z) { z->ket = z->c; if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 140 && z->p[z->c - 1] != 143)) return 0; - if (!find_among_b(z, a_3, 2)) return 0; + if (!find_among_b(z, a_3, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -481,25 +472,27 @@ static int r_reflexive(struct SN_env * z) { static int r_verb(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_4, 46); + among_var = find_among_b(z, a_4, 46, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int m1 = z->l - z->c; (void)m1; - if (!(eq_s_b(z, 2, s_4))) goto lab1; - goto lab0; - lab1: - z->c = z->l - m1; + do { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 2, s_4))) goto lab0; + break; + lab0: + z->c = z->l - v_1; if (!(eq_s_b(z, 2, s_5))) return 0; - } - lab0: - { int ret = slice_del(z); + } while (0); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -509,9 +502,10 @@ static int r_verb(struct SN_env * z) { static int r_noun(struct SN_env * z) { z->ket = z->c; - if (!find_among_b(z, a_5, 36)) return 0; + if (!find_among_b(z, a_5, 36, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -520,12 +514,14 @@ static int r_noun(struct SN_env * z) { static int r_derivational(struct SN_env * z) { z->ket = z->c; if (z->c - 5 <= z->lb || (z->p[z->c - 1] != 130 && z->p[z->c - 1] != 140)) return 0; - if (!find_among_b(z, a_6, 2)) return 0; + if (!find_among_b(z, a_6, 2, 0)) return 0; z->bra = z->c; - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -534,30 +530,34 @@ static int r_derivational(struct SN_env * z) { static int r_tidy_up(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_7, 4); + among_var = find_among_b(z, a_7, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; if (!(eq_s_b(z, 2, s_6))) return 0; z->bra = z->c; if (!(eq_s_b(z, 2, s_7))) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (!(eq_s_b(z, 2, s_8))) return 0; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -566,116 +566,139 @@ static int r_tidy_up(struct SN_env * z) { } extern int russian_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; z->bra = z->c; if (!(eq_s(z, 2, s_9))) goto lab2; z->ket = z->c; - z->c = c3; + z->c = v_3; break; lab2: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } } - { int ret = slice_from_s(z, 2, s_10); + { + int ret = slice_from_s(z, 2, s_10); if (ret < 0) return ret; } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - - { int mlimit4; - if (z->c < z->I[1]) return 0; - mlimit4 = z->lb; z->lb = z->I[1]; - { int m5 = z->l - z->c; (void)m5; - { int m6 = z->l - z->c; (void)m6; - { int ret = r_perfective_gerund(z); - if (ret == 0) goto lab5; + { + int v_4; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_4 = z->lb; z->lb = ((SN_local *)z)->i_pV; + { + int v_5 = z->l - z->c; + do { + int v_6 = z->l - z->c; + { + int ret = r_perfective_gerund(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m6; - { int m7 = z->l - z->c; (void)m7; - { int ret = r_reflexive(z); - if (ret == 0) { z->c = z->l - m7; goto lab6; } + break; + lab4: + z->c = z->l - v_6; + { + int v_7 = z->l - z->c; + { + int ret = r_reflexive(z); + if (ret == 0) { z->c = z->l - v_7; goto lab5; } if (ret < 0) return ret; } - lab6: + lab5: ; } - { int m8 = z->l - z->c; (void)m8; - { int ret = r_adjectival(z); - if (ret == 0) goto lab8; + do { + int v_8 = z->l - z->c; + { + int ret = r_adjectival(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m8; - { int ret = r_verb(z); - if (ret == 0) goto lab9; + break; + lab6: + z->c = z->l - v_8; + { + int ret = r_verb(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } - goto lab7; - lab9: - z->c = z->l - m8; - { int ret = r_noun(z); + break; + lab7: + z->c = z->l - v_8; + { + int ret = r_noun(z); if (ret == 0) goto lab3; if (ret < 0) return ret; } - } - lab7: - ; - } - lab4: + } while (0); + } while (0); lab3: - z->c = z->l - m5; + z->c = z->l - v_5; } - { int m9 = z->l - z->c; (void)m9; + { + int v_9 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_11))) { z->c = z->l - m9; goto lab10; } + if (!(eq_s_b(z, 2, s_11))) { z->c = z->l - v_9; goto lab8; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab10: + lab8: ; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_derivational(z); + { + int v_10 = z->l - z->c; + { + int ret = r_derivational(z); if (ret < 0) return ret; } - z->c = z->l - m10; + z->c = z->l - v_10; } - { int m11 = z->l - z->c; (void)m11; - { int ret = r_tidy_up(z); + { + int v_11 = z->l - z->c; + { + int ret = r_tidy_up(z); if (ret < 0) return ret; } - z->c = z->l - m11; + z->c = z->l - v_11; } - z->lb = mlimit4; + z->lb = v_4; } z->c = z->lb; return 1; } -extern struct SN_env * russian_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * russian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void russian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void russian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_serbian.c b/src/backend/snowball/libstemmer/stem_UTF_8_serbian.c index e20301b72ffb6..974f52a24885a 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_serbian.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_serbian.c @@ -1,6 +1,18 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from serbian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_serbian.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; + unsigned char b_no_diacritics; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +21,7 @@ extern int serbian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_Step_3(struct SN_env * z); static int r_Step_2(struct SN_env * z); static int r_Step_1(struct SN_env * z); @@ -16,4487 +29,6 @@ static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_prelude(struct SN_env * z); static int r_cyr_to_lat(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * serbian_UTF_8_create_env(void); -extern void serbian_UTF_8_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[2] = { 0xD0, 0xB0 }; -static const symbol s_0_1[2] = { 0xD0, 0xB1 }; -static const symbol s_0_2[2] = { 0xD0, 0xB2 }; -static const symbol s_0_3[2] = { 0xD0, 0xB3 }; -static const symbol s_0_4[2] = { 0xD0, 0xB4 }; -static const symbol s_0_5[2] = { 0xD0, 0xB5 }; -static const symbol s_0_6[2] = { 0xD0, 0xB6 }; -static const symbol s_0_7[2] = { 0xD0, 0xB7 }; -static const symbol s_0_8[2] = { 0xD0, 0xB8 }; -static const symbol s_0_9[2] = { 0xD0, 0xBA }; -static const symbol s_0_10[2] = { 0xD0, 0xBB }; -static const symbol s_0_11[2] = { 0xD0, 0xBC }; -static const symbol s_0_12[2] = { 0xD0, 0xBD }; -static const symbol s_0_13[2] = { 0xD0, 0xBE }; -static const symbol s_0_14[2] = { 0xD0, 0xBF }; -static const symbol s_0_15[2] = { 0xD1, 0x80 }; -static const symbol s_0_16[2] = { 0xD1, 0x81 }; -static const symbol s_0_17[2] = { 0xD1, 0x82 }; -static const symbol s_0_18[2] = { 0xD1, 0x83 }; -static const symbol s_0_19[2] = { 0xD1, 0x84 }; -static const symbol s_0_20[2] = { 0xD1, 0x85 }; -static const symbol s_0_21[2] = { 0xD1, 0x86 }; -static const symbol s_0_22[2] = { 0xD1, 0x87 }; -static const symbol s_0_23[2] = { 0xD1, 0x88 }; -static const symbol s_0_24[2] = { 0xD1, 0x92 }; -static const symbol s_0_25[2] = { 0xD1, 0x98 }; -static const symbol s_0_26[2] = { 0xD1, 0x99 }; -static const symbol s_0_27[2] = { 0xD1, 0x9A }; -static const symbol s_0_28[2] = { 0xD1, 0x9B }; -static const symbol s_0_29[2] = { 0xD1, 0x9F }; - -static const struct among a_0[30] = -{ -{ 2, s_0_0, -1, 1, 0}, -{ 2, s_0_1, -1, 2, 0}, -{ 2, s_0_2, -1, 3, 0}, -{ 2, s_0_3, -1, 4, 0}, -{ 2, s_0_4, -1, 5, 0}, -{ 2, s_0_5, -1, 7, 0}, -{ 2, s_0_6, -1, 8, 0}, -{ 2, s_0_7, -1, 9, 0}, -{ 2, s_0_8, -1, 10, 0}, -{ 2, s_0_9, -1, 12, 0}, -{ 2, s_0_10, -1, 13, 0}, -{ 2, s_0_11, -1, 15, 0}, -{ 2, s_0_12, -1, 16, 0}, -{ 2, s_0_13, -1, 18, 0}, -{ 2, s_0_14, -1, 19, 0}, -{ 2, s_0_15, -1, 20, 0}, -{ 2, s_0_16, -1, 21, 0}, -{ 2, s_0_17, -1, 22, 0}, -{ 2, s_0_18, -1, 24, 0}, -{ 2, s_0_19, -1, 25, 0}, -{ 2, s_0_20, -1, 26, 0}, -{ 2, s_0_21, -1, 27, 0}, -{ 2, s_0_22, -1, 28, 0}, -{ 2, s_0_23, -1, 30, 0}, -{ 2, s_0_24, -1, 6, 0}, -{ 2, s_0_25, -1, 11, 0}, -{ 2, s_0_26, -1, 14, 0}, -{ 2, s_0_27, -1, 17, 0}, -{ 2, s_0_28, -1, 23, 0}, -{ 2, s_0_29, -1, 29, 0} -}; - -static const symbol s_1_0[4] = { 'd', 'a', 'b', 'a' }; -static const symbol s_1_1[5] = { 'a', 'j', 'a', 'c', 'a' }; -static const symbol s_1_2[5] = { 'e', 'j', 'a', 'c', 'a' }; -static const symbol s_1_3[5] = { 'l', 'j', 'a', 'c', 'a' }; -static const symbol s_1_4[5] = { 'n', 'j', 'a', 'c', 'a' }; -static const symbol s_1_5[5] = { 'o', 'j', 'a', 'c', 'a' }; -static const symbol s_1_6[5] = { 'a', 'l', 'a', 'c', 'a' }; -static const symbol s_1_7[5] = { 'e', 'l', 'a', 'c', 'a' }; -static const symbol s_1_8[5] = { 'o', 'l', 'a', 'c', 'a' }; -static const symbol s_1_9[4] = { 'm', 'a', 'c', 'a' }; -static const symbol s_1_10[4] = { 'n', 'a', 'c', 'a' }; -static const symbol s_1_11[4] = { 'r', 'a', 'c', 'a' }; -static const symbol s_1_12[4] = { 's', 'a', 'c', 'a' }; -static const symbol s_1_13[4] = { 'v', 'a', 'c', 'a' }; -static const symbol s_1_14[5] = { 0xC5, 0xA1, 'a', 'c', 'a' }; -static const symbol s_1_15[4] = { 'a', 'o', 'c', 'a' }; -static const symbol s_1_16[5] = { 'a', 'c', 'a', 'k', 'a' }; -static const symbol s_1_17[5] = { 'a', 'j', 'a', 'k', 'a' }; -static const symbol s_1_18[5] = { 'o', 'j', 'a', 'k', 'a' }; -static const symbol s_1_19[5] = { 'a', 'n', 'a', 'k', 'a' }; -static const symbol s_1_20[5] = { 'a', 't', 'a', 'k', 'a' }; -static const symbol s_1_21[5] = { 'e', 't', 'a', 'k', 'a' }; -static const symbol s_1_22[5] = { 'i', 't', 'a', 'k', 'a' }; -static const symbol s_1_23[5] = { 'o', 't', 'a', 'k', 'a' }; -static const symbol s_1_24[5] = { 'u', 't', 'a', 'k', 'a' }; -static const symbol s_1_25[6] = { 'a', 0xC4, 0x8D, 'a', 'k', 'a' }; -static const symbol s_1_26[5] = { 'e', 's', 'a', 'm', 'a' }; -static const symbol s_1_27[5] = { 'i', 'z', 'a', 'm', 'a' }; -static const symbol s_1_28[6] = { 'j', 'a', 'c', 'i', 'm', 'a' }; -static const symbol s_1_29[6] = { 'n', 'i', 'c', 'i', 'm', 'a' }; -static const symbol s_1_30[6] = { 't', 'i', 'c', 'i', 'm', 'a' }; -static const symbol s_1_31[8] = { 't', 'e', 't', 'i', 'c', 'i', 'm', 'a' }; -static const symbol s_1_32[6] = { 'z', 'i', 'c', 'i', 'm', 'a' }; -static const symbol s_1_33[6] = { 'a', 't', 'c', 'i', 'm', 'a' }; -static const symbol s_1_34[6] = { 'u', 't', 'c', 'i', 'm', 'a' }; -static const symbol s_1_35[6] = { 0xC4, 0x8D, 'c', 'i', 'm', 'a' }; -static const symbol s_1_36[6] = { 'p', 'e', 's', 'i', 'm', 'a' }; -static const symbol s_1_37[6] = { 'i', 'n', 'z', 'i', 'm', 'a' }; -static const symbol s_1_38[6] = { 'l', 'o', 'z', 'i', 'm', 'a' }; -static const symbol s_1_39[6] = { 'm', 'e', 't', 'a', 'r', 'a' }; -static const symbol s_1_40[7] = { 'c', 'e', 'n', 't', 'a', 'r', 'a' }; -static const symbol s_1_41[6] = { 'i', 's', 't', 'a', 'r', 'a' }; -static const symbol s_1_42[5] = { 'e', 'k', 'a', 't', 'a' }; -static const symbol s_1_43[5] = { 'a', 'n', 'a', 't', 'a' }; -static const symbol s_1_44[6] = { 'n', 's', 't', 'a', 'v', 'a' }; -static const symbol s_1_45[7] = { 'k', 'u', 's', 't', 'a', 'v', 'a' }; -static const symbol s_1_46[4] = { 'a', 'j', 'a', 'c' }; -static const symbol s_1_47[4] = { 'e', 'j', 'a', 'c' }; -static const symbol s_1_48[4] = { 'l', 'j', 'a', 'c' }; -static const symbol s_1_49[4] = { 'n', 'j', 'a', 'c' }; -static const symbol s_1_50[5] = { 'a', 'n', 'j', 'a', 'c' }; -static const symbol s_1_51[4] = { 'o', 'j', 'a', 'c' }; -static const symbol s_1_52[4] = { 'a', 'l', 'a', 'c' }; -static const symbol s_1_53[4] = { 'e', 'l', 'a', 'c' }; -static const symbol s_1_54[4] = { 'o', 'l', 'a', 'c' }; -static const symbol s_1_55[3] = { 'm', 'a', 'c' }; -static const symbol s_1_56[3] = { 'n', 'a', 'c' }; -static const symbol s_1_57[3] = { 'r', 'a', 'c' }; -static const symbol s_1_58[3] = { 's', 'a', 'c' }; -static const symbol s_1_59[3] = { 'v', 'a', 'c' }; -static const symbol s_1_60[4] = { 0xC5, 0xA1, 'a', 'c' }; -static const symbol s_1_61[4] = { 'j', 'e', 'b', 'e' }; -static const symbol s_1_62[4] = { 'o', 'l', 'c', 'e' }; -static const symbol s_1_63[4] = { 'k', 'u', 's', 'e' }; -static const symbol s_1_64[4] = { 'r', 'a', 'v', 'e' }; -static const symbol s_1_65[4] = { 's', 'a', 'v', 'e' }; -static const symbol s_1_66[5] = { 0xC5, 0xA1, 'a', 'v', 'e' }; -static const symbol s_1_67[4] = { 'b', 'a', 'c', 'i' }; -static const symbol s_1_68[4] = { 'j', 'a', 'c', 'i' }; -static const symbol s_1_69[7] = { 't', 'v', 'e', 'n', 'i', 'c', 'i' }; -static const symbol s_1_70[5] = { 's', 'n', 'i', 'c', 'i' }; -static const symbol s_1_71[6] = { 't', 'e', 't', 'i', 'c', 'i' }; -static const symbol s_1_72[5] = { 'b', 'o', 'j', 'c', 'i' }; -static const symbol s_1_73[5] = { 'v', 'o', 'j', 'c', 'i' }; -static const symbol s_1_74[5] = { 'o', 'j', 's', 'c', 'i' }; -static const symbol s_1_75[4] = { 'a', 't', 'c', 'i' }; -static const symbol s_1_76[4] = { 'i', 't', 'c', 'i' }; -static const symbol s_1_77[4] = { 'u', 't', 'c', 'i' }; -static const symbol s_1_78[4] = { 0xC4, 0x8D, 'c', 'i' }; -static const symbol s_1_79[4] = { 'p', 'e', 's', 'i' }; -static const symbol s_1_80[4] = { 'i', 'n', 'z', 'i' }; -static const symbol s_1_81[4] = { 'l', 'o', 'z', 'i' }; -static const symbol s_1_82[4] = { 'a', 'c', 'a', 'k' }; -static const symbol s_1_83[4] = { 'u', 's', 'a', 'k' }; -static const symbol s_1_84[4] = { 'a', 't', 'a', 'k' }; -static const symbol s_1_85[4] = { 'e', 't', 'a', 'k' }; -static const symbol s_1_86[4] = { 'i', 't', 'a', 'k' }; -static const symbol s_1_87[4] = { 'o', 't', 'a', 'k' }; -static const symbol s_1_88[4] = { 'u', 't', 'a', 'k' }; -static const symbol s_1_89[5] = { 'a', 0xC4, 0x8D, 'a', 'k' }; -static const symbol s_1_90[5] = { 'u', 0xC5, 0xA1, 'a', 'k' }; -static const symbol s_1_91[4] = { 'i', 'z', 'a', 'm' }; -static const symbol s_1_92[5] = { 't', 'i', 'c', 'a', 'n' }; -static const symbol s_1_93[5] = { 'c', 'a', 'j', 'a', 'n' }; -static const symbol s_1_94[6] = { 0xC4, 0x8D, 'a', 'j', 'a', 'n' }; -static const symbol s_1_95[6] = { 'v', 'o', 'l', 'j', 'a', 'n' }; -static const symbol s_1_96[5] = { 'e', 's', 'k', 'a', 'n' }; -static const symbol s_1_97[4] = { 'a', 'l', 'a', 'n' }; -static const symbol s_1_98[5] = { 'b', 'i', 'l', 'a', 'n' }; -static const symbol s_1_99[5] = { 'g', 'i', 'l', 'a', 'n' }; -static const symbol s_1_100[5] = { 'n', 'i', 'l', 'a', 'n' }; -static const symbol s_1_101[5] = { 'r', 'i', 'l', 'a', 'n' }; -static const symbol s_1_102[5] = { 's', 'i', 'l', 'a', 'n' }; -static const symbol s_1_103[5] = { 't', 'i', 'l', 'a', 'n' }; -static const symbol s_1_104[6] = { 'a', 'v', 'i', 'l', 'a', 'n' }; -static const symbol s_1_105[5] = { 'l', 'a', 'r', 'a', 'n' }; -static const symbol s_1_106[4] = { 'e', 'r', 'a', 'n' }; -static const symbol s_1_107[4] = { 'a', 's', 'a', 'n' }; -static const symbol s_1_108[4] = { 'e', 's', 'a', 'n' }; -static const symbol s_1_109[5] = { 'd', 'u', 's', 'a', 'n' }; -static const symbol s_1_110[5] = { 'k', 'u', 's', 'a', 'n' }; -static const symbol s_1_111[4] = { 'a', 't', 'a', 'n' }; -static const symbol s_1_112[6] = { 'p', 'l', 'e', 't', 'a', 'n' }; -static const symbol s_1_113[5] = { 't', 'e', 't', 'a', 'n' }; -static const symbol s_1_114[5] = { 'a', 'n', 't', 'a', 'n' }; -static const symbol s_1_115[6] = { 'p', 'r', 'a', 'v', 'a', 'n' }; -static const symbol s_1_116[6] = { 's', 't', 'a', 'v', 'a', 'n' }; -static const symbol s_1_117[5] = { 's', 'i', 'v', 'a', 'n' }; -static const symbol s_1_118[5] = { 't', 'i', 'v', 'a', 'n' }; -static const symbol s_1_119[4] = { 'o', 'z', 'a', 'n' }; -static const symbol s_1_120[6] = { 't', 'i', 0xC4, 0x8D, 'a', 'n' }; -static const symbol s_1_121[5] = { 'a', 0xC5, 0xA1, 'a', 'n' }; -static const symbol s_1_122[6] = { 'd', 'u', 0xC5, 0xA1, 'a', 'n' }; -static const symbol s_1_123[5] = { 'm', 'e', 't', 'a', 'r' }; -static const symbol s_1_124[6] = { 'c', 'e', 'n', 't', 'a', 'r' }; -static const symbol s_1_125[5] = { 'i', 's', 't', 'a', 'r' }; -static const symbol s_1_126[4] = { 'e', 'k', 'a', 't' }; -static const symbol s_1_127[4] = { 'e', 'n', 'a', 't' }; -static const symbol s_1_128[4] = { 'o', 's', 'c', 'u' }; -static const symbol s_1_129[6] = { 'o', 0xC5, 0xA1, 0xC4, 0x87, 'u' }; - -static const struct among a_1[130] = -{ -{ 4, s_1_0, -1, 73, 0}, -{ 5, s_1_1, -1, 12, 0}, -{ 5, s_1_2, -1, 14, 0}, -{ 5, s_1_3, -1, 13, 0}, -{ 5, s_1_4, -1, 85, 0}, -{ 5, s_1_5, -1, 15, 0}, -{ 5, s_1_6, -1, 82, 0}, -{ 5, s_1_7, -1, 83, 0}, -{ 5, s_1_8, -1, 84, 0}, -{ 4, s_1_9, -1, 75, 0}, -{ 4, s_1_10, -1, 76, 0}, -{ 4, s_1_11, -1, 81, 0}, -{ 4, s_1_12, -1, 80, 0}, -{ 4, s_1_13, -1, 79, 0}, -{ 5, s_1_14, -1, 18, 0}, -{ 4, s_1_15, -1, 82, 0}, -{ 5, s_1_16, -1, 55, 0}, -{ 5, s_1_17, -1, 16, 0}, -{ 5, s_1_18, -1, 17, 0}, -{ 5, s_1_19, -1, 78, 0}, -{ 5, s_1_20, -1, 58, 0}, -{ 5, s_1_21, -1, 59, 0}, -{ 5, s_1_22, -1, 60, 0}, -{ 5, s_1_23, -1, 61, 0}, -{ 5, s_1_24, -1, 62, 0}, -{ 6, s_1_25, -1, 54, 0}, -{ 5, s_1_26, -1, 67, 0}, -{ 5, s_1_27, -1, 87, 0}, -{ 6, s_1_28, -1, 5, 0}, -{ 6, s_1_29, -1, 23, 0}, -{ 6, s_1_30, -1, 24, 0}, -{ 8, s_1_31, 30, 21, 0}, -{ 6, s_1_32, -1, 25, 0}, -{ 6, s_1_33, -1, 58, 0}, -{ 6, s_1_34, -1, 62, 0}, -{ 6, s_1_35, -1, 74, 0}, -{ 6, s_1_36, -1, 2, 0}, -{ 6, s_1_37, -1, 19, 0}, -{ 6, s_1_38, -1, 1, 0}, -{ 6, s_1_39, -1, 68, 0}, -{ 7, s_1_40, -1, 69, 0}, -{ 6, s_1_41, -1, 70, 0}, -{ 5, s_1_42, -1, 86, 0}, -{ 5, s_1_43, -1, 53, 0}, -{ 6, s_1_44, -1, 22, 0}, -{ 7, s_1_45, -1, 29, 0}, -{ 4, s_1_46, -1, 12, 0}, -{ 4, s_1_47, -1, 14, 0}, -{ 4, s_1_48, -1, 13, 0}, -{ 4, s_1_49, -1, 85, 0}, -{ 5, s_1_50, 49, 11, 0}, -{ 4, s_1_51, -1, 15, 0}, -{ 4, s_1_52, -1, 82, 0}, -{ 4, s_1_53, -1, 83, 0}, -{ 4, s_1_54, -1, 84, 0}, -{ 3, s_1_55, -1, 75, 0}, -{ 3, s_1_56, -1, 76, 0}, -{ 3, s_1_57, -1, 81, 0}, -{ 3, s_1_58, -1, 80, 0}, -{ 3, s_1_59, -1, 79, 0}, -{ 4, s_1_60, -1, 18, 0}, -{ 4, s_1_61, -1, 88, 0}, -{ 4, s_1_62, -1, 84, 0}, -{ 4, s_1_63, -1, 27, 0}, -{ 4, s_1_64, -1, 42, 0}, -{ 4, s_1_65, -1, 52, 0}, -{ 5, s_1_66, -1, 51, 0}, -{ 4, s_1_67, -1, 89, 0}, -{ 4, s_1_68, -1, 5, 0}, -{ 7, s_1_69, -1, 20, 0}, -{ 5, s_1_70, -1, 26, 0}, -{ 6, s_1_71, -1, 21, 0}, -{ 5, s_1_72, -1, 4, 0}, -{ 5, s_1_73, -1, 3, 0}, -{ 5, s_1_74, -1, 66, 0}, -{ 4, s_1_75, -1, 58, 0}, -{ 4, s_1_76, -1, 60, 0}, -{ 4, s_1_77, -1, 62, 0}, -{ 4, s_1_78, -1, 74, 0}, -{ 4, s_1_79, -1, 2, 0}, -{ 4, s_1_80, -1, 19, 0}, -{ 4, s_1_81, -1, 1, 0}, -{ 4, s_1_82, -1, 55, 0}, -{ 4, s_1_83, -1, 57, 0}, -{ 4, s_1_84, -1, 58, 0}, -{ 4, s_1_85, -1, 59, 0}, -{ 4, s_1_86, -1, 60, 0}, -{ 4, s_1_87, -1, 61, 0}, -{ 4, s_1_88, -1, 62, 0}, -{ 5, s_1_89, -1, 54, 0}, -{ 5, s_1_90, -1, 56, 0}, -{ 4, s_1_91, -1, 87, 0}, -{ 5, s_1_92, -1, 65, 0}, -{ 5, s_1_93, -1, 7, 0}, -{ 6, s_1_94, -1, 6, 0}, -{ 6, s_1_95, -1, 77, 0}, -{ 5, s_1_96, -1, 63, 0}, -{ 4, s_1_97, -1, 40, 0}, -{ 5, s_1_98, -1, 33, 0}, -{ 5, s_1_99, -1, 37, 0}, -{ 5, s_1_100, -1, 39, 0}, -{ 5, s_1_101, -1, 38, 0}, -{ 5, s_1_102, -1, 36, 0}, -{ 5, s_1_103, -1, 34, 0}, -{ 6, s_1_104, -1, 35, 0}, -{ 5, s_1_105, -1, 9, 0}, -{ 4, s_1_106, -1, 8, 0}, -{ 4, s_1_107, -1, 91, 0}, -{ 4, s_1_108, -1, 10, 0}, -{ 5, s_1_109, -1, 31, 0}, -{ 5, s_1_110, -1, 28, 0}, -{ 4, s_1_111, -1, 47, 0}, -{ 6, s_1_112, -1, 50, 0}, -{ 5, s_1_113, -1, 49, 0}, -{ 5, s_1_114, -1, 32, 0}, -{ 6, s_1_115, -1, 44, 0}, -{ 6, s_1_116, -1, 43, 0}, -{ 5, s_1_117, -1, 46, 0}, -{ 5, s_1_118, -1, 45, 0}, -{ 4, s_1_119, -1, 41, 0}, -{ 6, s_1_120, -1, 64, 0}, -{ 5, s_1_121, -1, 90, 0}, -{ 6, s_1_122, -1, 30, 0}, -{ 5, s_1_123, -1, 68, 0}, -{ 6, s_1_124, -1, 69, 0}, -{ 5, s_1_125, -1, 70, 0}, -{ 4, s_1_126, -1, 86, 0}, -{ 4, s_1_127, -1, 48, 0}, -{ 4, s_1_128, -1, 72, 0}, -{ 6, s_1_129, -1, 71, 0} -}; - -static const symbol s_2_0[3] = { 'a', 'c', 'a' }; -static const symbol s_2_1[3] = { 'e', 'c', 'a' }; -static const symbol s_2_2[3] = { 'u', 'c', 'a' }; -static const symbol s_2_3[2] = { 'g', 'a' }; -static const symbol s_2_4[5] = { 'a', 'c', 'e', 'g', 'a' }; -static const symbol s_2_5[5] = { 'e', 'c', 'e', 'g', 'a' }; -static const symbol s_2_6[5] = { 'u', 'c', 'e', 'g', 'a' }; -static const symbol s_2_7[8] = { 'a', 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_8[8] = { 'e', 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_9[8] = { 's', 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_10[9] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_11[6] = { 'k', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_12[7] = { 's', 'k', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_13[8] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_14[7] = { 'e', 'l', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_15[6] = { 'n', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_16[7] = { 'o', 's', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_17[7] = { 'a', 't', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_18[9] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_19[9] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_20[8] = { 'a', 's', 't', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_21[7] = { 'a', 'v', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_22[7] = { 'e', 'v', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_23[7] = { 'i', 'v', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_24[7] = { 'o', 'v', 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_25[8] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'g', 'a' }; -static const symbol s_2_26[6] = { 'a', 'n', 'j', 'e', 'g', 'a' }; -static const symbol s_2_27[6] = { 'e', 'n', 'j', 'e', 'g', 'a' }; -static const symbol s_2_28[6] = { 's', 'n', 'j', 'e', 'g', 'a' }; -static const symbol s_2_29[7] = { 0xC5, 0xA1, 'n', 'j', 'e', 'g', 'a' }; -static const symbol s_2_30[4] = { 'k', 'e', 'g', 'a' }; -static const symbol s_2_31[5] = { 's', 'k', 'e', 'g', 'a' }; -static const symbol s_2_32[6] = { 0xC5, 0xA1, 'k', 'e', 'g', 'a' }; -static const symbol s_2_33[5] = { 'e', 'l', 'e', 'g', 'a' }; -static const symbol s_2_34[4] = { 'n', 'e', 'g', 'a' }; -static const symbol s_2_35[5] = { 'a', 'n', 'e', 'g', 'a' }; -static const symbol s_2_36[5] = { 'e', 'n', 'e', 'g', 'a' }; -static const symbol s_2_37[5] = { 's', 'n', 'e', 'g', 'a' }; -static const symbol s_2_38[6] = { 0xC5, 0xA1, 'n', 'e', 'g', 'a' }; -static const symbol s_2_39[5] = { 'o', 's', 'e', 'g', 'a' }; -static const symbol s_2_40[5] = { 'a', 't', 'e', 'g', 'a' }; -static const symbol s_2_41[7] = { 'e', 'v', 'i', 't', 'e', 'g', 'a' }; -static const symbol s_2_42[7] = { 'o', 'v', 'i', 't', 'e', 'g', 'a' }; -static const symbol s_2_43[6] = { 'a', 's', 't', 'e', 'g', 'a' }; -static const symbol s_2_44[5] = { 'a', 'v', 'e', 'g', 'a' }; -static const symbol s_2_45[5] = { 'e', 'v', 'e', 'g', 'a' }; -static const symbol s_2_46[5] = { 'i', 'v', 'e', 'g', 'a' }; -static const symbol s_2_47[5] = { 'o', 'v', 'e', 'g', 'a' }; -static const symbol s_2_48[6] = { 'a', 0xC4, 0x87, 'e', 'g', 'a' }; -static const symbol s_2_49[6] = { 'e', 0xC4, 0x87, 'e', 'g', 'a' }; -static const symbol s_2_50[6] = { 'u', 0xC4, 0x87, 'e', 'g', 'a' }; -static const symbol s_2_51[6] = { 'o', 0xC5, 0xA1, 'e', 'g', 'a' }; -static const symbol s_2_52[5] = { 'a', 'c', 'o', 'g', 'a' }; -static const symbol s_2_53[5] = { 'e', 'c', 'o', 'g', 'a' }; -static const symbol s_2_54[5] = { 'u', 'c', 'o', 'g', 'a' }; -static const symbol s_2_55[6] = { 'a', 'n', 'j', 'o', 'g', 'a' }; -static const symbol s_2_56[6] = { 'e', 'n', 'j', 'o', 'g', 'a' }; -static const symbol s_2_57[6] = { 's', 'n', 'j', 'o', 'g', 'a' }; -static const symbol s_2_58[7] = { 0xC5, 0xA1, 'n', 'j', 'o', 'g', 'a' }; -static const symbol s_2_59[4] = { 'k', 'o', 'g', 'a' }; -static const symbol s_2_60[5] = { 's', 'k', 'o', 'g', 'a' }; -static const symbol s_2_61[6] = { 0xC5, 0xA1, 'k', 'o', 'g', 'a' }; -static const symbol s_2_62[4] = { 'l', 'o', 'g', 'a' }; -static const symbol s_2_63[5] = { 'e', 'l', 'o', 'g', 'a' }; -static const symbol s_2_64[4] = { 'n', 'o', 'g', 'a' }; -static const symbol s_2_65[6] = { 'c', 'i', 'n', 'o', 'g', 'a' }; -static const symbol s_2_66[7] = { 0xC4, 0x8D, 'i', 'n', 'o', 'g', 'a' }; -static const symbol s_2_67[5] = { 'o', 's', 'o', 'g', 'a' }; -static const symbol s_2_68[5] = { 'a', 't', 'o', 'g', 'a' }; -static const symbol s_2_69[7] = { 'e', 'v', 'i', 't', 'o', 'g', 'a' }; -static const symbol s_2_70[7] = { 'o', 'v', 'i', 't', 'o', 'g', 'a' }; -static const symbol s_2_71[6] = { 'a', 's', 't', 'o', 'g', 'a' }; -static const symbol s_2_72[5] = { 'a', 'v', 'o', 'g', 'a' }; -static const symbol s_2_73[5] = { 'e', 'v', 'o', 'g', 'a' }; -static const symbol s_2_74[5] = { 'i', 'v', 'o', 'g', 'a' }; -static const symbol s_2_75[5] = { 'o', 'v', 'o', 'g', 'a' }; -static const symbol s_2_76[6] = { 'a', 0xC4, 0x87, 'o', 'g', 'a' }; -static const symbol s_2_77[6] = { 'e', 0xC4, 0x87, 'o', 'g', 'a' }; -static const symbol s_2_78[6] = { 'u', 0xC4, 0x87, 'o', 'g', 'a' }; -static const symbol s_2_79[6] = { 'o', 0xC5, 0xA1, 'o', 'g', 'a' }; -static const symbol s_2_80[3] = { 'u', 'g', 'a' }; -static const symbol s_2_81[3] = { 'a', 'j', 'a' }; -static const symbol s_2_82[4] = { 'c', 'a', 'j', 'a' }; -static const symbol s_2_83[4] = { 'l', 'a', 'j', 'a' }; -static const symbol s_2_84[4] = { 'r', 'a', 'j', 'a' }; -static const symbol s_2_85[5] = { 0xC4, 0x87, 'a', 'j', 'a' }; -static const symbol s_2_86[5] = { 0xC4, 0x8D, 'a', 'j', 'a' }; -static const symbol s_2_87[5] = { 0xC4, 0x91, 'a', 'j', 'a' }; -static const symbol s_2_88[4] = { 'b', 'i', 'j', 'a' }; -static const symbol s_2_89[4] = { 'c', 'i', 'j', 'a' }; -static const symbol s_2_90[4] = { 'd', 'i', 'j', 'a' }; -static const symbol s_2_91[4] = { 'f', 'i', 'j', 'a' }; -static const symbol s_2_92[4] = { 'g', 'i', 'j', 'a' }; -static const symbol s_2_93[6] = { 'a', 'n', 'j', 'i', 'j', 'a' }; -static const symbol s_2_94[6] = { 'e', 'n', 'j', 'i', 'j', 'a' }; -static const symbol s_2_95[6] = { 's', 'n', 'j', 'i', 'j', 'a' }; -static const symbol s_2_96[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'a' }; -static const symbol s_2_97[4] = { 'k', 'i', 'j', 'a' }; -static const symbol s_2_98[5] = { 's', 'k', 'i', 'j', 'a' }; -static const symbol s_2_99[6] = { 0xC5, 0xA1, 'k', 'i', 'j', 'a' }; -static const symbol s_2_100[4] = { 'l', 'i', 'j', 'a' }; -static const symbol s_2_101[5] = { 'e', 'l', 'i', 'j', 'a' }; -static const symbol s_2_102[4] = { 'm', 'i', 'j', 'a' }; -static const symbol s_2_103[4] = { 'n', 'i', 'j', 'a' }; -static const symbol s_2_104[6] = { 'g', 'a', 'n', 'i', 'j', 'a' }; -static const symbol s_2_105[6] = { 'm', 'a', 'n', 'i', 'j', 'a' }; -static const symbol s_2_106[6] = { 'p', 'a', 'n', 'i', 'j', 'a' }; -static const symbol s_2_107[6] = { 'r', 'a', 'n', 'i', 'j', 'a' }; -static const symbol s_2_108[6] = { 't', 'a', 'n', 'i', 'j', 'a' }; -static const symbol s_2_109[4] = { 'p', 'i', 'j', 'a' }; -static const symbol s_2_110[4] = { 'r', 'i', 'j', 'a' }; -static const symbol s_2_111[6] = { 'r', 'a', 'r', 'i', 'j', 'a' }; -static const symbol s_2_112[4] = { 's', 'i', 'j', 'a' }; -static const symbol s_2_113[5] = { 'o', 's', 'i', 'j', 'a' }; -static const symbol s_2_114[4] = { 't', 'i', 'j', 'a' }; -static const symbol s_2_115[5] = { 'a', 't', 'i', 'j', 'a' }; -static const symbol s_2_116[7] = { 'e', 'v', 'i', 't', 'i', 'j', 'a' }; -static const symbol s_2_117[7] = { 'o', 'v', 'i', 't', 'i', 'j', 'a' }; -static const symbol s_2_118[5] = { 'o', 't', 'i', 'j', 'a' }; -static const symbol s_2_119[6] = { 'a', 's', 't', 'i', 'j', 'a' }; -static const symbol s_2_120[5] = { 'a', 'v', 'i', 'j', 'a' }; -static const symbol s_2_121[5] = { 'e', 'v', 'i', 'j', 'a' }; -static const symbol s_2_122[5] = { 'i', 'v', 'i', 'j', 'a' }; -static const symbol s_2_123[5] = { 'o', 'v', 'i', 'j', 'a' }; -static const symbol s_2_124[4] = { 'z', 'i', 'j', 'a' }; -static const symbol s_2_125[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'a' }; -static const symbol s_2_126[5] = { 0xC5, 0xBE, 'i', 'j', 'a' }; -static const symbol s_2_127[4] = { 'a', 'n', 'j', 'a' }; -static const symbol s_2_128[4] = { 'e', 'n', 'j', 'a' }; -static const symbol s_2_129[4] = { 's', 'n', 'j', 'a' }; -static const symbol s_2_130[5] = { 0xC5, 0xA1, 'n', 'j', 'a' }; -static const symbol s_2_131[2] = { 'k', 'a' }; -static const symbol s_2_132[3] = { 's', 'k', 'a' }; -static const symbol s_2_133[4] = { 0xC5, 0xA1, 'k', 'a' }; -static const symbol s_2_134[3] = { 'a', 'l', 'a' }; -static const symbol s_2_135[5] = { 'a', 'c', 'a', 'l', 'a' }; -static const symbol s_2_136[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'a' }; -static const symbol s_2_137[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'a' }; -static const symbol s_2_138[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'a' }; -static const symbol s_2_139[5] = { 'i', 'j', 'a', 'l', 'a' }; -static const symbol s_2_140[6] = { 'i', 'n', 'j', 'a', 'l', 'a' }; -static const symbol s_2_141[4] = { 'n', 'a', 'l', 'a' }; -static const symbol s_2_142[5] = { 'i', 'r', 'a', 'l', 'a' }; -static const symbol s_2_143[5] = { 'u', 'r', 'a', 'l', 'a' }; -static const symbol s_2_144[4] = { 't', 'a', 'l', 'a' }; -static const symbol s_2_145[6] = { 'a', 's', 't', 'a', 'l', 'a' }; -static const symbol s_2_146[6] = { 'i', 's', 't', 'a', 'l', 'a' }; -static const symbol s_2_147[6] = { 'o', 's', 't', 'a', 'l', 'a' }; -static const symbol s_2_148[5] = { 'a', 'v', 'a', 'l', 'a' }; -static const symbol s_2_149[5] = { 'e', 'v', 'a', 'l', 'a' }; -static const symbol s_2_150[5] = { 'i', 'v', 'a', 'l', 'a' }; -static const symbol s_2_151[5] = { 'o', 'v', 'a', 'l', 'a' }; -static const symbol s_2_152[5] = { 'u', 'v', 'a', 'l', 'a' }; -static const symbol s_2_153[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'a' }; -static const symbol s_2_154[3] = { 'e', 'l', 'a' }; -static const symbol s_2_155[3] = { 'i', 'l', 'a' }; -static const symbol s_2_156[5] = { 'a', 'c', 'i', 'l', 'a' }; -static const symbol s_2_157[6] = { 'l', 'u', 'c', 'i', 'l', 'a' }; -static const symbol s_2_158[4] = { 'n', 'i', 'l', 'a' }; -static const symbol s_2_159[8] = { 'a', 's', 't', 'a', 'n', 'i', 'l', 'a' }; -static const symbol s_2_160[8] = { 'i', 's', 't', 'a', 'n', 'i', 'l', 'a' }; -static const symbol s_2_161[8] = { 'o', 's', 't', 'a', 'n', 'i', 'l', 'a' }; -static const symbol s_2_162[6] = { 'r', 'o', 's', 'i', 'l', 'a' }; -static const symbol s_2_163[6] = { 'j', 'e', 't', 'i', 'l', 'a' }; -static const symbol s_2_164[5] = { 'o', 'z', 'i', 'l', 'a' }; -static const symbol s_2_165[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'a' }; -static const symbol s_2_166[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'a' }; -static const symbol s_2_167[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'a' }; -static const symbol s_2_168[3] = { 'o', 'l', 'a' }; -static const symbol s_2_169[4] = { 'a', 's', 'l', 'a' }; -static const symbol s_2_170[4] = { 'n', 'u', 'l', 'a' }; -static const symbol s_2_171[4] = { 'g', 'a', 'm', 'a' }; -static const symbol s_2_172[6] = { 'l', 'o', 'g', 'a', 'm', 'a' }; -static const symbol s_2_173[5] = { 'u', 'g', 'a', 'm', 'a' }; -static const symbol s_2_174[5] = { 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_175[6] = { 'c', 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_176[6] = { 'l', 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_177[6] = { 'r', 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_178[7] = { 0xC4, 0x87, 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_179[7] = { 0xC4, 0x8D, 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_180[7] = { 0xC4, 0x91, 'a', 'j', 'a', 'm', 'a' }; -static const symbol s_2_181[6] = { 'b', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_182[6] = { 'c', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_183[6] = { 'd', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_184[6] = { 'f', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_185[6] = { 'g', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_186[6] = { 'l', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_187[6] = { 'm', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_188[6] = { 'n', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_189[8] = { 'g', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_190[8] = { 'm', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_191[8] = { 'p', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_192[8] = { 'r', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_193[8] = { 't', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_194[6] = { 'p', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_195[6] = { 'r', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_196[6] = { 's', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_197[6] = { 't', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_198[6] = { 'z', 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_199[7] = { 0xC5, 0xBE, 'i', 'j', 'a', 'm', 'a' }; -static const symbol s_2_200[5] = { 'a', 'l', 'a', 'm', 'a' }; -static const symbol s_2_201[7] = { 'i', 'j', 'a', 'l', 'a', 'm', 'a' }; -static const symbol s_2_202[6] = { 'n', 'a', 'l', 'a', 'm', 'a' }; -static const symbol s_2_203[5] = { 'e', 'l', 'a', 'm', 'a' }; -static const symbol s_2_204[5] = { 'i', 'l', 'a', 'm', 'a' }; -static const symbol s_2_205[6] = { 'r', 'a', 'm', 'a', 'm', 'a' }; -static const symbol s_2_206[6] = { 'l', 'e', 'm', 'a', 'm', 'a' }; -static const symbol s_2_207[5] = { 'i', 'n', 'a', 'm', 'a' }; -static const symbol s_2_208[6] = { 'c', 'i', 'n', 'a', 'm', 'a' }; -static const symbol s_2_209[7] = { 0xC4, 0x8D, 'i', 'n', 'a', 'm', 'a' }; -static const symbol s_2_210[4] = { 'r', 'a', 'm', 'a' }; -static const symbol s_2_211[5] = { 'a', 'r', 'a', 'm', 'a' }; -static const symbol s_2_212[5] = { 'd', 'r', 'a', 'm', 'a' }; -static const symbol s_2_213[5] = { 'e', 'r', 'a', 'm', 'a' }; -static const symbol s_2_214[5] = { 'o', 'r', 'a', 'm', 'a' }; -static const symbol s_2_215[6] = { 'b', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_216[6] = { 'g', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_217[6] = { 'j', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_218[6] = { 'k', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_219[6] = { 'n', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_220[6] = { 't', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_221[6] = { 'v', 'a', 's', 'a', 'm', 'a' }; -static const symbol s_2_222[5] = { 'e', 's', 'a', 'm', 'a' }; -static const symbol s_2_223[5] = { 'i', 's', 'a', 'm', 'a' }; -static const symbol s_2_224[5] = { 'e', 't', 'a', 'm', 'a' }; -static const symbol s_2_225[6] = { 'e', 's', 't', 'a', 'm', 'a' }; -static const symbol s_2_226[6] = { 'i', 's', 't', 'a', 'm', 'a' }; -static const symbol s_2_227[6] = { 'k', 's', 't', 'a', 'm', 'a' }; -static const symbol s_2_228[6] = { 'o', 's', 't', 'a', 'm', 'a' }; -static const symbol s_2_229[5] = { 'a', 'v', 'a', 'm', 'a' }; -static const symbol s_2_230[5] = { 'e', 'v', 'a', 'm', 'a' }; -static const symbol s_2_231[5] = { 'i', 'v', 'a', 'm', 'a' }; -static const symbol s_2_232[7] = { 'b', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_233[7] = { 'g', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_234[7] = { 'j', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_235[7] = { 'k', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_236[7] = { 'n', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_237[7] = { 't', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_238[7] = { 'v', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_239[6] = { 'e', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_240[6] = { 'i', 0xC5, 0xA1, 'a', 'm', 'a' }; -static const symbol s_2_241[4] = { 'l', 'e', 'm', 'a' }; -static const symbol s_2_242[5] = { 'a', 'c', 'i', 'm', 'a' }; -static const symbol s_2_243[5] = { 'e', 'c', 'i', 'm', 'a' }; -static const symbol s_2_244[5] = { 'u', 'c', 'i', 'm', 'a' }; -static const symbol s_2_245[5] = { 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_246[6] = { 'c', 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_247[6] = { 'l', 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_248[6] = { 'r', 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_249[7] = { 0xC4, 0x87, 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_250[7] = { 0xC4, 0x8D, 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_251[7] = { 0xC4, 0x91, 'a', 'j', 'i', 'm', 'a' }; -static const symbol s_2_252[6] = { 'b', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_253[6] = { 'c', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_254[6] = { 'd', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_255[6] = { 'f', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_256[6] = { 'g', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_257[8] = { 'a', 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_258[8] = { 'e', 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_259[8] = { 's', 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_260[9] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_261[6] = { 'k', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_262[7] = { 's', 'k', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_263[8] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_264[6] = { 'l', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_265[7] = { 'e', 'l', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_266[6] = { 'm', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_267[6] = { 'n', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_268[8] = { 'g', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_269[8] = { 'm', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_270[8] = { 'p', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_271[8] = { 'r', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_272[8] = { 't', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_273[6] = { 'p', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_274[6] = { 'r', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_275[6] = { 's', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_276[7] = { 'o', 's', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_277[6] = { 't', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_278[7] = { 'a', 't', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_279[9] = { 'e', 'v', 'i', 't', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_280[9] = { 'o', 'v', 'i', 't', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_281[8] = { 'a', 's', 't', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_282[7] = { 'a', 'v', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_283[7] = { 'e', 'v', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_284[7] = { 'i', 'v', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_285[7] = { 'o', 'v', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_286[6] = { 'z', 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_287[8] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_288[7] = { 0xC5, 0xBE, 'i', 'j', 'i', 'm', 'a' }; -static const symbol s_2_289[6] = { 'a', 'n', 'j', 'i', 'm', 'a' }; -static const symbol s_2_290[6] = { 'e', 'n', 'j', 'i', 'm', 'a' }; -static const symbol s_2_291[6] = { 's', 'n', 'j', 'i', 'm', 'a' }; -static const symbol s_2_292[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'm', 'a' }; -static const symbol s_2_293[4] = { 'k', 'i', 'm', 'a' }; -static const symbol s_2_294[5] = { 's', 'k', 'i', 'm', 'a' }; -static const symbol s_2_295[6] = { 0xC5, 0xA1, 'k', 'i', 'm', 'a' }; -static const symbol s_2_296[5] = { 'a', 'l', 'i', 'm', 'a' }; -static const symbol s_2_297[7] = { 'i', 'j', 'a', 'l', 'i', 'm', 'a' }; -static const symbol s_2_298[6] = { 'n', 'a', 'l', 'i', 'm', 'a' }; -static const symbol s_2_299[5] = { 'e', 'l', 'i', 'm', 'a' }; -static const symbol s_2_300[5] = { 'i', 'l', 'i', 'm', 'a' }; -static const symbol s_2_301[7] = { 'o', 'z', 'i', 'l', 'i', 'm', 'a' }; -static const symbol s_2_302[5] = { 'o', 'l', 'i', 'm', 'a' }; -static const symbol s_2_303[6] = { 'l', 'e', 'm', 'i', 'm', 'a' }; -static const symbol s_2_304[4] = { 'n', 'i', 'm', 'a' }; -static const symbol s_2_305[5] = { 'a', 'n', 'i', 'm', 'a' }; -static const symbol s_2_306[5] = { 'i', 'n', 'i', 'm', 'a' }; -static const symbol s_2_307[6] = { 'c', 'i', 'n', 'i', 'm', 'a' }; -static const symbol s_2_308[7] = { 0xC4, 0x8D, 'i', 'n', 'i', 'm', 'a' }; -static const symbol s_2_309[5] = { 'o', 'n', 'i', 'm', 'a' }; -static const symbol s_2_310[5] = { 'a', 'r', 'i', 'm', 'a' }; -static const symbol s_2_311[5] = { 'd', 'r', 'i', 'm', 'a' }; -static const symbol s_2_312[5] = { 'e', 'r', 'i', 'm', 'a' }; -static const symbol s_2_313[5] = { 'o', 'r', 'i', 'm', 'a' }; -static const symbol s_2_314[6] = { 'b', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_315[6] = { 'g', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_316[6] = { 'j', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_317[6] = { 'k', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_318[6] = { 'n', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_319[6] = { 't', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_320[6] = { 'v', 'a', 's', 'i', 'm', 'a' }; -static const symbol s_2_321[5] = { 'e', 's', 'i', 'm', 'a' }; -static const symbol s_2_322[5] = { 'i', 's', 'i', 'm', 'a' }; -static const symbol s_2_323[5] = { 'o', 's', 'i', 'm', 'a' }; -static const symbol s_2_324[5] = { 'a', 't', 'i', 'm', 'a' }; -static const symbol s_2_325[7] = { 'i', 'k', 'a', 't', 'i', 'm', 'a' }; -static const symbol s_2_326[6] = { 'l', 'a', 't', 'i', 'm', 'a' }; -static const symbol s_2_327[5] = { 'e', 't', 'i', 'm', 'a' }; -static const symbol s_2_328[7] = { 'e', 'v', 'i', 't', 'i', 'm', 'a' }; -static const symbol s_2_329[7] = { 'o', 'v', 'i', 't', 'i', 'm', 'a' }; -static const symbol s_2_330[6] = { 'a', 's', 't', 'i', 'm', 'a' }; -static const symbol s_2_331[6] = { 'e', 's', 't', 'i', 'm', 'a' }; -static const symbol s_2_332[6] = { 'i', 's', 't', 'i', 'm', 'a' }; -static const symbol s_2_333[6] = { 'k', 's', 't', 'i', 'm', 'a' }; -static const symbol s_2_334[6] = { 'o', 's', 't', 'i', 'm', 'a' }; -static const symbol s_2_335[7] = { 'i', 0xC5, 0xA1, 't', 'i', 'm', 'a' }; -static const symbol s_2_336[5] = { 'a', 'v', 'i', 'm', 'a' }; -static const symbol s_2_337[5] = { 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_338[7] = { 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_339[8] = { 'c', 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_340[8] = { 'l', 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_341[8] = { 'r', 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_342[9] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_343[9] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_344[9] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; -static const symbol s_2_345[5] = { 'i', 'v', 'i', 'm', 'a' }; -static const symbol s_2_346[5] = { 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_347[6] = { 'g', 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_348[7] = { 'u', 'g', 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_349[6] = { 'l', 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_350[7] = { 'o', 'l', 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_351[6] = { 'm', 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_352[7] = { 'o', 'n', 'o', 'v', 'i', 'm', 'a' }; -static const symbol s_2_353[6] = { 's', 't', 'v', 'i', 'm', 'a' }; -static const symbol s_2_354[7] = { 0xC5, 0xA1, 't', 'v', 'i', 'm', 'a' }; -static const symbol s_2_355[6] = { 'a', 0xC4, 0x87, 'i', 'm', 'a' }; -static const symbol s_2_356[6] = { 'e', 0xC4, 0x87, 'i', 'm', 'a' }; -static const symbol s_2_357[6] = { 'u', 0xC4, 0x87, 'i', 'm', 'a' }; -static const symbol s_2_358[7] = { 'b', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_359[7] = { 'g', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_360[7] = { 'j', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_361[7] = { 'k', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_362[7] = { 'n', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_363[7] = { 't', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_364[7] = { 'v', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_365[6] = { 'e', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_366[6] = { 'i', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_367[6] = { 'o', 0xC5, 0xA1, 'i', 'm', 'a' }; -static const symbol s_2_368[2] = { 'n', 'a' }; -static const symbol s_2_369[3] = { 'a', 'n', 'a' }; -static const symbol s_2_370[5] = { 'a', 'c', 'a', 'n', 'a' }; -static const symbol s_2_371[5] = { 'u', 'r', 'a', 'n', 'a' }; -static const symbol s_2_372[4] = { 't', 'a', 'n', 'a' }; -static const symbol s_2_373[5] = { 'a', 'v', 'a', 'n', 'a' }; -static const symbol s_2_374[5] = { 'e', 'v', 'a', 'n', 'a' }; -static const symbol s_2_375[5] = { 'i', 'v', 'a', 'n', 'a' }; -static const symbol s_2_376[5] = { 'u', 'v', 'a', 'n', 'a' }; -static const symbol s_2_377[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'a' }; -static const symbol s_2_378[5] = { 'a', 'c', 'e', 'n', 'a' }; -static const symbol s_2_379[6] = { 'l', 'u', 'c', 'e', 'n', 'a' }; -static const symbol s_2_380[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'a' }; -static const symbol s_2_381[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'a' }; -static const symbol s_2_382[3] = { 'i', 'n', 'a' }; -static const symbol s_2_383[4] = { 'c', 'i', 'n', 'a' }; -static const symbol s_2_384[5] = { 'a', 'n', 'i', 'n', 'a' }; -static const symbol s_2_385[5] = { 0xC4, 0x8D, 'i', 'n', 'a' }; -static const symbol s_2_386[3] = { 'o', 'n', 'a' }; -static const symbol s_2_387[3] = { 'a', 'r', 'a' }; -static const symbol s_2_388[3] = { 'd', 'r', 'a' }; -static const symbol s_2_389[3] = { 'e', 'r', 'a' }; -static const symbol s_2_390[3] = { 'o', 'r', 'a' }; -static const symbol s_2_391[4] = { 'b', 'a', 's', 'a' }; -static const symbol s_2_392[4] = { 'g', 'a', 's', 'a' }; -static const symbol s_2_393[4] = { 'j', 'a', 's', 'a' }; -static const symbol s_2_394[4] = { 'k', 'a', 's', 'a' }; -static const symbol s_2_395[4] = { 'n', 'a', 's', 'a' }; -static const symbol s_2_396[4] = { 't', 'a', 's', 'a' }; -static const symbol s_2_397[4] = { 'v', 'a', 's', 'a' }; -static const symbol s_2_398[3] = { 'e', 's', 'a' }; -static const symbol s_2_399[3] = { 'i', 's', 'a' }; -static const symbol s_2_400[3] = { 'o', 's', 'a' }; -static const symbol s_2_401[3] = { 'a', 't', 'a' }; -static const symbol s_2_402[5] = { 'i', 'k', 'a', 't', 'a' }; -static const symbol s_2_403[4] = { 'l', 'a', 't', 'a' }; -static const symbol s_2_404[3] = { 'e', 't', 'a' }; -static const symbol s_2_405[5] = { 'e', 'v', 'i', 't', 'a' }; -static const symbol s_2_406[5] = { 'o', 'v', 'i', 't', 'a' }; -static const symbol s_2_407[4] = { 'a', 's', 't', 'a' }; -static const symbol s_2_408[4] = { 'e', 's', 't', 'a' }; -static const symbol s_2_409[4] = { 'i', 's', 't', 'a' }; -static const symbol s_2_410[4] = { 'k', 's', 't', 'a' }; -static const symbol s_2_411[4] = { 'o', 's', 't', 'a' }; -static const symbol s_2_412[4] = { 'n', 'u', 't', 'a' }; -static const symbol s_2_413[5] = { 'i', 0xC5, 0xA1, 't', 'a' }; -static const symbol s_2_414[3] = { 'a', 'v', 'a' }; -static const symbol s_2_415[3] = { 'e', 'v', 'a' }; -static const symbol s_2_416[5] = { 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_417[6] = { 'c', 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_418[6] = { 'l', 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_419[6] = { 'r', 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_420[7] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_421[7] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_422[7] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'a' }; -static const symbol s_2_423[3] = { 'i', 'v', 'a' }; -static const symbol s_2_424[3] = { 'o', 'v', 'a' }; -static const symbol s_2_425[4] = { 'g', 'o', 'v', 'a' }; -static const symbol s_2_426[5] = { 'u', 'g', 'o', 'v', 'a' }; -static const symbol s_2_427[4] = { 'l', 'o', 'v', 'a' }; -static const symbol s_2_428[5] = { 'o', 'l', 'o', 'v', 'a' }; -static const symbol s_2_429[4] = { 'm', 'o', 'v', 'a' }; -static const symbol s_2_430[5] = { 'o', 'n', 'o', 'v', 'a' }; -static const symbol s_2_431[4] = { 's', 't', 'v', 'a' }; -static const symbol s_2_432[5] = { 0xC5, 0xA1, 't', 'v', 'a' }; -static const symbol s_2_433[4] = { 'a', 0xC4, 0x87, 'a' }; -static const symbol s_2_434[4] = { 'e', 0xC4, 0x87, 'a' }; -static const symbol s_2_435[4] = { 'u', 0xC4, 0x87, 'a' }; -static const symbol s_2_436[5] = { 'b', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_437[5] = { 'g', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_438[5] = { 'j', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_439[5] = { 'k', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_440[5] = { 'n', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_441[5] = { 't', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_442[5] = { 'v', 'a', 0xC5, 0xA1, 'a' }; -static const symbol s_2_443[4] = { 'e', 0xC5, 0xA1, 'a' }; -static const symbol s_2_444[4] = { 'i', 0xC5, 0xA1, 'a' }; -static const symbol s_2_445[4] = { 'o', 0xC5, 0xA1, 'a' }; -static const symbol s_2_446[3] = { 'a', 'c', 'e' }; -static const symbol s_2_447[3] = { 'e', 'c', 'e' }; -static const symbol s_2_448[3] = { 'u', 'c', 'e' }; -static const symbol s_2_449[4] = { 'l', 'u', 'c', 'e' }; -static const symbol s_2_450[6] = { 'a', 's', 't', 'a', 'd', 'e' }; -static const symbol s_2_451[6] = { 'i', 's', 't', 'a', 'd', 'e' }; -static const symbol s_2_452[6] = { 'o', 's', 't', 'a', 'd', 'e' }; -static const symbol s_2_453[2] = { 'g', 'e' }; -static const symbol s_2_454[4] = { 'l', 'o', 'g', 'e' }; -static const symbol s_2_455[3] = { 'u', 'g', 'e' }; -static const symbol s_2_456[3] = { 'a', 'j', 'e' }; -static const symbol s_2_457[4] = { 'c', 'a', 'j', 'e' }; -static const symbol s_2_458[4] = { 'l', 'a', 'j', 'e' }; -static const symbol s_2_459[4] = { 'r', 'a', 'j', 'e' }; -static const symbol s_2_460[6] = { 'a', 's', 't', 'a', 'j', 'e' }; -static const symbol s_2_461[6] = { 'i', 's', 't', 'a', 'j', 'e' }; -static const symbol s_2_462[6] = { 'o', 's', 't', 'a', 'j', 'e' }; -static const symbol s_2_463[5] = { 0xC4, 0x87, 'a', 'j', 'e' }; -static const symbol s_2_464[5] = { 0xC4, 0x8D, 'a', 'j', 'e' }; -static const symbol s_2_465[5] = { 0xC4, 0x91, 'a', 'j', 'e' }; -static const symbol s_2_466[3] = { 'i', 'j', 'e' }; -static const symbol s_2_467[4] = { 'b', 'i', 'j', 'e' }; -static const symbol s_2_468[4] = { 'c', 'i', 'j', 'e' }; -static const symbol s_2_469[4] = { 'd', 'i', 'j', 'e' }; -static const symbol s_2_470[4] = { 'f', 'i', 'j', 'e' }; -static const symbol s_2_471[4] = { 'g', 'i', 'j', 'e' }; -static const symbol s_2_472[6] = { 'a', 'n', 'j', 'i', 'j', 'e' }; -static const symbol s_2_473[6] = { 'e', 'n', 'j', 'i', 'j', 'e' }; -static const symbol s_2_474[6] = { 's', 'n', 'j', 'i', 'j', 'e' }; -static const symbol s_2_475[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e' }; -static const symbol s_2_476[4] = { 'k', 'i', 'j', 'e' }; -static const symbol s_2_477[5] = { 's', 'k', 'i', 'j', 'e' }; -static const symbol s_2_478[6] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e' }; -static const symbol s_2_479[4] = { 'l', 'i', 'j', 'e' }; -static const symbol s_2_480[5] = { 'e', 'l', 'i', 'j', 'e' }; -static const symbol s_2_481[4] = { 'm', 'i', 'j', 'e' }; -static const symbol s_2_482[4] = { 'n', 'i', 'j', 'e' }; -static const symbol s_2_483[6] = { 'g', 'a', 'n', 'i', 'j', 'e' }; -static const symbol s_2_484[6] = { 'm', 'a', 'n', 'i', 'j', 'e' }; -static const symbol s_2_485[6] = { 'p', 'a', 'n', 'i', 'j', 'e' }; -static const symbol s_2_486[6] = { 'r', 'a', 'n', 'i', 'j', 'e' }; -static const symbol s_2_487[6] = { 't', 'a', 'n', 'i', 'j', 'e' }; -static const symbol s_2_488[4] = { 'p', 'i', 'j', 'e' }; -static const symbol s_2_489[4] = { 'r', 'i', 'j', 'e' }; -static const symbol s_2_490[4] = { 's', 'i', 'j', 'e' }; -static const symbol s_2_491[5] = { 'o', 's', 'i', 'j', 'e' }; -static const symbol s_2_492[4] = { 't', 'i', 'j', 'e' }; -static const symbol s_2_493[5] = { 'a', 't', 'i', 'j', 'e' }; -static const symbol s_2_494[7] = { 'e', 'v', 'i', 't', 'i', 'j', 'e' }; -static const symbol s_2_495[7] = { 'o', 'v', 'i', 't', 'i', 'j', 'e' }; -static const symbol s_2_496[6] = { 'a', 's', 't', 'i', 'j', 'e' }; -static const symbol s_2_497[5] = { 'a', 'v', 'i', 'j', 'e' }; -static const symbol s_2_498[5] = { 'e', 'v', 'i', 'j', 'e' }; -static const symbol s_2_499[5] = { 'i', 'v', 'i', 'j', 'e' }; -static const symbol s_2_500[5] = { 'o', 'v', 'i', 'j', 'e' }; -static const symbol s_2_501[4] = { 'z', 'i', 'j', 'e' }; -static const symbol s_2_502[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e' }; -static const symbol s_2_503[5] = { 0xC5, 0xBE, 'i', 'j', 'e' }; -static const symbol s_2_504[4] = { 'a', 'n', 'j', 'e' }; -static const symbol s_2_505[4] = { 'e', 'n', 'j', 'e' }; -static const symbol s_2_506[4] = { 's', 'n', 'j', 'e' }; -static const symbol s_2_507[5] = { 0xC5, 0xA1, 'n', 'j', 'e' }; -static const symbol s_2_508[3] = { 'u', 'j', 'e' }; -static const symbol s_2_509[6] = { 'l', 'u', 'c', 'u', 'j', 'e' }; -static const symbol s_2_510[5] = { 'i', 'r', 'u', 'j', 'e' }; -static const symbol s_2_511[7] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e' }; -static const symbol s_2_512[2] = { 'k', 'e' }; -static const symbol s_2_513[3] = { 's', 'k', 'e' }; -static const symbol s_2_514[4] = { 0xC5, 0xA1, 'k', 'e' }; -static const symbol s_2_515[3] = { 'a', 'l', 'e' }; -static const symbol s_2_516[5] = { 'a', 'c', 'a', 'l', 'e' }; -static const symbol s_2_517[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'e' }; -static const symbol s_2_518[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'e' }; -static const symbol s_2_519[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'e' }; -static const symbol s_2_520[5] = { 'i', 'j', 'a', 'l', 'e' }; -static const symbol s_2_521[6] = { 'i', 'n', 'j', 'a', 'l', 'e' }; -static const symbol s_2_522[4] = { 'n', 'a', 'l', 'e' }; -static const symbol s_2_523[5] = { 'i', 'r', 'a', 'l', 'e' }; -static const symbol s_2_524[5] = { 'u', 'r', 'a', 'l', 'e' }; -static const symbol s_2_525[4] = { 't', 'a', 'l', 'e' }; -static const symbol s_2_526[6] = { 'a', 's', 't', 'a', 'l', 'e' }; -static const symbol s_2_527[6] = { 'i', 's', 't', 'a', 'l', 'e' }; -static const symbol s_2_528[6] = { 'o', 's', 't', 'a', 'l', 'e' }; -static const symbol s_2_529[5] = { 'a', 'v', 'a', 'l', 'e' }; -static const symbol s_2_530[5] = { 'e', 'v', 'a', 'l', 'e' }; -static const symbol s_2_531[5] = { 'i', 'v', 'a', 'l', 'e' }; -static const symbol s_2_532[5] = { 'o', 'v', 'a', 'l', 'e' }; -static const symbol s_2_533[5] = { 'u', 'v', 'a', 'l', 'e' }; -static const symbol s_2_534[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'e' }; -static const symbol s_2_535[3] = { 'e', 'l', 'e' }; -static const symbol s_2_536[3] = { 'i', 'l', 'e' }; -static const symbol s_2_537[5] = { 'a', 'c', 'i', 'l', 'e' }; -static const symbol s_2_538[6] = { 'l', 'u', 'c', 'i', 'l', 'e' }; -static const symbol s_2_539[4] = { 'n', 'i', 'l', 'e' }; -static const symbol s_2_540[6] = { 'r', 'o', 's', 'i', 'l', 'e' }; -static const symbol s_2_541[6] = { 'j', 'e', 't', 'i', 'l', 'e' }; -static const symbol s_2_542[5] = { 'o', 'z', 'i', 'l', 'e' }; -static const symbol s_2_543[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'e' }; -static const symbol s_2_544[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'e' }; -static const symbol s_2_545[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'e' }; -static const symbol s_2_546[3] = { 'o', 'l', 'e' }; -static const symbol s_2_547[4] = { 'a', 's', 'l', 'e' }; -static const symbol s_2_548[4] = { 'n', 'u', 'l', 'e' }; -static const symbol s_2_549[4] = { 'r', 'a', 'm', 'e' }; -static const symbol s_2_550[4] = { 'l', 'e', 'm', 'e' }; -static const symbol s_2_551[5] = { 'a', 'c', 'o', 'm', 'e' }; -static const symbol s_2_552[5] = { 'e', 'c', 'o', 'm', 'e' }; -static const symbol s_2_553[5] = { 'u', 'c', 'o', 'm', 'e' }; -static const symbol s_2_554[6] = { 'a', 'n', 'j', 'o', 'm', 'e' }; -static const symbol s_2_555[6] = { 'e', 'n', 'j', 'o', 'm', 'e' }; -static const symbol s_2_556[6] = { 's', 'n', 'j', 'o', 'm', 'e' }; -static const symbol s_2_557[7] = { 0xC5, 0xA1, 'n', 'j', 'o', 'm', 'e' }; -static const symbol s_2_558[4] = { 'k', 'o', 'm', 'e' }; -static const symbol s_2_559[5] = { 's', 'k', 'o', 'm', 'e' }; -static const symbol s_2_560[6] = { 0xC5, 0xA1, 'k', 'o', 'm', 'e' }; -static const symbol s_2_561[5] = { 'e', 'l', 'o', 'm', 'e' }; -static const symbol s_2_562[4] = { 'n', 'o', 'm', 'e' }; -static const symbol s_2_563[6] = { 'c', 'i', 'n', 'o', 'm', 'e' }; -static const symbol s_2_564[7] = { 0xC4, 0x8D, 'i', 'n', 'o', 'm', 'e' }; -static const symbol s_2_565[5] = { 'o', 's', 'o', 'm', 'e' }; -static const symbol s_2_566[5] = { 'a', 't', 'o', 'm', 'e' }; -static const symbol s_2_567[7] = { 'e', 'v', 'i', 't', 'o', 'm', 'e' }; -static const symbol s_2_568[7] = { 'o', 'v', 'i', 't', 'o', 'm', 'e' }; -static const symbol s_2_569[6] = { 'a', 's', 't', 'o', 'm', 'e' }; -static const symbol s_2_570[5] = { 'a', 'v', 'o', 'm', 'e' }; -static const symbol s_2_571[5] = { 'e', 'v', 'o', 'm', 'e' }; -static const symbol s_2_572[5] = { 'i', 'v', 'o', 'm', 'e' }; -static const symbol s_2_573[5] = { 'o', 'v', 'o', 'm', 'e' }; -static const symbol s_2_574[6] = { 'a', 0xC4, 0x87, 'o', 'm', 'e' }; -static const symbol s_2_575[6] = { 'e', 0xC4, 0x87, 'o', 'm', 'e' }; -static const symbol s_2_576[6] = { 'u', 0xC4, 0x87, 'o', 'm', 'e' }; -static const symbol s_2_577[6] = { 'o', 0xC5, 0xA1, 'o', 'm', 'e' }; -static const symbol s_2_578[2] = { 'n', 'e' }; -static const symbol s_2_579[3] = { 'a', 'n', 'e' }; -static const symbol s_2_580[5] = { 'a', 'c', 'a', 'n', 'e' }; -static const symbol s_2_581[5] = { 'u', 'r', 'a', 'n', 'e' }; -static const symbol s_2_582[4] = { 't', 'a', 'n', 'e' }; -static const symbol s_2_583[6] = { 'a', 's', 't', 'a', 'n', 'e' }; -static const symbol s_2_584[6] = { 'i', 's', 't', 'a', 'n', 'e' }; -static const symbol s_2_585[6] = { 'o', 's', 't', 'a', 'n', 'e' }; -static const symbol s_2_586[5] = { 'a', 'v', 'a', 'n', 'e' }; -static const symbol s_2_587[5] = { 'e', 'v', 'a', 'n', 'e' }; -static const symbol s_2_588[5] = { 'i', 'v', 'a', 'n', 'e' }; -static const symbol s_2_589[5] = { 'u', 'v', 'a', 'n', 'e' }; -static const symbol s_2_590[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'e' }; -static const symbol s_2_591[5] = { 'a', 'c', 'e', 'n', 'e' }; -static const symbol s_2_592[6] = { 'l', 'u', 'c', 'e', 'n', 'e' }; -static const symbol s_2_593[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'e' }; -static const symbol s_2_594[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'e' }; -static const symbol s_2_595[3] = { 'i', 'n', 'e' }; -static const symbol s_2_596[4] = { 'c', 'i', 'n', 'e' }; -static const symbol s_2_597[5] = { 'a', 'n', 'i', 'n', 'e' }; -static const symbol s_2_598[5] = { 0xC4, 0x8D, 'i', 'n', 'e' }; -static const symbol s_2_599[3] = { 'o', 'n', 'e' }; -static const symbol s_2_600[3] = { 'a', 'r', 'e' }; -static const symbol s_2_601[3] = { 'd', 'r', 'e' }; -static const symbol s_2_602[3] = { 'e', 'r', 'e' }; -static const symbol s_2_603[3] = { 'o', 'r', 'e' }; -static const symbol s_2_604[3] = { 'a', 's', 'e' }; -static const symbol s_2_605[4] = { 'b', 'a', 's', 'e' }; -static const symbol s_2_606[5] = { 'a', 'c', 'a', 's', 'e' }; -static const symbol s_2_607[4] = { 'g', 'a', 's', 'e' }; -static const symbol s_2_608[4] = { 'j', 'a', 's', 'e' }; -static const symbol s_2_609[8] = { 'a', 's', 't', 'a', 'j', 'a', 's', 'e' }; -static const symbol s_2_610[8] = { 'i', 's', 't', 'a', 'j', 'a', 's', 'e' }; -static const symbol s_2_611[8] = { 'o', 's', 't', 'a', 'j', 'a', 's', 'e' }; -static const symbol s_2_612[6] = { 'i', 'n', 'j', 'a', 's', 'e' }; -static const symbol s_2_613[4] = { 'k', 'a', 's', 'e' }; -static const symbol s_2_614[4] = { 'n', 'a', 's', 'e' }; -static const symbol s_2_615[5] = { 'i', 'r', 'a', 's', 'e' }; -static const symbol s_2_616[5] = { 'u', 'r', 'a', 's', 'e' }; -static const symbol s_2_617[4] = { 't', 'a', 's', 'e' }; -static const symbol s_2_618[4] = { 'v', 'a', 's', 'e' }; -static const symbol s_2_619[5] = { 'a', 'v', 'a', 's', 'e' }; -static const symbol s_2_620[5] = { 'e', 'v', 'a', 's', 'e' }; -static const symbol s_2_621[5] = { 'i', 'v', 'a', 's', 'e' }; -static const symbol s_2_622[5] = { 'o', 'v', 'a', 's', 'e' }; -static const symbol s_2_623[5] = { 'u', 'v', 'a', 's', 'e' }; -static const symbol s_2_624[3] = { 'e', 's', 'e' }; -static const symbol s_2_625[3] = { 'i', 's', 'e' }; -static const symbol s_2_626[5] = { 'a', 'c', 'i', 's', 'e' }; -static const symbol s_2_627[6] = { 'l', 'u', 'c', 'i', 's', 'e' }; -static const symbol s_2_628[6] = { 'r', 'o', 's', 'i', 's', 'e' }; -static const symbol s_2_629[6] = { 'j', 'e', 't', 'i', 's', 'e' }; -static const symbol s_2_630[3] = { 'o', 's', 'e' }; -static const symbol s_2_631[8] = { 'a', 's', 't', 'a', 'd', 'o', 's', 'e' }; -static const symbol s_2_632[8] = { 'i', 's', 't', 'a', 'd', 'o', 's', 'e' }; -static const symbol s_2_633[8] = { 'o', 's', 't', 'a', 'd', 'o', 's', 'e' }; -static const symbol s_2_634[3] = { 'a', 't', 'e' }; -static const symbol s_2_635[5] = { 'a', 'c', 'a', 't', 'e' }; -static const symbol s_2_636[5] = { 'i', 'k', 'a', 't', 'e' }; -static const symbol s_2_637[4] = { 'l', 'a', 't', 'e' }; -static const symbol s_2_638[5] = { 'i', 'r', 'a', 't', 'e' }; -static const symbol s_2_639[5] = { 'u', 'r', 'a', 't', 'e' }; -static const symbol s_2_640[4] = { 't', 'a', 't', 'e' }; -static const symbol s_2_641[5] = { 'a', 'v', 'a', 't', 'e' }; -static const symbol s_2_642[5] = { 'e', 'v', 'a', 't', 'e' }; -static const symbol s_2_643[5] = { 'i', 'v', 'a', 't', 'e' }; -static const symbol s_2_644[5] = { 'u', 'v', 'a', 't', 'e' }; -static const symbol s_2_645[6] = { 'a', 0xC4, 0x8D, 'a', 't', 'e' }; -static const symbol s_2_646[3] = { 'e', 't', 'e' }; -static const symbol s_2_647[8] = { 'a', 's', 't', 'a', 'd', 'e', 't', 'e' }; -static const symbol s_2_648[8] = { 'i', 's', 't', 'a', 'd', 'e', 't', 'e' }; -static const symbol s_2_649[8] = { 'o', 's', 't', 'a', 'd', 'e', 't', 'e' }; -static const symbol s_2_650[8] = { 'a', 's', 't', 'a', 'j', 'e', 't', 'e' }; -static const symbol s_2_651[8] = { 'i', 's', 't', 'a', 'j', 'e', 't', 'e' }; -static const symbol s_2_652[8] = { 'o', 's', 't', 'a', 'j', 'e', 't', 'e' }; -static const symbol s_2_653[5] = { 'i', 'j', 'e', 't', 'e' }; -static const symbol s_2_654[6] = { 'i', 'n', 'j', 'e', 't', 'e' }; -static const symbol s_2_655[5] = { 'u', 'j', 'e', 't', 'e' }; -static const symbol s_2_656[8] = { 'l', 'u', 'c', 'u', 'j', 'e', 't', 'e' }; -static const symbol s_2_657[7] = { 'i', 'r', 'u', 'j', 'e', 't', 'e' }; -static const symbol s_2_658[9] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 't', 'e' }; -static const symbol s_2_659[4] = { 'n', 'e', 't', 'e' }; -static const symbol s_2_660[8] = { 'a', 's', 't', 'a', 'n', 'e', 't', 'e' }; -static const symbol s_2_661[8] = { 'i', 's', 't', 'a', 'n', 'e', 't', 'e' }; -static const symbol s_2_662[8] = { 'o', 's', 't', 'a', 'n', 'e', 't', 'e' }; -static const symbol s_2_663[6] = { 'a', 's', 't', 'e', 't', 'e' }; -static const symbol s_2_664[3] = { 'i', 't', 'e' }; -static const symbol s_2_665[5] = { 'a', 'c', 'i', 't', 'e' }; -static const symbol s_2_666[6] = { 'l', 'u', 'c', 'i', 't', 'e' }; -static const symbol s_2_667[4] = { 'n', 'i', 't', 'e' }; -static const symbol s_2_668[8] = { 'a', 's', 't', 'a', 'n', 'i', 't', 'e' }; -static const symbol s_2_669[8] = { 'i', 's', 't', 'a', 'n', 'i', 't', 'e' }; -static const symbol s_2_670[8] = { 'o', 's', 't', 'a', 'n', 'i', 't', 'e' }; -static const symbol s_2_671[6] = { 'r', 'o', 's', 'i', 't', 'e' }; -static const symbol s_2_672[6] = { 'j', 'e', 't', 'i', 't', 'e' }; -static const symbol s_2_673[6] = { 'a', 's', 't', 'i', 't', 'e' }; -static const symbol s_2_674[5] = { 'e', 'v', 'i', 't', 'e' }; -static const symbol s_2_675[5] = { 'o', 'v', 'i', 't', 'e' }; -static const symbol s_2_676[6] = { 'a', 0xC4, 0x8D, 'i', 't', 'e' }; -static const symbol s_2_677[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 't', 'e' }; -static const symbol s_2_678[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 't', 'e' }; -static const symbol s_2_679[4] = { 'a', 'j', 't', 'e' }; -static const symbol s_2_680[6] = { 'u', 'r', 'a', 'j', 't', 'e' }; -static const symbol s_2_681[5] = { 't', 'a', 'j', 't', 'e' }; -static const symbol s_2_682[7] = { 'a', 's', 't', 'a', 'j', 't', 'e' }; -static const symbol s_2_683[7] = { 'i', 's', 't', 'a', 'j', 't', 'e' }; -static const symbol s_2_684[7] = { 'o', 's', 't', 'a', 'j', 't', 'e' }; -static const symbol s_2_685[6] = { 'a', 'v', 'a', 'j', 't', 'e' }; -static const symbol s_2_686[6] = { 'e', 'v', 'a', 'j', 't', 'e' }; -static const symbol s_2_687[6] = { 'i', 'v', 'a', 'j', 't', 'e' }; -static const symbol s_2_688[6] = { 'u', 'v', 'a', 'j', 't', 'e' }; -static const symbol s_2_689[4] = { 'i', 'j', 't', 'e' }; -static const symbol s_2_690[7] = { 'l', 'u', 'c', 'u', 'j', 't', 'e' }; -static const symbol s_2_691[6] = { 'i', 'r', 'u', 'j', 't', 'e' }; -static const symbol s_2_692[8] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 't', 'e' }; -static const symbol s_2_693[4] = { 'a', 's', 't', 'e' }; -static const symbol s_2_694[6] = { 'a', 'c', 'a', 's', 't', 'e' }; -static const symbol s_2_695[9] = { 'a', 's', 't', 'a', 'j', 'a', 's', 't', 'e' }; -static const symbol s_2_696[9] = { 'i', 's', 't', 'a', 'j', 'a', 's', 't', 'e' }; -static const symbol s_2_697[9] = { 'o', 's', 't', 'a', 'j', 'a', 's', 't', 'e' }; -static const symbol s_2_698[7] = { 'i', 'n', 'j', 'a', 's', 't', 'e' }; -static const symbol s_2_699[6] = { 'i', 'r', 'a', 's', 't', 'e' }; -static const symbol s_2_700[6] = { 'u', 'r', 'a', 's', 't', 'e' }; -static const symbol s_2_701[5] = { 't', 'a', 's', 't', 'e' }; -static const symbol s_2_702[6] = { 'a', 'v', 'a', 's', 't', 'e' }; -static const symbol s_2_703[6] = { 'e', 'v', 'a', 's', 't', 'e' }; -static const symbol s_2_704[6] = { 'i', 'v', 'a', 's', 't', 'e' }; -static const symbol s_2_705[6] = { 'o', 'v', 'a', 's', 't', 'e' }; -static const symbol s_2_706[6] = { 'u', 'v', 'a', 's', 't', 'e' }; -static const symbol s_2_707[7] = { 'a', 0xC4, 0x8D, 'a', 's', 't', 'e' }; -static const symbol s_2_708[4] = { 'e', 's', 't', 'e' }; -static const symbol s_2_709[4] = { 'i', 's', 't', 'e' }; -static const symbol s_2_710[6] = { 'a', 'c', 'i', 's', 't', 'e' }; -static const symbol s_2_711[7] = { 'l', 'u', 'c', 'i', 's', 't', 'e' }; -static const symbol s_2_712[5] = { 'n', 'i', 's', 't', 'e' }; -static const symbol s_2_713[7] = { 'r', 'o', 's', 'i', 's', 't', 'e' }; -static const symbol s_2_714[7] = { 'j', 'e', 't', 'i', 's', 't', 'e' }; -static const symbol s_2_715[7] = { 'a', 0xC4, 0x8D, 'i', 's', 't', 'e' }; -static const symbol s_2_716[8] = { 'l', 'u', 0xC4, 0x8D, 'i', 's', 't', 'e' }; -static const symbol s_2_717[8] = { 'r', 'o', 0xC5, 0xA1, 'i', 's', 't', 'e' }; -static const symbol s_2_718[4] = { 'k', 's', 't', 'e' }; -static const symbol s_2_719[4] = { 'o', 's', 't', 'e' }; -static const symbol s_2_720[9] = { 'a', 's', 't', 'a', 'd', 'o', 's', 't', 'e' }; -static const symbol s_2_721[9] = { 'i', 's', 't', 'a', 'd', 'o', 's', 't', 'e' }; -static const symbol s_2_722[9] = { 'o', 's', 't', 'a', 'd', 'o', 's', 't', 'e' }; -static const symbol s_2_723[5] = { 'n', 'u', 's', 't', 'e' }; -static const symbol s_2_724[5] = { 'i', 0xC5, 0xA1, 't', 'e' }; -static const symbol s_2_725[3] = { 'a', 'v', 'e' }; -static const symbol s_2_726[3] = { 'e', 'v', 'e' }; -static const symbol s_2_727[5] = { 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_728[6] = { 'c', 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_729[6] = { 'l', 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_730[6] = { 'r', 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_731[7] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_732[7] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_733[7] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'e' }; -static const symbol s_2_734[3] = { 'i', 'v', 'e' }; -static const symbol s_2_735[3] = { 'o', 'v', 'e' }; -static const symbol s_2_736[4] = { 'g', 'o', 'v', 'e' }; -static const symbol s_2_737[5] = { 'u', 'g', 'o', 'v', 'e' }; -static const symbol s_2_738[4] = { 'l', 'o', 'v', 'e' }; -static const symbol s_2_739[5] = { 'o', 'l', 'o', 'v', 'e' }; -static const symbol s_2_740[4] = { 'm', 'o', 'v', 'e' }; -static const symbol s_2_741[5] = { 'o', 'n', 'o', 'v', 'e' }; -static const symbol s_2_742[4] = { 'a', 0xC4, 0x87, 'e' }; -static const symbol s_2_743[4] = { 'e', 0xC4, 0x87, 'e' }; -static const symbol s_2_744[4] = { 'u', 0xC4, 0x87, 'e' }; -static const symbol s_2_745[4] = { 'a', 0xC4, 0x8D, 'e' }; -static const symbol s_2_746[5] = { 'l', 'u', 0xC4, 0x8D, 'e' }; -static const symbol s_2_747[4] = { 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_748[5] = { 'b', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_749[5] = { 'g', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_750[5] = { 'j', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_751[9] = { 'a', 's', 't', 'a', 'j', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_752[9] = { 'i', 's', 't', 'a', 'j', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_753[9] = { 'o', 's', 't', 'a', 'j', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_754[7] = { 'i', 'n', 'j', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_755[5] = { 'k', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_756[5] = { 'n', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_757[6] = { 'i', 'r', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_758[6] = { 'u', 'r', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_759[5] = { 't', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_760[5] = { 'v', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_761[6] = { 'a', 'v', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_762[6] = { 'e', 'v', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_763[6] = { 'i', 'v', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_764[6] = { 'o', 'v', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_765[6] = { 'u', 'v', 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_766[7] = { 'a', 0xC4, 0x8D, 'a', 0xC5, 0xA1, 'e' }; -static const symbol s_2_767[4] = { 'e', 0xC5, 0xA1, 'e' }; -static const symbol s_2_768[4] = { 'i', 0xC5, 0xA1, 'e' }; -static const symbol s_2_769[7] = { 'j', 'e', 't', 'i', 0xC5, 0xA1, 'e' }; -static const symbol s_2_770[7] = { 'a', 0xC4, 0x8D, 'i', 0xC5, 0xA1, 'e' }; -static const symbol s_2_771[8] = { 'l', 'u', 0xC4, 0x8D, 'i', 0xC5, 0xA1, 'e' }; -static const symbol s_2_772[8] = { 'r', 'o', 0xC5, 0xA1, 'i', 0xC5, 0xA1, 'e' }; -static const symbol s_2_773[4] = { 'o', 0xC5, 0xA1, 'e' }; -static const symbol s_2_774[9] = { 'a', 's', 't', 'a', 'd', 'o', 0xC5, 0xA1, 'e' }; -static const symbol s_2_775[9] = { 'i', 's', 't', 'a', 'd', 'o', 0xC5, 0xA1, 'e' }; -static const symbol s_2_776[9] = { 'o', 's', 't', 'a', 'd', 'o', 0xC5, 0xA1, 'e' }; -static const symbol s_2_777[4] = { 'a', 'c', 'e', 'g' }; -static const symbol s_2_778[4] = { 'e', 'c', 'e', 'g' }; -static const symbol s_2_779[4] = { 'u', 'c', 'e', 'g' }; -static const symbol s_2_780[7] = { 'a', 'n', 'j', 'i', 'j', 'e', 'g' }; -static const symbol s_2_781[7] = { 'e', 'n', 'j', 'i', 'j', 'e', 'g' }; -static const symbol s_2_782[7] = { 's', 'n', 'j', 'i', 'j', 'e', 'g' }; -static const symbol s_2_783[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'g' }; -static const symbol s_2_784[5] = { 'k', 'i', 'j', 'e', 'g' }; -static const symbol s_2_785[6] = { 's', 'k', 'i', 'j', 'e', 'g' }; -static const symbol s_2_786[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'g' }; -static const symbol s_2_787[6] = { 'e', 'l', 'i', 'j', 'e', 'g' }; -static const symbol s_2_788[5] = { 'n', 'i', 'j', 'e', 'g' }; -static const symbol s_2_789[6] = { 'o', 's', 'i', 'j', 'e', 'g' }; -static const symbol s_2_790[6] = { 'a', 't', 'i', 'j', 'e', 'g' }; -static const symbol s_2_791[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'g' }; -static const symbol s_2_792[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'g' }; -static const symbol s_2_793[7] = { 'a', 's', 't', 'i', 'j', 'e', 'g' }; -static const symbol s_2_794[6] = { 'a', 'v', 'i', 'j', 'e', 'g' }; -static const symbol s_2_795[6] = { 'e', 'v', 'i', 'j', 'e', 'g' }; -static const symbol s_2_796[6] = { 'i', 'v', 'i', 'j', 'e', 'g' }; -static const symbol s_2_797[6] = { 'o', 'v', 'i', 'j', 'e', 'g' }; -static const symbol s_2_798[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'g' }; -static const symbol s_2_799[5] = { 'a', 'n', 'j', 'e', 'g' }; -static const symbol s_2_800[5] = { 'e', 'n', 'j', 'e', 'g' }; -static const symbol s_2_801[5] = { 's', 'n', 'j', 'e', 'g' }; -static const symbol s_2_802[6] = { 0xC5, 0xA1, 'n', 'j', 'e', 'g' }; -static const symbol s_2_803[3] = { 'k', 'e', 'g' }; -static const symbol s_2_804[4] = { 'e', 'l', 'e', 'g' }; -static const symbol s_2_805[3] = { 'n', 'e', 'g' }; -static const symbol s_2_806[4] = { 'a', 'n', 'e', 'g' }; -static const symbol s_2_807[4] = { 'e', 'n', 'e', 'g' }; -static const symbol s_2_808[4] = { 's', 'n', 'e', 'g' }; -static const symbol s_2_809[5] = { 0xC5, 0xA1, 'n', 'e', 'g' }; -static const symbol s_2_810[4] = { 'o', 's', 'e', 'g' }; -static const symbol s_2_811[4] = { 'a', 't', 'e', 'g' }; -static const symbol s_2_812[4] = { 'a', 'v', 'e', 'g' }; -static const symbol s_2_813[4] = { 'e', 'v', 'e', 'g' }; -static const symbol s_2_814[4] = { 'i', 'v', 'e', 'g' }; -static const symbol s_2_815[4] = { 'o', 'v', 'e', 'g' }; -static const symbol s_2_816[5] = { 'a', 0xC4, 0x87, 'e', 'g' }; -static const symbol s_2_817[5] = { 'e', 0xC4, 0x87, 'e', 'g' }; -static const symbol s_2_818[5] = { 'u', 0xC4, 0x87, 'e', 'g' }; -static const symbol s_2_819[5] = { 'o', 0xC5, 0xA1, 'e', 'g' }; -static const symbol s_2_820[4] = { 'a', 'c', 'o', 'g' }; -static const symbol s_2_821[4] = { 'e', 'c', 'o', 'g' }; -static const symbol s_2_822[4] = { 'u', 'c', 'o', 'g' }; -static const symbol s_2_823[5] = { 'a', 'n', 'j', 'o', 'g' }; -static const symbol s_2_824[5] = { 'e', 'n', 'j', 'o', 'g' }; -static const symbol s_2_825[5] = { 's', 'n', 'j', 'o', 'g' }; -static const symbol s_2_826[6] = { 0xC5, 0xA1, 'n', 'j', 'o', 'g' }; -static const symbol s_2_827[3] = { 'k', 'o', 'g' }; -static const symbol s_2_828[4] = { 's', 'k', 'o', 'g' }; -static const symbol s_2_829[5] = { 0xC5, 0xA1, 'k', 'o', 'g' }; -static const symbol s_2_830[4] = { 'e', 'l', 'o', 'g' }; -static const symbol s_2_831[3] = { 'n', 'o', 'g' }; -static const symbol s_2_832[5] = { 'c', 'i', 'n', 'o', 'g' }; -static const symbol s_2_833[6] = { 0xC4, 0x8D, 'i', 'n', 'o', 'g' }; -static const symbol s_2_834[4] = { 'o', 's', 'o', 'g' }; -static const symbol s_2_835[4] = { 'a', 't', 'o', 'g' }; -static const symbol s_2_836[6] = { 'e', 'v', 'i', 't', 'o', 'g' }; -static const symbol s_2_837[6] = { 'o', 'v', 'i', 't', 'o', 'g' }; -static const symbol s_2_838[5] = { 'a', 's', 't', 'o', 'g' }; -static const symbol s_2_839[4] = { 'a', 'v', 'o', 'g' }; -static const symbol s_2_840[4] = { 'e', 'v', 'o', 'g' }; -static const symbol s_2_841[4] = { 'i', 'v', 'o', 'g' }; -static const symbol s_2_842[4] = { 'o', 'v', 'o', 'g' }; -static const symbol s_2_843[5] = { 'a', 0xC4, 0x87, 'o', 'g' }; -static const symbol s_2_844[5] = { 'e', 0xC4, 0x87, 'o', 'g' }; -static const symbol s_2_845[5] = { 'u', 0xC4, 0x87, 'o', 'g' }; -static const symbol s_2_846[5] = { 'o', 0xC5, 0xA1, 'o', 'g' }; -static const symbol s_2_847[2] = { 'a', 'h' }; -static const symbol s_2_848[4] = { 'a', 'c', 'a', 'h' }; -static const symbol s_2_849[7] = { 'a', 's', 't', 'a', 'j', 'a', 'h' }; -static const symbol s_2_850[7] = { 'i', 's', 't', 'a', 'j', 'a', 'h' }; -static const symbol s_2_851[7] = { 'o', 's', 't', 'a', 'j', 'a', 'h' }; -static const symbol s_2_852[5] = { 'i', 'n', 'j', 'a', 'h' }; -static const symbol s_2_853[4] = { 'i', 'r', 'a', 'h' }; -static const symbol s_2_854[4] = { 'u', 'r', 'a', 'h' }; -static const symbol s_2_855[3] = { 't', 'a', 'h' }; -static const symbol s_2_856[4] = { 'a', 'v', 'a', 'h' }; -static const symbol s_2_857[4] = { 'e', 'v', 'a', 'h' }; -static const symbol s_2_858[4] = { 'i', 'v', 'a', 'h' }; -static const symbol s_2_859[4] = { 'o', 'v', 'a', 'h' }; -static const symbol s_2_860[4] = { 'u', 'v', 'a', 'h' }; -static const symbol s_2_861[5] = { 'a', 0xC4, 0x8D, 'a', 'h' }; -static const symbol s_2_862[2] = { 'i', 'h' }; -static const symbol s_2_863[4] = { 'a', 'c', 'i', 'h' }; -static const symbol s_2_864[4] = { 'e', 'c', 'i', 'h' }; -static const symbol s_2_865[4] = { 'u', 'c', 'i', 'h' }; -static const symbol s_2_866[5] = { 'l', 'u', 'c', 'i', 'h' }; -static const symbol s_2_867[7] = { 'a', 'n', 'j', 'i', 'j', 'i', 'h' }; -static const symbol s_2_868[7] = { 'e', 'n', 'j', 'i', 'j', 'i', 'h' }; -static const symbol s_2_869[7] = { 's', 'n', 'j', 'i', 'j', 'i', 'h' }; -static const symbol s_2_870[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i', 'h' }; -static const symbol s_2_871[5] = { 'k', 'i', 'j', 'i', 'h' }; -static const symbol s_2_872[6] = { 's', 'k', 'i', 'j', 'i', 'h' }; -static const symbol s_2_873[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i', 'h' }; -static const symbol s_2_874[6] = { 'e', 'l', 'i', 'j', 'i', 'h' }; -static const symbol s_2_875[5] = { 'n', 'i', 'j', 'i', 'h' }; -static const symbol s_2_876[6] = { 'o', 's', 'i', 'j', 'i', 'h' }; -static const symbol s_2_877[6] = { 'a', 't', 'i', 'j', 'i', 'h' }; -static const symbol s_2_878[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'i', 'h' }; -static const symbol s_2_879[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'i', 'h' }; -static const symbol s_2_880[7] = { 'a', 's', 't', 'i', 'j', 'i', 'h' }; -static const symbol s_2_881[6] = { 'a', 'v', 'i', 'j', 'i', 'h' }; -static const symbol s_2_882[6] = { 'e', 'v', 'i', 'j', 'i', 'h' }; -static const symbol s_2_883[6] = { 'i', 'v', 'i', 'j', 'i', 'h' }; -static const symbol s_2_884[6] = { 'o', 'v', 'i', 'j', 'i', 'h' }; -static const symbol s_2_885[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i', 'h' }; -static const symbol s_2_886[5] = { 'a', 'n', 'j', 'i', 'h' }; -static const symbol s_2_887[5] = { 'e', 'n', 'j', 'i', 'h' }; -static const symbol s_2_888[5] = { 's', 'n', 'j', 'i', 'h' }; -static const symbol s_2_889[6] = { 0xC5, 0xA1, 'n', 'j', 'i', 'h' }; -static const symbol s_2_890[3] = { 'k', 'i', 'h' }; -static const symbol s_2_891[4] = { 's', 'k', 'i', 'h' }; -static const symbol s_2_892[5] = { 0xC5, 0xA1, 'k', 'i', 'h' }; -static const symbol s_2_893[4] = { 'e', 'l', 'i', 'h' }; -static const symbol s_2_894[3] = { 'n', 'i', 'h' }; -static const symbol s_2_895[5] = { 'c', 'i', 'n', 'i', 'h' }; -static const symbol s_2_896[6] = { 0xC4, 0x8D, 'i', 'n', 'i', 'h' }; -static const symbol s_2_897[4] = { 'o', 's', 'i', 'h' }; -static const symbol s_2_898[5] = { 'r', 'o', 's', 'i', 'h' }; -static const symbol s_2_899[4] = { 'a', 't', 'i', 'h' }; -static const symbol s_2_900[5] = { 'j', 'e', 't', 'i', 'h' }; -static const symbol s_2_901[6] = { 'e', 'v', 'i', 't', 'i', 'h' }; -static const symbol s_2_902[6] = { 'o', 'v', 'i', 't', 'i', 'h' }; -static const symbol s_2_903[5] = { 'a', 's', 't', 'i', 'h' }; -static const symbol s_2_904[4] = { 'a', 'v', 'i', 'h' }; -static const symbol s_2_905[4] = { 'e', 'v', 'i', 'h' }; -static const symbol s_2_906[4] = { 'i', 'v', 'i', 'h' }; -static const symbol s_2_907[4] = { 'o', 'v', 'i', 'h' }; -static const symbol s_2_908[5] = { 'a', 0xC4, 0x87, 'i', 'h' }; -static const symbol s_2_909[5] = { 'e', 0xC4, 0x87, 'i', 'h' }; -static const symbol s_2_910[5] = { 'u', 0xC4, 0x87, 'i', 'h' }; -static const symbol s_2_911[5] = { 'a', 0xC4, 0x8D, 'i', 'h' }; -static const symbol s_2_912[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 'h' }; -static const symbol s_2_913[5] = { 'o', 0xC5, 0xA1, 'i', 'h' }; -static const symbol s_2_914[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 'h' }; -static const symbol s_2_915[7] = { 'a', 's', 't', 'a', 'd', 'o', 'h' }; -static const symbol s_2_916[7] = { 'i', 's', 't', 'a', 'd', 'o', 'h' }; -static const symbol s_2_917[7] = { 'o', 's', 't', 'a', 'd', 'o', 'h' }; -static const symbol s_2_918[4] = { 'a', 'c', 'u', 'h' }; -static const symbol s_2_919[4] = { 'e', 'c', 'u', 'h' }; -static const symbol s_2_920[4] = { 'u', 'c', 'u', 'h' }; -static const symbol s_2_921[5] = { 'a', 0xC4, 0x87, 'u', 'h' }; -static const symbol s_2_922[5] = { 'e', 0xC4, 0x87, 'u', 'h' }; -static const symbol s_2_923[5] = { 'u', 0xC4, 0x87, 'u', 'h' }; -static const symbol s_2_924[3] = { 'a', 'c', 'i' }; -static const symbol s_2_925[5] = { 'a', 'c', 'e', 'c', 'i' }; -static const symbol s_2_926[4] = { 'i', 'e', 'c', 'i' }; -static const symbol s_2_927[5] = { 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_928[7] = { 'i', 'r', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_929[7] = { 'u', 'r', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_930[8] = { 'a', 's', 't', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_931[8] = { 'i', 's', 't', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_932[8] = { 'o', 's', 't', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_933[7] = { 'a', 'v', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_934[7] = { 'e', 'v', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_935[7] = { 'i', 'v', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_936[7] = { 'u', 'v', 'a', 'j', 'u', 'c', 'i' }; -static const symbol s_2_937[5] = { 'u', 'j', 'u', 'c', 'i' }; -static const symbol s_2_938[8] = { 'l', 'u', 'c', 'u', 'j', 'u', 'c', 'i' }; -static const symbol s_2_939[7] = { 'i', 'r', 'u', 'j', 'u', 'c', 'i' }; -static const symbol s_2_940[4] = { 'l', 'u', 'c', 'i' }; -static const symbol s_2_941[4] = { 'n', 'u', 'c', 'i' }; -static const symbol s_2_942[5] = { 'e', 't', 'u', 'c', 'i' }; -static const symbol s_2_943[6] = { 'a', 's', 't', 'u', 'c', 'i' }; -static const symbol s_2_944[2] = { 'g', 'i' }; -static const symbol s_2_945[3] = { 'u', 'g', 'i' }; -static const symbol s_2_946[3] = { 'a', 'j', 'i' }; -static const symbol s_2_947[4] = { 'c', 'a', 'j', 'i' }; -static const symbol s_2_948[4] = { 'l', 'a', 'j', 'i' }; -static const symbol s_2_949[4] = { 'r', 'a', 'j', 'i' }; -static const symbol s_2_950[5] = { 0xC4, 0x87, 'a', 'j', 'i' }; -static const symbol s_2_951[5] = { 0xC4, 0x8D, 'a', 'j', 'i' }; -static const symbol s_2_952[5] = { 0xC4, 0x91, 'a', 'j', 'i' }; -static const symbol s_2_953[4] = { 'b', 'i', 'j', 'i' }; -static const symbol s_2_954[4] = { 'c', 'i', 'j', 'i' }; -static const symbol s_2_955[4] = { 'd', 'i', 'j', 'i' }; -static const symbol s_2_956[4] = { 'f', 'i', 'j', 'i' }; -static const symbol s_2_957[4] = { 'g', 'i', 'j', 'i' }; -static const symbol s_2_958[6] = { 'a', 'n', 'j', 'i', 'j', 'i' }; -static const symbol s_2_959[6] = { 'e', 'n', 'j', 'i', 'j', 'i' }; -static const symbol s_2_960[6] = { 's', 'n', 'j', 'i', 'j', 'i' }; -static const symbol s_2_961[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i' }; -static const symbol s_2_962[4] = { 'k', 'i', 'j', 'i' }; -static const symbol s_2_963[5] = { 's', 'k', 'i', 'j', 'i' }; -static const symbol s_2_964[6] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i' }; -static const symbol s_2_965[4] = { 'l', 'i', 'j', 'i' }; -static const symbol s_2_966[5] = { 'e', 'l', 'i', 'j', 'i' }; -static const symbol s_2_967[4] = { 'm', 'i', 'j', 'i' }; -static const symbol s_2_968[4] = { 'n', 'i', 'j', 'i' }; -static const symbol s_2_969[6] = { 'g', 'a', 'n', 'i', 'j', 'i' }; -static const symbol s_2_970[6] = { 'm', 'a', 'n', 'i', 'j', 'i' }; -static const symbol s_2_971[6] = { 'p', 'a', 'n', 'i', 'j', 'i' }; -static const symbol s_2_972[6] = { 'r', 'a', 'n', 'i', 'j', 'i' }; -static const symbol s_2_973[6] = { 't', 'a', 'n', 'i', 'j', 'i' }; -static const symbol s_2_974[4] = { 'p', 'i', 'j', 'i' }; -static const symbol s_2_975[4] = { 'r', 'i', 'j', 'i' }; -static const symbol s_2_976[4] = { 's', 'i', 'j', 'i' }; -static const symbol s_2_977[5] = { 'o', 's', 'i', 'j', 'i' }; -static const symbol s_2_978[4] = { 't', 'i', 'j', 'i' }; -static const symbol s_2_979[5] = { 'a', 't', 'i', 'j', 'i' }; -static const symbol s_2_980[7] = { 'e', 'v', 'i', 't', 'i', 'j', 'i' }; -static const symbol s_2_981[7] = { 'o', 'v', 'i', 't', 'i', 'j', 'i' }; -static const symbol s_2_982[6] = { 'a', 's', 't', 'i', 'j', 'i' }; -static const symbol s_2_983[5] = { 'a', 'v', 'i', 'j', 'i' }; -static const symbol s_2_984[5] = { 'e', 'v', 'i', 'j', 'i' }; -static const symbol s_2_985[5] = { 'i', 'v', 'i', 'j', 'i' }; -static const symbol s_2_986[5] = { 'o', 'v', 'i', 'j', 'i' }; -static const symbol s_2_987[4] = { 'z', 'i', 'j', 'i' }; -static const symbol s_2_988[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i' }; -static const symbol s_2_989[5] = { 0xC5, 0xBE, 'i', 'j', 'i' }; -static const symbol s_2_990[4] = { 'a', 'n', 'j', 'i' }; -static const symbol s_2_991[4] = { 'e', 'n', 'j', 'i' }; -static const symbol s_2_992[4] = { 's', 'n', 'j', 'i' }; -static const symbol s_2_993[5] = { 0xC5, 0xA1, 'n', 'j', 'i' }; -static const symbol s_2_994[2] = { 'k', 'i' }; -static const symbol s_2_995[3] = { 's', 'k', 'i' }; -static const symbol s_2_996[4] = { 0xC5, 0xA1, 'k', 'i' }; -static const symbol s_2_997[3] = { 'a', 'l', 'i' }; -static const symbol s_2_998[5] = { 'a', 'c', 'a', 'l', 'i' }; -static const symbol s_2_999[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'i' }; -static const symbol s_2_1000[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'i' }; -static const symbol s_2_1001[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'i' }; -static const symbol s_2_1002[5] = { 'i', 'j', 'a', 'l', 'i' }; -static const symbol s_2_1003[6] = { 'i', 'n', 'j', 'a', 'l', 'i' }; -static const symbol s_2_1004[4] = { 'n', 'a', 'l', 'i' }; -static const symbol s_2_1005[5] = { 'i', 'r', 'a', 'l', 'i' }; -static const symbol s_2_1006[5] = { 'u', 'r', 'a', 'l', 'i' }; -static const symbol s_2_1007[4] = { 't', 'a', 'l', 'i' }; -static const symbol s_2_1008[6] = { 'a', 's', 't', 'a', 'l', 'i' }; -static const symbol s_2_1009[6] = { 'i', 's', 't', 'a', 'l', 'i' }; -static const symbol s_2_1010[6] = { 'o', 's', 't', 'a', 'l', 'i' }; -static const symbol s_2_1011[5] = { 'a', 'v', 'a', 'l', 'i' }; -static const symbol s_2_1012[5] = { 'e', 'v', 'a', 'l', 'i' }; -static const symbol s_2_1013[5] = { 'i', 'v', 'a', 'l', 'i' }; -static const symbol s_2_1014[5] = { 'o', 'v', 'a', 'l', 'i' }; -static const symbol s_2_1015[5] = { 'u', 'v', 'a', 'l', 'i' }; -static const symbol s_2_1016[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'i' }; -static const symbol s_2_1017[3] = { 'e', 'l', 'i' }; -static const symbol s_2_1018[3] = { 'i', 'l', 'i' }; -static const symbol s_2_1019[5] = { 'a', 'c', 'i', 'l', 'i' }; -static const symbol s_2_1020[6] = { 'l', 'u', 'c', 'i', 'l', 'i' }; -static const symbol s_2_1021[4] = { 'n', 'i', 'l', 'i' }; -static const symbol s_2_1022[6] = { 'r', 'o', 's', 'i', 'l', 'i' }; -static const symbol s_2_1023[6] = { 'j', 'e', 't', 'i', 'l', 'i' }; -static const symbol s_2_1024[5] = { 'o', 'z', 'i', 'l', 'i' }; -static const symbol s_2_1025[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'i' }; -static const symbol s_2_1026[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'i' }; -static const symbol s_2_1027[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'i' }; -static const symbol s_2_1028[3] = { 'o', 'l', 'i' }; -static const symbol s_2_1029[4] = { 'a', 's', 'l', 'i' }; -static const symbol s_2_1030[4] = { 'n', 'u', 'l', 'i' }; -static const symbol s_2_1031[4] = { 'r', 'a', 'm', 'i' }; -static const symbol s_2_1032[4] = { 'l', 'e', 'm', 'i' }; -static const symbol s_2_1033[2] = { 'n', 'i' }; -static const symbol s_2_1034[3] = { 'a', 'n', 'i' }; -static const symbol s_2_1035[5] = { 'a', 'c', 'a', 'n', 'i' }; -static const symbol s_2_1036[5] = { 'u', 'r', 'a', 'n', 'i' }; -static const symbol s_2_1037[4] = { 't', 'a', 'n', 'i' }; -static const symbol s_2_1038[5] = { 'a', 'v', 'a', 'n', 'i' }; -static const symbol s_2_1039[5] = { 'e', 'v', 'a', 'n', 'i' }; -static const symbol s_2_1040[5] = { 'i', 'v', 'a', 'n', 'i' }; -static const symbol s_2_1041[5] = { 'u', 'v', 'a', 'n', 'i' }; -static const symbol s_2_1042[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'i' }; -static const symbol s_2_1043[5] = { 'a', 'c', 'e', 'n', 'i' }; -static const symbol s_2_1044[6] = { 'l', 'u', 'c', 'e', 'n', 'i' }; -static const symbol s_2_1045[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'i' }; -static const symbol s_2_1046[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'i' }; -static const symbol s_2_1047[3] = { 'i', 'n', 'i' }; -static const symbol s_2_1048[4] = { 'c', 'i', 'n', 'i' }; -static const symbol s_2_1049[5] = { 0xC4, 0x8D, 'i', 'n', 'i' }; -static const symbol s_2_1050[3] = { 'o', 'n', 'i' }; -static const symbol s_2_1051[3] = { 'a', 'r', 'i' }; -static const symbol s_2_1052[3] = { 'd', 'r', 'i' }; -static const symbol s_2_1053[3] = { 'e', 'r', 'i' }; -static const symbol s_2_1054[3] = { 'o', 'r', 'i' }; -static const symbol s_2_1055[4] = { 'b', 'a', 's', 'i' }; -static const symbol s_2_1056[4] = { 'g', 'a', 's', 'i' }; -static const symbol s_2_1057[4] = { 'j', 'a', 's', 'i' }; -static const symbol s_2_1058[4] = { 'k', 'a', 's', 'i' }; -static const symbol s_2_1059[4] = { 'n', 'a', 's', 'i' }; -static const symbol s_2_1060[4] = { 't', 'a', 's', 'i' }; -static const symbol s_2_1061[4] = { 'v', 'a', 's', 'i' }; -static const symbol s_2_1062[3] = { 'e', 's', 'i' }; -static const symbol s_2_1063[3] = { 'i', 's', 'i' }; -static const symbol s_2_1064[3] = { 'o', 's', 'i' }; -static const symbol s_2_1065[4] = { 'a', 'v', 's', 'i' }; -static const symbol s_2_1066[6] = { 'a', 'c', 'a', 'v', 's', 'i' }; -static const symbol s_2_1067[6] = { 'i', 'r', 'a', 'v', 's', 'i' }; -static const symbol s_2_1068[5] = { 't', 'a', 'v', 's', 'i' }; -static const symbol s_2_1069[6] = { 'e', 't', 'a', 'v', 's', 'i' }; -static const symbol s_2_1070[7] = { 'a', 's', 't', 'a', 'v', 's', 'i' }; -static const symbol s_2_1071[7] = { 'i', 's', 't', 'a', 'v', 's', 'i' }; -static const symbol s_2_1072[7] = { 'o', 's', 't', 'a', 'v', 's', 'i' }; -static const symbol s_2_1073[4] = { 'i', 'v', 's', 'i' }; -static const symbol s_2_1074[5] = { 'n', 'i', 'v', 's', 'i' }; -static const symbol s_2_1075[7] = { 'r', 'o', 's', 'i', 'v', 's', 'i' }; -static const symbol s_2_1076[5] = { 'n', 'u', 'v', 's', 'i' }; -static const symbol s_2_1077[3] = { 'a', 't', 'i' }; -static const symbol s_2_1078[5] = { 'a', 'c', 'a', 't', 'i' }; -static const symbol s_2_1079[8] = { 'a', 's', 't', 'a', 'j', 'a', 't', 'i' }; -static const symbol s_2_1080[8] = { 'i', 's', 't', 'a', 'j', 'a', 't', 'i' }; -static const symbol s_2_1081[8] = { 'o', 's', 't', 'a', 'j', 'a', 't', 'i' }; -static const symbol s_2_1082[6] = { 'i', 'n', 'j', 'a', 't', 'i' }; -static const symbol s_2_1083[5] = { 'i', 'k', 'a', 't', 'i' }; -static const symbol s_2_1084[4] = { 'l', 'a', 't', 'i' }; -static const symbol s_2_1085[5] = { 'i', 'r', 'a', 't', 'i' }; -static const symbol s_2_1086[5] = { 'u', 'r', 'a', 't', 'i' }; -static const symbol s_2_1087[4] = { 't', 'a', 't', 'i' }; -static const symbol s_2_1088[6] = { 'a', 's', 't', 'a', 't', 'i' }; -static const symbol s_2_1089[6] = { 'i', 's', 't', 'a', 't', 'i' }; -static const symbol s_2_1090[6] = { 'o', 's', 't', 'a', 't', 'i' }; -static const symbol s_2_1091[5] = { 'a', 'v', 'a', 't', 'i' }; -static const symbol s_2_1092[5] = { 'e', 'v', 'a', 't', 'i' }; -static const symbol s_2_1093[5] = { 'i', 'v', 'a', 't', 'i' }; -static const symbol s_2_1094[5] = { 'o', 'v', 'a', 't', 'i' }; -static const symbol s_2_1095[5] = { 'u', 'v', 'a', 't', 'i' }; -static const symbol s_2_1096[6] = { 'a', 0xC4, 0x8D, 'a', 't', 'i' }; -static const symbol s_2_1097[3] = { 'e', 't', 'i' }; -static const symbol s_2_1098[3] = { 'i', 't', 'i' }; -static const symbol s_2_1099[5] = { 'a', 'c', 'i', 't', 'i' }; -static const symbol s_2_1100[6] = { 'l', 'u', 'c', 'i', 't', 'i' }; -static const symbol s_2_1101[4] = { 'n', 'i', 't', 'i' }; -static const symbol s_2_1102[6] = { 'r', 'o', 's', 'i', 't', 'i' }; -static const symbol s_2_1103[6] = { 'j', 'e', 't', 'i', 't', 'i' }; -static const symbol s_2_1104[5] = { 'e', 'v', 'i', 't', 'i' }; -static const symbol s_2_1105[5] = { 'o', 'v', 'i', 't', 'i' }; -static const symbol s_2_1106[6] = { 'a', 0xC4, 0x8D, 'i', 't', 'i' }; -static const symbol s_2_1107[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 't', 'i' }; -static const symbol s_2_1108[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 't', 'i' }; -static const symbol s_2_1109[4] = { 'a', 's', 't', 'i' }; -static const symbol s_2_1110[4] = { 'e', 's', 't', 'i' }; -static const symbol s_2_1111[4] = { 'i', 's', 't', 'i' }; -static const symbol s_2_1112[4] = { 'k', 's', 't', 'i' }; -static const symbol s_2_1113[4] = { 'o', 's', 't', 'i' }; -static const symbol s_2_1114[4] = { 'n', 'u', 't', 'i' }; -static const symbol s_2_1115[3] = { 'a', 'v', 'i' }; -static const symbol s_2_1116[3] = { 'e', 'v', 'i' }; -static const symbol s_2_1117[5] = { 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1118[6] = { 'c', 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1119[6] = { 'l', 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1120[6] = { 'r', 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1121[7] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1122[7] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1123[7] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'i' }; -static const symbol s_2_1124[3] = { 'i', 'v', 'i' }; -static const symbol s_2_1125[3] = { 'o', 'v', 'i' }; -static const symbol s_2_1126[4] = { 'g', 'o', 'v', 'i' }; -static const symbol s_2_1127[5] = { 'u', 'g', 'o', 'v', 'i' }; -static const symbol s_2_1128[4] = { 'l', 'o', 'v', 'i' }; -static const symbol s_2_1129[5] = { 'o', 'l', 'o', 'v', 'i' }; -static const symbol s_2_1130[4] = { 'm', 'o', 'v', 'i' }; -static const symbol s_2_1131[5] = { 'o', 'n', 'o', 'v', 'i' }; -static const symbol s_2_1132[5] = { 'i', 'e', 0xC4, 0x87, 'i' }; -static const symbol s_2_1133[7] = { 'a', 0xC4, 0x8D, 'e', 0xC4, 0x87, 'i' }; -static const symbol s_2_1134[6] = { 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1135[8] = { 'i', 'r', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1136[8] = { 'u', 'r', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1137[9] = { 'a', 's', 't', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1138[9] = { 'i', 's', 't', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1139[9] = { 'o', 's', 't', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1140[8] = { 'a', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1141[8] = { 'e', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1142[8] = { 'i', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1143[8] = { 'u', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1144[6] = { 'u', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1145[8] = { 'i', 'r', 'u', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1146[10] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1147[5] = { 'n', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1148[6] = { 'e', 't', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1149[7] = { 'a', 's', 't', 'u', 0xC4, 0x87, 'i' }; -static const symbol s_2_1150[4] = { 'a', 0xC4, 0x8D, 'i' }; -static const symbol s_2_1151[5] = { 'l', 'u', 0xC4, 0x8D, 'i' }; -static const symbol s_2_1152[5] = { 'b', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1153[5] = { 'g', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1154[5] = { 'j', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1155[5] = { 'k', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1156[5] = { 'n', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1157[5] = { 't', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1158[5] = { 'v', 'a', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1159[4] = { 'e', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1160[4] = { 'i', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1161[4] = { 'o', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1162[5] = { 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1163[7] = { 'i', 'r', 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1164[6] = { 't', 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1165[7] = { 'e', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1166[8] = { 'a', 's', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1167[8] = { 'i', 's', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1168[8] = { 'o', 's', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1169[8] = { 'a', 0xC4, 0x8D, 'a', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1170[5] = { 'i', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1171[6] = { 'n', 'i', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1172[9] = { 'r', 'o', 0xC5, 0xA1, 'i', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1173[6] = { 'n', 'u', 'v', 0xC5, 0xA1, 'i' }; -static const symbol s_2_1174[2] = { 'a', 'j' }; -static const symbol s_2_1175[4] = { 'u', 'r', 'a', 'j' }; -static const symbol s_2_1176[3] = { 't', 'a', 'j' }; -static const symbol s_2_1177[4] = { 'a', 'v', 'a', 'j' }; -static const symbol s_2_1178[4] = { 'e', 'v', 'a', 'j' }; -static const symbol s_2_1179[4] = { 'i', 'v', 'a', 'j' }; -static const symbol s_2_1180[4] = { 'u', 'v', 'a', 'j' }; -static const symbol s_2_1181[2] = { 'i', 'j' }; -static const symbol s_2_1182[4] = { 'a', 'c', 'o', 'j' }; -static const symbol s_2_1183[4] = { 'e', 'c', 'o', 'j' }; -static const symbol s_2_1184[4] = { 'u', 'c', 'o', 'j' }; -static const symbol s_2_1185[7] = { 'a', 'n', 'j', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1186[7] = { 'e', 'n', 'j', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1187[7] = { 's', 'n', 'j', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1188[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1189[5] = { 'k', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1190[6] = { 's', 'k', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1191[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1192[6] = { 'e', 'l', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1193[5] = { 'n', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1194[6] = { 'o', 's', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1195[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1196[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1197[7] = { 'a', 's', 't', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1198[6] = { 'a', 'v', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1199[6] = { 'e', 'v', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1200[6] = { 'i', 'v', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1201[6] = { 'o', 'v', 'i', 'j', 'o', 'j' }; -static const symbol s_2_1202[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'o', 'j' }; -static const symbol s_2_1203[5] = { 'a', 'n', 'j', 'o', 'j' }; -static const symbol s_2_1204[5] = { 'e', 'n', 'j', 'o', 'j' }; -static const symbol s_2_1205[5] = { 's', 'n', 'j', 'o', 'j' }; -static const symbol s_2_1206[6] = { 0xC5, 0xA1, 'n', 'j', 'o', 'j' }; -static const symbol s_2_1207[3] = { 'k', 'o', 'j' }; -static const symbol s_2_1208[4] = { 's', 'k', 'o', 'j' }; -static const symbol s_2_1209[5] = { 0xC5, 0xA1, 'k', 'o', 'j' }; -static const symbol s_2_1210[4] = { 'a', 'l', 'o', 'j' }; -static const symbol s_2_1211[4] = { 'e', 'l', 'o', 'j' }; -static const symbol s_2_1212[3] = { 'n', 'o', 'j' }; -static const symbol s_2_1213[5] = { 'c', 'i', 'n', 'o', 'j' }; -static const symbol s_2_1214[6] = { 0xC4, 0x8D, 'i', 'n', 'o', 'j' }; -static const symbol s_2_1215[4] = { 'o', 's', 'o', 'j' }; -static const symbol s_2_1216[4] = { 'a', 't', 'o', 'j' }; -static const symbol s_2_1217[6] = { 'e', 'v', 'i', 't', 'o', 'j' }; -static const symbol s_2_1218[6] = { 'o', 'v', 'i', 't', 'o', 'j' }; -static const symbol s_2_1219[5] = { 'a', 's', 't', 'o', 'j' }; -static const symbol s_2_1220[4] = { 'a', 'v', 'o', 'j' }; -static const symbol s_2_1221[4] = { 'e', 'v', 'o', 'j' }; -static const symbol s_2_1222[4] = { 'i', 'v', 'o', 'j' }; -static const symbol s_2_1223[4] = { 'o', 'v', 'o', 'j' }; -static const symbol s_2_1224[5] = { 'a', 0xC4, 0x87, 'o', 'j' }; -static const symbol s_2_1225[5] = { 'e', 0xC4, 0x87, 'o', 'j' }; -static const symbol s_2_1226[5] = { 'u', 0xC4, 0x87, 'o', 'j' }; -static const symbol s_2_1227[5] = { 'o', 0xC5, 0xA1, 'o', 'j' }; -static const symbol s_2_1228[5] = { 'l', 'u', 'c', 'u', 'j' }; -static const symbol s_2_1229[4] = { 'i', 'r', 'u', 'j' }; -static const symbol s_2_1230[6] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j' }; -static const symbol s_2_1231[2] = { 'a', 'l' }; -static const symbol s_2_1232[4] = { 'i', 'r', 'a', 'l' }; -static const symbol s_2_1233[4] = { 'u', 'r', 'a', 'l' }; -static const symbol s_2_1234[2] = { 'e', 'l' }; -static const symbol s_2_1235[2] = { 'i', 'l' }; -static const symbol s_2_1236[2] = { 'a', 'm' }; -static const symbol s_2_1237[4] = { 'a', 'c', 'a', 'm' }; -static const symbol s_2_1238[4] = { 'i', 'r', 'a', 'm' }; -static const symbol s_2_1239[4] = { 'u', 'r', 'a', 'm' }; -static const symbol s_2_1240[3] = { 't', 'a', 'm' }; -static const symbol s_2_1241[4] = { 'a', 'v', 'a', 'm' }; -static const symbol s_2_1242[4] = { 'e', 'v', 'a', 'm' }; -static const symbol s_2_1243[4] = { 'i', 'v', 'a', 'm' }; -static const symbol s_2_1244[4] = { 'u', 'v', 'a', 'm' }; -static const symbol s_2_1245[5] = { 'a', 0xC4, 0x8D, 'a', 'm' }; -static const symbol s_2_1246[2] = { 'e', 'm' }; -static const symbol s_2_1247[4] = { 'a', 'c', 'e', 'm' }; -static const symbol s_2_1248[4] = { 'e', 'c', 'e', 'm' }; -static const symbol s_2_1249[4] = { 'u', 'c', 'e', 'm' }; -static const symbol s_2_1250[7] = { 'a', 's', 't', 'a', 'd', 'e', 'm' }; -static const symbol s_2_1251[7] = { 'i', 's', 't', 'a', 'd', 'e', 'm' }; -static const symbol s_2_1252[7] = { 'o', 's', 't', 'a', 'd', 'e', 'm' }; -static const symbol s_2_1253[4] = { 'a', 'j', 'e', 'm' }; -static const symbol s_2_1254[5] = { 'c', 'a', 'j', 'e', 'm' }; -static const symbol s_2_1255[5] = { 'l', 'a', 'j', 'e', 'm' }; -static const symbol s_2_1256[5] = { 'r', 'a', 'j', 'e', 'm' }; -static const symbol s_2_1257[7] = { 'a', 's', 't', 'a', 'j', 'e', 'm' }; -static const symbol s_2_1258[7] = { 'i', 's', 't', 'a', 'j', 'e', 'm' }; -static const symbol s_2_1259[7] = { 'o', 's', 't', 'a', 'j', 'e', 'm' }; -static const symbol s_2_1260[6] = { 0xC4, 0x87, 'a', 'j', 'e', 'm' }; -static const symbol s_2_1261[6] = { 0xC4, 0x8D, 'a', 'j', 'e', 'm' }; -static const symbol s_2_1262[6] = { 0xC4, 0x91, 'a', 'j', 'e', 'm' }; -static const symbol s_2_1263[4] = { 'i', 'j', 'e', 'm' }; -static const symbol s_2_1264[7] = { 'a', 'n', 'j', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1265[7] = { 'e', 'n', 'j', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1266[7] = { 's', 'n', 'j', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1267[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1268[5] = { 'k', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1269[6] = { 's', 'k', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1270[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1271[5] = { 'l', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1272[6] = { 'e', 'l', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1273[5] = { 'n', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1274[7] = { 'r', 'a', 'r', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1275[5] = { 's', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1276[6] = { 'o', 's', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1277[6] = { 'a', 't', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1278[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1279[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1280[6] = { 'o', 't', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1281[7] = { 'a', 's', 't', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1282[6] = { 'a', 'v', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1283[6] = { 'e', 'v', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1284[6] = { 'i', 'v', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1285[6] = { 'o', 'v', 'i', 'j', 'e', 'm' }; -static const symbol s_2_1286[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'm' }; -static const symbol s_2_1287[5] = { 'a', 'n', 'j', 'e', 'm' }; -static const symbol s_2_1288[5] = { 'e', 'n', 'j', 'e', 'm' }; -static const symbol s_2_1289[5] = { 'i', 'n', 'j', 'e', 'm' }; -static const symbol s_2_1290[5] = { 's', 'n', 'j', 'e', 'm' }; -static const symbol s_2_1291[6] = { 0xC5, 0xA1, 'n', 'j', 'e', 'm' }; -static const symbol s_2_1292[4] = { 'u', 'j', 'e', 'm' }; -static const symbol s_2_1293[7] = { 'l', 'u', 'c', 'u', 'j', 'e', 'm' }; -static const symbol s_2_1294[6] = { 'i', 'r', 'u', 'j', 'e', 'm' }; -static const symbol s_2_1295[8] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 'm' }; -static const symbol s_2_1296[3] = { 'k', 'e', 'm' }; -static const symbol s_2_1297[4] = { 's', 'k', 'e', 'm' }; -static const symbol s_2_1298[5] = { 0xC5, 0xA1, 'k', 'e', 'm' }; -static const symbol s_2_1299[4] = { 'e', 'l', 'e', 'm' }; -static const symbol s_2_1300[3] = { 'n', 'e', 'm' }; -static const symbol s_2_1301[4] = { 'a', 'n', 'e', 'm' }; -static const symbol s_2_1302[7] = { 'a', 's', 't', 'a', 'n', 'e', 'm' }; -static const symbol s_2_1303[7] = { 'i', 's', 't', 'a', 'n', 'e', 'm' }; -static const symbol s_2_1304[7] = { 'o', 's', 't', 'a', 'n', 'e', 'm' }; -static const symbol s_2_1305[4] = { 'e', 'n', 'e', 'm' }; -static const symbol s_2_1306[4] = { 's', 'n', 'e', 'm' }; -static const symbol s_2_1307[5] = { 0xC5, 0xA1, 'n', 'e', 'm' }; -static const symbol s_2_1308[5] = { 'b', 'a', 's', 'e', 'm' }; -static const symbol s_2_1309[5] = { 'g', 'a', 's', 'e', 'm' }; -static const symbol s_2_1310[5] = { 'j', 'a', 's', 'e', 'm' }; -static const symbol s_2_1311[5] = { 'k', 'a', 's', 'e', 'm' }; -static const symbol s_2_1312[5] = { 'n', 'a', 's', 'e', 'm' }; -static const symbol s_2_1313[5] = { 't', 'a', 's', 'e', 'm' }; -static const symbol s_2_1314[5] = { 'v', 'a', 's', 'e', 'm' }; -static const symbol s_2_1315[4] = { 'e', 's', 'e', 'm' }; -static const symbol s_2_1316[4] = { 'i', 's', 'e', 'm' }; -static const symbol s_2_1317[4] = { 'o', 's', 'e', 'm' }; -static const symbol s_2_1318[4] = { 'a', 't', 'e', 'm' }; -static const symbol s_2_1319[4] = { 'e', 't', 'e', 'm' }; -static const symbol s_2_1320[6] = { 'e', 'v', 'i', 't', 'e', 'm' }; -static const symbol s_2_1321[6] = { 'o', 'v', 'i', 't', 'e', 'm' }; -static const symbol s_2_1322[5] = { 'a', 's', 't', 'e', 'm' }; -static const symbol s_2_1323[5] = { 'i', 's', 't', 'e', 'm' }; -static const symbol s_2_1324[6] = { 'i', 0xC5, 0xA1, 't', 'e', 'm' }; -static const symbol s_2_1325[4] = { 'a', 'v', 'e', 'm' }; -static const symbol s_2_1326[4] = { 'e', 'v', 'e', 'm' }; -static const symbol s_2_1327[4] = { 'i', 'v', 'e', 'm' }; -static const symbol s_2_1328[5] = { 'a', 0xC4, 0x87, 'e', 'm' }; -static const symbol s_2_1329[5] = { 'e', 0xC4, 0x87, 'e', 'm' }; -static const symbol s_2_1330[5] = { 'u', 0xC4, 0x87, 'e', 'm' }; -static const symbol s_2_1331[6] = { 'b', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1332[6] = { 'g', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1333[6] = { 'j', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1334[6] = { 'k', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1335[6] = { 'n', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1336[6] = { 't', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1337[6] = { 'v', 'a', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1338[5] = { 'e', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1339[5] = { 'i', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1340[5] = { 'o', 0xC5, 0xA1, 'e', 'm' }; -static const symbol s_2_1341[2] = { 'i', 'm' }; -static const symbol s_2_1342[4] = { 'a', 'c', 'i', 'm' }; -static const symbol s_2_1343[4] = { 'e', 'c', 'i', 'm' }; -static const symbol s_2_1344[4] = { 'u', 'c', 'i', 'm' }; -static const symbol s_2_1345[5] = { 'l', 'u', 'c', 'i', 'm' }; -static const symbol s_2_1346[7] = { 'a', 'n', 'j', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1347[7] = { 'e', 'n', 'j', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1348[7] = { 's', 'n', 'j', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1349[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1350[5] = { 'k', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1351[6] = { 's', 'k', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1352[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1353[6] = { 'e', 'l', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1354[5] = { 'n', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1355[6] = { 'o', 's', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1356[6] = { 'a', 't', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1357[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1358[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1359[7] = { 'a', 's', 't', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1360[6] = { 'a', 'v', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1361[6] = { 'e', 'v', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1362[6] = { 'i', 'v', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1363[6] = { 'o', 'v', 'i', 'j', 'i', 'm' }; -static const symbol s_2_1364[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i', 'm' }; -static const symbol s_2_1365[5] = { 'a', 'n', 'j', 'i', 'm' }; -static const symbol s_2_1366[5] = { 'e', 'n', 'j', 'i', 'm' }; -static const symbol s_2_1367[5] = { 's', 'n', 'j', 'i', 'm' }; -static const symbol s_2_1368[6] = { 0xC5, 0xA1, 'n', 'j', 'i', 'm' }; -static const symbol s_2_1369[3] = { 'k', 'i', 'm' }; -static const symbol s_2_1370[4] = { 's', 'k', 'i', 'm' }; -static const symbol s_2_1371[5] = { 0xC5, 0xA1, 'k', 'i', 'm' }; -static const symbol s_2_1372[4] = { 'e', 'l', 'i', 'm' }; -static const symbol s_2_1373[3] = { 'n', 'i', 'm' }; -static const symbol s_2_1374[5] = { 'c', 'i', 'n', 'i', 'm' }; -static const symbol s_2_1375[6] = { 0xC4, 0x8D, 'i', 'n', 'i', 'm' }; -static const symbol s_2_1376[4] = { 'o', 's', 'i', 'm' }; -static const symbol s_2_1377[5] = { 'r', 'o', 's', 'i', 'm' }; -static const symbol s_2_1378[4] = { 'a', 't', 'i', 'm' }; -static const symbol s_2_1379[5] = { 'j', 'e', 't', 'i', 'm' }; -static const symbol s_2_1380[6] = { 'e', 'v', 'i', 't', 'i', 'm' }; -static const symbol s_2_1381[6] = { 'o', 'v', 'i', 't', 'i', 'm' }; -static const symbol s_2_1382[5] = { 'a', 's', 't', 'i', 'm' }; -static const symbol s_2_1383[4] = { 'a', 'v', 'i', 'm' }; -static const symbol s_2_1384[4] = { 'e', 'v', 'i', 'm' }; -static const symbol s_2_1385[4] = { 'i', 'v', 'i', 'm' }; -static const symbol s_2_1386[4] = { 'o', 'v', 'i', 'm' }; -static const symbol s_2_1387[5] = { 'a', 0xC4, 0x87, 'i', 'm' }; -static const symbol s_2_1388[5] = { 'e', 0xC4, 0x87, 'i', 'm' }; -static const symbol s_2_1389[5] = { 'u', 0xC4, 0x87, 'i', 'm' }; -static const symbol s_2_1390[5] = { 'a', 0xC4, 0x8D, 'i', 'm' }; -static const symbol s_2_1391[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 'm' }; -static const symbol s_2_1392[5] = { 'o', 0xC5, 0xA1, 'i', 'm' }; -static const symbol s_2_1393[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 'm' }; -static const symbol s_2_1394[4] = { 'a', 'c', 'o', 'm' }; -static const symbol s_2_1395[4] = { 'e', 'c', 'o', 'm' }; -static const symbol s_2_1396[4] = { 'u', 'c', 'o', 'm' }; -static const symbol s_2_1397[3] = { 'g', 'o', 'm' }; -static const symbol s_2_1398[5] = { 'l', 'o', 'g', 'o', 'm' }; -static const symbol s_2_1399[4] = { 'u', 'g', 'o', 'm' }; -static const symbol s_2_1400[5] = { 'b', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1401[5] = { 'c', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1402[5] = { 'd', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1403[5] = { 'f', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1404[5] = { 'g', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1405[5] = { 'l', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1406[5] = { 'm', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1407[5] = { 'n', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1408[7] = { 'g', 'a', 'n', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1409[7] = { 'm', 'a', 'n', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1410[7] = { 'p', 'a', 'n', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1411[7] = { 'r', 'a', 'n', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1412[7] = { 't', 'a', 'n', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1413[5] = { 'p', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1414[5] = { 'r', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1415[5] = { 's', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1416[5] = { 't', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1417[5] = { 'z', 'i', 'j', 'o', 'm' }; -static const symbol s_2_1418[6] = { 0xC5, 0xBE, 'i', 'j', 'o', 'm' }; -static const symbol s_2_1419[5] = { 'a', 'n', 'j', 'o', 'm' }; -static const symbol s_2_1420[5] = { 'e', 'n', 'j', 'o', 'm' }; -static const symbol s_2_1421[5] = { 's', 'n', 'j', 'o', 'm' }; -static const symbol s_2_1422[6] = { 0xC5, 0xA1, 'n', 'j', 'o', 'm' }; -static const symbol s_2_1423[3] = { 'k', 'o', 'm' }; -static const symbol s_2_1424[4] = { 's', 'k', 'o', 'm' }; -static const symbol s_2_1425[5] = { 0xC5, 0xA1, 'k', 'o', 'm' }; -static const symbol s_2_1426[4] = { 'a', 'l', 'o', 'm' }; -static const symbol s_2_1427[6] = { 'i', 'j', 'a', 'l', 'o', 'm' }; -static const symbol s_2_1428[5] = { 'n', 'a', 'l', 'o', 'm' }; -static const symbol s_2_1429[4] = { 'e', 'l', 'o', 'm' }; -static const symbol s_2_1430[4] = { 'i', 'l', 'o', 'm' }; -static const symbol s_2_1431[6] = { 'o', 'z', 'i', 'l', 'o', 'm' }; -static const symbol s_2_1432[4] = { 'o', 'l', 'o', 'm' }; -static const symbol s_2_1433[5] = { 'r', 'a', 'm', 'o', 'm' }; -static const symbol s_2_1434[5] = { 'l', 'e', 'm', 'o', 'm' }; -static const symbol s_2_1435[3] = { 'n', 'o', 'm' }; -static const symbol s_2_1436[4] = { 'a', 'n', 'o', 'm' }; -static const symbol s_2_1437[4] = { 'i', 'n', 'o', 'm' }; -static const symbol s_2_1438[5] = { 'c', 'i', 'n', 'o', 'm' }; -static const symbol s_2_1439[6] = { 'a', 'n', 'i', 'n', 'o', 'm' }; -static const symbol s_2_1440[6] = { 0xC4, 0x8D, 'i', 'n', 'o', 'm' }; -static const symbol s_2_1441[4] = { 'o', 'n', 'o', 'm' }; -static const symbol s_2_1442[4] = { 'a', 'r', 'o', 'm' }; -static const symbol s_2_1443[4] = { 'd', 'r', 'o', 'm' }; -static const symbol s_2_1444[4] = { 'e', 'r', 'o', 'm' }; -static const symbol s_2_1445[4] = { 'o', 'r', 'o', 'm' }; -static const symbol s_2_1446[5] = { 'b', 'a', 's', 'o', 'm' }; -static const symbol s_2_1447[5] = { 'g', 'a', 's', 'o', 'm' }; -static const symbol s_2_1448[5] = { 'j', 'a', 's', 'o', 'm' }; -static const symbol s_2_1449[5] = { 'k', 'a', 's', 'o', 'm' }; -static const symbol s_2_1450[5] = { 'n', 'a', 's', 'o', 'm' }; -static const symbol s_2_1451[5] = { 't', 'a', 's', 'o', 'm' }; -static const symbol s_2_1452[5] = { 'v', 'a', 's', 'o', 'm' }; -static const symbol s_2_1453[4] = { 'e', 's', 'o', 'm' }; -static const symbol s_2_1454[4] = { 'i', 's', 'o', 'm' }; -static const symbol s_2_1455[4] = { 'o', 's', 'o', 'm' }; -static const symbol s_2_1456[4] = { 'a', 't', 'o', 'm' }; -static const symbol s_2_1457[6] = { 'i', 'k', 'a', 't', 'o', 'm' }; -static const symbol s_2_1458[5] = { 'l', 'a', 't', 'o', 'm' }; -static const symbol s_2_1459[4] = { 'e', 't', 'o', 'm' }; -static const symbol s_2_1460[6] = { 'e', 'v', 'i', 't', 'o', 'm' }; -static const symbol s_2_1461[6] = { 'o', 'v', 'i', 't', 'o', 'm' }; -static const symbol s_2_1462[5] = { 'a', 's', 't', 'o', 'm' }; -static const symbol s_2_1463[5] = { 'e', 's', 't', 'o', 'm' }; -static const symbol s_2_1464[5] = { 'i', 's', 't', 'o', 'm' }; -static const symbol s_2_1465[5] = { 'k', 's', 't', 'o', 'm' }; -static const symbol s_2_1466[5] = { 'o', 's', 't', 'o', 'm' }; -static const symbol s_2_1467[4] = { 'a', 'v', 'o', 'm' }; -static const symbol s_2_1468[4] = { 'e', 'v', 'o', 'm' }; -static const symbol s_2_1469[4] = { 'i', 'v', 'o', 'm' }; -static const symbol s_2_1470[4] = { 'o', 'v', 'o', 'm' }; -static const symbol s_2_1471[5] = { 'l', 'o', 'v', 'o', 'm' }; -static const symbol s_2_1472[5] = { 'm', 'o', 'v', 'o', 'm' }; -static const symbol s_2_1473[5] = { 's', 't', 'v', 'o', 'm' }; -static const symbol s_2_1474[6] = { 0xC5, 0xA1, 't', 'v', 'o', 'm' }; -static const symbol s_2_1475[5] = { 'a', 0xC4, 0x87, 'o', 'm' }; -static const symbol s_2_1476[5] = { 'e', 0xC4, 0x87, 'o', 'm' }; -static const symbol s_2_1477[5] = { 'u', 0xC4, 0x87, 'o', 'm' }; -static const symbol s_2_1478[6] = { 'b', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1479[6] = { 'g', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1480[6] = { 'j', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1481[6] = { 'k', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1482[6] = { 'n', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1483[6] = { 't', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1484[6] = { 'v', 'a', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1485[5] = { 'e', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1486[5] = { 'i', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1487[5] = { 'o', 0xC5, 0xA1, 'o', 'm' }; -static const symbol s_2_1488[2] = { 'a', 'n' }; -static const symbol s_2_1489[4] = { 'a', 'c', 'a', 'n' }; -static const symbol s_2_1490[4] = { 'i', 'r', 'a', 'n' }; -static const symbol s_2_1491[4] = { 'u', 'r', 'a', 'n' }; -static const symbol s_2_1492[3] = { 't', 'a', 'n' }; -static const symbol s_2_1493[4] = { 'a', 'v', 'a', 'n' }; -static const symbol s_2_1494[4] = { 'e', 'v', 'a', 'n' }; -static const symbol s_2_1495[4] = { 'i', 'v', 'a', 'n' }; -static const symbol s_2_1496[4] = { 'u', 'v', 'a', 'n' }; -static const symbol s_2_1497[5] = { 'a', 0xC4, 0x8D, 'a', 'n' }; -static const symbol s_2_1498[4] = { 'a', 'c', 'e', 'n' }; -static const symbol s_2_1499[5] = { 'l', 'u', 'c', 'e', 'n' }; -static const symbol s_2_1500[5] = { 'a', 0xC4, 0x8D, 'e', 'n' }; -static const symbol s_2_1501[6] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n' }; -static const symbol s_2_1502[4] = { 'a', 'n', 'i', 'n' }; -static const symbol s_2_1503[2] = { 'a', 'o' }; -static const symbol s_2_1504[4] = { 'a', 'c', 'a', 'o' }; -static const symbol s_2_1505[7] = { 'a', 's', 't', 'a', 'j', 'a', 'o' }; -static const symbol s_2_1506[7] = { 'i', 's', 't', 'a', 'j', 'a', 'o' }; -static const symbol s_2_1507[7] = { 'o', 's', 't', 'a', 'j', 'a', 'o' }; -static const symbol s_2_1508[5] = { 'i', 'n', 'j', 'a', 'o' }; -static const symbol s_2_1509[4] = { 'i', 'r', 'a', 'o' }; -static const symbol s_2_1510[4] = { 'u', 'r', 'a', 'o' }; -static const symbol s_2_1511[3] = { 't', 'a', 'o' }; -static const symbol s_2_1512[5] = { 'a', 's', 't', 'a', 'o' }; -static const symbol s_2_1513[5] = { 'i', 's', 't', 'a', 'o' }; -static const symbol s_2_1514[5] = { 'o', 's', 't', 'a', 'o' }; -static const symbol s_2_1515[4] = { 'a', 'v', 'a', 'o' }; -static const symbol s_2_1516[4] = { 'e', 'v', 'a', 'o' }; -static const symbol s_2_1517[4] = { 'i', 'v', 'a', 'o' }; -static const symbol s_2_1518[4] = { 'o', 'v', 'a', 'o' }; -static const symbol s_2_1519[4] = { 'u', 'v', 'a', 'o' }; -static const symbol s_2_1520[5] = { 'a', 0xC4, 0x8D, 'a', 'o' }; -static const symbol s_2_1521[2] = { 'g', 'o' }; -static const symbol s_2_1522[3] = { 'u', 'g', 'o' }; -static const symbol s_2_1523[2] = { 'i', 'o' }; -static const symbol s_2_1524[4] = { 'a', 'c', 'i', 'o' }; -static const symbol s_2_1525[5] = { 'l', 'u', 'c', 'i', 'o' }; -static const symbol s_2_1526[3] = { 'l', 'i', 'o' }; -static const symbol s_2_1527[3] = { 'n', 'i', 'o' }; -static const symbol s_2_1528[5] = { 'r', 'a', 'r', 'i', 'o' }; -static const symbol s_2_1529[3] = { 's', 'i', 'o' }; -static const symbol s_2_1530[5] = { 'r', 'o', 's', 'i', 'o' }; -static const symbol s_2_1531[5] = { 'j', 'e', 't', 'i', 'o' }; -static const symbol s_2_1532[4] = { 'o', 't', 'i', 'o' }; -static const symbol s_2_1533[5] = { 'a', 0xC4, 0x8D, 'i', 'o' }; -static const symbol s_2_1534[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 'o' }; -static const symbol s_2_1535[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 'o' }; -static const symbol s_2_1536[4] = { 'b', 'i', 'j', 'o' }; -static const symbol s_2_1537[4] = { 'c', 'i', 'j', 'o' }; -static const symbol s_2_1538[4] = { 'd', 'i', 'j', 'o' }; -static const symbol s_2_1539[4] = { 'f', 'i', 'j', 'o' }; -static const symbol s_2_1540[4] = { 'g', 'i', 'j', 'o' }; -static const symbol s_2_1541[4] = { 'l', 'i', 'j', 'o' }; -static const symbol s_2_1542[4] = { 'm', 'i', 'j', 'o' }; -static const symbol s_2_1543[4] = { 'n', 'i', 'j', 'o' }; -static const symbol s_2_1544[4] = { 'p', 'i', 'j', 'o' }; -static const symbol s_2_1545[4] = { 'r', 'i', 'j', 'o' }; -static const symbol s_2_1546[4] = { 's', 'i', 'j', 'o' }; -static const symbol s_2_1547[4] = { 't', 'i', 'j', 'o' }; -static const symbol s_2_1548[4] = { 'z', 'i', 'j', 'o' }; -static const symbol s_2_1549[5] = { 0xC5, 0xBE, 'i', 'j', 'o' }; -static const symbol s_2_1550[4] = { 'a', 'n', 'j', 'o' }; -static const symbol s_2_1551[4] = { 'e', 'n', 'j', 'o' }; -static const symbol s_2_1552[4] = { 's', 'n', 'j', 'o' }; -static const symbol s_2_1553[5] = { 0xC5, 0xA1, 'n', 'j', 'o' }; -static const symbol s_2_1554[2] = { 'k', 'o' }; -static const symbol s_2_1555[3] = { 's', 'k', 'o' }; -static const symbol s_2_1556[4] = { 0xC5, 0xA1, 'k', 'o' }; -static const symbol s_2_1557[3] = { 'a', 'l', 'o' }; -static const symbol s_2_1558[5] = { 'a', 'c', 'a', 'l', 'o' }; -static const symbol s_2_1559[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'o' }; -static const symbol s_2_1560[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'o' }; -static const symbol s_2_1561[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'o' }; -static const symbol s_2_1562[5] = { 'i', 'j', 'a', 'l', 'o' }; -static const symbol s_2_1563[6] = { 'i', 'n', 'j', 'a', 'l', 'o' }; -static const symbol s_2_1564[4] = { 'n', 'a', 'l', 'o' }; -static const symbol s_2_1565[5] = { 'i', 'r', 'a', 'l', 'o' }; -static const symbol s_2_1566[5] = { 'u', 'r', 'a', 'l', 'o' }; -static const symbol s_2_1567[4] = { 't', 'a', 'l', 'o' }; -static const symbol s_2_1568[6] = { 'a', 's', 't', 'a', 'l', 'o' }; -static const symbol s_2_1569[6] = { 'i', 's', 't', 'a', 'l', 'o' }; -static const symbol s_2_1570[6] = { 'o', 's', 't', 'a', 'l', 'o' }; -static const symbol s_2_1571[5] = { 'a', 'v', 'a', 'l', 'o' }; -static const symbol s_2_1572[5] = { 'e', 'v', 'a', 'l', 'o' }; -static const symbol s_2_1573[5] = { 'i', 'v', 'a', 'l', 'o' }; -static const symbol s_2_1574[5] = { 'o', 'v', 'a', 'l', 'o' }; -static const symbol s_2_1575[5] = { 'u', 'v', 'a', 'l', 'o' }; -static const symbol s_2_1576[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'o' }; -static const symbol s_2_1577[3] = { 'e', 'l', 'o' }; -static const symbol s_2_1578[3] = { 'i', 'l', 'o' }; -static const symbol s_2_1579[5] = { 'a', 'c', 'i', 'l', 'o' }; -static const symbol s_2_1580[6] = { 'l', 'u', 'c', 'i', 'l', 'o' }; -static const symbol s_2_1581[4] = { 'n', 'i', 'l', 'o' }; -static const symbol s_2_1582[6] = { 'r', 'o', 's', 'i', 'l', 'o' }; -static const symbol s_2_1583[6] = { 'j', 'e', 't', 'i', 'l', 'o' }; -static const symbol s_2_1584[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'o' }; -static const symbol s_2_1585[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'o' }; -static const symbol s_2_1586[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'o' }; -static const symbol s_2_1587[4] = { 'a', 's', 'l', 'o' }; -static const symbol s_2_1588[4] = { 'n', 'u', 'l', 'o' }; -static const symbol s_2_1589[3] = { 'a', 'm', 'o' }; -static const symbol s_2_1590[5] = { 'a', 'c', 'a', 'm', 'o' }; -static const symbol s_2_1591[4] = { 'r', 'a', 'm', 'o' }; -static const symbol s_2_1592[5] = { 'i', 'r', 'a', 'm', 'o' }; -static const symbol s_2_1593[5] = { 'u', 'r', 'a', 'm', 'o' }; -static const symbol s_2_1594[4] = { 't', 'a', 'm', 'o' }; -static const symbol s_2_1595[5] = { 'a', 'v', 'a', 'm', 'o' }; -static const symbol s_2_1596[5] = { 'e', 'v', 'a', 'm', 'o' }; -static const symbol s_2_1597[5] = { 'i', 'v', 'a', 'm', 'o' }; -static const symbol s_2_1598[5] = { 'u', 'v', 'a', 'm', 'o' }; -static const symbol s_2_1599[6] = { 'a', 0xC4, 0x8D, 'a', 'm', 'o' }; -static const symbol s_2_1600[3] = { 'e', 'm', 'o' }; -static const symbol s_2_1601[8] = { 'a', 's', 't', 'a', 'd', 'e', 'm', 'o' }; -static const symbol s_2_1602[8] = { 'i', 's', 't', 'a', 'd', 'e', 'm', 'o' }; -static const symbol s_2_1603[8] = { 'o', 's', 't', 'a', 'd', 'e', 'm', 'o' }; -static const symbol s_2_1604[8] = { 'a', 's', 't', 'a', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1605[8] = { 'i', 's', 't', 'a', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1606[8] = { 'o', 's', 't', 'a', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1607[5] = { 'i', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1608[6] = { 'i', 'n', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1609[5] = { 'u', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1610[8] = { 'l', 'u', 'c', 'u', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1611[7] = { 'i', 'r', 'u', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1612[9] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 'm', 'o' }; -static const symbol s_2_1613[4] = { 'l', 'e', 'm', 'o' }; -static const symbol s_2_1614[4] = { 'n', 'e', 'm', 'o' }; -static const symbol s_2_1615[8] = { 'a', 's', 't', 'a', 'n', 'e', 'm', 'o' }; -static const symbol s_2_1616[8] = { 'i', 's', 't', 'a', 'n', 'e', 'm', 'o' }; -static const symbol s_2_1617[8] = { 'o', 's', 't', 'a', 'n', 'e', 'm', 'o' }; -static const symbol s_2_1618[5] = { 'e', 't', 'e', 'm', 'o' }; -static const symbol s_2_1619[6] = { 'a', 's', 't', 'e', 'm', 'o' }; -static const symbol s_2_1620[3] = { 'i', 'm', 'o' }; -static const symbol s_2_1621[5] = { 'a', 'c', 'i', 'm', 'o' }; -static const symbol s_2_1622[6] = { 'l', 'u', 'c', 'i', 'm', 'o' }; -static const symbol s_2_1623[4] = { 'n', 'i', 'm', 'o' }; -static const symbol s_2_1624[8] = { 'a', 's', 't', 'a', 'n', 'i', 'm', 'o' }; -static const symbol s_2_1625[8] = { 'i', 's', 't', 'a', 'n', 'i', 'm', 'o' }; -static const symbol s_2_1626[8] = { 'o', 's', 't', 'a', 'n', 'i', 'm', 'o' }; -static const symbol s_2_1627[6] = { 'r', 'o', 's', 'i', 'm', 'o' }; -static const symbol s_2_1628[5] = { 'e', 't', 'i', 'm', 'o' }; -static const symbol s_2_1629[6] = { 'j', 'e', 't', 'i', 'm', 'o' }; -static const symbol s_2_1630[6] = { 'a', 's', 't', 'i', 'm', 'o' }; -static const symbol s_2_1631[6] = { 'a', 0xC4, 0x8D, 'i', 'm', 'o' }; -static const symbol s_2_1632[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'm', 'o' }; -static const symbol s_2_1633[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'm', 'o' }; -static const symbol s_2_1634[4] = { 'a', 'j', 'm', 'o' }; -static const symbol s_2_1635[6] = { 'u', 'r', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1636[5] = { 't', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1637[7] = { 'a', 's', 't', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1638[7] = { 'i', 's', 't', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1639[7] = { 'o', 's', 't', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1640[6] = { 'a', 'v', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1641[6] = { 'e', 'v', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1642[6] = { 'i', 'v', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1643[6] = { 'u', 'v', 'a', 'j', 'm', 'o' }; -static const symbol s_2_1644[4] = { 'i', 'j', 'm', 'o' }; -static const symbol s_2_1645[4] = { 'u', 'j', 'm', 'o' }; -static const symbol s_2_1646[7] = { 'l', 'u', 'c', 'u', 'j', 'm', 'o' }; -static const symbol s_2_1647[6] = { 'i', 'r', 'u', 'j', 'm', 'o' }; -static const symbol s_2_1648[8] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'm', 'o' }; -static const symbol s_2_1649[4] = { 'a', 's', 'm', 'o' }; -static const symbol s_2_1650[6] = { 'a', 'c', 'a', 's', 'm', 'o' }; -static const symbol s_2_1651[9] = { 'a', 's', 't', 'a', 'j', 'a', 's', 'm', 'o' }; -static const symbol s_2_1652[9] = { 'i', 's', 't', 'a', 'j', 'a', 's', 'm', 'o' }; -static const symbol s_2_1653[9] = { 'o', 's', 't', 'a', 'j', 'a', 's', 'm', 'o' }; -static const symbol s_2_1654[7] = { 'i', 'n', 'j', 'a', 's', 'm', 'o' }; -static const symbol s_2_1655[6] = { 'i', 'r', 'a', 's', 'm', 'o' }; -static const symbol s_2_1656[6] = { 'u', 'r', 'a', 's', 'm', 'o' }; -static const symbol s_2_1657[5] = { 't', 'a', 's', 'm', 'o' }; -static const symbol s_2_1658[6] = { 'a', 'v', 'a', 's', 'm', 'o' }; -static const symbol s_2_1659[6] = { 'e', 'v', 'a', 's', 'm', 'o' }; -static const symbol s_2_1660[6] = { 'i', 'v', 'a', 's', 'm', 'o' }; -static const symbol s_2_1661[6] = { 'o', 'v', 'a', 's', 'm', 'o' }; -static const symbol s_2_1662[6] = { 'u', 'v', 'a', 's', 'm', 'o' }; -static const symbol s_2_1663[7] = { 'a', 0xC4, 0x8D, 'a', 's', 'm', 'o' }; -static const symbol s_2_1664[4] = { 'i', 's', 'm', 'o' }; -static const symbol s_2_1665[6] = { 'a', 'c', 'i', 's', 'm', 'o' }; -static const symbol s_2_1666[7] = { 'l', 'u', 'c', 'i', 's', 'm', 'o' }; -static const symbol s_2_1667[5] = { 'n', 'i', 's', 'm', 'o' }; -static const symbol s_2_1668[7] = { 'r', 'o', 's', 'i', 's', 'm', 'o' }; -static const symbol s_2_1669[7] = { 'j', 'e', 't', 'i', 's', 'm', 'o' }; -static const symbol s_2_1670[7] = { 'a', 0xC4, 0x8D, 'i', 's', 'm', 'o' }; -static const symbol s_2_1671[8] = { 'l', 'u', 0xC4, 0x8D, 'i', 's', 'm', 'o' }; -static const symbol s_2_1672[8] = { 'r', 'o', 0xC5, 0xA1, 'i', 's', 'm', 'o' }; -static const symbol s_2_1673[9] = { 'a', 's', 't', 'a', 'd', 'o', 's', 'm', 'o' }; -static const symbol s_2_1674[9] = { 'i', 's', 't', 'a', 'd', 'o', 's', 'm', 'o' }; -static const symbol s_2_1675[9] = { 'o', 's', 't', 'a', 'd', 'o', 's', 'm', 'o' }; -static const symbol s_2_1676[5] = { 'n', 'u', 's', 'm', 'o' }; -static const symbol s_2_1677[2] = { 'n', 'o' }; -static const symbol s_2_1678[3] = { 'a', 'n', 'o' }; -static const symbol s_2_1679[5] = { 'a', 'c', 'a', 'n', 'o' }; -static const symbol s_2_1680[5] = { 'u', 'r', 'a', 'n', 'o' }; -static const symbol s_2_1681[4] = { 't', 'a', 'n', 'o' }; -static const symbol s_2_1682[5] = { 'a', 'v', 'a', 'n', 'o' }; -static const symbol s_2_1683[5] = { 'e', 'v', 'a', 'n', 'o' }; -static const symbol s_2_1684[5] = { 'i', 'v', 'a', 'n', 'o' }; -static const symbol s_2_1685[5] = { 'u', 'v', 'a', 'n', 'o' }; -static const symbol s_2_1686[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'o' }; -static const symbol s_2_1687[5] = { 'a', 'c', 'e', 'n', 'o' }; -static const symbol s_2_1688[6] = { 'l', 'u', 'c', 'e', 'n', 'o' }; -static const symbol s_2_1689[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'o' }; -static const symbol s_2_1690[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'o' }; -static const symbol s_2_1691[3] = { 'i', 'n', 'o' }; -static const symbol s_2_1692[4] = { 'c', 'i', 'n', 'o' }; -static const symbol s_2_1693[5] = { 0xC4, 0x8D, 'i', 'n', 'o' }; -static const symbol s_2_1694[3] = { 'a', 't', 'o' }; -static const symbol s_2_1695[5] = { 'i', 'k', 'a', 't', 'o' }; -static const symbol s_2_1696[4] = { 'l', 'a', 't', 'o' }; -static const symbol s_2_1697[3] = { 'e', 't', 'o' }; -static const symbol s_2_1698[5] = { 'e', 'v', 'i', 't', 'o' }; -static const symbol s_2_1699[5] = { 'o', 'v', 'i', 't', 'o' }; -static const symbol s_2_1700[4] = { 'a', 's', 't', 'o' }; -static const symbol s_2_1701[4] = { 'e', 's', 't', 'o' }; -static const symbol s_2_1702[4] = { 'i', 's', 't', 'o' }; -static const symbol s_2_1703[4] = { 'k', 's', 't', 'o' }; -static const symbol s_2_1704[4] = { 'o', 's', 't', 'o' }; -static const symbol s_2_1705[4] = { 'n', 'u', 't', 'o' }; -static const symbol s_2_1706[3] = { 'n', 'u', 'o' }; -static const symbol s_2_1707[3] = { 'a', 'v', 'o' }; -static const symbol s_2_1708[3] = { 'e', 'v', 'o' }; -static const symbol s_2_1709[3] = { 'i', 'v', 'o' }; -static const symbol s_2_1710[3] = { 'o', 'v', 'o' }; -static const symbol s_2_1711[4] = { 's', 't', 'v', 'o' }; -static const symbol s_2_1712[5] = { 0xC5, 0xA1, 't', 'v', 'o' }; -static const symbol s_2_1713[2] = { 'a', 's' }; -static const symbol s_2_1714[4] = { 'a', 'c', 'a', 's' }; -static const symbol s_2_1715[4] = { 'i', 'r', 'a', 's' }; -static const symbol s_2_1716[4] = { 'u', 'r', 'a', 's' }; -static const symbol s_2_1717[3] = { 't', 'a', 's' }; -static const symbol s_2_1718[4] = { 'a', 'v', 'a', 's' }; -static const symbol s_2_1719[4] = { 'e', 'v', 'a', 's' }; -static const symbol s_2_1720[4] = { 'i', 'v', 'a', 's' }; -static const symbol s_2_1721[4] = { 'u', 'v', 'a', 's' }; -static const symbol s_2_1722[2] = { 'e', 's' }; -static const symbol s_2_1723[7] = { 'a', 's', 't', 'a', 'd', 'e', 's' }; -static const symbol s_2_1724[7] = { 'i', 's', 't', 'a', 'd', 'e', 's' }; -static const symbol s_2_1725[7] = { 'o', 's', 't', 'a', 'd', 'e', 's' }; -static const symbol s_2_1726[7] = { 'a', 's', 't', 'a', 'j', 'e', 's' }; -static const symbol s_2_1727[7] = { 'i', 's', 't', 'a', 'j', 'e', 's' }; -static const symbol s_2_1728[7] = { 'o', 's', 't', 'a', 'j', 'e', 's' }; -static const symbol s_2_1729[4] = { 'i', 'j', 'e', 's' }; -static const symbol s_2_1730[5] = { 'i', 'n', 'j', 'e', 's' }; -static const symbol s_2_1731[4] = { 'u', 'j', 'e', 's' }; -static const symbol s_2_1732[7] = { 'l', 'u', 'c', 'u', 'j', 'e', 's' }; -static const symbol s_2_1733[6] = { 'i', 'r', 'u', 'j', 'e', 's' }; -static const symbol s_2_1734[3] = { 'n', 'e', 's' }; -static const symbol s_2_1735[7] = { 'a', 's', 't', 'a', 'n', 'e', 's' }; -static const symbol s_2_1736[7] = { 'i', 's', 't', 'a', 'n', 'e', 's' }; -static const symbol s_2_1737[7] = { 'o', 's', 't', 'a', 'n', 'e', 's' }; -static const symbol s_2_1738[4] = { 'e', 't', 'e', 's' }; -static const symbol s_2_1739[5] = { 'a', 's', 't', 'e', 's' }; -static const symbol s_2_1740[2] = { 'i', 's' }; -static const symbol s_2_1741[4] = { 'a', 'c', 'i', 's' }; -static const symbol s_2_1742[5] = { 'l', 'u', 'c', 'i', 's' }; -static const symbol s_2_1743[3] = { 'n', 'i', 's' }; -static const symbol s_2_1744[5] = { 'r', 'o', 's', 'i', 's' }; -static const symbol s_2_1745[5] = { 'j', 'e', 't', 'i', 's' }; -static const symbol s_2_1746[2] = { 'a', 't' }; -static const symbol s_2_1747[4] = { 'a', 'c', 'a', 't' }; -static const symbol s_2_1748[7] = { 'a', 's', 't', 'a', 'j', 'a', 't' }; -static const symbol s_2_1749[7] = { 'i', 's', 't', 'a', 'j', 'a', 't' }; -static const symbol s_2_1750[7] = { 'o', 's', 't', 'a', 'j', 'a', 't' }; -static const symbol s_2_1751[5] = { 'i', 'n', 'j', 'a', 't' }; -static const symbol s_2_1752[4] = { 'i', 'r', 'a', 't' }; -static const symbol s_2_1753[4] = { 'u', 'r', 'a', 't' }; -static const symbol s_2_1754[3] = { 't', 'a', 't' }; -static const symbol s_2_1755[5] = { 'a', 's', 't', 'a', 't' }; -static const symbol s_2_1756[5] = { 'i', 's', 't', 'a', 't' }; -static const symbol s_2_1757[5] = { 'o', 's', 't', 'a', 't' }; -static const symbol s_2_1758[4] = { 'a', 'v', 'a', 't' }; -static const symbol s_2_1759[4] = { 'e', 'v', 'a', 't' }; -static const symbol s_2_1760[4] = { 'i', 'v', 'a', 't' }; -static const symbol s_2_1761[6] = { 'i', 'r', 'i', 'v', 'a', 't' }; -static const symbol s_2_1762[4] = { 'o', 'v', 'a', 't' }; -static const symbol s_2_1763[4] = { 'u', 'v', 'a', 't' }; -static const symbol s_2_1764[5] = { 'a', 0xC4, 0x8D, 'a', 't' }; -static const symbol s_2_1765[2] = { 'i', 't' }; -static const symbol s_2_1766[4] = { 'a', 'c', 'i', 't' }; -static const symbol s_2_1767[5] = { 'l', 'u', 'c', 'i', 't' }; -static const symbol s_2_1768[5] = { 'r', 'o', 's', 'i', 't' }; -static const symbol s_2_1769[5] = { 'j', 'e', 't', 'i', 't' }; -static const symbol s_2_1770[5] = { 'a', 0xC4, 0x8D, 'i', 't' }; -static const symbol s_2_1771[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 't' }; -static const symbol s_2_1772[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 't' }; -static const symbol s_2_1773[3] = { 'n', 'u', 't' }; -static const symbol s_2_1774[6] = { 'a', 's', 't', 'a', 'd', 'u' }; -static const symbol s_2_1775[6] = { 'i', 's', 't', 'a', 'd', 'u' }; -static const symbol s_2_1776[6] = { 'o', 's', 't', 'a', 'd', 'u' }; -static const symbol s_2_1777[2] = { 'g', 'u' }; -static const symbol s_2_1778[4] = { 'l', 'o', 'g', 'u' }; -static const symbol s_2_1779[3] = { 'u', 'g', 'u' }; -static const symbol s_2_1780[3] = { 'a', 'h', 'u' }; -static const symbol s_2_1781[5] = { 'a', 'c', 'a', 'h', 'u' }; -static const symbol s_2_1782[8] = { 'a', 's', 't', 'a', 'j', 'a', 'h', 'u' }; -static const symbol s_2_1783[8] = { 'i', 's', 't', 'a', 'j', 'a', 'h', 'u' }; -static const symbol s_2_1784[8] = { 'o', 's', 't', 'a', 'j', 'a', 'h', 'u' }; -static const symbol s_2_1785[6] = { 'i', 'n', 'j', 'a', 'h', 'u' }; -static const symbol s_2_1786[5] = { 'i', 'r', 'a', 'h', 'u' }; -static const symbol s_2_1787[5] = { 'u', 'r', 'a', 'h', 'u' }; -static const symbol s_2_1788[5] = { 'a', 'v', 'a', 'h', 'u' }; -static const symbol s_2_1789[5] = { 'e', 'v', 'a', 'h', 'u' }; -static const symbol s_2_1790[5] = { 'i', 'v', 'a', 'h', 'u' }; -static const symbol s_2_1791[5] = { 'o', 'v', 'a', 'h', 'u' }; -static const symbol s_2_1792[5] = { 'u', 'v', 'a', 'h', 'u' }; -static const symbol s_2_1793[6] = { 'a', 0xC4, 0x8D, 'a', 'h', 'u' }; -static const symbol s_2_1794[3] = { 'a', 'j', 'u' }; -static const symbol s_2_1795[4] = { 'c', 'a', 'j', 'u' }; -static const symbol s_2_1796[5] = { 'a', 'c', 'a', 'j', 'u' }; -static const symbol s_2_1797[4] = { 'l', 'a', 'j', 'u' }; -static const symbol s_2_1798[4] = { 'r', 'a', 'j', 'u' }; -static const symbol s_2_1799[5] = { 'i', 'r', 'a', 'j', 'u' }; -static const symbol s_2_1800[5] = { 'u', 'r', 'a', 'j', 'u' }; -static const symbol s_2_1801[4] = { 't', 'a', 'j', 'u' }; -static const symbol s_2_1802[6] = { 'a', 's', 't', 'a', 'j', 'u' }; -static const symbol s_2_1803[6] = { 'i', 's', 't', 'a', 'j', 'u' }; -static const symbol s_2_1804[6] = { 'o', 's', 't', 'a', 'j', 'u' }; -static const symbol s_2_1805[5] = { 'a', 'v', 'a', 'j', 'u' }; -static const symbol s_2_1806[5] = { 'e', 'v', 'a', 'j', 'u' }; -static const symbol s_2_1807[5] = { 'i', 'v', 'a', 'j', 'u' }; -static const symbol s_2_1808[5] = { 'u', 'v', 'a', 'j', 'u' }; -static const symbol s_2_1809[5] = { 0xC4, 0x87, 'a', 'j', 'u' }; -static const symbol s_2_1810[5] = { 0xC4, 0x8D, 'a', 'j', 'u' }; -static const symbol s_2_1811[6] = { 'a', 0xC4, 0x8D, 'a', 'j', 'u' }; -static const symbol s_2_1812[5] = { 0xC4, 0x91, 'a', 'j', 'u' }; -static const symbol s_2_1813[3] = { 'i', 'j', 'u' }; -static const symbol s_2_1814[4] = { 'b', 'i', 'j', 'u' }; -static const symbol s_2_1815[4] = { 'c', 'i', 'j', 'u' }; -static const symbol s_2_1816[4] = { 'd', 'i', 'j', 'u' }; -static const symbol s_2_1817[4] = { 'f', 'i', 'j', 'u' }; -static const symbol s_2_1818[4] = { 'g', 'i', 'j', 'u' }; -static const symbol s_2_1819[6] = { 'a', 'n', 'j', 'i', 'j', 'u' }; -static const symbol s_2_1820[6] = { 'e', 'n', 'j', 'i', 'j', 'u' }; -static const symbol s_2_1821[6] = { 's', 'n', 'j', 'i', 'j', 'u' }; -static const symbol s_2_1822[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'u' }; -static const symbol s_2_1823[4] = { 'k', 'i', 'j', 'u' }; -static const symbol s_2_1824[4] = { 'l', 'i', 'j', 'u' }; -static const symbol s_2_1825[5] = { 'e', 'l', 'i', 'j', 'u' }; -static const symbol s_2_1826[4] = { 'm', 'i', 'j', 'u' }; -static const symbol s_2_1827[4] = { 'n', 'i', 'j', 'u' }; -static const symbol s_2_1828[6] = { 'g', 'a', 'n', 'i', 'j', 'u' }; -static const symbol s_2_1829[6] = { 'm', 'a', 'n', 'i', 'j', 'u' }; -static const symbol s_2_1830[6] = { 'p', 'a', 'n', 'i', 'j', 'u' }; -static const symbol s_2_1831[6] = { 'r', 'a', 'n', 'i', 'j', 'u' }; -static const symbol s_2_1832[6] = { 't', 'a', 'n', 'i', 'j', 'u' }; -static const symbol s_2_1833[4] = { 'p', 'i', 'j', 'u' }; -static const symbol s_2_1834[4] = { 'r', 'i', 'j', 'u' }; -static const symbol s_2_1835[6] = { 'r', 'a', 'r', 'i', 'j', 'u' }; -static const symbol s_2_1836[4] = { 's', 'i', 'j', 'u' }; -static const symbol s_2_1837[5] = { 'o', 's', 'i', 'j', 'u' }; -static const symbol s_2_1838[4] = { 't', 'i', 'j', 'u' }; -static const symbol s_2_1839[5] = { 'a', 't', 'i', 'j', 'u' }; -static const symbol s_2_1840[5] = { 'o', 't', 'i', 'j', 'u' }; -static const symbol s_2_1841[5] = { 'a', 'v', 'i', 'j', 'u' }; -static const symbol s_2_1842[5] = { 'e', 'v', 'i', 'j', 'u' }; -static const symbol s_2_1843[5] = { 'i', 'v', 'i', 'j', 'u' }; -static const symbol s_2_1844[5] = { 'o', 'v', 'i', 'j', 'u' }; -static const symbol s_2_1845[4] = { 'z', 'i', 'j', 'u' }; -static const symbol s_2_1846[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'u' }; -static const symbol s_2_1847[5] = { 0xC5, 0xBE, 'i', 'j', 'u' }; -static const symbol s_2_1848[4] = { 'a', 'n', 'j', 'u' }; -static const symbol s_2_1849[4] = { 'e', 'n', 'j', 'u' }; -static const symbol s_2_1850[4] = { 's', 'n', 'j', 'u' }; -static const symbol s_2_1851[5] = { 0xC5, 0xA1, 'n', 'j', 'u' }; -static const symbol s_2_1852[3] = { 'u', 'j', 'u' }; -static const symbol s_2_1853[6] = { 'l', 'u', 'c', 'u', 'j', 'u' }; -static const symbol s_2_1854[5] = { 'i', 'r', 'u', 'j', 'u' }; -static const symbol s_2_1855[7] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'u' }; -static const symbol s_2_1856[2] = { 'k', 'u' }; -static const symbol s_2_1857[3] = { 's', 'k', 'u' }; -static const symbol s_2_1858[4] = { 0xC5, 0xA1, 'k', 'u' }; -static const symbol s_2_1859[3] = { 'a', 'l', 'u' }; -static const symbol s_2_1860[5] = { 'i', 'j', 'a', 'l', 'u' }; -static const symbol s_2_1861[4] = { 'n', 'a', 'l', 'u' }; -static const symbol s_2_1862[3] = { 'e', 'l', 'u' }; -static const symbol s_2_1863[3] = { 'i', 'l', 'u' }; -static const symbol s_2_1864[5] = { 'o', 'z', 'i', 'l', 'u' }; -static const symbol s_2_1865[3] = { 'o', 'l', 'u' }; -static const symbol s_2_1866[4] = { 'r', 'a', 'm', 'u' }; -static const symbol s_2_1867[5] = { 'a', 'c', 'e', 'm', 'u' }; -static const symbol s_2_1868[5] = { 'e', 'c', 'e', 'm', 'u' }; -static const symbol s_2_1869[5] = { 'u', 'c', 'e', 'm', 'u' }; -static const symbol s_2_1870[8] = { 'a', 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1871[8] = { 'e', 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1872[8] = { 's', 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1873[9] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1874[6] = { 'k', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1875[7] = { 's', 'k', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1876[8] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1877[7] = { 'e', 'l', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1878[6] = { 'n', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1879[7] = { 'o', 's', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1880[7] = { 'a', 't', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1881[9] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1882[9] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1883[8] = { 'a', 's', 't', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1884[7] = { 'a', 'v', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1885[7] = { 'e', 'v', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1886[7] = { 'i', 'v', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1887[7] = { 'o', 'v', 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1888[8] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1889[6] = { 'a', 'n', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1890[6] = { 'e', 'n', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1891[6] = { 's', 'n', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1892[7] = { 0xC5, 0xA1, 'n', 'j', 'e', 'm', 'u' }; -static const symbol s_2_1893[4] = { 'k', 'e', 'm', 'u' }; -static const symbol s_2_1894[5] = { 's', 'k', 'e', 'm', 'u' }; -static const symbol s_2_1895[6] = { 0xC5, 0xA1, 'k', 'e', 'm', 'u' }; -static const symbol s_2_1896[4] = { 'l', 'e', 'm', 'u' }; -static const symbol s_2_1897[5] = { 'e', 'l', 'e', 'm', 'u' }; -static const symbol s_2_1898[4] = { 'n', 'e', 'm', 'u' }; -static const symbol s_2_1899[5] = { 'a', 'n', 'e', 'm', 'u' }; -static const symbol s_2_1900[5] = { 'e', 'n', 'e', 'm', 'u' }; -static const symbol s_2_1901[5] = { 's', 'n', 'e', 'm', 'u' }; -static const symbol s_2_1902[6] = { 0xC5, 0xA1, 'n', 'e', 'm', 'u' }; -static const symbol s_2_1903[5] = { 'o', 's', 'e', 'm', 'u' }; -static const symbol s_2_1904[5] = { 'a', 't', 'e', 'm', 'u' }; -static const symbol s_2_1905[7] = { 'e', 'v', 'i', 't', 'e', 'm', 'u' }; -static const symbol s_2_1906[7] = { 'o', 'v', 'i', 't', 'e', 'm', 'u' }; -static const symbol s_2_1907[6] = { 'a', 's', 't', 'e', 'm', 'u' }; -static const symbol s_2_1908[5] = { 'a', 'v', 'e', 'm', 'u' }; -static const symbol s_2_1909[5] = { 'e', 'v', 'e', 'm', 'u' }; -static const symbol s_2_1910[5] = { 'i', 'v', 'e', 'm', 'u' }; -static const symbol s_2_1911[5] = { 'o', 'v', 'e', 'm', 'u' }; -static const symbol s_2_1912[6] = { 'a', 0xC4, 0x87, 'e', 'm', 'u' }; -static const symbol s_2_1913[6] = { 'e', 0xC4, 0x87, 'e', 'm', 'u' }; -static const symbol s_2_1914[6] = { 'u', 0xC4, 0x87, 'e', 'm', 'u' }; -static const symbol s_2_1915[6] = { 'o', 0xC5, 0xA1, 'e', 'm', 'u' }; -static const symbol s_2_1916[5] = { 'a', 'c', 'o', 'm', 'u' }; -static const symbol s_2_1917[5] = { 'e', 'c', 'o', 'm', 'u' }; -static const symbol s_2_1918[5] = { 'u', 'c', 'o', 'm', 'u' }; -static const symbol s_2_1919[6] = { 'a', 'n', 'j', 'o', 'm', 'u' }; -static const symbol s_2_1920[6] = { 'e', 'n', 'j', 'o', 'm', 'u' }; -static const symbol s_2_1921[6] = { 's', 'n', 'j', 'o', 'm', 'u' }; -static const symbol s_2_1922[7] = { 0xC5, 0xA1, 'n', 'j', 'o', 'm', 'u' }; -static const symbol s_2_1923[4] = { 'k', 'o', 'm', 'u' }; -static const symbol s_2_1924[5] = { 's', 'k', 'o', 'm', 'u' }; -static const symbol s_2_1925[6] = { 0xC5, 0xA1, 'k', 'o', 'm', 'u' }; -static const symbol s_2_1926[5] = { 'e', 'l', 'o', 'm', 'u' }; -static const symbol s_2_1927[4] = { 'n', 'o', 'm', 'u' }; -static const symbol s_2_1928[6] = { 'c', 'i', 'n', 'o', 'm', 'u' }; -static const symbol s_2_1929[7] = { 0xC4, 0x8D, 'i', 'n', 'o', 'm', 'u' }; -static const symbol s_2_1930[5] = { 'o', 's', 'o', 'm', 'u' }; -static const symbol s_2_1931[5] = { 'a', 't', 'o', 'm', 'u' }; -static const symbol s_2_1932[7] = { 'e', 'v', 'i', 't', 'o', 'm', 'u' }; -static const symbol s_2_1933[7] = { 'o', 'v', 'i', 't', 'o', 'm', 'u' }; -static const symbol s_2_1934[6] = { 'a', 's', 't', 'o', 'm', 'u' }; -static const symbol s_2_1935[5] = { 'a', 'v', 'o', 'm', 'u' }; -static const symbol s_2_1936[5] = { 'e', 'v', 'o', 'm', 'u' }; -static const symbol s_2_1937[5] = { 'i', 'v', 'o', 'm', 'u' }; -static const symbol s_2_1938[5] = { 'o', 'v', 'o', 'm', 'u' }; -static const symbol s_2_1939[6] = { 'a', 0xC4, 0x87, 'o', 'm', 'u' }; -static const symbol s_2_1940[6] = { 'e', 0xC4, 0x87, 'o', 'm', 'u' }; -static const symbol s_2_1941[6] = { 'u', 0xC4, 0x87, 'o', 'm', 'u' }; -static const symbol s_2_1942[6] = { 'o', 0xC5, 0xA1, 'o', 'm', 'u' }; -static const symbol s_2_1943[2] = { 'n', 'u' }; -static const symbol s_2_1944[3] = { 'a', 'n', 'u' }; -static const symbol s_2_1945[6] = { 'a', 's', 't', 'a', 'n', 'u' }; -static const symbol s_2_1946[6] = { 'i', 's', 't', 'a', 'n', 'u' }; -static const symbol s_2_1947[6] = { 'o', 's', 't', 'a', 'n', 'u' }; -static const symbol s_2_1948[3] = { 'i', 'n', 'u' }; -static const symbol s_2_1949[4] = { 'c', 'i', 'n', 'u' }; -static const symbol s_2_1950[5] = { 'a', 'n', 'i', 'n', 'u' }; -static const symbol s_2_1951[5] = { 0xC4, 0x8D, 'i', 'n', 'u' }; -static const symbol s_2_1952[3] = { 'o', 'n', 'u' }; -static const symbol s_2_1953[3] = { 'a', 'r', 'u' }; -static const symbol s_2_1954[3] = { 'd', 'r', 'u' }; -static const symbol s_2_1955[3] = { 'e', 'r', 'u' }; -static const symbol s_2_1956[3] = { 'o', 'r', 'u' }; -static const symbol s_2_1957[4] = { 'b', 'a', 's', 'u' }; -static const symbol s_2_1958[4] = { 'g', 'a', 's', 'u' }; -static const symbol s_2_1959[4] = { 'j', 'a', 's', 'u' }; -static const symbol s_2_1960[4] = { 'k', 'a', 's', 'u' }; -static const symbol s_2_1961[4] = { 'n', 'a', 's', 'u' }; -static const symbol s_2_1962[4] = { 't', 'a', 's', 'u' }; -static const symbol s_2_1963[4] = { 'v', 'a', 's', 'u' }; -static const symbol s_2_1964[3] = { 'e', 's', 'u' }; -static const symbol s_2_1965[3] = { 'i', 's', 'u' }; -static const symbol s_2_1966[3] = { 'o', 's', 'u' }; -static const symbol s_2_1967[3] = { 'a', 't', 'u' }; -static const symbol s_2_1968[5] = { 'i', 'k', 'a', 't', 'u' }; -static const symbol s_2_1969[4] = { 'l', 'a', 't', 'u' }; -static const symbol s_2_1970[3] = { 'e', 't', 'u' }; -static const symbol s_2_1971[5] = { 'e', 'v', 'i', 't', 'u' }; -static const symbol s_2_1972[5] = { 'o', 'v', 'i', 't', 'u' }; -static const symbol s_2_1973[4] = { 'a', 's', 't', 'u' }; -static const symbol s_2_1974[4] = { 'e', 's', 't', 'u' }; -static const symbol s_2_1975[4] = { 'i', 's', 't', 'u' }; -static const symbol s_2_1976[4] = { 'k', 's', 't', 'u' }; -static const symbol s_2_1977[4] = { 'o', 's', 't', 'u' }; -static const symbol s_2_1978[5] = { 'i', 0xC5, 0xA1, 't', 'u' }; -static const symbol s_2_1979[3] = { 'a', 'v', 'u' }; -static const symbol s_2_1980[3] = { 'e', 'v', 'u' }; -static const symbol s_2_1981[3] = { 'i', 'v', 'u' }; -static const symbol s_2_1982[3] = { 'o', 'v', 'u' }; -static const symbol s_2_1983[4] = { 'l', 'o', 'v', 'u' }; -static const symbol s_2_1984[4] = { 'm', 'o', 'v', 'u' }; -static const symbol s_2_1985[4] = { 's', 't', 'v', 'u' }; -static const symbol s_2_1986[5] = { 0xC5, 0xA1, 't', 'v', 'u' }; -static const symbol s_2_1987[5] = { 'b', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1988[5] = { 'g', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1989[5] = { 'j', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1990[5] = { 'k', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1991[5] = { 'n', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1992[5] = { 't', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1993[5] = { 'v', 'a', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1994[4] = { 'e', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1995[4] = { 'i', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1996[4] = { 'o', 0xC5, 0xA1, 'u' }; -static const symbol s_2_1997[4] = { 'a', 'v', 'a', 'v' }; -static const symbol s_2_1998[4] = { 'e', 'v', 'a', 'v' }; -static const symbol s_2_1999[4] = { 'i', 'v', 'a', 'v' }; -static const symbol s_2_2000[4] = { 'u', 'v', 'a', 'v' }; -static const symbol s_2_2001[3] = { 'k', 'o', 'v' }; -static const symbol s_2_2002[3] = { 'a', 0xC5, 0xA1 }; -static const symbol s_2_2003[5] = { 'i', 'r', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2004[5] = { 'u', 'r', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2005[4] = { 't', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2006[5] = { 'a', 'v', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2007[5] = { 'e', 'v', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2008[5] = { 'i', 'v', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2009[5] = { 'u', 'v', 'a', 0xC5, 0xA1 }; -static const symbol s_2_2010[6] = { 'a', 0xC4, 0x8D, 'a', 0xC5, 0xA1 }; -static const symbol s_2_2011[3] = { 'e', 0xC5, 0xA1 }; -static const symbol s_2_2012[8] = { 'a', 's', 't', 'a', 'd', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2013[8] = { 'i', 's', 't', 'a', 'd', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2014[8] = { 'o', 's', 't', 'a', 'd', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2015[8] = { 'a', 's', 't', 'a', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2016[8] = { 'i', 's', 't', 'a', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2017[8] = { 'o', 's', 't', 'a', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2018[5] = { 'i', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2019[6] = { 'i', 'n', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2020[5] = { 'u', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2021[7] = { 'i', 'r', 'u', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2022[9] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2023[4] = { 'n', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2024[8] = { 'a', 's', 't', 'a', 'n', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2025[8] = { 'i', 's', 't', 'a', 'n', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2026[8] = { 'o', 's', 't', 'a', 'n', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2027[5] = { 'e', 't', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2028[6] = { 'a', 's', 't', 'e', 0xC5, 0xA1 }; -static const symbol s_2_2029[3] = { 'i', 0xC5, 0xA1 }; -static const symbol s_2_2030[4] = { 'n', 'i', 0xC5, 0xA1 }; -static const symbol s_2_2031[6] = { 'j', 'e', 't', 'i', 0xC5, 0xA1 }; -static const symbol s_2_2032[6] = { 'a', 0xC4, 0x8D, 'i', 0xC5, 0xA1 }; -static const symbol s_2_2033[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 0xC5, 0xA1 }; -static const symbol s_2_2034[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 0xC5, 0xA1 }; - -static const struct among a_2[2035] = -{ -{ 3, s_2_0, -1, 124, 0}, -{ 3, s_2_1, -1, 125, 0}, -{ 3, s_2_2, -1, 126, 0}, -{ 2, s_2_3, -1, 20, 0}, -{ 5, s_2_4, 3, 124, 0}, -{ 5, s_2_5, 3, 125, 0}, -{ 5, s_2_6, 3, 126, 0}, -{ 8, s_2_7, 3, 84, 0}, -{ 8, s_2_8, 3, 85, 0}, -{ 8, s_2_9, 3, 122, 0}, -{ 9, s_2_10, 3, 86, 0}, -{ 6, s_2_11, 3, 95, 0}, -{ 7, s_2_12, 11, 1, 0}, -{ 8, s_2_13, 11, 2, 0}, -{ 7, s_2_14, 3, 83, 0}, -{ 6, s_2_15, 3, 13, 0}, -{ 7, s_2_16, 3, 123, 0}, -{ 7, s_2_17, 3, 120, 0}, -{ 9, s_2_18, 3, 92, 0}, -{ 9, s_2_19, 3, 93, 0}, -{ 8, s_2_20, 3, 94, 0}, -{ 7, s_2_21, 3, 77, 0}, -{ 7, s_2_22, 3, 78, 0}, -{ 7, s_2_23, 3, 79, 0}, -{ 7, s_2_24, 3, 80, 0}, -{ 8, s_2_25, 3, 91, 0}, -{ 6, s_2_26, 3, 84, 0}, -{ 6, s_2_27, 3, 85, 0}, -{ 6, s_2_28, 3, 122, 0}, -{ 7, s_2_29, 3, 86, 0}, -{ 4, s_2_30, 3, 95, 0}, -{ 5, s_2_31, 30, 1, 0}, -{ 6, s_2_32, 30, 2, 0}, -{ 5, s_2_33, 3, 83, 0}, -{ 4, s_2_34, 3, 13, 0}, -{ 5, s_2_35, 34, 10, 0}, -{ 5, s_2_36, 34, 87, 0}, -{ 5, s_2_37, 34, 159, 0}, -{ 6, s_2_38, 34, 88, 0}, -{ 5, s_2_39, 3, 123, 0}, -{ 5, s_2_40, 3, 120, 0}, -{ 7, s_2_41, 3, 92, 0}, -{ 7, s_2_42, 3, 93, 0}, -{ 6, s_2_43, 3, 94, 0}, -{ 5, s_2_44, 3, 77, 0}, -{ 5, s_2_45, 3, 78, 0}, -{ 5, s_2_46, 3, 79, 0}, -{ 5, s_2_47, 3, 80, 0}, -{ 6, s_2_48, 3, 14, 0}, -{ 6, s_2_49, 3, 15, 0}, -{ 6, s_2_50, 3, 16, 0}, -{ 6, s_2_51, 3, 91, 0}, -{ 5, s_2_52, 3, 124, 0}, -{ 5, s_2_53, 3, 125, 0}, -{ 5, s_2_54, 3, 126, 0}, -{ 6, s_2_55, 3, 84, 0}, -{ 6, s_2_56, 3, 85, 0}, -{ 6, s_2_57, 3, 122, 0}, -{ 7, s_2_58, 3, 86, 0}, -{ 4, s_2_59, 3, 95, 0}, -{ 5, s_2_60, 59, 1, 0}, -{ 6, s_2_61, 59, 2, 0}, -{ 4, s_2_62, 3, 19, 0}, -{ 5, s_2_63, 62, 83, 0}, -{ 4, s_2_64, 3, 13, 0}, -{ 6, s_2_65, 64, 137, 0}, -{ 7, s_2_66, 64, 89, 0}, -{ 5, s_2_67, 3, 123, 0}, -{ 5, s_2_68, 3, 120, 0}, -{ 7, s_2_69, 3, 92, 0}, -{ 7, s_2_70, 3, 93, 0}, -{ 6, s_2_71, 3, 94, 0}, -{ 5, s_2_72, 3, 77, 0}, -{ 5, s_2_73, 3, 78, 0}, -{ 5, s_2_74, 3, 79, 0}, -{ 5, s_2_75, 3, 80, 0}, -{ 6, s_2_76, 3, 14, 0}, -{ 6, s_2_77, 3, 15, 0}, -{ 6, s_2_78, 3, 16, 0}, -{ 6, s_2_79, 3, 91, 0}, -{ 3, s_2_80, 3, 18, 0}, -{ 3, s_2_81, -1, 109, 0}, -{ 4, s_2_82, 81, 26, 0}, -{ 4, s_2_83, 81, 30, 0}, -{ 4, s_2_84, 81, 31, 0}, -{ 5, s_2_85, 81, 28, 0}, -{ 5, s_2_86, 81, 27, 0}, -{ 5, s_2_87, 81, 29, 0}, -{ 4, s_2_88, -1, 32, 0}, -{ 4, s_2_89, -1, 33, 0}, -{ 4, s_2_90, -1, 34, 0}, -{ 4, s_2_91, -1, 40, 0}, -{ 4, s_2_92, -1, 39, 0}, -{ 6, s_2_93, -1, 84, 0}, -{ 6, s_2_94, -1, 85, 0}, -{ 6, s_2_95, -1, 122, 0}, -{ 7, s_2_96, -1, 86, 0}, -{ 4, s_2_97, -1, 95, 0}, -{ 5, s_2_98, 97, 1, 0}, -{ 6, s_2_99, 97, 2, 0}, -{ 4, s_2_100, -1, 24, 0}, -{ 5, s_2_101, 100, 83, 0}, -{ 4, s_2_102, -1, 37, 0}, -{ 4, s_2_103, -1, 13, 0}, -{ 6, s_2_104, 103, 9, 0}, -{ 6, s_2_105, 103, 6, 0}, -{ 6, s_2_106, 103, 7, 0}, -{ 6, s_2_107, 103, 8, 0}, -{ 6, s_2_108, 103, 5, 0}, -{ 4, s_2_109, -1, 41, 0}, -{ 4, s_2_110, -1, 42, 0}, -{ 6, s_2_111, 110, 21, 0}, -{ 4, s_2_112, -1, 23, 0}, -{ 5, s_2_113, 112, 123, 0}, -{ 4, s_2_114, -1, 44, 0}, -{ 5, s_2_115, 114, 120, 0}, -{ 7, s_2_116, 114, 92, 0}, -{ 7, s_2_117, 114, 93, 0}, -{ 5, s_2_118, 114, 22, 0}, -{ 6, s_2_119, 114, 94, 0}, -{ 5, s_2_120, -1, 77, 0}, -{ 5, s_2_121, -1, 78, 0}, -{ 5, s_2_122, -1, 79, 0}, -{ 5, s_2_123, -1, 80, 0}, -{ 4, s_2_124, -1, 45, 0}, -{ 6, s_2_125, -1, 91, 0}, -{ 5, s_2_126, -1, 38, 0}, -{ 4, s_2_127, -1, 84, 0}, -{ 4, s_2_128, -1, 85, 0}, -{ 4, s_2_129, -1, 122, 0}, -{ 5, s_2_130, -1, 86, 0}, -{ 2, s_2_131, -1, 95, 0}, -{ 3, s_2_132, 131, 1, 0}, -{ 4, s_2_133, 131, 2, 0}, -{ 3, s_2_134, -1, 104, 0}, -{ 5, s_2_135, 134, 128, 0}, -{ 8, s_2_136, 134, 106, 0}, -{ 8, s_2_137, 134, 107, 0}, -{ 8, s_2_138, 134, 108, 0}, -{ 5, s_2_139, 134, 47, 0}, -{ 6, s_2_140, 134, 114, 0}, -{ 4, s_2_141, 134, 46, 0}, -{ 5, s_2_142, 134, 100, 0}, -{ 5, s_2_143, 134, 105, 0}, -{ 4, s_2_144, 134, 113, 0}, -{ 6, s_2_145, 144, 110, 0}, -{ 6, s_2_146, 144, 111, 0}, -{ 6, s_2_147, 144, 112, 0}, -{ 5, s_2_148, 134, 97, 0}, -{ 5, s_2_149, 134, 96, 0}, -{ 5, s_2_150, 134, 98, 0}, -{ 5, s_2_151, 134, 76, 0}, -{ 5, s_2_152, 134, 99, 0}, -{ 6, s_2_153, 134, 102, 0}, -{ 3, s_2_154, -1, 83, 0}, -{ 3, s_2_155, -1, 116, 0}, -{ 5, s_2_156, 155, 124, 0}, -{ 6, s_2_157, 155, 121, 0}, -{ 4, s_2_158, 155, 103, 0}, -{ 8, s_2_159, 158, 110, 0}, -{ 8, s_2_160, 158, 111, 0}, -{ 8, s_2_161, 158, 112, 0}, -{ 6, s_2_162, 155, 127, 0}, -{ 6, s_2_163, 155, 118, 0}, -{ 5, s_2_164, 155, 48, 0}, -{ 6, s_2_165, 155, 101, 0}, -{ 7, s_2_166, 155, 117, 0}, -{ 7, s_2_167, 155, 90, 0}, -{ 3, s_2_168, -1, 50, 0}, -{ 4, s_2_169, -1, 115, 0}, -{ 4, s_2_170, -1, 13, 0}, -{ 4, s_2_171, -1, 20, 0}, -{ 6, s_2_172, 171, 19, 0}, -{ 5, s_2_173, 171, 18, 0}, -{ 5, s_2_174, -1, 109, 0}, -{ 6, s_2_175, 174, 26, 0}, -{ 6, s_2_176, 174, 30, 0}, -{ 6, s_2_177, 174, 31, 0}, -{ 7, s_2_178, 174, 28, 0}, -{ 7, s_2_179, 174, 27, 0}, -{ 7, s_2_180, 174, 29, 0}, -{ 6, s_2_181, -1, 32, 0}, -{ 6, s_2_182, -1, 33, 0}, -{ 6, s_2_183, -1, 34, 0}, -{ 6, s_2_184, -1, 40, 0}, -{ 6, s_2_185, -1, 39, 0}, -{ 6, s_2_186, -1, 35, 0}, -{ 6, s_2_187, -1, 37, 0}, -{ 6, s_2_188, -1, 36, 0}, -{ 8, s_2_189, 188, 9, 0}, -{ 8, s_2_190, 188, 6, 0}, -{ 8, s_2_191, 188, 7, 0}, -{ 8, s_2_192, 188, 8, 0}, -{ 8, s_2_193, 188, 5, 0}, -{ 6, s_2_194, -1, 41, 0}, -{ 6, s_2_195, -1, 42, 0}, -{ 6, s_2_196, -1, 43, 0}, -{ 6, s_2_197, -1, 44, 0}, -{ 6, s_2_198, -1, 45, 0}, -{ 7, s_2_199, -1, 38, 0}, -{ 5, s_2_200, -1, 104, 0}, -{ 7, s_2_201, 200, 47, 0}, -{ 6, s_2_202, 200, 46, 0}, -{ 5, s_2_203, -1, 119, 0}, -{ 5, s_2_204, -1, 116, 0}, -{ 6, s_2_205, -1, 52, 0}, -{ 6, s_2_206, -1, 51, 0}, -{ 5, s_2_207, -1, 11, 0}, -{ 6, s_2_208, 207, 137, 0}, -{ 7, s_2_209, 207, 89, 0}, -{ 4, s_2_210, -1, 52, 0}, -{ 5, s_2_211, 210, 53, 0}, -{ 5, s_2_212, 210, 54, 0}, -{ 5, s_2_213, 210, 55, 0}, -{ 5, s_2_214, 210, 56, 0}, -{ 6, s_2_215, -1, 135, 0}, -{ 6, s_2_216, -1, 131, 0}, -{ 6, s_2_217, -1, 129, 0}, -{ 6, s_2_218, -1, 133, 0}, -{ 6, s_2_219, -1, 132, 0}, -{ 6, s_2_220, -1, 130, 0}, -{ 6, s_2_221, -1, 134, 0}, -{ 5, s_2_222, -1, 152, 0}, -{ 5, s_2_223, -1, 154, 0}, -{ 5, s_2_224, -1, 70, 0}, -{ 6, s_2_225, -1, 71, 0}, -{ 6, s_2_226, -1, 72, 0}, -{ 6, s_2_227, -1, 73, 0}, -{ 6, s_2_228, -1, 74, 0}, -{ 5, s_2_229, -1, 77, 0}, -{ 5, s_2_230, -1, 78, 0}, -{ 5, s_2_231, -1, 79, 0}, -{ 7, s_2_232, -1, 63, 0}, -{ 7, s_2_233, -1, 64, 0}, -{ 7, s_2_234, -1, 61, 0}, -{ 7, s_2_235, -1, 62, 0}, -{ 7, s_2_236, -1, 60, 0}, -{ 7, s_2_237, -1, 59, 0}, -{ 7, s_2_238, -1, 65, 0}, -{ 6, s_2_239, -1, 66, 0}, -{ 6, s_2_240, -1, 67, 0}, -{ 4, s_2_241, -1, 51, 0}, -{ 5, s_2_242, -1, 124, 0}, -{ 5, s_2_243, -1, 125, 0}, -{ 5, s_2_244, -1, 126, 0}, -{ 5, s_2_245, -1, 109, 0}, -{ 6, s_2_246, 245, 26, 0}, -{ 6, s_2_247, 245, 30, 0}, -{ 6, s_2_248, 245, 31, 0}, -{ 7, s_2_249, 245, 28, 0}, -{ 7, s_2_250, 245, 27, 0}, -{ 7, s_2_251, 245, 29, 0}, -{ 6, s_2_252, -1, 32, 0}, -{ 6, s_2_253, -1, 33, 0}, -{ 6, s_2_254, -1, 34, 0}, -{ 6, s_2_255, -1, 40, 0}, -{ 6, s_2_256, -1, 39, 0}, -{ 8, s_2_257, -1, 84, 0}, -{ 8, s_2_258, -1, 85, 0}, -{ 8, s_2_259, -1, 122, 0}, -{ 9, s_2_260, -1, 86, 0}, -{ 6, s_2_261, -1, 95, 0}, -{ 7, s_2_262, 261, 1, 0}, -{ 8, s_2_263, 261, 2, 0}, -{ 6, s_2_264, -1, 35, 0}, -{ 7, s_2_265, 264, 83, 0}, -{ 6, s_2_266, -1, 37, 0}, -{ 6, s_2_267, -1, 13, 0}, -{ 8, s_2_268, 267, 9, 0}, -{ 8, s_2_269, 267, 6, 0}, -{ 8, s_2_270, 267, 7, 0}, -{ 8, s_2_271, 267, 8, 0}, -{ 8, s_2_272, 267, 5, 0}, -{ 6, s_2_273, -1, 41, 0}, -{ 6, s_2_274, -1, 42, 0}, -{ 6, s_2_275, -1, 43, 0}, -{ 7, s_2_276, 275, 123, 0}, -{ 6, s_2_277, -1, 44, 0}, -{ 7, s_2_278, 277, 120, 0}, -{ 9, s_2_279, 277, 92, 0}, -{ 9, s_2_280, 277, 93, 0}, -{ 8, s_2_281, 277, 94, 0}, -{ 7, s_2_282, -1, 77, 0}, -{ 7, s_2_283, -1, 78, 0}, -{ 7, s_2_284, -1, 79, 0}, -{ 7, s_2_285, -1, 80, 0}, -{ 6, s_2_286, -1, 45, 0}, -{ 8, s_2_287, -1, 91, 0}, -{ 7, s_2_288, -1, 38, 0}, -{ 6, s_2_289, -1, 84, 0}, -{ 6, s_2_290, -1, 85, 0}, -{ 6, s_2_291, -1, 122, 0}, -{ 7, s_2_292, -1, 86, 0}, -{ 4, s_2_293, -1, 95, 0}, -{ 5, s_2_294, 293, 1, 0}, -{ 6, s_2_295, 293, 2, 0}, -{ 5, s_2_296, -1, 104, 0}, -{ 7, s_2_297, 296, 47, 0}, -{ 6, s_2_298, 296, 46, 0}, -{ 5, s_2_299, -1, 83, 0}, -{ 5, s_2_300, -1, 116, 0}, -{ 7, s_2_301, 300, 48, 0}, -{ 5, s_2_302, -1, 50, 0}, -{ 6, s_2_303, -1, 51, 0}, -{ 4, s_2_304, -1, 13, 0}, -{ 5, s_2_305, 304, 10, 0}, -{ 5, s_2_306, 304, 11, 0}, -{ 6, s_2_307, 306, 137, 0}, -{ 7, s_2_308, 306, 89, 0}, -{ 5, s_2_309, 304, 12, 0}, -{ 5, s_2_310, -1, 53, 0}, -{ 5, s_2_311, -1, 54, 0}, -{ 5, s_2_312, -1, 55, 0}, -{ 5, s_2_313, -1, 56, 0}, -{ 6, s_2_314, -1, 135, 0}, -{ 6, s_2_315, -1, 131, 0}, -{ 6, s_2_316, -1, 129, 0}, -{ 6, s_2_317, -1, 133, 0}, -{ 6, s_2_318, -1, 132, 0}, -{ 6, s_2_319, -1, 130, 0}, -{ 6, s_2_320, -1, 134, 0}, -{ 5, s_2_321, -1, 57, 0}, -{ 5, s_2_322, -1, 58, 0}, -{ 5, s_2_323, -1, 123, 0}, -{ 5, s_2_324, -1, 120, 0}, -{ 7, s_2_325, 324, 68, 0}, -{ 6, s_2_326, 324, 69, 0}, -{ 5, s_2_327, -1, 70, 0}, -{ 7, s_2_328, -1, 92, 0}, -{ 7, s_2_329, -1, 93, 0}, -{ 6, s_2_330, -1, 94, 0}, -{ 6, s_2_331, -1, 71, 0}, -{ 6, s_2_332, -1, 72, 0}, -{ 6, s_2_333, -1, 73, 0}, -{ 6, s_2_334, -1, 74, 0}, -{ 7, s_2_335, -1, 75, 0}, -{ 5, s_2_336, -1, 77, 0}, -{ 5, s_2_337, -1, 78, 0}, -{ 7, s_2_338, 337, 109, 0}, -{ 8, s_2_339, 338, 26, 0}, -{ 8, s_2_340, 338, 30, 0}, -{ 8, s_2_341, 338, 31, 0}, -{ 9, s_2_342, 338, 28, 0}, -{ 9, s_2_343, 338, 27, 0}, -{ 9, s_2_344, 338, 29, 0}, -{ 5, s_2_345, -1, 79, 0}, -{ 5, s_2_346, -1, 80, 0}, -{ 6, s_2_347, 346, 20, 0}, -{ 7, s_2_348, 347, 17, 0}, -{ 6, s_2_349, 346, 82, 0}, -{ 7, s_2_350, 349, 49, 0}, -{ 6, s_2_351, 346, 81, 0}, -{ 7, s_2_352, 346, 12, 0}, -{ 6, s_2_353, -1, 3, 0}, -{ 7, s_2_354, -1, 4, 0}, -{ 6, s_2_355, -1, 14, 0}, -{ 6, s_2_356, -1, 15, 0}, -{ 6, s_2_357, -1, 16, 0}, -{ 7, s_2_358, -1, 63, 0}, -{ 7, s_2_359, -1, 64, 0}, -{ 7, s_2_360, -1, 61, 0}, -{ 7, s_2_361, -1, 62, 0}, -{ 7, s_2_362, -1, 60, 0}, -{ 7, s_2_363, -1, 59, 0}, -{ 7, s_2_364, -1, 65, 0}, -{ 6, s_2_365, -1, 66, 0}, -{ 6, s_2_366, -1, 67, 0}, -{ 6, s_2_367, -1, 91, 0}, -{ 2, s_2_368, -1, 13, 0}, -{ 3, s_2_369, 368, 10, 0}, -{ 5, s_2_370, 369, 128, 0}, -{ 5, s_2_371, 369, 105, 0}, -{ 4, s_2_372, 369, 113, 0}, -{ 5, s_2_373, 369, 97, 0}, -{ 5, s_2_374, 369, 96, 0}, -{ 5, s_2_375, 369, 98, 0}, -{ 5, s_2_376, 369, 99, 0}, -{ 6, s_2_377, 369, 102, 0}, -{ 5, s_2_378, 368, 124, 0}, -{ 6, s_2_379, 368, 121, 0}, -{ 6, s_2_380, 368, 101, 0}, -{ 7, s_2_381, 368, 117, 0}, -{ 3, s_2_382, 368, 11, 0}, -{ 4, s_2_383, 382, 137, 0}, -{ 5, s_2_384, 382, 10, 0}, -{ 5, s_2_385, 382, 89, 0}, -{ 3, s_2_386, 368, 12, 0}, -{ 3, s_2_387, -1, 53, 0}, -{ 3, s_2_388, -1, 54, 0}, -{ 3, s_2_389, -1, 55, 0}, -{ 3, s_2_390, -1, 56, 0}, -{ 4, s_2_391, -1, 135, 0}, -{ 4, s_2_392, -1, 131, 0}, -{ 4, s_2_393, -1, 129, 0}, -{ 4, s_2_394, -1, 133, 0}, -{ 4, s_2_395, -1, 132, 0}, -{ 4, s_2_396, -1, 130, 0}, -{ 4, s_2_397, -1, 134, 0}, -{ 3, s_2_398, -1, 57, 0}, -{ 3, s_2_399, -1, 58, 0}, -{ 3, s_2_400, -1, 123, 0}, -{ 3, s_2_401, -1, 120, 0}, -{ 5, s_2_402, 401, 68, 0}, -{ 4, s_2_403, 401, 69, 0}, -{ 3, s_2_404, -1, 70, 0}, -{ 5, s_2_405, -1, 92, 0}, -{ 5, s_2_406, -1, 93, 0}, -{ 4, s_2_407, -1, 94, 0}, -{ 4, s_2_408, -1, 71, 0}, -{ 4, s_2_409, -1, 72, 0}, -{ 4, s_2_410, -1, 73, 0}, -{ 4, s_2_411, -1, 74, 0}, -{ 4, s_2_412, -1, 13, 0}, -{ 5, s_2_413, -1, 75, 0}, -{ 3, s_2_414, -1, 77, 0}, -{ 3, s_2_415, -1, 78, 0}, -{ 5, s_2_416, 415, 109, 0}, -{ 6, s_2_417, 416, 26, 0}, -{ 6, s_2_418, 416, 30, 0}, -{ 6, s_2_419, 416, 31, 0}, -{ 7, s_2_420, 416, 28, 0}, -{ 7, s_2_421, 416, 27, 0}, -{ 7, s_2_422, 416, 29, 0}, -{ 3, s_2_423, -1, 79, 0}, -{ 3, s_2_424, -1, 80, 0}, -{ 4, s_2_425, 424, 20, 0}, -{ 5, s_2_426, 425, 17, 0}, -{ 4, s_2_427, 424, 82, 0}, -{ 5, s_2_428, 427, 49, 0}, -{ 4, s_2_429, 424, 81, 0}, -{ 5, s_2_430, 424, 12, 0}, -{ 4, s_2_431, -1, 3, 0}, -{ 5, s_2_432, -1, 4, 0}, -{ 4, s_2_433, -1, 14, 0}, -{ 4, s_2_434, -1, 15, 0}, -{ 4, s_2_435, -1, 16, 0}, -{ 5, s_2_436, -1, 63, 0}, -{ 5, s_2_437, -1, 64, 0}, -{ 5, s_2_438, -1, 61, 0}, -{ 5, s_2_439, -1, 62, 0}, -{ 5, s_2_440, -1, 60, 0}, -{ 5, s_2_441, -1, 59, 0}, -{ 5, s_2_442, -1, 65, 0}, -{ 4, s_2_443, -1, 66, 0}, -{ 4, s_2_444, -1, 67, 0}, -{ 4, s_2_445, -1, 91, 0}, -{ 3, s_2_446, -1, 124, 0}, -{ 3, s_2_447, -1, 125, 0}, -{ 3, s_2_448, -1, 126, 0}, -{ 4, s_2_449, 448, 121, 0}, -{ 6, s_2_450, -1, 110, 0}, -{ 6, s_2_451, -1, 111, 0}, -{ 6, s_2_452, -1, 112, 0}, -{ 2, s_2_453, -1, 20, 0}, -{ 4, s_2_454, 453, 19, 0}, -{ 3, s_2_455, 453, 18, 0}, -{ 3, s_2_456, -1, 104, 0}, -{ 4, s_2_457, 456, 26, 0}, -{ 4, s_2_458, 456, 30, 0}, -{ 4, s_2_459, 456, 31, 0}, -{ 6, s_2_460, 456, 106, 0}, -{ 6, s_2_461, 456, 107, 0}, -{ 6, s_2_462, 456, 108, 0}, -{ 5, s_2_463, 456, 28, 0}, -{ 5, s_2_464, 456, 27, 0}, -{ 5, s_2_465, 456, 29, 0}, -{ 3, s_2_466, -1, 116, 0}, -{ 4, s_2_467, 466, 32, 0}, -{ 4, s_2_468, 466, 33, 0}, -{ 4, s_2_469, 466, 34, 0}, -{ 4, s_2_470, 466, 40, 0}, -{ 4, s_2_471, 466, 39, 0}, -{ 6, s_2_472, 466, 84, 0}, -{ 6, s_2_473, 466, 85, 0}, -{ 6, s_2_474, 466, 122, 0}, -{ 7, s_2_475, 466, 86, 0}, -{ 4, s_2_476, 466, 95, 0}, -{ 5, s_2_477, 476, 1, 0}, -{ 6, s_2_478, 476, 2, 0}, -{ 4, s_2_479, 466, 35, 0}, -{ 5, s_2_480, 479, 83, 0}, -{ 4, s_2_481, 466, 37, 0}, -{ 4, s_2_482, 466, 13, 0}, -{ 6, s_2_483, 482, 9, 0}, -{ 6, s_2_484, 482, 6, 0}, -{ 6, s_2_485, 482, 7, 0}, -{ 6, s_2_486, 482, 8, 0}, -{ 6, s_2_487, 482, 5, 0}, -{ 4, s_2_488, 466, 41, 0}, -{ 4, s_2_489, 466, 42, 0}, -{ 4, s_2_490, 466, 43, 0}, -{ 5, s_2_491, 490, 123, 0}, -{ 4, s_2_492, 466, 44, 0}, -{ 5, s_2_493, 492, 120, 0}, -{ 7, s_2_494, 492, 92, 0}, -{ 7, s_2_495, 492, 93, 0}, -{ 6, s_2_496, 492, 94, 0}, -{ 5, s_2_497, 466, 77, 0}, -{ 5, s_2_498, 466, 78, 0}, -{ 5, s_2_499, 466, 79, 0}, -{ 5, s_2_500, 466, 80, 0}, -{ 4, s_2_501, 466, 45, 0}, -{ 6, s_2_502, 466, 91, 0}, -{ 5, s_2_503, 466, 38, 0}, -{ 4, s_2_504, -1, 84, 0}, -{ 4, s_2_505, -1, 85, 0}, -{ 4, s_2_506, -1, 122, 0}, -{ 5, s_2_507, -1, 86, 0}, -{ 3, s_2_508, -1, 25, 0}, -{ 6, s_2_509, 508, 121, 0}, -{ 5, s_2_510, 508, 100, 0}, -{ 7, s_2_511, 508, 117, 0}, -{ 2, s_2_512, -1, 95, 0}, -{ 3, s_2_513, 512, 1, 0}, -{ 4, s_2_514, 512, 2, 0}, -{ 3, s_2_515, -1, 104, 0}, -{ 5, s_2_516, 515, 128, 0}, -{ 8, s_2_517, 515, 106, 0}, -{ 8, s_2_518, 515, 107, 0}, -{ 8, s_2_519, 515, 108, 0}, -{ 5, s_2_520, 515, 47, 0}, -{ 6, s_2_521, 515, 114, 0}, -{ 4, s_2_522, 515, 46, 0}, -{ 5, s_2_523, 515, 100, 0}, -{ 5, s_2_524, 515, 105, 0}, -{ 4, s_2_525, 515, 113, 0}, -{ 6, s_2_526, 525, 110, 0}, -{ 6, s_2_527, 525, 111, 0}, -{ 6, s_2_528, 525, 112, 0}, -{ 5, s_2_529, 515, 97, 0}, -{ 5, s_2_530, 515, 96, 0}, -{ 5, s_2_531, 515, 98, 0}, -{ 5, s_2_532, 515, 76, 0}, -{ 5, s_2_533, 515, 99, 0}, -{ 6, s_2_534, 515, 102, 0}, -{ 3, s_2_535, -1, 83, 0}, -{ 3, s_2_536, -1, 116, 0}, -{ 5, s_2_537, 536, 124, 0}, -{ 6, s_2_538, 536, 121, 0}, -{ 4, s_2_539, 536, 103, 0}, -{ 6, s_2_540, 536, 127, 0}, -{ 6, s_2_541, 536, 118, 0}, -{ 5, s_2_542, 536, 48, 0}, -{ 6, s_2_543, 536, 101, 0}, -{ 7, s_2_544, 536, 117, 0}, -{ 7, s_2_545, 536, 90, 0}, -{ 3, s_2_546, -1, 50, 0}, -{ 4, s_2_547, -1, 115, 0}, -{ 4, s_2_548, -1, 13, 0}, -{ 4, s_2_549, -1, 52, 0}, -{ 4, s_2_550, -1, 51, 0}, -{ 5, s_2_551, -1, 124, 0}, -{ 5, s_2_552, -1, 125, 0}, -{ 5, s_2_553, -1, 126, 0}, -{ 6, s_2_554, -1, 84, 0}, -{ 6, s_2_555, -1, 85, 0}, -{ 6, s_2_556, -1, 122, 0}, -{ 7, s_2_557, -1, 86, 0}, -{ 4, s_2_558, -1, 95, 0}, -{ 5, s_2_559, 558, 1, 0}, -{ 6, s_2_560, 558, 2, 0}, -{ 5, s_2_561, -1, 83, 0}, -{ 4, s_2_562, -1, 13, 0}, -{ 6, s_2_563, 562, 137, 0}, -{ 7, s_2_564, 562, 89, 0}, -{ 5, s_2_565, -1, 123, 0}, -{ 5, s_2_566, -1, 120, 0}, -{ 7, s_2_567, -1, 92, 0}, -{ 7, s_2_568, -1, 93, 0}, -{ 6, s_2_569, -1, 94, 0}, -{ 5, s_2_570, -1, 77, 0}, -{ 5, s_2_571, -1, 78, 0}, -{ 5, s_2_572, -1, 79, 0}, -{ 5, s_2_573, -1, 80, 0}, -{ 6, s_2_574, -1, 14, 0}, -{ 6, s_2_575, -1, 15, 0}, -{ 6, s_2_576, -1, 16, 0}, -{ 6, s_2_577, -1, 91, 0}, -{ 2, s_2_578, -1, 13, 0}, -{ 3, s_2_579, 578, 10, 0}, -{ 5, s_2_580, 579, 128, 0}, -{ 5, s_2_581, 579, 105, 0}, -{ 4, s_2_582, 579, 113, 0}, -{ 6, s_2_583, 582, 110, 0}, -{ 6, s_2_584, 582, 111, 0}, -{ 6, s_2_585, 582, 112, 0}, -{ 5, s_2_586, 579, 97, 0}, -{ 5, s_2_587, 579, 96, 0}, -{ 5, s_2_588, 579, 98, 0}, -{ 5, s_2_589, 579, 99, 0}, -{ 6, s_2_590, 579, 102, 0}, -{ 5, s_2_591, 578, 124, 0}, -{ 6, s_2_592, 578, 121, 0}, -{ 6, s_2_593, 578, 101, 0}, -{ 7, s_2_594, 578, 117, 0}, -{ 3, s_2_595, 578, 11, 0}, -{ 4, s_2_596, 595, 137, 0}, -{ 5, s_2_597, 595, 10, 0}, -{ 5, s_2_598, 595, 89, 0}, -{ 3, s_2_599, 578, 12, 0}, -{ 3, s_2_600, -1, 53, 0}, -{ 3, s_2_601, -1, 54, 0}, -{ 3, s_2_602, -1, 55, 0}, -{ 3, s_2_603, -1, 56, 0}, -{ 3, s_2_604, -1, 161, 0}, -{ 4, s_2_605, 604, 135, 0}, -{ 5, s_2_606, 604, 128, 0}, -{ 4, s_2_607, 604, 131, 0}, -{ 4, s_2_608, 604, 129, 0}, -{ 8, s_2_609, 608, 138, 0}, -{ 8, s_2_610, 608, 139, 0}, -{ 8, s_2_611, 608, 140, 0}, -{ 6, s_2_612, 608, 150, 0}, -{ 4, s_2_613, 604, 133, 0}, -{ 4, s_2_614, 604, 132, 0}, -{ 5, s_2_615, 604, 155, 0}, -{ 5, s_2_616, 604, 156, 0}, -{ 4, s_2_617, 604, 130, 0}, -{ 4, s_2_618, 604, 134, 0}, -{ 5, s_2_619, 618, 144, 0}, -{ 5, s_2_620, 618, 145, 0}, -{ 5, s_2_621, 618, 146, 0}, -{ 5, s_2_622, 618, 148, 0}, -{ 5, s_2_623, 618, 147, 0}, -{ 3, s_2_624, -1, 57, 0}, -{ 3, s_2_625, -1, 58, 0}, -{ 5, s_2_626, 625, 124, 0}, -{ 6, s_2_627, 625, 121, 0}, -{ 6, s_2_628, 625, 127, 0}, -{ 6, s_2_629, 625, 149, 0}, -{ 3, s_2_630, -1, 123, 0}, -{ 8, s_2_631, 630, 141, 0}, -{ 8, s_2_632, 630, 142, 0}, -{ 8, s_2_633, 630, 143, 0}, -{ 3, s_2_634, -1, 104, 0}, -{ 5, s_2_635, 634, 128, 0}, -{ 5, s_2_636, 634, 68, 0}, -{ 4, s_2_637, 634, 69, 0}, -{ 5, s_2_638, 634, 100, 0}, -{ 5, s_2_639, 634, 105, 0}, -{ 4, s_2_640, 634, 113, 0}, -{ 5, s_2_641, 634, 97, 0}, -{ 5, s_2_642, 634, 96, 0}, -{ 5, s_2_643, 634, 98, 0}, -{ 5, s_2_644, 634, 99, 0}, -{ 6, s_2_645, 634, 102, 0}, -{ 3, s_2_646, -1, 70, 0}, -{ 8, s_2_647, 646, 110, 0}, -{ 8, s_2_648, 646, 111, 0}, -{ 8, s_2_649, 646, 112, 0}, -{ 8, s_2_650, 646, 106, 0}, -{ 8, s_2_651, 646, 107, 0}, -{ 8, s_2_652, 646, 108, 0}, -{ 5, s_2_653, 646, 116, 0}, -{ 6, s_2_654, 646, 114, 0}, -{ 5, s_2_655, 646, 25, 0}, -{ 8, s_2_656, 655, 121, 0}, -{ 7, s_2_657, 655, 100, 0}, -{ 9, s_2_658, 655, 117, 0}, -{ 4, s_2_659, 646, 13, 0}, -{ 8, s_2_660, 659, 110, 0}, -{ 8, s_2_661, 659, 111, 0}, -{ 8, s_2_662, 659, 112, 0}, -{ 6, s_2_663, 646, 115, 0}, -{ 3, s_2_664, -1, 116, 0}, -{ 5, s_2_665, 664, 124, 0}, -{ 6, s_2_666, 664, 121, 0}, -{ 4, s_2_667, 664, 13, 0}, -{ 8, s_2_668, 667, 110, 0}, -{ 8, s_2_669, 667, 111, 0}, -{ 8, s_2_670, 667, 112, 0}, -{ 6, s_2_671, 664, 127, 0}, -{ 6, s_2_672, 664, 118, 0}, -{ 6, s_2_673, 664, 115, 0}, -{ 5, s_2_674, 664, 92, 0}, -{ 5, s_2_675, 664, 93, 0}, -{ 6, s_2_676, 664, 101, 0}, -{ 7, s_2_677, 664, 117, 0}, -{ 7, s_2_678, 664, 90, 0}, -{ 4, s_2_679, -1, 104, 0}, -{ 6, s_2_680, 679, 105, 0}, -{ 5, s_2_681, 679, 113, 0}, -{ 7, s_2_682, 681, 106, 0}, -{ 7, s_2_683, 681, 107, 0}, -{ 7, s_2_684, 681, 108, 0}, -{ 6, s_2_685, 679, 97, 0}, -{ 6, s_2_686, 679, 96, 0}, -{ 6, s_2_687, 679, 98, 0}, -{ 6, s_2_688, 679, 99, 0}, -{ 4, s_2_689, -1, 116, 0}, -{ 7, s_2_690, -1, 121, 0}, -{ 6, s_2_691, -1, 100, 0}, -{ 8, s_2_692, -1, 117, 0}, -{ 4, s_2_693, -1, 94, 0}, -{ 6, s_2_694, 693, 128, 0}, -{ 9, s_2_695, 693, 106, 0}, -{ 9, s_2_696, 693, 107, 0}, -{ 9, s_2_697, 693, 108, 0}, -{ 7, s_2_698, 693, 114, 0}, -{ 6, s_2_699, 693, 100, 0}, -{ 6, s_2_700, 693, 105, 0}, -{ 5, s_2_701, 693, 113, 0}, -{ 6, s_2_702, 693, 97, 0}, -{ 6, s_2_703, 693, 96, 0}, -{ 6, s_2_704, 693, 98, 0}, -{ 6, s_2_705, 693, 76, 0}, -{ 6, s_2_706, 693, 99, 0}, -{ 7, s_2_707, 693, 102, 0}, -{ 4, s_2_708, -1, 71, 0}, -{ 4, s_2_709, -1, 72, 0}, -{ 6, s_2_710, 709, 124, 0}, -{ 7, s_2_711, 709, 121, 0}, -{ 5, s_2_712, 709, 103, 0}, -{ 7, s_2_713, 709, 127, 0}, -{ 7, s_2_714, 709, 118, 0}, -{ 7, s_2_715, 709, 101, 0}, -{ 8, s_2_716, 709, 117, 0}, -{ 8, s_2_717, 709, 90, 0}, -{ 4, s_2_718, -1, 73, 0}, -{ 4, s_2_719, -1, 74, 0}, -{ 9, s_2_720, 719, 110, 0}, -{ 9, s_2_721, 719, 111, 0}, -{ 9, s_2_722, 719, 112, 0}, -{ 5, s_2_723, -1, 13, 0}, -{ 5, s_2_724, -1, 75, 0}, -{ 3, s_2_725, -1, 77, 0}, -{ 3, s_2_726, -1, 78, 0}, -{ 5, s_2_727, 726, 109, 0}, -{ 6, s_2_728, 727, 26, 0}, -{ 6, s_2_729, 727, 30, 0}, -{ 6, s_2_730, 727, 31, 0}, -{ 7, s_2_731, 727, 28, 0}, -{ 7, s_2_732, 727, 27, 0}, -{ 7, s_2_733, 727, 29, 0}, -{ 3, s_2_734, -1, 79, 0}, -{ 3, s_2_735, -1, 80, 0}, -{ 4, s_2_736, 735, 20, 0}, -{ 5, s_2_737, 736, 17, 0}, -{ 4, s_2_738, 735, 82, 0}, -{ 5, s_2_739, 738, 49, 0}, -{ 4, s_2_740, 735, 81, 0}, -{ 5, s_2_741, 735, 12, 0}, -{ 4, s_2_742, -1, 14, 0}, -{ 4, s_2_743, -1, 15, 0}, -{ 4, s_2_744, -1, 16, 0}, -{ 4, s_2_745, -1, 101, 0}, -{ 5, s_2_746, -1, 117, 0}, -{ 4, s_2_747, -1, 104, 0}, -{ 5, s_2_748, 747, 63, 0}, -{ 5, s_2_749, 747, 64, 0}, -{ 5, s_2_750, 747, 61, 0}, -{ 9, s_2_751, 750, 106, 0}, -{ 9, s_2_752, 750, 107, 0}, -{ 9, s_2_753, 750, 108, 0}, -{ 7, s_2_754, 750, 114, 0}, -{ 5, s_2_755, 747, 62, 0}, -{ 5, s_2_756, 747, 60, 0}, -{ 6, s_2_757, 747, 100, 0}, -{ 6, s_2_758, 747, 105, 0}, -{ 5, s_2_759, 747, 59, 0}, -{ 5, s_2_760, 747, 65, 0}, -{ 6, s_2_761, 760, 97, 0}, -{ 6, s_2_762, 760, 96, 0}, -{ 6, s_2_763, 760, 98, 0}, -{ 6, s_2_764, 760, 76, 0}, -{ 6, s_2_765, 760, 99, 0}, -{ 7, s_2_766, 747, 102, 0}, -{ 4, s_2_767, -1, 66, 0}, -{ 4, s_2_768, -1, 67, 0}, -{ 7, s_2_769, 768, 118, 0}, -{ 7, s_2_770, 768, 101, 0}, -{ 8, s_2_771, 768, 117, 0}, -{ 8, s_2_772, 768, 90, 0}, -{ 4, s_2_773, -1, 91, 0}, -{ 9, s_2_774, 773, 110, 0}, -{ 9, s_2_775, 773, 111, 0}, -{ 9, s_2_776, 773, 112, 0}, -{ 4, s_2_777, -1, 124, 0}, -{ 4, s_2_778, -1, 125, 0}, -{ 4, s_2_779, -1, 126, 0}, -{ 7, s_2_780, -1, 84, 0}, -{ 7, s_2_781, -1, 85, 0}, -{ 7, s_2_782, -1, 122, 0}, -{ 8, s_2_783, -1, 86, 0}, -{ 5, s_2_784, -1, 95, 0}, -{ 6, s_2_785, 784, 1, 0}, -{ 7, s_2_786, 784, 2, 0}, -{ 6, s_2_787, -1, 83, 0}, -{ 5, s_2_788, -1, 13, 0}, -{ 6, s_2_789, -1, 123, 0}, -{ 6, s_2_790, -1, 120, 0}, -{ 8, s_2_791, -1, 92, 0}, -{ 8, s_2_792, -1, 93, 0}, -{ 7, s_2_793, -1, 94, 0}, -{ 6, s_2_794, -1, 77, 0}, -{ 6, s_2_795, -1, 78, 0}, -{ 6, s_2_796, -1, 79, 0}, -{ 6, s_2_797, -1, 80, 0}, -{ 7, s_2_798, -1, 91, 0}, -{ 5, s_2_799, -1, 84, 0}, -{ 5, s_2_800, -1, 85, 0}, -{ 5, s_2_801, -1, 122, 0}, -{ 6, s_2_802, -1, 86, 0}, -{ 3, s_2_803, -1, 95, 0}, -{ 4, s_2_804, -1, 83, 0}, -{ 3, s_2_805, -1, 13, 0}, -{ 4, s_2_806, 805, 10, 0}, -{ 4, s_2_807, 805, 87, 0}, -{ 4, s_2_808, 805, 159, 0}, -{ 5, s_2_809, 805, 88, 0}, -{ 4, s_2_810, -1, 123, 0}, -{ 4, s_2_811, -1, 120, 0}, -{ 4, s_2_812, -1, 77, 0}, -{ 4, s_2_813, -1, 78, 0}, -{ 4, s_2_814, -1, 79, 0}, -{ 4, s_2_815, -1, 80, 0}, -{ 5, s_2_816, -1, 14, 0}, -{ 5, s_2_817, -1, 15, 0}, -{ 5, s_2_818, -1, 16, 0}, -{ 5, s_2_819, -1, 91, 0}, -{ 4, s_2_820, -1, 124, 0}, -{ 4, s_2_821, -1, 125, 0}, -{ 4, s_2_822, -1, 126, 0}, -{ 5, s_2_823, -1, 84, 0}, -{ 5, s_2_824, -1, 85, 0}, -{ 5, s_2_825, -1, 122, 0}, -{ 6, s_2_826, -1, 86, 0}, -{ 3, s_2_827, -1, 95, 0}, -{ 4, s_2_828, 827, 1, 0}, -{ 5, s_2_829, 827, 2, 0}, -{ 4, s_2_830, -1, 83, 0}, -{ 3, s_2_831, -1, 13, 0}, -{ 5, s_2_832, 831, 137, 0}, -{ 6, s_2_833, 831, 89, 0}, -{ 4, s_2_834, -1, 123, 0}, -{ 4, s_2_835, -1, 120, 0}, -{ 6, s_2_836, -1, 92, 0}, -{ 6, s_2_837, -1, 93, 0}, -{ 5, s_2_838, -1, 94, 0}, -{ 4, s_2_839, -1, 77, 0}, -{ 4, s_2_840, -1, 78, 0}, -{ 4, s_2_841, -1, 79, 0}, -{ 4, s_2_842, -1, 80, 0}, -{ 5, s_2_843, -1, 14, 0}, -{ 5, s_2_844, -1, 15, 0}, -{ 5, s_2_845, -1, 16, 0}, -{ 5, s_2_846, -1, 91, 0}, -{ 2, s_2_847, -1, 104, 0}, -{ 4, s_2_848, 847, 128, 0}, -{ 7, s_2_849, 847, 106, 0}, -{ 7, s_2_850, 847, 107, 0}, -{ 7, s_2_851, 847, 108, 0}, -{ 5, s_2_852, 847, 114, 0}, -{ 4, s_2_853, 847, 100, 0}, -{ 4, s_2_854, 847, 105, 0}, -{ 3, s_2_855, 847, 113, 0}, -{ 4, s_2_856, 847, 97, 0}, -{ 4, s_2_857, 847, 96, 0}, -{ 4, s_2_858, 847, 98, 0}, -{ 4, s_2_859, 847, 76, 0}, -{ 4, s_2_860, 847, 99, 0}, -{ 5, s_2_861, 847, 102, 0}, -{ 2, s_2_862, -1, 116, 0}, -{ 4, s_2_863, 862, 124, 0}, -{ 4, s_2_864, 862, 125, 0}, -{ 4, s_2_865, 862, 126, 0}, -{ 5, s_2_866, 865, 121, 0}, -{ 7, s_2_867, 862, 84, 0}, -{ 7, s_2_868, 862, 85, 0}, -{ 7, s_2_869, 862, 122, 0}, -{ 8, s_2_870, 862, 86, 0}, -{ 5, s_2_871, 862, 95, 0}, -{ 6, s_2_872, 871, 1, 0}, -{ 7, s_2_873, 871, 2, 0}, -{ 6, s_2_874, 862, 83, 0}, -{ 5, s_2_875, 862, 13, 0}, -{ 6, s_2_876, 862, 123, 0}, -{ 6, s_2_877, 862, 120, 0}, -{ 8, s_2_878, 862, 92, 0}, -{ 8, s_2_879, 862, 93, 0}, -{ 7, s_2_880, 862, 94, 0}, -{ 6, s_2_881, 862, 77, 0}, -{ 6, s_2_882, 862, 78, 0}, -{ 6, s_2_883, 862, 79, 0}, -{ 6, s_2_884, 862, 80, 0}, -{ 7, s_2_885, 862, 91, 0}, -{ 5, s_2_886, 862, 84, 0}, -{ 5, s_2_887, 862, 85, 0}, -{ 5, s_2_888, 862, 122, 0}, -{ 6, s_2_889, 862, 86, 0}, -{ 3, s_2_890, 862, 95, 0}, -{ 4, s_2_891, 890, 1, 0}, -{ 5, s_2_892, 890, 2, 0}, -{ 4, s_2_893, 862, 83, 0}, -{ 3, s_2_894, 862, 13, 0}, -{ 5, s_2_895, 894, 137, 0}, -{ 6, s_2_896, 894, 89, 0}, -{ 4, s_2_897, 862, 123, 0}, -{ 5, s_2_898, 897, 127, 0}, -{ 4, s_2_899, 862, 120, 0}, -{ 5, s_2_900, 862, 118, 0}, -{ 6, s_2_901, 862, 92, 0}, -{ 6, s_2_902, 862, 93, 0}, -{ 5, s_2_903, 862, 94, 0}, -{ 4, s_2_904, 862, 77, 0}, -{ 4, s_2_905, 862, 78, 0}, -{ 4, s_2_906, 862, 79, 0}, -{ 4, s_2_907, 862, 80, 0}, -{ 5, s_2_908, 862, 14, 0}, -{ 5, s_2_909, 862, 15, 0}, -{ 5, s_2_910, 862, 16, 0}, -{ 5, s_2_911, 862, 101, 0}, -{ 6, s_2_912, 862, 117, 0}, -{ 5, s_2_913, 862, 91, 0}, -{ 6, s_2_914, 913, 90, 0}, -{ 7, s_2_915, -1, 110, 0}, -{ 7, s_2_916, -1, 111, 0}, -{ 7, s_2_917, -1, 112, 0}, -{ 4, s_2_918, -1, 124, 0}, -{ 4, s_2_919, -1, 125, 0}, -{ 4, s_2_920, -1, 126, 0}, -{ 5, s_2_921, -1, 14, 0}, -{ 5, s_2_922, -1, 15, 0}, -{ 5, s_2_923, -1, 16, 0}, -{ 3, s_2_924, -1, 124, 0}, -{ 5, s_2_925, -1, 124, 0}, -{ 4, s_2_926, -1, 162, 0}, -{ 5, s_2_927, -1, 161, 0}, -{ 7, s_2_928, 927, 155, 0}, -{ 7, s_2_929, 927, 156, 0}, -{ 8, s_2_930, 927, 138, 0}, -{ 8, s_2_931, 927, 139, 0}, -{ 8, s_2_932, 927, 140, 0}, -{ 7, s_2_933, 927, 144, 0}, -{ 7, s_2_934, 927, 145, 0}, -{ 7, s_2_935, 927, 146, 0}, -{ 7, s_2_936, 927, 147, 0}, -{ 5, s_2_937, -1, 157, 0}, -{ 8, s_2_938, 937, 121, 0}, -{ 7, s_2_939, 937, 155, 0}, -{ 4, s_2_940, -1, 121, 0}, -{ 4, s_2_941, -1, 164, 0}, -{ 5, s_2_942, -1, 153, 0}, -{ 6, s_2_943, -1, 136, 0}, -{ 2, s_2_944, -1, 20, 0}, -{ 3, s_2_945, 944, 18, 0}, -{ 3, s_2_946, -1, 109, 0}, -{ 4, s_2_947, 946, 26, 0}, -{ 4, s_2_948, 946, 30, 0}, -{ 4, s_2_949, 946, 31, 0}, -{ 5, s_2_950, 946, 28, 0}, -{ 5, s_2_951, 946, 27, 0}, -{ 5, s_2_952, 946, 29, 0}, -{ 4, s_2_953, -1, 32, 0}, -{ 4, s_2_954, -1, 33, 0}, -{ 4, s_2_955, -1, 34, 0}, -{ 4, s_2_956, -1, 40, 0}, -{ 4, s_2_957, -1, 39, 0}, -{ 6, s_2_958, -1, 84, 0}, -{ 6, s_2_959, -1, 85, 0}, -{ 6, s_2_960, -1, 122, 0}, -{ 7, s_2_961, -1, 86, 0}, -{ 4, s_2_962, -1, 95, 0}, -{ 5, s_2_963, 962, 1, 0}, -{ 6, s_2_964, 962, 2, 0}, -{ 4, s_2_965, -1, 35, 0}, -{ 5, s_2_966, 965, 83, 0}, -{ 4, s_2_967, -1, 37, 0}, -{ 4, s_2_968, -1, 13, 0}, -{ 6, s_2_969, 968, 9, 0}, -{ 6, s_2_970, 968, 6, 0}, -{ 6, s_2_971, 968, 7, 0}, -{ 6, s_2_972, 968, 8, 0}, -{ 6, s_2_973, 968, 5, 0}, -{ 4, s_2_974, -1, 41, 0}, -{ 4, s_2_975, -1, 42, 0}, -{ 4, s_2_976, -1, 43, 0}, -{ 5, s_2_977, 976, 123, 0}, -{ 4, s_2_978, -1, 44, 0}, -{ 5, s_2_979, 978, 120, 0}, -{ 7, s_2_980, 978, 92, 0}, -{ 7, s_2_981, 978, 93, 0}, -{ 6, s_2_982, 978, 94, 0}, -{ 5, s_2_983, -1, 77, 0}, -{ 5, s_2_984, -1, 78, 0}, -{ 5, s_2_985, -1, 79, 0}, -{ 5, s_2_986, -1, 80, 0}, -{ 4, s_2_987, -1, 45, 0}, -{ 6, s_2_988, -1, 91, 0}, -{ 5, s_2_989, -1, 38, 0}, -{ 4, s_2_990, -1, 84, 0}, -{ 4, s_2_991, -1, 85, 0}, -{ 4, s_2_992, -1, 122, 0}, -{ 5, s_2_993, -1, 86, 0}, -{ 2, s_2_994, -1, 95, 0}, -{ 3, s_2_995, 994, 1, 0}, -{ 4, s_2_996, 994, 2, 0}, -{ 3, s_2_997, -1, 104, 0}, -{ 5, s_2_998, 997, 128, 0}, -{ 8, s_2_999, 997, 106, 0}, -{ 8, s_2_1000, 997, 107, 0}, -{ 8, s_2_1001, 997, 108, 0}, -{ 5, s_2_1002, 997, 47, 0}, -{ 6, s_2_1003, 997, 114, 0}, -{ 4, s_2_1004, 997, 46, 0}, -{ 5, s_2_1005, 997, 100, 0}, -{ 5, s_2_1006, 997, 105, 0}, -{ 4, s_2_1007, 997, 113, 0}, -{ 6, s_2_1008, 1007, 110, 0}, -{ 6, s_2_1009, 1007, 111, 0}, -{ 6, s_2_1010, 1007, 112, 0}, -{ 5, s_2_1011, 997, 97, 0}, -{ 5, s_2_1012, 997, 96, 0}, -{ 5, s_2_1013, 997, 98, 0}, -{ 5, s_2_1014, 997, 76, 0}, -{ 5, s_2_1015, 997, 99, 0}, -{ 6, s_2_1016, 997, 102, 0}, -{ 3, s_2_1017, -1, 83, 0}, -{ 3, s_2_1018, -1, 116, 0}, -{ 5, s_2_1019, 1018, 124, 0}, -{ 6, s_2_1020, 1018, 121, 0}, -{ 4, s_2_1021, 1018, 103, 0}, -{ 6, s_2_1022, 1018, 127, 0}, -{ 6, s_2_1023, 1018, 118, 0}, -{ 5, s_2_1024, 1018, 48, 0}, -{ 6, s_2_1025, 1018, 101, 0}, -{ 7, s_2_1026, 1018, 117, 0}, -{ 7, s_2_1027, 1018, 90, 0}, -{ 3, s_2_1028, -1, 50, 0}, -{ 4, s_2_1029, -1, 115, 0}, -{ 4, s_2_1030, -1, 13, 0}, -{ 4, s_2_1031, -1, 52, 0}, -{ 4, s_2_1032, -1, 51, 0}, -{ 2, s_2_1033, -1, 13, 0}, -{ 3, s_2_1034, 1033, 10, 0}, -{ 5, s_2_1035, 1034, 128, 0}, -{ 5, s_2_1036, 1034, 105, 0}, -{ 4, s_2_1037, 1034, 113, 0}, -{ 5, s_2_1038, 1034, 97, 0}, -{ 5, s_2_1039, 1034, 96, 0}, -{ 5, s_2_1040, 1034, 98, 0}, -{ 5, s_2_1041, 1034, 99, 0}, -{ 6, s_2_1042, 1034, 102, 0}, -{ 5, s_2_1043, 1033, 124, 0}, -{ 6, s_2_1044, 1033, 121, 0}, -{ 6, s_2_1045, 1033, 101, 0}, -{ 7, s_2_1046, 1033, 117, 0}, -{ 3, s_2_1047, 1033, 11, 0}, -{ 4, s_2_1048, 1047, 137, 0}, -{ 5, s_2_1049, 1047, 89, 0}, -{ 3, s_2_1050, 1033, 12, 0}, -{ 3, s_2_1051, -1, 53, 0}, -{ 3, s_2_1052, -1, 54, 0}, -{ 3, s_2_1053, -1, 55, 0}, -{ 3, s_2_1054, -1, 56, 0}, -{ 4, s_2_1055, -1, 135, 0}, -{ 4, s_2_1056, -1, 131, 0}, -{ 4, s_2_1057, -1, 129, 0}, -{ 4, s_2_1058, -1, 133, 0}, -{ 4, s_2_1059, -1, 132, 0}, -{ 4, s_2_1060, -1, 130, 0}, -{ 4, s_2_1061, -1, 134, 0}, -{ 3, s_2_1062, -1, 152, 0}, -{ 3, s_2_1063, -1, 154, 0}, -{ 3, s_2_1064, -1, 123, 0}, -{ 4, s_2_1065, -1, 161, 0}, -{ 6, s_2_1066, 1065, 128, 0}, -{ 6, s_2_1067, 1065, 155, 0}, -{ 5, s_2_1068, 1065, 160, 0}, -{ 6, s_2_1069, 1068, 153, 0}, -{ 7, s_2_1070, 1068, 141, 0}, -{ 7, s_2_1071, 1068, 142, 0}, -{ 7, s_2_1072, 1068, 143, 0}, -{ 4, s_2_1073, -1, 162, 0}, -{ 5, s_2_1074, 1073, 158, 0}, -{ 7, s_2_1075, 1073, 127, 0}, -{ 5, s_2_1076, -1, 164, 0}, -{ 3, s_2_1077, -1, 104, 0}, -{ 5, s_2_1078, 1077, 128, 0}, -{ 8, s_2_1079, 1077, 106, 0}, -{ 8, s_2_1080, 1077, 107, 0}, -{ 8, s_2_1081, 1077, 108, 0}, -{ 6, s_2_1082, 1077, 114, 0}, -{ 5, s_2_1083, 1077, 68, 0}, -{ 4, s_2_1084, 1077, 69, 0}, -{ 5, s_2_1085, 1077, 100, 0}, -{ 5, s_2_1086, 1077, 105, 0}, -{ 4, s_2_1087, 1077, 113, 0}, -{ 6, s_2_1088, 1087, 110, 0}, -{ 6, s_2_1089, 1087, 111, 0}, -{ 6, s_2_1090, 1087, 112, 0}, -{ 5, s_2_1091, 1077, 97, 0}, -{ 5, s_2_1092, 1077, 96, 0}, -{ 5, s_2_1093, 1077, 98, 0}, -{ 5, s_2_1094, 1077, 76, 0}, -{ 5, s_2_1095, 1077, 99, 0}, -{ 6, s_2_1096, 1077, 102, 0}, -{ 3, s_2_1097, -1, 70, 0}, -{ 3, s_2_1098, -1, 116, 0}, -{ 5, s_2_1099, 1098, 124, 0}, -{ 6, s_2_1100, 1098, 121, 0}, -{ 4, s_2_1101, 1098, 103, 0}, -{ 6, s_2_1102, 1098, 127, 0}, -{ 6, s_2_1103, 1098, 118, 0}, -{ 5, s_2_1104, 1098, 92, 0}, -{ 5, s_2_1105, 1098, 93, 0}, -{ 6, s_2_1106, 1098, 101, 0}, -{ 7, s_2_1107, 1098, 117, 0}, -{ 7, s_2_1108, 1098, 90, 0}, -{ 4, s_2_1109, -1, 94, 0}, -{ 4, s_2_1110, -1, 71, 0}, -{ 4, s_2_1111, -1, 72, 0}, -{ 4, s_2_1112, -1, 73, 0}, -{ 4, s_2_1113, -1, 74, 0}, -{ 4, s_2_1114, -1, 13, 0}, -{ 3, s_2_1115, -1, 77, 0}, -{ 3, s_2_1116, -1, 78, 0}, -{ 5, s_2_1117, 1116, 109, 0}, -{ 6, s_2_1118, 1117, 26, 0}, -{ 6, s_2_1119, 1117, 30, 0}, -{ 6, s_2_1120, 1117, 31, 0}, -{ 7, s_2_1121, 1117, 28, 0}, -{ 7, s_2_1122, 1117, 27, 0}, -{ 7, s_2_1123, 1117, 29, 0}, -{ 3, s_2_1124, -1, 79, 0}, -{ 3, s_2_1125, -1, 80, 0}, -{ 4, s_2_1126, 1125, 20, 0}, -{ 5, s_2_1127, 1126, 17, 0}, -{ 4, s_2_1128, 1125, 82, 0}, -{ 5, s_2_1129, 1128, 49, 0}, -{ 4, s_2_1130, 1125, 81, 0}, -{ 5, s_2_1131, 1125, 12, 0}, -{ 5, s_2_1132, -1, 116, 0}, -{ 7, s_2_1133, -1, 101, 0}, -{ 6, s_2_1134, -1, 104, 0}, -{ 8, s_2_1135, 1134, 100, 0}, -{ 8, s_2_1136, 1134, 105, 0}, -{ 9, s_2_1137, 1134, 106, 0}, -{ 9, s_2_1138, 1134, 107, 0}, -{ 9, s_2_1139, 1134, 108, 0}, -{ 8, s_2_1140, 1134, 97, 0}, -{ 8, s_2_1141, 1134, 96, 0}, -{ 8, s_2_1142, 1134, 98, 0}, -{ 8, s_2_1143, 1134, 99, 0}, -{ 6, s_2_1144, -1, 25, 0}, -{ 8, s_2_1145, 1144, 100, 0}, -{ 10, s_2_1146, 1144, 117, 0}, -{ 5, s_2_1147, -1, 13, 0}, -{ 6, s_2_1148, -1, 70, 0}, -{ 7, s_2_1149, -1, 115, 0}, -{ 4, s_2_1150, -1, 101, 0}, -{ 5, s_2_1151, -1, 117, 0}, -{ 5, s_2_1152, -1, 63, 0}, -{ 5, s_2_1153, -1, 64, 0}, -{ 5, s_2_1154, -1, 61, 0}, -{ 5, s_2_1155, -1, 62, 0}, -{ 5, s_2_1156, -1, 60, 0}, -{ 5, s_2_1157, -1, 59, 0}, -{ 5, s_2_1158, -1, 65, 0}, -{ 4, s_2_1159, -1, 66, 0}, -{ 4, s_2_1160, -1, 67, 0}, -{ 4, s_2_1161, -1, 91, 0}, -{ 5, s_2_1162, -1, 104, 0}, -{ 7, s_2_1163, 1162, 100, 0}, -{ 6, s_2_1164, 1162, 113, 0}, -{ 7, s_2_1165, 1164, 70, 0}, -{ 8, s_2_1166, 1164, 110, 0}, -{ 8, s_2_1167, 1164, 111, 0}, -{ 8, s_2_1168, 1164, 112, 0}, -{ 8, s_2_1169, 1162, 102, 0}, -{ 5, s_2_1170, -1, 116, 0}, -{ 6, s_2_1171, 1170, 103, 0}, -{ 9, s_2_1172, 1170, 90, 0}, -{ 6, s_2_1173, -1, 13, 0}, -{ 2, s_2_1174, -1, 104, 0}, -{ 4, s_2_1175, 1174, 105, 0}, -{ 3, s_2_1176, 1174, 113, 0}, -{ 4, s_2_1177, 1174, 97, 0}, -{ 4, s_2_1178, 1174, 96, 0}, -{ 4, s_2_1179, 1174, 98, 0}, -{ 4, s_2_1180, 1174, 99, 0}, -{ 2, s_2_1181, -1, 116, 0}, -{ 4, s_2_1182, -1, 124, 0}, -{ 4, s_2_1183, -1, 125, 0}, -{ 4, s_2_1184, -1, 126, 0}, -{ 7, s_2_1185, -1, 84, 0}, -{ 7, s_2_1186, -1, 85, 0}, -{ 7, s_2_1187, -1, 122, 0}, -{ 8, s_2_1188, -1, 86, 0}, -{ 5, s_2_1189, -1, 95, 0}, -{ 6, s_2_1190, 1189, 1, 0}, -{ 7, s_2_1191, 1189, 2, 0}, -{ 6, s_2_1192, -1, 83, 0}, -{ 5, s_2_1193, -1, 13, 0}, -{ 6, s_2_1194, -1, 123, 0}, -{ 8, s_2_1195, -1, 92, 0}, -{ 8, s_2_1196, -1, 93, 0}, -{ 7, s_2_1197, -1, 94, 0}, -{ 6, s_2_1198, -1, 77, 0}, -{ 6, s_2_1199, -1, 78, 0}, -{ 6, s_2_1200, -1, 79, 0}, -{ 6, s_2_1201, -1, 80, 0}, -{ 7, s_2_1202, -1, 91, 0}, -{ 5, s_2_1203, -1, 84, 0}, -{ 5, s_2_1204, -1, 85, 0}, -{ 5, s_2_1205, -1, 122, 0}, -{ 6, s_2_1206, -1, 86, 0}, -{ 3, s_2_1207, -1, 95, 0}, -{ 4, s_2_1208, 1207, 1, 0}, -{ 5, s_2_1209, 1207, 2, 0}, -{ 4, s_2_1210, -1, 104, 0}, -{ 4, s_2_1211, -1, 83, 0}, -{ 3, s_2_1212, -1, 13, 0}, -{ 5, s_2_1213, 1212, 137, 0}, -{ 6, s_2_1214, 1212, 89, 0}, -{ 4, s_2_1215, -1, 123, 0}, -{ 4, s_2_1216, -1, 120, 0}, -{ 6, s_2_1217, -1, 92, 0}, -{ 6, s_2_1218, -1, 93, 0}, -{ 5, s_2_1219, -1, 94, 0}, -{ 4, s_2_1220, -1, 77, 0}, -{ 4, s_2_1221, -1, 78, 0}, -{ 4, s_2_1222, -1, 79, 0}, -{ 4, s_2_1223, -1, 80, 0}, -{ 5, s_2_1224, -1, 14, 0}, -{ 5, s_2_1225, -1, 15, 0}, -{ 5, s_2_1226, -1, 16, 0}, -{ 5, s_2_1227, -1, 91, 0}, -{ 5, s_2_1228, -1, 121, 0}, -{ 4, s_2_1229, -1, 100, 0}, -{ 6, s_2_1230, -1, 117, 0}, -{ 2, s_2_1231, -1, 104, 0}, -{ 4, s_2_1232, 1231, 100, 0}, -{ 4, s_2_1233, 1231, 105, 0}, -{ 2, s_2_1234, -1, 119, 0}, -{ 2, s_2_1235, -1, 116, 0}, -{ 2, s_2_1236, -1, 104, 0}, -{ 4, s_2_1237, 1236, 128, 0}, -{ 4, s_2_1238, 1236, 100, 0}, -{ 4, s_2_1239, 1236, 105, 0}, -{ 3, s_2_1240, 1236, 113, 0}, -{ 4, s_2_1241, 1236, 97, 0}, -{ 4, s_2_1242, 1236, 96, 0}, -{ 4, s_2_1243, 1236, 98, 0}, -{ 4, s_2_1244, 1236, 99, 0}, -{ 5, s_2_1245, 1236, 102, 0}, -{ 2, s_2_1246, -1, 119, 0}, -{ 4, s_2_1247, 1246, 124, 0}, -{ 4, s_2_1248, 1246, 125, 0}, -{ 4, s_2_1249, 1246, 126, 0}, -{ 7, s_2_1250, 1246, 110, 0}, -{ 7, s_2_1251, 1246, 111, 0}, -{ 7, s_2_1252, 1246, 112, 0}, -{ 4, s_2_1253, 1246, 104, 0}, -{ 5, s_2_1254, 1253, 26, 0}, -{ 5, s_2_1255, 1253, 30, 0}, -{ 5, s_2_1256, 1253, 31, 0}, -{ 7, s_2_1257, 1253, 106, 0}, -{ 7, s_2_1258, 1253, 107, 0}, -{ 7, s_2_1259, 1253, 108, 0}, -{ 6, s_2_1260, 1253, 28, 0}, -{ 6, s_2_1261, 1253, 27, 0}, -{ 6, s_2_1262, 1253, 29, 0}, -{ 4, s_2_1263, 1246, 116, 0}, -{ 7, s_2_1264, 1263, 84, 0}, -{ 7, s_2_1265, 1263, 85, 0}, -{ 7, s_2_1266, 1263, 123, 0}, -{ 8, s_2_1267, 1263, 86, 0}, -{ 5, s_2_1268, 1263, 95, 0}, -{ 6, s_2_1269, 1268, 1, 0}, -{ 7, s_2_1270, 1268, 2, 0}, -{ 5, s_2_1271, 1263, 24, 0}, -{ 6, s_2_1272, 1271, 83, 0}, -{ 5, s_2_1273, 1263, 13, 0}, -{ 7, s_2_1274, 1263, 21, 0}, -{ 5, s_2_1275, 1263, 23, 0}, -{ 6, s_2_1276, 1275, 123, 0}, -{ 6, s_2_1277, 1263, 120, 0}, -{ 8, s_2_1278, 1263, 92, 0}, -{ 8, s_2_1279, 1263, 93, 0}, -{ 6, s_2_1280, 1263, 22, 0}, -{ 7, s_2_1281, 1263, 94, 0}, -{ 6, s_2_1282, 1263, 77, 0}, -{ 6, s_2_1283, 1263, 78, 0}, -{ 6, s_2_1284, 1263, 79, 0}, -{ 6, s_2_1285, 1263, 80, 0}, -{ 7, s_2_1286, 1263, 91, 0}, -{ 5, s_2_1287, 1246, 84, 0}, -{ 5, s_2_1288, 1246, 85, 0}, -{ 5, s_2_1289, 1246, 114, 0}, -{ 5, s_2_1290, 1246, 122, 0}, -{ 6, s_2_1291, 1246, 86, 0}, -{ 4, s_2_1292, 1246, 25, 0}, -{ 7, s_2_1293, 1292, 121, 0}, -{ 6, s_2_1294, 1292, 100, 0}, -{ 8, s_2_1295, 1292, 117, 0}, -{ 3, s_2_1296, 1246, 95, 0}, -{ 4, s_2_1297, 1296, 1, 0}, -{ 5, s_2_1298, 1296, 2, 0}, -{ 4, s_2_1299, 1246, 83, 0}, -{ 3, s_2_1300, 1246, 13, 0}, -{ 4, s_2_1301, 1300, 10, 0}, -{ 7, s_2_1302, 1301, 110, 0}, -{ 7, s_2_1303, 1301, 111, 0}, -{ 7, s_2_1304, 1301, 112, 0}, -{ 4, s_2_1305, 1300, 87, 0}, -{ 4, s_2_1306, 1300, 159, 0}, -{ 5, s_2_1307, 1300, 88, 0}, -{ 5, s_2_1308, 1246, 135, 0}, -{ 5, s_2_1309, 1246, 131, 0}, -{ 5, s_2_1310, 1246, 129, 0}, -{ 5, s_2_1311, 1246, 133, 0}, -{ 5, s_2_1312, 1246, 132, 0}, -{ 5, s_2_1313, 1246, 130, 0}, -{ 5, s_2_1314, 1246, 134, 0}, -{ 4, s_2_1315, 1246, 152, 0}, -{ 4, s_2_1316, 1246, 154, 0}, -{ 4, s_2_1317, 1246, 123, 0}, -{ 4, s_2_1318, 1246, 120, 0}, -{ 4, s_2_1319, 1246, 70, 0}, -{ 6, s_2_1320, 1246, 92, 0}, -{ 6, s_2_1321, 1246, 93, 0}, -{ 5, s_2_1322, 1246, 94, 0}, -{ 5, s_2_1323, 1246, 151, 0}, -{ 6, s_2_1324, 1246, 75, 0}, -{ 4, s_2_1325, 1246, 77, 0}, -{ 4, s_2_1326, 1246, 78, 0}, -{ 4, s_2_1327, 1246, 79, 0}, -{ 5, s_2_1328, 1246, 14, 0}, -{ 5, s_2_1329, 1246, 15, 0}, -{ 5, s_2_1330, 1246, 16, 0}, -{ 6, s_2_1331, 1246, 63, 0}, -{ 6, s_2_1332, 1246, 64, 0}, -{ 6, s_2_1333, 1246, 61, 0}, -{ 6, s_2_1334, 1246, 62, 0}, -{ 6, s_2_1335, 1246, 60, 0}, -{ 6, s_2_1336, 1246, 59, 0}, -{ 6, s_2_1337, 1246, 65, 0}, -{ 5, s_2_1338, 1246, 66, 0}, -{ 5, s_2_1339, 1246, 67, 0}, -{ 5, s_2_1340, 1246, 91, 0}, -{ 2, s_2_1341, -1, 116, 0}, -{ 4, s_2_1342, 1341, 124, 0}, -{ 4, s_2_1343, 1341, 125, 0}, -{ 4, s_2_1344, 1341, 126, 0}, -{ 5, s_2_1345, 1344, 121, 0}, -{ 7, s_2_1346, 1341, 84, 0}, -{ 7, s_2_1347, 1341, 85, 0}, -{ 7, s_2_1348, 1341, 122, 0}, -{ 8, s_2_1349, 1341, 86, 0}, -{ 5, s_2_1350, 1341, 95, 0}, -{ 6, s_2_1351, 1350, 1, 0}, -{ 7, s_2_1352, 1350, 2, 0}, -{ 6, s_2_1353, 1341, 83, 0}, -{ 5, s_2_1354, 1341, 13, 0}, -{ 6, s_2_1355, 1341, 123, 0}, -{ 6, s_2_1356, 1341, 120, 0}, -{ 8, s_2_1357, 1341, 92, 0}, -{ 8, s_2_1358, 1341, 93, 0}, -{ 7, s_2_1359, 1341, 94, 0}, -{ 6, s_2_1360, 1341, 77, 0}, -{ 6, s_2_1361, 1341, 78, 0}, -{ 6, s_2_1362, 1341, 79, 0}, -{ 6, s_2_1363, 1341, 80, 0}, -{ 7, s_2_1364, 1341, 91, 0}, -{ 5, s_2_1365, 1341, 84, 0}, -{ 5, s_2_1366, 1341, 85, 0}, -{ 5, s_2_1367, 1341, 122, 0}, -{ 6, s_2_1368, 1341, 86, 0}, -{ 3, s_2_1369, 1341, 95, 0}, -{ 4, s_2_1370, 1369, 1, 0}, -{ 5, s_2_1371, 1369, 2, 0}, -{ 4, s_2_1372, 1341, 83, 0}, -{ 3, s_2_1373, 1341, 13, 0}, -{ 5, s_2_1374, 1373, 137, 0}, -{ 6, s_2_1375, 1373, 89, 0}, -{ 4, s_2_1376, 1341, 123, 0}, -{ 5, s_2_1377, 1376, 127, 0}, -{ 4, s_2_1378, 1341, 120, 0}, -{ 5, s_2_1379, 1341, 118, 0}, -{ 6, s_2_1380, 1341, 92, 0}, -{ 6, s_2_1381, 1341, 93, 0}, -{ 5, s_2_1382, 1341, 94, 0}, -{ 4, s_2_1383, 1341, 77, 0}, -{ 4, s_2_1384, 1341, 78, 0}, -{ 4, s_2_1385, 1341, 79, 0}, -{ 4, s_2_1386, 1341, 80, 0}, -{ 5, s_2_1387, 1341, 14, 0}, -{ 5, s_2_1388, 1341, 15, 0}, -{ 5, s_2_1389, 1341, 16, 0}, -{ 5, s_2_1390, 1341, 101, 0}, -{ 6, s_2_1391, 1341, 117, 0}, -{ 5, s_2_1392, 1341, 91, 0}, -{ 6, s_2_1393, 1392, 90, 0}, -{ 4, s_2_1394, -1, 124, 0}, -{ 4, s_2_1395, -1, 125, 0}, -{ 4, s_2_1396, -1, 126, 0}, -{ 3, s_2_1397, -1, 20, 0}, -{ 5, s_2_1398, 1397, 19, 0}, -{ 4, s_2_1399, 1397, 18, 0}, -{ 5, s_2_1400, -1, 32, 0}, -{ 5, s_2_1401, -1, 33, 0}, -{ 5, s_2_1402, -1, 34, 0}, -{ 5, s_2_1403, -1, 40, 0}, -{ 5, s_2_1404, -1, 39, 0}, -{ 5, s_2_1405, -1, 35, 0}, -{ 5, s_2_1406, -1, 37, 0}, -{ 5, s_2_1407, -1, 36, 0}, -{ 7, s_2_1408, 1407, 9, 0}, -{ 7, s_2_1409, 1407, 6, 0}, -{ 7, s_2_1410, 1407, 7, 0}, -{ 7, s_2_1411, 1407, 8, 0}, -{ 7, s_2_1412, 1407, 5, 0}, -{ 5, s_2_1413, -1, 41, 0}, -{ 5, s_2_1414, -1, 42, 0}, -{ 5, s_2_1415, -1, 43, 0}, -{ 5, s_2_1416, -1, 44, 0}, -{ 5, s_2_1417, -1, 45, 0}, -{ 6, s_2_1418, -1, 38, 0}, -{ 5, s_2_1419, -1, 84, 0}, -{ 5, s_2_1420, -1, 85, 0}, -{ 5, s_2_1421, -1, 122, 0}, -{ 6, s_2_1422, -1, 86, 0}, -{ 3, s_2_1423, -1, 95, 0}, -{ 4, s_2_1424, 1423, 1, 0}, -{ 5, s_2_1425, 1423, 2, 0}, -{ 4, s_2_1426, -1, 104, 0}, -{ 6, s_2_1427, 1426, 47, 0}, -{ 5, s_2_1428, 1426, 46, 0}, -{ 4, s_2_1429, -1, 83, 0}, -{ 4, s_2_1430, -1, 116, 0}, -{ 6, s_2_1431, 1430, 48, 0}, -{ 4, s_2_1432, -1, 50, 0}, -{ 5, s_2_1433, -1, 52, 0}, -{ 5, s_2_1434, -1, 51, 0}, -{ 3, s_2_1435, -1, 13, 0}, -{ 4, s_2_1436, 1435, 10, 0}, -{ 4, s_2_1437, 1435, 11, 0}, -{ 5, s_2_1438, 1437, 137, 0}, -{ 6, s_2_1439, 1437, 10, 0}, -{ 6, s_2_1440, 1437, 89, 0}, -{ 4, s_2_1441, 1435, 12, 0}, -{ 4, s_2_1442, -1, 53, 0}, -{ 4, s_2_1443, -1, 54, 0}, -{ 4, s_2_1444, -1, 55, 0}, -{ 4, s_2_1445, -1, 56, 0}, -{ 5, s_2_1446, -1, 135, 0}, -{ 5, s_2_1447, -1, 131, 0}, -{ 5, s_2_1448, -1, 129, 0}, -{ 5, s_2_1449, -1, 133, 0}, -{ 5, s_2_1450, -1, 132, 0}, -{ 5, s_2_1451, -1, 130, 0}, -{ 5, s_2_1452, -1, 134, 0}, -{ 4, s_2_1453, -1, 57, 0}, -{ 4, s_2_1454, -1, 58, 0}, -{ 4, s_2_1455, -1, 123, 0}, -{ 4, s_2_1456, -1, 120, 0}, -{ 6, s_2_1457, 1456, 68, 0}, -{ 5, s_2_1458, 1456, 69, 0}, -{ 4, s_2_1459, -1, 70, 0}, -{ 6, s_2_1460, -1, 92, 0}, -{ 6, s_2_1461, -1, 93, 0}, -{ 5, s_2_1462, -1, 94, 0}, -{ 5, s_2_1463, -1, 71, 0}, -{ 5, s_2_1464, -1, 72, 0}, -{ 5, s_2_1465, -1, 73, 0}, -{ 5, s_2_1466, -1, 74, 0}, -{ 4, s_2_1467, -1, 77, 0}, -{ 4, s_2_1468, -1, 78, 0}, -{ 4, s_2_1469, -1, 79, 0}, -{ 4, s_2_1470, -1, 80, 0}, -{ 5, s_2_1471, 1470, 82, 0}, -{ 5, s_2_1472, 1470, 81, 0}, -{ 5, s_2_1473, -1, 3, 0}, -{ 6, s_2_1474, -1, 4, 0}, -{ 5, s_2_1475, -1, 14, 0}, -{ 5, s_2_1476, -1, 15, 0}, -{ 5, s_2_1477, -1, 16, 0}, -{ 6, s_2_1478, -1, 63, 0}, -{ 6, s_2_1479, -1, 64, 0}, -{ 6, s_2_1480, -1, 61, 0}, -{ 6, s_2_1481, -1, 62, 0}, -{ 6, s_2_1482, -1, 60, 0}, -{ 6, s_2_1483, -1, 59, 0}, -{ 6, s_2_1484, -1, 65, 0}, -{ 5, s_2_1485, -1, 66, 0}, -{ 5, s_2_1486, -1, 67, 0}, -{ 5, s_2_1487, -1, 91, 0}, -{ 2, s_2_1488, -1, 104, 0}, -{ 4, s_2_1489, 1488, 128, 0}, -{ 4, s_2_1490, 1488, 100, 0}, -{ 4, s_2_1491, 1488, 105, 0}, -{ 3, s_2_1492, 1488, 113, 0}, -{ 4, s_2_1493, 1488, 97, 0}, -{ 4, s_2_1494, 1488, 96, 0}, -{ 4, s_2_1495, 1488, 98, 0}, -{ 4, s_2_1496, 1488, 99, 0}, -{ 5, s_2_1497, 1488, 102, 0}, -{ 4, s_2_1498, -1, 124, 0}, -{ 5, s_2_1499, -1, 121, 0}, -{ 5, s_2_1500, -1, 101, 0}, -{ 6, s_2_1501, -1, 117, 0}, -{ 4, s_2_1502, -1, 10, 0}, -{ 2, s_2_1503, -1, 104, 0}, -{ 4, s_2_1504, 1503, 128, 0}, -{ 7, s_2_1505, 1503, 106, 0}, -{ 7, s_2_1506, 1503, 107, 0}, -{ 7, s_2_1507, 1503, 108, 0}, -{ 5, s_2_1508, 1503, 114, 0}, -{ 4, s_2_1509, 1503, 100, 0}, -{ 4, s_2_1510, 1503, 105, 0}, -{ 3, s_2_1511, 1503, 113, 0}, -{ 5, s_2_1512, 1511, 110, 0}, -{ 5, s_2_1513, 1511, 111, 0}, -{ 5, s_2_1514, 1511, 112, 0}, -{ 4, s_2_1515, 1503, 97, 0}, -{ 4, s_2_1516, 1503, 96, 0}, -{ 4, s_2_1517, 1503, 98, 0}, -{ 4, s_2_1518, 1503, 76, 0}, -{ 4, s_2_1519, 1503, 99, 0}, -{ 5, s_2_1520, 1503, 102, 0}, -{ 2, s_2_1521, -1, 20, 0}, -{ 3, s_2_1522, 1521, 18, 0}, -{ 2, s_2_1523, -1, 116, 0}, -{ 4, s_2_1524, 1523, 124, 0}, -{ 5, s_2_1525, 1523, 121, 0}, -{ 3, s_2_1526, 1523, 24, 0}, -{ 3, s_2_1527, 1523, 103, 0}, -{ 5, s_2_1528, 1523, 21, 0}, -{ 3, s_2_1529, 1523, 23, 0}, -{ 5, s_2_1530, 1529, 127, 0}, -{ 5, s_2_1531, 1523, 118, 0}, -{ 4, s_2_1532, 1523, 22, 0}, -{ 5, s_2_1533, 1523, 101, 0}, -{ 6, s_2_1534, 1523, 117, 0}, -{ 6, s_2_1535, 1523, 90, 0}, -{ 4, s_2_1536, -1, 32, 0}, -{ 4, s_2_1537, -1, 33, 0}, -{ 4, s_2_1538, -1, 34, 0}, -{ 4, s_2_1539, -1, 40, 0}, -{ 4, s_2_1540, -1, 39, 0}, -{ 4, s_2_1541, -1, 35, 0}, -{ 4, s_2_1542, -1, 37, 0}, -{ 4, s_2_1543, -1, 36, 0}, -{ 4, s_2_1544, -1, 41, 0}, -{ 4, s_2_1545, -1, 42, 0}, -{ 4, s_2_1546, -1, 43, 0}, -{ 4, s_2_1547, -1, 44, 0}, -{ 4, s_2_1548, -1, 45, 0}, -{ 5, s_2_1549, -1, 38, 0}, -{ 4, s_2_1550, -1, 84, 0}, -{ 4, s_2_1551, -1, 85, 0}, -{ 4, s_2_1552, -1, 122, 0}, -{ 5, s_2_1553, -1, 86, 0}, -{ 2, s_2_1554, -1, 95, 0}, -{ 3, s_2_1555, 1554, 1, 0}, -{ 4, s_2_1556, 1554, 2, 0}, -{ 3, s_2_1557, -1, 104, 0}, -{ 5, s_2_1558, 1557, 128, 0}, -{ 8, s_2_1559, 1557, 106, 0}, -{ 8, s_2_1560, 1557, 107, 0}, -{ 8, s_2_1561, 1557, 108, 0}, -{ 5, s_2_1562, 1557, 47, 0}, -{ 6, s_2_1563, 1557, 114, 0}, -{ 4, s_2_1564, 1557, 46, 0}, -{ 5, s_2_1565, 1557, 100, 0}, -{ 5, s_2_1566, 1557, 105, 0}, -{ 4, s_2_1567, 1557, 113, 0}, -{ 6, s_2_1568, 1567, 110, 0}, -{ 6, s_2_1569, 1567, 111, 0}, -{ 6, s_2_1570, 1567, 112, 0}, -{ 5, s_2_1571, 1557, 97, 0}, -{ 5, s_2_1572, 1557, 96, 0}, -{ 5, s_2_1573, 1557, 98, 0}, -{ 5, s_2_1574, 1557, 76, 0}, -{ 5, s_2_1575, 1557, 99, 0}, -{ 6, s_2_1576, 1557, 102, 0}, -{ 3, s_2_1577, -1, 83, 0}, -{ 3, s_2_1578, -1, 116, 0}, -{ 5, s_2_1579, 1578, 124, 0}, -{ 6, s_2_1580, 1578, 121, 0}, -{ 4, s_2_1581, 1578, 103, 0}, -{ 6, s_2_1582, 1578, 127, 0}, -{ 6, s_2_1583, 1578, 118, 0}, -{ 6, s_2_1584, 1578, 101, 0}, -{ 7, s_2_1585, 1578, 117, 0}, -{ 7, s_2_1586, 1578, 90, 0}, -{ 4, s_2_1587, -1, 115, 0}, -{ 4, s_2_1588, -1, 13, 0}, -{ 3, s_2_1589, -1, 104, 0}, -{ 5, s_2_1590, 1589, 128, 0}, -{ 4, s_2_1591, 1589, 52, 0}, -{ 5, s_2_1592, 1591, 100, 0}, -{ 5, s_2_1593, 1591, 105, 0}, -{ 4, s_2_1594, 1589, 113, 0}, -{ 5, s_2_1595, 1589, 97, 0}, -{ 5, s_2_1596, 1589, 96, 0}, -{ 5, s_2_1597, 1589, 98, 0}, -{ 5, s_2_1598, 1589, 99, 0}, -{ 6, s_2_1599, 1589, 102, 0}, -{ 3, s_2_1600, -1, 119, 0}, -{ 8, s_2_1601, 1600, 110, 0}, -{ 8, s_2_1602, 1600, 111, 0}, -{ 8, s_2_1603, 1600, 112, 0}, -{ 8, s_2_1604, 1600, 106, 0}, -{ 8, s_2_1605, 1600, 107, 0}, -{ 8, s_2_1606, 1600, 108, 0}, -{ 5, s_2_1607, 1600, 116, 0}, -{ 6, s_2_1608, 1600, 114, 0}, -{ 5, s_2_1609, 1600, 25, 0}, -{ 8, s_2_1610, 1609, 121, 0}, -{ 7, s_2_1611, 1609, 100, 0}, -{ 9, s_2_1612, 1609, 117, 0}, -{ 4, s_2_1613, 1600, 51, 0}, -{ 4, s_2_1614, 1600, 13, 0}, -{ 8, s_2_1615, 1614, 110, 0}, -{ 8, s_2_1616, 1614, 111, 0}, -{ 8, s_2_1617, 1614, 112, 0}, -{ 5, s_2_1618, 1600, 70, 0}, -{ 6, s_2_1619, 1600, 115, 0}, -{ 3, s_2_1620, -1, 116, 0}, -{ 5, s_2_1621, 1620, 124, 0}, -{ 6, s_2_1622, 1620, 121, 0}, -{ 4, s_2_1623, 1620, 13, 0}, -{ 8, s_2_1624, 1623, 110, 0}, -{ 8, s_2_1625, 1623, 111, 0}, -{ 8, s_2_1626, 1623, 112, 0}, -{ 6, s_2_1627, 1620, 127, 0}, -{ 5, s_2_1628, 1620, 70, 0}, -{ 6, s_2_1629, 1628, 118, 0}, -{ 6, s_2_1630, 1620, 115, 0}, -{ 6, s_2_1631, 1620, 101, 0}, -{ 7, s_2_1632, 1620, 117, 0}, -{ 7, s_2_1633, 1620, 90, 0}, -{ 4, s_2_1634, -1, 104, 0}, -{ 6, s_2_1635, 1634, 105, 0}, -{ 5, s_2_1636, 1634, 113, 0}, -{ 7, s_2_1637, 1636, 106, 0}, -{ 7, s_2_1638, 1636, 107, 0}, -{ 7, s_2_1639, 1636, 108, 0}, -{ 6, s_2_1640, 1634, 97, 0}, -{ 6, s_2_1641, 1634, 96, 0}, -{ 6, s_2_1642, 1634, 98, 0}, -{ 6, s_2_1643, 1634, 99, 0}, -{ 4, s_2_1644, -1, 116, 0}, -{ 4, s_2_1645, -1, 25, 0}, -{ 7, s_2_1646, 1645, 121, 0}, -{ 6, s_2_1647, 1645, 100, 0}, -{ 8, s_2_1648, 1645, 117, 0}, -{ 4, s_2_1649, -1, 104, 0}, -{ 6, s_2_1650, 1649, 128, 0}, -{ 9, s_2_1651, 1649, 106, 0}, -{ 9, s_2_1652, 1649, 107, 0}, -{ 9, s_2_1653, 1649, 108, 0}, -{ 7, s_2_1654, 1649, 114, 0}, -{ 6, s_2_1655, 1649, 100, 0}, -{ 6, s_2_1656, 1649, 105, 0}, -{ 5, s_2_1657, 1649, 113, 0}, -{ 6, s_2_1658, 1649, 97, 0}, -{ 6, s_2_1659, 1649, 96, 0}, -{ 6, s_2_1660, 1649, 98, 0}, -{ 6, s_2_1661, 1649, 76, 0}, -{ 6, s_2_1662, 1649, 99, 0}, -{ 7, s_2_1663, 1649, 102, 0}, -{ 4, s_2_1664, -1, 116, 0}, -{ 6, s_2_1665, 1664, 124, 0}, -{ 7, s_2_1666, 1664, 121, 0}, -{ 5, s_2_1667, 1664, 103, 0}, -{ 7, s_2_1668, 1664, 127, 0}, -{ 7, s_2_1669, 1664, 118, 0}, -{ 7, s_2_1670, 1664, 101, 0}, -{ 8, s_2_1671, 1664, 117, 0}, -{ 8, s_2_1672, 1664, 90, 0}, -{ 9, s_2_1673, -1, 110, 0}, -{ 9, s_2_1674, -1, 111, 0}, -{ 9, s_2_1675, -1, 112, 0}, -{ 5, s_2_1676, -1, 13, 0}, -{ 2, s_2_1677, -1, 13, 0}, -{ 3, s_2_1678, 1677, 104, 0}, -{ 5, s_2_1679, 1678, 128, 0}, -{ 5, s_2_1680, 1678, 105, 0}, -{ 4, s_2_1681, 1678, 113, 0}, -{ 5, s_2_1682, 1678, 97, 0}, -{ 5, s_2_1683, 1678, 96, 0}, -{ 5, s_2_1684, 1678, 98, 0}, -{ 5, s_2_1685, 1678, 99, 0}, -{ 6, s_2_1686, 1678, 102, 0}, -{ 5, s_2_1687, 1677, 124, 0}, -{ 6, s_2_1688, 1677, 121, 0}, -{ 6, s_2_1689, 1677, 101, 0}, -{ 7, s_2_1690, 1677, 117, 0}, -{ 3, s_2_1691, 1677, 11, 0}, -{ 4, s_2_1692, 1691, 137, 0}, -{ 5, s_2_1693, 1691, 89, 0}, -{ 3, s_2_1694, -1, 120, 0}, -{ 5, s_2_1695, 1694, 68, 0}, -{ 4, s_2_1696, 1694, 69, 0}, -{ 3, s_2_1697, -1, 70, 0}, -{ 5, s_2_1698, -1, 92, 0}, -{ 5, s_2_1699, -1, 93, 0}, -{ 4, s_2_1700, -1, 94, 0}, -{ 4, s_2_1701, -1, 71, 0}, -{ 4, s_2_1702, -1, 72, 0}, -{ 4, s_2_1703, -1, 73, 0}, -{ 4, s_2_1704, -1, 74, 0}, -{ 4, s_2_1705, -1, 13, 0}, -{ 3, s_2_1706, -1, 13, 0}, -{ 3, s_2_1707, -1, 77, 0}, -{ 3, s_2_1708, -1, 78, 0}, -{ 3, s_2_1709, -1, 79, 0}, -{ 3, s_2_1710, -1, 80, 0}, -{ 4, s_2_1711, -1, 3, 0}, -{ 5, s_2_1712, -1, 4, 0}, -{ 2, s_2_1713, -1, 161, 0}, -{ 4, s_2_1714, 1713, 128, 0}, -{ 4, s_2_1715, 1713, 155, 0}, -{ 4, s_2_1716, 1713, 156, 0}, -{ 3, s_2_1717, 1713, 160, 0}, -{ 4, s_2_1718, 1713, 144, 0}, -{ 4, s_2_1719, 1713, 145, 0}, -{ 4, s_2_1720, 1713, 146, 0}, -{ 4, s_2_1721, 1713, 147, 0}, -{ 2, s_2_1722, -1, 163, 0}, -{ 7, s_2_1723, 1722, 141, 0}, -{ 7, s_2_1724, 1722, 142, 0}, -{ 7, s_2_1725, 1722, 143, 0}, -{ 7, s_2_1726, 1722, 138, 0}, -{ 7, s_2_1727, 1722, 139, 0}, -{ 7, s_2_1728, 1722, 140, 0}, -{ 4, s_2_1729, 1722, 162, 0}, -{ 5, s_2_1730, 1722, 150, 0}, -{ 4, s_2_1731, 1722, 157, 0}, -{ 7, s_2_1732, 1731, 121, 0}, -{ 6, s_2_1733, 1731, 155, 0}, -{ 3, s_2_1734, 1722, 164, 0}, -{ 7, s_2_1735, 1734, 141, 0}, -{ 7, s_2_1736, 1734, 142, 0}, -{ 7, s_2_1737, 1734, 143, 0}, -{ 4, s_2_1738, 1722, 153, 0}, -{ 5, s_2_1739, 1722, 136, 0}, -{ 2, s_2_1740, -1, 162, 0}, -{ 4, s_2_1741, 1740, 124, 0}, -{ 5, s_2_1742, 1740, 121, 0}, -{ 3, s_2_1743, 1740, 158, 0}, -{ 5, s_2_1744, 1740, 127, 0}, -{ 5, s_2_1745, 1740, 149, 0}, -{ 2, s_2_1746, -1, 104, 0}, -{ 4, s_2_1747, 1746, 128, 0}, -{ 7, s_2_1748, 1746, 106, 0}, -{ 7, s_2_1749, 1746, 107, 0}, -{ 7, s_2_1750, 1746, 108, 0}, -{ 5, s_2_1751, 1746, 114, 0}, -{ 4, s_2_1752, 1746, 100, 0}, -{ 4, s_2_1753, 1746, 105, 0}, -{ 3, s_2_1754, 1746, 113, 0}, -{ 5, s_2_1755, 1754, 110, 0}, -{ 5, s_2_1756, 1754, 111, 0}, -{ 5, s_2_1757, 1754, 112, 0}, -{ 4, s_2_1758, 1746, 97, 0}, -{ 4, s_2_1759, 1746, 96, 0}, -{ 4, s_2_1760, 1746, 98, 0}, -{ 6, s_2_1761, 1760, 100, 0}, -{ 4, s_2_1762, 1746, 76, 0}, -{ 4, s_2_1763, 1746, 99, 0}, -{ 5, s_2_1764, 1746, 102, 0}, -{ 2, s_2_1765, -1, 116, 0}, -{ 4, s_2_1766, 1765, 124, 0}, -{ 5, s_2_1767, 1765, 121, 0}, -{ 5, s_2_1768, 1765, 127, 0}, -{ 5, s_2_1769, 1765, 118, 0}, -{ 5, s_2_1770, 1765, 101, 0}, -{ 6, s_2_1771, 1765, 117, 0}, -{ 6, s_2_1772, 1765, 90, 0}, -{ 3, s_2_1773, -1, 13, 0}, -{ 6, s_2_1774, -1, 110, 0}, -{ 6, s_2_1775, -1, 111, 0}, -{ 6, s_2_1776, -1, 112, 0}, -{ 2, s_2_1777, -1, 20, 0}, -{ 4, s_2_1778, 1777, 19, 0}, -{ 3, s_2_1779, 1777, 18, 0}, -{ 3, s_2_1780, -1, 104, 0}, -{ 5, s_2_1781, 1780, 128, 0}, -{ 8, s_2_1782, 1780, 106, 0}, -{ 8, s_2_1783, 1780, 107, 0}, -{ 8, s_2_1784, 1780, 108, 0}, -{ 6, s_2_1785, 1780, 114, 0}, -{ 5, s_2_1786, 1780, 100, 0}, -{ 5, s_2_1787, 1780, 105, 0}, -{ 5, s_2_1788, 1780, 97, 0}, -{ 5, s_2_1789, 1780, 96, 0}, -{ 5, s_2_1790, 1780, 98, 0}, -{ 5, s_2_1791, 1780, 76, 0}, -{ 5, s_2_1792, 1780, 99, 0}, -{ 6, s_2_1793, 1780, 102, 0}, -{ 3, s_2_1794, -1, 104, 0}, -{ 4, s_2_1795, 1794, 26, 0}, -{ 5, s_2_1796, 1795, 128, 0}, -{ 4, s_2_1797, 1794, 30, 0}, -{ 4, s_2_1798, 1794, 31, 0}, -{ 5, s_2_1799, 1798, 100, 0}, -{ 5, s_2_1800, 1798, 105, 0}, -{ 4, s_2_1801, 1794, 113, 0}, -{ 6, s_2_1802, 1801, 106, 0}, -{ 6, s_2_1803, 1801, 107, 0}, -{ 6, s_2_1804, 1801, 108, 0}, -{ 5, s_2_1805, 1794, 97, 0}, -{ 5, s_2_1806, 1794, 96, 0}, -{ 5, s_2_1807, 1794, 98, 0}, -{ 5, s_2_1808, 1794, 99, 0}, -{ 5, s_2_1809, 1794, 28, 0}, -{ 5, s_2_1810, 1794, 27, 0}, -{ 6, s_2_1811, 1810, 102, 0}, -{ 5, s_2_1812, 1794, 29, 0}, -{ 3, s_2_1813, -1, 116, 0}, -{ 4, s_2_1814, 1813, 32, 0}, -{ 4, s_2_1815, 1813, 33, 0}, -{ 4, s_2_1816, 1813, 34, 0}, -{ 4, s_2_1817, 1813, 40, 0}, -{ 4, s_2_1818, 1813, 39, 0}, -{ 6, s_2_1819, 1813, 84, 0}, -{ 6, s_2_1820, 1813, 85, 0}, -{ 6, s_2_1821, 1813, 122, 0}, -{ 7, s_2_1822, 1813, 86, 0}, -{ 4, s_2_1823, 1813, 95, 0}, -{ 4, s_2_1824, 1813, 24, 0}, -{ 5, s_2_1825, 1824, 83, 0}, -{ 4, s_2_1826, 1813, 37, 0}, -{ 4, s_2_1827, 1813, 13, 0}, -{ 6, s_2_1828, 1827, 9, 0}, -{ 6, s_2_1829, 1827, 6, 0}, -{ 6, s_2_1830, 1827, 7, 0}, -{ 6, s_2_1831, 1827, 8, 0}, -{ 6, s_2_1832, 1827, 5, 0}, -{ 4, s_2_1833, 1813, 41, 0}, -{ 4, s_2_1834, 1813, 42, 0}, -{ 6, s_2_1835, 1834, 21, 0}, -{ 4, s_2_1836, 1813, 23, 0}, -{ 5, s_2_1837, 1836, 123, 0}, -{ 4, s_2_1838, 1813, 44, 0}, -{ 5, s_2_1839, 1838, 120, 0}, -{ 5, s_2_1840, 1838, 22, 0}, -{ 5, s_2_1841, 1813, 77, 0}, -{ 5, s_2_1842, 1813, 78, 0}, -{ 5, s_2_1843, 1813, 79, 0}, -{ 5, s_2_1844, 1813, 80, 0}, -{ 4, s_2_1845, 1813, 45, 0}, -{ 6, s_2_1846, 1813, 91, 0}, -{ 5, s_2_1847, 1813, 38, 0}, -{ 4, s_2_1848, -1, 84, 0}, -{ 4, s_2_1849, -1, 85, 0}, -{ 4, s_2_1850, -1, 122, 0}, -{ 5, s_2_1851, -1, 86, 0}, -{ 3, s_2_1852, -1, 25, 0}, -{ 6, s_2_1853, 1852, 121, 0}, -{ 5, s_2_1854, 1852, 100, 0}, -{ 7, s_2_1855, 1852, 117, 0}, -{ 2, s_2_1856, -1, 95, 0}, -{ 3, s_2_1857, 1856, 1, 0}, -{ 4, s_2_1858, 1856, 2, 0}, -{ 3, s_2_1859, -1, 104, 0}, -{ 5, s_2_1860, 1859, 47, 0}, -{ 4, s_2_1861, 1859, 46, 0}, -{ 3, s_2_1862, -1, 83, 0}, -{ 3, s_2_1863, -1, 116, 0}, -{ 5, s_2_1864, 1863, 48, 0}, -{ 3, s_2_1865, -1, 50, 0}, -{ 4, s_2_1866, -1, 52, 0}, -{ 5, s_2_1867, -1, 124, 0}, -{ 5, s_2_1868, -1, 125, 0}, -{ 5, s_2_1869, -1, 126, 0}, -{ 8, s_2_1870, -1, 84, 0}, -{ 8, s_2_1871, -1, 85, 0}, -{ 8, s_2_1872, -1, 122, 0}, -{ 9, s_2_1873, -1, 86, 0}, -{ 6, s_2_1874, -1, 95, 0}, -{ 7, s_2_1875, 1874, 1, 0}, -{ 8, s_2_1876, 1874, 2, 0}, -{ 7, s_2_1877, -1, 83, 0}, -{ 6, s_2_1878, -1, 13, 0}, -{ 7, s_2_1879, -1, 123, 0}, -{ 7, s_2_1880, -1, 120, 0}, -{ 9, s_2_1881, -1, 92, 0}, -{ 9, s_2_1882, -1, 93, 0}, -{ 8, s_2_1883, -1, 94, 0}, -{ 7, s_2_1884, -1, 77, 0}, -{ 7, s_2_1885, -1, 78, 0}, -{ 7, s_2_1886, -1, 79, 0}, -{ 7, s_2_1887, -1, 80, 0}, -{ 8, s_2_1888, -1, 91, 0}, -{ 6, s_2_1889, -1, 84, 0}, -{ 6, s_2_1890, -1, 85, 0}, -{ 6, s_2_1891, -1, 122, 0}, -{ 7, s_2_1892, -1, 86, 0}, -{ 4, s_2_1893, -1, 95, 0}, -{ 5, s_2_1894, 1893, 1, 0}, -{ 6, s_2_1895, 1893, 2, 0}, -{ 4, s_2_1896, -1, 51, 0}, -{ 5, s_2_1897, 1896, 83, 0}, -{ 4, s_2_1898, -1, 13, 0}, -{ 5, s_2_1899, 1898, 10, 0}, -{ 5, s_2_1900, 1898, 87, 0}, -{ 5, s_2_1901, 1898, 159, 0}, -{ 6, s_2_1902, 1898, 88, 0}, -{ 5, s_2_1903, -1, 123, 0}, -{ 5, s_2_1904, -1, 120, 0}, -{ 7, s_2_1905, -1, 92, 0}, -{ 7, s_2_1906, -1, 93, 0}, -{ 6, s_2_1907, -1, 94, 0}, -{ 5, s_2_1908, -1, 77, 0}, -{ 5, s_2_1909, -1, 78, 0}, -{ 5, s_2_1910, -1, 79, 0}, -{ 5, s_2_1911, -1, 80, 0}, -{ 6, s_2_1912, -1, 14, 0}, -{ 6, s_2_1913, -1, 15, 0}, -{ 6, s_2_1914, -1, 16, 0}, -{ 6, s_2_1915, -1, 91, 0}, -{ 5, s_2_1916, -1, 124, 0}, -{ 5, s_2_1917, -1, 125, 0}, -{ 5, s_2_1918, -1, 126, 0}, -{ 6, s_2_1919, -1, 84, 0}, -{ 6, s_2_1920, -1, 85, 0}, -{ 6, s_2_1921, -1, 122, 0}, -{ 7, s_2_1922, -1, 86, 0}, -{ 4, s_2_1923, -1, 95, 0}, -{ 5, s_2_1924, 1923, 1, 0}, -{ 6, s_2_1925, 1923, 2, 0}, -{ 5, s_2_1926, -1, 83, 0}, -{ 4, s_2_1927, -1, 13, 0}, -{ 6, s_2_1928, 1927, 137, 0}, -{ 7, s_2_1929, 1927, 89, 0}, -{ 5, s_2_1930, -1, 123, 0}, -{ 5, s_2_1931, -1, 120, 0}, -{ 7, s_2_1932, -1, 92, 0}, -{ 7, s_2_1933, -1, 93, 0}, -{ 6, s_2_1934, -1, 94, 0}, -{ 5, s_2_1935, -1, 77, 0}, -{ 5, s_2_1936, -1, 78, 0}, -{ 5, s_2_1937, -1, 79, 0}, -{ 5, s_2_1938, -1, 80, 0}, -{ 6, s_2_1939, -1, 14, 0}, -{ 6, s_2_1940, -1, 15, 0}, -{ 6, s_2_1941, -1, 16, 0}, -{ 6, s_2_1942, -1, 91, 0}, -{ 2, s_2_1943, -1, 13, 0}, -{ 3, s_2_1944, 1943, 10, 0}, -{ 6, s_2_1945, 1944, 110, 0}, -{ 6, s_2_1946, 1944, 111, 0}, -{ 6, s_2_1947, 1944, 112, 0}, -{ 3, s_2_1948, 1943, 11, 0}, -{ 4, s_2_1949, 1948, 137, 0}, -{ 5, s_2_1950, 1948, 10, 0}, -{ 5, s_2_1951, 1948, 89, 0}, -{ 3, s_2_1952, 1943, 12, 0}, -{ 3, s_2_1953, -1, 53, 0}, -{ 3, s_2_1954, -1, 54, 0}, -{ 3, s_2_1955, -1, 55, 0}, -{ 3, s_2_1956, -1, 56, 0}, -{ 4, s_2_1957, -1, 135, 0}, -{ 4, s_2_1958, -1, 131, 0}, -{ 4, s_2_1959, -1, 129, 0}, -{ 4, s_2_1960, -1, 133, 0}, -{ 4, s_2_1961, -1, 132, 0}, -{ 4, s_2_1962, -1, 130, 0}, -{ 4, s_2_1963, -1, 134, 0}, -{ 3, s_2_1964, -1, 57, 0}, -{ 3, s_2_1965, -1, 58, 0}, -{ 3, s_2_1966, -1, 123, 0}, -{ 3, s_2_1967, -1, 120, 0}, -{ 5, s_2_1968, 1967, 68, 0}, -{ 4, s_2_1969, 1967, 69, 0}, -{ 3, s_2_1970, -1, 70, 0}, -{ 5, s_2_1971, -1, 92, 0}, -{ 5, s_2_1972, -1, 93, 0}, -{ 4, s_2_1973, -1, 94, 0}, -{ 4, s_2_1974, -1, 71, 0}, -{ 4, s_2_1975, -1, 72, 0}, -{ 4, s_2_1976, -1, 73, 0}, -{ 4, s_2_1977, -1, 74, 0}, -{ 5, s_2_1978, -1, 75, 0}, -{ 3, s_2_1979, -1, 77, 0}, -{ 3, s_2_1980, -1, 78, 0}, -{ 3, s_2_1981, -1, 79, 0}, -{ 3, s_2_1982, -1, 80, 0}, -{ 4, s_2_1983, 1982, 82, 0}, -{ 4, s_2_1984, 1982, 81, 0}, -{ 4, s_2_1985, -1, 3, 0}, -{ 5, s_2_1986, -1, 4, 0}, -{ 5, s_2_1987, -1, 63, 0}, -{ 5, s_2_1988, -1, 64, 0}, -{ 5, s_2_1989, -1, 61, 0}, -{ 5, s_2_1990, -1, 62, 0}, -{ 5, s_2_1991, -1, 60, 0}, -{ 5, s_2_1992, -1, 59, 0}, -{ 5, s_2_1993, -1, 65, 0}, -{ 4, s_2_1994, -1, 66, 0}, -{ 4, s_2_1995, -1, 67, 0}, -{ 4, s_2_1996, -1, 91, 0}, -{ 4, s_2_1997, -1, 97, 0}, -{ 4, s_2_1998, -1, 96, 0}, -{ 4, s_2_1999, -1, 98, 0}, -{ 4, s_2_2000, -1, 99, 0}, -{ 3, s_2_2001, -1, 95, 0}, -{ 3, s_2_2002, -1, 104, 0}, -{ 5, s_2_2003, 2002, 100, 0}, -{ 5, s_2_2004, 2002, 105, 0}, -{ 4, s_2_2005, 2002, 113, 0}, -{ 5, s_2_2006, 2002, 97, 0}, -{ 5, s_2_2007, 2002, 96, 0}, -{ 5, s_2_2008, 2002, 98, 0}, -{ 5, s_2_2009, 2002, 99, 0}, -{ 6, s_2_2010, 2002, 102, 0}, -{ 3, s_2_2011, -1, 119, 0}, -{ 8, s_2_2012, 2011, 110, 0}, -{ 8, s_2_2013, 2011, 111, 0}, -{ 8, s_2_2014, 2011, 112, 0}, -{ 8, s_2_2015, 2011, 106, 0}, -{ 8, s_2_2016, 2011, 107, 0}, -{ 8, s_2_2017, 2011, 108, 0}, -{ 5, s_2_2018, 2011, 116, 0}, -{ 6, s_2_2019, 2011, 114, 0}, -{ 5, s_2_2020, 2011, 25, 0}, -{ 7, s_2_2021, 2020, 100, 0}, -{ 9, s_2_2022, 2020, 117, 0}, -{ 4, s_2_2023, 2011, 13, 0}, -{ 8, s_2_2024, 2023, 110, 0}, -{ 8, s_2_2025, 2023, 111, 0}, -{ 8, s_2_2026, 2023, 112, 0}, -{ 5, s_2_2027, 2011, 70, 0}, -{ 6, s_2_2028, 2011, 115, 0}, -{ 3, s_2_2029, -1, 116, 0}, -{ 4, s_2_2030, 2029, 103, 0}, -{ 6, s_2_2031, 2029, 118, 0}, -{ 6, s_2_2032, 2029, 101, 0}, -{ 7, s_2_2033, 2029, 117, 0}, -{ 7, s_2_2034, 2029, 90, 0} -}; - -static const symbol s_3_0[1] = { 'a' }; -static const symbol s_3_1[3] = { 'o', 'g', 'a' }; -static const symbol s_3_2[3] = { 'a', 'm', 'a' }; -static const symbol s_3_3[3] = { 'i', 'm', 'a' }; -static const symbol s_3_4[3] = { 'e', 'n', 'a' }; -static const symbol s_3_5[1] = { 'e' }; -static const symbol s_3_6[2] = { 'o', 'g' }; -static const symbol s_3_7[4] = { 'a', 'n', 'o', 'g' }; -static const symbol s_3_8[4] = { 'e', 'n', 'o', 'g' }; -static const symbol s_3_9[4] = { 'a', 'n', 'i', 'h' }; -static const symbol s_3_10[4] = { 'e', 'n', 'i', 'h' }; -static const symbol s_3_11[1] = { 'i' }; -static const symbol s_3_12[3] = { 'a', 'n', 'i' }; -static const symbol s_3_13[3] = { 'e', 'n', 'i' }; -static const symbol s_3_14[4] = { 'a', 'n', 'o', 'j' }; -static const symbol s_3_15[4] = { 'e', 'n', 'o', 'j' }; -static const symbol s_3_16[4] = { 'a', 'n', 'i', 'm' }; -static const symbol s_3_17[4] = { 'e', 'n', 'i', 'm' }; -static const symbol s_3_18[2] = { 'o', 'm' }; -static const symbol s_3_19[4] = { 'e', 'n', 'o', 'm' }; -static const symbol s_3_20[1] = { 'o' }; -static const symbol s_3_21[3] = { 'a', 'n', 'o' }; -static const symbol s_3_22[3] = { 'e', 'n', 'o' }; -static const symbol s_3_23[3] = { 'o', 's', 't' }; -static const symbol s_3_24[1] = { 'u' }; -static const symbol s_3_25[3] = { 'e', 'n', 'u' }; - -static const struct among a_3[26] = -{ -{ 1, s_3_0, -1, 1, 0}, -{ 3, s_3_1, 0, 1, 0}, -{ 3, s_3_2, 0, 1, 0}, -{ 3, s_3_3, 0, 1, 0}, -{ 3, s_3_4, 0, 1, 0}, -{ 1, s_3_5, -1, 1, 0}, -{ 2, s_3_6, -1, 1, 0}, -{ 4, s_3_7, 6, 1, 0}, -{ 4, s_3_8, 6, 1, 0}, -{ 4, s_3_9, -1, 1, 0}, -{ 4, s_3_10, -1, 1, 0}, -{ 1, s_3_11, -1, 1, 0}, -{ 3, s_3_12, 11, 1, 0}, -{ 3, s_3_13, 11, 1, 0}, -{ 4, s_3_14, -1, 1, 0}, -{ 4, s_3_15, -1, 1, 0}, -{ 4, s_3_16, -1, 1, 0}, -{ 4, s_3_17, -1, 1, 0}, -{ 2, s_3_18, -1, 1, 0}, -{ 4, s_3_19, 18, 1, 0}, -{ 1, s_3_20, -1, 1, 0}, -{ 3, s_3_21, 20, 1, 0}, -{ 3, s_3_22, 20, 1, 0}, -{ 3, s_3_23, -1, 1, 0}, -{ 1, s_3_24, -1, 1, 0}, -{ 3, s_3_25, 24, 1, 0} -}; - -static const unsigned char g_v[] = { 17, 65, 16 }; - -static const unsigned char g_sa[] = { 65, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 128 }; - -static const unsigned char g_ca[] = { 119, 95, 23, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 16 }; - -static const unsigned char g_rg[] = { 1 }; static const symbol s_0[] = { 'a' }; static const symbol s_1[] = { 'b' }; @@ -4790,814 +322,5408 @@ static const symbol s_288[] = { 'i' }; static const symbol s_289[] = { 'e' }; static const symbol s_290[] = { 'n' }; +static const symbol s_0_0[2] = { 0xD0, 0xB0 }; +static const symbol s_0_1[2] = { 0xD0, 0xB1 }; +static const symbol s_0_2[2] = { 0xD0, 0xB2 }; +static const symbol s_0_3[2] = { 0xD0, 0xB3 }; +static const symbol s_0_4[2] = { 0xD0, 0xB4 }; +static const symbol s_0_5[2] = { 0xD0, 0xB5 }; +static const symbol s_0_6[2] = { 0xD0, 0xB6 }; +static const symbol s_0_7[2] = { 0xD0, 0xB7 }; +static const symbol s_0_8[2] = { 0xD0, 0xB8 }; +static const symbol s_0_9[2] = { 0xD0, 0xBA }; +static const symbol s_0_10[2] = { 0xD0, 0xBB }; +static const symbol s_0_11[2] = { 0xD0, 0xBC }; +static const symbol s_0_12[2] = { 0xD0, 0xBD }; +static const symbol s_0_13[2] = { 0xD0, 0xBE }; +static const symbol s_0_14[2] = { 0xD0, 0xBF }; +static const symbol s_0_15[2] = { 0xD1, 0x80 }; +static const symbol s_0_16[2] = { 0xD1, 0x81 }; +static const symbol s_0_17[2] = { 0xD1, 0x82 }; +static const symbol s_0_18[2] = { 0xD1, 0x83 }; +static const symbol s_0_19[2] = { 0xD1, 0x84 }; +static const symbol s_0_20[2] = { 0xD1, 0x85 }; +static const symbol s_0_21[2] = { 0xD1, 0x86 }; +static const symbol s_0_22[2] = { 0xD1, 0x87 }; +static const symbol s_0_23[2] = { 0xD1, 0x88 }; +static const symbol s_0_24[2] = { 0xD1, 0x92 }; +static const symbol s_0_25[2] = { 0xD1, 0x98 }; +static const symbol s_0_26[2] = { 0xD1, 0x99 }; +static const symbol s_0_27[2] = { 0xD1, 0x9A }; +static const symbol s_0_28[2] = { 0xD1, 0x9B }; +static const symbol s_0_29[2] = { 0xD1, 0x9F }; +static const struct among a_0[30] = { +{ 2, s_0_0, 0, 1, 0}, +{ 2, s_0_1, 0, 2, 0}, +{ 2, s_0_2, 0, 3, 0}, +{ 2, s_0_3, 0, 4, 0}, +{ 2, s_0_4, 0, 5, 0}, +{ 2, s_0_5, 0, 7, 0}, +{ 2, s_0_6, 0, 8, 0}, +{ 2, s_0_7, 0, 9, 0}, +{ 2, s_0_8, 0, 10, 0}, +{ 2, s_0_9, 0, 12, 0}, +{ 2, s_0_10, 0, 13, 0}, +{ 2, s_0_11, 0, 15, 0}, +{ 2, s_0_12, 0, 16, 0}, +{ 2, s_0_13, 0, 18, 0}, +{ 2, s_0_14, 0, 19, 0}, +{ 2, s_0_15, 0, 20, 0}, +{ 2, s_0_16, 0, 21, 0}, +{ 2, s_0_17, 0, 22, 0}, +{ 2, s_0_18, 0, 24, 0}, +{ 2, s_0_19, 0, 25, 0}, +{ 2, s_0_20, 0, 26, 0}, +{ 2, s_0_21, 0, 27, 0}, +{ 2, s_0_22, 0, 28, 0}, +{ 2, s_0_23, 0, 30, 0}, +{ 2, s_0_24, 0, 6, 0}, +{ 2, s_0_25, 0, 11, 0}, +{ 2, s_0_26, 0, 14, 0}, +{ 2, s_0_27, 0, 17, 0}, +{ 2, s_0_28, 0, 23, 0}, +{ 2, s_0_29, 0, 29, 0} +}; + +static const symbol s_1_0[4] = { 'd', 'a', 'b', 'a' }; +static const symbol s_1_1[5] = { 'a', 'j', 'a', 'c', 'a' }; +static const symbol s_1_2[5] = { 'e', 'j', 'a', 'c', 'a' }; +static const symbol s_1_3[5] = { 'l', 'j', 'a', 'c', 'a' }; +static const symbol s_1_4[5] = { 'n', 'j', 'a', 'c', 'a' }; +static const symbol s_1_5[5] = { 'o', 'j', 'a', 'c', 'a' }; +static const symbol s_1_6[5] = { 'a', 'l', 'a', 'c', 'a' }; +static const symbol s_1_7[5] = { 'e', 'l', 'a', 'c', 'a' }; +static const symbol s_1_8[5] = { 'o', 'l', 'a', 'c', 'a' }; +static const symbol s_1_9[4] = { 'm', 'a', 'c', 'a' }; +static const symbol s_1_10[4] = { 'n', 'a', 'c', 'a' }; +static const symbol s_1_11[4] = { 'r', 'a', 'c', 'a' }; +static const symbol s_1_12[4] = { 's', 'a', 'c', 'a' }; +static const symbol s_1_13[4] = { 'v', 'a', 'c', 'a' }; +static const symbol s_1_14[5] = { 0xC5, 0xA1, 'a', 'c', 'a' }; +static const symbol s_1_15[4] = { 'a', 'o', 'c', 'a' }; +static const symbol s_1_16[5] = { 'a', 'c', 'a', 'k', 'a' }; +static const symbol s_1_17[5] = { 'a', 'j', 'a', 'k', 'a' }; +static const symbol s_1_18[5] = { 'o', 'j', 'a', 'k', 'a' }; +static const symbol s_1_19[5] = { 'a', 'n', 'a', 'k', 'a' }; +static const symbol s_1_20[5] = { 'a', 't', 'a', 'k', 'a' }; +static const symbol s_1_21[5] = { 'e', 't', 'a', 'k', 'a' }; +static const symbol s_1_22[5] = { 'i', 't', 'a', 'k', 'a' }; +static const symbol s_1_23[5] = { 'o', 't', 'a', 'k', 'a' }; +static const symbol s_1_24[5] = { 'u', 't', 'a', 'k', 'a' }; +static const symbol s_1_25[6] = { 'a', 0xC4, 0x8D, 'a', 'k', 'a' }; +static const symbol s_1_26[5] = { 'e', 's', 'a', 'm', 'a' }; +static const symbol s_1_27[5] = { 'i', 'z', 'a', 'm', 'a' }; +static const symbol s_1_28[6] = { 'j', 'a', 'c', 'i', 'm', 'a' }; +static const symbol s_1_29[6] = { 'n', 'i', 'c', 'i', 'm', 'a' }; +static const symbol s_1_30[6] = { 't', 'i', 'c', 'i', 'm', 'a' }; +static const symbol s_1_31[8] = { 't', 'e', 't', 'i', 'c', 'i', 'm', 'a' }; +static const symbol s_1_32[6] = { 'z', 'i', 'c', 'i', 'm', 'a' }; +static const symbol s_1_33[6] = { 'a', 't', 'c', 'i', 'm', 'a' }; +static const symbol s_1_34[6] = { 'u', 't', 'c', 'i', 'm', 'a' }; +static const symbol s_1_35[6] = { 0xC4, 0x8D, 'c', 'i', 'm', 'a' }; +static const symbol s_1_36[6] = { 'p', 'e', 's', 'i', 'm', 'a' }; +static const symbol s_1_37[6] = { 'i', 'n', 'z', 'i', 'm', 'a' }; +static const symbol s_1_38[6] = { 'l', 'o', 'z', 'i', 'm', 'a' }; +static const symbol s_1_39[6] = { 'm', 'e', 't', 'a', 'r', 'a' }; +static const symbol s_1_40[7] = { 'c', 'e', 'n', 't', 'a', 'r', 'a' }; +static const symbol s_1_41[6] = { 'i', 's', 't', 'a', 'r', 'a' }; +static const symbol s_1_42[5] = { 'e', 'k', 'a', 't', 'a' }; +static const symbol s_1_43[5] = { 'a', 'n', 'a', 't', 'a' }; +static const symbol s_1_44[6] = { 'n', 's', 't', 'a', 'v', 'a' }; +static const symbol s_1_45[7] = { 'k', 'u', 's', 't', 'a', 'v', 'a' }; +static const symbol s_1_46[4] = { 'a', 'j', 'a', 'c' }; +static const symbol s_1_47[4] = { 'e', 'j', 'a', 'c' }; +static const symbol s_1_48[4] = { 'l', 'j', 'a', 'c' }; +static const symbol s_1_49[4] = { 'n', 'j', 'a', 'c' }; +static const symbol s_1_50[5] = { 'a', 'n', 'j', 'a', 'c' }; +static const symbol s_1_51[4] = { 'o', 'j', 'a', 'c' }; +static const symbol s_1_52[4] = { 'a', 'l', 'a', 'c' }; +static const symbol s_1_53[4] = { 'e', 'l', 'a', 'c' }; +static const symbol s_1_54[4] = { 'o', 'l', 'a', 'c' }; +static const symbol s_1_55[3] = { 'm', 'a', 'c' }; +static const symbol s_1_56[3] = { 'n', 'a', 'c' }; +static const symbol s_1_57[3] = { 'r', 'a', 'c' }; +static const symbol s_1_58[3] = { 's', 'a', 'c' }; +static const symbol s_1_59[3] = { 'v', 'a', 'c' }; +static const symbol s_1_60[4] = { 0xC5, 0xA1, 'a', 'c' }; +static const symbol s_1_61[4] = { 'j', 'e', 'b', 'e' }; +static const symbol s_1_62[4] = { 'o', 'l', 'c', 'e' }; +static const symbol s_1_63[4] = { 'k', 'u', 's', 'e' }; +static const symbol s_1_64[4] = { 'r', 'a', 'v', 'e' }; +static const symbol s_1_65[4] = { 's', 'a', 'v', 'e' }; +static const symbol s_1_66[5] = { 0xC5, 0xA1, 'a', 'v', 'e' }; +static const symbol s_1_67[4] = { 'b', 'a', 'c', 'i' }; +static const symbol s_1_68[4] = { 'j', 'a', 'c', 'i' }; +static const symbol s_1_69[7] = { 't', 'v', 'e', 'n', 'i', 'c', 'i' }; +static const symbol s_1_70[5] = { 's', 'n', 'i', 'c', 'i' }; +static const symbol s_1_71[6] = { 't', 'e', 't', 'i', 'c', 'i' }; +static const symbol s_1_72[5] = { 'b', 'o', 'j', 'c', 'i' }; +static const symbol s_1_73[5] = { 'v', 'o', 'j', 'c', 'i' }; +static const symbol s_1_74[5] = { 'o', 'j', 's', 'c', 'i' }; +static const symbol s_1_75[4] = { 'a', 't', 'c', 'i' }; +static const symbol s_1_76[4] = { 'i', 't', 'c', 'i' }; +static const symbol s_1_77[4] = { 'u', 't', 'c', 'i' }; +static const symbol s_1_78[4] = { 0xC4, 0x8D, 'c', 'i' }; +static const symbol s_1_79[4] = { 'p', 'e', 's', 'i' }; +static const symbol s_1_80[4] = { 'i', 'n', 'z', 'i' }; +static const symbol s_1_81[4] = { 'l', 'o', 'z', 'i' }; +static const symbol s_1_82[4] = { 'a', 'c', 'a', 'k' }; +static const symbol s_1_83[4] = { 'u', 's', 'a', 'k' }; +static const symbol s_1_84[4] = { 'a', 't', 'a', 'k' }; +static const symbol s_1_85[4] = { 'e', 't', 'a', 'k' }; +static const symbol s_1_86[4] = { 'i', 't', 'a', 'k' }; +static const symbol s_1_87[4] = { 'o', 't', 'a', 'k' }; +static const symbol s_1_88[4] = { 'u', 't', 'a', 'k' }; +static const symbol s_1_89[5] = { 'a', 0xC4, 0x8D, 'a', 'k' }; +static const symbol s_1_90[5] = { 'u', 0xC5, 0xA1, 'a', 'k' }; +static const symbol s_1_91[4] = { 'i', 'z', 'a', 'm' }; +static const symbol s_1_92[5] = { 't', 'i', 'c', 'a', 'n' }; +static const symbol s_1_93[5] = { 'c', 'a', 'j', 'a', 'n' }; +static const symbol s_1_94[6] = { 0xC4, 0x8D, 'a', 'j', 'a', 'n' }; +static const symbol s_1_95[6] = { 'v', 'o', 'l', 'j', 'a', 'n' }; +static const symbol s_1_96[5] = { 'e', 's', 'k', 'a', 'n' }; +static const symbol s_1_97[4] = { 'a', 'l', 'a', 'n' }; +static const symbol s_1_98[5] = { 'b', 'i', 'l', 'a', 'n' }; +static const symbol s_1_99[5] = { 'g', 'i', 'l', 'a', 'n' }; +static const symbol s_1_100[5] = { 'n', 'i', 'l', 'a', 'n' }; +static const symbol s_1_101[5] = { 'r', 'i', 'l', 'a', 'n' }; +static const symbol s_1_102[5] = { 's', 'i', 'l', 'a', 'n' }; +static const symbol s_1_103[5] = { 't', 'i', 'l', 'a', 'n' }; +static const symbol s_1_104[6] = { 'a', 'v', 'i', 'l', 'a', 'n' }; +static const symbol s_1_105[5] = { 'l', 'a', 'r', 'a', 'n' }; +static const symbol s_1_106[4] = { 'e', 'r', 'a', 'n' }; +static const symbol s_1_107[4] = { 'a', 's', 'a', 'n' }; +static const symbol s_1_108[4] = { 'e', 's', 'a', 'n' }; +static const symbol s_1_109[5] = { 'd', 'u', 's', 'a', 'n' }; +static const symbol s_1_110[5] = { 'k', 'u', 's', 'a', 'n' }; +static const symbol s_1_111[4] = { 'a', 't', 'a', 'n' }; +static const symbol s_1_112[6] = { 'p', 'l', 'e', 't', 'a', 'n' }; +static const symbol s_1_113[5] = { 't', 'e', 't', 'a', 'n' }; +static const symbol s_1_114[5] = { 'a', 'n', 't', 'a', 'n' }; +static const symbol s_1_115[6] = { 'p', 'r', 'a', 'v', 'a', 'n' }; +static const symbol s_1_116[6] = { 's', 't', 'a', 'v', 'a', 'n' }; +static const symbol s_1_117[5] = { 's', 'i', 'v', 'a', 'n' }; +static const symbol s_1_118[5] = { 't', 'i', 'v', 'a', 'n' }; +static const symbol s_1_119[4] = { 'o', 'z', 'a', 'n' }; +static const symbol s_1_120[6] = { 't', 'i', 0xC4, 0x8D, 'a', 'n' }; +static const symbol s_1_121[5] = { 'a', 0xC5, 0xA1, 'a', 'n' }; +static const symbol s_1_122[6] = { 'd', 'u', 0xC5, 0xA1, 'a', 'n' }; +static const symbol s_1_123[5] = { 'm', 'e', 't', 'a', 'r' }; +static const symbol s_1_124[6] = { 'c', 'e', 'n', 't', 'a', 'r' }; +static const symbol s_1_125[5] = { 'i', 's', 't', 'a', 'r' }; +static const symbol s_1_126[4] = { 'e', 'k', 'a', 't' }; +static const symbol s_1_127[4] = { 'e', 'n', 'a', 't' }; +static const symbol s_1_128[4] = { 'o', 's', 'c', 'u' }; +static const symbol s_1_129[6] = { 'o', 0xC5, 0xA1, 0xC4, 0x87, 'u' }; +static const struct among a_1[130] = { +{ 4, s_1_0, 0, 73, 0}, +{ 5, s_1_1, 0, 12, 0}, +{ 5, s_1_2, 0, 14, 0}, +{ 5, s_1_3, 0, 13, 0}, +{ 5, s_1_4, 0, 85, 0}, +{ 5, s_1_5, 0, 15, 0}, +{ 5, s_1_6, 0, 82, 0}, +{ 5, s_1_7, 0, 83, 0}, +{ 5, s_1_8, 0, 84, 0}, +{ 4, s_1_9, 0, 75, 0}, +{ 4, s_1_10, 0, 76, 0}, +{ 4, s_1_11, 0, 81, 0}, +{ 4, s_1_12, 0, 80, 0}, +{ 4, s_1_13, 0, 79, 0}, +{ 5, s_1_14, 0, 18, 0}, +{ 4, s_1_15, 0, 82, 0}, +{ 5, s_1_16, 0, 55, 0}, +{ 5, s_1_17, 0, 16, 0}, +{ 5, s_1_18, 0, 17, 0}, +{ 5, s_1_19, 0, 78, 0}, +{ 5, s_1_20, 0, 58, 0}, +{ 5, s_1_21, 0, 59, 0}, +{ 5, s_1_22, 0, 60, 0}, +{ 5, s_1_23, 0, 61, 0}, +{ 5, s_1_24, 0, 62, 0}, +{ 6, s_1_25, 0, 54, 0}, +{ 5, s_1_26, 0, 67, 0}, +{ 5, s_1_27, 0, 87, 0}, +{ 6, s_1_28, 0, 5, 0}, +{ 6, s_1_29, 0, 23, 0}, +{ 6, s_1_30, 0, 24, 0}, +{ 8, s_1_31, -1, 21, 0}, +{ 6, s_1_32, 0, 25, 0}, +{ 6, s_1_33, 0, 58, 0}, +{ 6, s_1_34, 0, 62, 0}, +{ 6, s_1_35, 0, 74, 0}, +{ 6, s_1_36, 0, 2, 0}, +{ 6, s_1_37, 0, 19, 0}, +{ 6, s_1_38, 0, 1, 0}, +{ 6, s_1_39, 0, 68, 0}, +{ 7, s_1_40, 0, 69, 0}, +{ 6, s_1_41, 0, 70, 0}, +{ 5, s_1_42, 0, 86, 0}, +{ 5, s_1_43, 0, 53, 0}, +{ 6, s_1_44, 0, 22, 0}, +{ 7, s_1_45, 0, 29, 0}, +{ 4, s_1_46, 0, 12, 0}, +{ 4, s_1_47, 0, 14, 0}, +{ 4, s_1_48, 0, 13, 0}, +{ 4, s_1_49, 0, 85, 0}, +{ 5, s_1_50, -1, 11, 0}, +{ 4, s_1_51, 0, 15, 0}, +{ 4, s_1_52, 0, 82, 0}, +{ 4, s_1_53, 0, 83, 0}, +{ 4, s_1_54, 0, 84, 0}, +{ 3, s_1_55, 0, 75, 0}, +{ 3, s_1_56, 0, 76, 0}, +{ 3, s_1_57, 0, 81, 0}, +{ 3, s_1_58, 0, 80, 0}, +{ 3, s_1_59, 0, 79, 0}, +{ 4, s_1_60, 0, 18, 0}, +{ 4, s_1_61, 0, 88, 0}, +{ 4, s_1_62, 0, 84, 0}, +{ 4, s_1_63, 0, 27, 0}, +{ 4, s_1_64, 0, 42, 0}, +{ 4, s_1_65, 0, 52, 0}, +{ 5, s_1_66, 0, 51, 0}, +{ 4, s_1_67, 0, 89, 0}, +{ 4, s_1_68, 0, 5, 0}, +{ 7, s_1_69, 0, 20, 0}, +{ 5, s_1_70, 0, 26, 0}, +{ 6, s_1_71, 0, 21, 0}, +{ 5, s_1_72, 0, 4, 0}, +{ 5, s_1_73, 0, 3, 0}, +{ 5, s_1_74, 0, 66, 0}, +{ 4, s_1_75, 0, 58, 0}, +{ 4, s_1_76, 0, 60, 0}, +{ 4, s_1_77, 0, 62, 0}, +{ 4, s_1_78, 0, 74, 0}, +{ 4, s_1_79, 0, 2, 0}, +{ 4, s_1_80, 0, 19, 0}, +{ 4, s_1_81, 0, 1, 0}, +{ 4, s_1_82, 0, 55, 0}, +{ 4, s_1_83, 0, 57, 0}, +{ 4, s_1_84, 0, 58, 0}, +{ 4, s_1_85, 0, 59, 0}, +{ 4, s_1_86, 0, 60, 0}, +{ 4, s_1_87, 0, 61, 0}, +{ 4, s_1_88, 0, 62, 0}, +{ 5, s_1_89, 0, 54, 0}, +{ 5, s_1_90, 0, 56, 0}, +{ 4, s_1_91, 0, 87, 0}, +{ 5, s_1_92, 0, 65, 0}, +{ 5, s_1_93, 0, 7, 0}, +{ 6, s_1_94, 0, 6, 0}, +{ 6, s_1_95, 0, 77, 0}, +{ 5, s_1_96, 0, 63, 0}, +{ 4, s_1_97, 0, 40, 0}, +{ 5, s_1_98, 0, 33, 0}, +{ 5, s_1_99, 0, 37, 0}, +{ 5, s_1_100, 0, 39, 0}, +{ 5, s_1_101, 0, 38, 0}, +{ 5, s_1_102, 0, 36, 0}, +{ 5, s_1_103, 0, 34, 0}, +{ 6, s_1_104, 0, 35, 0}, +{ 5, s_1_105, 0, 9, 0}, +{ 4, s_1_106, 0, 8, 0}, +{ 4, s_1_107, 0, 91, 0}, +{ 4, s_1_108, 0, 10, 0}, +{ 5, s_1_109, 0, 31, 0}, +{ 5, s_1_110, 0, 28, 0}, +{ 4, s_1_111, 0, 47, 0}, +{ 6, s_1_112, 0, 50, 0}, +{ 5, s_1_113, 0, 49, 0}, +{ 5, s_1_114, 0, 32, 0}, +{ 6, s_1_115, 0, 44, 0}, +{ 6, s_1_116, 0, 43, 0}, +{ 5, s_1_117, 0, 46, 0}, +{ 5, s_1_118, 0, 45, 0}, +{ 4, s_1_119, 0, 41, 0}, +{ 6, s_1_120, 0, 64, 0}, +{ 5, s_1_121, 0, 90, 0}, +{ 6, s_1_122, 0, 30, 0}, +{ 5, s_1_123, 0, 68, 0}, +{ 6, s_1_124, 0, 69, 0}, +{ 5, s_1_125, 0, 70, 0}, +{ 4, s_1_126, 0, 86, 0}, +{ 4, s_1_127, 0, 48, 0}, +{ 4, s_1_128, 0, 72, 0}, +{ 6, s_1_129, 0, 71, 0} +}; + +static const symbol s_2_0[3] = { 'a', 'c', 'a' }; +static const symbol s_2_1[3] = { 'e', 'c', 'a' }; +static const symbol s_2_2[3] = { 'u', 'c', 'a' }; +static const symbol s_2_3[2] = { 'g', 'a' }; +static const symbol s_2_4[5] = { 'a', 'c', 'e', 'g', 'a' }; +static const symbol s_2_5[5] = { 'e', 'c', 'e', 'g', 'a' }; +static const symbol s_2_6[5] = { 'u', 'c', 'e', 'g', 'a' }; +static const symbol s_2_7[8] = { 'a', 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_8[8] = { 'e', 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_9[8] = { 's', 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_10[9] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_11[6] = { 'k', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_12[7] = { 's', 'k', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_13[8] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_14[7] = { 'e', 'l', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_15[6] = { 'n', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_16[7] = { 'o', 's', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_17[7] = { 'a', 't', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_18[9] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_19[9] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_20[8] = { 'a', 's', 't', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_21[7] = { 'a', 'v', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_22[7] = { 'e', 'v', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_23[7] = { 'i', 'v', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_24[7] = { 'o', 'v', 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_25[8] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'g', 'a' }; +static const symbol s_2_26[6] = { 'a', 'n', 'j', 'e', 'g', 'a' }; +static const symbol s_2_27[6] = { 'e', 'n', 'j', 'e', 'g', 'a' }; +static const symbol s_2_28[6] = { 's', 'n', 'j', 'e', 'g', 'a' }; +static const symbol s_2_29[7] = { 0xC5, 0xA1, 'n', 'j', 'e', 'g', 'a' }; +static const symbol s_2_30[4] = { 'k', 'e', 'g', 'a' }; +static const symbol s_2_31[5] = { 's', 'k', 'e', 'g', 'a' }; +static const symbol s_2_32[6] = { 0xC5, 0xA1, 'k', 'e', 'g', 'a' }; +static const symbol s_2_33[5] = { 'e', 'l', 'e', 'g', 'a' }; +static const symbol s_2_34[4] = { 'n', 'e', 'g', 'a' }; +static const symbol s_2_35[5] = { 'a', 'n', 'e', 'g', 'a' }; +static const symbol s_2_36[5] = { 'e', 'n', 'e', 'g', 'a' }; +static const symbol s_2_37[5] = { 's', 'n', 'e', 'g', 'a' }; +static const symbol s_2_38[6] = { 0xC5, 0xA1, 'n', 'e', 'g', 'a' }; +static const symbol s_2_39[5] = { 'o', 's', 'e', 'g', 'a' }; +static const symbol s_2_40[5] = { 'a', 't', 'e', 'g', 'a' }; +static const symbol s_2_41[7] = { 'e', 'v', 'i', 't', 'e', 'g', 'a' }; +static const symbol s_2_42[7] = { 'o', 'v', 'i', 't', 'e', 'g', 'a' }; +static const symbol s_2_43[6] = { 'a', 's', 't', 'e', 'g', 'a' }; +static const symbol s_2_44[5] = { 'a', 'v', 'e', 'g', 'a' }; +static const symbol s_2_45[5] = { 'e', 'v', 'e', 'g', 'a' }; +static const symbol s_2_46[5] = { 'i', 'v', 'e', 'g', 'a' }; +static const symbol s_2_47[5] = { 'o', 'v', 'e', 'g', 'a' }; +static const symbol s_2_48[6] = { 'a', 0xC4, 0x87, 'e', 'g', 'a' }; +static const symbol s_2_49[6] = { 'e', 0xC4, 0x87, 'e', 'g', 'a' }; +static const symbol s_2_50[6] = { 'u', 0xC4, 0x87, 'e', 'g', 'a' }; +static const symbol s_2_51[6] = { 'o', 0xC5, 0xA1, 'e', 'g', 'a' }; +static const symbol s_2_52[5] = { 'a', 'c', 'o', 'g', 'a' }; +static const symbol s_2_53[5] = { 'e', 'c', 'o', 'g', 'a' }; +static const symbol s_2_54[5] = { 'u', 'c', 'o', 'g', 'a' }; +static const symbol s_2_55[6] = { 'a', 'n', 'j', 'o', 'g', 'a' }; +static const symbol s_2_56[6] = { 'e', 'n', 'j', 'o', 'g', 'a' }; +static const symbol s_2_57[6] = { 's', 'n', 'j', 'o', 'g', 'a' }; +static const symbol s_2_58[7] = { 0xC5, 0xA1, 'n', 'j', 'o', 'g', 'a' }; +static const symbol s_2_59[4] = { 'k', 'o', 'g', 'a' }; +static const symbol s_2_60[5] = { 's', 'k', 'o', 'g', 'a' }; +static const symbol s_2_61[6] = { 0xC5, 0xA1, 'k', 'o', 'g', 'a' }; +static const symbol s_2_62[4] = { 'l', 'o', 'g', 'a' }; +static const symbol s_2_63[5] = { 'e', 'l', 'o', 'g', 'a' }; +static const symbol s_2_64[4] = { 'n', 'o', 'g', 'a' }; +static const symbol s_2_65[6] = { 'c', 'i', 'n', 'o', 'g', 'a' }; +static const symbol s_2_66[7] = { 0xC4, 0x8D, 'i', 'n', 'o', 'g', 'a' }; +static const symbol s_2_67[5] = { 'o', 's', 'o', 'g', 'a' }; +static const symbol s_2_68[5] = { 'a', 't', 'o', 'g', 'a' }; +static const symbol s_2_69[7] = { 'e', 'v', 'i', 't', 'o', 'g', 'a' }; +static const symbol s_2_70[7] = { 'o', 'v', 'i', 't', 'o', 'g', 'a' }; +static const symbol s_2_71[6] = { 'a', 's', 't', 'o', 'g', 'a' }; +static const symbol s_2_72[5] = { 'a', 'v', 'o', 'g', 'a' }; +static const symbol s_2_73[5] = { 'e', 'v', 'o', 'g', 'a' }; +static const symbol s_2_74[5] = { 'i', 'v', 'o', 'g', 'a' }; +static const symbol s_2_75[5] = { 'o', 'v', 'o', 'g', 'a' }; +static const symbol s_2_76[6] = { 'a', 0xC4, 0x87, 'o', 'g', 'a' }; +static const symbol s_2_77[6] = { 'e', 0xC4, 0x87, 'o', 'g', 'a' }; +static const symbol s_2_78[6] = { 'u', 0xC4, 0x87, 'o', 'g', 'a' }; +static const symbol s_2_79[6] = { 'o', 0xC5, 0xA1, 'o', 'g', 'a' }; +static const symbol s_2_80[3] = { 'u', 'g', 'a' }; +static const symbol s_2_81[3] = { 'a', 'j', 'a' }; +static const symbol s_2_82[4] = { 'c', 'a', 'j', 'a' }; +static const symbol s_2_83[4] = { 'l', 'a', 'j', 'a' }; +static const symbol s_2_84[4] = { 'r', 'a', 'j', 'a' }; +static const symbol s_2_85[5] = { 0xC4, 0x87, 'a', 'j', 'a' }; +static const symbol s_2_86[5] = { 0xC4, 0x8D, 'a', 'j', 'a' }; +static const symbol s_2_87[5] = { 0xC4, 0x91, 'a', 'j', 'a' }; +static const symbol s_2_88[4] = { 'b', 'i', 'j', 'a' }; +static const symbol s_2_89[4] = { 'c', 'i', 'j', 'a' }; +static const symbol s_2_90[4] = { 'd', 'i', 'j', 'a' }; +static const symbol s_2_91[4] = { 'f', 'i', 'j', 'a' }; +static const symbol s_2_92[4] = { 'g', 'i', 'j', 'a' }; +static const symbol s_2_93[6] = { 'a', 'n', 'j', 'i', 'j', 'a' }; +static const symbol s_2_94[6] = { 'e', 'n', 'j', 'i', 'j', 'a' }; +static const symbol s_2_95[6] = { 's', 'n', 'j', 'i', 'j', 'a' }; +static const symbol s_2_96[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'a' }; +static const symbol s_2_97[4] = { 'k', 'i', 'j', 'a' }; +static const symbol s_2_98[5] = { 's', 'k', 'i', 'j', 'a' }; +static const symbol s_2_99[6] = { 0xC5, 0xA1, 'k', 'i', 'j', 'a' }; +static const symbol s_2_100[4] = { 'l', 'i', 'j', 'a' }; +static const symbol s_2_101[5] = { 'e', 'l', 'i', 'j', 'a' }; +static const symbol s_2_102[4] = { 'm', 'i', 'j', 'a' }; +static const symbol s_2_103[4] = { 'n', 'i', 'j', 'a' }; +static const symbol s_2_104[6] = { 'g', 'a', 'n', 'i', 'j', 'a' }; +static const symbol s_2_105[6] = { 'm', 'a', 'n', 'i', 'j', 'a' }; +static const symbol s_2_106[6] = { 'p', 'a', 'n', 'i', 'j', 'a' }; +static const symbol s_2_107[6] = { 'r', 'a', 'n', 'i', 'j', 'a' }; +static const symbol s_2_108[6] = { 't', 'a', 'n', 'i', 'j', 'a' }; +static const symbol s_2_109[4] = { 'p', 'i', 'j', 'a' }; +static const symbol s_2_110[4] = { 'r', 'i', 'j', 'a' }; +static const symbol s_2_111[6] = { 'r', 'a', 'r', 'i', 'j', 'a' }; +static const symbol s_2_112[4] = { 's', 'i', 'j', 'a' }; +static const symbol s_2_113[5] = { 'o', 's', 'i', 'j', 'a' }; +static const symbol s_2_114[4] = { 't', 'i', 'j', 'a' }; +static const symbol s_2_115[5] = { 'a', 't', 'i', 'j', 'a' }; +static const symbol s_2_116[7] = { 'e', 'v', 'i', 't', 'i', 'j', 'a' }; +static const symbol s_2_117[7] = { 'o', 'v', 'i', 't', 'i', 'j', 'a' }; +static const symbol s_2_118[5] = { 'o', 't', 'i', 'j', 'a' }; +static const symbol s_2_119[6] = { 'a', 's', 't', 'i', 'j', 'a' }; +static const symbol s_2_120[5] = { 'a', 'v', 'i', 'j', 'a' }; +static const symbol s_2_121[5] = { 'e', 'v', 'i', 'j', 'a' }; +static const symbol s_2_122[5] = { 'i', 'v', 'i', 'j', 'a' }; +static const symbol s_2_123[5] = { 'o', 'v', 'i', 'j', 'a' }; +static const symbol s_2_124[4] = { 'z', 'i', 'j', 'a' }; +static const symbol s_2_125[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'a' }; +static const symbol s_2_126[5] = { 0xC5, 0xBE, 'i', 'j', 'a' }; +static const symbol s_2_127[4] = { 'a', 'n', 'j', 'a' }; +static const symbol s_2_128[4] = { 'e', 'n', 'j', 'a' }; +static const symbol s_2_129[4] = { 's', 'n', 'j', 'a' }; +static const symbol s_2_130[5] = { 0xC5, 0xA1, 'n', 'j', 'a' }; +static const symbol s_2_131[2] = { 'k', 'a' }; +static const symbol s_2_132[3] = { 's', 'k', 'a' }; +static const symbol s_2_133[4] = { 0xC5, 0xA1, 'k', 'a' }; +static const symbol s_2_134[3] = { 'a', 'l', 'a' }; +static const symbol s_2_135[5] = { 'a', 'c', 'a', 'l', 'a' }; +static const symbol s_2_136[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'a' }; +static const symbol s_2_137[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'a' }; +static const symbol s_2_138[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'a' }; +static const symbol s_2_139[5] = { 'i', 'j', 'a', 'l', 'a' }; +static const symbol s_2_140[6] = { 'i', 'n', 'j', 'a', 'l', 'a' }; +static const symbol s_2_141[4] = { 'n', 'a', 'l', 'a' }; +static const symbol s_2_142[5] = { 'i', 'r', 'a', 'l', 'a' }; +static const symbol s_2_143[5] = { 'u', 'r', 'a', 'l', 'a' }; +static const symbol s_2_144[4] = { 't', 'a', 'l', 'a' }; +static const symbol s_2_145[6] = { 'a', 's', 't', 'a', 'l', 'a' }; +static const symbol s_2_146[6] = { 'i', 's', 't', 'a', 'l', 'a' }; +static const symbol s_2_147[6] = { 'o', 's', 't', 'a', 'l', 'a' }; +static const symbol s_2_148[5] = { 'a', 'v', 'a', 'l', 'a' }; +static const symbol s_2_149[5] = { 'e', 'v', 'a', 'l', 'a' }; +static const symbol s_2_150[5] = { 'i', 'v', 'a', 'l', 'a' }; +static const symbol s_2_151[5] = { 'o', 'v', 'a', 'l', 'a' }; +static const symbol s_2_152[5] = { 'u', 'v', 'a', 'l', 'a' }; +static const symbol s_2_153[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'a' }; +static const symbol s_2_154[3] = { 'e', 'l', 'a' }; +static const symbol s_2_155[3] = { 'i', 'l', 'a' }; +static const symbol s_2_156[5] = { 'a', 'c', 'i', 'l', 'a' }; +static const symbol s_2_157[6] = { 'l', 'u', 'c', 'i', 'l', 'a' }; +static const symbol s_2_158[4] = { 'n', 'i', 'l', 'a' }; +static const symbol s_2_159[8] = { 'a', 's', 't', 'a', 'n', 'i', 'l', 'a' }; +static const symbol s_2_160[8] = { 'i', 's', 't', 'a', 'n', 'i', 'l', 'a' }; +static const symbol s_2_161[8] = { 'o', 's', 't', 'a', 'n', 'i', 'l', 'a' }; +static const symbol s_2_162[6] = { 'r', 'o', 's', 'i', 'l', 'a' }; +static const symbol s_2_163[6] = { 'j', 'e', 't', 'i', 'l', 'a' }; +static const symbol s_2_164[5] = { 'o', 'z', 'i', 'l', 'a' }; +static const symbol s_2_165[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'a' }; +static const symbol s_2_166[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'a' }; +static const symbol s_2_167[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'a' }; +static const symbol s_2_168[3] = { 'o', 'l', 'a' }; +static const symbol s_2_169[4] = { 'a', 's', 'l', 'a' }; +static const symbol s_2_170[4] = { 'n', 'u', 'l', 'a' }; +static const symbol s_2_171[4] = { 'g', 'a', 'm', 'a' }; +static const symbol s_2_172[6] = { 'l', 'o', 'g', 'a', 'm', 'a' }; +static const symbol s_2_173[5] = { 'u', 'g', 'a', 'm', 'a' }; +static const symbol s_2_174[5] = { 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_175[6] = { 'c', 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_176[6] = { 'l', 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_177[6] = { 'r', 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_178[7] = { 0xC4, 0x87, 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_179[7] = { 0xC4, 0x8D, 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_180[7] = { 0xC4, 0x91, 'a', 'j', 'a', 'm', 'a' }; +static const symbol s_2_181[6] = { 'b', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_182[6] = { 'c', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_183[6] = { 'd', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_184[6] = { 'f', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_185[6] = { 'g', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_186[6] = { 'l', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_187[6] = { 'm', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_188[6] = { 'n', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_189[8] = { 'g', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_190[8] = { 'm', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_191[8] = { 'p', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_192[8] = { 'r', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_193[8] = { 't', 'a', 'n', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_194[6] = { 'p', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_195[6] = { 'r', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_196[6] = { 's', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_197[6] = { 't', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_198[6] = { 'z', 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_199[7] = { 0xC5, 0xBE, 'i', 'j', 'a', 'm', 'a' }; +static const symbol s_2_200[5] = { 'a', 'l', 'a', 'm', 'a' }; +static const symbol s_2_201[7] = { 'i', 'j', 'a', 'l', 'a', 'm', 'a' }; +static const symbol s_2_202[6] = { 'n', 'a', 'l', 'a', 'm', 'a' }; +static const symbol s_2_203[5] = { 'e', 'l', 'a', 'm', 'a' }; +static const symbol s_2_204[5] = { 'i', 'l', 'a', 'm', 'a' }; +static const symbol s_2_205[6] = { 'r', 'a', 'm', 'a', 'm', 'a' }; +static const symbol s_2_206[6] = { 'l', 'e', 'm', 'a', 'm', 'a' }; +static const symbol s_2_207[5] = { 'i', 'n', 'a', 'm', 'a' }; +static const symbol s_2_208[6] = { 'c', 'i', 'n', 'a', 'm', 'a' }; +static const symbol s_2_209[7] = { 0xC4, 0x8D, 'i', 'n', 'a', 'm', 'a' }; +static const symbol s_2_210[4] = { 'r', 'a', 'm', 'a' }; +static const symbol s_2_211[5] = { 'a', 'r', 'a', 'm', 'a' }; +static const symbol s_2_212[5] = { 'd', 'r', 'a', 'm', 'a' }; +static const symbol s_2_213[5] = { 'e', 'r', 'a', 'm', 'a' }; +static const symbol s_2_214[5] = { 'o', 'r', 'a', 'm', 'a' }; +static const symbol s_2_215[6] = { 'b', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_216[6] = { 'g', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_217[6] = { 'j', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_218[6] = { 'k', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_219[6] = { 'n', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_220[6] = { 't', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_221[6] = { 'v', 'a', 's', 'a', 'm', 'a' }; +static const symbol s_2_222[5] = { 'e', 's', 'a', 'm', 'a' }; +static const symbol s_2_223[5] = { 'i', 's', 'a', 'm', 'a' }; +static const symbol s_2_224[5] = { 'e', 't', 'a', 'm', 'a' }; +static const symbol s_2_225[6] = { 'e', 's', 't', 'a', 'm', 'a' }; +static const symbol s_2_226[6] = { 'i', 's', 't', 'a', 'm', 'a' }; +static const symbol s_2_227[6] = { 'k', 's', 't', 'a', 'm', 'a' }; +static const symbol s_2_228[6] = { 'o', 's', 't', 'a', 'm', 'a' }; +static const symbol s_2_229[5] = { 'a', 'v', 'a', 'm', 'a' }; +static const symbol s_2_230[5] = { 'e', 'v', 'a', 'm', 'a' }; +static const symbol s_2_231[5] = { 'i', 'v', 'a', 'm', 'a' }; +static const symbol s_2_232[7] = { 'b', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_233[7] = { 'g', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_234[7] = { 'j', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_235[7] = { 'k', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_236[7] = { 'n', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_237[7] = { 't', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_238[7] = { 'v', 'a', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_239[6] = { 'e', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_240[6] = { 'i', 0xC5, 0xA1, 'a', 'm', 'a' }; +static const symbol s_2_241[4] = { 'l', 'e', 'm', 'a' }; +static const symbol s_2_242[5] = { 'a', 'c', 'i', 'm', 'a' }; +static const symbol s_2_243[5] = { 'e', 'c', 'i', 'm', 'a' }; +static const symbol s_2_244[5] = { 'u', 'c', 'i', 'm', 'a' }; +static const symbol s_2_245[5] = { 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_246[6] = { 'c', 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_247[6] = { 'l', 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_248[6] = { 'r', 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_249[7] = { 0xC4, 0x87, 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_250[7] = { 0xC4, 0x8D, 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_251[7] = { 0xC4, 0x91, 'a', 'j', 'i', 'm', 'a' }; +static const symbol s_2_252[6] = { 'b', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_253[6] = { 'c', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_254[6] = { 'd', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_255[6] = { 'f', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_256[6] = { 'g', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_257[8] = { 'a', 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_258[8] = { 'e', 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_259[8] = { 's', 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_260[9] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_261[6] = { 'k', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_262[7] = { 's', 'k', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_263[8] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_264[6] = { 'l', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_265[7] = { 'e', 'l', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_266[6] = { 'm', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_267[6] = { 'n', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_268[8] = { 'g', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_269[8] = { 'm', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_270[8] = { 'p', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_271[8] = { 'r', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_272[8] = { 't', 'a', 'n', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_273[6] = { 'p', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_274[6] = { 'r', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_275[6] = { 's', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_276[7] = { 'o', 's', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_277[6] = { 't', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_278[7] = { 'a', 't', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_279[9] = { 'e', 'v', 'i', 't', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_280[9] = { 'o', 'v', 'i', 't', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_281[8] = { 'a', 's', 't', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_282[7] = { 'a', 'v', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_283[7] = { 'e', 'v', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_284[7] = { 'i', 'v', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_285[7] = { 'o', 'v', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_286[6] = { 'z', 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_287[8] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_288[7] = { 0xC5, 0xBE, 'i', 'j', 'i', 'm', 'a' }; +static const symbol s_2_289[6] = { 'a', 'n', 'j', 'i', 'm', 'a' }; +static const symbol s_2_290[6] = { 'e', 'n', 'j', 'i', 'm', 'a' }; +static const symbol s_2_291[6] = { 's', 'n', 'j', 'i', 'm', 'a' }; +static const symbol s_2_292[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'm', 'a' }; +static const symbol s_2_293[4] = { 'k', 'i', 'm', 'a' }; +static const symbol s_2_294[5] = { 's', 'k', 'i', 'm', 'a' }; +static const symbol s_2_295[6] = { 0xC5, 0xA1, 'k', 'i', 'm', 'a' }; +static const symbol s_2_296[5] = { 'a', 'l', 'i', 'm', 'a' }; +static const symbol s_2_297[7] = { 'i', 'j', 'a', 'l', 'i', 'm', 'a' }; +static const symbol s_2_298[6] = { 'n', 'a', 'l', 'i', 'm', 'a' }; +static const symbol s_2_299[5] = { 'e', 'l', 'i', 'm', 'a' }; +static const symbol s_2_300[5] = { 'i', 'l', 'i', 'm', 'a' }; +static const symbol s_2_301[7] = { 'o', 'z', 'i', 'l', 'i', 'm', 'a' }; +static const symbol s_2_302[5] = { 'o', 'l', 'i', 'm', 'a' }; +static const symbol s_2_303[6] = { 'l', 'e', 'm', 'i', 'm', 'a' }; +static const symbol s_2_304[4] = { 'n', 'i', 'm', 'a' }; +static const symbol s_2_305[5] = { 'a', 'n', 'i', 'm', 'a' }; +static const symbol s_2_306[5] = { 'i', 'n', 'i', 'm', 'a' }; +static const symbol s_2_307[6] = { 'c', 'i', 'n', 'i', 'm', 'a' }; +static const symbol s_2_308[7] = { 0xC4, 0x8D, 'i', 'n', 'i', 'm', 'a' }; +static const symbol s_2_309[5] = { 'o', 'n', 'i', 'm', 'a' }; +static const symbol s_2_310[5] = { 'a', 'r', 'i', 'm', 'a' }; +static const symbol s_2_311[5] = { 'd', 'r', 'i', 'm', 'a' }; +static const symbol s_2_312[5] = { 'e', 'r', 'i', 'm', 'a' }; +static const symbol s_2_313[5] = { 'o', 'r', 'i', 'm', 'a' }; +static const symbol s_2_314[6] = { 'b', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_315[6] = { 'g', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_316[6] = { 'j', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_317[6] = { 'k', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_318[6] = { 'n', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_319[6] = { 't', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_320[6] = { 'v', 'a', 's', 'i', 'm', 'a' }; +static const symbol s_2_321[5] = { 'e', 's', 'i', 'm', 'a' }; +static const symbol s_2_322[5] = { 'i', 's', 'i', 'm', 'a' }; +static const symbol s_2_323[5] = { 'o', 's', 'i', 'm', 'a' }; +static const symbol s_2_324[5] = { 'a', 't', 'i', 'm', 'a' }; +static const symbol s_2_325[7] = { 'i', 'k', 'a', 't', 'i', 'm', 'a' }; +static const symbol s_2_326[6] = { 'l', 'a', 't', 'i', 'm', 'a' }; +static const symbol s_2_327[5] = { 'e', 't', 'i', 'm', 'a' }; +static const symbol s_2_328[7] = { 'e', 'v', 'i', 't', 'i', 'm', 'a' }; +static const symbol s_2_329[7] = { 'o', 'v', 'i', 't', 'i', 'm', 'a' }; +static const symbol s_2_330[6] = { 'a', 's', 't', 'i', 'm', 'a' }; +static const symbol s_2_331[6] = { 'e', 's', 't', 'i', 'm', 'a' }; +static const symbol s_2_332[6] = { 'i', 's', 't', 'i', 'm', 'a' }; +static const symbol s_2_333[6] = { 'k', 's', 't', 'i', 'm', 'a' }; +static const symbol s_2_334[6] = { 'o', 's', 't', 'i', 'm', 'a' }; +static const symbol s_2_335[7] = { 'i', 0xC5, 0xA1, 't', 'i', 'm', 'a' }; +static const symbol s_2_336[5] = { 'a', 'v', 'i', 'm', 'a' }; +static const symbol s_2_337[5] = { 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_338[7] = { 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_339[8] = { 'c', 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_340[8] = { 'l', 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_341[8] = { 'r', 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_342[9] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_343[9] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_344[9] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'i', 'm', 'a' }; +static const symbol s_2_345[5] = { 'i', 'v', 'i', 'm', 'a' }; +static const symbol s_2_346[5] = { 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_347[6] = { 'g', 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_348[7] = { 'u', 'g', 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_349[6] = { 'l', 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_350[7] = { 'o', 'l', 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_351[6] = { 'm', 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_352[7] = { 'o', 'n', 'o', 'v', 'i', 'm', 'a' }; +static const symbol s_2_353[6] = { 's', 't', 'v', 'i', 'm', 'a' }; +static const symbol s_2_354[7] = { 0xC5, 0xA1, 't', 'v', 'i', 'm', 'a' }; +static const symbol s_2_355[6] = { 'a', 0xC4, 0x87, 'i', 'm', 'a' }; +static const symbol s_2_356[6] = { 'e', 0xC4, 0x87, 'i', 'm', 'a' }; +static const symbol s_2_357[6] = { 'u', 0xC4, 0x87, 'i', 'm', 'a' }; +static const symbol s_2_358[7] = { 'b', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_359[7] = { 'g', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_360[7] = { 'j', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_361[7] = { 'k', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_362[7] = { 'n', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_363[7] = { 't', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_364[7] = { 'v', 'a', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_365[6] = { 'e', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_366[6] = { 'i', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_367[6] = { 'o', 0xC5, 0xA1, 'i', 'm', 'a' }; +static const symbol s_2_368[2] = { 'n', 'a' }; +static const symbol s_2_369[3] = { 'a', 'n', 'a' }; +static const symbol s_2_370[5] = { 'a', 'c', 'a', 'n', 'a' }; +static const symbol s_2_371[5] = { 'u', 'r', 'a', 'n', 'a' }; +static const symbol s_2_372[4] = { 't', 'a', 'n', 'a' }; +static const symbol s_2_373[5] = { 'a', 'v', 'a', 'n', 'a' }; +static const symbol s_2_374[5] = { 'e', 'v', 'a', 'n', 'a' }; +static const symbol s_2_375[5] = { 'i', 'v', 'a', 'n', 'a' }; +static const symbol s_2_376[5] = { 'u', 'v', 'a', 'n', 'a' }; +static const symbol s_2_377[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'a' }; +static const symbol s_2_378[5] = { 'a', 'c', 'e', 'n', 'a' }; +static const symbol s_2_379[6] = { 'l', 'u', 'c', 'e', 'n', 'a' }; +static const symbol s_2_380[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'a' }; +static const symbol s_2_381[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'a' }; +static const symbol s_2_382[3] = { 'i', 'n', 'a' }; +static const symbol s_2_383[4] = { 'c', 'i', 'n', 'a' }; +static const symbol s_2_384[5] = { 'a', 'n', 'i', 'n', 'a' }; +static const symbol s_2_385[5] = { 0xC4, 0x8D, 'i', 'n', 'a' }; +static const symbol s_2_386[3] = { 'o', 'n', 'a' }; +static const symbol s_2_387[3] = { 'a', 'r', 'a' }; +static const symbol s_2_388[3] = { 'd', 'r', 'a' }; +static const symbol s_2_389[3] = { 'e', 'r', 'a' }; +static const symbol s_2_390[3] = { 'o', 'r', 'a' }; +static const symbol s_2_391[4] = { 'b', 'a', 's', 'a' }; +static const symbol s_2_392[4] = { 'g', 'a', 's', 'a' }; +static const symbol s_2_393[4] = { 'j', 'a', 's', 'a' }; +static const symbol s_2_394[4] = { 'k', 'a', 's', 'a' }; +static const symbol s_2_395[4] = { 'n', 'a', 's', 'a' }; +static const symbol s_2_396[4] = { 't', 'a', 's', 'a' }; +static const symbol s_2_397[4] = { 'v', 'a', 's', 'a' }; +static const symbol s_2_398[3] = { 'e', 's', 'a' }; +static const symbol s_2_399[3] = { 'i', 's', 'a' }; +static const symbol s_2_400[3] = { 'o', 's', 'a' }; +static const symbol s_2_401[3] = { 'a', 't', 'a' }; +static const symbol s_2_402[5] = { 'i', 'k', 'a', 't', 'a' }; +static const symbol s_2_403[4] = { 'l', 'a', 't', 'a' }; +static const symbol s_2_404[3] = { 'e', 't', 'a' }; +static const symbol s_2_405[5] = { 'e', 'v', 'i', 't', 'a' }; +static const symbol s_2_406[5] = { 'o', 'v', 'i', 't', 'a' }; +static const symbol s_2_407[4] = { 'a', 's', 't', 'a' }; +static const symbol s_2_408[4] = { 'e', 's', 't', 'a' }; +static const symbol s_2_409[4] = { 'i', 's', 't', 'a' }; +static const symbol s_2_410[4] = { 'k', 's', 't', 'a' }; +static const symbol s_2_411[4] = { 'o', 's', 't', 'a' }; +static const symbol s_2_412[4] = { 'n', 'u', 't', 'a' }; +static const symbol s_2_413[5] = { 'i', 0xC5, 0xA1, 't', 'a' }; +static const symbol s_2_414[3] = { 'a', 'v', 'a' }; +static const symbol s_2_415[3] = { 'e', 'v', 'a' }; +static const symbol s_2_416[5] = { 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_417[6] = { 'c', 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_418[6] = { 'l', 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_419[6] = { 'r', 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_420[7] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_421[7] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_422[7] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'a' }; +static const symbol s_2_423[3] = { 'i', 'v', 'a' }; +static const symbol s_2_424[3] = { 'o', 'v', 'a' }; +static const symbol s_2_425[4] = { 'g', 'o', 'v', 'a' }; +static const symbol s_2_426[5] = { 'u', 'g', 'o', 'v', 'a' }; +static const symbol s_2_427[4] = { 'l', 'o', 'v', 'a' }; +static const symbol s_2_428[5] = { 'o', 'l', 'o', 'v', 'a' }; +static const symbol s_2_429[4] = { 'm', 'o', 'v', 'a' }; +static const symbol s_2_430[5] = { 'o', 'n', 'o', 'v', 'a' }; +static const symbol s_2_431[4] = { 's', 't', 'v', 'a' }; +static const symbol s_2_432[5] = { 0xC5, 0xA1, 't', 'v', 'a' }; +static const symbol s_2_433[4] = { 'a', 0xC4, 0x87, 'a' }; +static const symbol s_2_434[4] = { 'e', 0xC4, 0x87, 'a' }; +static const symbol s_2_435[4] = { 'u', 0xC4, 0x87, 'a' }; +static const symbol s_2_436[5] = { 'b', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_437[5] = { 'g', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_438[5] = { 'j', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_439[5] = { 'k', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_440[5] = { 'n', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_441[5] = { 't', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_442[5] = { 'v', 'a', 0xC5, 0xA1, 'a' }; +static const symbol s_2_443[4] = { 'e', 0xC5, 0xA1, 'a' }; +static const symbol s_2_444[4] = { 'i', 0xC5, 0xA1, 'a' }; +static const symbol s_2_445[4] = { 'o', 0xC5, 0xA1, 'a' }; +static const symbol s_2_446[3] = { 'a', 'c', 'e' }; +static const symbol s_2_447[3] = { 'e', 'c', 'e' }; +static const symbol s_2_448[3] = { 'u', 'c', 'e' }; +static const symbol s_2_449[4] = { 'l', 'u', 'c', 'e' }; +static const symbol s_2_450[6] = { 'a', 's', 't', 'a', 'd', 'e' }; +static const symbol s_2_451[6] = { 'i', 's', 't', 'a', 'd', 'e' }; +static const symbol s_2_452[6] = { 'o', 's', 't', 'a', 'd', 'e' }; +static const symbol s_2_453[2] = { 'g', 'e' }; +static const symbol s_2_454[4] = { 'l', 'o', 'g', 'e' }; +static const symbol s_2_455[3] = { 'u', 'g', 'e' }; +static const symbol s_2_456[3] = { 'a', 'j', 'e' }; +static const symbol s_2_457[4] = { 'c', 'a', 'j', 'e' }; +static const symbol s_2_458[4] = { 'l', 'a', 'j', 'e' }; +static const symbol s_2_459[4] = { 'r', 'a', 'j', 'e' }; +static const symbol s_2_460[6] = { 'a', 's', 't', 'a', 'j', 'e' }; +static const symbol s_2_461[6] = { 'i', 's', 't', 'a', 'j', 'e' }; +static const symbol s_2_462[6] = { 'o', 's', 't', 'a', 'j', 'e' }; +static const symbol s_2_463[5] = { 0xC4, 0x87, 'a', 'j', 'e' }; +static const symbol s_2_464[5] = { 0xC4, 0x8D, 'a', 'j', 'e' }; +static const symbol s_2_465[5] = { 0xC4, 0x91, 'a', 'j', 'e' }; +static const symbol s_2_466[3] = { 'i', 'j', 'e' }; +static const symbol s_2_467[4] = { 'b', 'i', 'j', 'e' }; +static const symbol s_2_468[4] = { 'c', 'i', 'j', 'e' }; +static const symbol s_2_469[4] = { 'd', 'i', 'j', 'e' }; +static const symbol s_2_470[4] = { 'f', 'i', 'j', 'e' }; +static const symbol s_2_471[4] = { 'g', 'i', 'j', 'e' }; +static const symbol s_2_472[6] = { 'a', 'n', 'j', 'i', 'j', 'e' }; +static const symbol s_2_473[6] = { 'e', 'n', 'j', 'i', 'j', 'e' }; +static const symbol s_2_474[6] = { 's', 'n', 'j', 'i', 'j', 'e' }; +static const symbol s_2_475[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e' }; +static const symbol s_2_476[4] = { 'k', 'i', 'j', 'e' }; +static const symbol s_2_477[5] = { 's', 'k', 'i', 'j', 'e' }; +static const symbol s_2_478[6] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e' }; +static const symbol s_2_479[4] = { 'l', 'i', 'j', 'e' }; +static const symbol s_2_480[5] = { 'e', 'l', 'i', 'j', 'e' }; +static const symbol s_2_481[4] = { 'm', 'i', 'j', 'e' }; +static const symbol s_2_482[4] = { 'n', 'i', 'j', 'e' }; +static const symbol s_2_483[6] = { 'g', 'a', 'n', 'i', 'j', 'e' }; +static const symbol s_2_484[6] = { 'm', 'a', 'n', 'i', 'j', 'e' }; +static const symbol s_2_485[6] = { 'p', 'a', 'n', 'i', 'j', 'e' }; +static const symbol s_2_486[6] = { 'r', 'a', 'n', 'i', 'j', 'e' }; +static const symbol s_2_487[6] = { 't', 'a', 'n', 'i', 'j', 'e' }; +static const symbol s_2_488[4] = { 'p', 'i', 'j', 'e' }; +static const symbol s_2_489[4] = { 'r', 'i', 'j', 'e' }; +static const symbol s_2_490[4] = { 's', 'i', 'j', 'e' }; +static const symbol s_2_491[5] = { 'o', 's', 'i', 'j', 'e' }; +static const symbol s_2_492[4] = { 't', 'i', 'j', 'e' }; +static const symbol s_2_493[5] = { 'a', 't', 'i', 'j', 'e' }; +static const symbol s_2_494[7] = { 'e', 'v', 'i', 't', 'i', 'j', 'e' }; +static const symbol s_2_495[7] = { 'o', 'v', 'i', 't', 'i', 'j', 'e' }; +static const symbol s_2_496[6] = { 'a', 's', 't', 'i', 'j', 'e' }; +static const symbol s_2_497[5] = { 'a', 'v', 'i', 'j', 'e' }; +static const symbol s_2_498[5] = { 'e', 'v', 'i', 'j', 'e' }; +static const symbol s_2_499[5] = { 'i', 'v', 'i', 'j', 'e' }; +static const symbol s_2_500[5] = { 'o', 'v', 'i', 'j', 'e' }; +static const symbol s_2_501[4] = { 'z', 'i', 'j', 'e' }; +static const symbol s_2_502[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e' }; +static const symbol s_2_503[5] = { 0xC5, 0xBE, 'i', 'j', 'e' }; +static const symbol s_2_504[4] = { 'a', 'n', 'j', 'e' }; +static const symbol s_2_505[4] = { 'e', 'n', 'j', 'e' }; +static const symbol s_2_506[4] = { 's', 'n', 'j', 'e' }; +static const symbol s_2_507[5] = { 0xC5, 0xA1, 'n', 'j', 'e' }; +static const symbol s_2_508[3] = { 'u', 'j', 'e' }; +static const symbol s_2_509[6] = { 'l', 'u', 'c', 'u', 'j', 'e' }; +static const symbol s_2_510[5] = { 'i', 'r', 'u', 'j', 'e' }; +static const symbol s_2_511[7] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e' }; +static const symbol s_2_512[2] = { 'k', 'e' }; +static const symbol s_2_513[3] = { 's', 'k', 'e' }; +static const symbol s_2_514[4] = { 0xC5, 0xA1, 'k', 'e' }; +static const symbol s_2_515[3] = { 'a', 'l', 'e' }; +static const symbol s_2_516[5] = { 'a', 'c', 'a', 'l', 'e' }; +static const symbol s_2_517[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'e' }; +static const symbol s_2_518[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'e' }; +static const symbol s_2_519[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'e' }; +static const symbol s_2_520[5] = { 'i', 'j', 'a', 'l', 'e' }; +static const symbol s_2_521[6] = { 'i', 'n', 'j', 'a', 'l', 'e' }; +static const symbol s_2_522[4] = { 'n', 'a', 'l', 'e' }; +static const symbol s_2_523[5] = { 'i', 'r', 'a', 'l', 'e' }; +static const symbol s_2_524[5] = { 'u', 'r', 'a', 'l', 'e' }; +static const symbol s_2_525[4] = { 't', 'a', 'l', 'e' }; +static const symbol s_2_526[6] = { 'a', 's', 't', 'a', 'l', 'e' }; +static const symbol s_2_527[6] = { 'i', 's', 't', 'a', 'l', 'e' }; +static const symbol s_2_528[6] = { 'o', 's', 't', 'a', 'l', 'e' }; +static const symbol s_2_529[5] = { 'a', 'v', 'a', 'l', 'e' }; +static const symbol s_2_530[5] = { 'e', 'v', 'a', 'l', 'e' }; +static const symbol s_2_531[5] = { 'i', 'v', 'a', 'l', 'e' }; +static const symbol s_2_532[5] = { 'o', 'v', 'a', 'l', 'e' }; +static const symbol s_2_533[5] = { 'u', 'v', 'a', 'l', 'e' }; +static const symbol s_2_534[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'e' }; +static const symbol s_2_535[3] = { 'e', 'l', 'e' }; +static const symbol s_2_536[3] = { 'i', 'l', 'e' }; +static const symbol s_2_537[5] = { 'a', 'c', 'i', 'l', 'e' }; +static const symbol s_2_538[6] = { 'l', 'u', 'c', 'i', 'l', 'e' }; +static const symbol s_2_539[4] = { 'n', 'i', 'l', 'e' }; +static const symbol s_2_540[6] = { 'r', 'o', 's', 'i', 'l', 'e' }; +static const symbol s_2_541[6] = { 'j', 'e', 't', 'i', 'l', 'e' }; +static const symbol s_2_542[5] = { 'o', 'z', 'i', 'l', 'e' }; +static const symbol s_2_543[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'e' }; +static const symbol s_2_544[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'e' }; +static const symbol s_2_545[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'e' }; +static const symbol s_2_546[3] = { 'o', 'l', 'e' }; +static const symbol s_2_547[4] = { 'a', 's', 'l', 'e' }; +static const symbol s_2_548[4] = { 'n', 'u', 'l', 'e' }; +static const symbol s_2_549[4] = { 'r', 'a', 'm', 'e' }; +static const symbol s_2_550[4] = { 'l', 'e', 'm', 'e' }; +static const symbol s_2_551[5] = { 'a', 'c', 'o', 'm', 'e' }; +static const symbol s_2_552[5] = { 'e', 'c', 'o', 'm', 'e' }; +static const symbol s_2_553[5] = { 'u', 'c', 'o', 'm', 'e' }; +static const symbol s_2_554[6] = { 'a', 'n', 'j', 'o', 'm', 'e' }; +static const symbol s_2_555[6] = { 'e', 'n', 'j', 'o', 'm', 'e' }; +static const symbol s_2_556[6] = { 's', 'n', 'j', 'o', 'm', 'e' }; +static const symbol s_2_557[7] = { 0xC5, 0xA1, 'n', 'j', 'o', 'm', 'e' }; +static const symbol s_2_558[4] = { 'k', 'o', 'm', 'e' }; +static const symbol s_2_559[5] = { 's', 'k', 'o', 'm', 'e' }; +static const symbol s_2_560[6] = { 0xC5, 0xA1, 'k', 'o', 'm', 'e' }; +static const symbol s_2_561[5] = { 'e', 'l', 'o', 'm', 'e' }; +static const symbol s_2_562[4] = { 'n', 'o', 'm', 'e' }; +static const symbol s_2_563[6] = { 'c', 'i', 'n', 'o', 'm', 'e' }; +static const symbol s_2_564[7] = { 0xC4, 0x8D, 'i', 'n', 'o', 'm', 'e' }; +static const symbol s_2_565[5] = { 'o', 's', 'o', 'm', 'e' }; +static const symbol s_2_566[5] = { 'a', 't', 'o', 'm', 'e' }; +static const symbol s_2_567[7] = { 'e', 'v', 'i', 't', 'o', 'm', 'e' }; +static const symbol s_2_568[7] = { 'o', 'v', 'i', 't', 'o', 'm', 'e' }; +static const symbol s_2_569[6] = { 'a', 's', 't', 'o', 'm', 'e' }; +static const symbol s_2_570[5] = { 'a', 'v', 'o', 'm', 'e' }; +static const symbol s_2_571[5] = { 'e', 'v', 'o', 'm', 'e' }; +static const symbol s_2_572[5] = { 'i', 'v', 'o', 'm', 'e' }; +static const symbol s_2_573[5] = { 'o', 'v', 'o', 'm', 'e' }; +static const symbol s_2_574[6] = { 'a', 0xC4, 0x87, 'o', 'm', 'e' }; +static const symbol s_2_575[6] = { 'e', 0xC4, 0x87, 'o', 'm', 'e' }; +static const symbol s_2_576[6] = { 'u', 0xC4, 0x87, 'o', 'm', 'e' }; +static const symbol s_2_577[6] = { 'o', 0xC5, 0xA1, 'o', 'm', 'e' }; +static const symbol s_2_578[2] = { 'n', 'e' }; +static const symbol s_2_579[3] = { 'a', 'n', 'e' }; +static const symbol s_2_580[5] = { 'a', 'c', 'a', 'n', 'e' }; +static const symbol s_2_581[5] = { 'u', 'r', 'a', 'n', 'e' }; +static const symbol s_2_582[4] = { 't', 'a', 'n', 'e' }; +static const symbol s_2_583[6] = { 'a', 's', 't', 'a', 'n', 'e' }; +static const symbol s_2_584[6] = { 'i', 's', 't', 'a', 'n', 'e' }; +static const symbol s_2_585[6] = { 'o', 's', 't', 'a', 'n', 'e' }; +static const symbol s_2_586[5] = { 'a', 'v', 'a', 'n', 'e' }; +static const symbol s_2_587[5] = { 'e', 'v', 'a', 'n', 'e' }; +static const symbol s_2_588[5] = { 'i', 'v', 'a', 'n', 'e' }; +static const symbol s_2_589[5] = { 'u', 'v', 'a', 'n', 'e' }; +static const symbol s_2_590[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'e' }; +static const symbol s_2_591[5] = { 'a', 'c', 'e', 'n', 'e' }; +static const symbol s_2_592[6] = { 'l', 'u', 'c', 'e', 'n', 'e' }; +static const symbol s_2_593[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'e' }; +static const symbol s_2_594[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'e' }; +static const symbol s_2_595[3] = { 'i', 'n', 'e' }; +static const symbol s_2_596[4] = { 'c', 'i', 'n', 'e' }; +static const symbol s_2_597[5] = { 'a', 'n', 'i', 'n', 'e' }; +static const symbol s_2_598[5] = { 0xC4, 0x8D, 'i', 'n', 'e' }; +static const symbol s_2_599[3] = { 'o', 'n', 'e' }; +static const symbol s_2_600[3] = { 'a', 'r', 'e' }; +static const symbol s_2_601[3] = { 'd', 'r', 'e' }; +static const symbol s_2_602[3] = { 'e', 'r', 'e' }; +static const symbol s_2_603[3] = { 'o', 'r', 'e' }; +static const symbol s_2_604[3] = { 'a', 's', 'e' }; +static const symbol s_2_605[4] = { 'b', 'a', 's', 'e' }; +static const symbol s_2_606[5] = { 'a', 'c', 'a', 's', 'e' }; +static const symbol s_2_607[4] = { 'g', 'a', 's', 'e' }; +static const symbol s_2_608[4] = { 'j', 'a', 's', 'e' }; +static const symbol s_2_609[8] = { 'a', 's', 't', 'a', 'j', 'a', 's', 'e' }; +static const symbol s_2_610[8] = { 'i', 's', 't', 'a', 'j', 'a', 's', 'e' }; +static const symbol s_2_611[8] = { 'o', 's', 't', 'a', 'j', 'a', 's', 'e' }; +static const symbol s_2_612[6] = { 'i', 'n', 'j', 'a', 's', 'e' }; +static const symbol s_2_613[4] = { 'k', 'a', 's', 'e' }; +static const symbol s_2_614[4] = { 'n', 'a', 's', 'e' }; +static const symbol s_2_615[5] = { 'i', 'r', 'a', 's', 'e' }; +static const symbol s_2_616[5] = { 'u', 'r', 'a', 's', 'e' }; +static const symbol s_2_617[4] = { 't', 'a', 's', 'e' }; +static const symbol s_2_618[4] = { 'v', 'a', 's', 'e' }; +static const symbol s_2_619[5] = { 'a', 'v', 'a', 's', 'e' }; +static const symbol s_2_620[5] = { 'e', 'v', 'a', 's', 'e' }; +static const symbol s_2_621[5] = { 'i', 'v', 'a', 's', 'e' }; +static const symbol s_2_622[5] = { 'o', 'v', 'a', 's', 'e' }; +static const symbol s_2_623[5] = { 'u', 'v', 'a', 's', 'e' }; +static const symbol s_2_624[3] = { 'e', 's', 'e' }; +static const symbol s_2_625[3] = { 'i', 's', 'e' }; +static const symbol s_2_626[5] = { 'a', 'c', 'i', 's', 'e' }; +static const symbol s_2_627[6] = { 'l', 'u', 'c', 'i', 's', 'e' }; +static const symbol s_2_628[6] = { 'r', 'o', 's', 'i', 's', 'e' }; +static const symbol s_2_629[6] = { 'j', 'e', 't', 'i', 's', 'e' }; +static const symbol s_2_630[3] = { 'o', 's', 'e' }; +static const symbol s_2_631[8] = { 'a', 's', 't', 'a', 'd', 'o', 's', 'e' }; +static const symbol s_2_632[8] = { 'i', 's', 't', 'a', 'd', 'o', 's', 'e' }; +static const symbol s_2_633[8] = { 'o', 's', 't', 'a', 'd', 'o', 's', 'e' }; +static const symbol s_2_634[3] = { 'a', 't', 'e' }; +static const symbol s_2_635[5] = { 'a', 'c', 'a', 't', 'e' }; +static const symbol s_2_636[5] = { 'i', 'k', 'a', 't', 'e' }; +static const symbol s_2_637[4] = { 'l', 'a', 't', 'e' }; +static const symbol s_2_638[5] = { 'i', 'r', 'a', 't', 'e' }; +static const symbol s_2_639[5] = { 'u', 'r', 'a', 't', 'e' }; +static const symbol s_2_640[4] = { 't', 'a', 't', 'e' }; +static const symbol s_2_641[5] = { 'a', 'v', 'a', 't', 'e' }; +static const symbol s_2_642[5] = { 'e', 'v', 'a', 't', 'e' }; +static const symbol s_2_643[5] = { 'i', 'v', 'a', 't', 'e' }; +static const symbol s_2_644[5] = { 'u', 'v', 'a', 't', 'e' }; +static const symbol s_2_645[6] = { 'a', 0xC4, 0x8D, 'a', 't', 'e' }; +static const symbol s_2_646[3] = { 'e', 't', 'e' }; +static const symbol s_2_647[8] = { 'a', 's', 't', 'a', 'd', 'e', 't', 'e' }; +static const symbol s_2_648[8] = { 'i', 's', 't', 'a', 'd', 'e', 't', 'e' }; +static const symbol s_2_649[8] = { 'o', 's', 't', 'a', 'd', 'e', 't', 'e' }; +static const symbol s_2_650[8] = { 'a', 's', 't', 'a', 'j', 'e', 't', 'e' }; +static const symbol s_2_651[8] = { 'i', 's', 't', 'a', 'j', 'e', 't', 'e' }; +static const symbol s_2_652[8] = { 'o', 's', 't', 'a', 'j', 'e', 't', 'e' }; +static const symbol s_2_653[5] = { 'i', 'j', 'e', 't', 'e' }; +static const symbol s_2_654[6] = { 'i', 'n', 'j', 'e', 't', 'e' }; +static const symbol s_2_655[5] = { 'u', 'j', 'e', 't', 'e' }; +static const symbol s_2_656[8] = { 'l', 'u', 'c', 'u', 'j', 'e', 't', 'e' }; +static const symbol s_2_657[7] = { 'i', 'r', 'u', 'j', 'e', 't', 'e' }; +static const symbol s_2_658[9] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 't', 'e' }; +static const symbol s_2_659[4] = { 'n', 'e', 't', 'e' }; +static const symbol s_2_660[8] = { 'a', 's', 't', 'a', 'n', 'e', 't', 'e' }; +static const symbol s_2_661[8] = { 'i', 's', 't', 'a', 'n', 'e', 't', 'e' }; +static const symbol s_2_662[8] = { 'o', 's', 't', 'a', 'n', 'e', 't', 'e' }; +static const symbol s_2_663[6] = { 'a', 's', 't', 'e', 't', 'e' }; +static const symbol s_2_664[3] = { 'i', 't', 'e' }; +static const symbol s_2_665[5] = { 'a', 'c', 'i', 't', 'e' }; +static const symbol s_2_666[6] = { 'l', 'u', 'c', 'i', 't', 'e' }; +static const symbol s_2_667[4] = { 'n', 'i', 't', 'e' }; +static const symbol s_2_668[8] = { 'a', 's', 't', 'a', 'n', 'i', 't', 'e' }; +static const symbol s_2_669[8] = { 'i', 's', 't', 'a', 'n', 'i', 't', 'e' }; +static const symbol s_2_670[8] = { 'o', 's', 't', 'a', 'n', 'i', 't', 'e' }; +static const symbol s_2_671[6] = { 'r', 'o', 's', 'i', 't', 'e' }; +static const symbol s_2_672[6] = { 'j', 'e', 't', 'i', 't', 'e' }; +static const symbol s_2_673[6] = { 'a', 's', 't', 'i', 't', 'e' }; +static const symbol s_2_674[5] = { 'e', 'v', 'i', 't', 'e' }; +static const symbol s_2_675[5] = { 'o', 'v', 'i', 't', 'e' }; +static const symbol s_2_676[6] = { 'a', 0xC4, 0x8D, 'i', 't', 'e' }; +static const symbol s_2_677[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 't', 'e' }; +static const symbol s_2_678[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 't', 'e' }; +static const symbol s_2_679[4] = { 'a', 'j', 't', 'e' }; +static const symbol s_2_680[6] = { 'u', 'r', 'a', 'j', 't', 'e' }; +static const symbol s_2_681[5] = { 't', 'a', 'j', 't', 'e' }; +static const symbol s_2_682[7] = { 'a', 's', 't', 'a', 'j', 't', 'e' }; +static const symbol s_2_683[7] = { 'i', 's', 't', 'a', 'j', 't', 'e' }; +static const symbol s_2_684[7] = { 'o', 's', 't', 'a', 'j', 't', 'e' }; +static const symbol s_2_685[6] = { 'a', 'v', 'a', 'j', 't', 'e' }; +static const symbol s_2_686[6] = { 'e', 'v', 'a', 'j', 't', 'e' }; +static const symbol s_2_687[6] = { 'i', 'v', 'a', 'j', 't', 'e' }; +static const symbol s_2_688[6] = { 'u', 'v', 'a', 'j', 't', 'e' }; +static const symbol s_2_689[4] = { 'i', 'j', 't', 'e' }; +static const symbol s_2_690[7] = { 'l', 'u', 'c', 'u', 'j', 't', 'e' }; +static const symbol s_2_691[6] = { 'i', 'r', 'u', 'j', 't', 'e' }; +static const symbol s_2_692[8] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 't', 'e' }; +static const symbol s_2_693[4] = { 'a', 's', 't', 'e' }; +static const symbol s_2_694[6] = { 'a', 'c', 'a', 's', 't', 'e' }; +static const symbol s_2_695[9] = { 'a', 's', 't', 'a', 'j', 'a', 's', 't', 'e' }; +static const symbol s_2_696[9] = { 'i', 's', 't', 'a', 'j', 'a', 's', 't', 'e' }; +static const symbol s_2_697[9] = { 'o', 's', 't', 'a', 'j', 'a', 's', 't', 'e' }; +static const symbol s_2_698[7] = { 'i', 'n', 'j', 'a', 's', 't', 'e' }; +static const symbol s_2_699[6] = { 'i', 'r', 'a', 's', 't', 'e' }; +static const symbol s_2_700[6] = { 'u', 'r', 'a', 's', 't', 'e' }; +static const symbol s_2_701[5] = { 't', 'a', 's', 't', 'e' }; +static const symbol s_2_702[6] = { 'a', 'v', 'a', 's', 't', 'e' }; +static const symbol s_2_703[6] = { 'e', 'v', 'a', 's', 't', 'e' }; +static const symbol s_2_704[6] = { 'i', 'v', 'a', 's', 't', 'e' }; +static const symbol s_2_705[6] = { 'o', 'v', 'a', 's', 't', 'e' }; +static const symbol s_2_706[6] = { 'u', 'v', 'a', 's', 't', 'e' }; +static const symbol s_2_707[7] = { 'a', 0xC4, 0x8D, 'a', 's', 't', 'e' }; +static const symbol s_2_708[4] = { 'e', 's', 't', 'e' }; +static const symbol s_2_709[4] = { 'i', 's', 't', 'e' }; +static const symbol s_2_710[6] = { 'a', 'c', 'i', 's', 't', 'e' }; +static const symbol s_2_711[7] = { 'l', 'u', 'c', 'i', 's', 't', 'e' }; +static const symbol s_2_712[5] = { 'n', 'i', 's', 't', 'e' }; +static const symbol s_2_713[7] = { 'r', 'o', 's', 'i', 's', 't', 'e' }; +static const symbol s_2_714[7] = { 'j', 'e', 't', 'i', 's', 't', 'e' }; +static const symbol s_2_715[7] = { 'a', 0xC4, 0x8D, 'i', 's', 't', 'e' }; +static const symbol s_2_716[8] = { 'l', 'u', 0xC4, 0x8D, 'i', 's', 't', 'e' }; +static const symbol s_2_717[8] = { 'r', 'o', 0xC5, 0xA1, 'i', 's', 't', 'e' }; +static const symbol s_2_718[4] = { 'k', 's', 't', 'e' }; +static const symbol s_2_719[4] = { 'o', 's', 't', 'e' }; +static const symbol s_2_720[9] = { 'a', 's', 't', 'a', 'd', 'o', 's', 't', 'e' }; +static const symbol s_2_721[9] = { 'i', 's', 't', 'a', 'd', 'o', 's', 't', 'e' }; +static const symbol s_2_722[9] = { 'o', 's', 't', 'a', 'd', 'o', 's', 't', 'e' }; +static const symbol s_2_723[5] = { 'n', 'u', 's', 't', 'e' }; +static const symbol s_2_724[5] = { 'i', 0xC5, 0xA1, 't', 'e' }; +static const symbol s_2_725[3] = { 'a', 'v', 'e' }; +static const symbol s_2_726[3] = { 'e', 'v', 'e' }; +static const symbol s_2_727[5] = { 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_728[6] = { 'c', 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_729[6] = { 'l', 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_730[6] = { 'r', 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_731[7] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_732[7] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_733[7] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'e' }; +static const symbol s_2_734[3] = { 'i', 'v', 'e' }; +static const symbol s_2_735[3] = { 'o', 'v', 'e' }; +static const symbol s_2_736[4] = { 'g', 'o', 'v', 'e' }; +static const symbol s_2_737[5] = { 'u', 'g', 'o', 'v', 'e' }; +static const symbol s_2_738[4] = { 'l', 'o', 'v', 'e' }; +static const symbol s_2_739[5] = { 'o', 'l', 'o', 'v', 'e' }; +static const symbol s_2_740[4] = { 'm', 'o', 'v', 'e' }; +static const symbol s_2_741[5] = { 'o', 'n', 'o', 'v', 'e' }; +static const symbol s_2_742[4] = { 'a', 0xC4, 0x87, 'e' }; +static const symbol s_2_743[4] = { 'e', 0xC4, 0x87, 'e' }; +static const symbol s_2_744[4] = { 'u', 0xC4, 0x87, 'e' }; +static const symbol s_2_745[4] = { 'a', 0xC4, 0x8D, 'e' }; +static const symbol s_2_746[5] = { 'l', 'u', 0xC4, 0x8D, 'e' }; +static const symbol s_2_747[4] = { 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_748[5] = { 'b', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_749[5] = { 'g', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_750[5] = { 'j', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_751[9] = { 'a', 's', 't', 'a', 'j', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_752[9] = { 'i', 's', 't', 'a', 'j', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_753[9] = { 'o', 's', 't', 'a', 'j', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_754[7] = { 'i', 'n', 'j', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_755[5] = { 'k', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_756[5] = { 'n', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_757[6] = { 'i', 'r', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_758[6] = { 'u', 'r', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_759[5] = { 't', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_760[5] = { 'v', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_761[6] = { 'a', 'v', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_762[6] = { 'e', 'v', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_763[6] = { 'i', 'v', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_764[6] = { 'o', 'v', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_765[6] = { 'u', 'v', 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_766[7] = { 'a', 0xC4, 0x8D, 'a', 0xC5, 0xA1, 'e' }; +static const symbol s_2_767[4] = { 'e', 0xC5, 0xA1, 'e' }; +static const symbol s_2_768[4] = { 'i', 0xC5, 0xA1, 'e' }; +static const symbol s_2_769[7] = { 'j', 'e', 't', 'i', 0xC5, 0xA1, 'e' }; +static const symbol s_2_770[7] = { 'a', 0xC4, 0x8D, 'i', 0xC5, 0xA1, 'e' }; +static const symbol s_2_771[8] = { 'l', 'u', 0xC4, 0x8D, 'i', 0xC5, 0xA1, 'e' }; +static const symbol s_2_772[8] = { 'r', 'o', 0xC5, 0xA1, 'i', 0xC5, 0xA1, 'e' }; +static const symbol s_2_773[4] = { 'o', 0xC5, 0xA1, 'e' }; +static const symbol s_2_774[9] = { 'a', 's', 't', 'a', 'd', 'o', 0xC5, 0xA1, 'e' }; +static const symbol s_2_775[9] = { 'i', 's', 't', 'a', 'd', 'o', 0xC5, 0xA1, 'e' }; +static const symbol s_2_776[9] = { 'o', 's', 't', 'a', 'd', 'o', 0xC5, 0xA1, 'e' }; +static const symbol s_2_777[4] = { 'a', 'c', 'e', 'g' }; +static const symbol s_2_778[4] = { 'e', 'c', 'e', 'g' }; +static const symbol s_2_779[4] = { 'u', 'c', 'e', 'g' }; +static const symbol s_2_780[7] = { 'a', 'n', 'j', 'i', 'j', 'e', 'g' }; +static const symbol s_2_781[7] = { 'e', 'n', 'j', 'i', 'j', 'e', 'g' }; +static const symbol s_2_782[7] = { 's', 'n', 'j', 'i', 'j', 'e', 'g' }; +static const symbol s_2_783[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'g' }; +static const symbol s_2_784[5] = { 'k', 'i', 'j', 'e', 'g' }; +static const symbol s_2_785[6] = { 's', 'k', 'i', 'j', 'e', 'g' }; +static const symbol s_2_786[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'g' }; +static const symbol s_2_787[6] = { 'e', 'l', 'i', 'j', 'e', 'g' }; +static const symbol s_2_788[5] = { 'n', 'i', 'j', 'e', 'g' }; +static const symbol s_2_789[6] = { 'o', 's', 'i', 'j', 'e', 'g' }; +static const symbol s_2_790[6] = { 'a', 't', 'i', 'j', 'e', 'g' }; +static const symbol s_2_791[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'g' }; +static const symbol s_2_792[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'g' }; +static const symbol s_2_793[7] = { 'a', 's', 't', 'i', 'j', 'e', 'g' }; +static const symbol s_2_794[6] = { 'a', 'v', 'i', 'j', 'e', 'g' }; +static const symbol s_2_795[6] = { 'e', 'v', 'i', 'j', 'e', 'g' }; +static const symbol s_2_796[6] = { 'i', 'v', 'i', 'j', 'e', 'g' }; +static const symbol s_2_797[6] = { 'o', 'v', 'i', 'j', 'e', 'g' }; +static const symbol s_2_798[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'g' }; +static const symbol s_2_799[5] = { 'a', 'n', 'j', 'e', 'g' }; +static const symbol s_2_800[5] = { 'e', 'n', 'j', 'e', 'g' }; +static const symbol s_2_801[5] = { 's', 'n', 'j', 'e', 'g' }; +static const symbol s_2_802[6] = { 0xC5, 0xA1, 'n', 'j', 'e', 'g' }; +static const symbol s_2_803[3] = { 'k', 'e', 'g' }; +static const symbol s_2_804[4] = { 'e', 'l', 'e', 'g' }; +static const symbol s_2_805[3] = { 'n', 'e', 'g' }; +static const symbol s_2_806[4] = { 'a', 'n', 'e', 'g' }; +static const symbol s_2_807[4] = { 'e', 'n', 'e', 'g' }; +static const symbol s_2_808[4] = { 's', 'n', 'e', 'g' }; +static const symbol s_2_809[5] = { 0xC5, 0xA1, 'n', 'e', 'g' }; +static const symbol s_2_810[4] = { 'o', 's', 'e', 'g' }; +static const symbol s_2_811[4] = { 'a', 't', 'e', 'g' }; +static const symbol s_2_812[4] = { 'a', 'v', 'e', 'g' }; +static const symbol s_2_813[4] = { 'e', 'v', 'e', 'g' }; +static const symbol s_2_814[4] = { 'i', 'v', 'e', 'g' }; +static const symbol s_2_815[4] = { 'o', 'v', 'e', 'g' }; +static const symbol s_2_816[5] = { 'a', 0xC4, 0x87, 'e', 'g' }; +static const symbol s_2_817[5] = { 'e', 0xC4, 0x87, 'e', 'g' }; +static const symbol s_2_818[5] = { 'u', 0xC4, 0x87, 'e', 'g' }; +static const symbol s_2_819[5] = { 'o', 0xC5, 0xA1, 'e', 'g' }; +static const symbol s_2_820[4] = { 'a', 'c', 'o', 'g' }; +static const symbol s_2_821[4] = { 'e', 'c', 'o', 'g' }; +static const symbol s_2_822[4] = { 'u', 'c', 'o', 'g' }; +static const symbol s_2_823[5] = { 'a', 'n', 'j', 'o', 'g' }; +static const symbol s_2_824[5] = { 'e', 'n', 'j', 'o', 'g' }; +static const symbol s_2_825[5] = { 's', 'n', 'j', 'o', 'g' }; +static const symbol s_2_826[6] = { 0xC5, 0xA1, 'n', 'j', 'o', 'g' }; +static const symbol s_2_827[3] = { 'k', 'o', 'g' }; +static const symbol s_2_828[4] = { 's', 'k', 'o', 'g' }; +static const symbol s_2_829[5] = { 0xC5, 0xA1, 'k', 'o', 'g' }; +static const symbol s_2_830[4] = { 'e', 'l', 'o', 'g' }; +static const symbol s_2_831[3] = { 'n', 'o', 'g' }; +static const symbol s_2_832[5] = { 'c', 'i', 'n', 'o', 'g' }; +static const symbol s_2_833[6] = { 0xC4, 0x8D, 'i', 'n', 'o', 'g' }; +static const symbol s_2_834[4] = { 'o', 's', 'o', 'g' }; +static const symbol s_2_835[4] = { 'a', 't', 'o', 'g' }; +static const symbol s_2_836[6] = { 'e', 'v', 'i', 't', 'o', 'g' }; +static const symbol s_2_837[6] = { 'o', 'v', 'i', 't', 'o', 'g' }; +static const symbol s_2_838[5] = { 'a', 's', 't', 'o', 'g' }; +static const symbol s_2_839[4] = { 'a', 'v', 'o', 'g' }; +static const symbol s_2_840[4] = { 'e', 'v', 'o', 'g' }; +static const symbol s_2_841[4] = { 'i', 'v', 'o', 'g' }; +static const symbol s_2_842[4] = { 'o', 'v', 'o', 'g' }; +static const symbol s_2_843[5] = { 'a', 0xC4, 0x87, 'o', 'g' }; +static const symbol s_2_844[5] = { 'e', 0xC4, 0x87, 'o', 'g' }; +static const symbol s_2_845[5] = { 'u', 0xC4, 0x87, 'o', 'g' }; +static const symbol s_2_846[5] = { 'o', 0xC5, 0xA1, 'o', 'g' }; +static const symbol s_2_847[2] = { 'a', 'h' }; +static const symbol s_2_848[4] = { 'a', 'c', 'a', 'h' }; +static const symbol s_2_849[7] = { 'a', 's', 't', 'a', 'j', 'a', 'h' }; +static const symbol s_2_850[7] = { 'i', 's', 't', 'a', 'j', 'a', 'h' }; +static const symbol s_2_851[7] = { 'o', 's', 't', 'a', 'j', 'a', 'h' }; +static const symbol s_2_852[5] = { 'i', 'n', 'j', 'a', 'h' }; +static const symbol s_2_853[4] = { 'i', 'r', 'a', 'h' }; +static const symbol s_2_854[4] = { 'u', 'r', 'a', 'h' }; +static const symbol s_2_855[3] = { 't', 'a', 'h' }; +static const symbol s_2_856[4] = { 'a', 'v', 'a', 'h' }; +static const symbol s_2_857[4] = { 'e', 'v', 'a', 'h' }; +static const symbol s_2_858[4] = { 'i', 'v', 'a', 'h' }; +static const symbol s_2_859[4] = { 'o', 'v', 'a', 'h' }; +static const symbol s_2_860[4] = { 'u', 'v', 'a', 'h' }; +static const symbol s_2_861[5] = { 'a', 0xC4, 0x8D, 'a', 'h' }; +static const symbol s_2_862[2] = { 'i', 'h' }; +static const symbol s_2_863[4] = { 'a', 'c', 'i', 'h' }; +static const symbol s_2_864[4] = { 'e', 'c', 'i', 'h' }; +static const symbol s_2_865[4] = { 'u', 'c', 'i', 'h' }; +static const symbol s_2_866[5] = { 'l', 'u', 'c', 'i', 'h' }; +static const symbol s_2_867[7] = { 'a', 'n', 'j', 'i', 'j', 'i', 'h' }; +static const symbol s_2_868[7] = { 'e', 'n', 'j', 'i', 'j', 'i', 'h' }; +static const symbol s_2_869[7] = { 's', 'n', 'j', 'i', 'j', 'i', 'h' }; +static const symbol s_2_870[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i', 'h' }; +static const symbol s_2_871[5] = { 'k', 'i', 'j', 'i', 'h' }; +static const symbol s_2_872[6] = { 's', 'k', 'i', 'j', 'i', 'h' }; +static const symbol s_2_873[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i', 'h' }; +static const symbol s_2_874[6] = { 'e', 'l', 'i', 'j', 'i', 'h' }; +static const symbol s_2_875[5] = { 'n', 'i', 'j', 'i', 'h' }; +static const symbol s_2_876[6] = { 'o', 's', 'i', 'j', 'i', 'h' }; +static const symbol s_2_877[6] = { 'a', 't', 'i', 'j', 'i', 'h' }; +static const symbol s_2_878[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'i', 'h' }; +static const symbol s_2_879[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'i', 'h' }; +static const symbol s_2_880[7] = { 'a', 's', 't', 'i', 'j', 'i', 'h' }; +static const symbol s_2_881[6] = { 'a', 'v', 'i', 'j', 'i', 'h' }; +static const symbol s_2_882[6] = { 'e', 'v', 'i', 'j', 'i', 'h' }; +static const symbol s_2_883[6] = { 'i', 'v', 'i', 'j', 'i', 'h' }; +static const symbol s_2_884[6] = { 'o', 'v', 'i', 'j', 'i', 'h' }; +static const symbol s_2_885[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i', 'h' }; +static const symbol s_2_886[5] = { 'a', 'n', 'j', 'i', 'h' }; +static const symbol s_2_887[5] = { 'e', 'n', 'j', 'i', 'h' }; +static const symbol s_2_888[5] = { 's', 'n', 'j', 'i', 'h' }; +static const symbol s_2_889[6] = { 0xC5, 0xA1, 'n', 'j', 'i', 'h' }; +static const symbol s_2_890[3] = { 'k', 'i', 'h' }; +static const symbol s_2_891[4] = { 's', 'k', 'i', 'h' }; +static const symbol s_2_892[5] = { 0xC5, 0xA1, 'k', 'i', 'h' }; +static const symbol s_2_893[4] = { 'e', 'l', 'i', 'h' }; +static const symbol s_2_894[3] = { 'n', 'i', 'h' }; +static const symbol s_2_895[5] = { 'c', 'i', 'n', 'i', 'h' }; +static const symbol s_2_896[6] = { 0xC4, 0x8D, 'i', 'n', 'i', 'h' }; +static const symbol s_2_897[4] = { 'o', 's', 'i', 'h' }; +static const symbol s_2_898[5] = { 'r', 'o', 's', 'i', 'h' }; +static const symbol s_2_899[4] = { 'a', 't', 'i', 'h' }; +static const symbol s_2_900[5] = { 'j', 'e', 't', 'i', 'h' }; +static const symbol s_2_901[6] = { 'e', 'v', 'i', 't', 'i', 'h' }; +static const symbol s_2_902[6] = { 'o', 'v', 'i', 't', 'i', 'h' }; +static const symbol s_2_903[5] = { 'a', 's', 't', 'i', 'h' }; +static const symbol s_2_904[4] = { 'a', 'v', 'i', 'h' }; +static const symbol s_2_905[4] = { 'e', 'v', 'i', 'h' }; +static const symbol s_2_906[4] = { 'i', 'v', 'i', 'h' }; +static const symbol s_2_907[4] = { 'o', 'v', 'i', 'h' }; +static const symbol s_2_908[5] = { 'a', 0xC4, 0x87, 'i', 'h' }; +static const symbol s_2_909[5] = { 'e', 0xC4, 0x87, 'i', 'h' }; +static const symbol s_2_910[5] = { 'u', 0xC4, 0x87, 'i', 'h' }; +static const symbol s_2_911[5] = { 'a', 0xC4, 0x8D, 'i', 'h' }; +static const symbol s_2_912[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 'h' }; +static const symbol s_2_913[5] = { 'o', 0xC5, 0xA1, 'i', 'h' }; +static const symbol s_2_914[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 'h' }; +static const symbol s_2_915[7] = { 'a', 's', 't', 'a', 'd', 'o', 'h' }; +static const symbol s_2_916[7] = { 'i', 's', 't', 'a', 'd', 'o', 'h' }; +static const symbol s_2_917[7] = { 'o', 's', 't', 'a', 'd', 'o', 'h' }; +static const symbol s_2_918[4] = { 'a', 'c', 'u', 'h' }; +static const symbol s_2_919[4] = { 'e', 'c', 'u', 'h' }; +static const symbol s_2_920[4] = { 'u', 'c', 'u', 'h' }; +static const symbol s_2_921[5] = { 'a', 0xC4, 0x87, 'u', 'h' }; +static const symbol s_2_922[5] = { 'e', 0xC4, 0x87, 'u', 'h' }; +static const symbol s_2_923[5] = { 'u', 0xC4, 0x87, 'u', 'h' }; +static const symbol s_2_924[3] = { 'a', 'c', 'i' }; +static const symbol s_2_925[5] = { 'a', 'c', 'e', 'c', 'i' }; +static const symbol s_2_926[4] = { 'i', 'e', 'c', 'i' }; +static const symbol s_2_927[5] = { 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_928[7] = { 'i', 'r', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_929[7] = { 'u', 'r', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_930[8] = { 'a', 's', 't', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_931[8] = { 'i', 's', 't', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_932[8] = { 'o', 's', 't', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_933[7] = { 'a', 'v', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_934[7] = { 'e', 'v', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_935[7] = { 'i', 'v', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_936[7] = { 'u', 'v', 'a', 'j', 'u', 'c', 'i' }; +static const symbol s_2_937[5] = { 'u', 'j', 'u', 'c', 'i' }; +static const symbol s_2_938[8] = { 'l', 'u', 'c', 'u', 'j', 'u', 'c', 'i' }; +static const symbol s_2_939[7] = { 'i', 'r', 'u', 'j', 'u', 'c', 'i' }; +static const symbol s_2_940[4] = { 'l', 'u', 'c', 'i' }; +static const symbol s_2_941[4] = { 'n', 'u', 'c', 'i' }; +static const symbol s_2_942[5] = { 'e', 't', 'u', 'c', 'i' }; +static const symbol s_2_943[6] = { 'a', 's', 't', 'u', 'c', 'i' }; +static const symbol s_2_944[2] = { 'g', 'i' }; +static const symbol s_2_945[3] = { 'u', 'g', 'i' }; +static const symbol s_2_946[3] = { 'a', 'j', 'i' }; +static const symbol s_2_947[4] = { 'c', 'a', 'j', 'i' }; +static const symbol s_2_948[4] = { 'l', 'a', 'j', 'i' }; +static const symbol s_2_949[4] = { 'r', 'a', 'j', 'i' }; +static const symbol s_2_950[5] = { 0xC4, 0x87, 'a', 'j', 'i' }; +static const symbol s_2_951[5] = { 0xC4, 0x8D, 'a', 'j', 'i' }; +static const symbol s_2_952[5] = { 0xC4, 0x91, 'a', 'j', 'i' }; +static const symbol s_2_953[4] = { 'b', 'i', 'j', 'i' }; +static const symbol s_2_954[4] = { 'c', 'i', 'j', 'i' }; +static const symbol s_2_955[4] = { 'd', 'i', 'j', 'i' }; +static const symbol s_2_956[4] = { 'f', 'i', 'j', 'i' }; +static const symbol s_2_957[4] = { 'g', 'i', 'j', 'i' }; +static const symbol s_2_958[6] = { 'a', 'n', 'j', 'i', 'j', 'i' }; +static const symbol s_2_959[6] = { 'e', 'n', 'j', 'i', 'j', 'i' }; +static const symbol s_2_960[6] = { 's', 'n', 'j', 'i', 'j', 'i' }; +static const symbol s_2_961[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i' }; +static const symbol s_2_962[4] = { 'k', 'i', 'j', 'i' }; +static const symbol s_2_963[5] = { 's', 'k', 'i', 'j', 'i' }; +static const symbol s_2_964[6] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i' }; +static const symbol s_2_965[4] = { 'l', 'i', 'j', 'i' }; +static const symbol s_2_966[5] = { 'e', 'l', 'i', 'j', 'i' }; +static const symbol s_2_967[4] = { 'm', 'i', 'j', 'i' }; +static const symbol s_2_968[4] = { 'n', 'i', 'j', 'i' }; +static const symbol s_2_969[6] = { 'g', 'a', 'n', 'i', 'j', 'i' }; +static const symbol s_2_970[6] = { 'm', 'a', 'n', 'i', 'j', 'i' }; +static const symbol s_2_971[6] = { 'p', 'a', 'n', 'i', 'j', 'i' }; +static const symbol s_2_972[6] = { 'r', 'a', 'n', 'i', 'j', 'i' }; +static const symbol s_2_973[6] = { 't', 'a', 'n', 'i', 'j', 'i' }; +static const symbol s_2_974[4] = { 'p', 'i', 'j', 'i' }; +static const symbol s_2_975[4] = { 'r', 'i', 'j', 'i' }; +static const symbol s_2_976[4] = { 's', 'i', 'j', 'i' }; +static const symbol s_2_977[5] = { 'o', 's', 'i', 'j', 'i' }; +static const symbol s_2_978[4] = { 't', 'i', 'j', 'i' }; +static const symbol s_2_979[5] = { 'a', 't', 'i', 'j', 'i' }; +static const symbol s_2_980[7] = { 'e', 'v', 'i', 't', 'i', 'j', 'i' }; +static const symbol s_2_981[7] = { 'o', 'v', 'i', 't', 'i', 'j', 'i' }; +static const symbol s_2_982[6] = { 'a', 's', 't', 'i', 'j', 'i' }; +static const symbol s_2_983[5] = { 'a', 'v', 'i', 'j', 'i' }; +static const symbol s_2_984[5] = { 'e', 'v', 'i', 'j', 'i' }; +static const symbol s_2_985[5] = { 'i', 'v', 'i', 'j', 'i' }; +static const symbol s_2_986[5] = { 'o', 'v', 'i', 'j', 'i' }; +static const symbol s_2_987[4] = { 'z', 'i', 'j', 'i' }; +static const symbol s_2_988[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i' }; +static const symbol s_2_989[5] = { 0xC5, 0xBE, 'i', 'j', 'i' }; +static const symbol s_2_990[4] = { 'a', 'n', 'j', 'i' }; +static const symbol s_2_991[4] = { 'e', 'n', 'j', 'i' }; +static const symbol s_2_992[4] = { 's', 'n', 'j', 'i' }; +static const symbol s_2_993[5] = { 0xC5, 0xA1, 'n', 'j', 'i' }; +static const symbol s_2_994[2] = { 'k', 'i' }; +static const symbol s_2_995[3] = { 's', 'k', 'i' }; +static const symbol s_2_996[4] = { 0xC5, 0xA1, 'k', 'i' }; +static const symbol s_2_997[3] = { 'a', 'l', 'i' }; +static const symbol s_2_998[5] = { 'a', 'c', 'a', 'l', 'i' }; +static const symbol s_2_999[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'i' }; +static const symbol s_2_1000[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'i' }; +static const symbol s_2_1001[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'i' }; +static const symbol s_2_1002[5] = { 'i', 'j', 'a', 'l', 'i' }; +static const symbol s_2_1003[6] = { 'i', 'n', 'j', 'a', 'l', 'i' }; +static const symbol s_2_1004[4] = { 'n', 'a', 'l', 'i' }; +static const symbol s_2_1005[5] = { 'i', 'r', 'a', 'l', 'i' }; +static const symbol s_2_1006[5] = { 'u', 'r', 'a', 'l', 'i' }; +static const symbol s_2_1007[4] = { 't', 'a', 'l', 'i' }; +static const symbol s_2_1008[6] = { 'a', 's', 't', 'a', 'l', 'i' }; +static const symbol s_2_1009[6] = { 'i', 's', 't', 'a', 'l', 'i' }; +static const symbol s_2_1010[6] = { 'o', 's', 't', 'a', 'l', 'i' }; +static const symbol s_2_1011[5] = { 'a', 'v', 'a', 'l', 'i' }; +static const symbol s_2_1012[5] = { 'e', 'v', 'a', 'l', 'i' }; +static const symbol s_2_1013[5] = { 'i', 'v', 'a', 'l', 'i' }; +static const symbol s_2_1014[5] = { 'o', 'v', 'a', 'l', 'i' }; +static const symbol s_2_1015[5] = { 'u', 'v', 'a', 'l', 'i' }; +static const symbol s_2_1016[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'i' }; +static const symbol s_2_1017[3] = { 'e', 'l', 'i' }; +static const symbol s_2_1018[3] = { 'i', 'l', 'i' }; +static const symbol s_2_1019[5] = { 'a', 'c', 'i', 'l', 'i' }; +static const symbol s_2_1020[6] = { 'l', 'u', 'c', 'i', 'l', 'i' }; +static const symbol s_2_1021[4] = { 'n', 'i', 'l', 'i' }; +static const symbol s_2_1022[6] = { 'r', 'o', 's', 'i', 'l', 'i' }; +static const symbol s_2_1023[6] = { 'j', 'e', 't', 'i', 'l', 'i' }; +static const symbol s_2_1024[5] = { 'o', 'z', 'i', 'l', 'i' }; +static const symbol s_2_1025[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'i' }; +static const symbol s_2_1026[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'i' }; +static const symbol s_2_1027[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'i' }; +static const symbol s_2_1028[3] = { 'o', 'l', 'i' }; +static const symbol s_2_1029[4] = { 'a', 's', 'l', 'i' }; +static const symbol s_2_1030[4] = { 'n', 'u', 'l', 'i' }; +static const symbol s_2_1031[4] = { 'r', 'a', 'm', 'i' }; +static const symbol s_2_1032[4] = { 'l', 'e', 'm', 'i' }; +static const symbol s_2_1033[2] = { 'n', 'i' }; +static const symbol s_2_1034[3] = { 'a', 'n', 'i' }; +static const symbol s_2_1035[5] = { 'a', 'c', 'a', 'n', 'i' }; +static const symbol s_2_1036[5] = { 'u', 'r', 'a', 'n', 'i' }; +static const symbol s_2_1037[4] = { 't', 'a', 'n', 'i' }; +static const symbol s_2_1038[5] = { 'a', 'v', 'a', 'n', 'i' }; +static const symbol s_2_1039[5] = { 'e', 'v', 'a', 'n', 'i' }; +static const symbol s_2_1040[5] = { 'i', 'v', 'a', 'n', 'i' }; +static const symbol s_2_1041[5] = { 'u', 'v', 'a', 'n', 'i' }; +static const symbol s_2_1042[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'i' }; +static const symbol s_2_1043[5] = { 'a', 'c', 'e', 'n', 'i' }; +static const symbol s_2_1044[6] = { 'l', 'u', 'c', 'e', 'n', 'i' }; +static const symbol s_2_1045[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'i' }; +static const symbol s_2_1046[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'i' }; +static const symbol s_2_1047[3] = { 'i', 'n', 'i' }; +static const symbol s_2_1048[4] = { 'c', 'i', 'n', 'i' }; +static const symbol s_2_1049[5] = { 0xC4, 0x8D, 'i', 'n', 'i' }; +static const symbol s_2_1050[3] = { 'o', 'n', 'i' }; +static const symbol s_2_1051[3] = { 'a', 'r', 'i' }; +static const symbol s_2_1052[3] = { 'd', 'r', 'i' }; +static const symbol s_2_1053[3] = { 'e', 'r', 'i' }; +static const symbol s_2_1054[3] = { 'o', 'r', 'i' }; +static const symbol s_2_1055[4] = { 'b', 'a', 's', 'i' }; +static const symbol s_2_1056[4] = { 'g', 'a', 's', 'i' }; +static const symbol s_2_1057[4] = { 'j', 'a', 's', 'i' }; +static const symbol s_2_1058[4] = { 'k', 'a', 's', 'i' }; +static const symbol s_2_1059[4] = { 'n', 'a', 's', 'i' }; +static const symbol s_2_1060[4] = { 't', 'a', 's', 'i' }; +static const symbol s_2_1061[4] = { 'v', 'a', 's', 'i' }; +static const symbol s_2_1062[3] = { 'e', 's', 'i' }; +static const symbol s_2_1063[3] = { 'i', 's', 'i' }; +static const symbol s_2_1064[3] = { 'o', 's', 'i' }; +static const symbol s_2_1065[4] = { 'a', 'v', 's', 'i' }; +static const symbol s_2_1066[6] = { 'a', 'c', 'a', 'v', 's', 'i' }; +static const symbol s_2_1067[6] = { 'i', 'r', 'a', 'v', 's', 'i' }; +static const symbol s_2_1068[5] = { 't', 'a', 'v', 's', 'i' }; +static const symbol s_2_1069[6] = { 'e', 't', 'a', 'v', 's', 'i' }; +static const symbol s_2_1070[7] = { 'a', 's', 't', 'a', 'v', 's', 'i' }; +static const symbol s_2_1071[7] = { 'i', 's', 't', 'a', 'v', 's', 'i' }; +static const symbol s_2_1072[7] = { 'o', 's', 't', 'a', 'v', 's', 'i' }; +static const symbol s_2_1073[4] = { 'i', 'v', 's', 'i' }; +static const symbol s_2_1074[5] = { 'n', 'i', 'v', 's', 'i' }; +static const symbol s_2_1075[7] = { 'r', 'o', 's', 'i', 'v', 's', 'i' }; +static const symbol s_2_1076[5] = { 'n', 'u', 'v', 's', 'i' }; +static const symbol s_2_1077[3] = { 'a', 't', 'i' }; +static const symbol s_2_1078[5] = { 'a', 'c', 'a', 't', 'i' }; +static const symbol s_2_1079[8] = { 'a', 's', 't', 'a', 'j', 'a', 't', 'i' }; +static const symbol s_2_1080[8] = { 'i', 's', 't', 'a', 'j', 'a', 't', 'i' }; +static const symbol s_2_1081[8] = { 'o', 's', 't', 'a', 'j', 'a', 't', 'i' }; +static const symbol s_2_1082[6] = { 'i', 'n', 'j', 'a', 't', 'i' }; +static const symbol s_2_1083[5] = { 'i', 'k', 'a', 't', 'i' }; +static const symbol s_2_1084[4] = { 'l', 'a', 't', 'i' }; +static const symbol s_2_1085[5] = { 'i', 'r', 'a', 't', 'i' }; +static const symbol s_2_1086[5] = { 'u', 'r', 'a', 't', 'i' }; +static const symbol s_2_1087[4] = { 't', 'a', 't', 'i' }; +static const symbol s_2_1088[6] = { 'a', 's', 't', 'a', 't', 'i' }; +static const symbol s_2_1089[6] = { 'i', 's', 't', 'a', 't', 'i' }; +static const symbol s_2_1090[6] = { 'o', 's', 't', 'a', 't', 'i' }; +static const symbol s_2_1091[5] = { 'a', 'v', 'a', 't', 'i' }; +static const symbol s_2_1092[5] = { 'e', 'v', 'a', 't', 'i' }; +static const symbol s_2_1093[5] = { 'i', 'v', 'a', 't', 'i' }; +static const symbol s_2_1094[5] = { 'o', 'v', 'a', 't', 'i' }; +static const symbol s_2_1095[5] = { 'u', 'v', 'a', 't', 'i' }; +static const symbol s_2_1096[6] = { 'a', 0xC4, 0x8D, 'a', 't', 'i' }; +static const symbol s_2_1097[3] = { 'e', 't', 'i' }; +static const symbol s_2_1098[3] = { 'i', 't', 'i' }; +static const symbol s_2_1099[5] = { 'a', 'c', 'i', 't', 'i' }; +static const symbol s_2_1100[6] = { 'l', 'u', 'c', 'i', 't', 'i' }; +static const symbol s_2_1101[4] = { 'n', 'i', 't', 'i' }; +static const symbol s_2_1102[6] = { 'r', 'o', 's', 'i', 't', 'i' }; +static const symbol s_2_1103[6] = { 'j', 'e', 't', 'i', 't', 'i' }; +static const symbol s_2_1104[5] = { 'e', 'v', 'i', 't', 'i' }; +static const symbol s_2_1105[5] = { 'o', 'v', 'i', 't', 'i' }; +static const symbol s_2_1106[6] = { 'a', 0xC4, 0x8D, 'i', 't', 'i' }; +static const symbol s_2_1107[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 't', 'i' }; +static const symbol s_2_1108[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 't', 'i' }; +static const symbol s_2_1109[4] = { 'a', 's', 't', 'i' }; +static const symbol s_2_1110[4] = { 'e', 's', 't', 'i' }; +static const symbol s_2_1111[4] = { 'i', 's', 't', 'i' }; +static const symbol s_2_1112[4] = { 'k', 's', 't', 'i' }; +static const symbol s_2_1113[4] = { 'o', 's', 't', 'i' }; +static const symbol s_2_1114[4] = { 'n', 'u', 't', 'i' }; +static const symbol s_2_1115[3] = { 'a', 'v', 'i' }; +static const symbol s_2_1116[3] = { 'e', 'v', 'i' }; +static const symbol s_2_1117[5] = { 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1118[6] = { 'c', 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1119[6] = { 'l', 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1120[6] = { 'r', 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1121[7] = { 0xC4, 0x87, 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1122[7] = { 0xC4, 0x8D, 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1123[7] = { 0xC4, 0x91, 'a', 'j', 'e', 'v', 'i' }; +static const symbol s_2_1124[3] = { 'i', 'v', 'i' }; +static const symbol s_2_1125[3] = { 'o', 'v', 'i' }; +static const symbol s_2_1126[4] = { 'g', 'o', 'v', 'i' }; +static const symbol s_2_1127[5] = { 'u', 'g', 'o', 'v', 'i' }; +static const symbol s_2_1128[4] = { 'l', 'o', 'v', 'i' }; +static const symbol s_2_1129[5] = { 'o', 'l', 'o', 'v', 'i' }; +static const symbol s_2_1130[4] = { 'm', 'o', 'v', 'i' }; +static const symbol s_2_1131[5] = { 'o', 'n', 'o', 'v', 'i' }; +static const symbol s_2_1132[5] = { 'i', 'e', 0xC4, 0x87, 'i' }; +static const symbol s_2_1133[7] = { 'a', 0xC4, 0x8D, 'e', 0xC4, 0x87, 'i' }; +static const symbol s_2_1134[6] = { 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1135[8] = { 'i', 'r', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1136[8] = { 'u', 'r', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1137[9] = { 'a', 's', 't', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1138[9] = { 'i', 's', 't', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1139[9] = { 'o', 's', 't', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1140[8] = { 'a', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1141[8] = { 'e', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1142[8] = { 'i', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1143[8] = { 'u', 'v', 'a', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1144[6] = { 'u', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1145[8] = { 'i', 'r', 'u', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1146[10] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1147[5] = { 'n', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1148[6] = { 'e', 't', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1149[7] = { 'a', 's', 't', 'u', 0xC4, 0x87, 'i' }; +static const symbol s_2_1150[4] = { 'a', 0xC4, 0x8D, 'i' }; +static const symbol s_2_1151[5] = { 'l', 'u', 0xC4, 0x8D, 'i' }; +static const symbol s_2_1152[5] = { 'b', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1153[5] = { 'g', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1154[5] = { 'j', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1155[5] = { 'k', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1156[5] = { 'n', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1157[5] = { 't', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1158[5] = { 'v', 'a', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1159[4] = { 'e', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1160[4] = { 'i', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1161[4] = { 'o', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1162[5] = { 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1163[7] = { 'i', 'r', 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1164[6] = { 't', 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1165[7] = { 'e', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1166[8] = { 'a', 's', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1167[8] = { 'i', 's', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1168[8] = { 'o', 's', 't', 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1169[8] = { 'a', 0xC4, 0x8D, 'a', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1170[5] = { 'i', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1171[6] = { 'n', 'i', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1172[9] = { 'r', 'o', 0xC5, 0xA1, 'i', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1173[6] = { 'n', 'u', 'v', 0xC5, 0xA1, 'i' }; +static const symbol s_2_1174[2] = { 'a', 'j' }; +static const symbol s_2_1175[4] = { 'u', 'r', 'a', 'j' }; +static const symbol s_2_1176[3] = { 't', 'a', 'j' }; +static const symbol s_2_1177[4] = { 'a', 'v', 'a', 'j' }; +static const symbol s_2_1178[4] = { 'e', 'v', 'a', 'j' }; +static const symbol s_2_1179[4] = { 'i', 'v', 'a', 'j' }; +static const symbol s_2_1180[4] = { 'u', 'v', 'a', 'j' }; +static const symbol s_2_1181[2] = { 'i', 'j' }; +static const symbol s_2_1182[4] = { 'a', 'c', 'o', 'j' }; +static const symbol s_2_1183[4] = { 'e', 'c', 'o', 'j' }; +static const symbol s_2_1184[4] = { 'u', 'c', 'o', 'j' }; +static const symbol s_2_1185[7] = { 'a', 'n', 'j', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1186[7] = { 'e', 'n', 'j', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1187[7] = { 's', 'n', 'j', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1188[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1189[5] = { 'k', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1190[6] = { 's', 'k', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1191[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1192[6] = { 'e', 'l', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1193[5] = { 'n', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1194[6] = { 'o', 's', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1195[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1196[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1197[7] = { 'a', 's', 't', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1198[6] = { 'a', 'v', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1199[6] = { 'e', 'v', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1200[6] = { 'i', 'v', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1201[6] = { 'o', 'v', 'i', 'j', 'o', 'j' }; +static const symbol s_2_1202[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'o', 'j' }; +static const symbol s_2_1203[5] = { 'a', 'n', 'j', 'o', 'j' }; +static const symbol s_2_1204[5] = { 'e', 'n', 'j', 'o', 'j' }; +static const symbol s_2_1205[5] = { 's', 'n', 'j', 'o', 'j' }; +static const symbol s_2_1206[6] = { 0xC5, 0xA1, 'n', 'j', 'o', 'j' }; +static const symbol s_2_1207[3] = { 'k', 'o', 'j' }; +static const symbol s_2_1208[4] = { 's', 'k', 'o', 'j' }; +static const symbol s_2_1209[5] = { 0xC5, 0xA1, 'k', 'o', 'j' }; +static const symbol s_2_1210[4] = { 'a', 'l', 'o', 'j' }; +static const symbol s_2_1211[4] = { 'e', 'l', 'o', 'j' }; +static const symbol s_2_1212[3] = { 'n', 'o', 'j' }; +static const symbol s_2_1213[5] = { 'c', 'i', 'n', 'o', 'j' }; +static const symbol s_2_1214[6] = { 0xC4, 0x8D, 'i', 'n', 'o', 'j' }; +static const symbol s_2_1215[4] = { 'o', 's', 'o', 'j' }; +static const symbol s_2_1216[4] = { 'a', 't', 'o', 'j' }; +static const symbol s_2_1217[6] = { 'e', 'v', 'i', 't', 'o', 'j' }; +static const symbol s_2_1218[6] = { 'o', 'v', 'i', 't', 'o', 'j' }; +static const symbol s_2_1219[5] = { 'a', 's', 't', 'o', 'j' }; +static const symbol s_2_1220[4] = { 'a', 'v', 'o', 'j' }; +static const symbol s_2_1221[4] = { 'e', 'v', 'o', 'j' }; +static const symbol s_2_1222[4] = { 'i', 'v', 'o', 'j' }; +static const symbol s_2_1223[4] = { 'o', 'v', 'o', 'j' }; +static const symbol s_2_1224[5] = { 'a', 0xC4, 0x87, 'o', 'j' }; +static const symbol s_2_1225[5] = { 'e', 0xC4, 0x87, 'o', 'j' }; +static const symbol s_2_1226[5] = { 'u', 0xC4, 0x87, 'o', 'j' }; +static const symbol s_2_1227[5] = { 'o', 0xC5, 0xA1, 'o', 'j' }; +static const symbol s_2_1228[5] = { 'l', 'u', 'c', 'u', 'j' }; +static const symbol s_2_1229[4] = { 'i', 'r', 'u', 'j' }; +static const symbol s_2_1230[6] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j' }; +static const symbol s_2_1231[2] = { 'a', 'l' }; +static const symbol s_2_1232[4] = { 'i', 'r', 'a', 'l' }; +static const symbol s_2_1233[4] = { 'u', 'r', 'a', 'l' }; +static const symbol s_2_1234[2] = { 'e', 'l' }; +static const symbol s_2_1235[2] = { 'i', 'l' }; +static const symbol s_2_1236[2] = { 'a', 'm' }; +static const symbol s_2_1237[4] = { 'a', 'c', 'a', 'm' }; +static const symbol s_2_1238[4] = { 'i', 'r', 'a', 'm' }; +static const symbol s_2_1239[4] = { 'u', 'r', 'a', 'm' }; +static const symbol s_2_1240[3] = { 't', 'a', 'm' }; +static const symbol s_2_1241[4] = { 'a', 'v', 'a', 'm' }; +static const symbol s_2_1242[4] = { 'e', 'v', 'a', 'm' }; +static const symbol s_2_1243[4] = { 'i', 'v', 'a', 'm' }; +static const symbol s_2_1244[4] = { 'u', 'v', 'a', 'm' }; +static const symbol s_2_1245[5] = { 'a', 0xC4, 0x8D, 'a', 'm' }; +static const symbol s_2_1246[2] = { 'e', 'm' }; +static const symbol s_2_1247[4] = { 'a', 'c', 'e', 'm' }; +static const symbol s_2_1248[4] = { 'e', 'c', 'e', 'm' }; +static const symbol s_2_1249[4] = { 'u', 'c', 'e', 'm' }; +static const symbol s_2_1250[7] = { 'a', 's', 't', 'a', 'd', 'e', 'm' }; +static const symbol s_2_1251[7] = { 'i', 's', 't', 'a', 'd', 'e', 'm' }; +static const symbol s_2_1252[7] = { 'o', 's', 't', 'a', 'd', 'e', 'm' }; +static const symbol s_2_1253[4] = { 'a', 'j', 'e', 'm' }; +static const symbol s_2_1254[5] = { 'c', 'a', 'j', 'e', 'm' }; +static const symbol s_2_1255[5] = { 'l', 'a', 'j', 'e', 'm' }; +static const symbol s_2_1256[5] = { 'r', 'a', 'j', 'e', 'm' }; +static const symbol s_2_1257[7] = { 'a', 's', 't', 'a', 'j', 'e', 'm' }; +static const symbol s_2_1258[7] = { 'i', 's', 't', 'a', 'j', 'e', 'm' }; +static const symbol s_2_1259[7] = { 'o', 's', 't', 'a', 'j', 'e', 'm' }; +static const symbol s_2_1260[6] = { 0xC4, 0x87, 'a', 'j', 'e', 'm' }; +static const symbol s_2_1261[6] = { 0xC4, 0x8D, 'a', 'j', 'e', 'm' }; +static const symbol s_2_1262[6] = { 0xC4, 0x91, 'a', 'j', 'e', 'm' }; +static const symbol s_2_1263[4] = { 'i', 'j', 'e', 'm' }; +static const symbol s_2_1264[7] = { 'a', 'n', 'j', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1265[7] = { 'e', 'n', 'j', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1266[7] = { 's', 'n', 'j', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1267[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1268[5] = { 'k', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1269[6] = { 's', 'k', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1270[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1271[5] = { 'l', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1272[6] = { 'e', 'l', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1273[5] = { 'n', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1274[7] = { 'r', 'a', 'r', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1275[5] = { 's', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1276[6] = { 'o', 's', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1277[6] = { 'a', 't', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1278[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1279[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1280[6] = { 'o', 't', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1281[7] = { 'a', 's', 't', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1282[6] = { 'a', 'v', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1283[6] = { 'e', 'v', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1284[6] = { 'i', 'v', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1285[6] = { 'o', 'v', 'i', 'j', 'e', 'm' }; +static const symbol s_2_1286[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'm' }; +static const symbol s_2_1287[5] = { 'a', 'n', 'j', 'e', 'm' }; +static const symbol s_2_1288[5] = { 'e', 'n', 'j', 'e', 'm' }; +static const symbol s_2_1289[5] = { 'i', 'n', 'j', 'e', 'm' }; +static const symbol s_2_1290[5] = { 's', 'n', 'j', 'e', 'm' }; +static const symbol s_2_1291[6] = { 0xC5, 0xA1, 'n', 'j', 'e', 'm' }; +static const symbol s_2_1292[4] = { 'u', 'j', 'e', 'm' }; +static const symbol s_2_1293[7] = { 'l', 'u', 'c', 'u', 'j', 'e', 'm' }; +static const symbol s_2_1294[6] = { 'i', 'r', 'u', 'j', 'e', 'm' }; +static const symbol s_2_1295[8] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 'm' }; +static const symbol s_2_1296[3] = { 'k', 'e', 'm' }; +static const symbol s_2_1297[4] = { 's', 'k', 'e', 'm' }; +static const symbol s_2_1298[5] = { 0xC5, 0xA1, 'k', 'e', 'm' }; +static const symbol s_2_1299[4] = { 'e', 'l', 'e', 'm' }; +static const symbol s_2_1300[3] = { 'n', 'e', 'm' }; +static const symbol s_2_1301[4] = { 'a', 'n', 'e', 'm' }; +static const symbol s_2_1302[7] = { 'a', 's', 't', 'a', 'n', 'e', 'm' }; +static const symbol s_2_1303[7] = { 'i', 's', 't', 'a', 'n', 'e', 'm' }; +static const symbol s_2_1304[7] = { 'o', 's', 't', 'a', 'n', 'e', 'm' }; +static const symbol s_2_1305[4] = { 'e', 'n', 'e', 'm' }; +static const symbol s_2_1306[4] = { 's', 'n', 'e', 'm' }; +static const symbol s_2_1307[5] = { 0xC5, 0xA1, 'n', 'e', 'm' }; +static const symbol s_2_1308[5] = { 'b', 'a', 's', 'e', 'm' }; +static const symbol s_2_1309[5] = { 'g', 'a', 's', 'e', 'm' }; +static const symbol s_2_1310[5] = { 'j', 'a', 's', 'e', 'm' }; +static const symbol s_2_1311[5] = { 'k', 'a', 's', 'e', 'm' }; +static const symbol s_2_1312[5] = { 'n', 'a', 's', 'e', 'm' }; +static const symbol s_2_1313[5] = { 't', 'a', 's', 'e', 'm' }; +static const symbol s_2_1314[5] = { 'v', 'a', 's', 'e', 'm' }; +static const symbol s_2_1315[4] = { 'e', 's', 'e', 'm' }; +static const symbol s_2_1316[4] = { 'i', 's', 'e', 'm' }; +static const symbol s_2_1317[4] = { 'o', 's', 'e', 'm' }; +static const symbol s_2_1318[4] = { 'a', 't', 'e', 'm' }; +static const symbol s_2_1319[4] = { 'e', 't', 'e', 'm' }; +static const symbol s_2_1320[6] = { 'e', 'v', 'i', 't', 'e', 'm' }; +static const symbol s_2_1321[6] = { 'o', 'v', 'i', 't', 'e', 'm' }; +static const symbol s_2_1322[5] = { 'a', 's', 't', 'e', 'm' }; +static const symbol s_2_1323[5] = { 'i', 's', 't', 'e', 'm' }; +static const symbol s_2_1324[6] = { 'i', 0xC5, 0xA1, 't', 'e', 'm' }; +static const symbol s_2_1325[4] = { 'a', 'v', 'e', 'm' }; +static const symbol s_2_1326[4] = { 'e', 'v', 'e', 'm' }; +static const symbol s_2_1327[4] = { 'i', 'v', 'e', 'm' }; +static const symbol s_2_1328[5] = { 'a', 0xC4, 0x87, 'e', 'm' }; +static const symbol s_2_1329[5] = { 'e', 0xC4, 0x87, 'e', 'm' }; +static const symbol s_2_1330[5] = { 'u', 0xC4, 0x87, 'e', 'm' }; +static const symbol s_2_1331[6] = { 'b', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1332[6] = { 'g', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1333[6] = { 'j', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1334[6] = { 'k', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1335[6] = { 'n', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1336[6] = { 't', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1337[6] = { 'v', 'a', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1338[5] = { 'e', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1339[5] = { 'i', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1340[5] = { 'o', 0xC5, 0xA1, 'e', 'm' }; +static const symbol s_2_1341[2] = { 'i', 'm' }; +static const symbol s_2_1342[4] = { 'a', 'c', 'i', 'm' }; +static const symbol s_2_1343[4] = { 'e', 'c', 'i', 'm' }; +static const symbol s_2_1344[4] = { 'u', 'c', 'i', 'm' }; +static const symbol s_2_1345[5] = { 'l', 'u', 'c', 'i', 'm' }; +static const symbol s_2_1346[7] = { 'a', 'n', 'j', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1347[7] = { 'e', 'n', 'j', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1348[7] = { 's', 'n', 'j', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1349[8] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1350[5] = { 'k', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1351[6] = { 's', 'k', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1352[7] = { 0xC5, 0xA1, 'k', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1353[6] = { 'e', 'l', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1354[5] = { 'n', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1355[6] = { 'o', 's', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1356[6] = { 'a', 't', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1357[8] = { 'e', 'v', 'i', 't', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1358[8] = { 'o', 'v', 'i', 't', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1359[7] = { 'a', 's', 't', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1360[6] = { 'a', 'v', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1361[6] = { 'e', 'v', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1362[6] = { 'i', 'v', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1363[6] = { 'o', 'v', 'i', 'j', 'i', 'm' }; +static const symbol s_2_1364[7] = { 'o', 0xC5, 0xA1, 'i', 'j', 'i', 'm' }; +static const symbol s_2_1365[5] = { 'a', 'n', 'j', 'i', 'm' }; +static const symbol s_2_1366[5] = { 'e', 'n', 'j', 'i', 'm' }; +static const symbol s_2_1367[5] = { 's', 'n', 'j', 'i', 'm' }; +static const symbol s_2_1368[6] = { 0xC5, 0xA1, 'n', 'j', 'i', 'm' }; +static const symbol s_2_1369[3] = { 'k', 'i', 'm' }; +static const symbol s_2_1370[4] = { 's', 'k', 'i', 'm' }; +static const symbol s_2_1371[5] = { 0xC5, 0xA1, 'k', 'i', 'm' }; +static const symbol s_2_1372[4] = { 'e', 'l', 'i', 'm' }; +static const symbol s_2_1373[3] = { 'n', 'i', 'm' }; +static const symbol s_2_1374[5] = { 'c', 'i', 'n', 'i', 'm' }; +static const symbol s_2_1375[6] = { 0xC4, 0x8D, 'i', 'n', 'i', 'm' }; +static const symbol s_2_1376[4] = { 'o', 's', 'i', 'm' }; +static const symbol s_2_1377[5] = { 'r', 'o', 's', 'i', 'm' }; +static const symbol s_2_1378[4] = { 'a', 't', 'i', 'm' }; +static const symbol s_2_1379[5] = { 'j', 'e', 't', 'i', 'm' }; +static const symbol s_2_1380[6] = { 'e', 'v', 'i', 't', 'i', 'm' }; +static const symbol s_2_1381[6] = { 'o', 'v', 'i', 't', 'i', 'm' }; +static const symbol s_2_1382[5] = { 'a', 's', 't', 'i', 'm' }; +static const symbol s_2_1383[4] = { 'a', 'v', 'i', 'm' }; +static const symbol s_2_1384[4] = { 'e', 'v', 'i', 'm' }; +static const symbol s_2_1385[4] = { 'i', 'v', 'i', 'm' }; +static const symbol s_2_1386[4] = { 'o', 'v', 'i', 'm' }; +static const symbol s_2_1387[5] = { 'a', 0xC4, 0x87, 'i', 'm' }; +static const symbol s_2_1388[5] = { 'e', 0xC4, 0x87, 'i', 'm' }; +static const symbol s_2_1389[5] = { 'u', 0xC4, 0x87, 'i', 'm' }; +static const symbol s_2_1390[5] = { 'a', 0xC4, 0x8D, 'i', 'm' }; +static const symbol s_2_1391[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 'm' }; +static const symbol s_2_1392[5] = { 'o', 0xC5, 0xA1, 'i', 'm' }; +static const symbol s_2_1393[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 'm' }; +static const symbol s_2_1394[4] = { 'a', 'c', 'o', 'm' }; +static const symbol s_2_1395[4] = { 'e', 'c', 'o', 'm' }; +static const symbol s_2_1396[4] = { 'u', 'c', 'o', 'm' }; +static const symbol s_2_1397[3] = { 'g', 'o', 'm' }; +static const symbol s_2_1398[5] = { 'l', 'o', 'g', 'o', 'm' }; +static const symbol s_2_1399[4] = { 'u', 'g', 'o', 'm' }; +static const symbol s_2_1400[5] = { 'b', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1401[5] = { 'c', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1402[5] = { 'd', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1403[5] = { 'f', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1404[5] = { 'g', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1405[5] = { 'l', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1406[5] = { 'm', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1407[5] = { 'n', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1408[7] = { 'g', 'a', 'n', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1409[7] = { 'm', 'a', 'n', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1410[7] = { 'p', 'a', 'n', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1411[7] = { 'r', 'a', 'n', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1412[7] = { 't', 'a', 'n', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1413[5] = { 'p', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1414[5] = { 'r', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1415[5] = { 's', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1416[5] = { 't', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1417[5] = { 'z', 'i', 'j', 'o', 'm' }; +static const symbol s_2_1418[6] = { 0xC5, 0xBE, 'i', 'j', 'o', 'm' }; +static const symbol s_2_1419[5] = { 'a', 'n', 'j', 'o', 'm' }; +static const symbol s_2_1420[5] = { 'e', 'n', 'j', 'o', 'm' }; +static const symbol s_2_1421[5] = { 's', 'n', 'j', 'o', 'm' }; +static const symbol s_2_1422[6] = { 0xC5, 0xA1, 'n', 'j', 'o', 'm' }; +static const symbol s_2_1423[3] = { 'k', 'o', 'm' }; +static const symbol s_2_1424[4] = { 's', 'k', 'o', 'm' }; +static const symbol s_2_1425[5] = { 0xC5, 0xA1, 'k', 'o', 'm' }; +static const symbol s_2_1426[4] = { 'a', 'l', 'o', 'm' }; +static const symbol s_2_1427[6] = { 'i', 'j', 'a', 'l', 'o', 'm' }; +static const symbol s_2_1428[5] = { 'n', 'a', 'l', 'o', 'm' }; +static const symbol s_2_1429[4] = { 'e', 'l', 'o', 'm' }; +static const symbol s_2_1430[4] = { 'i', 'l', 'o', 'm' }; +static const symbol s_2_1431[6] = { 'o', 'z', 'i', 'l', 'o', 'm' }; +static const symbol s_2_1432[4] = { 'o', 'l', 'o', 'm' }; +static const symbol s_2_1433[5] = { 'r', 'a', 'm', 'o', 'm' }; +static const symbol s_2_1434[5] = { 'l', 'e', 'm', 'o', 'm' }; +static const symbol s_2_1435[3] = { 'n', 'o', 'm' }; +static const symbol s_2_1436[4] = { 'a', 'n', 'o', 'm' }; +static const symbol s_2_1437[4] = { 'i', 'n', 'o', 'm' }; +static const symbol s_2_1438[5] = { 'c', 'i', 'n', 'o', 'm' }; +static const symbol s_2_1439[6] = { 'a', 'n', 'i', 'n', 'o', 'm' }; +static const symbol s_2_1440[6] = { 0xC4, 0x8D, 'i', 'n', 'o', 'm' }; +static const symbol s_2_1441[4] = { 'o', 'n', 'o', 'm' }; +static const symbol s_2_1442[4] = { 'a', 'r', 'o', 'm' }; +static const symbol s_2_1443[4] = { 'd', 'r', 'o', 'm' }; +static const symbol s_2_1444[4] = { 'e', 'r', 'o', 'm' }; +static const symbol s_2_1445[4] = { 'o', 'r', 'o', 'm' }; +static const symbol s_2_1446[5] = { 'b', 'a', 's', 'o', 'm' }; +static const symbol s_2_1447[5] = { 'g', 'a', 's', 'o', 'm' }; +static const symbol s_2_1448[5] = { 'j', 'a', 's', 'o', 'm' }; +static const symbol s_2_1449[5] = { 'k', 'a', 's', 'o', 'm' }; +static const symbol s_2_1450[5] = { 'n', 'a', 's', 'o', 'm' }; +static const symbol s_2_1451[5] = { 't', 'a', 's', 'o', 'm' }; +static const symbol s_2_1452[5] = { 'v', 'a', 's', 'o', 'm' }; +static const symbol s_2_1453[4] = { 'e', 's', 'o', 'm' }; +static const symbol s_2_1454[4] = { 'i', 's', 'o', 'm' }; +static const symbol s_2_1455[4] = { 'o', 's', 'o', 'm' }; +static const symbol s_2_1456[4] = { 'a', 't', 'o', 'm' }; +static const symbol s_2_1457[6] = { 'i', 'k', 'a', 't', 'o', 'm' }; +static const symbol s_2_1458[5] = { 'l', 'a', 't', 'o', 'm' }; +static const symbol s_2_1459[4] = { 'e', 't', 'o', 'm' }; +static const symbol s_2_1460[6] = { 'e', 'v', 'i', 't', 'o', 'm' }; +static const symbol s_2_1461[6] = { 'o', 'v', 'i', 't', 'o', 'm' }; +static const symbol s_2_1462[5] = { 'a', 's', 't', 'o', 'm' }; +static const symbol s_2_1463[5] = { 'e', 's', 't', 'o', 'm' }; +static const symbol s_2_1464[5] = { 'i', 's', 't', 'o', 'm' }; +static const symbol s_2_1465[5] = { 'k', 's', 't', 'o', 'm' }; +static const symbol s_2_1466[5] = { 'o', 's', 't', 'o', 'm' }; +static const symbol s_2_1467[4] = { 'a', 'v', 'o', 'm' }; +static const symbol s_2_1468[4] = { 'e', 'v', 'o', 'm' }; +static const symbol s_2_1469[4] = { 'i', 'v', 'o', 'm' }; +static const symbol s_2_1470[4] = { 'o', 'v', 'o', 'm' }; +static const symbol s_2_1471[5] = { 'l', 'o', 'v', 'o', 'm' }; +static const symbol s_2_1472[5] = { 'm', 'o', 'v', 'o', 'm' }; +static const symbol s_2_1473[5] = { 's', 't', 'v', 'o', 'm' }; +static const symbol s_2_1474[6] = { 0xC5, 0xA1, 't', 'v', 'o', 'm' }; +static const symbol s_2_1475[5] = { 'a', 0xC4, 0x87, 'o', 'm' }; +static const symbol s_2_1476[5] = { 'e', 0xC4, 0x87, 'o', 'm' }; +static const symbol s_2_1477[5] = { 'u', 0xC4, 0x87, 'o', 'm' }; +static const symbol s_2_1478[6] = { 'b', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1479[6] = { 'g', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1480[6] = { 'j', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1481[6] = { 'k', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1482[6] = { 'n', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1483[6] = { 't', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1484[6] = { 'v', 'a', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1485[5] = { 'e', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1486[5] = { 'i', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1487[5] = { 'o', 0xC5, 0xA1, 'o', 'm' }; +static const symbol s_2_1488[2] = { 'a', 'n' }; +static const symbol s_2_1489[4] = { 'a', 'c', 'a', 'n' }; +static const symbol s_2_1490[4] = { 'i', 'r', 'a', 'n' }; +static const symbol s_2_1491[4] = { 'u', 'r', 'a', 'n' }; +static const symbol s_2_1492[3] = { 't', 'a', 'n' }; +static const symbol s_2_1493[4] = { 'a', 'v', 'a', 'n' }; +static const symbol s_2_1494[4] = { 'e', 'v', 'a', 'n' }; +static const symbol s_2_1495[4] = { 'i', 'v', 'a', 'n' }; +static const symbol s_2_1496[4] = { 'u', 'v', 'a', 'n' }; +static const symbol s_2_1497[5] = { 'a', 0xC4, 0x8D, 'a', 'n' }; +static const symbol s_2_1498[4] = { 'a', 'c', 'e', 'n' }; +static const symbol s_2_1499[5] = { 'l', 'u', 'c', 'e', 'n' }; +static const symbol s_2_1500[5] = { 'a', 0xC4, 0x8D, 'e', 'n' }; +static const symbol s_2_1501[6] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n' }; +static const symbol s_2_1502[4] = { 'a', 'n', 'i', 'n' }; +static const symbol s_2_1503[2] = { 'a', 'o' }; +static const symbol s_2_1504[4] = { 'a', 'c', 'a', 'o' }; +static const symbol s_2_1505[7] = { 'a', 's', 't', 'a', 'j', 'a', 'o' }; +static const symbol s_2_1506[7] = { 'i', 's', 't', 'a', 'j', 'a', 'o' }; +static const symbol s_2_1507[7] = { 'o', 's', 't', 'a', 'j', 'a', 'o' }; +static const symbol s_2_1508[5] = { 'i', 'n', 'j', 'a', 'o' }; +static const symbol s_2_1509[4] = { 'i', 'r', 'a', 'o' }; +static const symbol s_2_1510[4] = { 'u', 'r', 'a', 'o' }; +static const symbol s_2_1511[3] = { 't', 'a', 'o' }; +static const symbol s_2_1512[5] = { 'a', 's', 't', 'a', 'o' }; +static const symbol s_2_1513[5] = { 'i', 's', 't', 'a', 'o' }; +static const symbol s_2_1514[5] = { 'o', 's', 't', 'a', 'o' }; +static const symbol s_2_1515[4] = { 'a', 'v', 'a', 'o' }; +static const symbol s_2_1516[4] = { 'e', 'v', 'a', 'o' }; +static const symbol s_2_1517[4] = { 'i', 'v', 'a', 'o' }; +static const symbol s_2_1518[4] = { 'o', 'v', 'a', 'o' }; +static const symbol s_2_1519[4] = { 'u', 'v', 'a', 'o' }; +static const symbol s_2_1520[5] = { 'a', 0xC4, 0x8D, 'a', 'o' }; +static const symbol s_2_1521[2] = { 'g', 'o' }; +static const symbol s_2_1522[3] = { 'u', 'g', 'o' }; +static const symbol s_2_1523[2] = { 'i', 'o' }; +static const symbol s_2_1524[4] = { 'a', 'c', 'i', 'o' }; +static const symbol s_2_1525[5] = { 'l', 'u', 'c', 'i', 'o' }; +static const symbol s_2_1526[3] = { 'l', 'i', 'o' }; +static const symbol s_2_1527[3] = { 'n', 'i', 'o' }; +static const symbol s_2_1528[5] = { 'r', 'a', 'r', 'i', 'o' }; +static const symbol s_2_1529[3] = { 's', 'i', 'o' }; +static const symbol s_2_1530[5] = { 'r', 'o', 's', 'i', 'o' }; +static const symbol s_2_1531[5] = { 'j', 'e', 't', 'i', 'o' }; +static const symbol s_2_1532[4] = { 'o', 't', 'i', 'o' }; +static const symbol s_2_1533[5] = { 'a', 0xC4, 0x8D, 'i', 'o' }; +static const symbol s_2_1534[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 'o' }; +static const symbol s_2_1535[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 'o' }; +static const symbol s_2_1536[4] = { 'b', 'i', 'j', 'o' }; +static const symbol s_2_1537[4] = { 'c', 'i', 'j', 'o' }; +static const symbol s_2_1538[4] = { 'd', 'i', 'j', 'o' }; +static const symbol s_2_1539[4] = { 'f', 'i', 'j', 'o' }; +static const symbol s_2_1540[4] = { 'g', 'i', 'j', 'o' }; +static const symbol s_2_1541[4] = { 'l', 'i', 'j', 'o' }; +static const symbol s_2_1542[4] = { 'm', 'i', 'j', 'o' }; +static const symbol s_2_1543[4] = { 'n', 'i', 'j', 'o' }; +static const symbol s_2_1544[4] = { 'p', 'i', 'j', 'o' }; +static const symbol s_2_1545[4] = { 'r', 'i', 'j', 'o' }; +static const symbol s_2_1546[4] = { 's', 'i', 'j', 'o' }; +static const symbol s_2_1547[4] = { 't', 'i', 'j', 'o' }; +static const symbol s_2_1548[4] = { 'z', 'i', 'j', 'o' }; +static const symbol s_2_1549[5] = { 0xC5, 0xBE, 'i', 'j', 'o' }; +static const symbol s_2_1550[4] = { 'a', 'n', 'j', 'o' }; +static const symbol s_2_1551[4] = { 'e', 'n', 'j', 'o' }; +static const symbol s_2_1552[4] = { 's', 'n', 'j', 'o' }; +static const symbol s_2_1553[5] = { 0xC5, 0xA1, 'n', 'j', 'o' }; +static const symbol s_2_1554[2] = { 'k', 'o' }; +static const symbol s_2_1555[3] = { 's', 'k', 'o' }; +static const symbol s_2_1556[4] = { 0xC5, 0xA1, 'k', 'o' }; +static const symbol s_2_1557[3] = { 'a', 'l', 'o' }; +static const symbol s_2_1558[5] = { 'a', 'c', 'a', 'l', 'o' }; +static const symbol s_2_1559[8] = { 'a', 's', 't', 'a', 'j', 'a', 'l', 'o' }; +static const symbol s_2_1560[8] = { 'i', 's', 't', 'a', 'j', 'a', 'l', 'o' }; +static const symbol s_2_1561[8] = { 'o', 's', 't', 'a', 'j', 'a', 'l', 'o' }; +static const symbol s_2_1562[5] = { 'i', 'j', 'a', 'l', 'o' }; +static const symbol s_2_1563[6] = { 'i', 'n', 'j', 'a', 'l', 'o' }; +static const symbol s_2_1564[4] = { 'n', 'a', 'l', 'o' }; +static const symbol s_2_1565[5] = { 'i', 'r', 'a', 'l', 'o' }; +static const symbol s_2_1566[5] = { 'u', 'r', 'a', 'l', 'o' }; +static const symbol s_2_1567[4] = { 't', 'a', 'l', 'o' }; +static const symbol s_2_1568[6] = { 'a', 's', 't', 'a', 'l', 'o' }; +static const symbol s_2_1569[6] = { 'i', 's', 't', 'a', 'l', 'o' }; +static const symbol s_2_1570[6] = { 'o', 's', 't', 'a', 'l', 'o' }; +static const symbol s_2_1571[5] = { 'a', 'v', 'a', 'l', 'o' }; +static const symbol s_2_1572[5] = { 'e', 'v', 'a', 'l', 'o' }; +static const symbol s_2_1573[5] = { 'i', 'v', 'a', 'l', 'o' }; +static const symbol s_2_1574[5] = { 'o', 'v', 'a', 'l', 'o' }; +static const symbol s_2_1575[5] = { 'u', 'v', 'a', 'l', 'o' }; +static const symbol s_2_1576[6] = { 'a', 0xC4, 0x8D, 'a', 'l', 'o' }; +static const symbol s_2_1577[3] = { 'e', 'l', 'o' }; +static const symbol s_2_1578[3] = { 'i', 'l', 'o' }; +static const symbol s_2_1579[5] = { 'a', 'c', 'i', 'l', 'o' }; +static const symbol s_2_1580[6] = { 'l', 'u', 'c', 'i', 'l', 'o' }; +static const symbol s_2_1581[4] = { 'n', 'i', 'l', 'o' }; +static const symbol s_2_1582[6] = { 'r', 'o', 's', 'i', 'l', 'o' }; +static const symbol s_2_1583[6] = { 'j', 'e', 't', 'i', 'l', 'o' }; +static const symbol s_2_1584[6] = { 'a', 0xC4, 0x8D, 'i', 'l', 'o' }; +static const symbol s_2_1585[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'l', 'o' }; +static const symbol s_2_1586[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'l', 'o' }; +static const symbol s_2_1587[4] = { 'a', 's', 'l', 'o' }; +static const symbol s_2_1588[4] = { 'n', 'u', 'l', 'o' }; +static const symbol s_2_1589[3] = { 'a', 'm', 'o' }; +static const symbol s_2_1590[5] = { 'a', 'c', 'a', 'm', 'o' }; +static const symbol s_2_1591[4] = { 'r', 'a', 'm', 'o' }; +static const symbol s_2_1592[5] = { 'i', 'r', 'a', 'm', 'o' }; +static const symbol s_2_1593[5] = { 'u', 'r', 'a', 'm', 'o' }; +static const symbol s_2_1594[4] = { 't', 'a', 'm', 'o' }; +static const symbol s_2_1595[5] = { 'a', 'v', 'a', 'm', 'o' }; +static const symbol s_2_1596[5] = { 'e', 'v', 'a', 'm', 'o' }; +static const symbol s_2_1597[5] = { 'i', 'v', 'a', 'm', 'o' }; +static const symbol s_2_1598[5] = { 'u', 'v', 'a', 'm', 'o' }; +static const symbol s_2_1599[6] = { 'a', 0xC4, 0x8D, 'a', 'm', 'o' }; +static const symbol s_2_1600[3] = { 'e', 'm', 'o' }; +static const symbol s_2_1601[8] = { 'a', 's', 't', 'a', 'd', 'e', 'm', 'o' }; +static const symbol s_2_1602[8] = { 'i', 's', 't', 'a', 'd', 'e', 'm', 'o' }; +static const symbol s_2_1603[8] = { 'o', 's', 't', 'a', 'd', 'e', 'm', 'o' }; +static const symbol s_2_1604[8] = { 'a', 's', 't', 'a', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1605[8] = { 'i', 's', 't', 'a', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1606[8] = { 'o', 's', 't', 'a', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1607[5] = { 'i', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1608[6] = { 'i', 'n', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1609[5] = { 'u', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1610[8] = { 'l', 'u', 'c', 'u', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1611[7] = { 'i', 'r', 'u', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1612[9] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 'm', 'o' }; +static const symbol s_2_1613[4] = { 'l', 'e', 'm', 'o' }; +static const symbol s_2_1614[4] = { 'n', 'e', 'm', 'o' }; +static const symbol s_2_1615[8] = { 'a', 's', 't', 'a', 'n', 'e', 'm', 'o' }; +static const symbol s_2_1616[8] = { 'i', 's', 't', 'a', 'n', 'e', 'm', 'o' }; +static const symbol s_2_1617[8] = { 'o', 's', 't', 'a', 'n', 'e', 'm', 'o' }; +static const symbol s_2_1618[5] = { 'e', 't', 'e', 'm', 'o' }; +static const symbol s_2_1619[6] = { 'a', 's', 't', 'e', 'm', 'o' }; +static const symbol s_2_1620[3] = { 'i', 'm', 'o' }; +static const symbol s_2_1621[5] = { 'a', 'c', 'i', 'm', 'o' }; +static const symbol s_2_1622[6] = { 'l', 'u', 'c', 'i', 'm', 'o' }; +static const symbol s_2_1623[4] = { 'n', 'i', 'm', 'o' }; +static const symbol s_2_1624[8] = { 'a', 's', 't', 'a', 'n', 'i', 'm', 'o' }; +static const symbol s_2_1625[8] = { 'i', 's', 't', 'a', 'n', 'i', 'm', 'o' }; +static const symbol s_2_1626[8] = { 'o', 's', 't', 'a', 'n', 'i', 'm', 'o' }; +static const symbol s_2_1627[6] = { 'r', 'o', 's', 'i', 'm', 'o' }; +static const symbol s_2_1628[5] = { 'e', 't', 'i', 'm', 'o' }; +static const symbol s_2_1629[6] = { 'j', 'e', 't', 'i', 'm', 'o' }; +static const symbol s_2_1630[6] = { 'a', 's', 't', 'i', 'm', 'o' }; +static const symbol s_2_1631[6] = { 'a', 0xC4, 0x8D, 'i', 'm', 'o' }; +static const symbol s_2_1632[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 'm', 'o' }; +static const symbol s_2_1633[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 'm', 'o' }; +static const symbol s_2_1634[4] = { 'a', 'j', 'm', 'o' }; +static const symbol s_2_1635[6] = { 'u', 'r', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1636[5] = { 't', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1637[7] = { 'a', 's', 't', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1638[7] = { 'i', 's', 't', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1639[7] = { 'o', 's', 't', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1640[6] = { 'a', 'v', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1641[6] = { 'e', 'v', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1642[6] = { 'i', 'v', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1643[6] = { 'u', 'v', 'a', 'j', 'm', 'o' }; +static const symbol s_2_1644[4] = { 'i', 'j', 'm', 'o' }; +static const symbol s_2_1645[4] = { 'u', 'j', 'm', 'o' }; +static const symbol s_2_1646[7] = { 'l', 'u', 'c', 'u', 'j', 'm', 'o' }; +static const symbol s_2_1647[6] = { 'i', 'r', 'u', 'j', 'm', 'o' }; +static const symbol s_2_1648[8] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'm', 'o' }; +static const symbol s_2_1649[4] = { 'a', 's', 'm', 'o' }; +static const symbol s_2_1650[6] = { 'a', 'c', 'a', 's', 'm', 'o' }; +static const symbol s_2_1651[9] = { 'a', 's', 't', 'a', 'j', 'a', 's', 'm', 'o' }; +static const symbol s_2_1652[9] = { 'i', 's', 't', 'a', 'j', 'a', 's', 'm', 'o' }; +static const symbol s_2_1653[9] = { 'o', 's', 't', 'a', 'j', 'a', 's', 'm', 'o' }; +static const symbol s_2_1654[7] = { 'i', 'n', 'j', 'a', 's', 'm', 'o' }; +static const symbol s_2_1655[6] = { 'i', 'r', 'a', 's', 'm', 'o' }; +static const symbol s_2_1656[6] = { 'u', 'r', 'a', 's', 'm', 'o' }; +static const symbol s_2_1657[5] = { 't', 'a', 's', 'm', 'o' }; +static const symbol s_2_1658[6] = { 'a', 'v', 'a', 's', 'm', 'o' }; +static const symbol s_2_1659[6] = { 'e', 'v', 'a', 's', 'm', 'o' }; +static const symbol s_2_1660[6] = { 'i', 'v', 'a', 's', 'm', 'o' }; +static const symbol s_2_1661[6] = { 'o', 'v', 'a', 's', 'm', 'o' }; +static const symbol s_2_1662[6] = { 'u', 'v', 'a', 's', 'm', 'o' }; +static const symbol s_2_1663[7] = { 'a', 0xC4, 0x8D, 'a', 's', 'm', 'o' }; +static const symbol s_2_1664[4] = { 'i', 's', 'm', 'o' }; +static const symbol s_2_1665[6] = { 'a', 'c', 'i', 's', 'm', 'o' }; +static const symbol s_2_1666[7] = { 'l', 'u', 'c', 'i', 's', 'm', 'o' }; +static const symbol s_2_1667[5] = { 'n', 'i', 's', 'm', 'o' }; +static const symbol s_2_1668[7] = { 'r', 'o', 's', 'i', 's', 'm', 'o' }; +static const symbol s_2_1669[7] = { 'j', 'e', 't', 'i', 's', 'm', 'o' }; +static const symbol s_2_1670[7] = { 'a', 0xC4, 0x8D, 'i', 's', 'm', 'o' }; +static const symbol s_2_1671[8] = { 'l', 'u', 0xC4, 0x8D, 'i', 's', 'm', 'o' }; +static const symbol s_2_1672[8] = { 'r', 'o', 0xC5, 0xA1, 'i', 's', 'm', 'o' }; +static const symbol s_2_1673[9] = { 'a', 's', 't', 'a', 'd', 'o', 's', 'm', 'o' }; +static const symbol s_2_1674[9] = { 'i', 's', 't', 'a', 'd', 'o', 's', 'm', 'o' }; +static const symbol s_2_1675[9] = { 'o', 's', 't', 'a', 'd', 'o', 's', 'm', 'o' }; +static const symbol s_2_1676[5] = { 'n', 'u', 's', 'm', 'o' }; +static const symbol s_2_1677[2] = { 'n', 'o' }; +static const symbol s_2_1678[3] = { 'a', 'n', 'o' }; +static const symbol s_2_1679[5] = { 'a', 'c', 'a', 'n', 'o' }; +static const symbol s_2_1680[5] = { 'u', 'r', 'a', 'n', 'o' }; +static const symbol s_2_1681[4] = { 't', 'a', 'n', 'o' }; +static const symbol s_2_1682[5] = { 'a', 'v', 'a', 'n', 'o' }; +static const symbol s_2_1683[5] = { 'e', 'v', 'a', 'n', 'o' }; +static const symbol s_2_1684[5] = { 'i', 'v', 'a', 'n', 'o' }; +static const symbol s_2_1685[5] = { 'u', 'v', 'a', 'n', 'o' }; +static const symbol s_2_1686[6] = { 'a', 0xC4, 0x8D, 'a', 'n', 'o' }; +static const symbol s_2_1687[5] = { 'a', 'c', 'e', 'n', 'o' }; +static const symbol s_2_1688[6] = { 'l', 'u', 'c', 'e', 'n', 'o' }; +static const symbol s_2_1689[6] = { 'a', 0xC4, 0x8D, 'e', 'n', 'o' }; +static const symbol s_2_1690[7] = { 'l', 'u', 0xC4, 0x8D, 'e', 'n', 'o' }; +static const symbol s_2_1691[3] = { 'i', 'n', 'o' }; +static const symbol s_2_1692[4] = { 'c', 'i', 'n', 'o' }; +static const symbol s_2_1693[5] = { 0xC4, 0x8D, 'i', 'n', 'o' }; +static const symbol s_2_1694[3] = { 'a', 't', 'o' }; +static const symbol s_2_1695[5] = { 'i', 'k', 'a', 't', 'o' }; +static const symbol s_2_1696[4] = { 'l', 'a', 't', 'o' }; +static const symbol s_2_1697[3] = { 'e', 't', 'o' }; +static const symbol s_2_1698[5] = { 'e', 'v', 'i', 't', 'o' }; +static const symbol s_2_1699[5] = { 'o', 'v', 'i', 't', 'o' }; +static const symbol s_2_1700[4] = { 'a', 's', 't', 'o' }; +static const symbol s_2_1701[4] = { 'e', 's', 't', 'o' }; +static const symbol s_2_1702[4] = { 'i', 's', 't', 'o' }; +static const symbol s_2_1703[4] = { 'k', 's', 't', 'o' }; +static const symbol s_2_1704[4] = { 'o', 's', 't', 'o' }; +static const symbol s_2_1705[4] = { 'n', 'u', 't', 'o' }; +static const symbol s_2_1706[3] = { 'n', 'u', 'o' }; +static const symbol s_2_1707[3] = { 'a', 'v', 'o' }; +static const symbol s_2_1708[3] = { 'e', 'v', 'o' }; +static const symbol s_2_1709[3] = { 'i', 'v', 'o' }; +static const symbol s_2_1710[3] = { 'o', 'v', 'o' }; +static const symbol s_2_1711[4] = { 's', 't', 'v', 'o' }; +static const symbol s_2_1712[5] = { 0xC5, 0xA1, 't', 'v', 'o' }; +static const symbol s_2_1713[2] = { 'a', 's' }; +static const symbol s_2_1714[4] = { 'a', 'c', 'a', 's' }; +static const symbol s_2_1715[4] = { 'i', 'r', 'a', 's' }; +static const symbol s_2_1716[4] = { 'u', 'r', 'a', 's' }; +static const symbol s_2_1717[3] = { 't', 'a', 's' }; +static const symbol s_2_1718[4] = { 'a', 'v', 'a', 's' }; +static const symbol s_2_1719[4] = { 'e', 'v', 'a', 's' }; +static const symbol s_2_1720[4] = { 'i', 'v', 'a', 's' }; +static const symbol s_2_1721[4] = { 'u', 'v', 'a', 's' }; +static const symbol s_2_1722[2] = { 'e', 's' }; +static const symbol s_2_1723[7] = { 'a', 's', 't', 'a', 'd', 'e', 's' }; +static const symbol s_2_1724[7] = { 'i', 's', 't', 'a', 'd', 'e', 's' }; +static const symbol s_2_1725[7] = { 'o', 's', 't', 'a', 'd', 'e', 's' }; +static const symbol s_2_1726[7] = { 'a', 's', 't', 'a', 'j', 'e', 's' }; +static const symbol s_2_1727[7] = { 'i', 's', 't', 'a', 'j', 'e', 's' }; +static const symbol s_2_1728[7] = { 'o', 's', 't', 'a', 'j', 'e', 's' }; +static const symbol s_2_1729[4] = { 'i', 'j', 'e', 's' }; +static const symbol s_2_1730[5] = { 'i', 'n', 'j', 'e', 's' }; +static const symbol s_2_1731[4] = { 'u', 'j', 'e', 's' }; +static const symbol s_2_1732[7] = { 'l', 'u', 'c', 'u', 'j', 'e', 's' }; +static const symbol s_2_1733[6] = { 'i', 'r', 'u', 'j', 'e', 's' }; +static const symbol s_2_1734[3] = { 'n', 'e', 's' }; +static const symbol s_2_1735[7] = { 'a', 's', 't', 'a', 'n', 'e', 's' }; +static const symbol s_2_1736[7] = { 'i', 's', 't', 'a', 'n', 'e', 's' }; +static const symbol s_2_1737[7] = { 'o', 's', 't', 'a', 'n', 'e', 's' }; +static const symbol s_2_1738[4] = { 'e', 't', 'e', 's' }; +static const symbol s_2_1739[5] = { 'a', 's', 't', 'e', 's' }; +static const symbol s_2_1740[2] = { 'i', 's' }; +static const symbol s_2_1741[4] = { 'a', 'c', 'i', 's' }; +static const symbol s_2_1742[5] = { 'l', 'u', 'c', 'i', 's' }; +static const symbol s_2_1743[3] = { 'n', 'i', 's' }; +static const symbol s_2_1744[5] = { 'r', 'o', 's', 'i', 's' }; +static const symbol s_2_1745[5] = { 'j', 'e', 't', 'i', 's' }; +static const symbol s_2_1746[2] = { 'a', 't' }; +static const symbol s_2_1747[4] = { 'a', 'c', 'a', 't' }; +static const symbol s_2_1748[7] = { 'a', 's', 't', 'a', 'j', 'a', 't' }; +static const symbol s_2_1749[7] = { 'i', 's', 't', 'a', 'j', 'a', 't' }; +static const symbol s_2_1750[7] = { 'o', 's', 't', 'a', 'j', 'a', 't' }; +static const symbol s_2_1751[5] = { 'i', 'n', 'j', 'a', 't' }; +static const symbol s_2_1752[4] = { 'i', 'r', 'a', 't' }; +static const symbol s_2_1753[4] = { 'u', 'r', 'a', 't' }; +static const symbol s_2_1754[3] = { 't', 'a', 't' }; +static const symbol s_2_1755[5] = { 'a', 's', 't', 'a', 't' }; +static const symbol s_2_1756[5] = { 'i', 's', 't', 'a', 't' }; +static const symbol s_2_1757[5] = { 'o', 's', 't', 'a', 't' }; +static const symbol s_2_1758[4] = { 'a', 'v', 'a', 't' }; +static const symbol s_2_1759[4] = { 'e', 'v', 'a', 't' }; +static const symbol s_2_1760[4] = { 'i', 'v', 'a', 't' }; +static const symbol s_2_1761[6] = { 'i', 'r', 'i', 'v', 'a', 't' }; +static const symbol s_2_1762[4] = { 'o', 'v', 'a', 't' }; +static const symbol s_2_1763[4] = { 'u', 'v', 'a', 't' }; +static const symbol s_2_1764[5] = { 'a', 0xC4, 0x8D, 'a', 't' }; +static const symbol s_2_1765[2] = { 'i', 't' }; +static const symbol s_2_1766[4] = { 'a', 'c', 'i', 't' }; +static const symbol s_2_1767[5] = { 'l', 'u', 'c', 'i', 't' }; +static const symbol s_2_1768[5] = { 'r', 'o', 's', 'i', 't' }; +static const symbol s_2_1769[5] = { 'j', 'e', 't', 'i', 't' }; +static const symbol s_2_1770[5] = { 'a', 0xC4, 0x8D, 'i', 't' }; +static const symbol s_2_1771[6] = { 'l', 'u', 0xC4, 0x8D, 'i', 't' }; +static const symbol s_2_1772[6] = { 'r', 'o', 0xC5, 0xA1, 'i', 't' }; +static const symbol s_2_1773[3] = { 'n', 'u', 't' }; +static const symbol s_2_1774[6] = { 'a', 's', 't', 'a', 'd', 'u' }; +static const symbol s_2_1775[6] = { 'i', 's', 't', 'a', 'd', 'u' }; +static const symbol s_2_1776[6] = { 'o', 's', 't', 'a', 'd', 'u' }; +static const symbol s_2_1777[2] = { 'g', 'u' }; +static const symbol s_2_1778[4] = { 'l', 'o', 'g', 'u' }; +static const symbol s_2_1779[3] = { 'u', 'g', 'u' }; +static const symbol s_2_1780[3] = { 'a', 'h', 'u' }; +static const symbol s_2_1781[5] = { 'a', 'c', 'a', 'h', 'u' }; +static const symbol s_2_1782[8] = { 'a', 's', 't', 'a', 'j', 'a', 'h', 'u' }; +static const symbol s_2_1783[8] = { 'i', 's', 't', 'a', 'j', 'a', 'h', 'u' }; +static const symbol s_2_1784[8] = { 'o', 's', 't', 'a', 'j', 'a', 'h', 'u' }; +static const symbol s_2_1785[6] = { 'i', 'n', 'j', 'a', 'h', 'u' }; +static const symbol s_2_1786[5] = { 'i', 'r', 'a', 'h', 'u' }; +static const symbol s_2_1787[5] = { 'u', 'r', 'a', 'h', 'u' }; +static const symbol s_2_1788[5] = { 'a', 'v', 'a', 'h', 'u' }; +static const symbol s_2_1789[5] = { 'e', 'v', 'a', 'h', 'u' }; +static const symbol s_2_1790[5] = { 'i', 'v', 'a', 'h', 'u' }; +static const symbol s_2_1791[5] = { 'o', 'v', 'a', 'h', 'u' }; +static const symbol s_2_1792[5] = { 'u', 'v', 'a', 'h', 'u' }; +static const symbol s_2_1793[6] = { 'a', 0xC4, 0x8D, 'a', 'h', 'u' }; +static const symbol s_2_1794[3] = { 'a', 'j', 'u' }; +static const symbol s_2_1795[4] = { 'c', 'a', 'j', 'u' }; +static const symbol s_2_1796[5] = { 'a', 'c', 'a', 'j', 'u' }; +static const symbol s_2_1797[4] = { 'l', 'a', 'j', 'u' }; +static const symbol s_2_1798[4] = { 'r', 'a', 'j', 'u' }; +static const symbol s_2_1799[5] = { 'i', 'r', 'a', 'j', 'u' }; +static const symbol s_2_1800[5] = { 'u', 'r', 'a', 'j', 'u' }; +static const symbol s_2_1801[4] = { 't', 'a', 'j', 'u' }; +static const symbol s_2_1802[6] = { 'a', 's', 't', 'a', 'j', 'u' }; +static const symbol s_2_1803[6] = { 'i', 's', 't', 'a', 'j', 'u' }; +static const symbol s_2_1804[6] = { 'o', 's', 't', 'a', 'j', 'u' }; +static const symbol s_2_1805[5] = { 'a', 'v', 'a', 'j', 'u' }; +static const symbol s_2_1806[5] = { 'e', 'v', 'a', 'j', 'u' }; +static const symbol s_2_1807[5] = { 'i', 'v', 'a', 'j', 'u' }; +static const symbol s_2_1808[5] = { 'u', 'v', 'a', 'j', 'u' }; +static const symbol s_2_1809[5] = { 0xC4, 0x87, 'a', 'j', 'u' }; +static const symbol s_2_1810[5] = { 0xC4, 0x8D, 'a', 'j', 'u' }; +static const symbol s_2_1811[6] = { 'a', 0xC4, 0x8D, 'a', 'j', 'u' }; +static const symbol s_2_1812[5] = { 0xC4, 0x91, 'a', 'j', 'u' }; +static const symbol s_2_1813[3] = { 'i', 'j', 'u' }; +static const symbol s_2_1814[4] = { 'b', 'i', 'j', 'u' }; +static const symbol s_2_1815[4] = { 'c', 'i', 'j', 'u' }; +static const symbol s_2_1816[4] = { 'd', 'i', 'j', 'u' }; +static const symbol s_2_1817[4] = { 'f', 'i', 'j', 'u' }; +static const symbol s_2_1818[4] = { 'g', 'i', 'j', 'u' }; +static const symbol s_2_1819[6] = { 'a', 'n', 'j', 'i', 'j', 'u' }; +static const symbol s_2_1820[6] = { 'e', 'n', 'j', 'i', 'j', 'u' }; +static const symbol s_2_1821[6] = { 's', 'n', 'j', 'i', 'j', 'u' }; +static const symbol s_2_1822[7] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'u' }; +static const symbol s_2_1823[4] = { 'k', 'i', 'j', 'u' }; +static const symbol s_2_1824[4] = { 'l', 'i', 'j', 'u' }; +static const symbol s_2_1825[5] = { 'e', 'l', 'i', 'j', 'u' }; +static const symbol s_2_1826[4] = { 'm', 'i', 'j', 'u' }; +static const symbol s_2_1827[4] = { 'n', 'i', 'j', 'u' }; +static const symbol s_2_1828[6] = { 'g', 'a', 'n', 'i', 'j', 'u' }; +static const symbol s_2_1829[6] = { 'm', 'a', 'n', 'i', 'j', 'u' }; +static const symbol s_2_1830[6] = { 'p', 'a', 'n', 'i', 'j', 'u' }; +static const symbol s_2_1831[6] = { 'r', 'a', 'n', 'i', 'j', 'u' }; +static const symbol s_2_1832[6] = { 't', 'a', 'n', 'i', 'j', 'u' }; +static const symbol s_2_1833[4] = { 'p', 'i', 'j', 'u' }; +static const symbol s_2_1834[4] = { 'r', 'i', 'j', 'u' }; +static const symbol s_2_1835[6] = { 'r', 'a', 'r', 'i', 'j', 'u' }; +static const symbol s_2_1836[4] = { 's', 'i', 'j', 'u' }; +static const symbol s_2_1837[5] = { 'o', 's', 'i', 'j', 'u' }; +static const symbol s_2_1838[4] = { 't', 'i', 'j', 'u' }; +static const symbol s_2_1839[5] = { 'a', 't', 'i', 'j', 'u' }; +static const symbol s_2_1840[5] = { 'o', 't', 'i', 'j', 'u' }; +static const symbol s_2_1841[5] = { 'a', 'v', 'i', 'j', 'u' }; +static const symbol s_2_1842[5] = { 'e', 'v', 'i', 'j', 'u' }; +static const symbol s_2_1843[5] = { 'i', 'v', 'i', 'j', 'u' }; +static const symbol s_2_1844[5] = { 'o', 'v', 'i', 'j', 'u' }; +static const symbol s_2_1845[4] = { 'z', 'i', 'j', 'u' }; +static const symbol s_2_1846[6] = { 'o', 0xC5, 0xA1, 'i', 'j', 'u' }; +static const symbol s_2_1847[5] = { 0xC5, 0xBE, 'i', 'j', 'u' }; +static const symbol s_2_1848[4] = { 'a', 'n', 'j', 'u' }; +static const symbol s_2_1849[4] = { 'e', 'n', 'j', 'u' }; +static const symbol s_2_1850[4] = { 's', 'n', 'j', 'u' }; +static const symbol s_2_1851[5] = { 0xC5, 0xA1, 'n', 'j', 'u' }; +static const symbol s_2_1852[3] = { 'u', 'j', 'u' }; +static const symbol s_2_1853[6] = { 'l', 'u', 'c', 'u', 'j', 'u' }; +static const symbol s_2_1854[5] = { 'i', 'r', 'u', 'j', 'u' }; +static const symbol s_2_1855[7] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'u' }; +static const symbol s_2_1856[2] = { 'k', 'u' }; +static const symbol s_2_1857[3] = { 's', 'k', 'u' }; +static const symbol s_2_1858[4] = { 0xC5, 0xA1, 'k', 'u' }; +static const symbol s_2_1859[3] = { 'a', 'l', 'u' }; +static const symbol s_2_1860[5] = { 'i', 'j', 'a', 'l', 'u' }; +static const symbol s_2_1861[4] = { 'n', 'a', 'l', 'u' }; +static const symbol s_2_1862[3] = { 'e', 'l', 'u' }; +static const symbol s_2_1863[3] = { 'i', 'l', 'u' }; +static const symbol s_2_1864[5] = { 'o', 'z', 'i', 'l', 'u' }; +static const symbol s_2_1865[3] = { 'o', 'l', 'u' }; +static const symbol s_2_1866[4] = { 'r', 'a', 'm', 'u' }; +static const symbol s_2_1867[5] = { 'a', 'c', 'e', 'm', 'u' }; +static const symbol s_2_1868[5] = { 'e', 'c', 'e', 'm', 'u' }; +static const symbol s_2_1869[5] = { 'u', 'c', 'e', 'm', 'u' }; +static const symbol s_2_1870[8] = { 'a', 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1871[8] = { 'e', 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1872[8] = { 's', 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1873[9] = { 0xC5, 0xA1, 'n', 'j', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1874[6] = { 'k', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1875[7] = { 's', 'k', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1876[8] = { 0xC5, 0xA1, 'k', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1877[7] = { 'e', 'l', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1878[6] = { 'n', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1879[7] = { 'o', 's', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1880[7] = { 'a', 't', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1881[9] = { 'e', 'v', 'i', 't', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1882[9] = { 'o', 'v', 'i', 't', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1883[8] = { 'a', 's', 't', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1884[7] = { 'a', 'v', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1885[7] = { 'e', 'v', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1886[7] = { 'i', 'v', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1887[7] = { 'o', 'v', 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1888[8] = { 'o', 0xC5, 0xA1, 'i', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1889[6] = { 'a', 'n', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1890[6] = { 'e', 'n', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1891[6] = { 's', 'n', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1892[7] = { 0xC5, 0xA1, 'n', 'j', 'e', 'm', 'u' }; +static const symbol s_2_1893[4] = { 'k', 'e', 'm', 'u' }; +static const symbol s_2_1894[5] = { 's', 'k', 'e', 'm', 'u' }; +static const symbol s_2_1895[6] = { 0xC5, 0xA1, 'k', 'e', 'm', 'u' }; +static const symbol s_2_1896[4] = { 'l', 'e', 'm', 'u' }; +static const symbol s_2_1897[5] = { 'e', 'l', 'e', 'm', 'u' }; +static const symbol s_2_1898[4] = { 'n', 'e', 'm', 'u' }; +static const symbol s_2_1899[5] = { 'a', 'n', 'e', 'm', 'u' }; +static const symbol s_2_1900[5] = { 'e', 'n', 'e', 'm', 'u' }; +static const symbol s_2_1901[5] = { 's', 'n', 'e', 'm', 'u' }; +static const symbol s_2_1902[6] = { 0xC5, 0xA1, 'n', 'e', 'm', 'u' }; +static const symbol s_2_1903[5] = { 'o', 's', 'e', 'm', 'u' }; +static const symbol s_2_1904[5] = { 'a', 't', 'e', 'm', 'u' }; +static const symbol s_2_1905[7] = { 'e', 'v', 'i', 't', 'e', 'm', 'u' }; +static const symbol s_2_1906[7] = { 'o', 'v', 'i', 't', 'e', 'm', 'u' }; +static const symbol s_2_1907[6] = { 'a', 's', 't', 'e', 'm', 'u' }; +static const symbol s_2_1908[5] = { 'a', 'v', 'e', 'm', 'u' }; +static const symbol s_2_1909[5] = { 'e', 'v', 'e', 'm', 'u' }; +static const symbol s_2_1910[5] = { 'i', 'v', 'e', 'm', 'u' }; +static const symbol s_2_1911[5] = { 'o', 'v', 'e', 'm', 'u' }; +static const symbol s_2_1912[6] = { 'a', 0xC4, 0x87, 'e', 'm', 'u' }; +static const symbol s_2_1913[6] = { 'e', 0xC4, 0x87, 'e', 'm', 'u' }; +static const symbol s_2_1914[6] = { 'u', 0xC4, 0x87, 'e', 'm', 'u' }; +static const symbol s_2_1915[6] = { 'o', 0xC5, 0xA1, 'e', 'm', 'u' }; +static const symbol s_2_1916[5] = { 'a', 'c', 'o', 'm', 'u' }; +static const symbol s_2_1917[5] = { 'e', 'c', 'o', 'm', 'u' }; +static const symbol s_2_1918[5] = { 'u', 'c', 'o', 'm', 'u' }; +static const symbol s_2_1919[6] = { 'a', 'n', 'j', 'o', 'm', 'u' }; +static const symbol s_2_1920[6] = { 'e', 'n', 'j', 'o', 'm', 'u' }; +static const symbol s_2_1921[6] = { 's', 'n', 'j', 'o', 'm', 'u' }; +static const symbol s_2_1922[7] = { 0xC5, 0xA1, 'n', 'j', 'o', 'm', 'u' }; +static const symbol s_2_1923[4] = { 'k', 'o', 'm', 'u' }; +static const symbol s_2_1924[5] = { 's', 'k', 'o', 'm', 'u' }; +static const symbol s_2_1925[6] = { 0xC5, 0xA1, 'k', 'o', 'm', 'u' }; +static const symbol s_2_1926[5] = { 'e', 'l', 'o', 'm', 'u' }; +static const symbol s_2_1927[4] = { 'n', 'o', 'm', 'u' }; +static const symbol s_2_1928[6] = { 'c', 'i', 'n', 'o', 'm', 'u' }; +static const symbol s_2_1929[7] = { 0xC4, 0x8D, 'i', 'n', 'o', 'm', 'u' }; +static const symbol s_2_1930[5] = { 'o', 's', 'o', 'm', 'u' }; +static const symbol s_2_1931[5] = { 'a', 't', 'o', 'm', 'u' }; +static const symbol s_2_1932[7] = { 'e', 'v', 'i', 't', 'o', 'm', 'u' }; +static const symbol s_2_1933[7] = { 'o', 'v', 'i', 't', 'o', 'm', 'u' }; +static const symbol s_2_1934[6] = { 'a', 's', 't', 'o', 'm', 'u' }; +static const symbol s_2_1935[5] = { 'a', 'v', 'o', 'm', 'u' }; +static const symbol s_2_1936[5] = { 'e', 'v', 'o', 'm', 'u' }; +static const symbol s_2_1937[5] = { 'i', 'v', 'o', 'm', 'u' }; +static const symbol s_2_1938[5] = { 'o', 'v', 'o', 'm', 'u' }; +static const symbol s_2_1939[6] = { 'a', 0xC4, 0x87, 'o', 'm', 'u' }; +static const symbol s_2_1940[6] = { 'e', 0xC4, 0x87, 'o', 'm', 'u' }; +static const symbol s_2_1941[6] = { 'u', 0xC4, 0x87, 'o', 'm', 'u' }; +static const symbol s_2_1942[6] = { 'o', 0xC5, 0xA1, 'o', 'm', 'u' }; +static const symbol s_2_1943[2] = { 'n', 'u' }; +static const symbol s_2_1944[3] = { 'a', 'n', 'u' }; +static const symbol s_2_1945[6] = { 'a', 's', 't', 'a', 'n', 'u' }; +static const symbol s_2_1946[6] = { 'i', 's', 't', 'a', 'n', 'u' }; +static const symbol s_2_1947[6] = { 'o', 's', 't', 'a', 'n', 'u' }; +static const symbol s_2_1948[3] = { 'i', 'n', 'u' }; +static const symbol s_2_1949[4] = { 'c', 'i', 'n', 'u' }; +static const symbol s_2_1950[5] = { 'a', 'n', 'i', 'n', 'u' }; +static const symbol s_2_1951[5] = { 0xC4, 0x8D, 'i', 'n', 'u' }; +static const symbol s_2_1952[3] = { 'o', 'n', 'u' }; +static const symbol s_2_1953[3] = { 'a', 'r', 'u' }; +static const symbol s_2_1954[3] = { 'd', 'r', 'u' }; +static const symbol s_2_1955[3] = { 'e', 'r', 'u' }; +static const symbol s_2_1956[3] = { 'o', 'r', 'u' }; +static const symbol s_2_1957[4] = { 'b', 'a', 's', 'u' }; +static const symbol s_2_1958[4] = { 'g', 'a', 's', 'u' }; +static const symbol s_2_1959[4] = { 'j', 'a', 's', 'u' }; +static const symbol s_2_1960[4] = { 'k', 'a', 's', 'u' }; +static const symbol s_2_1961[4] = { 'n', 'a', 's', 'u' }; +static const symbol s_2_1962[4] = { 't', 'a', 's', 'u' }; +static const symbol s_2_1963[4] = { 'v', 'a', 's', 'u' }; +static const symbol s_2_1964[3] = { 'e', 's', 'u' }; +static const symbol s_2_1965[3] = { 'i', 's', 'u' }; +static const symbol s_2_1966[3] = { 'o', 's', 'u' }; +static const symbol s_2_1967[3] = { 'a', 't', 'u' }; +static const symbol s_2_1968[5] = { 'i', 'k', 'a', 't', 'u' }; +static const symbol s_2_1969[4] = { 'l', 'a', 't', 'u' }; +static const symbol s_2_1970[3] = { 'e', 't', 'u' }; +static const symbol s_2_1971[5] = { 'e', 'v', 'i', 't', 'u' }; +static const symbol s_2_1972[5] = { 'o', 'v', 'i', 't', 'u' }; +static const symbol s_2_1973[4] = { 'a', 's', 't', 'u' }; +static const symbol s_2_1974[4] = { 'e', 's', 't', 'u' }; +static const symbol s_2_1975[4] = { 'i', 's', 't', 'u' }; +static const symbol s_2_1976[4] = { 'k', 's', 't', 'u' }; +static const symbol s_2_1977[4] = { 'o', 's', 't', 'u' }; +static const symbol s_2_1978[5] = { 'i', 0xC5, 0xA1, 't', 'u' }; +static const symbol s_2_1979[3] = { 'a', 'v', 'u' }; +static const symbol s_2_1980[3] = { 'e', 'v', 'u' }; +static const symbol s_2_1981[3] = { 'i', 'v', 'u' }; +static const symbol s_2_1982[3] = { 'o', 'v', 'u' }; +static const symbol s_2_1983[4] = { 'l', 'o', 'v', 'u' }; +static const symbol s_2_1984[4] = { 'm', 'o', 'v', 'u' }; +static const symbol s_2_1985[4] = { 's', 't', 'v', 'u' }; +static const symbol s_2_1986[5] = { 0xC5, 0xA1, 't', 'v', 'u' }; +static const symbol s_2_1987[5] = { 'b', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1988[5] = { 'g', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1989[5] = { 'j', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1990[5] = { 'k', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1991[5] = { 'n', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1992[5] = { 't', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1993[5] = { 'v', 'a', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1994[4] = { 'e', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1995[4] = { 'i', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1996[4] = { 'o', 0xC5, 0xA1, 'u' }; +static const symbol s_2_1997[4] = { 'a', 'v', 'a', 'v' }; +static const symbol s_2_1998[4] = { 'e', 'v', 'a', 'v' }; +static const symbol s_2_1999[4] = { 'i', 'v', 'a', 'v' }; +static const symbol s_2_2000[4] = { 'u', 'v', 'a', 'v' }; +static const symbol s_2_2001[3] = { 'k', 'o', 'v' }; +static const symbol s_2_2002[3] = { 'a', 0xC5, 0xA1 }; +static const symbol s_2_2003[5] = { 'i', 'r', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2004[5] = { 'u', 'r', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2005[4] = { 't', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2006[5] = { 'a', 'v', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2007[5] = { 'e', 'v', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2008[5] = { 'i', 'v', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2009[5] = { 'u', 'v', 'a', 0xC5, 0xA1 }; +static const symbol s_2_2010[6] = { 'a', 0xC4, 0x8D, 'a', 0xC5, 0xA1 }; +static const symbol s_2_2011[3] = { 'e', 0xC5, 0xA1 }; +static const symbol s_2_2012[8] = { 'a', 's', 't', 'a', 'd', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2013[8] = { 'i', 's', 't', 'a', 'd', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2014[8] = { 'o', 's', 't', 'a', 'd', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2015[8] = { 'a', 's', 't', 'a', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2016[8] = { 'i', 's', 't', 'a', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2017[8] = { 'o', 's', 't', 'a', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2018[5] = { 'i', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2019[6] = { 'i', 'n', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2020[5] = { 'u', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2021[7] = { 'i', 'r', 'u', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2022[9] = { 'l', 'u', 0xC4, 0x8D, 'u', 'j', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2023[4] = { 'n', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2024[8] = { 'a', 's', 't', 'a', 'n', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2025[8] = { 'i', 's', 't', 'a', 'n', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2026[8] = { 'o', 's', 't', 'a', 'n', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2027[5] = { 'e', 't', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2028[6] = { 'a', 's', 't', 'e', 0xC5, 0xA1 }; +static const symbol s_2_2029[3] = { 'i', 0xC5, 0xA1 }; +static const symbol s_2_2030[4] = { 'n', 'i', 0xC5, 0xA1 }; +static const symbol s_2_2031[6] = { 'j', 'e', 't', 'i', 0xC5, 0xA1 }; +static const symbol s_2_2032[6] = { 'a', 0xC4, 0x8D, 'i', 0xC5, 0xA1 }; +static const symbol s_2_2033[7] = { 'l', 'u', 0xC4, 0x8D, 'i', 0xC5, 0xA1 }; +static const symbol s_2_2034[7] = { 'r', 'o', 0xC5, 0xA1, 'i', 0xC5, 0xA1 }; +static const struct among a_2[2035] = { +{ 3, s_2_0, 0, 124, 0}, +{ 3, s_2_1, 0, 125, 0}, +{ 3, s_2_2, 0, 126, 0}, +{ 2, s_2_3, 0, 20, 0}, +{ 5, s_2_4, -1, 124, 0}, +{ 5, s_2_5, -2, 125, 0}, +{ 5, s_2_6, -3, 126, 0}, +{ 8, s_2_7, -4, 84, 0}, +{ 8, s_2_8, -5, 85, 0}, +{ 8, s_2_9, -6, 122, 0}, +{ 9, s_2_10, -7, 86, 0}, +{ 6, s_2_11, -8, 95, 0}, +{ 7, s_2_12, -1, 1, 0}, +{ 8, s_2_13, -2, 2, 0}, +{ 7, s_2_14, -11, 83, 0}, +{ 6, s_2_15, -12, 13, 0}, +{ 7, s_2_16, -13, 123, 0}, +{ 7, s_2_17, -14, 120, 0}, +{ 9, s_2_18, -15, 92, 0}, +{ 9, s_2_19, -16, 93, 0}, +{ 8, s_2_20, -17, 94, 0}, +{ 7, s_2_21, -18, 77, 0}, +{ 7, s_2_22, -19, 78, 0}, +{ 7, s_2_23, -20, 79, 0}, +{ 7, s_2_24, -21, 80, 0}, +{ 8, s_2_25, -22, 91, 0}, +{ 6, s_2_26, -23, 84, 0}, +{ 6, s_2_27, -24, 85, 0}, +{ 6, s_2_28, -25, 122, 0}, +{ 7, s_2_29, -26, 86, 0}, +{ 4, s_2_30, -27, 95, 0}, +{ 5, s_2_31, -1, 1, 0}, +{ 6, s_2_32, -2, 2, 0}, +{ 5, s_2_33, -30, 83, 0}, +{ 4, s_2_34, -31, 13, 0}, +{ 5, s_2_35, -1, 10, 0}, +{ 5, s_2_36, -2, 87, 0}, +{ 5, s_2_37, -3, 159, 0}, +{ 6, s_2_38, -4, 88, 0}, +{ 5, s_2_39, -36, 123, 0}, +{ 5, s_2_40, -37, 120, 0}, +{ 7, s_2_41, -38, 92, 0}, +{ 7, s_2_42, -39, 93, 0}, +{ 6, s_2_43, -40, 94, 0}, +{ 5, s_2_44, -41, 77, 0}, +{ 5, s_2_45, -42, 78, 0}, +{ 5, s_2_46, -43, 79, 0}, +{ 5, s_2_47, -44, 80, 0}, +{ 6, s_2_48, -45, 14, 0}, +{ 6, s_2_49, -46, 15, 0}, +{ 6, s_2_50, -47, 16, 0}, +{ 6, s_2_51, -48, 91, 0}, +{ 5, s_2_52, -49, 124, 0}, +{ 5, s_2_53, -50, 125, 0}, +{ 5, s_2_54, -51, 126, 0}, +{ 6, s_2_55, -52, 84, 0}, +{ 6, s_2_56, -53, 85, 0}, +{ 6, s_2_57, -54, 122, 0}, +{ 7, s_2_58, -55, 86, 0}, +{ 4, s_2_59, -56, 95, 0}, +{ 5, s_2_60, -1, 1, 0}, +{ 6, s_2_61, -2, 2, 0}, +{ 4, s_2_62, -59, 19, 0}, +{ 5, s_2_63, -1, 83, 0}, +{ 4, s_2_64, -61, 13, 0}, +{ 6, s_2_65, -1, 137, 0}, +{ 7, s_2_66, -2, 89, 0}, +{ 5, s_2_67, -64, 123, 0}, +{ 5, s_2_68, -65, 120, 0}, +{ 7, s_2_69, -66, 92, 0}, +{ 7, s_2_70, -67, 93, 0}, +{ 6, s_2_71, -68, 94, 0}, +{ 5, s_2_72, -69, 77, 0}, +{ 5, s_2_73, -70, 78, 0}, +{ 5, s_2_74, -71, 79, 0}, +{ 5, s_2_75, -72, 80, 0}, +{ 6, s_2_76, -73, 14, 0}, +{ 6, s_2_77, -74, 15, 0}, +{ 6, s_2_78, -75, 16, 0}, +{ 6, s_2_79, -76, 91, 0}, +{ 3, s_2_80, -77, 18, 0}, +{ 3, s_2_81, 0, 109, 0}, +{ 4, s_2_82, -1, 26, 0}, +{ 4, s_2_83, -2, 30, 0}, +{ 4, s_2_84, -3, 31, 0}, +{ 5, s_2_85, -4, 28, 0}, +{ 5, s_2_86, -5, 27, 0}, +{ 5, s_2_87, -6, 29, 0}, +{ 4, s_2_88, 0, 32, 0}, +{ 4, s_2_89, 0, 33, 0}, +{ 4, s_2_90, 0, 34, 0}, +{ 4, s_2_91, 0, 40, 0}, +{ 4, s_2_92, 0, 39, 0}, +{ 6, s_2_93, 0, 84, 0}, +{ 6, s_2_94, 0, 85, 0}, +{ 6, s_2_95, 0, 122, 0}, +{ 7, s_2_96, 0, 86, 0}, +{ 4, s_2_97, 0, 95, 0}, +{ 5, s_2_98, -1, 1, 0}, +{ 6, s_2_99, -2, 2, 0}, +{ 4, s_2_100, 0, 24, 0}, +{ 5, s_2_101, -1, 83, 0}, +{ 4, s_2_102, 0, 37, 0}, +{ 4, s_2_103, 0, 13, 0}, +{ 6, s_2_104, -1, 9, 0}, +{ 6, s_2_105, -2, 6, 0}, +{ 6, s_2_106, -3, 7, 0}, +{ 6, s_2_107, -4, 8, 0}, +{ 6, s_2_108, -5, 5, 0}, +{ 4, s_2_109, 0, 41, 0}, +{ 4, s_2_110, 0, 42, 0}, +{ 6, s_2_111, -1, 21, 0}, +{ 4, s_2_112, 0, 23, 0}, +{ 5, s_2_113, -1, 123, 0}, +{ 4, s_2_114, 0, 44, 0}, +{ 5, s_2_115, -1, 120, 0}, +{ 7, s_2_116, -2, 92, 0}, +{ 7, s_2_117, -3, 93, 0}, +{ 5, s_2_118, -4, 22, 0}, +{ 6, s_2_119, -5, 94, 0}, +{ 5, s_2_120, 0, 77, 0}, +{ 5, s_2_121, 0, 78, 0}, +{ 5, s_2_122, 0, 79, 0}, +{ 5, s_2_123, 0, 80, 0}, +{ 4, s_2_124, 0, 45, 0}, +{ 6, s_2_125, 0, 91, 0}, +{ 5, s_2_126, 0, 38, 0}, +{ 4, s_2_127, 0, 84, 0}, +{ 4, s_2_128, 0, 85, 0}, +{ 4, s_2_129, 0, 122, 0}, +{ 5, s_2_130, 0, 86, 0}, +{ 2, s_2_131, 0, 95, 0}, +{ 3, s_2_132, -1, 1, 0}, +{ 4, s_2_133, -2, 2, 0}, +{ 3, s_2_134, 0, 104, 0}, +{ 5, s_2_135, -1, 128, 0}, +{ 8, s_2_136, -2, 106, 0}, +{ 8, s_2_137, -3, 107, 0}, +{ 8, s_2_138, -4, 108, 0}, +{ 5, s_2_139, -5, 47, 0}, +{ 6, s_2_140, -6, 114, 0}, +{ 4, s_2_141, -7, 46, 0}, +{ 5, s_2_142, -8, 100, 0}, +{ 5, s_2_143, -9, 105, 0}, +{ 4, s_2_144, -10, 113, 0}, +{ 6, s_2_145, -1, 110, 0}, +{ 6, s_2_146, -2, 111, 0}, +{ 6, s_2_147, -3, 112, 0}, +{ 5, s_2_148, -14, 97, 0}, +{ 5, s_2_149, -15, 96, 0}, +{ 5, s_2_150, -16, 98, 0}, +{ 5, s_2_151, -17, 76, 0}, +{ 5, s_2_152, -18, 99, 0}, +{ 6, s_2_153, -19, 102, 0}, +{ 3, s_2_154, 0, 83, 0}, +{ 3, s_2_155, 0, 116, 0}, +{ 5, s_2_156, -1, 124, 0}, +{ 6, s_2_157, -2, 121, 0}, +{ 4, s_2_158, -3, 103, 0}, +{ 8, s_2_159, -1, 110, 0}, +{ 8, s_2_160, -2, 111, 0}, +{ 8, s_2_161, -3, 112, 0}, +{ 6, s_2_162, -7, 127, 0}, +{ 6, s_2_163, -8, 118, 0}, +{ 5, s_2_164, -9, 48, 0}, +{ 6, s_2_165, -10, 101, 0}, +{ 7, s_2_166, -11, 117, 0}, +{ 7, s_2_167, -12, 90, 0}, +{ 3, s_2_168, 0, 50, 0}, +{ 4, s_2_169, 0, 115, 0}, +{ 4, s_2_170, 0, 13, 0}, +{ 4, s_2_171, 0, 20, 0}, +{ 6, s_2_172, -1, 19, 0}, +{ 5, s_2_173, -2, 18, 0}, +{ 5, s_2_174, 0, 109, 0}, +{ 6, s_2_175, -1, 26, 0}, +{ 6, s_2_176, -2, 30, 0}, +{ 6, s_2_177, -3, 31, 0}, +{ 7, s_2_178, -4, 28, 0}, +{ 7, s_2_179, -5, 27, 0}, +{ 7, s_2_180, -6, 29, 0}, +{ 6, s_2_181, 0, 32, 0}, +{ 6, s_2_182, 0, 33, 0}, +{ 6, s_2_183, 0, 34, 0}, +{ 6, s_2_184, 0, 40, 0}, +{ 6, s_2_185, 0, 39, 0}, +{ 6, s_2_186, 0, 35, 0}, +{ 6, s_2_187, 0, 37, 0}, +{ 6, s_2_188, 0, 36, 0}, +{ 8, s_2_189, -1, 9, 0}, +{ 8, s_2_190, -2, 6, 0}, +{ 8, s_2_191, -3, 7, 0}, +{ 8, s_2_192, -4, 8, 0}, +{ 8, s_2_193, -5, 5, 0}, +{ 6, s_2_194, 0, 41, 0}, +{ 6, s_2_195, 0, 42, 0}, +{ 6, s_2_196, 0, 43, 0}, +{ 6, s_2_197, 0, 44, 0}, +{ 6, s_2_198, 0, 45, 0}, +{ 7, s_2_199, 0, 38, 0}, +{ 5, s_2_200, 0, 104, 0}, +{ 7, s_2_201, -1, 47, 0}, +{ 6, s_2_202, -2, 46, 0}, +{ 5, s_2_203, 0, 119, 0}, +{ 5, s_2_204, 0, 116, 0}, +{ 6, s_2_205, 0, 52, 0}, +{ 6, s_2_206, 0, 51, 0}, +{ 5, s_2_207, 0, 11, 0}, +{ 6, s_2_208, -1, 137, 0}, +{ 7, s_2_209, -2, 89, 0}, +{ 4, s_2_210, 0, 52, 0}, +{ 5, s_2_211, -1, 53, 0}, +{ 5, s_2_212, -2, 54, 0}, +{ 5, s_2_213, -3, 55, 0}, +{ 5, s_2_214, -4, 56, 0}, +{ 6, s_2_215, 0, 135, 0}, +{ 6, s_2_216, 0, 131, 0}, +{ 6, s_2_217, 0, 129, 0}, +{ 6, s_2_218, 0, 133, 0}, +{ 6, s_2_219, 0, 132, 0}, +{ 6, s_2_220, 0, 130, 0}, +{ 6, s_2_221, 0, 134, 0}, +{ 5, s_2_222, 0, 152, 0}, +{ 5, s_2_223, 0, 154, 0}, +{ 5, s_2_224, 0, 70, 0}, +{ 6, s_2_225, 0, 71, 0}, +{ 6, s_2_226, 0, 72, 0}, +{ 6, s_2_227, 0, 73, 0}, +{ 6, s_2_228, 0, 74, 0}, +{ 5, s_2_229, 0, 77, 0}, +{ 5, s_2_230, 0, 78, 0}, +{ 5, s_2_231, 0, 79, 0}, +{ 7, s_2_232, 0, 63, 0}, +{ 7, s_2_233, 0, 64, 0}, +{ 7, s_2_234, 0, 61, 0}, +{ 7, s_2_235, 0, 62, 0}, +{ 7, s_2_236, 0, 60, 0}, +{ 7, s_2_237, 0, 59, 0}, +{ 7, s_2_238, 0, 65, 0}, +{ 6, s_2_239, 0, 66, 0}, +{ 6, s_2_240, 0, 67, 0}, +{ 4, s_2_241, 0, 51, 0}, +{ 5, s_2_242, 0, 124, 0}, +{ 5, s_2_243, 0, 125, 0}, +{ 5, s_2_244, 0, 126, 0}, +{ 5, s_2_245, 0, 109, 0}, +{ 6, s_2_246, -1, 26, 0}, +{ 6, s_2_247, -2, 30, 0}, +{ 6, s_2_248, -3, 31, 0}, +{ 7, s_2_249, -4, 28, 0}, +{ 7, s_2_250, -5, 27, 0}, +{ 7, s_2_251, -6, 29, 0}, +{ 6, s_2_252, 0, 32, 0}, +{ 6, s_2_253, 0, 33, 0}, +{ 6, s_2_254, 0, 34, 0}, +{ 6, s_2_255, 0, 40, 0}, +{ 6, s_2_256, 0, 39, 0}, +{ 8, s_2_257, 0, 84, 0}, +{ 8, s_2_258, 0, 85, 0}, +{ 8, s_2_259, 0, 122, 0}, +{ 9, s_2_260, 0, 86, 0}, +{ 6, s_2_261, 0, 95, 0}, +{ 7, s_2_262, -1, 1, 0}, +{ 8, s_2_263, -2, 2, 0}, +{ 6, s_2_264, 0, 35, 0}, +{ 7, s_2_265, -1, 83, 0}, +{ 6, s_2_266, 0, 37, 0}, +{ 6, s_2_267, 0, 13, 0}, +{ 8, s_2_268, -1, 9, 0}, +{ 8, s_2_269, -2, 6, 0}, +{ 8, s_2_270, -3, 7, 0}, +{ 8, s_2_271, -4, 8, 0}, +{ 8, s_2_272, -5, 5, 0}, +{ 6, s_2_273, 0, 41, 0}, +{ 6, s_2_274, 0, 42, 0}, +{ 6, s_2_275, 0, 43, 0}, +{ 7, s_2_276, -1, 123, 0}, +{ 6, s_2_277, 0, 44, 0}, +{ 7, s_2_278, -1, 120, 0}, +{ 9, s_2_279, -2, 92, 0}, +{ 9, s_2_280, -3, 93, 0}, +{ 8, s_2_281, -4, 94, 0}, +{ 7, s_2_282, 0, 77, 0}, +{ 7, s_2_283, 0, 78, 0}, +{ 7, s_2_284, 0, 79, 0}, +{ 7, s_2_285, 0, 80, 0}, +{ 6, s_2_286, 0, 45, 0}, +{ 8, s_2_287, 0, 91, 0}, +{ 7, s_2_288, 0, 38, 0}, +{ 6, s_2_289, 0, 84, 0}, +{ 6, s_2_290, 0, 85, 0}, +{ 6, s_2_291, 0, 122, 0}, +{ 7, s_2_292, 0, 86, 0}, +{ 4, s_2_293, 0, 95, 0}, +{ 5, s_2_294, -1, 1, 0}, +{ 6, s_2_295, -2, 2, 0}, +{ 5, s_2_296, 0, 104, 0}, +{ 7, s_2_297, -1, 47, 0}, +{ 6, s_2_298, -2, 46, 0}, +{ 5, s_2_299, 0, 83, 0}, +{ 5, s_2_300, 0, 116, 0}, +{ 7, s_2_301, -1, 48, 0}, +{ 5, s_2_302, 0, 50, 0}, +{ 6, s_2_303, 0, 51, 0}, +{ 4, s_2_304, 0, 13, 0}, +{ 5, s_2_305, -1, 10, 0}, +{ 5, s_2_306, -2, 11, 0}, +{ 6, s_2_307, -1, 137, 0}, +{ 7, s_2_308, -2, 89, 0}, +{ 5, s_2_309, -5, 12, 0}, +{ 5, s_2_310, 0, 53, 0}, +{ 5, s_2_311, 0, 54, 0}, +{ 5, s_2_312, 0, 55, 0}, +{ 5, s_2_313, 0, 56, 0}, +{ 6, s_2_314, 0, 135, 0}, +{ 6, s_2_315, 0, 131, 0}, +{ 6, s_2_316, 0, 129, 0}, +{ 6, s_2_317, 0, 133, 0}, +{ 6, s_2_318, 0, 132, 0}, +{ 6, s_2_319, 0, 130, 0}, +{ 6, s_2_320, 0, 134, 0}, +{ 5, s_2_321, 0, 57, 0}, +{ 5, s_2_322, 0, 58, 0}, +{ 5, s_2_323, 0, 123, 0}, +{ 5, s_2_324, 0, 120, 0}, +{ 7, s_2_325, -1, 68, 0}, +{ 6, s_2_326, -2, 69, 0}, +{ 5, s_2_327, 0, 70, 0}, +{ 7, s_2_328, 0, 92, 0}, +{ 7, s_2_329, 0, 93, 0}, +{ 6, s_2_330, 0, 94, 0}, +{ 6, s_2_331, 0, 71, 0}, +{ 6, s_2_332, 0, 72, 0}, +{ 6, s_2_333, 0, 73, 0}, +{ 6, s_2_334, 0, 74, 0}, +{ 7, s_2_335, 0, 75, 0}, +{ 5, s_2_336, 0, 77, 0}, +{ 5, s_2_337, 0, 78, 0}, +{ 7, s_2_338, -1, 109, 0}, +{ 8, s_2_339, -1, 26, 0}, +{ 8, s_2_340, -2, 30, 0}, +{ 8, s_2_341, -3, 31, 0}, +{ 9, s_2_342, -4, 28, 0}, +{ 9, s_2_343, -5, 27, 0}, +{ 9, s_2_344, -6, 29, 0}, +{ 5, s_2_345, 0, 79, 0}, +{ 5, s_2_346, 0, 80, 0}, +{ 6, s_2_347, -1, 20, 0}, +{ 7, s_2_348, -1, 17, 0}, +{ 6, s_2_349, -3, 82, 0}, +{ 7, s_2_350, -1, 49, 0}, +{ 6, s_2_351, -5, 81, 0}, +{ 7, s_2_352, -6, 12, 0}, +{ 6, s_2_353, 0, 3, 0}, +{ 7, s_2_354, 0, 4, 0}, +{ 6, s_2_355, 0, 14, 0}, +{ 6, s_2_356, 0, 15, 0}, +{ 6, s_2_357, 0, 16, 0}, +{ 7, s_2_358, 0, 63, 0}, +{ 7, s_2_359, 0, 64, 0}, +{ 7, s_2_360, 0, 61, 0}, +{ 7, s_2_361, 0, 62, 0}, +{ 7, s_2_362, 0, 60, 0}, +{ 7, s_2_363, 0, 59, 0}, +{ 7, s_2_364, 0, 65, 0}, +{ 6, s_2_365, 0, 66, 0}, +{ 6, s_2_366, 0, 67, 0}, +{ 6, s_2_367, 0, 91, 0}, +{ 2, s_2_368, 0, 13, 0}, +{ 3, s_2_369, -1, 10, 0}, +{ 5, s_2_370, -1, 128, 0}, +{ 5, s_2_371, -2, 105, 0}, +{ 4, s_2_372, -3, 113, 0}, +{ 5, s_2_373, -4, 97, 0}, +{ 5, s_2_374, -5, 96, 0}, +{ 5, s_2_375, -6, 98, 0}, +{ 5, s_2_376, -7, 99, 0}, +{ 6, s_2_377, -8, 102, 0}, +{ 5, s_2_378, -10, 124, 0}, +{ 6, s_2_379, -11, 121, 0}, +{ 6, s_2_380, -12, 101, 0}, +{ 7, s_2_381, -13, 117, 0}, +{ 3, s_2_382, -14, 11, 0}, +{ 4, s_2_383, -1, 137, 0}, +{ 5, s_2_384, -2, 10, 0}, +{ 5, s_2_385, -3, 89, 0}, +{ 3, s_2_386, -18, 12, 0}, +{ 3, s_2_387, 0, 53, 0}, +{ 3, s_2_388, 0, 54, 0}, +{ 3, s_2_389, 0, 55, 0}, +{ 3, s_2_390, 0, 56, 0}, +{ 4, s_2_391, 0, 135, 0}, +{ 4, s_2_392, 0, 131, 0}, +{ 4, s_2_393, 0, 129, 0}, +{ 4, s_2_394, 0, 133, 0}, +{ 4, s_2_395, 0, 132, 0}, +{ 4, s_2_396, 0, 130, 0}, +{ 4, s_2_397, 0, 134, 0}, +{ 3, s_2_398, 0, 57, 0}, +{ 3, s_2_399, 0, 58, 0}, +{ 3, s_2_400, 0, 123, 0}, +{ 3, s_2_401, 0, 120, 0}, +{ 5, s_2_402, -1, 68, 0}, +{ 4, s_2_403, -2, 69, 0}, +{ 3, s_2_404, 0, 70, 0}, +{ 5, s_2_405, 0, 92, 0}, +{ 5, s_2_406, 0, 93, 0}, +{ 4, s_2_407, 0, 94, 0}, +{ 4, s_2_408, 0, 71, 0}, +{ 4, s_2_409, 0, 72, 0}, +{ 4, s_2_410, 0, 73, 0}, +{ 4, s_2_411, 0, 74, 0}, +{ 4, s_2_412, 0, 13, 0}, +{ 5, s_2_413, 0, 75, 0}, +{ 3, s_2_414, 0, 77, 0}, +{ 3, s_2_415, 0, 78, 0}, +{ 5, s_2_416, -1, 109, 0}, +{ 6, s_2_417, -1, 26, 0}, +{ 6, s_2_418, -2, 30, 0}, +{ 6, s_2_419, -3, 31, 0}, +{ 7, s_2_420, -4, 28, 0}, +{ 7, s_2_421, -5, 27, 0}, +{ 7, s_2_422, -6, 29, 0}, +{ 3, s_2_423, 0, 79, 0}, +{ 3, s_2_424, 0, 80, 0}, +{ 4, s_2_425, -1, 20, 0}, +{ 5, s_2_426, -1, 17, 0}, +{ 4, s_2_427, -3, 82, 0}, +{ 5, s_2_428, -1, 49, 0}, +{ 4, s_2_429, -5, 81, 0}, +{ 5, s_2_430, -6, 12, 0}, +{ 4, s_2_431, 0, 3, 0}, +{ 5, s_2_432, 0, 4, 0}, +{ 4, s_2_433, 0, 14, 0}, +{ 4, s_2_434, 0, 15, 0}, +{ 4, s_2_435, 0, 16, 0}, +{ 5, s_2_436, 0, 63, 0}, +{ 5, s_2_437, 0, 64, 0}, +{ 5, s_2_438, 0, 61, 0}, +{ 5, s_2_439, 0, 62, 0}, +{ 5, s_2_440, 0, 60, 0}, +{ 5, s_2_441, 0, 59, 0}, +{ 5, s_2_442, 0, 65, 0}, +{ 4, s_2_443, 0, 66, 0}, +{ 4, s_2_444, 0, 67, 0}, +{ 4, s_2_445, 0, 91, 0}, +{ 3, s_2_446, 0, 124, 0}, +{ 3, s_2_447, 0, 125, 0}, +{ 3, s_2_448, 0, 126, 0}, +{ 4, s_2_449, -1, 121, 0}, +{ 6, s_2_450, 0, 110, 0}, +{ 6, s_2_451, 0, 111, 0}, +{ 6, s_2_452, 0, 112, 0}, +{ 2, s_2_453, 0, 20, 0}, +{ 4, s_2_454, -1, 19, 0}, +{ 3, s_2_455, -2, 18, 0}, +{ 3, s_2_456, 0, 104, 0}, +{ 4, s_2_457, -1, 26, 0}, +{ 4, s_2_458, -2, 30, 0}, +{ 4, s_2_459, -3, 31, 0}, +{ 6, s_2_460, -4, 106, 0}, +{ 6, s_2_461, -5, 107, 0}, +{ 6, s_2_462, -6, 108, 0}, +{ 5, s_2_463, -7, 28, 0}, +{ 5, s_2_464, -8, 27, 0}, +{ 5, s_2_465, -9, 29, 0}, +{ 3, s_2_466, 0, 116, 0}, +{ 4, s_2_467, -1, 32, 0}, +{ 4, s_2_468, -2, 33, 0}, +{ 4, s_2_469, -3, 34, 0}, +{ 4, s_2_470, -4, 40, 0}, +{ 4, s_2_471, -5, 39, 0}, +{ 6, s_2_472, -6, 84, 0}, +{ 6, s_2_473, -7, 85, 0}, +{ 6, s_2_474, -8, 122, 0}, +{ 7, s_2_475, -9, 86, 0}, +{ 4, s_2_476, -10, 95, 0}, +{ 5, s_2_477, -1, 1, 0}, +{ 6, s_2_478, -2, 2, 0}, +{ 4, s_2_479, -13, 35, 0}, +{ 5, s_2_480, -1, 83, 0}, +{ 4, s_2_481, -15, 37, 0}, +{ 4, s_2_482, -16, 13, 0}, +{ 6, s_2_483, -1, 9, 0}, +{ 6, s_2_484, -2, 6, 0}, +{ 6, s_2_485, -3, 7, 0}, +{ 6, s_2_486, -4, 8, 0}, +{ 6, s_2_487, -5, 5, 0}, +{ 4, s_2_488, -22, 41, 0}, +{ 4, s_2_489, -23, 42, 0}, +{ 4, s_2_490, -24, 43, 0}, +{ 5, s_2_491, -1, 123, 0}, +{ 4, s_2_492, -26, 44, 0}, +{ 5, s_2_493, -1, 120, 0}, +{ 7, s_2_494, -2, 92, 0}, +{ 7, s_2_495, -3, 93, 0}, +{ 6, s_2_496, -4, 94, 0}, +{ 5, s_2_497, -31, 77, 0}, +{ 5, s_2_498, -32, 78, 0}, +{ 5, s_2_499, -33, 79, 0}, +{ 5, s_2_500, -34, 80, 0}, +{ 4, s_2_501, -35, 45, 0}, +{ 6, s_2_502, -36, 91, 0}, +{ 5, s_2_503, -37, 38, 0}, +{ 4, s_2_504, 0, 84, 0}, +{ 4, s_2_505, 0, 85, 0}, +{ 4, s_2_506, 0, 122, 0}, +{ 5, s_2_507, 0, 86, 0}, +{ 3, s_2_508, 0, 25, 0}, +{ 6, s_2_509, -1, 121, 0}, +{ 5, s_2_510, -2, 100, 0}, +{ 7, s_2_511, -3, 117, 0}, +{ 2, s_2_512, 0, 95, 0}, +{ 3, s_2_513, -1, 1, 0}, +{ 4, s_2_514, -2, 2, 0}, +{ 3, s_2_515, 0, 104, 0}, +{ 5, s_2_516, -1, 128, 0}, +{ 8, s_2_517, -2, 106, 0}, +{ 8, s_2_518, -3, 107, 0}, +{ 8, s_2_519, -4, 108, 0}, +{ 5, s_2_520, -5, 47, 0}, +{ 6, s_2_521, -6, 114, 0}, +{ 4, s_2_522, -7, 46, 0}, +{ 5, s_2_523, -8, 100, 0}, +{ 5, s_2_524, -9, 105, 0}, +{ 4, s_2_525, -10, 113, 0}, +{ 6, s_2_526, -1, 110, 0}, +{ 6, s_2_527, -2, 111, 0}, +{ 6, s_2_528, -3, 112, 0}, +{ 5, s_2_529, -14, 97, 0}, +{ 5, s_2_530, -15, 96, 0}, +{ 5, s_2_531, -16, 98, 0}, +{ 5, s_2_532, -17, 76, 0}, +{ 5, s_2_533, -18, 99, 0}, +{ 6, s_2_534, -19, 102, 0}, +{ 3, s_2_535, 0, 83, 0}, +{ 3, s_2_536, 0, 116, 0}, +{ 5, s_2_537, -1, 124, 0}, +{ 6, s_2_538, -2, 121, 0}, +{ 4, s_2_539, -3, 103, 0}, +{ 6, s_2_540, -4, 127, 0}, +{ 6, s_2_541, -5, 118, 0}, +{ 5, s_2_542, -6, 48, 0}, +{ 6, s_2_543, -7, 101, 0}, +{ 7, s_2_544, -8, 117, 0}, +{ 7, s_2_545, -9, 90, 0}, +{ 3, s_2_546, 0, 50, 0}, +{ 4, s_2_547, 0, 115, 0}, +{ 4, s_2_548, 0, 13, 0}, +{ 4, s_2_549, 0, 52, 0}, +{ 4, s_2_550, 0, 51, 0}, +{ 5, s_2_551, 0, 124, 0}, +{ 5, s_2_552, 0, 125, 0}, +{ 5, s_2_553, 0, 126, 0}, +{ 6, s_2_554, 0, 84, 0}, +{ 6, s_2_555, 0, 85, 0}, +{ 6, s_2_556, 0, 122, 0}, +{ 7, s_2_557, 0, 86, 0}, +{ 4, s_2_558, 0, 95, 0}, +{ 5, s_2_559, -1, 1, 0}, +{ 6, s_2_560, -2, 2, 0}, +{ 5, s_2_561, 0, 83, 0}, +{ 4, s_2_562, 0, 13, 0}, +{ 6, s_2_563, -1, 137, 0}, +{ 7, s_2_564, -2, 89, 0}, +{ 5, s_2_565, 0, 123, 0}, +{ 5, s_2_566, 0, 120, 0}, +{ 7, s_2_567, 0, 92, 0}, +{ 7, s_2_568, 0, 93, 0}, +{ 6, s_2_569, 0, 94, 0}, +{ 5, s_2_570, 0, 77, 0}, +{ 5, s_2_571, 0, 78, 0}, +{ 5, s_2_572, 0, 79, 0}, +{ 5, s_2_573, 0, 80, 0}, +{ 6, s_2_574, 0, 14, 0}, +{ 6, s_2_575, 0, 15, 0}, +{ 6, s_2_576, 0, 16, 0}, +{ 6, s_2_577, 0, 91, 0}, +{ 2, s_2_578, 0, 13, 0}, +{ 3, s_2_579, -1, 10, 0}, +{ 5, s_2_580, -1, 128, 0}, +{ 5, s_2_581, -2, 105, 0}, +{ 4, s_2_582, -3, 113, 0}, +{ 6, s_2_583, -1, 110, 0}, +{ 6, s_2_584, -2, 111, 0}, +{ 6, s_2_585, -3, 112, 0}, +{ 5, s_2_586, -7, 97, 0}, +{ 5, s_2_587, -8, 96, 0}, +{ 5, s_2_588, -9, 98, 0}, +{ 5, s_2_589, -10, 99, 0}, +{ 6, s_2_590, -11, 102, 0}, +{ 5, s_2_591, -13, 124, 0}, +{ 6, s_2_592, -14, 121, 0}, +{ 6, s_2_593, -15, 101, 0}, +{ 7, s_2_594, -16, 117, 0}, +{ 3, s_2_595, -17, 11, 0}, +{ 4, s_2_596, -1, 137, 0}, +{ 5, s_2_597, -2, 10, 0}, +{ 5, s_2_598, -3, 89, 0}, +{ 3, s_2_599, -21, 12, 0}, +{ 3, s_2_600, 0, 53, 0}, +{ 3, s_2_601, 0, 54, 0}, +{ 3, s_2_602, 0, 55, 0}, +{ 3, s_2_603, 0, 56, 0}, +{ 3, s_2_604, 0, 161, 0}, +{ 4, s_2_605, -1, 135, 0}, +{ 5, s_2_606, -2, 128, 0}, +{ 4, s_2_607, -3, 131, 0}, +{ 4, s_2_608, -4, 129, 0}, +{ 8, s_2_609, -1, 138, 0}, +{ 8, s_2_610, -2, 139, 0}, +{ 8, s_2_611, -3, 140, 0}, +{ 6, s_2_612, -4, 150, 0}, +{ 4, s_2_613, -9, 133, 0}, +{ 4, s_2_614, -10, 132, 0}, +{ 5, s_2_615, -11, 155, 0}, +{ 5, s_2_616, -12, 156, 0}, +{ 4, s_2_617, -13, 130, 0}, +{ 4, s_2_618, -14, 134, 0}, +{ 5, s_2_619, -1, 144, 0}, +{ 5, s_2_620, -2, 145, 0}, +{ 5, s_2_621, -3, 146, 0}, +{ 5, s_2_622, -4, 148, 0}, +{ 5, s_2_623, -5, 147, 0}, +{ 3, s_2_624, 0, 57, 0}, +{ 3, s_2_625, 0, 58, 0}, +{ 5, s_2_626, -1, 124, 0}, +{ 6, s_2_627, -2, 121, 0}, +{ 6, s_2_628, -3, 127, 0}, +{ 6, s_2_629, -4, 149, 0}, +{ 3, s_2_630, 0, 123, 0}, +{ 8, s_2_631, -1, 141, 0}, +{ 8, s_2_632, -2, 142, 0}, +{ 8, s_2_633, -3, 143, 0}, +{ 3, s_2_634, 0, 104, 0}, +{ 5, s_2_635, -1, 128, 0}, +{ 5, s_2_636, -2, 68, 0}, +{ 4, s_2_637, -3, 69, 0}, +{ 5, s_2_638, -4, 100, 0}, +{ 5, s_2_639, -5, 105, 0}, +{ 4, s_2_640, -6, 113, 0}, +{ 5, s_2_641, -7, 97, 0}, +{ 5, s_2_642, -8, 96, 0}, +{ 5, s_2_643, -9, 98, 0}, +{ 5, s_2_644, -10, 99, 0}, +{ 6, s_2_645, -11, 102, 0}, +{ 3, s_2_646, 0, 70, 0}, +{ 8, s_2_647, -1, 110, 0}, +{ 8, s_2_648, -2, 111, 0}, +{ 8, s_2_649, -3, 112, 0}, +{ 8, s_2_650, -4, 106, 0}, +{ 8, s_2_651, -5, 107, 0}, +{ 8, s_2_652, -6, 108, 0}, +{ 5, s_2_653, -7, 116, 0}, +{ 6, s_2_654, -8, 114, 0}, +{ 5, s_2_655, -9, 25, 0}, +{ 8, s_2_656, -1, 121, 0}, +{ 7, s_2_657, -2, 100, 0}, +{ 9, s_2_658, -3, 117, 0}, +{ 4, s_2_659, -13, 13, 0}, +{ 8, s_2_660, -1, 110, 0}, +{ 8, s_2_661, -2, 111, 0}, +{ 8, s_2_662, -3, 112, 0}, +{ 6, s_2_663, -17, 115, 0}, +{ 3, s_2_664, 0, 116, 0}, +{ 5, s_2_665, -1, 124, 0}, +{ 6, s_2_666, -2, 121, 0}, +{ 4, s_2_667, -3, 13, 0}, +{ 8, s_2_668, -1, 110, 0}, +{ 8, s_2_669, -2, 111, 0}, +{ 8, s_2_670, -3, 112, 0}, +{ 6, s_2_671, -7, 127, 0}, +{ 6, s_2_672, -8, 118, 0}, +{ 6, s_2_673, -9, 115, 0}, +{ 5, s_2_674, -10, 92, 0}, +{ 5, s_2_675, -11, 93, 0}, +{ 6, s_2_676, -12, 101, 0}, +{ 7, s_2_677, -13, 117, 0}, +{ 7, s_2_678, -14, 90, 0}, +{ 4, s_2_679, 0, 104, 0}, +{ 6, s_2_680, -1, 105, 0}, +{ 5, s_2_681, -2, 113, 0}, +{ 7, s_2_682, -1, 106, 0}, +{ 7, s_2_683, -2, 107, 0}, +{ 7, s_2_684, -3, 108, 0}, +{ 6, s_2_685, -6, 97, 0}, +{ 6, s_2_686, -7, 96, 0}, +{ 6, s_2_687, -8, 98, 0}, +{ 6, s_2_688, -9, 99, 0}, +{ 4, s_2_689, 0, 116, 0}, +{ 7, s_2_690, 0, 121, 0}, +{ 6, s_2_691, 0, 100, 0}, +{ 8, s_2_692, 0, 117, 0}, +{ 4, s_2_693, 0, 94, 0}, +{ 6, s_2_694, -1, 128, 0}, +{ 9, s_2_695, -2, 106, 0}, +{ 9, s_2_696, -3, 107, 0}, +{ 9, s_2_697, -4, 108, 0}, +{ 7, s_2_698, -5, 114, 0}, +{ 6, s_2_699, -6, 100, 0}, +{ 6, s_2_700, -7, 105, 0}, +{ 5, s_2_701, -8, 113, 0}, +{ 6, s_2_702, -9, 97, 0}, +{ 6, s_2_703, -10, 96, 0}, +{ 6, s_2_704, -11, 98, 0}, +{ 6, s_2_705, -12, 76, 0}, +{ 6, s_2_706, -13, 99, 0}, +{ 7, s_2_707, -14, 102, 0}, +{ 4, s_2_708, 0, 71, 0}, +{ 4, s_2_709, 0, 72, 0}, +{ 6, s_2_710, -1, 124, 0}, +{ 7, s_2_711, -2, 121, 0}, +{ 5, s_2_712, -3, 103, 0}, +{ 7, s_2_713, -4, 127, 0}, +{ 7, s_2_714, -5, 118, 0}, +{ 7, s_2_715, -6, 101, 0}, +{ 8, s_2_716, -7, 117, 0}, +{ 8, s_2_717, -8, 90, 0}, +{ 4, s_2_718, 0, 73, 0}, +{ 4, s_2_719, 0, 74, 0}, +{ 9, s_2_720, -1, 110, 0}, +{ 9, s_2_721, -2, 111, 0}, +{ 9, s_2_722, -3, 112, 0}, +{ 5, s_2_723, 0, 13, 0}, +{ 5, s_2_724, 0, 75, 0}, +{ 3, s_2_725, 0, 77, 0}, +{ 3, s_2_726, 0, 78, 0}, +{ 5, s_2_727, -1, 109, 0}, +{ 6, s_2_728, -1, 26, 0}, +{ 6, s_2_729, -2, 30, 0}, +{ 6, s_2_730, -3, 31, 0}, +{ 7, s_2_731, -4, 28, 0}, +{ 7, s_2_732, -5, 27, 0}, +{ 7, s_2_733, -6, 29, 0}, +{ 3, s_2_734, 0, 79, 0}, +{ 3, s_2_735, 0, 80, 0}, +{ 4, s_2_736, -1, 20, 0}, +{ 5, s_2_737, -1, 17, 0}, +{ 4, s_2_738, -3, 82, 0}, +{ 5, s_2_739, -1, 49, 0}, +{ 4, s_2_740, -5, 81, 0}, +{ 5, s_2_741, -6, 12, 0}, +{ 4, s_2_742, 0, 14, 0}, +{ 4, s_2_743, 0, 15, 0}, +{ 4, s_2_744, 0, 16, 0}, +{ 4, s_2_745, 0, 101, 0}, +{ 5, s_2_746, 0, 117, 0}, +{ 4, s_2_747, 0, 104, 0}, +{ 5, s_2_748, -1, 63, 0}, +{ 5, s_2_749, -2, 64, 0}, +{ 5, s_2_750, -3, 61, 0}, +{ 9, s_2_751, -1, 106, 0}, +{ 9, s_2_752, -2, 107, 0}, +{ 9, s_2_753, -3, 108, 0}, +{ 7, s_2_754, -4, 114, 0}, +{ 5, s_2_755, -8, 62, 0}, +{ 5, s_2_756, -9, 60, 0}, +{ 6, s_2_757, -10, 100, 0}, +{ 6, s_2_758, -11, 105, 0}, +{ 5, s_2_759, -12, 59, 0}, +{ 5, s_2_760, -13, 65, 0}, +{ 6, s_2_761, -1, 97, 0}, +{ 6, s_2_762, -2, 96, 0}, +{ 6, s_2_763, -3, 98, 0}, +{ 6, s_2_764, -4, 76, 0}, +{ 6, s_2_765, -5, 99, 0}, +{ 7, s_2_766, -19, 102, 0}, +{ 4, s_2_767, 0, 66, 0}, +{ 4, s_2_768, 0, 67, 0}, +{ 7, s_2_769, -1, 118, 0}, +{ 7, s_2_770, -2, 101, 0}, +{ 8, s_2_771, -3, 117, 0}, +{ 8, s_2_772, -4, 90, 0}, +{ 4, s_2_773, 0, 91, 0}, +{ 9, s_2_774, -1, 110, 0}, +{ 9, s_2_775, -2, 111, 0}, +{ 9, s_2_776, -3, 112, 0}, +{ 4, s_2_777, 0, 124, 0}, +{ 4, s_2_778, 0, 125, 0}, +{ 4, s_2_779, 0, 126, 0}, +{ 7, s_2_780, 0, 84, 0}, +{ 7, s_2_781, 0, 85, 0}, +{ 7, s_2_782, 0, 122, 0}, +{ 8, s_2_783, 0, 86, 0}, +{ 5, s_2_784, 0, 95, 0}, +{ 6, s_2_785, -1, 1, 0}, +{ 7, s_2_786, -2, 2, 0}, +{ 6, s_2_787, 0, 83, 0}, +{ 5, s_2_788, 0, 13, 0}, +{ 6, s_2_789, 0, 123, 0}, +{ 6, s_2_790, 0, 120, 0}, +{ 8, s_2_791, 0, 92, 0}, +{ 8, s_2_792, 0, 93, 0}, +{ 7, s_2_793, 0, 94, 0}, +{ 6, s_2_794, 0, 77, 0}, +{ 6, s_2_795, 0, 78, 0}, +{ 6, s_2_796, 0, 79, 0}, +{ 6, s_2_797, 0, 80, 0}, +{ 7, s_2_798, 0, 91, 0}, +{ 5, s_2_799, 0, 84, 0}, +{ 5, s_2_800, 0, 85, 0}, +{ 5, s_2_801, 0, 122, 0}, +{ 6, s_2_802, 0, 86, 0}, +{ 3, s_2_803, 0, 95, 0}, +{ 4, s_2_804, 0, 83, 0}, +{ 3, s_2_805, 0, 13, 0}, +{ 4, s_2_806, -1, 10, 0}, +{ 4, s_2_807, -2, 87, 0}, +{ 4, s_2_808, -3, 159, 0}, +{ 5, s_2_809, -4, 88, 0}, +{ 4, s_2_810, 0, 123, 0}, +{ 4, s_2_811, 0, 120, 0}, +{ 4, s_2_812, 0, 77, 0}, +{ 4, s_2_813, 0, 78, 0}, +{ 4, s_2_814, 0, 79, 0}, +{ 4, s_2_815, 0, 80, 0}, +{ 5, s_2_816, 0, 14, 0}, +{ 5, s_2_817, 0, 15, 0}, +{ 5, s_2_818, 0, 16, 0}, +{ 5, s_2_819, 0, 91, 0}, +{ 4, s_2_820, 0, 124, 0}, +{ 4, s_2_821, 0, 125, 0}, +{ 4, s_2_822, 0, 126, 0}, +{ 5, s_2_823, 0, 84, 0}, +{ 5, s_2_824, 0, 85, 0}, +{ 5, s_2_825, 0, 122, 0}, +{ 6, s_2_826, 0, 86, 0}, +{ 3, s_2_827, 0, 95, 0}, +{ 4, s_2_828, -1, 1, 0}, +{ 5, s_2_829, -2, 2, 0}, +{ 4, s_2_830, 0, 83, 0}, +{ 3, s_2_831, 0, 13, 0}, +{ 5, s_2_832, -1, 137, 0}, +{ 6, s_2_833, -2, 89, 0}, +{ 4, s_2_834, 0, 123, 0}, +{ 4, s_2_835, 0, 120, 0}, +{ 6, s_2_836, 0, 92, 0}, +{ 6, s_2_837, 0, 93, 0}, +{ 5, s_2_838, 0, 94, 0}, +{ 4, s_2_839, 0, 77, 0}, +{ 4, s_2_840, 0, 78, 0}, +{ 4, s_2_841, 0, 79, 0}, +{ 4, s_2_842, 0, 80, 0}, +{ 5, s_2_843, 0, 14, 0}, +{ 5, s_2_844, 0, 15, 0}, +{ 5, s_2_845, 0, 16, 0}, +{ 5, s_2_846, 0, 91, 0}, +{ 2, s_2_847, 0, 104, 0}, +{ 4, s_2_848, -1, 128, 0}, +{ 7, s_2_849, -2, 106, 0}, +{ 7, s_2_850, -3, 107, 0}, +{ 7, s_2_851, -4, 108, 0}, +{ 5, s_2_852, -5, 114, 0}, +{ 4, s_2_853, -6, 100, 0}, +{ 4, s_2_854, -7, 105, 0}, +{ 3, s_2_855, -8, 113, 0}, +{ 4, s_2_856, -9, 97, 0}, +{ 4, s_2_857, -10, 96, 0}, +{ 4, s_2_858, -11, 98, 0}, +{ 4, s_2_859, -12, 76, 0}, +{ 4, s_2_860, -13, 99, 0}, +{ 5, s_2_861, -14, 102, 0}, +{ 2, s_2_862, 0, 116, 0}, +{ 4, s_2_863, -1, 124, 0}, +{ 4, s_2_864, -2, 125, 0}, +{ 4, s_2_865, -3, 126, 0}, +{ 5, s_2_866, -1, 121, 0}, +{ 7, s_2_867, -5, 84, 0}, +{ 7, s_2_868, -6, 85, 0}, +{ 7, s_2_869, -7, 122, 0}, +{ 8, s_2_870, -8, 86, 0}, +{ 5, s_2_871, -9, 95, 0}, +{ 6, s_2_872, -1, 1, 0}, +{ 7, s_2_873, -2, 2, 0}, +{ 6, s_2_874, -12, 83, 0}, +{ 5, s_2_875, -13, 13, 0}, +{ 6, s_2_876, -14, 123, 0}, +{ 6, s_2_877, -15, 120, 0}, +{ 8, s_2_878, -16, 92, 0}, +{ 8, s_2_879, -17, 93, 0}, +{ 7, s_2_880, -18, 94, 0}, +{ 6, s_2_881, -19, 77, 0}, +{ 6, s_2_882, -20, 78, 0}, +{ 6, s_2_883, -21, 79, 0}, +{ 6, s_2_884, -22, 80, 0}, +{ 7, s_2_885, -23, 91, 0}, +{ 5, s_2_886, -24, 84, 0}, +{ 5, s_2_887, -25, 85, 0}, +{ 5, s_2_888, -26, 122, 0}, +{ 6, s_2_889, -27, 86, 0}, +{ 3, s_2_890, -28, 95, 0}, +{ 4, s_2_891, -1, 1, 0}, +{ 5, s_2_892, -2, 2, 0}, +{ 4, s_2_893, -31, 83, 0}, +{ 3, s_2_894, -32, 13, 0}, +{ 5, s_2_895, -1, 137, 0}, +{ 6, s_2_896, -2, 89, 0}, +{ 4, s_2_897, -35, 123, 0}, +{ 5, s_2_898, -1, 127, 0}, +{ 4, s_2_899, -37, 120, 0}, +{ 5, s_2_900, -38, 118, 0}, +{ 6, s_2_901, -39, 92, 0}, +{ 6, s_2_902, -40, 93, 0}, +{ 5, s_2_903, -41, 94, 0}, +{ 4, s_2_904, -42, 77, 0}, +{ 4, s_2_905, -43, 78, 0}, +{ 4, s_2_906, -44, 79, 0}, +{ 4, s_2_907, -45, 80, 0}, +{ 5, s_2_908, -46, 14, 0}, +{ 5, s_2_909, -47, 15, 0}, +{ 5, s_2_910, -48, 16, 0}, +{ 5, s_2_911, -49, 101, 0}, +{ 6, s_2_912, -50, 117, 0}, +{ 5, s_2_913, -51, 91, 0}, +{ 6, s_2_914, -1, 90, 0}, +{ 7, s_2_915, 0, 110, 0}, +{ 7, s_2_916, 0, 111, 0}, +{ 7, s_2_917, 0, 112, 0}, +{ 4, s_2_918, 0, 124, 0}, +{ 4, s_2_919, 0, 125, 0}, +{ 4, s_2_920, 0, 126, 0}, +{ 5, s_2_921, 0, 14, 0}, +{ 5, s_2_922, 0, 15, 0}, +{ 5, s_2_923, 0, 16, 0}, +{ 3, s_2_924, 0, 124, 0}, +{ 5, s_2_925, 0, 124, 0}, +{ 4, s_2_926, 0, 162, 0}, +{ 5, s_2_927, 0, 161, 0}, +{ 7, s_2_928, -1, 155, 0}, +{ 7, s_2_929, -2, 156, 0}, +{ 8, s_2_930, -3, 138, 0}, +{ 8, s_2_931, -4, 139, 0}, +{ 8, s_2_932, -5, 140, 0}, +{ 7, s_2_933, -6, 144, 0}, +{ 7, s_2_934, -7, 145, 0}, +{ 7, s_2_935, -8, 146, 0}, +{ 7, s_2_936, -9, 147, 0}, +{ 5, s_2_937, 0, 157, 0}, +{ 8, s_2_938, -1, 121, 0}, +{ 7, s_2_939, -2, 155, 0}, +{ 4, s_2_940, 0, 121, 0}, +{ 4, s_2_941, 0, 164, 0}, +{ 5, s_2_942, 0, 153, 0}, +{ 6, s_2_943, 0, 136, 0}, +{ 2, s_2_944, 0, 20, 0}, +{ 3, s_2_945, -1, 18, 0}, +{ 3, s_2_946, 0, 109, 0}, +{ 4, s_2_947, -1, 26, 0}, +{ 4, s_2_948, -2, 30, 0}, +{ 4, s_2_949, -3, 31, 0}, +{ 5, s_2_950, -4, 28, 0}, +{ 5, s_2_951, -5, 27, 0}, +{ 5, s_2_952, -6, 29, 0}, +{ 4, s_2_953, 0, 32, 0}, +{ 4, s_2_954, 0, 33, 0}, +{ 4, s_2_955, 0, 34, 0}, +{ 4, s_2_956, 0, 40, 0}, +{ 4, s_2_957, 0, 39, 0}, +{ 6, s_2_958, 0, 84, 0}, +{ 6, s_2_959, 0, 85, 0}, +{ 6, s_2_960, 0, 122, 0}, +{ 7, s_2_961, 0, 86, 0}, +{ 4, s_2_962, 0, 95, 0}, +{ 5, s_2_963, -1, 1, 0}, +{ 6, s_2_964, -2, 2, 0}, +{ 4, s_2_965, 0, 35, 0}, +{ 5, s_2_966, -1, 83, 0}, +{ 4, s_2_967, 0, 37, 0}, +{ 4, s_2_968, 0, 13, 0}, +{ 6, s_2_969, -1, 9, 0}, +{ 6, s_2_970, -2, 6, 0}, +{ 6, s_2_971, -3, 7, 0}, +{ 6, s_2_972, -4, 8, 0}, +{ 6, s_2_973, -5, 5, 0}, +{ 4, s_2_974, 0, 41, 0}, +{ 4, s_2_975, 0, 42, 0}, +{ 4, s_2_976, 0, 43, 0}, +{ 5, s_2_977, -1, 123, 0}, +{ 4, s_2_978, 0, 44, 0}, +{ 5, s_2_979, -1, 120, 0}, +{ 7, s_2_980, -2, 92, 0}, +{ 7, s_2_981, -3, 93, 0}, +{ 6, s_2_982, -4, 94, 0}, +{ 5, s_2_983, 0, 77, 0}, +{ 5, s_2_984, 0, 78, 0}, +{ 5, s_2_985, 0, 79, 0}, +{ 5, s_2_986, 0, 80, 0}, +{ 4, s_2_987, 0, 45, 0}, +{ 6, s_2_988, 0, 91, 0}, +{ 5, s_2_989, 0, 38, 0}, +{ 4, s_2_990, 0, 84, 0}, +{ 4, s_2_991, 0, 85, 0}, +{ 4, s_2_992, 0, 122, 0}, +{ 5, s_2_993, 0, 86, 0}, +{ 2, s_2_994, 0, 95, 0}, +{ 3, s_2_995, -1, 1, 0}, +{ 4, s_2_996, -2, 2, 0}, +{ 3, s_2_997, 0, 104, 0}, +{ 5, s_2_998, -1, 128, 0}, +{ 8, s_2_999, -2, 106, 0}, +{ 8, s_2_1000, -3, 107, 0}, +{ 8, s_2_1001, -4, 108, 0}, +{ 5, s_2_1002, -5, 47, 0}, +{ 6, s_2_1003, -6, 114, 0}, +{ 4, s_2_1004, -7, 46, 0}, +{ 5, s_2_1005, -8, 100, 0}, +{ 5, s_2_1006, -9, 105, 0}, +{ 4, s_2_1007, -10, 113, 0}, +{ 6, s_2_1008, -1, 110, 0}, +{ 6, s_2_1009, -2, 111, 0}, +{ 6, s_2_1010, -3, 112, 0}, +{ 5, s_2_1011, -14, 97, 0}, +{ 5, s_2_1012, -15, 96, 0}, +{ 5, s_2_1013, -16, 98, 0}, +{ 5, s_2_1014, -17, 76, 0}, +{ 5, s_2_1015, -18, 99, 0}, +{ 6, s_2_1016, -19, 102, 0}, +{ 3, s_2_1017, 0, 83, 0}, +{ 3, s_2_1018, 0, 116, 0}, +{ 5, s_2_1019, -1, 124, 0}, +{ 6, s_2_1020, -2, 121, 0}, +{ 4, s_2_1021, -3, 103, 0}, +{ 6, s_2_1022, -4, 127, 0}, +{ 6, s_2_1023, -5, 118, 0}, +{ 5, s_2_1024, -6, 48, 0}, +{ 6, s_2_1025, -7, 101, 0}, +{ 7, s_2_1026, -8, 117, 0}, +{ 7, s_2_1027, -9, 90, 0}, +{ 3, s_2_1028, 0, 50, 0}, +{ 4, s_2_1029, 0, 115, 0}, +{ 4, s_2_1030, 0, 13, 0}, +{ 4, s_2_1031, 0, 52, 0}, +{ 4, s_2_1032, 0, 51, 0}, +{ 2, s_2_1033, 0, 13, 0}, +{ 3, s_2_1034, -1, 10, 0}, +{ 5, s_2_1035, -1, 128, 0}, +{ 5, s_2_1036, -2, 105, 0}, +{ 4, s_2_1037, -3, 113, 0}, +{ 5, s_2_1038, -4, 97, 0}, +{ 5, s_2_1039, -5, 96, 0}, +{ 5, s_2_1040, -6, 98, 0}, +{ 5, s_2_1041, -7, 99, 0}, +{ 6, s_2_1042, -8, 102, 0}, +{ 5, s_2_1043, -10, 124, 0}, +{ 6, s_2_1044, -11, 121, 0}, +{ 6, s_2_1045, -12, 101, 0}, +{ 7, s_2_1046, -13, 117, 0}, +{ 3, s_2_1047, -14, 11, 0}, +{ 4, s_2_1048, -1, 137, 0}, +{ 5, s_2_1049, -2, 89, 0}, +{ 3, s_2_1050, -17, 12, 0}, +{ 3, s_2_1051, 0, 53, 0}, +{ 3, s_2_1052, 0, 54, 0}, +{ 3, s_2_1053, 0, 55, 0}, +{ 3, s_2_1054, 0, 56, 0}, +{ 4, s_2_1055, 0, 135, 0}, +{ 4, s_2_1056, 0, 131, 0}, +{ 4, s_2_1057, 0, 129, 0}, +{ 4, s_2_1058, 0, 133, 0}, +{ 4, s_2_1059, 0, 132, 0}, +{ 4, s_2_1060, 0, 130, 0}, +{ 4, s_2_1061, 0, 134, 0}, +{ 3, s_2_1062, 0, 152, 0}, +{ 3, s_2_1063, 0, 154, 0}, +{ 3, s_2_1064, 0, 123, 0}, +{ 4, s_2_1065, 0, 161, 0}, +{ 6, s_2_1066, -1, 128, 0}, +{ 6, s_2_1067, -2, 155, 0}, +{ 5, s_2_1068, -3, 160, 0}, +{ 6, s_2_1069, -1, 153, 0}, +{ 7, s_2_1070, -2, 141, 0}, +{ 7, s_2_1071, -3, 142, 0}, +{ 7, s_2_1072, -4, 143, 0}, +{ 4, s_2_1073, 0, 162, 0}, +{ 5, s_2_1074, -1, 158, 0}, +{ 7, s_2_1075, -2, 127, 0}, +{ 5, s_2_1076, 0, 164, 0}, +{ 3, s_2_1077, 0, 104, 0}, +{ 5, s_2_1078, -1, 128, 0}, +{ 8, s_2_1079, -2, 106, 0}, +{ 8, s_2_1080, -3, 107, 0}, +{ 8, s_2_1081, -4, 108, 0}, +{ 6, s_2_1082, -5, 114, 0}, +{ 5, s_2_1083, -6, 68, 0}, +{ 4, s_2_1084, -7, 69, 0}, +{ 5, s_2_1085, -8, 100, 0}, +{ 5, s_2_1086, -9, 105, 0}, +{ 4, s_2_1087, -10, 113, 0}, +{ 6, s_2_1088, -1, 110, 0}, +{ 6, s_2_1089, -2, 111, 0}, +{ 6, s_2_1090, -3, 112, 0}, +{ 5, s_2_1091, -14, 97, 0}, +{ 5, s_2_1092, -15, 96, 0}, +{ 5, s_2_1093, -16, 98, 0}, +{ 5, s_2_1094, -17, 76, 0}, +{ 5, s_2_1095, -18, 99, 0}, +{ 6, s_2_1096, -19, 102, 0}, +{ 3, s_2_1097, 0, 70, 0}, +{ 3, s_2_1098, 0, 116, 0}, +{ 5, s_2_1099, -1, 124, 0}, +{ 6, s_2_1100, -2, 121, 0}, +{ 4, s_2_1101, -3, 103, 0}, +{ 6, s_2_1102, -4, 127, 0}, +{ 6, s_2_1103, -5, 118, 0}, +{ 5, s_2_1104, -6, 92, 0}, +{ 5, s_2_1105, -7, 93, 0}, +{ 6, s_2_1106, -8, 101, 0}, +{ 7, s_2_1107, -9, 117, 0}, +{ 7, s_2_1108, -10, 90, 0}, +{ 4, s_2_1109, 0, 94, 0}, +{ 4, s_2_1110, 0, 71, 0}, +{ 4, s_2_1111, 0, 72, 0}, +{ 4, s_2_1112, 0, 73, 0}, +{ 4, s_2_1113, 0, 74, 0}, +{ 4, s_2_1114, 0, 13, 0}, +{ 3, s_2_1115, 0, 77, 0}, +{ 3, s_2_1116, 0, 78, 0}, +{ 5, s_2_1117, -1, 109, 0}, +{ 6, s_2_1118, -1, 26, 0}, +{ 6, s_2_1119, -2, 30, 0}, +{ 6, s_2_1120, -3, 31, 0}, +{ 7, s_2_1121, -4, 28, 0}, +{ 7, s_2_1122, -5, 27, 0}, +{ 7, s_2_1123, -6, 29, 0}, +{ 3, s_2_1124, 0, 79, 0}, +{ 3, s_2_1125, 0, 80, 0}, +{ 4, s_2_1126, -1, 20, 0}, +{ 5, s_2_1127, -1, 17, 0}, +{ 4, s_2_1128, -3, 82, 0}, +{ 5, s_2_1129, -1, 49, 0}, +{ 4, s_2_1130, -5, 81, 0}, +{ 5, s_2_1131, -6, 12, 0}, +{ 5, s_2_1132, 0, 116, 0}, +{ 7, s_2_1133, 0, 101, 0}, +{ 6, s_2_1134, 0, 104, 0}, +{ 8, s_2_1135, -1, 100, 0}, +{ 8, s_2_1136, -2, 105, 0}, +{ 9, s_2_1137, -3, 106, 0}, +{ 9, s_2_1138, -4, 107, 0}, +{ 9, s_2_1139, -5, 108, 0}, +{ 8, s_2_1140, -6, 97, 0}, +{ 8, s_2_1141, -7, 96, 0}, +{ 8, s_2_1142, -8, 98, 0}, +{ 8, s_2_1143, -9, 99, 0}, +{ 6, s_2_1144, 0, 25, 0}, +{ 8, s_2_1145, -1, 100, 0}, +{ 10, s_2_1146, -2, 117, 0}, +{ 5, s_2_1147, 0, 13, 0}, +{ 6, s_2_1148, 0, 70, 0}, +{ 7, s_2_1149, 0, 115, 0}, +{ 4, s_2_1150, 0, 101, 0}, +{ 5, s_2_1151, 0, 117, 0}, +{ 5, s_2_1152, 0, 63, 0}, +{ 5, s_2_1153, 0, 64, 0}, +{ 5, s_2_1154, 0, 61, 0}, +{ 5, s_2_1155, 0, 62, 0}, +{ 5, s_2_1156, 0, 60, 0}, +{ 5, s_2_1157, 0, 59, 0}, +{ 5, s_2_1158, 0, 65, 0}, +{ 4, s_2_1159, 0, 66, 0}, +{ 4, s_2_1160, 0, 67, 0}, +{ 4, s_2_1161, 0, 91, 0}, +{ 5, s_2_1162, 0, 104, 0}, +{ 7, s_2_1163, -1, 100, 0}, +{ 6, s_2_1164, -2, 113, 0}, +{ 7, s_2_1165, -1, 70, 0}, +{ 8, s_2_1166, -2, 110, 0}, +{ 8, s_2_1167, -3, 111, 0}, +{ 8, s_2_1168, -4, 112, 0}, +{ 8, s_2_1169, -7, 102, 0}, +{ 5, s_2_1170, 0, 116, 0}, +{ 6, s_2_1171, -1, 103, 0}, +{ 9, s_2_1172, -2, 90, 0}, +{ 6, s_2_1173, 0, 13, 0}, +{ 2, s_2_1174, 0, 104, 0}, +{ 4, s_2_1175, -1, 105, 0}, +{ 3, s_2_1176, -2, 113, 0}, +{ 4, s_2_1177, -3, 97, 0}, +{ 4, s_2_1178, -4, 96, 0}, +{ 4, s_2_1179, -5, 98, 0}, +{ 4, s_2_1180, -6, 99, 0}, +{ 2, s_2_1181, 0, 116, 0}, +{ 4, s_2_1182, 0, 124, 0}, +{ 4, s_2_1183, 0, 125, 0}, +{ 4, s_2_1184, 0, 126, 0}, +{ 7, s_2_1185, 0, 84, 0}, +{ 7, s_2_1186, 0, 85, 0}, +{ 7, s_2_1187, 0, 122, 0}, +{ 8, s_2_1188, 0, 86, 0}, +{ 5, s_2_1189, 0, 95, 0}, +{ 6, s_2_1190, -1, 1, 0}, +{ 7, s_2_1191, -2, 2, 0}, +{ 6, s_2_1192, 0, 83, 0}, +{ 5, s_2_1193, 0, 13, 0}, +{ 6, s_2_1194, 0, 123, 0}, +{ 8, s_2_1195, 0, 92, 0}, +{ 8, s_2_1196, 0, 93, 0}, +{ 7, s_2_1197, 0, 94, 0}, +{ 6, s_2_1198, 0, 77, 0}, +{ 6, s_2_1199, 0, 78, 0}, +{ 6, s_2_1200, 0, 79, 0}, +{ 6, s_2_1201, 0, 80, 0}, +{ 7, s_2_1202, 0, 91, 0}, +{ 5, s_2_1203, 0, 84, 0}, +{ 5, s_2_1204, 0, 85, 0}, +{ 5, s_2_1205, 0, 122, 0}, +{ 6, s_2_1206, 0, 86, 0}, +{ 3, s_2_1207, 0, 95, 0}, +{ 4, s_2_1208, -1, 1, 0}, +{ 5, s_2_1209, -2, 2, 0}, +{ 4, s_2_1210, 0, 104, 0}, +{ 4, s_2_1211, 0, 83, 0}, +{ 3, s_2_1212, 0, 13, 0}, +{ 5, s_2_1213, -1, 137, 0}, +{ 6, s_2_1214, -2, 89, 0}, +{ 4, s_2_1215, 0, 123, 0}, +{ 4, s_2_1216, 0, 120, 0}, +{ 6, s_2_1217, 0, 92, 0}, +{ 6, s_2_1218, 0, 93, 0}, +{ 5, s_2_1219, 0, 94, 0}, +{ 4, s_2_1220, 0, 77, 0}, +{ 4, s_2_1221, 0, 78, 0}, +{ 4, s_2_1222, 0, 79, 0}, +{ 4, s_2_1223, 0, 80, 0}, +{ 5, s_2_1224, 0, 14, 0}, +{ 5, s_2_1225, 0, 15, 0}, +{ 5, s_2_1226, 0, 16, 0}, +{ 5, s_2_1227, 0, 91, 0}, +{ 5, s_2_1228, 0, 121, 0}, +{ 4, s_2_1229, 0, 100, 0}, +{ 6, s_2_1230, 0, 117, 0}, +{ 2, s_2_1231, 0, 104, 0}, +{ 4, s_2_1232, -1, 100, 0}, +{ 4, s_2_1233, -2, 105, 0}, +{ 2, s_2_1234, 0, 119, 0}, +{ 2, s_2_1235, 0, 116, 0}, +{ 2, s_2_1236, 0, 104, 0}, +{ 4, s_2_1237, -1, 128, 0}, +{ 4, s_2_1238, -2, 100, 0}, +{ 4, s_2_1239, -3, 105, 0}, +{ 3, s_2_1240, -4, 113, 0}, +{ 4, s_2_1241, -5, 97, 0}, +{ 4, s_2_1242, -6, 96, 0}, +{ 4, s_2_1243, -7, 98, 0}, +{ 4, s_2_1244, -8, 99, 0}, +{ 5, s_2_1245, -9, 102, 0}, +{ 2, s_2_1246, 0, 119, 0}, +{ 4, s_2_1247, -1, 124, 0}, +{ 4, s_2_1248, -2, 125, 0}, +{ 4, s_2_1249, -3, 126, 0}, +{ 7, s_2_1250, -4, 110, 0}, +{ 7, s_2_1251, -5, 111, 0}, +{ 7, s_2_1252, -6, 112, 0}, +{ 4, s_2_1253, -7, 104, 0}, +{ 5, s_2_1254, -1, 26, 0}, +{ 5, s_2_1255, -2, 30, 0}, +{ 5, s_2_1256, -3, 31, 0}, +{ 7, s_2_1257, -4, 106, 0}, +{ 7, s_2_1258, -5, 107, 0}, +{ 7, s_2_1259, -6, 108, 0}, +{ 6, s_2_1260, -7, 28, 0}, +{ 6, s_2_1261, -8, 27, 0}, +{ 6, s_2_1262, -9, 29, 0}, +{ 4, s_2_1263, -17, 116, 0}, +{ 7, s_2_1264, -1, 84, 0}, +{ 7, s_2_1265, -2, 85, 0}, +{ 7, s_2_1266, -3, 123, 0}, +{ 8, s_2_1267, -4, 86, 0}, +{ 5, s_2_1268, -5, 95, 0}, +{ 6, s_2_1269, -1, 1, 0}, +{ 7, s_2_1270, -2, 2, 0}, +{ 5, s_2_1271, -8, 24, 0}, +{ 6, s_2_1272, -1, 83, 0}, +{ 5, s_2_1273, -10, 13, 0}, +{ 7, s_2_1274, -11, 21, 0}, +{ 5, s_2_1275, -12, 23, 0}, +{ 6, s_2_1276, -1, 123, 0}, +{ 6, s_2_1277, -14, 120, 0}, +{ 8, s_2_1278, -15, 92, 0}, +{ 8, s_2_1279, -16, 93, 0}, +{ 6, s_2_1280, -17, 22, 0}, +{ 7, s_2_1281, -18, 94, 0}, +{ 6, s_2_1282, -19, 77, 0}, +{ 6, s_2_1283, -20, 78, 0}, +{ 6, s_2_1284, -21, 79, 0}, +{ 6, s_2_1285, -22, 80, 0}, +{ 7, s_2_1286, -23, 91, 0}, +{ 5, s_2_1287, -41, 84, 0}, +{ 5, s_2_1288, -42, 85, 0}, +{ 5, s_2_1289, -43, 114, 0}, +{ 5, s_2_1290, -44, 122, 0}, +{ 6, s_2_1291, -45, 86, 0}, +{ 4, s_2_1292, -46, 25, 0}, +{ 7, s_2_1293, -1, 121, 0}, +{ 6, s_2_1294, -2, 100, 0}, +{ 8, s_2_1295, -3, 117, 0}, +{ 3, s_2_1296, -50, 95, 0}, +{ 4, s_2_1297, -1, 1, 0}, +{ 5, s_2_1298, -2, 2, 0}, +{ 4, s_2_1299, -53, 83, 0}, +{ 3, s_2_1300, -54, 13, 0}, +{ 4, s_2_1301, -1, 10, 0}, +{ 7, s_2_1302, -1, 110, 0}, +{ 7, s_2_1303, -2, 111, 0}, +{ 7, s_2_1304, -3, 112, 0}, +{ 4, s_2_1305, -5, 87, 0}, +{ 4, s_2_1306, -6, 159, 0}, +{ 5, s_2_1307, -7, 88, 0}, +{ 5, s_2_1308, -62, 135, 0}, +{ 5, s_2_1309, -63, 131, 0}, +{ 5, s_2_1310, -64, 129, 0}, +{ 5, s_2_1311, -65, 133, 0}, +{ 5, s_2_1312, -66, 132, 0}, +{ 5, s_2_1313, -67, 130, 0}, +{ 5, s_2_1314, -68, 134, 0}, +{ 4, s_2_1315, -69, 152, 0}, +{ 4, s_2_1316, -70, 154, 0}, +{ 4, s_2_1317, -71, 123, 0}, +{ 4, s_2_1318, -72, 120, 0}, +{ 4, s_2_1319, -73, 70, 0}, +{ 6, s_2_1320, -74, 92, 0}, +{ 6, s_2_1321, -75, 93, 0}, +{ 5, s_2_1322, -76, 94, 0}, +{ 5, s_2_1323, -77, 151, 0}, +{ 6, s_2_1324, -78, 75, 0}, +{ 4, s_2_1325, -79, 77, 0}, +{ 4, s_2_1326, -80, 78, 0}, +{ 4, s_2_1327, -81, 79, 0}, +{ 5, s_2_1328, -82, 14, 0}, +{ 5, s_2_1329, -83, 15, 0}, +{ 5, s_2_1330, -84, 16, 0}, +{ 6, s_2_1331, -85, 63, 0}, +{ 6, s_2_1332, -86, 64, 0}, +{ 6, s_2_1333, -87, 61, 0}, +{ 6, s_2_1334, -88, 62, 0}, +{ 6, s_2_1335, -89, 60, 0}, +{ 6, s_2_1336, -90, 59, 0}, +{ 6, s_2_1337, -91, 65, 0}, +{ 5, s_2_1338, -92, 66, 0}, +{ 5, s_2_1339, -93, 67, 0}, +{ 5, s_2_1340, -94, 91, 0}, +{ 2, s_2_1341, 0, 116, 0}, +{ 4, s_2_1342, -1, 124, 0}, +{ 4, s_2_1343, -2, 125, 0}, +{ 4, s_2_1344, -3, 126, 0}, +{ 5, s_2_1345, -1, 121, 0}, +{ 7, s_2_1346, -5, 84, 0}, +{ 7, s_2_1347, -6, 85, 0}, +{ 7, s_2_1348, -7, 122, 0}, +{ 8, s_2_1349, -8, 86, 0}, +{ 5, s_2_1350, -9, 95, 0}, +{ 6, s_2_1351, -1, 1, 0}, +{ 7, s_2_1352, -2, 2, 0}, +{ 6, s_2_1353, -12, 83, 0}, +{ 5, s_2_1354, -13, 13, 0}, +{ 6, s_2_1355, -14, 123, 0}, +{ 6, s_2_1356, -15, 120, 0}, +{ 8, s_2_1357, -16, 92, 0}, +{ 8, s_2_1358, -17, 93, 0}, +{ 7, s_2_1359, -18, 94, 0}, +{ 6, s_2_1360, -19, 77, 0}, +{ 6, s_2_1361, -20, 78, 0}, +{ 6, s_2_1362, -21, 79, 0}, +{ 6, s_2_1363, -22, 80, 0}, +{ 7, s_2_1364, -23, 91, 0}, +{ 5, s_2_1365, -24, 84, 0}, +{ 5, s_2_1366, -25, 85, 0}, +{ 5, s_2_1367, -26, 122, 0}, +{ 6, s_2_1368, -27, 86, 0}, +{ 3, s_2_1369, -28, 95, 0}, +{ 4, s_2_1370, -1, 1, 0}, +{ 5, s_2_1371, -2, 2, 0}, +{ 4, s_2_1372, -31, 83, 0}, +{ 3, s_2_1373, -32, 13, 0}, +{ 5, s_2_1374, -1, 137, 0}, +{ 6, s_2_1375, -2, 89, 0}, +{ 4, s_2_1376, -35, 123, 0}, +{ 5, s_2_1377, -1, 127, 0}, +{ 4, s_2_1378, -37, 120, 0}, +{ 5, s_2_1379, -38, 118, 0}, +{ 6, s_2_1380, -39, 92, 0}, +{ 6, s_2_1381, -40, 93, 0}, +{ 5, s_2_1382, -41, 94, 0}, +{ 4, s_2_1383, -42, 77, 0}, +{ 4, s_2_1384, -43, 78, 0}, +{ 4, s_2_1385, -44, 79, 0}, +{ 4, s_2_1386, -45, 80, 0}, +{ 5, s_2_1387, -46, 14, 0}, +{ 5, s_2_1388, -47, 15, 0}, +{ 5, s_2_1389, -48, 16, 0}, +{ 5, s_2_1390, -49, 101, 0}, +{ 6, s_2_1391, -50, 117, 0}, +{ 5, s_2_1392, -51, 91, 0}, +{ 6, s_2_1393, -1, 90, 0}, +{ 4, s_2_1394, 0, 124, 0}, +{ 4, s_2_1395, 0, 125, 0}, +{ 4, s_2_1396, 0, 126, 0}, +{ 3, s_2_1397, 0, 20, 0}, +{ 5, s_2_1398, -1, 19, 0}, +{ 4, s_2_1399, -2, 18, 0}, +{ 5, s_2_1400, 0, 32, 0}, +{ 5, s_2_1401, 0, 33, 0}, +{ 5, s_2_1402, 0, 34, 0}, +{ 5, s_2_1403, 0, 40, 0}, +{ 5, s_2_1404, 0, 39, 0}, +{ 5, s_2_1405, 0, 35, 0}, +{ 5, s_2_1406, 0, 37, 0}, +{ 5, s_2_1407, 0, 36, 0}, +{ 7, s_2_1408, -1, 9, 0}, +{ 7, s_2_1409, -2, 6, 0}, +{ 7, s_2_1410, -3, 7, 0}, +{ 7, s_2_1411, -4, 8, 0}, +{ 7, s_2_1412, -5, 5, 0}, +{ 5, s_2_1413, 0, 41, 0}, +{ 5, s_2_1414, 0, 42, 0}, +{ 5, s_2_1415, 0, 43, 0}, +{ 5, s_2_1416, 0, 44, 0}, +{ 5, s_2_1417, 0, 45, 0}, +{ 6, s_2_1418, 0, 38, 0}, +{ 5, s_2_1419, 0, 84, 0}, +{ 5, s_2_1420, 0, 85, 0}, +{ 5, s_2_1421, 0, 122, 0}, +{ 6, s_2_1422, 0, 86, 0}, +{ 3, s_2_1423, 0, 95, 0}, +{ 4, s_2_1424, -1, 1, 0}, +{ 5, s_2_1425, -2, 2, 0}, +{ 4, s_2_1426, 0, 104, 0}, +{ 6, s_2_1427, -1, 47, 0}, +{ 5, s_2_1428, -2, 46, 0}, +{ 4, s_2_1429, 0, 83, 0}, +{ 4, s_2_1430, 0, 116, 0}, +{ 6, s_2_1431, -1, 48, 0}, +{ 4, s_2_1432, 0, 50, 0}, +{ 5, s_2_1433, 0, 52, 0}, +{ 5, s_2_1434, 0, 51, 0}, +{ 3, s_2_1435, 0, 13, 0}, +{ 4, s_2_1436, -1, 10, 0}, +{ 4, s_2_1437, -2, 11, 0}, +{ 5, s_2_1438, -1, 137, 0}, +{ 6, s_2_1439, -2, 10, 0}, +{ 6, s_2_1440, -3, 89, 0}, +{ 4, s_2_1441, -6, 12, 0}, +{ 4, s_2_1442, 0, 53, 0}, +{ 4, s_2_1443, 0, 54, 0}, +{ 4, s_2_1444, 0, 55, 0}, +{ 4, s_2_1445, 0, 56, 0}, +{ 5, s_2_1446, 0, 135, 0}, +{ 5, s_2_1447, 0, 131, 0}, +{ 5, s_2_1448, 0, 129, 0}, +{ 5, s_2_1449, 0, 133, 0}, +{ 5, s_2_1450, 0, 132, 0}, +{ 5, s_2_1451, 0, 130, 0}, +{ 5, s_2_1452, 0, 134, 0}, +{ 4, s_2_1453, 0, 57, 0}, +{ 4, s_2_1454, 0, 58, 0}, +{ 4, s_2_1455, 0, 123, 0}, +{ 4, s_2_1456, 0, 120, 0}, +{ 6, s_2_1457, -1, 68, 0}, +{ 5, s_2_1458, -2, 69, 0}, +{ 4, s_2_1459, 0, 70, 0}, +{ 6, s_2_1460, 0, 92, 0}, +{ 6, s_2_1461, 0, 93, 0}, +{ 5, s_2_1462, 0, 94, 0}, +{ 5, s_2_1463, 0, 71, 0}, +{ 5, s_2_1464, 0, 72, 0}, +{ 5, s_2_1465, 0, 73, 0}, +{ 5, s_2_1466, 0, 74, 0}, +{ 4, s_2_1467, 0, 77, 0}, +{ 4, s_2_1468, 0, 78, 0}, +{ 4, s_2_1469, 0, 79, 0}, +{ 4, s_2_1470, 0, 80, 0}, +{ 5, s_2_1471, -1, 82, 0}, +{ 5, s_2_1472, -2, 81, 0}, +{ 5, s_2_1473, 0, 3, 0}, +{ 6, s_2_1474, 0, 4, 0}, +{ 5, s_2_1475, 0, 14, 0}, +{ 5, s_2_1476, 0, 15, 0}, +{ 5, s_2_1477, 0, 16, 0}, +{ 6, s_2_1478, 0, 63, 0}, +{ 6, s_2_1479, 0, 64, 0}, +{ 6, s_2_1480, 0, 61, 0}, +{ 6, s_2_1481, 0, 62, 0}, +{ 6, s_2_1482, 0, 60, 0}, +{ 6, s_2_1483, 0, 59, 0}, +{ 6, s_2_1484, 0, 65, 0}, +{ 5, s_2_1485, 0, 66, 0}, +{ 5, s_2_1486, 0, 67, 0}, +{ 5, s_2_1487, 0, 91, 0}, +{ 2, s_2_1488, 0, 104, 0}, +{ 4, s_2_1489, -1, 128, 0}, +{ 4, s_2_1490, -2, 100, 0}, +{ 4, s_2_1491, -3, 105, 0}, +{ 3, s_2_1492, -4, 113, 0}, +{ 4, s_2_1493, -5, 97, 0}, +{ 4, s_2_1494, -6, 96, 0}, +{ 4, s_2_1495, -7, 98, 0}, +{ 4, s_2_1496, -8, 99, 0}, +{ 5, s_2_1497, -9, 102, 0}, +{ 4, s_2_1498, 0, 124, 0}, +{ 5, s_2_1499, 0, 121, 0}, +{ 5, s_2_1500, 0, 101, 0}, +{ 6, s_2_1501, 0, 117, 0}, +{ 4, s_2_1502, 0, 10, 0}, +{ 2, s_2_1503, 0, 104, 0}, +{ 4, s_2_1504, -1, 128, 0}, +{ 7, s_2_1505, -2, 106, 0}, +{ 7, s_2_1506, -3, 107, 0}, +{ 7, s_2_1507, -4, 108, 0}, +{ 5, s_2_1508, -5, 114, 0}, +{ 4, s_2_1509, -6, 100, 0}, +{ 4, s_2_1510, -7, 105, 0}, +{ 3, s_2_1511, -8, 113, 0}, +{ 5, s_2_1512, -1, 110, 0}, +{ 5, s_2_1513, -2, 111, 0}, +{ 5, s_2_1514, -3, 112, 0}, +{ 4, s_2_1515, -12, 97, 0}, +{ 4, s_2_1516, -13, 96, 0}, +{ 4, s_2_1517, -14, 98, 0}, +{ 4, s_2_1518, -15, 76, 0}, +{ 4, s_2_1519, -16, 99, 0}, +{ 5, s_2_1520, -17, 102, 0}, +{ 2, s_2_1521, 0, 20, 0}, +{ 3, s_2_1522, -1, 18, 0}, +{ 2, s_2_1523, 0, 116, 0}, +{ 4, s_2_1524, -1, 124, 0}, +{ 5, s_2_1525, -2, 121, 0}, +{ 3, s_2_1526, -3, 24, 0}, +{ 3, s_2_1527, -4, 103, 0}, +{ 5, s_2_1528, -5, 21, 0}, +{ 3, s_2_1529, -6, 23, 0}, +{ 5, s_2_1530, -1, 127, 0}, +{ 5, s_2_1531, -8, 118, 0}, +{ 4, s_2_1532, -9, 22, 0}, +{ 5, s_2_1533, -10, 101, 0}, +{ 6, s_2_1534, -11, 117, 0}, +{ 6, s_2_1535, -12, 90, 0}, +{ 4, s_2_1536, 0, 32, 0}, +{ 4, s_2_1537, 0, 33, 0}, +{ 4, s_2_1538, 0, 34, 0}, +{ 4, s_2_1539, 0, 40, 0}, +{ 4, s_2_1540, 0, 39, 0}, +{ 4, s_2_1541, 0, 35, 0}, +{ 4, s_2_1542, 0, 37, 0}, +{ 4, s_2_1543, 0, 36, 0}, +{ 4, s_2_1544, 0, 41, 0}, +{ 4, s_2_1545, 0, 42, 0}, +{ 4, s_2_1546, 0, 43, 0}, +{ 4, s_2_1547, 0, 44, 0}, +{ 4, s_2_1548, 0, 45, 0}, +{ 5, s_2_1549, 0, 38, 0}, +{ 4, s_2_1550, 0, 84, 0}, +{ 4, s_2_1551, 0, 85, 0}, +{ 4, s_2_1552, 0, 122, 0}, +{ 5, s_2_1553, 0, 86, 0}, +{ 2, s_2_1554, 0, 95, 0}, +{ 3, s_2_1555, -1, 1, 0}, +{ 4, s_2_1556, -2, 2, 0}, +{ 3, s_2_1557, 0, 104, 0}, +{ 5, s_2_1558, -1, 128, 0}, +{ 8, s_2_1559, -2, 106, 0}, +{ 8, s_2_1560, -3, 107, 0}, +{ 8, s_2_1561, -4, 108, 0}, +{ 5, s_2_1562, -5, 47, 0}, +{ 6, s_2_1563, -6, 114, 0}, +{ 4, s_2_1564, -7, 46, 0}, +{ 5, s_2_1565, -8, 100, 0}, +{ 5, s_2_1566, -9, 105, 0}, +{ 4, s_2_1567, -10, 113, 0}, +{ 6, s_2_1568, -1, 110, 0}, +{ 6, s_2_1569, -2, 111, 0}, +{ 6, s_2_1570, -3, 112, 0}, +{ 5, s_2_1571, -14, 97, 0}, +{ 5, s_2_1572, -15, 96, 0}, +{ 5, s_2_1573, -16, 98, 0}, +{ 5, s_2_1574, -17, 76, 0}, +{ 5, s_2_1575, -18, 99, 0}, +{ 6, s_2_1576, -19, 102, 0}, +{ 3, s_2_1577, 0, 83, 0}, +{ 3, s_2_1578, 0, 116, 0}, +{ 5, s_2_1579, -1, 124, 0}, +{ 6, s_2_1580, -2, 121, 0}, +{ 4, s_2_1581, -3, 103, 0}, +{ 6, s_2_1582, -4, 127, 0}, +{ 6, s_2_1583, -5, 118, 0}, +{ 6, s_2_1584, -6, 101, 0}, +{ 7, s_2_1585, -7, 117, 0}, +{ 7, s_2_1586, -8, 90, 0}, +{ 4, s_2_1587, 0, 115, 0}, +{ 4, s_2_1588, 0, 13, 0}, +{ 3, s_2_1589, 0, 104, 0}, +{ 5, s_2_1590, -1, 128, 0}, +{ 4, s_2_1591, -2, 52, 0}, +{ 5, s_2_1592, -1, 100, 0}, +{ 5, s_2_1593, -2, 105, 0}, +{ 4, s_2_1594, -5, 113, 0}, +{ 5, s_2_1595, -6, 97, 0}, +{ 5, s_2_1596, -7, 96, 0}, +{ 5, s_2_1597, -8, 98, 0}, +{ 5, s_2_1598, -9, 99, 0}, +{ 6, s_2_1599, -10, 102, 0}, +{ 3, s_2_1600, 0, 119, 0}, +{ 8, s_2_1601, -1, 110, 0}, +{ 8, s_2_1602, -2, 111, 0}, +{ 8, s_2_1603, -3, 112, 0}, +{ 8, s_2_1604, -4, 106, 0}, +{ 8, s_2_1605, -5, 107, 0}, +{ 8, s_2_1606, -6, 108, 0}, +{ 5, s_2_1607, -7, 116, 0}, +{ 6, s_2_1608, -8, 114, 0}, +{ 5, s_2_1609, -9, 25, 0}, +{ 8, s_2_1610, -1, 121, 0}, +{ 7, s_2_1611, -2, 100, 0}, +{ 9, s_2_1612, -3, 117, 0}, +{ 4, s_2_1613, -13, 51, 0}, +{ 4, s_2_1614, -14, 13, 0}, +{ 8, s_2_1615, -1, 110, 0}, +{ 8, s_2_1616, -2, 111, 0}, +{ 8, s_2_1617, -3, 112, 0}, +{ 5, s_2_1618, -18, 70, 0}, +{ 6, s_2_1619, -19, 115, 0}, +{ 3, s_2_1620, 0, 116, 0}, +{ 5, s_2_1621, -1, 124, 0}, +{ 6, s_2_1622, -2, 121, 0}, +{ 4, s_2_1623, -3, 13, 0}, +{ 8, s_2_1624, -1, 110, 0}, +{ 8, s_2_1625, -2, 111, 0}, +{ 8, s_2_1626, -3, 112, 0}, +{ 6, s_2_1627, -7, 127, 0}, +{ 5, s_2_1628, -8, 70, 0}, +{ 6, s_2_1629, -1, 118, 0}, +{ 6, s_2_1630, -10, 115, 0}, +{ 6, s_2_1631, -11, 101, 0}, +{ 7, s_2_1632, -12, 117, 0}, +{ 7, s_2_1633, -13, 90, 0}, +{ 4, s_2_1634, 0, 104, 0}, +{ 6, s_2_1635, -1, 105, 0}, +{ 5, s_2_1636, -2, 113, 0}, +{ 7, s_2_1637, -1, 106, 0}, +{ 7, s_2_1638, -2, 107, 0}, +{ 7, s_2_1639, -3, 108, 0}, +{ 6, s_2_1640, -6, 97, 0}, +{ 6, s_2_1641, -7, 96, 0}, +{ 6, s_2_1642, -8, 98, 0}, +{ 6, s_2_1643, -9, 99, 0}, +{ 4, s_2_1644, 0, 116, 0}, +{ 4, s_2_1645, 0, 25, 0}, +{ 7, s_2_1646, -1, 121, 0}, +{ 6, s_2_1647, -2, 100, 0}, +{ 8, s_2_1648, -3, 117, 0}, +{ 4, s_2_1649, 0, 104, 0}, +{ 6, s_2_1650, -1, 128, 0}, +{ 9, s_2_1651, -2, 106, 0}, +{ 9, s_2_1652, -3, 107, 0}, +{ 9, s_2_1653, -4, 108, 0}, +{ 7, s_2_1654, -5, 114, 0}, +{ 6, s_2_1655, -6, 100, 0}, +{ 6, s_2_1656, -7, 105, 0}, +{ 5, s_2_1657, -8, 113, 0}, +{ 6, s_2_1658, -9, 97, 0}, +{ 6, s_2_1659, -10, 96, 0}, +{ 6, s_2_1660, -11, 98, 0}, +{ 6, s_2_1661, -12, 76, 0}, +{ 6, s_2_1662, -13, 99, 0}, +{ 7, s_2_1663, -14, 102, 0}, +{ 4, s_2_1664, 0, 116, 0}, +{ 6, s_2_1665, -1, 124, 0}, +{ 7, s_2_1666, -2, 121, 0}, +{ 5, s_2_1667, -3, 103, 0}, +{ 7, s_2_1668, -4, 127, 0}, +{ 7, s_2_1669, -5, 118, 0}, +{ 7, s_2_1670, -6, 101, 0}, +{ 8, s_2_1671, -7, 117, 0}, +{ 8, s_2_1672, -8, 90, 0}, +{ 9, s_2_1673, 0, 110, 0}, +{ 9, s_2_1674, 0, 111, 0}, +{ 9, s_2_1675, 0, 112, 0}, +{ 5, s_2_1676, 0, 13, 0}, +{ 2, s_2_1677, 0, 13, 0}, +{ 3, s_2_1678, -1, 104, 0}, +{ 5, s_2_1679, -1, 128, 0}, +{ 5, s_2_1680, -2, 105, 0}, +{ 4, s_2_1681, -3, 113, 0}, +{ 5, s_2_1682, -4, 97, 0}, +{ 5, s_2_1683, -5, 96, 0}, +{ 5, s_2_1684, -6, 98, 0}, +{ 5, s_2_1685, -7, 99, 0}, +{ 6, s_2_1686, -8, 102, 0}, +{ 5, s_2_1687, -10, 124, 0}, +{ 6, s_2_1688, -11, 121, 0}, +{ 6, s_2_1689, -12, 101, 0}, +{ 7, s_2_1690, -13, 117, 0}, +{ 3, s_2_1691, -14, 11, 0}, +{ 4, s_2_1692, -1, 137, 0}, +{ 5, s_2_1693, -2, 89, 0}, +{ 3, s_2_1694, 0, 120, 0}, +{ 5, s_2_1695, -1, 68, 0}, +{ 4, s_2_1696, -2, 69, 0}, +{ 3, s_2_1697, 0, 70, 0}, +{ 5, s_2_1698, 0, 92, 0}, +{ 5, s_2_1699, 0, 93, 0}, +{ 4, s_2_1700, 0, 94, 0}, +{ 4, s_2_1701, 0, 71, 0}, +{ 4, s_2_1702, 0, 72, 0}, +{ 4, s_2_1703, 0, 73, 0}, +{ 4, s_2_1704, 0, 74, 0}, +{ 4, s_2_1705, 0, 13, 0}, +{ 3, s_2_1706, 0, 13, 0}, +{ 3, s_2_1707, 0, 77, 0}, +{ 3, s_2_1708, 0, 78, 0}, +{ 3, s_2_1709, 0, 79, 0}, +{ 3, s_2_1710, 0, 80, 0}, +{ 4, s_2_1711, 0, 3, 0}, +{ 5, s_2_1712, 0, 4, 0}, +{ 2, s_2_1713, 0, 161, 0}, +{ 4, s_2_1714, -1, 128, 0}, +{ 4, s_2_1715, -2, 155, 0}, +{ 4, s_2_1716, -3, 156, 0}, +{ 3, s_2_1717, -4, 160, 0}, +{ 4, s_2_1718, -5, 144, 0}, +{ 4, s_2_1719, -6, 145, 0}, +{ 4, s_2_1720, -7, 146, 0}, +{ 4, s_2_1721, -8, 147, 0}, +{ 2, s_2_1722, 0, 163, 0}, +{ 7, s_2_1723, -1, 141, 0}, +{ 7, s_2_1724, -2, 142, 0}, +{ 7, s_2_1725, -3, 143, 0}, +{ 7, s_2_1726, -4, 138, 0}, +{ 7, s_2_1727, -5, 139, 0}, +{ 7, s_2_1728, -6, 140, 0}, +{ 4, s_2_1729, -7, 162, 0}, +{ 5, s_2_1730, -8, 150, 0}, +{ 4, s_2_1731, -9, 157, 0}, +{ 7, s_2_1732, -1, 121, 0}, +{ 6, s_2_1733, -2, 155, 0}, +{ 3, s_2_1734, -12, 164, 0}, +{ 7, s_2_1735, -1, 141, 0}, +{ 7, s_2_1736, -2, 142, 0}, +{ 7, s_2_1737, -3, 143, 0}, +{ 4, s_2_1738, -16, 153, 0}, +{ 5, s_2_1739, -17, 136, 0}, +{ 2, s_2_1740, 0, 162, 0}, +{ 4, s_2_1741, -1, 124, 0}, +{ 5, s_2_1742, -2, 121, 0}, +{ 3, s_2_1743, -3, 158, 0}, +{ 5, s_2_1744, -4, 127, 0}, +{ 5, s_2_1745, -5, 149, 0}, +{ 2, s_2_1746, 0, 104, 0}, +{ 4, s_2_1747, -1, 128, 0}, +{ 7, s_2_1748, -2, 106, 0}, +{ 7, s_2_1749, -3, 107, 0}, +{ 7, s_2_1750, -4, 108, 0}, +{ 5, s_2_1751, -5, 114, 0}, +{ 4, s_2_1752, -6, 100, 0}, +{ 4, s_2_1753, -7, 105, 0}, +{ 3, s_2_1754, -8, 113, 0}, +{ 5, s_2_1755, -1, 110, 0}, +{ 5, s_2_1756, -2, 111, 0}, +{ 5, s_2_1757, -3, 112, 0}, +{ 4, s_2_1758, -12, 97, 0}, +{ 4, s_2_1759, -13, 96, 0}, +{ 4, s_2_1760, -14, 98, 0}, +{ 6, s_2_1761, -1, 100, 0}, +{ 4, s_2_1762, -16, 76, 0}, +{ 4, s_2_1763, -17, 99, 0}, +{ 5, s_2_1764, -18, 102, 0}, +{ 2, s_2_1765, 0, 116, 0}, +{ 4, s_2_1766, -1, 124, 0}, +{ 5, s_2_1767, -2, 121, 0}, +{ 5, s_2_1768, -3, 127, 0}, +{ 5, s_2_1769, -4, 118, 0}, +{ 5, s_2_1770, -5, 101, 0}, +{ 6, s_2_1771, -6, 117, 0}, +{ 6, s_2_1772, -7, 90, 0}, +{ 3, s_2_1773, 0, 13, 0}, +{ 6, s_2_1774, 0, 110, 0}, +{ 6, s_2_1775, 0, 111, 0}, +{ 6, s_2_1776, 0, 112, 0}, +{ 2, s_2_1777, 0, 20, 0}, +{ 4, s_2_1778, -1, 19, 0}, +{ 3, s_2_1779, -2, 18, 0}, +{ 3, s_2_1780, 0, 104, 0}, +{ 5, s_2_1781, -1, 128, 0}, +{ 8, s_2_1782, -2, 106, 0}, +{ 8, s_2_1783, -3, 107, 0}, +{ 8, s_2_1784, -4, 108, 0}, +{ 6, s_2_1785, -5, 114, 0}, +{ 5, s_2_1786, -6, 100, 0}, +{ 5, s_2_1787, -7, 105, 0}, +{ 5, s_2_1788, -8, 97, 0}, +{ 5, s_2_1789, -9, 96, 0}, +{ 5, s_2_1790, -10, 98, 0}, +{ 5, s_2_1791, -11, 76, 0}, +{ 5, s_2_1792, -12, 99, 0}, +{ 6, s_2_1793, -13, 102, 0}, +{ 3, s_2_1794, 0, 104, 0}, +{ 4, s_2_1795, -1, 26, 0}, +{ 5, s_2_1796, -1, 128, 0}, +{ 4, s_2_1797, -3, 30, 0}, +{ 4, s_2_1798, -4, 31, 0}, +{ 5, s_2_1799, -1, 100, 0}, +{ 5, s_2_1800, -2, 105, 0}, +{ 4, s_2_1801, -7, 113, 0}, +{ 6, s_2_1802, -1, 106, 0}, +{ 6, s_2_1803, -2, 107, 0}, +{ 6, s_2_1804, -3, 108, 0}, +{ 5, s_2_1805, -11, 97, 0}, +{ 5, s_2_1806, -12, 96, 0}, +{ 5, s_2_1807, -13, 98, 0}, +{ 5, s_2_1808, -14, 99, 0}, +{ 5, s_2_1809, -15, 28, 0}, +{ 5, s_2_1810, -16, 27, 0}, +{ 6, s_2_1811, -1, 102, 0}, +{ 5, s_2_1812, -18, 29, 0}, +{ 3, s_2_1813, 0, 116, 0}, +{ 4, s_2_1814, -1, 32, 0}, +{ 4, s_2_1815, -2, 33, 0}, +{ 4, s_2_1816, -3, 34, 0}, +{ 4, s_2_1817, -4, 40, 0}, +{ 4, s_2_1818, -5, 39, 0}, +{ 6, s_2_1819, -6, 84, 0}, +{ 6, s_2_1820, -7, 85, 0}, +{ 6, s_2_1821, -8, 122, 0}, +{ 7, s_2_1822, -9, 86, 0}, +{ 4, s_2_1823, -10, 95, 0}, +{ 4, s_2_1824, -11, 24, 0}, +{ 5, s_2_1825, -1, 83, 0}, +{ 4, s_2_1826, -13, 37, 0}, +{ 4, s_2_1827, -14, 13, 0}, +{ 6, s_2_1828, -1, 9, 0}, +{ 6, s_2_1829, -2, 6, 0}, +{ 6, s_2_1830, -3, 7, 0}, +{ 6, s_2_1831, -4, 8, 0}, +{ 6, s_2_1832, -5, 5, 0}, +{ 4, s_2_1833, -20, 41, 0}, +{ 4, s_2_1834, -21, 42, 0}, +{ 6, s_2_1835, -1, 21, 0}, +{ 4, s_2_1836, -23, 23, 0}, +{ 5, s_2_1837, -1, 123, 0}, +{ 4, s_2_1838, -25, 44, 0}, +{ 5, s_2_1839, -1, 120, 0}, +{ 5, s_2_1840, -2, 22, 0}, +{ 5, s_2_1841, -28, 77, 0}, +{ 5, s_2_1842, -29, 78, 0}, +{ 5, s_2_1843, -30, 79, 0}, +{ 5, s_2_1844, -31, 80, 0}, +{ 4, s_2_1845, -32, 45, 0}, +{ 6, s_2_1846, -33, 91, 0}, +{ 5, s_2_1847, -34, 38, 0}, +{ 4, s_2_1848, 0, 84, 0}, +{ 4, s_2_1849, 0, 85, 0}, +{ 4, s_2_1850, 0, 122, 0}, +{ 5, s_2_1851, 0, 86, 0}, +{ 3, s_2_1852, 0, 25, 0}, +{ 6, s_2_1853, -1, 121, 0}, +{ 5, s_2_1854, -2, 100, 0}, +{ 7, s_2_1855, -3, 117, 0}, +{ 2, s_2_1856, 0, 95, 0}, +{ 3, s_2_1857, -1, 1, 0}, +{ 4, s_2_1858, -2, 2, 0}, +{ 3, s_2_1859, 0, 104, 0}, +{ 5, s_2_1860, -1, 47, 0}, +{ 4, s_2_1861, -2, 46, 0}, +{ 3, s_2_1862, 0, 83, 0}, +{ 3, s_2_1863, 0, 116, 0}, +{ 5, s_2_1864, -1, 48, 0}, +{ 3, s_2_1865, 0, 50, 0}, +{ 4, s_2_1866, 0, 52, 0}, +{ 5, s_2_1867, 0, 124, 0}, +{ 5, s_2_1868, 0, 125, 0}, +{ 5, s_2_1869, 0, 126, 0}, +{ 8, s_2_1870, 0, 84, 0}, +{ 8, s_2_1871, 0, 85, 0}, +{ 8, s_2_1872, 0, 122, 0}, +{ 9, s_2_1873, 0, 86, 0}, +{ 6, s_2_1874, 0, 95, 0}, +{ 7, s_2_1875, -1, 1, 0}, +{ 8, s_2_1876, -2, 2, 0}, +{ 7, s_2_1877, 0, 83, 0}, +{ 6, s_2_1878, 0, 13, 0}, +{ 7, s_2_1879, 0, 123, 0}, +{ 7, s_2_1880, 0, 120, 0}, +{ 9, s_2_1881, 0, 92, 0}, +{ 9, s_2_1882, 0, 93, 0}, +{ 8, s_2_1883, 0, 94, 0}, +{ 7, s_2_1884, 0, 77, 0}, +{ 7, s_2_1885, 0, 78, 0}, +{ 7, s_2_1886, 0, 79, 0}, +{ 7, s_2_1887, 0, 80, 0}, +{ 8, s_2_1888, 0, 91, 0}, +{ 6, s_2_1889, 0, 84, 0}, +{ 6, s_2_1890, 0, 85, 0}, +{ 6, s_2_1891, 0, 122, 0}, +{ 7, s_2_1892, 0, 86, 0}, +{ 4, s_2_1893, 0, 95, 0}, +{ 5, s_2_1894, -1, 1, 0}, +{ 6, s_2_1895, -2, 2, 0}, +{ 4, s_2_1896, 0, 51, 0}, +{ 5, s_2_1897, -1, 83, 0}, +{ 4, s_2_1898, 0, 13, 0}, +{ 5, s_2_1899, -1, 10, 0}, +{ 5, s_2_1900, -2, 87, 0}, +{ 5, s_2_1901, -3, 159, 0}, +{ 6, s_2_1902, -4, 88, 0}, +{ 5, s_2_1903, 0, 123, 0}, +{ 5, s_2_1904, 0, 120, 0}, +{ 7, s_2_1905, 0, 92, 0}, +{ 7, s_2_1906, 0, 93, 0}, +{ 6, s_2_1907, 0, 94, 0}, +{ 5, s_2_1908, 0, 77, 0}, +{ 5, s_2_1909, 0, 78, 0}, +{ 5, s_2_1910, 0, 79, 0}, +{ 5, s_2_1911, 0, 80, 0}, +{ 6, s_2_1912, 0, 14, 0}, +{ 6, s_2_1913, 0, 15, 0}, +{ 6, s_2_1914, 0, 16, 0}, +{ 6, s_2_1915, 0, 91, 0}, +{ 5, s_2_1916, 0, 124, 0}, +{ 5, s_2_1917, 0, 125, 0}, +{ 5, s_2_1918, 0, 126, 0}, +{ 6, s_2_1919, 0, 84, 0}, +{ 6, s_2_1920, 0, 85, 0}, +{ 6, s_2_1921, 0, 122, 0}, +{ 7, s_2_1922, 0, 86, 0}, +{ 4, s_2_1923, 0, 95, 0}, +{ 5, s_2_1924, -1, 1, 0}, +{ 6, s_2_1925, -2, 2, 0}, +{ 5, s_2_1926, 0, 83, 0}, +{ 4, s_2_1927, 0, 13, 0}, +{ 6, s_2_1928, -1, 137, 0}, +{ 7, s_2_1929, -2, 89, 0}, +{ 5, s_2_1930, 0, 123, 0}, +{ 5, s_2_1931, 0, 120, 0}, +{ 7, s_2_1932, 0, 92, 0}, +{ 7, s_2_1933, 0, 93, 0}, +{ 6, s_2_1934, 0, 94, 0}, +{ 5, s_2_1935, 0, 77, 0}, +{ 5, s_2_1936, 0, 78, 0}, +{ 5, s_2_1937, 0, 79, 0}, +{ 5, s_2_1938, 0, 80, 0}, +{ 6, s_2_1939, 0, 14, 0}, +{ 6, s_2_1940, 0, 15, 0}, +{ 6, s_2_1941, 0, 16, 0}, +{ 6, s_2_1942, 0, 91, 0}, +{ 2, s_2_1943, 0, 13, 0}, +{ 3, s_2_1944, -1, 10, 0}, +{ 6, s_2_1945, -1, 110, 0}, +{ 6, s_2_1946, -2, 111, 0}, +{ 6, s_2_1947, -3, 112, 0}, +{ 3, s_2_1948, -5, 11, 0}, +{ 4, s_2_1949, -1, 137, 0}, +{ 5, s_2_1950, -2, 10, 0}, +{ 5, s_2_1951, -3, 89, 0}, +{ 3, s_2_1952, -9, 12, 0}, +{ 3, s_2_1953, 0, 53, 0}, +{ 3, s_2_1954, 0, 54, 0}, +{ 3, s_2_1955, 0, 55, 0}, +{ 3, s_2_1956, 0, 56, 0}, +{ 4, s_2_1957, 0, 135, 0}, +{ 4, s_2_1958, 0, 131, 0}, +{ 4, s_2_1959, 0, 129, 0}, +{ 4, s_2_1960, 0, 133, 0}, +{ 4, s_2_1961, 0, 132, 0}, +{ 4, s_2_1962, 0, 130, 0}, +{ 4, s_2_1963, 0, 134, 0}, +{ 3, s_2_1964, 0, 57, 0}, +{ 3, s_2_1965, 0, 58, 0}, +{ 3, s_2_1966, 0, 123, 0}, +{ 3, s_2_1967, 0, 120, 0}, +{ 5, s_2_1968, -1, 68, 0}, +{ 4, s_2_1969, -2, 69, 0}, +{ 3, s_2_1970, 0, 70, 0}, +{ 5, s_2_1971, 0, 92, 0}, +{ 5, s_2_1972, 0, 93, 0}, +{ 4, s_2_1973, 0, 94, 0}, +{ 4, s_2_1974, 0, 71, 0}, +{ 4, s_2_1975, 0, 72, 0}, +{ 4, s_2_1976, 0, 73, 0}, +{ 4, s_2_1977, 0, 74, 0}, +{ 5, s_2_1978, 0, 75, 0}, +{ 3, s_2_1979, 0, 77, 0}, +{ 3, s_2_1980, 0, 78, 0}, +{ 3, s_2_1981, 0, 79, 0}, +{ 3, s_2_1982, 0, 80, 0}, +{ 4, s_2_1983, -1, 82, 0}, +{ 4, s_2_1984, -2, 81, 0}, +{ 4, s_2_1985, 0, 3, 0}, +{ 5, s_2_1986, 0, 4, 0}, +{ 5, s_2_1987, 0, 63, 0}, +{ 5, s_2_1988, 0, 64, 0}, +{ 5, s_2_1989, 0, 61, 0}, +{ 5, s_2_1990, 0, 62, 0}, +{ 5, s_2_1991, 0, 60, 0}, +{ 5, s_2_1992, 0, 59, 0}, +{ 5, s_2_1993, 0, 65, 0}, +{ 4, s_2_1994, 0, 66, 0}, +{ 4, s_2_1995, 0, 67, 0}, +{ 4, s_2_1996, 0, 91, 0}, +{ 4, s_2_1997, 0, 97, 0}, +{ 4, s_2_1998, 0, 96, 0}, +{ 4, s_2_1999, 0, 98, 0}, +{ 4, s_2_2000, 0, 99, 0}, +{ 3, s_2_2001, 0, 95, 0}, +{ 3, s_2_2002, 0, 104, 0}, +{ 5, s_2_2003, -1, 100, 0}, +{ 5, s_2_2004, -2, 105, 0}, +{ 4, s_2_2005, -3, 113, 0}, +{ 5, s_2_2006, -4, 97, 0}, +{ 5, s_2_2007, -5, 96, 0}, +{ 5, s_2_2008, -6, 98, 0}, +{ 5, s_2_2009, -7, 99, 0}, +{ 6, s_2_2010, -8, 102, 0}, +{ 3, s_2_2011, 0, 119, 0}, +{ 8, s_2_2012, -1, 110, 0}, +{ 8, s_2_2013, -2, 111, 0}, +{ 8, s_2_2014, -3, 112, 0}, +{ 8, s_2_2015, -4, 106, 0}, +{ 8, s_2_2016, -5, 107, 0}, +{ 8, s_2_2017, -6, 108, 0}, +{ 5, s_2_2018, -7, 116, 0}, +{ 6, s_2_2019, -8, 114, 0}, +{ 5, s_2_2020, -9, 25, 0}, +{ 7, s_2_2021, -1, 100, 0}, +{ 9, s_2_2022, -2, 117, 0}, +{ 4, s_2_2023, -12, 13, 0}, +{ 8, s_2_2024, -1, 110, 0}, +{ 8, s_2_2025, -2, 111, 0}, +{ 8, s_2_2026, -3, 112, 0}, +{ 5, s_2_2027, -16, 70, 0}, +{ 6, s_2_2028, -17, 115, 0}, +{ 3, s_2_2029, 0, 116, 0}, +{ 4, s_2_2030, -1, 103, 0}, +{ 6, s_2_2031, -2, 118, 0}, +{ 6, s_2_2032, -3, 101, 0}, +{ 7, s_2_2033, -4, 117, 0}, +{ 7, s_2_2034, -5, 90, 0} +}; + +static const symbol s_3_0[1] = { 'a' }; +static const symbol s_3_1[3] = { 'o', 'g', 'a' }; +static const symbol s_3_2[3] = { 'a', 'm', 'a' }; +static const symbol s_3_3[3] = { 'i', 'm', 'a' }; +static const symbol s_3_4[3] = { 'e', 'n', 'a' }; +static const symbol s_3_5[1] = { 'e' }; +static const symbol s_3_6[2] = { 'o', 'g' }; +static const symbol s_3_7[4] = { 'a', 'n', 'o', 'g' }; +static const symbol s_3_8[4] = { 'e', 'n', 'o', 'g' }; +static const symbol s_3_9[4] = { 'a', 'n', 'i', 'h' }; +static const symbol s_3_10[4] = { 'e', 'n', 'i', 'h' }; +static const symbol s_3_11[1] = { 'i' }; +static const symbol s_3_12[3] = { 'a', 'n', 'i' }; +static const symbol s_3_13[3] = { 'e', 'n', 'i' }; +static const symbol s_3_14[4] = { 'a', 'n', 'o', 'j' }; +static const symbol s_3_15[4] = { 'e', 'n', 'o', 'j' }; +static const symbol s_3_16[4] = { 'a', 'n', 'i', 'm' }; +static const symbol s_3_17[4] = { 'e', 'n', 'i', 'm' }; +static const symbol s_3_18[2] = { 'o', 'm' }; +static const symbol s_3_19[4] = { 'e', 'n', 'o', 'm' }; +static const symbol s_3_20[1] = { 'o' }; +static const symbol s_3_21[3] = { 'a', 'n', 'o' }; +static const symbol s_3_22[3] = { 'e', 'n', 'o' }; +static const symbol s_3_23[3] = { 'o', 's', 't' }; +static const symbol s_3_24[1] = { 'u' }; +static const symbol s_3_25[3] = { 'e', 'n', 'u' }; +static const struct among a_3[26] = { +{ 1, s_3_0, 0, 1, 0}, +{ 3, s_3_1, -1, 1, 0}, +{ 3, s_3_2, -2, 1, 0}, +{ 3, s_3_3, -3, 1, 0}, +{ 3, s_3_4, -4, 1, 0}, +{ 1, s_3_5, 0, 1, 0}, +{ 2, s_3_6, 0, 1, 0}, +{ 4, s_3_7, -1, 1, 0}, +{ 4, s_3_8, -2, 1, 0}, +{ 4, s_3_9, 0, 1, 0}, +{ 4, s_3_10, 0, 1, 0}, +{ 1, s_3_11, 0, 1, 0}, +{ 3, s_3_12, -1, 1, 0}, +{ 3, s_3_13, -2, 1, 0}, +{ 4, s_3_14, 0, 1, 0}, +{ 4, s_3_15, 0, 1, 0}, +{ 4, s_3_16, 0, 1, 0}, +{ 4, s_3_17, 0, 1, 0}, +{ 2, s_3_18, 0, 1, 0}, +{ 4, s_3_19, -1, 1, 0}, +{ 1, s_3_20, 0, 1, 0}, +{ 3, s_3_21, -1, 1, 0}, +{ 3, s_3_22, -2, 1, 0}, +{ 3, s_3_23, 0, 1, 0}, +{ 1, s_3_24, 0, 1, 0}, +{ 3, s_3_25, -1, 1, 0} +}; + +static const unsigned char g_v[] = { 17, 65, 16 }; + +static const unsigned char g_sa[] = { 65, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 128 }; + +static const unsigned char g_ca[] = { 119, 95, 23, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 16 }; + +static const unsigned char g_rg[] = { 1 }; + static int r_cyr_to_lat(struct SN_env * z) { int among_var; - { int c1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 30); + among_var = find_among(z, a_0, 30, 0); if (!among_var) goto lab2; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 1, s_6); + { + int ret = slice_from_s(z, 1, s_6); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 1, s_9); + { + int ret = slice_from_s(z, 1, s_9); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 1, s_10); + { + int ret = slice_from_s(z, 1, s_10); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 1, s_11); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 2, s_13); + { + int ret = slice_from_s(z, 2, s_13); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 1, s_14); + { + int ret = slice_from_s(z, 1, s_14); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 1, s_15); + { + int ret = slice_from_s(z, 1, s_15); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 2, s_16); + { + int ret = slice_from_s(z, 2, s_16); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 1, s_17); + { + int ret = slice_from_s(z, 1, s_17); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 1, s_18); + { + int ret = slice_from_s(z, 1, s_18); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 1, s_19); + { + int ret = slice_from_s(z, 1, s_19); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 1, s_20); + { + int ret = slice_from_s(z, 1, s_20); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 1, s_21); + { + int ret = slice_from_s(z, 1, s_21); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 2, s_22); + { + int ret = slice_from_s(z, 2, s_22); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 1, s_23); + { + int ret = slice_from_s(z, 1, s_23); if (ret < 0) return ret; } break; case 25: - { int ret = slice_from_s(z, 1, s_24); + { + int ret = slice_from_s(z, 1, s_24); if (ret < 0) return ret; } break; case 26: - { int ret = slice_from_s(z, 1, s_25); + { + int ret = slice_from_s(z, 1, s_25); if (ret < 0) return ret; } break; case 27: - { int ret = slice_from_s(z, 1, s_26); + { + int ret = slice_from_s(z, 1, s_26); if (ret < 0) return ret; } break; case 28: - { int ret = slice_from_s(z, 2, s_27); + { + int ret = slice_from_s(z, 2, s_27); if (ret < 0) return ret; } break; case 29: - { int ret = slice_from_s(z, 3, s_28); + { + int ret = slice_from_s(z, 3, s_28); if (ret < 0) return ret; } break; case 30: - { int ret = slice_from_s(z, 2, s_29); + { + int ret = slice_from_s(z, 2, s_29); if (ret < 0) return ret; } break; } - z->c = c3; + z->c = v_3; break; lab2: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } return 1; } static int r_prelude(struct SN_env * z) { - { int c1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; if (in_grouping_U(z, g_ca, 98, 382, 0)) goto lab2; z->bra = z->c; if (!(eq_s(z, 3, s_30))) goto lab2; z->ket = z->c; if (in_grouping_U(z, g_ca, 98, 382, 0)) goto lab2; - { int ret = slice_from_s(z, 1, s_31); + { + int ret = slice_from_s(z, 1, s_31); if (ret < 0) return ret; } - z->c = c3; + z->c = v_3; break; lab2: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } - { int c4 = z->c; - while(1) { - int c5 = z->c; - while(1) { - int c6 = z->c; + { + int v_4 = z->c; + while (1) { + int v_5 = z->c; + while (1) { + int v_6 = z->c; if (in_grouping_U(z, g_ca, 98, 382, 0)) goto lab5; z->bra = z->c; if (!(eq_s(z, 2, s_32))) goto lab5; z->ket = z->c; if (in_grouping_U(z, g_ca, 98, 382, 0)) goto lab5; - { int ret = slice_from_s(z, 1, s_33); + { + int ret = slice_from_s(z, 1, s_33); if (ret < 0) return ret; } - z->c = c6; + z->c = v_6; break; lab5: - z->c = c6; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_6; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab4; z->c = ret; } } continue; lab4: - z->c = c5; + z->c = v_5; break; } - z->c = c4; + z->c = v_4; } - { int c7 = z->c; - while(1) { - int c8 = z->c; - while(1) { - int c9 = z->c; + { + int v_7 = z->c; + while (1) { + int v_8 = z->c; + while (1) { + int v_9 = z->c; z->bra = z->c; if (!(eq_s(z, 2, s_34))) goto lab8; z->ket = z->c; - { int ret = slice_from_s(z, 2, s_35); + { + int ret = slice_from_s(z, 2, s_35); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; break; lab8: - z->c = c9; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_9; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab7; z->c = ret; } } continue; lab7: - z->c = c8; + z->c = v_8; break; } - z->c = c7; + z->c = v_7; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[1] = 1; - { int c1 = z->c; - + ((SN_local *)z)->b_no_diacritics = 1; + { + int v_1 = z->c; { int ret = out_grouping_U(z, g_sa, 263, 382, 1); if (ret < 0) goto lab0; z->c += ret; } - z->I[1] = 0; + ((SN_local *)z)->b_no_diacritics = 0; lab0: - z->c = c1; + z->c = v_1; } - z->I[0] = z->l; - { int c2 = z->c; - + ((SN_local *)z)->i_p1 = z->l; + { + int v_2 = z->c; { int ret = out_grouping_U(z, g_v, 97, 117, 1); if (ret < 0) goto lab1; z->c += ret; } - z->I[0] = z->c; - if (z->I[0] >= 2) goto lab1; - + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= 2) goto lab1; { int ret = in_grouping_U(z, g_v, 97, 117, 1); if (ret < 0) goto lab1; z->c += ret; } - z->I[0] = z->c; + ((SN_local *)z)->i_p1 = z->c; lab1: - z->c = c2; + z->c = v_2; } - { int c3 = z->c; - while(1) { + { + int v_3 = z->c; + while (1) { if (z->c == z->l || z->p[z->c] != 'r') goto lab3; z->c++; break; lab3: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab2; z->c = ret; } } - { int c4 = z->c; - if (z->c < 2) goto lab5; - goto lab4; - lab5: - z->c = c4; - + do { + int v_4 = z->c; + if (z->c < 2) goto lab4; + break; + lab4: + z->c = v_4; { int ret = in_grouping_U(z, g_rg, 114, 114, 1); if (ret < 0) goto lab2; z->c += ret; } - } - lab4: - if ((z->I[0] - z->c) <= 1) goto lab2; - z->I[0] = z->c; + } while (0); + if ((((SN_local *)z)->i_p1 - z->c) <= 1) goto lab2; + ((SN_local *)z)->i_p1 = z->c; lab2: - z->c = c3; + z->c = v_3; } return 1; } static int r_R1(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_Step_1(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((3435050 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_1, 130); + among_var = find_among_b(z, a_1, 130, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_36); + { + int ret = slice_from_s(z, 4, s_36); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_37); + { + int ret = slice_from_s(z, 3, s_37); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 5, s_38); + { + int ret = slice_from_s(z, 5, s_38); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 5, s_39); + { + int ret = slice_from_s(z, 5, s_39); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_40); + { + int ret = slice_from_s(z, 3, s_40); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_41); + { + int ret = slice_from_s(z, 6, s_41); if (ret < 0) return ret; } break; case 7: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 5, s_42); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 5, s_42); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 4, s_43); + { + int ret = slice_from_s(z, 4, s_43); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 5, s_44); + { + int ret = slice_from_s(z, 5, s_44); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 4, s_45); + { + int ret = slice_from_s(z, 4, s_45); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 5, s_46); + { + int ret = slice_from_s(z, 5, s_46); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 4, s_47); + { + int ret = slice_from_s(z, 4, s_47); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 4, s_48); + { + int ret = slice_from_s(z, 4, s_48); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 4, s_49); + { + int ret = slice_from_s(z, 4, s_49); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 4, s_50); + { + int ret = slice_from_s(z, 4, s_50); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 4, s_51); + { + int ret = slice_from_s(z, 4, s_51); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 4, s_52); + { + int ret = slice_from_s(z, 4, s_52); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 4, s_53); + { + int ret = slice_from_s(z, 4, s_53); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 3, s_54); + { + int ret = slice_from_s(z, 3, s_54); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 6, s_55); + { + int ret = slice_from_s(z, 6, s_55); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 6, s_56); + { + int ret = slice_from_s(z, 6, s_56); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 5, s_57); + { + int ret = slice_from_s(z, 5, s_57); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 3, s_58); + { + int ret = slice_from_s(z, 3, s_58); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 3, s_59); + { + int ret = slice_from_s(z, 3, s_59); if (ret < 0) return ret; } break; case 25: - { int ret = slice_from_s(z, 3, s_60); + { + int ret = slice_from_s(z, 3, s_60); if (ret < 0) return ret; } break; case 26: - { int ret = slice_from_s(z, 4, s_61); + { + int ret = slice_from_s(z, 4, s_61); if (ret < 0) return ret; } break; case 27: - { int ret = slice_from_s(z, 4, s_62); + { + int ret = slice_from_s(z, 4, s_62); if (ret < 0) return ret; } break; case 28: - { int ret = slice_from_s(z, 5, s_63); + { + int ret = slice_from_s(z, 5, s_63); if (ret < 0) return ret; } break; case 29: - { int ret = slice_from_s(z, 6, s_64); + { + int ret = slice_from_s(z, 6, s_64); if (ret < 0) return ret; } break; case 30: - { int ret = slice_from_s(z, 6, s_65); + { + int ret = slice_from_s(z, 6, s_65); if (ret < 0) return ret; } break; case 31: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 5, s_66); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 5, s_66); if (ret < 0) return ret; } break; case 32: - { int ret = slice_from_s(z, 5, s_67); + { + int ret = slice_from_s(z, 5, s_67); if (ret < 0) return ret; } break; case 33: - { int ret = slice_from_s(z, 5, s_68); + { + int ret = slice_from_s(z, 5, s_68); if (ret < 0) return ret; } break; case 34: - { int ret = slice_from_s(z, 5, s_69); + { + int ret = slice_from_s(z, 5, s_69); if (ret < 0) return ret; } break; case 35: - { int ret = slice_from_s(z, 6, s_70); + { + int ret = slice_from_s(z, 6, s_70); if (ret < 0) return ret; } break; case 36: - { int ret = slice_from_s(z, 5, s_71); + { + int ret = slice_from_s(z, 5, s_71); if (ret < 0) return ret; } break; case 37: - { int ret = slice_from_s(z, 5, s_72); + { + int ret = slice_from_s(z, 5, s_72); if (ret < 0) return ret; } break; case 38: - { int ret = slice_from_s(z, 5, s_73); + { + int ret = slice_from_s(z, 5, s_73); if (ret < 0) return ret; } break; case 39: - { int ret = slice_from_s(z, 5, s_74); + { + int ret = slice_from_s(z, 5, s_74); if (ret < 0) return ret; } break; case 40: - { int ret = slice_from_s(z, 4, s_75); + { + int ret = slice_from_s(z, 4, s_75); if (ret < 0) return ret; } break; case 41: - { int ret = slice_from_s(z, 4, s_76); + { + int ret = slice_from_s(z, 4, s_76); if (ret < 0) return ret; } break; case 42: - { int ret = slice_from_s(z, 4, s_77); + { + int ret = slice_from_s(z, 4, s_77); if (ret < 0) return ret; } break; case 43: - { int ret = slice_from_s(z, 6, s_78); + { + int ret = slice_from_s(z, 6, s_78); if (ret < 0) return ret; } break; case 44: - { int ret = slice_from_s(z, 6, s_79); + { + int ret = slice_from_s(z, 6, s_79); if (ret < 0) return ret; } break; case 45: - { int ret = slice_from_s(z, 5, s_80); + { + int ret = slice_from_s(z, 5, s_80); if (ret < 0) return ret; } break; case 46: - { int ret = slice_from_s(z, 5, s_81); + { + int ret = slice_from_s(z, 5, s_81); if (ret < 0) return ret; } break; case 47: - { int ret = slice_from_s(z, 4, s_82); + { + int ret = slice_from_s(z, 4, s_82); if (ret < 0) return ret; } break; case 48: - { int ret = slice_from_s(z, 4, s_83); + { + int ret = slice_from_s(z, 4, s_83); if (ret < 0) return ret; } break; case 49: - { int ret = slice_from_s(z, 5, s_84); + { + int ret = slice_from_s(z, 5, s_84); if (ret < 0) return ret; } break; case 50: - { int ret = slice_from_s(z, 6, s_85); + { + int ret = slice_from_s(z, 6, s_85); if (ret < 0) return ret; } break; case 51: - { int ret = slice_from_s(z, 5, s_86); + { + int ret = slice_from_s(z, 5, s_86); if (ret < 0) return ret; } break; case 52: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_87); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_87); if (ret < 0) return ret; } break; case 53: - { int ret = slice_from_s(z, 4, s_88); + { + int ret = slice_from_s(z, 4, s_88); if (ret < 0) return ret; } break; case 54: - { int ret = slice_from_s(z, 5, s_89); + { + int ret = slice_from_s(z, 5, s_89); if (ret < 0) return ret; } break; case 55: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_90); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_90); if (ret < 0) return ret; } break; case 56: - { int ret = slice_from_s(z, 5, s_91); + { + int ret = slice_from_s(z, 5, s_91); if (ret < 0) return ret; } break; case 57: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_92); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_92); if (ret < 0) return ret; } break; case 58: - { int ret = slice_from_s(z, 4, s_93); + { + int ret = slice_from_s(z, 4, s_93); if (ret < 0) return ret; } break; case 59: - { int ret = slice_from_s(z, 4, s_94); + { + int ret = slice_from_s(z, 4, s_94); if (ret < 0) return ret; } break; case 60: - { int ret = slice_from_s(z, 4, s_95); + { + int ret = slice_from_s(z, 4, s_95); if (ret < 0) return ret; } break; case 61: - { int ret = slice_from_s(z, 4, s_96); + { + int ret = slice_from_s(z, 4, s_96); if (ret < 0) return ret; } break; case 62: - { int ret = slice_from_s(z, 4, s_97); + { + int ret = slice_from_s(z, 4, s_97); if (ret < 0) return ret; } break; case 63: - { int ret = slice_from_s(z, 5, s_98); + { + int ret = slice_from_s(z, 5, s_98); if (ret < 0) return ret; } break; case 64: - { int ret = slice_from_s(z, 6, s_99); + { + int ret = slice_from_s(z, 6, s_99); if (ret < 0) return ret; } break; case 65: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 5, s_100); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 5, s_100); if (ret < 0) return ret; } break; case 66: - { int ret = slice_from_s(z, 5, s_101); + { + int ret = slice_from_s(z, 5, s_101); if (ret < 0) return ret; } break; case 67: - { int ret = slice_from_s(z, 4, s_102); + { + int ret = slice_from_s(z, 4, s_102); if (ret < 0) return ret; } break; case 68: - { int ret = slice_from_s(z, 5, s_103); + { + int ret = slice_from_s(z, 5, s_103); if (ret < 0) return ret; } break; case 69: - { int ret = slice_from_s(z, 6, s_104); + { + int ret = slice_from_s(z, 6, s_104); if (ret < 0) return ret; } break; case 70: - { int ret = slice_from_s(z, 5, s_105); + { + int ret = slice_from_s(z, 5, s_105); if (ret < 0) return ret; } break; case 71: - { int ret = slice_from_s(z, 4, s_106); + { + int ret = slice_from_s(z, 4, s_106); if (ret < 0) return ret; } break; case 72: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_107); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_107); if (ret < 0) return ret; } break; case 73: - { int ret = slice_from_s(z, 3, s_108); + { + int ret = slice_from_s(z, 3, s_108); if (ret < 0) return ret; } break; case 74: - { int ret = slice_from_s(z, 4, s_109); + { + int ret = slice_from_s(z, 4, s_109); if (ret < 0) return ret; } break; case 75: - { int ret = slice_from_s(z, 3, s_110); + { + int ret = slice_from_s(z, 3, s_110); if (ret < 0) return ret; } break; case 76: - { int ret = slice_from_s(z, 3, s_111); + { + int ret = slice_from_s(z, 3, s_111); if (ret < 0) return ret; } break; case 77: - { int ret = slice_from_s(z, 6, s_112); + { + int ret = slice_from_s(z, 6, s_112); if (ret < 0) return ret; } break; case 78: - { int ret = slice_from_s(z, 4, s_113); + { + int ret = slice_from_s(z, 4, s_113); if (ret < 0) return ret; } break; case 79: - { int ret = slice_from_s(z, 3, s_114); + { + int ret = slice_from_s(z, 3, s_114); if (ret < 0) return ret; } break; case 80: - { int ret = slice_from_s(z, 3, s_115); + { + int ret = slice_from_s(z, 3, s_115); if (ret < 0) return ret; } break; case 81: - { int ret = slice_from_s(z, 3, s_116); + { + int ret = slice_from_s(z, 3, s_116); if (ret < 0) return ret; } break; case 82: - { int ret = slice_from_s(z, 4, s_117); + { + int ret = slice_from_s(z, 4, s_117); if (ret < 0) return ret; } break; case 83: - { int ret = slice_from_s(z, 4, s_118); + { + int ret = slice_from_s(z, 4, s_118); if (ret < 0) return ret; } break; case 84: - { int ret = slice_from_s(z, 4, s_119); + { + int ret = slice_from_s(z, 4, s_119); if (ret < 0) return ret; } break; case 85: - { int ret = slice_from_s(z, 4, s_120); + { + int ret = slice_from_s(z, 4, s_120); if (ret < 0) return ret; } break; case 86: - { int ret = slice_from_s(z, 4, s_121); + { + int ret = slice_from_s(z, 4, s_121); if (ret < 0) return ret; } break; case 87: - { int ret = slice_from_s(z, 4, s_122); + { + int ret = slice_from_s(z, 4, s_122); if (ret < 0) return ret; } break; case 88: - { int ret = slice_from_s(z, 4, s_123); + { + int ret = slice_from_s(z, 4, s_123); if (ret < 0) return ret; } break; case 89: - { int ret = slice_from_s(z, 4, s_124); + { + int ret = slice_from_s(z, 4, s_124); if (ret < 0) return ret; } break; case 90: - { int ret = slice_from_s(z, 5, s_125); + { + int ret = slice_from_s(z, 5, s_125); if (ret < 0) return ret; } break; case 91: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_126); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_126); if (ret < 0) return ret; } break; @@ -5608,874 +5734,1039 @@ static int r_Step_1(struct SN_env * z) { static int r_Step_2(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_2, 2035); + among_var = find_among_b(z, a_2, 2035, 0); if (!among_var) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } switch (among_var) { case 1: - { int ret = slice_from_s(z, 2, s_127); + { + int ret = slice_from_s(z, 2, s_127); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_128); + { + int ret = slice_from_s(z, 3, s_128); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_129); + { + int ret = slice_from_s(z, 3, s_129); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 4, s_130); + { + int ret = slice_from_s(z, 4, s_130); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 5, s_131); + { + int ret = slice_from_s(z, 5, s_131); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 5, s_132); + { + int ret = slice_from_s(z, 5, s_132); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 5, s_133); + { + int ret = slice_from_s(z, 5, s_133); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 5, s_134); + { + int ret = slice_from_s(z, 5, s_134); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 5, s_135); + { + int ret = slice_from_s(z, 5, s_135); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 2, s_136); + { + int ret = slice_from_s(z, 2, s_136); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 2, s_137); + { + int ret = slice_from_s(z, 2, s_137); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 2, s_138); + { + int ret = slice_from_s(z, 2, s_138); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 1, s_139); + { + int ret = slice_from_s(z, 1, s_139); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 3, s_140); + { + int ret = slice_from_s(z, 3, s_140); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 3, s_141); + { + int ret = slice_from_s(z, 3, s_141); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 3, s_142); + { + int ret = slice_from_s(z, 3, s_142); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 4, s_143); + { + int ret = slice_from_s(z, 4, s_143); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 2, s_144); + { + int ret = slice_from_s(z, 2, s_144); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 3, s_145); + { + int ret = slice_from_s(z, 3, s_145); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 1, s_146); + { + int ret = slice_from_s(z, 1, s_146); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 4, s_147); + { + int ret = slice_from_s(z, 4, s_147); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 3, s_148); + { + int ret = slice_from_s(z, 3, s_148); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 2, s_149); + { + int ret = slice_from_s(z, 2, s_149); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 2, s_150); + { + int ret = slice_from_s(z, 2, s_150); if (ret < 0) return ret; } break; case 25: - { int ret = slice_from_s(z, 2, s_151); + { + int ret = slice_from_s(z, 2, s_151); if (ret < 0) return ret; } break; case 26: - { int ret = slice_from_s(z, 3, s_152); + { + int ret = slice_from_s(z, 3, s_152); if (ret < 0) return ret; } break; case 27: - { int ret = slice_from_s(z, 4, s_153); + { + int ret = slice_from_s(z, 4, s_153); if (ret < 0) return ret; } break; case 28: - { int ret = slice_from_s(z, 4, s_154); + { + int ret = slice_from_s(z, 4, s_154); if (ret < 0) return ret; } break; case 29: - { int ret = slice_from_s(z, 4, s_155); + { + int ret = slice_from_s(z, 4, s_155); if (ret < 0) return ret; } break; case 30: - { int ret = slice_from_s(z, 3, s_156); + { + int ret = slice_from_s(z, 3, s_156); if (ret < 0) return ret; } break; case 31: - { int ret = slice_from_s(z, 3, s_157); + { + int ret = slice_from_s(z, 3, s_157); if (ret < 0) return ret; } break; case 32: - { int ret = slice_from_s(z, 3, s_158); + { + int ret = slice_from_s(z, 3, s_158); if (ret < 0) return ret; } break; case 33: - { int ret = slice_from_s(z, 3, s_159); + { + int ret = slice_from_s(z, 3, s_159); if (ret < 0) return ret; } break; case 34: - { int ret = slice_from_s(z, 3, s_160); + { + int ret = slice_from_s(z, 3, s_160); if (ret < 0) return ret; } break; case 35: - { int ret = slice_from_s(z, 3, s_161); + { + int ret = slice_from_s(z, 3, s_161); if (ret < 0) return ret; } break; case 36: - { int ret = slice_from_s(z, 3, s_162); + { + int ret = slice_from_s(z, 3, s_162); if (ret < 0) return ret; } break; case 37: - { int ret = slice_from_s(z, 3, s_163); + { + int ret = slice_from_s(z, 3, s_163); if (ret < 0) return ret; } break; case 38: - { int ret = slice_from_s(z, 4, s_164); + { + int ret = slice_from_s(z, 4, s_164); if (ret < 0) return ret; } break; case 39: - { int ret = slice_from_s(z, 3, s_165); + { + int ret = slice_from_s(z, 3, s_165); if (ret < 0) return ret; } break; case 40: - { int ret = slice_from_s(z, 3, s_166); + { + int ret = slice_from_s(z, 3, s_166); if (ret < 0) return ret; } break; case 41: - { int ret = slice_from_s(z, 3, s_167); + { + int ret = slice_from_s(z, 3, s_167); if (ret < 0) return ret; } break; case 42: - { int ret = slice_from_s(z, 3, s_168); + { + int ret = slice_from_s(z, 3, s_168); if (ret < 0) return ret; } break; case 43: - { int ret = slice_from_s(z, 3, s_169); + { + int ret = slice_from_s(z, 3, s_169); if (ret < 0) return ret; } break; case 44: - { int ret = slice_from_s(z, 3, s_170); + { + int ret = slice_from_s(z, 3, s_170); if (ret < 0) return ret; } break; case 45: - { int ret = slice_from_s(z, 3, s_171); + { + int ret = slice_from_s(z, 3, s_171); if (ret < 0) return ret; } break; case 46: - { int ret = slice_from_s(z, 3, s_172); + { + int ret = slice_from_s(z, 3, s_172); if (ret < 0) return ret; } break; case 47: - { int ret = slice_from_s(z, 4, s_173); + { + int ret = slice_from_s(z, 4, s_173); if (ret < 0) return ret; } break; case 48: - { int ret = slice_from_s(z, 4, s_174); + { + int ret = slice_from_s(z, 4, s_174); if (ret < 0) return ret; } break; case 49: - { int ret = slice_from_s(z, 4, s_175); + { + int ret = slice_from_s(z, 4, s_175); if (ret < 0) return ret; } break; case 50: - { int ret = slice_from_s(z, 2, s_176); + { + int ret = slice_from_s(z, 2, s_176); if (ret < 0) return ret; } break; case 51: - { int ret = slice_from_s(z, 3, s_177); + { + int ret = slice_from_s(z, 3, s_177); if (ret < 0) return ret; } break; case 52: - { int ret = slice_from_s(z, 3, s_178); + { + int ret = slice_from_s(z, 3, s_178); if (ret < 0) return ret; } break; case 53: - { int ret = slice_from_s(z, 2, s_179); + { + int ret = slice_from_s(z, 2, s_179); if (ret < 0) return ret; } break; case 54: - { int ret = slice_from_s(z, 2, s_180); + { + int ret = slice_from_s(z, 2, s_180); if (ret < 0) return ret; } break; case 55: - { int ret = slice_from_s(z, 2, s_181); + { + int ret = slice_from_s(z, 2, s_181); if (ret < 0) return ret; } break; case 56: - { int ret = slice_from_s(z, 2, s_182); + { + int ret = slice_from_s(z, 2, s_182); if (ret < 0) return ret; } break; case 57: - { int ret = slice_from_s(z, 2, s_183); + { + int ret = slice_from_s(z, 2, s_183); if (ret < 0) return ret; } break; case 58: - { int ret = slice_from_s(z, 2, s_184); + { + int ret = slice_from_s(z, 2, s_184); if (ret < 0) return ret; } break; case 59: - { int ret = slice_from_s(z, 4, s_185); + { + int ret = slice_from_s(z, 4, s_185); if (ret < 0) return ret; } break; case 60: - { int ret = slice_from_s(z, 4, s_186); + { + int ret = slice_from_s(z, 4, s_186); if (ret < 0) return ret; } break; case 61: - { int ret = slice_from_s(z, 4, s_187); + { + int ret = slice_from_s(z, 4, s_187); if (ret < 0) return ret; } break; case 62: - { int ret = slice_from_s(z, 4, s_188); + { + int ret = slice_from_s(z, 4, s_188); if (ret < 0) return ret; } break; case 63: - { int ret = slice_from_s(z, 4, s_189); + { + int ret = slice_from_s(z, 4, s_189); if (ret < 0) return ret; } break; case 64: - { int ret = slice_from_s(z, 4, s_190); + { + int ret = slice_from_s(z, 4, s_190); if (ret < 0) return ret; } break; case 65: - { int ret = slice_from_s(z, 4, s_191); + { + int ret = slice_from_s(z, 4, s_191); if (ret < 0) return ret; } break; case 66: - { int ret = slice_from_s(z, 3, s_192); + { + int ret = slice_from_s(z, 3, s_192); if (ret < 0) return ret; } break; case 67: - { int ret = slice_from_s(z, 3, s_193); + { + int ret = slice_from_s(z, 3, s_193); if (ret < 0) return ret; } break; case 68: - { int ret = slice_from_s(z, 4, s_194); + { + int ret = slice_from_s(z, 4, s_194); if (ret < 0) return ret; } break; case 69: - { int ret = slice_from_s(z, 3, s_195); + { + int ret = slice_from_s(z, 3, s_195); if (ret < 0) return ret; } break; case 70: - { int ret = slice_from_s(z, 2, s_196); + { + int ret = slice_from_s(z, 2, s_196); if (ret < 0) return ret; } break; case 71: - { int ret = slice_from_s(z, 3, s_197); + { + int ret = slice_from_s(z, 3, s_197); if (ret < 0) return ret; } break; case 72: - { int ret = slice_from_s(z, 3, s_198); + { + int ret = slice_from_s(z, 3, s_198); if (ret < 0) return ret; } break; case 73: - { int ret = slice_from_s(z, 3, s_199); + { + int ret = slice_from_s(z, 3, s_199); if (ret < 0) return ret; } break; case 74: - { int ret = slice_from_s(z, 3, s_200); + { + int ret = slice_from_s(z, 3, s_200); if (ret < 0) return ret; } break; case 75: - { int ret = slice_from_s(z, 4, s_201); + { + int ret = slice_from_s(z, 4, s_201); if (ret < 0) return ret; } break; case 76: - { int ret = slice_from_s(z, 3, s_202); + { + int ret = slice_from_s(z, 3, s_202); if (ret < 0) return ret; } break; case 77: - { int ret = slice_from_s(z, 2, s_203); + { + int ret = slice_from_s(z, 2, s_203); if (ret < 0) return ret; } break; case 78: - { int ret = slice_from_s(z, 2, s_204); + { + int ret = slice_from_s(z, 2, s_204); if (ret < 0) return ret; } break; case 79: - { int ret = slice_from_s(z, 2, s_205); + { + int ret = slice_from_s(z, 2, s_205); if (ret < 0) return ret; } break; case 80: - { int ret = slice_from_s(z, 2, s_206); + { + int ret = slice_from_s(z, 2, s_206); if (ret < 0) return ret; } break; case 81: - { int ret = slice_from_s(z, 3, s_207); + { + int ret = slice_from_s(z, 3, s_207); if (ret < 0) return ret; } break; case 82: - { int ret = slice_from_s(z, 3, s_208); + { + int ret = slice_from_s(z, 3, s_208); if (ret < 0) return ret; } break; case 83: - { int ret = slice_from_s(z, 2, s_209); + { + int ret = slice_from_s(z, 2, s_209); if (ret < 0) return ret; } break; case 84: - { int ret = slice_from_s(z, 3, s_210); + { + int ret = slice_from_s(z, 3, s_210); if (ret < 0) return ret; } break; case 85: - { int ret = slice_from_s(z, 3, s_211); + { + int ret = slice_from_s(z, 3, s_211); if (ret < 0) return ret; } break; case 86: - { int ret = slice_from_s(z, 4, s_212); + { + int ret = slice_from_s(z, 4, s_212); if (ret < 0) return ret; } break; case 87: - { int ret = slice_from_s(z, 2, s_213); + { + int ret = slice_from_s(z, 2, s_213); if (ret < 0) return ret; } break; case 88: - { int ret = slice_from_s(z, 3, s_214); + { + int ret = slice_from_s(z, 3, s_214); if (ret < 0) return ret; } break; case 89: - { int ret = slice_from_s(z, 4, s_215); + { + int ret = slice_from_s(z, 4, s_215); if (ret < 0) return ret; } break; case 90: - { int ret = slice_from_s(z, 5, s_216); + { + int ret = slice_from_s(z, 5, s_216); if (ret < 0) return ret; } break; case 91: - { int ret = slice_from_s(z, 3, s_217); + { + int ret = slice_from_s(z, 3, s_217); if (ret < 0) return ret; } break; case 92: - { int ret = slice_from_s(z, 4, s_218); + { + int ret = slice_from_s(z, 4, s_218); if (ret < 0) return ret; } break; case 93: - { int ret = slice_from_s(z, 4, s_219); + { + int ret = slice_from_s(z, 4, s_219); if (ret < 0) return ret; } break; case 94: - { int ret = slice_from_s(z, 3, s_220); + { + int ret = slice_from_s(z, 3, s_220); if (ret < 0) return ret; } break; case 95: - { int ret = slice_from_s(z, 1, s_221); + { + int ret = slice_from_s(z, 1, s_221); if (ret < 0) return ret; } break; case 96: - { int ret = slice_from_s(z, 3, s_222); + { + int ret = slice_from_s(z, 3, s_222); if (ret < 0) return ret; } break; case 97: - { int ret = slice_from_s(z, 3, s_223); + { + int ret = slice_from_s(z, 3, s_223); if (ret < 0) return ret; } break; case 98: - { int ret = slice_from_s(z, 3, s_224); + { + int ret = slice_from_s(z, 3, s_224); if (ret < 0) return ret; } break; case 99: - { int ret = slice_from_s(z, 3, s_225); + { + int ret = slice_from_s(z, 3, s_225); if (ret < 0) return ret; } break; case 100: - { int ret = slice_from_s(z, 2, s_226); + { + int ret = slice_from_s(z, 2, s_226); if (ret < 0) return ret; } break; case 101: - { int ret = slice_from_s(z, 3, s_227); + { + int ret = slice_from_s(z, 3, s_227); if (ret < 0) return ret; } break; case 102: - { int ret = slice_from_s(z, 4, s_228); + { + int ret = slice_from_s(z, 4, s_228); if (ret < 0) return ret; } break; case 103: - { int ret = slice_from_s(z, 2, s_229); + { + int ret = slice_from_s(z, 2, s_229); if (ret < 0) return ret; } break; case 104: - { int ret = slice_from_s(z, 1, s_230); + { + int ret = slice_from_s(z, 1, s_230); if (ret < 0) return ret; } break; case 105: - { int ret = slice_from_s(z, 2, s_231); + { + int ret = slice_from_s(z, 2, s_231); if (ret < 0) return ret; } break; case 106: - { int ret = slice_from_s(z, 5, s_232); + { + int ret = slice_from_s(z, 5, s_232); if (ret < 0) return ret; } break; case 107: - { int ret = slice_from_s(z, 5, s_233); + { + int ret = slice_from_s(z, 5, s_233); if (ret < 0) return ret; } break; case 108: - { int ret = slice_from_s(z, 5, s_234); + { + int ret = slice_from_s(z, 5, s_234); if (ret < 0) return ret; } break; case 109: - { int ret = slice_from_s(z, 2, s_235); + { + int ret = slice_from_s(z, 2, s_235); if (ret < 0) return ret; } break; case 110: - { int ret = slice_from_s(z, 4, s_236); + { + int ret = slice_from_s(z, 4, s_236); if (ret < 0) return ret; } break; case 111: - { int ret = slice_from_s(z, 4, s_237); + { + int ret = slice_from_s(z, 4, s_237); if (ret < 0) return ret; } break; case 112: - { int ret = slice_from_s(z, 4, s_238); + { + int ret = slice_from_s(z, 4, s_238); if (ret < 0) return ret; } break; case 113: - { int ret = slice_from_s(z, 2, s_239); + { + int ret = slice_from_s(z, 2, s_239); if (ret < 0) return ret; } break; case 114: - { int ret = slice_from_s(z, 3, s_240); + { + int ret = slice_from_s(z, 3, s_240); if (ret < 0) return ret; } break; case 115: - { int ret = slice_from_s(z, 2, s_241); + { + int ret = slice_from_s(z, 2, s_241); if (ret < 0) return ret; } break; case 116: - { int ret = slice_from_s(z, 1, s_242); + { + int ret = slice_from_s(z, 1, s_242); if (ret < 0) return ret; } break; case 117: - { int ret = slice_from_s(z, 4, s_243); + { + int ret = slice_from_s(z, 4, s_243); if (ret < 0) return ret; } break; case 118: - { int ret = slice_from_s(z, 4, s_244); + { + int ret = slice_from_s(z, 4, s_244); if (ret < 0) return ret; } break; case 119: - { int ret = slice_from_s(z, 1, s_245); + { + int ret = slice_from_s(z, 1, s_245); if (ret < 0) return ret; } break; case 120: - { int ret = slice_from_s(z, 2, s_246); + { + int ret = slice_from_s(z, 2, s_246); if (ret < 0) return ret; } break; case 121: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_247); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_247); if (ret < 0) return ret; } break; case 122: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_248); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_248); if (ret < 0) return ret; } break; case 123: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_249); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_249); if (ret < 0) return ret; } break; case 124: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_250); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_250); if (ret < 0) return ret; } break; case 125: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_251); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_251); if (ret < 0) return ret; } break; case 126: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_252); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_252); if (ret < 0) return ret; } break; case 127: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_253); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_253); if (ret < 0) return ret; } break; case 128: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_254); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_254); if (ret < 0) return ret; } break; case 129: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_255); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_255); if (ret < 0) return ret; } break; case 130: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_256); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_256); if (ret < 0) return ret; } break; case 131: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_257); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_257); if (ret < 0) return ret; } break; case 132: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_258); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_258); if (ret < 0) return ret; } break; case 133: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_259); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_259); if (ret < 0) return ret; } break; case 134: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_260); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_260); if (ret < 0) return ret; } break; case 135: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_261); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_261); if (ret < 0) return ret; } break; case 136: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_262); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_262); if (ret < 0) return ret; } break; case 137: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_263); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_263); if (ret < 0) return ret; } break; case 138: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 5, s_264); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 5, s_264); if (ret < 0) return ret; } break; case 139: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 5, s_265); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 5, s_265); if (ret < 0) return ret; } break; case 140: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 5, s_266); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 5, s_266); if (ret < 0) return ret; } break; case 141: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_267); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_267); if (ret < 0) return ret; } break; case 142: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_268); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_268); if (ret < 0) return ret; } break; case 143: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_269); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_269); if (ret < 0) return ret; } break; case 144: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_270); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_270); if (ret < 0) return ret; } break; case 145: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_271); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_271); if (ret < 0) return ret; } break; case 146: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_272); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_272); if (ret < 0) return ret; } break; case 147: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_273); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_273); if (ret < 0) return ret; } break; case 148: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_274); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_274); if (ret < 0) return ret; } break; case 149: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 4, s_275); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 4, s_275); if (ret < 0) return ret; } break; case 150: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_276); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_276); if (ret < 0) return ret; } break; case 151: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 3, s_277); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 3, s_277); if (ret < 0) return ret; } break; case 152: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_278); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_278); if (ret < 0) return ret; } break; case 153: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_279); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_279); if (ret < 0) return ret; } break; case 154: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_280); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_280); if (ret < 0) return ret; } break; case 155: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_281); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_281); if (ret < 0) return ret; } break; case 156: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_282); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_282); if (ret < 0) return ret; } break; case 157: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_283); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_283); if (ret < 0) return ret; } break; case 158: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_284); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_284); if (ret < 0) return ret; } break; case 159: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_285); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_285); if (ret < 0) return ret; } break; case 160: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 2, s_286); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 2, s_286); if (ret < 0) return ret; } break; case 161: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 1, s_287); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 1, s_287); if (ret < 0) return ret; } break; case 162: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 1, s_288); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 1, s_288); if (ret < 0) return ret; } break; case 163: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 1, s_289); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 1, s_289); if (ret < 0) return ret; } break; case 164: - if (!(z->I[1])) return 0; - { int ret = slice_from_s(z, 1, s_290); + if (!((SN_local *)z)->b_no_diacritics) return 0; + { + int ret = slice_from_s(z, 1, s_290); if (ret < 0) return ret; } break; @@ -6486,61 +6777,76 @@ static int r_Step_2(struct SN_env * z) { static int r_Step_3(struct SN_env * z) { z->ket = z->c; if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((3188642 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_3, 26)) return 0; + if (!find_among_b(z, a_3, 26, 0)) return 0; z->bra = z->c; - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 0, 0); + { + int ret = slice_from_s(z, 0, 0); if (ret < 0) return ret; } return 1; } extern int serbian_UTF_8_stem(struct SN_env * z) { - - { int ret = r_cyr_to_lat(z); + { + int ret = r_cyr_to_lat(z); if (ret < 0) return ret; } - - { int ret = r_prelude(z); + { + int ret = r_prelude(z); if (ret < 0) return ret; } - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_Step_1(z); + { + int v_1 = z->l - z->c; + { + int ret = r_Step_1(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_Step_2(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int ret = r_Step_2(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_Step_3(z); + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_Step_3(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } z->c = z->lb; return 1; } -extern struct SN_env * serbian_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * serbian_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->b_no_diacritics = 0; + } + return z; +} -extern void serbian_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void serbian_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_spanish.c b/src/backend/snowball/libstemmer/stem_UTF_8_spanish.c index 1b2e27bcc7241..8fa7185402718 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_spanish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_spanish.c @@ -1,6 +1,19 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from spanish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_spanish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p2; + int i_p1; + int i_pV; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +22,7 @@ extern int spanish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_residual_suffix(struct SN_env * z); static int r_verb_suffix(struct SN_env * z); static int r_y_verb_suffix(struct SN_env * z); @@ -19,32 +33,36 @@ static int r_R1(struct SN_env * z); static int r_RV(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_postlude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * spanish_UTF_8_create_env(void); -extern void spanish_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 'a' }; +static const symbol s_1[] = { 'e' }; +static const symbol s_2[] = { 'i' }; +static const symbol s_3[] = { 'o' }; +static const symbol s_4[] = { 'u' }; +static const symbol s_5[] = { 'i', 'e', 'n', 'd', 'o' }; +static const symbol s_6[] = { 'a', 'n', 'd', 'o' }; +static const symbol s_7[] = { 'a', 'r' }; +static const symbol s_8[] = { 'e', 'r' }; +static const symbol s_9[] = { 'i', 'r' }; +static const symbol s_10[] = { 'i', 'c' }; +static const symbol s_11[] = { 'l', 'o', 'g' }; +static const symbol s_12[] = { 'u' }; +static const symbol s_13[] = { 'e', 'n', 't', 'e' }; +static const symbol s_14[] = { 'a', 't' }; +static const symbol s_15[] = { 'a', 't' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_1[2] = { 0xC3, 0xA1 }; static const symbol s_0_2[2] = { 0xC3, 0xA9 }; static const symbol s_0_3[2] = { 0xC3, 0xAD }; static const symbol s_0_4[2] = { 0xC3, 0xB3 }; static const symbol s_0_5[2] = { 0xC3, 0xBA }; - -static const struct among a_0[6] = -{ -{ 0, 0, -1, 6, 0}, -{ 2, s_0_1, 0, 1, 0}, -{ 2, s_0_2, 0, 2, 0}, -{ 2, s_0_3, 0, 3, 0}, -{ 2, s_0_4, 0, 4, 0}, -{ 2, s_0_5, 0, 5, 0} +static const struct among a_0[6] = { +{ 0, 0, 0, 6, 0}, +{ 2, s_0_1, -1, 1, 0}, +{ 2, s_0_2, -2, 2, 0}, +{ 2, s_0_3, -3, 3, 0}, +{ 2, s_0_4, -4, 4, 0}, +{ 2, s_0_5, -5, 5, 0} }; static const symbol s_1_0[2] = { 'l', 'a' }; @@ -60,22 +78,20 @@ static const symbol s_1_9[3] = { 'l', 'e', 's' }; static const symbol s_1_10[3] = { 'l', 'o', 's' }; static const symbol s_1_11[5] = { 's', 'e', 'l', 'o', 's' }; static const symbol s_1_12[3] = { 'n', 'o', 's' }; - -static const struct among a_1[13] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 4, s_1_1, 0, -1, 0}, -{ 2, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0}, -{ 2, s_1_4, -1, -1, 0}, -{ 2, s_1_5, -1, -1, 0}, -{ 4, s_1_6, 5, -1, 0}, -{ 3, s_1_7, -1, -1, 0}, -{ 5, s_1_8, 7, -1, 0}, -{ 3, s_1_9, -1, -1, 0}, -{ 3, s_1_10, -1, -1, 0}, -{ 5, s_1_11, 10, -1, 0}, -{ 3, s_1_12, -1, -1, 0} +static const struct among a_1[13] = { +{ 2, s_1_0, 0, -1, 0}, +{ 4, s_1_1, -1, -1, 0}, +{ 2, s_1_2, 0, -1, 0}, +{ 2, s_1_3, 0, -1, 0}, +{ 2, s_1_4, 0, -1, 0}, +{ 2, s_1_5, 0, -1, 0}, +{ 4, s_1_6, -1, -1, 0}, +{ 3, s_1_7, 0, -1, 0}, +{ 5, s_1_8, -1, -1, 0}, +{ 3, s_1_9, 0, -1, 0}, +{ 3, s_1_10, 0, -1, 0}, +{ 5, s_1_11, -1, -1, 0}, +{ 3, s_1_12, 0, -1, 0} }; static const symbol s_2_0[4] = { 'a', 'n', 'd', 'o' }; @@ -89,55 +105,47 @@ static const symbol s_2_7[2] = { 'i', 'r' }; static const symbol s_2_8[3] = { 0xC3, 0xA1, 'r' }; static const symbol s_2_9[3] = { 0xC3, 0xA9, 'r' }; static const symbol s_2_10[3] = { 0xC3, 0xAD, 'r' }; - -static const struct among a_2[11] = -{ -{ 4, s_2_0, -1, 6, 0}, -{ 5, s_2_1, -1, 6, 0}, -{ 5, s_2_2, -1, 7, 0}, -{ 5, s_2_3, -1, 2, 0}, -{ 6, s_2_4, -1, 1, 0}, -{ 2, s_2_5, -1, 6, 0}, -{ 2, s_2_6, -1, 6, 0}, -{ 2, s_2_7, -1, 6, 0}, -{ 3, s_2_8, -1, 3, 0}, -{ 3, s_2_9, -1, 4, 0}, -{ 3, s_2_10, -1, 5, 0} +static const struct among a_2[11] = { +{ 4, s_2_0, 0, 6, 0}, +{ 5, s_2_1, 0, 6, 0}, +{ 5, s_2_2, 0, 7, 0}, +{ 5, s_2_3, 0, 2, 0}, +{ 6, s_2_4, 0, 1, 0}, +{ 2, s_2_5, 0, 6, 0}, +{ 2, s_2_6, 0, 6, 0}, +{ 2, s_2_7, 0, 6, 0}, +{ 3, s_2_8, 0, 3, 0}, +{ 3, s_2_9, 0, 4, 0}, +{ 3, s_2_10, 0, 5, 0} }; static const symbol s_3_0[2] = { 'i', 'c' }; static const symbol s_3_1[2] = { 'a', 'd' }; static const symbol s_3_2[2] = { 'o', 's' }; static const symbol s_3_3[2] = { 'i', 'v' }; - -static const struct among a_3[4] = -{ -{ 2, s_3_0, -1, -1, 0}, -{ 2, s_3_1, -1, -1, 0}, -{ 2, s_3_2, -1, -1, 0}, -{ 2, s_3_3, -1, 1, 0} +static const struct among a_3[4] = { +{ 2, s_3_0, 0, -1, 0}, +{ 2, s_3_1, 0, -1, 0}, +{ 2, s_3_2, 0, -1, 0}, +{ 2, s_3_3, 0, 1, 0} }; static const symbol s_4_0[4] = { 'a', 'b', 'l', 'e' }; static const symbol s_4_1[4] = { 'i', 'b', 'l', 'e' }; static const symbol s_4_2[4] = { 'a', 'n', 't', 'e' }; - -static const struct among a_4[3] = -{ -{ 4, s_4_0, -1, 1, 0}, -{ 4, s_4_1, -1, 1, 0}, -{ 4, s_4_2, -1, 1, 0} +static const struct among a_4[3] = { +{ 4, s_4_0, 0, 1, 0}, +{ 4, s_4_1, 0, 1, 0}, +{ 4, s_4_2, 0, 1, 0} }; static const symbol s_5_0[2] = { 'i', 'c' }; static const symbol s_5_1[4] = { 'a', 'b', 'i', 'l' }; static const symbol s_5_2[2] = { 'i', 'v' }; - -static const struct among a_5[3] = -{ -{ 2, s_5_0, -1, 1, 0}, -{ 4, s_5_1, -1, 1, 0}, -{ 2, s_5_2, -1, 1, 0} +static const struct among a_5[3] = { +{ 2, s_5_0, 0, 1, 0}, +{ 4, s_5_1, 0, 1, 0}, +{ 2, s_5_2, 0, 1, 0} }; static const symbol s_6_0[3] = { 'i', 'c', 'a' }; @@ -155,86 +163,88 @@ static const symbol s_6_11[4] = { 'i', 'b', 'l', 'e' }; static const symbol s_6_12[4] = { 'a', 'n', 't', 'e' }; static const symbol s_6_13[5] = { 'm', 'e', 'n', 't', 'e' }; static const symbol s_6_14[6] = { 'a', 'm', 'e', 'n', 't', 'e' }; -static const symbol s_6_15[6] = { 'a', 'c', 'i', 0xC3, 0xB3, 'n' }; -static const symbol s_6_16[6] = { 'u', 'c', 'i', 0xC3, 0xB3, 'n' }; -static const symbol s_6_17[3] = { 'i', 'c', 'o' }; -static const symbol s_6_18[4] = { 'i', 's', 'm', 'o' }; -static const symbol s_6_19[3] = { 'o', 's', 'o' }; -static const symbol s_6_20[7] = { 'a', 'm', 'i', 'e', 'n', 't', 'o' }; -static const symbol s_6_21[7] = { 'i', 'm', 'i', 'e', 'n', 't', 'o' }; -static const symbol s_6_22[3] = { 'i', 'v', 'o' }; -static const symbol s_6_23[4] = { 'a', 'd', 'o', 'r' }; -static const symbol s_6_24[4] = { 'i', 'c', 'a', 's' }; -static const symbol s_6_25[6] = { 'a', 'n', 'c', 'i', 'a', 's' }; -static const symbol s_6_26[6] = { 'e', 'n', 'c', 'i', 'a', 's' }; -static const symbol s_6_27[6] = { 'a', 'd', 'o', 'r', 'a', 's' }; -static const symbol s_6_28[4] = { 'o', 's', 'a', 's' }; -static const symbol s_6_29[5] = { 'i', 's', 't', 'a', 's' }; -static const symbol s_6_30[4] = { 'i', 'v', 'a', 's' }; -static const symbol s_6_31[5] = { 'a', 'n', 'z', 'a', 's' }; -static const symbol s_6_32[7] = { 'l', 'o', 'g', 0xC3, 0xAD, 'a', 's' }; -static const symbol s_6_33[6] = { 'i', 'd', 'a', 'd', 'e', 's' }; -static const symbol s_6_34[5] = { 'a', 'b', 'l', 'e', 's' }; -static const symbol s_6_35[5] = { 'i', 'b', 'l', 'e', 's' }; -static const symbol s_6_36[7] = { 'a', 'c', 'i', 'o', 'n', 'e', 's' }; -static const symbol s_6_37[7] = { 'u', 'c', 'i', 'o', 'n', 'e', 's' }; -static const symbol s_6_38[6] = { 'a', 'd', 'o', 'r', 'e', 's' }; -static const symbol s_6_39[5] = { 'a', 'n', 't', 'e', 's' }; -static const symbol s_6_40[4] = { 'i', 'c', 'o', 's' }; -static const symbol s_6_41[5] = { 'i', 's', 'm', 'o', 's' }; -static const symbol s_6_42[4] = { 'o', 's', 'o', 's' }; -static const symbol s_6_43[8] = { 'a', 'm', 'i', 'e', 'n', 't', 'o', 's' }; -static const symbol s_6_44[8] = { 'i', 'm', 'i', 'e', 'n', 't', 'o', 's' }; -static const symbol s_6_45[4] = { 'i', 'v', 'o', 's' }; - -static const struct among a_6[46] = -{ -{ 3, s_6_0, -1, 1, 0}, -{ 5, s_6_1, -1, 2, 0}, -{ 5, s_6_2, -1, 5, 0}, -{ 5, s_6_3, -1, 2, 0}, -{ 3, s_6_4, -1, 1, 0}, -{ 4, s_6_5, -1, 1, 0}, -{ 3, s_6_6, -1, 9, 0}, -{ 4, s_6_7, -1, 1, 0}, -{ 6, s_6_8, -1, 3, 0}, -{ 4, s_6_9, -1, 8, 0}, -{ 4, s_6_10, -1, 1, 0}, -{ 4, s_6_11, -1, 1, 0}, -{ 4, s_6_12, -1, 2, 0}, -{ 5, s_6_13, -1, 7, 0}, -{ 6, s_6_14, 13, 6, 0}, -{ 6, s_6_15, -1, 2, 0}, -{ 6, s_6_16, -1, 4, 0}, -{ 3, s_6_17, -1, 1, 0}, -{ 4, s_6_18, -1, 1, 0}, -{ 3, s_6_19, -1, 1, 0}, -{ 7, s_6_20, -1, 1, 0}, -{ 7, s_6_21, -1, 1, 0}, -{ 3, s_6_22, -1, 9, 0}, -{ 4, s_6_23, -1, 2, 0}, -{ 4, s_6_24, -1, 1, 0}, -{ 6, s_6_25, -1, 2, 0}, -{ 6, s_6_26, -1, 5, 0}, -{ 6, s_6_27, -1, 2, 0}, -{ 4, s_6_28, -1, 1, 0}, -{ 5, s_6_29, -1, 1, 0}, -{ 4, s_6_30, -1, 9, 0}, -{ 5, s_6_31, -1, 1, 0}, -{ 7, s_6_32, -1, 3, 0}, -{ 6, s_6_33, -1, 8, 0}, -{ 5, s_6_34, -1, 1, 0}, -{ 5, s_6_35, -1, 1, 0}, -{ 7, s_6_36, -1, 2, 0}, -{ 7, s_6_37, -1, 4, 0}, -{ 6, s_6_38, -1, 2, 0}, -{ 5, s_6_39, -1, 2, 0}, -{ 4, s_6_40, -1, 1, 0}, -{ 5, s_6_41, -1, 1, 0}, -{ 4, s_6_42, -1, 1, 0}, -{ 8, s_6_43, -1, 1, 0}, -{ 8, s_6_44, -1, 1, 0}, -{ 4, s_6_45, -1, 9, 0} +static const symbol s_6_15[5] = { 'a', 'c', 'i', 'o', 'n' }; +static const symbol s_6_16[5] = { 'u', 'c', 'i', 'o', 'n' }; +static const symbol s_6_17[6] = { 'a', 'c', 'i', 0xC3, 0xB3, 'n' }; +static const symbol s_6_18[6] = { 'u', 'c', 'i', 0xC3, 0xB3, 'n' }; +static const symbol s_6_19[3] = { 'i', 'c', 'o' }; +static const symbol s_6_20[4] = { 'i', 's', 'm', 'o' }; +static const symbol s_6_21[3] = { 'o', 's', 'o' }; +static const symbol s_6_22[7] = { 'a', 'm', 'i', 'e', 'n', 't', 'o' }; +static const symbol s_6_23[7] = { 'i', 'm', 'i', 'e', 'n', 't', 'o' }; +static const symbol s_6_24[3] = { 'i', 'v', 'o' }; +static const symbol s_6_25[4] = { 'a', 'd', 'o', 'r' }; +static const symbol s_6_26[4] = { 'i', 'c', 'a', 's' }; +static const symbol s_6_27[6] = { 'a', 'n', 'c', 'i', 'a', 's' }; +static const symbol s_6_28[6] = { 'e', 'n', 'c', 'i', 'a', 's' }; +static const symbol s_6_29[6] = { 'a', 'd', 'o', 'r', 'a', 's' }; +static const symbol s_6_30[4] = { 'o', 's', 'a', 's' }; +static const symbol s_6_31[5] = { 'i', 's', 't', 'a', 's' }; +static const symbol s_6_32[4] = { 'i', 'v', 'a', 's' }; +static const symbol s_6_33[5] = { 'a', 'n', 'z', 'a', 's' }; +static const symbol s_6_34[7] = { 'l', 'o', 'g', 0xC3, 0xAD, 'a', 's' }; +static const symbol s_6_35[6] = { 'i', 'd', 'a', 'd', 'e', 's' }; +static const symbol s_6_36[5] = { 'a', 'b', 'l', 'e', 's' }; +static const symbol s_6_37[5] = { 'i', 'b', 'l', 'e', 's' }; +static const symbol s_6_38[7] = { 'a', 'c', 'i', 'o', 'n', 'e', 's' }; +static const symbol s_6_39[7] = { 'u', 'c', 'i', 'o', 'n', 'e', 's' }; +static const symbol s_6_40[6] = { 'a', 'd', 'o', 'r', 'e', 's' }; +static const symbol s_6_41[5] = { 'a', 'n', 't', 'e', 's' }; +static const symbol s_6_42[4] = { 'i', 'c', 'o', 's' }; +static const symbol s_6_43[5] = { 'i', 's', 'm', 'o', 's' }; +static const symbol s_6_44[4] = { 'o', 's', 'o', 's' }; +static const symbol s_6_45[8] = { 'a', 'm', 'i', 'e', 'n', 't', 'o', 's' }; +static const symbol s_6_46[8] = { 'i', 'm', 'i', 'e', 'n', 't', 'o', 's' }; +static const symbol s_6_47[4] = { 'i', 'v', 'o', 's' }; +static const struct among a_6[48] = { +{ 3, s_6_0, 0, 1, 0}, +{ 5, s_6_1, 0, 2, 0}, +{ 5, s_6_2, 0, 5, 0}, +{ 5, s_6_3, 0, 2, 0}, +{ 3, s_6_4, 0, 1, 0}, +{ 4, s_6_5, 0, 1, 0}, +{ 3, s_6_6, 0, 9, 0}, +{ 4, s_6_7, 0, 1, 0}, +{ 6, s_6_8, 0, 3, 0}, +{ 4, s_6_9, 0, 8, 0}, +{ 4, s_6_10, 0, 1, 0}, +{ 4, s_6_11, 0, 1, 0}, +{ 4, s_6_12, 0, 2, 0}, +{ 5, s_6_13, 0, 7, 0}, +{ 6, s_6_14, -1, 6, 0}, +{ 5, s_6_15, 0, 2, 0}, +{ 5, s_6_16, 0, 4, 0}, +{ 6, s_6_17, 0, 2, 0}, +{ 6, s_6_18, 0, 4, 0}, +{ 3, s_6_19, 0, 1, 0}, +{ 4, s_6_20, 0, 1, 0}, +{ 3, s_6_21, 0, 1, 0}, +{ 7, s_6_22, 0, 1, 0}, +{ 7, s_6_23, 0, 1, 0}, +{ 3, s_6_24, 0, 9, 0}, +{ 4, s_6_25, 0, 2, 0}, +{ 4, s_6_26, 0, 1, 0}, +{ 6, s_6_27, 0, 2, 0}, +{ 6, s_6_28, 0, 5, 0}, +{ 6, s_6_29, 0, 2, 0}, +{ 4, s_6_30, 0, 1, 0}, +{ 5, s_6_31, 0, 1, 0}, +{ 4, s_6_32, 0, 9, 0}, +{ 5, s_6_33, 0, 1, 0}, +{ 7, s_6_34, 0, 3, 0}, +{ 6, s_6_35, 0, 8, 0}, +{ 5, s_6_36, 0, 1, 0}, +{ 5, s_6_37, 0, 1, 0}, +{ 7, s_6_38, 0, 2, 0}, +{ 7, s_6_39, 0, 4, 0}, +{ 6, s_6_40, 0, 2, 0}, +{ 5, s_6_41, 0, 2, 0}, +{ 4, s_6_42, 0, 1, 0}, +{ 5, s_6_43, 0, 1, 0}, +{ 4, s_6_44, 0, 1, 0}, +{ 8, s_6_45, 0, 1, 0}, +{ 8, s_6_46, 0, 1, 0}, +{ 4, s_6_47, 0, 9, 0} }; static const symbol s_7_0[2] = { 'y', 'a' }; @@ -249,21 +259,19 @@ static const symbol s_7_8[3] = { 'y', 'e', 's' }; static const symbol s_7_9[4] = { 'y', 'a', 'i', 's' }; static const symbol s_7_10[5] = { 'y', 'a', 'm', 'o', 's' }; static const symbol s_7_11[3] = { 'y', 0xC3, 0xB3 }; - -static const struct among a_7[12] = -{ -{ 2, s_7_0, -1, 1, 0}, -{ 2, s_7_1, -1, 1, 0}, -{ 3, s_7_2, -1, 1, 0}, -{ 3, s_7_3, -1, 1, 0}, -{ 5, s_7_4, -1, 1, 0}, -{ 5, s_7_5, -1, 1, 0}, -{ 2, s_7_6, -1, 1, 0}, -{ 3, s_7_7, -1, 1, 0}, -{ 3, s_7_8, -1, 1, 0}, -{ 4, s_7_9, -1, 1, 0}, -{ 5, s_7_10, -1, 1, 0}, -{ 3, s_7_11, -1, 1, 0} +static const struct among a_7[12] = { +{ 2, s_7_0, 0, 1, 0}, +{ 2, s_7_1, 0, 1, 0}, +{ 3, s_7_2, 0, 1, 0}, +{ 3, s_7_3, 0, 1, 0}, +{ 5, s_7_4, 0, 1, 0}, +{ 5, s_7_5, 0, 1, 0}, +{ 2, s_7_6, 0, 1, 0}, +{ 3, s_7_7, 0, 1, 0}, +{ 3, s_7_8, 0, 1, 0}, +{ 4, s_7_9, 0, 1, 0}, +{ 5, s_7_10, 0, 1, 0}, +{ 3, s_7_11, 0, 1, 0} }; static const symbol s_8_0[3] = { 'a', 'b', 'a' }; @@ -362,105 +370,103 @@ static const symbol s_8_92[4] = { 'a', 'r', 0xC3, 0xA9 }; static const symbol s_8_93[4] = { 'e', 'r', 0xC3, 0xA9 }; static const symbol s_8_94[4] = { 'i', 'r', 0xC3, 0xA9 }; static const symbol s_8_95[3] = { 'i', 0xC3, 0xB3 }; - -static const struct among a_8[96] = -{ -{ 3, s_8_0, -1, 2, 0}, -{ 3, s_8_1, -1, 2, 0}, -{ 3, s_8_2, -1, 2, 0}, -{ 3, s_8_3, -1, 2, 0}, -{ 4, s_8_4, -1, 2, 0}, -{ 3, s_8_5, -1, 2, 0}, -{ 5, s_8_6, 5, 2, 0}, -{ 5, s_8_7, 5, 2, 0}, -{ 5, s_8_8, 5, 2, 0}, -{ 2, s_8_9, -1, 2, 0}, -{ 2, s_8_10, -1, 2, 0}, -{ 2, s_8_11, -1, 2, 0}, -{ 3, s_8_12, -1, 2, 0}, -{ 4, s_8_13, -1, 2, 0}, -{ 4, s_8_14, -1, 2, 0}, -{ 4, s_8_15, -1, 2, 0}, -{ 2, s_8_16, -1, 2, 0}, -{ 4, s_8_17, 16, 2, 0}, -{ 4, s_8_18, 16, 2, 0}, -{ 5, s_8_19, 16, 2, 0}, -{ 4, s_8_20, 16, 2, 0}, -{ 6, s_8_21, 20, 2, 0}, -{ 6, s_8_22, 20, 2, 0}, -{ 6, s_8_23, 20, 2, 0}, -{ 2, s_8_24, -1, 1, 0}, -{ 4, s_8_25, 24, 2, 0}, -{ 5, s_8_26, 24, 2, 0}, -{ 4, s_8_27, -1, 2, 0}, -{ 5, s_8_28, -1, 2, 0}, -{ 5, s_8_29, -1, 2, 0}, -{ 5, s_8_30, -1, 2, 0}, -{ 5, s_8_31, -1, 2, 0}, -{ 3, s_8_32, -1, 2, 0}, -{ 3, s_8_33, -1, 2, 0}, -{ 4, s_8_34, -1, 2, 0}, -{ 5, s_8_35, -1, 2, 0}, -{ 2, s_8_36, -1, 2, 0}, -{ 2, s_8_37, -1, 2, 0}, -{ 2, s_8_38, -1, 2, 0}, -{ 2, s_8_39, -1, 2, 0}, -{ 4, s_8_40, 39, 2, 0}, -{ 4, s_8_41, 39, 2, 0}, -{ 4, s_8_42, 39, 2, 0}, -{ 4, s_8_43, 39, 2, 0}, -{ 5, s_8_44, 39, 2, 0}, -{ 4, s_8_45, 39, 2, 0}, -{ 6, s_8_46, 45, 2, 0}, -{ 6, s_8_47, 45, 2, 0}, -{ 6, s_8_48, 45, 2, 0}, -{ 2, s_8_49, -1, 1, 0}, -{ 4, s_8_50, 49, 2, 0}, -{ 5, s_8_51, 49, 2, 0}, -{ 5, s_8_52, -1, 2, 0}, -{ 5, s_8_53, -1, 2, 0}, -{ 6, s_8_54, -1, 2, 0}, -{ 5, s_8_55, -1, 2, 0}, -{ 7, s_8_56, 55, 2, 0}, -{ 7, s_8_57, 55, 2, 0}, -{ 7, s_8_58, 55, 2, 0}, -{ 5, s_8_59, -1, 2, 0}, -{ 6, s_8_60, -1, 2, 0}, -{ 6, s_8_61, -1, 2, 0}, -{ 6, s_8_62, -1, 2, 0}, -{ 4, s_8_63, -1, 2, 0}, -{ 4, s_8_64, -1, 1, 0}, -{ 6, s_8_65, 64, 2, 0}, -{ 6, s_8_66, 64, 2, 0}, -{ 6, s_8_67, 64, 2, 0}, -{ 4, s_8_68, -1, 2, 0}, -{ 4, s_8_69, -1, 2, 0}, -{ 4, s_8_70, -1, 2, 0}, -{ 7, s_8_71, 70, 2, 0}, -{ 7, s_8_72, 70, 2, 0}, -{ 8, s_8_73, 70, 2, 0}, -{ 6, s_8_74, 70, 2, 0}, -{ 8, s_8_75, 74, 2, 0}, -{ 8, s_8_76, 74, 2, 0}, -{ 8, s_8_77, 74, 2, 0}, -{ 4, s_8_78, -1, 1, 0}, -{ 6, s_8_79, 78, 2, 0}, -{ 6, s_8_80, 78, 2, 0}, -{ 6, s_8_81, 78, 2, 0}, -{ 7, s_8_82, 78, 2, 0}, -{ 8, s_8_83, 78, 2, 0}, -{ 4, s_8_84, -1, 2, 0}, -{ 5, s_8_85, -1, 2, 0}, -{ 5, s_8_86, -1, 2, 0}, -{ 5, s_8_87, -1, 2, 0}, -{ 3, s_8_88, -1, 2, 0}, -{ 4, s_8_89, -1, 2, 0}, -{ 4, s_8_90, -1, 2, 0}, -{ 4, s_8_91, -1, 2, 0}, -{ 4, s_8_92, -1, 2, 0}, -{ 4, s_8_93, -1, 2, 0}, -{ 4, s_8_94, -1, 2, 0}, -{ 3, s_8_95, -1, 2, 0} +static const struct among a_8[96] = { +{ 3, s_8_0, 0, 2, 0}, +{ 3, s_8_1, 0, 2, 0}, +{ 3, s_8_2, 0, 2, 0}, +{ 3, s_8_3, 0, 2, 0}, +{ 4, s_8_4, 0, 2, 0}, +{ 3, s_8_5, 0, 2, 0}, +{ 5, s_8_6, -1, 2, 0}, +{ 5, s_8_7, -2, 2, 0}, +{ 5, s_8_8, -3, 2, 0}, +{ 2, s_8_9, 0, 2, 0}, +{ 2, s_8_10, 0, 2, 0}, +{ 2, s_8_11, 0, 2, 0}, +{ 3, s_8_12, 0, 2, 0}, +{ 4, s_8_13, 0, 2, 0}, +{ 4, s_8_14, 0, 2, 0}, +{ 4, s_8_15, 0, 2, 0}, +{ 2, s_8_16, 0, 2, 0}, +{ 4, s_8_17, -1, 2, 0}, +{ 4, s_8_18, -2, 2, 0}, +{ 5, s_8_19, -3, 2, 0}, +{ 4, s_8_20, -4, 2, 0}, +{ 6, s_8_21, -1, 2, 0}, +{ 6, s_8_22, -2, 2, 0}, +{ 6, s_8_23, -3, 2, 0}, +{ 2, s_8_24, 0, 1, 0}, +{ 4, s_8_25, -1, 2, 0}, +{ 5, s_8_26, -2, 2, 0}, +{ 4, s_8_27, 0, 2, 0}, +{ 5, s_8_28, 0, 2, 0}, +{ 5, s_8_29, 0, 2, 0}, +{ 5, s_8_30, 0, 2, 0}, +{ 5, s_8_31, 0, 2, 0}, +{ 3, s_8_32, 0, 2, 0}, +{ 3, s_8_33, 0, 2, 0}, +{ 4, s_8_34, 0, 2, 0}, +{ 5, s_8_35, 0, 2, 0}, +{ 2, s_8_36, 0, 2, 0}, +{ 2, s_8_37, 0, 2, 0}, +{ 2, s_8_38, 0, 2, 0}, +{ 2, s_8_39, 0, 2, 0}, +{ 4, s_8_40, -1, 2, 0}, +{ 4, s_8_41, -2, 2, 0}, +{ 4, s_8_42, -3, 2, 0}, +{ 4, s_8_43, -4, 2, 0}, +{ 5, s_8_44, -5, 2, 0}, +{ 4, s_8_45, -6, 2, 0}, +{ 6, s_8_46, -1, 2, 0}, +{ 6, s_8_47, -2, 2, 0}, +{ 6, s_8_48, -3, 2, 0}, +{ 2, s_8_49, 0, 1, 0}, +{ 4, s_8_50, -1, 2, 0}, +{ 5, s_8_51, -2, 2, 0}, +{ 5, s_8_52, 0, 2, 0}, +{ 5, s_8_53, 0, 2, 0}, +{ 6, s_8_54, 0, 2, 0}, +{ 5, s_8_55, 0, 2, 0}, +{ 7, s_8_56, -1, 2, 0}, +{ 7, s_8_57, -2, 2, 0}, +{ 7, s_8_58, -3, 2, 0}, +{ 5, s_8_59, 0, 2, 0}, +{ 6, s_8_60, 0, 2, 0}, +{ 6, s_8_61, 0, 2, 0}, +{ 6, s_8_62, 0, 2, 0}, +{ 4, s_8_63, 0, 2, 0}, +{ 4, s_8_64, 0, 1, 0}, +{ 6, s_8_65, -1, 2, 0}, +{ 6, s_8_66, -2, 2, 0}, +{ 6, s_8_67, -3, 2, 0}, +{ 4, s_8_68, 0, 2, 0}, +{ 4, s_8_69, 0, 2, 0}, +{ 4, s_8_70, 0, 2, 0}, +{ 7, s_8_71, -1, 2, 0}, +{ 7, s_8_72, -2, 2, 0}, +{ 8, s_8_73, -3, 2, 0}, +{ 6, s_8_74, -4, 2, 0}, +{ 8, s_8_75, -1, 2, 0}, +{ 8, s_8_76, -2, 2, 0}, +{ 8, s_8_77, -3, 2, 0}, +{ 4, s_8_78, 0, 1, 0}, +{ 6, s_8_79, -1, 2, 0}, +{ 6, s_8_80, -2, 2, 0}, +{ 6, s_8_81, -3, 2, 0}, +{ 7, s_8_82, -4, 2, 0}, +{ 8, s_8_83, -5, 2, 0}, +{ 4, s_8_84, 0, 2, 0}, +{ 5, s_8_85, 0, 2, 0}, +{ 5, s_8_86, 0, 2, 0}, +{ 5, s_8_87, 0, 2, 0}, +{ 3, s_8_88, 0, 2, 0}, +{ 4, s_8_89, 0, 2, 0}, +{ 4, s_8_90, 0, 2, 0}, +{ 4, s_8_91, 0, 2, 0}, +{ 4, s_8_92, 0, 2, 0}, +{ 4, s_8_93, 0, 2, 0}, +{ 4, s_8_94, 0, 2, 0}, +{ 3, s_8_95, 0, 2, 0} }; static const symbol s_9_0[1] = { 'a' }; @@ -471,163 +477,145 @@ static const symbol s_9_4[2] = { 0xC3, 0xA1 }; static const symbol s_9_5[2] = { 0xC3, 0xA9 }; static const symbol s_9_6[2] = { 0xC3, 0xAD }; static const symbol s_9_7[2] = { 0xC3, 0xB3 }; - -static const struct among a_9[8] = -{ -{ 1, s_9_0, -1, 1, 0}, -{ 1, s_9_1, -1, 2, 0}, -{ 1, s_9_2, -1, 1, 0}, -{ 2, s_9_3, -1, 1, 0}, -{ 2, s_9_4, -1, 1, 0}, -{ 2, s_9_5, -1, 2, 0}, -{ 2, s_9_6, -1, 1, 0}, -{ 2, s_9_7, -1, 1, 0} +static const struct among a_9[8] = { +{ 1, s_9_0, 0, 1, 0}, +{ 1, s_9_1, 0, 2, 0}, +{ 1, s_9_2, 0, 1, 0}, +{ 2, s_9_3, 0, 1, 0}, +{ 2, s_9_4, 0, 1, 0}, +{ 2, s_9_5, 0, 2, 0}, +{ 2, s_9_6, 0, 1, 0}, +{ 2, s_9_7, 0, 1, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 17, 4, 10 }; -static const symbol s_0[] = { 'a' }; -static const symbol s_1[] = { 'e' }; -static const symbol s_2[] = { 'i' }; -static const symbol s_3[] = { 'o' }; -static const symbol s_4[] = { 'u' }; -static const symbol s_5[] = { 'i', 'e', 'n', 'd', 'o' }; -static const symbol s_6[] = { 'a', 'n', 'd', 'o' }; -static const symbol s_7[] = { 'a', 'r' }; -static const symbol s_8[] = { 'e', 'r' }; -static const symbol s_9[] = { 'i', 'r' }; -static const symbol s_10[] = { 'i', 'c' }; -static const symbol s_11[] = { 'l', 'o', 'g' }; -static const symbol s_12[] = { 'u' }; -static const symbol s_13[] = { 'e', 'n', 't', 'e' }; -static const symbol s_14[] = { 'a', 't' }; -static const symbol s_15[] = { 'a', 't' }; - static int r_mark_regions(struct SN_env * z) { - z->I[2] = z->l; - z->I[1] = z->l; - z->I[0] = z->l; - { int c1 = z->c; - { int c2 = z->c; - if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab2; - { int c3 = z->c; - if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab4; - + ((SN_local *)z)->i_pV = z->l; + ((SN_local *)z)->i_p1 = z->l; + ((SN_local *)z)->i_p2 = z->l; + { + int v_1 = z->c; + do { + int v_2 = z->c; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab1; + do { + int v_3 = z->c; + if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab2; { int ret = out_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab4; + if (ret < 0) goto lab2; z->c += ret; } - goto lab3; - lab4: - z->c = c3; - if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab2; - + break; + lab2: + z->c = v_3; + if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab1; { int ret = in_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab2; + if (ret < 0) goto lab1; z->c += ret; } - } - lab3: - goto lab1; - lab2: - z->c = c2; + } while (0); + break; + lab1: + z->c = v_2; if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab0; - { int c4 = z->c; - if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab6; - + do { + int v_4 = z->c; + if (out_grouping_U(z, g_v, 97, 252, 0)) goto lab3; { int ret = out_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab6; + if (ret < 0) goto lab3; z->c += ret; } - goto lab5; - lab6: - z->c = c4; + break; + lab3: + z->c = v_4; if (in_grouping_U(z, g_v, 97, 252, 0)) goto lab0; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } - } - lab5: - ; - } - lab1: - z->I[2] = z->c; + } while (0); + } while (0); + ((SN_local *)z)->i_pV = z->c; lab0: - z->c = c1; + z->c = v_1; } - { int c5 = z->c; - + { + int v_5 = z->c; { int ret = out_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[1] = z->c; - + ((SN_local *)z)->i_p1 = z->c; { int ret = out_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - { int ret = in_grouping_U(z, g_v, 97, 252, 1); - if (ret < 0) goto lab7; + if (ret < 0) goto lab4; z->c += ret; } - z->I[0] = z->c; - lab7: - z->c = c5; + ((SN_local *)z)->i_p2 = z->c; + lab4: + z->c = v_5; } return 1; } static int r_postlude(struct SN_env * z) { int among_var; - while(1) { - int c1 = z->c; + while (1) { + int v_1 = z->c; z->bra = z->c; if (z->c + 1 >= z->l || z->p[z->c + 1] >> 5 != 5 || !((67641858 >> (z->p[z->c + 1] & 0x1f)) & 1)) among_var = 6; else - among_var = find_among(z, a_0, 6); + among_var = find_among(z, a_0, 6, 0); z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_0); + { + int ret = slice_from_s(z, 1, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 1, s_1); + { + int ret = slice_from_s(z, 1, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_2); + { + int ret = slice_from_s(z, 1, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_3); + { + int ret = slice_from_s(z, 1, s_3); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 1, s_4); + { + int ret = slice_from_s(z, 1, s_4); if (ret < 0) return ret; } break; case 6: - { int ret = skip_utf8(z->p, z->c, z->l, 1); + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } @@ -635,76 +623,84 @@ static int r_postlude(struct SN_env * z) { } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; } static int r_RV(struct SN_env * z) { - return z->I[2] <= z->c; + return ((SN_local *)z)->i_pV <= z->c; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R2(struct SN_env * z) { - return z->I[0] <= z->c; + return ((SN_local *)z)->i_p2 <= z->c; } static int r_attached_pronoun(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((557090 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_1, 13)) return 0; + if (!find_among_b(z, a_1, 13, 0)) return 0; z->bra = z->c; if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 111 && z->p[z->c - 1] != 114)) return 0; - among_var = find_among_b(z, a_2, 11); + among_var = find_among_b(z, a_2, 11, 0); if (!among_var) return 0; - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } switch (among_var) { case 1: z->bra = z->c; - { int ret = slice_from_s(z, 5, s_5); + { + int ret = slice_from_s(z, 5, s_5); if (ret < 0) return ret; } break; case 2: z->bra = z->c; - { int ret = slice_from_s(z, 4, s_6); + { + int ret = slice_from_s(z, 4, s_6); if (ret < 0) return ret; } break; case 3: z->bra = z->c; - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 4: z->bra = z->c; - { int ret = slice_from_s(z, 2, s_8); + { + int ret = slice_from_s(z, 2, s_8); if (ret < 0) return ret; } break; case 5: z->bra = z->c; - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; case 6: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 7: if (z->c <= z->lb || z->p[z->c - 1] != 'u') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -716,34 +712,41 @@ static int r_standard_suffix(struct SN_env * z) { int among_var; z->ket = z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((835634 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - among_var = find_among_b(z, a_6, 46); + among_var = find_among_b(z, a_6, 48, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_10))) { z->c = z->l - m1; goto lab0; } + if (!(eq_s_b(z, 2, s_10))) { z->c = z->l - v_1; goto lab0; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -751,59 +754,72 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 3: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 3, s_11); + { + int ret = slice_from_s(z, 3, s_11); if (ret < 0) return ret; } break; case 4: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 1, s_12); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } break; case 5: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_from_s(z, 4, s_13); + { + int ret = slice_from_s(z, 4, s_13); if (ret < 0) return ret; } break; case 6: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m2; goto lab1; } - among_var = find_among_b(z, a_3, 4); - if (!among_var) { z->c = z->l - m2; goto lab1; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4718616 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_2; goto lab1; } + among_var = find_among_b(z, a_3, 4, 0); + if (!among_var) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } switch (among_var) { case 1: z->ket = z->c; - if (!(eq_s_b(z, 2, s_14))) { z->c = z->l - m2; goto lab1; } + if (!(eq_s_b(z, 2, s_14))) { z->c = z->l - v_2; goto lab1; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m2; goto lab1; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -813,22 +829,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 7: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; z->ket = z->c; - if (z->c - 3 <= z->lb || z->p[z->c - 1] != 101) { z->c = z->l - m3; goto lab2; } - if (!find_among_b(z, a_4, 3)) { z->c = z->l - m3; goto lab2; } + if (z->c - 3 <= z->lb || z->p[z->c - 1] != 101) { z->c = z->l - v_3; goto lab2; } + if (!find_among_b(z, a_4, 3, 0)) { z->c = z->l - v_3; goto lab2; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m3; goto lab2; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_3; goto lab2; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab2: @@ -836,22 +857,27 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 8: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - m4; goto lab3; } - if (!find_among_b(z, a_5, 3)) { z->c = z->l - m4; goto lab3; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((4198408 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->c = z->l - v_4; goto lab3; } + if (!find_among_b(z, a_5, 3, 0)) { z->c = z->l - v_4; goto lab3; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m4; goto lab3; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab3: @@ -859,21 +885,26 @@ static int r_standard_suffix(struct SN_env * z) { } break; case 9: - { int ret = r_R2(z); + { + int ret = r_R2(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - m5; goto lab4; } + if (!(eq_s_b(z, 2, s_15))) { z->c = z->l - v_5; goto lab4; } z->bra = z->c; - { int ret = r_R2(z); - if (ret == 0) { z->c = z->l - m5; goto lab4; } + { + int ret = r_R2(z); + if (ret == 0) { z->c = z->l - v_5; goto lab4; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab4: @@ -885,18 +916,19 @@ static int r_standard_suffix(struct SN_env * z) { } static int r_y_verb_suffix(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - if (!find_among_b(z, a_7, 12)) { z->lb = mlimit1; return 0; } + if (!find_among_b(z, a_7, 12, 0)) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } if (z->c <= z->lb || z->p[z->c - 1] != 'u') return 0; z->c--; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -904,36 +936,40 @@ static int r_y_verb_suffix(struct SN_env * z) { static int r_verb_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[2]) return 0; - mlimit1 = z->lb; z->lb = z->I[2]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_pV) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_pV; z->ket = z->c; - among_var = find_among_b(z, a_8, 96); - if (!among_var) { z->lb = mlimit1; return 0; } + among_var = find_among_b(z, a_8, 96, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int m2 = z->l - z->c; (void)m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - m2; goto lab0; } + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - v_2; goto lab0; } z->c--; - { int m_test3 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - m2; goto lab0; } + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - v_2; goto lab0; } z->c--; - z->c = z->l - m_test3; + z->c = z->l - v_3; } lab0: ; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -944,40 +980,48 @@ static int r_verb_suffix(struct SN_env * z) { static int r_residual_suffix(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_9, 8); + among_var = find_among_b(z, a_9, 8, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_RV(z); + { + int ret = r_RV(z); if (ret <= 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - m1; goto lab0; } + if (z->c <= z->lb || z->p[z->c - 1] != 'u') { z->c = z->l - v_1; goto lab0; } z->c--; z->bra = z->c; - { int m_test2 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - m1; goto lab0; } + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'g') { z->c = z->l - v_1; goto lab0; } z->c--; - z->c = z->l - m_test2; + z->c = z->l - v_2; } - { int ret = r_RV(z); - if (ret == 0) { z->c = z->l - m1; goto lab0; } + { + int ret = r_RV(z); + if (ret == 0) { z->c = z->l - v_1; goto lab0; } if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } lab0: @@ -989,60 +1033,79 @@ static int r_residual_suffix(struct SN_env * z) { } extern int spanish_UTF_8_stem(struct SN_env * z) { - - { int ret = r_mark_regions(z); + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_attached_pronoun(z); + { + int v_1 = z->l - z->c; + { + int ret = r_attached_pronoun(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_standard_suffix(z); - if (ret == 0) goto lab2; + { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + { + int ret = r_standard_suffix(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m3; - { int ret = r_y_verb_suffix(z); - if (ret == 0) goto lab3; + break; + lab1: + z->c = z->l - v_3; + { + int ret = r_y_verb_suffix(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab1; - lab3: - z->c = z->l - m3; - { int ret = r_verb_suffix(z); + break; + lab2: + z->c = z->l - v_3; + { + int ret = r_verb_suffix(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab1: + } while (0); lab0: - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_residual_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_residual_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; - { int c5 = z->c; - { int ret = r_postlude(z); + { + int v_5 = z->c; + { + int ret = r_postlude(z); if (ret < 0) return ret; } - z->c = c5; + z->c = v_5; } return 1; } -extern struct SN_env * spanish_UTF_8_create_env(void) { return SN_create_env(0, 3); } +extern struct SN_env * spanish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p2 = 0; + ((SN_local *)z)->i_p1 = 0; + ((SN_local *)z)->i_pV = 0; + } + return z; +} -extern void spanish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void spanish_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_swedish.c b/src/backend/snowball/libstemmer/stem_UTF_8_swedish.c index c082cb0778dc0..c64b2d84d9211 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_swedish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_swedish.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from swedish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_swedish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,133 +20,169 @@ extern int swedish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_other_suffix(struct SN_env * z); static int r_consonant_pair(struct SN_env * z); static int r_main_suffix(struct SN_env * z); static int r_mark_regions(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - +static int r_et_condition(struct SN_env * z); -extern struct SN_env * swedish_UTF_8_create_env(void); -extern void swedish_UTF_8_close_env(struct SN_env * z); - - -#ifdef __cplusplus -} -#endif -static const symbol s_0_0[1] = { 'a' }; -static const symbol s_0_1[4] = { 'a', 'r', 'n', 'a' }; -static const symbol s_0_2[4] = { 'e', 'r', 'n', 'a' }; -static const symbol s_0_3[7] = { 'h', 'e', 't', 'e', 'r', 'n', 'a' }; -static const symbol s_0_4[4] = { 'o', 'r', 'n', 'a' }; -static const symbol s_0_5[2] = { 'a', 'd' }; -static const symbol s_0_6[1] = { 'e' }; -static const symbol s_0_7[3] = { 'a', 'd', 'e' }; -static const symbol s_0_8[4] = { 'a', 'n', 'd', 'e' }; -static const symbol s_0_9[4] = { 'a', 'r', 'n', 'e' }; -static const symbol s_0_10[3] = { 'a', 'r', 'e' }; -static const symbol s_0_11[4] = { 'a', 's', 't', 'e' }; -static const symbol s_0_12[2] = { 'e', 'n' }; -static const symbol s_0_13[5] = { 'a', 'n', 'd', 'e', 'n' }; -static const symbol s_0_14[4] = { 'a', 'r', 'e', 'n' }; -static const symbol s_0_15[5] = { 'h', 'e', 't', 'e', 'n' }; -static const symbol s_0_16[3] = { 'e', 'r', 'n' }; -static const symbol s_0_17[2] = { 'a', 'r' }; -static const symbol s_0_18[2] = { 'e', 'r' }; -static const symbol s_0_19[5] = { 'h', 'e', 't', 'e', 'r' }; -static const symbol s_0_20[2] = { 'o', 'r' }; -static const symbol s_0_21[1] = { 's' }; -static const symbol s_0_22[2] = { 'a', 's' }; -static const symbol s_0_23[5] = { 'a', 'r', 'n', 'a', 's' }; -static const symbol s_0_24[5] = { 'e', 'r', 'n', 'a', 's' }; -static const symbol s_0_25[5] = { 'o', 'r', 'n', 'a', 's' }; -static const symbol s_0_26[2] = { 'e', 's' }; -static const symbol s_0_27[4] = { 'a', 'd', 'e', 's' }; -static const symbol s_0_28[5] = { 'a', 'n', 'd', 'e', 's' }; -static const symbol s_0_29[3] = { 'e', 'n', 's' }; -static const symbol s_0_30[5] = { 'a', 'r', 'e', 'n', 's' }; -static const symbol s_0_31[6] = { 'h', 'e', 't', 'e', 'n', 's' }; -static const symbol s_0_32[4] = { 'e', 'r', 'n', 's' }; -static const symbol s_0_33[2] = { 'a', 't' }; -static const symbol s_0_34[5] = { 'a', 'n', 'd', 'e', 't' }; -static const symbol s_0_35[3] = { 'h', 'e', 't' }; -static const symbol s_0_36[3] = { 'a', 's', 't' }; +static const symbol s_0[] = { 'e', 't' }; +static const symbol s_1[] = { 0xC3, 0xB6, 's' }; +static const symbol s_2[] = { 'f', 'u', 'l', 'l' }; -static const struct among a_0[37] = -{ -{ 1, s_0_0, -1, 1, 0}, -{ 4, s_0_1, 0, 1, 0}, -{ 4, s_0_2, 0, 1, 0}, -{ 7, s_0_3, 2, 1, 0}, -{ 4, s_0_4, 0, 1, 0}, -{ 2, s_0_5, -1, 1, 0}, -{ 1, s_0_6, -1, 1, 0}, -{ 3, s_0_7, 6, 1, 0}, -{ 4, s_0_8, 6, 1, 0}, -{ 4, s_0_9, 6, 1, 0}, -{ 3, s_0_10, 6, 1, 0}, -{ 4, s_0_11, 6, 1, 0}, -{ 2, s_0_12, -1, 1, 0}, -{ 5, s_0_13, 12, 1, 0}, -{ 4, s_0_14, 12, 1, 0}, -{ 5, s_0_15, 12, 1, 0}, -{ 3, s_0_16, -1, 1, 0}, -{ 2, s_0_17, -1, 1, 0}, -{ 2, s_0_18, -1, 1, 0}, -{ 5, s_0_19, 18, 1, 0}, -{ 2, s_0_20, -1, 1, 0}, -{ 1, s_0_21, -1, 2, 0}, -{ 2, s_0_22, 21, 1, 0}, -{ 5, s_0_23, 22, 1, 0}, -{ 5, s_0_24, 22, 1, 0}, -{ 5, s_0_25, 22, 1, 0}, -{ 2, s_0_26, 21, 1, 0}, -{ 4, s_0_27, 26, 1, 0}, -{ 5, s_0_28, 26, 1, 0}, -{ 3, s_0_29, 21, 1, 0}, -{ 5, s_0_30, 29, 1, 0}, -{ 6, s_0_31, 29, 1, 0}, -{ 4, s_0_32, 21, 1, 0}, -{ 2, s_0_33, -1, 1, 0}, -{ 5, s_0_34, -1, 1, 0}, -{ 3, s_0_35, -1, 1, 0}, -{ 3, s_0_36, -1, 1, 0} +static const symbol s_0_0[3] = { 'f', 'a', 'b' }; +static const symbol s_0_1[1] = { 'h' }; +static const symbol s_0_2[3] = { 'p', 'a', 'k' }; +static const symbol s_0_3[3] = { 'r', 'a', 'k' }; +static const symbol s_0_4[4] = { 's', 't', 'a', 'k' }; +static const symbol s_0_5[3] = { 'k', 'o', 'm' }; +static const symbol s_0_6[3] = { 'i', 'e', 't' }; +static const symbol s_0_7[3] = { 'c', 'i', 't' }; +static const symbol s_0_8[3] = { 'd', 'i', 't' }; +static const symbol s_0_9[4] = { 'a', 'l', 'i', 't' }; +static const symbol s_0_10[4] = { 'i', 'l', 'i', 't' }; +static const symbol s_0_11[3] = { 'm', 'i', 't' }; +static const symbol s_0_12[3] = { 'n', 'i', 't' }; +static const symbol s_0_13[3] = { 'p', 'i', 't' }; +static const symbol s_0_14[3] = { 'r', 'i', 't' }; +static const symbol s_0_15[3] = { 's', 'i', 't' }; +static const symbol s_0_16[3] = { 't', 'i', 't' }; +static const symbol s_0_17[3] = { 'u', 'i', 't' }; +static const symbol s_0_18[4] = { 'i', 'v', 'i', 't' }; +static const symbol s_0_19[4] = { 'k', 'v', 'i', 't' }; +static const symbol s_0_20[3] = { 'x', 'i', 't' }; +static const struct among a_0[21] = { +{ 3, s_0_0, 0, -1, 0}, +{ 1, s_0_1, 0, -1, 0}, +{ 3, s_0_2, 0, -1, 0}, +{ 3, s_0_3, 0, -1, 0}, +{ 4, s_0_4, 0, -1, 0}, +{ 3, s_0_5, 0, -1, 0}, +{ 3, s_0_6, 0, -1, 0}, +{ 3, s_0_7, 0, -1, 0}, +{ 3, s_0_8, 0, -1, 0}, +{ 4, s_0_9, 0, -1, 0}, +{ 4, s_0_10, 0, -1, 0}, +{ 3, s_0_11, 0, -1, 0}, +{ 3, s_0_12, 0, -1, 0}, +{ 3, s_0_13, 0, -1, 0}, +{ 3, s_0_14, 0, -1, 0}, +{ 3, s_0_15, 0, -1, 0}, +{ 3, s_0_16, 0, -1, 0}, +{ 3, s_0_17, 0, -1, 0}, +{ 4, s_0_18, 0, -1, 0}, +{ 4, s_0_19, 0, -1, 0}, +{ 3, s_0_20, 0, -1, 0} }; -static const symbol s_1_0[2] = { 'd', 'd' }; -static const symbol s_1_1[2] = { 'g', 'd' }; -static const symbol s_1_2[2] = { 'n', 'n' }; -static const symbol s_1_3[2] = { 'd', 't' }; -static const symbol s_1_4[2] = { 'g', 't' }; -static const symbol s_1_5[2] = { 'k', 't' }; -static const symbol s_1_6[2] = { 't', 't' }; - -static const struct among a_1[7] = -{ -{ 2, s_1_0, -1, -1, 0}, -{ 2, s_1_1, -1, -1, 0}, -{ 2, s_1_2, -1, -1, 0}, -{ 2, s_1_3, -1, -1, 0}, -{ 2, s_1_4, -1, -1, 0}, -{ 2, s_1_5, -1, -1, 0}, -{ 2, s_1_6, -1, -1, 0} +static const symbol s_1_0[1] = { 'a' }; +static const symbol s_1_1[4] = { 'a', 'r', 'n', 'a' }; +static const symbol s_1_2[4] = { 'e', 'r', 'n', 'a' }; +static const symbol s_1_3[7] = { 'h', 'e', 't', 'e', 'r', 'n', 'a' }; +static const symbol s_1_4[4] = { 'o', 'r', 'n', 'a' }; +static const symbol s_1_5[2] = { 'a', 'd' }; +static const symbol s_1_6[1] = { 'e' }; +static const symbol s_1_7[3] = { 'a', 'd', 'e' }; +static const symbol s_1_8[4] = { 'a', 'n', 'd', 'e' }; +static const symbol s_1_9[4] = { 'a', 'r', 'n', 'e' }; +static const symbol s_1_10[3] = { 'a', 'r', 'e' }; +static const symbol s_1_11[4] = { 'a', 's', 't', 'e' }; +static const symbol s_1_12[2] = { 'e', 'n' }; +static const symbol s_1_13[5] = { 'a', 'n', 'd', 'e', 'n' }; +static const symbol s_1_14[4] = { 'a', 'r', 'e', 'n' }; +static const symbol s_1_15[5] = { 'h', 'e', 't', 'e', 'n' }; +static const symbol s_1_16[3] = { 'e', 'r', 'n' }; +static const symbol s_1_17[2] = { 'a', 'r' }; +static const symbol s_1_18[2] = { 'e', 'r' }; +static const symbol s_1_19[5] = { 'h', 'e', 't', 'e', 'r' }; +static const symbol s_1_20[2] = { 'o', 'r' }; +static const symbol s_1_21[1] = { 's' }; +static const symbol s_1_22[2] = { 'a', 's' }; +static const symbol s_1_23[5] = { 'a', 'r', 'n', 'a', 's' }; +static const symbol s_1_24[5] = { 'e', 'r', 'n', 'a', 's' }; +static const symbol s_1_25[5] = { 'o', 'r', 'n', 'a', 's' }; +static const symbol s_1_26[2] = { 'e', 's' }; +static const symbol s_1_27[4] = { 'a', 'd', 'e', 's' }; +static const symbol s_1_28[5] = { 'a', 'n', 'd', 'e', 's' }; +static const symbol s_1_29[3] = { 'e', 'n', 's' }; +static const symbol s_1_30[5] = { 'a', 'r', 'e', 'n', 's' }; +static const symbol s_1_31[6] = { 'h', 'e', 't', 'e', 'n', 's' }; +static const symbol s_1_32[4] = { 'e', 'r', 'n', 's' }; +static const symbol s_1_33[2] = { 'a', 't' }; +static const symbol s_1_34[2] = { 'e', 't' }; +static const symbol s_1_35[5] = { 'a', 'n', 'd', 'e', 't' }; +static const symbol s_1_36[3] = { 'h', 'e', 't' }; +static const symbol s_1_37[3] = { 'a', 's', 't' }; +static const struct among a_1[38] = { +{ 1, s_1_0, 0, 1, 0}, +{ 4, s_1_1, -1, 1, 0}, +{ 4, s_1_2, -2, 1, 0}, +{ 7, s_1_3, -1, 1, 0}, +{ 4, s_1_4, -4, 1, 0}, +{ 2, s_1_5, 0, 1, 0}, +{ 1, s_1_6, 0, 1, 0}, +{ 3, s_1_7, -1, 1, 0}, +{ 4, s_1_8, -2, 1, 0}, +{ 4, s_1_9, -3, 1, 0}, +{ 3, s_1_10, -4, 1, 0}, +{ 4, s_1_11, -5, 1, 0}, +{ 2, s_1_12, 0, 1, 0}, +{ 5, s_1_13, -1, 1, 0}, +{ 4, s_1_14, -2, 1, 0}, +{ 5, s_1_15, -3, 1, 0}, +{ 3, s_1_16, 0, 1, 0}, +{ 2, s_1_17, 0, 1, 0}, +{ 2, s_1_18, 0, 1, 0}, +{ 5, s_1_19, -1, 1, 0}, +{ 2, s_1_20, 0, 1, 0}, +{ 1, s_1_21, 0, 2, 0}, +{ 2, s_1_22, -1, 1, 0}, +{ 5, s_1_23, -1, 1, 0}, +{ 5, s_1_24, -2, 1, 0}, +{ 5, s_1_25, -3, 1, 0}, +{ 2, s_1_26, -5, 1, 0}, +{ 4, s_1_27, -1, 1, 0}, +{ 5, s_1_28, -2, 1, 0}, +{ 3, s_1_29, -8, 1, 0}, +{ 5, s_1_30, -1, 1, 0}, +{ 6, s_1_31, -2, 1, 0}, +{ 4, s_1_32, -11, 1, 0}, +{ 2, s_1_33, 0, 1, 0}, +{ 2, s_1_34, 0, 3, 0}, +{ 5, s_1_35, -1, 1, 0}, +{ 3, s_1_36, -2, 1, 0}, +{ 3, s_1_37, 0, 1, 0} }; -static const symbol s_2_0[2] = { 'i', 'g' }; -static const symbol s_2_1[3] = { 'l', 'i', 'g' }; -static const symbol s_2_2[3] = { 'e', 'l', 's' }; -static const symbol s_2_3[5] = { 'f', 'u', 'l', 'l', 't' }; -static const symbol s_2_4[4] = { 0xC3, 0xB6, 's', 't' }; +static const symbol s_2_0[2] = { 'd', 'd' }; +static const symbol s_2_1[2] = { 'g', 'd' }; +static const symbol s_2_2[2] = { 'n', 'n' }; +static const symbol s_2_3[2] = { 'd', 't' }; +static const symbol s_2_4[2] = { 'g', 't' }; +static const symbol s_2_5[2] = { 'k', 't' }; +static const symbol s_2_6[2] = { 't', 't' }; +static const struct among a_2[7] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 2, s_2_2, 0, -1, 0}, +{ 2, s_2_3, 0, -1, 0}, +{ 2, s_2_4, 0, -1, 0}, +{ 2, s_2_5, 0, -1, 0}, +{ 2, s_2_6, 0, -1, 0} +}; -static const struct among a_2[5] = -{ -{ 2, s_2_0, -1, 1, 0}, -{ 3, s_2_1, 0, 1, 0}, -{ 3, s_2_2, -1, 1, 0}, -{ 5, s_2_3, -1, 3, 0}, -{ 4, s_2_4, -1, 2, 0} +static const symbol s_3_0[2] = { 'i', 'g' }; +static const symbol s_3_1[3] = { 'l', 'i', 'g' }; +static const symbol s_3_2[3] = { 'e', 'l', 's' }; +static const symbol s_3_3[5] = { 'f', 'u', 'l', 'l', 't' }; +static const symbol s_3_4[4] = { 0xC3, 0xB6, 's', 't' }; +static const struct among a_3[5] = { +{ 2, s_3_0, 0, 1, 0}, +{ 3, s_3_1, -1, 1, 0}, +{ 3, s_3_2, 0, 1, 0}, +{ 5, s_3_3, 0, 3, 0}, +{ 4, s_3_4, 0, 2, 0} }; static const unsigned char g_v[] = { 17, 65, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 0, 32 }; @@ -144,57 +191,104 @@ static const unsigned char g_s_ending[] = { 119, 127, 149 }; static const unsigned char g_ost_ending[] = { 173, 58 }; -static const symbol s_0[] = { 0xC3, 0xB6, 's' }; -static const symbol s_1[] = { 'f', 'u', 'l', 'l' }; - static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c_test1 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); if (ret < 0) return 0; z->c = ret; } - z->I[0] = z->c; - z->c = c_test1; + i_x = z->c; + z->c = v_1; + } + { + int ret = out_grouping_U(z, g_v, 97, 246, 1); + if (ret < 0) return 0; + z->c += ret; } - - if (out_grouping_U(z, g_v, 97, 246, 1) < 0) return 0; - { int ret = in_grouping_U(z, g_v, 97, 246, 1); if (ret < 0) return 0; z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab0; - z->I[1] = z->I[0]; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab0; + ((SN_local *)z)->i_p1 = i_x; lab0: return 1; } +static int r_et_condition(struct SN_env * z) { + { + int v_1 = z->l - z->c; + if (out_grouping_b_U(z, g_v, 97, 246, 0)) return 0; + if (in_grouping_b_U(z, g_v, 97, 246, 0)) return 0; + if (z->c > z->lb) goto lab0; + return 0; + lab0: + z->c = z->l - v_1; + { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1059076 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab1; + if (!find_among_b(z, a_0, 21, 0)) goto lab1; + return 0; + lab1: + z->c = z->l - v_2; + } + } + return 1; +} + static int r_main_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851442 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_0, 37); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1851442 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_1, 38, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - if (in_grouping_b_U(z, g_s_ending, 98, 121, 0)) return 0; - { int ret = slice_del(z); + do { + int v_2 = z->l - z->c; + if (!(eq_s_b(z, 2, s_0))) goto lab0; + { + int ret = r_et_condition(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; + } + z->bra = z->c; + break; + lab0: + z->c = z->l - v_2; + if (in_grouping_b_U(z, g_s_ending, 98, 121, 0)) return 0; + } while (0); + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + break; + case 3: + { + int ret = r_et_condition(z); + if (ret <= 0) return ret; + } + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -203,56 +297,62 @@ static int r_main_suffix(struct SN_env * z) { } static int r_consonant_pair(struct SN_env * z) { - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; - { int m2 = z->l - z->c; (void)m2; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1064976 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - if (!find_among_b(z, a_1, 7)) { z->lb = mlimit1; return 0; } - z->c = z->l - m2; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; + { + int v_2 = z->l - z->c; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1064976 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + if (!find_among_b(z, a_2, 7, 0)) { z->lb = v_1; return 0; } + z->c = z->l - v_2; z->ket = z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) { z->lb = mlimit1; return 0; } + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) { z->lb = v_1; return 0; } z->c = ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } } - z->lb = mlimit1; + z->lb = v_1; } return 1; } static int r_other_suffix(struct SN_env * z) { int among_var; - - { int mlimit1; - if (z->c < z->I[1]) return 0; - mlimit1 = z->lb; z->lb = z->I[1]; + { + int v_1; + if (z->c < ((SN_local *)z)->i_p1) return 0; + v_1 = z->lb; z->lb = ((SN_local *)z)->i_p1; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = mlimit1; return 0; } - among_var = find_among_b(z, a_2, 5); - if (!among_var) { z->lb = mlimit1; return 0; } + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((1572992 >> (z->p[z->c - 1] & 0x1f)) & 1)) { z->lb = v_1; return 0; } + among_var = find_among_b(z, a_3, 5, 0); + if (!among_var) { z->lb = v_1; return 0; } z->bra = z->c; - z->lb = mlimit1; + z->lb = v_1; } switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: if (in_grouping_b_U(z, g_ost_ending, 105, 118, 0)) return 0; - { int ret = slice_from_s(z, 3, s_0); + { + int ret = slice_from_s(z, 3, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 4, s_1); + { + int ret = slice_from_s(z, 4, s_2); if (ret < 0) return ret; } break; @@ -261,37 +361,52 @@ static int r_other_suffix(struct SN_env * z) { } extern int swedish_UTF_8_stem(struct SN_env * z) { - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - { int m2 = z->l - z->c; (void)m2; - { int ret = r_main_suffix(z); + { + int v_2 = z->l - z->c; + { + int ret = r_main_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_consonant_pair(z); + { + int v_3 = z->l - z->c; + { + int ret = r_consonant_pair(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_other_suffix(z); + { + int v_4 = z->l - z->c; + { + int ret = r_other_suffix(z); if (ret < 0) return ret; } - z->c = z->l - m4; + z->c = z->l - v_4; } z->c = z->lb; return 1; } -extern struct SN_env * swedish_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * swedish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void swedish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void swedish_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_tamil.c b/src/backend/snowball/libstemmer/stem_UTF_8_tamil.c index 72e610fdb1ea8..0ce62ffee1ba7 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_tamil.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_tamil.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from tamil.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_tamil.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + unsigned char b_found_vetrumai_urupu; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +20,7 @@ extern int tamil_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_has_min_length(struct SN_env * z); static int r_remove_common_word_endings(struct SN_env * z); static int r_remove_tense_suffixes(struct SN_env * z); @@ -23,29 +35,58 @@ static int r_remove_pronoun_prefixes(struct SN_env * z); static int r_remove_question_prefixes(struct SN_env * z); static int r_remove_question_suffixes(struct SN_env * z); static int r_remove_plural_suffix(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * tamil_UTF_8_create_env(void); -extern void tamil_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xE0, 0xAE, 0x93 }; +static const symbol s_1[] = { 0xE0, 0xAE, 0x92 }; +static const symbol s_2[] = { 0xE0, 0xAE, 0x89 }; +static const symbol s_3[] = { 0xE0, 0xAE, 0x8A }; +static const symbol s_4[] = { 0xE0, 0xAE, 0x8E }; +static const symbol s_5[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_6[] = { 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; +static const symbol s_7[] = { 0xE0, 0xAE, 0xB2, 0xE0, 0xAF, 0x8D }; +static const symbol s_8[] = { 0xE0, 0xAE, 0x9F, 0xE0, 0xAF, 0x81 }; +static const symbol s_9[] = { 0xE0, 0xAF, 0x88 }; +static const symbol s_10[] = { 0xE0, 0xAE, 0xAE, 0xE0, 0xAF, 0x8D }; +static const symbol s_11[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_12[] = { 0xE0, 0xAE, 0xAE, 0xE0, 0xAF, 0x8D }; +static const symbol s_13[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_14[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_15[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_16[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_17[] = { 0xE0, 0xAF, 0x81, 0xE0, 0xAE, 0x99, 0xE0, 0xAF, 0x8D }; +static const symbol s_18[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_19[] = { 0xE0, 0xAE, 0xB2, 0xE0, 0xAF, 0x8D }; +static const symbol s_20[] = { 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; +static const symbol s_21[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_22[] = { 0xE0, 0xAF, 0x81, 0xE0, 0xAE, 0xAE, 0xE0, 0xAF, 0x8D }; +static const symbol s_23[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_24[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_25[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_26[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_27[] = { 0xE0, 0xAE, 0xAE }; +static const symbol s_28[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_29[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_30[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_31[] = { 0xE0, 0xAE, 0xBF }; +static const symbol s_32[] = { 0xE0, 0xAF, 0x88 }; +static const symbol s_33[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_34[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_35[] = { 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x8D }; +static const symbol s_36[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_37[] = { 0xE0, 0xAE, 0x9A }; +static const symbol s_38[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_39[] = { 0xE0, 0xAF, 0x8D }; +static const symbol s_40[] = { 0xE0, 0xAF, 0x8D }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[6] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAF, 0x81 }; static const symbol s_0_1[6] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAF, 0x82 }; static const symbol s_0_2[6] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAF, 0x8A }; static const symbol s_0_3[6] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAF, 0x8B }; - -static const struct among a_0[4] = -{ -{ 6, s_0_0, -1, 3, 0}, -{ 6, s_0_1, -1, 4, 0}, -{ 6, s_0_2, -1, 2, 0}, -{ 6, s_0_3, -1, 1, 0} +static const struct among a_0[4] = { +{ 6, s_0_0, 0, 3, 0}, +{ 6, s_0_1, 0, 4, 0}, +{ 6, s_0_2, 0, 2, 0}, +{ 6, s_0_3, 0, 1, 0} }; static const symbol s_1_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -58,30 +99,26 @@ static const symbol s_1_6[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_1_7[3] = { 0xE0, 0xAE, 0xAE }; static const symbol s_1_8[3] = { 0xE0, 0xAE, 0xAF }; static const symbol s_1_9[3] = { 0xE0, 0xAE, 0xB5 }; - -static const struct among a_1[10] = -{ -{ 3, s_1_0, -1, -1, 0}, -{ 3, s_1_1, -1, -1, 0}, -{ 3, s_1_2, -1, -1, 0}, -{ 3, s_1_3, -1, -1, 0}, -{ 3, s_1_4, -1, -1, 0}, -{ 3, s_1_5, -1, -1, 0}, -{ 3, s_1_6, -1, -1, 0}, -{ 3, s_1_7, -1, -1, 0}, -{ 3, s_1_8, -1, -1, 0}, -{ 3, s_1_9, -1, -1, 0} +static const struct among a_1[10] = { +{ 3, s_1_0, 0, -1, 0}, +{ 3, s_1_1, 0, -1, 0}, +{ 3, s_1_2, 0, -1, 0}, +{ 3, s_1_3, 0, -1, 0}, +{ 3, s_1_4, 0, -1, 0}, +{ 3, s_1_5, 0, -1, 0}, +{ 3, s_1_6, 0, -1, 0}, +{ 3, s_1_7, 0, -1, 0}, +{ 3, s_1_8, 0, -1, 0}, +{ 3, s_1_9, 0, -1, 0} }; static const symbol s_2_0[3] = { 0xE0, 0xAF, 0x80 }; static const symbol s_2_1[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_2_2[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_2[3] = -{ -{ 3, s_2_0, -1, -1, 0}, -{ 3, s_2_1, -1, -1, 0}, -{ 3, s_2_2, -1, -1, 0} +static const struct among a_2[3] = { +{ 3, s_2_0, 0, -1, 0}, +{ 3, s_2_1, 0, -1, 0}, +{ 3, s_2_2, 0, -1, 0} }; static const symbol s_3_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -92,27 +129,23 @@ static const symbol s_3_4[3] = { 0xE0, 0xAF, 0x87 }; static const symbol s_3_5[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_3_6[3] = { 0xE0, 0xAE, 0xBE }; static const symbol s_3_7[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_3[8] = -{ -{ 3, s_3_0, -1, -1, 0}, -{ 3, s_3_1, -1, -1, 0}, -{ 3, s_3_2, -1, -1, 0}, -{ 3, s_3_3, -1, -1, 0}, -{ 3, s_3_4, -1, -1, 0}, -{ 3, s_3_5, -1, -1, 0}, -{ 3, s_3_6, -1, -1, 0}, -{ 3, s_3_7, -1, -1, 0} +static const struct among a_3[8] = { +{ 3, s_3_0, 0, -1, 0}, +{ 3, s_3_1, 0, -1, 0}, +{ 3, s_3_2, 0, -1, 0}, +{ 3, s_3_3, 0, -1, 0}, +{ 3, s_3_4, 0, -1, 0}, +{ 3, s_3_5, 0, -1, 0}, +{ 3, s_3_6, 0, -1, 0}, +{ 3, s_3_7, 0, -1, 0} }; static const symbol s_4_1[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_4_2[3] = { 0xE0, 0xAF, 0x8D }; - -static const struct among a_4[3] = -{ -{ 0, 0, -1, 2, 0}, -{ 3, s_4_1, 0, 1, 0}, -{ 3, s_4_2, 0, 1, 0} +static const struct among a_4[3] = { +{ 0, 0, 0, 2, 0}, +{ 3, s_4_1, -1, 1, 0}, +{ 3, s_4_2, -2, 1, 0} }; static const symbol s_5_0[6] = { 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x81 }; @@ -132,26 +165,24 @@ static const symbol s_5_13[6] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAF, 0x8D }; static const symbol s_5_14[9] = { 0xE0, 0xAE, 0xA8, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0xA4 }; static const symbol s_5_15[3] = { 0xE0, 0xAE, 0xAF }; static const symbol s_5_16[3] = { 0xE0, 0xAE, 0xB5 }; - -static const struct among a_5[17] = -{ -{ 6, s_5_0, -1, 8, 0}, -{ 9, s_5_1, -1, 7, 0}, -{ 15, s_5_2, -1, 7, 0}, -{ 12, s_5_3, -1, 3, 0}, -{ 12, s_5_4, -1, 4, 0}, -{ 6, s_5_5, -1, 9, 0}, -{ 12, s_5_6, -1, 5, 0}, -{ 12, s_5_7, -1, 6, 0}, -{ 12, s_5_8, -1, 1, 0}, -{ 6, s_5_9, -1, 1, 0}, -{ 12, s_5_10, -1, 3, 0}, -{ 6, s_5_11, -1, 2, 0}, -{ 12, s_5_12, -1, 4, 0}, -{ 6, s_5_13, -1, 1, 0}, -{ 9, s_5_14, -1, 1, 0}, -{ 3, s_5_15, -1, 1, 0}, -{ 3, s_5_16, -1, 1, 0} +static const struct among a_5[17] = { +{ 6, s_5_0, 0, 8, 0}, +{ 9, s_5_1, 0, 7, 0}, +{ 15, s_5_2, 0, 7, 0}, +{ 12, s_5_3, 0, 3, 0}, +{ 12, s_5_4, 0, 4, 0}, +{ 6, s_5_5, 0, 9, 0}, +{ 12, s_5_6, 0, 5, 0}, +{ 12, s_5_7, 0, 6, 0}, +{ 12, s_5_8, 0, 1, 0}, +{ 6, s_5_9, 0, 1, 0}, +{ 12, s_5_10, 0, 3, 0}, +{ 6, s_5_11, 0, 2, 0}, +{ 12, s_5_12, 0, 4, 0}, +{ 6, s_5_13, 0, 1, 0}, +{ 9, s_5_14, 0, 1, 0}, +{ 3, s_5_15, 0, 1, 0}, +{ 3, s_5_16, 0, 1, 0} }; static const symbol s_6_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -160,15 +191,13 @@ static const symbol s_6_2[3] = { 0xE0, 0xAE, 0x9F }; static const symbol s_6_3[3] = { 0xE0, 0xAE, 0xA4 }; static const symbol s_6_4[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_6_5[3] = { 0xE0, 0xAE, 0xB1 }; - -static const struct among a_6[6] = -{ -{ 3, s_6_0, -1, -1, 0}, -{ 3, s_6_1, -1, -1, 0}, -{ 3, s_6_2, -1, -1, 0}, -{ 3, s_6_3, -1, -1, 0}, -{ 3, s_6_4, -1, -1, 0}, -{ 3, s_6_5, -1, -1, 0} +static const struct among a_6[6] = { +{ 3, s_6_0, 0, -1, 0}, +{ 3, s_6_1, 0, -1, 0}, +{ 3, s_6_2, 0, -1, 0}, +{ 3, s_6_3, 0, -1, 0}, +{ 3, s_6_4, 0, -1, 0}, +{ 3, s_6_5, 0, -1, 0} }; static const symbol s_7_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -177,15 +206,13 @@ static const symbol s_7_2[3] = { 0xE0, 0xAE, 0x9F }; static const symbol s_7_3[3] = { 0xE0, 0xAE, 0xA4 }; static const symbol s_7_4[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_7_5[3] = { 0xE0, 0xAE, 0xB1 }; - -static const struct among a_7[6] = -{ -{ 3, s_7_0, -1, -1, 0}, -{ 3, s_7_1, -1, -1, 0}, -{ 3, s_7_2, -1, -1, 0}, -{ 3, s_7_3, -1, -1, 0}, -{ 3, s_7_4, -1, -1, 0}, -{ 3, s_7_5, -1, -1, 0} +static const struct among a_7[6] = { +{ 3, s_7_0, 0, -1, 0}, +{ 3, s_7_1, 0, -1, 0}, +{ 3, s_7_2, 0, -1, 0}, +{ 3, s_7_3, 0, -1, 0}, +{ 3, s_7_4, 0, -1, 0}, +{ 3, s_7_5, 0, -1, 0} }; static const symbol s_8_0[3] = { 0xE0, 0xAE, 0x9E }; @@ -199,20 +226,18 @@ static const symbol s_8_7[3] = { 0xE0, 0xAE, 0xB2 }; static const symbol s_8_8[3] = { 0xE0, 0xAE, 0xB3 }; static const symbol s_8_9[3] = { 0xE0, 0xAE, 0xB4 }; static const symbol s_8_10[3] = { 0xE0, 0xAE, 0xB5 }; - -static const struct among a_8[11] = -{ -{ 3, s_8_0, -1, -1, 0}, -{ 3, s_8_1, -1, -1, 0}, -{ 3, s_8_2, -1, -1, 0}, -{ 3, s_8_3, -1, -1, 0}, -{ 3, s_8_4, -1, -1, 0}, -{ 3, s_8_5, -1, -1, 0}, -{ 3, s_8_6, -1, -1, 0}, -{ 3, s_8_7, -1, -1, 0}, -{ 3, s_8_8, -1, -1, 0}, -{ 3, s_8_9, -1, -1, 0}, -{ 3, s_8_10, -1, -1, 0} +static const struct among a_8[11] = { +{ 3, s_8_0, 0, -1, 0}, +{ 3, s_8_1, 0, -1, 0}, +{ 3, s_8_2, 0, -1, 0}, +{ 3, s_8_3, 0, -1, 0}, +{ 3, s_8_4, 0, -1, 0}, +{ 3, s_8_5, 0, -1, 0}, +{ 3, s_8_6, 0, -1, 0}, +{ 3, s_8_7, 0, -1, 0}, +{ 3, s_8_8, 0, -1, 0}, +{ 3, s_8_9, 0, -1, 0}, +{ 3, s_8_10, 0, -1, 0} }; static const symbol s_9_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -224,29 +249,25 @@ static const symbol s_9_5[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_9_6[3] = { 0xE0, 0xAF, 0x8D }; static const symbol s_9_7[3] = { 0xE0, 0xAE, 0xBE }; static const symbol s_9_8[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_9[9] = -{ -{ 3, s_9_0, -1, -1, 0}, -{ 3, s_9_1, -1, -1, 0}, -{ 3, s_9_2, -1, -1, 0}, -{ 3, s_9_3, -1, -1, 0}, -{ 3, s_9_4, -1, -1, 0}, -{ 3, s_9_5, -1, -1, 0}, -{ 3, s_9_6, -1, -1, 0}, -{ 3, s_9_7, -1, -1, 0}, -{ 3, s_9_8, -1, -1, 0} +static const struct among a_9[9] = { +{ 3, s_9_0, 0, -1, 0}, +{ 3, s_9_1, 0, -1, 0}, +{ 3, s_9_2, 0, -1, 0}, +{ 3, s_9_3, 0, -1, 0}, +{ 3, s_9_4, 0, -1, 0}, +{ 3, s_9_5, 0, -1, 0}, +{ 3, s_9_6, 0, -1, 0}, +{ 3, s_9_7, 0, -1, 0}, +{ 3, s_9_8, 0, -1, 0} }; static const symbol s_10_0[3] = { 0xE0, 0xAE, 0x85 }; static const symbol s_10_1[3] = { 0xE0, 0xAE, 0x87 }; static const symbol s_10_2[3] = { 0xE0, 0xAE, 0x89 }; - -static const struct among a_10[3] = -{ -{ 3, s_10_0, -1, -1, 0}, -{ 3, s_10_1, -1, -1, 0}, -{ 3, s_10_2, -1, -1, 0} +static const struct among a_10[3] = { +{ 3, s_10_0, 0, -1, 0}, +{ 3, s_10_1, 0, -1, 0}, +{ 3, s_10_2, 0, -1, 0} }; static const symbol s_11_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -259,19 +280,17 @@ static const symbol s_11_6[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_11_7[3] = { 0xE0, 0xAE, 0xAE }; static const symbol s_11_8[3] = { 0xE0, 0xAE, 0xAF }; static const symbol s_11_9[3] = { 0xE0, 0xAE, 0xB5 }; - -static const struct among a_11[10] = -{ -{ 3, s_11_0, -1, -1, 0}, -{ 3, s_11_1, -1, -1, 0}, -{ 3, s_11_2, -1, -1, 0}, -{ 3, s_11_3, -1, -1, 0}, -{ 3, s_11_4, -1, -1, 0}, -{ 3, s_11_5, -1, -1, 0}, -{ 3, s_11_6, -1, -1, 0}, -{ 3, s_11_7, -1, -1, 0}, -{ 3, s_11_8, -1, -1, 0}, -{ 3, s_11_9, -1, -1, 0} +static const struct among a_11[10] = { +{ 3, s_11_0, 0, -1, 0}, +{ 3, s_11_1, 0, -1, 0}, +{ 3, s_11_2, 0, -1, 0}, +{ 3, s_11_3, 0, -1, 0}, +{ 3, s_11_4, 0, -1, 0}, +{ 3, s_11_5, 0, -1, 0}, +{ 3, s_11_6, 0, -1, 0}, +{ 3, s_11_7, 0, -1, 0}, +{ 3, s_11_8, 0, -1, 0}, +{ 3, s_11_9, 0, -1, 0} }; static const symbol s_12_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -280,48 +299,40 @@ static const symbol s_12_2[3] = { 0xE0, 0xAE, 0x9F }; static const symbol s_12_3[3] = { 0xE0, 0xAE, 0xA4 }; static const symbol s_12_4[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_12_5[3] = { 0xE0, 0xAE, 0xB1 }; - -static const struct among a_12[6] = -{ -{ 3, s_12_0, -1, -1, 0}, -{ 3, s_12_1, -1, -1, 0}, -{ 3, s_12_2, -1, -1, 0}, -{ 3, s_12_3, -1, -1, 0}, -{ 3, s_12_4, -1, -1, 0}, -{ 3, s_12_5, -1, -1, 0} +static const struct among a_12[6] = { +{ 3, s_12_0, 0, -1, 0}, +{ 3, s_12_1, 0, -1, 0}, +{ 3, s_12_2, 0, -1, 0}, +{ 3, s_12_3, 0, -1, 0}, +{ 3, s_12_4, 0, -1, 0}, +{ 3, s_12_5, 0, -1, 0} }; static const symbol s_13_0[9] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; static const symbol s_13_1[18] = { 0xE0, 0xAF, 0x81, 0xE0, 0xAE, 0x99, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; static const symbol s_13_2[15] = { 0xE0, 0xAE, 0x9F, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; static const symbol s_13_3[15] = { 0xE0, 0xAE, 0xB1, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; - -static const struct among a_13[4] = -{ -{ 9, s_13_0, -1, 4, 0}, -{ 18, s_13_1, 0, 1, 0}, -{ 15, s_13_2, 0, 3, 0}, -{ 15, s_13_3, 0, 2, 0} +static const struct among a_13[4] = { +{ 9, s_13_0, 0, 4, 0}, +{ 18, s_13_1, -1, 1, 0}, +{ 15, s_13_2, -2, 3, 0}, +{ 15, s_13_3, -3, 2, 0} }; static const symbol s_14_0[3] = { 0xE0, 0xAF, 0x87 }; static const symbol s_14_1[3] = { 0xE0, 0xAF, 0x8B }; static const symbol s_14_2[3] = { 0xE0, 0xAE, 0xBE }; - -static const struct among a_14[3] = -{ -{ 3, s_14_0, -1, -1, 0}, -{ 3, s_14_1, -1, -1, 0}, -{ 3, s_14_2, -1, -1, 0} +static const struct among a_14[3] = { +{ 3, s_14_0, 0, -1, 0}, +{ 3, s_14_1, 0, -1, 0}, +{ 3, s_14_2, 0, -1, 0} }; static const symbol s_15_0[6] = { 0xE0, 0xAE, 0xAA, 0xE0, 0xAE, 0xBF }; static const symbol s_15_1[6] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAE, 0xBF }; - -static const struct among a_15[2] = -{ -{ 6, s_15_0, -1, -1, 0}, -{ 6, s_15_1, -1, -1, 0} +static const struct among a_15[2] = { +{ 6, s_15_0, 0, -1, 0}, +{ 6, s_15_1, 0, -1, 0} }; static const symbol s_16_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -332,17 +343,15 @@ static const symbol s_16_4[3] = { 0xE0, 0xAF, 0x87 }; static const symbol s_16_5[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_16_6[3] = { 0xE0, 0xAE, 0xBE }; static const symbol s_16_7[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_16[8] = -{ -{ 3, s_16_0, -1, -1, 0}, -{ 3, s_16_1, -1, -1, 0}, -{ 3, s_16_2, -1, -1, 0}, -{ 3, s_16_3, -1, -1, 0}, -{ 3, s_16_4, -1, -1, 0}, -{ 3, s_16_5, -1, -1, 0}, -{ 3, s_16_6, -1, -1, 0}, -{ 3, s_16_7, -1, -1, 0} +static const struct among a_16[8] = { +{ 3, s_16_0, 0, -1, 0}, +{ 3, s_16_1, 0, -1, 0}, +{ 3, s_16_2, 0, -1, 0}, +{ 3, s_16_3, 0, -1, 0}, +{ 3, s_16_4, 0, -1, 0}, +{ 3, s_16_5, 0, -1, 0}, +{ 3, s_16_6, 0, -1, 0}, +{ 3, s_16_7, 0, -1, 0} }; static const symbol s_17_0[15] = { 0xE0, 0xAE, 0xAA, 0xE0, 0xAE, 0x9F, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0x9F, 0xE0, 0xAF, 0x81 }; @@ -371,35 +380,33 @@ static const symbol s_17_22[9] = { 0xE0, 0xAE, 0xBE, 0xE0, 0xAE, 0x95, 0xE0, 0xA static const symbol s_17_23[9] = { 0xE0, 0xAE, 0xAA, 0xE0, 0xAE, 0x9F, 0xE0, 0xAE, 0xBF }; static const symbol s_17_24[15] = { 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0xB1, 0xE0, 0xAE, 0xBF }; static const symbol s_17_25[15] = { 0xE0, 0xAE, 0xAA, 0xE0, 0xAE, 0xB1, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0xB1, 0xE0, 0xAE, 0xBF }; - -static const struct among a_17[26] = -{ -{ 15, s_17_0, -1, 3, 0}, -{ 18, s_17_1, -1, 3, 0}, -{ 9, s_17_2, -1, 3, 0}, -{ 12, s_17_3, -1, 3, 0}, -{ 18, s_17_4, -1, 3, 0}, -{ 15, s_17_5, -1, 1, 0}, -{ 9, s_17_6, -1, 1, 0}, -{ 15, s_17_7, -1, 1, 0}, -{ 12, s_17_8, -1, 1, 0}, -{ 15, s_17_9, -1, 1, 0}, -{ 12, s_17_10, -1, 1, 0}, -{ 21, s_17_11, -1, 3, 0}, -{ 12, s_17_12, -1, 3, 0}, -{ 15, s_17_13, -1, 3, 0}, -{ 6, s_17_14, -1, 1, 0}, -{ 9, s_17_15, -1, 3, 0}, -{ 18, s_17_16, 15, 3, 0}, -{ 12, s_17_17, -1, 1, 0}, -{ 12, s_17_18, -1, 1, 0}, -{ 15, s_17_19, -1, 3, 0}, -{ 9, s_17_20, -1, 2, 0}, -{ 12, s_17_21, -1, 1, 0}, -{ 9, s_17_22, -1, 1, 0}, -{ 9, s_17_23, -1, 3, 0}, -{ 15, s_17_24, -1, 1, 0}, -{ 15, s_17_25, -1, 3, 0} +static const struct among a_17[26] = { +{ 15, s_17_0, 0, 3, 0}, +{ 18, s_17_1, 0, 3, 0}, +{ 9, s_17_2, 0, 3, 0}, +{ 12, s_17_3, 0, 3, 0}, +{ 18, s_17_4, 0, 3, 0}, +{ 15, s_17_5, 0, 1, 0}, +{ 9, s_17_6, 0, 1, 0}, +{ 15, s_17_7, 0, 1, 0}, +{ 12, s_17_8, 0, 1, 0}, +{ 15, s_17_9, 0, 1, 0}, +{ 12, s_17_10, 0, 1, 0}, +{ 21, s_17_11, 0, 3, 0}, +{ 12, s_17_12, 0, 3, 0}, +{ 15, s_17_13, 0, 3, 0}, +{ 6, s_17_14, 0, 1, 0}, +{ 9, s_17_15, 0, 3, 0}, +{ 18, s_17_16, -1, 3, 0}, +{ 12, s_17_17, 0, 1, 0}, +{ 12, s_17_18, 0, 1, 0}, +{ 15, s_17_19, 0, 3, 0}, +{ 9, s_17_20, 0, 2, 0}, +{ 12, s_17_21, 0, 1, 0}, +{ 9, s_17_22, 0, 1, 0}, +{ 9, s_17_23, 0, 3, 0}, +{ 15, s_17_24, 0, 1, 0}, +{ 15, s_17_25, 0, 3, 0} }; static const symbol s_18_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -410,17 +417,15 @@ static const symbol s_18_4[3] = { 0xE0, 0xAF, 0x87 }; static const symbol s_18_5[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_18_6[3] = { 0xE0, 0xAE, 0xBE }; static const symbol s_18_7[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_18[8] = -{ -{ 3, s_18_0, -1, -1, 0}, -{ 3, s_18_1, -1, -1, 0}, -{ 3, s_18_2, -1, -1, 0}, -{ 3, s_18_3, -1, -1, 0}, -{ 3, s_18_4, -1, -1, 0}, -{ 3, s_18_5, -1, -1, 0}, -{ 3, s_18_6, -1, -1, 0}, -{ 3, s_18_7, -1, -1, 0} +static const struct among a_18[8] = { +{ 3, s_18_0, 0, -1, 0}, +{ 3, s_18_1, 0, -1, 0}, +{ 3, s_18_2, 0, -1, 0}, +{ 3, s_18_3, 0, -1, 0}, +{ 3, s_18_4, 0, -1, 0}, +{ 3, s_18_5, 0, -1, 0}, +{ 3, s_18_6, 0, -1, 0}, +{ 3, s_18_7, 0, -1, 0} }; static const symbol s_19_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -431,17 +436,15 @@ static const symbol s_19_4[3] = { 0xE0, 0xAF, 0x87 }; static const symbol s_19_5[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_19_6[3] = { 0xE0, 0xAE, 0xBE }; static const symbol s_19_7[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_19[8] = -{ -{ 3, s_19_0, -1, -1, 0}, -{ 3, s_19_1, -1, -1, 0}, -{ 3, s_19_2, -1, -1, 0}, -{ 3, s_19_3, -1, -1, 0}, -{ 3, s_19_4, -1, -1, 0}, -{ 3, s_19_5, -1, -1, 0}, -{ 3, s_19_6, -1, -1, 0}, -{ 3, s_19_7, -1, -1, 0} +static const struct among a_19[8] = { +{ 3, s_19_0, 0, -1, 0}, +{ 3, s_19_1, 0, -1, 0}, +{ 3, s_19_2, 0, -1, 0}, +{ 3, s_19_3, 0, -1, 0}, +{ 3, s_19_4, 0, -1, 0}, +{ 3, s_19_5, 0, -1, 0}, +{ 3, s_19_6, 0, -1, 0}, +{ 3, s_19_7, 0, -1, 0} }; static const symbol s_20_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -466,31 +469,29 @@ static const symbol s_20_18[9] = { 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xB2, 0xE0, 0xA static const symbol s_20_19[9] = { 0xE0, 0xAF, 0x81, 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; static const symbol s_20_20[12] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAF, 0x80, 0xE0, 0xAE, 0xB4, 0xE0, 0xAF, 0x8D }; static const symbol s_20_21[9] = { 0xE0, 0xAE, 0xB5, 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0x9F }; - -static const struct among a_20[22] = -{ -{ 3, s_20_0, -1, 7, 0}, -{ 9, s_20_1, -1, 2, 0}, -{ 9, s_20_2, -1, 2, 0}, -{ 6, s_20_3, -1, 6, 0}, -{ 21, s_20_4, 3, 2, 0}, -{ 15, s_20_5, -1, 2, 0}, -{ 9, s_20_6, -1, 2, 0}, -{ 6, s_20_7, -1, 1, 0}, -{ 9, s_20_8, -1, 1, 0}, -{ 12, s_20_9, -1, 1, 0}, -{ 9, s_20_10, -1, 3, 0}, -{ 12, s_20_11, -1, 4, 0}, -{ 12, s_20_12, -1, 1, 0}, -{ 9, s_20_13, -1, 2, 0}, -{ 6, s_20_14, -1, 5, 0}, -{ 12, s_20_15, 14, 1, 0}, -{ 12, s_20_16, 14, 2, 0}, -{ 9, s_20_17, 14, 2, 0}, -{ 9, s_20_18, 14, 2, 0}, -{ 9, s_20_19, -1, 2, 0}, -{ 12, s_20_20, -1, 1, 0}, -{ 9, s_20_21, -1, 2, 0} +static const struct among a_20[22] = { +{ 3, s_20_0, 0, 7, 0}, +{ 9, s_20_1, 0, 2, 0}, +{ 9, s_20_2, 0, 2, 0}, +{ 6, s_20_3, 0, 6, 0}, +{ 21, s_20_4, -1, 2, 0}, +{ 15, s_20_5, 0, 2, 0}, +{ 9, s_20_6, 0, 2, 0}, +{ 6, s_20_7, 0, 1, 0}, +{ 9, s_20_8, 0, 1, 0}, +{ 12, s_20_9, 0, 1, 0}, +{ 9, s_20_10, 0, 3, 0}, +{ 12, s_20_11, 0, 4, 0}, +{ 12, s_20_12, 0, 1, 0}, +{ 9, s_20_13, 0, 2, 0}, +{ 6, s_20_14, 0, 5, 0}, +{ 12, s_20_15, -1, 1, 0}, +{ 12, s_20_16, -2, 2, 0}, +{ 9, s_20_17, -3, 2, 0}, +{ 9, s_20_18, -4, 2, 0}, +{ 9, s_20_19, 0, 2, 0}, +{ 12, s_20_20, 0, 1, 0}, +{ 9, s_20_21, 0, 2, 0} }; static const symbol s_21_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -499,15 +500,13 @@ static const symbol s_21_2[3] = { 0xE0, 0xAE, 0x9F }; static const symbol s_21_3[3] = { 0xE0, 0xAE, 0xA4 }; static const symbol s_21_4[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_21_5[3] = { 0xE0, 0xAE, 0xB1 }; - -static const struct among a_21[6] = -{ -{ 3, s_21_0, -1, -1, 0}, -{ 3, s_21_1, -1, -1, 0}, -{ 3, s_21_2, -1, -1, 0}, -{ 3, s_21_3, -1, -1, 0}, -{ 3, s_21_4, -1, -1, 0}, -{ 3, s_21_5, -1, -1, 0} +static const struct among a_21[6] = { +{ 3, s_21_0, 0, -1, 0}, +{ 3, s_21_1, 0, -1, 0}, +{ 3, s_21_2, 0, -1, 0}, +{ 3, s_21_3, 0, -1, 0}, +{ 3, s_21_4, 0, -1, 0}, +{ 3, s_21_5, 0, -1, 0} }; static const symbol s_22_0[3] = { 0xE0, 0xAE, 0x95 }; @@ -516,15 +515,13 @@ static const symbol s_22_2[3] = { 0xE0, 0xAE, 0x9F }; static const symbol s_22_3[3] = { 0xE0, 0xAE, 0xA4 }; static const symbol s_22_4[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_22_5[3] = { 0xE0, 0xAE, 0xB1 }; - -static const struct among a_22[6] = -{ -{ 3, s_22_0, -1, -1, 0}, -{ 3, s_22_1, -1, -1, 0}, -{ 3, s_22_2, -1, -1, 0}, -{ 3, s_22_3, -1, -1, 0}, -{ 3, s_22_4, -1, -1, 0}, -{ 3, s_22_5, -1, -1, 0} +static const struct among a_22[6] = { +{ 3, s_22_0, 0, -1, 0}, +{ 3, s_22_1, 0, -1, 0}, +{ 3, s_22_2, 0, -1, 0}, +{ 3, s_22_3, 0, -1, 0}, +{ 3, s_22_4, 0, -1, 0}, +{ 3, s_22_5, 0, -1, 0} }; static const symbol s_23_0[3] = { 0xE0, 0xAE, 0x85 }; @@ -539,21 +536,19 @@ static const symbol s_23_8[3] = { 0xE0, 0xAE, 0x90 }; static const symbol s_23_9[3] = { 0xE0, 0xAE, 0x92 }; static const symbol s_23_10[3] = { 0xE0, 0xAE, 0x93 }; static const symbol s_23_11[3] = { 0xE0, 0xAE, 0x94 }; - -static const struct among a_23[12] = -{ -{ 3, s_23_0, -1, -1, 0}, -{ 3, s_23_1, -1, -1, 0}, -{ 3, s_23_2, -1, -1, 0}, -{ 3, s_23_3, -1, -1, 0}, -{ 3, s_23_4, -1, -1, 0}, -{ 3, s_23_5, -1, -1, 0}, -{ 3, s_23_6, -1, -1, 0}, -{ 3, s_23_7, -1, -1, 0}, -{ 3, s_23_8, -1, -1, 0}, -{ 3, s_23_9, -1, -1, 0}, -{ 3, s_23_10, -1, -1, 0}, -{ 3, s_23_11, -1, -1, 0} +static const struct among a_23[12] = { +{ 3, s_23_0, 0, -1, 0}, +{ 3, s_23_1, 0, -1, 0}, +{ 3, s_23_2, 0, -1, 0}, +{ 3, s_23_3, 0, -1, 0}, +{ 3, s_23_4, 0, -1, 0}, +{ 3, s_23_5, 0, -1, 0}, +{ 3, s_23_6, 0, -1, 0}, +{ 3, s_23_7, 0, -1, 0}, +{ 3, s_23_8, 0, -1, 0}, +{ 3, s_23_9, 0, -1, 0}, +{ 3, s_23_10, 0, -1, 0}, +{ 3, s_23_11, 0, -1, 0} }; static const symbol s_24_0[3] = { 0xE0, 0xAF, 0x80 }; @@ -564,17 +559,15 @@ static const symbol s_24_4[3] = { 0xE0, 0xAF, 0x87 }; static const symbol s_24_5[3] = { 0xE0, 0xAF, 0x88 }; static const symbol s_24_6[3] = { 0xE0, 0xAE, 0xBE }; static const symbol s_24_7[3] = { 0xE0, 0xAE, 0xBF }; - -static const struct among a_24[8] = -{ -{ 3, s_24_0, -1, -1, 0}, -{ 3, s_24_1, -1, -1, 0}, -{ 3, s_24_2, -1, -1, 0}, -{ 3, s_24_3, -1, -1, 0}, -{ 3, s_24_4, -1, -1, 0}, -{ 3, s_24_5, -1, -1, 0}, -{ 3, s_24_6, -1, -1, 0}, -{ 3, s_24_7, -1, -1, 0} +static const struct among a_24[8] = { +{ 3, s_24_0, 0, -1, 0}, +{ 3, s_24_1, 0, -1, 0}, +{ 3, s_24_2, 0, -1, 0}, +{ 3, s_24_3, 0, -1, 0}, +{ 3, s_24_4, 0, -1, 0}, +{ 3, s_24_5, 0, -1, 0}, +{ 3, s_24_6, 0, -1, 0}, +{ 3, s_24_7, 0, -1, 0} }; static const symbol s_25_0[6] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAF, 0x81 }; @@ -623,55 +616,53 @@ static const symbol s_25_42[3] = { 0xE0, 0xAE, 0xA9 }; static const symbol s_25_43[3] = { 0xE0, 0xAE, 0xAA }; static const symbol s_25_44[3] = { 0xE0, 0xAE, 0xAF }; static const symbol s_25_45[3] = { 0xE0, 0xAE, 0xBE }; - -static const struct among a_25[46] = -{ -{ 6, s_25_0, -1, 6, 0}, -{ 9, s_25_1, -1, 1, 0}, -{ 6, s_25_2, -1, 3, 0}, -{ 15, s_25_3, -1, 1, 0}, -{ 6, s_25_4, -1, 1, 0}, -{ 6, s_25_5, -1, 1, 0}, -{ 12, s_25_6, -1, 1, 0}, -{ 9, s_25_7, -1, 5, 0}, -{ 9, s_25_8, -1, 1, 0}, -{ 9, s_25_9, -1, 1, 0}, -{ 9, s_25_10, -1, 2, 0}, -{ 9, s_25_11, -1, 4, 0}, -{ 12, s_25_12, 11, 1, 0}, -{ 12, s_25_13, -1, 1, 0}, -{ 12, s_25_14, -1, 1, 0}, -{ 12, s_25_15, -1, 5, 0}, -{ 12, s_25_16, -1, 1, 0}, -{ 12, s_25_17, -1, 1, 0}, -{ 9, s_25_18, -1, 5, 0}, -{ 9, s_25_19, -1, 5, 0}, -{ 9, s_25_20, -1, 5, 0}, -{ 9, s_25_21, -1, 1, 0}, -{ 9, s_25_22, -1, 1, 0}, -{ 9, s_25_23, -1, 5, 0}, -{ 9, s_25_24, -1, 5, 0}, -{ 9, s_25_25, -1, 5, 0}, -{ 9, s_25_26, -1, 1, 0}, -{ 9, s_25_27, -1, 1, 0}, -{ 12, s_25_28, -1, 5, 0}, -{ 9, s_25_29, -1, 1, 0}, -{ 9, s_25_30, -1, 5, 0}, -{ 12, s_25_31, 30, 1, 0}, -{ 12, s_25_32, 30, 1, 0}, -{ 24, s_25_33, -1, 1, 0}, -{ 12, s_25_34, -1, 5, 0}, -{ 9, s_25_35, -1, 1, 0}, -{ 9, s_25_36, -1, 1, 0}, -{ 9, s_25_37, -1, 1, 0}, -{ 9, s_25_38, -1, 5, 0}, -{ 12, s_25_39, 38, 1, 0}, -{ 3, s_25_40, -1, 1, 0}, -{ 3, s_25_41, -1, 1, 0}, -{ 3, s_25_42, -1, 1, 0}, -{ 3, s_25_43, -1, 1, 0}, -{ 3, s_25_44, -1, 1, 0}, -{ 3, s_25_45, -1, 5, 0} +static const struct among a_25[46] = { +{ 6, s_25_0, 0, 6, 0}, +{ 9, s_25_1, 0, 1, 0}, +{ 6, s_25_2, 0, 3, 0}, +{ 15, s_25_3, 0, 1, 0}, +{ 6, s_25_4, 0, 1, 0}, +{ 6, s_25_5, 0, 1, 0}, +{ 12, s_25_6, 0, 1, 0}, +{ 9, s_25_7, 0, 5, 0}, +{ 9, s_25_8, 0, 1, 0}, +{ 9, s_25_9, 0, 1, 0}, +{ 9, s_25_10, 0, 2, 0}, +{ 9, s_25_11, 0, 4, 0}, +{ 12, s_25_12, -1, 1, 0}, +{ 12, s_25_13, 0, 1, 0}, +{ 12, s_25_14, 0, 1, 0}, +{ 12, s_25_15, 0, 5, 0}, +{ 12, s_25_16, 0, 1, 0}, +{ 12, s_25_17, 0, 1, 0}, +{ 9, s_25_18, 0, 5, 0}, +{ 9, s_25_19, 0, 5, 0}, +{ 9, s_25_20, 0, 5, 0}, +{ 9, s_25_21, 0, 1, 0}, +{ 9, s_25_22, 0, 1, 0}, +{ 9, s_25_23, 0, 5, 0}, +{ 9, s_25_24, 0, 5, 0}, +{ 9, s_25_25, 0, 5, 0}, +{ 9, s_25_26, 0, 1, 0}, +{ 9, s_25_27, 0, 1, 0}, +{ 12, s_25_28, 0, 5, 0}, +{ 9, s_25_29, 0, 1, 0}, +{ 9, s_25_30, 0, 5, 0}, +{ 12, s_25_31, -1, 1, 0}, +{ 12, s_25_32, -2, 1, 0}, +{ 24, s_25_33, 0, 1, 0}, +{ 12, s_25_34, 0, 5, 0}, +{ 9, s_25_35, 0, 1, 0}, +{ 9, s_25_36, 0, 1, 0}, +{ 9, s_25_37, 0, 1, 0}, +{ 9, s_25_38, 0, 5, 0}, +{ 12, s_25_39, -1, 1, 0}, +{ 3, s_25_40, 0, 1, 0}, +{ 3, s_25_41, 0, 1, 0}, +{ 3, s_25_42, 0, 1, 0}, +{ 3, s_25_43, 0, 1, 0}, +{ 3, s_25_44, 0, 1, 0}, +{ 3, s_25_45, 0, 5, 0} }; static const symbol s_26_0[18] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0xB1, 0xE0, 0xAF, 0x8D }; @@ -680,59 +671,15 @@ static const symbol s_26_2[12] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xBF, 0xE0, 0xA static const symbol s_26_3[15] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0xB1 }; static const symbol s_26_4[18] = { 0xE0, 0xAE, 0xBE, 0xE0, 0xAE, 0xA8, 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x8D, 0xE0, 0xAE, 0xB1 }; static const symbol s_26_5[9] = { 0xE0, 0xAE, 0x95, 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xB1 }; - -static const struct among a_26[6] = -{ -{ 18, s_26_0, -1, -1, 0}, -{ 21, s_26_1, -1, -1, 0}, -{ 12, s_26_2, -1, -1, 0}, -{ 15, s_26_3, -1, -1, 0}, -{ 18, s_26_4, -1, -1, 0}, -{ 9, s_26_5, -1, -1, 0} +static const struct among a_26[6] = { +{ 18, s_26_0, 0, -1, 0}, +{ 21, s_26_1, 0, -1, 0}, +{ 12, s_26_2, 0, -1, 0}, +{ 15, s_26_3, 0, -1, 0}, +{ 18, s_26_4, 0, -1, 0}, +{ 9, s_26_5, 0, -1, 0} }; -static const symbol s_0[] = { 0xE0, 0xAE, 0x93 }; -static const symbol s_1[] = { 0xE0, 0xAE, 0x92 }; -static const symbol s_2[] = { 0xE0, 0xAE, 0x89 }; -static const symbol s_3[] = { 0xE0, 0xAE, 0x8A }; -static const symbol s_4[] = { 0xE0, 0xAE, 0x8E }; -static const symbol s_5[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_6[] = { 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; -static const symbol s_7[] = { 0xE0, 0xAE, 0xB2, 0xE0, 0xAF, 0x8D }; -static const symbol s_8[] = { 0xE0, 0xAE, 0x9F, 0xE0, 0xAF, 0x81 }; -static const symbol s_9[] = { 0xE0, 0xAF, 0x88 }; -static const symbol s_10[] = { 0xE0, 0xAE, 0xAE, 0xE0, 0xAF, 0x8D }; -static const symbol s_11[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_12[] = { 0xE0, 0xAE, 0xAE, 0xE0, 0xAF, 0x8D }; -static const symbol s_13[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_14[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_15[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_16[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_17[] = { 0xE0, 0xAF, 0x81, 0xE0, 0xAE, 0x99, 0xE0, 0xAF, 0x8D }; -static const symbol s_18[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_19[] = { 0xE0, 0xAE, 0xB2, 0xE0, 0xAF, 0x8D }; -static const symbol s_20[] = { 0xE0, 0xAE, 0xB3, 0xE0, 0xAF, 0x8D }; -static const symbol s_21[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_22[] = { 0xE0, 0xAF, 0x81, 0xE0, 0xAE, 0xAE, 0xE0, 0xAF, 0x8D }; -static const symbol s_23[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_24[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_25[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_26[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_27[] = { 0xE0, 0xAE, 0xAE }; -static const symbol s_28[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_29[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_30[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_31[] = { 0xE0, 0xAE, 0xBF }; -static const symbol s_32[] = { 0xE0, 0xAF, 0x88 }; -static const symbol s_33[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_34[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_35[] = { 0xE0, 0xAE, 0xBF, 0xE0, 0xAE, 0xA9, 0xE0, 0xAF, 0x8D }; -static const symbol s_36[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_37[] = { 0xE0, 0xAE, 0x9A }; -static const symbol s_38[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_39[] = { 0xE0, 0xAF, 0x8D }; -static const symbol s_40[] = { 0xE0, 0xAF, 0x8D }; - static int r_has_min_length(struct SN_env * z) { return len_utf8(z->p) > 4; } @@ -741,27 +688,31 @@ static int r_fix_va_start(struct SN_env * z) { int among_var; z->bra = z->c; if (z->c + 5 >= z->l || z->p[z->c + 5] >> 5 != 4 || !((3078 >> (z->p[z->c + 5] & 0x1f)) & 1)) return 0; - among_var = find_among(z, a_0, 4); + among_var = find_among(z, a_0, 4, 0); if (!among_var) return 0; z->ket = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 3, s_0); + { + int ret = slice_from_s(z, 3, s_0); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_1); + { + int ret = slice_from_s(z, 3, s_1); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 3, s_2); + { + int ret = slice_from_s(z, 3, s_2); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 3, s_3); + { + int ret = slice_from_s(z, 3, s_3); if (ret < 0) return ret; } break; @@ -770,19 +721,21 @@ static int r_fix_va_start(struct SN_env * z) { } static int r_fix_endings(struct SN_env * z) { - { int c1 = z->c; - while(1) { - int c2 = z->c; - { int ret = r_fix_ending(z); + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + { + int ret = r_fix_ending(z); if (ret == 0) goto lab1; if (ret < 0) return ret; } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } return 1; } @@ -790,17 +743,20 @@ static int r_fix_endings(struct SN_env * z) { static int r_remove_question_prefixes(struct SN_env * z) { z->bra = z->c; if (!(eq_s(z, 3, s_4))) return 0; - if (!find_among(z, a_1, 10)) return 0; + if (!find_among(z, a_1, 10, 0)) return 0; if (!(eq_s(z, 3, s_5))) return 0; z->ket = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int c1 = z->c; - { int ret = r_fix_va_start(z); + { + int v_1 = z->c; + { + int ret = r_fix_va_start(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } return 1; } @@ -809,129 +765,145 @@ static int r_fix_ending(struct SN_env * z) { int among_var; if (len_utf8(z->p) <= 3) return 0; z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; + do { + int v_1 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_5, 17); - if (!among_var) goto lab1; + among_var = find_among_b(z, a_5, 17, 0); + if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m_test2 = z->l - z->c; - if (!find_among_b(z, a_2, 3)) goto lab1; - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + if (!find_among_b(z, a_2, 3, 0)) goto lab0; + z->c = z->l - v_2; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 6, s_6); + { + int ret = slice_from_s(z, 6, s_6); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 6, s_7); + { + int ret = slice_from_s(z, 6, s_7); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 6, s_8); + { + int ret = slice_from_s(z, 6, s_8); if (ret < 0) return ret; } break; case 6: - if (!(z->I[0])) goto lab1; - { int m3 = z->l - z->c; (void)m3; - if (!(eq_s_b(z, 3, s_9))) goto lab2; - goto lab1; - lab2: - z->c = z->l - m3; + if (!((SN_local *)z)->b_found_vetrumai_urupu) goto lab0; + { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 3, s_9))) goto lab1; + goto lab0; + lab1: + z->c = z->l - v_3; } - { int ret = slice_from_s(z, 6, s_10); + { + int ret = slice_from_s(z, 6, s_10); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_11); + { + int ret = slice_from_s(z, 3, s_11); if (ret < 0) return ret; } break; case 8: - { int m4 = z->l - z->c; (void)m4; - if (!find_among_b(z, a_3, 8)) goto lab3; - goto lab1; - lab3: - z->c = z->l - m4; + { + int v_4 = z->l - z->c; + if (!find_among_b(z, a_3, 8, 0)) goto lab2; + goto lab0; + lab2: + z->c = z->l - v_4; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 9: if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 136 && z->p[z->c - 1] != 141)) among_var = 2; else - among_var = find_among_b(z, a_4, 3); + among_var = find_among_b(z, a_4, 3, 0); switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 6, s_12); + { + int ret = slice_from_s(z, 6, s_12); if (ret < 0) return ret; } break; } break; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; if (!(eq_s_b(z, 3, s_13))) return 0; - { int m5 = z->l - z->c; (void)m5; - if (!find_among_b(z, a_6, 6)) goto lab5; - { int m6 = z->l - z->c; (void)m6; - if (!(eq_s_b(z, 3, s_14))) { z->c = z->l - m6; goto lab6; } - if (!find_among_b(z, a_7, 6)) { z->c = z->l - m6; goto lab6; } - lab6: + do { + int v_5 = z->l - z->c; + if (!find_among_b(z, a_6, 6, 0)) goto lab3; + { + int v_6 = z->l - z->c; + if (!(eq_s_b(z, 3, s_14))) { z->c = z->l - v_6; goto lab4; } + if (!find_among_b(z, a_7, 6, 0)) { z->c = z->l - v_6; goto lab4; } + lab4: ; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab4; - lab5: - z->c = z->l - m5; - if (!find_among_b(z, a_8, 11)) goto lab7; + break; + lab3: + z->c = z->l - v_5; + if (!find_among_b(z, a_8, 11, 0)) goto lab5; z->bra = z->c; - if (!(eq_s_b(z, 3, s_15))) goto lab7; - { int ret = slice_del(z); + if (!(eq_s_b(z, 3, s_15))) goto lab5; + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab4; - lab7: - z->c = z->l - m5; - { int m_test7 = z->l - z->c; - if (!find_among_b(z, a_9, 9)) return 0; - z->c = z->l - m_test7; + break; + lab5: + z->c = z->l - v_5; + { + int v_7 = z->l - z->c; + if (!find_among_b(z, a_9, 9, 0)) return 0; + z->c = z->l - v_7; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab4: - ; - } -lab0: + } while (0); + } while (0); z->c = z->lb; return 1; } @@ -939,18 +911,21 @@ static int r_fix_ending(struct SN_env * z) { static int r_remove_pronoun_prefixes(struct SN_env * z) { z->bra = z->c; if (z->c + 2 >= z->l || z->p[z->c + 2] >> 5 != 4 || !((672 >> (z->p[z->c + 2] & 0x1f)) & 1)) return 0; - if (!find_among(z, a_10, 3)) return 0; - if (!find_among(z, a_11, 10)) return 0; + if (!find_among(z, a_10, 3, 0)) return 0; + if (!find_among(z, a_11, 10, 0)) return 0; if (!(eq_s(z, 3, s_16))) return 0; z->ket = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int c1 = z->c; - { int ret = r_fix_va_start(z); + { + int v_1 = z->c; + { + int ret = r_fix_va_start(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } return 1; } @@ -958,40 +933,44 @@ static int r_remove_pronoun_prefixes(struct SN_env * z) { static int r_remove_plural_suffix(struct SN_env * z) { int among_var; z->lb = z->c; z->c = z->l; - z->ket = z->c; if (z->c - 8 <= z->lb || z->p[z->c - 1] != 141) return 0; - among_var = find_among_b(z, a_13, 4); + among_var = find_among_b(z, a_13, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int m1 = z->l - z->c; (void)m1; - if (!find_among_b(z, a_12, 6)) goto lab1; - { int ret = slice_from_s(z, 9, s_17); + do { + int v_1 = z->l - z->c; + if (!find_among_b(z, a_12, 6, 0)) goto lab0; + { + int ret = slice_from_s(z, 9, s_17); if (ret < 0) return ret; } - goto lab0; - lab1: - z->c = z->l - m1; - { int ret = slice_from_s(z, 3, s_18); + break; + lab0: + z->c = z->l - v_1; + { + int ret = slice_from_s(z, 3, s_18); if (ret < 0) return ret; } - } - lab0: + } while (0); break; case 2: - { int ret = slice_from_s(z, 6, s_19); + { + int ret = slice_from_s(z, 6, s_19); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 6, s_20); + { + int ret = slice_from_s(z, 6, s_20); if (ret < 0) return ret; } break; case 4: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; @@ -1001,40 +980,43 @@ static int r_remove_plural_suffix(struct SN_env * z) { } static int r_remove_question_suffixes(struct SN_env * z) { - { int ret = r_has_min_length(z); + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - if (!find_among_b(z, a_14, 3)) goto lab0; + if (!find_among_b(z, a_14, 3, 0)) goto lab0; z->bra = z->c; - { int ret = slice_from_s(z, 3, s_21); + { + int ret = slice_from_s(z, 3, s_21); if (ret < 0) return ret; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } z->c = z->lb; - - { int ret = r_fix_endings(z); + { + int ret = r_fix_endings(z); if (ret < 0) return ret; } return 1; } static int r_remove_command_suffixes(struct SN_env * z) { - { int ret = r_has_min_length(z); + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - z->ket = z->c; if (z->c - 5 <= z->lb || z->p[z->c - 1] != 191) return 0; - if (!find_among_b(z, a_15, 2)) return 0; + if (!find_among_b(z, a_15, 2, 0)) return 0; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->c = z->lb; @@ -1042,64 +1024,71 @@ static int r_remove_command_suffixes(struct SN_env * z) { } static int r_remove_um(struct SN_env * z) { - { int ret = r_has_min_length(z); + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - z->ket = z->c; if (!(eq_s_b(z, 9, s_22))) return 0; z->bra = z->c; - { int ret = slice_from_s(z, 3, s_23); + { + int ret = slice_from_s(z, 3, s_23); if (ret < 0) return ret; } z->c = z->lb; - { int c1 = z->c; - { int ret = r_fix_ending(z); + { + int v_1 = z->c; + { + int ret = r_fix_ending(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } return 1; } static int r_remove_common_word_endings(struct SN_env * z) { int among_var; - { int ret = r_has_min_length(z); + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - z->ket = z->c; - among_var = find_among_b(z, a_17, 26); + among_var = find_among_b(z, a_17, 26, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 3, s_24); + { + int ret = slice_from_s(z, 3, s_24); if (ret < 0) return ret; } break; case 2: - { int m1 = z->l - z->c; (void)m1; - if (!find_among_b(z, a_16, 8)) goto lab0; + { + int v_1 = z->l - z->c; + if (!find_among_b(z, a_16, 8, 0)) goto lab0; return 0; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int ret = slice_from_s(z, 3, s_25); + { + int ret = slice_from_s(z, 3, s_25); if (ret < 0) return ret; } break; case 3: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } z->c = z->lb; - - { int ret = r_fix_endings(z); + { + int ret = r_fix_endings(z); if (ret < 0) return ret; } return 1; @@ -1107,141 +1096,155 @@ static int r_remove_common_word_endings(struct SN_env * z) { static int r_remove_vetrumai_urupukal(struct SN_env * z) { int among_var; - z->I[0] = 0; - { int ret = r_has_min_length(z); + ((SN_local *)z)->b_found_vetrumai_urupu = 0; + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int m_test2 = z->l - z->c; + do { + int v_1 = z->l - z->c; + { + int v_2 = z->l - z->c; z->ket = z->c; - if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((-2147475197 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab1; - among_var = find_among_b(z, a_20, 22); - if (!among_var) goto lab1; + if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((-2147475197 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab0; + among_var = find_among_b(z, a_20, 22, 0); + if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 3, s_26); + { + int ret = slice_from_s(z, 3, s_26); if (ret < 0) return ret; } break; case 3: - { int m3 = z->l - z->c; (void)m3; - if (!(eq_s_b(z, 3, s_27))) goto lab2; - goto lab1; - lab2: - z->c = z->l - m3; + { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 3, s_27))) goto lab1; + goto lab0; + lab1: + z->c = z->l - v_3; } - { int ret = slice_from_s(z, 3, s_28); + { + int ret = slice_from_s(z, 3, s_28); if (ret < 0) return ret; } break; case 4: - if (len_utf8(z->p) < 7) goto lab1; - { int ret = slice_from_s(z, 3, s_29); + if (len_utf8(z->p) < 7) goto lab0; + { + int ret = slice_from_s(z, 3, s_29); if (ret < 0) return ret; } break; case 5: - { int m4 = z->l - z->c; (void)m4; - if (!find_among_b(z, a_18, 8)) goto lab3; - goto lab1; - lab3: - z->c = z->l - m4; + { + int v_4 = z->l - z->c; + if (!find_among_b(z, a_18, 8, 0)) goto lab2; + goto lab0; + lab2: + z->c = z->l - v_4; } - { int ret = slice_from_s(z, 3, s_30); + { + int ret = slice_from_s(z, 3, s_30); if (ret < 0) return ret; } break; case 6: - { int m5 = z->l - z->c; (void)m5; - if (!find_among_b(z, a_19, 8)) goto lab4; - goto lab1; - lab4: - z->c = z->l - m5; + { + int v_5 = z->l - z->c; + if (!find_among_b(z, a_19, 8, 0)) goto lab3; + goto lab0; + lab3: + z->c = z->l - v_5; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 3, s_31); + { + int ret = slice_from_s(z, 3, s_31); if (ret < 0) return ret; } break; } - z->c = z->l - m_test2; + z->c = z->l - v_2; } - goto lab0; - lab1: - z->c = z->l - m1; - { int m_test6 = z->l - z->c; + break; + lab0: + z->c = z->l - v_1; + { + int v_6 = z->l - z->c; z->ket = z->c; if (!(eq_s_b(z, 3, s_32))) return 0; - { int m7 = z->l - z->c; (void)m7; - { int m8 = z->l - z->c; (void)m8; - if (!find_among_b(z, a_21, 6)) goto lab7; - goto lab6; - lab7: - z->c = z->l - m8; + do { + int v_7 = z->l - z->c; + { + int v_8 = z->l - z->c; + if (!find_among_b(z, a_21, 6, 0)) goto lab5; + goto lab4; + lab5: + z->c = z->l - v_8; } - goto lab5; - lab6: - z->c = z->l - m7; - { int m_test9 = z->l - z->c; - if (!find_among_b(z, a_22, 6)) return 0; + break; + lab4: + z->c = z->l - v_7; + { + int v_9 = z->l - z->c; + if (!find_among_b(z, a_22, 6, 0)) return 0; if (!(eq_s_b(z, 3, s_33))) return 0; - z->c = z->l - m_test9; + z->c = z->l - v_9; } - } - lab5: + } while (0); z->bra = z->c; - { int ret = slice_from_s(z, 3, s_34); + { + int ret = slice_from_s(z, 3, s_34); if (ret < 0) return ret; } - z->c = z->l - m_test6; + z->c = z->l - v_6; } - } -lab0: - z->I[0] = 1; - { int m10 = z->l - z->c; (void)m10; + } while (0); + ((SN_local *)z)->b_found_vetrumai_urupu = 1; + { + int v_10 = z->l - z->c; z->ket = z->c; - if (!(eq_s_b(z, 9, s_35))) goto lab8; + if (!(eq_s_b(z, 9, s_35))) goto lab6; z->bra = z->c; - { int ret = slice_from_s(z, 3, s_36); + { + int ret = slice_from_s(z, 3, s_36); if (ret < 0) return ret; } - lab8: - z->c = z->l - m10; + lab6: + z->c = z->l - v_10; } z->c = z->lb; - - { int ret = r_fix_endings(z); + { + int ret = r_fix_endings(z); if (ret < 0) return ret; } return 1; } static int r_remove_tense_suffixes(struct SN_env * z) { - z->I[1] = 1; - while(1) { - int c1 = z->c; - if (!(z->I[1])) goto lab0; - { int c2 = z->c; - { int ret = r_remove_tense_suffix(z); - if (ret < 0) return ret; - } - z->c = c2; + while (1) { + int v_1 = z->c; + { + int ret = r_remove_tense_suffix(z); + if (ret == 0) goto lab0; + if (ret < 0) return ret; } continue; lab0: - z->c = c1; + z->c = v_1; break; } return 1; @@ -1249,168 +1252,212 @@ static int r_remove_tense_suffixes(struct SN_env * z) { static int r_remove_tense_suffix(struct SN_env * z) { int among_var; - z->I[1] = 0; - { int ret = r_has_min_length(z); + int b_found_a_match; + b_found_a_match = 0; + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int m_test2 = z->l - z->c; + { + int v_1 = z->l - z->c; + { + int v_2 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_25, 46); + among_var = find_among_b(z, a_25, 46, 0); if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int m3 = z->l - z->c; (void)m3; + { + int v_3 = z->l - z->c; if (z->c - 2 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((1951712 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab1; - if (!find_among_b(z, a_23, 12)) goto lab1; + if (!find_among_b(z, a_23, 12, 0)) goto lab1; goto lab0; lab1: - z->c = z->l - m3; + z->c = z->l - v_3; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 3: - { int m4 = z->l - z->c; (void)m4; - if (!find_among_b(z, a_24, 8)) goto lab2; + { + int v_4 = z->l - z->c; + if (!find_among_b(z, a_24, 8, 0)) goto lab2; goto lab0; lab2: - z->c = z->l - m4; + z->c = z->l - v_4; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 4: - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; if (!(eq_s_b(z, 3, s_37))) goto lab3; goto lab0; lab3: - z->c = z->l - m5; + z->c = z->l - v_5; } - { int ret = slice_from_s(z, 3, s_38); + { + int ret = slice_from_s(z, 3, s_38); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 3, s_39); + { + int ret = slice_from_s(z, 3, s_39); if (ret < 0) return ret; } break; case 6: - { int m_test6 = z->l - z->c; + { + int v_6 = z->l - z->c; if (!(eq_s_b(z, 3, s_40))) goto lab0; - z->c = z->l - m_test6; + z->c = z->l - v_6; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - z->I[1] = 1; - z->c = z->l - m_test2; + b_found_a_match = 1; + z->c = z->l - v_2; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m7 = z->l - z->c; (void)m7; + { + int v_7 = z->l - z->c; z->ket = z->c; if (z->c - 8 <= z->lb || (z->p[z->c - 1] != 141 && z->p[z->c - 1] != 177)) goto lab4; - if (!find_among_b(z, a_26, 6)) goto lab4; + if (!find_among_b(z, a_26, 6, 0)) goto lab4; z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->I[1] = 1; + b_found_a_match = 1; lab4: - z->c = z->l - m7; + z->c = z->l - v_7; } z->c = z->lb; - - { int ret = r_fix_endings(z); + { + int ret = r_fix_endings(z); if (ret < 0) return ret; } - return 1; + return b_found_a_match; } extern int tamil_UTF_8_stem(struct SN_env * z) { - z->I[0] = 0; - { int c1 = z->c; - { int ret = r_fix_ending(z); + ((SN_local *)z)->b_found_vetrumai_urupu = 0; + { + int v_1 = z->c; + { + int ret = r_fix_ending(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } - { int ret = r_has_min_length(z); + { + int ret = r_has_min_length(z); if (ret <= 0) return ret; } - { int c2 = z->c; - { int ret = r_remove_question_prefixes(z); + { + int v_2 = z->c; + { + int ret = r_remove_question_prefixes(z); if (ret < 0) return ret; } - z->c = c2; + z->c = v_2; } - { int c3 = z->c; - { int ret = r_remove_pronoun_prefixes(z); + { + int v_3 = z->c; + { + int ret = r_remove_pronoun_prefixes(z); if (ret < 0) return ret; } - z->c = c3; + z->c = v_3; } - { int c4 = z->c; - { int ret = r_remove_question_suffixes(z); + { + int v_4 = z->c; + { + int ret = r_remove_question_suffixes(z); if (ret < 0) return ret; } - z->c = c4; + z->c = v_4; } - { int c5 = z->c; - { int ret = r_remove_um(z); + { + int v_5 = z->c; + { + int ret = r_remove_um(z); if (ret < 0) return ret; } - z->c = c5; + z->c = v_5; } - { int c6 = z->c; - { int ret = r_remove_common_word_endings(z); + { + int v_6 = z->c; + { + int ret = r_remove_common_word_endings(z); if (ret < 0) return ret; } - z->c = c6; + z->c = v_6; } - { int c7 = z->c; - { int ret = r_remove_vetrumai_urupukal(z); + { + int v_7 = z->c; + { + int ret = r_remove_vetrumai_urupukal(z); if (ret < 0) return ret; } - z->c = c7; + z->c = v_7; } - { int c8 = z->c; - { int ret = r_remove_plural_suffix(z); + { + int v_8 = z->c; + { + int ret = r_remove_plural_suffix(z); if (ret < 0) return ret; } - z->c = c8; + z->c = v_8; } - { int c9 = z->c; - { int ret = r_remove_command_suffixes(z); + { + int v_9 = z->c; + { + int ret = r_remove_command_suffixes(z); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; } - { int c10 = z->c; - { int ret = r_remove_tense_suffixes(z); + { + int v_10 = z->c; + { + int ret = r_remove_tense_suffixes(z); if (ret < 0) return ret; } - z->c = c10; + z->c = v_10; } return 1; } -extern struct SN_env * tamil_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * tamil_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_found_vetrumai_urupu = 0; + } + return z; +} -extern void tamil_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void tamil_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_turkish.c b/src/backend/snowball/libstemmer/stem_UTF_8_turkish.c index efb1b30604b17..dde3a52dbf89b 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_turkish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_turkish.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from turkish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_turkish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + unsigned char b_continue_stemming_noun_suffixes; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,6 +20,7 @@ extern int turkish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_stem_suffix_chain_before_ki(struct SN_env * z); static int r_stem_noun_suffixes(struct SN_env * z); static int r_stem_nominal_verb_suffixes(struct SN_env * z); @@ -50,18 +62,26 @@ static int r_mark_cAsInA(struct SN_env * z); static int r_is_reserved_word(struct SN_env * z); static int r_check_vowel_harmony(struct SN_env * z); static int r_append_U_to_stems_ending_with_d_or_g(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * turkish_UTF_8_create_env(void); -extern void turkish_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xC4, 0xB1 }; +static const symbol s_1[] = { 0xC3, 0xB6 }; +static const symbol s_2[] = { 0xC3, 0xBC }; +static const symbol s_3[] = { 'k', 'i' }; +static const symbol s_4[] = { 'k', 'e', 'n' }; +static const symbol s_5[] = { 'p' }; +static const symbol s_6[] = { 0xC3, 0xA7 }; +static const symbol s_7[] = { 't' }; +static const symbol s_8[] = { 'k' }; +static const symbol s_9[] = { 0xC4, 0xB1 }; +static const symbol s_10[] = { 0xC4, 0xB1 }; +static const symbol s_11[] = { 'i' }; +static const symbol s_12[] = { 'u' }; +static const symbol s_13[] = { 0xC3, 0xB6 }; +static const symbol s_14[] = { 0xC3, 0xBC }; +static const symbol s_15[] = { 0xC3, 0xBC }; +static const symbol s_16[] = { 'a', 'd' }; +static const symbol s_17[] = { 's', 'o', 'y' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[1] = { 'm' }; static const symbol s_0_1[1] = { 'n' }; static const symbol s_0_2[3] = { 'm', 'i', 'z' }; @@ -72,208 +92,165 @@ static const symbol s_0_6[4] = { 'm', 0xC4, 0xB1, 'z' }; static const symbol s_0_7[4] = { 'n', 0xC4, 0xB1, 'z' }; static const symbol s_0_8[4] = { 'm', 0xC3, 0xBC, 'z' }; static const symbol s_0_9[4] = { 'n', 0xC3, 0xBC, 'z' }; - -static const struct among a_0[10] = -{ -{ 1, s_0_0, -1, -1, 0}, -{ 1, s_0_1, -1, -1, 0}, -{ 3, s_0_2, -1, -1, 0}, -{ 3, s_0_3, -1, -1, 0}, -{ 3, s_0_4, -1, -1, 0}, -{ 3, s_0_5, -1, -1, 0}, -{ 4, s_0_6, -1, -1, 0}, -{ 4, s_0_7, -1, -1, 0}, -{ 4, s_0_8, -1, -1, 0}, -{ 4, s_0_9, -1, -1, 0} +static const struct among a_0[10] = { +{ 1, s_0_0, 0, -1, 0}, +{ 1, s_0_1, 0, -1, 0}, +{ 3, s_0_2, 0, -1, 0}, +{ 3, s_0_3, 0, -1, 0}, +{ 3, s_0_4, 0, -1, 0}, +{ 3, s_0_5, 0, -1, 0}, +{ 4, s_0_6, 0, -1, 0}, +{ 4, s_0_7, 0, -1, 0}, +{ 4, s_0_8, 0, -1, 0}, +{ 4, s_0_9, 0, -1, 0} }; static const symbol s_1_0[4] = { 'l', 'e', 'r', 'i' }; static const symbol s_1_1[5] = { 'l', 'a', 'r', 0xC4, 0xB1 }; - -static const struct among a_1[2] = -{ -{ 4, s_1_0, -1, -1, 0}, -{ 5, s_1_1, -1, -1, 0} +static const struct among a_1[2] = { +{ 4, s_1_0, 0, -1, 0}, +{ 5, s_1_1, 0, -1, 0} }; static const symbol s_2_0[2] = { 'n', 'i' }; static const symbol s_2_1[2] = { 'n', 'u' }; static const symbol s_2_2[3] = { 'n', 0xC4, 0xB1 }; static const symbol s_2_3[3] = { 'n', 0xC3, 0xBC }; - -static const struct among a_2[4] = -{ -{ 2, s_2_0, -1, -1, 0}, -{ 2, s_2_1, -1, -1, 0}, -{ 3, s_2_2, -1, -1, 0}, -{ 3, s_2_3, -1, -1, 0} +static const struct among a_2[4] = { +{ 2, s_2_0, 0, -1, 0}, +{ 2, s_2_1, 0, -1, 0}, +{ 3, s_2_2, 0, -1, 0}, +{ 3, s_2_3, 0, -1, 0} }; static const symbol s_3_0[2] = { 'i', 'n' }; static const symbol s_3_1[2] = { 'u', 'n' }; static const symbol s_3_2[3] = { 0xC4, 0xB1, 'n' }; static const symbol s_3_3[3] = { 0xC3, 0xBC, 'n' }; - -static const struct among a_3[4] = -{ -{ 2, s_3_0, -1, -1, 0}, -{ 2, s_3_1, -1, -1, 0}, -{ 3, s_3_2, -1, -1, 0}, -{ 3, s_3_3, -1, -1, 0} -}; - -static const symbol s_4_0[1] = { 'a' }; -static const symbol s_4_1[1] = { 'e' }; - -static const struct among a_4[2] = -{ -{ 1, s_4_0, -1, -1, 0}, -{ 1, s_4_1, -1, -1, 0} +static const struct among a_3[4] = { +{ 2, s_3_0, 0, -1, 0}, +{ 2, s_3_1, 0, -1, 0}, +{ 3, s_3_2, 0, -1, 0}, +{ 3, s_3_3, 0, -1, 0} }; static const symbol s_5_0[2] = { 'n', 'a' }; static const symbol s_5_1[2] = { 'n', 'e' }; - -static const struct among a_5[2] = -{ -{ 2, s_5_0, -1, -1, 0}, -{ 2, s_5_1, -1, -1, 0} +static const struct among a_5[2] = { +{ 2, s_5_0, 0, -1, 0}, +{ 2, s_5_1, 0, -1, 0} }; static const symbol s_6_0[2] = { 'd', 'a' }; static const symbol s_6_1[2] = { 't', 'a' }; static const symbol s_6_2[2] = { 'd', 'e' }; static const symbol s_6_3[2] = { 't', 'e' }; - -static const struct among a_6[4] = -{ -{ 2, s_6_0, -1, -1, 0}, -{ 2, s_6_1, -1, -1, 0}, -{ 2, s_6_2, -1, -1, 0}, -{ 2, s_6_3, -1, -1, 0} +static const struct among a_6[4] = { +{ 2, s_6_0, 0, -1, 0}, +{ 2, s_6_1, 0, -1, 0}, +{ 2, s_6_2, 0, -1, 0}, +{ 2, s_6_3, 0, -1, 0} }; static const symbol s_7_0[3] = { 'n', 'd', 'a' }; static const symbol s_7_1[3] = { 'n', 'd', 'e' }; - -static const struct among a_7[2] = -{ -{ 3, s_7_0, -1, -1, 0}, -{ 3, s_7_1, -1, -1, 0} +static const struct among a_7[2] = { +{ 3, s_7_0, 0, -1, 0}, +{ 3, s_7_1, 0, -1, 0} }; static const symbol s_8_0[3] = { 'd', 'a', 'n' }; static const symbol s_8_1[3] = { 't', 'a', 'n' }; static const symbol s_8_2[3] = { 'd', 'e', 'n' }; static const symbol s_8_3[3] = { 't', 'e', 'n' }; - -static const struct among a_8[4] = -{ -{ 3, s_8_0, -1, -1, 0}, -{ 3, s_8_1, -1, -1, 0}, -{ 3, s_8_2, -1, -1, 0}, -{ 3, s_8_3, -1, -1, 0} +static const struct among a_8[4] = { +{ 3, s_8_0, 0, -1, 0}, +{ 3, s_8_1, 0, -1, 0}, +{ 3, s_8_2, 0, -1, 0}, +{ 3, s_8_3, 0, -1, 0} }; static const symbol s_9_0[4] = { 'n', 'd', 'a', 'n' }; static const symbol s_9_1[4] = { 'n', 'd', 'e', 'n' }; - -static const struct among a_9[2] = -{ -{ 4, s_9_0, -1, -1, 0}, -{ 4, s_9_1, -1, -1, 0} +static const struct among a_9[2] = { +{ 4, s_9_0, 0, -1, 0}, +{ 4, s_9_1, 0, -1, 0} }; static const symbol s_10_0[2] = { 'l', 'a' }; static const symbol s_10_1[2] = { 'l', 'e' }; - -static const struct among a_10[2] = -{ -{ 2, s_10_0, -1, -1, 0}, -{ 2, s_10_1, -1, -1, 0} +static const struct among a_10[2] = { +{ 2, s_10_0, 0, -1, 0}, +{ 2, s_10_1, 0, -1, 0} }; static const symbol s_11_0[2] = { 'c', 'a' }; static const symbol s_11_1[2] = { 'c', 'e' }; - -static const struct among a_11[2] = -{ -{ 2, s_11_0, -1, -1, 0}, -{ 2, s_11_1, -1, -1, 0} +static const struct among a_11[2] = { +{ 2, s_11_0, 0, -1, 0}, +{ 2, s_11_1, 0, -1, 0} }; static const symbol s_12_0[2] = { 'i', 'm' }; static const symbol s_12_1[2] = { 'u', 'm' }; static const symbol s_12_2[3] = { 0xC4, 0xB1, 'm' }; static const symbol s_12_3[3] = { 0xC3, 0xBC, 'm' }; - -static const struct among a_12[4] = -{ -{ 2, s_12_0, -1, -1, 0}, -{ 2, s_12_1, -1, -1, 0}, -{ 3, s_12_2, -1, -1, 0}, -{ 3, s_12_3, -1, -1, 0} +static const struct among a_12[4] = { +{ 2, s_12_0, 0, -1, 0}, +{ 2, s_12_1, 0, -1, 0}, +{ 3, s_12_2, 0, -1, 0}, +{ 3, s_12_3, 0, -1, 0} }; static const symbol s_13_0[3] = { 's', 'i', 'n' }; static const symbol s_13_1[3] = { 's', 'u', 'n' }; static const symbol s_13_2[4] = { 's', 0xC4, 0xB1, 'n' }; static const symbol s_13_3[4] = { 's', 0xC3, 0xBC, 'n' }; - -static const struct among a_13[4] = -{ -{ 3, s_13_0, -1, -1, 0}, -{ 3, s_13_1, -1, -1, 0}, -{ 4, s_13_2, -1, -1, 0}, -{ 4, s_13_3, -1, -1, 0} +static const struct among a_13[4] = { +{ 3, s_13_0, 0, -1, 0}, +{ 3, s_13_1, 0, -1, 0}, +{ 4, s_13_2, 0, -1, 0}, +{ 4, s_13_3, 0, -1, 0} }; static const symbol s_14_0[2] = { 'i', 'z' }; static const symbol s_14_1[2] = { 'u', 'z' }; static const symbol s_14_2[3] = { 0xC4, 0xB1, 'z' }; static const symbol s_14_3[3] = { 0xC3, 0xBC, 'z' }; - -static const struct among a_14[4] = -{ -{ 2, s_14_0, -1, -1, 0}, -{ 2, s_14_1, -1, -1, 0}, -{ 3, s_14_2, -1, -1, 0}, -{ 3, s_14_3, -1, -1, 0} +static const struct among a_14[4] = { +{ 2, s_14_0, 0, -1, 0}, +{ 2, s_14_1, 0, -1, 0}, +{ 3, s_14_2, 0, -1, 0}, +{ 3, s_14_3, 0, -1, 0} }; static const symbol s_15_0[5] = { 's', 'i', 'n', 'i', 'z' }; static const symbol s_15_1[5] = { 's', 'u', 'n', 'u', 'z' }; static const symbol s_15_2[7] = { 's', 0xC4, 0xB1, 'n', 0xC4, 0xB1, 'z' }; static const symbol s_15_3[7] = { 's', 0xC3, 0xBC, 'n', 0xC3, 0xBC, 'z' }; - -static const struct among a_15[4] = -{ -{ 5, s_15_0, -1, -1, 0}, -{ 5, s_15_1, -1, -1, 0}, -{ 7, s_15_2, -1, -1, 0}, -{ 7, s_15_3, -1, -1, 0} +static const struct among a_15[4] = { +{ 5, s_15_0, 0, -1, 0}, +{ 5, s_15_1, 0, -1, 0}, +{ 7, s_15_2, 0, -1, 0}, +{ 7, s_15_3, 0, -1, 0} }; static const symbol s_16_0[3] = { 'l', 'a', 'r' }; static const symbol s_16_1[3] = { 'l', 'e', 'r' }; - -static const struct among a_16[2] = -{ -{ 3, s_16_0, -1, -1, 0}, -{ 3, s_16_1, -1, -1, 0} +static const struct among a_16[2] = { +{ 3, s_16_0, 0, -1, 0}, +{ 3, s_16_1, 0, -1, 0} }; static const symbol s_17_0[3] = { 'n', 'i', 'z' }; static const symbol s_17_1[3] = { 'n', 'u', 'z' }; static const symbol s_17_2[4] = { 'n', 0xC4, 0xB1, 'z' }; static const symbol s_17_3[4] = { 'n', 0xC3, 0xBC, 'z' }; - -static const struct among a_17[4] = -{ -{ 3, s_17_0, -1, -1, 0}, -{ 3, s_17_1, -1, -1, 0}, -{ 4, s_17_2, -1, -1, 0}, -{ 4, s_17_3, -1, -1, 0} +static const struct among a_17[4] = { +{ 3, s_17_0, 0, -1, 0}, +{ 3, s_17_1, 0, -1, 0}, +{ 4, s_17_2, 0, -1, 0}, +{ 4, s_17_3, 0, -1, 0} }; static const symbol s_18_0[3] = { 'd', 'i', 'r' }; @@ -284,26 +261,22 @@ static const symbol s_18_4[4] = { 'd', 0xC4, 0xB1, 'r' }; static const symbol s_18_5[4] = { 't', 0xC4, 0xB1, 'r' }; static const symbol s_18_6[4] = { 'd', 0xC3, 0xBC, 'r' }; static const symbol s_18_7[4] = { 't', 0xC3, 0xBC, 'r' }; - -static const struct among a_18[8] = -{ -{ 3, s_18_0, -1, -1, 0}, -{ 3, s_18_1, -1, -1, 0}, -{ 3, s_18_2, -1, -1, 0}, -{ 3, s_18_3, -1, -1, 0}, -{ 4, s_18_4, -1, -1, 0}, -{ 4, s_18_5, -1, -1, 0}, -{ 4, s_18_6, -1, -1, 0}, -{ 4, s_18_7, -1, -1, 0} +static const struct among a_18[8] = { +{ 3, s_18_0, 0, -1, 0}, +{ 3, s_18_1, 0, -1, 0}, +{ 3, s_18_2, 0, -1, 0}, +{ 3, s_18_3, 0, -1, 0}, +{ 4, s_18_4, 0, -1, 0}, +{ 4, s_18_5, 0, -1, 0}, +{ 4, s_18_6, 0, -1, 0}, +{ 4, s_18_7, 0, -1, 0} }; static const symbol s_19_0[7] = { 'c', 'a', 's', 0xC4, 0xB1, 'n', 'a' }; static const symbol s_19_1[6] = { 'c', 'e', 's', 'i', 'n', 'e' }; - -static const struct among a_19[2] = -{ -{ 7, s_19_0, -1, -1, 0}, -{ 6, s_19_1, -1, -1, 0} +static const struct among a_19[2] = { +{ 7, s_19_0, 0, -1, 0}, +{ 6, s_19_1, 0, -1, 0} }; static const symbol s_20_0[2] = { 'd', 'i' }; @@ -338,41 +311,39 @@ static const symbol s_20_28[3] = { 'd', 0xC4, 0xB1 }; static const symbol s_20_29[3] = { 't', 0xC4, 0xB1 }; static const symbol s_20_30[3] = { 'd', 0xC3, 0xBC }; static const symbol s_20_31[3] = { 't', 0xC3, 0xBC }; - -static const struct among a_20[32] = -{ -{ 2, s_20_0, -1, -1, 0}, -{ 2, s_20_1, -1, -1, 0}, -{ 3, s_20_2, -1, -1, 0}, -{ 3, s_20_3, -1, -1, 0}, -{ 3, s_20_4, -1, -1, 0}, -{ 3, s_20_5, -1, -1, 0}, -{ 4, s_20_6, -1, -1, 0}, -{ 4, s_20_7, -1, -1, 0}, -{ 4, s_20_8, -1, -1, 0}, -{ 4, s_20_9, -1, -1, 0}, -{ 3, s_20_10, -1, -1, 0}, -{ 3, s_20_11, -1, -1, 0}, -{ 3, s_20_12, -1, -1, 0}, -{ 3, s_20_13, -1, -1, 0}, -{ 4, s_20_14, -1, -1, 0}, -{ 4, s_20_15, -1, -1, 0}, -{ 4, s_20_16, -1, -1, 0}, -{ 4, s_20_17, -1, -1, 0}, -{ 3, s_20_18, -1, -1, 0}, -{ 3, s_20_19, -1, -1, 0}, -{ 3, s_20_20, -1, -1, 0}, -{ 3, s_20_21, -1, -1, 0}, -{ 4, s_20_22, -1, -1, 0}, -{ 4, s_20_23, -1, -1, 0}, -{ 4, s_20_24, -1, -1, 0}, -{ 4, s_20_25, -1, -1, 0}, -{ 2, s_20_26, -1, -1, 0}, -{ 2, s_20_27, -1, -1, 0}, -{ 3, s_20_28, -1, -1, 0}, -{ 3, s_20_29, -1, -1, 0}, -{ 3, s_20_30, -1, -1, 0}, -{ 3, s_20_31, -1, -1, 0} +static const struct among a_20[32] = { +{ 2, s_20_0, 0, -1, 0}, +{ 2, s_20_1, 0, -1, 0}, +{ 3, s_20_2, 0, -1, 0}, +{ 3, s_20_3, 0, -1, 0}, +{ 3, s_20_4, 0, -1, 0}, +{ 3, s_20_5, 0, -1, 0}, +{ 4, s_20_6, 0, -1, 0}, +{ 4, s_20_7, 0, -1, 0}, +{ 4, s_20_8, 0, -1, 0}, +{ 4, s_20_9, 0, -1, 0}, +{ 3, s_20_10, 0, -1, 0}, +{ 3, s_20_11, 0, -1, 0}, +{ 3, s_20_12, 0, -1, 0}, +{ 3, s_20_13, 0, -1, 0}, +{ 4, s_20_14, 0, -1, 0}, +{ 4, s_20_15, 0, -1, 0}, +{ 4, s_20_16, 0, -1, 0}, +{ 4, s_20_17, 0, -1, 0}, +{ 3, s_20_18, 0, -1, 0}, +{ 3, s_20_19, 0, -1, 0}, +{ 3, s_20_20, 0, -1, 0}, +{ 3, s_20_21, 0, -1, 0}, +{ 4, s_20_22, 0, -1, 0}, +{ 4, s_20_23, 0, -1, 0}, +{ 4, s_20_24, 0, -1, 0}, +{ 4, s_20_25, 0, -1, 0}, +{ 2, s_20_26, 0, -1, 0}, +{ 2, s_20_27, 0, -1, 0}, +{ 3, s_20_28, 0, -1, 0}, +{ 3, s_20_29, 0, -1, 0}, +{ 3, s_20_30, 0, -1, 0}, +{ 3, s_20_31, 0, -1, 0} }; static const symbol s_21_0[2] = { 's', 'a' }; @@ -383,43 +354,37 @@ static const symbol s_21_4[3] = { 's', 'a', 'm' }; static const symbol s_21_5[3] = { 's', 'e', 'm' }; static const symbol s_21_6[3] = { 's', 'a', 'n' }; static const symbol s_21_7[3] = { 's', 'e', 'n' }; - -static const struct among a_21[8] = -{ -{ 2, s_21_0, -1, -1, 0}, -{ 2, s_21_1, -1, -1, 0}, -{ 3, s_21_2, -1, -1, 0}, -{ 3, s_21_3, -1, -1, 0}, -{ 3, s_21_4, -1, -1, 0}, -{ 3, s_21_5, -1, -1, 0}, -{ 3, s_21_6, -1, -1, 0}, -{ 3, s_21_7, -1, -1, 0} +static const struct among a_21[8] = { +{ 2, s_21_0, 0, -1, 0}, +{ 2, s_21_1, 0, -1, 0}, +{ 3, s_21_2, 0, -1, 0}, +{ 3, s_21_3, 0, -1, 0}, +{ 3, s_21_4, 0, -1, 0}, +{ 3, s_21_5, 0, -1, 0}, +{ 3, s_21_6, 0, -1, 0}, +{ 3, s_21_7, 0, -1, 0} }; static const symbol s_22_0[4] = { 'm', 'i', 0xC5, 0x9F }; static const symbol s_22_1[4] = { 'm', 'u', 0xC5, 0x9F }; static const symbol s_22_2[5] = { 'm', 0xC4, 0xB1, 0xC5, 0x9F }; static const symbol s_22_3[5] = { 'm', 0xC3, 0xBC, 0xC5, 0x9F }; - -static const struct among a_22[4] = -{ -{ 4, s_22_0, -1, -1, 0}, -{ 4, s_22_1, -1, -1, 0}, -{ 5, s_22_2, -1, -1, 0}, -{ 5, s_22_3, -1, -1, 0} +static const struct among a_22[4] = { +{ 4, s_22_0, 0, -1, 0}, +{ 4, s_22_1, 0, -1, 0}, +{ 5, s_22_2, 0, -1, 0}, +{ 5, s_22_3, 0, -1, 0} }; static const symbol s_23_0[1] = { 'b' }; static const symbol s_23_1[1] = { 'c' }; static const symbol s_23_2[1] = { 'd' }; static const symbol s_23_3[2] = { 0xC4, 0x9F }; - -static const struct among a_23[4] = -{ -{ 1, s_23_0, -1, 1, 0}, -{ 1, s_23_1, -1, 2, 0}, -{ 1, s_23_2, -1, 3, 0}, -{ 2, s_23_3, -1, 4, 0} +static const struct among a_23[4] = { +{ 1, s_23_0, 0, 1, 0}, +{ 1, s_23_1, 0, 2, 0}, +{ 1, s_23_2, 0, 3, 0}, +{ 2, s_23_3, 0, 4, 0} }; static const unsigned char g_vowel[] = { 17, 65, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 8, 0, 0, 0, 0, 0, 0, 1 }; @@ -438,728 +403,710 @@ static const unsigned char g_vowel5[] = { 65 }; static const unsigned char g_vowel6[] = { 65 }; -static const symbol s_0[] = { 0xC4, 0xB1 }; -static const symbol s_1[] = { 0xC3, 0xB6 }; -static const symbol s_2[] = { 0xC3, 0xBC }; -static const symbol s_3[] = { 'k', 'i' }; -static const symbol s_4[] = { 'k', 'e', 'n' }; -static const symbol s_5[] = { 'p' }; -static const symbol s_6[] = { 0xC3, 0xA7 }; -static const symbol s_7[] = { 't' }; -static const symbol s_8[] = { 'k' }; -static const symbol s_9[] = { 0xC4, 0xB1 }; -static const symbol s_10[] = { 0xC4, 0xB1 }; -static const symbol s_11[] = { 'i' }; -static const symbol s_12[] = { 'u' }; -static const symbol s_13[] = { 0xC3, 0xB6 }; -static const symbol s_14[] = { 0xC3, 0xBC }; -static const symbol s_15[] = { 0xC3, 0xBC }; -static const symbol s_16[] = { 'a', 'd' }; -static const symbol s_17[] = { 's', 'o', 'y' }; - static int r_check_vowel_harmony(struct SN_env * z) { - { int m_test1 = z->l - z->c; - + { + int v_1 = z->l - z->c; if (out_grouping_b_U(z, g_vowel, 97, 305, 1) < 0) return 0; - { int m2 = z->l - z->c; (void)m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'a') goto lab1; + do { + int v_2 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'a') goto lab0; z->c--; - - if (out_grouping_b_U(z, g_vowel1, 97, 305, 1) < 0) goto lab1; - goto lab0; - lab1: - z->c = z->l - m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab2; + if (out_grouping_b_U(z, g_vowel1, 97, 305, 1) < 0) goto lab0; + break; + lab0: + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab1; z->c--; - - if (out_grouping_b_U(z, g_vowel2, 101, 252, 1) < 0) goto lab2; - goto lab0; + if (out_grouping_b_U(z, g_vowel2, 101, 252, 1) < 0) goto lab1; + break; + lab1: + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_0))) goto lab2; + if (out_grouping_b_U(z, g_vowel3, 97, 305, 1) < 0) goto lab2; + break; lab2: - z->c = z->l - m2; - if (!(eq_s_b(z, 2, s_0))) goto lab3; - - if (out_grouping_b_U(z, g_vowel3, 97, 305, 1) < 0) goto lab3; - goto lab0; + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab3; + z->c--; + if (out_grouping_b_U(z, g_vowel4, 101, 105, 1) < 0) goto lab3; + break; lab3: - z->c = z->l - m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab4; + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab4; z->c--; - - if (out_grouping_b_U(z, g_vowel4, 101, 105, 1) < 0) goto lab4; - goto lab0; + if (out_grouping_b_U(z, g_vowel5, 111, 117, 1) < 0) goto lab4; + break; lab4: - z->c = z->l - m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab5; - z->c--; - - if (out_grouping_b_U(z, g_vowel5, 111, 117, 1) < 0) goto lab5; - goto lab0; + z->c = z->l - v_2; + if (!(eq_s_b(z, 2, s_1))) goto lab5; + if (out_grouping_b_U(z, g_vowel6, 246, 252, 1) < 0) goto lab5; + break; lab5: - z->c = z->l - m2; - if (!(eq_s_b(z, 2, s_1))) goto lab6; - - if (out_grouping_b_U(z, g_vowel6, 246, 252, 1) < 0) goto lab6; - goto lab0; - lab6: - z->c = z->l - m2; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab7; + z->c = z->l - v_2; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab6; z->c--; - - if (out_grouping_b_U(z, g_vowel5, 111, 117, 1) < 0) goto lab7; - goto lab0; - lab7: - z->c = z->l - m2; + if (out_grouping_b_U(z, g_vowel5, 111, 117, 1) < 0) goto lab6; + break; + lab6: + z->c = z->l - v_2; if (!(eq_s_b(z, 2, s_2))) return 0; - if (out_grouping_b_U(z, g_vowel6, 246, 252, 1) < 0) return 0; - } - lab0: - z->c = z->l - m_test1; + } while (0); + z->c = z->l - v_1; } return 1; } static int r_mark_suffix_with_optional_n_consonant(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'n') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') goto lab0; z->c--; - { int m_test2 = z->l - z->c; - if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab1; - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab0; + z->c = z->l - v_2; } - goto lab0; - lab1: - z->c = z->l - m1; - { int m3 = z->l - z->c; (void)m3; - { int m_test4 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'n') goto lab2; - z->c--; - z->c = z->l - m_test4; - } + break; + lab0: + z->c = z->l - v_1; + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'n') goto lab1; + z->c--; return 0; - lab2: - z->c = z->l - m3; + lab1: + z->c = z->l - v_3; } - { int m_test5 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int v_4 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) return 0; - z->c = z->l - m_test5; + z->c = z->l - v_4; } - } -lab0: + } while (0); return 1; } static int r_mark_suffix_with_optional_s_consonant(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab0; z->c--; - { int m_test2 = z->l - z->c; - if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab1; - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab0; + z->c = z->l - v_2; } - goto lab0; - lab1: - z->c = z->l - m1; - { int m3 = z->l - z->c; (void)m3; - { int m_test4 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab2; - z->c--; - z->c = z->l - m_test4; - } + break; + lab0: + z->c = z->l - v_1; + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 's') goto lab1; + z->c--; return 0; - lab2: - z->c = z->l - m3; + lab1: + z->c = z->l - v_3; } - { int m_test5 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int v_4 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) return 0; - z->c = z->l - m_test5; + z->c = z->l - v_4; } - } -lab0: + } while (0); return 1; } static int r_mark_suffix_with_optional_y_consonant(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab0; z->c--; - { int m_test2 = z->l - z->c; - if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab1; - z->c = z->l - m_test2; + { + int v_2 = z->l - z->c; + if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab0; + z->c = z->l - v_2; } - goto lab0; - lab1: - z->c = z->l - m1; - { int m3 = z->l - z->c; (void)m3; - { int m_test4 = z->l - z->c; - if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab2; - z->c--; - z->c = z->l - m_test4; - } + break; + lab0: + z->c = z->l - v_1; + { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'y') goto lab1; + z->c--; return 0; - lab2: - z->c = z->l - m3; + lab1: + z->c = z->l - v_3; } - { int m_test5 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int v_4 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } if (in_grouping_b_U(z, g_vowel, 97, 305, 0)) return 0; - z->c = z->l - m_test5; + z->c = z->l - v_4; } - } -lab0: + } while (0); return 1; } static int r_mark_suffix_with_optional_U_vowel(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; - if (in_grouping_b_U(z, g_U, 105, 305, 0)) goto lab1; - { int m_test2 = z->l - z->c; - if (out_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab1; - z->c = z->l - m_test2; - } - goto lab0; - lab1: - z->c = z->l - m1; - { int m3 = z->l - z->c; (void)m3; - { int m_test4 = z->l - z->c; - if (in_grouping_b_U(z, g_U, 105, 305, 0)) goto lab2; - z->c = z->l - m_test4; - } + do { + int v_1 = z->l - z->c; + if (in_grouping_b_U(z, g_U, 105, 305, 0)) goto lab0; + { + int v_2 = z->l - z->c; + if (out_grouping_b_U(z, g_vowel, 97, 305, 0)) goto lab0; + z->c = z->l - v_2; + } + break; + lab0: + z->c = z->l - v_1; + { + int v_3 = z->l - z->c; + if (in_grouping_b_U(z, g_U, 105, 305, 0)) goto lab1; return 0; - lab2: - z->c = z->l - m3; + lab1: + z->c = z->l - v_3; } - { int m_test5 = z->l - z->c; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + { + int v_4 = z->l - z->c; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); if (ret < 0) return 0; z->c = ret; } if (out_grouping_b_U(z, g_vowel, 97, 305, 0)) return 0; - z->c = z->l - m_test5; + z->c = z->l - v_4; } - } -lab0: + } while (0); return 1; } static int r_mark_possessives(struct SN_env * z) { if (z->c <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((67133440 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_0, 10)) return 0; - { int ret = r_mark_suffix_with_optional_U_vowel(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_0, 10, 0)) return 0; + return r_mark_suffix_with_optional_U_vowel(z); } static int r_mark_sU(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (in_grouping_b_U(z, g_U, 105, 305, 0)) return 0; - { int ret = r_mark_suffix_with_optional_s_consonant(z); - if (ret <= 0) return ret; - } - return 1; + return r_mark_suffix_with_optional_s_consonant(z); } static int r_mark_lArI(struct SN_env * z) { if (z->c - 3 <= z->lb || (z->p[z->c - 1] != 105 && z->p[z->c - 1] != 177)) return 0; - if (!find_among_b(z, a_1, 2)) return 0; - return 1; + return find_among_b(z, a_1, 2, 0) != 0; } static int r_mark_yU(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (in_grouping_b_U(z, g_U, 105, 305, 0)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_nU(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } - if (!find_among_b(z, a_2, 4)) return 0; - return 1; + return find_among_b(z, a_2, 4, 0) != 0; } static int r_mark_nUn(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || z->p[z->c - 1] != 110) return 0; - if (!find_among_b(z, a_3, 4)) return 0; - { int ret = r_mark_suffix_with_optional_n_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_3, 4, 0)) return 0; + return r_mark_suffix_with_optional_n_consonant(z); } static int r_mark_yA(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_4, 2)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + z->c--; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_nA(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_5, 2)) return 0; - return 1; + return find_among_b(z, a_5, 2, 0) != 0; } static int r_mark_DA(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_6, 4)) return 0; - return 1; + return find_among_b(z, a_6, 4, 0) != 0; } static int r_mark_ndA(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 2 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_7, 2)) return 0; - return 1; + return find_among_b(z, a_7, 2, 0) != 0; } static int r_mark_DAn(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 2 <= z->lb || z->p[z->c - 1] != 110) return 0; - if (!find_among_b(z, a_8, 4)) return 0; - return 1; + return find_among_b(z, a_8, 4, 0) != 0; } static int r_mark_ndAn(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 3 <= z->lb || z->p[z->c - 1] != 110) return 0; - if (!find_among_b(z, a_9, 2)) return 0; - return 1; + return find_among_b(z, a_9, 2, 0) != 0; } static int r_mark_ylA(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_10, 2)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_10, 2, 0)) return 0; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_ki(struct SN_env * z) { - if (!(eq_s_b(z, 2, s_3))) return 0; - return 1; + return eq_s_b(z, 2, s_3); } static int r_mark_ncA(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_11, 2)) return 0; - { int ret = r_mark_suffix_with_optional_n_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_11, 2, 0)) return 0; + return r_mark_suffix_with_optional_n_consonant(z); } static int r_mark_yUm(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || z->p[z->c - 1] != 109) return 0; - if (!find_among_b(z, a_12, 4)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_12, 4, 0)) return 0; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_sUn(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 2 <= z->lb || z->p[z->c - 1] != 110) return 0; - if (!find_among_b(z, a_13, 4)) return 0; - return 1; + return find_among_b(z, a_13, 4, 0) != 0; } static int r_mark_yUz(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 1 <= z->lb || z->p[z->c - 1] != 122) return 0; - if (!find_among_b(z, a_14, 4)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_14, 4, 0)) return 0; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_sUnUz(struct SN_env * z) { if (z->c - 4 <= z->lb || z->p[z->c - 1] != 122) return 0; - if (!find_among_b(z, a_15, 4)) return 0; - return 1; + return find_among_b(z, a_15, 4, 0) != 0; } static int r_mark_lAr(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 2 <= z->lb || z->p[z->c - 1] != 114) return 0; - if (!find_among_b(z, a_16, 2)) return 0; - return 1; + return find_among_b(z, a_16, 2, 0) != 0; } static int r_mark_nUz(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 2 <= z->lb || z->p[z->c - 1] != 122) return 0; - if (!find_among_b(z, a_17, 4)) return 0; - return 1; + return find_among_b(z, a_17, 4, 0) != 0; } static int r_mark_DUr(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 2 <= z->lb || z->p[z->c - 1] != 114) return 0; - if (!find_among_b(z, a_18, 8)) return 0; - return 1; + return find_among_b(z, a_18, 8, 0) != 0; } static int r_mark_cAsInA(struct SN_env * z) { if (z->c - 5 <= z->lb || (z->p[z->c - 1] != 97 && z->p[z->c - 1] != 101)) return 0; - if (!find_among_b(z, a_19, 2)) return 0; - return 1; + return find_among_b(z, a_19, 2, 0) != 0; } static int r_mark_yDU(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); - if (ret <= 0) return ret; - } - if (!find_among_b(z, a_20, 32)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } - return 1; + if (!find_among_b(z, a_20, 32, 0)) return 0; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_ysA(struct SN_env * z) { if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 3 || !((26658 >> (z->p[z->c - 1] & 0x1f)) & 1)) return 0; - if (!find_among_b(z, a_21, 8)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_21, 8, 0)) return 0; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_ymUs_(struct SN_env * z) { - { int ret = r_check_vowel_harmony(z); + { + int ret = r_check_vowel_harmony(z); if (ret <= 0) return ret; } if (z->c - 3 <= z->lb || z->p[z->c - 1] != 159) return 0; - if (!find_among_b(z, a_22, 4)) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + if (!find_among_b(z, a_22, 4, 0)) return 0; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_mark_yken(struct SN_env * z) { if (!(eq_s_b(z, 3, s_4))) return 0; - { int ret = r_mark_suffix_with_optional_y_consonant(z); - if (ret <= 0) return ret; - } - return 1; + return r_mark_suffix_with_optional_y_consonant(z); } static int r_stem_nominal_verb_suffixes(struct SN_env * z) { z->ket = z->c; - z->I[0] = 1; - { int m1 = z->l - z->c; (void)m1; - { int m2 = z->l - z->c; (void)m2; - { int ret = r_mark_ymUs_(z); - if (ret == 0) goto lab3; + ((SN_local *)z)->b_continue_stemming_noun_suffixes = 1; + do { + int v_1 = z->l - z->c; + do { + int v_2 = z->l - z->c; + { + int ret = r_mark_ymUs_(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - goto lab2; - lab3: - z->c = z->l - m2; - { int ret = r_mark_yDU(z); - if (ret == 0) goto lab4; + break; + lab1: + z->c = z->l - v_2; + { + int ret = r_mark_yDU(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } - goto lab2; - lab4: - z->c = z->l - m2; - { int ret = r_mark_ysA(z); - if (ret == 0) goto lab5; + break; + lab2: + z->c = z->l - v_2; + { + int ret = r_mark_ysA(z); + if (ret == 0) goto lab3; if (ret < 0) return ret; } - goto lab2; - lab5: - z->c = z->l - m2; - { int ret = r_mark_yken(z); - if (ret == 0) goto lab1; + break; + lab3: + z->c = z->l - v_2; + { + int ret = r_mark_yken(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } - } - lab2: - goto lab0; - lab1: - z->c = z->l - m1; - { int ret = r_mark_cAsInA(z); - if (ret == 0) goto lab6; + } while (0); + break; + lab0: + z->c = z->l - v_1; + { + int ret = r_mark_cAsInA(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_mark_sUnUz(z); - if (ret == 0) goto lab8; + do { + int v_3 = z->l - z->c; + { + int ret = r_mark_sUnUz(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } - goto lab7; - lab8: - z->c = z->l - m3; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab9; + break; + lab5: + z->c = z->l - v_3; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - goto lab7; - lab9: - z->c = z->l - m3; - { int ret = r_mark_yUm(z); - if (ret == 0) goto lab10; + break; + lab6: + z->c = z->l - v_3; + { + int ret = r_mark_yUm(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } - goto lab7; - lab10: - z->c = z->l - m3; - { int ret = r_mark_sUn(z); - if (ret == 0) goto lab11; + break; + lab7: + z->c = z->l - v_3; + { + int ret = r_mark_sUn(z); + if (ret == 0) goto lab8; if (ret < 0) return ret; } - goto lab7; - lab11: - z->c = z->l - m3; - { int ret = r_mark_yUz(z); - if (ret == 0) goto lab12; + break; + lab8: + z->c = z->l - v_3; + { + int ret = r_mark_yUz(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - goto lab7; - lab12: - z->c = z->l - m3; - } - lab7: - { int ret = r_mark_ymUs_(z); - if (ret == 0) goto lab6; + break; + lab9: + z->c = z->l - v_3; + } while (0); + { + int ret = r_mark_ymUs_(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } - goto lab0; - lab6: - z->c = z->l - m1; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab13; + break; + lab4: + z->c = z->l - v_1; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab10; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; + { + int v_4 = z->l - z->c; z->ket = z->c; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_mark_DUr(z); - if (ret == 0) goto lab16; + do { + int v_5 = z->l - z->c; + { + int ret = r_mark_DUr(z); + if (ret == 0) goto lab12; if (ret < 0) return ret; } - goto lab15; - lab16: - z->c = z->l - m5; - { int ret = r_mark_yDU(z); - if (ret == 0) goto lab17; + break; + lab12: + z->c = z->l - v_5; + { + int ret = r_mark_yDU(z); + if (ret == 0) goto lab13; if (ret < 0) return ret; } - goto lab15; - lab17: - z->c = z->l - m5; - { int ret = r_mark_ysA(z); - if (ret == 0) goto lab18; + break; + lab13: + z->c = z->l - v_5; + { + int ret = r_mark_ysA(z); + if (ret == 0) goto lab14; if (ret < 0) return ret; } - goto lab15; - lab18: - z->c = z->l - m5; - { int ret = r_mark_ymUs_(z); - if (ret == 0) { z->c = z->l - m4; goto lab14; } + break; + lab14: + z->c = z->l - v_5; + { + int ret = r_mark_ymUs_(z); + if (ret == 0) { z->c = z->l - v_4; goto lab11; } if (ret < 0) return ret; } - } - lab15: - lab14: + } while (0); + lab11: ; } - z->I[0] = 0; - goto lab0; - lab13: - z->c = z->l - m1; - { int ret = r_mark_nUz(z); - if (ret == 0) goto lab19; + ((SN_local *)z)->b_continue_stemming_noun_suffixes = 0; + break; + lab10: + z->c = z->l - v_1; + { + int ret = r_mark_nUz(z); + if (ret == 0) goto lab15; if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; - { int ret = r_mark_yDU(z); - if (ret == 0) goto lab21; + do { + int v_6 = z->l - z->c; + { + int ret = r_mark_yDU(z); + if (ret == 0) goto lab16; if (ret < 0) return ret; } - goto lab20; - lab21: - z->c = z->l - m6; - { int ret = r_mark_ysA(z); - if (ret == 0) goto lab19; - if (ret < 0) return ret; + break; + lab16: + z->c = z->l - v_6; + { + int ret = r_mark_ysA(z); + if (ret == 0) goto lab15; + if (ret < 0) return ret; } - } - lab20: - goto lab0; - lab19: - z->c = z->l - m1; - { int m7 = z->l - z->c; (void)m7; - { int ret = r_mark_sUnUz(z); - if (ret == 0) goto lab24; + } while (0); + break; + lab15: + z->c = z->l - v_1; + do { + int v_7 = z->l - z->c; + { + int ret = r_mark_sUnUz(z); + if (ret == 0) goto lab18; if (ret < 0) return ret; } - goto lab23; - lab24: - z->c = z->l - m7; - { int ret = r_mark_yUz(z); - if (ret == 0) goto lab25; + break; + lab18: + z->c = z->l - v_7; + { + int ret = r_mark_yUz(z); + if (ret == 0) goto lab19; if (ret < 0) return ret; } - goto lab23; - lab25: - z->c = z->l - m7; - { int ret = r_mark_sUn(z); - if (ret == 0) goto lab26; + break; + lab19: + z->c = z->l - v_7; + { + int ret = r_mark_sUn(z); + if (ret == 0) goto lab20; if (ret < 0) return ret; } - goto lab23; - lab26: - z->c = z->l - m7; - { int ret = r_mark_yUm(z); - if (ret == 0) goto lab22; + break; + lab20: + z->c = z->l - v_7; + { + int ret = r_mark_yUm(z); + if (ret == 0) goto lab17; if (ret < 0) return ret; } - } - lab23: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m8 = z->l - z->c; (void)m8; + { + int v_8 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_ymUs_(z); - if (ret == 0) { z->c = z->l - m8; goto lab27; } + { + int ret = r_mark_ymUs_(z); + if (ret == 0) { z->c = z->l - v_8; goto lab21; } if (ret < 0) return ret; } - lab27: + lab21: ; } - goto lab0; - lab22: - z->c = z->l - m1; - { int ret = r_mark_DUr(z); + break; + lab17: + z->c = z->l - v_1; + { + int ret = r_mark_DUr(z); if (ret <= 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; + { + int v_9 = z->l - z->c; z->ket = z->c; - { int m10 = z->l - z->c; (void)m10; - { int ret = r_mark_sUnUz(z); - if (ret == 0) goto lab30; + do { + int v_10 = z->l - z->c; + { + int ret = r_mark_sUnUz(z); + if (ret == 0) goto lab23; if (ret < 0) return ret; } - goto lab29; - lab30: - z->c = z->l - m10; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab31; + break; + lab23: + z->c = z->l - v_10; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab24; if (ret < 0) return ret; } - goto lab29; - lab31: - z->c = z->l - m10; - { int ret = r_mark_yUm(z); - if (ret == 0) goto lab32; + break; + lab24: + z->c = z->l - v_10; + { + int ret = r_mark_yUm(z); + if (ret == 0) goto lab25; if (ret < 0) return ret; } - goto lab29; - lab32: - z->c = z->l - m10; - { int ret = r_mark_sUn(z); - if (ret == 0) goto lab33; + break; + lab25: + z->c = z->l - v_10; + { + int ret = r_mark_sUn(z); + if (ret == 0) goto lab26; if (ret < 0) return ret; } - goto lab29; - lab33: - z->c = z->l - m10; - { int ret = r_mark_yUz(z); - if (ret == 0) goto lab34; + break; + lab26: + z->c = z->l - v_10; + { + int ret = r_mark_yUz(z); + if (ret == 0) goto lab27; if (ret < 0) return ret; } - goto lab29; - lab34: - z->c = z->l - m10; - } - lab29: - { int ret = r_mark_ymUs_(z); - if (ret == 0) { z->c = z->l - m9; goto lab28; } + break; + lab27: + z->c = z->l - v_10; + } while (0); + { + int ret = r_mark_ymUs_(z); + if (ret == 0) { z->c = z->l - v_9; goto lab22; } if (ret < 0) return ret; } - lab28: + lab22: ; } - } -lab0: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } return 1; @@ -1167,734 +1114,864 @@ static int r_stem_nominal_verb_suffixes(struct SN_env * z) { static int r_stem_suffix_chain_before_ki(struct SN_env * z) { z->ket = z->c; - { int ret = r_mark_ki(z); + { + int ret = r_mark_ki(z); if (ret <= 0) return ret; } - { int m1 = z->l - z->c; (void)m1; - { int ret = r_mark_DA(z); - if (ret == 0) goto lab1; + do { + int v_1 = z->l - z->c; + { + int ret = r_mark_DA(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; + { + int v_2 = z->l - z->c; z->ket = z->c; - { int m3 = z->l - z->c; (void)m3; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab4; + do { + int v_3 = z->l - z->c; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m4 = z->l - z->c; (void)m4; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m4; goto lab5; } + { + int v_4 = z->l - z->c; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_4; goto lab3; } if (ret < 0) return ret; } - lab5: + lab3: ; } - goto lab3; - lab4: - z->c = z->l - m3; - { int ret = r_mark_possessives(z); - if (ret == 0) { z->c = z->l - m2; goto lab2; } + break; + lab2: + z->c = z->l - v_3; + { + int ret = r_mark_possessives(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m5 = z->l - z->c; (void)m5; + { + int v_5 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m5; goto lab6; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_5; goto lab4; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m5; goto lab6; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_5; goto lab4; } if (ret < 0) return ret; } - lab6: + lab4: ; } - } - lab3: - lab2: + } while (0); + lab1: ; } - goto lab0; - lab1: - z->c = z->l - m1; - { int ret = r_mark_nUn(z); - if (ret == 0) goto lab7; + break; + lab0: + z->c = z->l - v_1; + { + int ret = r_mark_nUn(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; + { + int v_6 = z->l - z->c; z->ket = z->c; - { int m7 = z->l - z->c; (void)m7; - { int ret = r_mark_lArI(z); - if (ret == 0) goto lab10; + do { + int v_7 = z->l - z->c; + { + int ret = r_mark_lArI(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab9; - lab10: - z->c = z->l - m7; + break; + lab7: + z->c = z->l - v_7; z->ket = z->c; - { int m8 = z->l - z->c; (void)m8; - { int ret = r_mark_possessives(z); - if (ret == 0) goto lab13; + do { + int v_8 = z->l - z->c; + { + int ret = r_mark_possessives(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - goto lab12; - lab13: - z->c = z->l - m8; - { int ret = r_mark_sU(z); - if (ret == 0) goto lab11; + break; + lab9: + z->c = z->l - v_8; + { + int ret = r_mark_sU(z); + if (ret == 0) goto lab8; if (ret < 0) return ret; } - } - lab12: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; + { + int v_9 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m9; goto lab14; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_9; goto lab10; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m9; goto lab14; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_9; goto lab10; } if (ret < 0) return ret; } - lab14: + lab10: ; } - goto lab9; - lab11: - z->c = z->l - m7; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m6; goto lab8; } + break; + lab8: + z->c = z->l - v_7; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_6; goto lab6; } if (ret < 0) return ret; } - } - lab9: - lab8: + } while (0); + lab6: ; } - goto lab0; - lab7: - z->c = z->l - m1; - { int ret = r_mark_ndA(z); + break; + lab5: + z->c = z->l - v_1; + { + int ret = r_mark_ndA(z); if (ret <= 0) return ret; } - { int m10 = z->l - z->c; (void)m10; - { int ret = r_mark_lArI(z); - if (ret == 0) goto lab16; + do { + int v_10 = z->l - z->c; + { + int ret = r_mark_lArI(z); + if (ret == 0) goto lab11; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab15; - lab16: - z->c = z->l - m10; - { int ret = r_mark_sU(z); - if (ret == 0) goto lab17; + break; + lab11: + z->c = z->l - v_10; + { + int ret = r_mark_sU(z); + if (ret == 0) goto lab12; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m11 = z->l - z->c; (void)m11; + { + int v_11 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m11; goto lab18; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_11; goto lab13; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m11; goto lab18; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_11; goto lab13; } if (ret < 0) return ret; } - lab18: + lab13: ; } - goto lab15; - lab17: - z->c = z->l - m10; - { int ret = r_stem_suffix_chain_before_ki(z); + break; + lab12: + z->c = z->l - v_10; + { + int ret = r_stem_suffix_chain_before_ki(z); if (ret <= 0) return ret; } - } - lab15: - ; - } -lab0: + } while (0); + } while (0); return 1; } static int r_stem_noun_suffixes(struct SN_env * z) { - { int m1 = z->l - z->c; (void)m1; + do { + int v_1 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab1; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab0; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m2 = z->l - z->c; (void)m2; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m2; goto lab2; } + { + int v_2 = z->l - z->c; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_2; goto lab1; } if (ret < 0) return ret; } - lab2: + lab1: ; } - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; z->ket = z->c; - { int ret = r_mark_ncA(z); - if (ret == 0) goto lab3; + { + int ret = r_mark_ncA(z); + if (ret == 0) goto lab2; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m3 = z->l - z->c; (void)m3; - { int m4 = z->l - z->c; (void)m4; + { + int v_3 = z->l - z->c; + do { + int v_4 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lArI(z); - if (ret == 0) goto lab6; + { + int ret = r_mark_lArI(z); + if (ret == 0) goto lab4; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab5; - lab6: - z->c = z->l - m4; + break; + lab4: + z->c = z->l - v_4; z->ket = z->c; - { int m5 = z->l - z->c; (void)m5; - { int ret = r_mark_possessives(z); - if (ret == 0) goto lab9; + do { + int v_5 = z->l - z->c; + { + int ret = r_mark_possessives(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - goto lab8; - lab9: - z->c = z->l - m5; - { int ret = r_mark_sU(z); - if (ret == 0) goto lab7; + break; + lab6: + z->c = z->l - v_5; + { + int ret = r_mark_sU(z); + if (ret == 0) goto lab5; if (ret < 0) return ret; } - } - lab8: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m6 = z->l - z->c; (void)m6; + { + int v_6 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m6; goto lab10; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_6; goto lab7; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m6; goto lab10; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_6; goto lab7; } if (ret < 0) return ret; } - lab10: + lab7: ; } - goto lab5; - lab7: - z->c = z->l - m4; + break; + lab5: + z->c = z->l - v_4; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m3; goto lab4; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_3; goto lab3; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m3; goto lab4; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_3; goto lab3; } if (ret < 0) return ret; } - } - lab5: - lab4: + } while (0); + lab3: ; } - goto lab0; - lab3: - z->c = z->l - m1; + break; + lab2: + z->c = z->l - v_1; z->ket = z->c; - { int m7 = z->l - z->c; (void)m7; - { int ret = r_mark_ndA(z); - if (ret == 0) goto lab13; + do { + int v_7 = z->l - z->c; + { + int ret = r_mark_ndA(z); + if (ret == 0) goto lab9; if (ret < 0) return ret; } - goto lab12; - lab13: - z->c = z->l - m7; - { int ret = r_mark_nA(z); - if (ret == 0) goto lab11; + break; + lab9: + z->c = z->l - v_7; + { + int ret = r_mark_nA(z); + if (ret == 0) goto lab8; if (ret < 0) return ret; } - } - lab12: - { int m8 = z->l - z->c; (void)m8; - { int ret = r_mark_lArI(z); - if (ret == 0) goto lab15; + } while (0); + do { + int v_8 = z->l - z->c; + { + int ret = r_mark_lArI(z); + if (ret == 0) goto lab10; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab14; - lab15: - z->c = z->l - m8; - { int ret = r_mark_sU(z); - if (ret == 0) goto lab16; + break; + lab10: + z->c = z->l - v_8; + { + int ret = r_mark_sU(z); + if (ret == 0) goto lab11; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m9 = z->l - z->c; (void)m9; + { + int v_9 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m9; goto lab17; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_9; goto lab12; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m9; goto lab17; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_9; goto lab12; } if (ret < 0) return ret; } - lab17: + lab12: ; } - goto lab14; - lab16: - z->c = z->l - m8; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) goto lab11; + break; + lab11: + z->c = z->l - v_8; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) goto lab8; if (ret < 0) return ret; } - } - lab14: - goto lab0; - lab11: - z->c = z->l - m1; + } while (0); + break; + lab8: + z->c = z->l - v_1; z->ket = z->c; - { int m10 = z->l - z->c; (void)m10; - { int ret = r_mark_ndAn(z); - if (ret == 0) goto lab20; + do { + int v_10 = z->l - z->c; + { + int ret = r_mark_ndAn(z); + if (ret == 0) goto lab14; if (ret < 0) return ret; } - goto lab19; - lab20: - z->c = z->l - m10; - { int ret = r_mark_nU(z); - if (ret == 0) goto lab18; + break; + lab14: + z->c = z->l - v_10; + { + int ret = r_mark_nU(z); + if (ret == 0) goto lab13; if (ret < 0) return ret; } - } - lab19: - { int m11 = z->l - z->c; (void)m11; - { int ret = r_mark_sU(z); - if (ret == 0) goto lab22; + } while (0); + do { + int v_11 = z->l - z->c; + { + int ret = r_mark_sU(z); + if (ret == 0) goto lab15; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m12 = z->l - z->c; (void)m12; + { + int v_12 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m12; goto lab23; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_12; goto lab16; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m12; goto lab23; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_12; goto lab16; } if (ret < 0) return ret; } - lab23: + lab16: ; } - goto lab21; - lab22: - z->c = z->l - m11; - { int ret = r_mark_lArI(z); - if (ret == 0) goto lab18; + break; + lab15: + z->c = z->l - v_11; + { + int ret = r_mark_lArI(z); + if (ret == 0) goto lab13; if (ret < 0) return ret; } - } - lab21: - goto lab0; - lab18: - z->c = z->l - m1; + } while (0); + break; + lab13: + z->c = z->l - v_1; z->ket = z->c; - { int ret = r_mark_DAn(z); - if (ret == 0) goto lab24; + { + int ret = r_mark_DAn(z); + if (ret == 0) goto lab17; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m13 = z->l - z->c; (void)m13; + { + int v_13 = z->l - z->c; z->ket = z->c; - { int m14 = z->l - z->c; (void)m14; - { int ret = r_mark_possessives(z); - if (ret == 0) goto lab27; + do { + int v_14 = z->l - z->c; + { + int ret = r_mark_possessives(z); + if (ret == 0) goto lab19; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m15 = z->l - z->c; (void)m15; + { + int v_15 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m15; goto lab28; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_15; goto lab20; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m15; goto lab28; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_15; goto lab20; } if (ret < 0) return ret; } - lab28: + lab20: ; } - goto lab26; - lab27: - z->c = z->l - m14; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab29; + break; + lab19: + z->c = z->l - v_14; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab21; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m16 = z->l - z->c; (void)m16; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m16; goto lab30; } + { + int v_16 = z->l - z->c; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_16; goto lab22; } if (ret < 0) return ret; } - lab30: + lab22: ; } - goto lab26; - lab29: - z->c = z->l - m14; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m13; goto lab25; } + break; + lab21: + z->c = z->l - v_14; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_13; goto lab18; } if (ret < 0) return ret; } - } - lab26: - lab25: + } while (0); + lab18: ; } - goto lab0; - lab24: - z->c = z->l - m1; + break; + lab17: + z->c = z->l - v_1; z->ket = z->c; - { int m17 = z->l - z->c; (void)m17; - { int ret = r_mark_nUn(z); - if (ret == 0) goto lab33; + do { + int v_17 = z->l - z->c; + { + int ret = r_mark_nUn(z); + if (ret == 0) goto lab24; if (ret < 0) return ret; } - goto lab32; - lab33: - z->c = z->l - m17; - { int ret = r_mark_ylA(z); - if (ret == 0) goto lab31; + break; + lab24: + z->c = z->l - v_17; + { + int ret = r_mark_ylA(z); + if (ret == 0) goto lab23; if (ret < 0) return ret; } - } - lab32: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m18 = z->l - z->c; (void)m18; - { int m19 = z->l - z->c; (void)m19; + { + int v_18 = z->l - z->c; + do { + int v_19 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) goto lab36; + { + int ret = r_mark_lAr(z); + if (ret == 0) goto lab26; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) goto lab36; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) goto lab26; if (ret < 0) return ret; } - goto lab35; - lab36: - z->c = z->l - m19; + break; + lab26: + z->c = z->l - v_19; z->ket = z->c; - { int m20 = z->l - z->c; (void)m20; - { int ret = r_mark_possessives(z); - if (ret == 0) goto lab39; + do { + int v_20 = z->l - z->c; + { + int ret = r_mark_possessives(z); + if (ret == 0) goto lab28; if (ret < 0) return ret; } - goto lab38; - lab39: - z->c = z->l - m20; - { int ret = r_mark_sU(z); - if (ret == 0) goto lab37; + break; + lab28: + z->c = z->l - v_20; + { + int ret = r_mark_sU(z); + if (ret == 0) goto lab27; if (ret < 0) return ret; } - } - lab38: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m21 = z->l - z->c; (void)m21; + { + int v_21 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m21; goto lab40; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_21; goto lab29; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m21; goto lab40; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_21; goto lab29; } if (ret < 0) return ret; } - lab40: + lab29: ; } - goto lab35; - lab37: - z->c = z->l - m19; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m18; goto lab34; } + break; + lab27: + z->c = z->l - v_19; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_18; goto lab25; } if (ret < 0) return ret; } - } - lab35: - lab34: + } while (0); + lab25: ; } - goto lab0; - lab31: - z->c = z->l - m1; + break; + lab23: + z->c = z->l - v_1; z->ket = z->c; - { int ret = r_mark_lArI(z); - if (ret == 0) goto lab41; + { + int ret = r_mark_lArI(z); + if (ret == 0) goto lab30; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab0; - lab41: - z->c = z->l - m1; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) goto lab42; + break; + lab30: + z->c = z->l - v_1; + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) goto lab31; if (ret < 0) return ret; } - goto lab0; - lab42: - z->c = z->l - m1; + break; + lab31: + z->c = z->l - v_1; z->ket = z->c; - { int m22 = z->l - z->c; (void)m22; - { int ret = r_mark_DA(z); - if (ret == 0) goto lab45; + do { + int v_22 = z->l - z->c; + { + int ret = r_mark_DA(z); + if (ret == 0) goto lab33; if (ret < 0) return ret; } - goto lab44; - lab45: - z->c = z->l - m22; - { int ret = r_mark_yU(z); - if (ret == 0) goto lab46; + break; + lab33: + z->c = z->l - v_22; + { + int ret = r_mark_yU(z); + if (ret == 0) goto lab34; if (ret < 0) return ret; } - goto lab44; - lab46: - z->c = z->l - m22; - { int ret = r_mark_yA(z); - if (ret == 0) goto lab43; + break; + lab34: + z->c = z->l - v_22; + { + int ret = r_mark_yA(z); + if (ret == 0) goto lab32; if (ret < 0) return ret; } - } - lab44: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m23 = z->l - z->c; (void)m23; + { + int v_23 = z->l - z->c; z->ket = z->c; - { int m24 = z->l - z->c; (void)m24; - { int ret = r_mark_possessives(z); - if (ret == 0) goto lab49; + do { + int v_24 = z->l - z->c; + { + int ret = r_mark_possessives(z); + if (ret == 0) goto lab36; if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m25 = z->l - z->c; (void)m25; + { + int v_25 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m25; goto lab50; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_25; goto lab37; } if (ret < 0) return ret; } - lab50: + lab37: ; } - goto lab48; - lab49: - z->c = z->l - m24; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m23; goto lab47; } + break; + lab36: + z->c = z->l - v_24; + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_23; goto lab35; } if (ret < 0) return ret; } - } - lab48: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m23; goto lab47; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_23; goto lab35; } if (ret < 0) return ret; } - lab47: + lab35: ; } - goto lab0; - lab43: - z->c = z->l - m1; + break; + lab32: + z->c = z->l - v_1; z->ket = z->c; - { int m26 = z->l - z->c; (void)m26; - { int ret = r_mark_possessives(z); - if (ret == 0) goto lab52; + do { + int v_26 = z->l - z->c; + { + int ret = r_mark_possessives(z); + if (ret == 0) goto lab38; if (ret < 0) return ret; } - goto lab51; - lab52: - z->c = z->l - m26; - { int ret = r_mark_sU(z); + break; + lab38: + z->c = z->l - v_26; + { + int ret = r_mark_sU(z); if (ret <= 0) return ret; } - } - lab51: + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int m27 = z->l - z->c; (void)m27; + { + int v_27 = z->l - z->c; z->ket = z->c; - { int ret = r_mark_lAr(z); - if (ret == 0) { z->c = z->l - m27; goto lab53; } + { + int ret = r_mark_lAr(z); + if (ret == 0) { z->c = z->l - v_27; goto lab39; } if (ret < 0) return ret; } z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - { int ret = r_stem_suffix_chain_before_ki(z); - if (ret == 0) { z->c = z->l - m27; goto lab53; } + { + int ret = r_stem_suffix_chain_before_ki(z); + if (ret == 0) { z->c = z->l - v_27; goto lab39; } if (ret < 0) return ret; } - lab53: + lab39: ; } - } -lab0: + } while (0); return 1; } static int r_post_process_last_consonants(struct SN_env * z) { int among_var; z->ket = z->c; - among_var = find_among_b(z, a_23, 4); + among_var = find_among_b(z, a_23, 4, 0); if (!among_var) return 0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 1, s_5); + { + int ret = slice_from_s(z, 1, s_5); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 2, s_6); + { + int ret = slice_from_s(z, 2, s_6); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 1, s_7); + { + int ret = slice_from_s(z, 1, s_7); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 1, s_8); + { + int ret = slice_from_s(z, 1, s_8); if (ret < 0) return ret; } break; @@ -1905,86 +1982,90 @@ static int r_post_process_last_consonants(struct SN_env * z) { static int r_append_U_to_stems_ending_with_d_or_g(struct SN_env * z) { z->ket = z->c; z->bra = z->c; - { int m1 = z->l - z->c; (void)m1; - if (z->c <= z->lb || z->p[z->c - 1] != 'd') goto lab1; + do { + int v_1 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'd') goto lab0; z->c--; - goto lab0; - lab1: - z->c = z->l - m1; + break; + lab0: + z->c = z->l - v_1; if (z->c <= z->lb || z->p[z->c - 1] != 'g') return 0; z->c--; - } -lab0: - + } while (0); if (out_grouping_b_U(z, g_vowel, 97, 305, 1) < 0) return 0; - { int m2 = z->l - z->c; (void)m2; - { int m3 = z->l - z->c; (void)m3; - if (z->c <= z->lb || z->p[z->c - 1] != 'a') goto lab5; + do { + int v_2 = z->l - z->c; + do { + int v_3 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'a') goto lab2; z->c--; - goto lab4; - lab5: - z->c = z->l - m3; - if (!(eq_s_b(z, 2, s_9))) goto lab3; - } - lab4: - { int ret = slice_from_s(z, 2, s_10); + break; + lab2: + z->c = z->l - v_3; + if (!(eq_s_b(z, 2, s_9))) goto lab1; + } while (0); + { + int ret = slice_from_s(z, 2, s_10); if (ret < 0) return ret; } - goto lab2; - lab3: - z->c = z->l - m2; - { int m4 = z->l - z->c; (void)m4; - if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab8; + break; + lab1: + z->c = z->l - v_2; + do { + int v_4 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'e') goto lab4; z->c--; - goto lab7; - lab8: - z->c = z->l - m4; - if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab6; + break; + lab4: + z->c = z->l - v_4; + if (z->c <= z->lb || z->p[z->c - 1] != 'i') goto lab3; z->c--; - } - lab7: - { int ret = slice_from_s(z, 1, s_11); + } while (0); + { + int ret = slice_from_s(z, 1, s_11); if (ret < 0) return ret; } - goto lab2; - lab6: - z->c = z->l - m2; - { int m5 = z->l - z->c; (void)m5; - if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab11; + break; + lab3: + z->c = z->l - v_2; + do { + int v_5 = z->l - z->c; + if (z->c <= z->lb || z->p[z->c - 1] != 'o') goto lab6; z->c--; - goto lab10; - lab11: - z->c = z->l - m5; - if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab9; + break; + lab6: + z->c = z->l - v_5; + if (z->c <= z->lb || z->p[z->c - 1] != 'u') goto lab5; z->c--; - } - lab10: - { int ret = slice_from_s(z, 1, s_12); + } while (0); + { + int ret = slice_from_s(z, 1, s_12); if (ret < 0) return ret; } - goto lab2; - lab9: - z->c = z->l - m2; - { int m6 = z->l - z->c; (void)m6; - if (!(eq_s_b(z, 2, s_13))) goto lab13; - goto lab12; - lab13: - z->c = z->l - m6; + break; + lab5: + z->c = z->l - v_2; + do { + int v_6 = z->l - z->c; + if (!(eq_s_b(z, 2, s_13))) goto lab7; + break; + lab7: + z->c = z->l - v_6; if (!(eq_s_b(z, 2, s_14))) return 0; - } - lab12: - { int ret = slice_from_s(z, 2, s_15); + } while (0); + { + int ret = slice_from_s(z, 2, s_15); if (ret < 0) return ret; } - } -lab2: + } while (0); return 1; } static int r_is_reserved_word(struct SN_env * z) { if (!(eq_s_b(z, 2, s_16))) return 0; - { int m1 = z->l - z->c; (void)m1; - if (!(eq_s_b(z, 3, s_17))) { z->c = z->l - m1; goto lab0; } + { + int v_1 = z->l - z->c; + if (!(eq_s_b(z, 3, s_17))) { z->c = z->l - v_1; goto lab0; } lab0: ; } @@ -1993,36 +2074,76 @@ static int r_is_reserved_word(struct SN_env * z) { } static int r_remove_proper_noun_suffix(struct SN_env * z) { - { int c1 = z->c; - while(1) { - int c2 = z->c; - if (z->c == z->l || z->p[z->c] != '\'') goto lab1; - z->c++; - z->c = c2; + { + int v_1 = z->c; + z->bra = z->c; + while (1) { + int v_2 = z->c; + { + int v_3 = z->c; + if (z->c == z->l || z->p[z->c] != '\'') goto lab2; + z->c++; + goto lab1; + lab2: + z->c = v_3; + } + z->c = v_2; break; lab1: - z->c = c2; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_2; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab0; z->c = ret; } } + z->ket = z->c; + { + int ret = slice_del(z); + if (ret < 0) return ret; + } + lab0: + z->c = v_1; + } + { + int v_4 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 2); + if (ret < 0) goto lab3; + z->c = ret; + } + while (1) { + int v_5 = z->c; + if (z->c == z->l || z->p[z->c] != '\'') goto lab4; + z->c++; + z->c = v_5; + break; + lab4: + z->c = v_5; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); + if (ret < 0) goto lab3; + z->c = ret; + } + } z->bra = z->c; z->c = z->l; z->ket = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - lab0: - z->c = c1; + lab3: + z->c = v_4; } return 1; } static int r_more_than_one_syllable_word(struct SN_env * z) { - { int c_test1 = z->c; - { int i; for (i = 2; i > 0; i--) - { + { + int v_1 = z->c; + { + int i; for (i = 2; i > 0; i--) { { int ret = out_grouping_U(z, g_vowel, 97, 305, 1); if (ret < 0) return 0; @@ -2030,70 +2151,84 @@ static int r_more_than_one_syllable_word(struct SN_env * z) { } } } - z->c = c_test1; + z->c = v_1; } return 1; } static int r_postlude(struct SN_env * z) { z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_is_reserved_word(z); + { + int v_1 = z->l - z->c; + { + int ret = r_is_reserved_word(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } return 0; lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m2 = z->l - z->c; (void)m2; - { int ret = r_append_U_to_stems_ending_with_d_or_g(z); + { + int v_2 = z->l - z->c; + { + int ret = r_append_U_to_stems_ending_with_d_or_g(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } - { int m3 = z->l - z->c; (void)m3; - { int ret = r_post_process_last_consonants(z); + { + int v_3 = z->l - z->c; + { + int ret = r_post_process_last_consonants(z); if (ret < 0) return ret; } - z->c = z->l - m3; + z->c = z->l - v_3; } z->c = z->lb; return 1; } extern int turkish_UTF_8_stem(struct SN_env * z) { - - { int ret = r_remove_proper_noun_suffix(z); + { + int ret = r_remove_proper_noun_suffix(z); if (ret < 0) return ret; } - { int ret = r_more_than_one_syllable_word(z); + { + int ret = r_more_than_one_syllable_word(z); if (ret <= 0) return ret; } z->lb = z->c; z->c = z->l; - - { int m1 = z->l - z->c; (void)m1; - { int ret = r_stem_nominal_verb_suffixes(z); + { + int v_1 = z->l - z->c; + { + int ret = r_stem_nominal_verb_suffixes(z); if (ret < 0) return ret; } - z->c = z->l - m1; + z->c = z->l - v_1; } - if (!(z->I[0])) return 0; - { int m2 = z->l - z->c; (void)m2; - { int ret = r_stem_noun_suffixes(z); + if (!((SN_local *)z)->b_continue_stemming_noun_suffixes) return 0; + { + int v_2 = z->l - z->c; + { + int ret = r_stem_noun_suffixes(z); if (ret < 0) return ret; } - z->c = z->l - m2; + z->c = z->l - v_2; } z->c = z->lb; - { int ret = r_postlude(z); - if (ret <= 0) return ret; - } - return 1; + return r_postlude(z); } -extern struct SN_env * turkish_UTF_8_create_env(void) { return SN_create_env(0, 1); } +extern struct SN_env * turkish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->b_continue_stemming_noun_suffixes = 0; + } + return z; +} -extern void turkish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void turkish_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/stem_UTF_8_yiddish.c b/src/backend/snowball/libstemmer/stem_UTF_8_yiddish.c index d98347407d606..e2415abae9f50 100644 --- a/src/backend/snowball/libstemmer/stem_UTF_8_yiddish.c +++ b/src/backend/snowball/libstemmer/stem_UTF_8_yiddish.c @@ -1,6 +1,17 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from yiddish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ -#include "header.h" +#include "stem_UTF_8_yiddish.h" + +#include + +#include "snowball_runtime.h" + +struct SN_local { + struct SN_env z; + int i_p1; +}; + +typedef struct SN_local SN_local; #ifdef __cplusplus extern "C" { @@ -9,23 +20,101 @@ extern int yiddish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif + static int r_standard_suffix(struct SN_env * z); static int r_R1plus3(struct SN_env * z); static int r_R1(struct SN_env * z); static int r_mark_regions(struct SN_env * z); static int r_prelude(struct SN_env * z); -#ifdef __cplusplus -extern "C" { -#endif - - -extern struct SN_env * yiddish_UTF_8_create_env(void); -extern void yiddish_UTF_8_close_env(struct SN_env * z); +static const symbol s_0[] = { 0xD6, 0xBC }; +static const symbol s_1[] = { 0xD7, 0xB0 }; +static const symbol s_2[] = { 0xD6, 0xB4 }; +static const symbol s_3[] = { 0xD7, 0xB1 }; +static const symbol s_4[] = { 0xD6, 0xB4 }; +static const symbol s_5[] = { 0xD7, 0xB2 }; +static const symbol s_6[] = { 0xD7, 0x9B }; +static const symbol s_7[] = { 0xD7, 0x9E }; +static const symbol s_8[] = { 0xD7, 0xA0 }; +static const symbol s_9[] = { 0xD7, 0xA4 }; +static const symbol s_10[] = { 0xD7, 0xA6 }; +static const symbol s_11[] = { 0xD7, 0x92, 0xD7, 0xA2 }; +static const symbol s_12[] = { 0xD7, 0x9C, 0xD7, 0x98 }; +static const symbol s_13[] = { 0xD7, 0x91, 0xD7, 0xA0 }; +static const symbol s_14[] = { 'G', 'E' }; +static const symbol s_15[] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0x92, 0xD7, 0xA0 }; +static const symbol s_16[] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0xA7, 0xD7, 0x98 }; +static const symbol s_17[] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0xA7, 0xD7, 0xA0 }; +static const symbol s_18[] = { 0xD7, 0x92, 0xD7, 0xA2, 0xD7, 0x91, 0xD7, 0xA0 }; +static const symbol s_19[] = { 0xD7, 0x92, 0xD7, 0xA2 }; +static const symbol s_20[] = { 'G', 'E' }; +static const symbol s_21[] = { 0xD7, 0xA6, 0xD7, 0x95 }; +static const symbol s_22[] = { 'T', 'S', 'U' }; +static const symbol s_23[] = { 0xD7, 0x99, 0xD7, 0xA2 }; +static const symbol s_24[] = { 0xD7, 0x92, 0xD7, 0xB2 }; +static const symbol s_25[] = { 0xD7, 0xA0, 0xD7, 0xA2, 0xD7, 0x9E }; +static const symbol s_26[] = { 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0x93 }; +static const symbol s_27[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x98 }; +static const symbol s_28[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0xA1 }; +static const symbol s_29[] = { 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x96 }; +static const symbol s_30[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_31[] = { 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x98 }; +static const symbol s_32[] = { 0xD7, 0xA7, 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_33[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_34[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0xA1 }; +static const symbol s_35[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x92 }; +static const symbol s_36[] = { 0xD7, 0xA9, 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0xA1 }; +static const symbol s_37[] = { 0xD7, 0xA9, 0xD7, 0xA0, 0xD7, 0xB2, 0xD7, 0x93 }; +static const symbol s_38[] = { 0xD7, 0xA9, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_39[] = { 0xD7, 0x91, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x93 }; +static const symbol s_40[] = { 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0x98, 0xD7, 0xA9 }; +static const symbol s_41[] = { 0xD7, 0x96, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_42[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0xA7 }; +static const symbol s_43[] = { 0xD7, 0xA6, 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_44[] = { 0xD7, 0xA9, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_45[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x92 }; +static const symbol s_46[] = { 0xD7, 0x94, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_47[] = { 0xD7, 0xA4, 0xD7, 0x90, 0xD7, 0xA8, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA8 }; +static const symbol s_48[] = { 0xD7, 0xA9, 0xD7, 0x98, 0xD7, 0xB2 }; +static const symbol s_49[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xA2, 0xD7, 0xA8 }; +static const symbol s_50[] = { 0xD7, 0x98 }; +static const symbol s_51[] = { 0xD7, 0x91, 0xD7, 0xA8, 0xD7, 0x90, 0xD7, 0x9B }; +static const symbol s_52[] = { 0xD7, 0x92, 0xD7, 0xA2 }; +static const symbol s_53[] = { 0xD7, 0x91, 0xD7, 0xA8, 0xD7, 0xA2, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_54[] = { 0xD7, 0x92, 0xD7, 0xB2 }; +static const symbol s_55[] = { 0xD7, 0xA0, 0xD7, 0xA2, 0xD7, 0x9E }; +static const symbol s_56[] = { 0xD7, 0xA9, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_57[] = { 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0x93 }; +static const symbol s_58[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x98 }; +static const symbol s_59[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0xA1 }; +static const symbol s_60[] = { 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x96 }; +static const symbol s_61[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_62[] = { 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x98 }; +static const symbol s_63[] = { 0xD7, 0xA7, 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_64[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_65[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0xA1 }; +static const symbol s_66[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x92 }; +static const symbol s_67[] = { 0xD7, 0xA9, 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0xA1 }; +static const symbol s_68[] = { 0xD7, 0xA9, 0xD7, 0xA0, 0xD7, 0xB2, 0xD7, 0x93 }; +static const symbol s_69[] = { 0xD7, 0x91, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x93 }; +static const symbol s_70[] = { 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0x98, 0xD7, 0xA9 }; +static const symbol s_71[] = { 0xD7, 0x96, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_72[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0xA7 }; +static const symbol s_73[] = { 0xD7, 0xA6, 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_74[] = { 0xD7, 0xA9, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_75[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x92 }; +static const symbol s_76[] = { 0xD7, 0x94, 0xD7, 0xB2, 0xD7, 0x91 }; +static const symbol s_77[] = { 0xD7, 0xA4, 0xD7, 0x90, 0xD7, 0xA8, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA8 }; +static const symbol s_78[] = { 0xD7, 0xA9, 0xD7, 0x98, 0xD7, 0xB2 }; +static const symbol s_79[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xA2, 0xD7, 0xA8 }; +static const symbol s_80[] = { 0xD7, 0x91, 0xD7, 0xA8, 0xD7, 0xA2, 0xD7, 0xA0, 0xD7, 0x92 }; +static const symbol s_81[] = { 0xD7, 0x94 }; +static const symbol s_82[] = { 0xD7, 0x92 }; +static const symbol s_83[] = { 0xD7, 0xA9 }; +static const symbol s_84[] = { 0xD7, 0x99, 0xD7, 0xA1 }; +static const symbol s_85[] = { 'G', 'E' }; +static const symbol s_86[] = { 'T', 'S', 'U' }; -#ifdef __cplusplus -} -#endif static const symbol s_0_0[4] = { 0xD7, 0x95, 0xD7, 0x95 }; static const symbol s_0_1[4] = { 0xD7, 0x95, 0xD7, 0x99 }; static const symbol s_0_2[4] = { 0xD7, 0x99, 0xD7, 0x99 }; @@ -34,17 +123,15 @@ static const symbol s_0_4[2] = { 0xD7, 0x9D }; static const symbol s_0_5[2] = { 0xD7, 0x9F }; static const symbol s_0_6[2] = { 0xD7, 0xA3 }; static const symbol s_0_7[2] = { 0xD7, 0xA5 }; - -static const struct among a_0[8] = -{ -{ 4, s_0_0, -1, 1, 0}, -{ 4, s_0_1, -1, 2, 0}, -{ 4, s_0_2, -1, 3, 0}, -{ 2, s_0_3, -1, 4, 0}, -{ 2, s_0_4, -1, 5, 0}, -{ 2, s_0_5, -1, 6, 0}, -{ 2, s_0_6, -1, 7, 0}, -{ 2, s_0_7, -1, 8, 0} +static const struct among a_0[8] = { +{ 4, s_0_0, 0, 1, 0}, +{ 4, s_0_1, 0, 2, 0}, +{ 4, s_0_2, 0, 3, 0}, +{ 2, s_0_3, 0, 4, 0}, +{ 2, s_0_4, 0, 5, 0}, +{ 2, s_0_5, 0, 6, 0}, +{ 2, s_0_6, 0, 7, 0}, +{ 2, s_0_7, 0, 8, 0} }; static const symbol s_1_0[10] = { 0xD7, 0x90, 0xD7, 0x93, 0xD7, 0x95, 0xD7, 0xA8, 0xD7, 0x9B }; @@ -87,62 +174,58 @@ static const symbol s_1_36[14] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0x96, 0xD7, 0x9 static const symbol s_1_37[10] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0xA0, 0xD7, 0xB1, 0xD7, 0xA4 }; static const symbol s_1_38[10] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0xA8, 0xD7, 0x99, 0xD7, 0xA7 }; static const symbol s_1_39[4] = { 0xD7, 0xA6, 0xD7, 0xA2 }; - -static const struct among a_1[40] = -{ -{ 10, s_1_0, -1, 1, 0}, -{ 8, s_1_1, -1, 1, 0}, -{ 8, s_1_2, -1, 1, 0}, -{ 8, s_1_3, -1, 1, 0}, -{ 6, s_1_4, -1, 1, 0}, -{ 12, s_1_5, -1, 1, 0}, -{ 10, s_1_6, -1, 1, 0}, -{ 4, s_1_7, -1, 1, 0}, -{ 6, s_1_8, 7, 1, 0}, -{ 14, s_1_9, 8, 1, 0}, -{ 12, s_1_10, 7, 1, 0}, -{ 4, s_1_11, -1, 1, 0}, -{ 8, s_1_12, 11, 1, 0}, -{ 10, s_1_13, -1, 1, 0}, -{ 8, s_1_14, -1, 1, 0}, -{ 8, s_1_15, -1, 1, 0}, -{ 14, s_1_16, -1, 1, 0}, -{ 12, s_1_17, -1, 1, 0}, -{ 8, s_1_18, -1, 1, 0}, -{ 8, s_1_19, -1, 1, 0}, -{ 8, s_1_20, -1, 1, 0}, -{ 8, s_1_21, -1, 1, 0}, -{ 6, s_1_22, -1, 1, 0}, -{ 6, s_1_23, -1, 1, 0}, -{ 6, s_1_24, -1, 1, 0}, -{ 4, s_1_25, -1, 1, 0}, -{ 4, s_1_26, -1, 1, 0}, -{ 8, s_1_27, -1, 1, 0}, -{ 6, s_1_28, -1, 1, 0}, -{ 6, s_1_29, -1, 1, 0}, -{ 6, s_1_30, -1, 1, 0}, -{ 6, s_1_31, -1, 1, 0}, -{ 10, s_1_32, 31, 1, 0}, -{ 10, s_1_33, 31, 1, 0}, -{ 16, s_1_34, -1, 1, 0}, -{ 4, s_1_35, -1, 1, 0}, -{ 14, s_1_36, 35, 1, 0}, -{ 10, s_1_37, 35, 1, 0}, -{ 10, s_1_38, 35, 1, 0}, -{ 4, s_1_39, -1, 1, 0} +static const struct among a_1[40] = { +{ 10, s_1_0, 0, 1, 0}, +{ 8, s_1_1, 0, 1, 0}, +{ 8, s_1_2, 0, 1, 0}, +{ 8, s_1_3, 0, 1, 0}, +{ 6, s_1_4, 0, 1, 0}, +{ 12, s_1_5, 0, 1, 0}, +{ 10, s_1_6, 0, 1, 0}, +{ 4, s_1_7, 0, 1, 0}, +{ 6, s_1_8, -1, 1, 0}, +{ 14, s_1_9, -1, 1, 0}, +{ 12, s_1_10, -3, 1, 0}, +{ 4, s_1_11, 0, 1, 0}, +{ 8, s_1_12, -1, 1, 0}, +{ 10, s_1_13, 0, 1, 0}, +{ 8, s_1_14, 0, 1, 0}, +{ 8, s_1_15, 0, 1, 0}, +{ 14, s_1_16, 0, 1, 0}, +{ 12, s_1_17, 0, 1, 0}, +{ 8, s_1_18, 0, 1, 0}, +{ 8, s_1_19, 0, 1, 0}, +{ 8, s_1_20, 0, 1, 0}, +{ 8, s_1_21, 0, 1, 0}, +{ 6, s_1_22, 0, 1, 0}, +{ 6, s_1_23, 0, 1, 0}, +{ 6, s_1_24, 0, 1, 0}, +{ 4, s_1_25, 0, 1, 0}, +{ 4, s_1_26, 0, 1, 0}, +{ 8, s_1_27, 0, 1, 0}, +{ 6, s_1_28, 0, 1, 0}, +{ 6, s_1_29, 0, 1, 0}, +{ 6, s_1_30, 0, 1, 0}, +{ 6, s_1_31, 0, 1, 0}, +{ 10, s_1_32, -1, 1, 0}, +{ 10, s_1_33, -2, 1, 0}, +{ 16, s_1_34, 0, 1, 0}, +{ 4, s_1_35, 0, 1, 0}, +{ 14, s_1_36, -1, 1, 0}, +{ 10, s_1_37, -2, 1, 0}, +{ 10, s_1_38, -3, 1, 0}, +{ 4, s_1_39, 0, 1, 0} }; static const symbol s_2_0[6] = { 0xD7, 0x93, 0xD7, 0x96, 0xD7, 0xA9 }; static const symbol s_2_1[6] = { 0xD7, 0xA9, 0xD7, 0x98, 0xD7, 0xA8 }; static const symbol s_2_2[6] = { 0xD7, 0xA9, 0xD7, 0x98, 0xD7, 0xA9 }; static const symbol s_2_3[6] = { 0xD7, 0xA9, 0xD7, 0xA4, 0xD7, 0xA8 }; - -static const struct among a_2[4] = -{ -{ 6, s_2_0, -1, -1, 0}, -{ 6, s_2_1, -1, -1, 0}, -{ 6, s_2_2, -1, -1, 0}, -{ 6, s_2_3, -1, -1, 0} +static const struct among a_2[4] = { +{ 6, s_2_0, 0, -1, 0}, +{ 6, s_2_1, 0, -1, 0}, +{ 6, s_2_2, 0, -1, 0}, +{ 6, s_2_3, 0, -1, 0} }; static const symbol s_3_0[8] = { 0xD7, 0xA7, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0x91 }; @@ -171,35 +254,33 @@ static const symbol s_3_22[10] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0x95, 0xD7, 0xA static const symbol s_3_23[12] = { 0xD7, 0xA4, 0xD7, 0x90, 0xD7, 0xA8, 0xD7, 0x9C, 0xD7, 0xB1, 0xD7, 0xA8 }; static const symbol s_3_24[8] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xB1, 0xD7, 0xA8 }; static const symbol s_3_25[8] = { 0xD7, 0xB0, 0xD7, 0x95, 0xD7, 0x98, 0xD7, 0xA9 }; - -static const struct among a_3[26] = -{ -{ 8, s_3_0, -1, 9, 0}, -{ 6, s_3_1, -1, 10, 0}, -{ 8, s_3_2, 1, 7, 0}, -{ 8, s_3_3, 1, 15, 0}, -{ 6, s_3_4, -1, 23, 0}, -{ 8, s_3_5, -1, 12, 0}, -{ 8, s_3_6, -1, 1, 0}, -{ 8, s_3_7, -1, 18, 0}, -{ 10, s_3_8, -1, 21, 0}, -{ 10, s_3_9, -1, 20, 0}, -{ 6, s_3_10, -1, 22, 0}, -{ 8, s_3_11, -1, 16, 0}, -{ 6, s_3_12, -1, 6, 0}, -{ 6, s_3_13, -1, 4, 0}, -{ 6, s_3_14, -1, 8, 0}, -{ 6, s_3_15, -1, 3, 0}, -{ 8, s_3_16, -1, 14, 0}, -{ 6, s_3_17, -1, 2, 0}, -{ 8, s_3_18, -1, 25, 0}, -{ 6, s_3_19, -1, 5, 0}, -{ 8, s_3_20, -1, 13, 0}, -{ 6, s_3_21, -1, 11, 0}, -{ 10, s_3_22, -1, 19, 0}, -{ 12, s_3_23, -1, 24, 0}, -{ 8, s_3_24, -1, 26, 0}, -{ 8, s_3_25, -1, 17, 0} +static const struct among a_3[26] = { +{ 8, s_3_0, 0, 9, 0}, +{ 6, s_3_1, 0, 10, 0}, +{ 8, s_3_2, -1, 7, 0}, +{ 8, s_3_3, -2, 15, 0}, +{ 6, s_3_4, 0, 23, 0}, +{ 8, s_3_5, 0, 12, 0}, +{ 8, s_3_6, 0, 1, 0}, +{ 8, s_3_7, 0, 18, 0}, +{ 10, s_3_8, 0, 21, 0}, +{ 10, s_3_9, 0, 20, 0}, +{ 6, s_3_10, 0, 22, 0}, +{ 8, s_3_11, 0, 16, 0}, +{ 6, s_3_12, 0, 6, 0}, +{ 6, s_3_13, 0, 4, 0}, +{ 6, s_3_14, 0, 8, 0}, +{ 6, s_3_15, 0, 3, 0}, +{ 8, s_3_16, 0, 14, 0}, +{ 6, s_3_17, 0, 2, 0}, +{ 8, s_3_18, 0, 25, 0}, +{ 6, s_3_19, 0, 5, 0}, +{ 8, s_3_20, 0, 13, 0}, +{ 6, s_3_21, 0, 11, 0}, +{ 10, s_3_22, 0, 19, 0}, +{ 12, s_3_23, 0, 24, 0}, +{ 8, s_3_24, 0, 26, 0}, +{ 8, s_3_25, 0, 17, 0} }; static const symbol s_4_0[6] = { 0xD7, 0x95, 0xD7, 0xA0, 0xD7, 0x92 }; @@ -281,88 +362,86 @@ static const symbol s_4_75[8] = { 0xD7, 0xA2, 0xD7, 0x98, 0xD7, 0xA2, 0xD7, 0xA8 static const symbol s_4_76[8] = { 0xD7, 0xA2, 0xD7, 0xA0, 0xD7, 0xA2, 0xD7, 0xA8 }; static const symbol s_4_77[10] = { 0xD7, 0x98, 0xD7, 0xA2, 0xD7, 0xA0, 0xD7, 0xA2, 0xD7, 0xA8 }; static const symbol s_4_78[4] = { 0xD7, 0x95, 0xD7, 0xAA }; - -static const struct among a_4[79] = -{ -{ 6, s_4_0, -1, 1, 0}, -{ 6, s_4_1, -1, 1, 0}, -{ 2, s_4_2, -1, 1, 0}, -{ 10, s_4_3, 2, 31, 0}, -{ 4, s_4_4, 2, 1, 0}, -{ 6, s_4_5, 4, 33, 0}, -{ 4, s_4_6, 2, 1, 0}, -{ 8, s_4_7, 2, 1, 0}, -{ 6, s_4_8, 2, 1, 0}, -{ 6, s_4_9, 2, 1, 0}, -{ 8, s_4_10, 9, 1, 0}, -{ 6, s_4_11, -1, 1, 0}, -{ 8, s_4_12, 11, 1, 0}, -{ 6, s_4_13, -1, 1, 0}, -{ 4, s_4_14, -1, 1, 0}, -{ 4, s_4_15, -1, 1, 0}, -{ 8, s_4_16, 15, 3, 0}, -{ 10, s_4_17, 16, 4, 0}, -{ 2, s_4_18, -1, 1, 0}, -{ 10, s_4_19, 18, 14, 0}, -{ 8, s_4_20, 18, 15, 0}, -{ 10, s_4_21, 20, 12, 0}, -{ 10, s_4_22, 20, 7, 0}, -{ 8, s_4_23, 18, 27, 0}, -{ 10, s_4_24, 18, 17, 0}, -{ 10, s_4_25, 18, 22, 0}, -{ 12, s_4_26, 18, 25, 0}, -{ 12, s_4_27, 18, 24, 0}, -{ 8, s_4_28, 18, 26, 0}, -{ 10, s_4_29, 18, 20, 0}, -{ 8, s_4_30, 18, 11, 0}, -{ 4, s_4_31, 18, 4, 0}, -{ 10, s_4_32, 31, 9, 0}, -{ 10, s_4_33, 31, 13, 0}, -{ 10, s_4_34, 31, 8, 0}, -{ 10, s_4_35, 31, 19, 0}, -{ 6, s_4_36, 31, 1, 0}, -{ 8, s_4_37, 36, 1, 0}, -{ 6, s_4_38, 31, 1, 0}, -{ 10, s_4_39, 18, 10, 0}, -{ 10, s_4_40, 18, 18, 0}, -{ 10, s_4_41, 18, 16, 0}, -{ 4, s_4_42, 18, 1, 0}, -{ 12, s_4_43, 42, 5, 0}, -{ 8, s_4_44, 42, 1, 0}, -{ 10, s_4_45, 42, 6, 0}, -{ 10, s_4_46, 42, 1, 0}, -{ 12, s_4_47, 42, 29, 0}, -{ 12, s_4_48, 18, 23, 0}, -{ 14, s_4_49, 18, 28, 0}, -{ 10, s_4_50, 18, 30, 0}, -{ 10, s_4_51, 18, 21, 0}, -{ 6, s_4_52, 18, 5, 0}, -{ 2, s_4_53, -1, 1, 0}, -{ 4, s_4_54, 53, 4, 0}, -{ 6, s_4_55, 54, 1, 0}, -{ 4, s_4_56, 53, 1, 0}, -{ 6, s_4_57, 56, 4, 0}, -{ 6, s_4_58, 56, 3, 0}, -{ 4, s_4_59, 53, 1, 0}, -{ 6, s_4_60, 59, 2, 0}, -{ 8, s_4_61, 59, 1, 0}, -{ 6, s_4_62, 53, 1, 0}, -{ 10, s_4_63, 62, 1, 0}, -{ 2, s_4_64, -1, 1, 0}, -{ 4, s_4_65, 64, 4, 0}, -{ 6, s_4_66, 65, 1, 0}, -{ 6, s_4_67, 65, 1, 0}, -{ 4, s_4_68, 64, -1, 0}, -{ 6, s_4_69, 64, 1, 0}, -{ 6, s_4_70, 64, 3, 0}, -{ 8, s_4_71, 70, 4, 0}, -{ 4, s_4_72, -1, 1, 0}, -{ 6, s_4_73, 72, 4, 0}, -{ 8, s_4_74, 73, 1, 0}, -{ 8, s_4_75, 73, 1, 0}, -{ 8, s_4_76, 72, 3, 0}, -{ 10, s_4_77, 76, 4, 0}, -{ 4, s_4_78, -1, 32, 0} +static const struct among a_4[79] = { +{ 6, s_4_0, 0, 1, 0}, +{ 6, s_4_1, 0, 1, 0}, +{ 2, s_4_2, 0, 1, 0}, +{ 10, s_4_3, -1, 31, 0}, +{ 4, s_4_4, -2, 1, 0}, +{ 6, s_4_5, -1, 33, 0}, +{ 4, s_4_6, -4, 1, 0}, +{ 8, s_4_7, -5, 1, 0}, +{ 6, s_4_8, -6, 1, 0}, +{ 6, s_4_9, -7, 1, 0}, +{ 8, s_4_10, -1, 1, 0}, +{ 6, s_4_11, 0, 1, 0}, +{ 8, s_4_12, -1, 1, 0}, +{ 6, s_4_13, 0, 1, 0}, +{ 4, s_4_14, 0, 1, 0}, +{ 4, s_4_15, 0, 1, 0}, +{ 8, s_4_16, -1, 3, 0}, +{ 10, s_4_17, -1, 4, 0}, +{ 2, s_4_18, 0, 1, 0}, +{ 10, s_4_19, -1, 14, 0}, +{ 8, s_4_20, -2, 15, 0}, +{ 10, s_4_21, -1, 12, 0}, +{ 10, s_4_22, -2, 7, 0}, +{ 8, s_4_23, -5, 27, 0}, +{ 10, s_4_24, -6, 17, 0}, +{ 10, s_4_25, -7, 22, 0}, +{ 12, s_4_26, -8, 25, 0}, +{ 12, s_4_27, -9, 24, 0}, +{ 8, s_4_28, -10, 26, 0}, +{ 10, s_4_29, -11, 20, 0}, +{ 8, s_4_30, -12, 11, 0}, +{ 4, s_4_31, -13, 4, 0}, +{ 10, s_4_32, -1, 9, 0}, +{ 10, s_4_33, -2, 13, 0}, +{ 10, s_4_34, -3, 8, 0}, +{ 10, s_4_35, -4, 19, 0}, +{ 6, s_4_36, -5, 1, 0}, +{ 8, s_4_37, -1, 1, 0}, +{ 6, s_4_38, -7, 1, 0}, +{ 10, s_4_39, -21, 10, 0}, +{ 10, s_4_40, -22, 18, 0}, +{ 10, s_4_41, -23, 16, 0}, +{ 4, s_4_42, -24, 1, 0}, +{ 12, s_4_43, -1, 5, 0}, +{ 8, s_4_44, -2, 1, 0}, +{ 10, s_4_45, -3, 6, 0}, +{ 10, s_4_46, -4, 1, 0}, +{ 12, s_4_47, -5, 29, 0}, +{ 12, s_4_48, -30, 23, 0}, +{ 14, s_4_49, -31, 28, 0}, +{ 10, s_4_50, -32, 30, 0}, +{ 10, s_4_51, -33, 21, 0}, +{ 6, s_4_52, -34, 5, 0}, +{ 2, s_4_53, 0, 1, 0}, +{ 4, s_4_54, -1, 4, 0}, +{ 6, s_4_55, -1, 1, 0}, +{ 4, s_4_56, -3, 1, 0}, +{ 6, s_4_57, -1, 4, 0}, +{ 6, s_4_58, -2, 3, 0}, +{ 4, s_4_59, -6, 1, 0}, +{ 6, s_4_60, -1, 2, 0}, +{ 8, s_4_61, -2, 1, 0}, +{ 6, s_4_62, -9, 1, 0}, +{ 10, s_4_63, -1, 1, 0}, +{ 2, s_4_64, 0, 1, 0}, +{ 4, s_4_65, -1, 4, 0}, +{ 6, s_4_66, -1, 1, 0}, +{ 6, s_4_67, -2, 1, 0}, +{ 4, s_4_68, -4, -1, 0}, +{ 6, s_4_69, -5, 1, 0}, +{ 6, s_4_70, -6, 3, 0}, +{ 8, s_4_71, -1, 4, 0}, +{ 4, s_4_72, 0, 1, 0}, +{ 6, s_4_73, -1, 4, 0}, +{ 8, s_4_74, -1, 1, 0}, +{ 8, s_4_75, -2, 1, 0}, +{ 8, s_4_76, -4, 3, 0}, +{ 10, s_4_77, -1, 4, 0}, +{ 4, s_4_78, 0, 32, 0} }; static const symbol s_5_0[6] = { 0xD7, 0x95, 0xD7, 0xA0, 0xD7, 0x92 }; @@ -371,15 +450,13 @@ static const symbol s_5_2[6] = { 0xD7, 0x94, 0xD7, 0xB2, 0xD7, 0x98 }; static const symbol s_5_3[6] = { 0xD7, 0xA7, 0xD7, 0xB2, 0xD7, 0x98 }; static const symbol s_5_4[8] = { 0xD7, 0x99, 0xD7, 0xA7, 0xD7, 0xB2, 0xD7, 0x98 }; static const symbol s_5_5[2] = { 0xD7, 0x9C }; - -static const struct among a_5[6] = -{ -{ 6, s_5_0, -1, 1, 0}, -{ 8, s_5_1, -1, 1, 0}, -{ 6, s_5_2, -1, 1, 0}, -{ 6, s_5_3, -1, 1, 0}, -{ 8, s_5_4, 3, 1, 0}, -{ 2, s_5_5, -1, 2, 0} +static const struct among a_5[6] = { +{ 6, s_5_0, 0, 1, 0}, +{ 8, s_5_1, 0, 1, 0}, +{ 6, s_5_2, 0, 1, 0}, +{ 6, s_5_3, 0, 1, 0}, +{ 8, s_5_4, -1, 1, 0}, +{ 2, s_5_5, 0, 2, 0} }; static const symbol s_6_0[4] = { 0xD7, 0x99, 0xD7, 0x92 }; @@ -391,18 +468,16 @@ static const symbol s_6_5[8] = { 0xD7, 0x91, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA7 static const symbol s_6_6[8] = { 0xD7, 0x92, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA7 }; static const symbol s_6_7[6] = { 0xD7, 0xA0, 0xD7, 0x99, 0xD7, 0xA7 }; static const symbol s_6_8[4] = { 0xD7, 0x99, 0xD7, 0xA9 }; - -static const struct among a_6[9] = -{ -{ 4, s_6_0, -1, 1, 0}, -{ 4, s_6_1, -1, 1, 0}, -{ 6, s_6_2, 1, 1, 0}, -{ 8, s_6_3, 2, 1, 0}, -{ 10, s_6_4, 3, 1, 0}, -{ 8, s_6_5, 1, -1, 0}, -{ 8, s_6_6, 1, -1, 0}, -{ 6, s_6_7, 1, 1, 0}, -{ 4, s_6_8, -1, 1, 0} +static const struct among a_6[9] = { +{ 4, s_6_0, 0, 1, 0}, +{ 4, s_6_1, 0, 1, 0}, +{ 6, s_6_2, -1, 1, 0}, +{ 8, s_6_3, -1, 1, 0}, +{ 10, s_6_4, -1, 1, 0}, +{ 8, s_6_5, -4, -1, 0}, +{ 8, s_6_6, -5, -1, 0}, +{ 6, s_6_7, -6, 1, 0}, +{ 4, s_6_8, 0, 1, 0} }; static const unsigned char g_niked[] = { 255, 155, 6 }; @@ -411,826 +486,856 @@ static const unsigned char g_vowel[] = { 33, 2, 4, 0, 6 }; static const unsigned char g_consonant[] = { 239, 254, 253, 131 }; -static const symbol s_0[] = { 0xD6, 0xBC }; -static const symbol s_1[] = { 0xD7, 0xB0 }; -static const symbol s_2[] = { 0xD6, 0xB4 }; -static const symbol s_3[] = { 0xD7, 0xB1 }; -static const symbol s_4[] = { 0xD6, 0xB4 }; -static const symbol s_5[] = { 0xD7, 0xB2 }; -static const symbol s_6[] = { 0xD7, 0x9B }; -static const symbol s_7[] = { 0xD7, 0x9E }; -static const symbol s_8[] = { 0xD7, 0xA0 }; -static const symbol s_9[] = { 0xD7, 0xA4 }; -static const symbol s_10[] = { 0xD7, 0xA6 }; -static const symbol s_11[] = { 0xD7, 0x92, 0xD7, 0xA2 }; -static const symbol s_12[] = { 0xD7, 0x9C, 0xD7, 0x98 }; -static const symbol s_13[] = { 0xD7, 0x91, 0xD7, 0xA0 }; -static const symbol s_14[] = { 'G', 'E' }; -static const symbol s_15[] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0x92, 0xD7, 0xA0 }; -static const symbol s_16[] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0xA7, 0xD7, 0x98 }; -static const symbol s_17[] = { 0xD7, 0xA6, 0xD7, 0x95, 0xD7, 0xA7, 0xD7, 0xA0 }; -static const symbol s_18[] = { 0xD7, 0x92, 0xD7, 0xA2, 0xD7, 0x91, 0xD7, 0xA0 }; -static const symbol s_19[] = { 0xD7, 0x92, 0xD7, 0xA2 }; -static const symbol s_20[] = { 'G', 'E' }; -static const symbol s_21[] = { 0xD7, 0xA6, 0xD7, 0x95 }; -static const symbol s_22[] = { 'T', 'S', 'U' }; -static const symbol s_23[] = { 0xD7, 0x99, 0xD7, 0xA2 }; -static const symbol s_24[] = { 0xD7, 0x92, 0xD7, 0xB2 }; -static const symbol s_25[] = { 0xD7, 0xA0, 0xD7, 0xA2, 0xD7, 0x9E }; -static const symbol s_26[] = { 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0x93 }; -static const symbol s_27[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x98 }; -static const symbol s_28[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0xA1 }; -static const symbol s_29[] = { 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x96 }; -static const symbol s_30[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_31[] = { 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x98 }; -static const symbol s_32[] = { 0xD7, 0xA7, 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_33[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_34[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0xA1 }; -static const symbol s_35[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x92 }; -static const symbol s_36[] = { 0xD7, 0xA9, 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0xA1 }; -static const symbol s_37[] = { 0xD7, 0xA9, 0xD7, 0xA0, 0xD7, 0xB2, 0xD7, 0x93 }; -static const symbol s_38[] = { 0xD7, 0xA9, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_39[] = { 0xD7, 0x91, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x93 }; -static const symbol s_40[] = { 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0x98, 0xD7, 0xA9 }; -static const symbol s_41[] = { 0xD7, 0x96, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_42[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0xA7 }; -static const symbol s_43[] = { 0xD7, 0xA6, 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_44[] = { 0xD7, 0xA9, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_45[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x92 }; -static const symbol s_46[] = { 0xD7, 0x94, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_47[] = { 0xD7, 0xA4, 0xD7, 0x90, 0xD7, 0xA8, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA8 }; -static const symbol s_48[] = { 0xD7, 0xA9, 0xD7, 0x98, 0xD7, 0xB2 }; -static const symbol s_49[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xA2, 0xD7, 0xA8 }; -static const symbol s_50[] = { 0xD7, 0x98 }; -static const symbol s_51[] = { 0xD7, 0x91, 0xD7, 0xA8, 0xD7, 0x90, 0xD7, 0x9B }; -static const symbol s_52[] = { 0xD7, 0x92, 0xD7, 0xA2 }; -static const symbol s_53[] = { 0xD7, 0x91, 0xD7, 0xA8, 0xD7, 0xA2, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_54[] = { 0xD7, 0x92, 0xD7, 0xB2 }; -static const symbol s_55[] = { 0xD7, 0xA0, 0xD7, 0xA2, 0xD7, 0x9E }; -static const symbol s_56[] = { 0xD7, 0xA9, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_57[] = { 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0x93 }; -static const symbol s_58[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x98 }; -static const symbol s_59[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0xA1 }; -static const symbol s_60[] = { 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x96 }; -static const symbol s_61[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_62[] = { 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x98 }; -static const symbol s_63[] = { 0xD7, 0xA7, 0xD7, 0x9C, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_64[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_65[] = { 0xD7, 0xA8, 0xD7, 0xB2, 0xD7, 0xA1 }; -static const symbol s_66[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xB2, 0xD7, 0x92 }; -static const symbol s_67[] = { 0xD7, 0xA9, 0xD7, 0x9E, 0xD7, 0xB2, 0xD7, 0xA1 }; -static const symbol s_68[] = { 0xD7, 0xA9, 0xD7, 0xA0, 0xD7, 0xB2, 0xD7, 0x93 }; -static const symbol s_69[] = { 0xD7, 0x91, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x93 }; -static const symbol s_70[] = { 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0x98, 0xD7, 0xA9 }; -static const symbol s_71[] = { 0xD7, 0x96, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_72[] = { 0xD7, 0x98, 0xD7, 0xA8, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0xA7 }; -static const symbol s_73[] = { 0xD7, 0xA6, 0xD7, 0xB0, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_74[] = { 0xD7, 0xA9, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_75[] = { 0xD7, 0x91, 0xD7, 0xB2, 0xD7, 0x92 }; -static const symbol s_76[] = { 0xD7, 0x94, 0xD7, 0xB2, 0xD7, 0x91 }; -static const symbol s_77[] = { 0xD7, 0xA4, 0xD7, 0x90, 0xD7, 0xA8, 0xD7, 0x9C, 0xD7, 0x99, 0xD7, 0xA8 }; -static const symbol s_78[] = { 0xD7, 0xA9, 0xD7, 0x98, 0xD7, 0xB2 }; -static const symbol s_79[] = { 0xD7, 0xA9, 0xD7, 0xB0, 0xD7, 0xA2, 0xD7, 0xA8 }; -static const symbol s_80[] = { 0xD7, 0x91, 0xD7, 0xA8, 0xD7, 0xA2, 0xD7, 0xA0, 0xD7, 0x92 }; -static const symbol s_81[] = { 0xD7, 0x94 }; -static const symbol s_82[] = { 0xD7, 0x92 }; -static const symbol s_83[] = { 0xD7, 0xA9 }; -static const symbol s_84[] = { 0xD7, 0x99, 0xD7, 0xA1 }; -static const symbol s_85[] = { 'G', 'E' }; -static const symbol s_86[] = { 'T', 'S', 'U' }; - static int r_prelude(struct SN_env * z) { int among_var; - { int c1 = z->c; - while(1) { - int c2 = z->c; - while(1) { - int c3 = z->c; + { + int v_1 = z->c; + while (1) { + int v_2 = z->c; + while (1) { + int v_3 = z->c; z->bra = z->c; - among_var = find_among(z, a_0, 8); + among_var = find_among(z, a_0, 8, 0); if (!among_var) goto lab2; z->ket = z->c; switch (among_var) { case 1: - { int c4 = z->c; + { + int v_4 = z->c; if (!(eq_s(z, 2, s_0))) goto lab3; goto lab2; lab3: - z->c = c4; + z->c = v_4; } - { int ret = slice_from_s(z, 2, s_1); + { + int ret = slice_from_s(z, 2, s_1); if (ret < 0) return ret; } break; case 2: - { int c5 = z->c; + { + int v_5 = z->c; if (!(eq_s(z, 2, s_2))) goto lab4; goto lab2; lab4: - z->c = c5; + z->c = v_5; } - { int ret = slice_from_s(z, 2, s_3); + { + int ret = slice_from_s(z, 2, s_3); if (ret < 0) return ret; } break; case 3: - { int c6 = z->c; + { + int v_6 = z->c; if (!(eq_s(z, 2, s_4))) goto lab5; goto lab2; lab5: - z->c = c6; + z->c = v_6; } - { int ret = slice_from_s(z, 2, s_5); + { + int ret = slice_from_s(z, 2, s_5); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 2, s_6); + { + int ret = slice_from_s(z, 2, s_6); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 2, s_7); + { + int ret = slice_from_s(z, 2, s_7); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 2, s_8); + { + int ret = slice_from_s(z, 2, s_8); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 2, s_9); + { + int ret = slice_from_s(z, 2, s_9); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 2, s_10); + { + int ret = slice_from_s(z, 2, s_10); if (ret < 0) return ret; } break; } - z->c = c3; + z->c = v_3; break; lab2: - z->c = c3; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_3; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab1; z->c = ret; } } continue; lab1: - z->c = c2; + z->c = v_2; break; } - z->c = c1; + z->c = v_1; } - { int c7 = z->c; - while(1) { - int c8 = z->c; - while(1) { - int c9 = z->c; + { + int v_7 = z->c; + while (1) { + int v_8 = z->c; + while (1) { + int v_9 = z->c; z->bra = z->c; if (in_grouping_U(z, g_niked, 1456, 1474, 0)) goto lab8; z->ket = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->c = c9; + z->c = v_9; break; lab8: - z->c = c9; - { int ret = skip_utf8(z->p, z->c, z->l, 1); + z->c = v_9; + { + int ret = skip_utf8(z->p, z->c, z->l, 1); if (ret < 0) goto lab7; z->c = ret; } } continue; lab7: - z->c = c8; + z->c = v_8; break; } - z->c = c7; + z->c = v_7; } return 1; } static int r_mark_regions(struct SN_env * z) { - z->I[1] = z->l; - { int c1 = z->c; + int i_x; + ((SN_local *)z)->i_p1 = z->l; + { + int v_1 = z->c; z->bra = z->c; - if (!(eq_s(z, 4, s_11))) { z->c = c1; goto lab0; } + if (!(eq_s(z, 4, s_11))) { z->c = v_1; goto lab0; } z->ket = z->c; - { int c2 = z->c; - { int c3 = z->c; - if (!(eq_s(z, 4, s_12))) goto lab3; - goto lab2; + { + int v_2 = z->c; + do { + int v_3 = z->c; + if (!(eq_s(z, 4, s_12))) goto lab2; + break; + lab2: + z->c = v_3; + if (!(eq_s(z, 4, s_13))) goto lab3; + break; lab3: - z->c = c3; - if (!(eq_s(z, 4, s_13))) goto lab4; - goto lab2; - lab4: - z->c = c3; + z->c = v_3; if (z->c < z->l) goto lab1; - } - lab2: - { z->c = c1; goto lab0; } + } while (0); + { z->c = v_1; goto lab0; } lab1: - z->c = c2; + z->c = v_2; } - { int ret = slice_from_s(z, 2, s_14); + { + int ret = slice_from_s(z, 2, s_14); if (ret < 0) return ret; } lab0: ; } - { int c4 = z->c; - if (!find_among(z, a_1, 40)) { z->c = c4; goto lab5; } - { int c5 = z->c; - { int c_test6 = z->c; - { int c7 = z->c; - if (!(eq_s(z, 8, s_15))) goto lab9; - goto lab8; - lab9: - z->c = c7; - if (!(eq_s(z, 8, s_16))) goto lab10; - goto lab8; - lab10: - z->c = c7; - if (!(eq_s(z, 8, s_17))) goto lab7; - } - lab8: - if (z->c < z->l) goto lab7; - z->c = c_test6; + { + int v_4 = z->c; + if (!find_among(z, a_1, 40, 0)) { z->c = v_4; goto lab4; } + do { + int v_5 = z->c; + { + int v_6 = z->c; + do { + int v_7 = z->c; + if (!(eq_s(z, 8, s_15))) goto lab6; + break; + lab6: + z->c = v_7; + if (!(eq_s(z, 8, s_16))) goto lab7; + break; + lab7: + z->c = v_7; + if (!(eq_s(z, 8, s_17))) goto lab5; + } while (0); + if (z->c < z->l) goto lab5; + z->c = v_6; } - goto lab6; - lab7: - z->c = c5; - { int c_test8 = z->c; - if (!(eq_s(z, 8, s_18))) goto lab11; - z->c = c_test8; + break; + lab5: + z->c = v_5; + { + int v_8 = z->c; + if (!(eq_s(z, 8, s_18))) goto lab8; + z->c = v_8; } - goto lab6; - lab11: - z->c = c5; + break; + lab8: + z->c = v_5; z->bra = z->c; - if (!(eq_s(z, 4, s_19))) goto lab12; + if (!(eq_s(z, 4, s_19))) goto lab9; z->ket = z->c; - { int ret = slice_from_s(z, 2, s_20); + { + int ret = slice_from_s(z, 2, s_20); if (ret < 0) return ret; } - goto lab6; - lab12: - z->c = c5; + break; + lab9: + z->c = v_5; z->bra = z->c; - if (!(eq_s(z, 4, s_21))) { z->c = c4; goto lab5; } + if (!(eq_s(z, 4, s_21))) { z->c = v_4; goto lab4; } z->ket = z->c; - { int ret = slice_from_s(z, 3, s_22); + { + int ret = slice_from_s(z, 3, s_22); if (ret < 0) return ret; } - } - lab6: - lab5: + } while (0); + lab4: ; } - { int c_test9 = z->c; - { int ret = skip_utf8(z->p, z->c, z->l, 3); + { + int v_9 = z->c; + { + int ret = skip_utf8(z->p, z->c, z->l, 3); if (ret < 0) return 0; z->c = ret; } - z->I[0] = z->c; - z->c = c_test9; + i_x = z->c; + z->c = v_9; } - { int c10 = z->c; - if (z->c + 5 >= z->l || (z->p[z->c + 5] != 169 && z->p[z->c + 5] != 168)) { z->c = c10; goto lab13; } - if (!find_among(z, a_2, 4)) { z->c = c10; goto lab13; } - lab13: + { + int v_10 = z->c; + if (z->c + 5 >= z->l || (z->p[z->c + 5] != 169 && z->p[z->c + 5] != 168)) { z->c = v_10; goto lab10; } + if (!find_among(z, a_2, 4, 0)) { z->c = v_10; goto lab10; } + lab10: ; } - { int c11 = z->c; - if (in_grouping_U(z, g_consonant, 1489, 1520, 0)) goto lab14; - if (in_grouping_U(z, g_consonant, 1489, 1520, 0)) goto lab14; - if (in_grouping_U(z, g_consonant, 1489, 1520, 0)) goto lab14; - z->I[1] = z->c; + { + int v_11 = z->c; + if (in_grouping_U(z, g_consonant, 1489, 1520, 0)) goto lab11; + if (in_grouping_U(z, g_consonant, 1489, 1520, 0)) goto lab11; + if (in_grouping_U(z, g_consonant, 1489, 1520, 0)) goto lab11; + ((SN_local *)z)->i_p1 = z->c; return 0; - lab14: - z->c = c11; + lab11: + z->c = v_11; } - - if (out_grouping_U(z, g_vowel, 1488, 1522, 1) < 0) return 0; - while(1) { - if (in_grouping_U(z, g_vowel, 1488, 1522, 0)) goto lab15; - continue; - lab15: - break; + { + int ret = out_grouping_U(z, g_vowel, 1488, 1522, 1); + if (ret < 0) return 0; + z->c += ret; } - z->I[1] = z->c; - - if (z->I[1] >= z->I[0]) goto lab16; - z->I[1] = z->I[0]; -lab16: + if (in_grouping_U(z, g_vowel, 1488, 1522, 1) < 0) return 0; + ((SN_local *)z)->i_p1 = z->c; + if (((SN_local *)z)->i_p1 >= i_x) goto lab12; + ((SN_local *)z)->i_p1 = i_x; +lab12: return 1; } static int r_R1(struct SN_env * z) { - return z->I[1] <= z->c; + return ((SN_local *)z)->i_p1 <= z->c; } static int r_R1plus3(struct SN_env * z) { - return z->I[1] <= (z->c + 6); + return ((SN_local *)z)->i_p1 <= (z->c + 6); } static int r_standard_suffix(struct SN_env * z) { int among_var; - { int m1 = z->l - z->c; (void)m1; + { + int v_1 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_4, 79); + among_var = find_among_b(z, a_4, 79, 0); if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int ret = slice_from_s(z, 4, s_23); + { + int ret = slice_from_s(z, 4, s_23); if (ret < 0) return ret; } break; case 3: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } z->ket = z->c; - among_var = find_among_b(z, a_3, 26); + among_var = find_among_b(z, a_3, 26, 0); if (!among_var) goto lab0; z->bra = z->c; switch (among_var) { case 1: - { int ret = slice_from_s(z, 4, s_24); + { + int ret = slice_from_s(z, 4, s_24); if (ret < 0) return ret; } break; case 2: - { int ret = slice_from_s(z, 6, s_25); + { + int ret = slice_from_s(z, 6, s_25); if (ret < 0) return ret; } break; case 3: - { int ret = slice_from_s(z, 6, s_26); + { + int ret = slice_from_s(z, 6, s_26); if (ret < 0) return ret; } break; case 4: - { int ret = slice_from_s(z, 6, s_27); + { + int ret = slice_from_s(z, 6, s_27); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 6, s_28); + { + int ret = slice_from_s(z, 6, s_28); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_29); + { + int ret = slice_from_s(z, 6, s_29); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 8, s_30); + { + int ret = slice_from_s(z, 8, s_30); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 6, s_31); + { + int ret = slice_from_s(z, 6, s_31); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 8, s_32); + { + int ret = slice_from_s(z, 8, s_32); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 6, s_33); + { + int ret = slice_from_s(z, 6, s_33); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 6, s_34); + { + int ret = slice_from_s(z, 6, s_34); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 8, s_35); + { + int ret = slice_from_s(z, 8, s_35); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 8, s_36); + { + int ret = slice_from_s(z, 8, s_36); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 8, s_37); + { + int ret = slice_from_s(z, 8, s_37); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 8, s_38); + { + int ret = slice_from_s(z, 8, s_38); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 8, s_39); + { + int ret = slice_from_s(z, 8, s_39); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 8, s_40); + { + int ret = slice_from_s(z, 8, s_40); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 8, s_41); + { + int ret = slice_from_s(z, 8, s_41); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 10, s_42); + { + int ret = slice_from_s(z, 10, s_42); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 10, s_43); + { + int ret = slice_from_s(z, 10, s_43); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 10, s_44); + { + int ret = slice_from_s(z, 10, s_44); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 6, s_45); + { + int ret = slice_from_s(z, 6, s_45); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 6, s_46); + { + int ret = slice_from_s(z, 6, s_46); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 12, s_47); + { + int ret = slice_from_s(z, 12, s_47); if (ret < 0) return ret; } break; case 25: - { int ret = slice_from_s(z, 6, s_48); + { + int ret = slice_from_s(z, 6, s_48); if (ret < 0) return ret; } break; case 26: - { int ret = slice_from_s(z, 8, s_49); + { + int ret = slice_from_s(z, 8, s_49); if (ret < 0) return ret; } break; } break; case 4: - { int m2 = z->l - z->c; (void)m2; - { int ret = r_R1(z); - if (ret == 0) goto lab2; + do { + int v_2 = z->l - z->c; + { + int ret = r_R1(z); + if (ret == 0) goto lab1; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - goto lab1; - lab2: - z->c = z->l - m2; - { int ret = slice_from_s(z, 2, s_50); + break; + lab1: + z->c = z->l - v_2; + { + int ret = slice_from_s(z, 2, s_50); if (ret < 0) return ret; } - } - lab1: + } while (0); z->ket = z->c; if (!(eq_s_b(z, 8, s_51))) goto lab0; - { int m3 = z->l - z->c; (void)m3; - if (!(eq_s_b(z, 4, s_52))) { z->c = z->l - m3; goto lab3; } - lab3: + { + int v_3 = z->l - z->c; + if (!(eq_s_b(z, 4, s_52))) { z->c = z->l - v_3; goto lab2; } + lab2: ; } z->bra = z->c; - { int ret = slice_from_s(z, 10, s_53); + { + int ret = slice_from_s(z, 10, s_53); if (ret < 0) return ret; } break; case 5: - { int ret = slice_from_s(z, 4, s_54); + { + int ret = slice_from_s(z, 4, s_54); if (ret < 0) return ret; } break; case 6: - { int ret = slice_from_s(z, 6, s_55); + { + int ret = slice_from_s(z, 6, s_55); if (ret < 0) return ret; } break; case 7: - { int ret = slice_from_s(z, 8, s_56); + { + int ret = slice_from_s(z, 8, s_56); if (ret < 0) return ret; } break; case 8: - { int ret = slice_from_s(z, 6, s_57); + { + int ret = slice_from_s(z, 6, s_57); if (ret < 0) return ret; } break; case 9: - { int ret = slice_from_s(z, 6, s_58); + { + int ret = slice_from_s(z, 6, s_58); if (ret < 0) return ret; } break; case 10: - { int ret = slice_from_s(z, 6, s_59); + { + int ret = slice_from_s(z, 6, s_59); if (ret < 0) return ret; } break; case 11: - { int ret = slice_from_s(z, 6, s_60); + { + int ret = slice_from_s(z, 6, s_60); if (ret < 0) return ret; } break; case 12: - { int ret = slice_from_s(z, 8, s_61); + { + int ret = slice_from_s(z, 8, s_61); if (ret < 0) return ret; } break; case 13: - { int ret = slice_from_s(z, 6, s_62); + { + int ret = slice_from_s(z, 6, s_62); if (ret < 0) return ret; } break; case 14: - { int ret = slice_from_s(z, 8, s_63); + { + int ret = slice_from_s(z, 8, s_63); if (ret < 0) return ret; } break; case 15: - { int ret = slice_from_s(z, 6, s_64); + { + int ret = slice_from_s(z, 6, s_64); if (ret < 0) return ret; } break; case 16: - { int ret = slice_from_s(z, 6, s_65); + { + int ret = slice_from_s(z, 6, s_65); if (ret < 0) return ret; } break; case 17: - { int ret = slice_from_s(z, 8, s_66); + { + int ret = slice_from_s(z, 8, s_66); if (ret < 0) return ret; } break; case 18: - { int ret = slice_from_s(z, 8, s_67); + { + int ret = slice_from_s(z, 8, s_67); if (ret < 0) return ret; } break; case 19: - { int ret = slice_from_s(z, 8, s_68); + { + int ret = slice_from_s(z, 8, s_68); if (ret < 0) return ret; } break; case 20: - { int ret = slice_from_s(z, 8, s_69); + { + int ret = slice_from_s(z, 8, s_69); if (ret < 0) return ret; } break; case 21: - { int ret = slice_from_s(z, 8, s_70); + { + int ret = slice_from_s(z, 8, s_70); if (ret < 0) return ret; } break; case 22: - { int ret = slice_from_s(z, 8, s_71); + { + int ret = slice_from_s(z, 8, s_71); if (ret < 0) return ret; } break; case 23: - { int ret = slice_from_s(z, 10, s_72); + { + int ret = slice_from_s(z, 10, s_72); if (ret < 0) return ret; } break; case 24: - { int ret = slice_from_s(z, 10, s_73); + { + int ret = slice_from_s(z, 10, s_73); if (ret < 0) return ret; } break; case 25: - { int ret = slice_from_s(z, 10, s_74); + { + int ret = slice_from_s(z, 10, s_74); if (ret < 0) return ret; } break; case 26: - { int ret = slice_from_s(z, 6, s_75); + { + int ret = slice_from_s(z, 6, s_75); if (ret < 0) return ret; } break; case 27: - { int ret = slice_from_s(z, 6, s_76); + { + int ret = slice_from_s(z, 6, s_76); if (ret < 0) return ret; } break; case 28: - { int ret = slice_from_s(z, 12, s_77); + { + int ret = slice_from_s(z, 12, s_77); if (ret < 0) return ret; } break; case 29: - { int ret = slice_from_s(z, 6, s_78); + { + int ret = slice_from_s(z, 6, s_78); if (ret < 0) return ret; } break; case 30: - { int ret = slice_from_s(z, 8, s_79); + { + int ret = slice_from_s(z, 8, s_79); if (ret < 0) return ret; } break; case 31: - { int ret = slice_from_s(z, 10, s_80); + { + int ret = slice_from_s(z, 10, s_80); if (ret < 0) return ret; } break; case 32: - { int ret = r_R1(z); + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int ret = slice_from_s(z, 2, s_81); + { + int ret = slice_from_s(z, 2, s_81); if (ret < 0) return ret; } break; case 33: - { int m4 = z->l - z->c; (void)m4; - { int m5 = z->l - z->c; (void)m5; - if (!(eq_s_b(z, 2, s_82))) goto lab7; - goto lab6; - lab7: - z->c = z->l - m5; - if (!(eq_s_b(z, 2, s_83))) goto lab5; - } - lab6: - { int m6 = z->l - z->c; (void)m6; - { int ret = r_R1plus3(z); - if (ret == 0) { z->c = z->l - m6; goto lab8; } + do { + int v_4 = z->l - z->c; + do { + int v_5 = z->l - z->c; + if (!(eq_s_b(z, 2, s_82))) goto lab4; + break; + lab4: + z->c = z->l - v_5; + if (!(eq_s_b(z, 2, s_83))) goto lab3; + } while (0); + { + int v_6 = z->l - z->c; + { + int ret = r_R1plus3(z); + if (ret == 0) { z->c = z->l - v_6; goto lab5; } if (ret < 0) return ret; } - { int ret = slice_from_s(z, 4, s_84); + { + int ret = slice_from_s(z, 4, s_84); if (ret < 0) return ret; } - lab8: + lab5: ; } - goto lab4; - lab5: - z->c = z->l - m4; - { int ret = r_R1(z); + break; + lab3: + z->c = z->l - v_4; + { + int ret = r_R1(z); if (ret == 0) goto lab0; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - } - lab4: + } while (0); break; } lab0: - z->c = z->l - m1; + z->c = z->l - v_1; } - { int m7 = z->l - z->c; (void)m7; + { + int v_7 = z->l - z->c; z->ket = z->c; - if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((285474816 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab9; - among_var = find_among_b(z, a_5, 6); - if (!among_var) goto lab9; + if (z->c - 1 <= z->lb || z->p[z->c - 1] >> 5 != 4 || !((285474816 >> (z->p[z->c - 1] & 0x1f)) & 1)) goto lab6; + among_var = find_among_b(z, a_5, 6, 0); + if (!among_var) goto lab6; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); - if (ret == 0) goto lab9; + { + int ret = r_R1(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; case 2: - { int ret = r_R1(z); - if (ret == 0) goto lab9; + { + int ret = r_R1(z); + if (ret == 0) goto lab6; if (ret < 0) return ret; } - if (in_grouping_b_U(z, g_consonant, 1489, 1520, 0)) goto lab9; - { int ret = slice_del(z); + if (in_grouping_b_U(z, g_consonant, 1489, 1520, 0)) goto lab6; + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - lab9: - z->c = z->l - m7; + lab6: + z->c = z->l - v_7; } - { int m8 = z->l - z->c; (void)m8; + { + int v_8 = z->l - z->c; z->ket = z->c; - among_var = find_among_b(z, a_6, 9); - if (!among_var) goto lab10; + among_var = find_among_b(z, a_6, 9, 0); + if (!among_var) goto lab7; z->bra = z->c; switch (among_var) { case 1: - { int ret = r_R1(z); - if (ret == 0) goto lab10; + { + int ret = r_R1(z); + if (ret == 0) goto lab7; if (ret < 0) return ret; } - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } break; } - lab10: - z->c = z->l - m8; + lab7: + z->c = z->l - v_8; } - { int m9 = z->l - z->c; (void)m9; - while(1) { - int m10 = z->l - z->c; (void)m10; - while(1) { - int m11 = z->l - z->c; (void)m11; + { + int v_9 = z->l - z->c; + while (1) { + int v_10 = z->l - z->c; + while (1) { + int v_11 = z->l - z->c; z->ket = z->c; - { int m12 = z->l - z->c; (void)m12; - if (!(eq_s_b(z, 2, s_85))) goto lab15; - goto lab14; - lab15: - z->c = z->l - m12; - if (!(eq_s_b(z, 3, s_86))) goto lab13; - } - lab14: + do { + int v_12 = z->l - z->c; + if (!(eq_s_b(z, 2, s_85))) goto lab11; + break; + lab11: + z->c = z->l - v_12; + if (!(eq_s_b(z, 3, s_86))) goto lab10; + } while (0); z->bra = z->c; - { int ret = slice_del(z); + { + int ret = slice_del(z); if (ret < 0) return ret; } - z->c = z->l - m11; + z->c = z->l - v_11; break; - lab13: - z->c = z->l - m11; - { int ret = skip_b_utf8(z->p, z->c, z->lb, 1); - if (ret < 0) goto lab12; + lab10: + z->c = z->l - v_11; + { + int ret = skip_b_utf8(z->p, z->c, z->lb, 1); + if (ret < 0) goto lab9; z->c = ret; } } continue; - lab12: - z->c = z->l - m10; + lab9: + z->c = z->l - v_10; break; } - z->c = z->l - m9; + z->c = z->l - v_9; } return 1; } extern int yiddish_UTF_8_stem(struct SN_env * z) { - - { int ret = r_prelude(z); + { + int ret = r_prelude(z); if (ret < 0) return ret; } - { int c1 = z->c; - { int ret = r_mark_regions(z); + { + int v_1 = z->c; + { + int ret = r_mark_regions(z); if (ret < 0) return ret; } - z->c = c1; + z->c = v_1; } z->lb = z->c; z->c = z->l; - - - { int ret = r_standard_suffix(z); + { + int ret = r_standard_suffix(z); if (ret < 0) return ret; } z->c = z->lb; return 1; } -extern struct SN_env * yiddish_UTF_8_create_env(void) { return SN_create_env(0, 2); } +extern struct SN_env * yiddish_UTF_8_create_env(void) { + struct SN_env * z = SN_new_env(sizeof(SN_local)); + if (z) { + ((SN_local *)z)->i_p1 = 0; + } + return z; +} -extern void yiddish_UTF_8_close_env(struct SN_env * z) { SN_close_env(z, 0); } +extern void yiddish_UTF_8_close_env(struct SN_env * z) { + SN_delete_env(z); +} diff --git a/src/backend/snowball/libstemmer/utilities.c b/src/backend/snowball/libstemmer/utilities.c index 8acc18541d5a8..f441ce81dc8a0 100644 --- a/src/backend/snowball/libstemmer/utilities.c +++ b/src/backend/snowball/libstemmer/utilities.c @@ -1,11 +1,27 @@ -#include "header.h" +#include "snowball_runtime.h" + +#ifdef SNOWBALL_RUNTIME_THROW_EXCEPTIONS +# include +# include +# define SNOWBALL_RETURN_OK return +# define SNOWBALL_RETURN_OR_THROW(R, E) throw E +# define SNOWBALL_PROPAGATE_ERR(F) F +#else +# define SNOWBALL_RETURN_OK return 0 +# define SNOWBALL_RETURN_OR_THROW(R, E) return R +# define SNOWBALL_PROPAGATE_ERR(F) do { \ + int snowball_err = F; \ + if (snowball_err < 0) return snowball_err; \ + } while (0) +#endif #define CREATE_SIZE 1 extern symbol * create_s(void) { symbol * p; void * mem = malloc(HEAD + (CREATE_SIZE + 1) * sizeof(symbol)); - if (mem == NULL) return NULL; + if (mem == NULL) + SNOWBALL_RETURN_OR_THROW(NULL, std::bad_alloc()); p = (symbol *) (HEAD + (char *) mem); CAPACITY(p) = CREATE_SIZE; SET_SIZE(p, 0); @@ -230,7 +246,8 @@ extern int eq_v_b(struct SN_env * z, const symbol * p) { return eq_s_b(z, SIZE(p), p); } -extern int find_among(struct SN_env * z, const struct among * v, int v_size) { +extern int find_among(struct SN_env * z, const struct among * v, int v_size, + int (*call_among_func)(struct SN_env*)) { int i = 0; int j = v_size; @@ -277,25 +294,26 @@ extern int find_among(struct SN_env * z, const struct among * v, int v_size) { first_key_inspected = 1; } } + w = v + i; while (1) { - w = v + i; if (common_i >= w->s_size) { z->c = c + w->s_size; - if (w->function == NULL) return w->result; - { - int res = w->function(z); + if (!w->function) return w->result; + z->af = w->function; + if (call_among_func(z)) { z->c = c + w->s_size; - if (res) return w->result; + return w->result; } } - i = w->substring_i; - if (i < 0) return 0; + if (!w->substring_i) return 0; + w += w->substring_i; } } /* find_among_b is for backwards processing. Same comments apply */ -extern int find_among_b(struct SN_env * z, const struct among * v, int v_size) { +extern int find_among_b(struct SN_env * z, const struct among * v, int v_size, + int (*call_among_func)(struct SN_env*)) { int i = 0; int j = v_size; @@ -332,59 +350,49 @@ extern int find_among_b(struct SN_env * z, const struct among * v, int v_size) { first_key_inspected = 1; } } + w = v + i; while (1) { - w = v + i; if (common_i >= w->s_size) { z->c = c - w->s_size; - if (w->function == NULL) return w->result; - { - int res = w->function(z); + if (!w->function) return w->result; + z->af = w->function; + if (call_among_func(z)) { z->c = c - w->s_size; - if (res) return w->result; + return w->result; } } - i = w->substring_i; - if (i < 0) return 0; + if (!w->substring_i) return 0; + w += w->substring_i; } } /* Increase the size of the buffer pointed to by p to at least n symbols. - * If insufficient memory, returns NULL and frees the old buffer. + * On success, returns 0. If insufficient memory, returns -1. */ -static symbol * increase_size(symbol * p, int n) { - symbol * q; +static int increase_size(symbol ** p, int n) { int new_size = n + 20; - void * mem = realloc((char *) p - HEAD, + void * mem = realloc((char *) *p - HEAD, HEAD + (new_size + 1) * sizeof(symbol)); - if (mem == NULL) { - lose_s(p); - return NULL; - } + symbol * q; + if (mem == NULL) return -1; q = (symbol *) (HEAD + (char *)mem); CAPACITY(q) = new_size; - return q; + *p = q; + return 0; } /* to replace symbols between c_bra and c_ket in z->p by the s_size symbols at s. Returns 0 on success, -1 on error. - Also, frees z->p (and sets it to NULL) on error. */ -extern int replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const symbol * s, int * adjptr) +extern SNOWBALL_ERR replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const symbol * s) { - int adjustment; - int len; - if (z->p == NULL) { - z->p = create_s(); - if (z->p == NULL) return -1; - } - adjustment = s_size - (c_ket - c_bra); - len = SIZE(z->p); + int adjustment = s_size - (c_ket - c_bra); if (adjustment != 0) { + int len = SIZE(z->p); if (adjustment + len > CAPACITY(z->p)) { - z->p = increase_size(z->p, adjustment + len); - if (z->p == NULL) return -1; + SNOWBALL_PROPAGATE_ERR(increase_size(&z->p, adjustment + len)); } memmove(z->p + c_ket + adjustment, z->p + c_ket, @@ -397,82 +405,97 @@ extern int replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const z->c = c_bra; } if (s_size) memmove(z->p + c_bra, s, s_size * sizeof(symbol)); - if (adjptr != NULL) - *adjptr = adjustment; - return 0; + SNOWBALL_RETURN_OK; } -static int slice_check(struct SN_env * z) { +# define REPLACE_S(Z, B, K, SIZE, S) \ + SNOWBALL_PROPAGATE_ERR(replace_s(Z, B, K, SIZE, S)) + +static SNOWBALL_ERR slice_check(struct SN_env * z) { if (z->bra < 0 || z->bra > z->ket || z->ket > z->l || - z->p == NULL || z->l > SIZE(z->p)) /* this line could be removed */ { #if 0 fprintf(stderr, "faulty slice operation:\n"); debug(z, -1, 0); #endif - return -1; + SNOWBALL_RETURN_OR_THROW(-1, std::logic_error("Snowball slice invalid")); } - return 0; + SNOWBALL_RETURN_OK; } -extern int slice_from_s(struct SN_env * z, int s_size, const symbol * s) { - if (slice_check(z)) return -1; - return replace_s(z, z->bra, z->ket, s_size, s, NULL); +# define SLICE_CHECK(Z) SNOWBALL_PROPAGATE_ERR(slice_check(Z)) + +extern SNOWBALL_ERR slice_from_s(struct SN_env * z, int s_size, const symbol * s) { + SLICE_CHECK(z); + REPLACE_S(z, z->bra, z->ket, s_size, s); + z->ket = z->bra + s_size; + SNOWBALL_RETURN_OK; } -extern int slice_from_v(struct SN_env * z, const symbol * p) { +extern SNOWBALL_ERR slice_from_v(struct SN_env * z, const symbol * p) { return slice_from_s(z, SIZE(p), p); } -extern int slice_del(struct SN_env * z) { - return slice_from_s(z, 0, NULL); +extern SNOWBALL_ERR slice_del(struct SN_env * z) { + SLICE_CHECK(z); + { + int slice_size = z->ket - z->bra; + if (slice_size != 0) { + int len = SIZE(z->p); + memmove(z->p + z->bra, + z->p + z->ket, + (len - z->ket) * sizeof(symbol)); + SET_SIZE(z->p, len - slice_size); + z->l -= slice_size; + if (z->c >= z->ket) + z->c -= slice_size; + else if (z->c > z->bra) + z->c = z->bra; + } + } + z->ket = z->bra; + SNOWBALL_RETURN_OK; } -extern int insert_s(struct SN_env * z, int bra, int ket, int s_size, const symbol * s) { - int adjustment; - if (replace_s(z, bra, ket, s_size, s, &adjustment)) - return -1; - if (bra <= z->bra) z->bra += adjustment; - if (bra <= z->ket) z->ket += adjustment; - return 0; +extern SNOWBALL_ERR insert_s(struct SN_env * z, int bra, int ket, int s_size, const symbol * s) { + REPLACE_S(z, bra, ket, s_size, s); + if (bra <= z->ket) { + int adjustment = s_size - (ket - bra); + z->ket += adjustment; + if (bra <= z->bra) z->bra += adjustment; + } + SNOWBALL_RETURN_OK; } -extern int insert_v(struct SN_env * z, int bra, int ket, const symbol * p) { +extern SNOWBALL_ERR insert_v(struct SN_env * z, int bra, int ket, const symbol * p) { return insert_s(z, bra, ket, SIZE(p), p); } -extern symbol * slice_to(struct SN_env * z, symbol * p) { - if (slice_check(z)) { - lose_s(p); - return NULL; - } +extern SNOWBALL_ERR slice_to(struct SN_env * z, symbol ** p) { + SLICE_CHECK(z); { int len = z->ket - z->bra; - if (CAPACITY(p) < len) { - p = increase_size(p, len); - if (p == NULL) - return NULL; + if (CAPACITY(*p) < len) { + SNOWBALL_PROPAGATE_ERR(increase_size(p, len)); } - memmove(p, z->p + z->bra, len * sizeof(symbol)); - SET_SIZE(p, len); + memmove(*p, z->p + z->bra, len * sizeof(symbol)); + SET_SIZE(*p, len); } - return p; + SNOWBALL_RETURN_OK; } -extern symbol * assign_to(struct SN_env * z, symbol * p) { +extern SNOWBALL_ERR assign_to(struct SN_env * z, symbol ** p) { int len = z->l; - if (CAPACITY(p) < len) { - p = increase_size(p, len); - if (p == NULL) - return NULL; + if (CAPACITY(*p) < len) { + SNOWBALL_PROPAGATE_ERR(increase_size(p, len)); } - memmove(p, z->p, len * sizeof(symbol)); - SET_SIZE(p, len); - return p; + memmove(*p, z->p, len * sizeof(symbol)); + SET_SIZE(*p, len); + SNOWBALL_RETURN_OK; } extern int len_utf8(const symbol * p) { @@ -484,25 +507,3 @@ extern int len_utf8(const symbol * p) { } return len; } - -#if 0 -extern void debug(struct SN_env * z, int number, int line_count) { - int i; - int limit = SIZE(z->p); - /*if (number >= 0) printf("%3d (line %4d): '", number, line_count);*/ - if (number >= 0) printf("%3d (line %4d): [%d]'", number, line_count,limit); - for (i = 0; i <= limit; i++) { - if (z->lb == i) printf("{"); - if (z->bra == i) printf("["); - if (z->c == i) printf("|"); - if (z->ket == i) printf("]"); - if (z->l == i) printf("}"); - if (i < limit) - { int ch = z->p[i]; - if (ch == 0) ch = '#'; - printf("%c", ch); - } - } - printf("'\n"); -} -#endif diff --git a/src/backend/snowball/meson.build b/src/backend/snowball/meson.build index 8e73d9d736823..6834110d333a2 100644 --- a/src/backend/snowball/meson.build +++ b/src/backend/snowball/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group dict_snowball_sources = files( 'libstemmer/api.c', @@ -11,6 +11,7 @@ dict_snowball_sources += files( 'libstemmer/stem_ISO_8859_1_catalan.c', 'libstemmer/stem_ISO_8859_1_danish.c', 'libstemmer/stem_ISO_8859_1_dutch.c', + 'libstemmer/stem_ISO_8859_1_dutch_porter.c', 'libstemmer/stem_ISO_8859_1_english.c', 'libstemmer/stem_ISO_8859_1_finnish.c', 'libstemmer/stem_ISO_8859_1_french.c', @@ -24,6 +25,7 @@ dict_snowball_sources += files( 'libstemmer/stem_ISO_8859_1_spanish.c', 'libstemmer/stem_ISO_8859_1_swedish.c', 'libstemmer/stem_ISO_8859_2_hungarian.c', + 'libstemmer/stem_ISO_8859_2_polish.c', 'libstemmer/stem_KOI8_R_russian.c', 'libstemmer/stem_UTF_8_arabic.c', 'libstemmer/stem_UTF_8_armenian.c', @@ -31,7 +33,9 @@ dict_snowball_sources += files( 'libstemmer/stem_UTF_8_catalan.c', 'libstemmer/stem_UTF_8_danish.c', 'libstemmer/stem_UTF_8_dutch.c', + 'libstemmer/stem_UTF_8_dutch_porter.c', 'libstemmer/stem_UTF_8_english.c', + 'libstemmer/stem_UTF_8_esperanto.c', 'libstemmer/stem_UTF_8_estonian.c', 'libstemmer/stem_UTF_8_finnish.c', 'libstemmer/stem_UTF_8_french.c', @@ -45,6 +49,7 @@ dict_snowball_sources += files( 'libstemmer/stem_UTF_8_lithuanian.c', 'libstemmer/stem_UTF_8_nepali.c', 'libstemmer/stem_UTF_8_norwegian.c', + 'libstemmer/stem_UTF_8_polish.c', 'libstemmer/stem_UTF_8_porter.c', 'libstemmer/stem_UTF_8_portuguese.c', 'libstemmer/stem_UTF_8_romanian.c', @@ -57,8 +62,9 @@ dict_snowball_sources += files( 'libstemmer/stem_UTF_8_yiddish.c', ) -# see comment in src/include/snowball/header.h -stemmer_inc = include_directories('../../include/snowball') +# see comment in src/include/snowball/snowball_runtime.h +stemmer_inc = include_directories('../../include/snowball', + '../../include/snowball/libstemmer') if host_system == 'windows' dict_snowball_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ diff --git a/src/backend/snowball/snowball.sql.in b/src/backend/snowball/snowball.sql.in index c0692a7750840..428a7592e65b9 100644 --- a/src/backend/snowball/snowball.sql.in +++ b/src/backend/snowball/snowball.sql.in @@ -1,7 +1,7 @@ /* * text search configuration for _LANGNAME_ language * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/backend/snowball/snowball.sql.in * diff --git a/src/backend/snowball/snowball_create.pl b/src/backend/snowball/snowball_create.pl index dffa8feb76900..3043f1117e57a 100644 --- a/src/backend/snowball/snowball_create.pl +++ b/src/backend/snowball/snowball_create.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -19,6 +19,7 @@ danish dutch english + esperanto estonian finnish french @@ -32,6 +33,7 @@ lithuanian nepali norwegian + polish portuguese romanian russian diff --git a/src/backend/snowball/snowball_func.sql.in b/src/backend/snowball/snowball_func.sql.in index 4e94f46bf1812..56ccb5f08dea3 100644 --- a/src/backend/snowball/snowball_func.sql.in +++ b/src/backend/snowball/snowball_func.sql.in @@ -1,7 +1,7 @@ /* * Create underlying C functions for Snowball stemmers * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/backend/snowball/snowball_func.sql.in * diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile index 4672bd90f225b..7ff5938b02731 100644 --- a/src/backend/statistics/Makefile +++ b/src/backend/statistics/Makefile @@ -16,6 +16,7 @@ OBJS = \ attribute_stats.o \ dependencies.o \ extended_stats.o \ + extended_stats_funcs.o \ mcv.o \ mvdistinct.o \ relation_stats.o \ diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index ab198076401b0..a6b118a8d721a 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -6,7 +6,7 @@ * Code supporting the direct import of relation attribute statistics, similar * to what is done by the ANALYZE command. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -19,9 +19,9 @@ #include "access/heapam.h" #include "catalog/indexing.h" -#include "catalog/pg_collation.h" +#include "catalog/namespace.h" #include "catalog/pg_operator.h" -#include "nodes/nodeFuncs.h" +#include "nodes/makefuncs.h" #include "statistics/statistics.h" #include "statistics/stat_utils.h" #include "utils/array.h" @@ -30,9 +30,10 @@ #include "utils/lsyscache.h" #include "utils/syscache.h" -#define DEFAULT_NULL_FRAC Float4GetDatum(0.0) -#define DEFAULT_AVG_WIDTH Int32GetDatum(0) /* unknown */ -#define DEFAULT_N_DISTINCT Float4GetDatum(0.0) /* unknown */ +/* + * Positional argument numbers, names, and types for + * attribute_statistics_update() and pg_restore_attribute_stats(). + */ enum attribute_stats_argnum { @@ -80,6 +81,11 @@ static struct StatsArgInfo attarginfo[] = [NUM_ATTRIBUTE_STATS_ARGS] = {0} }; +/* + * Positional argument numbers, names, and types for + * pg_clear_attribute_stats(). + */ + enum clear_attribute_stats_argnum { C_ATTRELSCHEMA_ARG = 0, @@ -99,24 +105,9 @@ static struct StatsArgInfo cleararginfo[] = }; static bool attribute_statistics_update(FunctionCallInfo fcinfo); -static Node *get_attr_expr(Relation rel, int attnum); -static void get_attr_stat_type(Oid reloid, AttrNumber attnum, - Oid *atttypid, int32 *atttypmod, - char *atttyptype, Oid *atttypcoll, - Oid *eq_opr, Oid *lt_opr); -static bool get_elem_stat_type(Oid atttypid, char atttyptype, - Oid *elemtypid, Oid *elem_eq_opr); -static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, - Oid typid, int32 typmod, bool *ok); -static void set_stats_slot(Datum *values, bool *nulls, bool *replaces, - int16 stakind, Oid staop, Oid stacoll, - Datum stanumbers, bool stanumbers_isnull, - Datum stavalues, bool stavalues_isnull); static void upsert_pg_statistic(Relation starel, HeapTuple oldtup, - Datum *values, bool *nulls, bool *replaces); + const Datum *values, const bool *nulls, const bool *replaces); static bool delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit); -static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited, - Datum *values, bool *nulls, bool *replaces); /* * Insert or Update Attribute Statistics @@ -143,6 +134,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo) char *attname; AttrNumber attnum; bool inherited; + Oid locked_table = InvalidOid; Relation starel; HeapTuple statup; @@ -182,8 +174,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo) nspname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELSCHEMA_ARG)); relname = TextDatumGetCString(PG_GETARG_DATUM(ATTRELNAME_ARG)); - reloid = stats_lookup_relid(nspname, relname); - if (RecoveryInProgress()) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), @@ -191,7 +181,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo) errhint("Statistics cannot be modified during recovery."))); /* lock before looking up attribute */ - stats_lock_check_privileges(reloid); + reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1), + ShareUpdateExclusiveLock, 0, + RangeVarCallbackForStats, &locked_table); /* user can specify either attname or attnum, but not both */ if (!PG_ARGISNULL(ATTNAME_ARG)) @@ -199,7 +191,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo) if (!PG_ARGISNULL(ATTNUM_ARG)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("cannot specify both attname and attnum"))); + errmsg("cannot specify both \"%s\" and \"%s\"", "attname", "attnum"))); attname = TextDatumGetCString(PG_GETARG_DATUM(ATTNAME_ARG)); attnum = get_attnum(reloid, attname); /* note that this test covers attisdropped cases too: */ @@ -225,7 +217,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("must specify either attname or attnum"))); + errmsg("must specify either \"%s\" or \"%s\"", "attname", "attnum"))); attname = NULL; /* keep compiler quiet */ attnum = 0; } @@ -285,20 +277,21 @@ attribute_statistics_update(FunctionCallInfo fcinfo) } /* derive information from attribute */ - get_attr_stat_type(reloid, attnum, - &atttypid, &atttypmod, - &atttyptype, &atttypcoll, - &eq_opr, <_opr); + statatt_get_type(reloid, attnum, + &atttypid, &atttypmod, + &atttyptype, &atttypcoll, + &eq_opr, <_opr); /* if needed, derive element type */ if (do_mcelem || do_dechist) { - if (!get_elem_stat_type(atttypid, atttyptype, - &elemtypid, &elem_eq_opr)) + if (!statatt_get_elem_type(atttypid, atttyptype, + &elemtypid, &elem_eq_opr)) { ereport(WARNING, - (errmsg("unable to determine element type of attribute \"%s\"", attname), - errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST."))); + (errmsg("could not determine element type of column \"%s\"", attname), + errdetail("Cannot set %s or %s.", + "STATISTIC_KIND_MCELEM", "STATISTIC_KIND_DECHIST"))); elemtypid = InvalidOid; elem_eq_opr = InvalidOid; @@ -313,8 +306,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo) { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("could not determine less-than operator for attribute \"%s\"", attname), - errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION."))); + errmsg("could not determine less-than operator for column \"%s\"", attname), + errdetail("Cannot set %s or %s.", + "STATISTIC_KIND_HISTOGRAM", "STATISTIC_KIND_CORRELATION"))); do_histogram = false; do_correlation = false; @@ -327,8 +321,9 @@ attribute_statistics_update(FunctionCallInfo fcinfo) { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("attribute \"%s\" is not a range type", attname), - errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM."))); + errmsg("column \"%s\" is not a range type", attname), + errdetail("Cannot set %s or %s.", + "STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM", "STATISTIC_KIND_BOUNDS_HISTOGRAM"))); do_bounds_histogram = false; do_range_length_histogram = false; @@ -339,14 +334,14 @@ attribute_statistics_update(FunctionCallInfo fcinfo) starel = table_open(StatisticRelationId, RowExclusiveLock); - statup = SearchSysCache3(STATRELATTINH, reloid, attnum, inherited); + statup = SearchSysCache3(STATRELATTINH, ObjectIdGetDatum(reloid), Int16GetDatum(attnum), BoolGetDatum(inherited)); /* initialize from existing tuple if exists */ if (HeapTupleIsValid(statup)) heap_deform_tuple(statup, RelationGetDescr(starel), values, nulls); else - init_empty_stats_tuple(reloid, attnum, inherited, values, nulls, - replaces); + statatt_init_empty_tuple(reloid, attnum, inherited, values, nulls, + replaces); /* if specified, set to argument values */ if (!PG_ARGISNULL(NULL_FRAC_ARG)) @@ -370,18 +365,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo) { bool converted; Datum stanumbers = PG_GETARG_DATUM(MOST_COMMON_FREQS_ARG); - Datum stavalues = text_to_stavalues("most_common_vals", - &array_in_fn, - PG_GETARG_DATUM(MOST_COMMON_VALS_ARG), - atttypid, atttypmod, - &converted); + Datum stavalues = statatt_build_stavalues("most_common_vals", + &array_in_fn, + PG_GETARG_DATUM(MOST_COMMON_VALS_ARG), + atttypid, atttypmod, + &converted); if (converted) { - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_MCV, - eq_opr, atttypcoll, - stanumbers, false, stavalues, false); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_MCV, + eq_opr, atttypcoll, + stanumbers, false, stavalues, false); } else result = false; @@ -393,18 +388,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo) Datum stavalues; bool converted = false; - stavalues = text_to_stavalues("histogram_bounds", - &array_in_fn, - PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG), - atttypid, atttypmod, - &converted); + stavalues = statatt_build_stavalues("histogram_bounds", + &array_in_fn, + PG_GETARG_DATUM(HISTOGRAM_BOUNDS_ARG), + atttypid, atttypmod, + &converted); if (converted) { - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_HISTOGRAM, - lt_opr, atttypcoll, - 0, true, stavalues, false); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_HISTOGRAM, + lt_opr, atttypcoll, + 0, true, stavalues, false); } else result = false; @@ -417,10 +412,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo) ArrayType *arry = construct_array_builtin(elems, 1, FLOAT4OID); Datum stanumbers = PointerGetDatum(arry); - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_CORRELATION, - lt_opr, atttypcoll, - stanumbers, false, 0, true); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_CORRELATION, + lt_opr, atttypcoll, + stanumbers, false, 0, true); } /* STATISTIC_KIND_MCELEM */ @@ -430,18 +425,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo) bool converted = false; Datum stavalues; - stavalues = text_to_stavalues("most_common_elems", - &array_in_fn, - PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG), - elemtypid, atttypmod, - &converted); + stavalues = statatt_build_stavalues("most_common_elems", + &array_in_fn, + PG_GETARG_DATUM(MOST_COMMON_ELEMS_ARG), + elemtypid, atttypmod, + &converted); if (converted) { - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_MCELEM, - elem_eq_opr, atttypcoll, - stanumbers, false, stavalues, false); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_MCELEM, + elem_eq_opr, atttypcoll, + stanumbers, false, stavalues, false); } else result = false; @@ -452,10 +447,10 @@ attribute_statistics_update(FunctionCallInfo fcinfo) { Datum stanumbers = PG_GETARG_DATUM(ELEM_COUNT_HISTOGRAM_ARG); - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_DECHIST, - elem_eq_opr, atttypcoll, - stanumbers, false, 0, true); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_DECHIST, + elem_eq_opr, atttypcoll, + stanumbers, false, 0, true); } /* @@ -470,18 +465,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo) bool converted = false; Datum stavalues; - stavalues = text_to_stavalues("range_bounds_histogram", - &array_in_fn, - PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG), - atttypid, atttypmod, - &converted); + stavalues = statatt_build_stavalues("range_bounds_histogram", + &array_in_fn, + PG_GETARG_DATUM(RANGE_BOUNDS_HISTOGRAM_ARG), + atttypid, atttypmod, + &converted); if (converted) { - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_BOUNDS_HISTOGRAM, - InvalidOid, InvalidOid, - 0, true, stavalues, false); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_BOUNDS_HISTOGRAM, + InvalidOid, InvalidOid, + 0, true, stavalues, false); } else result = false; @@ -498,17 +493,17 @@ attribute_statistics_update(FunctionCallInfo fcinfo) bool converted = false; Datum stavalues; - stavalues = text_to_stavalues("range_length_histogram", - &array_in_fn, - PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG), - FLOAT8OID, 0, &converted); + stavalues = statatt_build_stavalues("range_length_histogram", + &array_in_fn, + PG_GETARG_DATUM(RANGE_LENGTH_HISTOGRAM_ARG), + FLOAT8OID, 0, &converted); if (converted) { - set_stats_slot(values, nulls, replaces, - STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM, - Float8LessOperator, InvalidOid, - stanumbers, false, stavalues, false); + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM, + Float8LessOperator, InvalidOid, + stanumbers, false, stavalues, false); } else result = false; @@ -523,297 +518,12 @@ attribute_statistics_update(FunctionCallInfo fcinfo) return result; } -/* - * If this relation is an index and that index has expressions in it, and - * the attnum specified is known to be an expression, then we must walk - * the list attributes up to the specified attnum to get the right - * expression. - */ -static Node * -get_attr_expr(Relation rel, int attnum) -{ - List *index_exprs; - ListCell *indexpr_item; - - /* relation is not an index */ - if (rel->rd_rel->relkind != RELKIND_INDEX && - rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) - return NULL; - - index_exprs = RelationGetIndexExpressions(rel); - - /* index has no expressions to give */ - if (index_exprs == NIL) - return NULL; - - /* - * The index attnum points directly to a relation attnum, then it's not an - * expression attribute. - */ - if (rel->rd_index->indkey.values[attnum - 1] != 0) - return NULL; - - indexpr_item = list_head(rel->rd_indexprs); - - for (int i = 0; i < attnum - 1; i++) - if (rel->rd_index->indkey.values[i] == 0) - indexpr_item = lnext(rel->rd_indexprs, indexpr_item); - - if (indexpr_item == NULL) /* shouldn't happen */ - elog(ERROR, "too few entries in indexprs list"); - - return (Node *) lfirst(indexpr_item); -} - -/* - * Derive type information from the attribute. - */ -static void -get_attr_stat_type(Oid reloid, AttrNumber attnum, - Oid *atttypid, int32 *atttypmod, - char *atttyptype, Oid *atttypcoll, - Oid *eq_opr, Oid *lt_opr) -{ - Relation rel = relation_open(reloid, AccessShareLock); - Form_pg_attribute attr; - HeapTuple atup; - Node *expr; - TypeCacheEntry *typcache; - - atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid), - Int16GetDatum(attnum)); - - /* Attribute not found */ - if (!HeapTupleIsValid(atup)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("attribute %d of relation \"%s\" does not exist", - attnum, RelationGetRelationName(rel)))); - - attr = (Form_pg_attribute) GETSTRUCT(atup); - - if (attr->attisdropped) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("attribute %d of relation \"%s\" does not exist", - attnum, RelationGetRelationName(rel)))); - - expr = get_attr_expr(rel, attr->attnum); - - /* - * When analyzing an expression index, believe the expression tree's type - * not the column datatype --- the latter might be the opckeytype storage - * type of the opclass, which is not interesting for our purposes. This - * mimics the behavior of examine_attribute(). - */ - if (expr == NULL) - { - *atttypid = attr->atttypid; - *atttypmod = attr->atttypmod; - *atttypcoll = attr->attcollation; - } - else - { - *atttypid = exprType(expr); - *atttypmod = exprTypmod(expr); - - if (OidIsValid(attr->attcollation)) - *atttypcoll = attr->attcollation; - else - *atttypcoll = exprCollation(expr); - } - ReleaseSysCache(atup); - - /* - * If it's a multirange, step down to the range type, as is done by - * multirange_typanalyze(). - */ - if (type_is_multirange(*atttypid)) - *atttypid = get_multirange_range(*atttypid); - - /* finds the right operators even if atttypid is a domain */ - typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR); - *atttyptype = typcache->typtype; - *eq_opr = typcache->eq_opr; - *lt_opr = typcache->lt_opr; - - /* - * Special case: collation for tsvector is DEFAULT_COLLATION_OID. See - * compute_tsvector_stats(). - */ - if (*atttypid == TSVECTOROID) - *atttypcoll = DEFAULT_COLLATION_OID; - - relation_close(rel, NoLock); -} - -/* - * Derive element type information from the attribute type. - */ -static bool -get_elem_stat_type(Oid atttypid, char atttyptype, - Oid *elemtypid, Oid *elem_eq_opr) -{ - TypeCacheEntry *elemtypcache; - - if (atttypid == TSVECTOROID) - { - /* - * Special case: element type for tsvector is text. See - * compute_tsvector_stats(). - */ - *elemtypid = TEXTOID; - } - else - { - /* find underlying element type through any domain */ - *elemtypid = get_base_element_type(atttypid); - } - - if (!OidIsValid(*elemtypid)) - return false; - - /* finds the right operator even if elemtypid is a domain */ - elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR); - if (!OidIsValid(elemtypcache->eq_opr)) - return false; - - *elem_eq_opr = elemtypcache->eq_opr; - - return true; -} - -/* - * Cast a text datum into an array with element type elemtypid. - * - * If an error is encountered, capture it and re-throw a WARNING, and set ok - * to false. If the resulting array contains NULLs, raise a WARNING and set ok - * to false. Otherwise, set ok to true. - */ -static Datum -text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid, - int32 typmod, bool *ok) -{ - LOCAL_FCINFO(fcinfo, 8); - char *s; - Datum result; - ErrorSaveContext escontext = {T_ErrorSaveContext}; - - escontext.details_wanted = true; - - s = TextDatumGetCString(d); - - InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid, - (Node *) &escontext, NULL); - - fcinfo->args[0].value = CStringGetDatum(s); - fcinfo->args[0].isnull = false; - fcinfo->args[1].value = ObjectIdGetDatum(typid); - fcinfo->args[1].isnull = false; - fcinfo->args[2].value = Int32GetDatum(typmod); - fcinfo->args[2].isnull = false; - - result = FunctionCallInvoke(fcinfo); - - pfree(s); - - if (escontext.error_occurred) - { - escontext.error_data->elevel = WARNING; - ThrowErrorData(escontext.error_data); - *ok = false; - return (Datum) 0; - } - - if (array_contains_nulls(DatumGetArrayTypeP(result))) - { - ereport(WARNING, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"%s\" array cannot contain NULL values", staname))); - *ok = false; - return (Datum) 0; - } - - *ok = true; - - return result; -} - -/* - * Find and update the slot with the given stakind, or use the first empty - * slot. - */ -static void -set_stats_slot(Datum *values, bool *nulls, bool *replaces, - int16 stakind, Oid staop, Oid stacoll, - Datum stanumbers, bool stanumbers_isnull, - Datum stavalues, bool stavalues_isnull) -{ - int slotidx; - int first_empty = -1; - AttrNumber stakind_attnum; - AttrNumber staop_attnum; - AttrNumber stacoll_attnum; - - /* find existing slot with given stakind */ - for (slotidx = 0; slotidx < STATISTIC_NUM_SLOTS; slotidx++) - { - stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx; - - if (first_empty < 0 && - DatumGetInt16(values[stakind_attnum]) == 0) - first_empty = slotidx; - if (DatumGetInt16(values[stakind_attnum]) == stakind) - break; - } - - if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0) - slotidx = first_empty; - - if (slotidx >= STATISTIC_NUM_SLOTS) - ereport(ERROR, - (errmsg("maximum number of statistics slots exceeded: %d", - slotidx + 1))); - - stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx; - staop_attnum = Anum_pg_statistic_staop1 - 1 + slotidx; - stacoll_attnum = Anum_pg_statistic_stacoll1 - 1 + slotidx; - - if (DatumGetInt16(values[stakind_attnum]) != stakind) - { - values[stakind_attnum] = Int16GetDatum(stakind); - replaces[stakind_attnum] = true; - } - if (DatumGetObjectId(values[staop_attnum]) != staop) - { - values[staop_attnum] = ObjectIdGetDatum(staop); - replaces[staop_attnum] = true; - } - if (DatumGetObjectId(values[stacoll_attnum]) != stacoll) - { - values[stacoll_attnum] = ObjectIdGetDatum(stacoll); - replaces[stacoll_attnum] = true; - } - if (!stanumbers_isnull) - { - values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers; - nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false; - replaces[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = true; - } - if (!stavalues_isnull) - { - values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues; - nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false; - replaces[Anum_pg_statistic_stavalues1 - 1 + slotidx] = true; - } -} - /* * Upsert the pg_statistic record. */ static void upsert_pg_statistic(Relation starel, HeapTuple oldtup, - Datum *values, bool *nulls, bool *replaces) + const Datum *values, const bool *nulls, const bool *replaces) { HeapTuple newtup; @@ -864,44 +574,6 @@ delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit) return result; } -/* - * Initialize values and nulls for a new stats tuple. - */ -static void -init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited, - Datum *values, bool *nulls, bool *replaces) -{ - memset(nulls, true, sizeof(bool) * Natts_pg_statistic); - memset(replaces, true, sizeof(bool) * Natts_pg_statistic); - - /* must initialize non-NULL attributes */ - - values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid); - nulls[Anum_pg_statistic_starelid - 1] = false; - values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum); - nulls[Anum_pg_statistic_staattnum - 1] = false; - values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited); - nulls[Anum_pg_statistic_stainherit - 1] = false; - - values[Anum_pg_statistic_stanullfrac - 1] = DEFAULT_NULL_FRAC; - nulls[Anum_pg_statistic_stanullfrac - 1] = false; - values[Anum_pg_statistic_stawidth - 1] = DEFAULT_AVG_WIDTH; - nulls[Anum_pg_statistic_stawidth - 1] = false; - values[Anum_pg_statistic_stadistinct - 1] = DEFAULT_N_DISTINCT; - nulls[Anum_pg_statistic_stadistinct - 1] = false; - - /* initialize stakind, staop, and stacoll slots */ - for (int slotnum = 0; slotnum < STATISTIC_NUM_SLOTS; slotnum++) - { - values[Anum_pg_statistic_stakind1 + slotnum - 1] = (Datum) 0; - nulls[Anum_pg_statistic_stakind1 + slotnum - 1] = false; - values[Anum_pg_statistic_staop1 + slotnum - 1] = InvalidOid; - nulls[Anum_pg_statistic_staop1 + slotnum - 1] = false; - values[Anum_pg_statistic_stacoll1 + slotnum - 1] = InvalidOid; - nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false; - } -} - /* * Delete statistics for the given attribute. */ @@ -914,6 +586,7 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS) char *attname; AttrNumber attnum; bool inherited; + Oid locked_table = InvalidOid; stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELSCHEMA_ARG); stats_check_required_arg(fcinfo, cleararginfo, C_ATTRELNAME_ARG); @@ -923,15 +596,15 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS) nspname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELSCHEMA_ARG)); relname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTRELNAME_ARG)); - reloid = stats_lookup_relid(nspname, relname); - if (RecoveryInProgress()) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("recovery is in progress"), errhint("Statistics cannot be modified during recovery."))); - stats_lock_check_privileges(reloid); + reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1), + ShareUpdateExclusiveLock, 0, + RangeVarCallbackForStats, &locked_table); attname = TextDatumGetCString(PG_GETARG_DATUM(C_ATTNAME_ARG)); attnum = get_attnum(reloid, attname); diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c index eb2fc4366b4a7..e3a2f5817e0c7 100644 --- a/src/backend/statistics/dependencies.c +++ b/src/backend/statistics/dependencies.c @@ -3,7 +3,7 @@ * dependencies.c * POSTGRES functional dependencies * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -16,23 +16,17 @@ #include "access/htup_details.h" #include "catalog/pg_statistic_ext.h" #include "catalog/pg_statistic_ext_data.h" -#include "lib/stringinfo.h" #include "nodes/nodeFuncs.h" -#include "nodes/nodes.h" -#include "nodes/pathnodes.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "parser/parsetree.h" #include "statistics/extended_stats_internal.h" -#include "statistics/statistics.h" #include "utils/fmgroids.h" -#include "utils/fmgrprotos.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/selfuncs.h" #include "utils/syscache.h" #include "utils/typcache.h" -#include "varatt.h" /* size of the struct header fields (magic, type, ndeps) */ #define SizeOfHeader (3 * sizeof(uint32)) @@ -156,7 +150,7 @@ generate_dependencies_recurse(DependencyGenerator state, int index, static void generate_dependencies(DependencyGenerator state) { - AttrNumber *current = (AttrNumber *) palloc0(sizeof(AttrNumber) * state->k); + AttrNumber *current = palloc0_array(AttrNumber, state->k); generate_dependencies_recurse(state, 0, 0, current); @@ -177,8 +171,8 @@ DependencyGenerator_init(int n, int k) Assert((n >= k) && (k > 0)); /* allocate the DependencyGenerator state */ - state = (DependencyGenerator) palloc0(sizeof(DependencyGeneratorData)); - state->dependencies = (AttrNumber *) palloc(k * sizeof(AttrNumber)); + state = palloc0_object(DependencyGeneratorData); + state->dependencies = palloc_array(AttrNumber, k); state->ndependencies = 0; state->current = 0; @@ -243,7 +237,7 @@ dependency_degree(StatsBuildData *data, int k, AttrNumber *dependency) * Translate the array of indexes to regular attnums for the dependency * (we will need this to identify the columns in StatsBuildData). */ - attnums_dep = (AttrNumber *) palloc(k * sizeof(AttrNumber)); + attnums_dep = palloc_array(AttrNumber, k); for (i = 0; i < k; i++) attnums_dep[i] = data->attnums[dependency[i]]; @@ -408,8 +402,7 @@ statext_dependencies_build(StatsBuildData *data) /* initialize the list of dependencies */ if (dependencies == NULL) { - dependencies - = (MVDependencies *) palloc0(sizeof(MVDependencies)); + dependencies = palloc0_object(MVDependencies); dependencies->magic = STATS_DEPS_MAGIC; dependencies->type = STATS_DEPS_TYPE_BASIC; @@ -511,7 +504,7 @@ statext_dependencies_deserialize(bytea *data) VARSIZE_ANY_EXHDR(data), SizeOfHeader); /* read the MVDependencies header */ - dependencies = (MVDependencies *) palloc0(sizeof(MVDependencies)); + dependencies = palloc0_object(MVDependencies); /* initialize pointer to the data part (skip the varlena header) */ tmp = VARDATA_ANY(data); @@ -586,6 +579,82 @@ statext_dependencies_deserialize(bytea *data) return dependencies; } +/* + * Free allocations of a MVDependencies. + */ +void +statext_dependencies_free(MVDependencies *dependencies) +{ + for (int i = 0; i < dependencies->ndeps; i++) + pfree(dependencies->deps[i]); + pfree(dependencies); +} + +/* + * Validate a set of MVDependencies against the extended statistics object + * definition. + * + * Every MVDependencies must be checked to ensure that the attnums in the + * attributes list correspond to attnums/expressions defined by the + * extended statistics object. + * + * Positive attnums are attributes which must be found in the stxkeys, while + * negative attnums correspond to an expression number, no attribute number + * can be below (0 - numexprs). + */ +bool +statext_dependencies_validate(const MVDependencies *dependencies, + const int2vector *stxkeys, + int numexprs, int elevel) +{ + int attnum_expr_lowbound = 0 - numexprs; + + /* Scan through each dependency entry */ + for (int i = 0; i < dependencies->ndeps; i++) + { + const MVDependency *dep = dependencies->deps[i]; + + /* + * Cross-check each attribute in a dependency entry with the extended + * stats object definition. + */ + for (int j = 0; j < dep->nattributes; j++) + { + AttrNumber attnum = dep->attributes[j]; + bool ok = false; + + if (attnum > 0) + { + /* attribute number in stxkeys */ + for (int k = 0; k < stxkeys->dim1; k++) + { + if (attnum == stxkeys->values[k]) + { + ok = true; + break; + } + } + } + else if ((attnum < 0) && (attnum >= attnum_expr_lowbound)) + { + /* attribute number for an expression */ + ok = true; + } + + if (!ok) + { + ereport(elevel, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("could not validate \"%s\" object: invalid attribute number %d found", + "pg_dependencies", attnum))); + return false; + } + } + } + + return true; +} + /* * dependency_is_fully_matched * checks that a functional dependency is fully matched given clauses on @@ -643,91 +712,6 @@ statext_dependencies_load(Oid mvoid, bool inh) return result; } -/* - * pg_dependencies_in - input routine for type pg_dependencies. - * - * pg_dependencies is real enough to be a table column, but it has no operations - * of its own, and disallows input too - */ -Datum -pg_dependencies_in(PG_FUNCTION_ARGS) -{ - /* - * pg_node_list stores the data in binary form and parsing text input is - * not needed, so disallow this. - */ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot accept a value of type %s", "pg_dependencies"))); - - PG_RETURN_VOID(); /* keep compiler quiet */ -} - -/* - * pg_dependencies - output routine for type pg_dependencies. - */ -Datum -pg_dependencies_out(PG_FUNCTION_ARGS) -{ - bytea *data = PG_GETARG_BYTEA_PP(0); - MVDependencies *dependencies = statext_dependencies_deserialize(data); - int i, - j; - StringInfoData str; - - initStringInfo(&str); - appendStringInfoChar(&str, '{'); - - for (i = 0; i < dependencies->ndeps; i++) - { - MVDependency *dependency = dependencies->deps[i]; - - if (i > 0) - appendStringInfoString(&str, ", "); - - appendStringInfoChar(&str, '"'); - for (j = 0; j < dependency->nattributes; j++) - { - if (j == dependency->nattributes - 1) - appendStringInfoString(&str, " => "); - else if (j > 0) - appendStringInfoString(&str, ", "); - - appendStringInfo(&str, "%d", dependency->attributes[j]); - } - appendStringInfo(&str, "\": %f", dependency->degree); - } - - appendStringInfoChar(&str, '}'); - - PG_RETURN_CSTRING(str.data); -} - -/* - * pg_dependencies_recv - binary input routine for type pg_dependencies. - */ -Datum -pg_dependencies_recv(PG_FUNCTION_ARGS) -{ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot accept a value of type %s", "pg_dependencies"))); - - PG_RETURN_VOID(); /* keep compiler quiet */ -} - -/* - * pg_dependencies_send - binary output routine for type pg_dependencies. - * - * Functional dependencies are serialized in a bytea value (although the type - * is named differently), so let's just send that. - */ -Datum -pg_dependencies_send(PG_FUNCTION_ARGS) -{ - return byteasend(fcinfo); -} - /* * dependency_is_compatible_clause * Determines if the clause is compatible with functional dependencies @@ -876,7 +860,7 @@ dependency_is_compatible_clause(Node *clause, Index relid, AttrNumber *attnum) * A boolean expression "x" can be interpreted as "x = true", so * proceed with seeing if it's a suitable Var. */ - clause_expr = (Node *) clause; + clause_expr = clause; } /* @@ -1050,7 +1034,7 @@ clauselist_apply_dependencies(PlannerInfo *root, List *clauses, * and mark all the corresponding clauses as estimated. */ nattrs = bms_num_members(attnums); - attr_sel = (Selectivity *) palloc(sizeof(Selectivity) * nattrs); + attr_sel = palloc_array(Selectivity, nattrs); attidx = 0; i = -1; @@ -1303,7 +1287,7 @@ dependency_is_compatible_expression(Node *clause, Index relid, List *statlist, N * A boolean expression "x" can be interpreted as "x = true", so * proceed with seeing if it's a suitable Var. */ - clause_expr = (Node *) clause; + clause_expr = clause; } /* @@ -1397,8 +1381,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, if (!has_stats_of_kind(rel->statlist, STATS_EXT_DEPENDENCIES)) return 1.0; - list_attnums = (AttrNumber *) palloc(sizeof(AttrNumber) * - list_length(clauses)); + list_attnums = palloc_array(AttrNumber, list_length(clauses)); /* * We allocate space as if every clause was a unique expression, although @@ -1406,7 +1389,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, * we'll translate to attnums, and there might be duplicates. But it's * easier and cheaper to just do one allocation than repalloc later. */ - unique_exprs = (Node **) palloc(sizeof(Node *) * list_length(clauses)); + unique_exprs = palloc_array(Node *, list_length(clauses)); unique_exprs_cnt = 0; /* @@ -1559,8 +1542,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, * make it just the right size, but it's likely wasteful anyway thanks to * moving the freed chunks to freelists etc. */ - func_dependencies = (MVDependencies **) palloc(sizeof(MVDependencies *) * - list_length(rel->statlist)); + func_dependencies = palloc_array(MVDependencies *, list_length(rel->statlist)); nfunc_dependencies = 0; total_ndeps = 0; @@ -1783,8 +1765,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root, * Work out which dependencies we can apply, starting with the * widest/strongest ones, and proceeding to smaller/weaker ones. */ - dependencies = (MVDependency **) palloc(sizeof(MVDependency *) * - total_ndeps); + dependencies = palloc_array(MVDependency *, total_ndeps); ndependencies = 0; while (true) diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c index a8b63ec0884a9..2b83355d26ea6 100644 --- a/src/backend/statistics/extended_stats.c +++ b/src/backend/statistics/extended_stats.c @@ -6,7 +6,7 @@ * Generic code supporting statistics objects created via CREATE STATISTICS. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -32,6 +32,7 @@ #include "parser/parsetree.h" #include "pgstat.h" #include "postmaster/autovacuum.h" +#include "rewrite/rewriteHandler.h" #include "statistics/extended_stats_internal.h" #include "statistics/statistics.h" #include "utils/acl.h" @@ -73,7 +74,7 @@ typedef struct StatExtEntry } StatExtEntry; -static List *fetch_statentries_for_relation(Relation pg_statext, Oid relid); +static List *fetch_statentries_for_relation(Relation pg_statext, Relation rel); static VacAttrStats **lookup_var_attr_stats(Bitmapset *attrs, List *exprs, int nvacatts, VacAttrStats **vacatts); static void statext_store(Oid statOid, bool inh, @@ -125,7 +126,7 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows, /* the list of stats has to be allocated outside the memory context */ pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock); - statslist = fetch_statentries_for_relation(pg_stext, RelationGetRelid(onerel)); + statslist = fetch_statentries_for_relation(pg_stext, onerel); /* memory context for building each statistics object */ cxt = AllocSetContextCreate(CurrentMemoryContext, @@ -245,6 +246,40 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows, table_close(pg_stext, RowExclusiveLock); } +/* + * Test if the given relation has extended statistics objects. + */ +bool +HasRelationExtStatistics(Relation onerel) +{ + Relation pg_statext; + SysScanDesc scan; + ScanKeyData skey; + bool found; + + pg_statext = table_open(StatisticExtRelationId, RowExclusiveLock); + + /* + * Prepare to scan pg_statistic_ext for entries having stxrelid = this + * rel. + */ + ScanKeyInit(&skey, + Anum_pg_statistic_ext_stxrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(onerel))); + + scan = systable_beginscan(pg_statext, StatisticExtRelidIndexId, true, + NULL, 1, &skey); + + found = HeapTupleIsValid(systable_getnext(scan)); + + systable_endscan(scan); + + table_close(pg_statext, RowExclusiveLock); + + return found; +} + /* * ComputeExtStatisticsRows * Compute number of rows required by extended statistics on a table. @@ -279,7 +314,7 @@ ComputeExtStatisticsRows(Relation onerel, oldcxt = MemoryContextSwitchTo(cxt); pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock); - lstats = fetch_statentries_for_relation(pg_stext, RelationGetRelid(onerel)); + lstats = fetch_statentries_for_relation(pg_stext, onerel); foreach(lc, lstats) { @@ -416,12 +451,13 @@ statext_is_kind_built(HeapTuple htup, char type) * Return a list (of StatExtEntry) of statistics objects for the given relation. */ static List * -fetch_statentries_for_relation(Relation pg_statext, Oid relid) +fetch_statentries_for_relation(Relation pg_statext, Relation rel) { SysScanDesc scan; ScanKeyData skey; HeapTuple htup; List *result = NIL; + Oid relid = RelationGetRelid(rel); /* * Prepare to scan pg_statistic_ext for entries having stxrelid = this @@ -446,7 +482,7 @@ fetch_statentries_for_relation(Relation pg_statext, Oid relid) Form_pg_statistic_ext staForm; List *exprs = NIL; - entry = palloc0(sizeof(StatExtEntry)); + entry = palloc0_object(StatExtEntry); staForm = (Form_pg_statistic_ext) GETSTRUCT(htup); entry->statOid = staForm->oid; entry->schema = get_namespace_name(staForm->stxnamespace); @@ -491,6 +527,9 @@ fetch_statentries_for_relation(Relation pg_statext, Oid relid) pfree(exprsString); + /* Expand virtual generated columns in the expressions */ + exprs = (List *) expand_generated_columns_in_expr((Node *) exprs, rel, 1); + /* * Run the expressions through eval_const_expressions. This is not * just an optimization, but is necessary, because the planner @@ -532,7 +571,7 @@ examine_attribute(Node *expr) /* * Create the VacAttrStats struct. */ - stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats)); + stats = palloc0_object(VacAttrStats); stats->attstattarget = -1; /* @@ -613,7 +652,7 @@ examine_expression(Node *expr, int stattarget) /* * Create the VacAttrStats struct. */ - stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats)); + stats = palloc0_object(VacAttrStats); /* * We can't have statistics target specified for the expression, so we @@ -736,6 +775,16 @@ lookup_var_attr_stats(Bitmapset *attrs, List *exprs, stats[i] = examine_attribute(expr); + /* + * If the expression has been found as non-analyzable, give up. We + * will not be able to build extended stats with it. + */ + if (stats[i] == NULL) + { + pfree(stats); + return NULL; + } + /* * XXX We need tuple descriptor later, and we just grab it from * stats[0]->tupDesc (see e.g. statext_mcv_build). But as coded @@ -862,8 +911,8 @@ int multi_sort_compare(const void *a, const void *b, void *arg) { MultiSortSupport mss = (MultiSortSupport) arg; - SortItem *ia = (SortItem *) a; - SortItem *ib = (SortItem *) b; + const SortItem *ia = a; + const SortItem *ib = b; int i; for (i = 0; i < mss->ndims; i++) @@ -915,8 +964,8 @@ multi_sort_compare_dims(int start, int end, int compare_scalars_simple(const void *a, const void *b, void *arg) { - return compare_datums_simple(*(Datum *) a, - *(Datum *) b, + return compare_datums_simple(*(const Datum *) a, + *(const Datum *) b, (SortSupport) arg); } @@ -946,7 +995,7 @@ build_attnums_array(Bitmapset *attrs, int nexprs, int *numattrs) *numattrs = num; /* build attnums from the bitmapset */ - attnums = (AttrNumber *) palloc(sizeof(AttrNumber) * num); + attnums = palloc_array(AttrNumber, num); i = 0; j = -1; while ((j = bms_next_member(attrs, j)) >= 0) @@ -986,10 +1035,9 @@ build_sorted_items(StatsBuildData *data, int *nitems, { int i, j, - len, nrows; int nvalues = data->numrows * numattrs; - + Size len; SortItem *items; Datum *values; bool *isnull; @@ -997,14 +1045,16 @@ build_sorted_items(StatsBuildData *data, int *nitems, int *typlen; /* Compute the total amount of memory we need (both items and values). */ - len = data->numrows * sizeof(SortItem) + nvalues * (sizeof(Datum) + sizeof(bool)); + len = MAXALIGN(data->numrows * sizeof(SortItem)) + + nvalues * (sizeof(Datum) + sizeof(bool)); /* Allocate the memory and split it into the pieces. */ ptr = palloc0(len); /* items to sort */ items = (SortItem *) ptr; - ptr += data->numrows * sizeof(SortItem); + /* MAXALIGN ensures that the following Datums are suitably aligned */ + ptr += MAXALIGN(data->numrows * sizeof(SortItem)); /* values and null flags */ values = (Datum *) ptr; @@ -1027,7 +1077,7 @@ build_sorted_items(StatsBuildData *data, int *nitems, } /* build a local cache of typlen for all attributes */ - typlen = (int *) palloc(sizeof(int) * data->nattnums); + typlen = palloc_array(int, data->nattnums); for (i = 0; i < data->nattnums; i++) typlen[i] = get_typlen(data->stats[i]->attrtypid); @@ -1317,6 +1367,9 @@ choose_best_statistics(List *stats, char requiredkind, bool inh, * so we can't cope with system columns. * *exprs: input/output parameter collecting primitive subclauses within * the clause tree + * *leakproof: input/output parameter recording the leakproofness of the + * clause tree. This should be true initially, and will be set to false + * if any operator function used in an OpExpr is not leakproof. * * Returns false if there is something we definitively can't handle. * On true return, we can proceed to match the *exprs against statistics. @@ -1324,7 +1377,7 @@ choose_best_statistics(List *stats, char requiredkind, bool inh, static bool statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, Index relid, Bitmapset **attnums, - List **exprs) + List **exprs, bool *leakproof) { /* Look inside any binary-compatible relabeling (as in examine_variable) */ if (IsA(clause, RelabelType)) @@ -1359,7 +1412,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, /* (Var/Expr op Const) or (Const op Var/Expr) */ if (is_opclause(clause)) { - RangeTblEntry *rte = root->simple_rte_array[relid]; OpExpr *expr = (OpExpr *) clause; Node *clause_expr; @@ -1394,24 +1446,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, return false; } - /* - * If there are any securityQuals on the RTE from security barrier - * views or RLS policies, then the user may not have access to all the - * table's data, and we must check that the operator is leakproof. - * - * If the operator is leaky, then we must ignore this clause for the - * purposes of estimating with MCV lists, otherwise the operator might - * reveal values from the MCV list that the user doesn't have - * permission to see. - */ - if (rte->securityQuals != NIL && - !get_func_leakproof(get_opcode(expr->opno))) - return false; + /* Check if the operator is leakproof */ + if (*leakproof) + *leakproof = get_func_leakproof(get_opcode(expr->opno)); /* Check (Var op Const) or (Const op Var) clauses by recursing. */ if (IsA(clause_expr, Var)) return statext_is_compatible_clause_internal(root, clause_expr, - relid, attnums, exprs); + relid, attnums, + exprs, leakproof); /* Otherwise we have (Expr op Const) or (Const op Expr). */ *exprs = lappend(*exprs, clause_expr); @@ -1421,7 +1464,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, /* Var/Expr IN Array */ if (IsA(clause, ScalarArrayOpExpr)) { - RangeTblEntry *rte = root->simple_rte_array[relid]; ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) clause; Node *clause_expr; bool expronleft; @@ -1461,24 +1503,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, return false; } - /* - * If there are any securityQuals on the RTE from security barrier - * views or RLS policies, then the user may not have access to all the - * table's data, and we must check that the operator is leakproof. - * - * If the operator is leaky, then we must ignore this clause for the - * purposes of estimating with MCV lists, otherwise the operator might - * reveal values from the MCV list that the user doesn't have - * permission to see. - */ - if (rte->securityQuals != NIL && - !get_func_leakproof(get_opcode(expr->opno))) - return false; + /* Check if the operator is leakproof */ + if (*leakproof) + *leakproof = get_func_leakproof(get_opcode(expr->opno)); /* Check Var IN Array clauses by recursing. */ if (IsA(clause_expr, Var)) return statext_is_compatible_clause_internal(root, clause_expr, - relid, attnums, exprs); + relid, attnums, + exprs, leakproof); /* Otherwise we have Expr IN Array. */ *exprs = lappend(*exprs, clause_expr); @@ -1515,7 +1548,8 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, */ if (!statext_is_compatible_clause_internal(root, (Node *) lfirst(lc), - relid, attnums, exprs)) + relid, attnums, exprs, + leakproof)) return false; } @@ -1529,8 +1563,10 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause, /* Check Var IS NULL clauses by recursing. */ if (IsA(nt->arg, Var)) - return statext_is_compatible_clause_internal(root, (Node *) (nt->arg), - relid, attnums, exprs); + return statext_is_compatible_clause_internal(root, + (Node *) (nt->arg), + relid, attnums, + exprs, leakproof); /* Otherwise we have Expr IS NULL. */ *exprs = lappend(*exprs, nt->arg); @@ -1569,11 +1605,9 @@ static bool statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid, Bitmapset **attnums, List **exprs) { - RangeTblEntry *rte = root->simple_rte_array[relid]; - RelOptInfo *rel = root->simple_rel_array[relid]; RestrictInfo *rinfo; int clause_relid; - Oid userid; + bool leakproof; /* * Special-case handling for bare BoolExpr AND clauses, because the @@ -1613,18 +1647,31 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid, clause_relid != relid) return false; - /* Check the clause and determine what attributes it references. */ + /* + * Check the clause, determine what attributes it references, and whether + * it includes any non-leakproof operators. + */ + leakproof = true; if (!statext_is_compatible_clause_internal(root, (Node *) rinfo->clause, - relid, attnums, exprs)) + relid, attnums, exprs, + &leakproof)) return false; /* - * Check that the user has permission to read all required attributes. + * If the clause includes any non-leakproof operators, check that the user + * has permission to read all required attributes, otherwise the operators + * might reveal values from the MCV list that the user doesn't have + * permission to see. We require all rows to be selectable --- there must + * be no securityQuals from security barrier views or RLS policies. See + * similar code in examine_variable(), examine_simple_variable(), and + * statistic_proc_security_check(). + * + * Note that for an inheritance child, the permission checks are performed + * on the inheritance root parent, and whole-table select privilege on the + * parent doesn't guarantee that the user could read all columns of the + * child. Therefore we must check all referenced columns. */ - userid = OidIsValid(rel->userid) ? rel->userid : GetUserId(); - - /* Table-level SELECT privilege is sufficient for all columns */ - if (pg_class_aclcheck(rte->relid, userid, ACL_SELECT) != ACLCHECK_OK) + if (!leakproof) { Bitmapset *clause_attnums = NULL; int attnum = -1; @@ -1649,26 +1696,9 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid, if (*exprs != NIL) pull_varattnos((Node *) *exprs, relid, &clause_attnums); - attnum = -1; - while ((attnum = bms_next_member(clause_attnums, attnum)) >= 0) - { - /* Undo the offset */ - AttrNumber attno = attnum + FirstLowInvalidHeapAttributeNumber; - - if (attno == InvalidAttrNumber) - { - /* Whole-row reference, so must have access to all columns */ - if (pg_attribute_aclcheck_all(rte->relid, userid, ACL_SELECT, - ACLMASK_ALL) != ACLCHECK_OK) - return false; - } - else - { - if (pg_attribute_aclcheck(rte->relid, attno, userid, - ACL_SELECT) != ACLCHECK_OK) - return false; - } - } + /* Must have permission to read all rows from these columns */ + if (!all_rows_selectable(root, relid, clause_attnums)) + return false; } /* If we reach here, the clause is OK */ @@ -1726,11 +1756,10 @@ statext_mcv_clauselist_selectivity(PlannerInfo *root, List *clauses, int varReli if (!has_stats_of_kind(rel->statlist, STATS_EXT_MCV)) return sel; - list_attnums = (Bitmapset **) palloc(sizeof(Bitmapset *) * - list_length(clauses)); + list_attnums = palloc_array(Bitmapset *, list_length(clauses)); /* expressions extracted from complex expressions */ - list_exprs = (List **) palloc(sizeof(Node *) * list_length(clauses)); + list_exprs = palloc_array(List *, list_length(clauses)); /* * Pre-process the clauses list to extract the attnums and expressions @@ -2073,13 +2102,13 @@ examine_opclause_args(List *args, Node **exprp, Const **cstp, if (IsA(rightop, Const)) { - expr = (Node *) leftop; + expr = leftop; cst = (Const *) rightop; expronleft = true; } else if (IsA(leftop, Const)) { - expr = (Node *) rightop; + expr = rightop; cst = (Const *) leftop; expronleft = false; } @@ -2416,6 +2445,9 @@ serialize_expr_stats(AnlExprData *exprdata, int nexprs) /* * Loads pg_statistic record from expression statistics for expression * identified by the supplied index. + * + * Returns the pg_statistic record found, or NULL if there is no statistics + * data to use. */ HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx) @@ -2444,6 +2476,13 @@ statext_expressions_load(Oid stxoid, bool inh, int idx) deconstruct_expanded_array(eah); + if (eah->dnulls && eah->dnulls[idx]) + { + /* No data found for this expression, give up. */ + ReleaseSysCache(htup); + return NULL; + } + td = DatumGetHeapTupleHeader(eah->dvalues[idx]); /* Build a temporary HeapTuple control structure */ @@ -2618,7 +2657,7 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows, } else { - result->values[idx][i] = (Datum) datum; + result->values[idx][i] = datum; result->nulls[idx][i] = false; } diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c new file mode 100644 index 0000000000000..26a78913cc899 --- /dev/null +++ b/src/backend/statistics/extended_stats_funcs.c @@ -0,0 +1,1808 @@ +/*------------------------------------------------------------------------- + * + * extended_stats_funcs.c + * Functions for manipulating extended statistics. + * + * This file includes the set of facilities required to support the direct + * manipulations of extended statistics objects. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/statistics/extended_stats_funcs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/heapam.h" +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/pg_collation_d.h" +#include "catalog/pg_database.h" +#include "catalog/pg_operator.h" +#include "catalog/pg_statistic_ext.h" +#include "catalog/pg_statistic_ext_data.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "statistics/extended_stats_internal.h" +#include "statistics/stat_utils.h" +#include "utils/acl.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/jsonb.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" +#include "utils/typcache.h" + + +/* + * Index of the arguments for the SQL functions. + */ +enum extended_stats_argnum +{ + RELSCHEMA_ARG = 0, + RELNAME_ARG, + STATSCHEMA_ARG, + STATNAME_ARG, + INHERITED_ARG, + NDISTINCT_ARG, + DEPENDENCIES_ARG, + MOST_COMMON_VALS_ARG, + MOST_COMMON_FREQS_ARG, + MOST_COMMON_BASE_FREQS_ARG, + EXPRESSIONS_ARG, + NUM_EXTENDED_STATS_ARGS, +}; + +/* + * The argument names and type OIDs of the arguments for the SQL + * functions. + */ +static struct StatsArgInfo extarginfo[] = +{ + [RELSCHEMA_ARG] = {"schemaname", TEXTOID}, + [RELNAME_ARG] = {"relname", TEXTOID}, + [STATSCHEMA_ARG] = {"statistics_schemaname", TEXTOID}, + [STATNAME_ARG] = {"statistics_name", TEXTOID}, + [INHERITED_ARG] = {"inherited", BOOLOID}, + [NDISTINCT_ARG] = {"n_distinct", PG_NDISTINCTOID}, + [DEPENDENCIES_ARG] = {"dependencies", PG_DEPENDENCIESOID}, + [MOST_COMMON_VALS_ARG] = {"most_common_vals", TEXTARRAYOID}, + [MOST_COMMON_FREQS_ARG] = {"most_common_freqs", FLOAT8ARRAYOID}, + [MOST_COMMON_BASE_FREQS_ARG] = {"most_common_base_freqs", FLOAT8ARRAYOID}, + [EXPRESSIONS_ARG] = {"exprs", JSONBOID}, + [NUM_EXTENDED_STATS_ARGS] = {0}, +}; + +/* + * An index of the elements of a stxdexpr Datum, which repeat for each + * expression in the extended statistics object. + */ +enum extended_stats_exprs_element +{ + NULL_FRAC_ELEM = 0, + AVG_WIDTH_ELEM, + N_DISTINCT_ELEM, + MOST_COMMON_VALS_ELEM, + MOST_COMMON_FREQS_ELEM, + HISTOGRAM_BOUNDS_ELEM, + CORRELATION_ELEM, + MOST_COMMON_ELEMS_ELEM, + MOST_COMMON_ELEM_FREQS_ELEM, + ELEM_COUNT_HISTOGRAM_ELEM, + RANGE_LENGTH_HISTOGRAM_ELEM, + RANGE_EMPTY_FRAC_ELEM, + RANGE_BOUNDS_HISTOGRAM_ELEM, + NUM_ATTRIBUTE_STATS_ELEMS +}; + +/* + * The argument names of the repeating arguments for stxdexpr. + */ +static const char *extexprargname[NUM_ATTRIBUTE_STATS_ELEMS] = +{ + "null_frac", + "avg_width", + "n_distinct", + "most_common_vals", + "most_common_freqs", + "histogram_bounds", + "correlation", + "most_common_elems", + "most_common_elem_freqs", + "elem_count_histogram", + "range_length_histogram", + "range_empty_frac", + "range_bounds_histogram" +}; + +static bool extended_statistics_update(FunctionCallInfo fcinfo); + +static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid, + const char *stxname); +static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited); + +/* + * Track the extended statistics kinds expected for a pg_statistic_ext + * tuple. + */ +typedef struct +{ + bool ndistinct; + bool dependencies; + bool mcv; + bool expressions; +} StakindFlags; + +static void expand_stxkind(HeapTuple tup, StakindFlags *enabled); +static void upsert_pg_statistic_ext_data(const Datum *values, + const bool *nulls, + const bool *replaces); + +static bool check_mcvlist_array(const ArrayType *arr, int argindex, + int required_ndims, int mcv_length); +static Datum import_expressions(Relation pgsd, int numexprs, + Oid *atttypids, int32 *atttypmods, + Oid *atttypcolls, Jsonb *exprs_jsonb, + bool *exprs_is_perfect); +static Datum import_mcv(const ArrayType *mcv_arr, + const ArrayType *freqs_arr, + const ArrayType *base_freqs_arr, + Oid *atttypids, int32 *atttypmods, + Oid *atttypcolls, int numattrs, + bool *ok); + +static char *jbv_string_get_cstr(JsonbValue *jval); +static bool jbv_to_infunc_datum(JsonbValue *jval, PGFunction func, + AttrNumber exprnum, const char *argname, + Datum *datum); +static bool key_in_expr_argnames(JsonbValue *key); +static bool check_all_expr_argnames_valid(JsonbContainer *cont, AttrNumber exprnum); +static Datum array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, + int32 typmod, AttrNumber exprnum, + const char *element_name, bool *ok); +static Datum import_pg_statistic(Relation pgsd, JsonbContainer *cont, + AttrNumber exprnum, FmgrInfo *array_in_fn, + Oid typid, int32 typmod, Oid typcoll, + bool *pg_statistic_ok); + +/* + * Fetch a pg_statistic_ext row by name and namespace OID. + */ +static HeapTuple +get_pg_statistic_ext(Relation pg_stext, Oid nspoid, const char *stxname) +{ + ScanKeyData key[2]; + SysScanDesc scan; + HeapTuple tup; + Oid stxoid = InvalidOid; + + ScanKeyInit(&key[0], + Anum_pg_statistic_ext_stxname, + BTEqualStrategyNumber, + F_NAMEEQ, + CStringGetDatum(stxname)); + ScanKeyInit(&key[1], + Anum_pg_statistic_ext_stxnamespace, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(nspoid)); + + /* + * Try to find matching pg_statistic_ext row. + */ + scan = systable_beginscan(pg_stext, + StatisticExtNameIndexId, + true, + NULL, + 2, + key); + + /* Lookup is based on a unique index, so we get either 0 or 1 tuple. */ + tup = systable_getnext(scan); + + if (HeapTupleIsValid(tup)) + stxoid = ((Form_pg_statistic_ext) GETSTRUCT(tup))->oid; + + systable_endscan(scan); + + if (!OidIsValid(stxoid)) + return NULL; + + return SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxoid)); +} + +/* + * Decode the stxkind column so that we know which stats types to expect, + * returning a StakindFlags set depending on the stats kinds expected by + * a pg_statistic_ext tuple. + */ +static void +expand_stxkind(HeapTuple tup, StakindFlags *enabled) +{ + Datum datum; + ArrayType *arr; + char *kinds; + + datum = SysCacheGetAttrNotNull(STATEXTOID, + tup, + Anum_pg_statistic_ext_stxkind); + arr = DatumGetArrayTypeP(datum); + if (ARR_NDIM(arr) != 1 || ARR_HASNULL(arr) || ARR_ELEMTYPE(arr) != CHAROID) + elog(ERROR, "stxkind is not a one-dimension char array"); + + kinds = (char *) ARR_DATA_PTR(arr); + + for (int i = 0; i < ARR_DIMS(arr)[0]; i++) + { + switch (kinds[i]) + { + case STATS_EXT_NDISTINCT: + enabled->ndistinct = true; + break; + case STATS_EXT_DEPENDENCIES: + enabled->dependencies = true; + break; + case STATS_EXT_MCV: + enabled->mcv = true; + break; + case STATS_EXT_EXPRESSIONS: + enabled->expressions = true; + break; + default: + elog(ERROR, "incorrect stxkind %c found", kinds[i]); + break; + } + } +} + +/* + * Perform the actual storage of a pg_statistic_ext_data tuple. + */ +static void +upsert_pg_statistic_ext_data(const Datum *values, const bool *nulls, + const bool *replaces) +{ + Relation pg_stextdata; + HeapTuple stxdtup; + HeapTuple newtup; + + pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock); + + stxdtup = SearchSysCache2(STATEXTDATASTXOID, + values[Anum_pg_statistic_ext_data_stxoid - 1], + values[Anum_pg_statistic_ext_data_stxdinherit - 1]); + + if (HeapTupleIsValid(stxdtup)) + { + newtup = heap_modify_tuple(stxdtup, + RelationGetDescr(pg_stextdata), + values, + nulls, + replaces); + CatalogTupleUpdate(pg_stextdata, &newtup->t_self, newtup); + ReleaseSysCache(stxdtup); + } + else + { + newtup = heap_form_tuple(RelationGetDescr(pg_stextdata), values, nulls); + CatalogTupleInsert(pg_stextdata, newtup); + } + + heap_freetuple(newtup); + + CommandCounterIncrement(); + + table_close(pg_stextdata, RowExclusiveLock); +} + +/* + * Insert or update an extended statistics object. + * + * Major errors, such as the table not existing or permission errors, are + * reported as ERRORs. There are a couple of paths that generate a WARNING, + * like when the statistics object or its schema do not exist, a conversion + * failure on one statistic kind, or when other statistic kinds may still + * be updated. + */ +static bool +extended_statistics_update(FunctionCallInfo fcinfo) +{ + char *relnspname; + char *relname; + Oid nspoid; + char *nspname; + char *stxname; + bool inherited; + Relation pg_stext = NULL; + HeapTuple tup = NULL; + + StakindFlags enabled = {false, false, false, false}; + StakindFlags has = {false, false, false, false}; + + Form_pg_statistic_ext stxform; + + Datum values[Natts_pg_statistic_ext_data] = {0}; + bool nulls[Natts_pg_statistic_ext_data] = {0}; + bool replaces[Natts_pg_statistic_ext_data] = {0}; + bool success = true; + Datum exprdatum; + bool isnull; + List *exprs = NIL; + int numattnums = 0; + int numexprs = 0; + int numattrs = 0; + + /* arrays of type info, if we need them */ + Oid *atttypids = NULL; + int32 *atttypmods = NULL; + Oid *atttypcolls = NULL; + Oid relid; + Oid locked_table = InvalidOid; + + /* + * Fill out the StakindFlags "has" structure based on which parameters + * were provided to the function. + * + * The MCV stats composite value is an array of record type, but this is + * externally represented as three arrays that must be interleaved into + * the array of records (pg_stats_ext stores four arrays, + * most_common_val_nulls is built from the contents of most_common_vals). + * Therefore, none of the three array values is meaningful unless the + * other two are also present and in sync in terms of array length. + */ + has.mcv = (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) && + !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) && + !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG)); + has.ndistinct = !PG_ARGISNULL(NDISTINCT_ARG); + has.dependencies = !PG_ARGISNULL(DEPENDENCIES_ARG); + has.expressions = !PG_ARGISNULL(EXPRESSIONS_ARG); + + if (RecoveryInProgress()) + { + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is in progress"), + errhint("Statistics cannot be modified during recovery.")); + return false; + } + + /* relation arguments */ + stats_check_required_arg(fcinfo, extarginfo, RELSCHEMA_ARG); + relnspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG)); + stats_check_required_arg(fcinfo, extarginfo, RELNAME_ARG); + relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG)); + + /* extended statistics arguments */ + stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG); + nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG)); + stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG); + stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG)); + stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG); + inherited = PG_GETARG_BOOL(INHERITED_ARG); + + /* + * First open the relation where we expect to find the statistics. This + * is similar to relation and attribute statistics, so as ACL checks are + * done before any locks are taken, even before any attempts related to + * the extended stats object. + */ + relid = RangeVarGetRelidExtended(makeRangeVar(relnspname, relname, -1), + ShareUpdateExclusiveLock, 0, + RangeVarCallbackForStats, &locked_table); + + nspoid = get_namespace_oid(nspname, true); + if (nspoid == InvalidOid) + { + ereport(WARNING, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find schema \"%s\"", nspname)); + success = false; + goto cleanup; + } + + pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock); + tup = get_pg_statistic_ext(pg_stext, nspoid, stxname); + + if (!HeapTupleIsValid(tup)) + { + ereport(WARNING, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find extended statistics object \"%s.%s\"", + nspname, stxname)); + success = false; + goto cleanup; + } + + stxform = (Form_pg_statistic_ext) GETSTRUCT(tup); + + /* + * The relation tracked by the stats object has to match with the relation + * we have already locked. + */ + if (stxform->stxrelid != relid) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not restore extended statistics object \"%s.%s\": incorrect relation \"%s.%s\" specified", + nspname, stxname, + relnspname, relname)); + + success = false; + goto cleanup; + } + + /* Find out what extended statistics kinds we should expect. */ + expand_stxkind(tup, &enabled); + numattnums = stxform->stxkeys.dim1; + + /* decode expression (if any) */ + exprdatum = SysCacheGetAttr(STATEXTOID, + tup, + Anum_pg_statistic_ext_stxexprs, + &isnull); + if (!isnull) + { + char *s; + + s = TextDatumGetCString(exprdatum); + exprs = (List *) stringToNode(s); + pfree(s); + + /* + * Run the expressions through eval_const_expressions(). This is not + * just an optimization, but is necessary, because the planner will be + * comparing them to similarly-processed qual clauses, and may fail to + * detect valid matches without this. + * + * We must not use canonicalize_qual(), however, since these are not + * qual expressions. + */ + exprs = (List *) eval_const_expressions(NULL, (Node *) exprs); + + /* May as well fix opfuncids too */ + fix_opfuncids((Node *) exprs); + + /* Compute the number of expression, for input validation. */ + numexprs = list_length(exprs); + } + + numattrs = numattnums + numexprs; + + /* + * If the object cannot support ndistinct, we should not have data for it. + */ + if (has.ndistinct && !enabled.ndistinct) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot specify parameter \"%s\"", + extarginfo[NDISTINCT_ARG].argname), + errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.", + nspname, stxname)); + + has.ndistinct = false; + success = false; + } + + /* + * If the object cannot support dependencies, we should not have data for + * it. + */ + if (has.dependencies && !enabled.dependencies) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot specify parameter \"%s\"", + extarginfo[DEPENDENCIES_ARG].argname), + errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.", + nspname, stxname)); + has.dependencies = false; + success = false; + } + + /* + * If the object cannot hold an MCV value, but any of the MCV parameters + * are set, then issue a WARNING and ensure that we do not try to load MCV + * stats later. In pg_stats_ext, most_common_val_nulls, most_common_freqs + * and most_common_base_freqs are NULL if most_common_vals is NULL. + */ + if (!enabled.mcv) + { + if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) || + !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) || + !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG)) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot specify parameters \"%s\", \"%s\" or \"%s\"", + extarginfo[MOST_COMMON_VALS_ARG].argname, + extarginfo[MOST_COMMON_FREQS_ARG].argname, + extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname), + errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.", + nspname, stxname)); + + has.mcv = false; + success = false; + } + } + else if (!has.mcv) + { + /* + * If we do not have all of the MCV arrays set while the extended + * statistics object expects something, something is wrong. This + * issues a WARNING if a partial input has been provided. + */ + if (!PG_ARGISNULL(MOST_COMMON_VALS_ARG) || + !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) || + !PG_ARGISNULL(MOST_COMMON_BASE_FREQS_ARG)) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not use \"%s\", \"%s\" and \"%s\": missing one or more parameters", + extarginfo[MOST_COMMON_VALS_ARG].argname, + extarginfo[MOST_COMMON_FREQS_ARG].argname, + extarginfo[MOST_COMMON_BASE_FREQS_ARG].argname)); + success = false; + } + } + + /* + * If the object cannot support expressions, we should not have data for + * them. + */ + if (has.expressions && !enabled.expressions) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot specify parameter \"%s\"", + extarginfo[EXPRESSIONS_ARG].argname), + errhint("Extended statistics object \"%s.%s\" does not support statistics of this type.", + nspname, stxname)); + + has.expressions = false; + success = false; + } + + /* + * Either of these statistic types requires that we supply a semi-filled + * VacAttrStatsP array. + * + * It is not possible to use the existing lookup_var_attr_stats() and + * examine_attribute() because these functions will skip attributes where + * attstattarget is 0, and we may have statistics data to import for those + * attributes. + */ + if (has.mcv || has.expressions) + { + atttypids = palloc0_array(Oid, numattrs); + atttypmods = palloc0_array(int32, numattrs); + atttypcolls = palloc0_array(Oid, numattrs); + + /* + * The leading stxkeys are attribute numbers up through numattnums. + * These keys must be in ascending AttrNumber order, but we do not + * rely on that. + */ + for (int i = 0; i < numattnums; i++) + { + AttrNumber attnum = stxform->stxkeys.values[i]; + HeapTuple atup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(relid), + Int16GetDatum(attnum)); + + Form_pg_attribute attr; + + /* Attribute not found */ + if (!HeapTupleIsValid(atup)) + elog(ERROR, "stxkeys references nonexistent attnum %d", attnum); + + attr = (Form_pg_attribute) GETSTRUCT(atup); + + if (attr->attisdropped) + elog(ERROR, "stxkeys references dropped attnum %d", attnum); + + atttypids[i] = attr->atttypid; + atttypmods[i] = attr->atttypmod; + atttypcolls[i] = attr->attcollation; + ReleaseSysCache(atup); + } + + /* + * After all the positive number attnums in stxkeys come the negative + * numbers (if any) which represent expressions in the order that they + * appear in stxdexpr. Because the expressions are always + * monotonically decreasing from -1, there is no point in looking at + * the values in stxkeys, it's enough to know how many of them there + * are. + */ + for (int i = numattnums; i < numattrs; i++) + { + Node *expr = list_nth(exprs, i - numattnums); + + atttypids[i] = exprType(expr); + atttypmods[i] = exprTypmod(expr); + atttypcolls[i] = exprCollation(expr); + } + } + + /* + * Populate the pg_statistic_ext_data result tuple. + */ + + /* Primary Key: cannot be NULL or replaced. */ + values[Anum_pg_statistic_ext_data_stxoid - 1] = ObjectIdGetDatum(stxform->oid); + nulls[Anum_pg_statistic_ext_data_stxoid - 1] = false; + values[Anum_pg_statistic_ext_data_stxdinherit - 1] = BoolGetDatum(inherited); + nulls[Anum_pg_statistic_ext_data_stxdinherit - 1] = false; + + /* All unspecified parameters will be left unmodified */ + nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true; + nulls[Anum_pg_statistic_ext_data_stxddependencies - 1] = true; + nulls[Anum_pg_statistic_ext_data_stxdmcv - 1] = true; + nulls[Anum_pg_statistic_ext_data_stxdexpr - 1] = true; + + /* + * For each stats kind, deserialize the data at hand and perform a round + * of validation. The resulting tuple is filled with a set of updated + * values. + */ + + if (has.ndistinct) + { + Datum ndistinct_datum = PG_GETARG_DATUM(NDISTINCT_ARG); + bytea *data = DatumGetByteaPP(ndistinct_datum); + MVNDistinct *ndistinct = statext_ndistinct_deserialize(data); + + if (statext_ndistinct_validate(ndistinct, &stxform->stxkeys, + numexprs, WARNING)) + { + values[Anum_pg_statistic_ext_data_stxdndistinct - 1] = ndistinct_datum; + nulls[Anum_pg_statistic_ext_data_stxdndistinct - 1] = false; + replaces[Anum_pg_statistic_ext_data_stxdndistinct - 1] = true; + } + else + success = false; + + statext_ndistinct_free(ndistinct); + } + + if (has.dependencies) + { + Datum dependencies_datum = PG_GETARG_DATUM(DEPENDENCIES_ARG); + bytea *data = DatumGetByteaPP(dependencies_datum); + MVDependencies *dependencies = statext_dependencies_deserialize(data); + + if (statext_dependencies_validate(dependencies, &stxform->stxkeys, + numexprs, WARNING)) + { + values[Anum_pg_statistic_ext_data_stxddependencies - 1] = dependencies_datum; + nulls[Anum_pg_statistic_ext_data_stxddependencies - 1] = false; + replaces[Anum_pg_statistic_ext_data_stxddependencies - 1] = true; + } + else + success = false; + + statext_dependencies_free(dependencies); + } + + if (has.mcv) + { + Datum datum; + bool val_ok = false; + + datum = import_mcv(PG_GETARG_ARRAYTYPE_P(MOST_COMMON_VALS_ARG), + PG_GETARG_ARRAYTYPE_P(MOST_COMMON_FREQS_ARG), + PG_GETARG_ARRAYTYPE_P(MOST_COMMON_BASE_FREQS_ARG), + atttypids, atttypmods, atttypcolls, numattrs, + &val_ok); + + if (val_ok) + { + Assert(datum != (Datum) 0); + values[Anum_pg_statistic_ext_data_stxdmcv - 1] = datum; + nulls[Anum_pg_statistic_ext_data_stxdmcv - 1] = false; + replaces[Anum_pg_statistic_ext_data_stxdmcv - 1] = true; + } + else + success = false; + } + + if (has.expressions) + { + Datum datum; + Relation pgsd; + bool ok = false; + + pgsd = table_open(StatisticRelationId, RowExclusiveLock); + + /* + * Generate the expressions array. + * + * The atttypids, atttypmods, and atttypcolls arrays have all the + * regular attributes listed first, so we can pass those arrays with a + * start point after the last regular attribute. There are numexprs + * elements remaining. + */ + datum = import_expressions(pgsd, numexprs, + &atttypids[numattnums], + &atttypmods[numattnums], + &atttypcolls[numattnums], + PG_GETARG_JSONB_P(EXPRESSIONS_ARG), + &ok); + + table_close(pgsd, RowExclusiveLock); + + if (ok) + { + Assert(datum != (Datum) 0); + values[Anum_pg_statistic_ext_data_stxdexpr - 1] = datum; + replaces[Anum_pg_statistic_ext_data_stxdexpr - 1] = true; + nulls[Anum_pg_statistic_ext_data_stxdexpr - 1] = false; + } + else + success = false; + } + + upsert_pg_statistic_ext_data(values, nulls, replaces); + +cleanup: + if (HeapTupleIsValid(tup)) + heap_freetuple(tup); + if (pg_stext != NULL) + table_close(pg_stext, RowExclusiveLock); + if (atttypids != NULL) + pfree(atttypids); + if (atttypmods != NULL) + pfree(atttypmods); + if (atttypcolls != NULL) + pfree(atttypcolls); + return success; +} + +/* + * Consistency checks to ensure that other mcvlist arrays are in alignment + * with the mcv array. + */ +static bool +check_mcvlist_array(const ArrayType *arr, int argindex, int required_ndims, + int mcv_length) +{ + if (ARR_NDIM(arr) != required_ndims) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse array \"%s\": incorrect number of dimensions (%d required)", + extarginfo[argindex].argname, required_ndims)); + return false; + } + + if (array_contains_nulls(arr)) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse array \"%s\": NULL value found", + extarginfo[argindex].argname)); + return false; + } + + if (ARR_DIMS(arr)[0] != mcv_length) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse array \"%s\": incorrect number of elements (same as \"%s\" required)", + extarginfo[argindex].argname, + extarginfo[MOST_COMMON_VALS_ARG].argname)); + return false; + } + + return true; +} + +/* + * Create the stxdmcv datum from the equal-sized arrays of most common values, + * their null flags, and the frequency and base frequency associated with + * each value. + */ +static Datum +import_mcv(const ArrayType *mcv_arr, const ArrayType *freqs_arr, + const ArrayType *base_freqs_arr, Oid *atttypids, int32 *atttypmods, + Oid *atttypcolls, int numattrs, bool *ok) +{ + int nitems; + Datum *mcv_elems; + bool *mcv_nulls; + int check_nummcv; + Datum mcv = (Datum) 0; + + *ok = false; + + /* + * mcv_arr is an array of arrays. Each inner array must have the same + * number of elements "numattrs". + */ + if (ARR_NDIM(mcv_arr) != 2) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse array \"%s\": incorrect number of dimensions (%d required)", + extarginfo[MOST_COMMON_VALS_ARG].argname, 2)); + goto mcv_error; + } + + if (ARR_DIMS(mcv_arr)[1] != numattrs) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse array \"%s\": found %d attributes but expected %d", + extarginfo[MOST_COMMON_VALS_ARG].argname, + ARR_DIMS(mcv_arr)[1], numattrs)); + goto mcv_error; + } + + /* + * "most_common_freqs" and "most_common_base_freqs" arrays must be of the + * same length, one-dimension and cannot contain NULLs. We use mcv_arr as + * the reference array for determining their length. + */ + nitems = ARR_DIMS(mcv_arr)[0]; + if (!check_mcvlist_array(freqs_arr, MOST_COMMON_FREQS_ARG, 1, nitems) || + !check_mcvlist_array(base_freqs_arr, MOST_COMMON_BASE_FREQS_ARG, 1, nitems)) + { + /* inconsistent input arrays found */ + goto mcv_error; + } + + /* + * This part builds the contents for "most_common_val_nulls", based on the + * values from "most_common_vals". + */ + deconstruct_array_builtin(mcv_arr, TEXTOID, &mcv_elems, + &mcv_nulls, &check_nummcv); + + mcv = statext_mcv_import(WARNING, numattrs, + atttypids, atttypmods, atttypcolls, + nitems, mcv_elems, mcv_nulls, + (float8 *) ARR_DATA_PTR(freqs_arr), + (float8 *) ARR_DATA_PTR(base_freqs_arr)); + + *ok = (mcv != (Datum) 0); + +mcv_error: + return mcv; +} + +/* + * Check if key is found in the list of expression argnames. + */ +static bool +key_in_expr_argnames(JsonbValue *key) +{ + Assert(key->type == jbvString); + for (int i = 0; i < NUM_ATTRIBUTE_STATS_ELEMS; i++) + { + if (strncmp(extexprargname[i], key->val.string.val, key->val.string.len) == 0) + return true; + } + return false; +} + +/* + * Verify that all of the keys in the object are valid argnames. + */ +static bool +check_all_expr_argnames_valid(JsonbContainer *cont, AttrNumber exprnum) +{ + bool all_keys_valid = true; + + JsonbIterator *jbit; + JsonbIteratorToken jitok; + JsonbValue jkey; + + Assert(JsonContainerIsObject(cont)); + + jbit = JsonbIteratorInit(cont); + + /* We always start off with a BEGIN OBJECT */ + jitok = JsonbIteratorNext(&jbit, &jkey, false); + Assert(jitok == WJB_BEGIN_OBJECT); + + while (true) + { + JsonbValue jval; + + jitok = JsonbIteratorNext(&jbit, &jkey, false); + + /* + * We have run of keys. This is the only condition where it is + * memory-safe to break out of the loop. + */ + if (jitok == WJB_END_OBJECT) + break; + + /* We can only find keys inside an object */ + Assert(jitok == WJB_KEY); + Assert(jkey.type == jbvString); + + /* A value must follow the key */ + jitok = JsonbIteratorNext(&jbit, &jval, false); + Assert(jitok == WJB_VALUE); + + /* + * If we have already found an invalid key, there is no point in + * looking for more, because additional WARNINGs are just clutter. We + * must continue iterating over the json to ensure that we clean up + * all allocated memory. + */ + if (!all_keys_valid) + continue; + + if (!key_in_expr_argnames(&jkey)) + { + char *bad_element_name = jbv_string_get_cstr(&jkey); + + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not import element in expression %d: invalid key name", + exprnum)); + + pfree(bad_element_name); + all_keys_valid = false; + } + } + return all_keys_valid; +} + +/* + * Simple conversion of jbvString to cstring + */ +static char * +jbv_string_get_cstr(JsonbValue *jval) +{ + char *s; + + Assert(jval->type == jbvString); + + s = palloc0(jval->val.string.len + 1); + memcpy(s, jval->val.string.val, jval->val.string.len); + + return s; +} + +/* + * Apply a jbvString value to a safe scalar input function. + */ +static bool +jbv_to_infunc_datum(JsonbValue *jval, PGFunction func, AttrNumber exprnum, + const char *argname, Datum *datum) +{ + ErrorSaveContext escontext = { + .type = T_ErrorSaveContext, + .details_wanted = true + }; + + char *s = jbv_string_get_cstr(jval); + bool ok; + + ok = DirectInputFunctionCallSafe(func, s, InvalidOid, -1, + (Node *) &escontext, datum); + + /* + * If we got a type import error, use the report generated and add an + * error hint before throwing a warning. + */ + if (!ok) + { + StringInfoData hint_str; + + initStringInfo(&hint_str); + appendStringInfo(&hint_str, + "Element \"%s\" in expression %d could not be parsed.", + argname, exprnum); + + escontext.error_data->elevel = WARNING; + escontext.error_data->hint = hint_str.data; + + ThrowErrorData(escontext.error_data); + pfree(hint_str.data); + } + + pfree(s); + return ok; +} + +/* + * Build an array datum with element type elemtypid from a text datum, used as + * value of an attribute in a pg_statistic tuple. + * + * If an error is encountered, capture it, and reduce the elevel to WARNING. + * + * This is an adaptation of statatt_build_stavalues(). + */ +static Datum +array_in_safe(FmgrInfo *array_in, const char *s, Oid typid, int32 typmod, + AttrNumber exprnum, const char *element_name, bool *ok) +{ + LOCAL_FCINFO(fcinfo, 3); + Datum result; + + ErrorSaveContext escontext = { + .type = T_ErrorSaveContext, + .details_wanted = true + }; + + *ok = false; + InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid, + (Node *) &escontext, NULL); + + fcinfo->args[0].value = CStringGetDatum(s); + fcinfo->args[0].isnull = false; + fcinfo->args[1].value = ObjectIdGetDatum(typid); + fcinfo->args[1].isnull = false; + fcinfo->args[2].value = Int32GetDatum(typmod); + fcinfo->args[2].isnull = false; + + result = FunctionCallInvoke(fcinfo); + + /* + * If the array_in function returned an error, we will want to report that + * ERROR as a WARNING, and add some location context to the error message. + * Overwriting the existing hint (if any) is not ideal, and an error + * context would only work for level >= ERROR. + */ + if (escontext.error_occurred) + { + StringInfoData hint_str; + + initStringInfo(&hint_str); + appendStringInfo(&hint_str, + "Element \"%s\" in expression %d could not be parsed.", + element_name, exprnum); + escontext.error_data->elevel = WARNING; + escontext.error_data->hint = hint_str.data; + ThrowErrorData(escontext.error_data); + pfree(hint_str.data); + return (Datum) 0; + } + + if (array_contains_nulls(DatumGetArrayTypeP(result))) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not import element \"%s\" in expression %d: null value found", + element_name, exprnum)); + return (Datum) 0; + } + + *ok = true; + return result; +} + +/* + * Create a pg_statistic tuple from an expression JSONB container. + * + * The pg_statistic tuple is pre-populated with acceptable defaults, therefore + * even if there is an issue with all of the keys in the container, we can + * still return a legit tuple datum. + * + * Set pg_statistic_ok to true if all of the values found in the container + * were imported without issue. pg_statistic_ok is switched to "true" once + * the full pg_statistic tuple has been built and validated. + */ +static Datum +import_pg_statistic(Relation pgsd, JsonbContainer *cont, + AttrNumber exprnum, FmgrInfo *array_in_fn, + Oid typid, int32 typmod, Oid typcoll, + bool *pg_statistic_ok) +{ + const char *argname = extarginfo[EXPRESSIONS_ARG].argname; + TypeCacheEntry *typcache; + Datum values[Natts_pg_statistic]; + bool nulls[Natts_pg_statistic]; + bool replaces[Natts_pg_statistic]; + HeapTuple pgstup = NULL; + Datum pgstdat = (Datum) 0; + Oid elemtypid = InvalidOid; + Oid elemeqopr = InvalidOid; + bool found[NUM_ATTRIBUTE_STATS_ELEMS] = {0}; + JsonbValue val[NUM_ATTRIBUTE_STATS_ELEMS] = {0}; + + /* Assume the worst by default. */ + *pg_statistic_ok = false; + + if (!JsonContainerIsObject(cont)) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element in expression %d", + argname, exprnum)); + goto pg_statistic_error; + } + + /* + * Loop through all keys that we need to look up. If any value found is + * neither a string nor a NULL, there is not much we can do, so just give + * on the entire tuple for this expression. + */ + for (int i = 0; i < NUM_ATTRIBUTE_STATS_ELEMS; i++) + { + const char *s = extexprargname[i]; + int len = strlen(s); + + if (getKeyJsonValueFromContainer(cont, s, len, &val[i]) == NULL) + continue; + + switch (val[i].type) + { + case jbvString: + found[i] = true; + break; + + case jbvNull: + break; + + default: + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element in expression %d", argname, exprnum), + errhint("Value of element \"%s\" must be type a null or a string.", s)); + goto pg_statistic_error; + } + } + + /* Look for invalid keys */ + if (!check_all_expr_argnames_valid(cont, exprnum)) + goto pg_statistic_error; + + /* + * There are two arg pairs, MCV+MCF and MCEV+MCEF. Both values must + * either be found or not be found. Any disagreement is a warning. Once + * we have ruled out disagreeing pairs, we can use either found flag as a + * proxy for the other. + */ + if (found[MOST_COMMON_VALS_ELEM] != found[MOST_COMMON_FREQS_ELEM]) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element in expression %d", + argname, exprnum), + errhint("\"%s\" and \"%s\" must be both either strings or nulls.", + extexprargname[MOST_COMMON_VALS_ELEM], + extexprargname[MOST_COMMON_FREQS_ELEM])); + goto pg_statistic_error; + } + if (found[MOST_COMMON_ELEMS_ELEM] != found[MOST_COMMON_ELEM_FREQS_ELEM]) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element in expression %d", + argname, exprnum), + errhint("\"%s\" and \"%s\" must be both either strings or nulls.", + extexprargname[MOST_COMMON_ELEMS_ELEM], + extexprargname[MOST_COMMON_ELEM_FREQS_ELEM])); + goto pg_statistic_error; + } + + /* + * Range types may expect three values to be set. All three of them must + * either be found or not be found. Any disagreement is a warning. + */ + if (found[RANGE_LENGTH_HISTOGRAM_ELEM] != found[RANGE_EMPTY_FRAC_ELEM] || + found[RANGE_LENGTH_HISTOGRAM_ELEM] != found[RANGE_BOUNDS_HISTOGRAM_ELEM]) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element in expression %d", + argname, exprnum), + errhint("\"%s\", \"%s\", and \"%s\" must be all either strings or all nulls.", + extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM], + extexprargname[RANGE_EMPTY_FRAC_ELEM], + extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM])); + goto pg_statistic_error; + } + + /* This finds the right operators even if atttypid is a domain */ + typcache = lookup_type_cache(typid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR); + + statatt_init_empty_tuple(InvalidOid, InvalidAttrNumber, false, + values, nulls, replaces); + + /* + * Special case: collation for tsvector is DEFAULT_COLLATION_OID. See + * compute_tsvector_stats(). + */ + if (typid == TSVECTOROID) + typcoll = DEFAULT_COLLATION_OID; + + /* + * We only need to fetch element type and eq operator if we have a stat of + * type MCELEM or DECHIST, otherwise the values are unnecessary and not + * meaningful. + */ + if (found[MOST_COMMON_ELEMS_ELEM] || found[ELEM_COUNT_HISTOGRAM_ELEM]) + { + if (!statatt_get_elem_type(typid, typcache->typtype, + &elemtypid, &elemeqopr)) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element type in expression %d", + argname, exprnum)); + goto pg_statistic_error; + } + } + + /* + * These three fields can only be set if dealing with a range or + * multi-range type. + */ + if (found[RANGE_LENGTH_HISTOGRAM_ELEM] || + found[RANGE_EMPTY_FRAC_ELEM] || + found[RANGE_BOUNDS_HISTOGRAM_ELEM]) + { + if (typcache->typtype != TYPTYPE_RANGE && + typcache->typtype != TYPTYPE_MULTIRANGE) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid data in expression %d", + argname, exprnum), + errhint("\"%s\", \"%s\", and \"%s\" can only be set for a range type.", + extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM], + extexprargname[RANGE_EMPTY_FRAC_ELEM], + extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM])); + goto pg_statistic_error; + } + } + + /* null_frac */ + if (found[NULL_FRAC_ELEM]) + { + Datum datum; + + if (jbv_to_infunc_datum(&val[NULL_FRAC_ELEM], float4in, exprnum, + extexprargname[NULL_FRAC_ELEM], &datum)) + values[Anum_pg_statistic_stanullfrac - 1] = datum; + else + goto pg_statistic_error; + } + + /* avg_width */ + if (found[AVG_WIDTH_ELEM]) + { + Datum datum; + + if (jbv_to_infunc_datum(&val[AVG_WIDTH_ELEM], int4in, exprnum, + extexprargname[AVG_WIDTH_ELEM], &datum)) + values[Anum_pg_statistic_stawidth - 1] = datum; + else + goto pg_statistic_error; + } + + /* n_distinct */ + if (found[N_DISTINCT_ELEM]) + { + Datum datum; + + if (jbv_to_infunc_datum(&val[N_DISTINCT_ELEM], float4in, exprnum, + extexprargname[N_DISTINCT_ELEM], &datum)) + values[Anum_pg_statistic_stadistinct - 1] = datum; + else + goto pg_statistic_error; + } + + /* + * The STAKIND statistics are the same as the ones found in attribute + * stats. However, these are all derived from json strings, whereas the + * ones derived for attribute stats are a mix of datatypes. This limits + * the opportunities for code sharing between the two. + * + * Some statistic kinds have both a stanumbers and a stavalues components. + * In those cases, both values must either be NOT NULL or both NULL, and + * if they aren't then we need to reject that stakind completely. + * Currently we go a step further and reject the expression array + * completely. + */ + + if (found[MOST_COMMON_VALS_ELEM]) + { + Datum stavalues; + Datum stanumbers; + bool val_ok = false; + bool num_ok = false; + char *s; + + s = jbv_string_get_cstr(&val[MOST_COMMON_VALS_ELEM]); + stavalues = array_in_safe(array_in_fn, s, typid, typmod, exprnum, + extexprargname[MOST_COMMON_VALS_ELEM], + &val_ok); + + pfree(s); + + s = jbv_string_get_cstr(&val[MOST_COMMON_FREQS_ELEM]); + stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum, + extexprargname[MOST_COMMON_FREQS_ELEM], + &num_ok); + pfree(s); + + /* Only set the slot if both datums have been built */ + if (val_ok && num_ok) + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_MCV, + typcache->eq_opr, typcoll, + stanumbers, false, stavalues, false); + else + goto pg_statistic_error; + } + + /* STATISTIC_KIND_HISTOGRAM */ + if (found[HISTOGRAM_BOUNDS_ELEM]) + { + Datum stavalues; + bool val_ok = false; + char *s = jbv_string_get_cstr(&val[HISTOGRAM_BOUNDS_ELEM]); + + stavalues = array_in_safe(array_in_fn, s, typid, typmod, exprnum, + extexprargname[HISTOGRAM_BOUNDS_ELEM], + &val_ok); + pfree(s); + + if (val_ok) + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_HISTOGRAM, + typcache->lt_opr, typcoll, + 0, true, stavalues, false); + else + goto pg_statistic_error; + } + + /* STATISTIC_KIND_CORRELATION */ + if (found[CORRELATION_ELEM]) + { + Datum corr[] = {(Datum) 0}; + + if (jbv_to_infunc_datum(&val[CORRELATION_ELEM], float4in, exprnum, + extexprargname[CORRELATION_ELEM], &corr[0])) + { + ArrayType *arry = construct_array_builtin(corr, 1, FLOAT4OID); + Datum stanumbers = PointerGetDatum(arry); + + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_CORRELATION, + typcache->lt_opr, typcoll, + stanumbers, false, 0, true); + } + else + goto pg_statistic_error; + } + + /* STATISTIC_KIND_MCELEM */ + if (found[MOST_COMMON_ELEMS_ELEM]) + { + Datum stavalues; + Datum stanumbers; + bool val_ok = false; + bool num_ok = false; + char *s; + + s = jbv_string_get_cstr(&val[MOST_COMMON_ELEMS_ELEM]); + stavalues = array_in_safe(array_in_fn, s, elemtypid, typmod, exprnum, + extexprargname[MOST_COMMON_ELEMS_ELEM], + &val_ok); + pfree(s); + + + s = jbv_string_get_cstr(&val[MOST_COMMON_ELEM_FREQS_ELEM]); + stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum, + extexprargname[MOST_COMMON_ELEM_FREQS_ELEM], + &num_ok); + pfree(s); + + /* Only set the slot if both datums have been built */ + if (val_ok && num_ok) + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_MCELEM, + elemeqopr, typcoll, + stanumbers, false, stavalues, false); + else + goto pg_statistic_error; + } + + /* STATISTIC_KIND_DECHIST */ + if (found[ELEM_COUNT_HISTOGRAM_ELEM]) + { + Datum stanumbers; + bool num_ok = false; + char *s; + + s = jbv_string_get_cstr(&val[ELEM_COUNT_HISTOGRAM_ELEM]); + stanumbers = array_in_safe(array_in_fn, s, FLOAT4OID, -1, exprnum, + extexprargname[ELEM_COUNT_HISTOGRAM_ELEM], + &num_ok); + pfree(s); + + if (num_ok) + statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_DECHIST, + elemeqopr, typcoll, stanumbers, false, 0, true); + else + goto pg_statistic_error; + } + + /* + * STATISTIC_KIND_BOUNDS_HISTOGRAM + * + * This stakind appears before STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM even + * though it is numerically greater, and all other stakinds appear in + * numerical order. + */ + if (found[RANGE_BOUNDS_HISTOGRAM_ELEM]) + { + Datum stavalues; + bool val_ok = false; + char *s; + Oid rtypid = typid; + + /* + * If it's a multirange, step down to the range type, as is done by + * multirange_typanalyze(). + */ + if (type_is_multirange(typid)) + rtypid = get_multirange_range(typid); + + s = jbv_string_get_cstr(&val[RANGE_BOUNDS_HISTOGRAM_ELEM]); + + stavalues = array_in_safe(array_in_fn, s, rtypid, typmod, exprnum, + extexprargname[RANGE_BOUNDS_HISTOGRAM_ELEM], + &val_ok); + + if (val_ok) + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_BOUNDS_HISTOGRAM, + InvalidOid, InvalidOid, + 0, true, stavalues, false); + else + goto pg_statistic_error; + } + + /* STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM */ + if (found[RANGE_LENGTH_HISTOGRAM_ELEM]) + { + Datum empty_frac[] = {(Datum) 0}; + Datum stavalues; + Datum stanumbers; + bool val_ok = false; + char *s; + + if (jbv_to_infunc_datum(&val[RANGE_EMPTY_FRAC_ELEM], float4in, exprnum, + extexprargname[RANGE_EMPTY_FRAC_ELEM], &empty_frac[0])) + { + ArrayType *arry = construct_array_builtin(empty_frac, 1, FLOAT4OID); + + stanumbers = PointerGetDatum(arry); + } + else + goto pg_statistic_error; + + s = jbv_string_get_cstr(&val[RANGE_LENGTH_HISTOGRAM_ELEM]); + stavalues = array_in_safe(array_in_fn, s, FLOAT8OID, -1, exprnum, + extexprargname[RANGE_LENGTH_HISTOGRAM_ELEM], + &val_ok); + + if (val_ok) + statatt_set_slot(values, nulls, replaces, + STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM, + Float8LessOperator, InvalidOid, + stanumbers, false, stavalues, false); + else + goto pg_statistic_error; + } + + pgstup = heap_form_tuple(RelationGetDescr(pgsd), values, nulls); + pgstdat = heap_copy_tuple_as_datum(pgstup, RelationGetDescr(pgsd)); + + heap_freetuple(pgstup); + + *pg_statistic_ok = true; + + return pgstdat; + +pg_statistic_error: + return (Datum) 0; +} + +/* + * Create the stxdexpr datum, which is an array of pg_statistic rows with all + * of the object identification fields left at defaults, using the json array + * of objects/nulls referenced against the datatypes for the expressions. + * + * The exprs_is_perfect will be set to true if all pg_statistic rows were + * imported cleanly. If any of them experienced a problem (and thus were + * set as if they were null), then the expression is kept but exprs_is_perfect + * will be marked as false. + * + * This datum is needed to fill out a complete pg_statistic_ext_data tuple. + */ +static Datum +import_expressions(Relation pgsd, int numexprs, + Oid *atttypids, int32 *atttypmods, + Oid *atttypcolls, Jsonb *exprs_jsonb, + bool *exprs_is_perfect) +{ + const char *argname = extarginfo[EXPRESSIONS_ARG].argname; + Oid pgstypoid = get_rel_type_id(StatisticRelationId); + ArrayBuildState *astate = NULL; + Datum result = (Datum) 0; + int num_import_ok = 0; + JsonbContainer *root; + int num_root_elements; + + FmgrInfo array_in_fn; + + *exprs_is_perfect = false; + + /* Json schema must be [{expr},...] */ + if (!JB_ROOT_IS_ARRAY(exprs_jsonb)) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": root-level array required", argname)); + goto exprs_error; + } + + root = &exprs_jsonb->root; + + /* + * The number of elements in the array must match the number of + * expressions in the stats object definition. + */ + num_root_elements = JsonContainerSize(root); + if (numexprs != num_root_elements) + { + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": incorrect number of elements (%d required)", + argname, num_root_elements)); + goto exprs_error; + } + + fmgr_info(F_ARRAY_IN, &array_in_fn); + + /* + * Iterate over each expected expression object in the array. Some of + * them could be null. If the element is a completely wrong data type, + * give a WARNING and then treat the element like a NULL element in the + * result array. + * + * Each expression *MUST* have a value appended in the result pg_statistic + * array. + */ + for (int i = 0; i < numexprs; i++) + { + Datum pgstdat = (Datum) 0; + bool isnull = false; + AttrNumber exprattnum = -1 - i; + + JsonbValue *elem = getIthJsonbValueFromContainer(root, i); + + switch (elem->type) + { + case jbvBinary: + { + bool sta_ok = false; + + /* a real stats object */ + pgstdat = import_pg_statistic(pgsd, elem->val.binary.data, + exprattnum, &array_in_fn, + atttypids[i], atttypmods[i], + atttypcolls[i], &sta_ok); + + /* + * If some incorrect data has been found, assign NULL for + * this expression as a mean to give up. + */ + if (sta_ok) + num_import_ok++; + else + { + isnull = true; + pgstdat = (Datum) 0; + } + } + break; + + case jbvNull: + /* NULL placeholder for invalid data, still fine */ + isnull = true; + num_import_ok++; + break; + + default: + /* cannot possibly be valid */ + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse \"%s\": invalid element in expression %d", + argname, exprattnum)); + goto exprs_error; + } + + astate = accumArrayResult(astate, pgstdat, isnull, pgstypoid, + CurrentMemoryContext); + } + + /* + * The expressions datum is perfect *if and only if* all of the + * pg_statistic elements were also ok, for a number of elements equal to + * the number of expressions. Anything else means a failure in restoring + * the data of this statistics object. + */ + *exprs_is_perfect = (num_import_ok == numexprs); + + if (astate != NULL) + result = makeArrayResult(astate, CurrentMemoryContext); + + return result; + +exprs_error: + if (astate != NULL) + pfree(astate); + return (Datum) 0; +}; + +/* + * Remove an existing pg_statistic_ext_data row for a given pg_statistic_ext + * row and "inherited" pair. + */ +static bool +delete_pg_statistic_ext_data(Oid stxoid, bool inherited) +{ + Relation sed = table_open(StatisticExtDataRelationId, RowExclusiveLock); + HeapTuple oldtup; + bool result = false; + + /* Is there already a pg_statistic_ext_data tuple for this attribute? */ + oldtup = SearchSysCache2(STATEXTDATASTXOID, + ObjectIdGetDatum(stxoid), + BoolGetDatum(inherited)); + + if (HeapTupleIsValid(oldtup)) + { + CatalogTupleDelete(sed, &oldtup->t_self); + ReleaseSysCache(oldtup); + result = true; + } + + table_close(sed, RowExclusiveLock); + + CommandCounterIncrement(); + + return result; +} + +/* + * Restore (insert or replace) statistics for the given statistics object. + * + * This function accepts variadic arguments in key-value pairs, which are + * given to stats_fill_fcinfo_from_arg_pairs to be mapped into positional + * arguments. + */ +Datum +pg_restore_extended_stats(PG_FUNCTION_ARGS) +{ + LOCAL_FCINFO(positional_fcinfo, NUM_EXTENDED_STATS_ARGS); + bool result = true; + + InitFunctionCallInfoData(*positional_fcinfo, NULL, NUM_EXTENDED_STATS_ARGS, + InvalidOid, NULL, NULL); + + if (!stats_fill_fcinfo_from_arg_pairs(fcinfo, positional_fcinfo, extarginfo)) + result = false; + + if (!extended_statistics_update(positional_fcinfo)) + result = false; + + PG_RETURN_BOOL(result); +} + +/* + * Delete statistics for the given statistics object. + */ +Datum +pg_clear_extended_stats(PG_FUNCTION_ARGS) +{ + char *relnspname; + char *relname; + char *nspname; + Oid nspoid; + Oid relid; + char *stxname; + bool inherited; + Relation pg_stext; + HeapTuple tup; + Form_pg_statistic_ext stxform; + Oid locked_table = InvalidOid; + + /* relation arguments */ + stats_check_required_arg(fcinfo, extarginfo, RELSCHEMA_ARG); + relnspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG)); + stats_check_required_arg(fcinfo, extarginfo, RELNAME_ARG); + relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG)); + + /* extended statistics arguments */ + stats_check_required_arg(fcinfo, extarginfo, STATSCHEMA_ARG); + nspname = TextDatumGetCString(PG_GETARG_DATUM(STATSCHEMA_ARG)); + stats_check_required_arg(fcinfo, extarginfo, STATNAME_ARG); + stxname = TextDatumGetCString(PG_GETARG_DATUM(STATNAME_ARG)); + stats_check_required_arg(fcinfo, extarginfo, INHERITED_ARG); + inherited = PG_GETARG_BOOL(INHERITED_ARG); + + if (RecoveryInProgress()) + { + ereport(WARNING, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("recovery is in progress"), + errhint("Statistics cannot be modified during recovery.")); + PG_RETURN_VOID(); + } + + /* + * First open the relation where we expect to find the statistics. This + * is similar to relation and attribute statistics, so as ACL checks are + * done before any locks are taken, even before any attempts related to + * the extended stats object. + */ + relid = RangeVarGetRelidExtended(makeRangeVar(relnspname, relname, -1), + ShareUpdateExclusiveLock, 0, + RangeVarCallbackForStats, &locked_table); + + /* Now check if the namespace of the stats object exists. */ + nspoid = get_namespace_oid(nspname, true); + if (nspoid == InvalidOid) + { + ereport(WARNING, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find schema \"%s\"", nspname)); + PG_RETURN_VOID(); + } + + pg_stext = table_open(StatisticExtRelationId, RowExclusiveLock); + tup = get_pg_statistic_ext(pg_stext, nspoid, stxname); + + if (!HeapTupleIsValid(tup)) + { + table_close(pg_stext, RowExclusiveLock); + ereport(WARNING, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("could not find extended statistics object \"%s.%s\"", + nspname, stxname)); + PG_RETURN_VOID(); + } + + stxform = (Form_pg_statistic_ext) GETSTRUCT(tup); + + /* + * This should be consistent, based on the lock taken on the table when we + * started. + */ + if (stxform->stxrelid != relid) + { + table_close(pg_stext, RowExclusiveLock); + ereport(WARNING, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not clear extended statistics object \"%s.%s\": incorrect relation \"%s.%s\" specified", + get_namespace_name(nspoid), stxname, + relnspname, relname)); + PG_RETURN_VOID(); + } + + delete_pg_statistic_ext_data(stxform->oid, inherited); + heap_freetuple(tup); + + table_close(pg_stext, RowExclusiveLock); + + PG_RETURN_VOID(); +} diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c index d98cda698d941..0b7da605a4c68 100644 --- a/src/backend/statistics/mcv.c +++ b/src/backend/statistics/mcv.c @@ -4,7 +4,7 @@ * POSTGRES multivariate MCV lists * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -14,8 +14,6 @@ */ #include "postgres.h" -#include - #include "access/htup_details.h" #include "catalog/pg_statistic_ext.h" #include "catalog/pg_statistic_ext_data.h" @@ -270,7 +268,7 @@ statext_mcv_build(StatsBuildData *data, double totalrows, int stattarget) + sizeof(SortSupportData)); /* compute frequencies for values in each column */ - nfreqs = (int *) palloc0(sizeof(int) * numattrs); + nfreqs = palloc0_array(int, numattrs); freqs = build_column_frequencies(groups, ngroups, mss, nfreqs); /* @@ -294,8 +292,8 @@ statext_mcv_build(StatsBuildData *data, double totalrows, int stattarget) /* just point to the proper place in the list */ MCVItem *item = &mcvlist->items[i]; - item->values = (Datum *) palloc(sizeof(Datum) * numattrs); - item->isnull = (bool *) palloc(sizeof(bool) * numattrs); + item->values = palloc_array(Datum, numattrs); + item->isnull = palloc_array(bool, numattrs); /* copy values for the group */ memcpy(item->values, groups[i].values, sizeof(Datum) * numattrs); @@ -402,8 +400,8 @@ count_distinct_groups(int numrows, SortItem *items, MultiSortSupport mss) static int compare_sort_item_count(const void *a, const void *b, void *arg) { - SortItem *ia = (SortItem *) a; - SortItem *ib = (SortItem *) b; + const SortItem *ia = a; + const SortItem *ib = b; if (ia->count == ib->count) return 0; @@ -465,8 +463,8 @@ static int sort_item_compare(const void *a, const void *b, void *arg) { SortSupport ssup = (SortSupport) arg; - SortItem *ia = (SortItem *) a; - SortItem *ib = (SortItem *) b; + const SortItem *ia = a; + const SortItem *ib = b; return ApplySortComparator(ia->values[0], ia->isnull[0], ib->values[0], ib->isnull[0], @@ -635,8 +633,8 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) char *endptr PG_USED_FOR_ASSERTS_ONLY; /* values per dimension (and number of non-NULL values) */ - Datum **values = (Datum **) palloc0(sizeof(Datum *) * ndims); - int *counts = (int *) palloc0(sizeof(int) * ndims); + Datum **values = palloc0_array(Datum *, ndims); + int *counts = palloc0_array(int, ndims); /* * We'll include some rudimentary information about the attribute types @@ -646,10 +644,10 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) * the statistics gets dropped automatically. We need to store the info * about the arrays of deduplicated values anyway. */ - info = (DimensionInfo *) palloc0(sizeof(DimensionInfo) * ndims); + info = palloc0_array(DimensionInfo, ndims); /* sort support data for all attributes included in the MCV list */ - ssup = (SortSupport) palloc0(sizeof(SortSupportData) * ndims); + ssup = palloc0_array(SortSupportData, ndims); /* collect and deduplicate values for each dimension (attribute) */ for (dim = 0; dim < ndims; dim++) @@ -668,7 +666,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) info[dim].typbyval = stats[dim]->attrtype->typbyval; /* allocate space for values in the attribute and collect them */ - values[dim] = (Datum *) palloc0(sizeof(Datum) * mcvlist->nitems); + values[dim] = palloc0_array(Datum, mcvlist->nitems); for (i = 0; i < mcvlist->nitems; i++) { @@ -767,7 +765,7 @@ statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats) values[dim][i] = PointerGetDatum(PG_DETOAST_DATUM(values[dim][i])); /* serialized length (uint32 length + data) */ - len = VARSIZE_ANY_EXHDR(values[dim][i]); + len = VARSIZE_ANY_EXHDR(DatumGetPointer(values[dim][i])); info[dim].nbytes += sizeof(uint32); /* length */ info[dim].nbytes += len; /* value (no header) */ @@ -1037,7 +1035,7 @@ statext_mcv_deserialize(bytea *data) /* pointer to the data part (skip the varlena header) */ raw = (char *) data; ptr = VARDATA_ANY(raw); - endptr = (char *) raw + VARSIZE_ANY(data); + endptr = raw + VARSIZE_ANY(data); /* get the header and perform further sanity checks */ memcpy(&mcvlist->magic, ptr, sizeof(uint32)); @@ -1134,11 +1132,11 @@ statext_mcv_deserialize(bytea *data) * original values (it might go away). */ datalen = 0; /* space for by-ref data */ - map = (Datum **) palloc(ndims * sizeof(Datum *)); + map = palloc_array(Datum *, ndims); for (dim = 0; dim < ndims; dim++) { - map[dim] = (Datum *) palloc(sizeof(Datum) * info[dim].nvalues); + map[dim] = palloc_array(Datum, info[dim].nvalues); /* space needed for a copy of data for by-ref types */ datalen += info[dim].nbytes_aligned; @@ -1609,7 +1607,7 @@ mcv_get_match_bitmap(PlannerInfo *root, List *clauses, Assert(mcvlist->nitems > 0); Assert(mcvlist->nitems <= STATS_MCVLIST_MAX_ITEMS); - matches = palloc(sizeof(bool) * mcvlist->nitems); + matches = palloc_array(bool, mcvlist->nitems); memset(matches, !is_or, sizeof(bool) * mcvlist->nitems); /* @@ -2134,7 +2132,7 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat, /* build the OR-matches bitmap, if not built already */ if (*or_matches == NULL) - *or_matches = palloc0(sizeof(bool) * mcv->nitems); + *or_matches = palloc0_array(bool, mcv->nitems); /* build the match bitmap for the new clause */ new_matches = mcv_get_match_bitmap(root, list_make1(clause), stat->keys, @@ -2173,3 +2171,164 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat, return s; } + +/* + * Free allocations of a MCVList. + */ +void +statext_mcv_free(MCVList *mcvlist) +{ + for (int i = 0; i < mcvlist->nitems; i++) + { + MCVItem *item = &mcvlist->items[i]; + + pfree(item->values); + pfree(item->isnull); + } + pfree(mcvlist); +} + +/* + * Create the MCV composite datum, which is a serialization of an array of + * MCVItems. + * + * The inputs consist of four separate arrays of equal length "numitems" + * (mcv_elems, mcv_nulls, freqs and base_freqs) that form the basics of + * what is stored in the catalogs. These form an array of composite + * records defined by the three atttypX arrays of equal length "numattrs". + * + * If any data element fails to convert to the input type specified for that + * attribute, then function will return a NULL Datum if elevel < ERROR. + */ +Datum +statext_mcv_import(int elevel, int numattrs, + Oid *atttypids, int32 *atttypmods, Oid *atttypcolls, + int nitems, Datum *mcv_elems, bool *mcv_nulls, + float8 *freqs, float8 *base_freqs) +{ + MCVList *mcvlist; + bytea *bytes; + VacAttrStats **vastats; + + /* + * Allocate the MCV list structure, set the global parameters. + */ + mcvlist = (MCVList *) palloc0(offsetof(MCVList, items) + + (sizeof(MCVItem) * nitems)); + + mcvlist->magic = STATS_MCV_MAGIC; + mcvlist->type = STATS_MCV_TYPE_BASIC; + mcvlist->ndimensions = numattrs; + mcvlist->nitems = nitems; + + /* Set the values for the 1-D arrays and allocate space for the 2-D arrays */ + for (int i = 0; i < nitems; i++) + { + MCVItem *item = &mcvlist->items[i]; + + item->frequency = freqs[i]; + item->base_frequency = base_freqs[i]; + item->values = (Datum *) palloc0_array(Datum, numattrs); + item->isnull = (bool *) palloc0_array(bool, numattrs); + } + + /* + * Walk through each dimension, determine the input function for that + * type, and then attempt to convert all values in that column via that + * function. We approach this column-wise because it is simpler to deal + * with one input function at time, and possibly more cache-friendly. + */ + for (int j = 0; j < numattrs; j++) + { + FmgrInfo finfo; + Oid ioparam; + Oid infunc; + int index = j; + + getTypeInputInfo(atttypids[j], &infunc, &ioparam); + fmgr_info(infunc, &finfo); + + /* store info about data type OIDs */ + mcvlist->types[j] = atttypids[j]; + + for (int i = 0; i < nitems; i++) + { + MCVItem *item = &mcvlist->items[i]; + + if (mcv_nulls[index]) + { + /* NULL value detected, hence no input to process */ + item->values[j] = (Datum) 0; + item->isnull[j] = true; + } + else + { + char *s = TextDatumGetCString(mcv_elems[index]); + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + if (!InputFunctionCallSafe(&finfo, s, ioparam, atttypmods[j], + (Node *) &escontext, &item->values[j])) + { + ereport(elevel, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not parse MCV element \"%s\": incorrect value", s))); + pfree(s); + goto error; + } + + pfree(s); + } + + index += numattrs; + } + } + + /* + * The function statext_mcv_serialize() requires an array of pointers to + * VacAttrStats records, but only a few fields within those records have + * to be filled out. + */ + vastats = (VacAttrStats **) palloc0_array(VacAttrStats *, numattrs); + + for (int i = 0; i < numattrs; i++) + { + Oid typid = atttypids[i]; + HeapTuple typtuple; + + typtuple = SearchSysCacheCopy1(TYPEOID, ObjectIdGetDatum(typid)); + + if (!HeapTupleIsValid(typtuple)) + elog(ERROR, "cache lookup failed for type %u", typid); + + vastats[i] = palloc0_object(VacAttrStats); + + vastats[i]->attrtype = (Form_pg_type) GETSTRUCT(typtuple); + vastats[i]->attrtypid = typid; + vastats[i]->attrcollid = atttypcolls[i]; + } + + bytes = statext_mcv_serialize(mcvlist, vastats); + + for (int i = 0; i < numattrs; i++) + { + pfree(vastats[i]); + } + pfree((void *) vastats); + + pfree(mcv_elems); + pfree(mcv_nulls); + + if (bytes == NULL) + { + ereport(elevel, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not import MCV list"))); + goto error; + } + + return PointerGetDatum(bytes); + +error: + statext_mcv_free(mcvlist); + return (Datum) 0; +} diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build index 07cfacd8c39ff..9a7bf55e3014e 100644 --- a/src/backend/statistics/meson.build +++ b/src/backend/statistics/meson.build @@ -1,9 +1,10 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'attribute_stats.c', 'dependencies.c', 'extended_stats.c', + 'extended_stats_funcs.c', 'mcv.c', 'mvdistinct.c', 'relation_stats.c', diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c index 7e7a63405c8ba..4f8f578a22f2b 100644 --- a/src/backend/statistics/mvdistinct.c +++ b/src/backend/statistics/mvdistinct.c @@ -13,7 +13,7 @@ * estimates are already available in pg_statistic. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -27,10 +27,7 @@ #include "catalog/pg_statistic_ext.h" #include "catalog/pg_statistic_ext_data.h" -#include "lib/stringinfo.h" #include "statistics/extended_stats_internal.h" -#include "statistics/statistics.h" -#include "utils/fmgrprotos.h" #include "utils/syscache.h" #include "utils/typcache.h" #include "varatt.h" @@ -113,7 +110,7 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data) MVNDistinctItem *item = &result->items[itemcnt]; int j; - item->attributes = palloc(sizeof(AttrNumber) * k); + item->attributes = palloc_array(AttrNumber, k); item->nattributes = k; /* translate the indexes to attnums */ @@ -329,85 +326,79 @@ statext_ndistinct_deserialize(bytea *data) } /* - * pg_ndistinct_in - * input routine for type pg_ndistinct - * - * pg_ndistinct is real enough to be a table column, but it has no - * operations of its own, and disallows input (just like pg_node_tree). + * Free allocations of a MVNDistinct. */ -Datum -pg_ndistinct_in(PG_FUNCTION_ARGS) +void +statext_ndistinct_free(MVNDistinct *ndistinct) { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot accept a value of type %s", "pg_ndistinct"))); - - PG_RETURN_VOID(); /* keep compiler quiet */ + for (int i = 0; i < ndistinct->nitems; i++) + pfree(ndistinct->items[i].attributes); + pfree(ndistinct); } /* - * pg_ndistinct - * output routine for type pg_ndistinct + * Validate a set of MVNDistincts against the extended statistics object + * definition. * - * Produces a human-readable representation of the value. + * Every MVNDistinctItem must be checked to ensure that the attnums in the + * attributes list correspond to attnums/expressions defined by the extended + * statistics object. + * + * Positive attnums are attributes which must be found in the stxkeys, + * while negative attnums correspond to an expression number, no attribute + * number can be below (0 - numexprs). */ -Datum -pg_ndistinct_out(PG_FUNCTION_ARGS) +bool +statext_ndistinct_validate(const MVNDistinct *ndistinct, + const int2vector *stxkeys, + int numexprs, int elevel) { - bytea *data = PG_GETARG_BYTEA_PP(0); - MVNDistinct *ndist = statext_ndistinct_deserialize(data); - int i; - StringInfoData str; + int attnum_expr_lowbound = 0 - numexprs; - initStringInfo(&str); - appendStringInfoChar(&str, '{'); - - for (i = 0; i < ndist->nitems; i++) + /* Scan through each MVNDistinct entry */ + for (int i = 0; i < ndistinct->nitems; i++) { - int j; - MVNDistinctItem item = ndist->items[i]; - - if (i > 0) - appendStringInfoString(&str, ", "); + MVNDistinctItem item = ndistinct->items[i]; - for (j = 0; j < item.nattributes; j++) + /* + * Cross-check each attribute in a MVNDistinct entry with the extended + * stats object definition. + */ + for (int j = 0; j < item.nattributes; j++) { AttrNumber attnum = item.attributes[j]; + bool ok = false; - appendStringInfo(&str, "%s%d", (j == 0) ? "\"" : ", ", attnum); + if (attnum > 0) + { + /* attribute number in stxkeys */ + for (int k = 0; k < stxkeys->dim1; k++) + { + if (attnum == stxkeys->values[k]) + { + ok = true; + break; + } + } + } + else if ((attnum < 0) && (attnum >= attnum_expr_lowbound)) + { + /* attribute number for an expression */ + ok = true; + } + + if (!ok) + { + ereport(elevel, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("could not validate \"%s\" object: invalid attribute number %d found", + "pg_ndistinct", attnum))); + return false; + } } - appendStringInfo(&str, "\": %d", (int) item.ndistinct); } - appendStringInfoChar(&str, '}'); - - PG_RETURN_CSTRING(str.data); -} - -/* - * pg_ndistinct_recv - * binary input routine for type pg_ndistinct - */ -Datum -pg_ndistinct_recv(PG_FUNCTION_ARGS) -{ - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot accept a value of type %s", "pg_ndistinct"))); - - PG_RETURN_VOID(); /* keep compiler quiet */ -} - -/* - * pg_ndistinct_send - * binary output routine for type pg_ndistinct - * - * n-distinct is serialized into a bytea value, so let's send that. - */ -Datum -pg_ndistinct_send(PG_FUNCTION_ARGS) -{ - return byteasend(fcinfo); + return true; } /* @@ -444,9 +435,9 @@ ndistinct_for_combination(double totalrows, StatsBuildData *data, * using the specified column combination as dimensions. We could try to * sort in place, but it'd probably be more complex and bug-prone. */ - items = (SortItem *) palloc(numrows * sizeof(SortItem)); - values = (Datum *) palloc0(sizeof(Datum) * numrows * k); - isnull = (bool *) palloc0(sizeof(bool) * numrows * k); + items = palloc_array(SortItem, numrows); + values = palloc0_array(Datum, numrows * k); + isnull = palloc0_array(bool, numrows * k); for (i = 0; i < numrows; i++) { @@ -593,12 +584,12 @@ generator_init(int n, int k) Assert((n >= k) && (k > 0)); /* allocate the generator state as a single chunk of memory */ - state = (CombinationGenerator *) palloc(sizeof(CombinationGenerator)); + state = palloc_object(CombinationGenerator); state->ncombinations = n_choose_k(n, k); /* pre-allocate space for all combinations */ - state->combinations = (int *) palloc(sizeof(int) * k * state->ncombinations); + state->combinations = palloc_array(int, k * state->ncombinations); state->current = 0; state->k = k; @@ -691,7 +682,7 @@ generate_combinations_recurse(CombinationGenerator *state, static void generate_combinations(CombinationGenerator *state) { - int *current = (int *) palloc0(sizeof(int) * state->k); + int *current = palloc0_array(int, state->k); generate_combinations_recurse(state, 0, 0, current); diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c index cd3a75b621a0c..d6631e9a9a47c 100644 --- a/src/backend/statistics/relation_stats.c +++ b/src/backend/statistics/relation_stats.c @@ -6,7 +6,7 @@ * Code supporting the direct import of relation statistics, similar to * what is done by the ANALYZE command. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -20,6 +20,7 @@ #include "access/heapam.h" #include "catalog/indexing.h" #include "catalog/namespace.h" +#include "nodes/makefuncs.h" #include "statistics/stat_utils.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -82,6 +83,7 @@ relation_statistics_update(FunctionCallInfo fcinfo) Datum values[4] = {0}; bool nulls[4] = {0}; int nreplaces = 0; + Oid locked_table = InvalidOid; stats_check_required_arg(fcinfo, relarginfo, RELSCHEMA_ARG); stats_check_required_arg(fcinfo, relarginfo, RELNAME_ARG); @@ -89,15 +91,15 @@ relation_statistics_update(FunctionCallInfo fcinfo) nspname = TextDatumGetCString(PG_GETARG_DATUM(RELSCHEMA_ARG)); relname = TextDatumGetCString(PG_GETARG_DATUM(RELNAME_ARG)); - reloid = stats_lookup_relid(nspname, relname); - if (RecoveryInProgress()) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("recovery is in progress"), errhint("Statistics cannot be modified during recovery."))); - stats_lock_check_privileges(reloid); + reloid = RangeVarGetRelidExtended(makeRangeVar(nspname, relname, -1), + ShareUpdateExclusiveLock, 0, + RangeVarCallbackForStats, &locked_table); if (!PG_ARGISNULL(RELPAGES_ARG)) { @@ -112,7 +114,7 @@ relation_statistics_update(FunctionCallInfo fcinfo) { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("reltuples cannot be < -1.0"))); + errmsg("argument \"%s\" must not be less than -1.0", "reltuples"))); result = false; } else diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c index a9a3224efe6fd..9c680f1cb3717 100644 --- a/src/backend/statistics/stat_utils.c +++ b/src/backend/statistics/stat_utils.c @@ -5,7 +5,7 @@ * * Code supporting the direct manipulation of statistics. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -16,12 +16,17 @@ #include "postgres.h" +#include "access/htup_details.h" #include "access/relation.h" #include "catalog/index.h" #include "catalog/namespace.h" +#include "catalog/pg_class.h" +#include "catalog/pg_collation.h" #include "catalog/pg_database.h" +#include "catalog/pg_statistic.h" #include "funcapi.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "statistics/stat_utils.h" #include "storage/lmgr.h" #include "utils/acl.h" @@ -29,6 +34,17 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/syscache.h" +#include "utils/typcache.h" + +/* Default values assigned to new pg_statistic tuples. */ +#define DEFAULT_STATATT_NULL_FRAC Float4GetDatum(0.0) /* stanullfrac */ +#define DEFAULT_STATATT_AVG_WIDTH Int32GetDatum(0) /* stawidth, same as + * unknown */ +#define DEFAULT_STATATT_N_DISTINCT Float4GetDatum(0.0) /* stadistinct, same as + * unknown */ + +static Node *statatt_get_index_expr(Relation rel, int attnum); /* * Ensure that a given argument is not null. @@ -41,7 +57,7 @@ stats_check_required_arg(FunctionCallInfo fcinfo, if (PG_ARGISNULL(argnum)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"%s\" cannot be NULL", + errmsg("argument \"%s\" must not be null", arginfo[argnum].argname))); } @@ -68,7 +84,7 @@ stats_check_arg_array(FunctionCallInfo fcinfo, { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"%s\" cannot be a multidimensional array", + errmsg("argument \"%s\" must not be a multidimensional array", arginfo[argnum].argname))); return false; } @@ -77,7 +93,7 @@ stats_check_arg_array(FunctionCallInfo fcinfo, { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"%s\" array cannot contain NULL values", + errmsg("argument \"%s\" array must not contain null values", arginfo[argnum].argname))); return false; } @@ -108,7 +124,7 @@ stats_check_arg_pair(FunctionCallInfo fcinfo, ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("\"%s\" must be specified when \"%s\" is specified", + errmsg("argument \"%s\" must be specified when argument \"%s\" is specified", arginfo[nullarg].argname, arginfo[otherarg].argname))); @@ -119,53 +135,84 @@ stats_check_arg_pair(FunctionCallInfo fcinfo, } /* - * Lock relation in ShareUpdateExclusive mode, check privileges, and close the - * relation (but retain the lock). - * * A role has privileges to set statistics on the relation if any of the * following are true: * - the role owns the current database and the relation is not shared * - the role has the MAINTAIN privilege on the relation */ void -stats_lock_check_privileges(Oid reloid) +RangeVarCallbackForStats(const RangeVar *relation, + Oid relId, Oid oldRelId, void *arg) { - Relation table; - Oid table_oid = reloid; - Oid index_oid = InvalidOid; - LOCKMODE index_lockmode = NoLock; + Oid *locked_oid = (Oid *) arg; + Oid table_oid = relId; + HeapTuple tuple; + Form_pg_class form; + char relkind; /* - * For indexes, we follow the locking behavior in do_analyze_rel() and - * check_lock_if_inplace_updateable_rel(), which is to lock the table - * first in ShareUpdateExclusive mode and then the index in AccessShare - * mode. - * - * Partitioned indexes are treated differently than normal indexes in - * check_lock_if_inplace_updateable_rel(), so we take a - * ShareUpdateExclusive lock on both the partitioned table and the - * partitioned index. + * If we previously locked some other index's heap, and the name we're + * looking up no longer refers to that relation, release the now-useless + * lock. */ - switch (get_rel_relkind(reloid)) + if (relId != oldRelId && OidIsValid(*locked_oid)) { - case RELKIND_INDEX: - index_oid = reloid; - table_oid = IndexGetRelation(index_oid, false); - index_lockmode = AccessShareLock; - break; - case RELKIND_PARTITIONED_INDEX: - index_oid = reloid; - table_oid = IndexGetRelation(index_oid, false); - index_lockmode = ShareUpdateExclusiveLock; - break; - default: - break; + UnlockRelationOid(*locked_oid, ShareUpdateExclusiveLock); + *locked_oid = InvalidOid; + } + + /* If the relation does not exist, there's nothing more to do. */ + if (!OidIsValid(relId)) + return; + + /* If the relation does exist, check whether it's an index. */ + relkind = get_rel_relkind(relId); + if (relkind == RELKIND_INDEX || + relkind == RELKIND_PARTITIONED_INDEX) + table_oid = IndexGetRelation(relId, false); + + /* + * If retrying yields the same OID, there are a couple of extremely + * unlikely scenarios we need to handle. + */ + if (relId == oldRelId) + { + /* + * If a previous lookup found an index, but the current lookup did + * not, the index was dropped and the OID was reused for something + * else between lookups. In theory, we could simply drop our lock on + * the index's parent table and proceed, but in the interest of + * avoiding complexity, we just error. + */ + if (table_oid == relId && OidIsValid(*locked_oid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" was concurrently dropped", + relation->relname))); + + /* + * If the current lookup found an index but a previous lookup either + * did not find an index or found one with a different parent + * relation, the relation was dropped and the OID was reused for an + * index between lookups. RangeVarGetRelidExtended() will have + * already locked the index at this point, so we can't just lock the + * newly discovered parent table OID without risking deadlock. As + * above, we just error in this case. + */ + if (table_oid != relId && table_oid != *locked_oid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("index \"%s\" was concurrently created", + relation->relname))); } - table = relation_open(table_oid, ShareUpdateExclusiveLock); + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for OID %u", table_oid); + form = (Form_pg_class) GETSTRUCT(tuple); /* the relkinds that can be used with ANALYZE */ - switch (table->rd_rel->relkind) + switch (form->relkind) { case RELKIND_RELATION: case RELKIND_MATVIEW: @@ -176,62 +223,36 @@ stats_lock_check_privileges(Oid reloid) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot modify statistics for relation \"%s\"", - RelationGetRelationName(table)), - errdetail_relkind_not_supported(table->rd_rel->relkind))); + NameStr(form->relname)), + errdetail_relkind_not_supported(form->relkind))); } - if (OidIsValid(index_oid)) - { - Relation index; - - Assert(index_lockmode != NoLock); - index = relation_open(index_oid, index_lockmode); - - Assert(index->rd_index && index->rd_index->indrelid == table_oid); - - /* retain lock on index */ - relation_close(index, NoLock); - } - - if (table->rd_rel->relisshared) + if (form->relisshared) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot modify statistics for shared relation"))); + /* Check permissions */ if (!object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())) { - AclResult aclresult = pg_class_aclcheck(RelationGetRelid(table), + AclResult aclresult = pg_class_aclcheck(table_oid, GetUserId(), ACL_MAINTAIN); if (aclresult != ACLCHECK_OK) aclcheck_error(aclresult, - get_relkind_objtype(table->rd_rel->relkind), - NameStr(table->rd_rel->relname)); + get_relkind_objtype(form->relkind), + NameStr(form->relname)); } - /* retain lock on table */ - relation_close(table, NoLock); -} + ReleaseSysCache(tuple); -/* - * Lookup relation oid from schema and relation name. - */ -Oid -stats_lookup_relid(const char *nspname, const char *relname) -{ - Oid nspoid; - Oid reloid; - - nspoid = LookupExplicitNamespace(nspname, false); - reloid = get_relname_relid(relname, nspoid); - if (!OidIsValid(reloid)) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_TABLE), - errmsg("relation \"%s.%s\" does not exist", - nspname, relname))); - - return reloid; + /* Lock heap before index to avoid deadlock. */ + if (relId != oldRelId && table_oid != relId) + { + LockRelationOid(table_oid, ShareUpdateExclusiveLock); + *locked_oid = table_oid; + } } @@ -263,7 +284,7 @@ stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype) if (argtype != expectedtype) { ereport(WARNING, - (errmsg("argument \"%s\" has type \"%s\", expected type \"%s\"", + (errmsg("argument \"%s\" has type %s, expected type %s", argname, format_type_be(argtype), format_type_be(expectedtype)))); return false; @@ -272,6 +293,50 @@ stats_check_arg_type(const char *argname, Oid argtype, Oid expectedtype) return true; } +/* + * Check if attribute of an index is an expression, then retrieve the + * expression if is it the case. + * + * If the attnum specified is known to be an expression, then we must + * walk the list attributes up to the specified attnum to get the right + * expression. + */ +static Node * +statatt_get_index_expr(Relation rel, int attnum) +{ + List *index_exprs; + ListCell *indexpr_item; + + /* relation is not an index */ + if (rel->rd_rel->relkind != RELKIND_INDEX && + rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) + return NULL; + + index_exprs = RelationGetIndexExpressions(rel); + + /* index has no expressions to give */ + if (index_exprs == NIL) + return NULL; + + /* + * The index's attnum points directly to a relation attnum, hence it is + * not an expression attribute. + */ + if (rel->rd_index->indkey.values[attnum - 1] != 0) + return NULL; + + indexpr_item = list_head(rel->rd_indexprs); + + for (int i = 0; i < attnum - 1; i++) + if (rel->rd_index->indkey.values[i] == 0) + indexpr_item = lnext(rel->rd_indexprs, indexpr_item); + + if (indexpr_item == NULL) /* shouldn't happen */ + elog(ERROR, "too few entries in indexprs list"); + + return (Node *) lfirst(indexpr_item); +} + /* * Translate variadic argument pairs from 'pairs_fcinfo' into a * 'positional_fcinfo' appropriate for calling relation_statistics_update() or @@ -319,11 +384,11 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo, if (argnulls[i]) ereport(ERROR, - (errmsg("name at variadic position %d is NULL", i + 1))); + (errmsg("name at variadic position %d is null", i + 1))); if (types[i] != TEXTOID) ereport(ERROR, - (errmsg("name at variadic position %d has type \"%s\", expected type \"%s\"", + (errmsg("name at variadic position %d has type %s, expected type %s", i + 1, format_type_be(types[i]), format_type_be(TEXTOID)))); @@ -357,3 +422,325 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo, return result; } + +/* + * Derive type information from a relation attribute. + * + * This is needed for setting most slot statistics for all data types. + * + * This duplicates the logic in examine_attribute() but it will not skip the + * attribute if the attstattarget is 0. + * + * This information, retrieved from pg_attribute and pg_type with some + * specific handling for index expressions, is a prerequisite to calling + * any of the other statatt_*() functions. + */ +void +statatt_get_type(Oid reloid, AttrNumber attnum, + Oid *atttypid, int32 *atttypmod, + char *atttyptype, Oid *atttypcoll, + Oid *eq_opr, Oid *lt_opr) +{ + Relation rel = relation_open(reloid, AccessShareLock); + Form_pg_attribute attr; + HeapTuple atup; + Node *expr; + TypeCacheEntry *typcache; + + atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid), + Int16GetDatum(attnum)); + + /* Attribute not found */ + if (!HeapTupleIsValid(atup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column %d of relation \"%s\" does not exist", + attnum, RelationGetRelationName(rel)))); + + attr = (Form_pg_attribute) GETSTRUCT(atup); + + if (attr->attisdropped) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column %d of relation \"%s\" does not exist", + attnum, RelationGetRelationName(rel)))); + + expr = statatt_get_index_expr(rel, attr->attnum); + + /* + * When analyzing an expression index, believe the expression tree's type + * not the column datatype --- the latter might be the opckeytype storage + * type of the opclass, which is not interesting for our purposes. This + * mimics the behavior of examine_attribute(). + */ + if (expr == NULL) + { + *atttypid = attr->atttypid; + *atttypmod = attr->atttypmod; + *atttypcoll = attr->attcollation; + } + else + { + *atttypid = exprType(expr); + *atttypmod = exprTypmod(expr); + + if (OidIsValid(attr->attcollation)) + *atttypcoll = attr->attcollation; + else + *atttypcoll = exprCollation(expr); + } + ReleaseSysCache(atup); + + /* + * If it's a multirange, step down to the range type, as is done by + * multirange_typanalyze(). + */ + if (type_is_multirange(*atttypid)) + *atttypid = get_multirange_range(*atttypid); + + /* finds the right operators even if atttypid is a domain */ + typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR); + *atttyptype = typcache->typtype; + *eq_opr = typcache->eq_opr; + *lt_opr = typcache->lt_opr; + + /* + * Special case: collation for tsvector is DEFAULT_COLLATION_OID. See + * compute_tsvector_stats(). + */ + if (*atttypid == TSVECTOROID) + *atttypcoll = DEFAULT_COLLATION_OID; + + relation_close(rel, NoLock); +} + +/* + * Derive element type information from the attribute type. This information + * is needed when the given type is one that contains elements of other types. + * + * The atttypid and atttyptype should be derived from a previous call to + * statatt_get_type(). + */ +bool +statatt_get_elem_type(Oid atttypid, char atttyptype, + Oid *elemtypid, Oid *elem_eq_opr) +{ + TypeCacheEntry *elemtypcache; + + if (atttypid == TSVECTOROID) + { + /* + * Special case: element type for tsvector is text. See + * compute_tsvector_stats(). + */ + *elemtypid = TEXTOID; + } + else + { + /* find underlying element type through any domain */ + *elemtypid = get_base_element_type(atttypid); + } + + if (!OidIsValid(*elemtypid)) + return false; + + /* finds the right operator even if elemtypid is a domain */ + elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR); + if (!OidIsValid(elemtypcache->eq_opr)) + return false; + + *elem_eq_opr = elemtypcache->eq_opr; + + return true; +} + +/* + * Build an array with element type elemtypid from a text datum, used as + * value of an attribute in a tuple to-be-inserted into pg_statistic. + * + * The typid and typmod should be derived from a previous call to + * statatt_get_type(). + * + * If an error is encountered, capture it and throw a WARNING, with "ok" set + * to false. If the resulting array contains NULLs, raise a WARNING and + * set "ok" to false. When the operation succeeds, set "ok" to true. + */ +Datum +statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid, + int32 typmod, bool *ok) +{ + LOCAL_FCINFO(fcinfo, 8); + char *s; + Datum result; + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + escontext.details_wanted = true; + + s = TextDatumGetCString(d); + + InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid, + (Node *) &escontext, NULL); + + fcinfo->args[0].value = CStringGetDatum(s); + fcinfo->args[0].isnull = false; + fcinfo->args[1].value = ObjectIdGetDatum(typid); + fcinfo->args[1].isnull = false; + fcinfo->args[2].value = Int32GetDatum(typmod); + fcinfo->args[2].isnull = false; + + result = FunctionCallInvoke(fcinfo); + + pfree(s); + + if (escontext.error_occurred) + { + escontext.error_data->elevel = WARNING; + ThrowErrorData(escontext.error_data); + *ok = false; + return (Datum) 0; + } + + if (array_contains_nulls(DatumGetArrayTypeP(result))) + { + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"%s\" array must not contain null values", staname))); + *ok = false; + return (Datum) 0; + } + + *ok = true; + + return result; +} + +/* + * Find and update the slot of a stakind, or use the first empty slot. + * + * Core statistics types expect the stakind value to be one of the + * STATISTIC_KIND_* constants defined in pg_statistic.h, but types defined + * by extensions are not restricted to those values. + * + * In the case of core statistics, the required staop is determined by the + * stakind given and will either be a hardcoded oid, or the eq/lt operator + * derived from statatt_get_type(). Likewise, types defined by extensions + * have no such restriction. + * + * The stacoll value should be either the atttypcoll derived from + * statatt_get_type(), or a hardcoded value required by that particular + * stakind. + * + * The value/null pairs for stanumbers and stavalues should be calculated + * based on the stakind, using statatt_build_stavalues() or constructed arrays. + */ +void +statatt_set_slot(Datum *values, bool *nulls, bool *replaces, + int16 stakind, Oid staop, Oid stacoll, + Datum stanumbers, bool stanumbers_isnull, + Datum stavalues, bool stavalues_isnull) +{ + int slotidx; + int first_empty = -1; + AttrNumber stakind_attnum; + AttrNumber staop_attnum; + AttrNumber stacoll_attnum; + + /* find existing slot with given stakind */ + for (slotidx = 0; slotidx < STATISTIC_NUM_SLOTS; slotidx++) + { + stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx; + + if (first_empty < 0 && + DatumGetInt16(values[stakind_attnum]) == 0) + first_empty = slotidx; + if (DatumGetInt16(values[stakind_attnum]) == stakind) + break; + } + + if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0) + slotidx = first_empty; + + if (slotidx >= STATISTIC_NUM_SLOTS) + ereport(ERROR, + (errmsg("maximum number of statistics slots exceeded: %d", + slotidx + 1))); + + stakind_attnum = Anum_pg_statistic_stakind1 - 1 + slotidx; + staop_attnum = Anum_pg_statistic_staop1 - 1 + slotidx; + stacoll_attnum = Anum_pg_statistic_stacoll1 - 1 + slotidx; + + if (DatumGetInt16(values[stakind_attnum]) != stakind) + { + values[stakind_attnum] = Int16GetDatum(stakind); + replaces[stakind_attnum] = true; + } + if (DatumGetObjectId(values[staop_attnum]) != staop) + { + values[staop_attnum] = ObjectIdGetDatum(staop); + replaces[staop_attnum] = true; + } + if (DatumGetObjectId(values[stacoll_attnum]) != stacoll) + { + values[stacoll_attnum] = ObjectIdGetDatum(stacoll); + replaces[stacoll_attnum] = true; + } + if (!stanumbers_isnull) + { + values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers; + nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false; + replaces[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = true; + } + if (!stavalues_isnull) + { + values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues; + nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false; + replaces[Anum_pg_statistic_stavalues1 - 1 + slotidx] = true; + } +} + +/* + * Initialize values and nulls for a new pg_statistic tuple. + * + * The caller is responsible for allocating the arrays where the results are + * stored, which should be of size Natts_pg_statistic. + * + * When using this routine for a tuple inserted into pg_statistic, reloid, + * attnum and inherited flags should all be set. + * + * When using this routine for a tuple that is an element of a stxdexpr + * array inserted into pg_statistic_ext_data, reloid, attnum and inherited + * should be respectively set to InvalidOid, InvalidAttrNumber and false. + */ +void +statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited, + Datum *values, bool *nulls, bool *replaces) +{ + memset(nulls, true, sizeof(bool) * Natts_pg_statistic); + memset(replaces, true, sizeof(bool) * Natts_pg_statistic); + + /* This must initialize non-NULL attributes */ + values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid); + nulls[Anum_pg_statistic_starelid - 1] = false; + values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum); + nulls[Anum_pg_statistic_staattnum - 1] = false; + values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited); + nulls[Anum_pg_statistic_stainherit - 1] = false; + + values[Anum_pg_statistic_stanullfrac - 1] = DEFAULT_STATATT_NULL_FRAC; + nulls[Anum_pg_statistic_stanullfrac - 1] = false; + values[Anum_pg_statistic_stawidth - 1] = DEFAULT_STATATT_AVG_WIDTH; + nulls[Anum_pg_statistic_stawidth - 1] = false; + values[Anum_pg_statistic_stadistinct - 1] = DEFAULT_STATATT_N_DISTINCT; + nulls[Anum_pg_statistic_stadistinct - 1] = false; + + /* initialize stakind, staop, and stacoll slots */ + for (int slotnum = 0; slotnum < STATISTIC_NUM_SLOTS; slotnum++) + { + values[Anum_pg_statistic_stakind1 + slotnum - 1] = (Datum) 0; + nulls[Anum_pg_statistic_stakind1 + slotnum - 1] = false; + values[Anum_pg_statistic_staop1 + slotnum - 1] = ObjectIdGetDatum(InvalidOid); + nulls[Anum_pg_statistic_staop1 + slotnum - 1] = false; + values[Anum_pg_statistic_stacoll1 + slotnum - 1] = ObjectIdGetDatum(InvalidOid); + nulls[Anum_pg_statistic_stacoll1 + slotnum - 1] = false; + } +} diff --git a/src/backend/storage/Makefile b/src/backend/storage/Makefile index eec03f6f2b4c5..2afb42ca96e40 100644 --- a/src/backend/storage/Makefile +++ b/src/backend/storage/Makefile @@ -8,6 +8,16 @@ subdir = src/backend/storage top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = aio buffer file freespace ipc large_object lmgr page smgr sync +SUBDIRS = \ + aio \ + buffer \ + file \ + freespace \ + ipc \ + large_object \ + lmgr \ + page \ + smgr \ + sync include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/storage/aio/README.md b/src/backend/storage/aio/README.md index f10b5c7e31ec7..72ae3b3737d51 100644 --- a/src/backend/storage/aio/README.md +++ b/src/backend/storage/aio/README.md @@ -94,7 +94,7 @@ pgaio_io_register_callbacks(ioh, PGAIO_HCB_SHARED_BUFFER_READV, 0); * * In this example we're reading only a single buffer, hence the 1. */ -pgaio_io_set_handle_data_32(ioh, (uint32 *) buffer, 1); +pgaio_io_set_handle_data_32(ioh, (uint32 *) &buffer, 1); /* * Pass the AIO handle to lower-level function. When operating on the level of @@ -119,8 +119,9 @@ pgaio_io_set_handle_data_32(ioh, (uint32 *) buffer, 1); * e.g. due to reaching a limit on the number of unsubmitted IOs, and even * complete before smgrstartreadv() returns. */ +void *page = BufferGetBlock(buffer); smgrstartreadv(ioh, operation->smgr, forknum, blkno, - BufferGetBlock(buffer), 1); + &page, 1); /* * To benefit from AIO, it is beneficial to perform other work, including diff --git a/src/backend/storage/aio/aio.c b/src/backend/storage/aio/aio.c index c64d815ebd12a..8f7e26607b915 100644 --- a/src/backend/storage/aio/aio.c +++ b/src/backend/storage/aio/aio.c @@ -27,7 +27,7 @@ * - README.md - higher-level overview over AIO * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -53,7 +53,7 @@ static inline void pgaio_io_update_state(PgAioHandle *ioh, PgAioHandleState new_state); static void pgaio_io_reclaim(PgAioHandle *ioh); -static void pgaio_io_resowner_register(PgAioHandle *ioh); +static void pgaio_io_resowner_register(PgAioHandle *ioh, struct ResourceOwnerData *resowner); static void pgaio_io_wait_for_free(void); static PgAioHandle *pgaio_io_from_wref(PgAioWaitRef *iow, uint64 *ref_generation); static const char *pgaio_io_state_get_name(PgAioHandleState s); @@ -89,6 +89,9 @@ static const IoMethodOps *const pgaio_method_ops_table[] = { #endif }; +StaticAssertDecl(lengthof(io_method_options) == lengthof(pgaio_method_ops_table) + 1, + "io_method_options out of sync with pgaio_method_ops_table"); + /* callbacks for the configured io_method, set by assign_io_method */ const IoMethodOps *pgaio_method_ops; @@ -214,7 +217,7 @@ pgaio_io_acquire_nb(struct ResourceOwnerData *resowner, PgAioReturn *ret) pgaio_my_backend->handed_out_io = ioh; if (resowner) - pgaio_io_resowner_register(ioh); + pgaio_io_resowner_register(ioh, resowner); if (ret) { @@ -275,7 +278,7 @@ pgaio_io_release_resowner(dlist_node *ioh_node, bool on_error) ResourceOwnerForgetAioHandle(ioh->resowner, &ioh->resowner_node); ioh->resowner = NULL; - switch (ioh->state) + switch ((PgAioHandleState) ioh->state) { case PGAIO_HS_IDLE: elog(ERROR, "unexpected"); @@ -403,13 +406,13 @@ pgaio_io_update_state(PgAioHandle *ioh, PgAioHandleState new_state) } static void -pgaio_io_resowner_register(PgAioHandle *ioh) +pgaio_io_resowner_register(PgAioHandle *ioh, struct ResourceOwnerData *resowner) { Assert(!ioh->resowner); - Assert(CurrentResourceOwner); + Assert(resowner); - ResourceOwnerRememberAioHandle(CurrentResourceOwner, &ioh->resowner_node); - ioh->resowner = CurrentResourceOwner; + ResourceOwnerRememberAioHandle(resowner, &ioh->resowner_node); + ioh->resowner = resowner; } /* @@ -556,6 +559,13 @@ bool pgaio_io_was_recycled(PgAioHandle *ioh, uint64 ref_generation, PgAioHandleState *state) { *state = ioh->state; + + /* + * Ensure that we don't see an earlier state of the handle than ioh->state + * due to compiler or CPU reordering. This protects both ->generation as + * directly used here, and other fields in the handle accessed in the + * caller if the handle was not reused. + */ pg_read_barrier(); return ioh->generation != ref_generation; @@ -612,7 +622,7 @@ pgaio_io_wait(PgAioHandle *ioh, uint64 ref_generation) pgaio_method_ops->wait_one(ioh, ref_generation); continue; } - /* fallthrough */ + pg_fallthrough; /* waiting for owner to submit */ case PGAIO_HS_DEFINED: @@ -752,7 +762,7 @@ pgaio_io_wait_for_free(void) { int reclaimed = 0; - pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %d in-flight, %d idle IOs", + pgaio_debug(DEBUG2, "waiting for free IO with %d pending, %u in-flight, %u idle IOs", pgaio_my_backend->num_staged_ios, dclist_count(&pgaio_my_backend->in_flight_ios), dclist_count(&pgaio_my_backend->idle_ios)); @@ -773,7 +783,12 @@ pgaio_io_wait_for_free(void) * Note that no interrupts are processed between the state check * and the call to reclaim - that's important as otherwise an * interrupt could have already reclaimed the handle. + * + * Need to ensure that there's no reordering, in the more common + * paths, where we wait for IO, that's done by + * pgaio_io_was_recycled(). */ + pg_read_barrier(); pgaio_io_reclaim(ioh); reclaimed++; } @@ -797,7 +812,7 @@ pgaio_io_wait_for_free(void) if (dclist_count(&pgaio_my_backend->in_flight_ios) == 0) ereport(ERROR, errmsg_internal("no free IOs despite no in-flight IOs"), - errdetail_internal("%d pending, %d in-flight, %d idle IOs", + errdetail_internal("%d pending, %u in-flight, %u idle IOs", pgaio_my_backend->num_staged_ios, dclist_count(&pgaio_my_backend->in_flight_ios), dclist_count(&pgaio_my_backend->idle_ios))); @@ -813,7 +828,7 @@ pgaio_io_wait_for_free(void) &pgaio_my_backend->in_flight_ios); uint64 generation = ioh->generation; - switch (ioh->state) + switch ((PgAioHandleState) ioh->state) { /* should not be in in-flight list */ case PGAIO_HS_IDLE: @@ -828,7 +843,7 @@ pgaio_io_wait_for_free(void) case PGAIO_HS_COMPLETED_IO: case PGAIO_HS_SUBMITTED: pgaio_debug_io(DEBUG2, ioh, - "waiting for free io with %d in flight", + "waiting for free io with %u in flight", dclist_count(&pgaio_my_backend->in_flight_ios)); /* @@ -852,7 +867,12 @@ pgaio_io_wait_for_free(void) * check and the call to reclaim - that's important as * otherwise an interrupt could have already reclaimed the * handle. + * + * Need to ensure that there's no reordering, in the more + * common paths, where we wait for IO, that's done by + * pgaio_io_was_recycled(). */ + pg_read_barrier(); pgaio_io_reclaim(ioh); break; } @@ -999,6 +1019,21 @@ pgaio_wref_check_done(PgAioWaitRef *iow) am_owner = ioh->owner_procno == MyProcNumber; + /* + * If the IO is not executing synchronously, allow the IO method to check + * if the IO already has completed. + */ + if (pgaio_method_ops->check_one && !(ioh->flags & PGAIO_HF_SYNCHRONOUS)) + { + pgaio_method_ops->check_one(ioh, ref_generation); + + if (pgaio_io_was_recycled(ioh, ref_generation, &state)) + return true; + + if (state == PGAIO_HS_IDLE) + return true; + } + if (state == PGAIO_HS_COMPLETED_SHARED || state == PGAIO_HS_COMPLETED_LOCAL) { @@ -1012,11 +1047,6 @@ pgaio_wref_check_done(PgAioWaitRef *iow) return true; } - /* - * XXX: It likely would be worth checking in with the io method, to give - * the IO method a chance to check if there are completion events queued. - */ - return false; } @@ -1252,7 +1282,7 @@ pgaio_closing_fd(int fd) break; pgaio_debug_io(DEBUG2, ioh, - "waiting for IO before FD %d gets closed, %d in-flight IOs", + "waiting for IO before FD %d gets closed, %u in-flight IOs", fd, dclist_count(&pgaio_my_backend->in_flight_ios)); /* see comment in pgaio_io_wait_for_free() about raciness */ @@ -1288,7 +1318,7 @@ pgaio_shutdown(int code, Datum arg) uint64 generation = ioh->generation; pgaio_debug_io(DEBUG2, ioh, - "waiting for IO to complete during shutdown, %d in-flight IOs", + "waiting for IO to complete during shutdown, %u in-flight IOs", dclist_count(&pgaio_my_backend->in_flight_ios)); /* see comment in pgaio_io_wait_for_free() about raciness */ @@ -1301,8 +1331,8 @@ pgaio_shutdown(int code, Datum arg) void assign_io_method(int newval, void *extra) { + Assert(newval < lengthof(pgaio_method_ops_table)); Assert(pgaio_method_ops_table[newval] != NULL); - Assert(newval < lengthof(io_method_options)); pgaio_method_ops = pgaio_method_ops_table[newval]; } diff --git a/src/backend/storage/aio/aio_callback.c b/src/backend/storage/aio/aio_callback.c index 0ad9795bb7e0c..206b81f5028e0 100644 --- a/src/backend/storage/aio/aio_callback.c +++ b/src/backend/storage/aio/aio_callback.c @@ -4,7 +4,7 @@ * AIO - Functionality related to callbacks that can be registered on IO * Handles * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -256,6 +256,9 @@ pgaio_io_call_complete_shared(PgAioHandle *ioh) pgaio_result_status_string(result.status), result.id, result.error_data, result.result); result = ce->cb->complete_shared(ioh, result, cb_data); + + /* the callback should never transition to unknown */ + Assert(result.status != PGAIO_RS_UNKNOWN); } ioh->distilled_result = result; @@ -290,6 +293,7 @@ pgaio_io_call_complete_local(PgAioHandle *ioh) /* start with distilled result from shared callback */ result = ioh->distilled_result; + Assert(result.status != PGAIO_RS_UNKNOWN); for (int i = ioh->num_callbacks; i > 0; i--) { @@ -306,6 +310,9 @@ pgaio_io_call_complete_local(PgAioHandle *ioh) pgaio_result_status_string(result.status), result.id, result.error_data, result.result); result = ce->cb->complete_local(ioh, result, cb_data); + + /* the callback should never transition to unknown */ + Assert(result.status != PGAIO_RS_UNKNOWN); } /* diff --git a/src/backend/storage/aio/aio_funcs.c b/src/backend/storage/aio/aio_funcs.c index 584e683371a31..bcdd82318f7bd 100644 --- a/src/backend/storage/aio/aio_funcs.c +++ b/src/backend/storage/aio/aio_funcs.c @@ -4,7 +4,7 @@ * AIO - SQL interface for AIO * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -56,7 +56,7 @@ pg_get_aios(PG_FUNCTION_ARGS) for (uint64 i = 0; i < pgaio_ctl->io_handle_count; i++) { PgAioHandle *live_ioh = &pgaio_ctl->io_handles[i]; - uint32 ioh_id = pgaio_io_get_id(live_ioh); + int ioh_id = pgaio_io_get_id(live_ioh); Datum values[PG_GET_AIOS_COLS] = {0}; bool nulls[PG_GET_AIOS_COLS] = {0}; ProcNumber owner; @@ -149,10 +149,10 @@ pg_get_aios(PG_FUNCTION_ARGS) if (owner_pid != 0) values[0] = Int32GetDatum(owner_pid); else - nulls[0] = false; + nulls[0] = true; /* column: IO's id */ - values[1] = ioh_id; + values[1] = Int32GetDatum(ioh_id); /* column: IO's generation */ values[2] = Int64GetDatum(start_generation); @@ -175,7 +175,7 @@ pg_get_aios(PG_FUNCTION_ARGS) values[4] = CStringGetTextDatum(pgaio_io_get_op_name(&ioh_copy)); /* columns: details about the IO's operation (offset, length) */ - switch (ioh_copy.op) + switch ((PgAioOp) ioh_copy.op) { case PGAIO_OP_INVALID: nulls[5] = true; diff --git a/src/backend/storage/aio/aio_init.c b/src/backend/storage/aio/aio_init.c index 885c3940c6626..da30d792a8830 100644 --- a/src/backend/storage/aio/aio_init.c +++ b/src/backend/storage/aio/aio_init.c @@ -3,7 +3,7 @@ * aio_init.c * AIO - Subsystem Initialization * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -23,28 +23,32 @@ #include "storage/ipc.h" #include "storage/proc.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/guc.h" +static void AioShmemRequest(void *arg); +static void AioShmemInit(void *arg); +static void AioShmemAttach(void *arg); -static Size -AioCtlShmemSize(void) -{ - Size sz; +const ShmemCallbacks AioShmemCallbacks = { + .request_fn = AioShmemRequest, + .init_fn = AioShmemInit, + .attach_fn = AioShmemAttach, +}; - /* pgaio_ctl itself */ - sz = offsetof(PgAioCtl, io_handles); - - return sz; -} +static PgAioBackend *AioBackendShmemPtr; +static PgAioHandle *AioHandleShmemPtr; +static struct iovec *AioHandleIOVShmemPtr; +static uint64 *AioHandleDataShmemPtr; static uint32 AioProcs(void) { /* * While AIO workers don't need their own AIO context, we can't currently - * guarantee nothing gets assigned to the a ProcNumber for an IO worker if - * we just subtracted MAX_IO_WORKERS. + * guarantee that nothing gets assigned to an IO worker's ProcNumber if we + * just subtracted MAX_IO_WORKERS. */ return MaxBackends + NUM_AUXILIARY_PROCS; } @@ -113,12 +117,15 @@ AioChooseMaxConcurrency(void) return Min(max_proportional_pins, 64); } -Size -AioShmemSize(void) +/* + * Register AIO subsystem's shared memory needs. + */ +static void +AioShmemRequest(void *arg) { - Size sz = 0; - /* + * Resolve io_max_concurrency if not already done + * * We prefer to report this value's source as PGC_S_DYNAMIC_DEFAULT. * However, if the DBA explicitly set io_max_concurrency = -1 in the * config file, then PGC_S_DYNAMIC_DEFAULT will fail to override that and @@ -136,48 +143,52 @@ AioShmemSize(void) PGC_S_OVERRIDE); } - sz = add_size(sz, AioCtlShmemSize()); - sz = add_size(sz, AioBackendShmemSize()); - sz = add_size(sz, AioHandleShmemSize()); - sz = add_size(sz, AioHandleIOVShmemSize()); - sz = add_size(sz, AioHandleDataShmemSize()); - - /* Reserve space for method specific resources. */ - if (pgaio_method_ops->shmem_size) - sz = add_size(sz, pgaio_method_ops->shmem_size()); - - return sz; + ShmemRequestStruct(.name = "AioCtl", + .size = sizeof(PgAioCtl), + .ptr = (void **) &pgaio_ctl, + ); + + ShmemRequestStruct(.name = "AioBackend", + .size = AioBackendShmemSize(), + .ptr = (void **) &AioBackendShmemPtr, + ); + + ShmemRequestStruct(.name = "AioHandle", + .size = AioHandleShmemSize(), + .ptr = (void **) &AioHandleShmemPtr, + ); + + ShmemRequestStruct(.name = "AioHandleIOV", + .size = AioHandleIOVShmemSize(), + .ptr = (void **) &AioHandleIOVShmemPtr, + ); + + ShmemRequestStruct(.name = "AioHandleData", + .size = AioHandleDataShmemSize(), + .ptr = (void **) &AioHandleDataShmemPtr, + ); + + if (pgaio_method_ops->shmem_callbacks.request_fn) + pgaio_method_ops->shmem_callbacks.request_fn(pgaio_method_ops->shmem_callbacks.opaque_arg); } -void -AioShmemInit(void) +/* + * Initialize AIO shared memory during postmaster startup. + */ +static void +AioShmemInit(void *arg) { - bool found; uint32 io_handle_off = 0; uint32 iovec_off = 0; uint32 per_backend_iovecs = io_max_concurrency * io_max_combine_limit; - pgaio_ctl = (PgAioCtl *) - ShmemInitStruct("AioCtl", AioCtlShmemSize(), &found); - - if (found) - goto out; - - memset(pgaio_ctl, 0, AioCtlShmemSize()); - pgaio_ctl->io_handle_count = AioProcs() * io_max_concurrency; pgaio_ctl->iovec_count = AioProcs() * per_backend_iovecs; - pgaio_ctl->backend_state = (PgAioBackend *) - ShmemInitStruct("AioBackend", AioBackendShmemSize(), &found); - - pgaio_ctl->io_handles = (PgAioHandle *) - ShmemInitStruct("AioHandle", AioHandleShmemSize(), &found); - - pgaio_ctl->iovecs = (struct iovec *) - ShmemInitStruct("AioHandleIOV", AioHandleIOVShmemSize(), &found); - pgaio_ctl->handle_data = (uint64 *) - ShmemInitStruct("AioHandleData", AioHandleDataShmemSize(), &found); + pgaio_ctl->backend_state = AioBackendShmemPtr; + pgaio_ctl->io_handles = AioHandleShmemPtr; + pgaio_ctl->iovecs = AioHandleIOVShmemPtr; + pgaio_ctl->handle_data = AioHandleDataShmemPtr; for (int procno = 0; procno < AioProcs(); procno++) { @@ -212,10 +223,15 @@ AioShmemInit(void) } } -out: - /* Initialize IO method specific resources. */ - if (pgaio_method_ops->shmem_init) - pgaio_method_ops->shmem_init(!found); + if (pgaio_method_ops->shmem_callbacks.init_fn) + pgaio_method_ops->shmem_callbacks.init_fn(pgaio_method_ops->shmem_callbacks.opaque_arg); +} + +static void +AioShmemAttach(void *arg) +{ + if (pgaio_method_ops->shmem_callbacks.attach_fn) + pgaio_method_ops->shmem_callbacks.attach_fn(pgaio_method_ops->shmem_callbacks.opaque_arg); } void diff --git a/src/backend/storage/aio/aio_io.c b/src/backend/storage/aio/aio_io.c index 520b5077df25a..72b4c9feb3a69 100644 --- a/src/backend/storage/aio/aio_io.c +++ b/src/backend/storage/aio/aio_io.c @@ -7,7 +7,7 @@ * independent support functions for actually performing IO. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -121,7 +121,7 @@ pgaio_io_perform_synchronously(PgAioHandle *ioh) START_CRIT_SECTION(); /* Perform IO. */ - switch (ioh->op) + switch ((PgAioOp) ioh->op) { case PGAIO_OP_READV: pgstat_report_wait_start(WAIT_EVENT_DATA_FILE_READ); @@ -176,7 +176,7 @@ pgaio_io_get_op_name(PgAioHandle *ioh) { Assert(ioh->op >= 0 && ioh->op < PGAIO_OP_COUNT); - switch (ioh->op) + switch ((PgAioOp) ioh->op) { case PGAIO_OP_INVALID: return "invalid"; @@ -198,7 +198,7 @@ pgaio_io_uses_fd(PgAioHandle *ioh, int fd) { Assert(ioh->state >= PGAIO_HS_DEFINED); - switch (ioh->op) + switch ((PgAioOp) ioh->op) { case PGAIO_OP_READV: return ioh->op_data.read.fd == fd; @@ -222,7 +222,7 @@ pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov) *iov = &pgaio_ctl->iovecs[ioh->iovec_off]; - switch (ioh->op) + switch ((PgAioOp) ioh->op) { case PGAIO_OP_READV: return ioh->op_data.read.iov_length; diff --git a/src/backend/storage/aio/aio_target.c b/src/backend/storage/aio/aio_target.c index 161f0f1edf68c..fa98f010d5ac0 100644 --- a/src/backend/storage/aio/aio_target.c +++ b/src/backend/storage/aio/aio_target.c @@ -3,7 +3,7 @@ * aio_target.c * AIO - Functionality related to executing IO for different targets * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/storage/aio/meson.build b/src/backend/storage/aio/meson.build index da6df2d3654f9..6b8d33c976d82 100644 --- a/src/backend/storage/aio/meson.build +++ b/src/backend/storage/aio/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group backend_sources += files( 'aio.c', diff --git a/src/backend/storage/aio/method_io_uring.c b/src/backend/storage/aio/method_io_uring.c index c719ba2727a81..c0f9fc9c303b1 100644 --- a/src/backend/storage/aio/method_io_uring.c +++ b/src/backend/storage/aio/method_io_uring.c @@ -13,7 +13,7 @@ * We likely will want to introduce a backend-local io_uring instance in the * future, e.g. for FE/BE network IO. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -29,6 +29,9 @@ #ifdef IOMETHOD_IO_URING_ENABLED +#include +#include + #include #include "miscadmin.h" @@ -46,11 +49,12 @@ /* Entry points for IoMethodOps. */ -static size_t pgaio_uring_shmem_size(void); -static void pgaio_uring_shmem_init(bool first_time); +static void pgaio_uring_shmem_request(void *arg); +static void pgaio_uring_shmem_init(void *arg); static void pgaio_uring_init_backend(void); static int pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios); static void pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation); +static void pgaio_uring_check_one(PgAioHandle *ioh, uint64 ref_generation); /* helper functions */ static void pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe); @@ -66,23 +70,26 @@ const IoMethodOps pgaio_uring_ops = { */ .wait_on_fd_before_close = true, - .shmem_size = pgaio_uring_shmem_size, - .shmem_init = pgaio_uring_shmem_init, + .shmem_callbacks.request_fn = pgaio_uring_shmem_request, + .shmem_callbacks.init_fn = pgaio_uring_shmem_init, .init_backend = pgaio_uring_init_backend, .submit = pgaio_uring_submit, .wait_one = pgaio_uring_wait_one, + .check_one = pgaio_uring_check_one, }; /* * Per-backend state when using io_method=io_uring - * - * Align the whole struct to a cacheline boundary, to prevent false sharing - * between completion_lock and prior backend's io_uring_ring. */ -typedef struct pg_attribute_aligned (PG_CACHE_LINE_SIZE) -PgAioUringContext +typedef struct PgAioUringContext { + /* + * Align the whole struct to a cacheline boundary, to prevent false + * sharing between completion_lock and prior backend's io_uring_ring. + */ + alignas(PG_CACHE_LINE_SIZE) + /* * Multiple backends can process completions for this backend's io_uring * instance (e.g. when the backend issuing IO is busy doing something @@ -94,12 +101,32 @@ PgAioUringContext struct io_uring io_uring_ring; } PgAioUringContext; +/* + * Information about the capabilities that io_uring has. + * + * Depending on liburing and kernel version different features are + * supported. At least for the kernel a kernel version check does not suffice + * as various vendors do backport features to older kernels :(. + */ +typedef struct PgAioUringCaps +{ + bool checked; + /* -1 if io_uring_queue_init_mem() is unsupported */ + int mem_init_size; +} PgAioUringCaps; + + /* PgAioUringContexts for all backends */ static PgAioUringContext *pgaio_uring_contexts; /* the current backend's context */ static PgAioUringContext *pgaio_my_uring_context; +static PgAioUringCaps pgaio_uring_caps = +{ + .checked = false, + .mem_init_size = -1, +}; static uint32 pgaio_uring_procs(void) @@ -111,29 +138,189 @@ pgaio_uring_procs(void) return MaxBackends + NUM_AUXILIARY_PROCS - MAX_IO_WORKERS; } -static Size +/* + * Initializes pgaio_uring_caps, unless that's already done. + */ +static void +pgaio_uring_check_capabilities(void) +{ + if (pgaio_uring_caps.checked) + return; + + /* + * By default io_uring creates a shared memory mapping for each io_uring + * instance, leading to a large number of memory mappings. Unfortunately a + * large number of memory mappings slows things down, backend exit is + * particularly affected. To address that, newer kernels (6.5) support + * using user-provided memory for the memory, by putting the relevant + * memory into shared memory we don't need any additional mappings. + * + * To know whether this is supported, we unfortunately need to probe the + * kernel by trying to create a ring with userspace-provided memory. This + * also has a secondary benefit: We can determine precisely how much + * memory we need for each io_uring instance. + */ +#if defined(HAVE_IO_URING_QUEUE_INIT_MEM) && defined(IORING_SETUP_NO_MMAP) + { + struct io_uring test_ring; + size_t ring_size; + void *ring_ptr; + struct io_uring_params p = {0}; + int ret; + + /* + * Liburing does not yet provide an API to query how much memory a + * ring will need. So we over-estimate it here. As the memory is freed + * just below that's small temporary waste of memory. + * + * 1MB is more than enough for rings within io_max_concurrency's + * range. + */ + ring_size = 1024 * 1024; + + /* + * Hard to believe a system exists where 1MB would not be a multiple + * of the page size. But it's cheap to ensure... + */ + ring_size -= ring_size % sysconf(_SC_PAGESIZE); + + ring_ptr = mmap(NULL, ring_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (ring_ptr == MAP_FAILED) + elog(ERROR, + "mmap(%zu) to determine io_uring_queue_init_mem() support failed: %m", + ring_size); + + ret = io_uring_queue_init_mem(io_max_concurrency, &test_ring, &p, ring_ptr, ring_size); + if (ret > 0) + { + pgaio_uring_caps.mem_init_size = ret; + + elog(DEBUG1, + "can use combined memory mapping for io_uring, each ring needs %d bytes", + ret); + + /* clean up the created ring, it was just for a test */ + io_uring_queue_exit(&test_ring); + } + else + { + /* + * There are different reasons for ring creation to fail, but it's + * ok to treat that just as io_uring_queue_init_mem() not being + * supported. We'll report a more detailed error in + * pgaio_uring_shmem_init(). + */ + errno = -ret; + elog(DEBUG1, + "cannot use combined memory mapping for io_uring, ring creation failed: %m"); + + } + + if (munmap(ring_ptr, ring_size) != 0) + elog(ERROR, "munmap() failed: %m"); + } +#else + { + elog(DEBUG1, + "can't use combined memory mapping for io_uring, kernel or liburing too old"); + } +#endif + + pgaio_uring_caps.checked = true; +} + +/* + * Memory for all PgAioUringContext instances + */ +static size_t pgaio_uring_context_shmem_size(void) { return mul_size(pgaio_uring_procs(), sizeof(PgAioUringContext)); } +/* + * Memory for the combined memory used by io_uring instances. Returns 0 if + * that is not supported by kernel/liburing. + */ +static size_t +pgaio_uring_ring_shmem_size(void) +{ + size_t sz = 0; + + if (pgaio_uring_caps.mem_init_size > 0) + { + /* + * Memory for rings needs to be allocated to the page boundary, + * reserve space. Luckily it does not need to be aligned to hugepage + * boundaries, even if huge pages are used. + */ + sz = add_size(sz, sysconf(_SC_PAGESIZE)); + sz = add_size(sz, mul_size(pgaio_uring_procs(), + pgaio_uring_caps.mem_init_size)); + } + + return sz; +} + static size_t pgaio_uring_shmem_size(void) { - return pgaio_uring_context_shmem_size(); + size_t sz; + + sz = pgaio_uring_context_shmem_size(); + sz = add_size(sz, pgaio_uring_ring_shmem_size()); + + return sz; } static void -pgaio_uring_shmem_init(bool first_time) +pgaio_uring_shmem_request(void *arg) { - int TotalProcs = MaxBackends + NUM_AUXILIARY_PROCS - MAX_IO_WORKERS; - bool found; + /* + * Kernel and liburing support for various features influences how much + * shmem we need, perform the necessary checks. + */ + pgaio_uring_check_capabilities(); - pgaio_uring_contexts = (PgAioUringContext *) - ShmemInitStruct("AioUring", pgaio_uring_shmem_size(), &found); + ShmemRequestStruct(.name = "AioUringContext", + .size = pgaio_uring_shmem_size(), + .ptr = (void **) &pgaio_uring_contexts, + ); +} - if (found) - return; +static void +pgaio_uring_shmem_init(void *arg) +{ + int TotalProcs = pgaio_uring_procs(); + char *shmem; + size_t ring_mem_remain = 0; + char *ring_mem_next = 0; + + /* + * We allocate memory for all PgAioUringContext instances and, if + * supported, the memory required for each of the io_uring instances, in + * one combined allocation. + * + * pgaio_uring_contexts is already set to the base of the allocation. + */ + shmem = (char *) pgaio_uring_contexts; + shmem += pgaio_uring_context_shmem_size(); + + /* if supported, handle memory alignment / sizing for io_uring memory */ + if (pgaio_uring_caps.mem_init_size > 0) + { + ring_mem_remain = pgaio_uring_ring_shmem_size(); + ring_mem_next = shmem; + + /* align to page boundary, see also pgaio_uring_ring_shmem_size() */ + ring_mem_next = (char *) TYPEALIGN(sysconf(_SC_PAGESIZE), ring_mem_next); + + /* account for alignment */ + ring_mem_remain -= ring_mem_next - shmem; + shmem += ring_mem_next - shmem; + + shmem += ring_mem_remain; + } for (int contextno = 0; contextno < TotalProcs; contextno++) { @@ -158,7 +345,28 @@ pgaio_uring_shmem_init(bool first_time) * be worth using that - also need to evaluate if that causes * noticeable additional contention? */ - ret = io_uring_queue_init(io_max_concurrency, &context->io_uring_ring, 0); + + /* + * If supported (c.f. pgaio_uring_check_capabilities()), create ring + * with its data in shared memory. Otherwise fall back io_uring + * creating a memory mapping for each ring. + */ +#if defined(HAVE_IO_URING_QUEUE_INIT_MEM) && defined(IORING_SETUP_NO_MMAP) + if (pgaio_uring_caps.mem_init_size > 0) + { + struct io_uring_params p = {0}; + + ret = io_uring_queue_init_mem(io_max_concurrency, &context->io_uring_ring, &p, ring_mem_next, ring_mem_remain); + + ring_mem_remain -= ret; + ring_mem_next += ret; + } + else +#endif + { + ret = io_uring_queue_init(io_max_concurrency, &context->io_uring_ring, 0); + } + if (ret < 0) { char *hint = NULL; @@ -179,7 +387,7 @@ pgaio_uring_shmem_init(bool first_time) else if (-ret == ENOSYS) { err = ERRCODE_FEATURE_NOT_SUPPORTED; - hint = _("Kernel does not support io_uring."); + hint = _("The kernel does not support io_uring."); } /* update errno to allow %m to work */ @@ -207,7 +415,6 @@ static int pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios) { struct io_uring *uring_instance = &pgaio_my_uring_context->io_uring_ring; - int in_flight_before = dclist_count(&pgaio_my_backend->in_flight_ios); Assert(num_staged_ios <= PGAIO_SUBMIT_BATCH_SIZE); @@ -223,27 +430,6 @@ pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios) pgaio_io_prepare_submit(ioh); pgaio_uring_sq_from_io(ioh, sqe); - - /* - * io_uring executes IO in process context if possible. That's - * generally good, as it reduces context switching. When performing a - * lot of buffered IO that means that copying between page cache and - * userspace memory happens in the foreground, as it can't be - * offloaded to DMA hardware as is possible when using direct IO. When - * executing a lot of buffered IO this causes io_uring to be slower - * than worker mode, as worker mode parallelizes the copying. io_uring - * can be told to offload work to worker threads instead. - * - * If an IO is buffered IO and we already have IOs in flight or - * multiple IOs are being submitted, we thus tell io_uring to execute - * the IO in the background. We don't do so for the first few IOs - * being submitted as executing in this process' context has lower - * latency. - */ - if (in_flight_before > 4 && (ioh->flags & PGAIO_HF_BUFFERED)) - io_uring_sqe_set_flags(sqe, IOSQE_ASYNC); - - in_flight_before++; } while (true) @@ -361,13 +547,14 @@ pgaio_uring_drain_locked(PgAioUringContext *context) for (int i = 0; i < ncqes; i++) { struct io_uring_cqe *cqe = cqes[i]; - PgAioHandle *ioh; + PgAioHandle *ioh = io_uring_cqe_get_data(cqe); + int result = cqe->res; - ioh = io_uring_cqe_get_data(cqe); errcallback.arg = ioh; + io_uring_cqe_seen(&context->io_uring_ring, cqe); - pgaio_io_process_completion(ioh, cqe->res); + pgaio_io_process_completion(ioh, result); errcallback.arg = NULL; } @@ -400,9 +587,9 @@ pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation) while (true) { pgaio_debug_io(DEBUG3, ioh, - "wait_one io_gen: %llu, ref_gen: %llu, cycle %d", - (long long unsigned) ioh->generation, - (long long unsigned) ref_generation, + "wait_one io_gen: %" PRIu64 ", ref_gen: %" PRIu64 ", cycle %d", + ioh->generation, + ref_generation, waited); if (pgaio_io_was_recycled(ioh, ref_generation, &state) || @@ -457,12 +644,108 @@ pgaio_uring_wait_one(PgAioHandle *ioh, uint64 ref_generation) waited); } +static void +pgaio_uring_check_one(PgAioHandle *ioh, uint64 ref_generation) +{ + ProcNumber owner_procno = ioh->owner_procno; + PgAioUringContext *owner_context = &pgaio_uring_contexts[owner_procno]; + + /* + * This check is not reliable when not holding the completion lock, but + * it's a useful cheap pre-check to see if it's worth trying to get the + * completion lock. + */ + if (!io_uring_cq_ready(&owner_context->io_uring_ring)) + return; + + /* + * If the completion lock is currently held, the holder will likely + * process any pending completions, give up. + */ + if (!LWLockConditionalAcquire(&owner_context->completion_lock, LW_EXCLUSIVE)) + return; + + pgaio_debug_io(DEBUG3, ioh, + "check_one io_gen: %" PRIu64 ", ref_gen: %" PRIu64, + ioh->generation, + ref_generation); + + /* + * Recheck if there are any completions, another backend could have + * processed them since we checked above, or our unlocked pre-check could + * have been reading outdated values. + * + * It is possible that the IO handle has been reused since the start of + * the call, but now that we have the lock, we can just as well drain all + * completions. + */ + if (io_uring_cq_ready(&owner_context->io_uring_ring)) + pgaio_uring_drain_locked(owner_context); + + LWLockRelease(&owner_context->completion_lock); +} + +/* + * io_uring executes IO in process context if possible. That's generally good, + * as it reduces context switching. When performing a lot of buffered IO that + * means that copying between page cache and userspace memory happens in the + * foreground, as it can't be offloaded to DMA hardware as is possible when + * using direct IO. When executing a lot of buffered IO this causes io_uring + * to be slower than worker mode, as worker mode parallelizes the + * copying. io_uring can be told to offload work to worker threads instead. + * + * If the IOs are small, we only benefit from forcing things into the + * background if there is a lot of IO, as otherwise the overhead from context + * switching is higher than the gain. + * + * If IOs are large, there is benefit from asynchronous processing at lower + * queue depths, as IO latency is less of a crucial factor and parallelizing + * memory copies is more important. In addition, it is important to trigger + * asynchronous processing even at low queue depth, as with foreground + * processing we might never actually reach deep enough IO depths to trigger + * asynchronous processing, which in turn would deprive readahead control + * logic of information about whether a deeper look-ahead distance would be + * advantageous. + * + * We have done some basic benchmarking to validate the thresholds used, but + * it's quite plausible that there are better values. See + * https://postgr.es/m/3gkuvs3lz3u3skuaxfkxnsysfqslf2srigl6546vhesekve6v2%40va3r5esummvg + * for some details of this benchmarking. + */ +static bool +pgaio_uring_should_use_async(PgAioHandle *ioh, size_t io_size) +{ + /* + * With DIO there's no benefit from forcing asynchronous processing, as + * io_uring will never execute direct IO synchronously during submission. + */ + if (!(ioh->flags & PGAIO_HF_BUFFERED)) + return false; + + /* + * Once the IO queue depth is not that shallow anymore, the overhead of + * dispatching to the background is a less significant factor. + */ + if (dclist_count(&pgaio_my_backend->in_flight_ios) > 4) + return true; + + /* + * If the IO is larger, the gains from parallelizing the memory copy are + * larger and typically the impact of the latency is smaller. + */ + if (io_size >= (BLCKSZ * 4)) + return true; + + return false; +} + static void pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe) { struct iovec *iov; + size_t io_size = 0; - switch (ioh->op) + switch ((PgAioOp) ioh->op) { case PGAIO_OP_READV: iov = &pgaio_ctl->iovecs[ioh->iovec_off]; @@ -473,6 +756,8 @@ pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe) iov->iov_base, iov->iov_len, ioh->op_data.read.offset); + + io_size = iov->iov_len; } else { @@ -482,7 +767,13 @@ pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe) ioh->op_data.read.iov_length, ioh->op_data.read.offset); + for (int i = 0; i < ioh->op_data.read.iov_length; i++, iov++) + io_size += iov->iov_len; } + + if (pgaio_uring_should_use_async(ioh, io_size)) + io_uring_sqe_set_flags(sqe, IOSQE_ASYNC); + break; case PGAIO_OP_WRITEV: @@ -503,6 +794,12 @@ pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe) ioh->op_data.write.iov_length, ioh->op_data.write.offset); } + + /* + * For now don't trigger use of IOSQE_ASYNC for writes, it's not + * clear there is a performance benefit in doing so. + */ + break; case PGAIO_OP_INVALID: diff --git a/src/backend/storage/aio/method_sync.c b/src/backend/storage/aio/method_sync.c index 902c2428d4158..6e7447b48daff 100644 --- a/src/backend/storage/aio/method_sync.c +++ b/src/backend/storage/aio/method_sync.c @@ -7,7 +7,7 @@ * methods might also fall back to the synchronous method for functionality * they cannot provide. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/storage/aio/method_worker.c b/src/backend/storage/aio/method_worker.c index 743cccc2acd18..63e34d66690d4 100644 --- a/src/backend/storage/aio/method_worker.c +++ b/src/backend/storage/aio/method_worker.c @@ -11,14 +11,13 @@ * infrastructure for reopening the file, and must processed synchronously by * the client code when submitted. * - * So that the submitter can make just one system call when submitting a batch - * of IOs, wakeups "fan out"; each woken IO worker can wake two more. XXX This - * could be improved by using futexes instead of latches to wake N waiters. + * The pool of workers tries to stabilize at a size that can handle recently + * seen variation in demand, within the configured limits. * * This method of AIO is available in all builds on all operating systems, and * is the default. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -29,6 +28,8 @@ #include "postgres.h" +#include + #include "libpq/pqsignal.h" #include "miscadmin.h" #include "port/pg_bitutils.h" @@ -40,50 +41,86 @@ #include "storage/io_worker.h" #include "storage/ipc.h" #include "storage/latch.h" +#include "storage/lwlock.h" +#include "storage/pmsignal.h" #include "storage/proc.h" +#include "storage/shmem.h" #include "tcop/tcopprot.h" #include "utils/injection_point.h" #include "utils/memdebug.h" #include "utils/ps_status.h" #include "utils/wait_event.h" +/* + * Saturation for counters used to estimate wakeup:IO ratio. + * + * We maintain hist_wakeups for wakeups received and hist_ios for IOs + * processed by each worker. When either counter reaches this saturation + * value, we divide both by two. The result is an exponentially decaying + * ratio of wakeups to IOs, with a very short memory. + * + * If a worker is itself experiencing useless wakeups, it assumes that + * higher-numbered workers would experience even more, so it should end the + * chain. + */ +#define PGAIO_WORKER_WAKEUP_RATIO_SATURATE 4 -/* How many workers should each worker wake up if needed? */ -#define IO_WORKER_WAKEUP_FANOUT 2 - +/* Debugging support: show current IO and wakeups:ios statistics in ps. */ +/* #define PGAIO_WORKER_SHOW_PS_INFO */ -typedef struct AioWorkerSubmissionQueue +typedef struct PgAioWorkerSubmissionQueue { uint32 size; - uint32 mask; uint32 head; uint32 tail; - uint32 ios[FLEXIBLE_ARRAY_MEMBER]; -} AioWorkerSubmissionQueue; + int sqes[FLEXIBLE_ARRAY_MEMBER]; +} PgAioWorkerSubmissionQueue; -typedef struct AioWorkerSlot +typedef struct PgAioWorkerSlot { - Latch *latch; - bool in_use; -} AioWorkerSlot; + ProcNumber proc_number; +} PgAioWorkerSlot; + +/* + * Sets of worker IDs are held in a simple bitmap, accessed through functions + * that provide a more readable abstraction. If we wanted to support more + * workers than that, the contention on the single queue would surely get too + * high, so we might want to consider multiple pools instead of widening this. + */ +typedef uint64 PgAioWorkerSet; + +#define PGAIO_WORKERSET_BITS (sizeof(PgAioWorkerSet) * CHAR_BIT) -typedef struct AioWorkerControl +static_assert(PGAIO_WORKERSET_BITS >= MAX_IO_WORKERS, "too small"); + +typedef struct PgAioWorkerControl { - uint64 idle_worker_mask; - AioWorkerSlot workers[FLEXIBLE_ARRAY_MEMBER]; -} AioWorkerControl; + /* Seen by postmaster */ + bool grow; + bool grow_signal_sent; + + /* Protected by AioWorkerSubmissionQueueLock. */ + PgAioWorkerSet idle_workerset; + /* Protected by AioWorkerControlLock. */ + PgAioWorkerSet workerset; + int nworkers; -static size_t pgaio_worker_shmem_size(void); -static void pgaio_worker_shmem_init(bool first_time); + /* Protected by AioWorkerControlLock. */ + PgAioWorkerSlot workers[FLEXIBLE_ARRAY_MEMBER]; +} PgAioWorkerControl; + + +static void pgaio_worker_shmem_request(void *arg); +static void pgaio_worker_shmem_init(void *arg); static bool pgaio_worker_needs_synchronous_execution(PgAioHandle *ioh); static int pgaio_worker_submit(uint16 num_staged_ios, PgAioHandle **staged_ios); const IoMethodOps pgaio_worker_ops = { - .shmem_size = pgaio_worker_shmem_size, - .shmem_init = pgaio_worker_shmem_init, + .shmem_callbacks.request_fn = pgaio_worker_shmem_request, + .shmem_callbacks.init_fn = pgaio_worker_shmem_init, .needs_synchronous_execution = pgaio_worker_needs_synchronous_execution, .submit = pgaio_worker_submit, @@ -91,97 +128,295 @@ const IoMethodOps pgaio_worker_ops = { /* GUCs */ -int io_workers = 3; +int io_min_workers = 2; +int io_max_workers = 8; +int io_worker_idle_timeout = 60000; +int io_worker_launch_interval = 100; static int io_worker_queue_size = 64; -static int MyIoWorkerId; -static AioWorkerSubmissionQueue *io_worker_submission_queue; -static AioWorkerControl *io_worker_control; +static int MyIoWorkerId = -1; +static PgAioWorkerSubmissionQueue *io_worker_submission_queue; +static PgAioWorkerControl *io_worker_control; -static size_t -pgaio_worker_queue_shmem_size(int *queue_size) +static void +pgaio_workerset_initialize(PgAioWorkerSet *set) { - /* Round size up to next power of two so we can make a mask. */ - *queue_size = pg_nextpower2_32(io_worker_queue_size); + *set = 0; +} - return offsetof(AioWorkerSubmissionQueue, ios) + - sizeof(uint32) * *queue_size; +static bool +pgaio_workerset_is_empty(PgAioWorkerSet *set) +{ + return *set == 0; } -static size_t -pgaio_worker_control_shmem_size(void) +static PgAioWorkerSet +pgaio_workerset_singleton(int worker) { - return offsetof(AioWorkerControl, workers) + - sizeof(AioWorkerSlot) * MAX_IO_WORKERS; + Assert(worker >= 0 && worker < MAX_IO_WORKERS); + return UINT64_C(1) << worker; } -static size_t -pgaio_worker_shmem_size(void) +static void +pgaio_workerset_all(PgAioWorkerSet *set) { - size_t sz; - int queue_size; + *set = UINT64_MAX >> (PGAIO_WORKERSET_BITS - MAX_IO_WORKERS); +} + +static void +pgaio_workerset_subtract(PgAioWorkerSet *set1, const PgAioWorkerSet *set2) +{ + *set1 &= ~*set2; +} + +static void +pgaio_workerset_insert(PgAioWorkerSet *set, int worker) +{ + Assert(worker >= 0 && worker < MAX_IO_WORKERS); + *set |= pgaio_workerset_singleton(worker); +} + +static void +pgaio_workerset_remove(PgAioWorkerSet *set, int worker) +{ + Assert(worker >= 0 && worker < MAX_IO_WORKERS); + *set &= ~pgaio_workerset_singleton(worker); +} + +static void +pgaio_workerset_remove_lte(PgAioWorkerSet *set, int worker) +{ + Assert(worker >= 0 && worker < MAX_IO_WORKERS); + *set &= (~(PgAioWorkerSet) 0) << (worker + 1); +} - sz = pgaio_worker_queue_shmem_size(&queue_size); - sz = add_size(sz, pgaio_worker_control_shmem_size()); +static int +pgaio_workerset_get_highest(PgAioWorkerSet *set) +{ + Assert(!pgaio_workerset_is_empty(set)); + return pg_leftmost_one_pos64(*set); +} - return sz; +static int +pgaio_workerset_get_lowest(PgAioWorkerSet *set) +{ + Assert(!pgaio_workerset_is_empty(set)); + return pg_rightmost_one_pos64(*set); } +static int +pgaio_workerset_pop_lowest(PgAioWorkerSet *set) +{ + int worker = pgaio_workerset_get_lowest(set); + + pgaio_workerset_remove(set, worker); + return worker; +} + +#ifdef USE_ASSERT_CHECKING +static bool +pgaio_workerset_contains(PgAioWorkerSet *set, int worker) +{ + Assert(worker >= 0 && worker < MAX_IO_WORKERS); + return (*set & pgaio_workerset_singleton(worker)) != 0; +} + +static int +pgaio_workerset_count(PgAioWorkerSet *set) +{ + return pg_popcount64(*set); +} +#endif + static void -pgaio_worker_shmem_init(bool first_time) +pgaio_worker_shmem_request(void *arg) { - bool found; + size_t size; int queue_size; - io_worker_submission_queue = - ShmemInitStruct("AioWorkerSubmissionQueue", - pgaio_worker_queue_shmem_size(&queue_size), - &found); - if (!found) - { - io_worker_submission_queue->size = queue_size; - io_worker_submission_queue->head = 0; - io_worker_submission_queue->tail = 0; - } + /* Round size up to next power of two so we can make a mask. */ + queue_size = pg_nextpower2_32(io_worker_queue_size); + + size = offsetof(PgAioWorkerSubmissionQueue, sqes) + sizeof(int) * queue_size; + ShmemRequestStruct(.name = "AioWorkerSubmissionQueue", + .size = size, + .ptr = (void **) &io_worker_submission_queue, + ); + + size = offsetof(PgAioWorkerControl, workers) + sizeof(PgAioWorkerSlot) * MAX_IO_WORKERS; + ShmemRequestStruct(.name = "AioWorkerControl", + .size = size, + .ptr = (void **) &io_worker_control, + ); +} - io_worker_control = - ShmemInitStruct("AioWorkerControl", - pgaio_worker_control_shmem_size(), - &found); - if (!found) - { - io_worker_control->idle_worker_mask = 0; - for (int i = 0; i < MAX_IO_WORKERS; ++i) - { - io_worker_control->workers[i].latch = NULL; - io_worker_control->workers[i].in_use = false; - } - } +static void +pgaio_worker_shmem_init(void *arg) +{ + int queue_size; + + /* Round size up like in pgaio_worker_shmem_request() */ + queue_size = pg_nextpower2_32(io_worker_queue_size); + + io_worker_submission_queue->size = queue_size; + io_worker_submission_queue->head = 0; + io_worker_submission_queue->tail = 0; + io_worker_control->grow = false; + pgaio_workerset_initialize(&io_worker_control->workerset); + pgaio_workerset_initialize(&io_worker_control->idle_workerset); + + for (int i = 0; i < MAX_IO_WORKERS; ++i) + io_worker_control->workers[i].proc_number = INVALID_PROC_NUMBER; +} + +/* + * Tell postmaster that we think a new worker is needed. + */ +static void +pgaio_worker_request_grow(void) +{ + /* + * Suppress useless signaling if we already know that we're at the + * maximum. This uses an unlocked read of nworkers, but that's OK for + * this heuristic purpose. + */ + if (io_worker_control->nworkers >= io_max_workers) + return; + + /* Already requested? */ + if (io_worker_control->grow) + return; + + io_worker_control->grow = true; + pg_memory_barrier(); + + /* + * If the postmaster has already been signaled, don't do it again until + * the postmaster clears this flag. There is no point in repeated signals + * if grow is being set and cleared repeatedly while the postmaster is + * waiting for io_worker_launch_interval, which it applies even to + * canceled requests. + */ + if (io_worker_control->grow_signal_sent) + return; + + io_worker_control->grow_signal_sent = true; + pg_memory_barrier(); + SendPostmasterSignal(PMSIGNAL_IO_WORKER_GROW); +} + +/* + * Cancel any request for a new worker, after observing an empty queue. + */ +static void +pgaio_worker_cancel_grow(void) +{ + if (!io_worker_control->grow) + return; + + io_worker_control->grow = false; + pg_memory_barrier(); +} + +/* + * Called by the postmaster to check if a new worker has been requested (but + * possibly canceled since). + */ +bool +pgaio_worker_pm_test_grow_signal_sent(void) +{ + pg_memory_barrier(); + return io_worker_control && io_worker_control->grow_signal_sent; +} + +/* + * Called by the postmaster to check if a new worker has been requested and + * not canceled since. + */ +bool +pgaio_worker_pm_test_grow(void) +{ + pg_memory_barrier(); + return io_worker_control && io_worker_control->grow; +} + +/* + * Called by the postmaster to clear the request for a new worker. + */ +void +pgaio_worker_pm_clear_grow_signal_sent(void) +{ + if (!io_worker_control) + return; + + io_worker_control->grow = false; + io_worker_control->grow_signal_sent = false; + pg_memory_barrier(); } static int -pgaio_choose_idle_worker(void) +pgaio_worker_choose_idle(int only_workers_above) { + PgAioWorkerSet workerset; int worker; - if (io_worker_control->idle_worker_mask == 0) + Assert(LWLockHeldByMeInMode(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE)); + + workerset = io_worker_control->idle_workerset; + if (only_workers_above >= 0) + pgaio_workerset_remove_lte(&workerset, only_workers_above); + if (pgaio_workerset_is_empty(&workerset)) return -1; - /* Find the lowest bit position, and clear it. */ - worker = pg_rightmost_one_pos64(io_worker_control->idle_worker_mask); - io_worker_control->idle_worker_mask &= ~(UINT64_C(1) << worker); + /* Find the lowest numbered idle worker and mark it not idle. */ + worker = pgaio_workerset_get_lowest(&workerset); + pgaio_workerset_remove(&io_worker_control->idle_workerset, worker); return worker; } +/* + * Try to wake a worker by setting its latch, to tell it there are IOs to + * process in the submission queue. + */ +static void +pgaio_worker_wake(int worker) +{ + ProcNumber proc_number; + + /* + * If the selected worker is concurrently exiting, then pgaio_worker_die() + * had not yet removed it as of when we saw it in idle_workerset. That's + * OK, because it will wake all remaining workers to close wakeup-vs-exit + * races: *someone* will see the queued IO. If there are no workers + * running, the postmaster will start a new one. + */ + proc_number = io_worker_control->workers[worker].proc_number; + if (proc_number != INVALID_PROC_NUMBER) + SetLatch(&GetPGProcByNumber(proc_number)->procLatch); +} + +/* + * Try to wake a set of workers. Used on pool change, to close races + * described in the callers. + */ +static void +pgaio_workerset_wake(PgAioWorkerSet workerset) +{ + while (!pgaio_workerset_is_empty(&workerset)) + pgaio_worker_wake(pgaio_workerset_pop_lowest(&workerset)); +} + static bool pgaio_worker_submission_queue_insert(PgAioHandle *ioh) { - AioWorkerSubmissionQueue *queue; + PgAioWorkerSubmissionQueue *queue; uint32 new_head; + Assert(LWLockHeldByMeInMode(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE)); + queue = io_worker_submission_queue; new_head = (queue->head + 1) & (queue->size - 1); if (new_head == queue->tail) @@ -191,23 +426,25 @@ pgaio_worker_submission_queue_insert(PgAioHandle *ioh) return false; /* full */ } - queue->ios[queue->head] = pgaio_io_get_id(ioh); + queue->sqes[queue->head] = pgaio_io_get_id(ioh); queue->head = new_head; return true; } -static uint32 +static int pgaio_worker_submission_queue_consume(void) { - AioWorkerSubmissionQueue *queue; - uint32 result; + PgAioWorkerSubmissionQueue *queue; + int result; + + Assert(LWLockHeldByMeInMode(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE)); queue = io_worker_submission_queue; if (queue->tail == queue->head) - return UINT32_MAX; /* empty */ + return -1; /* empty */ - result = queue->ios[queue->tail]; + result = queue->sqes[queue->tail]; queue->tail = (queue->tail + 1) & (queue->size - 1); return result; @@ -219,6 +456,8 @@ pgaio_worker_submission_queue_depth(void) uint32 head; uint32 tail; + Assert(LWLockHeldByMeInMode(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE)); + head = io_worker_submission_queue->head; tail = io_worker_submission_queue->tail; @@ -239,46 +478,50 @@ pgaio_worker_needs_synchronous_execution(PgAioHandle *ioh) || !pgaio_io_can_reopen(ioh); } -static void -pgaio_worker_submit_internal(int nios, PgAioHandle *ios[]) +static int +pgaio_worker_submit(uint16 num_staged_ios, PgAioHandle **staged_ios) { - PgAioHandle *synchronous_ios[PGAIO_SUBMIT_BATCH_SIZE]; + PgAioHandle **synchronous_ios = NULL; int nsync = 0; - Latch *wakeup = NULL; - int worker; + int worker = -1; - Assert(nios <= PGAIO_SUBMIT_BATCH_SIZE); + Assert(num_staged_ios <= PGAIO_SUBMIT_BATCH_SIZE); - LWLockAcquire(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE); - for (int i = 0; i < nios; ++i) + for (int i = 0; i < num_staged_ios; i++) + pgaio_io_prepare_submit(staged_ios[i]); + + if (LWLockConditionalAcquire(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE)) { - Assert(!pgaio_worker_needs_synchronous_execution(ios[i])); - if (!pgaio_worker_submission_queue_insert(ios[i])) + for (int i = 0; i < num_staged_ios; ++i) { - /* - * We'll do it synchronously, but only after we've sent as many as - * we can to workers, to maximize concurrency. - */ - synchronous_ios[nsync++] = ios[i]; - continue; + Assert(!pgaio_worker_needs_synchronous_execution(staged_ios[i])); + if (!pgaio_worker_submission_queue_insert(staged_ios[i])) + { + /* + * Do the rest synchronously. If the queue is full, give up + * and do the rest synchronously. We're holding an exclusive + * lock on the queue so nothing can consume entries. + */ + synchronous_ios = &staged_ios[i]; + nsync = (num_staged_ios - i); + + break; + } } + /* Choose one worker to wake for this batch. */ + worker = pgaio_worker_choose_idle(-1); + LWLockRelease(AioWorkerSubmissionQueueLock); - if (wakeup == NULL) - { - /* Choose an idle worker to wake up if we haven't already. */ - worker = pgaio_choose_idle_worker(); - if (worker >= 0) - wakeup = io_worker_control->workers[worker].latch; - - pgaio_debug_io(DEBUG4, ios[i], - "choosing worker %d", - worker); - } + /* Wake up chosen worker. It will wake peers if necessary. */ + if (worker != -1) + pgaio_worker_wake(worker); + } + else + { + /* do everything synchronously, no wakeup needed */ + synchronous_ios = staged_ios; + nsync = num_staged_ios; } - LWLockRelease(AioWorkerSubmissionQueueLock); - - if (wakeup) - SetLatch(wakeup); /* Run whatever is left synchronously. */ if (nsync > 0) @@ -288,19 +531,6 @@ pgaio_worker_submit_internal(int nios, PgAioHandle *ios[]) pgaio_io_perform_synchronously(synchronous_ios[i]); } } -} - -static int -pgaio_worker_submit(uint16 num_staged_ios, PgAioHandle **staged_ios) -{ - for (int i = 0; i < num_staged_ios; i++) - { - PgAioHandle *ioh = staged_ios[i]; - - pgaio_io_prepare_submit(ioh); - } - - pgaio_worker_submit_internal(num_staged_ios, staged_ios); return num_staged_ios; } @@ -312,13 +542,30 @@ pgaio_worker_submit(uint16 num_staged_ios, PgAioHandle **staged_ios) static void pgaio_worker_die(int code, Datum arg) { - LWLockAcquire(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE); - Assert(io_worker_control->workers[MyIoWorkerId].in_use); - Assert(io_worker_control->workers[MyIoWorkerId].latch == MyLatch); + PgAioWorkerSet notify_set; - io_worker_control->workers[MyIoWorkerId].in_use = false; - io_worker_control->workers[MyIoWorkerId].latch = NULL; + LWLockAcquire(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE); + pgaio_workerset_remove(&io_worker_control->idle_workerset, MyIoWorkerId); LWLockRelease(AioWorkerSubmissionQueueLock); + + LWLockAcquire(AioWorkerControlLock, LW_EXCLUSIVE); + Assert(io_worker_control->workers[MyIoWorkerId].proc_number == MyProcNumber); + io_worker_control->workers[MyIoWorkerId].proc_number = INVALID_PROC_NUMBER; + Assert(pgaio_workerset_contains(&io_worker_control->workerset, MyIoWorkerId)); + pgaio_workerset_remove(&io_worker_control->workerset, MyIoWorkerId); + notify_set = io_worker_control->workerset; + Assert(io_worker_control->nworkers > 0); + io_worker_control->nworkers--; + Assert(pgaio_workerset_count(&io_worker_control->workerset) == + io_worker_control->nworkers); + LWLockRelease(AioWorkerControlLock); + + /* + * Notify other workers on pool change. This allows the new highest + * worker to know that it is now the one that can time out, and closes a + * wakeup-loss race described in pgaio_worker_wake(). + */ + pgaio_workerset_wake(notify_set); } /* @@ -328,33 +575,38 @@ pgaio_worker_die(int code, Datum arg) static void pgaio_worker_register(void) { + PgAioWorkerSet free_workerset; + PgAioWorkerSet old_workerset; + MyIoWorkerId = -1; - /* - * XXX: This could do with more fine-grained locking. But it's also not - * very common for the number of workers to change at the moment... - */ - LWLockAcquire(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE); + LWLockAcquire(AioWorkerControlLock, LW_EXCLUSIVE); + /* Find lowest unused worker ID. */ + pgaio_workerset_all(&free_workerset); + pgaio_workerset_subtract(&free_workerset, &io_worker_control->workerset); + if (!pgaio_workerset_is_empty(&free_workerset)) + MyIoWorkerId = pgaio_workerset_get_lowest(&free_workerset); + if (MyIoWorkerId == -1) + elog(ERROR, "couldn't find a free worker ID"); - for (int i = 0; i < MAX_IO_WORKERS; ++i) - { - if (!io_worker_control->workers[i].in_use) - { - Assert(io_worker_control->workers[i].latch == NULL); - io_worker_control->workers[i].in_use = true; - MyIoWorkerId = i; - break; - } - else - Assert(io_worker_control->workers[i].latch != NULL); - } + Assert(io_worker_control->workers[MyIoWorkerId].proc_number == + INVALID_PROC_NUMBER); + io_worker_control->workers[MyIoWorkerId].proc_number = MyProcNumber; - if (MyIoWorkerId == -1) - elog(ERROR, "couldn't find a free worker slot"); + old_workerset = io_worker_control->workerset; + Assert(!pgaio_workerset_contains(&old_workerset, MyIoWorkerId)); + pgaio_workerset_insert(&io_worker_control->workerset, MyIoWorkerId); + io_worker_control->nworkers++; + Assert(io_worker_control->nworkers <= MAX_IO_WORKERS); + Assert(pgaio_workerset_count(&io_worker_control->workerset) == + io_worker_control->nworkers); + LWLockRelease(AioWorkerControlLock); - io_worker_control->idle_worker_mask |= (UINT64_C(1) << MyIoWorkerId); - io_worker_control->workers[MyIoWorkerId].latch = MyLatch; - LWLockRelease(AioWorkerSubmissionQueueLock); + /* + * Notify other workers on pool change. If we were the highest worker, + * this allows the new highest worker to know that it can time out. + */ + pgaio_workerset_wake(old_workerset); on_shmem_exit(pgaio_worker_die, 0); } @@ -380,16 +632,49 @@ pgaio_worker_error_callback(void *arg) errcontext("I/O worker executing I/O on behalf of process %d", owner_pid); } +/* + * Check if this backend is allowed to time out, and thus should use a + * non-infinite sleep time. Only the highest-numbered worker is allowed to + * time out, and only if the pool is above io_min_workers. Serializing + * timeouts keeps IDs in a range 0..N without gaps, and avoids undershooting + * io_min_workers. + * + * The result is only instantaneously true and may be temporarily inconsistent + * in different workers around transitions, but all workers are woken up on + * pool size or GUC changes making the result eventually consistent. + */ +static bool +pgaio_worker_can_timeout(void) +{ + PgAioWorkerSet workerset; + + if (MyIoWorkerId < io_min_workers) + return false; + + /* Serialize against pool size changes. */ + LWLockAcquire(AioWorkerControlLock, LW_SHARED); + workerset = io_worker_control->workerset; + LWLockRelease(AioWorkerControlLock); + + if (MyIoWorkerId != pgaio_workerset_get_highest(&workerset)) + return false; + + return true; +} + void IoWorkerMain(const void *startup_data, size_t startup_data_len) { sigjmp_buf local_sigjmp_buf; + TimestampTz idle_timeout_abs = 0; + int timeout_guc_used = 0; PgAioHandle *volatile error_ioh = NULL; ErrorContextCallback errcallback = {0}; volatile int error_errno = 0; char cmd[128]; + int hist_ios = 0; + int hist_wakeups = 0; - MyBackendType = B_IO_WORKER; AuxiliaryProcessMainCommon(); pqsignal(SIGHUP, SignalHandlerForConfigReload); @@ -399,10 +684,10 @@ IoWorkerMain(const void *startup_data, size_t startup_data_len) * Ignore SIGTERM, will get explicit shutdown via SIGUSR2 later in the * shutdown sequence, similar to checkpointer. */ - pqsignal(SIGTERM, SIG_IGN); + pqsignal(SIGTERM, PG_SIG_IGN); /* SIGQUIT handler was already set up by InitPostmasterChild */ - pqsignal(SIGALRM, SIG_IGN); - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGALRM, PG_SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); pqsignal(SIGUSR2, SignalHandlerForShutdownRequest); @@ -456,47 +741,121 @@ IoWorkerMain(const void *startup_data, size_t startup_data_len) while (!ShutdownRequestPending) { uint32 io_index; - Latch *latches[IO_WORKER_WAKEUP_FANOUT]; - int nlatches = 0; - int nwakeups = 0; - int worker; + int worker = -1; + int queue_depth = 0; + bool maybe_grow = false; - /* Try to get a job to do. */ + /* + * Try to get a job to do. + * + * The lwlock acquisition also provides the necessary memory barrier + * to ensure that we don't see an outdated data in the handle. + */ LWLockAcquire(AioWorkerSubmissionQueueLock, LW_EXCLUSIVE); - if ((io_index = pgaio_worker_submission_queue_consume()) == UINT32_MAX) + if ((io_index = pgaio_worker_submission_queue_consume()) == -1) { - /* - * Nothing to do. Mark self idle. - * - * XXX: Invent some kind of back pressure to reduce useless - * wakeups? - */ - io_worker_control->idle_worker_mask |= (UINT64_C(1) << MyIoWorkerId); + /* Nothing to do. Mark self idle. */ + pgaio_workerset_insert(&io_worker_control->idle_workerset, + MyIoWorkerId); } else { /* Got one. Clear idle flag. */ - io_worker_control->idle_worker_mask &= ~(UINT64_C(1) << MyIoWorkerId); + pgaio_workerset_remove(&io_worker_control->idle_workerset, + MyIoWorkerId); - /* See if we can wake up some peers. */ - nwakeups = Min(pgaio_worker_submission_queue_depth(), - IO_WORKER_WAKEUP_FANOUT); - for (int i = 0; i < nwakeups; ++i) + /* + * See if we should wake up a higher numbered peer. Only do that + * if this worker is not receiving spurious wakeups itself. The + * intention is create a frontier beyond which idle workers stay + * asleep. + * + * This heuristic tries to discover the useful wakeup propagation + * chain length when IOs are very fast and workers wake up to find + * that all IOs have already been taken. + * + * If we chose not to wake a worker when we ideally should have, + * then the ratio will soon change to correct that. + */ + if (hist_wakeups <= hist_ios) { - if ((worker = pgaio_choose_idle_worker()) < 0) - break; - latches[nlatches++] = io_worker_control->workers[worker].latch; + queue_depth = pgaio_worker_submission_queue_depth(); + if (queue_depth > 0) + { + /* Choose a worker higher than me to wake. */ + worker = pgaio_worker_choose_idle(MyIoWorkerId); + if (worker == -1) + maybe_grow = true; + } } } LWLockRelease(AioWorkerSubmissionQueueLock); - for (int i = 0; i < nlatches; ++i) - SetLatch(latches[i]); + /* Propagate wakeups. */ + if (worker != -1) + { + pgaio_worker_wake(worker); + } + else if (maybe_grow) + { + /* + * We know there was at least one more item in the queue, and we + * failed to find a higher-numbered idle worker to wake. Now we + * decide if we should try to start one more worker. + * + * We do this with a simple heuristic: is the queue depth greater + * than the current number of workers? + * + * Consider the following situations: + * + * 1. The queue depth is constantly increasing, because IOs are + * arriving faster than they can possibly be serviced. It doesn't + * matter much which threshold we choose, as we will surely hit + * it. Crossing the current worker count is a useful signal + * because it's clearly too deep to avoid queuing latency already, + * but still leaves a small window of opportunity to improve the + * situation before the queue overflows. + * + * 2. The worker pool is keeping up, no latency is being + * introduced and an extra worker would be a waste of resources. + * Queue depth distributions tend to be heavily skewed, with long + * tails of low probability spikes (due to submission clustering, + * scheduling, jitter, stalls, noisy neighbors, etc). We want a + * number that is very unlikely to be triggered by an outlier, and + * we bet that an exponential or similar distribution whose + * outliers never reach this threshold must be almost entirely + * concentrated at the low end. If we do see a spike as big as + * the worker count, we take it as a signal that the distribution + * is surely too wide. + * + * On its own, this is an extremely crude signal. When combined + * with the wakeup propagation test that precedes it (but on its + * own tends to overshoot) and io_worker_launch_interval, the + * result is that we gradually test each pool size until we find + * one that doesn't trigger further expansion, and then hold it + * for at least io_worker_idle_timeout. + * + * XXX Perhaps ideas from queueing theory or control theory could + * do a better job of this. + */ - if (io_index != UINT32_MAX) + /* Read nworkers without lock for this heuristic purpose. */ + if (queue_depth > io_worker_control->nworkers) + pgaio_worker_request_grow(); + } + + if (io_index != -1) { PgAioHandle *ioh = NULL; + /* Cancel timeout and update wakeup:work ratio. */ + idle_timeout_abs = 0; + if (++hist_ios == PGAIO_WORKER_WAKEUP_RATIO_SATURATE) + { + hist_wakeups /= 2; + hist_ios /= 2; + } + ioh = &pgaio_ctl->io_handles[io_index]; error_ioh = ioh; errcallback.arg = ioh; @@ -549,6 +908,19 @@ IoWorkerMain(const void *startup_data, size_t startup_data_len) } #endif +#ifdef PGAIO_WORKER_SHOW_PS_INFO + { + char *description = pgaio_io_get_target_description(ioh); + + sprintf(cmd, "%d: [%s] %s", + MyIoWorkerId, + pgaio_io_get_op_name(ioh), + description); + pfree(description); + set_ps_display(cmd); + } +#endif + /* * We don't expect this to ever fail with ERROR or FATAL, no need * to keep error_ioh set to the IO. @@ -562,12 +934,90 @@ IoWorkerMain(const void *startup_data, size_t startup_data_len) } else { - WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, -1, - WAIT_EVENT_IO_WORKER_MAIN); + int timeout_ms; + + /* Cancel new worker request if pending. */ + pgaio_worker_cancel_grow(); + + /* Compute the remaining allowed idle time. */ + if (io_worker_idle_timeout == -1) + { + /* Never time out. */ + timeout_ms = -1; + } + else + { + TimestampTz now = GetCurrentTimestamp(); + + /* If the GUC changes, reset timer. */ + if (idle_timeout_abs != 0 && + io_worker_idle_timeout != timeout_guc_used) + idle_timeout_abs = 0; + + /* Only the highest-numbered worker can time out. */ + if (pgaio_worker_can_timeout()) + { + if (idle_timeout_abs == 0) + { + /* + * I have just been promoted to the timeout worker, or + * the GUC changed. Compute new absolute time from + * now. + */ + idle_timeout_abs = + TimestampTzPlusMilliseconds(now, + io_worker_idle_timeout); + timeout_guc_used = io_worker_idle_timeout; + } + timeout_ms = + TimestampDifferenceMilliseconds(now, idle_timeout_abs); + } + else + { + /* No timeout for me. */ + idle_timeout_abs = 0; + timeout_ms = -1; + } + } + +#ifdef PGAIO_WORKER_SHOW_PS_INFO + sprintf(cmd, "%d: idle, wakeups:ios = %d:%d", + MyIoWorkerId, hist_wakeups, hist_ios); + set_ps_display(cmd); +#endif + + if (WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH | WL_TIMEOUT, + timeout_ms, + WAIT_EVENT_IO_WORKER_MAIN) == WL_TIMEOUT) + { + /* WL_TIMEOUT */ + if (pgaio_worker_can_timeout()) + if (GetCurrentTimestamp() >= idle_timeout_abs) + break; + } + else + { + /* WL_LATCH_SET */ + if (++hist_wakeups == PGAIO_WORKER_WAKEUP_RATIO_SATURATE) + { + hist_wakeups /= 2; + hist_ios /= 2; + } + } ResetLatch(MyLatch); } CHECK_FOR_INTERRUPTS(); + + if (ConfigReloadPending) + { + ConfigReloadPending = false; + ProcessConfigFile(PGC_SIGHUP); + + /* If io_max_workers has been decreased, exit highest first. */ + if (MyIoWorkerId >= io_max_workers) + break; + } } error_context_stack = errcallback.previous; diff --git a/src/backend/storage/aio/read_stream.c b/src/backend/storage/aio/read_stream.c index 0e7f5557f5cb9..2374b4cd5076c 100644 --- a/src/backend/storage/aio/read_stream.c +++ b/src/backend/storage/aio/read_stream.c @@ -18,11 +18,13 @@ * to StartReadBuffers() so that a new one can begin to form. * * The algorithm for controlling the look-ahead distance is based on recent - * cache hit and miss history. When no I/O is necessary, there is no benefit - * in looking ahead more than one block. This is the default initial - * assumption, but when blocks needing I/O are streamed, the distance is - * increased rapidly to try to benefit from I/O combining and concurrency. It - * is reduced gradually when cached blocks are streamed. + * cache / miss history, as well as whether we need to wait for I/O completion + * after a miss. When no I/O is necessary, there is no benefit in looking + * ahead more than one block. This is the default initial assumption. When + * blocks needing I/O are streamed, the combine distance is increased to + * benefit from I/O combining and the read-ahead distance is increased + * whenever we need to wait for I/O to try to benefit from increased I/O + * concurrency. Both are reduced gradually when cached blocks are streamed. * * The main data structure is a circular queue of buffers of size * max_pinned_buffers plus some extra space for technical reasons, ready to be @@ -61,7 +63,7 @@ * does block 60. * * - * Portions Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2024-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -72,6 +74,7 @@ #include "postgres.h" #include "miscadmin.h" +#include "executor/instrument_node.h" #include "storage/aio.h" #include "storage/fd.h" #include "storage/smgr.h" @@ -98,14 +101,32 @@ struct ReadStream int16 max_pinned_buffers; int16 forwarded_buffers; int16 pinned_buffers; - int16 distance; + + /* + * Limit of how far, in blocks, to look-ahead for IO combining and for + * read-ahead. + * + * The limits for read-ahead and combining are handled separately to allow + * for IO combining even in cases where the I/O subsystem can keep up at a + * low read-ahead distance, as doing larger IOs is more efficient. + * + * Set to 0 when the end of the stream is reached. + */ + int16 combine_distance; + int16 readahead_distance; + uint16 distance_decay_holdoff; int16 initialized_buffers; + int16 resume_readahead_distance; + int16 resume_combine_distance; int read_buffers_flags; bool sync_mode; /* using io_method=sync */ bool batch_mode; /* READ_STREAM_USE_BATCHING */ bool advice_enabled; bool temporary; + /* scan stats counters */ + IOStats *stats; + /* * One-block buffer to support 'ungetting' a block number, to resolve flow * control problems when I/Os are split. @@ -171,6 +192,73 @@ block_range_read_stream_cb(ReadStream *stream, return InvalidBlockNumber; } +/* + * Update stream stats with current pinned buffer depth. + * + * Called once per buffer returned to the consumer in read_stream_next_buffer(). + * Records the number of pinned buffers at that moment, so we can compute the + * average look-ahead depth. + */ +static inline void +read_stream_count_prefetch(ReadStream *stream) +{ + IOStats *stats = stream->stats; + + if (stats == NULL) + return; + + stats->prefetch_count++; + stats->distance_sum += stream->pinned_buffers; + if (stream->pinned_buffers > stats->distance_max) + stats->distance_max = stream->pinned_buffers; +} + +/* + * Update stream stats about size of I/O requests. + * + * We count the number of I/O requests, size of requests (counted in blocks) + * and number of in-progress I/Os. + */ +static inline void +read_stream_count_io(ReadStream *stream, int nblocks, int in_progress) +{ + IOStats *stats = stream->stats; + + if (stats == NULL) + return; + + stats->io_count++; + stats->io_nblocks += nblocks; + stats->io_in_progress += in_progress; +} + +/* + * Update stream stats about waits for I/O when consuming buffers. + * + * We count the number of I/O waits while pulling buffers out of a stream. + */ +static inline void +read_stream_count_wait(ReadStream *stream) +{ + IOStats *stats = stream->stats; + + if (stats == NULL) + return; + + stats->wait_count++; +} + +/* + * Enable collection of stats into the provided IOStats. + */ +void +read_stream_enable_stats(ReadStream *stream, IOStats *stats) +{ + stream->stats = stats; + if (stream->stats) + stream->stats->distance_capacity = stream->max_pinned_buffers; +} + /* * Ask the callback which block it would like us to read next, with a one block * buffer in front to allow read_stream_unget_block() to work. @@ -247,12 +335,33 @@ read_stream_start_pending_read(ReadStream *stream) Assert(stream->pinned_buffers + stream->pending_read_nblocks <= stream->max_pinned_buffers); +#ifdef USE_ASSERT_CHECKING /* We had better not be overwriting an existing pinned buffer. */ if (stream->pinned_buffers > 0) Assert(stream->next_buffer_index != stream->oldest_buffer_index); else Assert(stream->next_buffer_index == stream->oldest_buffer_index); + /* + * Pinned buffers forwarded by a preceding StartReadBuffers() call that + * had to split the operation should match the leading blocks of this + * following StartReadBuffers() call. + */ + Assert(stream->forwarded_buffers <= stream->pending_read_nblocks); + for (int i = 0; i < stream->forwarded_buffers; ++i) + Assert(BufferGetBlockNumber(stream->buffers[stream->next_buffer_index + i]) == + stream->pending_read_blocknum + i); + + /* + * Check that we've cleared the queue/overflow entries corresponding to + * the rest of the blocks covered by this read, unless it's the first go + * around and we haven't even initialized them yet. + */ + for (int i = stream->forwarded_buffers; i < stream->pending_read_nblocks; ++i) + Assert(stream->next_buffer_index + i >= stream->initialized_buffers || + stream->buffers[stream->next_buffer_index + i] == InvalidBuffer); +#endif + /* Do we need to issue read-ahead advice? */ flags = stream->read_buffers_flags; if (stream->advice_enabled) @@ -262,7 +371,7 @@ read_stream_start_pending_read(ReadStream *stream) /* * Sequential: Issue advice until the preadv() calls have caught * up with the first advice issued for this sequential region, and - * then stay of the way of the kernel's own read-ahead. + * then stay out of the way of the kernel's own read-ahead. */ if (stream->seq_until_processed != InvalidBlockNumber) flags |= READ_BUFFERS_ISSUE_ADVICE; @@ -309,8 +418,8 @@ read_stream_start_pending_read(ReadStream *stream) /* Shrink distance: no more look-ahead until buffers are released. */ new_distance = stream->pinned_buffers + buffer_limit; - if (stream->distance > new_distance) - stream->distance = new_distance; + if (stream->readahead_distance > new_distance) + stream->readahead_distance = new_distance; /* Unless we have nothing to give the consumer, stop here. */ if (stream->pinned_buffers > 0) @@ -342,9 +451,39 @@ read_stream_start_pending_read(ReadStream *stream) /* Remember whether we need to wait before returning this buffer. */ if (!need_wait) { - /* Look-ahead distance decays, no I/O necessary. */ - if (stream->distance > 1) - stream->distance--; + /* + * If there currently is no IO in progress, and we have not needed to + * issue IO recently, decay the look-ahead distance. We detect if we + * had to issue IO recently by having a decay holdoff that's set to + * the max look-ahead distance whenever we need to do IO. This is + * important to ensure we eventually reach a high enough distance to + * perform IO asynchronously when starting out with a small look-ahead + * distance. + */ + if (stream->ios_in_progress == 0) + { + if (stream->distance_decay_holdoff > 0) + stream->distance_decay_holdoff--; + else + { + if (stream->readahead_distance > 1) + stream->readahead_distance--; + + /* + * For now we reduce the IO combine distance after + * sufficiently many buffer hits. There is no clear + * performance argument for doing so, but at the moment we + * need to do so to make the entrance into fast_path work + * correctly: We require combine_distance == 1 to enter + * fast-path, as without that condition we would wrongly + * re-enter fast-path when readahead_distance == 1 and + * pinned_buffers == 1, as we would not yet have prepared + * another IO in that situation. + */ + if (stream->combine_distance > 1) + stream->combine_distance--; + } + } } else { @@ -358,6 +497,9 @@ read_stream_start_pending_read(ReadStream *stream) Assert(stream->ios_in_progress < stream->max_ios); stream->ios_in_progress++; stream->seq_blocknum = stream->pending_read_blocknum + nblocks; + + /* update I/O stats */ + read_stream_count_io(stream, nblocks, stream->ios_in_progress); } /* @@ -404,6 +546,114 @@ read_stream_start_pending_read(ReadStream *stream) return true; } +/* + * Should we continue to perform look ahead? Looking ahead may allow us to + * make the pending IO larger via IO combining or to issue more read-ahead. + */ +static inline bool +read_stream_should_look_ahead(ReadStream *stream) +{ + /* If the callback has signaled end-of-stream, we're done */ + if (stream->readahead_distance == 0) + return false; + + /* never start more IOs than our cap */ + if (stream->ios_in_progress >= stream->max_ios) + return false; + + /* + * Allow looking further ahead if we are in the process of building a + * larger IO, the IO is not yet big enough, and we don't yet have IO in + * flight. + * + * We do so to allow building larger reads when readahead_distance is + * small (e.g. because the I/O subsystem is keeping up or + * effective_io_concurrency is small). That's a useful goal because larger + * reads are more CPU efficient than smaller reads, even if the system is + * not IO bound. + * + * The reason we do *not* do so when we already have a read prepared (i.e. + * why we check for pinned_buffers == 0) is once we are actually reading + * ahead, we don't need it: + * + * - We won't issue unnecessarily small reads as + * read_stream_should_issue_now() will return false until the IO is + * suitably sized. The issuance of the pending read will be delayed until + * enough buffers have been consumed. + * + * - If we are not reading ahead aggressively enough, future + * WaitReadBuffers() calls will return true, leading to readahead_distance + * being increased. After that more full-sized IOs can be issued. + * + * Furthermore, if we did not have the pinned_buffers == 0 condition, we + * might end up issuing I/O more aggressively than we need. + * + * Note that a return of true here can lead to exceeding the read-ahead + * limit, but we won't exceed the buffer pin limit (because pinned_buffers + * == 0 and combine_distance is capped by max_pinned_buffers). + */ + if (stream->pending_read_nblocks > 0 && + stream->pinned_buffers == 0 && + stream->pending_read_nblocks < stream->combine_distance) + return true; + + /* + * Don't start more read-ahead if that'd put us over the distance limit + * for doing read-ahead. As stream->readahead_distance is capped by + * max_pinned_buffers, this prevents us from looking ahead so far that it + * would put us over the pin limit. + */ + if (stream->pinned_buffers + stream->pending_read_nblocks >= stream->readahead_distance) + return false; + + return true; +} + +/* + * We don't start the pending read just because we've hit the distance limit, + * preferring to give it another chance to grow to full io_combine_limit size + * once more buffers have been consumed. But this is not desirable in all + * situations - see below. + */ +static inline bool +read_stream_should_issue_now(ReadStream *stream) +{ + int16 pending_read_nblocks = stream->pending_read_nblocks; + + /* there is no pending IO that could be issued */ + if (pending_read_nblocks == 0) + return false; + + /* never start more IOs than our cap */ + if (stream->ios_in_progress >= stream->max_ios) + return false; + + /* + * If the callback has signaled end-of-stream, start the pending read + * immediately. There is no further potential for IO combining. + */ + if (stream->readahead_distance == 0) + return true; + + /* + * If we've already reached combine_distance, there's no chance of growing + * the read further. + */ + if (pending_read_nblocks >= stream->combine_distance) + return true; + + /* + * If we currently have no reads in flight or prepared, issue the IO once + * we are not looking ahead further. This ensures there's always at least + * one IO prepared. + */ + if (stream->pinned_buffers == 0 && + !read_stream_should_look_ahead(stream)) + return true; + + return false; +} + static void read_stream_look_ahead(ReadStream *stream) { @@ -416,14 +666,13 @@ read_stream_look_ahead(ReadStream *stream) if (stream->batch_mode) pgaio_enter_batchmode(); - while (stream->ios_in_progress < stream->max_ios && - stream->pinned_buffers + stream->pending_read_nblocks < stream->distance) + while (read_stream_should_look_ahead(stream)) { BlockNumber blocknum; int16 buffer_index; void *per_buffer_data; - if (stream->pending_read_nblocks == stream->io_combine_limit) + if (read_stream_should_issue_now(stream)) { read_stream_start_pending_read(stream); continue; @@ -443,7 +692,8 @@ read_stream_look_ahead(ReadStream *stream) if (blocknum == InvalidBlockNumber) { /* End of stream. */ - stream->distance = 0; + stream->readahead_distance = 0; + stream->combine_distance = 0; break; } @@ -475,21 +725,13 @@ read_stream_look_ahead(ReadStream *stream) } /* - * We don't start the pending read just because we've hit the distance - * limit, preferring to give it another chance to grow to full - * io_combine_limit size once more buffers have been consumed. However, - * if we've already reached io_combine_limit, or we've reached the - * distance limit and there isn't anything pinned yet, or the callback has - * signaled end-of-stream, we start the read immediately. Note that the - * pending read can exceed the distance goal, if the latter was reduced - * after hitting the per-backend buffer limit. + * Check if the pending read should be issued now, or if we should give it + * another chance to grow to the full size. + * + * Note that the pending read can exceed the distance goal, if the latter + * was reduced after hitting the per-backend buffer limit. */ - if (stream->pending_read_nblocks > 0 && - (stream->pending_read_nblocks == stream->io_combine_limit || - (stream->pending_read_nblocks >= stream->distance && - stream->pinned_buffers == 0) || - stream->distance == 0) && - stream->ios_in_progress < stream->max_ios) + if (read_stream_should_issue_now(stream)) read_stream_start_pending_read(stream); /* @@ -498,7 +740,7 @@ read_stream_look_ahead(ReadStream *stream) * stream. In the worst case we can always make progress one buffer at a * time. */ - Assert(stream->pinned_buffers > 0 || stream->distance == 0); + Assert(stream->pinned_buffers > 0 || stream->readahead_distance == 0); if (stream->batch_mode) pgaio_exit_batchmode(); @@ -535,10 +777,9 @@ read_stream_begin_impl(int flags, Oid tablespace_id; /* - * Decide how many I/Os we will allow to run at the same time. That - * currently means advice to the kernel to tell it that we will soon read. - * This number also affects how far we look ahead for opportunities to - * start more I/Os. + * Decide how many I/Os we will allow to run at the same time. This + * number also affects how far we look ahead for opportunities to start + * more I/Os. */ tablespace_id = smgr->smgr_rlocator.locator.spcOid; if (!OidIsValid(MyDatabaseId) || @@ -680,6 +921,7 @@ read_stream_begin_impl(int flags, stream->seq_blocknum = InvalidBlockNumber; stream->seq_until_processed = InvalidBlockNumber; stream->temporary = SmgrIsTemp(smgr); + stream->distance_decay_holdoff = 0; /* * Skip the initial ramp-up phase if the caller says we're going to be @@ -687,9 +929,17 @@ read_stream_begin_impl(int flags, * doing full io_combine_limit sized reads. */ if (flags & READ_STREAM_FULL) - stream->distance = Min(max_pinned_buffers, stream->io_combine_limit); + { + stream->readahead_distance = Min(max_pinned_buffers, stream->io_combine_limit); + stream->combine_distance = Min(max_pinned_buffers, stream->io_combine_limit); + } else - stream->distance = 1; + { + stream->readahead_distance = 1; + stream->combine_distance = 1; + } + stream->resume_readahead_distance = stream->readahead_distance; + stream->resume_combine_distance = stream->combine_distance; /* * Since we always access the same relation, we can initialize parts of @@ -788,7 +1038,8 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) Assert(stream->ios_in_progress == 0); Assert(stream->forwarded_buffers == 0); Assert(stream->pinned_buffers == 1); - Assert(stream->distance == 1); + Assert(stream->readahead_distance == 1); + Assert(stream->combine_distance == 1); Assert(stream->pending_read_nblocks == 0); Assert(stream->per_buffer_data_size == 0); Assert(stream->initialized_buffers > stream->oldest_buffer_index); @@ -810,6 +1061,21 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) if (stream->advice_enabled) flags |= READ_BUFFERS_ISSUE_ADVICE; + /* + * While in fast-path, execute any IO that we might encounter + * synchronously. Because we are, right now, only looking one + * block ahead, dispatching any occasional IO to workers would + * have the overhead of dispatching to workers, without any + * realistic chance of the IO completing before we need it. We + * will switch to non-synchronous IO after this. + * + * Arguably we should do so only for worker, as there's far less + * dispatch overhead with io_uring. However, tests so far have not + * shown a clear downside and additional io_method awareness here + * seems not great from an abstraction POV. + */ + flags |= READ_BUFFERS_SYNCHRONOUSLY; + /* * Pin a buffer for the next call. Same buffer entry, and * arbitrary I/O entry (they're all free). We don't have to @@ -828,6 +1094,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) flags))) { /* Fast return. */ + read_stream_count_prefetch(stream); return buffer; } @@ -837,11 +1104,24 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) stream->ios_in_progress = 1; stream->ios[0].buffer_index = oldest_buffer_index; stream->seq_blocknum = next_blocknum + 1; + + /* + * XXX: It might be worth triggering additional read-ahead here, + * to avoid having to effectively do another synchronous IO for + * the next block (if it were also a miss). + */ + + /* update I/O stats */ + read_stream_count_io(stream, 1, stream->ios_in_progress); + + /* update prefetch distance */ + read_stream_count_prefetch(stream); } else { /* No more blocks, end of stream. */ - stream->distance = 0; + stream->readahead_distance = 0; + stream->combine_distance = 0; stream->oldest_buffer_index = stream->next_buffer_index; stream->pinned_buffers = 0; stream->buffers[oldest_buffer_index] = InvalidBuffer; @@ -857,7 +1137,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) Assert(stream->oldest_buffer_index == stream->next_buffer_index); /* End of stream reached? */ - if (stream->distance == 0) + if (stream->readahead_distance == 0) return InvalidBuffer; /* @@ -871,7 +1151,7 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) /* End of stream reached? */ if (stream->pinned_buffers == 0) { - Assert(stream->distance == 0); + Assert(stream->readahead_distance == 0); return InvalidBuffer; } } @@ -892,23 +1172,97 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) stream->ios[stream->oldest_io_index].buffer_index == oldest_buffer_index) { int16 io_index = stream->oldest_io_index; - int32 distance; /* wider temporary value, clamped below */ + bool needed_wait; /* Sanity check that we still agree on the buffers. */ Assert(stream->ios[io_index].op.buffers == &stream->buffers[oldest_buffer_index]); - WaitReadBuffers(&stream->ios[io_index].op); + needed_wait = WaitReadBuffers(&stream->ios[io_index].op); Assert(stream->ios_in_progress > 0); stream->ios_in_progress--; if (++stream->oldest_io_index == stream->max_ios) stream->oldest_io_index = 0; - /* Look-ahead distance ramps up rapidly after we do I/O. */ - distance = stream->distance * 2; - distance = Min(distance, stream->max_pinned_buffers); - stream->distance = distance; + /* + * If the IO was executed synchronously, we will never see + * WaitReadBuffers() block. Treat it as if it did block. This is + * particularly crucial when effective_io_concurrency=0 is used, as + * all IO will be synchronous. Without treating synchronous IO as + * having waited, we'd never allow the distance to get large enough to + * allow for IO combining, resulting in bad performance. + */ + if (stream->ios[io_index].op.flags & READ_BUFFERS_SYNCHRONOUSLY) + needed_wait = true; + + /* Count it as a wait if we need to wait for IO */ + if (needed_wait) + read_stream_count_wait(stream); + + /* + * Have the read-ahead distance ramp up rapidly after we needed to + * wait for IO. We only increase the read-ahead-distance when we + * needed to wait, to avoid increasing the distance further than + * necessary, as looking ahead too far can be costly, both due to the + * cost of unnecessarily pinning many buffers and due to doing IOs + * that may never be consumed if the stream is ended/reset before + * completion. + * + * If we did not need to wait, the current distance was evidently + * sufficient. + * + * NB: Must not increase the distance if we already reached the end of + * the stream, as stream->readahead_distance == 0 is used to keep + * track of having reached the end. + */ + if (stream->readahead_distance > 0 && needed_wait) + { + /* wider temporary value, due to overflow risk */ + int32 readahead_distance; + + readahead_distance = stream->readahead_distance * 2; + readahead_distance = Min(readahead_distance, stream->max_pinned_buffers); + stream->readahead_distance = readahead_distance; + } + + /* + * As we needed IO, prevent distances from being reduced within our + * maximum look-ahead window. This avoids collapsing distances too + * quickly in workloads where most of the required blocks are cached, + * but where the remaining IOs are a sufficient enough factor to cause + * a substantial slowdown if executed synchronously. + * + * There are valid arguments for preventing decay for max_ios or for + * max_pinned_buffers. But the argument for max_pinned_buffers seems + * clearer - if we can't see any misses within the maximum look-ahead + * distance, we can't do any useful read-ahead. + */ + stream->distance_decay_holdoff = stream->max_pinned_buffers; + + /* + * Whether we needed to wait or not, allow for more IO combining if we + * needed to do IO. The reason to do so independent of needing to wait + * is that when the data is resident in the kernel page cache, IO + * combining reduces the syscall / dispatch overhead, making it + * worthwhile regardless of needing to wait. + * + * It is also important with io_uring as it will never signal the need + * to wait for reads if all the data is in the page cache. There are + * heuristics to deal with that in method_io_uring.c, but they only + * work when the IO gets large enough. + */ + if (stream->combine_distance > 0 && + stream->combine_distance < stream->io_combine_limit) + { + /* wider temporary value, due to overflow risk */ + int32 combine_distance; + + combine_distance = stream->combine_distance * 2; + combine_distance = Min(combine_distance, stream->io_combine_limit); + combine_distance = Min(combine_distance, stream->max_pinned_buffers); + stream->combine_distance = combine_distance; + } /* * If we've reached the first block of a sequential region we're @@ -958,6 +1312,8 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) } #endif + read_stream_count_prefetch(stream); + /* Pin transferred to caller. */ Assert(stream->pinned_buffers > 0); stream->pinned_buffers--; @@ -975,10 +1331,24 @@ read_stream_next_buffer(ReadStream *stream, void **per_buffer_data) if (stream->ios_in_progress == 0 && stream->forwarded_buffers == 0 && stream->pinned_buffers == 1 && - stream->distance == 1 && + stream->readahead_distance == 1 && + stream->combine_distance == 1 && stream->pending_read_nblocks == 0 && stream->per_buffer_data_size == 0) { + /* + * The fast path spins on one buffer entry repeatedly instead of + * rotating through the whole queue and clearing the entries behind + * it. If the buffer it starts with happened to be forwarded between + * StartReadBuffers() calls and also wrapped around the circular queue + * partway through, then a copy also exists in the overflow zone, and + * it won't clear it out as the regular path would. Do that now, so + * it doesn't need code for that. + */ + if (stream->oldest_buffer_index < stream->io_combine_limit - 1) + stream->buffers[stream->queue_size + stream->oldest_buffer_index] = + InvalidBuffer; + stream->fast_path = true; } #endif @@ -1000,6 +1370,33 @@ read_stream_next_block(ReadStream *stream, BufferAccessStrategy *strategy) return read_stream_get_block(stream, NULL); } +/* + * Temporarily stop consuming block numbers from the block number callback. + * If called inside the block number callback, its return value should be + * returned by the callback. + */ +BlockNumber +read_stream_pause(ReadStream *stream) +{ + stream->resume_readahead_distance = stream->readahead_distance; + stream->resume_combine_distance = stream->combine_distance; + stream->readahead_distance = 0; + stream->combine_distance = 0; + return InvalidBlockNumber; +} + +/* + * Resume looking ahead after the block number callback reported + * end-of-stream. This is useful for streams of self-referential blocks, after + * a buffer needed to be consumed and examined to find more block numbers. + */ +void +read_stream_resume(ReadStream *stream) +{ + stream->readahead_distance = stream->resume_readahead_distance; + stream->combine_distance = stream->resume_combine_distance; +} + /* * Reset a read stream by releasing any queued up buffers, allowing the stream * to be used again for different blocks. This can be used to clear an @@ -1013,7 +1410,8 @@ read_stream_reset(ReadStream *stream) Buffer buffer; /* Stop looking ahead. */ - stream->distance = 0; + stream->readahead_distance = 0; + stream->combine_distance = 0; /* Forget buffered block number and fast path state. */ stream->buffered_blocknum = InvalidBlockNumber; @@ -1045,7 +1443,11 @@ read_stream_reset(ReadStream *stream) Assert(stream->ios_in_progress == 0); /* Start off assuming data is cached. */ - stream->distance = 1; + stream->readahead_distance = 1; + stream->combine_distance = 1; + stream->resume_readahead_distance = stream->readahead_distance; + stream->resume_combine_distance = stream->combine_distance; + stream->distance_decay_holdoff = 0; } /* diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README index a182fcd660ccb..b332e002ba13b 100644 --- a/src/backend/storage/buffer/README +++ b/src/backend/storage/buffer/README @@ -25,21 +25,26 @@ that might need to do such a wait is instead handled by waiting to obtain the relation-level lock, which is why you'd better hold one first.) Pins may not be held across transaction boundaries, however. -Buffer content locks: there are two kinds of buffer lock, shared and exclusive, -which act just as you'd expect: multiple backends can hold shared locks on -the same buffer, but an exclusive lock prevents anyone else from holding -either shared or exclusive lock. (These can alternatively be called READ -and WRITE locks.) These locks are intended to be short-term: they should not -be held for long. Buffer locks are acquired and released by LockBuffer(). -It will *not* work for a single backend to try to acquire multiple locks on -the same buffer. One must pin a buffer before trying to lock it. +Buffer content locks: there are three kinds of buffer lock, shared, +share-exclusive and exclusive: +a) multiple backends can hold shared locks on the same buffer + (alternatively called a READ lock) +b) one backend can hold a share-exclusive lock on a buffer while multiple + backends can hold a share lock +c) an exclusive lock prevents anyone else from holding a shared, + share-exclusive or exclusive lock. + (alternatively called a WRITE lock) + +These locks are intended to be short-term: they should not be held for long. +Buffer locks are acquired and released by LockBuffer(). It will *not* work +for a single backend to try to acquire multiple locks on the same buffer. One +must pin a buffer before trying to lock it. Buffer access rules: -1. To scan a page for tuples, one must hold a pin and either shared or -exclusive content lock. To examine the commit status (XIDs and status bits) -of a tuple in a shared buffer, one must likewise hold a pin and either shared -or exclusive lock. +1. To scan a page for tuples, one must hold a pin and at least a share lock. +To examine the commit status (XIDs and status bits) of a tuple in a shared +buffer, one must likewise hold a pin and at least a share lock. 2. Once one has determined that a tuple is interesting (visible to the current transaction) one may drop the content lock, yet continue to access @@ -55,19 +60,25 @@ one must hold a pin and an exclusive content lock on the containing buffer. This ensures that no one else might see a partially-updated state of the tuple while they are doing visibility checks. -4. It is considered OK to update tuple commit status bits (ie, OR the -values HEAP_XMIN_COMMITTED, HEAP_XMIN_INVALID, HEAP_XMAX_COMMITTED, or -HEAP_XMAX_INVALID into t_infomask) while holding only a shared lock and -pin on a buffer. This is OK because another backend looking at the tuple -at about the same time would OR the same bits into the field, so there -is little or no risk of conflicting update; what's more, if there did -manage to be a conflict it would merely mean that one bit-update would -be lost and need to be done again later. These four bits are only hints -(they cache the results of transaction status lookups in pg_xact), so no -great harm is done if they get reset to zero by conflicting updates. -Note, however, that a tuple is frozen by setting both HEAP_XMIN_INVALID -and HEAP_XMIN_COMMITTED; this is a critical update and accordingly requires -an exclusive buffer lock (and it must also be WAL-logged). +4. Non-critical information on a page ("hint bits") may be modified while +holding only a share-exclusive lock and pin on the page. To do so in cases +where only a share lock is already held, use BufferBeginSetHintBits() & +BufferFinishSetHintBits() (if multiple hint bits are to be set) or +BufferSetHintBits16() (if a single hint bit is set). + +E.g. for heapam, a share-exclusive lock allows to update tuple commit status +bits (ie, OR the values HEAP_XMIN_COMMITTED, HEAP_XMIN_INVALID, +HEAP_XMAX_COMMITTED, or HEAP_XMAX_INVALID into t_infomask) while holding only +a share-exclusive lock and pin on a buffer. This is OK because another +backend looking at the tuple at about the same time would OR the same bits +into the field, so there is little or no risk of conflicting update; what's +more, if there did manage to be a conflict it would merely mean that one +bit-update would be lost and need to be done again later. These four bits are +only hints (they cache the results of transaction status lookups in pg_xact), +so no great harm is done if they get reset to zero by conflicting updates. +Note, however, that a tuple is frozen by setting both HEAP_XMIN_INVALID and +HEAP_XMIN_COMMITTED; this is a critical update and accordingly requires an +exclusive buffer lock (and it must also be WAL-logged). 5. To physically remove a tuple or compact free space on a page, one must hold a pin and an exclusive lock, *and* observe while holding the @@ -80,7 +91,6 @@ buffer (increment the refcount) while one is performing the cleanup, but it won't be able to actually examine the page until it acquires shared or exclusive content lock. - Obtaining the lock needed under rule #5 is done by the bufmgr routines LockBufferForCleanup() or ConditionalLockBufferForCleanup(). They first get an exclusive lock and then check to see if the shared pin count is currently @@ -96,6 +106,10 @@ VACUUM's use, since we don't allow multiple VACUUMs concurrently on a single relation anyway. Anyone wishing to obtain a cleanup lock outside of recovery or a VACUUM must use the conditional variant of the function. +6. To write out a buffer, a share-exclusive lock needs to be held. This +prevents the buffer from being modified while written out, which could corrupt +checksums and cause issues on the OS or device level when direct-IO is used. + Buffer Manager's Internal Locking --------------------------------- @@ -128,11 +142,11 @@ independently. If it is necessary to lock more than one partition at a time, they must be locked in partition-number order to avoid risk of deadlock. * A separate system-wide spinlock, buffer_strategy_lock, provides mutual -exclusion for operations that access the buffer free list or select -buffers for replacement. A spinlock is used here rather than a lightweight -lock for efficiency; no other locks of any sort should be acquired while -buffer_strategy_lock is held. This is essential to allow buffer replacement -to happen in multiple backends with reasonable concurrency. +exclusion for operations that select buffers for replacement. A spinlock is +used here rather than a lightweight lock for efficiency; no other locks of any +sort should be acquired while buffer_strategy_lock is held. This is essential +to allow buffer replacement to happen in multiple backends with reasonable +concurrency. * Each buffer header contains a spinlock that must be taken when examining or changing fields of that buffer header. This allows operations such as @@ -158,18 +172,8 @@ unset by sleeping on the buffer's condition variable. Normal Buffer Replacement Strategy ---------------------------------- -There is a "free list" of buffers that are prime candidates for replacement. -In particular, buffers that are completely free (contain no valid page) are -always in this list. We could also throw buffers into this list if we -consider their pages unlikely to be needed soon; however, the current -algorithm never does that. The list is singly-linked using fields in the -buffer headers; we maintain head and tail pointers in global variables. -(Note: although the list links are in the buffer headers, they are -considered to be protected by the buffer_strategy_lock, not the buffer-header -spinlocks.) To choose a victim buffer to recycle when there are no free -buffers available, we use a simple clock-sweep algorithm, which avoids the -need to take system-wide locks during common operations. It works like -this: +To choose a victim buffer to recycle we use a simple clock-sweep algorithm. It +works like this: Each buffer header contains a usage counter, which is incremented (up to a small limit value) whenever the buffer is pinned. (This requires only the @@ -184,20 +188,14 @@ The algorithm for a process that needs to obtain a victim buffer is: 1. Obtain buffer_strategy_lock. -2. If buffer free list is nonempty, remove its head buffer. Release -buffer_strategy_lock. If the buffer is pinned or has a nonzero usage count, -it cannot be used; ignore it go back to step 1. Otherwise, pin the buffer, -and return it. - -3. Otherwise, the buffer free list is empty. Select the buffer pointed to by -nextVictimBuffer, and circularly advance nextVictimBuffer for next time. -Release buffer_strategy_lock. +2. Select the buffer pointed to by nextVictimBuffer, and circularly advance +nextVictimBuffer for next time. Release buffer_strategy_lock. -4. If the selected buffer is pinned or has a nonzero usage count, it cannot +3. If the selected buffer is pinned or has a nonzero usage count, it cannot be used. Decrement its usage count (if nonzero), reacquire buffer_strategy_lock, and return to step 3 to examine the next buffer. -5. Pin the selected buffer, and return. +4. Pin the selected buffer, and return. (Note that if the selected buffer is dirty, we will have to write it out before we can recycle it; if someone else pins the buffer meanwhile we will @@ -211,9 +209,9 @@ Buffer Ring Replacement Strategy When running a query that needs to access a large number of pages just once, such as VACUUM or a large sequential scan, a different strategy is used. A page that has been touched only by such a scan is unlikely to be needed -again soon, so instead of running the normal clock sweep algorithm and +again soon, so instead of running the normal clock-sweep algorithm and blowing out the entire buffer cache, a small ring of buffers is allocated -using the normal clock sweep algorithm and those buffers are reused for the +using the normal clock-sweep algorithm and those buffers are reused for the whole scan. This also implies that much of the write traffic caused by such a statement will be done by the backend itself and not pushed off onto other processes. @@ -234,7 +232,7 @@ the ring strategy effectively degrades to the normal strategy. VACUUM uses a ring like sequential scans, however, the size of this ring is controlled by the vacuum_buffer_usage_limit GUC. Dirty pages are not removed -from the ring. Instead, WAL is flushed if needed to allow reuse of the +from the ring. Instead, the WAL is flushed if needed to allow reuse of the buffers. Before introducing the buffer ring strategy in 8.3, VACUUM's buffers were sent to the freelist, which was effectively a buffer ring of 1 buffer, resulting in excessive WAL flushing. diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c index ed1dc488a42b4..1407c930c56af 100644 --- a/src/backend/storage/buffer/buf_init.c +++ b/src/backend/storage/buffer/buf_init.c @@ -3,7 +3,7 @@ * buf_init.c * buffer manager initialization routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,9 @@ #include "storage/aio.h" #include "storage/buf_internals.h" #include "storage/bufmgr.h" +#include "storage/proclist.h" +#include "storage/shmem.h" +#include "storage/subsystems.h" BufferDescPadded *BufferDescriptors; char *BufferBlocks; @@ -24,6 +27,15 @@ ConditionVariableMinimallyPadded *BufferIOCVArray; WritebackContext BackendWritebackContext; CkptSortItem *CkptBufferIds; +static void BufferManagerShmemRequest(void *arg); +static void BufferManagerShmemInit(void *arg); +static void BufferManagerShmemAttach(void *arg); + +const ShmemCallbacks BufferManagerShmemCallbacks = { + .request_fn = BufferManagerShmemRequest, + .init_fn = BufferManagerShmemInit, + .attach_fn = BufferManagerShmemAttach, +}; /* * Data Structures: @@ -59,37 +71,31 @@ CkptSortItem *CkptBufferIds; /* - * Initialize shared buffer pool - * - * This is called once during shared-memory initialization (either in the - * postmaster, or in a standalone backend). + * Register shared memory area for the buffer pool. */ -void -BufferManagerShmemInit(void) +static void +BufferManagerShmemRequest(void *arg) { - bool foundBufs, - foundDescs, - foundIOCV, - foundBufCkpt; - + ShmemRequestStruct(.name = "Buffer Descriptors", + .size = NBuffers * sizeof(BufferDescPadded), /* Align descriptors to a cacheline boundary. */ - BufferDescriptors = (BufferDescPadded *) - ShmemInitStruct("Buffer Descriptors", - NBuffers * sizeof(BufferDescPadded), - &foundDescs); + .alignment = PG_CACHE_LINE_SIZE, + .ptr = (void **) &BufferDescriptors, + ); + ShmemRequestStruct(.name = "Buffer Blocks", + .size = NBuffers * (Size) BLCKSZ, /* Align buffer pool on IO page size boundary. */ - BufferBlocks = (char *) - TYPEALIGN(PG_IO_ALIGN_SIZE, - ShmemInitStruct("Buffer Blocks", - NBuffers * (Size) BLCKSZ + PG_IO_ALIGN_SIZE, - &foundBufs)); - - /* Align condition variables to cacheline boundary. */ - BufferIOCVArray = (ConditionVariableMinimallyPadded *) - ShmemInitStruct("Buffer IO Condition Variables", - NBuffers * sizeof(ConditionVariableMinimallyPadded), - &foundIOCV); + .alignment = PG_IO_ALIGN_SIZE, + .ptr = (void **) &BufferBlocks, + ); + + ShmemRequestStruct(.name = "Buffer IO Condition Variables", + .size = NBuffers * sizeof(ConditionVariableMinimallyPadded), + /* Align descriptors to a cacheline boundary. */ + .alignment = PG_CACHE_LINE_SIZE, + .ptr = (void **) &BufferIOCVArray, + ); /* * The array used to sort to-be-checkpointed buffer ids is located in @@ -98,91 +104,50 @@ BufferManagerShmemInit(void) * the checkpointer is restarted, memory allocation failures would be * painful. */ - CkptBufferIds = (CkptSortItem *) - ShmemInitStruct("Checkpoint BufferIds", - NBuffers * sizeof(CkptSortItem), &foundBufCkpt); + ShmemRequestStruct(.name = "Checkpoint BufferIds", + .size = NBuffers * sizeof(CkptSortItem), + .ptr = (void **) &CkptBufferIds, + ); +} - if (foundDescs || foundBufs || foundIOCV || foundBufCkpt) - { - /* should find all of these, or none of them */ - Assert(foundDescs && foundBufs && foundIOCV && foundBufCkpt); - /* note: this path is only taken in EXEC_BACKEND case */ - } - else +/* + * Initialize shared buffer pool + * + * This is called once during shared-memory initialization (either in the + * postmaster, or in a standalone backend). + */ +static void +BufferManagerShmemInit(void *arg) +{ + /* + * Initialize all the buffer headers. + */ + for (int i = 0; i < NBuffers; i++) { - int i; - - /* - * Initialize all the buffer headers. - */ - for (i = 0; i < NBuffers; i++) - { - BufferDesc *buf = GetBufferDescriptor(i); - - ClearBufferTag(&buf->tag); - - pg_atomic_init_u32(&buf->state, 0); - buf->wait_backend_pgprocno = INVALID_PROC_NUMBER; - - buf->buf_id = i; + BufferDesc *buf = GetBufferDescriptor(i); - pgaio_wref_clear(&buf->io_wref); + ClearBufferTag(&buf->tag); - /* - * Initially link all the buffers together as unused. Subsequent - * management of this list is done by freelist.c. - */ - buf->freeNext = i + 1; + pg_atomic_init_u64(&buf->state, 0); + buf->wait_backend_pgprocno = INVALID_PROC_NUMBER; - LWLockInitialize(BufferDescriptorGetContentLock(buf), - LWTRANCHE_BUFFER_CONTENT); + buf->buf_id = i; - ConditionVariableInit(BufferDescriptorGetIOCV(buf)); - } + pgaio_wref_clear(&buf->io_wref); - /* Correct last entry of linked list */ - GetBufferDescriptor(NBuffers - 1)->freeNext = FREENEXT_END_OF_LIST; + proclist_init(&buf->lock_waiters); + ConditionVariableInit(BufferDescriptorGetIOCV(buf)); } - /* Init other shared buffer-management stuff */ - StrategyInitialize(!foundDescs); - /* Initialize per-backend file flush context */ WritebackContextInit(&BackendWritebackContext, &backend_flush_after); } -/* - * BufferManagerShmemSize - * - * compute the size of shared memory for the buffer pool including - * data pages, buffer descriptors, hash tables, etc. - */ -Size -BufferManagerShmemSize(void) +static void +BufferManagerShmemAttach(void *arg) { - Size size = 0; - - /* size of buffer descriptors */ - size = add_size(size, mul_size(NBuffers, sizeof(BufferDescPadded))); - /* to allow aligning buffer descriptors */ - size = add_size(size, PG_CACHE_LINE_SIZE); - - /* size of data pages, plus alignment padding */ - size = add_size(size, PG_IO_ALIGN_SIZE); - size = add_size(size, mul_size(NBuffers, BLCKSZ)); - - /* size of stuff controlled by freelist.c */ - size = add_size(size, StrategyShmemSize()); - - /* size of I/O condition variables */ - size = add_size(size, mul_size(NBuffers, - sizeof(ConditionVariableMinimallyPadded))); - /* to allow aligning the above */ - size = add_size(size, PG_CACHE_LINE_SIZE); - - /* size of checkpoint sort array in bufmgr.c */ - size = add_size(size, mul_size(NBuffers, sizeof(CkptSortItem))); - - return size; + /* Initialize per-backend file flush context */ + WritebackContextInit(&BackendWritebackContext, + &backend_flush_after); } diff --git a/src/backend/storage/buffer/buf_table.c b/src/backend/storage/buffer/buf_table.c index a50955d5286ca..347bf267d73fe 100644 --- a/src/backend/storage/buffer/buf_table.c +++ b/src/backend/storage/buffer/buf_table.c @@ -10,7 +10,7 @@ * before the lock is released (see notes in README). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -22,6 +22,7 @@ #include "postgres.h" #include "storage/buf_internals.h" +#include "storage/subsystems.h" /* entry for buffer lookup hashtable */ typedef struct @@ -32,37 +33,42 @@ typedef struct static HTAB *SharedBufHash; +static void BufTableShmemRequest(void *arg); -/* - * Estimate space needed for mapping hashtable - * size is the desired hash table size (possibly more than NBuffers) - */ -Size -BufTableShmemSize(int size) -{ - return hash_estimate_size(size, sizeof(BufferLookupEnt)); -} +const ShmemCallbacks BufTableShmemCallbacks = { + .request_fn = BufTableShmemRequest, + /* no special initialization needed, the hash table will start empty */ +}; /* - * Initialize shmem hash table for mapping buffers + * Register shmem hash table for mapping buffers. * size is the desired hash table size (possibly more than NBuffers) */ void -InitBufTable(int size) +BufTableShmemRequest(void *arg) { - HASHCTL info; - - /* assume no locking is needed yet */ - - /* BufferTag maps to Buffer */ - info.keysize = sizeof(BufferTag); - info.entrysize = sizeof(BufferLookupEnt); - info.num_partitions = NUM_BUFFER_PARTITIONS; - - SharedBufHash = ShmemInitHash("Shared Buffer Lookup Table", - size, size, - &info, - HASH_ELEM | HASH_BLOBS | HASH_PARTITION); + int size; + + /* + * Request the shared buffer lookup hashtable. + * + * Since we can't tolerate running out of lookup table entries, we must be + * sure to specify an adequate table size here. The maximum steady-state + * usage is of course NBuffers entries, but BufferAlloc() tries to insert + * a new entry before deleting the old. In principle this could be + * happening in each partition concurrently, so we could need as many as + * NBuffers + NUM_BUFFER_PARTITIONS entries. + */ + size = NBuffers + NUM_BUFFER_PARTITIONS; + + ShmemRequestHash(.name = "Shared Buffer Lookup Table", + .nelems = size, + .ptr = &SharedBufHash, + .hash_info.keysize = sizeof(BufferTag), + .hash_info.entrysize = sizeof(BufferLookupEnt), + .hash_info.num_partitions = NUM_BUFFER_PARTITIONS, + .hash_flags = HASH_ELEM | HASH_BLOBS | HASH_PARTITION | HASH_FIXED_SIZE, + ); } /* diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index f93131a645ea8..1878efb4aa998 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -3,7 +3,7 @@ * bufmgr.c * buffer manager interface routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -45,6 +45,7 @@ #endif #include "catalog/storage.h" #include "catalog/storage_xlog.h" +#include "common/hashfn.h" #include "executor/instrument.h" #include "lib/binaryheap.h" #include "miscadmin.h" @@ -58,6 +59,8 @@ #include "storage/ipc.h" #include "storage/lmgr.h" #include "storage/proc.h" +#include "storage/proclist.h" +#include "storage/procsignal.h" #include "storage/read_stream.h" #include "storage/smgr.h" #include "storage/standby.h" @@ -66,6 +69,7 @@ #include "utils/rel.h" #include "utils/resowner.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* Note: these two macros only work on shared buffers, not local ones! */ @@ -90,12 +94,53 @@ */ #define BUF_DROP_FULL_SCAN_THRESHOLD (uint64) (NBuffers / 32) +/* + * This is separated out from PrivateRefCountEntry to allow for copying all + * the data members via struct assignment. + */ +typedef struct PrivateRefCountData +{ + /* + * How many times has the buffer been pinned by this backend. + */ + int32 refcount; + + /* + * Is the buffer locked by this backend? BUFFER_LOCK_UNLOCK indicates that + * the buffer is not locked. + */ + BufferLockMode lockmode; +} PrivateRefCountData; + typedef struct PrivateRefCountEntry { + /* + * Note that this needs to be same as the entry's corresponding + * PrivateRefCountArrayKeys[i], if the entry is stored in the array. We + * store it in both places as this is used for the hashtable key and + * because it is more convenient (passing around a PrivateRefCountEntry + * suffices to identify the buffer) and faster (checking the keys array is + * faster when checking many entries, checking the entry is faster if just + * checking a single entry). + */ Buffer buffer; - int32 refcount; + + char status; + + PrivateRefCountData data; } PrivateRefCountEntry; +#define SH_PREFIX refcount +#define SH_ELEMENT_TYPE PrivateRefCountEntry +#define SH_KEY_TYPE Buffer +#define SH_KEY buffer +#define SH_HASH_KEY(tb, key) murmurhash32((uint32) (key)) +#define SH_EQUAL(tb, a, b) ((a) == (b)) +#define SH_SCOPE static inline +#define SH_DECLARE +#define SH_DEFINE +#include "lib/simplehash.h" + /* 64 bytes, about the size of a cache line on common systems */ #define REFCOUNT_ARRAY_ENTRIES 8 @@ -188,13 +233,16 @@ static BufferDesc *PinCountWaitBuf = NULL; * Each buffer also has a private refcount that keeps track of the number of * times the buffer is pinned in the current process. This is so that the * shared refcount needs to be modified only once if a buffer is pinned more - * than once by an individual backend. It's also used to check that no buffers - * are still pinned at the end of transactions and when exiting. + * than once by an individual backend. It's also used to check that no + * buffers are still pinned at the end of transactions and when exiting. We + * also use this mechanism to track whether this backend has a buffer locked, + * and, if so, in what mode. * * * To avoid - as we used to - requiring an array with NBuffers entries to keep * track of local buffers, we use a small sequentially searched array - * (PrivateRefCountArray) and an overflow hash table (PrivateRefCountHash) to + * (PrivateRefCountArrayKeys, with the corresponding data stored in + * PrivateRefCountArray) and an overflow hash table (PrivateRefCountHash) to * keep track of backend local pins. * * Until no more than REFCOUNT_ARRAY_ENTRIES buffers are pinned at once, all @@ -212,11 +260,13 @@ static BufferDesc *PinCountWaitBuf = NULL; * memory allocations in NewPrivateRefCountEntry() which can be important * because in some scenarios it's called with a spinlock held... */ +static Buffer PrivateRefCountArrayKeys[REFCOUNT_ARRAY_ENTRIES]; static struct PrivateRefCountEntry PrivateRefCountArray[REFCOUNT_ARRAY_ENTRIES]; -static HTAB *PrivateRefCountHash = NULL; +static refcount_hash *PrivateRefCountHash = NULL; static int32 PrivateRefCountOverflowed = 0; static uint32 PrivateRefCountClock = 0; -static PrivateRefCountEntry *ReservedRefCountEntry = NULL; +static int ReservedRefCountSlot = -1; +static int PrivateRefCountEntryLast = -1; static uint32 MaxProportionalPins; @@ -229,8 +279,8 @@ static void ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref); /* ResourceOwner callbacks to hold in-progress I/Os and buffer pins */ static void ResOwnerReleaseBufferIO(Datum res); static char *ResOwnerPrintBufferIO(Datum res); -static void ResOwnerReleaseBufferPin(Datum res); -static char *ResOwnerPrintBufferPin(Datum res); +static void ResOwnerReleaseBuffer(Datum res); +static char *ResOwnerPrintBuffer(Datum res); const ResourceOwnerDesc buffer_io_resowner_desc = { @@ -241,13 +291,13 @@ const ResourceOwnerDesc buffer_io_resowner_desc = .DebugPrint = ResOwnerPrintBufferIO }; -const ResourceOwnerDesc buffer_pin_resowner_desc = +const ResourceOwnerDesc buffer_resowner_desc = { - .name = "buffer pin", + .name = "buffer", .release_phase = RESOURCE_RELEASE_BEFORE_LOCKS, .release_priority = RELEASE_PRIO_BUFFER_PINS, - .ReleaseResource = ResOwnerReleaseBufferPin, - .DebugPrint = ResOwnerPrintBufferPin + .ReleaseResource = ResOwnerReleaseBuffer, + .DebugPrint = ResOwnerPrintBuffer }; /* @@ -259,7 +309,7 @@ static void ReservePrivateRefCountEntry(void) { /* Already reserved (or freed), nothing to do */ - if (ReservedRefCountEntry != NULL) + if (ReservedRefCountSlot != -1) return; /* @@ -271,16 +321,19 @@ ReservePrivateRefCountEntry(void) for (i = 0; i < REFCOUNT_ARRAY_ENTRIES; i++) { - PrivateRefCountEntry *res; - - res = &PrivateRefCountArray[i]; - - if (res->buffer == InvalidBuffer) + if (PrivateRefCountArrayKeys[i] == InvalidBuffer) { - ReservedRefCountEntry = res; - return; + ReservedRefCountSlot = i; + + /* + * We could return immediately, but iterating till the end of + * the array allows compiler-autovectorization. + */ } } + + if (ReservedRefCountSlot != -1) + return; } /* @@ -292,27 +345,37 @@ ReservePrivateRefCountEntry(void) * Move entry from the current clock position in the array into the * hashtable. Use that slot. */ + int victim_slot; + PrivateRefCountEntry *victim_entry; PrivateRefCountEntry *hashent; bool found; /* select victim slot */ - ReservedRefCountEntry = - &PrivateRefCountArray[PrivateRefCountClock++ % REFCOUNT_ARRAY_ENTRIES]; + victim_slot = PrivateRefCountClock++ % REFCOUNT_ARRAY_ENTRIES; + victim_entry = &PrivateRefCountArray[victim_slot]; + ReservedRefCountSlot = victim_slot; /* Better be used, otherwise we shouldn't get here. */ - Assert(ReservedRefCountEntry->buffer != InvalidBuffer); + Assert(PrivateRefCountArrayKeys[victim_slot] != InvalidBuffer); + Assert(PrivateRefCountArray[victim_slot].buffer != InvalidBuffer); + Assert(PrivateRefCountArrayKeys[victim_slot] == PrivateRefCountArray[victim_slot].buffer); /* enter victim array entry into hashtable */ - hashent = hash_search(PrivateRefCountHash, - &(ReservedRefCountEntry->buffer), - HASH_ENTER, - &found); + hashent = refcount_insert(PrivateRefCountHash, + PrivateRefCountArrayKeys[victim_slot], + &found); Assert(!found); - hashent->refcount = ReservedRefCountEntry->refcount; + /* move data from the entry in the array to the hash entry */ + hashent->data = victim_entry->data; /* clear the now free array slot */ - ReservedRefCountEntry->buffer = InvalidBuffer; - ReservedRefCountEntry->refcount = 0; + PrivateRefCountArrayKeys[victim_slot] = InvalidBuffer; + victim_entry->buffer = InvalidBuffer; + + /* clear the whole data member, just for future proofing */ + memset(&victim_entry->data, 0, sizeof(victim_entry->data)); + victim_entry->data.refcount = 0; + victim_entry->data.lockmode = BUFFER_LOCK_UNLOCK; PrivateRefCountOverflowed++; } @@ -327,45 +390,57 @@ NewPrivateRefCountEntry(Buffer buffer) PrivateRefCountEntry *res; /* only allowed to be called when a reservation has been made */ - Assert(ReservedRefCountEntry != NULL); + Assert(ReservedRefCountSlot != -1); /* use up the reserved entry */ - res = ReservedRefCountEntry; - ReservedRefCountEntry = NULL; + res = &PrivateRefCountArray[ReservedRefCountSlot]; /* and fill it */ + PrivateRefCountArrayKeys[ReservedRefCountSlot] = buffer; res->buffer = buffer; - res->refcount = 0; + res->data.refcount = 0; + res->data.lockmode = BUFFER_LOCK_UNLOCK; + + /* update cache for the next lookup */ + PrivateRefCountEntryLast = ReservedRefCountSlot; + + ReservedRefCountSlot = -1; return res; } /* - * Return the PrivateRefCount entry for the passed buffer. - * - * Returns NULL if a buffer doesn't have a refcount entry. Otherwise, if - * do_move is true, and the entry resides in the hashtable the entry is - * optimized for frequent access by moving it to the array. + * Slow-path for GetPrivateRefCountEntry(). This is big enough to not be worth + * inlining. This particularly seems to be true if the compiler is capable of + * auto-vectorizing the code, as that imposes additional stack-alignment + * requirements etc. */ -static PrivateRefCountEntry * -GetPrivateRefCountEntry(Buffer buffer, bool do_move) +static pg_noinline PrivateRefCountEntry * +GetPrivateRefCountEntrySlow(Buffer buffer, bool do_move) { PrivateRefCountEntry *res; + int match = -1; int i; - Assert(BufferIsValid(buffer)); - Assert(!BufferIsLocal(buffer)); - /* * First search for references in the array, that'll be sufficient in the * majority of cases. */ for (i = 0; i < REFCOUNT_ARRAY_ENTRIES; i++) { - res = &PrivateRefCountArray[i]; + if (PrivateRefCountArrayKeys[i] == buffer) + { + match = i; + /* see ReservePrivateRefCountEntry() for why we don't return */ + } + } + + if (likely(match != -1)) + { + /* update cache for the next lookup */ + PrivateRefCountEntryLast = match; - if (res->buffer == buffer) - return res; + return &PrivateRefCountArray[match]; } /* @@ -378,7 +453,7 @@ GetPrivateRefCountEntry(Buffer buffer, bool do_move) if (PrivateRefCountOverflowed == 0) return NULL; - res = hash_search(PrivateRefCountHash, &buffer, HASH_FIND, NULL); + res = refcount_lookup(PrivateRefCountHash, buffer); if (res == NULL) return NULL; @@ -390,32 +465,74 @@ GetPrivateRefCountEntry(Buffer buffer, bool do_move) else { /* move buffer from hashtable into the free array slot */ - bool found; PrivateRefCountEntry *free; + PrivateRefCountData data; + + /* Save data and delete from hashtable while res is still valid */ + data = res->data; + refcount_delete_item(PrivateRefCountHash, res); + Assert(PrivateRefCountOverflowed > 0); + PrivateRefCountOverflowed--; /* Ensure there's a free array slot */ ReservePrivateRefCountEntry(); /* Use up the reserved slot */ - Assert(ReservedRefCountEntry != NULL); - free = ReservedRefCountEntry; - ReservedRefCountEntry = NULL; + Assert(ReservedRefCountSlot != -1); + free = &PrivateRefCountArray[ReservedRefCountSlot]; + Assert(PrivateRefCountArrayKeys[ReservedRefCountSlot] == free->buffer); Assert(free->buffer == InvalidBuffer); /* and fill it */ free->buffer = buffer; - free->refcount = res->refcount; + free->data = data; + PrivateRefCountArrayKeys[ReservedRefCountSlot] = buffer; + /* update cache for the next lookup */ + PrivateRefCountEntryLast = ReservedRefCountSlot; - /* delete from hashtable */ - hash_search(PrivateRefCountHash, &buffer, HASH_REMOVE, &found); - Assert(found); - Assert(PrivateRefCountOverflowed > 0); - PrivateRefCountOverflowed--; + ReservedRefCountSlot = -1; return free; } } +/* + * Return the PrivateRefCount entry for the passed buffer. + * + * Returns NULL if a buffer doesn't have a refcount entry. Otherwise, if + * do_move is true, and the entry resides in the hashtable the entry is + * optimized for frequent access by moving it to the array. + */ +static inline PrivateRefCountEntry * +GetPrivateRefCountEntry(Buffer buffer, bool do_move) +{ + Assert(BufferIsValid(buffer)); + Assert(!BufferIsLocal(buffer)); + + /* + * It's very common to look up the same buffer repeatedly. To make that + * fast, we have a one-entry cache. + * + * In contrast to the loop in GetPrivateRefCountEntrySlow(), here it + * faster to check PrivateRefCountArray[].buffer, as in the case of a hit + * fewer addresses are computed and fewer cachelines are accessed. Whereas + * in GetPrivateRefCountEntrySlow()'s case, checking + * PrivateRefCountArrayKeys saves a lot of memory accesses. + */ + if (likely(PrivateRefCountEntryLast != -1) && + likely(PrivateRefCountArray[PrivateRefCountEntryLast].buffer == buffer)) + { + return &PrivateRefCountArray[PrivateRefCountEntryLast]; + } + + /* + * The code for the cached lookup is small enough to be worth inlining + * into the caller. In the miss case however, that empirically doesn't + * seem worth it. + */ + return GetPrivateRefCountEntrySlow(buffer, do_move); +} + /* * Returns how many times the passed buffer is pinned by this backend. * @@ -437,7 +554,7 @@ GetPrivateRefCount(Buffer buffer) if (ref == NULL) return 0; - return ref->refcount; + return ref->data.refcount; } /* @@ -447,27 +564,26 @@ GetPrivateRefCount(Buffer buffer) static void ForgetPrivateRefCountEntry(PrivateRefCountEntry *ref) { - Assert(ref->refcount == 0); + Assert(ref->data.refcount == 0); + Assert(ref->data.lockmode == BUFFER_LOCK_UNLOCK); if (ref >= &PrivateRefCountArray[0] && ref < &PrivateRefCountArray[REFCOUNT_ARRAY_ENTRIES]) { ref->buffer = InvalidBuffer; + PrivateRefCountArrayKeys[ref - PrivateRefCountArray] = InvalidBuffer; + /* * Mark the just used entry as reserved - in many scenarios that * allows us to avoid ever having to search the array/hash for free * entries. */ - ReservedRefCountEntry = ref; + ReservedRefCountSlot = ref - PrivateRefCountArray; } else { - bool found; - Buffer buffer = ref->buffer; - - hash_search(PrivateRefCountHash, &buffer, HASH_REMOVE, &found); - Assert(found); + refcount_delete_item(PrivateRefCountHash, ref); Assert(PrivateRefCountOverflowed > 0); PrivateRefCountOverflowed--; } @@ -512,12 +628,12 @@ static BlockNumber ExtendBufferedRelShared(BufferManagerRelation bmr, BlockNumber extend_upto, Buffer *buffers, uint32 *extended_by); -static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy); +static bool PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy, + bool skip_if_not_valid); static void PinBuffer_Locked(BufferDesc *buf); static void UnpinBuffer(BufferDesc *buf); static void UnpinBufferNoOwner(BufferDesc *buf); static void BufferSync(int flags); -static uint32 WaitBufHdrUnlocked(BufferDesc *buf); static int SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context); static void WaitIO(BufferDesc *buf); @@ -532,7 +648,14 @@ static inline BufferDesc *BufferAlloc(SMgrRelation smgr, bool *foundPtr, IOContext io_context); static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress); static void CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete); + +static pg_attribute_always_inline void TrackBufferHit(IOObject io_object, + IOContext io_context, + Relation rel, char persistence, SMgrRelation smgr, + ForkNumber forknum, BlockNumber blocknum); static Buffer GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context); +static void FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln, + IOObject io_object, IOContext io_context); static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, IOContext io_context); static void FindAndDropRelationBuffers(RelFileLocator rlocator, @@ -545,14 +668,27 @@ static void RelationCopyStorageUsingBuffer(RelFileLocator srclocator, static void AtProcExit_Buffers(int code, Datum arg); static void CheckForBufferLeaks(void); #ifdef USE_ASSERT_CHECKING -static void AssertNotCatalogBufferLock(LWLock *lock, LWLockMode mode, - void *unused_context); +static void AssertNotCatalogBufferLock(Buffer buffer, BufferLockMode mode); #endif static int rlocator_comparator(const void *p1, const void *p2); static inline int buffertag_comparator(const BufferTag *ba, const BufferTag *bb); static inline int ckpt_buforder_comparator(const CkptSortItem *a, const CkptSortItem *b); static int ts_ckpt_progress_comparator(Datum a, Datum b, void *arg); +static void BufferLockAcquire(Buffer buffer, BufferDesc *buf_hdr, BufferLockMode mode); +static void BufferLockUnlock(Buffer buffer, BufferDesc *buf_hdr); +static bool BufferLockConditional(Buffer buffer, BufferDesc *buf_hdr, BufferLockMode mode); +static bool BufferLockHeldByMeInMode(BufferDesc *buf_hdr, BufferLockMode mode); +static bool BufferLockHeldByMe(BufferDesc *buf_hdr); +static inline void BufferLockDisown(Buffer buffer, BufferDesc *buf_hdr); +static inline int BufferLockDisownInternal(Buffer buffer, BufferDesc *buf_hdr); +static inline bool BufferLockAttempt(BufferDesc *buf_hdr, BufferLockMode mode); +static void BufferLockQueueSelf(BufferDesc *buf_hdr, BufferLockMode mode); +static void BufferLockDequeueSelf(BufferDesc *buf_hdr); +static void BufferLockWakeup(BufferDesc *buf_hdr, bool wake_exclusive); +static void BufferLockProcessRelease(BufferDesc *buf_hdr, BufferLockMode mode, uint64 lockstate); +static inline uint64 BufferLockReleaseSub(BufferLockMode mode); + /* * Implementation of PrefetchBuffer() for shared buffers. @@ -684,8 +820,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN { BufferDesc *bufHdr; BufferTag tag; - uint32 buf_state; - bool have_private_ref; + uint64 buf_state; Assert(BufferIsValid(recent_buffer)); @@ -698,7 +833,7 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN int b = -recent_buffer - 1; bufHdr = GetLocalBufferDescriptor(b); - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); /* Is it still valid and holding the right tag? */ if ((buf_state & BM_VALID) && BufferTagsEqual(&tag, &bufHdr->tag)) @@ -713,38 +848,24 @@ ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockN else { bufHdr = GetBufferDescriptor(recent_buffer - 1); - have_private_ref = GetPrivateRefCount(recent_buffer) > 0; /* - * Do we already have this buffer pinned with a private reference? If - * so, it must be valid and it is safe to check the tag without - * locking. If not, we have to lock the header first and then check. + * Is it still valid and holding the right tag? We do an unlocked tag + * comparison first, to make it unlikely that we'll increment the + * usage counter of the wrong buffer, if someone calls us with a very + * out of date recent_buffer. Then we'll check it again if we get the + * pin. */ - if (have_private_ref) - buf_state = pg_atomic_read_u32(&bufHdr->state); - else - buf_state = LockBufHdr(bufHdr); - - if ((buf_state & BM_VALID) && BufferTagsEqual(&tag, &bufHdr->tag)) + if (BufferTagsEqual(&tag, &bufHdr->tag) && + PinBuffer(bufHdr, NULL, true)) { - /* - * It's now safe to pin the buffer. We can't pin first and ask - * questions later, because it might confuse code paths like - * InvalidateBuffer() if we pinned a random non-matching buffer. - */ - if (have_private_ref) - PinBuffer(bufHdr, NULL); /* bump pin count */ - else - PinBuffer_Locked(bufHdr); /* pin for first time */ - - pgBufferUsage.shared_blks_hit++; - - return true; + if (BufferTagsEqual(&tag, &bufHdr->tag)) + { + pgBufferUsage.shared_blks_hit++; + return true; + } + UnpinBuffer(bufHdr); } - - /* If we locked the header above, now unlock. */ - if (!have_private_ref) - UnlockBufHdr(bufHdr, buf_state); } return false; @@ -896,14 +1017,11 @@ ExtendBufferedRelBy(BufferManagerRelation bmr, uint32 *extended_by) { Assert((bmr.rel != NULL) != (bmr.smgr != NULL)); - Assert(bmr.smgr == NULL || bmr.relpersistence != 0); + Assert(bmr.smgr == NULL || bmr.relpersistence != '\0'); Assert(extend_by > 0); - if (bmr.smgr == NULL) - { - bmr.smgr = RelationGetSmgr(bmr.rel); + if (bmr.relpersistence == '\0') bmr.relpersistence = bmr.rel->rd_rel->relpersistence; - } return ExtendBufferedRelCommon(bmr, fork, strategy, flags, extend_by, InvalidBlockNumber, @@ -932,14 +1050,11 @@ ExtendBufferedRelTo(BufferManagerRelation bmr, Buffer buffers[64]; Assert((bmr.rel != NULL) != (bmr.smgr != NULL)); - Assert(bmr.smgr == NULL || bmr.relpersistence != 0); + Assert(bmr.smgr == NULL || bmr.relpersistence != '\0'); Assert(extend_to != InvalidBlockNumber && extend_to > 0); - if (bmr.smgr == NULL) - { - bmr.smgr = RelationGetSmgr(bmr.rel); + if (bmr.relpersistence == '\0') bmr.relpersistence = bmr.rel->rd_rel->relpersistence; - } /* * If desired, create the file if it doesn't exist. If @@ -947,15 +1062,15 @@ ExtendBufferedRelTo(BufferManagerRelation bmr, * an smgrexists call. */ if ((flags & EB_CREATE_FORK_IF_NEEDED) && - (bmr.smgr->smgr_cached_nblocks[fork] == 0 || - bmr.smgr->smgr_cached_nblocks[fork] == InvalidBlockNumber) && - !smgrexists(bmr.smgr, fork)) + (BMR_GET_SMGR(bmr)->smgr_cached_nblocks[fork] == 0 || + BMR_GET_SMGR(bmr)->smgr_cached_nblocks[fork] == InvalidBlockNumber) && + !smgrexists(BMR_GET_SMGR(bmr), fork)) { LockRelationForExtension(bmr.rel, ExclusiveLock); /* recheck, fork might have been created concurrently */ - if (!smgrexists(bmr.smgr, fork)) - smgrcreate(bmr.smgr, fork, flags & EB_PERFORMING_RECOVERY); + if (!smgrexists(BMR_GET_SMGR(bmr), fork)) + smgrcreate(BMR_GET_SMGR(bmr), fork, flags & EB_PERFORMING_RECOVERY); UnlockRelationForExtension(bmr.rel, ExclusiveLock); } @@ -965,13 +1080,13 @@ ExtendBufferedRelTo(BufferManagerRelation bmr, * kernel. */ if (flags & EB_CLEAR_SIZE_CACHE) - bmr.smgr->smgr_cached_nblocks[fork] = InvalidBlockNumber; + BMR_GET_SMGR(bmr)->smgr_cached_nblocks[fork] = InvalidBlockNumber; /* * Estimate how many pages we'll need to extend by. This avoids acquiring * unnecessarily many victim buffers. */ - current_size = smgrnblocks(bmr.smgr, fork); + current_size = smgrnblocks(BMR_GET_SMGR(bmr), fork); /* * Since no-one else can be looking at the page contents yet, there is no @@ -1015,7 +1130,7 @@ ExtendBufferedRelTo(BufferManagerRelation bmr, if (buffer == InvalidBuffer) { Assert(extended_by == 0); - buffer = ReadBuffer_common(bmr.rel, bmr.smgr, bmr.relpersistence, + buffer = ReadBuffer_common(bmr.rel, BMR_GET_SMGR(bmr), bmr.relpersistence, fork, extend_to - 1, mode, strategy); } @@ -1033,6 +1148,7 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid) BufferDesc *bufHdr; bool need_to_zero; bool isLocalBuf = BufferIsLocal(buffer); + StartBufferIOResult sbres; Assert(mode == RBM_ZERO_AND_LOCK || mode == RBM_ZERO_AND_CLEANUP_LOCK); @@ -1044,24 +1160,30 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid) */ need_to_zero = false; } - else if (isLocalBuf) - { - /* Simple case for non-shared buffers. */ - bufHdr = GetLocalBufferDescriptor(-buffer - 1); - need_to_zero = StartLocalBufferIO(bufHdr, true, false); - } else { - /* - * Take BM_IO_IN_PROGRESS, or discover that BM_VALID has been set - * concurrently. Even though we aren't doing I/O, that ensures that - * we don't zero a page that someone else has pinned. An exclusive - * content lock wouldn't be enough, because readers are allowed to - * drop the content lock after determining that a tuple is visible - * (see buffer access rules in README). - */ - bufHdr = GetBufferDescriptor(buffer - 1); - need_to_zero = StartBufferIO(bufHdr, true, false); + if (isLocalBuf) + { + /* Simple case for non-shared buffers. */ + bufHdr = GetLocalBufferDescriptor(-buffer - 1); + sbres = StartLocalBufferIO(bufHdr, true, true, NULL); + } + else + { + /* + * Take BM_IO_IN_PROGRESS, or discover that BM_VALID has been set + * concurrently. Even though we aren't doing I/O, that ensures + * that we don't zero a page that someone else has pinned. An + * exclusive content lock wouldn't be enough, because readers are + * allowed to drop the content lock after determining that a tuple + * is visible (see buffer access rules in README). + */ + bufHdr = GetBufferDescriptor(buffer - 1); + sbres = StartSharedBufferIO(bufHdr, true, true, NULL); + } + + Assert(sbres != BUFFER_IO_IN_PROGRESS); + need_to_zero = sbres == BUFFER_IO_READY_FOR_IO; } if (need_to_zero) @@ -1080,7 +1202,7 @@ ZeroAndLockBuffer(Buffer buffer, ReadBufferMode mode, bool already_valid) * already valid.) */ if (!isLocalBuf) - LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_EXCLUSIVE); + LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); /* Set BM_VALID, terminate IO, and wake up any waiters */ if (isLocalBuf) @@ -1113,11 +1235,11 @@ PinBufferForBlock(Relation rel, ForkNumber forkNum, BlockNumber blockNum, BufferAccessStrategy strategy, + IOObject io_object, + IOContext io_context, bool *foundPtr) { BufferDesc *bufHdr; - IOContext io_context; - IOObject io_object; Assert(blockNum != P_NEW); @@ -1126,17 +1248,6 @@ PinBufferForBlock(Relation rel, persistence == RELPERSISTENCE_PERMANENT || persistence == RELPERSISTENCE_UNLOGGED)); - if (persistence == RELPERSISTENCE_TEMP) - { - io_context = IOCONTEXT_NORMAL; - io_object = IOOBJECT_TEMP_RELATION; - } - else - { - io_context = IOContextForStrategy(strategy); - io_object = IOOBJECT_RELATION; - } - TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum, smgr->smgr_rlocator.locator.spcOid, smgr->smgr_rlocator.locator.dbOid, @@ -1144,18 +1255,14 @@ PinBufferForBlock(Relation rel, smgr->smgr_rlocator.backend); if (persistence == RELPERSISTENCE_TEMP) - { bufHdr = LocalBufferAlloc(smgr, forkNum, blockNum, foundPtr); - if (*foundPtr) - pgBufferUsage.local_blks_hit++; - } else - { bufHdr = BufferAlloc(smgr, persistence, forkNum, blockNum, strategy, foundPtr, io_context); - if (*foundPtr) - pgBufferUsage.shared_blks_hit++; - } + + if (*foundPtr) + TrackBufferHit(io_object, io_context, rel, persistence, smgr, forkNum, blockNum); + if (rel) { /* @@ -1164,21 +1271,6 @@ PinBufferForBlock(Relation rel, * zeroed instead), the per-relation stats always count them. */ pgstat_count_buffer_read(rel); - if (*foundPtr) - pgstat_count_buffer_hit(rel); - } - if (*foundPtr) - { - pgstat_count_io_op(io_object, io_context, IOOP_HIT, 1, 0); - if (VacuumCostActive) - VacuumCostBalance += VacuumCostPageHit; - - TRACE_POSTGRESQL_BUFFER_READ_DONE(forkNum, blockNum, - smgr->smgr_rlocator.locator.spcOid, - smgr->smgr_rlocator.locator.dbOid, - smgr->smgr_rlocator.locator.relNumber, - smgr->smgr_rlocator.backend, - true); } return BufferDescriptorGetBuffer(bufHdr); @@ -1229,9 +1321,23 @@ ReadBuffer_common(Relation rel, SMgrRelation smgr, char smgr_persistence, mode == RBM_ZERO_AND_LOCK)) { bool found; + IOContext io_context; + IOObject io_object; + + if (persistence == RELPERSISTENCE_TEMP) + { + io_context = IOCONTEXT_NORMAL; + io_object = IOOBJECT_TEMP_RELATION; + } + else + { + io_context = IOContextForStrategy(strategy); + io_object = IOOBJECT_RELATION; + } buffer = PinBufferForBlock(rel, smgr, persistence, - forkNum, blockNum, strategy, &found); + forkNum, blockNum, strategy, + io_object, io_context, &found); ZeroAndLockBuffer(buffer, mode, found); return buffer; } @@ -1269,11 +1375,24 @@ StartReadBuffersImpl(ReadBuffersOperation *operation, int actual_nblocks = *nblocks; int maxcombine = 0; bool did_start_io; + IOContext io_context; + IOObject io_object; Assert(*nblocks == 1 || allow_forwarding); Assert(*nblocks > 0); Assert(*nblocks <= MAX_IO_COMBINE_LIMIT); + if (operation->persistence == RELPERSISTENCE_TEMP) + { + io_context = IOCONTEXT_NORMAL; + io_object = IOOBJECT_TEMP_RELATION; + } + else + { + io_context = IOContextForStrategy(operation->strategy); + io_object = IOOBJECT_RELATION; + } + for (int i = 0; i < actual_nblocks; ++i) { bool found; @@ -1311,8 +1430,8 @@ StartReadBuffersImpl(ReadBuffersOperation *operation, bufHdr = GetLocalBufferDescriptor(-buffers[i] - 1); else bufHdr = GetBufferDescriptor(buffers[i] - 1); - Assert(pg_atomic_read_u32(&bufHdr->state) & BM_TAG_VALID); - found = pg_atomic_read_u32(&bufHdr->state) & BM_VALID; + Assert(pg_atomic_read_u64(&bufHdr->state) & BM_TAG_VALID); + found = pg_atomic_read_u64(&bufHdr->state) & BM_VALID; } else { @@ -1322,6 +1441,7 @@ StartReadBuffersImpl(ReadBuffersOperation *operation, operation->forknum, blockNum + i, operation->strategy, + io_object, io_context, &found); } @@ -1484,11 +1604,6 @@ StartReadBuffersImpl(ReadBuffersOperation *operation, * buffers must remain valid until WaitReadBuffers() is called, and any * forwarded buffers must also be preserved for a continuing call unless * they are explicitly released. - * - * Currently the I/O is only started with optional operating system advice if - * requested by the caller with READ_BUFFERS_ISSUE_ADVICE, and the real I/O - * happens synchronously in WaitReadBuffers(). In future work, true I/O could - * be initiated here. */ bool StartReadBuffers(ReadBuffersOperation *operation, @@ -1543,51 +1658,43 @@ CheckReadBuffersOperation(ReadBuffersOperation *operation, bool is_complete) GetBufferDescriptor(buffer - 1); Assert(BufferGetBlockNumber(buffer) == operation->blocknum + i); - Assert(pg_atomic_read_u32(&buf_hdr->state) & BM_TAG_VALID); + Assert(pg_atomic_read_u64(&buf_hdr->state) & BM_TAG_VALID); if (i < operation->nblocks_done) - Assert(pg_atomic_read_u32(&buf_hdr->state) & BM_VALID); + Assert(pg_atomic_read_u64(&buf_hdr->state) & BM_VALID); } #endif } -/* helper for ReadBuffersCanStartIO(), to avoid repetition */ -static inline bool -ReadBuffersCanStartIOOnce(Buffer buffer, bool nowait) -{ - if (BufferIsLocal(buffer)) - return StartLocalBufferIO(GetLocalBufferDescriptor(-buffer - 1), - true, nowait); - else - return StartBufferIO(GetBufferDescriptor(buffer - 1), true, nowait); -} - /* - * Helper for AsyncReadBuffers that tries to get the buffer ready for IO. + * We track various stats related to buffer hits. Because this is done in a + * few separate places, this helper exists for convenience. */ -static inline bool -ReadBuffersCanStartIO(Buffer buffer, bool nowait) +static pg_attribute_always_inline void +TrackBufferHit(IOObject io_object, IOContext io_context, + Relation rel, char persistence, SMgrRelation smgr, + ForkNumber forknum, BlockNumber blocknum) { - /* - * If this backend currently has staged IO, we need to submit the pending - * IO before waiting for the right to issue IO, to avoid the potential for - * deadlocks (and, more commonly, unnecessary delays for other backends). - */ - if (!nowait && pgaio_have_staged()) - { - if (ReadBuffersCanStartIOOnce(buffer, true)) - return true; + TRACE_POSTGRESQL_BUFFER_READ_DONE(forknum, + blocknum, + smgr->smgr_rlocator.locator.spcOid, + smgr->smgr_rlocator.locator.dbOid, + smgr->smgr_rlocator.locator.relNumber, + smgr->smgr_rlocator.backend, + true); - /* - * Unfortunately StartBufferIO() returning false doesn't allow to - * distinguish between the buffer already being valid and IO already - * being in progress. Since IO already being in progress is quite - * rare, this approach seems fine. - */ - pgaio_submit_staged(); - } + if (persistence == RELPERSISTENCE_TEMP) + pgBufferUsage.local_blks_hit += 1; + else + pgBufferUsage.shared_blks_hit += 1; + + pgstat_count_io_op(io_object, io_context, IOOP_HIT, 1, 0); - return ReadBuffersCanStartIOOnce(buffer, nowait); + if (VacuumCostActive) + VacuumCostBalance += VacuumCostPageHit; + + if (rel) + pgstat_count_buffer_hit(rel); } /* @@ -1633,12 +1740,19 @@ ProcessReadBuffersResult(ReadBuffersOperation *operation) Assert(operation->nblocks_done <= operation->nblocks); } -void +/* + * Wait for the IO operation initiated by StartReadBuffers() et al to + * complete. + * + * Returns true if we needed to wait for the IO operation, false otherwise. + */ +bool WaitReadBuffers(ReadBuffersOperation *operation) { PgAioReturn *aio_ret = &operation->io_return; IOContext io_context; IOObject io_object; + bool needed_wait = false; if (operation->persistence == RELPERSISTENCE_TEMP) { @@ -1693,13 +1807,17 @@ WaitReadBuffers(ReadBuffersOperation *operation) * b) reports some time as waiting, even if we never waited * * we first check if we already know the IO is complete. + * + * Note that operation->io_return is uninitialized for foreign IO, + * so we cannot use the cheaper PGAIO_RS_UNKNOWN pre-check. */ - if (aio_ret->result.status == PGAIO_RS_UNKNOWN && + if ((operation->foreign_io || aio_ret->result.status == PGAIO_RS_UNKNOWN) && !pgaio_wref_check_done(&operation->io_wref)) { instr_time io_start = pgstat_prepare_io_time(track_io_timing); pgaio_wref_wait(&operation->io_wref); + needed_wait = true; /* * The IO operation itself was already counted earlier, in @@ -1713,11 +1831,45 @@ WaitReadBuffers(ReadBuffersOperation *operation) Assert(pgaio_wref_check_done(&operation->io_wref)); } - /* - * We now are sure the IO completed. Check the results. This - * includes reporting on errors if there were any. - */ - ProcessReadBuffersResult(operation); + if (unlikely(operation->foreign_io)) + { + Buffer buffer = operation->buffers[operation->nblocks_done]; + BufferDesc *desc = BufferIsLocal(buffer) ? + GetLocalBufferDescriptor(-buffer - 1) : + GetBufferDescriptor(buffer - 1); + uint64 buf_state = pg_atomic_read_u64(&desc->state); + + if (buf_state & BM_VALID) + { + BlockNumber blocknum = operation->blocknum + operation->nblocks_done; + + operation->nblocks_done += 1; + Assert(operation->nblocks_done <= operation->nblocks); + + /* + * Track this as a 'hit' for this backend. The backend + * performing the IO will track it as a 'read'. + */ + TrackBufferHit(io_object, io_context, + operation->rel, operation->persistence, + operation->smgr, operation->forknum, + blocknum); + } + + /* + * If the foreign IO failed and left the buffer invalid, + * nblocks_done is not incremented. The retry loop below will + * call AsyncReadBuffers() which will attempt the IO itself. + */ + } + else + { + /* + * We now are sure the IO completed. Check the results. This + * includes reporting on errors if there were any. + */ + ProcessReadBuffersResult(operation); + } } /* @@ -1730,6 +1882,12 @@ WaitReadBuffers(ReadBuffersOperation *operation) CHECK_FOR_INTERRUPTS(); + /* + * If the IO completed only partially, we need to perform additional + * work, consider that a form of having had to wait. + */ + needed_wait = true; + /* * This may only complete the IO partially, either because some * buffers were already valid, or because of a partial read. @@ -1746,6 +1904,7 @@ WaitReadBuffers(ReadBuffersOperation *operation) CheckReadBuffersOperation(operation, true); /* NB: READ_DONE tracepoint was already executed in completion callback */ + return needed_wait; } /* @@ -1763,17 +1922,18 @@ WaitReadBuffers(ReadBuffersOperation *operation) * affected by the call. If the first buffer is valid, *nblocks_progress is * set to 1 and operation->nblocks_done is incremented. * - * Returns true if IO was initiated, false if no IO was necessary. + * Returns true if IO was initiated or is already in progress (foreign IO), + * false if the buffer was already valid. */ static bool AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) { Buffer *buffers = &operation->buffers[0]; int flags = operation->flags; - BlockNumber blocknum = operation->blocknum; ForkNumber forknum = operation->forknum; char persistence = operation->persistence; int16 nblocks_done = operation->nblocks_done; + BlockNumber blocknum = operation->blocknum + nblocks_done; Buffer *io_buffers = &operation->buffers[nblocks_done]; int io_buffers_len = 0; PgAioHandle *ioh; @@ -1781,21 +1941,13 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) void *io_pages[MAX_IO_COMBINE_LIMIT]; IOContext io_context; IOObject io_object; - bool did_start_io; - - /* - * When this IO is executed synchronously, either because the caller will - * immediately block waiting for the IO or because IOMETHOD_SYNC is used, - * the AIO subsystem needs to know. - */ - if (flags & READ_BUFFERS_SYNCHRONOUSLY) - ioh_flags |= PGAIO_HF_SYNCHRONOUS; + instr_time io_start; + StartBufferIOResult status; if (persistence == RELPERSISTENCE_TEMP) { io_context = IOCONTEXT_NORMAL; io_object = IOOBJECT_TEMP_RELATION; - ioh_flags |= PGAIO_HF_REFERENCES_LOCAL; } else { @@ -1803,6 +1955,17 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) io_object = IOOBJECT_RELATION; } + /* + * When this IO is executed synchronously, either because the caller will + * immediately block waiting for the IO or because IOMETHOD_SYNC is used, + * the AIO subsystem needs to know. + */ + if (flags & READ_BUFFERS_SYNCHRONOUSLY) + ioh_flags |= PGAIO_HF_SYNCHRONOUS; + + if (persistence == RELPERSISTENCE_TEMP) + ioh_flags |= PGAIO_HF_REFERENCES_LOCAL; + /* * If zero_damaged_pages is enabled, add the READ_BUFFERS_ZERO_ON_ERROR * flag. The reason for that is that, hopefully, zero_damaged_pages isn't @@ -1833,8 +1996,9 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) pgstat_prepare_report_checksum_failure(operation->smgr->smgr_rlocator.locator.dbOid); /* - * Get IO handle before ReadBuffersCanStartIO(), as pgaio_io_acquire() - * might block, which we don't want after setting IO_IN_PROGRESS. + * We must get an IO handle before StartBufferIO(), as pgaio_io_acquire() + * might block, which we don't want after setting IO_IN_PROGRESS. If we + * don't need to do the IO, we'll release the handle. * * If we need to wait for IO before we can get a handle, submit * already-staged IO first, so that other backends don't need to wait. @@ -1853,133 +2017,152 @@ AsyncReadBuffers(ReadBuffersOperation *operation, int *nblocks_progress) if (unlikely(!ioh)) { pgaio_submit_staged(); - ioh = pgaio_io_acquire(CurrentResourceOwner, &operation->io_return); } + operation->foreign_io = false; + pgaio_wref_clear(&operation->io_wref); + /* - * Check if we can start IO on the first to-be-read buffer. + * Try to start IO on the first buffer in a new run of blocks. If AIO is + * in progress, be it in this backend or another backend, we just + * associate the wait reference with the operation and wait in + * WaitReadBuffers(). This turns out to be important for performance in + * two workloads: + * + * 1) A read stream that has to read the same block multiple times within + * the readahead distance. This can happen e.g. for the table accesses of + * an index scan. + * + * 2) Concurrent scans by multiple backends on the same relation. + * + * If we were to synchronously wait for the in-progress IO, we'd not be + * able to keep enough I/O in flight. * - * If an I/O is already in progress in another backend, we want to wait - * for the outcome: either done, or something went wrong and we will - * retry. + * If we do find there is ongoing I/O for the buffer, we set up a 1-block + * ReadBuffersOperation that WaitReadBuffers then can wait on. + * + * It's possible that another backend has started IO on the buffer but not + * yet set its wait reference. In this case, we have no choice but to wait + * for either the wait reference to be valid or the IO to be done. */ - if (!ReadBuffersCanStartIO(buffers[nblocks_done], false)) + status = StartBufferIO(buffers[nblocks_done], true, true, + &operation->io_wref); + if (status != BUFFER_IO_READY_FOR_IO) { - /* - * Someone else has already completed this block, we're done. - * - * When IO is necessary, ->nblocks_done is updated in - * ProcessReadBuffersResult(), but that is not called if no IO is - * necessary. Thus update here. - */ - operation->nblocks_done += 1; - *nblocks_progress = 1; - pgaio_io_release(ioh); - pgaio_wref_clear(&operation->io_wref); - did_start_io = false; - - /* - * Report and track this as a 'hit' for this backend, even though it - * must have started out as a miss in PinBufferForBlock(). The other - * backend will track this as a 'read'. - */ - TRACE_POSTGRESQL_BUFFER_READ_DONE(forknum, blocknum + operation->nblocks_done, - operation->smgr->smgr_rlocator.locator.spcOid, - operation->smgr->smgr_rlocator.locator.dbOid, - operation->smgr->smgr_rlocator.locator.relNumber, - operation->smgr->smgr_rlocator.backend, - true); + *nblocks_progress = 1; + if (status == BUFFER_IO_ALREADY_DONE) + { + /* + * Someone has already completed this block, we're done. + * + * When IO is necessary, ->nblocks_done is updated in + * ProcessReadBuffersResult(), but that is not called if no IO is + * necessary. Thus update here. + */ + operation->nblocks_done += 1; + Assert(operation->nblocks_done <= operation->nblocks); - if (persistence == RELPERSISTENCE_TEMP) - pgBufferUsage.local_blks_hit += 1; - else - pgBufferUsage.shared_blks_hit += 1; + Assert(!pgaio_wref_valid(&operation->io_wref)); - if (operation->rel) - pgstat_count_buffer_hit(operation->rel); + /* + * Report and track this as a 'hit' for this backend, even though + * it must have started out as a miss in PinBufferForBlock(). The + * other backend will track this as a 'read'. + */ + TrackBufferHit(io_object, io_context, + operation->rel, operation->persistence, + operation->smgr, operation->forknum, + blocknum); + return false; + } - pgstat_count_io_op(io_object, io_context, IOOP_HIT, 1, 0); + /* The IO is already in-progress */ + Assert(status == BUFFER_IO_IN_PROGRESS); + Assert(pgaio_wref_valid(&operation->io_wref)); + operation->foreign_io = true; - if (VacuumCostActive) - VacuumCostBalance += VacuumCostPageHit; + return true; } - else - { - instr_time io_start; - /* We found a buffer that we need to read in. */ - Assert(io_buffers[0] == buffers[nblocks_done]); - io_pages[0] = BufferGetBlock(buffers[nblocks_done]); - io_buffers_len = 1; + Assert(io_buffers[0] == buffers[nblocks_done]); + io_pages[0] = BufferGetBlock(buffers[nblocks_done]); + io_buffers_len = 1; - /* - * How many neighboring-on-disk blocks can we scatter-read into other - * buffers at the same time? In this case we don't wait if we see an - * I/O already in progress. We already set BM_IO_IN_PROGRESS for the - * head block, so we should get on with that I/O as soon as possible. - */ - for (int i = nblocks_done + 1; i < operation->nblocks; i++) - { - if (!ReadBuffersCanStartIO(buffers[i], true)) - break; - /* Must be consecutive block numbers. */ - Assert(BufferGetBlockNumber(buffers[i - 1]) == - BufferGetBlockNumber(buffers[i]) - 1); - Assert(io_buffers[io_buffers_len] == buffers[i]); + /* + * NB: As little code as possible should be added between the + * StartBufferIO() above, the further StartBufferIO()s below and the + * smgrstartreadv(), as some of the buffers are now marked as + * IO_IN_PROGRESS and will thus cause other backends to wait. + */ - io_pages[io_buffers_len++] = BufferGetBlock(buffers[i]); - } + /* + * How many neighboring-on-disk blocks can we scatter-read into other + * buffers at the same time? In this case we don't wait if we see an I/O + * already in progress (see comment above). + */ + for (int i = nblocks_done + 1; i < operation->nblocks; i++) + { + /* Must be consecutive block numbers. */ + Assert(BufferGetBlockNumber(buffers[i - 1]) == + BufferGetBlockNumber(buffers[i]) - 1); - /* get a reference to wait for in WaitReadBuffers() */ - pgaio_io_get_wref(ioh, &operation->io_wref); + status = StartBufferIO(buffers[i], true, false, NULL); + if (status != BUFFER_IO_READY_FOR_IO) + break; - /* provide the list of buffers to the completion callbacks */ - pgaio_io_set_handle_data_32(ioh, (uint32 *) io_buffers, io_buffers_len); + Assert(io_buffers[io_buffers_len] == buffers[i]); - pgaio_io_register_callbacks(ioh, - persistence == RELPERSISTENCE_TEMP ? - PGAIO_HCB_LOCAL_BUFFER_READV : - PGAIO_HCB_SHARED_BUFFER_READV, - flags); + io_pages[io_buffers_len++] = BufferGetBlock(buffers[i]); + } - pgaio_io_set_flag(ioh, ioh_flags); + /* get a reference to wait for in WaitReadBuffers() */ + pgaio_io_get_wref(ioh, &operation->io_wref); - /* --- - * Even though we're trying to issue IO asynchronously, track the time - * in smgrstartreadv(): - * - if io_method == IOMETHOD_SYNC, we will always perform the IO - * immediately - * - the io method might not support the IO (e.g. worker IO for a temp - * table) - * --- - */ - io_start = pgstat_prepare_io_time(track_io_timing); - smgrstartreadv(ioh, operation->smgr, forknum, - blocknum + nblocks_done, - io_pages, io_buffers_len); - pgstat_count_io_op_time(io_object, io_context, IOOP_READ, - io_start, 1, io_buffers_len * BLCKSZ); + /* provide the list of buffers to the completion callbacks */ + pgaio_io_set_handle_data_32(ioh, (uint32 *) io_buffers, io_buffers_len); - if (persistence == RELPERSISTENCE_TEMP) - pgBufferUsage.local_blks_read += io_buffers_len; - else - pgBufferUsage.shared_blks_read += io_buffers_len; + pgaio_io_register_callbacks(ioh, + persistence == RELPERSISTENCE_TEMP ? + PGAIO_HCB_LOCAL_BUFFER_READV : + PGAIO_HCB_SHARED_BUFFER_READV, + flags); - /* - * Track vacuum cost when issuing IO, not after waiting for it. - * Otherwise we could end up issuing a lot of IO in a short timespan, - * despite a low cost limit. - */ - if (VacuumCostActive) - VacuumCostBalance += VacuumCostPageMiss * io_buffers_len; + pgaio_io_set_flag(ioh, ioh_flags); - *nblocks_progress = io_buffers_len; - did_start_io = true; - } + /* --- + * Even though we're trying to issue IO asynchronously, track the time + * in smgrstartreadv(): + * - if io_method == IOMETHOD_SYNC, we will always perform the IO + * immediately + * - the io method might not support the IO (e.g. worker IO for a temp + * table) + * --- + */ + io_start = pgstat_prepare_io_time(track_io_timing); + smgrstartreadv(ioh, operation->smgr, forknum, + blocknum, + io_pages, io_buffers_len); + pgstat_count_io_op_time(io_object, io_context, IOOP_READ, + io_start, 1, io_buffers_len * BLCKSZ); - return did_start_io; + if (persistence == RELPERSISTENCE_TEMP) + pgBufferUsage.local_blks_read += io_buffers_len; + else + pgBufferUsage.shared_blks_read += io_buffers_len; + + /* + * Track vacuum cost when issuing IO, not after waiting for it. Otherwise + * we could end up issuing a lot of IO in a short timespan, despite a low + * cost limit. + */ + if (VacuumCostActive) + VacuumCostBalance += VacuumCostPageMiss * io_buffers_len; + + *nblocks_progress = io_buffers_len; + + return true; } /* @@ -2013,7 +2196,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, int existing_buf_id; Buffer victim_buffer; BufferDesc *victim_buf_hdr; - uint32 victim_buf_state; + uint64 victim_buf_state; + uint64 set_bits = 0; /* Make sure we will have room to remember the buffer pin */ ResourceOwnerEnlarge(CurrentResourceOwner); @@ -2041,7 +2225,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, */ buf = GetBufferDescriptor(existing_buf_id); - valid = PinBuffer(buf, strategy); + valid = PinBuffer(buf, strategy, false); /* Can release the mapping lock as soon as we've pinned it */ LWLockRelease(newPartitionLock); @@ -2099,17 +2283,11 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, */ UnpinBuffer(victim_buf_hdr); - /* - * The victim buffer we acquired previously is clean and unused, let - * it be found again quickly - */ - StrategyFreeBuffer(victim_buf_hdr); - /* remaining code should match code at top of routine */ existing_buf_hdr = GetBufferDescriptor(existing_buf_id); - valid = PinBuffer(existing_buf_hdr, strategy); + valid = PinBuffer(existing_buf_hdr, strategy, false); /* Can release the mapping lock as soon as we've pinned it */ LWLockRelease(newPartitionLock); @@ -2146,11 +2324,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, * checkpoints, except for their "init" forks, which need to be treated * just like permanent relations. */ - victim_buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; if (relpersistence == RELPERSISTENCE_PERMANENT || forkNum == INIT_FORKNUM) - victim_buf_state |= BM_PERMANENT; + set_bits |= BM_PERMANENT; - UnlockBufHdr(victim_buf_hdr, victim_buf_state); + UnlockBufHdrExt(victim_buf_hdr, victim_buf_state, + set_bits, 0, 0); LWLockRelease(newPartitionLock); @@ -2163,8 +2342,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum, } /* - * InvalidateBuffer -- mark a shared buffer invalid and return it to the - * freelist. + * InvalidateBuffer -- mark a shared buffer invalid. * * The buffer header spinlock must be held at entry. We drop it before * returning. (This is sane because the caller must have locked the @@ -2186,14 +2364,12 @@ InvalidateBuffer(BufferDesc *buf) uint32 oldHash; /* hash value for oldTag */ LWLock *oldPartitionLock; /* buffer partition lock for it */ uint32 oldFlags; - uint32 buf_state; + uint64 buf_state; /* Save the original buffer tag before dropping the spinlock */ oldTag = buf->tag; - buf_state = pg_atomic_read_u32(&buf->state); - Assert(buf_state & BM_LOCKED); - UnlockBufHdr(buf, buf_state); + UnlockBufHdr(buf); /* * Need to compute the old tag's hashcode and partition lock ID. XXX is it @@ -2217,7 +2393,7 @@ InvalidateBuffer(BufferDesc *buf) /* If it's changed while we were waiting for lock, do nothing */ if (!BufferTagsEqual(&buf->tag, &oldTag)) { - UnlockBufHdr(buf, buf_state); + UnlockBufHdr(buf); LWLockRelease(oldPartitionLock); return; } @@ -2234,7 +2410,7 @@ InvalidateBuffer(BufferDesc *buf) */ if (BUF_STATE_GET_REFCOUNT(buf_state) != 0) { - UnlockBufHdr(buf, buf_state); + UnlockBufHdr(buf); LWLockRelease(oldPartitionLock); /* safety check: should definitely not be our *own* pin */ if (GetPrivateRefCount(BufferDescriptorGetBuffer(buf)) > 0) @@ -2243,14 +2419,23 @@ InvalidateBuffer(BufferDesc *buf) goto retry; } + /* + * An invalidated buffer should not have any backends waiting to lock the + * buffer, therefore BM_LOCK_WAKE_IN_PROGRESS should not be set. + */ + Assert(!(buf_state & BM_LOCK_WAKE_IN_PROGRESS)); + /* * Clear out the buffer's tag and flags. We must do this to ensure that * linear scans of the buffer array don't think the buffer is valid. */ oldFlags = buf_state & BUF_FLAG_MASK; ClearBufferTag(&buf->tag); - buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK); - UnlockBufHdr(buf, buf_state); + + UnlockBufHdrExt(buf, buf_state, + 0, + BUF_FLAG_MASK | BUF_USAGECOUNT_MASK, + 0); /* * Remove the buffer from the lookup hashtable, if it was in there. @@ -2262,11 +2447,6 @@ InvalidateBuffer(BufferDesc *buf) * Done with mapping lock. */ LWLockRelease(oldPartitionLock); - - /* - * Insert the buffer at the head of the list of free buffers. - */ - StrategyFreeBuffer(buf); } /* @@ -2281,7 +2461,7 @@ InvalidateBuffer(BufferDesc *buf) static bool InvalidateVictimBuffer(BufferDesc *buf_hdr) { - uint32 buf_state; + uint64 buf_state; uint32 hash; LWLock *partition_lock; BufferTag tag; @@ -2315,12 +2495,18 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr) { Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); - UnlockBufHdr(buf_hdr, buf_state); + UnlockBufHdr(buf_hdr); LWLockRelease(partition_lock); return false; } + /* + * An invalidated buffer should not have any backends waiting to lock the + * buffer, therefore BM_LOCK_WAKE_IN_PROGRESS should not be set. + */ + Assert(!(buf_state & BM_LOCK_WAKE_IN_PROGRESS)); + /* * Clear out the buffer's tag and flags and usagecount. This is not * strictly required, as BM_TAG_VALID/BM_VALID needs to be checked before @@ -2329,8 +2515,10 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr) * tag (see e.g. FlushDatabaseBuffers()). */ ClearBufferTag(&buf_hdr->tag); - buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK); - UnlockBufHdr(buf_hdr, buf_state); + UnlockBufHdrExt(buf_hdr, buf_state, + 0, + BUF_FLAG_MASK | BUF_USAGECOUNT_MASK, + 0); Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); @@ -2339,9 +2527,10 @@ InvalidateVictimBuffer(BufferDesc *buf_hdr) LWLockRelease(partition_lock); + buf_state = pg_atomic_read_u64(&buf_hdr->state); Assert(!(buf_state & (BM_DIRTY | BM_VALID | BM_TAG_VALID))); Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); - Assert(BUF_STATE_GET_REFCOUNT(pg_atomic_read_u32(&buf_hdr->state)) > 0); + Assert(BUF_STATE_GET_REFCOUNT(pg_atomic_read_u64(&buf_hdr->state)) > 0); return true; } @@ -2351,12 +2540,12 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) { BufferDesc *buf_hdr; Buffer buf; - uint32 buf_state; + uint64 buf_state; bool from_ring; /* - * Ensure, while the spinlock's not yet held, that there's a free refcount - * entry, and a resource owner slot for the pin. + * Ensure, before we pin a victim buffer, that there's a free refcount + * entry and resource owner slot for the pin. */ ReservePrivateRefCountEntry(); ResourceOwnerEnlarge(CurrentResourceOwner); @@ -2365,17 +2554,12 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) again: /* - * Select a victim buffer. The buffer is returned with its header - * spinlock still held! + * Select a victim buffer. The buffer is returned pinned and owned by + * this backend. */ buf_hdr = StrategyGetBuffer(strategy, &buf_state, &from_ring); buf = BufferDescriptorGetBuffer(buf_hdr); - Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0); - - /* Pin the buffer and then release the buffer spinlock */ - PinBuffer_Locked(buf_hdr); - /* * We shouldn't have any other pins for this buffer. */ @@ -2383,34 +2567,31 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) /* * If the buffer was dirty, try to write it out. There is a race - * condition here, in that someone might dirty it after we released the - * buffer header lock above, or even while we are writing it out (since - * our share-lock won't prevent hint-bit updates). We will recheck the - * dirty bit after re-locking the buffer header. + * condition here, another backend could dirty the buffer between + * StrategyGetBuffer() checking that it is not in use and invalidating the + * buffer below. That's addressed by InvalidateVictimBuffer() verifying + * that the buffer is not dirty. */ if (buf_state & BM_DIRTY) { - LWLock *content_lock; - Assert(buf_state & BM_TAG_VALID); Assert(buf_state & BM_VALID); /* - * We need a share-lock on the buffer contents to write it out (else - * we might write invalid data, eg because someone else is compacting - * the page contents while we write). We must use a conditional lock - * acquisition here to avoid deadlock. Even though the buffer was not - * pinned (and therefore surely not locked) when StrategyGetBuffer - * returned it, someone else could have pinned and exclusive-locked it - * by the time we get here. If we try to get the lock unconditionally, - * we'd block waiting for them; if they later block waiting for us, - * deadlock ensues. (This has been observed to happen when two - * backends are both trying to split btree index pages, and the second - * one just happens to be trying to split the page the first one got - * from StrategyGetBuffer.) + * We need a share-exclusive lock on the buffer contents to write it + * out (else we might write invalid data, eg because someone else is + * compacting the page contents while we write). We must use a + * conditional lock acquisition here to avoid deadlock. Even though + * the buffer was not pinned (and therefore surely not locked) when + * StrategyGetBuffer returned it, someone else could have pinned and + * (share-)exclusive-locked it by the time we get here. If we try to + * get the lock unconditionally, we'd block waiting for them; if they + * later block waiting for us, deadlock ensues. (This has been + * observed to happen when two backends are both trying to split btree + * index pages, and the second one just happens to be trying to split + * the page the first one got from StrategyGetBuffer.) */ - content_lock = BufferDescriptorGetContentLock(buf_hdr); - if (!LWLockConditionalAcquire(content_lock, LW_SHARED)) + if (!BufferLockConditional(buf, buf_hdr, BUFFER_LOCK_SHARE_EXCLUSIVE)) { /* * Someone else has locked the buffer, so give it up and loop back @@ -2421,33 +2602,28 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) } /* - * If using a nondefault strategy, and writing the buffer would - * require a WAL flush, let the strategy decide whether to go ahead - * and write/reuse the buffer or to choose another victim. We need a - * lock to inspect the page LSN, so this can't be done inside - * StrategyGetBuffer. + * If using a nondefault strategy, and this victim came from the + * strategy ring, let the strategy decide whether to reject it when + * reusing it would require a WAL flush. This only applies to + * permanent buffers; unlogged buffers can have fake LSNs, so + * XLogNeedsFlush() is not meaningful for them. + * + * We need to hold the content lock in at least share-exclusive mode + * to safely inspect the page LSN, so this couldn't have been done + * inside StrategyGetBuffer(). */ - if (strategy != NULL) + if (strategy && from_ring && + buf_state & BM_PERMANENT && + XLogNeedsFlush(BufferGetLSN(buf_hdr)) && + StrategyRejectBuffer(strategy, buf_hdr, from_ring)) { - XLogRecPtr lsn; - - /* Read the LSN while holding buffer header lock */ - buf_state = LockBufHdr(buf_hdr); - lsn = BufferGetLSN(buf_hdr); - UnlockBufHdr(buf_hdr, buf_state); - - if (XLogNeedsFlush(lsn) - && StrategyRejectBuffer(strategy, buf_hdr, from_ring)) - { - LWLockRelease(content_lock); - UnpinBuffer(buf_hdr); - goto again; - } + UnlockReleaseBuffer(buf); + goto again; } /* OK, do the I/O */ FlushBuffer(buf_hdr, NULL, IOOBJECT_RELATION, io_context); - LWLockRelease(content_lock); + LockBuffer(buf, BUFFER_LOCK_UNLOCK); ScheduleBufferTagForWriteback(&BackendWritebackContext, io_context, &buf_hdr->tag); @@ -2489,7 +2665,7 @@ GetVictimBuffer(BufferAccessStrategy strategy, IOContext io_context) /* a final set of sanity checks */ #ifdef USE_ASSERT_CHECKING - buf_state = pg_atomic_read_u32(&buf_hdr->state); + buf_state = pg_atomic_read_u64(&buf_hdr->state); Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 1); Assert(!(buf_state & (BM_TAG_VALID | BM_VALID | BM_DIRTY))); @@ -2575,10 +2751,10 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr, BlockNumber first_block; TRACE_POSTGRESQL_BUFFER_EXTEND_START(fork, - bmr.smgr->smgr_rlocator.locator.spcOid, - bmr.smgr->smgr_rlocator.locator.dbOid, - bmr.smgr->smgr_rlocator.locator.relNumber, - bmr.smgr->smgr_rlocator.backend, + BMR_GET_SMGR(bmr)->smgr_rlocator.locator.spcOid, + BMR_GET_SMGR(bmr)->smgr_rlocator.locator.dbOid, + BMR_GET_SMGR(bmr)->smgr_rlocator.locator.relNumber, + BMR_GET_SMGR(bmr)->smgr_rlocator.backend, extend_by); if (bmr.relpersistence == RELPERSISTENCE_TEMP) @@ -2592,10 +2768,10 @@ ExtendBufferedRelCommon(BufferManagerRelation bmr, *extended_by = extend_by; TRACE_POSTGRESQL_BUFFER_EXTEND_DONE(fork, - bmr.smgr->smgr_rlocator.locator.spcOid, - bmr.smgr->smgr_rlocator.locator.dbOid, - bmr.smgr->smgr_rlocator.locator.relNumber, - bmr.smgr->smgr_rlocator.backend, + BMR_GET_SMGR(bmr)->smgr_rlocator.locator.spcOid, + BMR_GET_SMGR(bmr)->smgr_rlocator.locator.dbOid, + BMR_GET_SMGR(bmr)->smgr_rlocator.locator.relNumber, + BMR_GET_SMGR(bmr)->smgr_rlocator.backend, *extended_by, first_block); @@ -2661,9 +2837,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, * kernel. */ if (flags & EB_CLEAR_SIZE_CACHE) - bmr.smgr->smgr_cached_nblocks[fork] = InvalidBlockNumber; + BMR_GET_SMGR(bmr)->smgr_cached_nblocks[fork] = InvalidBlockNumber; - first_block = smgrnblocks(bmr.smgr, fork); + first_block = smgrnblocks(BMR_GET_SMGR(bmr), fork); /* * Now that we have the accurate relation size, check if the caller wants @@ -2684,11 +2860,6 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, { BufferDesc *buf_hdr = GetBufferDescriptor(buffers[i] - 1); - /* - * The victim buffer we acquired previously is clean and unused, - * let it be found again quickly - */ - StrategyFreeBuffer(buf_hdr); UnpinBuffer(buf_hdr); } @@ -2706,7 +2877,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("cannot extend relation %s beyond %u blocks", - relpath(bmr.smgr->smgr_rlocator, fork).str, + relpath(BMR_GET_SMGR(bmr)->smgr_rlocator, fork).str, MaxBlockNumber))); /* @@ -2728,7 +2899,8 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, ResourceOwnerEnlarge(CurrentResourceOwner); ReservePrivateRefCountEntry(); - InitBufferTag(&tag, &bmr.smgr->smgr_rlocator.locator, fork, first_block + i); + InitBufferTag(&tag, &BMR_GET_SMGR(bmr)->smgr_rlocator.locator, fork, + first_block + i); hash = BufTableHashCode(&tag); partition_lock = BufMappingPartitionLock(hash); @@ -2743,12 +2915,10 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, * because mdread doesn't complain about reads beyond EOF (when * zero_damaged_pages is ON) and so a previous attempt to read a block * beyond EOF could have left a "valid" zero-filled buffer. - * Unfortunately, we have also seen this case occurring because of - * buggy Linux kernels that sometimes return an lseek(SEEK_END) result - * that doesn't account for a recent write. In that situation, the - * pre-existing buffer would contain valid data that we don't want to - * overwrite. Since the legitimate cases should always have left a - * zero-filled buffer, complain if not PageIsNew. + * + * This has also been observed when relation was overwritten by + * external process. Since the legitimate cases should always have + * left a zero-filled buffer, complain if not PageIsNew. */ if (existing_id >= 0) { @@ -2760,15 +2930,9 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, * Pin the existing buffer before releasing the partition lock, * preventing it from being evicted. */ - valid = PinBuffer(existing_hdr, strategy); + valid = PinBuffer(existing_hdr, strategy, false); LWLockRelease(partition_lock); - - /* - * The victim buffer we acquired previously is clean and unused, - * let it be found again quickly - */ - StrategyFreeBuffer(victim_buf_hdr); UnpinBuffer(victim_buf_hdr); buffers[i] = BufferDescriptorGetBuffer(existing_hdr); @@ -2776,51 +2940,57 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, if (valid && !PageIsNew((Page) buf_block)) ereport(ERROR, - (errmsg("unexpected data beyond EOF in block %u of relation %s", + (errmsg("unexpected data beyond EOF in block %u of relation \"%s\"", existing_hdr->tag.blockNum, - relpath(bmr.smgr->smgr_rlocator, fork).str), - errhint("This has been seen to occur with buggy kernels; consider updating your system."))); + relpath(BMR_GET_SMGR(bmr)->smgr_rlocator, fork).str))); /* * We *must* do smgr[zero]extend before succeeding, else the page * will not be reserved by the kernel, and the next P_NEW call * will decide to return the same page. Clear the BM_VALID bit, - * do StartBufferIO() and proceed. + * do StartSharedBufferIO() and proceed. * * Loop to handle the very small possibility that someone re-sets - * BM_VALID between our clearing it and StartBufferIO inspecting - * it. + * BM_VALID between our clearing it and StartSharedBufferIO + * inspecting it. */ - do + while (true) { - uint32 buf_state = LockBufHdr(existing_hdr); + StartBufferIOResult sbres; + + pg_atomic_fetch_and_u64(&existing_hdr->state, ~BM_VALID); + + sbres = StartSharedBufferIO(existing_hdr, true, true, NULL); - buf_state &= ~BM_VALID; - UnlockBufHdr(existing_hdr, buf_state); - } while (!StartBufferIO(existing_hdr, true, false)); + if (sbres != BUFFER_IO_ALREADY_DONE) + break; + } } else { - uint32 buf_state; + uint64 buf_state; + uint64 set_bits = 0; buf_state = LockBufHdr(victim_buf_hdr); /* some sanity checks while we hold the buffer header lock */ - Assert(!(buf_state & (BM_VALID | BM_TAG_VALID | BM_DIRTY | BM_JUST_DIRTIED))); + Assert(!(buf_state & (BM_VALID | BM_TAG_VALID | BM_DIRTY))); Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 1); victim_buf_hdr->tag = tag; - buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; + set_bits |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; if (bmr.relpersistence == RELPERSISTENCE_PERMANENT || fork == INIT_FORKNUM) - buf_state |= BM_PERMANENT; + set_bits |= BM_PERMANENT; - UnlockBufHdr(victim_buf_hdr, buf_state); + UnlockBufHdrExt(victim_buf_hdr, buf_state, + set_bits, 0, + 0); LWLockRelease(partition_lock); /* XXX: could combine the locked operations in it with the above */ - StartBufferIO(victim_buf_hdr, true, false); + StartSharedBufferIO(victim_buf_hdr, true, true, NULL); } } @@ -2836,7 +3006,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, * * We don't need to set checksum for all-zero pages. */ - smgrzeroextend(bmr.smgr, fork, first_block, extend_by, false); + smgrzeroextend(BMR_GET_SMGR(bmr), fork, first_block, extend_by, false); /* * Release the file-extension lock; it's now OK for someone else to extend @@ -2868,7 +3038,7 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, } if (lock) - LWLockAcquire(BufferDescriptorGetContentLock(buf_hdr), LW_EXCLUSIVE); + LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); TerminateBufferIO(buf_hdr, false, BM_VALID, true, false); } @@ -2881,14 +3051,40 @@ ExtendBufferedRelShared(BufferManagerRelation bmr, } /* - * BufferIsExclusiveLocked + * BufferIsLockedByMe + * + * Checks if this backend has the buffer locked in any mode. + * + * Buffer must be pinned. + */ +bool +BufferIsLockedByMe(Buffer buffer) +{ + BufferDesc *bufHdr; + + Assert(BufferIsPinned(buffer)); + + if (BufferIsLocal(buffer)) + { + /* Content locks are not maintained for local buffers. */ + return true; + } + else + { + bufHdr = GetBufferDescriptor(buffer - 1); + return BufferLockHeldByMe(bufHdr); + } +} + +/* + * BufferIsLockedByMeInMode * - * Checks if buffer is exclusive-locked. + * Checks if this backend has the buffer locked in the specified mode. * * Buffer must be pinned. */ bool -BufferIsExclusiveLocked(Buffer buffer) +BufferIsLockedByMeInMode(Buffer buffer, BufferLockMode mode) { BufferDesc *bufHdr; @@ -2902,8 +3098,7 @@ BufferIsExclusiveLocked(Buffer buffer) else { bufHdr = GetBufferDescriptor(buffer - 1); - return LWLockHeldByMeInMode(BufferDescriptorGetContentLock(bufHdr), - LW_EXCLUSIVE); + return BufferLockHeldByMeInMode(bufHdr, mode); } } @@ -2912,7 +3107,7 @@ BufferIsExclusiveLocked(Buffer buffer) * * Checks if buffer is already dirty. * - * Buffer must be pinned and exclusive-locked. (Without an exclusive lock, + * Buffer must be pinned and [share-]exclusive-locked. (Without such a lock, * the result may be stale before it's returned.) */ bool @@ -2932,11 +3127,11 @@ BufferIsDirty(Buffer buffer) else { bufHdr = GetBufferDescriptor(buffer - 1); - Assert(LWLockHeldByMeInMode(BufferDescriptorGetContentLock(bufHdr), - LW_EXCLUSIVE)); + Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_SHARE_EXCLUSIVE) || + BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE)); } - return pg_atomic_read_u32(&bufHdr->state) & BM_DIRTY; + return pg_atomic_read_u64(&bufHdr->state) & BM_DIRTY; } /* @@ -2952,8 +3147,8 @@ void MarkBufferDirty(Buffer buffer) { BufferDesc *bufHdr; - uint32 buf_state; - uint32 old_buf_state; + uint64 buf_state; + uint64 old_buf_state; if (!BufferIsValid(buffer)) elog(ERROR, "bad buffer ID: %d", buffer); @@ -2967,10 +3162,13 @@ MarkBufferDirty(Buffer buffer) bufHdr = GetBufferDescriptor(buffer - 1); Assert(BufferIsPinned(buffer)); - Assert(LWLockHeldByMeInMode(BufferDescriptorGetContentLock(bufHdr), - LW_EXCLUSIVE)); + Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE)); - old_buf_state = pg_atomic_read_u32(&bufHdr->state); + /* + * NB: We have to wait for the buffer header spinlock to be not held, as + * TerminateBufferIO() relies on the spinlock. + */ + old_buf_state = pg_atomic_read_u64(&bufHdr->state); for (;;) { if (old_buf_state & BM_LOCKED) @@ -2979,9 +3177,9 @@ MarkBufferDirty(Buffer buffer) buf_state = old_buf_state; Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); - buf_state |= BM_DIRTY | BM_JUST_DIRTIED; + buf_state |= BM_DIRTY; - if (pg_atomic_compare_exchange_u32(&bufHdr->state, &old_buf_state, + if (pg_atomic_compare_exchange_u64(&bufHdr->state, &old_buf_state, buf_state)) break; } @@ -3066,33 +3264,46 @@ ReleaseAndReadBuffer(Buffer buffer, * must have been done already. * * Returns true if buffer is BM_VALID, else false. This provision allows - * some callers to avoid an extra spinlock cycle. + * some callers to avoid an extra spinlock cycle. If skip_if_not_valid is + * true, then a false return value also indicates that the buffer was + * (recently) invalid and has not been pinned. */ static bool -PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy) +PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy, + bool skip_if_not_valid) { Buffer b = BufferDescriptorGetBuffer(buf); bool result; PrivateRefCountEntry *ref; Assert(!BufferIsLocal(b)); - Assert(ReservedRefCountEntry != NULL); + Assert(ReservedRefCountSlot != -1); ref = GetPrivateRefCountEntry(b, true); if (ref == NULL) { - uint32 buf_state; - uint32 old_buf_state; + uint64 buf_state; + uint64 old_buf_state; - ref = NewPrivateRefCountEntry(b); - - old_buf_state = pg_atomic_read_u32(&buf->state); + old_buf_state = pg_atomic_read_u64(&buf->state); for (;;) { - if (old_buf_state & BM_LOCKED) + if (unlikely(skip_if_not_valid && !(old_buf_state & BM_VALID))) + return false; + + /* + * We're not allowed to increase the refcount while the buffer + * header spinlock is held. Wait for the lock to be released. + */ + if (unlikely(old_buf_state & BM_LOCKED)) + { old_buf_state = WaitBufHdrUnlocked(buf); + /* perform checks at the top of the loop again */ + continue; + } + buf_state = old_buf_state; /* increase refcount */ @@ -3114,19 +3325,12 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy) buf_state += BUF_USAGECOUNT_ONE; } - if (pg_atomic_compare_exchange_u32(&buf->state, &old_buf_state, + if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state, buf_state)) { result = (buf_state & BM_VALID) != 0; - /* - * Assume that we acquired a buffer pin for the purposes of - * Valgrind buffer client checks (even in !result case) to - * keep things simple. Buffers that are unsafe to access are - * not generally guaranteed to be marked undefined or - * non-accessible in any case. - */ - VALGRIND_MAKE_MEM_DEFINED(BufHdrGetBlock(buf), BLCKSZ); + TrackNewBufferPin(b); break; } } @@ -3148,12 +3352,13 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy) * that the buffer page is legitimately non-accessible here. We * cannot meddle with that. */ - result = (pg_atomic_read_u32(&buf->state) & BM_VALID) != 0; + result = (pg_atomic_read_u64(&buf->state) & BM_VALID) != 0; + + Assert(ref->data.refcount > 0); + ref->data.refcount++; + ResourceOwnerRememberBuffer(CurrentResourceOwner, b); } - ref->refcount++; - Assert(ref->refcount > 0); - ResourceOwnerRememberBuffer(CurrentResourceOwner, b); return result; } @@ -3182,9 +3387,7 @@ PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy) static void PinBuffer_Locked(BufferDesc *buf) { - Buffer b; - PrivateRefCountEntry *ref; - uint32 buf_state; + uint64 old_buf_state; /* * As explained, We don't expect any preexisting pins. That allows us to @@ -3192,28 +3395,16 @@ PinBuffer_Locked(BufferDesc *buf) */ Assert(GetPrivateRefCountEntry(BufferDescriptorGetBuffer(buf), false) == NULL); - /* - * Buffer can't have a preexisting pin, so mark its page as defined to - * Valgrind (this is similar to the PinBuffer() case where the backend - * doesn't already have a buffer pin) - */ - VALGRIND_MAKE_MEM_DEFINED(BufHdrGetBlock(buf), BLCKSZ); - /* * Since we hold the buffer spinlock, we can update the buffer state and * release the lock in one operation. */ - buf_state = pg_atomic_read_u32(&buf->state); - Assert(buf_state & BM_LOCKED); - buf_state += BUF_REFCOUNT_ONE; - UnlockBufHdr(buf, buf_state); - - b = BufferDescriptorGetBuffer(buf); + old_buf_state = pg_atomic_read_u64(&buf->state); - ref = NewPrivateRefCountEntry(b); - ref->refcount++; + UnlockBufHdrExt(buf, old_buf_state, + 0, 0, 1); - ResourceOwnerRememberBuffer(CurrentResourceOwner, b); + TrackNewBufferPin(BufferDescriptorGetBuffer(buf)); } /* @@ -3238,7 +3429,7 @@ WakePinCountWaiter(BufferDesc *buf) * BM_PIN_COUNT_WAITER if it stops waiting for a reason other than this * backend waking it up. */ - uint32 buf_state = LockBufHdr(buf); + uint64 buf_state = LockBufHdr(buf); if ((buf_state & BM_PIN_COUNT_WAITER) && BUF_STATE_GET_REFCOUNT(buf_state) == 1) @@ -3246,12 +3437,13 @@ WakePinCountWaiter(BufferDesc *buf) /* we just released the last pin other than the waiter's */ int wait_backend_pgprocno = buf->wait_backend_pgprocno; - buf_state &= ~BM_PIN_COUNT_WAITER; - UnlockBufHdr(buf, buf_state); + UnlockBufHdrExt(buf, buf_state, + 0, BM_PIN_COUNT_WAITER, + 0); ProcSendSignal(wait_backend_pgprocno); } else - UnlockBufHdr(buf, buf_state); + UnlockBufHdr(buf); } /* @@ -3280,12 +3472,11 @@ UnpinBufferNoOwner(BufferDesc *buf) /* not moving as we're likely deleting it soon anyway */ ref = GetPrivateRefCountEntry(b, false); Assert(ref != NULL); - Assert(ref->refcount > 0); - ref->refcount--; - if (ref->refcount == 0) + Assert(ref->data.refcount > 0); + ref->data.refcount--; + if (ref->data.refcount == 0) { - uint32 buf_state; - uint32 old_buf_state; + uint64 old_buf_state; /* * Mark buffer non-accessible to Valgrind. @@ -3296,38 +3487,50 @@ UnpinBufferNoOwner(BufferDesc *buf) */ VALGRIND_MAKE_MEM_NOACCESS(BufHdrGetBlock(buf), BLCKSZ); - /* I'd better not still hold the buffer content lock */ - Assert(!LWLockHeldByMe(BufferDescriptorGetContentLock(buf))); - /* - * Decrement the shared reference count. - * - * Since buffer spinlock holder can update status using just write, - * it's not safe to use atomic decrement here; thus use a CAS loop. + * I'd better not still hold the buffer content lock. Can't use + * BufferIsLockedByMe(), as that asserts the buffer is pinned. */ - old_buf_state = pg_atomic_read_u32(&buf->state); - for (;;) - { - if (old_buf_state & BM_LOCKED) - old_buf_state = WaitBufHdrUnlocked(buf); - - buf_state = old_buf_state; - - buf_state -= BUF_REFCOUNT_ONE; + Assert(!BufferLockHeldByMe(buf)); - if (pg_atomic_compare_exchange_u32(&buf->state, &old_buf_state, - buf_state)) - break; - } + /* decrement the shared reference count */ + old_buf_state = pg_atomic_fetch_sub_u64(&buf->state, BUF_REFCOUNT_ONE); /* Support LockBufferForCleanup() */ - if (buf_state & BM_PIN_COUNT_WAITER) + if (old_buf_state & BM_PIN_COUNT_WAITER) WakePinCountWaiter(buf); ForgetPrivateRefCountEntry(ref); } } +/* + * Set up backend-local tracking of a buffer pinned the first time by this + * backend. + */ +inline void +TrackNewBufferPin(Buffer buf) +{ + PrivateRefCountEntry *ref; + + ref = NewPrivateRefCountEntry(buf); + ref->data.refcount++; + + ResourceOwnerRememberBuffer(CurrentResourceOwner, buf); + + /* + * This is the first pin for this page by this backend, mark its page as + * defined to valgrind. While the page contents might not actually be + * valid yet, we don't currently guarantee that such pages are marked + * undefined or non-accessible. + * + * It's not necessarily the prettiest to do this here, but otherwise we'd + * need this block of code in multiple places. + */ + VALGRIND_MAKE_MEM_DEFINED(BufHdrGetBlock(GetBufferDescriptor(buf - 1)), + BLCKSZ); +} + #define ST_SORT sort_checkpoint_bufferids #define ST_ELEMENT_TYPE CkptSortItem #define ST_COMPARE(a, b) ckpt_buforder_comparator(a, b) @@ -3339,16 +3542,16 @@ UnpinBufferNoOwner(BufferDesc *buf) * BufferSync -- Write out all dirty buffers in the pool. * * This is called at checkpoint time to write out all dirty shared buffers. - * The checkpoint request flags should be passed in. If CHECKPOINT_IMMEDIATE - * is set, we disable delays between writes; if CHECKPOINT_IS_SHUTDOWN, - * CHECKPOINT_END_OF_RECOVERY or CHECKPOINT_FLUSH_ALL is set, we write even - * unlogged buffers, which are otherwise skipped. The remaining flags + * The checkpoint request flags should be passed in. If CHECKPOINT_FAST is + * set, we disable delays between writes; if CHECKPOINT_IS_SHUTDOWN, + * CHECKPOINT_END_OF_RECOVERY or CHECKPOINT_FLUSH_UNLOGGED is set, we write + * even unlogged buffers, which are otherwise skipped. The remaining flags * currently have no effect here. */ static void BufferSync(int flags) { - uint32 buf_state; + uint64 buf_state; int buf_id; int num_to_scan; int num_spaces; @@ -3358,7 +3561,7 @@ BufferSync(int flags) Oid last_tsid; binaryheap *ts_heap; int i; - int mask = BM_DIRTY; + uint64 mask = BM_DIRTY; WritebackContext wb_context; /* @@ -3367,7 +3570,7 @@ BufferSync(int flags) * recovery, we write all dirty buffers. */ if (!((flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY | - CHECKPOINT_FLUSH_ALL)))) + CHECKPOINT_FLUSH_UNLOGGED)))) mask |= BM_PERMANENT; /* @@ -3390,6 +3593,7 @@ BufferSync(int flags) for (buf_id = 0; buf_id < NBuffers; buf_id++) { BufferDesc *bufHdr = GetBufferDescriptor(buf_id); + uint64 set_bits = 0; /* * Header spinlock is enough to examine BM_DIRTY, see comment in @@ -3401,7 +3605,7 @@ BufferSync(int flags) { CkptSortItem *item; - buf_state |= BM_CHECKPOINT_NEEDED; + set_bits = BM_CHECKPOINT_NEEDED; item = &CkptBufferIds[num_to_scan++]; item->buf_id = buf_id; @@ -3411,7 +3615,9 @@ BufferSync(int flags) item->blockNum = bufHdr->tag.blockNum; } - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdrExt(bufHdr, buf_state, + set_bits, 0, + 0); /* Check for barrier events in case NBuffers is large. */ if (ProcSignalBarrierPending) @@ -3554,7 +3760,7 @@ BufferSync(int flags) * write the buffer though we didn't need to. It doesn't seem worth * guarding against this, though. */ - if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED) + if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED) { if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN) { @@ -3616,7 +3822,7 @@ BufferSync(int flags) * This is called periodically by the background writer process. * * Returns true if it's appropriate for the bgwriter process to go into - * low-power hibernation mode. (This happens if the strategy clock sweep + * low-power hibernation mode. (This happens if the strategy clock-sweep * has been "lapped" and no buffer allocations have occurred recently, * or if the bgwriter has been effectively disabled by setting * bgwriter_lru_maxpages to 0.) @@ -3666,8 +3872,8 @@ BgBufferSync(WritebackContext *wb_context) uint32 new_recent_alloc; /* - * Find out where the freelist clock sweep currently is, and how many - * buffer allocations have happened since our last call. + * Find out where the clock-sweep currently is, and how many buffer + * allocations have happened since our last call. */ strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc); @@ -3687,8 +3893,8 @@ BgBufferSync(WritebackContext *wb_context) /* * Compute strategy_delta = how many buffers have been scanned by the - * clock sweep since last time. If first time through, assume none. Then - * see if we are still ahead of the clock sweep, and if so, how many + * clock-sweep since last time. If first time through, assume none. Then + * see if we are still ahead of the clock-sweep, and if so, how many * buffers we could scan before we'd catch up with it and "lap" it. Note: * weird-looking coding of xxx_passes comparisons are to avoid bogus * behavior when the passes counts wrap around. @@ -3924,7 +4130,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) { BufferDesc *bufHdr = GetBufferDescriptor(buf_id); int result = 0; - uint32 buf_state; + uint64 buf_state; BufferTag tag; /* Make sure we can handle the pin */ @@ -3950,27 +4156,24 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context) else if (skip_recently_used) { /* Caller told us not to write recently-used buffers */ - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); return result; } if (!(buf_state & BM_VALID) || !(buf_state & BM_DIRTY)) { /* It's clean, so nothing to do */ - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); return result; } /* - * Pin it, share-lock it, write it. (FlushBuffer will do nothing if the - * buffer is clean by the time we've locked it.) + * Pin it, share-exclusive-lock it, write it. (FlushBuffer will do + * nothing if the buffer is clean by the time we've locked it.) */ PinBuffer_Locked(bufHdr); - LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); - - LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); + FlushUnlockedBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); tag = bufHdr->tag; @@ -4012,8 +4215,6 @@ AtEOXact_Buffers(bool isCommit) void InitBufferManagerAccess(void) { - HASHCTL hash_ctl; - /* * An advisory limit on the number of pins each backend should hold, based * on shared_buffers and the maximum number of connections possible. @@ -4024,12 +4225,9 @@ InitBufferManagerAccess(void) MaxProportionalPins = NBuffers / (MaxBackends + NUM_AUXILIARY_PROCS); memset(&PrivateRefCountArray, 0, sizeof(PrivateRefCountArray)); + memset(&PrivateRefCountArrayKeys, 0, sizeof(PrivateRefCountArrayKeys)); - hash_ctl.keysize = sizeof(int32); - hash_ctl.entrysize = sizeof(PrivateRefCountEntry); - - PrivateRefCountHash = hash_create("PrivateRefCount", 100, &hash_ctl, - HASH_ELEM | HASH_BLOBS); + PrivateRefCountHash = refcount_create(CurrentMemoryContext, 100, NULL); /* * AtProcExit_Buffers needs LWLock access, and thereby has to be called at @@ -4073,10 +4271,10 @@ CheckForBufferLeaks(void) /* check the array */ for (i = 0; i < REFCOUNT_ARRAY_ENTRIES; i++) { - res = &PrivateRefCountArray[i]; - - if (res->buffer != InvalidBuffer) + if (PrivateRefCountArrayKeys[i] != InvalidBuffer) { + res = &PrivateRefCountArray[i]; + s = DebugPrintBufferRefcount(res->buffer); elog(WARNING, "buffer refcount leak: %s", s); pfree(s); @@ -4088,10 +4286,10 @@ CheckForBufferLeaks(void) /* if necessary search the hash */ if (PrivateRefCountOverflowed) { - HASH_SEQ_STATUS hstat; + refcount_iterator iter; - hash_seq_init(&hstat, PrivateRefCountHash); - while ((res = (PrivateRefCountEntry *) hash_seq_search(&hstat)) != NULL) + refcount_start_iterate(PrivateRefCountHash, &iter); + while ((res = refcount_iterate(PrivateRefCountHash, &iter)) != NULL) { s = DebugPrintBufferRefcount(res->buffer); elog(WARNING, "buffer refcount leak: %s", s); @@ -4109,9 +4307,9 @@ CheckForBufferLeaks(void) * Check for exclusive-locked catalog buffers. This is the core of * AssertCouldGetRelation(). * - * A backend would self-deadlock on LWLocks if the catalog scan read the - * exclusive-locked buffer. The main threat is exclusive-locked buffers of - * catalogs used in relcache, because a catcache search on any catalog may + * A backend would self-deadlock on the content lock if the catalog scan read + * the exclusive-locked buffer. The main threat is exclusive-locked buffers + * of catalogs used in relcache, because a catcache search on any catalog may * build that catalog's relcache entry. We don't have an inventory of * catalogs relcache uses, so just check buffers of most catalogs. * @@ -4125,26 +4323,45 @@ CheckForBufferLeaks(void) void AssertBufferLocksPermitCatalogRead(void) { - ForEachLWLockHeldByMe(AssertNotCatalogBufferLock, NULL); + PrivateRefCountEntry *res; + + /* check the array */ + for (int i = 0; i < REFCOUNT_ARRAY_ENTRIES; i++) + { + if (PrivateRefCountArrayKeys[i] != InvalidBuffer) + { + res = &PrivateRefCountArray[i]; + + if (res->buffer == InvalidBuffer) + continue; + + AssertNotCatalogBufferLock(res->buffer, res->data.lockmode); + } + } + + /* if necessary search the hash */ + if (PrivateRefCountOverflowed) + { + refcount_iterator iter; + + refcount_start_iterate(PrivateRefCountHash, &iter); + while ((res = refcount_iterate(PrivateRefCountHash, &iter)) != NULL) + { + AssertNotCatalogBufferLock(res->buffer, res->data.lockmode); + } + } } static void -AssertNotCatalogBufferLock(LWLock *lock, LWLockMode mode, - void *unused_context) +AssertNotCatalogBufferLock(Buffer buffer, BufferLockMode mode) { - BufferDesc *bufHdr; + BufferDesc *bufHdr = GetBufferDescriptor(buffer - 1); BufferTag tag; Oid relid; - if (mode != LW_EXCLUSIVE) + if (mode != BUFFER_LOCK_EXCLUSIVE) return; - if (!((BufferDescPadded *) lock > BufferDescriptors && - (BufferDescPadded *) lock < BufferDescriptors + NBuffers)) - return; /* not a buffer lock */ - - bufHdr = (BufferDesc *) - ((char *) lock - offsetof(BufferDesc, content_lock)); tag = bufHdr->tag; /* @@ -4175,7 +4392,7 @@ DebugPrintBufferRefcount(Buffer buffer) int32 loccount; char *result; ProcNumber backend; - uint32 buf_state; + uint64 buf_state; Assert(BufferIsValid(buffer)); if (BufferIsLocal(buffer)) @@ -4191,10 +4408,10 @@ DebugPrintBufferRefcount(Buffer buffer) backend = INVALID_PROC_NUMBER; } - /* theoretically we should lock the bufhdr here */ - buf_state = pg_atomic_read_u32(&buf->state); + /* theoretically we should lock the bufHdr here */ + buf_state = pg_atomic_read_u64(&buf->state); - result = psprintf("[%03d] (rel=%s, blockNum=%u, flags=0x%x, refcount=%u %d)", + result = psprintf("[%03d] (rel=%s, blockNum=%u, flags=0x%" PRIx64 ", refcount=%u %d)", buffer, relpathbackend(BufTagGetRelFileLocator(&buf->tag), backend, BufTagGetForkNum(&buf->tag)).str, @@ -4276,11 +4493,8 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum, * However, we will need to force the changes to disk via fsync before * we can checkpoint WAL. * - * The caller must hold a pin on the buffer and have share-locked the - * buffer contents. (Note: a share-lock does not prevent updates of - * hint bits in the buffer, so the page could change while the write - * is in progress, but we assume that that will not invalidate the data - * written.) + * The caller must hold a pin on the buffer and have + * (share-)exclusively-locked the buffer contents. * * If the caller has an smgr reference for the buffer's relation, pass it * as the second parameter. If not, pass NULL. @@ -4293,15 +4507,16 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, ErrorContextCallback errcallback; instr_time io_start; Block bufBlock; - char *bufToWrite; - uint32 buf_state; + + Assert(BufferLockHeldByMeInMode(buf, BUFFER_LOCK_EXCLUSIVE) || + BufferLockHeldByMeInMode(buf, BUFFER_LOCK_SHARE_EXCLUSIVE)); /* * Try to start an I/O operation. If StartBufferIO returns false, then * someone else flushed the buffer before we could, so we need not do * anything. */ - if (!StartBufferIO(buf, false, false)) + if (StartSharedBufferIO(buf, false, true, NULL) == BUFFER_IO_ALREADY_DONE) return; /* Setup error traceback support for ereport() */ @@ -4320,18 +4535,12 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, reln->smgr_rlocator.locator.dbOid, reln->smgr_rlocator.locator.relNumber); - buf_state = LockBufHdr(buf); - /* - * Run PageGetLSN while holding header lock, since we don't have the - * buffer locked exclusively in all cases. + * As we hold at least a share-exclusive lock on the buffer, the LSN + * cannot change during the flush (and thus can't be torn). */ recptr = BufferGetLSN(buf); - /* To check if block content changes while flushing. - vadim 01/17/97 */ - buf_state &= ~BM_JUST_DIRTIED; - UnlockBufHdr(buf, buf_state); - /* * Force XLOG flush up to buffer's LSN. This implements the basic WAL * rule that log updates must hit disk before any of the data-file changes @@ -4341,15 +4550,15 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, * lost after a crash anyway. Most unlogged relation pages do not bear * LSNs since we never emit WAL records for them, and therefore flushing * up through the buffer LSN would be useless, but harmless. However, - * GiST indexes use LSNs internally to track page-splits, and therefore - * unlogged GiST pages bear "fake" LSNs generated by - * GetFakeLSNForUnloggedRel. It is unlikely but possible that the fake + * some index AMs use LSNs internally to detect concurrent page + * modifications, and therefore unlogged index pages bear "fake" LSNs + * generated by XLogGetFakeLSN. It is unlikely but possible that the fake * LSN counter could advance past the WAL insertion point; and if it did * happen, attempting to flush WAL through that location would fail, with * disastrous system-wide consequences. To make sure that can't happen, * skip the flush if the buffer isn't permanent. */ - if (buf_state & BM_PERMANENT) + if (pg_atomic_read_u64(&buf->state) & BM_PERMANENT) XLogFlush(recptr); /* @@ -4360,22 +4569,15 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, */ bufBlock = BufHdrGetBlock(buf); - /* - * Update page checksum if desired. Since we have only shared lock on the - * buffer, other processes might be updating hint bits in it, so we must - * copy the page to private storage if we do checksumming. - */ - bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum); + /* Update page checksum if desired. */ + PageSetChecksum((Page) bufBlock, buf->tag.blockNum); io_start = pgstat_prepare_io_time(track_io_timing); - /* - * bufToWrite is either the shared buffer or a copy, as appropriate. - */ smgrwrite(reln, BufTagGetForkNum(&buf->tag), buf->tag.blockNum, - bufToWrite, + bufBlock, false); /* @@ -4396,14 +4598,13 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, * When a strategy is not in use, the write can only be a "regular" write * of a dirty shared buffer (IOCONTEXT_NORMAL IOOP_WRITE). */ - pgstat_count_io_op_time(IOOBJECT_RELATION, io_context, + pgstat_count_io_op_time(io_object, io_context, IOOP_WRITE, io_start, 1, BLCKSZ); pgBufferUsage.shared_blks_written++; /* - * Mark the buffer as clean (unless BM_JUST_DIRTIED has become set) and - * end the BM_IO_IN_PROGRESS state. + * Mark the buffer as clean and end the BM_IO_IN_PROGRESS state. */ TerminateBufferIO(buf, true, 0, true, false); @@ -4417,6 +4618,21 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOObject io_object, error_context_stack = errcallback.previous; } +/* + * Convenience wrapper around FlushBuffer() that locks/unlocks the buffer + * before/after calling FlushBuffer(). + */ +static void +FlushUnlockedBuffer(BufferDesc *buf, SMgrRelation reln, + IOObject io_object, IOContext io_context) +{ + Buffer buffer = BufferDescriptorGetBuffer(buf); + + BufferLockAcquire(buffer, buf, BUFFER_LOCK_SHARE_EXCLUSIVE); + FlushBuffer(buf, reln, io_object, io_context); + BufferLockUnlock(buffer, buf); +} + /* * RelationGetNumberOfBlocksInFork * Determines the current number of pages in the specified relation fork. @@ -4478,39 +4694,50 @@ BufferIsPermanent(Buffer buffer) * not random garbage. */ bufHdr = GetBufferDescriptor(buffer - 1); - return (pg_atomic_read_u32(&bufHdr->state) & BM_PERMANENT) != 0; + return (pg_atomic_read_u64(&bufHdr->state) & BM_PERMANENT) != 0; } /* * BufferGetLSNAtomic - * Retrieves the LSN of the buffer atomically using a buffer header lock. - * This is necessary for some callers who may not have an exclusive lock - * on the buffer. + * Retrieves the LSN of the buffer atomically. + * + * This is necessary for some callers who may only hold a share lock on + * the buffer. A share lock allows a concurrent backend to set hint bits + * on the page, which in turn may require a WAL record to be emitted. + * + * On platforms with 8 byte atomic reads/writes, we don't need to do any + * additional locking. On platforms not supporting such 8 byte atomic + * reads/writes, we need to actually take the header lock. */ XLogRecPtr BufferGetLSNAtomic(Buffer buffer) { - char *page = BufferGetPage(buffer); - BufferDesc *bufHdr; - XLogRecPtr lsn; - uint32 buf_state; - - /* - * If we don't need locking for correctness, fastpath out. - */ - if (!XLogHintBitIsNeeded() || BufferIsLocal(buffer)) - return PageGetLSN(page); - /* Make sure we've got a real buffer, and that we hold a pin on it. */ Assert(BufferIsValid(buffer)); Assert(BufferIsPinned(buffer)); - bufHdr = GetBufferDescriptor(buffer - 1); - buf_state = LockBufHdr(bufHdr); - lsn = PageGetLSN(page); - UnlockBufHdr(bufHdr, buf_state); +#ifdef PG_HAVE_8BYTE_SINGLE_COPY_ATOMICITY + return PageGetLSN(BufferGetPage(buffer)); +#else + { + char *page = BufferGetPage(buffer); + BufferDesc *bufHdr; + XLogRecPtr lsn; + + /* + * If we don't need locking for correctness, fastpath out. + */ + if (!XLogHintBitIsNeeded() || BufferIsLocal(buffer)) + return PageGetLSN(page); + + bufHdr = GetBufferDescriptor(buffer - 1); + LockBufHdr(bufHdr); + lsn = PageGetLSN(page); + UnlockBufHdr(bufHdr); - return lsn; + return lsn; + } +#endif } /* --------------------------------------------------------------------- @@ -4550,11 +4777,9 @@ DropRelationBuffers(SMgrRelation smgr_reln, ForkNumber *forkNum, if (RelFileLocatorBackendIsTemp(rlocator)) { if (rlocator.backend == MyProcNumber) - { - for (j = 0; j < nforks; j++) - DropRelationLocalBuffers(rlocator.locator, forkNum[j], - firstDelBlock[j]); - } + DropRelationLocalBuffers(rlocator.locator, forkNum, nforks, + firstDelBlock); + return; } @@ -4611,7 +4836,6 @@ DropRelationBuffers(SMgrRelation smgr_reln, ForkNumber *forkNum, for (i = 0; i < NBuffers; i++) { BufferDesc *bufHdr = GetBufferDescriptor(i); - uint32 buf_state; /* * We can make this a tad faster by prechecking the buffer tag before @@ -4632,7 +4856,7 @@ DropRelationBuffers(SMgrRelation smgr_reln, ForkNumber *forkNum, if (!BufTagMatchesRelFileLocator(&bufHdr->tag, &rlocator.locator)) continue; - buf_state = LockBufHdr(bufHdr); + LockBufHdr(bufHdr); for (j = 0; j < nforks; j++) { @@ -4645,7 +4869,7 @@ DropRelationBuffers(SMgrRelation smgr_reln, ForkNumber *forkNum, } } if (j >= nforks) - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } } @@ -4672,7 +4896,7 @@ DropRelationsAllBuffers(SMgrRelation *smgr_reln, int nlocators) if (nlocators == 0) return; - rels = palloc(sizeof(SMgrRelation) * nlocators); /* non-local relations */ + rels = palloc_array(SMgrRelation, nlocators); /* non-local relations */ /* If it's a local relation, it's localbuf.c's problem. */ for (i = 0; i < nlocators; i++) @@ -4754,7 +4978,7 @@ DropRelationsAllBuffers(SMgrRelation *smgr_reln, int nlocators) } pfree(block); - locators = palloc(sizeof(RelFileLocator) * n); /* non-local relations */ + locators = palloc_array(RelFileLocator, n); /* non-local relations */ for (i = 0; i < n; i++) locators[i] = rels[i]->smgr_rlocator.locator; @@ -4774,7 +4998,6 @@ DropRelationsAllBuffers(SMgrRelation *smgr_reln, int nlocators) { RelFileLocator *rlocator = NULL; BufferDesc *bufHdr = GetBufferDescriptor(i); - uint32 buf_state; /* * As in DropRelationBuffers, an unlocked precheck should be safe and @@ -4808,11 +5031,11 @@ DropRelationsAllBuffers(SMgrRelation *smgr_reln, int nlocators) if (rlocator == NULL) continue; - buf_state = LockBufHdr(bufHdr); + LockBufHdr(bufHdr); if (BufTagMatchesRelFileLocator(&bufHdr->tag, rlocator)) InvalidateBuffer(bufHdr); /* releases spinlock */ else - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } pfree(locators); @@ -4842,7 +5065,6 @@ FindAndDropRelationBuffers(RelFileLocator rlocator, ForkNumber forkNum, LWLock *bufPartitionLock; /* buffer partition lock for it */ int buf_id; BufferDesc *bufHdr; - uint32 buf_state; /* create a tag so we can lookup the buffer */ InitBufferTag(&bufTag, &rlocator, forkNum, curBlock); @@ -4867,14 +5089,14 @@ FindAndDropRelationBuffers(RelFileLocator rlocator, ForkNumber forkNum, * evicted by some other backend loading blocks for a different * relation after we release lock on the BufMapping table. */ - buf_state = LockBufHdr(bufHdr); + LockBufHdr(bufHdr); if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rlocator) && BufTagGetForkNum(&bufHdr->tag) == forkNum && bufHdr->tag.blockNum >= firstDelBlock) InvalidateBuffer(bufHdr); /* releases spinlock */ else - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } } @@ -4902,7 +5124,6 @@ DropDatabaseBuffers(Oid dbid) for (i = 0; i < NBuffers; i++) { BufferDesc *bufHdr = GetBufferDescriptor(i); - uint32 buf_state; /* * As in DropRelationBuffers, an unlocked precheck should be safe and @@ -4911,11 +5132,11 @@ DropDatabaseBuffers(Oid dbid) if (bufHdr->tag.dbOid != dbid) continue; - buf_state = LockBufHdr(bufHdr); + LockBufHdr(bufHdr); if (bufHdr->tag.dbOid == dbid) InvalidateBuffer(bufHdr); /* releases spinlock */ else - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } } @@ -4948,11 +5169,11 @@ FlushRelationBuffers(Relation rel) { for (i = 0; i < NLocBuffer; i++) { - uint32 buf_state; + uint64 buf_state; bufHdr = GetLocalBufferDescriptor(i); if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator) && - ((buf_state = pg_atomic_read_u32(&bufHdr->state)) & + ((buf_state = pg_atomic_read_u64(&bufHdr->state)) & (BM_VALID | BM_DIRTY)) == (BM_VALID | BM_DIRTY)) { ErrorContextCallback errcallback; @@ -4988,7 +5209,7 @@ FlushRelationBuffers(Relation rel) for (i = 0; i < NBuffers; i++) { - uint32 buf_state; + uint64 buf_state; bufHdr = GetBufferDescriptor(i); @@ -5008,13 +5229,11 @@ FlushRelationBuffers(Relation rel) (buf_state & (BM_VALID | BM_DIRTY)) == (BM_VALID | BM_DIRTY)) { PinBuffer_Locked(bufHdr); - LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL); - LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); + FlushUnlockedBuffer(bufHdr, srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL); UnpinBuffer(bufHdr); } else - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } } @@ -5038,7 +5257,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels) return; /* fill-in array for qsort */ - srels = palloc(sizeof(SMgrSortArray) * nrels); + srels = palloc_array(SMgrSortArray, nrels); for (i = 0; i < nrels; i++) { @@ -5062,7 +5281,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels) { SMgrSortArray *srelent = NULL; BufferDesc *bufHdr = GetBufferDescriptor(i); - uint32 buf_state; + uint64 buf_state; /* * As in DropRelationBuffers, an unlocked precheck should be safe and @@ -5105,13 +5324,11 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels) (buf_state & (BM_VALID | BM_DIRTY)) == (BM_VALID | BM_DIRTY)) { PinBuffer_Locked(bufHdr); - LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, srelent->srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL); - LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); + FlushUnlockedBuffer(bufHdr, srelent->srel, IOOBJECT_RELATION, IOCONTEXT_NORMAL); UnpinBuffer(bufHdr); } else - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } pfree(srels); @@ -5313,7 +5530,7 @@ FlushDatabaseBuffers(Oid dbid) for (i = 0; i < NBuffers; i++) { - uint32 buf_state; + uint64 buf_state; bufHdr = GetBufferDescriptor(i); @@ -5333,19 +5550,17 @@ FlushDatabaseBuffers(Oid dbid) (buf_state & (BM_VALID | BM_DIRTY)) == (BM_VALID | BM_DIRTY)) { PinBuffer_Locked(bufHdr); - LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED); - FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); - LWLockRelease(BufferDescriptorGetContentLock(bufHdr)); + FlushUnlockedBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); UnpinBuffer(bufHdr); } else - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); } } /* - * Flush a previously, shared or exclusively, locked and pinned buffer to the - * OS. + * Flush a previously, share-exclusively or exclusively, locked and pinned + * buffer to the OS. */ void FlushOneBuffer(Buffer buffer) @@ -5359,7 +5574,7 @@ FlushOneBuffer(Buffer buffer) bufHdr = GetBufferDescriptor(buffer - 1); - Assert(LWLockHeldByMe(BufferDescriptorGetContentLock(bufHdr))); + Assert(BufferIsLockedByMe(buffer)); FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); } @@ -5382,13 +5597,65 @@ ReleaseBuffer(Buffer buffer) /* * UnlockReleaseBuffer -- release the content lock and pin on a buffer * - * This is just a shorthand for a common combination. + * This is just a, more efficient, shorthand for a common combination. */ void UnlockReleaseBuffer(Buffer buffer) { - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buffer); + int mode; + BufferDesc *buf; + PrivateRefCountEntry *ref; + uint64 sub; + uint64 lockstate; + + Assert(BufferIsPinned(buffer)); + + if (BufferIsLocal(buffer)) + { + UnpinLocalBuffer(buffer); + return; + } + + ResourceOwnerForgetBuffer(CurrentResourceOwner, buffer); + + buf = GetBufferDescriptor(buffer - 1); + + mode = BufferLockDisownInternal(buffer, buf); + + /* compute state modification for lock release */ + sub = BufferLockReleaseSub(mode); + + /* compute state modification for pin release */ + ref = GetPrivateRefCountEntry(buffer, false); + Assert(ref != NULL); + Assert(ref->data.refcount > 0); + ref->data.refcount--; + + /* no more backend local pins, reduce shared pin count */ + if (likely(ref->data.refcount == 0)) + { + /* See comment in UnpinBufferNoOwner() */ + VALGRIND_MAKE_MEM_NOACCESS(BufHdrGetBlock(buf), BLCKSZ); + + sub |= BUF_REFCOUNT_ONE; + ForgetPrivateRefCountEntry(ref); + } + + /* perform the lock and pin release in one atomic op */ + lockstate = pg_atomic_sub_fetch_u64(&buf->state, sub); + + /* wake up waiters for the lock */ + BufferLockProcessRelease(buf, mode, lockstate); + + /* wake up waiter for the pin release */ + if (lockstate & BM_PIN_COUNT_WAITER) + WakePinCountWaiter(buf); + + /* + * Now okay to allow cancel/die interrupts again, which were held when the + * lock was acquired. + */ + RESUME_INTERRUPTS(); } /* @@ -5412,64 +5679,45 @@ IncrBufferRefCount(Buffer buffer) ref = GetPrivateRefCountEntry(buffer, true); Assert(ref != NULL); - ref->refcount++; + ref->data.refcount++; } ResourceOwnerRememberBuffer(CurrentResourceOwner, buffer); } /* - * MarkBufferDirtyHint - * - * Mark a buffer dirty for non-critical changes. - * - * This is essentially the same as MarkBufferDirty, except: + * Shared-buffer only helper for MarkBufferDirtyHint() and + * BufferSetHintBits16(). * - * 1. The caller does not write WAL; so if checksums are enabled, we may need - * to write an XLOG_FPI_FOR_HINT WAL record to protect against torn pages. - * 2. The caller might have only share-lock instead of exclusive-lock on the - * buffer's content lock. - * 3. This function does not guarantee that the buffer is always marked dirty - * (due to a race condition), so it cannot be used for important changes. + * This is separated out because it turns out that the repeated checks for + * local buffers, repeated GetBufferDescriptor() and repeated reading of the + * buffer's state sufficiently hurts the performance of BufferSetHintBits16(). */ -void -MarkBufferDirtyHint(Buffer buffer, bool buffer_std) +static inline void +MarkSharedBufferDirtyHint(Buffer buffer, BufferDesc *bufHdr, uint64 lockstate, + bool buffer_std) { - BufferDesc *bufHdr; Page page = BufferGetPage(buffer); - if (!BufferIsValid(buffer)) - elog(ERROR, "bad buffer ID: %d", buffer); - - if (BufferIsLocal(buffer)) - { - MarkLocalBufferDirty(buffer); - return; - } + Assert(GetPrivateRefCount(buffer) > 0); - bufHdr = GetBufferDescriptor(buffer - 1); - - Assert(GetPrivateRefCount(buffer) > 0); - /* here, either share or exclusive lock is OK */ - Assert(LWLockHeldByMe(BufferDescriptorGetContentLock(bufHdr))); + /* here, either share-exclusive or exclusive lock is OK */ + Assert(BufferLockHeldByMeInMode(bufHdr, BUFFER_LOCK_EXCLUSIVE) || + BufferLockHeldByMeInMode(bufHdr, BUFFER_LOCK_SHARE_EXCLUSIVE)); /* * This routine might get called many times on the same page, if we are * making the first scan after commit of an xact that added/deleted many - * tuples. So, be as quick as we can if the buffer is already dirty. We - * do this by not acquiring spinlock if it looks like the status bits are - * already set. Since we make this test unlocked, there's a chance we - * might fail to notice that the flags have just been cleared, and failed - * to reset them, due to memory-ordering issues. But since this function - * is only intended to be used in cases where failing to write out the - * data would be harmless anyway, it doesn't really matter. + * tuples. So, be as quick as we can if the buffer is already dirty. + * + * As we are holding (at least) a share-exclusive lock, nobody could have + * cleaned or dirtied the page concurrently, so we can just rely on the + * previously fetched value here without any danger of races. */ - if ((pg_atomic_read_u32(&bufHdr->state) & (BM_DIRTY | BM_JUST_DIRTIED)) != - (BM_DIRTY | BM_JUST_DIRTIED)) + if (unlikely(!(lockstate & BM_DIRTY))) { XLogRecPtr lsn = InvalidXLogRecPtr; - bool dirtied = false; - bool delayChkptFlags = false; - uint32 buf_state; + bool wal_log = false; + uint64 buf_state; /* * If we need to protect hint bit updates from torn writes, WAL-log a @@ -5480,8 +5728,7 @@ MarkBufferDirtyHint(Buffer buffer, bool buffer_std) * We don't check full_page_writes here because that logic is included * when we call XLogInsert() since the value changes dynamically. */ - if (XLogHintBitIsNeeded() && - (pg_atomic_read_u32(&bufHdr->state) & BM_PERMANENT)) + if (XLogHintBitIsNeeded() && (lockstate & BM_PERMANENT)) { /* * If we must not write WAL, due to a relfilelocator-specific @@ -5495,73 +5742,100 @@ MarkBufferDirtyHint(Buffer buffer, bool buffer_std) RelFileLocatorSkippingWAL(BufTagGetRelFileLocator(&bufHdr->tag))) return; - /* - * If the block is already dirty because we either made a change - * or set a hint already, then we don't need to write a full page - * image. Note that aggressive cleaning of blocks dirtied by hint - * bit setting would increase the call rate. Bulk setting of hint - * bits would reduce the call rate... - * - * We must issue the WAL record before we mark the buffer dirty. - * Otherwise we might write the page before we write the WAL. That - * causes a race condition, since a checkpoint might occur between - * writing the WAL record and marking the buffer dirty. We solve - * that with a kluge, but one that is already in use during - * transaction commit to prevent race conditions. Basically, we - * simply prevent the checkpoint WAL record from being written - * until we have marked the buffer dirty. We don't start the - * checkpoint flush until we have marked dirty, so our checkpoint - * must flush the change to disk successfully or the checkpoint - * never gets written, so crash recovery will fix. - * - * It's possible we may enter here without an xid, so it is - * essential that CreateCheckPoint waits for virtual transactions - * rather than full transactionids. - */ - Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) == 0); - MyProc->delayChkptFlags |= DELAY_CHKPT_START; - delayChkptFlags = true; - lsn = XLogSaveBufferForHint(buffer, buffer_std); + wal_log = true; } + /* + * We must mark the page dirty before we emit the WAL record, as per + * the usual rules, to ensure that BufferSync()/SyncOneBuffer() try to + * flush the buffer, even if we haven't inserted the WAL record yet. + * As we hold at least a share-exclusive lock, checkpoints will wait + * for this backend to be done with the buffer before continuing. If + * we did it the other way round, a checkpoint could start between + * writing the WAL record and marking the buffer dirty. + */ buf_state = LockBufHdr(bufHdr); + /* + * It should not be possible for the buffer to already be dirty, see + * comment above. + */ + Assert(!(buf_state & BM_DIRTY)); Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); + UnlockBufHdrExt(bufHdr, buf_state, + BM_DIRTY, + 0, 0); - if (!(buf_state & BM_DIRTY)) - { - dirtied = true; /* Means "will be dirtied by this action" */ + /* + * If the block is already dirty because we either made a change or + * set a hint already, then we don't need to write a full page image. + * Note that aggressive cleaning of blocks dirtied by hint bit setting + * would increase the call rate. Bulk setting of hint bits would + * reduce the call rate... + */ + if (wal_log) + lsn = XLogSaveBufferForHint(buffer, buffer_std); + if (XLogRecPtrIsValid(lsn)) + { /* - * Set the page LSN if we wrote a backup block. We aren't supposed - * to set this when only holding a share lock but as long as we - * serialise it somehow we're OK. We choose to set LSN while - * holding the buffer header lock, which causes any reader of an - * LSN who holds only a share lock to also obtain a buffer header - * lock before using PageGetLSN(), which is enforced in - * BufferGetLSNAtomic(). + * Set the page LSN if we wrote a backup block. To allow backends + * that only hold a share lock on the buffer to read the LSN in a + * tear-free manner, we set the page LSN while holding the buffer + * header lock. This allows any reader of an LSN who holds only a + * share lock to also obtain a buffer header lock before using + * PageGetLSN() to read the LSN in a tear free way. This is done + * in BufferGetLSNAtomic(). * * If checksums are enabled, you might think we should reset the * checksum here. That will happen when the page is written * sometime later in this checkpoint cycle. */ - if (!XLogRecPtrIsInvalid(lsn)) - PageSetLSN(page, lsn); + buf_state = LockBufHdr(bufHdr); + PageSetLSN(page, lsn); + UnlockBufHdr(bufHdr); } - buf_state |= BM_DIRTY | BM_JUST_DIRTIED; - UnlockBufHdr(bufHdr, buf_state); + pgBufferUsage.shared_blks_dirtied++; + if (VacuumCostActive) + VacuumCostBalance += VacuumCostPageDirty; + } +} - if (delayChkptFlags) - MyProc->delayChkptFlags &= ~DELAY_CHKPT_START; +/* + * MarkBufferDirtyHint + * + * Mark a buffer dirty for non-critical changes. + * + * This is essentially the same as MarkBufferDirty, except: + * + * 1. The caller does not write WAL; so if checksums are enabled, we may need + * to write an XLOG_FPI_FOR_HINT WAL record to protect against torn pages. + * 2. The caller might have only a share-exclusive-lock instead of an + * exclusive-lock on the buffer's content lock. + * 3. This function does not guarantee that the buffer is always marked dirty + * (it e.g. can't always on a hot standby), so it cannot be used for + * important changes. + */ +inline void +MarkBufferDirtyHint(Buffer buffer, bool buffer_std) +{ + BufferDesc *bufHdr; - if (dirtied) - { - pgBufferUsage.shared_blks_dirtied++; - if (VacuumCostActive) - VacuumCostBalance += VacuumCostPageDirty; - } + bufHdr = GetBufferDescriptor(buffer - 1); + + if (!BufferIsValid(buffer)) + elog(ERROR, "bad buffer ID: %d", buffer); + + if (BufferIsLocal(buffer)) + { + MarkLocalBufferDirty(buffer); + return; } + + MarkSharedBufferDirtyHint(buffer, bufHdr, + pg_atomic_read_u64(&bufHdr->state), + buffer_std); } /* @@ -5569,9 +5843,10 @@ MarkBufferDirtyHint(Buffer buffer, bool buffer_std) * * Used to clean up after errors. * - * Currently, we can expect that lwlock.c's LWLockReleaseAll() took care - * of releasing buffer content locks per se; the only thing we need to deal - * with here is clearing any PIN_COUNT request that was in progress. + * Currently, we can expect that resource owner cleanup, via + * ResOwnerReleaseBuffer(), took care of releasing buffer content locks per + * se; the only thing we need to deal with here is clearing any PIN_COUNT + * request that was in progress. */ void UnlockBuffers(void) @@ -5580,7 +5855,8 @@ UnlockBuffers(void) if (buf) { - uint32 buf_state; + uint64 buf_state; + uint64 unset_bits = 0; buf_state = LockBufHdr(buf); @@ -5590,34 +5866,744 @@ UnlockBuffers(void) */ if ((buf_state & BM_PIN_COUNT_WAITER) != 0 && buf->wait_backend_pgprocno == MyProcNumber) - buf_state &= ~BM_PIN_COUNT_WAITER; + unset_bits = BM_PIN_COUNT_WAITER; - UnlockBufHdr(buf, buf_state); + UnlockBufHdrExt(buf, buf_state, + 0, unset_bits, + 0); PinCountWaitBuf = NULL; } } /* - * Acquire or release the content_lock for the buffer. + * Acquire the buffer content lock in the specified mode + * + * If the lock is not available, sleep until it is. + * + * Side effect: cancel/die interrupts are held off until lock release. + * + * This uses almost the same locking approach as lwlock.c's + * LWLockAcquire(). See documentation at the top of lwlock.c for a more + * detailed discussion. + * + * The reason that this, and most of the other BufferLock* functions, get both + * the Buffer and BufferDesc* as parameters, is that looking up one from the + * other repeatedly shows up noticeably in profiles. + * + * Callers should provide a constant for mode, for more efficient code + * generation. + */ +static inline void +BufferLockAcquire(Buffer buffer, BufferDesc *buf_hdr, BufferLockMode mode) +{ + PrivateRefCountEntry *entry; + int extraWaits = 0; + + /* + * Get reference to the refcount entry before we hold the lock, it seems + * better to do before holding the lock. + */ + entry = GetPrivateRefCountEntry(buffer, true); + + /* + * We better not already hold a lock on the buffer. + */ + Assert(entry->data.lockmode == BUFFER_LOCK_UNLOCK); + + /* + * Lock out cancel/die interrupts until we exit the code section protected + * by the content lock. This ensures that interrupts will not interfere + * with manipulations of data structures in shared memory. + */ + HOLD_INTERRUPTS(); + + for (;;) + { + uint32 wait_event = 0; /* initialized to avoid compiler warning */ + bool mustwait; + + /* + * Try to grab the lock the first time, we're not in the waitqueue + * yet/anymore. + */ + mustwait = BufferLockAttempt(buf_hdr, mode); + + if (likely(!mustwait)) + { + break; + } + + /* + * Ok, at this point we couldn't grab the lock on the first try. We + * cannot simply queue ourselves to the end of the list and wait to be + * woken up because by now the lock could long have been released. + * Instead add us to the queue and try to grab the lock again. If we + * succeed we need to revert the queuing and be happy, otherwise we + * recheck the lock. If we still couldn't grab it, we know that the + * other locker will see our queue entries when releasing since they + * existed before we checked for the lock. + */ + + /* add to the queue */ + BufferLockQueueSelf(buf_hdr, mode); + + /* we're now guaranteed to be woken up if necessary */ + mustwait = BufferLockAttempt(buf_hdr, mode); + + /* ok, grabbed the lock the second time round, need to undo queueing */ + if (!mustwait) + { + BufferLockDequeueSelf(buf_hdr); + break; + } + + switch (mode) + { + case BUFFER_LOCK_EXCLUSIVE: + wait_event = WAIT_EVENT_BUFFER_EXCLUSIVE; + break; + case BUFFER_LOCK_SHARE_EXCLUSIVE: + wait_event = WAIT_EVENT_BUFFER_SHARE_EXCLUSIVE; + break; + case BUFFER_LOCK_SHARE: + wait_event = WAIT_EVENT_BUFFER_SHARED; + break; + case BUFFER_LOCK_UNLOCK: + pg_unreachable(); + + } + pgstat_report_wait_start(wait_event); + + /* + * Wait until awakened. + * + * It is possible that we get awakened for a reason other than being + * signaled by BufferLockWakeup(). If so, loop back and wait again. + * Once we've gotten the lock, re-increment the sema by the number of + * additional signals received. + */ + for (;;) + { + PGSemaphoreLock(MyProc->sem); + if (MyProc->lwWaiting == LW_WS_NOT_WAITING) + break; + extraWaits++; + } + + pgstat_report_wait_end(); + + /* Retrying, allow BufferLockReleaseSub to release waiters again. */ + pg_atomic_fetch_and_u64(&buf_hdr->state, ~BM_LOCK_WAKE_IN_PROGRESS); + } + + /* Remember that we now hold this lock */ + entry->data.lockmode = mode; + + /* + * Fix the process wait semaphore's count for any absorbed wakeups. + */ + while (unlikely(extraWaits-- > 0)) + PGSemaphoreUnlock(MyProc->sem); +} + +/* + * Release a previously acquired buffer content lock. + */ +static void +BufferLockUnlock(Buffer buffer, BufferDesc *buf_hdr) +{ + BufferLockMode mode; + uint64 oldstate; + uint64 sub; + + mode = BufferLockDisownInternal(buffer, buf_hdr); + + /* + * Release my hold on lock, after that it can immediately be acquired by + * others, even if we still have to wakeup other waiters. + */ + sub = BufferLockReleaseSub(mode); + + oldstate = pg_atomic_sub_fetch_u64(&buf_hdr->state, sub); + + BufferLockProcessRelease(buf_hdr, mode, oldstate); + + /* + * Now okay to allow cancel/die interrupts. + */ + RESUME_INTERRUPTS(); +} + + +/* + * Acquire the content lock for the buffer, but only if we don't have to wait. + * + * It is allowed to try to conditionally acquire a lock on a buffer that this + * backend has already locked, but the lock acquisition will always fail, even + * if the new lock acquisition does not conflict with an already held lock + * (e.g. two share locks). This is because we currently do not have space to + * track multiple lock ownerships of the same buffer within one backend. That + * is ok for the current uses of BufferLockConditional(). + */ +static bool +BufferLockConditional(Buffer buffer, BufferDesc *buf_hdr, BufferLockMode mode) +{ + PrivateRefCountEntry *entry = GetPrivateRefCountEntry(buffer, true); + bool mustwait; + + /* + * As described above, if we're trying to lock a buffer this backend + * already has locked, return false, independent of the existing and + * desired lock level. + */ + if (entry->data.lockmode != BUFFER_LOCK_UNLOCK) + return false; + + /* + * Lock out cancel/die interrupts until we exit the code section protected + * by the content lock. This ensures that interrupts will not interfere + * with manipulations of data structures in shared memory. + */ + HOLD_INTERRUPTS(); + + /* Check for the lock */ + mustwait = BufferLockAttempt(buf_hdr, mode); + + if (mustwait) + { + /* Failed to get lock, so release interrupt holdoff */ + RESUME_INTERRUPTS(); + } + else + { + entry->data.lockmode = mode; + } + + return !mustwait; +} + +/* + * Internal function that tries to atomically acquire the content lock in the + * passed in mode. + * + * This function will not block waiting for a lock to become free - that's the + * caller's job. + * + * Similar to LWLockAttemptLock(). + */ +static inline bool +BufferLockAttempt(BufferDesc *buf_hdr, BufferLockMode mode) +{ + uint64 old_state; + + /* + * Read once outside the loop, later iterations will get the newer value + * via compare & exchange. + */ + old_state = pg_atomic_read_u64(&buf_hdr->state); + + /* loop until we've determined whether we could acquire the lock or not */ + while (true) + { + uint64 desired_state; + bool lock_free; + + desired_state = old_state; + + if (mode == BUFFER_LOCK_EXCLUSIVE) + { + lock_free = (old_state & BM_LOCK_MASK) == 0; + if (lock_free) + desired_state += BM_LOCK_VAL_EXCLUSIVE; + } + else if (mode == BUFFER_LOCK_SHARE_EXCLUSIVE) + { + lock_free = (old_state & (BM_LOCK_VAL_EXCLUSIVE | BM_LOCK_VAL_SHARE_EXCLUSIVE)) == 0; + if (lock_free) + desired_state += BM_LOCK_VAL_SHARE_EXCLUSIVE; + } + else + { + lock_free = (old_state & BM_LOCK_VAL_EXCLUSIVE) == 0; + if (lock_free) + desired_state += BM_LOCK_VAL_SHARED; + } + + /* + * Attempt to swap in the state we are expecting. If we didn't see + * lock to be free, that's just the old value. If we saw it as free, + * we'll attempt to mark it acquired. The reason that we always swap + * in the value is that this doubles as a memory barrier. We could try + * to be smarter and only swap in values if we saw the lock as free, + * but benchmark haven't shown it as beneficial so far. + * + * Retry if the value changed since we last looked at it. + */ + if (likely(pg_atomic_compare_exchange_u64(&buf_hdr->state, + &old_state, desired_state))) + { + if (lock_free) + { + /* Great! Got the lock. */ + return false; + } + else + return true; /* somebody else has the lock */ + } + } + + pg_unreachable(); +} + +/* + * Add ourselves to the end of the content lock's wait queue. + */ +static void +BufferLockQueueSelf(BufferDesc *buf_hdr, BufferLockMode mode) +{ + /* + * If we don't have a PGPROC structure, there's no way to wait. This + * should never occur, since MyProc should only be null during shared + * memory initialization. + */ + if (MyProc == NULL) + elog(PANIC, "cannot wait without a PGPROC structure"); + + if (MyProc->lwWaiting != LW_WS_NOT_WAITING) + elog(PANIC, "queueing for lock while waiting on another one"); + + LockBufHdr(buf_hdr); + + /* setting the flag is protected by the spinlock */ + pg_atomic_fetch_or_u64(&buf_hdr->state, BM_LOCK_HAS_WAITERS); + + /* + * These are currently used both for lwlocks and buffer content locks, + * which is acceptable, although not pretty, because a backend can't wait + * for both types of locks at the same time. + */ + MyProc->lwWaiting = LW_WS_WAITING; + MyProc->lwWaitMode = mode; + + proclist_push_tail(&buf_hdr->lock_waiters, MyProcNumber, lwWaitLink); + + /* Can release the mutex now */ + UnlockBufHdr(buf_hdr); +} + +/* + * Remove ourselves from the waitlist. + * + * This is used if we queued ourselves because we thought we needed to sleep + * but, after further checking, we discovered that we don't actually need to + * do so. + */ +static void +BufferLockDequeueSelf(BufferDesc *buf_hdr) +{ + bool on_waitlist; + + LockBufHdr(buf_hdr); + + on_waitlist = MyProc->lwWaiting == LW_WS_WAITING; + if (on_waitlist) + proclist_delete(&buf_hdr->lock_waiters, MyProcNumber, lwWaitLink); + + if (proclist_is_empty(&buf_hdr->lock_waiters) && + (pg_atomic_read_u64(&buf_hdr->state) & BM_LOCK_HAS_WAITERS) != 0) + { + pg_atomic_fetch_and_u64(&buf_hdr->state, ~BM_LOCK_HAS_WAITERS); + } + + /* XXX: combine with fetch_and above? */ + UnlockBufHdr(buf_hdr); + + /* clear waiting state again, nice for debugging */ + if (on_waitlist) + MyProc->lwWaiting = LW_WS_NOT_WAITING; + else + { + int extraWaits = 0; + + + /* + * Somebody else dequeued us and has or will wake us up. Deal with the + * superfluous absorption of a wakeup. + */ + + /* + * Clear BM_LOCK_WAKE_IN_PROGRESS if somebody woke us before we + * removed ourselves - they'll have set it. + */ + pg_atomic_fetch_and_u64(&buf_hdr->state, ~BM_LOCK_WAKE_IN_PROGRESS); + + /* + * Now wait for the scheduled wakeup, otherwise our ->lwWaiting would + * get reset at some inconvenient point later. Most of the time this + * will immediately return. + */ + for (;;) + { + PGSemaphoreLock(MyProc->sem); + if (MyProc->lwWaiting == LW_WS_NOT_WAITING) + break; + extraWaits++; + } + + /* + * Fix the process wait semaphore's count for any absorbed wakeups. + */ + while (extraWaits-- > 0) + PGSemaphoreUnlock(MyProc->sem); + } +} + +/* + * Stop treating lock as held by current backend. + * + * After calling this function it's the callers responsibility to ensure that + * the lock gets released, even in case of an error. This only is desirable if + * the lock is going to be released in a different process than the process + * that acquired it. + */ +static inline void +BufferLockDisown(Buffer buffer, BufferDesc *buf_hdr) +{ + BufferLockDisownInternal(buffer, buf_hdr); + RESUME_INTERRUPTS(); +} + +/* + * Stop treating lock as held by current backend. + * + * This is the code that can be shared between actually releasing a lock + * (BufferLockUnlock()) and just not tracking ownership of the lock anymore + * without releasing the lock (BufferLockDisown()). + */ +static inline int +BufferLockDisownInternal(Buffer buffer, BufferDesc *buf_hdr) +{ + BufferLockMode mode; + PrivateRefCountEntry *ref; + + ref = GetPrivateRefCountEntry(buffer, false); + if (ref == NULL) + elog(ERROR, "lock %d is not held", buffer); + mode = ref->data.lockmode; + ref->data.lockmode = BUFFER_LOCK_UNLOCK; + + return mode; +} + +/* + * Wakeup all the lockers that currently have a chance to acquire the lock. + * + * wake_exclusive indicates whether exclusive lock waiters should be woken up. + */ +static void +BufferLockWakeup(BufferDesc *buf_hdr, bool wake_exclusive) +{ + bool new_wake_in_progress = false; + bool wake_share_exclusive = true; + proclist_head wakeup; + proclist_mutable_iter iter; + + proclist_init(&wakeup); + + /* lock wait list while collecting backends to wake up */ + LockBufHdr(buf_hdr); + + proclist_foreach_modify(iter, &buf_hdr->lock_waiters, lwWaitLink) + { + PGPROC *waiter = GetPGProcByNumber(iter.cur); + + /* + * Already woke up a conflicting lock, so skip over this wait list + * entry. + */ + if (!wake_exclusive && waiter->lwWaitMode == BUFFER_LOCK_EXCLUSIVE) + continue; + if (!wake_share_exclusive && waiter->lwWaitMode == BUFFER_LOCK_SHARE_EXCLUSIVE) + continue; + + proclist_delete(&buf_hdr->lock_waiters, iter.cur, lwWaitLink); + proclist_push_tail(&wakeup, iter.cur, lwWaitLink); + + /* + * Prevent additional wakeups until retryer gets to run. Backends that + * are just waiting for the lock to become free don't retry + * automatically. + */ + new_wake_in_progress = true; + + /* + * Signal that the process isn't on the wait list anymore. This allows + * BufferLockDequeueSelf() to remove itself from the waitlist with a + * proclist_delete(), rather than having to check if it has been + * removed from the list. + */ + Assert(waiter->lwWaiting == LW_WS_WAITING); + waiter->lwWaiting = LW_WS_PENDING_WAKEUP; + + /* + * Don't wakeup further waiters after waking a conflicting waiter. + */ + if (waiter->lwWaitMode == BUFFER_LOCK_SHARE) + { + /* + * Share locks conflict with exclusive locks. + */ + wake_exclusive = false; + } + else if (waiter->lwWaitMode == BUFFER_LOCK_SHARE_EXCLUSIVE) + { + /* + * Share-exclusive locks conflict with share-exclusive and + * exclusive locks. + */ + wake_exclusive = false; + wake_share_exclusive = false; + } + else if (waiter->lwWaitMode == BUFFER_LOCK_EXCLUSIVE) + { + /* + * Exclusive locks conflict with all other locks, there's no point + * in waking up anybody else. + */ + break; + } + } + + Assert(proclist_is_empty(&wakeup) || pg_atomic_read_u64(&buf_hdr->state) & BM_LOCK_HAS_WAITERS); + + /* unset required flags, and release lock, in one fell swoop */ + { + uint64 old_state; + uint64 desired_state; + + old_state = pg_atomic_read_u64(&buf_hdr->state); + while (true) + { + desired_state = old_state; + + /* compute desired flags */ + + if (new_wake_in_progress) + desired_state |= BM_LOCK_WAKE_IN_PROGRESS; + else + desired_state &= ~BM_LOCK_WAKE_IN_PROGRESS; + + if (proclist_is_empty(&buf_hdr->lock_waiters)) + desired_state &= ~BM_LOCK_HAS_WAITERS; + + desired_state &= ~BM_LOCKED; /* release lock */ + + if (pg_atomic_compare_exchange_u64(&buf_hdr->state, &old_state, + desired_state)) + break; + } + } + + /* Awaken any waiters I removed from the queue. */ + proclist_foreach_modify(iter, &wakeup, lwWaitLink) + { + PGPROC *waiter = GetPGProcByNumber(iter.cur); + + proclist_delete(&wakeup, iter.cur, lwWaitLink); + + /* + * Guarantee that lwWaiting being unset only becomes visible once the + * unlink from the link has completed. Otherwise the target backend + * could be woken up for other reason and enqueue for a new lock - if + * that happens before the list unlink happens, the list would end up + * being corrupted. + * + * The barrier pairs with the LockBufHdr() when enqueuing for another + * lock. + */ + pg_write_barrier(); + waiter->lwWaiting = LW_WS_NOT_WAITING; + PGSemaphoreUnlock(waiter->sem); + } +} + +/* + * Compute subtraction from buffer state for a release of a held lock in + * `mode`. + * + * This is separated from BufferLockUnlock() as we want to combine the lock + * release with other atomic operations when possible, leading to the lock + * release being done in multiple places, each needing to compute what to + * subtract from the lock state. + */ +static inline uint64 +BufferLockReleaseSub(BufferLockMode mode) +{ + /* + * Turns out that a switch() leads gcc to generate sufficiently worse code + * for this to show up in profiles... + */ + if (mode == BUFFER_LOCK_EXCLUSIVE) + return BM_LOCK_VAL_EXCLUSIVE; + else if (mode == BUFFER_LOCK_SHARE_EXCLUSIVE) + return BM_LOCK_VAL_SHARE_EXCLUSIVE; + else + { + Assert(mode == BUFFER_LOCK_SHARE); + return BM_LOCK_VAL_SHARED; + } + + return 0; /* keep compiler quiet */ +} + +/* + * Handle work that needs to be done after releasing a lock that was held in + * `mode`, where `lockstate` is the result of the atomic operation modifying + * the state variable. + * + * This is separated from BufferLockUnlock() as we want to combine the lock + * release with other atomic operations when possible, leading to the lock + * release being done in multiple places. + */ +static void +BufferLockProcessRelease(BufferDesc *buf_hdr, BufferLockMode mode, uint64 lockstate) +{ + bool check_waiters = false; + bool wake_exclusive = false; + + /* nobody else can have that kind of lock */ + Assert(!(lockstate & BM_LOCK_VAL_EXCLUSIVE)); + + /* + * If we're still waiting for backends to get scheduled, don't wake them + * up again. Otherwise check if we need to look through the waitqueue to + * wake other backends. + */ + if ((lockstate & BM_LOCK_HAS_WAITERS) && + !(lockstate & BM_LOCK_WAKE_IN_PROGRESS)) + { + if ((lockstate & BM_LOCK_MASK) == 0) + { + /* + * We released a lock and the lock was, in that moment, free. We + * therefore can wake waiters for any kind of lock. + */ + check_waiters = true; + wake_exclusive = true; + } + else if (mode == BUFFER_LOCK_SHARE_EXCLUSIVE) + { + /* + * We released the lock, but another backend still holds a lock. + * We can't have released an exclusive lock, as there couldn't + * have been other lock holders. If we released a share lock, no + * waiters need to be woken up, as there must be other share + * lockers. However, if we held a share-exclusive lock, another + * backend now could acquire a share-exclusive lock. + */ + check_waiters = true; + wake_exclusive = false; + } + } + + /* + * As waking up waiters requires the spinlock to be acquired, only do so + * if necessary. + */ + if (check_waiters) + BufferLockWakeup(buf_hdr, wake_exclusive); +} + +/* + * BufferLockHeldByMeInMode - test whether my process holds the content lock + * in the specified mode + * + * This is meant as debug support only. + */ +static bool +BufferLockHeldByMeInMode(BufferDesc *buf_hdr, BufferLockMode mode) +{ + PrivateRefCountEntry *entry = + GetPrivateRefCountEntry(BufferDescriptorGetBuffer(buf_hdr), false); + + if (!entry) + return false; + else + return entry->data.lockmode == mode; +} + +/* + * BufferLockHeldByMe - test whether my process holds the content lock in any + * mode + * + * This is meant as debug support only. + */ +static bool +BufferLockHeldByMe(BufferDesc *buf_hdr) +{ + PrivateRefCountEntry *entry = + GetPrivateRefCountEntry(BufferDescriptorGetBuffer(buf_hdr), false); + + if (!entry) + return false; + else + return entry->data.lockmode != BUFFER_LOCK_UNLOCK; +} + +/* + * Release the content lock for the buffer. */ void -LockBuffer(Buffer buffer, int mode) +UnlockBuffer(Buffer buffer) { - BufferDesc *buf; + BufferDesc *buf_hdr; Assert(BufferIsPinned(buffer)); if (BufferIsLocal(buffer)) return; /* local buffers need no lock */ - buf = GetBufferDescriptor(buffer - 1); + buf_hdr = GetBufferDescriptor(buffer - 1); + BufferLockUnlock(buffer, buf_hdr); +} - if (mode == BUFFER_LOCK_UNLOCK) - LWLockRelease(BufferDescriptorGetContentLock(buf)); - else if (mode == BUFFER_LOCK_SHARE) - LWLockAcquire(BufferDescriptorGetContentLock(buf), LW_SHARED); +/* + * Acquire the content_lock for the buffer. + */ +void +LockBufferInternal(Buffer buffer, BufferLockMode mode) +{ + BufferDesc *buf_hdr; + + /* + * We can't wait if we haven't got a PGPROC. This should only occur + * during bootstrap or shared memory initialization. Put an Assert here + * to catch unsafe coding practices. + */ + Assert(!(MyProc == NULL && IsUnderPostmaster)); + + /* handled in LockBuffer() wrapper */ + Assert(mode != BUFFER_LOCK_UNLOCK); + + Assert(BufferIsPinned(buffer)); + if (BufferIsLocal(buffer)) + return; /* local buffers need no lock */ + + buf_hdr = GetBufferDescriptor(buffer - 1); + + /* + * Test the most frequent lock modes first. While a switch (mode) would be + * nice, at least gcc generates considerably worse code for it. + * + * Call BufferLockAcquire() with a constant argument for mode, to generate + * more efficient code for the different lock modes. + */ + if (mode == BUFFER_LOCK_SHARE) + BufferLockAcquire(buffer, buf_hdr, BUFFER_LOCK_SHARE); else if (mode == BUFFER_LOCK_EXCLUSIVE) - LWLockAcquire(BufferDescriptorGetContentLock(buf), LW_EXCLUSIVE); + BufferLockAcquire(buffer, buf_hdr, BUFFER_LOCK_EXCLUSIVE); + else if (mode == BUFFER_LOCK_SHARE_EXCLUSIVE) + BufferLockAcquire(buffer, buf_hdr, BUFFER_LOCK_SHARE_EXCLUSIVE); else elog(ERROR, "unrecognized buffer lock mode: %d", mode); } @@ -5638,8 +6624,7 @@ ConditionalLockBuffer(Buffer buffer) buf = GetBufferDescriptor(buffer - 1); - return LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf), - LW_EXCLUSIVE); + return BufferLockConditional(buffer, buf, BUFFER_LOCK_EXCLUSIVE); } /* @@ -5709,7 +6694,8 @@ LockBufferForCleanup(Buffer buffer) for (;;) { - uint32 buf_state; + uint64 buf_state; + uint64 unset_bits = 0; /* Try to acquire lock */ LockBuffer(buffer, BUFFER_LOCK_EXCLUSIVE); @@ -5719,7 +6705,7 @@ LockBufferForCleanup(Buffer buffer) if (BUF_STATE_GET_REFCOUNT(buf_state) == 1) { /* Successfully acquired exclusive lock with pincount 1 */ - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); /* * Emit the log message if recovery conflict on buffer pin was @@ -5727,7 +6713,7 @@ LockBufferForCleanup(Buffer buffer) * deadlock_timeout for it. */ if (logged_recovery_conflict) - LogRecoveryConflict(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN, + LogRecoveryConflict(RECOVERY_CONFLICT_BUFFERPIN, waitStart, GetCurrentTimestamp(), NULL, false); @@ -5742,14 +6728,15 @@ LockBufferForCleanup(Buffer buffer) /* Failed, so mark myself as waiting for pincount 1 */ if (buf_state & BM_PIN_COUNT_WAITER) { - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); LockBuffer(buffer, BUFFER_LOCK_UNLOCK); elog(ERROR, "multiple backends attempting to wait for pincount 1"); } bufHdr->wait_backend_pgprocno = MyProcNumber; PinCountWaitBuf = bufHdr; - buf_state |= BM_PIN_COUNT_WAITER; - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdrExt(bufHdr, buf_state, + BM_PIN_COUNT_WAITER, 0, + 0); LockBuffer(buffer, BUFFER_LOCK_UNLOCK); /* Wait to be signaled by UnpinBuffer() */ @@ -5777,7 +6764,7 @@ LockBufferForCleanup(Buffer buffer) if (TimestampDifferenceExceeds(waitStart, now, DeadlockTimeout)) { - LogRecoveryConflict(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN, + LogRecoveryConflict(RECOVERY_CONFLICT_BUFFERPIN, waitStart, now, NULL, true); logged_recovery_conflict = true; } @@ -5798,7 +6785,7 @@ LockBufferForCleanup(Buffer buffer) SetStartupBufferPinWaitBufId(-1); } else - ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN); + ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP); /* * Remove flag marking us as waiter. Normally this will not be set @@ -5811,8 +6798,11 @@ LockBufferForCleanup(Buffer buffer) buf_state = LockBufHdr(bufHdr); if ((buf_state & BM_PIN_COUNT_WAITER) != 0 && bufHdr->wait_backend_pgprocno == MyProcNumber) - buf_state &= ~BM_PIN_COUNT_WAITER; - UnlockBufHdr(bufHdr, buf_state); + unset_bits |= BM_PIN_COUNT_WAITER; + + UnlockBufHdrExt(bufHdr, buf_state, + 0, unset_bits, + 0); PinCountWaitBuf = NULL; /* Loop back and try again */ @@ -5853,7 +6843,7 @@ bool ConditionalLockBufferForCleanup(Buffer buffer) { BufferDesc *bufHdr; - uint32 buf_state, + uint64 buf_state, refcount; Assert(BufferIsValid(buffer)); @@ -5889,64 +6879,249 @@ ConditionalLockBufferForCleanup(Buffer buffer) if (refcount == 1) { /* Successfully acquired exclusive lock with pincount 1 */ - UnlockBufHdr(bufHdr, buf_state); + UnlockBufHdr(bufHdr); return true; } - /* Failed, so release the lock */ - UnlockBufHdr(bufHdr, buf_state); - LockBuffer(buffer, BUFFER_LOCK_UNLOCK); - return false; + /* Failed, so release the lock */ + UnlockBufHdr(bufHdr); + LockBuffer(buffer, BUFFER_LOCK_UNLOCK); + return false; +} + +/* + * IsBufferCleanupOK - as above, but we already have the lock + * + * Check whether it's OK to perform cleanup on a buffer we've already + * locked. If we observe that the pin count is 1, our exclusive lock + * happens to be a cleanup lock, and we can proceed with anything that + * would have been allowable had we sought a cleanup lock originally. + */ +bool +IsBufferCleanupOK(Buffer buffer) +{ + BufferDesc *bufHdr; + uint64 buf_state; + + Assert(BufferIsValid(buffer)); + + /* see AIO related comment in LockBufferForCleanup() */ + + if (BufferIsLocal(buffer)) + { + /* There should be exactly one pin */ + if (LocalRefCount[-buffer - 1] != 1) + return false; + /* Nobody else to wait for */ + return true; + } + + /* There should be exactly one local pin */ + if (GetPrivateRefCount(buffer) != 1) + return false; + + bufHdr = GetBufferDescriptor(buffer - 1); + + /* caller must hold exclusive lock on buffer */ + Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE)); + + buf_state = LockBufHdr(bufHdr); + + Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); + if (BUF_STATE_GET_REFCOUNT(buf_state) == 1) + { + /* pincount is OK. */ + UnlockBufHdr(bufHdr); + return true; + } + + UnlockBufHdr(bufHdr); + return false; +} + +/* + * Helper for BufferBeginSetHintBits() and BufferSetHintBits16(). + * + * This checks if the current lock mode already suffices to allow hint bits + * being set and, if not, whether the current lock can be upgraded. + * + * Updates *lockstate when returning true. + */ +static inline bool +SharedBufferBeginSetHintBits(Buffer buffer, BufferDesc *buf_hdr, uint64 *lockstate) +{ + uint64 old_state; + PrivateRefCountEntry *ref; + BufferLockMode mode; + + ref = GetPrivateRefCountEntry(buffer, true); + + if (ref == NULL) + elog(ERROR, "buffer is not pinned"); + + mode = ref->data.lockmode; + if (mode == BUFFER_LOCK_UNLOCK) + elog(ERROR, "buffer is not locked"); + + /* we're done if we are already holding a sufficient lock level */ + if (mode == BUFFER_LOCK_EXCLUSIVE || mode == BUFFER_LOCK_SHARE_EXCLUSIVE) + { + *lockstate = pg_atomic_read_u64(&buf_hdr->state); + return true; + } + + /* + * We are only holding a share lock right now, try to upgrade it to + * SHARE_EXCLUSIVE. + */ + Assert(mode == BUFFER_LOCK_SHARE); + + old_state = pg_atomic_read_u64(&buf_hdr->state); + while (true) + { + uint64 desired_state; + + desired_state = old_state; + + /* + * Can't upgrade if somebody else holds the lock in exclusive or + * share-exclusive mode. + */ + if (unlikely((old_state & (BM_LOCK_VAL_EXCLUSIVE | BM_LOCK_VAL_SHARE_EXCLUSIVE)) != 0)) + { + return false; + } + + /* currently held lock state */ + desired_state -= BM_LOCK_VAL_SHARED; + + /* new lock level */ + desired_state += BM_LOCK_VAL_SHARE_EXCLUSIVE; + + if (likely(pg_atomic_compare_exchange_u64(&buf_hdr->state, + &old_state, desired_state))) + { + ref->data.lockmode = BUFFER_LOCK_SHARE_EXCLUSIVE; + *lockstate = desired_state; + + return true; + } + } +} + +/* + * Try to acquire the right to set hint bits on the buffer. + * + * To be allowed to set hint bits, this backend needs to hold either a + * share-exclusive or an exclusive lock. In case this backend only holds a + * share lock, this function will try to upgrade the lock to + * share-exclusive. The caller is only allowed to set hint bits if true is + * returned. + * + * Once BufferBeginSetHintBits() has returned true, hint bits may be set + * without further calls to BufferBeginSetHintBits(), until the buffer is + * unlocked. + * + * + * Requiring a share-exclusive lock to set hint bits prevents setting hint + * bits on buffers that are currently being written out, which could corrupt + * the checksum on the page. Flushing buffers also requires a share-exclusive + * lock. + * + * Due to a lock >= share-exclusive being required to set hint bits, only one + * backend can set hint bits at a time. Allowing multiple backends to set hint + * bits would require more complicated locking: For setting hint bits we'd + * need to store the count of backends currently setting hint bits, for I/O we + * would need another lock-level conflicting with the hint-setting + * lock-level. Given that the share-exclusive lock for setting hint bits is + * only held for a short time, that backends often would just set the same + * hint bits and that the cost of occasionally not setting hint bits in hotly + * accessed pages is fairly low, this seems like an acceptable tradeoff. + */ +bool +BufferBeginSetHintBits(Buffer buffer) +{ + BufferDesc *buf_hdr; + uint64 lockstate; + + if (BufferIsLocal(buffer)) + { + /* + * NB: Will need to check if there is a write in progress, once it is + * possible for writes to be done asynchronously. + */ + return true; + } + + buf_hdr = GetBufferDescriptor(buffer - 1); + + return SharedBufferBeginSetHintBits(buffer, buf_hdr, &lockstate); +} + +/* + * End a phase of setting hint bits on this buffer, started with + * BufferBeginSetHintBits(). + * + * This would strictly speaking not be required (i.e. the caller could do + * MarkBufferDirtyHint() if so desired), but allows us to perform some sanity + * checks. + */ +void +BufferFinishSetHintBits(Buffer buffer, bool mark_dirty, bool buffer_std) +{ + if (!BufferIsLocal(buffer)) + Assert(BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_SHARE_EXCLUSIVE) || + BufferIsLockedByMeInMode(buffer, BUFFER_LOCK_EXCLUSIVE)); + + if (mark_dirty) + MarkBufferDirtyHint(buffer, buffer_std); } /* - * IsBufferCleanupOK - as above, but we already have the lock + * Try to set hint bits on a single 16bit value in a buffer. * - * Check whether it's OK to perform cleanup on a buffer we've already - * locked. If we observe that the pin count is 1, our exclusive lock - * happens to be a cleanup lock, and we can proceed with anything that - * would have been allowable had we sought a cleanup lock originally. + * If hint bits are allowed to be set, set *ptr = val, try to mark the buffer + * dirty and return true. Otherwise false is returned. + * + * *ptr needs to be a pointer to memory within the buffer. + * + * This is a bit faster than BufferBeginSetHintBits() / + * BufferFinishSetHintBits() when setting hints once in a buffer, but slower + * than the former when setting hint bits multiple times in the same buffer. */ bool -IsBufferCleanupOK(Buffer buffer) +BufferSetHintBits16(uint16 *ptr, uint16 val, Buffer buffer) { - BufferDesc *bufHdr; - uint32 buf_state; - - Assert(BufferIsValid(buffer)); + BufferDesc *buf_hdr; + uint64 lockstate; +#ifdef USE_ASSERT_CHECKING + char *page; - /* see AIO related comment in LockBufferForCleanup() */ + /* verify that the address is on the page */ + page = BufferGetPage(buffer); + Assert((char *) ptr >= page && (char *) ptr < (page + BLCKSZ)); +#endif if (BufferIsLocal(buffer)) { - /* There should be exactly one pin */ - if (LocalRefCount[-buffer - 1] != 1) - return false; - /* Nobody else to wait for */ + *ptr = val; + + MarkLocalBufferDirty(buffer); + return true; } - /* There should be exactly one local pin */ - if (GetPrivateRefCount(buffer) != 1) - return false; - - bufHdr = GetBufferDescriptor(buffer - 1); + buf_hdr = GetBufferDescriptor(buffer - 1); - /* caller must hold exclusive lock on buffer */ - Assert(LWLockHeldByMeInMode(BufferDescriptorGetContentLock(bufHdr), - LW_EXCLUSIVE)); + if (SharedBufferBeginSetHintBits(buffer, buf_hdr, &lockstate)) + { + *ptr = val; - buf_state = LockBufHdr(bufHdr); + MarkSharedBufferDirtyHint(buffer, buf_hdr, lockstate, true); - Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); - if (BUF_STATE_GET_REFCOUNT(buf_state) == 1) - { - /* pincount is OK. */ - UnlockBufHdr(bufHdr, buf_state); return true; } - UnlockBufHdr(bufHdr, buf_state); return false; } @@ -5965,10 +7140,17 @@ WaitIO(BufferDesc *buf) { ConditionVariable *cv = BufferDescriptorGetIOCV(buf); + /* + * Should never end up here with unsubmitted IO, as no AIO unaware code + * may be used while in batch mode and AIO aware code needs to have + * submitted all staged IO to avoid deadlocks & slowness. + */ + Assert(!pgaio_have_staged()); + ConditionVariablePrepareToSleep(cv); for (;;) { - uint32 buf_state; + uint64 buf_state; PgAioWaitRef iow; /* @@ -5984,7 +7166,7 @@ WaitIO(BufferDesc *buf) * clearing the wref while it's being read. */ iow = buf->io_wref; - UnlockBufHdr(buf, buf_state); + UnlockBufHdr(buf); /* no IO in progress, we don't need to wait */ if (!(buf_state & BM_IO_IN_PROGRESS)) @@ -6017,32 +7199,48 @@ WaitIO(BufferDesc *buf) } /* - * StartBufferIO: begin I/O on this buffer + * StartSharedBufferIO: begin I/O on this buffer * (Assumptions) - * My process is executing no IO on this buffer * The buffer is Pinned * - * In some scenarios multiple backends could attempt the same I/O operation - * concurrently. If someone else has already started I/O on this buffer then - * we will wait for completion of the IO using WaitIO(). - * - * Input operations are only attempted on buffers that are not BM_VALID, - * and output operations only on buffers that are BM_VALID and BM_DIRTY, - * so we can always tell if the work is already done. - * - * Returns true if we successfully marked the buffer as I/O busy, - * false if someone else already did the work. - * - * If nowait is true, then we don't wait for an I/O to be finished by another - * backend. In that case, false indicates either that the I/O was already - * finished, or is still in progress. This is useful for callers that want to - * find out if they can perform the I/O as part of a larger operation, without - * waiting for the answer or distinguishing the reasons why not. + * In several scenarios the buffer may already be undergoing I/O in this or + * another backend. How to best handle that depends on the caller's + * situation. It might be appropriate to wait synchronously (e.g., because the + * buffer is about to be invalidated); wait asynchronously, using the buffer's + * IO wait reference (e.g., because the caller is doing readahead and doesn't + * need the buffer to be ready immediately); or to not wait at all (e.g., + * because the caller is trying to combine IO for this buffer with another + * buffer). + * + * How and whether to wait is controlled by the wait and io_wref + * parameters. In detail: + * + * - If the caller passes a non-NULL io_wref and the buffer has an I/O wait + * reference, the *io_wref is set to the buffer's io_wref and + * BUFFER_IO_IN_PROGRESS is returned. This is done regardless of the wait + * parameter. + * + * - If the caller passes a NULL io_wref (i.e. the caller does not want to + * asynchronously wait for the completion of the IO), wait = false and the + * buffer is undergoing IO, BUFFER_IO_IN_PROGRESS is returned. + * + * - If wait = true and either the buffer does not have a wait reference, + * or the caller passes io_wref = NULL, WaitIO() is used to wait for the IO + * to complete. To avoid the potential of deadlocks and unnecessary delays, + * all staged I/O is submitted before waiting. + * + * Input operations are only attempted on buffers that are not BM_VALID, and + * output operations only on buffers that are BM_VALID and BM_DIRTY, so we can + * always tell if the work is already done. If no I/O is necessary, + * BUFFER_IO_ALREADY_DONE is returned. + * + * If we successfully marked the buffer as BM_IO_IN_PROGRESS, + * BUFFER_IO_READY_FOR_IO is returned. */ -bool -StartBufferIO(BufferDesc *buf, bool forInput, bool nowait) +StartBufferIOResult +StartSharedBufferIO(BufferDesc *buf, bool forInput, bool wait, PgAioWaitRef *io_wref) { - uint32 buf_state; + uint64 buf_state; ResourceOwnerEnlarge(CurrentResourceOwner); @@ -6052,10 +7250,42 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait) if (!(buf_state & BM_IO_IN_PROGRESS)) break; - UnlockBufHdr(buf, buf_state); - if (nowait) - return false; - WaitIO(buf); + + /* Join the existing IO */ + if (io_wref != NULL && pgaio_wref_valid(&buf->io_wref)) + { + *io_wref = buf->io_wref; + UnlockBufHdr(buf); + + return BUFFER_IO_IN_PROGRESS; + } + else if (!wait) + { + UnlockBufHdr(buf); + return BUFFER_IO_IN_PROGRESS; + } + else + { + /* + * With wait = true, we always have to wait if the caller has + * passed io_wref = NULL. + * + * Even with io_wref != NULL, we have to wait if the buffer's wait + * ref is not valid but the IO is in progress, someone else + * started IO but hasn't set the wait ref yet. We have no choice + * but to wait until the IO completes. + */ + UnlockBufHdr(buf); + + /* + * If this backend currently has staged IO, submit it before + * waiting for in-progress IO, to avoid potential deadlocks and + * unnecessary delays. + */ + pgaio_submit_staged(); + + WaitIO(buf); + } } /* Once we get here, there is definitely no I/O active on this buffer */ @@ -6063,17 +7293,47 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait) /* Check if someone else already did the I/O */ if (forInput ? (buf_state & BM_VALID) : !(buf_state & BM_DIRTY)) { - UnlockBufHdr(buf, buf_state); - return false; + UnlockBufHdr(buf); + return BUFFER_IO_ALREADY_DONE; } - buf_state |= BM_IO_IN_PROGRESS; - UnlockBufHdr(buf, buf_state); + /* + * No IO in progress and not already done; we will start IO. It's possible + * that the IO was in progress but we're not done, because the IO errored + * out. We'll do the IO ourselves. + */ + UnlockBufHdrExt(buf, buf_state, + BM_IO_IN_PROGRESS, 0, + 0); ResourceOwnerRememberBufferIO(CurrentResourceOwner, BufferDescriptorGetBuffer(buf)); - return true; + return BUFFER_IO_READY_FOR_IO; +} + +/* + * Wrapper around StartSharedBufferIO / StartLocalBufferIO. Only to be used + * when the caller doesn't otherwise need to care about local vs shared. See + * StartSharedBufferIO() for details. + */ +StartBufferIOResult +StartBufferIO(Buffer buffer, bool forInput, bool wait, PgAioWaitRef *io_wref) +{ + BufferDesc *buf_hdr; + + if (BufferIsLocal(buffer)) + { + buf_hdr = GetLocalBufferDescriptor(-buffer - 1); + + return StartLocalBufferIO(buf_hdr, forInput, wait, io_wref); + } + else + { + buf_hdr = GetBufferDescriptor(buffer - 1); + + return StartSharedBufferIO(buf_hdr, forInput, wait, io_wref); + } } /* @@ -6083,10 +7343,8 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait) * BM_IO_IN_PROGRESS bit is set for the buffer * The buffer is Pinned * - * If clear_dirty is true and BM_JUST_DIRTIED is not set, we clear the - * buffer's BM_DIRTY flag. This is appropriate when terminating a - * successful write. The check on BM_JUST_DIRTIED is necessary to avoid - * marking the buffer clean if it was re-dirtied while we were writing. + * If clear_dirty is true, we clear the buffer's BM_DIRTY flag. This is + * appropriate when terminating a successful write. * * set_flag_bits gets ORed into the buffer's flags. It must include * BM_IO_ERROR in a failure case. For successful completion it could @@ -6097,32 +7355,35 @@ StartBufferIO(BufferDesc *buf, bool forInput, bool nowait) * is being released) */ void -TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits, +TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag_bits, bool forget_owner, bool release_aio) { - uint32 buf_state; + uint64 buf_state; + uint64 unset_flag_bits = 0; + int refcount_change = 0; buf_state = LockBufHdr(buf); Assert(buf_state & BM_IO_IN_PROGRESS); - buf_state &= ~BM_IO_IN_PROGRESS; + unset_flag_bits |= BM_IO_IN_PROGRESS; /* Clear earlier errors, if this IO failed, it'll be marked again */ - buf_state &= ~BM_IO_ERROR; + unset_flag_bits |= BM_IO_ERROR; - if (clear_dirty && !(buf_state & BM_JUST_DIRTIED)) - buf_state &= ~(BM_DIRTY | BM_CHECKPOINT_NEEDED); + if (clear_dirty) + unset_flag_bits |= BM_DIRTY | BM_CHECKPOINT_NEEDED; if (release_aio) { /* release ownership by the AIO subsystem */ Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); - buf_state -= BUF_REFCOUNT_ONE; + refcount_change = -1; pgaio_wref_clear(&buf->io_wref); } - buf_state |= set_flag_bits; - UnlockBufHdr(buf, buf_state); + buf_state = UnlockBufHdrExt(buf, buf_state, + set_flag_bits, unset_flag_bits, + refcount_change); if (forget_owner) ResourceOwnerForgetBufferIO(CurrentResourceOwner, @@ -6145,8 +7406,8 @@ TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits, /* * AbortBufferIO: Clean up active buffer I/O after an error. * - * All LWLocks we might have held have been released, - * but we haven't yet released buffer pins, so the buffer is still pinned. + * All LWLocks & content locks we might have held have been released, but we + * haven't yet released buffer pins, so the buffer is still pinned. * * If I/O was in progress, we always set BM_IO_ERROR, even though it's * possible the error condition wasn't related to the I/O. @@ -6159,7 +7420,7 @@ static void AbortBufferIO(Buffer buffer) { BufferDesc *buf_hdr = GetBufferDescriptor(buffer - 1); - uint32 buf_state; + uint64 buf_state; buf_state = LockBufHdr(buf_hdr); Assert(buf_state & (BM_IO_IN_PROGRESS | BM_TAG_VALID)); @@ -6167,12 +7428,12 @@ AbortBufferIO(Buffer buffer) if (!(buf_state & BM_VALID)) { Assert(!(buf_state & BM_DIRTY)); - UnlockBufHdr(buf_hdr, buf_state); + UnlockBufHdr(buf_hdr); } else { Assert(buf_state & BM_DIRTY); - UnlockBufHdr(buf_hdr, buf_state); + UnlockBufHdr(buf_hdr); /* Issue notice if this is not the first failure... */ if (buf_state & BM_IO_ERROR) @@ -6201,7 +7462,7 @@ shared_buffer_write_error_callback(void *arg) /* Buffer is pinned, so we can read the tag without locking the spinlock */ if (bufHdr != NULL) - errcontext("writing block %u of relation %s", + errcontext("writing block %u of relation \"%s\"", bufHdr->tag.blockNum, relpathperm(BufTagGetRelFileLocator(&bufHdr->tag), BufTagGetForkNum(&bufHdr->tag)).str); @@ -6216,7 +7477,7 @@ local_buffer_write_error_callback(void *arg) BufferDesc *bufHdr = (BufferDesc *) arg; if (bufHdr != NULL) - errcontext("writing block %u of relation %s", + errcontext("writing block %u of relation \"%s\"", bufHdr->tag.blockNum, relpathbackend(BufTagGetRelFileLocator(&bufHdr->tag), MyProcNumber, @@ -6253,26 +7514,44 @@ rlocator_comparator(const void *p1, const void *p2) /* * Lock buffer header - set BM_LOCKED in buffer state. */ -uint32 +uint64 LockBufHdr(BufferDesc *desc) { - SpinDelayStatus delayStatus; - uint32 old_buf_state; + uint64 old_buf_state; Assert(!BufferIsLocal(BufferDescriptorGetBuffer(desc))); - init_local_spin_delay(&delayStatus); - while (true) { - /* set BM_LOCKED flag */ - old_buf_state = pg_atomic_fetch_or_u32(&desc->state, BM_LOCKED); - /* if it wasn't set before we're OK */ - if (!(old_buf_state & BM_LOCKED)) - break; - perform_spin_delay(&delayStatus); + /* + * Always try once to acquire the lock directly, without setting up + * the spin-delay infrastructure. The work necessary for that shows up + * in profiles and is rarely necessary. + */ + old_buf_state = pg_atomic_fetch_or_u64(&desc->state, BM_LOCKED); + if (likely(!(old_buf_state & BM_LOCKED))) + break; /* got lock */ + + /* and then spin without atomic operations until lock is released */ + { + SpinDelayStatus delayStatus; + + init_local_spin_delay(&delayStatus); + + while (old_buf_state & BM_LOCKED) + { + perform_spin_delay(&delayStatus); + old_buf_state = pg_atomic_read_u64(&desc->state); + } + finish_spin_delay(&delayStatus); + } + + /* + * Retry. The lock might obviously already be re-acquired by the time + * we're attempting to get it again. + */ } - finish_spin_delay(&delayStatus); + return old_buf_state | BM_LOCKED; } @@ -6283,20 +7562,20 @@ LockBufHdr(BufferDesc *desc) * Obviously the buffer could be locked by the time the value is returned, so * this is primarily useful in CAS style loops. */ -static uint32 +pg_noinline uint64 WaitBufHdrUnlocked(BufferDesc *buf) { SpinDelayStatus delayStatus; - uint32 buf_state; + uint64 buf_state; init_local_spin_delay(&delayStatus); - buf_state = pg_atomic_read_u32(&buf->state); + buf_state = pg_atomic_read_u64(&buf->state); while (buf_state & BM_LOCKED) { perform_spin_delay(&delayStatus); - buf_state = pg_atomic_read_u32(&buf->state); + buf_state = pg_atomic_read_u64(&buf->state); } finish_spin_delay(&delayStatus); @@ -6375,8 +7654,8 @@ ckpt_buforder_comparator(const CkptSortItem *a, const CkptSortItem *b) static int ts_ckpt_progress_comparator(Datum a, Datum b, void *arg) { - CkptTsStatus *sa = (CkptTsStatus *) a; - CkptTsStatus *sb = (CkptTsStatus *) b; + CkptTsStatus *sa = (CkptTsStatus *) DatumGetPointer(a); + CkptTsStatus *sb = (CkptTsStatus *) DatumGetPointer(b); /* we want a min-heap, so return 1 for the a < b */ if (sa->progress < sb->progress) @@ -6556,8 +7835,14 @@ ResOwnerPrintBufferIO(Datum res) return psprintf("lost track of buffer IO on buffer %d", buffer); } +/* + * Release buffer as part of resource owner cleanup. This will only be called + * if the buffer is pinned. If this backend held the content lock at the time + * of the error we also need to release that (note that it is not possible to + * hold a content lock without a pin). + */ static void -ResOwnerReleaseBufferPin(Datum res) +ResOwnerReleaseBuffer(Datum res) { Buffer buffer = DatumGetInt32(res); @@ -6568,11 +7853,32 @@ ResOwnerReleaseBufferPin(Datum res) if (BufferIsLocal(buffer)) UnpinLocalBufferNoOwner(buffer); else + { + PrivateRefCountEntry *ref; + + ref = GetPrivateRefCountEntry(buffer, false); + + /* not having a private refcount would imply resowner corruption */ + Assert(ref != NULL); + + /* + * If the buffer was locked at the time of the resowner release, + * release the lock now. This should only happen after errors. + */ + if (ref->data.lockmode != BUFFER_LOCK_UNLOCK) + { + BufferDesc *buf = GetBufferDescriptor(buffer - 1); + + HOLD_INTERRUPTS(); /* match the upcoming RESUME_INTERRUPTS */ + BufferLockUnlock(buffer, buf); + } + UnpinBufferNoOwner(GetBufferDescriptor(buffer - 1)); + } } static char * -ResOwnerPrintBufferPin(Datum res) +ResOwnerPrintBuffer(Datum res) { return DebugPrintBufferRefcount(DatumGetInt32(res)); } @@ -6584,24 +7890,24 @@ ResOwnerPrintBufferPin(Datum res) static bool EvictUnpinnedBufferInternal(BufferDesc *desc, bool *buffer_flushed) { - uint32 buf_state; + uint64 buf_state; bool result; *buffer_flushed = false; - buf_state = pg_atomic_read_u32(&(desc->state)); + buf_state = pg_atomic_read_u64(&(desc->state)); Assert(buf_state & BM_LOCKED); if ((buf_state & BM_VALID) == 0) { - UnlockBufHdr(desc, buf_state); + UnlockBufHdr(desc); return false; } /* Check that it's not pinned already. */ if (BUF_STATE_GET_REFCOUNT(buf_state) > 0) { - UnlockBufHdr(desc, buf_state); + UnlockBufHdr(desc); return false; } @@ -6610,10 +7916,8 @@ EvictUnpinnedBufferInternal(BufferDesc *desc, bool *buffer_flushed) /* If it was dirty, try to clean it once. */ if (buf_state & BM_DIRTY) { - LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED); - FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); + FlushUnlockedBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL); *buffer_flushed = true; - LWLockRelease(BufferDescriptorGetContentLock(desc)); } /* This will return false if it becomes dirty or someone else pins it. */ @@ -6685,10 +7989,12 @@ EvictAllUnpinnedBuffers(int32 *buffers_evicted, int32 *buffers_flushed, for (int buf = 1; buf <= NBuffers; buf++) { BufferDesc *desc = GetBufferDescriptor(buf - 1); - uint32 buf_state; + uint64 buf_state; bool buffer_flushed; - buf_state = pg_atomic_read_u32(&desc->state); + CHECK_FOR_INTERRUPTS(); + + buf_state = pg_atomic_read_u64(&desc->state); if (!(buf_state & BM_VALID)) continue; @@ -6735,9 +8041,11 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, for (int buf = 1; buf <= NBuffers; buf++) { BufferDesc *desc = GetBufferDescriptor(buf - 1); - uint32 buf_state = pg_atomic_read_u32(&(desc->state)); + uint64 buf_state = pg_atomic_read_u64(&(desc->state)); bool buffer_flushed; + CHECK_FOR_INTERRUPTS(); + /* An unlocked precheck should be safe and saves some cycles. */ if ((buf_state & BM_VALID) == 0 || !BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator)) @@ -6753,7 +8061,7 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, if ((buf_state & BM_VALID) == 0 || !BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator)) { - UnlockBufHdr(desc, buf_state); + UnlockBufHdr(desc); continue; } @@ -6767,6 +8075,194 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, } } +/* + * Helper function to mark unpinned buffer dirty whose buffer header lock is + * already acquired. + */ +static bool +MarkDirtyUnpinnedBufferInternal(Buffer buf, BufferDesc *desc, + bool *buffer_already_dirty) +{ + uint64 buf_state; + bool result = false; + + *buffer_already_dirty = false; + + buf_state = pg_atomic_read_u64(&(desc->state)); + Assert(buf_state & BM_LOCKED); + + if ((buf_state & BM_VALID) == 0) + { + UnlockBufHdr(desc); + return false; + } + + /* Check that it's not pinned already. */ + if (BUF_STATE_GET_REFCOUNT(buf_state) > 0) + { + UnlockBufHdr(desc); + return false; + } + + /* Pin the buffer and then release the buffer spinlock */ + PinBuffer_Locked(desc); + + /* If it was not already dirty, mark it as dirty. */ + if (!(buf_state & BM_DIRTY)) + { + BufferLockAcquire(buf, desc, BUFFER_LOCK_EXCLUSIVE); + MarkBufferDirty(buf); + result = true; + BufferLockUnlock(buf, desc); + } + else + *buffer_already_dirty = true; + + UnpinBuffer(desc); + + return result; +} + +/* + * Try to mark the provided shared buffer as dirty. + * + * This function is intended for testing/development use only! + * + * Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside. + * + * The buffer_already_dirty parameter is mandatory and indicate if the buffer + * could not be dirtied because it is already dirty. + * + * Returns true if the buffer has successfully been marked as dirty. + */ +bool +MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty) +{ + BufferDesc *desc; + bool buffer_dirtied = false; + + Assert(!BufferIsLocal(buf)); + + /* Make sure we can pin the buffer. */ + ResourceOwnerEnlarge(CurrentResourceOwner); + ReservePrivateRefCountEntry(); + + desc = GetBufferDescriptor(buf - 1); + LockBufHdr(desc); + + buffer_dirtied = MarkDirtyUnpinnedBufferInternal(buf, desc, buffer_already_dirty); + /* Both can not be true at the same time */ + Assert(!(buffer_dirtied && *buffer_already_dirty)); + + return buffer_dirtied; +} + +/* + * Try to mark all the shared buffers containing provided relation's pages as + * dirty. + * + * This function is intended for testing/development use only! See + * MarkDirtyUnpinnedBuffer(). + * + * The buffers_* parameters are mandatory and indicate the total count of + * buffers that: + * - buffers_dirtied - were dirtied + * - buffers_already_dirty - were already dirty + * - buffers_skipped - could not be dirtied because of a reason different + * than a buffer being already dirty. + */ +void +MarkDirtyRelUnpinnedBuffers(Relation rel, + int32 *buffers_dirtied, + int32 *buffers_already_dirty, + int32 *buffers_skipped) +{ + Assert(!RelationUsesLocalBuffers(rel)); + + *buffers_dirtied = 0; + *buffers_already_dirty = 0; + *buffers_skipped = 0; + + for (int buf = 1; buf <= NBuffers; buf++) + { + BufferDesc *desc = GetBufferDescriptor(buf - 1); + uint64 buf_state = pg_atomic_read_u64(&(desc->state)); + bool buffer_already_dirty; + + CHECK_FOR_INTERRUPTS(); + + /* An unlocked precheck should be safe and saves some cycles. */ + if ((buf_state & BM_VALID) == 0 || + !BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator)) + continue; + + /* Make sure we can pin the buffer. */ + ResourceOwnerEnlarge(CurrentResourceOwner); + ReservePrivateRefCountEntry(); + + buf_state = LockBufHdr(desc); + + /* recheck, could have changed without the lock */ + if ((buf_state & BM_VALID) == 0 || + !BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator)) + { + UnlockBufHdr(desc); + continue; + } + + if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty)) + (*buffers_dirtied)++; + else if (buffer_already_dirty) + (*buffers_already_dirty)++; + else + (*buffers_skipped)++; + } +} + +/* + * Try to mark all the shared buffers as dirty. + * + * This function is intended for testing/development use only! See + * MarkDirtyUnpinnedBuffer(). + * + * See MarkDirtyRelUnpinnedBuffers() above for details about the buffers_* + * parameters. + */ +void +MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied, + int32 *buffers_already_dirty, + int32 *buffers_skipped) +{ + *buffers_dirtied = 0; + *buffers_already_dirty = 0; + *buffers_skipped = 0; + + for (int buf = 1; buf <= NBuffers; buf++) + { + BufferDesc *desc = GetBufferDescriptor(buf - 1); + uint64 buf_state; + bool buffer_already_dirty; + + CHECK_FOR_INTERRUPTS(); + + buf_state = pg_atomic_read_u64(&desc->state); + if (!(buf_state & BM_VALID)) + continue; + + ResourceOwnerEnlarge(CurrentResourceOwner); + ReservePrivateRefCountEntry(); + + LockBufHdr(desc); + + if (MarkDirtyUnpinnedBufferInternal(buf, desc, &buffer_already_dirty)) + (*buffers_dirtied)++; + else if (buffer_already_dirty) + (*buffers_already_dirty)++; + else + (*buffers_skipped)++; + } +} + /* * Generic implementation of the AIO handle staging callback for readv/writev * on local/shared buffers. @@ -6800,7 +8296,7 @@ buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp) BufferDesc *buf_hdr = is_temp ? GetLocalBufferDescriptor(-buffer - 1) : GetBufferDescriptor(buffer - 1); - uint32 buf_state; + uint64 buf_state; /* * Check that all the buffers are actually ones that could conceivably @@ -6818,7 +8314,7 @@ buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp) } if (is_temp) - buf_state = pg_atomic_read_u32(&buf_hdr->state); + buf_state = pg_atomic_read_u64(&buf_hdr->state); else buf_state = LockBufHdr(buf_hdr); @@ -6851,13 +8347,15 @@ buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp) * * This pin is released again in TerminateBufferIO(). */ - buf_state += BUF_REFCOUNT_ONE; buf_hdr->io_wref = io_ref; if (is_temp) - pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state); + { + buf_state += BUF_REFCOUNT_ONE; + pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state); + } else - UnlockBufHdr(buf_hdr, buf_state); + UnlockBufHdrExt(buf_hdr, buf_state, 0, 0, 1); /* * Ensure the content lock that prevents buffer modifications while @@ -6866,16 +8364,12 @@ buffer_stage_common(PgAioHandle *ioh, bool is_write, bool is_temp) */ if (is_write && !is_temp) { - LWLock *content_lock; - - content_lock = BufferDescriptorGetContentLock(buf_hdr); - - Assert(LWLockHeldByMe(content_lock)); + Assert(BufferLockHeldByMe(buf_hdr)); /* * Lock is now owned by AIO subsystem. */ - LWLockDisown(content_lock); + BufferLockDisown(buffer, buf_hdr); } /* @@ -6950,9 +8444,9 @@ buffer_readv_encode_error(PgAioResult *result, error_count > 0 ? error_count : zeroed_count; uint8 first_off; - StaticAssertStmt(PG_IOV_MAX <= 1 << READV_COUNT_BITS, + StaticAssertDecl(PG_IOV_MAX <= 1 << READV_COUNT_BITS, "PG_IOV_MAX is bigger than reserved space for error data"); - StaticAssertStmt((1 + 1 + 3 * READV_COUNT_BITS) <= PGAIO_RESULT_ERROR_BITS, + StaticAssertDecl((1 + 1 + 3 * READV_COUNT_BITS) <= PGAIO_RESULT_ERROR_BITS, "PGAIO_RESULT_ERROR_BITS is insufficient for buffer_readv"); /* @@ -7040,13 +8534,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer, : GetBufferDescriptor(buffer - 1); BufferTag tag = buf_hdr->tag; char *bufdata = BufferGetBlock(buffer); - uint32 set_flag_bits; + uint64 set_flag_bits; int piv_flags; /* check that the buffer is in the expected state for a read */ #ifdef USE_ASSERT_CHECKING { - uint32 buf_state = pg_atomic_read_u32(&buf_hdr->state); + uint64 buf_state = pg_atomic_read_u64(&buf_hdr->state); Assert(buf_state & BM_TAG_VALID); Assert(!(buf_state & BM_VALID)); @@ -7073,6 +8567,13 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer, if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES) piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE; + /* + * If the buffers are marked for zero on error, we want to log that in + * case of a checksum failure. + */ + if (flags & READ_BUFFERS_ZERO_ON_ERROR) + piv_flags |= PIV_ZERO_BUFFERS_ON_ERROR; + /* Check for garbage data. */ if (!failed) { @@ -7315,13 +8816,15 @@ buffer_readv_report(PgAioResult result, const PgAioTargetData *td, ereport(elevel, errcode(ERRCODE_DATA_CORRUPTED), - errmsg("zeroing %u page(s) and ignoring %u checksum failure(s) among blocks %u..%u of relation %s", + errmsg("zeroing %u page(s) and ignoring %u checksum failure(s) among blocks %u..%u of relation \"%s\"", affected_count, checkfail_count, first, last, rpath.str), affected_count > 1 ? - errdetail("Block %u held first zeroed page.", + errdetail("Block %u held the first zeroed page.", first + first_off) : 0, - errhint("See server log for details about the other %u invalid block(s).", - affected_count + checkfail_count - 1)); + errhint_plural("See server log for details about the other %d invalid block.", + "See server log for details about the other %d invalid blocks.", + affected_count + checkfail_count - 1, + affected_count + checkfail_count - 1)); return; } @@ -7334,25 +8837,25 @@ buffer_readv_report(PgAioResult result, const PgAioTargetData *td, { Assert(!zeroed_any); /* can't have invalid pages when zeroing them */ affected_count = zeroed_or_error_count; - msg_one = _("invalid page in block %u of relation %s"); - msg_mult = _("%u invalid pages among blocks %u..%u of relation %s"); - det_mult = _("Block %u held first invalid page."); + msg_one = _("invalid page in block %u of relation \"%s\""); + msg_mult = _("%u invalid pages among blocks %u..%u of relation \"%s\""); + det_mult = _("Block %u held the first invalid page."); hint_mult = _("See server log for the other %u invalid block(s)."); } else if (zeroed_any && !ignored_any) { affected_count = zeroed_or_error_count; - msg_one = _("invalid page in block %u of relation %s; zeroing out page"); - msg_mult = _("zeroing out %u invalid pages among blocks %u..%u of relation %s"); - det_mult = _("Block %u held first zeroed page."); + msg_one = _("invalid page in block %u of relation \"%s\"; zeroing out page"); + msg_mult = _("zeroing out %u invalid pages among blocks %u..%u of relation \"%s\""); + det_mult = _("Block %u held the first zeroed page."); hint_mult = _("See server log for the other %u zeroed block(s)."); } else if (!zeroed_any && ignored_any) { affected_count = checkfail_count; - msg_one = _("ignoring checksum failure in block %u of relation %s"); - msg_mult = _("ignoring %u checksum failures among blocks %u..%u of relation %s"); - det_mult = _("Block %u held first ignored page."); + msg_one = _("ignoring checksum failure in block %u of relation \"%s\""); + msg_mult = _("ignoring %u checksum failures among blocks %u..%u of relation \"%s\""); + det_mult = _("Block %u held the first ignored page."); hint_mult = _("See server log for the other %u ignored block(s)."); } else diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c index 01909be027258..fdb5bad7910a2 100644 --- a/src/backend/storage/buffer/freelist.c +++ b/src/backend/storage/buffer/freelist.c @@ -4,7 +4,7 @@ * routines for managing the buffer pool's replacement strategy. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -20,6 +20,8 @@ #include "storage/buf_internals.h" #include "storage/bufmgr.h" #include "storage/proc.h" +#include "storage/shmem.h" +#include "storage/subsystems.h" #define INT_ACCESS_ONCE(var) ((int)(*((volatile int *)&(var)))) @@ -33,25 +35,17 @@ typedef struct slock_t buffer_strategy_lock; /* - * Clock sweep hand: index of next buffer to consider grabbing. Note that + * clock-sweep hand: index of next buffer to consider grabbing. Note that * this isn't a concrete buffer - we only ever increase the value. So, to * get an actual buffer, it needs to be used modulo NBuffers. */ pg_atomic_uint32 nextVictimBuffer; - int firstFreeBuffer; /* Head of list of unused buffers */ - int lastFreeBuffer; /* Tail of list of unused buffers */ - - /* - * NOTE: lastFreeBuffer is undefined when firstFreeBuffer is -1 (that is, - * when the list is empty) - */ - /* * Statistics. These counters should be wide enough that they can't * overflow during a single bgwriter cycle. */ - uint32 completePasses; /* Complete cycles of the clock sweep */ + uint32 completePasses; /* Complete cycles of the clock-sweep */ pg_atomic_uint32 numBufferAllocs; /* Buffers allocated since last reset */ /* @@ -64,6 +58,14 @@ typedef struct /* Pointers to shared state */ static BufferStrategyControl *StrategyControl = NULL; +static void StrategyCtlShmemRequest(void *arg); +static void StrategyCtlShmemInit(void *arg); + +const ShmemCallbacks StrategyCtlShmemCallbacks = { + .request_fn = StrategyCtlShmemRequest, + .init_fn = StrategyCtlShmemInit, +}; + /* * Private (non-shared) state for managing a ring of shared buffers to re-use. * This is currently the only kind of BufferAccessStrategy object, but someday @@ -94,7 +96,7 @@ typedef struct BufferAccessStrategyData /* Prototypes for internal functions */ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy, - uint32 *buf_state); + uint64 *buf_state); static void AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf); @@ -163,42 +165,27 @@ ClockSweepTick(void) return victim; } -/* - * have_free_buffer -- a lockless check to see if there is a free buffer in - * buffer pool. - * - * If the result is true that will become stale once free buffers are moved out - * by other operations, so the caller who strictly want to use a free buffer - * should not call this. - */ -bool -have_free_buffer(void) -{ - if (StrategyControl->firstFreeBuffer >= 0) - return true; - else - return false; -} - /* * StrategyGetBuffer * * Called by the bufmgr to get the next candidate buffer to use in - * BufferAlloc(). The only hard requirement BufferAlloc() has is that + * GetVictimBuffer(). The only hard requirement GetVictimBuffer() has is that * the selected buffer must not currently be pinned by anyone. * * strategy is a BufferAccessStrategy object, or NULL for default strategy. * - * To ensure that no one else can pin the buffer before we do, we must - * return the buffer with the buffer header spinlock still held. + * It is the callers responsibility to ensure the buffer ownership can be + * tracked via TrackNewBufferPin(). + * + * The buffer is pinned and marked as owned, using TrackNewBufferPin(), + * before returning. */ BufferDesc * -StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring) +StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring) { BufferDesc *buf; int bgwprocno; int trycounter; - uint32 local_buf_state; /* to avoid repeated (de-)referencing */ *from_ring = false; @@ -239,7 +226,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_r * actually fine because procLatch isn't ever freed, so we just can * potentially set the wrong process' (or no process') latch. */ - SetLatch(&ProcGlobal->allProcs[bgwprocno].procLatch); + SetLatch(&GetPGProcByNumber(bgwprocno)->procLatch); } /* @@ -249,134 +236,84 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_r */ pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1); - /* - * First check, without acquiring the lock, whether there's buffers in the - * freelist. Since we otherwise don't require the spinlock in every - * StrategyGetBuffer() invocation, it'd be sad to acquire it here - - * uselessly in most cases. That obviously leaves a race where a buffer is - * put on the freelist but we don't see the store yet - but that's pretty - * harmless, it'll just get used during the next buffer acquisition. - * - * If there's buffers on the freelist, acquire the spinlock to pop one - * buffer of the freelist. Then check whether that buffer is usable and - * repeat if not. - * - * Note that the freeNext fields are considered to be protected by the - * buffer_strategy_lock not the individual buffer spinlocks, so it's OK to - * manipulate them without holding the spinlock. - */ - if (StrategyControl->firstFreeBuffer >= 0) + /* Use the "clock sweep" algorithm to find a free buffer */ + trycounter = NBuffers; + for (;;) { - while (true) - { - /* Acquire the spinlock to remove element from the freelist */ - SpinLockAcquire(&StrategyControl->buffer_strategy_lock); + uint64 old_buf_state; + uint64 local_buf_state; - if (StrategyControl->firstFreeBuffer < 0) - { - SpinLockRelease(&StrategyControl->buffer_strategy_lock); - break; - } - - buf = GetBufferDescriptor(StrategyControl->firstFreeBuffer); - Assert(buf->freeNext != FREENEXT_NOT_IN_LIST); - - /* Unconditionally remove buffer from freelist */ - StrategyControl->firstFreeBuffer = buf->freeNext; - buf->freeNext = FREENEXT_NOT_IN_LIST; + buf = GetBufferDescriptor(ClockSweepTick()); - /* - * Release the lock so someone else can access the freelist while - * we check out this buffer. - */ - SpinLockRelease(&StrategyControl->buffer_strategy_lock); + /* + * Check whether the buffer can be used and pin it if so. Do this + * using a CAS loop, to avoid having to lock the buffer header. + */ + old_buf_state = pg_atomic_read_u64(&buf->state); + for (;;) + { + local_buf_state = old_buf_state; /* * If the buffer is pinned or has a nonzero usage_count, we cannot - * use it; discard it and retry. (This can only happen if VACUUM - * put a valid buffer in the freelist and then someone else used - * it before we got to it. It's probably impossible altogether as - * of 8.3, but we'd better check anyway.) + * use it; decrement the usage_count (unless pinned) and keep + * scanning. */ - local_buf_state = LockBufHdr(buf); - if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0 - && BUF_STATE_GET_USAGECOUNT(local_buf_state) == 0) + + if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0) { - if (strategy != NULL) - AddBufferToRing(strategy, buf); - *buf_state = local_buf_state; - return buf; + if (--trycounter == 0) + { + /* + * We've scanned all the buffers without making any state + * changes, so all the buffers are pinned (or were when we + * looked at them). We could hope that someone will free + * one eventually, but it's probably better to fail than + * to risk getting stuck in an infinite loop. + */ + elog(ERROR, "no unpinned buffers available"); + } + break; } - UnlockBufHdr(buf, local_buf_state); - } - } - - /* Nothing on the freelist, so run the "clock sweep" algorithm */ - trycounter = NBuffers; - for (;;) - { - buf = GetBufferDescriptor(ClockSweepTick()); - /* - * If the buffer is pinned or has a nonzero usage_count, we cannot use - * it; decrement the usage_count (unless pinned) and keep scanning. - */ - local_buf_state = LockBufHdr(buf); + /* See equivalent code in PinBuffer() */ + if (unlikely(local_buf_state & BM_LOCKED)) + { + old_buf_state = WaitBufHdrUnlocked(buf); + continue; + } - if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0) - { if (BUF_STATE_GET_USAGECOUNT(local_buf_state) != 0) { local_buf_state -= BUF_USAGECOUNT_ONE; - trycounter = NBuffers; + if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state, + local_buf_state)) + { + trycounter = NBuffers; + break; + } } else { - /* Found a usable buffer */ - if (strategy != NULL) - AddBufferToRing(strategy, buf); - *buf_state = local_buf_state; - return buf; - } - } - else if (--trycounter == 0) - { - /* - * We've scanned all the buffers without making any state changes, - * so all the buffers are pinned (or were when we looked at them). - * We could hope that someone will free one eventually, but it's - * probably better to fail than to risk getting stuck in an - * infinite loop. - */ - UnlockBufHdr(buf, local_buf_state); - elog(ERROR, "no unpinned buffers available"); - } - UnlockBufHdr(buf, local_buf_state); - } -} + /* pin the buffer if the CAS succeeds */ + local_buf_state += BUF_REFCOUNT_ONE; -/* - * StrategyFreeBuffer: put a buffer on the freelist - */ -void -StrategyFreeBuffer(BufferDesc *buf) -{ - SpinLockAcquire(&StrategyControl->buffer_strategy_lock); + if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state, + local_buf_state)) + { + /* Found a usable buffer */ + if (strategy != NULL) + AddBufferToRing(strategy, buf); + *buf_state = local_buf_state; - /* - * It is possible that we are told to put something in the freelist that - * is already in it; don't screw up the list if so. - */ - if (buf->freeNext == FREENEXT_NOT_IN_LIST) - { - buf->freeNext = StrategyControl->firstFreeBuffer; - if (buf->freeNext < 0) - StrategyControl->lastFreeBuffer = buf->buf_id; - StrategyControl->firstFreeBuffer = buf->buf_id; - } + TrackNewBufferPin(BufferDescriptorGetBuffer(buf)); - SpinLockRelease(&StrategyControl->buffer_strategy_lock); + return buf; + } + } + } + } } /* @@ -442,87 +379,35 @@ StrategyNotifyBgWriter(int bgwprocno) /* - * StrategyShmemSize - * - * estimate the size of shared memory used by the freelist-related structures. - * - * Note: for somewhat historical reasons, the buffer lookup hashtable size - * is also determined here. + * StrategyCtlShmemRequest -- request shared memory for the buffer + * cache replacement strategy. */ -Size -StrategyShmemSize(void) +static void +StrategyCtlShmemRequest(void *arg) { - Size size = 0; - - /* size of lookup hash table ... see comment in StrategyInitialize */ - size = add_size(size, BufTableShmemSize(NBuffers + NUM_BUFFER_PARTITIONS)); - - /* size of the shared replacement strategy control block */ - size = add_size(size, MAXALIGN(sizeof(BufferStrategyControl))); - - return size; + ShmemRequestStruct(.name = "Buffer Strategy Status", + .size = sizeof(BufferStrategyControl), + .ptr = (void **) &StrategyControl + ); } /* - * StrategyInitialize -- initialize the buffer cache replacement - * strategy. - * - * Assumes: All of the buffers are already built into a linked list. - * Only called by postmaster and only during initialization. + * StrategyCtlShmemInit -- initialize the buffer cache replacement strategy. */ -void -StrategyInitialize(bool init) +static void +StrategyCtlShmemInit(void *arg) { - bool found; - - /* - * Initialize the shared buffer lookup hashtable. - * - * Since we can't tolerate running out of lookup table entries, we must be - * sure to specify an adequate table size here. The maximum steady-state - * usage is of course NBuffers entries, but BufferAlloc() tries to insert - * a new entry before deleting the old. In principle this could be - * happening in each partition concurrently, so we could need as many as - * NBuffers + NUM_BUFFER_PARTITIONS entries. - */ - InitBufTable(NBuffers + NUM_BUFFER_PARTITIONS); - - /* - * Get or create the shared strategy control block - */ - StrategyControl = (BufferStrategyControl *) - ShmemInitStruct("Buffer Strategy Status", - sizeof(BufferStrategyControl), - &found); + SpinLockInit(&StrategyControl->buffer_strategy_lock); - if (!found) - { - /* - * Only done once, usually in postmaster - */ - Assert(init); - - SpinLockInit(&StrategyControl->buffer_strategy_lock); - - /* - * Grab the whole linked list of free buffers for our strategy. We - * assume it was previously set up by BufferManagerShmemInit(). - */ - StrategyControl->firstFreeBuffer = 0; - StrategyControl->lastFreeBuffer = NBuffers - 1; + /* Initialize the clock-sweep pointer */ + pg_atomic_init_u32(&StrategyControl->nextVictimBuffer, 0); - /* Initialize the clock sweep pointer */ - pg_atomic_init_u32(&StrategyControl->nextVictimBuffer, 0); + /* Clear statistics */ + StrategyControl->completePasses = 0; + pg_atomic_init_u32(&StrategyControl->numBufferAllocs, 0); - /* Clear statistics */ - StrategyControl->completePasses = 0; - pg_atomic_init_u32(&StrategyControl->numBufferAllocs, 0); - - /* No pending notification */ - StrategyControl->bgwprocno = -1; - } - else - Assert(!init); + /* No pending notification */ + StrategyControl->bgwprocno = -1; } @@ -731,14 +616,16 @@ FreeAccessStrategy(BufferAccessStrategy strategy) * GetBufferFromRing -- returns a buffer from the ring, or NULL if the * ring is empty / not usable. * - * The bufhdr spin lock is held on the returned buffer. + * The buffer is pinned and marked as owned, using TrackNewBufferPin(), before + * returning. */ static BufferDesc * -GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state) +GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state) { BufferDesc *buf; Buffer bufnum; - uint32 local_buf_state; /* to avoid repeated (de-)referencing */ + uint64 old_buf_state; + uint64 local_buf_state; /* to avoid repeated (de-)referencing */ /* Advance to next ring slot */ @@ -754,24 +641,49 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state) if (bufnum == InvalidBuffer) return NULL; + buf = GetBufferDescriptor(bufnum - 1); + /* - * If the buffer is pinned we cannot use it under any circumstances. - * - * If usage_count is 0 or 1 then the buffer is fair game (we expect 1, - * since our own previous usage of the ring element would have left it - * there, but it might've been decremented by clock sweep since then). A - * higher usage_count indicates someone else has touched the buffer, so we - * shouldn't re-use it. + * Check whether the buffer can be used and pin it if so. Do this using a + * CAS loop, to avoid having to lock the buffer header. */ - buf = GetBufferDescriptor(bufnum - 1); - local_buf_state = LockBufHdr(buf); - if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0 - && BUF_STATE_GET_USAGECOUNT(local_buf_state) <= 1) + old_buf_state = pg_atomic_read_u64(&buf->state); + for (;;) { - *buf_state = local_buf_state; - return buf; + local_buf_state = old_buf_state; + + /* + * If the buffer is pinned we cannot use it under any circumstances. + * + * If usage_count is 0 or 1 then the buffer is fair game (we expect 1, + * since our own previous usage of the ring element would have left it + * there, but it might've been decremented by clock-sweep since then). + * A higher usage_count indicates someone else has touched the buffer, + * so we shouldn't re-use it. + */ + if (BUF_STATE_GET_REFCOUNT(local_buf_state) != 0 + || BUF_STATE_GET_USAGECOUNT(local_buf_state) > 1) + break; + + /* See equivalent code in PinBuffer() */ + if (unlikely(local_buf_state & BM_LOCKED)) + { + old_buf_state = WaitBufHdrUnlocked(buf); + continue; + } + + /* pin the buffer if the CAS succeeds */ + local_buf_state += BUF_REFCOUNT_ONE; + + if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state, + local_buf_state)) + { + *buf_state = local_buf_state; + + TrackNewBufferPin(BufferDescriptorGetBuffer(buf)); + return buf; + } } - UnlockBufHdr(buf, local_buf_state); /* * Tell caller to allocate a new buffer with the normal allocation diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c index 63101d56a074b..4870c8e13d010 100644 --- a/src/backend/storage/buffer/localbuf.c +++ b/src/backend/storage/buffer/localbuf.c @@ -4,7 +4,7 @@ * local buffer manager. Fast buffer manager for temporary tables, * which never need to be WAL-logged or checkpointed, etc. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "utils/guc_hooks.h" #include "utils/memdebug.h" #include "utils/memutils.h" +#include "utils/rel.h" #include "utils/resowner.h" @@ -147,7 +148,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum, } else { - uint32 buf_state; + uint64 buf_state; victim_buffer = GetLocalVictimBuffer(); bufid = -victim_buffer - 1; @@ -164,10 +165,10 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum, */ bufHdr->tag = newTag; - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); buf_state &= ~(BUF_FLAG_MASK | BUF_USAGECOUNT_MASK); buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; - pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); + pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state); *foundPtr = false; } @@ -188,9 +189,10 @@ FlushLocalBuffer(BufferDesc *bufHdr, SMgrRelation reln) /* * Try to start an I/O operation. There currently are no reasons for - * StartLocalBufferIO to return false, so we raise an error in that case. + * StartLocalBufferIO to return anything other than + * BUFFER_IO_READY_FOR_IO, so we raise an error in that case. */ - if (!StartLocalBufferIO(bufHdr, false, false)) + if (StartLocalBufferIO(bufHdr, false, true, NULL) != BUFFER_IO_READY_FOR_IO) elog(ERROR, "failed to start write IO on local buffer"); /* Find smgr relation for buffer */ @@ -198,7 +200,7 @@ FlushLocalBuffer(BufferDesc *bufHdr, SMgrRelation reln) reln = smgropen(BufTagGetRelFileLocator(&bufHdr->tag), MyProcNumber); - PageSetChecksumInplace(localpage, bufHdr->tag.blockNum); + PageSetChecksum(localpage, bufHdr->tag.blockNum); io_start = pgstat_prepare_io_time(track_io_timing); @@ -229,7 +231,7 @@ GetLocalVictimBuffer(void) ResourceOwnerEnlarge(CurrentResourceOwner); /* - * Need to get a new buffer. We use a clock sweep algorithm (essentially + * Need to get a new buffer. We use a clock-sweep algorithm (essentially * the same as what freelist.c does now...) */ trycounter = NLocBuffer; @@ -244,12 +246,12 @@ GetLocalVictimBuffer(void) if (LocalRefCount[victim_bufid] == 0) { - uint32 buf_state = pg_atomic_read_u32(&bufHdr->state); + uint64 buf_state = pg_atomic_read_u64(&bufHdr->state); if (BUF_STATE_GET_USAGECOUNT(buf_state) > 0) { buf_state -= BUF_USAGECOUNT_ONE; - pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); + pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state); trycounter = NLocBuffer; } else if (BUF_STATE_GET_REFCOUNT(buf_state) > 0) @@ -285,13 +287,13 @@ GetLocalVictimBuffer(void) * this buffer is not referenced but it might still be dirty. if that's * the case, write it out before reusing it! */ - if (pg_atomic_read_u32(&bufHdr->state) & BM_DIRTY) + if (pg_atomic_read_u64(&bufHdr->state) & BM_DIRTY) FlushLocalBuffer(bufHdr, NULL); /* * Remove the victim buffer from the hashtable and mark as invalid. */ - if (pg_atomic_read_u32(&bufHdr->state) & BM_TAG_VALID) + if (pg_atomic_read_u64(&bufHdr->state) & BM_TAG_VALID) { InvalidateLocalBuffer(bufHdr, false); @@ -305,16 +307,24 @@ GetLocalVictimBuffer(void) uint32 GetLocalPinLimit(void) { - /* Every backend has its own temporary buffers, and can pin them all. */ - return num_temp_buffers; + /* + * Every backend has its own temporary buffers, but we leave headroom for + * concurrent pin-holders -- like multiple scans in the same query. + */ + return num_temp_buffers / 4; } /* see GetAdditionalPinLimit() */ uint32 GetAdditionalLocalPinLimit(void) { + uint32 total = GetLocalPinLimit(); + Assert(NLocalPinnedBuffers <= num_temp_buffers); - return num_temp_buffers - NLocalPinnedBuffers; + + if (NLocalPinnedBuffers >= total) + return 0; + return total - NLocalPinnedBuffers; } /* see LimitAdditionalPins() */ @@ -372,7 +382,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr, MemSet(buf_block, 0, BLCKSZ); } - first_block = smgrnblocks(bmr.smgr, fork); + first_block = smgrnblocks(BMR_GET_SMGR(bmr), fork); if (extend_upto != InvalidBlockNumber) { @@ -391,7 +401,7 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr, ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("cannot extend relation %s beyond %u blocks", - relpath(bmr.smgr->smgr_rlocator, fork).str, + relpath(BMR_GET_SMGR(bmr)->smgr_rlocator, fork).str, MaxBlockNumber))); for (uint32 i = 0; i < extend_by; i++) @@ -408,14 +418,15 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr, /* in case we need to pin an existing buffer below */ ResourceOwnerEnlarge(CurrentResourceOwner); - InitBufferTag(&tag, &bmr.smgr->smgr_rlocator.locator, fork, first_block + i); + InitBufferTag(&tag, &BMR_GET_SMGR(bmr)->smgr_rlocator.locator, fork, + first_block + i); hresult = (LocalBufferLookupEnt *) hash_search(LocalBufHash, &tag, HASH_ENTER, &found); if (found) { BufferDesc *existing_hdr; - uint32 buf_state; + uint64 buf_state; UnpinLocalBuffer(BufferDescriptorGetBuffer(victim_buf_hdr)); @@ -426,37 +437,37 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr, /* * Clear the BM_VALID bit, do StartLocalBufferIO() and proceed. */ - buf_state = pg_atomic_read_u32(&existing_hdr->state); + buf_state = pg_atomic_read_u64(&existing_hdr->state); Assert(buf_state & BM_TAG_VALID); Assert(!(buf_state & BM_DIRTY)); buf_state &= ~BM_VALID; - pg_atomic_unlocked_write_u32(&existing_hdr->state, buf_state); + pg_atomic_unlocked_write_u64(&existing_hdr->state, buf_state); /* no need to loop for local buffers */ - StartLocalBufferIO(existing_hdr, true, false); + StartLocalBufferIO(existing_hdr, true, true, NULL); } else { - uint32 buf_state = pg_atomic_read_u32(&victim_buf_hdr->state); + uint64 buf_state = pg_atomic_read_u64(&victim_buf_hdr->state); - Assert(!(buf_state & (BM_VALID | BM_TAG_VALID | BM_DIRTY | BM_JUST_DIRTIED))); + Assert(!(buf_state & (BM_VALID | BM_TAG_VALID | BM_DIRTY))); victim_buf_hdr->tag = tag; buf_state |= BM_TAG_VALID | BUF_USAGECOUNT_ONE; - pg_atomic_unlocked_write_u32(&victim_buf_hdr->state, buf_state); + pg_atomic_unlocked_write_u64(&victim_buf_hdr->state, buf_state); hresult->id = victim_buf_id; - StartLocalBufferIO(victim_buf_hdr, true, false); + StartLocalBufferIO(victim_buf_hdr, true, true, NULL); } } io_start = pgstat_prepare_io_time(track_io_timing); /* actually extend relation */ - smgrzeroextend(bmr.smgr, fork, first_block, extend_by, false); + smgrzeroextend(BMR_GET_SMGR(bmr), fork, first_block, extend_by, false); pgstat_count_io_op_time(IOOBJECT_TEMP_RELATION, IOCONTEXT_NORMAL, IOOP_EXTEND, io_start, 1, extend_by * BLCKSZ); @@ -465,13 +476,13 @@ ExtendBufferedRelLocal(BufferManagerRelation bmr, { Buffer buf = buffers[i]; BufferDesc *buf_hdr; - uint32 buf_state; + uint64 buf_state; buf_hdr = GetLocalBufferDescriptor(-buf - 1); - buf_state = pg_atomic_read_u32(&buf_hdr->state); + buf_state = pg_atomic_read_u64(&buf_hdr->state); buf_state |= BM_VALID; - pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state); + pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state); } *extended_by = extend_by; @@ -490,7 +501,7 @@ MarkLocalBufferDirty(Buffer buffer) { int bufid; BufferDesc *bufHdr; - uint32 buf_state; + uint64 buf_state; Assert(BufferIsLocal(buffer)); @@ -504,64 +515,79 @@ MarkLocalBufferDirty(Buffer buffer) bufHdr = GetLocalBufferDescriptor(bufid); - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); if (!(buf_state & BM_DIRTY)) pgBufferUsage.local_blks_dirtied++; buf_state |= BM_DIRTY; - pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); + pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state); } /* - * Like StartBufferIO, but for local buffers + * Like StartSharedBufferIO, but for local buffers */ -bool -StartLocalBufferIO(BufferDesc *bufHdr, bool forInput, bool nowait) +StartBufferIOResult +StartLocalBufferIO(BufferDesc *bufHdr, bool forInput, bool wait, PgAioWaitRef *io_wref) { - uint32 buf_state; + uint64 buf_state; /* * With AIO the buffer could have IO in progress, e.g. when there are two - * scans of the same relation. Either wait for the other IO or return - * false. + * scans of the same relation. Either wait for the other IO (if wait = + * true and io_wref == NULL) or return BUFFER_IO_IN_PROGRESS; */ if (pgaio_wref_valid(&bufHdr->io_wref)) { - PgAioWaitRef iow = bufHdr->io_wref; + PgAioWaitRef buf_wref = bufHdr->io_wref; - if (nowait) - return false; + if (io_wref != NULL) + { + /* We've already asynchronously started this IO, so join it */ + *io_wref = buf_wref; + return BUFFER_IO_IN_PROGRESS; + } - pgaio_wref_wait(&iow); + /* + * For temp buffers we should never need to wait in + * StartLocalBufferIO() when called with io_wref == NULL while there + * are staged IOs, as it's not allowed to call code that is not aware + * of AIO while in batch mode. + */ + Assert(!pgaio_have_staged()); + + if (!wait) + return BUFFER_IO_IN_PROGRESS; + + pgaio_wref_wait(&buf_wref); } /* Once we get here, there is definitely no I/O active on this buffer */ /* Check if someone else already did the I/O */ - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); if (forInput ? (buf_state & BM_VALID) : !(buf_state & BM_DIRTY)) { - return false; + return BUFFER_IO_ALREADY_DONE; } /* BM_IO_IN_PROGRESS isn't currently used for local buffers */ /* local buffers don't track IO using resowners */ - return true; + return BUFFER_IO_READY_FOR_IO; } /* * Like TerminateBufferIO, but for local buffers */ void -TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, uint32 set_flag_bits, +TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, uint64 set_flag_bits, bool release_aio) { /* Only need to adjust flags */ - uint32 buf_state = pg_atomic_read_u32(&bufHdr->state); + uint64 buf_state = pg_atomic_read_u64(&bufHdr->state); /* BM_IO_IN_PROGRESS isn't currently used for local buffers */ @@ -580,7 +606,7 @@ TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, uint32 set_flag_bit } buf_state |= set_flag_bits; - pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); + pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state); /* local buffers don't track IO using resowners */ @@ -604,7 +630,7 @@ InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced) { Buffer buffer = BufferDescriptorGetBuffer(bufHdr); int bufid = -buffer - 1; - uint32 buf_state; + uint64 buf_state; LocalBufferLookupEnt *hresult; /* @@ -620,7 +646,7 @@ InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced) Assert(!pgaio_wref_valid(&bufHdr->io_wref)); } - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); /* * We need to test not just LocalRefCount[bufid] but also the BufferDesc @@ -629,7 +655,7 @@ InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced) */ if (check_unreferenced && (LocalRefCount[bufid] != 0 || BUF_STATE_GET_REFCOUNT(buf_state) != 0)) - elog(ERROR, "block %u of %s is still referenced (local %u)", + elog(ERROR, "block %u of %s is still referenced (local %d)", bufHdr->tag.blockNum, relpathbackend(BufTagGetRelFileLocator(&bufHdr->tag), MyProcNumber, @@ -645,7 +671,7 @@ InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced) ClearBufferTag(&bufHdr->tag); buf_state &= ~BUF_FLAG_MASK; buf_state &= ~BUF_USAGECOUNT_MASK; - pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state); + pg_atomic_unlocked_write_u64(&bufHdr->state, buf_state); } /* @@ -660,24 +686,31 @@ InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced) * See DropRelationBuffers in bufmgr.c for more notes. */ void -DropRelationLocalBuffers(RelFileLocator rlocator, ForkNumber forkNum, - BlockNumber firstDelBlock) +DropRelationLocalBuffers(RelFileLocator rlocator, ForkNumber *forkNum, + int nforks, BlockNumber *firstDelBlock) { int i; + int j; for (i = 0; i < NLocBuffer; i++) { BufferDesc *bufHdr = GetLocalBufferDescriptor(i); - uint32 buf_state; + uint64 buf_state; - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); - if ((buf_state & BM_TAG_VALID) && - BufTagMatchesRelFileLocator(&bufHdr->tag, &rlocator) && - BufTagGetForkNum(&bufHdr->tag) == forkNum && - bufHdr->tag.blockNum >= firstDelBlock) + if (!(buf_state & BM_TAG_VALID) || + !BufTagMatchesRelFileLocator(&bufHdr->tag, &rlocator)) + continue; + + for (j = 0; j < nforks; j++) { - InvalidateLocalBuffer(bufHdr, true); + if (BufTagGetForkNum(&bufHdr->tag) == forkNum[j] && + bufHdr->tag.blockNum >= firstDelBlock[j]) + { + InvalidateLocalBuffer(bufHdr, true); + break; + } } } } @@ -697,9 +730,9 @@ DropRelationAllLocalBuffers(RelFileLocator rlocator) for (i = 0; i < NLocBuffer; i++) { BufferDesc *bufHdr = GetLocalBufferDescriptor(i); - uint32 buf_state; + uint64 buf_state; - buf_state = pg_atomic_read_u32(&bufHdr->state); + buf_state = pg_atomic_read_u64(&bufHdr->state); if ((buf_state & BM_TAG_VALID) && BufTagMatchesRelFileLocator(&bufHdr->tag, &rlocator)) @@ -795,11 +828,11 @@ InitLocalBuffers(void) bool PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount) { - uint32 buf_state; + uint64 buf_state; Buffer buffer = BufferDescriptorGetBuffer(buf_hdr); int bufid = -buffer - 1; - buf_state = pg_atomic_read_u32(&buf_hdr->state); + buf_state = pg_atomic_read_u64(&buf_hdr->state); if (LocalRefCount[bufid] == 0) { @@ -810,7 +843,7 @@ PinLocalBuffer(BufferDesc *buf_hdr, bool adjust_usagecount) { buf_state += BUF_USAGECOUNT_ONE; } - pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state); + pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state); /* * See comment in PinBuffer(). @@ -847,14 +880,14 @@ UnpinLocalBufferNoOwner(Buffer buffer) if (--LocalRefCount[buffid] == 0) { BufferDesc *buf_hdr = GetLocalBufferDescriptor(buffid); - uint32 buf_state; + uint64 buf_state; NLocalPinnedBuffers--; - buf_state = pg_atomic_read_u32(&buf_hdr->state); + buf_state = pg_atomic_read_u64(&buf_hdr->state); Assert(BUF_STATE_GET_REFCOUNT(buf_state) > 0); buf_state -= BUF_REFCOUNT_ONE; - pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state); + pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state); /* see comment in UnpinBufferNoOwner */ VALGRIND_MAKE_MEM_NOACCESS(LocalBufHdrGetBlock(buf_hdr), BLCKSZ); @@ -925,10 +958,11 @@ GetLocalBufferStorage(void) num_bufs = Min(num_bufs, MaxAllocSize / BLCKSZ); /* Buffers should be I/O aligned. */ - cur_block = (char *) - TYPEALIGN(PG_IO_ALIGN_SIZE, - MemoryContextAlloc(LocalBufferContext, - num_bufs * BLCKSZ + PG_IO_ALIGN_SIZE)); + cur_block = MemoryContextAllocAligned(LocalBufferContext, + num_bufs * BLCKSZ, + PG_IO_ALIGN_SIZE, + 0); + next_buf_in_block = 0; num_bufs_in_block = num_bufs; } diff --git a/src/backend/storage/buffer/meson.build b/src/backend/storage/buffer/meson.build index 448976d2400bd..ed84bf089716a 100644 --- a/src/backend/storage/buffer/meson.build +++ b/src/backend/storage/buffer/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'buf_init.c', diff --git a/src/backend/storage/file/buffile.c b/src/backend/storage/file/buffile.c index 366d70d38a195..c4afe4d368a34 100644 --- a/src/backend/storage/file/buffile.c +++ b/src/backend/storage/file/buffile.c @@ -3,7 +3,7 @@ * buffile.c * Management of large buffered temporary files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -53,6 +53,7 @@ #include "storage/bufmgr.h" #include "storage/fd.h" #include "utils/resowner.h" +#include "utils/wait_event.h" /* * We break BufFiles into gigabyte-sized segments, regardless of RELSEG_SIZE. @@ -92,9 +93,9 @@ struct BufFile * Position as seen by user of BufFile is (curFile, curOffset + pos). */ int curFile; /* file index (0..n) part of current pos */ - off_t curOffset; /* offset part of current pos */ - int pos; /* next read/write position in buffer */ - int nbytes; /* total # of valid bytes in buffer */ + pgoff_t curOffset; /* offset part of current pos */ + int64 pos; /* next read/write position in buffer */ + int64 nbytes; /* total # of valid bytes in buffer */ /* * XXX Should ideally use PGIOAlignedBlock, but might need a way to avoid @@ -117,7 +118,7 @@ static File MakeNewFileSetSegment(BufFile *buffile, int segment); static BufFile * makeBufFileCommon(int nfiles) { - BufFile *file = (BufFile *) palloc(sizeof(BufFile)); + BufFile *file = palloc_object(BufFile); file->numFiles = nfiles; file->isInterXact = false; @@ -140,7 +141,7 @@ makeBufFile(File firstfile) { BufFile *file = makeBufFileCommon(1); - file->files = (File *) palloc(sizeof(File)); + file->files = palloc_object(File); file->files[0] = firstfile; file->readOnly = false; file->fileset = NULL; @@ -271,7 +272,7 @@ BufFileCreateFileSet(FileSet *fileset, const char *name) file = makeBufFileCommon(1); file->fileset = fileset; file->name = pstrdup(name); - file->files = (File *) palloc(sizeof(File)); + file->files = palloc_object(File); file->files[0] = MakeNewFileSetSegment(file, 0); file->readOnly = false; @@ -297,7 +298,7 @@ BufFileOpenFileSet(FileSet *fileset, const char *name, int mode, File *files; int nfiles = 0; - files = palloc(sizeof(File) * capacity); + files = palloc_array(File, capacity); /* * We don't know how many segments there are, so we'll probe the @@ -309,7 +310,7 @@ BufFileOpenFileSet(FileSet *fileset, const char *name, int mode, if (nfiles + 1 > capacity) { capacity *= 2; - files = repalloc(files, sizeof(File) * capacity); + files = repalloc_array(files, File, capacity); } /* Try to load a segment. */ FileSetSegmentName(segment_name, name, nfiles); @@ -493,8 +494,8 @@ BufFileLoadBuffer(BufFile *file) static void BufFileDumpBuffer(BufFile *file) { - int wpos = 0; - int bytestowrite; + int64 wpos = 0; + int64 bytestowrite; File thisfile; /* @@ -503,7 +504,7 @@ BufFileDumpBuffer(BufFile *file) */ while (wpos < file->nbytes) { - off_t availbytes; + int64 availbytes; instr_time io_start; instr_time io_time; @@ -524,8 +525,8 @@ BufFileDumpBuffer(BufFile *file) bytestowrite = file->nbytes - wpos; availbytes = MAX_PHYSICAL_FILESIZE - file->curOffset; - if ((off_t) bytestowrite > availbytes) - bytestowrite = (int) availbytes; + if (bytestowrite > availbytes) + bytestowrite = availbytes; thisfile = file->files[file->curFile]; @@ -729,7 +730,7 @@ BufFileFlush(BufFile *file) * BufFileSeek * * Like fseek(), except that target position needs two values in order to - * work when logical filesize exceeds maximum value representable by off_t. + * work when logical filesize exceeds maximum value representable by pgoff_t. * We do not support relative seeks across more than that, however. * I/O errors are reported by ereport(). * @@ -737,10 +738,10 @@ BufFileFlush(BufFile *file) * impossible seek is attempted. */ int -BufFileSeek(BufFile *file, int fileno, off_t offset, int whence) +BufFileSeek(BufFile *file, int fileno, pgoff_t offset, int whence) { int newFile; - off_t newOffset; + pgoff_t newOffset; switch (whence) { @@ -754,8 +755,7 @@ BufFileSeek(BufFile *file, int fileno, off_t offset, int whence) /* * Relative seek considers only the signed offset, ignoring - * fileno. Note that large offsets (> 1 GB) risk overflow in this - * add, unless we have 64-bit off_t. + * fileno. */ newFile = file->curFile; newOffset = (file->curOffset + file->pos) + offset; @@ -795,7 +795,7 @@ BufFileSeek(BufFile *file, int fileno, off_t offset, int whence) * whether reading or writing, but buffer remains dirty if we were * writing. */ - file->pos = (int) (newOffset - file->curOffset); + file->pos = (int64) (newOffset - file->curOffset); return 0; } /* Otherwise, must reposition buffer, so flush any dirty data */ @@ -830,7 +830,7 @@ BufFileSeek(BufFile *file, int fileno, off_t offset, int whence) } void -BufFileTell(BufFile *file, int *fileno, off_t *offset) +BufFileTell(BufFile *file, int *fileno, pgoff_t *offset) { *fileno = file->curFile; *offset = file->curOffset + file->pos; @@ -852,7 +852,7 @@ BufFileSeekBlock(BufFile *file, int64 blknum) { return BufFileSeek(file, (int) (blknum / BUFFILE_SEG_SIZE), - (off_t) (blknum % BUFFILE_SEG_SIZE) * BLCKSZ, + (pgoff_t) (blknum % BUFFILE_SEG_SIZE) * BLCKSZ, SEEK_SET); } @@ -925,11 +925,11 @@ BufFileAppend(BufFile *target, BufFile *source) * and the offset. */ void -BufFileTruncateFileSet(BufFile *file, int fileno, off_t offset) +BufFileTruncateFileSet(BufFile *file, int fileno, pgoff_t offset) { int numFiles = file->numFiles; int newFile = fileno; - off_t newOffset = file->curOffset; + pgoff_t newOffset = file->curOffset; char segment_name[MAXPGPATH]; int i; @@ -984,10 +984,10 @@ BufFileTruncateFileSet(BufFile *file, int fileno, off_t offset) { /* No need to reset the current pos if the new pos is greater. */ if (newOffset <= file->curOffset + file->pos) - file->pos = (int) (newOffset - file->curOffset); + file->pos = (int64) newOffset - file->curOffset; /* Adjust the nbytes for the current buffer. */ - file->nbytes = (int) (newOffset - file->curOffset); + file->nbytes = (int64) newOffset - file->curOffset; } else if (newFile == file->curFile && newOffset < file->curOffset) diff --git a/src/backend/storage/file/copydir.c b/src/backend/storage/file/copydir.c index aa8c64a2c9eb5..5ee141f13a538 100644 --- a/src/backend/storage/file/copydir.c +++ b/src/backend/storage/file/copydir.c @@ -3,7 +3,7 @@ * copydir.c * copies a directory * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * While "xcopy /e /i /q" works fine for copying directories, on Windows XP @@ -29,6 +29,7 @@ #include "pgstat.h" #include "storage/copydir.h" #include "storage/fd.h" +#include "utils/wait_event.h" /* GUCs */ int file_copy_method = FILE_COPY_METHOD_COPY; diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c index 0e8299dd55646..a8be066afe0d6 100644 --- a/src/backend/storage/file/fd.c +++ b/src/backend/storage/file/fd.c @@ -3,7 +3,7 @@ * fd.c * Virtual file descriptor code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -101,6 +101,7 @@ #include "utils/guc_hooks.h" #include "utils/resowner.h" #include "utils/varlena.h" +#include "utils/wait_event.h" /* Define PG_FLUSH_DATA_WORKS if we have an implementation for pg_flush_data */ #if defined(HAVE_SYNC_FILE_RANGE) @@ -164,6 +165,9 @@ bool data_sync_retry = false; /* How SyncDataDirectory() should do its job. */ int recovery_init_sync_method = DATA_DIR_SYNC_METHOD_FSYNC; +/* How data files should be bulk-extended with zeros. */ +int file_extend_method = DEFAULT_FILE_EXTEND_METHOD; + /* Which kinds of files should be opened with PG_O_DIRECT. */ int io_direct_flags; @@ -201,7 +205,7 @@ typedef struct vfd File nextFree; /* link to next free VFD, if in freelist */ File lruMoreRecently; /* doubly linked recency-of-use list */ File lruLessRecently; - off_t fileSize; /* current size of file (0 if not temporary) */ + pgoff_t fileSize; /* current size of file (0 if not temporary) */ char *fileName; /* name of file, or NULL for unused VFD */ /* NB: fileName is malloc'd, and must be free'd when closing the VFD */ int fileFlags; /* open(2) flags for (re)opening the file */ @@ -400,25 +404,22 @@ pg_fsync(int fd) * portable, even if it runs ok on the current system. * * We assert here that a descriptor for a file was opened with write - * permissions (either O_RDWR or O_WRONLY) and for a directory without - * write permissions (O_RDONLY). + * permissions (i.e., not O_RDONLY) and for a directory without write + * permissions (O_RDONLY). Notice that the assertion check is made even + * if fsync() is disabled. * - * Ignore any fstat errors and let the follow-up fsync() do its work. - * Doing this sanity check here counts for the case where fsync() is - * disabled. + * If fstat() fails, ignore it and let the follow-up fsync() complain. */ if (fstat(fd, &st) == 0) { int desc_flags = fcntl(fd, F_GETFL); - /* - * O_RDONLY is historically 0, so just make sure that for directories - * no write flags are used. - */ + desc_flags &= O_ACCMODE; + if (S_ISDIR(st.st_mode)) - Assert((desc_flags & (O_RDWR | O_WRONLY)) == 0); + Assert(desc_flags == O_RDONLY); else - Assert((desc_flags & (O_RDWR | O_WRONLY)) != 0); + Assert(desc_flags != O_RDONLY); } errno = 0; #endif @@ -522,7 +523,7 @@ pg_file_exists(const char *name) * offset of 0 with nbytes 0 means that the entire file should be flushed */ void -pg_flush_data(int fd, off_t offset, off_t nbytes) +pg_flush_data(int fd, pgoff_t offset, pgoff_t nbytes) { /* * Right now file flushing is primarily used to avoid making later @@ -638,7 +639,7 @@ pg_flush_data(int fd, off_t offset, off_t nbytes) * may simply not be enough address space. If so, silently fall * through to the next implementation. */ - if (nbytes <= (off_t) SSIZE_MAX) + if (nbytes <= (pgoff_t) SSIZE_MAX) p = mmap(NULL, nbytes, PROT_READ, MAP_SHARED, fd, offset); else p = MAP_FAILED; @@ -700,7 +701,7 @@ pg_flush_data(int fd, off_t offset, off_t nbytes) * Truncate an open file to a given length. */ static int -pg_ftruncate(int fd, off_t length) +pg_ftruncate(int fd, pgoff_t length) { int ret; @@ -717,7 +718,7 @@ pg_ftruncate(int fd, off_t length) * Truncate a file to a given length by name. */ int -pg_truncate(const char *path, off_t length) +pg_truncate(const char *path, pgoff_t length) { int ret; #ifdef WIN32 @@ -1114,23 +1115,6 @@ BasicOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode) tryAgain: #ifdef PG_O_DIRECT_USE_F_NOCACHE - - /* - * The value we defined to stand in for O_DIRECT when simulating it with - * F_NOCACHE had better not collide with any of the standard flags. - */ - StaticAssertStmt((PG_O_DIRECT & - (O_APPEND | - O_CLOEXEC | - O_CREAT | - O_DSYNC | - O_EXCL | - O_RDWR | - O_RDONLY | - O_SYNC | - O_TRUNC | - O_WRONLY)) == 0, - "PG_O_DIRECT value collides with standard flag"); fd = open(fileName, fileFlags & ~PG_O_DIRECT, fileMode); #else fd = open(fileName, fileFlags, fileMode); @@ -1529,7 +1513,7 @@ FileAccess(File file) * Called whenever a temporary file is deleted to report its size. */ static void -ReportTemporaryFileUsage(const char *path, off_t size) +ReportTemporaryFileUsage(const char *path, pgoff_t size) { pgstat_report_tempfile(size); @@ -2080,7 +2064,7 @@ FileClose(File file) * this. */ int -FilePrefetch(File file, off_t offset, off_t amount, uint32 wait_event_info) +FilePrefetch(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info) { Assert(FileIsValid(file)); @@ -2136,7 +2120,7 @@ FilePrefetch(File file, off_t offset, off_t amount, uint32 wait_event_info) } void -FileWriteback(File file, off_t offset, off_t nbytes, uint32 wait_event_info) +FileWriteback(File file, pgoff_t offset, pgoff_t nbytes, uint32 wait_event_info) { int returnCode; @@ -2162,7 +2146,7 @@ FileWriteback(File file, off_t offset, off_t nbytes, uint32 wait_event_info) } ssize_t -FileReadV(File file, const struct iovec *iov, int iovcnt, off_t offset, +FileReadV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info) { ssize_t returnCode; @@ -2219,7 +2203,7 @@ FileReadV(File file, const struct iovec *iov, int iovcnt, off_t offset, int FileStartReadV(PgAioHandle *ioh, File file, - int iovcnt, off_t offset, + int iovcnt, pgoff_t offset, uint32 wait_event_info) { int returnCode; @@ -2244,7 +2228,7 @@ FileStartReadV(PgAioHandle *ioh, File file, } ssize_t -FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset, +FileWriteV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info) { ssize_t returnCode; @@ -2273,7 +2257,7 @@ FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset, */ if (temp_file_limit >= 0 && (vfdP->fdstate & FD_TEMP_FILE_LIMIT)) { - off_t past_write = offset; + pgoff_t past_write = offset; for (int i = 0; i < iovcnt; ++i) past_write += iov[i].iov_len; @@ -2312,7 +2296,7 @@ FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset, */ if (vfdP->fdstate & FD_TEMP_FILE_LIMIT) { - off_t past_write = offset + returnCode; + pgoff_t past_write = offset + returnCode; if (past_write > vfdP->fileSize) { @@ -2376,7 +2360,7 @@ FileSync(File file, uint32 wait_event_info) * appropriate error. */ int -FileZero(File file, off_t offset, off_t amount, uint32 wait_event_info) +FileZero(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info) { int returnCode; ssize_t written; @@ -2421,7 +2405,7 @@ FileZero(File file, off_t offset, off_t amount, uint32 wait_event_info) * appropriate error. */ int -FileFallocate(File file, off_t offset, off_t amount, uint32 wait_event_info) +FileFallocate(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info) { #ifdef HAVE_POSIX_FALLOCATE int returnCode; @@ -2460,7 +2444,7 @@ FileFallocate(File file, off_t offset, off_t amount, uint32 wait_event_info) return FileZero(file, offset, amount, wait_event_info); } -off_t +pgoff_t FileSize(File file) { Assert(FileIsValid(file)); @@ -2471,14 +2455,14 @@ FileSize(File file) if (FileIsNotOpen(file)) { if (FileAccess(file) < 0) - return (off_t) -1; + return (pgoff_t) -1; } return lseek(VfdCache[file].fd, 0, SEEK_END); } int -FileTruncate(File file, off_t offset, uint32 wait_event_info) +FileTruncate(File file, pgoff_t offset, uint32 wait_event_info) { int returnCode; @@ -2764,11 +2748,11 @@ OpenPipeStream(const char *command, const char *mode) TryAgain: fflush(NULL); - pqsignal(SIGPIPE, SIG_DFL); + pqsignal(SIGPIPE, PG_SIG_DFL); errno = 0; file = popen(command, mode); save_errno = errno; - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); errno = save_errno; if (file != NULL) { @@ -3188,9 +3172,10 @@ GetNextTempTableSpace(void) /* * AtEOSubXact_Files * - * Take care of subtransaction commit/abort. At abort, we close temp files - * that the subtransaction may have opened. At commit, we reassign the - * files that were opened to the parent subtransaction. + * Take care of subtransaction commit/abort. At abort, we close AllocateDescs + * that the subtransaction may have opened. At commit, we reassign them to + * the parent subtransaction. (Temporary files are tracked by ResourceOwners + * instead.) */ void AtEOSubXact_Files(bool isCommit, SubTransactionId mySubid, diff --git a/src/backend/storage/file/fileset.c b/src/backend/storage/file/fileset.c index 64141c7cb91c9..e794cabcab8b9 100644 --- a/src/backend/storage/file/fileset.c +++ b/src/backend/storage/file/fileset.c @@ -3,7 +3,7 @@ * fileset.c * Management of named temporary files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -114,7 +114,8 @@ FileSetCreate(FileSet *fileset, const char *name) } /* - * Open a file that was created with FileSetCreate() */ + * Open a file that was created with FileSetCreate() + */ File FileSetOpen(FileSet *fileset, const char *name, int mode) { @@ -185,7 +186,7 @@ FileSetPath(char *path, FileSet *fileset, Oid tablespace) static Oid ChooseTablespace(const FileSet *fileset, const char *name) { - uint32 hash = hash_any((const unsigned char *) name, strlen(name)); + uint32 hash = hash_bytes((const unsigned char *) name, strlen(name)); return fileset->tablespaces[hash % fileset->ntablespaces]; } diff --git a/src/backend/storage/file/meson.build b/src/backend/storage/file/meson.build index d1a939a2ad95d..795402589b0b9 100644 --- a/src/backend/storage/file/meson.build +++ b/src/backend/storage/file/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'buffile.c', diff --git a/src/backend/storage/file/reinit.c b/src/backend/storage/file/reinit.c index 5c8275cf53653..25fa215130928 100644 --- a/src/backend/storage/file/reinit.c +++ b/src/backend/storage/file/reinit.c @@ -3,7 +3,7 @@ * reinit.c * Reinitialization of unlogged relations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/storage/file/sharedfileset.c b/src/backend/storage/file/sharedfileset.c index 43e4c72aab3ae..d76bd72dc63c4 100644 --- a/src/backend/storage/file/sharedfileset.c +++ b/src/backend/storage/file/sharedfileset.c @@ -3,7 +3,7 @@ * sharedfileset.c * Shared temporary file management. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/storage/freespace/freespace.c b/src/backend/storage/freespace/freespace.c index 4773a9cc65e6c..40d67a96178bc 100644 --- a/src/backend/storage/freespace/freespace.c +++ b/src/backend/storage/freespace/freespace.c @@ -4,7 +4,7 @@ * POSTGRES free space map for quickly finding free space in relations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -904,14 +904,19 @@ fsm_vacuum_page(Relation rel, FSMAddress addr, max_avail = fsm_get_max_avail(page); /* - * Reset the next slot pointer. This encourages the use of low-numbered - * pages, increasing the chances that a later vacuum can truncate the - * relation. We don't bother with a lock here, nor with marking the page - * dirty if it wasn't already, since this is just a hint. + * Try to reset the next slot pointer. This encourages the use of + * low-numbered pages, increasing the chances that a later vacuum can + * truncate the relation. We don't bother with marking the page dirty if + * it wasn't already, since this is just a hint. */ - ((FSMPage) PageGetContents(page))->fp_next_slot = 0; + LockBuffer(buf, BUFFER_LOCK_SHARE); + if (BufferBeginSetHintBits(buf)) + { + ((FSMPage) PageGetContents(page))->fp_next_slot = 0; + BufferFinishSetHintBits(buf, false, false); + } - ReleaseBuffer(buf); + UnlockReleaseBuffer(buf); return max_avail; } diff --git a/src/backend/storage/freespace/fsmpage.c b/src/backend/storage/freespace/fsmpage.c index 66a5c80b5a6c6..a2657c4033b9b 100644 --- a/src/backend/storage/freespace/fsmpage.c +++ b/src/backend/storage/freespace/fsmpage.c @@ -4,7 +4,7 @@ * routines to search and manipulate one FSM page. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -298,9 +298,18 @@ fsm_search_avail(Buffer buf, uint8 minvalue, bool advancenext, * lock and get a garbled next pointer every now and then, than take the * concurrency hit of an exclusive lock. * + * Without an exclusive lock, we need to use the hint bit infrastructure + * to be allowed to modify the page. + * * Wrap-around is handled at the beginning of this function. */ - fsmpage->fp_next_slot = slot + (advancenext ? 1 : 0); + if (exclusive_lock_held || BufferBeginSetHintBits(buf)) + { + fsmpage->fp_next_slot = slot + (advancenext ? 1 : 0); + + if (!exclusive_lock_held) + BufferFinishSetHintBits(buf, false, false); + } return slot; } diff --git a/src/backend/storage/freespace/indexfsm.c b/src/backend/storage/freespace/indexfsm.c index 614261e4e22fc..85fbbab6c9c3c 100644 --- a/src/backend/storage/freespace/indexfsm.c +++ b/src/backend/storage/freespace/indexfsm.c @@ -4,7 +4,7 @@ * POSTGRES free space map for quickly finding free pages in relations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/storage/freespace/meson.build b/src/backend/storage/freespace/meson.build index 16cfae0e5dd0a..7710f44431194 100644 --- a/src/backend/storage/freespace/meson.build +++ b/src/backend/storage/freespace/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'freespace.c', diff --git a/src/backend/storage/ipc/Makefile b/src/backend/storage/ipc/Makefile index 9a07f6e1d92ab..f71653bbe482e 100644 --- a/src/backend/storage/ipc/Makefile +++ b/src/backend/storage/ipc/Makefile @@ -22,6 +22,7 @@ OBJS = \ shm_mq.o \ shm_toc.o \ shmem.o \ + shmem_hash.o \ signalfuncs.o \ sinval.o \ sinvaladt.o \ diff --git a/src/backend/storage/ipc/barrier.c b/src/backend/storage/ipc/barrier.c index cb99c5f06b042..3fba281a75cd2 100644 --- a/src/backend/storage/ipc/barrier.c +++ b/src/backend/storage/ipc/barrier.c @@ -3,7 +3,7 @@ * barrier.c * Barriers for synchronizing cooperating processes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * From Wikipedia[1]: "In parallel computing, a barrier is a type of diff --git a/src/backend/storage/ipc/dsm.c b/src/backend/storage/ipc/dsm.c index f92a52a00e68d..8b69df4ff2635 100644 --- a/src/backend/storage/ipc/dsm.c +++ b/src/backend/storage/ipc/dsm.c @@ -14,7 +14,7 @@ * hard postmaster crash, remaining segments will be removed, if they * still exist, at the next postmaster startup. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -43,6 +43,7 @@ #include "storage/lwlock.h" #include "storage/pg_shmem.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/freepage.h" #include "utils/memutils.h" #include "utils/resowner.h" @@ -109,6 +110,15 @@ static bool dsm_init_done = false; /* Preallocated DSM space in the main shared memory region. */ static void *dsm_main_space_begin = NULL; +static size_t dsm_main_space_size; + +static void dsm_main_space_request(void *arg); +static void dsm_main_space_init(void *arg); + +const ShmemCallbacks dsm_shmem_callbacks = { + .request_fn = dsm_main_space_request, + .init_fn = dsm_main_space_init, +}; /* * List of dynamic shared memory segments used by this backend. @@ -464,42 +474,40 @@ dsm_set_control_handle(dsm_handle h) #endif /* - * Reserve some space in the main shared memory segment for DSM segments. + * Reserve space in the main shared memory segment for DSM segments. */ -size_t -dsm_estimate_size(void) +static void +dsm_main_space_request(void *arg) { - return 1024 * 1024 * (size_t) min_dynamic_shared_memory; + dsm_main_space_size = 1024 * 1024 * (size_t) min_dynamic_shared_memory; + + if (dsm_main_space_size == 0) + return; + + ShmemRequestStruct(.name = "Preallocated DSM", + .size = dsm_main_space_size, + .ptr = &dsm_main_space_begin, + ); } -/* - * Initialize space in the main shared memory segment for DSM segments. - */ -void -dsm_shmem_init(void) +static void +dsm_main_space_init(void *arg) { - size_t size = dsm_estimate_size(); - bool found; + FreePageManager *fpm = (FreePageManager *) dsm_main_space_begin; + size_t first_page = 0; + size_t pages; - if (size == 0) + if (dsm_main_space_size == 0) return; - dsm_main_space_begin = ShmemInitStruct("Preallocated DSM", size, &found); - if (!found) - { - FreePageManager *fpm = (FreePageManager *) dsm_main_space_begin; - size_t first_page = 0; - size_t pages; - - /* Reserve space for the FreePageManager. */ - while (first_page * FPM_PAGE_SIZE < sizeof(FreePageManager)) - ++first_page; - - /* Initialize it and give it all the rest of the space. */ - FreePageManagerInitialize(fpm, dsm_main_space_begin); - pages = (size / FPM_PAGE_SIZE) - first_page; - FreePageManagerPut(fpm, first_page, pages); - } + /* Reserve space for the FreePageManager. */ + while (first_page * FPM_PAGE_SIZE < sizeof(FreePageManager)) + ++first_page; + + /* Initialize it and give it all the rest of the space. */ + FreePageManagerInitialize(fpm, dsm_main_space_begin); + pages = (dsm_main_space_size / FPM_PAGE_SIZE) - first_page; + FreePageManagerPut(fpm, first_page, pages); } /* diff --git a/src/backend/storage/ipc/dsm_impl.c b/src/backend/storage/ipc/dsm_impl.c index 6bf8ab5bb5b5e..e8c07805f594e 100644 --- a/src/backend/storage/ipc/dsm_impl.c +++ b/src/backend/storage/ipc/dsm_impl.c @@ -36,7 +36,7 @@ * * As ever, Windows requires its own implementation. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -68,6 +68,7 @@ #include "storage/fd.h" #include "utils/guc.h" #include "utils/memutils.h" +#include "utils/wait_event.h" #ifdef USE_DSM_POSIX static bool dsm_impl_posix(dsm_op op, dsm_handle handle, Size request_size, diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 1d4fd31ffedbc..2b56977659bad 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -15,7 +15,21 @@ * current backend. This function guarantees that only one backend * initializes the segment and that all other backends just attach it. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * A DSA can be created in or retrieved from the registry by calling + * GetNamedDSA(). As with GetNamedDSMSegment(), if a DSA with the provided + * name does not yet exist, it is created. Otherwise, GetNamedDSA() + * ensures the DSA is attached to the current backend. This function + * guarantees that only one backend initializes the DSA and that all other + * backends just attach it. + * + * A dshash table can be created in or retrieved from the registry by + * calling GetNamedDSHash(). As with GetNamedDSMSegment(), if a hash + * table with the provided name does not yet exist, it is created. + * Otherwise, GetNamedDSHash() ensures the hash table is attached to the + * current backend. This function guarantees that only one backend + * initializes the table and that all other backends just attach it. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -26,11 +40,15 @@ #include "postgres.h" +#include "funcapi.h" #include "lib/dshash.h" #include "storage/dsm_registry.h" #include "storage/lwlock.h" #include "storage/shmem.h" +#include "storage/subsystems.h" +#include "utils/builtins.h" #include "utils/memutils.h" +#include "utils/tuplestore.h" typedef struct DSMRegistryCtxStruct { @@ -40,15 +58,61 @@ typedef struct DSMRegistryCtxStruct static DSMRegistryCtxStruct *DSMRegistryCtx; -typedef struct DSMRegistryEntry +static void DSMRegistryShmemRequest(void *arg); +static void DSMRegistryShmemInit(void *arg); + +const ShmemCallbacks DSMRegistryShmemCallbacks = { + .request_fn = DSMRegistryShmemRequest, + .init_fn = DSMRegistryShmemInit, +}; + +typedef struct NamedDSMState { - char name[64]; dsm_handle handle; size_t size; +} NamedDSMState; + +typedef struct NamedDSAState +{ + dsa_handle handle; + int tranche; +} NamedDSAState; + +typedef struct NamedDSHState +{ + dsa_handle dsa_handle; + dshash_table_handle dsh_handle; + int tranche; +} NamedDSHState; + +typedef enum DSMREntryType +{ + DSMR_ENTRY_TYPE_DSM, + DSMR_ENTRY_TYPE_DSA, + DSMR_ENTRY_TYPE_DSH, +} DSMREntryType; + +static const char *const DSMREntryTypeNames[] = +{ + [DSMR_ENTRY_TYPE_DSM] = "segment", + [DSMR_ENTRY_TYPE_DSA] = "area", + [DSMR_ENTRY_TYPE_DSH] = "hash", +}; + +typedef struct DSMRegistryEntry +{ + char name[NAMEDATALEN]; + DSMREntryType type; + union + { + NamedDSMState dsm; + NamedDSAState dsa; + NamedDSHState dsh; + }; } DSMRegistryEntry; static const dshash_parameters dsh_params = { - offsetof(DSMRegistryEntry, handle), + offsetof(DSMRegistryEntry, type), sizeof(DSMRegistryEntry), dshash_strcmp, dshash_strhash, @@ -59,27 +123,20 @@ static const dshash_parameters dsh_params = { static dsa_area *dsm_registry_dsa; static dshash_table *dsm_registry_table; -Size -DSMRegistryShmemSize(void) +static void +DSMRegistryShmemRequest(void *arg) { - return MAXALIGN(sizeof(DSMRegistryCtxStruct)); + ShmemRequestStruct(.name = "DSM Registry Data", + .size = sizeof(DSMRegistryCtxStruct), + .ptr = (void **) &DSMRegistryCtx, + ); } -void -DSMRegistryShmemInit(void) +static void +DSMRegistryShmemInit(void *arg) { - bool found; - - DSMRegistryCtx = (DSMRegistryCtxStruct *) - ShmemInitStruct("DSM Registry Data", - DSMRegistryShmemSize(), - &found); - - if (!found) - { - DSMRegistryCtx->dsah = DSA_HANDLE_INVALID; - DSMRegistryCtx->dshh = DSHASH_HANDLE_INVALID; - } + DSMRegistryCtx->dsah = DSA_HANDLE_INVALID; + DSMRegistryCtx->dshh = DSHASH_HANDLE_INVALID; } /* @@ -101,9 +158,10 @@ init_dsm_registry(void) { /* Initialize dynamic shared hash table for registry. */ dsm_registry_dsa = dsa_create(LWTRANCHE_DSM_REGISTRY_DSA); + dsm_registry_table = dshash_create(dsm_registry_dsa, &dsh_params, NULL); + dsa_pin(dsm_registry_dsa); dsa_pin_mapping(dsm_registry_dsa); - dsm_registry_table = dshash_create(dsm_registry_dsa, &dsh_params, NULL); /* Store handles in shared memory for other backends to use. */ DSMRegistryCtx->dsah = dsa_get_handle(dsm_registry_dsa); @@ -125,15 +183,19 @@ init_dsm_registry(void) * Initialize or attach a named DSM segment. * * This routine returns the address of the segment. init_callback is called to - * initialize the segment when it is first created. + * initialize the segment when it is first created. 'arg' is passed through to + * the initialization callback function. */ void * GetNamedDSMSegment(const char *name, size_t size, - void (*init_callback) (void *ptr), bool *found) + void (*init_callback) (void *ptr, void *arg), + bool *found, void *arg) { DSMRegistryEntry *entry; MemoryContext oldcontext; void *ret; + NamedDSMState *state; + dsm_segment *seg; Assert(found); @@ -141,7 +203,7 @@ GetNamedDSMSegment(const char *name, size_t size, ereport(ERROR, (errmsg("DSM segment name cannot be empty"))); - if (strlen(name) >= offsetof(DSMRegistryEntry, handle)) + if (strlen(name) >= offsetof(DSMRegistryEntry, type)) ereport(ERROR, (errmsg("DSM segment name too long"))); @@ -156,41 +218,227 @@ GetNamedDSMSegment(const char *name, size_t size, init_dsm_registry(); entry = dshash_find_or_insert(dsm_registry_table, name, found); + state = &entry->dsm; if (!(*found)) { + entry->type = DSMR_ENTRY_TYPE_DSM; + state->handle = DSM_HANDLE_INVALID; + state->size = size; + } + else if (entry->type != DSMR_ENTRY_TYPE_DSM) + ereport(ERROR, + (errmsg("requested DSM segment does not match type of existing entry"))); + else if (state->size != size) + ereport(ERROR, + (errmsg("requested DSM segment size does not match size of existing segment"))); + + if (state->handle == DSM_HANDLE_INVALID) + { + *found = false; + /* Initialize the segment. */ - dsm_segment *seg = dsm_create(size, 0); + seg = dsm_create(size, 0); + + if (init_callback) + (*init_callback) (dsm_segment_address(seg), arg); dsm_pin_segment(seg); dsm_pin_mapping(seg); - entry->handle = dsm_segment_handle(seg); - entry->size = size; - ret = dsm_segment_address(seg); - - if (init_callback) - (*init_callback) (ret); - } - else if (entry->size != size) - { - ereport(ERROR, - (errmsg("requested DSM segment size does not match size of " - "existing segment"))); + state->handle = dsm_segment_handle(seg); } else { - dsm_segment *seg = dsm_find_mapping(entry->handle); - /* If the existing segment is not already attached, attach it now. */ + seg = dsm_find_mapping(state->handle); if (seg == NULL) { - seg = dsm_attach(entry->handle); + seg = dsm_attach(state->handle); if (seg == NULL) elog(ERROR, "could not map dynamic shared memory segment"); dsm_pin_mapping(seg); } + } + + ret = dsm_segment_address(seg); + dshash_release_lock(dsm_registry_table, entry); + MemoryContextSwitchTo(oldcontext); + + return ret; +} + +/* + * Initialize or attach a named DSA. + * + * This routine returns a pointer to the DSA. A new LWLock tranche ID will be + * generated if needed. Note that the lock tranche will be registered with the + * provided name. Also note that this should be called at most once for a + * given DSA in each backend. + */ +dsa_area * +GetNamedDSA(const char *name, bool *found) +{ + DSMRegistryEntry *entry; + MemoryContext oldcontext; + dsa_area *ret; + NamedDSAState *state; + + Assert(found); + + if (!name || *name == '\0') + ereport(ERROR, + (errmsg("DSA name cannot be empty"))); + + if (strlen(name) >= offsetof(DSMRegistryEntry, type)) + ereport(ERROR, + (errmsg("DSA name too long"))); + + /* Be sure any local memory allocated by DSM/DSA routines is persistent. */ + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + + /* Connect to the registry. */ + init_dsm_registry(); + + entry = dshash_find_or_insert(dsm_registry_table, name, found); + state = &entry->dsa; + if (!(*found)) + { + entry->type = DSMR_ENTRY_TYPE_DSA; + state->handle = DSA_HANDLE_INVALID; + state->tranche = -1; + } + else if (entry->type != DSMR_ENTRY_TYPE_DSA) + ereport(ERROR, + (errmsg("requested DSA does not match type of existing entry"))); + + if (state->tranche == -1) + { + *found = false; - ret = dsm_segment_address(seg); + /* Initialize the LWLock tranche for the DSA. */ + state->tranche = LWLockNewTrancheId(name); + } + + if (state->handle == DSA_HANDLE_INVALID) + { + *found = false; + + /* Initialize the DSA. */ + ret = dsa_create(state->tranche); + dsa_pin(ret); + dsa_pin_mapping(ret); + + /* Store handle for other backends to use. */ + state->handle = dsa_get_handle(ret); + } + else if (dsa_is_attached(state->handle)) + ereport(ERROR, + (errmsg("requested DSA already attached to current process"))); + else + { + /* Attach to existing DSA. */ + ret = dsa_attach(state->handle); + dsa_pin_mapping(ret); + } + + dshash_release_lock(dsm_registry_table, entry); + MemoryContextSwitchTo(oldcontext); + + return ret; +} + +/* + * Initialize or attach a named dshash table. + * + * This routine returns the address of the table. The tranche_id member of + * params is ignored; a new LWLock tranche ID will be generated if needed. + * Note that the lock tranche will be registered with the provided name. Also + * note that this should be called at most once for a given table in each + * backend. + */ +dshash_table * +GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) +{ + DSMRegistryEntry *entry; + MemoryContext oldcontext; + dshash_table *ret; + NamedDSHState *dsh_state; + + Assert(params); + Assert(found); + + if (!name || *name == '\0') + ereport(ERROR, + (errmsg("DSHash name cannot be empty"))); + + if (strlen(name) >= offsetof(DSMRegistryEntry, type)) + ereport(ERROR, + (errmsg("DSHash name too long"))); + + /* Be sure any local memory allocated by DSM/DSA routines is persistent. */ + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + + /* Connect to the registry. */ + init_dsm_registry(); + + entry = dshash_find_or_insert(dsm_registry_table, name, found); + dsh_state = &entry->dsh; + if (!(*found)) + { + entry->type = DSMR_ENTRY_TYPE_DSH; + dsh_state->dsa_handle = DSA_HANDLE_INVALID; + dsh_state->dsh_handle = DSHASH_HANDLE_INVALID; + dsh_state->tranche = -1; + } + else if (entry->type != DSMR_ENTRY_TYPE_DSH) + ereport(ERROR, + (errmsg("requested DSHash does not match type of existing entry"))); + + if (dsh_state->tranche == -1) + { + *found = false; + + /* Initialize the LWLock tranche for the hash table. */ + dsh_state->tranche = LWLockNewTrancheId(name); + } + + if (dsh_state->dsa_handle == DSA_HANDLE_INVALID) + { + dshash_parameters params_copy; + dsa_area *dsa; + + *found = false; + + /* Initialize the DSA for the hash table. */ + dsa = dsa_create(dsh_state->tranche); + + /* Initialize the dshash table. */ + memcpy(¶ms_copy, params, sizeof(dshash_parameters)); + params_copy.tranche_id = dsh_state->tranche; + ret = dshash_create(dsa, ¶ms_copy, NULL); + + dsa_pin(dsa); + dsa_pin_mapping(dsa); + + /* Store handles for other backends to use. */ + dsh_state->dsa_handle = dsa_get_handle(dsa); + dsh_state->dsh_handle = dshash_get_hash_table_handle(ret); + } + else if (dsa_is_attached(dsh_state->dsa_handle)) + ereport(ERROR, + (errmsg("requested DSHash already attached to current process"))); + else + { + dsa_area *dsa; + + /* XXX: Should we verify params matches what table was created with? */ + + /* Attach to existing DSA for the hash table. */ + dsa = dsa_attach(dsh_state->dsa_handle); + dsa_pin_mapping(dsa); + + /* Attach to existing dshash table. */ + ret = dshash_attach(dsa, params, dsh_state->dsh_handle, NULL); } dshash_release_lock(dsm_registry_table, entry); @@ -198,3 +446,47 @@ GetNamedDSMSegment(const char *name, size_t size, return ret; } + +Datum +pg_get_dsm_registry_allocations(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + DSMRegistryEntry *entry; + MemoryContext oldcontext; + dshash_seq_status status; + + InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC); + + /* Be sure any local memory allocated by DSM/DSA routines is persistent. */ + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + init_dsm_registry(); + MemoryContextSwitchTo(oldcontext); + + dshash_seq_init(&status, dsm_registry_table, false); + while ((entry = dshash_seq_next(&status)) != NULL) + { + Datum vals[3]; + bool nulls[3] = {0}; + + vals[0] = CStringGetTextDatum(entry->name); + vals[1] = CStringGetTextDatum(DSMREntryTypeNames[entry->type]); + + /* Be careful to only return the sizes of initialized entries. */ + if (entry->type == DSMR_ENTRY_TYPE_DSM && + entry->dsm.handle != DSM_HANDLE_INVALID) + vals[2] = Int64GetDatum(entry->dsm.size); + else if (entry->type == DSMR_ENTRY_TYPE_DSA && + entry->dsa.handle != DSA_HANDLE_INVALID) + vals[2] = Int64GetDatum(dsa_get_total_size_from_handle(entry->dsa.handle)); + else if (entry->type == DSMR_ENTRY_TYPE_DSH && + entry->dsh.dsa_handle !=DSA_HANDLE_INVALID) + vals[2] = Int64GetDatum(dsa_get_total_size_from_handle(entry->dsh.dsa_handle)); + else + nulls[2] = true; + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, vals, nulls); + } + dshash_seq_term(&status); + + return (Datum) 0; +} diff --git a/src/backend/storage/ipc/ipc.c b/src/backend/storage/ipc/ipc.c index 567739b5be93a..cb944edd8dfdd 100644 --- a/src/backend/storage/ipc/ipc.c +++ b/src/backend/storage/ipc/ipc.c @@ -8,7 +8,7 @@ * exit-time cleanup for either a postmaster or a backend. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,6 +29,7 @@ #endif #include "storage/dsm.h" #include "storage/ipc.h" +#include "storage/lwlock.h" #include "tcop/tcopprot.h" @@ -229,13 +230,19 @@ shmem_exit(int code) { shmem_exit_inprogress = true; + /* + * Release any LWLocks we might be holding before callbacks run. This + * prevents accessing locks in detached DSM segments and allows callbacks + * to acquire new locks. + */ + LWLockReleaseAll(); + /* * Call before_shmem_exit callbacks. * * These should be things that need most of the system to still be up and * working, such as cleanup of temp relations, which requires catalog - * access; or things that need to be completed because later cleanup steps - * depend on them, such as releasing lwlocks. + * access. */ elog(DEBUG3, "shmem_exit(%d): %d before_shmem_exit callbacks to make", code, before_shmem_exit_index); @@ -399,7 +406,7 @@ cancel_before_shmem_exit(pg_on_exit_callback function, Datum arg) before_shmem_exit_list[before_shmem_exit_index - 1].arg == arg) --before_shmem_exit_index; else - elog(ERROR, "before_shmem_exit callback (%p,0x%" PRIxPTR ") is not the latest entry", + elog(ERROR, "before_shmem_exit callback (%p,0x%" PRIx64 ") is not the latest entry", function, arg); } diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f66..e149a738c8d28 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -3,7 +3,7 @@ * ipci.c * POSTGRES inter-process communication initialization code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,43 +14,16 @@ */ #include "postgres.h" -#include "access/clog.h" -#include "access/commit_ts.h" -#include "access/multixact.h" -#include "access/nbtree.h" -#include "access/subtrans.h" -#include "access/syncscan.h" -#include "access/transam.h" -#include "access/twophase.h" -#include "access/xlogprefetcher.h" -#include "access/xlogrecovery.h" -#include "commands/async.h" #include "miscadmin.h" #include "pgstat.h" -#include "postmaster/autovacuum.h" -#include "postmaster/bgworker_internals.h" -#include "postmaster/bgwriter.h" -#include "postmaster/walsummarizer.h" -#include "replication/logicallauncher.h" -#include "replication/origin.h" -#include "replication/slot.h" -#include "replication/slotsync.h" -#include "replication/walreceiver.h" -#include "replication/walsender.h" -#include "storage/aio_subsys.h" -#include "storage/bufmgr.h" #include "storage/dsm.h" -#include "storage/dsm_registry.h" #include "storage/ipc.h" +#include "storage/lock.h" #include "storage/pg_shmem.h" -#include "storage/pmsignal.h" -#include "storage/predicate.h" #include "storage/proc.h" -#include "storage/procarray.h" -#include "storage/procsignal.h" -#include "storage/sinvaladt.h" +#include "storage/shmem_internal.h" +#include "storage/subsystems.h" #include "utils/guc.h" -#include "utils/injection_point.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -59,8 +32,6 @@ shmem_startup_hook_type shmem_startup_hook = NULL; static Size total_addin_request = 0; -static void CreateOrAttachShmemStructs(void); - /* * RequestAddinShmemSpace * Request that extra shmem space be allocated for use by @@ -80,23 +51,12 @@ RequestAddinShmemSpace(Size size) /* * CalculateShmemSize - * Calculates the amount of shared memory and number of semaphores needed. - * - * If num_semaphores is not NULL, it will be set to the number of semaphores - * required. + * Calculates the amount of shared memory needed. */ Size -CalculateShmemSize(int *num_semaphores) +CalculateShmemSize(void) { Size size; - int numSemas; - - /* Compute number of semaphores we'll need */ - numSemas = ProcGlobalSemas(); - - /* Return the number of semaphores if requested by the caller */ - if (num_semaphores) - *num_semaphores = numSemas; /* * Size of the Postgres shared-memory block is estimated via moderately- @@ -108,48 +68,7 @@ CalculateShmemSize(int *num_semaphores) * during the actual allocation phase. */ size = 100000; - size = add_size(size, PGSemaphoreShmemSize(numSemas)); - size = add_size(size, hash_estimate_size(SHMEM_INDEX_SIZE, - sizeof(ShmemIndexEnt))); - size = add_size(size, dsm_estimate_size()); - size = add_size(size, DSMRegistryShmemSize()); - size = add_size(size, BufferManagerShmemSize()); - size = add_size(size, LockManagerShmemSize()); - size = add_size(size, PredicateLockShmemSize()); - size = add_size(size, ProcGlobalShmemSize()); - size = add_size(size, XLogPrefetchShmemSize()); - size = add_size(size, VarsupShmemSize()); - size = add_size(size, XLOGShmemSize()); - size = add_size(size, XLogRecoveryShmemSize()); - size = add_size(size, CLOGShmemSize()); - size = add_size(size, CommitTsShmemSize()); - size = add_size(size, SUBTRANSShmemSize()); - size = add_size(size, TwoPhaseShmemSize()); - size = add_size(size, BackgroundWorkerShmemSize()); - size = add_size(size, MultiXactShmemSize()); - size = add_size(size, LWLockShmemSize()); - size = add_size(size, ProcArrayShmemSize()); - size = add_size(size, BackendStatusShmemSize()); - size = add_size(size, SharedInvalShmemSize()); - size = add_size(size, PMSignalShmemSize()); - size = add_size(size, ProcSignalShmemSize()); - size = add_size(size, CheckpointerShmemSize()); - size = add_size(size, AutoVacuumShmemSize()); - size = add_size(size, ReplicationSlotsShmemSize()); - size = add_size(size, ReplicationOriginShmemSize()); - size = add_size(size, WalSndShmemSize()); - size = add_size(size, WalRcvShmemSize()); - size = add_size(size, WalSummarizerShmemSize()); - size = add_size(size, PgArchShmemSize()); - size = add_size(size, ApplyLauncherShmemSize()); - size = add_size(size, BTreeShmemSize()); - size = add_size(size, SyncScanShmemSize()); - size = add_size(size, AsyncShmemSize()); - size = add_size(size, StatsShmemSize()); - size = add_size(size, WaitEventCustomShmemSize()); - size = add_size(size, InjectionPointShmemSize()); - size = add_size(size, SlotSyncShmemSize()); - size = add_size(size, AioShmemSize()); + size = add_size(size, ShmemGetRequestedSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -182,7 +101,8 @@ AttachSharedMemoryStructs(void) */ InitializeFastPathLocks(); - CreateOrAttachShmemStructs(); + /* Establish pointers to all shared memory areas in this backend */ + ShmemAttachRequested(); /* * Now give loadable modules a chance to set up their shmem allocations @@ -202,12 +122,11 @@ CreateSharedMemoryAndSemaphores(void) PGShmemHeader *shim; PGShmemHeader *seghdr; Size size; - int numSemas; Assert(!IsUnderPostmaster); /* Compute the size of the shared-memory block */ - size = CalculateShmemSize(&numSemas); + size = CalculateShmemSize(); elog(DEBUG3, "invoking IpcMemoryCreate(size=%zu)", size); /* @@ -222,20 +141,13 @@ CreateSharedMemoryAndSemaphores(void) Assert(strcmp("unknown", GetConfigOption("huge_pages_status", false, false)) != 0); - InitShmemAccess(seghdr); - - /* - * Create semaphores - */ - PGReserveSemaphores(numSemas); - /* * Set up shared memory allocation mechanism */ - InitShmemAllocation(); + InitShmemAllocator(seghdr); - /* Initialize subsystems */ - CreateOrAttachShmemStructs(); + /* Initialize all shmem areas */ + ShmemInitRequested(); /* Initialize dynamic shared memory facilities. */ dsm_postmaster_startup(shim); @@ -248,101 +160,23 @@ CreateSharedMemoryAndSemaphores(void) } /* - * Initialize various subsystems, setting up their data structures in - * shared memory. - * - * This is called by the postmaster or by a standalone backend. - * It is also called by a backend forked from the postmaster in the - * EXEC_BACKEND case. In the latter case, the shared memory segment - * already exists and has been physically attached to, but we have to - * initialize pointers in local memory that reference the shared structures, - * because we didn't inherit the correct pointer values from the postmaster - * as we do in the fork() scenario. The easiest way to do that is to run - * through the same code as before. (Note that the called routines mostly - * check IsUnderPostmaster, rather than EXEC_BACKEND, to detect this case. - * This is a bit code-wasteful and could be cleaned up.) + * Early initialization of various subsystems, giving them a chance to + * register their shared memory needs before the shared memory segment is + * allocated. */ -static void -CreateOrAttachShmemStructs(void) +void +RegisterBuiltinShmemCallbacks(void) { /* - * Now initialize LWLocks, which do shared memory allocation and are - * needed for InitShmemIndex. - */ - CreateLWLocks(); - - /* - * Set up shmem.c index hashtable + * Call RegisterShmemCallbacks(...) on each subsystem listed in + * subsystemlist.h */ - InitShmemIndex(); +#define PG_SHMEM_SUBSYSTEM(subsystem_callbacks) \ + RegisterShmemCallbacks(&(subsystem_callbacks)); - dsm_shmem_init(); - DSMRegistryShmemInit(); +#include "storage/subsystemlist.h" - /* - * Set up xlog, clog, and buffers - */ - VarsupShmemInit(); - XLOGShmemInit(); - XLogPrefetchShmemInit(); - XLogRecoveryShmemInit(); - CLOGShmemInit(); - CommitTsShmemInit(); - SUBTRANSShmemInit(); - MultiXactShmemInit(); - BufferManagerShmemInit(); - - /* - * Set up lock manager - */ - LockManagerShmemInit(); - - /* - * Set up predicate lock manager - */ - PredicateLockShmemInit(); - - /* - * Set up process table - */ - if (!IsUnderPostmaster) - InitProcGlobal(); - ProcArrayShmemInit(); - BackendStatusShmemInit(); - TwoPhaseShmemInit(); - BackgroundWorkerShmemInit(); - - /* - * Set up shared-inval messaging - */ - SharedInvalShmemInit(); - - /* - * Set up interprocess signaling mechanisms - */ - PMSignalShmemInit(); - ProcSignalShmemInit(); - CheckpointerShmemInit(); - AutoVacuumShmemInit(); - ReplicationSlotsShmemInit(); - ReplicationOriginShmemInit(); - WalSndShmemInit(); - WalRcvShmemInit(); - WalSummarizerShmemInit(); - PgArchShmemInit(); - ApplyLauncherShmemInit(); - SlotSyncShmemInit(); - - /* - * Set up other modules that need some shared memory space - */ - BTreeShmemInit(); - SyncScanShmemInit(); - AsyncShmemInit(); - StatsShmemInit(); - WaitEventCustomShmemInit(); - InjectionPointShmemInit(); - AioShmemInit(); +#undef PG_SHMEM_SUBSYSTEM } /* @@ -358,12 +192,11 @@ InitializeShmemGUCs(void) Size size_b; Size size_mb; Size hp_size; - int num_semas; /* * Calculate the shared memory size and round up to the nearest megabyte. */ - size_b = CalculateShmemSize(&num_semas); + size_b = CalculateShmemSize(); size_mb = add_size(size_b, (1024 * 1024) - 1) / (1024 * 1024); sprintf(buf, "%zu", size_mb); SetConfigOption("shared_memory_size", buf, @@ -377,12 +210,14 @@ InitializeShmemGUCs(void) { Size hp_required; - hp_required = add_size(size_b / hp_size, 1); + hp_required = size_b / hp_size; + if (size_b % hp_size != 0) + hp_required = add_size(hp_required, 1); sprintf(buf, "%zu", hp_required); SetConfigOption("shared_memory_size_in_huge_pages", buf, PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT); } - sprintf(buf, "%d", num_semas); + sprintf(buf, "%d", ProcGlobalSemas()); SetConfigOption("num_os_semaphores", buf, PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT); } diff --git a/src/backend/storage/ipc/latch.c b/src/backend/storage/ipc/latch.c index c6aefd2f688dd..7d4f4cf32bb2d 100644 --- a/src/backend/storage/ipc/latch.c +++ b/src/backend/storage/ipc/latch.c @@ -8,7 +8,7 @@ * signal handler sets a flag variable. See latch.h for more information * on how to use them. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -80,10 +80,10 @@ InitLatch(Latch *latch) * current process. * * InitSharedLatch needs to be called in postmaster before forking child - * processes, usually right after allocating the shared memory block - * containing the latch with ShmemInitStruct. (The Unix implementation - * doesn't actually require that, but the Windows one does.) Because of - * this restriction, we have no concurrency issues to worry about here. + * processes, usually right after initializing the shared memory block + * containing the latch. (The Unix implementation doesn't actually require + * that, but the Windows one does.) Because of this restriction, we have no + * concurrency issues to worry about here. * * Note that other handles created in this module are never marked as * inheritable. Thus we do not need to worry about cleaning up child @@ -187,9 +187,11 @@ WaitLatch(Latch *latch, int wakeEvents, long timeout, if (!(wakeEvents & WL_LATCH_SET)) latch = NULL; ModifyWaitEvent(LatchWaitSet, LatchWaitSetLatchPos, WL_LATCH_SET, latch); - ModifyWaitEvent(LatchWaitSet, LatchWaitSetPostmasterDeathPos, - (wakeEvents & (WL_EXIT_ON_PM_DEATH | WL_POSTMASTER_DEATH)), - NULL); + + if (IsUnderPostmaster) + ModifyWaitEvent(LatchWaitSet, LatchWaitSetPostmasterDeathPos, + (wakeEvents & (WL_EXIT_ON_PM_DEATH | WL_POSTMASTER_DEATH)), + NULL); if (WaitEventSetWait(LatchWaitSet, (wakeEvents & WL_TIMEOUT) ? timeout : -1, diff --git a/src/backend/storage/ipc/meson.build b/src/backend/storage/ipc/meson.build index b1b73dac3bed6..b8c31e29967da 100644 --- a/src/backend/storage/ipc/meson.build +++ b/src/backend/storage/ipc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'barrier.c', @@ -14,6 +14,7 @@ backend_sources += files( 'shm_mq.c', 'shm_toc.c', 'shmem.c', + 'shmem_hash.c', 'signalfuncs.c', 'sinval.c', 'sinvaladt.c', diff --git a/src/backend/storage/ipc/pmsignal.c b/src/backend/storage/ipc/pmsignal.c index f2ea01622f9f4..bdad5fdd0434e 100644 --- a/src/backend/storage/ipc/pmsignal.c +++ b/src/backend/storage/ipc/pmsignal.c @@ -4,7 +4,7 @@ * routines for signaling between the postmaster and its child processes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -27,6 +27,7 @@ #include "storage/ipc.h" #include "storage/pmsignal.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/memutils.h" @@ -83,6 +84,14 @@ struct PMSignalData /* PMSignalState pointer is valid in both postmaster and child processes */ NON_EXEC_STATIC volatile PMSignalData *PMSignalState = NULL; +static void PMSignalShmemRequest(void *); +static void PMSignalShmemInit(void *); + +const ShmemCallbacks PMSignalShmemCallbacks = { + .request_fn = PMSignalShmemRequest, + .init_fn = PMSignalShmemInit, +}; + /* * Local copy of PMSignalState->num_child_flags, only valid in the * postmaster. Postmaster keeps a local copy so that it doesn't need to @@ -123,39 +132,29 @@ postmaster_death_handler(SIGNAL_ARGS) static void MarkPostmasterChildInactive(int code, Datum arg); /* - * PMSignalShmemSize - * Compute space needed for pmsignal.c's shared memory + * PMSignalShmemRequest - Register pmsignal.c's shared memory needs */ -Size -PMSignalShmemSize(void) +static void +PMSignalShmemRequest(void *arg) { - Size size; + size_t size; - size = offsetof(PMSignalData, PMChildFlags); - size = add_size(size, mul_size(MaxLivePostmasterChildren(), - sizeof(sig_atomic_t))); + num_child_flags = MaxLivePostmasterChildren(); - return size; + size = add_size(offsetof(PMSignalData, PMChildFlags), + mul_size(num_child_flags, sizeof(sig_atomic_t))); + ShmemRequestStruct(.name = "PMSignalState", + .size = size, + .ptr = (void **) &PMSignalState, + ); } -/* - * PMSignalShmemInit - initialize during shared-memory creation - */ -void -PMSignalShmemInit(void) +static void +PMSignalShmemInit(void *arg) { - bool found; - - PMSignalState = (PMSignalData *) - ShmemInitStruct("PMSignalState", PMSignalShmemSize(), &found); - - if (!found) - { - /* initialize all flags to zeroes */ - MemSet(unvolatize(PMSignalData *, PMSignalState), 0, PMSignalShmemSize()); - num_child_flags = MaxLivePostmasterChildren(); - PMSignalState->num_child_flags = num_child_flags; - } + Assert(PMSignalState); + Assert(num_child_flags > 0); + PMSignalState->num_child_flags = num_child_flags; } /* diff --git a/src/backend/storage/ipc/procarray.c b/src/backend/storage/ipc/procarray.c index e5b945a9ee39c..9299bcebbda87 100644 --- a/src/backend/storage/ipc/procarray.c +++ b/src/backend/storage/ipc/procarray.c @@ -34,7 +34,7 @@ * happen, it would tie up KnownAssignedXids indefinitely, so we protect * ourselves by pruning the array when a valid list of running XIDs arrives. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -54,16 +54,21 @@ #include "access/xlogutils.h" #include "catalog/catalog.h" #include "catalog/pg_authid.h" -#include "commands/dbcommands.h" #include "miscadmin.h" #include "pgstat.h" +#include "postmaster/bgworker.h" #include "port/pg_lfind.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/procsignal.h" +#include "storage/subsystems.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/injection_point.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/snapmgr.h" +#include "utils/wait_event.h" #define UINT32_ACCESS_ONCE(var) ((uint32)(*((volatile uint32 *)&(var)))) @@ -99,6 +104,18 @@ typedef struct ProcArrayStruct int pgprocnos[FLEXIBLE_ARRAY_MEMBER]; } ProcArrayStruct; +static void ProcArrayShmemRequest(void *arg); +static void ProcArrayShmemInit(void *arg); +static void ProcArrayShmemAttach(void *arg); + +static ProcArrayStruct *procArray; + +const struct ShmemCallbacks ProcArrayShmemCallbacks = { + .request_fn = ProcArrayShmemRequest, + .init_fn = ProcArrayShmemInit, + .attach_fn = ProcArrayShmemAttach, +}; + /* * State for the GlobalVisTest* family of functions. Those functions can * e.g. be used to decide if a deleted row can be removed without violating @@ -265,9 +282,6 @@ typedef enum KAXCompressReason KAX_STARTUP_PROCESS_IDLE, /* startup process is about to sleep */ } KAXCompressReason; - -static ProcArrayStruct *procArray; - static PGPROC *allProcs; /* @@ -278,8 +292,11 @@ static TransactionId cachedXidIsNotInProgress = InvalidTransactionId; /* * Bookkeeping for tracking emulated transactions in recovery */ + static TransactionId *KnownAssignedXids; + static bool *KnownAssignedXidsValid; + static TransactionId latestObservedXid = InvalidTransactionId; /* @@ -370,19 +387,13 @@ static inline FullTransactionId FullXidRelativeTo(FullTransactionId rel, static void GlobalVisUpdateApply(ComputeXidHorizonsResult *horizons); /* - * Report shared-memory space needed by ProcArrayShmemInit + * Register the shared PGPROC array during postmaster startup. */ -Size -ProcArrayShmemSize(void) +static void +ProcArrayShmemRequest(void *arg) { - Size size; - - /* Size of the ProcArray structure itself */ #define PROCARRAY_MAXPROCS (MaxBackends + max_prepared_xacts) - size = offsetof(ProcArrayStruct, pgprocnos); - size = add_size(size, mul_size(sizeof(int), PROCARRAY_MAXPROCS)); - /* * During Hot Standby processing we have a data structure called * KnownAssignedXids, created in shared memory. Local data structures are @@ -401,64 +412,49 @@ ProcArrayShmemSize(void) if (EnableHotStandby) { - size = add_size(size, - mul_size(sizeof(TransactionId), - TOTAL_MAX_CACHED_SUBXIDS)); - size = add_size(size, - mul_size(sizeof(bool), TOTAL_MAX_CACHED_SUBXIDS)); + ShmemRequestStruct(.name = "KnownAssignedXids", + .size = mul_size(sizeof(TransactionId), TOTAL_MAX_CACHED_SUBXIDS), + .ptr = (void **) &KnownAssignedXids, + ); + + ShmemRequestStruct(.name = "KnownAssignedXidsValid", + .size = mul_size(sizeof(bool), TOTAL_MAX_CACHED_SUBXIDS), + .ptr = (void **) &KnownAssignedXidsValid, + ); } - return size; + /* Register the ProcArray shared structure */ + ShmemRequestStruct(.name = "Proc Array", + .size = add_size(offsetof(ProcArrayStruct, pgprocnos), + mul_size(sizeof(int), PROCARRAY_MAXPROCS)), + .ptr = (void **) &procArray, + ); } /* * Initialize the shared PGPROC array during postmaster startup. */ -void -ProcArrayShmemInit(void) +static void +ProcArrayShmemInit(void *arg) { - bool found; - - /* Create or attach to the ProcArray shared structure */ - procArray = (ProcArrayStruct *) - ShmemInitStruct("Proc Array", - add_size(offsetof(ProcArrayStruct, pgprocnos), - mul_size(sizeof(int), - PROCARRAY_MAXPROCS)), - &found); - - if (!found) - { - /* - * We're the first - initialize. - */ - procArray->numProcs = 0; - procArray->maxProcs = PROCARRAY_MAXPROCS; - procArray->maxKnownAssignedXids = TOTAL_MAX_CACHED_SUBXIDS; - procArray->numKnownAssignedXids = 0; - procArray->tailKnownAssignedXids = 0; - procArray->headKnownAssignedXids = 0; - procArray->lastOverflowedXid = InvalidTransactionId; - procArray->replication_slot_xmin = InvalidTransactionId; - procArray->replication_slot_catalog_xmin = InvalidTransactionId; - TransamVariables->xactCompletionCount = 1; - } + procArray->numProcs = 0; + procArray->maxProcs = PROCARRAY_MAXPROCS; + procArray->maxKnownAssignedXids = TOTAL_MAX_CACHED_SUBXIDS; + procArray->numKnownAssignedXids = 0; + procArray->tailKnownAssignedXids = 0; + procArray->headKnownAssignedXids = 0; + procArray->lastOverflowedXid = InvalidTransactionId; + procArray->replication_slot_xmin = InvalidTransactionId; + procArray->replication_slot_catalog_xmin = InvalidTransactionId; + TransamVariables->xactCompletionCount = 1; allProcs = ProcGlobal->allProcs; +} - /* Create or attach to the KnownAssignedXids arrays too, if needed */ - if (EnableHotStandby) - { - KnownAssignedXids = (TransactionId *) - ShmemInitStruct("KnownAssignedXids", - mul_size(sizeof(TransactionId), - TOTAL_MAX_CACHED_SUBXIDS), - &found); - KnownAssignedXidsValid = (bool *) - ShmemInitStruct("KnownAssignedXidsValid", - mul_size(sizeof(bool), TOTAL_MAX_CACHED_SUBXIDS), - &found); - } +static void +ProcArrayShmemAttach(void *arg) +{ + allProcs = ProcGlobal->allProcs; } /* @@ -706,8 +702,6 @@ ProcArrayEndTransaction(PGPROC *proc, TransactionId latestXid) /* be sure this is cleared in abort */ proc->delayChkptFlags = 0; - proc->recoveryConflictPending = false; - /* must be cleared with xid/xmin: */ /* avoid unnecessarily dirtying shared cachelines */ if (proc->statusFlags & PROC_VACUUM_STATE_MASK) @@ -748,8 +742,6 @@ ProcArrayEndTransactionInternal(PGPROC *proc, TransactionId latestXid) /* be sure this is cleared in abort */ proc->delayChkptFlags = 0; - proc->recoveryConflictPending = false; - /* must be cleared with xid/xmin: */ /* avoid unnecessarily dirtying shared cachelines */ if (proc->statusFlags & PROC_VACUUM_STATE_MASK) @@ -931,7 +923,6 @@ ProcArrayClearTransaction(PGPROC *proc) proc->vxid.lxid = InvalidLocalTransactionId; proc->xmin = InvalidTransactionId; - proc->recoveryConflictPending = false; Assert(!(proc->statusFlags & PROC_VACUUM_STATE_MASK)); Assert(!proc->delayChkptFlags); @@ -1162,7 +1153,7 @@ ProcArrayApplyRecoveryInfo(RunningTransactions running) * Allocate a temporary array to avoid modifying the array passed as * argument. */ - xids = palloc(sizeof(TransactionId) * (running->xcnt + running->subxcnt)); + xids = palloc_array(TransactionId, running->xcnt + running->subxcnt); /* * Add to the temp array any xids which have not already completed. @@ -1622,58 +1613,6 @@ TransactionIdIsInProgress(TransactionId xid) return false; } -/* - * TransactionIdIsActive -- is xid the top-level XID of an active backend? - * - * This differs from TransactionIdIsInProgress in that it ignores prepared - * transactions, as well as transactions running on the primary if we're in - * hot standby. Also, we ignore subtransactions since that's not needed - * for current uses. - */ -bool -TransactionIdIsActive(TransactionId xid) -{ - bool result = false; - ProcArrayStruct *arrayP = procArray; - TransactionId *other_xids = ProcGlobal->xids; - int i; - - /* - * Don't bother checking a transaction older than RecentXmin; it could not - * possibly still be running. - */ - if (TransactionIdPrecedes(xid, RecentXmin)) - return false; - - LWLockAcquire(ProcArrayLock, LW_SHARED); - - for (i = 0; i < arrayP->numProcs; i++) - { - int pgprocno = arrayP->pgprocnos[i]; - PGPROC *proc = &allProcs[pgprocno]; - TransactionId pxid; - - /* Fetch xid just once - see GetNewTransactionId */ - pxid = UINT32_ACCESS_ONCE(other_xids[i]); - - if (!TransactionIdIsValid(pxid)) - continue; - - if (proc->pid == 0) - continue; /* ignore prepared transactions */ - - if (TransactionIdEquals(pxid, xid)) - { - result = true; - break; - } - } - - LWLockRelease(ProcArrayLock); - - return result; -} - /* * Determine XID horizons. @@ -2684,9 +2623,11 @@ ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc) * * Note that if any transaction has overflowed its cached subtransactions * then there is no real need include any subtransactions. + * + * If 'dbid' is valid, only gather transactions running in that database. */ RunningTransactions -GetRunningTransactionData(void) +GetRunningTransactionData(Oid dbid) { /* result workspace */ static RunningTransactionsData CurrentRunningXactsData; @@ -2761,6 +2702,18 @@ GetRunningTransactionData(void) if (!TransactionIdIsValid(xid)) continue; + /* + * Filter by database OID if requested. + */ + if (OidIsValid(dbid)) + { + int pgprocno = arrayP->pgprocnos[index]; + PGPROC *proc = &allProcs[pgprocno]; + + if (proc->databaseId != dbid) + continue; + } + /* * Be careful not to exclude any xids before calculating the values of * oldestRunningXid and suboverflowed, since these are used to clean @@ -2811,6 +2764,12 @@ GetRunningTransactionData(void) PGPROC *proc = &allProcs[pgprocno]; int nsubxids; + /* + * Filter by database OID if requested. + */ + if (OidIsValid(dbid) && proc->databaseId != dbid) + continue; + /* * Save subtransaction XIDs. Other backends can't add or remove * entries while we're holding XidGenLock. @@ -2844,6 +2803,7 @@ GetRunningTransactionData(void) * increases if slots do. */ + CurrentRunningXacts->dbid = dbid; CurrentRunningXacts->xcnt = count - subcount; CurrentRunningXacts->subxcnt = subcount; CurrentRunningXacts->subxid_status = suboverflowed ? SUBXIDS_IN_SUBTRANS : SUBXIDS_IN_ARRAY; @@ -2866,8 +2826,10 @@ GetRunningTransactionData(void) * * Similar to GetSnapshotData but returns just oldestActiveXid. We include * all PGPROCs with an assigned TransactionId, even VACUUM processes. - * We look at all databases, though there is no need to include WALSender - * since this has no effect on hot standby conflicts. + * + * If allDbs is true, we look at all databases, though there is no need to + * include WALSender since this has no effect on hot standby conflicts. If + * allDbs is false, skip processes attached to other databases. * * This is never executed during recovery so there is no need to look at * KnownAssignedXids. @@ -2875,9 +2837,12 @@ GetRunningTransactionData(void) * We don't worry about updating other counters, we want to keep this as * simple as possible and leave GetSnapshotData() as the primary code for * that bookkeeping. + * + * inCommitOnly indicates getting the oldestActiveXid among the transactions + * in the commit critical section. */ TransactionId -GetOldestActiveTransactionId(void) +GetOldestActiveTransactionId(bool inCommitOnly, bool allDbs) { ProcArrayStruct *arrayP = procArray; TransactionId *other_xids = ProcGlobal->xids; @@ -2904,6 +2869,8 @@ GetOldestActiveTransactionId(void) for (index = 0; index < arrayP->numProcs; index++) { TransactionId xid; + int pgprocno = arrayP->pgprocnos[index]; + PGPROC *proc = &allProcs[pgprocno]; /* Fetch xid just once - see GetNewTransactionId */ xid = UINT32_ACCESS_ONCE(other_xids[index]); @@ -2911,6 +2878,13 @@ GetOldestActiveTransactionId(void) if (!TransactionIdIsNormal(xid)) continue; + if (inCommitOnly && + (proc->delayChkptFlags & DELAY_CHKPT_IN_COMMIT) == 0) + continue; + + if (!allDbs && proc->databaseId != MyDatabaseId) + continue; + if (TransactionIdPrecedes(xid, oldestRunningXid)) oldestRunningXid = xid; @@ -3050,8 +3024,7 @@ GetVirtualXIDsDelayingChkpt(int *nvxids, int type) Assert(type != 0); /* allocate what's certainly enough result space */ - vxids = (VirtualTransactionId *) - palloc(sizeof(VirtualTransactionId) * arrayP->maxProcs); + vxids = palloc_array(VirtualTransactionId, arrayP->maxProcs); LWLockAcquire(ProcArrayLock, LW_SHARED); @@ -3331,8 +3304,7 @@ GetCurrentVirtualXIDs(TransactionId limitXmin, bool excludeXmin0, int index; /* allocate what's certainly enough result space */ - vxids = (VirtualTransactionId *) - palloc(sizeof(VirtualTransactionId) * arrayP->maxProcs); + vxids = palloc_array(VirtualTransactionId, arrayP->maxProcs); LWLockAcquire(ProcArrayLock, LW_SHARED); @@ -3483,19 +3455,46 @@ GetConflictingVirtualXIDs(TransactionId limitXmin, Oid dbOid) } /* - * CancelVirtualTransaction - used in recovery conflict processing + * SignalRecoveryConflict -- signal that a process is blocking recovery * - * Returns pid of the process signaled, or 0 if not found. + * The 'pid' is redundant with 'proc', but it acts as a cross-check to + * detect process had exited and the PGPROC entry was reused for a different + * process. + * + * Returns true if the process was signaled, or false if not found. */ -pid_t -CancelVirtualTransaction(VirtualTransactionId vxid, ProcSignalReason sigmode) +bool +SignalRecoveryConflict(PGPROC *proc, pid_t pid, RecoveryConflictReason reason) { - return SignalVirtualTransaction(vxid, sigmode, true); + bool found = false; + + LWLockAcquire(ProcArrayLock, LW_SHARED); + + /* + * Kill the pid if it's still here. If not, that's what we wanted so + * ignore any errors. + */ + if (proc->pid == pid) + { + (void) pg_atomic_fetch_or_u32(&proc->pendingRecoveryConflicts, (1 << reason)); + + /* wake up the process */ + (void) SendProcSignal(pid, PROCSIG_RECOVERY_CONFLICT, GetNumberFromPGProc(proc)); + found = true; + } + + LWLockRelease(ProcArrayLock); + + return found; } -pid_t -SignalVirtualTransaction(VirtualTransactionId vxid, ProcSignalReason sigmode, - bool conflictPending) +/* + * SignalRecoveryConflictWithVirtualXID -- signal that a VXID is blocking recovery + * + * Like SignalRecoveryConflict, but the target is identified by VXID + */ +bool +SignalRecoveryConflictWithVirtualXID(VirtualTransactionId vxid, RecoveryConflictReason reason) { ProcArrayStruct *arrayP = procArray; int index; @@ -3514,15 +3513,16 @@ SignalVirtualTransaction(VirtualTransactionId vxid, ProcSignalReason sigmode, if (procvxid.procNumber == vxid.procNumber && procvxid.localTransactionId == vxid.localTransactionId) { - proc->recoveryConflictPending = conflictPending; pid = proc->pid; if (pid != 0) { + (void) pg_atomic_fetch_or_u32(&proc->pendingRecoveryConflicts, (1 << reason)); + /* * Kill the pid if it's still here. If not, that's what we * wanted so ignore any errors. */ - (void) SendProcSignal(pid, sigmode, vxid.procNumber); + (void) SendProcSignal(pid, PROCSIG_RECOVERY_CONFLICT, vxid.procNumber); } break; } @@ -3530,7 +3530,50 @@ SignalVirtualTransaction(VirtualTransactionId vxid, ProcSignalReason sigmode, LWLockRelease(ProcArrayLock); - return pid; + return pid != 0; +} + +/* + * SignalRecoveryConflictWithDatabase -- signal backends using specified database + * + * Like SignalRecoveryConflict, but signals all backends using the database. + */ +void +SignalRecoveryConflictWithDatabase(Oid databaseid, RecoveryConflictReason reason) +{ + ProcArrayStruct *arrayP = procArray; + int index; + + /* tell all backends to die */ + LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); + + for (index = 0; index < arrayP->numProcs; index++) + { + int pgprocno = arrayP->pgprocnos[index]; + PGPROC *proc = &allProcs[pgprocno]; + + if (databaseid == InvalidOid || proc->databaseId == databaseid) + { + VirtualTransactionId procvxid; + pid_t pid; + + GET_VXID_FROM_PGPROC(procvxid, *proc); + + pid = proc->pid; + if (pid != 0) + { + (void) pg_atomic_fetch_or_u32(&proc->pendingRecoveryConflicts, (1 << reason)); + + /* + * Kill the pid if it's still here. If not, that's what we + * wanted so ignore any errors. + */ + (void) SendProcSignal(pid, PROCSIG_RECOVERY_CONFLICT, procvxid.procNumber); + } + } + } + + LWLockRelease(ProcArrayLock); } /* @@ -3640,7 +3683,7 @@ CountDBConnections(Oid databaseid) if (proc->pid == 0) continue; /* do not count prepared xacts */ - if (!proc->isRegularBackend) + if (proc->backendType != B_BACKEND) continue; /* count only regular backend processes */ if (!OidIsValid(databaseid) || proc->databaseId == databaseid) @@ -3652,46 +3695,6 @@ CountDBConnections(Oid databaseid) return count; } -/* - * CancelDBBackends --- cancel backends that are using specified database - */ -void -CancelDBBackends(Oid databaseid, ProcSignalReason sigmode, bool conflictPending) -{ - ProcArrayStruct *arrayP = procArray; - int index; - - /* tell all backends to die */ - LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); - - for (index = 0; index < arrayP->numProcs; index++) - { - int pgprocno = arrayP->pgprocnos[index]; - PGPROC *proc = &allProcs[pgprocno]; - - if (databaseid == InvalidOid || proc->databaseId == databaseid) - { - VirtualTransactionId procvxid; - pid_t pid; - - GET_VXID_FROM_PGPROC(procvxid, *proc); - - proc->recoveryConflictPending = conflictPending; - pid = proc->pid; - if (pid != 0) - { - /* - * Kill the pid if it's still here. If not, that's what we - * wanted so ignore any errors. - */ - (void) SendProcSignal(pid, sigmode, procvxid.procNumber); - } - } - } - - LWLockRelease(ProcArrayLock); -} - /* * CountUserBackends --- count backends that are used by specified user * (only regular backends, not any type of background worker) @@ -3712,7 +3715,7 @@ CountUserBackends(Oid roleid) if (proc->pid == 0) continue; /* do not count prepared xacts */ - if (!proc->isRegularBackend) + if (proc->backendType != B_BACKEND) continue; /* count only regular backend processes */ if (proc->roleId == roleid) count++; @@ -3727,8 +3730,10 @@ CountUserBackends(Oid roleid) * CountOtherDBBackends -- check for other backends running in the given DB * * If there are other backends in the DB, we will wait a maximum of 5 seconds - * for them to exit. Autovacuum backends are encouraged to exit early by - * sending them SIGTERM, but normal user backends are just waited for. + * for them to exit (or 0.3s for testing purposes). Autovacuum backends are + * encouraged to exit early by sending them SIGTERM, but normal user backends + * are just waited for. If background workers connected to this database are + * marked as interruptible, they are terminated. * * The current backend is always ignored; it is caller's responsibility to * check whether the current backend uses the given DB, if it's important. @@ -3753,10 +3758,19 @@ CountOtherDBBackends(Oid databaseId, int *nbackends, int *nprepared) #define MAXAUTOVACPIDS 10 /* max autovacs to SIGTERM per iteration */ int autovac_pids[MAXAUTOVACPIDS]; - int tries; - /* 50 tries with 100ms sleep between tries makes 5 sec total wait */ - for (tries = 0; tries < 50; tries++) + /* + * Retry up to 50 times with 100ms between attempts (max 5s total). Can be + * reduced to 3 attempts (max 0.3s total) to speed up tests. + */ + int ntries = 50; + +#ifdef USE_INJECTION_POINTS + if (IS_INJECTION_POINT_ATTACHED("procarray-reduce-count")) + ntries = 3; +#endif + + for (int tries = 0; tries < ntries; tries++) { int nautovacs = 0; bool found = false; @@ -3806,6 +3820,12 @@ CountOtherDBBackends(Oid databaseId, int *nbackends, int *nprepared) for (index = 0; index < nautovacs; index++) (void) kill(autovac_pids[index], SIGTERM); /* ignore any error */ + /* + * Terminate all background workers for this database, if they have + * requested it (BGWORKER_INTERRUPTIBLE). + */ + TerminateBackgroundWorkersForDatabase(databaseId); + /* sleep, then try again */ pg_usleep(100 * 1000L); /* 100ms */ } @@ -4216,11 +4236,17 @@ GlobalVisUpdate(void) * The state passed needs to have been initialized for the relation fxid is * from (NULL is also OK), otherwise the result may not be correct. * + * If allow_update is false, the GlobalVisState boundaries will not be updated + * even if it would otherwise be beneficial. This is useful for callers that + * do not want GlobalVisState to advance at all, for example because they need + * a conservative answer based on the current boundaries. + * * See comment for GlobalVisState for details. */ bool GlobalVisTestIsRemovableFullXid(GlobalVisState *state, - FullTransactionId fxid) + FullTransactionId fxid, + bool allow_update) { /* * If fxid is older than maybe_needed bound, it definitely is visible to @@ -4241,7 +4267,7 @@ GlobalVisTestIsRemovableFullXid(GlobalVisState *state, * might not exist a snapshot considering fxid running. If it makes sense, * update boundaries and recheck. */ - if (GlobalVisTestShouldUpdate(state)) + if (allow_update && GlobalVisTestShouldUpdate(state)) { GlobalVisUpdate(); @@ -4261,7 +4287,8 @@ GlobalVisTestIsRemovableFullXid(GlobalVisState *state, * relfrozenxid). */ bool -GlobalVisTestIsRemovableXid(GlobalVisState *state, TransactionId xid) +GlobalVisTestIsRemovableXid(GlobalVisState *state, TransactionId xid, + bool allow_update) { FullTransactionId fxid; @@ -4275,7 +4302,33 @@ GlobalVisTestIsRemovableXid(GlobalVisState *state, TransactionId xid) */ fxid = FullXidRelativeTo(state->definitely_needed, xid); - return GlobalVisTestIsRemovableFullXid(state, fxid); + return GlobalVisTestIsRemovableFullXid(state, fxid, allow_update); +} + +/* + * Wrapper around GlobalVisTestIsRemovableXid() for use when examining live + * tuples. Returns true if the given XID may be considered running by at least + * one snapshot. + * + * This function alone is insufficient to determine tuple visibility; callers + * must also consider the XID's commit status. Its purpose is purely semantic: + * when applied to live tuples, GlobalVisTestIsRemovableXid() is checking + * whether the inserting transaction is still considered running, not whether + * the tuple is removable. Live tuples are, by definition, not removable, but + * the snapshot criteria for "transaction still running" are identical to + * those used for removal XIDs. + * + * If allow_update is true, the GlobalVisState boundaries may be updated. If + * it is false, they definitely will not be updated. + * + * See the comment above GlobalVisTestIsRemovable[Full]Xid() for details on + * the required preconditions for calling this function. + */ +bool +GlobalVisTestXidConsideredRunning(GlobalVisState *state, TransactionId xid, + bool allow_update) +{ + return !GlobalVisTestIsRemovableXid(state, xid, allow_update); } /* @@ -4289,7 +4342,7 @@ GlobalVisCheckRemovableFullXid(Relation rel, FullTransactionId fxid) state = GlobalVisTestFor(rel); - return GlobalVisTestIsRemovableFullXid(state, fxid); + return GlobalVisTestIsRemovableFullXid(state, fxid, true); } /* @@ -4303,7 +4356,7 @@ GlobalVisCheckRemovableXid(Relation rel, TransactionId xid) state = GlobalVisTestFor(rel); - return GlobalVisTestIsRemovableXid(state, xid); + return GlobalVisTestIsRemovableXid(state, xid, true); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index a9bb540b55ac2..264e4c22ca6a0 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -4,7 +4,7 @@ * Routines for interprocess signaling * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -19,19 +19,26 @@ #include "access/parallel.h" #include "commands/async.h" +#include "commands/repack.h" #include "miscadmin.h" #include "pgstat.h" #include "port/pg_bitutils.h" +#include "postmaster/datachecksum_state.h" +#include "replication/logicalctl.h" #include "replication/logicalworker.h" +#include "replication/slotsync.h" #include "replication/walsender.h" #include "storage/condition_variable.h" #include "storage/ipc.h" #include "storage/latch.h" +#include "storage/proc.h" #include "storage/shmem.h" #include "storage/sinval.h" #include "storage/smgr.h" +#include "storage/subsystems.h" #include "tcop/tcopprot.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* * The SIGUSR1 signal is multiplexed to support signaling multiple event @@ -102,7 +109,16 @@ struct ProcSignalHeader #define BARRIER_CLEAR_BIT(flags, type) \ ((flags) &= ~(((uint32) 1) << (uint32) (type))) +static void ProcSignalShmemRequest(void *arg); +static void ProcSignalShmemInit(void *arg); + +const ShmemCallbacks ProcSignalShmemCallbacks = { + .request_fn = ProcSignalShmemRequest, + .init_fn = ProcSignalShmemInit, +}; + NON_EXEC_STATIC ProcSignalHeader *ProcSignal = NULL; + static ProcSignalSlot *MyProcSignalSlot = NULL; static bool CheckProcSignal(ProcSignalReason reason); @@ -110,51 +126,39 @@ static void CleanupProcSignalState(int status, Datum arg); static void ResetProcSignalBarrierBits(uint32 flags); /* - * ProcSignalShmemSize - * Compute space needed for ProcSignal's shared memory + * ProcSignalShmemRequest + * Register ProcSignal's shared memory needs at postmaster startup */ -Size -ProcSignalShmemSize(void) +static void +ProcSignalShmemRequest(void *arg) { Size size; size = mul_size(NumProcSignalSlots, sizeof(ProcSignalSlot)); size = add_size(size, offsetof(ProcSignalHeader, psh_slot)); - return size; + + ShmemRequestStruct(.name = "ProcSignal", + .size = size, + .ptr = (void **) &ProcSignal, + ); } -/* - * ProcSignalShmemInit - * Allocate and initialize ProcSignal's shared memory - */ -void -ProcSignalShmemInit(void) +static void +ProcSignalShmemInit(void *arg) { - Size size = ProcSignalShmemSize(); - bool found; + pg_atomic_init_u64(&ProcSignal->psh_barrierGeneration, 0); - ProcSignal = (ProcSignalHeader *) - ShmemInitStruct("ProcSignal", size, &found); - - /* If we're first, initialize. */ - if (!found) + for (int i = 0; i < NumProcSignalSlots; ++i) { - int i; - - pg_atomic_init_u64(&ProcSignal->psh_barrierGeneration, 0); + ProcSignalSlot *slot = &ProcSignal->psh_slot[i]; - for (i = 0; i < NumProcSignalSlots; ++i) - { - ProcSignalSlot *slot = &ProcSignal->psh_slot[i]; - - SpinLockInit(&slot->pss_mutex); - pg_atomic_init_u32(&slot->pss_pid, 0); - slot->pss_cancel_key_len = 0; - MemSet(slot->pss_signalFlags, 0, sizeof(slot->pss_signalFlags)); - pg_atomic_init_u64(&slot->pss_barrierGeneration, PG_UINT64_MAX); - pg_atomic_init_u32(&slot->pss_barrierCheckMask, 0); - ConditionVariableInit(&slot->pss_barrierCV); - } + SpinLockInit(&slot->pss_mutex); + pg_atomic_init_u32(&slot->pss_pid, 0); + slot->pss_cancel_key_len = 0; + MemSet(slot->pss_signalFlags, 0, sizeof(slot->pss_signalFlags)); + pg_atomic_init_u64(&slot->pss_barrierGeneration, PG_UINT64_MAX); + pg_atomic_init_u32(&slot->pss_barrierCheckMask, 0); + ConditionVariableInit(&slot->pss_barrierCV); } } @@ -576,6 +580,16 @@ ProcessProcSignalBarrier(void) case PROCSIGNAL_BARRIER_SMGRRELEASE: processed = ProcessBarrierSmgrRelease(); break; + case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO: + processed = ProcessBarrierUpdateXLogLogicalInfo(); + break; + + case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON: + case PROCSIGNAL_BARRIER_CHECKSUM_ON: + case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF: + case PROCSIGNAL_BARRIER_CHECKSUM_OFF: + processed = AbsorbDataChecksumsBarrier(type); + break; } /* @@ -694,26 +708,14 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_DATABASE)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_DATABASE); - - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_TABLESPACE)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_TABLESPACE); + if (CheckProcSignal(PROCSIG_REPACK_MESSAGE)) + HandleRepackMessageInterrupt(); - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_LOCK)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_LOCK); + if (CheckProcSignal(PROCSIG_SLOTSYNC_MESSAGE)) + HandleSlotSyncMessageInterrupt(); - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_SNAPSHOT)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_SNAPSHOT); - - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT); - - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK); - - if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN)) - HandleRecoveryConflictInterrupt(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN); + if (CheckProcSignal(PROCSIG_RECOVERY_CONFLICT)) + HandleRecoveryConflictInterrupt(); SetLatch(MyLatch); } @@ -728,7 +730,11 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) void SendCancelRequest(int backendPID, const uint8 *cancel_key, int cancel_key_len) { - Assert(backendPID != 0); + if (backendPID == 0) + { + ereport(LOG, (errmsg("invalid cancel request with PID 0"))); + return; + } /* * See if we have a matching backend. Reading the pss_pid and diff --git a/src/backend/storage/ipc/shm_mq.c b/src/backend/storage/ipc/shm_mq.c index 2c79a649f4632..26b24158ed90f 100644 --- a/src/backend/storage/ipc/shm_mq.c +++ b/src/backend/storage/ipc/shm_mq.c @@ -8,7 +8,7 @@ * and only the receiver may receive. This is intended to allow a user * backend to communicate with worker backends that it has registered. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/storage/ipc/shm_mq.c @@ -22,9 +22,11 @@ #include "pgstat.h" #include "port/pg_bitutils.h" #include "postmaster/bgworker.h" +#include "storage/proc.h" #include "storage/shm_mq.h" #include "storage/spin.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* * This structure represents the actual queue, stored in shared memory. @@ -289,7 +291,7 @@ shm_mq_get_sender(shm_mq *mq) shm_mq_handle * shm_mq_attach(shm_mq *mq, dsm_segment *seg, BackgroundWorkerHandle *handle) { - shm_mq_handle *mqh = palloc(sizeof(shm_mq_handle)); + shm_mq_handle *mqh = palloc_object(shm_mq_handle); Assert(mq->mq_receiver == MyProc || mq->mq_sender == MyProc); mqh->mqh_queue = mq; @@ -1041,7 +1043,7 @@ shm_mq_send_bytes(shm_mq_handle *mqh, Size nbytes, const void *data, */ pg_memory_barrier(); memcpy(&mq->mq_ring[mq->mq_ring_offset + offset], - (char *) data + sent, sendnow); + (const char *) data + sent, sendnow); sent += sendnow; /* diff --git a/src/backend/storage/ipc/shm_toc.c b/src/backend/storage/ipc/shm_toc.c index 0ad466e5705b4..2f9fbb0a519dd 100644 --- a/src/backend/storage/ipc/shm_toc.c +++ b/src/backend/storage/ipc/shm_toc.c @@ -3,7 +3,7 @@ * shm_toc.c * shared memory segment table of contents * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/storage/ipc/shm_toc.c @@ -186,6 +186,13 @@ shm_toc_insert(shm_toc *toc, uint64 key, void *address) total_bytes = vtoc->toc_total_bytes; allocated_bytes = vtoc->toc_allocated_bytes; nentry = vtoc->toc_nentry; + +#ifdef USE_ASSERT_CHECKING + /* Verify no duplicate keys */ + for (Size i = 0; i < nentry; i++) + Assert(vtoc->toc_entry[i].key != key); +#endif + toc_bytes = offsetof(shm_toc, toc_entry) + nentry * sizeof(shm_toc_entry) + allocated_bytes; diff --git a/src/backend/storage/ipc/shmem.c b/src/backend/storage/ipc/shmem.c index c9ae3b45b76b1..a4346b9bf9c1d 100644 --- a/src/backend/storage/ipc/shmem.c +++ b/src/backend/storage/ipc/shmem.c @@ -3,7 +3,7 @@ * shmem.c * create shared memory and initialize shared memory data structures. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,126 +19,738 @@ * methods). The routines in this file are used for allocating and * binding to shared memory data structures. * - * NOTES: - * (a) There are three kinds of shared memory data structures - * available to POSTGRES: fixed-size structures, queues and hash - * tables. Fixed-size structures contain things like global variables - * for a module and should never be allocated after the shared memory - * initialization phase. Hash tables have a fixed maximum size, but - * their actual size can vary dynamically. When entries are added - * to the table, more space is allocated. Queues link data structures - * that have been allocated either within fixed-size structures or as hash - * buckets. Each shared data structure has a string name to identify - * it (assigned in the module that declares it). - * - * (b) During initialization, each module looks for its - * shared data structures in a hash table called the "Shmem Index". - * If the data structure is not present, the caller can allocate - * a new one and initialize it. If the data structure is present, - * the caller "attaches" to the structure by initializing a pointer - * in the local address space. - * The shmem index has two purposes: first, it gives us - * a simple model of how the world looks when a backend process - * initializes. If something is present in the shmem index, - * it is initialized. If it is not, it is uninitialized. Second, - * the shmem index allows us to allocate shared memory on demand - * instead of trying to preallocate structures and hard-wire the - * sizes and locations in header files. If you are using a lot - * of shared memory in a lot of different places (and changing - * things during development), this is important. - * - * (c) In standard Unix-ish environments, individual backends do not - * need to re-establish their local pointers into shared memory, because - * they inherit correct values of those variables via fork() from the - * postmaster. However, this does not work in the EXEC_BACKEND case. - * In ports using EXEC_BACKEND, new backends have to set up their local - * pointers using the method described in (b) above. - * - * (d) memory allocation model: shared memory can never be - * freed, once allocated. Each hash table has its own free list, - * so hash buckets can be reused when an item is deleted. However, - * if one hash table grows very large and then shrinks, its space - * cannot be redistributed to other tables. We could build a simple - * hash bucket garbage collector if need be. Right now, it seems - * unnecessary. + * This module provides facilities to allocate fixed-size structures in shared + * memory, for things like variables shared between all backend processes. + * Each such structure has a string name to identify it, specified when it is + * requested. shmem_hash.c provides a shared hash table implementation on top + * of that. + * + * Shared memory areas should usually not be allocated after postmaster + * startup, although we do allow small allocations later for the benefit of + * extension modules that are loaded after startup. Despite that allowance, + * extensions that need shared memory should be added in + * shared_preload_libraries, because the allowance is quite small and there is + * no guarantee that any memory is available after startup. + * + * Nowadays, there is also another way to allocate shared memory called + * Dynamic Shared Memory. See dsm.c for that facility. One big difference + * between traditional shared memory handled by shmem.c and dynamic shared + * memory is that traditional shared memory areas are mapped to the same + * address in all processes, so you can use normal pointers in shared memory + * structs. With Dynamic Shared Memory, you must use offsets or DSA pointers + * instead. + * + * Shared memory managed by shmem.c can never be freed, once allocated. Each + * hash table has its own free list, so hash buckets can be reused when an + * item is deleted. + * + * Usage + * ----- + * + * To allocate shared memory, you need to register a set of callback functions + * which handle the lifecycle of the allocation. In the request_fn callback, + * call ShmemRequestStruct() with the desired name and size. When the area is + * later allocated or attached to, the global variable pointed to by the .ptr + * option is set to the shared memory location of the allocation. The init_fn + * callback can perform additional initialization. + * + * typedef struct MyShmemData { + * ... + * } MyShmemData; + * + * static MyShmemData *MyShmem; + * + * static void my_shmem_request(void *arg); + * static void my_shmem_init(void *arg); + * + * const ShmemCallbacks MyShmemCallbacks = { + * .request_fn = my_shmem_request, + * .init_fn = my_shmem_init, + * }; + * + * static void + * my_shmem_request(void *arg) + * { + * ShmemRequestStruct(.name = "My shmem area", + * .size = sizeof(MyShmemData), + * .ptr = (void **) &MyShmem, + * ); + * } + * + * In builtin PostgreSQL code, add the callbacks to the list in + * src/include/storage/subsystemlist.h. In an add-in module, you can register + * the callbacks by calling RegisterShmemCallbacks(&MyShmemCallbacks) in the + * extension's _PG_init() function. + * + * Lifecycle + * --------- + * + * Initializing shared memory happens in multiple phases. In the first phase, + * during postmaster startup, all the request_fn callbacks are called. Only + * after all the request_fn callbacks have been called and all the shmem areas + * have been requested by the ShmemRequestStruct() calls we know how much + * shared memory we need in total. After that, postmaster allocates global + * shared memory segment, and calls all the init_fn callbacks to initialize + * all the requested shmem areas. + * + * In standard Unix-ish environments, individual backends do not need to + * re-establish their local pointers into shared memory, because they inherit + * correct values of those variables via fork() from the postmaster. However, + * this does not work in the EXEC_BACKEND case. In ports using EXEC_BACKEND, + * backend startup also calls the shmem_request callbacks to re-establish the + * knowledge about each shared memory area, sets the pointer variables + * (*options->ptr), and calls the attach_fn callback, if any, for additional + * per-backend setup. + * + * Legacy ShmemInitStruct()/ShmemInitHash() functions + * -------------------------------------------------- + * + * ShmemInitStruct()/ShmemInitHash() is another way of registering shmem + * areas. It pre-dates the ShmemRequestStruct()/ShmemRequestHash() functions, + * and should not be used in new code, but as of this writing it is still + * widely used in extensions. + * + * To allocate a shmem area with ShmemInitStruct(), you need to separately + * register the size needed for the area by calling RequestAddinShmemSpace() + * from the extension's shmem_request_hook, and allocate the area by calling + * ShmemInitStruct() from the extension's shmem_startup_hook. There are no + * init/attach callbacks. Instead, the caller of ShmemInitStruct() must check + * the return status of ShmemInitStruct() and initialize the struct if it was + * not previously initialized. + * + * Calling ShmemAlloc() directly + * ----------------------------- + * + * There's a more low-level way of allocating shared memory too: you can call + * ShmemAlloc() directly. It's used to implement the higher level mechanisms, + * and should generally not be called directly. */ #include "postgres.h" +#include + +#include "access/slru.h" +#include "common/int.h" #include "fmgr.h" #include "funcapi.h" #include "miscadmin.h" +#include "port/pg_bitutils.h" #include "port/pg_numa.h" #include "storage/lwlock.h" #include "storage/pg_shmem.h" #include "storage/shmem.h" +#include "storage/shmem_internal.h" #include "storage/spin.h" #include "utils/builtins.h" +#include "utils/tuplestore.h" + +/* + * Registered callbacks. + * + * During postmaster startup, we accumulate the callbacks from all subsystems + * in this list. + * + * This is in process private memory, although on Unix-like systems, we expect + * all the registrations to happen at postmaster startup time and be inherited + * by all the child processes via fork(). + */ +static List *registered_shmem_callbacks; + +/* + * In the shmem request phase, all the shmem areas requested with the + * ShmemRequest*() functions are accumulated here. + */ +typedef struct +{ + ShmemStructOpts *options; + ShmemRequestKind kind; +} ShmemRequest; + +static List *pending_shmem_requests; + +/* + * Per-process state machine, for sanity checking that we do things in the + * right order. + * + * Postmaster: + * INITIAL -> REQUESTING -> INITIALIZING -> DONE + * + * Backends in EXEC_BACKEND mode: + * INITIAL -> REQUESTING -> ATTACHING -> DONE + * + * Late request: + * DONE -> REQUESTING -> AFTER_STARTUP_ATTACH_OR_INIT -> DONE + */ +enum shmem_request_state +{ + /* Initial state */ + SRS_INITIAL, + + /* + * When we start calling the shmem_request callbacks, we enter the + * SRS_REQUESTING phase. All ShmemRequestStruct calls happen in this + * state. + */ + SRS_REQUESTING, + + /* + * Postmaster has finished all shmem requests, and is now initializing the + * shared memory segment. init_fn callbacks are called in this state. + */ + SRS_INITIALIZING, + + /* + * A postmaster child process is starting up. attach_fn callbacks are + * called in this state. + */ + SRS_ATTACHING, + + /* An after-startup allocation or attachment is in progress */ + SRS_AFTER_STARTUP_ATTACH_OR_INIT, -static void *ShmemAllocRaw(Size size, Size *allocated_size); + /* Normal state after shmem initialization / attachment */ + SRS_DONE, +}; +static enum shmem_request_state shmem_request_state = SRS_INITIAL; + +/* + * This is the first data structure stored in the shared memory segment, at + * the offset that PGShmemHeader->content_offset points to. Allocations by + * ShmemAlloc() are carved out of the space after this. + * + * For the base pointer and the total size of the shmem segment, we rely on + * the PGShmemHeader. + */ +typedef struct ShmemAllocatorData +{ + Size free_offset; /* offset to first free space from ShmemBase */ + + /* protects 'free_offset' */ + slock_t shmem_lock; + + HASHHDR *index; /* location of ShmemIndex */ + size_t index_size; /* size of shmem region holding ShmemIndex */ + LWLock index_lock; /* protects ShmemIndex */ +} ShmemAllocatorData; + +#define ShmemIndexLock (&ShmemAllocator->index_lock) + +static void *ShmemAllocRaw(Size size, Size alignment, Size *allocated_size); /* shared memory global variables */ static PGShmemHeader *ShmemSegHdr; /* shared mem segment header */ - static void *ShmemBase; /* start address of shared memory */ - static void *ShmemEnd; /* end+1 address of shared memory */ -slock_t *ShmemLock; /* spinlock for shared memory and LWLock - * allocation */ +static ShmemAllocatorData *ShmemAllocator; + +/* + * ShmemIndex is a global directory of shmem areas, itself also stored in the + * shared memory. + */ +static HTAB *ShmemIndex; + + /* max size of data structure string name */ +#define SHMEM_INDEX_KEYSIZE (48) + +/* + * # of additional entries to reserve in the shmem index table, for + * allocations after postmaster startup. (This is not a hard limit, the hash + * table can grow larger than that if there is shared memory available) + */ +#define SHMEM_INDEX_ADDITIONAL_SIZE (128) -static HTAB *ShmemIndex = NULL; /* primary index hashtable for shmem */ +/* this is a hash bucket in the shmem index table */ +typedef struct +{ + char key[SHMEM_INDEX_KEYSIZE]; /* string name */ + void *location; /* location in shared mem */ + Size size; /* # bytes requested for the structure */ + Size allocated_size; /* # bytes actually allocated */ +} ShmemIndexEnt; /* To get reliable results for NUMA inquiry we need to "touch pages" once */ static bool firstNumaTouch = true; +static void CallShmemCallbacksAfterStartup(const ShmemCallbacks *callbacks); +static void InitShmemIndexEntry(ShmemRequest *request); +static bool AttachShmemIndexEntry(ShmemRequest *request, bool missing_ok); + Datum pg_numa_available(PG_FUNCTION_ARGS); /* - * InitShmemAccess() --- set up basic pointers to shared memory. + * ShmemRequestStruct() --- request a named shared memory area + * + * Subsystems call this to register their shared memory needs. This is + * usually done early in postmaster startup, before the shared memory segment + * has been created, so that the size can be included in the estimate for + * total amount of shared memory needed. We set aside a small amount of + * memory for allocations that happen later, for the benefit of non-preloaded + * extensions, but that should not be relied upon. + * + * This does not yet allocate the memory, but merely registers the need for + * it. The actual allocation happens later in the postmaster startup + * sequence. + * + * This must be called from a shmem_request callback function, registered with + * RegisterShmemCallbacks(). This enforces a coding pattern that works the + * same in normal Unix systems and with EXEC_BACKEND. On Unix systems, the + * shmem_request callbacks are called once, early in postmaster startup, and + * the child processes inherit the struct descriptors and any other + * per-process state from the postmaster. In EXEC_BACKEND mode, shmem_request + * callbacks are *also* called in each backend, at backend startup, to + * re-establish the struct descriptors. By calling the same function in both + * cases, we ensure that all the shmem areas are registered the same way in + * all processes. + * + * 'options' defines the name and size of the area, and any other optional + * features. Leave unused options as zeros. The options are copied to + * longer-lived memory, so it doesn't need to live after the + * ShmemRequestStruct() call and can point to a local variable in the calling + * function. The 'name' must point to a long-lived string though, only the + * pointer to it is copied. */ void -InitShmemAccess(PGShmemHeader *seghdr) +ShmemRequestStructWithOpts(const ShmemStructOpts *options) { - ShmemSegHdr = seghdr; - ShmemBase = seghdr; - ShmemEnd = (char *) ShmemBase + seghdr->totalsize; + ShmemStructOpts *options_copy; + + options_copy = MemoryContextAlloc(TopMemoryContext, + sizeof(ShmemStructOpts)); + memcpy(options_copy, options, sizeof(ShmemStructOpts)); + + ShmemRequestInternal(options_copy, SHMEM_KIND_STRUCT); } /* - * InitShmemAllocation() --- set up shared-memory space allocation. + * Internal workhorse of ShmemRequestStruct() and ShmemRequestHash(). * - * This should be called only in the postmaster or a standalone backend. + * Note: Unlike in the public ShmemRequestStruct() and ShmemRequestHash() + * functions, 'options' is *not* copied. It must be allocated in + * TopMemoryContext by the caller, and will be freed after the init/attach + * callbacks have been called. This allows ShmemRequestHash() to pass a + * pointer to the extended ShmemHashOpts struct instead. */ void -InitShmemAllocation(void) +ShmemRequestInternal(ShmemStructOpts *options, ShmemRequestKind kind) { - PGShmemHeader *shmhdr = ShmemSegHdr; - char *aligned; + ShmemRequest *request; + + /* Check the options */ + if (options->name == NULL) + elog(ERROR, "shared memory request is missing 'name' option"); + + if (IsUnderPostmaster) + { + if (options->size <= 0 && options->size != SHMEM_ATTACH_UNKNOWN_SIZE) + elog(ERROR, "invalid size %zd for shared memory request for \"%s\"", + options->size, options->name); + } + else + { + if (options->size == SHMEM_ATTACH_UNKNOWN_SIZE) + elog(ERROR, "SHMEM_ATTACH_UNKNOWN_SIZE cannot be used during startup"); + if (options->size <= 0) + elog(ERROR, "invalid size %zd for shared memory request for \"%s\"", + options->size, options->name); + } - Assert(shmhdr != NULL); + if (options->alignment != 0 && pg_nextpower2_size_t(options->alignment) != options->alignment) + elog(ERROR, "invalid alignment %zu for shared memory request for \"%s\"", + options->alignment, options->name); + + /* Check that we're in the right state */ + if (shmem_request_state != SRS_REQUESTING) + elog(ERROR, "ShmemRequestStruct can only be called from a shmem_request callback"); + + /* Check that it's not already registered in this process */ + foreach_ptr(ShmemRequest, existing, pending_shmem_requests) + { + if (strcmp(existing->options->name, options->name) == 0) + ereport(ERROR, + (errmsg("shared memory struct \"%s\" is already registered", + options->name))); + } + + /* Request looks valid, remember it */ + request = palloc(sizeof(ShmemRequest)); + request->options = options; + request->kind = kind; + pending_shmem_requests = lappend(pending_shmem_requests, request); +} + +/* + * ShmemGetRequestedSize() --- estimate the total size of all registered shared + * memory structures. + * + * This is called at postmaster startup, before the shared memory segment has + * been created. + */ +size_t +ShmemGetRequestedSize(void) +{ + size_t size; + + /* memory needed for the ShmemIndex */ + size = hash_estimate_size(list_length(pending_shmem_requests) + SHMEM_INDEX_ADDITIONAL_SIZE, + sizeof(ShmemIndexEnt)); + size = CACHELINEALIGN(size); + + /* memory needed for all the requested areas */ + foreach_ptr(ShmemRequest, request, pending_shmem_requests) + { + size_t alignment = request->options->alignment; + + /* pad the start address for alignment like ShmemAllocRaw() does */ + if (alignment < PG_CACHE_LINE_SIZE) + alignment = PG_CACHE_LINE_SIZE; + size = TYPEALIGN(alignment, size); + + size = add_size(size, request->options->size); + } + + return size; +} + +/* + * ShmemInitRequested() --- allocate and initialize requested shared memory + * structures. + * + * This is called once at postmaster startup, after the shared memory segment + * has been created. + */ +void +ShmemInitRequested(void) +{ + /* should be called only by the postmaster or a standalone backend */ + Assert(!IsUnderPostmaster); + Assert(shmem_request_state == SRS_INITIALIZING); + + /* + * Initialize the ShmemIndex entries and perform basic initialization of + * all the requested memory areas. There are no concurrent processes yet, + * so no need for locking. + */ + foreach_ptr(ShmemRequest, request, pending_shmem_requests) + { + InitShmemIndexEntry(request); + pfree(request->options); + } + list_free_deep(pending_shmem_requests); + pending_shmem_requests = NIL; /* - * Initialize the spinlock used by ShmemAlloc. We must use - * ShmemAllocUnlocked, since obviously ShmemAlloc can't be called yet. + * Call the subsystem-specific init callbacks to finish initialization of + * all the areas. */ - ShmemLock = (slock_t *) ShmemAllocUnlocked(sizeof(slock_t)); + foreach_ptr(const ShmemCallbacks, callbacks, registered_shmem_callbacks) + { + if (callbacks->init_fn) + callbacks->init_fn(callbacks->opaque_arg); + } - SpinLockInit(ShmemLock); + shmem_request_state = SRS_DONE; +} + +/* + * Re-establish process private state related to shmem areas. + * + * This is called at backend startup in EXEC_BACKEND mode, in every backend. + */ +#ifdef EXEC_BACKEND +void +ShmemAttachRequested(void) +{ + ListCell *lc; + + /* Must be initializing a (non-standalone) backend */ + Assert(IsUnderPostmaster); + Assert(ShmemAllocator->index != NULL); + Assert(shmem_request_state == SRS_REQUESTING); + shmem_request_state = SRS_ATTACHING; + + LWLockAcquire(ShmemIndexLock, LW_SHARED); + + /* + * Attach to all the requested memory areas. + */ + foreach_ptr(ShmemRequest, request, pending_shmem_requests) + { + AttachShmemIndexEntry(request, false); + pfree(request->options); + } + list_free_deep(pending_shmem_requests); + pending_shmem_requests = NIL; + + /* Call attach callbacks */ + foreach(lc, registered_shmem_callbacks) + { + const ShmemCallbacks *callbacks = (const ShmemCallbacks *) lfirst(lc); + + if (callbacks->attach_fn) + callbacks->attach_fn(callbacks->opaque_arg); + } + + LWLockRelease(ShmemIndexLock); + + shmem_request_state = SRS_DONE; +} +#endif + +/* + * Insert requested shmem area into the shared memory index and initialize it. + * + * Note that this only does performs basic initialization depending on + * ShmemRequestKind, like setting the global pointer variable to the area for + * SHMEM_KIND_STRUCT or setting up the backend-private HTAB control struct. + * This does *not* call the subsystem-specific init callbacks. That's done + * later after all the shmem areas have been initialized or attached to. + */ +static void +InitShmemIndexEntry(ShmemRequest *request) +{ + const char *name = request->options->name; + ShmemIndexEnt *index_entry; + bool found; + size_t allocated_size; + void *structPtr; + + /* look it up in the shmem index */ + index_entry = (ShmemIndexEnt *) + hash_search(ShmemIndex, name, HASH_ENTER_NULL, &found); + if (found) + elog(ERROR, "shared memory struct \"%s\" is already initialized", name); + if (!index_entry) + { + /* tried to add it to the hash table, but there was no space */ + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("could not create ShmemIndex entry for data structure \"%s\"", + name))); + } + + /* + * We inserted the entry to the shared memory index. Allocate requested + * amount of shared memory for it, and initialize the index entry. + */ + structPtr = ShmemAllocRaw(request->options->size, + request->options->alignment, + &allocated_size); + if (structPtr == NULL) + { + /* out of memory; remove the failed ShmemIndex entry */ + hash_search(ShmemIndex, name, HASH_REMOVE, NULL); + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("not enough shared memory for data structure" + " \"%s\" (%zd bytes requested)", + name, request->options->size))); + } + index_entry->size = request->options->size; + index_entry->allocated_size = allocated_size; + index_entry->location = structPtr; + + /* Initialize depending on the kind of shmem area it is */ + switch (request->kind) + { + case SHMEM_KIND_STRUCT: + if (request->options->ptr) + *(request->options->ptr) = index_entry->location; + break; + case SHMEM_KIND_HASH: + shmem_hash_init(structPtr, request->options); + break; + case SHMEM_KIND_SLRU: + shmem_slru_init(structPtr, request->options); + break; + } +} + +/* + * Look up a named shmem area in the shared memory index and attach to it. + * + * Note that this only performs the basic attachment actions depending on + * ShmemRequestKind, like setting the global pointer variable to the area for + * SHMEM_KIND_STRUCT or setting up the backend-private HTAB control struct. + * This does *not* call the subsystem-specific attach callbacks. That's done + * later after all the shmem areas have been initialized or attached to. + */ +static bool +AttachShmemIndexEntry(ShmemRequest *request, bool missing_ok) +{ + const char *name = request->options->name; + ShmemIndexEnt *index_entry; + + /* Look it up in the shmem index */ + index_entry = (ShmemIndexEnt *) + hash_search(ShmemIndex, name, HASH_FIND, NULL); + if (!index_entry) + { + if (!missing_ok) + ereport(ERROR, + (errmsg("could not find ShmemIndex entry for data structure \"%s\"", + request->options->name))); + return false; + } + + /* Check that the size in the index matches the request */ + if (index_entry->size != request->options->size && + request->options->size != SHMEM_ATTACH_UNKNOWN_SIZE) + { + ereport(ERROR, + (errmsg("shared memory struct \"%s\" was created with" + " different size: existing %zu, requested %zd", + name, index_entry->size, request->options->size))); + } + + /* + * Re-establish the caller's pointer variable, or do other actions to + * attach depending on the kind of shmem area it is. + */ + switch (request->kind) + { + case SHMEM_KIND_STRUCT: + if (request->options->ptr) + *(request->options->ptr) = index_entry->location; + break; + case SHMEM_KIND_HASH: + shmem_hash_attach(index_entry->location, request->options); + break; + case SHMEM_KIND_SLRU: + shmem_slru_attach(index_entry->location, request->options); + break; + } + + return true; +} + +/* + * InitShmemAllocator() --- set up basic pointers to shared memory. + * + * Called at postmaster or stand-alone backend startup, to initialize the + * allocator's data structure in the shared memory segment. In EXEC_BACKEND, + * this is also called at backend startup, to set up pointers to the + * already-initialized data structure. + */ +void +InitShmemAllocator(PGShmemHeader *seghdr) +{ + Size offset; + int64 hash_nelems; + HASHCTL info; + int hash_flags; + +#ifndef EXEC_BACKEND + Assert(!IsUnderPostmaster); +#endif + Assert(seghdr != NULL); + + if (IsUnderPostmaster) + { + Assert(shmem_request_state == SRS_INITIAL); + } + else + { + Assert(shmem_request_state == SRS_REQUESTING); + shmem_request_state = SRS_INITIALIZING; + } + + /* + * We assume the pointer and offset are MAXALIGN. Not a hard requirement, + * but it's true today and keeps the math below simpler. + */ + Assert(seghdr == (void *) MAXALIGN(seghdr)); + Assert(seghdr->content_offset == MAXALIGN(seghdr->content_offset)); /* * Allocations after this point should go through ShmemAlloc, which * expects to allocate everything on cache line boundaries. Make sure the * first allocation begins on a cache line boundary. */ - aligned = (char *) - (CACHELINEALIGN((((char *) shmhdr) + shmhdr->freeoffset))); - shmhdr->freeoffset = aligned - (char *) shmhdr; + offset = CACHELINEALIGN(seghdr->content_offset + sizeof(ShmemAllocatorData)); + if (offset > seghdr->totalsize) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of shared memory (%zu bytes requested)", + offset))); - /* ShmemIndex can't be set up yet (need LWLocks first) */ - shmhdr->index = NULL; - ShmemIndex = (HTAB *) NULL; + /* + * In postmaster or stand-alone backend, initialize the shared memory + * allocator so that we can allocate shared memory for ShmemIndex using + * ShmemAlloc(). In a regular backend just set up the pointers required + * by ShmemAlloc(). + */ + ShmemAllocator = (ShmemAllocatorData *) ((char *) seghdr + seghdr->content_offset); + if (!IsUnderPostmaster) + { + SpinLockInit(&ShmemAllocator->shmem_lock); + ShmemAllocator->free_offset = offset; + LWLockInitialize(&ShmemAllocator->index_lock, LWTRANCHE_SHMEM_INDEX); + } + + ShmemSegHdr = seghdr; + ShmemBase = seghdr; + ShmemEnd = (char *) ShmemBase + seghdr->totalsize; + + /* + * Create (or attach to) the shared memory index of shmem areas. + * + * This is the same initialization as ShmemInitHash() does, but we cannot + * use ShmemInitHash() here because it relies on ShmemIndex being already + * initialized. + */ + hash_nelems = list_length(pending_shmem_requests) + SHMEM_INDEX_ADDITIONAL_SIZE; + + info.keysize = SHMEM_INDEX_KEYSIZE; + info.entrysize = sizeof(ShmemIndexEnt); + hash_flags = HASH_ELEM | HASH_STRINGS | HASH_FIXED_SIZE; + + if (!IsUnderPostmaster) + { + ShmemAllocator->index_size = hash_estimate_size(hash_nelems, info.entrysize); + ShmemAllocator->index = (HASHHDR *) ShmemAlloc(ShmemAllocator->index_size); + } + ShmemIndex = shmem_hash_create(ShmemAllocator->index, + ShmemAllocator->index_size, + IsUnderPostmaster, + "ShmemIndex", hash_nelems, + &info, hash_flags); + Assert(ShmemIndex != NULL); + + /* + * Add an entry for ShmemIndex itself into ShmemIndex, so that it's + * visible in the pg_shmem_allocations view + */ + if (!IsUnderPostmaster) + { + bool found; + ShmemIndexEnt *result = (ShmemIndexEnt *) + hash_search(ShmemIndex, "ShmemIndex", HASH_ENTER, &found); + + Assert(!found); + result->size = ShmemAllocator->index_size; + result->allocated_size = ShmemAllocator->index_size; + result->location = ShmemAllocator->index; + } +} + +/* + * Reset state on postmaster crash restart. + */ +void +ResetShmemAllocator(void) +{ + Assert(!IsUnderPostmaster); + shmem_request_state = SRS_INITIAL; + + pending_shmem_requests = NIL; + + /* + * Note that we don't clear the registered callbacks. We will need to + * call them again as we restart + */ } /* @@ -146,7 +758,7 @@ InitShmemAllocation(void) * * Throws error if request cannot be satisfied. * - * Assumes ShmemLock and ShmemSegHdr are initialized. + * Assumes ShmemSegHdr is initialized. */ void * ShmemAlloc(Size size) @@ -154,7 +766,7 @@ ShmemAlloc(Size size) void *newSpace; Size allocated_size; - newSpace = ShmemAllocRaw(size, &allocated_size); + newSpace = ShmemAllocRaw(size, 0, &allocated_size); if (!newSpace) ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), @@ -173,7 +785,7 @@ ShmemAllocNoError(Size size) { Size allocated_size; - return ShmemAllocRaw(size, &allocated_size); + return ShmemAllocRaw(size, 0, &allocated_size); } /* @@ -183,8 +795,9 @@ ShmemAllocNoError(Size size) * be equal to the number requested plus any padding we choose to add. */ static void * -ShmemAllocRaw(Size size, Size *allocated_size) +ShmemAllocRaw(Size size, Size alignment, Size *allocated_size) { + Size rawStart; Size newStart; Size newFree; void *newSpace; @@ -200,68 +813,31 @@ ShmemAllocRaw(Size size, Size *allocated_size) * structures out to a power-of-two size - but without this, even that * won't be sufficient. */ - size = CACHELINEALIGN(size); - *allocated_size = size; + if (alignment < PG_CACHE_LINE_SIZE) + alignment = PG_CACHE_LINE_SIZE; Assert(ShmemSegHdr != NULL); - SpinLockAcquire(ShmemLock); + SpinLockAcquire(&ShmemAllocator->shmem_lock); - newStart = ShmemSegHdr->freeoffset; + rawStart = ShmemAllocator->free_offset; + newStart = TYPEALIGN(alignment, rawStart); newFree = newStart + size; if (newFree <= ShmemSegHdr->totalsize) { newSpace = (char *) ShmemBase + newStart; - ShmemSegHdr->freeoffset = newFree; + ShmemAllocator->free_offset = newFree; } else newSpace = NULL; - SpinLockRelease(ShmemLock); + SpinLockRelease(&ShmemAllocator->shmem_lock); /* note this assert is okay with newSpace == NULL */ - Assert(newSpace == (void *) CACHELINEALIGN(newSpace)); - - return newSpace; -} - -/* - * ShmemAllocUnlocked -- allocate max-aligned chunk from shared memory - * - * Allocate space without locking ShmemLock. This should be used for, - * and only for, allocations that must happen before ShmemLock is ready. - * - * We consider maxalign, rather than cachealign, sufficient here. - */ -void * -ShmemAllocUnlocked(Size size) -{ - Size newStart; - Size newFree; - void *newSpace; - - /* - * Ensure allocated space is adequately aligned. - */ - size = MAXALIGN(size); - - Assert(ShmemSegHdr != NULL); - - newStart = ShmemSegHdr->freeoffset; - - newFree = newStart + size; - if (newFree > ShmemSegHdr->totalsize) - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("out of shared memory (%zu bytes requested)", - size))); - ShmemSegHdr->freeoffset = newFree; - - newSpace = (char *) ShmemBase + newStart; - - Assert(newSpace == (void *) MAXALIGN(newSpace)); + Assert(newSpace == (void *) TYPEALIGN(alignment, newSpace)); + *allocated_size = newFree - rawStart; return newSpace; } @@ -277,94 +853,143 @@ ShmemAddrIsValid(const void *addr) } /* - * InitShmemIndex() --- set up or attach to shmem index table. + * Register callbacks that define a shared memory area (or multiple areas). + * + * The system will call the callbacks at different stages of postmaster or + * backend startup, to allocate and initialize the area. + * + * This is normally called early during postmaster startup, but if the + * SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP is set, this can also be used after + * startup, although after startup there's no guarantee that there's enough + * shared memory available. When called after startup, this immediately calls + * the right callbacks depending on whether another backend had already + * initialized the area. + * + * Note: In EXEC_BACKEND mode, this needs to be called in every backend + * process. That's needed because we cannot pass down the callback function + * pointers from the postmaster process, because different processes may have + * loaded libraries to different addresses. */ void -InitShmemIndex(void) +RegisterShmemCallbacks(const ShmemCallbacks *callbacks) { - HASHCTL info; - - /* - * Create the shared memory shmem index. - * - * Since ShmemInitHash calls ShmemInitStruct, which expects the ShmemIndex - * hashtable to exist already, we have a bit of a circularity problem in - * initializing the ShmemIndex itself. The special "ShmemIndex" hash - * table name will tell ShmemInitStruct to fake it. - */ - info.keysize = SHMEM_INDEX_KEYSIZE; - info.entrysize = sizeof(ShmemIndexEnt); + if (shmem_request_state == SRS_DONE && IsUnderPostmaster) + { + /* + * After-startup initialization or attachment. Call the appropriate + * callbacks immediately. + */ + if ((callbacks->flags & SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP) == 0) + elog(ERROR, "cannot request shared memory at this time"); - ShmemIndex = ShmemInitHash("ShmemIndex", - SHMEM_INDEX_SIZE, SHMEM_INDEX_SIZE, - &info, - HASH_ELEM | HASH_STRINGS); + CallShmemCallbacksAfterStartup(callbacks); + } + else + { + /* Remember the callbacks for later */ + registered_shmem_callbacks = lappend(registered_shmem_callbacks, + (void *) callbacks); + } } /* - * ShmemInitHash -- Create and initialize, or attach to, a - * shared memory hash table. - * - * We assume caller is doing some kind of synchronization - * so that two processes don't try to create/initialize the same - * table at once. (In practice, all creations are done in the postmaster - * process; child processes should always be attaching to existing tables.) - * - * max_size is the estimated maximum number of hashtable entries. This is - * not a hard limit, but the access efficiency will degrade if it is - * exceeded substantially (since it's used to compute directory size and - * the hash table buckets will get overfull). - * - * init_size is the number of hashtable entries to preallocate. For a table - * whose maximum size is certain, this should be equal to max_size; that - * ensures that no run-time out-of-shared-memory failures can occur. - * - * *infoP and hash_flags must specify at least the entry sizes and key - * comparison semantics (see hash_create()). Flag bits and values specific - * to shared-memory hash tables are added here, except that callers may - * choose to specify HASH_PARTITION and/or HASH_FIXED_SIZE. - * - * Note: before Postgres 9.0, this function returned NULL for some failure - * cases. Now, it always throws error instead, so callers need not check - * for NULL. + * Register a shmem area (or multiple areas) after startup. */ -HTAB * -ShmemInitHash(const char *name, /* table string name for shmem index */ - long init_size, /* initial table size */ - long max_size, /* max size of the table */ - HASHCTL *infoP, /* info about key and bucket size */ - int hash_flags) /* info about infoP */ +static void +CallShmemCallbacksAfterStartup(const ShmemCallbacks *callbacks) { - bool found; - void *location; + bool found_any; + bool notfound_any; + + Assert(shmem_request_state == SRS_DONE); + shmem_request_state = SRS_REQUESTING; /* - * Hash tables allocated in shared memory have a fixed directory; it can't - * grow or other backends wouldn't be able to find it. So, make sure we - * make it big enough to start with. - * - * The shared memory allocator must be specified too. + * Call the request callback first. The callback makes ShmemRequest*() + * calls for each shmem area, adding them to pending_shmem_requests. */ - infoP->dsize = infoP->max_dsize = hash_select_dirsize(max_size); - infoP->alloc = ShmemAllocNoError; - hash_flags |= HASH_SHARED_MEM | HASH_ALLOC | HASH_DIRSIZE; + Assert(pending_shmem_requests == NIL); + if (callbacks->request_fn) + callbacks->request_fn(callbacks->opaque_arg); + shmem_request_state = SRS_AFTER_STARTUP_ATTACH_OR_INIT; - /* look it up in the shmem index */ - location = ShmemInitStruct(name, - hash_get_shared_size(infoP, hash_flags), - &found); + if (pending_shmem_requests == NIL) + { + shmem_request_state = SRS_DONE; + return; + } + + /* Hold ShmemIndexLock while we allocate all the shmem entries */ + LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE); /* - * if it already exists, attach to it rather than allocate and initialize - * new space + * Check if the requested shared memory areas have already been + * initialized. We assume all the areas requested by the request callback + * to form a coherent unit such that they're all already initialized or + * none. Otherwise it would be ambiguous which callback, init or attach, + * to callback afterwards. */ - if (found) - hash_flags |= HASH_ATTACH; + found_any = notfound_any = false; + foreach_ptr(ShmemRequest, request, pending_shmem_requests) + { + if (hash_search(ShmemIndex, request->options->name, HASH_FIND, NULL)) + found_any = true; + else + notfound_any = true; + } + if (found_any && notfound_any) + elog(ERROR, "found some but not all"); - /* Pass location of hashtable header to hash_create */ - infoP->hctl = (HASHHDR *) location; + /* + * Allocate or attach all the shmem areas requested by the request_fn + * callback. + */ + foreach_ptr(ShmemRequest, request, pending_shmem_requests) + { + if (found_any) + AttachShmemIndexEntry(request, false); + else + InitShmemIndexEntry(request); - return hash_create(name, init_size, infoP, hash_flags); + pfree(request->options); + } + list_free_deep(pending_shmem_requests); + pending_shmem_requests = NIL; + + /* Finish by calling the appropriate subsystem-specific callback */ + if (found_any) + { + if (callbacks->attach_fn) + callbacks->attach_fn(callbacks->opaque_arg); + } + else + { + if (callbacks->init_fn) + callbacks->init_fn(callbacks->opaque_arg); + } + + LWLockRelease(ShmemIndexLock); + shmem_request_state = SRS_DONE; +} + +/* + * Call all shmem request callbacks. + */ +void +ShmemCallRequestCallbacks(void) +{ + ListCell *lc; + + Assert(shmem_request_state == SRS_INITIAL); + shmem_request_state = SRS_REQUESTING; + + foreach(lc, registered_shmem_callbacks) + { + const ShmemCallbacks *callbacks = (const ShmemCallbacks *) lfirst(lc); + + if (callbacks->request_fn) + callbacks->request_fn(callbacks->opaque_arg); + } } /* @@ -379,113 +1004,43 @@ ShmemInitHash(const char *name, /* table string name for shmem index */ * Returns: pointer to the object. *foundPtr is set true if the object was * already in the shmem index (hence, already initialized). * - * Note: before Postgres 9.0, this function returned NULL for some failure - * cases. Now, it always throws error instead, so callers need not check - * for NULL. + * Note: This is a legacy interface, kept for backwards compatibility with + * extensions. Use ShmemRequestStruct() in new code! */ void * ShmemInitStruct(const char *name, Size size, bool *foundPtr) { - ShmemIndexEnt *result; - void *structPtr; + void *ptr = NULL; + ShmemStructOpts options = { + .name = name, + .size = size, + .ptr = &ptr, + }; + ShmemRequest request = {&options, SHMEM_KIND_STRUCT}; + + Assert(shmem_request_state == SRS_DONE || + shmem_request_state == SRS_INITIALIZING || + shmem_request_state == SRS_REQUESTING); LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE); - if (!ShmemIndex) - { - PGShmemHeader *shmemseghdr = ShmemSegHdr; - - /* Must be trying to create/attach to ShmemIndex itself */ - Assert(strcmp(name, "ShmemIndex") == 0); - - if (IsUnderPostmaster) - { - /* Must be initializing a (non-standalone) backend */ - Assert(shmemseghdr->index != NULL); - structPtr = shmemseghdr->index; - *foundPtr = true; - } - else - { - /* - * If the shmem index doesn't exist, we are bootstrapping: we must - * be trying to init the shmem index itself. - * - * Notice that the ShmemIndexLock is released before the shmem - * index has been initialized. This should be OK because no other - * process can be accessing shared memory yet. - */ - Assert(shmemseghdr->index == NULL); - structPtr = ShmemAlloc(size); - shmemseghdr->index = structPtr; - *foundPtr = false; - } - LWLockRelease(ShmemIndexLock); - return structPtr; - } - - /* look it up in the shmem index */ - result = (ShmemIndexEnt *) - hash_search(ShmemIndex, name, HASH_ENTER_NULL, foundPtr); - - if (!result) - { - LWLockRelease(ShmemIndexLock); - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("could not create ShmemIndex entry for data structure \"%s\"", - name))); - } - - if (*foundPtr) - { - /* - * Structure is in the shmem index so someone else has allocated it - * already. The size better be the same as the size we are trying to - * initialize to, or there is a name conflict (or worse). - */ - if (result->size != size) - { - LWLockRelease(ShmemIndexLock); - ereport(ERROR, - (errmsg("ShmemIndex entry size is wrong for data structure" - " \"%s\": expected %zu, actual %zu", - name, size, result->size))); - } - structPtr = result->location; - } - else - { - Size allocated_size; + /* + * During postmaster startup, look up the existing entry if any. + */ + *foundPtr = false; + if (IsUnderPostmaster) + *foundPtr = AttachShmemIndexEntry(&request, true); - /* It isn't in the table yet. allocate and initialize it */ - structPtr = ShmemAllocRaw(size, &allocated_size); - if (structPtr == NULL) - { - /* out of memory; remove the failed ShmemIndex entry */ - hash_search(ShmemIndex, name, HASH_REMOVE, NULL); - LWLockRelease(ShmemIndexLock); - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("not enough shared memory for data structure" - " \"%s\" (%zu bytes requested)", - name, size))); - } - result->size = size; - result->allocated_size = allocated_size; - result->location = structPtr; - } + /* Initialize it if not found */ + if (!*foundPtr) + InitShmemIndexEntry(&request); LWLockRelease(ShmemIndexLock); - Assert(ShmemAddrIsValid(structPtr)); - - Assert(structPtr == (void *) CACHELINEALIGN(structPtr)); - - return structPtr; + Assert(ptr != NULL); + return ptr; } - /* * Add two Size values, checking for overflow */ @@ -494,9 +1049,7 @@ add_size(Size s1, Size s2) { Size result; - result = s1 + s2; - /* We are assuming Size is an unsigned type here... */ - if (result < s1 || result < s2) + if (pg_add_size_overflow(s1, s2, &result)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("requested shared memory size overflows size_t"))); @@ -511,11 +1064,7 @@ mul_size(Size s1, Size s2) { Size result; - if (s1 == 0 || s2 == 0) - return 0; - result = s1 * s2; - /* We are assuming Size is an unsigned type here... */ - if (result / s2 != s1) + if (pg_mul_size_overflow(s1, s2, &result)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("requested shared memory size overflows size_t"))); @@ -557,15 +1106,15 @@ pg_get_shmem_allocations(PG_FUNCTION_ARGS) /* output shared memory allocated but not counted via the shmem index */ values[0] = CStringGetTextDatum(""); nulls[1] = true; - values[2] = Int64GetDatum(ShmemSegHdr->freeoffset - named_allocated); + values[2] = Int64GetDatum(ShmemAllocator->free_offset - named_allocated); values[3] = values[2]; tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); /* output as-of-yet unused shared memory */ nulls[0] = true; - values[1] = Int64GetDatum(ShmemSegHdr->freeoffset); + values[1] = Int64GetDatum(ShmemAllocator->free_offset); nulls[1] = false; - values[2] = Int64GetDatum(ShmemSegHdr->totalsize - ShmemSegHdr->freeoffset); + values[2] = Int64GetDatum(ShmemSegHdr->totalsize - ShmemAllocator->free_offset); values[3] = values[2]; tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); @@ -603,19 +1152,16 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) InitMaterializedSRF(fcinfo, 0); max_nodes = pg_numa_get_max_node(); - nodes = palloc(sizeof(Size) * (max_nodes + 1)); + nodes = palloc_array(Size, max_nodes + 2); /* - * Different database block sizes (4kB, 8kB, ..., 32kB) can be used, while - * the OS may have different memory page sizes. + * Shared memory allocations can vary in size and may not align with OS + * memory page boundaries, while NUMA queries work on pages. * - * To correctly map between them, we need to: 1. Determine the OS memory - * page size 2. Calculate how many OS pages are used by all buffer blocks - * 3. Calculate how many OS pages are contained within each database - * block. - * - * This information is needed before calling move_pages() for NUMA memory - * node inquiry. + * To correctly map each allocation to NUMA nodes, we need to: 1. + * Determine the OS memory page size. 2. Align each allocation's start/end + * addresses to page boundaries. 3. Query NUMA node information for all + * pages spanning the allocation. */ os_page_size = pg_get_shmem_pagesize(); @@ -631,8 +1177,8 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) * them using only fraction of the total pages. */ shm_total_page_count = (ShmemSegHdr->totalsize / os_page_size) + 1; - page_ptrs = palloc0(sizeof(void *) * shm_total_page_count); - pages_status = palloc(sizeof(int) * shm_total_page_count); + page_ptrs = palloc0_array(void *, shm_total_page_count); + pages_status = palloc_array(int, shm_total_page_count); if (firstNumaTouch) elog(DEBUG1, "NUMA: page-faulting shared memory segments for proper NUMA readouts"); @@ -642,7 +1188,6 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) hash_seq_init(&hstat, ShmemIndex); /* output all allocated entries */ - memset(nulls, 0, sizeof(nulls)); while ((ent = (ShmemIndexEnt *) hash_seq_search(&hstat)) != NULL) { int i; @@ -679,12 +1224,10 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) */ for (i = 0; i < shm_ent_page_count; i++) { - volatile uint64 touch pg_attribute_unused(); - page_ptrs[i] = startptr + (i * os_page_size); if (firstNumaTouch) - pg_numa_touch_mem_if_required(touch, page_ptrs[i]); + pg_numa_touch_mem_if_required(page_ptrs[i]); CHECK_FOR_INTERRUPTS(); } @@ -693,22 +1236,33 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) elog(ERROR, "failed NUMA pages inquiry status: %m"); /* Count number of NUMA nodes used for this shared memory entry */ - memset(nodes, 0, sizeof(Size) * (max_nodes + 1)); + memset(nodes, 0, sizeof(Size) * (max_nodes + 2)); for (i = 0; i < shm_ent_page_count; i++) { int s = pages_status[i]; /* Ensure we are adding only valid index to the array */ - if (s < 0 || s > max_nodes) + if (s >= 0 && s <= max_nodes) { - elog(ERROR, "invalid NUMA node id outside of allowed range " - "[0, " UINT64_FORMAT "]: %d", max_nodes, s); + /* valid NUMA node */ + nodes[s]++; + continue; + } + else if (s == -2) + { + /* -2 means ENOENT (e.g. page was moved to swap) */ + nodes[max_nodes + 1]++; + continue; } - nodes[s]++; + elog(ERROR, "invalid NUMA node id outside of allowed range " + "[0, " UINT64_FORMAT "]: %d", max_nodes, s); } + /* no NULLs for regular nodes */ + memset(nulls, 0, sizeof(nulls)); + /* * Add one entry for each NUMA node, including those without allocated * memory for this segment. @@ -716,12 +1270,20 @@ pg_get_shmem_allocations_numa(PG_FUNCTION_ARGS) for (i = 0; i <= max_nodes; i++) { values[0] = CStringGetTextDatum(ent->key); - values[1] = i; + values[1] = Int32GetDatum(i); values[2] = Int64GetDatum(nodes[i] * os_page_size); tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } + + /* The last entry is used for pages without a NUMA node. */ + nulls[1] = true; + values[0] = CStringGetTextDatum(ent->key); + values[2] = Int64GetDatum(nodes[max_nodes + 1] * os_page_size); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); } LWLockRelease(ShmemIndexLock); diff --git a/src/backend/storage/ipc/shmem_hash.c b/src/backend/storage/ipc/shmem_hash.c new file mode 100644 index 0000000000000..c28d673cbd200 --- /dev/null +++ b/src/backend/storage/ipc/shmem_hash.c @@ -0,0 +1,206 @@ +/*------------------------------------------------------------------------- + * + * shmem_hash.c + * hash table implementation in shared memory + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * A shared memory hash table implementation on top of the named, fixed-size + * shared memory areas managed by shmem.c. Each hash table has its own free + * list, so hash buckets can be reused when an item is deleted. + * + * IDENTIFICATION + * src/backend/storage/ipc/shmem_hash.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "storage/shmem.h" +#include "storage/shmem_internal.h" +#include "utils/memutils.h" + +/* + * A very simple allocator used to carve out different parts of a hash table + * from a previously allocated contiguous shared memory area. + */ +typedef struct shmem_hash_allocator +{ + char *next; /* start of free space in the area */ + char *end; /* end of the shmem area */ +} shmem_hash_allocator; + +static void *ShmemHashAlloc(Size size, void *alloc_arg); + +/* + * ShmemRequestHash -- Request a shared memory hash table. + * + * Similar to ShmemRequestStruct(), but requests a hash table instead of an + * opaque area. + */ +void +ShmemRequestHashWithOpts(const ShmemHashOpts *options) +{ + ShmemHashOpts *options_copy; + + Assert(options->name != NULL); + + options_copy = MemoryContextAlloc(TopMemoryContext, + sizeof(ShmemHashOpts)); + memcpy(options_copy, options, sizeof(ShmemHashOpts)); + + /* Set options for the fixed-size area holding the hash table */ + options_copy->base.name = options->name; + options_copy->base.size = hash_estimate_size(options_copy->nelems, + options_copy->hash_info.entrysize); + + ShmemRequestInternal(&options_copy->base, SHMEM_KIND_HASH); +} + +void +shmem_hash_init(void *location, ShmemStructOpts *base_options) +{ + ShmemHashOpts *options = (ShmemHashOpts *) base_options; + int hash_flags = options->hash_flags; + HTAB *htab; + + options->hash_info.hctl = location; + htab = shmem_hash_create(location, options->base.size, false, + options->name, + options->nelems, &options->hash_info, hash_flags); + + if (options->ptr) + *options->ptr = htab; +} + +void +shmem_hash_attach(void *location, ShmemStructOpts *base_options) +{ + ShmemHashOpts *options = (ShmemHashOpts *) base_options; + int hash_flags = options->hash_flags; + HTAB *htab; + + /* attach to it rather than allocate and initialize new space */ + hash_flags |= HASH_ATTACH; + options->hash_info.hctl = location; + Assert(options->hash_info.hctl != NULL); + htab = shmem_hash_create(location, options->base.size, true, + options->name, + options->nelems, &options->hash_info, hash_flags); + + if (options->ptr) + *options->ptr = htab; +} + +/* + * ShmemInitHash -- Create and initialize, or attach to, a + * shared memory hash table. + * + * We assume caller is doing some kind of synchronization + * so that two processes don't try to create/initialize the same + * table at once. (In practice, all creations are done in the postmaster + * process; child processes should always be attaching to existing tables.) + * + * nelems is the maximum number of hashtable entries. + * + * *infoP and hash_flags must specify at least the entry sizes and key + * comparison semantics (see hash_create()). Flag bits and values specific + * to shared-memory hash tables are added here, except that callers may + * choose to specify HASH_PARTITION. + * + * Note: This is a legacy interface, kept for backwards compatibility with + * extensions. Use ShmemRequestHash() in new code! + */ +HTAB * +ShmemInitHash(const char *name, /* table string name for shmem index */ + int64 nelems, /* size of the table */ + HASHCTL *infoP, /* info about key and bucket size */ + int hash_flags) /* info about infoP */ +{ + bool found; + size_t size; + void *location; + + size = hash_estimate_size(nelems, infoP->entrysize); + + /* + * Look it up in the shmem index or allocate. + * + * NOTE: The area is requested internally as SHMEM_KIND_STRUCT instead of + * SHMEM_KIND_HASH. That's correct because we do the hash table + * initialization by calling shmem_hash_create() ourselves. (We don't + * expose the request kind to users; if we did, that would be confusing.) + */ + location = ShmemInitStruct(name, size, &found); + + return shmem_hash_create(location, size, found, + name, nelems, infoP, hash_flags); +} + +/* + * Initialize or attach to a shared hash table in the given shmem region. + * + * This is exposed to allow InitShmemAllocator() to share the logic for + * bootstrapping the ShmemIndex hash table. + */ +HTAB * +shmem_hash_create(void *location, size_t size, bool found, + const char *name, int64 nelems, HASHCTL *infoP, int hash_flags) +{ + shmem_hash_allocator allocator; + + /* + * Hash tables allocated in shared memory have a fixed directory and have + * all elements allocated upfront. We don't support growing because we'd + * need to grow the underlying shmem region with it. + * + * The shared memory allocator must be specified too. + */ + infoP->alloc = ShmemHashAlloc; + infoP->alloc_arg = NULL; + hash_flags |= HASH_SHARED_MEM | HASH_ALLOC | HASH_FIXED_SIZE; + + /* + * if it already exists, attach to it rather than allocate and initialize + * new space + */ + if (!found) + { + allocator.next = (char *) location; + allocator.end = (char *) location + size; + infoP->alloc_arg = &allocator; + } + else + { + /* Pass location of hashtable header to hash_create */ + infoP->hctl = (HASHHDR *) location; + hash_flags |= HASH_ATTACH; + } + + return hash_create(name, nelems, infoP, hash_flags); +} + +/* + * ShmemHashAlloc -- alloc callback for shared memory hash tables + * + * Carve out the allocation from a pre-allocated region. All shared memory + * hash tables are initialized with HASH_FIXED_SIZE, so all the allocations + * happen upfront during initialization and no locking is required. + */ +static void * +ShmemHashAlloc(Size size, void *alloc_arg) +{ + shmem_hash_allocator *allocator = (shmem_hash_allocator *) alloc_arg; + void *result; + + size = MAXALIGN(size); + + if (allocator->end - allocator->next < size) + return NULL; + result = allocator->next; + allocator->next += size; + + return result; +} diff --git a/src/backend/storage/ipc/signalfuncs.c b/src/backend/storage/ipc/signalfuncs.c index a3a670ba24741..800b699de21dd 100644 --- a/src/backend/storage/ipc/signalfuncs.c +++ b/src/backend/storage/ipc/signalfuncs.c @@ -3,7 +3,7 @@ * signalfuncs.c * Functions for signaling backends * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "storage/procarray.h" #include "utils/acl.h" #include "utils/fmgrprotos.h" +#include "utils/wait_event.h" /* @@ -87,10 +88,7 @@ pg_signal_backend(int pid, int sig) */ if (!OidIsValid(proc->roleId) || superuser_arg(proc->roleId)) { - ProcNumber procNumber = GetNumberFromPGProc(proc); - BackendType backendType = pgstat_get_backend_type_by_proc_number(procNumber); - - if (backendType == B_AUTOVAC_WORKER) + if (proc->backendType == B_AUTOVAC_WORKER) { if (!has_privs_of_role(GetUserId(), ROLE_PG_SIGNAL_AUTOVACUUM_WORKER)) return SIGNAL_BACKEND_NOAUTOVAC; diff --git a/src/backend/storage/ipc/sinval.c b/src/backend/storage/ipc/sinval.c index 6eea8e8716918..1540c7e06962e 100644 --- a/src/backend/storage/ipc/sinval.c +++ b/src/backend/storage/ipc/sinval.c @@ -3,7 +3,7 @@ * sinval.c * POSTGRES shared cache invalidation communication code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -160,8 +160,7 @@ HandleCatchupInterrupt(void) catchupInterruptPending = true; - /* make sure the event is processed in due course */ - SetLatch(MyLatch); + /* latch will be set by procsignal_sigusr1_handler */ } /* diff --git a/src/backend/storage/ipc/sinvaladt.c b/src/backend/storage/ipc/sinvaladt.c index c5748b690f408..37a21ffaf1a21 100644 --- a/src/backend/storage/ipc/sinvaladt.c +++ b/src/backend/storage/ipc/sinvaladt.c @@ -3,7 +3,7 @@ * sinvaladt.c * POSTGRES shared cache invalidation data manager. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "storage/shmem.h" #include "storage/sinvaladt.h" #include "storage/spin.h" +#include "storage/subsystems.h" /* * Conceptually, the shared cache invalidation messages are stored in an @@ -205,6 +206,14 @@ typedef struct SISeg static SISeg *shmInvalBuffer; /* pointer to the shared inval buffer */ +static void SharedInvalShmemRequest(void *arg); +static void SharedInvalShmemInit(void *arg); + +const ShmemCallbacks SharedInvalShmemCallbacks = { + .request_fn = SharedInvalShmemRequest, + .init_fn = SharedInvalShmemInit, +}; + static LocalTransactionId nextLocalTransactionId; @@ -212,10 +221,11 @@ static void CleanupInvalidationState(int status, Datum arg); /* - * SharedInvalShmemSize --- return shared-memory space needed + * SharedInvalShmemRequest + * Register shared memory needs for the SI message buffer */ -Size -SharedInvalShmemSize(void) +static void +SharedInvalShmemRequest(void *arg) { Size size; @@ -223,26 +233,18 @@ SharedInvalShmemSize(void) size = add_size(size, mul_size(sizeof(ProcState), NumProcStateSlots)); /* procState */ size = add_size(size, mul_size(sizeof(int), NumProcStateSlots)); /* pgprocnos */ - return size; + ShmemRequestStruct(.name = "shmInvalBuffer", + .size = size, + .ptr = (void **) &shmInvalBuffer, + ); } -/* - * SharedInvalShmemInit - * Create and initialize the SI message buffer - */ -void -SharedInvalShmemInit(void) +static void +SharedInvalShmemInit(void *arg) { int i; - bool found; - - /* Allocate space in shared memory */ - shmInvalBuffer = (SISeg *) - ShmemInitStruct("shmInvalBuffer", SharedInvalShmemSize(), &found); - if (found) - return; - /* Clear message counters, save size of procState array, init spinlock */ + /* Clear message counters, init spinlock */ shmInvalBuffer->minMsgNum = 0; shmInvalBuffer->maxMsgNum = 0; shmInvalBuffer->nextThreshold = CLEANUP_MIN; @@ -331,7 +333,7 @@ CleanupInvalidationState(int status, Datum arg) ProcState *stateP; int i; - Assert(PointerIsValid(segP)); + Assert(segP); LWLockAcquire(SInvalWriteLock, LW_EXCLUSIVE); diff --git a/src/backend/storage/ipc/standby.c b/src/backend/storage/ipc/standby.c index 7fa8d9247e097..29af773394832 100644 --- a/src/backend/storage/ipc/standby.c +++ b/src/backend/storage/ipc/standby.c @@ -7,7 +7,7 @@ * AccessExclusiveLocks and starting snapshots for Hot Standby mode. * Plus conflict recovery processing. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -35,6 +35,7 @@ #include "utils/ps_status.h" #include "utils/timeout.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* User-settable GUC parameters */ int max_standby_archive_delay = 30 * 1000; @@ -71,13 +72,13 @@ static volatile sig_atomic_t got_standby_delay_timeout = false; static volatile sig_atomic_t got_standby_lock_timeout = false; static void ResolveRecoveryConflictWithVirtualXIDs(VirtualTransactionId *waitlist, - ProcSignalReason reason, + RecoveryConflictReason reason, uint32 wait_event_info, bool report_waiting); -static void SendRecoveryConflictWithBufferPin(ProcSignalReason reason); +static void SendRecoveryConflictWithBufferPin(RecoveryConflictReason reason); static XLogRecPtr LogCurrentRunningXacts(RunningTransactions CurrRunningXacts); static void LogAccessExclusiveLocks(int nlocks, xl_standby_lock *locks); -static const char *get_recovery_conflict_desc(ProcSignalReason reason); +static const char *get_recovery_conflict_desc(RecoveryConflictReason reason); /* * InitRecoveryTransactionEnvironment @@ -271,7 +272,7 @@ WaitExceedsMaxStandbyDelay(uint32 wait_event_info) * to be resolved or not. */ void -LogRecoveryConflict(ProcSignalReason reason, TimestampTz wait_start, +LogRecoveryConflict(RecoveryConflictReason reason, TimestampTz wait_start, TimestampTz now, VirtualTransactionId *wait_list, bool still_waiting) { @@ -358,7 +359,8 @@ LogRecoveryConflict(ProcSignalReason reason, TimestampTz wait_start, */ static void ResolveRecoveryConflictWithVirtualXIDs(VirtualTransactionId *waitlist, - ProcSignalReason reason, uint32 wait_event_info, + RecoveryConflictReason reason, + uint32 wait_event_info, bool report_waiting) { TimestampTz waitStart = 0; @@ -384,19 +386,19 @@ ResolveRecoveryConflictWithVirtualXIDs(VirtualTransactionId *waitlist, /* Is it time to kill it? */ if (WaitExceedsMaxStandbyDelay(wait_event_info)) { - pid_t pid; + bool signaled; /* * Now find out who to throw out of the balloon. */ Assert(VirtualTransactionIdIsValid(*waitlist)); - pid = CancelVirtualTransaction(*waitlist, reason); + signaled = SignalRecoveryConflictWithVirtualXID(*waitlist, reason); /* * Wait a little bit for it to die so that we avoid flooding * an unresponsive backend when system is heavily loaded. */ - if (pid != 0) + if (signaled) pg_usleep(5000L); } @@ -474,10 +476,11 @@ ResolveRecoveryConflictWithSnapshot(TransactionId snapshotConflictHorizon, /* * If we get passed InvalidTransactionId then we do nothing (no conflict). * - * This can happen when replaying already-applied WAL records after a - * standby crash or restart, or when replaying an XLOG_HEAP2_VISIBLE - * record that marks as frozen a page which was already all-visible. It's - * also quite common with records generated during index deletion + * This can happen whenever the changes in the WAL record do not affect + * visibility on a standby. For example: a record that only freezes an + * xmax from a locker. + * + * It's also quite common with records generated during index deletion * (original execution of the deletion can reason that a recovery conflict * which is sufficient for the deletion operation must take place before * replay of the deletion record itself). @@ -489,7 +492,7 @@ ResolveRecoveryConflictWithSnapshot(TransactionId snapshotConflictHorizon, backends = GetConflictingVirtualXIDs(snapshotConflictHorizon, locator.dbOid); ResolveRecoveryConflictWithVirtualXIDs(backends, - PROCSIG_RECOVERY_CONFLICT_SNAPSHOT, + RECOVERY_CONFLICT_SNAPSHOT, WAIT_EVENT_RECOVERY_CONFLICT_SNAPSHOT, true); @@ -499,7 +502,7 @@ ResolveRecoveryConflictWithSnapshot(TransactionId snapshotConflictHorizon, * seems OK, given that this kind of conflict should not normally be * reached, e.g. due to using a physical replication slot. */ - if (wal_level >= WAL_LEVEL_LOGICAL && isCatalogRel) + if (IsLogicalDecodingEnabled() && isCatalogRel) InvalidateObsoleteReplicationSlots(RS_INVAL_HORIZON, 0, locator.dbOid, snapshotConflictHorizon); } @@ -560,7 +563,7 @@ ResolveRecoveryConflictWithTablespace(Oid tsid) temp_file_users = GetConflictingVirtualXIDs(InvalidTransactionId, InvalidOid); ResolveRecoveryConflictWithVirtualXIDs(temp_file_users, - PROCSIG_RECOVERY_CONFLICT_TABLESPACE, + RECOVERY_CONFLICT_TABLESPACE, WAIT_EVENT_RECOVERY_CONFLICT_TABLESPACE, true); } @@ -581,7 +584,7 @@ ResolveRecoveryConflictWithDatabase(Oid dbid) */ while (CountDBBackends(dbid) > 0) { - CancelDBBackends(dbid, PROCSIG_RECOVERY_CONFLICT_DATABASE, true); + SignalRecoveryConflictWithDatabase(dbid, RECOVERY_CONFLICT_DATABASE); /* * Wait awhile for them to die so that we avoid flooding an @@ -665,7 +668,7 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict) * because the caller, WaitOnLock(), has already reported that. */ ResolveRecoveryConflictWithVirtualXIDs(backends, - PROCSIG_RECOVERY_CONFLICT_LOCK, + RECOVERY_CONFLICT_LOCK, PG_WAIT_LOCK | locktag.locktag_type, false); } @@ -723,9 +726,8 @@ ResolveRecoveryConflictWithLock(LOCKTAG locktag, bool logging_conflict) */ while (VirtualTransactionIdIsValid(*backends)) { - SignalVirtualTransaction(*backends, - PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK, - false); + (void) SignalRecoveryConflictWithVirtualXID(*backends, + RECOVERY_CONFLICT_STARTUP_DEADLOCK); backends++; } @@ -803,7 +805,7 @@ ResolveRecoveryConflictWithBufferPin(void) /* * We're already behind, so clear a path as quickly as possible. */ - SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN); + SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN); } else { @@ -840,10 +842,10 @@ ResolveRecoveryConflictWithBufferPin(void) * SIGHUP signal handler, etc cannot do that because it uses the different * latch from that ProcWaitForSignal() waits on. */ - ProcWaitForSignal(WAIT_EVENT_BUFFER_PIN); + ProcWaitForSignal(WAIT_EVENT_BUFFER_CLEANUP); if (got_standby_delay_timeout) - SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN); + SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN); else if (got_standby_deadlock_timeout) { /* @@ -859,7 +861,7 @@ ResolveRecoveryConflictWithBufferPin(void) * not be so harmful because the period that the buffer is kept pinned * is basically no so long. But we should fix this? */ - SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK); + SendRecoveryConflictWithBufferPin(RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK); } /* @@ -874,18 +876,18 @@ ResolveRecoveryConflictWithBufferPin(void) } static void -SendRecoveryConflictWithBufferPin(ProcSignalReason reason) +SendRecoveryConflictWithBufferPin(RecoveryConflictReason reason) { - Assert(reason == PROCSIG_RECOVERY_CONFLICT_BUFFERPIN || - reason == PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK); + Assert(reason == RECOVERY_CONFLICT_BUFFERPIN || + reason == RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK); /* * We send signal to all backends to ask them if they are holding the - * buffer pin which is delaying the Startup process. We must not set the - * conflict flag yet, since most backends will be innocent. Let the - * SIGUSR1 handling in each backend decide their own fate. + * buffer pin which is delaying the Startup process. Most of them will be + * innocent, but we let the SIGUSR1 handling in each backend decide their + * own fate. */ - CancelDBBackends(InvalidOid, reason, false); + SignalRecoveryConflictWithDatabase(InvalidOid, reason); } /* @@ -1186,6 +1188,14 @@ standby_redo(XLogReaderState *record) xl_running_xacts *xlrec = (xl_running_xacts *) XLogRecGetData(record); RunningTransactionsData running; + /* + * Records issued for specific database are not suitable for physical + * replication because that affects the whole cluster. In particular, + * the list of XID is probably incomplete here. + */ + if (OidIsValid(xlrec->dbid)) + return; + running.xcnt = xlrec->xcnt; running.subxcnt = xlrec->subxcnt; running.subxid_status = xlrec->subxid_overflow ? SUBXIDS_MISSING : SUBXIDS_IN_ARRAY; @@ -1275,16 +1285,28 @@ standby_redo(XLogReaderState *record) * as there's no independent knob to just enable logical decoding. For * details of how this is used, check snapbuild.c's introductory comment. * + * If 'dbid' is valid, only gather transactions running in that + * database. snapbuild.c can use such running xacts information for faster + * startup, but it still needs normal (cluster-wide) during the actual + * decoding - see standby_decode() and SnapBuildProcessRunningXacts() for + * details. Other processes (e.g. checkpointer) issue the cluster-wide records + * whether logical decoding is active or not. + * + * Please be careful about using this argument for other purposes. In + * particular, physical replication *must* ignore the database-specific + * records, exactly because they do not cover the whole cluster - see + * standby_redo(). * * Returns the RecPtr of the last inserted record. */ XLogRecPtr -LogStandbySnapshot(void) +LogStandbySnapshot(Oid dbid) { XLogRecPtr recptr; RunningTransactions running; xl_standby_lock *locks; int nlocks; + bool logical_decoding_enabled = IsLogicalDecodingEnabled(); Assert(XLogStandbyInfoActive()); @@ -1311,7 +1333,7 @@ LogStandbySnapshot(void) * Log details of all in-progress transactions. This should be the last * record we write, because standby will open up when it sees this. */ - running = GetRunningTransactionData(); + running = GetRunningTransactionData(dbid); /* * GetRunningTransactionData() acquired ProcArrayLock, we must release it. @@ -1325,13 +1347,13 @@ LogStandbySnapshot(void) * record. Fortunately this routine isn't executed frequently, and it's * only a shared lock. */ - if (wal_level < WAL_LEVEL_LOGICAL) + if (!logical_decoding_enabled) LWLockRelease(ProcArrayLock); recptr = LogCurrentRunningXacts(running); /* Release lock if we kept it longer ... */ - if (wal_level >= WAL_LEVEL_LOGICAL) + if (logical_decoding_enabled) LWLockRelease(ProcArrayLock); /* GetRunningTransactionData() acquired XidGenLock, we must release it */ @@ -1355,6 +1377,7 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts) xl_running_xacts xlrec; XLogRecPtr recptr; + xlrec.dbid = CurrRunningXacts->dbid; xlrec.xcnt = CurrRunningXacts->xcnt; xlrec.subxcnt = CurrRunningXacts->subxcnt; xlrec.subxid_overflow = (CurrRunningXacts->subxid_status != SUBXIDS_IN_ARRAY); @@ -1376,7 +1399,7 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts) if (xlrec.subxid_overflow) elog(DEBUG2, - "snapshot of %d running transactions overflowed (lsn %X/%X oldest xid %u latest complete %u next xid %u)", + "snapshot of %d running transactions overflowed (lsn %X/%08X oldest xid %u latest complete %u next xid %u)", CurrRunningXacts->xcnt, LSN_FORMAT_ARGS(recptr), CurrRunningXacts->oldestRunningXid, @@ -1384,7 +1407,7 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts) CurrRunningXacts->nextXid); else elog(DEBUG2, - "snapshot of %d+%d running transaction ids (lsn %X/%X oldest xid %u latest complete %u next xid %u)", + "snapshot of %d+%d running transaction ids (lsn %X/%08X oldest xid %u latest complete %u next xid %u)", CurrRunningXacts->xcnt, CurrRunningXacts->subxcnt, LSN_FORMAT_ARGS(recptr), CurrRunningXacts->oldestRunningXid, @@ -1489,35 +1512,36 @@ LogStandbyInvalidations(int nmsgs, SharedInvalidationMessage *msgs, /* Return the description of recovery conflict */ static const char * -get_recovery_conflict_desc(ProcSignalReason reason) +get_recovery_conflict_desc(RecoveryConflictReason reason) { const char *reasonDesc = _("unknown reason"); switch (reason) { - case PROCSIG_RECOVERY_CONFLICT_BUFFERPIN: + case RECOVERY_CONFLICT_BUFFERPIN: reasonDesc = _("recovery conflict on buffer pin"); break; - case PROCSIG_RECOVERY_CONFLICT_LOCK: + case RECOVERY_CONFLICT_LOCK: reasonDesc = _("recovery conflict on lock"); break; - case PROCSIG_RECOVERY_CONFLICT_TABLESPACE: + case RECOVERY_CONFLICT_TABLESPACE: reasonDesc = _("recovery conflict on tablespace"); break; - case PROCSIG_RECOVERY_CONFLICT_SNAPSHOT: + case RECOVERY_CONFLICT_SNAPSHOT: reasonDesc = _("recovery conflict on snapshot"); break; - case PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT: + case RECOVERY_CONFLICT_LOGICALSLOT: reasonDesc = _("recovery conflict on replication slot"); break; - case PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK: + case RECOVERY_CONFLICT_STARTUP_DEADLOCK: + reasonDesc = _("recovery conflict on deadlock"); + break; + case RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK: reasonDesc = _("recovery conflict on buffer deadlock"); break; - case PROCSIG_RECOVERY_CONFLICT_DATABASE: + case RECOVERY_CONFLICT_DATABASE: reasonDesc = _("recovery conflict on database"); break; - default: - break; } return reasonDesc; diff --git a/src/backend/storage/ipc/waiteventset.c b/src/backend/storage/ipc/waiteventset.c index 7c0e66900f98d..627dba0a842dc 100644 --- a/src/backend/storage/ipc/waiteventset.c +++ b/src/backend/storage/ipc/waiteventset.c @@ -37,7 +37,7 @@ * The Windows implementation uses Windows events that are inherited by all * postmaster child processes. There's no need for the self-pipe trick there. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -67,6 +67,7 @@ #include "libpq/pqsignal.h" #include "miscadmin.h" #include "pgstat.h" +#include "port/atomics.h" #include "portability/instr_time.h" #include "postmaster/postmaster.h" #include "storage/fd.h" @@ -76,6 +77,7 @@ #include "storage/waiteventset.h" #include "utils/memutils.h" #include "utils/resowner.h" +#include "utils/wait_event.h" /* * Select the fd readiness primitive to use. Normally the "most modern" @@ -346,7 +348,7 @@ InitializeWaitEventSupport(void) #ifdef WAIT_USE_KQUEUE /* Ignore SIGURG, because we'll receive it via kqueue. */ - pqsignal(SIGURG, SIG_IGN); + pqsignal(SIGURG, PG_SIG_IGN); #endif } @@ -461,7 +463,6 @@ CreateWaitEventSet(ResourceOwner resowner, int nevents) * pending signals are serviced. */ set->handles[0] = pgwin32_signal_event; - StaticAssertStmt(WSA_INVALID_EVENT == NULL, ""); #endif return set; @@ -978,6 +979,8 @@ WaitEventAdjustKqueue(WaitEventSet *set, WaitEvent *event, int old_events) #endif #if defined(WAIT_USE_WIN32) +StaticAssertDecl(WSA_INVALID_EVENT == NULL, ""); + static void WaitEventAdjustWin32(WaitEventSet *set, WaitEvent *event) { @@ -1476,7 +1479,7 @@ WaitEventSetWaitBlock(WaitEventSet *set, int cur_timeout, struct pollfd *cur_pollfd; /* Sleep */ - rc = poll(set->pollfds, set->nevents, (int) cur_timeout); + rc = poll(set->pollfds, set->nevents, cur_timeout); /* Check return code */ if (rc < 0) @@ -1529,15 +1532,15 @@ WaitEventSetWaitBlock(WaitEventSet *set, int cur_timeout, (cur_pollfd->revents & (POLLIN | POLLHUP | POLLERR | POLLNVAL))) { /* - * We expect an POLLHUP when the remote end is closed, but because + * We expect a POLLHUP when the remote end is closed, but because * we don't expect the pipe to become readable or to have any * errors either, treat those cases as postmaster death, too. * * Be paranoid about a spurious event signaling the postmaster as * being dead. There have been reports about that happening with * older primitives (select(2) to be specific), and a spurious - * WL_POSTMASTER_DEATH event would be painful. Re-checking doesn't - * cost much. + * WL_POSTMASTER_DEATH event would be painful. Re-checking + * doesn't cost much. */ if (!PostmasterIsAliveInternal()) { @@ -2009,7 +2012,7 @@ ResOwnerReleaseWaitEventSet(Datum res) * NB: be sure to save and restore errno around it. (That's standard practice * in most signal handlers, of course, but we used to omit it in handlers that * only set a flag.) XXX - * + * * NB: this function is called from critical sections and signal handlers so * throwing an error is not a good idea. * diff --git a/src/backend/storage/large_object/inv_api.c b/src/backend/storage/large_object/inv_api.c index 68b76f2cc18a0..a3cce496c20be 100644 --- a/src/backend/storage/large_object/inv_api.c +++ b/src/backend/storage/large_object/inv_api.c @@ -19,7 +19,7 @@ * memory context given to inv_open (for LargeObjectDesc structs). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -142,7 +142,7 @@ getdatafield(Form_pg_largeobject tuple, if (VARATT_IS_EXTENDED(datafield)) { datafield = (bytea *) - detoast_attr((struct varlena *) datafield); + detoast_attr((varlena *) datafield); freeit = true; } len = VARSIZE(datafield) - VARHDRSZ; @@ -298,7 +298,7 @@ inv_open(Oid lobjId, int flags, MemoryContext mcxt) void inv_close(LargeObjectDesc *obj_desc) { - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); pfree(obj_desc); } @@ -344,7 +344,7 @@ inv_getsize(LargeObjectDesc *obj_desc) SysScanDesc sd; HeapTuple tuple; - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); open_lo_relation(); @@ -389,7 +389,7 @@ inv_seek(LargeObjectDesc *obj_desc, int64 offset, int whence) { int64 newoffset; - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); /* * We allow seek/tell if you have either read or write permission, so no @@ -436,7 +436,7 @@ inv_seek(LargeObjectDesc *obj_desc, int64 offset, int whence) int64 inv_tell(LargeObjectDesc *obj_desc) { - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); /* * We allow seek/tell if you have either read or write permission, so no @@ -459,7 +459,7 @@ inv_read(LargeObjectDesc *obj_desc, char *buf, int nbytes) SysScanDesc sd; HeapTuple tuple; - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); Assert(buf != NULL); if ((obj_desc->flags & IFS_RDLOCK) == 0) @@ -556,12 +556,10 @@ inv_write(LargeObjectDesc *obj_desc, const char *buf, int nbytes) bool pfreeit; union { - bytea hdr; + alignas(int32) bytea hdr; /* this is to make the union big enough for a LO data chunk: */ char data[LOBLKSIZE + VARHDRSZ]; - /* ensure union is aligned well enough: */ - int32 align_it; - } workbuf; + } workbuf = {0}; char *workb = VARDATA(&workbuf.hdr); HeapTuple newtup; Datum values[Natts_pg_largeobject]; @@ -569,7 +567,7 @@ inv_write(LargeObjectDesc *obj_desc, const char *buf, int nbytes) bool replace[Natts_pg_largeobject]; CatalogIndexState indstate; - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); Assert(buf != NULL); /* enforce writability because snapshot is probably wrong otherwise */ @@ -747,12 +745,10 @@ inv_truncate(LargeObjectDesc *obj_desc, int64 len) Form_pg_largeobject olddata; union { - bytea hdr; + alignas(int32) bytea hdr; /* this is to make the union big enough for a LO data chunk: */ char data[LOBLKSIZE + VARHDRSZ]; - /* ensure union is aligned well enough: */ - int32 align_it; - } workbuf; + } workbuf = {0}; char *workb = VARDATA(&workbuf.hdr); HeapTuple newtup; Datum values[Natts_pg_largeobject]; @@ -760,7 +756,7 @@ inv_truncate(LargeObjectDesc *obj_desc, int64 len) bool replace[Natts_pg_largeobject]; CatalogIndexState indstate; - Assert(PointerIsValid(obj_desc)); + Assert(obj_desc); /* enforce writability because snapshot is probably wrong otherwise */ if ((obj_desc->flags & IFS_WRLOCK) == 0) diff --git a/src/backend/storage/large_object/meson.build b/src/backend/storage/large_object/meson.build index 1d94b44419255..2d0c6416b2cd5 100644 --- a/src/backend/storage/large_object/meson.build +++ b/src/backend/storage/large_object/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'inv_api.c', diff --git a/src/backend/storage/lmgr/Makefile b/src/backend/storage/lmgr/Makefile index 6cbaf23b855f6..a5fbc24ddad6e 100644 --- a/src/backend/storage/lmgr/Makefile +++ b/src/backend/storage/lmgr/Makefile @@ -24,13 +24,9 @@ OBJS = \ include $(top_srcdir)/src/backend/common.mk -ifdef TAS -TASPATH = $(top_builddir)/src/backend/port/tas.o -endif - s_lock_test: s_lock.c $(top_builddir)/src/common/libpgcommon.a $(top_builddir)/src/port/libpgport.a $(CC) $(CPPFLAGS) $(CFLAGS) -DS_LOCK_TEST=1 $(srcdir)/s_lock.c \ - $(TASPATH) -L $(top_builddir)/src/common -lpgcommon \ + -L $(top_builddir)/src/common -lpgcommon \ -L $(top_builddir)/src/port -lpgport -lm -o s_lock_test lwlocknames.h: ../../../include/storage/lwlocklist.h ../../utils/activity/wait_event_names.txt generate-lwlocknames.pl diff --git a/src/backend/storage/lmgr/condition_variable.c b/src/backend/storage/lmgr/condition_variable.c index 228303e77f7ca..1f16b3f74757b 100644 --- a/src/backend/storage/lmgr/condition_variable.c +++ b/src/backend/storage/lmgr/condition_variable.c @@ -8,7 +8,7 @@ * interrupted, unlike LWLock waits. Condition variables are safe * to use within dynamic shared memory segments. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/storage/lmgr/condition_variable.c @@ -18,6 +18,8 @@ #include "postgres.h" +#include + #include "miscadmin.h" #include "portability/instr_time.h" #include "storage/condition_variable.h" diff --git a/src/backend/storage/lmgr/deadlock.c b/src/backend/storage/lmgr/deadlock.c index c4bfaaa67ac8c..b8962d875b657 100644 --- a/src/backend/storage/lmgr/deadlock.c +++ b/src/backend/storage/lmgr/deadlock.c @@ -7,7 +7,7 @@ * detection and resolution algorithms. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -135,10 +135,9 @@ static PGPROC *blocking_autovacuum_proc = NULL; * This does per-backend initialization of the deadlock checker; primarily, * allocation of working memory for DeadLockCheck. We do this per-backend * since there's no percentage in making the kernel do copy-on-write - * inheritance of workspace from the postmaster. We want to allocate the - * space at startup because (a) the deadlock checker might be invoked when - * there's no free memory left, and (b) the checker is normally run inside a - * signal handler, which is a very dangerous place to invoke palloc from. + * inheritance of workspace from the postmaster. We allocate the space at + * startup because the deadlock checker is run with all the partitions of the + * lock table locked, and we want to keep that section as short as possible. */ void InitDeadLockChecking(void) @@ -192,11 +191,13 @@ InitDeadLockChecking(void) * last MaxBackends entries in possibleConstraints[] are reserved as * output workspace for FindLockCycle. */ - StaticAssertStmt(MAX_BACKENDS_BITS <= (32 - 3), - "MAX_BACKENDS_BITS too big for * 4"); - maxPossibleConstraints = MaxBackends * 4; - possibleConstraints = - (EDGE *) palloc(maxPossibleConstraints * sizeof(EDGE)); + { + StaticAssertDecl(MAX_BACKENDS_BITS <= (32 - 3), + "MAX_BACKENDS_BITS too big for * 4"); + maxPossibleConstraints = MaxBackends * 4; + possibleConstraints = + (EDGE *) palloc(maxPossibleConstraints * sizeof(EDGE)); + } MemoryContextSwitchTo(oldcxt); } @@ -213,8 +214,7 @@ InitDeadLockChecking(void) * * On failure, deadlock details are recorded in deadlockDetails[] for * subsequent printing by DeadLockReport(). That activity is separate - * because (a) we don't want to do it while holding all those LWLocks, - * and (b) we are typically invoked inside a signal handler. + * because we don't want to do it while holding all those LWLocks. */ DeadLockState DeadLockCheck(PGPROC *proc) @@ -262,7 +262,7 @@ DeadLockCheck(PGPROC *proc) /* Reset the queue and re-add procs in the desired order */ dclist_init(waitQueue); for (int j = 0; j < nProcs; j++) - dclist_push_tail(waitQueue, &procs[j]->links); + dclist_push_tail(waitQueue, &procs[j]->waitLink); #ifdef DEBUG_DEADLOCK PrintLockQueue(lock, "rearranged to:"); @@ -504,7 +504,7 @@ FindLockCycleRecurse(PGPROC *checkProc, * If the process is waiting, there is an outgoing waits-for edge to each * process that blocks it. */ - if (checkProc->links.next != NULL && checkProc->waitLock != NULL && + if (!dlist_node_is_detached(&checkProc->waitLink) && FindLockCycleRecurseMember(checkProc, checkProc, depth, softEdges, nSoftEdges)) return true; @@ -522,7 +522,7 @@ FindLockCycleRecurse(PGPROC *checkProc, memberProc = dlist_container(PGPROC, lockGroupLink, iter.cur); - if (memberProc->links.next != NULL && memberProc->waitLock != NULL && + if (!dlist_node_is_detached(&memberProc->waitLink) && memberProc->waitLock != NULL && memberProc != checkProc && FindLockCycleRecurseMember(memberProc, checkProc, depth, softEdges, nSoftEdges)) @@ -715,7 +715,7 @@ FindLockCycleRecurseMember(PGPROC *checkProc, { dclist_foreach(proc_iter, waitQueue) { - proc = dlist_container(PGPROC, links, proc_iter.cur); + proc = dlist_container(PGPROC, waitLink, proc_iter.cur); if (proc->lockGroupLeader == checkProcLeader) lastGroupMember = proc; @@ -730,7 +730,7 @@ FindLockCycleRecurseMember(PGPROC *checkProc, { PGPROC *leader; - proc = dlist_container(PGPROC, links, proc_iter.cur); + proc = dlist_container(PGPROC, waitLink, proc_iter.cur); leader = proc->lockGroupLeader == NULL ? proc : proc->lockGroupLeader; @@ -879,7 +879,7 @@ TopoSort(LOCK *lock, i = 0; dclist_foreach(proc_iter, waitQueue) { - proc = dlist_container(PGPROC, links, proc_iter.cur); + proc = dlist_container(PGPROC, waitLink, proc_iter.cur); topoProcs[i++] = proc; } Assert(i == queue_size); @@ -1059,7 +1059,7 @@ PrintLockQueue(LOCK *lock, const char *info) dclist_foreach(proc_iter, waitQueue) { - PGPROC *proc = dlist_container(PGPROC, links, proc_iter.cur); + PGPROC *proc = dlist_container(PGPROC, waitLink, proc_iter.cur); printf(" %d", proc->pid); } diff --git a/src/backend/storage/lmgr/generate-lwlocknames.pl b/src/backend/storage/lmgr/generate-lwlocknames.pl index 4441b7cba0c5f..b49007167b096 100644 --- a/src/backend/storage/lmgr/generate-lwlocknames.pl +++ b/src/backend/storage/lmgr/generate-lwlocknames.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate lwlocknames.h from lwlocklist.h -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -10,7 +10,6 @@ my $output_path = '.'; my $lastlockidx = -1; -my $continue = "\n"; GetOptions('outdir:s' => \$output_path); @@ -28,18 +27,24 @@ # -# First, record the predefined LWLocks listed in wait_event_names.txt. We'll -# cross-check those with the ones in lwlocklist.h. +# First, record the predefined LWLocks and built-in tranches listed in +# wait_event_names.txt. We'll cross-check those with the ones in lwlocklist.h. # +my @wait_event_tranches; my @wait_event_lwlocks; my $record_lwlocks = 0; +my $in_tranches = 0; while (<$wait_event_names>) { chomp; # Check for end marker. - last if /^# END OF PREDEFINED LWLOCKS/; + if (/^# END OF PREDEFINED LWLOCKS/) + { + $in_tranches = 1; + next; + } # Skip comments and empty lines. next if /^#/; @@ -55,13 +60,29 @@ # Go to the next line if we are not yet recording LWLocks. next if not $record_lwlocks; + # Stop recording if we reach another section. + last if /^Section:/; + # Record the LWLock. (my $waiteventname, my $waitevendocsentence) = split(/\t/, $_); - push(@wait_event_lwlocks, $waiteventname); + + if ($in_tranches) + { + push(@wait_event_tranches, $waiteventname); + } + else + { + push(@wait_event_lwlocks, $waiteventname); + } } +# +# While gathering the list of predefined LWLocks, cross-check the lists in +# lwlocklist.h with the wait events we just recorded. +# my $in_comment = 0; -my $i = 0; +my $lwlock_count = 0; +my $tranche_count = 0; while (<$lwlocklist>) { chomp; @@ -82,40 +103,72 @@ next; } - die "unable to parse lwlocklist.h line \"$_\"" - unless /^PG_LWLOCK\((\d+),\s+(\w+)\)$/; + # + # Gather list of predefined LWLocks and cross-check with the wait events. + # + if (/^PG_LWLOCK\((\d+),\s+(\w+)\)$/) + { + my ($lockidx, $lockname) = ($1, $2); - (my $lockidx, my $lockname) = ($1, $2); + die "lwlocklist.h not in order" if $lockidx < $lastlockidx; + die "lwlocklist.h has duplicates" if $lockidx == $lastlockidx; - die "lwlocklist.h not in order" if $lockidx < $lastlockidx; - die "lwlocklist.h has duplicates" if $lockidx == $lastlockidx; + die "$lockname defined in lwlocklist.h but missing from " + . "wait_event_names.txt" + if $lwlock_count >= scalar @wait_event_lwlocks; + die "lists of predefined LWLocks do not match (first mismatch at " + . "$wait_event_lwlocks[$lwlock_count] in wait_event_names.txt and " + . "$lockname in lwlocklist.h)" + if $wait_event_lwlocks[$lwlock_count] ne $lockname; - die "$lockname defined in lwlocklist.h but missing from " - . "wait_event_names.txt" - if $i >= scalar @wait_event_lwlocks; - die "lists of predefined LWLocks do not match (first mismatch at " - . "$wait_event_lwlocks[$i] in wait_event_names.txt and $lockname in " - . "lwlocklist.h)" - if $wait_event_lwlocks[$i] ne $lockname; - $i++; + $lwlock_count++; - while ($lastlockidx < $lockidx - 1) + while ($lastlockidx < $lockidx - 1) + { + ++$lastlockidx; + } + $lastlockidx = $lockidx; + + # Add a "Lock" suffix to each lock name, as the C code depends on that. + printf $h "#define %-32s (&MainLWLockArray[$lockidx].lock)\n", + $lockname . "Lock"; + + next; + } + + # + # Cross-check the built-in LWLock tranches with the wait events. + # + if (/^PG_LWLOCKTRANCHE\((\w+),\s+(\w+)\)$/) { - ++$lastlockidx; - $continue = ",\n"; + my ($tranche_id, $tranche_name) = ($1, $2); + + die "$tranche_name defined in lwlocklist.h but missing from " + . "wait_event_names.txt" + if $tranche_count >= scalar @wait_event_tranches; + die + "lists of built-in LWLock tranches do not match (first mismatch at " + . "$wait_event_tranches[$tranche_count] in wait_event_names.txt and " + . "$tranche_name in lwlocklist.h)" + if $wait_event_tranches[$tranche_count] ne $tranche_name; + + $tranche_count++; + + next; } - $lastlockidx = $lockidx; - $continue = ",\n"; - # Add a "Lock" suffix to each lock name, as the C code depends on that - printf $h "#define %-32s (&MainLWLockArray[$lockidx].lock)\n", - $lockname . "Lock"; + die "unable to parse lwlocklist.h line \"$_\""; } die - "$wait_event_lwlocks[$i] defined in wait_event_names.txt but missing from " - . "lwlocklist.h" - if $i < scalar @wait_event_lwlocks; + "$wait_event_lwlocks[$lwlock_count] defined in wait_event_names.txt but " + . " missing from lwlocklist.h" + if $lwlock_count < scalar @wait_event_lwlocks; + +die + "$wait_event_tranches[$tranche_count] defined in wait_event_names.txt but " + . "missing from lwlocklist.h" + if $tranche_count < scalar @wait_event_tranches; print $h "\n"; printf $h "#define NUM_INDIVIDUAL_LWLOCKS %s\n", $lastlockidx + 1; diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index f50962983c37b..2ccf7237fee94 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -3,7 +3,7 @@ * lmgr.c * POSTGRES lock manager code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -55,7 +55,7 @@ typedef struct XactLockTableWaitInfo { XLTW_Oper oper; Relation rel; - ItemPointer ctid; + const ItemPointerData *ctid; } XactLockTableWaitInfo; static void XactLockTableWaitErrorCb(void *arg); @@ -559,7 +559,7 @@ UnlockPage(Relation relation, BlockNumber blkno, LOCKMODE lockmode) * tuple. See heap_lock_tuple before using this! */ void -LockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode) +LockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE lockmode) { LOCKTAG tag; @@ -579,7 +579,7 @@ LockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode) * Returns true iff the lock was acquired. */ bool -ConditionalLockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode, +ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE lockmode, bool logLockFailure) { LOCKTAG tag; @@ -598,7 +598,7 @@ ConditionalLockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode, * UnlockTuple */ void -UnlockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode) +UnlockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE lockmode) { LOCKTAG tag; @@ -660,7 +660,7 @@ XactLockTableDelete(TransactionId xid) * and if so wait for its parent. */ void -XactLockTableWait(TransactionId xid, Relation rel, ItemPointer ctid, +XactLockTableWait(TransactionId xid, Relation rel, const ItemPointerData *ctid, XLTW_Oper oper) { LOCKTAG tag; @@ -717,7 +717,10 @@ XactLockTableWait(TransactionId xid, Relation rel, ItemPointer ctid, * through, to avoid slowing down the normal case.) */ if (!first) + { + CHECK_FOR_INTERRUPTS(); pg_usleep(1000L); + } first = false; xid = SubTransGetTopmostTransaction(xid); } @@ -757,7 +760,10 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) /* See XactLockTableWait about this case */ if (!first) + { + CHECK_FOR_INTERRUPTS(); pg_usleep(1000L); + } first = false; xid = SubTransGetTopmostTransaction(xid); } diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index 86b06b9223f0b..8d246ed5a4e32 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -3,7 +3,7 @@ * lock.c * POSTGRES primary lock mechanism * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -39,11 +39,14 @@ #include "access/xlogutils.h" #include "miscadmin.h" #include "pg_trace.h" +#include "pgstat.h" #include "storage/lmgr.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/shmem.h" #include "storage/spin.h" #include "storage/standby.h" +#include "storage/subsystems.h" #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/resowner.h" @@ -51,7 +54,7 @@ /* GUC variables */ int max_locks_per_xact; /* used to set the lock table size */ -bool log_lock_failure = false; +bool log_lock_failures = false; #define NLOCKENTS() \ mul_size(max_locks_per_xact, add_size(MaxBackends, max_prepared_xacts)) @@ -311,6 +314,14 @@ typedef struct static volatile FastPathStrongRelationLockData *FastPathStrongRelationLocks; +static void LockManagerShmemRequest(void *arg); +static void LockManagerShmemInit(void *arg); + +const ShmemCallbacks LockManagerShmemCallbacks = { + .request_fn = LockManagerShmemRequest, + .init_fn = LockManagerShmemInit, +}; + /* * Pointers to hash tables containing lock state @@ -415,6 +426,7 @@ static void GrantLockLocal(LOCALLOCK *locallock, ResourceOwner owner); static void BeginStrongLockAcquire(LOCALLOCK *locallock, uint32 fasthashcode); static void FinishStrongLockAcquire(void); static ProcWaitStatus WaitOnLock(LOCALLOCK *locallock, ResourceOwner owner); +static void waitonlock_error_callback(void *arg); static void ReleaseLockIfHeld(LOCALLOCK *locallock, bool sessionLock); static void LockReassignOwner(LOCALLOCK *locallock, ResourceOwner parent); static bool UnGrantLock(LOCK *lock, LOCKMODE lockmode, @@ -430,71 +442,57 @@ static void GetSingleProcBlockerStatusData(PGPROC *blocked_proc, /* - * Initialize the lock manager's shmem data structures. + * Register the lock manager's shmem data structures. * - * This is called from CreateSharedMemoryAndSemaphores(), which see for more - * comments. In the normal postmaster case, the shared hash tables are - * created here, and backends inherit pointers to them via fork(). In the - * EXEC_BACKEND case, each backend re-executes this code to obtain pointers to - * the already existing shared hash tables. In either case, each backend must - * also call InitLockManagerAccess() to create the locallock hash table. + * In addition to this, each backend must also call InitLockManagerAccess() to + * create the locallock hash table. */ -void -LockManagerShmemInit(void) +static void +LockManagerShmemRequest(void *arg) { - HASHCTL info; - long init_table_size, - max_table_size; - bool found; + int64 max_table_size; /* - * Compute init/max size to request for lock hashtables. Note these - * calculations must agree with LockManagerShmemSize! + * Compute sizes for lock hashtables. */ max_table_size = NLOCKENTS(); - init_table_size = max_table_size / 2; /* - * Allocate hash table for LOCK structs. This stores per-locked-object + * Hash table for LOCK structs. This stores per-locked-object * information. */ - info.keysize = sizeof(LOCKTAG); - info.entrysize = sizeof(LOCK); - info.num_partitions = NUM_LOCK_PARTITIONS; - - LockMethodLockHash = ShmemInitHash("LOCK hash", - init_table_size, - max_table_size, - &info, - HASH_ELEM | HASH_BLOBS | HASH_PARTITION); + ShmemRequestHash(.name = "LOCK hash", + .nelems = max_table_size, + .ptr = &LockMethodLockHash, + .hash_info.keysize = sizeof(LOCKTAG), + .hash_info.entrysize = sizeof(LOCK), + .hash_info.num_partitions = NUM_LOCK_PARTITIONS, + .hash_flags = HASH_ELEM | HASH_BLOBS | HASH_PARTITION, + ); /* Assume an average of 2 holders per lock */ max_table_size *= 2; - init_table_size *= 2; - /* - * Allocate hash table for PROCLOCK structs. This stores - * per-lock-per-holder information. - */ - info.keysize = sizeof(PROCLOCKTAG); - info.entrysize = sizeof(PROCLOCK); - info.hash = proclock_hash; - info.num_partitions = NUM_LOCK_PARTITIONS; - - LockMethodProcLockHash = ShmemInitHash("PROCLOCK hash", - init_table_size, - max_table_size, - &info, - HASH_ELEM | HASH_FUNCTION | HASH_PARTITION); + ShmemRequestHash(.name = "PROCLOCK hash", + .nelems = max_table_size, + .ptr = &LockMethodProcLockHash, + .hash_info.keysize = sizeof(PROCLOCKTAG), + .hash_info.entrysize = sizeof(PROCLOCK), + .hash_info.hash = proclock_hash, + .hash_info.num_partitions = NUM_LOCK_PARTITIONS, + .hash_flags = HASH_ELEM | HASH_FUNCTION | HASH_PARTITION, + ); + + ShmemRequestStruct(.name = "Fast Path Strong Relation Lock Data", + .size = sizeof(FastPathStrongRelationLockData), + .ptr = (void **) (void *) &FastPathStrongRelationLocks, + ); +} - /* - * Allocate fast-path structures. - */ - FastPathStrongRelationLocks = - ShmemInitStruct("Fast Path Strong Relation Lock Data", - sizeof(FastPathStrongRelationLockData), &found); - if (!found) - SpinLockInit(&FastPathStrongRelationLocks->mutex); +static void +LockManagerShmemInit(void *arg) +{ + SpinLockInit(&FastPathStrongRelationLocks->mutex); } /* @@ -589,7 +587,7 @@ proclock_hash(const void *key, Size keysize) * intermediate variable to suppress cast-pointer-to-int warnings. */ procptr = PointerGetDatum(proclocktag->myProc); - lockhash ^= ((uint32) procptr) << LOG2_NUM_LOCK_PARTITIONS; + lockhash ^= DatumGetUInt32(procptr) << LOG2_NUM_LOCK_PARTITIONS; return lockhash; } @@ -610,7 +608,7 @@ ProcLockHashCode(const PROCLOCKTAG *proclocktag, uint32 hashcode) * This must match proclock_hash()! */ procptr = PointerGetDatum(proclocktag->myProc); - lockhash ^= ((uint32) procptr) << LOG2_NUM_LOCK_PARTITIONS; + lockhash ^= DatumGetUInt32(procptr) << LOG2_NUM_LOCK_PARTITIONS; return lockhash; } @@ -983,36 +981,47 @@ LockAcquireExtended(const LOCKTAG *locktag, * lock type on a relation we have already locked using the fast-path, but * for now we don't worry about that case either. */ - if (EligibleForRelationFastPath(locktag, lockmode) && - FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP) + if (EligibleForRelationFastPath(locktag, lockmode)) { - uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode); - bool acquired; + if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < + FP_LOCK_SLOTS_PER_GROUP) + { + uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode); + bool acquired; - /* - * LWLockAcquire acts as a memory sequencing point, so it's safe to - * assume that any strong locker whose increment to - * FastPathStrongRelationLocks->counts becomes visible after we test - * it has yet to begin to transfer fast-path locks. - */ - LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE); - if (FastPathStrongRelationLocks->count[fasthashcode] != 0) - acquired = false; + /* + * LWLockAcquire acts as a memory sequencing point, so it's safe + * to assume that any strong locker whose increment to + * FastPathStrongRelationLocks->counts becomes visible after we + * test it has yet to begin to transfer fast-path locks. + */ + LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE); + if (FastPathStrongRelationLocks->count[fasthashcode] != 0) + acquired = false; + else + acquired = FastPathGrantRelationLock(locktag->locktag_field2, + lockmode); + LWLockRelease(&MyProc->fpInfoLock); + if (acquired) + { + /* + * The locallock might contain stale pointers to some old + * shared objects; we MUST reset these to null before + * considering the lock to be acquired via fast-path. + */ + locallock->lock = NULL; + locallock->proclock = NULL; + GrantLockLocal(locallock, owner); + return LOCKACQUIRE_OK; + } + } else - acquired = FastPathGrantRelationLock(locktag->locktag_field2, - lockmode); - LWLockRelease(&MyProc->fpInfoLock); - if (acquired) { /* - * The locallock might contain stale pointers to some old shared - * objects; we MUST reset these to null before considering the - * lock to be acquired via fast-path. + * Increment the lock statistics counter if lock could not be + * acquired via the fast-path. */ - locallock->lock = NULL; - locallock->proclock = NULL; - GrantLockLocal(locallock, owner); - return LOCKACQUIRE_OK; + pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type); } } @@ -1931,6 +1940,7 @@ static ProcWaitStatus WaitOnLock(LOCALLOCK *locallock, ResourceOwner owner) { ProcWaitStatus result; + ErrorContextCallback waiterrcontext; TRACE_POSTGRESQL_LOCK_WAIT_START(locallock->tag.lock.locktag_field1, locallock->tag.lock.locktag_field2, @@ -1939,6 +1949,12 @@ WaitOnLock(LOCALLOCK *locallock, ResourceOwner owner) locallock->tag.lock.locktag_type, locallock->tag.mode); + /* Setup error traceback support for ereport() */ + waiterrcontext.callback = waitonlock_error_callback; + waiterrcontext.arg = locallock; + waiterrcontext.previous = error_context_stack; + error_context_stack = &waiterrcontext; + /* adjust the process title to indicate that it's waiting */ set_ps_display_suffix("waiting"); @@ -1990,6 +2006,8 @@ WaitOnLock(LOCALLOCK *locallock, ResourceOwner owner) /* reset ps display to remove the suffix */ set_ps_display_remove_suffix(); + error_context_stack = waiterrcontext.previous; + TRACE_POSTGRESQL_LOCK_WAIT_DONE(locallock->tag.lock.locktag_field1, locallock->tag.lock.locktag_field2, locallock->tag.lock.locktag_field3, @@ -2000,6 +2018,28 @@ WaitOnLock(LOCALLOCK *locallock, ResourceOwner owner) return result; } +/* + * error context callback for failures in WaitOnLock + * + * We report which lock was being waited on, in the same style used in + * deadlock reports. This helps with lock timeout errors in particular. + */ +static void +waitonlock_error_callback(void *arg) +{ + LOCALLOCK *locallock = (LOCALLOCK *) arg; + const LOCKTAG *tag = &locallock->tag.lock; + LOCKMODE mode = locallock->tag.mode; + StringInfoData locktagbuf; + + initStringInfo(&locktagbuf); + DescribeLockTag(&locktagbuf, tag); + + errcontext("waiting for %s on %s", + GetLockmodeName(tag->locktag_lockmethodid, mode), + locktagbuf.data); +} + /* * Remove a proc from the wait-queue it is on (caller must know it is on one). * This is only used when the proc has failed to get the lock, so we set its @@ -2020,13 +2060,13 @@ RemoveFromWaitQueue(PGPROC *proc, uint32 hashcode) /* Make sure proc is waiting */ Assert(proc->waitStatus == PROC_WAIT_STATUS_WAITING); - Assert(proc->links.next != NULL); + Assert(!dlist_node_is_detached(&proc->waitLink)); Assert(waitLock); Assert(!dclist_is_empty(&waitLock->waitProcs)); Assert(0 < lockmethodid && lockmethodid < lengthof(LockMethods)); /* Remove proc from lock's wait queue */ - dclist_delete_from_thoroughly(&waitLock->waitProcs, &proc->links); + dclist_delete_from_thoroughly(&waitLock->waitProcs, &proc->waitLink); /* Undo increments of request counts by waiting process */ Assert(waitLock->nRequested > 0); @@ -2844,7 +2884,7 @@ FastPathTransferRelationLocks(LockMethod lockMethodTable, const LOCKTAG *locktag */ for (i = 0; i < ProcGlobal->allProcCount; i++) { - PGPROC *proc = &ProcGlobal->allProcs[i]; + PGPROC *proc = GetPGProcByNumber(i); uint32 j; LWLockAcquire(&proc->fpInfoLock, LW_EXCLUSIVE); @@ -3068,9 +3108,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp) (MaxBackends + max_prepared_xacts + 1)); } else - vxids = (VirtualTransactionId *) - palloc0(sizeof(VirtualTransactionId) * - (MaxBackends + max_prepared_xacts + 1)); + vxids = palloc0_array(VirtualTransactionId, (MaxBackends + max_prepared_xacts + 1)); /* Compute hash code and partition lock, and look up conflicting modes. */ hashcode = LockTagHashCode(locktag); @@ -3103,7 +3141,7 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp) */ for (i = 0; i < ProcGlobal->allProcCount; i++) { - PGPROC *proc = &ProcGlobal->allProcs[i]; + PGPROC *proc = GetPGProcByNumber(i); uint32 j; /* A backend never blocks itself */ @@ -3539,9 +3577,9 @@ AtPrepare_Locks(void) * but that probably costs more cycles. */ void -PostPrepare_Locks(TransactionId xid) +PostPrepare_Locks(FullTransactionId fxid) { - PGPROC *newproc = TwoPhaseGetDummyProc(xid, false); + PGPROC *newproc = TwoPhaseGetDummyProc(fxid, false); HASH_SEQ_STATUS status; LOCALLOCK *locallock; LOCK *lock; @@ -3719,31 +3757,6 @@ PostPrepare_Locks(TransactionId xid) } -/* - * Estimate shared-memory space used for lock tables - */ -Size -LockManagerShmemSize(void) -{ - Size size = 0; - long max_table_size; - - /* lock hash table */ - max_table_size = NLOCKENTS(); - size = add_size(size, hash_estimate_size(max_table_size, sizeof(LOCK))); - - /* proclock hash table */ - max_table_size *= 2; - size = add_size(size, hash_estimate_size(max_table_size, sizeof(PROCLOCK))); - - /* - * Since NLOCKENTS is only an estimate, add 10% safety margin. - */ - size = add_size(size, size / 10); - - return size; -} - /* * GetLockStatusData - Return a summary of the lock manager's internal * status, for use in a user-level reporting function. @@ -3769,12 +3782,12 @@ GetLockStatusData(void) int el; int i; - data = (LockData *) palloc(sizeof(LockData)); + data = palloc_object(LockData); /* Guess how much space we'll need. */ els = MaxBackends; el = 0; - data->locks = (LockInstanceData *) palloc(sizeof(LockInstanceData) * els); + data->locks = palloc_array(LockInstanceData, els); /* * First, we iterate through the per-backend fast-path arrays, locking @@ -3790,7 +3803,7 @@ GetLockStatusData(void) */ for (i = 0; i < ProcGlobal->allProcCount; ++i) { - PGPROC *proc = &ProcGlobal->allProcs[i]; + PGPROC *proc = GetPGProcByNumber(i); /* Skip backends with pid=0, as they don't hold fast-path locks */ if (proc->pid == 0) @@ -3969,7 +3982,7 @@ GetBlockerStatusData(int blocked_pid) PGPROC *proc; int i; - data = (BlockedProcsData *) palloc(sizeof(BlockedProcsData)); + data = palloc_object(BlockedProcsData); /* * Guess how much space we'll need, and preallocate. Most of the time @@ -3979,9 +3992,9 @@ GetBlockerStatusData(int blocked_pid) */ data->nprocs = data->nlocks = data->npids = 0; data->maxprocs = data->maxlocks = data->maxpids = MaxBackends; - data->procs = (BlockedProcData *) palloc(sizeof(BlockedProcData) * data->maxprocs); - data->locks = (LockInstanceData *) palloc(sizeof(LockInstanceData) * data->maxlocks); - data->waiter_pids = (int *) palloc(sizeof(int) * data->maxpids); + data->procs = palloc_array(BlockedProcData, data->maxprocs); + data->locks = palloc_array(LockInstanceData, data->maxlocks); + data->waiter_pids = palloc_array(int, data->maxpids); /* * In order to search the ProcArray for blocked_pid and assume that that @@ -4113,12 +4126,11 @@ GetSingleProcBlockerStatusData(PGPROC *blocked_proc, BlockedProcsData *data) /* Collect PIDs from the lock's wait queue, stopping at blocked_proc */ dclist_foreach(proc_iter, waitQueue) { - PGPROC *queued_proc = dlist_container(PGPROC, links, proc_iter.cur); + PGPROC *queued_proc = dlist_container(PGPROC, waitLink, proc_iter.cur); if (queued_proc == blocked_proc) break; data->waiter_pids[data->npids++] = queued_proc->pid; - queued_proc = (PGPROC *) queued_proc->links.next; } bproc->num_locks = data->nlocks - bproc->first_lock; @@ -4324,11 +4336,11 @@ DumpAllLocks(void) * and PANIC anyway. */ void -lock_twophase_recover(TransactionId xid, uint16 info, +lock_twophase_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { TwoPhaseLockRecord *rec = (TwoPhaseLockRecord *) recdata; - PGPROC *proc = TwoPhaseGetDummyProc(xid, false); + PGPROC *proc = TwoPhaseGetDummyProc(fxid, false); LOCKTAG *locktag; LOCKMODE lockmode; LOCKMETHODID lockmethodid; @@ -4505,7 +4517,7 @@ lock_twophase_recover(TransactionId xid, uint16 info, * starting up into hot standby mode. */ void -lock_twophase_standby_recover(TransactionId xid, uint16 info, +lock_twophase_standby_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { TwoPhaseLockRecord *rec = (TwoPhaseLockRecord *) recdata; @@ -4524,7 +4536,7 @@ lock_twophase_standby_recover(TransactionId xid, uint16 info, if (lockmode == AccessExclusiveLock && locktag->locktag_type == LOCKTAG_RELATION) { - StandbyAcquireAccessExclusiveLock(xid, + StandbyAcquireAccessExclusiveLock(XidFromFullTransactionId(fxid), locktag->locktag_field1 /* dboid */ , locktag->locktag_field2 /* reloid */ ); } @@ -4537,11 +4549,11 @@ lock_twophase_standby_recover(TransactionId xid, uint16 info, * Find and release the lock indicated by the 2PC record. */ void -lock_twophase_postcommit(TransactionId xid, uint16 info, +lock_twophase_postcommit(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { TwoPhaseLockRecord *rec = (TwoPhaseLockRecord *) recdata; - PGPROC *proc = TwoPhaseGetDummyProc(xid, true); + PGPROC *proc = TwoPhaseGetDummyProc(fxid, true); LOCKTAG *locktag; LOCKMETHODID lockmethodid; LockMethod lockMethodTable; @@ -4563,10 +4575,10 @@ lock_twophase_postcommit(TransactionId xid, uint16 info, * This is actually just the same as the COMMIT case. */ void -lock_twophase_postabort(TransactionId xid, uint16 info, +lock_twophase_postabort(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { - lock_twophase_postcommit(xid, info, recdata, len); + lock_twophase_postcommit(fxid, info, recdata, len); } /* diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c index 46f44bc45113f..b1ad396ba7987 100644 --- a/src/backend/storage/lmgr/lwlock.c +++ b/src/backend/storage/lmgr/lwlock.c @@ -20,7 +20,7 @@ * appropriate value for a free lock. The meaning of the variable is up to * the caller, the lightweight lock code just assigns and compares it. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -84,7 +84,9 @@ #include "storage/proclist.h" #include "storage/procnumber.h" #include "storage/spin.h" +#include "storage/subsystems.h" #include "utils/memutils.h" +#include "utils/wait_event.h" #ifdef LWLOCK_STATS #include "utils/hsearch.h" @@ -92,7 +94,7 @@ #define LW_FLAG_HAS_WAITERS ((uint32) 1 << 31) -#define LW_FLAG_RELEASE_OK ((uint32) 1 << 30) +#define LW_FLAG_WAKE_IN_PROGRESS ((uint32) 1 << 30) #define LW_FLAG_LOCKED ((uint32) 1 << 29) #define LW_FLAG_BITS 3 #define LW_FLAG_MASK (((1<num_user_defined */ +static int LocalNumUserDefinedTranches; + +/* + * NamedLWLockTrancheRequests is a list of tranches requested with + * RequestNamedLWLockTranche(). It is only valid in the postmaster; after + * startup the tranches are tracked in LWLockTranches in shared memory. + */ typedef struct NamedLWLockTrancheRequest { char tranche_name[NAMEDATALEN]; int num_lwlocks; } NamedLWLockTrancheRequest; -static NamedLWLockTrancheRequest *NamedLWLockTrancheRequestArray = NULL; -static int NamedLWLockTrancheRequestsAllocated = 0; +static List *NamedLWLockTrancheRequests = NIL; -/* - * NamedLWLockTrancheRequests is both the valid length of the request array, - * and the length of the shared-memory NamedLWLockTrancheArray later on. - * This variable and NamedLWLockTrancheArray are non-static so that - * postmaster.c can copy them to child processes in EXEC_BACKEND builds. - */ -int NamedLWLockTrancheRequests = 0; +/* Size of MainLWLockArray. Only valid in postmaster. */ +static int num_main_array_locks; + +static void LWLockShmemRequest(void *arg); +static void LWLockShmemInit(void *arg); + +const ShmemCallbacks LWLockCallbacks = { + .request_fn = LWLockShmemRequest, + .init_fn = LWLockShmemInit, +}; -/* points to data in shared memory: */ -NamedLWLockTranche *NamedLWLockTrancheArray = NULL; -static void InitializeLWLocks(void); static inline void LWLockReportWaitStart(LWLock *lock); static inline void LWLockReportWaitEnd(void); static const char *GetLWTrancheName(uint16 trancheId); @@ -281,14 +264,14 @@ PRINT_LWDEBUG(const char *where, LWLock *lock, LWLockMode mode) ereport(LOG, (errhidestmt(true), errhidecontext(true), - errmsg_internal("%d: %s(%s %p): excl %u shared %u haswaiters %u waiters %u rOK %d", + errmsg_internal("%d: %s(%s %p): excl %u shared %u haswaiters %u waiters %u waking %d", MyProcPid, where, T_NAME(lock), lock, (state & LW_VAL_EXCLUSIVE) != 0, state & LW_SHARED_MASK, (state & LW_FLAG_HAS_WAITERS) != 0, pg_atomic_read_u32(&lock->nwaiters), - (state & LW_FLAG_RELEASE_OK) != 0))); + (state & LW_FLAG_WAKE_IN_PROGRESS) != 0))); } } @@ -410,156 +393,110 @@ get_lwlock_stats_entry(LWLock *lock) /* - * Compute number of LWLocks required by named tranches. These will be - * allocated in the main array. + * Compute number of LWLocks required by user-defined tranches requested with + * RequestNamedLWLockTranche(). These will be allocated in the main array. */ static int NumLWLocksForNamedTranches(void) { int numLocks = 0; - int i; - for (i = 0; i < NamedLWLockTrancheRequests; i++) - numLocks += NamedLWLockTrancheRequestArray[i].num_lwlocks; + foreach_ptr(NamedLWLockTrancheRequest, request, NamedLWLockTrancheRequests) + { + numLocks += request->num_lwlocks; + } return numLocks; } /* - * Compute shmem space needed for LWLocks and named tranches. + * Request shmem space for user-defined tranches and the main LWLock array. */ -Size -LWLockShmemSize(void) +static void +LWLockShmemRequest(void *arg) { - Size size; - int i; - int numLocks = NUM_FIXED_LWLOCKS; + size_t size; - /* Calculate total number of locks needed in the main array. */ - numLocks += NumLWLocksForNamedTranches(); + /* Space for user-defined tranches */ + ShmemRequestStruct(.name = "LWLock tranches", + .size = sizeof(LWLockTrancheShmemData), + .ptr = (void **) &LWLockTranches, + ); - /* Space for the LWLock array. */ - size = mul_size(numLocks, sizeof(LWLockPadded)); - - /* Space for dynamic allocation counter, plus room for alignment. */ - size = add_size(size, sizeof(int) + LWLOCK_PADDED_SIZE); - - /* space for named tranches. */ - size = add_size(size, mul_size(NamedLWLockTrancheRequests, sizeof(NamedLWLockTranche))); - - /* space for name of each tranche. */ - for (i = 0; i < NamedLWLockTrancheRequests; i++) - size = add_size(size, strlen(NamedLWLockTrancheRequestArray[i].tranche_name) + 1); - - return size; -} - -/* - * Allocate shmem space for the main LWLock array and all tranches and - * initialize it. We also register extension LWLock tranches here. - */ -void -CreateLWLocks(void) -{ + /* Space for the LWLock array */ if (!IsUnderPostmaster) { - Size spaceLocks = LWLockShmemSize(); - int *LWLockCounter; - char *ptr; - - /* Allocate space */ - ptr = (char *) ShmemAlloc(spaceLocks); - - /* Leave room for dynamic allocation of tranches */ - ptr += sizeof(int); - - /* Ensure desired alignment of LWLock array */ - ptr += LWLOCK_PADDED_SIZE - ((uintptr_t) ptr) % LWLOCK_PADDED_SIZE; - - MainLWLockArray = (LWLockPadded *) ptr; - - /* - * Initialize the dynamic-allocation counter for tranches, which is - * stored just before the first LWLock. - */ - LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int)); - *LWLockCounter = LWTRANCHE_FIRST_USER_DEFINED; - - /* Initialize all LWLocks */ - InitializeLWLocks(); + num_main_array_locks = NUM_FIXED_LWLOCKS + NumLWLocksForNamedTranches(); + size = num_main_array_locks * sizeof(LWLockPadded); } + else + size = SHMEM_ATTACH_UNKNOWN_SIZE; - /* Register named extension LWLock tranches in the current process. */ - for (int i = 0; i < NamedLWLockTrancheRequests; i++) - LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId, - NamedLWLockTrancheArray[i].trancheName); + ShmemRequestStruct(.name = "Main LWLock array", + .size = size, + .ptr = (void **) &MainLWLockArray, + ); } /* - * Initialize LWLocks that are fixed and those belonging to named tranches. + * Initialize shmem space for user-defined tranches and the main LWLock array. */ static void -InitializeLWLocks(void) +LWLockShmemInit(void *arg) { - int numNamedLocks = NumLWLocksForNamedTranches(); - int id; - int i; - int j; - LWLockPadded *lock; + int pos; + + /* Initialize the dynamic-allocation counter for tranches */ + LWLockTranches->num_user_defined = 0; + + SpinLockInit(&LWLockTranches->lock); + + /* + * Allocate and initialize all LWLocks in the main array. It includes all + * LWLocks for built-in tranches and those requested with + * RequestNamedLWLockTranche(). + */ + pos = 0; /* Initialize all individual LWLocks in main array */ - for (id = 0, lock = MainLWLockArray; id < NUM_INDIVIDUAL_LWLOCKS; id++, lock++) - LWLockInitialize(&lock->lock, id); + for (int id = 0; id < NUM_INDIVIDUAL_LWLOCKS; id++) + LWLockInitialize(&MainLWLockArray[pos++].lock, id); /* Initialize buffer mapping LWLocks in main array */ - lock = MainLWLockArray + BUFFER_MAPPING_LWLOCK_OFFSET; - for (id = 0; id < NUM_BUFFER_PARTITIONS; id++, lock++) - LWLockInitialize(&lock->lock, LWTRANCHE_BUFFER_MAPPING); + Assert(pos == BUFFER_MAPPING_LWLOCK_OFFSET); + for (int i = 0; i < NUM_BUFFER_PARTITIONS; i++) + LWLockInitialize(&MainLWLockArray[pos++].lock, LWTRANCHE_BUFFER_MAPPING); /* Initialize lmgrs' LWLocks in main array */ - lock = MainLWLockArray + LOCK_MANAGER_LWLOCK_OFFSET; - for (id = 0; id < NUM_LOCK_PARTITIONS; id++, lock++) - LWLockInitialize(&lock->lock, LWTRANCHE_LOCK_MANAGER); + Assert(pos == LOCK_MANAGER_LWLOCK_OFFSET); + for (int i = 0; i < NUM_LOCK_PARTITIONS; i++) + LWLockInitialize(&MainLWLockArray[pos++].lock, LWTRANCHE_LOCK_MANAGER); /* Initialize predicate lmgrs' LWLocks in main array */ - lock = MainLWLockArray + PREDICATELOCK_MANAGER_LWLOCK_OFFSET; - for (id = 0; id < NUM_PREDICATELOCK_PARTITIONS; id++, lock++) - LWLockInitialize(&lock->lock, LWTRANCHE_PREDICATE_LOCK_MANAGER); + Assert(pos == PREDICATELOCK_MANAGER_LWLOCK_OFFSET); + for (int i = 0; i < NUM_PREDICATELOCK_PARTITIONS; i++) + LWLockInitialize(&MainLWLockArray[pos++].lock, LWTRANCHE_PREDICATE_LOCK_MANAGER); /* - * Copy the info about any named tranches into shared memory (so that - * other processes can see it), and initialize the requested LWLocks. + * Copy the info about any user-defined tranches into shared memory (so + * that other processes can see it), and initialize the requested LWLocks. */ - if (NamedLWLockTrancheRequests > 0) + Assert(pos == NUM_FIXED_LWLOCKS); + foreach_ptr(NamedLWLockTrancheRequest, request, NamedLWLockTrancheRequests) { - char *trancheNames; - - NamedLWLockTrancheArray = (NamedLWLockTranche *) - &MainLWLockArray[NUM_FIXED_LWLOCKS + numNamedLocks]; - - trancheNames = (char *) NamedLWLockTrancheArray + - (NamedLWLockTrancheRequests * sizeof(NamedLWLockTranche)); - lock = &MainLWLockArray[NUM_FIXED_LWLOCKS]; - - for (i = 0; i < NamedLWLockTrancheRequests; i++) - { - NamedLWLockTrancheRequest *request; - NamedLWLockTranche *tranche; - char *name; - - request = &NamedLWLockTrancheRequestArray[i]; - tranche = &NamedLWLockTrancheArray[i]; + int idx = (LWLockTranches->num_user_defined++); - name = trancheNames; - trancheNames += strlen(request->tranche_name) + 1; - strcpy(name, request->tranche_name); - tranche->trancheId = LWLockNewTrancheId(); - tranche->trancheName = name; + strlcpy(LWLockTranches->user_defined[idx].name, + request->tranche_name, + NAMEDATALEN); + LWLockTranches->user_defined[idx].main_array_idx = pos; - for (j = 0; j < request->num_lwlocks; j++, lock++) - LWLockInitialize(&lock->lock, tranche->trancheId); - } + for (int i = 0; i < request->num_lwlocks; i++) + LWLockInitialize(&MainLWLockArray[pos++].lock, LWTRANCHE_FIRST_USER_DEFINED + idx); } + + /* Cross-check that we agree on the total size with LWLockShmemRequest() */ + Assert(pos == num_main_array_locks); } /* @@ -584,22 +521,32 @@ InitLWLockAccess(void) LWLockPadded * GetNamedLWLockTranche(const char *tranche_name) { - int lock_pos; - int i; + SpinLockAcquire(&LWLockTranches->lock); + LocalNumUserDefinedTranches = LWLockTranches->num_user_defined; + SpinLockRelease(&LWLockTranches->lock); /* * Obtain the position of base address of LWLock belonging to requested - * tranche_name in MainLWLockArray. LWLocks for named tranches are placed - * in MainLWLockArray after fixed locks. + * tranche_name in MainLWLockArray. LWLocks for user-defined tranches + * requested with RequestNamedLWLockTranche() are placed in + * MainLWLockArray after fixed locks. */ - lock_pos = NUM_FIXED_LWLOCKS; - for (i = 0; i < NamedLWLockTrancheRequests; i++) + for (int i = 0; i < LocalNumUserDefinedTranches; i++) { - if (strcmp(NamedLWLockTrancheRequestArray[i].tranche_name, + if (strcmp(LWLockTranches->user_defined[i].name, tranche_name) == 0) - return &MainLWLockArray[lock_pos]; + { + int lock_pos = LWLockTranches->user_defined[i].main_array_idx; - lock_pos += NamedLWLockTrancheRequestArray[i].num_lwlocks; + /* + * GetNamedLWLockTranche() should only be used for locks requested + * with RequestNamedLWLockTranche(), not those allocated with + * LWLockNewTrancheId(). + */ + if (lock_pos == -1) + elog(ERROR, "requested tranche was not registered with RequestNamedLWLockTranche()"); + return &MainLWLockArray[lock_pos]; + } } elog(ERROR, "requested tranche is not registered"); @@ -609,61 +556,52 @@ GetNamedLWLockTranche(const char *tranche_name) } /* - * Allocate a new tranche ID. + * Allocate a new tranche ID with the provided name. */ int -LWLockNewTrancheId(void) +LWLockNewTrancheId(const char *name) { - int result; - int *LWLockCounter; - - LWLockCounter = (int *) ((char *) MainLWLockArray - sizeof(int)); - /* We use the ShmemLock spinlock to protect LWLockCounter */ - SpinLockAcquire(ShmemLock); - result = (*LWLockCounter)++; - SpinLockRelease(ShmemLock); + int idx; - return result; -} + if (!name) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("tranche name cannot be NULL"))); -/* - * Register a dynamic tranche name in the lookup table of the current process. - * - * This routine will save a pointer to the tranche name passed as an argument, - * so the name should be allocated in a backend-lifetime context - * (shared memory, TopMemoryContext, static constant, or similar). - * - * The tranche name will be user-visible as a wait event name, so try to - * use a name that fits the style for those. - */ -void -LWLockRegisterTranche(int tranche_id, const char *tranche_name) -{ - /* This should only be called for user-defined tranches. */ - if (tranche_id < LWTRANCHE_FIRST_USER_DEFINED) - return; + if (strlen(name) >= NAMEDATALEN) + ereport(ERROR, + (errcode(ERRCODE_NAME_TOO_LONG), + errmsg("tranche name too long"), + errdetail("LWLock tranche names must be no longer than %d bytes.", + NAMEDATALEN - 1))); - /* Convert to array index. */ - tranche_id -= LWTRANCHE_FIRST_USER_DEFINED; + /* The counter and the tranche names are protected by the spinlock */ + SpinLockAcquire(&LWLockTranches->lock); - /* If necessary, create or enlarge array. */ - if (tranche_id >= LWLockTrancheNamesAllocated) + if (LWLockTranches->num_user_defined >= MAX_USER_DEFINED_TRANCHES) { - int newalloc; + SpinLockRelease(&LWLockTranches->lock); + ereport(ERROR, + (errmsg("maximum number of tranches already registered"), + errdetail("No more than %d tranches may be registered.", + MAX_USER_DEFINED_TRANCHES))); + } - newalloc = pg_nextpower2_32(Max(8, tranche_id + 1)); + /* Allocate an entry in the user_defined array */ + idx = (LWLockTranches->num_user_defined)++; - if (LWLockTrancheNames == NULL) - LWLockTrancheNames = (const char **) - MemoryContextAllocZero(TopMemoryContext, - newalloc * sizeof(char *)); - else - LWLockTrancheNames = - repalloc0_array(LWLockTrancheNames, const char *, LWLockTrancheNamesAllocated, newalloc); - LWLockTrancheNamesAllocated = newalloc; - } + /* update our local copy while we're at it */ + LocalNumUserDefinedTranches = LWLockTranches->num_user_defined; + + /* Initialize it */ + strlcpy(LWLockTranches->user_defined[idx].name, name, NAMEDATALEN); + + /* the locks are not in the main array */ + LWLockTranches->user_defined[idx].main_array_idx = -1; - LWLockTrancheNames[tranche_id] = tranche_name; + SpinLockRelease(&LWLockTranches->lock); + + return LWTRANCHE_FIRST_USER_DEFINED + idx; } /* @@ -682,34 +620,47 @@ void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks) { NamedLWLockTrancheRequest *request; + MemoryContext oldcontext; if (!process_shmem_requests_in_progress) elog(FATAL, "cannot request additional LWLocks outside shmem_request_hook"); - if (NamedLWLockTrancheRequestArray == NULL) + if (!tranche_name) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("tranche name cannot be NULL"))); + + if (strlen(tranche_name) >= NAMEDATALEN) + ereport(ERROR, + (errcode(ERRCODE_NAME_TOO_LONG), + errmsg("tranche name too long"), + errdetail("LWLock tranche names must be no longer than %d bytes.", + NAMEDATALEN - 1))); + + if (list_length(NamedLWLockTrancheRequests) >= MAX_USER_DEFINED_TRANCHES) + ereport(ERROR, + (errmsg("maximum number of tranches already registered"), + errdetail("No more than %d tranches may be registered.", + MAX_USER_DEFINED_TRANCHES))); + + /* Check that the name isn't already in use */ + foreach_ptr(NamedLWLockTrancheRequest, existing, NamedLWLockTrancheRequests) { - NamedLWLockTrancheRequestsAllocated = 16; - NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *) - MemoryContextAlloc(TopMemoryContext, - NamedLWLockTrancheRequestsAllocated - * sizeof(NamedLWLockTrancheRequest)); + if (strcmp(existing->tranche_name, tranche_name) == 0) + elog(ERROR, "requested tranche \"%s\" is already registered", tranche_name); } - if (NamedLWLockTrancheRequests >= NamedLWLockTrancheRequestsAllocated) - { - int i = pg_nextpower2_32(NamedLWLockTrancheRequests + 1); - - NamedLWLockTrancheRequestArray = (NamedLWLockTrancheRequest *) - repalloc(NamedLWLockTrancheRequestArray, - i * sizeof(NamedLWLockTrancheRequest)); - NamedLWLockTrancheRequestsAllocated = i; - } + if (IsPostmasterEnvironment) + oldcontext = MemoryContextSwitchTo(PostmasterContext); + else + oldcontext = MemoryContextSwitchTo(TopMemoryContext); - request = &NamedLWLockTrancheRequestArray[NamedLWLockTrancheRequests]; - Assert(strlen(tranche_name) + 1 <= NAMEDATALEN); + request = palloc0(sizeof(NamedLWLockTrancheRequest)); strlcpy(request->tranche_name, tranche_name, NAMEDATALEN); request->num_lwlocks = num_lwlocks; - NamedLWLockTrancheRequests++; + NamedLWLockTrancheRequests = lappend(NamedLWLockTrancheRequests, request); + + MemoryContextSwitchTo(oldcontext); } /* @@ -718,7 +669,10 @@ RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks) void LWLockInitialize(LWLock *lock, int tranche_id) { - pg_atomic_init_u32(&lock->state, LW_FLAG_RELEASE_OK); + /* verify the tranche_id is valid */ + (void) GetLWTrancheName(tranche_id); + + pg_atomic_init_u32(&lock->state, 0); #ifdef LOCK_DEBUG pg_atomic_init_u32(&lock->nwaiters, 0); #endif @@ -754,22 +708,36 @@ LWLockReportWaitEnd(void) static const char * GetLWTrancheName(uint16 trancheId) { + int idx; + /* Built-in tranche or individual LWLock? */ if (trancheId < LWTRANCHE_FIRST_USER_DEFINED) return BuiltinTrancheNames[trancheId]; /* - * It's an extension tranche, so look in LWLockTrancheNames[]. However, - * it's possible that the tranche has never been registered in the current - * process, in which case give up and return "extension". + * It's an extension tranche, so look in LWLockTranches->user_defined. + */ + idx = trancheId - LWTRANCHE_FIRST_USER_DEFINED; + + /* + * We only ever add new entries to LWLockTranches->user_defined, so most + * lookups can avoid taking the spinlock as long as the backend-local + * counter (LocalNumUserDefinedTranches) is greater than the requested + * tranche ID. Else, we need to first update the backend-local counter + * with the spinlock held before attempting the lookup again. In + * practice, the latter case is probably rare. */ - trancheId -= LWTRANCHE_FIRST_USER_DEFINED; + if (idx >= LocalNumUserDefinedTranches) + { + SpinLockAcquire(&LWLockTranches->lock); + LocalNumUserDefinedTranches = LWLockTranches->num_user_defined; + SpinLockRelease(&LWLockTranches->lock); - if (trancheId >= LWLockTrancheNamesAllocated || - LWLockTrancheNames[trancheId] == NULL) - return "extension"; + if (idx >= LocalNumUserDefinedTranches) + elog(ERROR, "tranche %d is not registered", trancheId); + } - return LWLockTrancheNames[trancheId]; + return LWLockTranches->user_defined[idx].name; } /* @@ -876,9 +844,13 @@ LWLockWaitListLock(LWLock *lock) while (true) { - /* always try once to acquire lock directly */ + /* + * Always try once to acquire the lock directly, without setting up + * the spin-delay infrastructure. The work necessary for that shows up + * in profiles and is rarely necessary. + */ old_state = pg_atomic_fetch_or_u32(&lock->state, LW_FLAG_LOCKED); - if (!(old_state & LW_FLAG_LOCKED)) + if (likely(!(old_state & LW_FLAG_LOCKED))) break; /* got lock */ /* and then spin without atomic operations until lock is released */ @@ -931,15 +903,13 @@ LWLockWaitListUnlock(LWLock *lock) static void LWLockWakeup(LWLock *lock) { - bool new_release_ok; + bool new_wake_in_progress = false; bool wokeup_somebody = false; proclist_head wakeup; proclist_mutable_iter iter; proclist_init(&wakeup); - new_release_ok = true; - /* lock wait list while collecting backends to wake up */ LWLockWaitListLock(lock); @@ -960,7 +930,7 @@ LWLockWakeup(LWLock *lock) * that are just waiting for the lock to become free don't retry * automatically. */ - new_release_ok = false; + new_wake_in_progress = true; /* * Don't wakeup (further) exclusive locks. @@ -999,12 +969,12 @@ LWLockWakeup(LWLock *lock) /* compute desired flags */ - if (new_release_ok) - desired_state |= LW_FLAG_RELEASE_OK; + if (new_wake_in_progress) + desired_state |= LW_FLAG_WAKE_IN_PROGRESS; else - desired_state &= ~LW_FLAG_RELEASE_OK; + desired_state &= ~LW_FLAG_WAKE_IN_PROGRESS; - if (proclist_is_empty(&wakeup)) + if (proclist_is_empty(&lock->waiters)) desired_state &= ~LW_FLAG_HAS_WAITERS; desired_state &= ~LW_FLAG_LOCKED; /* release lock */ @@ -1133,10 +1103,10 @@ LWLockDequeueSelf(LWLock *lock) */ /* - * Reset RELEASE_OK flag if somebody woke us before we removed - * ourselves - they'll have set it to false. + * Clear LW_FLAG_WAKE_IN_PROGRESS if somebody woke us before we + * removed ourselves - they'll have set it. */ - pg_atomic_fetch_or_u32(&lock->state, LW_FLAG_RELEASE_OK); + pg_atomic_fetch_and_u32(&lock->state, ~LW_FLAG_WAKE_IN_PROGRESS); /* * Now wait for the scheduled wakeup, otherwise our ->lwWaiting would @@ -1303,7 +1273,7 @@ LWLockAcquire(LWLock *lock, LWLockMode mode) } /* Retrying, allow LWLockRelease to release waiters again. */ - pg_atomic_fetch_or_u32(&lock->state, LW_FLAG_RELEASE_OK); + pg_atomic_fetch_and_u32(&lock->state, ~LW_FLAG_WAKE_IN_PROGRESS); #ifdef LOCK_DEBUG { @@ -1638,10 +1608,10 @@ LWLockWaitForVar(LWLock *lock, pg_atomic_uint64 *valptr, uint64 oldval, LWLockQueueSelf(lock, LW_WAIT_UNTIL_FREE); /* - * Set RELEASE_OK flag, to make sure we get woken up as soon as the - * lock is released. + * Clear LW_FLAG_WAKE_IN_PROGRESS flag, to make sure we get woken up + * as soon as the lock is released. */ - pg_atomic_fetch_or_u32(&lock->state, LW_FLAG_RELEASE_OK); + pg_atomic_fetch_and_u32(&lock->state, ~LW_FLAG_WAKE_IN_PROGRESS); /* * We're now guaranteed to be woken up if necessary. Recheck the lock @@ -1787,25 +1757,18 @@ LWLockUpdateVar(LWLock *lock, pg_atomic_uint64 *valptr, uint64 val) /* - * Stop treating lock as held by current backend. - * - * This is the code that can be shared between actually releasing a lock - * (LWLockRelease()) and just not tracking ownership of the lock anymore - * without releasing the lock (LWLockDisown()). - * - * Returns the mode in which the lock was held by the current backend. - * - * NB: This does not call RESUME_INTERRUPTS(), but leaves that responsibility - * of the caller. + * LWLockRelease - release a previously acquired lock * * NB: This will leave lock->owner pointing to the current backend (if * LOCK_DEBUG is set). This is somewhat intentional, as it makes it easier to * debug cases of missing wakeups during lock release. */ -static inline LWLockMode -LWLockDisownInternal(LWLock *lock) +void +LWLockRelease(LWLock *lock) { LWLockMode mode; + uint32 oldstate; + bool check_waiters; int i; /* @@ -1825,18 +1788,7 @@ LWLockDisownInternal(LWLock *lock) for (; i < num_held_lwlocks; i++) held_lwlocks[i] = held_lwlocks[i + 1]; - return mode; -} - -/* - * Helper function to release lock, shared between LWLockRelease() and - * LWLockReleaseDisowned(). - */ -static void -LWLockReleaseInternal(LWLock *lock, LWLockMode mode) -{ - uint32 oldstate; - bool check_waiters; + PRINT_LWDEBUG("LWLockRelease", lock, mode); /* * Release my hold on lock, after that it can immediately be acquired by @@ -1854,11 +1806,11 @@ LWLockReleaseInternal(LWLock *lock, LWLockMode mode) TRACE_POSTGRESQL_LWLOCK_RELEASE(T_NAME(lock)); /* - * We're still waiting for backends to get scheduled, don't wake them up - * again. + * Check if we're still waiting for backends to get scheduled, if so, + * don't wake them up again. */ - if ((oldstate & (LW_FLAG_HAS_WAITERS | LW_FLAG_RELEASE_OK)) == - (LW_FLAG_HAS_WAITERS | LW_FLAG_RELEASE_OK) && + if ((oldstate & LW_FLAG_HAS_WAITERS) && + !(oldstate & LW_FLAG_WAKE_IN_PROGRESS) && (oldstate & LW_LOCK_MASK) == 0) check_waiters = true; else @@ -1874,38 +1826,6 @@ LWLockReleaseInternal(LWLock *lock, LWLockMode mode) LOG_LWDEBUG("LWLockRelease", lock, "releasing waiters"); LWLockWakeup(lock); } -} - - -/* - * Stop treating lock as held by current backend. - * - * After calling this function it's the callers responsibility to ensure that - * the lock gets released (via LWLockReleaseDisowned()), even in case of an - * error. This only is desirable if the lock is going to be released in a - * different process than the process that acquired it. - */ -void -LWLockDisown(LWLock *lock) -{ - LWLockDisownInternal(lock); - - RESUME_INTERRUPTS(); -} - -/* - * LWLockRelease - release a previously acquired lock - */ -void -LWLockRelease(LWLock *lock) -{ - LWLockMode mode; - - mode = LWLockDisownInternal(lock); - - PRINT_LWDEBUG("LWLockRelease", lock, mode); - - LWLockReleaseInternal(lock, mode); /* * Now okay to allow cancel/die interrupts. @@ -1913,15 +1833,6 @@ LWLockRelease(LWLock *lock) RESUME_INTERRUPTS(); } -/* - * Release lock previously disowned with LWLockDisown(). - */ -void -LWLockReleaseDisowned(LWLock *lock, LWLockMode mode) -{ - LWLockReleaseInternal(lock, mode); -} - /* * LWLockReleaseClearVar - release a previously acquired lock, reset variable */ @@ -1946,6 +1857,10 @@ LWLockReleaseClearVar(LWLock *lock, pg_atomic_uint64 *valptr, uint64 val) * unchanged by this operation. This is necessary since InterruptHoldoffCount * has been set to an appropriate level earlier in error recovery. We could * decrement it below zero if we allow it to drop for each released lock! + * + * Note that this function must be safe to call even before the LWLock + * subsystem has been initialized (e.g., during early startup failures). + * In that case, num_held_lwlocks will be 0 and we do nothing. */ void LWLockReleaseAll(void) @@ -1956,24 +1871,11 @@ LWLockReleaseAll(void) LWLockRelease(held_lwlocks[num_held_lwlocks - 1].lock); } -} - -/* - * ForEachLWLockHeldByMe - run a callback for each held lock - * - * This is meant as debug support only. - */ -void -ForEachLWLockHeldByMe(void (*callback) (LWLock *, LWLockMode, void *), - void *context) -{ - int i; - - for (i = 0; i < num_held_lwlocks; i++) - callback(held_lwlocks[i].lock, held_lwlocks[i].mode, context); + Assert(num_held_lwlocks == 0); } + /* * LWLockHeldByMe - test whether my process holds a lock in any mode * diff --git a/src/backend/storage/lmgr/meson.build b/src/backend/storage/lmgr/meson.build index a5490c1047fae..c961ddfbb7116 100644 --- a/src/backend/storage/lmgr/meson.build +++ b/src/backend/storage/lmgr/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'condition_variable.c', diff --git a/src/backend/storage/lmgr/predicate.c b/src/backend/storage/lmgr/predicate.c index d82114ffca165..0ae85b7d5b4be 100644 --- a/src/backend/storage/lmgr/predicate.c +++ b/src/backend/storage/lmgr/predicate.c @@ -140,7 +140,7 @@ * SLRU per-bank locks * - Protects SerialSlruCtl * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -152,10 +152,6 @@ /* * INTERFACE ROUTINES * - * housekeeping for setting up shared memory predicate lock structures - * PredicateLockShmemInit(void) - * PredicateLockShmemSize(void) - * * predicate lock reporting * GetPredicateLockStatusData(void) * PageIsPredicateLocked(Relation relation, BlockNumber blkno) @@ -168,7 +164,7 @@ * PredicateLockRelation(Relation relation, Snapshot snapshot) * PredicateLockPage(Relation relation, BlockNumber blkno, * Snapshot snapshot) - * PredicateLockTID(Relation relation, ItemPointer tid, Snapshot snapshot, + * PredicateLockTID(Relation relation, const ItemPointerData *tid, Snapshot snapshot, * TransactionId tuple_xid) * PredicateLockPageSplit(Relation relation, BlockNumber oldblkno, * BlockNumber newblkno) @@ -180,7 +176,7 @@ * conflict detection (may also trigger rollback) * CheckForSerializableConflictOut(Relation relation, TransactionId xid, * Snapshot snapshot) - * CheckForSerializableConflictIn(Relation relation, ItemPointer tid, + * CheckForSerializableConflictIn(Relation relation, const ItemPointerData *tid, * BlockNumber blkno) * CheckTableForSerializableConflictIn(Relation relation) * @@ -190,8 +186,8 @@ * two-phase commit support * AtPrepare_PredicateLocks(void); * PostPrepare_PredicateLocks(TransactionId xid); - * PredicateLockTwoPhaseFinish(TransactionId xid, bool isCommit); - * predicatelock_twophase_recover(TransactionId xid, uint16 info, + * PredicateLockTwoPhaseFinish(FullTransactionId fxid, bool isCommit); + * predicatelock_twophase_recover(FullTransactionId fxid, uint16 info, * void *recdata, uint32 len); */ @@ -211,9 +207,12 @@ #include "storage/predicate_internals.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/guc_hooks.h" #include "utils/rel.h" #include "utils/snapmgr.h" +#include "utils/wait_event.h" /* Uncomment the next line to test the graceful degradation code. */ /* #define TEST_SUMMARIZE_SERIAL */ @@ -321,9 +320,12 @@ /* * The SLRU buffer area through which we access the old xids. */ -static SlruCtlData SerialSlruCtlData; +static bool SerialPagePrecedesLogically(int64 page1, int64 page2); +static int serial_errdetail_for_io_error(const void *opaque_data); + +static SlruDesc SerialSlruDesc; -#define SerialSlruCtl (&SerialSlruCtlData) +#define SerialSlruCtl (&SerialSlruDesc) #define SERIAL_PAGESIZE BLCKSZ #define SERIAL_ENTRYSIZE sizeof(SerCommitSeqNo) @@ -383,6 +385,17 @@ int max_predicate_locks_per_page; /* in guc_tables.c */ */ static PredXactList PredXact; +static void PredicateLockShmemRequest(void *arg); +static void PredicateLockShmemInit(void *arg); +static void PredicateLockShmemAttach(void *arg); + +const ShmemCallbacks PredicateLockShmemCallbacks = { + .request_fn = PredicateLockShmemRequest, + .init_fn = PredicateLockShmemInit, + .attach_fn = PredicateLockShmemAttach, +}; + + /* * This provides a pool of RWConflict data elements to use in conflict lists * between transactions. @@ -430,6 +443,8 @@ static bool MyXactDidWrite = false; */ static SERIALIZABLEXACT *SavedSerializableXact = InvalidSerializableXact; +static int64 max_serializable_xacts; + /* local functions */ static SERIALIZABLEXACT *CreatePredXact(void); @@ -441,13 +456,12 @@ static void SetPossibleUnsafeConflict(SERIALIZABLEXACT *roXact, SERIALIZABLEXACT static void ReleaseRWConflict(RWConflict conflict); static void FlagSxactUnsafe(SERIALIZABLEXACT *sxact); -static bool SerialPagePrecedesLogically(int64 page1, int64 page2); -static void SerialInit(void); static void SerialAdd(TransactionId xid, SerCommitSeqNo minConflictCommitSeqNo); static SerCommitSeqNo SerialGetMinConflictCommitSeqNo(TransactionId xid); static void SerialSetActiveSerXmin(TransactionId xid); static uint32 predicatelock_hash(const void *key, Size keysize); + static void SummarizeOldestCommittedSxact(void); static Snapshot GetSafeSnapshot(Snapshot origSnapshot); static Snapshot GetSerializableTransactionSnapshotInt(Snapshot snapshot, @@ -742,6 +756,14 @@ SerialPagePrecedesLogically(int64 page1, int64 page2) TransactionIdPrecedes(xid1, xid2 + SERIAL_ENTRIESPERPAGE - 1)); } +static int +serial_errdetail_for_io_error(const void *opaque_data) +{ + TransactionId xid = *(const TransactionId *) opaque_data; + + return errdetail("Could not access serializable CSN of transaction %u.", xid); +} + #ifdef USE_ASSERT_CHECKING static void SerialPagePrecedesLogicallyUnitTests(void) @@ -799,47 +821,6 @@ SerialPagePrecedesLogicallyUnitTests(void) } #endif -/* - * Initialize for the tracking of old serializable committed xids. - */ -static void -SerialInit(void) -{ - bool found; - - /* - * Set up SLRU management of the pg_serial data. - */ - SerialSlruCtl->PagePrecedes = SerialPagePrecedesLogically; - SimpleLruInit(SerialSlruCtl, "serializable", - serializable_buffers, 0, "pg_serial", - LWTRANCHE_SERIAL_BUFFER, LWTRANCHE_SERIAL_SLRU, - SYNC_HANDLER_NONE, false); -#ifdef USE_ASSERT_CHECKING - SerialPagePrecedesLogicallyUnitTests(); -#endif - SlruPagePrecedesUnitTests(SerialSlruCtl, SERIAL_ENTRIESPERPAGE); - - /* - * Create or attach to the SerialControl structure. - */ - serialControl = (SerialControl) - ShmemInitStruct("SerialControlData", sizeof(SerialControlData), &found); - - Assert(found == IsUnderPostmaster); - if (!found) - { - /* - * Set control information to reflect empty SLRU. - */ - LWLockAcquire(SerialControlLock, LW_EXCLUSIVE); - serialControl->headPage = -1; - serialControl->headXid = InvalidTransactionId; - serialControl->tailXid = InvalidTransactionId; - LWLockRelease(SerialControlLock); - } -} - /* * GUC check_hook for serializable_buffers */ @@ -930,7 +911,7 @@ SerialAdd(TransactionId xid, SerCommitSeqNo minConflictCommitSeqNo) else { LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = SimpleLruReadPage(SerialSlruCtl, targetPage, true, xid); + slotno = SimpleLruReadPage(SerialSlruCtl, targetPage, true, &xid); } SerialValue(slotno, xid) = minConflictCommitSeqNo; @@ -974,7 +955,7 @@ SerialGetMinConflictCommitSeqNo(TransactionId xid) * but will return with that lock held, which must then be released. */ slotno = SimpleLruReadPage_ReadOnly(SerialSlruCtl, - SerialPage(xid), xid); + SerialPage(xid), &xid); val = SerialValue(slotno, xid); LWLockRelease(SimpleLruGetBankLock(SerialSlruCtl, SerialPage(xid))); return val; @@ -1132,165 +1113,79 @@ CheckPointPredicate(void) /*------------------------------------------------------------------------*/ /* - * PredicateLockShmemInit -- Initialize the predicate locking data structures. - * - * This is called from CreateSharedMemoryAndSemaphores(), which see for - * more comments. In the normal postmaster case, the shared hash tables - * are created here. Backends inherit the pointers - * to the shared tables via fork(). In the EXEC_BACKEND case, each - * backend re-executes this code to obtain pointers to the already existing - * shared hash tables. + * PredicateLockShmemRequest -- Register the predicate locking data structures. */ -void -PredicateLockShmemInit(void) +static void +PredicateLockShmemRequest(void *arg) { - HASHCTL info; - long max_table_size; - Size requestSize; - bool found; - -#ifndef EXEC_BACKEND - Assert(!IsUnderPostmaster); -#endif - - /* - * Compute size of predicate lock target hashtable. Note these - * calculations must agree with PredicateLockShmemSize! - */ - max_table_size = NPREDICATELOCKTARGETENTS(); + int64 max_predicate_lock_targets; + int64 max_predicate_locks; + int64 max_rw_conflicts; /* - * Allocate hash table for PREDICATELOCKTARGET structs. This stores + * Register hash table for PREDICATELOCKTARGET structs. This stores * per-predicate-lock-target information. */ - info.keysize = sizeof(PREDICATELOCKTARGETTAG); - info.entrysize = sizeof(PREDICATELOCKTARGET); - info.num_partitions = NUM_PREDICATELOCK_PARTITIONS; - - PredicateLockTargetHash = ShmemInitHash("PREDICATELOCKTARGET hash", - max_table_size, - max_table_size, - &info, - HASH_ELEM | HASH_BLOBS | - HASH_PARTITION | HASH_FIXED_SIZE); - - /* - * Reserve a dummy entry in the hash table; we use it to make sure there's - * always one entry available when we need to split or combine a page, - * because running out of space there could mean aborting a - * non-serializable transaction. - */ - if (!IsUnderPostmaster) - { - (void) hash_search(PredicateLockTargetHash, &ScratchTargetTag, - HASH_ENTER, &found); - Assert(!found); - } + max_predicate_lock_targets = NPREDICATELOCKTARGETENTS(); - /* Pre-calculate the hash and partition lock of the scratch entry */ - ScratchTargetTagHash = PredicateLockTargetTagHashCode(&ScratchTargetTag); - ScratchPartitionLock = PredicateLockHashPartitionLock(ScratchTargetTagHash); + ShmemRequestHash(.name = "PREDICATELOCKTARGET hash", + .nelems = max_predicate_lock_targets, + .ptr = &PredicateLockTargetHash, + .hash_info.keysize = sizeof(PREDICATELOCKTARGETTAG), + .hash_info.entrysize = sizeof(PREDICATELOCKTARGET), + .hash_info.num_partitions = NUM_PREDICATELOCK_PARTITIONS, + .hash_flags = HASH_ELEM | HASH_BLOBS | HASH_PARTITION | HASH_FIXED_SIZE, + ); /* * Allocate hash table for PREDICATELOCK structs. This stores per * xact-lock-of-a-target information. + * + * Assume an average of 2 xacts per target. */ - info.keysize = sizeof(PREDICATELOCKTAG); - info.entrysize = sizeof(PREDICATELOCK); - info.hash = predicatelock_hash; - info.num_partitions = NUM_PREDICATELOCK_PARTITIONS; - - /* Assume an average of 2 xacts per target */ - max_table_size *= 2; - - PredicateLockHash = ShmemInitHash("PREDICATELOCK hash", - max_table_size, - max_table_size, - &info, - HASH_ELEM | HASH_FUNCTION | - HASH_PARTITION | HASH_FIXED_SIZE); - - /* - * Compute size for serializable transaction hashtable. Note these - * calculations must agree with PredicateLockShmemSize! - */ - max_table_size = (MaxBackends + max_prepared_xacts); + max_predicate_locks = max_predicate_lock_targets * 2; + + ShmemRequestHash(.name = "PREDICATELOCK hash", + .nelems = max_predicate_locks, + .ptr = &PredicateLockHash, + .hash_info.keysize = sizeof(PREDICATELOCKTAG), + .hash_info.entrysize = sizeof(PREDICATELOCK), + .hash_info.hash = predicatelock_hash, + .hash_info.num_partitions = NUM_PREDICATELOCK_PARTITIONS, + .hash_flags = HASH_ELEM | HASH_FUNCTION | HASH_PARTITION | HASH_FIXED_SIZE, + ); /* - * Allocate a list to hold information on transactions participating in - * predicate locking. + * Compute size for serializable transaction hashtable. * * Assume an average of 10 predicate locking transactions per backend. * This allows aggressive cleanup while detail is present before data must * be summarized for storage in SLRU and the "dummy" transaction. */ - max_table_size *= 10; - - requestSize = add_size(PredXactListDataSize, - (mul_size((Size) max_table_size, - sizeof(SERIALIZABLEXACT)))); + max_serializable_xacts = (MaxBackends + max_prepared_xacts) * 10; - PredXact = ShmemInitStruct("PredXactList", - requestSize, - &found); - Assert(found == IsUnderPostmaster); - if (!found) - { - int i; - - /* clean everything, both the header and the element */ - memset(PredXact, 0, requestSize); - - dlist_init(&PredXact->availableList); - dlist_init(&PredXact->activeList); - PredXact->SxactGlobalXmin = InvalidTransactionId; - PredXact->SxactGlobalXminCount = 0; - PredXact->WritableSxactCount = 0; - PredXact->LastSxactCommitSeqNo = FirstNormalSerCommitSeqNo - 1; - PredXact->CanPartialClearThrough = 0; - PredXact->HavePartialClearedThrough = 0; - PredXact->element - = (SERIALIZABLEXACT *) ((char *) PredXact + PredXactListDataSize); - /* Add all elements to available list, clean. */ - for (i = 0; i < max_table_size; i++) - { - LWLockInitialize(&PredXact->element[i].perXactPredicateListLock, - LWTRANCHE_PER_XACT_PREDICATE_LIST); - dlist_push_tail(&PredXact->availableList, &PredXact->element[i].xactLink); - } - PredXact->OldCommittedSxact = CreatePredXact(); - SetInvalidVirtualTransactionId(PredXact->OldCommittedSxact->vxid); - PredXact->OldCommittedSxact->prepareSeqNo = 0; - PredXact->OldCommittedSxact->commitSeqNo = 0; - PredXact->OldCommittedSxact->SeqNo.lastCommitBeforeSnapshot = 0; - dlist_init(&PredXact->OldCommittedSxact->outConflicts); - dlist_init(&PredXact->OldCommittedSxact->inConflicts); - dlist_init(&PredXact->OldCommittedSxact->predicateLocks); - dlist_node_init(&PredXact->OldCommittedSxact->finishedLink); - dlist_init(&PredXact->OldCommittedSxact->possibleUnsafeConflicts); - PredXact->OldCommittedSxact->topXid = InvalidTransactionId; - PredXact->OldCommittedSxact->finishedBefore = InvalidTransactionId; - PredXact->OldCommittedSxact->xmin = InvalidTransactionId; - PredXact->OldCommittedSxact->flags = SXACT_FLAG_COMMITTED; - PredXact->OldCommittedSxact->pid = 0; - PredXact->OldCommittedSxact->pgprocno = INVALID_PROC_NUMBER; - } - /* This never changes, so let's keep a local copy. */ - OldCommittedSxact = PredXact->OldCommittedSxact; + /* + * Register a list to hold information on transactions participating in + * predicate locking. + */ + ShmemRequestStruct(.name = "PredXactList", + .size = add_size(PredXactListDataSize, + (mul_size((Size) max_serializable_xacts, + sizeof(SERIALIZABLEXACT)))), + .ptr = (void **) &PredXact, + ); /* - * Allocate hash table for SERIALIZABLEXID structs. This stores per-xid + * Register hash table for SERIALIZABLEXID structs. This stores per-xid * information for serializable transactions which have accessed data. */ - info.keysize = sizeof(SERIALIZABLEXIDTAG); - info.entrysize = sizeof(SERIALIZABLEXID); - - SerializableXidHash = ShmemInitHash("SERIALIZABLEXID hash", - max_table_size, - max_table_size, - &info, - HASH_ELEM | HASH_BLOBS | - HASH_FIXED_SIZE); + ShmemRequestHash(.name = "SERIALIZABLEXID hash", + .nelems = max_serializable_xacts, + .ptr = &SerializableXidHash, + .hash_info.keysize = sizeof(SERIALIZABLEXIDTAG), + .hash_info.entrysize = sizeof(SERIALIZABLEXID), + .hash_flags = HASH_ELEM | HASH_BLOBS | HASH_FIXED_SIZE, + ); /* * Allocate space for tracking rw-conflicts in lists attached to the @@ -1303,105 +1198,141 @@ PredicateLockShmemInit(void) * occasional transactions canceled when trying to flag conflicts. That's * probably OK. */ - max_table_size *= 5; - - requestSize = RWConflictPoolHeaderDataSize + - mul_size((Size) max_table_size, - RWConflictDataSize); - - RWConflictPool = ShmemInitStruct("RWConflictPool", - requestSize, - &found); - Assert(found == IsUnderPostmaster); - if (!found) - { - int i; - - /* clean everything, including the elements */ - memset(RWConflictPool, 0, requestSize); + max_rw_conflicts = max_serializable_xacts * 5; - dlist_init(&RWConflictPool->availableList); - RWConflictPool->element = (RWConflict) ((char *) RWConflictPool + - RWConflictPoolHeaderDataSize); - /* Add all elements to available list, clean. */ - for (i = 0; i < max_table_size; i++) - { - dlist_push_tail(&RWConflictPool->availableList, - &RWConflictPool->element[i].outLink); - } - } + ShmemRequestStruct(.name = "RWConflictPool", + .size = RWConflictPoolHeaderDataSize + mul_size((Size) max_rw_conflicts, + RWConflictDataSize), + .ptr = (void **) &RWConflictPool, + ); - /* - * Create or attach to the header for the list of finished serializable - * transactions. - */ - FinishedSerializableTransactions = (dlist_head *) - ShmemInitStruct("FinishedSerializableTransactions", - sizeof(dlist_head), - &found); - Assert(found == IsUnderPostmaster); - if (!found) - dlist_init(FinishedSerializableTransactions); + ShmemRequestStruct(.name = "FinishedSerializableTransactions", + .size = sizeof(dlist_head), + .ptr = (void **) &FinishedSerializableTransactions, + ); /* * Initialize the SLRU storage for old committed serializable * transactions. */ - SerialInit(); -} + SimpleLruRequest(.desc = &SerialSlruDesc, + .name = "serializable", + .Dir = "pg_serial", + .long_segment_names = false, -/* - * Estimate shared-memory space used for predicate lock table - */ -Size -PredicateLockShmemSize(void) -{ - Size size = 0; - long max_table_size; + .nslots = serializable_buffers, + + .sync_handler = SYNC_HANDLER_NONE, + .PagePrecedes = SerialPagePrecedesLogically, + .errdetail_for_io_error = serial_errdetail_for_io_error, + + .buffer_tranche_id = LWTRANCHE_SERIAL_BUFFER, + .bank_tranche_id = LWTRANCHE_SERIAL_SLRU, + ); +#ifdef USE_ASSERT_CHECKING + SerialPagePrecedesLogicallyUnitTests(); +#endif - /* predicate lock target hash table */ - max_table_size = NPREDICATELOCKTARGETENTS(); - size = add_size(size, hash_estimate_size(max_table_size, - sizeof(PREDICATELOCKTARGET))); + ShmemRequestStruct(.name = "SerialControlData", + .size = sizeof(SerialControlData), + .ptr = (void **) &serialControl, + ); +} - /* predicate lock hash table */ - max_table_size *= 2; - size = add_size(size, hash_estimate_size(max_table_size, - sizeof(PREDICATELOCK))); +static void +PredicateLockShmemInit(void *arg) +{ + int max_rw_conflicts; + bool found; /* - * Since NPREDICATELOCKTARGETENTS is only an estimate, add 10% safety - * margin. + * Reserve a dummy entry in the hash table; we use it to make sure there's + * always one entry available when we need to split or combine a page, + * because running out of space there could mean aborting a + * non-serializable transaction. */ - size = add_size(size, size / 10); + (void) hash_search(PredicateLockTargetHash, &ScratchTargetTag, + HASH_ENTER, &found); + Assert(!found); - /* transaction list */ - max_table_size = MaxBackends + max_prepared_xacts; - max_table_size *= 10; - size = add_size(size, PredXactListDataSize); - size = add_size(size, mul_size((Size) max_table_size, - sizeof(SERIALIZABLEXACT))); + dlist_init(&PredXact->availableList); + dlist_init(&PredXact->activeList); + PredXact->SxactGlobalXmin = InvalidTransactionId; + PredXact->SxactGlobalXminCount = 0; + PredXact->WritableSxactCount = 0; + PredXact->LastSxactCommitSeqNo = FirstNormalSerCommitSeqNo - 1; + PredXact->CanPartialClearThrough = 0; + PredXact->HavePartialClearedThrough = 0; + PredXact->element + = (SERIALIZABLEXACT *) ((char *) PredXact + PredXactListDataSize); + /* Add all elements to available list, clean. */ + for (int i = 0; i < max_serializable_xacts; i++) + { + LWLockInitialize(&PredXact->element[i].perXactPredicateListLock, + LWTRANCHE_PER_XACT_PREDICATE_LIST); + dlist_push_tail(&PredXact->availableList, &PredXact->element[i].xactLink); + } + PredXact->OldCommittedSxact = CreatePredXact(); + SetInvalidVirtualTransactionId(PredXact->OldCommittedSxact->vxid); + PredXact->OldCommittedSxact->prepareSeqNo = 0; + PredXact->OldCommittedSxact->commitSeqNo = 0; + PredXact->OldCommittedSxact->SeqNo.lastCommitBeforeSnapshot = 0; + dlist_init(&PredXact->OldCommittedSxact->outConflicts); + dlist_init(&PredXact->OldCommittedSxact->inConflicts); + dlist_init(&PredXact->OldCommittedSxact->predicateLocks); + dlist_node_init(&PredXact->OldCommittedSxact->finishedLink); + dlist_init(&PredXact->OldCommittedSxact->possibleUnsafeConflicts); + PredXact->OldCommittedSxact->topXid = InvalidTransactionId; + PredXact->OldCommittedSxact->finishedBefore = InvalidTransactionId; + PredXact->OldCommittedSxact->xmin = InvalidTransactionId; + PredXact->OldCommittedSxact->flags = SXACT_FLAG_COMMITTED; + PredXact->OldCommittedSxact->pid = 0; + PredXact->OldCommittedSxact->pgprocno = INVALID_PROC_NUMBER; + + /* Initialize the rw-conflict pool */ + dlist_init(&RWConflictPool->availableList); + RWConflictPool->element = (RWConflict) ((char *) RWConflictPool + + RWConflictPoolHeaderDataSize); + + max_rw_conflicts = max_serializable_xacts * 5; + + /* Add all elements to available list, clean. */ + for (int i = 0; i < max_rw_conflicts; i++) + { + dlist_push_tail(&RWConflictPool->availableList, + &RWConflictPool->element[i].outLink); + } - /* transaction xid table */ - size = add_size(size, hash_estimate_size(max_table_size, - sizeof(SERIALIZABLEXID))); + /* Initialize the list of finished serializable transactions */ + dlist_init(FinishedSerializableTransactions); - /* rw-conflict pool */ - max_table_size *= 5; - size = add_size(size, RWConflictPoolHeaderDataSize); - size = add_size(size, mul_size((Size) max_table_size, - RWConflictDataSize)); + /* Initialize SerialControl to reflect empty SLRU. */ + LWLockAcquire(SerialControlLock, LW_EXCLUSIVE); + serialControl->headPage = -1; + serialControl->headXid = InvalidTransactionId; + serialControl->tailXid = InvalidTransactionId; + LWLockRelease(SerialControlLock); - /* Head for list of finished serializable transactions. */ - size = add_size(size, sizeof(dlist_head)); + SlruPagePrecedesUnitTests(SerialSlruCtl, SERIAL_ENTRIESPERPAGE); - /* Shared memory structures for SLRU tracking of old committed xids. */ - size = add_size(size, sizeof(SerialControlData)); - size = add_size(size, SimpleLruShmemSize(serializable_buffers, 0)); + /* This never changes, so let's keep a local copy. */ + OldCommittedSxact = PredXact->OldCommittedSxact; - return size; + /* Pre-calculate the hash and partition lock of the scratch entry */ + ScratchTargetTagHash = PredicateLockTargetTagHashCode(&ScratchTargetTag); + ScratchPartitionLock = PredicateLockHashPartitionLock(ScratchTargetTagHash); } +static void +PredicateLockShmemAttach(void *arg) +{ + /* This never changes, so let's keep a local copy. */ + OldCommittedSxact = PredXact->OldCommittedSxact; + + /* Pre-calculate the hash and partition lock of the scratch entry */ + ScratchTargetTagHash = PredicateLockTargetTagHashCode(&ScratchTargetTag); + ScratchPartitionLock = PredicateLockHashPartitionLock(ScratchTargetTagHash); +} /* * Compute the hash code associated with a PREDICATELOCKTAG. @@ -1451,7 +1382,7 @@ GetPredicateLockStatusData(void) HASH_SEQ_STATUS seqstat; PREDICATELOCK *predlock; - data = (PredicateLockData *) palloc(sizeof(PredicateLockData)); + data = palloc_object(PredicateLockData); /* * To ensure consistency, take simultaneous locks on all partition locks @@ -1464,10 +1395,8 @@ GetPredicateLockStatusData(void) /* Get number of locks and allocate appropriately-sized arrays. */ els = hash_get_num_entries(PredicateLockHash); data->nelements = els; - data->locktags = (PREDICATELOCKTARGETTAG *) - palloc(sizeof(PREDICATELOCKTARGETTAG) * els); - data->xacts = (SERIALIZABLEXACT *) - palloc(sizeof(SERIALIZABLEXACT) * els); + data->locktags = palloc_array(PREDICATELOCKTARGETTAG, els); + data->xacts = palloc_array(SERIALIZABLEXACT, els); /* Scan through PredicateLockHash and copy contents */ @@ -2618,7 +2547,7 @@ PredicateLockPage(Relation relation, BlockNumber blkno, Snapshot snapshot) * Skip if this is a temporary table. */ void -PredicateLockTID(Relation relation, ItemPointer tid, Snapshot snapshot, +PredicateLockTID(Relation relation, const ItemPointerData *tid, Snapshot snapshot, TransactionId tuple_xid) { PREDICATELOCKTARGETTAG tag; @@ -4333,7 +4262,7 @@ CheckTargetForConflictsIn(PREDICATELOCKTARGETTAG *targettag) * tuple itself. */ void -CheckForSerializableConflictIn(Relation relation, ItemPointer tid, BlockNumber blkno) +CheckForSerializableConflictIn(Relation relation, const ItemPointerData *tid, BlockNumber blkno) { PREDICATELOCKTARGETTAG targettag; @@ -4856,7 +4785,7 @@ AtPrepare_PredicateLocks(void) * anyway. We only need to clean up our local state. */ void -PostPrepare_PredicateLocks(TransactionId xid) +PostPrepare_PredicateLocks(FullTransactionId fxid) { if (MySerializableXact == InvalidSerializableXact) return; @@ -4879,12 +4808,12 @@ PostPrepare_PredicateLocks(TransactionId xid) * commits or aborts. */ void -PredicateLockTwoPhaseFinish(TransactionId xid, bool isCommit) +PredicateLockTwoPhaseFinish(FullTransactionId fxid, bool isCommit) { SERIALIZABLEXID *sxid; SERIALIZABLEXIDTAG sxidtag; - sxidtag.xid = xid; + sxidtag.xid = XidFromFullTransactionId(fxid); LWLockAcquire(SerializableXactHashLock, LW_SHARED); sxid = (SERIALIZABLEXID *) @@ -4906,10 +4835,11 @@ PredicateLockTwoPhaseFinish(TransactionId xid, bool isCommit) * Re-acquire a predicate lock belonging to a transaction that was prepared. */ void -predicatelock_twophase_recover(TransactionId xid, uint16 info, +predicatelock_twophase_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { TwoPhasePredicateRecord *record; + TransactionId xid = XidFromFullTransactionId(fxid); Assert(len == sizeof(TwoPhasePredicateRecord)); @@ -4987,7 +4917,7 @@ predicatelock_twophase_recover(TransactionId xid, uint16 info, HASH_ENTER, &found); Assert(sxid != NULL); Assert(!found); - sxid->myXact = (SERIALIZABLEXACT *) sxact; + sxid->myXact = sxact; /* * Update global xmin. Note that this is a special case compared to diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32cb..1ac25068d62f2 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -3,7 +3,7 @@ * proc.c * routines to manage per-process shared memory data structure * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -33,9 +33,11 @@ #include #include +#include "access/clog.h" #include "access/transam.h" #include "access/twophase.h" #include "access/xlogutils.h" +#include "access/xlogwait.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" @@ -50,8 +52,11 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "storage/subsystems.h" +#include "utils/injection_point.h" #include "utils/timeout.h" #include "utils/timestamp.h" +#include "utils/wait_event.h" /* GUC variables */ int DeadlockTimeout = 1000; @@ -60,26 +65,29 @@ int LockTimeout = 0; int IdleInTransactionSessionTimeout = 0; int TransactionTimeout = 0; int IdleSessionTimeout = 0; -bool log_lock_waits = false; +bool log_lock_waits = true; /* Pointer to this process's PGPROC struct, if any */ PGPROC *MyProc = NULL; -/* - * This spinlock protects the freelist of recycled PGPROC structures. - * We cannot use an LWLock because the LWLock manager depends on already - * having a PGPROC and a wait semaphore! But these structures are touched - * relatively infrequently (only at backend startup or shutdown) and not for - * very long, so a spinlock is okay. - */ -NON_EXEC_STATIC slock_t *ProcStructLock = NULL; - /* Pointers to shared-memory structures */ PROC_HDR *ProcGlobal = NULL; +static void *AllProcsShmemPtr; +static void *FastPathLockArrayShmemPtr; NON_EXEC_STATIC PGPROC *AuxiliaryProcs = NULL; PGPROC *PreparedXactProcs = NULL; -static DeadLockState deadlock_state = DS_NOT_YET_CHECKED; +static void ProcGlobalShmemRequest(void *arg); +static void ProcGlobalShmemInit(void *arg); + +const ShmemCallbacks ProcGlobalShmemCallbacks = { + .request_fn = ProcGlobalShmemRequest, + .init_fn = ProcGlobalShmemInit, +}; + +static uint32 TotalProcs; +static size_t ProcGlobalAllProcsShmemSize; +static size_t FastPathLockArrayShmemSize; /* Is a deadlock check pending? */ static volatile sig_atomic_t got_deadlock_timeout; @@ -87,36 +95,16 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static void CheckDeadLock(void); - - -/* - * Report shared-memory space needed by PGPROC. - */ -static Size -PGProcShmemSize(void) -{ - Size size = 0; - Size TotalProcs = - add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); - - size = add_size(size, mul_size(TotalProcs, sizeof(PGPROC))); - size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->xids))); - size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->subxidStates))); - size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->statusFlags))); +static DeadLockState CheckDeadLock(void); - return size; -} /* - * Report shared-memory space needed by Fast-Path locks. + * Calculate shared-memory space needed by Fast-Path locks. */ static Size -FastPathLockShmemSize(void) +CalculateFastPathLockShmemSize(void) { Size size = 0; - Size TotalProcs = - add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); Size fpLockBitsSize, fpRelIdSize; @@ -129,29 +117,14 @@ FastPathLockShmemSize(void) size = add_size(size, mul_size(TotalProcs, (fpLockBitsSize + fpRelIdSize))); - return size; -} - -/* - * Report shared-memory space needed by InitProcGlobal. - */ -Size -ProcGlobalShmemSize(void) -{ - Size size = 0; - - /* ProcGlobal */ - size = add_size(size, sizeof(PROC_HDR)); - size = add_size(size, sizeof(slock_t)); - - size = add_size(size, PGProcShmemSize()); - size = add_size(size, FastPathLockShmemSize()); + Assert(TotalProcs > 0); + Assert(size > 0); return size; } /* - * Report number of semaphores needed by InitProcGlobal. + * Report number of semaphores needed by ProcGlobalShmemInit. */ int ProcGlobalSemas(void) @@ -164,7 +137,67 @@ ProcGlobalSemas(void) } /* - * InitProcGlobal - + * ProcGlobalShmemRequest + * Register shared memory needs. + * + * This is called during postmaster or standalone backend startup, and also + * during backend startup in EXEC_BACKEND mode. + */ +static void +ProcGlobalShmemRequest(void *arg) +{ + Size size; + + /* + * Reserve all the PGPROC structures we'll need. There are six separate + * consumers: (1) normal backends, (2) autovacuum workers and special + * workers, (3) background workers, (4) walsenders, (5) auxiliary + * processes, and (6) prepared transactions. (For largely-historical + * reasons, we combine autovacuum and special workers into one category + * with a single freelist.) Each PGPROC structure is dedicated to exactly + * one of these purposes, and they do not move between groups. + */ + TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + size = 0; + size = add_size(size, mul_size(TotalProcs, sizeof(PGPROC))); + size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->xids))); + size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->subxidStates))); + size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->statusFlags))); + ProcGlobalAllProcsShmemSize = size; + ShmemRequestStruct(.name = "PGPROC structures", + .size = ProcGlobalAllProcsShmemSize, + .ptr = &AllProcsShmemPtr, + ); + + if (!IsUnderPostmaster) + size = FastPathLockArrayShmemSize = CalculateFastPathLockShmemSize(); + else + size = SHMEM_ATTACH_UNKNOWN_SIZE; + ShmemRequestStruct(.name = "Fast-Path Lock Array", + .size = size, + .ptr = &FastPathLockArrayShmemPtr, + ); + + /* + * ProcGlobal is registered here in .ptr as usual, but it needs to be + * propagated specially in EXEC_BACKEND mode, because ProcGlobal needs to + * be accessed early at backend startup, before ShmemAttachRequested() has + * been called. + */ + ShmemRequestStruct(.name = "Proc Header", + .size = sizeof(PROC_HDR), + .ptr = (void **) &ProcGlobal, + ); + + /* Let the semaphore implementation register its shared memory needs */ + PGSemaphoreShmemRequest(ProcGlobalSemas()); +} + + +/* + * ProcGlobalShmemInit - * Initialize the global process table during postmaster or standalone * backend startup. * @@ -183,37 +216,25 @@ ProcGlobalSemas(void) * Another reason for creating semaphores here is that the semaphore * implementation typically requires us to create semaphores in the * postmaster, not in backends. - * - * Note: this is NOT called by individual backends under a postmaster, - * not even in the EXEC_BACKEND case. The ProcGlobal and AuxiliaryProcs - * pointers must be propagated specially for EXEC_BACKEND operation. */ -void -InitProcGlobal(void) +static void +ProcGlobalShmemInit(void *arg) { + char *ptr; + size_t requestSize; PGPROC *procs; int i, j; - bool found; - uint32 TotalProcs = MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts; /* Used for setup of per-backend fast-path slots. */ char *fpPtr, *fpEndPtr PG_USED_FOR_ASSERTS_ONLY; Size fpLockBitsSize, fpRelIdSize; - Size requestSize; - char *ptr; - - /* Create the ProcGlobal shared structure */ - ProcGlobal = (PROC_HDR *) - ShmemInitStruct("Proc Header", sizeof(PROC_HDR), &found); - Assert(!found); - /* - * Initialize the data structures. - */ + Assert(ProcGlobal); ProcGlobal->spins_per_delay = DEFAULT_SPINS_PER_DELAY; + SpinLockInit(&ProcGlobal->freeProcsLock); dlist_init(&ProcGlobal->freeProcs); dlist_init(&ProcGlobal->autovacFreeProcs); dlist_init(&ProcGlobal->bgworkerFreeProcs); @@ -224,68 +245,55 @@ InitProcGlobal(void) pg_atomic_init_u32(&ProcGlobal->procArrayGroupFirst, INVALID_PROC_NUMBER); pg_atomic_init_u32(&ProcGlobal->clogGroupFirst, INVALID_PROC_NUMBER); - /* - * Create and initialize all the PGPROC structures we'll need. There are - * six separate consumers: (1) normal backends, (2) autovacuum workers and - * special workers, (3) background workers, (4) walsenders, (5) auxiliary - * processes, and (6) prepared transactions. (For largely-historical - * reasons, we combine autovacuum and special workers into one category - * with a single freelist.) Each PGPROC structure is dedicated to exactly - * one of these purposes, and they do not move between groups. - */ - requestSize = PGProcShmemSize(); - - ptr = ShmemInitStruct("PGPROC structures", - requestSize, - &found); - + ptr = AllProcsShmemPtr; + requestSize = ProcGlobalAllProcsShmemSize; MemSet(ptr, 0, requestSize); + /* Carve out the allProcs array from the shared memory area */ procs = (PGPROC *) ptr; - ptr = (char *) ptr + TotalProcs * sizeof(PGPROC); + ptr = ptr + TotalProcs * sizeof(PGPROC); ProcGlobal->allProcs = procs; /* XXX allProcCount isn't really all of them; it excludes prepared xacts */ ProcGlobal->allProcCount = MaxBackends + NUM_AUXILIARY_PROCS; /* - * Allocate arrays mirroring PGPROC fields in a dense manner. See + * Carve out arrays mirroring PGPROC fields in a dense manner. See * PROC_HDR. * * XXX: It might make sense to increase padding for these arrays, given * how hotly they are accessed. */ ProcGlobal->xids = (TransactionId *) ptr; - ptr = (char *) ptr + (TotalProcs * sizeof(*ProcGlobal->xids)); + ptr = ptr + (TotalProcs * sizeof(*ProcGlobal->xids)); ProcGlobal->subxidStates = (XidCacheStatus *) ptr; - ptr = (char *) ptr + (TotalProcs * sizeof(*ProcGlobal->subxidStates)); + ptr = ptr + (TotalProcs * sizeof(*ProcGlobal->subxidStates)); ProcGlobal->statusFlags = (uint8 *) ptr; - ptr = (char *) ptr + (TotalProcs * sizeof(*ProcGlobal->statusFlags)); + ptr = ptr + (TotalProcs * sizeof(*ProcGlobal->statusFlags)); - /* make sure wer didn't overflow */ + /* make sure we didn't overflow */ Assert((ptr > (char *) procs) && (ptr <= (char *) procs + requestSize)); /* - * Allocate arrays for fast-path locks. Those are variable-length, so + * Initialize arrays for fast-path locks. Those are variable-length, so * can't be included in PGPROC directly. We allocate a separate piece of * shared memory and then divide that between backends. */ fpLockBitsSize = MAXALIGN(FastPathLockGroupsPerBackend * sizeof(uint64)); fpRelIdSize = MAXALIGN(FastPathLockSlotsPerBackend() * sizeof(Oid)); - requestSize = FastPathLockShmemSize(); - - fpPtr = ShmemInitStruct("Fast-Path Lock Array", - requestSize, - &found); - - MemSet(fpPtr, 0, requestSize); + fpPtr = FastPathLockArrayShmemPtr; + requestSize = FastPathLockArrayShmemSize; + memset(fpPtr, 0, requestSize); /* For asserts checking we did not overflow. */ fpEndPtr = fpPtr + requestSize; + /* Initialize semaphores */ + PGSemaphoreInit(ProcGlobalSemas()); + for (i = 0; i < TotalProcs; i++) { PGPROC *proc = &procs[i]; @@ -309,7 +317,7 @@ InitProcGlobal(void) * dummy PGPROCs don't need these though - they're never associated * with a real process */ - if (i < MaxBackends + NUM_AUXILIARY_PROCS) + if (i < FIRST_PREPARED_XACT_PROC_NUMBER) { proc->sem = PGSemaphoreCreate(); InitSharedLatch(&(proc->procLatch)); @@ -328,25 +336,25 @@ InitProcGlobal(void) if (i < MaxConnections) { /* PGPROC for normal backend, add to freeProcs list */ - dlist_push_tail(&ProcGlobal->freeProcs, &proc->links); + dlist_push_tail(&ProcGlobal->freeProcs, &proc->freeProcsLink); proc->procgloballist = &ProcGlobal->freeProcs; } else if (i < MaxConnections + autovacuum_worker_slots + NUM_SPECIAL_WORKER_PROCS) { /* PGPROC for AV or special worker, add to autovacFreeProcs list */ - dlist_push_tail(&ProcGlobal->autovacFreeProcs, &proc->links); + dlist_push_tail(&ProcGlobal->autovacFreeProcs, &proc->freeProcsLink); proc->procgloballist = &ProcGlobal->autovacFreeProcs; } else if (i < MaxConnections + autovacuum_worker_slots + NUM_SPECIAL_WORKER_PROCS + max_worker_processes) { /* PGPROC for bgworker, add to bgworkerFreeProcs list */ - dlist_push_tail(&ProcGlobal->bgworkerFreeProcs, &proc->links); + dlist_push_tail(&ProcGlobal->bgworkerFreeProcs, &proc->freeProcsLink); proc->procgloballist = &ProcGlobal->bgworkerFreeProcs; } else if (i < MaxBackends) { /* PGPROC for walsender, add to walsenderFreeProcs list */ - dlist_push_tail(&ProcGlobal->walsenderFreeProcs, &proc->links); + dlist_push_tail(&ProcGlobal->walsenderFreeProcs, &proc->freeProcsLink); proc->procgloballist = &ProcGlobal->walsenderFreeProcs; } @@ -374,13 +382,7 @@ InitProcGlobal(void) * processes and prepared transactions. */ AuxiliaryProcs = &procs[MaxBackends]; - PreparedXactProcs = &procs[MaxBackends + NUM_AUXILIARY_PROCS]; - - /* Create ProcStructLock spinlock, too */ - ProcStructLock = (slock_t *) ShmemInitStruct("ProcStructLock spinlock", - sizeof(slock_t), - &found); - SpinLockInit(ProcStructLock); + PreparedXactProcs = &procs[FIRST_PREPARED_XACT_PROC_NUMBER]; } /* @@ -411,7 +413,7 @@ InitProcess(void) /* * Decide which list should supply our PGPROC. This logic must match the - * way the freelists were constructed in InitProcGlobal(). + * way the freelists were constructed in ProcGlobalShmemInit(). */ if (AmAutoVacuumWorkerProcess() || AmSpecialWorkerProcess()) procgloballist = &ProcGlobal->autovacFreeProcs; @@ -426,17 +428,17 @@ InitProcess(void) * Try to get a proc struct from the appropriate free list. If this * fails, we must be out of PGPROC structures (not to mention semaphores). * - * While we are holding the ProcStructLock, also copy the current shared + * While we are holding the spinlock, also copy the current shared * estimate of spins_per_delay to local storage. */ - SpinLockAcquire(ProcStructLock); + SpinLockAcquire(&ProcGlobal->freeProcsLock); set_spins_per_delay(ProcGlobal->spins_per_delay); if (!dlist_is_empty(procgloballist)) { - MyProc = dlist_container(PGPROC, links, dlist_pop_head_node(procgloballist)); - SpinLockRelease(ProcStructLock); + MyProc = dlist_container(PGPROC, freeProcsLink, dlist_pop_head_node(procgloballist)); + SpinLockRelease(&ProcGlobal->freeProcsLock); } else { @@ -446,7 +448,7 @@ InitProcess(void) * error message. XXX do we need to give a different failure message * in the autovacuum case? */ - SpinLockRelease(ProcStructLock); + SpinLockRelease(&ProcGlobal->freeProcsLock); if (AmWalSenderProcess()) ereport(FATAL, (errcode(ERRCODE_TOO_MANY_CONNECTIONS), @@ -466,9 +468,9 @@ InitProcess(void) /* * Initialize all fields of MyProc, except for those previously - * initialized by InitProcGlobal. + * initialized by ProcGlobalShmemInit. */ - dlist_node_init(&MyProc->links); + dlist_node_init(&MyProc->freeProcsLink); MyProc->waitStatus = PROC_WAIT_STATUS_OK; MyProc->fpVXIDLock = false; MyProc->fpLocalTransactionId = InvalidLocalTransactionId; @@ -481,7 +483,7 @@ InitProcess(void) MyProc->databaseId = InvalidOid; MyProc->roleId = InvalidOid; MyProc->tempNamespaceId = InvalidOid; - MyProc->isRegularBackend = AmRegularBackendProcess(); + MyProc->backendType = MyBackendType; MyProc->delayChkptFlags = 0; MyProc->statusFlags = 0; /* NB -- autovac launcher intentionally does not set IS_AUTOVACUUM */ @@ -490,6 +492,7 @@ InitProcess(void) MyProc->lwWaiting = LW_WS_NOT_WAITING; MyProc->lwWaitMode = 0; MyProc->waitLock = NULL; + dlist_node_init(&MyProc->waitLink); MyProc->waitProcLock = NULL; pg_atomic_write_u64(&MyProc->waitStart, 0); #ifdef USE_ASSERT_CHECKING @@ -501,10 +504,10 @@ InitProcess(void) Assert(dlist_is_empty(&(MyProc->myProcLocks[i]))); } #endif - MyProc->recoveryConflictPending = false; + pg_atomic_write_u32(&MyProc->pendingRecoveryConflicts, 0); /* Initialize fields for sync rep */ - MyProc->waitLSN = 0; + MyProc->waitLSN = InvalidXLogRecPtr; MyProc->syncRepState = SYNC_REP_NOT_WAITING; dlist_node_init(&MyProc->syncRepLinks); @@ -598,7 +601,7 @@ InitProcessPhase2(void) * This is called by bgwriter and similar processes so that they will have a * MyProc value that's real enough to let them wait for LWLocks. The PGPROC * and sema that are assigned are one of the extra ones created during - * InitProcGlobal. + * ProcGlobalShmemInit. * * Auxiliary processes are presently not expected to wait for real (lockmgr) * locks, so we need not set up the deadlock checker. They are never added @@ -631,13 +634,13 @@ InitAuxiliaryProcess(void) RegisterPostmasterChildActive(); /* - * We use the ProcStructLock to protect assignment and releasing of + * We use the freeProcsLock to protect assignment and releasing of * AuxiliaryProcs entries. * - * While we are holding the ProcStructLock, also copy the current shared + * While we are holding the spinlock, also copy the current shared * estimate of spins_per_delay to local storage. */ - SpinLockAcquire(ProcStructLock); + SpinLockAcquire(&ProcGlobal->freeProcsLock); set_spins_per_delay(ProcGlobal->spins_per_delay); @@ -652,7 +655,7 @@ InitAuxiliaryProcess(void) } if (proctype >= NUM_AUXILIARY_PROCS) { - SpinLockRelease(ProcStructLock); + SpinLockRelease(&ProcGlobal->freeProcsLock); elog(FATAL, "all AuxiliaryProcs are in use"); } @@ -660,16 +663,16 @@ InitAuxiliaryProcess(void) /* use volatile pointer to prevent code rearrangement */ ((volatile PGPROC *) auxproc)->pid = MyProcPid; - SpinLockRelease(ProcStructLock); + SpinLockRelease(&ProcGlobal->freeProcsLock); MyProc = auxproc; MyProcNumber = GetNumberFromPGProc(MyProc); /* * Initialize all fields of MyProc, except for those previously - * initialized by InitProcGlobal. + * initialized by ProcGlobalShmemInit. */ - dlist_node_init(&MyProc->links); + dlist_node_init(&MyProc->freeProcsLink); MyProc->waitStatus = PROC_WAIT_STATUS_OK; MyProc->fpVXIDLock = false; MyProc->fpLocalTransactionId = InvalidLocalTransactionId; @@ -680,12 +683,13 @@ InitAuxiliaryProcess(void) MyProc->databaseId = InvalidOid; MyProc->roleId = InvalidOid; MyProc->tempNamespaceId = InvalidOid; - MyProc->isRegularBackend = false; + MyProc->backendType = MyBackendType; MyProc->delayChkptFlags = 0; MyProc->statusFlags = 0; MyProc->lwWaiting = LW_WS_NOT_WAITING; MyProc->lwWaitMode = 0; MyProc->waitLock = NULL; + dlist_node_init(&MyProc->waitLink); MyProc->waitProcLock = NULL; pg_atomic_write_u64(&MyProc->waitStart, 0); #ifdef USE_ASSERT_CHECKING @@ -697,6 +701,7 @@ InitAuxiliaryProcess(void) Assert(dlist_is_empty(&(MyProc->myProcLocks[i]))); } #endif + pg_atomic_write_u32(&MyProc->pendingRecoveryConflicts, 0); /* * Acquire ownership of the PGPROC's latch, so that we can use WaitLatch @@ -786,7 +791,7 @@ HaveNFreeProcs(int n, int *nfree) Assert(n > 0); Assert(nfree); - SpinLockAcquire(ProcStructLock); + SpinLockAcquire(&ProcGlobal->freeProcsLock); *nfree = 0; dlist_foreach(iter, &ProcGlobal->freeProcs) @@ -796,7 +801,7 @@ HaveNFreeProcs(int n, int *nfree) break; } - SpinLockRelease(ProcStructLock); + SpinLockRelease(&ProcGlobal->freeProcsLock); return (*nfree == n); } @@ -846,7 +851,7 @@ LockErrorCleanup(void) partitionLock = LockHashPartitionLock(lockAwaited->hashcode); LWLockAcquire(partitionLock, LW_EXCLUSIVE); - if (!dlist_node_is_detached(&MyProc->links)) + if (!dlist_node_is_detached(&MyProc->waitLink)) { /* We could not have been granted the lock yet */ RemoveFromWaitQueue(MyProc, lockAwaited->hashcode); @@ -947,6 +952,11 @@ ProcKill(int code, Datum arg) */ LWLockReleaseAll(); + /* + * Cleanup waiting for LSN if any. + */ + WaitLSNCleanup(); + /* Cancel any pending condition variable sleep, too */ ConditionVariableCancelSleep(); @@ -972,9 +982,9 @@ ProcKill(int code, Datum arg) procgloballist = leader->procgloballist; /* Leader exited first; return its PGPROC. */ - SpinLockAcquire(ProcStructLock); - dlist_push_head(procgloballist, &leader->links); - SpinLockRelease(ProcStructLock); + SpinLockAcquire(&ProcGlobal->freeProcsLock); + dlist_push_head(procgloballist, &leader->freeProcsLink); + SpinLockRelease(&ProcGlobal->freeProcsLock); } } else if (leader != MyProc) @@ -1005,7 +1015,7 @@ ProcKill(int code, Datum arg) proc->vxid.lxid = InvalidTransactionId; procgloballist = proc->procgloballist; - SpinLockAcquire(ProcStructLock); + SpinLockAcquire(&ProcGlobal->freeProcsLock); /* * If we're still a member of a locking group, that means we're a leader @@ -1018,17 +1028,13 @@ ProcKill(int code, Datum arg) Assert(dlist_is_empty(&proc->lockGroupMembers)); /* Return PGPROC structure (and semaphore) to appropriate freelist */ - dlist_push_tail(procgloballist, &proc->links); + dlist_push_tail(procgloballist, &proc->freeProcsLink); } /* Update shared estimate of spins_per_delay */ ProcGlobal->spins_per_delay = update_spins_per_delay(ProcGlobal->spins_per_delay); - SpinLockRelease(ProcStructLock); - - /* wake autovac launcher if needed -- see comments in FreeWorkerInfo */ - if (AutovacuumLauncherPid != 0) - kill(AutovacuumLauncherPid, SIGUSR2); + SpinLockRelease(&ProcGlobal->freeProcsLock); } /* @@ -1068,7 +1074,7 @@ AuxiliaryProcKill(int code, Datum arg) MyProcNumber = INVALID_PROC_NUMBER; DisownLatch(&proc->procLatch); - SpinLockAcquire(ProcStructLock); + SpinLockAcquire(&ProcGlobal->freeProcsLock); /* Mark auxiliary proc no longer in use */ proc->pid = 0; @@ -1078,7 +1084,7 @@ AuxiliaryProcKill(int code, Datum arg) /* Update shared estimate of spins_per_delay */ ProcGlobal->spins_per_delay = update_spins_per_delay(ProcGlobal->spins_per_delay); - SpinLockRelease(ProcStructLock); + SpinLockRelease(&ProcGlobal->freeProcsLock); } /* @@ -1211,7 +1217,7 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) dclist_foreach(iter, waitQueue) { - PGPROC *proc = dlist_container(PGPROC, links, iter.cur); + PGPROC *proc = dlist_container(PGPROC, waitLink, iter.cur); /* * If we're part of the same locking group as this waiter, its @@ -1275,9 +1281,9 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) * Insert self into queue, at the position determined above. */ if (insert_before) - dclist_insert_before(waitQueue, &insert_before->links, &MyProc->links); + dclist_insert_before(waitQueue, &insert_before->waitLink, &MyProc->waitLink); else - dclist_push_tail(waitQueue, &MyProc->links); + dclist_push_tail(waitQueue, &MyProc->waitLink); lock->waitMask |= LOCKBIT_ON(lockmode); @@ -1315,7 +1321,9 @@ ProcSleep(LOCALLOCK *locallock) TimestampTz standbyWaitStart = 0; bool allow_autovacuum_cancel = true; bool logged_recovery_conflict = false; + bool logged_lock_wait = false; ProcWaitStatus myWaitStatus; + DeadLockState deadlock_state; /* The caller must've armed the on-error cleanup mechanism */ Assert(GetAwaitedLock() == locallock); @@ -1441,7 +1449,7 @@ ProcSleep(LOCALLOCK *locallock) * because the startup process here has already waited * longer than deadlock_timeout. */ - LogRecoveryConflict(PROCSIG_RECOVERY_CONFLICT_LOCK, + LogRecoveryConflict(RECOVERY_CONFLICT_LOCK, standbyWaitStart, now, cnt > 0 ? vxids : NULL, true); logged_recovery_conflict = true; @@ -1456,7 +1464,7 @@ ProcSleep(LOCALLOCK *locallock) /* check for deadlocks first, as that's probably log-worthy */ if (got_deadlock_timeout) { - CheckDeadLock(); + deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; } CHECK_FOR_INTERRUPTS(); @@ -1553,105 +1561,134 @@ ProcSleep(LOCALLOCK *locallock) } /* - * If awoken after the deadlock check interrupt has run, and - * log_lock_waits is on, then report about the wait. + * If awoken after the deadlock check interrupt has run, increment the + * lock statistics counters and if log_lock_waits is on, then report + * about the wait. */ - if (log_lock_waits && deadlock_state != DS_NOT_YET_CHECKED) + if (deadlock_state != DS_NOT_YET_CHECKED) { - StringInfoData buf, - lock_waiters_sbuf, - lock_holders_sbuf; - const char *modename; long secs; int usecs; long msecs; - int lockHoldersNum = 0; - - initStringInfo(&buf); - initStringInfo(&lock_waiters_sbuf); - initStringInfo(&lock_holders_sbuf); - DescribeLockTag(&buf, &locallock->tag.lock); - modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid, - lockmode); + INJECTION_POINT("deadlock-timeout-fired", NULL); TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT), GetCurrentTimestamp(), &secs, &usecs); msecs = secs * 1000 + usecs / 1000; usecs = usecs % 1000; - /* Gather a list of all lock holders and waiters */ - LWLockAcquire(partitionLock, LW_SHARED); - GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf, - &lock_waiters_sbuf, &lockHoldersNum); - LWLockRelease(partitionLock); - - if (deadlock_state == DS_SOFT_DEADLOCK) - ereport(LOG, - (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms", - MyProcPid, modename, buf.data, msecs, usecs), - (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", - "Processes holding the lock: %s. Wait queue: %s.", - lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); - else if (deadlock_state == DS_HARD_DEADLOCK) - { - /* - * This message is a bit redundant with the error that will be - * reported subsequently, but in some cases the error report - * might not make it to the log (eg, if it's caught by an - * exception handler), and we want to ensure all long-wait - * events get logged. - */ - ereport(LOG, - (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms", - MyProcPid, modename, buf.data, msecs, usecs), - (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", - "Processes holding the lock: %s. Wait queue: %s.", - lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); - } + /* Increment the lock statistics counters if done waiting. */ + if (myWaitStatus == PROC_WAIT_STATUS_OK) + pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs); - if (myWaitStatus == PROC_WAIT_STATUS_WAITING) - ereport(LOG, - (errmsg("process %d still waiting for %s on %s after %ld.%03d ms", - MyProcPid, modename, buf.data, msecs, usecs), - (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", - "Processes holding the lock: %s. Wait queue: %s.", - lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); - else if (myWaitStatus == PROC_WAIT_STATUS_OK) - ereport(LOG, - (errmsg("process %d acquired %s on %s after %ld.%03d ms", - MyProcPid, modename, buf.data, msecs, usecs))); - else + if (log_lock_waits) { - Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR); - - /* - * Currently, the deadlock checker always kicks its own - * process, which means that we'll only see - * PROC_WAIT_STATUS_ERROR when deadlock_state == - * DS_HARD_DEADLOCK, and there's no need to print redundant - * messages. But for completeness and future-proofing, print - * a message if it looks like someone else kicked us off the - * lock. - */ - if (deadlock_state != DS_HARD_DEADLOCK) + StringInfoData buf, + lock_waiters_sbuf, + lock_holders_sbuf; + const char *modename; + int lockHoldersNum = 0; + + initStringInfo(&buf); + initStringInfo(&lock_waiters_sbuf); + initStringInfo(&lock_holders_sbuf); + + DescribeLockTag(&buf, &locallock->tag.lock); + modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid, + lockmode); + + /* Gather a list of all lock holders and waiters */ + LWLockAcquire(partitionLock, LW_SHARED); + GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf, + &lock_waiters_sbuf, &lockHoldersNum); + LWLockRelease(partitionLock); + + if (deadlock_state == DS_SOFT_DEADLOCK) ereport(LOG, - (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms", + (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms", MyProcPid, modename, buf.data, msecs, usecs), (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", "Processes holding the lock: %s. Wait queue: %s.", lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); + else if (deadlock_state == DS_HARD_DEADLOCK) + { + /* + * This message is a bit redundant with the error that + * will be reported subsequently, but in some cases the + * error report might not make it to the log (eg, if it's + * caught by an exception handler), and we want to ensure + * all long-wait events get logged. + */ + ereport(LOG, + (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms", + MyProcPid, modename, buf.data, msecs, usecs), + (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", + "Processes holding the lock: %s. Wait queue: %s.", + lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); + } + + if (myWaitStatus == PROC_WAIT_STATUS_WAITING) + { + /* + * Guard the "still waiting on lock" log message so it is + * reported at most once while waiting for the lock. + * + * Without this guard, the message can be emitted whenever + * the lock-wait sleep is interrupted (for example by + * SIGHUP for config reload or by + * client_connection_check_interval). For example, if + * client_connection_check_interval is set very low (e.g., + * 100 ms), the message could be logged repeatedly, + * flooding the log and making it difficult to use. + */ + if (!logged_lock_wait) + { + ereport(LOG, + (errmsg("process %d still waiting for %s on %s after %ld.%03d ms", + MyProcPid, modename, buf.data, msecs, usecs), + (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", + "Processes holding the lock: %s. Wait queue: %s.", + lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); + logged_lock_wait = true; + } + } + else if (myWaitStatus == PROC_WAIT_STATUS_OK) + ereport(LOG, + (errmsg("process %d acquired %s on %s after %ld.%03d ms", + MyProcPid, modename, buf.data, msecs, usecs))); + else + { + Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR); + + /* + * Currently, the deadlock checker always kicks its own + * process, which means that we'll only see + * PROC_WAIT_STATUS_ERROR when deadlock_state == + * DS_HARD_DEADLOCK, and there's no need to print + * redundant messages. But for completeness and + * future-proofing, print a message if it looks like + * someone else kicked us off the lock. + */ + if (deadlock_state != DS_HARD_DEADLOCK) + ereport(LOG, + (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms", + MyProcPid, modename, buf.data, msecs, usecs), + (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.", + "Processes holding the lock: %s. Wait queue: %s.", + lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data)))); + } + pfree(buf.data); + pfree(lock_holders_sbuf.data); + pfree(lock_waiters_sbuf.data); } /* * At this point we might still need to wait for the lock. Reset - * state so we don't print the above messages again. + * state so we don't print the above messages again if + * log_lock_waits is on. */ deadlock_state = DS_NO_DEADLOCK; - - pfree(buf.data); - pfree(lock_holders_sbuf.data); - pfree(lock_waiters_sbuf.data); } } while (myWaitStatus == PROC_WAIT_STATUS_WAITING); @@ -1682,7 +1719,7 @@ ProcSleep(LOCALLOCK *locallock) * startup process waited longer than deadlock_timeout for it. */ if (InHotStandby && logged_recovery_conflict) - LogRecoveryConflict(PROCSIG_RECOVERY_CONFLICT_LOCK, + LogRecoveryConflict(RECOVERY_CONFLICT_LOCK, standbyWaitStart, GetCurrentTimestamp(), NULL, false); @@ -1698,7 +1735,7 @@ ProcSleep(LOCALLOCK *locallock) /* * ProcWakeup -- wake up a process by setting its latch. * - * Also remove the process from the wait queue and set its links invalid. + * Also remove the process from the wait queue and set its waitLink invalid. * * The appropriate lock partition lock must be held by caller. * @@ -1710,19 +1747,19 @@ ProcSleep(LOCALLOCK *locallock) void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus) { - if (dlist_node_is_detached(&proc->links)) + if (dlist_node_is_detached(&proc->waitLink)) return; Assert(proc->waitStatus == PROC_WAIT_STATUS_WAITING); /* Remove process from wait queue */ - dclist_delete_from_thoroughly(&proc->waitLock->waitProcs, &proc->links); + dclist_delete_from_thoroughly(&proc->waitLock->waitProcs, &proc->waitLink); /* Clean up process' state and pass it the ok/fail signal */ proc->waitLock = NULL; proc->waitProcLock = NULL; proc->waitStatus = waitStatus; - pg_atomic_write_u64(&MyProc->waitStart, 0); + pg_atomic_write_u64(&proc->waitStart, 0); /* And awaken it */ SetLatch(&proc->procLatch); @@ -1747,7 +1784,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) dclist_foreach_modify(miter, waitQueue) { - PGPROC *proc = dlist_container(PGPROC, links, miter.cur); + PGPROC *proc = dlist_container(PGPROC, waitLink, miter.cur); LOCKMODE lockmode = proc->waitLockMode; /* @@ -1779,14 +1816,14 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * * We only get to this routine, if DEADLOCK_TIMEOUT fired while waiting for a * lock to be released by some other process. Check if there's a deadlock; if - * not, just return. (But signal ProcSleep to log a message, if - * log_lock_waits is true.) If we have a real deadlock, remove ourselves from - * the lock's wait queue and signal an error to ProcSleep. + * not, just return. If we have a real deadlock, remove ourselves from the + * lock's wait queue. */ -static void +static DeadLockState CheckDeadLock(void) { int i; + DeadLockState result; /* * Acquire exclusive lock on the entire shared lock data structures. Must @@ -1811,19 +1848,21 @@ CheckDeadLock(void) * We check by looking to see if we've been unlinked from the wait queue. * This is safe because we hold the lock partition lock. */ - if (MyProc->links.prev == NULL || - MyProc->links.next == NULL) + if (dlist_node_is_detached(&MyProc->waitLink)) + { + result = DS_NO_DEADLOCK; goto check_done; + } #ifdef LOCK_DEBUG if (Debug_deadlocks) DumpAllLocks(); #endif - /* Run the deadlock check, and set deadlock_state for use by ProcSleep */ - deadlock_state = DeadLockCheck(MyProc); + /* Run the deadlock check */ + result = DeadLockCheck(MyProc); - if (deadlock_state == DS_HARD_DEADLOCK) + if (result == DS_HARD_DEADLOCK) { /* * Oops. We have a deadlock. @@ -1835,7 +1874,7 @@ CheckDeadLock(void) * * RemoveFromWaitQueue sets MyProc->waitStatus to * PROC_WAIT_STATUS_ERROR, so ProcSleep will report an error after we - * return from the signal handler. + * return. */ Assert(MyProc->waitLock != NULL); RemoveFromWaitQueue(MyProc, LockTagHashCode(&(MyProc->waitLock->tag))); @@ -1862,6 +1901,8 @@ CheckDeadLock(void) check_done: for (i = NUM_LOCK_PARTITIONS; --i >= 0;) LWLockRelease(LockHashPartitionLockByIndex(i)); + + return result; } /* @@ -1988,7 +2029,7 @@ ProcSendSignal(ProcNumber procNumber) if (procNumber < 0 || procNumber >= ProcGlobal->allProcCount) elog(ERROR, "procNumber out of range"); - SetLatch(&ProcGlobal->allProcs[procNumber].procLatch); + SetLatch(&GetPGProcByNumber(procNumber)->procLatch); } /* diff --git a/src/backend/storage/lmgr/s_lock.c b/src/backend/storage/lmgr/s_lock.c index d26e192f4bc5f..6df568eccb35a 100644 --- a/src/backend/storage/lmgr/s_lock.c +++ b/src/backend/storage/lmgr/s_lock.c @@ -36,7 +36,7 @@ * the probability of unintended failure) than to fix the total time * spent. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -262,12 +262,6 @@ main() return 1; } - if (!S_LOCK_FREE(&test_lock.lock)) - { - printf("S_LOCK_TEST: failed, lock not initialized\n"); - return 1; - } - S_LOCK(&test_lock.lock); if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44) @@ -276,12 +270,6 @@ main() return 1; } - if (S_LOCK_FREE(&test_lock.lock)) - { - printf("S_LOCK_TEST: failed, lock not locked\n"); - return 1; - } - S_UNLOCK(&test_lock.lock); if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44) @@ -290,12 +278,6 @@ main() return 1; } - if (!S_LOCK_FREE(&test_lock.lock)) - { - printf("S_LOCK_TEST: failed, lock not unlocked\n"); - return 1; - } - S_LOCK(&test_lock.lock); if (test_lock.pad1 != 0x44 || test_lock.pad2 != 0x44) @@ -304,12 +286,6 @@ main() return 1; } - if (S_LOCK_FREE(&test_lock.lock)) - { - printf("S_LOCK_TEST: failed, lock not re-locked\n"); - return 1; - } - printf("S_LOCK_TEST: this will print %d stars and then\n", NUM_DELAYS); printf(" exit with a 'stuck spinlock' message\n"); printf(" if S_LOCK() and TAS() are working.\n"); diff --git a/src/backend/storage/meson.build b/src/backend/storage/meson.build index 0cd48844f1d9b..05637aa3a4416 100644 --- a/src/backend/storage/meson.build +++ b/src/backend/storage/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('aio') subdir('buffer') diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README index e30d7ac59adc5..73c36a639086c 100644 --- a/src/backend/storage/page/README +++ b/src/backend/storage/page/README @@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed 2010/12/22 on -hackers list. Current implementation requires this be enabled system-wide at initdb time, or -by using the pg_checksums tool on an offline cluster. +by using the pg_checksums tool on an offline cluster. Checksums can also be +enabled at runtime using pg_enable_data_checksums(), and disabled by using +pg_disable_data_checksums(). The checksum is not valid at all times on a data page!! The checksum is valid when the page leaves the shared pool and is checked diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c index dbb49ed9197d7..1fdfda59edd08 100644 --- a/src/backend/storage/page/bufpage.c +++ b/src/backend/storage/page/bufpage.c @@ -3,7 +3,7 @@ * bufpage.c * POSTGRES standard buffer page code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -107,7 +107,15 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail */ if (!PageIsNew(page)) { - if (DataChecksumsEnabled()) + /* + * There shouldn't be any check for interrupt calls happening in this + * codepath, but just to be on the safe side we hold interrupts since + * if they did happen the data checksum state could change during + * verifying checksums, which could lead to incorrect verification + * results. + */ + HOLD_INTERRUPTS(); + if (DataChecksumsNeedVerify()) { checksum = pg_checksum_page(page, blkno); @@ -118,6 +126,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail *checksum_failure_p = true; } } + RESUME_INTERRUPTS(); /* * The following checks don't prove the header is correct, only that @@ -151,8 +160,9 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0) ereport(flags & PIV_LOG_WARNING ? WARNING : LOG, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("page verification failed, calculated checksum %u but expected %u", - checksum, p->pd_checksum))); + errmsg("page verification failed, calculated checksum %u but expected %u%s", + checksum, p->pd_checksum, + (flags & PIV_ZERO_BUFFERS_ON_ERROR ? ", buffer will be zeroed" : "")))); if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE)) return true; @@ -191,7 +201,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail */ OffsetNumber PageAddItemExtended(Page page, - Item item, + const void *item, Size size, OffsetNumber offsetNumber, int flags) @@ -785,8 +795,8 @@ PageRepairFragmentation(Page page) if (totallen > (Size) (pd_special - pd_lower)) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted item lengths: total %u, available space %u", - (unsigned int) totallen, pd_special - pd_lower))); + errmsg("corrupted item lengths: total %zu, available space %u", + totallen, pd_special - pd_lower))); compactify_tuples(itemidbase, nstorage, page, presorted); } @@ -1088,8 +1098,8 @@ PageIndexTupleDelete(Page page, OffsetNumber offnum) offset != MAXALIGN(offset)) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted line pointer: offset = %u, size = %u", - offset, (unsigned int) size))); + errmsg("corrupted line pointer: offset = %u, size = %zu", + offset, size))); /* Amount of space to actually be deleted */ size = MAXALIGN(size); @@ -1229,8 +1239,8 @@ PageIndexMultiDelete(Page page, OffsetNumber *itemnos, int nitems) offset != MAXALIGN(offset)) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted line pointer: offset = %u, size = %u", - offset, (unsigned int) size))); + errmsg("corrupted line pointer: offset = %u, size = %zu", + offset, size))); if (nextitm < nitems && offnum == itemnos[nextitm]) { @@ -1262,8 +1272,8 @@ PageIndexMultiDelete(Page page, OffsetNumber *itemnos, int nitems) if (totallen > (Size) (pd_special - pd_lower)) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted item lengths: total %u, available space %u", - (unsigned int) totallen, pd_special - pd_lower))); + errmsg("corrupted item lengths: total %zu, available space %u", + totallen, pd_special - pd_lower))); /* * Looks good. Overwrite the line pointers with the copy, from which we've @@ -1326,8 +1336,8 @@ PageIndexTupleDeleteNoCompact(Page page, OffsetNumber offnum) offset != MAXALIGN(offset)) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted line pointer: offset = %u, size = %u", - offset, (unsigned int) size))); + errmsg("corrupted line pointer: offset = %u, size = %zu", + offset, size))); /* Amount of space to actually be deleted */ size = MAXALIGN(size); @@ -1402,7 +1412,7 @@ PageIndexTupleDeleteNoCompact(Page page, OffsetNumber offnum) */ bool PageIndexTupleOverwrite(Page page, OffsetNumber offnum, - Item newtup, Size newsize) + const void *newtup, Size newsize) { PageHeader phdr = (PageHeader) page; ItemId tupid; @@ -1438,8 +1448,8 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum, offset != MAXALIGN(offset)) ereport(ERROR, (errcode(ERRCODE_DATA_CORRUPTED), - errmsg("corrupted line pointer: offset = %u, size = %u", - offset, (unsigned int) oldsize))); + errmsg("corrupted line pointer: offset = %u, size = %d", + offset, oldsize))); /* * Determine actual change in space requirement, check for page overflow. @@ -1492,57 +1502,29 @@ PageIndexTupleOverwrite(Page page, OffsetNumber offnum, /* - * Set checksum for a page in shared buffers. + * Set checksum on a page. * - * If checksums are disabled, or if the page is not initialized, just return - * the input. Otherwise, we must make a copy of the page before calculating - * the checksum, to prevent concurrent modifications (e.g. setting hint bits) - * from making the final checksum invalid. It doesn't matter if we include or - * exclude hints during the copy, as long as we write a valid page and - * associated checksum. + * If the page is in shared buffers, it needs to be locked in at least + * share-exclusive mode. * - * Returns a pointer to the block-sized data that needs to be written. Uses - * statically-allocated memory, so the caller must immediately write the - * returned page and not refer to it again. - */ -char * -PageSetChecksumCopy(Page page, BlockNumber blkno) -{ - static char *pageCopy = NULL; - - /* If we don't need a checksum, just return the passed-in data */ - if (PageIsNew(page) || !DataChecksumsEnabled()) - return page; - - /* - * We allocate the copy space once and use it over on each subsequent - * call. The point of palloc'ing here, rather than having a static char - * array, is first to ensure adequate alignment for the checksumming code - * and second to avoid wasting space in processes that never call this. - */ - if (pageCopy == NULL) - pageCopy = MemoryContextAllocAligned(TopMemoryContext, - BLCKSZ, - PG_IO_ALIGN_SIZE, - 0); - - memcpy(pageCopy, page, BLCKSZ); - ((PageHeader) pageCopy)->pd_checksum = pg_checksum_page(pageCopy, blkno); - return pageCopy; -} - -/* - * Set checksum for a page in private memory. + * If checksums are disabled, or if the page is not initialized, just + * return. Otherwise compute and set the checksum. * - * This must only be used when we know that no other process can be modifying - * the page buffer. + * In the past this needed to be done on a copy of the page, due to the + * possibility of e.g., hint bits being set concurrently. However, this is not + * necessary anymore as hint bits won't be set while IO is going on. */ void -PageSetChecksumInplace(Page page, BlockNumber blkno) +PageSetChecksum(Page page, BlockNumber blkno) { + HOLD_INTERRUPTS(); /* If we don't need a checksum, just return */ - if (PageIsNew(page) || !DataChecksumsEnabled()) + if (PageIsNew(page) || !DataChecksumsNeedWrite()) + { + RESUME_INTERRUPTS(); return; + } ((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno); + RESUME_INTERRUPTS(); } diff --git a/src/backend/storage/page/checksum.c b/src/backend/storage/page/checksum.c index c913459b5a375..030c44f730897 100644 --- a/src/backend/storage/page/checksum.c +++ b/src/backend/storage/page/checksum.c @@ -3,7 +3,7 @@ * checksum.c * Checksum implementation for data pages. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -13,10 +13,52 @@ */ #include "postgres.h" +#include "port/pg_cpu.h" #include "storage/checksum.h" /* * The actual code is in storage/checksum_impl.h. This is done so that * external programs can incorporate the checksum code by #include'ing - * that file from the exported Postgres headers. (Compare our CRC code.) + * that file from the exported Postgres headers. (Compare our legacy + * CRC code in pg_crc.h.) + * The PG_CHECKSUM_INTERNAL symbol allows core to use hardware-specific + * coding without affecting external programs. */ +#define PG_CHECKSUM_INTERNAL #include "storage/checksum_impl.h" /* IWYU pragma: keep */ + + +static uint32 +pg_checksum_block_fallback(const PGChecksummablePage *page) +{ +#include "storage/checksum_block_internal.h" +} + +/* + * AVX2-optimized block checksum algorithm. + */ +#ifdef USE_AVX2_WITH_RUNTIME_CHECK +pg_attribute_target("avx2") +static uint32 +pg_checksum_block_avx2(const PGChecksummablePage *page) +{ +#include "storage/checksum_block_internal.h" +} +#endif /* USE_AVX2_WITH_RUNTIME_CHECK */ + +/* + * Choose the best available checksum implementation. + */ +static uint32 +pg_checksum_choose(const PGChecksummablePage *page) +{ + pg_checksum_block = pg_checksum_block_fallback; + +#ifdef USE_AVX2_WITH_RUNTIME_CHECK + if (x86_feature_available(PG_AVX2)) + pg_checksum_block = pg_checksum_block_avx2; +#endif + + return pg_checksum_block(page); +} + +static uint32 (*pg_checksum_block) (const PGChecksummablePage *page) = pg_checksum_choose; diff --git a/src/backend/storage/page/itemptr.c b/src/backend/storage/page/itemptr.c index ad65821572194..546874ebc5f18 100644 --- a/src/backend/storage/page/itemptr.c +++ b/src/backend/storage/page/itemptr.c @@ -3,7 +3,7 @@ * itemptr.c * POSTGRES disk item pointer code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -32,7 +32,7 @@ StaticAssertDecl(sizeof(ItemPointerData) == 3 * sizeof(uint16), * Asserts that the disk item pointers are both valid! */ bool -ItemPointerEquals(ItemPointer pointer1, ItemPointer pointer2) +ItemPointerEquals(const ItemPointerData *pointer1, const ItemPointerData *pointer2) { if (ItemPointerGetBlockNumber(pointer1) == ItemPointerGetBlockNumber(pointer2) && @@ -48,7 +48,7 @@ ItemPointerEquals(ItemPointer pointer1, ItemPointer pointer2) * Generic btree-style comparison for item pointers. */ int32 -ItemPointerCompare(ItemPointer arg1, ItemPointer arg2) +ItemPointerCompare(const ItemPointerData *arg1, const ItemPointerData *arg2) { /* * Use ItemPointerGet{Offset,Block}NumberNoCheck to avoid asserting diff --git a/src/backend/storage/page/meson.build b/src/backend/storage/page/meson.build index c3e4a805862a9..56510d246fafe 100644 --- a/src/backend/storage/page/meson.build +++ b/src/backend/storage/page/meson.build @@ -1,7 +1,15 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +checksum_backend_lib = static_library('checksum_backend_lib', + 'checksum.c', + dependencies: backend_build_deps, + kwargs: internal_lib_args, + c_args: vectorize_cflags + unroll_loops_cflags, +) + +backend_link_with += checksum_backend_lib backend_sources += files( 'bufpage.c', - 'checksum.c', 'itemptr.c', ) diff --git a/src/backend/storage/smgr/bulk_write.c b/src/backend/storage/smgr/bulk_write.c index b958be1571645..f3c24082a69b4 100644 --- a/src/backend/storage/smgr/bulk_write.c +++ b/src/backend/storage/smgr/bulk_write.c @@ -25,7 +25,7 @@ * even if a checkpoint happens concurrently. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -101,7 +101,7 @@ smgr_bulk_start_smgr(SMgrRelation smgr, ForkNumber forknum, bool use_wal) { BulkWriteState *state; - state = palloc(sizeof(BulkWriteState)); + state = palloc_object(BulkWriteState); state->smgr = smgr; state->forknum = forknum; state->use_wal = use_wal; @@ -279,7 +279,7 @@ smgr_bulk_flush(BulkWriteState *bulkstate) BlockNumber blkno = pending_writes[i].blkno; Page page = pending_writes[i].buf->data; - PageSetChecksumInplace(page, blkno); + PageSetChecksum(page, blkno); if (blkno >= bulkstate->relsize) { diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c index 2ccb0faceb5b6..dee29037b1696 100644 --- a/src/backend/storage/smgr/md.c +++ b/src/backend/storage/smgr/md.c @@ -10,7 +10,7 @@ * It doesn't matter whether the bits are on spinning rust or some other * storage technology. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -21,6 +21,7 @@ */ #include "postgres.h" +#include #include #include #include @@ -39,6 +40,7 @@ #include "storage/smgr.h" #include "storage/sync.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* * The magnetic disk storage manager keeps track of open file @@ -65,6 +67,15 @@ * out to an unlinked old copy of a segment file that will eventually * disappear. * + * RELSEG_SIZE must fit into BlockNumber; but since we expose its value + * as an integer GUC, it actually needs to fit in signed int. It's worth + * having a cross-check for this since configure's --with-segsize options + * could let people select insane values. + */ +StaticAssertDecl(RELSEG_SIZE > 0 && RELSEG_SIZE <= INT_MAX, + "RELSEG_SIZE must fit in an integer"); + +/* * File descriptors are stored in the per-fork md_seg_fds arrays inside * SMgrRelation. The length of these arrays is stored in md_num_open_segs. * Note that a fork's md_num_open_segs having a specific value does not @@ -477,7 +488,7 @@ void mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, const void *buffer, bool skipFsync) { - off_t seekpos; + pgoff_t seekpos; int nbytes; MdfdVec *v; @@ -505,9 +516,9 @@ mdextend(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, v = _mdfd_getseg(reln, forknum, blocknum, skipFsync, EXTENSION_CREATE); - seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + seekpos = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE); + Assert(seekpos < (pgoff_t) BLCKSZ * RELSEG_SIZE); if ((nbytes = FileWrite(v->mdfd_vfd, buffer, BLCKSZ, seekpos, WAIT_EVENT_DATA_FILE_EXTEND)) != BLCKSZ) { @@ -568,7 +579,7 @@ mdzeroextend(SMgrRelation reln, ForkNumber forknum, while (remblocks > 0) { BlockNumber segstartblock = curblocknum % ((BlockNumber) RELSEG_SIZE); - off_t seekpos = (off_t) BLCKSZ * segstartblock; + pgoff_t seekpos = (pgoff_t) BLCKSZ * segstartblock; int numblocks; if (segstartblock + remblocks > RELSEG_SIZE) @@ -592,13 +603,24 @@ mdzeroextend(SMgrRelation reln, ForkNumber forknum, * that decision should be made though? For now just use a cutoff of * 8, anything between 4 and 8 worked OK in some local testing. */ - if (numblocks > 8) + if (numblocks > 8 && + file_extend_method != FILE_EXTEND_METHOD_WRITE_ZEROS) { - int ret; + int ret = 0; - ret = FileFallocate(v->mdfd_vfd, - seekpos, (off_t) BLCKSZ * numblocks, - WAIT_EVENT_DATA_FILE_EXTEND); +#ifdef HAVE_POSIX_FALLOCATE + if (file_extend_method == FILE_EXTEND_METHOD_POSIX_FALLOCATE) + { + ret = FileFallocate(v->mdfd_vfd, + seekpos, (pgoff_t) BLCKSZ * numblocks, + WAIT_EVENT_DATA_FILE_EXTEND); + } + else +#endif + { + elog(ERROR, "unsupported file_extend_method: %d", + file_extend_method); + } if (ret != 0) { ereport(ERROR, @@ -620,7 +642,7 @@ mdzeroextend(SMgrRelation reln, ForkNumber forknum, * whole length of the extension. */ ret = FileZero(v->mdfd_vfd, - seekpos, (off_t) BLCKSZ * numblocks, + seekpos, (pgoff_t) BLCKSZ * numblocks, WAIT_EVENT_DATA_FILE_EXTEND); if (ret < 0) ereport(ERROR, @@ -735,7 +757,7 @@ mdprefetch(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, while (nblocks > 0) { - off_t seekpos; + pgoff_t seekpos; MdfdVec *v; int nblocks_this_segment; @@ -744,9 +766,9 @@ mdprefetch(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, if (v == NULL) return false; - seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + seekpos = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE); + Assert(seekpos < (pgoff_t) BLCKSZ * RELSEG_SIZE); nblocks_this_segment = Min(nblocks, @@ -841,7 +863,7 @@ mdreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, { struct iovec iov[PG_IOV_MAX]; int iovcnt; - off_t seekpos; + pgoff_t seekpos; int nbytes; MdfdVec *v; BlockNumber nblocks_this_segment; @@ -851,9 +873,9 @@ mdreadv(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, v = _mdfd_getseg(reln, forknum, blocknum, false, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY); - seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + seekpos = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE); + Assert(seekpos < (pgoff_t) BLCKSZ * RELSEG_SIZE); nblocks_this_segment = Min(nblocks, @@ -976,7 +998,7 @@ mdstartreadv(PgAioHandle *ioh, SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, void **buffers, BlockNumber nblocks) { - off_t seekpos; + pgoff_t seekpos; MdfdVec *v; BlockNumber nblocks_this_segment; struct iovec *iov; @@ -986,9 +1008,9 @@ mdstartreadv(PgAioHandle *ioh, v = _mdfd_getseg(reln, forknum, blocknum, false, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY); - seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + seekpos = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE); + Assert(seekpos < (pgoff_t) BLCKSZ * RELSEG_SIZE); nblocks_this_segment = Min(nblocks, @@ -1058,7 +1080,7 @@ mdwritev(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, { struct iovec iov[PG_IOV_MAX]; int iovcnt; - off_t seekpos; + pgoff_t seekpos; int nbytes; MdfdVec *v; BlockNumber nblocks_this_segment; @@ -1068,9 +1090,9 @@ mdwritev(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, v = _mdfd_getseg(reln, forknum, blocknum, skipFsync, EXTENSION_FAIL | EXTENSION_CREATE_RECOVERY); - seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + seekpos = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - Assert(seekpos < (off_t) BLCKSZ * RELSEG_SIZE); + Assert(seekpos < (pgoff_t) BLCKSZ * RELSEG_SIZE); nblocks_this_segment = Min(nblocks, @@ -1163,7 +1185,7 @@ mdwriteback(SMgrRelation reln, ForkNumber forknum, while (nblocks > 0) { BlockNumber nflush = nblocks; - off_t seekpos; + pgoff_t seekpos; MdfdVec *v; int segnum_start, segnum_end; @@ -1192,9 +1214,9 @@ mdwriteback(SMgrRelation reln, ForkNumber forknum, Assert(nflush >= 1); Assert(nflush <= nblocks); - seekpos = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + seekpos = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - FileWriteback(v->mdfd_vfd, seekpos, (off_t) BLCKSZ * nflush, WAIT_EVENT_DATA_FILE_FLUSH); + FileWriteback(v->mdfd_vfd, seekpos, (pgoff_t) BLCKSZ * nflush, WAIT_EVENT_DATA_FILE_FLUSH); nblocks -= nflush; blocknum += nflush; @@ -1272,6 +1294,9 @@ mdnblocks(SMgrRelation reln, ForkNumber forknum) * functions for this relation or handled interrupts in between. This makes * sure we have opened all active segments, so that truncate loop will get * them all! + * + * If nblocks > curnblk, the request is ignored when we are InRecovery, + * otherwise, an error is raised. */ void mdtruncate(SMgrRelation reln, ForkNumber forknum, @@ -1338,7 +1363,7 @@ mdtruncate(SMgrRelation reln, ForkNumber forknum, */ BlockNumber lastsegblocks = nblocks - priorblocks; - if (FileTruncate(v->mdfd_vfd, (off_t) lastsegblocks * BLCKSZ, WAIT_EVENT_DATA_FILE_TRUNCATE) < 0) + if (FileTruncate(v->mdfd_vfd, (pgoff_t) lastsegblocks * BLCKSZ, WAIT_EVENT_DATA_FILE_TRUNCATE) < 0) ereport(ERROR, (errcode_for_file_access(), errmsg("could not truncate file \"%s\" to %u blocks: %m", @@ -1474,9 +1499,9 @@ mdfd(SMgrRelation reln, ForkNumber forknum, BlockNumber blocknum, uint32 *off) v = _mdfd_getseg(reln, forknum, blocknum, false, EXTENSION_FAIL); - *off = (off_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); + *off = (pgoff_t) BLCKSZ * (blocknum % ((BlockNumber) RELSEG_SIZE)); - Assert(*off < (off_t) BLCKSZ * RELSEG_SIZE); + Assert(*off < (pgoff_t) BLCKSZ * RELSEG_SIZE); return FileGetRawDesc(v->mdfd_vfd); } @@ -1589,7 +1614,7 @@ DropRelationFiles(RelFileLocator *delrels, int ndelrels, bool isRedo) SMgrRelation *srels; int i; - srels = palloc(sizeof(SMgrRelation) * ndelrels); + srels = palloc_array(SMgrRelation, ndelrels); for (i = 0; i < ndelrels; i++) { SMgrRelation srel = smgropen(delrels[i], INVALID_PROC_NUMBER); @@ -1858,7 +1883,7 @@ _mdfd_getseg(SMgrRelation reln, ForkNumber forknum, BlockNumber blkno, static BlockNumber _mdnblocks(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg) { - off_t len; + pgoff_t len; len = FileSize(seg->mdfd_vfd); if (len < 0) diff --git a/src/backend/storage/smgr/meson.build b/src/backend/storage/smgr/meson.build index 9288e35a8529c..3785c40385061 100644 --- a/src/backend/storage/smgr/meson.build +++ b/src/backend/storage/smgr/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'bulk_write.c', diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c index bce37a36d51ba..5391640d8613d 100644 --- a/src/backend/storage/smgr/smgr.c +++ b/src/backend/storage/smgr/smgr.c @@ -52,7 +52,7 @@ * other, more complicated, problems would need to be fixed for that to be * viable (e.g. smgr.c is often called with interrupts already held). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -561,7 +561,7 @@ smgrdounlinkall(SMgrRelation *rels, int nrels, bool isRedo) * create an array which contains all relations to be dropped, and close * each relation's forks at the smgr level while at it */ - rlocators = palloc(sizeof(RelFileLocatorBackend) * nrels); + rlocators = palloc_array(RelFileLocatorBackend, nrels); for (i = 0; i < nrels; i++) { RelFileLocatorBackend rlocator = rels[i]->smgr_rlocator; @@ -898,7 +898,7 @@ smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks, /* Do the truncation */ for (i = 0; i < nforks; i++) { - /* Make the cached size is invalid if we encounter an error. */ + /* Make the cached size invalid if we encounter an error. */ reln->smgr_cached_nblocks[forknum[i]] = InvalidBlockNumber; smgrsw[reln->smgr_which].smgr_truncate(reln, forknum[i], @@ -910,8 +910,17 @@ smgrtruncate(SMgrRelation reln, ForkNumber *forknum, int nforks, * backends to invalidate their copies of smgr_cached_nblocks, and * these ones too at the next command boundary. But ensure they aren't * outright wrong until then. + * + * We can have nblocks > old_nblocks when a relation was truncated + * multiple times, a replica applied all the truncations, and later + * restarts from a restartpoint located before the truncations. The + * relation on disk will be the size of the last truncate. When + * replaying the first truncate, we will have nblocks > current size. + * In such cases, smgr_truncate does nothing, so set the cached size + * to the old size rather than the requested size. */ - reln->smgr_cached_nblocks[forknum[i]] = nblocks[i]; + reln->smgr_cached_nblocks[forknum[i]] = + nblocks[i] > old_nblocks[i] ? old_nblocks[i] : nblocks[i]; } } diff --git a/src/backend/storage/sync/meson.build b/src/backend/storage/sync/meson.build index 52cd52afd1791..325e779bdf88d 100644 --- a/src/backend/storage/sync/meson.build +++ b/src/backend/storage/sync/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'sync.c', diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c index fc16db90133bb..2c964b6f3d941 100644 --- a/src/backend/storage/sync/sync.c +++ b/src/backend/storage/sync/sync.c @@ -3,7 +3,7 @@ * sync.c * File synchronization management code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -31,6 +31,7 @@ #include "storage/md.h" #include "utils/hsearch.h" #include "utils/memutils.h" +#include "utils/wait_event.h" /* * In some contexts (currently, standalone backends and the checkpointer) @@ -531,7 +532,7 @@ RememberSyncRequest(const FileTag *ftag, SyncRequestType type) MemoryContext oldcxt = MemoryContextSwitchTo(pendingOpsCxt); PendingUnlinkEntry *entry; - entry = palloc(sizeof(PendingUnlinkEntry)); + entry = palloc_object(PendingUnlinkEntry); entry->tag = *ftag; entry->cycle_ctr = checkpoint_cycle_ctr; entry->canceled = false; diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index a7d1fec981f88..5abf276c89848 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -3,7 +3,7 @@ * backend_startup.c * Backend startup code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -492,7 +492,7 @@ static int ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) { int32 len; - char *buf; + char *buf = NULL; ProtocolVersion proto; MemoryContext oldcontext; @@ -516,7 +516,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) * scanners, which may be less benign, but it's not really our job to * notice those.) */ - return STATUS_ERROR; + goto fail; } if (pq_getbytes(((char *) &len) + 1, 3) == EOF) @@ -526,7 +526,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) ereport(COMMERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("incomplete startup packet"))); - return STATUS_ERROR; + goto fail; } len = pg_ntoh32(len); @@ -538,7 +538,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) ereport(COMMERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("invalid length of startup packet"))); - return STATUS_ERROR; + goto fail; } /* @@ -554,7 +554,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) ereport(COMMERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("incomplete startup packet"))); - return STATUS_ERROR; + goto fail; } pq_endmsgread(); @@ -568,7 +568,7 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) { ProcessCancelRequestPacket(port, buf, len); /* Not really an error, but we don't want to proceed further */ - return STATUS_ERROR; + goto fail; } if (proto == NEGOTIATE_SSL_CODE && !ssl_done) @@ -607,14 +607,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) ereport(COMMERROR, (errcode_for_socket_access(), errmsg("failed to send SSL negotiation response: %m"))); - return STATUS_ERROR; /* close the connection */ + goto fail; /* close the connection */ } #ifdef USE_SSL if (SSLok == 'S' && secure_open_server(port) == -1) - return STATUS_ERROR; + goto fail; #endif + pfree(buf); + /* * At this point we should have no data already buffered. If we do, * it was received before we performed the SSL handshake, so it wasn't @@ -661,14 +663,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) ereport(COMMERROR, (errcode_for_socket_access(), errmsg("failed to send GSSAPI negotiation response: %m"))); - return STATUS_ERROR; /* close the connection */ + goto fail; /* close the connection */ } #ifdef ENABLE_GSS if (GSSok == 'G' && secure_open_gssapi(port) == -1) - return STATUS_ERROR; + goto fail; #endif + pfree(buf); + /* * At this point we should have no data already buffered. If we do, * it was received before we performed the GSS handshake, so it wasn't @@ -821,6 +825,8 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) if (PG_PROTOCOL_MINOR(proto) > PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST) || unrecognized_protocol_options != NIL) SendNegotiateProtocolVersion(unrecognized_protocol_options); + + list_free_deep(unrecognized_protocol_options); } /* Check a user name was given. */ @@ -842,10 +848,9 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) if (strlen(port->user_name) >= NAMEDATALEN) port->user_name[NAMEDATALEN - 1] = '\0'; + Assert(MyBackendType == B_BACKEND || MyBackendType == B_DEAD_END_BACKEND); if (am_walsender) MyBackendType = B_WAL_SENDER; - else - MyBackendType = B_BACKEND; /* * Normal walsender backends, e.g. for streaming replication, are not @@ -863,7 +868,16 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done) */ MemoryContextSwitchTo(oldcontext); + pfree(buf); + return STATUS_OK; + +fail: + /* be tidy, just to avoid Valgrind complaints */ + if (buf) + pfree(buf); + + return STATUS_ERROR; } /* @@ -881,7 +895,7 @@ ProcessCancelRequestPacket(Port *port, void *pkt, int pktlen) { ereport(COMMERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg("invalid length of query cancel packet"))); + errmsg("invalid length of cancel request packet"))); return; } len = pktlen - offsetof(CancelRequestPacket, cancelAuthCode); @@ -889,7 +903,7 @@ ProcessCancelRequestPacket(Port *port, void *pkt, int pktlen) { ereport(COMMERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), - errmsg("invalid length of query cancel key"))); + errmsg("invalid length of cancel key in cancel request packet"))); return; } @@ -1077,7 +1091,7 @@ check_log_connections(char **newval, void **extra, GucSource source) if (!SplitIdentifierString(rawstring, ',', &elemlist)) { - GUC_check_errdetail("Invalid list syntax in parameter \"log_connections\"."); + GUC_check_errdetail("Invalid list syntax in parameter \"%s\".", "log_connections"); pfree(rawstring); list_free(elemlist); return false; diff --git a/src/backend/tcop/cmdtag.c b/src/backend/tcop/cmdtag.c index fa556187eecba..d38d5b390b9d1 100644 --- a/src/backend/tcop/cmdtag.c +++ b/src/backend/tcop/cmdtag.c @@ -3,7 +3,7 @@ * cmdtag.c * Data and routines for commandtag names and enumeration. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/tcop/dest.c b/src/backend/tcop/dest.c index b620766c93888..fb163930c896b 100644 --- a/src/backend/tcop/dest.c +++ b/src/backend/tcop/dest.c @@ -4,7 +4,7 @@ * support for communication destinations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -180,6 +180,7 @@ EndCommand(const QueryCompletion *qc, CommandDest dest, bool force_undecorated_o len = BuildQueryCompletionString(completionTag, qc, force_undecorated_output); pq_putmessage(PqMsg_CommandComplete, completionTag, len + 1); + break; case DestNone: case DestDebug: diff --git a/src/backend/tcop/fastpath.c b/src/backend/tcop/fastpath.c index 62f9ffa0dc088..52772bc90a857 100644 --- a/src/backend/tcop/fastpath.c +++ b/src/backend/tcop/fastpath.c @@ -3,7 +3,7 @@ * fastpath.c * routines to handle function requests from the frontend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/tcop/meson.build b/src/backend/tcop/meson.build index a312d93087bd0..31f6074f9c56c 100644 --- a/src/backend/tcop/meson.build +++ b/src/backend/tcop/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'backend_startup.c', diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index c242c8170b562..2c1f14b78891a 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3,7 +3,7 @@ * postgres.c * POSTGRES C Backend Interface * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -37,7 +37,9 @@ #include "catalog/pg_type.h" #include "commands/async.h" #include "commands/event_trigger.h" +#include "commands/explain_state.h" #include "commands/prepare.h" +#include "commands/repack.h" #include "common/pg_prng.h" #include "jit/jit.h" #include "libpq/libpq.h" @@ -50,22 +52,26 @@ #include "optimizer/optimizer.h" #include "parser/analyze.h" #include "parser/parser.h" -#include "pg_getopt.h" #include "pg_trace.h" #include "pgstat.h" +#include "port/pg_getopt_ctx.h" #include "postmaster/interrupt.h" #include "postmaster/postmaster.h" #include "replication/logicallauncher.h" #include "replication/logicalworker.h" +#include "replication/slotsync.h" #include "replication/slot.h" #include "replication/walsender.h" #include "rewrite/rewriteHandler.h" #include "storage/bufmgr.h" #include "storage/ipc.h" +#include "storage/fd.h" #include "storage/pmsignal.h" #include "storage/proc.h" #include "storage/procsignal.h" +#include "storage/shmem_internal.h" #include "storage/sinval.h" +#include "storage/standby.h" #include "tcop/backend_startup.h" #include "tcop/fastpath.h" #include "tcop/pquery.h" @@ -104,6 +110,14 @@ int client_connection_check_interval = 0; /* flags for non-system relation kinds to restrict use */ int restrict_nonsystem_relation_kind; +/* + * Include signal sender PID/UID as errdetail when available (SA_SIGINFO). + * The caller must supply the (already-captured) pid and uid values. + */ +#define ERRDETAIL_SIGNAL_SENDER(pid, uid) \ + ((pid) == 0 ? 0 : \ + errdetail("Signal sent by PID %d, UID %d.", (int) (pid), (int) (uid))) + /* ---------------- * private typedefs etc * ---------------- @@ -154,10 +168,6 @@ static const char *userDoption = NULL; /* -D switch */ static bool EchoQuery = false; /* -E switch */ static bool UseSemiNewlineNewline = false; /* -j switch */ -/* whether or not, and why, we were canceled by conflict with recovery */ -static volatile sig_atomic_t RecoveryConflictPending = false; -static volatile sig_atomic_t RecoveryConflictPendingReasons[NUM_PROCSIGNALS]; - /* reused buffer to pass to SendRowDescriptionMessage() */ static MemoryContext row_description_context = NULL; static StringInfoData row_description_buf; @@ -174,7 +184,6 @@ static void forbidden_in_wal_sender(char firstchar); static bool check_log_statement(List *stmt_list); static int errdetail_execute(List *raw_parsetree_list); static int errdetail_params(ParamListInfo params); -static int errdetail_abort(void); static void bind_param_error_callback(void *arg); static void start_xact_command(void); static void finish_xact_command(void); @@ -182,6 +191,9 @@ static bool IsTransactionExitStmt(Node *parsetree); static bool IsTransactionExitStmtList(List *pstmts); static bool IsTransactionStmtList(List *pstmts); static void drop_unnamed_stmt(void); +static void ProcessRecoveryConflictInterrupts(void); +static void ProcessRecoveryConflictInterrupt(RecoveryConflictReason reason); +static void report_recovery_conflict(RecoveryConflictReason reason); static void log_disconnections(int code, Datum arg); static void enable_statement_timeout(void); static void disable_statement_timeout(void); @@ -649,6 +661,10 @@ pg_parse_query(const char *query_string) TRACE_POSTGRESQL_QUERY_PARSE_DONE(query_string); + if (Debug_print_raw_parse) + elog_node_display(LOG, "raw parse tree", raw_parsetree_list, + Debug_pretty_print); + return raw_parsetree_list; } @@ -880,7 +896,7 @@ pg_rewrite_query(Query *query) */ PlannedStmt * pg_plan_query(Query *querytree, const char *query_string, int cursorOptions, - ParamListInfo boundParams) + ParamListInfo boundParams, ExplainState *es) { PlannedStmt *plan; @@ -897,7 +913,7 @@ pg_plan_query(Query *querytree, const char *query_string, int cursorOptions, ResetUsage(); /* call the optimizer */ - plan = planner(querytree, query_string, cursorOptions, boundParams); + plan = planner(querytree, query_string, cursorOptions, boundParams, es); if (log_planner_stats) ShowUsage("PLANNER STATISTICS"); @@ -988,11 +1004,12 @@ pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions, stmt->stmt_location = query->stmt_location; stmt->stmt_len = query->stmt_len; stmt->queryId = query->queryId; + stmt->planOrigin = PLAN_STMT_INTERNAL; } else { stmt = pg_plan_query(query, query_string, cursorOptions, - boundParams); + boundParams, NULL); } stmt_list = lappend(stmt_list, stmt); @@ -1111,7 +1128,7 @@ exec_simple_query(const char *query_string) /* * Get the command name for use in status display (it also becomes the - * default completion tag, down inside PortalRun). Set ps_status and + * default completion tag, in PortalDefineQuery). Set ps_status and * do any special start-of-SQL-command processing needed by the * destination. */ @@ -1135,8 +1152,7 @@ exec_simple_query(const char *query_string) ereport(ERROR, (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION), errmsg("current transaction is aborted, " - "commands ignored until end of transaction block"), - errdetail_abort())); + "commands ignored until end of transaction block"))); /* Make sure we are in a transaction command */ start_xact_command(); @@ -1492,8 +1508,7 @@ exec_parse_message(const char *query_string, /* string to execute */ ereport(ERROR, (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION), errmsg("current transaction is aborted, " - "commands ignored until end of transaction block"), - errdetail_abort())); + "commands ignored until end of transaction block"))); /* * Create the CachedPlanSource before we do parse analysis, since it @@ -1682,7 +1697,7 @@ exec_bind_message(StringInfo input_message) { Query *query = lfirst_node(Query, lc); - if (query->queryId != UINT64CONST(0)) + if (query->queryId != INT64CONST(0)) { pgstat_report_query_id(query->queryId, false); break; @@ -1744,8 +1759,7 @@ exec_bind_message(StringInfo input_message) ereport(ERROR, (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION), errmsg("current transaction is aborted, " - "commands ignored until end of transaction block"), - errdetail_abort())); + "commands ignored until end of transaction block"))); /* * Create the portal. Allow silent replacement of an existing portal only @@ -2034,7 +2048,7 @@ exec_bind_message(StringInfo input_message) { PlannedStmt *plan = lfirst_node(PlannedStmt, lc); - if (plan->planId != UINT64CONST(0)) + if (plan->planId != INT64CONST(0)) { pgstat_report_plan_id(plan->planId, false); break; @@ -2174,7 +2188,7 @@ exec_execute_message(const char *portal_name, long max_rows) { PlannedStmt *stmt = lfirst_node(PlannedStmt, lc); - if (stmt->queryId != UINT64CONST(0)) + if (stmt->queryId != INT64CONST(0)) { pgstat_report_query_id(stmt->queryId, false); break; @@ -2185,7 +2199,7 @@ exec_execute_message(const char *portal_name, long max_rows) { PlannedStmt *stmt = lfirst_node(PlannedStmt, lc); - if (stmt->planId != UINT64CONST(0)) + if (stmt->planId != INT64CONST(0)) { pgstat_report_plan_id(stmt->planId, false); break; @@ -2249,8 +2263,7 @@ exec_execute_message(const char *portal_name, long max_rows) ereport(ERROR, (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION), errmsg("current transaction is aborted, " - "commands ignored until end of transaction block"), - errdetail_abort())); + "commands ignored until end of transaction block"))); /* Check for cancel signal before we start execution */ CHECK_FOR_INTERRUPTS(); @@ -2530,54 +2543,40 @@ errdetail_params(ParamListInfo params) return 0; } -/* - * errdetail_abort - * - * Add an errdetail() line showing abort reason, if any. - */ -static int -errdetail_abort(void) -{ - if (MyProc->recoveryConflictPending) - errdetail("Abort reason: recovery conflict"); - - return 0; -} - /* * errdetail_recovery_conflict * * Add an errdetail() line showing conflict source. */ static int -errdetail_recovery_conflict(ProcSignalReason reason) +errdetail_recovery_conflict(RecoveryConflictReason reason) { switch (reason) { - case PROCSIG_RECOVERY_CONFLICT_BUFFERPIN: + case RECOVERY_CONFLICT_BUFFERPIN: errdetail("User was holding shared buffer pin for too long."); break; - case PROCSIG_RECOVERY_CONFLICT_LOCK: + case RECOVERY_CONFLICT_LOCK: errdetail("User was holding a relation lock for too long."); break; - case PROCSIG_RECOVERY_CONFLICT_TABLESPACE: + case RECOVERY_CONFLICT_TABLESPACE: errdetail("User was or might have been using tablespace that must be dropped."); break; - case PROCSIG_RECOVERY_CONFLICT_SNAPSHOT: + case RECOVERY_CONFLICT_SNAPSHOT: errdetail("User query might have needed to see row versions that must be removed."); break; - case PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT: + case RECOVERY_CONFLICT_LOGICALSLOT: errdetail("User was using a logical replication slot that must be invalidated."); break; - case PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK: + case RECOVERY_CONFLICT_STARTUP_DEADLOCK: + errdetail("User transaction caused deadlock with recovery."); + break; + case RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK: errdetail("User transaction caused buffer deadlock with recovery."); break; - case PROCSIG_RECOVERY_CONFLICT_DATABASE: + case RECOVERY_CONFLICT_DATABASE: errdetail("User was connected to a database that must be dropped."); break; - default: - break; - /* no errdetail */ } return 0; @@ -2686,8 +2685,7 @@ exec_describe_statement_message(const char *stmt_name) ereport(ERROR, (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION), errmsg("current transaction is aborted, " - "commands ignored until end of transaction block"), - errdetail_abort())); + "commands ignored until end of transaction block"))); if (whereToSendOutput != DestRemote) return; /* can't actually do anything... */ @@ -2763,8 +2761,7 @@ exec_describe_portal_message(const char *portal_name) ereport(ERROR, (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION), errmsg("current transaction is aborted, " - "commands ignored until end of transaction block"), - errdetail_abort())); + "commands ignored until end of transaction block"))); if (whereToSendOutput != DestRemote) return; /* can't actually do anything... */ @@ -3030,6 +3027,17 @@ die(SIGNAL_ARGS) { InterruptPending = true; ProcDiePending = true; + + /* + * Record who sent the signal. Will be 0 on platforms without + * SA_SIGINFO, which is fine -- ProcessInterrupts() checks for that. + * Only set on the first SIGTERM so we report the original sender. + */ + if (ProcDieSenderPid == 0) + { + ProcDieSenderPid = pg_siginfo->pid; + ProcDieSenderUid = pg_siginfo->uid; + } } /* for the cumulative stats system */ @@ -3082,15 +3090,14 @@ FloatExceptionHandler(SIGNAL_ARGS) } /* - * Tell the next CHECK_FOR_INTERRUPTS() to check for a particular type of - * recovery conflict. Runs in a SIGUSR1 handler. + * Tell the next CHECK_FOR_INTERRUPTS() to process recovery conflicts. Runs + * in a SIGUSR1 handler. */ void -HandleRecoveryConflictInterrupt(ProcSignalReason reason) +HandleRecoveryConflictInterrupt(void) { - RecoveryConflictPendingReasons[reason] = true; - RecoveryConflictPending = true; - InterruptPending = true; + if (pg_atomic_read_u32(&MyProc->pendingRecoveryConflicts) != 0) + InterruptPending = true; /* latch will be set by procsignal_sigusr1_handler */ } @@ -3098,49 +3105,73 @@ HandleRecoveryConflictInterrupt(ProcSignalReason reason) * Check one individual conflict reason. */ static void -ProcessRecoveryConflictInterrupt(ProcSignalReason reason) +ProcessRecoveryConflictInterrupt(RecoveryConflictReason reason) { switch (reason) { - case PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK: + case RECOVERY_CONFLICT_STARTUP_DEADLOCK: /* + * The startup process is waiting on a lock held by us, and has + * requested us to check if it is a deadlock (i.e. the deadlock + * timeout expired). + * * If we aren't waiting for a lock we can never deadlock. */ if (GetAwaitedLock() == NULL) return; - /* Intentional fall through to check wait for pin */ - /* FALLTHROUGH */ + /* Set the flag so that ProcSleep() will check for deadlocks. */ + CheckDeadLockAlert(); + return; - case PROCSIG_RECOVERY_CONFLICT_BUFFERPIN: + case RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK: /* - * If PROCSIG_RECOVERY_CONFLICT_BUFFERPIN is requested but we - * aren't blocking the Startup process there is nothing more to - * do. + * The startup process is waiting on a buffer pin, and has + * requested us to check if there is a deadlock involving the pin. * - * When PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK is requested, - * if we're waiting for locks and the startup process is not - * waiting for buffer pin (i.e., also waiting for locks), we set - * the flag so that ProcSleep() will check for deadlocks. + * If we're not waiting on a lock, there can be no deadlock. + */ + if (GetAwaitedLock() == NULL) + return; + + /* + * If we're not holding the buffer pin, also no deadlock. (The + * startup process doesn't know who's holding the pin, and sends + * this signal to *all* backends, so this is the common case.) */ if (!HoldingBufferPinThatDelaysRecovery()) - { - if (reason == PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK && - GetStartupBufferPinWaitBufId() < 0) - CheckDeadLockAlert(); return; - } - MyProc->recoveryConflictPending = true; + /* + * Otherwise, we probably have a deadlock. Unfortunately the + * normal deadlock detector doesn't know about buffer pins, so we + * cannot perform comprehensively deadlock check. Instead, we + * just assume that it is a deadlock if the above two conditions + * are met. In principle this can lead to false positives, but + * it's rare in practice because sessions in a hot standby server + * rarely hold locks that can block other backends. + */ + report_recovery_conflict(reason); + return; - /* Intentional fall through to error handling */ - /* FALLTHROUGH */ + case RECOVERY_CONFLICT_BUFFERPIN: - case PROCSIG_RECOVERY_CONFLICT_LOCK: - case PROCSIG_RECOVERY_CONFLICT_TABLESPACE: - case PROCSIG_RECOVERY_CONFLICT_SNAPSHOT: + /* + * Someone is holding a buffer pin that the startup process is + * waiting for, and it got tired of waiting. If that's us, error + * out to release the pin. + */ + if (!HoldingBufferPinThatDelaysRecovery()) + return; + + report_recovery_conflict(reason); + return; + + case RECOVERY_CONFLICT_LOCK: + case RECOVERY_CONFLICT_TABLESPACE: + case RECOVERY_CONFLICT_SNAPSHOT: /* * If we aren't in a transaction any longer then ignore. @@ -3148,108 +3179,128 @@ ProcessRecoveryConflictInterrupt(ProcSignalReason reason) if (!IsTransactionOrTransactionBlock()) return; - /* FALLTHROUGH */ + report_recovery_conflict(reason); + return; - case PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT: + case RECOVERY_CONFLICT_LOGICALSLOT: + report_recovery_conflict(reason); + return; - /* - * If we're not in a subtransaction then we are OK to throw an - * ERROR to resolve the conflict. Otherwise drop through to the - * FATAL case. - * - * PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT is a special case that - * always throws an ERROR (ie never promotes to FATAL), though it - * still has to respect QueryCancelHoldoffCount, so it shares this - * code path. Logical decoding slots are only acquired while - * performing logical decoding. During logical decoding no user - * controlled code is run. During [sub]transaction abort, the - * slot is released. Therefore user controlled code cannot - * intercept an error before the replication slot is released. - * - * XXX other times that we can throw just an ERROR *may* be - * PROCSIG_RECOVERY_CONFLICT_LOCK if no locks are held in parent - * transactions - * - * PROCSIG_RECOVERY_CONFLICT_SNAPSHOT if no snapshots are held by - * parent transactions and the transaction is not - * transaction-snapshot mode - * - * PROCSIG_RECOVERY_CONFLICT_TABLESPACE if no temp files or - * cursors open in parent transactions - */ - if (reason == PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT || - !IsSubTransaction()) - { - /* - * If we already aborted then we no longer need to cancel. We - * do this here since we do not wish to ignore aborted - * subtransactions, which must cause FATAL, currently. - */ - if (IsAbortedTransactionBlockState()) - return; + case RECOVERY_CONFLICT_DATABASE: - /* - * If a recovery conflict happens while we are waiting for - * input from the client, the client is presumably just - * sitting idle in a transaction, preventing recovery from - * making progress. We'll drop through to the FATAL case - * below to dislodge it, in that case. - */ - if (!DoingCommandRead) - { - /* Avoid losing sync in the FE/BE protocol. */ - if (QueryCancelHoldoffCount != 0) - { - /* - * Re-arm and defer this interrupt until later. See - * similar code in ProcessInterrupts(). - */ - RecoveryConflictPendingReasons[reason] = true; - RecoveryConflictPending = true; - InterruptPending = true; - return; - } + /* The database is being dropped; terminate the session */ + report_recovery_conflict(reason); + return; + } + elog(FATAL, "unrecognized conflict mode: %d", (int) reason); +} - /* - * We are cleared to throw an ERROR. Either it's the - * logical slot case, or we have a top-level transaction - * that we can abort and a conflict that isn't inherently - * non-retryable. - */ - LockErrorCleanup(); - pgstat_report_recovery_conflict(reason); - ereport(ERROR, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("canceling statement due to conflict with recovery"), - errdetail_recovery_conflict(reason))); - break; - } - } +/* + * This transaction or session is conflicting with recovery and needs to be + * killed. Roll back the transaction, if that's sufficient, or terminate the + * connection, or do nothing if we're already in an aborted state. + */ +static void +report_recovery_conflict(RecoveryConflictReason reason) +{ + bool fatal; + + if (reason == RECOVERY_CONFLICT_DATABASE) + { + /* note: no hint about reconnecting, and different errcode */ + pgstat_report_recovery_conflict(reason); + ereport(FATAL, + (errcode(ERRCODE_DATABASE_DROPPED), + errmsg("terminating connection due to conflict with recovery"), + errdetail_recovery_conflict(reason))); + } + if (reason == RECOVERY_CONFLICT_LOGICALSLOT) + { + /* + * RECOVERY_CONFLICT_LOGICALSLOT is a special case that always throws + * an ERROR (ie never promotes to FATAL), though it still has to + * respect QueryCancelHoldoffCount, so it shares this code path. + * Logical decoding slots are only acquired while performing logical + * decoding. During logical decoding no user controlled code is run. + * During [sub]transaction abort, the slot is released. Therefore + * user controlled code cannot intercept an error before the + * replication slot is released. + */ + fatal = false; + } + else + { + fatal = IsSubTransaction(); + } - /* Intentional fall through to session cancel */ - /* FALLTHROUGH */ + /* + * If we're not in a subtransaction then we are OK to throw an ERROR to + * resolve the conflict. + * + * XXX other times that we can throw just an ERROR *may* be + * RECOVERY_CONFLICT_LOCK if no locks are held in parent transactions + * + * RECOVERY_CONFLICT_SNAPSHOT if no snapshots are held by parent + * transactions and the transaction is not transaction-snapshot mode + * + * RECOVERY_CONFLICT_TABLESPACE if no temp files or cursors open in parent + * transactions + */ + if (!fatal) + { + /* + * If we already aborted then we no longer need to cancel. We do this + * here since we do not wish to ignore aborted subtransactions, which + * must cause FATAL, currently. + */ + if (IsAbortedTransactionBlockState()) + return; - case PROCSIG_RECOVERY_CONFLICT_DATABASE: + /* + * If a recovery conflict happens while we are waiting for input from + * the client, the client is presumably just sitting idle in a + * transaction, preventing recovery from making progress. We'll drop + * through to the FATAL case below to dislodge it, in that case. + */ + if (!DoingCommandRead) + { + /* Avoid losing sync in the FE/BE protocol. */ + if (QueryCancelHoldoffCount != 0) + { + /* + * Re-arm and defer this interrupt until later. See similar + * code in ProcessInterrupts(). + */ + (void) pg_atomic_fetch_or_u32(&MyProc->pendingRecoveryConflicts, (1 << reason)); + InterruptPending = true; + return; + } /* - * Retrying is not possible because the database is dropped, or we - * decided above that we couldn't resolve the conflict with an - * ERROR and fell through. Terminate the session. + * We are cleared to throw an ERROR. Either it's the logical slot + * case, or we have a top-level transaction that we can abort and + * a conflict that isn't inherently non-retryable. */ + LockErrorCleanup(); pgstat_report_recovery_conflict(reason); - ereport(FATAL, - (errcode(reason == PROCSIG_RECOVERY_CONFLICT_DATABASE ? - ERRCODE_DATABASE_DROPPED : - ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("terminating connection due to conflict with recovery"), - errdetail_recovery_conflict(reason), - errhint("In a moment you should be able to reconnect to the" - " database and repeat your command."))); - break; - - default: - elog(FATAL, "unrecognized conflict mode: %d", (int) reason); + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("canceling statement due to conflict with recovery"), + errdetail_recovery_conflict(reason))); + } } + + /* + * We couldn't resolve the conflict with ERROR, so terminate the whole + * session. + */ + pgstat_report_recovery_conflict(reason); + ereport(FATAL, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("terminating connection due to conflict with recovery"), + errdetail_recovery_conflict(reason), + errhint("In a moment you should be able to reconnect to the" + " database and repeat your command."))); } /* @@ -3258,6 +3309,8 @@ ProcessRecoveryConflictInterrupt(ProcSignalReason reason) static void ProcessRecoveryConflictInterrupts(void) { + uint32 pending; + /* * We don't need to worry about joggling the elbow of proc_exit, because * proc_exit_prepare() holds interrupts, so ProcessInterrupts() won't call @@ -3265,17 +3318,27 @@ ProcessRecoveryConflictInterrupts(void) */ Assert(!proc_exit_inprogress); Assert(InterruptHoldoffCount == 0); - Assert(RecoveryConflictPending); - RecoveryConflictPending = false; + /* Are any recovery conflict pending? */ + pending = pg_atomic_read_membarrier_u32(&MyProc->pendingRecoveryConflicts); + if (pending == 0) + return; - for (ProcSignalReason reason = PROCSIG_RECOVERY_CONFLICT_FIRST; - reason <= PROCSIG_RECOVERY_CONFLICT_LAST; + /* + * Check the conflicts one by one, clearing each flag only before + * processing the particular conflict. This ensures that if multiple + * conflicts are pending, we come back here to process the remaining + * conflicts, if an error is thrown during processing one of them. + */ + for (RecoveryConflictReason reason = 0; + reason < NUM_RECOVERY_CONFLICT_REASONS; reason++) { - if (RecoveryConflictPendingReasons[reason]) + if ((pending & (1 << reason)) != 0) { - RecoveryConflictPendingReasons[reason] = false; + /* clear the flag */ + (void) pg_atomic_fetch_and_u32(&MyProc->pendingRecoveryConflicts, ~(1 << reason)); + ProcessRecoveryConflictInterrupt(reason); } } @@ -3304,7 +3367,12 @@ ProcessInterrupts(void) if (ProcDiePending) { + int sender_pid = ProcDieSenderPid; + int sender_uid = ProcDieSenderUid; + ProcDiePending = false; + ProcDieSenderPid = 0; + ProcDieSenderUid = 0; QueryCancelPending = false; /* ProcDie trumps QueryCancel */ LockErrorCleanup(); /* As in quickdie, don't risk sending to client during auth */ @@ -3317,15 +3385,18 @@ ProcessInterrupts(void) else if (AmAutoVacuumWorkerProcess()) ereport(FATAL, (errcode(ERRCODE_ADMIN_SHUTDOWN), - errmsg("terminating autovacuum process due to administrator command"))); + errmsg("terminating autovacuum process due to administrator command"), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); else if (IsLogicalWorker()) ereport(FATAL, (errcode(ERRCODE_ADMIN_SHUTDOWN), - errmsg("terminating logical replication worker due to administrator command"))); + errmsg("terminating logical replication worker due to administrator command"), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); else if (IsLogicalLauncher()) { ereport(DEBUG1, - (errmsg_internal("logical replication launcher shutting down"))); + (errmsg_internal("logical replication launcher shutting down"), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); /* * The logical replication launcher can be stopped at any time. @@ -3336,23 +3407,27 @@ ProcessInterrupts(void) else if (AmWalReceiverProcess()) ereport(FATAL, (errcode(ERRCODE_ADMIN_SHUTDOWN), - errmsg("terminating walreceiver process due to administrator command"))); + errmsg("terminating walreceiver process due to administrator command"), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); else if (AmBackgroundWorkerProcess()) ereport(FATAL, (errcode(ERRCODE_ADMIN_SHUTDOWN), errmsg("terminating background worker \"%s\" due to administrator command", - MyBgworkerEntry->bgw_type))); + MyBgworkerEntry->bgw_type), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); else if (AmIoWorkerProcess()) { ereport(DEBUG1, - (errmsg_internal("io worker shutting down due to administrator command"))); + (errmsg_internal("io worker shutting down due to administrator command"), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); proc_exit(0); } else ereport(FATAL, (errcode(ERRCODE_ADMIN_SHUTDOWN), - errmsg("terminating connection due to administrator command"))); + errmsg("terminating connection due to administrator command"), + ERRDETAIL_SIGNAL_SENDER(sender_pid, sender_uid))); } if (CheckClientConnectionPending) @@ -3466,7 +3541,7 @@ ProcessInterrupts(void) } } - if (RecoveryConflictPending) + if (pg_atomic_read_u32(&MyProc->pendingRecoveryConflicts) != 0) ProcessRecoveryConflictInterrupts(); if (IdleInTransactionSessionTimeoutPending) @@ -3535,6 +3610,12 @@ ProcessInterrupts(void) if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); + + if (SlotSyncShutdownPending) + ProcessSlotSyncMessage(); + + if (RepackMessagePending) + ProcessRepackMessages(); } /* @@ -3696,7 +3777,10 @@ set_debug_options(int debug_flag, GucContext context, GucSource source) if (debug_flag >= 2) SetConfigOption("log_statement", "all", context, source); if (debug_flag >= 3) + { + SetConfigOption("debug_print_raw_parse", "true", context, source); SetConfigOption("debug_print_parse", "true", context, source); + } if (debug_flag >= 4) SetConfigOption("debug_print_plan", "true", context, source); if (debug_flag >= 5) @@ -3752,9 +3836,9 @@ get_stats_option_name(const char *arg) switch (arg[0]) { case 'p': - if (optarg[1] == 'a') /* "parser" */ + if (arg[1] == 'a') /* "parser" */ return "log_parser_stats"; - else if (optarg[1] == 'l') /* "planner" */ + else if (arg[1] == 'l') /* "planner" */ return "log_planner_stats"; break; @@ -3794,6 +3878,7 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, int errs = 0; GucSource gucsource; int flag; + pg_getopt_ctx optctx; if (secure) { @@ -3811,27 +3896,26 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, gucsource = PGC_S_CLIENT; /* switches came from client */ } -#ifdef HAVE_INT_OPTERR + /* + * Parse command-line options. CAUTION: keep this in sync with + * postmaster/postmaster.c (the option sets should not conflict) and with + * the common help() function in main/main.c. + */ + pg_getopt_start(&optctx, argc, argv, "B:bC:c:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:v:W:-:"); /* * Turn this off because it's either printed to stderr and not the log * where we'd want it, or argv[0] is now "--single", which would make for * a weird error message. We print our own error message below. */ - opterr = 0; -#endif + optctx.opterr = 0; - /* - * Parse command-line options. CAUTION: keep this in sync with - * postmaster/postmaster.c (the option sets should not conflict) and with - * the common help() function in main/main.c. - */ - while ((flag = getopt(argc, argv, "B:bC:c:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:v:W:-:")) != -1) + while ((flag = pg_getopt_next(&optctx)) != -1) { switch (flag) { case 'B': - SetConfigOption("shared_buffers", optarg, ctx, gucsource); + SetConfigOption("shared_buffers", optctx.optarg, ctx, gucsource); break; case 'b': @@ -3852,30 +3936,30 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, * returns DISPATCH_POSTMASTER if it doesn't find a match, so * error for anything else. */ - if (parse_dispatch_option(optarg) != DISPATCH_POSTMASTER) + if (parse_dispatch_option(optctx.optarg) != DISPATCH_POSTMASTER) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("--%s must be first argument", optarg))); + errmsg("--%s must be first argument", optctx.optarg))); - /* FALLTHROUGH */ + pg_fallthrough; case 'c': { char *name, *value; - ParseLongOption(optarg, &name, &value); + ParseLongOption(optctx.optarg, &name, &value); if (!value) { if (flag == '-') ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("--%s requires a value", - optarg))); + optctx.optarg))); else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("-c %s requires a value", - optarg))); + optctx.optarg))); } SetConfigOption(name, value, ctx, gucsource); pfree(name); @@ -3885,11 +3969,11 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, case 'D': if (secure) - userDoption = strdup(optarg); + userDoption = strdup(optctx.optarg); break; case 'd': - set_debug_options(atoi(optarg), ctx, gucsource); + set_debug_options(atoi(optctx.optarg), ctx, gucsource); break; case 'E': @@ -3906,12 +3990,12 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, break; case 'f': - if (!set_plan_disabling_options(optarg, ctx, gucsource)) + if (!set_plan_disabling_options(optctx.optarg, ctx, gucsource)) errs++; break; case 'h': - SetConfigOption("listen_addresses", optarg, ctx, gucsource); + SetConfigOption("listen_addresses", optctx.optarg, ctx, gucsource); break; case 'i': @@ -3924,7 +4008,7 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, break; case 'k': - SetConfigOption("unix_socket_directories", optarg, ctx, gucsource); + SetConfigOption("unix_socket_directories", optctx.optarg, ctx, gucsource); break; case 'l': @@ -3932,7 +4016,7 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, break; case 'N': - SetConfigOption("max_connections", optarg, ctx, gucsource); + SetConfigOption("max_connections", optctx.optarg, ctx, gucsource); break; case 'n': @@ -3948,17 +4032,17 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, break; case 'p': - SetConfigOption("port", optarg, ctx, gucsource); + SetConfigOption("port", optctx.optarg, ctx, gucsource); break; case 'r': /* send output (stdout and stderr) to the given file */ if (secure) - strlcpy(OutputFileName, optarg, MAXPGPATH); + strlcpy(OutputFileName, optctx.optarg, MAXPGPATH); break; case 'S': - SetConfigOption("work_mem", optarg, ctx, gucsource); + SetConfigOption("work_mem", optctx.optarg, ctx, gucsource); break; case 's': @@ -3971,7 +4055,7 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, case 't': { - const char *tmp = get_stats_option_name(optarg); + const char *tmp = get_stats_option_name(optctx.optarg); if (tmp) SetConfigOption(tmp, "true", ctx, gucsource); @@ -3990,11 +4074,11 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, * standalone backend. */ if (secure) - FrontendProtocol = (ProtocolVersion) atoi(optarg); + FrontendProtocol = (ProtocolVersion) atoi(optctx.optarg); break; case 'W': - SetConfigOption("post_auth_delay", optarg, ctx, gucsource); + SetConfigOption("post_auth_delay", optctx.optarg, ctx, gucsource); break; default: @@ -4009,36 +4093,27 @@ process_postgres_switches(int argc, char *argv[], GucContext ctx, /* * Optional database name should be there only if *dbname is NULL. */ - if (!errs && dbname && *dbname == NULL && argc - optind >= 1) - *dbname = strdup(argv[optind++]); + if (!errs && dbname && *dbname == NULL && argc - optctx.optind >= 1) + *dbname = strdup(argv[optctx.optind++]); - if (errs || argc != optind) + if (errs || argc != optctx.optind) { if (errs) - optind--; /* complain about the previous argument */ + optctx.optind--; /* complain about the previous argument */ /* spell the error message a bit differently depending on context */ if (IsUnderPostmaster) ereport(FATAL, errcode(ERRCODE_SYNTAX_ERROR), - errmsg("invalid command-line argument for server process: %s", argv[optind]), + errmsg("invalid command-line argument for server process: %s", argv[optctx.optind]), errhint("Try \"%s --help\" for more information.", progname)); else ereport(FATAL, errcode(ERRCODE_SYNTAX_ERROR), errmsg("%s: invalid command-line argument: %s", - progname, argv[optind]), + progname, argv[optctx.optind]), errhint("Try \"%s --help\" for more information.", progname)); } - - /* - * Reset getopt(3) library so that it will work correctly in subprocesses - * or when this function is called a second time with another array. - */ - optind = 1; -#ifdef HAVE_INT_OPTRESET - optreset = 1; /* some systems need this too */ -#endif } @@ -4102,6 +4177,9 @@ PostgresSingleUserMain(int argc, char *argv[], /* read control file (error checking and contains config ) */ LocalProcessControlFile(false); + /* Register the shared memory needs of all core subsystems. */ + RegisterBuiltinShmemCallbacks(); + /* * process any libraries that should be preloaded at postmaster start */ @@ -4120,10 +4198,21 @@ PostgresSingleUserMain(int argc, char *argv[], InitializeFastPathLocks(); /* - * Give preloaded libraries a chance to request additional shared memory. + * Also call any legacy shmem request hooks that might'be been installed + * by preloaded libraries. + * + * Note: this must be done before ShmemCallRequestCallbacks(), because the + * hooks may request LWLocks with RequestNamedLWLockTranche(), which in + * turn affects the size of the LWLock array calculated in lwlock.c. */ process_shmem_requests(); + /* + * Before computing the total size needed, give all subsystems, including + * add-ins, a chance to chance to adjust their requested shmem sizes. + */ + ShmemCallRequestCallbacks(); + /* * Now that loadable modules have had their chance to request additional * shared memory, determine the value of any runtime-computed GUCs that @@ -4238,17 +4327,17 @@ PostgresMain(const char *dbname, const char *username) * returns to outer loop. This seems safer than forcing exit in the * midst of output during who-knows-what operation... */ - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGUSR2, SIG_IGN); + pqsignal(SIGUSR2, PG_SIG_IGN); pqsignal(SIGFPE, FloatExceptionHandler); /* * Reset some signals that are accepted by postmaster but not by * backend */ - pqsignal(SIGCHLD, SIG_DFL); /* system() requires this on some - * platforms */ + pqsignal(SIGCHLD, PG_SIG_DFL); /* system() requires this on some + * platforms */ } /* Early initialization */ @@ -4355,7 +4444,7 @@ PostgresMain(const char *dbname, const char *username) /* * Create memory context and buffer used for RowDescription messages. As * SendRowDescriptionMessage(), via exec_describe_statement_message(), is - * frequently executed for ever single statement, we don't want to + * frequently executed for every single statement, we don't want to * allocate a separate buffer every time. */ row_description_context = AllocSetContextCreate(TopMemoryContext, @@ -4981,7 +5070,7 @@ PostgresMain(const char *dbname, const char *username) /* for the cumulative statistics system */ pgStatSessionEndCause = DISCONNECT_CLIENT_EOF; - /* FALLTHROUGH */ + pg_fallthrough; case PqMsg_Terminate: diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index d1593f38b35fd..ee73100082020 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -3,7 +3,7 @@ * pquery.c * POSTGRES process query command code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -74,7 +74,7 @@ CreateQueryDesc(PlannedStmt *plannedstmt, QueryEnvironment *queryEnv, int instrument_options) { - QueryDesc *qd = (QueryDesc *) palloc(sizeof(QueryDesc)); + QueryDesc *qd = palloc_object(QueryDesc); qd->operation = plannedstmt->commandType; /* operation */ qd->plannedstmt = plannedstmt; /* plan */ @@ -86,12 +86,13 @@ CreateQueryDesc(PlannedStmt *plannedstmt, qd->params = params; /* parameter values passed into query */ qd->queryEnv = queryEnv; qd->instrument_options = instrument_options; /* instrumentation wanted? */ + qd->query_instr_options = 0; /* null these fields until set by ExecutorStart */ qd->tupDesc = NULL; qd->estate = NULL; qd->planstate = NULL; - qd->totaltime = NULL; + qd->query_instr = NULL; /* not yet executed */ qd->already_executed = false; @@ -165,27 +166,22 @@ ProcessQuery(PlannedStmt *plan, */ if (qc) { - switch (queryDesc->operation) - { - case CMD_SELECT: - SetQueryCompletion(qc, CMDTAG_SELECT, queryDesc->estate->es_processed); - break; - case CMD_INSERT: - SetQueryCompletion(qc, CMDTAG_INSERT, queryDesc->estate->es_processed); - break; - case CMD_UPDATE: - SetQueryCompletion(qc, CMDTAG_UPDATE, queryDesc->estate->es_processed); - break; - case CMD_DELETE: - SetQueryCompletion(qc, CMDTAG_DELETE, queryDesc->estate->es_processed); - break; - case CMD_MERGE: - SetQueryCompletion(qc, CMDTAG_MERGE, queryDesc->estate->es_processed); - break; - default: - SetQueryCompletion(qc, CMDTAG_UNKNOWN, queryDesc->estate->es_processed); - break; - } + CommandTag tag; + + if (queryDesc->operation == CMD_SELECT) + tag = CMDTAG_SELECT; + else if (queryDesc->operation == CMD_INSERT) + tag = CMDTAG_INSERT; + else if (queryDesc->operation == CMD_UPDATE) + tag = CMDTAG_UPDATE; + else if (queryDesc->operation == CMD_DELETE) + tag = CMDTAG_DELETE; + else if (queryDesc->operation == CMD_MERGE) + tag = CMDTAG_MERGE; + else + tag = CMDTAG_UNKNOWN; + + SetQueryCompletion(qc, tag, queryDesc->estate->es_processed); } /* @@ -1163,10 +1159,11 @@ PortalRunUtility(Portal portal, PlannedStmt *pstmt, MemoryContextSwitchTo(portal->portalContext); /* - * Some utility commands (e.g., VACUUM) pop the ActiveSnapshot stack from - * under us, so don't complain if it's now empty. Otherwise, our snapshot - * should be the top one; pop it. Note that this could be a different - * snapshot from the one we made above; see EnsurePortalSnapshotExists. + * Some utility commands (e.g., VACUUM, WAIT FOR) pop the ActiveSnapshot + * stack from under us, so don't complain if it's now empty. Otherwise, + * our snapshot should be the top one; pop it. Note that this could be a + * different snapshot from the one we made above; see + * EnsurePortalSnapshotExists. */ if (portal->portalSnapshot != NULL && ActiveSnapshotSet()) { @@ -1350,24 +1347,15 @@ PortalRunMulti(Portal portal, PopActiveSnapshot(); /* - * If a query completion data was supplied, use it. Otherwise use the - * portal's query completion data. - * - * Exception: Clients expect INSERT/UPDATE/DELETE tags to have counts, so - * fake them with zeros. This can happen with DO INSTEAD rules if there - * is no replacement query of the same type as the original. We print "0 - * 0" here because technically there is no query of the matching tag type, - * and printing a non-zero count for a different query type seems wrong, - * e.g. an INSERT that does an UPDATE instead should not print "0 1" if - * one row was updated. See QueryRewrite(), step 3, for details. + * If a command tag was requested and we did not fill in a run-time- + * determined tag above, copy the parse-time tag from the Portal. (There + * might not be any tag there either, in edge cases such as empty prepared + * statements. That's OK.) */ - if (qc && qc->commandTag == CMDTAG_UNKNOWN) - { - if (portal->qc.commandTag != CMDTAG_UNKNOWN) - CopyQueryCompletion(qc, &portal->qc); - /* If the caller supplied a qc, we should have set it by now. */ - Assert(qc->commandTag != CMDTAG_UNKNOWN); - } + if (qc && + qc->commandTag == CMDTAG_UNKNOWN && + portal->qc.commandTag != CMDTAG_UNKNOWN) + CopyQueryCompletion(qc, &portal->qc); } /* @@ -1752,7 +1740,8 @@ PlannedStmtRequiresSnapshot(PlannedStmt *pstmt) IsA(utilityStmt, ListenStmt) || IsA(utilityStmt, NotifyStmt) || IsA(utilityStmt, UnlistenStmt) || - IsA(utilityStmt, CheckPointStmt)) + IsA(utilityStmt, CheckPointStmt) || + IsA(utilityStmt, WaitStmt)) return false; return true; diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 25fe3d5801665..73a56f1df1dc3 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -5,7 +5,7 @@ * commands. At one time acted as an interface between the Lisp and C * systems. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -26,7 +26,6 @@ #include "catalog/toasting.h" #include "commands/alter.h" #include "commands/async.h" -#include "commands/cluster.h" #include "commands/collationcmds.h" #include "commands/comment.h" #include "commands/conversioncmds.h" @@ -44,7 +43,9 @@ #include "commands/portalcmds.h" #include "commands/prepare.h" #include "commands/proclang.h" +#include "commands/propgraphcmds.h" #include "commands/publicationcmds.h" +#include "commands/repack.h" #include "commands/schemacmds.h" #include "commands/seclabel.h" #include "commands/sequence.h" @@ -56,6 +57,7 @@ #include "commands/user.h" #include "commands/vacuum.h" #include "commands/view.h" +#include "commands/wait.h" #include "miscadmin.h" #include "parser/parse_utilcmd.h" #include "postmaster/bgwriter.h" @@ -148,6 +150,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_AlterOperatorStmt: case T_AlterOwnerStmt: case T_AlterPolicyStmt: + case T_AlterPropGraphStmt: case T_AlterPublicationStmt: case T_AlterRoleSetStmt: case T_AlterRoleStmt: @@ -178,6 +181,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_CreateOpFamilyStmt: case T_CreatePLangStmt: case T_CreatePolicyStmt: + case T_CreatePropGraphStmt: case T_CreatePublicationStmt: case T_CreateRangeStmt: case T_CreateRoleStmt: @@ -266,6 +270,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) case T_PrepareStmt: case T_UnlistenStmt: case T_VariableSetStmt: + case T_WaitStmt: { /* * These modify only backend-local state, so they're OK to run @@ -277,9 +282,9 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) return COMMAND_OK_IN_RECOVERY | COMMAND_OK_IN_READ_ONLY_TXN; } - case T_ClusterStmt: case T_ReindexStmt: case T_VacuumStmt: + case T_RepackStmt: { /* * These commands write WAL, so they're not strictly @@ -288,9 +293,9 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree) * * However, they don't change the database state in a way that * would affect pg_dump output, so it's fine to run them in a - * read-only transaction. (CLUSTER might change the order of - * rows on disk, which could affect the ordering of pg_dump - * output, but that's not semantically significant.) + * read-only transaction. (REPACK/CLUSTER might change the + * order of rows on disk, which could affect the ordering of + * pg_dump output, but that's not semantically significant.) */ return COMMAND_OK_IN_READ_ONLY_TXN; } @@ -854,14 +859,14 @@ standard_ProcessUtility(PlannedStmt *pstmt, ExecuteCallStmt(castNode(CallStmt, parsetree), params, isAtomicContext, dest); break; - case T_ClusterStmt: - cluster(pstate, (ClusterStmt *) parsetree, isTopLevel); - break; - case T_VacuumStmt: ExecVacuum(pstate, (VacuumStmt *) parsetree, isTopLevel); break; + case T_RepackStmt: + ExecRepack(pstate, (RepackStmt *) parsetree, isTopLevel); + break; + case T_ExplainStmt: ExplainQuery(pstate, (ExplainStmt *) parsetree, params, dest); break; @@ -943,17 +948,7 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; case T_CheckPointStmt: - if (!has_privs_of_role(GetUserId(), ROLE_PG_CHECKPOINT)) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - /* translator: %s is name of a SQL command, eg CHECKPOINT */ - errmsg("permission denied to execute %s command", - "CHECKPOINT"), - errdetail("Only roles with privileges of the \"%s\" role may execute this command.", - "pg_checkpoint"))); - - RequestCheckpoint(CHECKPOINT_IMMEDIATE | CHECKPOINT_WAIT | - (RecoveryInProgress() ? 0 : CHECKPOINT_FORCE)); + ExecCheckpoint(pstate, (CheckPointStmt *) parsetree); break; /* @@ -1065,6 +1060,13 @@ standard_ProcessUtility(PlannedStmt *pstmt, break; } + case T_WaitStmt: + { + ExecWaitStmt(pstate, (WaitStmt *) parsetree, isTopLevel, + dest); + } + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(pstate, pstmt, queryString, @@ -1121,8 +1123,8 @@ ProcessUtilitySlow(ParseState *pstate, * relation and attribute manipulation */ case T_CreateSchemaStmt: - CreateSchemaCommand((CreateSchemaStmt *) parsetree, - queryString, + CreateSchemaCommand(pstate, + (CreateSchemaStmt *) parsetree, pstmt->stmt_location, pstmt->stmt_len); @@ -1244,6 +1246,7 @@ ProcessUtilitySlow(ParseState *pstate, wrapper->utilityStmt = stmt; wrapper->stmt_location = pstmt->stmt_location; wrapper->stmt_len = pstmt->stmt_len; + wrapper->planOrigin = PLAN_STMT_INTERNAL; ProcessUtility(wrapper, queryString, @@ -1343,7 +1346,7 @@ ProcessUtilitySlow(ParseState *pstate, */ switch (stmt->subtype) { - case 'T': /* ALTER DOMAIN DEFAULT */ + case AD_AlterDefault: /* * Recursively alter column default for table and, @@ -1353,30 +1356,30 @@ ProcessUtilitySlow(ParseState *pstate, AlterDomainDefault(stmt->typeName, stmt->def); break; - case 'N': /* ALTER DOMAIN DROP NOT NULL */ + case AD_DropNotNull: address = AlterDomainNotNull(stmt->typeName, false); break; - case 'O': /* ALTER DOMAIN SET NOT NULL */ + case AD_SetNotNull: address = AlterDomainNotNull(stmt->typeName, true); break; - case 'C': /* ADD CONSTRAINT */ + case AD_AddConstraint: address = AlterDomainAddConstraint(stmt->typeName, stmt->def, &secondaryObject); break; - case 'X': /* DROP CONSTRAINT */ + case AD_DropConstraint: address = AlterDomainDropConstraint(stmt->typeName, stmt->name, stmt->behavior, stmt->missing_ok); break; - case 'V': /* VALIDATE CONSTRAINT */ + case AD_ValidateConstraint: address = AlterDomainValidateConstraint(stmt->typeName, stmt->name); @@ -1542,7 +1545,8 @@ ProcessUtilitySlow(ParseState *pstate, /* ... and do it */ EventTriggerAlterTableStart(parsetree); address = - DefineIndex(relid, /* OID of heap relation */ + DefineIndex(pstate, + relid, /* OID of heap relation */ stmt, InvalidOid, /* no predefined OID */ InvalidOid, /* no parent index */ @@ -1739,6 +1743,14 @@ ProcessUtilitySlow(ParseState *pstate, commandCollected = true; break; + case T_CreatePropGraphStmt: + address = CreatePropGraph(pstate, (CreatePropGraphStmt *) parsetree); + break; + + case T_AlterPropGraphStmt: + address = AlterPropGraph(pstate, (AlterPropGraphStmt *) parsetree); + break; + case T_CreateTransformStmt: address = CreateTransform((CreateTransformStmt *) parsetree); break; @@ -1883,7 +1895,7 @@ ProcessUtilitySlow(ParseState *pstate, if (!IsA(rel, RangeVar)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("only a single relation is allowed in CREATE STATISTICS"))); + errmsg("CREATE STATISTICS only supports relation names in the FROM clause"))); /* * CREATE STATISTICS will influence future execution plans @@ -1901,7 +1913,7 @@ ProcessUtilitySlow(ParseState *pstate, /* Run parse analysis ... */ stmt = transformStatsStmt(relid, stmt, queryString); - address = CreateStatistics(stmt); + address = CreateStatistics(stmt, true); } break; @@ -1974,6 +1986,7 @@ ProcessUtilityForAlterTable(Node *stmt, AlterTableUtilityContext *context) wrapper->utilityStmt = stmt; wrapper->stmt_location = context->pstmt->stmt_location; wrapper->stmt_len = context->pstmt->stmt_len; + wrapper->planOrigin = PLAN_STMT_INTERNAL; ProcessUtility(wrapper, context->queryString, @@ -2000,13 +2013,14 @@ ExecDropStmt(DropStmt *stmt, bool isTopLevel) if (stmt->concurrent) PreventInTransactionBlock(isTopLevel, "DROP INDEX CONCURRENTLY"); - /* fall through */ + pg_fallthrough; case OBJECT_TABLE: case OBJECT_SEQUENCE: case OBJECT_VIEW: case OBJECT_MATVIEW: case OBJECT_FOREIGN_TABLE: + case OBJECT_PROPGRAPH: RemoveRelations(stmt); break; default: @@ -2067,6 +2081,9 @@ UtilityReturnsTuples(Node *parsetree) case T_VariableShowStmt: return true; + case T_WaitStmt: + return true; + default: return false; } @@ -2122,6 +2139,9 @@ UtilityTupleDescriptor(Node *parsetree) return GetPGVariableResultDesc(n->name); } + case T_WaitStmt: + return WaitStmtResultDesc((WaitStmt *) parsetree); + default: return NULL; } @@ -2283,6 +2303,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_PROCEDURE: tag = CMDTAG_ALTER_PROCEDURE; break; + case OBJECT_PROPGRAPH: + tag = CMDTAG_ALTER_PROPERTY_GRAPH; + break; case OBJECT_ROLE: tag = CMDTAG_ALTER_ROLE; break; @@ -2559,6 +2582,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_INDEX: tag = CMDTAG_DROP_INDEX; break; + case OBJECT_PROPGRAPH: + tag = CMDTAG_DROP_PROPERTY_GRAPH; + break; case OBJECT_TYPE: tag = CMDTAG_DROP_TYPE; break; @@ -2858,10 +2884,6 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_CALL; break; - case T_ClusterStmt: - tag = CMDTAG_CLUSTER; - break; - case T_VacuumStmt: if (((VacuumStmt *) parsetree)->is_vacuumcmd) tag = CMDTAG_VACUUM; @@ -2869,6 +2891,13 @@ CreateCommandTag(Node *parsetree) tag = CMDTAG_ANALYZE; break; + case T_RepackStmt: + if (((RepackStmt *) parsetree)->command == REPACK_COMMAND_CLUSTER) + tag = CMDTAG_CLUSTER; + else + tag = CMDTAG_REPACK; + break; + case T_ExplainStmt: tag = CMDTAG_EXPLAIN; break; @@ -2940,6 +2969,14 @@ CreateCommandTag(Node *parsetree) } break; + case T_CreatePropGraphStmt: + tag = CMDTAG_CREATE_PROPERTY_GRAPH; + break; + + case T_AlterPropGraphStmt: + tag = CMDTAG_ALTER_PROPERTY_GRAPH; + break; + case T_CreateTransformStmt: tag = CMDTAG_CREATE_TRANSFORM; break; @@ -3099,6 +3136,10 @@ CreateCommandTag(Node *parsetree) } break; + case T_WaitStmt: + tag = CMDTAG_WAIT; + break; + /* already-planned queries */ case T_PlannedStmt: { @@ -3506,7 +3547,7 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_ALL; break; - case T_ClusterStmt: + case T_RepackStmt: lev = LOGSTMT_DDL; break; @@ -3637,6 +3678,14 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_DDL; break; + case T_CreatePropGraphStmt: + lev = LOGSTMT_DDL; + break; + + case T_AlterPropGraphStmt: + lev = LOGSTMT_DDL; + break; + case T_CreateTransformStmt: lev = LOGSTMT_DDL; break; @@ -3697,6 +3746,10 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_DDL; break; + case T_WaitStmt: + lev = LOGSTMT_ALL; + break; + /* already-planned queries */ case T_PlannedStmt: { diff --git a/src/backend/tsearch/Makefile b/src/backend/tsearch/Makefile index 921411ecf9199..4a4361501090b 100644 --- a/src/backend/tsearch/Makefile +++ b/src/backend/tsearch/Makefile @@ -2,7 +2,7 @@ # # Makefile for backend/tsearch # -# Copyright (c) 2006-2025, PostgreSQL Global Development Group +# Copyright (c) 2006-2026, PostgreSQL Global Development Group # # src/backend/tsearch/Makefile # diff --git a/src/backend/tsearch/dict.c b/src/backend/tsearch/dict.c index eb968858683de..c026649da4564 100644 --- a/src/backend/tsearch/dict.c +++ b/src/backend/tsearch/dict.c @@ -3,7 +3,7 @@ * dict.c * Standard interface to dictionary * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -61,7 +61,7 @@ ts_lexize(PG_FUNCTION_ARGS) ptr = res; while (ptr->lexeme) ptr++; - da = (Datum *) palloc(sizeof(Datum) * (ptr - res)); + da = (Datum *) palloc_array(Datum, ptr - res); ptr = res; while (ptr->lexeme) { diff --git a/src/backend/tsearch/dict_ispell.c b/src/backend/tsearch/dict_ispell.c index 63bd193a78a89..ad5c26ebccbc6 100644 --- a/src/backend/tsearch/dict_ispell.c +++ b/src/backend/tsearch/dict_ispell.c @@ -3,7 +3,7 @@ * dict_ispell.c * Ispell dictionary interface * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -37,7 +37,7 @@ dispell_init(PG_FUNCTION_ARGS) stoploaded = false; ListCell *l; - d = (DictISpell *) palloc0(sizeof(DictISpell)); + d = palloc0_object(DictISpell); NIStartBuild(&(d->obj)); @@ -47,24 +47,30 @@ dispell_init(PG_FUNCTION_ARGS) if (strcmp(defel->defname, "dictfile") == 0) { + char *filename; + if (dictloaded) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("multiple DictFile parameters"))); - NIImportDictionary(&(d->obj), - get_tsearch_config_filename(defGetString(defel), - "dict")); + filename = get_tsearch_config_filename(defGetString(defel), + "dict"); + NIImportDictionary(&(d->obj), filename); + pfree(filename); dictloaded = true; } else if (strcmp(defel->defname, "afffile") == 0) { + char *filename; + if (affloaded) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("multiple AffFile parameters"))); - NIImportAffixes(&(d->obj), - get_tsearch_config_filename(defGetString(defel), - "affix")); + filename = get_tsearch_config_filename(defGetString(defel), + "affix"); + NIImportAffixes(&(d->obj), filename); + pfree(filename); affloaded = true; } else if (strcmp(defel->defname, "stopwords") == 0) diff --git a/src/backend/tsearch/dict_simple.c b/src/backend/tsearch/dict_simple.c index 2c972fc053870..44d945b2be82e 100644 --- a/src/backend/tsearch/dict_simple.c +++ b/src/backend/tsearch/dict_simple.c @@ -3,7 +3,7 @@ * dict_simple.c * Simple dictionary: just lowercase and check for stopword * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -31,7 +31,7 @@ Datum dsimple_init(PG_FUNCTION_ARGS) { List *dictoptions = (List *) PG_GETARG_POINTER(0); - DictSimple *d = (DictSimple *) palloc0(sizeof(DictSimple)); + DictSimple *d = palloc0_object(DictSimple); bool stoploaded = false, acceptloaded = false; ListCell *l; @@ -87,13 +87,13 @@ dsimple_lexize(PG_FUNCTION_ARGS) { /* reject as stopword */ pfree(txt); - res = palloc0(sizeof(TSLexeme) * 2); + res = palloc0_array(TSLexeme, 2); PG_RETURN_POINTER(res); } else if (d->accept) { /* accept */ - res = palloc0(sizeof(TSLexeme) * 2); + res = palloc0_array(TSLexeme, 2); res[0].lexeme = txt; PG_RETURN_POINTER(res); } diff --git a/src/backend/tsearch/dict_synonym.c b/src/backend/tsearch/dict_synonym.c index 0da5a9d686802..3937f25bcc6ca 100644 --- a/src/backend/tsearch/dict_synonym.c +++ b/src/backend/tsearch/dict_synonym.c @@ -3,7 +3,7 @@ * dict_synonym.c * Synonym dictionary: replace word by its synonym * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -50,7 +50,7 @@ findwrd(char *in, char **end, uint16 *flags) /* Skip leading spaces */ while (*in && isspace((unsigned char) *in)) - in += pg_mblen(in); + in += pg_mblen_cstr(in); /* Return NULL on empty lines */ if (*in == '\0') @@ -65,7 +65,7 @@ findwrd(char *in, char **end, uint16 *flags) while (*in && !isspace((unsigned char) *in)) { lastchar = in; - in += pg_mblen(in); + in += pg_mblen_cstr(in); } if (in - lastchar == 1 && t_iseq(lastchar, '*') && flags) @@ -134,7 +134,7 @@ dsynonym_init(PG_FUNCTION_ARGS) errmsg("could not open synonym file \"%s\": %m", filename))); - d = (DictSyn *) palloc0(sizeof(DictSyn)); + d = palloc0_object(DictSyn); while ((line = tsearch_readline(&trst)) != NULL) { @@ -169,12 +169,12 @@ dsynonym_init(PG_FUNCTION_ARGS) if (d->len == 0) { d->len = 64; - d->syn = (Syn *) palloc(sizeof(Syn) * d->len); + d->syn = palloc_array(Syn, d->len); } else { d->len *= 2; - d->syn = (Syn *) repalloc(d->syn, sizeof(Syn) * d->len); + d->syn = repalloc_array(d->syn, Syn, d->len); } } @@ -199,6 +199,7 @@ dsynonym_init(PG_FUNCTION_ARGS) } tsearch_readline_end(&trst); + pfree(filename); d->len = cur; qsort(d->syn, d->len, sizeof(Syn), compareSyn); @@ -235,7 +236,7 @@ dsynonym_lexize(PG_FUNCTION_ARGS) if (!found) PG_RETURN_POINTER(NULL); - res = palloc0(sizeof(TSLexeme) * 2); + res = palloc0_array(TSLexeme, 2); res[0].lexeme = pnstrdup(found->out, found->outlen); res[0].flags = found->flags; diff --git a/src/backend/tsearch/dict_thesaurus.c b/src/backend/tsearch/dict_thesaurus.c index 1bebe36a6910e..0fd4cf3dfa85f 100644 --- a/src/backend/tsearch/dict_thesaurus.c +++ b/src/backend/tsearch/dict_thesaurus.c @@ -3,7 +3,7 @@ * dict_thesaurus.c * Thesaurus dictionary: phrase to phrase substitution * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -78,12 +78,12 @@ newLexeme(DictThesaurus *d, char *b, char *e, uint32 idsubst, uint16 posinsubst) if (d->ntwrds == 0) { d->ntwrds = 16; - d->wrds = (TheLexeme *) palloc(sizeof(TheLexeme) * d->ntwrds); + d->wrds = palloc_array(TheLexeme, d->ntwrds); } else { d->ntwrds *= 2; - d->wrds = (TheLexeme *) repalloc(d->wrds, sizeof(TheLexeme) * d->ntwrds); + d->wrds = repalloc_array(d->wrds, TheLexeme, d->ntwrds); } } @@ -95,7 +95,7 @@ newLexeme(DictThesaurus *d, char *b, char *e, uint32 idsubst, uint16 posinsubst) memcpy(ptr->lexeme, b, e - b); ptr->lexeme[e - b] = '\0'; - ptr->entries = (LexemeInfo *) palloc(sizeof(LexemeInfo)); + ptr->entries = palloc_object(LexemeInfo); ptr->entries->nextentry = NULL; ptr->entries->idsubst = idsubst; @@ -118,12 +118,12 @@ addWrd(DictThesaurus *d, char *b, char *e, uint32 idsubst, uint16 nwrd, uint16 p if (d->nsubst == 0) { d->nsubst = 16; - d->subst = (TheSubstitute *) palloc(sizeof(TheSubstitute) * d->nsubst); + d->subst = palloc_array(TheSubstitute, d->nsubst); } else { d->nsubst *= 2; - d->subst = (TheSubstitute *) repalloc(d->subst, sizeof(TheSubstitute) * d->nsubst); + d->subst = repalloc_array(d->subst, TheSubstitute, d->nsubst); } } } @@ -137,12 +137,12 @@ addWrd(DictThesaurus *d, char *b, char *e, uint32 idsubst, uint16 nwrd, uint16 p if (ntres == 0) { ntres = 2; - ptr->res = (TSLexeme *) palloc(sizeof(TSLexeme) * ntres); + ptr->res = palloc_array(TSLexeme, ntres); } else { ntres *= 2; - ptr->res = (TSLexeme *) repalloc(ptr->res, sizeof(TSLexeme) * ntres); + ptr->res = repalloc_array(ptr->res, TSLexeme, ntres); } } @@ -167,17 +167,17 @@ addWrd(DictThesaurus *d, char *b, char *e, uint32 idsubst, uint16 nwrd, uint16 p static void thesaurusRead(const char *filename, DictThesaurus *d) { + char *real_filename = get_tsearch_config_filename(filename, "ths"); tsearch_readline_state trst; uint32 idsubst = 0; bool useasis = false; char *line; - filename = get_tsearch_config_filename(filename, "ths"); - if (!tsearch_readline_begin(&trst, filename)) + if (!tsearch_readline_begin(&trst, real_filename)) ereport(ERROR, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("could not open thesaurus file \"%s\": %m", - filename))); + real_filename))); while ((line = tsearch_readline(&trst)) != NULL) { @@ -191,7 +191,7 @@ thesaurusRead(const char *filename, DictThesaurus *d) /* is it a comment? */ while (*ptr && isspace((unsigned char) *ptr)) - ptr += pg_mblen(ptr); + ptr += pg_mblen_cstr(ptr); if (t_iseq(ptr, '#') || *ptr == '\0' || t_iseq(ptr, '\n') || t_iseq(ptr, '\r')) @@ -237,13 +237,13 @@ thesaurusRead(const char *filename, DictThesaurus *d) { useasis = true; state = TR_INSUBS; - beginwrd = ptr + pg_mblen(ptr); + beginwrd = ptr + pg_mblen_cstr(ptr); } else if (t_iseq(ptr, '\\')) { useasis = false; state = TR_INSUBS; - beginwrd = ptr + pg_mblen(ptr); + beginwrd = ptr + pg_mblen_cstr(ptr); } else if (!isspace((unsigned char) *ptr)) { @@ -267,7 +267,7 @@ thesaurusRead(const char *filename, DictThesaurus *d) else elog(ERROR, "unrecognized thesaurus state: %d", state); - ptr += pg_mblen(ptr); + ptr += pg_mblen_cstr(ptr); } if (state == TR_INSUBS) @@ -297,6 +297,7 @@ thesaurusRead(const char *filename, DictThesaurus *d) d->nsubst = idsubst; tsearch_readline_end(&trst); + pfree(real_filename); } static TheLexeme * @@ -308,7 +309,7 @@ addCompiledLexeme(TheLexeme *newwrds, int *nnw, int *tnm, TSLexeme *lexeme, Lexe newwrds = (TheLexeme *) repalloc(newwrds, sizeof(TheLexeme) * *tnm); } - newwrds[*nnw].entries = (LexemeInfo *) palloc(sizeof(LexemeInfo)); + newwrds[*nnw].entries = palloc_object(LexemeInfo); if (lexeme && lexeme->lexeme) { @@ -393,7 +394,7 @@ compileTheLexeme(DictThesaurus *d) int i, nnw = 0, tnm = 16; - TheLexeme *newwrds = (TheLexeme *) palloc(sizeof(TheLexeme) * tnm), + TheLexeme *newwrds = palloc_array(TheLexeme, tnm), *ptrwrds; for (i = 0; i < d->nwrds; i++) @@ -510,7 +511,7 @@ compileTheSubstitute(DictThesaurus *d) *inptr; int n = 2; - outptr = d->subst[i].res = (TSLexeme *) palloc(sizeof(TSLexeme) * n); + outptr = d->subst[i].res = palloc_array(TSLexeme, n); outptr->lexeme = NULL; inptr = rem; @@ -602,7 +603,7 @@ thesaurus_init(PG_FUNCTION_ARGS) List *namelist; ListCell *l; - d = (DictThesaurus *) palloc0(sizeof(DictThesaurus)); + d = palloc0_object(DictThesaurus); foreach(l, dictoptions) { @@ -755,7 +756,7 @@ copyTSLexeme(TheSubstitute *ts) TSLexeme *res; uint16 i; - res = (TSLexeme *) palloc(sizeof(TSLexeme) * (ts->reslen + 1)); + res = palloc_array(TSLexeme, ts->reslen + 1); for (i = 0; i < ts->reslen; i++) { res[i] = ts->res[i]; @@ -833,7 +834,7 @@ thesaurus_lexize(PG_FUNCTION_ARGS) ptr++; } - infos = (LexemeInfo **) palloc(sizeof(LexemeInfo *) * nlex); + infos = palloc_array(LexemeInfo *, nlex); for (i = 0; i < nlex; i++) if ((infos[i] = findTheLexeme(d, basevar[i].lexeme)) == NULL) break; diff --git a/src/backend/tsearch/meson.build b/src/backend/tsearch/meson.build index c109b21c3a257..8a55748f2541e 100644 --- a/src/backend/tsearch/meson.build +++ b/src/backend/tsearch/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'dict.c', diff --git a/src/backend/tsearch/regis.c b/src/backend/tsearch/regis.c index e59a8f0970994..51ba78fabbcd3 100644 --- a/src/backend/tsearch/regis.c +++ b/src/backend/tsearch/regis.c @@ -3,7 +3,7 @@ * regis.c * Fast regex subset * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -37,7 +37,7 @@ RS_isRegis(const char *str) { if (state == RS_IN_WAIT) { - if (t_isalpha(c)) + if (t_isalpha_cstr(c)) /* okay */ ; else if (t_iseq(c, '[')) state = RS_IN_ONEOF; @@ -48,14 +48,14 @@ RS_isRegis(const char *str) { if (t_iseq(c, '^')) state = RS_IN_NONEOF; - else if (t_isalpha(c)) + else if (t_isalpha_cstr(c)) state = RS_IN_ONEOF_IN; else return false; } else if (state == RS_IN_ONEOF_IN || state == RS_IN_NONEOF) { - if (t_isalpha(c)) + if (t_isalpha_cstr(c)) /* okay */ ; else if (t_iseq(c, ']')) state = RS_IN_WAIT; @@ -64,7 +64,7 @@ RS_isRegis(const char *str) } else elog(ERROR, "internal error in RS_isRegis: state %d", state); - c += pg_mblen(c); + c += pg_mblen_cstr(c); } return (state == RS_IN_WAIT); @@ -96,15 +96,14 @@ RS_compile(Regis *r, bool issuffix, const char *str) { if (state == RS_IN_WAIT) { - if (t_isalpha(c)) + if (t_isalpha_cstr(c)) { if (ptr) ptr = newRegisNode(ptr, len); else ptr = r->node = newRegisNode(NULL, len); - COPYCHAR(ptr->data, c); ptr->type = RSF_ONEOF; - ptr->len = pg_mblen(c); + ptr->len = ts_copychar_cstr(ptr->data, c); } else if (t_iseq(c, '[')) { @@ -125,10 +124,9 @@ RS_compile(Regis *r, bool issuffix, const char *str) ptr->type = RSF_NONEOF; state = RS_IN_NONEOF; } - else if (t_isalpha(c)) + else if (t_isalpha_cstr(c)) { - COPYCHAR(ptr->data, c); - ptr->len = pg_mblen(c); + ptr->len = ts_copychar_cstr(ptr->data, c); state = RS_IN_ONEOF_IN; } else /* shouldn't get here */ @@ -136,11 +134,8 @@ RS_compile(Regis *r, bool issuffix, const char *str) } else if (state == RS_IN_ONEOF_IN || state == RS_IN_NONEOF) { - if (t_isalpha(c)) - { - COPYCHAR(ptr->data + ptr->len, c); - ptr->len += pg_mblen(c); - } + if (t_isalpha_cstr(c)) + ptr->len += ts_copychar_cstr(ptr->data + ptr->len, c); else if (t_iseq(c, ']')) state = RS_IN_WAIT; else /* shouldn't get here */ @@ -148,7 +143,7 @@ RS_compile(Regis *r, bool issuffix, const char *str) } else elog(ERROR, "internal error in RS_compile: state %d", state); - c += pg_mblen(c); + c += pg_mblen_cstr(c); } if (state != RS_IN_WAIT) /* shouldn't get here */ @@ -187,10 +182,10 @@ mb_strchr(char *str, char *c) char *ptr = str; bool res = false; - clen = pg_mblen(c); + clen = pg_mblen_cstr(c); while (*ptr && !res) { - plen = pg_mblen(ptr); + plen = pg_mblen_cstr(ptr); if (plen == clen) { i = plen; @@ -219,7 +214,7 @@ RS_execute(Regis *r, char *str) while (*c) { len++; - c += pg_mblen(c); + c += pg_mblen_cstr(c); } if (len < r->nchar) @@ -230,7 +225,7 @@ RS_execute(Regis *r, char *str) { len -= r->nchar; while (len-- > 0) - c += pg_mblen(c); + c += pg_mblen_cstr(c); } @@ -250,7 +245,7 @@ RS_execute(Regis *r, char *str) elog(ERROR, "unrecognized regis node type: %d", ptr->type); } ptr = ptr->next; - c += pg_mblen(c); + c += pg_mblen_cstr(c); } return true; diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c index 146801885d738..15dccb47bf5f2 100644 --- a/src/backend/tsearch/spell.c +++ b/src/backend/tsearch/spell.c @@ -3,7 +3,7 @@ * spell.c * Normalizing word with ISpell * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * Ispell dictionary * ----------------- @@ -210,8 +210,8 @@ cmpspellaffix(const void *s1, const void *s2) static int cmpcmdflag(const void *f1, const void *f2) { - CompoundAffixFlag *fv1 = (CompoundAffixFlag *) f1, - *fv2 = (CompoundAffixFlag *) f2; + const CompoundAffixFlag *fv1 = f1; + const CompoundAffixFlag *fv2 = f2; Assert(fv1->flagMode == fv2->flagMode); @@ -233,7 +233,7 @@ findchar(char *str, int c) { if (t_iseq(str, c)) return str; - str += pg_mblen(str); + str += pg_mblen_cstr(str); } return NULL; @@ -246,7 +246,7 @@ findchar2(char *str, int c1, int c2) { if (t_iseq(str, c1) || t_iseq(str, c2)) return str; - str += pg_mblen(str); + str += pg_mblen_cstr(str); } return NULL; @@ -353,6 +353,7 @@ getNextFlagFromString(IspellDict *Conf, const char **sflagset, char *sflag) char *next; const char *sbuf = *sflagset; int maxstep; + int clen; bool stop = false; bool met_comma = false; @@ -364,11 +365,11 @@ getNextFlagFromString(IspellDict *Conf, const char **sflagset, char *sflag) { case FM_LONG: case FM_CHAR: - COPYCHAR(sflag, *sflagset); - sflag += pg_mblen(*sflagset); + clen = ts_copychar_cstr(sflag, *sflagset); + sflag += clen; /* Go to start of the next flag */ - *sflagset += pg_mblen(*sflagset); + *sflagset += clen; /* Check if we get all characters of flag */ maxstep--; @@ -418,7 +419,7 @@ getNextFlagFromString(IspellDict *Conf, const char **sflagset, char *sflag) *sflagset))); } - *sflagset += pg_mblen(*sflagset); + *sflagset += pg_mblen_cstr(*sflagset); } stop = true; break; @@ -544,7 +545,7 @@ NIImportDictionary(IspellDict *Conf, const char *filename) while (*s) { /* we allow only single encoded flags for faster works */ - if (pg_mblen(s) == 1 && isprint((unsigned char) *s) && !isspace((unsigned char) *s)) + if (pg_mblen_cstr(s) == 1 && isprint((unsigned char) *s) && !isspace((unsigned char) *s)) s++; else { @@ -565,7 +566,7 @@ NIImportDictionary(IspellDict *Conf, const char *filename) *s = '\0'; break; } - s += pg_mblen(s); + s += pg_mblen_cstr(s); } pstr = lowerstr_ctx(Conf, line); @@ -691,7 +692,7 @@ NIAddAffix(IspellDict *Conf, const char *flag, char flagflags, const char *mask, else { Conf->maffixes = 16; - Conf->Affix = (AFFIX *) palloc(Conf->maffixes * sizeof(AFFIX)); + Conf->Affix = palloc_array(AFFIX, Conf->maffixes); } } @@ -737,7 +738,7 @@ NIAddAffix(IspellDict *Conf, const char *flag, char flagflags, const char *mask, * allocated in the dictionary's memory context, and will be freed * automatically when it is destroyed. */ - Affix->reg.pregex = palloc(sizeof(regex_t)); + Affix->reg.pregex = palloc_object(regex_t); err = pg_regcomp(Affix->reg.pregex, wmask, wmasklen, REG_ADVANCED | REG_NOSUB, DEFAULT_COLLATION_OID); @@ -797,17 +798,17 @@ get_nextfield(char **str, char *next) while (**str) { + int clen = pg_mblen_cstr(*str); + if (state == PAE_WAIT_MASK) { if (t_iseq(*str, '#')) return false; else if (!isspace((unsigned char) **str)) { - int clen = pg_mblen(*str); - if (clen < avail) { - COPYCHAR(next, *str); + ts_copychar_with_len(next, *str, clen); next += clen; avail -= clen; } @@ -823,17 +824,15 @@ get_nextfield(char **str, char *next) } else { - int clen = pg_mblen(*str); - if (clen < avail) { - COPYCHAR(next, *str); + ts_copychar_with_len(next, *str, clen); next += clen; avail -= clen; } } } - *str += pg_mblen(*str); + *str += clen; } *next = '\0'; @@ -910,27 +909,35 @@ parse_ooaffentry(char *str, char *type, char *flag, char *find, * * An .affix file entry has the following format: * > [-,] + * + * Output buffers mask, find, repl must be of length BUFSIZ; + * we truncate the input to fit. */ static bool -parse_affentry(char *str, char *mask, char *find, char *repl) +parse_affentry(const char *str, char *mask, char *find, char *repl) { int state = PAE_WAIT_MASK; char *pmask = mask, *pfind = find, *prepl = repl; + char *emask = mask + BUFSIZ; + char *efind = find + BUFSIZ; + char *erepl = repl + BUFSIZ; *mask = *find = *repl = '\0'; while (*str) { + int clen = pg_mblen_cstr(str); + if (state == PAE_WAIT_MASK) { if (t_iseq(str, '#')) return false; else if (!isspace((unsigned char) *str)) { - COPYCHAR(pmask, str); - pmask += pg_mblen(str); + if (pmask < emask - clen) + pmask += ts_copychar_with_len(pmask, str, clen); state = PAE_INMASK; } } @@ -943,8 +950,8 @@ parse_affentry(char *str, char *mask, char *find, char *repl) } else if (!isspace((unsigned char) *str)) { - COPYCHAR(pmask, str); - pmask += pg_mblen(str); + if (pmask < emask - clen) + pmask += ts_copychar_with_len(pmask, str, clen); } } else if (state == PAE_WAIT_FIND) @@ -953,10 +960,10 @@ parse_affentry(char *str, char *mask, char *find, char *repl) { state = PAE_INFIND; } - else if (t_isalpha(str) || t_iseq(str, '\'') /* english 's */ ) + else if (t_isalpha_cstr(str) || t_iseq(str, '\'') /* english 's */ ) { - COPYCHAR(prepl, str); - prepl += pg_mblen(str); + if (prepl < erepl - clen) + prepl += ts_copychar_with_len(prepl, str, clen); state = PAE_INREPL; } else if (!isspace((unsigned char) *str)) @@ -971,10 +978,10 @@ parse_affentry(char *str, char *mask, char *find, char *repl) *pfind = '\0'; state = PAE_WAIT_REPL; } - else if (t_isalpha(str)) + else if (t_isalpha_cstr(str)) { - COPYCHAR(pfind, str); - pfind += pg_mblen(str); + if (pfind < efind - clen) + pfind += ts_copychar_with_len(pfind, str, clen); } else if (!isspace((unsigned char) *str)) ereport(ERROR, @@ -987,10 +994,10 @@ parse_affentry(char *str, char *mask, char *find, char *repl) { break; /* void repl */ } - else if (t_isalpha(str)) + else if (t_isalpha_cstr(str)) { - COPYCHAR(prepl, str); - prepl += pg_mblen(str); + if (prepl < erepl - clen) + prepl += ts_copychar_with_len(prepl, str, clen); state = PAE_INREPL; } else if (!isspace((unsigned char) *str)) @@ -1005,10 +1012,10 @@ parse_affentry(char *str, char *mask, char *find, char *repl) *prepl = '\0'; break; } - else if (t_isalpha(str)) + else if (t_isalpha_cstr(str)) { - COPYCHAR(prepl, str); - prepl += pg_mblen(str); + if (prepl < erepl - clen) + prepl += ts_copychar_with_len(prepl, str, clen); } else if (!isspace((unsigned char) *str)) ereport(ERROR, @@ -1018,7 +1025,7 @@ parse_affentry(char *str, char *mask, char *find, char *repl) else elog(ERROR, "unrecognized state in parse_affentry: %d", state); - str += pg_mblen(str); + str += clen; } *pmask = *pfind = *prepl = '\0'; @@ -1066,15 +1073,14 @@ setCompoundAffixFlagValue(IspellDict *Conf, CompoundAffixFlag *entry, * val: affix parameter. */ static void -addCompoundAffixFlagValue(IspellDict *Conf, char *s, uint32 val) +addCompoundAffixFlagValue(IspellDict *Conf, const char *s, uint32 val) { CompoundAffixFlag *newValue; char sbuf[BUFSIZ]; char *sflag; - int clen; while (*s && isspace((unsigned char) *s)) - s += pg_mblen(s); + s += pg_mblen_cstr(s); if (!*s) ereport(ERROR, @@ -1085,9 +1091,11 @@ addCompoundAffixFlagValue(IspellDict *Conf, char *s, uint32 val) sflag = sbuf; while (*s && !isspace((unsigned char) *s) && *s != '\n') { - clen = pg_mblen(s); - COPYCHAR(sflag, s); - sflag += clen; + int clen = pg_mblen_cstr(s); + + /* Truncate the input to fit in BUFSIZ */ + if (sflag < sbuf + BUFSIZ - clen) + sflag += ts_copychar_with_len(sflag, s, clen); s += clen; } *sflag = '\0'; @@ -1267,7 +1275,7 @@ NIImportOOAffixes(IspellDict *Conf, const char *filename) char *s = recoded + strlen("FLAG"); while (*s && isspace((unsigned char) *s)) - s += pg_mblen(s); + s += pg_mblen_cstr(s); if (*s) { @@ -1327,7 +1335,7 @@ NIImportOOAffixes(IspellDict *Conf, const char *filename) /* Also reserve place for empty flag set */ naffix++; - Conf->AffixData = (const char **) palloc0(naffix * sizeof(char *)); + Conf->AffixData = palloc0_array(const char *, naffix); Conf->lenAffixData = Conf->nAffixData = naffix; /* Add empty flag set into AffixData */ @@ -1466,11 +1474,11 @@ NIImportAffixes(IspellDict *Conf, const char *filename) if (s) { while (*s && !isspace((unsigned char) *s)) - s += pg_mblen(s); + s += pg_mblen_cstr(s); while (*s && isspace((unsigned char) *s)) - s += pg_mblen(s); + s += pg_mblen_cstr(s); - if (*s && pg_mblen(s) == 1) + if (*s && pg_mblen_cstr(s) == 1) { addCompoundAffixFlagValue(Conf, s, FF_COMPOUNDFLAG); Conf->usecompound = true; @@ -1499,7 +1507,7 @@ NIImportAffixes(IspellDict *Conf, const char *filename) flagflags = 0; while (*s && isspace((unsigned char) *s)) - s += pg_mblen(s); + s += pg_mblen_cstr(s); if (*s == '*') { @@ -1520,12 +1528,11 @@ NIImportAffixes(IspellDict *Conf, const char *filename) * be followed by EOL, whitespace, or ':'. Otherwise this is a * new-format flag command. */ - if (*s && pg_mblen(s) == 1) + if (*s && pg_mblen_cstr(s) == 1) { - COPYCHAR(flag, s); + flag[0] = *s++; flag[1] = '\0'; - s++; if (*s == '\0' || *s == '#' || *s == '\n' || *s == ':' || isspace((unsigned char) *s)) { @@ -1794,7 +1801,7 @@ NISortDictionary(IspellDict *Conf) * dictionary. Replace textual flag-field of Conf->Spell entries with * indexes into Conf->AffixData array. */ - Conf->AffixData = (const char **) palloc0(naffix * sizeof(const char *)); + Conf->AffixData = palloc0_array(const char *, naffix); curaffix = -1; for (i = 0; i < Conf->nspell; i++) @@ -1991,7 +1998,7 @@ NISortAffixes(IspellDict *Conf) /* Store compound affixes in the Conf->CompoundAffix array */ if (Conf->naffixes > 1) qsort(Conf->Affix, Conf->naffixes, sizeof(AFFIX), cmpaffix); - Conf->CompoundAffix = ptr = (CMPDAffix *) palloc(sizeof(CMPDAffix) * Conf->naffixes); + Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes); ptr->affix = NULL; for (i = 0; i < Conf->naffixes; i++) @@ -2072,9 +2079,32 @@ FindAffixes(AffixNode *node, const char *word, int wrdlen, int *level, int type) return NULL; } +/* + * Checks to see if affix applies to word, transforms word if so. + * The transformation consists of replacing Affix->replen leading or + * trailing bytes with the Affix->find string. + * + * word: input word + * len: length of input word + * Affix: affix to consider + * flagflags: context flags showing whether we are handling a compound word + * newword: output buffer (MUST be of length 2 * MAXNORMLEN) + * baselen: input/output argument + * + * If baselen isn't NULL, then *baselen is used to return the length of + * the non-changed part of the word when applying a suffix, and is used + * to detect whether the input contained only a prefix and suffix when + * later applying a prefix. + * + * Returns newword on success, or NULL if the affix can't be applied. + * On success, the modified word is stored into newword. + */ static char * CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *newword, int *baselen) { + size_t keeplen, + findlen; + /* * Check compound allow flags */ @@ -2107,15 +2137,27 @@ CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *neww return NULL; } + /* + * Protect against output buffer overrun (len < Affix->replen would be + * caller error, but check anyway) + */ + Assert(len == strlen(word)); + if (len < Affix->replen) + return NULL; + keeplen = len - Affix->replen; /* how much of word we will keep */ + findlen = strlen(Affix->find); + if (keeplen + findlen >= 2 * MAXNORMLEN) + return NULL; + /* * make replace pattern of affix */ if (Affix->type == FF_SUFFIX) { - strcpy(newword, word); - strcpy(newword + len - Affix->replen, Affix->find); + memcpy(newword, word, keeplen); + strcpy(newword + keeplen, Affix->find); if (baselen) /* store length of non-changed part of word */ - *baselen = len - Affix->replen; + *baselen = keeplen; } else { @@ -2123,10 +2165,10 @@ CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *neww * if prefix is an all non-changed part's length then all word * contains only prefix and suffix, so out */ - if (baselen && *baselen + strlen(Affix->find) <= Affix->replen) + if (baselen && *baselen + findlen <= Affix->replen) return NULL; - strcpy(newword, Affix->find); - strcat(newword, word + Affix->replen); + memcpy(newword, Affix->find, findlen); + strcpy(newword + findlen, word + Affix->replen); } /* @@ -2147,7 +2189,7 @@ CheckAffix(const char *word, size_t len, AFFIX *Affix, int flagflags, char *neww /* Convert data string to wide characters */ newword_len = strlen(newword); - data = (pg_wchar *) palloc((newword_len + 1) * sizeof(pg_wchar)); + data = palloc_array(pg_wchar, newword_len + 1); data_len = pg_mb2wchar_with_len(newword, data, newword_len); if (pg_regexec(Affix->reg.pregex, data, data_len, @@ -2197,7 +2239,7 @@ NormalizeSubWord(IspellDict *Conf, const char *word, int flag) if (wrdlen > MAXNORMLEN) return NULL; - cur = forms = (char **) palloc(MAX_NORM * sizeof(char *)); + cur = forms = palloc_array(char *, MAX_NORM); *cur = NULL; @@ -2320,7 +2362,7 @@ CheckCompoundAffixes(CMPDAffix **ptr, const char *word, int len, bool CheckInPla } else { - char *affbegin; + const char *affbegin; while ((*ptr)->affix) { @@ -2340,7 +2382,7 @@ CheckCompoundAffixes(CMPDAffix **ptr, const char *word, int len, bool CheckInPla static SplitVar * CopyVar(SplitVar *s, int makedup) { - SplitVar *v = (SplitVar *) palloc(sizeof(SplitVar)); + SplitVar *v = palloc_object(SplitVar); v->next = NULL; if (s) @@ -2348,7 +2390,7 @@ CopyVar(SplitVar *s, int makedup) int i; v->lenstem = s->lenstem; - v->stem = (char **) palloc(sizeof(char *) * v->lenstem); + v->stem = palloc_array(char *, v->lenstem); v->nstem = s->nstem; for (i = 0; i < s->nstem; i++) v->stem[i] = (makedup) ? pstrdup(s->stem[i]) : s->stem[i]; @@ -2356,7 +2398,7 @@ CopyVar(SplitVar *s, int makedup) else { v->lenstem = 16; - v->stem = (char **) palloc(sizeof(char *) * v->lenstem); + v->stem = palloc_array(char *, v->lenstem); v->nstem = 0; } return v; @@ -2463,9 +2505,9 @@ SplitToVariants(IspellDict *Conf, SPNode *snode, SplitVar *orig, const char *wor while (StopLow < StopHigh) { StopMiddle = StopLow + ((StopHigh - StopLow) >> 1); - if (StopMiddle->val == ((uint8 *) (word))[level]) + if (StopMiddle->val == ((const uint8 *) (word))[level]) break; - else if (StopMiddle->val < ((uint8 *) (word))[level]) + else if (StopMiddle->val < ((const uint8 *) (word))[level]) StopLow = StopMiddle + 1; else StopHigh = StopMiddle; @@ -2529,7 +2571,7 @@ static void addNorm(TSLexeme **lres, TSLexeme **lcur, char *word, int flags, uint16 NVariant) { if (*lres == NULL) - *lcur = *lres = (TSLexeme *) palloc(MAX_NORM * sizeof(TSLexeme)); + *lcur = *lres = palloc_array(TSLexeme, MAX_NORM); if (*lcur - *lres < MAX_NORM - 1) { diff --git a/src/backend/tsearch/to_tsany.c b/src/backend/tsearch/to_tsany.c index 4dfcc2cd3bd7e..d98def5b63fa0 100644 --- a/src/backend/tsearch/to_tsany.c +++ b/src/backend/tsearch/to_tsany.c @@ -3,7 +3,7 @@ * to_tsany.c * to_ts* function definitions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -84,7 +84,7 @@ uniqueWORD(ParsedWord *a, int32 l) { tmppos = LIMITPOS(a->pos.pos); a->alen = 2; - a->pos.apos = (uint16 *) palloc(sizeof(uint16) * a->alen); + a->pos.apos = palloc_array(uint16, a->alen); a->pos.apos[0] = 1; a->pos.apos[1] = tmppos; return l; @@ -103,7 +103,7 @@ uniqueWORD(ParsedWord *a, int32 l) */ tmppos = LIMITPOS(a->pos.pos); a->alen = 2; - a->pos.apos = (uint16 *) palloc(sizeof(uint16) * a->alen); + a->pos.apos = palloc_array(uint16, a->alen); a->pos.apos[0] = 1; a->pos.apos[1] = tmppos; @@ -123,7 +123,7 @@ uniqueWORD(ParsedWord *a, int32 l) res->word = ptr->word; tmppos = LIMITPOS(ptr->pos.pos); res->alen = 2; - res->pos.apos = (uint16 *) palloc(sizeof(uint16) * res->alen); + res->pos.apos = palloc_array(uint16, res->alen); res->pos.apos[0] = 1; res->pos.apos[1] = tmppos; } @@ -141,7 +141,7 @@ uniqueWORD(ParsedWord *a, int32 l) if (res->pos.apos[0] + 1 >= res->alen) { res->alen *= 2; - res->pos.apos = (uint16 *) repalloc(res->pos.apos, sizeof(uint16) * res->alen); + res->pos.apos = repalloc_array(res->pos.apos, uint16, res->alen); } if (res->pos.apos[0] == 0 || res->pos.apos[res->pos.apos[0]] != LIMITPOS(ptr->pos.pos)) { @@ -255,7 +255,7 @@ to_tsvector_byid(PG_FUNCTION_ARGS) prs.lenwords = MaxAllocSize / sizeof(ParsedWord); prs.curwords = 0; prs.pos = 0; - prs.words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs.lenwords); + prs.words = palloc_array(ParsedWord, prs.lenwords); parsetext(cfgId, &prs, VARDATA_ANY(in), VARSIZE_ANY_EXHDR(in)); @@ -453,7 +453,7 @@ add_to_tsvector(void *_state, char *elem_value, int elem_len) * (parsetext() will realloc it bigger as needed.) */ prs->lenwords = 16; - prs->words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs->lenwords); + prs->words = palloc_array(ParsedWord, prs->lenwords); prs->curwords = 0; prs->pos = 0; } @@ -489,7 +489,7 @@ add_to_tsvector(void *_state, char *elem_value, int elem_len) * and different variants are ORed together. */ static void -pushval_morph(Datum opaque, TSQueryParserState state, char *strval, int lenval, int16 weight, bool prefix) +pushval_morph(void *opaque, TSQueryParserState state, char *strval, int lenval, int16 weight, bool prefix) { int32 count = 0; ParsedText prs; @@ -498,12 +498,12 @@ pushval_morph(Datum opaque, TSQueryParserState state, char *strval, int lenval, cntvar = 0, cntpos = 0, cnt = 0; - MorphOpaque *data = (MorphOpaque *) DatumGetPointer(opaque); + MorphOpaque *data = opaque; prs.lenwords = 4; prs.curwords = 0; prs.pos = 0; - prs.words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs.lenwords); + prs.words = palloc_array(ParsedWord, prs.lenwords); parsetext(data->cfg_id, &prs, strval, lenval); @@ -594,7 +594,7 @@ to_tsquery_byid(PG_FUNCTION_ARGS) query = parse_tsquery(text_to_cstring(in), pushval_morph, - PointerGetDatum(&data), + &data, 0, NULL); @@ -631,7 +631,7 @@ plainto_tsquery_byid(PG_FUNCTION_ARGS) query = parse_tsquery(text_to_cstring(in), pushval_morph, - PointerGetDatum(&data), + &data, P_TSQ_PLAIN, NULL); @@ -669,7 +669,7 @@ phraseto_tsquery_byid(PG_FUNCTION_ARGS) query = parse_tsquery(text_to_cstring(in), pushval_morph, - PointerGetDatum(&data), + &data, P_TSQ_PLAIN, NULL); @@ -707,7 +707,7 @@ websearch_to_tsquery_byid(PG_FUNCTION_ARGS) query = parse_tsquery(text_to_cstring(in), pushval_morph, - PointerGetDatum(&data), + &data, P_TSQ_WEB, NULL); diff --git a/src/backend/tsearch/ts_locale.c b/src/backend/tsearch/ts_locale.c index b77d8c23d3694..df02ffb12fd36 100644 --- a/src/backend/tsearch/ts_locale.c +++ b/src/backend/tsearch/ts_locale.c @@ -3,7 +3,7 @@ * ts_locale.c * locale compatibility layer for tsearch * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -20,47 +20,43 @@ static void tsearch_readline_callback(void *arg); -/* - * The reason these functions use a 3-wchar_t output buffer, not 2 as you - * might expect, is that on Windows "wchar_t" is 16 bits and what we'll be - * getting from char2wchar() is UTF16 not UTF32. A single input character - * may therefore produce a surrogate pair rather than just one wchar_t; - * we also need room for a trailing null. When we do get a surrogate pair, - * we pass just the first code to iswdigit() etc, so that these functions will - * always return false for characters outside the Basic Multilingual Plane. - */ -#define WC_BUF_LEN 3 - -int -t_isalpha(const char *ptr) -{ - int clen = pg_mblen(ptr); - wchar_t character[WC_BUF_LEN]; - pg_locale_t mylocale = 0; /* TODO */ - - if (clen == 1 || database_ctype_is_c) - return isalpha(TOUCHAR(ptr)); - - char2wchar(character, WC_BUF_LEN, ptr, clen, mylocale); - - return iswalpha((wint_t) character[0]); -} - -int -t_isalnum(const char *ptr) -{ - int clen = pg_mblen(ptr); - wchar_t character[WC_BUF_LEN]; - pg_locale_t mylocale = 0; /* TODO */ - - if (clen == 1 || database_ctype_is_c) - return isalnum(TOUCHAR(ptr)); - - char2wchar(character, WC_BUF_LEN, ptr, clen, mylocale); - - return iswalnum((wint_t) character[0]); +/* space for a single character plus a trailing NUL */ +#define WC_BUF_LEN 2 + +#define GENERATE_T_ISCLASS_DEF(character_class) \ +/* mblen shall be that of the first character */ \ +int \ +t_is##character_class##_with_len(const char *ptr, int mblen) \ +{ \ + pg_wchar wstr[WC_BUF_LEN]; \ + int wlen pg_attribute_unused(); \ + wlen = pg_mb2wchar_with_len(ptr, wstr, mblen); \ + Assert(wlen <= 1); \ + /* pass single character, or NUL if empty */ \ + return pg_isw##character_class(wstr[0], pg_database_locale()); \ +} \ +\ +/* ptr shall point to a NUL-terminated string */ \ +int \ +t_is##character_class##_cstr(const char *ptr) \ +{ \ + return t_is##character_class##_with_len(ptr, pg_mblen_cstr(ptr)); \ +} \ +/* ptr shall point to a string with pre-validated encoding */ \ +int \ +t_is##character_class##_unbounded(const char *ptr) \ +{ \ + return t_is##character_class##_with_len(ptr, pg_mblen_unbounded(ptr)); \ +} \ +/* historical name for _unbounded */ \ +int \ +t_is##character_class(const char *ptr) \ +{ \ + return t_is##character_class##_unbounded(ptr); \ } +GENERATE_T_ISCLASS_DEF(alnum) +GENERATE_T_ISCLASS_DEF(alpha) /* * Set up to read a file using tsearch_readline(). This facility is diff --git a/src/backend/tsearch/ts_parse.c b/src/backend/tsearch/ts_parse.c index e5da6cf17ec19..64b62dcd3c0b5 100644 --- a/src/backend/tsearch/ts_parse.c +++ b/src/backend/tsearch/ts_parse.c @@ -3,7 +3,7 @@ * ts_parse.c * main parse functions for tsearch * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -99,7 +99,7 @@ LPLRemoveHead(ListParsedLex *list) static void LexizeAddLemm(LexizeData *ld, int type, char *lemm, int lenlemm) { - ParsedLex *newpl = (ParsedLex *) palloc(sizeof(ParsedLex)); + ParsedLex *newpl = palloc_object(ParsedLex); newpl->type = type; newpl->lemm = lemm; @@ -218,7 +218,7 @@ LexizeExec(LexizeData *ld, ParsedLex **correspondLexem) * position and go to multiword mode */ - ld->curDictId = DatumGetObjectId(map->dictIds[i]); + ld->curDictId = map->dictIds[i]; ld->posDict = i + 1; ld->curSub = curVal->next; if (res) @@ -275,7 +275,7 @@ LexizeExec(LexizeData *ld, ParsedLex **correspondLexem) * dictionaries ? */ for (i = 0; i < map->len && !dictExists; i++) - if (ld->curDictId == DatumGetObjectId(map->dictIds[i])) + if (ld->curDictId == map->dictIds[i]) dictExists = true; if (!dictExists) diff --git a/src/backend/tsearch/ts_selfuncs.c b/src/backend/tsearch/ts_selfuncs.c index 0c1d2bc1109da..64b60bb9513ec 100644 --- a/src/backend/tsearch/ts_selfuncs.c +++ b/src/backend/tsearch/ts_selfuncs.c @@ -3,7 +3,7 @@ * ts_selfuncs.c * Selectivity estimation functions for text search operators. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -47,8 +47,8 @@ typedef struct static Selectivity tsquerysel(VariableStatData *vardata, Datum constval); static Selectivity mcelem_tsquery_selec(TSQuery query, - Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers); + const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers); static Selectivity tsquery_opr_selec(QueryItem *item, char *operand, TextFreq *lookup, int length, float4 minfreq); static int compare_lexeme_textfreq(const void *e1, const void *e2); @@ -108,12 +108,14 @@ tsmatchsel(PG_FUNCTION_ARGS) * OK, there's a Var and a Const we're dealing with here. We need the * Const to be a TSQuery, else we can't do anything useful. We have to * check this because the Var might be the TSQuery not the TSVector. + * + * Also check that the Var really is a TSVector, in case this estimator is + * mistakenly attached to some other operator. */ - if (((Const *) other)->consttype == TSQUERYOID) + if (((Const *) other)->consttype == TSQUERYOID && + vardata.vartype == TSVECTOROID) { /* tsvector @@ tsquery or the other way around */ - Assert(vardata.vartype == TSVECTOROID); - selec = tsquerysel(&vardata, ((Const *) other)->constvalue); } else @@ -204,8 +206,8 @@ tsquerysel(VariableStatData *vardata, Datum constval) * Extract data from the pg_statistic arrays into useful format. */ static Selectivity -mcelem_tsquery_selec(TSQuery query, Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers) +mcelem_tsquery_selec(TSQuery query, const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers) { float4 minfreq; TextFreq *lookup; @@ -226,21 +228,21 @@ mcelem_tsquery_selec(TSQuery query, Datum *mcelem, int nmcelem, /* * Transpose the data into a single array so we can use bsearch(). */ - lookup = (TextFreq *) palloc(sizeof(TextFreq) * nmcelem); + lookup = palloc_array(TextFreq, nmcelem); for (i = 0; i < nmcelem; i++) { /* * The text Datums came from an array, so it cannot be compressed or * stored out-of-line -- it's safe to use VARSIZE_ANY*. */ - Assert(!VARATT_IS_COMPRESSED(mcelem[i]) && !VARATT_IS_EXTERNAL(mcelem[i])); + Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(mcelem[i])) && !VARATT_IS_EXTERNAL(DatumGetPointer(mcelem[i]))); lookup[i].element = (text *) DatumGetPointer(mcelem[i]); lookup[i].frequency = numbers[i]; } /* - * Grab the lowest frequency. compute_tsvector_stats() stored it for us in - * the one before the last cell of the Numbers array. See ts_typanalyze.c + * Grab the lowest MCE frequency. compute_tsvector_stats() stored it for + * us in the one before the last cell of the Numbers array. */ minfreq = numbers[nnumbers - 2]; @@ -374,8 +376,11 @@ tsquery_opr_selec(QueryItem *item, char *operand, else { /* - * The element is not in MCELEM. Punt, but assume that the - * selectivity cannot be more than minfreq / 2. + * The element is not in MCELEM. Estimate its frequency as + * half that of the least-frequent MCE. (We know it cannot be + * more than minfreq, and it could be a great deal less. Half + * seems like a good compromise.) For probably-historical + * reasons, clamp to not more than DEFAULT_TS_MATCH_SEL. */ selec = Min(DEFAULT_TS_MATCH_SEL, minfreq / 2); } diff --git a/src/backend/tsearch/ts_typanalyze.c b/src/backend/tsearch/ts_typanalyze.c index c5a71331ce8a0..48ee050e37fc9 100644 --- a/src/backend/tsearch/ts_typanalyze.c +++ b/src/backend/tsearch/ts_typanalyze.c @@ -3,7 +3,7 @@ * ts_typanalyze.c * functions for gathering statistics from tsvector columns * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -73,7 +73,7 @@ ts_typanalyze(PG_FUNCTION_ARGS) /* * compute_tsvector_stats() -- compute statistics for a tsvector column * - * This functions computes statistics that are useful for determining @@ + * This function computes statistics that are useful for determining @@ * operations' selectivity, along with the fraction of non-null rows and * average width. * @@ -312,7 +312,7 @@ compute_tsvector_stats(VacAttrStats *stats, /* * Construct an array of the interesting hashtable items, that is, * those meeting the cutoff frequency (s - epsilon)*N. Also identify - * the minimum and maximum frequencies among these items. + * the maximum frequency among these items. * * Since epsilon = s/10 and bucket_width = 1/epsilon, the cutoff * frequency is 9*N / bucket_width. @@ -320,18 +320,16 @@ compute_tsvector_stats(VacAttrStats *stats, cutoff_freq = 9 * lexeme_no / bucket_width; i = hash_get_num_entries(lexemes_tab); /* surely enough space */ - sort_table = (TrackItem **) palloc(sizeof(TrackItem *) * i); + sort_table = palloc_array(TrackItem *, i); hash_seq_init(&scan_status, lexemes_tab); track_len = 0; - minfreq = lexeme_no; maxfreq = 0; while ((item = (TrackItem *) hash_seq_search(&scan_status)) != NULL) { if (item->frequency > cutoff_freq) { sort_table[track_len++] = item; - minfreq = Min(minfreq, item->frequency); maxfreq = Max(maxfreq, item->frequency); } } @@ -346,19 +344,38 @@ compute_tsvector_stats(VacAttrStats *stats, * If we obtained more lexemes than we really want, get rid of those * with least frequencies. The easiest way is to qsort the array into * descending frequency order and truncate the array. + * + * If we did not find more elements than we want, then it is safe to + * assume that the stored MCE array will contain every element with + * frequency above the cutoff. In that case, rather than storing the + * smallest frequency we are keeping, we want to store the minimum + * frequency that would have been accepted as a valid MCE. The + * selectivity functions can assume that that is an upper bound on the + * frequency of elements not present in the array. + * + * If we found no candidate MCEs at all, we still want to record the + * cutoff frequency, since it's still valid to assume that no element + * has frequency more than that. */ if (num_mcelem < track_len) { qsort_interruptible(sort_table, track_len, sizeof(TrackItem *), trackitem_compare_frequencies_desc, NULL); - /* reset minfreq to the smallest frequency we're keeping */ + /* set minfreq to the smallest frequency we're keeping */ minfreq = sort_table[num_mcelem - 1]->frequency; } else + { num_mcelem = track_len; + /* set minfreq to the minimum frequency above the cutoff */ + minfreq = cutoff_freq + 1; + /* ensure maxfreq is nonzero, too */ + if (track_len == 0) + maxfreq = minfreq; + } /* Generate MCELEM slot entry */ - if (num_mcelem > 0) + if (num_mcelem >= 0) { MemoryContext old_context; Datum *mcelem_values; @@ -395,8 +412,8 @@ compute_tsvector_stats(VacAttrStats *stats, * create that for a tsvector column, since null elements aren't * possible.) */ - mcelem_values = (Datum *) palloc(num_mcelem * sizeof(Datum)); - mcelem_freqs = (float4 *) palloc((num_mcelem + 2) * sizeof(float4)); + mcelem_values = palloc_array(Datum, num_mcelem); + mcelem_freqs = palloc_array(float4, num_mcelem + 2); /* * See comments above about use of nonnull_cnt as the divisor for @@ -427,7 +444,7 @@ compute_tsvector_stats(VacAttrStats *stats, stats->statypid[0] = TEXTOID; stats->statyplen[0] = -1; /* typlen, -1 for varlena */ stats->statypbyval[0] = false; - stats->statypalign[0] = 'i'; + stats->statypalign[0] = TYPALIGN_INT; } } else diff --git a/src/backend/tsearch/ts_utils.c b/src/backend/tsearch/ts_utils.c index 0b4a57866448d..52cf65533e4ee 100644 --- a/src/backend/tsearch/ts_utils.c +++ b/src/backend/tsearch/ts_utils.c @@ -3,7 +3,7 @@ * ts_utils.c * various support functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -90,7 +90,7 @@ readstoplist(const char *fname, StopList *s, char *(*wordop) (const char *, size /* Trim trailing space */ while (*pbuf && !isspace((unsigned char) *pbuf)) - pbuf += pg_mblen(pbuf); + pbuf += pg_mblen_cstr(pbuf); *pbuf = '\0'; /* Skip empty lines */ @@ -105,12 +105,12 @@ readstoplist(const char *fname, StopList *s, char *(*wordop) (const char *, size if (reallen == 0) { reallen = 64; - stop = (char **) palloc(sizeof(char *) * reallen); + stop = palloc_array(char *, reallen); } else { reallen *= 2; - stop = (char **) repalloc(stop, sizeof(char *) * reallen); + stop = repalloc_array(stop, char *, reallen); } } diff --git a/src/backend/tsearch/wparser.c b/src/backend/tsearch/wparser.c index a8ddb6109910e..8a782b66030ab 100644 --- a/src/backend/tsearch/wparser.c +++ b/src/backend/tsearch/wparser.c @@ -3,7 +3,7 @@ * wparser.c * Standard interface to word parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -58,7 +58,7 @@ tt_setup_firstcall(FuncCallContext *funcctx, FunctionCallInfo fcinfo, oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - st = (TSTokenTypeStorage *) palloc(sizeof(TSTokenTypeStorage)); + st = palloc_object(TSTokenTypeStorage); st->cur = 0; /* lextype takes one dummy argument */ st->list = (LexDescr *) DatumGetPointer(OidFunctionCall1(prs->lextypeOid, @@ -173,10 +173,10 @@ prs_setup_firstcall(FuncCallContext *funcctx, FunctionCallInfo fcinfo, oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - st = (PrsStorage *) palloc(sizeof(PrsStorage)); + st = palloc_object(PrsStorage); st->cur = 0; st->len = 16; - st->list = (LexemeEntry *) palloc(sizeof(LexemeEntry) * st->len); + st->list = palloc_array(LexemeEntry, st->len); prsdata = DatumGetPointer(FunctionCall2(&prs->prsstart, PointerGetDatum(VARDATA_ANY(txt)), @@ -204,7 +204,7 @@ prs_setup_firstcall(FuncCallContext *funcctx, FunctionCallInfo fcinfo, st->len = st->cur; st->cur = 0; - funcctx->user_fctx = (void *) st; + funcctx->user_fctx = st; if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); funcctx->tuple_desc = tupdesc; @@ -307,7 +307,7 @@ ts_headline_byid_opt(PG_FUNCTION_ARGS) memset(&prs, 0, sizeof(HeadlineParsedText)); prs.lenwords = 32; - prs.words = (HeadlineWordEntry *) palloc(sizeof(HeadlineWordEntry) * prs.lenwords); + prs.words = palloc_array(HeadlineWordEntry, prs.lenwords); hlparsetext(cfg->cfgId, &prs, query, VARDATA_ANY(in), VARSIZE_ANY_EXHDR(in)); @@ -373,11 +373,11 @@ ts_headline_jsonb_byid_opt(PG_FUNCTION_ARGS) Jsonb *out; JsonTransformStringValuesAction action = (JsonTransformStringValuesAction) headline_json_value; HeadlineParsedText prs; - HeadlineJsonState *state = palloc0(sizeof(HeadlineJsonState)); + HeadlineJsonState *state = palloc0_object(HeadlineJsonState); memset(&prs, 0, sizeof(HeadlineParsedText)); prs.lenwords = 32; - prs.words = (HeadlineWordEntry *) palloc(sizeof(HeadlineWordEntry) * prs.lenwords); + prs.words = palloc_array(HeadlineWordEntry, prs.lenwords); state->prs = &prs; state->cfg = lookup_ts_config_cache(tsconfig); @@ -450,11 +450,11 @@ ts_headline_json_byid_opt(PG_FUNCTION_ARGS) JsonTransformStringValuesAction action = (JsonTransformStringValuesAction) headline_json_value; HeadlineParsedText prs; - HeadlineJsonState *state = palloc0(sizeof(HeadlineJsonState)); + HeadlineJsonState *state = palloc0_object(HeadlineJsonState); memset(&prs, 0, sizeof(HeadlineParsedText)); prs.lenwords = 32; - prs.words = (HeadlineWordEntry *) palloc(sizeof(HeadlineWordEntry) * prs.lenwords); + prs.words = palloc_array(HeadlineWordEntry, prs.lenwords); state->prs = &prs; state->cfg = lookup_ts_config_cache(tsconfig); diff --git a/src/backend/tsearch/wparser_def.c b/src/backend/tsearch/wparser_def.c index 79bcd32a0639e..8b9b34e762a8c 100644 --- a/src/backend/tsearch/wparser_def.c +++ b/src/backend/tsearch/wparser_def.c @@ -3,7 +3,7 @@ * wparser_def.c * Default text search parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -243,9 +243,7 @@ typedef struct TParser /* string and position information */ char *str; /* multibyte string */ int lenstr; /* length of mbstring */ - wchar_t *wstr; /* wide character string */ pg_wchar *pgwstr; /* wide character string for C-locale */ - bool usewide; /* State of parse */ int charmaxlen; @@ -271,7 +269,7 @@ static bool TParserGet(TParser *prs); static TParserPosition * newTParserPosition(TParserPosition *prev) { - TParserPosition *res = (TParserPosition *) palloc(sizeof(TParserPosition)); + TParserPosition *res = palloc_object(TParserPosition); if (prev) memcpy(res, prev, sizeof(TParserPosition)); @@ -288,38 +286,13 @@ newTParserPosition(TParserPosition *prev) static TParser * TParserInit(char *str, int len) { - TParser *prs = (TParser *) palloc0(sizeof(TParser)); + TParser *prs = palloc0_object(TParser); prs->charmaxlen = pg_database_encoding_max_length(); prs->str = str; prs->lenstr = len; - - /* - * Use wide char code only when max encoding length > 1. - */ - if (prs->charmaxlen > 1) - { - pg_locale_t mylocale = 0; /* TODO */ - - prs->usewide = true; - if (database_ctype_is_c) - { - /* - * char2wchar doesn't work for C-locale and sizeof(pg_wchar) could - * be different from sizeof(wchar_t) - */ - prs->pgwstr = (pg_wchar *) palloc(sizeof(pg_wchar) * (prs->lenstr + 1)); - pg_mb2wchar_with_len(prs->str, prs->pgwstr, prs->lenstr); - } - else - { - prs->wstr = (wchar_t *) palloc(sizeof(wchar_t) * (prs->lenstr + 1)); - char2wchar(prs->wstr, prs->lenstr + 1, prs->str, prs->lenstr, - mylocale); - } - } - else - prs->usewide = false; + prs->pgwstr = palloc_array(pg_wchar, prs->lenstr + 1); + pg_mb2wchar_with_len(prs->str, prs->pgwstr, prs->lenstr); prs->state = newTParserPosition(NULL); prs->state->state = TPS_Base; @@ -345,17 +318,14 @@ TParserInit(char *str, int len) static TParser * TParserCopyInit(const TParser *orig) { - TParser *prs = (TParser *) palloc0(sizeof(TParser)); + TParser *prs = palloc0_object(TParser); prs->charmaxlen = orig->charmaxlen; prs->str = orig->str + orig->state->posbyte; prs->lenstr = orig->lenstr - orig->state->posbyte; - prs->usewide = orig->usewide; if (orig->pgwstr) prs->pgwstr = orig->pgwstr + orig->state->poschar; - if (orig->wstr) - prs->wstr = orig->wstr + orig->state->poschar; prs->state = newTParserPosition(NULL); prs->state->state = TPS_Base; @@ -379,8 +349,6 @@ TParserClose(TParser *prs) prs->state = ptr; } - if (prs->wstr) - pfree(prs->wstr); if (prs->pgwstr) pfree(prs->pgwstr); @@ -412,13 +380,9 @@ TParserCopyClose(TParser *prs) /* - * Character-type support functions, equivalent to is* macros, but - * working with any possible encodings and locales. Notes: - * - with multibyte encoding and C-locale isw* function may fail - * or give wrong result. - * - multibyte encoding and C-locale often are used for - * Asian languages. - * - if locale is C then we use pgwstr instead of wstr. + * Character-type support functions using the database default locale. If the + * locale is C, and the input character is non-ascii, the value to be returned + * is determined by the 'nonascii' macro argument. */ #define p_iswhat(type, nonascii) \ @@ -426,19 +390,13 @@ TParserCopyClose(TParser *prs) static int \ p_is##type(TParser *prs) \ { \ + pg_locale_t locale = pg_database_locale(); \ + pg_wchar wc; \ Assert(prs->state); \ - if (prs->usewide) \ - { \ - if (prs->pgwstr) \ - { \ - unsigned int c = *(prs->pgwstr + prs->state->poschar); \ - if (c > 0x7f) \ - return nonascii; \ - return is##type(c); \ - } \ - return isw##type(*(prs->wstr + prs->state->poschar)); \ - } \ - return is##type(*(unsigned char *) (prs->str + prs->state->posbyte)); \ + wc = prs->pgwstr[prs->state->poschar]; \ + if (prs->charmaxlen > 1 && locale->ctype_is_c && wc > 0x7f) \ + return nonascii; \ + return pg_isw##type(wc, pg_database_locale()); \ } \ \ static int \ @@ -703,7 +661,7 @@ p_isspecial(TParser *prs) * Check that only in utf encoding, because other encodings aren't * supported by postgres or even exists. */ - if (GetDatabaseEncoding() == PG_UTF8 && prs->usewide) + if (GetDatabaseEncoding() == PG_UTF8) { static const pg_wchar strange_letter[] = { /* @@ -944,10 +902,7 @@ p_isspecial(TParser *prs) *StopMiddle; pg_wchar c; - if (prs->pgwstr) - c = *(prs->pgwstr + prs->state->poschar); - else - c = (pg_wchar) *(prs->wstr + prs->state->poschar); + c = *(prs->pgwstr + prs->state->poschar); while (StopLow < StopHigh) { @@ -1728,7 +1683,8 @@ TParserGet(TParser *prs) prs->state->charlen = 0; else prs->state->charlen = (prs->charmaxlen == 1) ? prs->charmaxlen : - pg_mblen(prs->str + prs->state->posbyte); + pg_mblen_range(prs->str + prs->state->posbyte, + prs->str + prs->lenstr); Assert(prs->state->posbyte + prs->state->charlen <= prs->lenstr); Assert(prs->state->state >= TPS_Base && prs->state->state < TPS_Null); @@ -1877,7 +1833,7 @@ TParserGet(TParser *prs) Datum prsd_lextype(PG_FUNCTION_ARGS) { - LexDescr *descr = (LexDescr *) palloc(sizeof(LexDescr) * (LASTNUM + 1)); + LexDescr *descr = palloc_array(LexDescr, LASTNUM + 1); int i; for (i = 1; i <= LASTNUM; i++) @@ -1994,7 +1950,7 @@ checkcondition_HL(void *opaque, QueryOperand *val, ExecPhraseData *data) if (!data->pos) { - data->pos = palloc(sizeof(WordEntryPos) * checkval->len); + data->pos = palloc_array(WordEntryPos, checkval->len); data->allocated = true; data->npos = 1; data->pos[0] = checkval->words[i].pos; diff --git a/src/backend/utils/.gitignore b/src/backend/utils/.gitignore index 068555695946f..fa9cfb39693db 100644 --- a/src/backend/utils/.gitignore +++ b/src/backend/utils/.gitignore @@ -2,5 +2,9 @@ /fmgroids.h /fmgrprotos.h /fmgr-stamp +/guc_tables.inc.c /probes.h /errcodes.h +/pgstat_wait_event.c +/wait_event_funcs_data.c +/wait_event_types.h diff --git a/src/backend/utils/Gen_dummy_probes.pl b/src/backend/utils/Gen_dummy_probes.pl index 489cccf3ece12..b32362aee5ab2 100644 --- a/src/backend/utils/Gen_dummy_probes.pl +++ b/src/backend/utils/Gen_dummy_probes.pl @@ -1,7 +1,7 @@ #------------------------------------------------------------------------- # Perl script to create dummy probes.h file when dtrace is not available # -# Copyright (c) 2008-2025, PostgreSQL Global Development Group +# Copyright (c) 2008-2026, PostgreSQL Global Development Group # # src/backend/utils/Gen_dummy_probes.pl #------------------------------------------------------------------------- diff --git a/src/backend/utils/Gen_fmgrtab.pl b/src/backend/utils/Gen_fmgrtab.pl index 247e1c6ab4c0d..4373eeb7ef672 100644 --- a/src/backend/utils/Gen_fmgrtab.pl +++ b/src/backend/utils/Gen_fmgrtab.pl @@ -5,7 +5,7 @@ # Perl script that generates fmgroids.h, fmgrprotos.h, and fmgrtab.c # from pg_proc.dat # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # @@ -109,7 +109,7 @@ * These macros can be used to avoid a catalog lookup when a specific * fmgr-callable function needs to be referenced. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES @@ -140,7 +140,7 @@ * fmgrprotos.h * Prototypes for built-in functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES @@ -166,7 +166,7 @@ * fmgrtab.c * The function manager's table of internal functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES diff --git a/src/backend/utils/Makefile b/src/backend/utils/Makefile index 140fbba5c222a..81b4a956bda3f 100644 --- a/src/backend/utils/Makefile +++ b/src/backend/utils/Makefile @@ -2,7 +2,7 @@ # # Makefile for backend/utils # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/utils/Makefile @@ -43,7 +43,7 @@ generated-header-symlinks: $(top_builddir)/src/include/utils/header-stamp submak submake-adt-headers: $(MAKE) -C adt jsonpath_gram.h -$(SUBDIRS:%=%-recursive): fmgr-stamp errcodes.h +$(SUBDIRS:%=%-recursive): fmgr-stamp errcodes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h # fmgr-stamp records the last time we ran Gen_fmgrtab.pl. We don't rely on # the timestamps of the individual output files, because the Perl script @@ -55,6 +55,15 @@ fmgr-stamp: Gen_fmgrtab.pl $(catalogdir)/Catalog.pm $(top_srcdir)/src/include/ca errcodes.h: $(top_srcdir)/src/backend/utils/errcodes.txt generate-errcodes.pl $(PERL) $(srcdir)/generate-errcodes.pl --outfile $@ $< +guc_tables.inc.c: $(top_srcdir)/src/backend/utils/misc/guc_parameters.dat $(top_srcdir)/src/backend/utils/misc/gen_guc_tables.pl + $(PERL) $(top_srcdir)/src/backend/utils/misc/gen_guc_tables.pl $< $@ + +pgstat_wait_event.c: wait_event_types.h +wait_event_funcs_data.c: wait_event_types.h + +wait_event_types.h: $(top_srcdir)/src/backend/utils/activity/wait_event_names.txt $(top_srcdir)/src/backend/utils/activity/generate-wait_event_types.pl + $(PERL) $(top_srcdir)/src/backend/utils/activity/generate-wait_event_types.pl --code $< + ifeq ($(enable_dtrace), yes) probes.h: postprocess_dtrace.sed probes.h.tmp sed -f $^ >$@ @@ -70,8 +79,8 @@ endif # These generated headers must be symlinked into src/include/. # We use header-stamp to record that we've done this because the symlinks # themselves may appear older than fmgr-stamp. -$(top_builddir)/src/include/utils/header-stamp: fmgr-stamp errcodes.h probes.h - cd '$(dir $@)' && for file in fmgroids.h fmgrprotos.h errcodes.h probes.h; do \ +$(top_builddir)/src/include/utils/header-stamp: fmgr-stamp errcodes.h probes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h + cd '$(dir $@)' && for file in fmgroids.h fmgrprotos.h errcodes.h probes.h guc_tables.inc.c pgstat_wait_event.c wait_event_funcs_data.c wait_event_types.h; do \ rm -f $$file && $(LN_S) "../../../$(subdir)/$$file" . ; \ done touch $@ @@ -89,4 +98,5 @@ uninstall-data: clean: rm -f probes.h probes.h.tmp - rm -f fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h + rm -f fmgroids.h fmgrprotos.h fmgrtab.c fmgr-stamp errcodes.h guc_tables.inc.c + rm -f wait_event_types.h pgstat_wait_event.c wait_event_funcs_data.c diff --git a/src/backend/utils/activity/.gitignore b/src/backend/utils/activity/.gitignore deleted file mode 100644 index bd0c0c7772984..0000000000000 --- a/src/backend/utils/activity/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/pgstat_wait_event.c -/wait_event_types.h -/wait_event_funcs_data.c diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile index 9c2443e1ecd37..ca3ef89bf5997 100644 --- a/src/backend/utils/activity/Makefile +++ b/src/backend/utils/activity/Makefile @@ -2,7 +2,7 @@ # # Makefile for backend/utils/activity # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/utils/activity/Makefile @@ -26,6 +26,7 @@ OBJS = \ pgstat_database.o \ pgstat_function.o \ pgstat_io.o \ + pgstat_lock.o \ pgstat_relation.o \ pgstat_replslot.o \ pgstat_shmem.o \ @@ -36,17 +37,8 @@ OBJS = \ wait_event.o \ wait_event_funcs.o -include $(top_srcdir)/src/backend/common.mk - -wait_event_funcs.o: wait_event_funcs_data.c -wait_event_funcs_data.c: wait_event_types.h - -wait_event.o: pgstat_wait_event.c -pgstat_wait_event.c: wait_event_types.h - touch $@ +# Force these dependencies to be known even without dependency info built: +wait_event.o: wait_event.c $(top_builddir)/src/backend/utils/pgstat_wait_event.c +wait_event_funcs.o: wait_event_funcs.c $(top_builddir)/src/backend/utils/wait_event_funcs_data.c -wait_event_types.h: $(top_srcdir)/src/backend/utils/activity/wait_event_names.txt generate-wait_event_types.pl - $(PERL) $(srcdir)/generate-wait_event_types.pl --code $< - -clean: - rm -f wait_event_types.h pgstat_wait_event.c wait_event_funcs_data.c +include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/utils/activity/backend_progress.c b/src/backend/utils/activity/backend_progress.c index 99a8c73bf0470..b0359771de50a 100644 --- a/src/backend/utils/activity/backend_progress.c +++ b/src/backend/utils/activity/backend_progress.c @@ -3,7 +3,7 @@ * * Command progress reporting infrastructure. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/backend/utils/activity/backend_progress.c * ---------- @@ -12,6 +12,7 @@ #include "access/parallel.h" #include "libpq/pqformat.h" +#include "storage/proc.h" #include "utils/backend_progress.h" #include "utils/backend_status.h" diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c index e1576e64b6d4c..d685fc5cd87c0 100644 --- a/src/backend/utils/activity/backend_status.c +++ b/src/backend/utils/activity/backend_status.c @@ -2,7 +2,7 @@ * backend_status.c * Backend status reporting infrastructure. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -19,6 +19,8 @@ #include "storage/ipc.h" #include "storage/proc.h" /* for MyProc */ #include "storage/procarray.h" +#include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/ascii.h" #include "utils/guc.h" /* for application_name */ #include "utils/memutils.h" @@ -73,133 +75,97 @@ static void pgstat_beshutdown_hook(int code, Datum arg); static void pgstat_read_current_status(void); static void pgstat_setup_backend_status_context(void); +static void BackendStatusShmemRequest(void *arg); +static void BackendStatusShmemInit(void *arg); +static void BackendStatusShmemAttach(void *arg); + +const ShmemCallbacks BackendStatusShmemCallbacks = { + .request_fn = BackendStatusShmemRequest, + .init_fn = BackendStatusShmemInit, + .attach_fn = BackendStatusShmemAttach, +}; /* - * Report shared-memory space needed by BackendStatusShmemInit. + * Register shared memory needs for backend status reporting. */ -Size -BackendStatusShmemSize(void) +static void +BackendStatusShmemRequest(void *arg) { - Size size; - - /* BackendStatusArray: */ - size = mul_size(sizeof(PgBackendStatus), NumBackendStatSlots); - /* BackendAppnameBuffer: */ - size = add_size(size, - mul_size(NAMEDATALEN, NumBackendStatSlots)); - /* BackendClientHostnameBuffer: */ - size = add_size(size, - mul_size(NAMEDATALEN, NumBackendStatSlots)); - /* BackendActivityBuffer: */ - size = add_size(size, - mul_size(pgstat_track_activity_query_size, NumBackendStatSlots)); + ShmemRequestStruct(.name = "Backend Status Array", + .size = mul_size(sizeof(PgBackendStatus), NumBackendStatSlots), + .ptr = (void **) &BackendStatusArray, + ); + + ShmemRequestStruct(.name = "Backend Application Name Buffer", + .size = mul_size(NAMEDATALEN, NumBackendStatSlots), + .ptr = (void **) &BackendAppnameBuffer, + ); + + ShmemRequestStruct(.name = "Backend Client Host Name Buffer", + .size = mul_size(NAMEDATALEN, NumBackendStatSlots), + .ptr = (void **) &BackendClientHostnameBuffer, + ); + + BackendActivityBufferSize = mul_size(pgstat_track_activity_query_size, + NumBackendStatSlots); + ShmemRequestStruct(.name = "Backend Activity Buffer", + .size = BackendActivityBufferSize, + .ptr = (void **) &BackendActivityBuffer + ); + #ifdef USE_SSL - /* BackendSslStatusBuffer: */ - size = add_size(size, - mul_size(sizeof(PgBackendSSLStatus), NumBackendStatSlots)); + ShmemRequestStruct(.name = "Backend SSL Status Buffer", + .size = mul_size(sizeof(PgBackendSSLStatus), NumBackendStatSlots), + .ptr = (void **) &BackendSslStatusBuffer, + ); #endif + #ifdef ENABLE_GSS - /* BackendGssStatusBuffer: */ - size = add_size(size, - mul_size(sizeof(PgBackendGSSStatus), NumBackendStatSlots)); + ShmemRequestStruct(.name = "Backend GSS Status Buffer", + .size = mul_size(sizeof(PgBackendGSSStatus), NumBackendStatSlots), + .ptr = (void **) &BackendGssStatusBuffer, + ); #endif - return size; } /* * Initialize the shared status array and several string buffers * during postmaster startup. */ -void -BackendStatusShmemInit(void) +static void +BackendStatusShmemInit(void *arg) { - Size size; - bool found; int i; char *buffer; - /* Create or attach to the shared array */ - size = mul_size(sizeof(PgBackendStatus), NumBackendStatSlots); - BackendStatusArray = (PgBackendStatus *) - ShmemInitStruct("Backend Status Array", size, &found); - - if (!found) + /* Initialize st_appname pointers. */ + buffer = BackendAppnameBuffer; + for (i = 0; i < NumBackendStatSlots; i++) { - /* - * We're the first - initialize. - */ - MemSet(BackendStatusArray, 0, size); + BackendStatusArray[i].st_appname = buffer; + buffer += NAMEDATALEN; } - /* Create or attach to the shared appname buffer */ - size = mul_size(NAMEDATALEN, NumBackendStatSlots); - BackendAppnameBuffer = (char *) - ShmemInitStruct("Backend Application Name Buffer", size, &found); - - if (!found) + /* Initialize st_clienthostname pointers. */ + buffer = BackendClientHostnameBuffer; + for (i = 0; i < NumBackendStatSlots; i++) { - MemSet(BackendAppnameBuffer, 0, size); - - /* Initialize st_appname pointers. */ - buffer = BackendAppnameBuffer; - for (i = 0; i < NumBackendStatSlots; i++) - { - BackendStatusArray[i].st_appname = buffer; - buffer += NAMEDATALEN; - } + BackendStatusArray[i].st_clienthostname = buffer; + buffer += NAMEDATALEN; } - /* Create or attach to the shared client hostname buffer */ - size = mul_size(NAMEDATALEN, NumBackendStatSlots); - BackendClientHostnameBuffer = (char *) - ShmemInitStruct("Backend Client Host Name Buffer", size, &found); - - if (!found) + /* Initialize st_activity pointers. */ + buffer = BackendActivityBuffer; + for (i = 0; i < NumBackendStatSlots; i++) { - MemSet(BackendClientHostnameBuffer, 0, size); - - /* Initialize st_clienthostname pointers. */ - buffer = BackendClientHostnameBuffer; - for (i = 0; i < NumBackendStatSlots; i++) - { - BackendStatusArray[i].st_clienthostname = buffer; - buffer += NAMEDATALEN; - } - } - - /* Create or attach to the shared activity buffer */ - BackendActivityBufferSize = mul_size(pgstat_track_activity_query_size, - NumBackendStatSlots); - BackendActivityBuffer = (char *) - ShmemInitStruct("Backend Activity Buffer", - BackendActivityBufferSize, - &found); - - if (!found) - { - MemSet(BackendActivityBuffer, 0, BackendActivityBufferSize); - - /* Initialize st_activity pointers. */ - buffer = BackendActivityBuffer; - for (i = 0; i < NumBackendStatSlots; i++) - { - BackendStatusArray[i].st_activity_raw = buffer; - buffer += pgstat_track_activity_query_size; - } + BackendStatusArray[i].st_activity_raw = buffer; + buffer += pgstat_track_activity_query_size; } #ifdef USE_SSL - /* Create or attach to the shared SSL status buffer */ - size = mul_size(sizeof(PgBackendSSLStatus), NumBackendStatSlots); - BackendSslStatusBuffer = (PgBackendSSLStatus *) - ShmemInitStruct("Backend SSL Status Buffer", size, &found); - - if (!found) { PgBackendSSLStatus *ptr; - MemSet(BackendSslStatusBuffer, 0, size); - /* Initialize st_sslstatus pointers. */ ptr = BackendSslStatusBuffer; for (i = 0; i < NumBackendStatSlots; i++) @@ -211,17 +177,9 @@ BackendStatusShmemInit(void) #endif #ifdef ENABLE_GSS - /* Create or attach to the shared GSSAPI status buffer */ - size = mul_size(sizeof(PgBackendGSSStatus), NumBackendStatSlots); - BackendGssStatusBuffer = (PgBackendGSSStatus *) - ShmemInitStruct("Backend GSS Status Buffer", size, &found); - - if (!found) { PgBackendGSSStatus *ptr; - MemSet(BackendGssStatusBuffer, 0, size); - /* Initialize st_gssstatus pointers. */ ptr = BackendGssStatusBuffer; for (i = 0; i < NumBackendStatSlots; i++) @@ -233,6 +191,13 @@ BackendStatusShmemInit(void) #endif } +static void +BackendStatusShmemAttach(void *arg) +{ + BackendActivityBufferSize = mul_size(pgstat_track_activity_query_size, + NumBackendStatSlots); +} + /* * Initialize pgstats backend activity state, and set up our on-proc-exit * hook. Called from InitPostgres and AuxiliaryProcessMain. MyProcNumber must @@ -320,8 +285,8 @@ pgstat_bestart_initial(void) lbeentry.st_state = STATE_STARTING; lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID; lbeentry.st_progress_command_target = InvalidOid; - lbeentry.st_query_id = UINT64CONST(0); - lbeentry.st_plan_id = UINT64CONST(0); + lbeentry.st_query_id = INT64CONST(0); + lbeentry.st_plan_id = INT64CONST(0); /* * we don't zero st_progress_param here to save cycles; nobody should @@ -599,8 +564,8 @@ pgstat_report_activity(BackendState state, const char *cmd_str) beentry->st_activity_start_timestamp = 0; /* st_xact_start_timestamp and wait_event_info are also disabled */ beentry->st_xact_start_timestamp = 0; - beentry->st_query_id = UINT64CONST(0); - beentry->st_plan_id = UINT64CONST(0); + beentry->st_query_id = INT64CONST(0); + beentry->st_plan_id = INT64CONST(0); proc->wait_event_info = 0; PGSTAT_END_WRITE_ACTIVITY(beentry); } @@ -662,8 +627,8 @@ pgstat_report_activity(BackendState state, const char *cmd_str) */ if (state == STATE_RUNNING) { - beentry->st_query_id = UINT64CONST(0); - beentry->st_plan_id = UINT64CONST(0); + beentry->st_query_id = INT64CONST(0); + beentry->st_plan_id = INT64CONST(0); } if (cmd_str != NULL) @@ -683,7 +648,7 @@ pgstat_report_activity(BackendState state, const char *cmd_str) * -------- */ void -pgstat_report_query_id(uint64 query_id, bool force) +pgstat_report_query_id(int64 query_id, bool force) { volatile PgBackendStatus *beentry = MyBEEntry; @@ -702,7 +667,7 @@ pgstat_report_query_id(uint64 query_id, bool force) * command, so ignore the one provided unless it's an explicit call to * reset the identifier. */ - if (beentry->st_query_id != 0 && !force) + if (beentry->st_query_id != INT64CONST(0) && !force) return; /* @@ -722,7 +687,7 @@ pgstat_report_query_id(uint64 query_id, bool force) * -------- */ void -pgstat_report_plan_id(uint64 plan_id, bool force) +pgstat_report_plan_id(int64 plan_id, bool force) { volatile PgBackendStatus *beentry = MyBEEntry; @@ -1134,7 +1099,7 @@ pgstat_get_crashed_backend_activity(int pid, char *buffer, int buflen) * * Return current backend's query identifier. */ -uint64 +int64 pgstat_get_my_query_id(void) { if (!MyBEEntry) @@ -1154,7 +1119,7 @@ pgstat_get_my_query_id(void) * * Return current backend's plan identifier. */ -uint64 +int64 pgstat_get_my_plan_id(void) { if (!MyBEEntry) @@ -1164,31 +1129,6 @@ pgstat_get_my_plan_id(void) return MyBEEntry->st_plan_id; } -/* ---------- - * pgstat_get_backend_type_by_proc_number() - - * - * Return the type of the backend with the specified ProcNumber. This looks - * directly at the BackendStatusArray, so the return value may be out of date. - * The only current use of this function is in pg_signal_backend(), which is - * inherently racy, so we don't worry too much about this. - * - * It is the caller's responsibility to use this wisely; at minimum, callers - * should ensure that procNumber is valid and perform the required permissions - * checks. - * ---------- - */ -BackendType -pgstat_get_backend_type_by_proc_number(ProcNumber procNumber) -{ - volatile PgBackendStatus *status = &BackendStatusArray[procNumber]; - - /* - * We bypass the changecount mechanism since fetching and storing an int - * is almost certainly atomic. - */ - return status->st_backendType; -} - /* ---------- * cmp_lbestatus * diff --git a/src/backend/utils/activity/generate-wait_event_types.pl b/src/backend/utils/activity/generate-wait_event_types.pl index 424ad9f115d34..d39a30d04783d 100644 --- a/src/backend/utils/activity/generate-wait_event_types.pl +++ b/src/backend/utils/activity/generate-wait_event_types.pl @@ -7,7 +7,7 @@ # - wait_event_funcs_data.c (if --code is passed) # - wait_event_types.sgml (if --docs is passed) # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/utils/activity/generate-wait_event_types.pl @@ -85,7 +85,7 @@ # Sort the lines based on the second column. # uc() is being used to force the comparison to be case-insensitive. my @lines_sorted = - sort { uc((split(/\t/, $a))[1]) cmp uc((split(/\t/, $b))[1]) } @lines; + sort { uc((split(/\t+/, $a))[1]) cmp uc((split(/\t+/, $b))[1]) } @lines; # If we are generating code, concat @lines_sorted and then # @abi_compatibility_lines. @@ -101,7 +101,7 @@ unless $line =~ /^(\w+)\t+(\w+)\t+("\w.*\.")$/; (my $waitclassname, my $waiteventname, my $waitevendocsentence) = - split(/\t/, $line); + ($1, $2, $3); # Generate the element name for the enums based on the # description. The C symbols are prefixed with "WAIT_EVENT_". @@ -150,7 +150,7 @@ * %s * Generated wait events infrastructure code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES @@ -334,12 +334,12 @@ sub usage { die <] [--code ] [ --sgml ] input_file +Usage: perl [--output ] [--code ] [ --docs ] input_file Options: --outdir Output directory (default '.') --code Generate C and header files. - --sgml Generate wait_event_types.sgml. + --docs Generate wait_event_types.sgml. generate-wait_event_types.pl generates the SGML documentation and code related to wait events. This should use wait_event_names.txt in input, or diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build index d8e56b49c247d..1aa7ece52908c 100644 --- a/src/backend/utils/activity/meson.build +++ b/src/backend/utils/activity/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'backend_progress.c', @@ -11,6 +11,7 @@ backend_sources += files( 'pgstat_database.c', 'pgstat_function.c', 'pgstat_io.c', + 'pgstat_lock.c', 'pgstat_relation.c', 'pgstat_replslot.c', 'pgstat_shmem.c', @@ -30,7 +31,6 @@ waitevent_sources = files( wait_event = static_library('wait_event_names', waitevent_sources, dependencies: [backend_code], - include_directories: include_directories('../../../include/utils'), kwargs: internal_lib_args, ) diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 8b57845e8709f..b67da88c7dc22 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -83,6 +83,7 @@ * - pgstat_database.c * - pgstat_function.c * - pgstat_io.c + * - pgstat_lock.c * - pgstat_relation.c * - pgstat_replslot.c * - pgstat_slru.c @@ -93,7 +94,7 @@ * specific kinds of stats. * * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat.c @@ -212,6 +213,11 @@ int pgstat_fetch_consistency = PGSTAT_FETCH_CONSISTENCY_CACHE; PgStat_LocalState pgStatLocal; +/* + * Track pending reports for fixed-numbered stats, used by + * pgstat_report_stat(). + */ +bool pgstat_report_fixed = false; /* ---------- * Local data @@ -308,6 +314,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .flush_pending_cb = pgstat_relation_flush_cb, .delete_pending_cb = pgstat_relation_delete_pending_cb, + .reset_timestamp_cb = pgstat_relation_reset_timestamp_cb, }, [PGSTAT_KIND_FUNCTION] = { @@ -322,6 +329,7 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .pending_size = sizeof(PgStat_FunctionCounts), .flush_pending_cb = pgstat_function_flush_cb, + .reset_timestamp_cb = pgstat_function_reset_timestamp_cb, }, [PGSTAT_KIND_REPLSLOT] = { @@ -370,7 +378,6 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .shared_data_off = offsetof(PgStatShared_Backend, stats), .shared_data_len = sizeof(((PgStatShared_Backend *) 0)->stats), - .have_static_pending_cb = pgstat_backend_have_pending_cb, .flush_static_cb = pgstat_backend_flush_cb, .reset_timestamp_cb = pgstat_backend_reset_timestamp_cb, }, @@ -437,12 +444,28 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .shared_data_len = sizeof(((PgStatShared_IO *) 0)->stats), .flush_static_cb = pgstat_io_flush_cb, - .have_static_pending_cb = pgstat_io_have_pending_cb, .init_shmem_cb = pgstat_io_init_shmem_cb, .reset_all_cb = pgstat_io_reset_all_cb, .snapshot_cb = pgstat_io_snapshot_cb, }, + [PGSTAT_KIND_LOCK] = { + .name = "lock", + + .fixed_amount = true, + .write_to_file = true, + + .snapshot_ctl_off = offsetof(PgStat_Snapshot, lock), + .shared_ctl_off = offsetof(PgStat_ShmemControl, lock), + .shared_data_off = offsetof(PgStatShared_Lock, stats), + .shared_data_len = sizeof(((PgStatShared_Lock *) 0)->stats), + + .flush_static_cb = pgstat_lock_flush_cb, + .init_shmem_cb = pgstat_lock_init_shmem_cb, + .reset_all_cb = pgstat_lock_reset_all_cb, + .snapshot_cb = pgstat_lock_snapshot_cb, + }, + [PGSTAT_KIND_SLRU] = { .name = "slru", @@ -455,7 +478,6 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .shared_data_len = sizeof(((PgStatShared_SLRU *) 0)->stats), .flush_static_cb = pgstat_slru_flush_cb, - .have_static_pending_cb = pgstat_slru_have_pending_cb, .init_shmem_cb = pgstat_slru_init_shmem_cb, .reset_all_cb = pgstat_slru_reset_all_cb, .snapshot_cb = pgstat_slru_snapshot_cb, @@ -474,7 +496,6 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .init_backend_cb = pgstat_wal_init_backend_cb, .flush_static_cb = pgstat_wal_flush_cb, - .have_static_pending_cb = pgstat_wal_have_pending_cb, .init_shmem_cb = pgstat_wal_init_shmem_cb, .reset_all_cb = pgstat_wal_reset_all_cb, .snapshot_cb = pgstat_wal_snapshot_cb, @@ -520,6 +541,7 @@ pgstat_discard_stats(void) /* NB: this needs to be done even in single user mode */ + /* First, cleanup the main pgstats file */ ret = unlink(PGSTAT_STAT_PERMANENT_FILENAME); if (ret != 0) { @@ -541,6 +563,15 @@ pgstat_discard_stats(void) PGSTAT_STAT_PERMANENT_FILENAME))); } + /* Finish callbacks, if required */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (kind_info && kind_info->finish) + kind_info->finish(STATS_DISCARD); + } + /* * Reset stats contents. This will set reset timestamps of fixed-numbered * stats to the current time (no variable stats exist). @@ -708,29 +739,10 @@ pgstat_report_stat(bool force) } /* Don't expend a clock check if nothing to do */ - if (dlist_is_empty(&pgStatPending)) + if (dlist_is_empty(&pgStatPending) && + !pgstat_report_fixed) { - bool do_flush = false; - - /* Check for pending stats */ - for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) - { - const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); - - if (!kind_info) - continue; - if (!kind_info->have_static_pending_cb) - continue; - - if (kind_info->have_static_pending_cb()) - { - do_flush = true; - break; - } - } - - if (!do_flush) - return 0; + return 0; } /* @@ -784,16 +796,19 @@ pgstat_report_stat(bool force) partial_flush |= pgstat_flush_pending_entries(nowait); /* flush of other stats kinds */ - for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + if (pgstat_report_fixed) { - const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); - if (!kind_info) - continue; - if (!kind_info->flush_static_cb) - continue; + if (!kind_info) + continue; + if (!kind_info->flush_static_cb) + continue; - partial_flush |= kind_info->flush_static_cb(nowait); + partial_flush |= kind_info->flush_static_cb(nowait); + } } last_flush = now; @@ -815,6 +830,7 @@ pgstat_report_stat(bool force) } pending_since = 0; + pgstat_report_fixed = false; return 0; } @@ -835,7 +851,7 @@ pgstat_force_next_flush(void) static bool match_db_entries(PgStatShared_HashEntry *entry, Datum match_data) { - return entry->key.dboid == DatumGetObjectId(MyDatabaseId); + return entry->key.dboid == MyDatabaseId; } /* @@ -944,9 +960,9 @@ pgstat_clear_snapshot(void) } void * -pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid) +pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, bool *may_free) { - PgStat_HashKey key; + PgStat_HashKey key = {0}; PgStat_EntryRef *entry_ref; void *stats_data; const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); @@ -955,10 +971,14 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid) Assert(IsUnderPostmaster || !IsPostmasterEnvironment); Assert(!kind_info->fixed_amount); - pgstat_prep_snapshot(); + /* + * Initialize *may_free to false. We'll change it to true later if we end + * up allocating the result in the caller's context and not caching it. + */ + if (may_free) + *may_free = false; - /* clear padding */ - memset(&key, 0, sizeof(struct PgStat_HashKey)); + pgstat_prep_snapshot(); key.kind = kind; key.dboid = dboid; @@ -1011,7 +1031,16 @@ pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid) * repeated accesses. */ if (pgstat_fetch_consistency == PGSTAT_FETCH_CONSISTENCY_NONE) + { stats_data = palloc(kind_info->shared_data_len); + + /* + * Since we allocated the result in the caller's context and aren't + * caching it, the caller can safely pfree() it. + */ + if (may_free) + *may_free = true; + } else stats_data = MemoryContextAlloc(pgStatLocal.snapshot.context, kind_info->shared_data_len); @@ -1504,6 +1533,10 @@ pgstat_register_kind(PgStat_Kind kind, const PgStat_KindInfo *kind_info) ereport(ERROR, (errmsg("custom cumulative statistics property is invalid"), errhint("Custom cumulative statistics require a shared memory size for fixed-numbered objects."))); + if (kind_info->track_entry_count) + ereport(ERROR, + (errmsg("custom cumulative statistics property is invalid"), + errhint("Custom cumulative statistics cannot use entry count tracking for fixed-numbered objects."))); } /* @@ -1562,20 +1595,18 @@ pgstat_assert_is_up(void) * ------------------------------------------------------------ */ -/* helpers for pgstat_write_statsfile() */ -static void -write_chunk(FILE *fpout, void *ptr, size_t len) +/* helper for pgstat_write_statsfile() */ +void +pgstat_write_chunk(FILE *fpout, void *ptr, size_t len) { int rc; rc = fwrite(ptr, len, 1, fpout); - /* we'll check for errors with ferror once at the end */ + /* We check for errors with ferror() when done writing the stats. */ (void) rc; } -#define write_chunk_s(fpout, ptr) write_chunk(fpout, ptr, sizeof(*ptr)) - /* * This function is called in the last process that is accessing the shared * stats so locking is not required. @@ -1617,7 +1648,7 @@ pgstat_write_statsfile(void) * Write the file header --- currently just a format ID. */ format_id = PGSTAT_FILE_FORMAT_ID; - write_chunk_s(fpout, &format_id); + pgstat_write_chunk_s(fpout, &format_id); /* Write various stats structs for fixed number of objects */ for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) @@ -1642,8 +1673,8 @@ pgstat_write_statsfile(void) ptr = pgStatLocal.snapshot.custom_data[kind - PGSTAT_KIND_CUSTOM_MIN]; fputc(PGSTAT_FILE_ENTRY_FIXED, fpout); - write_chunk_s(fpout, &kind); - write_chunk(fpout, ptr, info->shared_data_len); + pgstat_write_chunk_s(fpout, &kind); + pgstat_write_chunk(fpout, ptr, info->shared_data_len); } /* @@ -1697,7 +1728,7 @@ pgstat_write_statsfile(void) { /* normal stats entry, identified by PgStat_HashKey */ fputc(PGSTAT_FILE_ENTRY_HASH, fpout); - write_chunk_s(fpout, &ps->key); + pgstat_write_chunk_s(fpout, &ps->key); } else { @@ -1707,21 +1738,25 @@ pgstat_write_statsfile(void) kind_info->to_serialized_name(&ps->key, shstats, &name); fputc(PGSTAT_FILE_ENTRY_NAME, fpout); - write_chunk_s(fpout, &ps->key.kind); - write_chunk_s(fpout, &name); + pgstat_write_chunk_s(fpout, &ps->key.kind); + pgstat_write_chunk_s(fpout, &name); } /* Write except the header part of the entry */ - write_chunk(fpout, - pgstat_get_entry_data(ps->key.kind, shstats), - pgstat_get_entry_len(ps->key.kind)); + pgstat_write_chunk(fpout, + pgstat_get_entry_data(ps->key.kind, shstats), + pgstat_get_entry_len(ps->key.kind)); + + /* Write more data for the entry, if required */ + if (kind_info->to_serialized_data) + kind_info->to_serialized_data(&ps->key, shstats, fpout); } dshash_seq_term(&hstat); /* * No more output to be done. Close the temp file and replace the old * pgstat.stat with it. The ferror() check replaces testing for error - * after each individual fputc or fwrite (in write_chunk()) above. + * after each individual fputc or fwrite (in pgstat_write_chunk()) above. */ fputc(PGSTAT_FILE_ENTRY_END, fpout); @@ -1747,17 +1782,24 @@ pgstat_write_statsfile(void) /* durable_rename already emitted log message */ unlink(tmpfile); } + + /* Finish callbacks, if required */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (kind_info && kind_info->finish) + kind_info->finish(STATS_WRITE); + } } -/* helpers for pgstat_read_statsfile() */ -static bool -read_chunk(FILE *fpin, void *ptr, size_t len) +/* helper for pgstat_read_statsfile() */ +bool +pgstat_read_chunk(FILE *fpin, void *ptr, size_t len) { return fread(ptr, 1, len, fpin) == len; } -#define read_chunk_s(fpin, ptr) read_chunk(fpin, ptr, sizeof(*ptr)) - /* * Reads in existing statistics file into memory. * @@ -1801,7 +1843,7 @@ pgstat_read_statsfile(void) /* * Verify it's of the expected format. */ - if (!read_chunk_s(fpin, &format_id)) + if (!pgstat_read_chunk_s(fpin, &format_id)) { elog(WARNING, "could not read format ID"); goto error; @@ -1831,7 +1873,7 @@ pgstat_read_statsfile(void) char *ptr; /* entry for fixed-numbered stats */ - if (!read_chunk_s(fpin, &kind)) + if (!pgstat_read_chunk_s(fpin, &kind)) { elog(WARNING, "could not read stats kind for entry of type %c", t); goto error; @@ -1871,7 +1913,7 @@ pgstat_read_statsfile(void) info->shared_data_off; } - if (!read_chunk(fpin, ptr, info->shared_data_len)) + if (!pgstat_read_chunk(fpin, ptr, info->shared_data_len)) { elog(WARNING, "could not read data of stats kind %u for entry of type %c with size %u", kind, t, info->shared_data_len); @@ -1886,13 +1928,14 @@ pgstat_read_statsfile(void) PgStat_HashKey key; PgStatShared_HashEntry *p; PgStatShared_Common *header; + const PgStat_KindInfo *kind_info = NULL; CHECK_FOR_INTERRUPTS(); if (t == PGSTAT_FILE_ENTRY_HASH) { /* normal stats entry, identified by PgStat_HashKey */ - if (!read_chunk_s(fpin, &key)) + if (!pgstat_read_chunk_s(fpin, &key)) { elog(WARNING, "could not read key for entry of type %c", t); goto error; @@ -1906,7 +1949,8 @@ pgstat_read_statsfile(void) goto error; } - if (!pgstat_get_kind_info(key.kind)) + kind_info = pgstat_get_kind_info(key.kind); + if (!kind_info) { elog(WARNING, "could not find information of kind for entry %u/%u/%" PRIu64 " of type %c", key.kind, key.dboid, @@ -1917,16 +1961,15 @@ pgstat_read_statsfile(void) else { /* stats entry identified by name on disk (e.g. slots) */ - const PgStat_KindInfo *kind_info = NULL; PgStat_Kind kind; NameData name; - if (!read_chunk_s(fpin, &kind)) + if (!pgstat_read_chunk_s(fpin, &kind)) { elog(WARNING, "could not read stats kind for entry of type %c", t); goto error; } - if (!read_chunk_s(fpin, &name)) + if (!pgstat_read_chunk_s(fpin, &name)) { elog(WARNING, "could not read name of stats kind %u for entry of type %c", kind, t); @@ -1989,10 +2032,21 @@ pgstat_read_statsfile(void) header = pgstat_init_entry(key.kind, p); dshash_release_lock(pgStatLocal.shared_hash, p); + if (header == NULL) + { + /* + * It would be tempting to switch this ERROR to a + * WARNING, but it would mean that all the statistics + * are discarded when the environment fails on OOM. + */ + elog(ERROR, "could not allocate entry %u/%u/%" PRIu64 " of type %c", + key.kind, key.dboid, + key.objid, t); + } - if (!read_chunk(fpin, - pgstat_get_entry_data(key.kind, header), - pgstat_get_entry_len(key.kind))) + if (!pgstat_read_chunk(fpin, + pgstat_get_entry_data(key.kind, header), + pgstat_get_entry_len(key.kind))) { elog(WARNING, "could not read data for entry %u/%u/%" PRIu64 " of type %c", key.kind, key.dboid, @@ -2000,6 +2054,18 @@ pgstat_read_statsfile(void) goto error; } + /* read more data for the entry, if required */ + if (kind_info->from_serialized_data) + { + if (!kind_info->from_serialized_data(&key, header, fpin)) + { + elog(WARNING, "could not read auxiliary data for entry %u/%u/%" PRIu64 " of type %c", + key.kind, key.dboid, + key.objid, t); + goto error; + } + } + break; } case PGSTAT_FILE_ENTRY_END: @@ -2023,11 +2089,21 @@ pgstat_read_statsfile(void) } done: + /* First, cleanup the main stats file */ FreeFile(fpin); elog(DEBUG2, "removing permanent stats file \"%s\"", statfile); unlink(statfile); + /* Finish callbacks, if required */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (kind_info && kind_info->finish) + kind_info->finish(STATS_READ); + } + return; error: diff --git a/src/backend/utils/activity/pgstat_archiver.c b/src/backend/utils/activity/pgstat_archiver.c index c2ba2b31972e0..3d60035843f38 100644 --- a/src/backend/utils/activity/pgstat_archiver.c +++ b/src/backend/utils/activity/pgstat_archiver.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_archiver.c diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c index 51256277e8d37..73461c9bca590 100644 --- a/src/backend/utils/activity/pgstat_backend.c +++ b/src/backend/utils/activity/pgstat_backend.c @@ -15,7 +15,7 @@ * PgStat_EntryRef->pending, relying on PendingBackendStats instead so as it * is possible to report data within critical sections. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_backend.c @@ -25,6 +25,7 @@ #include "postgres.h" #include "access/xlog.h" +#include "executor/instrument.h" #include "storage/bufmgr.h" #include "storage/proc.h" #include "storage/procarray.h" @@ -41,9 +42,9 @@ static bool backend_has_iostats = false; /* * WAL usage counters saved from pgWalUsage at the previous call to - * pgstat_report_wal(). This is used to calculate how much WAL usage - * happens between pgstat_report_wal() calls, by subtracting the previous - * counters from the current ones. + * pgstat_flush_backend(). This is used to calculate how much WAL usage + * happens between pgstat_flush_backend() calls, by subtracting the + * previous counters from the current ones. */ static WalUsage prevBackendWalUsage; @@ -66,6 +67,7 @@ pgstat_count_backend_io_op_time(IOObject io_object, IOContext io_context, io_time); backend_has_iostats = true; + pgstat_report_fixed = true; } void @@ -81,6 +83,7 @@ pgstat_count_backend_io_op(IOObject io_object, IOContext io_context, PendingBackendStats.pending_io.bytes[io_object][io_context][io_op] += bytes; backend_has_iostats = true; + pgstat_report_fixed = true; } /* @@ -92,7 +95,8 @@ pgstat_fetch_stat_backend(ProcNumber procNumber) PgStat_Backend *backend_entry; backend_entry = (PgStat_Backend *) pgstat_fetch_entry(PGSTAT_KIND_BACKEND, - InvalidOid, procNumber); + InvalidOid, procNumber, + NULL); return backend_entry; } @@ -249,6 +253,7 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref) WALSTAT_ACC(wal_records, wal_usage_diff); WALSTAT_ACC(wal_fpi, wal_usage_diff); WALSTAT_ACC(wal_bytes, wal_usage_diff); + WALSTAT_ACC(wal_fpi_bytes, wal_usage_diff); #undef WALSTAT_ACC /* @@ -264,7 +269,7 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref) * if some statistics could not be flushed due to lock contention. */ bool -pgstat_flush_backend(bool nowait, bits32 flags) +pgstat_flush_backend(bool nowait, uint32 flags) { PgStat_EntryRef *entry_ref; bool has_pending_data = false; @@ -301,18 +306,6 @@ pgstat_flush_backend(bool nowait, bits32 flags) return false; } -/* - * Check if there are any backend stats waiting for flush. - */ -bool -pgstat_backend_have_pending_cb(void) -{ - if (!pgstat_tracks_backend_bktype(MyBackendType)) - return false; - - return (backend_has_iostats || pgstat_backend_wal_have_pending()); -} - /* * Callback to flush out locally pending backend statistics. * @@ -334,7 +327,7 @@ pgstat_create_backend(ProcNumber procnum) PgStatShared_Backend *shstatent; entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_BACKEND, InvalidOid, - MyProcNumber, false); + procnum, false); shstatent = (PgStatShared_Backend *) entry_ref->shared_stats; /* @@ -388,6 +381,8 @@ pgstat_tracks_backend_bktype(BackendType bktype) case B_CHECKPOINTER: case B_IO_WORKER: case B_STARTUP: + case B_DATACHECKSUMSWORKER_LAUNCHER: + case B_DATACHECKSUMSWORKER_WORKER: return false; case B_AUTOVAC_WORKER: diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c index 41eabc1d8bb03..ed2fd8011894b 100644 --- a/src/backend/utils/activity/pgstat_bgwriter.c +++ b/src/backend/utils/activity/pgstat_bgwriter.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_bgwriter.c diff --git a/src/backend/utils/activity/pgstat_checkpointer.c b/src/backend/utils/activity/pgstat_checkpointer.c index e65034a30a638..1f70194b7a7f6 100644 --- a/src/backend/utils/activity/pgstat_checkpointer.c +++ b/src/backend/utils/activity/pgstat_checkpointer.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_checkpointer.c diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c index b31f20d41bcc5..7f3bc0165931c 100644 --- a/src/backend/utils/activity/pgstat_database.c +++ b/src/backend/utils/activity/pgstat_database.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_database.c @@ -17,7 +17,7 @@ #include "postgres.h" -#include "storage/procsignal.h" +#include "storage/standby.h" #include "utils/pgstat_internal.h" #include "utils/timestamp.h" @@ -88,31 +88,41 @@ pgstat_report_recovery_conflict(int reason) dbentry = pgstat_prep_database_pending(MyDatabaseId); - switch (reason) + switch ((RecoveryConflictReason) reason) { - case PROCSIG_RECOVERY_CONFLICT_DATABASE: + case RECOVERY_CONFLICT_DATABASE: /* * Since we drop the information about the database as soon as it * replicates, there is no point in counting these conflicts. */ break; - case PROCSIG_RECOVERY_CONFLICT_TABLESPACE: + case RECOVERY_CONFLICT_TABLESPACE: dbentry->conflict_tablespace++; break; - case PROCSIG_RECOVERY_CONFLICT_LOCK: + case RECOVERY_CONFLICT_LOCK: dbentry->conflict_lock++; break; - case PROCSIG_RECOVERY_CONFLICT_SNAPSHOT: + case RECOVERY_CONFLICT_SNAPSHOT: dbentry->conflict_snapshot++; break; - case PROCSIG_RECOVERY_CONFLICT_BUFFERPIN: + case RECOVERY_CONFLICT_BUFFERPIN: dbentry->conflict_bufferpin++; break; - case PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT: + case RECOVERY_CONFLICT_LOGICALSLOT: dbentry->conflict_logicalslot++; break; - case PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK: + case RECOVERY_CONFLICT_STARTUP_DEADLOCK: + dbentry->conflict_startup_deadlock++; + break; + case RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK: + + /* + * The difference between RECOVERY_CONFLICT_STARTUP_DEADLOCK and + * RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK is merely whether a buffer + * pin was part of the deadlock. We use the same counter for both + * reasons. + */ dbentry->conflict_startup_deadlock++; break; } @@ -190,7 +200,7 @@ pgstat_report_checksum_failures_in_db(Oid dboid, int failurecount) Assert(entry_ref); if (!entry_ref) { - elog(WARNING, "could not report %d conflicts for DB %u", + elog(WARNING, "could not report %d checksum failures for database %u", failurecount, dboid); return; } @@ -233,7 +243,7 @@ pgstat_report_connect(Oid dboid) pgLastSessionReportTime = MyStartTimestamp; - dbentry = pgstat_prep_database_pending(MyDatabaseId); + dbentry = pgstat_prep_database_pending(dboid); dbentry->sessions++; } @@ -248,7 +258,7 @@ pgstat_report_disconnect(Oid dboid) if (!pgstat_should_report_connstat()) return; - dbentry = pgstat_prep_database_pending(MyDatabaseId); + dbentry = pgstat_prep_database_pending(dboid); switch (pgStatSessionEndCause) { @@ -278,7 +288,7 @@ PgStat_StatDBEntry * pgstat_fetch_stat_dbentry(Oid dboid) { return (PgStat_StatDBEntry *) - pgstat_fetch_entry(PGSTAT_KIND_DATABASE, dboid, InvalidOid); + pgstat_fetch_entry(PGSTAT_KIND_DATABASE, dboid, InvalidOid, NULL); } void @@ -409,7 +419,7 @@ pgstat_reset_database_timestamp(Oid dboid, TimestampTz ts) PgStat_EntryRef *dbref; PgStatShared_Database *dbentry; - dbref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE, MyDatabaseId, InvalidOid, + dbref = pgstat_get_entry_ref_locked(PGSTAT_KIND_DATABASE, dboid, InvalidOid, false); dbentry = (PgStatShared_Database *) dbref->shared_stats; diff --git a/src/backend/utils/activity/pgstat_function.c b/src/backend/utils/activity/pgstat_function.c index 6214f93d36e0c..d47d05e3d922c 100644 --- a/src/backend/utils/activity/pgstat_function.c +++ b/src/backend/utils/activity/pgstat_function.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_function.c @@ -214,6 +214,12 @@ pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) return true; } +void +pgstat_function_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts) +{ + ((PgStatShared_Function *) header)->stats.stat_reset_timestamp = ts; +} + /* * find any existing PgStat_FunctionCounts entry for specified function * @@ -239,5 +245,5 @@ PgStat_StatFuncEntry * pgstat_fetch_stat_funcentry(Oid func_id) { return (PgStat_StatFuncEntry *) - pgstat_fetch_entry(PGSTAT_KIND_FUNCTION, MyDatabaseId, func_id); + pgstat_fetch_entry(PGSTAT_KIND_FUNCTION, MyDatabaseId, func_id, NULL); } diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c index d8d26379a571e..2be26e9228361 100644 --- a/src/backend/utils/activity/pgstat_io.c +++ b/src/backend/utils/activity/pgstat_io.c @@ -7,7 +7,7 @@ * from pgstat.c to enforce the line between the statistics access / storage * implementation and the details about individual types of statistics. * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_io.c @@ -80,6 +80,7 @@ pgstat_count_io_op(IOObject io_object, IOContext io_context, IOOp io_op, pgstat_count_backend_io_op(io_object, io_context, io_op, cnt, bytes); have_iostats = true; + pgstat_report_fixed = true; } /* @@ -167,15 +168,6 @@ pgstat_fetch_stat_io(void) return &pgStatLocal.snapshot.io; } -/* - * Check if there any IO stats waiting for flush. - */ -bool -pgstat_io_have_pending_cb(void) -{ - return have_iostats; -} - /* * Simpler wrapper of pgstat_io_flush_cb() */ @@ -370,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype) case B_LOGGER: return false; + case B_DATACHECKSUMSWORKER_LAUNCHER: + case B_DATACHECKSUMSWORKER_WORKER: case B_AUTOVAC_LAUNCHER: case B_AUTOVAC_WORKER: case B_BACKEND: diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c new file mode 100644 index 0000000000000..aec64f8fb4b63 --- /dev/null +++ b/src/backend/utils/activity/pgstat_lock.c @@ -0,0 +1,150 @@ +/* ------------------------------------------------------------------------- + * + * pgstat_lock.c + * Implementation of lock statistics. + * + * This file contains the implementation of lock statistics. It is kept + * separate from pgstat.c to enforce the line between the statistics + * access / storage implementation and the details about individual types + * of statistics. + * + * Copyright (c) 2021-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/utils/activity/pgstat_lock.c + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "utils/pgstat_internal.h" + +static PgStat_PendingLock PendingLockStats; +static bool have_lockstats = false; + +PgStat_Lock * +pgstat_fetch_stat_lock(void) +{ + pgstat_snapshot_fixed(PGSTAT_KIND_LOCK); + + return &pgStatLocal.snapshot.lock; +} + +/* + * Simpler wrapper of pgstat_lock_flush_cb() + */ +void +pgstat_lock_flush(bool nowait) +{ + (void) pgstat_lock_flush_cb(nowait); +} + +/* + * Flush out locally pending lock statistics + * + * If no stats have been recorded, this function returns false. + * + * If nowait is true, this function returns true if the lock could not be + * acquired. Otherwise, return false. + */ +bool +pgstat_lock_flush_cb(bool nowait) +{ + LWLock *lckstat_lock; + PgStatShared_Lock *shstats; + + if (!have_lockstats) + return false; + + shstats = &pgStatLocal.shmem->lock; + lckstat_lock = &shstats->lock; + + if (!nowait) + LWLockAcquire(lckstat_lock, LW_EXCLUSIVE); + else if (!LWLockConditionalAcquire(lckstat_lock, LW_EXCLUSIVE)) + return true; + + for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++) + { +#define LOCKSTAT_ACC(fld) \ + (shstats->stats.stats[i].fld += PendingLockStats.stats[i].fld) + LOCKSTAT_ACC(waits); + LOCKSTAT_ACC(wait_time); + LOCKSTAT_ACC(fastpath_exceeded); +#undef LOCKSTAT_ACC + } + + LWLockRelease(lckstat_lock); + + memset(&PendingLockStats, 0, sizeof(PendingLockStats)); + have_lockstats = false; + + return false; +} + +void +pgstat_lock_init_shmem_cb(void *stats) +{ + PgStatShared_Lock *stat_shmem = (PgStatShared_Lock *) stats; + + LWLockInitialize(&stat_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + +void +pgstat_lock_reset_all_cb(TimestampTz ts) +{ + LWLock *lckstat_lock = &pgStatLocal.shmem->lock.lock; + + LWLockAcquire(lckstat_lock, LW_EXCLUSIVE); + + pgStatLocal.shmem->lock.stats.stat_reset_timestamp = ts; + + memset(pgStatLocal.shmem->lock.stats.stats, 0, + sizeof(pgStatLocal.shmem->lock.stats.stats)); + + LWLockRelease(lckstat_lock); +} + +void +pgstat_lock_snapshot_cb(void) +{ + LWLock *lckstat_lock = &pgStatLocal.shmem->lock.lock; + + LWLockAcquire(lckstat_lock, LW_SHARED); + + pgStatLocal.snapshot.lock = pgStatLocal.shmem->lock.stats; + + LWLockRelease(lckstat_lock); +} + +/* + * Increment counter for lock not acquired with the fast-path, per lock + * type, due to the fast-path slot limit reached. + * + * Note: This function should not be called in performance-sensitive paths, + * like lock acquisitions. + */ +void +pgstat_count_lock_fastpath_exceeded(uint8 locktag_type) +{ + Assert(locktag_type <= LOCKTAG_LAST_TYPE); + PendingLockStats.stats[locktag_type].fastpath_exceeded++; + have_lockstats = true; + pgstat_report_fixed = true; +} + +/* + * Increment the number of waits and wait time, per lock type. + * + * Note: This function should not be called in performance-sensitive paths, + * like lock acquisitions. + */ +void +pgstat_count_lock_waits(uint8 locktag_type, long msecs) +{ + Assert(locktag_type <= LOCKTAG_LAST_TYPE); + PendingLockStats.stats[locktag_type].waits++; + PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs; + have_lockstats = true; + pgstat_report_fixed = true; +} diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index 28587e2916b1d..b2ca28f83ba8a 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -3,12 +3,12 @@ * pgstat_relation.c * Implementation of relation statistics. * - * This file contains the implementation of function relation. It is kept + * This file contains the implementation of relation statistics. It is kept * separate from pgstat.c to enforce the line between the statistics access / * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_relation.c @@ -61,7 +61,8 @@ pgstat_copy_relation_stats(Relation dst, Relation src) PgStat_EntryRef *dst_ref; srcstats = pgstat_fetch_stat_tabentry_ext(src->rd_rel->relisshared, - RelationGetRelid(src)); + RelationGetRelid(src), + NULL); if (!srcstats) return; @@ -207,14 +208,13 @@ pgstat_drop_relation(Relation rel) * Report that the table was just vacuumed and flush IO statistics. */ void -pgstat_report_vacuum(Oid tableoid, bool shared, - PgStat_Counter livetuples, PgStat_Counter deadtuples, - TimestampTz starttime) +pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples, + PgStat_Counter deadtuples, TimestampTz starttime) { PgStat_EntryRef *entry_ref; PgStatShared_Relation *shtabentry; PgStat_StatTabEntry *tabentry; - Oid dboid = (shared ? InvalidOid : MyDatabaseId); + Oid dboid = (rel->rd_rel->relisshared ? InvalidOid : MyDatabaseId); TimestampTz ts; PgStat_Counter elapsedtime; @@ -226,8 +226,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared, elapsedtime = TimestampDifferenceMilliseconds(starttime, ts); /* block acquiring lock for the same reason as pgstat_report_autovac() */ - entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, - dboid, tableoid, false); + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, dboid, + RelationGetRelid(rel), false); shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats; tabentry = &shtabentry->stats; @@ -469,20 +469,21 @@ pgstat_update_heap_dead_tuples(Relation rel, int delta) PgStat_StatTabEntry * pgstat_fetch_stat_tabentry(Oid relid) { - return pgstat_fetch_stat_tabentry_ext(IsSharedRelation(relid), relid); + return pgstat_fetch_stat_tabentry_ext(IsSharedRelation(relid), relid, NULL); } /* * More efficient version of pgstat_fetch_stat_tabentry(), allowing to specify - * whether the to-be-accessed table is a shared relation or not. + * whether the to-be-accessed table is a shared relation or not. This version + * also returns whether the caller can pfree() the result if desired. */ PgStat_StatTabEntry * -pgstat_fetch_stat_tabentry_ext(bool shared, Oid reloid) +pgstat_fetch_stat_tabentry_ext(bool shared, Oid reloid, bool *may_free) { Oid dboid = (shared ? InvalidOid : MyDatabaseId); return (PgStat_StatTabEntry *) - pgstat_fetch_entry(PGSTAT_KIND_RELATION, dboid, reloid); + pgstat_fetch_entry(PGSTAT_KIND_RELATION, dboid, reloid, may_free); } /* @@ -514,7 +515,7 @@ find_tabstat_entry(Oid rel_id) } tabentry = (PgStat_TableStatus *) entry_ref->pending; - tablestatus = palloc(sizeof(PgStat_TableStatus)); + tablestatus = palloc_object(PgStat_TableStatus); *tablestatus = *tabentry; /* @@ -744,7 +745,7 @@ PostPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state) * Load the saved counts into our local pgstats state. */ void -pgstat_twophase_postcommit(TransactionId xid, uint16 info, +pgstat_twophase_postcommit(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata; @@ -780,7 +781,7 @@ pgstat_twophase_postcommit(TransactionId xid, uint16 info, * as aborted. */ void -pgstat_twophase_postabort(TransactionId xid, uint16 info, +pgstat_twophase_postabort(FullTransactionId fxid, uint16 info, void *recdata, uint32 len) { TwoPhasePgStatRecord *rec = (TwoPhasePgStatRecord *) recdata; @@ -910,6 +911,12 @@ pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref) pgstat_unlink_relation(pending->relation); } +void +pgstat_relation_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts) +{ + ((PgStatShared_Relation *) header)->stats.stat_reset_time = ts; +} + /* * Find or create a PgStat_TableStatus entry for rel. New entry is created and * initialized if not exists. diff --git a/src/backend/utils/activity/pgstat_replslot.c b/src/backend/utils/activity/pgstat_replslot.c index ccfb11c49bf82..0d00dd5d93aa5 100644 --- a/src/backend/utils/activity/pgstat_replslot.c +++ b/src/backend/utils/activity/pgstat_replslot.c @@ -16,7 +16,7 @@ * dropped while shut down, which is addressed by not restoring stats for * slots that cannot be found by name when starting up. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_replslot.c @@ -47,9 +47,8 @@ pgstat_reset_replslot(const char *name) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - /* Check if the slot exits with the given name. */ + /* Check if the slot exists with the given name. */ slot = SearchNamedReplicationSlot(name, false); - if (!slot) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -94,6 +93,7 @@ pgstat_report_replslot(ReplicationSlot *slot, const PgStat_StatReplSlotEntry *re REPLSLOT_ACC(stream_txns); REPLSLOT_ACC(stream_count); REPLSLOT_ACC(stream_bytes); + REPLSLOT_ACC(mem_exceeded_count); REPLSLOT_ACC(total_txns); REPLSLOT_ACC(total_bytes); #undef REPLSLOT_ACC @@ -101,6 +101,36 @@ pgstat_report_replslot(ReplicationSlot *slot, const PgStat_StatReplSlotEntry *re pgstat_unlock_entry(entry_ref); } +/* + * Report replication slot sync skip statistics. + * + * Similar to pgstat_report_replslot(), we can rely on the stats for the + * slot to exist and to belong to this slot. + */ +void +pgstat_report_replslotsync(ReplicationSlot *slot) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_ReplSlot *shstatent; + PgStat_StatReplSlotEntry *statent; + + /* Slot sync stats are valid only for synced logical slots on standby. */ + Assert(slot->data.synced); + Assert(RecoveryInProgress()); + + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_REPLSLOT, InvalidOid, + ReplicationSlotIndex(slot), false); + Assert(entry_ref != NULL); + + shstatent = (PgStatShared_ReplSlot *) entry_ref->shared_stats; + statent = &shstatent->stats; + + statent->slotsync_skip_count += 1; + statent->slotsync_last_skip = GetCurrentTimestamp(); + + pgstat_unlock_entry(entry_ref); +} + /* * Report replication slot creation. * @@ -132,7 +162,7 @@ pgstat_create_replslot(ReplicationSlot *slot) * Report replication slot has been acquired. * * This guarantees that a stats entry exists during later - * pgstat_report_replslot() calls. + * pgstat_report_replslot() or pgstat_report_replslotsync() calls. * * If we previously crashed, no stats data exists. But if we did not crash, * the stats do belong to this slot: @@ -178,7 +208,8 @@ pgstat_fetch_replslot(NameData slotname) if (idx != -1) slotentry = (PgStat_StatReplSlotEntry *) pgstat_fetch_entry(PGSTAT_KIND_REPLSLOT, - InvalidOid, idx); + InvalidOid, idx, + NULL); LWLockRelease(ReplicationSlotControlLock); diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c index 2e33293b00097..b8f354c818a06 100644 --- a/src/backend/utils/activity/pgstat_shmem.c +++ b/src/backend/utils/activity/pgstat_shmem.c @@ -3,7 +3,7 @@ * pgstat_shmem.c * Storage of stats entries in shared memory * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_shmem.c @@ -14,6 +14,7 @@ #include "pgstat.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/memutils.h" #include "utils/pgstat_internal.h" @@ -57,6 +58,13 @@ static void pgstat_release_matching_entry_refs(bool discard_pending, ReleaseMatc static void pgstat_setup_memcxt(void); +static void StatsShmemRequest(void *arg); +static void StatsShmemInit(void *arg); + +const ShmemCallbacks StatsShmemCallbacks = { + .request_fn = StatsShmemRequest, + .init_fn = StatsShmemInit, +}; /* parameter for the shared hash */ static const dshash_parameters dsh_params = { @@ -123,7 +131,7 @@ pgstat_dsa_init_size(void) /* * Compute shared memory space needed for cumulative statistics */ -Size +static Size StatsShmemSize(void) { Size sz; @@ -142,84 +150,91 @@ StatsShmemSize(void) continue; Assert(kind_info->shared_size != 0); - - sz += MAXALIGN(kind_info->shared_size); + sz = add_size(sz, MAXALIGN(kind_info->shared_size)); } return sz; } +/* + * Register shared memory area for cumulative statistics + */ +static void +StatsShmemRequest(void *arg) +{ + ShmemRequestStruct(.name = "Shared Memory Stats", + .size = StatsShmemSize(), + .ptr = (void **) &pgStatLocal.shmem, + ); +} + /* * Initialize cumulative statistics system during startup */ -void -StatsShmemInit(void) +static void +StatsShmemInit(void *arg) { - bool found; - Size sz; + dsa_area *dsa; + dshash_table *dsh; + PgStat_ShmemControl *ctl = pgStatLocal.shmem; + char *p = (char *) ctl; - sz = StatsShmemSize(); - pgStatLocal.shmem = (PgStat_ShmemControl *) - ShmemInitStruct("Shared Memory Stats", sz, &found); + /* the allocation of pgStatLocal.shmem itself */ + p += MAXALIGN(sizeof(PgStat_ShmemControl)); - if (!IsUnderPostmaster) - { - dsa_area *dsa; - dshash_table *dsh; - PgStat_ShmemControl *ctl = pgStatLocal.shmem; - char *p = (char *) ctl; + /* + * Create a small dsa allocation in plain shared memory. This is required + * because postmaster cannot use dsm segments. It also provides a small + * efficiency win. + */ + ctl->raw_dsa_area = p; + p += pgstat_dsa_init_size(); + dsa = dsa_create_in_place(ctl->raw_dsa_area, + pgstat_dsa_init_size(), + LWTRANCHE_PGSTATS_DSA, NULL); + dsa_pin(dsa); - Assert(!found); + /* + * To ensure dshash is created in "plain" shared memory, temporarily limit + * size of dsa to the initial size of the dsa. + */ + dsa_set_size_limit(dsa, pgstat_dsa_init_size()); - /* the allocation of pgStatLocal.shmem itself */ - p += MAXALIGN(sizeof(PgStat_ShmemControl)); + /* + * With the limit in place, create the dshash table. XXX: It'd be nice if + * there were dshash_create_in_place(). + */ + dsh = dshash_create(dsa, &dsh_params, NULL); + ctl->hash_handle = dshash_get_hash_table_handle(dsh); - /* - * Create a small dsa allocation in plain shared memory. This is - * required because postmaster cannot use dsm segments. It also - * provides a small efficiency win. - */ - ctl->raw_dsa_area = p; - p += MAXALIGN(pgstat_dsa_init_size()); - dsa = dsa_create_in_place(ctl->raw_dsa_area, - pgstat_dsa_init_size(), - LWTRANCHE_PGSTATS_DSA, 0); - dsa_pin(dsa); + /* lift limit set above */ + dsa_set_size_limit(dsa, -1); - /* - * To ensure dshash is created in "plain" shared memory, temporarily - * limit size of dsa to the initial size of the dsa. - */ - dsa_set_size_limit(dsa, pgstat_dsa_init_size()); + /* + * Postmaster will never access these again, thus free the local + * dsa/dshash references. + */ + dshash_detach(dsh); + dsa_detach(dsa); - /* - * With the limit in place, create the dshash table. XXX: It'd be nice - * if there were dshash_create_in_place(). - */ - dsh = dshash_create(dsa, &dsh_params, NULL); - ctl->hash_handle = dshash_get_hash_table_handle(dsh); + pg_atomic_init_u64(&ctl->gc_request_count, 1); - /* lift limit set above */ - dsa_set_size_limit(dsa, -1); + /* Do the per-kind initialization */ + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + char *ptr; - /* - * Postmaster will never access these again, thus free the local - * dsa/dshash references. - */ - dshash_detach(dsh); - dsa_detach(dsa); + if (!kind_info) + continue; - pg_atomic_init_u64(&ctl->gc_request_count, 1); + /* initialize entry count tracking */ + if (kind_info->track_entry_count) + pg_atomic_init_u64(&ctl->entry_counts[kind - 1], 0); /* initialize fixed-numbered stats */ - for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + if (kind_info->fixed_amount) { - const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); - char *ptr; - - if (!kind_info || !kind_info->fixed_amount) - continue; - if (pgstat_is_kind_builtin(kind)) ptr = ((char *) ctl) + kind_info->shared_ctl_off; else @@ -227,17 +242,14 @@ StatsShmemInit(void) int idx = kind - PGSTAT_KIND_CUSTOM_MIN; Assert(kind_info->shared_size != 0); - ctl->custom_data[idx] = ShmemAlloc(kind_info->shared_size); + ctl->custom_data[idx] = p; + p += MAXALIGN(kind_info->shared_size); ptr = ctl->custom_data[idx]; } kind_info->init_shmem_cb(ptr); } } - else - { - Assert(found); - } } void @@ -255,7 +267,8 @@ pgstat_attach_shmem(void) dsa_pin_mapping(pgStatLocal.dsa); pgStatLocal.shared_hash = dshash_attach(pgStatLocal.dsa, &dsh_params, - pgStatLocal.shmem->hash_handle, 0); + pgStatLocal.shmem->hash_handle, + NULL); MemoryContextSwitchTo(oldcontext); } @@ -289,6 +302,13 @@ pgstat_detach_shmem(void) * ------------------------------------------------------------ */ +/* + * Initialize entry newly-created. + * + * Returns NULL in the event of an allocation failure, so as callers can + * take cleanup actions as the entry initialized is already inserted in the + * shared hashtable. + */ PgStatShared_Common * pgstat_init_entry(PgStat_Kind kind, PgStatShared_HashEntry *shhashent) @@ -296,6 +316,7 @@ pgstat_init_entry(PgStat_Kind kind, /* Create new stats entry. */ dsa_pointer chunk; PgStatShared_Common *shheader; + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); /* * Initialize refcount to 1, marking it as valid / not dropped. The entry @@ -311,13 +332,22 @@ pgstat_init_entry(PgStat_Kind kind, pg_atomic_init_u32(&shhashent->generation, 0); shhashent->dropped = false; - chunk = dsa_allocate0(pgStatLocal.dsa, pgstat_get_kind_info(kind)->shared_size); + chunk = dsa_allocate_extended(pgStatLocal.dsa, + kind_info->shared_size, + DSA_ALLOC_ZERO | DSA_ALLOC_NO_OOM); + if (chunk == InvalidDsaPointer) + return NULL; + shheader = dsa_get_address(pgStatLocal.dsa, chunk); shheader->magic = 0xdeadbeef; /* Link the new entry from the hash entry. */ shhashent->body = chunk; + /* Increment entry count, if required. */ + if (kind_info->track_entry_count) + pg_atomic_fetch_add_u64(&pgStatLocal.shmem->entry_counts[kind - 1], 1); + LWLockInitialize(&shheader->lock, LWTRANCHE_PGSTATS_DATA); return shheader; @@ -444,14 +474,11 @@ PgStat_EntryRef * pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64 objid, bool create, bool *created_entry) { - PgStat_HashKey key; + PgStat_HashKey key = {0}; PgStatShared_HashEntry *shhashent; PgStatShared_Common *shheader = NULL; PgStat_EntryRef *entry_ref; - /* clear padding */ - memset(&key, 0, sizeof(struct PgStat_HashKey)); - key.kind = kind; key.dboid = dboid; key.objid = objid; @@ -509,6 +536,20 @@ pgstat_get_entry_ref(PgStat_Kind kind, Oid dboid, uint64 objid, bool create, if (!shfound) { shheader = pgstat_init_entry(kind, shhashent); + if (shheader == NULL) + { + /* + * Failed the allocation of a new entry, so clean up the + * shared hashtable before giving up. + */ + dshash_delete_entry(pgStatLocal.shared_hash, shhashent); + + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Failed while allocating entry %u/%u/%" PRIu64 ".", + key.kind, key.dboid, key.objid))); + } pgstat_acquire_entry_ref(entry_ref, shhashent, shheader); if (created_entry != NULL) @@ -836,6 +877,7 @@ static void pgstat_free_entry(PgStatShared_HashEntry *shent, dshash_seq_status *hstat) { dsa_pointer pdsa; + PgStat_Kind kind = shent->key.kind; /* * Fetch dsa pointer before deleting entry - that way we can free the @@ -849,6 +891,10 @@ pgstat_free_entry(PgStatShared_HashEntry *shent, dshash_seq_status *hstat) dshash_delete_current(hstat); dsa_free(pgStatLocal.dsa, pdsa); + + /* Decrement entry count, if required. */ + if (pgstat_get_kind_info(kind)->track_entry_count) + pg_atomic_sub_fetch_u64(&pgStatLocal.shmem->entry_counts[kind - 1], 1); } /* @@ -873,11 +919,12 @@ pgstat_drop_entry_internal(PgStatShared_HashEntry *shent, */ if (shent->dropped) elog(ERROR, - "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u", + "trying to drop stats entry already dropped: kind=%s dboid=%u objid=%" PRIu64 " refcount=%u generation=%u", pgstat_get_kind_info(shent->key.kind)->name, shent->key.dboid, shent->key.objid, - pg_atomic_read_u32(&shent->refcount)); + pg_atomic_read_u32(&shent->refcount), + pg_atomic_read_u32(&shent->generation)); shent->dropped = true; /* release refcount marking entry as not dropped */ @@ -961,13 +1008,10 @@ pgstat_drop_database_and_contents(Oid dboid) bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid) { - PgStat_HashKey key; + PgStat_HashKey key = {0}; PgStatShared_HashEntry *shent; bool freed = true; - /* clear padding */ - memset(&key, 0, sizeof(struct PgStat_HashKey)); - key.kind = kind; key.dboid = dboid; key.objid = objid; diff --git a/src/backend/utils/activity/pgstat_slru.c b/src/backend/utils/activity/pgstat_slru.c index b9e940dde45b6..f4dfe8697d750 100644 --- a/src/backend/utils/activity/pgstat_slru.c +++ b/src/backend/utils/activity/pgstat_slru.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_slru.c @@ -55,47 +55,33 @@ pgstat_reset_slru(const char *name) * SLRU statistics count accumulation functions --- called from slru.c */ -void -pgstat_count_slru_page_zeroed(int slru_idx) -{ - get_slru_entry(slru_idx)->blocks_zeroed += 1; +#define PGSTAT_COUNT_SLRU(stat) \ +void \ +CppConcat(pgstat_count_slru_,stat)(int slru_idx) \ +{ \ + get_slru_entry(slru_idx)->stat += 1; \ } -void -pgstat_count_slru_page_hit(int slru_idx) -{ - get_slru_entry(slru_idx)->blocks_hit += 1; -} +/* pgstat_count_slru_blocks_zeroed */ +PGSTAT_COUNT_SLRU(blocks_zeroed) -void -pgstat_count_slru_page_exists(int slru_idx) -{ - get_slru_entry(slru_idx)->blocks_exists += 1; -} +/* pgstat_count_slru_blocks_hit */ +PGSTAT_COUNT_SLRU(blocks_hit) -void -pgstat_count_slru_page_read(int slru_idx) -{ - get_slru_entry(slru_idx)->blocks_read += 1; -} +/* pgstat_count_slru_blocks_exists */ +PGSTAT_COUNT_SLRU(blocks_exists) -void -pgstat_count_slru_page_written(int slru_idx) -{ - get_slru_entry(slru_idx)->blocks_written += 1; -} +/* pgstat_count_slru_blocks_read */ +PGSTAT_COUNT_SLRU(blocks_read) -void -pgstat_count_slru_flush(int slru_idx) -{ - get_slru_entry(slru_idx)->flush += 1; -} +/* pgstat_count_slru_blocks_written */ +PGSTAT_COUNT_SLRU(blocks_written) -void -pgstat_count_slru_truncate(int slru_idx) -{ - get_slru_entry(slru_idx)->truncate += 1; -} +/* pgstat_count_slru_flush */ +PGSTAT_COUNT_SLRU(flush) + +/* pgstat_count_slru_truncate */ +PGSTAT_COUNT_SLRU(truncate) /* * Support function for the SQL-callable pgstat* functions. Returns @@ -133,6 +119,7 @@ pgstat_get_slru_index(const char *name) { int i; + Assert(name); for (i = 0; i < SLRU_NUM_ELEMENTS; i++) { if (strcmp(slru_names[i], name) == 0) @@ -143,15 +130,6 @@ pgstat_get_slru_index(const char *name) return (SLRU_NUM_ELEMENTS - 1); } -/* - * Check if there are any SLRU stats entries waiting for flush. - */ -bool -pgstat_slru_have_pending_cb(void) -{ - return have_slrustats; -} - /* * Flush out locally pending SLRU stats entries * @@ -247,6 +225,7 @@ get_slru_entry(int slru_idx) Assert((slru_idx >= 0) && (slru_idx < SLRU_NUM_ELEMENTS)); have_slrustats = true; + pgstat_report_fixed = true; return &pending_SLRUStats[slru_idx]; } diff --git a/src/backend/utils/activity/pgstat_subscription.c b/src/backend/utils/activity/pgstat_subscription.c index f9a1c831a07e6..3eaf3e0390fc0 100644 --- a/src/backend/utils/activity/pgstat_subscription.c +++ b/src/backend/utils/activity/pgstat_subscription.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_subscription.c @@ -17,6 +17,7 @@ #include "postgres.h" +#include "replication/worker_internal.h" #include "utils/pgstat_internal.h" @@ -24,19 +25,35 @@ * Report a subscription error. */ void -pgstat_report_subscription_error(Oid subid, bool is_apply_error) +pgstat_report_subscription_error(Oid subid) { PgStat_EntryRef *entry_ref; PgStat_BackendSubEntry *pending; + LogicalRepWorkerType wtype = get_logical_worker_type(); entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_SUBSCRIPTION, InvalidOid, subid, NULL); pending = entry_ref->pending; - if (is_apply_error) - pending->apply_error_count++; - else - pending->sync_error_count++; + switch (wtype) + { + case WORKERTYPE_APPLY: + pending->apply_error_count++; + break; + + case WORKERTYPE_SEQUENCESYNC: + pending->sync_seq_error_count++; + break; + + case WORKERTYPE_TABLESYNC: + pending->sync_table_error_count++; + break; + + default: + /* Should never happen. */ + Assert(0); + break; + } } /* @@ -90,7 +107,7 @@ PgStat_StatSubEntry * pgstat_fetch_stat_subscription(Oid subid) { return (PgStat_StatSubEntry *) - pgstat_fetch_entry(PGSTAT_KIND_SUBSCRIPTION, InvalidOid, subid); + pgstat_fetch_entry(PGSTAT_KIND_SUBSCRIPTION, InvalidOid, subid, NULL); } /* @@ -115,7 +132,8 @@ pgstat_subscription_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) #define SUB_ACC(fld) shsubent->stats.fld += localent->fld SUB_ACC(apply_error_count); - SUB_ACC(sync_error_count); + SUB_ACC(sync_seq_error_count); + SUB_ACC(sync_table_error_count); for (int i = 0; i < CONFLICT_NUM_TYPES; i++) SUB_ACC(conflict_count[i]); #undef SUB_ACC diff --git a/src/backend/utils/activity/pgstat_wal.c b/src/backend/utils/activity/pgstat_wal.c index 16a1ecb4d90d2..183e0a7a97bfb 100644 --- a/src/backend/utils/activity/pgstat_wal.c +++ b/src/backend/utils/activity/pgstat_wal.c @@ -8,7 +8,7 @@ * storage implementation and the details about individual types of * statistics. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_wal.c @@ -71,6 +71,15 @@ pgstat_fetch_stat_wal(void) return &pgStatLocal.snapshot.wal; } +/* + * To determine whether WAL usage happened. + */ +static inline bool +pgstat_wal_have_pending(void) +{ + return pgWalUsage.wal_records != prevWalUsage.wal_records; +} + /* * Calculate how much WAL usage counters have increased by subtracting the * previous counters from the current ones. @@ -92,7 +101,7 @@ pgstat_wal_flush_cb(bool nowait) * This function can be called even if nothing at all has happened. Avoid * taking lock for nothing in that case. */ - if (!pgstat_wal_have_pending_cb()) + if (!pgstat_wal_have_pending()) return false; /* @@ -112,6 +121,7 @@ pgstat_wal_flush_cb(bool nowait) WALSTAT_ACC(wal_records, wal_usage_diff); WALSTAT_ACC(wal_fpi, wal_usage_diff); WALSTAT_ACC(wal_bytes, wal_usage_diff); + WALSTAT_ACC(wal_fpi_bytes, wal_usage_diff); WALSTAT_ACC(wal_buffers_full, wal_usage_diff); #undef WALSTAT_ACC @@ -136,15 +146,6 @@ pgstat_wal_init_backend_cb(void) prevWalUsage = pgWalUsage; } -/* - * To determine whether WAL usage happened. - */ -bool -pgstat_wal_have_pending_cb(void) -{ - return pgWalUsage.wal_records != prevWalUsage.wal_records; -} - void pgstat_wal_init_shmem_cb(void *stats) { diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c index bc9864bd8d9d0..5e2d69e629794 100644 --- a/src/backend/utils/activity/pgstat_xact.c +++ b/src/backend/utils/activity/pgstat_xact.c @@ -3,7 +3,7 @@ * pgstat_xact.c * Transactional integration for the cumulative statistics system. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/activity/pgstat_xact.c diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c index d9b8f34a3559d..95635c7f56ce7 100644 --- a/src/backend/utils/activity/wait_event.c +++ b/src/backend/utils/activity/wait_event.c @@ -2,7 +2,7 @@ * wait_event.c * Wait event reporting infrastructure. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -22,14 +22,16 @@ */ #include "postgres.h" -#include "storage/lmgr.h" /* for GetLockNameFromTagType */ -#include "storage/lwlock.h" /* for GetLWLockIdentifier */ +#include "storage/lmgr.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "storage/subsystems.h" #include "storage/spin.h" #include "utils/wait_event.h" static const char *pgstat_get_wait_activity(WaitEventActivity w); -static const char *pgstat_get_wait_bufferpin(WaitEventBufferPin w); +static const char *pgstat_get_wait_buffer(WaitEventBuffer w); static const char *pgstat_get_wait_client(WaitEventClient w); static const char *pgstat_get_wait_ipc(WaitEventIPC w); static const char *pgstat_get_wait_timeout(WaitEventTimeout w); @@ -55,16 +57,14 @@ uint32 *my_wait_event_info = &local_my_wait_event_info; * For simplicity, we use the same ID counter across types of custom events. * We could end that anytime the need arises. * - * The size of the hash table is based on the assumption that - * WAIT_EVENT_CUSTOM_HASH_INIT_SIZE is enough for most cases, and it seems - * unlikely that the number of entries will reach - * WAIT_EVENT_CUSTOM_HASH_MAX_SIZE. + * The size of the hash table is based on the assumption that usually only a + * handful of entries are needed, but since it's small in absolute terms + * anyway, we leave a generous amount of headroom. */ static HTAB *WaitEventCustomHashByInfo; /* find names from infos */ static HTAB *WaitEventCustomHashByName; /* find infos from names */ -#define WAIT_EVENT_CUSTOM_HASH_INIT_SIZE 16 -#define WAIT_EVENT_CUSTOM_HASH_MAX_SIZE 128 +#define WAIT_EVENT_CUSTOM_HASH_SIZE 128 /* hash table entries */ typedef struct WaitEventCustomEntryByInfo @@ -96,61 +96,47 @@ static WaitEventCustomCounterData *WaitEventCustomCounter; static uint32 WaitEventCustomNew(uint32 classId, const char *wait_event_name); static const char *GetWaitEventCustomIdentifier(uint32 wait_event_info); +static void WaitEventCustomShmemRequest(void *arg); +static void WaitEventCustomShmemInit(void *arg); + +const ShmemCallbacks WaitEventCustomShmemCallbacks = { + .request_fn = WaitEventCustomShmemRequest, + .init_fn = WaitEventCustomShmemInit, +}; + /* - * Return the space for dynamic shared hash tables and dynamic allocation counter. + * Register shmem space for dynamic shared hash and dynamic allocation counter. */ -Size -WaitEventCustomShmemSize(void) +static void +WaitEventCustomShmemRequest(void *arg) { - Size sz; - - sz = MAXALIGN(sizeof(WaitEventCustomCounterData)); - sz = add_size(sz, hash_estimate_size(WAIT_EVENT_CUSTOM_HASH_MAX_SIZE, - sizeof(WaitEventCustomEntryByInfo))); - sz = add_size(sz, hash_estimate_size(WAIT_EVENT_CUSTOM_HASH_MAX_SIZE, - sizeof(WaitEventCustomEntryByName))); - return sz; + ShmemRequestStruct(.name = "WaitEventCustomCounterData", + .size = sizeof(WaitEventCustomCounterData), + .ptr = (void **) &WaitEventCustomCounter, + ); + ShmemRequestHash(.name = "WaitEventCustom hash by wait event information", + .ptr = &WaitEventCustomHashByInfo, + .nelems = WAIT_EVENT_CUSTOM_HASH_SIZE, + .hash_info.keysize = sizeof(uint32), + .hash_info.entrysize = sizeof(WaitEventCustomEntryByInfo), + .hash_flags = HASH_ELEM | HASH_BLOBS, + ); + ShmemRequestHash(.name = "WaitEventCustom hash by name", + .ptr = &WaitEventCustomHashByName, + .nelems = WAIT_EVENT_CUSTOM_HASH_SIZE, + /* key is a NULL-terminated string */ + .hash_info.keysize = sizeof(char[NAMEDATALEN]), + .hash_info.entrysize = sizeof(WaitEventCustomEntryByName), + .hash_flags = HASH_ELEM | HASH_STRINGS, + ); } -/* - * Allocate shmem space for dynamic shared hash and dynamic allocation counter. - */ -void -WaitEventCustomShmemInit(void) +static void +WaitEventCustomShmemInit(void *arg) { - bool found; - HASHCTL info; - - WaitEventCustomCounter = (WaitEventCustomCounterData *) - ShmemInitStruct("WaitEventCustomCounterData", - sizeof(WaitEventCustomCounterData), &found); - - if (!found) - { - /* initialize the allocation counter and its spinlock. */ - WaitEventCustomCounter->nextId = WAIT_EVENT_CUSTOM_INITIAL_ID; - SpinLockInit(&WaitEventCustomCounter->mutex); - } - - /* initialize or attach the hash tables to store custom wait events */ - info.keysize = sizeof(uint32); - info.entrysize = sizeof(WaitEventCustomEntryByInfo); - WaitEventCustomHashByInfo = - ShmemInitHash("WaitEventCustom hash by wait event information", - WAIT_EVENT_CUSTOM_HASH_INIT_SIZE, - WAIT_EVENT_CUSTOM_HASH_MAX_SIZE, - &info, - HASH_ELEM | HASH_BLOBS); - - /* key is a NULL-terminated string */ - info.keysize = sizeof(char[NAMEDATALEN]); - info.entrysize = sizeof(WaitEventCustomEntryByName); - WaitEventCustomHashByName = - ShmemInitHash("WaitEventCustom hash by name", - WAIT_EVENT_CUSTOM_HASH_INIT_SIZE, - WAIT_EVENT_CUSTOM_HASH_MAX_SIZE, - &info, - HASH_ELEM | HASH_STRINGS); + /* initialize the allocation counter and its spinlock. */ + WaitEventCustomCounter->nextId = WAIT_EVENT_CUSTOM_INITIAL_ID; + SpinLockInit(&WaitEventCustomCounter->mutex); } /* @@ -237,7 +223,7 @@ WaitEventCustomNew(uint32 classId, const char *wait_event_name) /* Allocate a new event Id */ SpinLockAcquire(&WaitEventCustomCounter->mutex); - if (WaitEventCustomCounter->nextId >= WAIT_EVENT_CUSTOM_HASH_MAX_SIZE) + if (WaitEventCustomCounter->nextId >= WAIT_EVENT_CUSTOM_HASH_SIZE) { SpinLockRelease(&WaitEventCustomCounter->mutex); ereport(ERROR, @@ -317,7 +303,7 @@ GetWaitEventCustomNames(uint32 classId, int *nwaitevents) els = hash_get_num_entries(WaitEventCustomHashByName); /* Allocate enough space for all entries */ - waiteventnames = palloc(els * sizeof(char *)); + waiteventnames = palloc_array(char *, els); /* Now scan the hash table to copy the data */ hash_seq_init(&hash_seq, WaitEventCustomHashByName); @@ -389,8 +375,8 @@ pgstat_get_wait_event_type(uint32 wait_event_info) case PG_WAIT_LOCK: event_type = "Lock"; break; - case PG_WAIT_BUFFERPIN: - event_type = "BufferPin"; + case PG_WAIT_BUFFER: + event_type = "Buffer"; break; case PG_WAIT_ACTIVITY: event_type = "Activity"; @@ -453,11 +439,11 @@ pgstat_get_wait_event(uint32 wait_event_info) case PG_WAIT_INJECTIONPOINT: event_name = GetWaitEventCustomIdentifier(wait_event_info); break; - case PG_WAIT_BUFFERPIN: + case PG_WAIT_BUFFER: { - WaitEventBufferPin w = (WaitEventBufferPin) wait_event_info; + WaitEventBuffer w = (WaitEventBuffer) wait_event_info; - event_name = pgstat_get_wait_bufferpin(w); + event_name = pgstat_get_wait_buffer(w); break; } case PG_WAIT_ACTIVITY: @@ -503,4 +489,4 @@ pgstat_get_wait_event(uint32 wait_event_info) return event_name; } -#include "pgstat_wait_event.c" +#include "utils/pgstat_wait_event.c" diff --git a/src/backend/utils/activity/wait_event_funcs.c b/src/backend/utils/activity/wait_event_funcs.c index ffbb57a80780f..ff683ae8c5ddf 100644 --- a/src/backend/utils/activity/wait_event_funcs.c +++ b/src/backend/utils/activity/wait_event_funcs.c @@ -3,7 +3,7 @@ * wait_event_funcs.c * Functions for accessing wait event data. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,7 @@ #include "funcapi.h" #include "utils/builtins.h" +#include "utils/tuplestore.h" #include "utils/wait_event.h" /* @@ -31,7 +32,7 @@ static const struct waitEventData[] = { -#include "wait_event_funcs_data.c" +#include "utils/wait_event_funcs_data.c" /* end of list */ {NULL, NULL, NULL} }; diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 5d9e04d682377..560659f956856 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -2,7 +2,7 @@ # wait_event_names.txt # PostgreSQL wait events # -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # This list serves as the basis for generating source and documentation files # related to wait events. @@ -14,13 +14,13 @@ # # The files generated from this one are: # -# src/backend/utils/activity/wait_event_types.h +# wait_event_types.h # typedef enum definitions for wait events. # -# src/backend/utils/activity/pgstat_wait_event.c +# pgstat_wait_event.c # C functions to get the wait event name based on the enum. # -# src/backend/utils/activity/wait_event_types.sgml +# wait_event_types.sgml # SGML tables of wait events for inclusion in the documentation. # # When adding a new wait event, make sure it is placed in the appropriate @@ -62,7 +62,7 @@ LOGICAL_APPLY_MAIN "Waiting in main loop of logical replication apply process." LOGICAL_LAUNCHER_MAIN "Waiting in main loop of logical replication launcher process." LOGICAL_PARALLEL_APPLY_MAIN "Waiting in main loop of logical replication parallel apply process." RECOVERY_WAL_STREAM "Waiting in main loop of startup process for WAL to arrive, during streaming recovery." -REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot sync worker." +REPLICATION_SLOTSYNC_MAIN "Waiting in main loop of slot synchronization." REPLICATION_SLOTSYNC_SHUTDOWN "Waiting for slot sync worker to shut down." SYSLOGGER_MAIN "Waiting in main loop of syslogger process." WAL_RECEIVER_MAIN "Waiting in main loop of WAL receiver process." @@ -89,6 +89,9 @@ LIBPQWALRECEIVER_CONNECT "Waiting in WAL receiver to establish connection to rem LIBPQWALRECEIVER_RECEIVE "Waiting in WAL receiver to receive data from remote server." SSL_OPEN_SERVER "Waiting for SSL while attempting connection." WAIT_FOR_STANDBY_CONFIRMATION "Waiting for WAL to be received and flushed by the physical standby." +WAIT_FOR_WAL_FLUSH "Waiting for WAL flush to reach a target LSN on a primary or standby." +WAIT_FOR_WAL_REPLAY "Waiting for WAL replay to reach a target LSN on a standby." +WAIT_FOR_WAL_WRITE "Waiting for WAL write to reach a target LSN on a standby." WAL_SENDER_WAIT_FOR_WAL "Waiting for WAL to be flushed in WAL sender process." WAL_SENDER_WRITE_DATA "Waiting for any activity when processing replies from WAL receiver in WAL sender process." @@ -116,6 +119,8 @@ CHECKPOINT_DELAY_COMPLETE "Waiting for a backend that blocks a checkpoint from c CHECKPOINT_DELAY_START "Waiting for a backend that blocks a checkpoint from starting." CHECKPOINT_DONE "Waiting for a checkpoint to complete." CHECKPOINT_START "Waiting for a checkpoint to start." +CHECKSUM_ENABLE_STARTCONDITION "Waiting for data checksums enabling to start." +CHECKSUM_ENABLE_TEMPTABLE_WAIT "Waiting for temporary tables to be dropped for data checksums to be enabled." EXECUTE_GATHER "Waiting for activity from a child process while executing a Gather plan node." HASH_BATCH_ALLOCATE "Waiting for an elected Parallel Hash participant to allocate a hash table." HASH_BATCH_ELECT "Waiting to elect a Parallel Hash participant to allocate a hash table." @@ -151,12 +156,12 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace." RECOVERY_END_COMMAND "Waiting for to complete." RECOVERY_PAUSE "Waiting for recovery to be resumed." +REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file." REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped." REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped." RESTORE_COMMAND "Waiting for to complete." SAFE_SNAPSHOT "Waiting to obtain a valid snapshot for a READ ONLY DEFERRABLE transaction." SYNC_REP "Waiting for confirmation from a remote server during synchronous replication." -WAL_BUFFER_INIT "Waiting on WAL buffer to be initialized." WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." @@ -174,6 +179,7 @@ Section: ClassName - WaitEventTimeout BASE_BACKUP_THROTTLE "Waiting during base backup when throttling activity." CHECKPOINT_WRITE_DELAY "Waiting between writes while performing a checkpoint." +COMMIT_DELAY "Waiting for commit delay before WAL flush." PG_SLEEP "Waiting due to a call to pg_sleep or a sibling function." RECOVERY_APPLY_DELAY "Waiting to apply WAL during recovery because of a delay setting." RECOVERY_RETRIEVE_RETRY_INTERVAL "Waiting during recovery when WAL data is not available from any source (pg_wal, archive or stream)." @@ -210,6 +216,8 @@ CONTROL_FILE_WRITE_UPDATE "Waiting for a write to update the pg_contro COPY_FILE_COPY "Waiting for a file copy operation." COPY_FILE_READ "Waiting for a read during a file copy operation." COPY_FILE_WRITE "Waiting for a write during a file copy operation." +COPY_FROM_READ "Waiting to read data from a pipe, a file or a program during COPY FROM." +COPY_TO_WRITE "Waiting to write data to a pipe, a file or a program during COPY TO." DATA_FILE_EXTEND "Waiting for a relation data file to be extended." DATA_FILE_FLUSH "Waiting for a relation data file to reach durable storage." DATA_FILE_IMMEDIATE_SYNC "Waiting for an immediate synchronization of a relation data file to durable storage." @@ -278,12 +286,15 @@ WAL_WRITE "Waiting for a write to a WAL file." ABI_compatibility: # -# Wait Events - Buffer Pin +# Wait Events - Buffer # -Section: ClassName - WaitEventBufferPin +Section: ClassName - WaitEventBuffer -BUFFER_PIN "Waiting to acquire an exclusive pin on a buffer." +BUFFER_CLEANUP "Waiting to acquire an exclusive pin on a buffer. Buffer pin waits can be protracted if another process holds an open cursor that last read data from the buffer in question." +BUFFER_SHARED "Waiting to acquire a shared lock on a buffer." +BUFFER_SHARE_EXCLUSIVE "Waiting to acquire a share exclusive lock on a buffer." +BUFFER_EXCLUSIVE "Waiting to acquire a exclusive lock on a buffer." ABI_compatibility: @@ -303,19 +314,22 @@ ABI_compatibility: # This class of wait events has its own set of C structure, so these are # only used for the documentation. # -# NB: Predefined LWLocks (i.e., those declared in lwlocklist.h) must be -# listed in the top section of locks and must be listed in the same order as in -# lwlocklist.h. +# NB: Predefined LWLocks (i.e., those declared with PG_LWLOCK in lwlocklist.h) +# must be listed before the "END OF PREDEFINED LWLOCKS" comment and must be +# listed in the same order as in lwlocklist.h. Likewise, the built-in LWLock +# tranches (i.e., those declared with PG_LWLOCKTRANCHE in lwlocklist.h) must be +# listed after the "END OF PREDEFINED LWLOCKS" comment and must be listed in +# the same order as lwlocklist.h. # Section: ClassName - WaitEventLWLock -ShmemIndex "Waiting to find or allocate space in shared memory." OidGen "Waiting to allocate a new OID." XidGen "Waiting to allocate a new transaction ID." ProcArray "Waiting to access the shared per-process data structures (typically, to get a snapshot or report a session's transaction ID)." SInvalRead "Waiting to retrieve messages from the shared catalog invalidation queue." SInvalWrite "Waiting to add a message to the shared catalog invalidation queue." +WALBufMapping "Waiting to replace a page in WAL buffers." WALWrite "Waiting for WAL buffers to be written to disk." ControlFile "Waiting to read or update the pg_control file or create a new WAL file." MultiXactGen "Waiting to read or update shared multixact state." @@ -352,14 +366,14 @@ DSMRegistry "Waiting to read or update the dynamic shared memory registry." InjectionPoint "Waiting to read or update information related to injection points." SerialControl "Waiting to read or update shared pg_serial state." AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." +WaitLSN "Waiting to read or update shared Wait-for-LSN state." +LogicalDecodingControl "Waiting to read or update logical decoding status information." +DataChecksumsWorker "Waiting for data checksums worker." +AioWorkerControl "Waiting to update AIO worker information." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) # -# Predefined LWLocks (i.e., those declared in lwlocknames.h) must be listed -# in the section above and must be listed in the same order as in -# lwlocknames.h. Other LWLocks must be listed in the section below. -# XactBuffer "Waiting for I/O on a transaction status SLRU buffer." CommitTsBuffer "Waiting for I/O on a commit timestamp SLRU buffer." @@ -367,9 +381,9 @@ SubtransBuffer "Waiting for I/O on a sub-transaction SLRU buffer." MultiXactOffsetBuffer "Waiting for I/O on a multixact offset SLRU buffer." MultiXactMemberBuffer "Waiting for I/O on a multixact member SLRU buffer." NotifyBuffer "Waiting for I/O on a NOTIFY message SLRU buffer." +NotifyChannelHash "Waiting to access the NOTIFY channel hash table." SerialBuffer "Waiting for I/O on a serializable transaction conflict SLRU buffer." WALInsert "Waiting to insert WAL data into a memory buffer." -BufferContent "Waiting to access a data page in memory." ReplicationOriginState "Waiting to read or update the progress of one replication origin." ReplicationSlotIO "Waiting for I/O on a replication slot." LockFastPath "Waiting to read or update a process' fast-path lock information." @@ -401,6 +415,8 @@ SerialSLRU "Waiting to access the serializable transaction conflict SLRU cache." SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." +AioUringCompletion "Waiting for another process to complete IO via io_uring." +ShmemIndex "Waiting to find or allocate space in shared memory." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 4a233b63c3280..0c7621957c183 100644 --- a/src/backend/utils/adt/Makefile +++ b/src/backend/utils/adt/Makefile @@ -23,6 +23,7 @@ OBJS = \ arrayutils.o \ ascii.o \ bool.o \ + bytea.o \ cash.o \ char.o \ cryptohashfuncs.o \ @@ -30,6 +31,7 @@ OBJS = \ datetime.o \ datum.o \ dbsize.o \ + ddlutils.o \ domains.o \ encode.o \ enum.o \ @@ -67,6 +69,7 @@ OBJS = \ misc.o \ multirangetypes.o \ multirangetypes_selfuncs.o \ + multixactfuncs.o \ name.o \ network.o \ network_gist.o \ @@ -75,14 +78,17 @@ OBJS = \ numeric.o \ numutils.o \ oid.o \ + oid8.o \ oracle_compat.o \ orderedsetaggs.o \ partitionfuncs.o \ + pg_dependencies.o \ pg_locale.o \ pg_locale_builtin.o \ pg_locale_icu.o \ pg_locale_libc.o \ pg_lsn.o \ + pg_ndistinct.o \ pg_upgrade_support.o \ pgstatfuncs.o \ pseudorandomfuncs.o \ diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index ca3c5ee3df3ae..01caa12eca705 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -3,7 +3,7 @@ * acl.c * Basic access control list data structures manipulation routines. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include #include "access/htup_details.h" +#include "bootstrap/bootstrap.h" #include "catalog/catalog.h" #include "catalog/namespace.h" #include "catalog/pg_auth_members.h" @@ -31,7 +32,6 @@ #include "catalog/pg_proc.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "commands/proclang.h" #include "commands/tablespace.h" #include "common/hashfn.h" @@ -40,6 +40,7 @@ #include "lib/bloomfilter.h" #include "lib/qunique.h" #include "miscadmin.h" +#include "port/pg_bitutils.h" #include "storage/large_object.h" #include "utils/acl.h" #include "utils/array.h" @@ -131,9 +132,26 @@ static AclMode convert_largeobject_priv_string(text *priv_type_text); static AclMode convert_role_priv_string(text *priv_type_text); static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); -static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); +static void RoleMembershipCacheCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); +/* + * Test whether an identifier char can be left unquoted in ACLs. + * + * Formerly, we used isalnum() even on non-ASCII characters, resulting in + * unportable behavior. To ensure dump compatibility with old versions, + * we now treat high-bit-set characters as always requiring quoting during + * putid(), but getid() will always accept them without quotes. + */ +static inline bool +is_safe_acl_char(unsigned char c, bool is_getid) +{ + if (IS_HIGHBIT_SET(c)) + return is_getid; + return isalnum(c) || c == '_'; +} + /* * getid * Consumes the first alphanumeric string (identifier) found in string @@ -159,21 +177,22 @@ getid(const char *s, char *n, Node *escontext) while (isspace((unsigned char) *s)) s++; - /* This code had better match what putid() does, below */ for (; *s != '\0' && - (isalnum((unsigned char) *s) || - *s == '_' || - *s == '"' || - in_quotes); + (in_quotes || *s == '"' || is_safe_acl_char(*s, true)); s++) { if (*s == '"') { + if (!in_quotes) + { + in_quotes = true; + continue; + } /* safe to look at next char (could be '\0' though) */ if (*(s + 1) != '"') { - in_quotes = !in_quotes; + in_quotes = false; continue; } /* it's an escaped double quote; skip the escaping char */ @@ -207,10 +226,10 @@ putid(char *p, const char *s) const char *src; bool safe = true; + /* Detect whether we need to use double quotes */ for (src = s; *src; src++) { - /* This test had better match what getid() does, above */ - if (!isalnum((unsigned char) *src) && *src != '_') + if (!is_safe_acl_char(*src, false)) { safe = false; break; @@ -244,6 +263,9 @@ putid(char *p, const char *s) * This routine is called by the parser as well as aclitemin(), hence * the added generality. * + * In bootstrap mode, we consult a hard-wired list of role names + * (see bootstrap.c) rather than trying to access the catalogs. + * * RETURNS: * the string position in 's' immediately following the ACL * specification. Also: @@ -359,7 +381,10 @@ aclparse(const char *s, AclItem *aip, Node *escontext) aip->ai_grantee = ACL_ID_PUBLIC; else { - aip->ai_grantee = get_role_oid(name, true); + if (IsBootstrapProcessingMode()) + aip->ai_grantee = boot_get_role_oid(name); + else + aip->ai_grantee = get_role_oid(name, true); if (!OidIsValid(aip->ai_grantee)) ereturn(escontext, NULL, (errcode(ERRCODE_UNDEFINED_OBJECT), @@ -368,7 +393,8 @@ aclparse(const char *s, AclItem *aip, Node *escontext) /* * XXX Allow a degree of backward compatibility by defaulting the grantor - * to the superuser. + * to the superuser. We condone that practice in the catalog .dat files + * (i.e., in bootstrap mode) for brevity; otherwise, issue a warning. */ if (*s == '/') { @@ -379,7 +405,10 @@ aclparse(const char *s, AclItem *aip, Node *escontext) ereturn(escontext, NULL, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("a name must follow the \"/\" sign"))); - aip->ai_grantor = get_role_oid(name2, true); + if (IsBootstrapProcessingMode()) + aip->ai_grantor = boot_get_role_oid(name2); + else + aip->ai_grantor = get_role_oid(name2, true); if (!OidIsValid(aip->ai_grantor)) ereturn(escontext, NULL, (errcode(ERRCODE_UNDEFINED_OBJECT), @@ -388,10 +417,11 @@ aclparse(const char *s, AclItem *aip, Node *escontext) else { aip->ai_grantor = BOOTSTRAP_SUPERUSERID; - ereport(WARNING, - (errcode(ERRCODE_INVALID_GRANTOR), - errmsg("defaulting grantor to user ID %u", - BOOTSTRAP_SUPERUSERID))); + if (!IsBootstrapProcessingMode()) + ereport(WARNING, + (errcode(ERRCODE_INVALID_GRANTOR), + errmsg("defaulting grantor to user ID %u", + BOOTSTRAP_SUPERUSERID))); } ACLITEM_SET_PRIVS_GOPTIONS(*aip, privs, goption); @@ -602,7 +632,7 @@ aclitemin(PG_FUNCTION_ARGS) Node *escontext = fcinfo->context; AclItem *aip; - aip = (AclItem *) palloc(sizeof(AclItem)); + aip = palloc_object(AclItem); s = aclparse(s, aip, escontext); if (s == NULL) @@ -623,6 +653,10 @@ aclitemin(PG_FUNCTION_ARGS) * Allocates storage for, and fills in, a new null-delimited string * containing a formatted ACL specification. See aclparse for details. * + * In bootstrap mode, this is called for debug printouts (initdb -d). + * We could ask bootstrap.c to provide an inverse of boot_get_role_oid(), + * but it seems at least as useful to just print numeric role OIDs. + * * RETURNS: * the new string */ @@ -645,7 +679,10 @@ aclitemout(PG_FUNCTION_ARGS) if (aip->ai_grantee != ACL_ID_PUBLIC) { - htup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(aip->ai_grantee)); + if (!IsBootstrapProcessingMode()) + htup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(aip->ai_grantee)); + else + htup = NULL; if (HeapTupleIsValid(htup)) { putid(p, NameStr(((Form_pg_authid) GETSTRUCT(htup))->rolname)); @@ -653,7 +690,7 @@ aclitemout(PG_FUNCTION_ARGS) } else { - /* Generate numeric OID if we don't find an entry */ + /* No such entry, or bootstrap mode: print numeric OID */ sprintf(p, "%u", aip->ai_grantee); } } @@ -673,7 +710,10 @@ aclitemout(PG_FUNCTION_ARGS) *p++ = '/'; *p = '\0'; - htup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(aip->ai_grantor)); + if (!IsBootstrapProcessingMode()) + htup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(aip->ai_grantor)); + else + htup = NULL; if (HeapTupleIsValid(htup)) { putid(p, NameStr(((Form_pg_authid) GETSTRUCT(htup))->rolname)); @@ -681,7 +721,7 @@ aclitemout(PG_FUNCTION_ARGS) } else { - /* Generate numeric OID if we don't find an entry */ + /* No such entry, or bootstrap mode: print numeric OID */ sprintf(p, "%u", aip->ai_grantor); } @@ -851,6 +891,10 @@ acldefault(ObjectType objtype, Oid ownerId) world_default = ACL_NO_RIGHTS; owner_default = ACL_ALL_RIGHTS_PARAMETER_ACL; break; + case OBJECT_PROPGRAPH: + world_default = ACL_NO_RIGHTS; + owner_default = ACL_ALL_RIGHTS_PROPGRAPH; + break; default: elog(ERROR, "unrecognized object type: %d", (int) objtype); world_default = ACL_NO_RIGHTS; /* keep compiler quiet */ @@ -1645,7 +1689,7 @@ makeaclitem(PG_FUNCTION_ARGS) priv = convert_any_priv_string(privtext, any_priv_map); - result = (AclItem *) palloc(sizeof(AclItem)); + result = palloc_object(AclItem); result->ai_grantee = grantee; result->ai_grantor = grantor; @@ -1802,10 +1846,11 @@ aclexplode(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 4, "is_grantable", BOOLOID, -1, 0); + TupleDescFinalize(tupdesc); funcctx->tuple_desc = BlessTupleDesc(tupdesc); /* allocate memory for user context */ - idx = (int *) palloc(sizeof(int[2])); + idx = palloc_array(int, 2); idx[0] = 0; /* ACL array item index */ idx[1] = -1; /* privilege type counter */ funcctx->user_fctx = idx; @@ -5051,7 +5096,8 @@ initialize_acl(void) * Syscache inval callback function */ static void -RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +RoleMembershipCacheCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { if (cacheid == DATABASEOID && hashvalue != cached_db_hash && @@ -5143,7 +5189,7 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type, MemoryContext oldctx; bloom_filter *bf = NULL; - Assert(OidIsValid(admin_of) == PointerIsValid(admin_role)); + Assert(OidIsValid(admin_of) == (admin_role != NULL)); if (admin_role != NULL) *admin_role = InvalidOid; @@ -5435,6 +5481,10 @@ select_best_admin(Oid member, Oid role) /* * Select the effective grantor ID for a GRANT or REVOKE operation. * + * If the GRANT/REVOKE has an explicit GRANTED BY clause, we always use + * exactly that role (which may result in granting/revoking no privileges). + * Otherwise, we seek a "best" grantor, starting with the current user. + * * The grantor must always be either the object owner or some role that has * been explicitly granted grant options. This ensures that all granted * privileges appear to flow from the object owner, and there are never @@ -5447,25 +5497,44 @@ select_best_admin(Oid member, Oid role) * role has 'em all. In this case we pick a role with the largest number * of desired options. Ties are broken in favor of closer ancestors. * - * roleId: the role attempting to do the GRANT/REVOKE + * grantedBy: the GRANTED BY clause of GRANT/REVOKE, or NULL if none * privileges: the privileges to be granted/revoked * acl: the ACL of the object in question * ownerId: the role owning the object in question * *grantorId: receives the OID of the role to do the grant as - * *grantOptions: receives the grant options actually held by grantorId - * - * If no grant options exist, we set grantorId to roleId, grantOptions to 0. + * *grantOptions: receives grant options actually held by grantorId (maybe 0) */ void -select_best_grantor(Oid roleId, AclMode privileges, +select_best_grantor(const RoleSpec *grantedBy, AclMode privileges, const Acl *acl, Oid ownerId, Oid *grantorId, AclMode *grantOptions) { + Oid roleId = GetUserId(); AclMode needed_goptions = ACL_GRANT_OPTION_FOR(privileges); List *roles_list; int nrights; ListCell *l; + /* + * If we have GRANTED BY, resolve it and verify current user is allowed to + * specify that role. + */ + if (grantedBy) + { + Oid grantor = get_rolespec_oid(grantedBy, false); + + if (!has_privs_of_role(roleId, grantor)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must inherit privileges of role \"%s\"", + GetUserNameFromId(grantor, false)))); + /* Use exactly that grantor, whether it has privileges or not */ + *grantorId = grantor; + *grantOptions = aclmask_direct(acl, grantor, ownerId, + needed_goptions, ACLMASK_ALL); + return; + } + /* * The object owner is always treated as having all grant options, so if * roleId is the owner it's easy. Also, if roleId is a superuser it's diff --git a/src/backend/utils/adt/amutils.c b/src/backend/utils/adt/amutils.c index 0af26d6acfab8..c81fb61a06a5e 100644 --- a/src/backend/utils/adt/amutils.c +++ b/src/backend/utils/adt/amutils.c @@ -3,7 +3,7 @@ * amutils.c * SQL-level APIs related to index access methods. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -156,7 +156,7 @@ indexam_property(FunctionCallInfo fcinfo, bool isnull = false; int natts = 0; IndexAMProperty prop; - IndexAmRoutine *routine; + const IndexAmRoutine *routine; /* Try to convert property name to enum (no error if not known) */ prop = lookup_prop_name(propname); @@ -452,7 +452,7 @@ pg_indexam_progress_phasename(PG_FUNCTION_ARGS) { Oid amoid = PG_GETARG_OID(0); int32 phasenum = PG_GETARG_INT32(1); - IndexAmRoutine *routine; + const IndexAmRoutine *routine; char *name; routine = GetIndexAmRoutineByAmId(amoid, true); diff --git a/src/backend/utils/adt/array_expanded.c b/src/backend/utils/adt/array_expanded.c index fc036d1eb3007..7e8352af52b29 100644 --- a/src/backend/utils/adt/array_expanded.c +++ b/src/backend/utils/adt/array_expanded.c @@ -3,7 +3,7 @@ * array_expanded.c * Basic functions for manipulating expanded arrays. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -238,6 +238,7 @@ EA_get_flat_size(ExpandedObjectHeader *eohptr) Datum *dvalues; bool *dnulls; Size nbytes; + uint8 typalignby; int i; Assert(eah->ea_magic == EA_MAGIC); @@ -261,18 +262,19 @@ EA_get_flat_size(ExpandedObjectHeader *eohptr) dvalues = eah->dvalues; dnulls = eah->dnulls; nbytes = 0; + typalignby = typalign_to_alignby(eah->typalign); for (i = 0; i < nelems; i++) { if (dnulls && dnulls[i]) continue; nbytes = att_addlength_datum(nbytes, eah->typlen, dvalues[i]); - nbytes = att_align_nominal(nbytes, eah->typalign); + nbytes = att_nominal_alignby(nbytes, typalignby); /* check for overflow of total request */ if (!AllocSizeIsValid(nbytes)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); } if (dnulls) diff --git a/src/backend/utils/adt/array_selfuncs.c b/src/backend/utils/adt/array_selfuncs.c index a69a84c2aee33..f2793e4bfd6ed 100644 --- a/src/backend/utils/adt/array_selfuncs.c +++ b/src/backend/utils/adt/array_selfuncs.c @@ -3,7 +3,7 @@ * array_selfuncs.c * Functions for selectivity estimation of array operators * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -39,25 +39,25 @@ static Selectivity calc_arraycontsel(VariableStatData *vardata, Datum constval, Oid elemtype, Oid operator); -static Selectivity mcelem_array_selec(ArrayType *array, +static Selectivity mcelem_array_selec(const ArrayType *array, TypeCacheEntry *typentry, - Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers, - float4 *hist, int nhist, + const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers, + const float4 *hist, int nhist, Oid operator); -static Selectivity mcelem_array_contain_overlap_selec(Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers, - Datum *array_data, int nitems, +static Selectivity mcelem_array_contain_overlap_selec(const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers, + const Datum *array_data, int nitems, Oid operator, TypeCacheEntry *typentry); -static Selectivity mcelem_array_contained_selec(Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers, - Datum *array_data, int nitems, - float4 *hist, int nhist, +static Selectivity mcelem_array_contained_selec(const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers, + const Datum *array_data, int nitems, + const float4 *hist, int nhist, Oid operator, TypeCacheEntry *typentry); static float *calc_hist(const float4 *hist, int nhist, int n); static float *calc_distr(const float *p, int n, int m, float rest); static int floor_log2(uint32 n); -static bool find_next_mcelem(Datum *mcelem, int nmcelem, Datum value, +static bool find_next_mcelem(const Datum *mcelem, int nmcelem, Datum value, int *index, TypeCacheEntry *typentry); static int element_compare(const void *key1, const void *key2, void *arg); static int float_compare_desc(const void *key1, const void *key2); @@ -425,10 +425,10 @@ calc_arraycontsel(VariableStatData *vardata, Datum constval, * mcelem_array_contained_selec depending on the operator. */ static Selectivity -mcelem_array_selec(ArrayType *array, TypeCacheEntry *typentry, - Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers, - float4 *hist, int nhist, +mcelem_array_selec(const ArrayType *array, TypeCacheEntry *typentry, + const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers, + const float4 *hist, int nhist, Oid operator) { Selectivity selec; @@ -518,9 +518,9 @@ mcelem_array_selec(ArrayType *array, TypeCacheEntry *typentry, * fraction of nonempty arrays in the column. */ static Selectivity -mcelem_array_contain_overlap_selec(Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers, - Datum *array_data, int nitems, +mcelem_array_contain_overlap_selec(const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers, + const Datum *array_data, int nitems, Oid operator, TypeCacheEntry *typentry) { Selectivity selec, @@ -544,12 +544,15 @@ mcelem_array_contain_overlap_selec(Datum *mcelem, int nmcelem, if (numbers) { - /* Grab the lowest observed frequency */ + /* Grab the minimal MCE frequency */ minfreq = numbers[nmcelem]; } else { - /* Without statistics make some default assumptions */ + /* + * Without statistics, use DEFAULT_CONTAIN_SEL (the factor of 2 will + * be removed again below). + */ minfreq = 2 * (float4) DEFAULT_CONTAIN_SEL; } @@ -621,8 +624,11 @@ mcelem_array_contain_overlap_selec(Datum *mcelem, int nmcelem, else { /* - * The element is not in MCELEM. Punt, but assume that the - * selectivity cannot be more than minfreq / 2. + * The element is not in MCELEM. Estimate its frequency as half + * that of the least-frequent MCE. (We know it cannot be more + * than minfreq, and it could be a great deal less. Half seems + * like a good compromise.) For probably-historical reasons, + * clamp to not more than DEFAULT_CONTAIN_SEL. */ elem_selec = Min(DEFAULT_CONTAIN_SEL, minfreq / 2); } @@ -693,10 +699,10 @@ mcelem_array_contain_overlap_selec(Datum *mcelem, int nmcelem, * ... * fn^on * (1 - fn)^(1 - on), o1, o2, ..., on) | o1 + o2 + .. on = m */ static Selectivity -mcelem_array_contained_selec(Datum *mcelem, int nmcelem, - float4 *numbers, int nnumbers, - Datum *array_data, int nitems, - float4 *hist, int nhist, +mcelem_array_contained_selec(const Datum *mcelem, int nmcelem, + const float4 *numbers, int nnumbers, + const Datum *array_data, int nitems, + const float4 *hist, int nhist, Oid operator, TypeCacheEntry *typentry) { int mcelem_index, @@ -728,7 +734,7 @@ mcelem_array_contained_selec(Datum *mcelem, int nmcelem, /* * Grab some of the summary statistics that compute_array_stats() stores: - * lowest frequency, frequency of null elements, and average distinct + * lowest MCE frequency, frequency of null elements, and average distinct * element count. */ minfreq = numbers[nmcelem]; @@ -753,7 +759,7 @@ mcelem_array_contained_selec(Datum *mcelem, int nmcelem, * elem_selec is array of estimated frequencies for elements in the * constant. */ - elem_selec = (float *) palloc(sizeof(float) * nitems); + elem_selec = palloc_array(float, nitems); /* Scan mcelem and array in parallel. */ mcelem_index = 0; @@ -802,8 +808,11 @@ mcelem_array_contained_selec(Datum *mcelem, int nmcelem, else { /* - * The element is not in MCELEM. Punt, but assume that the - * selectivity cannot be more than minfreq / 2. + * The element is not in MCELEM. Estimate its frequency as half + * that of the least-frequent MCE. (We know it cannot be more + * than minfreq, and it could be a great deal less. Half seems + * like a good compromise.) For probably-historical reasons, + * clamp to not more than DEFAULT_CONTAIN_SEL. */ elem_selec[unique_nitems] = Min(DEFAULT_CONTAIN_SEL, minfreq / 2); @@ -927,7 +936,7 @@ calc_hist(const float4 *hist, int nhist, int n) next_interval; float frac; - hist_part = (float *) palloc((n + 1) * sizeof(float)); + hist_part = palloc_array(float, n + 1); /* * frac is a probability contribution for each interval between histogram @@ -1019,8 +1028,8 @@ calc_distr(const float *p, int n, int m, float rest) * Since we return only the last row of the matrix and need only the * current and previous row for calculations, allocate two rows. */ - row = (float *) palloc((m + 1) * sizeof(float)); - prev_row = (float *) palloc((m + 1) * sizeof(float)); + row = palloc_array(float, m + 1); + prev_row = palloc_array(float, m + 1); /* M[0,0] = 1 */ row[0] = 1.0f; @@ -1127,7 +1136,7 @@ floor_log2(uint32 n) * exact match.) */ static bool -find_next_mcelem(Datum *mcelem, int nmcelem, Datum value, int *index, +find_next_mcelem(const Datum *mcelem, int nmcelem, Datum value, int *index, TypeCacheEntry *typentry) { int l = *index, diff --git a/src/backend/utils/adt/array_typanalyze.c b/src/backend/utils/adt/array_typanalyze.c index 6f61629b9778d..7bb000ddbd343 100644 --- a/src/backend/utils/adt/array_typanalyze.c +++ b/src/backend/utils/adt/array_typanalyze.c @@ -3,7 +3,7 @@ * array_typanalyze.c * Functions for gathering statistics from array columns * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -132,7 +132,7 @@ array_typanalyze(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); /* Store our findings for use by compute_array_stats() */ - extra_data = (ArrayAnalyzeExtraData *) palloc(sizeof(ArrayAnalyzeExtraData)); + extra_data = palloc_object(ArrayAnalyzeExtraData); extra_data->type_id = typentry->type_id; extra_data->eq_opr = typentry->eq_opr; extra_data->coll_id = stats->attrcollid; /* collation we should use */ @@ -461,7 +461,7 @@ compute_array_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, /* * Construct an array of the interesting hashtable items, that is, * those meeting the cutoff frequency (s - epsilon)*N. Also identify - * the minimum and maximum frequencies among these items. + * the maximum frequency among these items. * * Since epsilon = s/10 and bucket_width = 1/epsilon, the cutoff * frequency is 9*N / bucket_width. @@ -469,18 +469,16 @@ compute_array_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, cutoff_freq = 9 * element_no / bucket_width; i = hash_get_num_entries(elements_tab); /* surely enough space */ - sort_table = (TrackItem **) palloc(sizeof(TrackItem *) * i); + sort_table = palloc_array(TrackItem *, i); hash_seq_init(&scan_status, elements_tab); track_len = 0; - minfreq = element_no; maxfreq = 0; while ((item = (TrackItem *) hash_seq_search(&scan_status)) != NULL) { if (item->frequency > cutoff_freq) { sort_table[track_len++] = item; - minfreq = Min(minfreq, item->frequency); maxfreq = Max(maxfreq, item->frequency); } } @@ -497,19 +495,38 @@ compute_array_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, * If we obtained more elements than we really want, get rid of those * with least frequencies. The easiest way is to qsort the array into * descending frequency order and truncate the array. + * + * If we did not find more elements than we want, then it is safe to + * assume that the stored MCE array will contain every element with + * frequency above the cutoff. In that case, rather than storing the + * smallest frequency we are keeping, we want to store the minimum + * frequency that would have been accepted as a valid MCE. The + * selectivity functions can assume that that is an upper bound on the + * frequency of elements not present in the array. + * + * If we found no candidate MCEs at all, we still want to record the + * cutoff frequency, since it's still valid to assume that no element + * has frequency more than that. */ if (num_mcelem < track_len) { qsort_interruptible(sort_table, track_len, sizeof(TrackItem *), trackitem_compare_frequencies_desc, NULL); - /* reset minfreq to the smallest frequency we're keeping */ + /* set minfreq to the smallest frequency we're keeping */ minfreq = sort_table[num_mcelem - 1]->frequency; } else + { num_mcelem = track_len; + /* set minfreq to the minimum frequency above the cutoff */ + minfreq = cutoff_freq + 1; + /* ensure maxfreq is nonzero, too */ + if (track_len == 0) + maxfreq = minfreq; + } /* Generate MCELEM slot entry */ - if (num_mcelem > 0) + if (num_mcelem >= 0) { MemoryContext old_context; Datum *mcelem_values; @@ -589,8 +606,7 @@ compute_array_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, * Create an array of DECountItem pointers, and sort them into * increasing count order. */ - sorted_count_items = (DECountItem **) - palloc(sizeof(DECountItem *) * count_items_count); + sorted_count_items = palloc_array(DECountItem *, count_items_count); hash_seq_init(&scan_status, count_tab); j = 0; while ((count_item = (DECountItem *) hash_seq_search(&scan_status)) != NULL) diff --git a/src/backend/utils/adt/array_userfuncs.c b/src/backend/utils/adt/array_userfuncs.c index 8eb342e33823a..9d7f67cc188e8 100644 --- a/src/backend/utils/adt/array_userfuncs.c +++ b/src/backend/utils/adt/array_userfuncs.c @@ -3,7 +3,7 @@ * array_userfuncs.c * Misc user-visible array support functions * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/array_userfuncs.c @@ -24,6 +24,7 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/tuplesort.h" #include "utils/typcache.h" @@ -337,7 +338,7 @@ array_cat(PG_FUNCTION_ARGS) int i; char *dat1, *dat2; - bits8 *bitmap1, + uint8 *bitmap1, *bitmap2; Oid element_type; Oid element_type1; @@ -433,8 +434,8 @@ array_cat(PG_FUNCTION_ARGS) * themselves) of the input argument arrays */ ndims = ndims1; - dims = (int *) palloc(ndims * sizeof(int)); - lbs = (int *) palloc(ndims * sizeof(int)); + dims = palloc_array(int, ndims); + lbs = palloc_array(int, ndims); dims[0] = dims1[0] + dims2[0]; lbs[0] = lbs1[0]; @@ -459,8 +460,8 @@ array_cat(PG_FUNCTION_ARGS) * the first argument inserted at the front of the outer dimension */ ndims = ndims2; - dims = (int *) palloc(ndims * sizeof(int)); - lbs = (int *) palloc(ndims * sizeof(int)); + dims = palloc_array(int, ndims); + lbs = palloc_array(int, ndims); memcpy(dims, dims2, ndims * sizeof(int)); memcpy(lbs, lbs2, ndims * sizeof(int)); @@ -487,8 +488,8 @@ array_cat(PG_FUNCTION_ARGS) * second argument appended to the end of the outer dimension */ ndims = ndims1; - dims = (int *) palloc(ndims * sizeof(int)); - lbs = (int *) palloc(ndims * sizeof(int)); + dims = palloc_array(int, ndims); + lbs = palloc_array(int, ndims); memcpy(dims, dims1, ndims * sizeof(int)); memcpy(lbs, lbs1, ndims * sizeof(int)); @@ -1013,7 +1014,7 @@ array_agg_array_combine(PG_FUNCTION_ARGS) { int size = (state2->aitems + 7) / 8; - state1->nullbitmap = (bits8 *) palloc(size); + state1->nullbitmap = (uint8 *) palloc(size); memcpy(state1->nullbitmap, state2->nullbitmap, size); } @@ -1033,10 +1034,11 @@ array_agg_array_combine(PG_FUNCTION_ARGS) } /* We only need to combine the two states if state2 has any items */ - else if (state2->nitems > 0) + if (state2->nitems > 0) { MemoryContext oldContext; - int reqsize = state1->nbytes + state2->nbytes; + int reqsize; + int newnitems; int i; /* @@ -1059,6 +1061,17 @@ array_agg_array_combine(PG_FUNCTION_ARGS) errmsg("cannot accumulate arrays of different dimensionality"))); } + /* Types should match already. */ + Assert(state1->array_type == state2->array_type); + Assert(state1->element_type == state2->element_type); + + /* Calculate new sizes, guarding against overflow. */ + if (pg_add_s32_overflow(state1->nbytes, state2->nbytes, &reqsize) || + pg_add_s32_overflow(state1->nitems, state2->nitems, &newnitems)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); oldContext = MemoryContextSwitchTo(state1->mcontext); @@ -1073,35 +1086,34 @@ array_agg_array_combine(PG_FUNCTION_ARGS) state1->data = (char *) repalloc(state1->data, state1->abytes); } - if (state2->nullbitmap) + /* Combine the null bitmaps, if present. */ + if (state1->nullbitmap || state2->nullbitmap) { - int newnitems = state1->nitems + state2->nitems; - if (state1->nullbitmap == NULL) { /* * First input with nulls; we must retrospectively handle any * previous inputs by marking all their items non-null. */ - state1->aitems = pg_nextpower2_32(Max(256, newnitems + 1)); - state1->nullbitmap = (bits8 *) palloc((state1->aitems + 7) / 8); + state1->aitems = pg_nextpower2_32(Max(256, newnitems)); + state1->nullbitmap = (uint8 *) palloc((state1->aitems + 7) / 8); array_bitmap_copy(state1->nullbitmap, 0, NULL, 0, state1->nitems); } else if (newnitems > state1->aitems) { - int newaitems = state1->aitems + state2->aitems; - - state1->aitems = pg_nextpower2_32(newaitems); - state1->nullbitmap = (bits8 *) + state1->aitems = pg_nextpower2_32(newnitems); + state1->nullbitmap = (uint8 *) repalloc(state1->nullbitmap, (state1->aitems + 7) / 8); } + /* This will do the right thing if state2->nullbitmap is NULL: */ array_bitmap_copy(state1->nullbitmap, state1->nitems, state2->nullbitmap, 0, state2->nitems); } + /* Finally, combine the data and adjust sizes. */ memcpy(state1->data + state1->nbytes, state2->data, state2->nbytes); state1->nbytes += state2->nbytes; state1->nitems += state2->nitems; @@ -1109,9 +1121,6 @@ array_agg_array_combine(PG_FUNCTION_ARGS) state1->dims[0] += state2->dims[0]; /* remaining dims already match, per test above */ - Assert(state1->array_type == state2->array_type); - Assert(state1->element_type == state2->element_type); - MemoryContextSwitchTo(oldContext); } @@ -1238,7 +1247,7 @@ array_agg_array_deserialize(PG_FUNCTION_ARGS) { int size = (result->aitems + 7) / 8; - result->nullbitmap = (bits8 *) palloc(size); + result->nullbitmap = (uint8 *) palloc(size); temp = pq_getmsgbytes(&buf, size); memcpy(result->nullbitmap, temp, size); } diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c index c8f53c6fbe788..2933a95c4a59c 100644 --- a/src/backend/utils/adt/arrayfuncs.c +++ b/src/backend/utils/adt/arrayfuncs.c @@ -3,7 +3,7 @@ * arrayfuncs.c * Support functions for arrays. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include #include +#include "access/transam.h" #include "catalog/pg_type.h" #include "common/int.h" #include "funcapi.h" @@ -69,11 +70,12 @@ typedef struct ArrayIteratorData { /* basic info about the array, set up during array_create_iterator() */ ArrayType *arr; /* array we're iterating through */ - bits8 *nullbitmap; /* its null bitmap, if any */ + uint8 *nullbitmap; /* its null bitmap, if any */ int nitems; /* total number of elements in array */ int16 typlen; /* element type's length */ bool typbyval; /* element type's byval property */ char typalign; /* element type's align property */ + uint8 typalignby; /* typalign mapped to numeric alignment */ /* information about the requested slice size */ int slice_ndim; /* slice dimension, or 0 if not slicing */ @@ -86,7 +88,7 @@ typedef struct ArrayIteratorData /* current position information, updated on each iteration */ char *data_ptr; /* our current position in the array */ int current_item; /* the item # we're at in the array */ -} ArrayIteratorData; +} ArrayIteratorData; static bool ReadArrayDimensions(char **srcptr, int *ndim_p, int *dim, int *lBound, @@ -118,26 +120,26 @@ static Datum array_set_element_expanded(Datum arraydatum, Datum dataValue, bool isNull, int arraytyplen, int elmlen, bool elmbyval, char elmalign); -static bool array_get_isnull(const bits8 *nullbitmap, int offset); -static void array_set_isnull(bits8 *nullbitmap, int offset, bool isNull); +static bool array_get_isnull(const uint8 *nullbitmap, int offset); +static void array_set_isnull(uint8 *nullbitmap, int offset, bool isNull); static Datum ArrayCast(char *value, bool byval, int len); static int ArrayCastAndSet(Datum src, - int typlen, bool typbyval, char typalign, + int typlen, bool typbyval, uint8 typalignby, char *dest); -static char *array_seek(char *ptr, int offset, bits8 *nullbitmap, int nitems, +static char *array_seek(char *ptr, int offset, uint8 *nullbitmap, int nitems, int typlen, bool typbyval, char typalign); -static int array_nelems_size(char *ptr, int offset, bits8 *nullbitmap, +static int array_nelems_size(char *ptr, int offset, uint8 *nullbitmap, int nitems, int typlen, bool typbyval, char typalign); static int array_copy(char *destptr, int nitems, - char *srcptr, int offset, bits8 *nullbitmap, + char *srcptr, int offset, uint8 *nullbitmap, int typlen, bool typbyval, char typalign); -static int array_slice_size(char *arraydataptr, bits8 *arraynullsptr, +static int array_slice_size(char *arraydataptr, uint8 *arraynullsptr, int ndim, int *dim, int *lb, int *st, int *endp, int typlen, bool typbyval, char typalign); static void array_extract_slice(ArrayType *newarray, int ndim, int *dim, int *lb, - char *arraydataptr, bits8 *arraynullsptr, + char *arraydataptr, uint8 *arraynullsptr, int *st, int *endp, int typlen, bool typbyval, char typalign); static void array_insert_slice(ArrayType *destArray, ArrayType *origArray, @@ -186,6 +188,7 @@ array_in(PG_FUNCTION_ARGS) int typlen; bool typbyval; char typalign; + uint8 typalignby; char typdelim; Oid typioparam; char *p; @@ -231,6 +234,7 @@ array_in(PG_FUNCTION_ARGS) typlen = my_extra->typlen; typbyval = my_extra->typbyval; typalign = my_extra->typalign; + typalignby = typalign_to_alignby(typalign); typdelim = my_extra->typdelim; typioparam = my_extra->typioparam; @@ -327,13 +331,13 @@ array_in(PG_FUNCTION_ARGS) if (typlen == -1) values[i] = PointerGetDatum(PG_DETOAST_DATUM(values[i])); nbytes = att_addlength_datum(nbytes, typlen, values[i]); - nbytes = att_align_nominal(nbytes, typalign); + nbytes = att_nominal_alignby(nbytes, typalignby); /* check for overflow of total request */ if (!AllocSizeIsValid(nbytes)) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); } } if (hasnulls) @@ -491,8 +495,8 @@ ReadArrayDimensions(char **srcptr, int *ndim_p, int *dim, int *lBound, pg_add_s32_overflow(ub, 1, &ub)) ereturn(escontext, false, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); dim[ndim] = ub; ndim++; @@ -724,8 +728,8 @@ ReadArrayStr(char **srcptr, if (maxitems >= MaxArraySize) ereturn(escontext, false, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); maxitems = Min(maxitems * 2, MaxArraySize); values = repalloc_array(values, Datum, maxitems); nulls = repalloc_array(nulls, bool, maxitems); @@ -959,8 +963,8 @@ ReadArrayToken(char **srcptr, StringInfo elembuf, char typdelim, */ void CopyArrayEls(ArrayType *array, - Datum *values, - bool *nulls, + const Datum *values, + const bool *nulls, int nitems, int typlen, bool typbyval, @@ -968,9 +972,10 @@ CopyArrayEls(ArrayType *array, bool freedata) { char *p = ARR_DATA_PTR(array); - bits8 *bitmap = ARR_NULLBITMAP(array); + uint8 *bitmap = ARR_NULLBITMAP(array); int bitval = 0; int bitmask = 1; + uint8 typalignby = typalign_to_alignby(typalign); int i; if (typbyval) @@ -987,7 +992,7 @@ CopyArrayEls(ArrayType *array, else { bitval |= bitmask; - p += ArrayCastAndSet(values[i], typlen, typbyval, typalign, p); + p += ArrayCastAndSet(values[i], typlen, typbyval, typalignby, p); if (freedata) pfree(DatumGetPointer(values[i])); } @@ -1111,7 +1116,7 @@ array_out(PG_FUNCTION_ARGS) needquotes = (bool *) palloc(nitems * sizeof(bool)); overall_length = 0; - array_iter_setup(&iter, v); + array_iter_setup(&iter, v, typlen, typbyval, typalign); for (i = 0; i < nitems; i++) { @@ -1120,8 +1125,7 @@ array_out(PG_FUNCTION_ARGS) bool needquote; /* Get source element, checking for NULL */ - itemvalue = array_iter_next(&iter, &isnull, i, - typlen, typbyval, typalign); + itemvalue = array_iter_next(&iter, &isnull, i); if (isnull) { @@ -1467,6 +1471,7 @@ ReadArrayBinary(StringInfo buf, int i; bool hasnull; int32 totbytes; + uint8 typalignby = typalign_to_alignby(typalign); for (i = 0; i < nitems; i++) { @@ -1525,13 +1530,13 @@ ReadArrayBinary(StringInfo buf, if (typlen == -1) values[i] = PointerGetDatum(PG_DETOAST_DATUM(values[i])); totbytes = att_addlength_datum(totbytes, typlen, values[i]); - totbytes = att_align_nominal(totbytes, typalign); + totbytes = att_nominal_alignby(totbytes, typalignby); /* check for overflow of total request */ if (!AllocSizeIsValid(totbytes)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); } } *hasnulls = hasnull; @@ -1613,7 +1618,7 @@ array_send(PG_FUNCTION_ARGS) } /* Send the array elements using the element's own sendproc */ - array_iter_setup(&iter, v); + array_iter_setup(&iter, v, typlen, typbyval, typalign); for (i = 0; i < nitems; i++) { @@ -1621,8 +1626,7 @@ array_send(PG_FUNCTION_ARGS) bool isnull; /* Get source element, checking for NULL */ - itemvalue = array_iter_next(&iter, &isnull, i, - typlen, typbyval, typalign); + itemvalue = array_iter_next(&iter, &isnull, i); if (isnull) { @@ -1835,7 +1839,7 @@ array_get_element(Datum arraydatum, fixedLb[1]; char *arraydataptr, *retptr; - bits8 *arraynullsptr; + uint8 *arraynullsptr; if (arraytyplen > 0) { @@ -2049,7 +2053,7 @@ array_get_slice(Datum arraydatum, fixedLb[1]; Oid elemtype; char *arraydataptr; - bits8 *arraynullsptr; + uint8 *arraynullsptr; int32 dataoffset; int bytes, span[MAXDIM]; @@ -2217,7 +2221,7 @@ array_set_element(Datum arraydatum, offset; char *elt_ptr; bool newhasnulls; - bits8 *oldnullbitmap; + uint8 *oldnullbitmap; int oldnitems, newnitems, olddatasize, @@ -2230,6 +2234,7 @@ array_set_element(Datum arraydatum, addedafter, lenbefore, lenafter; + uint8 elmalignby = typalign_to_alignby(elmalign); if (arraytyplen > 0) { @@ -2256,8 +2261,8 @@ array_set_element(Datum arraydatum, resultarray = (char *) palloc(arraytyplen); memcpy(resultarray, DatumGetPointer(arraydatum), arraytyplen); - elt_ptr = (char *) resultarray + indx[0] * elmlen; - ArrayCastAndSet(dataValue, elmlen, elmbyval, elmalign, elt_ptr); + elt_ptr = resultarray + indx[0] * elmlen; + ArrayCastAndSet(dataValue, elmlen, elmbyval, elmalignby, elt_ptr); return PointerGetDatum(resultarray); } @@ -2338,8 +2343,8 @@ array_set_element(Datum arraydatum, pg_add_s32_overflow(dim[0], addedbefore, &dim[0])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); lb[0] = indx[0]; if (addedbefore > 1) newhasnulls = true; /* will insert nulls */ @@ -2353,8 +2358,8 @@ array_set_element(Datum arraydatum, pg_add_s32_overflow(dim[0], addedafter, &dim[0])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); if (addedafter > 1) newhasnulls = true; /* will insert nulls */ } @@ -2415,9 +2420,9 @@ array_set_element(Datum arraydatum, else { olditemlen = att_addlength_pointer(0, elmlen, elt_ptr); - olditemlen = att_align_nominal(olditemlen, elmalign); + olditemlen = att_nominal_alignby(olditemlen, elmalignby); } - lenafter = (int) (olddatasize - lenbefore - olditemlen); + lenafter = olddatasize - lenbefore - olditemlen; } if (isNull) @@ -2425,7 +2430,7 @@ array_set_element(Datum arraydatum, else { newitemlen = att_addlength_datum(0, elmlen, dataValue); - newitemlen = att_align_nominal(newitemlen, elmalign); + newitemlen = att_nominal_alignby(newitemlen, elmalignby); } newsize = overheadlen + lenbefore + newitemlen + lenafter; @@ -2448,7 +2453,7 @@ array_set_element(Datum arraydatum, (char *) array + oldoverheadlen, lenbefore); if (!isNull) - ArrayCastAndSet(dataValue, elmlen, elmbyval, elmalign, + ArrayCastAndSet(dataValue, elmlen, elmbyval, elmalignby, (char *) newarray + overheadlen + lenbefore); memcpy((char *) newarray + overheadlen + lenbefore + newitemlen, (char *) array + oldoverheadlen + lenbefore + olditemlen, @@ -2462,7 +2467,7 @@ array_set_element(Datum arraydatum, */ if (newhasnulls) { - bits8 *newnullbitmap = ARR_NULLBITMAP(newarray); + uint8 *newnullbitmap = ARR_NULLBITMAP(newarray); /* palloc0 above already marked any inserted positions as nulls */ /* Fix the inserted value */ @@ -2615,8 +2620,8 @@ array_set_element_expanded(Datum arraydatum, pg_add_s32_overflow(dim[0], addedbefore, &dim[0])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); lb[0] = indx[0]; dimschanged = true; if (addedbefore > 1) @@ -2631,8 +2636,8 @@ array_set_element_expanded(Datum arraydatum, pg_add_s32_overflow(dim[0], addedafter, &dim[0])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); dimschanged = true; if (addedafter > 1) newhasnulls = true; /* will insert nulls */ @@ -2892,8 +2897,8 @@ array_set_slice(Datum arraydatum, pg_add_s32_overflow(dim[i], 1, &dim[i])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); lb[i] = lowerIndx[i]; } @@ -2946,8 +2951,8 @@ array_set_slice(Datum arraydatum, pg_add_s32_overflow(dim[0], addedbefore, &dim[0])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); lb[0] = lowerIndx[0]; if (addedbefore > 1) newhasnulls = true; /* will insert nulls */ @@ -2961,8 +2966,8 @@ array_set_slice(Datum arraydatum, pg_add_s32_overflow(dim[0], addedafter, &dim[0])) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxArraySize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxArraySize))); if (addedafter > 1) newhasnulls = true; /* will insert nulls */ } @@ -3054,7 +3059,7 @@ array_set_slice(Datum arraydatum, int slicelb = Max(oldlb, lowerIndx[0]); int sliceub = Min(oldub, upperIndx[0]); char *oldarraydata = ARR_DATA_PTR(array); - bits8 *oldarraybitmap = ARR_NULLBITMAP(array); + uint8 *oldarraybitmap = ARR_NULLBITMAP(array); /* count/size of old array entries that will go before the slice */ itemsbefore = Min(slicelb, oldub + 1) - oldlb; @@ -3116,8 +3121,8 @@ array_set_slice(Datum arraydatum, /* fill in nulls bitmap if needed */ if (newhasnulls) { - bits8 *newnullbitmap = ARR_NULLBITMAP(newarray); - bits8 *oldnullbitmap = ARR_NULLBITMAP(array); + uint8 *newnullbitmap = ARR_NULLBITMAP(newarray); + uint8 *oldnullbitmap = ARR_NULLBITMAP(array); /* palloc0 above already marked any inserted positions as nulls */ array_bitmap_copy(newnullbitmap, addedbefore, @@ -3220,6 +3225,7 @@ array_map(Datum arrayd, int typlen; bool typbyval; char typalign; + uint8 typalignby; array_iter iter; ArrayMetaState *inp_extra; ArrayMetaState *ret_extra; @@ -3269,21 +3275,21 @@ array_map(Datum arrayd, typlen = ret_extra->typlen; typbyval = ret_extra->typbyval; typalign = ret_extra->typalign; + typalignby = typalign_to_alignby(typalign); /* Allocate temporary arrays for new values */ values = (Datum *) palloc(nitems * sizeof(Datum)); nulls = (bool *) palloc(nitems * sizeof(bool)); /* Loop over source data */ - array_iter_setup(&iter, v); + array_iter_setup(&iter, v, inp_typlen, inp_typbyval, inp_typalign); hasnulls = false; for (i = 0; i < nitems; i++) { /* Get source element, checking for NULL */ *transform_source = - array_iter_next(&iter, transform_source_isnull, i, - inp_typlen, inp_typbyval, inp_typalign); + array_iter_next(&iter, transform_source_isnull, i); /* Apply the given expression to source element */ values[i] = ExecEvalExpr(exprstate, econtext, &nulls[i]); @@ -3297,13 +3303,13 @@ array_map(Datum arrayd, values[i] = PointerGetDatum(PG_DETOAST_DATUM(values[i])); /* Update total result size */ nbytes = att_addlength_datum(nbytes, typlen, values[i]); - nbytes = att_align_nominal(nbytes, typalign); + nbytes = att_nominal_alignby(nbytes, typalignby); /* check for overflow of total request */ if (!AllocSizeIsValid(nbytes)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); } } @@ -3406,7 +3412,7 @@ construct_array_builtin(Datum *elems, int nelems, Oid elmtype) case FLOAT8OID: elmlen = sizeof(float8); - elmbyval = FLOAT8PASSBYVAL; + elmbyval = true; elmalign = TYPALIGN_DOUBLE; break; @@ -3424,7 +3430,7 @@ construct_array_builtin(Datum *elems, int nelems, Oid elmtype) case INT8OID: elmlen = sizeof(int64); - elmbyval = FLOAT8PASSBYVAL; + elmbyval = true; elmalign = TYPALIGN_DOUBLE; break; @@ -3504,6 +3510,7 @@ construct_md_array(Datum *elems, int32 dataoffset; int i; int nelems; + uint8 elmalignby = typalign_to_alignby(elmalign); if (ndims < 0) /* we do allow zero-dimension arrays */ ereport(ERROR, @@ -3537,13 +3544,13 @@ construct_md_array(Datum *elems, if (elmlen == -1) elems[i] = PointerGetDatum(PG_DETOAST_DATUM(elems[i])); nbytes = att_addlength_datum(nbytes, elmlen, elems[i]); - nbytes = att_align_nominal(nbytes, elmalign); + nbytes = att_nominal_alignby(nbytes, elmalignby); /* check for overflow of total request */ if (!AllocSizeIsValid(nbytes)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); } /* Allocate and initialize result array */ @@ -3581,7 +3588,7 @@ construct_empty_array(Oid elmtype) { ArrayType *result; - result = (ArrayType *) palloc0(sizeof(ArrayType)); + result = palloc0_object(ArrayType); SET_VARSIZE(result, sizeof(ArrayType)); result->ndim = 0; result->dataoffset = 0; @@ -3628,7 +3635,7 @@ construct_empty_expanded_array(Oid element_type, * to hard-wire values if the element type is hard-wired. */ void -deconstruct_array(ArrayType *array, +deconstruct_array(const ArrayType *array, Oid elmtype, int elmlen, bool elmbyval, char elmalign, Datum **elemsp, bool **nullsp, int *nelemsp) @@ -3637,16 +3644,17 @@ deconstruct_array(ArrayType *array, bool *nulls; int nelems; char *p; - bits8 *bitmap; + uint8 *bitmap; int bitmask; int i; + uint8 elmalignby = typalign_to_alignby(elmalign); Assert(ARR_ELEMTYPE(array) == elmtype); nelems = ArrayGetNItems(ARR_NDIM(array), ARR_DIMS(array)); - *elemsp = elems = (Datum *) palloc(nelems * sizeof(Datum)); + *elemsp = elems = palloc_array(Datum, nelems); if (nullsp) - *nullsp = nulls = (bool *) palloc0(nelems * sizeof(bool)); + *nullsp = nulls = palloc0_array(bool, nelems); else nulls = NULL; *nelemsp = nelems; @@ -3672,7 +3680,7 @@ deconstruct_array(ArrayType *array, { elems[i] = fetch_att(p, elmbyval, elmlen); p = att_addlength_pointer(p, elmlen, p); - p = (char *) att_align_nominal(p, elmalign); + p = (char *) att_nominal_alignby(p, elmalignby); } /* advance bitmap pointer if any */ @@ -3694,7 +3702,7 @@ deconstruct_array(ArrayType *array, * useful when manipulating arrays from/for system catalogs. */ void -deconstruct_array_builtin(ArrayType *array, +deconstruct_array_builtin(const ArrayType *array, Oid elmtype, Datum **elemsp, bool **nullsp, int *nelemsp) { @@ -3718,7 +3726,7 @@ deconstruct_array_builtin(ArrayType *array, case FLOAT8OID: elmlen = sizeof(float8); - elmbyval = FLOAT8PASSBYVAL; + elmbyval = true; elmalign = TYPALIGN_DOUBLE; break; @@ -3728,6 +3736,12 @@ deconstruct_array_builtin(ArrayType *array, elmalign = TYPALIGN_SHORT; break; + case INT4OID: + elmlen = sizeof(int32); + elmbyval = true; + elmalign = TYPALIGN_INT; + break; + case OIDOID: elmlen = sizeof(Oid); elmbyval = true; @@ -3764,10 +3778,10 @@ deconstruct_array_builtin(ArrayType *array, * if the array *might* contain a null. */ bool -array_contains_nulls(ArrayType *array) +array_contains_nulls(const ArrayType *array) { int nelems; - bits8 *bitmap; + uint8 *bitmap; int bitmask; /* Easy answer if there's no null bitmap */ @@ -3877,8 +3891,8 @@ array_eq(PG_FUNCTION_ARGS) /* Loop over source data */ nitems = ArrayGetNItems(ndims1, dims1); - array_iter_setup(&it1, array1); - array_iter_setup(&it2, array2); + array_iter_setup(&it1, array1, typlen, typbyval, typalign); + array_iter_setup(&it2, array2, typlen, typbyval, typalign); for (i = 0; i < nitems; i++) { @@ -3889,10 +3903,8 @@ array_eq(PG_FUNCTION_ARGS) bool oprresult; /* Get elements, checking for NULL */ - elt1 = array_iter_next(&it1, &isnull1, i, - typlen, typbyval, typalign); - elt2 = array_iter_next(&it2, &isnull2, i, - typlen, typbyval, typalign); + elt1 = array_iter_next(&it1, &isnull1, i); + elt2 = array_iter_next(&it2, &isnull2, i); /* * We consider two NULLs equal; NULL and not-NULL are unequal. @@ -4041,8 +4053,8 @@ array_cmp(FunctionCallInfo fcinfo) /* Loop over source data */ min_nitems = Min(nitems1, nitems2); - array_iter_setup(&it1, array1); - array_iter_setup(&it2, array2); + array_iter_setup(&it1, array1, typlen, typbyval, typalign); + array_iter_setup(&it2, array2, typlen, typbyval, typalign); for (i = 0; i < min_nitems; i++) { @@ -4053,8 +4065,8 @@ array_cmp(FunctionCallInfo fcinfo) int32 cmpresult; /* Get elements, checking for NULL */ - elt1 = array_iter_next(&it1, &isnull1, i, typlen, typbyval, typalign); - elt2 = array_iter_next(&it2, &isnull2, i, typlen, typbyval, typalign); + elt1 = array_iter_next(&it1, &isnull1, i); + elt2 = array_iter_next(&it2, &isnull2, i); /* * We consider two NULLs equal; NULL > not-NULL. @@ -4208,7 +4220,7 @@ hash_array(PG_FUNCTION_ARGS) * modify typentry, since that points directly into the type * cache. */ - record_typentry = palloc0(sizeof(*record_typentry)); + record_typentry = palloc0_object(TypeCacheEntry); record_typentry->type_id = element_type; /* fill in what we need below */ @@ -4237,7 +4249,7 @@ hash_array(PG_FUNCTION_ARGS) /* Loop over source data */ nitems = ArrayGetNItems(ndims, dims); - array_iter_setup(&iter, array); + array_iter_setup(&iter, array, typlen, typbyval, typalign); for (i = 0; i < nitems; i++) { @@ -4246,7 +4258,7 @@ hash_array(PG_FUNCTION_ARGS) uint32 elthash; /* Get element, checking for NULL */ - elt = array_iter_next(&iter, &isnull, i, typlen, typbyval, typalign); + elt = array_iter_next(&iter, &isnull, i); if (isnull) { @@ -4327,7 +4339,7 @@ hash_array_extended(PG_FUNCTION_ARGS) /* Loop over source data */ nitems = ArrayGetNItems(ndims, dims); - array_iter_setup(&iter, array); + array_iter_setup(&iter, array, typlen, typbyval, typalign); for (i = 0; i < nitems; i++) { @@ -4336,7 +4348,7 @@ hash_array_extended(PG_FUNCTION_ARGS) uint64 elthash; /* Get element, checking for NULL */ - elt = array_iter_next(&iter, &isnull, i, typlen, typbyval, typalign); + elt = array_iter_next(&iter, &isnull, i); if (isnull) { @@ -4450,7 +4462,7 @@ array_contain_compare(AnyArrayType *array1, AnyArrayType *array2, Oid collation, /* Loop over source data */ nelems1 = ArrayGetNItems(AARR_NDIM(array1), AARR_DIMS(array1)); - array_iter_setup(&it1, array1); + array_iter_setup(&it1, array1, typlen, typbyval, typalign); for (i = 0; i < nelems1; i++) { @@ -4458,7 +4470,7 @@ array_contain_compare(AnyArrayType *array1, AnyArrayType *array2, Oid collation, bool isnull1; /* Get element, checking for NULL */ - elt1 = array_iter_next(&it1, &isnull1, i, typlen, typbyval, typalign); + elt1 = array_iter_next(&it1, &isnull1, i); /* * We assume that the comparison operator is strict, so a NULL can't @@ -4596,12 +4608,12 @@ arraycontained(PG_FUNCTION_ARGS) ArrayIterator array_create_iterator(ArrayType *arr, int slice_ndim, ArrayMetaState *mstate) { - ArrayIterator iterator = palloc0(sizeof(ArrayIteratorData)); + ArrayIterator iterator = palloc0_object(ArrayIteratorData); /* * Sanity-check inputs --- caller should have got this right already */ - Assert(PointerIsValid(arr)); + Assert(arr); if (slice_ndim < 0 || slice_ndim > ARR_NDIM(arr)) elog(ERROR, "invalid arguments to array_create_iterator"); @@ -4625,6 +4637,7 @@ array_create_iterator(ArrayType *arr, int slice_ndim, ArrayMetaState *mstate) &iterator->typlen, &iterator->typbyval, &iterator->typalign); + iterator->typalignby = typalign_to_alignby(iterator->typalign); /* * Remember the slicing parameters. @@ -4699,7 +4712,7 @@ array_iterate(ArrayIterator iterator, Datum *value, bool *isnull) /* Move our data pointer forward to the next element */ p = att_addlength_pointer(p, iterator->typlen, p); - p = (char *) att_align_nominal(p, iterator->typalign); + p = (char *) att_nominal_alignby(p, iterator->typalignby); iterator->data_ptr = p; } } @@ -4729,7 +4742,7 @@ array_iterate(ArrayIterator iterator, Datum *value, bool *isnull) /* Move our data pointer forward to the next element */ p = att_addlength_pointer(p, iterator->typlen, p); - p = (char *) att_align_nominal(p, iterator->typalign); + p = (char *) att_nominal_alignby(p, iterator->typalignby); } } @@ -4778,7 +4791,7 @@ array_free_iterator(ArrayIterator iterator) * offset: 0-based linear element number of array element */ static bool -array_get_isnull(const bits8 *nullbitmap, int offset) +array_get_isnull(const uint8 *nullbitmap, int offset) { if (nullbitmap == NULL) return false; /* assume not null */ @@ -4795,7 +4808,7 @@ array_get_isnull(const bits8 *nullbitmap, int offset) * isNull: null status to set */ static void -array_set_isnull(bits8 *nullbitmap, int offset, bool isNull) +array_set_isnull(uint8 *nullbitmap, int offset, bool isNull) { int bitmask; @@ -4827,7 +4840,7 @@ static int ArrayCastAndSet(Datum src, int typlen, bool typbyval, - char typalign, + uint8 typalignby, char *dest) { int inc; @@ -4838,14 +4851,14 @@ ArrayCastAndSet(Datum src, store_att_byval(dest, src, typlen); else memmove(dest, DatumGetPointer(src), typlen); - inc = att_align_nominal(typlen, typalign); + inc = att_nominal_alignby(typlen, typalignby); } else { Assert(!typbyval); inc = att_addlength_datum(0, typlen, src); memmove(dest, DatumGetPointer(src), inc); - inc = att_align_nominal(inc, typalign); + inc = att_nominal_alignby(inc, typalignby); } return inc; @@ -4863,15 +4876,16 @@ ArrayCastAndSet(Datum src, * It is caller's responsibility to ensure that nitems is within range */ static char * -array_seek(char *ptr, int offset, bits8 *nullbitmap, int nitems, +array_seek(char *ptr, int offset, uint8 *nullbitmap, int nitems, int typlen, bool typbyval, char typalign) { + uint8 typalignby = typalign_to_alignby(typalign); int bitmask; int i; /* easy if fixed-size elements and no NULLs */ if (typlen > 0 && !nullbitmap) - return ptr + nitems * ((Size) att_align_nominal(typlen, typalign)); + return ptr + nitems * ((Size) att_nominal_alignby(typlen, typalignby)); /* seems worth having separate loops for NULL and no-NULLs cases */ if (nullbitmap) @@ -4884,7 +4898,7 @@ array_seek(char *ptr, int offset, bits8 *nullbitmap, int nitems, if (*nullbitmap & bitmask) { ptr = att_addlength_pointer(ptr, typlen, ptr); - ptr = (char *) att_align_nominal(ptr, typalign); + ptr = (char *) att_nominal_alignby(ptr, typalignby); } bitmask <<= 1; if (bitmask == 0x100) @@ -4899,7 +4913,7 @@ array_seek(char *ptr, int offset, bits8 *nullbitmap, int nitems, for (i = 0; i < nitems; i++) { ptr = att_addlength_pointer(ptr, typlen, ptr); - ptr = (char *) att_align_nominal(ptr, typalign); + ptr = (char *) att_nominal_alignby(ptr, typalignby); } } return ptr; @@ -4911,7 +4925,7 @@ array_seek(char *ptr, int offset, bits8 *nullbitmap, int nitems, * Parameters same as for array_seek */ static int -array_nelems_size(char *ptr, int offset, bits8 *nullbitmap, int nitems, +array_nelems_size(char *ptr, int offset, uint8 *nullbitmap, int nitems, int typlen, bool typbyval, char typalign) { return array_seek(ptr, offset, nullbitmap, nitems, @@ -4934,7 +4948,7 @@ array_nelems_size(char *ptr, int offset, bits8 *nullbitmap, int nitems, */ static int array_copy(char *destptr, int nitems, - char *srcptr, int offset, bits8 *nullbitmap, + char *srcptr, int offset, uint8 *nullbitmap, int typlen, bool typbyval, char typalign) { int numbytes; @@ -4963,8 +4977,8 @@ array_copy(char *destptr, int nitems, * to make it worth worrying too much. For the moment, KISS. */ void -array_bitmap_copy(bits8 *destbitmap, int destoffset, - const bits8 *srcbitmap, int srcoffset, +array_bitmap_copy(uint8 *destbitmap, int destoffset, + const uint8 *srcbitmap, int srcoffset, int nitems) { int destbitmask, @@ -5034,7 +5048,7 @@ array_bitmap_copy(bits8 *destbitmap, int destoffset, * We assume the caller has verified that the slice coordinates are valid. */ static int -array_slice_size(char *arraydataptr, bits8 *arraynullsptr, +array_slice_size(char *arraydataptr, uint8 *arraynullsptr, int ndim, int *dim, int *lb, int *st, int *endp, int typlen, bool typbyval, char typalign) @@ -5049,12 +5063,13 @@ array_slice_size(char *arraydataptr, bits8 *arraynullsptr, j, inc; int count = 0; + uint8 typalignby = typalign_to_alignby(typalign); mda_get_range(ndim, span, st, endp); /* Pretty easy for fixed element length without nulls ... */ if (typlen > 0 && !arraynullsptr) - return ArrayGetNItems(ndim, span) * att_align_nominal(typlen, typalign); + return ArrayGetNItems(ndim, span) * att_nominal_alignby(typlen, typalignby); /* Else gotta do it the hard way */ src_offset = ArrayGetOffset(ndim, dim, lb, st); @@ -5076,7 +5091,7 @@ array_slice_size(char *arraydataptr, bits8 *arraynullsptr, if (!array_get_isnull(arraynullsptr, src_offset)) { inc = att_addlength_pointer(0, typlen, ptr); - inc = att_align_nominal(inc, typalign); + inc = att_nominal_alignby(inc, typalignby); ptr += inc; count += inc; } @@ -5099,7 +5114,7 @@ array_extract_slice(ArrayType *newarray, int *dim, int *lb, char *arraydataptr, - bits8 *arraynullsptr, + uint8 *arraynullsptr, int *st, int *endp, int typlen, @@ -5107,7 +5122,7 @@ array_extract_slice(ArrayType *newarray, char typalign) { char *destdataptr = ARR_DATA_PTR(newarray); - bits8 *destnullsptr = ARR_NULLBITMAP(newarray); + uint8 *destnullsptr = ARR_NULLBITMAP(newarray); char *srcdataptr; int src_offset, dest_offset, @@ -5182,9 +5197,9 @@ array_insert_slice(ArrayType *destArray, char *destPtr = ARR_DATA_PTR(destArray); char *origPtr = ARR_DATA_PTR(origArray); char *srcPtr = ARR_DATA_PTR(srcArray); - bits8 *destBitmap = ARR_NULLBITMAP(destArray); - bits8 *origBitmap = ARR_NULLBITMAP(origArray); - bits8 *srcBitmap = ARR_NULLBITMAP(srcArray); + uint8 *destBitmap = ARR_NULLBITMAP(destArray); + uint8 *origBitmap = ARR_NULLBITMAP(origArray); + uint8 *srcBitmap = ARR_NULLBITMAP(srcArray); int orignitems = ArrayGetNItems(ARR_NDIM(origArray), ARR_DIMS(origArray)); int dest_offset, @@ -5374,8 +5389,8 @@ accumArrayResult(ArrayBuildState *astate, if (!AllocSizeIsValid(astate->alen * sizeof(Datum))) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); astate->dvalues = (Datum *) repalloc(astate->dvalues, astate->alen * sizeof(Datum)); astate->dnulls = (bool *) @@ -5664,7 +5679,7 @@ accumArrayResultArr(ArrayBuildStateArr *astate, * previous inputs by marking all their items non-null. */ astate->aitems = pg_nextpower2_32(Max(256, newnitems + 1)); - astate->nullbitmap = (bits8 *) palloc((astate->aitems + 7) / 8); + astate->nullbitmap = (uint8 *) palloc((astate->aitems + 7) / 8); array_bitmap_copy(astate->nullbitmap, 0, NULL, 0, astate->nitems); @@ -5672,7 +5687,7 @@ accumArrayResultArr(ArrayBuildStateArr *astate, else if (newnitems > astate->aitems) { astate->aitems = Max(astate->aitems * 2, newnitems); - astate->nullbitmap = (bits8 *) + astate->nullbitmap = (uint8 *) repalloc(astate->nullbitmap, (astate->aitems + 7) / 8); } array_bitmap_copy(astate->nullbitmap, astate->nitems, @@ -5686,7 +5701,7 @@ accumArrayResultArr(ArrayBuildStateArr *astate, MemoryContextSwitchTo(oldcontext); /* Release detoasted copy if any */ - if ((Pointer) arg != DatumGetPointer(dvalue)) + if (arg != DatumGetPointer(dvalue)) pfree(arg); return astate; @@ -5943,7 +5958,7 @@ generate_subscripts(PG_FUNCTION_ARGS) * switch to memory context appropriate for multiple function calls */ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - fctx = (generate_subscripts_fctx *) palloc(sizeof(generate_subscripts_fctx)); + fctx = palloc_object(generate_subscripts_fctx); lb = AARR_LBOUND(v); dimv = AARR_DIMS(v); @@ -6095,6 +6110,7 @@ array_fill_internal(ArrayType *dims, ArrayType *lbs, int16 elmlen; bool elmbyval; char elmalign; + uint8 elmalignby; ArrayMetaState *my_extra; /* @@ -6189,6 +6205,7 @@ array_fill_internal(ArrayType *dims, ArrayType *lbs, elmlen = my_extra->typlen; elmbyval = my_extra->typbyval; elmalign = my_extra->typalign; + elmalignby = typalign_to_alignby(elmalign); /* compute required space */ if (!isnull) @@ -6203,7 +6220,7 @@ array_fill_internal(ArrayType *dims, ArrayType *lbs, value = PointerGetDatum(PG_DETOAST_DATUM(value)); nbytes = att_addlength_datum(0, elmlen, value); - nbytes = att_align_nominal(nbytes, elmalign); + nbytes = att_nominal_alignby(nbytes, elmalignby); Assert(nbytes > 0); totbytes = nbytes * nitems; @@ -6213,8 +6230,8 @@ array_fill_internal(ArrayType *dims, ArrayType *lbs, !AllocSizeIsValid(totbytes)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); /* * This addition can't overflow, but it might cause us to go past @@ -6227,7 +6244,7 @@ array_fill_internal(ArrayType *dims, ArrayType *lbs, p = ARR_DATA_PTR(result); for (i = 0; i < nitems; i++) - p += ArrayCastAndSet(value, elmlen, elmbyval, elmalign, p); + p += ArrayCastAndSet(value, elmlen, elmbyval, elmalignby, p); } else { @@ -6258,9 +6275,6 @@ array_unnest(PG_FUNCTION_ARGS) array_iter iter; int nextelem; int numelems; - int16 elmlen; - bool elmbyval; - char elmalign; } array_unnest_fctx; FuncCallContext *funcctx; @@ -6271,6 +6285,9 @@ array_unnest(PG_FUNCTION_ARGS) if (SRF_IS_FIRSTCALL()) { AnyArrayType *arr; + int16 elmlen; + bool elmbyval; + char elmalign; /* create a function context for cross-call persistence */ funcctx = SRF_FIRSTCALL_INIT(); @@ -6290,25 +6307,26 @@ array_unnest(PG_FUNCTION_ARGS) arr = PG_GETARG_ANY_ARRAY_P(0); /* allocate memory for user context */ - fctx = (array_unnest_fctx *) palloc(sizeof(array_unnest_fctx)); - - /* initialize state */ - array_iter_setup(&fctx->iter, arr); - fctx->nextelem = 0; - fctx->numelems = ArrayGetNItems(AARR_NDIM(arr), AARR_DIMS(arr)); + fctx = palloc_object(array_unnest_fctx); + /* get element-type data */ if (VARATT_IS_EXPANDED_HEADER(arr)) { /* we can just grab the type data from expanded array */ - fctx->elmlen = arr->xpn.typlen; - fctx->elmbyval = arr->xpn.typbyval; - fctx->elmalign = arr->xpn.typalign; + elmlen = arr->xpn.typlen; + elmbyval = arr->xpn.typbyval; + elmalign = arr->xpn.typalign; } else get_typlenbyvalalign(AARR_ELEMTYPE(arr), - &fctx->elmlen, - &fctx->elmbyval, - &fctx->elmalign); + &elmlen, + &elmbyval, + &elmalign); + + /* initialize state */ + array_iter_setup(&fctx->iter, arr, elmlen, elmbyval, elmalign); + fctx->nextelem = 0; + fctx->numelems = ArrayGetNItems(AARR_NDIM(arr), AARR_DIMS(arr)); funcctx->user_fctx = fctx; MemoryContextSwitchTo(oldcontext); @@ -6323,8 +6341,7 @@ array_unnest(PG_FUNCTION_ARGS) int offset = fctx->nextelem++; Datum elem; - elem = array_iter_next(&fctx->iter, &fcinfo->isnull, offset, - fctx->elmlen, fctx->elmbyval, fctx->elmalign); + elem = array_iter_next(&fctx->iter, &fcinfo->isnull, offset); SRF_RETURN_NEXT(funcctx, elem); } @@ -6400,8 +6417,9 @@ array_replace_internal(ArrayType *array, int typlen; bool typbyval; char typalign; + uint8 typalignby; char *arraydataptr; - bits8 *bitmap; + uint8 *bitmap; int bitmask; bool changed = false; TypeCacheEntry *typentry; @@ -6444,6 +6462,7 @@ array_replace_internal(ArrayType *array, typlen = typentry->typlen; typbyval = typentry->typbyval; typalign = typentry->typalign; + typalignby = typalign_to_alignby(typalign); /* * Detoast values if they are toasted. The replacement value must be @@ -6505,7 +6524,7 @@ array_replace_internal(ArrayType *array, isNull = false; elt = fetch_att(arraydataptr, typbyval, typlen); arraydataptr = att_addlength_datum(arraydataptr, typlen, elt); - arraydataptr = (char *) att_align_nominal(arraydataptr, typalign); + arraydataptr = (char *) att_nominal_alignby(arraydataptr, typalignby); if (search_isnull) { @@ -6552,13 +6571,13 @@ array_replace_internal(ArrayType *array, { /* Update total result size */ nbytes = att_addlength_datum(nbytes, typlen, values[nresult]); - nbytes = att_align_nominal(nbytes, typalign); + nbytes = att_nominal_alignby(nbytes, typalignby); /* check for overflow of total request */ if (!AllocSizeIsValid(nbytes)) ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("array size exceeds the maximum allowed (%d)", - (int) MaxAllocSize))); + errmsg("array size exceeds the maximum allowed (%zu)", + MaxAllocSize))); } nresult++; } @@ -6859,6 +6878,7 @@ width_bucket_array_variable(Datum operand, int typlen = typentry->typlen; bool typbyval = typentry->typbyval; char typalign = typentry->typalign; + uint8 typalignby = typalign_to_alignby(typalign); int left; int right; @@ -6882,7 +6902,7 @@ width_bucket_array_variable(Datum operand, for (i = left; i < mid; i++) { ptr = att_addlength_pointer(ptr, typlen, ptr); - ptr = (char *) att_align_nominal(ptr, typalign); + ptr = (char *) att_nominal_alignby(ptr, typalignby); } locfcinfo->args[0].value = operand; @@ -6907,7 +6927,7 @@ width_bucket_array_variable(Datum operand, * ensures we do only O(N) array indexing work, not O(N^2). */ ptr = att_addlength_pointer(ptr, typlen, ptr); - thresholds_data = (char *) att_align_nominal(ptr, typalign); + thresholds_data = (char *) att_nominal_alignby(ptr, typalignby); } } diff --git a/src/backend/utils/adt/arraysubs.c b/src/backend/utils/adt/arraysubs.c index 2940fb8e8d737..2bf9e9509fb35 100644 --- a/src/backend/utils/adt/arraysubs.c +++ b/src/backend/utils/adt/arraysubs.c @@ -3,7 +3,7 @@ * arraysubs.c * Subscripting support functions for arrays. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "catalog/pg_type_d.h" #include "executor/execExpr.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -140,7 +141,7 @@ array_subscript_transform(SubscriptingRef *sbsref, upperIndexpr = lappend(upperIndexpr, subexpr); } - /* ... and store the transformed lists into the SubscriptRef node */ + /* ... and store the transformed lists into the SubscriptingRef node */ sbsref->refupperindexpr = upperIndexpr; sbsref->reflowerindexpr = lowerIndexpr; @@ -497,7 +498,7 @@ array_exec_setup(const SubscriptingRef *sbsref, /* * Allocate type-specific workspace. */ - workspace = (ArraySubWorkspace *) palloc(sizeof(ArraySubWorkspace)); + workspace = palloc_object(ArraySubWorkspace); sbsrefstate->workspace = workspace; /* diff --git a/src/backend/utils/adt/arrayutils.c b/src/backend/utils/adt/arrayutils.c index 650bb51d4cdb3..ca861a42037c0 100644 --- a/src/backend/utils/adt/arrayutils.c +++ b/src/backend/utils/adt/arrayutils.c @@ -3,7 +3,7 @@ * arrayutils.c * This file contains some support routines required for array functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/ascii.c b/src/backend/utils/adt/ascii.c index e3654dc7f3f22..d43e690c00063 100644 --- a/src/backend/utils/adt/ascii.c +++ b/src/backend/utils/adt/ascii.c @@ -2,7 +2,7 @@ * ascii.c * The PostgreSQL routine for string to ascii conversion. * - * Portions Copyright (c) 1999-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1999-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/ascii.c diff --git a/src/backend/utils/adt/bool.c b/src/backend/utils/adt/bool.c index 474653797b55b..090ad7df9cad2 100644 --- a/src/backend/utils/adt/bool.c +++ b/src/backend/utils/adt/bool.c @@ -3,7 +3,7 @@ * bool.c * Functions for the built-in type "bool". * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/bytea.c b/src/backend/utils/adt/bytea.c new file mode 100644 index 0000000000000..f6e3266ac3247 --- /dev/null +++ b/src/backend/utils/adt/bytea.c @@ -0,0 +1,1369 @@ +/*------------------------------------------------------------------------- + * + * bytea.c + * Functions for the bytea type. + * + * Portions Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/backend/utils/adt/bytea.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/detoast.h" +#include "common/hashfn.h" +#include "common/int.h" +#include "fmgr.h" +#include "lib/hyperloglog.h" +#include "libpq/pqformat.h" +#include "port/pg_bitutils.h" +#include "port/pg_bswap.h" +#include "utils/builtins.h" +#include "utils/bytea.h" +#include "utils/fmgrprotos.h" +#include "utils/guc.h" +#include "utils/memutils.h" +#include "utils/sortsupport.h" +#include "utils/uuid.h" +#include "varatt.h" + +/* GUC variable */ +int bytea_output = BYTEA_OUTPUT_HEX; + +static bytea *bytea_catenate(bytea *t1, bytea *t2); +static bytea *bytea_substring(Datum str, int S, int L, + bool length_not_specified); +static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl); + +typedef struct +{ + bool abbreviate; /* Should we abbreviate keys? */ + hyperLogLogState abbr_card; /* Abbreviated key cardinality state */ + hyperLogLogState full_card; /* Full key cardinality state */ + double prop_card; /* Required cardinality proportion */ +} ByteaSortSupport; + +/* Static function declarations for sort support */ +static int byteafastcmp(Datum x, Datum y, SortSupport ssup); +static Datum bytea_abbrev_convert(Datum original, SortSupport ssup); +static bool bytea_abbrev_abort(int memtupcount, SortSupport ssup); + +/* + * bytea_catenate + * Guts of byteacat(), broken out so it can be used by other functions + * + * Arguments can be in short-header form, but not compressed or out-of-line + */ +static bytea * +bytea_catenate(bytea *t1, bytea *t2) +{ + bytea *result; + int len1, + len2, + len; + char *ptr; + + len1 = VARSIZE_ANY_EXHDR(t1); + len2 = VARSIZE_ANY_EXHDR(t2); + + /* paranoia ... probably should throw error instead? */ + if (len1 < 0) + len1 = 0; + if (len2 < 0) + len2 = 0; + + len = len1 + len2 + VARHDRSZ; + result = (bytea *) palloc(len); + + /* Set size of result string... */ + SET_VARSIZE(result, len); + + /* Fill data field of result string... */ + ptr = VARDATA(result); + if (len1 > 0) + memcpy(ptr, VARDATA_ANY(t1), len1); + if (len2 > 0) + memcpy(ptr + len1, VARDATA_ANY(t2), len2); + + return result; +} + +#define PG_STR_GET_BYTEA(str_) \ + DatumGetByteaPP(DirectFunctionCall1(byteain, CStringGetDatum(str_))) + +static bytea * +bytea_substring(Datum str, + int S, + int L, + bool length_not_specified) +{ + int32 S1; /* adjusted start position */ + int32 L1; /* adjusted substring length */ + int32 E; /* end position */ + + /* + * The logic here should generally match text_substring(). + */ + S1 = Max(S, 1); + + if (length_not_specified) + { + /* + * Not passed a length - DatumGetByteaPSlice() grabs everything to the + * end of the string if we pass it a negative value for length. + */ + L1 = -1; + } + else if (L < 0) + { + /* SQL99 says to throw an error for E < S, i.e., negative length */ + ereport(ERROR, + (errcode(ERRCODE_SUBSTRING_ERROR), + errmsg("negative substring length not allowed"))); + L1 = -1; /* silence stupider compilers */ + } + else if (pg_add_s32_overflow(S, L, &E)) + { + /* + * L could be large enough for S + L to overflow, in which case the + * substring must run to end of string. + */ + L1 = -1; + } + else + { + /* + * A zero or negative value for the end position can happen if the + * start was negative or one. SQL99 says to return a zero-length + * string. + */ + if (E < 1) + return PG_STR_GET_BYTEA(""); + + L1 = E - S1; + } + + /* + * If the start position is past the end of the string, SQL99 says to + * return a zero-length string -- DatumGetByteaPSlice() will do that for + * us. We need only convert S1 to zero-based starting position. + */ + return DatumGetByteaPSlice(str, S1 - 1, L1); +} + +static bytea * +bytea_overlay(bytea *t1, bytea *t2, int sp, int sl) +{ + bytea *result; + bytea *s1; + bytea *s2; + int sp_pl_sl; + + /* + * Check for possible integer-overflow cases. For negative sp, throw a + * "substring length" error because that's what should be expected + * according to the spec's definition of OVERLAY(). + */ + if (sp <= 0) + ereport(ERROR, + (errcode(ERRCODE_SUBSTRING_ERROR), + errmsg("negative substring length not allowed"))); + if (pg_add_s32_overflow(sp, sl, &sp_pl_sl)) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer out of range"))); + + s1 = bytea_substring(PointerGetDatum(t1), 1, sp - 1, false); + s2 = bytea_substring(PointerGetDatum(t1), sp_pl_sl, -1, true); + result = bytea_catenate(s1, t2); + result = bytea_catenate(result, s2); + + return result; +} + +/***************************************************************************** + * USER I/O ROUTINES * + *****************************************************************************/ + +#define VAL(CH) ((CH) - '0') +#define DIG(VAL) ((VAL) + '0') + +/* + * byteain - converts from printable representation of byte array + * + * Non-printable characters must be passed as '\nnn' (octal) and are + * converted to internal form. '\' must be passed as '\\'. + */ +Datum +byteain(PG_FUNCTION_ARGS) +{ + char *inputText = PG_GETARG_CSTRING(0); + Node *escontext = fcinfo->context; + size_t len = strlen(inputText); + size_t bc; + char *tp; + char *rp; + bytea *result; + + /* Recognize hex input */ + if (inputText[0] == '\\' && inputText[1] == 'x') + { + bc = (len - 2) / 2 + VARHDRSZ; /* maximum possible length */ + result = palloc(bc); + bc = hex_decode_safe(inputText + 2, len - 2, VARDATA(result), + escontext); + SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */ + + PG_RETURN_BYTEA_P(result); + } + + /* Else, it's the traditional escaped style */ + result = (bytea *) palloc(len + VARHDRSZ); /* maximum possible length */ + + tp = inputText; + rp = VARDATA(result); + while (*tp != '\0') + { + if (tp[0] != '\\') + *rp++ = *tp++; + else if ((tp[1] >= '0' && tp[1] <= '3') && + (tp[2] >= '0' && tp[2] <= '7') && + (tp[3] >= '0' && tp[3] <= '7')) + { + int v; + + v = VAL(tp[1]); + v <<= 3; + v += VAL(tp[2]); + v <<= 3; + *rp++ = v + VAL(tp[3]); + + tp += 4; + } + else if (tp[1] == '\\') + { + *rp++ = '\\'; + tp += 2; + } + else + { + /* + * one backslash, not followed by another or ### valid octal + */ + ereturn(escontext, (Datum) 0, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("invalid input syntax for type %s", "bytea"))); + } + } + + bc = rp - VARDATA(result); /* actual length */ + SET_VARSIZE(result, bc + VARHDRSZ); + + PG_RETURN_BYTEA_P(result); +} + +/* + * byteaout - converts to printable representation of byte array + * + * In the traditional escaped format, non-printable characters are + * printed as '\nnn' (octal) and '\' as '\\'. + */ +Datum +byteaout(PG_FUNCTION_ARGS) +{ + bytea *vlena = PG_GETARG_BYTEA_PP(0); + char *result; + char *rp; + + if (bytea_output == BYTEA_OUTPUT_HEX) + { + /* Print hex format */ + rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 2 + 1); + *rp++ = '\\'; + *rp++ = 'x'; + rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp); + } + else if (bytea_output == BYTEA_OUTPUT_ESCAPE) + { + /* Print traditional escaped format */ + char *vp; + uint64 len; + int i; + + len = 1; /* empty string has 1 char */ + vp = VARDATA_ANY(vlena); + for (i = VARSIZE_ANY_EXHDR(vlena); i != 0; i--, vp++) + { + if (*vp == '\\') + len += 2; + else if ((unsigned char) *vp < 0x20 || (unsigned char) *vp > 0x7e) + len += 4; + else + len++; + } + + /* + * In principle len can't overflow uint32 if the input fit in 1GB, but + * for safety let's check rather than relying on palloc's internal + * check. + */ + if (len > MaxAllocSize) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg_internal("result of bytea output conversion is too large"))); + rp = result = (char *) palloc(len); + + vp = VARDATA_ANY(vlena); + for (i = VARSIZE_ANY_EXHDR(vlena); i != 0; i--, vp++) + { + if (*vp == '\\') + { + *rp++ = '\\'; + *rp++ = '\\'; + } + else if ((unsigned char) *vp < 0x20 || (unsigned char) *vp > 0x7e) + { + int val; /* holds unprintable chars */ + + val = *vp; + rp[0] = '\\'; + rp[3] = DIG(val & 07); + val >>= 3; + rp[2] = DIG(val & 07); + val >>= 3; + rp[1] = DIG(val & 03); + rp += 4; + } + else + *rp++ = *vp; + } + } + else + { + elog(ERROR, "unrecognized \"bytea_output\" setting: %d", + bytea_output); + rp = result = NULL; /* keep compiler quiet */ + } + *rp = '\0'; + PG_RETURN_CSTRING(result); +} + +/* + * bytearecv - converts external binary format to bytea + */ +Datum +bytearecv(PG_FUNCTION_ARGS) +{ + StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); + bytea *result; + int nbytes; + + nbytes = buf->len - buf->cursor; + result = (bytea *) palloc(nbytes + VARHDRSZ); + SET_VARSIZE(result, nbytes + VARHDRSZ); + pq_copymsgbytes(buf, VARDATA(result), nbytes); + PG_RETURN_BYTEA_P(result); +} + +/* + * byteasend - converts bytea to binary format + * + * This is a special case: just copy the input... + */ +Datum +byteasend(PG_FUNCTION_ARGS) +{ + bytea *vlena = PG_GETARG_BYTEA_P_COPY(0); + + PG_RETURN_BYTEA_P(vlena); +} + +Datum +bytea_string_agg_transfn(PG_FUNCTION_ARGS) +{ + StringInfo state; + + state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0); + + /* Append the value unless null, preceding it with the delimiter. */ + if (!PG_ARGISNULL(1)) + { + bytea *value = PG_GETARG_BYTEA_PP(1); + bool isfirst = false; + + /* + * You might think we can just throw away the first delimiter, however + * we must keep it as we may be a parallel worker doing partial + * aggregation building a state to send to the main process. We need + * to keep the delimiter of every aggregation so that the combine + * function can properly join up the strings of two separately + * partially aggregated results. The first delimiter is only stripped + * off in the final function. To know how much to strip off the front + * of the string, we store the length of the first delimiter in the + * StringInfo's cursor field, which we don't otherwise need here. + */ + if (state == NULL) + { + MemoryContext aggcontext; + MemoryContext oldcontext; + + if (!AggCheckCallContext(fcinfo, &aggcontext)) + { + /* cannot be called directly because of internal-type argument */ + elog(ERROR, "bytea_string_agg_transfn called in non-aggregate context"); + } + + /* + * Create state in aggregate context. It'll stay there across + * subsequent calls. + */ + oldcontext = MemoryContextSwitchTo(aggcontext); + state = makeStringInfo(); + MemoryContextSwitchTo(oldcontext); + + isfirst = true; + } + + if (!PG_ARGISNULL(2)) + { + bytea *delim = PG_GETARG_BYTEA_PP(2); + + appendBinaryStringInfo(state, VARDATA_ANY(delim), + VARSIZE_ANY_EXHDR(delim)); + if (isfirst) + state->cursor = VARSIZE_ANY_EXHDR(delim); + } + + appendBinaryStringInfo(state, VARDATA_ANY(value), + VARSIZE_ANY_EXHDR(value)); + } + + /* + * The transition type for string_agg() is declared to be "internal", + * which is a pass-by-value type the same size as a pointer. + */ + if (state) + PG_RETURN_POINTER(state); + PG_RETURN_NULL(); +} + +Datum +bytea_string_agg_finalfn(PG_FUNCTION_ARGS) +{ + StringInfo state; + + /* cannot be called directly because of internal-type argument */ + Assert(AggCheckCallContext(fcinfo, NULL)); + + state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0); + + if (state != NULL) + { + /* As per comment in transfn, strip data before the cursor position */ + bytea *result; + int strippedlen = state->len - state->cursor; + + result = (bytea *) palloc(strippedlen + VARHDRSZ); + SET_VARSIZE(result, strippedlen + VARHDRSZ); + memcpy(VARDATA(result), &state->data[state->cursor], strippedlen); + PG_RETURN_BYTEA_P(result); + } + else + PG_RETURN_NULL(); +} + +/*------------------------------------------------------------- + * byteaoctetlen + * + * get the number of bytes contained in an instance of type 'bytea' + *------------------------------------------------------------- + */ +Datum +byteaoctetlen(PG_FUNCTION_ARGS) +{ + Datum str = PG_GETARG_DATUM(0); + + /* We need not detoast the input at all */ + PG_RETURN_INT32(toast_raw_datum_size(str) - VARHDRSZ); +} + +/* + * byteacat - + * takes two bytea* and returns a bytea* that is the concatenation of + * the two. + * + * Cloned from textcat and modified as required. + */ +Datum +byteacat(PG_FUNCTION_ARGS) +{ + bytea *t1 = PG_GETARG_BYTEA_PP(0); + bytea *t2 = PG_GETARG_BYTEA_PP(1); + + PG_RETURN_BYTEA_P(bytea_catenate(t1, t2)); +} + +/* + * byteaoverlay + * Replace specified substring of first string with second + * + * The SQL standard defines OVERLAY() in terms of substring and concatenation. + * This code is a direct implementation of what the standard says. + */ +Datum +byteaoverlay(PG_FUNCTION_ARGS) +{ + bytea *t1 = PG_GETARG_BYTEA_PP(0); + bytea *t2 = PG_GETARG_BYTEA_PP(1); + int sp = PG_GETARG_INT32(2); /* substring start position */ + int sl = PG_GETARG_INT32(3); /* substring length */ + + PG_RETURN_BYTEA_P(bytea_overlay(t1, t2, sp, sl)); +} + +Datum +byteaoverlay_no_len(PG_FUNCTION_ARGS) +{ + bytea *t1 = PG_GETARG_BYTEA_PP(0); + bytea *t2 = PG_GETARG_BYTEA_PP(1); + int sp = PG_GETARG_INT32(2); /* substring start position */ + int sl; + + sl = VARSIZE_ANY_EXHDR(t2); /* defaults to length(t2) */ + PG_RETURN_BYTEA_P(bytea_overlay(t1, t2, sp, sl)); +} + +/* + * bytea_substr() + * Return a substring starting at the specified position. + * Cloned from text_substr and modified as required. + * + * Input: + * - string + * - starting position (is one-based) + * - string length (optional) + * + * If the starting position is zero or less, then return from the start of the string + * adjusting the length to be consistent with the "negative start" per SQL. + * If the length is less than zero, an ERROR is thrown. If no third argument + * (length) is provided, the length to the end of the string is assumed. + */ +Datum +bytea_substr(PG_FUNCTION_ARGS) +{ + PG_RETURN_BYTEA_P(bytea_substring(PG_GETARG_DATUM(0), + PG_GETARG_INT32(1), + PG_GETARG_INT32(2), + false)); +} + +/* + * bytea_substr_no_len - + * Wrapper to avoid opr_sanity failure due to + * one function accepting a different number of args. + */ +Datum +bytea_substr_no_len(PG_FUNCTION_ARGS) +{ + PG_RETURN_BYTEA_P(bytea_substring(PG_GETARG_DATUM(0), + PG_GETARG_INT32(1), + -1, + true)); +} + +/* + * bit_count + */ +Datum +bytea_bit_count(PG_FUNCTION_ARGS) +{ + bytea *t1 = PG_GETARG_BYTEA_PP(0); + + PG_RETURN_INT64(pg_popcount(VARDATA_ANY(t1), VARSIZE_ANY_EXHDR(t1))); +} + +/* + * byteapos - + * Return the position of the specified substring. + * Implements the SQL POSITION() function. + * Cloned from textpos and modified as required. + */ +Datum +byteapos(PG_FUNCTION_ARGS) +{ + bytea *t1 = PG_GETARG_BYTEA_PP(0); + bytea *t2 = PG_GETARG_BYTEA_PP(1); + int pos; + int px, + p; + int len1, + len2; + char *p1, + *p2; + + len1 = VARSIZE_ANY_EXHDR(t1); + len2 = VARSIZE_ANY_EXHDR(t2); + + if (len2 <= 0) + PG_RETURN_INT32(1); /* result for empty pattern */ + + p1 = VARDATA_ANY(t1); + p2 = VARDATA_ANY(t2); + + pos = 0; + px = (len1 - len2); + for (p = 0; p <= px; p++) + { + if ((*p2 == *p1) && (memcmp(p1, p2, len2) == 0)) + { + pos = p + 1; + break; + }; + p1++; + }; + + PG_RETURN_INT32(pos); +} + +/*------------------------------------------------------------- + * byteaGetByte + * + * this routine treats "bytea" as an array of bytes. + * It returns the Nth byte (a number between 0 and 255). + *------------------------------------------------------------- + */ +Datum +byteaGetByte(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + int32 n = PG_GETARG_INT32(1); + int len; + int byte; + + len = VARSIZE_ANY_EXHDR(v); + + if (n < 0 || n >= len) + ereport(ERROR, + (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), + errmsg("index %d out of valid range, 0..%d", + n, len - 1))); + + byte = ((unsigned char *) VARDATA_ANY(v))[n]; + + PG_RETURN_INT32(byte); +} + +/*------------------------------------------------------------- + * byteaGetBit + * + * This routine treats a "bytea" type like an array of bits. + * It returns the value of the Nth bit (0 or 1). + * + *------------------------------------------------------------- + */ +Datum +byteaGetBit(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + int64 n = PG_GETARG_INT64(1); + int byteNo, + bitNo; + int len; + int byte; + + len = VARSIZE_ANY_EXHDR(v); + + if (n < 0 || n >= (int64) len * 8) + ereport(ERROR, + (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), + errmsg("index %" PRId64 " out of valid range, 0..%" PRId64, + n, (int64) len * 8 - 1))); + + /* n/8 is now known < len, so safe to cast to int */ + byteNo = (int) (n / 8); + bitNo = (int) (n % 8); + + byte = ((unsigned char *) VARDATA_ANY(v))[byteNo]; + + if (byte & (1 << bitNo)) + PG_RETURN_INT32(1); + else + PG_RETURN_INT32(0); +} + +/*------------------------------------------------------------- + * byteaSetByte + * + * Given an instance of type 'bytea' creates a new one with + * the Nth byte set to the given value. + * + *------------------------------------------------------------- + */ +Datum +byteaSetByte(PG_FUNCTION_ARGS) +{ + bytea *res = PG_GETARG_BYTEA_P_COPY(0); + int32 n = PG_GETARG_INT32(1); + int32 newByte = PG_GETARG_INT32(2); + int len; + + len = VARSIZE(res) - VARHDRSZ; + + if (n < 0 || n >= len) + ereport(ERROR, + (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), + errmsg("index %d out of valid range, 0..%d", + n, len - 1))); + + /* + * Now set the byte. + */ + ((unsigned char *) VARDATA(res))[n] = newByte; + + PG_RETURN_BYTEA_P(res); +} + +/*------------------------------------------------------------- + * byteaSetBit + * + * Given an instance of type 'bytea' creates a new one with + * the Nth bit set to the given value. + * + *------------------------------------------------------------- + */ +Datum +byteaSetBit(PG_FUNCTION_ARGS) +{ + bytea *res = PG_GETARG_BYTEA_P_COPY(0); + int64 n = PG_GETARG_INT64(1); + int32 newBit = PG_GETARG_INT32(2); + int len; + int oldByte, + newByte; + int byteNo, + bitNo; + + len = VARSIZE(res) - VARHDRSZ; + + if (n < 0 || n >= (int64) len * 8) + ereport(ERROR, + (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), + errmsg("index %" PRId64 " out of valid range, 0..%" PRId64, + n, (int64) len * 8 - 1))); + + /* n/8 is now known < len, so safe to cast to int */ + byteNo = (int) (n / 8); + bitNo = (int) (n % 8); + + /* + * sanity check! + */ + if (newBit != 0 && newBit != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("new bit must be 0 or 1"))); + + /* + * Update the byte. + */ + oldByte = ((unsigned char *) VARDATA(res))[byteNo]; + + if (newBit == 0) + newByte = oldByte & (~(1 << bitNo)); + else + newByte = oldByte | (1 << bitNo); + + ((unsigned char *) VARDATA(res))[byteNo] = newByte; + + PG_RETURN_BYTEA_P(res); +} + +/* + * Return reversed bytea + */ +Datum +bytea_reverse(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + const char *p = VARDATA_ANY(v); + int len = VARSIZE_ANY_EXHDR(v); + const char *endp = p + len; + bytea *result = palloc(len + VARHDRSZ); + char *dst = (char *) VARDATA(result) + len; + + SET_VARSIZE(result, len + VARHDRSZ); + + while (p < endp) + *(--dst) = *p++; + + PG_RETURN_BYTEA_P(result); +} + + +/***************************************************************************** + * Comparison Functions used for bytea + * + * Note: btree indexes need these routines not to leak memory; therefore, + * be careful to free working copies of toasted datums. Most places don't + * need to be so careful. + *****************************************************************************/ + +Datum +byteaeq(PG_FUNCTION_ARGS) +{ + Datum arg1 = PG_GETARG_DATUM(0); + Datum arg2 = PG_GETARG_DATUM(1); + bool result; + Size len1, + len2; + + /* + * We can use a fast path for unequal lengths, which might save us from + * having to detoast one or both values. + */ + len1 = toast_raw_datum_size(arg1); + len2 = toast_raw_datum_size(arg2); + if (len1 != len2) + result = false; + else + { + bytea *barg1 = DatumGetByteaPP(arg1); + bytea *barg2 = DatumGetByteaPP(arg2); + + result = (memcmp(VARDATA_ANY(barg1), VARDATA_ANY(barg2), + len1 - VARHDRSZ) == 0); + + PG_FREE_IF_COPY(barg1, 0); + PG_FREE_IF_COPY(barg2, 1); + } + + PG_RETURN_BOOL(result); +} + +Datum +byteane(PG_FUNCTION_ARGS) +{ + Datum arg1 = PG_GETARG_DATUM(0); + Datum arg2 = PG_GETARG_DATUM(1); + bool result; + Size len1, + len2; + + /* + * We can use a fast path for unequal lengths, which might save us from + * having to detoast one or both values. + */ + len1 = toast_raw_datum_size(arg1); + len2 = toast_raw_datum_size(arg2); + if (len1 != len2) + result = true; + else + { + bytea *barg1 = DatumGetByteaPP(arg1); + bytea *barg2 = DatumGetByteaPP(arg2); + + result = (memcmp(VARDATA_ANY(barg1), VARDATA_ANY(barg2), + len1 - VARHDRSZ) != 0); + + PG_FREE_IF_COPY(barg1, 0); + PG_FREE_IF_COPY(barg2, 1); + } + + PG_RETURN_BOOL(result); +} + +Datum +bytealt(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + + PG_FREE_IF_COPY(arg1, 0); + PG_FREE_IF_COPY(arg2, 1); + + PG_RETURN_BOOL((cmp < 0) || ((cmp == 0) && (len1 < len2))); +} + +Datum +byteale(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + + PG_FREE_IF_COPY(arg1, 0); + PG_FREE_IF_COPY(arg2, 1); + + PG_RETURN_BOOL((cmp < 0) || ((cmp == 0) && (len1 <= len2))); +} + +Datum +byteagt(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + + PG_FREE_IF_COPY(arg1, 0); + PG_FREE_IF_COPY(arg2, 1); + + PG_RETURN_BOOL((cmp > 0) || ((cmp == 0) && (len1 > len2))); +} + +Datum +byteage(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + + PG_FREE_IF_COPY(arg1, 0); + PG_FREE_IF_COPY(arg2, 1); + + PG_RETURN_BOOL((cmp > 0) || ((cmp == 0) && (len1 >= len2))); +} + +Datum +byteacmp(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + if ((cmp == 0) && (len1 != len2)) + cmp = (len1 < len2) ? -1 : 1; + + PG_FREE_IF_COPY(arg1, 0); + PG_FREE_IF_COPY(arg2, 1); + + PG_RETURN_INT32(cmp); +} + +Datum +bytea_larger(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + bytea *result; + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + result = ((cmp > 0) || ((cmp == 0) && (len1 > len2)) ? arg1 : arg2); + + PG_RETURN_BYTEA_P(result); +} + +Datum +bytea_smaller(PG_FUNCTION_ARGS) +{ + bytea *arg1 = PG_GETARG_BYTEA_PP(0); + bytea *arg2 = PG_GETARG_BYTEA_PP(1); + bytea *result; + int len1, + len2; + int cmp; + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); + result = ((cmp < 0) || ((cmp == 0) && (len1 < len2)) ? arg1 : arg2); + + PG_RETURN_BYTEA_P(result); +} + +/* + * sortsupport comparison func + */ +static int +byteafastcmp(Datum x, Datum y, SortSupport ssup) +{ + bytea *arg1 = DatumGetByteaPP(x); + bytea *arg2 = DatumGetByteaPP(y); + char *a1p, + *a2p; + int len1, + len2, + result; + + a1p = VARDATA_ANY(arg1); + a2p = VARDATA_ANY(arg2); + + len1 = VARSIZE_ANY_EXHDR(arg1); + len2 = VARSIZE_ANY_EXHDR(arg2); + + result = memcmp(a1p, a2p, Min(len1, len2)); + if ((result == 0) && (len1 != len2)) + result = (len1 < len2) ? -1 : 1; + + /* We can't afford to leak memory here. */ + if (PointerGetDatum(arg1) != x) + pfree(arg1); + if (PointerGetDatum(arg2) != y) + pfree(arg2); + + return result; +} + +/* + * Conversion routine for sortsupport. Converts original to abbreviated key + * representation. Our encoding strategy is simple -- pack the first 8 bytes + * of the bytea data into a Datum (on little-endian machines, the bytes are + * stored in reverse order), and treat it as an unsigned integer. + */ +static Datum +bytea_abbrev_convert(Datum original, SortSupport ssup) +{ + const size_t max_prefix_bytes = sizeof(Datum); + ByteaSortSupport *bss = (ByteaSortSupport *) ssup->ssup_extra; + bytea *authoritative = DatumGetByteaPP(original); + char *authoritative_data = VARDATA_ANY(authoritative); + Datum res; + char *pres; + int len; + uint32 hash; + + pres = (char *) &res; + + /* memset(), so any non-overwritten bytes are NUL */ + memset(pres, 0, max_prefix_bytes); + len = VARSIZE_ANY_EXHDR(authoritative); + + /* + * Short byteas will have terminating NUL bytes in the abbreviated datum. + * Abbreviated comparison need not make a distinction between these NUL + * bytes, and NUL bytes representing actual NULs in the authoritative + * representation. + * + * Hopefully a comparison at or past one abbreviated key's terminating NUL + * byte will resolve the comparison without consulting the authoritative + * representation; specifically, some later non-NUL byte in the longer + * bytea can resolve the comparison against a subsequent terminating NUL + * in the shorter bytea. There will usually be what is effectively a + * "length-wise" resolution there and then. + * + * If that doesn't work out -- if all bytes in the longer bytea positioned + * at or past the offset of the smaller bytea (first) terminating NUL are + * actually representative of NUL bytes in the authoritative binary bytea + * (perhaps with some *terminating* NUL bytes towards the end of the + * longer bytea iff it happens to still be small) -- then an authoritative + * tie-breaker will happen, and do the right thing: explicitly consider + * bytea length. + */ + memcpy(pres, authoritative_data, Min(len, max_prefix_bytes)); + + /* + * Maintain approximate cardinality of both abbreviated keys and original, + * authoritative keys using HyperLogLog. Used as cheap insurance against + * the worst case, where we do many string abbreviations for no saving in + * full memcmp()-based comparisons. These statistics are used by + * bytea_abbrev_abort(). + * + * First, Hash key proper, or a significant fraction of it. Mix in length + * in order to compensate for cases where differences are past + * PG_CACHE_LINE_SIZE bytes, so as to limit the overhead of hashing. + */ + hash = DatumGetUInt32(hash_any((unsigned char *) authoritative_data, + Min(len, PG_CACHE_LINE_SIZE))); + + if (len > PG_CACHE_LINE_SIZE) + hash ^= DatumGetUInt32(hash_uint32((uint32) len)); + + addHyperLogLog(&bss->full_card, hash); + + /* Hash abbreviated key */ + { + uint32 tmp; + + tmp = DatumGetUInt32(res) ^ (uint32) (DatumGetUInt64(res) >> 32); + hash = DatumGetUInt32(hash_uint32(tmp)); + } + + addHyperLogLog(&bss->abbr_card, hash); + + /* + * Byteswap on little-endian machines. + * + * This is needed so that ssup_datum_unsigned_cmp() works correctly on all + * platforms. + */ + res = DatumBigEndianToNative(res); + + /* Don't leak memory here */ + if (PointerGetDatum(authoritative) != original) + pfree(authoritative); + + return res; +} + +/* + * Callback for estimating effectiveness of abbreviated key optimization, using + * heuristic rules. Returns value indicating if the abbreviation optimization + * should be aborted, based on its projected effectiveness. + * + * This is based on varstr_abbrev_abort(), but some comments have been elided + * for brevity. See there for more details. + */ +static bool +bytea_abbrev_abort(int memtupcount, SortSupport ssup) +{ + ByteaSortSupport *bss = (ByteaSortSupport *) ssup->ssup_extra; + double abbrev_distinct, + key_distinct; + + Assert(ssup->abbreviate); + + /* Have a little patience */ + if (memtupcount < 100) + return false; + + abbrev_distinct = estimateHyperLogLog(&bss->abbr_card); + key_distinct = estimateHyperLogLog(&bss->full_card); + + /* + * Clamp cardinality estimates to at least one distinct value. While + * NULLs are generally disregarded, if only NULL values were seen so far, + * that might misrepresent costs if we failed to clamp. + */ + if (abbrev_distinct < 1.0) + abbrev_distinct = 1.0; + + if (key_distinct < 1.0) + key_distinct = 1.0; + + if (trace_sort) + { + double norm_abbrev_card = abbrev_distinct / (double) memtupcount; + + elog(LOG, "bytea_abbrev: abbrev_distinct after %d: %f " + "(key_distinct: %f, norm_abbrev_card: %f, prop_card: %f)", + memtupcount, abbrev_distinct, key_distinct, norm_abbrev_card, + bss->prop_card); + } + + /* + * If the number of distinct abbreviated keys approximately matches the + * number of distinct original keys, continue with abbreviation. + */ + if (abbrev_distinct > key_distinct * bss->prop_card) + { + /* + * Decay required cardinality aggressively after 10,000 tuples. + */ + if (memtupcount > 10000) + bss->prop_card *= 0.65; + + return false; + } + + /* + * Abort abbreviation strategy. + */ + if (trace_sort) + elog(LOG, "bytea_abbrev: aborted abbreviation at %d " + "(abbrev_distinct: %f, key_distinct: %f, prop_card: %f)", + memtupcount, abbrev_distinct, key_distinct, bss->prop_card); + + return true; +} + +Datum +bytea_sortsupport(PG_FUNCTION_ARGS) +{ + SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0); + MemoryContext oldcontext; + + oldcontext = MemoryContextSwitchTo(ssup->ssup_cxt); + + ssup->comparator = byteafastcmp; + + /* + * Set up abbreviation support if requested. + */ + if (ssup->abbreviate) + { + ByteaSortSupport *bss; + + bss = palloc_object(ByteaSortSupport); + bss->abbreviate = true; + bss->prop_card = 0.20; + initHyperLogLog(&bss->abbr_card, 10); + initHyperLogLog(&bss->full_card, 10); + + ssup->ssup_extra = bss; + ssup->abbrev_full_comparator = ssup->comparator; + ssup->comparator = ssup_datum_unsigned_cmp; + ssup->abbrev_converter = bytea_abbrev_convert; + ssup->abbrev_abort = bytea_abbrev_abort; + } + + MemoryContextSwitchTo(oldcontext); + + PG_RETURN_VOID(); +} + +/* Cast bytea -> int2 */ +Datum +bytea_int2(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + int len = VARSIZE_ANY_EXHDR(v); + uint16 result; + + /* Check that the byte array is not too long */ + if (len > sizeof(result)) + ereturn(fcinfo->context, (Datum) 0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("smallint out of range")); + + /* Convert it to an integer; most significant bytes come first */ + result = 0; + for (int i = 0; i < len; i++) + { + result <<= BITS_PER_BYTE; + result |= ((unsigned char *) VARDATA_ANY(v))[i]; + } + + PG_RETURN_INT16(result); +} + +/* Cast bytea -> int4 */ +Datum +bytea_int4(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + int len = VARSIZE_ANY_EXHDR(v); + uint32 result; + + /* Check that the byte array is not too long */ + if (len > sizeof(result)) + ereturn(fcinfo->context, (Datum) 0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer out of range")); + + /* Convert it to an integer; most significant bytes come first */ + result = 0; + for (int i = 0; i < len; i++) + { + result <<= BITS_PER_BYTE; + result |= ((unsigned char *) VARDATA_ANY(v))[i]; + } + + PG_RETURN_INT32(result); +} + +/* Cast bytea -> int8 */ +Datum +bytea_int8(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + int len = VARSIZE_ANY_EXHDR(v); + uint64 result; + + /* Check that the byte array is not too long */ + if (len > sizeof(result)) + ereturn(fcinfo->context, (Datum) 0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("bigint out of range")); + + /* Convert it to an integer; most significant bytes come first */ + result = 0; + for (int i = 0; i < len; i++) + { + result <<= BITS_PER_BYTE; + result |= ((unsigned char *) VARDATA_ANY(v))[i]; + } + + PG_RETURN_INT64(result); +} + +/* Cast int2 -> bytea; can just use int2send() */ +Datum +int2_bytea(PG_FUNCTION_ARGS) +{ + return int2send(fcinfo); +} + +/* Cast int4 -> bytea; can just use int4send() */ +Datum +int4_bytea(PG_FUNCTION_ARGS) +{ + return int4send(fcinfo); +} + +/* Cast int8 -> bytea; can just use int8send() */ +Datum +int8_bytea(PG_FUNCTION_ARGS) +{ + return int8send(fcinfo); +} + +/* Cast bytea -> uuid */ +Datum +bytea_uuid(PG_FUNCTION_ARGS) +{ + bytea *v = PG_GETARG_BYTEA_PP(0); + int len = VARSIZE_ANY_EXHDR(v); + pg_uuid_t *uuid; + + if (len != UUID_LEN) + ereturn(fcinfo->context, (Datum) 0, + (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION), + errmsg("invalid input length for type %s", "uuid"), + errdetail("Expected %d bytes, got %d.", UUID_LEN, len))); + + uuid = palloc_object(pg_uuid_t); + memcpy(uuid->data, VARDATA_ANY(v), UUID_LEN); + PG_RETURN_UUID_P(uuid); +} + +/* Cast uuid -> bytea; can just use uuid_send() */ +Datum +uuid_bytea(PG_FUNCTION_ARGS) +{ + return uuid_send(fcinfo); +} diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c index 611d23f3cb0d8..f0487a60f0010 100644 --- a/src/backend/utils/adt/cash.c +++ b/src/backend/utils/adt/cash.c @@ -24,6 +24,7 @@ #include "common/int.h" #include "libpq/pqformat.h" +#include "nodes/miscnodes.h" #include "utils/builtins.h" #include "utils/cash.h" #include "utils/float.h" @@ -1035,7 +1036,7 @@ cash_words(PG_FUNCTION_ARGS) appendStringInfoString(&buf, m0 == 1 ? " cent" : " cents"); /* capitalize output */ - buf.data[0] = pg_toupper((unsigned char) buf.data[0]); + buf.data[0] = pg_ascii_toupper((unsigned char) buf.data[0]); /* return as text datum */ res = cstring_to_text_with_len(buf.data, buf.len); @@ -1106,12 +1107,12 @@ cash_numeric(PG_FUNCTION_ARGS) Datum numeric_cash(PG_FUNCTION_ARGS) { - Datum amount = PG_GETARG_DATUM(0); + Numeric amount = PG_GETARG_NUMERIC(0); Cash result; int fpoint; int64 scale; int i; - Datum numeric_scale; + Numeric numeric_scale; struct lconv *lconvert = PGLC_localeconv(); /* see comments about frac_digits in cash_in() */ @@ -1125,11 +1126,16 @@ numeric_cash(PG_FUNCTION_ARGS) scale *= 10; /* multiply the input amount by scale factor */ - numeric_scale = NumericGetDatum(int64_to_numeric(scale)); - amount = DirectFunctionCall2(numeric_mul, amount, numeric_scale); + numeric_scale = int64_to_numeric(scale); + + amount = numeric_mul_safe(amount, numeric_scale, fcinfo->context); + if (unlikely(SOFT_ERROR_OCCURRED(fcinfo->context))) + PG_RETURN_NULL(); /* note that numeric_int8 will round to nearest integer for us */ - result = DatumGetInt64(DirectFunctionCall1(numeric_int8, amount)); + result = numeric_int8_safe(amount, fcinfo->context); + if (unlikely(SOFT_ERROR_OCCURRED(fcinfo->context))) + PG_RETURN_NULL(); PG_RETURN_CASH(result); } @@ -1158,8 +1164,10 @@ int4_cash(PG_FUNCTION_ARGS) scale *= 10; /* compute amount * scale, checking for overflow */ - result = DatumGetInt64(DirectFunctionCall2(int8mul, Int64GetDatum(amount), - Int64GetDatum(scale))); + if (unlikely(pg_mul_s64_overflow(amount, scale, &result))) + ereturn(fcinfo->context, (Datum) 0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("bigint out of range")); PG_RETURN_CASH(result); } @@ -1188,8 +1196,10 @@ int8_cash(PG_FUNCTION_ARGS) scale *= 10; /* compute amount * scale, checking for overflow */ - result = DatumGetInt64(DirectFunctionCall2(int8mul, Int64GetDatum(amount), - Int64GetDatum(scale))); + if (unlikely(pg_mul_s64_overflow(amount, scale, &result))) + ereturn(fcinfo->context, (Datum) 0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("bigint out of range")); PG_RETURN_CASH(result); } diff --git a/src/backend/utils/adt/char.c b/src/backend/utils/adt/char.c index 22dbfc950b1fd..698863924ee23 100644 --- a/src/backend/utils/adt/char.c +++ b/src/backend/utils/adt/char.c @@ -4,7 +4,7 @@ * Functions for the built-in type "char" (not to be confused with * bpchar, which is the SQL CHAR(n) type). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -192,7 +192,7 @@ i4tochar(PG_FUNCTION_ARGS) int32 arg1 = PG_GETARG_INT32(0); if (arg1 < SCHAR_MIN || arg1 > SCHAR_MAX) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("\"char\" out of range"))); diff --git a/src/backend/utils/adt/cryptohashfuncs.c b/src/backend/utils/adt/cryptohashfuncs.c index 1cc7ddae3b56b..2561c68e6983a 100644 --- a/src/backend/utils/adt/cryptohashfuncs.c +++ b/src/backend/utils/adt/cryptohashfuncs.c @@ -3,7 +3,7 @@ * cryptohashfuncs.c * Cryptographic hash functions * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/utils/adt/date.c b/src/backend/utils/adt/date.c index 4227ab1a72bfb..c332744038038 100644 --- a/src/backend/utils/adt/date.c +++ b/src/backend/utils/adt/date.c @@ -3,7 +3,7 @@ * date.c * implements DATE and TIME data types specified in SQL standard * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * @@ -27,6 +27,7 @@ #include "common/int.h" #include "libpq/pqformat.h" #include "miscadmin.h" +#include "nodes/miscnodes.h" #include "nodes/supportnodes.h" #include "parser/scansup.h" #include "utils/array.h" @@ -37,14 +38,6 @@ #include "utils/skipsupport.h" #include "utils/sortsupport.h" -/* - * gcc's -ffast-math switch breaks routines that expect exact results from - * expressions like timeval / SECS_PER_HOUR, where timeval is double. - */ -#ifdef __FAST_MATH__ -#error -ffast-math is known to break this code -#endif - /* common code for timetypmodin and timetztypmodin */ static int32 @@ -357,7 +350,7 @@ GetSQLCurrentTime(int32 typmod) GetCurrentTimeUsec(tm, &fsec, &tz); - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); tm2timetz(tm, fsec, tz, result); AdjustTimeForTypmod(&(result->time), typmod); return result; @@ -615,24 +608,21 @@ date_mii(PG_FUNCTION_ARGS) /* * Promote date to timestamp. * - * On successful conversion, *overflow is set to zero if it's not NULL. + * If the date falls out of the valid range for the timestamp type, error + * handling proceeds based on escontext. * - * If the date is finite but out of the valid range for timestamp, then: - * if overflow is NULL, we throw an out-of-range error. - * if overflow is not NULL, we store +1 or -1 there to indicate the sign - * of the overflow, and return the appropriate timestamp infinity. + * If escontext is NULL, we throw an out-of-range error (hard error). + * If escontext is not NULL, we return NOBEGIN or NOEND for lower bound or + * upper bound overflow, respectively, and record a soft error. * - * Note: *overflow = -1 is actually not possible currently, since both - * datatypes have the same lower bound, Julian day zero. + * Note: Lower bound overflow is currently not possible, as both date and + * timestamp datatypes share the same lower boundary: Julian day zero. */ Timestamp -date2timestamp_opt_overflow(DateADT dateVal, int *overflow) +date2timestamp_safe(DateADT dateVal, Node *escontext) { Timestamp result; - if (overflow) - *overflow = 0; - if (DATE_IS_NOBEGIN(dateVal)) TIMESTAMP_NOBEGIN(result); else if (DATE_IS_NOEND(dateVal)) @@ -645,18 +635,10 @@ date2timestamp_opt_overflow(DateADT dateVal, int *overflow) */ if (dateVal >= (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE)) { - if (overflow) - { - *overflow = 1; - TIMESTAMP_NOEND(result); - return result; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range for timestamp"))); - } + TIMESTAMP_NOEND(result); + ereturn(escontext, result, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("date out of range for timestamp"))); } /* date is days since 2000, timestamp is microseconds since same... */ @@ -672,30 +654,27 @@ date2timestamp_opt_overflow(DateADT dateVal, int *overflow) static TimestampTz date2timestamp(DateADT dateVal) { - return date2timestamp_opt_overflow(dateVal, NULL); + return date2timestamp_safe(dateVal, NULL); } /* * Promote date to timestamp with time zone. * - * On successful conversion, *overflow is set to zero if it's not NULL. + * If the date falls out of the valid range for the timestamp type, error + * handling proceeds based on escontext. * - * If the date is finite but out of the valid range for timestamptz, then: - * if overflow is NULL, we throw an out-of-range error. - * if overflow is not NULL, we store +1 or -1 there to indicate the sign - * of the overflow, and return the appropriate timestamptz infinity. + * If escontext is NULL, we throw an out-of-range error (hard error). + * If escontext is not NULL, we return NOBEGIN or NOEND for lower bound or + * upper bound overflow, respectively, and record a soft error. */ TimestampTz -date2timestamptz_opt_overflow(DateADT dateVal, int *overflow) +date2timestamptz_safe(DateADT dateVal, Node *escontext) { TimestampTz result; struct pg_tm tt, *tm = &tt; int tz; - if (overflow) - *overflow = 0; - if (DATE_IS_NOBEGIN(dateVal)) TIMESTAMP_NOBEGIN(result); else if (DATE_IS_NOEND(dateVal)) @@ -708,18 +687,10 @@ date2timestamptz_opt_overflow(DateADT dateVal, int *overflow) */ if (dateVal >= (TIMESTAMP_END_JULIAN - POSTGRES_EPOCH_JDATE)) { - if (overflow) - { - *overflow = 1; - TIMESTAMP_NOEND(result); - return result; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range for timestamp"))); - } + TIMESTAMP_NOEND(result); + ereturn(escontext, result, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("date out of range for timestamp"))); } j2date(dateVal + POSTGRES_EPOCH_JDATE, @@ -737,40 +708,20 @@ date2timestamptz_opt_overflow(DateADT dateVal, int *overflow) */ if (!IS_VALID_TIMESTAMP(result)) { - if (overflow) - { - if (result < MIN_TIMESTAMP) - { - *overflow = -1; - TIMESTAMP_NOBEGIN(result); - } - else - { - *overflow = 1; - TIMESTAMP_NOEND(result); - } - } + if (result < MIN_TIMESTAMP) + TIMESTAMP_NOBEGIN(result); else - { - ereport(ERROR, - (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range for timestamp"))); - } + TIMESTAMP_NOEND(result); + + ereturn(escontext, result, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("date out of range for timestamp"))); } } return result; } -/* - * Promote date to timestamptz, throwing error for overflow. - */ -static TimestampTz -date2timestamptz(DateADT dateVal) -{ - return date2timestamptz_opt_overflow(dateVal, NULL); -} - /* * date2timestamp_no_overflow * @@ -808,15 +759,16 @@ int32 date_cmp_timestamp_internal(DateADT dateVal, Timestamp dt2) { Timestamp dt1; - int overflow; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - dt1 = date2timestamp_opt_overflow(dateVal, &overflow); - if (overflow > 0) + dt1 = date2timestamp_safe(dateVal, (Node *) &escontext); + if (escontext.error_occurred) { + Assert(TIMESTAMP_IS_NOEND(dt1)); /* NOBEGIN case cannot occur */ + /* dt1 is larger than any finite timestamp, but less than infinity */ return TIMESTAMP_IS_NOEND(dt2) ? -1 : +1; } - Assert(overflow == 0); /* -1 case cannot occur */ return timestamp_cmp_internal(dt1, dt2); } @@ -888,18 +840,22 @@ int32 date_cmp_timestamptz_internal(DateADT dateVal, TimestampTz dt2) { TimestampTz dt1; - int overflow; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - dt1 = date2timestamptz_opt_overflow(dateVal, &overflow); - if (overflow > 0) - { - /* dt1 is larger than any finite timestamp, but less than infinity */ - return TIMESTAMP_IS_NOEND(dt2) ? -1 : +1; - } - if (overflow < 0) + dt1 = date2timestamptz_safe(dateVal, (Node *) &escontext); + + if (escontext.error_occurred) { - /* dt1 is less than any finite timestamp, but more than -infinity */ - return TIMESTAMP_IS_NOBEGIN(dt2) ? +1 : -1; + if (TIMESTAMP_IS_NOEND(dt1)) + { + /* dt1 is larger than any finite timestamp, but less than infinity */ + return TIMESTAMP_IS_NOEND(dt2) ? -1 : +1; + } + if (TIMESTAMP_IS_NOBEGIN(dt1)) + { + /* dt1 is less than any finite timestamp, but more than -infinity */ + return TIMESTAMP_IS_NOBEGIN(dt2) ? +1 : -1; + } } return timestamptz_cmp_internal(dt1, dt2); @@ -1350,7 +1306,9 @@ date_timestamp(PG_FUNCTION_ARGS) DateADT dateVal = PG_GETARG_DATEADT(0); Timestamp result; - result = date2timestamp(dateVal); + result = date2timestamp_safe(dateVal, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_TIMESTAMP(result); } @@ -1363,6 +1321,31 @@ timestamp_date(PG_FUNCTION_ARGS) { Timestamp timestamp = PG_GETARG_TIMESTAMP(0); DateADT result; + + result = timestamp2date_safe(timestamp, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); + + PG_RETURN_DATEADT(result); +} + +/* + * Convert timestamp to date. + * + * If the timestamp falls out of the valid range for the date type, error + * handling proceeds based on escontext. + * + * If escontext is NULL, we throw an out-of-range error (hard error). + * If escontext is not NULL, we return NOBEGIN or NOEND for lower bound or + * upper bound overflow, respectively, and record a soft error. + * + * Note: given the ranges of the types, overflow is only possible at + * the lower bound of the range, but we don't assume that in this code. + */ +DateADT +timestamp2date_safe(Timestamp timestamp, Node *escontext) +{ + DateADT result; struct pg_tm tt, *tm = &tt; fsec_t fsec; @@ -1374,14 +1357,21 @@ timestamp_date(PG_FUNCTION_ARGS) else { if (timestamp2tm(timestamp, NULL, tm, &fsec, NULL, NULL) != 0) - ereport(ERROR, + { + if (timestamp < 0) + DATE_NOBEGIN(result); + else + DATE_NOEND(result); /* not actually reachable */ + + ereturn(escontext, result, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); + } result = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE; } - PG_RETURN_DATEADT(result); + return result; } @@ -1394,7 +1384,9 @@ date_timestamptz(PG_FUNCTION_ARGS) DateADT dateVal = PG_GETARG_DATEADT(0); TimestampTz result; - result = date2timestamptz(dateVal); + result = date2timestamptz_safe(dateVal, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_TIMESTAMP(result); } @@ -1408,6 +1400,31 @@ timestamptz_date(PG_FUNCTION_ARGS) { TimestampTz timestamp = PG_GETARG_TIMESTAMP(0); DateADT result; + + result = timestamptz2date_safe(timestamp, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); + + PG_RETURN_DATEADT(result); +} + +/* + * Convert timestamptz to date. + * + * If the timestamp falls out of the valid range for the date type, error + * handling proceeds based on escontext. + * + * If escontext is NULL, we throw an out-of-range error (hard error). + * If escontext is not NULL, we return NOBEGIN or NOEND for lower bound or + * upper bound overflow, respectively, and record a soft error. + * + * Note: given the ranges of the types, overflow is only possible at + * the lower bound of the range, but we don't assume that in this code. + */ +DateADT +timestamptz2date_safe(TimestampTz timestamp, Node *escontext) +{ + DateADT result; struct pg_tm tt, *tm = &tt; fsec_t fsec; @@ -1420,14 +1437,21 @@ timestamptz_date(PG_FUNCTION_ARGS) else { if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0) - ereport(ERROR, + { + if (timestamp < 0) + DATE_NOBEGIN(result); + else + DATE_NOEND(result); /* not actually reachable */ + + ereturn(escontext, result, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); + } result = date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) - POSTGRES_EPOCH_JDATE; } - PG_RETURN_DATEADT(result); + return result; } @@ -1979,7 +2003,7 @@ timestamp_time(PG_FUNCTION_ARGS) PG_RETURN_NULL(); if (timestamp2tm(timestamp, NULL, tm, &fsec, NULL, NULL) != 0) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); @@ -2010,7 +2034,7 @@ timestamptz_time(PG_FUNCTION_ARGS) PG_RETURN_NULL(); if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); @@ -2056,7 +2080,7 @@ time_interval(PG_FUNCTION_ARGS) TimeADT time = PG_GETARG_TIMEADT(0); Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); result->time = time; result->day = 0; @@ -2080,7 +2104,7 @@ interval_time(PG_FUNCTION_ARGS) TimeADT result; if (INTERVAL_NOT_FINITE(span)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("cannot convert infinite interval to time"))); @@ -2101,7 +2125,7 @@ time_mi_time(PG_FUNCTION_ARGS) TimeADT time2 = PG_GETARG_TIMEADT(1); Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); result->month = 0; result->day = 0; @@ -2368,7 +2392,7 @@ timetz_in(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); tm2timetz(tm, fsec, tz, result); AdjustTimeForTypmod(&(result->time), typmod); @@ -2407,7 +2431,7 @@ timetz_recv(PG_FUNCTION_ARGS) int32 typmod = PG_GETARG_INT32(2); TimeTzADT *result; - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = pq_getmsgint64(buf); @@ -2493,7 +2517,7 @@ timetz_scale(PG_FUNCTION_ARGS) int32 typmod = PG_GETARG_INT32(1); TimeTzADT *result; - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = time->time; result->zone = time->zone; @@ -2669,7 +2693,7 @@ timetz_pl_interval(PG_FUNCTION_ARGS) (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("cannot add infinite interval to time"))); - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = time->time + span->time; result->time -= result->time / USECS_PER_DAY * USECS_PER_DAY; @@ -2696,7 +2720,7 @@ timetz_mi_interval(PG_FUNCTION_ARGS) (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("cannot subtract infinite interval from time"))); - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = time->time - span->time; result->time -= result->time / USECS_PER_DAY * USECS_PER_DAY; @@ -2903,7 +2927,7 @@ time_timetz(PG_FUNCTION_ARGS) time2tm(time, tm, &fsec); tz = DetermineTimeZoneOffset(tm, session_timezone); - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = time; result->zone = tz; @@ -2929,11 +2953,11 @@ timestamptz_timetz(PG_FUNCTION_ARGS) PG_RETURN_NULL(); if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); tm2timetz(tm, fsec, tz, result); @@ -3166,7 +3190,7 @@ timetz_zone(PG_FUNCTION_ARGS) errmsg("timestamp out of range"))); } - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = t->time + (t->zone - tz) * USECS_PER_SEC; /* C99 modulo has the wrong sign convention for negative input */ @@ -3207,7 +3231,7 @@ timetz_izone(PG_FUNCTION_ARGS) tz = -(zone->time / USECS_PER_SEC); - result = (TimeTzADT *) palloc(sizeof(TimeTzADT)); + result = palloc_object(TimeTzADT); result->time = time->time + (time->zone - tz) * USECS_PER_SEC; /* C99 modulo has the wrong sign convention for negative input */ diff --git a/src/backend/utils/adt/datetime.c b/src/backend/utils/adt/datetime.c index 793d8a9adccdc..8f25c15fcfc33 100644 --- a/src/backend/utils/adt/datetime.c +++ b/src/backend/utils/adt/datetime.c @@ -3,7 +3,7 @@ * datetime.c * Support functions for date/time types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -30,6 +30,7 @@ #include "utils/date.h" #include "utils/datetime.h" #include "utils/guc.h" +#include "utils/tuplestore.h" #include "utils/tzparser.h" static int DecodeNumber(int flen, char *str, bool haveTextMonth, @@ -702,9 +703,18 @@ ParseFraction(char *cp, double *frac) } else { + /* + * On the other hand, let's reject anything that's not digits after + * the ".". strtod is happy with input like ".123e9", but that'd + * break callers' expectation that the result is in 0..1. (It's quite + * difficult to get here with such input, but not impossible.) + */ + if (strspn(cp + 1, "0123456789") != strlen(cp + 1)) + return DTERR_BAD_FORMAT; + errno = 0; *frac = strtod(cp, &cp); - /* check for parse failure */ + /* check for parse failure (probably redundant given prior check) */ if (*cp != '\0' || errno != 0) return DTERR_BAD_FORMAT; } @@ -2958,31 +2968,28 @@ DecodeNumberField(int len, char *str, int fmask, { char *cp; + /* + * This function was originally meant to cope only with DTK_NUMBER fields, + * but we now sometimes abuse it to parse (parts of) DTK_DATE fields, + * which can contain letters and other punctuation. Reject if it's not a + * valid DTK_NUMBER, that is digits and decimal point(s). (ParseFraction + * will reject if there's more than one decimal point.) + */ + if (strspn(str, "0123456789.") != len) + return DTERR_BAD_FORMAT; + /* * Have a decimal point? Then this is a date or something with a seconds * field... */ if ((cp = strchr(str, '.')) != NULL) { - /* - * Can we use ParseFractionalSecond here? Not clear whether trailing - * junk should be rejected ... - */ - if (cp[1] == '\0') - { - /* avoid assuming that strtod will accept "." */ - *fsec = 0; - } - else - { - double frac; + int dterr; - errno = 0; - frac = strtod(cp, NULL); - if (errno != 0) - return DTERR_BAD_FORMAT; - *fsec = rint(frac * 1000000); - } + /* Convert the fraction and store at *fsec */ + dterr = ParseFractionalSecond(cp, fsec); + if (dterr) + return dterr; /* Now truncate off the fraction for further processing */ *cp = '\0'; len = strlen(str); @@ -3588,7 +3595,7 @@ DecodeInterval(char **field, int *ftype, int nf, int range, * handle signed float numbers and signed year-month values. */ - /* FALLTHROUGH */ + pg_fallthrough; case DTK_DATE: case DTK_NUMBER: @@ -4022,7 +4029,7 @@ DecodeISO8601Interval(char *str, continue; } /* Else fall through to extended alternative format */ - /* FALLTHROUGH */ + pg_fallthrough; case '-': /* ISO 8601 4.4.3.3 Alternative Format, * Extended */ if (havefield) @@ -4105,7 +4112,7 @@ DecodeISO8601Interval(char *str, return 0; } /* Else fall through to extended alternative format */ - /* FALLTHROUGH */ + pg_fallthrough; case ':': /* ISO 8601 4.4.3.3 Alternative Format, * Extended */ if (havefield) @@ -5146,7 +5153,7 @@ pg_timezone_abbrevs_zone(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - pindex = (int *) palloc(sizeof(int)); + pindex = palloc_object(int); *pindex = 0; funcctx->user_fctx = pindex; @@ -5181,7 +5188,7 @@ pg_timezone_abbrevs_zone(PG_FUNCTION_ARGS) /* Convert offset (in seconds) to an interval; can't overflow */ MemSet(&itm_in, 0, sizeof(struct pg_itm_in)); itm_in.tm_usec = (int64) gmtoff * USECS_PER_SEC; - resInterval = (Interval *) palloc(sizeof(Interval)); + resInterval = palloc_object(Interval); (void) itmin2interval(&itm_in, resInterval); values[1] = IntervalPGetDatum(resInterval); @@ -5233,7 +5240,7 @@ pg_timezone_abbrevs_abbrevs(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - pindex = (int *) palloc(sizeof(int)); + pindex = palloc_object(int); *pindex = 0; funcctx->user_fctx = pindex; @@ -5304,7 +5311,7 @@ pg_timezone_abbrevs_abbrevs(PG_FUNCTION_ARGS) /* Convert offset (in seconds) to an interval; can't overflow */ MemSet(&itm_in, 0, sizeof(struct pg_itm_in)); itm_in.tm_usec = (int64) gmtoffset * USECS_PER_SEC; - resInterval = (Interval *) palloc(sizeof(Interval)); + resInterval = palloc_object(Interval); (void) itmin2interval(&itm_in, resInterval); values[1] = IntervalPGetDatum(resInterval); @@ -5372,7 +5379,7 @@ pg_timezone_names(PG_FUNCTION_ARGS) /* Convert tzoff to an interval; can't overflow */ MemSet(&itm_in, 0, sizeof(struct pg_itm_in)); itm_in.tm_usec = (int64) -tzoff * USECS_PER_SEC; - resInterval = (Interval *) palloc(sizeof(Interval)); + resInterval = palloc_object(Interval); (void) itmin2interval(&itm_in, resInterval); values[2] = IntervalPGetDatum(resInterval); diff --git a/src/backend/utils/adt/datum.c b/src/backend/utils/adt/datum.c index fcd5b1653dd3e..1d622e31a8329 100644 --- a/src/backend/utils/adt/datum.c +++ b/src/backend/utils/adt/datum.c @@ -3,7 +3,7 @@ * datum.c * POSTGRES Datum (abstract data type) manipulation routines. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -26,7 +26,7 @@ * The number of significant bytes are always equal to the typlen. * * C) if a type is not "byVal" and has typlen == -1, - * then the "Datum" always points to a "struct varlena". + * then the "Datum" always points to a "varlena". * This varlena structure has information about the actual length of this * particular instance of the type and about its value. * @@ -82,9 +82,9 @@ datumGetSize(Datum value, bool typByVal, int typLen) else if (typLen == -1) { /* It is a varlena datatype */ - struct varlena *s = (struct varlena *) DatumGetPointer(value); + varlena *s = (varlena *) DatumGetPointer(value); - if (!PointerIsValid(s)) + if (!s) ereport(ERROR, (errcode(ERRCODE_DATA_EXCEPTION), errmsg("invalid Datum pointer"))); @@ -96,7 +96,7 @@ datumGetSize(Datum value, bool typByVal, int typLen) /* It is a cstring datatype */ char *s = (char *) DatumGetPointer(value); - if (!PointerIsValid(s)) + if (!s) ereport(ERROR, (errcode(ERRCODE_DATA_EXCEPTION), errmsg("invalid Datum pointer"))); @@ -138,7 +138,7 @@ datumCopy(Datum value, bool typByVal, int typLen) else if (typLen == -1) { /* It is a varlena datatype */ - struct varlena *vl = (struct varlena *) DatumGetPointer(value); + varlena *vl = (varlena *) DatumGetPointer(value); if (VARATT_IS_EXTERNAL_EXPANDED(vl)) { @@ -258,8 +258,13 @@ datumIsEqual(Datum value1, Datum value2, bool typByVal, int typLen) /*------------------------------------------------------------------------- * datum_image_eq * - * Compares two datums for identical contents, based on byte images. Return - * true if the two datums are equal, false otherwise. + * Compares two datums for identical contents when coerced to a signed integer + * of typLen bytes. Return true if the two datums are equal, false otherwise. + * + * The coercion is required as we're not always careful to use the correct + * PG_RETURN_* macro. If we didn't do this, a Datum that's been formed and + * deformed into a tuple may not have the same signed representation as the + * other datum value. *------------------------------------------------------------------------- */ bool @@ -271,7 +276,21 @@ datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen) if (typByVal) { - result = (value1 == value2); + switch (typLen) + { + case sizeof(char): + result = (DatumGetChar(value1) == DatumGetChar(value2)); + break; + case sizeof(int16): + result = (DatumGetInt16(value1) == DatumGetInt16(value2)); + break; + case sizeof(int32): + result = (DatumGetInt32(value1) == DatumGetInt32(value2)); + break; + default: + result = (value1 == value2); + break; + } } else if (typLen > 0) { @@ -288,8 +307,8 @@ datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen) result = false; else { - struct varlena *arg1val; - struct varlena *arg2val; + varlena *arg1val; + varlena *arg2val; arg1val = PG_DETOAST_DATUM_PACKED(value1); arg2val = PG_DETOAST_DATUM_PACKED(value2); @@ -299,9 +318,9 @@ datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen) len1 - VARHDRSZ) == 0); /* Only free memory if it's a copy made here. */ - if ((Pointer) arg1val != (Pointer) value1) + if (arg1val != DatumGetPointer(value1)) pfree(arg1val); - if ((Pointer) arg2val != (Pointer) value2) + if (arg2val != DatumGetPointer(value2)) pfree(arg2val); } } @@ -328,10 +347,11 @@ datum_image_eq(Datum value1, Datum value2, bool typByVal, int typLen) /*------------------------------------------------------------------------- * datum_image_hash * - * Generate a hash value based on the binary representation of 'value'. Most - * use cases will want to use the hash function specific to the Datum's type, - * however, some corner cases require generating a hash value based on the - * actual bits rather than the logical value. + * Generate a hash value based on the binary representation of 'value' when + * represented as a signed integer of typLen bytes. Most use cases will want + * to use the hash function specific to the Datum's type, however, some corner + * cases require generating a hash value based on the actual bits rather than + * the logical value. *------------------------------------------------------------------------- */ uint32 @@ -341,12 +361,28 @@ datum_image_hash(Datum value, bool typByVal, int typLen) uint32 result; if (typByVal) + { + switch (typLen) + { + case sizeof(char): + value = CharGetDatum(DatumGetChar(value)); + break; + case sizeof(int16): + value = Int16GetDatum(DatumGetInt16(value)); + break; + case sizeof(int32): + value = Int32GetDatum(DatumGetInt32(value)); + break; + /* Nothing needs done for 64-bit types */ + } + result = hash_bytes((unsigned char *) &value, sizeof(Datum)); + } else if (typLen > 0) result = hash_bytes((unsigned char *) DatumGetPointer(value), typLen); else if (typLen == -1) { - struct varlena *val; + varlena *val; len = toast_raw_datum_size(value); @@ -355,7 +391,7 @@ datum_image_hash(Datum value, bool typByVal, int typLen) result = hash_bytes((unsigned char *) VARDATA_ANY(val), len - VARHDRSZ); /* Only free memory if it's a copy made here. */ - if ((Pointer) val != (Pointer) value) + if (val != DatumGetPointer(value)) pfree(val); } else if (typLen == -2) @@ -396,7 +432,9 @@ datum_image_hash(Datum value, bool typByVal, int typLen) Datum btequalimage(PG_FUNCTION_ARGS) { - /* Oid opcintype = PG_GETARG_OID(0); */ +#ifdef NOT_USED + Oid opcintype = PG_GETARG_OID(0); +#endif PG_RETURN_BOOL(true); } diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c index 25865b660ef83..cccc4a24c8405 100644 --- a/src/backend/utils/adt/dbsize.c +++ b/src/backend/utils/adt/dbsize.c @@ -2,7 +2,7 @@ * dbsize.c * Database object size functions, and related inquiries * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/dbsize.c @@ -19,12 +19,12 @@ #include "catalog/pg_authid.h" #include "catalog/pg_database.h" #include "catalog/pg_tablespace.h" -#include "commands/dbcommands.h" #include "commands/tablespace.h" #include "miscadmin.h" #include "storage/fd.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/lsyscache.h" #include "utils/numeric.h" #include "utils/rel.h" #include "utils/relfilenumbermap.h" @@ -938,6 +938,9 @@ pg_relation_filenode(PG_FUNCTION_ARGS) * * We don't fail but return NULL if we cannot find a mapping. * + * Temporary relations are not detected, returning NULL (see + * RelidByRelfilenumber() for the reasons). + * * InvalidOid can be passed instead of the current database's default * tablespace. */ diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c new file mode 100644 index 0000000000000..d83cda3342e2d --- /dev/null +++ b/src/backend/utils/adt/ddlutils.c @@ -0,0 +1,1180 @@ +/*------------------------------------------------------------------------- + * + * ddlutils.c + * Utility functions for generating DDL statements + * + * This file contains the pg_get_*_ddl family of functions that generate + * DDL statements to recreate database objects such as roles, tablespaces, + * and databases, along with common infrastructure for option parsing and + * pretty-printing. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/utils/adt/ddlutils.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/htup_details.h" +#include "access/table.h" +#include "catalog/pg_auth_members.h" +#include "catalog/pg_authid.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_database.h" +#include "catalog/pg_db_role_setting.h" +#include "catalog/pg_tablespace.h" +#include "commands/tablespace.h" +#include "common/relpath.h" +#include "funcapi.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "utils/acl.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/datetime.h" +#include "utils/fmgroids.h" +#include "utils/guc.h" +#include "utils/lsyscache.h" +#include "utils/pg_locale.h" +#include "utils/rel.h" +#include "utils/ruleutils.h" +#include "utils/syscache.h" +#include "utils/timestamp.h" +#include "utils/varlena.h" + +/* Option value types for DDL option parsing */ +typedef enum +{ + DDL_OPT_BOOL, + DDL_OPT_TEXT, + DDL_OPT_INT, +} DdlOptType; + +/* + * A single DDL option descriptor: caller fills in name and type, + * parse_ddl_options fills in isset + the appropriate value field. + */ +typedef struct DdlOption +{ + const char *name; /* option name (case-insensitive match) */ + DdlOptType type; /* expected value type */ + bool isset; /* true if caller supplied this option */ + /* fields for specific option types */ + union + { + bool boolval; /* filled in for DDL_OPT_BOOL */ + char *textval; /* filled in for DDL_OPT_TEXT (palloc'd) */ + int intval; /* filled in for DDL_OPT_INT */ + }; +} DdlOption; + + +static void parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start, + DdlOption *opts, int nopts); +static void append_ddl_option(StringInfo buf, bool pretty, int indent, + const char *fmt,...) + pg_attribute_printf(4, 5); +static void append_guc_value(StringInfo buf, const char *name, + const char *value); +static List *pg_get_role_ddl_internal(Oid roleid, bool pretty, + bool memberships); +static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner); +static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull); +static List *pg_get_database_ddl_internal(Oid dbid, bool pretty, + bool no_owner, bool no_tablespace); + + +/* + * parse_ddl_options + * Parse variadic name/value option pairs + * + * Options are passed as alternating key/value text pairs. The caller + * provides an array of DdlOption descriptors specifying the accepted + * option names and their types; this function matches each supplied + * pair against the array, validates the value, and fills in the + * result fields. + */ +static void +parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start, + DdlOption *opts, int nopts) +{ + Datum *args; + bool *nulls; + Oid *types; + int nargs; + + /* Clear all output fields */ + for (int i = 0; i < nopts; i++) + { + opts[i].isset = false; + switch (opts[i].type) + { + case DDL_OPT_BOOL: + opts[i].boolval = false; + break; + case DDL_OPT_TEXT: + opts[i].textval = NULL; + break; + case DDL_OPT_INT: + opts[i].intval = 0; + break; + } + } + + nargs = extract_variadic_args(fcinfo, variadic_start, true, + &args, &types, &nulls); + + if (nargs <= 0) + return; + + /* Handle DEFAULT NULL case */ + if (nargs == 1 && nulls[0]) + return; + + if (nargs % 2 != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("variadic arguments must be name/value pairs"), + errhint("Provide an even number of variadic arguments that can be divided into pairs."))); + + /* + * For each option name/value pair, find corresponding positional option + * for the option name, and assign the option value. + */ + for (int i = 0; i < nargs; i += 2) + { + char *name; + char *valstr; + DdlOption *opt = NULL; + + if (nulls[i]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("option name at variadic position %d is null", i + 1))); + + name = TextDatumGetCString(args[i]); + + if (nulls[i + 1]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("value for option \"%s\" must not be null", name))); + + /* Find matching option descriptor */ + for (int j = 0; j < nopts; j++) + { + if (pg_strcasecmp(name, opts[j].name) == 0) + { + opt = &opts[j]; + break; + } + } + + if (opt == NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized option: \"%s\"", name))); + + if (opt->isset) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("option \"%s\" is specified more than once", + name))); + + valstr = TextDatumGetCString(args[i + 1]); + + switch (opt->type) + { + case DDL_OPT_BOOL: + if (!parse_bool(valstr, &opt->boolval)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for boolean option \"%s\": %s", + name, valstr))); + break; + + case DDL_OPT_TEXT: + opt->textval = valstr; + valstr = NULL; /* don't pfree below */ + break; + + case DDL_OPT_INT: + { + char *endp; + long val; + + errno = 0; + val = strtol(valstr, &endp, 10); + if (*endp != '\0' || errno == ERANGE || + val < PG_INT32_MIN || val > PG_INT32_MAX) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid value for integer option \"%s\": %s", + name, valstr))); + opt->intval = (int) val; + } + break; + } + + opt->isset = true; + + if (valstr) + pfree(valstr); + pfree(name); + } +} + +/* + * Helper to append a formatted string with optional pretty-printing. + */ +static void +append_ddl_option(StringInfo buf, bool pretty, int indent, + const char *fmt,...) +{ + if (pretty) + { + appendStringInfoChar(buf, '\n'); + appendStringInfoSpaces(buf, indent); + } + else + appendStringInfoChar(buf, ' '); + + for (;;) + { + va_list args; + int needed; + + va_start(args, fmt); + needed = appendStringInfoVA(buf, fmt, args); + va_end(args); + if (needed == 0) + break; + enlargeStringInfo(buf, needed); + } +} + +/* + * append_guc_value + * Append a GUC setting value to buf, handling GUC_LIST_QUOTE properly. + * + * Variables marked GUC_LIST_QUOTE were already fully quoted before they + * were stored in the setconfig array. We break the list value apart + * and re-quote the elements as string literals. For all other variables + * we simply quote the value as a single string literal. + * + * The caller has already appended "SET TO " to buf. + */ +static void +append_guc_value(StringInfo buf, const char *name, const char *value) +{ + char *rawval; + + rawval = pstrdup(value); + + if (GetConfigOptionFlags(name, true) & GUC_LIST_QUOTE) + { + List *namelist; + bool first = true; + + /* Parse string into list of identifiers */ + if (!SplitGUCList(rawval, ',', &namelist)) + { + /* this shouldn't fail really */ + elog(ERROR, "invalid list syntax in setconfig item"); + } + /* Special case: represent an empty list as NULL */ + if (namelist == NIL) + appendStringInfoString(buf, "NULL"); + foreach_ptr(char, curname, namelist) + { + if (first) + first = false; + else + appendStringInfoString(buf, ", "); + appendStringInfoString(buf, quote_literal_cstr(curname)); + } + list_free(namelist); + } + else + appendStringInfoString(buf, quote_literal_cstr(rawval)); + + pfree(rawval); +} + +/* + * pg_get_role_ddl_internal + * Generate DDL statements to recreate a role + * + * Returns a List of palloc'd strings, each being a complete SQL statement. + * The first list element is always the CREATE ROLE statement; subsequent + * elements are ALTER ROLE SET statements for any role-specific or + * role-in-database configuration settings. If memberships is true, + * GRANT statements for role memberships are appended. + */ +static List * +pg_get_role_ddl_internal(Oid roleid, bool pretty, bool memberships) +{ + HeapTuple tuple; + Form_pg_authid roleform; + StringInfoData buf; + char *rolname; + Datum rolevaliduntil; + bool isnull; + Relation rel; + ScanKeyData scankey; + SysScanDesc scan; + List *statements = NIL; + + tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("role with OID %u does not exist", roleid))); + + roleform = (Form_pg_authid) GETSTRUCT(tuple); + rolname = pstrdup(NameStr(roleform->rolname)); + + /* User must have SELECT privilege on pg_authid. */ + if (pg_class_aclcheck(AuthIdRelationId, GetUserId(), ACL_SELECT) != ACLCHECK_OK) + { + ReleaseSysCache(tuple); + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied for role %s", rolname))); + } + + /* + * We don't support generating DDL for system roles. The primary reason + * for this is that users shouldn't be recreating them. + */ + if (IsReservedName(rolname)) + ereport(ERROR, + (errcode(ERRCODE_RESERVED_NAME), + errmsg("role name \"%s\" is reserved", rolname), + errdetail("Role names starting with \"pg_\" are reserved for system roles."))); + + initStringInfo(&buf); + appendStringInfo(&buf, "CREATE ROLE %s", quote_identifier(rolname)); + + /* + * Append role attributes. The order here follows the same sequence as + * you'd typically write them in a CREATE ROLE command, though any order + * is actually acceptable to the parser. + */ + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolsuper ? "SUPERUSER" : "NOSUPERUSER"); + + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolinherit ? "INHERIT" : "NOINHERIT"); + + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolcreaterole ? "CREATEROLE" : "NOCREATEROLE"); + + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolcreatedb ? "CREATEDB" : "NOCREATEDB"); + + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolcanlogin ? "LOGIN" : "NOLOGIN"); + + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolreplication ? "REPLICATION" : "NOREPLICATION"); + + append_ddl_option(&buf, pretty, 4, "%s", + roleform->rolbypassrls ? "BYPASSRLS" : "NOBYPASSRLS"); + + /* + * CONNECTION LIMIT is only interesting if it's not -1 (the default, + * meaning no limit). + */ + if (roleform->rolconnlimit >= 0) + append_ddl_option(&buf, pretty, 4, "CONNECTION LIMIT %d", + roleform->rolconnlimit); + + rolevaliduntil = SysCacheGetAttr(AUTHOID, tuple, + Anum_pg_authid_rolvaliduntil, + &isnull); + if (!isnull) + { + TimestampTz ts; + int tz; + struct pg_tm tm; + fsec_t fsec; + const char *tzn; + char ts_str[MAXDATELEN + 1]; + + ts = DatumGetTimestampTz(rolevaliduntil); + if (TIMESTAMP_NOT_FINITE(ts)) + EncodeSpecialTimestamp(ts, ts_str); + else if (timestamp2tm(ts, &tz, &tm, &fsec, &tzn, NULL) == 0) + EncodeDateTime(&tm, fsec, true, tz, tzn, USE_ISO_DATES, ts_str); + else + ereport(ERROR, + (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), + errmsg("timestamp out of range"))); + + append_ddl_option(&buf, pretty, 4, "VALID UNTIL %s", + quote_literal_cstr(ts_str)); + } + + ReleaseSysCache(tuple); + + /* + * We intentionally omit PASSWORD. There's no way to retrieve the + * original password text from the stored hash, and even if we could, + * exposing passwords through a SQL function would be a security issue. + * Users must set passwords separately after recreating roles. + */ + + appendStringInfoChar(&buf, ';'); + + statements = lappend(statements, pstrdup(buf.data)); + + /* + * Now scan pg_db_role_setting for ALTER ROLE SET configurations. + * + * These can be role-wide (setdatabase = 0) or specific to a particular + * database (setdatabase = a valid DB OID). It generates one ALTER + * statement per setting. + */ + rel = table_open(DbRoleSettingRelationId, AccessShareLock); + ScanKeyInit(&scankey, + Anum_pg_db_role_setting_setrole, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(roleid)); + scan = systable_beginscan(rel, DbRoleSettingDatidRolidIndexId, true, + NULL, 1, &scankey); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_db_role_setting setting = (Form_pg_db_role_setting) GETSTRUCT(tuple); + Oid datid = setting->setdatabase; + Datum datum; + ArrayType *role_settings; + Datum *settings; + bool *nulls; + int nsettings; + char *datname = NULL; + + /* + * If setdatabase is valid, this is a role-in-database setting; + * otherwise it's a role-wide setting. Look up the database name once + * for all settings in this row. + */ + if (OidIsValid(datid)) + { + datname = get_database_name(datid); + /* Database has been dropped; skip all settings in this row. */ + if (datname == NULL) + continue; + } + + /* + * The setconfig column is a text array in "name=value" format. It + * should never be null for a valid row, but be defensive. + */ + datum = heap_getattr(tuple, Anum_pg_db_role_setting_setconfig, + RelationGetDescr(rel), &isnull); + if (isnull) + continue; + + role_settings = DatumGetArrayTypePCopy(datum); + + deconstruct_array_builtin(role_settings, TEXTOID, &settings, &nulls, &nsettings); + + for (int i = 0; i < nsettings; i++) + { + char *s, + *p; + + if (nulls[i]) + continue; + + s = TextDatumGetCString(settings[i]); + p = strchr(s, '='); + if (p == NULL) + { + pfree(s); + continue; + } + *p++ = '\0'; + + /* Build a fresh ALTER ROLE statement for this setting */ + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER ROLE %s", quote_identifier(rolname)); + + if (datname != NULL) + appendStringInfo(&buf, " IN DATABASE %s", + quote_identifier(datname)); + + appendStringInfo(&buf, " SET %s TO ", + quote_identifier(s)); + + append_guc_value(&buf, s, p); + + appendStringInfoChar(&buf, ';'); + + statements = lappend(statements, pstrdup(buf.data)); + + pfree(s); + } + + pfree(settings); + pfree(nulls); + pfree(role_settings); + + if (datname != NULL) + pfree(datname); + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); + + /* + * Scan pg_auth_members for role memberships. We look for rows where + * member = roleid, meaning this role has been granted membership in other + * roles. + */ + if (memberships) + { + rel = table_open(AuthMemRelationId, AccessShareLock); + ScanKeyInit(&scankey, + Anum_pg_auth_members_member, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(roleid)); + scan = systable_beginscan(rel, AuthMemMemRoleIndexId, true, + NULL, 1, &scankey); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_auth_members memform = (Form_pg_auth_members) GETSTRUCT(tuple); + char *granted_role; + char *grantor; + + granted_role = GetUserNameFromId(memform->roleid, false); + grantor = GetUserNameFromId(memform->grantor, false); + + resetStringInfo(&buf); + appendStringInfo(&buf, "GRANT %s TO %s", + quote_identifier(granted_role), + quote_identifier(rolname)); + appendStringInfo(&buf, " WITH ADMIN %s, INHERIT %s, SET %s", + memform->admin_option ? "TRUE" : "FALSE", + memform->inherit_option ? "TRUE" : "FALSE", + memform->set_option ? "TRUE" : "FALSE"); + appendStringInfo(&buf, " GRANTED BY %s;", + quote_identifier(grantor)); + + statements = lappend(statements, pstrdup(buf.data)); + + pfree(granted_role); + pfree(grantor); + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); + } + + pfree(buf.data); + pfree(rolname); + + return statements; +} + +/* + * pg_get_role_ddl + * Return DDL to recreate a role as a set of text rows. + * + * Each row is a complete SQL statement. The first row is always the + * CREATE ROLE statement; subsequent rows are ALTER ROLE SET statements + * and optionally GRANT statements for role memberships. + * Returns no rows if the role argument is NULL. + */ +Datum +pg_get_role_ddl(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + List *statements; + + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + Oid roleid; + DdlOption opts[] = { + {"pretty", DDL_OPT_BOOL}, + {"memberships", DDL_OPT_BOOL}, + }; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (PG_ARGISNULL(0)) + { + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); + } + + roleid = PG_GETARG_OID(0); + parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + + statements = pg_get_role_ddl_internal(roleid, + opts[0].isset && opts[0].boolval, + !opts[1].isset || opts[1].boolval); + funcctx->user_fctx = statements; + funcctx->max_calls = list_length(statements); + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + statements = (List *) funcctx->user_fctx; + + if (funcctx->call_cntr < funcctx->max_calls) + { + char *stmt; + + stmt = list_nth(statements, funcctx->call_cntr); + + SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt)); + } + else + { + list_free_deep(statements); + SRF_RETURN_DONE(funcctx); + } +} + +/* + * pg_get_tablespace_ddl_internal + * Generate DDL statements to recreate a tablespace. + * + * Returns a List of palloc'd strings. The first element is the + * CREATE TABLESPACE statement; if the tablespace has reloptions, + * a second element with ALTER TABLESPACE SET (...) is appended. + */ +static List * +pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner) +{ + HeapTuple tuple; + Form_pg_tablespace tspForm; + StringInfoData buf; + char *spcname; + char *spcowner; + char *path; + bool isNull; + Datum datum; + List *statements = NIL; + + tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tsid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("tablespace with OID %u does not exist", + tsid))); + + tspForm = (Form_pg_tablespace) GETSTRUCT(tuple); + spcname = pstrdup(NameStr(tspForm->spcname)); + + /* User must have SELECT privilege on pg_tablespace. */ + if (pg_class_aclcheck(TableSpaceRelationId, GetUserId(), ACL_SELECT) != ACLCHECK_OK) + { + ReleaseSysCache(tuple); + aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_TABLESPACE, spcname); + } + + /* + * We don't support generating DDL for system tablespaces. The primary + * reason for this is that users shouldn't be recreating them. + */ + if (IsReservedName(spcname)) + ereport(ERROR, + (errcode(ERRCODE_RESERVED_NAME), + errmsg("tablespace name \"%s\" is reserved", spcname), + errdetail("Tablespace names starting with \"pg_\" are reserved for system tablespaces."))); + + initStringInfo(&buf); + + /* Start building the CREATE TABLESPACE statement */ + appendStringInfo(&buf, "CREATE TABLESPACE %s", quote_identifier(spcname)); + + /* Add OWNER clause */ + if (!no_owner) + { + spcowner = GetUserNameFromId(tspForm->spcowner, false); + append_ddl_option(&buf, pretty, 4, "OWNER %s", + quote_identifier(spcowner)); + pfree(spcowner); + } + + /* Find tablespace directory path */ + path = get_tablespace_location(tsid); + + /* Add directory LOCATION (path), if it exists */ + if (path[0] != '\0') + { + /* + * Special case: if the tablespace was created with GUC + * "allow_in_place_tablespaces = true" and "LOCATION ''", path will + * begin with "pg_tblspc/". In that case, show "LOCATION ''" as the + * user originally specified. + */ + if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0) + append_ddl_option(&buf, pretty, 4, "LOCATION ''"); + else + append_ddl_option(&buf, pretty, 4, "LOCATION %s", + quote_literal_cstr(path)); + } + pfree(path); + + appendStringInfoChar(&buf, ';'); + statements = lappend(statements, pstrdup(buf.data)); + + /* Check for tablespace options */ + datum = SysCacheGetAttr(TABLESPACEOID, tuple, + Anum_pg_tablespace_spcoptions, &isNull); + if (!isNull) + { + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER TABLESPACE %s SET (", + quote_identifier(spcname)); + get_reloptions(&buf, datum); + appendStringInfoString(&buf, ");"); + statements = lappend(statements, pstrdup(buf.data)); + } + + ReleaseSysCache(tuple); + pfree(spcname); + pfree(buf.data); + + return statements; +} + +/* + * pg_get_tablespace_ddl_srf - common SRF logic for tablespace DDL + */ +static Datum +pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull) +{ + FuncCallContext *funcctx; + List *statements; + + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + DdlOption opts[] = { + {"pretty", DDL_OPT_BOOL}, + {"owner", DDL_OPT_BOOL}, + }; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (isnull) + { + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); + } + + parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + + statements = pg_get_tablespace_ddl_internal(tsid, + opts[0].isset && opts[0].boolval, + opts[1].isset && !opts[1].boolval); + funcctx->user_fctx = statements; + funcctx->max_calls = list_length(statements); + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + statements = (List *) funcctx->user_fctx; + + if (funcctx->call_cntr < funcctx->max_calls) + { + char *stmt; + + stmt = (char *) list_nth(statements, funcctx->call_cntr); + + SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt)); + } + else + { + list_free_deep(statements); + SRF_RETURN_DONE(funcctx); + } +} + +/* + * pg_get_tablespace_ddl_oid + * Return DDL to recreate a tablespace, taking OID. + */ +Datum +pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS) +{ + Oid tsid = InvalidOid; + bool isnull; + + isnull = PG_ARGISNULL(0); + if (!isnull) + tsid = PG_GETARG_OID(0); + + return pg_get_tablespace_ddl_srf(fcinfo, tsid, isnull); +} + +/* + * pg_get_tablespace_ddl_name + * Return DDL to recreate a tablespace, taking name. + */ +Datum +pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS) +{ + Oid tsid = InvalidOid; + Name tspname; + bool isnull; + + isnull = PG_ARGISNULL(0); + + if (!isnull) + { + tspname = PG_GETARG_NAME(0); + tsid = get_tablespace_oid(NameStr(*tspname), false); + } + + return pg_get_tablespace_ddl_srf(fcinfo, tsid, isnull); +} + +/* + * pg_get_database_ddl_internal + * Generate DDL statements to recreate a database. + * + * Returns a List of palloc'd strings. The first element is the + * CREATE DATABASE statement; subsequent elements are ALTER DATABASE + * statements for properties and configuration settings. + */ +static List * +pg_get_database_ddl_internal(Oid dbid, bool pretty, + bool no_owner, bool no_tablespace) +{ + HeapTuple tuple; + Form_pg_database dbform; + StringInfoData buf; + bool isnull; + Datum datum; + const char *encoding; + char *dbname; + char *collate; + char *ctype; + Relation rel; + ScanKeyData scankey[2]; + SysScanDesc scan; + List *statements = NIL; + AclResult aclresult; + + tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("database with OID %u does not exist", dbid))); + + /* User must have connect privilege for target database. */ + aclresult = object_aclcheck(DatabaseRelationId, dbid, GetUserId(), ACL_CONNECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_DATABASE, + get_database_name(dbid)); + + dbform = (Form_pg_database) GETSTRUCT(tuple); + dbname = pstrdup(NameStr(dbform->datname)); + + /* + * Reject invalid databases. Deparsing a pg_database row in invalid state + * can produce SQL that is not executable, such as CONNECTION LIMIT = -2. + */ + if (database_is_invalid_form(dbform)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot generate DDL for invalid database \"%s\"", + dbname))); + + /* + * We don't support generating DDL for system databases. The primary + * reason for this is that users shouldn't be recreating them. + */ + if (strcmp(dbname, "template0") == 0 || strcmp(dbname, "template1") == 0) + ereport(ERROR, + (errcode(ERRCODE_RESERVED_NAME), + errmsg("database \"%s\" is a system database", dbname), + errdetail("DDL generation is not supported for template0 and template1."))); + + initStringInfo(&buf); + + /* --- Build CREATE DATABASE statement --- */ + appendStringInfo(&buf, "CREATE DATABASE %s", quote_identifier(dbname)); + + /* + * Always use template0: the target database already contains the catalog + * data from whatever template was used originally, so we must start from + * the pristine template to avoid duplication. + */ + append_ddl_option(&buf, pretty, 4, "WITH TEMPLATE = template0"); + + /* ENCODING */ + encoding = pg_encoding_to_char(dbform->encoding); + if (strlen(encoding) > 0) + append_ddl_option(&buf, pretty, 4, "ENCODING = %s", + quote_literal_cstr(encoding)); + + /* LOCALE_PROVIDER */ + if (dbform->datlocprovider == COLLPROVIDER_BUILTIN || + dbform->datlocprovider == COLLPROVIDER_ICU || + dbform->datlocprovider == COLLPROVIDER_LIBC) + append_ddl_option(&buf, pretty, 4, "LOCALE_PROVIDER = %s", + collprovider_name(dbform->datlocprovider)); + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("unrecognized locale provider: %c", + dbform->datlocprovider))); + + /* LOCALE, LC_COLLATE, LC_CTYPE */ + datum = SysCacheGetAttr(DATABASEOID, tuple, + Anum_pg_database_datcollate, &isnull); + collate = isnull ? NULL : TextDatumGetCString(datum); + datum = SysCacheGetAttr(DATABASEOID, tuple, + Anum_pg_database_datctype, &isnull); + ctype = isnull ? NULL : TextDatumGetCString(datum); + if (collate != NULL && ctype != NULL && strcmp(collate, ctype) == 0) + { + append_ddl_option(&buf, pretty, 4, "LOCALE = %s", + quote_literal_cstr(collate)); + } + else + { + if (collate != NULL) + append_ddl_option(&buf, pretty, 4, "LC_COLLATE = %s", + quote_literal_cstr(collate)); + if (ctype != NULL) + append_ddl_option(&buf, pretty, 4, "LC_CTYPE = %s", + quote_literal_cstr(ctype)); + } + + /* LOCALE (provider-specific) */ + datum = SysCacheGetAttr(DATABASEOID, tuple, + Anum_pg_database_datlocale, &isnull); + if (!isnull) + { + const char *locale = TextDatumGetCString(datum); + + if (dbform->datlocprovider == COLLPROVIDER_BUILTIN) + append_ddl_option(&buf, pretty, 4, "BUILTIN_LOCALE = %s", + quote_literal_cstr(locale)); + else if (dbform->datlocprovider == COLLPROVIDER_ICU) + append_ddl_option(&buf, pretty, 4, "ICU_LOCALE = %s", + quote_literal_cstr(locale)); + } + + /* ICU_RULES */ + datum = SysCacheGetAttr(DATABASEOID, tuple, + Anum_pg_database_daticurules, &isnull); + if (!isnull && dbform->datlocprovider == COLLPROVIDER_ICU) + append_ddl_option(&buf, pretty, 4, "ICU_RULES = %s", + quote_literal_cstr(TextDatumGetCString(datum))); + + /* TABLESPACE */ + if (!no_tablespace && OidIsValid(dbform->dattablespace)) + { + char *spcname = get_tablespace_name(dbform->dattablespace); + + if (pg_strcasecmp(spcname, "pg_default") != 0) + append_ddl_option(&buf, pretty, 4, "TABLESPACE = %s", + quote_identifier(spcname)); + } + + appendStringInfoChar(&buf, ';'); + statements = lappend(statements, pstrdup(buf.data)); + + /* OWNER */ + if (!no_owner && OidIsValid(dbform->datdba)) + { + char *owner = GetUserNameFromId(dbform->datdba, false); + + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER DATABASE %s OWNER TO %s;", + quote_identifier(dbname), quote_identifier(owner)); + pfree(owner); + statements = lappend(statements, pstrdup(buf.data)); + } + + /* CONNECTION LIMIT */ + if (dbform->datconnlimit != -1) + { + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER DATABASE %s CONNECTION LIMIT = %d;", + quote_identifier(dbname), dbform->datconnlimit); + statements = lappend(statements, pstrdup(buf.data)); + } + + /* IS_TEMPLATE */ + if (dbform->datistemplate) + { + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER DATABASE %s IS_TEMPLATE = true;", + quote_identifier(dbname)); + statements = lappend(statements, pstrdup(buf.data)); + } + + /* ALLOW_CONNECTIONS */ + if (!dbform->datallowconn) + { + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER DATABASE %s ALLOW_CONNECTIONS = false;", + quote_identifier(dbname)); + statements = lappend(statements, pstrdup(buf.data)); + } + + ReleaseSysCache(tuple); + + /* + * Now scan pg_db_role_setting for ALTER DATABASE SET configurations. + * + * It is only database-wide (setrole = 0). It generates one ALTER + * statement per setting. + */ + rel = table_open(DbRoleSettingRelationId, AccessShareLock); + ScanKeyInit(&scankey[0], + Anum_pg_db_role_setting_setdatabase, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(dbid)); + ScanKeyInit(&scankey[1], + Anum_pg_db_role_setting_setrole, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(InvalidOid)); + + scan = systable_beginscan(rel, DbRoleSettingDatidRolidIndexId, true, + NULL, 2, scankey); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + ArrayType *dbconfig; + Datum *settings; + bool *nulls; + int nsettings; + + /* + * The setconfig column is a text array in "name=value" format. It + * should never be null for a valid row, but be defensive. + */ + datum = heap_getattr(tuple, Anum_pg_db_role_setting_setconfig, + RelationGetDescr(rel), &isnull); + if (isnull) + continue; + + dbconfig = DatumGetArrayTypePCopy(datum); + + deconstruct_array_builtin(dbconfig, TEXTOID, &settings, &nulls, &nsettings); + + for (int i = 0; i < nsettings; i++) + { + char *s, + *p; + + if (nulls[i]) + continue; + + s = TextDatumGetCString(settings[i]); + p = strchr(s, '='); + if (p == NULL) + { + pfree(s); + continue; + } + *p++ = '\0'; + + resetStringInfo(&buf); + appendStringInfo(&buf, "ALTER DATABASE %s SET %s TO ", + quote_identifier(dbname), + quote_identifier(s)); + + append_guc_value(&buf, s, p); + + appendStringInfoChar(&buf, ';'); + + statements = lappend(statements, pstrdup(buf.data)); + + pfree(s); + } + + pfree(settings); + pfree(nulls); + pfree(dbconfig); + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); + + pfree(buf.data); + pfree(dbname); + + return statements; +} + +/* + * pg_get_database_ddl + * Return DDL to recreate a database as a set of text rows. + */ +Datum +pg_get_database_ddl(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + List *statements; + + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + Oid dbid; + DdlOption opts[] = { + {"pretty", DDL_OPT_BOOL}, + {"owner", DDL_OPT_BOOL}, + {"tablespace", DDL_OPT_BOOL}, + }; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (PG_ARGISNULL(0)) + { + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); + } + + dbid = PG_GETARG_OID(0); + parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + + statements = pg_get_database_ddl_internal(dbid, + opts[0].isset && opts[0].boolval, + opts[1].isset && !opts[1].boolval, + opts[2].isset && !opts[2].boolval); + funcctx->user_fctx = statements; + funcctx->max_calls = list_length(statements); + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + statements = (List *) funcctx->user_fctx; + + if (funcctx->call_cntr < funcctx->max_calls) + { + char *stmt; + + stmt = list_nth(statements, funcctx->call_cntr); + + SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt)); + } + else + { + list_free_deep(statements); + SRF_RETURN_DONE(funcctx); + } +} diff --git a/src/backend/utils/adt/domains.c b/src/backend/utils/adt/domains.c index e755b70e17ec9..50cd257e0bfe5 100644 --- a/src/backend/utils/adt/domains.c +++ b/src/backend/utils/adt/domains.c @@ -19,7 +19,7 @@ * to evaluate them in. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/encode.c b/src/backend/utils/adt/encode.c index 4ccaed815d17e..9ea3ddb49ec03 100644 --- a/src/backend/utils/adt/encode.c +++ b/src/backend/utils/adt/encode.c @@ -3,7 +3,7 @@ * encode.c * Various data encoding/decoding things. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -16,6 +16,7 @@ #include #include "mb/pg_wchar.h" +#include "port/simd.h" #include "utils/builtins.h" #include "utils/memutils.h" #include "varatt.h" @@ -63,7 +64,9 @@ binary_encode(PG_FUNCTION_ARGS) if (enc == NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized encoding: \"%s\"", namebuf))); + errmsg("unrecognized encoding: \"%s\"", namebuf), + errhint("Valid encodings are \"%s\", \"%s\", \"%s\", \"%s\", and \"%s\".", + "base32hex", "base64", "base64url", "escape", "hex"))); dataptr = VARDATA_ANY(data); datalen = VARSIZE_ANY_EXHDR(data); @@ -111,7 +114,9 @@ binary_decode(PG_FUNCTION_ARGS) if (enc == NULL) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized encoding: \"%s\"", namebuf))); + errmsg("unrecognized encoding: \"%s\"", namebuf), + errhint("Valid encodings are \"%s\", \"%s\", \"%s\", \"%s\", and \"%s\".", + "base32hex", "base64", "base64url", "escape", "hex"))); dataptr = VARDATA_ANY(data); datalen = VARSIZE_ANY_EXHDR(data); @@ -177,8 +182,8 @@ static const int8 hexlookup[128] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, }; -uint64 -hex_encode(const char *src, size_t len, char *dst) +static inline uint64 +hex_encode_scalar(const char *src, size_t len, char *dst) { const char *end = src + len; @@ -193,6 +198,55 @@ hex_encode(const char *src, size_t len, char *dst) return (uint64) len * 2; } +uint64 +hex_encode(const char *src, size_t len, char *dst) +{ +#ifdef USE_NO_SIMD + return hex_encode_scalar(src, len, dst); +#else + const uint64 tail_idx = len & ~(sizeof(Vector8) - 1); + uint64 i; + + /* + * This splits the high and low nibbles of each byte into separate + * vectors, adds the vectors to a mask that converts the nibbles to their + * equivalent ASCII bytes, and interleaves those bytes back together to + * form the final hex-encoded string. + */ + for (i = 0; i < tail_idx; i += sizeof(Vector8)) + { + Vector8 srcv; + Vector8 lo; + Vector8 hi; + Vector8 mask; + + vector8_load(&srcv, (const uint8 *) &src[i]); + + lo = vector8_and(srcv, vector8_broadcast(0x0f)); + mask = vector8_gt(lo, vector8_broadcast(0x9)); + mask = vector8_and(mask, vector8_broadcast('a' - '0' - 10)); + mask = vector8_add(mask, vector8_broadcast('0')); + lo = vector8_add(lo, mask); + + hi = vector8_and(srcv, vector8_broadcast(0xf0)); + hi = vector8_shift_right(hi, 4); + mask = vector8_gt(hi, vector8_broadcast(0x9)); + mask = vector8_and(mask, vector8_broadcast('a' - '0' - 10)); + mask = vector8_add(mask, vector8_broadcast('0')); + hi = vector8_add(hi, mask); + + vector8_store((uint8 *) &dst[i * 2], + vector8_interleave_low(hi, lo)); + vector8_store((uint8 *) &dst[i * 2 + sizeof(Vector8)], + vector8_interleave_high(hi, lo)); + } + + (void) hex_encode_scalar(src + i, len - i, dst + i * 2); + + return (uint64) len * 2; +#endif +} + static inline bool get_hex(const char *cp, char *out) { @@ -213,8 +267,8 @@ hex_decode(const char *src, size_t len, char *dst) return hex_decode_safe(src, len, dst, NULL); } -uint64 -hex_decode_safe(const char *src, size_t len, char *dst, Node *escontext) +static inline uint64 +hex_decode_safe_scalar(const char *src, size_t len, char *dst, Node *escontext) { const char *s, *srcend; @@ -236,7 +290,7 @@ hex_decode_safe(const char *src, size_t len, char *dst, Node *escontext) ereturn(escontext, 0, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid hexadecimal digit: \"%.*s\"", - pg_mblen(s), s))); + pg_mblen_range(s, srcend), s))); s++; if (s >= srcend) ereturn(escontext, 0, @@ -246,7 +300,7 @@ hex_decode_safe(const char *src, size_t len, char *dst, Node *escontext) ereturn(escontext, 0, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid hexadecimal digit: \"%.*s\"", - pg_mblen(s), s))); + pg_mblen_range(s, srcend), s))); s++; *p++ = (v1 << 4) | v2; } @@ -254,6 +308,85 @@ hex_decode_safe(const char *src, size_t len, char *dst, Node *escontext) return p - dst; } +/* + * This helper converts each byte to its binary-equivalent nibble by + * subtraction and combines them to form the return bytes (separated by zero + * bytes). Returns false if any input bytes are outside the expected ranges of + * ASCII values. Otherwise, returns true. + */ +#ifndef USE_NO_SIMD +static inline bool +hex_decode_simd_helper(const Vector8 src, Vector8 *dst) +{ + Vector8 sub; + Vector8 mask_hi = vector8_interleave_low(vector8_broadcast(0), vector8_broadcast(0x0f)); + Vector8 mask_lo = vector8_interleave_low(vector8_broadcast(0x0f), vector8_broadcast(0)); + Vector8 tmp; + bool ret; + + tmp = vector8_gt(vector8_broadcast('9' + 1), src); + sub = vector8_and(tmp, vector8_broadcast('0')); + + tmp = vector8_gt(src, vector8_broadcast('A' - 1)); + tmp = vector8_and(tmp, vector8_broadcast('A' - 10)); + sub = vector8_add(sub, tmp); + + tmp = vector8_gt(src, vector8_broadcast('a' - 1)); + tmp = vector8_and(tmp, vector8_broadcast('a' - 'A')); + sub = vector8_add(sub, tmp); + + *dst = vector8_issub(src, sub); + ret = !vector8_has_ge(*dst, 0x10); + + tmp = vector8_and(*dst, mask_hi); + tmp = vector8_shift_right(tmp, 8); + *dst = vector8_and(*dst, mask_lo); + *dst = vector8_shift_left(*dst, 4); + *dst = vector8_or(*dst, tmp); + return ret; +} +#endif /* ! USE_NO_SIMD */ + +uint64 +hex_decode_safe(const char *src, size_t len, char *dst, Node *escontext) +{ +#ifdef USE_NO_SIMD + return hex_decode_safe_scalar(src, len, dst, escontext); +#else + const uint64 tail_idx = len & ~(sizeof(Vector8) * 2 - 1); + uint64 i; + bool success = true; + + /* + * We must process 2 vectors at a time since the output will be half the + * length of the input. + */ + for (i = 0; i < tail_idx; i += sizeof(Vector8) * 2) + { + Vector8 srcv; + Vector8 dstv1; + Vector8 dstv2; + + vector8_load(&srcv, (const uint8 *) &src[i]); + success &= hex_decode_simd_helper(srcv, &dstv1); + + vector8_load(&srcv, (const uint8 *) &src[i + sizeof(Vector8)]); + success &= hex_decode_simd_helper(srcv, &dstv2); + + vector8_store((uint8 *) &dst[i / 2], vector8_pack_16(dstv1, dstv2)); + } + + /* + * If something didn't look right in the vector path, try again in the + * scalar path so that we can handle it correctly. + */ + if (!success) + i = 0; + + return i / 2 + hex_decode_safe_scalar(src + i, len - i, dst + i / 2, escontext); +#endif +} + static uint64 hex_enc_len(const char *src, size_t srclen) { @@ -267,12 +400,15 @@ hex_dec_len(const char *src, size_t srclen) } /* - * BASE64 + * BASE64 and BASE64URL */ static const char _base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static const char _base64url[] = +"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + static const int8 b64lookup[128] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, @@ -284,8 +420,15 @@ static const int8 b64lookup[128] = { 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, }; +/* + * pg_base64_encode_internal + * + * Helper for decoding base64 or base64url. When url is passed as true the + * input will be encoded using base64url. len bytes in src is encoded into + * dst. + */ static uint64 -pg_base64_encode(const char *src, size_t len, char *dst) +pg_base64_encode_internal(const char *src, size_t len, char *dst, bool url) { char *p, *lend = dst + 76; @@ -293,6 +436,7 @@ pg_base64_encode(const char *src, size_t len, char *dst) *end = src + len; int pos = 2; uint32 buf = 0; + const char *alphabet = url ? _base64url : _base64; s = src; p = dst; @@ -306,33 +450,64 @@ pg_base64_encode(const char *src, size_t len, char *dst) /* write it out */ if (pos < 0) { - *p++ = _base64[(buf >> 18) & 0x3f]; - *p++ = _base64[(buf >> 12) & 0x3f]; - *p++ = _base64[(buf >> 6) & 0x3f]; - *p++ = _base64[buf & 0x3f]; + *p++ = alphabet[(buf >> 18) & 0x3f]; + *p++ = alphabet[(buf >> 12) & 0x3f]; + *p++ = alphabet[(buf >> 6) & 0x3f]; + *p++ = alphabet[buf & 0x3f]; pos = 2; buf = 0; - } - if (p >= lend) - { - *p++ = '\n'; - lend = p + 76; + + if (!url && p >= lend) + { + *p++ = '\n'; + lend = p + 76; + } } } + + /* Handle remaining bytes in buf */ if (pos != 2) { - *p++ = _base64[(buf >> 18) & 0x3f]; - *p++ = _base64[(buf >> 12) & 0x3f]; - *p++ = (pos == 0) ? _base64[(buf >> 6) & 0x3f] : '='; - *p++ = '='; + *p++ = alphabet[(buf >> 18) & 0x3f]; + *p++ = alphabet[(buf >> 12) & 0x3f]; + + if (pos == 0) + { + *p++ = alphabet[(buf >> 6) & 0x3f]; + if (!url) + *p++ = '='; + } + else if (!url) + { + *p++ = '='; + *p++ = '='; + } } return p - dst; } static uint64 -pg_base64_decode(const char *src, size_t len, char *dst) +pg_base64_encode(const char *src, size_t len, char *dst) +{ + return pg_base64_encode_internal(src, len, dst, false); +} + +static uint64 +pg_base64url_encode(const char *src, size_t len, char *dst) +{ + return pg_base64_encode_internal(src, len, dst, true); +} + +/* + * pg_base64_decode_internal + * + * Helper for decoding base64 or base64url. When url is passed as true the + * input will be assumed to be encoded using base64url. + */ +static uint64 +pg_base64_decode_internal(const char *src, size_t len, char *dst, bool url) { const char *srcend = src + len, *s = src; @@ -350,6 +525,15 @@ pg_base64_decode(const char *src, size_t len, char *dst) if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue; + /* convert base64url to base64 */ + if (url) + { + if (c == '-') + c = '+'; + else if (c == '_') + c = '/'; + } + if (c == '=') { /* end sequence */ @@ -360,9 +544,12 @@ pg_base64_decode(const char *src, size_t len, char *dst) else if (pos == 3) end = 2; else + { + /* translator: %s is the name of an encoding scheme */ ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unexpected \"=\" while decoding base64 sequence"))); + errmsg("unexpected \"=\" while decoding %s sequence", url ? "base64url" : "base64"))); + } } b = 0; } @@ -372,10 +559,14 @@ pg_base64_decode(const char *src, size_t len, char *dst) if (c > 0 && c < 127) b = b64lookup[(unsigned char) c]; if (b < 0) + { + /* translator: %s is the name of an encoding scheme */ ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid symbol \"%.*s\" found while decoding base64 sequence", - pg_mblen(s - 1), s - 1))); + errmsg("invalid symbol \"%.*s\" found while decoding %s sequence", + pg_mblen_range(s - 1, srcend), s - 1, + url ? "base64url" : "base64"))); + } } /* add it to buffer */ buf = (buf << 6) + b; @@ -392,15 +583,40 @@ pg_base64_decode(const char *src, size_t len, char *dst) } } - if (pos != 0) + if (pos == 2) + { + buf <<= 12; + *p++ = (buf >> 16) & 0xFF; + } + else if (pos == 3) + { + buf <<= 6; + *p++ = (buf >> 16) & 0xFF; + *p++ = (buf >> 8) & 0xFF; + } + else if (pos != 0) + { + /* translator: %s is the name of an encoding scheme */ ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("invalid base64 end sequence"), + errmsg("invalid %s end sequence", url ? "base64url" : "base64"), errhint("Input data is missing padding, is truncated, or is otherwise corrupted."))); + } return p - dst; } +static uint64 +pg_base64_decode(const char *src, size_t len, char *dst) +{ + return pg_base64_decode_internal(src, len, dst, false); +} + +static uint64 +pg_base64url_decode(const char *src, size_t len, char *dst) +{ + return pg_base64_decode_internal(src, len, dst, true); +} static uint64 pg_base64_enc_len(const char *src, size_t srclen) @@ -415,6 +631,32 @@ pg_base64_dec_len(const char *src, size_t srclen) return ((uint64) srclen * 3) >> 2; } +static uint64 +pg_base64url_enc_len(const char *src, size_t srclen) +{ + /* + * Unlike standard base64, base64url doesn't use padding characters when + * the input length is not divisible by 3 + */ + return (srclen + 2) / 3 * 4; +} + +static uint64 +pg_base64url_dec_len(const char *src, size_t srclen) +{ + /* + * For base64, each 4 characters of input produce at most 3 bytes of + * output. For base64url without padding, we need to round up to the + * nearest 4 + */ + size_t adjusted_len = srclen; + + if (srclen % 4 != 0) + adjusted_len += 4 - (srclen % 4); + + return (adjusted_len * 3) / 4; +} + /* * Escape * Minimally escape bytea to text. @@ -583,6 +825,153 @@ esc_dec_len(const char *src, size_t srclen) return len; } +/* + * BASE32HEX + */ + +static const char base32hex_table[] = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + +static const int8 b32hexlookup[128] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; + +static uint64 +base32hex_enc_len(const char *src, size_t srclen) +{ + /* 5 bytes encode to 8 characters, round up to multiple of 8 for padding */ + return ((uint64) srclen + 4) / 5 * 8; +} + +static uint64 +base32hex_dec_len(const char *src, size_t srclen) +{ + /* Each 8 characters of input produces at most 5 bytes of output */ + return ((uint64) srclen * 5) / 8; +} + +static uint64 +base32hex_encode(const char *src, size_t srclen, char *dst) +{ + const unsigned char *data = (const unsigned char *) src; + uint32 bits_buffer = 0; + int bits_in_buffer = 0; + uint64 output_pos = 0; + size_t i; + + for (i = 0; i < srclen; i++) + { + /* Add 8 bits to the buffer */ + bits_buffer = (bits_buffer << 8) | data[i]; + bits_in_buffer += 8; + + /* Extract 5-bit chunks while we have enough bits */ + while (bits_in_buffer >= 5) + { + bits_in_buffer -= 5; + /* Extract top 5 bits */ + dst[output_pos++] = base32hex_table[(bits_buffer >> bits_in_buffer) & 0x1F]; + /* Clear the extracted bits by masking */ + bits_buffer &= ((1U << bits_in_buffer) - 1); + } + } + + /* Handle remaining bits (if any) */ + if (bits_in_buffer > 0) + dst[output_pos++] = base32hex_table[(bits_buffer << (5 - bits_in_buffer)) & 0x1F]; + + /* Add padding to make length a multiple of 8 (per RFC 4648) */ + while (output_pos % 8 != 0) + dst[output_pos++] = '='; + + return output_pos; +} + +static uint64 +base32hex_decode(const char *src, size_t srclen, char *dst) +{ + const char *srcend = src + srclen, + *s = src; + uint32 bits_buffer = 0; + int bits_in_buffer = 0; + uint64 output_pos = 0; + int pos = 0; /* position within 8-character group (0-7) */ + bool end = false; /* have we seen padding? */ + + while (s < srcend) + { + char c = *s++; + int val; + + /* Skip whitespace */ + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') + continue; + + if (c == '=') + { + /* + * The first padding is only valid at positions 2, 4, 5, or 7 + * within an 8-character group (corresponding to 1, 2, 3, or 4 + * input bytes). We only check the position for the first '=' + * character. + */ + if (!end) + { + if (pos != 2 && pos != 4 && pos != 5 && pos != 7) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unexpected \"=\" while decoding base32hex sequence"))); + end = true; + } + pos++; + continue; + } + + /* No data characters allowed after padding */ + if (end) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid symbol \"%.*s\" found while decoding base32hex sequence", + pg_mblen_range(s - 1, srcend), s - 1))); + + /* Decode base32hex character (0-9, A-V, case-insensitive) */ + val = -1; + if ((unsigned char) c < 128) + val = b32hexlookup[(unsigned char) c]; + if (val < 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid symbol \"%.*s\" found while decoding base32hex sequence", + pg_mblen_range(s - 1, srcend), s - 1))); + + /* Add 5 bits to buffer */ + bits_buffer = (bits_buffer << 5) | val; + bits_in_buffer += 5; + pos++; + + /* Extract 8-bit bytes when we have enough bits */ + while (bits_in_buffer >= 8) + { + bits_in_buffer -= 8; + dst[output_pos++] = (unsigned char) (bits_buffer >> bits_in_buffer); + /* Clear the extracted bits */ + bits_buffer &= ((1U << bits_in_buffer) - 1); + } + + /* Reset position after each complete 8-character group */ + if (pos == 8) + pos = 0; + } + + return output_pos; +} + /* * Common */ @@ -606,6 +995,18 @@ static const struct pg_base64_enc_len, pg_base64_dec_len, pg_base64_encode, pg_base64_decode } }, + { + "base64url", + { + pg_base64url_enc_len, pg_base64url_dec_len, pg_base64url_encode, pg_base64url_decode + } + }, + { + "base32hex", + { + base32hex_enc_len, base32hex_dec_len, base32hex_encode, base32hex_decode + } + }, { "escape", { diff --git a/src/backend/utils/adt/enum.c b/src/backend/utils/adt/enum.c index fcc6981632bac..286d4311ed5df 100644 --- a/src/backend/utils/adt/enum.c +++ b/src/backend/utils/adt/enum.c @@ -3,7 +3,7 @@ * enum.c * I/O functions, operators, aggregates etc for enum types * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/utils/adt/expandeddatum.c b/src/backend/utils/adt/expandeddatum.c index 6b4b8eaf005ce..b7d6003845774 100644 --- a/src/backend/utils/adt/expandeddatum.c +++ b/src/backend/utils/adt/expandeddatum.c @@ -3,7 +3,7 @@ * expandeddatum.c * Support functions for "expanded" value representations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/expandedrecord.c b/src/backend/utils/adt/expandedrecord.c index 13752db44e839..123792aa725ec 100644 --- a/src/backend/utils/adt/expandedrecord.c +++ b/src/backend/utils/adt/expandedrecord.c @@ -7,7 +7,7 @@ * store values of named composite types, domains over named composite types, * and record types (registered or anonymous). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -547,7 +547,7 @@ expanded_record_set_tuple(ExpandedRecordHeader *erh, for (i = 0; i < erh->nfields; i++) { if (!erh->dnulls[i] && - !(TupleDescAttr(tupdesc, i)->attbyval)) + !(TupleDescCompactAttr(tupdesc, i)->attbyval)) { char *oldValue = (char *) DatumGetPointer(erh->dvalues[i]); @@ -1159,7 +1159,7 @@ expanded_record_set_field_internal(ExpandedRecordHeader *erh, int fnumber, { /* Detoasting should be done in short-lived context. */ oldcxt = MemoryContextSwitchTo(get_short_term_cxt(erh)); - newValue = PointerGetDatum(detoast_external_attr((struct varlena *) DatumGetPointer(newValue))); + newValue = PointerGetDatum(detoast_external_attr((varlena *) DatumGetPointer(newValue))); MemoryContextSwitchTo(oldcxt); } else @@ -1305,7 +1305,7 @@ expanded_record_set_fields(ExpandedRecordHeader *erh, if (expand_external) { /* Detoast as requested while copying the value */ - newValue = PointerGetDatum(detoast_external_attr((struct varlena *) DatumGetPointer(newValue))); + newValue = PointerGetDatum(detoast_external_attr((varlena *) DatumGetPointer(newValue))); } else { diff --git a/src/backend/utils/adt/float.c b/src/backend/utils/adt/float.c index 6d20ae07ae7b0..362c29ab80356 100644 --- a/src/backend/utils/adt/float.c +++ b/src/backend/utils/adt/float.c @@ -3,7 +3,7 @@ * float.c * Functions for the built-in floating-point types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -29,6 +29,23 @@ #include "utils/sortsupport.h" +/* + * Reject building with gcc's -ffast-math switch. It breaks our handling of + * float Infinity and NaN values (via -ffinite-math-only), causes results to + * be less accurate than expected (via -funsafe-math-optimizations and + * -fexcess-precision=fast), and causes some math error reports to be missed + * (via -fno-math-errno). Unfortunately we can't easily detect cases where + * those options were given individually, but this at least catches the most + * obvious case. + * + * We test this only here, not in any header file, to allow extensions to use + * -ffast-math if they need to. But the inline functions in float.h will + * misbehave in such an extension, so its authors had better be careful. + */ +#ifdef __FAST_MATH__ +#error -ffast-math is known to break this code +#endif + /* * Configurable GUC parameter * @@ -106,6 +123,30 @@ float_zero_divide_error(void) errmsg("division by zero"))); } +float8 +float_overflow_error_ext(struct Node *escontext) +{ + ereturn(escontext, 0.0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value out of range: overflow")); +} + +float8 +float_underflow_error_ext(struct Node *escontext) +{ + ereturn(escontext, 0.0, + errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value out of range: underflow")); +} + +float8 +float_zero_divide_error_ext(struct Node *escontext) +{ + ereturn(escontext, 0.0, + (errcode(ERRCODE_DIVISION_BY_ZERO), + errmsg("division by zero"))); +} + /* * Returns -1 if 'val' represents negative infinity, 1 if 'val' @@ -1199,9 +1240,9 @@ dtof(PG_FUNCTION_ARGS) result = (float4) num; if (unlikely(isinf(result)) && !isinf(num)) - float_overflow_error(); + float_overflow_error_ext(fcinfo->context); if (unlikely(result == 0.0f) && num != 0.0) - float_underflow_error(); + float_underflow_error_ext(fcinfo->context); PG_RETURN_FLOAT4(result); } @@ -1224,7 +1265,7 @@ dtoi4(PG_FUNCTION_ARGS) /* Range check */ if (unlikely(isnan(num) || !FLOAT8_FITS_IN_INT32(num))) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("integer out of range"))); @@ -1249,7 +1290,7 @@ dtoi2(PG_FUNCTION_ARGS) /* Range check */ if (unlikely(isnan(num) || !FLOAT8_FITS_IN_INT16(num))) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("smallint out of range"))); @@ -1298,7 +1339,7 @@ ftoi4(PG_FUNCTION_ARGS) /* Range check */ if (unlikely(isnan(num) || !FLOAT4_FITS_IN_INT32(num))) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("integer out of range"))); @@ -1323,7 +1364,7 @@ ftoi2(PG_FUNCTION_ARGS) /* Range check */ if (unlikely(isnan(num) || !FLOAT4_FITS_IN_INT16(num))) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("smallint out of range"))); @@ -2852,6 +2893,12 @@ dlgamma(PG_FUNCTION_ARGS) float8 arg1 = PG_GETARG_FLOAT8(0); float8 result; + /* On some versions of AIX, lgamma(NaN) fails with ERANGE */ +#if defined(_AIX) + if (isnan(arg1)) + PG_RETURN_FLOAT8(arg1); +#endif + /* * Note: lgamma may not be thread-safe because it may write to a global * variable signgam, which may not be thread-local. However, this doesn't @@ -3319,9 +3366,21 @@ float8_stddev_samp(PG_FUNCTION_ARGS) * As with the preceding aggregates, we use the Youngs-Cramer algorithm to * reduce rounding errors in the aggregate final functions. * - * The transition datatype for all these aggregates is a 6-element array of + * The transition datatype for all these aggregates is an 8-element array of * float8, holding the values N, Sx=sum(X), Sxx=sum((X-Sx/N)^2), Sy=sum(Y), - * Syy=sum((Y-Sy/N)^2), Sxy=sum((X-Sx/N)*(Y-Sy/N)) in that order. + * Syy=sum((Y-Sy/N)^2), Sxy=sum((X-Sx/N)*(Y-Sy/N)), commonX, and commonY + * in that order. + * + * commonX is defined as the common X value if all the X values were the same, + * else NaN; likewise for commonY. This is useful for deciding whether corr() + * and related functions should return NULL. This representation cannot + * distinguish the-values-were-all-NaN from the-values-were-not-all-the-same, + * but that's okay because for this purpose we use the IEEE float arithmetic + * principle that two NaNs are never equal. The SQL standard doesn't mention + * NaNs, but it says that NULL is to be returned when N*sum(X*X) equals + * sum(X)*sum(X) (etc), and that shouldn't be considered true for NaNs. + * Testing this as written in the spec would be highly subject to roundoff + * error, so instead we directly track whether all the inputs are equal. * * Note that Y is the first argument to all these aggregates! * @@ -3345,17 +3404,21 @@ float8_regr_accum(PG_FUNCTION_ARGS) Sy, Syy, Sxy, + commonX, + commonY, tmpX, tmpY, scale; - transvalues = check_float8_array(transarray, "float8_regr_accum", 6); + transvalues = check_float8_array(transarray, "float8_regr_accum", 8); N = transvalues[0]; Sx = transvalues[1]; Sxx = transvalues[2]; Sy = transvalues[3]; Syy = transvalues[4]; Sxy = transvalues[5]; + commonX = transvalues[6]; + commonY = transvalues[7]; /* * Use the Youngs-Cramer algorithm to incorporate the new values into the @@ -3366,12 +3429,33 @@ float8_regr_accum(PG_FUNCTION_ARGS) Sy += newvalY; if (transvalues[0] > 0.0) { + /* + * Check to see if we have seen distinct inputs. We can use a test + * that's a bit cheaper than float8_ne() because if commonX is already + * NaN, it does not matter whether the != test returns true or not. + */ + if (newvalX != commonX || isnan(newvalX)) + commonX = get_float8_nan(); + if (newvalY != commonY || isnan(newvalY)) + commonY = get_float8_nan(); + tmpX = newvalX * N - Sx; tmpY = newvalY * N - Sy; scale = 1.0 / (N * transvalues[0]); - Sxx += tmpX * tmpX * scale; - Syy += tmpY * tmpY * scale; - Sxy += tmpX * tmpY * scale; + + /* + * If we have not seen distinct inputs, then Sxx, Syy, and/or Sxy + * should remain zero (since Sx's exact value would be N * commonX, + * etc). Updating them would just create the possibility of injecting + * roundoff error, and we need exact zero results so that the final + * functions will return NULL in the right cases. + */ + if (isnan(commonX)) + Sxx += tmpX * tmpX * scale; + if (isnan(commonY)) + Syy += tmpY * tmpY * scale; + if (isnan(commonX) && isnan(commonY)) + Sxy += tmpX * tmpY * scale; /* * Overflow check. We only report an overflow error when finite @@ -3410,6 +3494,9 @@ float8_regr_accum(PG_FUNCTION_ARGS) Sxx = Sxy = get_float8_nan(); if (isnan(newvalY) || isinf(newvalY)) Syy = Sxy = get_float8_nan(); + + commonX = newvalX; + commonY = newvalY; } /* @@ -3425,12 +3512,14 @@ float8_regr_accum(PG_FUNCTION_ARGS) transvalues[3] = Sy; transvalues[4] = Syy; transvalues[5] = Sxy; + transvalues[6] = commonX; + transvalues[7] = commonY; PG_RETURN_ARRAYTYPE_P(transarray); } else { - Datum transdatums[6]; + Datum transdatums[8]; ArrayType *result; transdatums[0] = Float8GetDatumFast(N); @@ -3439,8 +3528,10 @@ float8_regr_accum(PG_FUNCTION_ARGS) transdatums[3] = Float8GetDatumFast(Sy); transdatums[4] = Float8GetDatumFast(Syy); transdatums[5] = Float8GetDatumFast(Sxy); + transdatums[6] = Float8GetDatumFast(commonX); + transdatums[7] = Float8GetDatumFast(commonY); - result = construct_array_builtin(transdatums, 6, FLOAT8OID); + result = construct_array_builtin(transdatums, 8, FLOAT8OID); PG_RETURN_ARRAYTYPE_P(result); } @@ -3449,7 +3540,7 @@ float8_regr_accum(PG_FUNCTION_ARGS) /* * float8_regr_combine * - * An aggregate combine function used to combine two 6 fields + * An aggregate combine function used to combine two 8-fields * aggregate transition data into a single transition data. * This function is used only in two stage aggregation and * shouldn't be called outside aggregate context. @@ -3467,12 +3558,16 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sy1, Syy1, Sxy1, + Cx1, + Cy1, N2, Sx2, Sxx2, Sy2, Syy2, Sxy2, + Cx2, + Cy2, tmp1, tmp2, N, @@ -3480,10 +3575,12 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sxx, Sy, Syy, - Sxy; + Sxy, + Cx, + Cy; - transvalues1 = check_float8_array(transarray1, "float8_regr_combine", 6); - transvalues2 = check_float8_array(transarray2, "float8_regr_combine", 6); + transvalues1 = check_float8_array(transarray1, "float8_regr_combine", 8); + transvalues2 = check_float8_array(transarray2, "float8_regr_combine", 8); N1 = transvalues1[0]; Sx1 = transvalues1[1]; @@ -3491,6 +3588,8 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sy1 = transvalues1[3]; Syy1 = transvalues1[4]; Sxy1 = transvalues1[5]; + Cx1 = transvalues1[6]; + Cy1 = transvalues1[7]; N2 = transvalues2[0]; Sx2 = transvalues2[1]; @@ -3498,6 +3597,8 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sy2 = transvalues2[3]; Syy2 = transvalues2[4]; Sxy2 = transvalues2[5]; + Cx2 = transvalues2[6]; + Cy2 = transvalues2[7]; /*-------------------- * The transition values combine using a generalization of the @@ -3523,6 +3624,8 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sy = Sy2; Syy = Syy2; Sxy = Sxy2; + Cx = Cx2; + Cy = Cy2; } else if (N2 == 0.0) { @@ -3532,6 +3635,8 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sy = Sy1; Syy = Syy1; Sxy = Sxy1; + Cx = Cx1; + Cy = Cy1; } else { @@ -3549,6 +3654,14 @@ float8_regr_combine(PG_FUNCTION_ARGS) Sxy = Sxy1 + Sxy2 + N1 * N2 * tmp1 * tmp2 / N; if (unlikely(isinf(Sxy)) && !isinf(Sxy1) && !isinf(Sxy2)) float_overflow_error(); + if (float8_eq(Cx1, Cx2)) + Cx = Cx1; + else + Cx = get_float8_nan(); + if (float8_eq(Cy1, Cy2)) + Cy = Cy1; + else + Cy = get_float8_nan(); } /* @@ -3564,12 +3677,14 @@ float8_regr_combine(PG_FUNCTION_ARGS) transvalues1[3] = Sy; transvalues1[4] = Syy; transvalues1[5] = Sxy; + transvalues1[6] = Cx; + transvalues1[7] = Cy; PG_RETURN_ARRAYTYPE_P(transarray1); } else { - Datum transdatums[6]; + Datum transdatums[8]; ArrayType *result; transdatums[0] = Float8GetDatumFast(N); @@ -3578,8 +3693,10 @@ float8_regr_combine(PG_FUNCTION_ARGS) transdatums[3] = Float8GetDatumFast(Sy); transdatums[4] = Float8GetDatumFast(Syy); transdatums[5] = Float8GetDatumFast(Sxy); + transdatums[6] = Float8GetDatumFast(Cx); + transdatums[7] = Float8GetDatumFast(Cy); - result = construct_array_builtin(transdatums, 6, FLOAT8OID); + result = construct_array_builtin(transdatums, 8, FLOAT8OID); PG_RETURN_ARRAYTYPE_P(result); } @@ -3594,7 +3711,7 @@ float8_regr_sxx(PG_FUNCTION_ARGS) float8 N, Sxx; - transvalues = check_float8_array(transarray, "float8_regr_sxx", 6); + transvalues = check_float8_array(transarray, "float8_regr_sxx", 8); N = transvalues[0]; Sxx = transvalues[2]; @@ -3615,7 +3732,7 @@ float8_regr_syy(PG_FUNCTION_ARGS) float8 N, Syy; - transvalues = check_float8_array(transarray, "float8_regr_syy", 6); + transvalues = check_float8_array(transarray, "float8_regr_syy", 8); N = transvalues[0]; Syy = transvalues[4]; @@ -3636,7 +3753,7 @@ float8_regr_sxy(PG_FUNCTION_ARGS) float8 N, Sxy; - transvalues = check_float8_array(transarray, "float8_regr_sxy", 6); + transvalues = check_float8_array(transarray, "float8_regr_sxy", 8); N = transvalues[0]; Sxy = transvalues[5]; @@ -3655,16 +3772,22 @@ float8_regr_avgx(PG_FUNCTION_ARGS) ArrayType *transarray = PG_GETARG_ARRAYTYPE_P(0); float8 *transvalues; float8 N, - Sx; + Sx, + commonX; - transvalues = check_float8_array(transarray, "float8_regr_avgx", 6); + transvalues = check_float8_array(transarray, "float8_regr_avgx", 8); N = transvalues[0]; Sx = transvalues[1]; + commonX = transvalues[6]; /* if N is 0 we should return NULL */ if (N < 1.0) PG_RETURN_NULL(); + /* if all inputs were the same just return that, avoiding roundoff error */ + if (!isnan(commonX)) + PG_RETURN_FLOAT8(commonX); + PG_RETURN_FLOAT8(Sx / N); } @@ -3674,16 +3797,22 @@ float8_regr_avgy(PG_FUNCTION_ARGS) ArrayType *transarray = PG_GETARG_ARRAYTYPE_P(0); float8 *transvalues; float8 N, - Sy; + Sy, + commonY; - transvalues = check_float8_array(transarray, "float8_regr_avgy", 6); + transvalues = check_float8_array(transarray, "float8_regr_avgy", 8); N = transvalues[0]; Sy = transvalues[3]; + commonY = transvalues[7]; /* if N is 0 we should return NULL */ if (N < 1.0) PG_RETURN_NULL(); + /* if all inputs were the same just return that, avoiding roundoff error */ + if (!isnan(commonY)) + PG_RETURN_FLOAT8(commonY); + PG_RETURN_FLOAT8(Sy / N); } @@ -3695,7 +3824,7 @@ float8_covar_pop(PG_FUNCTION_ARGS) float8 N, Sxy; - transvalues = check_float8_array(transarray, "float8_covar_pop", 6); + transvalues = check_float8_array(transarray, "float8_covar_pop", 8); N = transvalues[0]; Sxy = transvalues[5]; @@ -3714,7 +3843,7 @@ float8_covar_samp(PG_FUNCTION_ARGS) float8 N, Sxy; - transvalues = check_float8_array(transarray, "float8_covar_samp", 6); + transvalues = check_float8_array(transarray, "float8_covar_samp", 8); N = transvalues[0]; Sxy = transvalues[5]; @@ -3733,9 +3862,12 @@ float8_corr(PG_FUNCTION_ARGS) float8 N, Sxx, Syy, - Sxy; + Sxy, + product, + sqrtproduct, + result; - transvalues = check_float8_array(transarray, "float8_corr", 6); + transvalues = check_float8_array(transarray, "float8_corr", 8); N = transvalues[0]; Sxx = transvalues[2]; Syy = transvalues[4]; @@ -3751,7 +3883,29 @@ float8_corr(PG_FUNCTION_ARGS) if (Sxx == 0 || Syy == 0) PG_RETURN_NULL(); - PG_RETURN_FLOAT8(Sxy / sqrt(Sxx * Syy)); + /* + * The product Sxx * Syy might underflow or overflow. If so, we can + * recover by computing sqrt(Sxx) * sqrt(Syy) instead of sqrt(Sxx * Syy). + * However, the double sqrt() calculation is a bit slower and less + * accurate, so don't do it if we don't have to. + */ + product = Sxx * Syy; + if (product == 0 || isinf(product)) + sqrtproduct = sqrt(Sxx) * sqrt(Syy); + else + sqrtproduct = sqrt(product); + result = Sxy / sqrtproduct; + + /* + * Despite all these precautions, this formula can yield results outside + * [-1, 1] due to roundoff error. Clamp it to the expected range. + */ + if (result < -1) + result = -1; + else if (result > 1) + result = 1; + + PG_RETURN_FLOAT8(result); } Datum @@ -3764,7 +3918,7 @@ float8_regr_r2(PG_FUNCTION_ARGS) Syy, Sxy; - transvalues = check_float8_array(transarray, "float8_regr_r2", 6); + transvalues = check_float8_array(transarray, "float8_regr_r2", 8); N = transvalues[0]; Sxx = transvalues[2]; Syy = transvalues[4]; @@ -3796,7 +3950,7 @@ float8_regr_slope(PG_FUNCTION_ARGS) Sxx, Sxy; - transvalues = check_float8_array(transarray, "float8_regr_slope", 6); + transvalues = check_float8_array(transarray, "float8_regr_slope", 8); N = transvalues[0]; Sxx = transvalues[2]; Sxy = transvalues[5]; @@ -3825,7 +3979,7 @@ float8_regr_intercept(PG_FUNCTION_ARGS) Sy, Sxy; - transvalues = check_float8_array(transarray, "float8_regr_intercept", 6); + transvalues = check_float8_array(transarray, "float8_regr_intercept", 8); N = transvalues[0]; Sx = transvalues[1]; Sxx = transvalues[2]; @@ -4065,10 +4219,11 @@ float84ge(PG_FUNCTION_ARGS) * in the histogram. width_bucket() returns an integer indicating the * bucket number that 'operand' belongs to in an equiwidth histogram * with the specified characteristics. An operand smaller than the - * lower bound is assigned to bucket 0. An operand greater than the - * upper bound is assigned to an additional bucket (with number - * count+1). We don't allow "NaN" for any of the float8 inputs, and we - * don't allow either of the histogram bounds to be +/- infinity. + * lower bound is assigned to bucket 0. An operand greater than or equal + * to the upper bound is assigned to an additional bucket (with number + * count+1). We don't allow the histogram bounds to be NaN or +/- infinity, + * but we do allow those values for the operand (taking NaN to be larger + * than any other value, as we do in comparisons). */ Datum width_bucket_float8(PG_FUNCTION_ARGS) @@ -4084,12 +4239,11 @@ width_bucket_float8(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION), errmsg("count must be greater than zero"))); - if (isnan(operand) || isnan(bound1) || isnan(bound2)) + if (isnan(bound1) || isnan(bound2)) ereport(ERROR, (errcode(ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION), - errmsg("operand, lower bound, and upper bound cannot be NaN"))); + errmsg("lower and upper bounds cannot be NaN"))); - /* Note that we allow "operand" to be infinite */ if (isinf(bound1) || isinf(bound2)) ereport(ERROR, (errcode(ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION), @@ -4097,15 +4251,15 @@ width_bucket_float8(PG_FUNCTION_ARGS) if (bound1 < bound2) { - if (operand < bound1) - result = 0; - else if (operand >= bound2) + if (isnan(operand) || operand >= bound2) { if (pg_add_s32_overflow(count, 1, &result)) ereport(ERROR, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("integer out of range"))); } + else if (operand < bound1) + result = 0; else { if (!isinf(bound2 - bound1)) @@ -4135,7 +4289,7 @@ width_bucket_float8(PG_FUNCTION_ARGS) } else if (bound1 > bound2) { - if (operand > bound1) + if (isnan(operand) || operand > bound1) result = 0; else if (operand <= bound2) { diff --git a/src/backend/utils/adt/format_type.c b/src/backend/utils/adt/format_type.c index 9948c26e76cd5..7a5695c624565 100644 --- a/src/backend/utils/adt/format_type.c +++ b/src/backend/utils/adt/format_type.c @@ -4,7 +4,7 @@ * Display type names "nicely". * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -62,7 +62,7 @@ format_type(PG_FUNCTION_ARGS) Oid type_oid; int32 typemod; char *result; - bits16 flags = FORMAT_TYPE_ALLOW_INVALID; + uint16 flags = FORMAT_TYPE_ALLOW_INVALID; /* Since this function is not strict, we must test for null args */ if (PG_ARGISNULL(0)) @@ -109,7 +109,7 @@ format_type(PG_FUNCTION_ARGS) * Returns a palloc'd string, or NULL. */ char * -format_type_extended(Oid type_oid, int32 typemod, bits16 flags) +format_type_extended(Oid type_oid, int32 typemod, uint16 flags) { HeapTuple tuple; Form_pg_type typeform; @@ -378,7 +378,7 @@ printTypmod(const char *typname, int32 typmod, Oid typmodout) if (typmodout == InvalidOid) { /* Default behavior: just print the integer typmod with parens */ - res = psprintf("%s(%d)", typname, (int) typmod); + res = psprintf("%s(%d)", typname, typmod); } else { @@ -448,11 +448,15 @@ oidvectortypes(PG_FUNCTION_ARGS) { oidvector *oidArray = (oidvector *) PG_GETARG_POINTER(0); char *result; - int numargs = oidArray->dim1; + int numargs; int num; size_t total; size_t left; + /* validate input before fetching dim1 */ + check_valid_oidvector(oidArray); + numargs = oidArray->dim1; + total = 20 * numargs + 1; result = palloc(total); result[0] = '\0'; diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c index 5bd1e01f7e463..9a8c99336b521 100644 --- a/src/backend/utils/adt/formatting.c +++ b/src/backend/utils/adt/formatting.c @@ -1,10 +1,10 @@ -/* ----------------------------------------------------------------------- +/*------------------------------------------------------------------------- * formatting.c * * src/backend/utils/adt/formatting.c * * - * Portions Copyright (c) 1999-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1999-2026, PostgreSQL Global Development Group * * * TO_CHAR(); TO_TIMESTAMP(); TO_DATE(); TO_NUMBER(); @@ -54,7 +54,7 @@ * than Oracle :-), * to_char('Hello', 'X X X X X') -> 'H e l l o' * - * ----------------------------------------------------------------------- + *------------------------------------------------------------------------- */ #ifdef DEBUG_TO_FROM_CHAR @@ -68,17 +68,9 @@ #include #include #include -#include -#ifdef USE_ICU -#include -#endif - -#include "catalog/pg_collation.h" #include "catalog/pg_type.h" #include "common/int.h" -#include "common/unicode_case.h" -#include "common/unicode_category.h" #include "mb/pg_wchar.h" #include "nodes/miscnodes.h" #include "parser/scansup.h" @@ -92,44 +84,46 @@ #include "varatt.h" -/* ---------- +/* * Routines flags - * ---------- */ #define DCH_FLAG 0x1 /* DATE-TIME flag */ #define NUM_FLAG 0x2 /* NUMBER flag */ #define STD_FLAG 0x4 /* STANDARD flag */ -/* ---------- +/* * KeyWord Index (ascii from position 32 (' ') to 126 (~)) - * ---------- */ #define KeyWord_INDEX_SIZE ('~' - ' ') #define KeyWord_INDEX_FILTER(_c) ((_c) <= ' ' || (_c) >= '~' ? 0 : 1) -/* ---------- +/* * Maximal length of one node - * ---------- */ #define DCH_MAX_ITEM_SIZ 12 /* max localized day name */ #define NUM_MAX_ITEM_SIZ 8 /* roman number (RN has 15 chars) */ -/* ---------- +/* * Format parser structs - * ---------- */ + +enum KeySuffixType +{ + SUFFTYPE_PREFIX = 1, + SUFFTYPE_POSTFIX = 2, +}; + typedef struct { const char *name; /* suffix string */ - int len, /* suffix length */ - id, /* used in node->suffix */ - type; /* prefix / postfix */ + size_t len; /* suffix length */ + int id; /* used in node->suffix */ + enum KeySuffixType type; /* prefix / postfix */ } KeySuffix; -/* ---------- +/* * FromCharDateMode - * ---------- * * This value is used to nominate one of several distinct (and mutually * exclusive) date conventions that a keyword can belong to. @@ -144,36 +138,33 @@ typedef enum typedef struct { const char *name; - int len; + size_t len; int id; bool is_digit; FromCharDateMode date_mode; } KeyWord; +enum FormatNodeType +{ + NODE_TYPE_END = 1, + NODE_TYPE_ACTION = 2, + NODE_TYPE_CHAR = 3, + NODE_TYPE_SEPARATOR = 4, + NODE_TYPE_SPACE = 5, +}; + typedef struct { - uint8 type; /* NODE_TYPE_XXX, see below */ + enum FormatNodeType type; char character[MAX_MULTIBYTE_CHAR_LEN + 1]; /* if type is CHAR */ - uint8 suffix; /* keyword prefix/suffix code, if any */ + uint8 suffix; /* keyword prefix/suffix code, if any + * (DCH_SUFFIX_*) */ const KeyWord *key; /* if type is ACTION */ } FormatNode; -#define NODE_TYPE_END 1 -#define NODE_TYPE_ACTION 2 -#define NODE_TYPE_CHAR 3 -#define NODE_TYPE_SEPARATOR 4 -#define NODE_TYPE_SPACE 5 - -#define SUFFTYPE_PREFIX 1 -#define SUFFTYPE_POSTFIX 2 - -#define CLOCK_24_HOUR 0 -#define CLOCK_12_HOUR 1 - -/* ---------- +/* * Full months - * ---------- */ static const char *const months_full[] = { "January", "February", "March", "April", "May", "June", "July", @@ -184,9 +175,9 @@ static const char *const days_short[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", NULL }; -/* ---------- +/* * AD / BC - * ---------- + * * There is no 0 AD. Years go from 1 BC to 1 AD, so we make it * positive and map year == -1 to year zero, and shift all negative * years up one. For interval years, we just return the year. @@ -216,9 +207,8 @@ static const char *const days_short[] = { static const char *const adbc_strings[] = {ad_STR, bc_STR, AD_STR, BC_STR, NULL}; static const char *const adbc_strings_long[] = {a_d_STR, b_c_STR, A_D_STR, B_C_STR, NULL}; -/* ---------- +/* * AM / PM - * ---------- */ #define A_M_STR "A.M." #define a_m_STR "a.m." @@ -243,11 +233,10 @@ static const char *const adbc_strings_long[] = {a_d_STR, b_c_STR, A_D_STR, B_C_S static const char *const ampm_strings[] = {am_STR, pm_STR, AM_STR, PM_STR, NULL}; static const char *const ampm_strings_long[] = {a_m_STR, p_m_STR, A_M_STR, P_M_STR, NULL}; -/* ---------- +/* * Months in roman-numeral * (Must be in reverse order for seq_search (in FROM_CHAR), because * 'VIII' must have higher precedence than 'V') - * ---------- */ static const char *const rm_months_upper[] = {"XII", "XI", "X", "IX", "VIII", "VII", "VI", "V", "IV", "III", "II", "I", NULL}; @@ -255,9 +244,8 @@ static const char *const rm_months_upper[] = static const char *const rm_months_lower[] = {"xii", "xi", "x", "ix", "viii", "vii", "vi", "v", "iv", "iii", "ii", "i", NULL}; -/* ---------- +/* * Roman numerals - * ---------- */ static const char *const rm1[] = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", NULL}; static const char *const rm10[] = {"X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC", NULL}; @@ -289,40 +277,46 @@ static const char *const rm100[] = {"C", "CC", "CCC", "CD", "D", "DC", "DCC", "D */ #define MAX_ROMAN_LEN 15 -/* ---------- +/* * Ordinal postfixes - * ---------- */ static const char *const numTH[] = {"ST", "ND", "RD", "TH", NULL}; static const char *const numth[] = {"st", "nd", "rd", "th", NULL}; -/* ---------- +/* * Flags & Options: - * ---------- */ -#define TH_UPPER 1 -#define TH_LOWER 2 +enum TH_Case +{ + TH_UPPER = 1, + TH_LOWER = 2, +}; + +enum NUMDesc_lsign +{ + NUM_LSIGN_PRE = -1, + NUM_LSIGN_POST = 1, + NUM_LSIGN_NONE = 0, +}; -/* ---------- +/* * Number description struct - * ---------- */ typedef struct { - int pre, /* (count) numbers before decimal */ - post, /* (count) numbers after decimal */ - lsign, /* want locales sign */ - flag, /* number parameters */ - pre_lsign_num, /* tmp value for lsign */ - multi, /* multiplier for 'V' */ - zero_start, /* position of first zero */ - zero_end, /* position of last zero */ - need_locale; /* needs it locale */ + int pre; /* (count) numbers before decimal */ + int post; /* (count) numbers after decimal */ + enum NUMDesc_lsign lsign; /* want locales sign */ + int flag; /* number parameters (NUM_F_*) */ + int pre_lsign_num; /* tmp value for lsign */ + int multi; /* multiplier for 'V' */ + int zero_start; /* position of first zero */ + int zero_end; /* position of last zero */ + bool need_locale; /* needs it locale */ } NUMDesc; -/* ---------- +/* * Flags for NUMBER version - * ---------- */ #define NUM_F_DECIMAL (1 << 1) #define NUM_F_LDECIMAL (1 << 2) @@ -339,13 +333,8 @@ typedef struct #define NUM_F_MINUS_POST (1 << 13) #define NUM_F_EEEE (1 << 14) -#define NUM_LSIGN_PRE (-1) -#define NUM_LSIGN_POST 1 -#define NUM_LSIGN_NONE 0 - -/* ---------- +/* * Tests - * ---------- */ #define IS_DECIMAL(_f) ((_f)->flag & NUM_F_DECIMAL) #define IS_LDECIMAL(_f) ((_f)->flag & NUM_F_LDECIMAL) @@ -360,7 +349,7 @@ typedef struct #define IS_MULTI(_f) ((_f)->flag & NUM_F_MULTI) #define IS_EEEE(_f) ((_f)->flag & NUM_F_EEEE) -/* ---------- +/* * Format picture cache * * We will cache datetime format pictures up to DCH_CACHE_SIZE bytes long; @@ -376,7 +365,6 @@ typedef struct * * The max number of entries in each cache is DCH_CACHE_ENTRIES * resp. NUM_CACHE_ENTRIES. - * ---------- */ #define DCH_CACHE_OVERHEAD \ MAXALIGN(sizeof(bool) + sizeof(int)) @@ -419,53 +407,49 @@ static NUMCacheEntry *NUMCache[NUM_CACHE_ENTRIES]; static int n_NUMCache = 0; /* current number of entries */ static int NUMCounter = 0; /* aging-event counter */ -/* ---------- +/* * For char->date/time conversion - * ---------- */ typedef struct { FromCharDateMode mode; - int hh, - pm, - mi, - ss, - ssss, - d, /* stored as 1-7, Sunday = 1, 0 means missing */ - dd, - ddd, - mm, - ms, - year, - bc, - ww, - w, - cc, - j, - us, - yysz, /* is it YY or YYYY ? */ - clock, /* 12 or 24 hour clock? */ - tzsign, /* +1, -1, or 0 if no TZH/TZM fields */ - tzh, - tzm, - ff; /* fractional precision */ + int hh; + int pm; + int mi; + int ss; + int ssss; + int d; /* stored as 1-7, Sunday = 1, 0 means missing */ + int dd; + int ddd; + int mm; + int ms; + int year; + int bc; + int ww; + int w; + int cc; + int j; + int us; + int yysz; /* is it YY or YYYY ? */ + bool clock_12_hour; /* 12 or 24 hour clock? */ + int tzsign; /* +1, -1, or 0 if no TZH/TZM fields */ + int tzh; + int tzm; + int ff; /* fractional precision */ bool has_tz; /* was there a TZ field? */ int gmtoffset; /* GMT offset of fixed-offset zone abbrev */ pg_tz *tzp; /* pg_tz for dynamic abbrev */ - char *abbrev; /* dynamic abbrev */ + const char *abbrev; /* dynamic abbrev */ } TmFromChar; -#define ZERO_tmfc(_X) memset(_X, 0, sizeof(TmFromChar)) - struct fmt_tz /* do_to_timestamp's timezone info output */ { bool has_tz; /* was there any TZ/TZH/TZM field? */ int gmtoffset; /* GMT offset in seconds */ }; -/* ---------- +/* * Debug - * ---------- */ #ifdef DEBUG_TO_FROM_CHAR #define DEBUG_TMFC(_X) \ @@ -473,7 +457,7 @@ struct fmt_tz /* do_to_timestamp's timezone info output */ (_X)->mode, (_X)->hh, (_X)->pm, (_X)->mi, (_X)->ss, (_X)->ssss, \ (_X)->d, (_X)->dd, (_X)->ddd, (_X)->mm, (_X)->ms, (_X)->year, \ (_X)->bc, (_X)->ww, (_X)->w, (_X)->cc, (_X)->j, (_X)->us, \ - (_X)->yysz, (_X)->clock) + (_X)->yysz, (_X)->clock_12_hour) #define DEBUG_TM(_X) \ elog(DEBUG_elog_output, "TM:\nsec %d\nyear %d\nmin %d\nwday %d\nhour %d\nyday %d\nmday %d\nnisdst %d\nmon %d\n",\ (_X)->tm_sec, (_X)->tm_year,\ @@ -484,13 +468,12 @@ struct fmt_tz /* do_to_timestamp's timezone info output */ #define DEBUG_TM(_X) #endif -/* ---------- +/* * Datetime to char conversion * * To support intervals as well as timestamps, we use a custom "tm" struct * that is almost like struct pg_tm, but has a 64-bit tm_hour field. * We omit the tm_isdst and tm_zone fields, which are not used here. - * ---------- */ struct fmt_tm { @@ -561,50 +544,74 @@ do { \ * KeyWord definitions *****************************************************************************/ -/* ---------- +/* * Suffixes (FormatNode.suffix is an OR of these codes) - * ---------- */ -#define DCH_S_FM 0x01 -#define DCH_S_TH 0x02 -#define DCH_S_th 0x04 -#define DCH_S_SP 0x08 -#define DCH_S_TM 0x10 +#define DCH_SUFFIX_FM 0x01 +#define DCH_SUFFIX_TH 0x02 +#define DCH_SUFFIX_th 0x04 +#define DCH_SUFFIX_SP 0x08 +#define DCH_SUFFIX_TM 0x10 -/* ---------- +/* * Suffix tests - * ---------- */ -#define S_THth(_s) ((((_s) & DCH_S_TH) || ((_s) & DCH_S_th)) ? 1 : 0) -#define S_TH(_s) (((_s) & DCH_S_TH) ? 1 : 0) -#define S_th(_s) (((_s) & DCH_S_th) ? 1 : 0) -#define S_TH_TYPE(_s) (((_s) & DCH_S_TH) ? TH_UPPER : TH_LOWER) +static inline bool +IS_SUFFIX_TH(uint8 _s) +{ + return (_s & DCH_SUFFIX_TH); +} + +static inline bool +IS_SUFFIX_th(uint8 _s) +{ + return (_s & DCH_SUFFIX_th); +} + +static inline bool +IS_SUFFIX_THth(uint8 _s) +{ + return IS_SUFFIX_TH(_s) || IS_SUFFIX_th(_s); +} + +static inline enum TH_Case +SUFFIX_TH_TYPE(uint8 _s) +{ + return _s & DCH_SUFFIX_TH ? TH_UPPER : TH_LOWER; +} /* Oracle toggles FM behavior, we don't; see docs. */ -#define S_FM(_s) (((_s) & DCH_S_FM) ? 1 : 0) -#define S_SP(_s) (((_s) & DCH_S_SP) ? 1 : 0) -#define S_TM(_s) (((_s) & DCH_S_TM) ? 1 : 0) +static inline bool +IS_SUFFIX_FM(uint8 _s) +{ + return (_s & DCH_SUFFIX_FM); +} + +static inline bool +IS_SUFFIX_TM(uint8 _s) +{ + return (_s & DCH_SUFFIX_TM); +} -/* ---------- +/* * Suffixes definition for DATE-TIME TO/FROM CHAR - * ---------- */ #define TM_SUFFIX_LEN 2 static const KeySuffix DCH_suff[] = { - {"FM", 2, DCH_S_FM, SUFFTYPE_PREFIX}, - {"fm", 2, DCH_S_FM, SUFFTYPE_PREFIX}, - {"TM", TM_SUFFIX_LEN, DCH_S_TM, SUFFTYPE_PREFIX}, - {"tm", 2, DCH_S_TM, SUFFTYPE_PREFIX}, - {"TH", 2, DCH_S_TH, SUFFTYPE_POSTFIX}, - {"th", 2, DCH_S_th, SUFFTYPE_POSTFIX}, - {"SP", 2, DCH_S_SP, SUFFTYPE_POSTFIX}, + {"FM", 2, DCH_SUFFIX_FM, SUFFTYPE_PREFIX}, + {"fm", 2, DCH_SUFFIX_FM, SUFFTYPE_PREFIX}, + {"TM", TM_SUFFIX_LEN, DCH_SUFFIX_TM, SUFFTYPE_PREFIX}, + {"tm", 2, DCH_SUFFIX_TM, SUFFTYPE_PREFIX}, + {"TH", 2, DCH_SUFFIX_TH, SUFFTYPE_POSTFIX}, + {"th", 2, DCH_SUFFIX_th, SUFFTYPE_POSTFIX}, + {"SP", 2, DCH_SUFFIX_SP, SUFFTYPE_POSTFIX}, /* last */ {NULL, 0, 0, 0} }; -/* ---------- +/* * Format-pictures (KeyWord). * * The KeyWord field; alphabetic sorted, *BUT* strings alike is sorted @@ -628,8 +635,6 @@ static const KeySuffix DCH_suff[] = { * 1) see in index to index['M' - 32], * 2) take keywords position (enum DCH_MI) from index * 3) run sequential search in keywords[] from this position - * - * ---------- */ typedef enum @@ -794,9 +799,8 @@ typedef enum _NUM_last_ } NUM_poz; -/* ---------- +/* * KeyWords for DATE-TIME version - * ---------- */ static const KeyWord DCH_keywords[] = { /* name, len, id, is_digit, date_mode */ @@ -917,11 +921,10 @@ static const KeyWord DCH_keywords[] = { {NULL, 0, 0, 0, 0} }; -/* ---------- +/* * KeyWords for NUMBER version * * The is_digit and date_mode fields are not relevant here. - * ---------- */ static const KeyWord NUM_keywords[] = { /* name, len, id is in Index */ @@ -967,9 +970,8 @@ static const KeyWord NUM_keywords[] = { }; -/* ---------- +/* * KeyWords index for DATE-TIME version - * ---------- */ static const int DCH_index[KeyWord_INDEX_SIZE] = { /* @@ -991,9 +993,8 @@ static const int DCH_index[KeyWord_INDEX_SIZE] = { /*---- chars over 126 are skipped ----*/ }; -/* ---------- +/* * KeyWords index for NUMBER version - * ---------- */ static const int NUM_index[KeyWord_INDEX_SIZE] = { /* @@ -1015,9 +1016,8 @@ static const int NUM_index[KeyWord_INDEX_SIZE] = { /*---- chars over 126 are skipped ----*/ }; -/* ---------- +/* * Number processor struct - * ---------- */ typedef struct NUMProc { @@ -1038,8 +1038,9 @@ typedef struct NUMProc char *number, /* string with number */ *number_p, /* pointer to current number position */ *inout, /* in / out buffer */ - *inout_p, /* pointer to current inout position */ - *last_relevant, /* last relevant number after decimal point */ + *inout_p; /* pointer to current inout position */ + + const char *last_relevant, /* last relevant number after decimal point */ *L_negative_sign, /* Locale */ *L_positive_sign, @@ -1062,13 +1063,12 @@ typedef struct NUMProc #define AMOUNT_TEST(s) (Np->inout_p <= Np->inout + (input_len - (s))) -/* ---------- +/* * Functions - * ---------- */ static const KeyWord *index_seq_search(const char *str, const KeyWord *kw, const int *index); -static const KeySuffix *suff_search(const char *str, const KeySuffix *suf, int type); +static const KeySuffix *suff_search(const char *str, const KeySuffix *suf, enum KeySuffixType type); static bool is_separator_char(const char *str); static void NUMDesc_prepare(NUMDesc *num, FormatNode *n); static void parse_format(FormatNode *node, const char *str, const KeyWord *kw, @@ -1084,38 +1084,39 @@ static void dump_index(const KeyWord *k, const int *index); static void dump_node(FormatNode *node, int max); #endif -static const char *get_th(char *num, int type); -static char *str_numth(char *dest, char *num, int type); +static const char *get_th(const char *num, enum TH_Case type); +static char *str_numth(char *dest, const char *num, enum TH_Case type); static int adjust_partial_year_to_2020(int year); -static int strspace_len(const char *str); +static size_t strspace_len(const char *str); static bool from_char_set_mode(TmFromChar *tmfc, const FromCharDateMode mode, Node *escontext); static bool from_char_set_int(int *dest, const int value, const FormatNode *node, Node *escontext); -static int from_char_parse_int_len(int *dest, const char **src, const int len, +static int from_char_parse_int_len(int *dest, const char **src, const size_t len, FormatNode *node, Node *escontext); static int from_char_parse_int(int *dest, const char **src, FormatNode *node, Node *escontext); -static int seq_search_ascii(const char *name, const char *const *array, int *len); -static int seq_search_localized(const char *name, char **array, int *len, +static int seq_search_ascii(const char *name, const char *const *array, size_t *len); +static int seq_search_localized(const char *name, char **array, size_t *len, Oid collid); static bool from_char_seq_search(int *dest, const char **src, const char *const *array, char **localized_array, Oid collid, FormatNode *node, Node *escontext); -static bool do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, +static bool do_to_timestamp(const text *date_txt, const text *fmt, Oid collid, bool std, struct pg_tm *tm, fsec_t *fsec, struct fmt_tz *tz, int *fprec, uint32 *flags, Node *escontext); -static char *fill_str(char *str, int c, int max); -static FormatNode *NUM_cache(int len, NUMDesc *Num, text *pars_str, bool *shouldFree); +static void fill_str(char *str, int c, int max); +static FormatNode *NUM_cache(int len, NUMDesc *Num, const text *pars_str, bool *shouldFree); static char *int_to_roman(int number); -static int roman_to_int(NUMProc *Np, int input_len); +static int roman_to_int(NUMProc *Np, size_t input_len); static void NUM_prepare_locale(NUMProc *Np); -static char *get_last_relevant_decnum(char *num); -static void NUM_numpart_from_char(NUMProc *Np, int id, int input_len); +static const char *get_last_relevant_decnum(const char *num); +static void NUM_numpart_from_char(NUMProc *Np, int id, size_t input_len); static void NUM_numpart_to_char(NUMProc *Np, int id); +static void NUM_add_locale_symbol(NUMProc *Np, const char *pattern); static char *NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, - char *number, int input_len, int to_char_out_pre_spaces, + char *number, size_t input_len, int to_char_out_pre_spaces, int sign, bool is_to_char, Oid collid); static DCHCacheEntry *DCH_cache_getnew(const char *str, bool std); static DCHCacheEntry *DCH_cache_search(const char *str, bool std); @@ -1125,11 +1126,10 @@ static NUMCacheEntry *NUM_cache_search(const char *str); static NUMCacheEntry *NUM_cache_fetch(const char *str); -/* ---------- +/* * Fast sequential search, use index for data selection which * go to seq. cycle (it is very fast for unwanted strings) * (can't be used binary search in format parsing) - * ---------- */ static const KeyWord * index_seq_search(const char *str, const KeyWord *kw, const int *index) @@ -1139,7 +1139,7 @@ index_seq_search(const char *str, const KeyWord *kw, const int *index) if (!KeyWord_INDEX_FILTER(*str)) return NULL; - if ((poz = *(index + (*str - ' '))) > -1) + if ((poz = index[*str - ' ']) > -1) { const KeyWord *k = kw + poz; @@ -1156,11 +1156,9 @@ index_seq_search(const char *str, const KeyWord *kw, const int *index) } static const KeySuffix * -suff_search(const char *str, const KeySuffix *suf, int type) +suff_search(const char *str, const KeySuffix *suf, enum KeySuffixType type) { - const KeySuffix *s; - - for (s = suf; s->name != NULL; s++) + for (const KeySuffix *s = suf; s->name != NULL; s++) { if (s->type != type) continue; @@ -1181,9 +1179,8 @@ is_separator_char(const char *str) !(*str >= '0' && *str <= '9')); } -/* ---------- +/* * Prepare NUMDesc (number description struct) via FormatNode struct - * ---------- */ static void NUMDesc_prepare(NUMDesc *num, FormatNode *n) @@ -1233,14 +1230,14 @@ NUMDesc_prepare(NUMDesc *num, FormatNode *n) break; case NUM_B: - if (num->pre == 0 && num->post == 0 && (!IS_ZERO(num))) + if (num->pre == 0 && num->post == 0 && !IS_ZERO(num)) num->flag |= NUM_F_BLANK; break; case NUM_D: num->flag |= NUM_F_LDECIMAL; num->need_locale = true; - /* FALLTHROUGH */ + pg_fallthrough; case NUM_DEC: if (IS_DECIMAL(num)) ereport(ERROR, @@ -1364,12 +1361,11 @@ NUMDesc_prepare(NUMDesc *num, FormatNode *n) errdetail("\"RN\" may only be used together with \"FM\"."))); } -/* ---------- +/* * Format parser, search small keywords and keyword's suffixes, and make * format-node tree. * * for DATE-TIME & NUMBER version - * ---------- */ static void parse_format(FormatNode *node, const char *str, const KeyWord *kw, @@ -1443,7 +1439,7 @@ parse_format(FormatNode *node, const char *str, const KeyWord *kw, ereport(ERROR, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("invalid datetime format separator: \"%s\"", - pnstrdup(str, pg_mblen(str))))); + pnstrdup(str, pg_mblen_cstr(str))))); if (*str == ' ') n->type = NODE_TYPE_SPACE; @@ -1473,7 +1469,7 @@ parse_format(FormatNode *node, const char *str, const KeyWord *kw, /* backslash quotes the next character, if any */ if (*str == '\\' && *(str + 1)) str++; - chlen = pg_mblen(str); + chlen = pg_mblen_cstr(str); n->type = NODE_TYPE_CHAR; memcpy(n->character, str, chlen); n->character[chlen] = '\0'; @@ -1491,7 +1487,7 @@ parse_format(FormatNode *node, const char *str, const KeyWord *kw, */ if (*str == '\\' && *(str + 1) == '"') str++; - chlen = pg_mblen(str); + chlen = pg_mblen_cstr(str); if ((flags & DCH_FLAG) && is_separator_char(str)) n->type = NODE_TYPE_SEPARATOR; @@ -1514,14 +1510,13 @@ parse_format(FormatNode *node, const char *str, const KeyWord *kw, n->suffix = 0; } -/* ---------- +/* * DEBUG: Dump the FormatNode Tree (debug) - * ---------- */ #ifdef DEBUG_TO_FROM_CHAR -#define DUMP_THth(_suf) (S_TH(_suf) ? "TH" : (S_th(_suf) ? "th" : " ")) -#define DUMP_FM(_suf) (S_FM(_suf) ? "FM" : " ") +#define DUMP_THth(_suf) (IS_SUFFIX_TH(_suf) ? "TH" : (IS_SUFFIX_th(_suf) ? "th" : " ")) +#define DUMP_FM(_suf) (IS_SUFFIX_FM(_suf) ? "FM" : " ") static void dump_node(FormatNode *node, int max) @@ -1554,18 +1549,18 @@ dump_node(FormatNode *node, int max) * Private utils *****************************************************************************/ -/* ---------- +/* * Return ST/ND/RD/TH for simple (1..9) numbers - * type --> 0 upper, 1 lower - * ---------- */ static const char * -get_th(char *num, int type) +get_th(const char *num, enum TH_Case type) { - int len = strlen(num), - last; + size_t len = strlen(num); + char last; + + Assert(len > 0); - last = *(num + (len - 1)); + last = num[len - 1]; if (!isdigit((unsigned char) last)) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), @@ -1575,7 +1570,7 @@ get_th(char *num, int type) * All "teens" (1[0-9]) get 'TH/th', while [02-9][123] still get * 'ST/st', 'ND/nd', 'RD/rd', respectively */ - if ((len > 1) && (num[len - 2] == '1')) + if (len > 1 && num[len - 2] == '1') last = 0; switch (last) @@ -1599,13 +1594,11 @@ get_th(char *num, int type) } } -/* ---------- +/* * Convert string-number to ordinal string-number - * type --> 0 upper, 1 lower - * ---------- */ static char * -str_numth(char *dest, char *num, int type) +str_numth(char *dest, const char *num, enum TH_Case type) { if (dest != num) strcpy(dest, num); @@ -1617,16 +1610,6 @@ str_numth(char *dest, char *num, int type) * upper/lower/initcap functions *****************************************************************************/ -/* - * If the system provides the needed functions for wide-character manipulation - * (which are all standardized by C99), then we implement upper/lower/initcap - * using wide-character functions, if necessary. Otherwise we use the - * traditional functions, which of course will not work as desired - * in multibyte character sets. Note that in either case we are effectively - * assuming that the database character encoding matches the encoding implied - * by LC_CTYPE. - */ - /* * collation-aware, wide-character-aware lower function * @@ -1843,7 +1826,7 @@ str_casefold(const char *buff, size_t nbytes, Oid collid) ereport(ERROR, (errcode(ERRCODE_INDETERMINATE_COLLATION), errmsg("could not determine which collation to use for %s function", - "lower()"), + "casefold()"), errhint("Use the COLLATE clause to set the collation explicitly."))); } @@ -1898,14 +1881,13 @@ char * asc_tolower(const char *buff, size_t nbytes) { char *result; - char *p; if (!buff) return NULL; result = pnstrdup(buff, nbytes); - for (p = result; *p; p++) + for (char *p = result; *p; p++) *p = pg_ascii_tolower((unsigned char) *p); return result; @@ -1921,14 +1903,13 @@ char * asc_toupper(const char *buff, size_t nbytes) { char *result; - char *p; if (!buff) return NULL; result = pnstrdup(buff, nbytes); - for (p = result; *p; p++) + for (char *p = result; *p; p++) *p = pg_ascii_toupper((unsigned char) *p); return result; @@ -1944,7 +1925,6 @@ char * asc_initcap(const char *buff, size_t nbytes) { char *result; - char *p; int wasalnum = false; if (!buff) @@ -1952,7 +1932,7 @@ asc_initcap(const char *buff, size_t nbytes) result = pnstrdup(buff, nbytes); - for (p = result; *p; p++) + for (char *p = result; *p; p++) { char c; @@ -2004,38 +1984,35 @@ asc_toupper_z(const char *buff) /* asc_initcap_z is not currently needed */ -/* ---------- +/* * Skip TM / th in FROM_CHAR * - * If S_THth is on, skip two chars, assuming there are two available - * ---------- + * If IS_SUFFIX_THth is on, skip two chars, assuming there are two available */ #define SKIP_THth(ptr, _suf) \ do { \ - if (S_THth(_suf)) \ + if (IS_SUFFIX_THth(_suf)) \ { \ - if (*(ptr)) (ptr) += pg_mblen(ptr); \ - if (*(ptr)) (ptr) += pg_mblen(ptr); \ + if (*(ptr)) (ptr) += pg_mblen_cstr(ptr); \ + if (*(ptr)) (ptr) += pg_mblen_cstr(ptr); \ } \ } while (0) #ifdef DEBUG_TO_FROM_CHAR -/* ----------- +/* * DEBUG: Call for debug and for index checking; (Show ASCII char * and defined keyword for each used position - * ---------- */ static void dump_index(const KeyWord *k, const int *index) { - int i, - count = 0, + int count = 0, free_i = 0; elog(DEBUG_elog_output, "TO-FROM_CHAR: Dump KeyWord Index:"); - for (i = 0; i < KeyWord_INDEX_SIZE; i++) + for (int i = 0; i < KeyWord_INDEX_SIZE; i++) { if (index[i] != -1) { @@ -2053,9 +2030,8 @@ dump_index(const KeyWord *k, const int *index) } #endif /* DEBUG */ -/* ---------- +/* * Return true if next format picture is not digit value - * ---------- */ static bool is_next_separator(FormatNode *n) @@ -2063,7 +2039,7 @@ is_next_separator(FormatNode *n) if (n->type == NODE_TYPE_END) return false; - if (n->type == NODE_TYPE_ACTION && S_THth(n->suffix)) + if (n->type == NODE_TYPE_ACTION && IS_SUFFIX_THth(n->suffix)) return true; /* @@ -2114,10 +2090,10 @@ adjust_partial_year_to_2020(int year) } -static int +static size_t strspace_len(const char *str) { - int len = 0; + size_t len = 0; while (*str && isspace((unsigned char) *str)) { @@ -2148,8 +2124,7 @@ from_char_set_mode(TmFromChar *tmfc, const FromCharDateMode mode, ereturn(escontext, false, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("invalid combination of date conventions"), - errhint("Do not mix Gregorian and ISO week date " - "conventions in a formatting template."))); + errhint("Do not mix Gregorian and ISO week date conventions in a formatting template."))); } return true; } @@ -2172,8 +2147,7 @@ from_char_set_int(int *dest, const int value, const FormatNode *node, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("conflicting values for \"%s\" field in formatting string", node->key->name), - errdetail("This value contradicts a previous setting " - "for the same field type."))); + errdetail("This value contradicts a previous setting for the same field type."))); *dest = value; return true; } @@ -2200,13 +2174,13 @@ from_char_set_int(int *dest, const int value, const FormatNode *node, * with DD and MI). */ static int -from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode *node, +from_char_parse_int_len(int *dest, const char **src, const size_t len, FormatNode *node, Node *escontext) { long result; char copy[DCH_MAX_ITEM_SIZ + 1]; const char *init = *src; - int used; + size_t used; /* * Skip any whitespace before parsing the integer. @@ -2214,9 +2188,9 @@ from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode * *src += strspace_len(*src); Assert(len <= DCH_MAX_ITEM_SIZ); - used = (int) strlcpy(copy, *src, len + 1); + used = strlcpy(copy, *src, len + 1); - if (S_FM(node->suffix) || is_next_separator(node)) + if (IS_SUFFIX_FM(node->suffix) || is_next_separator(node)) { /* * This node is in Fill Mode, or the next node is known to be a @@ -2241,10 +2215,9 @@ from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode * (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("source string too short for \"%s\" formatting field", node->key->name), - errdetail("Field requires %d characters, but only %d remain.", + errdetail("Field requires %zu characters, but only %zu remain.", len, used), - errhint("If your source string is not fixed-width, " - "try using the \"FM\" modifier."))); + errhint("If your source string is not fixed-width, try using the \"FM\" modifier."))); errno = 0; result = strtol(copy, &last, 10); @@ -2255,10 +2228,9 @@ from_char_parse_int_len(int *dest, const char **src, const int len, FormatNode * (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("invalid value \"%s\" for \"%s\"", copy, node->key->name), - errdetail("Field requires %d characters, but only %d could be parsed.", + errdetail("Field requires %zu characters, but only %zu could be parsed.", len, used), - errhint("If your source string is not fixed-width, " - "try using the \"FM\" modifier."))); + errhint("If your source string is not fixed-width, try using the \"FM\" modifier."))); *src += used; } @@ -2315,10 +2287,9 @@ from_char_parse_int(int *dest, const char **src, FormatNode *node, * suitable for comparisons to ASCII strings. */ static int -seq_search_ascii(const char *name, const char *const *array, int *len) +seq_search_ascii(const char *name, const char *const *array, size_t *len) { unsigned char firstc; - const char *const *a; *len = 0; @@ -2329,17 +2300,14 @@ seq_search_ascii(const char *name, const char *const *array, int *len) /* we handle first char specially to gain some speed */ firstc = pg_ascii_tolower((unsigned char) *name); - for (a = array; *a != NULL; a++) + for (const char *const *a = array; *a != NULL; a++) { - const char *p; - const char *n; - /* compare first chars */ if (pg_ascii_tolower((unsigned char) **a) != firstc) continue; /* compare rest of string */ - for (p = *a + 1, n = name + 1;; p++, n++) + for (const char *p = *a + 1, *n = name + 1;; p++, n++) { /* return success if we matched whole array entry */ if (*p == '\0') @@ -2372,9 +2340,8 @@ seq_search_ascii(const char *name, const char *const *array, int *len) * the arrays exported by pg_locale.c aren't const. */ static int -seq_search_localized(const char *name, char **array, int *len, Oid collid) +seq_search_localized(const char *name, char **array, size_t *len, Oid collid) { - char **a; char *upper_name; char *lower_name; @@ -2388,9 +2355,9 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) * The case-folding processing done below is fairly expensive, so before * doing that, make a quick pass to see if there is an exact match. */ - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { - int element_len = strlen(*a); + size_t element_len = strlen(*a); if (strncmp(name, *a, element_len) == 0) { @@ -2407,11 +2374,11 @@ seq_search_localized(const char *name, char **array, int *len, Oid collid) lower_name = str_tolower(upper_name, strlen(upper_name), collid); pfree(upper_name); - for (a = array; *a != NULL; a++) + for (char **a = array; *a != NULL; a++) { char *upper_element; char *lower_element; - int element_len; + size_t element_len; /* Likewise upper/lower-case array element */ upper_element = str_toupper(*a, strlen(*a), collid); @@ -2460,7 +2427,7 @@ from_char_seq_search(int *dest, const char **src, const char *const *array, char **localized_array, Oid collid, FormatNode *node, Node *escontext) { - int len; + size_t len; if (localized_array == NULL) *dest = seq_search_ascii(*src, array, &len); @@ -2474,9 +2441,8 @@ from_char_seq_search(int *dest, const char **src, const char *const *array, * any) to avoid including irrelevant data. */ char *copy = pstrdup(*src); - char *c; - for (c = copy; *c; c++) + for (char *c = copy; *c; c++) { if (scanner_isspace(*c)) { @@ -2489,22 +2455,19 @@ from_char_seq_search(int *dest, const char **src, const char *const *array, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), errmsg("invalid value \"%s\" for \"%s\"", copy, node->key->name), - errdetail("The given value did not match any of " - "the allowed values for this field."))); + errdetail("The given value did not match any of the allowed values for this field."))); } *src += len; return true; } -/* ---------- +/* * Process a TmToChar struct as denoted by a list of FormatNodes. * The formatted data is written to the string pointed to by 'out'. - * ---------- */ static void DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid collid) { - FormatNode *n; char *s; struct fmt_tm *tm = &in->tm; int i; @@ -2513,7 +2476,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col cache_locale_time(); s = out; - for (n = node; n->type != NODE_TYPE_END; n++) + for (FormatNode *n = node; n->type != NODE_TYPE_END; n++) { if (n->type != NODE_TYPE_ACTION) { @@ -2555,40 +2518,40 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col * display time as shown on a 12-hour clock, even for * intervals */ - sprintf(s, "%0*lld", S_FM(n->suffix) ? 0 : (tm->tm_hour >= 0) ? 2 : 3, + sprintf(s, "%0*lld", IS_SUFFIX_FM(n->suffix) ? 0 : (tm->tm_hour >= 0) ? 2 : 3, tm->tm_hour % (HOURS_PER_DAY / 2) == 0 ? (long long) (HOURS_PER_DAY / 2) : (long long) (tm->tm_hour % (HOURS_PER_DAY / 2))); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_HH24: - sprintf(s, "%0*lld", S_FM(n->suffix) ? 0 : (tm->tm_hour >= 0) ? 2 : 3, + sprintf(s, "%0*lld", IS_SUFFIX_FM(n->suffix) ? 0 : (tm->tm_hour >= 0) ? 2 : 3, (long long) tm->tm_hour); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_MI: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : (tm->tm_min >= 0) ? 2 : 3, + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : (tm->tm_min >= 0) ? 2 : 3, tm->tm_min); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_SS: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : (tm->tm_sec >= 0) ? 2 : 3, + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : (tm->tm_sec >= 0) ? 2 : 3, tm->tm_sec); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; #define DCH_to_char_fsec(frac_fmt, frac_val) \ sprintf(s, frac_fmt, (int) (frac_val)); \ - if (S_THth(n->suffix)) \ - str_numth(s, s, S_TH_TYPE(n->suffix)); \ + if (IS_SUFFIX_THth(n->suffix)) \ + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); \ s += strlen(s) case DCH_FF1: /* tenth of second */ @@ -2617,8 +2580,8 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col (long long) (tm->tm_hour * SECS_PER_HOUR + tm->tm_min * SECS_PER_MINUTE + tm->tm_sec)); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_tz: @@ -2658,7 +2621,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; sprintf(s, "%c%0*d", (tm->tm_gmtoff >= 0) ? '+' : '-', - S_FM(n->suffix) ? 0 : 2, + IS_SUFFIX_FM(n->suffix) ? 0 : 2, abs((int) tm->tm_gmtoff) / SECS_PER_HOUR); s += strlen(s); if (abs((int) tm->tm_gmtoff) % SECS_PER_HOUR != 0) @@ -2696,7 +2659,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_toupper_z(localized_full_months[tm->tm_mon - 1], collid); @@ -2708,7 +2671,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col errmsg("localized string format value too long"))); } else - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -9, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -9, asc_toupper_z(months_full[tm->tm_mon - 1])); s += strlen(s); break; @@ -2716,7 +2679,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_initcap_z(localized_full_months[tm->tm_mon - 1], collid); @@ -2728,7 +2691,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col errmsg("localized string format value too long"))); } else - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -9, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -9, months_full[tm->tm_mon - 1]); s += strlen(s); break; @@ -2736,7 +2699,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_tolower_z(localized_full_months[tm->tm_mon - 1], collid); @@ -2748,7 +2711,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col errmsg("localized string format value too long"))); } else - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -9, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -9, asc_tolower_z(months_full[tm->tm_mon - 1])); s += strlen(s); break; @@ -2756,7 +2719,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_toupper_z(localized_abbrev_months[tm->tm_mon - 1], collid); @@ -2775,7 +2738,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_initcap_z(localized_abbrev_months[tm->tm_mon - 1], collid); @@ -2794,7 +2757,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col INVALID_FOR_INTERVAL; if (!tm->tm_mon) break; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_tolower_z(localized_abbrev_months[tm->tm_mon - 1], collid); @@ -2810,15 +2773,15 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col s += strlen(s); break; case DCH_MM: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : (tm->tm_mon >= 0) ? 2 : 3, + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : (tm->tm_mon >= 0) ? 2 : 3, tm->tm_mon); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_DAY: INVALID_FOR_INTERVAL; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_toupper_z(localized_full_days[tm->tm_wday], collid); @@ -2830,13 +2793,13 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col errmsg("localized string format value too long"))); } else - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -9, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -9, asc_toupper_z(days[tm->tm_wday])); s += strlen(s); break; case DCH_Day: INVALID_FOR_INTERVAL; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_initcap_z(localized_full_days[tm->tm_wday], collid); @@ -2848,13 +2811,13 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col errmsg("localized string format value too long"))); } else - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -9, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -9, days[tm->tm_wday]); s += strlen(s); break; case DCH_day: INVALID_FOR_INTERVAL; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_tolower_z(localized_full_days[tm->tm_wday], collid); @@ -2866,13 +2829,13 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col errmsg("localized string format value too long"))); } else - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -9, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -9, asc_tolower_z(days[tm->tm_wday])); s += strlen(s); break; case DCH_DY: INVALID_FOR_INTERVAL; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_toupper_z(localized_abbrev_days[tm->tm_wday], collid); @@ -2889,7 +2852,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col break; case DCH_Dy: INVALID_FOR_INTERVAL; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_initcap_z(localized_abbrev_days[tm->tm_wday], collid); @@ -2906,7 +2869,7 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col break; case DCH_dy: INVALID_FOR_INTERVAL; - if (S_TM(n->suffix)) + if (IS_SUFFIX_TM(n->suffix)) { char *str = str_tolower_z(localized_abbrev_days[tm->tm_wday], collid); @@ -2923,54 +2886,54 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col break; case DCH_DDD: case DCH_IDDD: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : 3, + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : 3, (n->key->id == DCH_DDD) ? tm->tm_yday : date2isoyearday(tm->tm_year, tm->tm_mon, tm->tm_mday)); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_DD: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : 2, tm->tm_mday); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : 2, tm->tm_mday); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_D: INVALID_FOR_INTERVAL; sprintf(s, "%d", tm->tm_wday + 1); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_ID: INVALID_FOR_INTERVAL; sprintf(s, "%d", (tm->tm_wday == 0) ? 7 : tm->tm_wday); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_WW: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : 2, + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : 2, (tm->tm_yday - 1) / 7 + 1); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_IW: - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : 2, + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : 2, date2isoweek(tm->tm_year, tm->tm_mon, tm->tm_mday)); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_Q: if (!tm->tm_mon) break; sprintf(s, "%d", (tm->tm_mon - 1) / 3 + 1); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_CC: @@ -2986,25 +2949,25 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col i = tm->tm_year / 100 - 1; } if (i <= 99 && i >= -99) - sprintf(s, "%0*d", S_FM(n->suffix) ? 0 : (i >= 0) ? 2 : 3, i); + sprintf(s, "%0*d", IS_SUFFIX_FM(n->suffix) ? 0 : (i >= 0) ? 2 : 3, i); else sprintf(s, "%d", i); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_Y_YYY: i = ADJUST_YEAR(tm->tm_year, is_interval) / 1000; sprintf(s, "%d,%03d", i, ADJUST_YEAR(tm->tm_year, is_interval) - (i * 1000)); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_YYYY: case DCH_IYYY: sprintf(s, "%0*d", - S_FM(n->suffix) ? 0 : + IS_SUFFIX_FM(n->suffix) ? 0 : (ADJUST_YEAR(tm->tm_year, is_interval) >= 0) ? 4 : 5, (n->key->id == DCH_YYYY ? ADJUST_YEAR(tm->tm_year, is_interval) : @@ -3012,14 +2975,14 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col tm->tm_mon, tm->tm_mday), is_interval))); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_YYY: case DCH_IYY: sprintf(s, "%0*d", - S_FM(n->suffix) ? 0 : + IS_SUFFIX_FM(n->suffix) ? 0 : (ADJUST_YEAR(tm->tm_year, is_interval) >= 0) ? 3 : 4, (n->key->id == DCH_YYY ? ADJUST_YEAR(tm->tm_year, is_interval) : @@ -3027,14 +2990,14 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col tm->tm_mon, tm->tm_mday), is_interval)) % 1000); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_YY: case DCH_IY: sprintf(s, "%0*d", - S_FM(n->suffix) ? 0 : + IS_SUFFIX_FM(n->suffix) ? 0 : (ADJUST_YEAR(tm->tm_year, is_interval) >= 0) ? 2 : 3, (n->key->id == DCH_YY ? ADJUST_YEAR(tm->tm_year, is_interval) : @@ -3042,8 +3005,8 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col tm->tm_mon, tm->tm_mday), is_interval)) % 100); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_Y: @@ -3055,12 +3018,12 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col tm->tm_mon, tm->tm_mday), is_interval)) % 10); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_RM: - /* FALLTHROUGH */ + pg_fallthrough; case DCH_rm: /* @@ -3111,21 +3074,21 @@ DCH_to_char(FormatNode *node, bool is_interval, TmToChar *in, char *out, Oid col mon = MONTHS_PER_YEAR - tm->tm_mon; } - sprintf(s, "%*s", S_FM(n->suffix) ? 0 : -4, + sprintf(s, "%*s", IS_SUFFIX_FM(n->suffix) ? 0 : -4, months[mon]); s += strlen(s); } break; case DCH_W: sprintf(s, "%d", (tm->tm_mday - 1) / 7 + 1); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; case DCH_J: sprintf(s, "%d", date2j(tm->tm_year, tm->tm_mon, tm->tm_mday)); - if (S_THth(n->suffix)) - str_numth(s, s, S_TH_TYPE(n->suffix)); + if (IS_SUFFIX_THth(n->suffix)) + str_numth(s, s, SUFFIX_TH_TYPE(n->suffix)); s += strlen(s); break; } @@ -3221,7 +3184,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, * insist that the consumed character match the format's * character. */ - s += pg_mblen(s); + s += pg_mblen_cstr(s); } continue; } @@ -3243,11 +3206,11 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, if (extra_skip > 0) extra_skip--; else - s += pg_mblen(s); + s += pg_mblen_cstr(s); } else { - int chlen = pg_mblen(s); + int chlen = pg_mblen_cstr(s); /* * Standard mode requires strict match of format characters. @@ -3282,7 +3245,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, return; if (!from_char_set_int(&out->pm, value % 2, n, escontext)) return; - out->clock = CLOCK_12_HOUR; + out->clock_12_hour = true; break; case DCH_AM: case DCH_PM: @@ -3294,13 +3257,13 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, return; if (!from_char_set_int(&out->pm, value % 2, n, escontext)) return; - out->clock = CLOCK_12_HOUR; + out->clock_12_hour = true; break; case DCH_HH: case DCH_HH12: if (from_char_parse_int_len(&out->hh, &s, 2, n, escontext) < 0) return; - out->clock = CLOCK_12_HOUR; + out->clock_12_hour = true; SKIP_THth(s, n->suffix); break; case DCH_HH24: @@ -3338,7 +3301,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, case DCH_FF5: case DCH_FF6: out->ff = n->key->id - DCH_FF1 + 1; - /* FALLTHROUGH */ + pg_fallthrough; case DCH_US: /* microsecond */ len = from_char_parse_int_len(&out->us, &s, n->key->id == DCH_US ? 6 : @@ -3387,13 +3350,12 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, */ ereturn(escontext,, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), - errmsg("invalid value \"%s\" for \"%s\"", - s, n->key->name), + errmsg("invalid value \"%s\" for \"%s\"", s, n->key->name), errdetail("Time zone abbreviation is not recognized."))); } /* otherwise parse it like OF */ } - /* FALLTHROUGH */ + pg_fallthrough; case DCH_OF: /* OF is equivalent to TZH or TZH:TZM */ /* see TZH comments below */ @@ -3477,7 +3439,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, case DCH_Month: case DCH_month: if (!from_char_seq_search(&value, &s, months_full, - S_TM(n->suffix) ? localized_full_months : NULL, + IS_SUFFIX_TM(n->suffix) ? localized_full_months : NULL, collid, n, escontext)) return; @@ -3488,7 +3450,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, case DCH_Mon: case DCH_mon: if (!from_char_seq_search(&value, &s, months, - S_TM(n->suffix) ? localized_abbrev_months : NULL, + IS_SUFFIX_TM(n->suffix) ? localized_abbrev_months : NULL, collid, n, escontext)) return; @@ -3504,7 +3466,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, case DCH_Day: case DCH_day: if (!from_char_seq_search(&value, &s, days, - S_TM(n->suffix) ? localized_full_days : NULL, + IS_SUFFIX_TM(n->suffix) ? localized_full_days : NULL, collid, n, escontext)) return; @@ -3516,7 +3478,7 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, case DCH_Dy: case DCH_dy: if (!from_char_seq_search(&value, &s, days_short, - S_TM(n->suffix) ? localized_abbrev_days : NULL, + IS_SUFFIX_TM(n->suffix) ? localized_abbrev_days : NULL, collid, n, escontext)) return; @@ -3590,14 +3552,14 @@ DCH_from_char(FormatNode *node, const char *in, TmFromChar *out, if (matched < 2) ereturn(escontext,, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), - errmsg("invalid input string for \"Y,YYY\""))); + errmsg("invalid value \"%s\" for \"%s\"", s, "Y,YYY"))); /* years += (millennia * 1000); */ if (pg_mul_s32_overflow(millennia, 1000, &millennia) || pg_add_s32_overflow(years, millennia, &years)) ereturn(escontext,, (errcode(ERRCODE_DATETIME_FIELD_OVERFLOW), - errmsg("value for \"Y,YYY\" in source string is out of range"))); + errmsg("value for \"%s\" in source string is out of range", "Y,YYY"))); if (!from_char_set_int(&out->year, years, n, escontext)) return; @@ -3722,10 +3684,9 @@ DCH_prevent_counter_overflow(void) static int DCH_datetime_type(FormatNode *node) { - FormatNode *n; int flags = 0; - for (n = node; n->type != NODE_TYPE_END; n++) + for (FormatNode *n = node; n->type != NODE_TYPE_END; n++) { if (n->type != NODE_TYPE_ACTION) continue; @@ -3925,13 +3886,13 @@ DCH_cache_fetch(const char *str, bool std) * for formatting. */ static text * -datetime_to_char_body(TmToChar *tmtc, text *fmt, bool is_interval, Oid collid) +datetime_to_char_body(TmToChar *tmtc, const text *fmt, bool is_interval, Oid collid) { FormatNode *format; char *fmt_str, *result; bool incache; - int fmt_len; + size_t fmt_len; text *res; /* @@ -3989,9 +3950,8 @@ datetime_to_char_body(TmToChar *tmtc, text *fmt, bool is_interval, Oid collid) * Public routines ***************************************************************************/ -/* ------------------- +/* * TIMESTAMP to_char() - * ------------------- */ Datum timestamp_to_char(PG_FUNCTION_ARGS) @@ -4065,9 +4025,8 @@ timestamptz_to_char(PG_FUNCTION_ARGS) } -/* ------------------- +/* * INTERVAL to_char() - * ------------------- */ Datum interval_to_char(PG_FUNCTION_ARGS) @@ -4104,12 +4063,11 @@ interval_to_char(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(res); } -/* --------------------- +/* * TO_TIMESTAMP() * * Make Timestamp from date_str which is formatted at argument 'fmt' * ( to_timestamp is reverse to_char() ) - * --------------------- */ Datum to_timestamp(PG_FUNCTION_ARGS) @@ -4145,10 +4103,9 @@ to_timestamp(PG_FUNCTION_ARGS) PG_RETURN_TIMESTAMP(result); } -/* ---------- +/* * TO_DATE * Make Date from date_str which is formatted at argument 'fmt' - * ---------- */ Datum to_date(PG_FUNCTION_ARGS) @@ -4168,8 +4125,7 @@ to_date(PG_FUNCTION_ARGS) if (!IS_VALID_JULIAN(tm.tm_year, tm.tm_mon, tm.tm_mday)) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range: \"%s\"", - text_to_cstring(date_txt)))); + errmsg("date out of range: \"%s\"", text_to_cstring(date_txt)))); result = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - POSTGRES_EPOCH_JDATE; @@ -4177,8 +4133,7 @@ to_date(PG_FUNCTION_ARGS) if (!IS_VALID_DATE(result)) ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range: \"%s\"", - text_to_cstring(date_txt)))); + errmsg("date out of range: \"%s\"", text_to_cstring(date_txt)))); PG_RETURN_DATEADT(result); } @@ -4282,8 +4237,7 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, if (!IS_VALID_JULIAN(tm.tm_year, tm.tm_mon, tm.tm_mday)) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range: \"%s\"", - text_to_cstring(date_txt)))); + errmsg("date out of range: \"%s\"", text_to_cstring(date_txt)))); result = date2j(tm.tm_year, tm.tm_mon, tm.tm_mday) - POSTGRES_EPOCH_JDATE; @@ -4292,8 +4246,7 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, if (!IS_VALID_DATE(result)) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), - errmsg("date out of range: \"%s\"", - text_to_cstring(date_txt)))); + errmsg("date out of range: \"%s\"", text_to_cstring(date_txt)))); *typid = DATEOID; return DateADTGetDatum(result); @@ -4304,7 +4257,7 @@ parse_datetime(text *date_txt, text *fmt, Oid collid, bool strict, { if (flags & DCH_ZONED) { - TimeTzADT *result = palloc(sizeof(TimeTzADT)); + TimeTzADT *result = palloc_object(TimeTzADT); if (ftz.has_tz) { @@ -4365,7 +4318,7 @@ bool datetime_format_has_tz(const char *fmt_str) { bool incache; - int fmt_len = strlen(fmt_str); + size_t fmt_len = strlen(fmt_str); int result; FormatNode *format; @@ -4425,12 +4378,12 @@ datetime_format_has_tz(const char *fmt_str) * struct 'tm', 'fsec', struct 'tz', and 'fprec'. */ static bool -do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, +do_to_timestamp(const text *date_txt, const text *fmt, Oid collid, bool std, struct pg_tm *tm, fsec_t *fsec, struct fmt_tz *tz, int *fprec, uint32 *flags, Node *escontext) { FormatNode *format = NULL; - TmFromChar tmfc; + TmFromChar tmfc = {0}; int fmt_len; char *date_str; int fmask; @@ -4441,7 +4394,6 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, date_str = text_to_cstring(date_txt); - ZERO_tmfc(&tmfc); ZERO_tm(tm); *fsec = 0; tz->has_tz = false; @@ -4524,14 +4476,13 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, if (tmfc.hh) tm->tm_hour = tmfc.hh; - if (tmfc.clock == CLOCK_12_HOUR) + if (tmfc.clock_12_hour) { if (tm->tm_hour < 1 || tm->tm_hour > HOURS_PER_DAY / 2) { errsave(escontext, (errcode(ERRCODE_INVALID_DATETIME_FORMAT), - errmsg("hour \"%d\" is invalid for the 12-hour clock", - tm->tm_hour), + errmsg("hour \"%d\" is invalid for the 12-hour clock", tm->tm_hour), errhint("Use the 24-hour clock, or give an hour between 1 and 12."))); goto fail; } @@ -4857,27 +4808,17 @@ do_to_timestamp(text *date_txt, text *fmt, Oid collid, bool std, *********************************************************************/ -static char * +/* + * Fill str with character c max times, and add terminating \0. (So max+1 + * bytes are written altogether!) + */ +static void fill_str(char *str, int c, int max) { memset(str, c, max); - *(str + max) = '\0'; - return str; + str[max] = '\0'; } -#define zeroize_NUM(_n) \ -do { \ - (_n)->flag = 0; \ - (_n)->lsign = 0; \ - (_n)->pre = 0; \ - (_n)->post = 0; \ - (_n)->pre_lsign_num = 0; \ - (_n)->need_locale = 0; \ - (_n)->multi = 0; \ - (_n)->zero_start = 0; \ - (_n)->zero_end = 0; \ -} while(0) - /* This works the same as DCH_prevent_counter_overflow */ static inline void NUM_prevent_counter_overflow(void) @@ -4985,7 +4926,7 @@ NUM_cache_fetch(const char *str) */ ent = NUM_cache_getnew(str); - zeroize_NUM(&ent->Num); + memset(&ent->Num, 0, sizeof ent->Num); parse_format(ent->format, str, NUM_keywords, NULL, NUM_index, NUM_FLAG, &ent->Num); @@ -4995,12 +4936,11 @@ NUM_cache_fetch(const char *str) return ent; } -/* ---------- +/* * Cache routine for NUM to_char version - * ---------- */ static FormatNode * -NUM_cache(int len, NUMDesc *Num, text *pars_str, bool *shouldFree) +NUM_cache(int len, NUMDesc *Num, const text *pars_str, bool *shouldFree) { FormatNode *format = NULL; char *str; @@ -5017,7 +4957,7 @@ NUM_cache(int len, NUMDesc *Num, text *pars_str, bool *shouldFree) *shouldFree = true; - zeroize_NUM(Num); + memset(Num, 0, sizeof *Num); parse_format(format, str, NUM_keywords, NULL, NUM_index, NUM_FLAG, Num); @@ -5067,8 +5007,7 @@ int_to_roman(int number) { int len, num; - char *p, - *result, + char *result, numstr[12]; result = (char *) palloc(MAX_ROMAN_LEN + 1); @@ -5089,7 +5028,7 @@ int_to_roman(int number) len = snprintf(numstr, sizeof(numstr), "%d", number); Assert(len > 0 && len <= 4); - for (p = numstr; *p != '\0'; p++, --len) + for (char *p = numstr; *p != '\0'; p++, --len) { num = *p - ('0' + 1); if (num < 0) @@ -5123,10 +5062,10 @@ int_to_roman(int number) * If input is invalid, return -1. */ static int -roman_to_int(NUMProc *Np, int input_len) +roman_to_int(NUMProc *Np, size_t input_len) { int result = 0; - int len; + size_t len; char romanChars[MAX_ROMAN_LEN]; int romanValues[MAX_ROMAN_LEN]; int repeatCount = 1; @@ -5165,7 +5104,7 @@ roman_to_int(NUMProc *Np, int input_len) return -1; /* No valid roman numerals. */ /* Check for valid combinations and compute the represented value. */ - for (int i = 0; i < len; i++) + for (size_t i = 0; i < len; i++) { char currChar = romanChars[i]; int currValue = romanValues[i]; @@ -5268,9 +5207,8 @@ roman_to_int(NUMProc *Np, int input_len) } -/* ---------- +/* * Locale - * ---------- */ static void NUM_prepare_locale(NUMProc *Np) @@ -5346,18 +5284,17 @@ NUM_prepare_locale(NUMProc *Np) } } -/* ---------- +/* * Return pointer of last relevant number after decimal point * 12.0500 --> last relevant is '5' * 12.0000 --> last relevant is '.' * If there is no decimal point, return NULL (which will result in same * behavior as if FM hadn't been specified). - * ---------- */ -static char * -get_last_relevant_decnum(char *num) +static const char * +get_last_relevant_decnum(const char *num) { - char *result, + const char *result, *p = strchr(num, '.'); #ifdef DEBUG_TO_FROM_CHAR @@ -5378,12 +5315,11 @@ get_last_relevant_decnum(char *num) return result; } -/* ---------- +/* * Number extraction for TO_NUMBER() - * ---------- */ static void -NUM_numpart_from_char(NUMProc *Np, int id, int input_len) +NUM_numpart_from_char(NUMProc *Np, int id, size_t input_len) { bool isread = false; @@ -5417,7 +5353,7 @@ NUM_numpart_from_char(NUMProc *Np, int id, int input_len) */ if (IS_LSIGN(Np->Num) && Np->Num->lsign == NUM_LSIGN_PRE) { - int x = 0; + size_t x = 0; #ifdef DEBUG_TO_FROM_CHAR elog(DEBUG_elog_output, "Try read locale pre-sign (%c)", *Np->inout_p); @@ -5496,7 +5432,7 @@ NUM_numpart_from_char(NUMProc *Np, int id, int input_len) * Np->decimal is always just "." if we don't have a D format token. * So we just unconditionally match to Np->decimal. */ - int x = strlen(Np->decimal); + size_t x = strlen(Np->decimal); #ifdef DEBUG_TO_FROM_CHAR elog(DEBUG_elog_output, "Try read decimal point (%c)", @@ -5535,7 +5471,7 @@ NUM_numpart_from_char(NUMProc *Np, int id, int input_len) (Np->inout_p + 1) < Np->inout + input_len && !isdigit((unsigned char) *(Np->inout_p + 1))) { - int x; + size_t x; char *tmp = Np->inout_p++; #ifdef DEBUG_TO_FROM_CHAR @@ -5593,9 +5529,8 @@ NUM_numpart_from_char(NUMProc *Np, int id, int input_len) *(_n)->number == '0' && \ (_n)->Num->post != 0) -/* ---------- +/* * Add digit or sign to number-string - * ---------- */ static void NUM_numpart_to_char(NUMProc *Np, int id) @@ -5634,11 +5569,9 @@ NUM_numpart_to_char(NUMProc *Np, int id) { if (Np->Num->lsign == NUM_LSIGN_PRE) { - if (Np->sign == '-') - strcpy(Np->inout_p, Np->L_negative_sign); - else - strcpy(Np->inout_p, Np->L_positive_sign); - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, (Np->sign == '-') ? + Np->L_negative_sign : + Np->L_positive_sign); Np->sign_wrote = true; } } @@ -5703,8 +5636,7 @@ NUM_numpart_to_char(NUMProc *Np, int id) { if (!Np->last_relevant || *Np->last_relevant != '.') { - strcpy(Np->inout_p, Np->decimal); /* Write DEC/D */ - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, Np->decimal); /* Write DEC/D */ } /* @@ -5713,8 +5645,7 @@ NUM_numpart_to_char(NUMProc *Np, int id) else if (IS_FILLMODE(Np->Num) && Np->last_relevant && *Np->last_relevant == '.') { - strcpy(Np->inout_p, Np->decimal); /* Write DEC/D */ - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, Np->decimal); /* Write DEC/D */ } } else @@ -5772,11 +5703,9 @@ NUM_numpart_to_char(NUMProc *Np, int id) } else if (IS_LSIGN(Np->Num) && Np->Num->lsign == NUM_LSIGN_POST) { - if (Np->sign == '-') - strcpy(Np->inout_p, Np->L_negative_sign); - else - strcpy(Np->inout_p, Np->L_positive_sign); - Np->inout_p += strlen(Np->inout_p); + NUM_add_locale_symbol(Np, (Np->sign == '-') ? + Np->L_negative_sign : + Np->L_positive_sign); } } } @@ -5784,32 +5713,51 @@ NUM_numpart_to_char(NUMProc *Np, int id) ++Np->num_curr; } +/* + * Append locale-specific symbol to Np->inout. + * Note we don't null-terminate the output + */ +static void +NUM_add_locale_symbol(NUMProc *Np, const char *pattern) +{ + size_t pattern_len = strlen(pattern); + + /* Truncate symbol if it's potentially too long */ + if (unlikely(pattern_len > NUM_MAX_ITEM_SIZ)) + pattern_len = pg_mbcliplen(pattern, pattern_len, + NUM_MAX_ITEM_SIZ); + memcpy(Np->inout_p, pattern, pattern_len); + Np->inout_p += pattern_len; +} + /* * Skip over "n" input characters, but only if they aren't numeric data */ static void -NUM_eat_non_data_chars(NUMProc *Np, int n, int input_len) +NUM_eat_non_data_chars(NUMProc *Np, int n, size_t input_len) { + const char *end = Np->inout + input_len; + while (n-- > 0) { if (OVERLOAD_TEST) break; /* end of input */ if (strchr("0123456789.,+-", *Np->inout_p) != NULL) break; /* it's a data character */ - Np->inout_p += pg_mblen(Np->inout_p); + Np->inout_p += pg_mblen_range(Np->inout_p, end); } } static char * NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, - char *number, int input_len, int to_char_out_pre_spaces, + char *number, size_t input_len, int to_char_out_pre_spaces, int sign, bool is_to_char, Oid collid) { FormatNode *n; NUMProc _Np, *Np = &_Np; const char *pattern; - int pattern_len; + size_t pattern_len; MemSet(Np, 0, sizeof(NUMProc)); @@ -5890,7 +5838,7 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, */ if (Np->last_relevant && Np->Num->zero_end > Np->out_pre_spaces) { - int last_zero_pos; + size_t last_zero_pos; char *last_zero; /* note that Np->number cannot be zero-length here */ @@ -6029,6 +5977,10 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, pattern_len = strlen(pattern); if (Np->is_to_char) { + /* Truncate symbol if it's potentially too long */ + if (unlikely(pattern_len > NUM_MAX_ITEM_SIZ)) + pattern_len = pg_mbcliplen(pattern, pattern_len, + NUM_MAX_ITEM_SIZ); if (!Np->num_in) { if (IS_FILLMODE(Np->Num)) @@ -6036,19 +5988,21 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, else { /* just in case there are MB chars */ - pattern_len = pg_mbstrlen(pattern); + pattern_len = pg_mbstrlen_with_len(pattern, + pattern_len); memset(Np->inout_p, ' ', pattern_len); Np->inout_p += pattern_len - 1; } } else { - strcpy(Np->inout_p, pattern); + memcpy(Np->inout_p, pattern, pattern_len); Np->inout_p += pattern_len - 1; } } else { + /* Here we do not truncate the symbol ... */ if (!Np->num_in) { if (IS_FILLMODE(Np->Num)) @@ -6073,11 +6027,18 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, pattern = Np->L_currency_symbol; if (Np->is_to_char) { - strcpy(Np->inout_p, pattern); - Np->inout_p += strlen(pattern) - 1; + /* Truncate symbol if it's potentially too long */ + pattern_len = strlen(pattern); + if (unlikely(pattern_len > NUM_MAX_ITEM_SIZ)) + pattern_len = pg_mbcliplen(pattern, pattern_len, + NUM_MAX_ITEM_SIZ); + + memcpy(Np->inout_p, pattern, pattern_len); + Np->inout_p += pattern_len - 1; } else { + /* Here we do not truncate the symbol ... */ NUM_eat_non_data_chars(Np, pg_mbstrlen(pattern), input_len); continue; } @@ -6233,7 +6194,7 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, } else { - Np->inout_p += pg_mblen(Np->inout_p); + Np->inout_p += pg_mblen_range(Np->inout_p, Np->inout + input_len); } continue; } @@ -6264,10 +6225,9 @@ NUM_processor(FormatNode *node, NUMDesc *Num, char *inout, } } -/* ---------- +/* * MACRO: Start part of NUM - for all NUM's to_char variants * (sorry, but I hate copy same code - macro is better..) - * ---------- */ #define NUM_TOCHAR_prepare \ do { \ @@ -6278,13 +6238,12 @@ do { \ format = NUM_cache(len, &Num, fmt, &shouldFree); \ } while (0) -/* ---------- +/* * MACRO: Finish part of NUM - * ---------- */ #define NUM_TOCHAR_finish \ do { \ - int len; \ + size_t len; \ \ NUM_processor(format, &Num, VARDATA(result), numstr, 0, out_pre_spaces, sign, true, PG_GET_COLLATION()); \ \ @@ -6300,9 +6259,8 @@ do { \ SET_VARSIZE(result, len + VARHDRSZ); \ } while (0) -/* ------------------- +/* * NUMERIC to_number() (convert string to numeric) - * ------------------- */ Datum numeric_to_number(PG_FUNCTION_ARGS) @@ -6359,9 +6317,8 @@ numeric_to_number(PG_FUNCTION_ARGS) return result; } -/* ------------------ +/* * NUMERIC to_char() - * ------------------ */ Datum numeric_to_char(PG_FUNCTION_ARGS) @@ -6386,12 +6343,12 @@ numeric_to_char(PG_FUNCTION_ARGS) if (IS_ROMAN(&Num)) { int32 intvalue; - bool err; + ErrorSaveContext escontext = {T_ErrorSaveContext}; /* Round and convert to int */ - intvalue = numeric_int4_opt_error(value, &err); + intvalue = numeric_int4_safe(value, (Node *) &escontext); /* On overflow, just use PG_INT32_MAX; int_to_roman will cope */ - if (err) + if (escontext.error_occurred) intvalue = PG_INT32_MAX; numstr = int_to_roman(intvalue); } @@ -6431,7 +6388,7 @@ numeric_to_char(PG_FUNCTION_ARGS) } else { - int numstr_pre_len; + size_t numstr_pre_len; Numeric val = value; Numeric x; @@ -6487,9 +6444,8 @@ numeric_to_char(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } -/* --------------- +/* * INT4 to_char() - * --------------- */ Datum int4_to_char(PG_FUNCTION_ARGS) @@ -6529,7 +6485,7 @@ int4_to_char(PG_FUNCTION_ARGS) } else { - int numstr_pre_len; + size_t numstr_pre_len; if (IS_MULTI(&Num)) { @@ -6581,9 +6537,8 @@ int4_to_char(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } -/* --------------- +/* * INT8 to_char() - * --------------- */ Datum int8_to_char(PG_FUNCTION_ARGS) @@ -6639,7 +6594,7 @@ int8_to_char(PG_FUNCTION_ARGS) } else { - int numstr_pre_len; + size_t numstr_pre_len; if (IS_MULTI(&Num)) { @@ -6693,9 +6648,8 @@ int8_to_char(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } -/* ----------------- +/* * FLOAT4 to_char() - * ----------------- */ Datum float4_to_char(PG_FUNCTION_ARGS) @@ -6754,7 +6708,7 @@ float4_to_char(PG_FUNCTION_ARGS) { float4 val = value; char *orgnum; - int numstr_pre_len; + size_t numstr_pre_len; if (IS_MULTI(&Num)) { @@ -6806,9 +6760,8 @@ float4_to_char(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result); } -/* ----------------- +/* * FLOAT8 to_char() - * ----------------- */ Datum float8_to_char(PG_FUNCTION_ARGS) @@ -6867,7 +6820,7 @@ float8_to_char(PG_FUNCTION_ARGS) { float8 val = value; char *orgnum; - int numstr_pre_len; + size_t numstr_pre_len; if (IS_MULTI(&Num)) { diff --git a/src/backend/utils/adt/genfile.c b/src/backend/utils/adt/genfile.c index 80bb807fbe932..bfb949401d0ab 100644 --- a/src/backend/utils/adt/genfile.c +++ b/src/backend/utils/adt/genfile.c @@ -4,7 +4,7 @@ * Functions for direct access to files * * - * Copyright (c) 2004-2025, PostgreSQL Global Development Group + * Copyright (c) 2004-2026, PostgreSQL Global Development Group * * Author: Andreas Pflug * @@ -454,6 +454,7 @@ pg_stat_file(PG_FUNCTION_ARGS) "creation", TIMESTAMPTZOID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 6, "isdir", BOOLOID, -1, 0); + TupleDescFinalize(tupdesc); BlessTupleDesc(tupdesc); memset(isnull, false, sizeof(isnull)); diff --git a/src/backend/utils/adt/geo_ops.c b/src/backend/utils/adt/geo_ops.c index 377a1b3f3ade1..cc5ce013d0f07 100644 --- a/src/backend/utils/adt/geo_ops.c +++ b/src/backend/utils/adt/geo_ops.c @@ -13,7 +13,7 @@ * - circle * - polygon * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -77,12 +77,12 @@ enum path_delim /* Routines for points */ static inline void point_construct(Point *result, float8 x, float8 y); -static inline void point_add_point(Point *result, Point *pt1, Point *pt2); +static inline void point_add_point(Point *result, Point *pt1, Point *pt2, Node *escontext); static inline void point_sub_point(Point *result, Point *pt1, Point *pt2); static inline void point_mul_point(Point *result, Point *pt1, Point *pt2); static inline void point_div_point(Point *result, Point *pt1, Point *pt2); static inline bool point_eq_point(Point *pt1, Point *pt2); -static inline float8 point_dt(Point *pt1, Point *pt2); +static inline float8 point_dt(Point *pt1, Point *pt2, Node *escontext); static inline float8 point_sl(Point *pt1, Point *pt2); static int point_inside(Point *p, int npts, Point *plist); @@ -108,7 +108,7 @@ static float8 lseg_closept_lseg(Point *result, LSEG *on_lseg, LSEG *to_lseg); /* Routines for boxes */ static inline void box_construct(BOX *result, Point *pt1, Point *pt2); -static void box_cn(Point *center, BOX *box); +static void box_cn(Point *center, BOX *box, Node *escontext); static bool box_ov(BOX *box1, BOX *box2); static float8 box_ar(BOX *box); static float8 box_ht(BOX *box); @@ -125,7 +125,8 @@ static float8 circle_ar(CIRCLE *circle); /* Routines for polygons */ static void make_bound_box(POLYGON *poly); -static void poly_to_circle(CIRCLE *result, POLYGON *poly); +static POLYGON *circle_poly_internal(int32 npts, const CIRCLE *circle, FunctionCallInfo fcinfo); +static void poly_to_circle(CIRCLE *result, POLYGON *poly, Node *escontext); static bool lseg_inside_poly(Point *a, Point *b, POLYGON *poly, int start); static bool poly_contain_poly(POLYGON *contains_poly, POLYGON *contained_poly); static bool plist_same(int npts, Point *p1, Point *p2); @@ -423,7 +424,7 @@ box_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); Node *escontext = fcinfo->context; - BOX *box = (BOX *) palloc(sizeof(BOX)); + BOX *box = palloc_object(BOX); bool isopen; float8 x, y; @@ -470,7 +471,7 @@ box_recv(PG_FUNCTION_ARGS) float8 x, y; - box = (BOX *) palloc(sizeof(BOX)); + box = palloc_object(BOX); box->high.x = pq_getmsgfloat8(buf); box->high.y = pq_getmsgfloat8(buf); @@ -836,10 +837,10 @@ box_distance(PG_FUNCTION_ARGS) Point a, b; - box_cn(&a, box1); - box_cn(&b, box2); + box_cn(&a, box1, NULL); + box_cn(&b, box2, NULL); - PG_RETURN_FLOAT8(point_dt(&a, &b)); + PG_RETURN_FLOAT8(point_dt(&a, &b, NULL)); } @@ -849,9 +850,11 @@ Datum box_center(PG_FUNCTION_ARGS) { BOX *box = PG_GETARG_BOX_P(0); - Point *result = (Point *) palloc(sizeof(Point)); + Point *result = palloc_object(Point); - box_cn(result, box); + box_cn(result, box, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_POINT_P(result); } @@ -869,12 +872,27 @@ box_ar(BOX *box) /* box_cn - stores the centerpoint of the box into *center. */ static void -box_cn(Point *center, BOX *box) +box_cn(Point *center, BOX *box, Node *escontext) { - center->x = float8_div(float8_pl(box->high.x, box->low.x), 2.0); - center->y = float8_div(float8_pl(box->high.y, box->low.y), 2.0); -} + float8 x; + float8 y; + + x = float8_pl_safe(box->high.x, box->low.x, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + + center->x = float8_div_safe(x, 2.0, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + y = float8_pl_safe(box->high.y, box->low.y, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + + center->y = float8_div_safe(y, 2.0, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; +} /* box_wd - returns the width (length) of the box * (horizontal magnitude). @@ -914,7 +932,7 @@ box_intersect(PG_FUNCTION_ARGS) if (!box_ov(box1, box2)) PG_RETURN_NULL(); - result = (BOX *) palloc(sizeof(BOX)); + result = palloc_object(BOX); result->high.x = float8_min(box1->high.x, box2->high.x); result->low.x = float8_max(box1->low.x, box2->low.x); @@ -933,7 +951,7 @@ Datum box_diagonal(PG_FUNCTION_ARGS) { BOX *box = PG_GETARG_BOX_P(0); - LSEG *result = (LSEG *) palloc(sizeof(LSEG)); + LSEG *result = palloc_object(LSEG); statlseg_construct(result, &box->high, &box->low); @@ -980,7 +998,7 @@ line_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); Node *escontext = fcinfo->context; - LINE *line = (LINE *) palloc(sizeof(LINE)); + LINE *line = palloc_object(LINE); LSEG lseg; bool isopen; char *s; @@ -1040,7 +1058,7 @@ line_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); LINE *line; - line = (LINE *) palloc(sizeof(LINE)); + line = palloc_object(LINE); line->A = pq_getmsgfloat8(buf); line->B = pq_getmsgfloat8(buf); @@ -1116,7 +1134,7 @@ line_construct_pp(PG_FUNCTION_ARGS) { Point *pt1 = PG_GETARG_POINT_P(0); Point *pt2 = PG_GETARG_POINT_P(1); - LINE *result = (LINE *) palloc(sizeof(LINE)); + LINE *result = palloc_object(LINE); if (point_eq_point(pt1, pt2)) ereport(ERROR, @@ -1276,7 +1294,7 @@ line_distance(PG_FUNCTION_ARGS) PG_RETURN_FLOAT8(float8_div(fabs(float8_mi(l1->C, float8_mul(ratio, l2->C))), - HYPOT(l1->A, l1->B))); + hypot(l1->A, l1->B))); } /* line_interpt() @@ -1289,7 +1307,7 @@ line_interpt(PG_FUNCTION_ARGS) LINE *l2 = PG_GETARG_LINE_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (!line_interpt_line(result, l1, l2)) PG_RETURN_NULL(); @@ -1808,7 +1826,7 @@ path_length(PG_FUNCTION_ARGS) iprev = path->npts - 1; /* include the closure segment */ } - result = float8_pl(result, point_dt(&path->p[iprev], &path->p[i])); + result = float8_pl(result, point_dt(&path->p[iprev], &path->p[i], NULL)); } PG_RETURN_FLOAT8(result); @@ -1831,7 +1849,7 @@ Datum point_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); - Point *point = (Point *) palloc(sizeof(Point)); + Point *point = palloc_object(Point); /* Ignore failure from pair_decode, since our return value won't matter */ pair_decode(str, &point->x, &point->y, NULL, "point", str, fcinfo->context); @@ -1855,7 +1873,7 @@ point_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); Point *point; - point = (Point *) palloc(sizeof(Point)); + point = palloc_object(Point); point->x = pq_getmsgfloat8(buf); point->y = pq_getmsgfloat8(buf); PG_RETURN_POINT_P(point); @@ -1995,13 +2013,24 @@ point_distance(PG_FUNCTION_ARGS) Point *pt1 = PG_GETARG_POINT_P(0); Point *pt2 = PG_GETARG_POINT_P(1); - PG_RETURN_FLOAT8(point_dt(pt1, pt2)); + PG_RETURN_FLOAT8(point_dt(pt1, pt2, NULL)); } static inline float8 -point_dt(Point *pt1, Point *pt2) +point_dt(Point *pt1, Point *pt2, Node *escontext) { - return HYPOT(float8_mi(pt1->x, pt2->x), float8_mi(pt1->y, pt2->y)); + float8 x; + float8 y; + + x = float8_mi_safe(pt1->x, pt2->x, escontext); + if (unlikely(SOFT_ERROR_OCCURRED(escontext))) + return 0.0; + + y = float8_mi_safe(pt1->y, pt2->y, escontext); + if (unlikely(SOFT_ERROR_OCCURRED(escontext))) + return 0.0; + + return hypot(x, y); } Datum @@ -2066,7 +2095,7 @@ lseg_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); Node *escontext = fcinfo->context; - LSEG *lseg = (LSEG *) palloc(sizeof(LSEG)); + LSEG *lseg = palloc_object(LSEG); bool isopen; if (!path_decode(str, true, 2, &lseg->p[0], &isopen, NULL, "lseg", str, @@ -2094,7 +2123,7 @@ lseg_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); LSEG *lseg; - lseg = (LSEG *) palloc(sizeof(LSEG)); + lseg = palloc_object(LSEG); lseg->p[0].x = pq_getmsgfloat8(buf); lseg->p[0].y = pq_getmsgfloat8(buf); @@ -2130,7 +2159,7 @@ lseg_construct(PG_FUNCTION_ARGS) { Point *pt1 = PG_GETARG_POINT_P(0); Point *pt2 = PG_GETARG_POINT_P(1); - LSEG *result = (LSEG *) palloc(sizeof(LSEG)); + LSEG *result = palloc_object(LSEG); statlseg_construct(result, pt1, pt2); @@ -2173,7 +2202,7 @@ lseg_length(PG_FUNCTION_ARGS) { LSEG *lseg = PG_GETARG_LSEG_P(0); - PG_RETURN_FLOAT8(point_dt(&lseg->p[0], &lseg->p[1])); + PG_RETURN_FLOAT8(point_dt(&lseg->p[0], &lseg->p[1], NULL)); } /*---------------------------------------------------------- @@ -2258,8 +2287,8 @@ lseg_lt(PG_FUNCTION_ARGS) LSEG *l1 = PG_GETARG_LSEG_P(0); LSEG *l2 = PG_GETARG_LSEG_P(1); - PG_RETURN_BOOL(FPlt(point_dt(&l1->p[0], &l1->p[1]), - point_dt(&l2->p[0], &l2->p[1]))); + PG_RETURN_BOOL(FPlt(point_dt(&l1->p[0], &l1->p[1], NULL), + point_dt(&l2->p[0], &l2->p[1], NULL))); } Datum @@ -2268,8 +2297,8 @@ lseg_le(PG_FUNCTION_ARGS) LSEG *l1 = PG_GETARG_LSEG_P(0); LSEG *l2 = PG_GETARG_LSEG_P(1); - PG_RETURN_BOOL(FPle(point_dt(&l1->p[0], &l1->p[1]), - point_dt(&l2->p[0], &l2->p[1]))); + PG_RETURN_BOOL(FPle(point_dt(&l1->p[0], &l1->p[1], NULL), + point_dt(&l2->p[0], &l2->p[1], NULL))); } Datum @@ -2278,8 +2307,8 @@ lseg_gt(PG_FUNCTION_ARGS) LSEG *l1 = PG_GETARG_LSEG_P(0); LSEG *l2 = PG_GETARG_LSEG_P(1); - PG_RETURN_BOOL(FPgt(point_dt(&l1->p[0], &l1->p[1]), - point_dt(&l2->p[0], &l2->p[1]))); + PG_RETURN_BOOL(FPgt(point_dt(&l1->p[0], &l1->p[1], NULL), + point_dt(&l2->p[0], &l2->p[1], NULL))); } Datum @@ -2288,8 +2317,8 @@ lseg_ge(PG_FUNCTION_ARGS) LSEG *l1 = PG_GETARG_LSEG_P(0); LSEG *l2 = PG_GETARG_LSEG_P(1); - PG_RETURN_BOOL(FPge(point_dt(&l1->p[0], &l1->p[1]), - point_dt(&l2->p[0], &l2->p[1]))); + PG_RETURN_BOOL(FPge(point_dt(&l1->p[0], &l1->p[1], NULL), + point_dt(&l2->p[0], &l2->p[1], NULL))); } @@ -2317,13 +2346,31 @@ lseg_center(PG_FUNCTION_ARGS) { LSEG *lseg = PG_GETARG_LSEG_P(0); Point *result; + float8 x; + float8 y; + + result = palloc_object(Point); + + x = float8_pl_safe(lseg->p[0].x, lseg->p[1].x, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; - result = (Point *) palloc(sizeof(Point)); + result->x = float8_div_safe(x, 2.0, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; + + y = float8_pl_safe(lseg->p[0].y, lseg->p[1].y, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; - result->x = float8_div(float8_pl(lseg->p[0].x, lseg->p[1].x), 2.0); - result->y = float8_div(float8_pl(lseg->p[0].y, lseg->p[1].y), 2.0); + result->y = float8_div_safe(y, 2.0, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; PG_RETURN_POINT_P(result); + +fail: + PG_RETURN_NULL(); } @@ -2364,7 +2411,7 @@ lseg_interpt(PG_FUNCTION_ARGS) LSEG *l2 = PG_GETARG_LSEG_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (!lseg_interpt_lseg(result, l1, l2)) PG_RETURN_NULL(); @@ -2743,7 +2790,7 @@ line_closept_point(Point *result, LINE *line, Point *point) if (result != NULL) *result = closept; - return point_dt(&closept, point); + return point_dt(&closept, point, NULL); } Datum @@ -2753,7 +2800,7 @@ close_pl(PG_FUNCTION_ARGS) LINE *line = PG_GETARG_LINE_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (isnan(line_closept_point(result, line, pt))) PG_RETURN_NULL(); @@ -2784,7 +2831,7 @@ lseg_closept_point(Point *result, LSEG *lseg, Point *pt) if (result != NULL) *result = closept; - return point_dt(&closept, pt); + return point_dt(&closept, pt, NULL); } Datum @@ -2794,7 +2841,7 @@ close_ps(PG_FUNCTION_ARGS) LSEG *lseg = PG_GETARG_LSEG_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (isnan(lseg_closept_point(result, lseg, pt))) PG_RETURN_NULL(); @@ -2859,7 +2906,7 @@ close_lseg(PG_FUNCTION_ARGS) if (lseg_sl(l1) == lseg_sl(l2)) PG_RETURN_NULL(); - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (isnan(lseg_closept_lseg(result, l2, l1))) PG_RETURN_NULL(); @@ -2936,7 +2983,7 @@ close_pb(PG_FUNCTION_ARGS) BOX *box = PG_GETARG_BOX_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (isnan(box_closept_point(result, box, pt))) PG_RETURN_NULL(); @@ -2994,7 +3041,7 @@ close_ls(PG_FUNCTION_ARGS) if (lseg_sl(lseg) == line_sl(line)) PG_RETURN_NULL(); - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (isnan(lseg_closept_line(result, lseg, line))) PG_RETURN_NULL(); @@ -3066,7 +3113,7 @@ close_sb(PG_FUNCTION_ARGS) BOX *box = PG_GETARG_BOX_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); if (isnan(box_closept_lseg(result, box, lseg))) PG_RETURN_NULL(); @@ -3108,9 +3155,9 @@ on_pl(PG_FUNCTION_ARGS) static bool lseg_contain_point(LSEG *lseg, Point *pt) { - return FPeq(point_dt(pt, &lseg->p[0]) + - point_dt(pt, &lseg->p[1]), - point_dt(&lseg->p[0], &lseg->p[1])); + return FPeq(point_dt(pt, &lseg->p[0], NULL) + + point_dt(pt, &lseg->p[1], NULL), + point_dt(&lseg->p[0], &lseg->p[1], NULL)); } Datum @@ -3176,11 +3223,11 @@ on_ppath(PG_FUNCTION_ARGS) if (!path->closed) { n = path->npts - 1; - a = point_dt(pt, &path->p[0]); + a = point_dt(pt, &path->p[0], NULL); for (i = 0; i < n; i++) { - b = point_dt(pt, &path->p[i + 1]); - if (FPeq(float8_pl(a, b), point_dt(&path->p[i], &path->p[i + 1]))) + b = point_dt(pt, &path->p[i + 1], NULL); + if (FPeq(float8_pl(a, b), point_dt(&path->p[i], &path->p[i + 1], NULL))) PG_RETURN_BOOL(true); a = b; } @@ -3277,7 +3324,7 @@ box_interpt_lseg(Point *result, BOX *box, LSEG *lseg) if (result != NULL) { - box_cn(&point, box); + box_cn(&point, box, NULL); lseg_closept_point(result, lseg, &point); } @@ -4099,7 +4146,7 @@ construct_point(PG_FUNCTION_ARGS) float8 y = PG_GETARG_FLOAT8(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); point_construct(result, x, y); @@ -4108,11 +4155,20 @@ construct_point(PG_FUNCTION_ARGS) static inline void -point_add_point(Point *result, Point *pt1, Point *pt2) +point_add_point(Point *result, Point *pt1, Point *pt2, Node *escontext) { - point_construct(result, - float8_pl(pt1->x, pt2->x), - float8_pl(pt1->y, pt2->y)); + float8 x; + float8 y; + + x = float8_pl_safe(pt1->x, pt2->x, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + + y = float8_pl_safe(pt1->y, pt2->y, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + + point_construct(result, x, y); } Datum @@ -4122,9 +4178,9 @@ point_add(PG_FUNCTION_ARGS) Point *p2 = PG_GETARG_POINT_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); - point_add_point(result, p1, p2); + point_add_point(result, p1, p2, NULL); PG_RETURN_POINT_P(result); } @@ -4145,7 +4201,7 @@ point_sub(PG_FUNCTION_ARGS) Point *p2 = PG_GETARG_POINT_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); point_sub_point(result, p1, p2); @@ -4170,7 +4226,7 @@ point_mul(PG_FUNCTION_ARGS) Point *p2 = PG_GETARG_POINT_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); point_mul_point(result, p1, p2); @@ -4199,7 +4255,7 @@ point_div(PG_FUNCTION_ARGS) Point *p2 = PG_GETARG_POINT_P(1); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); point_div_point(result, p1, p2); @@ -4220,7 +4276,7 @@ points_box(PG_FUNCTION_ARGS) Point *p2 = PG_GETARG_POINT_P(1); BOX *result; - result = (BOX *) palloc(sizeof(BOX)); + result = palloc_object(BOX); box_construct(result, p1, p2); @@ -4234,10 +4290,10 @@ box_add(PG_FUNCTION_ARGS) Point *p = PG_GETARG_POINT_P(1); BOX *result; - result = (BOX *) palloc(sizeof(BOX)); + result = palloc_object(BOX); - point_add_point(&result->high, &box->high, p); - point_add_point(&result->low, &box->low, p); + point_add_point(&result->high, &box->high, p, NULL); + point_add_point(&result->low, &box->low, p, NULL); PG_RETURN_BOX_P(result); } @@ -4249,7 +4305,7 @@ box_sub(PG_FUNCTION_ARGS) Point *p = PG_GETARG_POINT_P(1); BOX *result; - result = (BOX *) palloc(sizeof(BOX)); + result = palloc_object(BOX); point_sub_point(&result->high, &box->high, p); point_sub_point(&result->low, &box->low, p); @@ -4266,7 +4322,7 @@ box_mul(PG_FUNCTION_ARGS) Point high, low; - result = (BOX *) palloc(sizeof(BOX)); + result = palloc_object(BOX); point_mul_point(&high, &box->high, p); point_mul_point(&low, &box->low, p); @@ -4285,7 +4341,7 @@ box_div(PG_FUNCTION_ARGS) Point high, low; - result = (BOX *) palloc(sizeof(BOX)); + result = palloc_object(BOX); point_div_point(&high, &box->high, p); point_div_point(&low, &box->low, p); @@ -4304,7 +4360,7 @@ point_box(PG_FUNCTION_ARGS) Point *pt = PG_GETARG_POINT_P(0); BOX *box; - box = (BOX *) palloc(sizeof(BOX)); + box = palloc_object(BOX); box->high.x = pt->x; box->low.x = pt->x; @@ -4324,7 +4380,7 @@ boxes_bound_box(PG_FUNCTION_ARGS) *box2 = PG_GETARG_BOX_P(1), *container; - container = (BOX *) palloc(sizeof(BOX)); + container = palloc_object(BOX); container->high.x = float8_max(box1->high.x, box2->high.x); container->low.x = float8_min(box1->low.x, box2->low.x); @@ -4400,7 +4456,7 @@ path_add_pt(PG_FUNCTION_ARGS) int i; for (i = 0; i < path->npts; i++) - point_add_point(&path->p[i], &path->p[i], point); + point_add_point(&path->p[i], &path->p[i], point, NULL); PG_RETURN_PATH_P(path); } @@ -4458,7 +4514,7 @@ path_poly(PG_FUNCTION_ARGS) /* This is not very consistent --- other similar cases return NULL ... */ if (!path->closed) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("open path cannot be converted to polygon"))); @@ -4506,9 +4562,12 @@ poly_center(PG_FUNCTION_ARGS) Point *result; CIRCLE circle; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); + + poly_to_circle(&circle, poly, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); - poly_to_circle(&circle, poly); *result = circle.center; PG_RETURN_POINT_P(result); @@ -4521,7 +4580,7 @@ poly_box(PG_FUNCTION_ARGS) POLYGON *poly = PG_GETARG_POLYGON_P(0); BOX *box; - box = (BOX *) palloc(sizeof(BOX)); + box = palloc_object(BOX); *box = poly->boundbox; PG_RETURN_BOX_P(box); @@ -4612,7 +4671,7 @@ circle_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); Node *escontext = fcinfo->context; - CIRCLE *circle = (CIRCLE *) palloc(sizeof(CIRCLE)); + CIRCLE *circle = palloc_object(CIRCLE); char *s, *cp; int depth = 0; @@ -4705,7 +4764,7 @@ circle_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); CIRCLE *circle; - circle = (CIRCLE *) palloc(sizeof(CIRCLE)); + circle = palloc_object(CIRCLE); circle->center.x = pq_getmsgfloat8(buf); circle->center.y = pq_getmsgfloat8(buf); @@ -4766,7 +4825,7 @@ circle_overlap(PG_FUNCTION_ARGS) CIRCLE *circle1 = PG_GETARG_CIRCLE_P(0); CIRCLE *circle2 = PG_GETARG_CIRCLE_P(1); - PG_RETURN_BOOL(FPle(point_dt(&circle1->center, &circle2->center), + PG_RETURN_BOOL(FPle(point_dt(&circle1->center, &circle2->center, NULL), float8_pl(circle1->radius, circle2->radius))); } @@ -4828,7 +4887,7 @@ circle_contained(PG_FUNCTION_ARGS) CIRCLE *circle1 = PG_GETARG_CIRCLE_P(0); CIRCLE *circle2 = PG_GETARG_CIRCLE_P(1); - PG_RETURN_BOOL(FPle(point_dt(&circle1->center, &circle2->center), + PG_RETURN_BOOL(FPle(point_dt(&circle1->center, &circle2->center, NULL), float8_mi(circle2->radius, circle1->radius))); } @@ -4840,7 +4899,7 @@ circle_contain(PG_FUNCTION_ARGS) CIRCLE *circle1 = PG_GETARG_CIRCLE_P(0); CIRCLE *circle2 = PG_GETARG_CIRCLE_P(1); - PG_RETURN_BOOL(FPle(point_dt(&circle1->center, &circle2->center), + PG_RETURN_BOOL(FPle(point_dt(&circle1->center, &circle2->center, NULL), float8_mi(circle1->radius, circle2->radius))); } @@ -4968,9 +5027,9 @@ circle_add_pt(PG_FUNCTION_ARGS) Point *point = PG_GETARG_POINT_P(1); CIRCLE *result; - result = (CIRCLE *) palloc(sizeof(CIRCLE)); + result = palloc_object(CIRCLE); - point_add_point(&result->center, &circle->center, point); + point_add_point(&result->center, &circle->center, point, NULL); result->radius = circle->radius; PG_RETURN_CIRCLE_P(result); @@ -4983,7 +5042,7 @@ circle_sub_pt(PG_FUNCTION_ARGS) Point *point = PG_GETARG_POINT_P(1); CIRCLE *result; - result = (CIRCLE *) palloc(sizeof(CIRCLE)); + result = palloc_object(CIRCLE); point_sub_point(&result->center, &circle->center, point); result->radius = circle->radius; @@ -5002,10 +5061,10 @@ circle_mul_pt(PG_FUNCTION_ARGS) Point *point = PG_GETARG_POINT_P(1); CIRCLE *result; - result = (CIRCLE *) palloc(sizeof(CIRCLE)); + result = palloc_object(CIRCLE); point_mul_point(&result->center, &circle->center, point); - result->radius = float8_mul(circle->radius, HYPOT(point->x, point->y)); + result->radius = float8_mul(circle->radius, hypot(point->x, point->y)); PG_RETURN_CIRCLE_P(result); } @@ -5017,10 +5076,10 @@ circle_div_pt(PG_FUNCTION_ARGS) Point *point = PG_GETARG_POINT_P(1); CIRCLE *result; - result = (CIRCLE *) palloc(sizeof(CIRCLE)); + result = palloc_object(CIRCLE); point_div_point(&result->center, &circle->center, point); - result->radius = float8_div(circle->radius, HYPOT(point->x, point->y)); + result->radius = float8_div(circle->radius, hypot(point->x, point->y)); PG_RETURN_CIRCLE_P(result); } @@ -5069,7 +5128,7 @@ circle_distance(PG_FUNCTION_ARGS) CIRCLE *circle2 = PG_GETARG_CIRCLE_P(1); float8 result; - result = float8_mi(point_dt(&circle1->center, &circle2->center), + result = float8_mi(point_dt(&circle1->center, &circle2->center, NULL), float8_pl(circle1->radius, circle2->radius)); if (result < 0.0) result = 0.0; @@ -5085,7 +5144,7 @@ circle_contain_pt(PG_FUNCTION_ARGS) Point *point = PG_GETARG_POINT_P(1); float8 d; - d = point_dt(&circle->center, point); + d = point_dt(&circle->center, point, NULL); PG_RETURN_BOOL(d <= circle->radius); } @@ -5097,7 +5156,7 @@ pt_contained_circle(PG_FUNCTION_ARGS) CIRCLE *circle = PG_GETARG_CIRCLE_P(1); float8 d; - d = point_dt(&circle->center, point); + d = point_dt(&circle->center, point, NULL); PG_RETURN_BOOL(d <= circle->radius); } @@ -5112,7 +5171,7 @@ dist_pc(PG_FUNCTION_ARGS) CIRCLE *circle = PG_GETARG_CIRCLE_P(1); float8 result; - result = float8_mi(point_dt(point, &circle->center), + result = float8_mi(point_dt(point, &circle->center, NULL), circle->radius); if (result < 0.0) result = 0.0; @@ -5130,7 +5189,7 @@ dist_cpoint(PG_FUNCTION_ARGS) Point *point = PG_GETARG_POINT_P(1); float8 result; - result = float8_mi(point_dt(point, &circle->center), circle->radius); + result = float8_mi(point_dt(point, &circle->center, NULL), circle->radius); if (result < 0.0) result = 0.0; @@ -5145,7 +5204,7 @@ circle_center(PG_FUNCTION_ARGS) CIRCLE *circle = PG_GETARG_CIRCLE_P(0); Point *result; - result = (Point *) palloc(sizeof(Point)); + result = palloc_object(Point); result->x = circle->center.x; result->y = circle->center.y; @@ -5173,7 +5232,7 @@ cr_circle(PG_FUNCTION_ARGS) float8 radius = PG_GETARG_FLOAT8(1); CIRCLE *result; - result = (CIRCLE *) palloc(sizeof(CIRCLE)); + result = palloc_object(CIRCLE); result->center.x = center->x; result->center.y = center->y; @@ -5189,16 +5248,32 @@ circle_box(PG_FUNCTION_ARGS) BOX *box; float8 delta; - box = (BOX *) palloc(sizeof(BOX)); + box = palloc_object(BOX); - delta = float8_div(circle->radius, sqrt(2.0)); + delta = float8_div_safe(circle->radius, sqrt(2.0), fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; + + box->high.x = float8_pl_safe(circle->center.x, delta, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; + + box->low.x = float8_mi_safe(circle->center.x, delta, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; + + box->high.y = float8_pl_safe(circle->center.y, delta, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; - box->high.x = float8_pl(circle->center.x, delta); - box->low.x = float8_mi(circle->center.x, delta); - box->high.y = float8_pl(circle->center.y, delta); - box->low.y = float8_mi(circle->center.y, delta); + box->low.y = float8_mi_safe(circle->center.y, delta, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; PG_RETURN_BOX_P(box); + +fail: + PG_RETURN_NULL(); } /* box_circle() @@ -5209,23 +5284,43 @@ box_circle(PG_FUNCTION_ARGS) { BOX *box = PG_GETARG_BOX_P(0); CIRCLE *circle; + float8 x; + float8 y; + + circle = palloc_object(CIRCLE); + + x = float8_pl_safe(box->high.x, box->low.x, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; + + circle->center.x = float8_div_safe(x, 2.0, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; - circle = (CIRCLE *) palloc(sizeof(CIRCLE)); + y = float8_pl_safe(box->high.y, box->low.y, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; - circle->center.x = float8_div(float8_pl(box->high.x, box->low.x), 2.0); - circle->center.y = float8_div(float8_pl(box->high.y, box->low.y), 2.0); + circle->center.y = float8_div_safe(y, 2.0, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; - circle->radius = point_dt(&circle->center, &box->high); + circle->radius = point_dt(&circle->center, &box->high, + fcinfo->context); + + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + goto fail; PG_RETURN_CIRCLE_P(circle); + +fail: + PG_RETURN_NULL(); } -Datum -circle_poly(PG_FUNCTION_ARGS) +static POLYGON * +circle_poly_internal(int32 npts, const CIRCLE *circle, FunctionCallInfo fcinfo) { - int32 npts = PG_GETARG_INT32(0); - CIRCLE *circle = PG_GETARG_CIRCLE_P(1); POLYGON *poly; int base_size, size; @@ -5234,12 +5329,12 @@ circle_poly(PG_FUNCTION_ARGS) float8 anglestep; if (FPzero(circle->radius)) - ereport(ERROR, + ereturn(fcinfo->context, NULL, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot convert circle with radius zero to polygon"))); if (npts < 2) - ereport(ERROR, + ereturn(fcinfo->context, NULL, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("must request at least 2 points"))); @@ -5248,7 +5343,7 @@ circle_poly(PG_FUNCTION_ARGS) /* Check for integer overflow */ if (base_size / npts != sizeof(poly->p[0]) || size <= base_size) - ereport(ERROR, + ereturn(fcinfo->context, NULL, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("too many points requested"))); @@ -5260,17 +5355,51 @@ circle_poly(PG_FUNCTION_ARGS) for (i = 0; i < npts; i++) { - angle = float8_mul(anglestep, i); + float8 temp; - poly->p[i].x = float8_mi(circle->center.x, - float8_mul(circle->radius, cos(angle))); - poly->p[i].y = float8_pl(circle->center.y, - float8_mul(circle->radius, sin(angle))); + angle = float8_mul_safe(anglestep, i, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + return NULL; + + temp = float8_mul_safe(circle->radius, cos(angle), fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + return NULL; + + poly->p[i].x = float8_mi_safe(circle->center.x, temp, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + return NULL; + + temp = float8_mul_safe(circle->radius, sin(angle), fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + return NULL; + + poly->p[i].y = float8_pl_safe(circle->center.y, temp, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + return NULL; } make_bound_box(poly); - PG_RETURN_POLYGON_P(poly); + return poly; +} + +Datum +circle_poly(PG_FUNCTION_ARGS) +{ + int32 npts = PG_GETARG_INT32(0); + CIRCLE *circle = PG_GETARG_CIRCLE_P(1); + + PG_RETURN_POLYGON_P(circle_poly_internal(npts, circle, fcinfo)); +} + +/* convert circle to 12-vertex polygon */ +Datum +circle_to_poly(PG_FUNCTION_ARGS) +{ + int32 npts = 12; + CIRCLE *circle = PG_GETARG_CIRCLE_P(0); + + PG_RETURN_POLYGON_P(circle_poly_internal(npts, circle, fcinfo)); } /* @@ -5282,9 +5411,10 @@ circle_poly(PG_FUNCTION_ARGS) * rather than straight average values of points - tgl 97/01/21. */ static void -poly_to_circle(CIRCLE *result, POLYGON *poly) +poly_to_circle(CIRCLE *result, POLYGON *poly, Node *escontext) { int i; + float8 x; Assert(poly->npts > 0); @@ -5293,14 +5423,34 @@ poly_to_circle(CIRCLE *result, POLYGON *poly) result->radius = 0; for (i = 0; i < poly->npts; i++) - point_add_point(&result->center, &result->center, &poly->p[i]); - result->center.x = float8_div(result->center.x, poly->npts); - result->center.y = float8_div(result->center.y, poly->npts); + { + point_add_point(&result->center, &result->center, &poly->p[i], escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + } + + result->center.x = float8_div_safe(result->center.x, poly->npts, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + + result->center.y = float8_div_safe(result->center.y, poly->npts, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; for (i = 0; i < poly->npts; i++) - result->radius = float8_pl(result->radius, - point_dt(&poly->p[i], &result->center)); - result->radius = float8_div(result->radius, poly->npts); + { + x = point_dt(&poly->p[i], &result->center, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + + result->radius = float8_pl_safe(result->radius, x, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; + } + + result->radius = float8_div_safe(result->radius, poly->npts, escontext); + if (SOFT_ERROR_OCCURRED(escontext)) + return; } Datum @@ -5309,9 +5459,11 @@ poly_circle(PG_FUNCTION_ARGS) POLYGON *poly = PG_GETARG_POLYGON_P(0); CIRCLE *result; - result = (CIRCLE *) palloc(sizeof(CIRCLE)); + result = palloc_object(CIRCLE); - poly_to_circle(result, poly); + poly_to_circle(result, poly, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_CIRCLE_P(result); } @@ -5492,71 +5644,3 @@ plist_same(int npts, Point *p1, Point *p2) return false; } - - -/*------------------------------------------------------------------------- - * Determine the hypotenuse. - * - * If required, x and y are swapped to make x the larger number. The - * traditional formula of x^2+y^2 is rearranged to factor x outside the - * sqrt. This allows computation of the hypotenuse for significantly - * larger values, and with a higher precision than when using the naive - * formula. In particular, this cannot overflow unless the final result - * would be out-of-range. - * - * sqrt( x^2 + y^2 ) = sqrt( x^2( 1 + y^2/x^2) ) - * = x * sqrt( 1 + y^2/x^2 ) - * = x * sqrt( 1 + y/x * y/x ) - * - * It is expected that this routine will eventually be replaced with the - * C99 hypot() function. - * - * This implementation conforms to IEEE Std 1003.1 and GLIBC, in that the - * case of hypot(inf,nan) results in INF, and not NAN. - *----------------------------------------------------------------------- - */ -float8 -pg_hypot(float8 x, float8 y) -{ - float8 yx, - result; - - /* Handle INF and NaN properly */ - if (isinf(x) || isinf(y)) - return get_float8_infinity(); - - if (isnan(x) || isnan(y)) - return get_float8_nan(); - - /* Else, drop any minus signs */ - x = fabs(x); - y = fabs(y); - - /* Swap x and y if needed to make x the larger one */ - if (x < y) - { - float8 temp = x; - - x = y; - y = temp; - } - - /* - * If y is zero, the hypotenuse is x. This test saves a few cycles in - * such cases, but more importantly it also protects against - * divide-by-zero errors, since now x >= y. - */ - if (y == 0.0) - return x; - - /* Determine the hypotenuse */ - yx = y / x; - result = x * sqrt(1.0 + (yx * yx)); - - if (unlikely(isinf(result))) - float_overflow_error(); - if (unlikely(result == 0.0)) - float_underflow_error(); - - return result; -} diff --git a/src/backend/utils/adt/geo_selfuncs.c b/src/backend/utils/adt/geo_selfuncs.c index 034585176608c..a8f7477a9dcaf 100644 --- a/src/backend/utils/adt/geo_selfuncs.c +++ b/src/backend/utils/adt/geo_selfuncs.c @@ -4,7 +4,7 @@ * Selectivity routines registered in the operator catalog in the * "oprrest" and "oprjoin" attributes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/geo_spgist.c b/src/backend/utils/adt/geo_spgist.c index fec33e9537243..7a19ca3892b44 100644 --- a/src/backend/utils/adt/geo_spgist.c +++ b/src/backend/utils/adt/geo_spgist.c @@ -62,7 +62,7 @@ * except the root. For the root node, we are setting the boundaries * that we don't yet have as infinity. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -92,8 +92,8 @@ static int compareDoubles(const void *a, const void *b) { - float8 x = *(float8 *) a; - float8 y = *(float8 *) b; + float8 x = *(const float8 *) a; + float8 y = *(const float8 *) b; if (x == y) return 0; @@ -156,7 +156,7 @@ getQuadrant(BOX *centroid, BOX *inBox) static RangeBox * getRangeBox(BOX *box) { - RangeBox *range_box = (RangeBox *) palloc(sizeof(RangeBox)); + RangeBox *range_box = palloc_object(RangeBox); range_box->left.low = box->low.x; range_box->left.high = box->high.x; @@ -176,7 +176,7 @@ getRangeBox(BOX *box) static RectBox * initRectBox(void) { - RectBox *rect_box = (RectBox *) palloc(sizeof(RectBox)); + RectBox *rect_box = palloc_object(RectBox); float8 infinity = get_float8_infinity(); rect_box->range_box_x.left.low = -infinity; @@ -204,7 +204,7 @@ initRectBox(void) static RectBox * nextRectBox(RectBox *rect_box, RangeBox *centroid, uint8 quadrant) { - RectBox *next_rect_box = (RectBox *) palloc(sizeof(RectBox)); + RectBox *next_rect_box = palloc_object(RectBox); memcpy(next_rect_box, rect_box, sizeof(RectBox)); @@ -390,7 +390,7 @@ pointToRectBoxDistance(Point *point, RectBox *rect_box) else dy = 0; - return HYPOT(dx, dy); + return hypot(dx, dy); } @@ -445,10 +445,10 @@ spg_box_quad_picksplit(PG_FUNCTION_ARGS) BOX *centroid; int median, i; - float8 *lowXs = palloc(sizeof(float8) * in->nTuples); - float8 *highXs = palloc(sizeof(float8) * in->nTuples); - float8 *lowYs = palloc(sizeof(float8) * in->nTuples); - float8 *highYs = palloc(sizeof(float8) * in->nTuples); + float8 *lowXs = palloc_array(float8, in->nTuples); + float8 *highXs = palloc_array(float8, in->nTuples); + float8 *lowYs = palloc_array(float8, in->nTuples); + float8 *highYs = palloc_array(float8, in->nTuples); /* Calculate median of all 4D coordinates */ for (i = 0; i < in->nTuples; i++) @@ -468,7 +468,7 @@ spg_box_quad_picksplit(PG_FUNCTION_ARGS) median = in->nTuples / 2; - centroid = palloc(sizeof(BOX)); + centroid = palloc_object(BOX); centroid->low.x = lowXs[median]; centroid->high.x = highXs[median]; @@ -482,8 +482,8 @@ spg_box_quad_picksplit(PG_FUNCTION_ARGS) out->nNodes = 16; out->nodeLabels = NULL; /* We don't need node labels. */ - out->mapTuplesToNodes = palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = palloc(sizeof(Datum) * in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); /* * Assign ranges to corresponding nodes according to quadrants relative to @@ -574,13 +574,13 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS) { /* Report that all nodes should be visited */ out->nNodes = in->nNodes; - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); for (i = 0; i < in->nNodes; i++) out->nodeNumbers[i] = i; if (in->norderbys > 0 && in->nNodes > 0) { - double *distances = palloc(sizeof(double) * in->norderbys); + double *distances = palloc_array(double, in->norderbys); int j; for (j = 0; j < in->norderbys; j++) @@ -590,12 +590,12 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS) distances[j] = pointToRectBoxDistance(pt, rect_box); } - out->distances = (double **) palloc(sizeof(double *) * in->nNodes); + out->distances = palloc_array(double *, in->nNodes); out->distances[0] = distances; for (i = 1; i < in->nNodes; i++) { - out->distances[i] = palloc(sizeof(double) * in->norderbys); + out->distances[i] = palloc_array(double, in->norderbys); memcpy(out->distances[i], distances, sizeof(double) * in->norderbys); } @@ -609,7 +609,7 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS) * following operations. */ centroid = getRangeBox(DatumGetBoxP(in->prefixDatum)); - queries = (RangeBox **) palloc(in->nkeys * sizeof(RangeBox *)); + queries = palloc_array(RangeBox *, in->nkeys); for (i = 0; i < in->nkeys; i++) { BOX *box = spg_box_quad_get_scankey_bbox(&in->scankeys[i], NULL); @@ -619,10 +619,10 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS) /* Allocate enough memory for nodes */ out->nNodes = 0; - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); - out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); + out->traversalValues = palloc_array(void *, in->nNodes); if (in->norderbys > 0) - out->distances = (double **) palloc(sizeof(double *) * in->nNodes); + out->distances = palloc_array(double *, in->nNodes); /* * We switch memory context, because we want to allocate memory for new @@ -703,7 +703,7 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS) if (in->norderbys > 0) { - double *distances = palloc(sizeof(double) * in->norderbys); + double *distances = palloc_array(double, in->norderbys); int j; out->distances[out->nNodes] = distances; @@ -878,7 +878,7 @@ spg_poly_quad_compress(PG_FUNCTION_ARGS) POLYGON *polygon = PG_GETARG_POLYGON_P(0); BOX *box; - box = (BOX *) palloc(sizeof(BOX)); + box = palloc_object(BOX); *box = polygon->boundbox; PG_RETURN_BOX_P(box); diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c index b62c3d944cf1a..bd23eda3f7932 100644 --- a/src/backend/utils/adt/hbafuncs.c +++ b/src/backend/utils/adt/hbafuncs.c @@ -3,7 +3,7 @@ * hbafuncs.c * Support functions for SQL views of authentication files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "catalog/objectaddress.h" #include "common/ip.h" #include "funcapi.h" @@ -21,6 +22,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/tuplestore.h" static ArrayType *get_hba_options(HbaLine *hba); @@ -133,25 +135,6 @@ get_hba_options(HbaLine *hba) CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope)); } - if (hba->auth_method == uaRADIUS) - { - if (hba->radiusservers_s) - options[noptions++] = - CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s)); - - if (hba->radiussecrets_s) - options[noptions++] = - CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s)); - - if (hba->radiusidentifiers_s) - options[noptions++] = - CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s)); - - if (hba->radiusports_s) - options[noptions++] = - CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s)); - } - if (hba->auth_method == uaOAuth) { if (hba->oauth_issuer) diff --git a/src/backend/utils/adt/inet_net_pton.c b/src/backend/utils/adt/inet_net_pton.c index ef2236d9f0430..3b0db2a379937 100644 --- a/src/backend/utils/adt/inet_net_pton.c +++ b/src/backend/utils/adt/inet_net_pton.c @@ -115,8 +115,7 @@ inet_cidr_pton_ipv4(const char *src, u_char *dst, size_t size) src++; /* skip x or X. */ while ((ch = *src++) != '\0' && isxdigit((unsigned char) ch)) { - if (isupper((unsigned char) ch)) - ch = tolower((unsigned char) ch); + ch = pg_ascii_tolower((unsigned char) ch); n = strchr(xdigits, ch) - xdigits; assert(n >= 0 && n <= 15); if (dirty == 0) diff --git a/src/backend/utils/adt/int.c b/src/backend/utils/adt/int.c index b5781989a64d5..4c894a49d5d80 100644 --- a/src/backend/utils/adt/int.c +++ b/src/backend/utils/adt/int.c @@ -3,7 +3,7 @@ * int.c * Functions for the built-in integer types (except int8). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -134,6 +134,30 @@ buildint2vector(const int16 *int2s, int n) return result; } +/* + * validate that an array object meets the restrictions of int2vector + * + * We need this because there are pathways by which a general int2[] array can + * be cast to int2vector, allowing the type's restrictions to be violated. + * All code that receives an int2vector as a SQL parameter should check this. + */ +static void +check_valid_int2vector(const int2vector *int2Array) +{ + /* + * We insist on ndim == 1 and dataoffset == 0 (that is, no nulls) because + * otherwise the array's layout will not be what calling code expects. We + * needn't be picky about the index lower bound though. Checking elemtype + * is just paranoia. + */ + if (int2Array->ndim != 1 || + int2Array->dataoffset != 0 || + int2Array->elemtype != INT2OID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("array is not a valid int2vector"))); +} + /* * int2vectorin - converts "num num ..." to internal form */ @@ -208,10 +232,14 @@ int2vectorout(PG_FUNCTION_ARGS) { int2vector *int2Array = (int2vector *) PG_GETARG_POINTER(0); int num, - nnums = int2Array->dim1; + nnums; char *rp; char *result; + /* validate input before fetching dim1 */ + check_valid_int2vector(int2Array); + nnums = int2Array->dim1; + /* assumes sign, 5 digits, ' ' */ rp = result = (char *) palloc(nnums * 7 + 1); for (num = 0; num < nnums; num++) @@ -272,6 +300,7 @@ int2vectorrecv(PG_FUNCTION_ARGS) Datum int2vectorsend(PG_FUNCTION_ARGS) { + /* We don't do check_valid_int2vector, since array_send won't care */ return array_send(fcinfo); } @@ -350,7 +379,7 @@ i4toi2(PG_FUNCTION_ARGS) int32 arg1 = PG_GETARG_INT32(0); if (unlikely(arg1 < SHRT_MIN) || unlikely(arg1 > SHRT_MAX)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("smallint out of range"))); @@ -1537,7 +1566,7 @@ generate_series_step_int4(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - fctx = (generate_series_fctx *) palloc(sizeof(generate_series_fctx)); + fctx = palloc_object(generate_series_fctx); /* * Use fctx to keep state from call to call. Seed current with the diff --git a/src/backend/utils/adt/int8.c b/src/backend/utils/adt/int8.c index 9dd5889f34c62..19bb30f2d0f53 100644 --- a/src/backend/utils/adt/int8.c +++ b/src/backend/utils/adt/int8.c @@ -3,7 +3,7 @@ * int8.c * Internal 64-bit integer operations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -24,7 +24,7 @@ #include "nodes/supportnodes.h" #include "optimizer/optimizer.h" #include "utils/builtins.h" - +#include "utils/fmgroids.h" typedef struct { @@ -718,76 +718,29 @@ int8lcm(PG_FUNCTION_ARGS) Datum int8inc(PG_FUNCTION_ARGS) { - /* - * When int8 is pass-by-reference, we provide this special case to avoid - * palloc overhead for COUNT(): when called as an aggregate, we know that - * the argument is modifiable local storage, so just update it in-place. - * (If int8 is pass-by-value, then of course this is useless as well as - * incorrect, so just ifdef it out.) - */ -#ifndef USE_FLOAT8_BYVAL /* controls int8 too */ - if (AggCheckCallContext(fcinfo, NULL)) - { - int64 *arg = (int64 *) PG_GETARG_POINTER(0); - - if (unlikely(pg_add_s64_overflow(*arg, 1, arg))) - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("bigint out of range"))); - - PG_RETURN_POINTER(arg); - } - else -#endif - { - /* Not called as an aggregate, so just do it the dumb way */ - int64 arg = PG_GETARG_INT64(0); - int64 result; + int64 arg = PG_GETARG_INT64(0); + int64 result; - if (unlikely(pg_add_s64_overflow(arg, 1, &result))) - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("bigint out of range"))); + if (unlikely(pg_add_s64_overflow(arg, 1, &result))) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("bigint out of range"))); - PG_RETURN_INT64(result); - } + PG_RETURN_INT64(result); } Datum int8dec(PG_FUNCTION_ARGS) { - /* - * When int8 is pass-by-reference, we provide this special case to avoid - * palloc overhead for COUNT(): when called as an aggregate, we know that - * the argument is modifiable local storage, so just update it in-place. - * (If int8 is pass-by-value, then of course this is useless as well as - * incorrect, so just ifdef it out.) - */ -#ifndef USE_FLOAT8_BYVAL /* controls int8 too */ - if (AggCheckCallContext(fcinfo, NULL)) - { - int64 *arg = (int64 *) PG_GETARG_POINTER(0); - - if (unlikely(pg_sub_s64_overflow(*arg, 1, arg))) - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("bigint out of range"))); - PG_RETURN_POINTER(arg); - } - else -#endif - { - /* Not called as an aggregate, so just do it the dumb way */ - int64 arg = PG_GETARG_INT64(0); - int64 result; + int64 arg = PG_GETARG_INT64(0); + int64 result; - if (unlikely(pg_sub_s64_overflow(arg, 1, &result))) - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("bigint out of range"))); + if (unlikely(pg_sub_s64_overflow(arg, 1, &result))) + ereport(ERROR, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("bigint out of range"))); - PG_RETURN_INT64(result); - } + PG_RETURN_INT64(result); } @@ -858,6 +811,53 @@ int8inc_support(PG_FUNCTION_ARGS) PG_RETURN_POINTER(req); } + if (IsA(rawreq, SupportRequestSimplifyAggref)) + { + SupportRequestSimplifyAggref *req = (SupportRequestSimplifyAggref *) rawreq; + Aggref *agg = req->aggref; + + /* + * Check for COUNT(ANY) and try to convert to COUNT(*). The input + * argument cannot be NULL, we can't have an ORDER BY / DISTINCT in + * the aggregate, and agglevelsup must be 0. + * + * Technically COUNT(ANY) must have 1 arg, but be paranoid and check. + */ + if (agg->aggfnoid == F_COUNT_ANY && list_length(agg->args) == 1) + { + TargetEntry *tle = (TargetEntry *) linitial(agg->args); + Expr *arg = tle->expr; + + /* Check for unsupported cases */ + if (agg->aggdistinct != NIL || agg->aggorder != NIL || + agg->agglevelsup != 0) + PG_RETURN_POINTER(NULL); + + /* If the arg isn't NULLable, do the conversion */ + if (expr_is_nonnullable(req->root, arg, NOTNULL_SOURCE_HASHTABLE)) + { + Aggref *newagg; + + /* We don't expect these to have been set yet */ + Assert(agg->aggtransno == -1); + Assert(agg->aggtranstype == InvalidOid); + + /* Convert COUNT(ANY) to COUNT(*) by making a new Aggref */ + newagg = makeNode(Aggref); + memcpy(newagg, agg, sizeof(Aggref)); + newagg->aggfnoid = F_COUNT_; + + /* count(*) has no args */ + newagg->aggargtypes = NULL; + newagg->args = NULL; + newagg->aggstar = true; + newagg->location = -1; + + PG_RETURN_POINTER(newagg); + } + } + } + PG_RETURN_POINTER(NULL); } @@ -1251,7 +1251,7 @@ int84(PG_FUNCTION_ARGS) int64 arg = PG_GETARG_INT64(0); if (unlikely(arg < PG_INT32_MIN) || unlikely(arg > PG_INT32_MAX)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("integer out of range"))); @@ -1272,7 +1272,7 @@ int82(PG_FUNCTION_ARGS) int64 arg = PG_GETARG_INT64(0); if (unlikely(arg < PG_INT16_MIN) || unlikely(arg > PG_INT16_MAX)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("smallint out of range"))); @@ -1307,7 +1307,7 @@ dtoi8(PG_FUNCTION_ARGS) /* Range check */ if (unlikely(isnan(num) || !FLOAT8_FITS_IN_INT64(num))) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("bigint out of range"))); @@ -1342,7 +1342,7 @@ ftoi8(PG_FUNCTION_ARGS) /* Range check */ if (unlikely(isnan(num) || !FLOAT4_FITS_IN_INT64(num))) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("bigint out of range"))); @@ -1355,7 +1355,7 @@ i8tooid(PG_FUNCTION_ARGS) int64 arg = PG_GETARG_INT64(0); if (unlikely(arg < 0) || unlikely(arg > PG_UINT32_MAX)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("OID out of range"))); @@ -1370,6 +1370,14 @@ oidtoi8(PG_FUNCTION_ARGS) PG_RETURN_INT64((int64) arg); } +Datum +oidtooid8(PG_FUNCTION_ARGS) +{ + Oid arg = PG_GETARG_OID(0); + + PG_RETURN_OID8((Oid8) arg); +} + /* * non-persistent numeric series generator */ @@ -1411,7 +1419,7 @@ generate_series_step_int8(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - fctx = (generate_series_fctx *) palloc(sizeof(generate_series_fctx)); + fctx = palloc_object(generate_series_fctx); /* * Use fctx to keep state from call to call. Seed current with the diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index 51452755f5868..0fee1b40d6374 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -3,7 +3,7 @@ * json.c * JSON data type support. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -13,7 +13,7 @@ */ #include "postgres.h" -#include "catalog/pg_proc.h" +#include "access/htup_details.h" #include "catalog/pg_type.h" #include "common/hashfn.h" #include "funcapi.h" @@ -25,6 +25,7 @@ #include "utils/date.h" #include "utils/datetime.h" #include "utils/fmgroids.h" +#include "utils/hsearch.h" #include "utils/json.h" #include "utils/jsonfuncs.h" #include "utils/lsyscache.h" @@ -85,10 +86,8 @@ typedef struct JsonAggState JsonUniqueBuilderState unique_check; } JsonAggState; -static void composite_to_json(Datum composite, StringInfo result, - bool use_line_feeds); static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, - Datum *vals, bool *nulls, int *valcount, + const Datum *vals, const bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, @@ -428,8 +427,8 @@ JsonEncodeDateTime(char *buf, Datum value, Oid typid, const int *tzp) * ourselves recursively to process the next dimension. */ static void -array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, Datum *vals, - bool *nulls, int *valcount, JsonTypeCategory tcategory, +array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, const Datum *vals, + const bool *nulls, int *valcount, JsonTypeCategory tcategory, Oid outfuncoid, bool use_line_feeds) { int i; @@ -516,8 +515,9 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) /* * Turn a composite / record into JSON. + * Exported so COPY TO can use it. */ -static void +void composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) { HeapTupleHeader td; @@ -630,13 +630,13 @@ Datum array_to_json(PG_FUNCTION_ARGS) { Datum array = PG_GETARG_DATUM(0); - StringInfo result; + StringInfoData result; - result = makeStringInfo(); + initStringInfo(&result); - array_to_json_internal(array, result, false); + array_to_json_internal(array, &result, false); - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_TEXT_P(cstring_to_text_with_len(result.data, result.len)); } /* @@ -647,13 +647,13 @@ array_to_json_pretty(PG_FUNCTION_ARGS) { Datum array = PG_GETARG_DATUM(0); bool use_line_feeds = PG_GETARG_BOOL(1); - StringInfo result; + StringInfoData result; - result = makeStringInfo(); + initStringInfo(&result); - array_to_json_internal(array, result, use_line_feeds); + array_to_json_internal(array, &result, use_line_feeds); - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_TEXT_P(cstring_to_text_with_len(result.data, result.len)); } /* @@ -663,13 +663,13 @@ Datum row_to_json(PG_FUNCTION_ARGS) { Datum array = PG_GETARG_DATUM(0); - StringInfo result; + StringInfoData result; - result = makeStringInfo(); + initStringInfo(&result); - composite_to_json(array, result, false); + composite_to_json(array, &result, false); - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_TEXT_P(cstring_to_text_with_len(result.data, result.len)); } /* @@ -680,56 +680,25 @@ row_to_json_pretty(PG_FUNCTION_ARGS) { Datum array = PG_GETARG_DATUM(0); bool use_line_feeds = PG_GETARG_BOOL(1); - StringInfo result; + StringInfoData result; - result = makeStringInfo(); + initStringInfo(&result); - composite_to_json(array, result, use_line_feeds); + composite_to_json(array, &result, use_line_feeds); - PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len)); + PG_RETURN_TEXT_P(cstring_to_text_with_len(result.data, result.len)); } /* * Is the given type immutable when coming out of a JSON context? - * - * At present, datetimes are all considered mutable, because they - * depend on timezone. XXX we should also drill down into objects - * and arrays, but do not. */ bool to_json_is_immutable(Oid typoid) { - JsonTypeCategory tcategory; - Oid outfuncoid; - - json_categorize_type(typoid, false, &tcategory, &outfuncoid); - - switch (tcategory) - { - case JSONTYPE_BOOL: - case JSONTYPE_JSON: - case JSONTYPE_JSONB: - case JSONTYPE_NULL: - return true; - - case JSONTYPE_DATE: - case JSONTYPE_TIMESTAMP: - case JSONTYPE_TIMESTAMPTZ: - return false; + bool has_mutable = false; - case JSONTYPE_ARRAY: - return false; /* TODO recurse into elements */ - - case JSONTYPE_COMPOSITE: - return false; /* TODO recurse into fields */ - - case JSONTYPE_NUMERIC: - case JSONTYPE_CAST: - case JSONTYPE_OTHER: - return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; - } - - return false; /* not reached */ + json_check_mutability(typoid, false, &has_mutable); + return !has_mutable; } /* @@ -762,12 +731,13 @@ to_json(PG_FUNCTION_ARGS) Datum datum_to_json(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) { - StringInfo result = makeStringInfo(); + StringInfoData result; - datum_to_json_internal(val, false, result, tcategory, outfuncoid, + initStringInfo(&result); + datum_to_json_internal(val, false, &result, tcategory, outfuncoid, false); - return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); + return PointerGetDatum(cstring_to_text_with_len(result.data, result.len)); } /* @@ -805,7 +775,7 @@ json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) * use the right context to enlarge the object if necessary. */ oldcontext = MemoryContextSwitchTo(aggcontext); - state = (JsonAggState *) palloc(sizeof(JsonAggState)); + state = palloc_object(JsonAggState); state->str = makeStringInfo(); MemoryContextSwitchTo(oldcontext); @@ -899,12 +869,12 @@ json_agg_finalfn(PG_FUNCTION_ARGS) static uint32 json_unique_hash(const void *key, Size keysize) { - const JsonUniqueHashEntry *entry = (JsonUniqueHashEntry *) key; + const JsonUniqueHashEntry *entry = (const JsonUniqueHashEntry *) key; uint32 hash = hash_bytes_uint32(entry->object_id); hash ^= hash_bytes((const unsigned char *) entry->key, entry->key_len); - return DatumGetUInt32(hash); + return hash; } static int @@ -1027,7 +997,7 @@ json_object_agg_transfn_worker(FunctionCallInfo fcinfo, * sure they use the right context to enlarge the object if necessary. */ oldcontext = MemoryContextSwitchTo(aggcontext); - state = (JsonAggState *) palloc(sizeof(JsonAggState)); + state = palloc_object(JsonAggState); state->str = makeStringInfo(); if (unique_keys) json_unique_builder_init(&state->unique_check); @@ -1346,25 +1316,25 @@ json_build_array_worker(int nargs, const Datum *args, const bool *nulls, const O { int i; const char *sep = ""; - StringInfo result; + StringInfoData result; - result = makeStringInfo(); + initStringInfo(&result); - appendStringInfoChar(result, '['); + appendStringInfoChar(&result, '['); for (i = 0; i < nargs; i++) { if (absent_on_null && nulls[i]) continue; - appendStringInfoString(result, sep); + appendStringInfoString(&result, sep); sep = ", "; - add_json(args[i], nulls[i], result, types[i], false); + add_json(args[i], nulls[i], &result, types[i], false); } - appendStringInfoChar(result, ']'); + appendStringInfoChar(&result, ']'); - return PointerGetDatum(cstring_to_text_with_len(result->data, result->len)); + return PointerGetDatum(cstring_to_text_with_len(result.data, result.len)); } /* @@ -1760,7 +1730,7 @@ json_unique_object_start(void *_state) return JSON_SUCCESS; /* push object entry to stack */ - entry = palloc(sizeof(*entry)); + entry = palloc_object(JsonUniqueStackEntry); entry->object_id = state->id_counter++; entry->parent = state->stack; state->stack = entry; diff --git a/src/backend/utils/adt/jsonb.c b/src/backend/utils/adt/jsonb.c index da94d424d617f..864c5ac1c85a5 100644 --- a/src/backend/utils/adt/jsonb.c +++ b/src/backend/utils/adt/jsonb.c @@ -3,7 +3,7 @@ * jsonb.c * I/O routines for jsonb type * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/jsonb.c @@ -13,29 +13,21 @@ #include "postgres.h" #include "access/htup_details.h" -#include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" #include "utils/builtins.h" +#include "utils/fmgroids.h" #include "utils/json.h" #include "utils/jsonb.h" #include "utils/jsonfuncs.h" #include "utils/lsyscache.h" #include "utils/typcache.h" -typedef struct JsonbInState -{ - JsonbParseState *parseState; - JsonbValue *res; - bool unique_keys; - Node *escontext; -} JsonbInState; - typedef struct JsonbAggState { - JsonbInState *res; + JsonbInState pstate; JsonTypeCategory key_category; Oid key_output_func; JsonTypeCategory val_category; @@ -62,7 +54,6 @@ static void datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *resul bool key_scalar); static void add_jsonb(Datum val, bool is_null, JsonbInState *result, Oid val_type, bool key_scalar); -static JsonbParseState *clone_parse_state(JsonbParseState *state); static char *JsonbToCStringWorker(StringInfo out, JsonbContainer *in, int estimated_len, bool indent); static void add_indent(StringInfo out, bool indent, int level); @@ -125,15 +116,16 @@ jsonb_send(PG_FUNCTION_ARGS) { Jsonb *jb = PG_GETARG_JSONB_P(0); StringInfoData buf; - StringInfo jtext = makeStringInfo(); + StringInfoData jtext; int version = 1; - (void) JsonbToCString(jtext, &jb->root, VARSIZE(jb)); + initStringInfo(&jtext); + (void) JsonbToCString(&jtext, &jb->root, VARSIZE(jb)); pq_begintypsend(&buf); pq_sendint8(&buf, version); - pq_sendtext(&buf, jtext->data, jtext->len); - destroyStringInfo(jtext); + pq_sendtext(&buf, jtext.data, jtext.len); + pfree(jtext.data); PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } @@ -269,8 +261,8 @@ jsonb_from_cstring(char *json, int len, bool unique_keys, Node *escontext) if (!pg_parse_json_or_errsave(&lex, &sem, escontext)) return (Datum) 0; - /* after parsing, the item member has the composed jsonb structure */ - PG_RETURN_POINTER(JsonbValueToJsonb(state.res)); + /* after parsing, the result field has the composed jsonb structure */ + PG_RETURN_POINTER(JsonbValueToJsonb(state.result)); } static bool @@ -291,7 +283,7 @@ jsonb_in_object_start(void *pstate) { JsonbInState *_state = (JsonbInState *) pstate; - _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(_state, WJB_BEGIN_OBJECT, NULL); _state->parseState->unique_keys = _state->unique_keys; return JSON_SUCCESS; @@ -302,7 +294,7 @@ jsonb_in_object_end(void *pstate) { JsonbInState *_state = (JsonbInState *) pstate; - _state->res = pushJsonbValue(&_state->parseState, WJB_END_OBJECT, NULL); + pushJsonbValue(_state, WJB_END_OBJECT, NULL); return JSON_SUCCESS; } @@ -312,7 +304,7 @@ jsonb_in_array_start(void *pstate) { JsonbInState *_state = (JsonbInState *) pstate; - _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_ARRAY, NULL); + pushJsonbValue(_state, WJB_BEGIN_ARRAY, NULL); return JSON_SUCCESS; } @@ -322,7 +314,7 @@ jsonb_in_array_end(void *pstate) { JsonbInState *_state = (JsonbInState *) pstate; - _state->res = pushJsonbValue(&_state->parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(_state, WJB_END_ARRAY, NULL); return JSON_SUCCESS; } @@ -340,7 +332,7 @@ jsonb_in_object_field_start(void *pstate, char *fname, bool isnull) return JSON_SEM_ACTION_FAILED; v.val.string.val = fname; - _state->res = pushJsonbValue(&_state->parseState, WJB_KEY, &v); + pushJsonbValue(_state, WJB_KEY, &v); return JSON_SUCCESS; } @@ -434,9 +426,9 @@ jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype) va.val.array.rawScalar = true; va.val.array.nElems = 1; - _state->res = pushJsonbValue(&_state->parseState, WJB_BEGIN_ARRAY, &va); - _state->res = pushJsonbValue(&_state->parseState, WJB_ELEM, &v); - _state->res = pushJsonbValue(&_state->parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(_state, WJB_BEGIN_ARRAY, &va); + pushJsonbValue(_state, WJB_ELEM, &v); + pushJsonbValue(_state, WJB_END_ARRAY, NULL); } else { @@ -445,10 +437,10 @@ jsonb_in_scalar(void *pstate, char *token, JsonTokenType tokentype) switch (o->type) { case jbvArray: - _state->res = pushJsonbValue(&_state->parseState, WJB_ELEM, &v); + pushJsonbValue(_state, WJB_ELEM, &v); break; case jbvObject: - _state->res = pushJsonbValue(&_state->parseState, WJB_VALUE, &v); + pushJsonbValue(_state, WJB_VALUE, &v); break; default: elog(ERROR, "unexpected parent of nested structure"); @@ -640,7 +632,8 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, bool key_scalar) { char *outputstr; - bool numeric_error; + Numeric numeric_val; + bool numeric_to_string; JsonbValue jb; bool scalar_jsonb = false; @@ -665,9 +658,6 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, } else { - if (tcategory == JSONTYPE_CAST) - val = OidFunctionCall1(outfuncoid, val); - switch (tcategory) { case JSONTYPE_ARRAY: @@ -691,41 +681,73 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, } break; case JSONTYPE_NUMERIC: - outputstr = OidOutputFunctionCall(outfuncoid, val); if (key_scalar) { - /* always quote keys */ + /* always stringify keys */ + numeric_to_string = true; + numeric_val = NULL; /* pacify stupider compilers */ + } + else + { + Datum numd; + + switch (outfuncoid) + { + case F_NUMERIC_OUT: + numeric_val = DatumGetNumeric(val); + break; + case F_INT2OUT: + numeric_val = int64_to_numeric(DatumGetInt16(val)); + break; + case F_INT4OUT: + numeric_val = int64_to_numeric(DatumGetInt32(val)); + break; + case F_INT8OUT: + numeric_val = int64_to_numeric(DatumGetInt64(val)); + break; +#ifdef NOT_USED + + /* + * Ideally we'd short-circuit these two cases + * using float[48]_numeric. However, those + * functions are currently slower than the generic + * coerce-via-I/O approach. And they may round + * off differently. Until/unless that gets fixed, + * continue to use coerce-via-I/O for floats. + */ + case F_FLOAT4OUT: + numd = DirectFunctionCall1(float4_numeric, val); + numeric_val = DatumGetNumeric(numd); + break; + case F_FLOAT8OUT: + numd = DirectFunctionCall1(float8_numeric, val); + numeric_val = DatumGetNumeric(numd); + break; +#endif + default: + outputstr = OidOutputFunctionCall(outfuncoid, val); + numd = DirectFunctionCall3(numeric_in, + CStringGetDatum(outputstr), + ObjectIdGetDatum(InvalidOid), + Int32GetDatum(-1)); + numeric_val = DatumGetNumeric(numd); + break; + } + /* Must convert to string if it's Inf or NaN */ + numeric_to_string = (numeric_is_inf(numeric_val) || + numeric_is_nan(numeric_val)); + } + if (numeric_to_string) + { + outputstr = OidOutputFunctionCall(outfuncoid, val); jb.type = jbvString; jb.val.string.len = strlen(outputstr); jb.val.string.val = outputstr; } else { - /* - * Make it numeric if it's a valid JSON number, otherwise - * a string. Invalid numeric output will always have an - * 'N' or 'n' in it (I think). - */ - numeric_error = (strchr(outputstr, 'N') != NULL || - strchr(outputstr, 'n') != NULL); - if (!numeric_error) - { - Datum numd; - - jb.type = jbvNumeric; - numd = DirectFunctionCall3(numeric_in, - CStringGetDatum(outputstr), - ObjectIdGetDatum(InvalidOid), - Int32GetDatum(-1)); - jb.val.numeric = DatumGetNumeric(numd); - pfree(outputstr); - } - else - { - jb.type = jbvString; - jb.val.string.len = strlen(outputstr); - jb.val.string.val = outputstr; - } + jb.type = jbvNumeric; + jb.val.numeric = numeric_val; } break; case JSONTYPE_DATE: @@ -747,6 +769,9 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, jb.val.string.len = strlen(jb.val.string.val); break; case JSONTYPE_CAST: + /* cast to JSON, and then process as JSON */ + val = OidFunctionCall1(outfuncoid, val); + pg_fallthrough; case JSONTYPE_JSON: { /* parse the json right into the existing result object */ @@ -794,21 +819,32 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, { if (type == WJB_END_ARRAY || type == WJB_END_OBJECT || type == WJB_BEGIN_ARRAY || type == WJB_BEGIN_OBJECT) - result->res = pushJsonbValue(&result->parseState, - type, NULL); + pushJsonbValue(result, type, NULL); else - result->res = pushJsonbValue(&result->parseState, - type, &jb); + pushJsonbValue(result, type, &jb); } } } break; default: - outputstr = OidOutputFunctionCall(outfuncoid, val); + /* special-case text types to save useless palloc/memcpy ops */ + if (outfuncoid == F_TEXTOUT || + outfuncoid == F_VARCHAROUT || + outfuncoid == F_BPCHAROUT) + { + text *txt = DatumGetTextPP(val); + + jb.val.string.len = VARSIZE_ANY_EXHDR(txt); + jb.val.string.val = VARDATA_ANY(txt); + } + else + { + outputstr = OidOutputFunctionCall(outfuncoid, val); + jb.val.string.len = strlen(outputstr); + jb.val.string.val = outputstr; + } jb.type = jbvString; - jb.val.string.len = strlen(outputstr); (void) checkStringLen(jb.val.string.len, NULL); - jb.val.string.val = outputstr; break; } } @@ -829,9 +865,9 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, va.val.array.rawScalar = true; va.val.array.nElems = 1; - result->res = pushJsonbValue(&result->parseState, WJB_BEGIN_ARRAY, &va); - result->res = pushJsonbValue(&result->parseState, WJB_ELEM, &jb); - result->res = pushJsonbValue(&result->parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(result, WJB_BEGIN_ARRAY, &va); + pushJsonbValue(result, WJB_ELEM, &jb); + pushJsonbValue(result, WJB_END_ARRAY, NULL); } else { @@ -840,12 +876,12 @@ datum_to_jsonb_internal(Datum val, bool is_null, JsonbInState *result, switch (o->type) { case jbvArray: - result->res = pushJsonbValue(&result->parseState, WJB_ELEM, &jb); + pushJsonbValue(result, WJB_ELEM, &jb); break; case jbvObject: - result->res = pushJsonbValue(&result->parseState, - key_scalar ? WJB_KEY : WJB_VALUE, - &jb); + pushJsonbValue(result, + key_scalar ? WJB_KEY : WJB_VALUE, + &jb); break; default: elog(ERROR, "unexpected parent of nested structure"); @@ -867,7 +903,7 @@ array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, const Da Assert(dim < ndims); - result->res = pushJsonbValue(&result->parseState, WJB_BEGIN_ARRAY, NULL); + pushJsonbValue(result, WJB_BEGIN_ARRAY, NULL); for (i = 1; i <= dims[dim]; i++) { @@ -884,7 +920,7 @@ array_dim_to_jsonb(JsonbInState *result, int dim, int ndims, int *dims, const Da } } - result->res = pushJsonbValue(&result->parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(result, WJB_END_ARRAY, NULL); } /* @@ -913,8 +949,8 @@ array_to_jsonb_internal(Datum array, JsonbInState *result) if (nitems <= 0) { - result->res = pushJsonbValue(&result->parseState, WJB_BEGIN_ARRAY, NULL); - result->res = pushJsonbValue(&result->parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(result, WJB_BEGIN_ARRAY, NULL); + pushJsonbValue(result, WJB_END_ARRAY, NULL); return; } @@ -961,7 +997,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) tmptup.t_data = td; tuple = &tmptup; - result->res = pushJsonbValue(&result->parseState, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(result, WJB_BEGIN_OBJECT, NULL); for (i = 0; i < tupdesc->natts; i++) { @@ -983,7 +1019,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) v.val.string.len = strlen(attname); v.val.string.val = attname; - result->res = pushJsonbValue(&result->parseState, WJB_KEY, &v); + pushJsonbValue(result, WJB_KEY, &v); val = heap_getattr(tuple, i + 1, tupdesc, &isnull); @@ -1000,7 +1036,7 @@ composite_to_jsonb(Datum composite, JsonbInState *result) false); } - result->res = pushJsonbValue(&result->parseState, WJB_END_OBJECT, NULL); + pushJsonbValue(result, WJB_END_OBJECT, NULL); ReleaseTupleDesc(tupdesc); } @@ -1040,45 +1076,14 @@ add_jsonb(Datum val, bool is_null, JsonbInState *result, /* * Is the given type immutable when coming out of a JSONB context? - * - * At present, datetimes are all considered mutable, because they - * depend on timezone. XXX we should also drill down into objects and - * arrays, but do not. */ bool to_jsonb_is_immutable(Oid typoid) { - JsonTypeCategory tcategory; - Oid outfuncoid; + bool has_mutable = false; - json_categorize_type(typoid, true, &tcategory, &outfuncoid); - - switch (tcategory) - { - case JSONTYPE_NULL: - case JSONTYPE_BOOL: - case JSONTYPE_JSON: - case JSONTYPE_JSONB: - return true; - - case JSONTYPE_DATE: - case JSONTYPE_TIMESTAMP: - case JSONTYPE_TIMESTAMPTZ: - return false; - - case JSONTYPE_ARRAY: - return false; /* TODO recurse into elements */ - - case JSONTYPE_COMPOSITE: - return false; /* TODO recurse into fields */ - - case JSONTYPE_NUMERIC: - case JSONTYPE_CAST: - case JSONTYPE_OTHER: - return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE; - } - - return false; /* not reached */ + json_check_mutability(typoid, true, &has_mutable); + return !has_mutable; } /* @@ -1118,7 +1123,7 @@ datum_to_jsonb(Datum val, JsonTypeCategory tcategory, Oid outfuncoid) datum_to_jsonb_internal(val, false, &result, tcategory, outfuncoid, false); - return JsonbPGetDatum(JsonbValueToJsonb(result.res)); + return JsonbPGetDatum(JsonbValueToJsonb(result.result)); } Datum @@ -1138,7 +1143,7 @@ jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls, const memset(&result, 0, sizeof(JsonbInState)); - result.res = pushJsonbValue(&result.parseState, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(&result, WJB_BEGIN_OBJECT, NULL); result.parseState->unique_keys = unique_keys; result.parseState->skip_nulls = absent_on_null; @@ -1165,9 +1170,9 @@ jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls, const add_jsonb(args[i + 1], nulls[i + 1], &result, types[i + 1], false); } - result.res = pushJsonbValue(&result.parseState, WJB_END_OBJECT, NULL); + pushJsonbValue(&result, WJB_END_OBJECT, NULL); - return JsonbPGetDatum(JsonbValueToJsonb(result.res)); + return JsonbPGetDatum(JsonbValueToJsonb(result.result)); } /* @@ -1200,10 +1205,10 @@ jsonb_build_object_noargs(PG_FUNCTION_ARGS) memset(&result, 0, sizeof(JsonbInState)); - (void) pushJsonbValue(&result.parseState, WJB_BEGIN_OBJECT, NULL); - result.res = pushJsonbValue(&result.parseState, WJB_END_OBJECT, NULL); + pushJsonbValue(&result, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(&result, WJB_END_OBJECT, NULL); - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_POINTER(JsonbValueToJsonb(result.result)); } Datum @@ -1215,7 +1220,7 @@ jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls, const memset(&result, 0, sizeof(JsonbInState)); - result.res = pushJsonbValue(&result.parseState, WJB_BEGIN_ARRAY, NULL); + pushJsonbValue(&result, WJB_BEGIN_ARRAY, NULL); for (i = 0; i < nargs; i++) { @@ -1225,9 +1230,9 @@ jsonb_build_array_worker(int nargs, const Datum *args, const bool *nulls, const add_jsonb(args[i], nulls[i], &result, types[i], false); } - result.res = pushJsonbValue(&result.parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(&result, WJB_END_ARRAY, NULL); - return JsonbPGetDatum(JsonbValueToJsonb(result.res)); + return JsonbPGetDatum(JsonbValueToJsonb(result.result)); } /* @@ -1261,10 +1266,10 @@ jsonb_build_array_noargs(PG_FUNCTION_ARGS) memset(&result, 0, sizeof(JsonbInState)); - (void) pushJsonbValue(&result.parseState, WJB_BEGIN_ARRAY, NULL); - result.res = pushJsonbValue(&result.parseState, WJB_END_ARRAY, NULL); + pushJsonbValue(&result, WJB_BEGIN_ARRAY, NULL); + pushJsonbValue(&result, WJB_END_ARRAY, NULL); - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_POINTER(JsonbValueToJsonb(result.result)); } @@ -1289,7 +1294,7 @@ jsonb_object(PG_FUNCTION_ARGS) memset(&result, 0, sizeof(JsonbInState)); - (void) pushJsonbValue(&result.parseState, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(&result, WJB_BEGIN_OBJECT, NULL); switch (ndims) { @@ -1340,7 +1345,7 @@ jsonb_object(PG_FUNCTION_ARGS) v.val.string.len = len; v.val.string.val = str; - (void) pushJsonbValue(&result.parseState, WJB_KEY, &v); + pushJsonbValue(&result, WJB_KEY, &v); if (in_nulls[i * 2 + 1]) { @@ -1357,16 +1362,16 @@ jsonb_object(PG_FUNCTION_ARGS) v.val.string.val = str; } - (void) pushJsonbValue(&result.parseState, WJB_VALUE, &v); + pushJsonbValue(&result, WJB_VALUE, &v); } pfree(in_datums); pfree(in_nulls); close_object: - result.res = pushJsonbValue(&result.parseState, WJB_END_OBJECT, NULL); + pushJsonbValue(&result, WJB_END_OBJECT, NULL); - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_POINTER(JsonbValueToJsonb(result.result)); } /* @@ -1393,7 +1398,7 @@ jsonb_object_two_arg(PG_FUNCTION_ARGS) memset(&result, 0, sizeof(JsonbInState)); - (void) pushJsonbValue(&result.parseState, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(&result, WJB_BEGIN_OBJECT, NULL); if (nkdims > 1 || nkdims != nvdims) ereport(ERROR, @@ -1430,7 +1435,7 @@ jsonb_object_two_arg(PG_FUNCTION_ARGS) v.val.string.len = len; v.val.string.val = str; - (void) pushJsonbValue(&result.parseState, WJB_KEY, &v); + pushJsonbValue(&result, WJB_KEY, &v); if (val_nulls[i]) { @@ -1447,7 +1452,7 @@ jsonb_object_two_arg(PG_FUNCTION_ARGS) v.val.string.val = str; } - (void) pushJsonbValue(&result.parseState, WJB_VALUE, &v); + pushJsonbValue(&result, WJB_VALUE, &v); } pfree(key_datums); @@ -1456,61 +1461,23 @@ jsonb_object_two_arg(PG_FUNCTION_ARGS) pfree(val_nulls); close_object: - result.res = pushJsonbValue(&result.parseState, WJB_END_OBJECT, NULL); + pushJsonbValue(&result, WJB_END_OBJECT, NULL); - PG_RETURN_POINTER(JsonbValueToJsonb(result.res)); + PG_RETURN_POINTER(JsonbValueToJsonb(result.result)); } /* - * shallow clone of a parse state, suitable for use in aggregate - * final functions that will only append to the values rather than - * change them. + * Functions for jsonb_agg, jsonb_object_agg, and variants */ -static JsonbParseState * -clone_parse_state(JsonbParseState *state) -{ - JsonbParseState *result, - *icursor, - *ocursor; - - if (state == NULL) - return NULL; - - result = palloc(sizeof(JsonbParseState)); - icursor = state; - ocursor = result; - for (;;) - { - ocursor->contVal = icursor->contVal; - ocursor->size = icursor->size; - ocursor->unique_keys = icursor->unique_keys; - ocursor->skip_nulls = icursor->skip_nulls; - icursor = icursor->next; - if (icursor == NULL) - break; - ocursor->next = palloc(sizeof(JsonbParseState)); - ocursor = ocursor->next; - } - ocursor->next = NULL; - - return result; -} static Datum jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) { - MemoryContext oldcontext, - aggcontext; + MemoryContext aggcontext; JsonbAggState *state; - JsonbInState elem; Datum val; JsonbInState *result; - bool single_scalar = false; - JsonbIterator *it; - Jsonb *jbelem; - JsonbValue v; - JsonbIteratorToken type; if (!AggCheckCallContext(fcinfo, &aggcontext)) { @@ -1529,13 +1496,10 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not determine input data type"))); - oldcontext = MemoryContextSwitchTo(aggcontext); - state = palloc(sizeof(JsonbAggState)); - result = palloc0(sizeof(JsonbInState)); - state->res = result; - result->res = pushJsonbValue(&result->parseState, - WJB_BEGIN_ARRAY, NULL); - MemoryContextSwitchTo(oldcontext); + state = MemoryContextAllocZero(aggcontext, sizeof(JsonbAggState)); + result = &state->pstate; + result->outcontext = aggcontext; + pushJsonbValue(result, WJB_BEGIN_ARRAY, NULL); json_categorize_type(arg_type, true, &state->val_category, &state->val_output_func); @@ -1543,78 +1507,23 @@ jsonb_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null) else { state = (JsonbAggState *) PG_GETARG_POINTER(0); - result = state->res; + result = &state->pstate; } if (absent_on_null && PG_ARGISNULL(1)) PG_RETURN_POINTER(state); - /* turn the argument into jsonb in the normal function context */ - + /* + * We run this code in the normal function context, so that we don't leak + * any cruft from datatype output functions and such into the aggcontext. + * But the "result" JsonbValue will be constructed in aggcontext, so that + * it remains available across calls. + */ val = PG_ARGISNULL(1) ? (Datum) 0 : PG_GETARG_DATUM(1); - memset(&elem, 0, sizeof(JsonbInState)); - - datum_to_jsonb_internal(val, PG_ARGISNULL(1), &elem, state->val_category, + datum_to_jsonb_internal(val, PG_ARGISNULL(1), result, state->val_category, state->val_output_func, false); - jbelem = JsonbValueToJsonb(elem.res); - - /* switch to the aggregate context for accumulation operations */ - - oldcontext = MemoryContextSwitchTo(aggcontext); - - it = JsonbIteratorInit(&jbelem->root); - - while ((type = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - switch (type) - { - case WJB_BEGIN_ARRAY: - if (v.val.array.rawScalar) - single_scalar = true; - else - result->res = pushJsonbValue(&result->parseState, - type, NULL); - break; - case WJB_END_ARRAY: - if (!single_scalar) - result->res = pushJsonbValue(&result->parseState, - type, NULL); - break; - case WJB_BEGIN_OBJECT: - case WJB_END_OBJECT: - result->res = pushJsonbValue(&result->parseState, - type, NULL); - break; - case WJB_ELEM: - case WJB_KEY: - case WJB_VALUE: - if (v.type == jbvString) - { - /* copy string values in the aggregate context */ - char *buf = palloc(v.val.string.len + 1); - - snprintf(buf, v.val.string.len + 1, "%s", v.val.string.val); - v.val.string.val = buf; - } - else if (v.type == jbvNumeric) - { - /* same for numeric */ - v.val.numeric = - DatumGetNumeric(DirectFunctionCall1(numeric_uplus, - NumericGetDatum(v.val.numeric))); - } - result->res = pushJsonbValue(&result->parseState, - type, &v); - break; - default: - elog(ERROR, "unknown jsonb iterator token type"); - } - } - - MemoryContextSwitchTo(oldcontext); - PG_RETURN_POINTER(state); } @@ -1652,19 +1561,19 @@ jsonb_agg_finalfn(PG_FUNCTION_ARGS) arg = (JsonbAggState *) PG_GETARG_POINTER(0); /* - * We need to do a shallow clone of the argument in case the final - * function is called more than once, so we avoid changing the argument. A - * shallow clone is sufficient as we aren't going to change any of the - * values, just add the final array end marker. + * The final function can be called more than once, so we must not change + * the stored JsonbValue data structure. Fortunately, the WJB_END_ARRAY + * action will only change fields in the JsonbInState struct itself, so we + * can simply invoke pushJsonbValue on a local copy of that. */ - memset(&result, 0, sizeof(JsonbInState)); + result = arg->pstate; - result.parseState = clone_parse_state(arg->res->parseState); + pushJsonbValue(&result, WJB_END_ARRAY, NULL); - result.res = pushJsonbValue(&result.parseState, - WJB_END_ARRAY, NULL); + /* We expect result.parseState == NULL after closing the array */ + Assert(result.parseState == NULL); - out = JsonbValueToJsonb(result.res); + out = JsonbValueToJsonb(result.result); PG_RETURN_POINTER(out); } @@ -1673,18 +1582,10 @@ static Datum jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null, bool unique_keys) { - MemoryContext oldcontext, - aggcontext; - JsonbInState elem; + MemoryContext aggcontext; JsonbAggState *state; Datum val; JsonbInState *result; - bool single_scalar; - JsonbIterator *it; - Jsonb *jbkey, - *jbval; - JsonbValue v; - JsonbIteratorToken type; bool skip; if (!AggCheckCallContext(fcinfo, &aggcontext)) @@ -1699,17 +1600,13 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, { Oid arg_type; - oldcontext = MemoryContextSwitchTo(aggcontext); - state = palloc(sizeof(JsonbAggState)); - result = palloc0(sizeof(JsonbInState)); - state->res = result; - result->res = pushJsonbValue(&result->parseState, - WJB_BEGIN_OBJECT, NULL); + state = MemoryContextAllocZero(aggcontext, sizeof(JsonbAggState)); + result = &state->pstate; + result->outcontext = aggcontext; + pushJsonbValue(result, WJB_BEGIN_OBJECT, NULL); result->parseState->unique_keys = unique_keys; result->parseState->skip_nulls = absent_on_null; - MemoryContextSwitchTo(oldcontext); - arg_type = get_fn_expr_argtype(fcinfo->flinfo, 1); if (arg_type == InvalidOid) @@ -1733,11 +1630,9 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, else { state = (JsonbAggState *) PG_GETARG_POINTER(0); - result = state->res; + result = &state->pstate; } - /* turn the argument into jsonb in the normal function context */ - if (PG_ARGISNULL(1)) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -1752,140 +1647,22 @@ jsonb_object_agg_transfn_worker(FunctionCallInfo fcinfo, if (skip && !unique_keys) PG_RETURN_POINTER(state); + /* + * We run this code in the normal function context, so that we don't leak + * any cruft from datatype output functions and such into the aggcontext. + * But the "result" JsonbValue will be constructed in aggcontext, so that + * it remains available across calls. + */ val = PG_GETARG_DATUM(1); - memset(&elem, 0, sizeof(JsonbInState)); - - datum_to_jsonb_internal(val, false, &elem, state->key_category, + datum_to_jsonb_internal(val, false, result, state->key_category, state->key_output_func, true); - jbkey = JsonbValueToJsonb(elem.res); - val = PG_ARGISNULL(2) ? (Datum) 0 : PG_GETARG_DATUM(2); - memset(&elem, 0, sizeof(JsonbInState)); - - datum_to_jsonb_internal(val, PG_ARGISNULL(2), &elem, state->val_category, + datum_to_jsonb_internal(val, PG_ARGISNULL(2), result, state->val_category, state->val_output_func, false); - jbval = JsonbValueToJsonb(elem.res); - - it = JsonbIteratorInit(&jbkey->root); - - /* switch to the aggregate context for accumulation operations */ - - oldcontext = MemoryContextSwitchTo(aggcontext); - - /* - * keys should be scalar, and we should have already checked for that - * above when calling datum_to_jsonb, so we only need to look for these - * things. - */ - - while ((type = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - switch (type) - { - case WJB_BEGIN_ARRAY: - if (!v.val.array.rawScalar) - elog(ERROR, "unexpected structure for key"); - break; - case WJB_ELEM: - if (v.type == jbvString) - { - /* copy string values in the aggregate context */ - char *buf = palloc(v.val.string.len + 1); - - snprintf(buf, v.val.string.len + 1, "%s", v.val.string.val); - v.val.string.val = buf; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("object keys must be strings"))); - } - result->res = pushJsonbValue(&result->parseState, - WJB_KEY, &v); - - if (skip) - { - v.type = jbvNull; - result->res = pushJsonbValue(&result->parseState, - WJB_VALUE, &v); - MemoryContextSwitchTo(oldcontext); - PG_RETURN_POINTER(state); - } - - break; - case WJB_END_ARRAY: - break; - default: - elog(ERROR, "unexpected structure for key"); - break; - } - } - - it = JsonbIteratorInit(&jbval->root); - - single_scalar = false; - - /* - * values can be anything, including structured and null, so we treat them - * as in json_agg_transfn, except that single scalars are always pushed as - * WJB_VALUE items. - */ - - while ((type = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - switch (type) - { - case WJB_BEGIN_ARRAY: - if (v.val.array.rawScalar) - single_scalar = true; - else - result->res = pushJsonbValue(&result->parseState, - type, NULL); - break; - case WJB_END_ARRAY: - if (!single_scalar) - result->res = pushJsonbValue(&result->parseState, - type, NULL); - break; - case WJB_BEGIN_OBJECT: - case WJB_END_OBJECT: - result->res = pushJsonbValue(&result->parseState, - type, NULL); - break; - case WJB_ELEM: - case WJB_KEY: - case WJB_VALUE: - if (v.type == jbvString) - { - /* copy string values in the aggregate context */ - char *buf = palloc(v.val.string.len + 1); - - snprintf(buf, v.val.string.len + 1, "%s", v.val.string.val); - v.val.string.val = buf; - } - else if (v.type == jbvNumeric) - { - /* same for numeric */ - v.val.numeric = - DatumGetNumeric(DirectFunctionCall1(numeric_uplus, - NumericGetDatum(v.val.numeric))); - } - result->res = pushJsonbValue(&result->parseState, - single_scalar ? WJB_VALUE : type, - &v); - break; - default: - elog(ERROR, "unknown jsonb iterator token type"); - } - } - - MemoryContextSwitchTo(oldcontext); - PG_RETURN_POINTER(state); } @@ -1942,20 +1719,24 @@ jsonb_object_agg_finalfn(PG_FUNCTION_ARGS) arg = (JsonbAggState *) PG_GETARG_POINTER(0); /* - * We need to do a shallow clone of the argument's res field in case the - * final function is called more than once, so we avoid changing the - * aggregate state value. A shallow clone is sufficient as we aren't - * going to change any of the values, just add the final object end - * marker. + * The final function can be called more than once, so we must not change + * the stored JsonbValue data structure. Fortunately, the WJB_END_OBJECT + * action will only destructively change fields in the JsonbInState struct + * itself, so we can simply invoke pushJsonbValue on a local copy of that. + * Note that this will run uniqueifyJsonbObject each time; that's hard to + * avoid, since duplicate pairs may have been added since the previous + * finalization. We assume uniqueifyJsonbObject can be applied repeatedly + * (with the same unique_keys/skip_nulls options) without damaging the + * data structure. */ - memset(&result, 0, sizeof(JsonbInState)); + result = arg->pstate; - result.parseState = clone_parse_state(arg->res->parseState); + pushJsonbValue(&result, WJB_END_OBJECT, NULL); - result.res = pushJsonbValue(&result.parseState, - WJB_END_OBJECT, NULL); + /* We expect result.parseState == NULL after closing the object */ + Assert(result.parseState == NULL); - out = JsonbValueToJsonb(result.res); + out = JsonbValueToJsonb(result.result); PG_RETURN_POINTER(out); } @@ -2004,8 +1785,8 @@ JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res) /* * Emit correct, translatable cast error message */ -static void -cannotCastJsonbValue(enum jbvType type, const char *sqltype) +static Datum +cannotCastJsonbValue(enum jbvType type, const char *sqltype, Node *escontext) { static const struct { @@ -2026,12 +1807,13 @@ cannotCastJsonbValue(enum jbvType type, const char *sqltype) for (i = 0; i < lengthof(messages); i++) if (messages[i].type == type) - ereport(ERROR, + ereturn(escontext, (Datum) 0, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg(messages[i].msg, sqltype))); /* should be unreachable */ elog(ERROR, "unknown jsonb type: %d", (int) type); + return (Datum) 0; } Datum @@ -2041,7 +1823,7 @@ jsonb_bool(PG_FUNCTION_ARGS) JsonbValue v; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "boolean"); + return cannotCastJsonbValue(v.type, "boolean", fcinfo->context); if (v.type == jbvNull) { @@ -2050,7 +1832,7 @@ jsonb_bool(PG_FUNCTION_ARGS) } if (v.type != jbvBool) - cannotCastJsonbValue(v.type, "boolean"); + return cannotCastJsonbValue(v.type, "boolean", fcinfo->context); PG_FREE_IF_COPY(in, 0); @@ -2065,7 +1847,7 @@ jsonb_numeric(PG_FUNCTION_ARGS) Numeric retValue; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "numeric"); + return cannotCastJsonbValue(v.type, "numeric", fcinfo->context); if (v.type == jbvNull) { @@ -2074,7 +1856,7 @@ jsonb_numeric(PG_FUNCTION_ARGS) } if (v.type != jbvNumeric) - cannotCastJsonbValue(v.type, "numeric"); + return cannotCastJsonbValue(v.type, "numeric", fcinfo->context); /* * v.val.numeric points into jsonb body, so we need to make a copy to @@ -2095,7 +1877,7 @@ jsonb_int2(PG_FUNCTION_ARGS) Datum retValue; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "smallint"); + return cannotCastJsonbValue(v.type, "smallint", fcinfo->context); if (v.type == jbvNull) { @@ -2104,7 +1886,7 @@ jsonb_int2(PG_FUNCTION_ARGS) } if (v.type != jbvNumeric) - cannotCastJsonbValue(v.type, "smallint"); + return cannotCastJsonbValue(v.type, "smallint", fcinfo->context); retValue = DirectFunctionCall1(numeric_int2, NumericGetDatum(v.val.numeric)); @@ -2122,7 +1904,7 @@ jsonb_int4(PG_FUNCTION_ARGS) Datum retValue; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "integer"); + return cannotCastJsonbValue(v.type, "integer", fcinfo->context); if (v.type == jbvNull) { @@ -2131,7 +1913,7 @@ jsonb_int4(PG_FUNCTION_ARGS) } if (v.type != jbvNumeric) - cannotCastJsonbValue(v.type, "integer"); + return cannotCastJsonbValue(v.type, "integer", fcinfo->context); retValue = DirectFunctionCall1(numeric_int4, NumericGetDatum(v.val.numeric)); @@ -2149,7 +1931,7 @@ jsonb_int8(PG_FUNCTION_ARGS) Datum retValue; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "bigint"); + return cannotCastJsonbValue(v.type, "bigint", fcinfo->context); if (v.type == jbvNull) { @@ -2158,7 +1940,7 @@ jsonb_int8(PG_FUNCTION_ARGS) } if (v.type != jbvNumeric) - cannotCastJsonbValue(v.type, "bigint"); + return cannotCastJsonbValue(v.type, "bigint", fcinfo->context); retValue = DirectFunctionCall1(numeric_int8, NumericGetDatum(v.val.numeric)); @@ -2176,7 +1958,7 @@ jsonb_float4(PG_FUNCTION_ARGS) Datum retValue; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "real"); + return cannotCastJsonbValue(v.type, "real", fcinfo->context); if (v.type == jbvNull) { @@ -2185,7 +1967,7 @@ jsonb_float4(PG_FUNCTION_ARGS) } if (v.type != jbvNumeric) - cannotCastJsonbValue(v.type, "real"); + return cannotCastJsonbValue(v.type, "real", fcinfo->context); retValue = DirectFunctionCall1(numeric_float4, NumericGetDatum(v.val.numeric)); @@ -2203,7 +1985,7 @@ jsonb_float8(PG_FUNCTION_ARGS) Datum retValue; if (!JsonbExtractScalar(&in->root, &v)) - cannotCastJsonbValue(v.type, "double precision"); + return cannotCastJsonbValue(v.type, "double precision", fcinfo->context); if (v.type == jbvNull) { @@ -2212,7 +1994,7 @@ jsonb_float8(PG_FUNCTION_ARGS) } if (v.type != jbvNumeric) - cannotCastJsonbValue(v.type, "double precision"); + return cannotCastJsonbValue(v.type, "double precision", fcinfo->context); retValue = DirectFunctionCall1(numeric_float8, NumericGetDatum(v.val.numeric)); diff --git a/src/backend/utils/adt/jsonb_gin.c b/src/backend/utils/adt/jsonb_gin.c index c1950792b5aea..d72a6441c5e96 100644 --- a/src/backend/utils/adt/jsonb_gin.c +++ b/src/backend/utils/adt/jsonb_gin.c @@ -3,7 +3,7 @@ * jsonb_gin.c * GIN support functions for jsonb * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * We provide two opclasses for jsonb indexing: jsonb_ops and jsonb_path_ops. * For their description see json.sgml and comments in jsonb.h. @@ -163,7 +163,7 @@ static void init_gin_entries(GinEntries *entries, int preallocated) { entries->allocated = preallocated; - entries->buf = preallocated ? palloc(sizeof(Datum) * preallocated) : NULL; + entries->buf = preallocated ? palloc_array(Datum, preallocated) : NULL; entries->count = 0; } @@ -178,13 +178,14 @@ add_gin_entry(GinEntries *entries, Datum entry) if (entries->allocated) { entries->allocated *= 2; - entries->buf = repalloc(entries->buf, - sizeof(Datum) * entries->allocated); + entries->buf = repalloc_array(entries->buf, + Datum, + entries->allocated); } else { entries->allocated = 8; - entries->buf = palloc(sizeof(Datum) * entries->allocated); + entries->buf = palloc_array(Datum, entries->allocated); } } @@ -307,7 +308,7 @@ jsonb_ops__add_path_item(JsonPathGinPath *path, JsonPathItem *jsp) return false; } - pentry = palloc(sizeof(*pentry)); + pentry = palloc_object(JsonPathGinPathItem); pentry->type = jsp->type; pentry->keyName = keyName; @@ -785,7 +786,7 @@ extract_jsp_query(JsonPath *jp, StrategyNumber strat, bool pathOps, if (!*nentries) return NULL; - *extra_data = palloc0(sizeof(**extra_data) * entries.count); + *extra_data = palloc0_array(Pointer, entries.count); **extra_data = (Pointer) node; return entries.buf; @@ -869,7 +870,7 @@ gin_extract_jsonb_query(PG_FUNCTION_ARGS) text *query = PG_GETARG_TEXT_PP(0); *nentries = 1; - entries = (Datum *) palloc(sizeof(Datum)); + entries = palloc_object(Datum); entries[0] = make_text_key(JGINFLAG_KEY, VARDATA_ANY(query), VARSIZE_ANY_EXHDR(query)); @@ -887,7 +888,7 @@ gin_extract_jsonb_query(PG_FUNCTION_ARGS) deconstruct_array_builtin(query, TEXTOID, &key_datums, &key_nulls, &key_count); - entries = (Datum *) palloc(sizeof(Datum) * key_count); + entries = palloc_array(Datum, key_count); for (i = 0, j = 0; i < key_count; i++) { @@ -896,8 +897,8 @@ gin_extract_jsonb_query(PG_FUNCTION_ARGS) continue; /* We rely on the array elements not being toasted */ entries[j++] = make_text_key(JGINFLAG_KEY, - VARDATA_ANY(key_datums[i]), - VARSIZE_ANY_EXHDR(key_datums[i])); + VARDATA_ANY(DatumGetPointer(key_datums[i])), + VARSIZE_ANY_EXHDR(DatumGetPointer(key_datums[i]))); } *nentries = j; @@ -930,8 +931,9 @@ gin_consistent_jsonb(PG_FUNCTION_ARGS) { bool *check = (bool *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* Jsonb *query = PG_GETARG_JSONB_P(2); */ +#ifdef NOT_USED + Jsonb *query = PG_GETARG_JSONB_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); @@ -999,8 +1001,7 @@ gin_consistent_jsonb(PG_FUNCTION_ARGS) if (nkeys > 0) { Assert(extra_data && extra_data[0]); - res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, - false) != GIN_FALSE; + res = execute_jsp_gin_node(extra_data[0], check, false) != GIN_FALSE; } } else @@ -1014,8 +1015,9 @@ gin_triconsistent_jsonb(PG_FUNCTION_ARGS) { GinTernaryValue *check = (GinTernaryValue *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* Jsonb *query = PG_GETARG_JSONB_P(2); */ +#ifdef NOT_USED + Jsonb *query = PG_GETARG_JSONB_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); GinTernaryValue res = GIN_MAYBE; @@ -1060,8 +1062,7 @@ gin_triconsistent_jsonb(PG_FUNCTION_ARGS) if (nkeys > 0) { Assert(extra_data && extra_data[0]); - res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, - true); + res = execute_jsp_gin_node(extra_data[0], check, true); /* Should always recheck the result */ if (res == GIN_TRUE) @@ -1126,7 +1127,7 @@ gin_extract_jsonb_path(PG_FUNCTION_ARGS) case WJB_BEGIN_OBJECT: /* Push a stack level for this object */ parent = stack; - stack = (PathHashStack *) palloc(sizeof(PathHashStack)); + stack = palloc_object(PathHashStack); /* * We pass forward hashes from outer nesting levels so that @@ -1221,8 +1222,9 @@ gin_consistent_jsonb_path(PG_FUNCTION_ARGS) { bool *check = (bool *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* Jsonb *query = PG_GETARG_JSONB_P(2); */ +#ifdef NOT_USED + Jsonb *query = PG_GETARG_JSONB_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); bool *recheck = (bool *) PG_GETARG_POINTER(5); @@ -1258,8 +1260,7 @@ gin_consistent_jsonb_path(PG_FUNCTION_ARGS) if (nkeys > 0) { Assert(extra_data && extra_data[0]); - res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, - false) != GIN_FALSE; + res = execute_jsp_gin_node(extra_data[0], check, false) != GIN_FALSE; } } else @@ -1273,8 +1274,9 @@ gin_triconsistent_jsonb_path(PG_FUNCTION_ARGS) { GinTernaryValue *check = (GinTernaryValue *) PG_GETARG_POINTER(0); StrategyNumber strategy = PG_GETARG_UINT16(1); - - /* Jsonb *query = PG_GETARG_JSONB_P(2); */ +#ifdef NOT_USED + Jsonb *query = PG_GETARG_JSONB_P(2); +#endif int32 nkeys = PG_GETARG_INT32(3); Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); GinTernaryValue res = GIN_MAYBE; @@ -1302,8 +1304,7 @@ gin_triconsistent_jsonb_path(PG_FUNCTION_ARGS) if (nkeys > 0) { Assert(extra_data && extra_data[0]); - res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, - true); + res = execute_jsp_gin_node(extra_data[0], check, true); /* Should always recheck the result */ if (res == GIN_TRUE) diff --git a/src/backend/utils/adt/jsonb_op.c b/src/backend/utils/adt/jsonb_op.c index fa5603f26e1d6..f52466039fdbc 100644 --- a/src/backend/utils/adt/jsonb_op.c +++ b/src/backend/utils/adt/jsonb_op.c @@ -3,7 +3,7 @@ * jsonb_op.c * Special operators for jsonb only, used by various index access methods * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -63,8 +63,8 @@ jsonb_exists_any(PG_FUNCTION_ARGS) strVal.type = jbvString; /* We rely on the array elements not being toasted */ - strVal.val.string.val = VARDATA_ANY(key_datums[i]); - strVal.val.string.len = VARSIZE_ANY_EXHDR(key_datums[i]); + strVal.val.string.val = VARDATA_ANY(DatumGetPointer(key_datums[i])); + strVal.val.string.len = VARSIZE_ANY_EXHDR(DatumGetPointer(key_datums[i])); if (findJsonbValueFromContainer(&jb->root, JB_FOBJECT | JB_FARRAY, @@ -96,8 +96,8 @@ jsonb_exists_all(PG_FUNCTION_ARGS) strVal.type = jbvString; /* We rely on the array elements not being toasted */ - strVal.val.string.val = VARDATA_ANY(key_datums[i]); - strVal.val.string.len = VARSIZE_ANY_EXHDR(key_datums[i]); + strVal.val.string.val = VARDATA_ANY(DatumGetPointer(key_datums[i])); + strVal.val.string.len = VARSIZE_ANY_EXHDR(DatumGetPointer(key_datums[i])); if (findJsonbValueFromContainer(&jb->root, JB_FOBJECT | JB_FARRAY, diff --git a/src/backend/utils/adt/jsonb_util.c b/src/backend/utils/adt/jsonb_util.c index c8b6c15e05975..91fb9ea09bf4f 100644 --- a/src/backend/utils/adt/jsonb_util.c +++ b/src/backend/utils/adt/jsonb_util.c @@ -3,7 +3,7 @@ * jsonb_util.c * converting between Jsonb and JsonbValues, and iterating. * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -14,10 +14,13 @@ #include "postgres.h" #include "catalog/pg_collation.h" +#include "catalog/pg_type.h" #include "common/hashfn.h" #include "miscadmin.h" #include "port/pg_bitutils.h" +#include "utils/date.h" #include "utils/datetime.h" +#include "utils/datum.h" #include "utils/fmgrprotos.h" #include "utils/json.h" #include "utils/jsonb.h" @@ -54,19 +57,20 @@ static short padBufferToInt(StringInfo buffer); static JsonbIterator *iteratorFromContainer(JsonbContainer *container, JsonbIterator *parent); static JsonbIterator *freeAndGetParent(JsonbIterator *it); -static JsonbParseState *pushState(JsonbParseState **pstate); -static void appendKey(JsonbParseState *pstate, JsonbValue *string); -static void appendValue(JsonbParseState *pstate, JsonbValue *scalarVal); -static void appendElement(JsonbParseState *pstate, JsonbValue *scalarVal); +static JsonbParseState *pushState(JsonbInState *pstate); +static void appendKey(JsonbInState *pstate, JsonbValue *string, bool needCopy); +static void appendValue(JsonbInState *pstate, JsonbValue *scalarVal, bool needCopy); +static void appendElement(JsonbInState *pstate, JsonbValue *scalarVal, bool needCopy); +static void copyScalarSubstructure(JsonbValue *v, MemoryContext outcontext); static int lengthCompareJsonbStringValue(const void *a, const void *b); static int lengthCompareJsonbString(const char *val1, int len1, const char *val2, int len2); static int lengthCompareJsonbPair(const void *a, const void *b, void *binequal); static void uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls); -static JsonbValue *pushJsonbValueScalar(JsonbParseState **pstate, - JsonbIteratorToken seq, - JsonbValue *scalarVal); +static void pushJsonbValueScalar(JsonbInState *pstate, + JsonbIteratorToken seq, + JsonbValue *scalarVal); void JsonbToJsonbValue(Jsonb *jsonb, JsonbValue *val) @@ -95,9 +99,8 @@ JsonbValueToJsonb(JsonbValue *val) if (IsAJsonbScalar(val)) { - /* Scalar value */ - JsonbParseState *pstate = NULL; - JsonbValue *res; + /* Scalar value, so wrap it in an array */ + JsonbInState pstate = {0}; JsonbValue scalarArray; scalarArray.type = jbvArray; @@ -106,9 +109,9 @@ JsonbValueToJsonb(JsonbValue *val) pushJsonbValue(&pstate, WJB_BEGIN_ARRAY, &scalarArray); pushJsonbValue(&pstate, WJB_ELEM, val); - res = pushJsonbValue(&pstate, WJB_END_ARRAY, NULL); + pushJsonbValue(&pstate, WJB_END_ARRAY, NULL); - out = convertToJsonb(res); + out = convertToJsonb(pstate.result); } else if (val->type == jbvObject || val->type == jbvArray) { @@ -277,22 +280,16 @@ compareJsonbContainers(JsonbContainer *a, JsonbContainer *b) else { /* - * It's safe to assume that the types differed, and that the va - * and vb values passed were set. - * - * If the two values were of the same container type, then there'd - * have been a chance to observe the variation in the number of - * elements/pairs (when processing WJB_BEGIN_OBJECT, say). They're - * either two heterogeneously-typed containers, or a container and - * some scalar type. - * - * We don't have to consider the WJB_END_ARRAY and WJB_END_OBJECT - * cases here, because we would have seen the corresponding - * WJB_BEGIN_ARRAY and WJB_BEGIN_OBJECT tokens first, and - * concluded that they don't match. + * It's not possible for one iterator to report end of array or + * object while the other one reports something else, because we + * would have detected a length mismatch when we processed the + * container-start tokens above. Likewise we can't see WJB_DONE + * from one but not the other. So we have two different-type + * containers, or a container and some scalar type, or two + * different scalar types. Sort on the basis of the type code. */ - Assert(ra != WJB_END_ARRAY && ra != WJB_END_OBJECT); - Assert(rb != WJB_END_ARRAY && rb != WJB_END_OBJECT); + Assert(ra != WJB_DONE && ra != WJB_END_ARRAY && ra != WJB_END_OBJECT); + Assert(rb != WJB_DONE && rb != WJB_END_ARRAY && rb != WJB_END_OBJECT); Assert(va.type != vb.type); Assert(va.type != jbvBinary); @@ -362,7 +359,7 @@ findJsonbValueFromContainer(JsonbContainer *container, uint32 flags, if ((flags & JB_FARRAY) && JsonContainerIsArray(container)) { - JsonbValue *result = palloc(sizeof(JsonbValue)); + JsonbValue *result = palloc_object(JsonbValue); char *base_addr = (char *) (children + count); uint32 offset = 0; int i; @@ -445,7 +442,7 @@ getKeyJsonValueFromContainer(JsonbContainer *container, int index = stopMiddle + count; if (!res) - res = palloc(sizeof(JsonbValue)); + res = palloc_object(JsonbValue); fillJsonbValue(container, index, baseAddr, getJsonbOffset(container, index), @@ -487,7 +484,7 @@ getIthJsonbValueFromContainer(JsonbContainer *container, uint32 i) if (i >= nelements) return NULL; - result = palloc(sizeof(JsonbValue)); + result = palloc_object(JsonbValue); fillJsonbValue(container, i, base_addr, getJsonbOffset(container, i), @@ -553,13 +550,23 @@ fillJsonbValue(JsonbContainer *container, int index, } /* - * Push JsonbValue into JsonbParseState. + * Push JsonbValue into JsonbInState. * - * Used when parsing JSON tokens to form Jsonb, or when converting an in-memory - * JsonbValue to a Jsonb. + * Used, for example, when parsing JSON input. * - * Initial state of *JsonbParseState is NULL, since it'll be allocated here - * originally (caller will get JsonbParseState back by reference). + * *pstate is typically initialized to all-zeroes, except that the caller + * may provide outcontext and/or escontext. (escontext is ignored by this + * function and its subroutines, however.) + * + * "seq" tells what is being pushed (start/end of array or object, key, + * value, etc). WJB_DONE is not used here, but the other values of + * JsonbIteratorToken are. We assume the caller passes a valid sequence + * of values. + * + * The passed "jbval" is typically transient storage, such as a local variable. + * We will copy it into the outcontext (CurrentMemoryContext by default). + * If outcontext isn't NULL, we will also make copies of any pass-by-reference + * scalar values. * * Only sequential tokens pertaining to non-container types should pass a * JsonbValue. There is one exception -- WJB_BEGIN_ARRAY callers may pass a @@ -568,18 +575,32 @@ fillJsonbValue(JsonbContainer *container, int index, * * Values of type jbvBinary, which are rolled up arrays and objects, * are unpacked before being added to the result. + * + * At the end of construction of a JsonbValue, pstate->result will reference + * the top-level JsonbValue object. */ -JsonbValue * -pushJsonbValue(JsonbParseState **pstate, JsonbIteratorToken seq, +void +pushJsonbValue(JsonbInState *pstate, JsonbIteratorToken seq, JsonbValue *jbval) { JsonbIterator *it; - JsonbValue *res = NULL; JsonbValue v; JsonbIteratorToken tok; int i; - if (jbval && (seq == WJB_ELEM || seq == WJB_VALUE) && jbval->type == jbvObject) + /* + * pushJsonbValueScalar handles all cases not involving pushing a + * container object as an ELEM or VALUE. + */ + if (!jbval || IsAJsonbScalar(jbval) || + (seq != WJB_ELEM && seq != WJB_VALUE)) + { + pushJsonbValueScalar(pstate, seq, jbval); + return; + } + + /* If an object or array is pushed, recursively push its contents */ + if (jbval->type == jbvObject) { pushJsonbValue(pstate, WJB_BEGIN_OBJECT, NULL); for (i = 0; i < jbval->val.object.nPairs; i++) @@ -587,32 +608,29 @@ pushJsonbValue(JsonbParseState **pstate, JsonbIteratorToken seq, pushJsonbValue(pstate, WJB_KEY, &jbval->val.object.pairs[i].key); pushJsonbValue(pstate, WJB_VALUE, &jbval->val.object.pairs[i].value); } - - return pushJsonbValue(pstate, WJB_END_OBJECT, NULL); + pushJsonbValue(pstate, WJB_END_OBJECT, NULL); + return; } - if (jbval && (seq == WJB_ELEM || seq == WJB_VALUE) && jbval->type == jbvArray) + if (jbval->type == jbvArray) { pushJsonbValue(pstate, WJB_BEGIN_ARRAY, NULL); for (i = 0; i < jbval->val.array.nElems; i++) { pushJsonbValue(pstate, WJB_ELEM, &jbval->val.array.elems[i]); } - - return pushJsonbValue(pstate, WJB_END_ARRAY, NULL); + pushJsonbValue(pstate, WJB_END_ARRAY, NULL); + return; } - if (!jbval || (seq != WJB_ELEM && seq != WJB_VALUE) || - jbval->type != jbvBinary) - { - /* drop through */ - return pushJsonbValueScalar(pstate, seq, jbval); - } + /* Else it must be a jbvBinary value; push its contents */ + Assert(jbval->type == jbvBinary); - /* unpack the binary and add each piece to the pstate */ it = JsonbIteratorInit(jbval->val.binary.data); - if ((jbval->val.binary.data->header & JB_FSCALAR) && *pstate) + /* ... with a special case for pushing a raw scalar */ + if ((jbval->val.binary.data->header & JB_FSCALAR) && + pstate->parseState != NULL) { tok = JsonbIteratorNext(&it, &v, true); Assert(tok == WJB_BEGIN_ARRAY); @@ -621,197 +639,290 @@ pushJsonbValue(JsonbParseState **pstate, JsonbIteratorToken seq, tok = JsonbIteratorNext(&it, &v, true); Assert(tok == WJB_ELEM); - res = pushJsonbValueScalar(pstate, seq, &v); + pushJsonbValueScalar(pstate, seq, &v); tok = JsonbIteratorNext(&it, &v, true); Assert(tok == WJB_END_ARRAY); Assert(it == NULL); - return res; + return; } while ((tok = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - res = pushJsonbValueScalar(pstate, tok, - tok < WJB_BEGIN_ARRAY || - (tok == WJB_BEGIN_ARRAY && - v.val.array.rawScalar) ? &v : NULL); - - return res; + pushJsonbValueScalar(pstate, tok, + tok < WJB_BEGIN_ARRAY || + (tok == WJB_BEGIN_ARRAY && + v.val.array.rawScalar) ? &v : NULL); } /* * Do the actual pushing, with only scalar or pseudo-scalar-array values * accepted. */ -static JsonbValue * -pushJsonbValueScalar(JsonbParseState **pstate, JsonbIteratorToken seq, +static void +pushJsonbValueScalar(JsonbInState *pstate, JsonbIteratorToken seq, JsonbValue *scalarVal) { - JsonbValue *result = NULL; + JsonbParseState *ppstate; + JsonbValue *val; + MemoryContext outcontext; switch (seq) { case WJB_BEGIN_ARRAY: Assert(!scalarVal || scalarVal->val.array.rawScalar); - *pstate = pushState(pstate); - result = &(*pstate)->contVal; - (*pstate)->contVal.type = jbvArray; - (*pstate)->contVal.val.array.nElems = 0; - (*pstate)->contVal.val.array.rawScalar = (scalarVal && - scalarVal->val.array.rawScalar); + ppstate = pushState(pstate); + val = &ppstate->contVal; + val->type = jbvArray; + val->val.array.nElems = 0; + val->val.array.rawScalar = (scalarVal && + scalarVal->val.array.rawScalar); if (scalarVal && scalarVal->val.array.nElems > 0) { /* Assume that this array is still really a scalar */ Assert(scalarVal->type == jbvArray); - (*pstate)->size = scalarVal->val.array.nElems; + ppstate->size = scalarVal->val.array.nElems; } else { - (*pstate)->size = 4; + ppstate->size = 4; /* initial guess at array size */ } - (*pstate)->contVal.val.array.elems = palloc(sizeof(JsonbValue) * - (*pstate)->size); + outcontext = pstate->outcontext ? pstate->outcontext : CurrentMemoryContext; + val->val.array.elems = MemoryContextAlloc(outcontext, + sizeof(JsonbValue) * + ppstate->size); break; case WJB_BEGIN_OBJECT: Assert(!scalarVal); - *pstate = pushState(pstate); - result = &(*pstate)->contVal; - (*pstate)->contVal.type = jbvObject; - (*pstate)->contVal.val.object.nPairs = 0; - (*pstate)->size = 4; - (*pstate)->contVal.val.object.pairs = palloc(sizeof(JsonbPair) * - (*pstate)->size); + ppstate = pushState(pstate); + val = &ppstate->contVal; + val->type = jbvObject; + val->val.object.nPairs = 0; + ppstate->size = 4; /* initial guess at object size */ + outcontext = pstate->outcontext ? pstate->outcontext : CurrentMemoryContext; + val->val.object.pairs = MemoryContextAlloc(outcontext, + sizeof(JsonbPair) * + ppstate->size); break; case WJB_KEY: Assert(scalarVal->type == jbvString); - appendKey(*pstate, scalarVal); + appendKey(pstate, scalarVal, true); break; case WJB_VALUE: Assert(IsAJsonbScalar(scalarVal)); - appendValue(*pstate, scalarVal); + appendValue(pstate, scalarVal, true); break; case WJB_ELEM: Assert(IsAJsonbScalar(scalarVal)); - appendElement(*pstate, scalarVal); + appendElement(pstate, scalarVal, true); break; case WJB_END_OBJECT: - uniqueifyJsonbObject(&(*pstate)->contVal, - (*pstate)->unique_keys, - (*pstate)->skip_nulls); - /* fall through! */ + ppstate = pstate->parseState; + uniqueifyJsonbObject(&ppstate->contVal, + ppstate->unique_keys, + ppstate->skip_nulls); + pg_fallthrough; case WJB_END_ARRAY: /* Steps here common to WJB_END_OBJECT case */ Assert(!scalarVal); - result = &(*pstate)->contVal; + ppstate = pstate->parseState; + val = &ppstate->contVal; /* * Pop stack and push current array/object as value in parent - * array/object + * array/object, or return it as the final result. We don't need + * to re-copy any scalars that are in the data structure. */ - *pstate = (*pstate)->next; - if (*pstate) + pstate->parseState = ppstate = ppstate->next; + if (ppstate) { - switch ((*pstate)->contVal.type) + switch (ppstate->contVal.type) { case jbvArray: - appendElement(*pstate, result); + appendElement(pstate, val, false); break; case jbvObject: - appendValue(*pstate, result); + appendValue(pstate, val, false); break; default: elog(ERROR, "invalid jsonb container type"); } } + else + pstate->result = val; break; default: elog(ERROR, "unrecognized jsonb sequential processing token"); } - - return result; } /* - * pushJsonbValue() worker: Iteration-like forming of Jsonb + * Push a new JsonbParseState onto the JsonbInState's stack + * + * As a notational convenience, the new state's address is returned. + * The caller must initialize the new state's contVal and size fields. */ static JsonbParseState * -pushState(JsonbParseState **pstate) +pushState(JsonbInState *pstate) { - JsonbParseState *ns = palloc(sizeof(JsonbParseState)); + MemoryContext outcontext = pstate->outcontext ? pstate->outcontext : CurrentMemoryContext; + JsonbParseState *ns = MemoryContextAlloc(outcontext, + sizeof(JsonbParseState)); - ns->next = *pstate; + ns->next = pstate->parseState; + /* This module never changes these fields, but callers can: */ ns->unique_keys = false; ns->skip_nulls = false; + pstate->parseState = ns; return ns; } /* - * pushJsonbValue() worker: Append a pair key to state when generating a Jsonb + * pushJsonbValue() worker: Append a pair key to pstate */ static void -appendKey(JsonbParseState *pstate, JsonbValue *string) +appendKey(JsonbInState *pstate, JsonbValue *string, bool needCopy) { - JsonbValue *object = &pstate->contVal; + JsonbParseState *ppstate = pstate->parseState; + JsonbValue *object = &ppstate->contVal; + JsonbPair *pair; Assert(object->type == jbvObject); Assert(string->type == jbvString); - if (object->val.object.nPairs >= JSONB_MAX_PAIRS) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("number of jsonb object pairs exceeds the maximum allowed (%zu)", - JSONB_MAX_PAIRS))); - - if (object->val.object.nPairs >= pstate->size) + if (object->val.object.nPairs >= ppstate->size) { - pstate->size *= 2; + if (unlikely(object->val.object.nPairs >= JSONB_MAX_PAIRS)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("number of jsonb object pairs exceeds the maximum allowed (%zu)", + JSONB_MAX_PAIRS))); + ppstate->size = Min(ppstate->size * 2, JSONB_MAX_PAIRS); object->val.object.pairs = repalloc(object->val.object.pairs, - sizeof(JsonbPair) * pstate->size); + sizeof(JsonbPair) * ppstate->size); } - object->val.object.pairs[object->val.object.nPairs].key = *string; - object->val.object.pairs[object->val.object.nPairs].order = object->val.object.nPairs; + pair = &object->val.object.pairs[object->val.object.nPairs]; + pair->key = *string; + pair->order = object->val.object.nPairs; + + if (needCopy) + copyScalarSubstructure(&pair->key, pstate->outcontext); } /* - * pushJsonbValue() worker: Append a pair value to state when generating a - * Jsonb + * pushJsonbValue() worker: Append a pair value to pstate */ static void -appendValue(JsonbParseState *pstate, JsonbValue *scalarVal) +appendValue(JsonbInState *pstate, JsonbValue *scalarVal, bool needCopy) { - JsonbValue *object = &pstate->contVal; + JsonbValue *object = &pstate->parseState->contVal; + JsonbPair *pair; Assert(object->type == jbvObject); - object->val.object.pairs[object->val.object.nPairs++].value = *scalarVal; + pair = &object->val.object.pairs[object->val.object.nPairs]; + pair->value = *scalarVal; + object->val.object.nPairs++; + + if (needCopy) + copyScalarSubstructure(&pair->value, pstate->outcontext); } /* - * pushJsonbValue() worker: Append an element to state when generating a Jsonb + * pushJsonbValue() worker: Append an array element to pstate */ static void -appendElement(JsonbParseState *pstate, JsonbValue *scalarVal) +appendElement(JsonbInState *pstate, JsonbValue *scalarVal, bool needCopy) { - JsonbValue *array = &pstate->contVal; + JsonbParseState *ppstate = pstate->parseState; + JsonbValue *array = &ppstate->contVal; + JsonbValue *elem; Assert(array->type == jbvArray); - if (array->val.array.nElems >= JSONB_MAX_ELEMS) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("number of jsonb array elements exceeds the maximum allowed (%zu)", - JSONB_MAX_ELEMS))); - - if (array->val.array.nElems >= pstate->size) + if (array->val.array.nElems >= ppstate->size) { - pstate->size *= 2; + if (unlikely(array->val.array.nElems >= JSONB_MAX_ELEMS)) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("number of jsonb array elements exceeds the maximum allowed (%zu)", + JSONB_MAX_ELEMS))); + ppstate->size = Min(ppstate->size * 2, JSONB_MAX_ELEMS); array->val.array.elems = repalloc(array->val.array.elems, - sizeof(JsonbValue) * pstate->size); + sizeof(JsonbValue) * ppstate->size); } - array->val.array.elems[array->val.array.nElems++] = *scalarVal; + elem = &array->val.array.elems[array->val.array.nElems]; + *elem = *scalarVal; + array->val.array.nElems++; + + if (needCopy) + copyScalarSubstructure(elem, pstate->outcontext); +} + +/* + * Copy any infrastructure of a scalar JsonbValue into the outcontext, + * adjusting the pointer(s) in *v. + * + * We need not deal with containers here, as the routines above ensure + * that they are built fresh. + */ +static void +copyScalarSubstructure(JsonbValue *v, MemoryContext outcontext) +{ + MemoryContext oldcontext; + + /* Nothing to do if caller did not specify an outcontext */ + if (outcontext == NULL) + return; + switch (v->type) + { + case jbvNull: + case jbvBool: + /* pass-by-value, nothing to do */ + break; + case jbvString: + { + char *buf = MemoryContextAlloc(outcontext, + v->val.string.len); + + memcpy(buf, v->val.string.val, v->val.string.len); + v->val.string.val = buf; + } + break; + case jbvNumeric: + oldcontext = MemoryContextSwitchTo(outcontext); + v->val.numeric = + DatumGetNumeric(datumCopy(NumericGetDatum(v->val.numeric), + false, -1)); + MemoryContextSwitchTo(oldcontext); + break; + case jbvDatetime: + switch (v->val.datetime.typid) + { + case DATEOID: + case TIMEOID: + case TIMESTAMPOID: + case TIMESTAMPTZOID: + /* pass-by-value, nothing to do */ + break; + case TIMETZOID: + /* pass-by-reference */ + oldcontext = MemoryContextSwitchTo(outcontext); + v->val.datetime.value = datumCopy(v->val.datetime.value, + false, TIMETZ_TYPLEN); + MemoryContextSwitchTo(oldcontext); + break; + default: + elog(ERROR, "unexpected jsonb datetime type oid %u", + v->val.datetime.typid); + } + break; + default: + elog(ERROR, "invalid jsonb scalar type"); + } } /* @@ -852,15 +963,20 @@ JsonbIteratorInit(JsonbContainer *container) * It is our job to expand the jbvBinary representation without bothering them * with it. However, clients should not take it upon themselves to touch array * or Object element/pair buffers, since their element/pair pointers are - * garbage. Also, *val will not be set when returning WJB_END_ARRAY or - * WJB_END_OBJECT, on the assumption that it's only useful to access values - * when recursing in. + * garbage. + * + * *val is not meaningful when the result is WJB_DONE, WJB_END_ARRAY or + * WJB_END_OBJECT. However, we set val->type = jbvNull in those cases, + * so that callers may assume that val->type is always well-defined. */ JsonbIteratorToken JsonbIteratorNext(JsonbIterator **it, JsonbValue *val, bool skipNested) { if (*it == NULL) + { + val->type = jbvNull; return WJB_DONE; + } /* * When stepping into a nested container, we jump back here to start @@ -898,6 +1014,7 @@ JsonbIteratorNext(JsonbIterator **it, JsonbValue *val, bool skipNested) * nesting). */ *it = freeAndGetParent(*it); + val->type = jbvNull; return WJB_END_ARRAY; } @@ -951,6 +1068,7 @@ JsonbIteratorNext(JsonbIterator **it, JsonbValue *val, bool skipNested) * of nesting). */ *it = freeAndGetParent(*it); + val->type = jbvNull; return WJB_END_OBJECT; } else @@ -995,8 +1113,10 @@ JsonbIteratorNext(JsonbIterator **it, JsonbValue *val, bool skipNested) return WJB_VALUE; } - elog(ERROR, "invalid iterator state"); - return -1; + elog(ERROR, "invalid jsonb iterator state"); + /* satisfy compilers that don't know that elog(ERROR) doesn't return */ + val->type = jbvNull; + return WJB_DONE; } /* @@ -1007,7 +1127,7 @@ iteratorFromContainer(JsonbContainer *container, JsonbIterator *parent) { JsonbIterator *it; - it = palloc0(sizeof(JsonbIterator)); + it = palloc0_object(JsonbIterator); it->container = container; it->parent = parent; it->nElems = JsonContainerSize(container); @@ -1253,7 +1373,7 @@ JsonbDeepContains(JsonbIterator **val, JsonbIterator **mContained) uint32 j = 0; /* Make room for all possible values */ - lhsConts = palloc(sizeof(JsonbValue) * nLhsElems); + lhsConts = palloc_array(JsonbValue, nLhsElems); for (i = 0; i < nLhsElems; i++) { @@ -1949,12 +2069,14 @@ lengthCompareJsonbPair(const void *a, const void *b, void *binequal) static void uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls) { + JsonbPair *pairs = object->val.object.pairs; + int nPairs = object->val.object.nPairs; bool hasNonUniq = false; Assert(object->type == jbvObject); - if (object->val.object.nPairs > 1) - qsort_arg(object->val.object.pairs, object->val.object.nPairs, sizeof(JsonbPair), + if (nPairs > 1) + qsort_arg(pairs, nPairs, sizeof(JsonbPair), lengthCompareJsonbPair, &hasNonUniq); if (hasNonUniq && unique_keys) @@ -1964,36 +2086,25 @@ uniqueifyJsonbObject(JsonbValue *object, bool unique_keys, bool skip_nulls) if (hasNonUniq || skip_nulls) { - JsonbPair *ptr, - *res; + int nNewPairs = 0; - while (skip_nulls && object->val.object.nPairs > 0 && - object->val.object.pairs->value.type == jbvNull) + for (int i = 0; i < nPairs; i++) { - /* If skip_nulls is true, remove leading items with null */ - object->val.object.pairs++; - object->val.object.nPairs--; - } - - if (object->val.object.nPairs > 0) - { - ptr = object->val.object.pairs + 1; - res = object->val.object.pairs; - - while (ptr - object->val.object.pairs < object->val.object.nPairs) - { - /* Avoid copying over duplicate or null */ - if (lengthCompareJsonbStringValue(ptr, res) != 0 && - (!skip_nulls || ptr->value.type != jbvNull)) - { - res++; - if (ptr != res) - memcpy(res, ptr, sizeof(JsonbPair)); - } - ptr++; - } + JsonbPair *ptr = pairs + i; - object->val.object.nPairs = res + 1 - object->val.object.pairs; + /* Skip duplicate keys */ + if (nNewPairs > 0 && + lengthCompareJsonbStringValue(&pairs[nNewPairs - 1].key, + &ptr->key) == 0) + continue; + /* Skip null values, if told to */ + if (skip_nulls && ptr->value.type == jbvNull) + continue; + /* Emit this pair, but avoid no-op copy */ + if (i > nNewPairs) + pairs[nNewPairs] = *ptr; + nNewPairs++; } + object->val.object.nPairs = nNewPairs; } } diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c index de64d49851251..f2745b29a3ff3 100644 --- a/src/backend/utils/adt/jsonbsubs.c +++ b/src/backend/utils/adt/jsonbsubs.c @@ -3,7 +3,7 @@ * jsonbsubs.c * Subscripting support functions for jsonb. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,6 +14,7 @@ */ #include "postgres.h" +#include "catalog/pg_type_d.h" #include "executor/execExpr.h" #include "nodes/nodeFuncs.h" #include "nodes/subscripting.h" @@ -51,7 +52,7 @@ jsonb_subscript_transform(SubscriptingRef *sbsref, /* * Transform and convert the subscript expressions. Jsonb subscripting - * does not support slices, look only and the upper index. + * does not support slices, look only at the upper index. */ foreach(idx, indirection) { @@ -152,7 +153,7 @@ jsonb_subscript_transform(SubscriptingRef *sbsref, upperIndexpr = lappend(upperIndexpr, subExpr); } - /* store the transformed lists into the SubscriptRef node */ + /* store the transformed lists into the SubscriptingRef node */ sbsref->refupperindexpr = upperIndexpr; sbsref->reflowerindexpr = NIL; diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c index bcb1720b6cde2..97cc3d6034092 100644 --- a/src/backend/utils/adt/jsonfuncs.c +++ b/src/backend/utils/adt/jsonfuncs.c @@ -3,7 +3,7 @@ * jsonfuncs.c * Functions to process JSON data types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -17,6 +17,8 @@ #include #include "access/htup_details.h" +#include "access/tupdesc.h" +#include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "common/int.h" #include "common/jsonapi.h" @@ -38,6 +40,7 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/typcache.h" /* Operations available for setPath */ @@ -475,18 +478,18 @@ static Datum populate_domain(DomainIOData *io, Oid typid, const char *colname, Node *escontext, bool omit_quotes); /* functions supporting jsonb_delete, jsonb_set and jsonb_concat */ -static JsonbValue *IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, - JsonbParseState **state); -static JsonbValue *setPath(JsonbIterator **it, Datum *path_elems, - bool *path_nulls, int path_len, - JsonbParseState **st, int level, JsonbValue *newval, - int op_type); -static void setPathObject(JsonbIterator **it, Datum *path_elems, - bool *path_nulls, int path_len, JsonbParseState **st, +static void IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, + JsonbInState *state); +static void setPath(JsonbIterator **it, const Datum *path_elems, + const bool *path_nulls, int path_len, + JsonbInState *st, int level, JsonbValue *newval, + int op_type); +static void setPathObject(JsonbIterator **it, const Datum *path_elems, + const bool *path_nulls, int path_len, JsonbInState *st, int level, JsonbValue *newval, uint32 npairs, int op_type); -static void setPathArray(JsonbIterator **it, Datum *path_elems, - bool *path_nulls, int path_len, JsonbParseState **st, +static void setPathArray(JsonbIterator **it, const Datum *path_elems, + const bool *path_nulls, int path_len, JsonbInState *st, int level, JsonbValue *newval, uint32 nelems, int op_type); @@ -593,12 +596,12 @@ jsonb_object_keys(PG_FUNCTION_ARGS) funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - state = palloc(sizeof(OkeysState)); + state = palloc_object(OkeysState); state->result_size = JB_ROOT_COUNT(jb); state->result_count = 0; state->sent_count = 0; - state->result = palloc(state->result_size * sizeof(char *)); + state->result = palloc_array(char *, state->result_size); it = JsonbIteratorInit(&jb->root); @@ -695,7 +698,7 @@ report_json_context(JsonLexContext *lex) { /* Advance to next multibyte character */ if (IS_HIGHBIT_SET(*context_start)) - context_start += pg_mblen(context_start); + context_start += pg_mblen_range(context_start, context_end); else context_start++; } @@ -744,14 +747,14 @@ json_object_keys(PG_FUNCTION_ARGS) funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - state = palloc(sizeof(OkeysState)); - sem = palloc0(sizeof(JsonSemAction)); + state = palloc_object(OkeysState); + sem = palloc0_object(JsonSemAction); state->lex = makeJsonLexContext(&lex, json, true); state->result_size = 256; state->result_count = 0; state->sent_count = 0; - state->result = palloc(256 * sizeof(char *)); + state->result = palloc_array(char *, 256); sem->semstate = state; sem->array_start = okeys_array_start; @@ -1045,8 +1048,8 @@ get_path_all(FunctionCallInfo fcinfo, bool as_text) deconstruct_array_builtin(path, TEXTOID, &pathtext, &pathnulls, &npath); - tpath = palloc(npath * sizeof(char *)); - ipath = palloc(npath * sizeof(int)); + tpath = palloc_array(char *, npath); + ipath = palloc_array(int, npath); for (i = 0; i < npath; i++) { @@ -1106,8 +1109,8 @@ get_worker(text *json, int npath, bool normalize_results) { - JsonSemAction *sem = palloc0(sizeof(JsonSemAction)); - GetState *state = palloc0(sizeof(GetState)); + JsonSemAction *sem = palloc0_object(JsonSemAction); + GetState *state = palloc0_object(GetState); Assert(npath >= 0); @@ -1118,8 +1121,8 @@ get_worker(text *json, state->npath = npath; state->path_names = tpath; state->path_indexes = ipath; - state->pathok = palloc0(sizeof(bool) * npath); - state->array_cur_index = palloc(sizeof(int) * npath); + state->pathok = palloc0_array(bool, npath); + state->array_cur_index = palloc_array(int, npath); if (npath > 0) state->pathok[0] = true; @@ -1528,7 +1531,7 @@ get_jsonb_path_all(FunctionCallInfo fcinfo, bool as_text) } Datum -jsonb_get_element(Jsonb *jb, Datum *path, int npath, bool *isnull, bool as_text) +jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull, bool as_text) { JsonbContainer *container = &jb->root; JsonbValue *jbvp = NULL; @@ -1676,30 +1679,29 @@ jsonb_get_element(Jsonb *jb, Datum *path, int npath, bool *isnull, bool as_text) } Datum -jsonb_set_element(Jsonb *jb, Datum *path, int path_len, +jsonb_set_element(Jsonb *jb, const Datum *path, int path_len, JsonbValue *newval) { - JsonbValue *res; - JsonbParseState *state = NULL; + JsonbInState state = {0}; JsonbIterator *it; - bool *path_nulls = palloc0(path_len * sizeof(bool)); + bool *path_nulls = palloc0_array(bool, path_len); if (newval->type == jbvArray && newval->val.array.rawScalar) *newval = newval->val.array.elems[0]; it = JsonbIteratorInit(&jb->root); - res = setPath(&it, path, path_nulls, path_len, &state, 0, newval, - JB_PATH_CREATE | JB_PATH_FILL_GAPS | - JB_PATH_CONSISTENT_POSITION); + setPath(&it, path, path_nulls, path_len, &state, 0, newval, + JB_PATH_CREATE | JB_PATH_FILL_GAPS | + JB_PATH_CONSISTENT_POSITION); pfree(path_nulls); - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(state.result)); } static void -push_null_elements(JsonbParseState **ps, int num) +push_null_elements(JsonbInState *ps, int num) { JsonbValue null; @@ -1718,8 +1720,8 @@ push_null_elements(JsonbParseState **ps, int num) * Caller is responsible to make sure such path does not exist yet. */ static void -push_path(JsonbParseState **st, int level, Datum *path_elems, - bool *path_nulls, int path_len, JsonbValue *newval) +push_path(JsonbInState *st, int level, const Datum *path_elems, + const bool *path_nulls, int path_len, JsonbValue *newval) { /* * tpath contains expected type of an empty jsonb created at each level @@ -1727,7 +1729,7 @@ push_path(JsonbParseState **st, int level, Datum *path_elems, * it contains only information about path slice from level to the end, * the access index must be normalized by level. */ - enum jbvType *tpath = palloc0((path_len - level) * sizeof(enum jbvType)); + enum jbvType *tpath = palloc0_array(enum jbvType, path_len - level); JsonbValue newkey; /* @@ -1758,15 +1760,15 @@ push_path(JsonbParseState **st, int level, Datum *path_elems, newkey.val.string.val = c; newkey.val.string.len = strlen(c); - (void) pushJsonbValue(st, WJB_BEGIN_OBJECT, NULL); - (void) pushJsonbValue(st, WJB_KEY, &newkey); + pushJsonbValue(st, WJB_BEGIN_OBJECT, NULL); + pushJsonbValue(st, WJB_KEY, &newkey); tpath[i - level] = jbvObject; } else { /* integer, an array is expected */ - (void) pushJsonbValue(st, WJB_BEGIN_ARRAY, NULL); + pushJsonbValue(st, WJB_BEGIN_ARRAY, NULL); push_null_elements(st, lindex); @@ -1776,11 +1778,9 @@ push_path(JsonbParseState **st, int level, Datum *path_elems, /* Insert an actual value for either an object or array */ if (tpath[(path_len - level) - 1] == jbvArray) - { - (void) pushJsonbValue(st, WJB_ELEM, newval); - } + pushJsonbValue(st, WJB_ELEM, newval); else - (void) pushJsonbValue(st, WJB_VALUE, newval); + pushJsonbValue(st, WJB_VALUE, newval); /* * Close everything up to the last but one level. The last one will be @@ -1792,9 +1792,9 @@ push_path(JsonbParseState **st, int level, Datum *path_elems, break; if (tpath[i - level] == jbvObject) - (void) pushJsonbValue(st, WJB_END_OBJECT, NULL); + pushJsonbValue(st, WJB_END_OBJECT, NULL); else - (void) pushJsonbValue(st, WJB_END_ARRAY, NULL); + pushJsonbValue(st, WJB_END_ARRAY, NULL); } } @@ -1856,14 +1856,14 @@ json_array_length(PG_FUNCTION_ARGS) JsonLexContext lex; JsonSemAction *sem; - state = palloc0(sizeof(AlenState)); + state = palloc0_object(AlenState); state->lex = makeJsonLexContext(&lex, json, false); /* palloc0 does this for us */ #if 0 state->count = 0; #endif - sem = palloc0(sizeof(JsonSemAction)); + sem = palloc0_object(JsonSemAction); sem->semstate = state; sem->object_start = alen_object_start; sem->scalar = alen_scalar; @@ -2027,7 +2027,7 @@ each_worker_jsonb(FunctionCallInfo fcinfo, const char *funcname, bool as_text) { /* a json null is an sql null in text mode */ nulls[1] = true; - values[1] = (Datum) NULL; + values[1] = (Datum) 0; } else values[1] = PointerGetDatum(JsonbValueAsText(&v)); @@ -2063,8 +2063,8 @@ each_worker(FunctionCallInfo fcinfo, bool as_text) ReturnSetInfo *rsi; EachState *state; - state = palloc0(sizeof(EachState)); - sem = palloc0(sizeof(JsonSemAction)); + state = palloc0_object(EachState); + sem = palloc0_object(JsonSemAction); rsi = (ReturnSetInfo *) fcinfo->resultinfo; @@ -2266,7 +2266,7 @@ elements_worker_jsonb(FunctionCallInfo fcinfo, const char *funcname, { /* a json null is an sql null in text mode */ nulls[0] = true; - values[0] = (Datum) NULL; + values[0] = (Datum) 0; } else values[0] = PointerGetDatum(JsonbValueAsText(&v)); @@ -2316,8 +2316,8 @@ elements_worker(FunctionCallInfo fcinfo, const char *funcname, bool as_text) /* elements only needs escaped strings when as_text */ makeJsonLexContext(&lex, json, as_text); - state = palloc0(sizeof(ElementsState)); - sem = palloc0(sizeof(JsonSemAction)); + state = palloc0_object(ElementsState); + sem = palloc0_object(JsonSemAction); InitMaterializedSRF(fcinfo, MAT_SRF_USE_EXPECTED_DESC | MAT_SRF_BLESS); rsi = (ReturnSetInfo *) fcinfo->resultinfo; @@ -2389,7 +2389,7 @@ elements_array_element_end(void *state, bool isnull) if (isnull && _state->normalize_results) { nulls[0] = true; - values[0] = (Datum) NULL; + values[0] = (Datum) 0; } else if (_state->next_scalar) { @@ -2572,8 +2572,8 @@ populate_array_assign_ndims(PopulateArrayContext *ctx, int ndims) } ctx->ndims = ndims; - ctx->dims = palloc(sizeof(int) * ndims); - ctx->sizes = palloc0(sizeof(int) * ndims); + ctx->dims = palloc_array(int, ndims); + ctx->sizes = palloc0_array(int, ndims); for (i = 0; i < ndims; i++) ctx->dims[i] = -1; /* dimensions are unknown yet */ @@ -2958,7 +2958,7 @@ populate_array(ArrayIOData *aio, Assert(ctx.ndims > 0); - lbs = palloc(sizeof(int) * ctx.ndims); + lbs = palloc_array(int, ctx.ndims); for (i = 0; i < ctx.ndims; i++) lbs[i] = 1; @@ -3824,8 +3824,8 @@ get_json_object_as_hash(const char *json, int len, const char *funcname, &ctl, HASH_ELEM | HASH_STRINGS | HASH_CONTEXT); - state = palloc0(sizeof(JHashState)); - sem = palloc0(sizeof(JsonSemAction)); + state = palloc0_object(JHashState); + sem = palloc0_object(JsonSemAction); state->function_name = funcname; state->hash = tab; @@ -4122,7 +4122,7 @@ populate_recordset_worker(FunctionCallInfo fcinfo, const char *funcname, */ update_cached_tupdesc(&cache->c.io.composite, cache->fn_mcxt); - state = palloc0(sizeof(PopulateRecordsetState)); + state = palloc0_object(PopulateRecordsetState); /* make tuplestore in a sufficiently long-lived memory context */ old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory); @@ -4141,7 +4141,7 @@ populate_recordset_worker(FunctionCallInfo fcinfo, const char *funcname, JsonLexContext lex; JsonSemAction *sem; - sem = palloc0(sizeof(JsonSemAction)); + sem = palloc0_object(JsonSemAction); makeJsonLexContext(&lex, json, true); @@ -4507,14 +4507,16 @@ json_strip_nulls(PG_FUNCTION_ARGS) text *json = PG_GETARG_TEXT_PP(0); bool strip_in_arrays = PG_NARGS() == 2 ? PG_GETARG_BOOL(1) : false; StripnullState *state; + StringInfoData strbuf; JsonLexContext lex; JsonSemAction *sem; - state = palloc0(sizeof(StripnullState)); - sem = palloc0(sizeof(JsonSemAction)); + state = palloc0_object(StripnullState); + sem = palloc0_object(JsonSemAction); + initStringInfo(&strbuf); state->lex = makeJsonLexContext(&lex, json, true); - state->strval = makeStringInfo(); + state->strval = &strbuf; state->skip_next_null = false; state->strip_in_arrays = strip_in_arrays; @@ -4542,8 +4544,7 @@ jsonb_strip_nulls(PG_FUNCTION_ARGS) Jsonb *jb = PG_GETARG_JSONB_P(0); bool strip_in_arrays = false; JsonbIterator *it; - JsonbParseState *parseState = NULL; - JsonbValue *res = NULL; + JsonbInState parseState = {0}; JsonbValue v, k; JsonbIteratorToken type; @@ -4579,7 +4580,7 @@ jsonb_strip_nulls(PG_FUNCTION_ARGS) continue; /* otherwise, do a delayed push of the key */ - (void) pushJsonbValue(&parseState, WJB_KEY, &k); + pushJsonbValue(&parseState, WJB_KEY, &k); } /* if strip_in_arrays is set, also skip null array elements */ @@ -4588,14 +4589,12 @@ jsonb_strip_nulls(PG_FUNCTION_ARGS) continue; if (type == WJB_VALUE || type == WJB_ELEM) - res = pushJsonbValue(&parseState, type, &v); + pushJsonbValue(&parseState, type, &v); else - res = pushJsonbValue(&parseState, type, NULL); + pushJsonbValue(&parseState, type, NULL); } - Assert(res != NULL); - - PG_RETURN_POINTER(JsonbValueToJsonb(res)); + PG_RETURN_POINTER(JsonbValueToJsonb(parseState.result)); } /* @@ -4607,11 +4606,12 @@ Datum jsonb_pretty(PG_FUNCTION_ARGS) { Jsonb *jb = PG_GETARG_JSONB_P(0); - StringInfo str = makeStringInfo(); + StringInfoData str; - JsonbToCStringIndent(str, &jb->root, VARSIZE(jb)); + initStringInfo(&str); + JsonbToCStringIndent(&str, &jb->root, VARSIZE(jb)); - PG_RETURN_TEXT_P(cstring_to_text_with_len(str->data, str->len)); + PG_RETURN_TEXT_P(cstring_to_text_with_len(str.data, str.len)); } /* @@ -4624,8 +4624,7 @@ jsonb_concat(PG_FUNCTION_ARGS) { Jsonb *jb1 = PG_GETARG_JSONB_P(0); Jsonb *jb2 = PG_GETARG_JSONB_P(1); - JsonbParseState *state = NULL; - JsonbValue *res; + JsonbInState state = {0}; JsonbIterator *it1, *it2; @@ -4646,11 +4645,9 @@ jsonb_concat(PG_FUNCTION_ARGS) it1 = JsonbIteratorInit(&jb1->root); it2 = JsonbIteratorInit(&jb2->root); - res = IteratorConcat(&it1, &it2, &state); + IteratorConcat(&it1, &it2, &state); - Assert(res != NULL); - - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(state.result)); } @@ -4667,10 +4664,9 @@ jsonb_delete(PG_FUNCTION_ARGS) text *key = PG_GETARG_TEXT_PP(1); char *keyptr = VARDATA_ANY(key); int keylen = VARSIZE_ANY_EXHDR(key); - JsonbParseState *state = NULL; + JsonbInState pstate = {0}; JsonbIterator *it; - JsonbValue v, - *res = NULL; + JsonbValue v; bool skipNested = false; JsonbIteratorToken r; @@ -4699,12 +4695,10 @@ jsonb_delete(PG_FUNCTION_ARGS) continue; } - res = pushJsonbValue(&state, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(&pstate, r, r < WJB_BEGIN_ARRAY ? &v : NULL); } - Assert(res != NULL); - - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(pstate.result)); } /* @@ -4721,10 +4715,9 @@ jsonb_delete_array(PG_FUNCTION_ARGS) Datum *keys_elems; bool *keys_nulls; int keys_len; - JsonbParseState *state = NULL; + JsonbInState pstate = {0}; JsonbIterator *it; - JsonbValue v, - *res = NULL; + JsonbValue v; bool skipNested = false; JsonbIteratorToken r; @@ -4766,8 +4759,8 @@ jsonb_delete_array(PG_FUNCTION_ARGS) continue; /* We rely on the array elements not being toasted */ - keyptr = VARDATA_ANY(keys_elems[i]); - keylen = VARSIZE_ANY_EXHDR(keys_elems[i]); + keyptr = VARDATA_ANY(DatumGetPointer(keys_elems[i])); + keylen = VARSIZE_ANY_EXHDR(DatumGetPointer(keys_elems[i])); if (keylen == v.val.string.len && memcmp(keyptr, v.val.string.val, keylen) == 0) { @@ -4785,12 +4778,10 @@ jsonb_delete_array(PG_FUNCTION_ARGS) } } - res = pushJsonbValue(&state, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(&pstate, r, r < WJB_BEGIN_ARRAY ? &v : NULL); } - Assert(res != NULL); - - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(pstate.result)); } /* @@ -4805,12 +4796,11 @@ jsonb_delete_idx(PG_FUNCTION_ARGS) { Jsonb *in = PG_GETARG_JSONB_P(0); int idx = PG_GETARG_INT32(1); - JsonbParseState *state = NULL; + JsonbInState pstate = {0}; JsonbIterator *it; uint32 i = 0, n; - JsonbValue v, - *res = NULL; + JsonbValue v; JsonbIteratorToken r; if (JB_ROOT_IS_SCALAR(in)) @@ -4843,7 +4833,7 @@ jsonb_delete_idx(PG_FUNCTION_ARGS) if (idx >= n) PG_RETURN_JSONB_P(in); - pushJsonbValue(&state, r, NULL); + pushJsonbValue(&pstate, r, NULL); while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE) { @@ -4853,12 +4843,10 @@ jsonb_delete_idx(PG_FUNCTION_ARGS) continue; } - res = pushJsonbValue(&state, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(&pstate, r, r < WJB_BEGIN_ARRAY ? &v : NULL); } - Assert(res != NULL); - - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(pstate.result)); } /* @@ -4872,12 +4860,11 @@ jsonb_set(PG_FUNCTION_ARGS) Jsonb *newjsonb = PG_GETARG_JSONB_P(2); JsonbValue newval; bool create = PG_GETARG_BOOL(3); - JsonbValue *res = NULL; Datum *path_elems; bool *path_nulls; int path_len; JsonbIterator *it; - JsonbParseState *st = NULL; + JsonbInState st = {0}; JsonbToJsonbValue(newjsonb, &newval); @@ -4901,12 +4888,10 @@ jsonb_set(PG_FUNCTION_ARGS) it = JsonbIteratorInit(&in->root); - res = setPath(&it, path_elems, path_nulls, path_len, &st, - 0, &newval, create ? JB_PATH_CREATE : JB_PATH_REPLACE); + setPath(&it, path_elems, path_nulls, path_len, &st, + 0, &newval, create ? JB_PATH_CREATE : JB_PATH_REPLACE); - Assert(res != NULL); - - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(st.result)); } @@ -4916,10 +4901,6 @@ jsonb_set(PG_FUNCTION_ARGS) Datum jsonb_set_lax(PG_FUNCTION_ARGS) { - /* Jsonb *in = PG_GETARG_JSONB_P(0); */ - /* ArrayType *path = PG_GETARG_ARRAYTYPE_P(1); */ - /* Jsonb *newval = PG_GETARG_JSONB_P(2); */ - /* bool create = PG_GETARG_BOOL(3); */ text *handle_null; char *handle_val; @@ -4985,12 +4966,11 @@ jsonb_delete_path(PG_FUNCTION_ARGS) { Jsonb *in = PG_GETARG_JSONB_P(0); ArrayType *path = PG_GETARG_ARRAYTYPE_P(1); - JsonbValue *res = NULL; Datum *path_elems; bool *path_nulls; int path_len; JsonbIterator *it; - JsonbParseState *st = NULL; + JsonbInState st = {0}; if (ARR_NDIM(path) > 1) ereport(ERROR, @@ -5012,12 +4992,10 @@ jsonb_delete_path(PG_FUNCTION_ARGS) it = JsonbIteratorInit(&in->root); - res = setPath(&it, path_elems, path_nulls, path_len, &st, - 0, NULL, JB_PATH_DELETE); - - Assert(res != NULL); + setPath(&it, path_elems, path_nulls, path_len, &st, + 0, NULL, JB_PATH_DELETE); - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(st.result)); } /* @@ -5031,12 +5009,11 @@ jsonb_insert(PG_FUNCTION_ARGS) Jsonb *newjsonb = PG_GETARG_JSONB_P(2); JsonbValue newval; bool after = PG_GETARG_BOOL(3); - JsonbValue *res = NULL; Datum *path_elems; bool *path_nulls; int path_len; JsonbIterator *it; - JsonbParseState *st = NULL; + JsonbInState st = {0}; JsonbToJsonbValue(newjsonb, &newval); @@ -5057,12 +5034,10 @@ jsonb_insert(PG_FUNCTION_ARGS) it = JsonbIteratorInit(&in->root); - res = setPath(&it, path_elems, path_nulls, path_len, &st, 0, &newval, - after ? JB_PATH_INSERT_AFTER : JB_PATH_INSERT_BEFORE); + setPath(&it, path_elems, path_nulls, path_len, &st, 0, &newval, + after ? JB_PATH_INSERT_AFTER : JB_PATH_INSERT_BEFORE); - Assert(res != NULL); - - PG_RETURN_JSONB_P(JsonbValueToJsonb(res)); + PG_RETURN_JSONB_P(JsonbValueToJsonb(st.result)); } /* @@ -5072,13 +5047,12 @@ jsonb_insert(PG_FUNCTION_ARGS) * In that case we just append the content of it2 to it1 without any * verifications. */ -static JsonbValue * +static void IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, - JsonbParseState **state) + JsonbInState *state) { JsonbValue v1, - v2, - *res = NULL; + v2; JsonbIteratorToken r1, r2, rk1, @@ -5109,7 +5083,7 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, * automatically override the value from the first object. */ while ((r2 = JsonbIteratorNext(it2, &v2, true)) != WJB_DONE) - res = pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL); + pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL); } else if (rk1 == WJB_BEGIN_ARRAY && rk2 == WJB_BEGIN_ARRAY) { @@ -5130,7 +5104,7 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, pushJsonbValue(state, WJB_ELEM, &v2); } - res = pushJsonbValue(state, WJB_END_ARRAY, NULL /* signal to sort */ ); + pushJsonbValue(state, WJB_END_ARRAY, NULL /* signal to sort */ ); } else if (rk1 == WJB_BEGIN_OBJECT) { @@ -5146,7 +5120,7 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, pushJsonbValue(state, r1, r1 != WJB_END_OBJECT ? &v1 : NULL); while ((r2 = JsonbIteratorNext(it2, &v2, true)) != WJB_DONE) - res = pushJsonbValue(state, r2, r2 != WJB_END_ARRAY ? &v2 : NULL); + pushJsonbValue(state, r2, r2 != WJB_END_ARRAY ? &v2 : NULL); } else { @@ -5165,10 +5139,8 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, while ((r2 = JsonbIteratorNext(it2, &v2, true)) != WJB_DONE) pushJsonbValue(state, r2, r2 != WJB_END_OBJECT ? &v2 : NULL); - res = pushJsonbValue(state, WJB_END_ARRAY, NULL); + pushJsonbValue(state, WJB_END_ARRAY, NULL); } - - return res; } /* @@ -5200,14 +5172,13 @@ IteratorConcat(JsonbIterator **it1, JsonbIterator **it2, * All path elements before the last must already exist * whatever bits in op_type are set, or nothing is done. */ -static JsonbValue * -setPath(JsonbIterator **it, Datum *path_elems, - bool *path_nulls, int path_len, - JsonbParseState **st, int level, JsonbValue *newval, int op_type) +static void +setPath(JsonbIterator **it, const Datum *path_elems, + const bool *path_nulls, int path_len, + JsonbInState *st, int level, JsonbValue *newval, int op_type) { JsonbValue v; JsonbIteratorToken r; - JsonbValue *res; check_stack_depth(); @@ -5237,20 +5208,20 @@ setPath(JsonbIterator **it, Datum *path_elems, errdetail("The path assumes key is a composite object, " "but it is a scalar value."))); - (void) pushJsonbValue(st, r, NULL); + pushJsonbValue(st, r, NULL); setPathArray(it, path_elems, path_nulls, path_len, st, level, newval, v.val.array.nElems, op_type); r = JsonbIteratorNext(it, &v, false); Assert(r == WJB_END_ARRAY); - res = pushJsonbValue(st, r, NULL); + pushJsonbValue(st, r, NULL); break; case WJB_BEGIN_OBJECT: - (void) pushJsonbValue(st, r, NULL); + pushJsonbValue(st, r, NULL); setPathObject(it, path_elems, path_nulls, path_len, st, level, newval, v.val.object.nPairs, op_type); r = JsonbIteratorNext(it, &v, true); Assert(r == WJB_END_OBJECT); - res = pushJsonbValue(st, r, NULL); + pushJsonbValue(st, r, NULL); break; case WJB_ELEM: case WJB_VALUE: @@ -5268,23 +5239,20 @@ setPath(JsonbIterator **it, Datum *path_elems, errdetail("The path assumes key is a composite object, " "but it is a scalar value."))); - res = pushJsonbValue(st, r, &v); + pushJsonbValue(st, r, &v); break; default: elog(ERROR, "unrecognized iterator result: %d", (int) r); - res = NULL; /* keep compiler quiet */ break; } - - return res; } /* * Object walker for setPath */ static void -setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, - int path_len, JsonbParseState **st, int level, +setPathObject(JsonbIterator **it, const Datum *path_elems, const bool *path_nulls, + int path_len, JsonbInState *st, int level, JsonbValue *newval, uint32 npairs, int op_type) { text *pathelem = NULL; @@ -5311,8 +5279,8 @@ setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, newkey.val.string.val = VARDATA_ANY(pathelem); newkey.val.string.len = VARSIZE_ANY_EXHDR(pathelem); - (void) pushJsonbValue(st, WJB_KEY, &newkey); - (void) pushJsonbValue(st, WJB_VALUE, newval); + pushJsonbValue(st, WJB_KEY, &newkey); + pushJsonbValue(st, WJB_VALUE, newval); } for (i = 0; i < npairs; i++) @@ -5344,13 +5312,13 @@ setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, r = JsonbIteratorNext(it, &v, true); /* skip value */ if (!(op_type & JB_PATH_DELETE)) { - (void) pushJsonbValue(st, WJB_KEY, &k); - (void) pushJsonbValue(st, WJB_VALUE, newval); + pushJsonbValue(st, WJB_KEY, &k); + pushJsonbValue(st, WJB_VALUE, newval); } } else { - (void) pushJsonbValue(st, r, &k); + pushJsonbValue(st, r, &k); setPath(it, path_elems, path_nulls, path_len, st, level + 1, newval, op_type); } @@ -5366,13 +5334,13 @@ setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, newkey.val.string.val = VARDATA_ANY(pathelem); newkey.val.string.len = VARSIZE_ANY_EXHDR(pathelem); - (void) pushJsonbValue(st, WJB_KEY, &newkey); - (void) pushJsonbValue(st, WJB_VALUE, newval); + pushJsonbValue(st, WJB_KEY, &newkey); + pushJsonbValue(st, WJB_VALUE, newval); } - (void) pushJsonbValue(st, r, &k); + pushJsonbValue(st, r, &k); r = JsonbIteratorNext(it, &v, false); - (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); if (r == WJB_BEGIN_ARRAY || r == WJB_BEGIN_OBJECT) { int walking_level = 1; @@ -5386,7 +5354,7 @@ setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, if (r == WJB_END_ARRAY || r == WJB_END_OBJECT) --walking_level; - (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); } } } @@ -5410,9 +5378,8 @@ setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, newkey.val.string.val = VARDATA_ANY(pathelem); newkey.val.string.len = VARSIZE_ANY_EXHDR(pathelem); - (void) pushJsonbValue(st, WJB_KEY, &newkey); - (void) push_path(st, level, path_elems, path_nulls, - path_len, newval); + pushJsonbValue(st, WJB_KEY, &newkey); + push_path(st, level, path_elems, path_nulls, path_len, newval); /* Result is closed with WJB_END_OBJECT outside of this function */ } @@ -5422,8 +5389,8 @@ setPathObject(JsonbIterator **it, Datum *path_elems, bool *path_nulls, * Array walker for setPath */ static void -setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, - int path_len, JsonbParseState **st, int level, +setPathArray(JsonbIterator **it, const Datum *path_elems, const bool *path_nulls, + int path_len, JsonbInState *st, int level, JsonbValue *newval, uint32 nelems, int op_type) { JsonbValue v; @@ -5491,7 +5458,7 @@ setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, if (op_type & JB_PATH_FILL_GAPS && nelems == 0 && idx > 0) push_null_elements(st, idx); - (void) pushJsonbValue(st, WJB_ELEM, newval); + pushJsonbValue(st, WJB_ELEM, newval); done = true; } @@ -5510,7 +5477,7 @@ setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, r = JsonbIteratorNext(it, &v, true); /* skip */ if (op_type & (JB_PATH_INSERT_BEFORE | JB_PATH_CREATE)) - (void) pushJsonbValue(st, WJB_ELEM, newval); + pushJsonbValue(st, WJB_ELEM, newval); /* * We should keep current value only in case of @@ -5518,20 +5485,20 @@ setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, * otherwise it should be deleted or replaced */ if (op_type & (JB_PATH_INSERT_AFTER | JB_PATH_INSERT_BEFORE)) - (void) pushJsonbValue(st, r, &v); + pushJsonbValue(st, r, &v); if (op_type & (JB_PATH_INSERT_AFTER | JB_PATH_REPLACE)) - (void) pushJsonbValue(st, WJB_ELEM, newval); + pushJsonbValue(st, WJB_ELEM, newval); } else - (void) setPath(it, path_elems, path_nulls, path_len, - st, level + 1, newval, op_type); + setPath(it, path_elems, path_nulls, path_len, + st, level + 1, newval, op_type); } else { r = JsonbIteratorNext(it, &v, false); - (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); if (r == WJB_BEGIN_ARRAY || r == WJB_BEGIN_OBJECT) { @@ -5546,7 +5513,7 @@ setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, if (r == WJB_END_ARRAY || r == WJB_END_OBJECT) --walking_level; - (void) pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(st, r, r < WJB_BEGIN_ARRAY ? &v : NULL); } } } @@ -5561,7 +5528,7 @@ setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, if (op_type & JB_PATH_FILL_GAPS && idx > nelems) push_null_elements(st, idx - nelems); - (void) pushJsonbValue(st, WJB_ELEM, newval); + pushJsonbValue(st, WJB_ELEM, newval); done = true; } @@ -5580,8 +5547,7 @@ setPathArray(JsonbIterator **it, Datum *path_elems, bool *path_nulls, if (idx > 0) push_null_elements(st, idx - nelems); - (void) push_path(st, level, path_elems, path_nulls, - path_len, newval); + push_path(st, level, path_elems, path_nulls, path_len, newval); /* Result is closed with WJB_END_OBJECT outside of this function */ } @@ -5733,8 +5699,8 @@ iterate_json_values(text *json, uint32 flags, void *action_state, JsonIterateStringValuesAction action) { JsonLexContext lex; - JsonSemAction *sem = palloc0(sizeof(JsonSemAction)); - IterateJsonStringValuesState *state = palloc0(sizeof(IterateJsonStringValuesState)); + JsonSemAction *sem = palloc0_object(JsonSemAction); + IterateJsonStringValuesState *state = palloc0_object(IterateJsonStringValuesState); state->lex = makeJsonLexContext(&lex, json, true); state->action = action; @@ -5807,10 +5773,9 @@ transform_jsonb_string_values(Jsonb *jsonb, void *action_state, JsonTransformStringValuesAction transform_action) { JsonbIterator *it; - JsonbValue v, - *res = NULL; + JsonbValue v; JsonbIteratorToken type; - JsonbParseState *st = NULL; + JsonbInState st = {0}; text *out; bool is_scalar = false; @@ -5826,27 +5791,27 @@ transform_jsonb_string_values(Jsonb *jsonb, void *action_state, out = pg_detoast_datum_packed(out); v.val.string.val = VARDATA_ANY(out); v.val.string.len = VARSIZE_ANY_EXHDR(out); - res = pushJsonbValue(&st, type, type < WJB_BEGIN_ARRAY ? &v : NULL); + pushJsonbValue(&st, type, type < WJB_BEGIN_ARRAY ? &v : NULL); } else { - res = pushJsonbValue(&st, type, (type == WJB_KEY || - type == WJB_VALUE || - type == WJB_ELEM) ? &v : NULL); + pushJsonbValue(&st, type, (type == WJB_KEY || + type == WJB_VALUE || + type == WJB_ELEM) ? &v : NULL); } } - if (res->type == jbvArray) - res->val.array.rawScalar = is_scalar; + if (st.result->type == jbvArray) + st.result->val.array.rawScalar = is_scalar; - return JsonbValueToJsonb(res); + return JsonbValueToJsonb(st.result); } /* * Iterate over a json, and apply a specified JsonTransformStringValuesAction * to every string value or element. Any necessary context for a * JsonTransformStringValuesAction can be passed in the action_state variable. - * Function returns a StringInfo, which is a copy of an original json with + * Function returns a Text Datum, which is a copy of an original json with * transformed values. */ text * @@ -5854,11 +5819,14 @@ transform_json_string_values(text *json, void *action_state, JsonTransformStringValuesAction transform_action) { JsonLexContext lex; - JsonSemAction *sem = palloc0(sizeof(JsonSemAction)); - TransformJsonStringValuesState *state = palloc0(sizeof(TransformJsonStringValuesState)); + JsonSemAction *sem = palloc0_object(JsonSemAction); + TransformJsonStringValuesState *state = palloc0_object(TransformJsonStringValuesState); + StringInfoData strbuf; + + initStringInfo(&strbuf); state->lex = makeJsonLexContext(&lex, json, true); - state->strval = makeStringInfo(); + state->strval = &strbuf; state->action = transform_action; state->action_state = action_state; @@ -6097,3 +6065,106 @@ json_categorize_type(Oid typoid, bool is_jsonb, break; } } + +/* + * Check whether a type conversion to JSON or JSONB involves any mutable + * functions. This recurses into container types (arrays, composites, + * ranges, multiranges, domains) to check their element/sub types. + * + * The caller must initialize *has_mutable to false before calling. + * If any mutable function is found, *has_mutable is set to true. + */ +void +json_check_mutability(Oid typoid, bool is_jsonb, bool *has_mutable) +{ + char att_typtype = get_typtype(typoid); + JsonTypeCategory tcategory; + Oid outfuncoid; + + /* since this function recurses, it could be driven to stack overflow */ + check_stack_depth(); + + Assert(has_mutable != NULL); + + if (*has_mutable) + return; + + if (att_typtype == TYPTYPE_DOMAIN) + { + json_check_mutability(getBaseType(typoid), is_jsonb, has_mutable); + return; + } + else if (att_typtype == TYPTYPE_COMPOSITE) + { + /* + * For a composite type, recurse into its attributes. Use the + * typcache to avoid opening the relation directly. + */ + TupleDesc tupdesc = lookup_rowtype_tupdesc(typoid, -1); + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped) + continue; + + json_check_mutability(attr->atttypid, is_jsonb, has_mutable); + if (*has_mutable) + break; + } + ReleaseTupleDesc(tupdesc); + return; + } + else if (att_typtype == TYPTYPE_RANGE) + { + json_check_mutability(get_range_subtype(typoid), is_jsonb, + has_mutable); + return; + } + else if (att_typtype == TYPTYPE_MULTIRANGE) + { + json_check_mutability(get_multirange_range(typoid), is_jsonb, + has_mutable); + return; + } + else + { + Oid att_typelem = get_element_type(typoid); + + if (OidIsValid(att_typelem)) + { + /* recurse into array element type */ + json_check_mutability(att_typelem, is_jsonb, has_mutable); + return; + } + } + + json_categorize_type(typoid, is_jsonb, &tcategory, &outfuncoid); + + switch (tcategory) + { + case JSONTYPE_NULL: + case JSONTYPE_BOOL: + case JSONTYPE_NUMERIC: + break; + + case JSONTYPE_DATE: + case JSONTYPE_TIMESTAMP: + case JSONTYPE_TIMESTAMPTZ: + *has_mutable = true; + break; + + case JSONTYPE_JSON: + case JSONTYPE_JSONB: + case JSONTYPE_ARRAY: + case JSONTYPE_COMPOSITE: + break; + + case JSONTYPE_CAST: + case JSONTYPE_OTHER: + if (func_volatile(outfuncoid) != PROVOLATILE_IMMUTABLE) + *has_mutable = true; + break; + } +} diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 762f7e8a09d39..7bfc18c988854 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -53,7 +53,7 @@ * | |__| |__||________________________||___________________| | * |_______________________________________________________________________| * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/jsonpath.c @@ -298,6 +298,8 @@ flattenJsonPathParseItem(StringInfo buf, int *result, struct Node *escontext, case jpiMod: case jpiStartsWith: case jpiDecimal: + case jpiStrReplace: + case jpiStrSplitPart: { /* * First, reserve place for left/right arg's positions, then @@ -351,7 +353,7 @@ flattenJsonPathParseItem(StringInfo buf, int *result, struct Node *escontext, break; case jpiFilter: argNestingLevel++; - /* FALLTHROUGH */ + pg_fallthrough; case jpiIsUnknown: case jpiNot: case jpiPlus: @@ -362,6 +364,9 @@ flattenJsonPathParseItem(StringInfo buf, int *result, struct Node *escontext, case jpiTimeTz: case jpiTimestamp: case jpiTimestampTz: + case jpiStrLtrim: + case jpiStrRtrim: + case jpiStrBtrim: { int32 arg = reserveSpaceForItemPointer(buf); @@ -457,6 +462,9 @@ flattenJsonPathParseItem(StringInfo buf, int *result, struct Node *escontext, case jpiInteger: case jpiNumber: case jpiStringFunc: + case jpiStrLower: + case jpiStrUpper: + case jpiStrInitcap: break; default: elog(ERROR, "unrecognized jsonpath item type: %d", item->type); @@ -487,13 +495,13 @@ alignStringInfoInt(StringInfo buf) { case 3: appendStringInfoCharMacro(buf, 0); - /* FALLTHROUGH */ + pg_fallthrough; case 2: appendStringInfoCharMacro(buf, 0); - /* FALLTHROUGH */ + pg_fallthrough; case 1: appendStringInfoCharMacro(buf, 0); - /* FALLTHROUGH */ + pg_fallthrough; default: break; } @@ -831,6 +839,60 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey, } appendStringInfoChar(buf, ')'); break; + case jpiStrReplace: + appendStringInfoString(buf, ".replace("); + jspGetLeftArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + appendStringInfoChar(buf, ','); + jspGetRightArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + appendStringInfoChar(buf, ')'); + break; + case jpiStrLower: + appendStringInfoString(buf, ".lower()"); + break; + case jpiStrUpper: + appendStringInfoString(buf, ".upper()"); + break; + case jpiStrSplitPart: + appendStringInfoString(buf, ".split_part("); + jspGetLeftArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + appendStringInfoChar(buf, ','); + jspGetRightArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + appendStringInfoChar(buf, ')'); + break; + case jpiStrLtrim: + appendStringInfoString(buf, ".ltrim("); + if (v->content.arg) + { + jspGetArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + } + appendStringInfoChar(buf, ')'); + break; + case jpiStrRtrim: + appendStringInfoString(buf, ".rtrim("); + if (v->content.arg) + { + jspGetArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + } + appendStringInfoChar(buf, ')'); + break; + case jpiStrBtrim: + appendStringInfoString(buf, ".btrim("); + if (v->content.arg) + { + jspGetArg(v, &elem); + printJsonPathItem(buf, &elem, false, false); + } + appendStringInfoChar(buf, ')'); + break; + case jpiStrInitcap: + appendStringInfoString(buf, ".initcap()"); + break; default: elog(ERROR, "unrecognized jsonpath item type: %d", v->type); } @@ -914,6 +976,22 @@ jspOperationName(JsonPathItemType type) return "timestamp"; case jpiTimestampTz: return "timestamp_tz"; + case jpiStrReplace: + return "replace"; + case jpiStrLower: + return "lower"; + case jpiStrUpper: + return "upper"; + case jpiStrLtrim: + return "ltrim"; + case jpiStrRtrim: + return "rtrim"; + case jpiStrBtrim: + return "btrim"; + case jpiStrInitcap: + return "initcap"; + case jpiStrSplitPart: + return "split_part"; default: elog(ERROR, "unrecognized jsonpath item type: %d", type); return NULL; @@ -1016,12 +1094,15 @@ jspInitByBuffer(JsonPathItem *v, char *base, int32 pos) case jpiInteger: case jpiNumber: case jpiStringFunc: + case jpiStrLower: + case jpiStrUpper: + case jpiStrInitcap: break; case jpiString: case jpiKey: case jpiVariable: read_int32(v->content.value.datalen, base, pos); - /* FALLTHROUGH */ + pg_fallthrough; case jpiNumeric: case jpiBool: v->content.value.data = base + pos; @@ -1041,6 +1122,8 @@ jspInitByBuffer(JsonPathItem *v, char *base, int32 pos) case jpiMod: case jpiStartsWith: case jpiDecimal: + case jpiStrReplace: + case jpiStrSplitPart: read_int32(v->content.args.left, base, pos); read_int32(v->content.args.right, base, pos); break; @@ -1055,6 +1138,9 @@ jspInitByBuffer(JsonPathItem *v, char *base, int32 pos) case jpiTimeTz: case jpiTimestamp: case jpiTimestampTz: + case jpiStrLtrim: + case jpiStrRtrim: + case jpiStrBtrim: read_int32(v->content.arg, base, pos); break; case jpiIndexArray: @@ -1090,7 +1176,10 @@ jspGetArg(JsonPathItem *v, JsonPathItem *a) v->type == jpiTime || v->type == jpiTimeTz || v->type == jpiTimestamp || - v->type == jpiTimestampTz); + v->type == jpiTimestampTz || + v->type == jpiStrLtrim || + v->type == jpiStrRtrim || + v->type == jpiStrBtrim); jspInitByBuffer(a, v->base, v->content.arg); } @@ -1152,7 +1241,15 @@ jspGetNext(JsonPathItem *v, JsonPathItem *a) v->type == jpiTime || v->type == jpiTimeTz || v->type == jpiTimestamp || - v->type == jpiTimestampTz); + v->type == jpiTimestampTz || + v->type == jpiStrReplace || + v->type == jpiStrLower || + v->type == jpiStrUpper || + v->type == jpiStrLtrim || + v->type == jpiStrRtrim || + v->type == jpiStrBtrim || + v->type == jpiStrInitcap || + v->type == jpiStrSplitPart); if (a) jspInitByBuffer(a, v->base, v->nextPos); @@ -1179,7 +1276,9 @@ jspGetLeftArg(JsonPathItem *v, JsonPathItem *a) v->type == jpiDiv || v->type == jpiMod || v->type == jpiStartsWith || - v->type == jpiDecimal); + v->type == jpiDecimal || + v->type == jpiStrReplace || + v->type == jpiStrSplitPart); jspInitByBuffer(a, v->base, v->content.args.left); } @@ -1201,7 +1300,9 @@ jspGetRightArg(JsonPathItem *v, JsonPathItem *a) v->type == jpiDiv || v->type == jpiMod || v->type == jpiStartsWith || - v->type == jpiDecimal); + v->type == jpiDecimal || + v->type == jpiStrReplace || + v->type == jpiStrSplitPart); jspInitByBuffer(a, v->base, v->content.args.right); } @@ -1433,7 +1534,7 @@ jspIsMutableWalker(JsonPathItem *jpi, struct JsonPathMutableContext *cxt) jspIsMutableWalker(&from, cxt); } - /* FALLTHROUGH */ + pg_fallthrough; case jpiAnyArray: if (!cxt->lax) @@ -1501,6 +1602,14 @@ jspIsMutableWalker(JsonPathItem *jpi, struct JsonPathMutableContext *cxt) case jpiInteger: case jpiNumber: case jpiStringFunc: + case jpiStrReplace: + case jpiStrLower: + case jpiStrUpper: + case jpiStrLtrim: + case jpiStrRtrim: + case jpiStrBtrim: + case jpiStrInitcap: + case jpiStrSplitPart: status = jpdsNonDateTime; break; diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index dbab24737ef1f..0ec9b4df2efe4 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -49,7 +49,7 @@ * we calculate operands first. Then we check that results are numeric * singleton lists, calculate the result and pass it to the next path item. * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/jsonpath_exec.c @@ -142,19 +142,46 @@ typedef enum JsonPathExecResult #define jperIsError(jper) ((jper) == jperError) /* - * List of jsonb values with shortcut for single-value list. + * List (or really array) of JsonbValues. This is the output representation + * of jsonpath evaluation. + * + * The initial or "base" chunk of a list is typically a local variable in + * a calling function. If we need more entries than will fit in the base + * chunk, we palloc more chunks. For notational simplicity, those are also + * treated as being of type JsonValueList, although they will have items[] + * arrays that are larger than BASE_JVL_ITEMS. + * + * Callers *must* initialize the base chunk with JsonValueListInit(). + * Typically they should free any extra chunks when done, using + * JsonValueListClear(), although some top-level functions skip that + * on the assumption that the caller's context will be reset soon. + * + * Note that most types of JsonbValue include pointers to external data, which + * will not be managed by the JsonValueList functions. We expect that such + * data is part of the input to the jsonpath operation, and the caller will + * see to it that it holds still for the duration of the operation. + * + * Most lists are short, though some can be quite long. So we set + * BASE_JVL_ITEMS small to conserve stack space, but grow the extra + * chunks aggressively. */ +#define BASE_JVL_ITEMS 2 /* number of items a base chunk holds */ +#define MIN_EXTRA_JVL_ITEMS 16 /* min number of items an extra chunk holds */ + typedef struct JsonValueList { - JsonbValue *singleton; - List *list; + int nitems; /* number of items stored in this chunk */ + int maxitems; /* allocated length of items[] */ + struct JsonValueList *next; /* => next chunk, if any */ + struct JsonValueList *last; /* => last chunk (only valid in base chunk) */ + JsonbValue items[BASE_JVL_ITEMS]; } JsonValueList; +/* State data for iterating through a JsonValueList */ typedef struct JsonValueListIterator { - JsonbValue *value; - List *list; - ListCell *next; + JsonValueList *chunk; /* current chunk of list */ + int nextitem; /* index of next value to return in chunk */ } JsonValueListIterator; /* Structures for JSON_TABLE execution */ @@ -252,7 +279,8 @@ typedef JsonPathBool (*JsonPathPredicateCallback) (JsonPathItem *jsp, JsonbValue *larg, JsonbValue *rarg, void *param); -typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, bool *error); +typedef Numeric (*BinaryArithmFunc) (Numeric num1, Numeric num2, + Node *escontext); static JsonPathExecResult executeJsonPath(JsonPath *path, void *vars, JsonPathGetVarCallback getVar, @@ -269,7 +297,7 @@ static JsonPathExecResult executeItemUnwrapTargetArray(JsonPathExecContext *cxt, JsonValueList *found, bool unwrapElements); static JsonPathExecResult executeNextItem(JsonPathExecContext *cxt, JsonPathItem *cur, JsonPathItem *next, - JsonbValue *v, JsonValueList *found, bool copy); + JsonbValue *v, JsonValueList *found); static JsonPathExecResult executeItemOptUnwrapResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, bool unwrap, JsonValueList *found); static JsonPathExecResult executeItemOptUnwrapResultNoThrow(JsonPathExecContext *cxt, JsonPathItem *jsp, @@ -301,6 +329,8 @@ static JsonPathExecResult executeNumericItemMethod(JsonPathExecContext *cxt, JsonValueList *found); static JsonPathExecResult executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, JsonValueList *found); +static JsonPathExecResult executeStringInternalMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, + JsonbValue *jb, JsonValueList *found); static JsonPathExecResult executeKeyValueMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, JsonValueList *found); static JsonPathExecResult appendBoolResult(JsonPathExecContext *cxt, @@ -331,20 +361,20 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListInit(JsonValueList *jvl); static void JsonValueListClear(JsonValueList *jvl); -static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); -static int JsonValueListLength(const JsonValueList *jvl); -static bool JsonValueListIsEmpty(JsonValueList *jvl); +static void JsonValueListAppend(JsonValueList *jvl, const JsonbValue *jbv); +static bool JsonValueListIsEmpty(const JsonValueList *jvl); +static bool JsonValueListIsSingleton(const JsonValueList *jvl); +static bool JsonValueListHasMultipleItems(const JsonValueList *jvl); static JsonbValue *JsonValueListHead(JsonValueList *jvl); -static List *JsonValueListGetList(JsonValueList *jvl); -static void JsonValueListInitIterator(const JsonValueList *jvl, +static void JsonValueListInitIterator(JsonValueList *jvl, JsonValueListIterator *it); -static JsonbValue *JsonValueListNext(const JsonValueList *jvl, - JsonValueListIterator *it); +static JsonbValue *JsonValueListNext(JsonValueListIterator *it); static JsonbValue *JsonbInitBinary(JsonbValue *jbv, Jsonb *jb); static int JsonbType(JsonbValue *jb); static JsonbValue *getScalar(JsonbValue *scalar, enum jbvType type); -static JsonbValue *wrapItemsInArray(const JsonValueList *items); +static JsonbValue *wrapItemsInArray(JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *cast_error); static void checkTimezoneIsUsedForCast(bool useTz, const char *type1, @@ -455,9 +485,9 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) { Jsonb *jb = PG_GETARG_JSONB_P(0); JsonPath *jp = PG_GETARG_JSONPATH_P(1); - JsonValueList found = {0}; Jsonb *vars = NULL; bool silent = true; + JsonValueList found; if (PG_NARGS() == 4) { @@ -465,6 +495,8 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) silent = PG_GETARG_BOOL(3); } + JsonValueListInit(&found); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, countVariablesFromJsonb, jb, !silent, &found, tz); @@ -472,7 +504,7 @@ jsonb_path_match_internal(FunctionCallInfo fcinfo, bool tz) PG_FREE_IF_COPY(jb, 0); PG_FREE_IF_COPY(jp, 1); - if (JsonValueListLength(&found) == 1) + if (JsonValueListIsSingleton(&found)) { JsonbValue *jbv = JsonValueListHead(&found); @@ -524,18 +556,17 @@ static Datum jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) { FuncCallContext *funcctx; - List *found; + JsonValueListIterator *iter; JsonbValue *v; - ListCell *c; if (SRF_IS_FIRSTCALL()) { JsonPath *jp; Jsonb *jb; - MemoryContext oldcontext; Jsonb *vars; bool silent; - JsonValueList found = {0}; + MemoryContext oldcontext; + JsonValueList *found; funcctx = SRF_FIRSTCALL_INIT(); oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); @@ -545,26 +576,29 @@ jsonb_path_query_internal(FunctionCallInfo fcinfo, bool tz) vars = PG_GETARG_JSONB_P_COPY(2); silent = PG_GETARG_BOOL(3); + found = palloc_object(JsonValueList); + JsonValueListInit(found); + (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, countVariablesFromJsonb, - jb, !silent, &found, tz); + jb, !silent, found, tz); + + iter = palloc_object(JsonValueListIterator); + JsonValueListInitIterator(found, iter); - funcctx->user_fctx = JsonValueListGetList(&found); + funcctx->user_fctx = iter; MemoryContextSwitchTo(oldcontext); } funcctx = SRF_PERCALL_SETUP(); - found = funcctx->user_fctx; + iter = funcctx->user_fctx; - c = list_head(found); + v = JsonValueListNext(iter); - if (c == NULL) + if (v == NULL) SRF_RETURN_DONE(funcctx); - v = lfirst(c); - funcctx->user_fctx = list_delete_first(found); - SRF_RETURN_NEXT(funcctx, JsonbPGetDatum(JsonbValueToJsonb(v))); } @@ -590,9 +624,11 @@ jsonb_path_query_array_internal(FunctionCallInfo fcinfo, bool tz) { Jsonb *jb = PG_GETARG_JSONB_P(0); JsonPath *jp = PG_GETARG_JSONPATH_P(1); - JsonValueList found = {0}; Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); + JsonValueList found; + + JsonValueListInit(&found); (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, countVariablesFromJsonb, @@ -623,15 +659,17 @@ jsonb_path_query_first_internal(FunctionCallInfo fcinfo, bool tz) { Jsonb *jb = PG_GETARG_JSONB_P(0); JsonPath *jp = PG_GETARG_JSONPATH_P(1); - JsonValueList found = {0}; Jsonb *vars = PG_GETARG_JSONB_P(2); bool silent = PG_GETARG_BOOL(3); + JsonValueList found; + + JsonValueListInit(&found); (void) executeJsonPath(jp, vars, getJsonPathVariableFromJsonb, countVariablesFromJsonb, jb, !silent, &found, tz); - if (JsonValueListLength(&found) >= 1) + if (!JsonValueListIsEmpty(&found)) PG_RETURN_JSONB_P(JsonbValueToJsonb(JsonValueListHead(&found))); else PG_RETURN_NULL(); @@ -709,14 +747,20 @@ executeJsonPath(JsonPath *path, void *vars, JsonPathGetVarCallback getVar, * In strict mode we must get a complete list of values to check that * there are no errors at all. */ - JsonValueList vals = {0}; + JsonValueList vals; + bool isempty; + + JsonValueListInit(&vals); res = executeItem(&cxt, &jsp, &jbv, &vals); + isempty = JsonValueListIsEmpty(&vals); + JsonValueListClear(&vals); + if (jperIsError(res)) return res; - return JsonValueListIsEmpty(&vals) ? jperNotFound : jperOk; + return isempty ? jperNotFound : jperOk; } res = executeItem(&cxt, &jsp, &jbv, result); @@ -760,8 +804,7 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiString: case jpiVariable: { - JsonbValue vbuf; - JsonbValue *v; + JsonbValue v; bool hasNext = jspGetNext(jsp, &elem); if (!hasNext && !found && jsp->type != jpiVariable) @@ -774,13 +817,11 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, break; } - v = hasNext ? &vbuf : palloc(sizeof(*v)); - baseObject = cxt->baseObject; - getJsonPathItem(cxt, jsp, v); + getJsonPathItem(cxt, jsp, &v); res = executeNextItem(cxt, jsp, &elem, - v, found, hasNext); + &v, found); cxt->baseObject = baseObject; } break; @@ -808,23 +849,23 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiAdd: return executeBinaryArithmExpr(cxt, jsp, jb, - numeric_add_opt_error, found); + numeric_add_safe, found); case jpiSub: return executeBinaryArithmExpr(cxt, jsp, jb, - numeric_sub_opt_error, found); + numeric_sub_safe, found); case jpiMul: return executeBinaryArithmExpr(cxt, jsp, jb, - numeric_mul_opt_error, found); + numeric_mul_safe, found); case jpiDiv: return executeBinaryArithmExpr(cxt, jsp, jb, - numeric_div_opt_error, found); + numeric_div_safe, found); case jpiMod: return executeBinaryArithmExpr(cxt, jsp, jb, - numeric_mod_opt_error, found); + numeric_mod_safe, found); case jpiPlus: return executeUnaryArithmExpr(cxt, jsp, jb, NULL, found); @@ -842,7 +883,7 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, jb, found, jspAutoUnwrap(cxt)); } else if (jspAutoWrap(cxt)) - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, jb, found); else if (!jspIgnoreStructuralErrors(cxt)) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_SQL_JSON_ARRAY_NOT_FOUND), @@ -931,12 +972,10 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, for (index = index_from; index <= index_to; index++) { JsonbValue *v; - bool copy; if (singleton) { v = jb; - copy = true; } else { @@ -945,15 +984,12 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (v == NULL) continue; - - copy = false; } if (!hasNext && !found) return jperOk; - res = executeNextItem(cxt, jsp, &elem, v, found, - copy); + res = executeNextItem(cxt, jsp, &elem, v, found); if (jperIsError(res)) break; @@ -991,7 +1027,7 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, savedIgnoreStructuralErrors = cxt->ignoreStructuralErrors; cxt->ignoreStructuralErrors = true; res = executeNextItem(cxt, jsp, &elem, - jb, found, true); + jb, found); cxt->ignoreStructuralErrors = savedIgnoreStructuralErrors; if (res == jperOk && !found) @@ -1024,11 +1060,8 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (v != NULL) { res = executeNextItem(cxt, jsp, NULL, - v, found, false); - - /* free value if it was not added to found list */ - if (jspHasNext(jsp) || !found) - pfree(v); + v, found); + pfree(v); } else if (!jspIgnoreStructuralErrors(cxt)) { @@ -1056,14 +1089,13 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, break; case jpiCurrent: - res = executeNextItem(cxt, jsp, NULL, cxt->current, - found, true); + res = executeNextItem(cxt, jsp, NULL, cxt->current, found); break; case jpiRoot: jb = cxt->root; baseObject = setBaseObject(cxt, jb, 0); - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, jb, found); cxt->baseObject = baseObject; break; @@ -1081,26 +1113,26 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, res = jperNotFound; else res = executeNextItem(cxt, jsp, NULL, - jb, found, true); + jb, found); break; } case jpiType: { - JsonbValue *jbv = palloc(sizeof(*jbv)); + JsonbValue jbv; - jbv->type = jbvString; - jbv->val.string.val = pstrdup(JsonbTypeName(jb)); - jbv->val.string.len = strlen(jbv->val.string.val); + jbv.type = jbvString; + jbv.val.string.val = pstrdup(JsonbTypeName(jb)); + jbv.val.string.len = strlen(jbv.val.string.val); - res = executeNextItem(cxt, jsp, NULL, jbv, - found, false); + res = executeNextItem(cxt, jsp, NULL, &jbv, found); } break; case jpiSize: { int size = JsonbArraySize(jb); + JsonbValue jbv; if (size < 0) { @@ -1117,12 +1149,10 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, size = 1; } - jb = palloc(sizeof(*jb)); - - jb->type = jbvNumeric; - jb->val.numeric = int64_to_numeric(size); + jbv.type = jbvNumeric; + jbv.val.numeric = int64_to_numeric(size); - res = executeNextItem(cxt, jsp, NULL, jb, found, false); + res = executeNextItem(cxt, jsp, NULL, &jbv, found); } break; @@ -1209,7 +1239,7 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, errmsg("jsonpath item method .%s() can only be applied to a string or numeric value", jspOperationName(jsp->type))))); - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, jb, found); } break; @@ -1232,8 +1262,7 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, case jpiLast: { - JsonbValue tmpjbv; - JsonbValue *lastjbv; + JsonbValue jbv; int last; bool hasNext = jspGetNext(jsp, &elem); @@ -1248,13 +1277,11 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, last = cxt->innermostArraySize - 1; - lastjbv = hasNext ? &tmpjbv : palloc(sizeof(*lastjbv)); - - lastjbv->type = jbvNumeric; - lastjbv->val.numeric = int64_to_numeric(last); + jbv.type = jbvNumeric; + jbv.val.numeric = int64_to_numeric(last); res = executeNextItem(cxt, jsp, &elem, - lastjbv, found, hasNext); + &jbv, found); } break; @@ -1269,11 +1296,12 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (jb->type == jbvNumeric) { - bool have_error; + ErrorSaveContext escontext = {T_ErrorSaveContext}; int64 val; - val = numeric_int8_opt_error(jb->val.numeric, &have_error); - if (have_error) + val = numeric_int8_safe(jb->val.numeric, + (Node *) &escontext); + if (escontext.error_occurred) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_NON_NUMERIC_SQL_JSON_ITEM), errmsg("argument \"%s\" of jsonpath item method .%s() is invalid for type %s", @@ -1312,12 +1340,11 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, errmsg("jsonpath item method .%s() can only be applied to a string or numeric value", jspOperationName(jsp->type))))); - jb = &jbv; - jb->type = jbvNumeric; - jb->val.numeric = DatumGetNumeric(DirectFunctionCall1(int8_numeric, + jbv.type = jbvNumeric; + jbv.val.numeric = DatumGetNumeric(DirectFunctionCall1(int8_numeric, datum)); - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, &jbv, found); } break; @@ -1385,11 +1412,10 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, errmsg("jsonpath item method .%s() can only be applied to a boolean, string, or numeric value", jspOperationName(jsp->type))))); - jb = &jbv; - jb->type = jbvBool; - jb->val.boolean = bval; + jbv.type = jbvBool; + jbv.val.boolean = bval; - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, &jbv, found); } break; @@ -1466,7 +1492,6 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, Datum dtypmod; int32 precision; int32 scale = 0; - bool have_error; bool noerr; ArrayType *arrtypmod; Datum datums[2]; @@ -1478,9 +1503,9 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (elem.type != jpiNumeric) elog(ERROR, "invalid jsonpath item type for .decimal() precision"); - precision = numeric_int4_opt_error(jspGetNumeric(&elem), - &have_error); - if (have_error) + precision = numeric_int4_safe(jspGetNumeric(&elem), + (Node *) &escontext); + if (escontext.error_occurred) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_NON_NUMERIC_SQL_JSON_ITEM), errmsg("precision of jsonpath item method .%s() is out of range for type integer", @@ -1492,9 +1517,9 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (elem.type != jpiNumeric) elog(ERROR, "invalid jsonpath item type for .decimal() scale"); - scale = numeric_int4_opt_error(jspGetNumeric(&elem), - &have_error); - if (have_error) + scale = numeric_int4_safe(jspGetNumeric(&elem), + (Node *) &escontext); + if (escontext.error_occurred) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_NON_NUMERIC_SQL_JSON_ITEM), errmsg("scale of jsonpath item method .%s() is out of range for type integer", @@ -1517,7 +1542,7 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, /* Convert numstr to Numeric with typmod */ Assert(numstr != NULL); noerr = DirectInputFunctionCallSafe(numeric_in, numstr, - InvalidOid, dtypmod, + InvalidOid, DatumGetInt32(dtypmod), (Node *) &escontext, &numdatum); @@ -1531,11 +1556,10 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, pfree(arrtypmod); } - jb = &jbv; - jb->type = jbvNumeric; - jb->val.numeric = num; + jbv.type = jbvNumeric; + jbv.val.numeric = num; - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, &jbv, found); } break; @@ -1550,11 +1574,12 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, if (jb->type == jbvNumeric) { - bool have_error; int32 val; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - val = numeric_int4_opt_error(jb->val.numeric, &have_error); - if (have_error) + val = numeric_int4_safe(jb->val.numeric, + (Node *) &escontext); + if (escontext.error_occurred) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_NON_NUMERIC_SQL_JSON_ITEM), errmsg("argument \"%s\" of jsonpath item method .%s() is invalid for type %s", @@ -1592,12 +1617,11 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, errmsg("jsonpath item method .%s() can only be applied to a string or numeric value", jspOperationName(jsp->type))))); - jb = &jbv; - jb->type = jbvNumeric; - jb->val.numeric = DatumGetNumeric(DirectFunctionCall1(int4_numeric, + jbv.type = jbvNumeric; + jbv.val.numeric = DatumGetNumeric(DirectFunctionCall1(int4_numeric, datum)); - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + res = executeNextItem(cxt, jsp, NULL, &jbv, found); } break; @@ -1649,13 +1673,28 @@ executeItemOptUnwrapTarget(JsonPathExecContext *cxt, JsonPathItem *jsp, break; } - jb = &jbv; Assert(tmp != NULL); /* We must have set tmp above */ - jb->val.string.val = tmp; - jb->val.string.len = strlen(jb->val.string.val); - jb->type = jbvString; + jbv.val.string.val = tmp; + jbv.val.string.len = strlen(jbv.val.string.val); + jbv.type = jbvString; + + res = executeNextItem(cxt, jsp, NULL, &jbv, found); + } + break; - res = executeNextItem(cxt, jsp, NULL, jb, found, true); + case jpiStrReplace: + case jpiStrLower: + case jpiStrUpper: + case jpiStrLtrim: + case jpiStrRtrim: + case jpiStrBtrim: + case jpiStrInitcap: + case jpiStrSplitPart: + { + if (unwrap && JsonbType(jb) == jbvArray) + return executeItemUnwrapTargetArray(cxt, jsp, jb, found, false); + + return executeStringInternalMethod(cxt, jsp, jb, found); } break; @@ -1692,7 +1731,7 @@ executeItemUnwrapTargetArray(JsonPathExecContext *cxt, JsonPathItem *jsp, static JsonPathExecResult executeNextItem(JsonPathExecContext *cxt, JsonPathItem *cur, JsonPathItem *next, - JsonbValue *v, JsonValueList *found, bool copy) + JsonbValue *v, JsonValueList *found) { JsonPathItem elem; bool hasNext; @@ -1711,7 +1750,7 @@ executeNextItem(JsonPathExecContext *cxt, return executeItem(cxt, next, v, found); if (found) - JsonValueListAppend(found, copy ? copyJsonbValue(v) : v); + JsonValueListAppend(found, v); return jperOk; } @@ -1727,16 +1766,23 @@ executeItemOptUnwrapResult(JsonPathExecContext *cxt, JsonPathItem *jsp, { if (unwrap && jspAutoUnwrap(cxt)) { - JsonValueList seq = {0}; + JsonValueList seq; JsonValueListIterator it; - JsonPathExecResult res = executeItem(cxt, jsp, jb, &seq); + JsonPathExecResult res; JsonbValue *item; + JsonValueListInit(&seq); + + res = executeItem(cxt, jsp, jb, &seq); + if (jperIsError(res)) + { + JsonValueListClear(&seq); return res; + } JsonValueListInitIterator(&seq, &it); - while ((item = JsonValueListNext(&seq, &it))) + while ((item = JsonValueListNext(&it))) { Assert(item->type != jbvArray); @@ -1746,6 +1792,8 @@ executeItemOptUnwrapResult(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonValueListAppend(found, item); } + JsonValueListClear(&seq); + return jperOk; } @@ -1876,15 +1924,22 @@ executeBoolItem(JsonPathExecContext *cxt, JsonPathItem *jsp, * In strict mode we must get a complete list of values to * check that there are no errors at all. */ - JsonValueList vals = {0}; - JsonPathExecResult res = - executeItemOptUnwrapResultNoThrow(cxt, &larg, jb, - false, &vals); + JsonValueList vals; + JsonPathExecResult res; + bool isempty; + + JsonValueListInit(&vals); + + res = executeItemOptUnwrapResultNoThrow(cxt, &larg, jb, + false, &vals); + + isempty = JsonValueListIsEmpty(&vals); + JsonValueListClear(&vals); if (jperIsError(res)) return jpbUnknown; - return JsonValueListIsEmpty(&vals) ? jpbFalse : jpbTrue; + return isempty ? jpbFalse : jpbTrue; } else { @@ -1986,7 +2041,7 @@ executeAnyItem(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbContainer *jbc, break; } else if (found) - JsonValueListAppend(found, copyJsonbValue(&v)); + JsonValueListAppend(found, &v); else return jperOk; } @@ -2028,16 +2083,22 @@ executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, { JsonPathExecResult res; JsonValueListIterator lseqit; - JsonValueList lseq = {0}; - JsonValueList rseq = {0}; + JsonValueList lseq; + JsonValueList rseq; JsonbValue *lval; bool error = false; bool found = false; + JsonValueListInit(&lseq); + JsonValueListInit(&rseq); + /* Left argument is always auto-unwrapped. */ res = executeItemOptUnwrapResultNoThrow(cxt, larg, jb, true, &lseq); if (jperIsError(res)) - return jpbUnknown; + { + error = true; + goto exit; + } if (rarg) { @@ -2045,11 +2106,14 @@ executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, res = executeItemOptUnwrapResultNoThrow(cxt, rarg, jb, unwrapRightArg, &rseq); if (jperIsError(res)) - return jpbUnknown; + { + error = true; + goto exit; + } } JsonValueListInitIterator(&lseq, &lseqit); - while ((lval = JsonValueListNext(&lseq, &lseqit))) + while ((lval = JsonValueListNext(&lseqit))) { JsonValueListIterator rseqit; JsonbValue *rval; @@ -2057,7 +2121,7 @@ executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, JsonValueListInitIterator(&rseq, &rseqit); if (rarg) - rval = JsonValueListNext(&rseq, &rseqit); + rval = JsonValueListNext(&rseqit); else rval = NULL; @@ -2068,25 +2132,30 @@ executePredicate(JsonPathExecContext *cxt, JsonPathItem *pred, if (res == jpbUnknown) { - if (jspStrictAbsenceOfErrors(cxt)) - return jpbUnknown; - error = true; + if (jspStrictAbsenceOfErrors(cxt)) + { + found = false; /* return unknown, not success */ + goto exit; + } } else if (res == jpbTrue) { - if (!jspStrictAbsenceOfErrors(cxt)) - return jpbTrue; - found = true; + if (!jspStrictAbsenceOfErrors(cxt)) + goto exit; } first = false; if (rarg) - rval = JsonValueListNext(&rseq, &rseqit); + rval = JsonValueListNext(&rseqit); } } +exit: + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); + if (found) /* possible only in strict mode */ return jpbTrue; @@ -2107,12 +2176,16 @@ executeBinaryArithmExpr(JsonPathExecContext *cxt, JsonPathItem *jsp, { JsonPathExecResult jper; JsonPathItem elem; - JsonValueList lseq = {0}; - JsonValueList rseq = {0}; + JsonValueList lseq; + JsonValueList rseq; JsonbValue *lval; JsonbValue *rval; + JsonbValue resval; Numeric res; + JsonValueListInit(&lseq); + JsonValueListInit(&rseq); + jspGetLeftArg(jsp, &elem); /* @@ -2121,27 +2194,43 @@ executeBinaryArithmExpr(JsonPathExecContext *cxt, JsonPathItem *jsp, */ jper = executeItemOptUnwrapResult(cxt, &elem, jb, true, &lseq); if (jperIsError(jper)) + { + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); return jper; + } jspGetRightArg(jsp, &elem); jper = executeItemOptUnwrapResult(cxt, &elem, jb, true, &rseq); if (jperIsError(jper)) + { + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); return jper; + } - if (JsonValueListLength(&lseq) != 1 || + if (!JsonValueListIsSingleton(&lseq) || !(lval = getScalar(JsonValueListHead(&lseq), jbvNumeric))) + { + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_SINGLETON_SQL_JSON_ITEM_REQUIRED), errmsg("left operand of jsonpath operator %s is not a single numeric value", jspOperationName(jsp->type))))); + } - if (JsonValueListLength(&rseq) != 1 || + if (!JsonValueListIsSingleton(&rseq) || !(rval = getScalar(JsonValueListHead(&rseq), jbvNumeric))) + { + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_SINGLETON_SQL_JSON_ITEM_REQUIRED), errmsg("right operand of jsonpath operator %s is not a single numeric value", jspOperationName(jsp->type))))); + } if (jspThrowErrors(cxt)) { @@ -2149,22 +2238,28 @@ executeBinaryArithmExpr(JsonPathExecContext *cxt, JsonPathItem *jsp, } else { - bool error = false; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - res = func(lval->val.numeric, rval->val.numeric, &error); + res = func(lval->val.numeric, rval->val.numeric, (Node *) &escontext); - if (error) + if (escontext.error_occurred) + { + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); return jperError; + } } + JsonValueListClear(&lseq); + JsonValueListClear(&rseq); + if (!jspGetNext(jsp, &elem) && !found) return jperOk; - lval = palloc(sizeof(*lval)); - lval->type = jbvNumeric; - lval->val.numeric = res; + resval.type = jbvNumeric; + resval.val.numeric = res; - return executeNextItem(cxt, jsp, &elem, lval, found, false); + return executeNextItem(cxt, jsp, &elem, &resval, found); } /* @@ -2178,34 +2273,40 @@ executeUnaryArithmExpr(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonPathExecResult jper; JsonPathExecResult jper2; JsonPathItem elem; - JsonValueList seq = {0}; + JsonValueList seq; JsonValueListIterator it; JsonbValue *val; bool hasNext; + JsonValueListInit(&seq); + jspGetArg(jsp, &elem); jper = executeItemOptUnwrapResult(cxt, &elem, jb, true, &seq); if (jperIsError(jper)) - return jper; + goto exit; jper = jperNotFound; hasNext = jspGetNext(jsp, &elem); JsonValueListInitIterator(&seq, &it); - while ((val = JsonValueListNext(&seq, &it))) + while ((val = JsonValueListNext(&it))) { if ((val = getScalar(val, jbvNumeric))) { if (!found && !hasNext) - return jperOk; + { + jper = jperOk; + goto exit; + } } else { if (!found && !hasNext) continue; /* skip non-numerics processing */ + JsonValueListClear(&seq); RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_SQL_JSON_NUMBER_NOT_FOUND), errmsg("operand of unary jsonpath operator %s is not a numeric value", @@ -2217,19 +2318,25 @@ executeUnaryArithmExpr(JsonPathExecContext *cxt, JsonPathItem *jsp, DatumGetNumeric(DirectFunctionCall1(func, NumericGetDatum(val->val.numeric))); - jper2 = executeNextItem(cxt, jsp, &elem, val, found, false); + jper2 = executeNextItem(cxt, jsp, &elem, val, found); if (jperIsError(jper2)) - return jper2; + { + jper = jper2; + goto exit; + } if (jper2 == jperOk) { - if (!found) - return jperOk; jper = jperOk; + if (!found) + goto exit; } } +exit: + JsonValueListClear(&seq); + return jper; } @@ -2300,6 +2407,7 @@ executeNumericItemMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, { JsonPathItem next; Datum datum; + JsonbValue jbv; if (unwrap && JsonbType(jb) == jbvArray) return executeItemUnwrapTargetArray(cxt, jsp, jb, found, false); @@ -2315,11 +2423,10 @@ executeNumericItemMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, if (!jspGetNext(jsp, &next) && !found) return jperOk; - jb = palloc(sizeof(*jb)); - jb->type = jbvNumeric; - jb->val.numeric = DatumGetNumeric(datum); + jbv.type = jbvNumeric; + jbv.val.numeric = DatumGetNumeric(datum); - return executeNextItem(cxt, jsp, &next, jb, found, false); + return executeNextItem(cxt, jsp, &next, &jbv, found); } /* @@ -2338,7 +2445,7 @@ static JsonPathExecResult executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, JsonValueList *found) { - JsonbValue jbvbuf; + JsonbValue jbv; Datum value; text *datetime; Oid collid; @@ -2433,7 +2540,7 @@ executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, if (jsp->type != jpiDatetime && jsp->type != jpiDate && jsp->content.arg) { - bool have_error; + ErrorSaveContext escontext = {T_ErrorSaveContext}; jspGetArg(jsp, &elem); @@ -2441,9 +2548,9 @@ executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, elog(ERROR, "invalid jsonpath item type for %s argument", jspOperationName(jsp->type)); - time_precision = numeric_int4_opt_error(jspGetNumeric(&elem), - &have_error); - if (have_error) + time_precision = numeric_int4_safe(jspGetNumeric(&elem), + (Node *) &escontext); + if (escontext.error_occurred) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_INVALID_ARGUMENT_FOR_SQL_JSON_DATETIME_FUNCTION), errmsg("time precision of jsonpath item method .%s() is out of range for type integer", @@ -2781,15 +2888,173 @@ executeDateTimeMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, if (!hasNext && !found) return res; - jb = hasNext ? &jbvbuf : palloc(sizeof(*jb)); + jbv.type = jbvDatetime; + jbv.val.datetime.value = value; + jbv.val.datetime.typid = typid; + jbv.val.datetime.typmod = typmod; + jbv.val.datetime.tz = tz; + + return executeNextItem(cxt, jsp, &elem, &jbv, found); +} + +/* + * Implementation of .upper(), .lower() et al. string methods, + * that forward their actual implementation to internal functions. + */ +static JsonPathExecResult +executeStringInternalMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, + JsonbValue *jb, JsonValueList *found) +{ + JsonbValue jbv; + bool hasNext; + JsonPathExecResult res = jperNotFound; + JsonPathItem elem; + Datum str; /* Datum representation for the current string + * value. The first argument to internal + * functions */ + char *resStr = NULL; + + Assert(jsp->type == jpiStrReplace || + jsp->type == jpiStrLower || + jsp->type == jpiStrUpper || + jsp->type == jpiStrLtrim || + jsp->type == jpiStrRtrim || + jsp->type == jpiStrBtrim || + jsp->type == jpiStrInitcap || + jsp->type == jpiStrSplitPart); + + if (!(jb = getScalar(jb, jbvString))) + RETURN_ERROR(ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("jsonpath item method .%s() can only be applied to a string", + jspOperationName(jsp->type))))); + + str = PointerGetDatum(cstring_to_text_with_len(jb->val.string.val, jb->val.string.len)); + + /* Dispatch to the appropriate internal string function */ + switch (jsp->type) + { + case jpiStrReplace: + { + char *from_str, + *to_str; + + jspGetLeftArg(jsp, &elem); + if (elem.type != jpiString) + elog(ERROR, "invalid jsonpath item type for .replace() from"); + + from_str = jspGetString(&elem, NULL); + + jspGetRightArg(jsp, &elem); + if (elem.type != jpiString) + elog(ERROR, "invalid jsonpath item type for .replace() to"); + + to_str = jspGetString(&elem, NULL); + + resStr = TextDatumGetCString(DirectFunctionCall3Coll(replace_text, + DEFAULT_COLLATION_OID, + str, + CStringGetTextDatum(from_str), + CStringGetTextDatum(to_str))); + break; + } + case jpiStrLower: + resStr = TextDatumGetCString(DirectFunctionCall1Coll(lower, DEFAULT_COLLATION_OID, str)); + break; + case jpiStrUpper: + resStr = TextDatumGetCString(DirectFunctionCall1Coll(upper, DEFAULT_COLLATION_OID, str)); + break; + case jpiStrLtrim: + case jpiStrRtrim: + case jpiStrBtrim: + { + PGFunction func1 = NULL; + PGFunction func2 = NULL; + + switch (jsp->type) + { + case jpiStrLtrim: + func1 = ltrim1; + func2 = ltrim; + break; + case jpiStrRtrim: + func1 = rtrim1; + func2 = rtrim; + break; + case jpiStrBtrim: + func1 = btrim1; + func2 = btrim; + break; + default: + break; + } + + if (jsp->content.arg) + { + char *characters_str; - jb->type = jbvDatetime; - jb->val.datetime.value = value; - jb->val.datetime.typid = typid; - jb->val.datetime.typmod = typmod; - jb->val.datetime.tz = tz; + jspGetArg(jsp, &elem); + if (elem.type != jpiString) + elog(ERROR, "invalid jsonpath item type for .%s() argument", + jspOperationName(jsp->type)); - return executeNextItem(cxt, jsp, &elem, jb, found, hasNext); + characters_str = jspGetString(&elem, NULL); + resStr = TextDatumGetCString(DirectFunctionCall2Coll(func2, + DEFAULT_COLLATION_OID, str, + CStringGetTextDatum(characters_str))); + } + else + { + resStr = TextDatumGetCString(DirectFunctionCall1Coll(func1, + DEFAULT_COLLATION_OID, str)); + } + break; + } + + case jpiStrInitcap: + resStr = TextDatumGetCString(DirectFunctionCall1Coll(initcap, DEFAULT_COLLATION_OID, str)); + break; + case jpiStrSplitPart: + { + char *from_str; + Numeric n; + + jspGetLeftArg(jsp, &elem); + if (elem.type != jpiString) + elog(ERROR, "invalid jsonpath item type for .split_part()"); + + from_str = jspGetString(&elem, NULL); + + jspGetRightArg(jsp, &elem); + if (elem.type != jpiNumeric) + elog(ERROR, "invalid jsonpath item type for .split_part()"); + + n = jspGetNumeric(&elem); + + resStr = TextDatumGetCString(DirectFunctionCall3Coll(split_part, + DEFAULT_COLLATION_OID, + str, + CStringGetTextDatum(from_str), + DirectFunctionCall1(numeric_int4, NumericGetDatum(n)))); + break; + } + default: + elog(ERROR, "unsupported jsonpath item type: %d", jsp->type); + } + + if (resStr) + res = jperOk; + + hasNext = jspGetNext(jsp, &elem); + + if (!hasNext && !found) + return res; + + jbv.type = jbvString; + jbv.val.string.val = resStr; + jbv.val.string.len = strlen(resStr); + + return executeNextItem(cxt, jsp, &elem, &jbv, found); } /* @@ -2872,8 +3137,7 @@ executeKeyValueMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, { JsonBaseObjectInfo baseObject; JsonbValue obj; - JsonbParseState *ps; - JsonbValue *keyval; + JsonbInState ps; Jsonb *jsonb; if (tok != WJB_KEY) @@ -2887,7 +3151,8 @@ executeKeyValueMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, tok = JsonbIteratorNext(&it, &val, true); Assert(tok == WJB_VALUE); - ps = NULL; + memset(&ps, 0, sizeof(ps)); + pushJsonbValue(&ps, WJB_BEGIN_OBJECT, NULL); pushJsonbValue(&ps, WJB_KEY, &keystr); @@ -2899,15 +3164,15 @@ executeKeyValueMethod(JsonPathExecContext *cxt, JsonPathItem *jsp, pushJsonbValue(&ps, WJB_KEY, &idstr); pushJsonbValue(&ps, WJB_VALUE, &idval); - keyval = pushJsonbValue(&ps, WJB_END_OBJECT, NULL); + pushJsonbValue(&ps, WJB_END_OBJECT, NULL); - jsonb = JsonbValueToJsonb(keyval); + jsonb = JsonbValueToJsonb(ps.result); JsonbInitBinary(&obj, jsonb); baseObject = setBaseObject(cxt, &obj, cxt->lastGeneratedObjectId++); - res = executeNextItem(cxt, jsp, &next, &obj, found, true); + res = executeNextItem(cxt, jsp, &next, &obj, found); cxt->baseObject = baseObject; @@ -2945,7 +3210,7 @@ appendBoolResult(JsonPathExecContext *cxt, JsonPathItem *jsp, jbv.val.boolean = res == jpbTrue; } - return executeNextItem(cxt, jsp, &next, &jbv, found, true); + return executeNextItem(cxt, jsp, &next, &jbv, found); } /* @@ -3016,7 +3281,7 @@ GetJsonPathVar(void *cxt, char *varName, int varNameLen, return NULL; } - result = palloc(sizeof(JsonbValue)); + result = palloc_object(JsonbValue); if (var->isnull) { *baseObjectId = 0; @@ -3074,8 +3339,8 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) case TEXTOID: case VARCHAROID: res->type = jbvString; - res->val.string.val = VARDATA_ANY(val); - res->val.string.len = VARSIZE_ANY_EXHDR(val); + res->val.string.val = VARDATA_ANY(DatumGetPointer(val)); + res->val.string.len = VARSIZE_ANY_EXHDR(DatumGetPointer(val)); break; case DATEOID: case TIMEOID: @@ -3443,7 +3708,7 @@ compareNumeric(Numeric a, Numeric b) static JsonbValue * copyJsonbValue(JsonbValue *src) { - JsonbValue *dst = palloc(sizeof(*dst)); + JsonbValue *dst = palloc_object(JsonbValue); *dst = *src; @@ -3459,28 +3724,40 @@ getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index) { JsonbValue *jbv; - JsonValueList found = {0}; - JsonPathExecResult res = executeItem(cxt, jsp, jb, &found); + JsonValueList found; + JsonPathExecResult res; Datum numeric_index; - bool have_error = false; + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + JsonValueListInit(&found); + + res = executeItem(cxt, jsp, jb, &found); if (jperIsError(res)) + { + JsonValueListClear(&found); return res; + } - if (JsonValueListLength(&found) != 1 || + if (!JsonValueListIsSingleton(&found) || !(jbv = getScalar(JsonValueListHead(&found), jbvNumeric))) + { + JsonValueListClear(&found); RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_INVALID_SQL_JSON_SUBSCRIPT), errmsg("jsonpath array subscript is not a single numeric value")))); + } numeric_index = DirectFunctionCall2(numeric_trunc, NumericGetDatum(jbv->val.numeric), Int32GetDatum(0)); - *index = numeric_int4_opt_error(DatumGetNumeric(numeric_index), - &have_error); + *index = numeric_int4_safe(DatumGetNumeric(numeric_index), + (Node *) &escontext); + + JsonValueListClear(&found); - if (have_error) + if (escontext.error_occurred) RETURN_ERROR(ereport(ERROR, (errcode(ERRCODE_INVALID_SQL_JSON_SUBSCRIPT), errmsg("jsonpath array subscript is out of integer range")))); @@ -3501,96 +3778,132 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +/* + * JsonValueList support functions + */ + +static void +JsonValueListInit(JsonValueList *jvl) +{ + jvl->nitems = 0; + jvl->maxitems = BASE_JVL_ITEMS; + jvl->next = NULL; + jvl->last = jvl; +} + static void JsonValueListClear(JsonValueList *jvl) { - jvl->singleton = NULL; - jvl->list = NIL; + JsonValueList *nxt; + + /* Release any extra chunks */ + for (JsonValueList *chunk = jvl->next; chunk != NULL; chunk = nxt) + { + nxt = chunk->next; + pfree(chunk); + } + /* ... and reset to empty */ + jvl->nitems = 0; + Assert(jvl->maxitems == BASE_JVL_ITEMS); + jvl->next = NULL; + jvl->last = jvl; } static void -JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) +JsonValueListAppend(JsonValueList *jvl, const JsonbValue *jbv) { - if (jvl->singleton) + JsonValueList *last = jvl->last; + + if (last->nitems < last->maxitems) { - jvl->list = list_make2(jvl->singleton, jbv); - jvl->singleton = NULL; + /* there's still room in the last existing chunk */ + last->items[last->nitems] = *jbv; + last->nitems++; } - else if (!jvl->list) - jvl->singleton = jbv; else - jvl->list = lappend(jvl->list, jbv); + { + /* need a new last chunk */ + JsonValueList *nxt; + int nxtsize; + + nxtsize = last->maxitems * 2; /* double the size with each chunk */ + nxtsize = Max(nxtsize, MIN_EXTRA_JVL_ITEMS); /* but at least this */ + nxt = palloc(offsetof(JsonValueList, items) + + nxtsize * sizeof(JsonbValue)); + nxt->nitems = 1; + nxt->maxitems = nxtsize; + nxt->next = NULL; + nxt->items[0] = *jbv; + last->next = nxt; + jvl->last = nxt; + } } -static int -JsonValueListLength(const JsonValueList *jvl) +static bool +JsonValueListIsEmpty(const JsonValueList *jvl) { - return jvl->singleton ? 1 : list_length(jvl->list); + /* We need not examine extra chunks for this */ + return (jvl->nitems == 0); } static bool -JsonValueListIsEmpty(JsonValueList *jvl) +JsonValueListIsSingleton(const JsonValueList *jvl) { - return !jvl->singleton && (jvl->list == NIL); +#if BASE_JVL_ITEMS > 1 + /* We need not examine extra chunks in this case */ + return (jvl->nitems == 1); +#else + return (jvl->nitems == 1 && jvl->next == NULL); +#endif } -static JsonbValue * -JsonValueListHead(JsonValueList *jvl) +static bool +JsonValueListHasMultipleItems(const JsonValueList *jvl) { - return jvl->singleton ? jvl->singleton : linitial(jvl->list); +#if BASE_JVL_ITEMS > 1 + /* We need not examine extra chunks in this case */ + return (jvl->nitems > 1); +#else + return (jvl->nitems == 1 && jvl->next != NULL); +#endif } -static List * -JsonValueListGetList(JsonValueList *jvl) +static JsonbValue * +JsonValueListHead(JsonValueList *jvl) { - if (jvl->singleton) - return list_make1(jvl->singleton); - - return jvl->list; + Assert(jvl->nitems > 0); + return &jvl->items[0]; } +/* + * JsonValueListIterator functions + */ + static void -JsonValueListInitIterator(const JsonValueList *jvl, JsonValueListIterator *it) +JsonValueListInitIterator(JsonValueList *jvl, JsonValueListIterator *it) { - if (jvl->singleton) - { - it->value = jvl->singleton; - it->list = NIL; - it->next = NULL; - } - else if (jvl->list != NIL) - { - it->value = (JsonbValue *) linitial(jvl->list); - it->list = jvl->list; - it->next = list_second_cell(jvl->list); - } - else - { - it->value = NULL; - it->list = NIL; - it->next = NULL; - } + it->chunk = jvl; + it->nextitem = 0; } /* * Get the next item from the sequence advancing iterator. + * Returns NULL if no more items. */ static JsonbValue * -JsonValueListNext(const JsonValueList *jvl, JsonValueListIterator *it) +JsonValueListNext(JsonValueListIterator *it) { - JsonbValue *result = it->value; - - if (it->next) - { - it->value = lfirst(it->next); - it->next = lnext(it->list, it->next); - } - else + if (it->chunk == NULL) + return NULL; + if (it->nextitem >= it->chunk->nitems) { - it->value = NULL; + it->chunk = it->chunk->next; + if (it->chunk == NULL) + return NULL; + it->nextitem = 0; + Assert(it->chunk->nitems > 0); } - - return result; + return &it->chunk->items[it->nextitem++]; } /* @@ -3645,19 +3958,21 @@ getScalar(JsonbValue *scalar, enum jbvType type) /* Construct a JSON array from the item list */ static JsonbValue * -wrapItemsInArray(const JsonValueList *items) +wrapItemsInArray(JsonValueList *items) { - JsonbParseState *ps = NULL; + JsonbInState ps = {0}; JsonValueListIterator it; JsonbValue *jbv; pushJsonbValue(&ps, WJB_BEGIN_ARRAY, NULL); JsonValueListInitIterator(items, &it); - while ((jbv = JsonValueListNext(items, &it))) + while ((jbv = JsonValueListNext(&it))) pushJsonbValue(&ps, WJB_ELEM, jbv); - return pushJsonbValue(&ps, WJB_END_ARRAY, NULL); + pushJsonbValue(&ps, WJB_END_ARRAY, NULL); + + return ps.result; } /* Check if the timezone required for casting from type1 to type2 is used */ @@ -3911,11 +4226,11 @@ JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, bool *error, List *vars, const char *column_name) { - JsonbValue *singleton; bool wrap; - JsonValueList found = {0}; + JsonValueList found; JsonPathExecResult res; - int count; + + JsonValueListInit(&found); res = executeJsonPath(jp, vars, GetJsonPathVar, CountJsonPathVars, @@ -3931,9 +4246,7 @@ JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, /* * Determine whether to wrap the result in a JSON array or not. * - * First, count the number of SQL/JSON items in the returned - * JsonValueList. If the list is empty (singleton == NULL), no wrapping is - * necessary. + * If the returned JsonValueList is empty, no wrapping is necessary. * * If the wrapper mode is JSW_NONE or JSW_UNSPEC, wrapping is explicitly * disabled. This enforces a WITHOUT WRAPPER clause, which is also the @@ -3946,16 +4259,14 @@ JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, * For JSW_CONDITIONAL, wrapping occurs only if there is more than one * SQL/JSON item in the list, enforcing a WITH CONDITIONAL WRAPPER clause. */ - count = JsonValueListLength(&found); - singleton = count > 0 ? JsonValueListHead(&found) : NULL; - if (singleton == NULL) + if (JsonValueListIsEmpty(&found)) wrap = false; else if (wrapper == JSW_NONE || wrapper == JSW_UNSPEC) wrap = false; else if (wrapper == JSW_UNCONDITIONAL) wrap = true; else if (wrapper == JSW_CONDITIONAL) - wrap = count > 1; + wrap = JsonValueListHasMultipleItems(&found); else { elog(ERROR, "unrecognized json wrapper %d", (int) wrapper); @@ -3965,8 +4276,8 @@ JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, if (wrap) return JsonbPGetDatum(JsonbValueToJsonb(wrapItemsInArray(&found))); - /* No wrapping means only one item is expected. */ - if (count > 1) + /* No wrapping means at most one item is expected. */ + if (JsonValueListHasMultipleItems(&found)) { if (error) { @@ -3987,8 +4298,8 @@ JsonPathQuery(Datum jb, JsonPath *jp, JsonWrapper wrapper, bool *empty, errhint("Use the WITH WRAPPER clause to wrap SQL/JSON items into an array."))); } - if (singleton) - return JsonbPGetDatum(JsonbValueToJsonb(singleton)); + if (!JsonValueListIsEmpty(&found)) + return JsonbPGetDatum(JsonbValueToJsonb(JsonValueListHead(&found))); *empty = true; return PointerGetDatum(NULL); @@ -4005,9 +4316,10 @@ JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars, const char *column_name) { JsonbValue *res; - JsonValueList found = {0}; + JsonValueList found; JsonPathExecResult jper PG_USED_FOR_ASSERTS_ONLY; - int count; + + JsonValueListInit(&found); jper = executeJsonPath(jp, vars, GetJsonPathVar, CountJsonPathVars, DatumGetJsonbP(jb), @@ -4022,15 +4334,13 @@ JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars, return NULL; } - count = JsonValueListLength(&found); - - *empty = (count == 0); + *empty = JsonValueListIsEmpty(&found); if (*empty) return NULL; /* JSON_VALUE expects to get only singletons. */ - if (count > 1) + if (JsonValueListHasMultipleItems(&found)) { if (error) { @@ -4049,7 +4359,7 @@ JsonPathValue(Datum jb, JsonPath *jp, bool *empty, bool *error, List *vars, errmsg("JSON path expression in JSON_VALUE must return single scalar item"))); } - res = JsonValueListHead(&found); + res = copyJsonbValue(JsonValueListHead(&found)); if (res->type == jbvBinary && JsonContainerIsScalar(res->val.binary.data)) JsonbExtractScalar(res->val.binary.data, res); @@ -4117,7 +4427,7 @@ JsonTableInitOpaque(TableFuncScanState *state, int natts) JsonExpr *je = castNode(JsonExpr, tf->docexpr); List *args = NIL; - cxt = palloc0(sizeof(JsonTableExecContext)); + cxt = palloc0_object(JsonTableExecContext); cxt->magic = JSON_TABLE_EXEC_CONTEXT_MAGIC; /* @@ -4136,7 +4446,7 @@ JsonTableInitOpaque(TableFuncScanState *state, int natts) { ExprState *state = lfirst_node(ExprState, exprlc); String *name = lfirst_node(String, namelc); - JsonPathVariable *var = palloc(sizeof(*var)); + JsonPathVariable *var = palloc_object(JsonPathVariable); var->name = pstrdup(name->sval); var->namelen = strlen(var->name); @@ -4154,8 +4464,7 @@ JsonTableInitOpaque(TableFuncScanState *state, int natts) } } - cxt->colplanstates = palloc(sizeof(JsonTablePlanState *) * - list_length(tf->colvalexprs)); + cxt->colplanstates = palloc_array(JsonTablePlanState *, list_length(tf->colvalexprs)); /* * Initialize plan for the root path and, recursively, also any child @@ -4193,10 +4502,11 @@ JsonTableInitPlan(JsonTableExecContext *cxt, JsonTablePlan *plan, JsonTablePlanState *parentstate, List *args, MemoryContext mcxt) { - JsonTablePlanState *planstate = palloc0(sizeof(*planstate)); + JsonTablePlanState *planstate = palloc0_object(JsonTablePlanState); planstate->plan = plan; planstate->parent = parentstate; + JsonValueListInit(&planstate->found); if (IsA(plan, JsonTablePathScan)) { @@ -4332,7 +4642,7 @@ JsonTablePlanScanNextRow(JsonTablePlanState *planstate) } /* Fetch new row from the list of found values to set as active. */ - jbv = JsonValueListNext(&planstate->found, &planstate->iter); + jbv = JsonValueListNext(&planstate->iter); /* End of list? */ if (jbv == NULL) @@ -4419,7 +4729,7 @@ JsonTablePlanJoinNextRow(JsonTablePlanState *planstate) */ if (!JsonTablePlanNextRow(planstate->right)) { - /* Right sibling ran out of row, so there are more rows. */ + /* Right sibling ran out of rows too, so there are no more rows. */ return false; } } diff --git a/src/backend/utils/adt/jsonpath_gram.y b/src/backend/utils/adt/jsonpath_gram.y index 499745a8fef65..f826697d098b7 100644 --- a/src/backend/utils/adt/jsonpath_gram.y +++ b/src/backend/utils/adt/jsonpath_gram.y @@ -6,7 +6,7 @@ * * Transforms tokenized jsonpath into tree of JsonPathParseItem structs. * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/jsonpath_gram.y @@ -86,16 +86,18 @@ static bool makeItemLikeRegex(JsonPathParseItem *expr, %token DATETIME_P %token BIGINT_P BOOLEAN_P DATE_P DECIMAL_P INTEGER_P NUMBER_P %token STRINGFUNC_P TIME_P TIME_TZ_P TIMESTAMP_P TIMESTAMP_TZ_P +%token STR_REPLACE_P STR_LOWER_P STR_UPPER_P STR_LTRIM_P STR_RTRIM_P STR_BTRIM_P + STR_INITCAP_P STR_SPLIT_PART_P %type result %type scalar_value path_primary expr array_accessor any_path accessor_op key predicate delimited_predicate index_elem starts_with_initial expr_or_predicate - datetime_template opt_datetime_template csv_elem - datetime_precision opt_datetime_precision + str_elem opt_str_arg int_elem + uint_elem opt_uint_arg -%type accessor_expr csv_list opt_csv_list +%type accessor_expr int_list opt_int_list str_int_args str_str_args %type index_list @@ -120,7 +122,7 @@ static bool makeItemLikeRegex(JsonPathParseItem *expr, result: mode expr_or_predicate { - *result = palloc(sizeof(JsonPathParseResult)); + *result = palloc_object(JsonPathParseResult); (*result)->expr = $2; (*result)->lax = $1; (void) yynerrs; @@ -254,7 +256,7 @@ accessor_op: | '.' any_path { $$ = $2; } | '.' method '(' ')' { $$ = makeItemType($2); } | '?' '(' predicate ')' { $$ = makeItemUnary(jpiFilter, $3); } - | '.' DECIMAL_P '(' opt_csv_list ')' + | '.' DECIMAL_P '(' opt_int_list ')' { if (list_length($4) == 0) $$ = makeItemBinary(jpiDecimal, NULL, NULL); @@ -268,19 +270,29 @@ accessor_op: errmsg("invalid input syntax for type %s", "jsonpath"), errdetail(".decimal() can only have an optional precision[,scale]."))); } - | '.' DATETIME_P '(' opt_datetime_template ')' + | '.' DATETIME_P '(' opt_str_arg ')' { $$ = makeItemUnary(jpiDatetime, $4); } - | '.' TIME_P '(' opt_datetime_precision ')' + | '.' TIME_P '(' opt_uint_arg ')' { $$ = makeItemUnary(jpiTime, $4); } - | '.' TIME_TZ_P '(' opt_datetime_precision ')' + | '.' TIME_TZ_P '(' opt_uint_arg ')' { $$ = makeItemUnary(jpiTimeTz, $4); } - | '.' TIMESTAMP_P '(' opt_datetime_precision ')' + | '.' TIMESTAMP_P '(' opt_uint_arg ')' { $$ = makeItemUnary(jpiTimestamp, $4); } - | '.' TIMESTAMP_TZ_P '(' opt_datetime_precision ')' + | '.' TIMESTAMP_TZ_P '(' opt_uint_arg ')' { $$ = makeItemUnary(jpiTimestampTz, $4); } + | '.' STR_REPLACE_P '(' str_str_args ')' + { $$ = makeItemBinary(jpiStrReplace, linitial($4), lsecond($4)); } + | '.' STR_SPLIT_PART_P '(' str_int_args ')' + { $$ = makeItemBinary(jpiStrSplitPart, linitial($4), lsecond($4)); } + | '.' STR_LTRIM_P '(' opt_str_arg ')' + { $$ = makeItemUnary(jpiStrLtrim, $4); } + | '.' STR_RTRIM_P '(' opt_str_arg ')' + { $$ = makeItemUnary(jpiStrRtrim, $4); } + | '.' STR_BTRIM_P '(' opt_str_arg ')' + { $$ = makeItemUnary(jpiStrBtrim, $4); } ; -csv_elem: +int_elem: INT_P { $$ = makeItemNumeric(&$1); } | '+' INT_P %prec UMINUS @@ -289,34 +301,42 @@ csv_elem: { $$ = makeItemUnary(jpiMinus, makeItemNumeric(&$2)); } ; -csv_list: - csv_elem { $$ = list_make1($1); } - | csv_list ',' csv_elem { $$ = lappend($1, $3); } +int_list: + int_elem { $$ = list_make1($1); } + | int_list ',' int_elem { $$ = lappend($1, $3); } ; -opt_csv_list: - csv_list { $$ = $1; } +opt_int_list: + int_list { $$ = $1; } | /* EMPTY */ { $$ = NULL; } ; -datetime_precision: +uint_elem: INT_P { $$ = makeItemNumeric(&$1); } ; -opt_datetime_precision: - datetime_precision { $$ = $1; } +opt_uint_arg: + uint_elem { $$ = $1; } | /* EMPTY */ { $$ = NULL; } ; -datetime_template: +str_elem: STRING_P { $$ = makeItemString(&$1); } ; -opt_datetime_template: - datetime_template { $$ = $1; } +opt_str_arg: + str_elem { $$ = $1; } | /* EMPTY */ { $$ = NULL; } ; +str_int_args: + str_elem ',' int_elem { $$ = list_make2($1, $3); } + ; + +str_str_args: + str_elem ',' str_elem { $$ = list_make2($1, $3); } + ; + key: key_name { $$ = makeItemKey(&$1); } ; @@ -357,6 +377,14 @@ key_name: | TIME_TZ_P | TIMESTAMP_P | TIMESTAMP_TZ_P + | STR_LOWER_P + | STR_UPPER_P + | STR_INITCAP_P + | STR_REPLACE_P + | STR_SPLIT_PART_P + | STR_LTRIM_P + | STR_RTRIM_P + | STR_BTRIM_P ; method: @@ -373,6 +401,9 @@ method: | INTEGER_P { $$ = jpiInteger; } | NUMBER_P { $$ = jpiNumber; } | STRINGFUNC_P { $$ = jpiStringFunc; } + | STR_LOWER_P { $$ = jpiStrLower; } + | STR_UPPER_P { $$ = jpiStrUpper; } + | STR_INITCAP_P { $$ = jpiStrInitcap; } ; %% @@ -384,7 +415,7 @@ method: static JsonPathParseItem * makeItemType(JsonPathItemType type) { - JsonPathParseItem *v = palloc(sizeof(*v)); + JsonPathParseItem *v = palloc_object(JsonPathParseItem); CHECK_FOR_INTERRUPTS(); @@ -599,7 +630,8 @@ makeItemLikeRegex(JsonPathParseItem *expr, JsonPathString *pattern, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid input syntax for type %s", "jsonpath"), errdetail("Unrecognized flag character \"%.*s\" in LIKE_REGEX predicate.", - pg_mblen(flags->val + i), flags->val + i))); + pg_mblen_range(flags->val + i, flags->val + flags->len), + flags->val + i))); break; } } diff --git a/src/backend/utils/adt/jsonpath_internal.h b/src/backend/utils/adt/jsonpath_internal.h index f78069857d02b..b56954c4b928c 100644 --- a/src/backend/utils/adt/jsonpath_internal.h +++ b/src/backend/utils/adt/jsonpath_internal.h @@ -3,7 +3,7 @@ * jsonpath_internal.h * Private definitions for jsonpath scanner & parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/utils/adt/jsonpath_internal.h @@ -22,10 +22,7 @@ typedef struct JsonPathString int total; } JsonPathString; -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif #include "utils/jsonpath.h" #include "jsonpath_gram.h" diff --git a/src/backend/utils/adt/jsonpath_scan.l b/src/backend/utils/adt/jsonpath_scan.l index c7aab83eeb4f6..e4fadcc2e6958 100644 --- a/src/backend/utils/adt/jsonpath_scan.l +++ b/src/backend/utils/adt/jsonpath_scan.l @@ -7,7 +7,7 @@ * Splits jsonpath string into tokens represented as JsonPathString structs. * Decodes unicode and hex escaped strings. * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/jsonpath_scan.l @@ -413,8 +413,13 @@ static const JsonPathKeyword keywords[] = { {4, true, TRUE_P, "true"}, {4, false, TYPE_P, "type"}, {4, false, WITH_P, "with"}, + {5, false, STR_BTRIM_P, "btrim"}, {5, true, FALSE_P, "false"}, {5, false, FLOOR_P, "floor"}, + {5, false, STR_LOWER_P, "lower"}, + {5, false, STR_LTRIM_P, "ltrim"}, + {5, false, STR_RTRIM_P, "rtrim"}, + {5, false, STR_UPPER_P, "upper"}, {6, false, BIGINT_P, "bigint"}, {6, false, DOUBLE_P, "double"}, {6, false, EXISTS_P, "exists"}, @@ -425,13 +430,16 @@ static const JsonPathKeyword keywords[] = { {7, false, BOOLEAN_P, "boolean"}, {7, false, CEILING_P, "ceiling"}, {7, false, DECIMAL_P, "decimal"}, + {7, false, STR_INITCAP_P, "initcap"}, {7, false, INTEGER_P, "integer"}, + {7, false, STR_REPLACE_P, "replace"}, {7, false, TIME_TZ_P, "time_tz"}, {7, false, UNKNOWN_P, "unknown"}, {8, false, DATETIME_P, "datetime"}, {8, false, KEYVALUE_P, "keyvalue"}, {9, false, TIMESTAMP_P, "timestamp"}, {10, false, LIKE_REGEX_P, "like_regex"}, + {10, false, STR_SPLIT_PART_P, "split_part"}, {12, false, TIMESTAMP_TZ_P, "timestamp_tz"}, }; @@ -574,7 +582,7 @@ hexval(char c, int *result, struct Node *escontext, yyscan_t yyscanner) /* Add given unicode character to scanstring */ static bool -addUnicodeChar(int ch, struct Node *escontext, yyscan_t yyscanner) +addUnicodeChar(char32_t ch, struct Node *escontext, yyscan_t yyscanner) { if (ch == 0) { @@ -607,7 +615,7 @@ addUnicodeChar(int ch, struct Node *escontext, yyscan_t yyscanner) /* Add unicode character, processing any surrogate pairs */ static bool -addUnicode(int ch, int *hi_surrogate, struct Node *escontext, yyscan_t yyscanner) +addUnicode(char32_t ch, int *hi_surrogate, struct Node *escontext, yyscan_t yyscanner) { if (is_utf16_surrogate_first(ch)) { @@ -655,7 +663,7 @@ parseUnicode(char *s, int l, struct Node *escontext, yyscan_t yyscanner) for (i = 2; i < l; i += 2) /* skip '\u' */ { - int ch = 0; + char32_t ch = 0; int j, si; diff --git a/src/backend/utils/adt/levenshtein.c b/src/backend/utils/adt/levenshtein.c index 15a90f6f50c8a..5b3d84029f6b9 100644 --- a/src/backend/utils/adt/levenshtein.c +++ b/src/backend/utils/adt/levenshtein.c @@ -16,7 +16,7 @@ * PHP 4.0.6 distribution for inspiration. Configurable penalty costs * extension is introduced by Volkan YAZICI (7/95). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -43,8 +43,8 @@ static text *MB_do_like_escape(text *pat, text *esc); static int UTF8_MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale); -static int SB_IMatchText(const char *t, int tlen, const char *p, int plen, - pg_locale_t locale); +static int C_IMatchText(const char *t, int tlen, const char *p, int plen, + pg_locale_t locale); static int GenericMatchText(const char *s, int slen, const char *p, int plen, Oid collation); static int Generic_Text_IC_like(text *str, text *pat, Oid collation); @@ -55,20 +55,20 @@ static int Generic_Text_IC_like(text *str, text *pat, Oid collation); *-------------------- */ static inline int -wchareq(const char *p1, const char *p2) +wchareq(const char *p1, int p1len, const char *p2, int p2len) { - int p1_len; + int p1clen; /* Optimization: quickly compare the first byte. */ if (*p1 != *p2) return 0; - p1_len = pg_mblen(p1); - if (pg_mblen(p2) != p1_len) + p1clen = pg_mblen_with_len(p1, p1len); + if (pg_mblen_with_len(p2, p2len) != p1clen) return 0; /* They are the same length */ - while (p1_len--) + while (p1clen--) { if (*p1++ != *p2++) return 0; @@ -84,32 +84,20 @@ wchareq(const char *p1, const char *p2) * of getting a single character transformed to the system's wchar_t format. * So now, we just downcase the strings using lower() and apply regular LIKE * comparison. This should be revisited when we install better locale support. - */ - -/* - * We do handle case-insensitive matching for single-byte encodings using + * + * We do handle case-insensitive matching for the C locale using * fold-on-the-fly processing, however. */ -static char -SB_lower_char(unsigned char c, pg_locale_t locale) -{ - if (locale->ctype_is_c) - return pg_ascii_tolower(c); - else if (locale->is_default) - return pg_tolower(c); - else - return tolower_l(c, locale->info.lt); -} #define NextByte(p, plen) ((p)++, (plen)--) /* Set up to compile like_match.c for multibyte characters */ -#define CHAREQ(p1, p2) wchareq((p1), (p2)) +#define CHAREQ(p1, p1len, p2, p2len) wchareq((p1), (p1len), (p2), (p2len)) #define NextChar(p, plen) \ - do { int __l = pg_mblen(p); (p) +=__l; (plen) -=__l; } while (0) + do { int __l = pg_mblen_with_len((p), (plen)); (p) +=__l; (plen) -=__l; } while (0) #define CopyAdvChar(dst, src, srclen) \ - do { int __l = pg_mblen(src); \ + do { int __l = pg_mblen_with_len((src), (srclen)); \ (srclen) -= __l; \ while (__l-- > 0) \ *(dst)++ = *(src)++; \ @@ -121,7 +109,7 @@ SB_lower_char(unsigned char c, pg_locale_t locale) #include "like_match.c" /* Set up to compile like_match.c for single-byte characters */ -#define CHAREQ(p1, p2) (*(p1) == *(p2)) +#define CHAREQ(p1, p1len, p2, p2len) (*(p1) == *(p2)) #define NextChar(p, plen) NextByte((p), (plen)) #define CopyAdvChar(dst, src, srclen) (*(dst)++ = *(src)++, (srclen)--) @@ -130,10 +118,10 @@ SB_lower_char(unsigned char c, pg_locale_t locale) #include "like_match.c" -/* setup to compile like_match.c for single byte case insensitive matches */ -#define MATCH_LOWER(t, locale) SB_lower_char((unsigned char) (t), locale) +/* setup to compile like_match.c for case-insensitive matches in C locale */ +#define MATCH_LOWER #define NextChar(p, plen) NextByte((p), (plen)) -#define MatchText SB_IMatchText +#define MatchText C_IMatchText #include "like_match.c" @@ -202,35 +190,37 @@ Generic_Text_IC_like(text *str, text *pat, Oid collation) errmsg("nondeterministic collations are not supported for ILIKE"))); /* - * For efficiency reasons, in the single byte case we don't call lower() - * on the pattern and text, but instead call SB_lower_char on each - * character. In the multi-byte case we don't have much choice :-(. Also, - * ICU does not support single-character case folding, so we go the long - * way. + * For efficiency reasons, in the C locale we don't call lower() on the + * pattern and text, but instead lowercase each character lazily. + * + * XXX: use casefolding instead? */ - if (pg_database_encoding_max_length() > 1 || (locale->provider == COLLPROVIDER_ICU)) + if (locale->ctype_is_c) { - pat = DatumGetTextPP(DirectFunctionCall1Coll(lower, collation, - PointerGetDatum(pat))); p = VARDATA_ANY(pat); plen = VARSIZE_ANY_EXHDR(pat); - str = DatumGetTextPP(DirectFunctionCall1Coll(lower, collation, - PointerGetDatum(str))); s = VARDATA_ANY(str); slen = VARSIZE_ANY_EXHDR(str); - if (GetDatabaseEncoding() == PG_UTF8) - return UTF8_MatchText(s, slen, p, plen, 0); - else - return MB_MatchText(s, slen, p, plen, 0); + return C_IMatchText(s, slen, p, plen, locale); } else { + pat = DatumGetTextPP(DirectFunctionCall1Coll(lower, collation, + PointerGetDatum(pat))); p = VARDATA_ANY(pat); plen = VARSIZE_ANY_EXHDR(pat); + str = DatumGetTextPP(DirectFunctionCall1Coll(lower, collation, + PointerGetDatum(str))); s = VARDATA_ANY(str); slen = VARSIZE_ANY_EXHDR(str); - return SB_IMatchText(s, slen, p, plen, locale); + + if (GetDatabaseEncoding() == PG_UTF8) + return UTF8_MatchText(s, slen, p, plen, 0); + else if (pg_database_encoding_max_length() > 1) + return MB_MatchText(s, slen, p, plen, 0); + else + return SB_MatchText(s, slen, p, plen, 0); } } diff --git a/src/backend/utils/adt/like_match.c b/src/backend/utils/adt/like_match.c index 892f8a745ea43..f5f72b82e2152 100644 --- a/src/backend/utils/adt/like_match.c +++ b/src/backend/utils/adt/like_match.c @@ -16,7 +16,7 @@ * do_like_escape - name of function if wanted - needs CHAREQ and CopyAdvChar * MATCH_LOWER - define for case (4) to specify case folding for 1-byte chars * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/like_match.c @@ -70,10 +70,14 @@ *-------------------- */ +/* + * MATCH_LOWER is defined for ILIKE in the C locale as an optimization. Other + * locales must casefold the inputs before matching. + */ #ifdef MATCH_LOWER -#define GETCHAR(t, locale) MATCH_LOWER(t, locale) +#define GETCHAR(t) pg_ascii_tolower(t) #else -#define GETCHAR(t, locale) (t) +#define GETCHAR(t) (t) #endif static int @@ -105,7 +109,7 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) ereport(ERROR, (errcode(ERRCODE_INVALID_ESCAPE_SEQUENCE), errmsg("LIKE pattern must not end with escape character"))); - if (GETCHAR(*p, locale) != GETCHAR(*t, locale)) + if (GETCHAR(*p) != GETCHAR(*t)) return LIKE_FALSE; } else if (*p == '%') @@ -167,14 +171,14 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) ereport(ERROR, (errcode(ERRCODE_INVALID_ESCAPE_SEQUENCE), errmsg("LIKE pattern must not end with escape character"))); - firstpat = GETCHAR(p[1], locale); + firstpat = GETCHAR(p[1]); } else - firstpat = GETCHAR(*p, locale); + firstpat = GETCHAR(*p); while (tlen > 0) { - if (GETCHAR(*t, locale) == firstpat || (locale && !locale->deterministic)) + if (GETCHAR(*t) == firstpat || (locale && !locale->deterministic)) { int matched = MatchText(t, tlen, p, plen, locale); @@ -342,7 +346,7 @@ MatchText(const char *t, int tlen, const char *p, int plen, pg_locale_t locale) NextChar(t1, t1len); } } - else if (GETCHAR(*p, locale) != GETCHAR(*t, locale)) + else if (GETCHAR(*p) != GETCHAR(*t)) { /* non-wildcard pattern char fails to match text char */ return LIKE_FALSE; @@ -438,6 +442,7 @@ do_like_escape(text *pat, text *esc) errhint("Escape string must be empty or one character."))); e = VARDATA_ANY(esc); + elen = VARSIZE_ANY_EXHDR(esc); /* * If specified escape is '\', just copy the pattern as-is. @@ -456,7 +461,7 @@ do_like_escape(text *pat, text *esc) afterescape = false; while (plen > 0) { - if (CHAREQ(p, e) && !afterescape) + if (CHAREQ(p, plen, e, elen) && !afterescape) { *r++ = '\\'; NextChar(p, plen); diff --git a/src/backend/utils/adt/like_support.c b/src/backend/utils/adt/like_support.c index 8fdc677371f4d..01cd6b10730ca 100644 --- a/src/backend/utils/adt/like_support.c +++ b/src/backend/utils/adt/like_support.c @@ -23,7 +23,7 @@ * from LIKE to indexscan limits rather harder than one might think ... * but that's the basic idea.) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -99,8 +99,6 @@ static Selectivity like_selectivity(const char *patt, int pattlen, static Selectivity regex_selectivity(const char *patt, int pattlen, bool case_insensitive, int fixed_prefix_len); -static int pattern_char_isalpha(char c, bool is_multibyte, - pg_locale_t locale); static Const *make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation); static Datum string_to_datum(const char *str, Oid datatype); @@ -986,8 +984,8 @@ icnlikejoinsel(PG_FUNCTION_ARGS) */ static Pattern_Prefix_Status -like_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, - Const **prefix_const, Selectivity *rest_selec) +like_fixed_prefix(Const *patt_const, Const **prefix_const, + Selectivity *rest_selec) { char *match; char *patt; @@ -995,34 +993,10 @@ like_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, Oid typeid = patt_const->consttype; int pos, match_pos; - bool is_multibyte = (pg_database_encoding_max_length() > 1); - pg_locale_t locale = 0; /* the right-hand const is type text or bytea */ Assert(typeid == BYTEAOID || typeid == TEXTOID); - if (case_insensitive) - { - if (typeid == BYTEAOID) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("case insensitive matching not supported on type bytea"))); - - if (!OidIsValid(collation)) - { - /* - * This typically means that the parser could not resolve a - * conflict of implicit collations, so report it that way. - */ - ereport(ERROR, - (errcode(ERRCODE_INDETERMINATE_COLLATION), - errmsg("could not determine which collation to use for ILIKE"), - errhint("Use the COLLATE clause to set the collation explicitly."))); - } - - locale = pg_newlocale_from_collation(collation); - } - if (typeid != BYTEAOID) { patt = TextDatumGetCString(patt_const->constvalue); @@ -1035,7 +1009,7 @@ like_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, pattlen = VARSIZE_ANY_EXHDR(bstr); patt = (char *) palloc(pattlen); memcpy(patt, VARDATA_ANY(bstr), pattlen); - Assert((Pointer) bstr == DatumGetPointer(patt_const->constvalue)); + Assert(bstr == DatumGetPointer(patt_const->constvalue)); } match = palloc(pattlen + 1); @@ -1055,11 +1029,6 @@ like_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, break; } - /* Stop if case-varying character (it's sort of a wildcard) */ - if (case_insensitive && - pattern_char_isalpha(patt[pos], is_multibyte, locale)) - break; - match[match_pos++] = patt[pos]; } @@ -1071,8 +1040,7 @@ like_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, *prefix_const = string_to_bytea_const(match, match_pos); if (rest_selec != NULL) - *rest_selec = like_selectivity(&patt[pos], pattlen - pos, - case_insensitive); + *rest_selec = like_selectivity(&patt[pos], pattlen - pos, false); pfree(patt); pfree(match); @@ -1087,6 +1055,112 @@ like_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, return Pattern_Prefix_None; } +/* + * Case-insensitive variant of like_fixed_prefix(). Multibyte and + * locale-aware for detecting cased characters. + */ +static Pattern_Prefix_Status +like_fixed_prefix_ci(Const *patt_const, Oid collation, Const **prefix_const, + Selectivity *rest_selec) +{ + text *val = DatumGetTextPP(patt_const->constvalue); + Oid typeid = patt_const->consttype; + int nbytes = VARSIZE_ANY_EXHDR(val); + int wpos; + pg_wchar *wpatt; + int wpattlen; + pg_wchar *wmatch; + int wmatch_pos = 0; + char *match; + int match_mblen; + pg_locale_t locale = 0; + + /* the right-hand const is type text or bytea */ + Assert(typeid == BYTEAOID || typeid == TEXTOID); + + if (typeid == BYTEAOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("case insensitive matching not supported on type bytea"))); + + if (!OidIsValid(collation)) + { + /* + * This typically means that the parser could not resolve a conflict + * of implicit collations, so report it that way. + */ + ereport(ERROR, + (errcode(ERRCODE_INDETERMINATE_COLLATION), + errmsg("could not determine which collation to use for ILIKE"), + errhint("Use the COLLATE clause to set the collation explicitly."))); + } + + locale = pg_newlocale_from_collation(collation); + + wpatt = palloc((nbytes + 1) * sizeof(pg_wchar)); + wpattlen = pg_mb2wchar_with_len(VARDATA_ANY(val), wpatt, nbytes); + + wmatch = palloc((nbytes + 1) * sizeof(pg_wchar)); + for (wpos = 0; wpos < wpattlen; wpos++) + { + /* % and _ are wildcard characters in LIKE */ + if (wpatt[wpos] == '%' || + wpatt[wpos] == '_') + break; + + /* Backslash escapes the next character */ + if (wpatt[wpos] == '\\') + { + wpos++; + if (wpos >= wpattlen) + break; + } + + /* + * For ILIKE, stop if it's a case-varying character (it's sort of a + * wildcard). + */ + if (pg_iswcased(wpatt[wpos], locale)) + break; + + wmatch[wmatch_pos++] = wpatt[wpos]; + } + + wmatch[wmatch_pos] = '\0'; + + match = palloc(pg_database_encoding_max_length() * wmatch_pos + 1); + match_mblen = pg_wchar2mb_with_len(wmatch, match, wmatch_pos); + match[match_mblen] = '\0'; + pfree(wmatch); + + *prefix_const = string_to_const(match, TEXTOID); + pfree(match); + + if (rest_selec != NULL) + { + int wrestlen = wpattlen - wmatch_pos; + char *rest; + int rest_mblen; + + rest = palloc(pg_database_encoding_max_length() * wrestlen + 1); + rest_mblen = pg_wchar2mb_with_len(&wpatt[wmatch_pos], rest, wrestlen); + + *rest_selec = like_selectivity(rest, rest_mblen, true); + pfree(rest); + } + + pfree(wpatt); + + /* in LIKE, an empty pattern is an exact match! */ + if (wpos == wpattlen) + return Pattern_Prefix_Exact; /* reached end of pattern, so exact */ + + if (wmatch_pos > 0) + return Pattern_Prefix_Partial; + + return Pattern_Prefix_None; +} + static Pattern_Prefix_Status regex_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation, Const **prefix_const, Selectivity *rest_selec) @@ -1164,12 +1238,11 @@ pattern_fixed_prefix(Const *patt, Pattern_Type ptype, Oid collation, switch (ptype) { case Pattern_Type_Like: - result = like_fixed_prefix(patt, false, collation, - prefix, rest_selec); + result = like_fixed_prefix(patt, prefix, rest_selec); break; case Pattern_Type_Like_IC: - result = like_fixed_prefix(patt, true, collation, - prefix, rest_selec); + result = like_fixed_prefix_ci(patt, collation, prefix, + rest_selec); break; case Pattern_Type_Regex: result = regex_fixed_prefix(patt, false, collation, @@ -1481,29 +1554,6 @@ regex_selectivity(const char *patt, int pattlen, bool case_insensitive, return sel; } -/* - * Check whether char is a letter (and, hence, subject to case-folding) - * - * In multibyte character sets or with ICU, we can't use isalpha, and it does - * not seem worth trying to convert to wchar_t to use iswalpha or u_isalpha. - * Instead, just assume any non-ASCII char is potentially case-varying, and - * hard-wire knowledge of which ASCII chars are letters. - */ -static int -pattern_char_isalpha(char c, bool is_multibyte, - pg_locale_t locale) -{ - if (locale->ctype_is_c) - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); - else if (is_multibyte && IS_HIGHBIT_SET(c)) - return true; - else if (locale->provider != COLLPROVIDER_LIBC) - return IS_HIGHBIT_SET(c) || - (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); - else - return isalpha_l((unsigned char) c, locale->info.lt); -} - /* * For bytea, the increment function need only increment the current byte @@ -1582,7 +1632,7 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation) len = VARSIZE_ANY_EXHDR(bstr); workstr = (char *) palloc(len); memcpy(workstr, VARDATA_ANY(bstr), len); - Assert((Pointer) bstr == DatumGetPointer(str_const->constvalue)); + Assert(bstr == DatumGetPointer(str_const->constvalue)); cmpstr = str_const->constvalue; } else diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c index 00e67fb46d074..4481c354fd619 100644 --- a/src/backend/utils/adt/lockfuncs.c +++ b/src/backend/utils/adt/lockfuncs.c @@ -3,7 +3,7 @@ * lockfuncs.c * Functions for SQL access to various lock-manager capabilities. * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/lockfuncs.c @@ -146,13 +146,14 @@ pg_lock_status(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 16, "waitstart", TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); funcctx->tuple_desc = BlessTupleDesc(tupdesc); /* * Collect all the locking information that we will format and send * out as a result set. */ - mystatus = (PG_Lock_Status *) palloc(sizeof(PG_Lock_Status)); + mystatus = palloc_object(PG_Lock_Status); funcctx->user_fctx = mystatus; mystatus->lockData = GetLockStatusData(); @@ -329,7 +330,7 @@ pg_lock_status(PG_FUNCTION_ARGS) values[1] = ObjectIdGetDatum(instance->locktag.locktag_field1); values[8] = ObjectIdGetDatum(instance->locktag.locktag_field2); values[6] = ObjectIdGetDatum(instance->locktag.locktag_field3); - values[9] = Int16GetDatum(instance->locktag.locktag_field4); + values[9] = UInt16GetDatum(instance->locktag.locktag_field4); nulls[2] = true; nulls[3] = true; nulls[4] = true; @@ -343,7 +344,7 @@ pg_lock_status(PG_FUNCTION_ARGS) values[1] = ObjectIdGetDatum(instance->locktag.locktag_field1); values[7] = ObjectIdGetDatum(instance->locktag.locktag_field2); values[8] = ObjectIdGetDatum(instance->locktag.locktag_field3); - values[9] = Int16GetDatum(instance->locktag.locktag_field4); + values[9] = UInt16GetDatum(instance->locktag.locktag_field4); nulls[2] = true; nulls[3] = true; nulls[4] = true; @@ -398,15 +399,15 @@ pg_lock_status(PG_FUNCTION_ARGS) values[0] = CStringGetTextDatum(PredicateLockTagTypeNames[lockType]); /* lock target */ - values[1] = GET_PREDICATELOCKTARGETTAG_DB(*predTag); - values[2] = GET_PREDICATELOCKTARGETTAG_RELATION(*predTag); + values[1] = ObjectIdGetDatum(GET_PREDICATELOCKTARGETTAG_DB(*predTag)); + values[2] = ObjectIdGetDatum(GET_PREDICATELOCKTARGETTAG_RELATION(*predTag)); if (lockType == PREDLOCKTAG_TUPLE) - values[4] = GET_PREDICATELOCKTARGETTAG_OFFSET(*predTag); + values[4] = UInt16GetDatum(GET_PREDICATELOCKTARGETTAG_OFFSET(*predTag)); else nulls[4] = true; if ((lockType == PREDLOCKTAG_TUPLE) || (lockType == PREDLOCKTAG_PAGE)) - values[3] = GET_PREDICATELOCKTARGETTAG_PAGE(*predTag); + values[3] = UInt32GetDatum(GET_PREDICATELOCKTARGETTAG_PAGE(*predTag)); else nulls[3] = true; diff --git a/src/backend/utils/adt/mac.c b/src/backend/utils/adt/mac.c index 3644e9735f5d0..923c5af54f8bf 100644 --- a/src/backend/utils/adt/mac.c +++ b/src/backend/utils/adt/mac.c @@ -3,7 +3,7 @@ * mac.c * PostgreSQL type definitions for 6 byte, EUI-48, MAC addresses. * - * Portions Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1998-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/mac.c @@ -14,11 +14,9 @@ #include "postgres.h" #include "common/hashfn.h" -#include "lib/hyperloglog.h" #include "libpq/pqformat.h" #include "port/pg_bswap.h" #include "utils/fmgrprotos.h" -#include "utils/guc.h" #include "utils/inet.h" #include "utils/sortsupport.h" @@ -33,15 +31,6 @@ #define lobits(addr) \ ((unsigned long)(((addr)->d<<16)|((addr)->e<<8)|((addr)->f))) -/* sortsupport for macaddr */ -typedef struct -{ - int64 input_count; /* number of non-null values seen */ - bool estimating; /* true if estimating cardinality */ - - hyperLogLogState abbr_card; /* cardinality estimator */ -} macaddr_sortsupport_state; - static int macaddr_cmp_internal(macaddr *a1, macaddr *a2); static int macaddr_fast_cmp(Datum x, Datum y, SortSupport ssup); static bool macaddr_abbrev_abort(int memtupcount, SortSupport ssup); @@ -101,7 +90,7 @@ macaddr_in(PG_FUNCTION_ARGS) (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("invalid octet value in \"macaddr\" value: \"%s\"", str))); - result = (macaddr *) palloc(sizeof(macaddr)); + result = palloc_object(macaddr); result->a = a; result->b = b; @@ -142,7 +131,7 @@ macaddr_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); macaddr *addr; - addr = (macaddr *) palloc(sizeof(macaddr)); + addr = palloc_object(macaddr); addr->a = pq_getmsgbyte(buf); addr->b = pq_getmsgbyte(buf); @@ -289,7 +278,7 @@ macaddr_not(PG_FUNCTION_ARGS) macaddr *addr = PG_GETARG_MACADDR_P(0); macaddr *result; - result = (macaddr *) palloc(sizeof(macaddr)); + result = palloc_object(macaddr); result->a = ~addr->a; result->b = ~addr->b; result->c = ~addr->c; @@ -306,7 +295,7 @@ macaddr_and(PG_FUNCTION_ARGS) macaddr *addr2 = PG_GETARG_MACADDR_P(1); macaddr *result; - result = (macaddr *) palloc(sizeof(macaddr)); + result = palloc_object(macaddr); result->a = addr1->a & addr2->a; result->b = addr1->b & addr2->b; result->c = addr1->c & addr2->c; @@ -323,7 +312,7 @@ macaddr_or(PG_FUNCTION_ARGS) macaddr *addr2 = PG_GETARG_MACADDR_P(1); macaddr *result; - result = (macaddr *) palloc(sizeof(macaddr)); + result = palloc_object(macaddr); result->a = addr1->a | addr2->a; result->b = addr1->b | addr2->b; result->c = addr1->c | addr2->c; @@ -343,7 +332,7 @@ macaddr_trunc(PG_FUNCTION_ARGS) macaddr *addr = PG_GETARG_MACADDR_P(0); macaddr *result; - result = (macaddr *) palloc(sizeof(macaddr)); + result = palloc_object(macaddr); result->a = addr->a; result->b = addr->b; @@ -369,24 +358,10 @@ macaddr_sortsupport(PG_FUNCTION_ARGS) if (ssup->abbreviate) { - macaddr_sortsupport_state *uss; - MemoryContext oldcontext; - - oldcontext = MemoryContextSwitchTo(ssup->ssup_cxt); - - uss = palloc(sizeof(macaddr_sortsupport_state)); - uss->input_count = 0; - uss->estimating = true; - initHyperLogLog(&uss->abbr_card, 10); - - ssup->ssup_extra = uss; - ssup->comparator = ssup_datum_unsigned_cmp; ssup->abbrev_converter = macaddr_abbrev_convert; ssup->abbrev_abort = macaddr_abbrev_abort; ssup->abbrev_full_comparator = macaddr_fast_cmp; - - MemoryContextSwitchTo(oldcontext); } PG_RETURN_VOID(); @@ -406,61 +381,13 @@ macaddr_fast_cmp(Datum x, Datum y, SortSupport ssup) } /* - * Callback for estimating effectiveness of abbreviated key optimization. - * - * We pay no attention to the cardinality of the non-abbreviated data, because - * there is no equality fast-path within authoritative macaddr comparator. + * Abbreviation is never aborted for macaddr because the 6-byte MAC address + * fits entirely within a 64-bit Datum, making the abbreviated key + * authoritative. */ static bool macaddr_abbrev_abort(int memtupcount, SortSupport ssup) { - macaddr_sortsupport_state *uss = ssup->ssup_extra; - double abbr_card; - - if (memtupcount < 10000 || uss->input_count < 10000 || !uss->estimating) - return false; - - abbr_card = estimateHyperLogLog(&uss->abbr_card); - - /* - * If we have >100k distinct values, then even if we were sorting many - * billion rows we'd likely still break even, and the penalty of undoing - * that many rows of abbrevs would probably not be worth it. At this point - * we stop counting because we know that we're now fully committed. - */ - if (abbr_card > 100000.0) - { - if (trace_sort) - elog(LOG, - "macaddr_abbrev: estimation ends at cardinality %f" - " after " INT64_FORMAT " values (%d rows)", - abbr_card, uss->input_count, memtupcount); - uss->estimating = false; - return false; - } - - /* - * Target minimum cardinality is 1 per ~2k of non-null inputs. 0.5 row - * fudge factor allows us to abort earlier on genuinely pathological data - * where we've had exactly one abbreviated value in the first 2k - * (non-null) rows. - */ - if (abbr_card < uss->input_count / 2000.0 + 0.5) - { - if (trace_sort) - elog(LOG, - "macaddr_abbrev: aborting abbreviation at cardinality %f" - " below threshold %f after " INT64_FORMAT " values (%d rows)", - abbr_card, uss->input_count / 2000.0 + 0.5, uss->input_count, - memtupcount); - return true; - } - - if (trace_sort) - elog(LOG, - "macaddr_abbrev: cardinality %f after " INT64_FORMAT - " values (%d rows)", abbr_card, uss->input_count, memtupcount); - return false; } @@ -469,48 +396,25 @@ macaddr_abbrev_abort(int memtupcount, SortSupport ssup) * to abbreviated key representation. * * Packs the bytes of a 6-byte MAC address into a Datum and treats it as an - * unsigned integer for purposes of comparison. On a 64-bit machine, there - * will be two zeroed bytes of padding. The integer is converted to native - * endianness to facilitate easy comparison. + * unsigned integer for purposes of comparison. There will be two zeroed bytes + * of padding. The integer is converted to native endianness to facilitate + * easy comparison. */ static Datum macaddr_abbrev_convert(Datum original, SortSupport ssup) { - macaddr_sortsupport_state *uss = ssup->ssup_extra; macaddr *authoritative = DatumGetMacaddrP(original); Datum res; /* - * On a 64-bit machine, zero out the 8-byte datum and copy the 6 bytes of - * the MAC address in. There will be two bytes of zero padding on the end - * of the least significant bits. + * Zero out the 8-byte Datum and copy in the 6 bytes of the MAC address. + * There will be two bytes of zero padding on the end of the least + * significant bits. */ -#if SIZEOF_DATUM == 8 - memset(&res, 0, SIZEOF_DATUM); + StaticAssertDecl(sizeof(res) >= sizeof(macaddr), + "Datum is too small for macaddr"); + memset(&res, 0, sizeof(res)); memcpy(&res, authoritative, sizeof(macaddr)); -#else /* SIZEOF_DATUM != 8 */ - memcpy(&res, authoritative, SIZEOF_DATUM); -#endif - uss->input_count += 1; - - /* - * Cardinality estimation. The estimate uses uint32, so on a 64-bit - * architecture, XOR the two 32-bit halves together to produce slightly - * more entropy. The two zeroed bytes won't have any practical impact on - * this operation. - */ - if (uss->estimating) - { - uint32 tmp; - -#if SIZEOF_DATUM == 8 - tmp = (uint32) res ^ (uint32) ((uint64) res >> 32); -#else /* SIZEOF_DATUM != 8 */ - tmp = (uint32) res; -#endif - - addHyperLogLog(&uss->abbr_card, DatumGetUInt32(hash_uint32(tmp))); - } /* * Byteswap on little-endian machines. diff --git a/src/backend/utils/adt/mac8.c b/src/backend/utils/adt/mac8.c index 08e41ba4eeabc..0425ea473a529 100644 --- a/src/backend/utils/adt/mac8.c +++ b/src/backend/utils/adt/mac8.c @@ -11,7 +11,7 @@ * The following code is written with the assumption that the OUI field * size is 24 bits. * - * Portions Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1998-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/mac8.c @@ -207,7 +207,7 @@ macaddr8_in(PG_FUNCTION_ARGS) else if (count != 8) goto fail; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = a; result->b = b; @@ -256,7 +256,7 @@ macaddr8_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); macaddr8 *addr; - addr = (macaddr8 *) palloc0(sizeof(macaddr8)); + addr = palloc0_object(macaddr8); addr->a = pq_getmsgbyte(buf); addr->b = pq_getmsgbyte(buf); @@ -417,7 +417,7 @@ macaddr8_not(PG_FUNCTION_ARGS) macaddr8 *addr = PG_GETARG_MACADDR8_P(0); macaddr8 *result; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = ~addr->a; result->b = ~addr->b; result->c = ~addr->c; @@ -437,7 +437,7 @@ macaddr8_and(PG_FUNCTION_ARGS) macaddr8 *addr2 = PG_GETARG_MACADDR8_P(1); macaddr8 *result; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = addr1->a & addr2->a; result->b = addr1->b & addr2->b; result->c = addr1->c & addr2->c; @@ -457,7 +457,7 @@ macaddr8_or(PG_FUNCTION_ARGS) macaddr8 *addr2 = PG_GETARG_MACADDR8_P(1); macaddr8 *result; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = addr1->a | addr2->a; result->b = addr1->b | addr2->b; result->c = addr1->c | addr2->c; @@ -479,7 +479,7 @@ macaddr8_trunc(PG_FUNCTION_ARGS) macaddr8 *addr = PG_GETARG_MACADDR8_P(0); macaddr8 *result; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = addr->a; result->b = addr->b; @@ -502,7 +502,7 @@ macaddr8_set7bit(PG_FUNCTION_ARGS) macaddr8 *addr = PG_GETARG_MACADDR8_P(0); macaddr8 *result; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = addr->a | 0x02; result->b = addr->b; @@ -526,7 +526,7 @@ macaddrtomacaddr8(PG_FUNCTION_ARGS) macaddr *addr6 = PG_GETARG_MACADDR_P(0); macaddr8 *result; - result = (macaddr8 *) palloc0(sizeof(macaddr8)); + result = palloc0_object(macaddr8); result->a = addr6->a; result->b = addr6->b; @@ -547,10 +547,10 @@ macaddr8tomacaddr(PG_FUNCTION_ARGS) macaddr8 *addr = PG_GETARG_MACADDR8_P(0); macaddr *result; - result = (macaddr *) palloc0(sizeof(macaddr)); + result = palloc0_object(macaddr); if ((addr->d != 0xFF) || (addr->e != 0xFE)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("macaddr8 data out of range to convert to macaddr"), errhint("Only addresses that have FF and FE as values in the " diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b4e1..1a4dbbeb8db8f 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -3,7 +3,7 @@ * mcxtfuncs.c * Functions to show backend memory context. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -15,13 +15,16 @@ #include "postgres.h" +#include "catalog/pg_type_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/procsignal.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/tuplestore.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -38,7 +41,7 @@ typedef struct MemoryContextId { MemoryContext context; int context_id; -} MemoryContextId; +} MemoryContextId; /* * int_list_to_array @@ -52,7 +55,7 @@ int_list_to_array(const List *list) ArrayType *result_array; length = list_length(list); - datum_array = (Datum *) palloc(length * sizeof(Datum)); + datum_array = palloc_array(Datum, length); foreach_int(i, list) datum_array[foreach_current_index(i)] = Int32GetDatum(i); diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build index 244f48f4fd711..d793f8145f6c2 100644 --- a/src/backend/utils/adt/meson.build +++ b/src/backend/utils/adt/meson.build @@ -1,4 +1,14 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +# Some code in numeric.c benefits from auto-vectorization +numeric_backend_lib = static_library('numeric_backend_lib', + 'numeric.c', + dependencies: backend_build_deps, + kwargs: internal_lib_args, + c_args: vectorize_cflags, +) + +backend_link_with += numeric_backend_lib backend_sources += files( 'acl.c', @@ -12,6 +22,7 @@ backend_sources += files( 'arrayutils.c', 'ascii.c', 'bool.c', + 'bytea.c', 'cash.c', 'char.c', 'cryptohashfuncs.c', @@ -19,6 +30,7 @@ backend_sources += files( 'datetime.c', 'datum.c', 'dbsize.c', + 'ddlutils.c', 'domains.c', 'encode.c', 'enum.c', @@ -54,22 +66,25 @@ backend_sources += files( 'misc.c', 'multirangetypes.c', 'multirangetypes_selfuncs.c', + 'multixactfuncs.c', 'name.c', 'network.c', 'network_gist.c', 'network_selfuncs.c', 'network_spgist.c', - 'numeric.c', 'numutils.c', 'oid.c', + 'oid8.c', 'oracle_compat.c', 'orderedsetaggs.c', 'partitionfuncs.c', + 'pg_dependencies.c', 'pg_locale.c', 'pg_locale_builtin.c', 'pg_locale_icu.c', 'pg_locale_libc.c', 'pg_lsn.c', + 'pg_ndistinct.c', 'pg_upgrade_support.c', 'pgstatfuncs.c', 'pseudorandomfuncs.c', diff --git a/src/backend/utils/adt/misc.c b/src/backend/utils/adt/misc.c index 6fcfd031428ed..c033e68ba1517 100644 --- a/src/backend/utils/adt/misc.c +++ b/src/backend/utils/adt/misc.c @@ -3,7 +3,7 @@ * misc.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -21,12 +21,12 @@ #include #include +#include "access/htup_details.h" #include "access/sysattr.h" #include "access/table.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_type.h" #include "catalog/system_fk_info.h" -#include "commands/dbcommands.h" #include "commands/tablespace.h" #include "common/keywords.h" #include "funcapi.h" @@ -46,6 +46,8 @@ #include "utils/ruleutils.h" #include "utils/syscache.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" +#include "utils/wait_event.h" /* @@ -86,7 +88,7 @@ count_nulls(FunctionCallInfo fcinfo, int ndims, nitems, *dims; - bits8 *bitmap; + uint8 *bitmap; Assert(PG_NARGS() == 1); @@ -186,6 +188,20 @@ pg_num_nonnulls(PG_FUNCTION_ARGS) PG_RETURN_INT32(nargs - nulls); } +/* + * error_on_null() + * Check if the input is the NULL value + */ +Datum +pg_error_on_null(PG_FUNCTION_ARGS) +{ + if (PG_ARGISNULL(0)) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed"))); + + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); +} /* * current_database() @@ -301,66 +317,12 @@ Datum pg_tablespace_location(PG_FUNCTION_ARGS) { Oid tablespaceOid = PG_GETARG_OID(0); - char sourcepath[MAXPGPATH]; - char targetpath[MAXPGPATH]; - int rllen; - struct stat st; - - /* - * It's useful to apply this function to pg_class.reltablespace, wherein - * zero means "the database's default tablespace". So, rather than - * throwing an error for zero, we choose to assume that's what is meant. - */ - if (tablespaceOid == InvalidOid) - tablespaceOid = MyDatabaseTableSpace; - - /* - * Return empty string for the cluster's default tablespaces - */ - if (tablespaceOid == DEFAULTTABLESPACE_OID || - tablespaceOid == GLOBALTABLESPACE_OID) - PG_RETURN_TEXT_P(cstring_to_text("")); - - /* - * Find the location of the tablespace by reading the symbolic link that - * is in pg_tblspc/. - */ - snprintf(sourcepath, sizeof(sourcepath), "%s/%u", PG_TBLSPC_DIR, tablespaceOid); - - /* - * Before reading the link, check if the source path is a link or a - * junction point. Note that a directory is possible for a tablespace - * created with allow_in_place_tablespaces enabled. If a directory is - * found, a relative path to the data directory is returned. - */ - if (lstat(sourcepath, &st) < 0) - { - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not stat file \"%s\": %m", - sourcepath))); - } - - if (!S_ISLNK(st.st_mode)) - PG_RETURN_TEXT_P(cstring_to_text(sourcepath)); + char *tablespaceLoc; - /* - * In presence of a link or a junction point, return the path pointing to. - */ - rllen = readlink(sourcepath, targetpath, sizeof(targetpath)); - if (rllen < 0) - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not read symbolic link \"%s\": %m", - sourcepath))); - if (rllen >= sizeof(targetpath)) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("symbolic link \"%s\" target is too long", - sourcepath))); - targetpath[rllen] = '\0'; + /* Get LOCATION string from its OID */ + tablespaceLoc = get_tablespace_location(tablespaceOid); - PG_RETURN_TEXT_P(cstring_to_text(targetpath)); + PG_RETURN_TEXT_P(cstring_to_text(tablespaceLoc)); } /* @@ -370,7 +332,20 @@ Datum pg_sleep(PG_FUNCTION_ARGS) { float8 secs = PG_GETARG_FLOAT8(0); - float8 endtime; + int64 usecs; + TimestampTz endtime; + + /* + * Convert the delay to int64 microseconds, rounding up any fraction, and + * silently limiting it to PG_INT64_MAX/2 microseconds (about 150K years) + * to ensure the computation of endtime won't overflow. Historically + * we've treated NaN as "no wait", not an error, so keep that behavior. + */ + if (isnan(secs) || secs <= 0.0) + PG_RETURN_VOID(); + secs *= USECS_PER_SEC; /* we assume overflow will produce +Inf */ + secs = ceil(secs); /* round up any fractional microsecond */ + usecs = (int64) Min(secs, (float8) (PG_INT64_MAX / 2)); /* * We sleep using WaitLatch, to ensure that we'll wake up promptly if an @@ -384,22 +359,20 @@ pg_sleep(PG_FUNCTION_ARGS) * less than the specified time when WaitLatch is terminated early by a * non-query-canceling signal such as SIGHUP. */ -#define GetNowFloat() ((float8) GetCurrentTimestamp() / 1000000.0) - - endtime = GetNowFloat() + secs; + endtime = GetCurrentTimestamp() + usecs; for (;;) { - float8 delay; + TimestampTz delay; long delay_ms; CHECK_FOR_INTERRUPTS(); - delay = endtime - GetNowFloat(); - if (delay >= 600.0) + delay = endtime - GetCurrentTimestamp(); + if (delay >= 600 * USECS_PER_SEC) delay_ms = 600000; - else if (delay > 0.0) - delay_ms = (long) ceil(delay * 1000.0); + else if (delay > 0) + delay_ms = (long) ((delay + 999) / 1000); else break; @@ -516,7 +489,7 @@ pg_get_catalog_foreign_keys(PG_FUNCTION_ARGS) * array_in, and it wouldn't be very efficient if we could. Fill an * FmgrInfo to use for the call. */ - arrayinp = (FmgrInfo *) palloc(sizeof(FmgrInfo)); + arrayinp = palloc_object(FmgrInfo); fmgr_info(F_ARRAY_IN, arrayinp); funcctx->user_fctx = arrayinp; diff --git a/src/backend/utils/adt/multirangetypes.c b/src/backend/utils/adt/multirangetypes.c index cd84ced5b487c..9548989d7820f 100644 --- a/src/backend/utils/adt/multirangetypes.c +++ b/src/backend/utils/adt/multirangetypes.c @@ -21,7 +21,7 @@ * for a particular range index. Offsets are counted starting from the end of * flags aligned to the bound type. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -68,11 +68,11 @@ typedef enum * Macros for accessing past MultirangeType parts of multirange: items, flags * and boundaries. */ -#define MultirangeGetItemsPtr(mr) ((uint32 *) ((Pointer) (mr) + \ +#define MultirangeGetItemsPtr(mr) ((uint32 *) ((char *) (mr) + \ sizeof(MultirangeType))) -#define MultirangeGetFlagsPtr(mr) ((uint8 *) ((Pointer) (mr) + \ +#define MultirangeGetFlagsPtr(mr) ((uint8 *) ((char *) (mr) + \ sizeof(MultirangeType) + ((mr)->rangeCount - 1) * sizeof(uint32))) -#define MultirangeGetBoundariesPtr(mr, align) ((Pointer) (mr) + \ +#define MultirangeGetBoundariesPtr(mr, align) ((char *) (mr) + \ att_align_nominal(sizeof(MultirangeType) + \ ((mr)->rangeCount - 1) * sizeof(uint32) + \ (mr)->rangeCount * sizeof(uint8), (align))) @@ -125,7 +125,7 @@ multirange_in(PG_FUNCTION_ARGS) int32 range_count = 0; int32 range_capacity = 8; RangeType *range; - RangeType **ranges = palloc(range_capacity * sizeof(RangeType *)); + RangeType **ranges = palloc_array(RangeType *, range_capacity); MultirangeIOData *cache; MultirangeType *ret; MultirangeParseState parse_state; @@ -348,7 +348,7 @@ multirange_recv(PG_FUNCTION_ARGS) cache = get_multirange_io_data(fcinfo, mltrngtypoid, IOFunc_receive); range_count = pq_getmsgint(buf, 4); - ranges = palloc(range_count * sizeof(RangeType *)); + ranges = palloc_array(RangeType *, range_count); initStringInfo(&tmpbuf); for (int i = 0; i < range_count; i++) @@ -378,31 +378,33 @@ multirange_send(PG_FUNCTION_ARGS) { MultirangeType *multirange = PG_GETARG_MULTIRANGE_P(0); Oid mltrngtypoid = MultirangeTypeGetOid(multirange); - StringInfo buf = makeStringInfo(); + StringInfoData buf; RangeType **ranges; int32 range_count; MultirangeIOData *cache; + initStringInfo(&buf); cache = get_multirange_io_data(fcinfo, mltrngtypoid, IOFunc_send); /* construct output */ - pq_begintypsend(buf); + pq_begintypsend(&buf); - pq_sendint32(buf, multirange->rangeCount); + pq_sendint32(&buf, multirange->rangeCount); multirange_deserialize(cache->typcache->rngtype, multirange, &range_count, &ranges); for (int i = 0; i < range_count; i++) { Datum range; + bytea *outputbytes; range = RangeTypePGetDatum(ranges[i]); - range = PointerGetDatum(SendFunctionCall(&cache->typioproc, range)); + outputbytes = SendFunctionCall(&cache->typioproc, range); - pq_sendint32(buf, VARSIZE(range) - VARHDRSZ); - pq_sendbytes(buf, VARDATA(range), VARSIZE(range) - VARHDRSZ); + pq_sendint32(&buf, VARSIZE(outputbytes) - VARHDRSZ); + pq_sendbytes(&buf, VARDATA(outputbytes), VARSIZE(outputbytes) - VARHDRSZ); } - PG_RETURN_BYTEA_P(pq_endtypsend(buf)); + PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } /* @@ -483,8 +485,9 @@ multirange_canonicalize(TypeCacheEntry *rangetyp, int32 input_range_count, int32 output_range_count = 0; /* Sort the ranges so we can find the ones that overlap/meet. */ - qsort_arg(ranges, input_range_count, sizeof(RangeType *), range_compare, - rangetyp); + if (ranges != NULL) + qsort_arg(ranges, input_range_count, sizeof(RangeType *), + range_compare, rangetyp); /* Now merge where possible: */ for (i = 0; i < input_range_count; i++) @@ -570,21 +573,22 @@ multirange_size_estimate(TypeCacheEntry *rangetyp, int32 range_count, RangeType **ranges) { char elemalign = rangetyp->rngelemtype->typalign; + uint8 elemalignby = typalign_to_alignby(elemalign); Size size; int32 i; /* * Count space for MultirangeType struct, items and flags. */ - size = att_align_nominal(sizeof(MultirangeType) + - Max(range_count - 1, 0) * sizeof(uint32) + - range_count * sizeof(uint8), elemalign); + size = att_nominal_alignby(sizeof(MultirangeType) + + Max(range_count - 1, 0) * sizeof(uint32) + + range_count * sizeof(uint8), elemalignby); /* Count space for range bounds */ for (i = 0; i < range_count; i++) - size += att_align_nominal(VARSIZE(ranges[i]) - - sizeof(RangeType) - - sizeof(char), elemalign); + size += att_nominal_alignby(VARSIZE(ranges[i]) - + sizeof(RangeType) - + sizeof(char), elemalignby); return size; } @@ -600,13 +604,14 @@ write_multirange_data(MultirangeType *multirange, TypeCacheEntry *rangetyp, uint32 prev_offset = 0; uint8 *flags; int32 i; - Pointer begin, - ptr; + const char *begin; + char *ptr; char elemalign = rangetyp->rngelemtype->typalign; + uint8 elemalignby = typalign_to_alignby(elemalign); items = MultirangeGetItemsPtr(multirange); flags = MultirangeGetFlagsPtr(multirange); - ptr = begin = MultirangeGetBoundariesPtr(multirange, elemalign); + begin = ptr = MultirangeGetBoundariesPtr(multirange, elemalign); for (i = 0; i < range_count; i++) { uint32 len; @@ -625,10 +630,10 @@ write_multirange_data(MultirangeType *multirange, TypeCacheEntry *rangetyp, items[i - 1] |= MULTIRANGE_ITEM_OFF_BIT; prev_offset = ptr - begin; } - flags[i] = *((Pointer) ranges[i] + VARSIZE(ranges[i]) - sizeof(char)); + flags[i] = *((char *) ranges[i] + VARSIZE(ranges[i]) - sizeof(char)); len = VARSIZE(ranges[i]) - sizeof(RangeType) - sizeof(char); - memcpy(ptr, (Pointer) (ranges[i] + 1), len); - ptr += att_align_nominal(len, elemalign); + memcpy(ptr, ranges[i] + 1, len); + ptr += att_nominal_alignby(len, elemalignby); } } @@ -697,8 +702,8 @@ multirange_get_range(TypeCacheEntry *rangetyp, { uint32 offset; uint8 flags; - Pointer begin, - ptr; + const char *begin; + char *ptr; int16 typlen = rangetyp->rngelemtype->typlen; char typalign = rangetyp->rngelemtype->typalign; uint32 len; @@ -708,7 +713,7 @@ multirange_get_range(TypeCacheEntry *rangetyp, offset = multirange_get_bounds_offset(multirange, i); flags = MultirangeGetFlagsPtr(multirange)[i]; - ptr = begin = MultirangeGetBoundariesPtr(multirange, typalign) + offset; + begin = ptr = MultirangeGetBoundariesPtr(multirange, typalign) + offset; /* * Calculate the size of bound values. In principle, we could get offset @@ -717,11 +722,11 @@ multirange_get_range(TypeCacheEntry *rangetyp, * exact size. */ if (RANGE_HAS_LBOUND(flags)) - ptr = (Pointer) att_addlength_pointer(ptr, typlen, ptr); + ptr = (char *) att_addlength_pointer(ptr, typlen, ptr); if (RANGE_HAS_UBOUND(flags)) { - ptr = (Pointer) att_align_pointer(ptr, typalign, typlen, ptr); - ptr = (Pointer) att_addlength_pointer(ptr, typlen, ptr); + ptr = (char *) att_align_pointer(ptr, typalign, typlen, ptr); + ptr = (char *) att_addlength_pointer(ptr, typlen, ptr); } len = (ptr - begin) + sizeof(RangeType) + sizeof(uint8); @@ -747,7 +752,7 @@ multirange_get_bounds(TypeCacheEntry *rangetyp, { uint32 offset; uint8 flags; - Pointer ptr; + const char *ptr; int16 typlen = rangetyp->rngelemtype->typlen; char typalign = rangetyp->rngelemtype->typalign; bool typbyval = rangetyp->rngelemtype->typbyval; @@ -768,7 +773,7 @@ multirange_get_bounds(TypeCacheEntry *rangetyp, { /* att_align_pointer cannot be necessary here */ lbound = fetch_att(ptr, typbyval, typlen); - ptr = (Pointer) att_addlength_pointer(ptr, typlen, ptr); + ptr = (char *) att_addlength_pointer(ptr, typlen, ptr); } else lbound = (Datum) 0; @@ -776,7 +781,7 @@ multirange_get_bounds(TypeCacheEntry *rangetyp, /* fetch upper bound, if any */ if (RANGE_HAS_UBOUND(flags)) { - ptr = (Pointer) att_align_pointer(ptr, typalign, typlen, ptr); + ptr = (char *) att_align_pointer(ptr, typalign, typlen, ptr); ubound = fetch_att(ptr, typbyval, typlen); /* no need for att_addlength_pointer */ } @@ -834,7 +839,7 @@ multirange_deserialize(TypeCacheEntry *rangetyp, { int i; - *ranges = palloc(*range_count * sizeof(RangeType *)); + *ranges = palloc_array(RangeType *, *range_count); for (i = 0; i < *range_count; i++) (*ranges)[i] = multirange_get_range(rangetyp, multirange, i); } @@ -1225,6 +1230,77 @@ multirange_minus_internal(Oid mltrngtypoid, TypeCacheEntry *rangetyp, return make_multirange(mltrngtypoid, rangetyp, range_count3, ranges3); } +/* + * multirange_minus_multi - like multirange_minus but returning the result as a + * SRF, with no rows if the result would be empty. + */ +Datum +multirange_minus_multi(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + MemoryContext oldcontext; + + if (!SRF_IS_FIRSTCALL()) + { + /* We never have more than one result */ + funcctx = SRF_PERCALL_SETUP(); + SRF_RETURN_DONE(funcctx); + } + else + { + MultirangeType *mr1; + MultirangeType *mr2; + Oid mltrngtypoid; + TypeCacheEntry *typcache; + TypeCacheEntry *rangetyp; + int32 range_count1; + int32 range_count2; + RangeType **ranges1; + RangeType **ranges2; + MultirangeType *mr; + + funcctx = SRF_FIRSTCALL_INIT(); + + /* + * switch to memory context appropriate for multiple function calls + */ + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* get args, detoasting into multi-call memory context */ + mr1 = PG_GETARG_MULTIRANGE_P(0); + mr2 = PG_GETARG_MULTIRANGE_P(1); + + mltrngtypoid = MultirangeTypeGetOid(mr1); + typcache = lookup_type_cache(mltrngtypoid, TYPECACHE_MULTIRANGE_INFO); + if (typcache->rngtype == NULL) + elog(ERROR, "type %u is not a multirange type", mltrngtypoid); + rangetyp = typcache->rngtype; + + if (MultirangeIsEmpty(mr1) || MultirangeIsEmpty(mr2)) + mr = mr1; + else + { + multirange_deserialize(rangetyp, mr1, &range_count1, &ranges1); + multirange_deserialize(rangetyp, mr2, &range_count2, &ranges2); + + mr = multirange_minus_internal(mltrngtypoid, + rangetyp, + range_count1, + ranges1, + range_count2, + ranges2); + } + + MemoryContextSwitchTo(oldcontext); + + funcctx = SRF_PERCALL_SETUP(); + if (MultirangeIsEmpty(mr)) + SRF_RETURN_DONE(funcctx); + else + SRF_RETURN_NEXT(funcctx, MultirangeTypePGetDatum(mr)); + } +} + /* multirange intersection */ Datum multirange_intersect(PG_FUNCTION_ARGS) @@ -2081,15 +2157,14 @@ range_overleft_multirange_internal(TypeCacheEntry *rangetyp, bool empty; if (RangeIsEmpty(r) || MultirangeIsEmpty(mr)) - PG_RETURN_BOOL(false); - + return false; range_deserialize(rangetyp, r, &lower1, &upper1, &empty); Assert(!empty); multirange_get_bounds(rangetyp, mr, mr->rangeCount - 1, &lower2, &upper2); - PG_RETURN_BOOL(range_cmp_bounds(rangetyp, &upper1, &upper2) <= 0); + return (range_cmp_bounds(rangetyp, &upper1, &upper2) <= 0); } Datum @@ -2166,7 +2241,7 @@ range_overright_multirange_internal(TypeCacheEntry *rangetyp, bool empty; if (RangeIsEmpty(r) || MultirangeIsEmpty(mr)) - PG_RETURN_BOOL(false); + return false; range_deserialize(rangetyp, r, &lower1, &upper1, &empty); Assert(!empty); @@ -2523,7 +2598,7 @@ multirange_adjacent_range(PG_FUNCTION_ARGS) TypeCacheEntry *typcache; if (RangeIsEmpty(r) || MultirangeIsEmpty(mr)) - return false; + PG_RETURN_BOOL(false); typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr)); @@ -2544,7 +2619,7 @@ multirange_adjacent_multirange(PG_FUNCTION_ARGS) upper2; if (MultirangeIsEmpty(mr1) || MultirangeIsEmpty(mr2)) - return false; + PG_RETURN_BOOL(false); typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr1)); @@ -2639,7 +2714,7 @@ multirange_cmp(PG_FUNCTION_ARGS) Datum multirange_lt(PG_FUNCTION_ARGS) { - int cmp = multirange_cmp(fcinfo); + int cmp = DatumGetInt32(multirange_cmp(fcinfo)); PG_RETURN_BOOL(cmp < 0); } @@ -2647,7 +2722,7 @@ multirange_lt(PG_FUNCTION_ARGS) Datum multirange_le(PG_FUNCTION_ARGS) { - int cmp = multirange_cmp(fcinfo); + int cmp = DatumGetInt32(multirange_cmp(fcinfo)); PG_RETURN_BOOL(cmp <= 0); } @@ -2655,7 +2730,7 @@ multirange_le(PG_FUNCTION_ARGS) Datum multirange_ge(PG_FUNCTION_ARGS) { - int cmp = multirange_cmp(fcinfo); + int cmp = DatumGetInt32(multirange_cmp(fcinfo)); PG_RETURN_BOOL(cmp >= 0); } @@ -2663,7 +2738,7 @@ multirange_ge(PG_FUNCTION_ARGS) Datum multirange_gt(PG_FUNCTION_ARGS) { - int cmp = multirange_cmp(fcinfo); + int cmp = DatumGetInt32(multirange_cmp(fcinfo)); PG_RETURN_BOOL(cmp > 0); } @@ -2746,7 +2821,7 @@ multirange_unnest(PG_FUNCTION_ARGS) mr = PG_GETARG_MULTIRANGE_P(0); /* allocate memory for user context */ - fctx = (multirange_unnest_fctx *) palloc(sizeof(multirange_unnest_fctx)); + fctx = palloc_object(multirange_unnest_fctx); /* initialize state */ fctx->mr = mr; @@ -2833,7 +2908,7 @@ hash_multirange(PG_FUNCTION_ARGS) upper_hash = 0; /* Merge hashes of flags and bounds */ - range_hash = hash_uint32((uint32) flags); + range_hash = hash_bytes_uint32((uint32) flags); range_hash ^= lower_hash; range_hash = pg_rotate_left32(range_hash, 1); range_hash ^= upper_hash; diff --git a/src/backend/utils/adt/multirangetypes_selfuncs.c b/src/backend/utils/adt/multirangetypes_selfuncs.c index b87bcf3ea306c..533111445e784 100644 --- a/src/backend/utils/adt/multirangetypes_selfuncs.c +++ b/src/backend/utils/adt/multirangetypes_selfuncs.c @@ -6,7 +6,7 @@ * Estimates are based on histograms of lower and upper bounds, and the * fraction of empty multiranges. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -49,10 +49,10 @@ static float8 get_position(TypeCacheEntry *typcache, const RangeBound *value, static float8 get_len_position(double value, double hist1, double hist2); static float8 get_distance(TypeCacheEntry *typcache, const RangeBound *bound1, const RangeBound *bound2); -static int length_hist_bsearch(Datum *length_hist_values, +static int length_hist_bsearch(const Datum *length_hist_values, int length_hist_nvalues, double value, bool equal); -static double calc_length_hist_frac(Datum *length_hist_values, +static double calc_length_hist_frac(const Datum *length_hist_values, int length_hist_nvalues, double length1, double length2, bool equal); static double calc_hist_selectivity_contained(TypeCacheEntry *typcache, @@ -60,14 +60,14 @@ static double calc_hist_selectivity_contained(TypeCacheEntry *typcache, RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, + const Datum *length_hist_values, int length_hist_nvalues); static double calc_hist_selectivity_contains(TypeCacheEntry *typcache, const RangeBound *lower, const RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, + const Datum *length_hist_values, int length_hist_nvalues); /* @@ -496,8 +496,8 @@ calc_hist_selectivity(TypeCacheEntry *typcache, VariableStatData *vardata, * bounds. */ nhist = hslot.nvalues; - hist_lower = (RangeBound *) palloc(sizeof(RangeBound) * nhist); - hist_upper = (RangeBound *) palloc(sizeof(RangeBound) * nhist); + hist_lower = palloc_array(RangeBound, nhist); + hist_upper = palloc_array(RangeBound, nhist); for (i = 0; i < nhist; i++) { bool empty; @@ -765,7 +765,7 @@ rbound_bsearch(TypeCacheEntry *typcache, const RangeBound *value, const RangeBou * given length, returns -1. */ static int -length_hist_bsearch(Datum *length_hist_values, int length_hist_nvalues, +length_hist_bsearch(const Datum *length_hist_values, int length_hist_nvalues, double value, bool equal) { int lower = -1, @@ -963,7 +963,7 @@ get_distance(TypeCacheEntry *typcache, const RangeBound *bound1, const RangeBoun * 'equal' is true). */ static double -calc_length_hist_frac(Datum *length_hist_values, int length_hist_nvalues, +calc_length_hist_frac(const Datum *length_hist_values, int length_hist_nvalues, double length1, double length2, bool equal) { double frac; @@ -1131,7 +1131,7 @@ static double calc_hist_selectivity_contained(TypeCacheEntry *typcache, const RangeBound *lower, RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, int length_hist_nvalues) + const Datum *length_hist_values, int length_hist_nvalues) { int i, upper_index; @@ -1252,7 +1252,7 @@ static double calc_hist_selectivity_contains(TypeCacheEntry *typcache, const RangeBound *lower, const RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, int length_hist_nvalues) + const Datum *length_hist_values, int length_hist_nvalues) { int i, lower_index; diff --git a/src/backend/utils/adt/multixactfuncs.c b/src/backend/utils/adt/multixactfuncs.c new file mode 100644 index 0000000000000..9fe2ebafa73f6 --- /dev/null +++ b/src/backend/utils/adt/multixactfuncs.c @@ -0,0 +1,140 @@ +/*------------------------------------------------------------------------- + * + * multixactfuncs.c + * Functions for accessing multixact-related data. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/utils/adt/multixactfuncs.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/multixact.h" +#include "access/multixact_internal.h" +#include "catalog/pg_authid_d.h" +#include "funcapi.h" +#include "miscadmin.h" +#include "utils/acl.h" +#include "utils/builtins.h" + +/* + * pg_get_multixact_members + * + * Returns information about the MultiXactMembers of the specified + * MultiXactId. + */ +Datum +pg_get_multixact_members(PG_FUNCTION_ARGS) +{ + typedef struct + { + MultiXactMember *members; + int nmembers; + int iter; + } mxact; + MultiXactId mxid = PG_GETARG_TRANSACTIONID(0); + mxact *multi; + FuncCallContext *funccxt; + + if (mxid < FirstMultiXactId) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid MultiXactId: %u", mxid))); + + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcxt; + TupleDesc tupdesc; + + funccxt = SRF_FIRSTCALL_INIT(); + oldcxt = MemoryContextSwitchTo(funccxt->multi_call_memory_ctx); + + multi = palloc_object(mxact); + /* no need to allow for old values here */ + multi->nmembers = GetMultiXactIdMembers(mxid, &multi->members, false, + false); + multi->iter = 0; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + funccxt->tuple_desc = tupdesc; + funccxt->attinmeta = TupleDescGetAttInMetadata(tupdesc); + funccxt->user_fctx = multi; + + MemoryContextSwitchTo(oldcxt); + } + + funccxt = SRF_PERCALL_SETUP(); + multi = (mxact *) funccxt->user_fctx; + + while (multi->iter < multi->nmembers) + { + HeapTuple tuple; + char *values[2]; + + values[0] = psprintf("%u", multi->members[multi->iter].xid); + values[1] = mxstatus_to_string(multi->members[multi->iter].status); + + tuple = BuildTupleFromCStrings(funccxt->attinmeta, values); + + multi->iter++; + pfree(values[0]); + SRF_RETURN_NEXT(funccxt, HeapTupleGetDatum(tuple)); + } + + SRF_RETURN_DONE(funccxt); +} + +/* + * pg_get_multixact_stats + * + * Returns statistics about current multixact usage. + */ +Datum +pg_get_multixact_stats(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[4]; + bool nulls[4]; + uint64 members; + MultiXactId oldestMultiXactId; + uint32 multixacts; + MultiXactOffset oldestOffset; + MultiXactOffset nextOffset; + uint64 membersBytes; + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("return type must be a row type"))); + + GetMultiXactInfo(&multixacts, &nextOffset, &oldestMultiXactId, &oldestOffset); + members = nextOffset - oldestOffset; + + membersBytes = MultiXactOffsetStorageSize(nextOffset, oldestOffset); + + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + /* + * Only superusers and roles with privileges of pg_read_all_stats can + * see details. + */ + memset(nulls, true, sizeof(bool) * tupdesc->natts); + } + else + { + values[0] = UInt32GetDatum(multixacts); + values[1] = Int64GetDatum(members); + values[2] = Int64GetDatum(membersBytes); + values[3] = UInt32GetDatum(oldestMultiXactId); + memset(nulls, false, sizeof(nulls)); + } + + return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)); +} diff --git a/src/backend/utils/adt/name.c b/src/backend/utils/adt/name.c index b2487881d54ab..4e9131a0b20fa 100644 --- a/src/backend/utils/adt/name.c +++ b/src/backend/utils/adt/name.c @@ -9,7 +9,7 @@ * always use NAMEDATALEN as the symbolic constant! - jolly 8/21/95 * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/network.c b/src/backend/utils/adt/network.c index f03fcc1147bb0..f4bf9c3b53285 100644 --- a/src/backend/utils/adt/network.c +++ b/src/backend/utils/adt/network.c @@ -12,8 +12,6 @@ #include #include -#include "access/stratnum.h" -#include "catalog/pg_opfamily.h" #include "catalog/pg_type.h" #include "common/hashfn.h" #include "common/ip.h" @@ -77,7 +75,7 @@ network_in(char *src, bool is_cidr, Node *escontext) int bits; inet *dst; - dst = (inet *) palloc0(sizeof(inet)); + dst = (inet *) palloc0_object(inet); /* * First, check to see if this is an IPv6 or IPv4 address. IPv6 addresses @@ -198,7 +196,7 @@ network_recv(StringInfo buf, bool is_cidr) i; /* make sure any unused bits in a CIDR value are zeroed */ - addr = (inet *) palloc0(sizeof(inet)); + addr = palloc0_object(inet); ip_family(addr) = pq_getmsgbyte(buf); if (ip_family(addr) != PGSQL_AF_INET && @@ -365,7 +363,7 @@ cidr_set_masklen(PG_FUNCTION_ARGS) inet * cidr_set_masklen_internal(const inet *src, int bits) { - inet *dst = (inet *) palloc0(sizeof(inet)); + inet *dst = palloc0_object(inet); ip_family(dst) = ip_family(src); ip_bits(dst) = bits; @@ -446,7 +444,7 @@ network_sortsupport(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(ssup->ssup_cxt); - uss = palloc(sizeof(network_sortsupport_state)); + uss = palloc_object(network_sortsupport_state); uss->input_count = 0; uss->estimating = true; initHyperLogLog(&uss->abbr_card, 10); @@ -553,7 +551,7 @@ network_abbrev_abort(int memtupcount, SortSupport ssup) * all their subnet bits *must* be zero (1.2.3.0/24). * * IPv4 and IPv6 are identical in this makeup, with the difference being that - * IPv4 addresses have a maximum of 32 bits compared to IPv6's 64 bits, so in + * IPv4 addresses have a maximum of 32 bits compared to IPv6's 128 bits, so in * IPv6 each part may be larger. * * inet/cidr types compare using these sorting rules. If inequality is detected @@ -569,24 +567,11 @@ network_abbrev_abort(int memtupcount, SortSupport ssup) * * When generating abbreviated keys for SortSupport, we pack as much as we can * into a datum while ensuring that when comparing those keys as integers, - * these rules will be respected. Exact contents depend on IP family and datum - * size. + * these rules will be respected. Exact contents depend on IP family: * * IPv4 * ---- * - * 4 byte datums: - * - * Start with 1 bit for the IP family (IPv4 or IPv6; this bit is present in - * every case below) followed by all but 1 of the netmasked bits. - * - * +----------+---------------------+ - * | 1 bit IP | 31 bits network | (1 bit network - * | family | (truncated) | omitted) - * +----------+---------------------+ - * - * 8 byte datums: - * * We have space to store all netmasked bits, followed by the netmask size, * followed by 25 bits of the subnet (25 bits is usually more than enough in * practice). cidr datums always have all-zero subnet bits. @@ -599,15 +584,6 @@ network_abbrev_abort(int memtupcount, SortSupport ssup) * IPv6 * ---- * - * 4 byte datums: - * - * +----------+---------------------+ - * | 1 bit IP | 31 bits network | (up to 97 bits - * | family | (truncated) | network omitted) - * +----------+---------------------+ - * - * 8 byte datums: - * * +----------+---------------------------------+ * | 1 bit IP | 63 bits network | (up to 65 bits * | family | (truncated) | network omitted) @@ -630,8 +606,7 @@ network_abbrev_convert(Datum original, SortSupport ssup) /* * Get an unsigned integer representation of the IP address by taking its * first 4 or 8 bytes. Always take all 4 bytes of an IPv4 address. Take - * the first 8 bytes of an IPv6 address with an 8 byte datum and 4 bytes - * otherwise. + * the first 8 bytes of an IPv6 address. * * We're consuming an array of unsigned char, so byteswap on little endian * systems (an inet's ipaddr field stores the most significant byte @@ -661,7 +636,7 @@ network_abbrev_convert(Datum original, SortSupport ssup) ipaddr_datum = DatumBigEndianToNative(ipaddr_datum); /* Initialize result with ipfamily (most significant) bit set */ - res = ((Datum) 1) << (SIZEOF_DATUM * BITS_PER_BYTE - 1); + res = ((Datum) 1) << (sizeof(Datum) * BITS_PER_BYTE - 1); } /* @@ -670,8 +645,7 @@ network_abbrev_convert(Datum original, SortSupport ssup) * while low order bits go in "subnet" component when there is space for * one. This is often accomplished by generating a temp datum subnet * bitmask, which we may reuse later when generating the subnet bits - * themselves. (Note that subnet bits are only used with IPv4 datums on - * platforms where datum is 8 bytes.) + * themselves. * * The number of bits in subnet is used to generate a datum subnet * bitmask. For example, with a /24 IPv4 datum there are 8 subnet bits @@ -683,14 +657,14 @@ network_abbrev_convert(Datum original, SortSupport ssup) subnet_size = ip_maxbits(authoritative) - ip_bits(authoritative); Assert(subnet_size >= 0); /* subnet size must work with prefix ipaddr cases */ - subnet_size %= SIZEOF_DATUM * BITS_PER_BYTE; + subnet_size %= sizeof(Datum) * BITS_PER_BYTE; if (ip_bits(authoritative) == 0) { /* Fit as many ipaddr bits as possible into subnet */ subnet_bitmask = ((Datum) 0) - 1; network = 0; } - else if (ip_bits(authoritative) < SIZEOF_DATUM * BITS_PER_BYTE) + else if (ip_bits(authoritative) < sizeof(Datum) * BITS_PER_BYTE) { /* Split ipaddr bits between network and subnet */ subnet_bitmask = (((Datum) 1) << subnet_size) - 1; @@ -703,12 +677,11 @@ network_abbrev_convert(Datum original, SortSupport ssup) network = ipaddr_datum; } -#if SIZEOF_DATUM == 8 if (ip_family(authoritative) == PGSQL_AF_INET) { /* - * IPv4 with 8 byte datums: keep all 32 netmasked bits, netmask size, - * and most significant 25 subnet bits + * IPv4: keep all 32 netmasked bits, netmask size, and most + * significant 25 subnet bits */ Datum netmask_size = (Datum) ip_bits(authoritative); Datum subnet; @@ -752,12 +725,11 @@ network_abbrev_convert(Datum original, SortSupport ssup) res |= network | netmask_size | subnet; } else -#endif { /* - * 4 byte datums, or IPv6 with 8 byte datums: Use as many of the - * netmasked bits as will fit in final abbreviated key. Avoid - * clobbering the ipfamily bit that was set earlier. + * IPv6: Use as many of the netmasked bits as will fit in final + * abbreviated key. Avoid clobbering the ipfamily bit that was set + * earlier. */ res |= network >> 1; } @@ -769,11 +741,7 @@ network_abbrev_convert(Datum original, SortSupport ssup) { uint32 tmp; -#if SIZEOF_DATUM == 8 - tmp = (uint32) res ^ (uint32) ((uint64) res >> 32); -#else /* SIZEOF_DATUM != 8 */ - tmp = (uint32) res; -#endif + tmp = DatumGetUInt32(res) ^ (uint32) (DatumGetUInt64(res) >> 32); addHyperLogLog(&uss->abbr_card, DatumGetUInt32(hash_uint32(tmp))); } @@ -1169,7 +1137,7 @@ network_show(PG_FUNCTION_ARGS) if (pg_inet_net_ntop(ip_family(ip), ip_addr(ip), ip_maxbits(ip), tmp, sizeof(tmp)) == NULL) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_INVALID_BINARY_REPRESENTATION), errmsg("could not format inet value: %m"))); @@ -1259,7 +1227,7 @@ network_broadcast(PG_FUNCTION_ARGS) *b; /* make sure any unused bits are zeroed */ - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); maxbytes = ip_addrsize(ip); bits = ip_bits(ip); @@ -1303,7 +1271,7 @@ network_network(PG_FUNCTION_ARGS) *b; /* make sure any unused bits are zeroed */ - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); bits = ip_bits(ip); a = ip_addr(ip); @@ -1346,7 +1314,7 @@ network_netmask(PG_FUNCTION_ARGS) unsigned char *b; /* make sure any unused bits are zeroed */ - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); bits = ip_bits(ip); b = ip_addr(dst); @@ -1389,7 +1357,7 @@ network_hostmask(PG_FUNCTION_ARGS) unsigned char *b; /* make sure any unused bits are zeroed */ - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); maxbytes = ip_addrsize(ip); bits = ip_maxbits(ip) - ip_bits(ip); @@ -1824,7 +1792,7 @@ inetnot(PG_FUNCTION_ARGS) inet *ip = PG_GETARG_INET_PP(0); inet *dst; - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); { int nb = ip_addrsize(ip); @@ -1850,7 +1818,7 @@ inetand(PG_FUNCTION_ARGS) inet *ip2 = PG_GETARG_INET_PP(1); inet *dst; - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); if (ip_family(ip) != ip_family(ip2)) ereport(ERROR, @@ -1882,7 +1850,7 @@ inetor(PG_FUNCTION_ARGS) inet *ip2 = PG_GETARG_INET_PP(1); inet *dst; - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); if (ip_family(ip) != ip_family(ip2)) ereport(ERROR, @@ -1912,7 +1880,7 @@ internal_inetpl(inet *ip, int64 addend) { inet *dst; - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); { int nb = ip_addrsize(ip); diff --git a/src/backend/utils/adt/network_gist.c b/src/backend/utils/adt/network_gist.c index a08c495378919..2eeb3dea12031 100644 --- a/src/backend/utils/adt/network_gist.c +++ b/src/backend/utils/adt/network_gist.c @@ -34,7 +34,7 @@ * twice as fast as for a simpler design in which a single field doubles as * the common prefix length and the minimum ip_bits value. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -117,8 +117,9 @@ inet_gist_consistent(PG_FUNCTION_ARGS) GISTENTRY *ent = (GISTENTRY *) PG_GETARG_POINTER(0); inet *query = PG_GETARG_INET_PP(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); GistInetKey *key = DatumGetInetKeyP(ent->key); int minbits, @@ -475,7 +476,7 @@ build_inet_union_key(int family, int minbits, int commonbits, GistInetKey *result; /* Make sure any unused bits are zeroed. */ - result = (GistInetKey *) palloc0(sizeof(GistInetKey)); + result = palloc0_object(GistInetKey); gk_ip_family(result) = family; gk_ip_minbits(result) = minbits; @@ -546,13 +547,13 @@ inet_gist_compress(PG_FUNCTION_ARGS) if (entry->leafkey) { - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); if (DatumGetPointer(entry->key) != NULL) { inet *in = DatumGetInetPP(entry->key); GistInetKey *r; - r = (GistInetKey *) palloc0(sizeof(GistInetKey)); + r = palloc0_object(GistInetKey); gk_ip_family(r) = ip_family(in); gk_ip_minbits(r) = ip_bits(in); @@ -594,14 +595,14 @@ inet_gist_fetch(PG_FUNCTION_ARGS) GISTENTRY *retval; inet *dst; - dst = (inet *) palloc0(sizeof(inet)); + dst = palloc0_object(inet); ip_family(dst) = gk_ip_family(key); ip_bits(dst) = gk_ip_minbits(key); memcpy(ip_addr(dst), gk_ip_addr(key), ip_addrsize(dst)); SET_INET_VARSIZE(dst); - retval = palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, InetPGetDatum(dst), entry->rel, entry->page, entry->offset, false); diff --git a/src/backend/utils/adt/network_selfuncs.c b/src/backend/utils/adt/network_selfuncs.c index 940cdafa54619..2a8d2ded9078f 100644 --- a/src/backend/utils/adt/network_selfuncs.c +++ b/src/backend/utils/adt/network_selfuncs.c @@ -7,7 +7,7 @@ * operators. Estimates are based on null fraction, most common values, * and histogram of inet/cidr columns. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -43,22 +43,22 @@ /* Maximum number of items to consider in join selectivity calculations */ #define MAX_CONSIDERED_ELEMS 1024 -static Selectivity networkjoinsel_inner(Oid operator, +static Selectivity networkjoinsel_inner(Oid operator, int opr_codenum, VariableStatData *vardata1, VariableStatData *vardata2); -static Selectivity networkjoinsel_semi(Oid operator, +static Selectivity networkjoinsel_semi(Oid operator, int opr_codenum, VariableStatData *vardata1, VariableStatData *vardata2); static Selectivity mcv_population(float4 *mcv_numbers, int mcv_nvalues); -static Selectivity inet_hist_value_sel(Datum *values, int nvalues, +static Selectivity inet_hist_value_sel(const Datum *values, int nvalues, Datum constvalue, int opr_codenum); static Selectivity inet_mcv_join_sel(Datum *mcv1_values, float4 *mcv1_numbers, int mcv1_nvalues, Datum *mcv2_values, float4 *mcv2_numbers, int mcv2_nvalues, Oid operator); -static Selectivity inet_mcv_hist_sel(Datum *mcv_values, float4 *mcv_numbers, - int mcv_nvalues, Datum *hist_values, int hist_nvalues, +static Selectivity inet_mcv_hist_sel(const Datum *mcv_values, float4 *mcv_numbers, + int mcv_nvalues, const Datum *hist_values, int hist_nvalues, int opr_codenum); -static Selectivity inet_hist_inclusion_join_sel(Datum *hist1_values, +static Selectivity inet_hist_inclusion_join_sel(const Datum *hist1_values, int hist1_nvalues, - Datum *hist2_values, int hist2_nvalues, + const Datum *hist2_values, int hist2_nvalues, int opr_codenum); static Selectivity inet_semi_join_sel(Datum lhs_value, bool mcv_exists, Datum *mcv_values, int mcv_nvalues, @@ -82,6 +82,7 @@ networksel(PG_FUNCTION_ARGS) Oid operator = PG_GETARG_OID(1); List *args = (List *) PG_GETARG_POINTER(2); int varRelid = PG_GETARG_INT32(3); + int opr_codenum; VariableStatData vardata; Node *other; bool varonleft; @@ -95,6 +96,14 @@ networksel(PG_FUNCTION_ARGS) nullfrac; FmgrInfo proc; + /* + * Before all else, verify that the operator is one of the ones supported + * by this function, which in turn proves that the input datatypes are + * what we expect. Otherwise, attaching this selectivity function to some + * unexpected operator could cause trouble. + */ + opr_codenum = inet_opr_codenum(operator); + /* * If expression is not (variable op something) or (something op * variable), then punt and return a default estimate. @@ -150,13 +159,12 @@ networksel(PG_FUNCTION_ARGS) STATISTIC_KIND_HISTOGRAM, InvalidOid, ATTSTATSSLOT_VALUES)) { - int opr_codenum = inet_opr_codenum(operator); + int h_codenum; /* Commute if needed, so we can consider histogram to be on the left */ - if (!varonleft) - opr_codenum = -opr_codenum; + h_codenum = varonleft ? opr_codenum : -opr_codenum; non_mcv_selec = inet_hist_value_sel(hslot.values, hslot.nvalues, - constvalue, opr_codenum); + constvalue, h_codenum); free_attstatsslot(&hslot); } @@ -203,10 +211,19 @@ networkjoinsel(PG_FUNCTION_ARGS) #endif SpecialJoinInfo *sjinfo = (SpecialJoinInfo *) PG_GETARG_POINTER(4); double selec; + int opr_codenum; VariableStatData vardata1; VariableStatData vardata2; bool join_is_reversed; + /* + * Before all else, verify that the operator is one of the ones supported + * by this function, which in turn proves that the input datatypes are + * what we expect. Otherwise, attaching this selectivity function to some + * unexpected operator could cause trouble. + */ + opr_codenum = inet_opr_codenum(operator); + get_join_variables(root, args, sjinfo, &vardata1, &vardata2, &join_is_reversed); @@ -220,15 +237,18 @@ networkjoinsel(PG_FUNCTION_ARGS) * Selectivity for left/full join is not exactly the same as inner * join, but we neglect the difference, as eqjoinsel does. */ - selec = networkjoinsel_inner(operator, &vardata1, &vardata2); + selec = networkjoinsel_inner(operator, opr_codenum, + &vardata1, &vardata2); break; case JOIN_SEMI: case JOIN_ANTI: /* Here, it's important that we pass the outer var on the left. */ if (!join_is_reversed) - selec = networkjoinsel_semi(operator, &vardata1, &vardata2); + selec = networkjoinsel_semi(operator, opr_codenum, + &vardata1, &vardata2); else selec = networkjoinsel_semi(get_commutator(operator), + -opr_codenum, &vardata2, &vardata1); break; default: @@ -260,7 +280,7 @@ networkjoinsel(PG_FUNCTION_ARGS) * Also, MCV vs histogram selectivity is not neglected as in eqjoinsel_inner(). */ static Selectivity -networkjoinsel_inner(Oid operator, +networkjoinsel_inner(Oid operator, int opr_codenum, VariableStatData *vardata1, VariableStatData *vardata2) { Form_pg_statistic stats; @@ -273,7 +293,6 @@ networkjoinsel_inner(Oid operator, mcv2_exists = false, hist1_exists = false, hist2_exists = false; - int opr_codenum; int mcv1_length = 0, mcv2_length = 0; AttStatsSlot mcv1_slot; @@ -325,8 +344,6 @@ networkjoinsel_inner(Oid operator, memset(&hist2_slot, 0, sizeof(hist2_slot)); } - opr_codenum = inet_opr_codenum(operator); - /* * Calculate selectivity for MCV vs MCV matches. */ @@ -387,7 +404,7 @@ networkjoinsel_inner(Oid operator, * histogram selectivity for semi/anti join cases. */ static Selectivity -networkjoinsel_semi(Oid operator, +networkjoinsel_semi(Oid operator, int opr_codenum, VariableStatData *vardata1, VariableStatData *vardata2) { Form_pg_statistic stats; @@ -401,7 +418,6 @@ networkjoinsel_semi(Oid operator, mcv2_exists = false, hist1_exists = false, hist2_exists = false; - int opr_codenum; FmgrInfo proc; int i, mcv1_length = 0, @@ -455,7 +471,6 @@ networkjoinsel_semi(Oid operator, memset(&hist2_slot, 0, sizeof(hist2_slot)); } - opr_codenum = inet_opr_codenum(operator); fmgr_info(get_opcode(operator), &proc); /* Estimate number of input rows represented by RHS histogram. */ @@ -601,7 +616,7 @@ mcv_population(float4 *mcv_numbers, int mcv_nvalues) * better option than not considering these buckets at all. */ static Selectivity -inet_hist_value_sel(Datum *values, int nvalues, Datum constvalue, +inet_hist_value_sel(const Datum *values, int nvalues, Datum constvalue, int opr_codenum) { Selectivity match = 0.0; @@ -702,8 +717,8 @@ inet_mcv_join_sel(Datum *mcv1_values, float4 *mcv1_numbers, int mcv1_nvalues, * the histogram. */ static Selectivity -inet_mcv_hist_sel(Datum *mcv_values, float4 *mcv_numbers, int mcv_nvalues, - Datum *hist_values, int hist_nvalues, +inet_mcv_hist_sel(const Datum *mcv_values, float4 *mcv_numbers, int mcv_nvalues, + const Datum *hist_values, int hist_nvalues, int opr_codenum) { Selectivity selec = 0.0; @@ -739,8 +754,8 @@ inet_mcv_hist_sel(Datum *mcv_values, float4 *mcv_numbers, int mcv_nvalues, * average? That would at least avoid non-commutative estimation results. */ static Selectivity -inet_hist_inclusion_join_sel(Datum *hist1_values, int hist1_nvalues, - Datum *hist2_values, int hist2_nvalues, +inet_hist_inclusion_join_sel(const Datum *hist1_values, int hist1_nvalues, + const Datum *hist2_values, int hist2_nvalues, int opr_codenum) { double match = 0.0; @@ -827,6 +842,9 @@ inet_semi_join_sel(Datum lhs_value, /* * Assign useful code numbers for the subnet inclusion/overlap operators * + * This will throw an error if the operator is not one of the ones we + * support in networksel() and networkjoinsel(). + * * Only inet_masklen_inclusion_cmp() and inet_hist_match_divider() depend * on the exact codes assigned here; but many other places in this file * know that they can negate a code to obtain the code for the commutator diff --git a/src/backend/utils/adt/network_spgist.c b/src/backend/utils/adt/network_spgist.c index a84747d927586..52e3c666d4f49 100644 --- a/src/backend/utils/adt/network_spgist.c +++ b/src/backend/utils/adt/network_spgist.c @@ -21,7 +21,7 @@ * the address family, everything goes into node 0 (which will probably * lead to creating an allTheSame tuple). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -50,7 +50,9 @@ static int inet_spg_consistent_bitmap(const inet *prefix, int nkeys, Datum inet_spg_config(PG_FUNCTION_ARGS) { - /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ +#ifdef NOT_USED + spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); +#endif spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); cfg->prefixType = CIDROID; @@ -196,8 +198,8 @@ inet_spg_picksplit(PG_FUNCTION_ARGS) /* Don't need labels; allocate output arrays */ out->nodeLabels = NULL; - out->mapTuplesToNodes = (int *) palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = (Datum *) palloc(sizeof(Datum) * in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); if (differentFamilies) { @@ -301,7 +303,7 @@ inet_spg_inner_consistent(PG_FUNCTION_ARGS) if (which) { - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); for (i = 0; i < in->nNodes; i++) { diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c index 40dcbc7b6710b..cb23dfe9b9506 100644 --- a/src/backend/utils/adt/numeric.c +++ b/src/backend/utils/adt/numeric.c @@ -11,7 +11,7 @@ * Transactions on Mathematical Software, Vol. 24, No. 4, December 1998, * pages 359-367. * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/numeric.c @@ -28,6 +28,7 @@ #include "common/hashfn.h" #include "common/int.h" +#include "common/int128.h" #include "funcapi.h" #include "lib/hyperloglog.h" #include "libpq/pqformat.h" @@ -47,8 +48,8 @@ * Uncomment the following to enable compilation of dump_numeric() * and dump_var() and to get a dump of any result produced by make_result(). * ---------- -#define NUMERIC_DEBUG */ +/* #define NUMERIC_DEBUG */ /* ---------- @@ -391,30 +392,21 @@ typedef struct NumericSumAccum /* * We define our own macros for packing and unpacking abbreviated-key - * representations for numeric values in order to avoid depending on - * USE_FLOAT8_BYVAL. The type of abbreviation we use is based only on - * the size of a datum, not the argument-passing convention for float8. + * representations, just to have a notational indication that that's + * what we're doing. Now that sizeof(Datum) is always 8, we can rely + * on fitting an int64 into Datum. * - * The range of abbreviations for finite values is from +PG_INT64/32_MAX - * to -PG_INT64/32_MAX. NaN has the abbreviation PG_INT64/32_MIN, and we + * The range of abbreviations for finite values is from +PG_INT64_MAX + * to -PG_INT64_MAX. NaN has the abbreviation PG_INT64_MIN, and we * define the sort ordering to make that work out properly (see further * comments below). PINF and NINF share the abbreviations of the largest * and smallest finite abbreviation classes. */ -#define NUMERIC_ABBREV_BITS (SIZEOF_DATUM * BITS_PER_BYTE) -#if SIZEOF_DATUM == 8 -#define NumericAbbrevGetDatum(X) ((Datum) (X)) -#define DatumGetNumericAbbrev(X) ((int64) (X)) +#define NumericAbbrevGetDatum(X) Int64GetDatum(X) +#define DatumGetNumericAbbrev(X) DatumGetInt64(X) #define NUMERIC_ABBREV_NAN NumericAbbrevGetDatum(PG_INT64_MIN) #define NUMERIC_ABBREV_PINF NumericAbbrevGetDatum(-PG_INT64_MAX) #define NUMERIC_ABBREV_NINF NumericAbbrevGetDatum(PG_INT64_MAX) -#else -#define NumericAbbrevGetDatum(X) ((Datum) (X)) -#define DatumGetNumericAbbrev(X) ((int32) (X)) -#define NUMERIC_ABBREV_NAN NumericAbbrevGetDatum(PG_INT32_MIN) -#define NUMERIC_ABBREV_PINF NumericAbbrevGetDatum(-PG_INT32_MAX) -#define NUMERIC_ABBREV_NINF NumericAbbrevGetDatum(PG_INT32_MAX) -#endif /* ---------- @@ -525,7 +517,7 @@ static void numericvar_deserialize(StringInfo buf, NumericVar *var); static Numeric duplicate_numeric(Numeric num); static Numeric make_result(const NumericVar *var); -static Numeric make_result_opt_error(const NumericVar *var, bool *have_error); +static Numeric make_result_safe(const NumericVar *var, Node *escontext); static bool apply_typmod(NumericVar *var, int32 typmod, Node *escontext); static bool apply_typmod_special(Numeric num, int32 typmod, Node *escontext); @@ -534,10 +526,7 @@ static bool numericvar_to_int32(const NumericVar *var, int32 *result); static bool numericvar_to_int64(const NumericVar *var, int64 *result); static void int64_to_numericvar(int64 val, NumericVar *var); static bool numericvar_to_uint64(const NumericVar *var, uint64 *result); -#ifdef HAVE_INT128 -static bool numericvar_to_int128(const NumericVar *var, int128 *result); -static void int128_to_numericvar(int128 val, NumericVar *var); -#endif +static void int128_to_numericvar(INT128 val, NumericVar *var); static double numericvar_to_double_no_overflow(const NumericVar *var); static Datum numeric_abbrev_convert(Datum original_datum, SortSupport ssup); @@ -728,7 +717,6 @@ numeric_in(PG_FUNCTION_ARGS) */ NumericVar value; int base; - bool have_error; init_var(&value); @@ -787,12 +775,7 @@ numeric_in(PG_FUNCTION_ARGS) if (!apply_typmod(&value, typmod, escontext)) PG_RETURN_NULL(); - res = make_result_opt_error(&value, &have_error); - - if (have_error) - ereturn(escontext, (Datum) 0, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value overflows numeric format"))); + res = make_result_safe(&value, escontext); free_var(&value); } @@ -1261,7 +1244,8 @@ numeric (PG_FUNCTION_ARGS) */ if (NUMERIC_IS_SPECIAL(num)) { - (void) apply_typmod_special(num, typmod, NULL); + if (!apply_typmod_special(num, typmod, fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_NUMERIC(duplicate_numeric(num)); } @@ -1312,8 +1296,9 @@ numeric (PG_FUNCTION_ARGS) init_var(&var); set_var_from_num(num, &var); - (void) apply_typmod(&var, typmod, NULL); - new = make_result(&var); + if (!apply_typmod(&var, typmod, fcinfo->context)) + PG_RETURN_NULL(); + new = make_result_safe(&var, fcinfo->context); free_var(&var); @@ -1776,8 +1761,7 @@ generate_series_step_numeric(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - fctx = (generate_series_numeric_fctx *) - palloc(sizeof(generate_series_numeric_fctx)); + fctx = palloc_object(generate_series_numeric_fctx); /* * Use fctx to keep state from call to call. Seed current with the @@ -1958,9 +1942,11 @@ generate_series_numeric_support(PG_FUNCTION_ARGS) * in the histogram. width_bucket() returns an integer indicating the * bucket number that 'operand' belongs to in an equiwidth histogram * with the specified characteristics. An operand smaller than the - * lower bound is assigned to bucket 0. An operand greater than the - * upper bound is assigned to an additional bucket (with number - * count+1). We don't allow "NaN" for any of the numeric arguments. + * lower bound is assigned to bucket 0. An operand greater than or equal + * to the upper bound is assigned to an additional bucket (with number + * count+1). We don't allow the histogram bounds to be NaN or +/- infinity, + * but we do allow those values for the operand (taking NaN to be larger + * than any other value, as we do in comparisons). */ Datum width_bucket_numeric(PG_FUNCTION_ARGS) @@ -1978,17 +1964,13 @@ width_bucket_numeric(PG_FUNCTION_ARGS) (errcode(ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION), errmsg("count must be greater than zero"))); - if (NUMERIC_IS_SPECIAL(operand) || - NUMERIC_IS_SPECIAL(bound1) || - NUMERIC_IS_SPECIAL(bound2)) + if (NUMERIC_IS_SPECIAL(bound1) || NUMERIC_IS_SPECIAL(bound2)) { - if (NUMERIC_IS_NAN(operand) || - NUMERIC_IS_NAN(bound1) || - NUMERIC_IS_NAN(bound2)) + if (NUMERIC_IS_NAN(bound1) || NUMERIC_IS_NAN(bound2)) ereport(ERROR, (errcode(ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION), - errmsg("operand, lower bound, and upper bound cannot be NaN"))); - /* We allow "operand" to be infinite; cmp_numerics will cope */ + errmsg("lower and upper bounds cannot be NaN"))); + if (NUMERIC_IS_INF(bound1) || NUMERIC_IS_INF(bound2)) ereport(ERROR, (errcode(ERRCODE_INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION), @@ -2100,12 +2082,11 @@ compute_bucket(Numeric operand, Numeric bound1, Numeric bound2, * while this could be worked on itself, the abbreviation strategy gives more * speedup in many common cases. * - * Two different representations are used for the abbreviated form, one in - * int32 and one in int64, whichever fits into a by-value Datum. In both cases - * the representation is negated relative to the original value, because we use - * the largest negative value for NaN, which sorts higher than other values. We - * convert the absolute value of the numeric to a 31-bit or 63-bit positive - * value, and then negate it if the original number was positive. + * The abbreviated format is an int64. The representation is negated relative + * to the original value, because we use the largest negative value for NaN, + * which sorts higher than other values. We convert the absolute value of the + * numeric to a 63-bit positive value, and then negate it if the original + * number was positive. * * We abort the abbreviation process if the abbreviation cardinality is below * 0.01% of the row count (1 per 10k non-null rows). The actual break-even @@ -2137,7 +2118,7 @@ numeric_sortsupport(PG_FUNCTION_ARGS) NumericSortSupport *nss; MemoryContext oldcontext = MemoryContextSwitchTo(ssup->ssup_cxt); - nss = palloc(sizeof(NumericSortSupport)); + nss = palloc_object(NumericSortSupport); /* * palloc a buffer for handling unaligned packed values in addition to @@ -2214,7 +2195,7 @@ numeric_abbrev_convert(Datum original_datum, SortSupport ssup) } /* should happen only for external/compressed toasts */ - if ((Pointer) original_varatt != DatumGetPointer(original_datum)) + if (original_varatt != DatumGetPointer(original_datum)) pfree(original_varatt); return result; @@ -2304,9 +2285,9 @@ numeric_fast_cmp(Datum x, Datum y, SortSupport ssup) result = cmp_numerics(nx, ny); - if ((Pointer) nx != DatumGetPointer(x)) + if (nx != DatumGetPointer(x)) pfree(nx); - if ((Pointer) ny != DatumGetPointer(y)) + if (ny != DatumGetPointer(y)) pfree(ny); return result; @@ -2332,7 +2313,7 @@ numeric_cmp_abbrev(Datum x, Datum y, SortSupport ssup) } /* - * Abbreviate a NumericVar according to the available bit size. + * Abbreviate a NumericVar into the 64-bit sortsupport size. * * The 31-bit value is constructed as: * @@ -2376,9 +2357,6 @@ numeric_cmp_abbrev(Datum x, Datum y, SortSupport ssup) * with all bits zero. This allows simple comparisons to work on the composite * value. */ - -#if NUMERIC_ABBREV_BITS == 64 - static Datum numeric_abbrev_convert_var(const NumericVar *var, NumericSortSupport *nss) { @@ -2402,13 +2380,13 @@ numeric_abbrev_convert_var(const NumericVar *var, NumericSortSupport *nss) { default: result |= ((int64) var->digits[3]); - /* FALLTHROUGH */ + pg_fallthrough; case 3: result |= ((int64) var->digits[2]) << 14; - /* FALLTHROUGH */ + pg_fallthrough; case 2: result |= ((int64) var->digits[1]) << 28; - /* FALLTHROUGH */ + pg_fallthrough; case 1: result |= ((int64) var->digits[0]) << 42; break; @@ -2430,84 +2408,6 @@ numeric_abbrev_convert_var(const NumericVar *var, NumericSortSupport *nss) return NumericAbbrevGetDatum(result); } -#endif /* NUMERIC_ABBREV_BITS == 64 */ - -#if NUMERIC_ABBREV_BITS == 32 - -static Datum -numeric_abbrev_convert_var(const NumericVar *var, NumericSortSupport *nss) -{ - int ndigits = var->ndigits; - int weight = var->weight; - int32 result; - - if (ndigits == 0 || weight < -11) - { - result = 0; - } - else if (weight > 20) - { - result = PG_INT32_MAX; - } - else - { - NumericDigit nxt1 = (ndigits > 1) ? var->digits[1] : 0; - - weight = (weight + 11) * 4; - - result = var->digits[0]; - - /* - * "result" now has 1 to 4 nonzero decimal digits. We pack in more - * digits to make 7 in total (largest we can fit in 24 bits) - */ - - if (result > 999) - { - /* already have 4 digits, add 3 more */ - result = (result * 1000) + (nxt1 / 10); - weight += 3; - } - else if (result > 99) - { - /* already have 3 digits, add 4 more */ - result = (result * 10000) + nxt1; - weight += 2; - } - else if (result > 9) - { - NumericDigit nxt2 = (ndigits > 2) ? var->digits[2] : 0; - - /* already have 2 digits, add 5 more */ - result = (result * 100000) + (nxt1 * 10) + (nxt2 / 1000); - weight += 1; - } - else - { - NumericDigit nxt2 = (ndigits > 2) ? var->digits[2] : 0; - - /* already have 1 digit, add 6 more */ - result = (result * 1000000) + (nxt1 * 100) + (nxt2 / 100); - } - - result = result | (weight << 24); - } - - /* the abbrev is negated relative to the original */ - if (var->sign == NUMERIC_POS) - result = -result; - - if (nss->estimating) - { - uint32 tmp = (uint32) result; - - addHyperLogLog(&nss->abbr_card, DatumGetUInt32(hash_uint32(tmp))); - } - - return NumericAbbrevGetDatum(result); -} - -#endif /* NUMERIC_ABBREV_BITS == 32 */ /* * Ordinary (non-sortsupport) comparisons follow. @@ -2969,20 +2869,18 @@ numeric_add(PG_FUNCTION_ARGS) Numeric num2 = PG_GETARG_NUMERIC(1); Numeric res; - res = numeric_add_opt_error(num1, num2, NULL); + res = numeric_add_safe(num1, num2, NULL); PG_RETURN_NUMERIC(res); } /* - * numeric_add_opt_error() - + * numeric_add_safe() - * - * Internal version of numeric_add(). If "*have_error" flag is provided, - * on error it's set to true, NULL returned. This is helpful when caller - * need to handle errors by itself. + * Internal version of numeric_add() with support for soft error reporting. */ Numeric -numeric_add_opt_error(Numeric num1, Numeric num2, bool *have_error) +numeric_add_safe(Numeric num1, Numeric num2, Node *escontext) { NumericVar arg1; NumericVar arg2; @@ -3026,7 +2924,7 @@ numeric_add_opt_error(Numeric num1, Numeric num2, bool *have_error) init_var(&result); add_var(&arg1, &arg2, &result); - res = make_result_opt_error(&result, have_error); + res = make_result_safe(&result, escontext); free_var(&result); @@ -3046,21 +2944,19 @@ numeric_sub(PG_FUNCTION_ARGS) Numeric num2 = PG_GETARG_NUMERIC(1); Numeric res; - res = numeric_sub_opt_error(num1, num2, NULL); + res = numeric_sub_safe(num1, num2, NULL); PG_RETURN_NUMERIC(res); } /* - * numeric_sub_opt_error() - + * numeric_sub_safe() - * - * Internal version of numeric_sub(). If "*have_error" flag is provided, - * on error it's set to true, NULL returned. This is helpful when caller - * need to handle errors by itself. + * Internal version of numeric_sub() with support for soft error reporting. */ Numeric -numeric_sub_opt_error(Numeric num1, Numeric num2, bool *have_error) +numeric_sub_safe(Numeric num1, Numeric num2, Node *escontext) { NumericVar arg1; NumericVar arg2; @@ -3104,7 +3000,7 @@ numeric_sub_opt_error(Numeric num1, Numeric num2, bool *have_error) init_var(&result); sub_var(&arg1, &arg2, &result); - res = make_result_opt_error(&result, have_error); + res = make_result_safe(&result, escontext); free_var(&result); @@ -3124,21 +3020,22 @@ numeric_mul(PG_FUNCTION_ARGS) Numeric num2 = PG_GETARG_NUMERIC(1); Numeric res; - res = numeric_mul_opt_error(num1, num2, NULL); + res = numeric_mul_safe(num1, num2, fcinfo->context); + + if (unlikely(SOFT_ERROR_OCCURRED(fcinfo->context))) + PG_RETURN_NULL(); PG_RETURN_NUMERIC(res); } /* - * numeric_mul_opt_error() - + * numeric_mul_safe() - * - * Internal version of numeric_mul(). If "*have_error" flag is provided, - * on error it's set to true, NULL returned. This is helpful when caller - * need to handle errors by itself. + * Internal version of numeric_mul() with support for soft error reporting. */ Numeric -numeric_mul_opt_error(Numeric num1, Numeric num2, bool *have_error) +numeric_mul_safe(Numeric num1, Numeric num2, Node *escontext) { NumericVar arg1; NumericVar arg2; @@ -3225,7 +3122,7 @@ numeric_mul_opt_error(Numeric num1, Numeric num2, bool *have_error) if (result.dscale > NUMERIC_DSCALE_MAX) round_var(&result, NUMERIC_DSCALE_MAX); - res = make_result_opt_error(&result, have_error); + res = make_result_safe(&result, escontext); free_var(&result); @@ -3245,21 +3142,19 @@ numeric_div(PG_FUNCTION_ARGS) Numeric num2 = PG_GETARG_NUMERIC(1); Numeric res; - res = numeric_div_opt_error(num1, num2, NULL); + res = numeric_div_safe(num1, num2, NULL); PG_RETURN_NUMERIC(res); } /* - * numeric_div_opt_error() - + * numeric_div_safe() - * - * Internal version of numeric_div(). If "*have_error" flag is provided, - * on error it's set to true, NULL returned. This is helpful when caller - * need to handle errors by itself. + * Internal version of numeric_div() with support for soft error reporting. */ Numeric -numeric_div_opt_error(Numeric num1, Numeric num2, bool *have_error) +numeric_div_safe(Numeric num1, Numeric num2, Node *escontext) { NumericVar arg1; NumericVar arg2; @@ -3267,9 +3162,6 @@ numeric_div_opt_error(Numeric num1, Numeric num2, bool *have_error) Numeric res; int rscale; - if (have_error) - *have_error = false; - /* * Handle NaN and infinities */ @@ -3284,15 +3176,7 @@ numeric_div_opt_error(Numeric num1, Numeric num2, bool *have_error) switch (numeric_sign_internal(num2)) { case 0: - if (have_error) - { - *have_error = true; - return NULL; - } - ereport(ERROR, - (errcode(ERRCODE_DIVISION_BY_ZERO), - errmsg("division by zero"))); - break; + goto division_by_zero; case 1: return make_result(&const_pinf); case -1: @@ -3307,15 +3191,7 @@ numeric_div_opt_error(Numeric num1, Numeric num2, bool *have_error) switch (numeric_sign_internal(num2)) { case 0: - if (have_error) - { - *have_error = true; - return NULL; - } - ereport(ERROR, - (errcode(ERRCODE_DIVISION_BY_ZERO), - errmsg("division by zero"))); - break; + goto division_by_zero; case 1: return make_result(&const_ninf); case -1: @@ -3346,25 +3222,25 @@ numeric_div_opt_error(Numeric num1, Numeric num2, bool *have_error) */ rscale = select_div_scale(&arg1, &arg2); - /* - * If "have_error" is provided, check for division by zero here - */ - if (have_error && (arg2.ndigits == 0 || arg2.digits[0] == 0)) - { - *have_error = true; - return NULL; - } + /* Check for division by zero */ + if (arg2.ndigits == 0 || arg2.digits[0] == 0) + goto division_by_zero; /* * Do the divide and return the result */ div_var(&arg1, &arg2, &result, rscale, true, true); - res = make_result_opt_error(&result, have_error); + res = make_result_safe(&result, escontext); free_var(&result); return res; + +division_by_zero: + ereturn(escontext, NULL, + errcode(ERRCODE_DIVISION_BY_ZERO), + errmsg("division by zero")); } @@ -3469,30 +3345,25 @@ numeric_mod(PG_FUNCTION_ARGS) Numeric num2 = PG_GETARG_NUMERIC(1); Numeric res; - res = numeric_mod_opt_error(num1, num2, NULL); + res = numeric_mod_safe(num1, num2, NULL); PG_RETURN_NUMERIC(res); } /* - * numeric_mod_opt_error() - + * numeric_mod_safe() - * - * Internal version of numeric_mod(). If "*have_error" flag is provided, - * on error it's set to true, NULL returned. This is helpful when caller - * need to handle errors by itself. + * Internal version of numeric_mod() with support for soft error reporting. */ Numeric -numeric_mod_opt_error(Numeric num1, Numeric num2, bool *have_error) +numeric_mod_safe(Numeric num1, Numeric num2, Node *escontext) { Numeric res; NumericVar arg1; NumericVar arg2; NumericVar result; - if (have_error) - *have_error = false; - /* * Handle NaN and infinities. We follow POSIX fmod() on this, except that * POSIX treats x-is-infinite and y-is-zero identically, raising EDOM and @@ -3505,16 +3376,8 @@ numeric_mod_opt_error(Numeric num1, Numeric num2, bool *have_error) if (NUMERIC_IS_INF(num1)) { if (numeric_sign_internal(num2) == 0) - { - if (have_error) - { - *have_error = true; - return NULL; - } - ereport(ERROR, - (errcode(ERRCODE_DIVISION_BY_ZERO), - errmsg("division by zero"))); - } + goto division_by_zero; + /* Inf % any nonzero = NaN */ return make_result(&const_nan); } @@ -3527,22 +3390,22 @@ numeric_mod_opt_error(Numeric num1, Numeric num2, bool *have_error) init_var(&result); - /* - * If "have_error" is provided, check for division by zero here - */ - if (have_error && (arg2.ndigits == 0 || arg2.digits[0] == 0)) - { - *have_error = true; - return NULL; - } + /* Check for division by zero */ + if (arg2.ndigits == 0 || arg2.digits[0] == 0) + goto division_by_zero; mod_var(&arg1, &arg2, &result); - res = make_result_opt_error(&result, NULL); + res = make_result_safe(&result, escontext); free_var(&result); return res; + +division_by_zero: + ereturn(escontext, NULL, + errcode(ERRCODE_DIVISION_BY_ZERO), + errmsg("division by zero")); } @@ -4465,25 +4328,13 @@ int64_div_fast_to_numeric(int64 val1, int log10val2) if (unlikely(pg_mul_s64_overflow(val1, factor, &new_val1))) { -#ifdef HAVE_INT128 /* do the multiplication using 128-bit integers */ - int128 tmp; + INT128 tmp; - tmp = (int128) val1 * (int128) factor; + tmp = int64_to_int128(0); + int128_add_int64_mul_int64(&tmp, val1, factor); int128_to_numericvar(tmp, &result); -#else - /* do the multiplication using numerics */ - NumericVar tmp; - - init_var(&tmp); - - int64_to_numericvar(val1, &result); - int64_to_numericvar(factor, &tmp); - mul_var(&result, &tmp, &result, 0); - - free_var(&tmp); -#endif } else int64_to_numericvar(new_val1, &result); @@ -4511,52 +4362,34 @@ int4_numeric(PG_FUNCTION_ARGS) PG_RETURN_NUMERIC(int64_to_numeric(val)); } +/* + * Internal version of numeric_int4() with support for soft error reporting. + */ int32 -numeric_int4_opt_error(Numeric num, bool *have_error) +numeric_int4_safe(Numeric num, Node *escontext) { NumericVar x; int32 result; - if (have_error) - *have_error = false; - if (NUMERIC_IS_SPECIAL(num)) { - if (have_error) - { - *have_error = true; - return 0; - } + if (NUMERIC_IS_NAN(num)) + ereturn(escontext, 0, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert NaN to %s", "integer"))); else - { - if (NUMERIC_IS_NAN(num)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot convert NaN to %s", "integer"))); - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot convert infinity to %s", "integer"))); - } + ereturn(escontext, 0, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert infinity to %s", "integer"))); } /* Convert to variable format, then convert to int4 */ init_var_from_num(num, &x); if (!numericvar_to_int32(&x, &result)) - { - if (have_error) - { - *have_error = true; - return 0; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("integer out of range"))); - } - } + ereturn(escontext, 0, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("integer out of range"))); return result; } @@ -4565,8 +4398,14 @@ Datum numeric_int4(PG_FUNCTION_ARGS) { Numeric num = PG_GETARG_NUMERIC(0); + int32 result; - PG_RETURN_INT32(numeric_int4_opt_error(num, NULL)); + result = numeric_int4_safe(num, fcinfo->context); + + if (unlikely(SOFT_ERROR_OCCURRED(fcinfo->context))) + PG_RETURN_NULL(); + + PG_RETURN_INT32(result); } /* @@ -4599,52 +4438,34 @@ int8_numeric(PG_FUNCTION_ARGS) PG_RETURN_NUMERIC(int64_to_numeric(val)); } +/* + * Internal version of numeric_int8() with support for soft error reporting. + */ int64 -numeric_int8_opt_error(Numeric num, bool *have_error) +numeric_int8_safe(Numeric num, Node *escontext) { NumericVar x; int64 result; - if (have_error) - *have_error = false; - if (NUMERIC_IS_SPECIAL(num)) { - if (have_error) - { - *have_error = true; - return 0; - } + if (NUMERIC_IS_NAN(num)) + ereturn(escontext, 0, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert NaN to %s", "bigint"))); else - { - if (NUMERIC_IS_NAN(num)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot convert NaN to %s", "bigint"))); - else - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot convert infinity to %s", "bigint"))); - } + ereturn(escontext, 0, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot convert infinity to %s", "bigint"))); } /* Convert to variable format, then convert to int8 */ init_var_from_num(num, &x); if (!numericvar_to_int64(&x, &result)) - { - if (have_error) - { - *have_error = true; - return 0; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("bigint out of range"))); - } - } + ereturn(escontext, 0, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("bigint out of range"))); return result; } @@ -4653,8 +4474,14 @@ Datum numeric_int8(PG_FUNCTION_ARGS) { Numeric num = PG_GETARG_NUMERIC(0); + int64 result; - PG_RETURN_INT64(numeric_int8_opt_error(num, NULL)); + result = numeric_int8_safe(num, fcinfo->context); + + if (unlikely(SOFT_ERROR_OCCURRED(fcinfo->context))) + PG_RETURN_NULL(); + + PG_RETURN_INT64(result); } @@ -4678,11 +4505,11 @@ numeric_int2(PG_FUNCTION_ARGS) if (NUMERIC_IS_SPECIAL(num)) { if (NUMERIC_IS_NAN(num)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot convert NaN to %s", "smallint"))); else - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot convert infinity to %s", "smallint"))); } @@ -4691,12 +4518,12 @@ numeric_int2(PG_FUNCTION_ARGS) init_var_from_num(num, &x); if (!numericvar_to_int64(&x, &val)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("smallint out of range"))); if (unlikely(val < PG_INT16_MIN) || unlikely(val > PG_INT16_MAX)) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("smallint out of range"))); @@ -4732,7 +4559,8 @@ float8_numeric(PG_FUNCTION_ARGS) init_var(&result); /* Assume we need not worry about leading/trailing spaces */ - (void) set_var_from_str(buf, buf, &result, &endptr, NULL); + if (!set_var_from_str(buf, buf, &result, &endptr, fcinfo->context)) + PG_RETURN_NULL(); res = make_result(&result); @@ -4761,10 +4589,14 @@ numeric_float8(PG_FUNCTION_ARGS) tmp = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num))); - - result = DirectFunctionCall1(float8in, CStringGetDatum(tmp)); - - pfree(tmp); + if (!DirectInputFunctionCallSafe(float8in, tmp, + InvalidOid, -1, + (Node *) fcinfo->context, + &result)) + { + pfree(tmp); + PG_RETURN_NULL(); + } PG_RETURN_DATUM(result); } @@ -4826,7 +4658,8 @@ float4_numeric(PG_FUNCTION_ARGS) init_var(&result); /* Assume we need not worry about leading/trailing spaces */ - (void) set_var_from_str(buf, buf, &result, &endptr, NULL); + if (!set_var_from_str(buf, buf, &result, &endptr, fcinfo->context)) + PG_RETURN_NULL(); res = make_result(&result); @@ -4856,7 +4689,14 @@ numeric_float4(PG_FUNCTION_ARGS) tmp = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num))); - result = DirectFunctionCall1(float4in, CStringGetDatum(tmp)); + if (!DirectInputFunctionCallSafe(float4in, tmp, + InvalidOid, -1, + (Node *) fcinfo->context, + &result)) + { + pfree(tmp); + PG_RETURN_NULL(); + } pfree(tmp); @@ -4903,8 +4743,8 @@ numeric_pg_lsn(PG_FUNCTION_ARGS) * Actually, it's a pointer to a NumericAggState allocated in the aggregate * context. The digit buffers for the NumericVars will be there too. * - * On platforms which support 128-bit integers some aggregates instead use a - * 128-bit integer based transition datatype to speed up calculations. + * For integer inputs, some aggregates use special-purpose 64-bit or 128-bit + * integer based transition datatypes to speed up calculations. * * ---------------------------------------------------------------------- */ @@ -4943,7 +4783,7 @@ makeNumericAggState(FunctionCallInfo fcinfo, bool calcSumX2) old_context = MemoryContextSwitchTo(agg_context); - state = (NumericAggState *) palloc0(sizeof(NumericAggState)); + state = palloc0_object(NumericAggState); state->calcSumX2 = calcSumX2; state->agg_context = agg_context; @@ -4961,7 +4801,7 @@ makeNumericAggStateCurrentContext(bool calcSumX2) { NumericAggState *state; - state = (NumericAggState *) palloc0(sizeof(NumericAggState)); + state = palloc0_object(NumericAggState); state->calcSumX2 = calcSumX2; state->agg_context = CurrentMemoryContext; @@ -5568,26 +5408,27 @@ numeric_accum_inv(PG_FUNCTION_ARGS) /* - * Integer data types in general use Numeric accumulators to share code - * and avoid risk of overflow. + * Integer data types in general use Numeric accumulators to share code and + * avoid risk of overflow. However for performance reasons optimized + * special-purpose accumulator routines are used when possible: * - * However for performance reasons optimized special-purpose accumulator - * routines are used when possible. + * For 16-bit and 32-bit inputs, N and sum(X) fit into 64-bit, so 64-bit + * accumulators are used for SUM and AVG of these data types. * - * On platforms with 128-bit integer support, the 128-bit routines will be - * used when sum(X) or sum(X*X) fit into 128-bit. + * For 16-bit and 32-bit inputs, sum(X^2) fits into 128-bit, so 128-bit + * accumulators are used for STDDEV_POP, STDDEV_SAMP, VAR_POP, and VAR_SAMP of + * these data types. * - * For 16 and 32 bit inputs, the N and sum(X) fit into 64-bit so the 64-bit - * accumulators will be used for SUM and AVG of these data types. + * For 64-bit inputs, sum(X) fits into 128-bit, so a 128-bit accumulator is + * used for SUM(int8) and AVG(int8). */ -#ifdef HAVE_INT128 typedef struct Int128AggState { bool calcSumX2; /* if true, calculate sumX2 */ int64 N; /* count of processed numbers */ - int128 sumX; /* sum of processed numbers */ - int128 sumX2; /* sum of squares of processed numbers */ + INT128 sumX; /* sum of processed numbers */ + INT128 sumX2; /* sum of squares of processed numbers */ } Int128AggState; /* @@ -5606,7 +5447,7 @@ makeInt128AggState(FunctionCallInfo fcinfo, bool calcSumX2) old_context = MemoryContextSwitchTo(agg_context); - state = (Int128AggState *) palloc0(sizeof(Int128AggState)); + state = palloc0_object(Int128AggState); state->calcSumX2 = calcSumX2; MemoryContextSwitchTo(old_context); @@ -5623,7 +5464,7 @@ makeInt128AggStateCurrentContext(bool calcSumX2) { Int128AggState *state; - state = (Int128AggState *) palloc0(sizeof(Int128AggState)); + state = palloc0_object(Int128AggState); state->calcSumX2 = calcSumX2; return state; @@ -5633,12 +5474,12 @@ makeInt128AggStateCurrentContext(bool calcSumX2) * Accumulate a new input value for 128-bit aggregate functions. */ static void -do_int128_accum(Int128AggState *state, int128 newval) +do_int128_accum(Int128AggState *state, int64 newval) { if (state->calcSumX2) - state->sumX2 += newval * newval; + int128_add_int64_mul_int64(&state->sumX2, newval, newval); - state->sumX += newval; + int128_add_int64(&state->sumX, newval); state->N++; } @@ -5646,43 +5487,28 @@ do_int128_accum(Int128AggState *state, int128 newval) * Remove an input value from the aggregated state. */ static void -do_int128_discard(Int128AggState *state, int128 newval) +do_int128_discard(Int128AggState *state, int64 newval) { if (state->calcSumX2) - state->sumX2 -= newval * newval; + int128_sub_int64_mul_int64(&state->sumX2, newval, newval); - state->sumX -= newval; + int128_sub_int64(&state->sumX, newval); state->N--; } -typedef Int128AggState PolyNumAggState; -#define makePolyNumAggState makeInt128AggState -#define makePolyNumAggStateCurrentContext makeInt128AggStateCurrentContext -#else -typedef NumericAggState PolyNumAggState; -#define makePolyNumAggState makeNumericAggState -#define makePolyNumAggStateCurrentContext makeNumericAggStateCurrentContext -#endif - Datum int2_accum(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* Create the state data on the first call */ if (state == NULL) - state = makePolyNumAggState(fcinfo, true); + state = makeInt128AggState(fcinfo, true); if (!PG_ARGISNULL(1)) - { -#ifdef HAVE_INT128 - do_int128_accum(state, (int128) PG_GETARG_INT16(1)); -#else - do_numeric_accum(state, int64_to_numeric(PG_GETARG_INT16(1))); -#endif - } + do_int128_accum(state, PG_GETARG_INT16(1)); PG_RETURN_POINTER(state); } @@ -5690,22 +5516,16 @@ int2_accum(PG_FUNCTION_ARGS) Datum int4_accum(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* Create the state data on the first call */ if (state == NULL) - state = makePolyNumAggState(fcinfo, true); + state = makeInt128AggState(fcinfo, true); if (!PG_ARGISNULL(1)) - { -#ifdef HAVE_INT128 - do_int128_accum(state, (int128) PG_GETARG_INT32(1)); -#else - do_numeric_accum(state, int64_to_numeric(PG_GETARG_INT32(1))); -#endif - } + do_int128_accum(state, PG_GETARG_INT32(1)); PG_RETURN_POINTER(state); } @@ -5728,21 +5548,21 @@ int8_accum(PG_FUNCTION_ARGS) } /* - * Combine function for numeric aggregates which require sumX2 + * Combine function for Int128AggState for aggregates which require sumX2 */ Datum numeric_poly_combine(PG_FUNCTION_ARGS) { - PolyNumAggState *state1; - PolyNumAggState *state2; + Int128AggState *state1; + Int128AggState *state2; MemoryContext agg_context; MemoryContext old_context; if (!AggCheckCallContext(fcinfo, &agg_context)) elog(ERROR, "aggregate function called in non-aggregate context"); - state1 = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); - state2 = PG_ARGISNULL(1) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(1); + state1 = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); + state2 = PG_ARGISNULL(1) ? NULL : (Int128AggState *) PG_GETARG_POINTER(1); if (state2 == NULL) PG_RETURN_POINTER(state1); @@ -5752,16 +5572,10 @@ numeric_poly_combine(PG_FUNCTION_ARGS) { old_context = MemoryContextSwitchTo(agg_context); - state1 = makePolyNumAggState(fcinfo, true); + state1 = makeInt128AggState(fcinfo, true); state1->N = state2->N; - -#ifdef HAVE_INT128 state1->sumX = state2->sumX; state1->sumX2 = state2->sumX2; -#else - accum_sum_copy(&state1->sumX, &state2->sumX); - accum_sum_copy(&state1->sumX2, &state2->sumX2); -#endif MemoryContextSwitchTo(old_context); @@ -5771,54 +5585,51 @@ numeric_poly_combine(PG_FUNCTION_ARGS) if (state2->N > 0) { state1->N += state2->N; + int128_add_int128(&state1->sumX, state2->sumX); + int128_add_int128(&state1->sumX2, state2->sumX2); + } + PG_RETURN_POINTER(state1); +} -#ifdef HAVE_INT128 - state1->sumX += state2->sumX; - state1->sumX2 += state2->sumX2; -#else - /* The rest of this needs to work in the aggregate context */ - old_context = MemoryContextSwitchTo(agg_context); - - /* Accumulate sums */ - accum_sum_combine(&state1->sumX, &state2->sumX); - accum_sum_combine(&state1->sumX2, &state2->sumX2); +/* + * int128_serialize - serialize a 128-bit integer to binary format + */ +static inline void +int128_serialize(StringInfo buf, INT128 val) +{ + pq_sendint64(buf, PG_INT128_HI_INT64(val)); + pq_sendint64(buf, PG_INT128_LO_UINT64(val)); +} - MemoryContextSwitchTo(old_context); -#endif +/* + * int128_deserialize - deserialize binary format to a 128-bit integer. + */ +static inline INT128 +int128_deserialize(StringInfo buf) +{ + int64 hi = pq_getmsgint64(buf); + uint64 lo = pq_getmsgint64(buf); - } - PG_RETURN_POINTER(state1); + return make_int128(hi, lo); } /* * numeric_poly_serialize - * Serialize PolyNumAggState into bytea for aggregate functions which + * Serialize Int128AggState into bytea for aggregate functions which * require sumX2. */ Datum numeric_poly_serialize(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; StringInfoData buf; bytea *result; - NumericVar tmp_var; /* Ensure we disallow calling when not in aggregate context */ if (!AggCheckCallContext(fcinfo, NULL)) elog(ERROR, "aggregate function called in non-aggregate context"); - state = (PolyNumAggState *) PG_GETARG_POINTER(0); - - /* - * If the platform supports int128 then sumX and sumX2 will be a 128 bit - * integer type. Here we'll convert that into a numeric type so that the - * combine state is in the same format for both int128 enabled machines - * and machines which don't support that type. The logic here is that one - * day we might like to send these over to another server for further - * processing and we want a standard format to work with. - */ - - init_var(&tmp_var); + state = (Int128AggState *) PG_GETARG_POINTER(0); pq_begintypsend(&buf); @@ -5826,48 +5637,33 @@ numeric_poly_serialize(PG_FUNCTION_ARGS) pq_sendint64(&buf, state->N); /* sumX */ -#ifdef HAVE_INT128 - int128_to_numericvar(state->sumX, &tmp_var); -#else - accum_sum_final(&state->sumX, &tmp_var); -#endif - numericvar_serialize(&buf, &tmp_var); + int128_serialize(&buf, state->sumX); /* sumX2 */ -#ifdef HAVE_INT128 - int128_to_numericvar(state->sumX2, &tmp_var); -#else - accum_sum_final(&state->sumX2, &tmp_var); -#endif - numericvar_serialize(&buf, &tmp_var); + int128_serialize(&buf, state->sumX2); result = pq_endtypsend(&buf); - free_var(&tmp_var); - PG_RETURN_BYTEA_P(result); } /* * numeric_poly_deserialize - * Deserialize PolyNumAggState from bytea for aggregate functions which + * Deserialize Int128AggState from bytea for aggregate functions which * require sumX2. */ Datum numeric_poly_deserialize(PG_FUNCTION_ARGS) { bytea *sstate; - PolyNumAggState *result; + Int128AggState *result; StringInfoData buf; - NumericVar tmp_var; if (!AggCheckCallContext(fcinfo, NULL)) elog(ERROR, "aggregate function called in non-aggregate context"); sstate = PG_GETARG_BYTEA_PP(0); - init_var(&tmp_var); - /* * Initialize a StringInfo so that we can "receive" it using the standard * recv-function infrastructure. @@ -5875,31 +5671,19 @@ numeric_poly_deserialize(PG_FUNCTION_ARGS) initReadOnlyStringInfo(&buf, VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate)); - result = makePolyNumAggStateCurrentContext(false); + result = makeInt128AggStateCurrentContext(false); /* N */ result->N = pq_getmsgint64(&buf); /* sumX */ - numericvar_deserialize(&buf, &tmp_var); -#ifdef HAVE_INT128 - numericvar_to_int128(&tmp_var, &result->sumX); -#else - accum_sum_add(&result->sumX, &tmp_var); -#endif + result->sumX = int128_deserialize(&buf); /* sumX2 */ - numericvar_deserialize(&buf, &tmp_var); -#ifdef HAVE_INT128 - numericvar_to_int128(&tmp_var, &result->sumX2); -#else - accum_sum_add(&result->sumX2, &tmp_var); -#endif + result->sumX2 = int128_deserialize(&buf); pq_getmsgend(&buf); - free_var(&tmp_var); - PG_RETURN_POINTER(result); } @@ -5909,43 +5693,37 @@ numeric_poly_deserialize(PG_FUNCTION_ARGS) Datum int8_avg_accum(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* Create the state data on the first call */ if (state == NULL) - state = makePolyNumAggState(fcinfo, false); + state = makeInt128AggState(fcinfo, false); if (!PG_ARGISNULL(1)) - { -#ifdef HAVE_INT128 - do_int128_accum(state, (int128) PG_GETARG_INT64(1)); -#else - do_numeric_accum(state, int64_to_numeric(PG_GETARG_INT64(1))); -#endif - } + do_int128_accum(state, PG_GETARG_INT64(1)); PG_RETURN_POINTER(state); } /* - * Combine function for PolyNumAggState for aggregates which don't require + * Combine function for Int128AggState for aggregates which don't require * sumX2 */ Datum int8_avg_combine(PG_FUNCTION_ARGS) { - PolyNumAggState *state1; - PolyNumAggState *state2; + Int128AggState *state1; + Int128AggState *state2; MemoryContext agg_context; MemoryContext old_context; if (!AggCheckCallContext(fcinfo, &agg_context)) elog(ERROR, "aggregate function called in non-aggregate context"); - state1 = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); - state2 = PG_ARGISNULL(1) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(1); + state1 = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); + state2 = PG_ARGISNULL(1) ? NULL : (Int128AggState *) PG_GETARG_POINTER(1); if (state2 == NULL) PG_RETURN_POINTER(state1); @@ -5955,14 +5733,10 @@ int8_avg_combine(PG_FUNCTION_ARGS) { old_context = MemoryContextSwitchTo(agg_context); - state1 = makePolyNumAggState(fcinfo, false); + state1 = makeInt128AggState(fcinfo, false); state1->N = state2->N; - -#ifdef HAVE_INT128 state1->sumX = state2->sumX; -#else - accum_sum_copy(&state1->sumX, &state2->sumX); -#endif + MemoryContextSwitchTo(old_context); PG_RETURN_POINTER(state1); @@ -5971,52 +5745,28 @@ int8_avg_combine(PG_FUNCTION_ARGS) if (state2->N > 0) { state1->N += state2->N; - -#ifdef HAVE_INT128 - state1->sumX += state2->sumX; -#else - /* The rest of this needs to work in the aggregate context */ - old_context = MemoryContextSwitchTo(agg_context); - - /* Accumulate sums */ - accum_sum_combine(&state1->sumX, &state2->sumX); - - MemoryContextSwitchTo(old_context); -#endif - + int128_add_int128(&state1->sumX, state2->sumX); } PG_RETURN_POINTER(state1); } /* * int8_avg_serialize - * Serialize PolyNumAggState into bytea using the standard - * recv-function infrastructure. + * Serialize Int128AggState into bytea for aggregate functions which + * don't require sumX2. */ Datum int8_avg_serialize(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; StringInfoData buf; bytea *result; - NumericVar tmp_var; /* Ensure we disallow calling when not in aggregate context */ if (!AggCheckCallContext(fcinfo, NULL)) elog(ERROR, "aggregate function called in non-aggregate context"); - state = (PolyNumAggState *) PG_GETARG_POINTER(0); - - /* - * If the platform supports int128 then sumX will be a 128 integer type. - * Here we'll convert that into a numeric type so that the combine state - * is in the same format for both int128 enabled machines and machines - * which don't support that type. The logic here is that one day we might - * like to send these over to another server for further processing and we - * want a standard format to work with. - */ - - init_var(&tmp_var); + state = (Int128AggState *) PG_GETARG_POINTER(0); pq_begintypsend(&buf); @@ -6024,39 +5774,30 @@ int8_avg_serialize(PG_FUNCTION_ARGS) pq_sendint64(&buf, state->N); /* sumX */ -#ifdef HAVE_INT128 - int128_to_numericvar(state->sumX, &tmp_var); -#else - accum_sum_final(&state->sumX, &tmp_var); -#endif - numericvar_serialize(&buf, &tmp_var); + int128_serialize(&buf, state->sumX); result = pq_endtypsend(&buf); - free_var(&tmp_var); - PG_RETURN_BYTEA_P(result); } /* * int8_avg_deserialize - * Deserialize bytea back into PolyNumAggState. + * Deserialize Int128AggState from bytea for aggregate functions which + * don't require sumX2. */ Datum int8_avg_deserialize(PG_FUNCTION_ARGS) { bytea *sstate; - PolyNumAggState *result; + Int128AggState *result; StringInfoData buf; - NumericVar tmp_var; if (!AggCheckCallContext(fcinfo, NULL)) elog(ERROR, "aggregate function called in non-aggregate context"); sstate = PG_GETARG_BYTEA_PP(0); - init_var(&tmp_var); - /* * Initialize a StringInfo so that we can "receive" it using the standard * recv-function infrastructure. @@ -6064,23 +5805,16 @@ int8_avg_deserialize(PG_FUNCTION_ARGS) initReadOnlyStringInfo(&buf, VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate)); - result = makePolyNumAggStateCurrentContext(false); + result = makeInt128AggStateCurrentContext(false); /* N */ result->N = pq_getmsgint64(&buf); /* sumX */ - numericvar_deserialize(&buf, &tmp_var); -#ifdef HAVE_INT128 - numericvar_to_int128(&tmp_var, &result->sumX); -#else - accum_sum_add(&result->sumX, &tmp_var); -#endif + result->sumX = int128_deserialize(&buf); pq_getmsgend(&buf); - free_var(&tmp_var); - PG_RETURN_POINTER(result); } @@ -6091,24 +5825,16 @@ int8_avg_deserialize(PG_FUNCTION_ARGS) Datum int2_accum_inv(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* Should not get here with no state */ if (state == NULL) elog(ERROR, "int2_accum_inv called with NULL state"); if (!PG_ARGISNULL(1)) - { -#ifdef HAVE_INT128 - do_int128_discard(state, (int128) PG_GETARG_INT16(1)); -#else - /* Should never fail, all inputs have dscale 0 */ - if (!do_numeric_discard(state, int64_to_numeric(PG_GETARG_INT16(1)))) - elog(ERROR, "do_numeric_discard failed unexpectedly"); -#endif - } + do_int128_discard(state, PG_GETARG_INT16(1)); PG_RETURN_POINTER(state); } @@ -6116,24 +5842,16 @@ int2_accum_inv(PG_FUNCTION_ARGS) Datum int4_accum_inv(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* Should not get here with no state */ if (state == NULL) elog(ERROR, "int4_accum_inv called with NULL state"); if (!PG_ARGISNULL(1)) - { -#ifdef HAVE_INT128 - do_int128_discard(state, (int128) PG_GETARG_INT32(1)); -#else - /* Should never fail, all inputs have dscale 0 */ - if (!do_numeric_discard(state, int64_to_numeric(PG_GETARG_INT32(1)))) - elog(ERROR, "do_numeric_discard failed unexpectedly"); -#endif - } + do_int128_discard(state, PG_GETARG_INT32(1)); PG_RETURN_POINTER(state); } @@ -6162,24 +5880,16 @@ int8_accum_inv(PG_FUNCTION_ARGS) Datum int8_avg_accum_inv(PG_FUNCTION_ARGS) { - PolyNumAggState *state; + Int128AggState *state; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* Should not get here with no state */ if (state == NULL) elog(ERROR, "int8_avg_accum_inv called with NULL state"); if (!PG_ARGISNULL(1)) - { -#ifdef HAVE_INT128 - do_int128_discard(state, (int128) PG_GETARG_INT64(1)); -#else - /* Should never fail, all inputs have dscale 0 */ - if (!do_numeric_discard(state, int64_to_numeric(PG_GETARG_INT64(1)))) - elog(ERROR, "do_numeric_discard failed unexpectedly"); -#endif - } + do_int128_discard(state, PG_GETARG_INT64(1)); PG_RETURN_POINTER(state); } @@ -6187,12 +5897,11 @@ int8_avg_accum_inv(PG_FUNCTION_ARGS) Datum numeric_poly_sum(PG_FUNCTION_ARGS) { -#ifdef HAVE_INT128 - PolyNumAggState *state; + Int128AggState *state; Numeric res; NumericVar result; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* If there were no non-null inputs, return NULL */ if (state == NULL || state->N == 0) @@ -6207,21 +5916,17 @@ numeric_poly_sum(PG_FUNCTION_ARGS) free_var(&result); PG_RETURN_NUMERIC(res); -#else - return numeric_sum(fcinfo); -#endif } Datum numeric_poly_avg(PG_FUNCTION_ARGS) { -#ifdef HAVE_INT128 - PolyNumAggState *state; + Int128AggState *state; NumericVar result; Datum countd, sumd; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); /* If there were no non-null inputs, return NULL */ if (state == NULL || state->N == 0) @@ -6237,9 +5942,6 @@ numeric_poly_avg(PG_FUNCTION_ARGS) free_var(&result); PG_RETURN_DATUM(DirectFunctionCall2(numeric_div, sumd, countd)); -#else - return numeric_avg(fcinfo); -#endif } Datum @@ -6472,7 +6174,6 @@ numeric_stddev_pop(PG_FUNCTION_ARGS) PG_RETURN_NUMERIC(res); } -#ifdef HAVE_INT128 static Numeric numeric_poly_stddev_internal(Int128AggState *state, bool variance, bool sample, @@ -6516,17 +6217,15 @@ numeric_poly_stddev_internal(Int128AggState *state, return res; } -#endif Datum numeric_poly_var_samp(PG_FUNCTION_ARGS) { -#ifdef HAVE_INT128 - PolyNumAggState *state; + Int128AggState *state; Numeric res; bool is_null; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); res = numeric_poly_stddev_internal(state, true, true, &is_null); @@ -6534,20 +6233,16 @@ numeric_poly_var_samp(PG_FUNCTION_ARGS) PG_RETURN_NULL(); else PG_RETURN_NUMERIC(res); -#else - return numeric_var_samp(fcinfo); -#endif } Datum numeric_poly_stddev_samp(PG_FUNCTION_ARGS) { -#ifdef HAVE_INT128 - PolyNumAggState *state; + Int128AggState *state; Numeric res; bool is_null; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); res = numeric_poly_stddev_internal(state, false, true, &is_null); @@ -6555,20 +6250,16 @@ numeric_poly_stddev_samp(PG_FUNCTION_ARGS) PG_RETURN_NULL(); else PG_RETURN_NUMERIC(res); -#else - return numeric_stddev_samp(fcinfo); -#endif } Datum numeric_poly_var_pop(PG_FUNCTION_ARGS) { -#ifdef HAVE_INT128 - PolyNumAggState *state; + Int128AggState *state; Numeric res; bool is_null; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); res = numeric_poly_stddev_internal(state, true, false, &is_null); @@ -6576,20 +6267,16 @@ numeric_poly_var_pop(PG_FUNCTION_ARGS) PG_RETURN_NULL(); else PG_RETURN_NUMERIC(res); -#else - return numeric_var_pop(fcinfo); -#endif } Datum numeric_poly_stddev_pop(PG_FUNCTION_ARGS) { -#ifdef HAVE_INT128 - PolyNumAggState *state; + Int128AggState *state; Numeric res; bool is_null; - state = PG_ARGISNULL(0) ? NULL : (PolyNumAggState *) PG_GETARG_POINTER(0); + state = PG_ARGISNULL(0) ? NULL : (Int128AggState *) PG_GETARG_POINTER(0); res = numeric_poly_stddev_internal(state, false, false, &is_null); @@ -6597,9 +6284,6 @@ numeric_poly_stddev_pop(PG_FUNCTION_ARGS) PG_RETURN_NULL(); else PG_RETURN_NUMERIC(res); -#else - return numeric_stddev_pop(fcinfo); -#endif } /* @@ -6625,6 +6309,7 @@ numeric_poly_stddev_pop(PG_FUNCTION_ARGS) Datum int2_sum(PG_FUNCTION_ARGS) { + int64 oldsum; int64 newval; if (PG_ARGISNULL(0)) @@ -6637,43 +6322,22 @@ int2_sum(PG_FUNCTION_ARGS) PG_RETURN_INT64(newval); } - /* - * If we're invoked as an aggregate, we can cheat and modify our first - * parameter in-place to avoid palloc overhead. If not, we need to return - * the new value of the transition variable. (If int8 is pass-by-value, - * then of course this is useless as well as incorrect, so just ifdef it - * out.) - */ -#ifndef USE_FLOAT8_BYVAL /* controls int8 too */ - if (AggCheckCallContext(fcinfo, NULL)) - { - int64 *oldsum = (int64 *) PG_GETARG_POINTER(0); - - /* Leave the running sum unchanged in the new input is null */ - if (!PG_ARGISNULL(1)) - *oldsum = *oldsum + (int64) PG_GETARG_INT16(1); - - PG_RETURN_POINTER(oldsum); - } - else -#endif - { - int64 oldsum = PG_GETARG_INT64(0); + oldsum = PG_GETARG_INT64(0); - /* Leave sum unchanged if new input is null. */ - if (PG_ARGISNULL(1)) - PG_RETURN_INT64(oldsum); + /* Leave sum unchanged if new input is null. */ + if (PG_ARGISNULL(1)) + PG_RETURN_INT64(oldsum); - /* OK to do the addition. */ - newval = oldsum + (int64) PG_GETARG_INT16(1); + /* OK to do the addition. */ + newval = oldsum + (int64) PG_GETARG_INT16(1); - PG_RETURN_INT64(newval); - } + PG_RETURN_INT64(newval); } Datum int4_sum(PG_FUNCTION_ARGS) { + int64 oldsum; int64 newval; if (PG_ARGISNULL(0)) @@ -6686,38 +6350,16 @@ int4_sum(PG_FUNCTION_ARGS) PG_RETURN_INT64(newval); } - /* - * If we're invoked as an aggregate, we can cheat and modify our first - * parameter in-place to avoid palloc overhead. If not, we need to return - * the new value of the transition variable. (If int8 is pass-by-value, - * then of course this is useless as well as incorrect, so just ifdef it - * out.) - */ -#ifndef USE_FLOAT8_BYVAL /* controls int8 too */ - if (AggCheckCallContext(fcinfo, NULL)) - { - int64 *oldsum = (int64 *) PG_GETARG_POINTER(0); - - /* Leave the running sum unchanged in the new input is null */ - if (!PG_ARGISNULL(1)) - *oldsum = *oldsum + (int64) PG_GETARG_INT32(1); - - PG_RETURN_POINTER(oldsum); - } - else -#endif - { - int64 oldsum = PG_GETARG_INT64(0); + oldsum = PG_GETARG_INT64(0); - /* Leave sum unchanged if new input is null. */ - if (PG_ARGISNULL(1)) - PG_RETURN_INT64(oldsum); + /* Leave sum unchanged if new input is null. */ + if (PG_ARGISNULL(1)) + PG_RETURN_INT64(oldsum); - /* OK to do the addition. */ - newval = oldsum + (int64) PG_GETARG_INT32(1); + /* OK to do the addition. */ + newval = oldsum + (int64) PG_GETARG_INT32(1); - PG_RETURN_INT64(newval); - } + PG_RETURN_INT64(newval); } /* @@ -7888,16 +7530,13 @@ duplicate_numeric(Numeric num) } /* - * make_result_opt_error() - + * make_result_safe() - * * Create the packed db numeric format in palloc()'d memory from * a variable. This will handle NaN and Infinity cases. - * - * If "have_error" isn't NULL, on overflow *have_error is set to true and - * NULL is returned. This is helpful when caller needs to handle errors. */ static Numeric -make_result_opt_error(const NumericVar *var, bool *have_error) +make_result_safe(const NumericVar *var, Node *escontext) { Numeric result; NumericDigit *digits = var->digits; @@ -7906,9 +7545,6 @@ make_result_opt_error(const NumericVar *var, bool *have_error) int n; Size len; - if (have_error) - *have_error = false; - if ((sign & NUMERIC_SIGN_MASK) == NUMERIC_SPECIAL) { /* @@ -7981,19 +7617,9 @@ make_result_opt_error(const NumericVar *var, bool *have_error) /* Check for overflow of int16 fields */ if (NUMERIC_WEIGHT(result) != weight || NUMERIC_DSCALE(result) != var->dscale) - { - if (have_error) - { - *have_error = true; - return NULL; - } - else - { - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("value overflows numeric format"))); - } - } + ereturn(escontext, NULL, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("value overflows numeric format"))); dump_numeric("make_result()", result); return result; @@ -8003,12 +7629,12 @@ make_result_opt_error(const NumericVar *var, bool *have_error) /* * make_result() - * - * An interface to make_result_opt_error() without "have_error" argument. + * An interface to make_result_safe() without "escontext" argument. */ static Numeric make_result(const NumericVar *var) { - return make_result_opt_error(var, NULL); + return make_result_safe(var, NULL); } @@ -8332,105 +7958,23 @@ numericvar_to_uint64(const NumericVar *var, uint64 *result) return true; } -#ifdef HAVE_INT128 -/* - * Convert numeric to int128, rounding if needed. - * - * If overflow, return false (no error is raised). Return true if okay. - */ -static bool -numericvar_to_int128(const NumericVar *var, int128 *result) -{ - NumericDigit *digits; - int ndigits; - int weight; - int i; - int128 val, - oldval; - bool neg; - NumericVar rounded; - - /* Round to nearest integer */ - init_var(&rounded); - set_var_from_var(var, &rounded); - round_var(&rounded, 0); - - /* Check for zero input */ - strip_var(&rounded); - ndigits = rounded.ndigits; - if (ndigits == 0) - { - *result = 0; - free_var(&rounded); - return true; - } - - /* - * For input like 10000000000, we must treat stripped digits as real. So - * the loop assumes there are weight+1 digits before the decimal point. - */ - weight = rounded.weight; - Assert(weight >= 0 && ndigits <= weight + 1); - - /* Construct the result */ - digits = rounded.digits; - neg = (rounded.sign == NUMERIC_NEG); - val = digits[0]; - for (i = 1; i <= weight; i++) - { - oldval = val; - val *= NBASE; - if (i < ndigits) - val += digits[i]; - - /* - * The overflow check is a bit tricky because we want to accept - * INT128_MIN, which will overflow the positive accumulator. We can - * detect this case easily though because INT128_MIN is the only - * nonzero value for which -val == val (on a two's complement machine, - * anyway). - */ - if ((val / NBASE) != oldval) /* possible overflow? */ - { - if (!neg || (-val) != val || val == 0 || oldval < 0) - { - free_var(&rounded); - return false; - } - } - } - - free_var(&rounded); - - *result = neg ? -val : val; - return true; -} - /* * Convert 128 bit integer to numeric. */ static void -int128_to_numericvar(int128 val, NumericVar *var) +int128_to_numericvar(INT128 val, NumericVar *var) { - uint128 uval, - newuval; + int sign; NumericDigit *ptr; int ndigits; + int32 dig; /* int128 can require at most 39 decimal digits; add one for safety */ alloc_var(var, 40 / DEC_DIGITS); - if (val < 0) - { - var->sign = NUMERIC_NEG; - uval = -val; - } - else - { - var->sign = NUMERIC_POS; - uval = val; - } + sign = int128_sign(val); + var->sign = sign < 0 ? NUMERIC_NEG : NUMERIC_POS; var->dscale = 0; - if (val == 0) + if (sign == 0) { var->ndigits = 0; var->weight = 0; @@ -8442,15 +7986,13 @@ int128_to_numericvar(int128 val, NumericVar *var) { ptr--; ndigits++; - newuval = uval / NBASE; - *ptr = uval - newuval * NBASE; - uval = newuval; - } while (uval); + int128_div_mod_int32(&val, NBASE, &dig); + *ptr = (NumericDigit) abs(dig); + } while (!int128_is_zero(val)); var->digits = ptr; var->ndigits = ndigits; var->weight = ndigits - 1; } -#endif /* * Convert a NumericVar to float8; if out of range, return +/- HUGE_VAL @@ -9306,22 +8848,22 @@ mul_var_short(const NumericVar *var1, const NumericVar *var2, term = PRODSUM5(var1digits, 0, var2digits, 4) + carry; res_digits[5] = (NumericDigit) (term % NBASE); carry = term / NBASE; - /* FALLTHROUGH */ + pg_fallthrough; case 5: term = PRODSUM4(var1digits, 0, var2digits, 3) + carry; res_digits[4] = (NumericDigit) (term % NBASE); carry = term / NBASE; - /* FALLTHROUGH */ + pg_fallthrough; case 4: term = PRODSUM3(var1digits, 0, var2digits, 2) + carry; res_digits[3] = (NumericDigit) (term % NBASE); carry = term / NBASE; - /* FALLTHROUGH */ + pg_fallthrough; case 3: term = PRODSUM2(var1digits, 0, var2digits, 1) + carry; res_digits[2] = (NumericDigit) (term % NBASE); carry = term / NBASE; - /* FALLTHROUGH */ + pg_fallthrough; case 2: term = PRODSUM1(var1digits, 0, var2digits, 0) + carry; res_digits[1] = (NumericDigit) (term % NBASE); diff --git a/src/backend/utils/adt/numutils.c b/src/backend/utils/adt/numutils.c index 3bf30774a0c94..47c2e21e6b337 100644 --- a/src/backend/utils/adt/numutils.c +++ b/src/backend/utils/adt/numutils.c @@ -3,7 +3,7 @@ * numutils.c * utility functions for I/O of built-in numeric types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,7 +14,6 @@ */ #include "postgres.h" -#include #include #include @@ -113,7 +112,7 @@ static const int8 hexlookup[128] = { * pg_strtoint16() will throw ereport() upon bad input format or overflow; * while pg_strtoint16_safe() instead returns such complaints in *escontext, * if it's an ErrorSaveContext. -* + * * NB: Accumulate input as an unsigned number, to deal with two's complement * representation of the most negative number, which can't be represented as a * signed positive number. diff --git a/src/backend/utils/adt/oid.c b/src/backend/utils/adt/oid.c index 4e880e7623273..a3419728971dc 100644 --- a/src/backend/utils/adt/oid.c +++ b/src/backend/utils/adt/oid.c @@ -3,7 +3,7 @@ * oid.c * Functions for the built-in type Oid ... also oidvector. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -107,6 +107,30 @@ buildoidvector(const Oid *oids, int n) return result; } +/* + * validate that an array object meets the restrictions of oidvector + * + * We need this because there are pathways by which a general oid[] array can + * be cast to oidvector, allowing the type's restrictions to be violated. + * All code that receives an oidvector as a SQL parameter should check this. + */ +void +check_valid_oidvector(const oidvector *oidArray) +{ + /* + * We insist on ndim == 1 and dataoffset == 0 (that is, no nulls) because + * otherwise the array's layout will not be what calling code expects. We + * needn't be picky about the index lower bound though. Checking elemtype + * is just paranoia. + */ + if (oidArray->ndim != 1 || + oidArray->dataoffset != 0 || + oidArray->elemtype != OIDOID) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("array is not a valid oidvector"))); +} + /* * oidvectorin - converts "num num ..." to internal form */ @@ -159,10 +183,14 @@ oidvectorout(PG_FUNCTION_ARGS) { oidvector *oidArray = (oidvector *) PG_GETARG_POINTER(0); int num, - nnums = oidArray->dim1; + nnums; char *rp; char *result; + /* validate input before fetching dim1 */ + check_valid_oidvector(oidArray); + nnums = oidArray->dim1; + /* assumes sign, 10 digits, ' ' */ rp = result = (char *) palloc(nnums * 12 + 1); for (num = 0; num < nnums; num++) @@ -225,6 +253,7 @@ oidvectorrecv(PG_FUNCTION_ARGS) Datum oidvectorsend(PG_FUNCTION_ARGS) { + /* We don't do check_valid_oidvector, since array_send won't care */ return array_send(fcinfo); } diff --git a/src/backend/utils/adt/oid8.c b/src/backend/utils/adt/oid8.c new file mode 100644 index 0000000000000..cfebcb13428ed --- /dev/null +++ b/src/backend/utils/adt/oid8.c @@ -0,0 +1,168 @@ +/*------------------------------------------------------------------------- + * + * oid8.c + * Functions for the built-in type Oid8 + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/utils/adt/oid8.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_type.h" +#include "libpq/pqformat.h" +#include "utils/builtins.h" + +#define MAXOID8LEN 20 + +/***************************************************************************** + * USER I/O ROUTINES * + *****************************************************************************/ + +Datum +oid8in(PG_FUNCTION_ARGS) +{ + char *s = PG_GETARG_CSTRING(0); + Oid8 result; + + result = uint64in_subr(s, NULL, "oid8", fcinfo->context); + PG_RETURN_OID8(result); +} + +Datum +oid8out(PG_FUNCTION_ARGS) +{ + Oid8 val = PG_GETARG_OID8(0); + char buf[MAXOID8LEN + 1]; + char *result; + int len; + + len = pg_ulltoa_n(val, buf) + 1; + buf[len - 1] = '\0'; + + /* + * Since the length is already known, we do a manual palloc() and memcpy() + * to avoid the strlen() call that would otherwise be done in pstrdup(). + */ + result = palloc(len); + memcpy(result, buf, len); + PG_RETURN_CSTRING(result); +} + +/* + * oid8recv - converts external binary format to oid8 + */ +Datum +oid8recv(PG_FUNCTION_ARGS) +{ + StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); + + PG_RETURN_OID8(pq_getmsgint64(buf)); +} + +/* + * oid8send - converts oid8 to binary format + */ +Datum +oid8send(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + StringInfoData buf; + + pq_begintypsend(&buf); + pq_sendint64(&buf, arg1); + PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); +} + +/***************************************************************************** + * PUBLIC ROUTINES * + *****************************************************************************/ + +Datum +oid8eq(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_BOOL(arg1 == arg2); +} + +Datum +oid8ne(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_BOOL(arg1 != arg2); +} + +Datum +oid8lt(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_BOOL(arg1 < arg2); +} + +Datum +oid8le(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_BOOL(arg1 <= arg2); +} + +Datum +oid8ge(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_BOOL(arg1 >= arg2); +} + +Datum +oid8gt(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_BOOL(arg1 > arg2); +} + +Datum +hashoid8(PG_FUNCTION_ARGS) +{ + return hashint8(fcinfo); +} + +Datum +hashoid8extended(PG_FUNCTION_ARGS) +{ + return hashint8extended(fcinfo); +} + +Datum +oid8larger(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_OID8((arg1 > arg2) ? arg1 : arg2); +} + +Datum +oid8smaller(PG_FUNCTION_ARGS) +{ + Oid8 arg1 = PG_GETARG_OID8(0); + Oid8 arg2 = PG_GETARG_OID8(1); + + PG_RETURN_OID8((arg1 < arg2) ? arg1 : arg2); +} diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index a24a2d208fba7..5b0d098bd07d5 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -2,7 +2,7 @@ * oracle_compat.c * Oracle compatible functions. * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * Author: Edmund Mergl * Multibyte enhancement: Tatsuo Ishii @@ -169,8 +169,8 @@ lpad(PG_FUNCTION_ARGS) char *ptr1, *ptr2, *ptr2start, - *ptr2end, *ptr_ret; + const char *ptr2end; int m, s1len, s2len; @@ -215,7 +215,7 @@ lpad(PG_FUNCTION_ARGS) while (m--) { - int mlen = pg_mblen(ptr2); + int mlen = pg_mblen_range(ptr2, ptr2end); memcpy(ptr_ret, ptr2, mlen); ptr_ret += mlen; @@ -228,7 +228,7 @@ lpad(PG_FUNCTION_ARGS) while (s1len--) { - int mlen = pg_mblen(ptr1); + int mlen = pg_mblen_unbounded(ptr1); memcpy(ptr_ret, ptr1, mlen); ptr_ret += mlen; @@ -267,8 +267,8 @@ rpad(PG_FUNCTION_ARGS) char *ptr1, *ptr2, *ptr2start, - *ptr2end, *ptr_ret; + const char *ptr2end; int m, s1len, s2len; @@ -308,11 +308,12 @@ rpad(PG_FUNCTION_ARGS) m = len - s1len; ptr1 = VARDATA_ANY(string1); + ptr_ret = VARDATA(ret); while (s1len--) { - int mlen = pg_mblen(ptr1); + int mlen = pg_mblen_unbounded(ptr1); memcpy(ptr_ret, ptr1, mlen); ptr_ret += mlen; @@ -324,7 +325,7 @@ rpad(PG_FUNCTION_ARGS) while (m--) { - int mlen = pg_mblen(ptr2); + int mlen = pg_mblen_range(ptr2, ptr2end); memcpy(ptr_ret, ptr2, mlen); ptr_ret += mlen; @@ -409,6 +410,7 @@ dotrim(const char *string, int stringlen, */ const char **stringchars; const char **setchars; + const char *setend; int *stringmblen; int *setmblen; int stringnchars; @@ -416,6 +418,7 @@ dotrim(const char *string, int stringlen, int resultndx; int resultnchars; const char *p; + const char *pend; int len; int mblen; const char *str_pos; @@ -426,10 +429,11 @@ dotrim(const char *string, int stringlen, stringnchars = 0; p = string; len = stringlen; + pend = p + len; while (len > 0) { stringchars[stringnchars] = p; - stringmblen[stringnchars] = mblen = pg_mblen(p); + stringmblen[stringnchars] = mblen = pg_mblen_range(p, pend); stringnchars++; p += mblen; len -= mblen; @@ -440,10 +444,11 @@ dotrim(const char *string, int stringlen, setnchars = 0; p = set; len = setlen; + setend = set + setlen; while (len > 0) { setchars[setnchars] = p; - setmblen[setnchars] = mblen = pg_mblen(p); + setmblen[setnchars] = mblen = pg_mblen_range(p, setend); setnchars++; p += mblen; len -= mblen; @@ -821,6 +826,8 @@ translate(PG_FUNCTION_ARGS) *to_end; char *source, *target; + const char *source_end; + const char *from_end; int m, fromlen, tolen, @@ -835,9 +842,11 @@ translate(PG_FUNCTION_ARGS) if (m <= 0) PG_RETURN_TEXT_P(string); source = VARDATA_ANY(string); + source_end = source + m; fromlen = VARSIZE_ANY_EXHDR(from); from_ptr = VARDATA_ANY(from); + from_end = from_ptr + fromlen; tolen = VARSIZE_ANY_EXHDR(to); to_ptr = VARDATA_ANY(to); to_end = to_ptr + tolen; @@ -861,12 +870,12 @@ translate(PG_FUNCTION_ARGS) while (m > 0) { - source_len = pg_mblen(source); + source_len = pg_mblen_range(source, source_end); from_index = 0; for (i = 0; i < fromlen; i += len) { - len = pg_mblen(&from_ptr[i]); + len = pg_mblen_range(&from_ptr[i], from_end); if (len == source_len && memcmp(source, &from_ptr[i], len) == 0) break; @@ -882,11 +891,11 @@ translate(PG_FUNCTION_ARGS) { if (p >= to_end) break; - p += pg_mblen(p); + p += pg_mblen_range(p, to_end); } if (p < to_end) { - len = pg_mblen(p); + len = pg_mblen_range(p, to_end); memcpy(target, p, len); target += len; retlen += len; diff --git a/src/backend/utils/adt/orderedsetaggs.c b/src/backend/utils/adt/orderedsetaggs.c index 9457d23971581..fd8b8676470dd 100644 --- a/src/backend/utils/adt/orderedsetaggs.c +++ b/src/backend/utils/adt/orderedsetaggs.c @@ -3,7 +3,7 @@ * orderedsetaggs.c * Ordered-set aggregate functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -153,7 +153,7 @@ ordered_set_startup(FunctionCallInfo fcinfo, bool use_tuples) qcontext = fcinfo->flinfo->fn_mcxt; oldcontext = MemoryContextSwitchTo(qcontext); - qstate = (OSAPerQueryState *) palloc0(sizeof(OSAPerQueryState)); + qstate = palloc0_object(OSAPerQueryState); qstate->aggref = aggref; qstate->qcontext = qcontext; @@ -233,6 +233,7 @@ ordered_set_startup(FunctionCallInfo fcinfo, bool use_tuples) -1, 0); + TupleDescFinalize(newdesc); FreeTupleDesc(qstate->tupdesc); qstate->tupdesc = newdesc; } @@ -278,7 +279,7 @@ ordered_set_startup(FunctionCallInfo fcinfo, bool use_tuples) /* Now build the stuff we need in group-lifespan context */ oldcontext = MemoryContextSwitchTo(gcontext); - osastate = (OSAPerGroupState *) palloc(sizeof(OSAPerGroupState)); + osastate = palloc_object(OSAPerGroupState); osastate->qstate = qstate; osastate->gcontext = gcontext; @@ -660,8 +661,8 @@ pct_info_cmp(const void *pa, const void *pb) */ static struct pct_info * setup_pct_info(int num_percentiles, - Datum *percentiles_datum, - bool *percentiles_null, + const Datum *percentiles_datum, + const bool *percentiles_null, int64 rowcount, bool continuous) { @@ -1007,7 +1008,7 @@ percentile_cont_float8_multi_final(PG_FUNCTION_ARGS) FLOAT8OID, /* hard-wired info on type float8 */ sizeof(float8), - FLOAT8PASSBYVAL, + true, TYPALIGN_DOUBLE, float8_lerp); } diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c index 61f07ac08b94d..e9db027aa2eac 100644 --- a/src/backend/utils/adt/partitionfuncs.c +++ b/src/backend/utils/adt/partitionfuncs.c @@ -3,7 +3,7 @@ * partitionfuncs.c * Functions for accessing partition-related metadata * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/pg_dependencies.c b/src/backend/utils/adt/pg_dependencies.c new file mode 100644 index 0000000000000..d8bd1c4a168cf --- /dev/null +++ b/src/backend/utils/adt/pg_dependencies.c @@ -0,0 +1,873 @@ +/*------------------------------------------------------------------------- + * + * pg_dependencies.c + * pg_dependencies data type support. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/utils/adt/pg_dependencies.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "common/int.h" +#include "common/jsonapi.h" +#include "lib/stringinfo.h" +#include "mb/pg_wchar.h" +#include "nodes/miscnodes.h" +#include "statistics/extended_stats_internal.h" +#include "statistics/statistics_format.h" +#include "utils/builtins.h" +#include "utils/float.h" +#include "utils/fmgrprotos.h" + +typedef enum +{ + DEPS_EXPECT_START = 0, + DEPS_EXPECT_ITEM, + DEPS_EXPECT_KEY, + DEPS_EXPECT_ATTNUM_LIST, + DEPS_EXPECT_ATTNUM, + DEPS_EXPECT_DEPENDENCY, + DEPS_EXPECT_DEGREE, + DEPS_PARSE_COMPLETE, +} DependenciesSemanticState; + +typedef struct +{ + const char *str; + DependenciesSemanticState state; + + List *dependency_list; + Node *escontext; + + bool found_attributes; /* Item has an attributes key */ + bool found_dependency; /* Item has an dependency key */ + bool found_degree; /* Item has degree key */ + List *attnum_list; /* Accumulated attribute numbers */ + AttrNumber dependency; + double degree; +} DependenciesParseState; + +/* + * Invoked at the start of each MVDependency object. + * + * The entire JSON document should be one array of MVDependency objects. + * + * If we are anywhere else in the document, it's an error. + */ +static JsonParseErrorType +dependencies_object_start(void *state) +{ + DependenciesParseState *parse = state; + + switch (parse->state) + { + case DEPS_EXPECT_ITEM: + /* Now we expect to see attributes/dependency/degree keys */ + parse->state = DEPS_EXPECT_KEY; + return JSON_SUCCESS; + + case DEPS_EXPECT_START: + /* pg_dependencies must begin with a '[' */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Initial element must be an array.")); + break; + + case DEPS_EXPECT_KEY: + /* In an object, expecting key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("A key was expected.")); + break; + + case DEPS_EXPECT_ATTNUM_LIST: + /* Just followed an "attributes": key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Value of \"%s\" must be an array of attribute numbers.", + PG_DEPENDENCIES_KEY_ATTRIBUTES)); + break; + + case DEPS_EXPECT_ATTNUM: + /* In an attribute number list, expect only scalar integers */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Attribute lists can only contain attribute numbers.")); + break; + + case DEPS_EXPECT_DEPENDENCY: + /* Just followed a "dependency" key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Value of \"%s\" must be an integer.", + PG_DEPENDENCIES_KEY_DEPENDENCY)); + break; + + case DEPS_EXPECT_DEGREE: + /* Just followed a "degree" key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Value of \"%s\" must be an integer.", + PG_DEPENDENCIES_KEY_DEGREE)); + break; + + default: + elog(ERROR, + "object start of \"%s\" found in unexpected parse state: %d.", + "pg_dependencies", (int) parse->state); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Invoked at the end of an object. + * + * Handle the end of an MVDependency object's JSON representation. + */ +static JsonParseErrorType +dependencies_object_end(void *state) +{ + DependenciesParseState *parse = state; + + MVDependency *dep; + + int natts = 0; + + if (parse->state != DEPS_EXPECT_KEY) + elog(ERROR, + "object end of \"%s\" found in unexpected parse state: %d.", + "pg_dependencies", (int) parse->state); + + if (!parse->found_attributes) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Item must contain \"%s\" key.", + PG_DEPENDENCIES_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + + if (!parse->found_dependency) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Item must contain \"%s\" key.", + PG_DEPENDENCIES_KEY_DEPENDENCY)); + return JSON_SEM_ACTION_FAILED; + } + + if (!parse->found_degree) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Item must contain \"%s\" key.", + PG_DEPENDENCIES_KEY_DEGREE)); + return JSON_SEM_ACTION_FAILED; + } + + /* + * We need at least one attribute number in a dependencies item, anything + * less is malformed. + */ + natts = list_length(parse->attnum_list); + if ((natts < 1) || (natts > (STATS_MAX_DIMENSIONS - 1))) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("The \"%s\" key must contain an array of at least %d and no more than %d elements.", + PG_DEPENDENCIES_KEY_ATTRIBUTES, 1, + STATS_MAX_DIMENSIONS - 1)); + return JSON_SEM_ACTION_FAILED; + } + + /* + * Allocate enough space for the dependency, the attribute numbers in the + * list and the final attribute number for the dependency. + */ + dep = palloc0(offsetof(MVDependency, attributes) + ((natts + 1) * sizeof(AttrNumber))); + dep->nattributes = natts + 1; + + dep->attributes[natts] = parse->dependency; + dep->degree = parse->degree; + + /* + * Assign attribute numbers to the attributes array, comparing each one + * against the dependency attribute to ensure that there there are no + * matches. + */ + for (int i = 0; i < natts; i++) + { + dep->attributes[i] = (AttrNumber) list_nth_int(parse->attnum_list, i); + if (dep->attributes[i] == parse->dependency) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Item \"%s\" with value %d has been found in the \"%s\" list.", + PG_DEPENDENCIES_KEY_DEPENDENCY, parse->dependency, + PG_DEPENDENCIES_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + } + + parse->dependency_list = lappend(parse->dependency_list, (void *) dep); + + /* + * Reset dependency item state variables to look for the next + * MVDependency. + */ + list_free(parse->attnum_list); + parse->attnum_list = NIL; + parse->dependency = 0; + parse->degree = 0.0; + parse->found_attributes = false; + parse->found_dependency = false; + parse->found_degree = false; + parse->state = DEPS_EXPECT_ITEM; + + return JSON_SUCCESS; +} + +/* + * Invoked at the start of an array. + * + * Dependency input format does not have arrays, so any array elements + * encountered are an error. + */ +static JsonParseErrorType +dependencies_array_start(void *state) +{ + DependenciesParseState *parse = state; + + switch (parse->state) + { + case DEPS_EXPECT_ATTNUM_LIST: + parse->state = DEPS_EXPECT_ATTNUM; + break; + case DEPS_EXPECT_START: + parse->state = DEPS_EXPECT_ITEM; + break; + default: + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Array has been found at an unexpected location.")); + return JSON_SEM_ACTION_FAILED; + } + + return JSON_SUCCESS; +} + +/* + * Invoked at the end of an array. + * + * Either the end of an attribute number list or the whole object. + */ +static JsonParseErrorType +dependencies_array_end(void *state) +{ + DependenciesParseState *parse = state; + + switch (parse->state) + { + case DEPS_EXPECT_ATTNUM: + if (list_length(parse->attnum_list) > 0) + { + parse->state = DEPS_EXPECT_KEY; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("The \"%s\" key must be a non-empty array.", + PG_DEPENDENCIES_KEY_ATTRIBUTES)); + break; + + case DEPS_EXPECT_ITEM: + if (list_length(parse->dependency_list) > 0) + { + parse->state = DEPS_PARSE_COMPLETE; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Item array cannot be empty.")); + break; + + default: + + /* + * This can only happen if a case was missed in + * dependencies_array_start(). + */ + elog(ERROR, + "array end of \"%s\" found in unexpected parse state: %d.", + "pg_dependencies", (int) parse->state); + break; + } + return JSON_SEM_ACTION_FAILED; +} + +/* + * Invoked at the start of a key/value field. + * + * The valid keys for the MVDependency object are: + * - attributes + * - dependency + * - degree + */ +static JsonParseErrorType +dependencies_object_field_start(void *state, char *fname, bool isnull) +{ + DependenciesParseState *parse = state; + + if (strcmp(fname, PG_DEPENDENCIES_KEY_ATTRIBUTES) == 0) + { + if (parse->found_attributes) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Multiple \"%s\" keys are not allowed.", + PG_DEPENDENCIES_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + + parse->found_attributes = true; + parse->state = DEPS_EXPECT_ATTNUM_LIST; + return JSON_SUCCESS; + } + + if (strcmp(fname, PG_DEPENDENCIES_KEY_DEPENDENCY) == 0) + { + if (parse->found_dependency) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Multiple \"%s\" keys are not allowed.", + PG_DEPENDENCIES_KEY_DEPENDENCY)); + return JSON_SEM_ACTION_FAILED; + } + + parse->found_dependency = true; + parse->state = DEPS_EXPECT_DEPENDENCY; + return JSON_SUCCESS; + } + + if (strcmp(fname, PG_DEPENDENCIES_KEY_DEGREE) == 0) + { + if (parse->found_degree) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Multiple \"%s\" keys are not allowed.", + PG_DEPENDENCIES_KEY_DEGREE)); + return JSON_SEM_ACTION_FAILED; + } + + parse->found_degree = true; + parse->state = DEPS_EXPECT_DEGREE; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Only allowed keys are \"%s\", \"%s\" and \"%s\".", + PG_DEPENDENCIES_KEY_ATTRIBUTES, + PG_DEPENDENCIES_KEY_DEPENDENCY, + PG_DEPENDENCIES_KEY_DEGREE)); + return JSON_SEM_ACTION_FAILED; +} + +/* + * Invoked at the start of an array element. + * + * pg_dependencies input format does not have arrays, so any array elements + * encountered are an error. + */ +static JsonParseErrorType +dependencies_array_element_start(void *state, bool isnull) +{ + DependenciesParseState *parse = state; + + switch (parse->state) + { + case DEPS_EXPECT_ATTNUM: + if (!isnull) + return JSON_SUCCESS; + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Attribute number array cannot be null.")); + break; + + case DEPS_EXPECT_ITEM: + if (!isnull) + return JSON_SUCCESS; + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Item list elements cannot be null.")); + break; + + default: + elog(ERROR, + "array element start of \"%s\" found in unexpected parse state: %d.", + "pg_dependencies", (int) parse->state); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Test for valid subsequent attribute number. + * + * If the previous value is positive, then current value must either be + * greater than the previous value, or negative. + * + * If the previous value is negative, then the value must be less than + * the previous value. + * + * Duplicate values are not allowed; that is already covered by the rules + * described above. + */ +static bool +valid_subsequent_attnum(const AttrNumber prev, const AttrNumber cur) +{ + Assert(prev != 0); + + if (prev > 0) + return ((cur > prev) || (cur < 0)); + + return (cur < prev); +} + +/* + * Handle scalar events from the dependencies input parser. + * + * There is only one case where we will encounter a scalar, and that is the + * dependency degree for the previous object key. + */ +static JsonParseErrorType +dependencies_scalar(void *state, char *token, JsonTokenType tokentype) +{ + DependenciesParseState *parse = state; + AttrNumber attnum; + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + switch (parse->state) + { + case DEPS_EXPECT_ATTNUM: + attnum = pg_strtoint16_safe(token, (Node *) &escontext); + + if (escontext.error_occurred) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Key \"%s\" has an incorrect value.", PG_DEPENDENCIES_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + + /* + * An attribute number cannot be zero or a negative number beyond + * the number of the possible expressions. + */ + if (attnum == 0 || attnum < (0 - STATS_MAX_DIMENSIONS)) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Invalid \"%s\" element has been found: %d.", + PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum)); + return JSON_SEM_ACTION_FAILED; + } + + if (parse->attnum_list != NIL) + { + const AttrNumber prev = llast_int(parse->attnum_list); + + if (!valid_subsequent_attnum(prev, attnum)) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Invalid \"%s\" element has been found: %d cannot follow %d.", + PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum, prev)); + return JSON_SEM_ACTION_FAILED; + } + } + + parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum); + return JSON_SUCCESS; + + case DEPS_EXPECT_DEPENDENCY: + parse->dependency = (AttrNumber) + pg_strtoint16_safe(token, (Node *) &escontext); + + if (escontext.error_occurred) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Key \"%s\" has an incorrect value.", PG_DEPENDENCIES_KEY_DEPENDENCY)); + return JSON_SEM_ACTION_FAILED; + } + + /* + * The dependency attribute number cannot be zero or a negative + * number beyond the number of the possible expressions. + */ + if (parse->dependency == 0 || parse->dependency < (0 - STATS_MAX_DIMENSIONS)) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Key \"%s\" has an incorrect value: %d.", + PG_DEPENDENCIES_KEY_DEPENDENCY, parse->dependency)); + return JSON_SEM_ACTION_FAILED; + } + + parse->state = DEPS_EXPECT_KEY; + return JSON_SUCCESS; + + case DEPS_EXPECT_DEGREE: + parse->degree = float8in_internal(token, NULL, "double", + token, (Node *) &escontext); + + if (escontext.error_occurred) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Key \"%s\" has an incorrect value.", PG_DEPENDENCIES_KEY_DEGREE)); + return JSON_SEM_ACTION_FAILED; + } + + parse->state = DEPS_EXPECT_KEY; + return JSON_SUCCESS; + + default: + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", parse->str), + errdetail("Unexpected scalar has been found.")); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Compare the attribute arrays of two MVDependency values, + * looking for duplicated sets. + */ +static bool +dep_attributes_eq(const MVDependency *a, const MVDependency *b) +{ + int i; + + if (a->nattributes != b->nattributes) + return false; + + for (i = 0; i < a->nattributes; i++) + { + if (a->attributes[i] != b->attributes[i]) + return false; + } + + return true; +} + +/* + * Generate a string representing an array of attribute numbers. + * Internally, the dependency attribute is the last element, so we + * leave that off. + * + * Freeing the allocated string is the responsibility of the caller. + */ +static char * +dep_attnum_list(const MVDependency *item) +{ + StringInfoData str; + + initStringInfo(&str); + + appendStringInfo(&str, "%d", item->attributes[0]); + + for (int i = 1; i < item->nattributes - 1; i++) + appendStringInfo(&str, ", %d", item->attributes[i]); + + return str.data; +} + +/* + * Return the dependency, which is the last attribute element. + */ +static AttrNumber +dep_attnum_dependency(const MVDependency *item) +{ + return item->attributes[item->nattributes - 1]; +} + +/* + * Attempt to build and serialize the MVDependencies object. + * + * This can only be executed after the completion of the JSON parsing. + * + * In the event of an error, set the error context and return NULL. + */ +static bytea * +build_mvdependencies(DependenciesParseState *parse, char *str) +{ + int ndeps = list_length(parse->dependency_list); + + MVDependencies *mvdeps; + bytea *bytes; + + switch (parse->state) + { + case DEPS_PARSE_COMPLETE: + + /* + * Parse ended in the expected place. We should have a list of + * items, but if we do not there is an issue with one of the + * earlier parse steps. + */ + if (ndeps == 0) + elog(ERROR, + "pg_dependencies parsing claims success with an empty item list."); + break; + + case DEPS_EXPECT_START: + /* blank */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", str), + errdetail("Value cannot be empty.")); + return NULL; + + default: + /* Unexpected end-state. */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", str), + errdetail("Unexpected end state has been found: %d.", parse->state)); + return NULL; + } + + mvdeps = palloc0(offsetof(MVDependencies, deps) + + (ndeps * sizeof(MVDependency *))); + mvdeps->magic = STATS_DEPS_MAGIC; + mvdeps->type = STATS_DEPS_TYPE_BASIC; + mvdeps->ndeps = ndeps; + + for (int i = 0; i < ndeps; i++) + { + /* + * Use the MVDependency objects in the dependency_list. + * + * Because we free the dependency_list after parsing is done, we + * cannot free it here. + */ + mvdeps->deps[i] = list_nth(parse->dependency_list, i); + + /* + * Ensure that this item does not duplicate the attributes of any + * pre-existing item. + */ + for (int j = 0; j < i; j++) + { + if (dep_attributes_eq(mvdeps->deps[i], mvdeps->deps[j])) + { + MVDependency *dep = mvdeps->deps[i]; + char *attnum_list = dep_attnum_list(dep); + AttrNumber attnum_dep = dep_attnum_dependency(dep); + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", str), + errdetail("Duplicated \"%s\" array has been found: [%s] for key \"%s\" and value %d.", + PG_DEPENDENCIES_KEY_ATTRIBUTES, attnum_list, + PG_DEPENDENCIES_KEY_DEPENDENCY, attnum_dep)); + pfree(mvdeps); + return NULL; + } + } + } + + bytes = statext_dependencies_serialize(mvdeps); + + /* + * No need to free the individual MVDependency objects, because they are + * still in the dependency_list, and will be freed with that. + */ + pfree(mvdeps); + + return bytes; +} + + +/* + * pg_dependencies_in - input routine for type pg_dependencies. + * + * This format is valid JSON, with the expected format: + * [{"attributes": [1,2], "dependency": -1, "degree": 1.0000}, + * {"attributes": [1,-1], "dependency": 2, "degree": 0.0000}, + * {"attributes": [2,-1], "dependency": 1, "degree": 1.0000}] + * + */ +Datum +pg_dependencies_in(PG_FUNCTION_ARGS) +{ + char *str = PG_GETARG_CSTRING(0); + bytea *bytes = NULL; + + DependenciesParseState parse_state; + JsonParseErrorType result; + JsonLexContext *lex; + JsonSemAction sem_action; + + /* initialize the semantic state */ + parse_state.str = str; + parse_state.state = DEPS_EXPECT_START; + parse_state.dependency_list = NIL; + parse_state.attnum_list = NIL; + parse_state.dependency = 0; + parse_state.degree = 0.0; + parse_state.found_attributes = false; + parse_state.found_dependency = false; + parse_state.found_degree = false; + parse_state.escontext = fcinfo->context; + + /* set callbacks */ + sem_action.semstate = (void *) &parse_state; + sem_action.object_start = dependencies_object_start; + sem_action.object_end = dependencies_object_end; + sem_action.array_start = dependencies_array_start; + sem_action.array_end = dependencies_array_end; + sem_action.array_element_start = dependencies_array_element_start; + sem_action.array_element_end = NULL; + sem_action.object_field_start = dependencies_object_field_start; + sem_action.object_field_end = NULL; + sem_action.scalar = dependencies_scalar; + + lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), PG_UTF8, true); + + result = pg_parse_json(lex, &sem_action); + freeJsonLexContext(lex); + + if (result == JSON_SUCCESS) + bytes = build_mvdependencies(&parse_state, str); + + list_free_deep(parse_state.dependency_list); + list_free(parse_state.attnum_list); + + if (bytes) + PG_RETURN_BYTEA_P(bytes); + + /* + * If escontext already set, just use that. Anything else is a generic + * JSON parse error. + */ + if (!SOFT_ERROR_OCCURRED(parse_state.escontext)) + errsave(parse_state.escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_dependencies: \"%s\"", str), + errdetail("Input data must be valid JSON.")); + + PG_RETURN_NULL(); +} + + +/* + * pg_dependencies_out - output routine for type pg_dependencies. + */ +Datum +pg_dependencies_out(PG_FUNCTION_ARGS) +{ + bytea *data = PG_GETARG_BYTEA_PP(0); + MVDependencies *dependencies = statext_dependencies_deserialize(data); + StringInfoData str; + + initStringInfo(&str); + appendStringInfoChar(&str, '['); + + for (int i = 0; i < dependencies->ndeps; i++) + { + MVDependency *dependency = dependencies->deps[i]; + + if (i > 0) + appendStringInfoString(&str, ", "); + + if (dependency->nattributes <= 1) + elog(ERROR, "invalid zero-length nattributes array in MVDependencies"); + + appendStringInfo(&str, "{\"" PG_DEPENDENCIES_KEY_ATTRIBUTES "\": [%d", + dependency->attributes[0]); + + for (int j = 1; j < dependency->nattributes - 1; j++) + appendStringInfo(&str, ", %d", dependency->attributes[j]); + + appendStringInfo(&str, "], \"" PG_DEPENDENCIES_KEY_DEPENDENCY "\": %d, " + "\"" PG_DEPENDENCIES_KEY_DEGREE "\": %f}", + dependency->attributes[dependency->nattributes - 1], + dependency->degree); + } + + appendStringInfoChar(&str, ']'); + + PG_RETURN_CSTRING(str.data); +} + +/* + * pg_dependencies_recv - binary input routine for type pg_dependencies. + */ +Datum +pg_dependencies_recv(PG_FUNCTION_ARGS) +{ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot accept a value of type %s", "pg_dependencies"))); + + PG_RETURN_VOID(); /* keep compiler quiet */ +} + +/* + * pg_dependencies_send - binary output routine for type pg_dependencies. + * + * Functional dependencies are serialized in a bytea value (although the type + * is named differently), so let's just send that. + */ +Datum +pg_dependencies_send(PG_FUNCTION_ARGS) +{ + return byteasend(fcinfo); +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index f5e31c433a0de..6c5c1019e1efd 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -2,7 +2,7 @@ * * PostgreSQL locale utilities * - * Portions Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/backend/utils/adt/pg_locale.c * @@ -32,6 +32,9 @@ #include "postgres.h" #include +#ifdef USE_ICU +#include +#endif #include "access/htup_details.h" #include "catalog/pg_collation.h" @@ -41,11 +44,11 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "utils/builtins.h" -#include "utils/formatting.h" #include "utils/guc_hooks.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" +#include "utils/pg_locale_c.h" #include "utils/relcache.h" #include "utils/syscache.h" @@ -80,31 +83,6 @@ extern pg_locale_t create_pg_locale_icu(Oid collid, MemoryContext context); extern pg_locale_t create_pg_locale_libc(Oid collid, MemoryContext context); extern char *get_collation_actual_version_libc(const char *collcollate); -extern size_t strlower_builtin(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strtitle_builtin(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strupper_builtin(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strfold_builtin(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); - -extern size_t strlower_icu(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strtitle_icu(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strupper_icu(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strfold_icu(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); - -extern size_t strlower_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strtitle_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strupper_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); - /* GUC settings */ char *locale_messages; char *locale_monetary; @@ -125,15 +103,18 @@ char *localized_full_days[7 + 1]; char *localized_abbrev_months[12 + 1]; char *localized_full_months[12 + 1]; -/* is the databases's LC_CTYPE the C locale? */ -bool database_ctype_is_c = false; - static pg_locale_t default_locale = NULL; /* indicates whether locale information cache is valid */ static bool CurrentLocaleConvValid = false; static bool CurrentLCTimeValid = false; +static struct pg_locale_struct c_locale = { + .deterministic = true, + .collate_is_c = true, + .ctype_is_c = true, +}; + /* Cache for collation-related knowledge */ typedef struct @@ -975,7 +956,7 @@ get_iso_localename(const char *winlocname) wchar_t wc_locale_name[LOCALE_NAME_MAX_LENGTH]; wchar_t buffer[LOCALE_NAME_MAX_LENGTH]; static char iso_lc_messages[LOCALE_NAME_MAX_LENGTH]; - char *period; + const char *period; int len; int ret_val; @@ -1093,6 +1074,9 @@ create_pg_locale(Oid collid, MemoryContext context) Assert((result->collate_is_c && result->collate == NULL) || (!result->collate_is_c && result->collate != NULL)); + Assert((result->ctype_is_c && result->ctype == NULL) || + (!result->ctype_is_c && result->ctype != NULL)); + datum = SysCacheGetAttr(COLLOID, tp, Anum_pg_collation_collversion, &isnull); if (!isnull) @@ -1172,11 +1156,27 @@ init_database_collation(void) PGLOCALE_SUPPORT_ERROR(dbform->datlocprovider); result->is_default = true; + + Assert((result->collate_is_c && result->collate == NULL) || + (!result->collate_is_c && result->collate != NULL)); + + Assert((result->ctype_is_c && result->ctype == NULL) || + (!result->ctype_is_c && result->ctype != NULL)); + ReleaseSysCache(tup); default_locale = result; } +/* + * Get database default locale. + */ +pg_locale_t +pg_database_locale(void) +{ + return pg_newlocale_from_collation(DEFAULT_COLLATION_OID); +} + /* * Create a pg_locale_t from a collation OID. Results are cached for the * lifetime of the backend. Thus, do not free the result with freelocale(). @@ -1194,6 +1194,13 @@ pg_newlocale_from_collation(Oid collid) if (collid == DEFAULT_COLLATION_OID) return default_locale; + /* + * Some callers expect C_COLLATION_OID to succeed even without catalog + * access. + */ + if (collid == C_COLLATION_OID) + return &c_locale; + if (!OidIsValid(collid)) elog(ERROR, "cache lookup failed for collation %u", collid); @@ -1218,10 +1225,10 @@ pg_newlocale_from_collation(Oid collid) * Make sure cache entry is marked invalid, in case we fail before * setting things. */ - cache_entry->locale = 0; + cache_entry->locale = NULL; } - if (cache_entry->locale == 0) + if (cache_entry->locale == NULL) { cache_entry->locale = create_pg_locale(collid, CollationCacheContext); } @@ -1253,81 +1260,119 @@ get_collation_actual_version(char collprovider, const char *collcollate) return collversion; } +/* lowercasing/casefolding in C locale */ +static size_t +strlower_c(char *dst, size_t dstsize, const char *src, ssize_t srclen) +{ + int i; + + srclen = (srclen >= 0) ? srclen : strlen(src); + for (i = 0; i < srclen && i < dstsize; i++) + dst[i] = pg_ascii_tolower(src[i]); + if (i < dstsize) + dst[i] = '\0'; + return srclen; +} + +/* titlecasing in C locale */ +static size_t +strtitle_c(char *dst, size_t dstsize, const char *src, ssize_t srclen) +{ + bool wasalnum = false; + int i; + + srclen = (srclen >= 0) ? srclen : strlen(src); + for (i = 0; i < srclen && i < dstsize; i++) + { + char c = src[i]; + + if (wasalnum) + dst[i] = pg_ascii_tolower(c); + else + dst[i] = pg_ascii_toupper(c); + + wasalnum = ((c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z')); + } + if (i < dstsize) + dst[i] = '\0'; + return srclen; +} + +/* uppercasing in C locale */ +static size_t +strupper_c(char *dst, size_t dstsize, const char *src, ssize_t srclen) +{ + int i; + + srclen = (srclen >= 0) ? srclen : strlen(src); + for (i = 0; i < srclen && i < dstsize; i++) + dst[i] = pg_ascii_toupper(src[i]); + if (i < dstsize) + dst[i] = '\0'; + return srclen; +} + size_t pg_strlower(char *dst, size_t dstsize, const char *src, ssize_t srclen, pg_locale_t locale) { - if (locale->provider == COLLPROVIDER_BUILTIN) - return strlower_builtin(dst, dstsize, src, srclen, locale); -#ifdef USE_ICU - else if (locale->provider == COLLPROVIDER_ICU) - return strlower_icu(dst, dstsize, src, srclen, locale); -#endif - else if (locale->provider == COLLPROVIDER_LIBC) - return strlower_libc(dst, dstsize, src, srclen, locale); + if (locale->ctype == NULL) + return strlower_c(dst, dstsize, src, srclen); else - /* shouldn't happen */ - PGLOCALE_SUPPORT_ERROR(locale->provider); - - return 0; /* keep compiler quiet */ + return locale->ctype->strlower(dst, dstsize, src, srclen, locale); } size_t pg_strtitle(char *dst, size_t dstsize, const char *src, ssize_t srclen, pg_locale_t locale) { - if (locale->provider == COLLPROVIDER_BUILTIN) - return strtitle_builtin(dst, dstsize, src, srclen, locale); -#ifdef USE_ICU - else if (locale->provider == COLLPROVIDER_ICU) - return strtitle_icu(dst, dstsize, src, srclen, locale); -#endif - else if (locale->provider == COLLPROVIDER_LIBC) - return strtitle_libc(dst, dstsize, src, srclen, locale); + if (locale->ctype == NULL) + return strtitle_c(dst, dstsize, src, srclen); else - /* shouldn't happen */ - PGLOCALE_SUPPORT_ERROR(locale->provider); - - return 0; /* keep compiler quiet */ + return locale->ctype->strtitle(dst, dstsize, src, srclen, locale); } size_t pg_strupper(char *dst, size_t dstsize, const char *src, ssize_t srclen, pg_locale_t locale) { - if (locale->provider == COLLPROVIDER_BUILTIN) - return strupper_builtin(dst, dstsize, src, srclen, locale); -#ifdef USE_ICU - else if (locale->provider == COLLPROVIDER_ICU) - return strupper_icu(dst, dstsize, src, srclen, locale); -#endif - else if (locale->provider == COLLPROVIDER_LIBC) - return strupper_libc(dst, dstsize, src, srclen, locale); + if (locale->ctype == NULL) + return strupper_c(dst, dstsize, src, srclen); else - /* shouldn't happen */ - PGLOCALE_SUPPORT_ERROR(locale->provider); - - return 0; /* keep compiler quiet */ + return locale->ctype->strupper(dst, dstsize, src, srclen, locale); } size_t pg_strfold(char *dst, size_t dstsize, const char *src, ssize_t srclen, pg_locale_t locale) { - if (locale->provider == COLLPROVIDER_BUILTIN) - return strfold_builtin(dst, dstsize, src, srclen, locale); -#ifdef USE_ICU - else if (locale->provider == COLLPROVIDER_ICU) - return strfold_icu(dst, dstsize, src, srclen, locale); -#endif - /* for libc, just use strlower */ - else if (locale->provider == COLLPROVIDER_LIBC) - return strlower_libc(dst, dstsize, src, srclen, locale); + /* in the C locale, casefolding is the same as lowercasing */ + if (locale->ctype == NULL) + return strlower_c(dst, dstsize, src, srclen); else - /* shouldn't happen */ - PGLOCALE_SUPPORT_ERROR(locale->provider); + return locale->ctype->strfold(dst, dstsize, src, srclen, locale); +} - return 0; /* keep compiler quiet */ +/* + * Lowercase an identifier using the database default locale. + * + * For historical reasons, does not use ordinary locale behavior. Should only + * be used for identifiers. XXX: can we make this equivalent to + * pg_strfold(..., default_locale)? + */ +size_t +pg_downcase_ident(char *dst, size_t dstsize, const char *src, ssize_t srclen) +{ + pg_locale_t locale = default_locale; + + if (locale == NULL || locale->ctype == NULL || + locale->ctype->downcase_ident == NULL) + return strlower_c(dst, dstsize, src, srclen); + else + return locale->ctype->downcase_ident(dst, dstsize, src, srclen, + locale); } /* @@ -1464,6 +1509,156 @@ pg_strnxfrm_prefix(char *dest, size_t destsize, const char *src, return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale); } +bool +pg_iswdigit(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISDIGIT)); + else + return locale->ctype->wc_isdigit(wc, locale); +} + +bool +pg_iswalpha(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISALPHA)); + else + return locale->ctype->wc_isalpha(wc, locale); +} + +bool +pg_iswalnum(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISALNUM)); + else + return locale->ctype->wc_isalnum(wc, locale); +} + +bool +pg_iswupper(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISUPPER)); + else + return locale->ctype->wc_isupper(wc, locale); +} + +bool +pg_iswlower(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISLOWER)); + else + return locale->ctype->wc_islower(wc, locale); +} + +bool +pg_iswgraph(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISGRAPH)); + else + return locale->ctype->wc_isgraph(wc, locale); +} + +bool +pg_iswprint(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISPRINT)); + else + return locale->ctype->wc_isprint(wc, locale); +} + +bool +pg_iswpunct(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISPUNCT)); + else + return locale->ctype->wc_ispunct(wc, locale); +} + +bool +pg_iswspace(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISSPACE)); + else + return locale->ctype->wc_isspace(wc, locale); +} + +bool +pg_iswxdigit(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + ((pg_char_properties[wc] & PG_ISDIGIT) || + ((wc >= 'A' && wc <= 'F') || + (wc >= 'a' && wc <= 'f')))); + else + return locale->ctype->wc_isxdigit(wc, locale); +} + +bool +pg_iswcased(pg_wchar wc, pg_locale_t locale) +{ + /* for the C locale, Cased and Alpha are equivalent */ + if (locale->ctype == NULL) + return (wc <= (pg_wchar) 127 && + (pg_char_properties[wc] & PG_ISALPHA)); + else + return locale->ctype->wc_iscased(wc, locale); +} + +pg_wchar +pg_towupper(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + { + if (wc <= (pg_wchar) 127) + return pg_ascii_toupper((unsigned char) wc); + return wc; + } + else + return locale->ctype->wc_toupper(wc, locale); +} + +pg_wchar +pg_towlower(pg_wchar wc, pg_locale_t locale) +{ + if (locale->ctype == NULL) + { + if (wc <= (pg_wchar) 127) + return pg_ascii_tolower((unsigned char) wc); + return wc; + } + else + return locale->ctype->wc_tolower(wc, locale); +} + +/* version of Unicode used by ICU */ +const char * +pg_icu_unicode_version(void) +{ +#ifdef USE_ICU + return U_UNICODE_VERSION; +#else + return NULL; +#endif +} + /* * Return required encoding ID for the given locale, or -1 if any encoding is * valid for the locale. diff --git a/src/backend/utils/adt/pg_locale_builtin.c b/src/backend/utils/adt/pg_locale_builtin.c index f51768830cd7b..794aa37df768b 100644 --- a/src/backend/utils/adt/pg_locale_builtin.c +++ b/src/backend/utils/adt/pg_locale_builtin.c @@ -2,7 +2,7 @@ * * PostgreSQL locale utilities for builtin provider * - * Portions Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/backend/utils/adt/pg_locale_builtin.c * @@ -15,25 +15,14 @@ #include "catalog/pg_collation.h" #include "common/unicode_case.h" #include "common/unicode_category.h" -#include "mb/pg_wchar.h" #include "miscadmin.h" #include "utils/builtins.h" -#include "utils/memutils.h" #include "utils/pg_locale.h" #include "utils/syscache.h" extern pg_locale_t create_pg_locale_builtin(Oid collid, MemoryContext context); extern char *get_collation_actual_version_builtin(const char *collcollate); -extern size_t strlower_builtin(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strtitle_builtin(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strupper_builtin(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strfold_builtin(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); - struct WordBoundaryState { @@ -45,6 +34,23 @@ struct WordBoundaryState bool prev_alnum; }; +/* + * In UTF-8, pg_wchar is guaranteed to be the code point value. + */ +static inline char32_t +to_char32(pg_wchar wc) +{ + Assert(GetDatabaseEncoding() == PG_UTF8); + return (char32_t) wc; +} + +static inline pg_wchar +to_pg_wchar(char32_t c32) +{ + Assert(GetDatabaseEncoding() == PG_UTF8); + return (pg_wchar) c32; +} + /* * Simple word boundary iterator that draws boundaries each time the result of * pg_u_isalnum() changes. @@ -57,7 +63,7 @@ initcap_wbnext(void *state) while (wbstate->offset < wbstate->len && wbstate->str[wbstate->offset] != '\0') { - pg_wchar u = utf8_to_unicode((unsigned char *) wbstate->str + + char32_t u = utf8_to_unicode((const unsigned char *) wbstate->str + wbstate->offset); bool curr_alnum = pg_u_isalnum(u, wbstate->posix); @@ -77,48 +83,148 @@ initcap_wbnext(void *state) return wbstate->len; } -size_t +static size_t strlower_builtin(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { return unicode_strlower(dest, destsize, src, srclen, - locale->info.builtin.casemap_full); + locale->builtin.casemap_full); } -size_t +static size_t strtitle_builtin(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { struct WordBoundaryState wbstate = { .str = src, - .len = srclen, + .len = (srclen < 0) ? strlen(src) : srclen, .offset = 0, - .posix = !locale->info.builtin.casemap_full, + .posix = !locale->builtin.casemap_full, .init = false, .prev_alnum = false, }; return unicode_strtitle(dest, destsize, src, srclen, - locale->info.builtin.casemap_full, + locale->builtin.casemap_full, initcap_wbnext, &wbstate); } -size_t +static size_t strupper_builtin(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { return unicode_strupper(dest, destsize, src, srclen, - locale->info.builtin.casemap_full); + locale->builtin.casemap_full); } -size_t +static size_t strfold_builtin(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { return unicode_strfold(dest, destsize, src, srclen, - locale->info.builtin.casemap_full); + locale->builtin.casemap_full); +} + +static bool +wc_isdigit_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isdigit(to_char32(wc), !locale->builtin.casemap_full); +} + +static bool +wc_isalpha_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isalpha(to_char32(wc)); +} + +static bool +wc_isalnum_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isalnum(to_char32(wc), !locale->builtin.casemap_full); +} + +static bool +wc_isupper_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isupper(to_char32(wc)); +} + +static bool +wc_islower_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_islower(to_char32(wc)); +} + +static bool +wc_isgraph_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isgraph(to_char32(wc)); +} + +static bool +wc_isprint_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isprint(to_char32(wc)); } +static bool +wc_ispunct_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_ispunct(to_char32(wc), !locale->builtin.casemap_full); +} + +static bool +wc_isspace_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isspace(to_char32(wc)); +} + +static bool +wc_isxdigit_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_isxdigit(to_char32(wc), !locale->builtin.casemap_full); +} + +static bool +wc_iscased_builtin(pg_wchar wc, pg_locale_t locale) +{ + return pg_u_prop_cased(to_char32(wc)); +} + +static pg_wchar +wc_toupper_builtin(pg_wchar wc, pg_locale_t locale) +{ + return to_pg_wchar(unicode_uppercase_simple(to_char32(wc))); +} + +static pg_wchar +wc_tolower_builtin(pg_wchar wc, pg_locale_t locale) +{ + return to_pg_wchar(unicode_lowercase_simple(to_char32(wc))); +} + +static const struct ctype_methods ctype_methods_builtin = { + .strlower = strlower_builtin, + .strtitle = strtitle_builtin, + .strupper = strupper_builtin, + .strfold = strfold_builtin, + /* uses plain ASCII semantics for historical reasons */ + .downcase_ident = NULL, + .wc_isdigit = wc_isdigit_builtin, + .wc_isalpha = wc_isalpha_builtin, + .wc_isalnum = wc_isalnum_builtin, + .wc_isupper = wc_isupper_builtin, + .wc_islower = wc_islower_builtin, + .wc_isgraph = wc_isgraph_builtin, + .wc_isprint = wc_isprint_builtin, + .wc_ispunct = wc_ispunct_builtin, + .wc_isspace = wc_isspace_builtin, + .wc_isxdigit = wc_isxdigit_builtin, + .wc_iscased = wc_iscased_builtin, + .wc_tolower = wc_tolower_builtin, + .wc_toupper = wc_toupper_builtin, +}; + pg_locale_t create_pg_locale_builtin(Oid collid, MemoryContext context) { @@ -156,12 +262,13 @@ create_pg_locale_builtin(Oid collid, MemoryContext context) result = MemoryContextAllocZero(context, sizeof(struct pg_locale_struct)); - result->info.builtin.locale = MemoryContextStrdup(context, locstr); - result->info.builtin.casemap_full = (strcmp(locstr, "PG_UNICODE_FAST") == 0); - result->provider = COLLPROVIDER_BUILTIN; + result->builtin.locale = MemoryContextStrdup(context, locstr); + result->builtin.casemap_full = (strcmp(locstr, "PG_UNICODE_FAST") == 0); result->deterministic = true; result->collate_is_c = true; result->ctype_is_c = (strcmp(locstr, "C") == 0); + if (!result->ctype_is_c) + result->ctype = &ctype_methods_builtin; return result; } diff --git a/src/backend/utils/adt/pg_locale_icu.c b/src/backend/utils/adt/pg_locale_icu.c index a32c32a0744bd..a4a4e82eb9e98 100644 --- a/src/backend/utils/adt/pg_locale_icu.c +++ b/src/backend/utils/adt/pg_locale_icu.c @@ -2,7 +2,7 @@ * * PostgreSQL locale utilities for ICU * - * Portions Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/backend/utils/adt/pg_locale_icu.c * @@ -12,7 +12,9 @@ #include "postgres.h" #ifdef USE_ICU +#include #include +#include #include /* @@ -48,19 +50,33 @@ #define TEXTBUFLEN 1024 extern pg_locale_t create_pg_locale_icu(Oid collid, MemoryContext context); -extern size_t strlower_icu(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strtitle_icu(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strupper_icu(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strfold_icu(char *dest, size_t destsize, const char *src, - ssize_t srclen, pg_locale_t locale); #ifdef USE_ICU extern UCollator *pg_ucol_open(const char *loc_str); +static UCaseMap *pg_ucasemap_open(const char *loc_str); +static size_t strlower_icu(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strtitle_icu(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strupper_icu(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strfold_icu(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strlower_icu_utf8(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strtitle_icu_utf8(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strupper_icu_utf8(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t strfold_icu_utf8(char *dest, size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static size_t downcase_ident_icu(char *dst, size_t dstsize, const char *src, + ssize_t srclen, pg_locale_t locale); +static int strncoll_icu(const char *arg1, ssize_t len1, + const char *arg2, ssize_t len2, + pg_locale_t locale); static size_t strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale); @@ -106,9 +122,9 @@ static size_t icu_from_uchar(char *dest, size_t destsize, const UChar *buff_uchar, int32_t len_uchar); static void icu_set_collation_attributes(UCollator *collator, const char *loc, UErrorCode *status); -static int32_t icu_convert_case(ICU_Convert_Func func, pg_locale_t mylocale, - UChar **buff_dest, UChar *buff_source, - int32_t len_source); +static int32_t icu_convert_case(ICU_Convert_Func func, char *dest, + size_t destsize, const char *src, + ssize_t srclen, pg_locale_t locale); static int32_t u_strToTitle_default_BI(UChar *dest, int32_t destCapacity, const UChar *src, int32_t srcLength, const char *locale, @@ -117,6 +133,24 @@ static int32_t u_strFoldCase_default(UChar *dest, int32_t destCapacity, const UChar *src, int32_t srcLength, const char *locale, UErrorCode *pErrorCode); +static int32_t foldcase_options(const char *locale); + +/* + * XXX: many of the functions below rely on casts directly from pg_wchar to + * UChar32, which is correct for UTF-8 and LATIN1, but not in general. + */ + +static pg_wchar +toupper_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_toupper(wc); +} + +static pg_wchar +tolower_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_tolower(wc); +} static const struct collate_methods collate_methods_icu = { .strncoll = strncoll_icu, @@ -136,6 +170,137 @@ static const struct collate_methods collate_methods_icu_utf8 = { .strxfrm_is_safe = true, }; +static bool +wc_isdigit_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isdigit(wc); +} + +static bool +wc_isalpha_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isalpha(wc); +} + +static bool +wc_isalnum_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isalnum(wc); +} + +static bool +wc_isupper_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isupper(wc); +} + +static bool +wc_islower_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_islower(wc); +} + +static bool +wc_isgraph_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isgraph(wc); +} + +static bool +wc_isprint_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isprint(wc); +} + +static bool +wc_ispunct_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_ispunct(wc); +} + +static bool +wc_isspace_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isspace(wc); +} + +static bool +wc_isxdigit_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_isxdigit(wc); +} + +static bool +wc_iscased_icu(pg_wchar wc, pg_locale_t locale) +{ + return u_hasBinaryProperty(wc, UCHAR_CASED); +} + +static const struct ctype_methods ctype_methods_icu = { + .strlower = strlower_icu, + .strtitle = strtitle_icu, + .strupper = strupper_icu, + .strfold = strfold_icu, + .downcase_ident = downcase_ident_icu, + .wc_isdigit = wc_isdigit_icu, + .wc_isalpha = wc_isalpha_icu, + .wc_isalnum = wc_isalnum_icu, + .wc_isupper = wc_isupper_icu, + .wc_islower = wc_islower_icu, + .wc_isgraph = wc_isgraph_icu, + .wc_isprint = wc_isprint_icu, + .wc_ispunct = wc_ispunct_icu, + .wc_isspace = wc_isspace_icu, + .wc_isxdigit = wc_isxdigit_icu, + .wc_iscased = wc_iscased_icu, + .wc_toupper = toupper_icu, + .wc_tolower = tolower_icu, +}; + +static const struct ctype_methods ctype_methods_icu_utf8 = { + .strlower = strlower_icu_utf8, + .strtitle = strtitle_icu_utf8, + .strupper = strupper_icu_utf8, + .strfold = strfold_icu_utf8, + /* uses plain ASCII semantics for historical reasons */ + .downcase_ident = NULL, + .wc_isdigit = wc_isdigit_icu, + .wc_isalpha = wc_isalpha_icu, + .wc_isalnum = wc_isalnum_icu, + .wc_isupper = wc_isupper_icu, + .wc_islower = wc_islower_icu, + .wc_isgraph = wc_isgraph_icu, + .wc_isprint = wc_isprint_icu, + .wc_ispunct = wc_ispunct_icu, + .wc_isspace = wc_isspace_icu, + .wc_isxdigit = wc_isxdigit_icu, + .wc_iscased = wc_iscased_icu, + .wc_toupper = toupper_icu, + .wc_tolower = tolower_icu, +}; + +/* + * ICU still depends on libc for compatibility with certain historical + * behavior for single-byte encodings. See downcase_ident_icu(). + * + * XXX: consider fixing by decoding the single byte into a code point, and + * using u_tolower(). + */ +static locale_t +make_libc_ctype_locale(const char *ctype) +{ + locale_t loc; + +#ifndef WIN32 + loc = newlocale(LC_CTYPE_MASK, ctype, NULL); +#else + loc = _create_locale(LC_ALL, ctype); +#endif + if (!loc) + report_newlocale_failure(ctype); + + return loc; +} #endif pg_locale_t @@ -146,6 +311,7 @@ create_pg_locale_icu(Oid collid, MemoryContext context) const char *iculocstr; const char *icurules = NULL; UCollator *collator; + locale_t loc = (locale_t) 0; pg_locale_t result; if (collid == DEFAULT_COLLATION_OID) @@ -168,6 +334,18 @@ create_pg_locale_icu(Oid collid, MemoryContext context) if (!isnull) icurules = TextDatumGetCString(datum); + /* libc only needed for default locale and single-byte encoding */ + if (pg_database_encoding_max_length() == 1) + { + const char *ctype; + + datum = SysCacheGetAttrNotNull(DATABASEOID, tp, + Anum_pg_database_datctype); + ctype = TextDatumGetCString(datum); + + loc = make_libc_ctype_locale(ctype); + } + ReleaseSysCache(tp); } else @@ -196,16 +374,23 @@ create_pg_locale_icu(Oid collid, MemoryContext context) collator = make_icu_collator(iculocstr, icurules); result = MemoryContextAllocZero(context, sizeof(struct pg_locale_struct)); - result->info.icu.locale = MemoryContextStrdup(context, iculocstr); - result->info.icu.ucol = collator; - result->provider = COLLPROVIDER_ICU; + result->icu.locale = MemoryContextStrdup(context, iculocstr); + result->icu.ucol = collator; + result->icu.lt = loc; result->deterministic = deterministic; result->collate_is_c = false; result->ctype_is_c = false; if (GetDatabaseEncoding() == PG_UTF8) + { + result->icu.ucasemap = pg_ucasemap_open(iculocstr); result->collate = &collate_methods_icu_utf8; + result->ctype = &ctype_methods_icu_utf8; + } else + { result->collate = &collate_methods_icu; + result->ctype = &ctype_methods_icu; + } return result; #else @@ -221,19 +406,15 @@ create_pg_locale_icu(Oid collid, MemoryContext context) #ifdef USE_ICU /* - * Wrapper around ucol_open() to handle API differences for older ICU - * versions. + * Check locale string and fix it if necessary. Returns a new palloc'd string. * - * Ensure that no path leaks a UCollator. + * In ICU versions 54 and earlier, "und" is not a recognized spelling of the + * root locale. If the first component of the locale is "und", replace with + * "root" before opening. */ -UCollator * -pg_ucol_open(const char *loc_str) +static char * +fix_icu_locale_str(const char *loc_str) { - UCollator *collator; - UErrorCode status; - const char *orig_str = loc_str; - char *fixed_str = NULL; - /* * Must never open default collator, because it depends on the environment * and may change at any time. Should not happen, but check here to catch @@ -246,16 +427,11 @@ pg_ucol_open(const char *loc_str) if (loc_str == NULL) elog(ERROR, "opening default collator is not supported"); - /* - * In ICU versions 54 and earlier, "und" is not a recognized spelling of - * the root locale. If the first component of the locale is "und", replace - * with "root" before opening. - */ if (U_ICU_VERSION_MAJOR_NUM < 55) { char lang[ULOC_LANG_CAPACITY]; + UErrorCode status = U_ZERO_ERROR; - status = U_ZERO_ERROR; uloc_getLanguage(loc_str, lang, ULOC_LANG_CAPACITY, &status); if (U_FAILURE(status) || status == U_STRING_NOT_TERMINATED_WARNING) { @@ -268,28 +444,47 @@ pg_ucol_open(const char *loc_str) if (strcmp(lang, "und") == 0) { const char *remainder = loc_str + strlen("und"); + char *fixed_str; fixed_str = palloc(strlen("root") + strlen(remainder) + 1); strcpy(fixed_str, "root"); strcat(fixed_str, remainder); - loc_str = fixed_str; + return fixed_str; } } + return pstrdup(loc_str); +} + +/* + * Wrapper around ucol_open() to handle API differences for older ICU + * versions. + * + * Ensure that no path leaks a UCollator. + */ +UCollator * +pg_ucol_open(const char *loc_str) +{ + UCollator *collator; + UErrorCode status; + char *fixed_str; + + fixed_str = fix_icu_locale_str(loc_str); + status = U_ZERO_ERROR; - collator = ucol_open(loc_str, &status); + collator = ucol_open(fixed_str, &status); if (U_FAILURE(status)) ereport(ERROR, /* use original string for error report */ (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not open collator for locale \"%s\": %s", - orig_str, u_errorName(status)))); + loc_str, u_errorName(status)))); if (U_ICU_VERSION_MAJOR_NUM < 54) { status = U_ZERO_ERROR; - icu_set_collation_attributes(collator, loc_str, &status); + icu_set_collation_attributes(collator, fixed_str, &status); /* * Pretend the error came from ucol_open(), for consistent error @@ -301,16 +496,43 @@ pg_ucol_open(const char *loc_str) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("could not open collator for locale \"%s\": %s", - orig_str, u_errorName(status)))); + loc_str, u_errorName(status)))); } } - if (fixed_str != NULL) - pfree(fixed_str); + pfree(fixed_str); return collator; } +/* + * Wrapper around ucasemap_open() to handle API differences for older ICU + * versions. + * + * Additionally makes sure we get the right options for case folding. + */ +static UCaseMap * +pg_ucasemap_open(const char *loc_str) +{ + UErrorCode status = U_ZERO_ERROR; + UCaseMap *casemap; + char *fixed_str; + + fixed_str = fix_icu_locale_str(loc_str); + + casemap = ucasemap_open(fixed_str, foldcase_options(fixed_str), &status); + if (U_FAILURE(status)) + /* use original string for error report */ + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not open casemap for locale \"%s\": %s", + loc_str, u_errorName(status))); + + pfree(fixed_str); + + return casemap; +} + /* * Create a UCollator with the given locale string and rules. * @@ -365,7 +587,7 @@ make_icu_collator(const char *iculocstr, const char *icurules) status = U_ZERO_ERROR; collator_all_rules = ucol_openRules(all_rules, u_strlen(all_rules), - UCOL_DEFAULT, UCOL_DEFAULT_STRENGTH, + UCOL_DEFAULT, UCOL_DEFAULT, NULL, &status); if (U_FAILURE(status)) { @@ -375,88 +597,127 @@ make_icu_collator(const char *iculocstr, const char *icurules) iculocstr, icurules, u_errorName(status)))); } + pfree(my_rules); + pfree(all_rules); return collator_all_rules; } } -size_t +static size_t strlower_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - int32_t len_uchar; - int32_t len_conv; - UChar *buff_uchar; - UChar *buff_conv; - size_t result_len; - - len_uchar = icu_to_uchar(&buff_uchar, src, srclen); - len_conv = icu_convert_case(u_strToLower, locale, - &buff_conv, buff_uchar, len_uchar); - result_len = icu_from_uchar(dest, destsize, buff_conv, len_conv); - pfree(buff_uchar); - pfree(buff_conv); - - return result_len; + return icu_convert_case(u_strToLower, dest, destsize, src, srclen, locale); } -size_t +static size_t strtitle_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - int32_t len_uchar; - int32_t len_conv; - UChar *buff_uchar; - UChar *buff_conv; - size_t result_len; - - len_uchar = icu_to_uchar(&buff_uchar, src, srclen); - len_conv = icu_convert_case(u_strToTitle_default_BI, locale, - &buff_conv, buff_uchar, len_uchar); - result_len = icu_from_uchar(dest, destsize, buff_conv, len_conv); - pfree(buff_uchar); - pfree(buff_conv); - - return result_len; + return icu_convert_case(u_strToTitle_default_BI, dest, destsize, src, srclen, locale); } -size_t +static size_t strupper_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - int32_t len_uchar; - int32_t len_conv; - UChar *buff_uchar; - UChar *buff_conv; - size_t result_len; - - len_uchar = icu_to_uchar(&buff_uchar, src, srclen); - len_conv = icu_convert_case(u_strToUpper, locale, - &buff_conv, buff_uchar, len_uchar); - result_len = icu_from_uchar(dest, destsize, buff_conv, len_conv); - pfree(buff_uchar); - pfree(buff_conv); - - return result_len; + return icu_convert_case(u_strToUpper, dest, destsize, src, srclen, locale); } -size_t +static size_t strfold_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - int32_t len_uchar; - int32_t len_conv; - UChar *buff_uchar; - UChar *buff_conv; - size_t result_len; + return icu_convert_case(u_strFoldCase_default, dest, destsize, src, srclen, locale); +} - len_uchar = icu_to_uchar(&buff_uchar, src, srclen); - len_conv = icu_convert_case(u_strFoldCase_default, locale, - &buff_conv, buff_uchar, len_uchar); - result_len = icu_from_uchar(dest, destsize, buff_conv, len_conv); - pfree(buff_uchar); - pfree(buff_conv); +static size_t +strlower_icu_utf8(char *dest, size_t destsize, const char *src, ssize_t srclen, + pg_locale_t locale) +{ + UErrorCode status = U_ZERO_ERROR; + int32_t needed; - return result_len; + needed = ucasemap_utf8ToLower(locale->icu.ucasemap, dest, destsize, src, srclen, &status); + if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) + ereport(ERROR, + errmsg("case conversion failed: %s", u_errorName(status))); + return needed; +} + +static size_t +strtitle_icu_utf8(char *dest, size_t destsize, const char *src, ssize_t srclen, + pg_locale_t locale) +{ + UErrorCode status = U_ZERO_ERROR; + int32_t needed; + + needed = ucasemap_utf8ToTitle(locale->icu.ucasemap, dest, destsize, src, srclen, &status); + if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) + ereport(ERROR, + errmsg("case conversion failed: %s", u_errorName(status))); + return needed; +} + +static size_t +strupper_icu_utf8(char *dest, size_t destsize, const char *src, ssize_t srclen, + pg_locale_t locale) +{ + UErrorCode status = U_ZERO_ERROR; + int32_t needed; + + needed = ucasemap_utf8ToUpper(locale->icu.ucasemap, dest, destsize, src, srclen, &status); + if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) + ereport(ERROR, + errmsg("case conversion failed: %s", u_errorName(status))); + return needed; +} + +static size_t +strfold_icu_utf8(char *dest, size_t destsize, const char *src, ssize_t srclen, + pg_locale_t locale) +{ + UErrorCode status = U_ZERO_ERROR; + int32_t needed; + + needed = ucasemap_utf8FoldCase(locale->icu.ucasemap, dest, destsize, src, srclen, &status); + if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) + ereport(ERROR, + errmsg("case conversion failed: %s", u_errorName(status))); + return needed; +} + +/* + * For historical compatibility, behavior is not multibyte-aware. + * + * NB: uses libc tolower() for single-byte encodings (also for historical + * compatibility), and therefore relies on the global LC_CTYPE setting. + */ +static size_t +downcase_ident_icu(char *dst, size_t dstsize, const char *src, + ssize_t srclen, pg_locale_t locale) +{ + int i; + bool libc_lower; + locale_t lt = locale->icu.lt; + + libc_lower = lt && (pg_database_encoding_max_length() == 1); + + for (i = 0; i < srclen && i < dstsize; i++) + { + unsigned char ch = (unsigned char) src[i]; + + if (ch >= 'A' && ch <= 'Z') + ch = pg_ascii_tolower(ch); + else if (libc_lower && IS_HIGHBIT_SET(ch) && isupper_l(ch, lt)) + ch = tolower_l(ch, lt); + dst[i] = (char) ch; + } + + if (i < dstsize) + dst[i] = '\0'; + + return srclen; } /* @@ -474,12 +735,10 @@ strncoll_icu_utf8(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2 int result; UErrorCode status; - Assert(locale->provider == COLLPROVIDER_ICU); - Assert(GetDatabaseEncoding() == PG_UTF8); status = U_ZERO_ERROR; - result = ucol_strcollUTF8(locale->info.icu.ucol, + result = ucol_strcollUTF8(locale->icu.ucol, arg1, len1, arg2, len2, &status); @@ -503,8 +762,6 @@ strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, size_t uchar_bsize; Size result_bsize; - Assert(locale->provider == COLLPROVIDER_ICU); - init_icu_converter(); ulen = uchar_length(icu_converter, src, srclen); @@ -518,7 +775,7 @@ strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen, ulen = uchar_convert(icu_converter, uchar, ulen + 1, src, srclen); - result_bsize = ucol_getSortKey(locale->info.icu.ucol, + result_bsize = ucol_getSortKey(locale->icu.ucol, uchar, ulen, (uint8_t *) dest, destsize); @@ -549,14 +806,12 @@ strnxfrm_prefix_icu_utf8(char *dest, size_t destsize, uint32_t state[2]; UErrorCode status; - Assert(locale->provider == COLLPROVIDER_ICU); - Assert(GetDatabaseEncoding() == PG_UTF8); uiter_setUTF8(&iter, src, srclen); state[0] = state[1] = 0; /* won't need that again */ status = U_ZERO_ERROR; - result = ucol_nextSortKeyPart(locale->info.icu.ucol, + result = ucol_nextSortKeyPart(locale->icu.ucol, &iter, state, (uint8_t *) dest, @@ -657,8 +912,8 @@ icu_from_uchar(char *dest, size_t destsize, const UChar *buff_uchar, int32_t len } static int32_t -icu_convert_case(ICU_Convert_Func func, pg_locale_t mylocale, - UChar **buff_dest, UChar *buff_source, int32_t len_source) +convert_case_uchar(ICU_Convert_Func func, pg_locale_t mylocale, + UChar **buff_dest, UChar *buff_source, int32_t len_source) { UErrorCode status; int32_t len_dest; @@ -667,7 +922,7 @@ icu_convert_case(ICU_Convert_Func func, pg_locale_t mylocale, *buff_dest = palloc(len_dest * sizeof(**buff_dest)); status = U_ZERO_ERROR; len_dest = func(*buff_dest, len_dest, buff_source, len_source, - mylocale->info.icu.locale, &status); + mylocale->icu.locale, &status); if (status == U_BUFFER_OVERFLOW_ERROR) { /* try again with adjusted length */ @@ -675,7 +930,7 @@ icu_convert_case(ICU_Convert_Func func, pg_locale_t mylocale, *buff_dest = palloc(len_dest * sizeof(**buff_dest)); status = U_ZERO_ERROR; len_dest = func(*buff_dest, len_dest, buff_source, len_source, - mylocale->info.icu.locale, &status); + mylocale->icu.locale, &status); } if (U_FAILURE(status)) ereport(ERROR, @@ -683,6 +938,26 @@ icu_convert_case(ICU_Convert_Func func, pg_locale_t mylocale, return len_dest; } +static int32_t +icu_convert_case(ICU_Convert_Func func, char *dest, size_t destsize, + const char *src, ssize_t srclen, pg_locale_t locale) +{ + int32_t len_uchar; + int32_t len_conv; + UChar *buff_uchar; + UChar *buff_conv; + size_t result_len; + + len_uchar = icu_to_uchar(&buff_uchar, src, srclen); + len_conv = convert_case_uchar(func, locale, &buff_conv, + buff_uchar, len_uchar); + result_len = icu_from_uchar(dest, destsize, buff_conv, len_conv); + pfree(buff_uchar); + pfree(buff_conv); + + return result_len; +} + static int32_t u_strToTitle_default_BI(UChar *dest, int32_t destCapacity, const UChar *src, int32_t srcLength, @@ -698,20 +973,27 @@ u_strFoldCase_default(UChar *dest, int32_t destCapacity, const UChar *src, int32_t srcLength, const char *locale, UErrorCode *pErrorCode) +{ + return u_strFoldCase(dest, destCapacity, src, srcLength, + foldcase_options(locale), pErrorCode); +} + +/* + * Return the correct u_strFoldCase() options for the given locale. + * + * Unlike the ICU APIs for lowercasing, titlecasing, and uppercasing, case + * folding does not accept a locale. Instead it just supports a single option + * relevant to Turkic languages 'az' and 'tr'; check for those languages. + */ +static int32_t +foldcase_options(const char *locale) { uint32 options = U_FOLD_CASE_DEFAULT; - char lang[3]; - UErrorCode status; + char lang[ULOC_LANG_CAPACITY]; + UErrorCode status = U_ZERO_ERROR; - /* - * Unlike the ICU APIs for lowercasing, titlecasing, and uppercasing, case - * folding does not accept a locale. Instead it just supports a single - * option relevant to Turkic languages 'az' and 'tr'; check for those - * languages to enable the option. - */ - status = U_ZERO_ERROR; - uloc_getLanguage(locale, lang, 3, &status); - if (U_SUCCESS(status)) + uloc_getLanguage(locale, lang, ULOC_LANG_CAPACITY, &status); + if (U_SUCCESS(status) && status != U_STRING_NOT_TERMINATED_WARNING) { /* * The option name is confusing, but it causes u_strFoldCase to use @@ -721,8 +1003,7 @@ u_strFoldCase_default(UChar *dest, int32_t destCapacity, options = U_FOLD_CASE_EXCLUDE_SPECIAL_I; } - return u_strFoldCase(dest, destCapacity, src, srcLength, - options, pErrorCode); + return options; } /* @@ -749,8 +1030,6 @@ strncoll_icu(const char *arg1, ssize_t len1, *uchar2; int result; - Assert(locale->provider == COLLPROVIDER_ICU); - /* if encoding is UTF8, use more efficient strncoll_icu_utf8 */ #ifdef HAVE_UCOL_STRCOLLUTF8 Assert(GetDatabaseEncoding() != PG_UTF8); @@ -773,7 +1052,7 @@ strncoll_icu(const char *arg1, ssize_t len1, ulen1 = uchar_convert(icu_converter, uchar1, ulen1 + 1, arg1, len1); ulen2 = uchar_convert(icu_converter, uchar2, ulen2 + 1, arg2, len2); - result = ucol_strcoll(locale->info.icu.ucol, + result = ucol_strcoll(locale->icu.ucol, uchar1, ulen1, uchar2, ulen2); @@ -799,8 +1078,6 @@ strnxfrm_prefix_icu(char *dest, size_t destsize, size_t uchar_bsize; Size result_bsize; - Assert(locale->provider == COLLPROVIDER_ICU); - /* if encoding is UTF8, use more efficient strnxfrm_prefix_icu_utf8 */ Assert(GetDatabaseEncoding() != PG_UTF8); @@ -820,7 +1097,7 @@ strnxfrm_prefix_icu(char *dest, size_t destsize, uiter_setString(&iter, uchar, ulen); state[0] = state[1] = 0; /* won't need that again */ status = U_ZERO_ERROR; - result_bsize = ucol_nextSortKeyPart(locale->info.icu.ucol, + result_bsize = ucol_nextSortKeyPart(locale->icu.ucol, &iter, state, (uint8_t *) dest, @@ -831,6 +1108,9 @@ strnxfrm_prefix_icu(char *dest, size_t destsize, (errmsg("sort key generation failed: %s", u_errorName(status)))); + if (buf != sbuf) + pfree(buf); + return result_bsize; } diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c index 199857e22dbec..78f6ea161a0ec 100644 --- a/src/backend/utils/adt/pg_locale_libc.c +++ b/src/backend/utils/adt/pg_locale_libc.c @@ -2,7 +2,7 @@ * * PostgreSQL locale utilities for libc * - * Portions Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/backend/utils/adt/pg_locale_libc.c * @@ -33,6 +33,45 @@ #include #endif +/* + * For the libc provider, to provide as much functionality as possible on a + * variety of platforms without going so far as to implement everything from + * scratch, we use several implementation strategies depending on the + * situation: + * + * 1. In C/POSIX collations, we use hard-wired code. We can't depend on + * the functions since those will obey LC_CTYPE. Note that these + * collations don't give a fig about multibyte characters. + * + * 2. When working in UTF8 encoding, we use the functions. + * This assumes that every platform uses Unicode codepoints directly + * as the wchar_t representation of Unicode. On some platforms + * wchar_t is only 16 bits wide, so we have to punt for codepoints > 0xFFFF. + * + * 3. In all other encodings, we use the functions for pg_wchar + * values up to 255, and punt for values above that. This is 100% correct + * only in single-byte encodings such as LATINn. However, non-Unicode + * multibyte encodings are mostly Far Eastern character sets for which the + * properties being tested here aren't very relevant for higher code values + * anyway. The difficulty with using the functions with + * non-Unicode multibyte encodings is that we can have no certainty that + * the platform's wchar_t representation matches what we do in pg_wchar + * conversions. + * + * As a special case, in the "default" collation, (2) and (3) force ASCII + * letters to follow ASCII upcase/downcase rules, while in a non-default + * collation we just let the library functions do what they will. The case + * where this matters is treatment of I/i in Turkish, and the behavior is + * meant to match the upper()/lower() SQL functions. + * + * We store the active collation setting in static variables. In principle + * it could be passed down to here via the regex library's "struct vars" data + * structure; but that would require somewhat invasive changes in the regex + * library, and right now there's no real benefit to be gained from that. + * + * NB: the coding here assumes pg_wchar is an unsigned type. + */ + /* * Size of stack buffer to use for string transformations, used to avoid heap * allocations in typical cases. This should be large enough that most strings @@ -43,13 +82,6 @@ extern pg_locale_t create_pg_locale_libc(Oid collid, MemoryContext context); -extern size_t strlower_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strtitle_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); -extern size_t strupper_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale); - static int strncoll_libc(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2, pg_locale_t locale); @@ -66,6 +98,9 @@ static int strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, pg_locale_t locale); #endif +static size_t char2wchar(wchar_t *to, size_t tolen, const char *from, + size_t fromlen, locale_t loc); + static size_t strlower_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale); @@ -85,6 +120,304 @@ static size_t strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale); +static bool +wc_isdigit_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isdigit_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isalpha_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isalpha_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isalnum_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isalnum_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isupper_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isupper_l((unsigned char) wc, locale->lt); +} + +static bool +wc_islower_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return islower_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isgraph_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isgraph_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isprint_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isprint_l((unsigned char) wc, locale->lt); +} + +static bool +wc_ispunct_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return ispunct_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isspace_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isspace_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isxdigit_libc_sb(pg_wchar wc, pg_locale_t locale) +{ +#ifndef WIN32 + return isxdigit_l((unsigned char) wc, locale->lt); +#else + return _isxdigit_l((unsigned char) wc, locale->lt); +#endif +} + +static bool +wc_iscased_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + return isupper_l((unsigned char) wc, locale->lt) || + islower_l((unsigned char) wc, locale->lt); +} + +static bool +wc_isdigit_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswdigit_l((wint_t) wc, locale->lt); +} + +static bool +wc_isalpha_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswalpha_l((wint_t) wc, locale->lt); +} + +static bool +wc_isalnum_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswalnum_l((wint_t) wc, locale->lt); +} + +static bool +wc_isupper_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswupper_l((wint_t) wc, locale->lt); +} + +static bool +wc_islower_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswlower_l((wint_t) wc, locale->lt); +} + +static bool +wc_isgraph_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswgraph_l((wint_t) wc, locale->lt); +} + +static bool +wc_isprint_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswprint_l((wint_t) wc, locale->lt); +} + +static bool +wc_ispunct_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswpunct_l((wint_t) wc, locale->lt); +} + +static bool +wc_isspace_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswspace_l((wint_t) wc, locale->lt); +} + +static bool +wc_isxdigit_libc_mb(pg_wchar wc, pg_locale_t locale) +{ +#ifndef WIN32 + return iswxdigit_l((wint_t) wc, locale->lt); +#else + return _iswxdigit_l((wint_t) wc, locale->lt); +#endif +} + +static bool +wc_iscased_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + return iswupper_l((wint_t) wc, locale->lt) || + iswlower_l((wint_t) wc, locale->lt); +} + +static pg_wchar +toupper_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + Assert(GetDatabaseEncoding() != PG_UTF8); + + /* force C behavior for ASCII characters, per comments above */ + if (locale->is_default && wc <= (pg_wchar) 127) + return pg_ascii_toupper((unsigned char) wc); + if (wc <= (pg_wchar) UCHAR_MAX) + return toupper_l((unsigned char) wc, locale->lt); + else + return wc; +} + +static pg_wchar +toupper_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + Assert(GetDatabaseEncoding() == PG_UTF8); + + /* force C behavior for ASCII characters, per comments above */ + if (locale->is_default && wc <= (pg_wchar) 127) + return pg_ascii_toupper((unsigned char) wc); + if (sizeof(wchar_t) >= 4 || wc <= (pg_wchar) 0xFFFF) + return towupper_l((wint_t) wc, locale->lt); + else + return wc; +} + +static pg_wchar +tolower_libc_sb(pg_wchar wc, pg_locale_t locale) +{ + Assert(GetDatabaseEncoding() != PG_UTF8); + + /* force C behavior for ASCII characters, per comments above */ + if (locale->is_default && wc <= (pg_wchar) 127) + return pg_ascii_tolower((unsigned char) wc); + if (wc <= (pg_wchar) UCHAR_MAX) + return tolower_l((unsigned char) wc, locale->lt); + else + return wc; +} + +static pg_wchar +tolower_libc_mb(pg_wchar wc, pg_locale_t locale) +{ + Assert(GetDatabaseEncoding() == PG_UTF8); + + /* force C behavior for ASCII characters, per comments above */ + if (locale->is_default && wc <= (pg_wchar) 127) + return pg_ascii_tolower((unsigned char) wc); + if (sizeof(wchar_t) >= 4 || wc <= (pg_wchar) 0xFFFF) + return towlower_l((wint_t) wc, locale->lt); + else + return wc; +} + +/* + * Characters A..Z always downcase to a..z, even in the Turkish + * locale. Characters beyond 127 use tolower(). + */ +static size_t +downcase_ident_libc_sb(char *dst, size_t dstsize, const char *src, + ssize_t srclen, pg_locale_t locale) +{ + locale_t loc = locale->lt; + int i; + + for (i = 0; i < srclen && i < dstsize; i++) + { + unsigned char ch = (unsigned char) src[i]; + + if (ch >= 'A' && ch <= 'Z') + ch = pg_ascii_tolower(ch); + else if (IS_HIGHBIT_SET(ch) && isupper_l(ch, loc)) + ch = tolower_l(ch, loc); + dst[i] = (char) ch; + } + + if (i < dstsize) + dst[i] = '\0'; + + return srclen; +} + +static const struct ctype_methods ctype_methods_libc_sb = { + .strlower = strlower_libc_sb, + .strtitle = strtitle_libc_sb, + .strupper = strupper_libc_sb, + /* in libc, casefolding is the same as lowercasing */ + .strfold = strlower_libc_sb, + .downcase_ident = downcase_ident_libc_sb, + .wc_isdigit = wc_isdigit_libc_sb, + .wc_isalpha = wc_isalpha_libc_sb, + .wc_isalnum = wc_isalnum_libc_sb, + .wc_isupper = wc_isupper_libc_sb, + .wc_islower = wc_islower_libc_sb, + .wc_isgraph = wc_isgraph_libc_sb, + .wc_isprint = wc_isprint_libc_sb, + .wc_ispunct = wc_ispunct_libc_sb, + .wc_isspace = wc_isspace_libc_sb, + .wc_isxdigit = wc_isxdigit_libc_sb, + .wc_iscased = wc_iscased_libc_sb, + .wc_toupper = toupper_libc_sb, + .wc_tolower = tolower_libc_sb, +}; + +/* + * Non-UTF8 multibyte encodings use multibyte semantics for case mapping, but + * single-byte semantics for pattern matching. + */ +static const struct ctype_methods ctype_methods_libc_other_mb = { + .strlower = strlower_libc_mb, + .strtitle = strtitle_libc_mb, + .strupper = strupper_libc_mb, + /* in libc, casefolding is the same as lowercasing */ + .strfold = strlower_libc_mb, + /* uses plain ASCII semantics for historical reasons */ + .downcase_ident = NULL, + .wc_isdigit = wc_isdigit_libc_sb, + .wc_isalpha = wc_isalpha_libc_sb, + .wc_isalnum = wc_isalnum_libc_sb, + .wc_isupper = wc_isupper_libc_sb, + .wc_islower = wc_islower_libc_sb, + .wc_isgraph = wc_isgraph_libc_sb, + .wc_isprint = wc_isprint_libc_sb, + .wc_ispunct = wc_ispunct_libc_sb, + .wc_isspace = wc_isspace_libc_sb, + .wc_isxdigit = wc_isxdigit_libc_sb, + .wc_iscased = wc_iscased_libc_sb, + .wc_toupper = toupper_libc_sb, + .wc_tolower = tolower_libc_sb, +}; + +static const struct ctype_methods ctype_methods_libc_utf8 = { + .strlower = strlower_libc_mb, + .strtitle = strtitle_libc_mb, + .strupper = strupper_libc_mb, + /* in libc, casefolding is the same as lowercasing */ + .strfold = strlower_libc_mb, + /* uses plain ASCII semantics for historical reasons */ + .downcase_ident = NULL, + .wc_isdigit = wc_isdigit_libc_mb, + .wc_isalpha = wc_isalpha_libc_mb, + .wc_isalnum = wc_isalnum_libc_mb, + .wc_isupper = wc_isupper_libc_mb, + .wc_islower = wc_islower_libc_mb, + .wc_isgraph = wc_isgraph_libc_mb, + .wc_isprint = wc_isprint_libc_mb, + .wc_ispunct = wc_ispunct_libc_mb, + .wc_isspace = wc_isspace_libc_mb, + .wc_isxdigit = wc_isxdigit_libc_mb, + .wc_iscased = wc_iscased_libc_mb, + .wc_toupper = toupper_libc_mb, + .wc_tolower = tolower_libc_mb, +}; + static const struct collate_methods collate_methods_libc = { .strncoll = strncoll_libc, .strnxfrm = strnxfrm_libc, @@ -119,36 +452,6 @@ static const struct collate_methods collate_methods_libc_win32_utf8 = { }; #endif -size_t -strlower_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale) -{ - if (pg_database_encoding_max_length() > 1) - return strlower_libc_mb(dst, dstsize, src, srclen, locale); - else - return strlower_libc_sb(dst, dstsize, src, srclen, locale); -} - -size_t -strtitle_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale) -{ - if (pg_database_encoding_max_length() > 1) - return strtitle_libc_mb(dst, dstsize, src, srclen, locale); - else - return strtitle_libc_sb(dst, dstsize, src, srclen, locale); -} - -size_t -strupper_libc(char *dst, size_t dstsize, const char *src, - ssize_t srclen, pg_locale_t locale) -{ - if (pg_database_encoding_max_length() > 1) - return strupper_libc_mb(dst, dstsize, src, srclen, locale); - else - return strupper_libc_sb(dst, dstsize, src, srclen, locale); -} - static size_t strlower_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) @@ -158,12 +461,9 @@ strlower_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, if (srclen + 1 <= destsize) { - locale_t loc = locale->info.lt; + locale_t loc = locale->lt; char *p; - if (srclen + 1 > destsize) - return srclen; - memcpy(dest, src, srclen); dest[srclen] = '\0'; @@ -177,7 +477,12 @@ strlower_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, for (p = dest; *p; p++) { if (locale->is_default) - *p = pg_tolower((unsigned char) *p); + { + if (*p >= 'A' && *p <= 'Z') + *p += 'a' - 'A'; + else if (IS_HIGHBIT_SET(*p) && isupper_l(*p, loc)) + *p = tolower_l((unsigned char) *p, loc); + } else *p = tolower_l((unsigned char) *p, loc); } @@ -190,7 +495,7 @@ static size_t strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - locale_t loc = locale->info.lt; + locale_t loc = locale->lt; size_t result_size; wchar_t *workspace; char *result; @@ -207,9 +512,9 @@ strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, errmsg("out of memory"))); /* Output workspace cannot have more codes than input bytes */ - workspace = (wchar_t *) palloc((srclen + 1) * sizeof(wchar_t)); + workspace = palloc_array(wchar_t, srclen + 1); - char2wchar(workspace, srclen + 1, src, srclen, locale); + char2wchar(workspace, srclen + 1, src, srclen, loc); for (curr_char = 0; workspace[curr_char] != 0; curr_char++) workspace[curr_char] = towlower_l(workspace[curr_char], loc); @@ -220,13 +525,13 @@ strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, max_size = curr_char * pg_database_encoding_max_length(); result = palloc(max_size + 1); - result_size = wchar2char(result, workspace, max_size + 1, locale); + result_size = wchar2char(result, workspace, max_size + 1, loc); - if (result_size + 1 > destsize) - return result_size; - - memcpy(dest, result, result_size); - dest[result_size] = '\0'; + if (destsize >= result_size + 1) + { + memcpy(dest, result, result_size); + dest[result_size] = '\0'; + } pfree(workspace); pfree(result); @@ -243,7 +548,7 @@ strtitle_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, if (srclen + 1 <= destsize) { - locale_t loc = locale->info.lt; + locale_t loc = locale->lt; int wasalnum = false; char *p; @@ -262,9 +567,19 @@ strtitle_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, if (locale->is_default) { if (wasalnum) - *p = pg_tolower((unsigned char) *p); + { + if (*p >= 'A' && *p <= 'Z') + *p += 'a' - 'A'; + else if (IS_HIGHBIT_SET(*p) && isupper_l(*p, loc)) + *p = tolower_l((unsigned char) *p, loc); + } else - *p = pg_toupper((unsigned char) *p); + { + if (*p >= 'a' && *p <= 'z') + *p -= 'a' - 'A'; + else if (IS_HIGHBIT_SET(*p) && islower_l(*p, loc)) + *p = toupper_l((unsigned char) *p, loc); + } } else { @@ -284,7 +599,7 @@ static size_t strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - locale_t loc = locale->info.lt; + locale_t loc = locale->lt; int wasalnum = false; size_t result_size; wchar_t *workspace; @@ -302,9 +617,9 @@ strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, errmsg("out of memory"))); /* Output workspace cannot have more codes than input bytes */ - workspace = (wchar_t *) palloc((srclen + 1) * sizeof(wchar_t)); + workspace = palloc_array(wchar_t, srclen + 1); - char2wchar(workspace, srclen + 1, src, srclen, locale); + char2wchar(workspace, srclen + 1, src, srclen, loc); for (curr_char = 0; workspace[curr_char] != 0; curr_char++) { @@ -321,13 +636,13 @@ strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, max_size = curr_char * pg_database_encoding_max_length(); result = palloc(max_size + 1); - result_size = wchar2char(result, workspace, max_size + 1, locale); + result_size = wchar2char(result, workspace, max_size + 1, loc); - if (result_size + 1 > destsize) - return result_size; - - memcpy(dest, result, result_size); - dest[result_size] = '\0'; + if (destsize >= result_size + 1) + { + memcpy(dest, result, result_size); + dest[result_size] = '\0'; + } pfree(workspace); pfree(result); @@ -344,7 +659,7 @@ strupper_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, if (srclen + 1 <= destsize) { - locale_t loc = locale->info.lt; + locale_t loc = locale->lt; char *p; memcpy(dest, src, srclen); @@ -360,7 +675,12 @@ strupper_libc_sb(char *dest, size_t destsize, const char *src, ssize_t srclen, for (p = dest; *p; p++) { if (locale->is_default) - *p = pg_toupper((unsigned char) *p); + { + if (*p >= 'a' && *p <= 'z') + *p -= 'a' - 'A'; + else if (IS_HIGHBIT_SET(*p) && islower_l(*p, loc)) + *p = toupper_l((unsigned char) *p, loc); + } else *p = toupper_l((unsigned char) *p, loc); } @@ -373,7 +693,7 @@ static size_t strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale) { - locale_t loc = locale->info.lt; + locale_t loc = locale->lt; size_t result_size; wchar_t *workspace; char *result; @@ -390,9 +710,9 @@ strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, errmsg("out of memory"))); /* Output workspace cannot have more codes than input bytes */ - workspace = (wchar_t *) palloc((srclen + 1) * sizeof(wchar_t)); + workspace = palloc_array(wchar_t, srclen + 1); - char2wchar(workspace, srclen + 1, src, srclen, locale); + char2wchar(workspace, srclen + 1, src, srclen, loc); for (curr_char = 0; workspace[curr_char] != 0; curr_char++) workspace[curr_char] = towupper_l(workspace[curr_char], loc); @@ -403,13 +723,13 @@ strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen, max_size = curr_char * pg_database_encoding_max_length(); result = palloc(max_size + 1); - result_size = wchar2char(result, workspace, max_size + 1, locale); - - if (result_size + 1 > destsize) - return result_size; + result_size = wchar2char(result, workspace, max_size + 1, loc); - memcpy(dest, result, result_size); - dest[result_size] = '\0'; + if (destsize >= result_size + 1) + { + memcpy(dest, result, result_size); + dest[result_size] = '\0'; + } pfree(workspace); pfree(result); @@ -465,13 +785,12 @@ create_pg_locale_libc(Oid collid, MemoryContext context) loc = make_libc_collator(collate, ctype); result = MemoryContextAllocZero(context, sizeof(struct pg_locale_struct)); - result->provider = COLLPROVIDER_LIBC; result->deterministic = true; result->collate_is_c = (strcmp(collate, "C") == 0) || (strcmp(collate, "POSIX") == 0); result->ctype_is_c = (strcmp(ctype, "C") == 0) || (strcmp(ctype, "POSIX") == 0); - result->info.lt = loc; + result->lt = loc; if (!result->collate_is_c) { #ifdef WIN32 @@ -481,6 +800,15 @@ create_pg_locale_libc(Oid collid, MemoryContext context) #endif result->collate = &collate_methods_libc; } + if (!result->ctype_is_c) + { + if (GetDatabaseEncoding() == PG_UTF8) + result->ctype = &ctype_methods_libc_utf8; + else if (pg_database_encoding_max_length() > 1) + result->ctype = &ctype_methods_libc_other_mb; + else + result->ctype = &ctype_methods_libc_sb; + } return result; } @@ -576,8 +904,6 @@ strncoll_libc(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2, const char *arg2n; int result; - Assert(locale->provider == COLLPROVIDER_LIBC); - if (bufsize1 + bufsize2 > TEXTBUFLEN) buf = palloc(bufsize1 + bufsize2); @@ -608,7 +934,7 @@ strncoll_libc(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2, arg2n = buf2; } - result = strcoll_l(arg1n, arg2n, locale->info.lt); + result = strcoll_l(arg1n, arg2n, locale->lt); if (buf != sbuf) pfree(buf); @@ -632,10 +958,8 @@ strnxfrm_libc(char *dest, size_t destsize, const char *src, ssize_t srclen, size_t bufsize = srclen + 1; size_t result; - Assert(locale->provider == COLLPROVIDER_LIBC); - if (srclen == -1) - return strxfrm_l(dest, src, destsize, locale->info.lt); + return strxfrm_l(dest, src, destsize, locale->lt); if (bufsize > TEXTBUFLEN) buf = palloc(bufsize); @@ -644,7 +968,7 @@ strnxfrm_libc(char *dest, size_t destsize, const char *src, ssize_t srclen, memcpy(buf, src, srclen); buf[srclen] = '\0'; - result = strxfrm_l(dest, buf, destsize, locale->info.lt); + result = strxfrm_l(dest, buf, destsize, locale->lt); if (buf != sbuf) pfree(buf); @@ -742,7 +1066,6 @@ strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, const char *arg2, int r; int result; - Assert(locale->provider == COLLPROVIDER_LIBC); Assert(GetDatabaseEncoding() == PG_UTF8); if (len1 == -1) @@ -787,7 +1110,7 @@ strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, const char *arg2, ((LPWSTR) a2p)[r] = 0; errno = 0; - result = wcscoll_l((LPWSTR) a1p, (LPWSTR) a2p, locale->info.lt); + result = wcscoll_l((LPWSTR) a1p, (LPWSTR) a2p, locale->lt); if (result == 2147483647) /* _NLSCMPERROR; missing from mingw headers */ ereport(ERROR, (errmsg("could not compare Unicode strings: %m"))); @@ -867,7 +1190,7 @@ wcstombs_l(char *dest, const wchar_t *src, size_t n, locale_t loc) #endif /* - * These functions convert from/to libc's wchar_t, *not* pg_wchar_t. + * These functions convert from/to libc's wchar_t, *not* pg_wchar. * Therefore we keep them here rather than with the mbutils code. */ @@ -879,7 +1202,7 @@ wcstombs_l(char *dest, const wchar_t *src, size_t n, locale_t loc) * zero-terminated. The output will be zero-terminated iff there is room. */ size_t -wchar2char(char *to, const wchar_t *from, size_t tolen, pg_locale_t locale) +wchar2char(char *to, const wchar_t *from, size_t tolen, locale_t loc) { size_t result; @@ -909,7 +1232,7 @@ wchar2char(char *to, const wchar_t *from, size_t tolen, pg_locale_t locale) } else #endif /* WIN32 */ - if (locale == (pg_locale_t) 0) + if (loc == (locale_t) 0) { /* Use wcstombs directly for the default locale */ result = wcstombs(to, from, tolen); @@ -917,7 +1240,7 @@ wchar2char(char *to, const wchar_t *from, size_t tolen, pg_locale_t locale) else { /* Use wcstombs_l for nondefault locales */ - result = wcstombs_l(to, from, tolen, locale->info.lt); + result = wcstombs_l(to, from, tolen, loc); } return result; @@ -932,9 +1255,9 @@ wchar2char(char *to, const wchar_t *from, size_t tolen, pg_locale_t locale) * input encoding. tolen is the maximum number of wchar_t's to store at *to. * The output will be zero-terminated iff there is room. */ -size_t +static size_t char2wchar(wchar_t *to, size_t tolen, const char *from, size_t fromlen, - pg_locale_t locale) + locale_t loc) { size_t result; @@ -969,7 +1292,7 @@ char2wchar(wchar_t *to, size_t tolen, const char *from, size_t fromlen, /* mbstowcs requires ending '\0' */ char *str = pnstrdup(from, fromlen); - if (locale == (pg_locale_t) 0) + if (loc == (locale_t) 0) { /* Use mbstowcs directly for the default locale */ result = mbstowcs(to, str, tolen); @@ -977,7 +1300,7 @@ char2wchar(wchar_t *to, size_t tolen, const char *from, size_t fromlen, else { /* Use mbstowcs_l for nondefault locales */ - result = mbstowcs_l(to, str, tolen, locale->info.lt); + result = mbstowcs_l(to, str, tolen, loc); } pfree(str); diff --git a/src/backend/utils/adt/pg_lsn.c b/src/backend/utils/adt/pg_lsn.c index 16311590a14a0..e3480b051ce64 100644 --- a/src/backend/utils/adt/pg_lsn.c +++ b/src/backend/utils/adt/pg_lsn.c @@ -3,7 +3,7 @@ * pg_lsn.c * Operations for the pg_lsn datatype. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -25,8 +25,11 @@ * Formatting and conversion routines. *---------------------------------------------------------*/ +/* + * Internal version of pg_lsn_in() with support for soft error reporting. + */ XLogRecPtr -pg_lsn_in_internal(const char *str, bool *have_error) +pg_lsn_in_safe(const char *str, Node *escontext) { int len1, len2; @@ -34,22 +37,14 @@ pg_lsn_in_internal(const char *str, bool *have_error) off; XLogRecPtr result; - Assert(have_error != NULL); - *have_error = false; - /* Sanity check input format. */ len1 = strspn(str, "0123456789abcdefABCDEF"); if (len1 < 1 || len1 > MAXPG_LSNCOMPONENT || str[len1] != '/') - { - *have_error = true; - return InvalidXLogRecPtr; - } + goto syntax_error; + len2 = strspn(str + len1 + 1, "0123456789abcdefABCDEF"); if (len2 < 1 || len2 > MAXPG_LSNCOMPONENT || str[len1 + 1 + len2] != '\0') - { - *have_error = true; - return InvalidXLogRecPtr; - } + goto syntax_error; /* Decode result. */ id = (uint32) strtoul(str, NULL, 16); @@ -57,6 +52,12 @@ pg_lsn_in_internal(const char *str, bool *have_error) result = ((uint64) id << 32) | off; return result; + +syntax_error: + ereturn(escontext, InvalidXLogRecPtr, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("invalid input syntax for type %s: \"%s\"", + "pg_lsn", str))); } Datum @@ -64,14 +65,8 @@ pg_lsn_in(PG_FUNCTION_ARGS) { char *str = PG_GETARG_CSTRING(0); XLogRecPtr result; - bool have_error = false; - result = pg_lsn_in_internal(str, &have_error); - if (have_error) - ereturn(fcinfo->context, (Datum) 0, - (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), - errmsg("invalid input syntax for type %s: \"%s\"", - "pg_lsn", str))); + result = pg_lsn_in_safe(str, fcinfo->context); PG_RETURN_LSN(result); } @@ -83,7 +78,7 @@ pg_lsn_out(PG_FUNCTION_ARGS) char buf[MAXPG_LSNLEN + 1]; char *result; - snprintf(buf, sizeof buf, "%X/%X", LSN_FORMAT_ARGS(lsn)); + snprintf(buf, sizeof buf, "%X/%08X", LSN_FORMAT_ARGS(lsn)); result = pstrdup(buf); PG_RETURN_CSTRING(result); } diff --git a/src/backend/utils/adt/pg_ndistinct.c b/src/backend/utils/adt/pg_ndistinct.c new file mode 100644 index 0000000000000..8d854012d6ee6 --- /dev/null +++ b/src/backend/utils/adt/pg_ndistinct.c @@ -0,0 +1,851 @@ +/*------------------------------------------------------------------------- + * + * pg_ndistinct.c + * pg_ndistinct data type support. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/utils/adt/pg_ndistinct.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "common/int.h" +#include "common/jsonapi.h" +#include "lib/stringinfo.h" +#include "mb/pg_wchar.h" +#include "nodes/miscnodes.h" +#include "statistics/extended_stats_internal.h" +#include "statistics/statistics_format.h" +#include "utils/builtins.h" +#include "utils/fmgrprotos.h" + +/* Parsing state data */ +typedef enum +{ + NDIST_EXPECT_START = 0, + NDIST_EXPECT_ITEM, + NDIST_EXPECT_KEY, + NDIST_EXPECT_ATTNUM_LIST, + NDIST_EXPECT_ATTNUM, + NDIST_EXPECT_NDISTINCT, + NDIST_EXPECT_COMPLETE, +} NDistinctSemanticState; + +typedef struct +{ + const char *str; + NDistinctSemanticState state; + + List *distinct_items; /* Accumulated complete MVNDistinctItems */ + Node *escontext; + + bool found_attributes; /* Item has "attributes" key */ + bool found_ndistinct; /* Item has "ndistinct" key */ + List *attnum_list; /* Accumulated attribute numbers */ + int32 ndistinct; +} NDistinctParseState; + +/* + * Invoked at the start of each MVNDistinctItem. + * + * The entire JSON document should be one array of MVNDistinctItem objects. + * If we are anywhere else in the document, it is an error. + */ +static JsonParseErrorType +ndistinct_object_start(void *state) +{ + NDistinctParseState *parse = state; + + switch (parse->state) + { + case NDIST_EXPECT_ITEM: + /* Now we expect to see attributes/ndistinct keys */ + parse->state = NDIST_EXPECT_KEY; + return JSON_SUCCESS; + + case NDIST_EXPECT_START: + /* pg_ndistinct must begin with a '[' */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Initial element must be an array.")); + break; + + case NDIST_EXPECT_KEY: + /* In an object, expecting key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("A key was expected.")); + break; + + case NDIST_EXPECT_ATTNUM_LIST: + /* Just followed an "attributes" key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Value of \"%s\" must be an array of attribute numbers.", + PG_NDISTINCT_KEY_ATTRIBUTES)); + break; + + case NDIST_EXPECT_ATTNUM: + /* In an attribute number list, expect only scalar integers */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Attribute lists can only contain attribute numbers.")); + break; + + case NDIST_EXPECT_NDISTINCT: + /* Just followed an "ndistinct" key */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Value of \"%s\" must be an integer.", + PG_NDISTINCT_KEY_NDISTINCT)); + break; + + default: + elog(ERROR, + "object start of \"%s\" found in unexpected parse state: %d.", + "pg_ndistinct", (int) parse->state); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Invoked at the end of an object. + * + * Check to ensure that it was a complete MVNDistinctItem + */ +static JsonParseErrorType +ndistinct_object_end(void *state) +{ + NDistinctParseState *parse = state; + + int natts = 0; + + MVNDistinctItem *item; + + if (parse->state != NDIST_EXPECT_KEY) + elog(ERROR, + "object end of \"%s\" found in unexpected parse state: %d.", + "pg_ndistinct", (int) parse->state); + + if (!parse->found_attributes) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Item must contain \"%s\" key.", + PG_NDISTINCT_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + + if (!parse->found_ndistinct) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Item must contain \"%s\" key.", + PG_NDISTINCT_KEY_NDISTINCT)); + return JSON_SEM_ACTION_FAILED; + } + + /* + * We need at least two attribute numbers for a ndistinct item, anything + * less is malformed. + */ + natts = list_length(parse->attnum_list); + if ((natts < 2) || (natts > STATS_MAX_DIMENSIONS)) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("The \"%s\" key must contain an array of at least %d and no more than %d attributes.", + PG_NDISTINCT_KEY_ATTRIBUTES, 2, STATS_MAX_DIMENSIONS)); + return JSON_SEM_ACTION_FAILED; + } + + /* Create the MVNDistinctItem */ + item = palloc_object(MVNDistinctItem); + item->nattributes = natts; + item->attributes = palloc0(natts * sizeof(AttrNumber)); + item->ndistinct = (double) parse->ndistinct; + + for (int i = 0; i < natts; i++) + item->attributes[i] = (AttrNumber) list_nth_int(parse->attnum_list, i); + + parse->distinct_items = lappend(parse->distinct_items, (void *) item); + + /* reset item state vars */ + list_free(parse->attnum_list); + parse->attnum_list = NIL; + parse->ndistinct = 0; + parse->found_attributes = false; + parse->found_ndistinct = false; + + /* Now we are looking for the next MVNDistinctItem */ + parse->state = NDIST_EXPECT_ITEM; + return JSON_SUCCESS; +} + + +/* + * Invoked at the start of an array. + * + * ndistinct input format has two types of arrays, the outer MVNDistinctItem + * array and the attribute number array within each MVNDistinctItem. + */ +static JsonParseErrorType +ndistinct_array_start(void *state) +{ + NDistinctParseState *parse = state; + + switch (parse->state) + { + case NDIST_EXPECT_ATTNUM_LIST: + parse->state = NDIST_EXPECT_ATTNUM; + break; + + case NDIST_EXPECT_START: + parse->state = NDIST_EXPECT_ITEM; + break; + + default: + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Array has been found at an unexpected location.")); + return JSON_SEM_ACTION_FAILED; + } + + return JSON_SUCCESS; +} + + +/* + * Invoked at the end of an array. + * + * Arrays can never be empty. + */ +static JsonParseErrorType +ndistinct_array_end(void *state) +{ + NDistinctParseState *parse = state; + + switch (parse->state) + { + case NDIST_EXPECT_ATTNUM: + if (list_length(parse->attnum_list) > 0) + { + /* + * The attribute number list is complete, look for more + * MVNDistinctItem keys. + */ + parse->state = NDIST_EXPECT_KEY; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("The \"%s\" key must be a non-empty array.", + PG_NDISTINCT_KEY_ATTRIBUTES)); + break; + + case NDIST_EXPECT_ITEM: + if (list_length(parse->distinct_items) > 0) + { + /* Item list is complete, we are done. */ + parse->state = NDIST_EXPECT_COMPLETE; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Item array cannot be empty.")); + break; + + default: + + /* + * This can only happen if a case was missed in + * ndistinct_array_start(). + */ + elog(ERROR, + "array end of \"%s\" found in unexpected parse state: %d.", + "pg_ndistinct", (int) parse->state); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Invoked at the start of a key/value field. + * + * The valid keys for the MVNDistinctItem object are: + * - attributes + * - ndistinct + */ +static JsonParseErrorType +ndistinct_object_field_start(void *state, char *fname, bool isnull) +{ + NDistinctParseState *parse = state; + + if (strcmp(fname, PG_NDISTINCT_KEY_ATTRIBUTES) == 0) + { + if (parse->found_attributes) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Multiple \"%s\" keys are not allowed.", + PG_NDISTINCT_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + parse->found_attributes = true; + parse->state = NDIST_EXPECT_ATTNUM_LIST; + return JSON_SUCCESS; + } + + if (strcmp(fname, PG_NDISTINCT_KEY_NDISTINCT) == 0) + { + if (parse->found_ndistinct) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Multiple \"%s\" keys are not allowed.", + PG_NDISTINCT_KEY_NDISTINCT)); + return JSON_SEM_ACTION_FAILED; + } + parse->found_ndistinct = true; + parse->state = NDIST_EXPECT_NDISTINCT; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Only allowed keys are \"%s\" and \"%s\".", + PG_NDISTINCT_KEY_ATTRIBUTES, + PG_NDISTINCT_KEY_NDISTINCT)); + return JSON_SEM_ACTION_FAILED; +} + +/* + * Invoked at the start of an array element. + * + * The overall structure of the datatype is an array, but there are also + * arrays as the value of every attributes key. + */ +static JsonParseErrorType +ndistinct_array_element_start(void *state, bool isnull) +{ + const NDistinctParseState *parse = state; + + switch (parse->state) + { + case NDIST_EXPECT_ATTNUM: + if (!isnull) + return JSON_SUCCESS; + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Attribute number array cannot be null.")); + break; + + case NDIST_EXPECT_ITEM: + if (!isnull) + return JSON_SUCCESS; + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Item list elements cannot be null.")); + + break; + + default: + elog(ERROR, + "array element start of \"%s\" found in unexpected parse state: %d.", + "pg_ndistinct", (int) parse->state); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Test for valid subsequent attribute number. + * + * If the previous value is positive, then current value must either be + * greater than the previous value, or negative. + * + * If the previous value is negative, then the value must be less than + * the previous value. + * + * Duplicate values are obviously not allowed, but that is already covered + * by the rules listed above. + */ +static bool +valid_subsequent_attnum(AttrNumber prev, AttrNumber cur) +{ + Assert(prev != 0); + + if (prev > 0) + return ((cur > prev) || (cur < 0)); + + return (cur < prev); +} + +/* + * Handle scalar events from the ndistinct input parser. + * + * Override integer parse error messages and replace them with errors + * specific to the context. + */ +static JsonParseErrorType +ndistinct_scalar(void *state, char *token, JsonTokenType tokentype) +{ + NDistinctParseState *parse = state; + AttrNumber attnum; + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + switch (parse->state) + { + case NDIST_EXPECT_ATTNUM: + attnum = pg_strtoint16_safe(token, (Node *) &escontext); + + if (escontext.error_occurred) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Key \"%s\" has an incorrect value.", PG_NDISTINCT_KEY_ATTRIBUTES)); + return JSON_SEM_ACTION_FAILED; + } + + /* + * The attribute number cannot be zero a negative number beyond + * the number of the possible expressions. + */ + if (attnum == 0 || attnum < (0 - STATS_MAX_DIMENSIONS)) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Invalid \"%s\" element has been found: %d.", + PG_NDISTINCT_KEY_ATTRIBUTES, attnum)); + return JSON_SEM_ACTION_FAILED; + } + + if (list_length(parse->attnum_list) > 0) + { + const AttrNumber prev = llast_int(parse->attnum_list); + + if (!valid_subsequent_attnum(prev, attnum)) + { + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Invalid \"%s\" element has been found: %d cannot follow %d.", + PG_NDISTINCT_KEY_ATTRIBUTES, attnum, prev)); + return JSON_SEM_ACTION_FAILED; + } + } + + parse->attnum_list = lappend_int(parse->attnum_list, (int) attnum); + return JSON_SUCCESS; + + case NDIST_EXPECT_NDISTINCT: + + /* + * While the structure dictates that ndistinct is a double + * precision floating point, it has always been an integer in the + * output generated. Therefore, we parse it as an integer here. + */ + parse->ndistinct = pg_strtoint32_safe(token, (Node *) &escontext); + + if (!escontext.error_occurred) + { + parse->state = NDIST_EXPECT_KEY; + return JSON_SUCCESS; + } + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Key \"%s\" has an incorrect value.", + PG_NDISTINCT_KEY_NDISTINCT)); + break; + + default: + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", parse->str), + errdetail("Unexpected scalar has been found.")); + break; + } + + return JSON_SEM_ACTION_FAILED; +} + +/* + * Compare the attribute arrays of two MVNDistinctItem values, + * looking for duplicate sets. Return true if a duplicate set is found. + * + * The arrays are required to be in canonical order (all positive numbers + * in ascending order first, followed by all negative numbers in descending + * order) so it's safe to compare the attrnums in order, stopping at the + * first difference. + */ +static bool +item_attributes_eq(const MVNDistinctItem *a, const MVNDistinctItem *b) +{ + if (a->nattributes != b->nattributes) + return false; + + for (int i = 0; i < a->nattributes; i++) + { + if (a->attributes[i] != b->attributes[i]) + return false; + } + + return true; +} + +/* + * Ensure that an attribute number appears as one of the attribute numbers + * in a MVNDistinctItem. + */ +static bool +item_has_attnum(const MVNDistinctItem *item, AttrNumber attnum) +{ + for (int i = 0; i < item->nattributes; i++) + { + if (attnum == item->attributes[i]) + return true; + } + return false; +} + +/* + * Ensure that the attributes in MVNDistinctItem A are a subset of the + * reference MVNDistinctItem B. + */ +static bool +item_is_attnum_subset(const MVNDistinctItem *item, + const MVNDistinctItem *refitem) +{ + for (int i = 0; i < item->nattributes; i++) + { + if (!item_has_attnum(refitem, item->attributes[i])) + return false; + } + return true; +} + +/* + * Generate a string representing an array of attribute numbers. + * + * Freeing the allocated string is the responsibility of the caller. + */ +static char * +item_attnum_list(const MVNDistinctItem *item) +{ + StringInfoData str; + + initStringInfo(&str); + + appendStringInfo(&str, "%d", item->attributes[0]); + + for (int i = 1; i < item->nattributes; i++) + appendStringInfo(&str, ", %d", item->attributes[i]); + + return str.data; +} + +/* + * Attempt to build and serialize the MVNDistinct object. + * + * This can only be executed after the completion of the JSON parsing. + * + * In the event of an error, set the error context and return NULL. + */ +static bytea * +build_mvndistinct(NDistinctParseState *parse, char *str) +{ + MVNDistinct *ndistinct; + int nitems = list_length(parse->distinct_items); + bytea *bytes; + int item_most_attrs = 0; + int item_most_attrs_idx = 0; + + switch (parse->state) + { + case NDIST_EXPECT_COMPLETE: + + /* + * Parsing has ended correctly and we should have a list of items. + * If we don't, something has been done wrong in one of the + * earlier parsing steps. + */ + if (nitems == 0) + elog(ERROR, + "cannot have empty item list after parsing success."); + break; + + case NDIST_EXPECT_START: + /* blank */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", str), + errdetail("Value cannot be empty.")); + return NULL; + + default: + /* Unexpected end-state. */ + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", str), + errdetail("Unexpected end state has been found: %d.", parse->state)); + return NULL; + } + + ndistinct = palloc(offsetof(MVNDistinct, items) + + nitems * sizeof(MVNDistinctItem)); + + ndistinct->magic = STATS_NDISTINCT_MAGIC; + ndistinct->type = STATS_NDISTINCT_TYPE_BASIC; + ndistinct->nitems = nitems; + + for (int i = 0; i < nitems; i++) + { + MVNDistinctItem *item = list_nth(parse->distinct_items, i); + + /* + * Ensure that this item does not duplicate the attributes of any + * pre-existing item. + */ + for (int j = 0; j < i; j++) + { + if (item_attributes_eq(item, &ndistinct->items[j])) + { + char *s = item_attnum_list(item); + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", str), + errdetail("Duplicated \"%s\" array has been found: [%s].", + PG_NDISTINCT_KEY_ATTRIBUTES, s)); + pfree(s); + return NULL; + } + } + + ndistinct->items[i].ndistinct = item->ndistinct; + ndistinct->items[i].nattributes = item->nattributes; + + /* + * This transfers free-ing responsibility from the distinct_items list + * to the ndistinct object. + */ + ndistinct->items[i].attributes = item->attributes; + + /* + * Keep track of the first longest attribute list. All other attribute + * lists must be a subset of this list. + */ + if (item->nattributes > item_most_attrs) + { + item_most_attrs = item->nattributes; + item_most_attrs_idx = i; + } + } + + /* + * Verify that all the sets of attribute numbers are a proper subset of + * the longest set recorded. This acts as an extra sanity check based on + * the input given. Note that this still needs to be cross-checked with + * the extended statistics objects this would be assigned to, but it + * provides one extra layer of protection. + */ + for (int i = 0; i < nitems; i++) + { + if (i == item_most_attrs_idx) + continue; + + if (!item_is_attnum_subset(&ndistinct->items[i], + &ndistinct->items[item_most_attrs_idx])) + { + const MVNDistinctItem *item = &ndistinct->items[i]; + const MVNDistinctItem *refitem = &ndistinct->items[item_most_attrs_idx]; + char *item_list = item_attnum_list(item); + char *refitem_list = item_attnum_list(refitem); + + errsave(parse->escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", str), + errdetail("\"%s\" array [%s] must be a subset of array [%s].", + PG_NDISTINCT_KEY_ATTRIBUTES, + item_list, refitem_list)); + pfree(item_list); + pfree(refitem_list); + return NULL; + } + } + + bytes = statext_ndistinct_serialize(ndistinct); + + /* + * Free the attribute lists, before the ndistinct itself. + */ + for (int i = 0; i < nitems; i++) + pfree(ndistinct->items[i].attributes); + pfree(ndistinct); + + return bytes; +} + +/* + * pg_ndistinct_in + * input routine for type pg_ndistinct. + */ +Datum +pg_ndistinct_in(PG_FUNCTION_ARGS) +{ + char *str = PG_GETARG_CSTRING(0); + NDistinctParseState parse_state; + JsonParseErrorType result; + JsonLexContext *lex; + JsonSemAction sem_action; + bytea *bytes = NULL; + + /* initialize semantic state */ + parse_state.str = str; + parse_state.state = NDIST_EXPECT_START; + parse_state.distinct_items = NIL; + parse_state.escontext = fcinfo->context; + parse_state.found_attributes = false; + parse_state.found_ndistinct = false; + parse_state.attnum_list = NIL; + parse_state.ndistinct = 0; + + /* set callbacks */ + sem_action.semstate = (void *) &parse_state; + sem_action.object_start = ndistinct_object_start; + sem_action.object_end = ndistinct_object_end; + sem_action.array_start = ndistinct_array_start; + sem_action.array_end = ndistinct_array_end; + sem_action.object_field_start = ndistinct_object_field_start; + sem_action.object_field_end = NULL; + sem_action.array_element_start = ndistinct_array_element_start; + sem_action.array_element_end = NULL; + sem_action.scalar = ndistinct_scalar; + + lex = makeJsonLexContextCstringLen(NULL, str, strlen(str), + PG_UTF8, true); + result = pg_parse_json(lex, &sem_action); + freeJsonLexContext(lex); + + if (result == JSON_SUCCESS) + bytes = build_mvndistinct(&parse_state, str); + + list_free(parse_state.attnum_list); + list_free_deep(parse_state.distinct_items); + + if (bytes) + PG_RETURN_BYTEA_P(bytes); + + /* + * If escontext already set, just use that. Anything else is a generic + * JSON parse error. + */ + if (!SOFT_ERROR_OCCURRED(parse_state.escontext)) + errsave(parse_state.escontext, + errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("malformed pg_ndistinct: \"%s\"", str), + errdetail("Input data must be valid JSON.")); + + PG_RETURN_NULL(); +} + +/* + * pg_ndistinct_out + * output routine for type pg_ndistinct + * + * Produces a human-readable representation of the value. + */ +Datum +pg_ndistinct_out(PG_FUNCTION_ARGS) +{ + bytea *data = PG_GETARG_BYTEA_PP(0); + MVNDistinct *ndist = statext_ndistinct_deserialize(data); + int i; + StringInfoData str; + + initStringInfo(&str); + appendStringInfoChar(&str, '['); + + for (i = 0; i < ndist->nitems; i++) + { + MVNDistinctItem item = ndist->items[i]; + + if (i > 0) + appendStringInfoString(&str, ", "); + + if (item.nattributes <= 0) + elog(ERROR, "invalid zero-length attribute array in MVNDistinct"); + + appendStringInfo(&str, "{\"" PG_NDISTINCT_KEY_ATTRIBUTES "\": [%d", + item.attributes[0]); + + for (int j = 1; j < item.nattributes; j++) + appendStringInfo(&str, ", %d", item.attributes[j]); + + appendStringInfo(&str, "], \"" PG_NDISTINCT_KEY_NDISTINCT "\": %d}", + (int) item.ndistinct); + } + + appendStringInfoChar(&str, ']'); + + PG_RETURN_CSTRING(str.data); +} + +/* + * pg_ndistinct_recv + * binary input routine for type pg_ndistinct + */ +Datum +pg_ndistinct_recv(PG_FUNCTION_ARGS) +{ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot accept a value of type %s", "pg_ndistinct"))); + + PG_RETURN_VOID(); /* keep compiler quiet */ +} + +/* + * pg_ndistinct_send + * binary output routine for type pg_ndistinct + * + * n-distinct is serialized into a bytea value, so let's send that. + */ +Datum +pg_ndistinct_send(PG_FUNCTION_ARGS) +{ + return byteasend(fcinfo); +} diff --git a/src/backend/utils/adt/pg_upgrade_support.c b/src/backend/utils/adt/pg_upgrade_support.c index d44f8c262baa2..b505a6b4feeb8 100644 --- a/src/backend/utils/adt/pg_upgrade_support.c +++ b/src/backend/utils/adt/pg_upgrade_support.c @@ -5,7 +5,7 @@ * to control oid and relfilenumber assignment, and do other special * hacks needed for pg_upgrade. * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/backend/utils/adt/pg_upgrade_support.c */ @@ -21,6 +21,7 @@ #include "commands/extension.h" #include "miscadmin.h" #include "replication/logical.h" +#include "replication/logicallauncher.h" #include "replication/origin.h" #include "replication/worker_internal.h" #include "storage/lmgr.h" @@ -281,11 +282,12 @@ binary_upgrade_set_missing_value(PG_FUNCTION_ARGS) * upgraded without data loss. */ Datum -binary_upgrade_logical_slot_has_caught_up(PG_FUNCTION_ARGS) +binary_upgrade_check_logical_slot_pending_wal(PG_FUNCTION_ARGS) { Name slot_name; XLogRecPtr end_of_wal; - bool found_pending_wal; + XLogRecPtr scan_cutoff_lsn; + XLogRecPtr last_pending_wal; CHECK_IS_BINARY_UPGRADE; @@ -296,6 +298,7 @@ binary_upgrade_logical_slot_has_caught_up(PG_FUNCTION_ARGS) Assert(has_rolreplication(GetUserId())); slot_name = PG_GETARG_NAME(0); + scan_cutoff_lsn = PG_GETARG_LSN(1); /* Acquire the given slot */ ReplicationSlotAcquire(NameStr(*slot_name), true, true); @@ -306,12 +309,16 @@ binary_upgrade_logical_slot_has_caught_up(PG_FUNCTION_ARGS) Assert(MyReplicationSlot->data.invalidated == RS_INVAL_NONE); end_of_wal = GetFlushRecPtr(NULL); - found_pending_wal = LogicalReplicationSlotHasPendingWal(end_of_wal); + last_pending_wal = LogicalReplicationSlotCheckPendingWal(end_of_wal, + scan_cutoff_lsn); /* Clean up */ ReplicationSlotRelease(); - PG_RETURN_BOOL(!found_pending_wal); + if (XLogRecPtrIsValid(last_pending_wal)) + PG_RETURN_LSN(last_pending_wal); + else + PG_RETURN_NULL(); } /* @@ -371,7 +378,7 @@ binary_upgrade_replorigin_advance(PG_FUNCTION_ARGS) Oid subid; char *subname; char originname[NAMEDATALEN]; - RepOriginId node; + ReplOriginId node; XLogRecPtr remote_commit; CHECK_IS_BINARY_UPGRADE; @@ -410,3 +417,21 @@ binary_upgrade_replorigin_advance(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + +/* + * binary_upgrade_create_conflict_detection_slot + * + * Create a replication slot to retain information necessary for conflict + * detection such as dead tuples, commit timestamps, and origins. + */ +Datum +binary_upgrade_create_conflict_detection_slot(PG_FUNCTION_ARGS) +{ + CHECK_IS_BINARY_UPGRADE; + + CreateConflictDetectionSlot(); + + ReplicationSlotRelease(); + + PG_RETURN_VOID(); +} diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 97af7c6554ff3..1408de387ea6f 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -3,7 +3,7 @@ * pgstatfuncs.c * Functions for accessing various forms of statistics data * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -31,6 +31,8 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" +#include "utils/wait_event.h" #define UINT32_ACCESS_ONCE(var) ((uint32)(*((volatile uint32 *)&(var)))) @@ -168,6 +170,9 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time) /* pg_stat_get_lastscan */ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan) +/* pg_stat_get_stat_reset_time */ +PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time) + Datum pg_stat_get_function_calls(PG_FUNCTION_ARGS) { @@ -200,6 +205,24 @@ PG_STAT_GET_FUNCENTRY_FLOAT8_MS(total_time) /* pg_stat_get_function_self_time */ PG_STAT_GET_FUNCENTRY_FLOAT8_MS(self_time) +Datum +pg_stat_get_function_stat_reset_time(PG_FUNCTION_ARGS) +{ + Oid funcid = PG_GETARG_OID(0); + TimestampTz result; + PgStat_StatFuncEntry *funcentry; + + if ((funcentry = pgstat_fetch_stat_funcentry(funcid)) == NULL) + result = 0; + else + result = funcentry->stat_reset_timestamp; + + if (result == 0) + PG_RETURN_NULL(); + else + PG_RETURN_TIMESTAMPTZ(result); +} + Datum pg_stat_get_backend_idset(PG_FUNCTION_ARGS) { @@ -266,14 +289,16 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS) cmdtype = PROGRESS_COMMAND_VACUUM; else if (pg_strcasecmp(cmd, "ANALYZE") == 0) cmdtype = PROGRESS_COMMAND_ANALYZE; - else if (pg_strcasecmp(cmd, "CLUSTER") == 0) - cmdtype = PROGRESS_COMMAND_CLUSTER; + else if (pg_strcasecmp(cmd, "REPACK") == 0) + cmdtype = PROGRESS_COMMAND_REPACK; else if (pg_strcasecmp(cmd, "CREATE INDEX") == 0) cmdtype = PROGRESS_COMMAND_CREATE_INDEX; else if (pg_strcasecmp(cmd, "BASEBACKUP") == 0) cmdtype = PROGRESS_COMMAND_BASEBACKUP; else if (pg_strcasecmp(cmd, "COPY") == 0) cmdtype = PROGRESS_COMMAND_COPY; + else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0) + cmdtype = PROGRESS_COMMAND_DATACHECKSUMS; else ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -640,10 +665,10 @@ pg_stat_get_activity(PG_FUNCTION_ARGS) values[28] = BoolGetDatum(false); /* GSS credentials not * delegated */ } - if (beentry->st_query_id == 0) + if (beentry->st_query_id == INT64CONST(0)) nulls[30] = true; else - values[30] = UInt64GetDatum(beentry->st_query_id); + values[30] = Int64GetDatum(beentry->st_query_id); } else { @@ -748,6 +773,7 @@ pg_stat_get_backend_subxact(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 2, "subxact_overflow", BOOLOID, -1, 0); + TupleDescFinalize(tupdesc); BlessTupleDesc(tupdesc); if ((local_beentry = pgstat_get_local_beentry_by_proc_number(procNumber)) != NULL) @@ -785,7 +811,7 @@ pg_stat_get_backend_activity(PG_FUNCTION_ARGS) activity = beentry->st_activity_raw; clipped_activity = pgstat_clip_activity(activity); - ret = cstring_to_text(activity); + ret = cstring_to_text(clipped_activity); pfree(clipped_activity); PG_RETURN_TEXT_P(ret); @@ -803,8 +829,14 @@ pg_stat_get_backend_wait_event_type(PG_FUNCTION_ARGS) wait_event_type = ""; else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid)) wait_event_type = ""; - else if ((proc = BackendPidGetProc(beentry->st_procpid)) != NULL) - wait_event_type = pgstat_get_wait_event_type(proc->wait_event_info); + else + { + proc = BackendPidGetProc(beentry->st_procpid); + if (!proc) + proc = AuxiliaryPidGetProc(beentry->st_procpid); + if (proc) + wait_event_type = pgstat_get_wait_event_type(proc->wait_event_info); + } if (!wait_event_type) PG_RETURN_NULL(); @@ -824,8 +856,14 @@ pg_stat_get_backend_wait_event(PG_FUNCTION_ARGS) wait_event = ""; else if (!HAS_PGSTAT_PERMISSIONS(beentry->st_userid)) wait_event = ""; - else if ((proc = BackendPidGetProc(beentry->st_procpid)) != NULL) - wait_event = pgstat_get_wait_event(proc->wait_event_info); + else + { + proc = BackendPidGetProc(beentry->st_procpid); + if (!proc) + proc = AuxiliaryPidGetProc(beentry->st_procpid); + if (proc) + wait_event = pgstat_get_wait_event(proc->wait_event_info); + } if (!wait_event) PG_RETURN_NULL(); @@ -1146,9 +1184,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS) int64 result; PgStat_StatDBEntry *dbentry; - if (!DataChecksumsEnabled()) - PG_RETURN_NULL(); - if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL) result = 0; else @@ -1164,9 +1199,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS) TimestampTz result; PgStat_StatDBEntry *dbentry; - if (!DataChecksumsEnabled()) - PG_RETURN_NULL(); - if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL) result = 0; else @@ -1510,7 +1542,7 @@ pg_stat_io_build_tuples(ReturnSetInfo *rsinfo, bktype_stats->bytes[io_obj][io_context][io_op]; /* Convert to numeric */ - snprintf(buf, sizeof buf, UINT64_FORMAT, byte); + snprintf(buf, sizeof buf, INT64_FORMAT, byte); values[byte_idx] = DirectFunctionCall3(numeric_in, CStringGetDatum(buf), ObjectIdGetDatum(0), @@ -1616,7 +1648,7 @@ static Datum pg_stat_wal_build_tuple(PgStat_WalCounters wal_counters, TimestampTz stat_reset_timestamp) { -#define PG_STAT_WAL_COLS 5 +#define PG_STAT_WAL_COLS 6 TupleDesc tupdesc; Datum values[PG_STAT_WAL_COLS] = {0}; bool nulls[PG_STAT_WAL_COLS] = {0}; @@ -1630,11 +1662,14 @@ pg_stat_wal_build_tuple(PgStat_WalCounters wal_counters, INT8OID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 3, "wal_bytes", NUMERICOID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 4, "wal_buffers_full", + TupleDescInitEntry(tupdesc, (AttrNumber) 4, "wal_fpi_bytes", + NUMERICOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 5, "wal_buffers_full", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 5, "stats_reset", + TupleDescInitEntry(tupdesc, (AttrNumber) 6, "stats_reset", TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); BlessTupleDesc(tupdesc); /* Fill values and NULLs */ @@ -1648,12 +1683,18 @@ pg_stat_wal_build_tuple(PgStat_WalCounters wal_counters, ObjectIdGetDatum(0), Int32GetDatum(-1)); - values[3] = Int64GetDatum(wal_counters.wal_buffers_full); + snprintf(buf, sizeof buf, UINT64_FORMAT, wal_counters.wal_fpi_bytes); + values[3] = DirectFunctionCall3(numeric_in, + CStringGetDatum(buf), + ObjectIdGetDatum(0), + Int32GetDatum(-1)); + + values[4] = Int64GetDatum(wal_counters.wal_buffers_full); if (stat_reset_timestamp != 0) - values[4] = TimestampTzGetDatum(stat_reset_timestamp); + values[5] = TimestampTzGetDatum(stat_reset_timestamp); else - nulls[4] = true; + nulls[5] = true; /* Returns the record as Datum */ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); @@ -1696,6 +1737,42 @@ pg_stat_get_wal(PG_FUNCTION_ARGS) wal_stats->stat_reset_timestamp)); } +Datum +pg_stat_get_lock(PG_FUNCTION_ARGS) +{ +#define PG_STAT_LOCK_COLS 5 + ReturnSetInfo *rsinfo; + PgStat_Lock *lock_stats; + + InitMaterializedSRF(fcinfo, 0); + rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + + lock_stats = pgstat_fetch_stat_lock(); + + for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++) + { + const char *locktypename; + Datum values[PG_STAT_LOCK_COLS] = {0}; + bool nulls[PG_STAT_LOCK_COLS] = {0}; + PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype]; + int i = 0; + + locktypename = LockTagTypeNames[lcktype]; + + values[i++] = CStringGetTextDatum(locktypename); + values[i++] = Int64GetDatum(lck_stats->waits); + values[i++] = Int64GetDatum(lck_stats->wait_time); + values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded); + values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp); + + Assert(i + 1 == PG_STAT_LOCK_COLS); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + + return (Datum) 0; +} + /* * Returns statistics of SLRU caches. */ @@ -1880,6 +1957,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) pgstat_reset_of_kind(PGSTAT_KIND_BGWRITER); pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER); pgstat_reset_of_kind(PGSTAT_KIND_IO); + pgstat_reset_of_kind(PGSTAT_KIND_LOCK); XLogPrefetchResetStats(); pgstat_reset_of_kind(PGSTAT_KIND_SLRU); pgstat_reset_of_kind(PGSTAT_KIND_WAL); @@ -1897,6 +1975,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER); else if (strcmp(target, "io") == 0) pgstat_reset_of_kind(PGSTAT_KIND_IO); + else if (strcmp(target, "lock") == 0) + pgstat_reset_of_kind(PGSTAT_KIND_LOCK); else if (strcmp(target, "recovery_prefetch") == 0) XLogPrefetchResetStats(); else if (strcmp(target, "slru") == 0) @@ -1913,7 +1993,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS) } /* - * Reset a statistics for a single object, which may be of current + * Reset statistics for a single object, which may be of current * database or shared across all databases in the cluster. */ Datum @@ -2056,6 +2136,7 @@ pg_stat_get_archiver(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 7, "stats_reset", TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); BlessTupleDesc(tupdesc); /* Get statistics about the archiver process */ @@ -2100,7 +2181,7 @@ pg_stat_get_archiver(PG_FUNCTION_ARGS) Datum pg_stat_get_replication_slot(PG_FUNCTION_ARGS) { -#define PG_STAT_GET_REPLICATION_SLOT_COLS 10 +#define PG_STAT_GET_REPLICATION_SLOT_COLS 13 text *slotname_text = PG_GETARG_TEXT_P(0); NameData slotname; TupleDesc tupdesc; @@ -2125,12 +2206,19 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS) INT8OID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 7, "stream_bytes", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 8, "total_txns", + TupleDescInitEntry(tupdesc, (AttrNumber) 8, "mem_exceeded_count", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 9, "total_txns", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 9, "total_bytes", + TupleDescInitEntry(tupdesc, (AttrNumber) 10, "total_bytes", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 10, "stats_reset", + TupleDescInitEntry(tupdesc, (AttrNumber) 11, "slotsync_skip_count", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 12, "slotsync_last_skip", + TIMESTAMPTZOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 13, "stats_reset", TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); BlessTupleDesc(tupdesc); namestrcpy(&slotname, text_to_cstring(slotname_text)); @@ -2152,13 +2240,20 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS) values[4] = Int64GetDatum(slotent->stream_txns); values[5] = Int64GetDatum(slotent->stream_count); values[6] = Int64GetDatum(slotent->stream_bytes); - values[7] = Int64GetDatum(slotent->total_txns); - values[8] = Int64GetDatum(slotent->total_bytes); + values[7] = Int64GetDatum(slotent->mem_exceeded_count); + values[8] = Int64GetDatum(slotent->total_txns); + values[9] = Int64GetDatum(slotent->total_bytes); + values[10] = Int64GetDatum(slotent->slotsync_skip_count); + + if (slotent->slotsync_last_skip == 0) + nulls[11] = true; + else + values[11] = TimestampTzGetDatum(slotent->slotsync_last_skip); if (slotent->stat_reset_timestamp == 0) - nulls[9] = true; + nulls[12] = true; else - values[9] = TimestampTzGetDatum(slotent->stat_reset_timestamp); + values[12] = TimestampTzGetDatum(slotent->stat_reset_timestamp); /* Returns the record as Datum */ PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); @@ -2171,7 +2266,7 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS) Datum pg_stat_get_subscription_stats(PG_FUNCTION_ARGS) { -#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS 11 +#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS 13 Oid subid = PG_GETARG_OID(0); TupleDesc tupdesc; Datum values[PG_STAT_GET_SUBSCRIPTION_STATS_COLS] = {0}; @@ -2189,24 +2284,29 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS) OIDOID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 2, "apply_error_count", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 3, "sync_error_count", + TupleDescInitEntry(tupdesc, (AttrNumber) 3, "sync_seq_error_count", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 4, "confl_insert_exists", + TupleDescInitEntry(tupdesc, (AttrNumber) 4, "sync_table_error_count", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 5, "confl_update_origin_differs", + TupleDescInitEntry(tupdesc, (AttrNumber) 5, "confl_insert_exists", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 6, "confl_update_exists", + TupleDescInitEntry(tupdesc, (AttrNumber) 6, "confl_update_origin_differs", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 7, "confl_update_missing", + TupleDescInitEntry(tupdesc, (AttrNumber) 7, "confl_update_exists", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 8, "confl_delete_origin_differs", + TupleDescInitEntry(tupdesc, (AttrNumber) 8, "confl_update_deleted", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 9, "confl_delete_missing", + TupleDescInitEntry(tupdesc, (AttrNumber) 9, "confl_update_missing", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 10, "confl_multiple_unique_conflicts", + TupleDescInitEntry(tupdesc, (AttrNumber) 10, "confl_delete_origin_differs", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 11, "stats_reset", + TupleDescInitEntry(tupdesc, (AttrNumber) 11, "confl_delete_missing", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 12, "confl_multiple_unique_conflicts", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 13, "stats_reset", TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); BlessTupleDesc(tupdesc); if (!subentry) @@ -2222,8 +2322,11 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS) /* apply_error_count */ values[i++] = Int64GetDatum(subentry->apply_error_count); - /* sync_error_count */ - values[i++] = Int64GetDatum(subentry->sync_error_count); + /* sync_seq_error_count */ + values[i++] = Int64GetDatum(subentry->sync_seq_error_count); + + /* sync_table_error_count */ + values[i++] = Int64GetDatum(subentry->sync_table_error_count); /* conflict count */ for (int nconflict = 0; nconflict < CONFLICT_NUM_TYPES; nconflict++) diff --git a/src/backend/utils/adt/pseudorandomfuncs.c b/src/backend/utils/adt/pseudorandomfuncs.c index e7b8045f92508..63ade749a2786 100644 --- a/src/backend/utils/adt/pseudorandomfuncs.c +++ b/src/backend/utils/adt/pseudorandomfuncs.c @@ -3,7 +3,7 @@ * pseudorandomfuncs.c * Functions giving SQL access to a pseudorandom number generator. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -17,6 +17,7 @@ #include "common/pg_prng.h" #include "miscadmin.h" +#include "utils/date.h" #include "utils/fmgrprotos.h" #include "utils/numeric.h" #include "utils/timestamp.h" @@ -25,6 +26,18 @@ static pg_prng_state prng_state; static bool prng_seed_set = false; +/* + * Macro for checking the range bounds of random(min, max) functions. Throws + * an error if they're the wrong way round. + */ +#define CHECK_RANGE_BOUNDS(rmin, rmax) \ + do { \ + if ((rmin) > (rmax)) \ + ereport(ERROR, \ + errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ + errmsg("lower bound must be less than or equal to upper bound")); \ + } while (0) + /* * initialize_prng() - * @@ -129,10 +142,7 @@ int4random(PG_FUNCTION_ARGS) int32 rmax = PG_GETARG_INT32(1); int32 result; - if (rmin > rmax) - ereport(ERROR, - errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("lower bound must be less than or equal to upper bound")); + CHECK_RANGE_BOUNDS(rmin, rmax); initialize_prng(); @@ -153,10 +163,7 @@ int8random(PG_FUNCTION_ARGS) int64 rmax = PG_GETARG_INT64(1); int64 result; - if (rmin > rmax) - ereport(ERROR, - errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("lower bound must be less than or equal to upper bound")); + CHECK_RANGE_BOUNDS(rmin, rmax); initialize_prng(); @@ -177,9 +184,90 @@ numeric_random(PG_FUNCTION_ARGS) Numeric rmax = PG_GETARG_NUMERIC(1); Numeric result; + /* Leave range bound checking to random_numeric() */ + initialize_prng(); result = random_numeric(&prng_state, rmin, rmax); PG_RETURN_NUMERIC(result); } + + +/* + * date_random() - + * + * Returns a random date chosen uniformly in the specified range. + */ +Datum +date_random(PG_FUNCTION_ARGS) +{ + int32 rmin = (int32) PG_GETARG_DATEADT(0); + int32 rmax = (int32) PG_GETARG_DATEADT(1); + DateADT result; + + CHECK_RANGE_BOUNDS(rmin, rmax); + + if (DATE_IS_NOBEGIN(rmin) || DATE_IS_NOEND(rmax)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("lower and upper bounds must be finite")); + + initialize_prng(); + + result = (DateADT) pg_prng_int64_range(&prng_state, rmin, rmax); + + PG_RETURN_DATEADT(result); +} + +/* + * timestamp_random() - + * + * Returns a random timestamp chosen uniformly in the specified range. + */ +Datum +timestamp_random(PG_FUNCTION_ARGS) +{ + int64 rmin = (int64) PG_GETARG_TIMESTAMP(0); + int64 rmax = (int64) PG_GETARG_TIMESTAMP(1); + Timestamp result; + + CHECK_RANGE_BOUNDS(rmin, rmax); + + if (TIMESTAMP_IS_NOBEGIN(rmin) || TIMESTAMP_IS_NOEND(rmax)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("lower and upper bounds must be finite")); + + initialize_prng(); + + result = (Timestamp) pg_prng_int64_range(&prng_state, rmin, rmax); + + PG_RETURN_TIMESTAMP(result); +} + +/* + * timestamptz_random() - + * + * Returns a random timestamptz chosen uniformly in the specified range. + */ +Datum +timestamptz_random(PG_FUNCTION_ARGS) +{ + int64 rmin = (int64) PG_GETARG_TIMESTAMPTZ(0); + int64 rmax = (int64) PG_GETARG_TIMESTAMPTZ(1); + TimestampTz result; + + CHECK_RANGE_BOUNDS(rmin, rmax); + + if (TIMESTAMP_IS_NOBEGIN(rmin) || TIMESTAMP_IS_NOEND(rmax)) + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("lower and upper bounds must be finite")); + + initialize_prng(); + + result = (TimestampTz) pg_prng_int64_range(&prng_state, rmin, rmax); + + PG_RETURN_TIMESTAMPTZ(result); +} diff --git a/src/backend/utils/adt/pseudotypes.c b/src/backend/utils/adt/pseudotypes.c index 317a1f2b282f9..4581c4b169778 100644 --- a/src/backend/utils/adt/pseudotypes.c +++ b/src/backend/utils/adt/pseudotypes.c @@ -11,7 +11,7 @@ * we do better?) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/adt/quote.c b/src/backend/utils/adt/quote.c index 551de59a07f35..d2ef6a9dc0f63 100644 --- a/src/backend/utils/adt/quote.c +++ b/src/backend/utils/adt/quote.c @@ -3,7 +3,7 @@ * quote.c * Functions for quoting identifiers and literals * - * Portions Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2000-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -37,11 +37,9 @@ quote_ident(PG_FUNCTION_ARGS) * quote_literal_internal - * helper function for quote_literal and quote_literal_cstr * - * NOTE: think not to make this function's behavior change with - * standard_conforming_strings. We don't know where the result - * literal will be used, and so we must generate a result that - * will work with either setting. Take a look at what dblink - * uses this for before thinking you know better. + * NOTE: This must produce output that will work in old servers with + * standard_conforming_strings = off. It's used for example by + * dblink, which may send the result to another server. */ static size_t quote_literal_internal(char *dst, const char *src, size_t len) diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c index 66cc0acf4a712..92dacd73dec46 100644 --- a/src/backend/utils/adt/rangetypes.c +++ b/src/backend/utils/adt/rangetypes.c @@ -19,7 +19,7 @@ * value; we must detoast it first. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -30,7 +30,9 @@ */ #include "postgres.h" +#include "access/tupmacs.h" #include "common/hashfn.h" +#include "funcapi.h" #include "libpq/pqformat.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -39,12 +41,14 @@ #include "optimizer/clauses.h" #include "optimizer/cost.h" #include "optimizer/optimizer.h" +#include "port/pg_bitutils.h" #include "utils/builtins.h" #include "utils/date.h" #include "utils/lsyscache.h" #include "utils/rangetypes.h" #include "utils/sortsupport.h" #include "utils/timestamp.h" +#include "varatt.h" /* fn_extra cache entry for one of the range I/O functions */ @@ -70,8 +74,8 @@ static char *range_deparse(char flags, const char *lbound_str, static char *range_bound_escape(const char *value); static Size datum_compute_size(Size data_length, Datum val, bool typbyval, char typalign, int16 typlen, char typstorage); -static Pointer datum_write(Pointer ptr, Datum datum, bool typbyval, - char typalign, int16 typlen, char typstorage); +static char *datum_write(char *ptr, Datum datum, bool typbyval, + char typalign, int16 typlen, char typstorage); static Node *find_simplified_clause(PlannerInfo *root, Expr *rangeExpr, Expr *elemExpr); static Expr *build_bound_expr(Expr *elemExpr, Datum val, @@ -263,7 +267,7 @@ Datum range_send(PG_FUNCTION_ARGS) { RangeType *range = PG_GETARG_RANGE_P(0); - StringInfo buf = makeStringInfo(); + StringInfoData buf; RangeIOData *cache; char flags; RangeBound lower; @@ -272,6 +276,8 @@ range_send(PG_FUNCTION_ARGS) check_stack_depth(); /* recurses when subtype is a range type */ + initStringInfo(&buf); + cache = get_range_io_data(fcinfo, RangeTypeGetOid(range), IOFunc_send); /* deserialize */ @@ -279,33 +285,31 @@ range_send(PG_FUNCTION_ARGS) flags = range_get_flags(range); /* construct output */ - pq_begintypsend(buf); + pq_begintypsend(&buf); - pq_sendbyte(buf, flags); + pq_sendbyte(&buf, flags); if (RANGE_HAS_LBOUND(flags)) { - Datum bound = PointerGetDatum(SendFunctionCall(&cache->typioproc, - lower.val)); + bytea *bound = SendFunctionCall(&cache->typioproc, lower.val); uint32 bound_len = VARSIZE(bound) - VARHDRSZ; char *bound_data = VARDATA(bound); - pq_sendint32(buf, bound_len); - pq_sendbytes(buf, bound_data, bound_len); + pq_sendint32(&buf, bound_len); + pq_sendbytes(&buf, bound_data, bound_len); } if (RANGE_HAS_UBOUND(flags)) { - Datum bound = PointerGetDatum(SendFunctionCall(&cache->typioproc, - upper.val)); + bytea *bound = SendFunctionCall(&cache->typioproc, upper.val); uint32 bound_len = VARSIZE(bound) - VARHDRSZ; char *bound_data = VARDATA(bound); - pq_sendint32(buf, bound_len); - pq_sendbytes(buf, bound_data, bound_len); + pq_sendint32(&buf, bound_len); + pq_sendbytes(&buf, bound_data, bound_len); } - PG_RETURN_BYTEA_P(pq_endtypsend(buf)); + PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } /* @@ -1077,8 +1081,8 @@ range_union_internal(TypeCacheEntry *typcache, RangeType *r1, RangeType *r2, return r1; if (strict && - !DatumGetBool(range_overlaps_internal(typcache, r1, r2)) && - !DatumGetBool(range_adjacent_internal(typcache, r1, r2))) + !range_overlaps_internal(typcache, r1, r2) && + !range_adjacent_internal(typcache, r1, r2)) ereport(ERROR, (errcode(ERRCODE_DATA_EXCEPTION), errmsg("result of range union would not be contiguous"))); @@ -1215,6 +1219,172 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT return false; } +/* + * range_minus_multi - like range_minus but as a SRF to accommodate splits, + * with no result rows if the result would be empty. + */ +Datum +range_minus_multi(PG_FUNCTION_ARGS) +{ + struct range_minus_multi_fctx + { + RangeType *rs[2]; + int n; + }; + + FuncCallContext *funcctx; + struct range_minus_multi_fctx *fctx; + MemoryContext oldcontext; + + /* stuff done only on the first call of the function */ + if (SRF_IS_FIRSTCALL()) + { + RangeType *r1; + RangeType *r2; + Oid rngtypid; + TypeCacheEntry *typcache; + + /* create a function context for cross-call persistence */ + funcctx = SRF_FIRSTCALL_INIT(); + + /* + * switch to memory context appropriate for multiple function calls + */ + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + r1 = PG_GETARG_RANGE_P(0); + r2 = PG_GETARG_RANGE_P(1); + + /* Different types should be prevented by ANYRANGE matching rules */ + if (RangeTypeGetOid(r1) != RangeTypeGetOid(r2)) + elog(ERROR, "range types do not match"); + + /* allocate memory for user context */ + fctx = palloc_object(struct range_minus_multi_fctx); + + /* + * Initialize state. We can't store the range typcache in fn_extra + * because the caller uses that for the SRF state. + */ + rngtypid = RangeTypeGetOid(r1); + typcache = lookup_type_cache(rngtypid, TYPECACHE_RANGE_INFO); + if (typcache->rngelemtype == NULL) + elog(ERROR, "type %u is not a range type", rngtypid); + range_minus_multi_internal(typcache, r1, r2, fctx->rs, &fctx->n); + + funcctx->user_fctx = fctx; + MemoryContextSwitchTo(oldcontext); + } + + /* stuff done on every call of the function */ + funcctx = SRF_PERCALL_SETUP(); + fctx = funcctx->user_fctx; + + if (funcctx->call_cntr < fctx->n) + { + /* + * We must keep these on separate lines because SRF_RETURN_NEXT does + * call_cntr++: + */ + RangeType *ret = fctx->rs[funcctx->call_cntr]; + + SRF_RETURN_NEXT(funcctx, RangeTypePGetDatum(ret)); + } + else + /* do when there is no more left */ + SRF_RETURN_DONE(funcctx); +} + +/* + * range_minus_multi_internal - Subtracts r2 from r1 + * + * The subtraction can produce zero, one, or two resulting ranges. We return + * the results by setting outputs and outputn to the ranges remaining and their + * count (respectively). The results will never contain empty ranges and will + * be ordered. Caller should set outputs to a two-element array of RangeType + * pointers. + */ +void +range_minus_multi_internal(TypeCacheEntry *typcache, RangeType *r1, + RangeType *r2, RangeType **outputs, int *outputn) +{ + int cmp_l1l2, + cmp_l1u2, + cmp_u1l2, + cmp_u1u2; + RangeBound lower1, + lower2; + RangeBound upper1, + upper2; + bool empty1, + empty2; + + range_deserialize(typcache, r1, &lower1, &upper1, &empty1); + range_deserialize(typcache, r2, &lower2, &upper2, &empty2); + + if (empty1) + { + /* if r1 is empty then r1 - r2 is empty, so return zero results */ + *outputn = 0; + return; + } + else if (empty2) + { + /* r2 is empty so the result is just r1 (which we know is not empty) */ + outputs[0] = r1; + *outputn = 1; + return; + } + + /* + * Use the same logic as range_minus_internal, but support the split case + */ + cmp_l1l2 = range_cmp_bounds(typcache, &lower1, &lower2); + cmp_l1u2 = range_cmp_bounds(typcache, &lower1, &upper2); + cmp_u1l2 = range_cmp_bounds(typcache, &upper1, &lower2); + cmp_u1u2 = range_cmp_bounds(typcache, &upper1, &upper2); + + if (cmp_l1l2 < 0 && cmp_u1u2 > 0) + { + lower2.inclusive = !lower2.inclusive; + lower2.lower = false; /* it will become the upper bound */ + outputs[0] = make_range(typcache, &lower1, &lower2, false, NULL); + + upper2.inclusive = !upper2.inclusive; + upper2.lower = true; /* it will become the lower bound */ + outputs[1] = make_range(typcache, &upper2, &upper1, false, NULL); + + *outputn = 2; + } + else if (cmp_l1u2 > 0 || cmp_u1l2 < 0) + { + outputs[0] = r1; + *outputn = 1; + } + else if (cmp_l1l2 >= 0 && cmp_u1u2 <= 0) + { + *outputn = 0; + } + else if (cmp_l1l2 <= 0 && cmp_u1l2 >= 0 && cmp_u1u2 <= 0) + { + lower2.inclusive = !lower2.inclusive; + lower2.lower = false; /* it will become the upper bound */ + outputs[0] = make_range(typcache, &lower1, &lower2, false, NULL); + *outputn = 1; + } + else if (cmp_l1l2 >= 0 && cmp_u1u2 >= 0 && cmp_l1u2 <= 0) + { + upper2.inclusive = !upper2.inclusive; + upper2.lower = true; /* it will become the lower bound */ + outputs[0] = make_range(typcache, &upper2, &upper1, false, NULL); + *outputn = 1; + } + else + { + elog(ERROR, "unexpected case in range_minus_multi"); + } +} + /* range -> range aggregate functions */ Datum @@ -1345,9 +1515,9 @@ range_fast_cmp(Datum a, Datum b, SortSupport ssup) cmp = range_cmp_bounds(typcache, &upper1, &upper2); } - if ((Datum) range_a != a) + if (range_a != DatumGetPointer(a)) pfree(range_a); - if ((Datum) range_b != b) + if (range_b != DatumGetPointer(b)) pfree(range_b); return cmp; @@ -1358,7 +1528,7 @@ range_fast_cmp(Datum a, Datum b, SortSupport ssup) Datum range_lt(PG_FUNCTION_ARGS) { - int cmp = range_cmp(fcinfo); + int cmp = DatumGetInt32(range_cmp(fcinfo)); PG_RETURN_BOOL(cmp < 0); } @@ -1366,7 +1536,7 @@ range_lt(PG_FUNCTION_ARGS) Datum range_le(PG_FUNCTION_ARGS) { - int cmp = range_cmp(fcinfo); + int cmp = DatumGetInt32(range_cmp(fcinfo)); PG_RETURN_BOOL(cmp <= 0); } @@ -1374,7 +1544,7 @@ range_le(PG_FUNCTION_ARGS) Datum range_ge(PG_FUNCTION_ARGS) { - int cmp = range_cmp(fcinfo); + int cmp = DatumGetInt32(range_cmp(fcinfo)); PG_RETURN_BOOL(cmp >= 0); } @@ -1382,7 +1552,7 @@ range_ge(PG_FUNCTION_ARGS) Datum range_gt(PG_FUNCTION_ARGS) { - int cmp = range_cmp(fcinfo); + int cmp = DatumGetInt32(range_cmp(fcinfo)); PG_RETURN_BOOL(cmp > 0); } @@ -1444,7 +1614,7 @@ hash_range(PG_FUNCTION_ARGS) upper_hash = 0; /* Merge hashes of flags and bounds */ - result = hash_uint32((uint32) flags); + result = hash_bytes_uint32((uint32) flags); result ^= lower_hash; result = pg_rotate_left32(result, 1); result ^= upper_hash; @@ -1924,7 +2094,7 @@ range_deserialize(TypeCacheEntry *typcache, const RangeType *range, int16 typlen; bool typbyval; char typalign; - Pointer ptr; + const char *ptr; Datum lbound; Datum ubound; @@ -1940,14 +2110,14 @@ range_deserialize(TypeCacheEntry *typcache, const RangeType *range, typalign = typcache->rngelemtype->typalign; /* initialize data pointer just after the range OID */ - ptr = (Pointer) (range + 1); + ptr = (const char *) (range + 1); /* fetch lower bound, if any */ if (RANGE_HAS_LBOUND(flags)) { /* att_align_pointer cannot be necessary here */ lbound = fetch_att(ptr, typbyval, typlen); - ptr = (Pointer) att_addlength_pointer(ptr, typlen, ptr); + ptr = (char *) att_addlength_pointer(ptr, typlen, ptr); } else lbound = (Datum) 0; @@ -1955,7 +2125,7 @@ range_deserialize(TypeCacheEntry *typcache, const RangeType *range, /* fetch upper bound, if any */ if (RANGE_HAS_UBOUND(flags)) { - ptr = (Pointer) att_align_pointer(ptr, typalign, typlen, ptr); + ptr = (char *) att_align_pointer(ptr, typalign, typlen, ptr); ubound = fetch_att(ptr, typbyval, typlen); /* no need for att_addlength_pointer */ } @@ -1987,7 +2157,7 @@ char range_get_flags(const RangeType *range) { /* fetch the flag byte from datum's last byte */ - return *((char *) range + VARSIZE(range) - 1); + return *((const char *) range + VARSIZE(range) - 1); } /* @@ -2192,8 +2362,8 @@ range_cmp_bound_values(TypeCacheEntry *typcache, const RangeBound *b1, int range_compare(const void *key1, const void *key2, void *arg) { - RangeType *r1 = *(RangeType **) key1; - RangeType *r2 = *(RangeType **) key2; + RangeType *r1 = *(RangeType *const *) key1; + RangeType *r2 = *(RangeType *const *) key2; TypeCacheEntry *typcache = (TypeCacheEntry *) arg; RangeBound lower1; RangeBound upper1; @@ -2769,8 +2939,8 @@ datum_compute_size(Size data_length, Datum val, bool typbyval, char typalign, * Write the given datum beginning at ptr (after advancing to correct * alignment, if needed). Return the pointer incremented by space used. */ -static Pointer -datum_write(Pointer ptr, Datum datum, bool typbyval, char typalign, +static char * +datum_write(char *ptr, Datum datum, bool typbyval, char typalign, int16 typlen, char typstorage) { Size data_length; diff --git a/src/backend/utils/adt/rangetypes_gist.c b/src/backend/utils/adt/rangetypes_gist.c index a60ee985e746b..1a01a8f4c3cc5 100644 --- a/src/backend/utils/adt/rangetypes_gist.c +++ b/src/backend/utils/adt/rangetypes_gist.c @@ -3,7 +3,7 @@ * rangetypes_gist.c * GiST support for range types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -251,7 +251,7 @@ multirange_gist_compress(PG_FUNCTION_ARGS) MultirangeType *mr = DatumGetMultirangeTypeP(entry->key); RangeType *r; TypeCacheEntry *typcache; - GISTENTRY *retval = palloc(sizeof(GISTENTRY)); + GISTENTRY *retval = palloc_object(GISTENTRY); typcache = multirange_get_typcache(fcinfo, MultirangeTypeGetOid(mr)); r = multirange_get_union_range(typcache->rngtype, mr); @@ -1240,8 +1240,7 @@ range_gist_single_sorting_split(TypeCacheEntry *typcache, maxoff = entryvec->n - 1; - sortItems = (SingleBoundSortItem *) - palloc(maxoff * sizeof(SingleBoundSortItem)); + sortItems = palloc_array(SingleBoundSortItem, maxoff); /* * Prepare auxiliary array and sort the values. @@ -1343,8 +1342,8 @@ range_gist_double_sorting_split(TypeCacheEntry *typcache, context.first = true; /* Allocate arrays for sorted range bounds */ - by_lower = (NonEmptyRange *) palloc(nentries * sizeof(NonEmptyRange)); - by_upper = (NonEmptyRange *) palloc(nentries * sizeof(NonEmptyRange)); + by_lower = palloc_array(NonEmptyRange, nentries); + by_upper = palloc_array(NonEmptyRange, nentries); /* Fill arrays of bounds */ for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) @@ -1499,8 +1498,8 @@ range_gist_double_sorting_split(TypeCacheEntry *typcache, */ /* Allocate vectors for results */ - v->spl_left = (OffsetNumber *) palloc(nentries * sizeof(OffsetNumber)); - v->spl_right = (OffsetNumber *) palloc(nentries * sizeof(OffsetNumber)); + v->spl_left = palloc_array(OffsetNumber, nentries); + v->spl_right = palloc_array(OffsetNumber, nentries); v->spl_nleft = 0; v->spl_nright = 0; @@ -1509,7 +1508,7 @@ range_gist_double_sorting_split(TypeCacheEntry *typcache, * either group without affecting overlap along selected axis. */ common_entries_count = 0; - common_entries = (CommonEntry *) palloc(nentries * sizeof(CommonEntry)); + common_entries = palloc_array(CommonEntry, nentries); /* * Distribute entries which can be distributed unambiguously, and collect @@ -1730,9 +1729,9 @@ get_gist_range_class(RangeType *range) static int single_bound_cmp(const void *a, const void *b, void *arg) { - SingleBoundSortItem *i1 = (SingleBoundSortItem *) a; - SingleBoundSortItem *i2 = (SingleBoundSortItem *) b; - TypeCacheEntry *typcache = (TypeCacheEntry *) arg; + const SingleBoundSortItem *i1 = a; + const SingleBoundSortItem *i2 = b; + TypeCacheEntry *typcache = arg; return range_cmp_bounds(typcache, &i1->bound, &i2->bound); } @@ -1743,9 +1742,9 @@ single_bound_cmp(const void *a, const void *b, void *arg) static int interval_cmp_lower(const void *a, const void *b, void *arg) { - NonEmptyRange *i1 = (NonEmptyRange *) a; - NonEmptyRange *i2 = (NonEmptyRange *) b; - TypeCacheEntry *typcache = (TypeCacheEntry *) arg; + const NonEmptyRange *i1 = a; + const NonEmptyRange *i2 = b; + TypeCacheEntry *typcache = arg; return range_cmp_bounds(typcache, &i1->lower, &i2->lower); } @@ -1756,9 +1755,9 @@ interval_cmp_lower(const void *a, const void *b, void *arg) static int interval_cmp_upper(const void *a, const void *b, void *arg) { - NonEmptyRange *i1 = (NonEmptyRange *) a; - NonEmptyRange *i2 = (NonEmptyRange *) b; - TypeCacheEntry *typcache = (TypeCacheEntry *) arg; + const NonEmptyRange *i1 = a; + const NonEmptyRange *i2 = b; + TypeCacheEntry *typcache = arg; return range_cmp_bounds(typcache, &i1->upper, &i2->upper); } @@ -1769,8 +1768,8 @@ interval_cmp_upper(const void *a, const void *b, void *arg) static int common_entry_cmp(const void *i1, const void *i2) { - double delta1 = ((CommonEntry *) i1)->delta; - double delta2 = ((CommonEntry *) i2)->delta; + double delta1 = ((const CommonEntry *) i1)->delta; + double delta2 = ((const CommonEntry *) i2)->delta; if (delta1 < delta2) return -1; diff --git a/src/backend/utils/adt/rangetypes_selfuncs.c b/src/backend/utils/adt/rangetypes_selfuncs.c index d126abc5a82ee..75f1e7567d5da 100644 --- a/src/backend/utils/adt/rangetypes_selfuncs.c +++ b/src/backend/utils/adt/rangetypes_selfuncs.c @@ -6,7 +6,7 @@ * Estimates are based on histograms of lower and upper bounds, and the * fraction of empty ranges. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -46,18 +46,18 @@ static float8 get_position(TypeCacheEntry *typcache, const RangeBound *value, static float8 get_len_position(double value, double hist1, double hist2); static float8 get_distance(TypeCacheEntry *typcache, const RangeBound *bound1, const RangeBound *bound2); -static int length_hist_bsearch(Datum *length_hist_values, +static int length_hist_bsearch(const Datum *length_hist_values, int length_hist_nvalues, double value, bool equal); -static double calc_length_hist_frac(Datum *length_hist_values, +static double calc_length_hist_frac(const Datum *length_hist_values, int length_hist_nvalues, double length1, double length2, bool equal); static double calc_hist_selectivity_contained(TypeCacheEntry *typcache, const RangeBound *lower, RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, int length_hist_nvalues); + const Datum *length_hist_values, int length_hist_nvalues); static double calc_hist_selectivity_contains(TypeCacheEntry *typcache, const RangeBound *lower, const RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, int length_hist_nvalues); + const Datum *length_hist_values, int length_hist_nvalues); /* * Returns a default selectivity estimate for given operator, when we don't @@ -412,8 +412,8 @@ calc_hist_selectivity(TypeCacheEntry *typcache, VariableStatData *vardata, * bounds. */ nhist = hslot.nvalues; - hist_lower = (RangeBound *) palloc(sizeof(RangeBound) * nhist); - hist_upper = (RangeBound *) palloc(sizeof(RangeBound) * nhist); + hist_lower = palloc_array(RangeBound, nhist); + hist_upper = palloc_array(RangeBound, nhist); for (i = 0; i < nhist; i++) { range_deserialize(typcache, DatumGetRangeTypeP(hslot.values[i]), @@ -654,7 +654,7 @@ rbound_bsearch(TypeCacheEntry *typcache, const RangeBound *value, const RangeBou * given length, returns -1. */ static int -length_hist_bsearch(Datum *length_hist_values, int length_hist_nvalues, +length_hist_bsearch(const Datum *length_hist_values, int length_hist_nvalues, double value, bool equal) { int lower = -1, @@ -852,7 +852,7 @@ get_distance(TypeCacheEntry *typcache, const RangeBound *bound1, const RangeBoun * 'equal' is true). */ static double -calc_length_hist_frac(Datum *length_hist_values, int length_hist_nvalues, +calc_length_hist_frac(const Datum *length_hist_values, int length_hist_nvalues, double length1, double length2, bool equal) { double frac; @@ -1018,7 +1018,7 @@ static double calc_hist_selectivity_contained(TypeCacheEntry *typcache, const RangeBound *lower, RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, int length_hist_nvalues) + const Datum *length_hist_values, int length_hist_nvalues) { int i, upper_index; @@ -1139,7 +1139,7 @@ static double calc_hist_selectivity_contains(TypeCacheEntry *typcache, const RangeBound *lower, const RangeBound *upper, const RangeBound *hist_lower, int hist_nvalues, - Datum *length_hist_values, int length_hist_nvalues) + const Datum *length_hist_values, int length_hist_nvalues) { int i, lower_index; diff --git a/src/backend/utils/adt/rangetypes_spgist.c b/src/backend/utils/adt/rangetypes_spgist.c index 9b6d7061a1812..b198375e64d60 100644 --- a/src/backend/utils/adt/rangetypes_spgist.c +++ b/src/backend/utils/adt/rangetypes_spgist.c @@ -25,7 +25,7 @@ * This implementation only uses the comparison function of the range element * datatype, therefore it works for any range type. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -59,7 +59,9 @@ static int adjacent_cmp_bounds(TypeCacheEntry *typcache, const RangeBound *arg, Datum spg_range_quad_config(PG_FUNCTION_ARGS) { - /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ +#ifdef NOT_USED + spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); +#endif spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); cfg->prefixType = ANYRANGEOID; @@ -185,9 +187,9 @@ spg_range_quad_choose(PG_FUNCTION_ARGS) static int bound_cmp(const void *a, const void *b, void *arg) { - RangeBound *ba = (RangeBound *) a; - RangeBound *bb = (RangeBound *) b; - TypeCacheEntry *typcache = (TypeCacheEntry *) arg; + const RangeBound *ba = a; + const RangeBound *bb = b; + TypeCacheEntry *typcache = arg; return range_cmp_bounds(typcache, ba, bb); } @@ -216,8 +218,8 @@ spg_range_quad_picksplit(PG_FUNCTION_ARGS) RangeTypeGetOid(DatumGetRangeTypeP(in->datums[0]))); /* Allocate memory for bounds */ - lowerBounds = palloc(sizeof(RangeBound) * in->nTuples); - upperBounds = palloc(sizeof(RangeBound) * in->nTuples); + lowerBounds = palloc_array(RangeBound, in->nTuples); + upperBounds = palloc_array(RangeBound, in->nTuples); j = 0; /* Deserialize bounds of ranges, count non-empty ranges */ @@ -243,8 +245,8 @@ spg_range_quad_picksplit(PG_FUNCTION_ARGS) out->prefixDatum = PointerGetDatum(NULL); out->nodeLabels = NULL; - out->mapTuplesToNodes = palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = palloc(sizeof(Datum) * in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); /* Place all ranges into node 0 */ for (i = 0; i < in->nTuples; i++) @@ -273,8 +275,8 @@ spg_range_quad_picksplit(PG_FUNCTION_ARGS) out->nNodes = (in->level == 0) ? 5 : 4; out->nodeLabels = NULL; /* we don't need node labels */ - out->mapTuplesToNodes = palloc(sizeof(int) * in->nTuples); - out->leafTupleDatums = palloc(sizeof(Datum) * in->nTuples); + out->mapTuplesToNodes = palloc_array(int, in->nTuples); + out->leafTupleDatums = palloc_array(Datum, in->nTuples); /* * Assign ranges to corresponding nodes according to quadrants relative to @@ -316,7 +318,7 @@ spg_range_quad_inner_consistent(PG_FUNCTION_ARGS) { /* Report that all nodes should be visited */ out->nNodes = in->nNodes; - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); for (i = 0; i < in->nNodes; i++) out->nodeNumbers[i] = i; PG_RETURN_VOID(); @@ -732,9 +734,9 @@ spg_range_quad_inner_consistent(PG_FUNCTION_ARGS) } /* We must descend into the quadrant(s) identified by 'which' */ - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); if (needPrevious) - out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes); + out->traversalValues = palloc_array(void *, in->nNodes); out->nNodes = 0; /* @@ -757,7 +759,7 @@ spg_range_quad_inner_consistent(PG_FUNCTION_ARGS) * because it's range */ previousCentroid = datumCopy(in->prefixDatum, false, -1); - out->traversalValues[out->nNodes] = (void *) previousCentroid; + out->traversalValues[out->nNodes] = DatumGetPointer(previousCentroid); } out->nodeNumbers[out->nNodes] = i - 1; out->nNodes++; diff --git a/src/backend/utils/adt/rangetypes_typanalyze.c b/src/backend/utils/adt/rangetypes_typanalyze.c index a18196d8a34a5..278d4e6941a37 100644 --- a/src/backend/utils/adt/rangetypes_typanalyze.c +++ b/src/backend/utils/adt/rangetypes_typanalyze.c @@ -13,7 +13,7 @@ * come from different tuples. In theory, the standard scalar selectivity * functions could be used with the combined histogram. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -111,9 +111,9 @@ float8_qsort_cmp(const void *a1, const void *a2, void *arg) static int range_bound_qsort_cmp(const void *a1, const void *a2, void *arg) { - RangeBound *b1 = (RangeBound *) a1; - RangeBound *b2 = (RangeBound *) a2; - TypeCacheEntry *typcache = (TypeCacheEntry *) arg; + const RangeBound *b1 = a1; + const RangeBound *b2 = a2; + TypeCacheEntry *typcache = arg; return range_cmp_bounds(typcache, b1, b2); } @@ -151,9 +151,9 @@ compute_range_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, has_subdiff = OidIsValid(typcache->rng_subdiff_finfo.fn_oid); /* Allocate memory to hold range bounds and lengths of the sample ranges. */ - lowers = (RangeBound *) palloc(sizeof(RangeBound) * samplerows); - uppers = (RangeBound *) palloc(sizeof(RangeBound) * samplerows); - lengths = (float8 *) palloc(sizeof(float8) * samplerows); + lowers = palloc_array(RangeBound, samplerows); + uppers = palloc_array(RangeBound, samplerows); + lengths = palloc_array(float8, samplerows); /* Loop over the sample ranges. */ for (range_no = 0; range_no < samplerows; range_no++) @@ -397,11 +397,11 @@ compute_range_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, stats->numvalues[slot_idx] = num_hist; stats->statypid[slot_idx] = FLOAT8OID; stats->statyplen[slot_idx] = sizeof(float8); - stats->statypbyval[slot_idx] = FLOAT8PASSBYVAL; - stats->statypalign[slot_idx] = 'd'; + stats->statypbyval[slot_idx] = true; + stats->statypalign[slot_idx] = TYPALIGN_DOUBLE; /* Store the fraction of empty ranges */ - emptyfrac = (float4 *) palloc(sizeof(float4)); + emptyfrac = palloc_object(float4); *emptyfrac = ((double) empty_cnt) / ((double) non_null_cnt); stats->stanumbers[slot_idx] = emptyfrac; stats->numnumbers[slot_idx] = 1; diff --git a/src/backend/utils/adt/regexp.c b/src/backend/utils/adt/regexp.c index edee1f7880bde..311b9877bbb96 100644 --- a/src/backend/utils/adt/regexp.c +++ b/src/backend/utils/adt/regexp.c @@ -3,7 +3,7 @@ * regexp.c * Postgres' interface to the regular expression package. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -189,7 +189,7 @@ RE_compile_and_cache(text *text_re, int cflags, Oid collation) */ /* Convert pattern string to wide characters */ - pattern = (pg_wchar *) palloc((text_re_len + 1) * sizeof(pg_wchar)); + pattern = palloc_array(pg_wchar, text_re_len + 1); pattern_len = pg_mb2wchar_with_len(text_re_val, pattern, text_re_len); @@ -329,7 +329,7 @@ RE_execute(regex_t *re, char *dat, int dat_len, bool match; /* Convert data string to wide characters */ - data = (pg_wchar *) palloc((dat_len + 1) * sizeof(pg_wchar)); + data = palloc_array(pg_wchar, dat_len + 1); data_len = pg_mb2wchar_with_len(dat, data, dat_len); /* Perform RE match and return result */ @@ -443,7 +443,7 @@ parse_re_flags(pg_re_flags *flags, text *opts) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid regular expression option: \"%.*s\"", - pg_mblen(opt_p + i), opt_p + i))); + pg_mblen_range(opt_p + i, opt_p + opt_len), opt_p + i))); break; } } @@ -673,12 +673,13 @@ textregexreplace(PG_FUNCTION_ARGS) if (VARSIZE_ANY_EXHDR(opt) > 0) { char *opt_p = VARDATA_ANY(opt); + const char *end_p = opt_p + VARSIZE_ANY_EXHDR(opt); if (*opt_p >= '0' && *opt_p <= '9') ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid regular expression option: \"%.*s\"", - pg_mblen(opt_p), opt_p), + pg_mblen_range(opt_p, end_p), opt_p), errhint("If you meant to use regexp_replace() with a start parameter, cast the fourth argument to integer explicitly."))); } @@ -772,12 +773,15 @@ similar_escape_internal(text *pat_text, text *esc_text) *r; int plen, elen; + const char *pend; bool afterescape = false; - bool incharclass = false; int nquotes = 0; + int bracket_depth = 0; /* square bracket nesting level */ + int charclass_pos = 0; /* position inside a character class */ p = VARDATA_ANY(pat_text); plen = VARSIZE_ANY_EXHDR(pat_text); + pend = p + plen; if (esc_text == NULL) { /* No ESCAPE clause provided; default to backslash as escape */ @@ -833,6 +837,17 @@ similar_escape_internal(text *pat_text, text *esc_text) * the relevant part separators in the above expansion. If the result * of this function is used in a plain regexp match (SIMILAR TO), the * escape-double-quotes have no effect on the match behavior. + * + * While we don't fully validate character classes (bracket expressions), + * we do need to parse them well enough to know where they end. + * "charclass_pos" tracks where we are in a character class. + * Its value is uninteresting when bracket_depth is 0. + * But when bracket_depth > 0, it will be + * 1: right after the opening '[' (a following '^' will negate + * the class, while ']' is a literal character) + * 2: right after a '^' after the opening '[' (']' is still a literal + * character) + * 3 or more: further inside the character class (']' ends the class) *---------- */ @@ -866,7 +881,7 @@ similar_escape_internal(text *pat_text, text *esc_text) if (elen > 1) { - int mblen = pg_mblen(p); + int mblen = pg_mblen_range(p, pend); if (mblen > 1) { @@ -904,7 +919,7 @@ similar_escape_internal(text *pat_text, text *esc_text) /* fast path */ if (afterescape) { - if (pchar == '"' && !incharclass) /* escape-double-quote? */ + if (pchar == '"' && bracket_depth < 1) /* escape-double-quote? */ { /* emit appropriate part separator, per notes above */ if (nquotes == 0) @@ -945,6 +960,12 @@ similar_escape_internal(text *pat_text, text *esc_text) */ *r++ = '\\'; *r++ = pchar; + + /* + * If we encounter an escaped character in a character class, + * we are no longer at the beginning. + */ + charclass_pos = 3; } afterescape = false; } @@ -953,18 +974,69 @@ similar_escape_internal(text *pat_text, text *esc_text) /* SQL escape character; do not send to output */ afterescape = true; } - else if (incharclass) + else if (bracket_depth > 0) { + /* inside a character class */ if (pchar == '\\') + { + /* + * If we're here, backslash is not the SQL escape character, + * so treat it as a literal class element, which requires + * doubling it. (This matches our behavior for backslashes + * outside character classes.) + */ *r++ = '\\'; + } *r++ = pchar; - if (pchar == ']') - incharclass = false; + + /* parse the character class well enough to identify ending ']' */ + if (pchar == ']' && charclass_pos > 2) + { + /* found the real end of a bracket pair */ + bracket_depth--; + /* don't reset charclass_pos, this may be an inner bracket */ + } + else if (pchar == '[') + { + /* start of a nested bracket pair */ + bracket_depth++; + + /* + * We are no longer at the beginning of a character class. + * (The nested bracket pair is a collating element, not a + * character class in its own right.) + */ + charclass_pos = 3; + } + else if (pchar == '^') + { + /* + * A caret right after the opening bracket negates the + * character class. In that case, the following will + * increment charclass_pos from 1 to 2, so that a following + * ']' is still a literal character and does not end the + * character class. If we are further inside a character + * class, charclass_pos might get incremented past 3, which is + * fine. + */ + charclass_pos++; + } + else + { + /* + * Anything else (including a backslash or leading ']') is an + * element of the character class, so we are no longer at the + * beginning of the class. + */ + charclass_pos = 3; + } } else if (pchar == '[') { + /* start of a character class */ *r++ = pchar; - incharclass = true; + bracket_depth = 1; + charclass_pos = 1; } else if (pchar == '%') { @@ -1320,8 +1392,8 @@ regexp_match(PG_FUNCTION_ARGS) Assert(matchctx->nmatches == 1); /* Create workspace that build_regexp_match_result needs */ - matchctx->elems = (Datum *) palloc(sizeof(Datum) * matchctx->npatterns); - matchctx->nulls = (bool *) palloc(sizeof(bool) * matchctx->npatterns); + matchctx->elems = palloc_array(Datum, matchctx->npatterns); + matchctx->nulls = palloc_array(bool, matchctx->npatterns); PG_RETURN_DATUM(PointerGetDatum(build_regexp_match_result(matchctx))); } @@ -1363,8 +1435,8 @@ regexp_matches(PG_FUNCTION_ARGS) true, false, false); /* Pre-create workspace that build_regexp_match_result needs */ - matchctx->elems = (Datum *) palloc(sizeof(Datum) * matchctx->npatterns); - matchctx->nulls = (bool *) palloc(sizeof(bool) * matchctx->npatterns); + matchctx->elems = palloc_array(Datum, matchctx->npatterns); + matchctx->nulls = palloc_array(bool, matchctx->npatterns); MemoryContextSwitchTo(oldcontext); funcctx->user_fctx = matchctx; @@ -1420,7 +1492,7 @@ setup_regexp_matches(text *orig_str, text *pattern, pg_re_flags *re_flags, bool ignore_degenerate, bool fetching_unmatched) { - regexp_matches_ctx *matchctx = palloc0(sizeof(regexp_matches_ctx)); + regexp_matches_ctx *matchctx = palloc0_object(regexp_matches_ctx); int eml = pg_database_encoding_max_length(); int orig_len; pg_wchar *wide_str; @@ -1440,7 +1512,7 @@ setup_regexp_matches(text *orig_str, text *pattern, pg_re_flags *re_flags, /* convert string to pg_wchar form for matching */ orig_len = VARSIZE_ANY_EXHDR(orig_str); - wide_str = (pg_wchar *) palloc(sizeof(pg_wchar) * (orig_len + 1)); + wide_str = palloc_array(pg_wchar, orig_len + 1); wide_len = pg_mb2wchar_with_len(VARDATA_ANY(orig_str), wide_str, orig_len); /* set up the compiled pattern */ @@ -1463,7 +1535,7 @@ setup_regexp_matches(text *orig_str, text *pattern, pg_re_flags *re_flags, } /* temporary output space for RE package */ - pmatch = palloc(sizeof(regmatch_t) * pmatch_len); + pmatch = palloc_array(regmatch_t, pmatch_len); /* * the real output space (grown dynamically if needed) @@ -1472,7 +1544,7 @@ setup_regexp_matches(text *orig_str, text *pattern, pg_re_flags *re_flags, * than at 2^27 */ array_len = re_flags->glob ? 255 : 31; - matchctx->match_locs = (int *) palloc(sizeof(int) * array_len); + matchctx->match_locs = palloc_array(int, array_len); array_idx = 0; /* search for the pattern, perhaps repeatedly */ diff --git a/src/backend/utils/adt/regproc.c b/src/backend/utils/adt/regproc.c index 5ee608a2b3921..64f293f4e98b0 100644 --- a/src/backend/utils/adt/regproc.c +++ b/src/backend/utils/adt/regproc.c @@ -8,7 +8,7 @@ * special I/O conversion routines. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,6 +25,7 @@ #include "catalog/namespace.h" #include "catalog/pg_class.h" #include "catalog/pg_collation.h" +#include "catalog/pg_database.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" #include "catalog/pg_ts_config.h" @@ -70,6 +71,7 @@ regprocin(PG_FUNCTION_ARGS) RegProcedure result; List *names; FuncCandidateList clist; + int fgc_flags; /* Handle "-" or numeric OID */ if (parseDashOrOid(pro_name_or_oid, &result, escontext)) @@ -92,7 +94,8 @@ regprocin(PG_FUNCTION_ARGS) if (names == NIL) PG_RETURN_NULL(); - clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, true); + clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, true, + &fgc_flags); if (clist == NULL) ereturn(escontext, (Datum) 0, @@ -163,13 +166,15 @@ regprocout(PG_FUNCTION_ARGS) { char *nspname; FuncCandidateList clist; + int fgc_flags; /* * Would this proc be found (uniquely!) by regprocin? If not, * qualify it. */ clist = FuncnameGetCandidates(list_make1(makeString(proname)), - -1, NIL, false, false, false, false); + -1, NIL, false, false, false, false, + &fgc_flags); if (clist != NULL && clist->next == NULL && clist->oid == proid) nspname = NULL; @@ -230,6 +235,7 @@ regprocedurein(PG_FUNCTION_ARGS) int nargs; Oid argtypes[FUNC_MAX_ARGS]; FuncCandidateList clist; + int fgc_flags; /* Handle "-" or numeric OID */ if (parseDashOrOid(pro_name_or_oid, &result, escontext)) @@ -250,8 +256,8 @@ regprocedurein(PG_FUNCTION_ARGS) escontext)) PG_RETURN_NULL(); - clist = FuncnameGetCandidates(names, nargs, NIL, false, false, - false, true); + clist = FuncnameGetCandidates(names, nargs, NIL, false, false, false, true, + &fgc_flags); for (; clist; clist = clist->next) { @@ -323,7 +329,7 @@ format_procedure_qualified(Oid procedure_oid) * always schema-qualify procedure names, regardless of search_path */ char * -format_procedure_extended(Oid procedure_oid, bits16 flags) +format_procedure_extended(Oid procedure_oid, uint16 flags) { char *result; HeapTuple proctup; @@ -482,6 +488,7 @@ regoperin(PG_FUNCTION_ARGS) Oid result; List *names; FuncCandidateList clist; + int fgc_flags; /* Handle "0" or numeric OID */ if (parseNumericOid(opr_name_or_oid, &result, escontext)) @@ -501,7 +508,7 @@ regoperin(PG_FUNCTION_ARGS) if (names == NIL) PG_RETURN_NULL(); - clist = OpernameGetCandidates(names, '\0', true); + clist = OpernameGetCandidates(names, '\0', true, &fgc_flags); if (clist == NULL) ereturn(escontext, (Datum) 0, @@ -571,13 +578,14 @@ regoperout(PG_FUNCTION_ARGS) else { FuncCandidateList clist; + int fgc_flags; /* * Would this oper be found (uniquely!) by regoperin? If not, * qualify it. */ clist = OpernameGetCandidates(list_make1(makeString(oprname)), - '\0', false); + '\0', false, &fgc_flags); if (clist != NULL && clist->next == NULL && clist->oid == oprid) result = pstrdup(oprname); @@ -719,7 +727,7 @@ to_regoperator(PG_FUNCTION_ARGS) * always schema-qualify operator names, regardless of search_path */ char * -format_operator_extended(Oid operator_oid, bits16 flags) +format_operator_extended(Oid operator_oid, uint16 flags) { char *result; HeapTuple opertup; @@ -1763,6 +1771,123 @@ regnamespacesend(PG_FUNCTION_ARGS) return oidsend(fcinfo); } +/* + * regdatabasein - converts database name to database OID + * + * We also accept a numeric OID, for symmetry with the output routine. + * + * '-' signifies unknown (OID 0). In all other cases, the input must + * match an existing pg_database entry. + */ +Datum +regdatabasein(PG_FUNCTION_ARGS) +{ + char *db_name_or_oid = PG_GETARG_CSTRING(0); + Node *escontext = fcinfo->context; + Oid result; + List *names; + + /* Handle "-" or numeric OID */ + if (parseDashOrOid(db_name_or_oid, &result, escontext)) + PG_RETURN_OID(result); + + /* The rest of this wouldn't work in bootstrap mode */ + if (IsBootstrapProcessingMode()) + elog(ERROR, "regdatabase values must be OIDs in bootstrap mode"); + + /* Normal case: see if the name matches any pg_database entry. */ + names = stringToQualifiedNameList(db_name_or_oid, escontext); + if (names == NIL) + PG_RETURN_NULL(); + + if (list_length(names) != 1) + ereturn(escontext, (Datum) 0, + (errcode(ERRCODE_INVALID_NAME), + errmsg("invalid name syntax"))); + + result = get_database_oid(strVal(linitial(names)), true); + + if (!OidIsValid(result)) + ereturn(escontext, (Datum) 0, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("database \"%s\" does not exist", + strVal(linitial(names))))); + + PG_RETURN_OID(result); +} + +/* + * to_regdatabase - converts database name to database OID + * + * If the name is not found, we return NULL. + */ +Datum +to_regdatabase(PG_FUNCTION_ARGS) +{ + char *db_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + Datum result; + ErrorSaveContext escontext = {T_ErrorSaveContext}; + + if (!DirectInputFunctionCallSafe(regdatabasein, db_name, + InvalidOid, -1, + (Node *) &escontext, + &result)) + PG_RETURN_NULL(); + PG_RETURN_DATUM(result); +} + +/* + * regdatabaseout - converts database OID to database name + */ +Datum +regdatabaseout(PG_FUNCTION_ARGS) +{ + Oid dboid = PG_GETARG_OID(0); + char *result; + + if (dboid == InvalidOid) + { + result = pstrdup("-"); + PG_RETURN_CSTRING(result); + } + + result = get_database_name(dboid); + + if (result) + { + /* pstrdup is not really necessary, but it avoids a compiler warning */ + result = pstrdup(quote_identifier(result)); + } + else + { + /* If OID doesn't match any database, return it numerically */ + result = (char *) palloc(NAMEDATALEN); + snprintf(result, NAMEDATALEN, "%u", dboid); + } + + PG_RETURN_CSTRING(result); +} + +/* + * regdatabaserecv - converts external binary format to regdatabase + */ +Datum +regdatabaserecv(PG_FUNCTION_ARGS) +{ + /* Exactly the same as oidrecv, so share code */ + return oidrecv(fcinfo); +} + +/* + * regdatabasesend - converts regdatabase to binary format + */ +Datum +regdatabasesend(PG_FUNCTION_ARGS) +{ + /* Exactly the same as oidsend, so share code */ + return oidsend(fcinfo); +} + /* * text_regclass: convert text to regclass * diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 6239900fa2892..f63a7f0b580c9 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -14,7 +14,7 @@ * plan --- consider improving this someday. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/backend/utils/adt/ri_triggers.c * @@ -23,14 +23,18 @@ #include "postgres.h" +#include "access/amapi.h" +#include "access/genam.h" #include "access/htup_details.h" +#include "access/skey.h" #include "access/sysattr.h" #include "access/table.h" #include "access/tableam.h" #include "access/xact.h" +#include "catalog/index.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" -#include "catalog/pg_proc.h" +#include "catalog/pg_namespace.h" #include "commands/trigger.h" #include "executor/executor.h" #include "executor/spi.h" @@ -43,10 +47,10 @@ #include "utils/datum.h" #include "utils/fmgroids.h" #include "utils/guc.h" +#include "utils/hsearch.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" -#include "utils/rangetypes.h" #include "utils/rel.h" #include "utils/rls.h" #include "utils/ruleutils.h" @@ -93,6 +97,7 @@ #define RI_TRIGTYPE_UPDATE 2 #define RI_TRIGTYPE_DELETE 3 +typedef struct FastPathMeta FastPathMeta; /* * RI_ConstraintInfo @@ -128,12 +133,32 @@ typedef struct RI_ConstraintInfo Oid pf_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = FK) */ Oid pp_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = PK) */ Oid ff_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (FK = FK) */ - Oid period_contained_by_oper; /* anyrange <@ anyrange */ + Oid period_contained_by_oper; /* anyrange <@ anyrange (or + * multiranges) */ Oid agged_period_contained_by_oper; /* fkattr <@ range_agg(pkattr) */ - Oid period_intersect_oper; /* anyrange * anyrange */ + Oid period_intersect_oper; /* anyrange * anyrange (or + * multiranges) */ dlist_node valid_link; /* Link in list of valid entries */ + + Oid conindid; + bool pk_is_partitioned; + + FastPathMeta *fpmeta; } RI_ConstraintInfo; +typedef struct RI_CompareHashEntry RI_CompareHashEntry; + +/* Fast-path metadata for RI checks on foreign key referencing tables */ +typedef struct FastPathMeta +{ + FmgrInfo eq_opr_finfo[RI_MAX_NUMKEYS]; + FmgrInfo cast_func_finfo[RI_MAX_NUMKEYS]; + RegProcedure regops[RI_MAX_NUMKEYS]; + Oid subtypes[RI_MAX_NUMKEYS]; + int strats[RI_MAX_NUMKEYS]; + AttrNumber index_attnos[RI_MAX_NUMKEYS]; /* index column positions */ +} FastPathMeta; + /* * RI_QueryKey * @@ -176,6 +201,55 @@ typedef struct RI_CompareHashEntry FmgrInfo cast_func_finfo; /* in case we must coerce input */ } RI_CompareHashEntry; +/* + * Maximum number of FK rows buffered before flushing. + * + * Larger batches amortize per-flush overhead and let the SK_SEARCHARRAY + * path walk more leaf pages in a single sorted traversal. But each + * buffered row is a materialized HeapTuple in flush_cxt, and the matched[] + * scan in ri_FastPathFlushArray() is O(batch_size) per index match. + * Benchmarking showed little difference between 16 and 64, with 256 + * consistently slower. 64 is a reasonable default. + */ +#define RI_FASTPATH_BATCH_SIZE 64 + +/* + * RI_FastPathEntry + * Per-constraint cache of resources needed by ri_FastPathBatchFlush(). + * + * One entry per constraint, keyed by pg_constraint OID. Created lazily + * by ri_FastPathGetEntry() on first use within a trigger-firing batch + * and torn down by ri_FastPathTeardown() at batch end. + * + * FK tuples are buffered in batch[] across trigger invocations and + * flushed when the buffer fills or the batch ends. + * + * RI_FastPathEntry is not subject to cache invalidation. The cached + * relations are held open with locks for the transaction duration, preventing + * relcache invalidation. The entry itself is torn down at batch end by + * ri_FastPathEndBatch(); on abort, ResourceOwner releases the cached + * relations and the XactCallback/SubXactCallback NULL the static cache pointer + * to prevent any subsequent access. + */ +typedef struct RI_FastPathEntry +{ + Oid conoid; /* hash key: pg_constraint OID */ + Oid fk_relid; /* for ri_FastPathEndBatch() */ + Relation pk_rel; + Relation idx_rel; + TupleTableSlot *pk_slot; + TupleTableSlot *fk_slot; + MemoryContext flush_cxt; /* short-lived context for per-flush work */ + + /* + * TODO: batch[] is HeapTuple[] because the AFTER trigger machinery + * currently passes tuples as HeapTuples. Once trigger infrastructure is + * slotified, this should use a slot array or whatever batched tuple + * storage abstraction exists at that point to be TAM-agnostic. + */ + HeapTuple batch[RI_FASTPATH_BATCH_SIZE]; + int batch_count; +} RI_FastPathEntry; /* * Local data @@ -185,6 +259,8 @@ static HTAB *ri_query_cache = NULL; static HTAB *ri_compare_cache = NULL; static dclist_head ri_constraint_cache_valid_list; +static HTAB *ri_fastpath_cache = NULL; +static bool ri_fastpath_callback_registered = false; /* * Local function prototypes @@ -213,16 +289,17 @@ static bool ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid, Datum lhs, Datum rhs); static void ri_InitHashTables(void); -static void InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue); +static void InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static SPIPlanPtr ri_FetchPreparedPlan(RI_QueryKey *key); static void ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan); static RI_CompareHashEntry *ri_HashCompareOp(Oid eq_opr, Oid typeid); static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind); -static const RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, - Relation trig_rel, bool rel_is_pk); -static const RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid); +static RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, + Relation trig_rel, bool rel_is_pk); +static RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid); static Oid get_ri_constraint_root(Oid constrOid); static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, RI_QueryKey *qkey, Relation fk_rel, Relation pk_rel); @@ -232,6 +309,33 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot, TupleTableSlot *newslot, bool is_restrict, bool detectNewRows, int expect_OK); +static void ri_FastPathCheck(RI_ConstraintInfo *riinfo, + Relation fk_rel, TupleTableSlot *newslot); +static void ri_FastPathBatchAdd(RI_ConstraintInfo *riinfo, + Relation fk_rel, TupleTableSlot *newslot); +static void ri_FastPathBatchFlush(RI_FastPathEntry *fpentry, Relation fk_rel, + RI_ConstraintInfo *riinfo); +static int ri_FastPathFlushArray(RI_FastPathEntry *fpentry, TupleTableSlot *fk_slot, + const RI_ConstraintInfo *riinfo, Relation fk_rel, + Snapshot snapshot, IndexScanDesc scandesc); +static int ri_FastPathFlushLoop(RI_FastPathEntry *fpentry, TupleTableSlot *fk_slot, + const RI_ConstraintInfo *riinfo, Relation fk_rel, + Snapshot snapshot, IndexScanDesc scandesc); +static bool ri_FastPathProbeOne(Relation pk_rel, Relation idx_rel, + IndexScanDesc scandesc, TupleTableSlot *slot, + Snapshot snapshot, const RI_ConstraintInfo *riinfo, + ScanKeyData *skey, int nkeys); +static bool ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap, + bool *concurrently_updated); +static bool ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo); +static void ri_CheckPermissions(Relation query_rel); +static bool recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, + int nkeys, TupleTableSlot *new_slot); +static void build_index_scankeys(const RI_ConstraintInfo *riinfo, + Relation idx_rel, Datum *pk_vals, + char *pk_nulls, ScanKey skeys); +static void ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo, + Relation fk_rel, Relation idx_rel); static void ri_ExtractValues(Relation rel, TupleTableSlot *slot, const RI_ConstraintInfo *riinfo, bool rel_is_pk, Datum *vals, char *nulls); @@ -239,6 +343,10 @@ pg_noreturn static void ri_ReportViolation(const RI_ConstraintInfo *riinfo, Relation pk_rel, Relation fk_rel, TupleTableSlot *violatorslot, TupleDesc tupdesc, int queryno, bool is_restrict, bool partgone); +static RI_FastPathEntry *ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, + Relation fk_rel); +static void ri_FastPathEndBatch(void *arg); +static void ri_FastPathTeardown(void); /* @@ -249,7 +357,7 @@ pg_noreturn static void ri_ReportViolation(const RI_ConstraintInfo *riinfo, static Datum RI_FKey_check(TriggerData *trigdata) { - const RI_ConstraintInfo *riinfo; + RI_ConstraintInfo *riinfo; Relation fk_rel; Relation pk_rel; TupleTableSlot *newslot; @@ -275,14 +383,7 @@ RI_FKey_check(TriggerData *trigdata) if (!table_tuple_satisfies_snapshot(trigdata->tg_relation, newslot, SnapshotSelf)) return PointerGetDatum(NULL); - /* - * Get the relation descriptors of the FK and PK tables. - * - * pk_rel is opened in RowShareLock mode since that's what our eventual - * SELECT FOR KEY SHARE will get on it. - */ fk_rel = trigdata->tg_relation; - pk_rel = table_open(riinfo->pk_relid, RowShareLock); switch (ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false)) { @@ -292,7 +393,6 @@ RI_FKey_check(TriggerData *trigdata) * No further check needed - an all-NULL key passes every type of * foreign key constraint. */ - table_close(pk_rel, RowShareLock); return PointerGetDatum(NULL); case RI_KEYS_SOME_NULL: @@ -317,7 +417,6 @@ RI_FKey_check(TriggerData *trigdata) errdetail("MATCH FULL does not allow mixing of null and nonnull key values."), errtableconstraint(fk_rel, NameStr(riinfo->conname)))); - table_close(pk_rel, RowShareLock); return PointerGetDatum(NULL); case FKCONSTR_MATCH_SIMPLE: @@ -326,7 +425,6 @@ RI_FKey_check(TriggerData *trigdata) * MATCH SIMPLE - if ANY column is null, the key passes * the constraint. */ - table_close(pk_rel, RowShareLock); return PointerGetDatum(NULL); #ifdef NOT_USED @@ -351,8 +449,41 @@ RI_FKey_check(TriggerData *trigdata) break; } + /* + * Fast path: probe the PK unique index directly, bypassing SPI. + * + * For non-partitioned, non-temporal FKs, we can skip the SPI machinery + * (plan cache, executor setup, etc.) and do a direct index scan + tuple + * lock. This is semantically equivalent to the SPI path below but avoids + * the per-row executor overhead. + * + * ri_FastPathBatchAdd() and ri_FastPathCheck() report the violation + * themselves if no matching PK row is found, so they only return on + * success. + */ + if (ri_fastpath_is_applicable(riinfo)) + { + if (AfterTriggerIsActive()) + { + /* Batched path: buffer and probe in groups */ + ri_FastPathBatchAdd(riinfo, fk_rel, newslot); + } + else + { + /* ALTER TABLE validation: per-row, no cache */ + ri_FastPathCheck(riinfo, fk_rel, newslot); + } + return PointerGetDatum(NULL); + } + SPI_connect(); + /* + * pk_rel is opened in RowShareLock mode since that's what our eventual + * SELECT FOR KEY SHARE will get on it. + */ + pk_rel = table_open(riinfo->pk_relid, RowShareLock); + /* Fetch or prepare a saved plan for the real check */ ri_BuildQueryKey(&qkey, riinfo, RI_PLAN_CHECK_LOOKUPPK); @@ -2210,11 +2341,11 @@ ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind) /* * Fetch the RI_ConstraintInfo struct for the trigger's FK constraint. */ -static const RI_ConstraintInfo * +static RI_ConstraintInfo * ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk) { Oid constraintOid = trigger->tgconstraint; - const RI_ConstraintInfo *riinfo; + RI_ConstraintInfo *riinfo; /* * Check that the FK constraint's OID is available; it might not be if @@ -2264,7 +2395,7 @@ ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk) /* * Fetch or create the RI_ConstraintInfo struct for an FK constraint. */ -static const RI_ConstraintInfo * +static RI_ConstraintInfo * ri_LoadConstraintInfo(Oid constraintOid) { RI_ConstraintInfo *riinfo; @@ -2345,6 +2476,11 @@ ri_LoadConstraintInfo(Oid constraintOid) &riinfo->period_intersect_oper); } + /* Metadata used by fast path. */ + riinfo->conindid = conForm->conindid; + riinfo->pk_is_partitioned = + (get_rel_relkind(riinfo->pk_relid) == RELKIND_PARTITIONED_TABLE); + ReleaseSysCache(tup); /* @@ -2355,6 +2491,8 @@ ri_LoadConstraintInfo(Oid constraintOid) riinfo->valid = true; + riinfo->fpmeta = NULL; + return riinfo; } @@ -2397,7 +2535,8 @@ get_ri_constraint_root(Oid constrOid) * data from changing under it --- but we may get cache flushes anyway.) */ static void -InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) +InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { dlist_mutable_iter iter; @@ -2427,6 +2566,11 @@ InvalidateConstraintCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) riinfo->rootHashValue == hashvalue) { riinfo->valid = false; + if (riinfo->fpmeta) + { + pfree(riinfo->fpmeta); + riinfo->fpmeta = NULL; + } /* Remove invalidated entries from the list, too */ dclist_delete_from(&ri_constraint_cache_valid_list, iter.cur); } @@ -2580,7 +2724,13 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, save_sec_context | SECURITY_LOCAL_USERID_CHANGE | SECURITY_NOFORCE_RLS); - /* Finally we can run the query. */ + /* + * Finally we can run the query. + * + * Set fire_triggers to false to ensure that AFTER triggers are queued in + * the outer query's after-trigger context and fire after all RI updates + * on the same row are complete, rather than immediately. + */ spi_result = SPI_execute_snapshot(qplan, vals, nulls, test_snapshot, crosscheck_snapshot, @@ -2615,6 +2765,735 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, return SPI_processed != 0; } +/* + * ri_FastPathCheck + * Perform per row FK existence check via direct index probe, + * bypassing SPI. + * + * If no matching PK row exists, report the violation via ri_ReportViolation(), + * otherwise, the function returns normally. + * + * Note: This is only used by the ALTER TABLE validation path. Other paths use + * ri_FastPathBatchAdd(). + */ +static void +ri_FastPathCheck(RI_ConstraintInfo *riinfo, + Relation fk_rel, TupleTableSlot *newslot) +{ + Relation pk_rel; + Relation idx_rel; + IndexScanDesc scandesc; + TupleTableSlot *slot; + Datum pk_vals[INDEX_MAX_KEYS]; + char pk_nulls[INDEX_MAX_KEYS]; + ScanKeyData skey[INDEX_MAX_KEYS]; + bool found = false; + Oid saved_userid; + int saved_sec_context; + Snapshot snapshot; + + /* + * Advance the command counter so the snapshot sees the effects of prior + * triggers in this statement. Mirrors what the SPI path does in + * ri_PerformCheck(). + */ + CommandCounterIncrement(); + snapshot = RegisterSnapshot(GetTransactionSnapshot()); + + pk_rel = table_open(riinfo->pk_relid, RowShareLock); + idx_rel = index_open(riinfo->conindid, AccessShareLock); + + slot = table_slot_create(pk_rel, NULL); + scandesc = index_beginscan(pk_rel, idx_rel, + snapshot, NULL, + riinfo->nkeys, 0, + SO_NONE); + + GetUserIdAndSecContext(&saved_userid, &saved_sec_context); + SetUserIdAndSecContext(RelationGetForm(pk_rel)->relowner, + saved_sec_context | + SECURITY_LOCAL_USERID_CHANGE | + SECURITY_NOFORCE_RLS); + ri_CheckPermissions(pk_rel); + + if (riinfo->fpmeta == NULL) + { + /* Reload to ensure it's valid. */ + riinfo = ri_LoadConstraintInfo(riinfo->constraint_id); + ri_populate_fastpath_metadata(riinfo, fk_rel, idx_rel); + } + Assert(riinfo->fpmeta); + ri_ExtractValues(fk_rel, newslot, riinfo, false, pk_vals, pk_nulls); + build_index_scankeys(riinfo, idx_rel, pk_vals, pk_nulls, skey); + found = ri_FastPathProbeOne(pk_rel, idx_rel, scandesc, slot, + snapshot, riinfo, skey, riinfo->nkeys); + SetUserIdAndSecContext(saved_userid, saved_sec_context); + index_endscan(scandesc); + ExecDropSingleTupleTableSlot(slot); + UnregisterSnapshot(snapshot); + + if (!found) + ri_ReportViolation(riinfo, pk_rel, fk_rel, + newslot, NULL, + RI_PLAN_CHECK_LOOKUPPK, false, false); + + index_close(idx_rel, NoLock); + table_close(pk_rel, NoLock); +} + +/* + * ri_FastPathBatchAdd + * Buffer a FK row for batched probing. + * + * Adds the row to the batch buffer. When the buffer is full, flushes all + * buffered rows by probing the PK index. Any violation is reported + * immediately during the flush via ri_ReportViolation (which does not return). + * + * Uses the per-batch cache (RI_FastPathEntry) to avoid per-row relation + * open/close, slot creation, etc. + * + * The batch is also flushed at end of trigger-firing cycle via + * ri_FastPathEndBatch(). + */ +static void +ri_FastPathBatchAdd(RI_ConstraintInfo *riinfo, + Relation fk_rel, TupleTableSlot *newslot) +{ + RI_FastPathEntry *fpentry = ri_FastPathGetEntry(riinfo, fk_rel); + MemoryContext oldcxt; + + oldcxt = MemoryContextSwitchTo(fpentry->flush_cxt); + fpentry->batch[fpentry->batch_count] = + ExecCopySlotHeapTuple(newslot); + fpentry->batch_count++; + MemoryContextSwitchTo(oldcxt); + + if (fpentry->batch_count >= RI_FASTPATH_BATCH_SIZE) + ri_FastPathBatchFlush(fpentry, fk_rel, riinfo); +} + +/* + * ri_FastPathBatchFlush + * Flush all buffered FK rows by probing the PK index. + * + * Dispatches to ri_FastPathFlushArray() for single-column FKs + * (using SK_SEARCHARRAY) or ri_FastPathFlushLoop() for multi-column + * FKs (per-row probing). Violations are reported immediately via + * ri_ReportViolation(), which does not return. + */ +static void +ri_FastPathBatchFlush(RI_FastPathEntry *fpentry, Relation fk_rel, + RI_ConstraintInfo *riinfo) +{ + Relation pk_rel = fpentry->pk_rel; + Relation idx_rel = fpentry->idx_rel; + TupleTableSlot *fk_slot = fpentry->fk_slot; + Snapshot snapshot; + IndexScanDesc scandesc; + Oid saved_userid; + int saved_sec_context; + MemoryContext oldcxt; + int violation_index; + + if (fpentry->batch_count == 0) + return; + + /* + * CCI and security context switch are done once for the entire batch. + * Per-row CCI is unnecessary because by the time a flush runs, all AFTER + * triggers for the buffered rows have already fired (trigger invocations + * strictly alternate per row), so a single CCI advances past all their + * effects. Per-row security context switch is unnecessary because each + * row's probe runs entirely as the PK table owner, same as the SPI path + * -- the only difference is that the SPI path sets and restores the + * context per row whereas we do it once around the whole batch. + */ + CommandCounterIncrement(); + snapshot = RegisterSnapshot(GetTransactionSnapshot()); + + /* + * build_index_scankeys() may palloc cast results for cross-type FKs. Use + * the entry's short-lived flush context so these don't accumulate across + * batches. + */ + oldcxt = MemoryContextSwitchTo(fpentry->flush_cxt); + + scandesc = index_beginscan(pk_rel, idx_rel, snapshot, NULL, + riinfo->nkeys, 0, SO_NONE); + + GetUserIdAndSecContext(&saved_userid, &saved_sec_context); + SetUserIdAndSecContext(RelationGetForm(pk_rel)->relowner, + saved_sec_context | + SECURITY_LOCAL_USERID_CHANGE | + SECURITY_NOFORCE_RLS); + + /* + * Check that the current user has permission to access pk_rel. Done here + * rather than at entry creation so that permission changes between + * flushes are respected, matching the per-row behavior of the SPI path, + * albeit checked once per flush rather than once per row, like in + * ri_FastPathCheck(). + */ + ri_CheckPermissions(pk_rel); + + if (riinfo->fpmeta == NULL) + { + /* Reload to ensure it's valid. */ + riinfo = ri_LoadConstraintInfo(riinfo->constraint_id); + ri_populate_fastpath_metadata(riinfo, fk_rel, idx_rel); + } + Assert(riinfo->fpmeta); + + /* Skip array overhead for single-row batches. */ + if (riinfo->nkeys == 1 && fpentry->batch_count > 1) + violation_index = ri_FastPathFlushArray(fpentry, fk_slot, riinfo, + fk_rel, snapshot, scandesc); + else + violation_index = ri_FastPathFlushLoop(fpentry, fk_slot, riinfo, + fk_rel, snapshot, scandesc); + + SetUserIdAndSecContext(saved_userid, saved_sec_context); + UnregisterSnapshot(snapshot); + index_endscan(scandesc); + + if (violation_index >= 0) + { + ExecStoreHeapTuple(fpentry->batch[violation_index], fk_slot, false); + ri_ReportViolation(riinfo, pk_rel, fk_rel, + fk_slot, NULL, + RI_PLAN_CHECK_LOOKUPPK, false, false); + } + + MemoryContextReset(fpentry->flush_cxt); + MemoryContextSwitchTo(oldcxt); + + /* Reset. */ + fpentry->batch_count = 0; +} + +/* + * ri_FastPathFlushLoop + * Multi-column fallback: probe the index once per buffered row. + * + * Used for composite foreign keys where SK_SEARCHARRAY does not + * apply, and also for single-row batches of single-column FKs where + * the array overhead is not worth it. + * + * Returns the index of the first violating row in the batch array, or -1 if + * all rows are valid. + */ +static int +ri_FastPathFlushLoop(RI_FastPathEntry *fpentry, TupleTableSlot *fk_slot, + const RI_ConstraintInfo *riinfo, Relation fk_rel, + Snapshot snapshot, IndexScanDesc scandesc) +{ + Relation pk_rel = fpentry->pk_rel; + Relation idx_rel = fpentry->idx_rel; + TupleTableSlot *pk_slot = fpentry->pk_slot; + Datum pk_vals[INDEX_MAX_KEYS]; + char pk_nulls[INDEX_MAX_KEYS]; + ScanKeyData skey[INDEX_MAX_KEYS]; + bool found = true; + + for (int i = 0; i < fpentry->batch_count; i++) + { + ExecStoreHeapTuple(fpentry->batch[i], fk_slot, false); + ri_ExtractValues(fk_rel, fk_slot, riinfo, false, pk_vals, pk_nulls); + build_index_scankeys(riinfo, idx_rel, pk_vals, pk_nulls, skey); + + found = ri_FastPathProbeOne(pk_rel, idx_rel, scandesc, pk_slot, + snapshot, riinfo, skey, riinfo->nkeys); + + /* Report first unmatched row */ + if (!found) + return i; + } + + /* All pass. */ + return -1; +} + +/* + * ri_FastPathFlushArray + * Single-column fast path using SK_SEARCHARRAY. + * + * Builds an array of FK values and does one index scan with + * SK_SEARCHARRAY. The index AM sorts and deduplicates the array + * internally, then walks matching leaf pages in order. Each + * matched PK tuple is locked and rechecked as before; a matched[] + * bitmap tracks which batch items were satisfied. + * + * Returns the index of the first violating row in the batch array, or -1 if + * all rows are valid. + */ +static int +ri_FastPathFlushArray(RI_FastPathEntry *fpentry, TupleTableSlot *fk_slot, + const RI_ConstraintInfo *riinfo, Relation fk_rel, + Snapshot snapshot, IndexScanDesc scandesc) +{ + FastPathMeta *fpmeta = riinfo->fpmeta; + Relation pk_rel = fpentry->pk_rel; + Relation idx_rel = fpentry->idx_rel; + TupleTableSlot *pk_slot = fpentry->pk_slot; + Datum search_vals[RI_FASTPATH_BATCH_SIZE]; + bool matched[RI_FASTPATH_BATCH_SIZE]; + int nvals = fpentry->batch_count; + Datum pk_vals[INDEX_MAX_KEYS]; + char pk_nulls[INDEX_MAX_KEYS]; + ScanKeyData skey[1]; + FmgrInfo *cast_func_finfo; + FmgrInfo *eq_opr_finfo; + Oid elem_type; + int16 elem_len; + bool elem_byval; + char elem_align; + ArrayType *arr; + + Assert(fpmeta); + + memset(matched, 0, nvals * sizeof(bool)); + + /* + * Extract FK values, casting to the operator's expected input type if + * needed (e.g. int8 FK -> int4 for int48eq). + */ + cast_func_finfo = &fpmeta->cast_func_finfo[0]; + eq_opr_finfo = &fpmeta->eq_opr_finfo[0]; + for (int i = 0; i < nvals; i++) + { + ExecStoreHeapTuple(fpentry->batch[i], fk_slot, false); + ri_ExtractValues(fk_rel, fk_slot, riinfo, false, pk_vals, pk_nulls); + + /* Cast if needed (e.g. int8 FK -> numeric PK) */ + if (OidIsValid(cast_func_finfo->fn_oid)) + search_vals[i] = FunctionCall3(cast_func_finfo, + pk_vals[0], + Int32GetDatum(-1), + BoolGetDatum(false)); + else + search_vals[i] = pk_vals[0]; + } + + /* + * Array element type must match the operator's right-hand input type, + * which is what the index comparison expects on the search side. + * ri_populate_fastpath_metadata() stores exactly this via + * get_op_opfamily_properties(), which returns the operator's right-hand + * type as the subtype for cross-type operators (e.g. int8 for int48eq) + * and the common type for same-type operators. + */ + elem_type = fpmeta->subtypes[0]; + Assert(OidIsValid(elem_type)); + get_typlenbyvalalign(elem_type, &elem_len, &elem_byval, &elem_align); + + arr = construct_array(search_vals, nvals, + elem_type, elem_len, elem_byval, elem_align); + + /* + * Build scan key with SK_SEARCHARRAY. The index AM code will internally + * sort and deduplicate, then walk leaf pages in order. + * + * PK indexes are always btree, which supports SK_SEARCHARRAY. + * + * This path handles single-column FKs only, so index_attnos[0] == 1. + */ + Assert(idx_rel->rd_indam->amsearcharray); + Assert(fpmeta->index_attnos[0] == 1); + ScanKeyEntryInitialize(&skey[0], + SK_SEARCHARRAY, + fpmeta->index_attnos[0], + fpmeta->strats[0], + fpmeta->subtypes[0], + idx_rel->rd_indcollation[fpmeta->index_attnos[0] - 1], + fpmeta->regops[0], + PointerGetDatum(arr)); + + index_rescan(scandesc, skey, 1, NULL, 0); + + /* + * Walk all matches. The index AM returns them in index order. For each + * match, find which batch item(s) it satisfies. + */ + while (index_getnext_slot(scandesc, ForwardScanDirection, pk_slot)) + { + Datum found_val; + bool found_null; + bool concurrently_updated; + ScanKeyData recheck_skey[1]; + + if (!ri_LockPKTuple(pk_rel, pk_slot, snapshot, &concurrently_updated)) + continue; + + /* Extract the PK value from the matched and locked tuple */ + found_val = slot_getattr(pk_slot, riinfo->pk_attnums[0], &found_null); + Assert(!found_null); + + if (concurrently_updated) + { + /* + * Build a single-key scankey for recheck. We need the actual PK + * value that was found, not the FK search value. + */ + ScanKeyEntryInitialize(&recheck_skey[0], 0, 1, + fpmeta->strats[0], + fpmeta->subtypes[0], + idx_rel->rd_indcollation[0], + fpmeta->regops[0], + found_val); + if (!recheck_matched_pk_tuple(idx_rel, recheck_skey, 1, pk_slot)) + continue; + } + + /* + * Linear scan to mark all batch items matching this PK value. + * O(batch_size) per match, O(batch_size^2) worst case -- fine for the + * current batch size of 64. + */ + for (int i = 0; i < nvals; i++) + { + if (!matched[i] && + DatumGetBool(FunctionCall2Coll(eq_opr_finfo, + idx_rel->rd_indcollation[0], + found_val, + search_vals[i]))) + matched[i] = true; + } + } + + /* Report first unmatched row */ + for (int i = 0; i < nvals; i++) + if (!matched[i]) + return i; + + /* All pass. */ + return -1; +} + +/* + * ri_FastPathProbeOne + * Probe the PK index for one set of scan keys, lock the matching + * tuple + * + * Returns true if a matching PK row was found, locked, and (if + * applicable) visible to the transaction snapshot. + */ +static bool +ri_FastPathProbeOne(Relation pk_rel, Relation idx_rel, + IndexScanDesc scandesc, TupleTableSlot *slot, + Snapshot snapshot, const RI_ConstraintInfo *riinfo, + ScanKeyData *skey, int nkeys) +{ + bool found = false; + + index_rescan(scandesc, skey, nkeys, NULL, 0); + + if (index_getnext_slot(scandesc, ForwardScanDirection, slot)) + { + bool concurrently_updated; + + if (ri_LockPKTuple(pk_rel, slot, snapshot, + &concurrently_updated)) + { + if (concurrently_updated) + found = recheck_matched_pk_tuple(idx_rel, skey, nkeys, slot); + else + found = true; + } + } + + return found; +} + +/* + * ri_LockPKTuple + * Lock a PK tuple found by the fast-path index scan. + * + * Calls table_tuple_lock() directly with handling specific to RI checks. + * Returns true if the tuple was successfully locked. + * + * Sets *concurrently_updated to true if the locked tuple was reached + * by following an update chain (tmfd.traversed), indicating the caller + * should recheck the key. + */ +static bool +ri_LockPKTuple(Relation pk_rel, TupleTableSlot *slot, Snapshot snap, + bool *concurrently_updated) +{ + TM_FailureData tmfd; + TM_Result result; + int lockflags = TUPLE_LOCK_FLAG_LOCK_UPDATE_IN_PROGRESS; + + *concurrently_updated = false; + + if (!IsolationUsesXactSnapshot()) + lockflags |= TUPLE_LOCK_FLAG_FIND_LAST_VERSION; + + result = table_tuple_lock(pk_rel, &slot->tts_tid, snap, + slot, GetCurrentCommandId(false), + LockTupleKeyShare, LockWaitBlock, + lockflags, &tmfd); + + switch (result) + { + case TM_Ok: + if (tmfd.traversed) + *concurrently_updated = true; + return true; + + case TM_Deleted: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + return false; + + case TM_Updated: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent update"))); + + /* + * In READ COMMITTED, FIND_LAST_VERSION should have chased the + * chain and returned TM_Ok. Getting here means something + * unexpected -- fall through to error. + */ + elog(ERROR, "unexpected table_tuple_lock status: %u", result); + break; + + case TM_SelfModified: + + /* + * The current command or a later command in this transaction + * modified the PK row. This shouldn't normally happen during an + * FK check (we're not modifying pk_rel), but handle it safely by + * treating the tuple as not found. + */ + return false; + + case TM_Invisible: + elog(ERROR, "attempted to lock invisible tuple"); + break; + + default: + elog(ERROR, "unrecognized table_tuple_lock status: %u", result); + break; + } + + return false; /* keep compiler quiet */ +} + +static bool +ri_fastpath_is_applicable(const RI_ConstraintInfo *riinfo) +{ + /* + * Partitioned referenced tables are skipped for simplicity, since they + * require routing the probe through the correct partition using + * PartitionDirectory. + */ + if (riinfo->pk_is_partitioned) + return false; + + /* + * Temporal foreign keys use range overlap and containment semantics (&&, + * <@, range_agg()) that inherently involve aggregation and multiple-row + * reasoning, so they stay on the SPI path. + */ + if (riinfo->hasperiod) + return false; + + return true; +} + +/* + * ri_CheckPermissions + * Check that the current user has permissions to look into the schema of + * and SELECT from 'query_rel' + */ +static void +ri_CheckPermissions(Relation query_rel) +{ + AclResult aclresult; + + /* USAGE on schema. */ + aclresult = object_aclcheck(NamespaceRelationId, + RelationGetNamespace(query_rel), + GetUserId(), ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(RelationGetNamespace(query_rel))); + + /* SELECT on relation. */ + aclresult = pg_class_aclcheck(RelationGetRelid(query_rel), GetUserId(), + ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLE, + RelationGetRelationName(query_rel)); +} + +/* + * recheck_matched_pk_tuple + * After following an update chain (tmfd.traversed), verify that + * the locked PK tuple still matches the original search keys. + * + * A non-key update (e.g. changing a non-PK column) creates a new tuple version + * that we've now locked, but the key is unchanged -- that's fine. A key + * update means the value we were looking for is gone, so we should treat it as + * not found. + */ +static bool +recheck_matched_pk_tuple(Relation idxrel, ScanKeyData *skeys, int nkeys, + TupleTableSlot *new_slot) +{ + /* + * TODO: BuildIndexInfo does a syscache lookup + palloc on every call. + * This only fires on the concurrent-update path (tmfd.traversed), which + * should be rare, so the cost is acceptable for now. If profiling shows + * otherwise, cache the IndexInfo in FastPathMeta. + */ + IndexInfo *indexInfo = BuildIndexInfo(idxrel); + Datum values[INDEX_MAX_KEYS]; + bool isnull[INDEX_MAX_KEYS]; + bool matched = true; + + /* PK indexes never have these. */ + Assert(indexInfo->ii_Expressions == NIL && + indexInfo->ii_ExclusionOps == NULL); + + /* Form the index values and isnull flags given the table tuple. */ + Assert(nkeys == indexInfo->ii_NumIndexKeyAttrs); + FormIndexDatum(indexInfo, new_slot, NULL, values, isnull); + for (int i = 0; i < nkeys; i++) + { + ScanKeyData *skey = &skeys[i]; + + /* A PK column can never be set to NULL. */ + Assert(!isnull[i]); + if (!DatumGetBool(FunctionCall2Coll(&skey->sk_func, + skey->sk_collation, + values[i], + skey->sk_argument))) + { + matched = false; + break; + } + } + + return matched; +} + +/* + * build_index_scankeys + * Build ScanKeys for a direct index probe of the PK's unique index. + * + * Uses cached compare entries, operator procedures, and strategy numbers + * from ri_populate_fastpath_metadata() rather than looking them up on + * each invocation. Casts FK values to the operator's expected input + * type if needed. + */ +static void +build_index_scankeys(const RI_ConstraintInfo *riinfo, + Relation idx_rel, Datum *pk_vals, + char *pk_nulls, ScanKey skeys) +{ + FastPathMeta *fpmeta = riinfo->fpmeta; + + Assert(fpmeta); + + /* + * May need to cast each of the individual values of the foreign key to + * the corresponding PK column's type if the equality operator demands it. + */ + for (int i = 0; i < riinfo->nkeys; i++) + { + if (pk_nulls[i] != 'n' && + OidIsValid(fpmeta->cast_func_finfo[i].fn_oid)) + pk_vals[i] = FunctionCall3(&fpmeta->cast_func_finfo[i], + pk_vals[i], + Int32GetDatum(-1), /* typmod */ + BoolGetDatum(false)); /* implicit coercion */ + } + + /* + * Set up ScanKeys for the index scan. This is essentially how + * ExecIndexBuildScanKeys() sets them up. Use the cached index_attnos and + * the corresponding collation since FK columns may be in a different + * order than PK index columns. Place each scan key at the array position + * corresponding to its index column, since btree requires keys to be + * ordered by attribute number. + */ + for (int i = 0; i < riinfo->nkeys; i++) + { + AttrNumber pkattrno = fpmeta->index_attnos[i]; + int skey_pos = pkattrno - 1; /* 0-based array position */ + + ScanKeyEntryInitialize(&skeys[skey_pos], 0, pkattrno, + fpmeta->strats[i], fpmeta->subtypes[i], + idx_rel->rd_indcollation[skey_pos], fpmeta->regops[i], + pk_vals[i]); + } +} + +/* + * ri_populate_fastpath_metadata + * Cache per-key metadata needed by build_index_scankeys(). + * + * Looks up the compare hash entry, operator procedure OID, and index + * strategy/subtype for each key column. Called lazily on first use + * and persists for the lifetime of the RI_ConstraintInfo entry. + */ +static void +ri_populate_fastpath_metadata(RI_ConstraintInfo *riinfo, + Relation fk_rel, Relation idx_rel) +{ + FastPathMeta *fpmeta; + MemoryContext oldcxt = MemoryContextSwitchTo(TopMemoryContext); + + Assert(riinfo != NULL && riinfo->valid); + + fpmeta = palloc_object(FastPathMeta); + for (int i = 0; i < riinfo->nkeys; i++) + { + Oid eq_opr = riinfo->pf_eq_oprs[i]; + Oid typeid = RIAttType(fk_rel, riinfo->fk_attnums[i]); + Oid lefttype; + RI_CompareHashEntry *entry = ri_HashCompareOp(eq_opr, typeid); + int idx_col; + + /* + * Find the index column position for this constraint key. The FK + * constraint may reference columns in a different order than they + * appear in the PK index, so we must map pk_attnums[i] to the + * corresponding index column position. + */ + for (idx_col = 0; idx_col < riinfo->nkeys; idx_col++) + { + if (idx_rel->rd_index->indkey.values[idx_col] == riinfo->pk_attnums[i]) + break; + } + Assert(idx_col < riinfo->nkeys); + + /* 1-based attribute number */ + fpmeta->index_attnos[i] = idx_col + 1; + + fmgr_info_copy(&fpmeta->cast_func_finfo[i], &entry->cast_func_finfo, + CurrentMemoryContext); + fmgr_info_copy(&fpmeta->eq_opr_finfo[i], &entry->eq_opr_finfo, + CurrentMemoryContext); + fpmeta->regops[i] = get_opcode(eq_opr); + + get_op_opfamily_properties(eq_opr, + idx_rel->rd_opfamily[idx_col], + false, + &fpmeta->strats[i], + &lefttype, + &fpmeta->subtypes[i]); + } + + riinfo->fpmeta = fpmeta; + MemoryContextSwitchTo(oldcxt); +} + /* * Extract fields from a tuple into Datum/nulls arrays */ @@ -3110,8 +3989,11 @@ ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid, /* * ri_HashCompareOp - * - * See if we know how to compare two values, and create a new hash entry - * if not. + * Look up or create a cache entry for the given equality operator and + * the caller's value type (typeid). The entry holds the operator's + * FmgrInfo and, if typeid doesn't match what the operator expects as + * its right-hand input, a cast function to coerce the value before + * comparison. */ static RI_CompareHashEntry * ri_HashCompareOp(Oid eq_opr, Oid typeid) @@ -3167,8 +4049,14 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid) * moment since that will never be generated for implicit coercions. */ op_input_types(eq_opr, &lefttype, &righttype); - Assert(lefttype == righttype); - if (typeid == lefttype) + + /* + * pf_eq_oprs (used by the fast path) can be cross-type when the FK + * and PK columns differ in type, e.g. int48eq for int4 PK / int8 FK. + * If the FK column's type already matches what the operator expects + * as its right-hand input, no cast is needed. + */ + if (typeid == righttype) castfunc = InvalidOid; /* simplest case */ else { @@ -3230,3 +4118,196 @@ RI_FKey_trigger_type(Oid tgfoid) return RI_TRIGGER_NONE; } + +/* + * ri_FastPathEndBatch + * Flush remaining rows and tear down cached state. + * + * Registered as an AfterTriggerBatchCallback. Note: the flush can + * do real work (CCI, security context switch, index probes) and can + * throw ERROR on a constraint violation. If that happens, + * ri_FastPathTeardown never runs; ResourceOwner + XactCallback + * handle resource cleanup on the abort path. + */ +static void +ri_FastPathEndBatch(void *arg) +{ + HASH_SEQ_STATUS status; + RI_FastPathEntry *entry; + + if (ri_fastpath_cache == NULL) + return; + + /* Flush any partial batches -- can throw ERROR */ + hash_seq_init(&status, ri_fastpath_cache); + while ((entry = hash_seq_search(&status)) != NULL) + { + if (entry->batch_count > 0) + { + Relation fk_rel = table_open(entry->fk_relid, AccessShareLock); + RI_ConstraintInfo *riinfo = ri_LoadConstraintInfo(entry->conoid); + + ri_FastPathBatchFlush(entry, fk_rel, riinfo); + table_close(fk_rel, NoLock); + } + } + + ri_FastPathTeardown(); +} + +/* + * ri_FastPathTeardown + * Tear down all cached fast-path state. + * + * Called from ri_FastPathEndBatch() after flushing any remaining rows. + */ +static void +ri_FastPathTeardown(void) +{ + HASH_SEQ_STATUS status; + RI_FastPathEntry *entry; + + if (ri_fastpath_cache == NULL) + return; + + hash_seq_init(&status, ri_fastpath_cache); + while ((entry = hash_seq_search(&status)) != NULL) + { + if (entry->idx_rel) + index_close(entry->idx_rel, NoLock); + if (entry->pk_rel) + table_close(entry->pk_rel, NoLock); + if (entry->pk_slot) + ExecDropSingleTupleTableSlot(entry->pk_slot); + if (entry->fk_slot) + ExecDropSingleTupleTableSlot(entry->fk_slot); + if (entry->flush_cxt) + MemoryContextDelete(entry->flush_cxt); + } + + hash_destroy(ri_fastpath_cache); + ri_fastpath_cache = NULL; + ri_fastpath_callback_registered = false; +} + +static bool ri_fastpath_xact_callback_registered = false; + +static void +ri_FastPathXactCallback(XactEvent event, void *arg) +{ + /* + * On abort, ResourceOwner already released relations; on commit, + * ri_FastPathTeardown already ran. Either way, just NULL the static + * pointers so they don't dangle into the next transaction. + */ + ri_fastpath_cache = NULL; + ri_fastpath_callback_registered = false; +} + +static void +ri_FastPathSubXactCallback(SubXactEvent event, SubTransactionId mySubid, + SubTransactionId parentSubid, void *arg) +{ + if (event == SUBXACT_EVENT_ABORT_SUB) + { + /* + * ResourceOwner already released relations. NULL the static pointers + * so the still-registered batch callback becomes a no-op for the rest + * of this transaction. + */ + ri_fastpath_cache = NULL; + ri_fastpath_callback_registered = false; + } +} + +/* + * ri_FastPathGetEntry + * Look up or create a per-batch cache entry for the given constraint. + * + * On first call for a constraint within a batch: opens pk_rel and the index, + * allocates slots for both FK row and the looked up PK row, and registers the + * cleanup callback. + * + * On subsequent calls: returns the existing entry. + */ +static RI_FastPathEntry * +ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, Relation fk_rel) +{ + RI_FastPathEntry *entry; + bool found; + + /* Create hash table on first use in this batch */ + if (ri_fastpath_cache == NULL) + { + HASHCTL ctl; + + if (!ri_fastpath_xact_callback_registered) + { + RegisterXactCallback(ri_FastPathXactCallback, NULL); + RegisterSubXactCallback(ri_FastPathSubXactCallback, NULL); + ri_fastpath_xact_callback_registered = true; + } + + ctl.keysize = sizeof(Oid); + ctl.entrysize = sizeof(RI_FastPathEntry); + ctl.hcxt = TopTransactionContext; + ri_fastpath_cache = hash_create("RI fast-path cache", + 16, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + } + + entry = hash_search(ri_fastpath_cache, &riinfo->constraint_id, + HASH_ENTER, &found); + + if (!found) + { + MemoryContext oldcxt; + + /* + * Zero out non-key fields so ri_FastPathTeardown is safe if we error + * out during partial initialization below. + */ + memset(((char *) entry) + offsetof(RI_FastPathEntry, pk_rel), 0, + sizeof(RI_FastPathEntry) - offsetof(RI_FastPathEntry, pk_rel)); + + oldcxt = MemoryContextSwitchTo(TopTransactionContext); + + entry->fk_relid = RelationGetRelid(fk_rel); + + /* + * Open PK table and its unique index. + * + * RowShareLock on pk_rel matches what the SPI path's SELECT ... FOR + * KEY SHARE would acquire as a relation-level lock. AccessShareLock + * on the index is standard for index scans. + * + * We don't release these locks until end of transaction, matching SPI + * behavior. + */ + entry->pk_rel = table_open(riinfo->pk_relid, RowShareLock); + entry->idx_rel = index_open(riinfo->conindid, AccessShareLock); + entry->pk_slot = table_slot_create(entry->pk_rel, NULL); + + /* + * Must be TTSOpsHeapTuple because ExecStoreHeapTuple() is used to + * load entries from batch[] into this slot for value extraction. + */ + entry->fk_slot = MakeSingleTupleTableSlot(RelationGetDescr(fk_rel), + &TTSOpsHeapTuple); + + entry->flush_cxt = AllocSetContextCreate(TopTransactionContext, + "RI fast path flush temporary context", + ALLOCSET_SMALL_SIZES); + MemoryContextSwitchTo(oldcxt); + + /* Ensure cleanup at end of this trigger-firing batch */ + if (!ri_fastpath_callback_registered) + { + RegisterAfterTriggerBatchCallback(ri_FastPathEndBatch, NULL); + ri_fastpath_callback_registered = true; + } + } + + return entry; +} diff --git a/src/backend/utils/adt/rowtypes.c b/src/backend/utils/adt/rowtypes.c index fe5edc0027da3..e4eb7111ee738 100644 --- a/src/backend/utils/adt/rowtypes.c +++ b/src/backend/utils/adt/rowtypes.c @@ -3,7 +3,7 @@ * rowtypes.c * I/O and comparison functions for generic composite types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -140,8 +140,8 @@ record_in(PG_FUNCTION_ARGS) my_extra->ncolumns = ncolumns; } - values = (Datum *) palloc(ncolumns * sizeof(Datum)); - nulls = (bool *) palloc(ncolumns * sizeof(bool)); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); /* * Scan the string. We use "buf" to accumulate the de-quoted data for @@ -383,8 +383,8 @@ record_out(PG_FUNCTION_ARGS) my_extra->ncolumns = ncolumns; } - values = (Datum *) palloc(ncolumns * sizeof(Datum)); - nulls = (bool *) palloc(ncolumns * sizeof(bool)); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); /* Break down the tuple into fields */ heap_deform_tuple(&tuple, tupdesc, values, nulls); @@ -539,8 +539,8 @@ record_recv(PG_FUNCTION_ARGS) my_extra->ncolumns = ncolumns; } - values = (Datum *) palloc(ncolumns * sizeof(Datum)); - nulls = (bool *) palloc(ncolumns * sizeof(bool)); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); /* Fetch number of columns user thinks it has */ usercols = pq_getmsgint(buf, 4); @@ -741,8 +741,8 @@ record_send(PG_FUNCTION_ARGS) my_extra->ncolumns = ncolumns; } - values = (Datum *) palloc(ncolumns * sizeof(Datum)); - nulls = (bool *) palloc(ncolumns * sizeof(bool)); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); /* Break down the tuple into fields */ heap_deform_tuple(&tuple, tupdesc, values, nulls); @@ -1515,8 +1515,8 @@ record_image_cmp(FunctionCallInfo fcinfo) { Size len1, len2; - struct varlena *arg1val; - struct varlena *arg2val; + varlena *arg1val; + varlena *arg2val; len1 = toast_raw_datum_size(values1[i1]); len2 = toast_raw_datum_size(values2[i2]); @@ -1529,9 +1529,9 @@ record_image_cmp(FunctionCallInfo fcinfo) if ((cmpresult == 0) && (len1 != len2)) cmpresult = (len1 < len2) ? -1 : 1; - if ((Pointer) arg1val != (Pointer) values1[i1]) + if (arg1val != DatumGetPointer(values1[i1])) pfree(arg1val); - if ((Pointer) arg2val != (Pointer) values2[i2]) + if (arg2val != DatumGetPointer(values2[i2])) pfree(arg2val); } else @@ -1863,8 +1863,8 @@ hash_record(PG_FUNCTION_ARGS) } /* Break down the tuple into fields */ - values = (Datum *) palloc(ncolumns * sizeof(Datum)); - nulls = (bool *) palloc(ncolumns * sizeof(bool)); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); heap_deform_tuple(&tuple, tupdesc, values, nulls); for (int i = 0; i < ncolumns; i++) @@ -1984,8 +1984,8 @@ hash_record_extended(PG_FUNCTION_ARGS) } /* Break down the tuple into fields */ - values = (Datum *) palloc(ncolumns * sizeof(Datum)); - nulls = (bool *) palloc(ncolumns * sizeof(bool)); + values = palloc_array(Datum, ncolumns); + nulls = palloc_array(bool, ncolumns); heap_deform_tuple(&tuple, tupdesc, values, nulls); for (int i = 0; i < ncolumns; i++) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd21b..78587d223cb62 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -4,7 +4,7 @@ * Functions to convert stored expressions/querytrees back to * source text * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -34,6 +34,11 @@ #include "catalog/pg_operator.h" #include "catalog/pg_partitioned_table.h" #include "catalog/pg_proc.h" +#include "catalog/pg_propgraph_element.h" +#include "catalog/pg_propgraph_element_label.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_label_property.h" +#include "catalog/pg_propgraph_property.h" #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" @@ -361,6 +366,9 @@ static char *pg_get_indexdef_worker(Oid indexrelid, int colno, bool attrsOnly, bool keysOnly, bool showTblSpc, bool inherits, int prettyFlags, bool missing_ok); +static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind); +static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid); +static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid); static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok); static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags, @@ -426,6 +434,7 @@ static void get_update_query_targetlist_def(Query *query, List *targetList, static void get_delete_query_def(Query *query, deparse_context *context); static void get_merge_query_def(Query *query, deparse_context *context); static void get_utility_query_def(Query *query, deparse_context *context); +static char *get_lock_clause_strength(LockClauseStrength strength); static void get_basic_select_query(Query *query, deparse_context *context); static void get_target_list(List *targetList, deparse_context *context); static void get_returning_clause(Query *query, deparse_context *context); @@ -515,6 +524,8 @@ static void get_rte_alias(RangeTblEntry *rte, int varno, bool use_as, deparse_context *context); static void get_column_alias_list(deparse_columns *colinfo, deparse_context *context); +static void get_for_portion_of(ForPortionOfExpr *forPortionOf, + deparse_context *context); static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_columns *colinfo, deparse_context *context); @@ -536,7 +547,7 @@ static void add_cast_to(StringInfo buf, Oid typid); static char *generate_qualified_type_name(Oid typid); static text *string_to_text(char *str); static char *flatten_reloptions(Oid relid); -static void get_reloptions(StringInfo buf, Datum reloptions); +void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); static void get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan, @@ -1246,7 +1257,7 @@ pg_get_indexdef_columns(Oid indexrelid, bool pretty) /* Internal version, extensible with flags to control its behavior */ char * -pg_get_indexdef_columns_extended(Oid indexrelid, bits16 flags) +pg_get_indexdef_columns_extended(Oid indexrelid, uint16 flags) { bool pretty = ((flags & RULE_INDEXDEF_PRETTY) != 0); bool keys_only = ((flags & RULE_INDEXDEF_KEYS_ONLY) != 0); @@ -1281,7 +1292,7 @@ pg_get_indexdef_worker(Oid indexrelid, int colno, Form_pg_index idxrec; Form_pg_class idxrelrec; Form_pg_am amrec; - IndexAmRoutine *amroutine; + const IndexAmRoutine *amroutine; List *indexprs; ListCell *indexpr_item; List *context; @@ -1600,6 +1611,354 @@ pg_get_querydef(Query *query, bool pretty) return buf.data; } +/* + * pg_get_propgraphdef - get the definition of a property graph + */ +Datum +pg_get_propgraphdef(PG_FUNCTION_ARGS) +{ + Oid pgrelid = PG_GETARG_OID(0); + StringInfoData buf; + HeapTuple classtup; + Form_pg_class classform; + char *name; + char *nsp; + + initStringInfo(&buf); + + classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(pgrelid)); + if (!HeapTupleIsValid(classtup)) + PG_RETURN_NULL(); + + classform = (Form_pg_class) GETSTRUCT(classtup); + name = NameStr(classform->relname); + + if (classform->relkind != RELKIND_PROPGRAPH) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a property graph", name))); + + nsp = get_namespace_name(classform->relnamespace); + + appendStringInfo(&buf, "CREATE PROPERTY GRAPH %s", + quote_qualified_identifier(nsp, name)); + + ReleaseSysCache(classtup); + + make_propgraphdef_elements(&buf, pgrelid, PGEKIND_VERTEX); + make_propgraphdef_elements(&buf, pgrelid, PGEKIND_EDGE); + + PG_RETURN_TEXT_P(string_to_text(buf.data)); +} + +/* + * Generates a VERTEX TABLES (...) or EDGE TABLES (...) clause. Pass in the + * property graph relation OID and the element kind (vertex or edge). Result + * is appended to buf. + */ +static void +make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind) +{ + Relation pgerel; + ScanKeyData scankey[1]; + SysScanDesc scan; + bool first; + HeapTuple tup; + + pgerel = table_open(PropgraphElementRelationId, AccessShareLock); + + ScanKeyInit(&scankey[0], + Anum_pg_propgraph_element_pgepgid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(pgrelid)); + + scan = systable_beginscan(pgerel, PropgraphElementAliasIndexId, true, NULL, 1, scankey); + + first = true; + while ((tup = systable_getnext(scan))) + { + Form_pg_propgraph_element pgeform = (Form_pg_propgraph_element) GETSTRUCT(tup); + char *relname; + Datum datum; + bool isnull; + + if (pgeform->pgekind != pgekind) + continue; + + if (first) + { + appendStringInfo(buf, "\n %s TABLES (\n", pgekind == PGEKIND_VERTEX ? "VERTEX" : "EDGE"); + first = false; + } + else + appendStringInfoString(buf, ",\n"); + + relname = get_rel_name(pgeform->pgerelid); + if (relname && strcmp(relname, NameStr(pgeform->pgealias)) == 0) + appendStringInfo(buf, " %s", + generate_relation_name(pgeform->pgerelid, NIL)); + else + appendStringInfo(buf, " %s AS %s", + generate_relation_name(pgeform->pgerelid, NIL), + quote_identifier(NameStr(pgeform->pgealias))); + + datum = heap_getattr(tup, Anum_pg_propgraph_element_pgekey, RelationGetDescr(pgerel), &isnull); + if (!isnull) + { + appendStringInfoString(buf, " KEY ("); + decompile_column_index_array(datum, pgeform->pgerelid, false, buf); + appendStringInfoChar(buf, ')'); + } + else + elog(ERROR, "null pgekey for element %u", pgeform->oid); + + if (pgekind == PGEKIND_EDGE) + { + Datum srckey; + Datum srcref; + Datum destkey; + Datum destref; + HeapTuple tup2; + Form_pg_propgraph_element pgeform2; + + datum = heap_getattr(tup, Anum_pg_propgraph_element_pgesrckey, RelationGetDescr(pgerel), &isnull); + srckey = isnull ? 0 : datum; + datum = heap_getattr(tup, Anum_pg_propgraph_element_pgesrcref, RelationGetDescr(pgerel), &isnull); + srcref = isnull ? 0 : datum; + datum = heap_getattr(tup, Anum_pg_propgraph_element_pgedestkey, RelationGetDescr(pgerel), &isnull); + destkey = isnull ? 0 : datum; + datum = heap_getattr(tup, Anum_pg_propgraph_element_pgedestref, RelationGetDescr(pgerel), &isnull); + destref = isnull ? 0 : datum; + + appendStringInfoString(buf, " SOURCE"); + tup2 = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(pgeform->pgesrcvertexid)); + if (!tup2) + elog(ERROR, "cache lookup failed for property graph element %u", pgeform->pgesrcvertexid); + pgeform2 = (Form_pg_propgraph_element) GETSTRUCT(tup2); + if (srckey) + { + appendStringInfoString(buf, " KEY ("); + decompile_column_index_array(srckey, pgeform->pgerelid, false, buf); + appendStringInfo(buf, ") REFERENCES %s (", quote_identifier(NameStr(pgeform2->pgealias))); + decompile_column_index_array(srcref, pgeform2->pgerelid, false, buf); + appendStringInfoChar(buf, ')'); + } + else + appendStringInfo(buf, " %s ", quote_identifier(NameStr(pgeform2->pgealias))); + ReleaseSysCache(tup2); + + appendStringInfoString(buf, " DESTINATION"); + tup2 = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(pgeform->pgedestvertexid)); + if (!tup2) + elog(ERROR, "cache lookup failed for property graph element %u", pgeform->pgedestvertexid); + pgeform2 = (Form_pg_propgraph_element) GETSTRUCT(tup2); + if (destkey) + { + appendStringInfoString(buf, " KEY ("); + decompile_column_index_array(destkey, pgeform->pgerelid, false, buf); + appendStringInfo(buf, ") REFERENCES %s (", quote_identifier(NameStr(pgeform2->pgealias))); + decompile_column_index_array(destref, pgeform2->pgerelid, false, buf); + appendStringInfoChar(buf, ')'); + } + else + appendStringInfo(buf, " %s", quote_identifier(NameStr(pgeform2->pgealias))); + ReleaseSysCache(tup2); + } + + make_propgraphdef_labels(buf, pgeform->oid, NameStr(pgeform->pgealias), pgeform->pgerelid); + } + if (!first) + appendStringInfoString(buf, "\n )"); + + systable_endscan(scan); + table_close(pgerel, AccessShareLock); +} + +struct oid_str_pair +{ + Oid oid; + char *str; +}; + +static int +list_oid_str_pair_cmp_by_str(const ListCell *p1, const ListCell *p2) +{ + struct oid_str_pair *v1 = lfirst(p1); + struct oid_str_pair *v2 = lfirst(p2); + + return strcmp(v1->str, v2->str); +} + +/* + * Generates label and properties list. Pass in the element OID, the element + * alias, and the graph relation OID. Result is appended to buf. + */ +static void +make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid) +{ + Relation pglrel; + ScanKeyData scankey[1]; + SysScanDesc scan; + int count; + HeapTuple tup; + List *label_list = NIL; + + pglrel = table_open(PropgraphElementLabelRelationId, AccessShareLock); + + ScanKeyInit(&scankey[0], + Anum_pg_propgraph_element_label_pgelelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(elid)); + + /* + * We want to output the labels in a deterministic order. So we first + * read all the data, then sort, then print it. + */ + scan = systable_beginscan(pglrel, PropgraphElementLabelElementLabelIndexId, true, NULL, 1, scankey); + + while ((tup = systable_getnext(scan))) + { + Form_pg_propgraph_element_label pgelform = (Form_pg_propgraph_element_label) GETSTRUCT(tup); + struct oid_str_pair *osp; + + osp = palloc_object(struct oid_str_pair); + osp->oid = pgelform->oid; + osp->str = get_propgraph_label_name(pgelform->pgellabelid); + + label_list = lappend(label_list, osp); + } + + systable_endscan(scan); + table_close(pglrel, AccessShareLock); + + count = list_length(label_list); + + /* Each element has at least one label. */ + Assert(count > 0); + + /* + * It is enough for the comparison function to compare just labels, since + * all the labels of an element table should have distinct names. + */ + list_sort(label_list, list_oid_str_pair_cmp_by_str); + + foreach_ptr(struct oid_str_pair, osp, label_list) + { + if (strcmp(osp->str, elalias) == 0) + { + /* If the default label is the only label, don't print anything. */ + if (count != 1) + appendStringInfoString(buf, " DEFAULT LABEL"); + } + else + appendStringInfo(buf, " LABEL %s", quote_identifier(osp->str)); + + make_propgraphdef_properties(buf, osp->oid, elrelid); + } +} + +/* + * Helper function for make_propgraphdef_properties(): Sort (propname, expr) + * pairs by name. + */ +static int +propdata_by_name_cmp(const ListCell *a, const ListCell *b) +{ + List *la = lfirst_node(List, a); + List *lb = lfirst_node(List, b); + char *pna = strVal(linitial(la)); + char *pnb = strVal(linitial(lb)); + + return strcmp(pna, pnb); +} + +/* + * Generates element table properties clause (PROPERTIES (...) or NO + * PROPERTIES). Pass in label OID and element table OID. Result is appended + * to buf. + */ +static void +make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid) +{ + Relation plprel; + ScanKeyData scankey[1]; + SysScanDesc scan; + HeapTuple tup; + List *outlist = NIL; + + plprel = table_open(PropgraphLabelPropertyRelationId, AccessShareLock); + + ScanKeyInit(&scankey[0], + Anum_pg_propgraph_label_property_plpellabelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ellabelid)); + + /* + * We want to output the properties in a deterministic order. So we first + * read all the data, then sort, then print it. + */ + scan = systable_beginscan(plprel, PropgraphLabelPropertyLabelPropIndexId, true, NULL, 1, scankey); + + while ((tup = systable_getnext(scan))) + { + Form_pg_propgraph_label_property plpform = (Form_pg_propgraph_label_property) GETSTRUCT(tup); + Datum exprDatum; + bool isnull; + char *tmp; + Node *expr; + char *propname; + + exprDatum = heap_getattr(tup, Anum_pg_propgraph_label_property_plpexpr, RelationGetDescr(plprel), &isnull); + Assert(!isnull); + tmp = TextDatumGetCString(exprDatum); + expr = stringToNode(tmp); + pfree(tmp); + + propname = get_propgraph_property_name(plpform->plppropid); + + outlist = lappend(outlist, list_make2(makeString(propname), expr)); + } + + systable_endscan(scan); + table_close(plprel, AccessShareLock); + + list_sort(outlist, propdata_by_name_cmp); + + if (outlist) + { + List *context; + ListCell *lc; + bool first = true; + + context = deparse_context_for(get_relation_name(elrelid), elrelid); + + appendStringInfoString(buf, " PROPERTIES ("); + + foreach(lc, outlist) + { + List *data = lfirst_node(List, lc); + char *propname = strVal(linitial(data)); + Node *expr = lsecond(data); + + if (first) + first = false; + else + appendStringInfoString(buf, ", "); + + if (IsA(expr, Var) && strcmp(propname, get_attname(elrelid, castNode(Var, expr)->varattno, false)) == 0) + appendStringInfoString(buf, quote_identifier(propname)); + else + appendStringInfo(buf, "%s AS %s", + deparse_expression_pretty(expr, context, false, false, 0, 0), + quote_identifier(propname)); + } + + appendStringInfoChar(buf, ')'); + } + else + appendStringInfoString(buf, " NO PROPERTIES"); +} + /* * pg_get_statisticsobjdef * Get the definition of an extended statistics object @@ -1620,7 +1979,6 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS) /* * Internal version for use by ALTER TABLE. - * Includes a tablespace clause in the result. * Returns a palloc'd C string; no pretty-printing. */ char * @@ -3088,7 +3446,8 @@ pg_get_functiondef(PG_FUNCTION_ARGS) * string literals. (The elements may be double-quoted as-is, * but we can't just feed them to the SQL parser; it would do * the wrong thing with elements that are zero-length or - * longer than NAMEDATALEN.) + * longer than NAMEDATALEN.) Also, we need a special case for + * empty lists. * * Variables that are not so marked should just be emitted as * simple string literals. If the variable is not known to @@ -3106,6 +3465,9 @@ pg_get_functiondef(PG_FUNCTION_ARGS) /* this shouldn't fail really */ elog(ERROR, "invalid list syntax in proconfig item"); } + /* Special case: represent an empty list as NULL */ + if (namelist == NIL) + appendStringInfoString(&buf, "NULL"); foreach(lc, namelist) { char *curname = (char *) lfirst(lc); @@ -3710,7 +4072,7 @@ deparse_context_for(const char *aliasname, Oid relid) deparse_namespace *dpns; RangeTblEntry *rte; - dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace)); + dpns = palloc0_object(deparse_namespace); /* Build a minimal RTE for the rel */ rte = makeNode(RangeTblEntry); @@ -3754,7 +4116,7 @@ deparse_context_for_plan_tree(PlannedStmt *pstmt, List *rtable_names) { deparse_namespace *dpns; - dpns = (deparse_namespace *) palloc0(sizeof(deparse_namespace)); + dpns = palloc0_object(deparse_namespace); /* Initialize fields that stay the same across the whole plan tree */ dpns->rtable = pstmt->rtable; @@ -5183,10 +5545,10 @@ set_deparse_plan(deparse_namespace *dpns, Plan *plan) * source, and all INNER_VAR Vars in other parts of the query refer to its * targetlist. * - * For ON CONFLICT .. UPDATE we just need the inner tlist to point to the - * excluded expression's tlist. (Similar to the SubqueryScan we don't want - * to reuse OUTER, it's used for RETURNING in some modify table cases, - * although not INSERT .. CONFLICT). + * For ON CONFLICT DO SELECT/UPDATE we just need the inner tlist to point + * to the excluded expression's tlist. (Similar to the SubqueryScan we + * don't want to reuse OUTER, it's used for RETURNING in some modify table + * cases, although not INSERT .. CONFLICT). */ if (IsA(plan, SubqueryScan)) dpns->inner_plan = ((SubqueryScan *) plan)->subplan; @@ -5640,6 +6002,9 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace, /* * Replace any Vars in the query's targetlist and havingQual that * reference GROUP outputs with the underlying grouping expressions. + * + * We can safely pass NULL for the root here. Preserving varnullingrels + * makes no difference to the deparsed source text. */ if (query->hasGroupRTE) { @@ -5994,30 +6359,9 @@ get_select_query_def(Query *query, deparse_context *context) if (rc->pushedDown) continue; - switch (rc->strength) - { - case LCS_NONE: - /* we intentionally throw an error for LCS_NONE */ - elog(ERROR, "unrecognized LockClauseStrength %d", - (int) rc->strength); - break; - case LCS_FORKEYSHARE: - appendContextKeyword(context, " FOR KEY SHARE", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); - break; - case LCS_FORSHARE: - appendContextKeyword(context, " FOR SHARE", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); - break; - case LCS_FORNOKEYUPDATE: - appendContextKeyword(context, " FOR NO KEY UPDATE", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); - break; - case LCS_FORUPDATE: - appendContextKeyword(context, " FOR UPDATE", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); - break; - } + appendContextKeyword(context, + get_lock_clause_strength(rc->strength), + -PRETTYINDENT_STD, PRETTYINDENT_STD, 0); appendStringInfo(buf, " OF %s", quote_identifier(get_rtable_name(rc->rti, @@ -6030,6 +6374,28 @@ get_select_query_def(Query *query, deparse_context *context) } } +static char * +get_lock_clause_strength(LockClauseStrength strength) +{ + switch (strength) + { + case LCS_NONE: + /* we intentionally throw an error for LCS_NONE */ + elog(ERROR, "unrecognized LockClauseStrength %d", + (int) strength); + break; + case LCS_FORKEYSHARE: + return " FOR KEY SHARE"; + case LCS_FORSHARE: + return " FOR SHARE"; + case LCS_FORNOKEYUPDATE: + return " FOR NO KEY UPDATE"; + case LCS_FORUPDATE: + return " FOR UPDATE"; + } + return NULL; /* keep compiler quiet */ +} + /* * Detect whether query looks like SELECT ... FROM VALUES(), * with no need to rename the output columns of the VALUES RTE. @@ -6187,7 +6553,9 @@ get_basic_select_query(Query *query, deparse_context *context) save_ingroupby = context->inGroupBy; context->inGroupBy = true; - if (query->groupingSets == NIL) + if (query->groupByAll) + appendStringInfoString(buf, "ALL"); + else if (query->groupingSets == NIL) { sep = ""; foreach(l, query->groupClause) @@ -7120,7 +7488,7 @@ get_insert_query_def(Query *query, deparse_context *context) { appendStringInfoString(buf, " DO NOTHING"); } - else + else if (confl->action == ONCONFLICT_UPDATE) { appendStringInfoString(buf, " DO UPDATE SET "); /* Deparse targetlist */ @@ -7135,6 +7503,23 @@ get_insert_query_def(Query *query, deparse_context *context) get_rule_expr(confl->onConflictWhere, context, false); } } + else + { + Assert(confl->action == ONCONFLICT_SELECT); + appendStringInfoString(buf, " DO SELECT"); + + /* Add FOR [KEY] UPDATE/SHARE clause if present */ + if (confl->lockStrength != LCS_NONE) + appendStringInfoString(buf, get_lock_clause_strength(confl->lockStrength)); + + /* Add a WHERE clause if given */ + if (confl->onConflictWhere != NULL) + { + appendContextKeyword(context, " WHERE ", + -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); + get_rule_expr(confl->onConflictWhere, context, false); + } + } } /* Add RETURNING if present */ @@ -7170,6 +7555,9 @@ get_update_query_def(Query *query, deparse_context *context) only_marker(rte), generate_relation_name(rte->relid, NIL)); + /* Print the FOR PORTION OF, if needed */ + get_for_portion_of(query->forPortionOf, context); + /* Print the relation alias, if needed */ get_rte_alias(rte, query->resultRelation, false, context); @@ -7374,6 +7762,9 @@ get_delete_query_def(Query *query, deparse_context *context) only_marker(rte), generate_relation_name(rte->relid, NIL)); + /* Print the FOR PORTION OF, if needed */ + get_for_portion_of(query->forPortionOf, context); + /* Print the relation alias, if needed */ get_rte_alias(rte, query->resultRelation, false, context); @@ -7583,6 +7974,171 @@ get_utility_query_def(Query *query, deparse_context *context) } } + +/* + * Parse back a graph label expression + */ +static void +get_graph_label_expr(Node *label_expr, deparse_context *context) +{ + StringInfo buf = context->buf; + + check_stack_depth(); + + switch (nodeTag(label_expr)) + { + case T_GraphLabelRef: + { + GraphLabelRef *lref = (GraphLabelRef *) label_expr; + + appendStringInfoString(buf, quote_identifier(get_propgraph_label_name(lref->labelid))); + break; + } + + case T_BoolExpr: + { + BoolExpr *be = (BoolExpr *) label_expr; + ListCell *lc; + bool first = true; + + Assert(be->boolop == OR_EXPR); + + foreach(lc, be->args) + { + if (!first) + { + if (be->boolop == OR_EXPR) + appendStringInfoChar(buf, '|'); + } + else + first = false; + get_graph_label_expr(lfirst(lc), context); + } + + break; + } + + default: + elog(ERROR, "unrecognized node type: %d", (int) nodeTag(label_expr)); + break; + } +} + +/* + * Parse back a path pattern expression + */ +static void +get_path_pattern_expr_def(List *path_pattern_expr, deparse_context *context) +{ + StringInfo buf = context->buf; + ListCell *lc; + + foreach(lc, path_pattern_expr) + { + GraphElementPattern *gep = lfirst_node(GraphElementPattern, lc); + const char *sep = ""; + + switch (gep->kind) + { + case VERTEX_PATTERN: + appendStringInfoChar(buf, '('); + break; + case EDGE_PATTERN_LEFT: + appendStringInfoString(buf, "<-["); + break; + case EDGE_PATTERN_RIGHT: + case EDGE_PATTERN_ANY: + appendStringInfoString(buf, "-["); + break; + case PAREN_EXPR: + appendStringInfoChar(buf, '('); + break; + } + + if (gep->variable) + { + appendStringInfoString(buf, quote_identifier(gep->variable)); + sep = " "; + } + + if (gep->labelexpr) + { + appendStringInfoString(buf, sep); + appendStringInfoString(buf, "IS "); + get_graph_label_expr(gep->labelexpr, context); + sep = " "; + } + + if (gep->subexpr) + { + appendStringInfoString(buf, sep); + get_path_pattern_expr_def(gep->subexpr, context); + sep = " "; + } + + if (gep->whereClause) + { + appendStringInfoString(buf, sep); + appendStringInfoString(buf, "WHERE "); + get_rule_expr(gep->whereClause, context, false); + } + + switch (gep->kind) + { + case VERTEX_PATTERN: + appendStringInfoChar(buf, ')'); + break; + case EDGE_PATTERN_LEFT: + case EDGE_PATTERN_ANY: + appendStringInfoString(buf, "]-"); + break; + case EDGE_PATTERN_RIGHT: + appendStringInfoString(buf, "]->"); + break; + case PAREN_EXPR: + appendStringInfoChar(buf, ')'); + break; + } + + if (gep->quantifier) + { + int lower = linitial_int(gep->quantifier); + int upper = lsecond_int(gep->quantifier); + + appendStringInfo(buf, "{%d,%d}", lower, upper); + } + } +} + +/* + * Parse back a graph pattern + */ +static void +get_graph_pattern_def(GraphPattern *graph_pattern, deparse_context *context) +{ + StringInfo buf = context->buf; + ListCell *lc; + bool first = true; + + foreach(lc, graph_pattern->path_pattern_list) + { + List *path_pattern_expr = lfirst_node(List, lc); + + if (!first) + appendStringInfoString(buf, ", "); + else + first = false; + + get_path_pattern_expr_def(path_pattern_expr, context); + } + + if (graph_pattern->whereClause) + { + appendStringInfoString(buf, "WHERE "); + get_rule_expr(graph_pattern->whereClause, context, false); + } +} + /* * Display a Var appropriately. * @@ -8193,6 +8749,7 @@ get_name_for_var_field(Var *var, int fieldno, case RTE_RELATION: case RTE_VALUES: case RTE_NAMEDTUPLESTORE: + case RTE_GRAPH_TABLE: case RTE_RESULT: /* @@ -8750,8 +9307,16 @@ get_parameter(Param *param, deparse_context *context) subplan = find_param_generator(param, context, &column); if (subplan) { - appendStringInfo(context->buf, "(%s%s).col%d", + const char *nameprefix; + + if (subplan->isInitPlan) + nameprefix = "InitPlan "; + else + nameprefix = "SubPlan "; + + appendStringInfo(context->buf, "(%s%s%s).col%d", subplan->useHashTable ? "hashed " : "", + nameprefix, subplan->plan_name, column + 1); return; @@ -8969,7 +9534,7 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags) } /* else do the same stuff as for T_SubLink et al. */ } - /* FALLTHROUGH */ + pg_fallthrough; case T_SubLink: case T_NullTest: @@ -9588,11 +10153,19 @@ get_rule_expr(Node *node, deparse_context *context, } else { + const char *nameprefix; + /* No referencing Params, so show the SubPlan's name */ + if (subplan->isInitPlan) + nameprefix = "InitPlan "; + else + nameprefix = "SubPlan "; if (subplan->useHashTable) - appendStringInfo(buf, "hashed %s)", subplan->plan_name); + appendStringInfo(buf, "hashed %s%s)", + nameprefix, subplan->plan_name); else - appendStringInfo(buf, "%s)", subplan->plan_name); + appendStringInfo(buf, "%s%s)", + nameprefix, subplan->plan_name); } } break; @@ -9612,11 +10185,18 @@ get_rule_expr(Node *node, deparse_context *context, foreach(lc, asplan->subplans) { SubPlan *splan = lfirst_node(SubPlan, lc); + const char *nameprefix; + if (splan->isInitPlan) + nameprefix = "InitPlan "; + else + nameprefix = "SubPlan "; if (splan->useHashTable) - appendStringInfo(buf, "hashed %s", splan->plan_name); + appendStringInfo(buf, "hashed %s%s", nameprefix, + splan->plan_name); else - appendStringInfoString(buf, splan->plan_name); + appendStringInfo(buf, "%s%s", nameprefix, + splan->plan_name); if (lnext(asplan->subplans, lc)) appendStringInfoString(buf, " or "); } @@ -9633,7 +10213,7 @@ get_rule_expr(Node *node, deparse_context *context, bool need_parens; /* - * Parenthesize the argument unless it's an SubscriptingRef or + * Parenthesize the argument unless it's a SubscriptingRef or * another FieldSelect. Note in particular that it would be * WRONG to not parenthesize a Var argument; simplicity is not * the issue here, having the right number of names is. @@ -9911,7 +10491,7 @@ get_rule_expr(Node *node, deparse_context *context, Node *e = (Node *) lfirst(arg); if (tupdesc == NULL || - !TupleDescAttr(tupdesc, i)->attisdropped) + !TupleDescCompactAttr(tupdesc, i)->attisdropped) { appendStringInfoString(buf, sep); /* Whole-row Vars need special treatment here */ @@ -9924,7 +10504,7 @@ get_rule_expr(Node *node, deparse_context *context, { while (i < tupdesc->natts) { - if (!TupleDescAttr(tupdesc, i)->attisdropped) + if (!TupleDescCompactAttr(tupdesc, i)->attisdropped) { appendStringInfoString(buf, sep); appendStringInfoString(buf, "NULL"); @@ -10123,7 +10703,7 @@ get_rule_expr(Node *node, deparse_context *context, if (needcomma) appendStringInfoString(buf, ", "); - get_rule_expr((Node *) e, context, true); + get_rule_expr(e, context, true); appendStringInfo(buf, " AS %s", quote_identifier(map_xml_name_to_sql_identifier(argname))); needcomma = true; @@ -10614,6 +11194,14 @@ get_rule_expr(Node *node, deparse_context *context, get_tablefunc((TableFunc *) node, context, showimplicit); break; + case T_GraphPropertyRef: + { + GraphPropertyRef *gpr = (GraphPropertyRef *) node; + + appendStringInfo(buf, "%s.%s", quote_identifier(gpr->elvarname), quote_identifier(get_propgraph_property_name(gpr->propid))); + break; + } + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; @@ -11090,7 +11678,12 @@ get_windowfunc_expr_helper(WindowFunc *wfunc, deparse_context *context, get_rule_expr((Node *) wfunc->aggfilter, context, false); } - appendStringInfoString(buf, ") OVER "); + appendStringInfoString(buf, ") "); + + if (wfunc->ignore_nulls == PARSER_IGNORE_NULLS) + appendStringInfoString(buf, "IGNORE NULLS "); + + appendStringInfoString(buf, "OVER "); if (context->windowClause) { @@ -11795,16 +12388,14 @@ simple_quote_literal(StringInfo buf, const char *val) const char *valptr; /* - * We form the string literal according to the prevailing setting of - * standard_conforming_strings; we never use E''. User is responsible for - * making sure result is used correctly. + * We always form the string literal according to standard SQL rules. */ appendStringInfoChar(buf, '\''); for (valptr = val; *valptr; valptr++) { char ch = *valptr; - if (SQL_STR_DOUBLE(ch, !standard_conforming_strings)) + if (SQL_STR_DOUBLE(ch, false)) appendStringInfoChar(buf, ch); appendStringInfoChar(buf, ch); } @@ -12493,6 +13084,35 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context) case RTE_TABLEFUNC: get_tablefunc(rte->tablefunc, context, true); break; + case RTE_GRAPH_TABLE: + appendStringInfoString(buf, "GRAPH_TABLE ("); + appendStringInfoString(buf, generate_relation_name(rte->relid, context->namespaces)); + appendStringInfoString(buf, " MATCH "); + get_graph_pattern_def(rte->graph_pattern, context); + appendStringInfoString(buf, " COLUMNS ("); + { + ListCell *lc; + bool first = true; + + foreach(lc, rte->graph_table_columns) + { + TargetEntry *te = lfirst_node(TargetEntry, lc); + deparse_context context = {0}; + + if (!first) + appendStringInfoString(buf, ", "); + else + first = false; + + context.buf = buf; + + get_rule_expr((Node *) te->expr, &context, false); + appendStringInfoString(buf, " AS "); + appendStringInfoString(buf, quote_identifier(te->resname)); + } + } + appendStringInfoString(buf, "))"); + break; case RTE_VALUES: /* Values list RTE */ appendStringInfoChar(buf, '('); @@ -12717,6 +13337,39 @@ get_rte_alias(RangeTblEntry *rte, int varno, bool use_as, quote_identifier(refname)); } +/* + * get_for_portion_of - print FOR PORTION OF if needed + * XXX: Newlines would help here, at least when pretty-printing. But then the + * alias and SET will be on their own line with a leading space. + */ +static void +get_for_portion_of(ForPortionOfExpr *forPortionOf, deparse_context *context) +{ + if (forPortionOf) + { + appendStringInfo(context->buf, " FOR PORTION OF %s", + quote_identifier(forPortionOf->range_name)); + + /* + * Try to write it as FROM ... TO ... if we received it that way, + * otherwise (targetRange). + */ + if (forPortionOf->targetFrom && forPortionOf->targetTo) + { + appendStringInfoString(context->buf, " FROM "); + get_rule_expr(forPortionOf->targetFrom, context, false); + appendStringInfoString(context->buf, " TO "); + get_rule_expr(forPortionOf->targetTo, context, false); + } + else + { + appendStringInfoString(context->buf, " ("); + get_rule_expr(forPortionOf->targetRange, context, false); + appendStringInfoChar(context->buf, ')'); + } + } +} + /* * get_column_alias_list - print column alias list for an RTE * @@ -13265,6 +13918,7 @@ generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, bool use_variadic; char *nspname; FuncDetailCode p_result; + int fgc_flags; Oid p_funcid; Oid p_rettype; bool p_retset; @@ -13323,6 +13977,7 @@ generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, p_result = func_get_detail(list_make1(makeString(proname)), NIL, argnames, nargs, argtypes, !use_variadic, true, false, + &fgc_flags, &p_funcid, &p_rettype, &p_retset, &p_nvargs, &p_vatype, &p_true_typeids, NULL); @@ -13584,7 +14239,7 @@ string_to_text(char *str) /* * Generate a C string representing a relation options from text[] datum. */ -static void +void get_reloptions(StringInfo buf, Datum reloptions) { Datum *options; @@ -13676,25 +14331,26 @@ char * get_range_partbound_string(List *bound_datums) { deparse_context context; - StringInfo buf = makeStringInfo(); + StringInfoData buf; ListCell *cell; char *sep; + initStringInfo(&buf); memset(&context, 0, sizeof(deparse_context)); - context.buf = buf; + context.buf = &buf; - appendStringInfoChar(buf, '('); + appendStringInfoChar(&buf, '('); sep = ""; foreach(cell, bound_datums) { PartitionRangeDatum *datum = lfirst_node(PartitionRangeDatum, cell); - appendStringInfoString(buf, sep); + appendStringInfoString(&buf, sep); if (datum->kind == PARTITION_RANGE_DATUM_MINVALUE) - appendStringInfoString(buf, "MINVALUE"); + appendStringInfoString(&buf, "MINVALUE"); else if (datum->kind == PARTITION_RANGE_DATUM_MAXVALUE) - appendStringInfoString(buf, "MAXVALUE"); + appendStringInfoString(&buf, "MAXVALUE"); else { Const *val = castNode(Const, datum->value); @@ -13703,7 +14359,7 @@ get_range_partbound_string(List *bound_datums) } sep = ", "; } - appendStringInfoChar(buf, ')'); + appendStringInfoChar(&buf, ')'); - return buf->data; + return buf.data; } diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index a96b1b9c0bc69..f2b58ebfe1ece 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -10,7 +10,7 @@ * Index cost functions are located via the index AM's API struct, * which is obtained from the handler function registered in pg_am. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -103,7 +103,6 @@ #include "access/table.h" #include "access/tableam.h" #include "access/visibilitymap.h" -#include "catalog/pg_am.h" #include "catalog/pg_collation.h" #include "catalog/pg_operator.h" #include "catalog/pg_statistic.h" @@ -144,26 +143,78 @@ #define DEFAULT_PAGE_CPU_MULTIPLIER 50.0 +/* + * In production builds, switch to hash-based MCV matching when the lists are + * large enough to amortize hash setup cost. (This threshold is compared to + * the sum of the lengths of the two MCV lists. This is simplistic but seems + * to work well enough.) In debug builds, we use a smaller threshold so that + * the regression tests cover both paths well. + */ +#ifndef USE_ASSERT_CHECKING +#define EQJOINSEL_MCV_HASH_THRESHOLD 200 +#else +#define EQJOINSEL_MCV_HASH_THRESHOLD 20 +#endif + +/* Entries in the simplehash hash table used by eqjoinsel_find_matches */ +typedef struct MCVHashEntry +{ + Datum value; /* the value represented by this entry */ + int index; /* its index in the relevant AttStatsSlot */ + uint32 hash; /* hash code for the Datum */ + char status; /* status code used by simplehash.h */ +} MCVHashEntry; + +/* private_data for the simplehash hash table */ +typedef struct MCVHashContext +{ + FunctionCallInfo equal_fcinfo; /* the equality join operator */ + FunctionCallInfo hash_fcinfo; /* the hash function to use */ + bool op_is_reversed; /* equality compares hash type to probe type */ + bool insert_mode; /* doing inserts or lookups? */ + bool hash_typbyval; /* typbyval of hashed data type */ + int16 hash_typlen; /* typlen of hashed data type */ +} MCVHashContext; + +/* forward reference */ +typedef struct MCVHashTable_hash MCVHashTable_hash; + /* Hooks for plugins to get control when we ask for stats */ get_relation_stats_hook_type get_relation_stats_hook = NULL; get_index_stats_hook_type get_index_stats_hook = NULL; static double eqsel_internal(PG_FUNCTION_ARGS, bool negate); -static double eqjoinsel_inner(Oid opfuncoid, Oid collation, +static double eqjoinsel_inner(FmgrInfo *eqproc, Oid collation, + Oid hashLeft, Oid hashRight, VariableStatData *vardata1, VariableStatData *vardata2, double nd1, double nd2, bool isdefault1, bool isdefault2, AttStatsSlot *sslot1, AttStatsSlot *sslot2, Form_pg_statistic stats1, Form_pg_statistic stats2, - bool have_mcvs1, bool have_mcvs2); -static double eqjoinsel_semi(Oid opfuncoid, Oid collation, + bool have_mcvs1, bool have_mcvs2, + bool *hasmatch1, bool *hasmatch2, + int *p_nmatches); +static double eqjoinsel_semi(FmgrInfo *eqproc, Oid collation, + Oid hashLeft, Oid hashRight, + bool op_is_reversed, VariableStatData *vardata1, VariableStatData *vardata2, double nd1, double nd2, bool isdefault1, bool isdefault2, AttStatsSlot *sslot1, AttStatsSlot *sslot2, Form_pg_statistic stats1, Form_pg_statistic stats2, bool have_mcvs1, bool have_mcvs2, + bool *hasmatch1, bool *hasmatch2, + int *p_nmatches, RelOptInfo *inner_rel); +static void eqjoinsel_find_matches(FmgrInfo *eqproc, Oid collation, + Oid hashLeft, Oid hashRight, + bool op_is_reversed, + AttStatsSlot *sslot1, AttStatsSlot *sslot2, + int nvalues1, int nvalues2, + bool *hasmatch1, bool *hasmatch2, + int *p_nmatches, double *p_matchprodfreq); +static uint32 hash_mcv(MCVHashTable_hash *tab, Datum key); +static bool mcvs_equal(MCVHashTable_hash *tab, Datum key0, Datum key1); static bool estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel, List **varinfos, double *ndistinct); static bool convert_to_scalar(Datum value, Oid valuetypid, Oid collid, @@ -191,6 +242,9 @@ static char *convert_string_datum(Datum value, Oid typid, Oid collid, bool *failure); static double convert_timevalue_to_scalar(Datum value, Oid typid, bool *failure); +static Node *strip_all_phvs_deep(PlannerInfo *root, Node *node); +static bool contain_placeholder_walker(Node *node, void *context); +static Node *strip_all_phvs_mutator(Node *node, void *context); static void examine_simple_variable(PlannerInfo *root, Var *var, VariableStatData *vardata); static void examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index, @@ -219,6 +273,20 @@ static RelOptInfo *find_join_input_rel(PlannerInfo *root, Relids relids); static double btcost_correlation(IndexOptInfo *index, VariableStatData *vardata); +/* Define support routines for MCV hash tables */ +#define SH_PREFIX MCVHashTable +#define SH_ELEMENT_TYPE MCVHashEntry +#define SH_KEY_TYPE Datum +#define SH_KEY value +#define SH_HASH_KEY(tab,key) hash_mcv(tab, key) +#define SH_EQUAL(tab,key0,key1) mcvs_equal(tab, key0, key1) +#define SH_SCOPE static inline +#define SH_STORE_HASH +#define SH_GET_HASH(tab,ent) (ent)->hash +#define SH_DEFINE +#define SH_DECLARE +#include "lib/simplehash.h" + /* * eqsel - Selectivity of "=" for any data types. @@ -1529,6 +1597,17 @@ boolvarsel(PlannerInfo *root, Node *arg, int varRelid) selec = var_eq_const(&vardata, BooleanEqualOperator, InvalidOid, BoolGetDatum(true), false, true, false); } + else if (is_funcclause(arg)) + { + /* + * If we have no stats and it's a function call, estimate 0.3333333. + * This seems a pretty unprincipled choice, but Postgres has been + * using that estimate for function calls since 1992. The hoariness + * of this behavior suggests that we should not be in too much hurry + * to use another value. + */ + selec = 0.3333333; + } else { /* Otherwise, the default estimate is 0.5 */ @@ -1939,6 +2018,15 @@ scalararraysel(PlannerInfo *root, if (arrayisnull) /* qual can't succeed if null array */ return (Selectivity) 0.0; arrayval = DatumGetArrayTypeP(arraydatum); + + /* + * When the array contains a NULL constant, same as var_eq_const, we + * assume the operator is strict and nothing will match, thus return + * 0.0. + */ + if (!useOr && array_contains_nulls(arrayval)) + return (Selectivity) 0.0; + get_typlenbyvalalign(ARR_ELEMTYPE(arrayval), &elmlen, &elmbyval, &elmalign); deconstruct_array(arrayval, @@ -2036,6 +2124,14 @@ scalararraysel(PlannerInfo *root, List *args; Selectivity s2; + /* + * When the array contains a NULL constant, same as var_eq_const, + * we assume the operator is strict and nothing will match, thus + * return 0.0. + */ + if (!useOr && IsA(elem, Const) && ((Const *) elem)->constisnull) + return (Selectivity) 0.0; + /* * Theoretically, if elem isn't of nominal_element_type we should * insert a RelabelType, but it seems unlikely that any operator @@ -2169,6 +2265,18 @@ estimate_array_length(PlannerInfo *root, Node *arrayexpr) AttStatsSlot sslot; double nelem = 0; + /* + * Skip calling examine_variable for Var with varno 0, which has no + * valid relation entry and would error in find_base_rel. Such a Var + * can appear when a nested set operation's output type doesn't match + * the parent's expected type, because recurse_set_operations builds a + * projection target list using generate_setop_tlist with varno 0, and + * if the required type coercion involves an ArrayCoerceExpr, we can + * be called on that Var. + */ + if (IsA(arrayexpr, Var) && ((Var *) arrayexpr)->varno == 0) + return 10; /* default guess, should match scalararraysel */ + examine_variable(root, arrayexpr, 0, &vardata); if (HeapTupleIsValid(vardata.statsTuple)) { @@ -2294,12 +2402,18 @@ eqjoinsel(PG_FUNCTION_ARGS) bool isdefault1; bool isdefault2; Oid opfuncoid; + FmgrInfo eqproc; + Oid hashLeft = InvalidOid; + Oid hashRight = InvalidOid; AttStatsSlot sslot1; AttStatsSlot sslot2; Form_pg_statistic stats1 = NULL; Form_pg_statistic stats2 = NULL; bool have_mcvs1 = false; bool have_mcvs2 = false; + bool *hasmatch1 = NULL; + bool *hasmatch2 = NULL; + int nmatches = 0; bool get_mcv_stats; bool join_is_reversed; RelOptInfo *inner_rel; @@ -2350,14 +2464,34 @@ eqjoinsel(PG_FUNCTION_ARGS) ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS); } + /* Prepare info usable by both eqjoinsel_inner and eqjoinsel_semi */ + if (have_mcvs1 && have_mcvs2) + { + fmgr_info(opfuncoid, &eqproc); + hasmatch1 = (bool *) palloc0(sslot1.nvalues * sizeof(bool)); + hasmatch2 = (bool *) palloc0(sslot2.nvalues * sizeof(bool)); + + /* + * If the MCV lists are long enough to justify hashing, try to look up + * hash functions for the join operator. + */ + if ((sslot1.nvalues + sslot2.nvalues) >= EQJOINSEL_MCV_HASH_THRESHOLD) + (void) get_op_hash_functions(operator, &hashLeft, &hashRight); + } + else + memset(&eqproc, 0, sizeof(eqproc)); /* silence uninit-var warnings */ + /* We need to compute the inner-join selectivity in all cases */ - selec_inner = eqjoinsel_inner(opfuncoid, collation, + selec_inner = eqjoinsel_inner(&eqproc, collation, + hashLeft, hashRight, &vardata1, &vardata2, nd1, nd2, isdefault1, isdefault2, &sslot1, &sslot2, stats1, stats2, - have_mcvs1, have_mcvs2); + have_mcvs1, have_mcvs2, + hasmatch1, hasmatch2, + &nmatches); switch (sjinfo->jointype) { @@ -2378,28 +2512,31 @@ eqjoinsel(PG_FUNCTION_ARGS) inner_rel = find_join_input_rel(root, sjinfo->min_righthand); if (!join_is_reversed) - selec = eqjoinsel_semi(opfuncoid, collation, + selec = eqjoinsel_semi(&eqproc, collation, + hashLeft, hashRight, + false, &vardata1, &vardata2, nd1, nd2, isdefault1, isdefault2, &sslot1, &sslot2, stats1, stats2, have_mcvs1, have_mcvs2, + hasmatch1, hasmatch2, + &nmatches, inner_rel); else - { - Oid commop = get_commutator(operator); - Oid commopfuncoid = OidIsValid(commop) ? get_opcode(commop) : InvalidOid; - - selec = eqjoinsel_semi(commopfuncoid, collation, + selec = eqjoinsel_semi(&eqproc, collation, + hashLeft, hashRight, + true, &vardata2, &vardata1, nd2, nd1, isdefault2, isdefault1, &sslot2, &sslot1, stats2, stats1, have_mcvs2, have_mcvs1, + hasmatch2, hasmatch1, + &nmatches, inner_rel); - } /* * We should never estimate the output of a semijoin to be more @@ -2427,6 +2564,11 @@ eqjoinsel(PG_FUNCTION_ARGS) ReleaseVariableStats(vardata1); ReleaseVariableStats(vardata2); + if (hasmatch1) + pfree(hasmatch1); + if (hasmatch2) + pfree(hasmatch2); + CLAMP_PROBABILITY(selec); PG_RETURN_FLOAT8((float8) selec); @@ -2435,17 +2577,24 @@ eqjoinsel(PG_FUNCTION_ARGS) /* * eqjoinsel_inner --- eqjoinsel for normal inner join * + * In addition to computing the selectivity estimate, this will fill + * hasmatch1[], hasmatch2[], and *p_nmatches (if have_mcvs1 && have_mcvs2). + * We may be able to re-use that data in eqjoinsel_semi. + * * We also use this for LEFT/FULL outer joins; it's not presently clear * that it's worth trying to distinguish them here. */ static double -eqjoinsel_inner(Oid opfuncoid, Oid collation, +eqjoinsel_inner(FmgrInfo *eqproc, Oid collation, + Oid hashLeft, Oid hashRight, VariableStatData *vardata1, VariableStatData *vardata2, double nd1, double nd2, bool isdefault1, bool isdefault2, AttStatsSlot *sslot1, AttStatsSlot *sslot2, Form_pg_statistic stats1, Form_pg_statistic stats2, - bool have_mcvs1, bool have_mcvs2) + bool have_mcvs1, bool have_mcvs2, + bool *hasmatch1, bool *hasmatch2, + int *p_nmatches) { double selec; @@ -2463,10 +2612,6 @@ eqjoinsel_inner(Oid opfuncoid, Oid collation, * results", Technical Report 1018, Computer Science Dept., University * of Wisconsin, Madison, March 1991 (available from ftp.cs.wisc.edu). */ - LOCAL_FCINFO(fcinfo, 2); - FmgrInfo eqproc; - bool *hasmatch1; - bool *hasmatch2; double nullfrac1 = stats1->stanullfrac; double nullfrac2 = stats2->stanullfrac; double matchprodfreq, @@ -2481,55 +2626,17 @@ eqjoinsel_inner(Oid opfuncoid, Oid collation, int i, nmatches; - fmgr_info(opfuncoid, &eqproc); - - /* - * Save a few cycles by setting up the fcinfo struct just once. Using - * FunctionCallInvoke directly also avoids failure if the eqproc - * returns NULL, though really equality functions should never do - * that. - */ - InitFunctionCallInfoData(*fcinfo, &eqproc, 2, collation, - NULL, NULL); - fcinfo->args[0].isnull = false; - fcinfo->args[1].isnull = false; - - hasmatch1 = (bool *) palloc0(sslot1->nvalues * sizeof(bool)); - hasmatch2 = (bool *) palloc0(sslot2->nvalues * sizeof(bool)); - - /* - * Note we assume that each MCV will match at most one member of the - * other MCV list. If the operator isn't really equality, there could - * be multiple matches --- but we don't look for them, both for speed - * and because the math wouldn't add up... - */ - matchprodfreq = 0.0; - nmatches = 0; - for (i = 0; i < sslot1->nvalues; i++) - { - int j; - - fcinfo->args[0].value = sslot1->values[i]; - - for (j = 0; j < sslot2->nvalues; j++) - { - Datum fresult; - - if (hasmatch2[j]) - continue; - fcinfo->args[1].value = sslot2->values[j]; - fcinfo->isnull = false; - fresult = FunctionCallInvoke(fcinfo); - if (!fcinfo->isnull && DatumGetBool(fresult)) - { - hasmatch1[i] = hasmatch2[j] = true; - matchprodfreq += sslot1->numbers[i] * sslot2->numbers[j]; - nmatches++; - break; - } - } - } + /* Fill the match arrays */ + eqjoinsel_find_matches(eqproc, collation, + hashLeft, hashRight, + false, + sslot1, sslot2, + sslot1->nvalues, sslot2->nvalues, + hasmatch1, hasmatch2, + p_nmatches, &matchprodfreq); + nmatches = *p_nmatches; CLAMP_PROBABILITY(matchprodfreq); + /* Sum up frequencies of matched and unmatched MCVs */ matchfreq1 = unmatchfreq1 = 0.0; for (i = 0; i < sslot1->nvalues; i++) @@ -2551,8 +2658,6 @@ eqjoinsel_inner(Oid opfuncoid, Oid collation, } CLAMP_PROBABILITY(matchfreq2); CLAMP_PROBABILITY(unmatchfreq2); - pfree(hasmatch1); - pfree(hasmatch2); /* * Compute total frequency of non-null values that are not in the MCV @@ -2632,17 +2737,24 @@ eqjoinsel_inner(Oid opfuncoid, Oid collation, * eqjoinsel_semi --- eqjoinsel for semi join * * (Also used for anti join, which we are supposed to estimate the same way.) - * Caller has ensured that vardata1 is the LHS variable. - * Unlike eqjoinsel_inner, we have to cope with opfuncoid being InvalidOid. + * Caller has ensured that vardata1 is the LHS variable; however, eqproc + * is for the original join operator, which might now need to have the inputs + * swapped in order to apply correctly. Also, if have_mcvs1 && have_mcvs2 + * then hasmatch1[], hasmatch2[], and *p_nmatches were filled by + * eqjoinsel_inner. */ static double -eqjoinsel_semi(Oid opfuncoid, Oid collation, +eqjoinsel_semi(FmgrInfo *eqproc, Oid collation, + Oid hashLeft, Oid hashRight, + bool op_is_reversed, VariableStatData *vardata1, VariableStatData *vardata2, double nd1, double nd2, bool isdefault1, bool isdefault2, AttStatsSlot *sslot1, AttStatsSlot *sslot2, Form_pg_statistic stats1, Form_pg_statistic stats2, bool have_mcvs1, bool have_mcvs2, + bool *hasmatch1, bool *hasmatch2, + int *p_nmatches, RelOptInfo *inner_rel) { double selec; @@ -2680,7 +2792,7 @@ eqjoinsel_semi(Oid opfuncoid, Oid collation, isdefault2 = false; } - if (have_mcvs1 && have_mcvs2 && OidIsValid(opfuncoid)) + if (have_mcvs1 && have_mcvs2) { /* * We have most-common-value lists for both relations. Run through @@ -2690,12 +2802,9 @@ eqjoinsel_semi(Oid opfuncoid, Oid collation, * lists. We still have to estimate for the remaining population, but * in a skewed distribution this gives us a big leg up in accuracy. */ - LOCAL_FCINFO(fcinfo, 2); - FmgrInfo eqproc; - bool *hasmatch1; - bool *hasmatch2; double nullfrac1 = stats1->stanullfrac; - double matchfreq1, + double matchprodfreq, + matchfreq1, uncertainfrac, uncertain; int i, @@ -2711,52 +2820,32 @@ eqjoinsel_semi(Oid opfuncoid, Oid collation, */ clamped_nvalues2 = Min(sslot2->nvalues, nd2); - fmgr_info(opfuncoid, &eqproc); - /* - * Save a few cycles by setting up the fcinfo struct just once. Using - * FunctionCallInvoke directly also avoids failure if the eqproc - * returns NULL, though really equality functions should never do - * that. - */ - InitFunctionCallInfoData(*fcinfo, &eqproc, 2, collation, - NULL, NULL); - fcinfo->args[0].isnull = false; - fcinfo->args[1].isnull = false; - - hasmatch1 = (bool *) palloc0(sslot1->nvalues * sizeof(bool)); - hasmatch2 = (bool *) palloc0(clamped_nvalues2 * sizeof(bool)); - - /* - * Note we assume that each MCV will match at most one member of the - * other MCV list. If the operator isn't really equality, there could - * be multiple matches --- but we don't look for them, both for speed - * and because the math wouldn't add up... + * If we did not set clamped_nvalues2 to less than sslot2->nvalues, + * then the hasmatch1[] and hasmatch2[] match flags computed by + * eqjoinsel_inner are still perfectly applicable, so we need not + * re-do the matching work. Note that it does not matter if + * op_is_reversed: we'd get the same answers. + * + * If we did clamp, then a different set of sslot2 values is to be + * compared, so we have to re-do the matching. */ - nmatches = 0; - for (i = 0; i < sslot1->nvalues; i++) + if (clamped_nvalues2 != sslot2->nvalues) { - int j; - - fcinfo->args[0].value = sslot1->values[i]; - - for (j = 0; j < clamped_nvalues2; j++) - { - Datum fresult; - - if (hasmatch2[j]) - continue; - fcinfo->args[1].value = sslot2->values[j]; - fcinfo->isnull = false; - fresult = FunctionCallInvoke(fcinfo); - if (!fcinfo->isnull && DatumGetBool(fresult)) - { - hasmatch1[i] = hasmatch2[j] = true; - nmatches++; - break; - } - } + /* Must re-zero the arrays */ + memset(hasmatch1, 0, sslot1->nvalues * sizeof(bool)); + memset(hasmatch2, 0, clamped_nvalues2 * sizeof(bool)); + /* Re-fill the match arrays */ + eqjoinsel_find_matches(eqproc, collation, + hashLeft, hashRight, + op_is_reversed, + sslot1, sslot2, + sslot1->nvalues, clamped_nvalues2, + hasmatch1, hasmatch2, + p_nmatches, &matchprodfreq); } + nmatches = *p_nmatches; + /* Sum up frequencies of matched MCVs */ matchfreq1 = 0.0; for (i = 0; i < sslot1->nvalues; i++) @@ -2765,8 +2854,6 @@ eqjoinsel_semi(Oid opfuncoid, Oid collation, matchfreq1 += sslot1->numbers[i]; } CLAMP_PROBABILITY(matchfreq1); - pfree(hasmatch1); - pfree(hasmatch2); /* * Now we need to estimate the fraction of relation 1 that has at @@ -2820,6 +2907,273 @@ eqjoinsel_semi(Oid opfuncoid, Oid collation, return selec; } +/* + * Identify matching MCVs for eqjoinsel_inner or eqjoinsel_semi. + * + * Inputs: + * eqproc: FmgrInfo for equality function to use (might be reversed) + * collation: OID of collation to use + * hashLeft, hashRight: OIDs of hash functions associated with equality op, + * or InvalidOid if we're not to use hashing + * op_is_reversed: indicates that eqproc compares right type to left type + * sslot1, sslot2: MCV values for the lefthand and righthand inputs + * nvalues1, nvalues2: number of values to be considered (can be less than + * sslotN->nvalues, but not more) + * Outputs: + * hasmatch1[], hasmatch2[]: pre-zeroed arrays of lengths nvalues1, nvalues2; + * entries are set to true if that MCV has a match on the other side + * *p_nmatches: receives number of MCV pairs that match + * *p_matchprodfreq: receives sum(sslot1->numbers[i] * sslot2->numbers[j]) + * for matching MCVs + * + * Note that hashLeft is for the eqproc's left-hand input type, hashRight + * for its right, regardless of op_is_reversed. + * + * Note we assume that each MCV will match at most one member of the other + * MCV list. If the operator isn't really equality, there could be multiple + * matches --- but we don't look for them, both for speed and because the + * math wouldn't add up... + */ +static void +eqjoinsel_find_matches(FmgrInfo *eqproc, Oid collation, + Oid hashLeft, Oid hashRight, + bool op_is_reversed, + AttStatsSlot *sslot1, AttStatsSlot *sslot2, + int nvalues1, int nvalues2, + bool *hasmatch1, bool *hasmatch2, + int *p_nmatches, double *p_matchprodfreq) +{ + LOCAL_FCINFO(fcinfo, 2); + double matchprodfreq = 0.0; + int nmatches = 0; + + /* + * Save a few cycles by setting up the fcinfo struct just once. Using + * FunctionCallInvoke directly also avoids failure if the eqproc returns + * NULL, though really equality functions should never do that. + */ + InitFunctionCallInfoData(*fcinfo, eqproc, 2, collation, + NULL, NULL); + fcinfo->args[0].isnull = false; + fcinfo->args[1].isnull = false; + + if (OidIsValid(hashLeft) && OidIsValid(hashRight)) + { + /* Use a hash table to speed up the matching */ + LOCAL_FCINFO(hash_fcinfo, 1); + FmgrInfo hash_proc; + MCVHashContext hashContext; + MCVHashTable_hash *hashTable; + AttStatsSlot *statsProbe; + AttStatsSlot *statsHash; + bool *hasMatchProbe; + bool *hasMatchHash; + int nvaluesProbe; + int nvaluesHash; + + /* Make sure we build the hash table on the smaller array. */ + if (sslot1->nvalues >= sslot2->nvalues) + { + statsProbe = sslot1; + statsHash = sslot2; + hasMatchProbe = hasmatch1; + hasMatchHash = hasmatch2; + nvaluesProbe = nvalues1; + nvaluesHash = nvalues2; + } + else + { + /* We'll have to reverse the direction of use of the operator. */ + op_is_reversed = !op_is_reversed; + statsProbe = sslot2; + statsHash = sslot1; + hasMatchProbe = hasmatch2; + hasMatchHash = hasmatch1; + nvaluesProbe = nvalues2; + nvaluesHash = nvalues1; + } + + /* + * Build the hash table on the smaller array, using the appropriate + * hash function for its data type. + */ + fmgr_info(op_is_reversed ? hashLeft : hashRight, &hash_proc); + InitFunctionCallInfoData(*hash_fcinfo, &hash_proc, 1, collation, + NULL, NULL); + hash_fcinfo->args[0].isnull = false; + + hashContext.equal_fcinfo = fcinfo; + hashContext.hash_fcinfo = hash_fcinfo; + hashContext.op_is_reversed = op_is_reversed; + hashContext.insert_mode = true; + get_typlenbyval(statsHash->valuetype, + &hashContext.hash_typlen, + &hashContext.hash_typbyval); + + hashTable = MCVHashTable_create(CurrentMemoryContext, + nvaluesHash, + &hashContext); + + for (int i = 0; i < nvaluesHash; i++) + { + bool found = false; + MCVHashEntry *entry = MCVHashTable_insert(hashTable, + statsHash->values[i], + &found); + + /* + * MCVHashTable_insert will only report "found" if the new value + * is equal to some previous one per datum_image_eq(). That + * probably shouldn't happen, since we're not expecting duplicates + * in the MCV list. If we do find a dup, just ignore it, leaving + * the hash entry's index pointing at the first occurrence. That + * matches the behavior that the non-hashed code path would have. + */ + if (likely(!found)) + entry->index = i; + } + + /* + * Prepare to probe the hash table. If the probe values are of a + * different data type, then we need to change hash functions. (This + * code relies on the assumption that since we defined SH_STORE_HASH, + * simplehash.h will never need to compute hash values for existing + * hash table entries.) + */ + hashContext.insert_mode = false; + if (hashLeft != hashRight) + { + fmgr_info(op_is_reversed ? hashRight : hashLeft, &hash_proc); + /* Resetting hash_fcinfo is probably unnecessary, but be safe */ + InitFunctionCallInfoData(*hash_fcinfo, &hash_proc, 1, collation, + NULL, NULL); + hash_fcinfo->args[0].isnull = false; + } + + /* Look up each probe value in turn. */ + for (int i = 0; i < nvaluesProbe; i++) + { + MCVHashEntry *entry = MCVHashTable_lookup(hashTable, + statsProbe->values[i]); + + /* As in the other code path, skip already-matched hash entries */ + if (entry != NULL && !hasMatchHash[entry->index]) + { + hasMatchHash[entry->index] = hasMatchProbe[i] = true; + nmatches++; + matchprodfreq += statsHash->numbers[entry->index] * statsProbe->numbers[i]; + } + } + + MCVHashTable_destroy(hashTable); + } + else + { + /* We're not to use hashing, so do it the O(N^2) way */ + int index1, + index2; + + /* Set up to supply the values in the order the operator expects */ + if (op_is_reversed) + { + index1 = 1; + index2 = 0; + } + else + { + index1 = 0; + index2 = 1; + } + + for (int i = 0; i < nvalues1; i++) + { + fcinfo->args[index1].value = sslot1->values[i]; + + for (int j = 0; j < nvalues2; j++) + { + Datum fresult; + + if (hasmatch2[j]) + continue; + fcinfo->args[index2].value = sslot2->values[j]; + fcinfo->isnull = false; + fresult = FunctionCallInvoke(fcinfo); + if (!fcinfo->isnull && DatumGetBool(fresult)) + { + hasmatch1[i] = hasmatch2[j] = true; + matchprodfreq += sslot1->numbers[i] * sslot2->numbers[j]; + nmatches++; + break; + } + } + } + } + + *p_nmatches = nmatches; + *p_matchprodfreq = matchprodfreq; +} + +/* + * Support functions for the hash tables used by eqjoinsel_find_matches + */ +static uint32 +hash_mcv(MCVHashTable_hash *tab, Datum key) +{ + MCVHashContext *context = (MCVHashContext *) tab->private_data; + FunctionCallInfo fcinfo = context->hash_fcinfo; + Datum fresult; + + fcinfo->args[0].value = key; + fcinfo->isnull = false; + fresult = FunctionCallInvoke(fcinfo); + Assert(!fcinfo->isnull); + return DatumGetUInt32(fresult); +} + +static bool +mcvs_equal(MCVHashTable_hash *tab, Datum key0, Datum key1) +{ + MCVHashContext *context = (MCVHashContext *) tab->private_data; + + if (context->insert_mode) + { + /* + * During the insertion step, any comparisons will be between two + * Datums of the hash table's data type, so if the given operator is + * cross-type it will be the wrong thing to use. Fortunately, we can + * use datum_image_eq instead. The MCV values should all be distinct + * anyway, so it's mostly pro-forma to compare them at all. + */ + return datum_image_eq(key0, key1, + context->hash_typbyval, context->hash_typlen); + } + else + { + FunctionCallInfo fcinfo = context->equal_fcinfo; + Datum fresult; + + /* + * Apply the operator the correct way around. Although simplehash.h + * doesn't document this explicitly, during lookups key0 is from the + * hash table while key1 is the probe value, so we should compare them + * in that order only if op_is_reversed. + */ + if (context->op_is_reversed) + { + fcinfo->args[0].value = key0; + fcinfo->args[1].value = key1; + } + else + { + fcinfo->args[0].value = key1; + fcinfo->args[1].value = key0; + } + fcinfo->isnull = false; + fresult = FunctionCallInvoke(fcinfo); + return (!fcinfo->isnull && DatumGetBool(fresult)); + } +} + /* * neqjoinsel - Join selectivity of "!=" */ @@ -3361,7 +3715,7 @@ add_unique_group_var(PlannerInfo *root, List *varinfos, } } - varinfo = (GroupVarInfo *) palloc(sizeof(GroupVarInfo)); + varinfo = palloc_object(GroupVarInfo); varinfo->var = var; varinfo->rel = vardata->rel; @@ -3799,18 +4153,25 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner, List *hashclauses, Selectivity *innerbucketsize) { - List *clauses = list_copy(hashclauses); - List *otherclauses = NIL; - double ndistinct = 1.0; + List *clauses; + List *otherclauses; + double ndistinct; if (list_length(hashclauses) <= 1) - + { /* * Nothing to do for a single clause. Could we employ univariate * extended stat here? */ return hashclauses; + } + /* "clauses" is the list of hashclauses we've not dealt with yet */ + clauses = list_copy(hashclauses); + /* "otherclauses" holds clauses we are going to return to caller */ + otherclauses = NIL; + /* current estimate of ndistinct */ + ndistinct = 1.0; while (clauses != NIL) { ListCell *lc; @@ -3875,12 +4236,13 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner, group_rel = root->simple_rel_array[relid]; } else if (group_relid != relid) - + { /* * Being in the group forming state we don't need other * clauses. */ continue; + } /* * We're going to add the new clause to the varinfos list. We @@ -3934,7 +4296,7 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner, * estimate_multivariate_ndistinct(), which doesn't care about * ndistinct and isdefault fields. Thus, skip these fields. */ - varinfo = (GroupVarInfo *) palloc0(sizeof(GroupVarInfo)); + varinfo = palloc0_object(GroupVarInfo); varinfo->var = expr; varinfo->rel = root->simple_rel_array[relid]; varinfos = lappend(varinfos, varinfo); @@ -4017,10 +4379,11 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner, * This attempts to determine two values: * * 1. The frequency of the most common value of the expression (returns - * zero into *mcv_freq if we can't get that). + * zero into *mcv_freq if we can't get that). This will be frequency + * relative to the entire underlying table. * * 2. The "bucketsize fraction", ie, average number of entries in a bucket - * divided by total tuples in relation. + * divided by total number of tuples to be hashed. * * XXX This is really pretty bogus since we're effectively assuming that the * distribution of hash keys will be the same after applying restriction @@ -4039,8 +4402,8 @@ estimate_multivariate_bucketsize(PlannerInfo *root, RelOptInfo *inner, * exactly those that will be probed most often. Therefore, the "average" * bucket size for costing purposes should really be taken as something close * to the "worst case" bucket size. We try to estimate this by adjusting the - * fraction if there are too few distinct data values, and then scaling up - * by the ratio of the most common value's frequency to the average frequency. + * fraction if there are too few distinct data values, and then clamping to + * at least the bucket size implied by the most common value's frequency. * * If no statistics are available, use a default estimate of 0.1. This will * discourage use of a hash rather strongly if the inner relation is large, @@ -4060,17 +4423,16 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets, { VariableStatData vardata; double estfract, - ndistinct, - stanullfrac, - avgfreq; + ndistinct; bool isdefault; AttStatsSlot sslot; examine_variable(root, hashkey, 0, &vardata); - /* Look up the frequency of the most common value, if available */ + /* Initialize *mcv_freq to "unknown" */ *mcv_freq = 0.0; + /* Look up the frequency of the most common value, if available */ if (HeapTupleIsValid(vardata.statsTuple)) { if (get_attstatsslot(&sslot, vardata.statsTuple, @@ -4084,6 +4446,17 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets, *mcv_freq = sslot.numbers[0]; free_attstatsslot(&sslot); } + else if (get_attstatsslot(&sslot, vardata.statsTuple, + STATISTIC_KIND_HISTOGRAM, InvalidOid, + 0)) + { + /* + * If there are no recorded MCVs, but we do have a histogram, then + * assume that ANALYZE determined that the column is unique. + */ + if (vardata.rel && vardata.rel->tuples > 0) + *mcv_freq = 1.0 / vardata.rel->tuples; + } } /* Get number of distinct values */ @@ -4100,20 +4473,6 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets, return; } - /* Get fraction that are null */ - if (HeapTupleIsValid(vardata.statsTuple)) - { - Form_pg_statistic stats; - - stats = (Form_pg_statistic) GETSTRUCT(vardata.statsTuple); - stanullfrac = stats->stanullfrac; - } - else - stanullfrac = 0.0; - - /* Compute avg freq of all distinct data values in raw relation */ - avgfreq = (1.0 - stanullfrac) / ndistinct; - /* * Adjust ndistinct to account for restriction clauses. Observe we are * assuming that the data distribution is affected uniformly by the @@ -4139,20 +4498,11 @@ estimate_hash_bucket_stats(PlannerInfo *root, Node *hashkey, double nbuckets, estfract = 1.0 / ndistinct; /* - * Adjust estimated bucketsize upward to account for skewed distribution. - */ - if (avgfreq > 0.0 && *mcv_freq > avgfreq) - estfract *= *mcv_freq / avgfreq; - - /* - * Clamp bucketsize to sane range (the above adjustment could easily - * produce an out-of-range result). We set the lower bound a little above - * zero, since zero isn't a very sane result. + * Clamp the bucketsize fraction to be not less than the MCV frequency, + * since whichever bucket the MCV values end up in will have at least that + * size. This has no effect if *mcv_freq is still zero. */ - if (estfract < 1.0e-6) - estfract = 1.0e-6; - else if (estfract > 1.0) - estfract = 1.0; + estfract = Max(estfract, *mcv_freq); *bucketsize_frac = (Selectivity) estfract; @@ -4620,6 +4970,7 @@ convert_to_scalar(Datum value, Oid valuetypid, Oid collid, double *scaledvalue, case REGDICTIONARYOID: case REGROLEOID: case REGNAMESPACEOID: + case REGDATABASEOID: *scaledvalue = convert_numeric_to_scalar(value, valuetypid, &failure); *scaledlobound = convert_numeric_to_scalar(lobound, boundstypid, @@ -4752,6 +5103,7 @@ convert_numeric_to_scalar(Datum value, Oid typid, bool *failure) case REGDICTIONARYOID: case REGROLEOID: case REGNAMESPACEOID: + case REGDATABASEOID: /* we can treat OIDs as integers... */ return (double) DatumGetObjectId(value); } @@ -5261,8 +5613,8 @@ ReleaseDummy(HeapTuple tuple) * varRelid: see specs for restriction selectivity functions * * Outputs: *vardata is filled as follows: - * var: the input expression (with any binary relabeling stripped, if - * it is or contains a variable; but otherwise the type is preserved) + * var: the input expression (with any phvs or binary relabeling stripped, + * if it is or contains a variable; but otherwise unchanged) * rel: RelOptInfo for relation containing variable; NULL if expression * contains no Vars (NOTE this could point to a RelOptInfo of a * subquery, not one in the current query). @@ -5279,8 +5631,8 @@ ReleaseDummy(HeapTuple tuple) * unique for this query. (Caution: this should be trusted for * statistical purposes only, since we do not check indimmediate nor * verify that the exact same definition of equality applies.) - * acl_ok: true if current user has permission to read the column(s) - * underlying the pg_statistic entry. This is consulted by + * acl_ok: true if current user has permission to read all table rows from + * the column(s) underlying the pg_statistic entry. This is consulted by * statistic_proc_security_check(). * * Caller is responsible for doing ReleaseVariableStats() before exiting. @@ -5300,22 +5652,31 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, /* Save the exposed type of the expression */ vardata->vartype = exprType(node); - /* Look inside any binary-compatible relabeling */ + /* + * PlaceHolderVars are transparent for the purpose of statistics lookup; + * they do not alter the value distribution of the underlying expression. + * However, they can obscure the structure, preventing us from recognizing + * matches to base columns, index expressions, or extended statistics. So + * strip them out first. + */ + basenode = strip_all_phvs_deep(root, node); - if (IsA(node, RelabelType)) - basenode = (Node *) ((RelabelType *) node)->arg; - else - basenode = node; + /* + * Look inside any binary-compatible relabeling. We need to handle nested + * RelabelType nodes here, because the prior stripping of PlaceHolderVars + * may have brought separate RelabelTypes into adjacency. + */ + while (IsA(basenode, RelabelType)) + basenode = (Node *) ((RelabelType *) basenode)->arg; /* Fast path for a simple Var */ - if (IsA(basenode, Var) && (varRelid == 0 || varRelid == ((Var *) basenode)->varno)) { Var *var = (Var *) basenode; /* Set up result fields other than the stats tuple */ - vardata->var = basenode; /* return Var without relabeling */ + vardata->var = basenode; /* return Var without phvs or relabeling */ vardata->rel = find_base_rel(root, var->varno); vardata->atttype = var->vartype; vardata->atttypmod = var->vartypmod; @@ -5352,7 +5713,7 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, { onerel = find_base_rel(root, relid); vardata->rel = onerel; - node = basenode; /* strip any relabeling */ + node = basenode; /* strip any phvs or relabeling */ } /* else treat it as a constant */ } @@ -5363,13 +5724,13 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, { /* treat it as a variable of a join relation */ vardata->rel = find_join_rel(root, varnos); - node = basenode; /* strip any relabeling */ + node = basenode; /* strip any phvs or relabeling */ } else if (bms_is_member(varRelid, varnos)) { /* ignore the vars belonging to other relations */ vardata->rel = find_base_rel(root, varRelid); - node = basenode; /* strip any relabeling */ + node = basenode; /* strip any phvs or relabeling */ /* note: no point in expressional-index search here */ } /* else treat it as a constant */ @@ -5399,7 +5760,6 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, */ ListCell *ilist; ListCell *slist; - Oid userid; /* * The nullingrels bits within the expression could prevent us from @@ -5409,17 +5769,6 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, if (bms_overlap(varnos, root->outer_join_rels)) node = remove_nulling_relids(node, root->outer_join_rels, NULL); - /* - * Determine the user ID to use for privilege checks: either - * onerel->userid if it's set (e.g., in case we're accessing the table - * via a view), or the current user otherwise. - * - * If we drill down to child relations, we keep using the same userid: - * it's going to be the same anyway, due to how we set up the relation - * tree (q.v. build_simple_rel). - */ - userid = OidIsValid(onerel->userid) ? onerel->userid : GetUserId(); - foreach(ilist, onerel->indexlist) { IndexOptInfo *index = (IndexOptInfo *) lfirst(ilist); @@ -5487,69 +5836,32 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, if (HeapTupleIsValid(vardata->statsTuple)) { - /* Get index's table for permission check */ - RangeTblEntry *rte; - - rte = planner_rt_fetch(index->rel->relid, root); - Assert(rte->rtekind == RTE_RELATION); - /* + * Test if user has permission to access all + * rows from the index's table. + * * For simplicity, we insist on the whole * table being selectable, rather than trying * to identify which column(s) the index - * depends on. Also require all rows to be - * selectable --- there must be no - * securityQuals from security barrier views - * or RLS policies. + * depends on. + * + * Note that for an inheritance child, + * permissions are checked on the inheritance + * root parent, and whole-table select + * privilege on the parent doesn't quite + * guarantee that the user could read all + * columns of the child. But in practice it's + * unlikely that any interesting security + * violation could result from allowing access + * to the expression index's stats, so we + * allow it anyway. See similar code in + * examine_simple_variable() for additional + * comments. */ vardata->acl_ok = - rte->securityQuals == NIL && - (pg_class_aclcheck(rte->relid, userid, - ACL_SELECT) == ACLCHECK_OK); - - /* - * If the user doesn't have permissions to - * access an inheritance child relation, check - * the permissions of the table actually - * mentioned in the query, since most likely - * the user does have that permission. Note - * that whole-table select privilege on the - * parent doesn't quite guarantee that the - * user could read all columns of the child. - * But in practice it's unlikely that any - * interesting security violation could result - * from allowing access to the expression - * index's stats, so we allow it anyway. See - * similar code in examine_simple_variable() - * for additional comments. - */ - if (!vardata->acl_ok && - root->append_rel_array != NULL) - { - AppendRelInfo *appinfo; - Index varno = index->rel->relid; - - appinfo = root->append_rel_array[varno]; - while (appinfo && - planner_rt_fetch(appinfo->parent_relid, - root)->rtekind == RTE_RELATION) - { - varno = appinfo->parent_relid; - appinfo = root->append_rel_array[varno]; - } - if (varno != index->rel->relid) - { - /* Repeat access check on this rel */ - rte = planner_rt_fetch(varno, root); - Assert(rte->rtekind == RTE_RELATION); - - vardata->acl_ok = - rte->securityQuals == NIL && - (pg_class_aclcheck(rte->relid, - userid, - ACL_SELECT) == ACLCHECK_OK); - } - } + all_rows_selectable(root, + index->rel->relid, + NULL); } else { @@ -5616,61 +5928,33 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, vardata->statsTuple = statext_expressions_load(info->statOid, rte->inh, pos); - vardata->freefunc = ReleaseDummy; + /* Nothing to release if no data found */ + if (vardata->statsTuple != NULL) + { + vardata->freefunc = ReleaseDummy; + } /* + * Test if user has permission to access all rows from the + * table. + * * For simplicity, we insist on the whole table being * selectable, rather than trying to identify which - * column(s) the statistics object depends on. Also - * require all rows to be selectable --- there must be no - * securityQuals from security barrier views or RLS - * policies. - */ - vardata->acl_ok = - rte->securityQuals == NIL && - (pg_class_aclcheck(rte->relid, userid, - ACL_SELECT) == ACLCHECK_OK); - - /* - * If the user doesn't have permissions to access an - * inheritance child relation, check the permissions of - * the table actually mentioned in the query, since most - * likely the user does have that permission. Note that - * whole-table select privilege on the parent doesn't - * quite guarantee that the user could read all columns of - * the child. But in practice it's unlikely that any - * interesting security violation could result from - * allowing access to the expression stats, so we allow it - * anyway. See similar code in examine_simple_variable() - * for additional comments. + * column(s) the statistics object depends on. + * + * Note that for an inheritance child, permissions are + * checked on the inheritance root parent, and whole-table + * select privilege on the parent doesn't quite guarantee + * that the user could read all columns of the child. But + * in practice it's unlikely that any interesting security + * violation could result from allowing access to the + * expression stats, so we allow it anyway. See similar + * code in examine_simple_variable() for additional + * comments. */ - if (!vardata->acl_ok && - root->append_rel_array != NULL) - { - AppendRelInfo *appinfo; - Index varno = onerel->relid; - - appinfo = root->append_rel_array[varno]; - while (appinfo && - planner_rt_fetch(appinfo->parent_relid, - root)->rtekind == RTE_RELATION) - { - varno = appinfo->parent_relid; - appinfo = root->append_rel_array[varno]; - } - if (varno != onerel->relid) - { - /* Repeat access check on this rel */ - rte = planner_rt_fetch(varno, root); - Assert(rte->rtekind == RTE_RELATION); - - vardata->acl_ok = - rte->securityQuals == NIL && - (pg_class_aclcheck(rte->relid, - userid, - ACL_SELECT) == ACLCHECK_OK); - } - } + vardata->acl_ok = all_rows_selectable(root, + onerel->relid, + NULL); break; } @@ -5683,6 +5967,63 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid, bms_free(varnos); } +/* + * strip_all_phvs_deep + * Deeply strip all PlaceHolderVars in an expression. + + * As a performance optimization, we first use a lightweight walker to check + * for the presence of any PlaceHolderVars. The expensive mutator is invoked + * only if a PlaceHolderVar is found, avoiding unnecessary memory allocation + * and tree copying in the common case where no PlaceHolderVars are present. + */ +static Node * +strip_all_phvs_deep(PlannerInfo *root, Node *node) +{ + /* If there are no PHVs anywhere, we needn't work hard */ + if (root->glob->lastPHId == 0) + return node; + + if (!contain_placeholder_walker(node, NULL)) + return node; + return strip_all_phvs_mutator(node, NULL); +} + +/* + * contain_placeholder_walker + * Lightweight walker to check if an expression contains any + * PlaceHolderVars + */ +static bool +contain_placeholder_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + if (IsA(node, PlaceHolderVar)) + return true; + + return expression_tree_walker(node, contain_placeholder_walker, context); +} + +/* + * strip_all_phvs_mutator + * Mutator to deeply strip all PlaceHolderVars + */ +static Node * +strip_all_phvs_mutator(Node *node, void *context) +{ + if (node == NULL) + return NULL; + if (IsA(node, PlaceHolderVar)) + { + /* Strip it and recurse into its contained expression */ + PlaceHolderVar *phv = (PlaceHolderVar *) node; + + return strip_all_phvs_mutator((Node *) phv->phexpr, context); + } + + return expression_tree_mutator(node, strip_all_phvs_mutator, context); +} + /* * examine_simple_variable * Handle a simple Var for examine_variable @@ -5725,109 +6066,20 @@ examine_simple_variable(PlannerInfo *root, Var *var, if (HeapTupleIsValid(vardata->statsTuple)) { - RelOptInfo *onerel = find_base_rel_noerr(root, var->varno); - Oid userid; - /* - * Check if user has permission to read this column. We require - * all rows to be accessible, so there must be no securityQuals - * from security barrier views or RLS policies. + * Test if user has permission to read all rows from this column. * - * Normally the Var will have an associated RelOptInfo from which - * we can find out which userid to do the check as; but it might - * not if it's a RETURNING Var for an INSERT target relation. In - * that case use the RTEPermissionInfo associated with the RTE. + * This requires that the user has the appropriate SELECT + * privileges and that there are no securityQuals from security + * barrier views or RLS policies. If that's not the case, then we + * only permit leakproof functions to be passed pg_statistic data + * in vardata, otherwise the functions might reveal data that the + * user doesn't have permission to see --- see + * statistic_proc_security_check(). */ - if (onerel) - userid = onerel->userid; - else - { - RTEPermissionInfo *perminfo; - - perminfo = getRTEPermissionInfo(root->parse->rteperminfos, rte); - userid = perminfo->checkAsUser; - } - if (!OidIsValid(userid)) - userid = GetUserId(); - vardata->acl_ok = - rte->securityQuals == NIL && - ((pg_class_aclcheck(rte->relid, userid, - ACL_SELECT) == ACLCHECK_OK) || - (pg_attribute_aclcheck(rte->relid, var->varattno, userid, - ACL_SELECT) == ACLCHECK_OK)); - - /* - * If the user doesn't have permissions to access an inheritance - * child relation or specifically this attribute, check the - * permissions of the table/column actually mentioned in the - * query, since most likely the user does have that permission - * (else the query will fail at runtime), and if the user can read - * the column there then he can get the values of the child table - * too. To do that, we must find out which of the root parent's - * attributes the child relation's attribute corresponds to. - */ - if (!vardata->acl_ok && var->varattno > 0 && - root->append_rel_array != NULL) - { - AppendRelInfo *appinfo; - Index varno = var->varno; - int varattno = var->varattno; - bool found = false; - - appinfo = root->append_rel_array[varno]; - - /* - * Partitions are mapped to their immediate parent, not the - * root parent, so must be ready to walk up multiple - * AppendRelInfos. But stop if we hit a parent that is not - * RTE_RELATION --- that's a flattened UNION ALL subquery, not - * an inheritance parent. - */ - while (appinfo && - planner_rt_fetch(appinfo->parent_relid, - root)->rtekind == RTE_RELATION) - { - int parent_varattno; - - found = false; - if (varattno <= 0 || varattno > appinfo->num_child_cols) - break; /* safety check */ - parent_varattno = appinfo->parent_colnos[varattno - 1]; - if (parent_varattno == 0) - break; /* Var is local to child */ - - varno = appinfo->parent_relid; - varattno = parent_varattno; - found = true; - - /* If the parent is itself a child, continue up. */ - appinfo = root->append_rel_array[varno]; - } - - /* - * In rare cases, the Var may be local to the child table, in - * which case, we've got to live with having no access to this - * column's stats. - */ - if (!found) - return; - - /* Repeat the access check on this parent rel & column */ - rte = planner_rt_fetch(varno, root); - Assert(rte->rtekind == RTE_RELATION); - - /* - * Fine to use the same userid as it's the same in all - * relations of a given inheritance tree. - */ - vardata->acl_ok = - rte->securityQuals == NIL && - ((pg_class_aclcheck(rte->relid, userid, - ACL_SELECT) == ACLCHECK_OK) || - (pg_attribute_aclcheck(rte->relid, varattno, userid, - ACL_SELECT) == ACLCHECK_OK)); - } + all_rows_selectable(root, var->varno, + bms_make_singleton(var->varattno - FirstLowInvalidHeapAttributeNumber)); } else { @@ -6024,6 +6276,214 @@ examine_simple_variable(PlannerInfo *root, Var *var, } } +/* + * all_rows_selectable + * Test whether the user has permission to select all rows from a given + * relation. + * + * Inputs: + * root: the planner info + * varno: the index of the relation (assumed to be an RTE_RELATION) + * varattnos: the attributes for which permission is required, or NULL if + * whole-table access is required + * + * Returns true if the user has the required select permissions, and there are + * no securityQuals from security barrier views or RLS policies. + * + * Note that if the relation is an inheritance child relation, securityQuals + * and access permissions are checked against the inheritance root parent (the + * relation actually mentioned in the query) --- see the comments in + * expand_single_inheritance_child() for an explanation of why it has to be + * done this way. + * + * If varattnos is non-NULL, its attribute numbers should be offset by + * FirstLowInvalidHeapAttributeNumber so that system attributes can be + * checked. If varattnos is NULL, only table-level SELECT privileges are + * checked, not any column-level privileges. + * + * Note: if the relation is accessed via a view, this function actually tests + * whether the view owner has permission to select from the relation. To + * ensure that the current user has permission, it is also necessary to check + * that the current user has permission to select from the view, which we do + * at planner-startup --- see subquery_planner(). + * + * This is exported so that other estimation functions can use it. + */ +bool +all_rows_selectable(PlannerInfo *root, Index varno, Bitmapset *varattnos) +{ + RelOptInfo *rel = find_base_rel_noerr(root, varno); + RangeTblEntry *rte = planner_rt_fetch(varno, root); + Oid userid; + int varattno; + + Assert(rte->rtekind == RTE_RELATION); + + /* + * Determine the user ID to use for privilege checks (either the current + * user or the view owner, if we're accessing the table via a view). + * + * Normally the relation will have an associated RelOptInfo from which we + * can find the userid, but it might not if it's a RETURNING Var for an + * INSERT target relation. In that case use the RTEPermissionInfo + * associated with the RTE. + * + * If we navigate up to a parent relation, we keep using the same userid, + * since it's the same in all relations of a given inheritance tree. + */ + if (rel) + userid = rel->userid; + else + { + RTEPermissionInfo *perminfo; + + perminfo = getRTEPermissionInfo(root->parse->rteperminfos, rte); + userid = perminfo->checkAsUser; + } + if (!OidIsValid(userid)) + userid = GetUserId(); + + /* + * Permissions and securityQuals must be checked on the table actually + * mentioned in the query, so if this is an inheritance child, navigate up + * to the inheritance root parent. If the user can read the whole table + * or the required columns there, then they can read from the child table + * too. For per-column checks, we must find out which of the root + * parent's attributes the child relation's attributes correspond to. + */ + if (root->append_rel_array != NULL) + { + AppendRelInfo *appinfo; + + appinfo = root->append_rel_array[varno]; + + /* + * Partitions are mapped to their immediate parent, not the root + * parent, so must be ready to walk up multiple AppendRelInfos. But + * stop if we hit a parent that is not RTE_RELATION --- that's a + * flattened UNION ALL subquery, not an inheritance parent. + */ + while (appinfo && + planner_rt_fetch(appinfo->parent_relid, + root)->rtekind == RTE_RELATION) + { + Bitmapset *parent_varattnos = NULL; + + /* + * For each child attribute, find the corresponding parent + * attribute. In rare cases, the attribute may be local to the + * child table, in which case, we've got to live with having no + * access to this column. + */ + varattno = -1; + while ((varattno = bms_next_member(varattnos, varattno)) >= 0) + { + AttrNumber attno; + AttrNumber parent_attno; + + attno = varattno + FirstLowInvalidHeapAttributeNumber; + + if (attno == InvalidAttrNumber) + { + /* + * Whole-row reference, so must map each column of the + * child to the parent table. + */ + for (attno = 1; attno <= appinfo->num_child_cols; attno++) + { + parent_attno = appinfo->parent_colnos[attno - 1]; + if (parent_attno == 0) + return false; /* attr is local to child */ + parent_varattnos = + bms_add_member(parent_varattnos, + parent_attno - FirstLowInvalidHeapAttributeNumber); + } + } + else + { + if (attno < 0) + { + /* System attnos are the same in all tables */ + parent_attno = attno; + } + else + { + if (attno > appinfo->num_child_cols) + return false; /* safety check */ + parent_attno = appinfo->parent_colnos[attno - 1]; + if (parent_attno == 0) + return false; /* attr is local to child */ + } + parent_varattnos = + bms_add_member(parent_varattnos, + parent_attno - FirstLowInvalidHeapAttributeNumber); + } + } + + /* If the parent is itself a child, continue up */ + varno = appinfo->parent_relid; + varattnos = parent_varattnos; + appinfo = root->append_rel_array[varno]; + } + + /* Perform the access check on this parent rel */ + rte = planner_rt_fetch(varno, root); + Assert(rte->rtekind == RTE_RELATION); + } + + /* + * For all rows to be accessible, there must be no securityQuals from + * security barrier views or RLS policies. + */ + if (rte->securityQuals != NIL) + return false; + + /* + * Test for table-level SELECT privilege. + * + * If varattnos is non-NULL, this is sufficient to give access to all + * requested attributes, even for a child table, since we have verified + * that all required child columns have matching parent columns. + * + * If varattnos is NULL (whole-table access requested), this doesn't + * necessarily guarantee that the user can read all columns of a child + * table, but we allow it anyway (see comments in examine_variable()) and + * don't bother checking any column privileges. + */ + if (pg_class_aclcheck(rte->relid, userid, ACL_SELECT) == ACLCHECK_OK) + return true; + + if (varattnos == NULL) + return false; /* whole-table access requested */ + + /* + * Don't have table-level SELECT privilege, so check per-column + * privileges. + */ + varattno = -1; + while ((varattno = bms_next_member(varattnos, varattno)) >= 0) + { + AttrNumber attno = varattno + FirstLowInvalidHeapAttributeNumber; + + if (attno == InvalidAttrNumber) + { + /* Whole-row reference, so must have access to all columns */ + if (pg_attribute_aclcheck_all(rte->relid, userid, ACL_SELECT, + ACLMASK_ALL) != ACLCHECK_OK) + return false; + } + else + { + if (pg_attribute_aclcheck(rte->relid, attno, userid, + ACL_SELECT) != ACLCHECK_OK) + return false; + } + } + + /* If we reach here, have all required column privileges */ + return true; +} + /* * examine_indexcol_variable * Try to look up statistical data about an index column/expression. @@ -6112,15 +6572,17 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index, /* * Check whether it is permitted to call func_oid passing some of the - * pg_statistic data in vardata. We allow this either if the user has SELECT - * privileges on the table or column underlying the pg_statistic data or if - * the function is marked leakproof. + * pg_statistic data in vardata. We allow this if either of the following + * conditions is met: (1) the user has SELECT privileges on the table or + * column underlying the pg_statistic data and there are no securityQuals from + * security barrier views or RLS policies, or (2) the function is marked + * leakproof. */ bool statistic_proc_security_check(VariableStatData *vardata, Oid func_oid) { if (vardata->acl_ok) - return true; + return true; /* have SELECT privs and no securityQuals */ if (!OidIsValid(func_oid)) return false; @@ -6514,6 +6976,13 @@ get_actual_variable_range(PlannerInfo *root, VariableStatData *vardata, if (index->hypothetical) continue; + /* + * get_actual_variable_endpoint uses the index-only-scan machinery, so + * ignore indexes that can't use it on their first column. + */ + if (!index->canreturn[0]) + continue; + /* * The first index column must match the desired variable, sortop, and * collation --- but we can use a descending-order index. @@ -6721,7 +7190,8 @@ get_actual_variable_endpoint(Relation heapRel, index_scan = index_beginscan(heapRel, indexRel, &SnapshotNonVacuumable, NULL, - 1, 0); + 1, 0, + SO_NONE); /* Set it up for index-only scan */ index_scan->xs_want_itup = true; index_rescan(index_scan, scankeys, 1, NULL, 0); @@ -6931,6 +7401,11 @@ index_other_operands_eval_cost(PlannerInfo *root, List *indexquals) return qual_arg_cost; } +/* + * Compute generic index access cost estimates. + * + * See struct GenericCosts in selfuncs.h for more info. + */ void genericcostestimate(PlannerInfo *root, IndexPath *path, @@ -7026,16 +7501,18 @@ genericcostestimate(PlannerInfo *root, * Estimate the number of index pages that will be retrieved. * * We use the simplistic method of taking a pro-rata fraction of the total - * number of index pages. In effect, this counts only leaf pages and not - * any overhead such as index metapage or upper tree levels. + * number of index leaf pages. We disregard any overhead such as index + * metapages or upper tree levels. * * In practice access to upper index levels is often nearly free because * those tend to stay in cache under load; moreover, the cost involved is * highly dependent on index type. We therefore ignore such costs here * and leave it to the caller to add a suitable charge if needed. */ - if (index->pages > 1 && index->tuples > 1) - numIndexPages = ceil(numIndexTuples * index->pages / index->tuples); + if (index->pages > costs->numNonLeafPages && index->tuples > 1) + numIndexPages = + ceil(numIndexTuples * (index->pages - costs->numNonLeafPages) + / index->tuples); else numIndexPages = 1.0; @@ -7626,9 +8103,18 @@ btcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, /* * Now do generic index cost estimation. + * + * While we expended effort to make realistic estimates of numIndexTuples + * and num_sa_scans, we are content to count only the btree metapage as + * non-leaf. btree fanout is typically high enough that upper pages are + * few relative to leaf pages, so accounting for them would move the + * estimates at most a percent or two. Given the uncertainty in just how + * many upper pages exist in a particular index, we'll skip trying to + * handle that. */ costs.numIndexTuples = numIndexTuples; costs.num_sa_scans = num_sa_scans; + costs.numNonLeafPages = 1; genericcostestimate(root, path, loop_count, &costs); @@ -7693,6 +8179,9 @@ hashcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, { GenericCosts costs = {0}; + /* As in btcostestimate, count only the metapage as non-leaf */ + costs.numNonLeafPages = 1; + genericcostestimate(root, path, loop_count, &costs); /* @@ -7737,6 +8226,8 @@ gistcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, GenericCosts costs = {0}; Cost descentCost; + /* GiST has no metapage, so we treat all pages as leaf pages */ + genericcostestimate(root, path, loop_count, &costs); /* @@ -7792,6 +8283,9 @@ spgcostestimate(PlannerInfo *root, IndexPath *path, double loop_count, GenericCosts costs = {0}; Cost descentCost; + /* As in btcostestimate, count only the metapage as non-leaf */ + costs.numNonLeafPages = 1; + genericcostestimate(root, path, loop_count, &costs); /* diff --git a/src/backend/utils/adt/skipsupport.c b/src/backend/utils/adt/skipsupport.c index 2bd35d2d27221..118721acf3f5d 100644 --- a/src/backend/utils/adt/skipsupport.c +++ b/src/backend/utils/adt/skipsupport.c @@ -4,7 +4,7 @@ * Support routines for B-Tree skip scan. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -38,7 +38,7 @@ PrepareSkipSupportFromOpclass(Oid opfamily, Oid opcintype, bool reverse) if (!OidIsValid(skipSupportFunction)) return NULL; - sksup = palloc(sizeof(SkipSupportData)); + sksup = palloc_object(SkipSupportData); OidFunctionCall1(skipSupportFunction, PointerGetDatum(sksup)); if (reverse) diff --git a/src/backend/utils/adt/tid.c b/src/backend/utils/adt/tid.c index 1b0df1117171a..9257886f1dad2 100644 --- a/src/backend/utils/adt/tid.c +++ b/src/backend/utils/adt/tid.c @@ -3,7 +3,7 @@ * tid.c * Functions for the built-in type tuple id * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,7 +17,6 @@ */ #include "postgres.h" -#include #include #include "access/sysattr.h" @@ -42,7 +41,7 @@ #define DELIM ',' #define NTIDARGS 2 -static ItemPointer currtid_for_view(Relation viewrel, ItemPointer tid); +static ItemPointer currtid_for_view(Relation viewrel, const ItemPointerData *tid); /* ---------------------------------------------------------------- * tidin @@ -84,7 +83,7 @@ tidin(PG_FUNCTION_ARGS) /* * Cope with possibility that unsigned long is wider than BlockNumber, in * which case strtoul will not raise an error for some values that are out - * of the range of BlockNumber. (See similar code in oidin().) + * of the range of BlockNumber. (See similar code in uint32in_subr().) */ #if SIZEOF_LONG > 4 if (cvt != (unsigned long) blockNumber && @@ -104,7 +103,7 @@ tidin(PG_FUNCTION_ARGS) "tid", str))); offsetNumber = (OffsetNumber) cvt; - result = (ItemPointer) palloc(sizeof(ItemPointerData)); + result = (ItemPointer) palloc_object(ItemPointerData); ItemPointerSet(result, blockNumber, offsetNumber); @@ -146,7 +145,7 @@ tidrecv(PG_FUNCTION_ARGS) blockNumber = pq_getmsgint(buf, sizeof(blockNumber)); offsetNumber = pq_getmsgint(buf, sizeof(offsetNumber)); - result = (ItemPointer) palloc(sizeof(ItemPointerData)); + result = (ItemPointer) palloc_object(ItemPointerData); ItemPointerSet(result, blockNumber, offsetNumber); @@ -280,6 +279,35 @@ hashtidextended(PG_FUNCTION_ARGS) seed); } +/* + * Extract the block number from a TID + * + * Returns int8 because BlockNumber is uint32, which exceeds the range of int4. + */ +Datum +tid_block(PG_FUNCTION_ARGS) +{ + ItemPointer tid = PG_GETARG_ITEMPOINTER(0); + + /* need to use NoCheck, as tidin allows InvalidBlockNumber */ + PG_RETURN_INT64((int64) ItemPointerGetBlockNumberNoCheck(tid)); +} + +/* + * Extract the offset number from a TID + * + * Returns int4 because OffsetNumber is uint16, which exceeds the range of + * int2. + */ +Datum +tid_offset(PG_FUNCTION_ARGS) +{ + ItemPointer tid = PG_GETARG_ITEMPOINTER(0); + + /* need to use NoCheck, as tidin allows InvalidOffsetNumber */ + PG_RETURN_INT32((int32) ItemPointerGetOffsetNumberNoCheck(tid)); +} + /* * Functions to get latest tid of a specified tuple. @@ -293,14 +321,14 @@ hashtidextended(PG_FUNCTION_ARGS) * relation "rel". */ static ItemPointer -currtid_internal(Relation rel, ItemPointer tid) +currtid_internal(Relation rel, const ItemPointerData *tid) { ItemPointer result; AclResult aclresult; Snapshot snapshot; TableScanDesc scan; - result = (ItemPointer) palloc(sizeof(ItemPointerData)); + result = (ItemPointer) palloc_object(ItemPointerData); aclresult = pg_class_aclcheck(RelationGetRelid(rel), GetUserId(), ACL_SELECT); @@ -335,7 +363,7 @@ currtid_internal(Relation rel, ItemPointer tid) * correspond to the CTID of a base relation. */ static ItemPointer -currtid_for_view(Relation viewrel, ItemPointer tid) +currtid_for_view(Relation viewrel, const ItemPointerData *tid) { TupleDesc att = RelationGetDescr(viewrel); RuleLock *rulelock; diff --git a/src/backend/utils/adt/timestamp.c b/src/backend/utils/adt/timestamp.c index 347089b762646..288d696be7750 100644 --- a/src/backend/utils/adt/timestamp.c +++ b/src/backend/utils/adt/timestamp.c @@ -3,7 +3,7 @@ * timestamp.c * Functions for the built-in SQL types "timestamp" and "interval". * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -40,15 +40,6 @@ #include "utils/skipsupport.h" #include "utils/sortsupport.h" -/* - * gcc's -ffast-math switch breaks routines that expect exact results from - * expressions like timeval / SECS_PER_HOUR, where timeval is double. - */ -#ifdef __FAST_MATH__ -#error -ffast-math is known to break this code -#endif - -#define SAMESIGN(a,b) (((a) < 0) == ((b) < 0)) /* Set at postmaster start */ TimestampTz PgStartTime; @@ -352,7 +343,8 @@ timestamp_scale(PG_FUNCTION_ARGS) result = timestamp; - AdjustTimestampForTypmod(&result, typmod, NULL); + if (!AdjustTimestampForTypmod(&result, typmod, fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_TIMESTAMP(result); } @@ -875,7 +867,8 @@ timestamptz_scale(PG_FUNCTION_ARGS) result = timestamp; - AdjustTimestampForTypmod(&result, typmod, NULL); + if (!AdjustTimestampForTypmod(&result, typmod, fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_TIMESTAMPTZ(result); } @@ -937,7 +930,7 @@ interval_in(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); switch (dtype) { @@ -1004,7 +997,7 @@ interval_recv(PG_FUNCTION_ARGS) int32 typmod = PG_GETARG_INT32(2); Interval *interval; - interval = (Interval *) palloc(sizeof(Interval)); + interval = palloc_object(Interval); interval->time = pq_getmsgint64(buf); interval->day = pq_getmsgint(buf, sizeof(interval->day)); @@ -1331,10 +1324,11 @@ interval_scale(PG_FUNCTION_ARGS) int32 typmod = PG_GETARG_INT32(1); Interval *result; - result = palloc(sizeof(Interval)); + result = palloc_object(Interval); *result = *interval; - AdjustIntervalForTypmod(result, typmod, NULL); + if (!AdjustIntervalForTypmod(result, typmod, fcinfo->context)) + PG_RETURN_NULL(); PG_RETURN_INTERVAL_P(result); } @@ -1545,7 +1539,7 @@ make_interval(PG_FUNCTION_ARGS) if (isinf(secs) || isnan(secs)) goto out_of_range; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* years and months -> months */ if (pg_mul_s32_overflow(years, MONTHS_PER_YEAR, &result->month) || @@ -2275,33 +2269,12 @@ timestamp_cmp(PG_FUNCTION_ARGS) PG_RETURN_INT32(timestamp_cmp_internal(dt1, dt2)); } -#if SIZEOF_DATUM < 8 -/* note: this is used for timestamptz also */ -static int -timestamp_fastcmp(Datum x, Datum y, SortSupport ssup) -{ - Timestamp a = DatumGetTimestamp(x); - Timestamp b = DatumGetTimestamp(y); - - return timestamp_cmp_internal(a, b); -} -#endif - Datum timestamp_sortsupport(PG_FUNCTION_ARGS) { SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0); -#if SIZEOF_DATUM >= 8 - - /* - * If this build has pass-by-value timestamps, then we can use a standard - * comparator function. - */ ssup->comparator = ssup_datum_signed_cmp; -#else - ssup->comparator = timestamp_fastcmp; -#endif PG_RETURN_VOID(); } @@ -2384,18 +2357,21 @@ int32 timestamp_cmp_timestamptz_internal(Timestamp timestampVal, TimestampTz dt2) { TimestampTz dt1; - int overflow; + ErrorSaveContext escontext = {T_ErrorSaveContext}; - dt1 = timestamp2timestamptz_opt_overflow(timestampVal, &overflow); - if (overflow > 0) - { - /* dt1 is larger than any finite timestamp, but less than infinity */ - return TIMESTAMP_IS_NOEND(dt2) ? -1 : +1; - } - if (overflow < 0) + dt1 = timestamp2timestamptz_safe(timestampVal, (Node *) &escontext); + if (escontext.error_occurred) { - /* dt1 is less than any finite timestamp, but more than -infinity */ - return TIMESTAMP_IS_NOBEGIN(dt2) ? +1 : -1; + if (TIMESTAMP_IS_NOEND(dt1)) + { + /* dt1 is larger than any finite timestamp, but less than infinity */ + return TIMESTAMP_IS_NOEND(dt2) ? -1 : +1; + } + if (TIMESTAMP_IS_NOBEGIN(dt1)) + { + /* dt1 is less than any finite timestamp, but more than -infinity */ + return TIMESTAMP_IS_NOBEGIN(dt2) ? +1 : -1; + } } return timestamptz_cmp_internal(dt1, dt2); @@ -2848,7 +2824,7 @@ timestamp_mi(PG_FUNCTION_ARGS) Timestamp dt2 = PG_GETARG_TIMESTAMP(1); Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle infinities. @@ -2943,7 +2919,7 @@ interval_justify_interval(PG_FUNCTION_ARGS) TimeOffset wholeday; int32 wholemonth; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); result->month = span->month; result->day = span->day; result->time = span->time; @@ -3022,7 +2998,7 @@ interval_justify_hours(PG_FUNCTION_ARGS) Interval *result; TimeOffset wholeday; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); result->month = span->month; result->day = span->day; result->time = span->time; @@ -3064,7 +3040,7 @@ interval_justify_days(PG_FUNCTION_ARGS) Interval *result; int32 wholemonth; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); result->month = span->month; result->day = span->day; result->time = span->time; @@ -3466,7 +3442,7 @@ interval_um(PG_FUNCTION_ARGS) Interval *interval = PG_GETARG_INTERVAL_P(0); Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); interval_um_internal(interval, result); PG_RETURN_INTERVAL_P(result); @@ -3524,7 +3500,7 @@ interval_pl(PG_FUNCTION_ARGS) Interval *span2 = PG_GETARG_INTERVAL_P(1); Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle infinities. @@ -3580,7 +3556,7 @@ interval_mi(PG_FUNCTION_ARGS) Interval *span2 = PG_GETARG_INTERVAL_P(1); Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle infinities. @@ -3634,7 +3610,7 @@ interval_mul(PG_FUNCTION_ARGS) orig_day = span->day; Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle NaN and infinities. @@ -3764,7 +3740,7 @@ interval_div(PG_FUNCTION_ARGS) orig_day = span->day; Interval *result; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); if (factor == 0.0) ereport(ERROR, @@ -3993,7 +3969,7 @@ makeIntervalAggState(FunctionCallInfo fcinfo) old_context = MemoryContextSwitchTo(agg_context); - state = (IntervalAggState *) palloc0(sizeof(IntervalAggState)); + state = palloc0_object(IntervalAggState); MemoryContextSwitchTo(old_context); @@ -4180,7 +4156,7 @@ interval_avg_deserialize(PG_FUNCTION_ARGS) initReadOnlyStringInfo(&buf, VARDATA_ANY(sstate), VARSIZE_ANY_EXHDR(sstate)); - result = (IntervalAggState *) palloc0(sizeof(IntervalAggState)); + result = palloc0_object(IntervalAggState); /* N */ result->N = pq_getmsgint64(&buf); @@ -4247,7 +4223,7 @@ interval_avg(PG_FUNCTION_ARGS) (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("interval out of range"))); - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); if (state->pInfcount > 0) INTERVAL_NOEND(result); else @@ -4284,7 +4260,7 @@ interval_sum(PG_FUNCTION_ARGS) (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("interval out of range"))); - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); if (state->pInfcount > 0) INTERVAL_NOEND(result); @@ -4317,7 +4293,7 @@ timestamp_age(PG_FUNCTION_ARGS) struct pg_tm tt2, *tm2 = &tt2; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle infinities. @@ -4465,7 +4441,7 @@ timestamptz_age(PG_FUNCTION_ARGS) int tz1; int tz2; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); /* * Handle infinities. @@ -4762,14 +4738,14 @@ timestamp_trunc(PG_FUNCTION_ARGS) tm->tm_year = ((tm->tm_year + 999) / 1000) * 1000 - 999; else tm->tm_year = -((999 - (tm->tm_year - 1)) / 1000) * 1000 + 1; - /* FALL THRU */ + pg_fallthrough; case DTK_CENTURY: /* see comments in timestamptz_trunc */ if (tm->tm_year > 0) tm->tm_year = ((tm->tm_year + 99) / 100) * 100 - 99; else tm->tm_year = -((99 - (tm->tm_year - 1)) / 100) * 100 + 1; - /* FALL THRU */ + pg_fallthrough; case DTK_DECADE: /* see comments in timestamptz_trunc */ if (val != DTK_MILLENNIUM && val != DTK_CENTURY) @@ -4779,25 +4755,25 @@ timestamp_trunc(PG_FUNCTION_ARGS) else tm->tm_year = -((8 - (tm->tm_year - 1)) / 10) * 10; } - /* FALL THRU */ + pg_fallthrough; case DTK_YEAR: tm->tm_mon = 1; - /* FALL THRU */ + pg_fallthrough; case DTK_QUARTER: tm->tm_mon = (3 * ((tm->tm_mon - 1) / 3)) + 1; - /* FALL THRU */ + pg_fallthrough; case DTK_MONTH: tm->tm_mday = 1; - /* FALL THRU */ + pg_fallthrough; case DTK_DAY: tm->tm_hour = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_HOUR: tm->tm_min = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_MINUTE: tm->tm_sec = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_SECOND: fsec = 0; break; @@ -4954,7 +4930,7 @@ timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp) case DTK_SECOND: case DTK_MILLISEC: case DTK_MICROSEC: - PG_RETURN_TIMESTAMPTZ(timestamp); + return timestamp; break; default: @@ -5008,14 +4984,14 @@ timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp) tm->tm_year = ((tm->tm_year + 999) / 1000) * 1000 - 999; else tm->tm_year = -((999 - (tm->tm_year - 1)) / 1000) * 1000 + 1; - /* FALL THRU */ + pg_fallthrough; case DTK_CENTURY: /* truncating to the century? as above: -100, 1, 101... */ if (tm->tm_year > 0) tm->tm_year = ((tm->tm_year + 99) / 100) * 100 - 99; else tm->tm_year = -((99 - (tm->tm_year - 1)) / 100) * 100 + 1; - /* FALL THRU */ + pg_fallthrough; case DTK_DECADE: /* @@ -5029,26 +5005,26 @@ timestamptz_trunc_internal(text *units, TimestampTz timestamp, pg_tz *tzp) else tm->tm_year = -((8 - (tm->tm_year - 1)) / 10) * 10; } - /* FALL THRU */ + pg_fallthrough; case DTK_YEAR: tm->tm_mon = 1; - /* FALL THRU */ + pg_fallthrough; case DTK_QUARTER: tm->tm_mon = (3 * ((tm->tm_mon - 1) / 3)) + 1; - /* FALL THRU */ + pg_fallthrough; case DTK_MONTH: tm->tm_mday = 1; - /* FALL THRU */ + pg_fallthrough; case DTK_DAY: tm->tm_hour = 0; redotz = true; /* for all cases >= DAY */ - /* FALL THRU */ + pg_fallthrough; case DTK_HOUR: tm->tm_min = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_MINUTE: tm->tm_sec = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_SECOND: fsec = 0; break; @@ -5138,7 +5114,7 @@ interval_trunc(PG_FUNCTION_ARGS) struct pg_itm tt, *tm = &tt; - result = (Interval *) palloc(sizeof(Interval)); + result = palloc_object(Interval); lowunits = downcase_truncate_identifier(VARDATA_ANY(units), VARSIZE_ANY_EXHDR(units), @@ -5179,7 +5155,7 @@ interval_trunc(PG_FUNCTION_ARGS) errmsg("unit \"%s\" not supported for type %s", lowunits, format_type_be(INTERVALOID)), (val == DTK_WEEK) ? errdetail("Months usually have fractional weeks.") : 0)); - result = 0; + result = NULL; } } @@ -5189,33 +5165,33 @@ interval_trunc(PG_FUNCTION_ARGS) case DTK_MILLENNIUM: /* caution: C division may have negative remainder */ tm->tm_year = (tm->tm_year / 1000) * 1000; - /* FALL THRU */ + pg_fallthrough; case DTK_CENTURY: /* caution: C division may have negative remainder */ tm->tm_year = (tm->tm_year / 100) * 100; - /* FALL THRU */ + pg_fallthrough; case DTK_DECADE: /* caution: C division may have negative remainder */ tm->tm_year = (tm->tm_year / 10) * 10; - /* FALL THRU */ + pg_fallthrough; case DTK_YEAR: tm->tm_mon = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_QUARTER: tm->tm_mon = 3 * (tm->tm_mon / 3); - /* FALL THRU */ + pg_fallthrough; case DTK_MONTH: tm->tm_mday = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_DAY: tm->tm_hour = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_HOUR: tm->tm_min = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_MINUTE: tm->tm_sec = 0; - /* FALL THRU */ + pg_fallthrough; case DTK_SECOND: tm->tm_usec = 0; break; @@ -5312,10 +5288,10 @@ isoweekdate2date(int isoweek, int wday, int *year, int *mon, int *mday) int date2isoweek(int year, int mon, int mday) { - float8 result; int day0, day4, - dayn; + dayn, + week; /* current day */ dayn = date2j(year, mon, mday); @@ -5338,13 +5314,13 @@ date2isoweek(int year, int mon, int mday) day0 = j2day(day4 - 1); } - result = (dayn - (day4 - day0)) / 7 + 1; + week = (dayn - (day4 - day0)) / 7 + 1; /* * Sometimes the last few days in a year will fall into the first week of * the next year, so check for this. */ - if (result >= 52) + if (week >= 52) { day4 = date2j(year + 1, 1, 4); @@ -5352,10 +5328,10 @@ date2isoweek(int year, int mon, int mday) day0 = j2day(day4 - 1); if (dayn >= day4 - day0) - result = (dayn - (day4 - day0)) / 7 + 1; + week = (dayn - (day4 - day0)) / 7 + 1; } - return (int) result; + return week; } @@ -5367,10 +5343,10 @@ date2isoweek(int year, int mon, int mday) int date2isoyear(int year, int mon, int mday) { - float8 result; int day0, day4, - dayn; + dayn, + week; /* current day */ dayn = date2j(year, mon, mday); @@ -5395,13 +5371,13 @@ date2isoyear(int year, int mon, int mday) year--; } - result = (dayn - (day4 - day0)) / 7 + 1; + week = (dayn - (day4 - day0)) / 7 + 1; /* * Sometimes the last few days in a year will fall into the first week of * the next year, so check for this. */ - if (result >= 52) + if (week >= 52) { day4 = date2j(year + 1, 1, 4); @@ -5650,11 +5626,11 @@ timestamp_part_common(PG_FUNCTION_ARGS, bool retnumeric) case DTK_JULIAN: if (retnumeric) - PG_RETURN_NUMERIC(numeric_add_opt_error(int64_to_numeric(date2j(tm->tm_year, tm->tm_mon, tm->tm_mday)), - numeric_div_opt_error(int64_to_numeric(((((tm->tm_hour * MINS_PER_HOUR) + tm->tm_min) * SECS_PER_MINUTE) + tm->tm_sec) * INT64CONST(1000000) + fsec), - int64_to_numeric(SECS_PER_DAY * INT64CONST(1000000)), - NULL), - NULL)); + PG_RETURN_NUMERIC(numeric_add_safe(int64_to_numeric(date2j(tm->tm_year, tm->tm_mon, tm->tm_mday)), + numeric_div_safe(int64_to_numeric(((((tm->tm_hour * MINS_PER_HOUR) + tm->tm_min) * SECS_PER_MINUTE) + tm->tm_sec) * INT64CONST(1000000) + fsec), + int64_to_numeric(SECS_PER_DAY * INT64CONST(1000000)), + NULL), + NULL)); else PG_RETURN_FLOAT8(date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) + ((((tm->tm_hour * MINS_PER_HOUR) + tm->tm_min) * SECS_PER_MINUTE) + @@ -5706,11 +5682,11 @@ timestamp_part_common(PG_FUNCTION_ARGS, bool retnumeric) result = int64_div_fast_to_numeric(timestamp - epoch, 6); else { - result = numeric_div_opt_error(numeric_sub_opt_error(int64_to_numeric(timestamp), - int64_to_numeric(epoch), - NULL), - int64_to_numeric(1000000), - NULL); + result = numeric_div_safe(numeric_sub_safe(int64_to_numeric(timestamp), + int64_to_numeric(epoch), + NULL), + int64_to_numeric(1000000), + NULL); result = DatumGetNumeric(DirectFunctionCall2(numeric_round, NumericGetDatum(result), Int32GetDatum(6))); @@ -5924,11 +5900,11 @@ timestamptz_part_common(PG_FUNCTION_ARGS, bool retnumeric) case DTK_JULIAN: if (retnumeric) - PG_RETURN_NUMERIC(numeric_add_opt_error(int64_to_numeric(date2j(tm->tm_year, tm->tm_mon, tm->tm_mday)), - numeric_div_opt_error(int64_to_numeric(((((tm->tm_hour * MINS_PER_HOUR) + tm->tm_min) * SECS_PER_MINUTE) + tm->tm_sec) * INT64CONST(1000000) + fsec), - int64_to_numeric(SECS_PER_DAY * INT64CONST(1000000)), - NULL), - NULL)); + PG_RETURN_NUMERIC(numeric_add_safe(int64_to_numeric(date2j(tm->tm_year, tm->tm_mon, tm->tm_mday)), + numeric_div_safe(int64_to_numeric(((((tm->tm_hour * MINS_PER_HOUR) + tm->tm_min) * SECS_PER_MINUTE) + tm->tm_sec) * INT64CONST(1000000) + fsec), + int64_to_numeric(SECS_PER_DAY * INT64CONST(1000000)), + NULL), + NULL)); else PG_RETURN_FLOAT8(date2j(tm->tm_year, tm->tm_mon, tm->tm_mday) + ((((tm->tm_hour * MINS_PER_HOUR) + tm->tm_min) * SECS_PER_MINUTE) + @@ -5977,11 +5953,11 @@ timestamptz_part_common(PG_FUNCTION_ARGS, bool retnumeric) result = int64_div_fast_to_numeric(timestamp - epoch, 6); else { - result = numeric_div_opt_error(numeric_sub_opt_error(int64_to_numeric(timestamp), - int64_to_numeric(epoch), - NULL), - int64_to_numeric(1000000), - NULL); + result = numeric_div_safe(numeric_sub_safe(int64_to_numeric(timestamp), + int64_to_numeric(epoch), + NULL), + int64_to_numeric(1000000), + NULL); result = DatumGetNumeric(DirectFunctionCall2(numeric_round, NumericGetDatum(result), Int32GetDatum(6))); @@ -6268,9 +6244,9 @@ interval_part_common(PG_FUNCTION_ARGS, bool retnumeric) result = int64_div_fast_to_numeric(val, 6); else result = - numeric_add_opt_error(int64_div_fast_to_numeric(interval->time, 6), - int64_to_numeric(secs_from_day_month), - NULL); + numeric_add_safe(int64_div_fast_to_numeric(interval->time, 6), + int64_to_numeric(secs_from_day_month), + NULL); PG_RETURN_NUMERIC(result); } @@ -6448,22 +6424,27 @@ Datum timestamp_timestamptz(PG_FUNCTION_ARGS) { Timestamp timestamp = PG_GETARG_TIMESTAMP(0); + TimestampTz result; + + result = timestamp2timestamptz_safe(timestamp, fcinfo->context); + if (SOFT_ERROR_OCCURRED(fcinfo->context)) + PG_RETURN_NULL(); - PG_RETURN_TIMESTAMPTZ(timestamp2timestamptz(timestamp)); + PG_RETURN_TIMESTAMPTZ(result); } /* * Convert timestamp to timestamp with time zone. * - * On successful conversion, *overflow is set to zero if it's not NULL. + * If the timestamp is finite but out of the valid range for timestamptz, + * error handling proceeds based on escontext. * - * If the timestamp is finite but out of the valid range for timestamptz, then: - * if overflow is NULL, we throw an out-of-range error. - * if overflow is not NULL, we store +1 or -1 there to indicate the sign - * of the overflow, and return the appropriate timestamptz infinity. + * If escontext is NULL, we throw an out-of-range error (hard error). + * If escontext is not NULL, we return NOBEGIN or NOEND for lower bound or + * upper bound overflow, respectively, and record a soft error. */ TimestampTz -timestamp2timestamptz_opt_overflow(Timestamp timestamp, int *overflow) +timestamp2timestamptz_safe(Timestamp timestamp, Node *escontext) { TimestampTz result; struct pg_tm tt, @@ -6471,13 +6452,10 @@ timestamp2timestamptz_opt_overflow(Timestamp timestamp, int *overflow) fsec_t fsec; int tz; - if (overflow) - *overflow = 0; - if (TIMESTAMP_NOT_FINITE(timestamp)) return timestamp; - /* We don't expect this to fail, but check it pro forma */ + /* timestamp2tm should not fail on valid timestamps, but cope */ if (timestamp2tm(timestamp, NULL, tm, &fsec, NULL, NULL) == 0) { tz = DetermineTimeZoneOffset(tm, session_timezone); @@ -6485,30 +6463,17 @@ timestamp2timestamptz_opt_overflow(Timestamp timestamp, int *overflow) result = dt2local(timestamp, -tz); if (IS_VALID_TIMESTAMP(result)) - { - return result; - } - else if (overflow) - { - if (result < MIN_TIMESTAMP) - { - *overflow = -1; - TIMESTAMP_NOBEGIN(result); - } - else - { - *overflow = 1; - TIMESTAMP_NOEND(result); - } return result; - } } - ereport(ERROR, + if (timestamp < 0) + TIMESTAMP_NOBEGIN(result); + else + TIMESTAMP_NOEND(result); + + ereturn(escontext, result, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); - - return 0; } /* @@ -6517,7 +6482,7 @@ timestamp2timestamptz_opt_overflow(Timestamp timestamp, int *overflow) static TimestampTz timestamp2timestamptz(Timestamp timestamp) { - return timestamp2timestamptz_opt_overflow(timestamp, NULL); + return timestamp2timestamptz_safe(timestamp, NULL); } /* timestamptz_timestamp() @@ -6527,12 +6492,36 @@ Datum timestamptz_timestamp(PG_FUNCTION_ARGS) { TimestampTz timestamp = PG_GETARG_TIMESTAMPTZ(0); + Timestamp result; + + result = timestamptz2timestamp_safe(timestamp, fcinfo->context); + if (unlikely(SOFT_ERROR_OCCURRED(fcinfo->context))) + PG_RETURN_NULL(); - PG_RETURN_TIMESTAMP(timestamptz2timestamp(timestamp)); + PG_RETURN_TIMESTAMP(result); } +/* + * Convert timestamptz to timestamp, throwing error for overflow. + */ static Timestamp timestamptz2timestamp(TimestampTz timestamp) +{ + return timestamptz2timestamp_safe(timestamp, NULL); +} + +/* + * Convert timestamp with time zone to timestamp. + * + * If the timestamptz is finite but out of the valid range for timestamp, + * error handling proceeds based on escontext. + * + * If escontext is NULL, we throw an out-of-range error (hard error). + * If escontext is not NULL, we return NOBEGIN or NOEND for lower bound or + * upper bound overflow, respectively, and record a soft error. + */ +Timestamp +timestamptz2timestamp_safe(TimestampTz timestamp, Node *escontext) { Timestamp result; struct pg_tm tt, @@ -6545,13 +6534,27 @@ timestamptz2timestamp(TimestampTz timestamp) else { if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0) - ereport(ERROR, + { + if (timestamp < 0) + TIMESTAMP_NOBEGIN(result); + else + TIMESTAMP_NOEND(result); + + ereturn(escontext, result, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); + } if (tm2timestamp(tm, fsec, NULL, &result) != 0) - ereport(ERROR, + { + if (timestamp < 0) + TIMESTAMP_NOBEGIN(result); + else + TIMESTAMP_NOEND(result); + + ereturn(escontext, result, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE), errmsg("timestamp out of range"))); + } } return result; } @@ -6688,8 +6691,7 @@ generate_series_timestamp(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - fctx = (generate_series_timestamp_fctx *) - palloc(sizeof(generate_series_timestamp_fctx)); + fctx = palloc_object(generate_series_timestamp_fctx); /* * Use fctx to keep state from call to call. Seed current with the @@ -6773,8 +6775,7 @@ generate_series_timestamptz_internal(FunctionCallInfo fcinfo) oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); /* allocate memory for user context */ - fctx = (generate_series_timestamptz_fctx *) - palloc(sizeof(generate_series_timestamptz_fctx)); + fctx = palloc_object(generate_series_timestamptz_fctx); /* * Use fctx to keep state from call to call. Seed current with the diff --git a/src/backend/utils/adt/trigfuncs.c b/src/backend/utils/adt/trigfuncs.c index 8371b7eb9f565..c9d88216a3289 100644 --- a/src/backend/utils/adt/trigfuncs.c +++ b/src/backend/utils/adt/trigfuncs.c @@ -4,7 +4,7 @@ * Builtin functions for useful trigger support. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/utils/adt/trigfuncs.c diff --git a/src/backend/utils/adt/tsginidx.c b/src/backend/utils/adt/tsginidx.c index 2712fd89df095..725e6e5da4863 100644 --- a/src/backend/utils/adt/tsginidx.c +++ b/src/backend/utils/adt/tsginidx.c @@ -3,7 +3,7 @@ * tsginidx.c * GIN support functions for tsvector_ops * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -73,7 +73,7 @@ gin_extract_tsvector(PG_FUNCTION_ARGS) int i; WordEntry *we = ARRPTR(vector); - entries = (Datum *) palloc(sizeof(Datum) * vector->size); + entries = palloc_array(Datum, vector->size); for (i = 0; i < vector->size; i++) { @@ -95,12 +95,14 @@ gin_extract_tsquery(PG_FUNCTION_ARGS) { TSQuery query = PG_GETARG_TSQUERY(0); int32 *nentries = (int32 *) PG_GETARG_POINTER(1); - - /* StrategyNumber strategy = PG_GETARG_UINT16(2); */ +#ifdef NOT_USED + StrategyNumber strategy = PG_GETARG_UINT16(2); +#endif bool **ptr_partialmatch = (bool **) PG_GETARG_POINTER(3); Pointer **extra_data = (Pointer **) PG_GETARG_POINTER(4); - - /* bool **nullFlags = (bool **) PG_GETARG_POINTER(5); */ +#ifdef NOT_USED + bool **nullFlags = (bool **) PG_GETARG_POINTER(5); +#endif int32 *searchMode = (int32 *) PG_GETARG_POINTER(6); Datum *entries = NULL; @@ -133,16 +135,16 @@ gin_extract_tsquery(PG_FUNCTION_ARGS) } *nentries = j; - entries = (Datum *) palloc(sizeof(Datum) * j); - partialmatch = *ptr_partialmatch = (bool *) palloc(sizeof(bool) * j); + entries = palloc_array(Datum, j); + partialmatch = *ptr_partialmatch = palloc_array(bool, j); /* * Make map to convert item's number to corresponding operand's (the * same, entry's) number. Entry's number is used in check array in * consistent method. We use the same map for each entry. */ - *extra_data = (Pointer *) palloc(sizeof(Pointer) * j); - map_item_operand = (int *) palloc0(sizeof(int) * query->size); + *extra_data = palloc_array(Pointer, j); + map_item_operand = palloc0_array(int, query->size); /* Now rescan the VAL items and fill in the arrays */ j = 0; @@ -214,11 +216,13 @@ Datum gin_tsquery_consistent(PG_FUNCTION_ARGS) { bool *check = (bool *) PG_GETARG_POINTER(0); - - /* StrategyNumber strategy = PG_GETARG_UINT16(1); */ +#ifdef NOT_USED + StrategyNumber strategy = PG_GETARG_UINT16(1); +#endif TSQuery query = PG_GETARG_TSQUERY(2); - - /* int32 nkeys = PG_GETARG_INT32(3); */ +#ifdef NOT_USED + int32 nkeys = PG_GETARG_INT32(3); +#endif Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); bool *recheck = (bool *) PG_GETARG_POINTER(5); bool res = false; @@ -263,11 +267,13 @@ Datum gin_tsquery_triconsistent(PG_FUNCTION_ARGS) { GinTernaryValue *check = (GinTernaryValue *) PG_GETARG_POINTER(0); - - /* StrategyNumber strategy = PG_GETARG_UINT16(1); */ +#ifdef NOT_USED + StrategyNumber strategy = PG_GETARG_UINT16(1); +#endif TSQuery query = PG_GETARG_TSQUERY(2); - - /* int32 nkeys = PG_GETARG_INT32(3); */ +#ifdef NOT_USED + int32 nkeys = PG_GETARG_INT32(3); +#endif Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); GinTernaryValue res = GIN_FALSE; diff --git a/src/backend/utils/adt/tsgistidx.c b/src/backend/utils/adt/tsgistidx.c index 935187b37c749..e35a25797a01a 100644 --- a/src/backend/utils/adt/tsgistidx.c +++ b/src/backend/utils/adt/tsgistidx.c @@ -3,7 +3,7 @@ * tsgistidx.c * GiST support functions for tsvector_ops * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -212,7 +212,7 @@ gtsvector_compress(PG_FUNCTION_ARGS) res = ressign; } - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, false); @@ -231,7 +231,7 @@ gtsvector_compress(PG_FUNCTION_ARGS) } res = gtsvector_alloc(SIGNKEY | ALLISTRUE, siglen, sign); - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(res), entry->rel, entry->page, entry->offset, false); @@ -251,7 +251,7 @@ gtsvector_decompress(PG_FUNCTION_ARGS) if (key != (SignTSVector *) DatumGetPointer(entry->key)) { - GISTENTRY *retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + GISTENTRY *retval = palloc_object(GISTENTRY); gistentryinit(*retval, PointerGetDatum(key), entry->rel, entry->page, @@ -326,9 +326,10 @@ gtsvector_consistent(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TSQuery query = PG_GETARG_TSQUERY(1); - - /* StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); */ - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); SignTSVector *key = (SignTSVector *) DatumGetPointer(entry->key); @@ -641,7 +642,7 @@ gtsvector_picksplit(PG_FUNCTION_ARGS) v->spl_left = (OffsetNumber *) palloc(nbytes); v->spl_right = (OffsetNumber *) palloc(nbytes); - cache = (CACHESIGN *) palloc(sizeof(CACHESIGN) * (maxoff + 2)); + cache = palloc_array(CACHESIGN, maxoff + 2); cache_sign = palloc(siglen * (maxoff + 2)); for (j = 0; j < maxoff + 2; j++) @@ -688,7 +689,7 @@ gtsvector_picksplit(PG_FUNCTION_ARGS) maxoff = OffsetNumberNext(maxoff); fillcache(&cache[maxoff], GETENTRY(entryvec, maxoff), siglen); /* sort before ... */ - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) { costvector[j - 1].pos = j; diff --git a/src/backend/utils/adt/tsquery.c b/src/backend/utils/adt/tsquery.c index 717de8073d58d..7e54f36c2a7b4 100644 --- a/src/backend/utils/adt/tsquery.c +++ b/src/backend/utils/adt/tsquery.c @@ -3,7 +3,7 @@ * tsquery.c * I/O functions for tsquery * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -120,7 +120,7 @@ get_modifiers(char *buf, int16 *weight, bool *prefix) return buf; buf++; - while (*buf && pg_mblen(buf) == 1) + while (*buf && pg_mblen_cstr(buf) == 1) { switch (*buf) { @@ -259,12 +259,12 @@ parse_or_operator(TSQueryParserState pstate) return false; /* it shouldn't be a part of any word */ - if (t_iseq(ptr, '-') || t_iseq(ptr, '_') || t_isalnum(ptr)) + if (t_iseq(ptr, '-') || t_iseq(ptr, '_') || t_isalnum_cstr(ptr)) return false; for (;;) { - ptr += pg_mblen(ptr); + ptr += pg_mblen_cstr(ptr); if (*ptr == '\0') /* got end of string without operand */ return false; @@ -390,7 +390,7 @@ gettoken_query_standard(TSQueryParserState state, int8 *operator, break; } - state->buf += pg_mblen(state->buf); + state->buf += pg_mblen_cstr(state->buf); } } @@ -502,7 +502,7 @@ gettoken_query_websearch(TSQueryParserState state, int8 *operator, break; } - state->buf += pg_mblen(state->buf); + state->buf += pg_mblen_cstr(state->buf); } } @@ -534,7 +534,7 @@ pushOperator(TSQueryParserState state, int8 oper, int16 distance) Assert(oper == OP_NOT || oper == OP_AND || oper == OP_OR || oper == OP_PHRASE); - tmp = (QueryOperator *) palloc0(sizeof(QueryOperator)); + tmp = palloc0_object(QueryOperator); tmp->type = QI_OPR; tmp->oper = oper; tmp->distance = (oper == OP_PHRASE) ? distance : 0; @@ -559,7 +559,7 @@ pushValue_internal(TSQueryParserState state, pg_crc32 valcrc, int distance, int errmsg("operand is too long in tsquery: \"%s\"", state->buffer))); - tmp = (QueryOperand *) palloc0(sizeof(QueryOperand)); + tmp = palloc0_object(QueryOperand); tmp->type = QI_VAL; tmp->weight = weight; tmp->prefix = prefix; @@ -617,7 +617,7 @@ pushStop(TSQueryParserState state) { QueryOperand *tmp; - tmp = (QueryOperand *) palloc0(sizeof(QueryOperand)); + tmp = palloc0_object(QueryOperand); tmp->type = QI_VALSTOP; state->polstr = lcons(tmp, state->polstr); @@ -671,7 +671,7 @@ cleanOpStack(TSQueryParserState state, static void makepol(TSQueryParserState state, PushFunction pushval, - Datum opaque) + void *opaque) { int8 operator = 0; ts_tokentype type; @@ -816,7 +816,7 @@ findoprnd(QueryItem *ptr, int size, bool *needcleanup) TSQuery parse_tsquery(char *buf, PushFunction pushval, - Datum opaque, + void *opaque, int flags, Node *escontext) { @@ -939,7 +939,7 @@ parse_tsquery(char *buf, } static void -pushval_asis(Datum opaque, TSQueryParserState state, char *strval, int lenval, +pushval_asis(void *opaque, TSQueryParserState state, char *strval, int lenval, int16 weight, bool prefix) { pushValue(state, strval, lenval, weight, prefix); @@ -956,7 +956,7 @@ tsqueryin(PG_FUNCTION_ARGS) PG_RETURN_TSQUERY(parse_tsquery(in, pushval_asis, - PointerGetDatum(NULL), + NULL, 0, escontext)); } @@ -1014,9 +1014,8 @@ infix(INFIX *in, int parentPriority, bool rightPhraseOp) *(in->cur) = '\\'; in->cur++; } - COPYCHAR(in->cur, op); - clen = pg_mblen(op); + clen = ts_copychar_cstr(in->cur, op); op += clen; in->cur += clen; } @@ -1101,7 +1100,7 @@ infix(INFIX *in, int parentPriority, bool rightPhraseOp) nrm.curpol = in->curpol; nrm.op = in->op; nrm.buflen = 16; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); /* get right operand */ infix(&nrm, priority, (op == OP_PHRASE)); @@ -1157,7 +1156,7 @@ tsqueryout(PG_FUNCTION_ARGS) } nrm.curpol = GETQUERY(query); nrm.buflen = 32; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); *(nrm.cur) = '\0'; nrm.op = GETOPERAND(query); infix(&nrm, -1 /* lowest priority */ , false); @@ -1385,7 +1384,7 @@ tsquerytree(PG_FUNCTION_ARGS) { nrm.curpol = q; nrm.buflen = 32; - nrm.cur = nrm.buf = (char *) palloc(sizeof(char) * nrm.buflen); + nrm.cur = nrm.buf = palloc_array(char, nrm.buflen); *(nrm.cur) = '\0'; nrm.op = GETOPERAND(query); infix(&nrm, -1, false); diff --git a/src/backend/utils/adt/tsquery_cleanup.c b/src/backend/utils/adt/tsquery_cleanup.c index 590d7c7989c7e..1dc9240970ef9 100644 --- a/src/backend/utils/adt/tsquery_cleanup.c +++ b/src/backend/utils/adt/tsquery_cleanup.c @@ -4,7 +4,7 @@ * Cleanup query from NOT values and/or stopword * Utility functions to correct work. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -32,7 +32,7 @@ typedef struct NODE static NODE * maketree(QueryItem *in) { - NODE *node = (NODE *) palloc(sizeof(NODE)); + NODE *node = palloc_object(NODE); /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); diff --git a/src/backend/utils/adt/tsquery_gist.c b/src/backend/utils/adt/tsquery_gist.c index f7f94c1c760f5..3108442a54ade 100644 --- a/src/backend/utils/adt/tsquery_gist.c +++ b/src/backend/utils/adt/tsquery_gist.c @@ -3,7 +3,7 @@ * tsquery_gist.c * GiST index support for tsquery * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -33,7 +33,7 @@ gtsquery_compress(PG_FUNCTION_ARGS) { TSQuerySign sign; - retval = (GISTENTRY *) palloc(sizeof(GISTENTRY)); + retval = palloc_object(GISTENTRY); sign = makeTSQuerySign(DatumGetTSQuery(entry->key)); gistentryinit(*retval, TSQuerySignGetDatum(sign), @@ -55,8 +55,9 @@ gtsquery_consistent(PG_FUNCTION_ARGS) GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); TSQuery query = PG_GETARG_TSQUERY(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); - - /* Oid subtype = PG_GETARG_OID(3); */ +#ifdef NOT_USED + Oid subtype = PG_GETARG_OID(3); +#endif bool *recheck = (bool *) PG_GETARG_POINTER(4); TSQuerySign key = DatumGetTSQuerySign(entry->key); TSQuerySign sq = makeTSQuerySign(query); @@ -213,7 +214,7 @@ gtsquery_picksplit(PG_FUNCTION_ARGS) datum_r = GETENTRY(entryvec, seed_2); maxoff = OffsetNumberNext(maxoff); - costvector = (SPLITCOST *) palloc(sizeof(SPLITCOST) * maxoff); + costvector = palloc_array(SPLITCOST, maxoff); for (j = FirstOffsetNumber; j <= maxoff; j = OffsetNumberNext(j)) { costvector[j - 1].pos = j; diff --git a/src/backend/utils/adt/tsquery_op.c b/src/backend/utils/adt/tsquery_op.c index bb77e923062cf..12bee7c970c91 100644 --- a/src/backend/utils/adt/tsquery_op.c +++ b/src/backend/utils/adt/tsquery_op.c @@ -3,7 +3,7 @@ * tsquery_op.c * Various operations with tsquery * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -32,17 +32,17 @@ tsquery_numnode(PG_FUNCTION_ARGS) static QTNode * join_tsqueries(TSQuery a, TSQuery b, int8 operator, uint16 distance) { - QTNode *res = (QTNode *) palloc0(sizeof(QTNode)); + QTNode *res = palloc0_object(QTNode); res->flags |= QTN_NEEDFREE; - res->valnode = (QueryItem *) palloc0(sizeof(QueryItem)); + res->valnode = palloc0_object(QueryItem); res->valnode->type = QI_OPR; res->valnode->qoperator.oper = operator; if (operator == OP_PHRASE) res->valnode->qoperator.distance = distance; - res->child = (QTNode **) palloc0(sizeof(QTNode *) * 2); + res->child = palloc0_array(QTNode *, 2); res->child[0] = QT2QTN(GETQUERY(b), GETOPERAND(b)); res->child[1] = QT2QTN(GETQUERY(a), GETOPERAND(a)); res->nchild = 2; @@ -165,15 +165,15 @@ tsquery_not(PG_FUNCTION_ARGS) if (a->size == 0) PG_RETURN_POINTER(a); - res = (QTNode *) palloc0(sizeof(QTNode)); + res = palloc0_object(QTNode); res->flags |= QTN_NEEDFREE; - res->valnode = (QueryItem *) palloc0(sizeof(QueryItem)); + res->valnode = palloc0_object(QueryItem); res->valnode->type = QI_OPR; res->valnode->qoperator.oper = OP_NOT; - res->child = (QTNode **) palloc0(sizeof(QTNode *)); + res->child = palloc0_object(QTNode *); res->child[0] = QT2QTN(GETQUERY(a), GETOPERAND(a)); res->nchild = 1; @@ -272,7 +272,7 @@ collectTSQueryValues(TSQuery a, int *nvalues_p) int nvalues = 0; int i; - values = (char **) palloc(sizeof(char *) * a->size); + values = palloc_array(char *, a->size); for (i = 0; i < a->size; i++) { diff --git a/src/backend/utils/adt/tsquery_rewrite.c b/src/backend/utils/adt/tsquery_rewrite.c index 2f9e81fbfea25..aace2a4c7d6d4 100644 --- a/src/backend/utils/adt/tsquery_rewrite.c +++ b/src/backend/utils/adt/tsquery_rewrite.c @@ -3,7 +3,7 @@ * tsquery_rewrite.c * Utilities for reconstructing tsquery * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/backend/utils/adt/tsquery_util.c b/src/backend/utils/adt/tsquery_util.c index 1c24b041aa29c..2eb215609f74d 100644 --- a/src/backend/utils/adt/tsquery_util.c +++ b/src/backend/utils/adt/tsquery_util.c @@ -3,7 +3,7 @@ * tsquery_util.c * Utilities for tsquery datatype * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -24,7 +24,7 @@ QTNode * QT2QTN(QueryItem *in, char *operand) { - QTNode *node = (QTNode *) palloc0(sizeof(QTNode)); + QTNode *node = palloc0_object(QTNode); /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); @@ -33,7 +33,7 @@ QT2QTN(QueryItem *in, char *operand) if (in->type == QI_OPR) { - node->child = (QTNode **) palloc0(sizeof(QTNode *) * 2); + node->child = palloc0_array(QTNode *, 2); node->child[0] = QT2QTN(in + 1, operand); node->sign = node->child[0]->sign; if (in->qoperator.oper == OP_NOT) @@ -226,7 +226,7 @@ QTNTernary(QTNode *in) int oldnchild = in->nchild; in->nchild += cc->nchild - 1; - in->child = (QTNode **) repalloc(in->child, in->nchild * sizeof(QTNode *)); + in->child = repalloc_array(in->child, QTNode *, in->nchild); if (i + 1 != oldnchild) memmove(in->child + i + cc->nchild, in->child + i + 1, @@ -262,10 +262,10 @@ QTNBinary(QTNode *in) while (in->nchild > 2) { - QTNode *nn = (QTNode *) palloc0(sizeof(QTNode)); + QTNode *nn = palloc0_object(QTNode); - nn->valnode = (QueryItem *) palloc0(sizeof(QueryItem)); - nn->child = (QTNode **) palloc0(sizeof(QTNode *) * 2); + nn->valnode = palloc0_object(QueryItem); + nn->child = palloc0_array(QTNode *, 2); nn->nchild = 2; nn->flags = QTN_NEEDFREE; @@ -400,10 +400,10 @@ QTNCopy(QTNode *in) /* since this function recurses, it could be driven to stack overflow. */ check_stack_depth(); - out = (QTNode *) palloc(sizeof(QTNode)); + out = palloc_object(QTNode); *out = *in; - out->valnode = (QueryItem *) palloc(sizeof(QueryItem)); + out->valnode = palloc_object(QueryItem); *(out->valnode) = *(in->valnode); out->flags |= QTN_NEEDFREE; @@ -418,7 +418,7 @@ QTNCopy(QTNode *in) { int i; - out->child = (QTNode **) palloc(sizeof(QTNode *) * in->nchild); + out->child = palloc_array(QTNode *, in->nchild); for (i = 0; i < in->nchild; i++) out->child[i] = QTNCopy(in->child[i]); diff --git a/src/backend/utils/adt/tsrank.c b/src/backend/utils/adt/tsrank.c index e863aa586535d..d35e5528d0a9f 100644 --- a/src/backend/utils/adt/tsrank.c +++ b/src/backend/utils/adt/tsrank.c @@ -3,7 +3,7 @@ * tsrank.c * rank tsvector by tsquery * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -160,7 +160,7 @@ SortAndUniqItems(TSQuery q, int *size) **ptr, **prevptr; - ptr = res = (QueryOperand **) palloc(sizeof(QueryOperand *) * *size); + ptr = res = palloc_array(QueryOperand *, *size); /* Collect all operands from the tree to res */ while ((*size)--) @@ -225,7 +225,7 @@ calc_rank_and(const float *w, TSVector t, TSQuery q) pfree(item); return calc_rank_or(w, t, q); } - pos = (WordEntryPosVector **) palloc0(sizeof(WordEntryPosVector *) * q->size); + pos = palloc0_array(WordEntryPosVector *, q->size); /* A dummy WordEntryPos array to use when haspos is false */ posnull.npos = 1; @@ -743,7 +743,7 @@ get_docrep(TSVector txt, QueryRepresentation *qr, int *doclen) cur = 0; DocRepresentation *doc; - doc = (DocRepresentation *) palloc(sizeof(DocRepresentation) * len); + doc = palloc_array(DocRepresentation, len); /* * Iterate through query to make DocRepresentation for words and it's @@ -815,7 +815,7 @@ get_docrep(TSVector txt, QueryRepresentation *qr, int *doclen) * Join QueryItem per WordEntry and its position */ storage.pos = doc->pos; - storage.data.query.items = palloc(sizeof(QueryItem *) * qr->query->size); + storage.data.query.items = palloc_array(QueryItem *, qr->query->size); storage.data.query.items[0] = doc->data.map.item; storage.data.query.nitem = 1; @@ -832,7 +832,7 @@ get_docrep(TSVector txt, QueryRepresentation *qr, int *doclen) *wptr = storage; wptr++; storage.pos = rptr->pos; - storage.data.query.items = palloc(sizeof(QueryItem *) * qr->query->size); + storage.data.query.items = palloc_array(QueryItem *, qr->query->size); storage.data.query.items[0] = rptr->data.map.item; storage.data.query.nitem = 1; } @@ -878,8 +878,7 @@ calc_rank_cd(const float4 *arrdata, TSVector txt, TSQuery query, int method) } qr.query = query; - qr.operandData = (QueryRepresentationOperand *) - palloc0(sizeof(QueryRepresentationOperand) * query->size); + qr.operandData = palloc0_array(QueryRepresentationOperand, query->size); doc = get_docrep(txt, &qr, &doclen); if (!doc) diff --git a/src/backend/utils/adt/tsvector.c b/src/backend/utils/adt/tsvector.c index 1fa2e3729bfab..024f5160cd4eb 100644 --- a/src/backend/utils/adt/tsvector.c +++ b/src/backend/utils/adt/tsvector.c @@ -3,7 +3,7 @@ * tsvector.c * I/O functions for tsvector * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -202,17 +202,17 @@ tsvectorin(PG_FUNCTION_ARGS) state = init_tsvector_parser(buf, 0, escontext); arrlen = 64; - arr = (WordEntryIN *) palloc(sizeof(WordEntryIN) * arrlen); - cur = tmpbuf = (char *) palloc(buflen); + arr = palloc_array(WordEntryIN, arrlen); + cur = tmpbuf = palloc_array(char, buflen); while (gettoken_tsvector(state, &token, &toklen, &pos, &poslen, NULL)) { if (toklen >= MAXSTRLEN) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg("word is too long (%ld bytes, max %ld bytes)", - (long) toklen, - (long) (MAXSTRLEN - 1)))); + errmsg("word is too long (%d bytes, max %d bytes)", + toklen, + MAXSTRLEN - 1))); if (cur - tmpbuf > MAXSTRPOS) ereturn(escontext, (Datum) 0, @@ -319,9 +319,9 @@ tsvectorout(PG_FUNCTION_ARGS) lenbuf = 0, pp; WordEntry *ptr = ARRPTR(out); - char *curbegin, - *curin, + char *curin, *curout; + const char *curend; lenbuf = out->size * 2 /* '' */ + out->size - 1 /* space */ + 2 /* \0 */ ; for (i = 0; i < out->size; i++) @@ -334,13 +334,14 @@ tsvectorout(PG_FUNCTION_ARGS) curout = outbuf = (char *) palloc(lenbuf); for (i = 0; i < out->size; i++) { - curbegin = curin = STRPTR(out) + ptr->pos; + curin = STRPTR(out) + ptr->pos; + curend = curin + ptr->len; if (i != 0) *curout++ = ' '; *curout++ = '\''; - while (curin - curbegin < ptr->len) + while (curin < curend) { - int len = pg_mblen(curin); + int len = pg_mblen_range(curin, curend); if (t_iseq(curin, '\'')) *curout++ = '\''; diff --git a/src/backend/utils/adt/tsvector_op.c b/src/backend/utils/adt/tsvector_op.c index 1fa1275ca63b2..d8dece42b9bec 100644 --- a/src/backend/utils/adt/tsvector_op.c +++ b/src/backend/utils/adt/tsvector_op.c @@ -3,7 +3,7 @@ * tsvector_op.c * operations over tsvector * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -75,7 +75,7 @@ static bool TS_execute_locations_recurse(QueryItem *curitem, void *arg, TSExecuteCallback chkcond, List **locations); -static int tsvector_bsearch(const TSVector tsv, char *lexeme, int lexeme_len); +static int tsvector_bsearch(const TSVectorData *tsv, char *lexeme, int lexeme_len); static Datum tsvector_update_trigger(PG_FUNCTION_ARGS, bool config_column); @@ -83,7 +83,7 @@ static Datum tsvector_update_trigger(PG_FUNCTION_ARGS, bool config_column); * Order: haspos, len, word, for all positions (pos, weight) */ static int -silly_cmp_tsvector(const TSVector a, const TSVector b) +silly_cmp_tsvector(const TSVectorData *a, const TSVectorData *b) { if (VARSIZE(a) < VARSIZE(b)) return -1; @@ -95,8 +95,8 @@ silly_cmp_tsvector(const TSVector a, const TSVector b) return 1; else { - WordEntry *aptr = ARRPTR(a); - WordEntry *bptr = ARRPTR(b); + const WordEntry *aptr = ARRPTR(a); + const WordEntry *bptr = ARRPTR(b); int i = 0; int res; @@ -329,8 +329,8 @@ tsvector_setweight_by_filter(PG_FUNCTION_ARGS) if (nulls[i]) continue; - lex = VARDATA(dlexemes[i]); - lex_len = VARSIZE(dlexemes[i]) - VARHDRSZ; + lex = VARDATA(DatumGetPointer(dlexemes[i])); + lex_len = VARSIZE(DatumGetPointer(dlexemes[i])) - VARHDRSZ; lex_pos = tsvector_bsearch(tsout, lex, lex_len); if (lex_pos >= 0 && (j = POSDATALEN(tsout, entry + lex_pos)) != 0) @@ -397,9 +397,9 @@ add_pos(TSVector src, WordEntry *srcptr, * found. */ static int -tsvector_bsearch(const TSVector tsv, char *lexeme, int lexeme_len) +tsvector_bsearch(const TSVectorData *tsv, char *lexeme, int lexeme_len) { - WordEntry *arrin = ARRPTR(tsv); + const WordEntry *arrin = ARRPTR(tsv); int StopLow = 0, StopHigh = tsv->size, StopMiddle, @@ -443,10 +443,10 @@ compare_text_lexemes(const void *va, const void *vb) { Datum a = *((const Datum *) va); Datum b = *((const Datum *) vb); - char *alex = VARDATA_ANY(a); - int alex_len = VARSIZE_ANY_EXHDR(a); - char *blex = VARDATA_ANY(b); - int blex_len = VARSIZE_ANY_EXHDR(b); + char *alex = VARDATA_ANY(DatumGetPointer(a)); + int alex_len = VARSIZE_ANY_EXHDR(DatumGetPointer(a)); + char *blex = VARDATA_ANY(DatumGetPointer(b)); + int blex_len = VARSIZE_ANY_EXHDR(DatumGetPointer(b)); return tsCompareString(alex, alex_len, blex, blex_len, false); } @@ -605,8 +605,8 @@ tsvector_delete_arr(PG_FUNCTION_ARGS) if (nulls[i]) continue; - lex = VARDATA(dlexemes[i]); - lex_len = VARSIZE(dlexemes[i]) - VARHDRSZ; + lex = VARDATA(DatumGetPointer(dlexemes[i])); + lex_len = VARSIZE(DatumGetPointer(dlexemes[i])) - VARHDRSZ; lex_pos = tsvector_bsearch(tsin, lex, lex_len); if (lex_pos >= 0) @@ -651,6 +651,7 @@ tsvector_unnest(PG_FUNCTION_ARGS) TEXTARRAYOID, -1, 0); if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) elog(ERROR, "return type must be a row type"); + TupleDescFinalize(tupdesc); funcctx->tuple_desc = tupdesc; funcctx->user_fctx = PG_GETARG_TSVECTOR_COPY(0); @@ -770,7 +771,7 @@ array_to_tsvector(PG_FUNCTION_ARGS) (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("lexeme array may not contain nulls"))); - if (VARSIZE(dlexemes[i]) - VARHDRSZ == 0) + if (VARSIZE(DatumGetPointer(dlexemes[i])) - VARHDRSZ == 0) ereport(ERROR, (errcode(ERRCODE_ZERO_LENGTH_CHARACTER_STRING), errmsg("lexeme array may not contain empty strings"))); @@ -786,7 +787,7 @@ array_to_tsvector(PG_FUNCTION_ARGS) /* Calculate space needed for surviving lexemes. */ for (i = 0; i < nitems; i++) - datalen += VARSIZE(dlexemes[i]) - VARHDRSZ; + datalen += VARSIZE(DatumGetPointer(dlexemes[i])) - VARHDRSZ; tslen = CALCDATASIZE(nitems, datalen); /* Allocate and fill tsvector. */ @@ -798,8 +799,8 @@ array_to_tsvector(PG_FUNCTION_ARGS) cur = STRPTR(tsout); for (i = 0; i < nitems; i++) { - char *lex = VARDATA(dlexemes[i]); - int lex_len = VARSIZE(dlexemes[i]) - VARHDRSZ; + char *lex = VARDATA(DatumGetPointer(dlexemes[i])); + int lex_len = VARSIZE(DatumGetPointer(dlexemes[i])) - VARHDRSZ; memcpy(cur, lex, lex_len); arrout[i].haspos = 0; @@ -1212,7 +1213,7 @@ checkclass_str(CHKVAL *chkval, WordEntry *entry, QueryOperand *val, /* * Filter position information by weights */ - dptr = data->pos = palloc(sizeof(WordEntryPos) * posvec->npos); + dptr = data->pos = palloc_array(WordEntryPos, posvec->npos); data->allocated = true; /* Is there a position with a matching weight? */ @@ -1391,12 +1392,12 @@ checkcondition_str(void *checkval, QueryOperand *val, ExecPhraseData *data) if (totalpos == 0) { totalpos = 256; - allpos = palloc(sizeof(WordEntryPos) * totalpos); + allpos = palloc_array(WordEntryPos, totalpos); } else { totalpos *= 2; - allpos = repalloc(allpos, sizeof(WordEntryPos) * totalpos); + allpos = repalloc_array(allpos, WordEntryPos, totalpos); } } @@ -2456,7 +2457,7 @@ ts_setup_firstcall(FunctionCallInfo fcinfo, FuncCallContext *funcctx, oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); - stat->stack = palloc0(sizeof(StatEntry *) * (stat->maxdepth + 1)); + stat->stack = palloc0_array(StatEntry *, stat->maxdepth + 1); stat->stackpos = 0; node = stat->root; @@ -2604,11 +2605,15 @@ ts_stat_sql(MemoryContext persistentContext, text *txt, text *ws) if (ws) { char *buf; + const char *end; buf = VARDATA_ANY(ws); - while (buf - VARDATA_ANY(ws) < VARSIZE_ANY_EXHDR(ws)) + end = buf + VARSIZE_ANY_EXHDR(ws); + while (buf < end) { - if (pg_mblen(buf) == 1) + int len = pg_mblen_range(buf, end); + + if (len == 1) { switch (*buf) { @@ -2632,7 +2637,7 @@ ts_stat_sql(MemoryContext persistentContext, text *txt, text *ws) stat->weight |= 0; } } - buf += pg_mblen(buf); + buf += len; } } @@ -2839,7 +2844,7 @@ tsvector_update_trigger(PG_FUNCTION_ARGS, bool config_column) prs.lenwords = 32; prs.curwords = 0; prs.pos = 0; - prs.words = (ParsedWord *) palloc(sizeof(ParsedWord) * prs.lenwords); + prs.words = palloc_array(ParsedWord, prs.lenwords); /* find all words in indexable column(s) */ for (i = 2; i < trigger->tgnargs; i++) diff --git a/src/backend/utils/adt/tsvector_parser.c b/src/backend/utils/adt/tsvector_parser.c index e1620d3ed1f2d..efeaeb5533423 100644 --- a/src/backend/utils/adt/tsvector_parser.c +++ b/src/backend/utils/adt/tsvector_parser.c @@ -3,7 +3,7 @@ * tsvector_parser.c * Parser for tsvector * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -58,7 +58,7 @@ init_tsvector_parser(char *input, int flags, Node *escontext) { TSVectorParseState state; - state = (TSVectorParseState) palloc(sizeof(struct TSVectorParseStateData)); + state = palloc_object(struct TSVectorParseStateData); state->prsbuf = input; state->bufstart = input; state->len = 32; @@ -208,8 +208,7 @@ gettoken_tsvector(TSVectorParseState state, PRSSYNTAXERROR; else if (!isspace((unsigned char) *state->prsbuf)) { - COPYCHAR(curpos, state->prsbuf); - curpos += pg_mblen(state->prsbuf); + curpos += ts_copychar_cstr(curpos, state->prsbuf); statecode = WAITENDWORD; } } @@ -223,8 +222,7 @@ gettoken_tsvector(TSVectorParseState state, else { RESIZEPRSBUF; - COPYCHAR(curpos, state->prsbuf); - curpos += pg_mblen(state->prsbuf); + curpos += ts_copychar_cstr(curpos, state->prsbuf); Assert(oldstate != 0); statecode = oldstate; } @@ -259,8 +257,7 @@ gettoken_tsvector(TSVectorParseState state, else { RESIZEPRSBUF; - COPYCHAR(curpos, state->prsbuf); - curpos += pg_mblen(state->prsbuf); + curpos += ts_copychar_cstr(curpos, state->prsbuf); } } else if (statecode == WAITENDCMPLX) @@ -279,8 +276,7 @@ gettoken_tsvector(TSVectorParseState state, else { RESIZEPRSBUF; - COPYCHAR(curpos, state->prsbuf); - curpos += pg_mblen(state->prsbuf); + curpos += ts_copychar_cstr(curpos, state->prsbuf); } } else if (statecode == WAITCHARCMPLX) @@ -288,8 +284,7 @@ gettoken_tsvector(TSVectorParseState state, if (!state->is_web && t_iseq(state->prsbuf, '\'')) { RESIZEPRSBUF; - COPYCHAR(curpos, state->prsbuf); - curpos += pg_mblen(state->prsbuf); + curpos += ts_copychar_cstr(curpos, state->prsbuf); statecode = WAITENDCMPLX; } else @@ -300,7 +295,7 @@ gettoken_tsvector(TSVectorParseState state, PRSSYNTAXERROR; if (state->oprisdelim) { - /* state->prsbuf+=pg_mblen(state->prsbuf); */ + /* state->prsbuf+=pg_mblen_cstr(state->prsbuf); */ RETURN_TOKEN; } else @@ -322,13 +317,13 @@ gettoken_tsvector(TSVectorParseState state, if (posalen == 0) { posalen = 4; - pos = (WordEntryPos *) palloc(sizeof(WordEntryPos) * posalen); + pos = palloc_array(WordEntryPos, posalen); npos = 0; } else if (npos + 1 >= posalen) { posalen *= 2; - pos = (WordEntryPos *) repalloc(pos, sizeof(WordEntryPos) * posalen); + pos = repalloc_array(pos, WordEntryPos, posalen); } npos++; WEP_SETPOS(pos[npos - 1], LIMITPOS(atoi(state->prsbuf))); @@ -383,6 +378,6 @@ gettoken_tsvector(TSVectorParseState state, statecode); /* get next char */ - state->prsbuf += pg_mblen(state->prsbuf); + state->prsbuf += pg_mblen_cstr(state->prsbuf); } } diff --git a/src/backend/utils/adt/uuid.c b/src/backend/utils/adt/uuid.c index bce7309c1833a..6ee3752ac78a7 100644 --- a/src/backend/utils/adt/uuid.c +++ b/src/backend/utils/adt/uuid.c @@ -3,7 +3,7 @@ * uuid.c * Functions for the built-in type "uuid". * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/uuid.c @@ -71,7 +71,7 @@ static int uuid_fast_cmp(Datum x, Datum y, SortSupport ssup); static bool uuid_abbrev_abort(int memtupcount, SortSupport ssup); static Datum uuid_abbrev_convert(Datum original, SortSupport ssup); static inline void uuid_set_version(pg_uuid_t *uuid, unsigned char version); -static inline int64 get_real_time_ns_ascending(); +static inline int64 get_real_time_ns_ascending(void); static pg_uuid_t *generate_uuidv7(uint64 unix_ts_ms, uint32 sub_ms); Datum @@ -80,7 +80,7 @@ uuid_in(PG_FUNCTION_ARGS) char *uuid_str = PG_GETARG_CSTRING(0); pg_uuid_t *uuid; - uuid = (pg_uuid_t *) palloc(sizeof(*uuid)); + uuid = palloc_object(pg_uuid_t); string_to_uuid(uuid_str, uuid, fcinfo->context); PG_RETURN_UUID_P(uuid); } @@ -288,7 +288,7 @@ uuid_sortsupport(PG_FUNCTION_ARGS) oldcontext = MemoryContextSwitchTo(ssup->ssup_cxt); - uss = palloc(sizeof(uuid_sortsupport_state)); + uss = palloc_object(uuid_sortsupport_state); uss->input_count = 0; uss->estimating = true; initHyperLogLog(&uss->abbr_card, 10); @@ -398,11 +398,7 @@ uuid_abbrev_convert(Datum original, SortSupport ssup) { uint32 tmp; -#if SIZEOF_DATUM == 8 - tmp = (uint32) res ^ (uint32) ((uint64) res >> 32); -#else /* SIZEOF_DATUM != 8 */ - tmp = (uint32) res; -#endif + tmp = DatumGetUInt32(res) ^ (uint32) (DatumGetUInt64(res) >> 32); addHyperLogLog(&uss->abbr_card, DatumGetUInt32(hash_uint32(tmp))); } @@ -549,7 +545,7 @@ gen_random_uuid(PG_FUNCTION_ARGS) * than the previous returned timestamp (on this backend). */ static inline int64 -get_real_time_ns_ascending() +get_real_time_ns_ascending(void) { static int64 previous_ns = 0; int64 ns; @@ -752,7 +748,7 @@ uuid_extract_timestamp(PG_FUNCTION_ARGS) + (((uint64) uuid->data[0]) << 40); /* convert ms to us, then adjust */ - ts = (TimestampTz) (tms * NS_PER_US) - + ts = (TimestampTz) (tms * US_PER_MS) - (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY * USECS_PER_SEC; PG_RETURN_TIMESTAMPTZ(ts); diff --git a/src/backend/utils/adt/varbit.c b/src/backend/utils/adt/varbit.c index 205a67dafc56b..7dde1b6db531d 100644 --- a/src/backend/utils/adt/varbit.c +++ b/src/backend/utils/adt/varbit.c @@ -20,7 +20,7 @@ * * Code originally contributed by Adriaan Joubert. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -154,13 +154,13 @@ bit_in(PG_FUNCTION_ARGS) Node *escontext = fcinfo->context; VarBit *result; /* The resulting bit string */ char *sp; /* pointer into the character string */ - bits8 *r; /* pointer into the result */ + uint8 *r; /* pointer into the result */ int len, /* Length of the whole data structure */ bitlen, /* Number of bits in the bit string */ slen; /* Length of the input string */ bool bit_not_hex; /* false = hex string true = bit string */ int bc; - bits8 x = 0; + uint8 x = 0; /* Check that the first character is a b or an x */ if (input_string[0] == 'b' || input_string[0] == 'B') @@ -232,7 +232,7 @@ bit_in(PG_FUNCTION_ARGS) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("\"%.*s\" is not a valid binary digit", - pg_mblen(sp), sp))); + pg_mblen_cstr(sp), sp))); x >>= 1; if (x == 0) @@ -248,16 +248,16 @@ bit_in(PG_FUNCTION_ARGS) for (bc = 0; *sp; sp++) { if (*sp >= '0' && *sp <= '9') - x = (bits8) (*sp - '0'); + x = (uint8) (*sp - '0'); else if (*sp >= 'A' && *sp <= 'F') - x = (bits8) (*sp - 'A') + 10; + x = (uint8) (*sp - 'A') + 10; else if (*sp >= 'a' && *sp <= 'f') - x = (bits8) (*sp - 'a') + 10; + x = (uint8) (*sp - 'a') + 10; else ereturn(escontext, (Datum) 0, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("\"%.*s\" is not a valid hexadecimal digit", - pg_mblen(sp), sp))); + pg_mblen_cstr(sp), sp))); if (bc) { @@ -291,7 +291,7 @@ bit_out(PG_FUNCTION_ARGS) VarBit *s = PG_GETARG_VARBIT_P(0); char *result, *r; - bits8 *sp; + uint8 *sp; int i, len, bitlen; @@ -401,7 +401,7 @@ bit(PG_FUNCTION_ARGS) PG_RETURN_VARBIT_P(arg); if (!isExplicit) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_STRING_DATA_LENGTH_MISMATCH), errmsg("bit string length %d does not match type bit(%d)", VARBITLEN(arg), len))); @@ -459,13 +459,13 @@ varbit_in(PG_FUNCTION_ARGS) Node *escontext = fcinfo->context; VarBit *result; /* The resulting bit string */ char *sp; /* pointer into the character string */ - bits8 *r; /* pointer into the result */ + uint8 *r; /* pointer into the result */ int len, /* Length of the whole data structure */ bitlen, /* Number of bits in the bit string */ slen; /* Length of the input string */ bool bit_not_hex; /* false = hex string true = bit string */ int bc; - bits8 x = 0; + uint8 x = 0; /* Check that the first character is a b or an x */ if (input_string[0] == 'b' || input_string[0] == 'B') @@ -533,7 +533,7 @@ varbit_in(PG_FUNCTION_ARGS) ereturn(escontext, (Datum) 0, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("\"%.*s\" is not a valid binary digit", - pg_mblen(sp), sp))); + pg_mblen_cstr(sp), sp))); x >>= 1; if (x == 0) @@ -549,16 +549,16 @@ varbit_in(PG_FUNCTION_ARGS) for (bc = 0; *sp; sp++) { if (*sp >= '0' && *sp <= '9') - x = (bits8) (*sp - '0'); + x = (uint8) (*sp - '0'); else if (*sp >= 'A' && *sp <= 'F') - x = (bits8) (*sp - 'A') + 10; + x = (uint8) (*sp - 'A') + 10; else if (*sp >= 'a' && *sp <= 'f') - x = (bits8) (*sp - 'a') + 10; + x = (uint8) (*sp - 'a') + 10; else ereturn(escontext, (Datum) 0, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("\"%.*s\" is not a valid hexadecimal digit", - pg_mblen(sp), sp))); + pg_mblen_cstr(sp), sp))); if (bc) { @@ -589,8 +589,8 @@ varbit_out(PG_FUNCTION_ARGS) VarBit *s = PG_GETARG_VARBIT_P(0); char *result, *r; - bits8 *sp; - bits8 x; + uint8 *sp; + uint8 x; int i, k, len; @@ -752,7 +752,7 @@ varbit(PG_FUNCTION_ARGS) PG_RETURN_VARBIT_P(arg); if (!isExplicit) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_STRING_DATA_RIGHT_TRUNCATION), errmsg("bit string too long for type bit varying(%d)", len))); @@ -982,7 +982,7 @@ bit_catenate(VarBit *arg1, VarBit *arg2) bytelen, bit1pad, bit2shift; - bits8 *pr, + uint8 *pr, *pa; bitlen1 = VARBITLEN(arg1); @@ -1063,7 +1063,7 @@ bitsubstring(VarBit *arg, int32 s, int32 l, bool length_not_specified) int32 e, s1, e1; - bits8 *r, + uint8 *r, *ps; bitlen = VARBITLEN(arg); @@ -1249,7 +1249,7 @@ bit_and(PG_FUNCTION_ARGS) bitlen1, bitlen2, i; - bits8 *p1, + uint8 *p1, *p2, *r; @@ -1290,7 +1290,7 @@ bit_or(PG_FUNCTION_ARGS) bitlen1, bitlen2, i; - bits8 *p1, + uint8 *p1, *p2, *r; @@ -1330,7 +1330,7 @@ bitxor(PG_FUNCTION_ARGS) bitlen1, bitlen2, i; - bits8 *p1, + uint8 *p1, *p2, *r; @@ -1366,7 +1366,7 @@ bitnot(PG_FUNCTION_ARGS) { VarBit *arg = PG_GETARG_VARBIT_P(0); VarBit *result; - bits8 *p, + uint8 *p, *r; result = (VarBit *) palloc(VARSIZE(arg)); @@ -1397,7 +1397,7 @@ bitshiftleft(PG_FUNCTION_ARGS) int byte_shift, ishift, len; - bits8 *p, + uint8 *p, *r; /* Negative shift is a shift to the right */ @@ -1464,7 +1464,7 @@ bitshiftright(PG_FUNCTION_ARGS) int byte_shift, ishift, len; - bits8 *p, + uint8 *p, *r; /* Negative shift is a shift to the left */ @@ -1533,7 +1533,7 @@ bitfromint4(PG_FUNCTION_ARGS) int32 a = PG_GETARG_INT32(0); int32 typmod = PG_GETARG_INT32(1); VarBit *result; - bits8 *r; + uint8 *r; int rlen; int destbitsleft, srcbitsleft; @@ -1554,7 +1554,7 @@ bitfromint4(PG_FUNCTION_ARGS) /* sign-fill any excess bytes in output */ while (destbitsleft >= srcbitsleft + 8) { - *r++ = (bits8) ((a < 0) ? BITMASK : 0); + *r++ = (uint8) ((a < 0) ? BITMASK : 0); destbitsleft -= 8; } /* store first fractional byte */ @@ -1565,19 +1565,19 @@ bitfromint4(PG_FUNCTION_ARGS) /* Force sign-fill in case the compiler implements >> as zero-fill */ if (a < 0) val |= ((unsigned int) -1) << (srcbitsleft + 8 - destbitsleft); - *r++ = (bits8) (val & BITMASK); + *r++ = (uint8) (val & BITMASK); destbitsleft -= 8; } /* Now srcbitsleft and destbitsleft are the same, need not track both */ /* store whole bytes */ while (destbitsleft >= 8) { - *r++ = (bits8) ((a >> (destbitsleft - 8)) & BITMASK); + *r++ = (uint8) ((a >> (destbitsleft - 8)) & BITMASK); destbitsleft -= 8; } /* store last fractional byte */ if (destbitsleft > 0) - *r = (bits8) ((a << (8 - destbitsleft)) & BITMASK); + *r = (uint8) ((a << (8 - destbitsleft)) & BITMASK); PG_RETURN_VARBIT_P(result); } @@ -1587,11 +1587,11 @@ bittoint4(PG_FUNCTION_ARGS) { VarBit *arg = PG_GETARG_VARBIT_P(0); uint32 result; - bits8 *r; + uint8 *r; /* Check that the bit string is not too long */ if (VARBITLEN(arg) > sizeof(result) * BITS_PER_BYTE) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("integer out of range"))); @@ -1613,7 +1613,7 @@ bitfromint8(PG_FUNCTION_ARGS) int64 a = PG_GETARG_INT64(0); int32 typmod = PG_GETARG_INT32(1); VarBit *result; - bits8 *r; + uint8 *r; int rlen; int destbitsleft, srcbitsleft; @@ -1634,7 +1634,7 @@ bitfromint8(PG_FUNCTION_ARGS) /* sign-fill any excess bytes in output */ while (destbitsleft >= srcbitsleft + 8) { - *r++ = (bits8) ((a < 0) ? BITMASK : 0); + *r++ = (uint8) ((a < 0) ? BITMASK : 0); destbitsleft -= 8; } /* store first fractional byte */ @@ -1645,19 +1645,19 @@ bitfromint8(PG_FUNCTION_ARGS) /* Force sign-fill in case the compiler implements >> as zero-fill */ if (a < 0) val |= ((unsigned int) -1) << (srcbitsleft + 8 - destbitsleft); - *r++ = (bits8) (val & BITMASK); + *r++ = (uint8) (val & BITMASK); destbitsleft -= 8; } /* Now srcbitsleft and destbitsleft are the same, need not track both */ /* store whole bytes */ while (destbitsleft >= 8) { - *r++ = (bits8) ((a >> (destbitsleft - 8)) & BITMASK); + *r++ = (uint8) ((a >> (destbitsleft - 8)) & BITMASK); destbitsleft -= 8; } /* store last fractional byte */ if (destbitsleft > 0) - *r = (bits8) ((a << (8 - destbitsleft)) & BITMASK); + *r = (uint8) ((a << (8 - destbitsleft)) & BITMASK); PG_RETURN_VARBIT_P(result); } @@ -1667,11 +1667,11 @@ bittoint8(PG_FUNCTION_ARGS) { VarBit *arg = PG_GETARG_VARBIT_P(0); uint64 result; - bits8 *r; + uint8 *r; /* Check that the bit string is not too long */ if (VARBITLEN(arg) > sizeof(result) * BITS_PER_BYTE) - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), errmsg("bigint out of range"))); @@ -1703,9 +1703,9 @@ bitposition(PG_FUNCTION_ARGS) str_length, i, is; - bits8 *s, /* pointer into substring */ + uint8 *s, /* pointer into substring */ *p; /* pointer into str */ - bits8 cmp, /* shifted substring byte to compare */ + uint8 cmp, /* shifted substring byte to compare */ mask1, /* mask for substring byte shifted right */ mask2, /* mask for substring byte shifted left */ end_mask, /* pad mask for last substring byte */ @@ -1812,7 +1812,7 @@ bitsetbit(PG_FUNCTION_ARGS) VarBit *result; int len, bitlen; - bits8 *r, + uint8 *r, *p; int byteNo, bitNo; @@ -1871,7 +1871,7 @@ bitgetbit(PG_FUNCTION_ARGS) VarBit *arg1 = PG_GETARG_VARBIT_P(0); int32 n = PG_GETARG_INT32(1); int bitlen; - bits8 *p; + uint8 *p; int byteNo, bitNo; diff --git a/src/backend/utils/adt/varchar.c b/src/backend/utils/adt/varchar.c index 3f40c9da1a0d5..a62e55eec196b 100644 --- a/src/backend/utils/adt/varchar.c +++ b/src/backend/utils/adt/varchar.c @@ -3,7 +3,7 @@ * varchar.c * Functions for the built-in types char(n) and varchar(n). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -158,8 +158,8 @@ bpchar_input(const char *s, size_t len, int32 atttypmod, Node *escontext) if (s[j] != ' ') ereturn(escontext, NULL, (errcode(ERRCODE_STRING_DATA_RIGHT_TRUNCATION), - errmsg("value too long for type character(%d)", - (int) maxlen))); + errmsg("value too long for type character(%zu)", + maxlen))); } /* @@ -307,7 +307,7 @@ bpchar(PG_FUNCTION_ARGS) { for (i = maxmblen; i < len; i++) if (s[i] != ' ') - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_STRING_DATA_RIGHT_TRUNCATION), errmsg("value too long for type character(%d)", maxlen))); @@ -472,8 +472,8 @@ varchar_input(const char *s, size_t len, int32 atttypmod, Node *escontext) if (s[j] != ' ') ereturn(escontext, NULL, (errcode(ERRCODE_STRING_DATA_RIGHT_TRUNCATION), - errmsg("value too long for type character varying(%d)", - (int) maxlen))); + errmsg("value too long for type character varying(%zu)", + maxlen))); } len = mbmaxlen; @@ -634,7 +634,7 @@ varchar(PG_FUNCTION_ARGS) { for (i = maxmblen; i < len; i++) if (s_data[i] != ' ') - ereport(ERROR, + ereturn(fcinfo->context, (Datum) 0, (errcode(ERRCODE_STRING_DATA_RIGHT_TRUNCATION), errmsg("value too long for type character varying(%d)", maxlen))); diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c index 3e4d5568bde89..c0ff51bd2fc16 100644 --- a/src/backend/utils/adt/varlena.c +++ b/src/backend/utils/adt/varlena.c @@ -3,7 +3,7 @@ * varlena.c * Functions for the variable-length built-in types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,6 +19,7 @@ #include "access/detoast.h" #include "access/toast_compression.h" +#include "access/tupmacs.h" #include "catalog/pg_collation.h" #include "catalog/pg_type.h" #include "common/hashfn.h" @@ -35,19 +36,15 @@ #include "port/pg_bswap.h" #include "regex/regex.h" #include "utils/builtins.h" -#include "utils/bytea.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" #include "utils/sortsupport.h" +#include "utils/tuplestore.h" #include "utils/varlena.h" - -/* GUC variable */ -int bytea_output = BYTEA_OUTPUT_HEX; - -typedef struct varlena VarString; +typedef varlena VarString; /* * State for text_position_* functions. @@ -97,7 +94,7 @@ typedef struct int last_returned; /* Last comparison result (cache) */ bool cache_blob; /* Does buf2 contain strxfrm() blob, etc? */ bool collate_c; - Oid typid; /* Actual datatype (text/bpchar/bytea/name) */ + Oid typid; /* Actual datatype (text/bpchar/name) */ hyperLogLogState abbr_card; /* Abbreviated key cardinality state */ hyperLogLogState full_card; /* Full key cardinality state */ double prop_card; /* Required cardinality proportion */ @@ -138,6 +135,7 @@ static text *text_substring(Datum str, int32 start, int32 length, bool length_not_specified); +static int pg_mbcharcliplen_chars(const char *mbstr, int len, int limit); static text *text_overlay(text *t1, text *t2, int sp, int sl); static int text_position(text *t1, text *t2, Oid collid); static void text_position_setup(text *t1, text *t2, Oid collid, TextPositionState *state); @@ -148,12 +146,6 @@ static int text_position_get_match_pos(TextPositionState *state); static void text_position_cleanup(TextPositionState *state); static void check_collation_set(Oid collid); static int text_cmp(text *arg1, text *arg2, Oid collid); -static bytea *bytea_catenate(bytea *t1, bytea *t2); -static bytea *bytea_substring(Datum str, - int S, - int L, - bool length_not_specified); -static bytea *bytea_overlay(bytea *t1, bytea *t2, int sp, int sl); static void appendStringInfoText(StringInfo str, const text *t); static bool split_text(FunctionCallInfo fcinfo, SplitTextOutputData *tstate); static void split_text_accum_result(SplitTextOutputData *tstate, @@ -279,307 +271,6 @@ text_to_cstring_buffer(const text *src, char *dst, size_t dst_len) * USER I/O ROUTINES * *****************************************************************************/ - -#define VAL(CH) ((CH) - '0') -#define DIG(VAL) ((VAL) + '0') - -/* - * byteain - converts from printable representation of byte array - * - * Non-printable characters must be passed as '\nnn' (octal) and are - * converted to internal form. '\' must be passed as '\\'. - * ereport(ERROR, ...) if bad form. - * - * BUGS: - * The input is scanned twice. - * The error checking of input is minimal. - */ -Datum -byteain(PG_FUNCTION_ARGS) -{ - char *inputText = PG_GETARG_CSTRING(0); - Node *escontext = fcinfo->context; - char *tp; - char *rp; - int bc; - bytea *result; - - /* Recognize hex input */ - if (inputText[0] == '\\' && inputText[1] == 'x') - { - size_t len = strlen(inputText); - - bc = (len - 2) / 2 + VARHDRSZ; /* maximum possible length */ - result = palloc(bc); - bc = hex_decode_safe(inputText + 2, len - 2, VARDATA(result), - escontext); - SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */ - - PG_RETURN_BYTEA_P(result); - } - - /* Else, it's the traditional escaped style */ - for (bc = 0, tp = inputText; *tp != '\0'; bc++) - { - if (tp[0] != '\\') - tp++; - else if ((tp[0] == '\\') && - (tp[1] >= '0' && tp[1] <= '3') && - (tp[2] >= '0' && tp[2] <= '7') && - (tp[3] >= '0' && tp[3] <= '7')) - tp += 4; - else if ((tp[0] == '\\') && - (tp[1] == '\\')) - tp += 2; - else - { - /* - * one backslash, not followed by another or ### valid octal - */ - ereturn(escontext, (Datum) 0, - (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), - errmsg("invalid input syntax for type %s", "bytea"))); - } - } - - bc += VARHDRSZ; - - result = (bytea *) palloc(bc); - SET_VARSIZE(result, bc); - - tp = inputText; - rp = VARDATA(result); - while (*tp != '\0') - { - if (tp[0] != '\\') - *rp++ = *tp++; - else if ((tp[0] == '\\') && - (tp[1] >= '0' && tp[1] <= '3') && - (tp[2] >= '0' && tp[2] <= '7') && - (tp[3] >= '0' && tp[3] <= '7')) - { - bc = VAL(tp[1]); - bc <<= 3; - bc += VAL(tp[2]); - bc <<= 3; - *rp++ = bc + VAL(tp[3]); - - tp += 4; - } - else if ((tp[0] == '\\') && - (tp[1] == '\\')) - { - *rp++ = '\\'; - tp += 2; - } - else - { - /* - * We should never get here. The first pass should not allow it. - */ - ereturn(escontext, (Datum) 0, - (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), - errmsg("invalid input syntax for type %s", "bytea"))); - } - } - - PG_RETURN_BYTEA_P(result); -} - -/* - * byteaout - converts to printable representation of byte array - * - * In the traditional escaped format, non-printable characters are - * printed as '\nnn' (octal) and '\' as '\\'. - */ -Datum -byteaout(PG_FUNCTION_ARGS) -{ - bytea *vlena = PG_GETARG_BYTEA_PP(0); - char *result; - char *rp; - - if (bytea_output == BYTEA_OUTPUT_HEX) - { - /* Print hex format */ - rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 2 + 1); - *rp++ = '\\'; - *rp++ = 'x'; - rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp); - } - else if (bytea_output == BYTEA_OUTPUT_ESCAPE) - { - /* Print traditional escaped format */ - char *vp; - uint64 len; - int i; - - len = 1; /* empty string has 1 char */ - vp = VARDATA_ANY(vlena); - for (i = VARSIZE_ANY_EXHDR(vlena); i != 0; i--, vp++) - { - if (*vp == '\\') - len += 2; - else if ((unsigned char) *vp < 0x20 || (unsigned char) *vp > 0x7e) - len += 4; - else - len++; - } - - /* - * In principle len can't overflow uint32 if the input fit in 1GB, but - * for safety let's check rather than relying on palloc's internal - * check. - */ - if (len > MaxAllocSize) - ereport(ERROR, - (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), - errmsg_internal("result of bytea output conversion is too large"))); - rp = result = (char *) palloc(len); - - vp = VARDATA_ANY(vlena); - for (i = VARSIZE_ANY_EXHDR(vlena); i != 0; i--, vp++) - { - if (*vp == '\\') - { - *rp++ = '\\'; - *rp++ = '\\'; - } - else if ((unsigned char) *vp < 0x20 || (unsigned char) *vp > 0x7e) - { - int val; /* holds unprintable chars */ - - val = *vp; - rp[0] = '\\'; - rp[3] = DIG(val & 07); - val >>= 3; - rp[2] = DIG(val & 07); - val >>= 3; - rp[1] = DIG(val & 03); - rp += 4; - } - else - *rp++ = *vp; - } - } - else - { - elog(ERROR, "unrecognized \"bytea_output\" setting: %d", - bytea_output); - rp = result = NULL; /* keep compiler quiet */ - } - *rp = '\0'; - PG_RETURN_CSTRING(result); -} - -/* - * bytearecv - converts external binary format to bytea - */ -Datum -bytearecv(PG_FUNCTION_ARGS) -{ - StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); - bytea *result; - int nbytes; - - nbytes = buf->len - buf->cursor; - result = (bytea *) palloc(nbytes + VARHDRSZ); - SET_VARSIZE(result, nbytes + VARHDRSZ); - pq_copymsgbytes(buf, VARDATA(result), nbytes); - PG_RETURN_BYTEA_P(result); -} - -/* - * byteasend - converts bytea to binary format - * - * This is a special case: just copy the input... - */ -Datum -byteasend(PG_FUNCTION_ARGS) -{ - bytea *vlena = PG_GETARG_BYTEA_P_COPY(0); - - PG_RETURN_BYTEA_P(vlena); -} - -Datum -bytea_string_agg_transfn(PG_FUNCTION_ARGS) -{ - StringInfo state; - - state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0); - - /* Append the value unless null, preceding it with the delimiter. */ - if (!PG_ARGISNULL(1)) - { - bytea *value = PG_GETARG_BYTEA_PP(1); - bool isfirst = false; - - /* - * You might think we can just throw away the first delimiter, however - * we must keep it as we may be a parallel worker doing partial - * aggregation building a state to send to the main process. We need - * to keep the delimiter of every aggregation so that the combine - * function can properly join up the strings of two separately - * partially aggregated results. The first delimiter is only stripped - * off in the final function. To know how much to strip off the front - * of the string, we store the length of the first delimiter in the - * StringInfo's cursor field, which we don't otherwise need here. - */ - if (state == NULL) - { - state = makeStringAggState(fcinfo); - isfirst = true; - } - - if (!PG_ARGISNULL(2)) - { - bytea *delim = PG_GETARG_BYTEA_PP(2); - - appendBinaryStringInfo(state, VARDATA_ANY(delim), - VARSIZE_ANY_EXHDR(delim)); - if (isfirst) - state->cursor = VARSIZE_ANY_EXHDR(delim); - } - - appendBinaryStringInfo(state, VARDATA_ANY(value), - VARSIZE_ANY_EXHDR(value)); - } - - /* - * The transition type for string_agg() is declared to be "internal", - * which is a pass-by-value type the same size as a pointer. - */ - if (state) - PG_RETURN_POINTER(state); - PG_RETURN_NULL(); -} - -Datum -bytea_string_agg_finalfn(PG_FUNCTION_ARGS) -{ - StringInfo state; - - /* cannot be called directly because of internal-type argument */ - Assert(AggCheckCallContext(fcinfo, NULL)); - - state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0); - - if (state != NULL) - { - /* As per comment in transfn, strip data before the cursor position */ - bytea *result; - int strippedlen = state->len - state->cursor; - - result = (bytea *) palloc(strippedlen + VARHDRSZ); - SET_VARSIZE(result, strippedlen + VARHDRSZ); - memcpy(VARDATA(result), &state->data[state->cursor], strippedlen); - PG_RETURN_BYTEA_P(result); - } - else - PG_RETURN_NULL(); -} - /* * textin - converts cstring to internal representation */ @@ -720,13 +411,12 @@ text_length(Datum str) { /* fastpath when max encoding length is one */ if (pg_database_encoding_max_length() == 1) - PG_RETURN_INT32(toast_raw_datum_size(str) - VARHDRSZ); + return (toast_raw_datum_size(str) - VARHDRSZ); else { text *t = DatumGetTextPP(str); - PG_RETURN_INT32(pg_mbstrlen_with_len(VARDATA_ANY(t), - VARSIZE_ANY_EXHDR(t))); + return (pg_mbstrlen_with_len(VARDATA_ANY(t), VARSIZE_ANY_EXHDR(t))); } } @@ -807,8 +497,11 @@ text_catenate(text *t1, text *t2) * charlen_to_bytelen() * Compute the number of bytes occupied by n characters starting at *p * - * It is caller's responsibility that there actually are n characters; - * the string need not be null-terminated. + * The caller shall ensure there are n complete characters. Callers achieve + * this by deriving "n" from regmatch_t findings from searching a wchar array. + * pg_mb2wchar_with_len() skips any trailing incomplete character, so regex + * matches will end no later than the last complete character. (The string + * need not be null-terminated.) */ static int charlen_to_bytelen(const char *p, int n) @@ -823,7 +516,7 @@ charlen_to_bytelen(const char *p, int n) const char *s; for (s = p; n > 0; n--) - s += pg_mblen(s); + s += pg_mblen_unbounded(s); /* caller verified encoding */ return s - p; } @@ -896,7 +589,7 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) int32 S = start; /* start position */ int32 S1; /* adjusted start position */ int32 L1; /* adjusted substring length */ - int32 E; /* end position */ + int32 E; /* end position, exclusive */ /* * SQL99 says S can be zero or negative (which we don't document), but we @@ -957,6 +650,7 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) int32 slice_start; int32 slice_size; int32 slice_strlen; + int32 slice_len; text *slice; int32 E1; int32 i; @@ -973,14 +667,14 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) if (length_not_specified) /* special case - get length to end of * string */ - slice_size = L1 = -1; + E = slice_size = L1 = -1; else if (length < 0) { /* SQL99 says to throw an error for E < S, i.e., negative length */ ereport(ERROR, (errcode(ERRCODE_SUBSTRING_ERROR), errmsg("negative substring length not allowed"))); - slice_size = L1 = -1; /* silence stupider compilers */ + E = slice_size = L1 = -1; /* silence stupider compilers */ } else if (pg_add_s32_overflow(S, length, &E)) { @@ -993,11 +687,11 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) else { /* - * A zero or negative value for the end position can happen if the - * start was negative or one. SQL99 says to return a zero-length - * string. + * Ending at position 1, exclusive, obviously yields an empty + * string. A zero or negative value can happen if the start was + * negative or one. SQL99 says to return a zero-length string. */ - if (E < 1) + if (E <= 1) return cstring_to_text(""); /* @@ -1007,11 +701,11 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) L1 = E - S1; /* - * Total slice size in bytes can't be any longer than the start - * position plus substring length times the encoding max length. - * If that overflows, we can just use -1. + * Total slice size in bytes can't be any longer than the + * inclusive end position times the encoding max length. If that + * overflows, we can just use -1. */ - if (pg_mul_s32_overflow(E, eml, &slice_size)) + if (pg_mul_s32_overflow(E - 1, eml, &slice_size)) slice_size = -1; } @@ -1026,16 +720,25 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) slice = (text *) DatumGetPointer(str); /* see if we got back an empty string */ - if (VARSIZE_ANY_EXHDR(slice) == 0) + slice_len = VARSIZE_ANY_EXHDR(slice); + if (slice_len == 0) { if (slice != (text *) DatumGetPointer(str)) pfree(slice); return cstring_to_text(""); } - /* Now we can get the actual length of the slice in MB characters */ - slice_strlen = pg_mbstrlen_with_len(VARDATA_ANY(slice), - VARSIZE_ANY_EXHDR(slice)); + /* + * Now we can get the actual length of the slice in MB characters, + * stopping at the end of the substring. Continuing beyond the + * substring end could find an incomplete character attributable + * solely to DatumGetTextPSlice() chopping in the middle of a + * character, and it would be superfluous work at best. + */ + slice_strlen = + (slice_size == -1 ? + pg_mbstrlen_with_len(VARDATA_ANY(slice), slice_len) : + pg_mbcharcliplen_chars(VARDATA_ANY(slice), slice_len, E - 1)); /* * Check that the start position wasn't > slice_strlen. If so, SQL99 @@ -1062,7 +765,7 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) */ p = VARDATA_ANY(slice); for (i = 0; i < S1 - 1; i++) - p += pg_mblen(p); + p += pg_mblen_unbounded(p); /* hang onto a pointer to our start position */ s = p; @@ -1072,7 +775,7 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) * length. */ for (i = S1; i < E1; i++) - p += pg_mblen(p); + p += pg_mblen_unbounded(p); ret = (text *) palloc(VARHDRSZ + (p - s)); SET_VARSIZE(ret, VARHDRSZ + (p - s)); @@ -1090,6 +793,35 @@ text_substring(Datum str, int32 start, int32 length, bool length_not_specified) return NULL; } +/* + * pg_mbcharcliplen_chars - + * Mirror pg_mbcharcliplen(), except return value unit is chars, not bytes. + * + * This mirrors all the dubious historical behavior, so it's static to + * discourage proliferation. The assertions are specific to the one caller. + */ +static int +pg_mbcharcliplen_chars(const char *mbstr, int len, int limit) +{ + int nch = 0; + int l; + + Assert(len > 0); + Assert(limit > 0); + Assert(pg_database_encoding_max_length() > 1); + + while (len > 0 && *mbstr) + { + l = pg_mblen_with_len(mbstr, len); + nch++; + if (nch == limit) + break; + len -= l; + mbstr += l; + } + return nch; +} + /* * textoverlay * Replace specified substring of first string with second @@ -1377,6 +1109,8 @@ text_position_next(TextPositionState *state) */ if (state->is_multibyte_char_in_char && state->locale->deterministic) { + const char *haystack_end = state->str1 + state->len1; + /* Walk one character at a time, until we reach the match. */ /* the search should never move backwards. */ @@ -1385,7 +1119,7 @@ text_position_next(TextPositionState *state) while (state->refpoint < matchptr) { /* step to next character. */ - state->refpoint += pg_mblen(state->refpoint); + state->refpoint += pg_mblen_range(state->refpoint, haystack_end); state->refpos++; /* @@ -1424,6 +1158,7 @@ text_position_next_internal(char *start_ptr, TextPositionState *state) const char *hptr; Assert(start_ptr >= haystack && start_ptr <= haystack_end); + Assert(needle_len > 0); state->last_match_len_tmp = needle_len; @@ -1436,19 +1171,26 @@ text_position_next_internal(char *start_ptr, TextPositionState *state) * needle under the given collation. * * Note, the found substring could have a different length than the - * needle, including being empty. Callers that want to skip over the - * found string need to read the length of the found substring from - * last_match_len rather than just using the length of their needle. + * needle. Callers that want to skip over the found string need to + * read the length of the found substring from last_match_len rather + * than just using the length of their needle. * * Most callers will require "greedy" semantics, meaning that we need * to find the longest such substring, not the shortest. For callers * that don't need greedy semantics, we can finish on the first match. + * + * This loop depends on the assumption that the needle is nonempty and + * any matching substring must also be nonempty. (Even if the + * collation would accept an empty match, returning one would send + * callers that search for successive matches into an infinite loop.) */ const char *result_hptr = NULL; hptr = start_ptr; while (hptr < haystack_end) { + const char *test_end; + /* * First check the common case that there is a match in the * haystack of exactly the length of the needle. @@ -1459,11 +1201,13 @@ text_position_next_internal(char *start_ptr, TextPositionState *state) return (char *) hptr; /* - * Else check if any of the possible substrings starting at hptr - * are equal to the needle. + * Else check if any of the non-empty substrings starting at hptr + * compare equal to the needle. */ - for (const char *test_end = hptr; test_end < haystack_end; test_end += pg_mblen(test_end)) + test_end = hptr; + do { + test_end += pg_mblen_range(test_end, haystack_end); if (pg_strncoll(hptr, (test_end - hptr), needle, needle_len, state->locale) == 0) { state->last_match_len_tmp = (test_end - hptr); @@ -1471,11 +1215,12 @@ text_position_next_internal(char *start_ptr, TextPositionState *state) if (!state->greedy) break; } - } + } while (test_end < haystack_end); + if (result_hptr) break; - hptr += pg_mblen(hptr); + hptr += pg_mblen_range(hptr, haystack_end); } return (char *) result_hptr; @@ -1919,10 +1664,8 @@ bttextsortsupport(PG_FUNCTION_ARGS) * Includes locale support, and support for BpChar semantics (i.e. removing * trailing spaces before comparison). * - * Relies on the assumption that text, VarChar, BpChar, and bytea all have the - * same representation. Callers that always use the C collation (e.g. - * non-collatable type callers like bytea) may have NUL bytes in their strings; - * this will not work with any other collation, though. + * Relies on the assumption that text, VarChar, and BpChar all have the + * same representation. */ void varstr_sortsupport(SortSupport ssup, Oid typid, Oid collid) @@ -1984,14 +1727,13 @@ varstr_sortsupport(SortSupport ssup, Oid typid, Oid collid) * * Even apart from the risk of broken locales, it's possible that * there are platforms where the use of abbreviated keys should be - * disabled at compile time. Having only 4 byte datums could make - * worst-case performance drastically more likely, for example. - * Moreover, macOS's strxfrm() implementation is known to not - * effectively concentrate a significant amount of entropy from the - * original string in earlier transformed blobs. It's possible that - * other supported platforms are similarly encumbered. So, if we ever - * get past disabling this categorically, we may still want or need to - * disable it for particular platforms. + * disabled at compile time. For example, macOS's strxfrm() + * implementation is known to not effectively concentrate a + * significant amount of entropy from the original string in earlier + * transformed blobs. It's possible that other supported platforms + * are similarly encumbered. So, if we ever get past disabling this + * categorically, we may still want or need to disable it for + * particular platforms. */ if (!pg_strxfrm_enabled(locale)) abbreviate = false; @@ -2006,7 +1748,7 @@ varstr_sortsupport(SortSupport ssup, Oid typid, Oid collid) */ if (abbreviate || !collate_c) { - sss = palloc(sizeof(VarStringSortSupport)); + sss = palloc_object(VarStringSortSupport); sss->buf1 = palloc(TEXTBUFLEN); sss->buflen1 = TEXTBUFLEN; sss->buf2 = palloc(TEXTBUFLEN); @@ -2286,7 +2028,7 @@ varstrfastcmp_locale(char *a1p, int len1, char *a2p, int len2, SortSupport ssup) * representation. Our encoding strategy is simple -- pack the first 8 bytes * of a strxfrm() blob into a Datum (on little-endian machines, the 8 bytes are * stored in reverse order), and treat it as an unsigned integer. When the "C" - * locale is used, or in case of bytea, just memcpy() from original instead. + * locale is used just memcpy() from original instead. */ static Datum varstr_abbrev_convert(Datum original, SortSupport ssup) @@ -2313,30 +2055,8 @@ varstr_abbrev_convert(Datum original, SortSupport ssup) /* * If we're using the C collation, use memcpy(), rather than strxfrm(), to - * abbreviate keys. The full comparator for the C locale is always - * memcmp(). It would be incorrect to allow bytea callers (callers that - * always force the C collation -- bytea isn't a collatable type, but this - * approach is convenient) to use strxfrm(). This is because bytea - * strings may contain NUL bytes. Besides, this should be faster, too. - * - * More generally, it's okay that bytea callers can have NUL bytes in - * strings because abbreviated cmp need not make a distinction between - * terminating NUL bytes, and NUL bytes representing actual NULs in the - * authoritative representation. Hopefully a comparison at or past one - * abbreviated key's terminating NUL byte will resolve the comparison - * without consulting the authoritative representation; specifically, some - * later non-NUL byte in the longer string can resolve the comparison - * against a subsequent terminating NUL in the shorter string. There will - * usually be what is effectively a "length-wise" resolution there and - * then. - * - * If that doesn't work out -- if all bytes in the longer string - * positioned at or past the offset of the smaller string's (first) - * terminating NUL are actually representative of NUL bytes in the - * authoritative binary string (perhaps with some *terminating* NUL bytes - * towards the end of the longer string iff it happens to still be small) - * -- then an authoritative tie-breaker will happen, and do the right - * thing: explicitly consider string length. + * abbreviate keys. The full comparator for the C locale is also + * memcmp(). This should be faster than strxfrm(). */ if (sss->collate_c) memcpy(pres, authoritative_data, Min(len, max_prefix_bytes)); @@ -2418,9 +2138,6 @@ varstr_abbrev_convert(Datum original, SortSupport ssup) * strxfrm() blob is itself NUL terminated, leaving no danger of * misinterpreting any NUL bytes not intended to be interpreted as * logically representing termination. - * - * (Actually, even if there were NUL bytes in the blob it would be - * okay. See remarks on bytea case above.) */ memcpy(pres, sss->buf2, Min(max_prefix_bytes, bsize)); } @@ -2445,18 +2162,12 @@ varstr_abbrev_convert(Datum original, SortSupport ssup) addHyperLogLog(&sss->full_card, hash); /* Hash abbreviated key */ -#if SIZEOF_DATUM == 8 { - uint32 lohalf, - hihalf; + uint32 tmp; - lohalf = (uint32) res; - hihalf = (uint32) (res >> 32); - hash = DatumGetUInt32(hash_uint32(lohalf ^ hihalf)); + tmp = DatumGetUInt32(res) ^ (uint32) (DatumGetUInt64(res) >> 32); + hash = DatumGetUInt32(hash_uint32(tmp)); } -#else /* SIZEOF_DATUM != 8 */ - hash = DatumGetUInt32(hash_uint32((uint32) res)); -#endif addHyperLogLog(&sss->abbr_card, hash); @@ -2507,10 +2218,10 @@ varstr_abbrev_abort(int memtupcount, SortSupport ssup) * NULLs are generally disregarded, if only NULL values were seen so far, * that might misrepresent costs if we failed to clamp. */ - if (abbrev_distinct <= 1.0) + if (abbrev_distinct < 1.0) abbrev_distinct = 1.0; - if (key_distinct <= 1.0) + if (key_distinct < 1.0) key_distinct = 1.0; /* @@ -2603,7 +2314,9 @@ varstr_abbrev_abort(int memtupcount, SortSupport ssup) Datum btvarstrequalimage(PG_FUNCTION_ARGS) { - /* Oid opcintype = PG_GETARG_OID(0); */ +#ifdef NOT_USED + Oid opcintype = PG_GETARG_OID(0); +#endif Oid collid = PG_GET_COLLATION(); pg_locale_t locale; @@ -2959,467 +2672,6 @@ bttext_pattern_sortsupport(PG_FUNCTION_ARGS) } -/*------------------------------------------------------------- - * byteaoctetlen - * - * get the number of bytes contained in an instance of type 'bytea' - *------------------------------------------------------------- - */ -Datum -byteaoctetlen(PG_FUNCTION_ARGS) -{ - Datum str = PG_GETARG_DATUM(0); - - /* We need not detoast the input at all */ - PG_RETURN_INT32(toast_raw_datum_size(str) - VARHDRSZ); -} - -/* - * byteacat - - * takes two bytea* and returns a bytea* that is the concatenation of - * the two. - * - * Cloned from textcat and modified as required. - */ -Datum -byteacat(PG_FUNCTION_ARGS) -{ - bytea *t1 = PG_GETARG_BYTEA_PP(0); - bytea *t2 = PG_GETARG_BYTEA_PP(1); - - PG_RETURN_BYTEA_P(bytea_catenate(t1, t2)); -} - -/* - * bytea_catenate - * Guts of byteacat(), broken out so it can be used by other functions - * - * Arguments can be in short-header form, but not compressed or out-of-line - */ -static bytea * -bytea_catenate(bytea *t1, bytea *t2) -{ - bytea *result; - int len1, - len2, - len; - char *ptr; - - len1 = VARSIZE_ANY_EXHDR(t1); - len2 = VARSIZE_ANY_EXHDR(t2); - - /* paranoia ... probably should throw error instead? */ - if (len1 < 0) - len1 = 0; - if (len2 < 0) - len2 = 0; - - len = len1 + len2 + VARHDRSZ; - result = (bytea *) palloc(len); - - /* Set size of result string... */ - SET_VARSIZE(result, len); - - /* Fill data field of result string... */ - ptr = VARDATA(result); - if (len1 > 0) - memcpy(ptr, VARDATA_ANY(t1), len1); - if (len2 > 0) - memcpy(ptr + len1, VARDATA_ANY(t2), len2); - - return result; -} - -#define PG_STR_GET_BYTEA(str_) \ - DatumGetByteaPP(DirectFunctionCall1(byteain, CStringGetDatum(str_))) - -/* - * bytea_substr() - * Return a substring starting at the specified position. - * Cloned from text_substr and modified as required. - * - * Input: - * - string - * - starting position (is one-based) - * - string length (optional) - * - * If the starting position is zero or less, then return from the start of the string - * adjusting the length to be consistent with the "negative start" per SQL. - * If the length is less than zero, an ERROR is thrown. If no third argument - * (length) is provided, the length to the end of the string is assumed. - */ -Datum -bytea_substr(PG_FUNCTION_ARGS) -{ - PG_RETURN_BYTEA_P(bytea_substring(PG_GETARG_DATUM(0), - PG_GETARG_INT32(1), - PG_GETARG_INT32(2), - false)); -} - -/* - * bytea_substr_no_len - - * Wrapper to avoid opr_sanity failure due to - * one function accepting a different number of args. - */ -Datum -bytea_substr_no_len(PG_FUNCTION_ARGS) -{ - PG_RETURN_BYTEA_P(bytea_substring(PG_GETARG_DATUM(0), - PG_GETARG_INT32(1), - -1, - true)); -} - -static bytea * -bytea_substring(Datum str, - int S, - int L, - bool length_not_specified) -{ - int32 S1; /* adjusted start position */ - int32 L1; /* adjusted substring length */ - int32 E; /* end position */ - - /* - * The logic here should generally match text_substring(). - */ - S1 = Max(S, 1); - - if (length_not_specified) - { - /* - * Not passed a length - DatumGetByteaPSlice() grabs everything to the - * end of the string if we pass it a negative value for length. - */ - L1 = -1; - } - else if (L < 0) - { - /* SQL99 says to throw an error for E < S, i.e., negative length */ - ereport(ERROR, - (errcode(ERRCODE_SUBSTRING_ERROR), - errmsg("negative substring length not allowed"))); - L1 = -1; /* silence stupider compilers */ - } - else if (pg_add_s32_overflow(S, L, &E)) - { - /* - * L could be large enough for S + L to overflow, in which case the - * substring must run to end of string. - */ - L1 = -1; - } - else - { - /* - * A zero or negative value for the end position can happen if the - * start was negative or one. SQL99 says to return a zero-length - * string. - */ - if (E < 1) - return PG_STR_GET_BYTEA(""); - - L1 = E - S1; - } - - /* - * If the start position is past the end of the string, SQL99 says to - * return a zero-length string -- DatumGetByteaPSlice() will do that for - * us. We need only convert S1 to zero-based starting position. - */ - return DatumGetByteaPSlice(str, S1 - 1, L1); -} - -/* - * byteaoverlay - * Replace specified substring of first string with second - * - * The SQL standard defines OVERLAY() in terms of substring and concatenation. - * This code is a direct implementation of what the standard says. - */ -Datum -byteaoverlay(PG_FUNCTION_ARGS) -{ - bytea *t1 = PG_GETARG_BYTEA_PP(0); - bytea *t2 = PG_GETARG_BYTEA_PP(1); - int sp = PG_GETARG_INT32(2); /* substring start position */ - int sl = PG_GETARG_INT32(3); /* substring length */ - - PG_RETURN_BYTEA_P(bytea_overlay(t1, t2, sp, sl)); -} - -Datum -byteaoverlay_no_len(PG_FUNCTION_ARGS) -{ - bytea *t1 = PG_GETARG_BYTEA_PP(0); - bytea *t2 = PG_GETARG_BYTEA_PP(1); - int sp = PG_GETARG_INT32(2); /* substring start position */ - int sl; - - sl = VARSIZE_ANY_EXHDR(t2); /* defaults to length(t2) */ - PG_RETURN_BYTEA_P(bytea_overlay(t1, t2, sp, sl)); -} - -static bytea * -bytea_overlay(bytea *t1, bytea *t2, int sp, int sl) -{ - bytea *result; - bytea *s1; - bytea *s2; - int sp_pl_sl; - - /* - * Check for possible integer-overflow cases. For negative sp, throw a - * "substring length" error because that's what should be expected - * according to the spec's definition of OVERLAY(). - */ - if (sp <= 0) - ereport(ERROR, - (errcode(ERRCODE_SUBSTRING_ERROR), - errmsg("negative substring length not allowed"))); - if (pg_add_s32_overflow(sp, sl, &sp_pl_sl)) - ereport(ERROR, - (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("integer out of range"))); - - s1 = bytea_substring(PointerGetDatum(t1), 1, sp - 1, false); - s2 = bytea_substring(PointerGetDatum(t1), sp_pl_sl, -1, true); - result = bytea_catenate(s1, t2); - result = bytea_catenate(result, s2); - - return result; -} - -/* - * bit_count - */ -Datum -bytea_bit_count(PG_FUNCTION_ARGS) -{ - bytea *t1 = PG_GETARG_BYTEA_PP(0); - - PG_RETURN_INT64(pg_popcount(VARDATA_ANY(t1), VARSIZE_ANY_EXHDR(t1))); -} - -/* - * byteapos - - * Return the position of the specified substring. - * Implements the SQL POSITION() function. - * Cloned from textpos and modified as required. - */ -Datum -byteapos(PG_FUNCTION_ARGS) -{ - bytea *t1 = PG_GETARG_BYTEA_PP(0); - bytea *t2 = PG_GETARG_BYTEA_PP(1); - int pos; - int px, - p; - int len1, - len2; - char *p1, - *p2; - - len1 = VARSIZE_ANY_EXHDR(t1); - len2 = VARSIZE_ANY_EXHDR(t2); - - if (len2 <= 0) - PG_RETURN_INT32(1); /* result for empty pattern */ - - p1 = VARDATA_ANY(t1); - p2 = VARDATA_ANY(t2); - - pos = 0; - px = (len1 - len2); - for (p = 0; p <= px; p++) - { - if ((*p2 == *p1) && (memcmp(p1, p2, len2) == 0)) - { - pos = p + 1; - break; - }; - p1++; - }; - - PG_RETURN_INT32(pos); -} - -/*------------------------------------------------------------- - * byteaGetByte - * - * this routine treats "bytea" as an array of bytes. - * It returns the Nth byte (a number between 0 and 255). - *------------------------------------------------------------- - */ -Datum -byteaGetByte(PG_FUNCTION_ARGS) -{ - bytea *v = PG_GETARG_BYTEA_PP(0); - int32 n = PG_GETARG_INT32(1); - int len; - int byte; - - len = VARSIZE_ANY_EXHDR(v); - - if (n < 0 || n >= len) - ereport(ERROR, - (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), - errmsg("index %d out of valid range, 0..%d", - n, len - 1))); - - byte = ((unsigned char *) VARDATA_ANY(v))[n]; - - PG_RETURN_INT32(byte); -} - -/*------------------------------------------------------------- - * byteaGetBit - * - * This routine treats a "bytea" type like an array of bits. - * It returns the value of the Nth bit (0 or 1). - * - *------------------------------------------------------------- - */ -Datum -byteaGetBit(PG_FUNCTION_ARGS) -{ - bytea *v = PG_GETARG_BYTEA_PP(0); - int64 n = PG_GETARG_INT64(1); - int byteNo, - bitNo; - int len; - int byte; - - len = VARSIZE_ANY_EXHDR(v); - - if (n < 0 || n >= (int64) len * 8) - ereport(ERROR, - (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), - errmsg("index %" PRId64 " out of valid range, 0..%" PRId64, - n, (int64) len * 8 - 1))); - - /* n/8 is now known < len, so safe to cast to int */ - byteNo = (int) (n / 8); - bitNo = (int) (n % 8); - - byte = ((unsigned char *) VARDATA_ANY(v))[byteNo]; - - if (byte & (1 << bitNo)) - PG_RETURN_INT32(1); - else - PG_RETURN_INT32(0); -} - -/*------------------------------------------------------------- - * byteaSetByte - * - * Given an instance of type 'bytea' creates a new one with - * the Nth byte set to the given value. - * - *------------------------------------------------------------- - */ -Datum -byteaSetByte(PG_FUNCTION_ARGS) -{ - bytea *res = PG_GETARG_BYTEA_P_COPY(0); - int32 n = PG_GETARG_INT32(1); - int32 newByte = PG_GETARG_INT32(2); - int len; - - len = VARSIZE(res) - VARHDRSZ; - - if (n < 0 || n >= len) - ereport(ERROR, - (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), - errmsg("index %d out of valid range, 0..%d", - n, len - 1))); - - /* - * Now set the byte. - */ - ((unsigned char *) VARDATA(res))[n] = newByte; - - PG_RETURN_BYTEA_P(res); -} - -/*------------------------------------------------------------- - * byteaSetBit - * - * Given an instance of type 'bytea' creates a new one with - * the Nth bit set to the given value. - * - *------------------------------------------------------------- - */ -Datum -byteaSetBit(PG_FUNCTION_ARGS) -{ - bytea *res = PG_GETARG_BYTEA_P_COPY(0); - int64 n = PG_GETARG_INT64(1); - int32 newBit = PG_GETARG_INT32(2); - int len; - int oldByte, - newByte; - int byteNo, - bitNo; - - len = VARSIZE(res) - VARHDRSZ; - - if (n < 0 || n >= (int64) len * 8) - ereport(ERROR, - (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), - errmsg("index %" PRId64 " out of valid range, 0..%" PRId64, - n, (int64) len * 8 - 1))); - - /* n/8 is now known < len, so safe to cast to int */ - byteNo = (int) (n / 8); - bitNo = (int) (n % 8); - - /* - * sanity check! - */ - if (newBit != 0 && newBit != 1) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("new bit must be 0 or 1"))); - - /* - * Update the byte. - */ - oldByte = ((unsigned char *) VARDATA(res))[byteNo]; - - if (newBit == 0) - newByte = oldByte & (~(1 << bitNo)); - else - newByte = oldByte | (1 << bitNo); - - ((unsigned char *) VARDATA(res))[byteNo] = newByte; - - PG_RETURN_BYTEA_P(res); -} - -/* - * Return reversed bytea - */ -Datum -bytea_reverse(PG_FUNCTION_ARGS) -{ - bytea *v = PG_GETARG_BYTEA_PP(0); - const char *p = VARDATA_ANY(v); - int len = VARSIZE_ANY_EXHDR(v); - const char *endp = p + len; - bytea *result = palloc(len + VARHDRSZ); - char *dst = (char *) VARDATA(result) + len; - - SET_VARSIZE(result, len + VARHDRSZ); - - while (p < endp) - *(--dst) = *p++; - - PG_RETURN_BYTEA_P(result); -} - - /* text_name() * Converts a text type to a Name type. */ @@ -3480,24 +2732,114 @@ textToQualifiedNameList(text *textval) (errcode(ERRCODE_INVALID_NAME), errmsg("invalid name syntax"))); - if (namelist == NIL) - ereport(ERROR, - (errcode(ERRCODE_INVALID_NAME), - errmsg("invalid name syntax"))); + if (namelist == NIL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("invalid name syntax"))); + + foreach(l, namelist) + { + char *curname = (char *) lfirst(l); + + result = lappend(result, makeString(pstrdup(curname))); + } + + pfree(rawname); + list_free(namelist); + + return result; +} + +/* + * scan_quoted_identifier - In-place scanner for quoted identifiers. + * + * *nextp should point to the opening double-quote character, and will be + * updated to point just past the end. *endp is set to the position of + * the closing quote. The return value is the identifier, or NULL if the + * matching close-quote cannot be found. + * + * If we find two consecutive double quote characters, that doesn't end the + * identifier: instead, we collapse them into a double quote and include them + * in the resulting token. Note that this requires overwriting the rest of the + * string in place, including the portion beyond the final value of *nextp. + */ +char * +scan_quoted_identifier(char **endp, char **nextp) +{ + char *token = *nextp + 1; + + for (;;) + { + *endp = strchr(*nextp + 1, '"'); + if (*endp == NULL) + return NULL; /* mismatched quotes */ + if ((*endp)[1] != '"') + break; /* found end of quoted identifier */ + /* Collapse adjacent quotes into one quote, and look again */ + memmove(*endp, *endp + 1, strlen(*endp)); + *nextp = *endp; + } + /* *endp now points at the terminating quote */ + *nextp = *endp + 1; + + return token; +} + +/* + * scan_identifier - In-place scanner for quoted or unquoted identifiers. + * + * On success, *endp is set to the position where the caller should write '\0' + * to null-terminate the token, and *nextp is advanced past the token (and past + * the closing quote, if any). The return value is the token content, or NULL + * if there is a syntax error (mismatched quotes or empty unquoted token). + * + * Unquoted identifiers are terminated by whitespace or the first occurrence + * of the separator character. Additionally, if downcase_unquoted = true, + * unquoted identifiers are downcased in place. See scan_quoted_identifier for + * an additional way in which we modify the string in place. + */ +char * +scan_identifier(char **endp, char **nextp, char separator, bool downcase_unquoted) +{ + char *token; + + if (**nextp == '"') + return scan_quoted_identifier(endp, nextp); + + /* Unquoted identifier --- extends to separator or whitespace */ + token = *nextp; + + while (**nextp && **nextp != separator && !scanner_isspace(**nextp)) + (*nextp)++; + + if (*nextp == token) + return NULL; /* empty token */ - foreach(l, namelist) + *endp = *nextp; + + if (downcase_unquoted) { - char *curname = (char *) lfirst(l); + /* + * Downcase the identifier, using same code as main lexer does. + * + * XXX because we want to overwrite the input in-place, we cannot + * support a downcasing transformation that increases the string + * length. This is not a problem given the current implementation of + * downcase_truncate_identifier, but we'll probably have to do + * something about this someday. + */ + int len = *endp - token; + char *downname = downcase_truncate_identifier(token, len, false); - result = lappend(result, makeString(pstrdup(curname))); + Assert(strlen(downname) <= len); + strncpy(token, downname, len); /* strncpy is required here */ + pfree(downname); } - pfree(rawname); - list_free(namelist); - - return result; + return token; } + /* * SplitIdentifierString --- parse a string containing identifiers * @@ -3534,7 +2876,7 @@ SplitIdentifierString(char *rawstring, char separator, nextp++; /* skip leading whitespace */ if (*nextp == '\0') - return true; /* allow empty string */ + return true; /* empty string represents empty list */ /* At the top of the loop, we are at start of a new identifier. */ do @@ -3542,53 +2884,9 @@ SplitIdentifierString(char *rawstring, char separator, char *curname; char *endp; - if (*nextp == '"') - { - /* Quoted name --- collapse quote-quote pairs, no downcasing */ - curname = nextp + 1; - for (;;) - { - endp = strchr(nextp + 1, '"'); - if (endp == NULL) - return false; /* mismatched quotes */ - if (endp[1] != '"') - break; /* found end of quoted name */ - /* Collapse adjacent quotes into one quote, and look again */ - memmove(endp, endp + 1, strlen(endp)); - nextp = endp; - } - /* endp now points at the terminating quote */ - nextp = endp + 1; - } - else - { - /* Unquoted name --- extends to separator or whitespace */ - char *downname; - int len; - - curname = nextp; - while (*nextp && *nextp != separator && - !scanner_isspace(*nextp)) - nextp++; - endp = nextp; - if (curname == nextp) - return false; /* empty unquoted name not allowed */ - - /* - * Downcase the identifier, using same code as main lexer does. - * - * XXX because we want to overwrite the input in-place, we cannot - * support a downcasing transformation that increases the string - * length. This is not a problem given the current implementation - * of downcase_truncate_identifier, but we'll probably have to do - * something about this someday. - */ - len = endp - curname; - downname = downcase_truncate_identifier(curname, len, false); - Assert(strlen(downname) <= len); - strncpy(curname, downname, len); /* strncpy is required here */ - pfree(downname); - } + curname = scan_identifier(&endp, &nextp, separator, true); + if (curname == NULL) + return false; /* mismatched quotes or empty name */ while (scanner_isspace(*nextp)) nextp++; /* skip trailing whitespace */ @@ -3661,7 +2959,7 @@ SplitDirectoriesString(char *rawstring, char separator, nextp++; /* skip leading whitespace */ if (*nextp == '\0') - return true; /* allow empty string */ + return true; /* empty string represents empty list */ /* At the top of the loop, we are at start of a new directory. */ do @@ -3672,20 +2970,9 @@ SplitDirectoriesString(char *rawstring, char separator, if (*nextp == '"') { /* Quoted name --- collapse quote-quote pairs */ - curname = nextp + 1; - for (;;) - { - endp = strchr(nextp + 1, '"'); - if (endp == NULL) - return false; /* mismatched quotes */ - if (endp[1] != '"') - break; /* found end of quoted name */ - /* Collapse adjacent quotes into one quote, and look again */ - memmove(endp, endp + 1, strlen(endp)); - nextp = endp; - } - /* endp now points at the terminating quote */ - nextp = endp + 1; + curname = scan_quoted_identifier(&endp, &nextp); + if (curname == NULL) + return false; /* mismatched quotes */ } else { @@ -3782,7 +3069,7 @@ SplitGUCList(char *rawstring, char separator, nextp++; /* skip leading whitespace */ if (*nextp == '\0') - return true; /* allow empty string */ + return true; /* empty string represents empty list */ /* At the top of the loop, we are at start of a new identifier. */ do @@ -3790,35 +3077,9 @@ SplitGUCList(char *rawstring, char separator, char *curname; char *endp; - if (*nextp == '"') - { - /* Quoted name --- collapse quote-quote pairs */ - curname = nextp + 1; - for (;;) - { - endp = strchr(nextp + 1, '"'); - if (endp == NULL) - return false; /* mismatched quotes */ - if (endp[1] != '"') - break; /* found end of quoted name */ - /* Collapse adjacent quotes into one quote, and look again */ - memmove(endp, endp + 1, strlen(endp)); - nextp = endp; - } - /* endp now points at the terminating quote */ - nextp = endp + 1; - } - else - { - /* Unquoted name --- extends to separator or whitespace */ - curname = nextp; - while (*nextp && *nextp != separator && - !scanner_isspace(*nextp)) - nextp++; - endp = nextp; - if (curname == nextp) - return false; /* empty unquoted name not allowed */ - } + curname = scan_identifier(&endp, &nextp, separator, false); + if (curname == NULL) + return false; /* mismatched quotes or empty name */ while (scanner_isspace(*nextp)) nextp++; /* skip trailing whitespace */ @@ -3849,331 +3110,6 @@ SplitGUCList(char *rawstring, char separator, return true; } - -/***************************************************************************** - * Comparison Functions used for bytea - * - * Note: btree indexes need these routines not to leak memory; therefore, - * be careful to free working copies of toasted datums. Most places don't - * need to be so careful. - *****************************************************************************/ - -Datum -byteaeq(PG_FUNCTION_ARGS) -{ - Datum arg1 = PG_GETARG_DATUM(0); - Datum arg2 = PG_GETARG_DATUM(1); - bool result; - Size len1, - len2; - - /* - * We can use a fast path for unequal lengths, which might save us from - * having to detoast one or both values. - */ - len1 = toast_raw_datum_size(arg1); - len2 = toast_raw_datum_size(arg2); - if (len1 != len2) - result = false; - else - { - bytea *barg1 = DatumGetByteaPP(arg1); - bytea *barg2 = DatumGetByteaPP(arg2); - - result = (memcmp(VARDATA_ANY(barg1), VARDATA_ANY(barg2), - len1 - VARHDRSZ) == 0); - - PG_FREE_IF_COPY(barg1, 0); - PG_FREE_IF_COPY(barg2, 1); - } - - PG_RETURN_BOOL(result); -} - -Datum -byteane(PG_FUNCTION_ARGS) -{ - Datum arg1 = PG_GETARG_DATUM(0); - Datum arg2 = PG_GETARG_DATUM(1); - bool result; - Size len1, - len2; - - /* - * We can use a fast path for unequal lengths, which might save us from - * having to detoast one or both values. - */ - len1 = toast_raw_datum_size(arg1); - len2 = toast_raw_datum_size(arg2); - if (len1 != len2) - result = true; - else - { - bytea *barg1 = DatumGetByteaPP(arg1); - bytea *barg2 = DatumGetByteaPP(arg2); - - result = (memcmp(VARDATA_ANY(barg1), VARDATA_ANY(barg2), - len1 - VARHDRSZ) != 0); - - PG_FREE_IF_COPY(barg1, 0); - PG_FREE_IF_COPY(barg2, 1); - } - - PG_RETURN_BOOL(result); -} - -Datum -bytealt(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - - PG_FREE_IF_COPY(arg1, 0); - PG_FREE_IF_COPY(arg2, 1); - - PG_RETURN_BOOL((cmp < 0) || ((cmp == 0) && (len1 < len2))); -} - -Datum -byteale(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - - PG_FREE_IF_COPY(arg1, 0); - PG_FREE_IF_COPY(arg2, 1); - - PG_RETURN_BOOL((cmp < 0) || ((cmp == 0) && (len1 <= len2))); -} - -Datum -byteagt(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - - PG_FREE_IF_COPY(arg1, 0); - PG_FREE_IF_COPY(arg2, 1); - - PG_RETURN_BOOL((cmp > 0) || ((cmp == 0) && (len1 > len2))); -} - -Datum -byteage(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - - PG_FREE_IF_COPY(arg1, 0); - PG_FREE_IF_COPY(arg2, 1); - - PG_RETURN_BOOL((cmp > 0) || ((cmp == 0) && (len1 >= len2))); -} - -Datum -byteacmp(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - if ((cmp == 0) && (len1 != len2)) - cmp = (len1 < len2) ? -1 : 1; - - PG_FREE_IF_COPY(arg1, 0); - PG_FREE_IF_COPY(arg2, 1); - - PG_RETURN_INT32(cmp); -} - -Datum -bytea_larger(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - bytea *result; - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - result = ((cmp > 0) || ((cmp == 0) && (len1 > len2)) ? arg1 : arg2); - - PG_RETURN_BYTEA_P(result); -} - -Datum -bytea_smaller(PG_FUNCTION_ARGS) -{ - bytea *arg1 = PG_GETARG_BYTEA_PP(0); - bytea *arg2 = PG_GETARG_BYTEA_PP(1); - bytea *result; - int len1, - len2; - int cmp; - - len1 = VARSIZE_ANY_EXHDR(arg1); - len2 = VARSIZE_ANY_EXHDR(arg2); - - cmp = memcmp(VARDATA_ANY(arg1), VARDATA_ANY(arg2), Min(len1, len2)); - result = ((cmp < 0) || ((cmp == 0) && (len1 < len2)) ? arg1 : arg2); - - PG_RETURN_BYTEA_P(result); -} - -Datum -bytea_sortsupport(PG_FUNCTION_ARGS) -{ - SortSupport ssup = (SortSupport) PG_GETARG_POINTER(0); - MemoryContext oldcontext; - - oldcontext = MemoryContextSwitchTo(ssup->ssup_cxt); - - /* Use generic string SortSupport, forcing "C" collation */ - varstr_sortsupport(ssup, BYTEAOID, C_COLLATION_OID); - - MemoryContextSwitchTo(oldcontext); - - PG_RETURN_VOID(); -} - -/* Cast bytea -> int2 */ -Datum -bytea_int2(PG_FUNCTION_ARGS) -{ - bytea *v = PG_GETARG_BYTEA_PP(0); - int len = VARSIZE_ANY_EXHDR(v); - uint16 result; - - /* Check that the byte array is not too long */ - if (len > sizeof(result)) - ereport(ERROR, - errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("smallint out of range")); - - /* Convert it to an integer; most significant bytes come first */ - result = 0; - for (int i = 0; i < len; i++) - { - result <<= BITS_PER_BYTE; - result |= ((unsigned char *) VARDATA_ANY(v))[i]; - } - - PG_RETURN_INT16(result); -} - -/* Cast bytea -> int4 */ -Datum -bytea_int4(PG_FUNCTION_ARGS) -{ - bytea *v = PG_GETARG_BYTEA_PP(0); - int len = VARSIZE_ANY_EXHDR(v); - uint32 result; - - /* Check that the byte array is not too long */ - if (len > sizeof(result)) - ereport(ERROR, - errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("integer out of range")); - - /* Convert it to an integer; most significant bytes come first */ - result = 0; - for (int i = 0; i < len; i++) - { - result <<= BITS_PER_BYTE; - result |= ((unsigned char *) VARDATA_ANY(v))[i]; - } - - PG_RETURN_INT32(result); -} - -/* Cast bytea -> int8 */ -Datum -bytea_int8(PG_FUNCTION_ARGS) -{ - bytea *v = PG_GETARG_BYTEA_PP(0); - int len = VARSIZE_ANY_EXHDR(v); - uint64 result; - - /* Check that the byte array is not too long */ - if (len > sizeof(result)) - ereport(ERROR, - errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), - errmsg("bigint out of range")); - - /* Convert it to an integer; most significant bytes come first */ - result = 0; - for (int i = 0; i < len; i++) - { - result <<= BITS_PER_BYTE; - result |= ((unsigned char *) VARDATA_ANY(v))[i]; - } - - PG_RETURN_INT64(result); -} - -/* Cast int2 -> bytea; can just use int2send() */ -Datum -int2_bytea(PG_FUNCTION_ARGS) -{ - return int2send(fcinfo); -} - -/* Cast int4 -> bytea; can just use int4send() */ -Datum -int4_bytea(PG_FUNCTION_ARGS) -{ - return int4send(fcinfo); -} - -/* Cast int8 -> bytea; can just use int8send() */ -Datum -int8_bytea(PG_FUNCTION_ARGS) -{ - return int8send(fcinfo); -} - /* * appendStringInfoText * @@ -4887,6 +3823,8 @@ split_text(FunctionCallInfo fcinfo, SplitTextOutputData *tstate) } else { + const char *end_ptr; + /* * When fldsep is NULL, each character in the input string becomes a * separate element in the result set. The separator is effectively @@ -4895,10 +3833,11 @@ split_text(FunctionCallInfo fcinfo, SplitTextOutputData *tstate) inputstring_len = VARSIZE_ANY_EXHDR(inputstring); start_ptr = VARDATA_ANY(inputstring); + end_ptr = start_ptr + inputstring_len; while (inputstring_len > 0) { - int chunk_len = pg_mblen(start_ptr); + int chunk_len = pg_mblen_range(start_ptr, end_ptr); CHECK_FOR_INTERRUPTS(); @@ -5018,10 +3957,11 @@ array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, int typlen; bool typbyval; char typalign; + uint8 typalignby; StringInfoData buf; bool printed = false; char *p; - bits8 *bitmap; + uint8 *bitmap; int bitmask; int i; ArrayMetaState *my_extra; @@ -5067,6 +4007,7 @@ array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, typlen = my_extra->typlen; typbyval = my_extra->typbyval; typalign = my_extra->typalign; + typalignby = typalign_to_alignby(typalign); p = ARR_DATA_PTR(v); bitmap = ARR_NULLBITMAP(v); @@ -5103,7 +4044,7 @@ array_to_text_internal(FunctionCallInfo fcinfo, ArrayType *v, printed = true; p = att_addlength_pointer(p, typlen, p); - p = (char *) att_align_nominal(p, typalign); + p = (char *) att_nominal_alignby(p, typalignby); } /* advance bitmap pointer if any */ @@ -5287,7 +4228,7 @@ pg_column_compression(PG_FUNCTION_ARGS) PG_RETURN_NULL(); /* get the compression method id stored in the compressed varlena */ - cmid = toast_get_compression_id((struct varlena *) + cmid = toast_get_compression_id((varlena *) DatumGetPointer(PG_GETARG_DATUM(0))); if (cmid == TOAST_INVALID_COMPRESSION_ID) PG_RETURN_NULL(); @@ -5316,8 +4257,8 @@ Datum pg_column_toast_chunk_id(PG_FUNCTION_ARGS) { int typlen; - struct varlena *attr; - struct varatt_external toast_pointer; + varlena *attr; + varatt_external toast_pointer; /* On first call, get the input type's typlen, and save at *fn_extra */ if (fcinfo->flinfo->fn_extra == NULL) @@ -5339,7 +4280,7 @@ pg_column_toast_chunk_id(PG_FUNCTION_ARGS) if (typlen != -1) PG_RETURN_NULL(); - attr = (struct varlena *) DatumGetPointer(PG_GETARG_DATUM(0)); + attr = (varlena *) DatumGetPointer(PG_GETARG_DATUM(0)); if (!VARATT_IS_EXTERNAL_ONDISK(attr)) PG_RETURN_NULL(); @@ -5802,7 +4743,7 @@ text_reverse(PG_FUNCTION_ARGS) { int sz; - sz = pg_mblen(p); + sz = pg_mblen_range(p, endp); dst -= sz; memcpy(dst, p, sz); p += sz; @@ -5963,7 +4904,7 @@ text_format(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized format() type specifier \"%.*s\"", - pg_mblen(cp), cp), + pg_mblen_range(cp, end_ptr), cp), errhint("For a single \"%%\" use \"%%%%\"."))); /* If indirect width was specified, get its value */ @@ -6084,7 +5025,7 @@ text_format(PG_FUNCTION_ARGS) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("unrecognized format() type specifier \"%.*s\"", - pg_mblen(cp), cp), + pg_mblen_range(cp, end_ptr), cp), errhint("For a single \"%%\" use \"%%%%\"."))); break; } @@ -6503,11 +5444,12 @@ unicode_version(PG_FUNCTION_ARGS) Datum icu_unicode_version(PG_FUNCTION_ARGS) { -#ifdef USE_ICU - PG_RETURN_TEXT_P(cstring_to_text(U_UNICODE_VERSION)); -#else - PG_RETURN_NULL(); -#endif + const char *version = pg_icu_unicode_version(); + + if (version) + PG_RETURN_TEXT_P(cstring_to_text(version)); + else + PG_RETURN_NULL(); } /* @@ -6525,12 +5467,12 @@ unicode_assigned(PG_FUNCTION_ARGS) ereport(ERROR, (errmsg("Unicode categorization can only be performed if server encoding is UTF8"))); - /* convert to pg_wchar */ + /* convert to char32_t */ size = pg_mbstrlen_with_len(VARDATA_ANY(input), VARSIZE_ANY_EXHDR(input)); p = (unsigned char *) VARDATA_ANY(input); for (int i = 0; i < size; i++) { - pg_wchar uchar = utf8_to_unicode(p); + char32_t uchar = utf8_to_unicode(p); int category = unicode_category(uchar); if (category == PG_U_UNASSIGNED) @@ -6549,24 +5491,24 @@ unicode_normalize_func(PG_FUNCTION_ARGS) char *formstr = text_to_cstring(PG_GETARG_TEXT_PP(1)); UnicodeNormalizationForm form; int size; - pg_wchar *input_chars; - pg_wchar *output_chars; + char32_t *input_chars; + char32_t *output_chars; unsigned char *p; text *result; int i; form = unicode_norm_form_from_string(formstr); - /* convert to pg_wchar */ + /* convert to char32_t */ size = pg_mbstrlen_with_len(VARDATA_ANY(input), VARSIZE_ANY_EXHDR(input)); - input_chars = palloc((size + 1) * sizeof(pg_wchar)); + input_chars = palloc((size + 1) * sizeof(char32_t)); p = (unsigned char *) VARDATA_ANY(input); for (i = 0; i < size; i++) { input_chars[i] = utf8_to_unicode(p); p += pg_utf_mblen(p); } - input_chars[i] = (pg_wchar) '\0'; + input_chars[i] = (char32_t) '\0'; Assert((char *) p == VARDATA_ANY(input) + VARSIZE_ANY_EXHDR(input)); /* action */ @@ -6574,7 +5516,7 @@ unicode_normalize_func(PG_FUNCTION_ARGS) /* convert back to UTF-8 string */ size = 0; - for (pg_wchar *wp = output_chars; *wp; wp++) + for (char32_t *wp = output_chars; *wp; wp++) { unsigned char buf[4]; @@ -6586,7 +5528,7 @@ unicode_normalize_func(PG_FUNCTION_ARGS) SET_VARSIZE(result, size + VARHDRSZ); p = (unsigned char *) VARDATA_ANY(result); - for (pg_wchar *wp = output_chars; *wp; wp++) + for (char32_t *wp = output_chars; *wp; wp++) { unicode_to_utf8(*wp, p); p += pg_utf_mblen(p); @@ -6615,8 +5557,8 @@ unicode_is_normalized(PG_FUNCTION_ARGS) char *formstr = text_to_cstring(PG_GETARG_TEXT_PP(1)); UnicodeNormalizationForm form; int size; - pg_wchar *input_chars; - pg_wchar *output_chars; + char32_t *input_chars; + char32_t *output_chars; unsigned char *p; int i; UnicodeNormalizationQC quickcheck; @@ -6625,16 +5567,16 @@ unicode_is_normalized(PG_FUNCTION_ARGS) form = unicode_norm_form_from_string(formstr); - /* convert to pg_wchar */ + /* convert to char32_t */ size = pg_mbstrlen_with_len(VARDATA_ANY(input), VARSIZE_ANY_EXHDR(input)); - input_chars = palloc((size + 1) * sizeof(pg_wchar)); + input_chars = palloc((size + 1) * sizeof(char32_t)); p = (unsigned char *) VARDATA_ANY(input); for (i = 0; i < size; i++) { input_chars[i] = utf8_to_unicode(p); p += pg_utf_mblen(p); } - input_chars[i] = (pg_wchar) '\0'; + input_chars[i] = (char32_t) '\0'; Assert((char *) p == VARDATA_ANY(input) + VARSIZE_ANY_EXHDR(input)); /* quick check (see UAX #15) */ @@ -6648,11 +5590,11 @@ unicode_is_normalized(PG_FUNCTION_ARGS) output_chars = unicode_normalize(form, input_chars); output_size = 0; - for (pg_wchar *wp = output_chars; *wp; wp++) + for (char32_t *wp = output_chars; *wp; wp++) output_size++; result = (size == output_size) && - (memcmp(input_chars, output_chars, size * sizeof(pg_wchar)) == 0); + (memcmp(input_chars, output_chars, size * sizeof(char32_t)) == 0); PG_RETURN_BOOL(result); } @@ -6708,7 +5650,7 @@ unistr(PG_FUNCTION_ARGS) int len; StringInfoData str; text *result; - pg_wchar pair_first = 0; + char16_t pair_first = 0; char cbuf[MAX_UNICODE_EQUIVALENT_STRING + 1]; instr = VARDATA_ANY(input_text); @@ -6732,7 +5674,7 @@ unistr(PG_FUNCTION_ARGS) else if ((len >= 5 && isxdigits_n(instr + 1, 4)) || (len >= 6 && instr[1] == 'u' && isxdigits_n(instr + 2, 4))) { - pg_wchar unicode; + char32_t unicode; int offset = instr[1] == 'u' ? 2 : 1; unicode = hexval_n(instr + offset, 4); @@ -6768,7 +5710,7 @@ unistr(PG_FUNCTION_ARGS) } else if (len >= 8 && instr[1] == '+' && isxdigits_n(instr + 2, 6)) { - pg_wchar unicode; + char32_t unicode; unicode = hexval_n(instr + 2, 6); @@ -6803,7 +5745,7 @@ unistr(PG_FUNCTION_ARGS) } else if (len >= 10 && instr[1] == 'U' && isxdigits_n(instr + 2, 8)) { - pg_wchar unicode; + char32_t unicode; unicode = hexval_n(instr + 2, 8); diff --git a/src/backend/utils/adt/version.c b/src/backend/utils/adt/version.c index 9f56ef3fb8ffc..ec3843cab9695 100644 --- a/src/backend/utils/adt/version.c +++ b/src/backend/utils/adt/version.c @@ -3,7 +3,7 @@ * version.c * Returns the PostgreSQL version string * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * IDENTIFICATION * diff --git a/src/backend/utils/adt/waitfuncs.c b/src/backend/utils/adt/waitfuncs.c index ddd0a57c0c597..135e7ba8a7a47 100644 --- a/src/backend/utils/adt/waitfuncs.c +++ b/src/backend/utils/adt/waitfuncs.c @@ -3,7 +3,7 @@ * waitfuncs.c * Functions for SQL access to syntheses of multiple contention types. * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/adt/waitfuncs.c @@ -73,7 +73,7 @@ pg_isolation_test_session_is_blocked(PG_FUNCTION_ARGS) * acquire heavyweight locks. */ blocking_pids_a = - DatumGetArrayTypeP(DirectFunctionCall1(pg_blocking_pids, blocked_pid)); + DatumGetArrayTypeP(DirectFunctionCall1(pg_blocking_pids, Int32GetDatum(blocked_pid))); Assert(ARR_ELEMTYPE(blocking_pids_a) == INT4OID); Assert(!array_contains_nulls(blocking_pids_a)); diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c index bb35f3bc4a981..78b7f05aba280 100644 --- a/src/backend/utils/adt/windowfuncs.c +++ b/src/backend/utils/adt/windowfuncs.c @@ -3,7 +3,7 @@ * windowfuncs.c * Standard window functions defined in SQL spec. * - * Portions Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2000-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -86,6 +86,7 @@ window_row_number(PG_FUNCTION_ARGS) WindowObject winobj = PG_WINDOW_OBJECT(); int64 curpos = WinGetCurrentPosition(winobj); + WinCheckAndInitializeNullTreatment(winobj, false, fcinfo); WinSetMarkPosition(winobj, curpos); PG_RETURN_INT64(curpos + 1); } @@ -141,6 +142,7 @@ window_rank(PG_FUNCTION_ARGS) rank_context *context; bool up; + WinCheckAndInitializeNullTreatment(winobj, false, fcinfo); up = rank_up(winobj); context = (rank_context *) WinGetPartitionLocalMemory(winobj, sizeof(rank_context)); @@ -203,6 +205,7 @@ window_dense_rank(PG_FUNCTION_ARGS) rank_context *context; bool up; + WinCheckAndInitializeNullTreatment(winobj, false, fcinfo); up = rank_up(winobj); context = (rank_context *) WinGetPartitionLocalMemory(winobj, sizeof(rank_context)); @@ -266,6 +269,7 @@ window_percent_rank(PG_FUNCTION_ARGS) int64 totalrows = WinGetPartitionRowCount(winobj); Assert(totalrows > 0); + WinCheckAndInitializeNullTreatment(winobj, false, fcinfo); up = rank_up(winobj); context = (rank_context *) @@ -335,6 +339,7 @@ window_cume_dist(PG_FUNCTION_ARGS) int64 totalrows = WinGetPartitionRowCount(winobj); Assert(totalrows > 0); + WinCheckAndInitializeNullTreatment(winobj, false, fcinfo); up = rank_up(winobj); context = (rank_context *) @@ -413,6 +418,7 @@ window_ntile(PG_FUNCTION_ARGS) WindowObject winobj = PG_WINDOW_OBJECT(); ntile_context *context; + WinCheckAndInitializeNullTreatment(winobj, false, fcinfo); context = (ntile_context *) WinGetPartitionLocalMemory(winobj, sizeof(ntile_context)); @@ -535,6 +541,7 @@ leadlag_common(FunctionCallInfo fcinfo, bool isnull; bool isout; + WinCheckAndInitializeNullTreatment(winobj, true, fcinfo); if (withoffset) { offset = DatumGetInt32(WinGetFuncArgCurrent(winobj, 1, &isnull)); @@ -652,6 +659,7 @@ window_first_value(PG_FUNCTION_ARGS) Datum result; bool isnull; + WinCheckAndInitializeNullTreatment(winobj, true, fcinfo); result = WinGetFuncArgInFrame(winobj, 0, 0, WINDOW_SEEK_HEAD, true, &isnull, NULL); @@ -673,6 +681,7 @@ window_last_value(PG_FUNCTION_ARGS) Datum result; bool isnull; + WinCheckAndInitializeNullTreatment(winobj, true, fcinfo); result = WinGetFuncArgInFrame(winobj, 0, 0, WINDOW_SEEK_TAIL, true, &isnull, NULL); @@ -696,6 +705,7 @@ window_nth_value(PG_FUNCTION_ARGS) bool isnull; int32 nth; + WinCheckAndInitializeNullTreatment(winobj, true, fcinfo); nth = DatumGetInt32(WinGetFuncArgCurrent(winobj, 1, &isnull)); if (isnull) PG_RETURN_NULL(); diff --git a/src/backend/utils/adt/xid.c b/src/backend/utils/adt/xid.c index 3d0c48769cce8..f746a5f97dd0e 100644 --- a/src/backend/utils/adt/xid.c +++ b/src/backend/utils/adt/xid.c @@ -3,7 +3,7 @@ * xid.c * POSTGRES transaction identifier and command identifier datatypes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -45,7 +45,7 @@ xidout(PG_FUNCTION_ARGS) TransactionId transactionId = PG_GETARG_TRANSACTIONID(0); char *result = (char *) palloc(16); - snprintf(result, 16, "%lu", (unsigned long) transactionId); + snprintf(result, 16, "%u", transactionId); PG_RETURN_CSTRING(result); } @@ -362,7 +362,7 @@ cidout(PG_FUNCTION_ARGS) CommandId c = PG_GETARG_COMMANDID(0); char *result = (char *) palloc(16); - snprintf(result, 16, "%lu", (unsigned long) c); + snprintf(result, 16, "%u", c); PG_RETURN_CSTRING(result); } diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c index 1da3964ca6fb8..c607e78d9acd9 100644 --- a/src/backend/utils/adt/xid8funcs.c +++ b/src/backend/utils/adt/xid8funcs.c @@ -15,7 +15,7 @@ * to users. The txid_XXX variants should eventually be dropped. * * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * Author: Jan Wieck, Afilias USA INC. * 64-bit txids: Marko Kreen, Skype Technologies * @@ -39,6 +39,7 @@ #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/xid8.h" +#include "varatt.h" /* @@ -193,7 +194,7 @@ is_visible_fxid(FullTransactionId value, const pg_snapshot *snap) #ifdef USE_BSEARCH_IF_NXIP_GREATER else if (snap->nxip > USE_BSEARCH_IF_NXIP_GREATER) { - void *res; + const void *res; res = bsearch(&value, snap->xip, snap->nxip, sizeof(FullTransactionId), cmp_fxid); diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c index a4150bff2eaea..2c7f778cfdb7a 100644 --- a/src/backend/utils/adt/xml.c +++ b/src/backend/utils/adt/xml.c @@ -4,7 +4,7 @@ * XML data type support. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/utils/adt/xml.c @@ -84,7 +84,6 @@ #include "catalog/namespace.h" #include "catalog/pg_class.h" #include "catalog/pg_type.h" -#include "commands/dbcommands.h" #include "executor/spi.h" #include "executor/tablefunc.h" #include "fmgr.h" @@ -373,6 +372,7 @@ xml_recv(PG_FUNCTION_ARGS) #ifdef USE_LIBXML StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); xmltype *result; + const char *input; char *str; char *newstr; int nbytes; @@ -386,7 +386,7 @@ xml_recv(PG_FUNCTION_ARGS) * parse that before converting to server encoding. */ nbytes = buf->len - buf->cursor; - str = (char *) pq_getmsgbytes(buf, nbytes); + input = pq_getmsgbytes(buf, nbytes); /* * We need a null-terminated string to pass to parse_xml_decl(). Rather @@ -395,7 +395,7 @@ xml_recv(PG_FUNCTION_ARGS) */ result = palloc(nbytes + 1 + VARHDRSZ); SET_VARSIZE(result, nbytes + VARHDRSZ); - memcpy(VARDATA(result), str, nbytes); + memcpy(VARDATA(result), input, nbytes); str = VARDATA(result); str[nbytes] = '\0'; @@ -529,14 +529,36 @@ xmltext(PG_FUNCTION_ARGS) #ifdef USE_LIBXML text *arg = PG_GETARG_TEXT_PP(0); text *result; - xmlChar *xmlbuf = NULL; + xmlChar *volatile xmlbuf = NULL; + PgXmlErrorContext *xmlerrcxt; - xmlbuf = xmlEncodeSpecialChars(NULL, xml_text2xmlChar(arg)); + /* First we gotta spin up some error handling. */ + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); - Assert(xmlbuf); + PG_TRY(); + { + xmlbuf = xmlEncodeSpecialChars(NULL, xml_text2xmlChar(arg)); + + if (xmlbuf == NULL || xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate xmlChar"); + + result = cstring_to_text_with_len((const char *) xmlbuf, + xmlStrlen(xmlbuf)); + } + PG_CATCH(); + { + if (xmlbuf) + xmlFree(xmlbuf); + + pg_xml_done(xmlerrcxt, true); + PG_RE_THROW(); + } + PG_END_TRY(); - result = cstring_to_text_with_len((const char *) xmlbuf, xmlStrlen(xmlbuf)); xmlFree(xmlbuf); + pg_xml_done(xmlerrcxt, false); + PG_RETURN_XML_P(result); #else NO_XML_SUPPORT(); @@ -638,7 +660,7 @@ texttoxml(PG_FUNCTION_ARGS) { text *data = PG_GETARG_TEXT_PP(0); - PG_RETURN_XML_P(xmlparse(data, xmloption, true)); + PG_RETURN_XML_P(xmlparse(data, xmloption, true, fcinfo->context)); } @@ -663,7 +685,7 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) volatile xmlBufferPtr buf = NULL; volatile xmlSaveCtxtPtr ctxt = NULL; ErrorSaveContext escontext = {T_ErrorSaveContext}; - PgXmlErrorContext *xmlerrcxt; + PgXmlErrorContext *volatile xmlerrcxt = NULL; #endif if (xmloption_arg != XMLOPTION_DOCUMENT && !indent) @@ -704,13 +726,18 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) return (text *) data; } - /* Otherwise, we gotta spin up some error handling. */ - xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); - + /* + * Otherwise, we gotta spin up some error handling. Unlike most other + * routines in this module, we already have a libxml "doc" structure to + * free, so we need to call pg_xml_init() inside the PG_TRY and be + * prepared for it to fail (typically due to palloc OOM). + */ PG_TRY(); { size_t decl_len = 0; + xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); + /* The serialized data will go into this buffer. */ buf = xmlBufferCreate(); @@ -770,7 +797,10 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) if (oldroot != NULL) xmlFreeNode(oldroot); - xmlAddChildList(root, content_nodes); + if (xmlAddChildList(root, content_nodes) == NULL || + xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not append xml node list"); /* * We use this node to insert newlines in the dump. Note: in at @@ -838,10 +868,10 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) xmlSaveClose(ctxt); if (buf) xmlBufferFree(buf); - if (doc) - xmlFreeDoc(doc); + xmlFreeDoc(doc); - pg_xml_done(xmlerrcxt, true); + if (xmlerrcxt) + pg_xml_done(xmlerrcxt, true); PG_RE_THROW(); } @@ -862,8 +892,8 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent) xmltype * xmlelement(XmlExpr *xexpr, - Datum *named_argvalue, bool *named_argnull, - Datum *argvalue, bool *argnull) + const Datum *named_argvalue, const bool *named_argnull, + const Datum *argvalue, const bool *argnull) { #ifdef USE_LIBXML xmltype *result; @@ -931,7 +961,10 @@ xmlelement(XmlExpr *xexpr, xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, "could not allocate xmlTextWriter"); - xmlTextWriterStartElement(writer, (xmlChar *) xexpr->name); + if (xmlTextWriterStartElement(writer, (xmlChar *) xexpr->name) < 0 || + xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not start xml element"); forboth(arg, named_arg_strings, narg, xexpr->arg_names) { @@ -939,19 +972,30 @@ xmlelement(XmlExpr *xexpr, char *argname = strVal(lfirst(narg)); if (str) - xmlTextWriterWriteAttribute(writer, - (xmlChar *) argname, - (xmlChar *) str); + { + if (xmlTextWriterWriteAttribute(writer, + (xmlChar *) argname, + (xmlChar *) str) < 0 || + xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not write xml attribute"); + } } foreach(arg, arg_strings) { char *str = (char *) lfirst(arg); - xmlTextWriterWriteRaw(writer, (xmlChar *) str); + if (xmlTextWriterWriteRaw(writer, (xmlChar *) str) < 0 || + xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not write raw xml text"); } - xmlTextWriterEndElement(writer); + if (xmlTextWriterEndElement(writer) < 0 || + xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR, + "could not end xml element"); /* we MUST do this now to flush data out to the buffer ... */ xmlFreeTextWriter(writer); @@ -985,14 +1029,18 @@ xmlelement(XmlExpr *xexpr, xmltype * -xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace) +xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace, Node *escontext) { #ifdef USE_LIBXML xmlDocPtr doc; doc = xml_parse(data, xmloption_arg, preserve_whitespace, - GetDatabaseEncoding(), NULL, NULL, NULL); - xmlFreeDoc(doc); + GetDatabaseEncoding(), NULL, NULL, escontext); + if (doc) + xmlFreeDoc(doc); + + if (SOFT_ERROR_OCCURRED(escontext)) + return NULL; return (xmltype *) data; #else @@ -1212,7 +1260,7 @@ pg_xml_init(PgXmlStrictness strictness) pg_xml_init_library(); /* Create error handling context structure */ - errcxt = (PgXmlErrorContext *) palloc(sizeof(PgXmlErrorContext)); + errcxt = palloc_object(PgXmlErrorContext); errcxt->magic = ERRCXT_MAGIC; errcxt->strictness = strictness; errcxt->err_occurred = false; @@ -1725,7 +1773,7 @@ xml_doctype_in_content(const xmlChar *str) * xmloption_arg, but a DOCTYPE node in the input can force DOCUMENT mode). * * If parsed_nodes isn't NULL and we parse in CONTENT mode, the list - * of parsed nodes from the xmlParseInNodeContext call will be returned + * of parsed nodes from the xmlParseBalancedChunkMemory call will be returned * to *parsed_nodes. (It is caller's responsibility to free that.) * * Errors normally result in ereport(ERROR), but if escontext is an @@ -1751,6 +1799,7 @@ xml_parse(text *data, XmlOptionType xmloption_arg, PgXmlErrorContext *xmlerrcxt; volatile xmlParserCtxtPtr ctxt = NULL; volatile xmlDocPtr doc = NULL; + volatile int save_keep_blanks = -1; /* * This step looks annoyingly redundant, but we must do it to have a @@ -1778,7 +1827,6 @@ xml_parse(text *data, XmlOptionType xmloption_arg, PG_TRY(); { bool parse_as_document = false; - int options; int res_code; size_t count = 0; xmlChar *version = NULL; @@ -1809,18 +1857,6 @@ xml_parse(text *data, XmlOptionType xmloption_arg, parse_as_document = true; } - /* - * Select parse options. - * - * Note that here we try to apply DTD defaults (XML_PARSE_DTDATTR) - * according to SQL/XML:2008 GR 10.16.7.d: 'Default values defined by - * internal DTD are applied'. As for external DTDs, we try to support - * them too (see SQL/XML:2008 GR 10.16.7.e), but that doesn't really - * happen because xmlPgEntityLoader prevents it. - */ - options = XML_PARSE_NOENT | XML_PARSE_DTDATTR - | (preserve_whitespace ? 0 : XML_PARSE_NOBLANKS); - /* initialize output parameters */ if (parsed_xmloptiontype != NULL) *parsed_xmloptiontype = parse_as_document ? XMLOPTION_DOCUMENT : @@ -1830,11 +1866,26 @@ xml_parse(text *data, XmlOptionType xmloption_arg, if (parse_as_document) { + int options; + + /* set up parser context used by xmlCtxtReadDoc */ ctxt = xmlNewParserCtxt(); if (ctxt == NULL || xmlerrcxt->err_occurred) xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, "could not allocate parser context"); + /* + * Select parse options. + * + * Note that here we try to apply DTD defaults (XML_PARSE_DTDATTR) + * according to SQL/XML:2008 GR 10.16.7.d: 'Default values defined + * by internal DTD are applied'. As for external DTDs, we try to + * support them too (see SQL/XML:2008 GR 10.16.7.e), but that + * doesn't really happen because xmlPgEntityLoader prevents it. + */ + options = XML_PARSE_NOENT | XML_PARSE_DTDATTR + | (preserve_whitespace ? 0 : XML_PARSE_NOBLANKS); + doc = xmlCtxtReadDoc(ctxt, utf8string, NULL, /* no URL */ "UTF-8", @@ -1856,10 +1907,7 @@ xml_parse(text *data, XmlOptionType xmloption_arg, } else { - xmlNodePtr root; - xmlNodePtr oldroot PG_USED_FOR_ASSERTS_ONLY; - - /* set up document with empty root node to be the context node */ + /* set up document that xmlParseBalancedChunkMemory will add to */ doc = xmlNewDoc(version); if (doc == NULL || xmlerrcxt->err_occurred) xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, @@ -1872,43 +1920,22 @@ xml_parse(text *data, XmlOptionType xmloption_arg, "could not allocate XML document"); doc->standalone = standalone; - root = xmlNewNode(NULL, (const xmlChar *) "content-root"); - if (root == NULL || xmlerrcxt->err_occurred) - xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, - "could not allocate xml node"); - - /* - * This attaches root to doc, so we need not free it separately; - * and there can't yet be any old root to free. - */ - oldroot = xmlDocSetRootElement(doc, root); - Assert(oldroot == NULL); + /* set parse options --- have to do this the ugly way */ + save_keep_blanks = xmlKeepBlanksDefault(preserve_whitespace ? 1 : 0); /* allow empty content */ if (*(utf8string + count)) { - xmlNodePtr node_list = NULL; - xmlParserErrors res; - - res = xmlParseInNodeContext(root, - (char *) utf8string + count, - strlen((char *) utf8string + count), - options, - &node_list); - - if (res != XML_ERR_OK || xmlerrcxt->err_occurred) + res_code = xmlParseBalancedChunkMemory(doc, NULL, NULL, 0, + utf8string + count, + parsed_nodes); + if (res_code != 0 || xmlerrcxt->err_occurred) { - xmlFreeNodeList(node_list); xml_errsave(escontext, xmlerrcxt, ERRCODE_INVALID_XML_CONTENT, "invalid XML content"); goto fail; } - - if (parsed_nodes != NULL) - *parsed_nodes = node_list; - else - xmlFreeNodeList(node_list); } } @@ -1917,6 +1944,8 @@ xml_parse(text *data, XmlOptionType xmloption_arg, } PG_CATCH(); { + if (save_keep_blanks != -1) + xmlKeepBlanksDefault(save_keep_blanks); if (doc != NULL) xmlFreeDoc(doc); if (ctxt != NULL) @@ -1928,6 +1957,9 @@ xml_parse(text *data, XmlOptionType xmloption_arg, } PG_END_TRY(); + if (save_keep_blanks != -1) + xmlKeepBlanksDefault(save_keep_blanks); + if (ctxt != NULL) xmlFreeParserCtxt(ctxt); @@ -2106,7 +2138,7 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) node->type == XML_ELEMENT_NODE) ? node->name : NULL; int domain = error->domain; int level = error->level; - StringInfo errorBuf; + StringInfoData errorBuf; /* * Defend against someone passing us a bogus context struct. @@ -2159,7 +2191,7 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) if (error->code == XML_ERR_NOT_WELL_BALANCED && xmlerrcxt->err_occurred) return; - /* fall through */ + pg_fallthrough; case XML_FROM_NONE: case XML_FROM_MEMORY: @@ -2183,16 +2215,16 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) } /* Prepare error message in errorBuf */ - errorBuf = makeStringInfo(); + initStringInfo(&errorBuf); if (error->line > 0) - appendStringInfo(errorBuf, "line %d: ", error->line); + appendStringInfo(&errorBuf, "line %d: ", error->line); if (name != NULL) - appendStringInfo(errorBuf, "element %s: ", name); + appendStringInfo(&errorBuf, "element %s: ", name); if (error->message != NULL) - appendStringInfoString(errorBuf, error->message); + appendStringInfoString(&errorBuf, error->message); else - appendStringInfoString(errorBuf, "(no message provided)"); + appendStringInfoString(&errorBuf, "(no message provided)"); /* * Append context information to errorBuf. @@ -2210,11 +2242,11 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) xmlGenericErrorFunc errFuncSaved = xmlGenericError; void *errCtxSaved = xmlGenericErrorContext; - xmlSetGenericErrorFunc(errorBuf, + xmlSetGenericErrorFunc(&errorBuf, (xmlGenericErrorFunc) appendStringInfo); /* Add context information to errorBuf */ - appendStringInfoLineSeparator(errorBuf); + appendStringInfoLineSeparator(&errorBuf); xmlParserPrintFileContext(input); @@ -2223,7 +2255,7 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) } /* Get rid of any trailing newlines in errorBuf */ - chopStringInfoNewlines(errorBuf); + chopStringInfoNewlines(&errorBuf); /* * Legacy error handling mode. err_occurred is never set, we just add the @@ -2236,10 +2268,10 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) if (xmlerrcxt->strictness == PG_XML_STRICTNESS_LEGACY) { appendStringInfoLineSeparator(&xmlerrcxt->err_buf); - appendBinaryStringInfo(&xmlerrcxt->err_buf, errorBuf->data, - errorBuf->len); + appendBinaryStringInfo(&xmlerrcxt->err_buf, errorBuf.data, + errorBuf.len); - destroyStringInfo(errorBuf); + pfree(errorBuf.data); return; } @@ -2254,23 +2286,23 @@ xml_errorHandler(void *data, PgXmlErrorPtr error) if (level >= XML_ERR_ERROR) { appendStringInfoLineSeparator(&xmlerrcxt->err_buf); - appendBinaryStringInfo(&xmlerrcxt->err_buf, errorBuf->data, - errorBuf->len); + appendBinaryStringInfo(&xmlerrcxt->err_buf, errorBuf.data, + errorBuf.len); xmlerrcxt->err_occurred = true; } else if (level >= XML_ERR_WARNING) { ereport(WARNING, - (errmsg_internal("%s", errorBuf->data))); + (errmsg_internal("%s", errorBuf.data))); } else { ereport(NOTICE, - (errmsg_internal("%s", errorBuf->data))); + (errmsg_internal("%s", errorBuf.data))); } - destroyStringInfo(errorBuf); + pfree(errorBuf.data); } @@ -2349,8 +2381,7 @@ sqlchar_to_unicode(const char *s) char *utf8string; pg_wchar ret[2]; /* need space for trailing zero */ - /* note we're not assuming s is null-terminated */ - utf8string = pg_server_to_any(s, pg_mblen(s), PG_UTF8); + utf8string = pg_server_to_any(s, pg_mblen_cstr(s), PG_UTF8); pg_encoding_mb2wchar_with_len(PG_UTF8, utf8string, ret, pg_encoding_mblen(PG_UTF8, utf8string)); @@ -2403,7 +2434,7 @@ map_sql_identifier_to_xml_name(const char *ident, bool fully_escaped, initStringInfo(&buf); - for (p = ident; *p; p += pg_mblen(p)) + for (p = ident; *p; p += pg_mblen_cstr(p)) { if (*p == ':' && (p == ident || fully_escaped)) appendStringInfoString(&buf, "_x003A_"); @@ -2428,7 +2459,7 @@ map_sql_identifier_to_xml_name(const char *ident, bool fully_escaped, : !is_valid_xml_namechar(u)) appendStringInfo(&buf, "_x%04X_", (unsigned int) u); else - appendBinaryStringInfo(&buf, p, pg_mblen(p)); + appendBinaryStringInfo(&buf, p, pg_mblen_cstr(p)); } } @@ -2451,7 +2482,7 @@ map_xml_name_to_sql_identifier(const char *name) initStringInfo(&buf); - for (p = name; *p; p += pg_mblen(p)) + for (p = name; *p; p += pg_mblen_cstr(p)) { if (*p == '_' && *(p + 1) == 'x' && isxdigit((unsigned char) *(p + 2)) @@ -2469,7 +2500,7 @@ map_xml_name_to_sql_identifier(const char *name) p += 6; } else - appendBinaryStringInfo(&buf, p, pg_mblen(p)); + appendBinaryStringInfo(&buf, p, pg_mblen_cstr(p)); } return buf.data; @@ -4220,20 +4251,27 @@ xml_xmlnodetoxmltype(xmlNodePtr cur, PgXmlErrorContext *xmlerrcxt) } else { - xmlChar *str; + xmlChar *volatile str = NULL; - str = xmlXPathCastNodeToString(cur); PG_TRY(); { + char *escaped; + + str = xmlXPathCastNodeToString(cur); + if (str == NULL || xmlerrcxt->err_occurred) + xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY, + "could not allocate xmlChar"); + /* Here we rely on XML having the same representation as TEXT */ - char *escaped = escape_xml((char *) str); + escaped = escape_xml((char *) str); result = (xmltype *) cstring_to_text(escaped); pfree(escaped); } PG_FINALLY(); { - xmlFree(str); + if (str) + xmlFree(str); } PG_END_TRY(); } @@ -4699,10 +4737,10 @@ XmlTableInitOpaque(TableFuncScanState *state, int natts) XmlTableBuilderData *xtCxt; PgXmlErrorContext *xmlerrcxt; - xtCxt = palloc0(sizeof(XmlTableBuilderData)); + xtCxt = palloc0_object(XmlTableBuilderData); xtCxt->magic = XMLTABLE_CONTEXT_MAGIC; xtCxt->natts = natts; - xtCxt->xpathscomp = palloc0(sizeof(xmlXPathCompExprPtr) * natts); + xtCxt->xpathscomp = palloc0_array(xmlXPathCompExprPtr, natts); xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL); @@ -4861,7 +4899,7 @@ XmlTableSetColumnFilter(TableFuncScanState *state, const char *path, int colnum) XmlTableBuilderData *xtCxt; xmlChar *xstr; - Assert(PointerIsValid(path)); + Assert(path); xtCxt = GetXmlTableBuilderPrivateData(state, "XmlTableSetColumnFilter"); diff --git a/src/backend/utils/cache/attoptcache.c b/src/backend/utils/cache/attoptcache.c index 5c8360c08b5f8..9244a23013e23 100644 --- a/src/backend/utils/cache/attoptcache.c +++ b/src/backend/utils/cache/attoptcache.c @@ -6,7 +6,7 @@ * Attribute options are cached separately from the fixed-size portion of * pg_attribute entries, which are handled by the relcache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -50,7 +50,8 @@ typedef struct * for that attribute. */ static void -InvalidateAttoptCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +InvalidateAttoptCacheCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { HASH_SEQ_STATUS status; AttoptCacheEntry *attopt; @@ -86,7 +87,7 @@ relatt_cache_syshash(const void *key, Size keysize) const AttoptCacheKey *ckey = key; Assert(keysize == sizeof(*ckey)); - return GetSysCacheHashValue2(ATTNUM, ckey->attrelid, ckey->attnum); + return GetSysCacheHashValue2(ATTNUM, ObjectIdGetDatum(ckey->attrelid), Int32GetDatum(ckey->attnum)); } /* diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 657648996c235..a8e7bf649d23f 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -3,7 +3,7 @@ * catcache.c * System catalog cache for tuples matching a key. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -117,10 +117,10 @@ static CatCTup *CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, static void ReleaseCatCacheWithOwner(HeapTuple tuple, ResourceOwner resowner); static void ReleaseCatCacheListWithOwner(CatCList *list, ResourceOwner resowner); -static void CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, - Datum *keys); -static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, - Datum *srckeys, Datum *dstkeys); +static void CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, const int *attnos, + const Datum *keys); +static void CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, const int *attnos, + const Datum *srckeys, Datum *dstkeys); /* @@ -205,6 +205,10 @@ nameeqfast(Datum a, Datum b) char *ca = NameStr(*DatumGetName(a)); char *cb = NameStr(*DatumGetName(b)); + /* + * Catalogs only use deterministic collations, so ignore column collation + * and use fast path. + */ return strncmp(ca, cb, NAMEDATALEN) == 0; } @@ -213,7 +217,11 @@ namehashfast(Datum datum) { char *key = NameStr(*DatumGetName(datum)); - return hash_any((unsigned char *) key, strlen(key)); + /* + * Catalogs only use deterministic collations, so ignore column collation + * and use fast path. + */ + return hash_bytes((unsigned char *) key, strlen(key)); } static bool @@ -244,17 +252,20 @@ static bool texteqfast(Datum a, Datum b) { /* - * The use of DEFAULT_COLLATION_OID is fairly arbitrary here. We just - * want to take the fast "deterministic" path in texteq(). + * Catalogs only use deterministic collations, so ignore column collation + * and use "C" locale for efficiency. */ - return DatumGetBool(DirectFunctionCall2Coll(texteq, DEFAULT_COLLATION_OID, a, b)); + return DatumGetBool(DirectFunctionCall2Coll(texteq, C_COLLATION_OID, a, b)); } static uint32 texthashfast(Datum datum) { - /* analogously here as in texteqfast() */ - return DatumGetInt32(DirectFunctionCall1Coll(hashtext, DEFAULT_COLLATION_OID, datum)); + /* + * Catalogs only use deterministic collations, so ignore column collation + * and use "C" locale for efficiency. + */ + return DatumGetInt32(DirectFunctionCall1Coll(hashtext, C_COLLATION_OID, datum)); } static bool @@ -317,6 +328,7 @@ GetCCHashEqFuncs(Oid keytype, CCHashFN *hashfunc, RegProcedure *eqfunc, CCFastEq case REGDICTIONARYOID: case REGROLEOID: case REGNAMESPACEOID: + case REGDATABASEOID: *hashfunc = int4hashfast; *fasteqfunc = int4eqfast; *eqfunc = F_OIDEQ; @@ -356,15 +368,15 @@ CatalogCacheComputeHashValue(CatCache *cache, int nkeys, case 4: oneHash = (cc_hashfunc[3]) (v4); hashValue ^= pg_rotate_left32(oneHash, 24); - /* FALLTHROUGH */ + pg_fallthrough; case 3: oneHash = (cc_hashfunc[2]) (v3); hashValue ^= pg_rotate_left32(oneHash, 16); - /* FALLTHROUGH */ + pg_fallthrough; case 2: oneHash = (cc_hashfunc[1]) (v2); hashValue ^= pg_rotate_left32(oneHash, 8); - /* FALLTHROUGH */ + pg_fallthrough; case 1: oneHash = (cc_hashfunc[0]) (v1); hashValue ^= oneHash; @@ -402,21 +414,21 @@ CatalogCacheComputeTupleHashValue(CatCache *cache, int nkeys, HeapTuple tuple) cc_tupdesc, &isNull); Assert(!isNull); - /* FALLTHROUGH */ + pg_fallthrough; case 3: v3 = fastgetattr(tuple, cc_keyno[2], cc_tupdesc, &isNull); Assert(!isNull); - /* FALLTHROUGH */ + pg_fallthrough; case 2: v2 = fastgetattr(tuple, cc_keyno[1], cc_tupdesc, &isNull); Assert(!isNull); - /* FALLTHROUGH */ + pg_fallthrough; case 1: v1 = fastgetattr(tuple, cc_keyno[0], @@ -460,14 +472,14 @@ static void CatCachePrintStats(int code, Datum arg) { slist_iter iter; - long cc_searches = 0; - long cc_hits = 0; - long cc_neg_hits = 0; - long cc_newloads = 0; - long cc_invals = 0; - long cc_nlists = 0; - long cc_lsearches = 0; - long cc_lhits = 0; + uint64 cc_searches = 0; + uint64 cc_hits = 0; + uint64 cc_neg_hits = 0; + uint64 cc_newloads = 0; + uint64 cc_invals = 0; + uint64 cc_nlists = 0; + uint64 cc_lsearches = 0; + uint64 cc_lhits = 0; slist_foreach(iter, &CacheHdr->ch_caches) { @@ -475,7 +487,10 @@ CatCachePrintStats(int code, Datum arg) if (cache->cc_ntup == 0 && cache->cc_searches == 0) continue; /* don't print unused caches */ - elog(DEBUG2, "catcache %s/%u: %d tup, %ld srch, %ld+%ld=%ld hits, %ld+%ld=%ld loads, %ld invals, %d lists, %ld lsrch, %ld lhits", + elog(DEBUG2, "catcache %s/%u: %d tup, %" PRIu64 " srch, %" PRIu64 "+%" + PRIu64 "=%" PRIu64 " hits, %" PRIu64 "+%" PRIu64 "=%" + PRIu64 " loads, %" PRIu64 " invals, %d lists, %" PRIu64 + " lsrch, %" PRIu64 " lhits", cache->cc_relname, cache->cc_indexoid, cache->cc_ntup, @@ -499,7 +514,10 @@ CatCachePrintStats(int code, Datum arg) cc_lsearches += cache->cc_lsearches; cc_lhits += cache->cc_lhits; } - elog(DEBUG2, "catcache totals: %d tup, %ld srch, %ld+%ld=%ld hits, %ld+%ld=%ld loads, %ld invals, %ld lists, %ld lsrch, %ld lhits", + elog(DEBUG2, "catcache totals: %d tup, %" PRIu64 " srch, %" PRIu64 "+%" + PRIu64 "=%" PRIu64 " hits, %" PRIu64 "+%" PRIu64 "=%" PRIu64 + " loads, %" PRIu64 " invals, %" PRIu64 " lists, %" PRIu64 + " lsrch, %" PRIu64 " lhits", CacheHdr->ch_ntup, cc_searches, cc_hits, @@ -828,7 +846,7 @@ ResetCatalogCachesExt(bool debug_discard) * kinds of trouble if a cache flush occurs while loading cache entries. * We now avoid the need to do it by copying cc_tupdesc out of the relcache, * rather than relying on the relcache to keep a tupdesc for us. Of course - * this assumes the tupdesc of a cachable system table will not change...) + * this assumes the tupdesc of a cacheable system table will not change...) */ void CatalogCacheFlushCatalog(Oid catId) @@ -913,7 +931,7 @@ InitCatCache(int id, */ if (CacheHdr == NULL) { - CacheHdr = (CatCacheHeader *) palloc(sizeof(CatCacheHeader)); + CacheHdr = palloc_object(CatCacheHeader); slist_init(&CacheHdr->ch_caches); CacheHdr->ch_ntup = 0; #ifdef CATCACHE_STATS @@ -1006,7 +1024,14 @@ RehashCatCache(CatCache *cp) int hashIndex = HASH_INDEX(ct->hash_value, newnbuckets); dlist_delete(iter.cur); - dlist_push_head(&newbucket[hashIndex], &ct->cache_elem); + + /* + * Note that each item is pushed at the tail of the new bucket, + * not its head. This is consistent with the SearchCatCache*() + * routines, where matching entries are moved at the front of the + * list to speed subsequent searches. + */ + dlist_push_tail(&newbucket[hashIndex], &ct->cache_elem); } } @@ -1044,7 +1069,14 @@ RehashCatCacheLists(CatCache *cp) int hashIndex = HASH_INDEX(cl->hash_value, newnbuckets); dlist_delete(iter.cur); - dlist_push_head(&newbucket[hashIndex], &cl->cache_elem); + + /* + * Note that each item is pushed at the tail of the new bucket, + * not its head. This is consistent with the SearchCatCache*() + * routines, where matching entries are moved at the front of the + * list to speed subsequent searches. + */ + dlist_push_tail(&newbucket[hashIndex], &cl->cache_elem); } } @@ -1661,7 +1693,7 @@ ReleaseCatCacheWithOwner(HeapTuple tuple, ResourceOwner resowner) ct->refcount--; if (resowner) - ResourceOwnerForgetCatCacheRef(CurrentResourceOwner, &ct->tuple); + ResourceOwnerForgetCatCacheRef(resowner, &ct->tuple); if ( #ifndef CATCACHE_FORCE_RELEASE @@ -2103,7 +2135,7 @@ ReleaseCatCacheListWithOwner(CatCList *list, ResourceOwner resowner) Assert(list->refcount > 0); list->refcount--; if (resowner) - ResourceOwnerForgetCatCacheListRef(CurrentResourceOwner, list); + ResourceOwnerForgetCatCacheListRef(resowner, list); if ( #ifndef CATCACHE_FORCE_RELEASE @@ -2200,20 +2232,18 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, dtp = ntp; /* Allocate memory for CatCTup and the cached tuple in one go */ - oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - - ct = (CatCTup *) palloc(sizeof(CatCTup) + - MAXIMUM_ALIGNOF + dtp->t_len); + ct = (CatCTup *) + MemoryContextAlloc(CacheMemoryContext, + MAXALIGN(sizeof(CatCTup)) + dtp->t_len); ct->tuple.t_len = dtp->t_len; ct->tuple.t_self = dtp->t_self; ct->tuple.t_tableOid = dtp->t_tableOid; ct->tuple.t_data = (HeapTupleHeader) - MAXALIGN(((char *) ct) + sizeof(CatCTup)); + (((char *) ct) + MAXALIGN(sizeof(CatCTup))); /* copy tuple contents */ memcpy((char *) ct->tuple.t_data, (const char *) dtp->t_data, dtp->t_len); - MemoryContextSwitchTo(oldcxt); if (dtp != ntp) heap_freetuple(dtp); @@ -2236,7 +2266,7 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, { /* Set up keys for a negative cache entry */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - ct = (CatCTup *) palloc(sizeof(CatCTup)); + ct = palloc_object(CatCTup); /* * Store keys - they'll point into separately allocated memory if not @@ -2278,21 +2308,18 @@ CatalogCacheCreateEntry(CatCache *cache, HeapTuple ntp, Datum *arguments, * Helper routine that frees keys stored in the keys array. */ static void -CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys) +CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, const int *attnos, const Datum *keys) { int i; for (i = 0; i < nkeys; i++) { int attnum = attnos[i]; - Form_pg_attribute att; /* system attribute are not supported in caches */ Assert(attnum > 0); - att = TupleDescAttr(tupdesc, attnum - 1); - - if (!att->attbyval) + if (!TupleDescCompactAttr(tupdesc, attnum - 1)->attbyval) pfree(DatumGetPointer(keys[i])); } } @@ -2303,8 +2330,8 @@ CatCacheFreeKeys(TupleDesc tupdesc, int nkeys, int *attnos, Datum *keys) * context. */ static void -CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, int *attnos, - Datum *srckeys, Datum *dstkeys) +CatCacheCopyKeys(TupleDesc tupdesc, int nkeys, const int *attnos, + const Datum *srckeys, Datum *dstkeys) { int i; @@ -2389,7 +2416,7 @@ PrepareToInvalidateCacheTuple(Relation relation, */ Assert(RelationIsValid(relation)); Assert(HeapTupleIsValid(tuple)); - Assert(PointerIsValid(function)); + Assert(function); Assert(CacheHdr != NULL); reloid = RelationGetRelid(relation); diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c index ce596bf563856..3fe89c9c98fc9 100644 --- a/src/backend/utils/cache/evtcache.c +++ b/src/backend/utils/cache/evtcache.c @@ -3,7 +3,7 @@ * evtcache.c * Special-purpose cache for event trigger data. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -49,7 +49,8 @@ static EventTriggerCacheStateType EventTriggerCacheState = ETCS_NEEDS_REBUILD; static void BuildEventTriggerCache(void); static void InvalidateEventCacheCallback(Datum arg, - int cacheid, uint32 hashvalue); + SysCacheIdentifier cacheid, + uint32 hashvalue); static Bitmapset *DecodeTextArrayToBitmapset(Datum array); /* @@ -78,7 +79,6 @@ BuildEventTriggerCache(void) { HASHCTL ctl; HTAB *cache; - MemoryContext oldcontext; Relation rel; Relation irel; SysScanDesc scan; @@ -110,9 +110,6 @@ BuildEventTriggerCache(void) (Datum) 0); } - /* Switch to correct memory context. */ - oldcontext = MemoryContextSwitchTo(EventTriggerCacheContext); - /* Prevent the memory context from being nuked while we're rebuilding. */ EventTriggerCacheState = ETCS_REBUILD_STARTED; @@ -145,6 +142,7 @@ BuildEventTriggerCache(void) bool evttags_isnull; EventTriggerCacheEntry *entry; bool found; + MemoryContext oldcontext; /* Get next tuple. */ tup = systable_getnext_ordered(scan, ForwardScanDirection); @@ -171,8 +169,11 @@ BuildEventTriggerCache(void) else continue; + /* Switch to correct memory context. */ + oldcontext = MemoryContextSwitchTo(EventTriggerCacheContext); + /* Allocate new cache item. */ - item = palloc0(sizeof(EventTriggerCacheItem)); + item = palloc0_object(EventTriggerCacheItem); item->fnoid = form->evtfoid; item->enabled = form->evtenabled; @@ -188,6 +189,9 @@ BuildEventTriggerCache(void) entry->triggerlist = lappend(entry->triggerlist, item); else entry->triggerlist = list_make1(item); + + /* Restore previous memory context. */ + MemoryContextSwitchTo(oldcontext); } /* Done with pg_event_trigger scan. */ @@ -195,9 +199,6 @@ BuildEventTriggerCache(void) index_close(irel, AccessShareLock); relation_close(rel, AccessShareLock); - /* Restore previous memory context. */ - MemoryContextSwitchTo(oldcontext); - /* Install new cache. */ EventTriggerCache = cache; @@ -240,6 +241,8 @@ DecodeTextArrayToBitmapset(Datum array) } pfree(elems); + if (arr != DatumGetPointer(array)) + pfree(arr); return bms; } @@ -252,7 +255,8 @@ DecodeTextArrayToBitmapset(Datum array) * memory leaks. */ static void -InvalidateEventCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +InvalidateEventCacheCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { /* * If the cache isn't valid, then there might be a rebuild in progress, so diff --git a/src/backend/utils/cache/funccache.c b/src/backend/utils/cache/funccache.c index 150c502a6121b..701c294b88d95 100644 --- a/src/backend/utils/cache/funccache.c +++ b/src/backend/utils/cache/funccache.c @@ -13,7 +13,7 @@ * function call will be dealing with. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -491,6 +491,7 @@ cached_function_compile(FunctionCallInfo fcinfo, CachedFunctionHashKey hashkey; bool function_valid = false; bool hashkey_valid = false; + bool new_function = false; /* * Lookup the pg_proc tuple by Oid; we'll need it in any case @@ -570,13 +571,15 @@ cached_function_compile(FunctionCallInfo fcinfo, /* * Create the new function struct, if not done already. The function - * structs are never thrown away, so keep them in TopMemoryContext. + * cache entry will be kept for the life of the backend, so put it in + * TopMemoryContext. */ Assert(cacheEntrySize >= sizeof(CachedFunction)); if (function == NULL) { function = (CachedFunction *) MemoryContextAllocZero(TopMemoryContext, cacheEntrySize); + new_function = true; } else { @@ -585,17 +588,36 @@ cached_function_compile(FunctionCallInfo fcinfo, } /* - * Fill in the CachedFunction part. fn_hashkey and use_count remain - * zeroes for now. + * However, if function compilation fails, we'd like not to leak the + * function struct, so use a PG_TRY block to prevent that. (It's up + * to the compile callback function to avoid its own internal leakage + * in such cases.) Unfortunately, freeing the struct is only safe if + * we just allocated it: otherwise there are probably fn_extra + * pointers to it. */ - function->fn_xmin = HeapTupleHeaderGetRawXmin(procTup->t_data); - function->fn_tid = procTup->t_self; - function->dcallback = dcallback; + PG_TRY(); + { + /* + * Do the hard, language-specific part. + */ + ccallback(fcinfo, procTup, &hashkey, function, forValidator); + } + PG_CATCH(); + { + if (new_function) + pfree(function); + PG_RE_THROW(); + } + PG_END_TRY(); /* - * Do the hard, language-specific part. + * Fill in the CachedFunction part. (We do this last to prevent the + * function from looking valid before it's fully built.) fn_hashkey + * will be set by cfunc_hashtable_insert; use_count remains zero. */ - ccallback(fcinfo, procTup, &hashkey, function, forValidator); + function->fn_xmin = HeapTupleHeaderGetRawXmin(procTup->t_data); + function->fn_tid = procTup->t_self; + function->dcallback = dcallback; /* * Add the completed struct to the hash table. diff --git a/src/backend/utils/cache/inval.c b/src/backend/utils/cache/inval.c index 02505c88b8e4c..d59216b28f16b 100644 --- a/src/backend/utils/cache/inval.c +++ b/src/backend/utils/cache/inval.c @@ -98,11 +98,11 @@ * likewise send the invalidation immediately, before ending the change's * critical section. This includes inplace heap updates, relmap, and smgr. * - * When wal_level=logical, write invalidations into WAL at each command end to - * support the decoding of the in-progress transactions. See - * CommandEndInvalidationMessages. + * When effective_wal_level is 'logical', write invalidations into WAL at + * each command end to support the decoding of the in-progress transactions. + * See CommandEndInvalidationMessages. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -758,7 +758,7 @@ PrepareInplaceInvalidationState(void) Assert(inplaceInvalInfo == NULL); /* gone after WAL insertion CritSection ends, so use current context */ - myInfo = (InvalidationInfo *) palloc0(sizeof(InvalidationInfo)); + myInfo = palloc0_object(InvalidationInfo); /* Stash our messages past end of the transactional messages, if any. */ if (transInvalInfo != NULL) @@ -1419,7 +1419,7 @@ CommandEndInvalidationMessages(void) ProcessInvalidationMessages(&transInvalInfo->ii.CurrentCmdInvalidMsgs, LocalExecuteInvalidationMessage); - /* WAL Log per-command invalidation messages for wal_level=logical */ + /* WAL Log per-command invalidation messages for logical decoding */ if (XLogLogicalInfoActive()) LogLogicalInvalidations(); @@ -1480,7 +1480,7 @@ CacheInvalidateHeapTupleCommon(Relation relation, else PrepareToInvalidateCacheTuple(relation, tuple, newtuple, RegisterCatcacheInvalidation, - (void *) info); + info); /* * Now, is this tuple one of the primary definers of a relcache entry? See @@ -1583,13 +1583,17 @@ CacheInvalidateHeapTuple(Relation relation, * implied. * * Like CacheInvalidateHeapTuple(), but for inplace updates. + * + * Just before and just after the inplace update, the tuple's cache keys must + * match those in key_equivalent_tuple. Cache keys consist of catcache lookup + * key columns and columns referencing pg_class.oid values, + * e.g. pg_constraint.conrelid, which would trigger relcache inval. */ void CacheInvalidateHeapTupleInplace(Relation relation, - HeapTuple tuple, - HeapTuple newtuple) + HeapTuple key_equivalent_tuple) { - CacheInvalidateHeapTupleCommon(relation, tuple, newtuple, + CacheInvalidateHeapTupleCommon(relation, key_equivalent_tuple, NULL, PrepareInplaceInvalidationState); } @@ -1753,7 +1757,7 @@ CacheInvalidateSmgr(RelFileLocatorBackend rlocator) SharedInvalidationMessage msg; /* verify optimization stated above stays valid */ - StaticAssertStmt(MAX_BACKENDS_BITS <= 23, + StaticAssertDecl(MAX_BACKENDS_BITS <= 23, "MAX_BACKENDS_BITS is too big for inval.c"); msg.sm.id = SHAREDINVALSMGR_ID; @@ -1809,7 +1813,7 @@ CacheInvalidateRelmap(Oid databaseId) * flush all cached state anyway. */ void -CacheRegisterSyscacheCallback(int cacheid, +CacheRegisterSyscacheCallback(SysCacheIdentifier cacheid, SyscacheCallbackFunction func, Datum arg) { @@ -1891,7 +1895,7 @@ CacheRegisterRelSyncCallback(RelSyncCallbackFunction func, * this module from knowing which catcache IDs correspond to which catalogs. */ void -CallSyscacheCallbacks(int cacheid, uint32 hashvalue) +CallSyscacheCallbacks(SysCacheIdentifier cacheid, uint32 hashvalue) { int i; diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index c460a72b75d90..1574f0c68c121 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -3,7 +3,7 @@ * lsyscache.c * Convenience routines for common queries in the system catalog cache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -26,6 +26,7 @@ #include "catalog/pg_class.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" +#include "catalog/pg_database.h" #include "catalog/pg_index.h" #include "catalog/pg_language.h" #include "catalog/pg_namespace.h" @@ -33,6 +34,8 @@ #include "catalog/pg_opfamily.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" +#include "catalog/pg_propgraph_label.h" +#include "catalog/pg_propgraph_property.h" #include "catalog/pg_publication.h" #include "catalog/pg_range.h" #include "catalog/pg_statistic.h" @@ -230,14 +233,7 @@ get_opmethod_canorder(Oid amoid) case BRIN_AM_OID: return false; default: - { - bool result; - IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(amoid, false); - - result = amroutine->amcanorder; - pfree(amroutine); - return result; - } + return GetIndexAmRoutineByAmId(amoid, false)->amcanorder; } } @@ -691,7 +687,7 @@ get_op_index_interpretation(Oid opno) if (!get_opmethod_canorder(op_form->amopmethod)) continue; - /* Get the operator's comparision type */ + /* Get the operator's comparison type */ cmptype = IndexAmTranslateStrategy(op_form->amopstrategy, op_form->amopmethod, op_form->amopfamily, @@ -701,8 +697,7 @@ get_op_index_interpretation(Oid opno) if (cmptype == COMPARE_INVALID) continue; - thisresult = (OpIndexInterpretation *) - palloc(sizeof(OpIndexInterpretation)); + thisresult = palloc_object(OpIndexInterpretation); thisresult->opfamily_id = op_form->amopfamily; thisresult->cmptype = cmptype; thisresult->oplefttype = op_form->amoplefttype; @@ -729,14 +724,14 @@ get_op_index_interpretation(Oid opno) { HeapTuple op_tuple = &catlist->members[i]->tuple; Form_pg_amop op_form = (Form_pg_amop) GETSTRUCT(op_tuple); - IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false); + const IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false); CompareType cmptype; /* must be ordering index */ if (!amroutine->amcanorder) continue; - /* Get the operator's comparision type */ + /* Get the operator's comparison type */ cmptype = IndexAmTranslateStrategy(op_form->amopstrategy, op_form->amopmethod, op_form->amopfamily, @@ -747,8 +742,7 @@ get_op_index_interpretation(Oid opno) continue; /* OK, report it as COMPARE_NE */ - thisresult = (OpIndexInterpretation *) - palloc(sizeof(OpIndexInterpretation)); + thisresult = palloc_object(OpIndexInterpretation); thisresult->opfamily_id = op_form->amopfamily; thisresult->cmptype = COMPARE_NE; thisresult->oplefttype = op_form->amoplefttype; @@ -769,9 +763,9 @@ get_op_index_interpretation(Oid opno) * semantics. * * This is trivially true if they are the same operator. Otherwise, - * Otherwise, we look to see if they both belong to an opfamily that - * guarantees compatible semantics for equality. Either finding allows us to - * assume that they have compatible notions of equality. (The reason we need + * we look to see if they both belong to an opfamily that guarantees + * compatible semantics for equality. Either finding allows us to assume + * that they have compatible notions of equality. (The reason we need * to do these pushups is that one might be a cross-type operator; for * instance int24eq vs int4eq.) */ @@ -801,15 +795,11 @@ equality_ops_are_compatible(Oid opno1, Oid opno2) * op_in_opfamily() is cheaper than GetIndexAmRoutineByAmId(), so * check it first */ - if (op_in_opfamily(opno2, op_form->amopfamily)) + if (op_in_opfamily(opno2, op_form->amopfamily) && + GetIndexAmRoutineByAmId(op_form->amopmethod, false)->amconsistentequality) { - IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false); - - if (amroutine->amconsistentequality) - { - result = true; - break; - } + result = true; + break; } } @@ -857,15 +847,52 @@ comparison_ops_are_compatible(Oid opno1, Oid opno2) * op_in_opfamily() is cheaper than GetIndexAmRoutineByAmId(), so * check it first */ - if (op_in_opfamily(opno2, op_form->amopfamily)) + if (op_in_opfamily(opno2, op_form->amopfamily) && + GetIndexAmRoutineByAmId(op_form->amopmethod, false)->amconsistentordering) { - IndexAmRoutine *amroutine = GetIndexAmRoutineByAmId(op_form->amopmethod, false); + result = true; + break; + } + } - if (amroutine->amconsistentordering) - { - result = true; - break; - } + ReleaseSysCacheList(catlist); + + return result; +} + +/* + * op_is_safe_index_member + * Check if the operator is a member of a B-tree or Hash operator family. + * + * We use this check as a proxy for "null-safety": if an operator is trusted by + * the btree or hash opfamily, it implies that the operator adheres to standard + * boolean behavior, and would not return NULL when given valid non-null + * inputs, as doing so would break index integrity. + */ +bool +op_is_safe_index_member(Oid opno) +{ + bool result = false; + CatCList *catlist; + int i; + + /* + * Search pg_amop to see if the target operator is registered for any + * btree or hash opfamily. + */ + catlist = SearchSysCacheList1(AMOPOPID, ObjectIdGetDatum(opno)); + + for (i = 0; i < catlist->n_members; i++) + { + HeapTuple tuple = &catlist->members[i]->tuple; + Form_pg_amop aform = (Form_pg_amop) GETSTRUCT(tuple); + + /* Check if the AM is B-tree or Hash */ + if (aform->amopmethod == BTREE_AM_OID || + aform->amopmethod == HASH_AM_OID) + { + result = true; + break; } } @@ -1247,6 +1274,32 @@ get_constraint_type(Oid conoid) return contype; } +/* ---------- DATABASE CACHE ---------- */ + +/* + * get_database_name - given a database OID, look up the name + * + * Returns a palloc'd string, or NULL if no such database. + */ +char * +get_database_name(Oid dbid) +{ + HeapTuple dbtuple; + char *result; + + dbtuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid)); + if (HeapTupleIsValid(dbtuple)) + { + result = pstrdup(NameStr(((Form_pg_database) GETSTRUCT(dbtuple))->datname)); + ReleaseSysCache(dbtuple); + } + else + result = NULL; + + return result; +} + + /* ---------- LANGUAGE CACHE ---------- */ char * @@ -2482,6 +2535,7 @@ get_type_io_data(Oid typid, { Oid typinput; Oid typoutput; + Oid typcollation; boot_get_type_io_data(typid, typlen, @@ -2490,7 +2544,8 @@ get_type_io_data(Oid typid, typdelim, typioparam, &typinput, - &typoutput); + &typoutput, + &typcollation); switch (which_func) { case IOFunc_input: @@ -3588,6 +3643,31 @@ get_range_collation(Oid rangeOid) return InvalidOid; } +/* + * get_range_constructor2 + * Gets the 2-arg constructor for the given rangetype. + * + * Raises an error if not found. + */ +RegProcedure +get_range_constructor2(Oid rangeOid) +{ + HeapTuple tp; + + tp = SearchSysCache1(RANGETYPE, ObjectIdGetDatum(rangeOid)); + if (HeapTupleIsValid(tp)) + { + Form_pg_range rngtup = (Form_pg_range) GETSTRUCT(tp); + RegProcedure result; + + result = rngtup->rngconstruct2; + ReleaseSysCache(tp); + return result; + } + else + elog(ERROR, "cache lookup failed for range type %u", rangeOid); +} + /* * get_range_multirange * Returns the multirange type of a given range type @@ -3817,7 +3897,7 @@ get_subscription_oid(const char *subname, bool missing_ok) Oid oid; oid = GetSysCacheOid2(SUBSCRIPTIONNAME, Anum_pg_subscription_oid, - MyDatabaseId, CStringGetDatum(subname)); + ObjectIdGetDatum(MyDatabaseId), CStringGetDatum(subname)); if (!OidIsValid(oid) && !missing_ok) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), @@ -3854,3 +3934,39 @@ get_subscription_name(Oid subid, bool missing_ok) return subname; } + +char * +get_propgraph_label_name(Oid labeloid) +{ + HeapTuple tuple; + char *labelname; + + tuple = SearchSysCache1(PROPGRAPHLABELOID, ObjectIdGetDatum(labeloid)); + if (!tuple) + { + elog(ERROR, "cache lookup failed for label %u", labeloid); + return NULL; + } + labelname = pstrdup(NameStr(((Form_pg_propgraph_label) GETSTRUCT(tuple))->pgllabel)); + ReleaseSysCache(tuple); + + return labelname; +} + +char * +get_propgraph_property_name(Oid propoid) +{ + HeapTuple tuple; + char *propname; + + tuple = SearchSysCache1(PROPGRAPHPROPOID, ObjectIdGetDatum(propoid)); + if (!tuple) + { + elog(ERROR, "cache lookup failed for property %u", propoid); + return NULL; + } + propname = pstrdup(NameStr(((Form_pg_propgraph_property) GETSTRUCT(tuple))->pgpname)); + ReleaseSysCache(tuple); + + return propname; +} diff --git a/src/backend/utils/cache/meson.build b/src/backend/utils/cache/meson.build index a1784dce5855b..a4435e0c3c634 100644 --- a/src/backend/utils/cache/meson.build +++ b/src/backend/utils/cache/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'attoptcache.c', diff --git a/src/backend/utils/cache/partcache.c b/src/backend/utils/cache/partcache.c index f5d7d70def0e8..3107075c9ad6c 100644 --- a/src/backend/utils/cache/partcache.c +++ b/src/backend/utils/cache/partcache.c @@ -4,7 +4,7 @@ * Support routines for manipulating partition information cached in * relcache * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -167,18 +167,18 @@ RelationBuildPartitionKey(Relation relation) /* Allocate assorted arrays in the partkeycxt, which we'll fill below */ oldcxt = MemoryContextSwitchTo(partkeycxt); - key->partattrs = (AttrNumber *) palloc0(key->partnatts * sizeof(AttrNumber)); - key->partopfamily = (Oid *) palloc0(key->partnatts * sizeof(Oid)); - key->partopcintype = (Oid *) palloc0(key->partnatts * sizeof(Oid)); - key->partsupfunc = (FmgrInfo *) palloc0(key->partnatts * sizeof(FmgrInfo)); - - key->partcollation = (Oid *) palloc0(key->partnatts * sizeof(Oid)); - key->parttypid = (Oid *) palloc0(key->partnatts * sizeof(Oid)); - key->parttypmod = (int32 *) palloc0(key->partnatts * sizeof(int32)); - key->parttyplen = (int16 *) palloc0(key->partnatts * sizeof(int16)); - key->parttypbyval = (bool *) palloc0(key->partnatts * sizeof(bool)); - key->parttypalign = (char *) palloc0(key->partnatts * sizeof(char)); - key->parttypcoll = (Oid *) palloc0(key->partnatts * sizeof(Oid)); + key->partattrs = palloc0_array(AttrNumber, key->partnatts); + key->partopfamily = palloc0_array(Oid, key->partnatts); + key->partopcintype = palloc0_array(Oid, key->partnatts); + key->partsupfunc = palloc0_array(FmgrInfo, key->partnatts); + + key->partcollation = palloc0_array(Oid, key->partnatts); + key->parttypid = palloc0_array(Oid, key->partnatts); + key->parttypmod = palloc0_array(int32, key->partnatts); + key->parttyplen = palloc0_array(int16, key->partnatts); + key->parttypbyval = palloc0_array(bool, key->partnatts); + key->parttypalign = palloc0_array(char, key->partnatts); + key->parttypcoll = palloc0_array(Oid, key->partnatts); MemoryContextSwitchTo(oldcxt); /* determine support function number to search for */ diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 89a1c79e984d1..698e7c1aa220f 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -44,7 +44,7 @@ * if the old one gets invalidated. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -106,8 +106,10 @@ static void ScanQueryForLocks(Query *parsetree, bool acquire); static bool ScanQueryWalker(Node *node, bool *acquire); static TupleDesc PlanCacheComputeResultDesc(List *stmt_list); static void PlanCacheRelCallback(Datum arg, Oid relid); -static void PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue); -static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue); +static void PlanCacheObjectCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); +static void PlanCacheSysCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); /* ResourceOwner callbacks to track plancache references */ static void ResOwnerReleaseCachedPlan(Datum res); @@ -180,7 +182,7 @@ InitPlanCache(void) * commandTag: command tag for query, or UNKNOWN if empty query */ CachedPlanSource * -CreateCachedPlan(RawStmt *raw_parse_tree, +CreateCachedPlan(const RawStmt *raw_parse_tree, const char *query_string, CommandTag commandTag) { @@ -207,7 +209,7 @@ CreateCachedPlan(RawStmt *raw_parse_tree, */ oldcxt = MemoryContextSwitchTo(source_context); - plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource)); + plansource = palloc0_object(CachedPlanSource); plansource->magic = CACHEDPLANSOURCE_MAGIC; plansource->raw_parse_tree = copyObject(raw_parse_tree); plansource->analyzed_parse_tree = NULL; @@ -307,7 +309,7 @@ CreateOneShotCachedPlan(RawStmt *raw_parse_tree, * Create and fill the CachedPlanSource struct within the caller's memory * context. Most fields are just left empty for the moment. */ - plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource)); + plansource = palloc0_object(CachedPlanSource); plansource->magic = CACHEDPLANSOURCE_MAGIC; plansource->raw_parse_tree = raw_parse_tree; plansource->analyzed_parse_tree = NULL; @@ -463,14 +465,13 @@ CompleteCachedPlan(CachedPlanSource *plansource, /* * Save the final parameter types (or other parameter specification data) - * into the source_context, as well as our other parameters. Also save - * the result tuple descriptor. + * into the source_context, as well as our other parameters. */ MemoryContextSwitchTo(source_context); if (num_params > 0) { - plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid)); + plansource->param_types = palloc_array(Oid, num_params); memcpy(plansource->param_types, param_types, num_params * sizeof(Oid)); } else @@ -480,9 +481,25 @@ CompleteCachedPlan(CachedPlanSource *plansource, plansource->parserSetupArg = parserSetupArg; plansource->cursor_options = cursor_options; plansource->fixed_result = fixed_result; - plansource->resultDesc = PlanCacheComputeResultDesc(querytree_list); + /* + * Also save the result tuple descriptor. PlanCacheComputeResultDesc may + * leak some cruft; normally we just accept that to save a copy step, but + * in USE_VALGRIND mode be tidy by running it in the caller's context. + */ +#ifdef USE_VALGRIND MemoryContextSwitchTo(oldcxt); + plansource->resultDesc = PlanCacheComputeResultDesc(querytree_list); + if (plansource->resultDesc) + { + MemoryContextSwitchTo(source_context); + plansource->resultDesc = CreateTupleDescCopy(plansource->resultDesc); + MemoryContextSwitchTo(oldcxt); + } +#else + plansource->resultDesc = PlanCacheComputeResultDesc(querytree_list); + MemoryContextSwitchTo(oldcxt); +#endif plansource->is_complete = true; plansource->is_valid = true; @@ -1104,7 +1121,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist, /* * Create and fill the CachedPlan struct within the new context. */ - plan = (CachedPlan *) palloc(sizeof(CachedPlan)); + plan = palloc_object(CachedPlan); plan->magic = CACHEDPLAN_MAGIC; plan->stmt_list = plist; @@ -1283,6 +1300,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams, CachedPlan *plan = NULL; List *qlist; bool customplan; + ListCell *lc; /* Assert caller is doing things in a sane order */ Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC); @@ -1385,6 +1403,13 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams, plan->is_saved = true; } + foreach(lc, plan->stmt_list) + { + PlannedStmt *pstmt = (PlannedStmt *) lfirst(lc); + + pstmt->planOrigin = customplan ? PLAN_STMT_CACHE_CUSTOM : PLAN_STMT_CACHE_GENERIC; + } + return plan; } @@ -1668,7 +1693,7 @@ CopyCachedPlan(CachedPlanSource *plansource) oldcxt = MemoryContextSwitchTo(source_context); - newsource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource)); + newsource = palloc0_object(CachedPlanSource); newsource->magic = CACHEDPLANSOURCE_MAGIC; newsource->raw_parse_tree = copyObject(plansource->raw_parse_tree); newsource->analyzed_parse_tree = copyObject(plansource->analyzed_parse_tree); @@ -1677,8 +1702,7 @@ CopyCachedPlan(CachedPlanSource *plansource) newsource->commandTag = plansource->commandTag; if (plansource->num_params > 0) { - newsource->param_types = (Oid *) - palloc(plansource->num_params * sizeof(Oid)); + newsource->param_types = palloc_array(Oid, plansource->num_params); memcpy(newsource->param_types, plansource->param_types, plansource->num_params * sizeof(Oid)); } @@ -1817,7 +1841,7 @@ GetCachedExpression(Node *expr) oldcxt = MemoryContextSwitchTo(cexpr_context); - cexpr = (CachedExpression *) palloc(sizeof(CachedExpression)); + cexpr = palloc_object(CachedExpression); cexpr->magic = CACHEDEXPR_MAGIC; cexpr->expr = copyObject(expr); cexpr->is_valid = true; @@ -1990,7 +2014,11 @@ ScanQueryForLocks(Query *parsetree, bool acquire) break; case RTE_SUBQUERY: - /* If this was a view, must lock/unlock the view */ + + /* + * If this was a view or a property graph, must lock/unlock + * it. + */ if (OidIsValid(rte->relid)) { if (acquire) @@ -2179,7 +2207,7 @@ PlanCacheRelCallback(Datum arg, Oid relid) * or all plans mentioning any member of this cache if hashvalue == 0. */ static void -PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue) +PlanCacheObjectCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { dlist_iter iter; @@ -2288,7 +2316,7 @@ PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue) * Just invalidate everything... */ static void -PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue) +PlanCacheSysCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { ResetPlanCache(); } diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 559ba9cdb2cde..e19f0d3e51cf3 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -3,7 +3,7 @@ * relcache.c * POSTGRES relation descriptor cache code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -75,7 +75,9 @@ #include "pgstat.h" #include "rewrite/rewriteDefine.h" #include "rewrite/rowsecurity.h" +#include "storage/fd.h" #include "storage/lmgr.h" +#include "storage/lock.h" #include "storage/smgr.h" #include "utils/array.h" #include "utils/builtins.h" @@ -422,7 +424,7 @@ AllocateRelationDesc(Form_pg_class relp) /* * allocate and zero space for new relation descriptor */ - relation = (Relation) palloc0(sizeof(RelationData)); + relation = palloc0_object(RelationData); /* make sure relation is marked as having no open file yet */ relation->rd_smgr = NULL; @@ -666,14 +668,6 @@ RelationBuildTupleDesc(Relation relation) elog(ERROR, "pg_attribute catalog is missing %d attribute(s) for relation OID %u", need, RelationGetRelid(relation)); - /* - * We can easily set the attcacheoff value for the first attribute: it - * must be zero. This eliminates the need for special cases for attnum=1 - * that used to exist in fastgetattr() and index_getattr(). - */ - if (RelationGetNumberOfAttributes(relation) > 0) - TupleDescCompactAttr(relation->rd_att, 0)->attcacheoff = 0; - /* * Set up constraint/default info */ @@ -729,6 +723,8 @@ RelationBuildTupleDesc(Relation relation) pfree(constr); relation->rd_att->constr = NULL; } + + TupleDescFinalize(relation->rd_att); } /* @@ -1420,22 +1416,17 @@ RelationInitPhysicalAddr(Relation relation) static void InitIndexAmRoutine(Relation relation) { - IndexAmRoutine *cached, - *tmp; + MemoryContext oldctx; /* - * Call the amhandler in current, short-lived memory context, just in case - * it leaks anything (it probably won't, but let's be paranoid). + * We formerly specified that the amhandler should return a palloc'd + * struct. That's now deprecated in favor of returning a pointer to a + * static struct, but to avoid completely breaking old external AMs, run + * the amhandler in the relation's rd_indexcxt. */ - tmp = GetIndexAmRoutine(relation->rd_amhandler); - - /* OK, now transfer the data into relation's rd_indexcxt. */ - cached = (IndexAmRoutine *) MemoryContextAlloc(relation->rd_indexcxt, - sizeof(IndexAmRoutine)); - memcpy(cached, tmp, sizeof(IndexAmRoutine)); - relation->rd_indam = cached; - - pfree(tmp); + oldctx = MemoryContextSwitchTo(relation->rd_indexcxt); + relation->rd_indam = GetIndexAmRoutine(relation->rd_amhandler); + MemoryContextSwitchTo(oldctx); } /* @@ -1902,7 +1893,7 @@ formrdesc(const char *relationName, Oid relationReltype, /* * allocate new relation desc, clear all fields of reldesc */ - relation = (Relation) palloc0(sizeof(RelationData)); + relation = palloc0_object(RelationData); /* make sure relation is marked as having no open file yet */ relation->rd_smgr = NULL; @@ -1988,13 +1979,12 @@ formrdesc(const char *relationName, Oid relationReltype, populate_compact_attribute(relation->rd_att, i); } - /* initialize first attribute's attcacheoff, cf RelationBuildTupleDesc */ - TupleDescCompactAttr(relation->rd_att, 0)->attcacheoff = 0; + TupleDescFinalize(relation->rd_att); /* mark not-null status */ if (has_not_null) { - TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr)); + TupleConstr *constr = palloc0_object(TupleConstr); constr->has_not_null = true; relation->rd_att->constr = constr; @@ -2896,7 +2886,7 @@ RelationForgetRelation(Oid rid) RelationIdCacheLookup(rid, relation); - if (!PointerIsValid(relation)) + if (!relation) return; /* not in cache, nothing to do */ if (!RelationHasReferenceCountZero(relation)) @@ -2941,7 +2931,7 @@ RelationCacheInvalidateEntry(Oid relationId) RelationIdCacheLookup(relationId, relation); - if (PointerIsValid(relation)) + if (relation) { relcacheInvalsReceived++; RelationFlushRelation(relation); @@ -3184,7 +3174,7 @@ AssertPendingSyncs_RelationCache(void) if ((LockTagType) locallock->tag.lock.locktag_type != LOCKTAG_RELATION) continue; - relid = ObjectIdGetDatum(locallock->tag.lock.locktag_field2); + relid = locallock->tag.lock.locktag_field2; r = RelationIdGetRelation(relid); if (!RelationIsValid(r)) continue; @@ -3579,7 +3569,7 @@ RelationBuildLocalRelation(const char *relname, /* * allocate a new relation descriptor and fill in basic state fields. */ - rel = (Relation) palloc0(sizeof(RelationData)); + rel = palloc0_object(RelationData); /* make sure relation is marked as having no open file yet */ rel->rd_smgr = NULL; @@ -3627,7 +3617,7 @@ RelationBuildLocalRelation(const char *relname, if (has_not_null) { - TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr)); + TupleConstr *constr = palloc0_object(TupleConstr); constr->has_not_null = true; rel->rd_att->constr = constr; @@ -3693,6 +3683,8 @@ RelationBuildLocalRelation(const char *relname, for (i = 0; i < natts; i++) TupleDescAttr(rel->rd_att, i)->attrelid = relid; + TupleDescFinalize(rel->rd_att); + rel->rd_rel->reltablespace = reltablespace; if (mapped_relation) @@ -4446,8 +4438,7 @@ BuildHardcodedDescriptor(int natts, const FormData_pg_attribute *attrs) populate_compact_attribute(result, i); } - /* initialize first attribute's attcacheoff, cf RelationBuildTupleDesc */ - TupleDescCompactAttr(result, 0)->attcacheoff = 0; + TupleDescFinalize(result); /* Note: we don't bother to set up a TupleConstr entry */ @@ -4658,12 +4649,6 @@ CheckNNConstraintFetch(Relation relation) break; } - check[found].ccenforced = conform->conenforced; - check[found].ccvalid = conform->convalidated; - check[found].ccnoinherit = conform->connoinherit; - check[found].ccname = MemoryContextStrdup(CacheMemoryContext, - NameStr(conform->conname)); - /* Grab and test conbin is actually set */ val = fastgetattr(htup, Anum_pg_constraint_conbin, @@ -4676,7 +4661,13 @@ CheckNNConstraintFetch(Relation relation) /* detoast and convert to cstring in caller's context */ char *s = TextDatumGetCString(val); + check[found].ccenforced = conform->conenforced; + check[found].ccvalid = conform->convalidated; + check[found].ccnoinherit = conform->connoinherit; + check[found].ccname = MemoryContextStrdup(CacheMemoryContext, + NameStr(conform->conname)); check[found].ccbin = MemoryContextStrdup(CacheMemoryContext, s); + pfree(s); found++; } @@ -5643,7 +5634,7 @@ RelationGetIdentityKeyBitmap(Relation relation) * This should be called only for an index that is known to have an associated * exclusion constraint or primary key/unique constraint using WITHOUT * OVERLAPS. - + * * It returns arrays (palloc'd in caller's context) of the exclusion operator * OIDs, their underlying functions' OIDs, and their strategy numbers in the * index's opclasses. We cache all this information since it requires a fair @@ -5670,9 +5661,9 @@ RelationGetExclusionInfo(Relation indexRelation, indnkeyatts = IndexRelationGetNumberOfKeyAttributes(indexRelation); /* Allocate result space in caller context */ - *operators = ops = (Oid *) palloc(sizeof(Oid) * indnkeyatts); - *procs = funcs = (Oid *) palloc(sizeof(Oid) * indnkeyatts); - *strategies = strats = (uint16 *) palloc(sizeof(uint16) * indnkeyatts); + *operators = ops = palloc_array(Oid, indnkeyatts); + *procs = funcs = palloc_array(Oid, indnkeyatts); + *strategies = strats = palloc_array(uint16, indnkeyatts); /* Quick exit if we have the data cached already */ if (indexRelation->rd_exclstrats != NULL) @@ -5763,9 +5754,9 @@ RelationGetExclusionInfo(Relation indexRelation, /* Save a copy of the results in the relcache entry. */ oldcxt = MemoryContextSwitchTo(indexRelation->rd_indexcxt); - indexRelation->rd_exclops = (Oid *) palloc(sizeof(Oid) * indnkeyatts); - indexRelation->rd_exclprocs = (Oid *) palloc(sizeof(Oid) * indnkeyatts); - indexRelation->rd_exclstrats = (uint16 *) palloc(sizeof(uint16) * indnkeyatts); + indexRelation->rd_exclops = palloc_array(Oid, indnkeyatts); + indexRelation->rd_exclprocs = palloc_array(Oid, indnkeyatts); + indexRelation->rd_exclstrats = palloc_array(uint16, indnkeyatts); memcpy(indexRelation->rd_exclops, ops, sizeof(Oid) * indnkeyatts); memcpy(indexRelation->rd_exclprocs, funcs, sizeof(Oid) * indnkeyatts); memcpy(indexRelation->rd_exclstrats, strats, sizeof(uint16) * indnkeyatts); @@ -5793,7 +5784,9 @@ RelationGetExclusionInfo(Relation indexRelation, void RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) { - List *puboids; + List *puboids = NIL; + List *exceptpuboids = NIL; + List *alltablespuboids; ListCell *lc; MemoryContext oldcxt; Oid schemaid; @@ -5831,28 +5824,49 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) pubdesc->gencols_valid_for_delete = true; /* Fetch the publication membership info. */ - puboids = GetRelationPublications(relid); + puboids = GetRelationIncludedPublications(relid); schemaid = RelationGetNamespace(relation); puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid)); if (relation->rd_rel->relispartition) { + Oid last_ancestor_relid; + /* Add publications that the ancestors are in too. */ ancestors = get_partition_ancestors(relid); + last_ancestor_relid = llast_oid(ancestors); foreach(lc, ancestors) { Oid ancestor = lfirst_oid(lc); puboids = list_concat_unique_oid(puboids, - GetRelationPublications(ancestor)); + GetRelationIncludedPublications(ancestor)); schemaid = get_rel_namespace(ancestor); puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid)); } + + /* + * Only the top-most ancestor can appear in the EXCEPT clause. + * Therefore, for a partition, exclusion must be evaluated at the + * top-most ancestor. + */ + exceptpuboids = GetRelationExcludedPublications(last_ancestor_relid); + } + else + { + /* + * For a regular table or a root partitioned table, check exclusion on + * table itself. + */ + exceptpuboids = GetRelationExcludedPublications(relid); } - puboids = list_concat_unique_oid(puboids, GetAllTablesPublications()); + alltablespuboids = GetAllTablesPublications(); + puboids = list_concat_unique_oid(puboids, + list_difference_oid(alltablespuboids, + exceptpuboids)); foreach(lc, puboids) { Oid pubid = lfirst_oid(lc); @@ -5959,7 +5973,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) /* Now save copy of the descriptor in the relcache entry. */ oldcxt = MemoryContextSwitchTo(CacheMemoryContext); - relation->rd_pubdesc = palloc(sizeof(PublicationDesc)); + relation->rd_pubdesc = palloc_object(PublicationDesc); memcpy(relation->rd_pubdesc, pubdesc, sizeof(PublicationDesc)); MemoryContextSwitchTo(oldcxt); } @@ -5967,7 +5981,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc) static bytea ** CopyIndexAttOptions(bytea **srcopts, int natts) { - bytea **opts = palloc(sizeof(*opts) * natts); + bytea **opts = palloc_array(bytea *, natts); for (int i = 0; i < natts; i++) { @@ -5999,7 +6013,7 @@ RelationGetIndexAttOptions(Relation relation, bool copy) return copy ? CopyIndexAttOptions(opts, natts) : opts; /* Get and parse opclass options. */ - opts = palloc0(sizeof(*opts) * natts); + opts = palloc0_array(bytea *, natts); for (i = 0; i < natts; i++) { @@ -6273,6 +6287,8 @@ load_relcache_init_file(bool shared) populate_compact_attribute(rel->rd_att, i); } + TupleDescFinalize(rel->rd_att); + /* next read the access method specific field */ if (fread(&len, 1, sizeof(len), fp) != sizeof(len)) goto read_failed; @@ -6292,7 +6308,7 @@ load_relcache_init_file(bool shared) /* mark not-null status */ if (has_not_null) { - TupleConstr *constr = (TupleConstr *) palloc0(sizeof(TupleConstr)); + TupleConstr *constr = palloc0_object(TupleConstr); constr->has_not_null = true; rel->rd_att->constr = constr; @@ -6991,5 +7007,5 @@ ResOwnerReleaseRelation(Datum res) Assert(rel->rd_refcnt > 0); rel->rd_refcnt -= 1; - RelationCloseCleanup((Relation) res); + RelationCloseCleanup((Relation) DatumGetPointer(res)); } diff --git a/src/backend/utils/cache/relfilenumbermap.c b/src/backend/utils/cache/relfilenumbermap.c index 8a2f6f8c69318..6f970fafa056b 100644 --- a/src/backend/utils/cache/relfilenumbermap.c +++ b/src/backend/utils/cache/relfilenumbermap.c @@ -3,7 +3,7 @@ * relfilenumbermap.c * relfilenumber to oid mapping cache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -130,6 +130,11 @@ InitializeRelfilenumberMap(void) * Map a relation's (tablespace, relfilenumber) to a relation's oid and cache * the result. * + * A temporary relation may share its relfilenumber with a permanent relation + * or temporary relations created in other backends. Being able to uniquely + * identify a temporary relation would require a backend's proc number, which + * we do not know about. Hence, this function ignores this case. + * * Returns InvalidOid if no relation matching the criteria could be found. */ Oid @@ -208,6 +213,9 @@ RelidByRelfilenumber(Oid reltablespace, RelFileNumber relfilenumber) { Form_pg_class classform = (Form_pg_class) GETSTRUCT(ntp); + if (classform->relpersistence == RELPERSISTENCE_TEMP) + continue; + if (found) elog(ERROR, "unexpected duplicate for tablespace %u, relfilenumber %u", diff --git a/src/backend/utils/cache/relmapper.c b/src/backend/utils/cache/relmapper.c index abf89f0776e99..3aaf466868d46 100644 --- a/src/backend/utils/cache/relmapper.c +++ b/src/backend/utils/cache/relmapper.c @@ -28,7 +28,7 @@ * all these files commit in a single map file update rather than being tied * to transaction commit. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -54,6 +54,7 @@ #include "storage/lwlock.h" #include "utils/inval.h" #include "utils/relmapper.h" +#include "utils/wait_event.h" /* diff --git a/src/backend/utils/cache/spccache.c b/src/backend/utils/cache/spccache.c index 2345859929833..362169b7d97a2 100644 --- a/src/backend/utils/cache/spccache.c +++ b/src/backend/utils/cache/spccache.c @@ -8,7 +8,7 @@ * be a measurable performance gain from doing this, but that might change * in the future as we add more options. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -52,7 +52,8 @@ typedef struct * tablespaces, nor do we expect them to be frequently modified. */ static void -InvalidateTableSpaceCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +InvalidateTableSpaceCacheCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue) { HASH_SEQ_STATUS status; TableSpaceCacheEntry *spc; diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index f944453a1d884..f4233f9e31a61 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -3,7 +3,7 @@ * syscache.c * System cache management routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -32,6 +32,7 @@ #include "lib/qunique.h" #include "miscadmin.h" #include "storage/lmgr.h" +#include "storage/lock.h" #include "utils/catcache.h" #include "utils/inval.h" #include "utils/lsyscache.h" @@ -109,7 +110,7 @@ static int oid_compare(const void *a, const void *b); void InitCatalogCache(void) { - int cacheId; + SysCacheIdentifier cacheId; Assert(!CacheInitialized); @@ -131,7 +132,7 @@ InitCatalogCache(void) cacheinfo[cacheId].nkeys, cacheinfo[cacheId].key, cacheinfo[cacheId].nbuckets); - if (!PointerIsValid(SysCache[cacheId])) + if (!SysCache[cacheId]) elog(ERROR, "could not initialize cache %u (%d)", cacheinfo[cacheId].reloid, cacheId); /* Accumulate data for OID lists, too */ @@ -179,7 +180,7 @@ InitCatalogCache(void) void InitCatalogCachePhase2(void) { - int cacheId; + SysCacheIdentifier cacheId; Assert(CacheInitialized); @@ -205,57 +206,52 @@ InitCatalogCachePhase2(void) * CAUTION: The tuple that is returned must NOT be freed by the caller! */ HeapTuple -SearchSysCache(int cacheId, +SearchSysCache(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4) { - Assert(cacheId >= 0 && cacheId < SysCacheSize && - PointerIsValid(SysCache[cacheId])); + Assert(cacheId >= 0 && cacheId < SysCacheSize && SysCache[cacheId]); return SearchCatCache(SysCache[cacheId], key1, key2, key3, key4); } HeapTuple -SearchSysCache1(int cacheId, +SearchSysCache1(SysCacheIdentifier cacheId, Datum key1) { - Assert(cacheId >= 0 && cacheId < SysCacheSize && - PointerIsValid(SysCache[cacheId])); + Assert(cacheId >= 0 && cacheId < SysCacheSize && SysCache[cacheId]); Assert(SysCache[cacheId]->cc_nkeys == 1); return SearchCatCache1(SysCache[cacheId], key1); } HeapTuple -SearchSysCache2(int cacheId, +SearchSysCache2(SysCacheIdentifier cacheId, Datum key1, Datum key2) { - Assert(cacheId >= 0 && cacheId < SysCacheSize && - PointerIsValid(SysCache[cacheId])); + Assert(cacheId >= 0 && cacheId < SysCacheSize && SysCache[cacheId]); Assert(SysCache[cacheId]->cc_nkeys == 2); return SearchCatCache2(SysCache[cacheId], key1, key2); } HeapTuple -SearchSysCache3(int cacheId, +SearchSysCache3(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3) { - Assert(cacheId >= 0 && cacheId < SysCacheSize && - PointerIsValid(SysCache[cacheId])); + Assert(cacheId >= 0 && cacheId < SysCacheSize && SysCache[cacheId]); Assert(SysCache[cacheId]->cc_nkeys == 3); return SearchCatCache3(SysCache[cacheId], key1, key2, key3); } HeapTuple -SearchSysCache4(int cacheId, +SearchSysCache4(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4) { - Assert(cacheId >= 0 && cacheId < SysCacheSize && - PointerIsValid(SysCache[cacheId])); + Assert(cacheId >= 0 && cacheId < SysCacheSize && SysCache[cacheId]); Assert(SysCache[cacheId]->cc_nkeys == 4); return SearchCatCache4(SysCache[cacheId], key1, key2, key3, key4); @@ -284,7 +280,7 @@ ReleaseSysCache(HeapTuple tuple) * doesn't prevent the "tuple concurrently updated" error. */ HeapTuple -SearchSysCacheLocked1(int cacheId, +SearchSysCacheLocked1(SysCacheIdentifier cacheId, Datum key1) { CatCache *cache = SysCache[cacheId]; @@ -376,7 +372,7 @@ SearchSysCacheLocked1(int cacheId, * heap_freetuple() the result when done with it. */ HeapTuple -SearchSysCacheCopy(int cacheId, +SearchSysCacheCopy(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, @@ -401,7 +397,7 @@ SearchSysCacheCopy(int cacheId, * heap_freetuple(). */ HeapTuple -SearchSysCacheLockedCopy1(int cacheId, +SearchSysCacheLockedCopy1(SysCacheIdentifier cacheId, Datum key1) { HeapTuple tuple, @@ -422,7 +418,7 @@ SearchSysCacheLockedCopy1(int cacheId, * No lock is retained on the syscache entry. */ bool -SearchSysCacheExists(int cacheId, +SearchSysCacheExists(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, @@ -445,7 +441,7 @@ SearchSysCacheExists(int cacheId, * No lock is retained on the syscache entry. */ Oid -GetSysCacheOid(int cacheId, +GetSysCacheOid(SysCacheIdentifier cacheId, AttrNumber oidcol, Datum key1, Datum key2, @@ -459,9 +455,9 @@ GetSysCacheOid(int cacheId, tuple = SearchSysCache(cacheId, key1, key2, key3, key4); if (!HeapTupleIsValid(tuple)) return InvalidOid; - result = heap_getattr(tuple, oidcol, - SysCache[cacheId]->cc_tupdesc, - &isNull); + result = DatumGetObjectId(heap_getattr(tuple, oidcol, + SysCache[cacheId]->cc_tupdesc, + &isNull)); Assert(!isNull); /* columns used as oids should never be NULL */ ReleaseSysCache(tuple); return result; @@ -597,7 +593,7 @@ SearchSysCacheCopyAttNum(Oid relid, int16 attnum) * a different cache for the same catalog the tuple was fetched from. */ Datum -SysCacheGetAttr(int cacheId, HeapTuple tup, +SysCacheGetAttr(SysCacheIdentifier cacheId, HeapTuple tup, AttrNumber attributeNumber, bool *isNull) { @@ -607,13 +603,12 @@ SysCacheGetAttr(int cacheId, HeapTuple tup, * valid (because the caller recently fetched the tuple via this same * cache), but there are cases where we have to initialize the cache here. */ - if (cacheId < 0 || cacheId >= SysCacheSize || - !PointerIsValid(SysCache[cacheId])) + if (cacheId < 0 || cacheId >= SysCacheSize || !SysCache[cacheId]) elog(ERROR, "invalid cache ID: %d", cacheId); - if (!PointerIsValid(SysCache[cacheId]->cc_tupdesc)) + if (!SysCache[cacheId]->cc_tupdesc) { InitCatCachePhase2(SysCache[cacheId], false); - Assert(PointerIsValid(SysCache[cacheId]->cc_tupdesc)); + Assert(SysCache[cacheId]->cc_tupdesc); } return heap_getattr(tup, attributeNumber, @@ -628,7 +623,7 @@ SysCacheGetAttr(int cacheId, HeapTuple tup, * be NULL. */ Datum -SysCacheGetAttrNotNull(int cacheId, HeapTuple tup, +SysCacheGetAttrNotNull(SysCacheIdentifier cacheId, HeapTuple tup, AttrNumber attributeNumber) { bool isnull; @@ -658,14 +653,13 @@ SysCacheGetAttrNotNull(int cacheId, HeapTuple tup, * catcache code that need to be able to compute the hash values. */ uint32 -GetSysCacheHashValue(int cacheId, +GetSysCacheHashValue(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4) { - if (cacheId < 0 || cacheId >= SysCacheSize || - !PointerIsValid(SysCache[cacheId])) + if (cacheId < 0 || cacheId >= SysCacheSize || !SysCache[cacheId]) elog(ERROR, "invalid cache ID: %d", cacheId); return GetCatCacheHashValue(SysCache[cacheId], key1, key2, key3, key4); @@ -675,11 +669,10 @@ GetSysCacheHashValue(int cacheId, * List-search interface */ struct catclist * -SearchSysCacheList(int cacheId, int nkeys, +SearchSysCacheList(SysCacheIdentifier cacheId, int nkeys, Datum key1, Datum key2, Datum key3) { - if (cacheId < 0 || cacheId >= SysCacheSize || - !PointerIsValid(SysCache[cacheId])) + if (cacheId < 0 || cacheId >= SysCacheSize || !SysCache[cacheId]) elog(ERROR, "invalid cache ID: %d", cacheId); return SearchCatCacheList(SysCache[cacheId], nkeys, @@ -695,13 +688,13 @@ SearchSysCacheList(int cacheId, int nkeys, * This routine is only quasi-public: it should only be used by inval.c. */ void -SysCacheInvalidate(int cacheId, uint32 hashValue) +SysCacheInvalidate(SysCacheIdentifier cacheId, uint32 hashValue) { if (cacheId < 0 || cacheId >= SysCacheSize) elog(ERROR, "invalid cache ID: %d", cacheId); /* if this cache isn't initialized yet, no need to do anything */ - if (!PointerIsValid(SysCache[cacheId])) + if (!SysCache[cacheId]) return; CatCacheInvalidate(SysCache[cacheId], hashValue); diff --git a/src/backend/utils/cache/ts_cache.c b/src/backend/utils/cache/ts_cache.c index 18cccd778fd8c..9e29f1386b025 100644 --- a/src/backend/utils/cache/ts_cache.c +++ b/src/backend/utils/cache/ts_cache.c @@ -17,7 +17,7 @@ * any database access. * * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/cache/ts_cache.c @@ -44,6 +44,7 @@ #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/guc_hooks.h" +#include "utils/hsearch.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -91,7 +92,7 @@ static Oid TSCurrentConfigCache = InvalidOid; * table address as the "arg". */ static void -InvalidateTSCacheCallBack(Datum arg, int cacheid, uint32 hashvalue) +InvalidateTSCacheCallBack(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { HTAB *hash = (HTAB *) DatumGetPointer(arg); HASH_SEQ_STATUS status; @@ -321,7 +322,9 @@ lookup_ts_dictionary_cache(Oid dictId) /* * Init method runs in dictionary's private memory context, and we - * make sure the options are stored there too + * make sure the options are stored there too. This typically + * results in a small amount of memory leakage, but it's not worth + * complicating the API for tmplinit functions to avoid it. */ oldcontext = MemoryContextSwitchTo(entry->dictCtx); diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c index f9aec38a11fb3..cebe7a916fbf9 100644 --- a/src/backend/utils/cache/typcache.c +++ b/src/backend/utils/cache/typcache.c @@ -31,7 +31,7 @@ * constraint changes are also tracked properly. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -235,8 +235,8 @@ shared_record_table_compare(const void *a, const void *b, size_t size, void *arg) { dsa_area *area = (dsa_area *) arg; - SharedRecordTableKey *k1 = (SharedRecordTableKey *) a; - SharedRecordTableKey *k2 = (SharedRecordTableKey *) b; + const SharedRecordTableKey *k1 = a; + const SharedRecordTableKey *k2 = b; TupleDesc t1; TupleDesc t2; @@ -259,8 +259,8 @@ shared_record_table_compare(const void *a, const void *b, size_t size, static uint32 shared_record_table_hash(const void *a, size_t size, void *arg) { - dsa_area *area = (dsa_area *) arg; - SharedRecordTableKey *k = (SharedRecordTableKey *) a; + dsa_area *area = arg; + const SharedRecordTableKey *k = a; TupleDesc t; if (k->shared) @@ -337,9 +337,12 @@ static bool multirange_element_has_hashing(TypeCacheEntry *typentry); static bool multirange_element_has_extended_hashing(TypeCacheEntry *typentry); static void cache_multirange_element_properties(TypeCacheEntry *typentry); static void TypeCacheRelCallback(Datum arg, Oid relid); -static void TypeCacheTypCallback(Datum arg, int cacheid, uint32 hashvalue); -static void TypeCacheOpcCallback(Datum arg, int cacheid, uint32 hashvalue); -static void TypeCacheConstrCallback(Datum arg, int cacheid, uint32 hashvalue); +static void TypeCacheTypCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); +static void TypeCacheOpcCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); +static void TypeCacheConstrCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); static void load_enum_cache_data(TypeCacheEntry *tcache); static EnumItem *find_enumitem(TypeCacheEnumData *enumdata, Oid arg); static int enum_oid_cmp(const void *left, const void *right); @@ -1171,9 +1174,6 @@ load_domaintype_info(TypeCacheEntry *typentry) elog(ERROR, "domain \"%s\" constraint \"%s\" has NULL conbin", NameStr(typTup->typname), NameStr(c->conname)); - /* Convert conbin to C string in caller context */ - constring = TextDatumGetCString(val); - /* Create the DomainConstraintCache object and context if needed */ if (dcc == NULL) { @@ -1189,9 +1189,8 @@ load_domaintype_info(TypeCacheEntry *typentry) dcc->dccRefCount = 0; } - /* Create node trees in DomainConstraintCache's context */ - oldcxt = MemoryContextSwitchTo(dcc->dccContext); - + /* Convert conbin to a node tree, still in caller's context */ + constring = TextDatumGetCString(val); check_expr = (Expr *) stringToNode(constring); /* @@ -1206,10 +1205,13 @@ load_domaintype_info(TypeCacheEntry *typentry) */ check_expr = expression_planner(check_expr); + /* Create only the minimally needed stuff in dccContext */ + oldcxt = MemoryContextSwitchTo(dcc->dccContext); + r = makeNode(DomainConstraintState); r->constrainttype = DOM_CONSTRAINT_CHECK; r->name = pstrdup(NameStr(c->conname)); - r->check_expr = check_expr; + r->check_expr = copyObject(check_expr); r->check_exprstate = NULL; MemoryContextSwitchTo(oldcxt); @@ -1483,10 +1485,14 @@ UpdateDomainConstraintRef(DomainConstraintRef *ref) /* * DomainHasConstraints --- utility routine to check if a domain has constraints * + * Returns true if the domain has any constraints at all. If has_volatile + * is not NULL, also checks whether any CHECK constraint contains a volatile + * expression and sets *has_volatile accordingly. + * * This is defined to return false, not fail, if type is not a domain. */ bool -DomainHasConstraints(Oid type_id) +DomainHasConstraints(Oid type_id, bool *has_volatile) { TypeCacheEntry *typentry; @@ -1496,7 +1502,26 @@ DomainHasConstraints(Oid type_id) */ typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO); - return (typentry->domainData != NULL); + if (typentry->domainData == NULL) + return false; + + if (has_volatile) + { + *has_volatile = false; + + foreach_node(DomainConstraintState, constrstate, + typentry->domainData->constraints) + { + if (constrstate->constrainttype == DOM_CONSTRAINT_CHECK && + contain_volatile_functions((Node *) constrstate->check_expr)) + { + *has_volatile = true; + break; + } + } + } + + return true; } @@ -2014,7 +2039,7 @@ lookup_rowtype_tupdesc_domain(Oid type_id, int32 typmod, bool noError) static uint32 record_type_typmod_hash(const void *data, size_t size) { - RecordCacheEntry *entry = (RecordCacheEntry *) data; + const RecordCacheEntry *entry = data; return hashRowType(entry->tupdesc); } @@ -2025,8 +2050,8 @@ record_type_typmod_hash(const void *data, size_t size) static int record_type_typmod_compare(const void *a, const void *b, size_t size) { - RecordCacheEntry *left = (RecordCacheEntry *) a; - RecordCacheEntry *right = (RecordCacheEntry *) b; + const RecordCacheEntry *left = a; + const RecordCacheEntry *right = b; return equalRowTypes(left->tupdesc, right->tupdesc) ? 0 : 1; } @@ -2430,7 +2455,7 @@ TypeCacheRelCallback(Datum arg, Oid relid) RelIdToTypeIdCacheEntry *relentry; /* - * Find an RelIdToTypeIdCacheHash entry, which should exist as soon as + * Find a RelIdToTypeIdCacheHash entry, which should exist as soon as * corresponding typcache entry has something to clean. */ relentry = (RelIdToTypeIdCacheEntry *) hash_search(RelIdToTypeIdCacheHash, @@ -2513,7 +2538,7 @@ TypeCacheRelCallback(Datum arg, Oid relid) * it as needing to be reloaded. */ static void -TypeCacheTypCallback(Datum arg, int cacheid, uint32 hashvalue) +TypeCacheTypCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { HASH_SEQ_STATUS status; TypeCacheEntry *typentry; @@ -2570,7 +2595,7 @@ TypeCacheTypCallback(Datum arg, int cacheid, uint32 hashvalue) * of members are not going to get cached here. */ static void -TypeCacheOpcCallback(Datum arg, int cacheid, uint32 hashvalue) +TypeCacheOpcCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { HASH_SEQ_STATUS status; TypeCacheEntry *typentry; @@ -2608,7 +2633,7 @@ TypeCacheOpcCallback(Datum arg, int cacheid, uint32 hashvalue) * approach to domain constraints. */ static void -TypeCacheConstrCallback(Datum arg, int cacheid, uint32 hashvalue) +TypeCacheConstrCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { TypeCacheEntry *typentry; @@ -2765,7 +2790,7 @@ load_enum_cache_data(TypeCacheEntry *tcache) * through. */ maxitems = 64; - items = (EnumItem *) palloc(sizeof(EnumItem) * maxitems); + items = palloc_array(EnumItem, maxitems); numitems = 0; /* Scan pg_enum for the members of the target enum type. */ diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index c96aa7c49efeb..5b25402ebbedb 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -2,7 +2,7 @@ # errcodes.txt # PostgreSQL error codes # -# Copyright (c) 2003-2025, PostgreSQL Global Development Group +# Copyright (c) 2003-2026, PostgreSQL Global Development Group # # This list serves as the basis for generating source files containing error # codes. It is kept in a common format to make sure all these source files have diff --git a/src/backend/utils/error/assert.c b/src/backend/utils/error/assert.c index 84b94f5e5f472..e24632fcfa9c9 100644 --- a/src/backend/utils/error/assert.c +++ b/src/backend/utils/error/assert.c @@ -3,7 +3,7 @@ * assert.c * Assert support code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -32,8 +32,7 @@ ExceptionalCondition(const char *conditionName, int lineNumber) { /* Report the failure on stderr (or local equivalent) */ - if (!PointerIsValid(conditionName) - || !PointerIsValid(fileName)) + if (!conditionName || !fileName) write_stderr("TRAP: ExceptionalCondition: bad arguments in PID %d\n", (int) getpid()); else diff --git a/src/backend/utils/error/csvlog.c b/src/backend/utils/error/csvlog.c index fdac3c048e36a..2b2b9484bdcbd 100644 --- a/src/backend/utils/error/csvlog.c +++ b/src/backend/utils/error/csvlog.c @@ -3,7 +3,7 @@ * csvlog.c * CSV logging * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -120,7 +120,7 @@ write_csvlog(ErrorData *edata) appendStringInfoChar(&buf, ','); /* session id */ - appendStringInfo(&buf, INT64_HEX_FORMAT ".%x", MyStartTime, MyProcPid); + appendStringInfo(&buf, "%" PRIx64 ".%x", MyStartTime, MyProcPid); appendStringInfoChar(&buf, ','); /* Line number */ diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index 47af743990fe9..1b8a73f589a12 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -43,7 +43,7 @@ * overflow.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -66,6 +66,10 @@ #include #endif +#ifdef _MSC_VER +#include +#endif + #include "access/xact.h" #include "common/ip.h" #include "libpq/libpq.h" @@ -82,6 +86,7 @@ #include "tcop/tcopprot.h" #include "utils/guc_hooks.h" #include "utils/memutils.h" +#include "utils/pg_locale.h" #include "utils/ps_status.h" #include "utils/varlena.h" @@ -140,6 +145,11 @@ static void write_syslog(int level, const char *line); static void write_eventlog(int level, const char *line, int len); #endif +#ifdef _MSC_VER +static bool backtrace_symbols_initialized = false; +static HANDLE backtrace_process = NULL; +#endif + /* We provide a small stack of ErrorData records for re-entrant cases */ #define ERRORDATA_STACK_SIZE 5 @@ -180,8 +190,10 @@ static void set_stack_entry_location(ErrorData *edata, const char *funcname); static bool matches_backtrace_functions(const char *funcname); static pg_noinline void set_backtrace(ErrorData *edata, int num_skip); +static void backtrace_cleanup(int code, Datum arg); static void set_errdata_field(MemoryContextData *cxt, char **ptr, const char *str); static void FreeErrorDataContents(ErrorData *edata); +static int log_min_messages_cmp(const ListCell *a, const ListCell *b); static void write_console(const char *line, int len); static const char *process_log_prefix_padding(const char *p, int *ppadding); static void log_line_prefix(StringInfo buf, ErrorData *edata); @@ -206,7 +218,7 @@ is_log_level_output(int elevel, int log_min_level) if (log_min_level == LOG || log_min_level <= ERROR) return true; } - else if (elevel == WARNING_CLIENT_ONLY) + else if (elevel == WARNING_CLIENT_ONLY || elevel == FATAL_CLIENT_ONLY) { /* never sent to log, regardless of log_min_level */ return false; @@ -235,7 +247,7 @@ is_log_level_output(int elevel, int log_min_level) static inline bool should_output_to_server(int elevel) { - return is_log_level_output(elevel, log_min_messages); + return is_log_level_output(elevel, log_min_messages[MyBackendType]); } /* @@ -542,18 +554,27 @@ errfinish(const char *filename, int lineno, const char *funcname) /* Emit the message to the right places */ EmitErrorReport(); - /* Now free up subsidiary data attached to stack entry, and release it */ - FreeErrorDataContents(edata); - errordata_stack_depth--; + /* + * If this is the outermost recursion level, we can clean up by resetting + * ErrorContext altogether (compare FlushErrorState), which is good + * because it cleans up any random leakages that might have occurred in + * places such as context callback functions. If we're nested, we can + * only safely remove the subsidiary data of the current stack entry. + */ + if (errordata_stack_depth == 0 && recursion_depth == 1) + MemoryContextReset(ErrorContext); + else + FreeErrorDataContents(edata); - /* Exit error-handling context */ + /* Release stack entry and exit error-handling context */ + errordata_stack_depth--; MemoryContextSwitchTo(oldcontext); recursion_depth--; /* * Perform error recovery action as specified by elevel. */ - if (elevel == FATAL) + if (elevel == FATAL || elevel == FATAL_CLIENT_ONLY) { /* * For a FATAL error, we let proc_exit clean up and exit. @@ -905,7 +926,9 @@ errcode_for_file_access(void) /* Wrong object type or state */ case ENOTDIR: /* Not a directory */ case EISDIR: /* Is a directory */ +#if defined(ENOTEMPTY) && (ENOTEMPTY != EEXIST) /* same code on AIX */ case ENOTEMPTY: /* Directory not empty */ +#endif edata->sqlerrcode = ERRCODE_WRONG_OBJECT_TYPE; break; @@ -1112,6 +1135,13 @@ errbacktrace(void) * specifies how many inner frames to skip. Use this to avoid showing the * internal backtrace support functions in the backtrace. This requires that * this and related functions are not inlined. + * + * The implementation is, unsurprisingly, platform-specific: + * - GNU libc and copycats: Uses backtrace() and backtrace_symbols() + * - Windows: Uses CaptureStackBackTrace() with DbgHelp for symbol resolution + * (requires PDB files; falls back to exported functions/raw addresses if + * unavailable) + * - Others (musl libc): unsupported */ static void set_backtrace(ErrorData *edata, int num_skip) @@ -1122,18 +1152,159 @@ set_backtrace(ErrorData *edata, int num_skip) #ifdef HAVE_BACKTRACE_SYMBOLS { - void *buf[100]; + void *frames[100]; int nframes; char **strfrms; - nframes = backtrace(buf, lengthof(buf)); - strfrms = backtrace_symbols(buf, nframes); - if (strfrms == NULL) + nframes = backtrace(frames, lengthof(frames)); + strfrms = backtrace_symbols(frames, nframes); + if (strfrms != NULL) + { + for (int i = num_skip; i < nframes; i++) + appendStringInfo(&errtrace, "\n%s", strfrms[i]); + free(strfrms); + } + else + appendStringInfoString(&errtrace, + "insufficient memory for backtrace generation"); + } +#elif defined(_MSC_VER) + { + void *frames[100]; + int nframes; + char buffer[sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(wchar_t)]; + PSYMBOL_INFOW psymbol; + + /* + * This is arranged so that we don't retry if we happen to fail to + * initialize state on the first attempt in any one process. + */ + if (!backtrace_symbols_initialized) + { + backtrace_symbols_initialized = true; + + if (DuplicateHandle(GetCurrentProcess(), + GetCurrentProcess(), + GetCurrentProcess(), + &backtrace_process, + 0, + FALSE, + DUPLICATE_SAME_ACCESS) == 0) + { + appendStringInfo(&errtrace, + "could not get process handle for backtrace: error code %lu", + GetLastError()); + edata->backtrace = errtrace.data; + return; + } + + SymSetOptions(SYMOPT_DEFERRED_LOADS | + SYMOPT_FAIL_CRITICAL_ERRORS | + SYMOPT_LOAD_LINES | + SYMOPT_UNDNAME); + + if (!SymInitialize(backtrace_process, NULL, TRUE)) + { + CloseHandle(backtrace_process); + backtrace_process = NULL; + appendStringInfo(&errtrace, + "could not initialize symbol handler: error code %lu", + GetLastError()); + edata->backtrace = errtrace.data; + return; + } + + on_proc_exit(backtrace_cleanup, 0); + } + + if (backtrace_process == NULL) + return; + + nframes = CaptureStackBackTrace(num_skip, lengthof(frames), frames, NULL); + + if (nframes == 0) + { + appendStringInfoString(&errtrace, "zero stack frames captured"); + edata->backtrace = errtrace.data; return; + } + + psymbol = (PSYMBOL_INFOW) buffer; + psymbol->MaxNameLen = MAX_SYM_NAME; + psymbol->SizeOfStruct = sizeof(SYMBOL_INFOW); - for (int i = num_skip; i < nframes; i++) - appendStringInfo(&errtrace, "\n%s", strfrms[i]); - free(strfrms); + for (int i = 0; i < nframes; i++) + { + DWORD64 address = (DWORD64) frames[i]; + DWORD64 displacement = 0; + BOOL sym_result; + + sym_result = SymFromAddrW(backtrace_process, + address, + &displacement, + psymbol); + if (sym_result == TRUE) + { + char symbol_name[MAX_SYM_NAME]; + size_t result; + + /* + * Convert symbol name from UTF-16 to database encoding using + * wchar2char(), which handles both UTF-8 and non-UTF-8 + * databases correctly on Windows. + */ + result = wchar2char(symbol_name, (const wchar_t *) psymbol->Name, + sizeof(symbol_name), NULL); + + if (result == (size_t) -1 || result == sizeof(symbol_name)) + { + /* Conversion failed, use address only */ + appendStringInfo(&errtrace, + "\n[0x%llx]", + (unsigned long long) address); + } + else + { + IMAGEHLP_LINEW64 line; + DWORD line_displacement = 0; + char filename[MAX_PATH]; + + line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64); + + /* Start with the common part: symbol+offset [address] */ + appendStringInfo(&errtrace, + "\n%s+0x%llx [0x%llx]", + symbol_name, + (unsigned long long) displacement, + (unsigned long long) address); + + /* Try to append line info if available */ + if (SymGetLineFromAddrW64(backtrace_process, + address, + &line_displacement, + &line)) + { + result = wchar2char(filename, (const wchar_t *) line.FileName, + sizeof(filename), NULL); + + if (result != (size_t) -1 && result != sizeof(filename)) + { + appendStringInfo(&errtrace, + " [%s:%lu]", + filename, + (unsigned long) line.LineNumber); + } + } + } + } + else + { + appendStringInfo(&errtrace, + "\n[0x%llx] (symbol lookup failed: error code %lu)", + (unsigned long long) address, + GetLastError()); + } + } } #else appendStringInfoString(&errtrace, @@ -1143,6 +1314,26 @@ set_backtrace(ErrorData *edata, int num_skip) edata->backtrace = errtrace.data; } +/* + * Cleanup function for set_backtrace(). + */ +pg_attribute_unused() +static void +backtrace_cleanup(int code, Datum arg) +{ +#ifdef _MSC_VER + /* + * Currently only used to clean up after SymInitialize. We shouldn't ever + * be called if backtrace_process is NULL, but better be safe. + */ + if (backtrace_process) + { + SymCleanup(backtrace_process); + backtrace_process = NULL; + } +#endif +} + /* * errmsg_internal --- add a primary error message text to the current error * @@ -1762,7 +1953,7 @@ CopyErrorData(void) Assert(CurrentMemoryContext != ErrorContext); /* Copy the struct itself */ - newedata = (ErrorData *) palloc(sizeof(ErrorData)); + newedata = palloc_object(ErrorData); memcpy(newedata, edata, sizeof(ErrorData)); /* @@ -2158,6 +2349,251 @@ DebugFileOpen(void) } +/* + * GUC check_hook for log_min_messages + * + * This value is parsed as a comma-separated list of zero or more TYPE:LEVEL + * elements. For each element, TYPE corresponds to a bkcategory value (see + * postmaster/proctypelist.h); LEVEL is one of server_message_level_options. + * + * In addition, there must be a single LEVEL element (with no TYPE part) + * which sets the default level for process types that aren't specified. + */ +bool +check_log_min_messages(char **newval, void **extra, GucSource source) +{ + char *rawstring; + List *elemlist; + StringInfoData buf; + char *result; + int newlevel[BACKEND_NUM_TYPES]; + bool assigned[BACKEND_NUM_TYPES] = {0}; + int defaultlevel = -1; /* -1 means not assigned */ + + const char *const process_types[] = { +#define PG_PROCTYPE(bktype, bkcategory, description, main_func, shmem_attach) \ + [bktype] = bkcategory, +#include "postmaster/proctypelist.h" +#undef PG_PROCTYPE + }; + + /* Need a modifiable copy of string. */ + rawstring = guc_strdup(LOG, *newval); + if (rawstring == NULL) + return false; + + /* Parse the string into a list. */ + if (!SplitGUCList(rawstring, ',', &elemlist)) + { + /* syntax error in list */ + GUC_check_errdetail("List syntax is invalid."); + list_free(elemlist); + guc_free(rawstring); + return false; + } + + /* Validate and assign log level and process type. */ + foreach_ptr(char, elem, elemlist) + { + char *sep = strchr(elem, ':'); + + /* + * If there's no ':' separator in the entry, this is the default log + * level. Otherwise it's a process type-specific entry. + */ + if (sep == NULL) + { + const struct config_enum_entry *entry; + bool found; + + /* Reject duplicates for default log level. */ + if (defaultlevel != -1) + { + GUC_check_errdetail("Redundant specification of default log level."); + goto lmm_fail; + } + + /* Validate the log level */ + found = false; + for (entry = server_message_level_options; entry && entry->name; entry++) + { + if (pg_strcasecmp(entry->name, elem) == 0) + { + defaultlevel = entry->val; + found = true; + break; + } + } + + if (!found) + { + GUC_check_errdetail("Unrecognized log level: \"%s\".", elem); + goto lmm_fail; + } + } + else + { + char *loglevel = sep + 1; + char *ptype = elem; + bool found; + int level; + const struct config_enum_entry *entry; + + /* + * Temporarily clobber the ':' with a string terminator, so that + * we can validate it. We restore this at the bottom. + */ + *sep = '\0'; + + /* Validate the log level */ + found = false; + for (entry = server_message_level_options; entry && entry->name; entry++) + { + if (pg_strcasecmp(entry->name, loglevel) == 0) + { + level = entry->val; + found = true; + break; + } + } + + if (!found) + { + GUC_check_errdetail("Unrecognized log level for process type \"%s\": \"%s\".", + ptype, loglevel); + goto lmm_fail; + } + + /* Is the process type name valid and unique? */ + found = false; + for (int i = 0; i < BACKEND_NUM_TYPES; i++) + { + if (pg_strcasecmp(process_types[i], ptype) == 0) + { + /* Reject duplicates for a process type. */ + if (assigned[i]) + { + GUC_check_errdetail("Redundant log level specification for process type \"%s\".", + ptype); + goto lmm_fail; + } + + newlevel[i] = level; + assigned[i] = true; + found = true; + + /* + * note: we must keep looking! some process types appear + * multiple times in proctypelist.h. + */ + } + } + + if (!found) + { + GUC_check_errdetail("Unrecognized process type \"%s\".", ptype); + goto lmm_fail; + } + + /* Put the separator back in place */ + *sep = ':'; + } + + /* all good */ + continue; + +lmm_fail: + guc_free(rawstring); + list_free(elemlist); + return false; + } + + /* + * The default log level must be specified. It is the fallback value. + */ + if (defaultlevel == -1) + { + GUC_check_errdetail("Default log level was not defined."); + guc_free(rawstring); + list_free(elemlist); + return false; + } + + /* Apply the default log level to all processes not listed. */ + for (int i = 0; i < BACKEND_NUM_TYPES; i++) + { + if (!assigned[i]) + newlevel[i] = defaultlevel; + } + + /* + * Save an ordered representation of the user-specified string, for the + * show_hook. + */ + list_sort(elemlist, log_min_messages_cmp); + + initStringInfoExt(&buf, strlen(rawstring) + 1); + foreach_ptr(char, elem, elemlist) + { + if (foreach_current_index(elem) == 0) + appendStringInfoString(&buf, elem); + else + appendStringInfo(&buf, ", %s", elem); + } + + result = guc_strdup(LOG, buf.data); + if (!result) + { + pfree(buf.data); + return false; + } + + guc_free(*newval); + *newval = result; + + guc_free(rawstring); + list_free(elemlist); + pfree(buf.data); + + /* + * Pass back data for assign_log_min_messages to use. + */ + *extra = guc_malloc(LOG, BACKEND_NUM_TYPES * sizeof(int)); + if (!*extra) + return false; + memcpy(*extra, newlevel, BACKEND_NUM_TYPES * sizeof(int)); + + return true; +} + +/* + * list_sort() callback for check_log_min_messages. The default element + * goes first; the rest are ordered by strcmp() of the process type. + */ +static int +log_min_messages_cmp(const ListCell *a, const ListCell *b) +{ + const char *s = lfirst(a); + const char *t = lfirst(b); + + if (strchr(s, ':') == NULL) + return -1; + else if (strchr(t, ':') == NULL) + return 1; + else + return strcmp(s, t); +} + +/* + * GUC assign_hook for log_min_messages + */ +void +assign_log_min_messages(const char *newval, void *extra) +{ + for (int i = 0; i < BACKEND_NUM_TYPES; i++) + log_min_messages[i] = ((int *) extra)[i]; +} + /* * GUC check_hook for backtrace_functions * @@ -2192,7 +2628,7 @@ check_backtrace_functions(char **newval, void **extra, GucSource source) return false; } - if (*newval[0] == '\0') + if ((*newval)[0] == '\0') { *extra = NULL; return true; @@ -2497,7 +2933,6 @@ GetACPEncoding(void) static void write_eventlog(int level, const char *line, int len) { - WCHAR *utf16; int eventlevel = EVENTLOG_ERROR_TYPE; static HANDLE evtHandle = INVALID_HANDLE_VALUE; @@ -2531,6 +2966,7 @@ write_eventlog(int level, const char *line, int len) break; case ERROR: case FATAL: + case FATAL_CLIENT_ONLY: case PANIC: default: eventlevel = EVENTLOG_ERROR_TYPE; @@ -2554,9 +2990,13 @@ write_eventlog(int level, const char *line, int len) CurrentMemoryContext != NULL && GetMessageEncoding() != GetACPEncoding()) { + WCHAR *utf16; + utf16 = pgwin32_message_to_UTF16(line, len, NULL); if (utf16) { + const WCHAR *utf16_const = utf16; + ReportEventW(evtHandle, eventlevel, 0, @@ -2564,7 +3004,7 @@ write_eventlog(int level, const char *line, int len) NULL, 1, 0, - (LPCWSTR *) &utf16, + &utf16_const, NULL); /* XXX Try ReportEventA() when ReportEventW() fails? */ @@ -2767,7 +3207,12 @@ get_backend_type_for_log(void) if (MyProcPid == PostmasterPid) backend_type_str = "postmaster"; else if (MyBackendType == B_BG_WORKER) - backend_type_str = MyBgworkerEntry->bgw_type; + { + if (MyBgworkerEntry) + backend_type_str = MyBgworkerEntry->bgw_type; + else + backend_type_str = "early bgworker"; + } else backend_type_str = GetBackendTypeDesc(MyBackendType); @@ -2956,12 +3401,12 @@ log_status_format(StringInfo buf, const char *format, ErrorData *edata) { char strfbuf[128]; - snprintf(strfbuf, sizeof(strfbuf) - 1, INT64_HEX_FORMAT ".%x", + snprintf(strfbuf, sizeof(strfbuf) - 1, "%" PRIx64 ".%x", MyStartTime, MyProcPid); appendStringInfo(buf, "%*s", padding, strfbuf); } else - appendStringInfo(buf, INT64_HEX_FORMAT ".%x", MyStartTime, MyProcPid); + appendStringInfo(buf, "%" PRIx64 ".%x", MyStartTime, MyProcPid); break; case 'p': if (padding != 0) @@ -3357,6 +3802,7 @@ send_message_to_server_log(ErrorData *edata) syslog_level = LOG_WARNING; break; case FATAL: + case FATAL_CLIENT_ONLY: syslog_level = LOG_ERR; break; case PANIC: @@ -3739,6 +4185,7 @@ error_severity(int elevel) prefix = gettext_noop("ERROR"); break; case FATAL: + case FATAL_CLIENT_ONLY: prefix = gettext_noop("FATAL"); break; case PANIC: @@ -3783,13 +4230,24 @@ write_stderr(const char *fmt,...) { va_list ap; + va_start(ap, fmt); + vwrite_stderr(fmt, ap); + va_end(ap); +} + + +/* + * Write errors to stderr (or by equal means when stderr is + * not available) - va_list version + */ +void +vwrite_stderr(const char *fmt, va_list ap) +{ #ifdef WIN32 char errbuf[2048]; /* Arbitrary size? */ #endif fmt = _(fmt); - - va_start(ap, fmt); #ifndef WIN32 /* On Unix, we just fprintf to stderr */ vfprintf(stderr, fmt, ap); @@ -3812,5 +4270,4 @@ write_stderr(const char *fmt,...) fflush(stderr); } #endif - va_end(ap); } diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c index 519eacf17f83c..2ff6a0040463f 100644 --- a/src/backend/utils/error/jsonlog.c +++ b/src/backend/utils/error/jsonlog.c @@ -3,7 +3,7 @@ * jsonlog.c * JSON logging * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -168,7 +168,7 @@ write_jsonlog(ErrorData *edata) } /* Session id */ - appendJSONKeyValueFmt(&buf, "session_id", true, INT64_HEX_FORMAT ".%x", + appendJSONKeyValueFmt(&buf, "session_id", true, "%" PRIx64 ".%x", MyStartTime, MyProcPid); /* Line number */ diff --git a/src/backend/utils/error/meson.build b/src/backend/utils/error/meson.build index 15a502bb6bec2..28b9b6df5c30b 100644 --- a/src/backend/utils/error/meson.build +++ b/src/backend/utils/error/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'assert.c', diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c index 603632581d04a..e636cc81cf8d9 100644 --- a/src/backend/utils/fmgr/dfmgr.c +++ b/src/backend/utils/fmgr/dfmgr.c @@ -3,7 +3,7 @@ * dfmgr.c * Dynamic function manager code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -99,6 +99,21 @@ load_external_function(const char *filename, const char *funcname, void *lib_handle; void *retval; + /* + * For extensions with hardcoded '$libdir/' library names, we strip the + * prefix to allow the library search path to be used. This is done only + * for simple names (e.g., "$libdir/foo"), not for nested paths (e.g., + * "$libdir/foo/bar"). + * + * For nested paths, 'expand_dynamic_library_name' directly expands the + * '$libdir' macro, so we leave them untouched. + */ + if (strncmp(filename, "$libdir/", 8) == 0) + { + if (first_dir_separator(filename + 8) == NULL) + filename += 8; + } + /* Expand the possibly-abbreviated filename to an exact path name */ fullname = expand_dynamic_library_name(filename); @@ -456,14 +471,6 @@ expand_dynamic_library_name(const char *name) Assert(name); - /* - * If the value starts with "$libdir/", strip that. This is because many - * extensions have hardcoded '$libdir/foo' as their library name, which - * prevents using the path. - */ - if (strncmp(name, "$libdir/", 8) == 0) - name += 8; - have_slash = (first_dir_separator(name) != NULL); if (!have_slash) diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c index 782291d999832..bfeceb7a92fc8 100644 --- a/src/backend/utils/fmgr/fmgr.c +++ b/src/backend/utils/fmgr/fmgr.c @@ -3,7 +3,7 @@ * fmgr.c * The Postgres function manager. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,7 @@ #include "postgres.h" #include "access/detoast.h" +#include "access/htup_details.h" #include "catalog/pg_language.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" @@ -30,6 +31,7 @@ #include "utils/builtins.h" #include "utils/fmgrtab.h" #include "utils/guc.h" +#include "utils/hsearch.h" #include "utils/lsyscache.h" #include "utils/syscache.h" @@ -1570,7 +1572,6 @@ InputFunctionCall(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod) * This is basically like InputFunctionCall, but the converted Datum is * returned into *result while the function result is true for success or * false for failure. Also, the caller may pass an ErrorSaveContext node. - * (We declare that as "fmNodePtr" to avoid including nodes.h in fmgr.h.) * * If escontext points to an ErrorSaveContext, any "soft" errors detected by * the input function will be reported by filling the escontext struct and @@ -1584,7 +1585,7 @@ InputFunctionCall(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod) bool InputFunctionCallSafe(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod, - fmNodePtr escontext, + Node *escontext, Datum *result) { LOCAL_FCINFO(fcinfo, 3); @@ -1639,7 +1640,7 @@ InputFunctionCallSafe(FmgrInfo *flinfo, char *str, bool DirectInputFunctionCallSafe(PGFunction func, char *str, Oid typioparam, int32 typmod, - fmNodePtr escontext, + Node *escontext, Datum *result) { LOCAL_FCINFO(fcinfo, 3); @@ -1788,48 +1789,13 @@ OidSendFunctionCall(Oid functionId, Datum val) } -/*------------------------------------------------------------------------- - * Support routines for standard maybe-pass-by-reference datatypes - * - * int8 and float8 can be passed by value if Datum is wide enough. - * (For backwards-compatibility reasons, we allow pass-by-ref to be chosen - * at compile time even if pass-by-val is possible.) - * - * Note: there is only one switch controlling the pass-by-value option for - * both int8 and float8; this is to avoid making things unduly complicated - * for the timestamp types, which might have either representation. - *------------------------------------------------------------------------- - */ - -#ifndef USE_FLOAT8_BYVAL /* controls int8 too */ - -Datum -Int64GetDatum(int64 X) -{ - int64 *retval = (int64 *) palloc(sizeof(int64)); - - *retval = X; - return PointerGetDatum(retval); -} - -Datum -Float8GetDatum(float8 X) -{ - float8 *retval = (float8 *) palloc(sizeof(float8)); - - *retval = X; - return PointerGetDatum(retval); -} -#endif /* USE_FLOAT8_BYVAL */ - - /*------------------------------------------------------------------------- * Support routines for toastable datatypes *------------------------------------------------------------------------- */ -struct varlena * -pg_detoast_datum(struct varlena *datum) +varlena * +pg_detoast_datum(varlena *datum) { if (VARATT_IS_EXTENDED(datum)) return detoast_attr(datum); @@ -1837,8 +1803,8 @@ pg_detoast_datum(struct varlena *datum) return datum; } -struct varlena * -pg_detoast_datum_copy(struct varlena *datum) +varlena * +pg_detoast_datum_copy(varlena *datum) { if (VARATT_IS_EXTENDED(datum)) return detoast_attr(datum); @@ -1846,22 +1812,22 @@ pg_detoast_datum_copy(struct varlena *datum) { /* Make a modifiable copy of the varlena object */ Size len = VARSIZE(datum); - struct varlena *result = (struct varlena *) palloc(len); + varlena *result = (varlena *) palloc(len); memcpy(result, datum, len); return result; } } -struct varlena * -pg_detoast_datum_slice(struct varlena *datum, int32 first, int32 count) +varlena * +pg_detoast_datum_slice(varlena *datum, int32 first, int32 count) { /* Only get the specified portion from the toast rel */ return detoast_attr_slice(datum, first, count); } -struct varlena * -pg_detoast_datum_packed(struct varlena *datum) +varlena * +pg_detoast_datum_packed(varlena *datum) { if (VARATT_IS_COMPRESSED(datum) || VARATT_IS_EXTERNAL(datum)) return detoast_attr(datum); diff --git a/src/backend/utils/fmgr/funcapi.c b/src/backend/utils/fmgr/funcapi.c index 5f2317211c9d4..6f0785067b8a6 100644 --- a/src/backend/utils/fmgr/funcapi.c +++ b/src/backend/utils/fmgr/funcapi.c @@ -4,7 +4,7 @@ * Utility and convenience functions for fmgr functions that return * sets and/or composite types, or deal with VARIADIC inputs. * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/fmgr/funcapi.c @@ -73,7 +73,7 @@ static TypeFuncClass get_type_func_class(Oid typid, Oid *base_typeid); * RECORD datatype. */ void -InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags) +InitMaterializedSRF(FunctionCallInfo fcinfo, uint32 flags) { bool random_access; ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; @@ -340,6 +340,8 @@ get_expr_result_type(Node *expr, exprCollation(col)); i++; } + TupleDescFinalize(tupdesc); + if (resultTypeId) *resultTypeId = rexpr->row_typeid; if (resultTupleDesc) @@ -1044,6 +1046,7 @@ resolve_polymorphic_tupdesc(TupleDesc tupdesc, oidvector *declared_args, } } + TupleDescFinalize(tupdesc); return true; } @@ -1436,7 +1439,7 @@ get_func_arg_info(HeapTuple procTup, &elems, NULL, &nelems); if (nelems != numargs) /* should not happen */ elog(ERROR, "proargnames must have the same number of elements as the function has arguments"); - *p_argnames = (char **) palloc(sizeof(char *) * numargs); + *p_argnames = palloc_array(char *, numargs); for (i = 0; i < numargs; i++) (*p_argnames)[i] = TextDatumGetCString(elems[i]); } @@ -1853,6 +1856,8 @@ build_function_result_tupdesc_d(char prokind, 0); } + TupleDescFinalize(desc); + return desc; } @@ -1970,6 +1975,7 @@ TypeGetTupleDesc(Oid typeoid, List *colaliases) typeoid, -1, 0); + TupleDescFinalize(tupdesc); } else if (functypclass == TYPEFUNC_RECORD) { diff --git a/src/backend/utils/fmgr/meson.build b/src/backend/utils/fmgr/meson.build index b1dcab93e7093..3a90deba979e2 100644 --- a/src/backend/utils/fmgr/meson.build +++ b/src/backend/utils/fmgr/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'dfmgr.c', diff --git a/src/backend/utils/generate-errcodes.pl b/src/backend/utils/generate-errcodes.pl index 95dc365af1d28..cac7278e14092 100644 --- a/src/backend/utils/generate-errcodes.pl +++ b/src/backend/utils/generate-errcodes.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate the errcodes.h header from errcodes.txt -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/backend/utils/hash/dynahash.c b/src/backend/utils/hash/dynahash.c index 1ad155d446e51..dc7ae64a5a98f 100644 --- a/src/backend/utils/hash/dynahash.c +++ b/src/backend/utils/hash/dynahash.c @@ -22,10 +22,11 @@ * lookup key's hash value as a partition number --- this will work because * of the way calc_bucket() maps hash values to bucket numbers. * - * For hash tables in shared memory, the memory allocator function should - * match malloc's semantics of returning NULL on failure. For hash tables - * in local memory, we typically use palloc() which will throw error on - * failure. The code in this file has to cope with both cases. + * The memory allocator function should match malloc's semantics of returning + * NULL on failure. (This is essential for hash tables in shared memory. + * For hash tables in local memory, we used to use palloc() which will throw + * error on failure; but we no longer do, so it's untested whether this + * module will still cope with that behavior.) * * dynahash.c provides support for these types of lookup keys: * @@ -52,7 +53,7 @@ * dynahash has better performance for large entries. * - Guarantees stable pointers to entries. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -79,9 +80,8 @@ * are not implemented; otherwise functionality is identical. * * Compilation controls: - * HASH_DEBUG controls some informative traces, mainly for debugging. - * HASH_STATISTICS causes HashAccesses and HashCollisions to be maintained; - * when combined with HASH_DEBUG, these are displayed by hdestroy(). + * HASH_STATISTICS causes some usage statistics to be maintained, which can be + * logged by calling hash_stats(). * * Problems & fixes to ejp@ausmelb.oz. WARNING: relies on pre-processor * concatenation property, in probably unnecessary code 'optimization'. @@ -98,30 +98,32 @@ #include "access/xact.h" #include "common/hashfn.h" +#include "lib/ilist.h" #include "port/pg_bitutils.h" #include "storage/shmem.h" #include "storage/spin.h" -#include "utils/dynahash.h" #include "utils/memutils.h" /* * Constants * - * A hash table has a top-level "directory", each of whose entries points - * to a "segment" of ssize bucket headers. The maximum number of hash - * buckets is thus dsize * ssize (but dsize may be expansible). Of course, - * the number of records in the table can be larger, but we don't want a - * whole lot of records per bucket or performance goes down. + * A hash table has a top-level "directory", each of whose entries points to a + * "segment" of HASH_SEGSIZE bucket headers. The maximum number of hash + * buckets is thus dsize * HASH_SEGSIZE (but dsize may be expansible). Of + * course, the number of records in the table can be larger, but we don't want + * a whole lot of records per bucket or performance goes down. * * In a hash table allocated in shared memory, the directory cannot be - * expanded because it must stay at a fixed address. The directory size - * should be selected using hash_select_dirsize (and you'd better have - * a good idea of the maximum number of entries!). For non-shared hash - * tables, the initial directory size can be left at the default. + * expanded because it must stay at a fixed address. The directory size is + * chosen at creation based on the initial number of elements, so even though + * we support allocating more elements later, performance will suffer if the + * table grows much beyond the initial size. (Currently, shared memory hash + * tables are only created by ShmemRequestHash()/ShmemInitHash() though, which + * doesn't support growing at all.) */ -#define DEF_SEGSIZE 256 -#define DEF_SEGSIZE_SHIFT 8 /* must be log2(DEF_SEGSIZE) */ +#define HASH_SEGSIZE 256 +#define HASH_SEGSIZE_SHIFT 8 /* must be log2(HASH_SEGSIZE) */ #define DEF_DIRSIZE 256 /* Number of freelists to be used for a partitioned hash table. */ @@ -153,7 +155,7 @@ typedef HASHBUCKET *HASHSEGMENT; typedef struct { slock_t mutex; /* spinlock for this freelist */ - long nentries; /* number of entries in associated buckets */ + int64 nentries; /* number of entries in associated buckets */ HASHELEMENT *freeList; /* chain of free elements */ } FreeListData; @@ -181,8 +183,8 @@ struct HASHHDR /* These fields can change, but not in a partitioned table */ /* Also, dsize can't change in a shared table, even if unpartitioned */ - long dsize; /* directory size */ - long nsegs; /* number of allocated segments (<= dsize) */ + int64 dsize; /* directory size */ + int64 nsegs; /* number of allocated segments (<= dsize) */ uint32 max_bucket; /* ID of maximum bucket in use */ uint32 high_mask; /* mask to modulo into entire table */ uint32 low_mask; /* mask to modulo into lower half of table */ @@ -190,11 +192,13 @@ struct HASHHDR /* These fields are fixed at hashtable creation */ Size keysize; /* hash key length in bytes */ Size entrysize; /* total user element size in bytes */ - long num_partitions; /* # partitions (must be power of 2), or 0 */ - long max_dsize; /* 'dsize' limit if directory is fixed size */ - long ssize; /* segment size --- must be power of 2 */ - int sshift; /* segment shift = log2(ssize) */ + int64 num_partitions; /* # partitions (must be power of 2), or 0 */ + int64 max_dsize; /* 'dsize' limit if directory is fixed size */ int nelem_alloc; /* number of entries to allocate at once */ + bool isfixed; /* if true, don't enlarge */ + + /* Current directory. In shared tables, this doesn't change */ + HASHSEGMENT *dir; #ifdef HASH_STATISTICS @@ -202,8 +206,9 @@ struct HASHHDR * Count statistics here. NB: stats code doesn't bother with mutex, so * counts could be corrupted a bit in a partitioned table. */ - long accesses; - long collisions; + uint64 accesses; + uint64 collisions; + uint64 expansions; #endif }; @@ -224,18 +229,26 @@ struct HTAB HashCompareFunc match; /* key comparison function */ HashCopyFunc keycopy; /* key copying function */ HashAllocFunc alloc; /* memory allocator */ + void *alloc_arg; /* opaque argument passed to allocator */ MemoryContext hcxt; /* memory context if default allocator used */ char *tabname; /* table name (for error messages) */ bool isshared; /* true if table is in shared memory */ - bool isfixed; /* if true, don't enlarge */ /* freezing a shared table isn't allowed, so we can keep state here */ bool frozen; /* true = no more inserts allowed */ /* We keep local copies of these fixed values to reduce contention */ Size keysize; /* hash key length in bytes */ - long ssize; /* segment size --- must be power of 2 */ - int sshift; /* segment shift = log2(ssize) */ + + /* + * In a USE_VALGRIND build, non-shared hashtables keep an slist chain of + * all the element blocks they have allocated. This pacifies Valgrind, + * which would otherwise often claim that the element blocks are "possibly + * lost" for lack of any non-interior pointers to their starts. + */ +#ifdef USE_VALGRIND + slist_head element_blocks; +#endif }; /* @@ -254,16 +267,10 @@ struct HTAB */ #define MOD(x,y) ((x) & ((y)-1)) -#ifdef HASH_STATISTICS -static long hash_accesses, - hash_collisions, - hash_expansions; -#endif - /* * Private function prototypes */ -static void *DynaHashAlloc(Size size); +static void *DynaHashAlloc(Size size, void *alloc_arg); static HASHSEGMENT seg_alloc(HTAB *hashp); static bool element_alloc(HTAB *hashp, int nelem, int freelist_idx); static bool dir_realloc(HTAB *hashp); @@ -271,12 +278,13 @@ static bool expand_table(HTAB *hashp); static HASHBUCKET get_hash_entry(HTAB *hashp, int freelist_idx); static void hdefault(HTAB *hashp); static int choose_nelem_alloc(Size entrysize); -static bool init_htab(HTAB *hashp, long nelem); +static bool init_htab(HTAB *hashp, int64 nelem); pg_noreturn static void hash_corrupted(HTAB *hashp); static uint32 hash_initial_lookup(HTAB *hashp, uint32 hashvalue, HASHBUCKET **bucketptr); -static long next_pow2_long(long num); -static int next_pow2_int(long num); +static int my_log2(int64 num); +static int64 next_pow2_int64(int64 num); +static int next_pow2_int(int64 num); static void register_seq_scan(HTAB *hashp); static void deregister_seq_scan(HTAB *hashp); static bool has_seq_scans(HTAB *hashp); @@ -285,14 +293,13 @@ static bool has_seq_scans(HTAB *hashp); /* * memory allocation support */ -static MemoryContext CurrentDynaHashCxt = NULL; - static void * -DynaHashAlloc(Size size) +DynaHashAlloc(Size size, void *alloc_arg) { - Assert(MemoryContextIsValid(CurrentDynaHashCxt)); - return MemoryContextAllocExtended(CurrentDynaHashCxt, size, - MCXT_ALLOC_NO_OOM); + MemoryContext hcxt = (MemoryContext) alloc_arg; + + Assert(MemoryContextIsValid(hcxt)); + return MemoryContextAllocExtended(hcxt, size, MCXT_ALLOC_NO_OOM); } @@ -331,7 +338,8 @@ string_compare(const char *key1, const char *key2, Size keysize) * under info->hcxt rather than under TopMemoryContext; the default * behavior is only suitable for session-lifespan hash tables. * Other flags bits are special-purpose and seldom used, except for those - * associated with shared-memory hash tables, for which see ShmemInitHash(). + * associated with shared-memory hash tables, for which see + * ShmemRequestHash(). * * Fields in *info are read only when the associated flags bit is set. * It is not necessary to initialize other fields of *info. @@ -349,10 +357,11 @@ string_compare(const char *key1, const char *key2, Size keysize) * large nelem will penalize hash_seq_search speed without buying much. */ HTAB * -hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) +hash_create(const char *tabname, int64 nelem, const HASHCTL *info, int flags) { HTAB *hashp; HASHHDR *hctl; + MemoryContext hcxt; /* * Hash tables now allocate space for key and data, but you have to say @@ -371,26 +380,27 @@ hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) * hash_destroy very simple. The memory context is made a child of either * a context specified by the caller, or TopMemoryContext if nothing is * specified. + * + * Note that HASH_ALLOC had better be set as well. */ if (flags & HASH_SHARED_MEM) { /* Set up to allocate the hash header */ - CurrentDynaHashCxt = TopMemoryContext; + hcxt = TopMemoryContext; } else { /* Create the hash table's private memory context */ if (flags & HASH_CONTEXT) - CurrentDynaHashCxt = info->hcxt; + hcxt = info->hcxt; else - CurrentDynaHashCxt = TopMemoryContext; - CurrentDynaHashCxt = AllocSetContextCreate(CurrentDynaHashCxt, - "dynahash", - ALLOCSET_DEFAULT_SIZES); + hcxt = TopMemoryContext; + hcxt = AllocSetContextCreate(hcxt, "dynahash", + ALLOCSET_DEFAULT_SIZES); } /* Initialize the hash header, plus a copy of the table name */ - hashp = (HTAB *) MemoryContextAlloc(CurrentDynaHashCxt, + hashp = (HTAB *) MemoryContextAlloc(hcxt, sizeof(HTAB) + strlen(tabname) + 1); MemSet(hashp, 0, sizeof(HTAB)); @@ -399,7 +409,7 @@ hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) /* If we have a private context, label it with hashtable's name */ if (!(flags & HASH_SHARED_MEM)) - MemoryContextSetIdentifier(CurrentDynaHashCxt, hashp->tabname); + MemoryContextSetIdentifier(hcxt, hashp->tabname); /* * Select the appropriate hash function (see comments at head of file). @@ -471,30 +481,31 @@ hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) /* And select the entry allocation function, too. */ if (flags & HASH_ALLOC) + { hashp->alloc = info->alloc; + hashp->alloc_arg = info->alloc_arg; + } else + { hashp->alloc = DynaHashAlloc; + hashp->alloc_arg = hcxt; + } if (flags & HASH_SHARED_MEM) { - /* - * ctl structure and directory are preallocated for shared memory - * tables. Note that HASH_DIRSIZE and HASH_ALLOC had better be set as - * well. - */ - hashp->hctl = info->hctl; - hashp->dir = (HASHSEGMENT *) (((char *) info->hctl) + sizeof(HASHHDR)); hashp->hcxt = NULL; hashp->isshared = true; /* hash table already exists, we're just attaching to it */ if (flags & HASH_ATTACH) { + /* Caller must pass the pointer to the shared header */ + Assert(info->hctl); + hashp->hctl = info->hctl; + /* make local copies of some heavily-used values */ - hctl = hashp->hctl; - hashp->keysize = hctl->keysize; - hashp->ssize = hctl->ssize; - hashp->sshift = hctl->sshift; + hashp->dir = info->hctl->dir; + hashp->keysize = info->hctl->keysize; return hashp; } @@ -504,18 +515,24 @@ hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) /* setup hash table defaults */ hashp->hctl = NULL; hashp->dir = NULL; - hashp->hcxt = CurrentDynaHashCxt; + hashp->hcxt = hcxt; hashp->isshared = false; } + /* + * Allocate the header structure. + * + * XXX: In case of a shared memory hash table, other processes need the + * pointer to the header to re-find the hash table. There is currently no + * explicit way to pass it back from here, the caller relies on the fact + * that this is the first allocation made with the alloc function. That's + * a little ugly, but works for now. + */ + hashp->hctl = (HASHHDR *) hashp->alloc(sizeof(HASHHDR), hashp->alloc_arg); if (!hashp->hctl) - { - hashp->hctl = (HASHHDR *) hashp->alloc(sizeof(HASHHDR)); - if (!hashp->hctl) - ereport(ERROR, - (errcode(ERRCODE_OUT_OF_MEMORY), - errmsg("out of memory"))); - } + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); hashp->frozen = false; @@ -538,31 +555,12 @@ hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) hctl->num_partitions = info->num_partitions; } - if (flags & HASH_SEGMENT) - { - hctl->ssize = info->ssize; - hctl->sshift = my_log2(info->ssize); - /* ssize had better be a power of 2 */ - Assert(hctl->ssize == (1L << hctl->sshift)); - } - - /* - * SHM hash tables have fixed directory size passed by the caller. - */ - if (flags & HASH_DIRSIZE) - { - hctl->max_dsize = info->max_dsize; - hctl->dsize = info->dsize; - } - /* remember the entry sizes, too */ hctl->keysize = info->keysize; hctl->entrysize = info->entrysize; /* make local copies of heavily-used constant fields */ hashp->keysize = hctl->keysize; - hashp->ssize = hctl->ssize; - hashp->sshift = hctl->sshift; /* Build the hash directory structure */ if (!init_htab(hashp, nelem)) @@ -618,8 +616,10 @@ hash_create(const char *tabname, long nelem, const HASHCTL *info, int flags) } } + /* Set isfixed if requested, but not till after we build initial entries */ if (flags & HASH_FIXED_SIZE) - hashp->isfixed = true; + hctl->isfixed = true; + return hashp; } @@ -633,19 +633,15 @@ hdefault(HTAB *hashp) MemSet(hctl, 0, sizeof(HASHHDR)); - hctl->dsize = DEF_DIRSIZE; - hctl->nsegs = 0; - hctl->num_partitions = 0; /* not partitioned */ /* table has no fixed maximum size */ hctl->max_dsize = NO_MAX_DSIZE; - hctl->ssize = DEF_SEGSIZE; - hctl->sshift = DEF_SEGSIZE_SHIFT; + hctl->isfixed = false; /* can be enlarged */ #ifdef HASH_STATISTICS - hctl->accesses = hctl->collisions = 0; + hctl->accesses = hctl->collisions = hctl->expansions = 0; #endif } @@ -687,7 +683,7 @@ choose_nelem_alloc(Size entrysize) * arrays */ static bool -init_htab(HTAB *hashp, long nelem) +init_htab(HTAB *hashp, int64 nelem) { HASHHDR *hctl = hashp->hctl; HASHSEGMENT *segp; @@ -723,30 +719,24 @@ init_htab(HTAB *hashp, long nelem) /* * Figure number of directory segments needed, round up to a power of 2 */ - nsegs = (nbuckets - 1) / hctl->ssize + 1; + nsegs = (nbuckets - 1) / HASH_SEGSIZE + 1; nsegs = next_pow2_int(nsegs); /* - * Make sure directory is big enough. If pre-allocated directory is too - * small, choke (caller screwed up). + * Make sure directory is big enough. */ - if (nsegs > hctl->dsize) - { - if (!(hashp->dir)) - hctl->dsize = nsegs; - else - return false; - } + hctl->dsize = Max(DEF_DIRSIZE, nsegs); + + /* SHM hash tables have a fixed directory. */ + if (hashp->isshared) + hctl->max_dsize = hctl->dsize; /* Allocate a directory */ - if (!(hashp->dir)) - { - CurrentDynaHashCxt = hashp->hcxt; - hashp->dir = (HASHSEGMENT *) - hashp->alloc(hctl->dsize * sizeof(HASHSEGMENT)); - if (!hashp->dir) - return false; - } + hctl->dir = (HASHSEGMENT *) + hashp->alloc(hctl->dsize * sizeof(HASHSEGMENT), hashp->alloc_arg); + if (!hctl->dir) + return false; + hashp->dir = hctl->dir; /* Allocate initial segments */ for (segp = hashp->dir; hctl->nsegs < nsegs; hctl->nsegs++, segp++) @@ -759,17 +749,6 @@ init_htab(HTAB *hashp, long nelem) /* Choose number of entries to allocate at a time */ hctl->nelem_alloc = choose_nelem_alloc(hctl->entrysize); -#ifdef HASH_DEBUG - fprintf(stderr, "init_htab:\n%s%p\n%s%ld\n%s%ld\n%s%d\n%s%ld\n%s%u\n%s%x\n%s%x\n%s%ld\n", - "TABLE POINTER ", hashp, - "DIRECTORY SIZE ", hctl->dsize, - "SEGMENT SIZE ", hctl->ssize, - "SEGMENT SHIFT ", hctl->sshift, - "MAX BUCKET ", hctl->max_bucket, - "HIGH MASK ", hctl->high_mask, - "LOW MASK ", hctl->low_mask, - "NSEGS ", hctl->nsegs); -#endif return true; } @@ -781,10 +760,10 @@ init_htab(HTAB *hashp, long nelem) * NB: assumes that all hash structure parameters have default values! */ Size -hash_estimate_size(long num_entries, Size entrysize) +hash_estimate_size(int64 num_entries, Size entrysize) { Size size; - long nBuckets, + int64 nBuckets, nSegments, nDirEntries, nElementAllocs, @@ -792,13 +771,11 @@ hash_estimate_size(long num_entries, Size entrysize) elementAllocCnt; /* estimate number of buckets wanted */ - nBuckets = next_pow2_long(num_entries); + nBuckets = next_pow2_int64(num_entries); /* # of segments needed for nBuckets */ - nSegments = next_pow2_long((nBuckets - 1) / DEF_SEGSIZE + 1); + nSegments = next_pow2_int64((nBuckets - 1) / HASH_SEGSIZE + 1); /* directory entries */ - nDirEntries = DEF_DIRSIZE; - while (nDirEntries < nSegments) - nDirEntries <<= 1; /* dir_alloc doubles dsize at each call */ + nDirEntries = Max(DEF_DIRSIZE, nSegments); /* fixed control info */ size = MAXALIGN(sizeof(HASHHDR)); /* but not HTAB, per above */ @@ -806,7 +783,7 @@ hash_estimate_size(long num_entries, Size entrysize) size = add_size(size, mul_size(nDirEntries, sizeof(HASHSEGMENT))); /* segments */ size = add_size(size, mul_size(nSegments, - MAXALIGN(DEF_SEGSIZE * sizeof(HASHBUCKET)))); + MAXALIGN(HASH_SEGSIZE * sizeof(HASHBUCKET)))); /* elements --- allocated in groups of choose_nelem_alloc() entries */ elementAllocCnt = choose_nelem_alloc(entrysize); nElementAllocs = (num_entries - 1) / elementAllocCnt + 1; @@ -818,47 +795,6 @@ hash_estimate_size(long num_entries, Size entrysize) return size; } -/* - * Select an appropriate directory size for a hashtable with the given - * maximum number of entries. - * This is only needed for hashtables in shared memory, whose directories - * cannot be expanded dynamically. - * NB: assumes that all hash structure parameters have default values! - * - * XXX this had better agree with the behavior of init_htab()... - */ -long -hash_select_dirsize(long num_entries) -{ - long nBuckets, - nSegments, - nDirEntries; - - /* estimate number of buckets wanted */ - nBuckets = next_pow2_long(num_entries); - /* # of segments needed for nBuckets */ - nSegments = next_pow2_long((nBuckets - 1) / DEF_SEGSIZE + 1); - /* directory entries */ - nDirEntries = DEF_DIRSIZE; - while (nDirEntries < nSegments) - nDirEntries <<= 1; /* dir_alloc doubles dsize at each call */ - - return nDirEntries; -} - -/* - * Compute the required initial memory allocation for a shared-memory - * hashtable with the given parameters. We need space for the HASHHDR - * and for the (non expansible) directory. - */ -Size -hash_get_shared_size(HASHCTL *info, int flags) -{ - Assert(flags & HASH_DIRSIZE); - Assert(info->dsize == info->max_dsize); - return sizeof(HASHHDR) + info->dsize * sizeof(HASHSEGMENT); -} - /********************** DESTROY ROUTINES ************************/ @@ -872,7 +808,7 @@ hash_destroy(HTAB *hashp) /* so this hashtable must have its own context */ Assert(hashp->hcxt != NULL); - hash_stats("destroy", hashp); + hash_stats(__func__, hashp); /* * Free everything by destroying the hash table's memory context. @@ -882,19 +818,16 @@ hash_destroy(HTAB *hashp) } void -hash_stats(const char *where, HTAB *hashp) +hash_stats(const char *caller, HTAB *hashp) { #ifdef HASH_STATISTICS - fprintf(stderr, "%s: this HTAB -- accesses %ld collisions %ld\n", - where, hashp->hctl->accesses, hashp->hctl->collisions); - - fprintf(stderr, "hash_stats: entries %ld keysize %ld maxp %u segmentcount %ld\n", - hash_get_num_entries(hashp), (long) hashp->hctl->keysize, - hashp->hctl->max_bucket, hashp->hctl->nsegs); - fprintf(stderr, "%s: total accesses %ld total collisions %ld\n", - where, hash_accesses, hash_collisions); - fprintf(stderr, "hash_stats: total expansions %ld\n", - hash_expansions); + HASHHDR *hctl = hashp->hctl; + + elog(DEBUG4, + "hash_stats: Caller: %s Table Name: \"%s\" Accesses: " UINT64_FORMAT " Collisions: " UINT64_FORMAT " Expansions: " UINT64_FORMAT " Entries: " INT64_FORMAT " Key Size: %zu Max Bucket: %u Segment Count: " INT64_FORMAT, + caller != NULL ? caller : "(unknown)", hashp->tabname, hctl->accesses, + hctl->collisions, hctl->expansions, hash_get_num_entries(hashp), + hctl->keysize, hctl->max_bucket, hctl->nsegs); #endif } @@ -980,7 +913,6 @@ hash_search_with_hash_value(HTAB *hashp, HashCompareFunc match; #ifdef HASH_STATISTICS - hash_accesses++; hctl->accesses++; #endif @@ -998,7 +930,7 @@ hash_search_with_hash_value(HTAB *hashp, * Can't split if running in partitioned mode, nor if frozen, nor if * table is the subject of any active hash_seq_search scans. */ - if (hctl->freeList[0].nentries > (long) hctl->max_bucket && + if (hctl->freeList[0].nentries > (int64) hctl->max_bucket && !IS_PARTITIONED(hctl) && !hashp->frozen && !has_seq_scans(hashp)) (void) expand_table(hashp); @@ -1024,7 +956,6 @@ hash_search_with_hash_value(HTAB *hashp, prevBucketPtr = &(currBucket->link); currBucket = *prevBucketPtr; #ifdef HASH_STATISTICS - hash_collisions++; hctl->collisions++; #endif } @@ -1158,7 +1089,8 @@ hash_update_hash_key(HTAB *hashp, HashCompareFunc match; #ifdef HASH_STATISTICS - hash_accesses++; + HASHHDR *hctl = hashp->hctl; + hctl->accesses++; #endif @@ -1212,7 +1144,6 @@ hash_update_hash_key(HTAB *hashp, prevBucketPtr = &(currBucket->link); currBucket = *prevBucketPtr; #ifdef HASH_STATISTICS - hash_collisions++; hctl->collisions++; #endif } @@ -1338,11 +1269,11 @@ get_hash_entry(HTAB *hashp, int freelist_idx) /* * hash_get_num_entries -- get the number of entries in a hashtable */ -long +int64 hash_get_num_entries(HTAB *hashp) { int i; - long sum = hashp->hctl->freeList[0].nentries; + int64 sum = hashp->hctl->freeList[0].nentries; /* * We currently don't bother with acquiring the mutexes; it's only @@ -1423,9 +1354,8 @@ hash_seq_search(HASH_SEQ_STATUS *status) HTAB *hashp; HASHHDR *hctl; uint32 max_bucket; - long ssize; - long segment_num; - long segment_ndx; + int64 segment_num; + int64 segment_ndx; HASHSEGMENT segp; uint32 curBucket; HASHELEMENT *curElem; @@ -1463,7 +1393,6 @@ hash_seq_search(HASH_SEQ_STATUS *status) curBucket = status->curBucket; hashp = status->hashp; hctl = hashp->hctl; - ssize = hashp->ssize; max_bucket = hctl->max_bucket; if (curBucket > max_bucket) @@ -1475,8 +1404,8 @@ hash_seq_search(HASH_SEQ_STATUS *status) /* * first find the right segment in the table directory. */ - segment_num = curBucket >> hashp->sshift; - segment_ndx = MOD(curBucket, ssize); + segment_num = curBucket >> HASH_SEGSIZE_SHIFT; + segment_ndx = MOD(curBucket, HASH_SEGSIZE); segp = hashp->dir[segment_num]; @@ -1495,7 +1424,7 @@ hash_seq_search(HASH_SEQ_STATUS *status) hash_seq_term(status); return NULL; /* search is done */ } - if (++segment_ndx >= ssize) + if (++segment_ndx >= HASH_SEGSIZE) { segment_num++; segment_ndx = 0; @@ -1554,11 +1483,11 @@ expand_table(HTAB *hashp) HASHHDR *hctl = hashp->hctl; HASHSEGMENT old_seg, new_seg; - long old_bucket, + int64 old_bucket, new_bucket; - long new_segnum, + int64 new_segnum, new_segndx; - long old_segnum, + int64 old_segnum, old_segndx; HASHBUCKET *oldlink, *newlink; @@ -1568,12 +1497,12 @@ expand_table(HTAB *hashp) Assert(!IS_PARTITIONED(hctl)); #ifdef HASH_STATISTICS - hash_expansions++; + hctl->expansions++; #endif new_bucket = hctl->max_bucket + 1; - new_segnum = new_bucket >> hashp->sshift; - new_segndx = MOD(new_bucket, hashp->ssize); + new_segnum = new_bucket >> HASH_SEGSIZE_SHIFT; + new_segndx = MOD(new_bucket, HASH_SEGSIZE); if (new_segnum >= hctl->nsegs) { @@ -1612,8 +1541,8 @@ expand_table(HTAB *hashp) * split at this point. With a different way of reducing the hash value, * that might not be true! */ - old_segnum = old_bucket >> hashp->sshift; - old_segndx = MOD(old_bucket, hashp->ssize); + old_segnum = old_bucket >> HASH_SEGSIZE_SHIFT; + old_segndx = MOD(old_bucket, HASH_SEGSIZE); old_seg = hashp->dir[old_segnum]; new_seg = hashp->dir[new_segnum]; @@ -1626,7 +1555,7 @@ expand_table(HTAB *hashp) currElement = nextElement) { nextElement = currElement->link; - if ((long) calc_bucket(hctl, currElement->hashvalue) == old_bucket) + if ((int64) calc_bucket(hctl, currElement->hashvalue) == old_bucket) { *oldlink = currElement; oldlink = &currElement->link; @@ -1650,9 +1579,9 @@ dir_realloc(HTAB *hashp) { HASHSEGMENT *p; HASHSEGMENT *old_p; - long new_dsize; - long old_dirsize; - long new_dirsize; + int64 new_dsize; + int64 old_dirsize; + int64 new_dirsize; if (hashp->hctl->max_dsize != NO_MAX_DSIZE) return false; @@ -1663,13 +1592,13 @@ dir_realloc(HTAB *hashp) new_dirsize = new_dsize * sizeof(HASHSEGMENT); old_p = hashp->dir; - CurrentDynaHashCxt = hashp->hcxt; - p = (HASHSEGMENT *) hashp->alloc((Size) new_dirsize); + p = (HASHSEGMENT *) hashp->alloc((Size) new_dirsize, hashp->alloc_arg); if (p != NULL) { memcpy(p, old_p, old_dirsize); MemSet(((char *) p) + old_dirsize, 0, new_dirsize - old_dirsize); + hashp->hctl->dir = p; hashp->dir = p; hashp->hctl->dsize = new_dsize; @@ -1689,13 +1618,12 @@ seg_alloc(HTAB *hashp) { HASHSEGMENT segp; - CurrentDynaHashCxt = hashp->hcxt; - segp = (HASHSEGMENT) hashp->alloc(sizeof(HASHBUCKET) * hashp->ssize); + segp = (HASHSEGMENT) hashp->alloc(sizeof(HASHBUCKET) * HASH_SEGSIZE, hashp->alloc_arg); if (!segp) return NULL; - MemSet(segp, 0, sizeof(HASHBUCKET) * hashp->ssize); + MemSet(segp, 0, sizeof(HASHBUCKET) * HASH_SEGSIZE); return segp; } @@ -1708,23 +1636,50 @@ element_alloc(HTAB *hashp, int nelem, int freelist_idx) { HASHHDR *hctl = hashp->hctl; Size elementSize; + Size requestSize; + char *allocedBlock; HASHELEMENT *firstElement; HASHELEMENT *tmpElement; HASHELEMENT *prevElement; int i; - if (hashp->isfixed) + if (hctl->isfixed) return false; /* Each element has a HASHELEMENT header plus user data. */ elementSize = MAXALIGN(sizeof(HASHELEMENT)) + MAXALIGN(hctl->entrysize); - CurrentDynaHashCxt = hashp->hcxt; - firstElement = (HASHELEMENT *) hashp->alloc(nelem * elementSize); + requestSize = nelem * elementSize; - if (!firstElement) + /* Add space for slist_node list link if we need one. */ +#ifdef USE_VALGRIND + if (!hashp->isshared) + requestSize += MAXALIGN(sizeof(slist_node)); +#endif + + /* Allocate the memory. */ + allocedBlock = hashp->alloc(requestSize, hashp->alloc_arg); + + if (!allocedBlock) return false; + /* + * If USE_VALGRIND, each allocated block of elements of a non-shared + * hashtable is chained into a list, so that Valgrind won't think it's + * been leaked. + */ +#ifdef USE_VALGRIND + if (hashp->isshared) + firstElement = (HASHELEMENT *) allocedBlock; + else + { + slist_push_head(&hashp->element_blocks, (slist_node *) allocedBlock); + firstElement = (HASHELEMENT *) (allocedBlock + MAXALIGN(sizeof(slist_node))); + } +#else + firstElement = (HASHELEMENT *) allocedBlock; +#endif + /* prepare to link all the new entries into the freelist */ prevElement = NULL; tmpElement = firstElement; @@ -1758,14 +1713,14 @@ hash_initial_lookup(HTAB *hashp, uint32 hashvalue, HASHBUCKET **bucketptr) { HASHHDR *hctl = hashp->hctl; HASHSEGMENT segp; - long segment_num; - long segment_ndx; + int64 segment_num; + int64 segment_ndx; uint32 bucket; bucket = calc_bucket(hctl, hashvalue); - segment_num = bucket >> hashp->sshift; - segment_ndx = MOD(bucket, hashp->ssize); + segment_num = bucket >> HASH_SEGSIZE_SHIFT; + segment_ndx = MOD(bucket, HASH_SEGSIZE); segp = hashp->dir[segment_num]; @@ -1791,34 +1746,30 @@ hash_corrupted(HTAB *hashp) } /* calculate ceil(log base 2) of num */ -int -my_log2(long num) +static int +my_log2(int64 num) { /* * guard against too-large input, which would be invalid for * pg_ceil_log2_*() */ - if (num > LONG_MAX / 2) - num = LONG_MAX / 2; + if (num > PG_INT64_MAX / 2) + num = PG_INT64_MAX / 2; -#if SIZEOF_LONG < 8 - return pg_ceil_log2_32(num); -#else return pg_ceil_log2_64(num); -#endif } -/* calculate first power of 2 >= num, bounded to what will fit in a long */ -static long -next_pow2_long(long num) +/* calculate first power of 2 >= num, bounded to what will fit in a int64 */ +static int64 +next_pow2_int64(int64 num) { /* my_log2's internal range check is sufficient */ - return 1L << my_log2(num); + return INT64CONST(1) << my_log2(num); } /* calculate first power of 2 >= num, bounded to what will fit in an int */ static int -next_pow2_int(long num) +next_pow2_int(int64 num) { if (num > INT_MAX / 2) num = INT_MAX / 2; diff --git a/src/backend/utils/hash/meson.build b/src/backend/utils/hash/meson.build index f67efb4c6b78b..f153ff03c36ac 100644 --- a/src/backend/utils/hash/meson.build +++ b/src/backend/utils/hash/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'dynahash.c', diff --git a/src/backend/utils/hash/pg_crc.c b/src/backend/utils/hash/pg_crc.c index e67a74ef85250..e9f74ede905d3 100644 --- a/src/backend/utils/hash/pg_crc.c +++ b/src/backend/utils/hash/pg_crc.c @@ -7,7 +7,7 @@ * A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS, available from * http://ross.net/crc/download/crc_v3.txt or several other net sites. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a0588a..bbd28d14d9948 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -3,7 +3,7 @@ * globals.c * global variable declarations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -43,6 +43,8 @@ volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; volatile uint32 CritSectionCount = 0; +volatile int ProcDieSenderPid = 0; +volatile int ProcDieSenderUid = 0; int MyProcPid; pg_time_t MyStartTime; @@ -143,6 +145,7 @@ int NBuffers = 16384; int MaxConnections = 100; int max_worker_processes = 8; int max_parallel_workers = 8; +int autovacuum_max_parallel_workers = 0; int MaxBackends = 0; /* GUC parameters for vacuum */ diff --git a/src/backend/utils/init/meson.build b/src/backend/utils/init/meson.build index c5d42e6305e15..1348671d71afc 100644 --- a/src/backend/utils/init/meson.build +++ b/src/backend/utils/init/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'globals.c', diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c index 43b4dbccc3de6..7ffc808073ac8 100644 --- a/src/backend/utils/init/miscinit.c +++ b/src/backend/utils/init/miscinit.c @@ -3,7 +3,7 @@ * miscinit.c * miscellaneous initialization support stuff * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -55,6 +55,7 @@ #include "utils/pidfile.h" #include "utils/syscache.h" #include "utils/varlena.h" +#include "utils/wait_event.h" #define DIRECTORY_LOCK_FILE "postmaster.pid" @@ -266,62 +267,11 @@ GetBackendTypeDesc(BackendType backendType) switch (backendType) { - case B_INVALID: - backendDesc = gettext_noop("not initialized"); - break; - case B_ARCHIVER: - backendDesc = gettext_noop("archiver"); - break; - case B_AUTOVAC_LAUNCHER: - backendDesc = gettext_noop("autovacuum launcher"); - break; - case B_AUTOVAC_WORKER: - backendDesc = gettext_noop("autovacuum worker"); - break; - case B_BACKEND: - backendDesc = gettext_noop("client backend"); - break; - case B_DEAD_END_BACKEND: - backendDesc = gettext_noop("dead-end client backend"); - break; - case B_BG_WORKER: - backendDesc = gettext_noop("background worker"); - break; - case B_BG_WRITER: - backendDesc = gettext_noop("background writer"); - break; - case B_CHECKPOINTER: - backendDesc = gettext_noop("checkpointer"); - break; - case B_IO_WORKER: - backendDesc = gettext_noop("io worker"); - break; - case B_LOGGER: - backendDesc = gettext_noop("logger"); - break; - case B_SLOTSYNC_WORKER: - backendDesc = gettext_noop("slotsync worker"); - break; - case B_STANDALONE_BACKEND: - backendDesc = gettext_noop("standalone backend"); - break; - case B_STARTUP: - backendDesc = gettext_noop("startup"); - break; - case B_WAL_RECEIVER: - backendDesc = gettext_noop("walreceiver"); - break; - case B_WAL_SENDER: - backendDesc = gettext_noop("walsender"); - break; - case B_WAL_SUMMARIZER: - backendDesc = gettext_noop("walsummarizer"); - break; - case B_WAL_WRITER: - backendDesc = gettext_noop("walwriter"); - break; +#define PG_PROCTYPE(bktype, bkcategory, description, main_func, shmem_attach) \ + case bktype: backendDesc = description; break; +#include "postmaster/proctypelist.h" +#undef PG_PROCTYPE } - return backendDesc; } @@ -895,7 +845,8 @@ InitializeSessionUserIdStandalone(void) * workers, in slot sync worker and in background workers. */ Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() || - AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess()); + AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() || + AmDataChecksumsWorkerProcess()); /* call only once */ Assert(!OidIsValid(AuthenticatedUserId)); @@ -1099,7 +1050,8 @@ EstimateClientConnectionInfoSpace(void) * Serialize MyClientConnectionInfo for use by parallel workers. */ void -SerializeClientConnectionInfo(Size maxsize, char *start_address) +SerializeClientConnectionInfo(Size maxsize PG_USED_FOR_ASSERTS_ONLY, + char *start_address) { SerializedClientConnectionInfo serialized = {0}; @@ -1183,7 +1135,6 @@ UnlinkLockFiles(int status, Datum arg) /* Should we complain if the unlink fails? */ } /* Since we're about to exit, no need to reclaim storage */ - lock_files = NIL; /* * Lock file removal should always be the last externally visible action diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index c86ceefda940b..6f074013aa955 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -3,7 +3,7 @@ * postinit.c * postgres initialization utilities * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -38,6 +38,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" #include "pgstat.h" +#include "port/pg_bitutils.h" #include "postmaster/autovacuum.h" #include "postmaster/postmaster.h" #include "replication/slot.h" @@ -70,6 +71,13 @@ #include "utils/syscache.h" #include "utils/timeout.h" +/* has this backend called EmitConnectionWarnings()? */ +static bool ConnectionWarningsEmitted; + +/* content of warnings to send via EmitConnectionWarnings() */ +static List *ConnectionWarningMessages; +static List *ConnectionWarningDetails; + static HeapTuple GetDatabaseTuple(const char *dbname); static HeapTuple GetDatabaseTupleByOid(Oid dboid); static void PerformAuthentication(Port *port); @@ -85,6 +93,7 @@ static void ClientCheckTimeoutHandler(void); static bool ThereIsAtLeastOneRole(void); static void process_startup_options(Port *port, bool am_superuser); static void process_settings(Oid databaseid, Oid roleid); +static void EmitConnectionWarnings(void); /*** InitPostgres support ***/ @@ -417,12 +426,11 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect datum = SysCacheGetAttrNotNull(DATABASEOID, tup, Anum_pg_database_datctype); ctype = TextDatumGetCString(datum); - if (pg_perm_setlocale(LC_COLLATE, collate) == NULL) - ereport(FATAL, - (errmsg("database locale is incompatible with operating system"), - errdetail("The database was initialized with LC_COLLATE \"%s\", " - " which is not recognized by setlocale().", collate), - errhint("Recreate the database with another locale or install the missing locale."))); + /* + * Historically, we set LC_COLLATE from datcollate, as well. That's no + * longer necessary because all collation behavior is handled through + * pg_locale_t. + */ if (pg_perm_setlocale(LC_CTYPE, ctype) == NULL) ereport(FATAL, @@ -431,10 +439,6 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect " which is not recognized by setlocale().", ctype), errhint("Recreate the database with another locale or install the missing locale."))); - if (strcmp(ctype, "C") == 0 || - strcmp(ctype, "POSIX") == 0) - database_ctype_is_c = true; - init_database_collation(); /* @@ -589,7 +593,7 @@ InitializeFastPathLocks(void) * value at FP_LOCK_GROUPS_PER_BACKEND_MAX and insist the value is at * least 1. * - * The default max_locks_per_transaction = 64 means 4 groups by default. + * The default max_locks_per_transaction = 128 means 8 groups by default. */ FastPathLockGroupsPerBackend = Max(Min(pg_nextpower2_32(max_locks_per_xact) / FP_LOCK_SLOTS_PER_GROUP, @@ -658,6 +662,9 @@ BaseInit(void) /* Initialize lock manager's local structs */ InitLockManagerAccess(); + /* Initialize logical info WAL logging state */ + InitializeProcessXLogLogicalInfo(); + /* * Initialize replication slots after pgstat. The exit hook might need to * drop ephemeral slots, which in turn triggers stats reporting. @@ -711,7 +718,7 @@ BaseInit(void) void InitPostgres(const char *in_dbname, Oid dboid, const char *username, Oid useroid, - bits32 flags, + uint32 flags, char *out_dbname) { bool bootstrap = IsBootstrapProcessingMode(); @@ -751,6 +758,24 @@ InitPostgres(const char *in_dbname, Oid dboid, ProcSignalInit(MyCancelKey, MyCancelKeyLength); + /* + * Initialize a local cache of the data_checksum_version, to be updated by + * the procsignal-based barriers. + * + * This intentionally happens after initializing the procsignal, otherwise + * we might miss a state change. This means we can get a barrier for the + * state we've just initialized. + * + * The postmaster (which is what gets forked into the new child process) + * does not handle barriers, therefore it may not have the current value + * of LocalDataChecksumVersion value (it'll have the value read from the + * control file, which may be arbitrarily old). + * + * NB: Even if the postmaster handled barriers, the value might still be + * stale, as it might have changed after this process forked. + */ + InitLocalDataChecksumState(); + /* * Also set up timeout handlers needed for backend operation. We need * these in every case except bootstrap. @@ -818,9 +843,9 @@ InitPostgres(const char *in_dbname, Oid dboid, RelationCacheInitializePhase2(); /* - * Set up process-exit callback to do pre-shutdown cleanup. This is the - * one of the first before_shmem_exit callbacks we register; thus, this - * will be one the last things we do before low-level modules like the + * Set up process-exit callback to do pre-shutdown cleanup. This is one + * of the first before_shmem_exit callbacks we register; thus, this will + * be one of the last things we do before low-level modules like the * buffer manager begin to close down. We need to have this in place * before we begin our first transaction --- if we fail during the * initialization transaction, as is entirely possible, we need the @@ -879,7 +904,7 @@ InitPostgres(const char *in_dbname, Oid dboid, errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.", username != NULL ? username : "postgres"))); } - else if (AmBackgroundWorkerProcess()) + else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess()) { if (username == NULL && !OidIsValid(useroid)) { @@ -989,6 +1014,9 @@ InitPostgres(const char *in_dbname, Oid dboid, /* close the transaction we started above */ CommitTransactionCommand(); + /* send any WARNINGs we've accumulated during initialization */ + EmitConnectionWarnings(); + return; } @@ -1234,6 +1262,9 @@ InitPostgres(const char *in_dbname, Oid dboid, /* close the transaction we started above */ if (!bootstrap) CommitTransactionCommand(); + + /* send any WARNINGs we've accumulated during initialization */ + EmitConnectionWarnings(); } /* @@ -1265,7 +1296,7 @@ process_startup_options(Port *port, bool am_superuser) maxac = 2 + (strlen(port->cmdline_options) + 1) / 2; - av = (char **) palloc(maxac * sizeof(char *)); + av = palloc_array(char *, maxac); ac = 0; av[ac++] = "postgres"; @@ -1448,3 +1479,58 @@ ThereIsAtLeastOneRole(void) return result; } + +/* + * Stores a warning message to be sent later via EmitConnectionWarnings(). + * Both msg and detail must be non-NULL. + * + * NB: Caller should ensure the strings are allocated in a long-lived context + * like TopMemoryContext. + */ +void +StoreConnectionWarning(char *msg, char *detail) +{ + MemoryContext oldcontext; + + Assert(msg); + Assert(detail); + + if (ConnectionWarningsEmitted) + elog(ERROR, "StoreConnectionWarning() called after EmitConnectionWarnings()"); + + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + + ConnectionWarningMessages = lappend(ConnectionWarningMessages, msg); + ConnectionWarningDetails = lappend(ConnectionWarningDetails, detail); + + MemoryContextSwitchTo(oldcontext); +} + +/* + * Sends the warning messages saved via StoreConnectionWarning() and frees the + * strings and lists. + * + * NB: This can only be called once per backend. + */ +static void +EmitConnectionWarnings(void) +{ + ListCell *lc_msg; + ListCell *lc_detail; + + if (ConnectionWarningsEmitted) + elog(ERROR, "EmitConnectionWarnings() called more than once"); + else + ConnectionWarningsEmitted = true; + + forboth(lc_msg, ConnectionWarningMessages, + lc_detail, ConnectionWarningDetails) + { + ereport(WARNING, + (errmsg("%s", (char *) lfirst(lc_msg)), + errdetail("%s", (char *) lfirst(lc_detail)))); + } + + list_free_deep(ConnectionWarningMessages); + list_free_deep(ConnectionWarningDetails); +} diff --git a/src/backend/utils/init/usercontext.c b/src/backend/utils/init/usercontext.c index 2cc42ea1b7746..d3727d8be194c 100644 --- a/src/backend/utils/init/usercontext.c +++ b/src/backend/utils/init/usercontext.c @@ -3,7 +3,7 @@ * usercontext.c * Convenience functions for running code as a different database user. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/mb/Unicode/Makefile b/src/backend/utils/mb/Unicode/Makefile index ad789b31e54b5..8d0218ffb0f94 100644 --- a/src/backend/utils/mb/Unicode/Makefile +++ b/src/backend/utils/mb/Unicode/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/backend/utils/mb/Unicode # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/Makefile # @@ -50,11 +50,11 @@ $(eval $(call map_rule,gbk,UCS_to_most.pl,CP936.TXT,GBK)) $(eval $(call map_rule,johab,UCS_to_JOHAB.pl,JOHAB.TXT)) $(eval $(call map_rule,uhc,UCS_to_UHC.pl,windows-949-2000.xml)) $(eval $(call map_rule,euc_jp,UCS_to_EUC_JP.pl,CP932.TXT JIS0212.TXT)) -$(eval $(call map_rule,euc_cn,UCS_to_EUC_CN.pl,gb-18030-2000.xml)) +$(eval $(call map_rule,euc_cn,UCS_to_EUC_CN.pl,gb18030-2022.ucm)) $(eval $(call map_rule,euc_kr,UCS_to_EUC_KR.pl,KSX1001.TXT)) $(eval $(call map_rule,euc_tw,UCS_to_EUC_TW.pl,CNS11643.TXT)) $(eval $(call map_rule,sjis,UCS_to_SJIS.pl,CP932.TXT)) -$(eval $(call map_rule,gb18030,UCS_to_GB18030.pl,gb-18030-2000.xml)) +$(eval $(call map_rule,gb18030,UCS_to_GB18030.pl,gb18030-2022.ucm)) $(eval $(call map_rule,big5,UCS_to_BIG5.pl,CP950.TXT BIG5.TXT CP950.TXT)) $(eval $(call map_rule,euc_jis_2004,UCS_to_EUC_JIS_2004.pl,euc-jis-2004-std.txt)) $(eval $(call map_rule,shift_jis_2004,UCS_to_SHIFT_JIS_2004.pl,sjis-0213-2004-std.txt)) @@ -75,9 +75,12 @@ BIG5.TXT CNS11643.TXT: euc-jis-2004-std.txt sjis-0213-2004-std.txt: $(DOWNLOAD) http://x0213.org/codetable/$(@F) -gb-18030-2000.xml windows-949-2000.xml: +windows-949-2000.xml: $(DOWNLOAD) https://raw.githubusercontent.com/unicode-org/icu-data/master/charset/data/xml/$(@F) +gb18030-2022.ucm: + $(DOWNLOAD) https://raw.githubusercontent.com/unicode-org/icu/refs/heads/main/icu4c/source/data/mappings/$(@F) + GB2312.TXT: $(DOWNLOAD) 'http://trac.greenstone.org/browser/trunk/gsdl/unicode/MAPPINGS/EASTASIA/GB/GB2312.TXT?rev=1842&format=txt' diff --git a/src/backend/utils/mb/Unicode/UCS_to_BIG5.pl b/src/backend/utils/mb/Unicode/UCS_to_BIG5.pl index abd73721c9628..949066b5cc6c9 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_BIG5.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_BIG5.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_BIG5.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_EUC_CN.pl b/src/backend/utils/mb/Unicode/UCS_to_EUC_CN.pl index f7776631e4c18..97f1a69d7947c 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_EUC_CN.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_EUC_CN.pl @@ -1,17 +1,18 @@ #! /usr/bin/perl # -# Copyright (c) 2007-2025, PostgreSQL Global Development Group +# Copyright (c) 2007-2026, PostgreSQL Global Development Group # -# src/backend/utils/mb/Unicode/UCS_to_GB18030.pl +# src/backend/utils/mb/Unicode/UCS_to_EUC_CN.pl # -# Generate UTF-8 <--> GB18030 code conversion tables from -# "gb-18030-2000.xml", obtained from -# http://source.icu-project.org/repos/icu/data/trunk/charset/data/xml/ +# Generate UTF-8 <--> EUC_CN code conversion tables from +# "gb18030-2022.ucm", obtained from +# https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/ # # The lines we care about in the source file look like -# -# where the "u" field is the Unicode code point in hex, -# and the "b" field is the hex byte sequence for GB18030 +# \xYY[\xYY...] |n +# where XXXX is the Unicode code point in hex, +# and the \xYY... is the hex byte sequence for GB18030, +# and n is a flag indicating the type of mapping. use strict; use warnings FATAL => 'all'; @@ -22,7 +23,7 @@ # Read the input -my $in_file = "gb-18030-2000.xml"; +my $in_file = "gb18030-2022.ucm"; open(my $in, '<', $in_file) || die("cannot open $in_file"); @@ -30,9 +31,18 @@ while (<$in>) { - next if (!m/\s+ + ((?:\\x[0-9A-Fa-f]{2})+)\s+ + \|(\d+)/x; + my ($u, $c, $flag) = ($1, $2, $3); + $c =~ s/\\x//g; + + # We only want round-trip mappings + next if ($flag ne '0'); + my $ucs = hex($u); my $code = hex($c); diff --git a/src/backend/utils/mb/Unicode/UCS_to_EUC_JIS_2004.pl b/src/backend/utils/mb/Unicode/UCS_to_EUC_JIS_2004.pl index cc3b65abc67b3..d9348cfd344a6 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_EUC_JIS_2004.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_EUC_JIS_2004.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2007-2025, PostgreSQL Global Development Group +# Copyright (c) 2007-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_EUC_JIS_2004.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_EUC_JP.pl b/src/backend/utils/mb/Unicode/UCS_to_EUC_JP.pl index c74c5e6a98fa2..7ca2fa3420438 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_EUC_JP.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_EUC_JP.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_EUC_JP.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_EUC_KR.pl b/src/backend/utils/mb/Unicode/UCS_to_EUC_KR.pl index 43f0ee75d6f4b..9a0a27661f2a9 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_EUC_KR.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_EUC_KR.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_EUC_KR.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_EUC_TW.pl b/src/backend/utils/mb/Unicode/UCS_to_EUC_TW.pl index 19da4b410f422..e3093a24bd105 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_EUC_TW.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_EUC_TW.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_EUC_TW.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_GB18030.pl b/src/backend/utils/mb/Unicode/UCS_to_GB18030.pl index ddcbd6ef0c478..42f409e9cf846 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_GB18030.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_GB18030.pl @@ -1,17 +1,18 @@ #! /usr/bin/perl # -# Copyright (c) 2007-2025, PostgreSQL Global Development Group +# Copyright (c) 2007-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_GB18030.pl # # Generate UTF-8 <--> GB18030 code conversion tables from -# "gb-18030-2000.xml", obtained from -# http://source.icu-project.org/repos/icu/data/trunk/charset/data/xml/ +# "gb18030-2022.ucm", obtained from +# https://github.com/unicode-org/icu/blob/main/icu4c/source/data/mappings/ # # The lines we care about in the source file look like -# -# where the "u" field is the Unicode code point in hex, -# and the "b" field is the hex byte sequence for GB18030 +# \xYY[\xYY...] |n +# where XXXX is the Unicode code point in hex, +# and the \xYY... is the hex byte sequence for GB18030, +# and n is a flag indicating the type of mapping. use strict; use warnings FATAL => 'all'; @@ -22,7 +23,7 @@ # Read the input -my $in_file = "gb-18030-2000.xml"; +my $in_file = "gb18030-2022.ucm"; open(my $in, '<', $in_file) || die("cannot open $in_file"); @@ -30,9 +31,18 @@ while (<$in>) { - next if (!m/\s+ + ((?:\\x[0-9A-Fa-f]{2})+)\s+ + \|(\d+)/x; + my ($u, $c, $flag) = ($1, $2, $3); + $c =~ s/\\x//g; + + # We only want round-trip mappings + next if ($flag ne '0'); + my $ucs = hex($u); my $code = hex($c); if ($code >= 0x80 && $ucs >= 0x0080) diff --git a/src/backend/utils/mb/Unicode/UCS_to_JOHAB.pl b/src/backend/utils/mb/Unicode/UCS_to_JOHAB.pl index cd70e66628aee..1ae81fa23168b 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_JOHAB.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_JOHAB.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_JOHAB.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_SHIFT_JIS_2004.pl b/src/backend/utils/mb/Unicode/UCS_to_SHIFT_JIS_2004.pl index 41a4334cf3977..85c755ad6dedf 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_SHIFT_JIS_2004.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_SHIFT_JIS_2004.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2007-2025, PostgreSQL Global Development Group +# Copyright (c) 2007-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_SHIFT_JIS_2004.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_SJIS.pl b/src/backend/utils/mb/Unicode/UCS_to_SJIS.pl index 66d0fe43ddf5e..57eb69557ce40 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_SJIS.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_SJIS.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_SJIS.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_UHC.pl b/src/backend/utils/mb/Unicode/UCS_to_UHC.pl index c6087b5c38270..3e0f276563345 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_UHC.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_UHC.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2007-2025, PostgreSQL Global Development Group +# Copyright (c) 2007-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_GB18030.pl # diff --git a/src/backend/utils/mb/Unicode/UCS_to_most.pl b/src/backend/utils/mb/Unicode/UCS_to_most.pl index b0009692521a2..e026ecdf75d6f 100755 --- a/src/backend/utils/mb/Unicode/UCS_to_most.pl +++ b/src/backend/utils/mb/Unicode/UCS_to_most.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/UCS_to_most.pl # diff --git a/src/backend/utils/mb/Unicode/convutils.pm b/src/backend/utils/mb/Unicode/convutils.pm index 99a397a0c37fa..cc74c72d37d45 100644 --- a/src/backend/utils/mb/Unicode/convutils.pm +++ b/src/backend/utils/mb/Unicode/convutils.pm @@ -1,5 +1,5 @@ # -# Copyright (c) 2001-2025, PostgreSQL Global Development Group +# Copyright (c) 2001-2026, PostgreSQL Global Development Group # # src/backend/utils/mb/Unicode/convutils.pm diff --git a/src/backend/utils/mb/Unicode/gb-18030-2000.xml b/src/backend/utils/mb/Unicode/gb-18030-2000.xml deleted file mode 100644 index fbbc9e334e9b3..0000000000000 --- a/src/backend/utils/mb/Unicode/gb-18030-2000.xml +++ /dev/null @@ -1,30916 +0,0 @@ - - - - - - 0x80 appears to be a valid (and unassigned) single-byte code, added to the validity. - - - New mapping data, changing all four-byte mappings to the BMP. - Removed mappings to single surrogates. - - - Original table. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/backend/utils/mb/Unicode/gb18030_to_utf8.map b/src/backend/utils/mb/Unicode/gb18030_to_utf8.map index 1c90d48fbaf76..da2b2047db52d 100644 --- a/src/backend/utils/mb/Unicode/gb18030_to_utf8.map +++ b/src/backend/utils/mb/Unicode/gb18030_to_utf8.map @@ -1,7 +1,7 @@ /* src/backend/utils/mb/Unicode/gb18030_to_utf8.map */ /* This file is generated by src/backend/utils/mb/Unicode/UCS_to_GB18030.pl */ -static const uint32 gb18030_to_unicode_tree_table[32795]; +static const uint32 gb18030_to_unicode_tree_table[32911]; static const pg_mb_radix_tree gb18030_to_unicode_tree = { @@ -37,7 +37,7 @@ static const pg_mb_radix_tree gb18030_to_unicode_tree = 0x39 /* b4_4_upper */ }; -static const uint32 gb18030_to_unicode_tree_table[32795] = +static const uint32 gb18030_to_unicode_tree_table[32911] = { /*** Dummy map, for invalid values - offset 0x00000 ***/ @@ -1885,7 +1885,7 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* 94 */ 0xee9799, 0xee979a, 0xee979b, 0xee979c, /* 98 */ 0xee979d, 0xee979e, 0xee979f, 0xee97a0, /* 9c */ 0xee97a1, 0xee97a2, 0xee97a3, 0xee97a4, - /* a0 */ 0xee97a5, 0xefbc81, 0xefbc82, 0xefbc83, + /* a0 */ 0x000000, 0xefbc81, 0xefbc82, 0xefbc83, /* a4 */ 0xefbfa5, 0xefbc85, 0xefbc86, 0xefbc87, /* a8 */ 0xefbc88, 0xefbc89, 0xefbc8a, 0xefbc8b, /* ac */ 0xefbc8c, 0xefbc8d, 0xefbc8e, 0xefbc8f, @@ -2052,13 +2052,13 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* cc */ 0x00cebc, 0x00cebd, 0x00cebe, 0x00cebf, /* d0 */ 0x00cf80, 0x00cf81, 0x00cf83, 0x00cf84, /* d4 */ 0x00cf85, 0x00cf86, 0x00cf87, 0x00cf88, - /* d8 */ 0x00cf89, 0xee9e8d, 0xee9e8e, 0xee9e8f, - /* dc */ 0xee9e90, 0xee9e91, 0xee9e92, 0xee9e93, + /* d8 */ 0x00cf89, 0xefb890, 0xefb892, 0xefb891, + /* dc */ 0xefb893, 0xefb894, 0xefb895, 0xefb896, /* e0 */ 0xefb8b5, 0xefb8b6, 0xefb8b9, 0xefb8ba, /* e4 */ 0xefb8bf, 0xefb980, 0xefb8bd, 0xefb8be, /* e8 */ 0xefb981, 0xefb982, 0xefb983, 0xefb984, - /* ec */ 0xee9e94, 0xee9e95, 0xefb8bb, 0xefb8bc, - /* f0 */ 0xefb8b7, 0xefb8b8, 0xefb8b1, 0xee9e96, + /* ec */ 0xefb897, 0xefb898, 0xefb8bb, 0xefb8bc, + /* f0 */ 0xefb8b7, 0xefb8b8, 0xefb8b1, 0xefb899, /* f4 */ 0xefb8b3, 0xefb8b4, 0xee9e97, 0xee9e98, /* f8 */ 0xee9e99, 0xee9e9a, 0xee9e9b, 0xee9e9c, /* fc */ 0xee9e9d, 0xee9e9e, 0xee9e9f, @@ -2147,7 +2147,7 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* b0 */ 0x00c3b2, 0x00c5ab, 0x00c3ba, 0x00c794, /* b4 */ 0x00c3b9, 0x00c796, 0x00c798, 0x00c79a, /* b8 */ 0x00c79c, 0x00c3bc, 0x00c3aa, 0x00c991, - /* bc */ 0xee9f87, 0x00c584, 0x00c588, 0x00c7b9, + /* bc */ 0xe1b8bf, 0x00c584, 0x00c588, 0x00c7b9, /* c0 */ 0x00c9a1, 0xee9f89, 0xee9f8a, 0xee9f8b, /* c4 */ 0xee9f8c, 0xe38485, 0xe38486, 0xe38487, /* c8 */ 0xe38488, 0xe38489, 0xe3848a, 0xe3848b, @@ -6508,25 +6508,25 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* 4c */ 0xefa8a4, 0xefa8a7, 0xefa8a8, 0xefa8a9, /* 50 */ 0xe2ba81, 0xeea096, 0xeea097, 0xeea098, /* 54 */ 0xe2ba84, 0xe391b3, 0xe39187, 0xe2ba88, - /* 58 */ 0xe2ba8b, 0xeea09e, 0xe3969e, 0xe3989a, + /* 58 */ 0xe2ba8b, 0xe9beb4, 0xe3969e, 0xe3989a, /* 5c */ 0xe3988e, 0xe2ba8c, 0xe2ba97, 0xe3a5ae, - /* 60 */ 0xe3a498, 0xeea0a6, 0xe3a78f, 0xe3a79f, - /* 64 */ 0xe3a9b3, 0xe3a790, 0xeea0ab, 0xeea0ac, + /* 60 */ 0xe3a498, 0xe9beb5, 0xe3a78f, 0xe3a79f, + /* 64 */ 0xe3a9b3, 0xe3a790, 0xe9beb6, 0xe9beb7, /* 68 */ 0xe3ad8e, 0xe3b1ae, 0xe3b3a0, 0xe2baa7, - /* 6c */ 0xeea0b1, 0xeea0b2, 0xe2baaa, 0xe48196, + /* 6c */ 0xeea0b1, 0xe9beb8, 0xe2baaa, 0xe48196, /* 70 */ 0xe4859f, 0xe2baae, 0xe48cb7, 0xe2bab3, /* 74 */ 0xe2bab6, 0xe2bab7, 0xeea0bb, 0xe48eb1, /* 78 */ 0xe48eac, 0xe2babb, 0xe48f9d, 0xe49396, - /* 7c */ 0xe499a1, 0xe4998c, 0xeea183, 0x000000, + /* 7c */ 0xe499a1, 0xe4998c, 0xe9beb9, 0x000000, /* 80 */ 0xe49ca3, 0xe49ca9, 0xe49dbc, 0xe49e8d, /* 84 */ 0xe2bb8a, 0xe4a587, 0xe4a5ba, 0xe4a5bd, /* 88 */ 0xe4a682, 0xe4a683, 0xe4a685, 0xe4a686, /* 8c */ 0xe4a69f, 0xe4a69b, 0xe4a6b7, 0xe4a6b6, - /* 90 */ 0xeea194, 0xeea195, 0xe4b2a3, 0xe4b29f, + /* 90 */ 0xe9beba, 0xeea195, 0xe4b2a3, 0xe4b29f, /* 94 */ 0xe4b2a0, 0xe4b2a1, 0xe4b1b7, 0xe4b2a2, /* 98 */ 0xe4b493, 0xe4b494, 0xe4b495, 0xe4b496, /* 9c */ 0xe4b497, 0xe4b498, 0xe4b499, 0xe4b6ae, - /* a0 */ 0xeea1a4, 0xee91a8, 0xee91a9, 0xee91aa, + /* a0 */ 0xe9bebb, 0xee91a8, 0xee91a9, 0xee91aa, /* a4 */ 0xee91ab, 0xee91ac, 0xee91ad, 0xee91ae, /* a8 */ 0xee91af, 0xee91b0, 0xee91b1, 0xee91b2, /* ac */ 0xee91b3, 0xee91b4, 0xee91b5, 0xee91b6, @@ -6558,55 +6558,86 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /*** Four byte table, byte #2: 81xx - offset 0x05f43 ***/ /* 30 */ 0x005f67, 0x000000, 0x000000, 0x000000, - /* 34 */ 0x000000, 0x000000, 0x005fc1, 0x00603f, - /* 38 */ 0x006067, 0x0060e5, + /* 34 */ 0x000000, 0x005fb9, 0x00602d, 0x0060ab, + /* 38 */ 0x0060d3, 0x006151, /*** Four byte table, byte #2: 82xx - offset 0x05f4d ***/ - /* 30 */ 0x006163, 0x0061e1, 0x006235, 0x0062b3, - /* 34 */ 0x00631c, 0x00639a, + /* 30 */ 0x0061cf, 0x00624d, 0x0062a1, 0x00631f, + /* 34 */ 0x006388, 0x006406, /* 4 trailing zero values shared with next segment */ /*** Four byte table, byte #2: 83xx - offset 0x05f53 ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* 34 */ 0x000000, 0x000000, 0x0063d2, 0x000000, + /* 34 */ 0x000000, 0x000000, 0x00643e, 0x000000, /* 38 */ 0x000000, 0x000000, /*** Four byte table, byte #2: 84xx - offset 0x05f5d ***/ - /* 30 */ 0x00644c, 0x0064c6, 0x000000, 0x000000, + /* 30 */ 0x0064b8, 0x006532, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0x000000, 0x000000, /*** Four byte table, byte #3: 8130xx - offset 0x05f67 ***/ - /* 81 */ 0x006544, 0x00654e, 0x006558, 0x006562, - /* 85 */ 0x00656c, 0x006576, 0x006580, 0x00658a, - /* 89 */ 0x006594, 0x00659e, 0x0065a8, 0x0065b2, - /* 8d */ 0x0065bc, 0x0065c6, 0x0065d0, 0x0065da, - /* 91 */ 0x0065e4, 0x0065ee, 0x0065f8, 0x006602, - /* 95 */ 0x00660c, 0x006616, 0x006620, 0x00662a, - /* 99 */ 0x006634, 0x00663e, 0x006648, 0x006652, - /* 9d */ 0x00665c, 0x006666, 0x006670, 0x00667a, - /* a1 */ 0x006684, 0x00668e, 0x006698, 0x0066a2, - /* a5 */ 0x0066ac, 0x0066b6, 0x0066c0, 0x0066ca, - /* a9 */ 0x0066d4, 0x0066de, 0x0066e8, 0x0066f2, - /* ad */ 0x0066fc, 0x006706, 0x006710, 0x00671a, - /* b1 */ 0x006724, 0x00672e, 0x006738, 0x006742, - /* b5 */ 0x00674c, 0x006756, 0x006760, 0x00676a, - /* b9 */ 0x006774, 0x00677e, 0x006788, 0x006792, - /* bd */ 0x00679c, 0x0067a6, 0x0067b0, 0x0067ba, - /* c1 */ 0x0067c4, 0x0067ce, 0x0067d8, 0x0067e2, - /* c5 */ 0x0067ec, 0x0067f6, 0x006800, 0x00680a, - /* c9 */ 0x006814, 0x00681e, 0x006828, 0x006832, - /* cd */ 0x00683c, 0x006846, 0x006850, 0x00685a, - /* d1 */ 0x006864, 0x00686e, 0x000000, 0x000000, + /* 81 */ 0x0065b0, 0x0065ba, 0x0065c4, 0x0065ce, + /* 85 */ 0x0065d8, 0x0065e2, 0x0065ec, 0x0065f6, + /* 89 */ 0x006600, 0x00660a, 0x006614, 0x00661e, + /* 8d */ 0x006628, 0x006632, 0x00663c, 0x006646, + /* 91 */ 0x006650, 0x00665a, 0x006664, 0x00666e, + /* 95 */ 0x006678, 0x006682, 0x00668c, 0x006696, + /* 99 */ 0x0066a0, 0x0066aa, 0x0066b4, 0x0066be, + /* 9d */ 0x0066c8, 0x0066d2, 0x0066dc, 0x0066e6, + /* a1 */ 0x0066f0, 0x0066fa, 0x006704, 0x00670e, + /* a5 */ 0x006718, 0x006722, 0x00672c, 0x006736, + /* a9 */ 0x006740, 0x00674a, 0x006754, 0x00675e, + /* ad */ 0x006768, 0x006772, 0x00677c, 0x006786, + /* b1 */ 0x006790, 0x00679a, 0x0067a4, 0x0067ae, + /* b5 */ 0x0067b8, 0x0067c2, 0x0067cc, 0x0067d6, + /* b9 */ 0x0067e0, 0x0067ea, 0x0067f4, 0x0067fe, + /* bd */ 0x006808, 0x006812, 0x00681c, 0x006826, + /* c1 */ 0x006830, 0x00683a, 0x006844, 0x00684e, + /* c5 */ 0x006858, 0x006862, 0x00686c, 0x006876, + /* c9 */ 0x006880, 0x00688a, 0x006894, 0x00689e, + /* cd */ 0x0068a8, 0x0068b2, 0x0068bc, 0x0068c6, + /* d1 */ 0x0068d0, 0x0068da, + /* 44 trailing zero values shared with next segment */ + + /*** Four byte table, byte #3: 8135xx - offset 0x05fb9 ***/ + + /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 85 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 89 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 8d */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 91 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 95 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 99 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 9d */ 0x000000, 0x000000, 0x000000, 0x000000, + /* a1 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* a5 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* a9 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* ad */ 0x000000, 0x000000, 0x000000, 0x000000, + /* b1 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* b5 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* b9 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* bd */ 0x000000, 0x000000, 0x000000, 0x000000, + /* c1 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* c5 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* c9 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* cd */ 0x000000, 0x000000, 0x000000, 0x000000, + /* d1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* d5 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* d9 */ 0x000000, 0x000000, - /* 36 trailing zero values shared with next segment */ + /* d9 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* dd */ 0x000000, 0x000000, 0x000000, 0x000000, + /* e1 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* e5 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* e9 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* ed */ 0x000000, 0x000000, 0x000000, 0x000000, + /* f1 */ 0x000000, 0x000000, 0x000000, 0x0068e4, + /* 10 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8136xx - offset 0x05fc1 ***/ + /*** Four byte table, byte #3: 8136xx - offset 0x0602d ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 85 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6617,45 +6648,45 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* 99 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 9d */ 0x000000, 0x000000, 0x000000, 0x000000, /* a1 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* a5 */ 0x006878, 0x006882, 0x00688c, 0x006896, - /* a9 */ 0x0068a0, 0x0068aa, 0x0068b4, 0x0068be, - /* ad */ 0x0068c8, 0x0068d2, 0x0068dc, 0x0068e6, - /* b1 */ 0x0068f0, 0x0068fa, 0x006904, 0x00690e, - /* b5 */ 0x006918, 0x006922, 0x00692c, 0x006936, - /* b9 */ 0x006940, 0x00694a, 0x006954, 0x00695e, - /* bd */ 0x006968, 0x006972, 0x00697c, 0x006986, - /* c1 */ 0x006990, 0x00699a, 0x0069a4, 0x0069ae, - /* c5 */ 0x0069b8, 0x0069c2, 0x0069cc, 0x0069d6, - /* c9 */ 0x0069e0, 0x0069ea, 0x0069f4, 0x0069fe, - /* cd */ 0x006a08, 0x006a12, 0x006a1c, 0x006a26, - /* d1 */ 0x006a30, 0x006a3a, 0x006a44, 0x006a4e, - /* d5 */ 0x006a58, 0x006a62, 0x006a6c, 0x006a76, - /* d9 */ 0x006a80, 0x006a8a, 0x006a94, 0x006a9e, - /* dd */ 0x006aa8, 0x006ab2, 0x006abc, 0x006ac6, - /* e1 */ 0x006ad0, 0x006ada, 0x006ae4, 0x006aee, - /* e5 */ 0x006af8, 0x006b02, 0x006b0c, 0x006b16, - /* e9 */ 0x006b20, 0x006b2a, 0x006b34, 0x006b3e, - /* ed */ 0x006b48, 0x006b52, 0x006b5c, 0x006b66, - /* f1 */ 0x006b70, 0x006b7a, 0x006b84, 0x006b8e, - /* f5 */ 0x006b98, 0x006ba2, 0x006bac, 0x006bb6, - /* f9 */ 0x006bc0, 0x006bca, 0x006bd4, 0x006bde, - /* fd */ 0x006be8, 0x006bf2, - - /*** Four byte table, byte #3: 8137xx - offset 0x0603f ***/ - - /* 81 */ 0x006bfc, 0x006c06, 0x006c10, 0x006c1a, - /* 85 */ 0x006c24, 0x006c2e, 0x006c38, 0x006c42, - /* 89 */ 0x006c4c, 0x006c56, 0x006c60, 0x006c6a, - /* 8d */ 0x006c74, 0x006c7e, 0x006c88, 0x006c92, - /* 91 */ 0x006c9c, 0x006ca6, 0x006cb0, 0x006cba, - /* 95 */ 0x006cc4, 0x006cce, 0x006cd8, 0x006ce2, - /* 99 */ 0x006cec, 0x006cf6, 0x006d00, 0x006d0a, - /* 9d */ 0x006d14, 0x006d1e, 0x006d28, 0x006d32, - /* a1 */ 0x006d3c, 0x006d46, 0x006d50, 0x006d5a, - /* a5 */ 0x006d64, 0x006d6e, 0x006d78, 0x006d82, + /* a5 */ 0x0068ec, 0x0068f6, 0x006900, 0x00690a, + /* a9 */ 0x006914, 0x00691e, 0x006928, 0x006932, + /* ad */ 0x00693c, 0x006946, 0x006950, 0x00695a, + /* b1 */ 0x006964, 0x00696e, 0x006978, 0x006982, + /* b5 */ 0x00698c, 0x006996, 0x0069a0, 0x0069aa, + /* b9 */ 0x0069b4, 0x0069be, 0x0069c8, 0x0069d2, + /* bd */ 0x0069dc, 0x0069e6, 0x0069f0, 0x0069fa, + /* c1 */ 0x006a04, 0x006a0e, 0x006a18, 0x006a22, + /* c5 */ 0x006a2c, 0x006a36, 0x006a40, 0x006a4a, + /* c9 */ 0x006a54, 0x006a5e, 0x006a68, 0x006a72, + /* cd */ 0x006a7c, 0x006a86, 0x006a90, 0x006a9a, + /* d1 */ 0x006aa4, 0x006aae, 0x006ab8, 0x006ac2, + /* d5 */ 0x006acc, 0x006ad6, 0x006ae0, 0x006aea, + /* d9 */ 0x006af4, 0x006afe, 0x006b08, 0x006b12, + /* dd */ 0x006b1c, 0x006b26, 0x006b30, 0x006b3a, + /* e1 */ 0x006b44, 0x006b4e, 0x006b58, 0x006b62, + /* e5 */ 0x006b6c, 0x006b76, 0x006b80, 0x006b8a, + /* e9 */ 0x006b94, 0x006b9e, 0x006ba8, 0x006bb2, + /* ed */ 0x006bbc, 0x006bc6, 0x006bd0, 0x006bda, + /* f1 */ 0x006be4, 0x006bee, 0x006bf8, 0x006c02, + /* f5 */ 0x006c0c, 0x006c16, 0x006c20, 0x006c2a, + /* f9 */ 0x006c34, 0x006c3e, 0x006c48, 0x006c52, + /* fd */ 0x006c5c, 0x006c66, + + /*** Four byte table, byte #3: 8137xx - offset 0x060ab ***/ + + /* 81 */ 0x006c70, 0x006c7a, 0x006c84, 0x006c8e, + /* 85 */ 0x006c98, 0x006ca2, 0x006cac, 0x006cb6, + /* 89 */ 0x006cc0, 0x006cca, 0x006cd4, 0x006cde, + /* 8d */ 0x006ce8, 0x006cf2, 0x006cfc, 0x006d06, + /* 91 */ 0x006d10, 0x006d1a, 0x006d24, 0x006d2e, + /* 95 */ 0x006d38, 0x006d42, 0x006d4c, 0x006d56, + /* 99 */ 0x006d60, 0x006d6a, 0x006d74, 0x006d7e, + /* 9d */ 0x006d88, 0x006d92, 0x006d9c, 0x006da6, + /* a1 */ 0x006db0, 0x006dba, 0x006dc4, 0x006dce, + /* a5 */ 0x006dd8, 0x006de2, 0x006dec, 0x006df6, /* 86 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8138xx - offset 0x06067 ***/ + /*** Four byte table, byte #3: 8138xx - offset 0x060d3 ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 85 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6688,55 +6719,55 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* f1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* f5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* f9 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* fd */ 0x006d8b, 0x006d95, - - /*** Four byte table, byte #3: 8139xx - offset 0x060e5 ***/ - - /* 81 */ 0x006d9f, 0x006da9, 0x006db3, 0x006dbd, - /* 85 */ 0x006dc7, 0x006dd1, 0x006ddb, 0x006de5, - /* 89 */ 0x006def, 0x006df9, 0x006e03, 0x006e0d, - /* 8d */ 0x006e17, 0x006e21, 0x006e2b, 0x006e35, - /* 91 */ 0x006e3f, 0x006e49, 0x006e53, 0x006e5d, - /* 95 */ 0x006e67, 0x006e71, 0x006e7b, 0x006e85, - /* 99 */ 0x006e8f, 0x006e99, 0x006ea3, 0x006ead, - /* 9d */ 0x006eb7, 0x006ec1, 0x006ecb, 0x006ed5, - /* a1 */ 0x006edf, 0x006ee9, 0x006ef3, 0x006efd, - /* a5 */ 0x006f07, 0x006f11, 0x006f1b, 0x006f25, - /* a9 */ 0x006f2f, 0x006f39, 0x006f43, 0x006f4d, - /* ad */ 0x006f57, 0x006f61, 0x006f6b, 0x006f75, - /* b1 */ 0x006f7f, 0x006f89, 0x006f93, 0x006f9d, - /* b5 */ 0x006fa7, 0x006fb1, 0x006fbb, 0x006fc5, - /* b9 */ 0x006fcf, 0x006fd9, 0x006fe3, 0x006fed, - /* bd */ 0x006ff7, 0x007001, 0x00700b, 0x007015, - /* c1 */ 0x00701f, 0x007029, 0x007033, 0x00703d, - /* c5 */ 0x007047, 0x007051, 0x00705b, 0x007065, - /* c9 */ 0x00706f, 0x007079, 0x007083, 0x00708d, - /* cd */ 0x007097, 0x0070a1, 0x0070ab, 0x0070b5, - /* d1 */ 0x0070bf, 0x0070c9, 0x0070d3, 0x0070dd, - /* d5 */ 0x0070e7, 0x0070f1, 0x0070fb, 0x007105, - /* d9 */ 0x00710f, 0x007119, 0x007123, 0x00712d, - /* dd */ 0x007137, 0x007141, 0x00714b, 0x007155, - /* e1 */ 0x00715f, 0x007169, 0x007173, 0x00717d, - /* e5 */ 0x007187, 0x007191, 0x00719b, 0x0071a5, - /* e9 */ 0x0071af, 0x0071b9, 0x0071c3, 0x0071cd, - /* ed */ 0x0071d7, 0x0071e1, 0x0071eb, 0x0071f5, - /* f1 */ 0x0071ff, 0x007209, 0x007213, 0x00721d, - /* f5 */ 0x007227, 0x007231, 0x00723b, 0x007245, - /* f9 */ 0x00724f, 0x007259, 0x007263, 0x00726d, - /* fd */ 0x007277, 0x007281, - - /*** Four byte table, byte #3: 8230xx - offset 0x06163 ***/ - - /* 81 */ 0x00728b, 0x007295, 0x00729f, 0x0072a9, - /* 85 */ 0x0072b3, 0x0072bd, 0x0072c7, 0x0072d1, - /* 89 */ 0x0072db, 0x0072e5, 0x0072ef, 0x0072f9, - /* 8d */ 0x007303, 0x00730d, 0x007317, 0x007321, - /* 91 */ 0x00732b, 0x007335, 0x00733f, 0x007349, - /* 95 */ 0x007353, 0x00735d, 0x007367, 0x007371, - /* 99 */ 0x00737b, 0x007385, 0x00738f, 0x007399, - /* 9d */ 0x0073a3, 0x0073ad, 0x0073b7, 0x0073c1, - /* a1 */ 0x0073cb, 0x0073d5, 0x0073df, 0x0073e9, - /* a5 */ 0x0073f3, 0x0073fd, 0x000000, 0x000000, + /* fd */ 0x006dff, 0x006e09, + + /*** Four byte table, byte #3: 8139xx - offset 0x06151 ***/ + + /* 81 */ 0x006e13, 0x006e1d, 0x006e27, 0x006e31, + /* 85 */ 0x006e3b, 0x006e45, 0x006e4f, 0x006e59, + /* 89 */ 0x006e63, 0x006e6d, 0x006e77, 0x006e81, + /* 8d */ 0x006e8b, 0x006e95, 0x006e9f, 0x006ea9, + /* 91 */ 0x006eb3, 0x006ebd, 0x006ec7, 0x006ed1, + /* 95 */ 0x006edb, 0x006ee5, 0x006eef, 0x006ef9, + /* 99 */ 0x006f03, 0x006f0d, 0x006f17, 0x006f21, + /* 9d */ 0x006f2b, 0x006f35, 0x006f3f, 0x006f49, + /* a1 */ 0x006f53, 0x006f5d, 0x006f67, 0x006f71, + /* a5 */ 0x006f7b, 0x006f85, 0x006f8f, 0x006f99, + /* a9 */ 0x006fa3, 0x006fad, 0x006fb7, 0x006fc1, + /* ad */ 0x006fcb, 0x006fd5, 0x006fdf, 0x006fe9, + /* b1 */ 0x006ff3, 0x006ffd, 0x007007, 0x007011, + /* b5 */ 0x00701b, 0x007025, 0x00702f, 0x007039, + /* b9 */ 0x007043, 0x00704d, 0x007057, 0x007061, + /* bd */ 0x00706b, 0x007075, 0x00707f, 0x007089, + /* c1 */ 0x007093, 0x00709d, 0x0070a7, 0x0070b1, + /* c5 */ 0x0070bb, 0x0070c5, 0x0070cf, 0x0070d9, + /* c9 */ 0x0070e3, 0x0070ed, 0x0070f7, 0x007101, + /* cd */ 0x00710b, 0x007115, 0x00711f, 0x007129, + /* d1 */ 0x007133, 0x00713d, 0x007147, 0x007151, + /* d5 */ 0x00715b, 0x007165, 0x00716f, 0x007179, + /* d9 */ 0x007183, 0x00718d, 0x007197, 0x0071a1, + /* dd */ 0x0071ab, 0x0071b5, 0x0071bf, 0x0071c9, + /* e1 */ 0x0071d3, 0x0071dd, 0x0071e7, 0x0071f1, + /* e5 */ 0x0071fb, 0x007205, 0x00720f, 0x007219, + /* e9 */ 0x007223, 0x00722d, 0x007237, 0x007241, + /* ed */ 0x00724b, 0x007255, 0x00725f, 0x007269, + /* f1 */ 0x007273, 0x00727d, 0x007287, 0x007291, + /* f5 */ 0x00729b, 0x0072a5, 0x0072af, 0x0072b9, + /* f9 */ 0x0072c3, 0x0072cd, 0x0072d7, 0x0072e1, + /* fd */ 0x0072eb, 0x0072f5, + + /*** Four byte table, byte #3: 8230xx - offset 0x061cf ***/ + + /* 81 */ 0x0072ff, 0x007309, 0x007313, 0x00731d, + /* 85 */ 0x007327, 0x007331, 0x00733b, 0x007345, + /* 89 */ 0x00734f, 0x007359, 0x007363, 0x00736d, + /* 8d */ 0x007377, 0x007381, 0x00738b, 0x007395, + /* 91 */ 0x00739f, 0x0073a9, 0x0073b3, 0x0073bd, + /* 95 */ 0x0073c7, 0x0073d1, 0x0073db, 0x0073e5, + /* 99 */ 0x0073ef, 0x0073f9, 0x007403, 0x00740d, + /* 9d */ 0x007417, 0x007421, 0x00742b, 0x007435, + /* a1 */ 0x00743f, 0x007449, 0x007453, 0x00745d, + /* a5 */ 0x007467, 0x007471, 0x000000, 0x000000, /* a9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* ad */ 0x000000, 0x000000, 0x000000, 0x000000, /* b1 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6755,37 +6786,37 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* e5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* e9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* ed */ 0x000000, 0x000000, 0x000000, 0x000000, - /* f1 */ 0x000000, 0x007400, 0x00740a, 0x007414, - /* f5 */ 0x00741e, 0x007428, 0x007432, 0x00743c, - /* f9 */ 0x007446, 0x007450, 0x00745a, 0x007464, - /* fd */ 0x00746e, 0x007478, - - /*** Four byte table, byte #3: 8231xx - offset 0x061e1 ***/ - - /* 81 */ 0x007482, 0x00748c, 0x007496, 0x0074a0, - /* 85 */ 0x0074aa, 0x0074b4, 0x0074be, 0x0074c8, - /* 89 */ 0x0074d2, 0x0074dc, 0x0074e6, 0x0074f0, - /* 8d */ 0x0074fa, 0x007504, 0x00750e, 0x007518, - /* 91 */ 0x007522, 0x00752c, 0x007536, 0x007540, - /* 95 */ 0x00754a, 0x007554, 0x00755e, 0x007568, - /* 99 */ 0x007572, 0x00757c, 0x007586, 0x007590, - /* 9d */ 0x00759a, 0x0075a4, 0x0075ae, 0x0075b8, - /* a1 */ 0x0075c2, 0x0075cc, 0x0075d6, 0x0075e0, - /* a5 */ 0x0075ea, 0x0075f4, 0x0075fe, 0x007608, - /* a9 */ 0x007612, 0x00761c, 0x007626, 0x007630, - /* ad */ 0x00763a, 0x007644, 0x00764e, 0x007658, - /* b1 */ 0x007662, 0x00766c, 0x007676, 0x007680, - /* b5 */ 0x00768a, 0x007694, 0x00769e, 0x0076a8, - /* b9 */ 0x0076b2, 0x0076bc, 0x0076c6, 0x0076d0, - /* bd */ 0x0076da, 0x0076e4, 0x0076ee, 0x0076f8, - /* c1 */ 0x007702, 0x00770c, 0x007716, 0x007720, - /* c5 */ 0x00772a, 0x007734, 0x00773e, 0x007748, - /* c9 */ 0x007752, 0x00775c, 0x007766, 0x007770, - /* cd */ 0x00777a, 0x007784, 0x00778e, 0x007798, - /* d1 */ 0x0077a2, 0x0077ac, 0x0077b6, 0x0077c0, + /* f1 */ 0x000000, 0x007474, 0x00747e, 0x007488, + /* f5 */ 0x007492, 0x00749c, 0x0074a6, 0x0074b0, + /* f9 */ 0x0074ba, 0x0074c4, 0x0074ce, 0x0074d8, + /* fd */ 0x0074e2, 0x0074ec, + + /*** Four byte table, byte #3: 8231xx - offset 0x0624d ***/ + + /* 81 */ 0x0074f6, 0x007500, 0x00750a, 0x007514, + /* 85 */ 0x00751e, 0x007528, 0x007532, 0x00753c, + /* 89 */ 0x007546, 0x007550, 0x00755a, 0x007564, + /* 8d */ 0x00756e, 0x007578, 0x007582, 0x00758c, + /* 91 */ 0x007596, 0x0075a0, 0x0075aa, 0x0075b4, + /* 95 */ 0x0075be, 0x0075c8, 0x0075d2, 0x0075dc, + /* 99 */ 0x0075e6, 0x0075f0, 0x0075fa, 0x007604, + /* 9d */ 0x00760e, 0x007618, 0x007622, 0x00762c, + /* a1 */ 0x007636, 0x007640, 0x00764a, 0x007654, + /* a5 */ 0x00765e, 0x007668, 0x007672, 0x00767c, + /* a9 */ 0x007686, 0x007690, 0x00769a, 0x0076a4, + /* ad */ 0x0076ae, 0x0076b8, 0x0076c2, 0x0076cc, + /* b1 */ 0x0076d6, 0x0076e0, 0x0076ea, 0x0076f4, + /* b5 */ 0x0076fe, 0x007708, 0x007712, 0x00771c, + /* b9 */ 0x007726, 0x007730, 0x00773a, 0x007744, + /* bd */ 0x00774e, 0x007758, 0x007762, 0x00776c, + /* c1 */ 0x007776, 0x007780, 0x00778a, 0x007794, + /* c5 */ 0x00779e, 0x0077a8, 0x0077b2, 0x0077bc, + /* c9 */ 0x0077c6, 0x0077d0, 0x0077da, 0x0077e4, + /* cd */ 0x0077ee, 0x0077f8, 0x007802, 0x00780c, + /* d1 */ 0x007816, 0x007820, 0x00782a, 0x007834, /* 42 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8232xx - offset 0x06235 ***/ + /*** Four byte table, byte #3: 8232xx - offset 0x062a1 ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 85 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6798,14 +6829,14 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* a1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* a5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* a9 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* ad */ 0x000000, 0x000000, 0x0077c8, 0x0077d2, - /* b1 */ 0x0077dc, 0x0077e6, 0x0077f0, 0x0077fa, - /* b5 */ 0x007804, 0x00780e, 0x007818, 0x007822, - /* b9 */ 0x00782c, 0x007836, 0x007840, 0x00784a, - /* bd */ 0x007854, 0x00785e, 0x007868, 0x007872, - /* c1 */ 0x00787c, 0x007886, 0x007890, 0x00789a, - /* c5 */ 0x0078a4, 0x0078ae, 0x0078b8, 0x0078c2, - /* c9 */ 0x0078cc, 0x000000, 0x000000, 0x000000, + /* ad */ 0x000000, 0x000000, 0x00783c, 0x007846, + /* b1 */ 0x007850, 0x00785a, 0x007864, 0x00786e, + /* b5 */ 0x007878, 0x007882, 0x00788c, 0x007896, + /* b9 */ 0x0078a0, 0x0078aa, 0x0078b4, 0x0078be, + /* bd */ 0x0078c8, 0x0078d2, 0x0078dc, 0x0078e6, + /* c1 */ 0x0078f0, 0x0078fa, 0x007904, 0x00790e, + /* c5 */ 0x007918, 0x007922, 0x00792c, 0x007936, + /* c9 */ 0x007940, 0x000000, 0x000000, 0x000000, /* cd */ 0x000000, 0x000000, 0x000000, 0x000000, /* d1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* d5 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6816,21 +6847,21 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* e9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* ed */ 0x000000, 0x000000, 0x000000, 0x000000, /* f1 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* f5 */ 0x000000, 0x000000, 0x000000, 0x0078d3, - /* f9 */ 0x0078dd, 0x0078e7, 0x0078f1, 0x0078fb, - /* fd */ 0x007905, 0x00790f, - - /*** Four byte table, byte #3: 8233xx - offset 0x062b3 ***/ - - /* 81 */ 0x007919, 0x007923, 0x00792d, 0x007937, - /* 85 */ 0x007941, 0x00794b, 0x007955, 0x00795f, - /* 89 */ 0x007969, 0x007973, 0x00797d, 0x007987, - /* 8d */ 0x007991, 0x00799b, 0x0079a5, 0x0079af, - /* 91 */ 0x0079b9, 0x0079c3, 0x0079cd, 0x0079d7, - /* 95 */ 0x0079e1, 0x0079eb, 0x0079f5, 0x0079ff, - /* 99 */ 0x007a09, 0x007a13, 0x007a1d, 0x007a27, - /* 9d */ 0x007a31, 0x007a3b, 0x007a45, 0x007a4f, - /* a1 */ 0x007a59, 0x007a63, 0x007a6d, 0x000000, + /* f5 */ 0x000000, 0x000000, 0x000000, 0x007947, + /* f9 */ 0x007951, 0x00795b, 0x007965, 0x00796f, + /* fd */ 0x007979, 0x007983, + + /*** Four byte table, byte #3: 8233xx - offset 0x0631f ***/ + + /* 81 */ 0x00798d, 0x007997, 0x0079a1, 0x0079ab, + /* 85 */ 0x0079b5, 0x0079bf, 0x0079c9, 0x0079d3, + /* 89 */ 0x0079dd, 0x0079e7, 0x0079f1, 0x0079fb, + /* 8d */ 0x007a05, 0x007a0f, 0x007a19, 0x007a23, + /* 91 */ 0x007a2d, 0x007a37, 0x007a41, 0x007a4b, + /* 95 */ 0x007a55, 0x007a5f, 0x007a69, 0x007a73, + /* 99 */ 0x007a7d, 0x007a87, 0x007a91, 0x007a9b, + /* 9d */ 0x007aa5, 0x007aaf, 0x007ab9, 0x007ac3, + /* a1 */ 0x007acd, 0x007ad7, 0x007ae1, 0x000000, /* a5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* a9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* ad */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6840,28 +6871,28 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* bd */ 0x000000, 0x000000, 0x000000, 0x000000, /* c1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* c5 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* c9 */ 0x007a76, 0x007a80, 0x007a8a, 0x007a94, - /* cd */ 0x007a9e, 0x007aa8, 0x007ab2, 0x007abc, - /* d1 */ 0x007ac6, 0x007ad0, 0x007ada, 0x007ae4, - /* d5 */ 0x007aee, 0x007af8, 0x007b02, 0x007b0c, - /* d9 */ 0x007b16, 0x007b20, 0x007b2a, 0x007b34, - /* dd */ 0x007b3e, 0x007b48, 0x007b52, 0x007b5c, - /* e1 */ 0x007b66, 0x007b70, 0x007b7a, 0x007b84, - /* e5 */ 0x007b8e, 0x007b98, 0x007ba2, 0x007bac, + /* c9 */ 0x007aea, 0x007af4, 0x007afe, 0x007b08, + /* cd */ 0x007b12, 0x007b1c, 0x007b26, 0x007b30, + /* d1 */ 0x007b3a, 0x007b44, 0x007b4e, 0x007b58, + /* d5 */ 0x007b62, 0x007b6c, 0x007b76, 0x007b80, + /* d9 */ 0x007b8a, 0x007b94, 0x007b9e, 0x007ba8, + /* dd */ 0x007bb2, 0x007bbc, 0x007bc6, 0x007bd0, + /* e1 */ 0x007bda, 0x007be4, 0x007bee, 0x007bf8, + /* e5 */ 0x007c02, 0x007c0c, 0x007c16, 0x007c20, /* e9 */ 0x000000, /* 21 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8234xx - offset 0x0631c ***/ + /*** Four byte table, byte #3: 8234xx - offset 0x06388 ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 85 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 89 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 8d */ 0x000000, 0x000000, 0x000000, 0x000000, /* 91 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* 95 */ 0x000000, 0x007bb4, 0x007bbe, 0x007bc8, - /* 99 */ 0x007bd2, 0x007bdc, 0x007be6, 0x007bf0, - /* 9d */ 0x007bfa, 0x007c04, 0x007c0e, 0x007c18, - /* a1 */ 0x007c22, 0x000000, 0x000000, 0x000000, + /* 95 */ 0x000000, 0x007c28, 0x007c32, 0x007c3c, + /* 99 */ 0x007c46, 0x007c50, 0x007c5a, 0x007c64, + /* 9d */ 0x007c6e, 0x007c78, 0x007c82, 0x007c8c, + /* a1 */ 0x007c96, 0x000000, 0x000000, 0x000000, /* a5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* a9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* ad */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6878,20 +6909,20 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* d9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* dd */ 0x000000, 0x000000, 0x000000, 0x000000, /* e1 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* e5 */ 0x000000, 0x000000, 0x007c28, 0x007c32, - /* e9 */ 0x007c3c, 0x007c46, 0x007c50, 0x007c5a, - /* ed */ 0x007c64, 0x007c6e, 0x007c78, 0x007c82, - /* f1 */ 0x007c8c, 0x007c96, 0x007ca0, 0x007caa, - /* f5 */ 0x007cb4, 0x007cbe, 0x007cc8, 0x007cd2, - /* f9 */ 0x007cdc, 0x007ce6, 0x007cf0, 0x007cfa, - /* fd */ 0x007d04, 0x007d0e, - - /*** Four byte table, byte #3: 8235xx - offset 0x0639a ***/ - - /* 81 */ 0x007d18, 0x007d22, 0x007d2c, 0x007d36, - /* 85 */ 0x007d40, 0x007d4a, 0x007d54, 0x007d5e, - /* 89 */ 0x007d68, 0x007d72, 0x007d7c, 0x007d86, - /* 8d */ 0x007d90, 0x007d9a, 0x007da4, 0x000000, + /* e5 */ 0x000000, 0x000000, 0x007c9c, 0x007ca6, + /* e9 */ 0x007cb0, 0x007cba, 0x007cc4, 0x007cce, + /* ed */ 0x007cd8, 0x007ce2, 0x007cec, 0x007cf6, + /* f1 */ 0x007d00, 0x007d0a, 0x007d14, 0x007d1e, + /* f5 */ 0x007d28, 0x007d32, 0x007d3c, 0x007d46, + /* f9 */ 0x007d50, 0x007d5a, 0x007d64, 0x007d6e, + /* fd */ 0x007d78, 0x007d82, + + /*** Four byte table, byte #3: 8235xx - offset 0x06406 ***/ + + /* 81 */ 0x007d8c, 0x007d96, 0x007da0, 0x007daa, + /* 85 */ 0x007db4, 0x007dbe, 0x007dc8, 0x007dd2, + /* 89 */ 0x007ddc, 0x007de6, 0x007df0, 0x007dfa, + /* 8d */ 0x007e04, 0x007e0e, 0x007e18, 0x000000, /* 91 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 95 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 99 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6904,7 +6935,7 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* b5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 70 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8336xx - offset 0x063d2 ***/ + /*** Four byte table, byte #3: 8336xx - offset 0x0643e ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 85 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6923,9 +6954,9 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* b9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* bd */ 0x000000, 0x000000, 0x000000, 0x000000, /* c1 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* c5 */ 0x000000, 0x000000, 0x007da7, 0x007db1, - /* c9 */ 0x007dbb, 0x007dc5, 0x007dcf, 0x007dd9, - /* cd */ 0x007de3, 0x007ded, 0x007df7, 0x000000, + /* c5 */ 0x000000, 0x000000, 0x007e1b, 0x007e25, + /* c9 */ 0x007e2f, 0x007e39, 0x007e43, 0x007e4d, + /* cd */ 0x007e57, 0x007e61, 0x007e6b, 0x000000, /* d1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* d5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* d9 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6939,15 +6970,15 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* f9 */ 0x000000, 0x000000, /* 4 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8430xx - offset 0x0644c ***/ + /*** Four byte table, byte #3: 8430xx - offset 0x064b8 ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* 85 */ 0x007e01, 0x007e0b, 0x007e15, 0x007e1f, - /* 89 */ 0x007e29, 0x007e33, 0x007e3d, 0x007e47, - /* 8d */ 0x007e51, 0x007e5b, 0x007e65, 0x007e6f, - /* 91 */ 0x007e79, 0x007e83, 0x007e8d, 0x007e97, - /* 95 */ 0x007ea1, 0x007eab, 0x007eb5, 0x007ebf, - /* 99 */ 0x007ec9, 0x007ed3, 0x007edd, 0x007ee7, + /* 85 */ 0x007e75, 0x007e7f, 0x007e89, 0x007e93, + /* 89 */ 0x007e9d, 0x007ea7, 0x007eb1, 0x007ebb, + /* 8d */ 0x007ec5, 0x007ecf, 0x007ed9, 0x007ee3, + /* 91 */ 0x007eed, 0x007ef7, 0x007f01, 0x007f0b, + /* 95 */ 0x007f15, 0x007f1f, 0x007f29, 0x007f33, + /* 99 */ 0x007f3d, 0x007f47, 0x007f51, 0x007f5b, /* 9d */ 0x000000, 0x000000, 0x000000, 0x000000, /* a1 */ 0x000000, 0x000000, 0x000000, 0x000000, /* a5 */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -6974,17 +7005,17 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* f9 */ 0x000000, 0x000000, /* 4 trailing zero values shared with next segment */ - /*** Four byte table, byte #3: 8431xx - offset 0x064c6 ***/ + /*** Four byte table, byte #3: 8431xx - offset 0x06532 ***/ /* 81 */ 0x000000, 0x000000, 0x000000, 0x000000, - /* 85 */ 0x007eef, 0x007ef9, 0x007f03, 0x007f0d, - /* 89 */ 0x007f17, 0x007f21, 0x007f2b, 0x007f35, - /* 8d */ 0x007f3f, 0x007f49, 0x007f53, 0x007f5d, - /* 91 */ 0x007f67, 0x007f71, 0x007f7b, 0x007f85, - /* 95 */ 0x007f8f, 0x007f99, 0x007fa3, 0x007fad, - /* 99 */ 0x007fb7, 0x007fc1, 0x007fcb, 0x007fd5, - /* 9d */ 0x007fdf, 0x007fe9, 0x007ff3, 0x007ffd, - /* a1 */ 0x008007, 0x008011, 0x000000, 0x000000, + /* 85 */ 0x007f63, 0x007f6d, 0x007f77, 0x007f81, + /* 89 */ 0x007f8b, 0x007f95, 0x007f9f, 0x007fa9, + /* 8d */ 0x007fb3, 0x007fbd, 0x007fc7, 0x007fd1, + /* 91 */ 0x007fdb, 0x007fe5, 0x007fef, 0x007ff9, + /* 95 */ 0x008003, 0x00800d, 0x008017, 0x008021, + /* 99 */ 0x00802b, 0x008035, 0x00803f, 0x008049, + /* 9d */ 0x008053, 0x00805d, 0x008067, 0x008071, + /* a1 */ 0x00807b, 0x008085, 0x000000, 0x000000, /* a5 */ 0x000000, 0x000000, 0x000000, 0x000000, /* a9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* ad */ 0x000000, 0x000000, 0x000000, 0x000000, @@ -7009,4141 +7040,4147 @@ static const uint32 gb18030_to_unicode_tree_table[32795] = /* f9 */ 0x000000, 0x000000, 0x000000, 0x000000, /* fd */ 0x000000, 0x000000, - /*** Four byte table, leaf: 813081xx - offset 0x06544 ***/ + /*** Four byte table, leaf: 813081xx - offset 0x065b0 ***/ /* 30 */ 0x00c280, 0x00c281, 0x00c282, 0x00c283, /* 34 */ 0x00c284, 0x00c285, 0x00c286, 0x00c287, /* 38 */ 0x00c288, 0x00c289, - /*** Four byte table, leaf: 813082xx - offset 0x0654e ***/ + /*** Four byte table, leaf: 813082xx - offset 0x065ba ***/ /* 30 */ 0x00c28a, 0x00c28b, 0x00c28c, 0x00c28d, /* 34 */ 0x00c28e, 0x00c28f, 0x00c290, 0x00c291, /* 38 */ 0x00c292, 0x00c293, - /*** Four byte table, leaf: 813083xx - offset 0x06558 ***/ + /*** Four byte table, leaf: 813083xx - offset 0x065c4 ***/ /* 30 */ 0x00c294, 0x00c295, 0x00c296, 0x00c297, /* 34 */ 0x00c298, 0x00c299, 0x00c29a, 0x00c29b, /* 38 */ 0x00c29c, 0x00c29d, - /*** Four byte table, leaf: 813084xx - offset 0x06562 ***/ + /*** Four byte table, leaf: 813084xx - offset 0x065ce ***/ /* 30 */ 0x00c29e, 0x00c29f, 0x00c2a0, 0x00c2a1, /* 34 */ 0x00c2a2, 0x00c2a3, 0x00c2a5, 0x00c2a6, /* 38 */ 0x00c2a9, 0x00c2aa, - /*** Four byte table, leaf: 813085xx - offset 0x0656c ***/ + /*** Four byte table, leaf: 813085xx - offset 0x065d8 ***/ /* 30 */ 0x00c2ab, 0x00c2ac, 0x00c2ad, 0x00c2ae, /* 34 */ 0x00c2af, 0x00c2b2, 0x00c2b3, 0x00c2b4, /* 38 */ 0x00c2b5, 0x00c2b6, - /*** Four byte table, leaf: 813086xx - offset 0x06576 ***/ + /*** Four byte table, leaf: 813086xx - offset 0x065e2 ***/ /* 30 */ 0x00c2b8, 0x00c2b9, 0x00c2ba, 0x00c2bb, /* 34 */ 0x00c2bc, 0x00c2bd, 0x00c2be, 0x00c2bf, /* 38 */ 0x00c380, 0x00c381, - /*** Four byte table, leaf: 813087xx - offset 0x06580 ***/ + /*** Four byte table, leaf: 813087xx - offset 0x065ec ***/ /* 30 */ 0x00c382, 0x00c383, 0x00c384, 0x00c385, /* 34 */ 0x00c386, 0x00c387, 0x00c388, 0x00c389, /* 38 */ 0x00c38a, 0x00c38b, - /*** Four byte table, leaf: 813088xx - offset 0x0658a ***/ + /*** Four byte table, leaf: 813088xx - offset 0x065f6 ***/ /* 30 */ 0x00c38c, 0x00c38d, 0x00c38e, 0x00c38f, /* 34 */ 0x00c390, 0x00c391, 0x00c392, 0x00c393, /* 38 */ 0x00c394, 0x00c395, - /*** Four byte table, leaf: 813089xx - offset 0x06594 ***/ + /*** Four byte table, leaf: 813089xx - offset 0x06600 ***/ /* 30 */ 0x00c396, 0x00c398, 0x00c399, 0x00c39a, /* 34 */ 0x00c39b, 0x00c39c, 0x00c39d, 0x00c39e, /* 38 */ 0x00c39f, 0x00c3a2, - /*** Four byte table, leaf: 81308axx - offset 0x0659e ***/ + /*** Four byte table, leaf: 81308axx - offset 0x0660a ***/ /* 30 */ 0x00c3a3, 0x00c3a4, 0x00c3a5, 0x00c3a6, /* 34 */ 0x00c3a7, 0x00c3ab, 0x00c3ae, 0x00c3af, /* 38 */ 0x00c3b0, 0x00c3b1, - /*** Four byte table, leaf: 81308bxx - offset 0x065a8 ***/ + /*** Four byte table, leaf: 81308bxx - offset 0x06614 ***/ /* 30 */ 0x00c3b4, 0x00c3b5, 0x00c3b6, 0x00c3b8, /* 34 */ 0x00c3bb, 0x00c3bd, 0x00c3be, 0x00c3bf, /* 38 */ 0x00c480, 0x00c482, - /*** Four byte table, leaf: 81308cxx - offset 0x065b2 ***/ + /*** Four byte table, leaf: 81308cxx - offset 0x0661e ***/ /* 30 */ 0x00c483, 0x00c484, 0x00c485, 0x00c486, /* 34 */ 0x00c487, 0x00c488, 0x00c489, 0x00c48a, /* 38 */ 0x00c48b, 0x00c48c, - /*** Four byte table, leaf: 81308dxx - offset 0x065bc ***/ + /*** Four byte table, leaf: 81308dxx - offset 0x06628 ***/ /* 30 */ 0x00c48d, 0x00c48e, 0x00c48f, 0x00c490, /* 34 */ 0x00c491, 0x00c492, 0x00c494, 0x00c495, /* 38 */ 0x00c496, 0x00c497, - /*** Four byte table, leaf: 81308exx - offset 0x065c6 ***/ + /*** Four byte table, leaf: 81308exx - offset 0x06632 ***/ /* 30 */ 0x00c498, 0x00c499, 0x00c49a, 0x00c49c, /* 34 */ 0x00c49d, 0x00c49e, 0x00c49f, 0x00c4a0, /* 38 */ 0x00c4a1, 0x00c4a2, - /*** Four byte table, leaf: 81308fxx - offset 0x065d0 ***/ + /*** Four byte table, leaf: 81308fxx - offset 0x0663c ***/ /* 30 */ 0x00c4a3, 0x00c4a4, 0x00c4a5, 0x00c4a6, /* 34 */ 0x00c4a7, 0x00c4a8, 0x00c4a9, 0x00c4aa, /* 38 */ 0x00c4ac, 0x00c4ad, - /*** Four byte table, leaf: 813090xx - offset 0x065da ***/ + /*** Four byte table, leaf: 813090xx - offset 0x06646 ***/ /* 30 */ 0x00c4ae, 0x00c4af, 0x00c4b0, 0x00c4b1, /* 34 */ 0x00c4b2, 0x00c4b3, 0x00c4b4, 0x00c4b5, /* 38 */ 0x00c4b6, 0x00c4b7, - /*** Four byte table, leaf: 813091xx - offset 0x065e4 ***/ + /*** Four byte table, leaf: 813091xx - offset 0x06650 ***/ /* 30 */ 0x00c4b8, 0x00c4b9, 0x00c4ba, 0x00c4bb, /* 34 */ 0x00c4bc, 0x00c4bd, 0x00c4be, 0x00c4bf, /* 38 */ 0x00c580, 0x00c581, - /*** Four byte table, leaf: 813092xx - offset 0x065ee ***/ + /*** Four byte table, leaf: 813092xx - offset 0x0665a ***/ /* 30 */ 0x00c582, 0x00c583, 0x00c585, 0x00c586, /* 34 */ 0x00c587, 0x00c589, 0x00c58a, 0x00c58b, /* 38 */ 0x00c58c, 0x00c58e, - /*** Four byte table, leaf: 813093xx - offset 0x065f8 ***/ + /*** Four byte table, leaf: 813093xx - offset 0x06664 ***/ /* 30 */ 0x00c58f, 0x00c590, 0x00c591, 0x00c592, /* 34 */ 0x00c593, 0x00c594, 0x00c595, 0x00c596, /* 38 */ 0x00c597, 0x00c598, - /*** Four byte table, leaf: 813094xx - offset 0x06602 ***/ + /*** Four byte table, leaf: 813094xx - offset 0x0666e ***/ /* 30 */ 0x00c599, 0x00c59a, 0x00c59b, 0x00c59c, /* 34 */ 0x00c59d, 0x00c59e, 0x00c59f, 0x00c5a0, /* 38 */ 0x00c5a1, 0x00c5a2, - /*** Four byte table, leaf: 813095xx - offset 0x0660c ***/ + /*** Four byte table, leaf: 813095xx - offset 0x06678 ***/ /* 30 */ 0x00c5a3, 0x00c5a4, 0x00c5a5, 0x00c5a6, /* 34 */ 0x00c5a7, 0x00c5a8, 0x00c5a9, 0x00c5aa, /* 38 */ 0x00c5ac, 0x00c5ad, - /*** Four byte table, leaf: 813096xx - offset 0x06616 ***/ + /*** Four byte table, leaf: 813096xx - offset 0x06682 ***/ /* 30 */ 0x00c5ae, 0x00c5af, 0x00c5b0, 0x00c5b1, /* 34 */ 0x00c5b2, 0x00c5b3, 0x00c5b4, 0x00c5b5, /* 38 */ 0x00c5b6, 0x00c5b7, - /*** Four byte table, leaf: 813097xx - offset 0x06620 ***/ + /*** Four byte table, leaf: 813097xx - offset 0x0668c ***/ /* 30 */ 0x00c5b8, 0x00c5b9, 0x00c5ba, 0x00c5bb, /* 34 */ 0x00c5bc, 0x00c5bd, 0x00c5be, 0x00c5bf, /* 38 */ 0x00c680, 0x00c681, - /*** Four byte table, leaf: 813098xx - offset 0x0662a ***/ + /*** Four byte table, leaf: 813098xx - offset 0x06696 ***/ /* 30 */ 0x00c682, 0x00c683, 0x00c684, 0x00c685, /* 34 */ 0x00c686, 0x00c687, 0x00c688, 0x00c689, /* 38 */ 0x00c68a, 0x00c68b, - /*** Four byte table, leaf: 813099xx - offset 0x06634 ***/ + /*** Four byte table, leaf: 813099xx - offset 0x066a0 ***/ /* 30 */ 0x00c68c, 0x00c68d, 0x00c68e, 0x00c68f, /* 34 */ 0x00c690, 0x00c691, 0x00c692, 0x00c693, /* 38 */ 0x00c694, 0x00c695, - /*** Four byte table, leaf: 81309axx - offset 0x0663e ***/ + /*** Four byte table, leaf: 81309axx - offset 0x066aa ***/ /* 30 */ 0x00c696, 0x00c697, 0x00c698, 0x00c699, /* 34 */ 0x00c69a, 0x00c69b, 0x00c69c, 0x00c69d, /* 38 */ 0x00c69e, 0x00c69f, - /*** Four byte table, leaf: 81309bxx - offset 0x06648 ***/ + /*** Four byte table, leaf: 81309bxx - offset 0x066b4 ***/ /* 30 */ 0x00c6a0, 0x00c6a1, 0x00c6a2, 0x00c6a3, /* 34 */ 0x00c6a4, 0x00c6a5, 0x00c6a6, 0x00c6a7, /* 38 */ 0x00c6a8, 0x00c6a9, - /*** Four byte table, leaf: 81309cxx - offset 0x06652 ***/ + /*** Four byte table, leaf: 81309cxx - offset 0x066be ***/ /* 30 */ 0x00c6aa, 0x00c6ab, 0x00c6ac, 0x00c6ad, /* 34 */ 0x00c6ae, 0x00c6af, 0x00c6b0, 0x00c6b1, /* 38 */ 0x00c6b2, 0x00c6b3, - /*** Four byte table, leaf: 81309dxx - offset 0x0665c ***/ + /*** Four byte table, leaf: 81309dxx - offset 0x066c8 ***/ /* 30 */ 0x00c6b4, 0x00c6b5, 0x00c6b6, 0x00c6b7, /* 34 */ 0x00c6b8, 0x00c6b9, 0x00c6ba, 0x00c6bb, /* 38 */ 0x00c6bc, 0x00c6bd, - /*** Four byte table, leaf: 81309exx - offset 0x06666 ***/ + /*** Four byte table, leaf: 81309exx - offset 0x066d2 ***/ /* 30 */ 0x00c6be, 0x00c6bf, 0x00c780, 0x00c781, /* 34 */ 0x00c782, 0x00c783, 0x00c784, 0x00c785, /* 38 */ 0x00c786, 0x00c787, - /*** Four byte table, leaf: 81309fxx - offset 0x06670 ***/ + /*** Four byte table, leaf: 81309fxx - offset 0x066dc ***/ /* 30 */ 0x00c788, 0x00c789, 0x00c78a, 0x00c78b, /* 34 */ 0x00c78c, 0x00c78d, 0x00c78f, 0x00c791, /* 38 */ 0x00c793, 0x00c795, - /*** Four byte table, leaf: 8130a0xx - offset 0x0667a ***/ + /*** Four byte table, leaf: 8130a0xx - offset 0x066e6 ***/ /* 30 */ 0x00c797, 0x00c799, 0x00c79b, 0x00c79d, /* 34 */ 0x00c79e, 0x00c79f, 0x00c7a0, 0x00c7a1, /* 38 */ 0x00c7a2, 0x00c7a3, - /*** Four byte table, leaf: 8130a1xx - offset 0x06684 ***/ + /*** Four byte table, leaf: 8130a1xx - offset 0x066f0 ***/ /* 30 */ 0x00c7a4, 0x00c7a5, 0x00c7a6, 0x00c7a7, /* 34 */ 0x00c7a8, 0x00c7a9, 0x00c7aa, 0x00c7ab, /* 38 */ 0x00c7ac, 0x00c7ad, - /*** Four byte table, leaf: 8130a2xx - offset 0x0668e ***/ + /*** Four byte table, leaf: 8130a2xx - offset 0x066fa ***/ /* 30 */ 0x00c7ae, 0x00c7af, 0x00c7b0, 0x00c7b1, /* 34 */ 0x00c7b2, 0x00c7b3, 0x00c7b4, 0x00c7b5, /* 38 */ 0x00c7b6, 0x00c7b7, - /*** Four byte table, leaf: 8130a3xx - offset 0x06698 ***/ + /*** Four byte table, leaf: 8130a3xx - offset 0x06704 ***/ /* 30 */ 0x00c7b8, 0x00c7ba, 0x00c7bb, 0x00c7bc, /* 34 */ 0x00c7bd, 0x00c7be, 0x00c7bf, 0x00c880, /* 38 */ 0x00c881, 0x00c882, - /*** Four byte table, leaf: 8130a4xx - offset 0x066a2 ***/ + /*** Four byte table, leaf: 8130a4xx - offset 0x0670e ***/ /* 30 */ 0x00c883, 0x00c884, 0x00c885, 0x00c886, /* 34 */ 0x00c887, 0x00c888, 0x00c889, 0x00c88a, /* 38 */ 0x00c88b, 0x00c88c, - /*** Four byte table, leaf: 8130a5xx - offset 0x066ac ***/ + /*** Four byte table, leaf: 8130a5xx - offset 0x06718 ***/ /* 30 */ 0x00c88d, 0x00c88e, 0x00c88f, 0x00c890, /* 34 */ 0x00c891, 0x00c892, 0x00c893, 0x00c894, /* 38 */ 0x00c895, 0x00c896, - /*** Four byte table, leaf: 8130a6xx - offset 0x066b6 ***/ + /*** Four byte table, leaf: 8130a6xx - offset 0x06722 ***/ /* 30 */ 0x00c897, 0x00c898, 0x00c899, 0x00c89a, /* 34 */ 0x00c89b, 0x00c89c, 0x00c89d, 0x00c89e, /* 38 */ 0x00c89f, 0x00c8a0, - /*** Four byte table, leaf: 8130a7xx - offset 0x066c0 ***/ + /*** Four byte table, leaf: 8130a7xx - offset 0x0672c ***/ /* 30 */ 0x00c8a1, 0x00c8a2, 0x00c8a3, 0x00c8a4, /* 34 */ 0x00c8a5, 0x00c8a6, 0x00c8a7, 0x00c8a8, /* 38 */ 0x00c8a9, 0x00c8aa, - /*** Four byte table, leaf: 8130a8xx - offset 0x066ca ***/ + /*** Four byte table, leaf: 8130a8xx - offset 0x06736 ***/ /* 30 */ 0x00c8ab, 0x00c8ac, 0x00c8ad, 0x00c8ae, /* 34 */ 0x00c8af, 0x00c8b0, 0x00c8b1, 0x00c8b2, /* 38 */ 0x00c8b3, 0x00c8b4, - /*** Four byte table, leaf: 8130a9xx - offset 0x066d4 ***/ + /*** Four byte table, leaf: 8130a9xx - offset 0x06740 ***/ /* 30 */ 0x00c8b5, 0x00c8b6, 0x00c8b7, 0x00c8b8, /* 34 */ 0x00c8b9, 0x00c8ba, 0x00c8bb, 0x00c8bc, /* 38 */ 0x00c8bd, 0x00c8be, - /*** Four byte table, leaf: 8130aaxx - offset 0x066de ***/ + /*** Four byte table, leaf: 8130aaxx - offset 0x0674a ***/ /* 30 */ 0x00c8bf, 0x00c980, 0x00c981, 0x00c982, /* 34 */ 0x00c983, 0x00c984, 0x00c985, 0x00c986, /* 38 */ 0x00c987, 0x00c988, - /*** Four byte table, leaf: 8130abxx - offset 0x066e8 ***/ + /*** Four byte table, leaf: 8130abxx - offset 0x06754 ***/ /* 30 */ 0x00c989, 0x00c98a, 0x00c98b, 0x00c98c, /* 34 */ 0x00c98d, 0x00c98e, 0x00c98f, 0x00c990, /* 38 */ 0x00c992, 0x00c993, - /*** Four byte table, leaf: 8130acxx - offset 0x066f2 ***/ + /*** Four byte table, leaf: 8130acxx - offset 0x0675e ***/ /* 30 */ 0x00c994, 0x00c995, 0x00c996, 0x00c997, /* 34 */ 0x00c998, 0x00c999, 0x00c99a, 0x00c99b, /* 38 */ 0x00c99c, 0x00c99d, - /*** Four byte table, leaf: 8130adxx - offset 0x066fc ***/ + /*** Four byte table, leaf: 8130adxx - offset 0x06768 ***/ /* 30 */ 0x00c99e, 0x00c99f, 0x00c9a0, 0x00c9a2, /* 34 */ 0x00c9a3, 0x00c9a4, 0x00c9a5, 0x00c9a6, /* 38 */ 0x00c9a7, 0x00c9a8, - /*** Four byte table, leaf: 8130aexx - offset 0x06706 ***/ + /*** Four byte table, leaf: 8130aexx - offset 0x06772 ***/ /* 30 */ 0x00c9a9, 0x00c9aa, 0x00c9ab, 0x00c9ac, /* 34 */ 0x00c9ad, 0x00c9ae, 0x00c9af, 0x00c9b0, /* 38 */ 0x00c9b1, 0x00c9b2, - /*** Four byte table, leaf: 8130afxx - offset 0x06710 ***/ + /*** Four byte table, leaf: 8130afxx - offset 0x0677c ***/ /* 30 */ 0x00c9b3, 0x00c9b4, 0x00c9b5, 0x00c9b6, /* 34 */ 0x00c9b7, 0x00c9b8, 0x00c9b9, 0x00c9ba, /* 38 */ 0x00c9bb, 0x00c9bc, - /*** Four byte table, leaf: 8130b0xx - offset 0x0671a ***/ + /*** Four byte table, leaf: 8130b0xx - offset 0x06786 ***/ /* 30 */ 0x00c9bd, 0x00c9be, 0x00c9bf, 0x00ca80, /* 34 */ 0x00ca81, 0x00ca82, 0x00ca83, 0x00ca84, /* 38 */ 0x00ca85, 0x00ca86, - /*** Four byte table, leaf: 8130b1xx - offset 0x06724 ***/ + /*** Four byte table, leaf: 8130b1xx - offset 0x06790 ***/ /* 30 */ 0x00ca87, 0x00ca88, 0x00ca89, 0x00ca8a, /* 34 */ 0x00ca8b, 0x00ca8c, 0x00ca8d, 0x00ca8e, /* 38 */ 0x00ca8f, 0x00ca90, - /*** Four byte table, leaf: 8130b2xx - offset 0x0672e ***/ + /*** Four byte table, leaf: 8130b2xx - offset 0x0679a ***/ /* 30 */ 0x00ca91, 0x00ca92, 0x00ca93, 0x00ca94, /* 34 */ 0x00ca95, 0x00ca96, 0x00ca97, 0x00ca98, /* 38 */ 0x00ca99, 0x00ca9a, - /*** Four byte table, leaf: 8130b3xx - offset 0x06738 ***/ + /*** Four byte table, leaf: 8130b3xx - offset 0x067a4 ***/ /* 30 */ 0x00ca9b, 0x00ca9c, 0x00ca9d, 0x00ca9e, /* 34 */ 0x00ca9f, 0x00caa0, 0x00caa1, 0x00caa2, /* 38 */ 0x00caa3, 0x00caa4, - /*** Four byte table, leaf: 8130b4xx - offset 0x06742 ***/ + /*** Four byte table, leaf: 8130b4xx - offset 0x067ae ***/ /* 30 */ 0x00caa5, 0x00caa6, 0x00caa7, 0x00caa8, /* 34 */ 0x00caa9, 0x00caaa, 0x00caab, 0x00caac, /* 38 */ 0x00caad, 0x00caae, - /*** Four byte table, leaf: 8130b5xx - offset 0x0674c ***/ + /*** Four byte table, leaf: 8130b5xx - offset 0x067b8 ***/ /* 30 */ 0x00caaf, 0x00cab0, 0x00cab1, 0x00cab2, /* 34 */ 0x00cab3, 0x00cab4, 0x00cab5, 0x00cab6, /* 38 */ 0x00cab7, 0x00cab8, - /*** Four byte table, leaf: 8130b6xx - offset 0x06756 ***/ + /*** Four byte table, leaf: 8130b6xx - offset 0x067c2 ***/ /* 30 */ 0x00cab9, 0x00caba, 0x00cabb, 0x00cabc, /* 34 */ 0x00cabd, 0x00cabe, 0x00cabf, 0x00cb80, /* 38 */ 0x00cb81, 0x00cb82, - /*** Four byte table, leaf: 8130b7xx - offset 0x06760 ***/ + /*** Four byte table, leaf: 8130b7xx - offset 0x067cc ***/ /* 30 */ 0x00cb83, 0x00cb84, 0x00cb85, 0x00cb86, /* 34 */ 0x00cb88, 0x00cb8c, 0x00cb8d, 0x00cb8e, /* 38 */ 0x00cb8f, 0x00cb90, - /*** Four byte table, leaf: 8130b8xx - offset 0x0676a ***/ + /*** Four byte table, leaf: 8130b8xx - offset 0x067d6 ***/ /* 30 */ 0x00cb91, 0x00cb92, 0x00cb93, 0x00cb94, /* 34 */ 0x00cb95, 0x00cb96, 0x00cb97, 0x00cb98, /* 38 */ 0x00cb9a, 0x00cb9b, - /*** Four byte table, leaf: 8130b9xx - offset 0x06774 ***/ + /*** Four byte table, leaf: 8130b9xx - offset 0x067e0 ***/ /* 30 */ 0x00cb9c, 0x00cb9d, 0x00cb9e, 0x00cb9f, /* 34 */ 0x00cba0, 0x00cba1, 0x00cba2, 0x00cba3, /* 38 */ 0x00cba4, 0x00cba5, - /*** Four byte table, leaf: 8130baxx - offset 0x0677e ***/ + /*** Four byte table, leaf: 8130baxx - offset 0x067ea ***/ /* 30 */ 0x00cba6, 0x00cba7, 0x00cba8, 0x00cba9, /* 34 */ 0x00cbaa, 0x00cbab, 0x00cbac, 0x00cbad, /* 38 */ 0x00cbae, 0x00cbaf, - /*** Four byte table, leaf: 8130bbxx - offset 0x06788 ***/ + /*** Four byte table, leaf: 8130bbxx - offset 0x067f4 ***/ /* 30 */ 0x00cbb0, 0x00cbb1, 0x00cbb2, 0x00cbb3, /* 34 */ 0x00cbb4, 0x00cbb5, 0x00cbb6, 0x00cbb7, /* 38 */ 0x00cbb8, 0x00cbb9, - /*** Four byte table, leaf: 8130bcxx - offset 0x06792 ***/ + /*** Four byte table, leaf: 8130bcxx - offset 0x067fe ***/ /* 30 */ 0x00cbba, 0x00cbbb, 0x00cbbc, 0x00cbbd, /* 34 */ 0x00cbbe, 0x00cbbf, 0x00cc80, 0x00cc81, /* 38 */ 0x00cc82, 0x00cc83, - /*** Four byte table, leaf: 8130bdxx - offset 0x0679c ***/ + /*** Four byte table, leaf: 8130bdxx - offset 0x06808 ***/ /* 30 */ 0x00cc84, 0x00cc85, 0x00cc86, 0x00cc87, /* 34 */ 0x00cc88, 0x00cc89, 0x00cc8a, 0x00cc8b, /* 38 */ 0x00cc8c, 0x00cc8d, - /*** Four byte table, leaf: 8130bexx - offset 0x067a6 ***/ + /*** Four byte table, leaf: 8130bexx - offset 0x06812 ***/ /* 30 */ 0x00cc8e, 0x00cc8f, 0x00cc90, 0x00cc91, /* 34 */ 0x00cc92, 0x00cc93, 0x00cc94, 0x00cc95, /* 38 */ 0x00cc96, 0x00cc97, - /*** Four byte table, leaf: 8130bfxx - offset 0x067b0 ***/ + /*** Four byte table, leaf: 8130bfxx - offset 0x0681c ***/ /* 30 */ 0x00cc98, 0x00cc99, 0x00cc9a, 0x00cc9b, /* 34 */ 0x00cc9c, 0x00cc9d, 0x00cc9e, 0x00cc9f, /* 38 */ 0x00cca0, 0x00cca1, - /*** Four byte table, leaf: 8130c0xx - offset 0x067ba ***/ + /*** Four byte table, leaf: 8130c0xx - offset 0x06826 ***/ /* 30 */ 0x00cca2, 0x00cca3, 0x00cca4, 0x00cca5, /* 34 */ 0x00cca6, 0x00cca7, 0x00cca8, 0x00cca9, /* 38 */ 0x00ccaa, 0x00ccab, - /*** Four byte table, leaf: 8130c1xx - offset 0x067c4 ***/ + /*** Four byte table, leaf: 8130c1xx - offset 0x06830 ***/ /* 30 */ 0x00ccac, 0x00ccad, 0x00ccae, 0x00ccaf, /* 34 */ 0x00ccb0, 0x00ccb1, 0x00ccb2, 0x00ccb3, /* 38 */ 0x00ccb4, 0x00ccb5, - /*** Four byte table, leaf: 8130c2xx - offset 0x067ce ***/ + /*** Four byte table, leaf: 8130c2xx - offset 0x0683a ***/ /* 30 */ 0x00ccb6, 0x00ccb7, 0x00ccb8, 0x00ccb9, /* 34 */ 0x00ccba, 0x00ccbb, 0x00ccbc, 0x00ccbd, /* 38 */ 0x00ccbe, 0x00ccbf, - /*** Four byte table, leaf: 8130c3xx - offset 0x067d8 ***/ + /*** Four byte table, leaf: 8130c3xx - offset 0x06844 ***/ /* 30 */ 0x00cd80, 0x00cd81, 0x00cd82, 0x00cd83, /* 34 */ 0x00cd84, 0x00cd85, 0x00cd86, 0x00cd87, /* 38 */ 0x00cd88, 0x00cd89, - /*** Four byte table, leaf: 8130c4xx - offset 0x067e2 ***/ + /*** Four byte table, leaf: 8130c4xx - offset 0x0684e ***/ /* 30 */ 0x00cd8a, 0x00cd8b, 0x00cd8c, 0x00cd8d, /* 34 */ 0x00cd8e, 0x00cd8f, 0x00cd90, 0x00cd91, /* 38 */ 0x00cd92, 0x00cd93, - /*** Four byte table, leaf: 8130c5xx - offset 0x067ec ***/ + /*** Four byte table, leaf: 8130c5xx - offset 0x06858 ***/ /* 30 */ 0x00cd94, 0x00cd95, 0x00cd96, 0x00cd97, /* 34 */ 0x00cd98, 0x00cd99, 0x00cd9a, 0x00cd9b, /* 38 */ 0x00cd9c, 0x00cd9d, - /*** Four byte table, leaf: 8130c6xx - offset 0x067f6 ***/ + /*** Four byte table, leaf: 8130c6xx - offset 0x06862 ***/ /* 30 */ 0x00cd9e, 0x00cd9f, 0x00cda0, 0x00cda1, /* 34 */ 0x00cda2, 0x00cda3, 0x00cda4, 0x00cda5, /* 38 */ 0x00cda6, 0x00cda7, - /*** Four byte table, leaf: 8130c7xx - offset 0x06800 ***/ + /*** Four byte table, leaf: 8130c7xx - offset 0x0686c ***/ /* 30 */ 0x00cda8, 0x00cda9, 0x00cdaa, 0x00cdab, /* 34 */ 0x00cdac, 0x00cdad, 0x00cdae, 0x00cdaf, /* 38 */ 0x00cdb0, 0x00cdb1, - /*** Four byte table, leaf: 8130c8xx - offset 0x0680a ***/ + /*** Four byte table, leaf: 8130c8xx - offset 0x06876 ***/ /* 30 */ 0x00cdb2, 0x00cdb3, 0x00cdb4, 0x00cdb5, /* 34 */ 0x00cdb6, 0x00cdb7, 0x00cdb8, 0x00cdb9, /* 38 */ 0x00cdba, 0x00cdbb, - /*** Four byte table, leaf: 8130c9xx - offset 0x06814 ***/ + /*** Four byte table, leaf: 8130c9xx - offset 0x06880 ***/ /* 30 */ 0x00cdbc, 0x00cdbd, 0x00cdbe, 0x00cdbf, /* 34 */ 0x00ce80, 0x00ce81, 0x00ce82, 0x00ce83, /* 38 */ 0x00ce84, 0x00ce85, - /*** Four byte table, leaf: 8130caxx - offset 0x0681e ***/ + /*** Four byte table, leaf: 8130caxx - offset 0x0688a ***/ /* 30 */ 0x00ce86, 0x00ce87, 0x00ce88, 0x00ce89, /* 34 */ 0x00ce8a, 0x00ce8b, 0x00ce8c, 0x00ce8d, /* 38 */ 0x00ce8e, 0x00ce8f, - /*** Four byte table, leaf: 8130cbxx - offset 0x06828 ***/ + /*** Four byte table, leaf: 8130cbxx - offset 0x06894 ***/ /* 30 */ 0x00ce90, 0x00cea2, 0x00ceaa, 0x00ceab, /* 34 */ 0x00ceac, 0x00cead, 0x00ceae, 0x00ceaf, /* 38 */ 0x00ceb0, 0x00cf82, - /*** Four byte table, leaf: 8130ccxx - offset 0x06832 ***/ + /*** Four byte table, leaf: 8130ccxx - offset 0x0689e ***/ /* 30 */ 0x00cf8a, 0x00cf8b, 0x00cf8c, 0x00cf8d, /* 34 */ 0x00cf8e, 0x00cf8f, 0x00cf90, 0x00cf91, /* 38 */ 0x00cf92, 0x00cf93, - /*** Four byte table, leaf: 8130cdxx - offset 0x0683c ***/ + /*** Four byte table, leaf: 8130cdxx - offset 0x068a8 ***/ /* 30 */ 0x00cf94, 0x00cf95, 0x00cf96, 0x00cf97, /* 34 */ 0x00cf98, 0x00cf99, 0x00cf9a, 0x00cf9b, /* 38 */ 0x00cf9c, 0x00cf9d, - /*** Four byte table, leaf: 8130cexx - offset 0x06846 ***/ + /*** Four byte table, leaf: 8130cexx - offset 0x068b2 ***/ /* 30 */ 0x00cf9e, 0x00cf9f, 0x00cfa0, 0x00cfa1, /* 34 */ 0x00cfa2, 0x00cfa3, 0x00cfa4, 0x00cfa5, /* 38 */ 0x00cfa6, 0x00cfa7, - /*** Four byte table, leaf: 8130cfxx - offset 0x06850 ***/ + /*** Four byte table, leaf: 8130cfxx - offset 0x068bc ***/ /* 30 */ 0x00cfa8, 0x00cfa9, 0x00cfaa, 0x00cfab, /* 34 */ 0x00cfac, 0x00cfad, 0x00cfae, 0x00cfaf, /* 38 */ 0x00cfb0, 0x00cfb1, - /*** Four byte table, leaf: 8130d0xx - offset 0x0685a ***/ + /*** Four byte table, leaf: 8130d0xx - offset 0x068c6 ***/ /* 30 */ 0x00cfb2, 0x00cfb3, 0x00cfb4, 0x00cfb5, /* 34 */ 0x00cfb6, 0x00cfb7, 0x00cfb8, 0x00cfb9, /* 38 */ 0x00cfba, 0x00cfbb, - /*** Four byte table, leaf: 8130d1xx - offset 0x06864 ***/ + /*** Four byte table, leaf: 8130d1xx - offset 0x068d0 ***/ /* 30 */ 0x00cfbc, 0x00cfbd, 0x00cfbe, 0x00cfbf, /* 34 */ 0x00d080, 0x00d082, 0x00d083, 0x00d084, /* 38 */ 0x00d085, 0x00d086, - /*** Four byte table, leaf: 8130d2xx - offset 0x0686e ***/ + /*** Four byte table, leaf: 8130d2xx - offset 0x068da ***/ /* 30 */ 0x00d087, 0x00d088, 0x00d089, 0x00d08a, /* 34 */ 0x00d08b, 0x00d08c, 0x00d08d, 0x00d08e, /* 38 */ 0x00d08f, 0x00d190, - /*** Four byte table, leaf: 8136a5xx - offset 0x06878 ***/ + /*** Four byte table, leaf: 8135f4xx - offset 0x068e4 ***/ + + /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, + /* 34 */ 0x000000, 0x000000, 0x000000, 0xee9f87, + /* 2 trailing zero values shared with next segment */ + + /*** Four byte table, leaf: 8136a5xx - offset 0x068ec ***/ /* 30 */ 0x000000, 0x000000, 0xe28091, 0xe28092, /* 34 */ 0xe28097, 0xe2809a, 0xe2809b, 0xe2809e, /* 38 */ 0xe2809f, 0xe280a0, - /*** Four byte table, leaf: 8136a6xx - offset 0x06882 ***/ + /*** Four byte table, leaf: 8136a6xx - offset 0x068f6 ***/ /* 30 */ 0xe280a1, 0xe280a2, 0xe280a3, 0xe280a4, /* 34 */ 0xe280a7, 0xe280a8, 0xe280a9, 0xe280aa, /* 38 */ 0xe280ab, 0xe280ac, - /*** Four byte table, leaf: 8136a7xx - offset 0x0688c ***/ + /*** Four byte table, leaf: 8136a7xx - offset 0x06900 ***/ /* 30 */ 0xe280ad, 0xe280ae, 0xe280af, 0xe280b1, /* 34 */ 0xe280b4, 0xe280b6, 0xe280b7, 0xe280b8, /* 38 */ 0xe280b9, 0xe280ba, - /*** Four byte table, leaf: 8136a8xx - offset 0x06896 ***/ + /*** Four byte table, leaf: 8136a8xx - offset 0x0690a ***/ /* 30 */ 0xe280bc, 0xe280bd, 0xe280be, 0xe280bf, /* 34 */ 0xe28180, 0xe28181, 0xe28182, 0xe28183, /* 38 */ 0xe28184, 0xe28185, - /*** Four byte table, leaf: 8136a9xx - offset 0x068a0 ***/ + /*** Four byte table, leaf: 8136a9xx - offset 0x06914 ***/ /* 30 */ 0xe28186, 0xe28187, 0xe28188, 0xe28189, /* 34 */ 0xe2818a, 0xe2818b, 0xe2818c, 0xe2818d, /* 38 */ 0xe2818e, 0xe2818f, - /*** Four byte table, leaf: 8136aaxx - offset 0x068aa ***/ + /*** Four byte table, leaf: 8136aaxx - offset 0x0691e ***/ /* 30 */ 0xe28190, 0xe28191, 0xe28192, 0xe28193, /* 34 */ 0xe28194, 0xe28195, 0xe28196, 0xe28197, /* 38 */ 0xe28198, 0xe28199, - /*** Four byte table, leaf: 8136abxx - offset 0x068b4 ***/ + /*** Four byte table, leaf: 8136abxx - offset 0x06928 ***/ /* 30 */ 0xe2819a, 0xe2819b, 0xe2819c, 0xe2819d, /* 34 */ 0xe2819e, 0xe2819f, 0xe281a0, 0xe281a1, /* 38 */ 0xe281a2, 0xe281a3, - /*** Four byte table, leaf: 8136acxx - offset 0x068be ***/ + /*** Four byte table, leaf: 8136acxx - offset 0x06932 ***/ /* 30 */ 0xe281a4, 0xe281a5, 0xe281a6, 0xe281a7, /* 34 */ 0xe281a8, 0xe281a9, 0xe281aa, 0xe281ab, /* 38 */ 0xe281ac, 0xe281ad, - /*** Four byte table, leaf: 8136adxx - offset 0x068c8 ***/ + /*** Four byte table, leaf: 8136adxx - offset 0x0693c ***/ /* 30 */ 0xe281ae, 0xe281af, 0xe281b0, 0xe281b1, /* 34 */ 0xe281b2, 0xe281b3, 0xe281b4, 0xe281b5, /* 38 */ 0xe281b6, 0xe281b7, - /*** Four byte table, leaf: 8136aexx - offset 0x068d2 ***/ + /*** Four byte table, leaf: 8136aexx - offset 0x06946 ***/ /* 30 */ 0xe281b8, 0xe281b9, 0xe281ba, 0xe281bb, /* 34 */ 0xe281bc, 0xe281bd, 0xe281be, 0xe281bf, /* 38 */ 0xe28280, 0xe28281, - /*** Four byte table, leaf: 8136afxx - offset 0x068dc ***/ + /*** Four byte table, leaf: 8136afxx - offset 0x06950 ***/ /* 30 */ 0xe28282, 0xe28283, 0xe28284, 0xe28285, /* 34 */ 0xe28286, 0xe28287, 0xe28288, 0xe28289, /* 38 */ 0xe2828a, 0xe2828b, - /*** Four byte table, leaf: 8136b0xx - offset 0x068e6 ***/ + /*** Four byte table, leaf: 8136b0xx - offset 0x0695a ***/ /* 30 */ 0xe2828c, 0xe2828d, 0xe2828e, 0xe2828f, /* 34 */ 0xe28290, 0xe28291, 0xe28292, 0xe28293, /* 38 */ 0xe28294, 0xe28295, - /*** Four byte table, leaf: 8136b1xx - offset 0x068f0 ***/ + /*** Four byte table, leaf: 8136b1xx - offset 0x06964 ***/ /* 30 */ 0xe28296, 0xe28297, 0xe28298, 0xe28299, /* 34 */ 0xe2829a, 0xe2829b, 0xe2829c, 0xe2829d, /* 38 */ 0xe2829e, 0xe2829f, - /*** Four byte table, leaf: 8136b2xx - offset 0x068fa ***/ + /*** Four byte table, leaf: 8136b2xx - offset 0x0696e ***/ /* 30 */ 0xe282a0, 0xe282a1, 0xe282a2, 0xe282a3, /* 34 */ 0xe282a4, 0xe282a5, 0xe282a6, 0xe282a7, /* 38 */ 0xe282a8, 0xe282a9, - /*** Four byte table, leaf: 8136b3xx - offset 0x06904 ***/ + /*** Four byte table, leaf: 8136b3xx - offset 0x06978 ***/ /* 30 */ 0xe282aa, 0xe282ab, 0xe282ad, 0xe282ae, /* 34 */ 0xe282af, 0xe282b0, 0xe282b1, 0xe282b2, /* 38 */ 0xe282b3, 0xe282b4, - /*** Four byte table, leaf: 8136b4xx - offset 0x0690e ***/ + /*** Four byte table, leaf: 8136b4xx - offset 0x06982 ***/ /* 30 */ 0xe282b5, 0xe282b6, 0xe282b7, 0xe282b8, /* 34 */ 0xe282b9, 0xe282ba, 0xe282bb, 0xe282bc, /* 38 */ 0xe282bd, 0xe282be, - /*** Four byte table, leaf: 8136b5xx - offset 0x06918 ***/ + /*** Four byte table, leaf: 8136b5xx - offset 0x0698c ***/ /* 30 */ 0xe282bf, 0xe28380, 0xe28381, 0xe28382, /* 34 */ 0xe28383, 0xe28384, 0xe28385, 0xe28386, /* 38 */ 0xe28387, 0xe28388, - /*** Four byte table, leaf: 8136b6xx - offset 0x06922 ***/ + /*** Four byte table, leaf: 8136b6xx - offset 0x06996 ***/ /* 30 */ 0xe28389, 0xe2838a, 0xe2838b, 0xe2838c, /* 34 */ 0xe2838d, 0xe2838e, 0xe2838f, 0xe28390, /* 38 */ 0xe28391, 0xe28392, - /*** Four byte table, leaf: 8136b7xx - offset 0x0692c ***/ + /*** Four byte table, leaf: 8136b7xx - offset 0x069a0 ***/ /* 30 */ 0xe28393, 0xe28394, 0xe28395, 0xe28396, /* 34 */ 0xe28397, 0xe28398, 0xe28399, 0xe2839a, /* 38 */ 0xe2839b, 0xe2839c, - /*** Four byte table, leaf: 8136b8xx - offset 0x06936 ***/ + /*** Four byte table, leaf: 8136b8xx - offset 0x069aa ***/ /* 30 */ 0xe2839d, 0xe2839e, 0xe2839f, 0xe283a0, /* 34 */ 0xe283a1, 0xe283a2, 0xe283a3, 0xe283a4, /* 38 */ 0xe283a5, 0xe283a6, - /*** Four byte table, leaf: 8136b9xx - offset 0x06940 ***/ + /*** Four byte table, leaf: 8136b9xx - offset 0x069b4 ***/ /* 30 */ 0xe283a7, 0xe283a8, 0xe283a9, 0xe283aa, /* 34 */ 0xe283ab, 0xe283ac, 0xe283ad, 0xe283ae, /* 38 */ 0xe283af, 0xe283b0, - /*** Four byte table, leaf: 8136baxx - offset 0x0694a ***/ + /*** Four byte table, leaf: 8136baxx - offset 0x069be ***/ /* 30 */ 0xe283b1, 0xe283b2, 0xe283b3, 0xe283b4, /* 34 */ 0xe283b5, 0xe283b6, 0xe283b7, 0xe283b8, /* 38 */ 0xe283b9, 0xe283ba, - /*** Four byte table, leaf: 8136bbxx - offset 0x06954 ***/ + /*** Four byte table, leaf: 8136bbxx - offset 0x069c8 ***/ /* 30 */ 0xe283bb, 0xe283bc, 0xe283bd, 0xe283be, /* 34 */ 0xe283bf, 0xe28480, 0xe28481, 0xe28482, /* 38 */ 0xe28484, 0xe28486, - /*** Four byte table, leaf: 8136bcxx - offset 0x0695e ***/ + /*** Four byte table, leaf: 8136bcxx - offset 0x069d2 ***/ /* 30 */ 0xe28487, 0xe28488, 0xe2848a, 0xe2848b, /* 34 */ 0xe2848c, 0xe2848d, 0xe2848e, 0xe2848f, /* 38 */ 0xe28490, 0xe28491, - /*** Four byte table, leaf: 8136bdxx - offset 0x06968 ***/ + /*** Four byte table, leaf: 8136bdxx - offset 0x069dc ***/ /* 30 */ 0xe28492, 0xe28493, 0xe28494, 0xe28495, /* 34 */ 0xe28497, 0xe28498, 0xe28499, 0xe2849a, /* 38 */ 0xe2849b, 0xe2849c, - /*** Four byte table, leaf: 8136bexx - offset 0x06972 ***/ + /*** Four byte table, leaf: 8136bexx - offset 0x069e6 ***/ /* 30 */ 0xe2849d, 0xe2849e, 0xe2849f, 0xe284a0, /* 34 */ 0xe284a2, 0xe284a3, 0xe284a4, 0xe284a5, /* 38 */ 0xe284a6, 0xe284a7, - /*** Four byte table, leaf: 8136bfxx - offset 0x0697c ***/ + /*** Four byte table, leaf: 8136bfxx - offset 0x069f0 ***/ /* 30 */ 0xe284a8, 0xe284a9, 0xe284aa, 0xe284ab, /* 34 */ 0xe284ac, 0xe284ad, 0xe284ae, 0xe284af, /* 38 */ 0xe284b0, 0xe284b1, - /*** Four byte table, leaf: 8136c0xx - offset 0x06986 ***/ + /*** Four byte table, leaf: 8136c0xx - offset 0x069fa ***/ /* 30 */ 0xe284b2, 0xe284b3, 0xe284b4, 0xe284b5, /* 34 */ 0xe284b6, 0xe284b7, 0xe284b8, 0xe284b9, /* 38 */ 0xe284ba, 0xe284bb, - /*** Four byte table, leaf: 8136c1xx - offset 0x06990 ***/ + /*** Four byte table, leaf: 8136c1xx - offset 0x06a04 ***/ /* 30 */ 0xe284bc, 0xe284bd, 0xe284be, 0xe284bf, /* 34 */ 0xe28580, 0xe28581, 0xe28582, 0xe28583, /* 38 */ 0xe28584, 0xe28585, - /*** Four byte table, leaf: 8136c2xx - offset 0x0699a ***/ + /*** Four byte table, leaf: 8136c2xx - offset 0x06a0e ***/ /* 30 */ 0xe28586, 0xe28587, 0xe28588, 0xe28589, /* 34 */ 0xe2858a, 0xe2858b, 0xe2858c, 0xe2858d, /* 38 */ 0xe2858e, 0xe2858f, - /*** Four byte table, leaf: 8136c3xx - offset 0x069a4 ***/ + /*** Four byte table, leaf: 8136c3xx - offset 0x06a18 ***/ /* 30 */ 0xe28590, 0xe28591, 0xe28592, 0xe28593, /* 34 */ 0xe28594, 0xe28595, 0xe28596, 0xe28597, /* 38 */ 0xe28598, 0xe28599, - /*** Four byte table, leaf: 8136c4xx - offset 0x069ae ***/ + /*** Four byte table, leaf: 8136c4xx - offset 0x06a22 ***/ /* 30 */ 0xe2859a, 0xe2859b, 0xe2859c, 0xe2859d, /* 34 */ 0xe2859e, 0xe2859f, 0xe285ac, 0xe285ad, /* 38 */ 0xe285ae, 0xe285af, - /*** Four byte table, leaf: 8136c5xx - offset 0x069b8 ***/ + /*** Four byte table, leaf: 8136c5xx - offset 0x06a2c ***/ /* 30 */ 0xe285ba, 0xe285bb, 0xe285bc, 0xe285bd, /* 34 */ 0xe285be, 0xe285bf, 0xe28680, 0xe28681, /* 38 */ 0xe28682, 0xe28683, - /*** Four byte table, leaf: 8136c6xx - offset 0x069c2 ***/ + /*** Four byte table, leaf: 8136c6xx - offset 0x06a36 ***/ /* 30 */ 0xe28684, 0xe28685, 0xe28686, 0xe28687, /* 34 */ 0xe28688, 0xe28689, 0xe2868a, 0xe2868b, /* 38 */ 0xe2868c, 0xe2868d, - /*** Four byte table, leaf: 8136c7xx - offset 0x069cc ***/ + /*** Four byte table, leaf: 8136c7xx - offset 0x06a40 ***/ /* 30 */ 0xe2868e, 0xe2868f, 0xe28694, 0xe28695, /* 34 */ 0xe2869a, 0xe2869b, 0xe2869c, 0xe2869d, /* 38 */ 0xe2869e, 0xe2869f, - /*** Four byte table, leaf: 8136c8xx - offset 0x069d6 ***/ + /*** Four byte table, leaf: 8136c8xx - offset 0x06a4a ***/ /* 30 */ 0xe286a0, 0xe286a1, 0xe286a2, 0xe286a3, /* 34 */ 0xe286a4, 0xe286a5, 0xe286a6, 0xe286a7, /* 38 */ 0xe286a8, 0xe286a9, - /*** Four byte table, leaf: 8136c9xx - offset 0x069e0 ***/ + /*** Four byte table, leaf: 8136c9xx - offset 0x06a54 ***/ /* 30 */ 0xe286aa, 0xe286ab, 0xe286ac, 0xe286ad, /* 34 */ 0xe286ae, 0xe286af, 0xe286b0, 0xe286b1, /* 38 */ 0xe286b2, 0xe286b3, - /*** Four byte table, leaf: 8136caxx - offset 0x069ea ***/ + /*** Four byte table, leaf: 8136caxx - offset 0x06a5e ***/ /* 30 */ 0xe286b4, 0xe286b5, 0xe286b6, 0xe286b7, /* 34 */ 0xe286b8, 0xe286b9, 0xe286ba, 0xe286bb, /* 38 */ 0xe286bc, 0xe286bd, - /*** Four byte table, leaf: 8136cbxx - offset 0x069f4 ***/ + /*** Four byte table, leaf: 8136cbxx - offset 0x06a68 ***/ /* 30 */ 0xe286be, 0xe286bf, 0xe28780, 0xe28781, /* 34 */ 0xe28782, 0xe28783, 0xe28784, 0xe28785, /* 38 */ 0xe28786, 0xe28787, - /*** Four byte table, leaf: 8136ccxx - offset 0x069fe ***/ + /*** Four byte table, leaf: 8136ccxx - offset 0x06a72 ***/ /* 30 */ 0xe28788, 0xe28789, 0xe2878a, 0xe2878b, /* 34 */ 0xe2878c, 0xe2878d, 0xe2878e, 0xe2878f, /* 38 */ 0xe28790, 0xe28791, - /*** Four byte table, leaf: 8136cdxx - offset 0x06a08 ***/ + /*** Four byte table, leaf: 8136cdxx - offset 0x06a7c ***/ /* 30 */ 0xe28792, 0xe28793, 0xe28794, 0xe28795, /* 34 */ 0xe28796, 0xe28797, 0xe28798, 0xe28799, /* 38 */ 0xe2879a, 0xe2879b, - /*** Four byte table, leaf: 8136cexx - offset 0x06a12 ***/ + /*** Four byte table, leaf: 8136cexx - offset 0x06a86 ***/ /* 30 */ 0xe2879c, 0xe2879d, 0xe2879e, 0xe2879f, /* 34 */ 0xe287a0, 0xe287a1, 0xe287a2, 0xe287a3, /* 38 */ 0xe287a4, 0xe287a5, - /*** Four byte table, leaf: 8136cfxx - offset 0x06a1c ***/ + /*** Four byte table, leaf: 8136cfxx - offset 0x06a90 ***/ /* 30 */ 0xe287a6, 0xe287a7, 0xe287a8, 0xe287a9, /* 34 */ 0xe287aa, 0xe287ab, 0xe287ac, 0xe287ad, /* 38 */ 0xe287ae, 0xe287af, - /*** Four byte table, leaf: 8136d0xx - offset 0x06a26 ***/ + /*** Four byte table, leaf: 8136d0xx - offset 0x06a9a ***/ /* 30 */ 0xe287b0, 0xe287b1, 0xe287b2, 0xe287b3, /* 34 */ 0xe287b4, 0xe287b5, 0xe287b6, 0xe287b7, /* 38 */ 0xe287b8, 0xe287b9, - /*** Four byte table, leaf: 8136d1xx - offset 0x06a30 ***/ + /*** Four byte table, leaf: 8136d1xx - offset 0x06aa4 ***/ /* 30 */ 0xe287ba, 0xe287bb, 0xe287bc, 0xe287bd, /* 34 */ 0xe287be, 0xe287bf, 0xe28880, 0xe28881, /* 38 */ 0xe28882, 0xe28883, - /*** Four byte table, leaf: 8136d2xx - offset 0x06a3a ***/ + /*** Four byte table, leaf: 8136d2xx - offset 0x06aae ***/ /* 30 */ 0xe28884, 0xe28885, 0xe28886, 0xe28887, /* 34 */ 0xe28889, 0xe2888a, 0xe2888b, 0xe2888c, /* 38 */ 0xe2888d, 0xe2888e, - /*** Four byte table, leaf: 8136d3xx - offset 0x06a44 ***/ + /*** Four byte table, leaf: 8136d3xx - offset 0x06ab8 ***/ /* 30 */ 0xe28890, 0xe28892, 0xe28893, 0xe28894, /* 34 */ 0xe28896, 0xe28897, 0xe28898, 0xe28899, /* 38 */ 0xe2889b, 0xe2889c, - /*** Four byte table, leaf: 8136d4xx - offset 0x06a4e ***/ + /*** Four byte table, leaf: 8136d4xx - offset 0x06ac2 ***/ /* 30 */ 0xe288a1, 0xe288a2, 0xe288a4, 0xe288a6, /* 34 */ 0xe288ac, 0xe288ad, 0xe288af, 0xe288b0, /* 38 */ 0xe288b1, 0xe288b2, - /*** Four byte table, leaf: 8136d5xx - offset 0x06a58 ***/ + /*** Four byte table, leaf: 8136d5xx - offset 0x06acc ***/ /* 30 */ 0xe288b3, 0xe288b8, 0xe288b9, 0xe288ba, /* 34 */ 0xe288bb, 0xe288bc, 0xe288be, 0xe288bf, /* 38 */ 0xe28980, 0xe28981, - /*** Four byte table, leaf: 8136d6xx - offset 0x06a62 ***/ + /*** Four byte table, leaf: 8136d6xx - offset 0x06ad6 ***/ /* 30 */ 0xe28982, 0xe28983, 0xe28984, 0xe28985, /* 34 */ 0xe28986, 0xe28987, 0xe28989, 0xe2898a, /* 38 */ 0xe2898b, 0xe2898d, - /*** Four byte table, leaf: 8136d7xx - offset 0x06a6c ***/ + /*** Four byte table, leaf: 8136d7xx - offset 0x06ae0 ***/ /* 30 */ 0xe2898e, 0xe2898f, 0xe28990, 0xe28991, /* 34 */ 0xe28993, 0xe28994, 0xe28995, 0xe28996, /* 38 */ 0xe28997, 0xe28998, - /*** Four byte table, leaf: 8136d8xx - offset 0x06a76 ***/ + /*** Four byte table, leaf: 8136d8xx - offset 0x06aea ***/ /* 30 */ 0xe28999, 0xe2899a, 0xe2899b, 0xe2899c, /* 34 */ 0xe2899d, 0xe2899e, 0xe2899f, 0xe289a2, /* 38 */ 0xe289a3, 0xe289a8, - /*** Four byte table, leaf: 8136d9xx - offset 0x06a80 ***/ + /*** Four byte table, leaf: 8136d9xx - offset 0x06af4 ***/ /* 30 */ 0xe289a9, 0xe289aa, 0xe289ab, 0xe289ac, /* 34 */ 0xe289ad, 0xe289b0, 0xe289b1, 0xe289b2, /* 38 */ 0xe289b3, 0xe289b4, - /*** Four byte table, leaf: 8136daxx - offset 0x06a8a ***/ + /*** Four byte table, leaf: 8136daxx - offset 0x06afe ***/ /* 30 */ 0xe289b5, 0xe289b6, 0xe289b7, 0xe289b8, /* 34 */ 0xe289b9, 0xe289ba, 0xe289bb, 0xe289bc, /* 38 */ 0xe289bd, 0xe289be, - /*** Four byte table, leaf: 8136dbxx - offset 0x06a94 ***/ + /*** Four byte table, leaf: 8136dbxx - offset 0x06b08 ***/ /* 30 */ 0xe289bf, 0xe28a80, 0xe28a81, 0xe28a82, /* 34 */ 0xe28a83, 0xe28a84, 0xe28a85, 0xe28a86, /* 38 */ 0xe28a87, 0xe28a88, - /*** Four byte table, leaf: 8136dcxx - offset 0x06a9e ***/ + /*** Four byte table, leaf: 8136dcxx - offset 0x06b12 ***/ /* 30 */ 0xe28a89, 0xe28a8a, 0xe28a8b, 0xe28a8c, /* 34 */ 0xe28a8d, 0xe28a8e, 0xe28a8f, 0xe28a90, /* 38 */ 0xe28a91, 0xe28a92, - /*** Four byte table, leaf: 8136ddxx - offset 0x06aa8 ***/ + /*** Four byte table, leaf: 8136ddxx - offset 0x06b1c ***/ /* 30 */ 0xe28a93, 0xe28a94, 0xe28a96, 0xe28a97, /* 34 */ 0xe28a98, 0xe28a9a, 0xe28a9b, 0xe28a9c, /* 38 */ 0xe28a9d, 0xe28a9e, - /*** Four byte table, leaf: 8136dexx - offset 0x06ab2 ***/ + /*** Four byte table, leaf: 8136dexx - offset 0x06b26 ***/ /* 30 */ 0xe28a9f, 0xe28aa0, 0xe28aa1, 0xe28aa2, /* 34 */ 0xe28aa3, 0xe28aa4, 0xe28aa6, 0xe28aa7, /* 38 */ 0xe28aa8, 0xe28aa9, - /*** Four byte table, leaf: 8136dfxx - offset 0x06abc ***/ + /*** Four byte table, leaf: 8136dfxx - offset 0x06b30 ***/ /* 30 */ 0xe28aaa, 0xe28aab, 0xe28aac, 0xe28aad, /* 34 */ 0xe28aae, 0xe28aaf, 0xe28ab0, 0xe28ab1, /* 38 */ 0xe28ab2, 0xe28ab3, - /*** Four byte table, leaf: 8136e0xx - offset 0x06ac6 ***/ + /*** Four byte table, leaf: 8136e0xx - offset 0x06b3a ***/ /* 30 */ 0xe28ab4, 0xe28ab5, 0xe28ab6, 0xe28ab7, /* 34 */ 0xe28ab8, 0xe28ab9, 0xe28aba, 0xe28abb, /* 38 */ 0xe28abc, 0xe28abd, - /*** Four byte table, leaf: 8136e1xx - offset 0x06ad0 ***/ + /*** Four byte table, leaf: 8136e1xx - offset 0x06b44 ***/ /* 30 */ 0xe28abe, 0xe28b80, 0xe28b81, 0xe28b82, /* 34 */ 0xe28b83, 0xe28b84, 0xe28b85, 0xe28b86, /* 38 */ 0xe28b87, 0xe28b88, - /*** Four byte table, leaf: 8136e2xx - offset 0x06ada ***/ + /*** Four byte table, leaf: 8136e2xx - offset 0x06b4e ***/ /* 30 */ 0xe28b89, 0xe28b8a, 0xe28b8b, 0xe28b8c, /* 34 */ 0xe28b8d, 0xe28b8e, 0xe28b8f, 0xe28b90, /* 38 */ 0xe28b91, 0xe28b92, - /*** Four byte table, leaf: 8136e3xx - offset 0x06ae4 ***/ + /*** Four byte table, leaf: 8136e3xx - offset 0x06b58 ***/ /* 30 */ 0xe28b93, 0xe28b94, 0xe28b95, 0xe28b96, /* 34 */ 0xe28b97, 0xe28b98, 0xe28b99, 0xe28b9a, /* 38 */ 0xe28b9b, 0xe28b9c, - /*** Four byte table, leaf: 8136e4xx - offset 0x06aee ***/ + /*** Four byte table, leaf: 8136e4xx - offset 0x06b62 ***/ /* 30 */ 0xe28b9d, 0xe28b9e, 0xe28b9f, 0xe28ba0, /* 34 */ 0xe28ba1, 0xe28ba2, 0xe28ba3, 0xe28ba4, /* 38 */ 0xe28ba5, 0xe28ba6, - /*** Four byte table, leaf: 8136e5xx - offset 0x06af8 ***/ + /*** Four byte table, leaf: 8136e5xx - offset 0x06b6c ***/ /* 30 */ 0xe28ba7, 0xe28ba8, 0xe28ba9, 0xe28baa, /* 34 */ 0xe28bab, 0xe28bac, 0xe28bad, 0xe28bae, /* 38 */ 0xe28baf, 0xe28bb0, - /*** Four byte table, leaf: 8136e6xx - offset 0x06b02 ***/ + /*** Four byte table, leaf: 8136e6xx - offset 0x06b76 ***/ /* 30 */ 0xe28bb1, 0xe28bb2, 0xe28bb3, 0xe28bb4, /* 34 */ 0xe28bb5, 0xe28bb6, 0xe28bb7, 0xe28bb8, /* 38 */ 0xe28bb9, 0xe28bba, - /*** Four byte table, leaf: 8136e7xx - offset 0x06b0c ***/ + /*** Four byte table, leaf: 8136e7xx - offset 0x06b80 ***/ /* 30 */ 0xe28bbb, 0xe28bbc, 0xe28bbd, 0xe28bbe, /* 34 */ 0xe28bbf, 0xe28c80, 0xe28c81, 0xe28c82, /* 38 */ 0xe28c83, 0xe28c84, - /*** Four byte table, leaf: 8136e8xx - offset 0x06b16 ***/ + /*** Four byte table, leaf: 8136e8xx - offset 0x06b8a ***/ /* 30 */ 0xe28c85, 0xe28c86, 0xe28c87, 0xe28c88, /* 34 */ 0xe28c89, 0xe28c8a, 0xe28c8b, 0xe28c8c, /* 38 */ 0xe28c8d, 0xe28c8e, - /*** Four byte table, leaf: 8136e9xx - offset 0x06b20 ***/ + /*** Four byte table, leaf: 8136e9xx - offset 0x06b94 ***/ /* 30 */ 0xe28c8f, 0xe28c90, 0xe28c91, 0xe28c93, /* 34 */ 0xe28c94, 0xe28c95, 0xe28c96, 0xe28c97, /* 38 */ 0xe28c98, 0xe28c99, - /*** Four byte table, leaf: 8136eaxx - offset 0x06b2a ***/ + /*** Four byte table, leaf: 8136eaxx - offset 0x06b9e ***/ /* 30 */ 0xe28c9a, 0xe28c9b, 0xe28c9c, 0xe28c9d, /* 34 */ 0xe28c9e, 0xe28c9f, 0xe28ca0, 0xe28ca1, /* 38 */ 0xe28ca2, 0xe28ca3, - /*** Four byte table, leaf: 8136ebxx - offset 0x06b34 ***/ + /*** Four byte table, leaf: 8136ebxx - offset 0x06ba8 ***/ /* 30 */ 0xe28ca4, 0xe28ca5, 0xe28ca6, 0xe28ca7, /* 34 */ 0xe28ca8, 0xe28ca9, 0xe28caa, 0xe28cab, /* 38 */ 0xe28cac, 0xe28cad, - /*** Four byte table, leaf: 8136ecxx - offset 0x06b3e ***/ + /*** Four byte table, leaf: 8136ecxx - offset 0x06bb2 ***/ /* 30 */ 0xe28cae, 0xe28caf, 0xe28cb0, 0xe28cb1, /* 34 */ 0xe28cb2, 0xe28cb3, 0xe28cb4, 0xe28cb5, /* 38 */ 0xe28cb6, 0xe28cb7, - /*** Four byte table, leaf: 8136edxx - offset 0x06b48 ***/ + /*** Four byte table, leaf: 8136edxx - offset 0x06bbc ***/ /* 30 */ 0xe28cb8, 0xe28cb9, 0xe28cba, 0xe28cbb, /* 34 */ 0xe28cbc, 0xe28cbd, 0xe28cbe, 0xe28cbf, /* 38 */ 0xe28d80, 0xe28d81, - /*** Four byte table, leaf: 8136eexx - offset 0x06b52 ***/ + /*** Four byte table, leaf: 8136eexx - offset 0x06bc6 ***/ /* 30 */ 0xe28d82, 0xe28d83, 0xe28d84, 0xe28d85, /* 34 */ 0xe28d86, 0xe28d87, 0xe28d88, 0xe28d89, /* 38 */ 0xe28d8a, 0xe28d8b, - /*** Four byte table, leaf: 8136efxx - offset 0x06b5c ***/ + /*** Four byte table, leaf: 8136efxx - offset 0x06bd0 ***/ /* 30 */ 0xe28d8c, 0xe28d8d, 0xe28d8e, 0xe28d8f, /* 34 */ 0xe28d90, 0xe28d91, 0xe28d92, 0xe28d93, /* 38 */ 0xe28d94, 0xe28d95, - /*** Four byte table, leaf: 8136f0xx - offset 0x06b66 ***/ + /*** Four byte table, leaf: 8136f0xx - offset 0x06bda ***/ /* 30 */ 0xe28d96, 0xe28d97, 0xe28d98, 0xe28d99, /* 34 */ 0xe28d9a, 0xe28d9b, 0xe28d9c, 0xe28d9d, /* 38 */ 0xe28d9e, 0xe28d9f, - /*** Four byte table, leaf: 8136f1xx - offset 0x06b70 ***/ + /*** Four byte table, leaf: 8136f1xx - offset 0x06be4 ***/ /* 30 */ 0xe28da0, 0xe28da1, 0xe28da2, 0xe28da3, /* 34 */ 0xe28da4, 0xe28da5, 0xe28da6, 0xe28da7, /* 38 */ 0xe28da8, 0xe28da9, - /*** Four byte table, leaf: 8136f2xx - offset 0x06b7a ***/ + /*** Four byte table, leaf: 8136f2xx - offset 0x06bee ***/ /* 30 */ 0xe28daa, 0xe28dab, 0xe28dac, 0xe28dad, /* 34 */ 0xe28dae, 0xe28daf, 0xe28db0, 0xe28db1, /* 38 */ 0xe28db2, 0xe28db3, - /*** Four byte table, leaf: 8136f3xx - offset 0x06b84 ***/ + /*** Four byte table, leaf: 8136f3xx - offset 0x06bf8 ***/ /* 30 */ 0xe28db4, 0xe28db5, 0xe28db6, 0xe28db7, /* 34 */ 0xe28db8, 0xe28db9, 0xe28dba, 0xe28dbb, /* 38 */ 0xe28dbc, 0xe28dbd, - /*** Four byte table, leaf: 8136f4xx - offset 0x06b8e ***/ + /*** Four byte table, leaf: 8136f4xx - offset 0x06c02 ***/ /* 30 */ 0xe28dbe, 0xe28dbf, 0xe28e80, 0xe28e81, /* 34 */ 0xe28e82, 0xe28e83, 0xe28e84, 0xe28e85, /* 38 */ 0xe28e86, 0xe28e87, - /*** Four byte table, leaf: 8136f5xx - offset 0x06b98 ***/ + /*** Four byte table, leaf: 8136f5xx - offset 0x06c0c ***/ /* 30 */ 0xe28e88, 0xe28e89, 0xe28e8a, 0xe28e8b, /* 34 */ 0xe28e8c, 0xe28e8d, 0xe28e8e, 0xe28e8f, /* 38 */ 0xe28e90, 0xe28e91, - /*** Four byte table, leaf: 8136f6xx - offset 0x06ba2 ***/ + /*** Four byte table, leaf: 8136f6xx - offset 0x06c16 ***/ /* 30 */ 0xe28e92, 0xe28e93, 0xe28e94, 0xe28e95, /* 34 */ 0xe28e96, 0xe28e97, 0xe28e98, 0xe28e99, /* 38 */ 0xe28e9a, 0xe28e9b, - /*** Four byte table, leaf: 8136f7xx - offset 0x06bac ***/ + /*** Four byte table, leaf: 8136f7xx - offset 0x06c20 ***/ /* 30 */ 0xe28e9c, 0xe28e9d, 0xe28e9e, 0xe28e9f, /* 34 */ 0xe28ea0, 0xe28ea1, 0xe28ea2, 0xe28ea3, /* 38 */ 0xe28ea4, 0xe28ea5, - /*** Four byte table, leaf: 8136f8xx - offset 0x06bb6 ***/ + /*** Four byte table, leaf: 8136f8xx - offset 0x06c2a ***/ /* 30 */ 0xe28ea6, 0xe28ea7, 0xe28ea8, 0xe28ea9, /* 34 */ 0xe28eaa, 0xe28eab, 0xe28eac, 0xe28ead, /* 38 */ 0xe28eae, 0xe28eaf, - /*** Four byte table, leaf: 8136f9xx - offset 0x06bc0 ***/ + /*** Four byte table, leaf: 8136f9xx - offset 0x06c34 ***/ /* 30 */ 0xe28eb0, 0xe28eb1, 0xe28eb2, 0xe28eb3, /* 34 */ 0xe28eb4, 0xe28eb5, 0xe28eb6, 0xe28eb7, /* 38 */ 0xe28eb8, 0xe28eb9, - /*** Four byte table, leaf: 8136faxx - offset 0x06bca ***/ + /*** Four byte table, leaf: 8136faxx - offset 0x06c3e ***/ /* 30 */ 0xe28eba, 0xe28ebb, 0xe28ebc, 0xe28ebd, /* 34 */ 0xe28ebe, 0xe28ebf, 0xe28f80, 0xe28f81, /* 38 */ 0xe28f82, 0xe28f83, - /*** Four byte table, leaf: 8136fbxx - offset 0x06bd4 ***/ + /*** Four byte table, leaf: 8136fbxx - offset 0x06c48 ***/ /* 30 */ 0xe28f84, 0xe28f85, 0xe28f86, 0xe28f87, /* 34 */ 0xe28f88, 0xe28f89, 0xe28f8a, 0xe28f8b, /* 38 */ 0xe28f8c, 0xe28f8d, - /*** Four byte table, leaf: 8136fcxx - offset 0x06bde ***/ + /*** Four byte table, leaf: 8136fcxx - offset 0x06c52 ***/ /* 30 */ 0xe28f8e, 0xe28f8f, 0xe28f90, 0xe28f91, /* 34 */ 0xe28f92, 0xe28f93, 0xe28f94, 0xe28f95, /* 38 */ 0xe28f96, 0xe28f97, - /*** Four byte table, leaf: 8136fdxx - offset 0x06be8 ***/ + /*** Four byte table, leaf: 8136fdxx - offset 0x06c5c ***/ /* 30 */ 0xe28f98, 0xe28f99, 0xe28f9a, 0xe28f9b, /* 34 */ 0xe28f9c, 0xe28f9d, 0xe28f9e, 0xe28f9f, /* 38 */ 0xe28fa0, 0xe28fa1, - /*** Four byte table, leaf: 8136fexx - offset 0x06bf2 ***/ + /*** Four byte table, leaf: 8136fexx - offset 0x06c66 ***/ /* 30 */ 0xe28fa2, 0xe28fa3, 0xe28fa4, 0xe28fa5, /* 34 */ 0xe28fa6, 0xe28fa7, 0xe28fa8, 0xe28fa9, /* 38 */ 0xe28faa, 0xe28fab, - /*** Four byte table, leaf: 813781xx - offset 0x06bfc ***/ + /*** Four byte table, leaf: 813781xx - offset 0x06c70 ***/ /* 30 */ 0xe28fac, 0xe28fad, 0xe28fae, 0xe28faf, /* 34 */ 0xe28fb0, 0xe28fb1, 0xe28fb2, 0xe28fb3, /* 38 */ 0xe28fb4, 0xe28fb5, - /*** Four byte table, leaf: 813782xx - offset 0x06c06 ***/ + /*** Four byte table, leaf: 813782xx - offset 0x06c7a ***/ /* 30 */ 0xe28fb6, 0xe28fb7, 0xe28fb8, 0xe28fb9, /* 34 */ 0xe28fba, 0xe28fbb, 0xe28fbc, 0xe28fbd, /* 38 */ 0xe28fbe, 0xe28fbf, - /*** Four byte table, leaf: 813783xx - offset 0x06c10 ***/ + /*** Four byte table, leaf: 813783xx - offset 0x06c84 ***/ /* 30 */ 0xe29080, 0xe29081, 0xe29082, 0xe29083, /* 34 */ 0xe29084, 0xe29085, 0xe29086, 0xe29087, /* 38 */ 0xe29088, 0xe29089, - /*** Four byte table, leaf: 813784xx - offset 0x06c1a ***/ + /*** Four byte table, leaf: 813784xx - offset 0x06c8e ***/ /* 30 */ 0xe2908a, 0xe2908b, 0xe2908c, 0xe2908d, /* 34 */ 0xe2908e, 0xe2908f, 0xe29090, 0xe29091, /* 38 */ 0xe29092, 0xe29093, - /*** Four byte table, leaf: 813785xx - offset 0x06c24 ***/ + /*** Four byte table, leaf: 813785xx - offset 0x06c98 ***/ /* 30 */ 0xe29094, 0xe29095, 0xe29096, 0xe29097, /* 34 */ 0xe29098, 0xe29099, 0xe2909a, 0xe2909b, /* 38 */ 0xe2909c, 0xe2909d, - /*** Four byte table, leaf: 813786xx - offset 0x06c2e ***/ + /*** Four byte table, leaf: 813786xx - offset 0x06ca2 ***/ /* 30 */ 0xe2909e, 0xe2909f, 0xe290a0, 0xe290a1, /* 34 */ 0xe290a2, 0xe290a3, 0xe290a4, 0xe290a5, /* 38 */ 0xe290a6, 0xe290a7, - /*** Four byte table, leaf: 813787xx - offset 0x06c38 ***/ + /*** Four byte table, leaf: 813787xx - offset 0x06cac ***/ /* 30 */ 0xe290a8, 0xe290a9, 0xe290aa, 0xe290ab, /* 34 */ 0xe290ac, 0xe290ad, 0xe290ae, 0xe290af, /* 38 */ 0xe290b0, 0xe290b1, - /*** Four byte table, leaf: 813788xx - offset 0x06c42 ***/ + /*** Four byte table, leaf: 813788xx - offset 0x06cb6 ***/ /* 30 */ 0xe290b2, 0xe290b3, 0xe290b4, 0xe290b5, /* 34 */ 0xe290b6, 0xe290b7, 0xe290b8, 0xe290b9, /* 38 */ 0xe290ba, 0xe290bb, - /*** Four byte table, leaf: 813789xx - offset 0x06c4c ***/ + /*** Four byte table, leaf: 813789xx - offset 0x06cc0 ***/ /* 30 */ 0xe290bc, 0xe290bd, 0xe290be, 0xe290bf, /* 34 */ 0xe29180, 0xe29181, 0xe29182, 0xe29183, /* 38 */ 0xe29184, 0xe29185, - /*** Four byte table, leaf: 81378axx - offset 0x06c56 ***/ + /*** Four byte table, leaf: 81378axx - offset 0x06cca ***/ /* 30 */ 0xe29186, 0xe29187, 0xe29188, 0xe29189, /* 34 */ 0xe2918a, 0xe2918b, 0xe2918c, 0xe2918d, /* 38 */ 0xe2918e, 0xe2918f, - /*** Four byte table, leaf: 81378bxx - offset 0x06c60 ***/ + /*** Four byte table, leaf: 81378bxx - offset 0x06cd4 ***/ /* 30 */ 0xe29190, 0xe29191, 0xe29192, 0xe29193, /* 34 */ 0xe29194, 0xe29195, 0xe29196, 0xe29197, /* 38 */ 0xe29198, 0xe29199, - /*** Four byte table, leaf: 81378cxx - offset 0x06c6a ***/ + /*** Four byte table, leaf: 81378cxx - offset 0x06cde ***/ /* 30 */ 0xe2919a, 0xe2919b, 0xe2919c, 0xe2919d, /* 34 */ 0xe2919e, 0xe2919f, 0xe291aa, 0xe291ab, /* 38 */ 0xe291ac, 0xe291ad, - /*** Four byte table, leaf: 81378dxx - offset 0x06c74 ***/ + /*** Four byte table, leaf: 81378dxx - offset 0x06ce8 ***/ /* 30 */ 0xe291ae, 0xe291af, 0xe291b0, 0xe291b1, /* 34 */ 0xe291b2, 0xe291b3, 0xe2929c, 0xe2929d, /* 38 */ 0xe2929e, 0xe2929f, - /*** Four byte table, leaf: 81378exx - offset 0x06c7e ***/ + /*** Four byte table, leaf: 81378exx - offset 0x06cf2 ***/ /* 30 */ 0xe292a0, 0xe292a1, 0xe292a2, 0xe292a3, /* 34 */ 0xe292a4, 0xe292a5, 0xe292a6, 0xe292a7, /* 38 */ 0xe292a8, 0xe292a9, - /*** Four byte table, leaf: 81378fxx - offset 0x06c88 ***/ + /*** Four byte table, leaf: 81378fxx - offset 0x06cfc ***/ /* 30 */ 0xe292aa, 0xe292ab, 0xe292ac, 0xe292ad, /* 34 */ 0xe292ae, 0xe292af, 0xe292b0, 0xe292b1, /* 38 */ 0xe292b2, 0xe292b3, - /*** Four byte table, leaf: 813790xx - offset 0x06c92 ***/ + /*** Four byte table, leaf: 813790xx - offset 0x06d06 ***/ /* 30 */ 0xe292b4, 0xe292b5, 0xe292b6, 0xe292b7, /* 34 */ 0xe292b8, 0xe292b9, 0xe292ba, 0xe292bb, /* 38 */ 0xe292bc, 0xe292bd, - /*** Four byte table, leaf: 813791xx - offset 0x06c9c ***/ + /*** Four byte table, leaf: 813791xx - offset 0x06d10 ***/ /* 30 */ 0xe292be, 0xe292bf, 0xe29380, 0xe29381, /* 34 */ 0xe29382, 0xe29383, 0xe29384, 0xe29385, /* 38 */ 0xe29386, 0xe29387, - /*** Four byte table, leaf: 813792xx - offset 0x06ca6 ***/ + /*** Four byte table, leaf: 813792xx - offset 0x06d1a ***/ /* 30 */ 0xe29388, 0xe29389, 0xe2938a, 0xe2938b, /* 34 */ 0xe2938c, 0xe2938d, 0xe2938e, 0xe2938f, /* 38 */ 0xe29390, 0xe29391, - /*** Four byte table, leaf: 813793xx - offset 0x06cb0 ***/ + /*** Four byte table, leaf: 813793xx - offset 0x06d24 ***/ /* 30 */ 0xe29392, 0xe29393, 0xe29394, 0xe29395, /* 34 */ 0xe29396, 0xe29397, 0xe29398, 0xe29399, /* 38 */ 0xe2939a, 0xe2939b, - /*** Four byte table, leaf: 813794xx - offset 0x06cba ***/ + /*** Four byte table, leaf: 813794xx - offset 0x06d2e ***/ /* 30 */ 0xe2939c, 0xe2939d, 0xe2939e, 0xe2939f, /* 34 */ 0xe293a0, 0xe293a1, 0xe293a2, 0xe293a3, /* 38 */ 0xe293a4, 0xe293a5, - /*** Four byte table, leaf: 813795xx - offset 0x06cc4 ***/ + /*** Four byte table, leaf: 813795xx - offset 0x06d38 ***/ /* 30 */ 0xe293a6, 0xe293a7, 0xe293a8, 0xe293a9, /* 34 */ 0xe293aa, 0xe293ab, 0xe293ac, 0xe293ad, /* 38 */ 0xe293ae, 0xe293af, - /*** Four byte table, leaf: 813796xx - offset 0x06cce ***/ + /*** Four byte table, leaf: 813796xx - offset 0x06d42 ***/ /* 30 */ 0xe293b0, 0xe293b1, 0xe293b2, 0xe293b3, /* 34 */ 0xe293b4, 0xe293b5, 0xe293b6, 0xe293b7, /* 38 */ 0xe293b8, 0xe293b9, - /*** Four byte table, leaf: 813797xx - offset 0x06cd8 ***/ + /*** Four byte table, leaf: 813797xx - offset 0x06d4c ***/ /* 30 */ 0xe293ba, 0xe293bb, 0xe293bc, 0xe293bd, /* 34 */ 0xe293be, 0xe293bf, 0xe2958c, 0xe2958d, /* 38 */ 0xe2958e, 0xe2958f, - /*** Four byte table, leaf: 813798xx - offset 0x06ce2 ***/ + /*** Four byte table, leaf: 813798xx - offset 0x06d56 ***/ /* 30 */ 0xe295b4, 0xe295b5, 0xe295b6, 0xe295b7, /* 34 */ 0xe295b8, 0xe295b9, 0xe295ba, 0xe295bb, /* 38 */ 0xe295bc, 0xe295bd, - /*** Four byte table, leaf: 813799xx - offset 0x06cec ***/ + /*** Four byte table, leaf: 813799xx - offset 0x06d60 ***/ /* 30 */ 0xe295be, 0xe295bf, 0xe29680, 0xe29690, /* 34 */ 0xe29691, 0xe29692, 0xe29696, 0xe29697, /* 38 */ 0xe29698, 0xe29699, - /*** Four byte table, leaf: 81379axx - offset 0x06cf6 ***/ + /*** Four byte table, leaf: 81379axx - offset 0x06d6a ***/ /* 30 */ 0xe2969a, 0xe2969b, 0xe2969c, 0xe2969d, /* 34 */ 0xe2969e, 0xe2969f, 0xe296a2, 0xe296a3, /* 38 */ 0xe296a4, 0xe296a5, - /*** Four byte table, leaf: 81379bxx - offset 0x06d00 ***/ + /*** Four byte table, leaf: 81379bxx - offset 0x06d74 ***/ /* 30 */ 0xe296a6, 0xe296a7, 0xe296a8, 0xe296a9, /* 34 */ 0xe296aa, 0xe296ab, 0xe296ac, 0xe296ad, /* 38 */ 0xe296ae, 0xe296af, - /*** Four byte table, leaf: 81379cxx - offset 0x06d0a ***/ + /*** Four byte table, leaf: 81379cxx - offset 0x06d7e ***/ /* 30 */ 0xe296b0, 0xe296b1, 0xe296b4, 0xe296b5, /* 34 */ 0xe296b6, 0xe296b7, 0xe296b8, 0xe296b9, /* 38 */ 0xe296ba, 0xe296bb, - /*** Four byte table, leaf: 81379dxx - offset 0x06d14 ***/ + /*** Four byte table, leaf: 81379dxx - offset 0x06d88 ***/ /* 30 */ 0xe296be, 0xe296bf, 0xe29780, 0xe29781, /* 34 */ 0xe29782, 0xe29783, 0xe29784, 0xe29785, /* 38 */ 0xe29788, 0xe29789, - /*** Four byte table, leaf: 81379exx - offset 0x06d1e ***/ + /*** Four byte table, leaf: 81379exx - offset 0x06d92 ***/ /* 30 */ 0xe2978a, 0xe2978c, 0xe2978d, 0xe29790, /* 34 */ 0xe29791, 0xe29792, 0xe29793, 0xe29794, /* 38 */ 0xe29795, 0xe29796, - /*** Four byte table, leaf: 81379fxx - offset 0x06d28 ***/ + /*** Four byte table, leaf: 81379fxx - offset 0x06d9c ***/ /* 30 */ 0xe29797, 0xe29798, 0xe29799, 0xe2979a, /* 34 */ 0xe2979b, 0xe2979c, 0xe2979d, 0xe2979e, /* 38 */ 0xe2979f, 0xe297a0, - /*** Four byte table, leaf: 8137a0xx - offset 0x06d32 ***/ + /*** Four byte table, leaf: 8137a0xx - offset 0x06da6 ***/ /* 30 */ 0xe297a1, 0xe297a6, 0xe297a7, 0xe297a8, /* 34 */ 0xe297a9, 0xe297aa, 0xe297ab, 0xe297ac, /* 38 */ 0xe297ad, 0xe297ae, - /*** Four byte table, leaf: 8137a1xx - offset 0x06d3c ***/ + /*** Four byte table, leaf: 8137a1xx - offset 0x06db0 ***/ /* 30 */ 0xe297af, 0xe297b0, 0xe297b1, 0xe297b2, /* 34 */ 0xe297b3, 0xe297b4, 0xe297b5, 0xe297b6, /* 38 */ 0xe297b7, 0xe297b8, - /*** Four byte table, leaf: 8137a2xx - offset 0x06d46 ***/ + /*** Four byte table, leaf: 8137a2xx - offset 0x06dba ***/ /* 30 */ 0xe297b9, 0xe297ba, 0xe297bb, 0xe297bc, /* 34 */ 0xe297bd, 0xe297be, 0xe297bf, 0xe29880, /* 38 */ 0xe29881, 0xe29882, - /*** Four byte table, leaf: 8137a3xx - offset 0x06d50 ***/ + /*** Four byte table, leaf: 8137a3xx - offset 0x06dc4 ***/ /* 30 */ 0xe29883, 0xe29884, 0xe29887, 0xe29888, /* 34 */ 0xe2988a, 0xe2988b, 0xe2988c, 0xe2988d, /* 38 */ 0xe2988e, 0xe2988f, - /*** Four byte table, leaf: 8137a4xx - offset 0x06d5a ***/ + /*** Four byte table, leaf: 8137a4xx - offset 0x06dce ***/ /* 30 */ 0xe29890, 0xe29891, 0xe29892, 0xe29893, /* 34 */ 0xe29894, 0xe29895, 0xe29896, 0xe29897, /* 38 */ 0xe29898, 0xe29899, - /*** Four byte table, leaf: 8137a5xx - offset 0x06d64 ***/ + /*** Four byte table, leaf: 8137a5xx - offset 0x06dd8 ***/ /* 30 */ 0xe2989a, 0xe2989b, 0xe2989c, 0xe2989d, /* 34 */ 0xe2989e, 0xe2989f, 0xe298a0, 0xe298a1, /* 38 */ 0xe298a2, 0xe298a3, - /*** Four byte table, leaf: 8137a6xx - offset 0x06d6e ***/ + /*** Four byte table, leaf: 8137a6xx - offset 0x06de2 ***/ /* 30 */ 0xe298a4, 0xe298a5, 0xe298a6, 0xe298a7, /* 34 */ 0xe298a8, 0xe298a9, 0xe298aa, 0xe298ab, /* 38 */ 0xe298ac, 0xe298ad, - /*** Four byte table, leaf: 8137a7xx - offset 0x06d78 ***/ + /*** Four byte table, leaf: 8137a7xx - offset 0x06dec ***/ /* 30 */ 0xe298ae, 0xe298af, 0xe298b0, 0xe298b1, /* 34 */ 0xe298b2, 0xe298b3, 0xe298b4, 0xe298b5, /* 38 */ 0xe298b6, 0xe298b7, - /*** Four byte table, leaf: 8137a8xx - offset 0x06d82 ***/ + /*** Four byte table, leaf: 8137a8xx - offset 0x06df6 ***/ /* 30 */ 0xe298b8, 0xe298b9, 0xe298ba, 0xe298bb, /* 34 */ 0xe298bc, 0xe298bd, 0xe298be, 0xe298bf, /* 38 */ 0xe29981, /* 1 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8138fdxx - offset 0x06d8b ***/ + /*** Four byte table, leaf: 8138fdxx - offset 0x06dff ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0x000000, 0xe2ba82, - /*** Four byte table, leaf: 8138fexx - offset 0x06d95 ***/ + /*** Four byte table, leaf: 8138fexx - offset 0x06e09 ***/ /* 30 */ 0xe2ba83, 0xe2ba85, 0xe2ba86, 0xe2ba87, /* 34 */ 0xe2ba89, 0xe2ba8a, 0xe2ba8d, 0xe2ba8e, /* 38 */ 0xe2ba8f, 0xe2ba90, - /*** Four byte table, leaf: 813981xx - offset 0x06d9f ***/ + /*** Four byte table, leaf: 813981xx - offset 0x06e13 ***/ /* 30 */ 0xe2ba91, 0xe2ba92, 0xe2ba93, 0xe2ba94, /* 34 */ 0xe2ba95, 0xe2ba96, 0xe2ba98, 0xe2ba99, /* 38 */ 0xe2ba9a, 0xe2ba9b, - /*** Four byte table, leaf: 813982xx - offset 0x06da9 ***/ + /*** Four byte table, leaf: 813982xx - offset 0x06e1d ***/ /* 30 */ 0xe2ba9c, 0xe2ba9d, 0xe2ba9e, 0xe2ba9f, /* 34 */ 0xe2baa0, 0xe2baa1, 0xe2baa2, 0xe2baa3, /* 38 */ 0xe2baa4, 0xe2baa5, - /*** Four byte table, leaf: 813983xx - offset 0x06db3 ***/ + /*** Four byte table, leaf: 813983xx - offset 0x06e27 ***/ /* 30 */ 0xe2baa6, 0xe2baa8, 0xe2baa9, 0xe2baab, /* 34 */ 0xe2baac, 0xe2baad, 0xe2baaf, 0xe2bab0, /* 38 */ 0xe2bab1, 0xe2bab2, - /*** Four byte table, leaf: 813984xx - offset 0x06dbd ***/ + /*** Four byte table, leaf: 813984xx - offset 0x06e31 ***/ /* 30 */ 0xe2bab4, 0xe2bab5, 0xe2bab8, 0xe2bab9, /* 34 */ 0xe2baba, 0xe2babc, 0xe2babd, 0xe2babe, /* 38 */ 0xe2babf, 0xe2bb80, - /*** Four byte table, leaf: 813985xx - offset 0x06dc7 ***/ + /*** Four byte table, leaf: 813985xx - offset 0x06e3b ***/ /* 30 */ 0xe2bb81, 0xe2bb82, 0xe2bb83, 0xe2bb84, /* 34 */ 0xe2bb85, 0xe2bb86, 0xe2bb87, 0xe2bb88, /* 38 */ 0xe2bb89, 0xe2bb8b, - /*** Four byte table, leaf: 813986xx - offset 0x06dd1 ***/ + /*** Four byte table, leaf: 813986xx - offset 0x06e45 ***/ /* 30 */ 0xe2bb8c, 0xe2bb8d, 0xe2bb8e, 0xe2bb8f, /* 34 */ 0xe2bb90, 0xe2bb91, 0xe2bb92, 0xe2bb93, /* 38 */ 0xe2bb94, 0xe2bb95, - /*** Four byte table, leaf: 813987xx - offset 0x06ddb ***/ + /*** Four byte table, leaf: 813987xx - offset 0x06e4f ***/ /* 30 */ 0xe2bb96, 0xe2bb97, 0xe2bb98, 0xe2bb99, /* 34 */ 0xe2bb9a, 0xe2bb9b, 0xe2bb9c, 0xe2bb9d, /* 38 */ 0xe2bb9e, 0xe2bb9f, - /*** Four byte table, leaf: 813988xx - offset 0x06de5 ***/ + /*** Four byte table, leaf: 813988xx - offset 0x06e59 ***/ /* 30 */ 0xe2bba0, 0xe2bba1, 0xe2bba2, 0xe2bba3, /* 34 */ 0xe2bba4, 0xe2bba5, 0xe2bba6, 0xe2bba7, /* 38 */ 0xe2bba8, 0xe2bba9, - /*** Four byte table, leaf: 813989xx - offset 0x06def ***/ + /*** Four byte table, leaf: 813989xx - offset 0x06e63 ***/ /* 30 */ 0xe2bbaa, 0xe2bbab, 0xe2bbac, 0xe2bbad, /* 34 */ 0xe2bbae, 0xe2bbaf, 0xe2bbb0, 0xe2bbb1, /* 38 */ 0xe2bbb2, 0xe2bbb3, - /*** Four byte table, leaf: 81398axx - offset 0x06df9 ***/ + /*** Four byte table, leaf: 81398axx - offset 0x06e6d ***/ /* 30 */ 0xe2bbb4, 0xe2bbb5, 0xe2bbb6, 0xe2bbb7, /* 34 */ 0xe2bbb8, 0xe2bbb9, 0xe2bbba, 0xe2bbbb, /* 38 */ 0xe2bbbc, 0xe2bbbd, - /*** Four byte table, leaf: 81398bxx - offset 0x06e03 ***/ + /*** Four byte table, leaf: 81398bxx - offset 0x06e77 ***/ /* 30 */ 0xe2bbbe, 0xe2bbbf, 0xe2bc80, 0xe2bc81, /* 34 */ 0xe2bc82, 0xe2bc83, 0xe2bc84, 0xe2bc85, /* 38 */ 0xe2bc86, 0xe2bc87, - /*** Four byte table, leaf: 81398cxx - offset 0x06e0d ***/ + /*** Four byte table, leaf: 81398cxx - offset 0x06e81 ***/ /* 30 */ 0xe2bc88, 0xe2bc89, 0xe2bc8a, 0xe2bc8b, /* 34 */ 0xe2bc8c, 0xe2bc8d, 0xe2bc8e, 0xe2bc8f, /* 38 */ 0xe2bc90, 0xe2bc91, - /*** Four byte table, leaf: 81398dxx - offset 0x06e17 ***/ + /*** Four byte table, leaf: 81398dxx - offset 0x06e8b ***/ /* 30 */ 0xe2bc92, 0xe2bc93, 0xe2bc94, 0xe2bc95, /* 34 */ 0xe2bc96, 0xe2bc97, 0xe2bc98, 0xe2bc99, /* 38 */ 0xe2bc9a, 0xe2bc9b, - /*** Four byte table, leaf: 81398exx - offset 0x06e21 ***/ + /*** Four byte table, leaf: 81398exx - offset 0x06e95 ***/ /* 30 */ 0xe2bc9c, 0xe2bc9d, 0xe2bc9e, 0xe2bc9f, /* 34 */ 0xe2bca0, 0xe2bca1, 0xe2bca2, 0xe2bca3, /* 38 */ 0xe2bca4, 0xe2bca5, - /*** Four byte table, leaf: 81398fxx - offset 0x06e2b ***/ + /*** Four byte table, leaf: 81398fxx - offset 0x06e9f ***/ /* 30 */ 0xe2bca6, 0xe2bca7, 0xe2bca8, 0xe2bca9, /* 34 */ 0xe2bcaa, 0xe2bcab, 0xe2bcac, 0xe2bcad, /* 38 */ 0xe2bcae, 0xe2bcaf, - /*** Four byte table, leaf: 813990xx - offset 0x06e35 ***/ + /*** Four byte table, leaf: 813990xx - offset 0x06ea9 ***/ /* 30 */ 0xe2bcb0, 0xe2bcb1, 0xe2bcb2, 0xe2bcb3, /* 34 */ 0xe2bcb4, 0xe2bcb5, 0xe2bcb6, 0xe2bcb7, /* 38 */ 0xe2bcb8, 0xe2bcb9, - /*** Four byte table, leaf: 813991xx - offset 0x06e3f ***/ + /*** Four byte table, leaf: 813991xx - offset 0x06eb3 ***/ /* 30 */ 0xe2bcba, 0xe2bcbb, 0xe2bcbc, 0xe2bcbd, /* 34 */ 0xe2bcbe, 0xe2bcbf, 0xe2bd80, 0xe2bd81, /* 38 */ 0xe2bd82, 0xe2bd83, - /*** Four byte table, leaf: 813992xx - offset 0x06e49 ***/ + /*** Four byte table, leaf: 813992xx - offset 0x06ebd ***/ /* 30 */ 0xe2bd84, 0xe2bd85, 0xe2bd86, 0xe2bd87, /* 34 */ 0xe2bd88, 0xe2bd89, 0xe2bd8a, 0xe2bd8b, /* 38 */ 0xe2bd8c, 0xe2bd8d, - /*** Four byte table, leaf: 813993xx - offset 0x06e53 ***/ + /*** Four byte table, leaf: 813993xx - offset 0x06ec7 ***/ /* 30 */ 0xe2bd8e, 0xe2bd8f, 0xe2bd90, 0xe2bd91, /* 34 */ 0xe2bd92, 0xe2bd93, 0xe2bd94, 0xe2bd95, /* 38 */ 0xe2bd96, 0xe2bd97, - /*** Four byte table, leaf: 813994xx - offset 0x06e5d ***/ + /*** Four byte table, leaf: 813994xx - offset 0x06ed1 ***/ /* 30 */ 0xe2bd98, 0xe2bd99, 0xe2bd9a, 0xe2bd9b, /* 34 */ 0xe2bd9c, 0xe2bd9d, 0xe2bd9e, 0xe2bd9f, /* 38 */ 0xe2bda0, 0xe2bda1, - /*** Four byte table, leaf: 813995xx - offset 0x06e67 ***/ + /*** Four byte table, leaf: 813995xx - offset 0x06edb ***/ /* 30 */ 0xe2bda2, 0xe2bda3, 0xe2bda4, 0xe2bda5, /* 34 */ 0xe2bda6, 0xe2bda7, 0xe2bda8, 0xe2bda9, /* 38 */ 0xe2bdaa, 0xe2bdab, - /*** Four byte table, leaf: 813996xx - offset 0x06e71 ***/ + /*** Four byte table, leaf: 813996xx - offset 0x06ee5 ***/ /* 30 */ 0xe2bdac, 0xe2bdad, 0xe2bdae, 0xe2bdaf, /* 34 */ 0xe2bdb0, 0xe2bdb1, 0xe2bdb2, 0xe2bdb3, /* 38 */ 0xe2bdb4, 0xe2bdb5, - /*** Four byte table, leaf: 813997xx - offset 0x06e7b ***/ + /*** Four byte table, leaf: 813997xx - offset 0x06eef ***/ /* 30 */ 0xe2bdb6, 0xe2bdb7, 0xe2bdb8, 0xe2bdb9, /* 34 */ 0xe2bdba, 0xe2bdbb, 0xe2bdbc, 0xe2bdbd, /* 38 */ 0xe2bdbe, 0xe2bdbf, - /*** Four byte table, leaf: 813998xx - offset 0x06e85 ***/ + /*** Four byte table, leaf: 813998xx - offset 0x06ef9 ***/ /* 30 */ 0xe2be80, 0xe2be81, 0xe2be82, 0xe2be83, /* 34 */ 0xe2be84, 0xe2be85, 0xe2be86, 0xe2be87, /* 38 */ 0xe2be88, 0xe2be89, - /*** Four byte table, leaf: 813999xx - offset 0x06e8f ***/ + /*** Four byte table, leaf: 813999xx - offset 0x06f03 ***/ /* 30 */ 0xe2be8a, 0xe2be8b, 0xe2be8c, 0xe2be8d, /* 34 */ 0xe2be8e, 0xe2be8f, 0xe2be90, 0xe2be91, /* 38 */ 0xe2be92, 0xe2be93, - /*** Four byte table, leaf: 81399axx - offset 0x06e99 ***/ + /*** Four byte table, leaf: 81399axx - offset 0x06f0d ***/ /* 30 */ 0xe2be94, 0xe2be95, 0xe2be96, 0xe2be97, /* 34 */ 0xe2be98, 0xe2be99, 0xe2be9a, 0xe2be9b, /* 38 */ 0xe2be9c, 0xe2be9d, - /*** Four byte table, leaf: 81399bxx - offset 0x06ea3 ***/ + /*** Four byte table, leaf: 81399bxx - offset 0x06f17 ***/ /* 30 */ 0xe2be9e, 0xe2be9f, 0xe2bea0, 0xe2bea1, /* 34 */ 0xe2bea2, 0xe2bea3, 0xe2bea4, 0xe2bea5, /* 38 */ 0xe2bea6, 0xe2bea7, - /*** Four byte table, leaf: 81399cxx - offset 0x06ead ***/ + /*** Four byte table, leaf: 81399cxx - offset 0x06f21 ***/ /* 30 */ 0xe2bea8, 0xe2bea9, 0xe2beaa, 0xe2beab, /* 34 */ 0xe2beac, 0xe2bead, 0xe2beae, 0xe2beaf, /* 38 */ 0xe2beb0, 0xe2beb1, - /*** Four byte table, leaf: 81399dxx - offset 0x06eb7 ***/ + /*** Four byte table, leaf: 81399dxx - offset 0x06f2b ***/ /* 30 */ 0xe2beb2, 0xe2beb3, 0xe2beb4, 0xe2beb5, /* 34 */ 0xe2beb6, 0xe2beb7, 0xe2beb8, 0xe2beb9, /* 38 */ 0xe2beba, 0xe2bebb, - /*** Four byte table, leaf: 81399exx - offset 0x06ec1 ***/ + /*** Four byte table, leaf: 81399exx - offset 0x06f35 ***/ /* 30 */ 0xe2bebc, 0xe2bebd, 0xe2bebe, 0xe2bebf, /* 34 */ 0xe2bf80, 0xe2bf81, 0xe2bf82, 0xe2bf83, /* 38 */ 0xe2bf84, 0xe2bf85, - /*** Four byte table, leaf: 81399fxx - offset 0x06ecb ***/ + /*** Four byte table, leaf: 81399fxx - offset 0x06f3f ***/ /* 30 */ 0xe2bf86, 0xe2bf87, 0xe2bf88, 0xe2bf89, /* 34 */ 0xe2bf8a, 0xe2bf8b, 0xe2bf8c, 0xe2bf8d, /* 38 */ 0xe2bf8e, 0xe2bf8f, - /*** Four byte table, leaf: 8139a0xx - offset 0x06ed5 ***/ + /*** Four byte table, leaf: 8139a0xx - offset 0x06f49 ***/ /* 30 */ 0xe2bf90, 0xe2bf91, 0xe2bf92, 0xe2bf93, /* 34 */ 0xe2bf94, 0xe2bf95, 0xe2bf96, 0xe2bf97, /* 38 */ 0xe2bf98, 0xe2bf99, - /*** Four byte table, leaf: 8139a1xx - offset 0x06edf ***/ + /*** Four byte table, leaf: 8139a1xx - offset 0x06f53 ***/ /* 30 */ 0xe2bf9a, 0xe2bf9b, 0xe2bf9c, 0xe2bf9d, /* 34 */ 0xe2bf9e, 0xe2bf9f, 0xe2bfa0, 0xe2bfa1, /* 38 */ 0xe2bfa2, 0xe2bfa3, - /*** Four byte table, leaf: 8139a2xx - offset 0x06ee9 ***/ + /*** Four byte table, leaf: 8139a2xx - offset 0x06f5d ***/ /* 30 */ 0xe2bfa4, 0xe2bfa5, 0xe2bfa6, 0xe2bfa7, /* 34 */ 0xe2bfa8, 0xe2bfa9, 0xe2bfaa, 0xe2bfab, /* 38 */ 0xe2bfac, 0xe2bfad, - /*** Four byte table, leaf: 8139a3xx - offset 0x06ef3 ***/ + /*** Four byte table, leaf: 8139a3xx - offset 0x06f67 ***/ /* 30 */ 0xe2bfae, 0xe2bfaf, 0xe2bfbc, 0xe2bfbd, /* 34 */ 0xe2bfbe, 0xe2bfbf, 0xe38084, 0xe38098, /* 38 */ 0xe38099, 0xe3809a, - /*** Four byte table, leaf: 8139a4xx - offset 0x06efd ***/ + /*** Four byte table, leaf: 8139a4xx - offset 0x06f71 ***/ /* 30 */ 0xe3809b, 0xe3809c, 0xe3809f, 0xe380a0, /* 34 */ 0xe380aa, 0xe380ab, 0xe380ac, 0xe380ad, /* 38 */ 0xe380ae, 0xe380af, - /*** Four byte table, leaf: 8139a5xx - offset 0x06f07 ***/ + /*** Four byte table, leaf: 8139a5xx - offset 0x06f7b ***/ /* 30 */ 0xe380b0, 0xe380b1, 0xe380b2, 0xe380b3, /* 34 */ 0xe380b4, 0xe380b5, 0xe380b6, 0xe380b7, /* 38 */ 0xe380b8, 0xe380b9, - /*** Four byte table, leaf: 8139a6xx - offset 0x06f11 ***/ + /*** Four byte table, leaf: 8139a6xx - offset 0x06f85 ***/ /* 30 */ 0xe380ba, 0xe380bb, 0xe380bc, 0xe380bd, /* 34 */ 0xe380bf, 0xe38180, 0xe38294, 0xe38295, /* 38 */ 0xe38296, 0xe38297, - /*** Four byte table, leaf: 8139a7xx - offset 0x06f1b ***/ + /*** Four byte table, leaf: 8139a7xx - offset 0x06f8f ***/ /* 30 */ 0xe38298, 0xe38299, 0xe3829a, 0xe3829f, /* 34 */ 0xe382a0, 0xe383b7, 0xe383b8, 0xe383b9, /* 38 */ 0xe383ba, 0xe383bb, - /*** Four byte table, leaf: 8139a8xx - offset 0x06f25 ***/ + /*** Four byte table, leaf: 8139a8xx - offset 0x06f99 ***/ /* 30 */ 0xe383bf, 0xe38480, 0xe38481, 0xe38482, /* 34 */ 0xe38483, 0xe38484, 0xe384aa, 0xe384ab, /* 38 */ 0xe384ac, 0xe384ad, - /*** Four byte table, leaf: 8139a9xx - offset 0x06f2f ***/ + /*** Four byte table, leaf: 8139a9xx - offset 0x06fa3 ***/ /* 30 */ 0xe384ae, 0xe384af, 0xe384b0, 0xe384b1, /* 34 */ 0xe384b2, 0xe384b3, 0xe384b4, 0xe384b5, /* 38 */ 0xe384b6, 0xe384b7, - /*** Four byte table, leaf: 8139aaxx - offset 0x06f39 ***/ + /*** Four byte table, leaf: 8139aaxx - offset 0x06fad ***/ /* 30 */ 0xe384b8, 0xe384b9, 0xe384ba, 0xe384bb, /* 34 */ 0xe384bc, 0xe384bd, 0xe384be, 0xe384bf, /* 38 */ 0xe38580, 0xe38581, - /*** Four byte table, leaf: 8139abxx - offset 0x06f43 ***/ + /*** Four byte table, leaf: 8139abxx - offset 0x06fb7 ***/ /* 30 */ 0xe38582, 0xe38583, 0xe38584, 0xe38585, /* 34 */ 0xe38586, 0xe38587, 0xe38588, 0xe38589, /* 38 */ 0xe3858a, 0xe3858b, - /*** Four byte table, leaf: 8139acxx - offset 0x06f4d ***/ + /*** Four byte table, leaf: 8139acxx - offset 0x06fc1 ***/ /* 30 */ 0xe3858c, 0xe3858d, 0xe3858e, 0xe3858f, /* 34 */ 0xe38590, 0xe38591, 0xe38592, 0xe38593, /* 38 */ 0xe38594, 0xe38595, - /*** Four byte table, leaf: 8139adxx - offset 0x06f57 ***/ + /*** Four byte table, leaf: 8139adxx - offset 0x06fcb ***/ /* 30 */ 0xe38596, 0xe38597, 0xe38598, 0xe38599, /* 34 */ 0xe3859a, 0xe3859b, 0xe3859c, 0xe3859d, /* 38 */ 0xe3859e, 0xe3859f, - /*** Four byte table, leaf: 8139aexx - offset 0x06f61 ***/ + /*** Four byte table, leaf: 8139aexx - offset 0x06fd5 ***/ /* 30 */ 0xe385a0, 0xe385a1, 0xe385a2, 0xe385a3, /* 34 */ 0xe385a4, 0xe385a5, 0xe385a6, 0xe385a7, /* 38 */ 0xe385a8, 0xe385a9, - /*** Four byte table, leaf: 8139afxx - offset 0x06f6b ***/ + /*** Four byte table, leaf: 8139afxx - offset 0x06fdf ***/ /* 30 */ 0xe385aa, 0xe385ab, 0xe385ac, 0xe385ad, /* 34 */ 0xe385ae, 0xe385af, 0xe385b0, 0xe385b1, /* 38 */ 0xe385b2, 0xe385b3, - /*** Four byte table, leaf: 8139b0xx - offset 0x06f75 ***/ + /*** Four byte table, leaf: 8139b0xx - offset 0x06fe9 ***/ /* 30 */ 0xe385b4, 0xe385b5, 0xe385b6, 0xe385b7, /* 34 */ 0xe385b8, 0xe385b9, 0xe385ba, 0xe385bb, /* 38 */ 0xe385bc, 0xe385bd, - /*** Four byte table, leaf: 8139b1xx - offset 0x06f7f ***/ + /*** Four byte table, leaf: 8139b1xx - offset 0x06ff3 ***/ /* 30 */ 0xe385be, 0xe385bf, 0xe38680, 0xe38681, /* 34 */ 0xe38682, 0xe38683, 0xe38684, 0xe38685, /* 38 */ 0xe38686, 0xe38687, - /*** Four byte table, leaf: 8139b2xx - offset 0x06f89 ***/ + /*** Four byte table, leaf: 8139b2xx - offset 0x06ffd ***/ /* 30 */ 0xe38688, 0xe38689, 0xe3868a, 0xe3868b, /* 34 */ 0xe3868c, 0xe3868d, 0xe3868e, 0xe3868f, /* 38 */ 0xe38690, 0xe38691, - /*** Four byte table, leaf: 8139b3xx - offset 0x06f93 ***/ + /*** Four byte table, leaf: 8139b3xx - offset 0x07007 ***/ /* 30 */ 0xe38692, 0xe38693, 0xe38694, 0xe38695, /* 34 */ 0xe38696, 0xe38697, 0xe38698, 0xe38699, /* 38 */ 0xe3869a, 0xe3869b, - /*** Four byte table, leaf: 8139b4xx - offset 0x06f9d ***/ + /*** Four byte table, leaf: 8139b4xx - offset 0x07011 ***/ /* 30 */ 0xe3869c, 0xe3869d, 0xe3869e, 0xe3869f, /* 34 */ 0xe386a0, 0xe386a1, 0xe386a2, 0xe386a3, /* 38 */ 0xe386a4, 0xe386a5, - /*** Four byte table, leaf: 8139b5xx - offset 0x06fa7 ***/ + /*** Four byte table, leaf: 8139b5xx - offset 0x0701b ***/ /* 30 */ 0xe386a6, 0xe386a7, 0xe386a8, 0xe386a9, /* 34 */ 0xe386aa, 0xe386ab, 0xe386ac, 0xe386ad, /* 38 */ 0xe386ae, 0xe386af, - /*** Four byte table, leaf: 8139b6xx - offset 0x06fb1 ***/ + /*** Four byte table, leaf: 8139b6xx - offset 0x07025 ***/ /* 30 */ 0xe386b0, 0xe386b1, 0xe386b2, 0xe386b3, /* 34 */ 0xe386b4, 0xe386b5, 0xe386b6, 0xe386b7, /* 38 */ 0xe386b8, 0xe386b9, - /*** Four byte table, leaf: 8139b7xx - offset 0x06fbb ***/ + /*** Four byte table, leaf: 8139b7xx - offset 0x0702f ***/ /* 30 */ 0xe386ba, 0xe386bb, 0xe386bc, 0xe386bd, /* 34 */ 0xe386be, 0xe386bf, 0xe38780, 0xe38781, /* 38 */ 0xe38782, 0xe38783, - /*** Four byte table, leaf: 8139b8xx - offset 0x06fc5 ***/ + /*** Four byte table, leaf: 8139b8xx - offset 0x07039 ***/ /* 30 */ 0xe38784, 0xe38785, 0xe38786, 0xe38787, /* 34 */ 0xe38788, 0xe38789, 0xe3878a, 0xe3878b, /* 38 */ 0xe3878c, 0xe3878d, - /*** Four byte table, leaf: 8139b9xx - offset 0x06fcf ***/ + /*** Four byte table, leaf: 8139b9xx - offset 0x07043 ***/ /* 30 */ 0xe3878e, 0xe3878f, 0xe38790, 0xe38791, /* 34 */ 0xe38792, 0xe38793, 0xe38794, 0xe38795, /* 38 */ 0xe38796, 0xe38797, - /*** Four byte table, leaf: 8139baxx - offset 0x06fd9 ***/ + /*** Four byte table, leaf: 8139baxx - offset 0x0704d ***/ /* 30 */ 0xe38798, 0xe38799, 0xe3879a, 0xe3879b, /* 34 */ 0xe3879c, 0xe3879d, 0xe3879e, 0xe3879f, /* 38 */ 0xe387a0, 0xe387a1, - /*** Four byte table, leaf: 8139bbxx - offset 0x06fe3 ***/ + /*** Four byte table, leaf: 8139bbxx - offset 0x07057 ***/ /* 30 */ 0xe387a2, 0xe387a3, 0xe387a4, 0xe387a5, /* 34 */ 0xe387a6, 0xe387a7, 0xe387a8, 0xe387a9, /* 38 */ 0xe387aa, 0xe387ab, - /*** Four byte table, leaf: 8139bcxx - offset 0x06fed ***/ + /*** Four byte table, leaf: 8139bcxx - offset 0x07061 ***/ /* 30 */ 0xe387ac, 0xe387ad, 0xe387ae, 0xe387af, /* 34 */ 0xe387b0, 0xe387b1, 0xe387b2, 0xe387b3, /* 38 */ 0xe387b4, 0xe387b5, - /*** Four byte table, leaf: 8139bdxx - offset 0x06ff7 ***/ + /*** Four byte table, leaf: 8139bdxx - offset 0x0706b ***/ /* 30 */ 0xe387b6, 0xe387b7, 0xe387b8, 0xe387b9, /* 34 */ 0xe387ba, 0xe387bb, 0xe387bc, 0xe387bd, /* 38 */ 0xe387be, 0xe387bf, - /*** Four byte table, leaf: 8139bexx - offset 0x07001 ***/ + /*** Four byte table, leaf: 8139bexx - offset 0x07075 ***/ /* 30 */ 0xe38880, 0xe38881, 0xe38882, 0xe38883, /* 34 */ 0xe38884, 0xe38885, 0xe38886, 0xe38887, /* 38 */ 0xe38888, 0xe38889, - /*** Four byte table, leaf: 8139bfxx - offset 0x0700b ***/ + /*** Four byte table, leaf: 8139bfxx - offset 0x0707f ***/ /* 30 */ 0xe3888a, 0xe3888b, 0xe3888c, 0xe3888d, /* 34 */ 0xe3888e, 0xe3888f, 0xe38890, 0xe38891, /* 38 */ 0xe38892, 0xe38893, - /*** Four byte table, leaf: 8139c0xx - offset 0x07015 ***/ + /*** Four byte table, leaf: 8139c0xx - offset 0x07089 ***/ /* 30 */ 0xe38894, 0xe38895, 0xe38896, 0xe38897, /* 34 */ 0xe38898, 0xe38899, 0xe3889a, 0xe3889b, /* 38 */ 0xe3889c, 0xe3889d, - /*** Four byte table, leaf: 8139c1xx - offset 0x0701f ***/ + /*** Four byte table, leaf: 8139c1xx - offset 0x07093 ***/ /* 30 */ 0xe3889e, 0xe3889f, 0xe388aa, 0xe388ab, /* 34 */ 0xe388ac, 0xe388ad, 0xe388ae, 0xe388af, /* 38 */ 0xe388b0, 0xe388b2, - /*** Four byte table, leaf: 8139c2xx - offset 0x07029 ***/ + /*** Four byte table, leaf: 8139c2xx - offset 0x0709d ***/ /* 30 */ 0xe388b3, 0xe388b4, 0xe388b5, 0xe388b6, /* 34 */ 0xe388b7, 0xe388b8, 0xe388b9, 0xe388ba, /* 38 */ 0xe388bb, 0xe388bc, - /*** Four byte table, leaf: 8139c3xx - offset 0x07033 ***/ + /*** Four byte table, leaf: 8139c3xx - offset 0x070a7 ***/ /* 30 */ 0xe388bd, 0xe388be, 0xe388bf, 0xe38980, /* 34 */ 0xe38981, 0xe38982, 0xe38983, 0xe38984, /* 38 */ 0xe38985, 0xe38986, - /*** Four byte table, leaf: 8139c4xx - offset 0x0703d ***/ + /*** Four byte table, leaf: 8139c4xx - offset 0x070b1 ***/ /* 30 */ 0xe38987, 0xe38988, 0xe38989, 0xe3898a, /* 34 */ 0xe3898b, 0xe3898c, 0xe3898d, 0xe3898e, /* 38 */ 0xe3898f, 0xe38990, - /*** Four byte table, leaf: 8139c5xx - offset 0x07047 ***/ + /*** Four byte table, leaf: 8139c5xx - offset 0x070bb ***/ /* 30 */ 0xe38991, 0xe38992, 0xe38993, 0xe38994, /* 34 */ 0xe38995, 0xe38996, 0xe38997, 0xe38998, /* 38 */ 0xe38999, 0xe3899a, - /*** Four byte table, leaf: 8139c6xx - offset 0x07051 ***/ + /*** Four byte table, leaf: 8139c6xx - offset 0x070c5 ***/ /* 30 */ 0xe3899b, 0xe3899c, 0xe3899d, 0xe3899e, /* 34 */ 0xe3899f, 0xe389a0, 0xe389a1, 0xe389a2, /* 38 */ 0xe389a3, 0xe389a4, - /*** Four byte table, leaf: 8139c7xx - offset 0x0705b ***/ + /*** Four byte table, leaf: 8139c7xx - offset 0x070cf ***/ /* 30 */ 0xe389a5, 0xe389a6, 0xe389a7, 0xe389a8, /* 34 */ 0xe389a9, 0xe389aa, 0xe389ab, 0xe389ac, /* 38 */ 0xe389ad, 0xe389ae, - /*** Four byte table, leaf: 8139c8xx - offset 0x07065 ***/ + /*** Four byte table, leaf: 8139c8xx - offset 0x070d9 ***/ /* 30 */ 0xe389af, 0xe389b0, 0xe389b1, 0xe389b2, /* 34 */ 0xe389b3, 0xe389b4, 0xe389b5, 0xe389b6, /* 38 */ 0xe389b7, 0xe389b8, - /*** Four byte table, leaf: 8139c9xx - offset 0x0706f ***/ + /*** Four byte table, leaf: 8139c9xx - offset 0x070e3 ***/ /* 30 */ 0xe389b9, 0xe389ba, 0xe389bb, 0xe389bc, /* 34 */ 0xe389bd, 0xe389be, 0xe389bf, 0xe38a80, /* 38 */ 0xe38a81, 0xe38a82, - /*** Four byte table, leaf: 8139caxx - offset 0x07079 ***/ + /*** Four byte table, leaf: 8139caxx - offset 0x070ed ***/ /* 30 */ 0xe38a83, 0xe38a84, 0xe38a85, 0xe38a86, /* 34 */ 0xe38a87, 0xe38a88, 0xe38a89, 0xe38a8a, /* 38 */ 0xe38a8b, 0xe38a8c, - /*** Four byte table, leaf: 8139cbxx - offset 0x07083 ***/ + /*** Four byte table, leaf: 8139cbxx - offset 0x070f7 ***/ /* 30 */ 0xe38a8d, 0xe38a8e, 0xe38a8f, 0xe38a90, /* 34 */ 0xe38a91, 0xe38a92, 0xe38a93, 0xe38a94, /* 38 */ 0xe38a95, 0xe38a96, - /*** Four byte table, leaf: 8139ccxx - offset 0x0708d ***/ + /*** Four byte table, leaf: 8139ccxx - offset 0x07101 ***/ /* 30 */ 0xe38a97, 0xe38a98, 0xe38a99, 0xe38a9a, /* 34 */ 0xe38a9b, 0xe38a9c, 0xe38a9d, 0xe38a9e, /* 38 */ 0xe38a9f, 0xe38aa0, - /*** Four byte table, leaf: 8139cdxx - offset 0x07097 ***/ + /*** Four byte table, leaf: 8139cdxx - offset 0x0710b ***/ /* 30 */ 0xe38aa1, 0xe38aa2, 0xe38aa4, 0xe38aa5, /* 34 */ 0xe38aa6, 0xe38aa7, 0xe38aa8, 0xe38aa9, /* 38 */ 0xe38aaa, 0xe38aab, - /*** Four byte table, leaf: 8139cexx - offset 0x070a1 ***/ + /*** Four byte table, leaf: 8139cexx - offset 0x07115 ***/ /* 30 */ 0xe38aac, 0xe38aad, 0xe38aae, 0xe38aaf, /* 34 */ 0xe38ab0, 0xe38ab1, 0xe38ab2, 0xe38ab3, /* 38 */ 0xe38ab4, 0xe38ab5, - /*** Four byte table, leaf: 8139cfxx - offset 0x070ab ***/ + /*** Four byte table, leaf: 8139cfxx - offset 0x0711f ***/ /* 30 */ 0xe38ab6, 0xe38ab7, 0xe38ab8, 0xe38ab9, /* 34 */ 0xe38aba, 0xe38abb, 0xe38abc, 0xe38abd, /* 38 */ 0xe38abe, 0xe38abf, - /*** Four byte table, leaf: 8139d0xx - offset 0x070b5 ***/ + /*** Four byte table, leaf: 8139d0xx - offset 0x07129 ***/ /* 30 */ 0xe38b80, 0xe38b81, 0xe38b82, 0xe38b83, /* 34 */ 0xe38b84, 0xe38b85, 0xe38b86, 0xe38b87, /* 38 */ 0xe38b88, 0xe38b89, - /*** Four byte table, leaf: 8139d1xx - offset 0x070bf ***/ + /*** Four byte table, leaf: 8139d1xx - offset 0x07133 ***/ /* 30 */ 0xe38b8a, 0xe38b8b, 0xe38b8c, 0xe38b8d, /* 34 */ 0xe38b8e, 0xe38b8f, 0xe38b90, 0xe38b91, /* 38 */ 0xe38b92, 0xe38b93, - /*** Four byte table, leaf: 8139d2xx - offset 0x070c9 ***/ + /*** Four byte table, leaf: 8139d2xx - offset 0x0713d ***/ /* 30 */ 0xe38b94, 0xe38b95, 0xe38b96, 0xe38b97, /* 34 */ 0xe38b98, 0xe38b99, 0xe38b9a, 0xe38b9b, /* 38 */ 0xe38b9c, 0xe38b9d, - /*** Four byte table, leaf: 8139d3xx - offset 0x070d3 ***/ + /*** Four byte table, leaf: 8139d3xx - offset 0x07147 ***/ /* 30 */ 0xe38b9e, 0xe38b9f, 0xe38ba0, 0xe38ba1, /* 34 */ 0xe38ba2, 0xe38ba3, 0xe38ba4, 0xe38ba5, /* 38 */ 0xe38ba6, 0xe38ba7, - /*** Four byte table, leaf: 8139d4xx - offset 0x070dd ***/ + /*** Four byte table, leaf: 8139d4xx - offset 0x07151 ***/ /* 30 */ 0xe38ba8, 0xe38ba9, 0xe38baa, 0xe38bab, /* 34 */ 0xe38bac, 0xe38bad, 0xe38bae, 0xe38baf, /* 38 */ 0xe38bb0, 0xe38bb1, - /*** Four byte table, leaf: 8139d5xx - offset 0x070e7 ***/ + /*** Four byte table, leaf: 8139d5xx - offset 0x0715b ***/ /* 30 */ 0xe38bb2, 0xe38bb3, 0xe38bb4, 0xe38bb5, /* 34 */ 0xe38bb6, 0xe38bb7, 0xe38bb8, 0xe38bb9, /* 38 */ 0xe38bba, 0xe38bbb, - /*** Four byte table, leaf: 8139d6xx - offset 0x070f1 ***/ + /*** Four byte table, leaf: 8139d6xx - offset 0x07165 ***/ /* 30 */ 0xe38bbc, 0xe38bbd, 0xe38bbe, 0xe38bbf, /* 34 */ 0xe38c80, 0xe38c81, 0xe38c82, 0xe38c83, /* 38 */ 0xe38c84, 0xe38c85, - /*** Four byte table, leaf: 8139d7xx - offset 0x070fb ***/ + /*** Four byte table, leaf: 8139d7xx - offset 0x0716f ***/ /* 30 */ 0xe38c86, 0xe38c87, 0xe38c88, 0xe38c89, /* 34 */ 0xe38c8a, 0xe38c8b, 0xe38c8c, 0xe38c8d, /* 38 */ 0xe38c8e, 0xe38c8f, - /*** Four byte table, leaf: 8139d8xx - offset 0x07105 ***/ + /*** Four byte table, leaf: 8139d8xx - offset 0x07179 ***/ /* 30 */ 0xe38c90, 0xe38c91, 0xe38c92, 0xe38c93, /* 34 */ 0xe38c94, 0xe38c95, 0xe38c96, 0xe38c97, /* 38 */ 0xe38c98, 0xe38c99, - /*** Four byte table, leaf: 8139d9xx - offset 0x0710f ***/ + /*** Four byte table, leaf: 8139d9xx - offset 0x07183 ***/ /* 30 */ 0xe38c9a, 0xe38c9b, 0xe38c9c, 0xe38c9d, /* 34 */ 0xe38c9e, 0xe38c9f, 0xe38ca0, 0xe38ca1, /* 38 */ 0xe38ca2, 0xe38ca3, - /*** Four byte table, leaf: 8139daxx - offset 0x07119 ***/ + /*** Four byte table, leaf: 8139daxx - offset 0x0718d ***/ /* 30 */ 0xe38ca4, 0xe38ca5, 0xe38ca6, 0xe38ca7, /* 34 */ 0xe38ca8, 0xe38ca9, 0xe38caa, 0xe38cab, /* 38 */ 0xe38cac, 0xe38cad, - /*** Four byte table, leaf: 8139dbxx - offset 0x07123 ***/ + /*** Four byte table, leaf: 8139dbxx - offset 0x07197 ***/ /* 30 */ 0xe38cae, 0xe38caf, 0xe38cb0, 0xe38cb1, /* 34 */ 0xe38cb2, 0xe38cb3, 0xe38cb4, 0xe38cb5, /* 38 */ 0xe38cb6, 0xe38cb7, - /*** Four byte table, leaf: 8139dcxx - offset 0x0712d ***/ + /*** Four byte table, leaf: 8139dcxx - offset 0x071a1 ***/ /* 30 */ 0xe38cb8, 0xe38cb9, 0xe38cba, 0xe38cbb, /* 34 */ 0xe38cbc, 0xe38cbd, 0xe38cbe, 0xe38cbf, /* 38 */ 0xe38d80, 0xe38d81, - /*** Four byte table, leaf: 8139ddxx - offset 0x07137 ***/ + /*** Four byte table, leaf: 8139ddxx - offset 0x071ab ***/ /* 30 */ 0xe38d82, 0xe38d83, 0xe38d84, 0xe38d85, /* 34 */ 0xe38d86, 0xe38d87, 0xe38d88, 0xe38d89, /* 38 */ 0xe38d8a, 0xe38d8b, - /*** Four byte table, leaf: 8139dexx - offset 0x07141 ***/ + /*** Four byte table, leaf: 8139dexx - offset 0x071b5 ***/ /* 30 */ 0xe38d8c, 0xe38d8d, 0xe38d8e, 0xe38d8f, /* 34 */ 0xe38d90, 0xe38d91, 0xe38d92, 0xe38d93, /* 38 */ 0xe38d94, 0xe38d95, - /*** Four byte table, leaf: 8139dfxx - offset 0x0714b ***/ + /*** Four byte table, leaf: 8139dfxx - offset 0x071bf ***/ /* 30 */ 0xe38d96, 0xe38d97, 0xe38d98, 0xe38d99, /* 34 */ 0xe38d9a, 0xe38d9b, 0xe38d9c, 0xe38d9d, /* 38 */ 0xe38d9e, 0xe38d9f, - /*** Four byte table, leaf: 8139e0xx - offset 0x07155 ***/ + /*** Four byte table, leaf: 8139e0xx - offset 0x071c9 ***/ /* 30 */ 0xe38da0, 0xe38da1, 0xe38da2, 0xe38da3, /* 34 */ 0xe38da4, 0xe38da5, 0xe38da6, 0xe38da7, /* 38 */ 0xe38da8, 0xe38da9, - /*** Four byte table, leaf: 8139e1xx - offset 0x0715f ***/ + /*** Four byte table, leaf: 8139e1xx - offset 0x071d3 ***/ /* 30 */ 0xe38daa, 0xe38dab, 0xe38dac, 0xe38dad, /* 34 */ 0xe38dae, 0xe38daf, 0xe38db0, 0xe38db1, /* 38 */ 0xe38db2, 0xe38db3, - /*** Four byte table, leaf: 8139e2xx - offset 0x07169 ***/ + /*** Four byte table, leaf: 8139e2xx - offset 0x071dd ***/ /* 30 */ 0xe38db4, 0xe38db5, 0xe38db6, 0xe38db7, /* 34 */ 0xe38db8, 0xe38db9, 0xe38dba, 0xe38dbb, /* 38 */ 0xe38dbc, 0xe38dbd, - /*** Four byte table, leaf: 8139e3xx - offset 0x07173 ***/ + /*** Four byte table, leaf: 8139e3xx - offset 0x071e7 ***/ /* 30 */ 0xe38dbe, 0xe38dbf, 0xe38e80, 0xe38e81, /* 34 */ 0xe38e82, 0xe38e83, 0xe38e84, 0xe38e85, /* 38 */ 0xe38e86, 0xe38e87, - /*** Four byte table, leaf: 8139e4xx - offset 0x0717d ***/ + /*** Four byte table, leaf: 8139e4xx - offset 0x071f1 ***/ /* 30 */ 0xe38e88, 0xe38e89, 0xe38e8a, 0xe38e8b, /* 34 */ 0xe38e8c, 0xe38e8d, 0xe38e90, 0xe38e91, /* 38 */ 0xe38e92, 0xe38e93, - /*** Four byte table, leaf: 8139e5xx - offset 0x07187 ***/ + /*** Four byte table, leaf: 8139e5xx - offset 0x071fb ***/ /* 30 */ 0xe38e94, 0xe38e95, 0xe38e96, 0xe38e97, /* 34 */ 0xe38e98, 0xe38e99, 0xe38e9a, 0xe38e9b, /* 38 */ 0xe38e9f, 0xe38ea0, - /*** Four byte table, leaf: 8139e6xx - offset 0x07191 ***/ + /*** Four byte table, leaf: 8139e6xx - offset 0x07205 ***/ /* 30 */ 0xe38ea2, 0xe38ea3, 0xe38ea4, 0xe38ea5, /* 34 */ 0xe38ea6, 0xe38ea7, 0xe38ea8, 0xe38ea9, /* 38 */ 0xe38eaa, 0xe38eab, - /*** Four byte table, leaf: 8139e7xx - offset 0x0719b ***/ + /*** Four byte table, leaf: 8139e7xx - offset 0x0720f ***/ /* 30 */ 0xe38eac, 0xe38ead, 0xe38eae, 0xe38eaf, /* 34 */ 0xe38eb0, 0xe38eb1, 0xe38eb2, 0xe38eb3, /* 38 */ 0xe38eb4, 0xe38eb5, - /*** Four byte table, leaf: 8139e8xx - offset 0x071a5 ***/ + /*** Four byte table, leaf: 8139e8xx - offset 0x07219 ***/ /* 30 */ 0xe38eb6, 0xe38eb7, 0xe38eb8, 0xe38eb9, /* 34 */ 0xe38eba, 0xe38ebb, 0xe38ebc, 0xe38ebd, /* 38 */ 0xe38ebe, 0xe38ebf, - /*** Four byte table, leaf: 8139e9xx - offset 0x071af ***/ + /*** Four byte table, leaf: 8139e9xx - offset 0x07223 ***/ /* 30 */ 0xe38f80, 0xe38f81, 0xe38f82, 0xe38f83, /* 34 */ 0xe38f85, 0xe38f86, 0xe38f87, 0xe38f88, /* 38 */ 0xe38f89, 0xe38f8a, - /*** Four byte table, leaf: 8139eaxx - offset 0x071b9 ***/ + /*** Four byte table, leaf: 8139eaxx - offset 0x0722d ***/ /* 30 */ 0xe38f8b, 0xe38f8c, 0xe38f8d, 0xe38f8f, /* 34 */ 0xe38f90, 0xe38f93, 0xe38f94, 0xe38f96, /* 38 */ 0xe38f97, 0xe38f98, - /*** Four byte table, leaf: 8139ebxx - offset 0x071c3 ***/ + /*** Four byte table, leaf: 8139ebxx - offset 0x07237 ***/ /* 30 */ 0xe38f99, 0xe38f9a, 0xe38f9b, 0xe38f9c, /* 34 */ 0xe38f9d, 0xe38f9e, 0xe38f9f, 0xe38fa0, /* 38 */ 0xe38fa1, 0xe38fa2, - /*** Four byte table, leaf: 8139ecxx - offset 0x071cd ***/ + /*** Four byte table, leaf: 8139ecxx - offset 0x07241 ***/ /* 30 */ 0xe38fa3, 0xe38fa4, 0xe38fa5, 0xe38fa6, /* 34 */ 0xe38fa7, 0xe38fa8, 0xe38fa9, 0xe38faa, /* 38 */ 0xe38fab, 0xe38fac, - /*** Four byte table, leaf: 8139edxx - offset 0x071d7 ***/ + /*** Four byte table, leaf: 8139edxx - offset 0x0724b ***/ /* 30 */ 0xe38fad, 0xe38fae, 0xe38faf, 0xe38fb0, /* 34 */ 0xe38fb1, 0xe38fb2, 0xe38fb3, 0xe38fb4, /* 38 */ 0xe38fb5, 0xe38fb6, - /*** Four byte table, leaf: 8139eexx - offset 0x071e1 ***/ + /*** Four byte table, leaf: 8139eexx - offset 0x07255 ***/ /* 30 */ 0xe38fb7, 0xe38fb8, 0xe38fb9, 0xe38fba, /* 34 */ 0xe38fbb, 0xe38fbc, 0xe38fbd, 0xe38fbe, /* 38 */ 0xe38fbf, 0xe39080, - /*** Four byte table, leaf: 8139efxx - offset 0x071eb ***/ + /*** Four byte table, leaf: 8139efxx - offset 0x0725f ***/ /* 30 */ 0xe39081, 0xe39082, 0xe39083, 0xe39084, /* 34 */ 0xe39085, 0xe39086, 0xe39087, 0xe39088, /* 38 */ 0xe39089, 0xe3908a, - /*** Four byte table, leaf: 8139f0xx - offset 0x071f5 ***/ + /*** Four byte table, leaf: 8139f0xx - offset 0x07269 ***/ /* 30 */ 0xe3908b, 0xe3908c, 0xe3908d, 0xe3908e, /* 34 */ 0xe3908f, 0xe39090, 0xe39091, 0xe39092, /* 38 */ 0xe39093, 0xe39094, - /*** Four byte table, leaf: 8139f1xx - offset 0x071ff ***/ + /*** Four byte table, leaf: 8139f1xx - offset 0x07273 ***/ /* 30 */ 0xe39095, 0xe39096, 0xe39097, 0xe39098, /* 34 */ 0xe39099, 0xe3909a, 0xe3909b, 0xe3909c, /* 38 */ 0xe3909d, 0xe3909e, - /*** Four byte table, leaf: 8139f2xx - offset 0x07209 ***/ + /*** Four byte table, leaf: 8139f2xx - offset 0x0727d ***/ /* 30 */ 0xe3909f, 0xe390a0, 0xe390a1, 0xe390a2, /* 34 */ 0xe390a3, 0xe390a4, 0xe390a5, 0xe390a6, /* 38 */ 0xe390a7, 0xe390a8, - /*** Four byte table, leaf: 8139f3xx - offset 0x07213 ***/ + /*** Four byte table, leaf: 8139f3xx - offset 0x07287 ***/ /* 30 */ 0xe390a9, 0xe390aa, 0xe390ab, 0xe390ac, /* 34 */ 0xe390ad, 0xe390ae, 0xe390af, 0xe390b0, /* 38 */ 0xe390b1, 0xe390b2, - /*** Four byte table, leaf: 8139f4xx - offset 0x0721d ***/ + /*** Four byte table, leaf: 8139f4xx - offset 0x07291 ***/ /* 30 */ 0xe390b3, 0xe390b4, 0xe390b5, 0xe390b6, /* 34 */ 0xe390b7, 0xe390b8, 0xe390b9, 0xe390ba, /* 38 */ 0xe390bb, 0xe390bc, - /*** Four byte table, leaf: 8139f5xx - offset 0x07227 ***/ + /*** Four byte table, leaf: 8139f5xx - offset 0x0729b ***/ /* 30 */ 0xe390bd, 0xe390be, 0xe390bf, 0xe39180, /* 34 */ 0xe39181, 0xe39182, 0xe39183, 0xe39184, /* 38 */ 0xe39185, 0xe39186, - /*** Four byte table, leaf: 8139f6xx - offset 0x07231 ***/ + /*** Four byte table, leaf: 8139f6xx - offset 0x072a5 ***/ /* 30 */ 0xe39188, 0xe39189, 0xe3918a, 0xe3918b, /* 34 */ 0xe3918c, 0xe3918d, 0xe3918e, 0xe3918f, /* 38 */ 0xe39190, 0xe39191, - /*** Four byte table, leaf: 8139f7xx - offset 0x0723b ***/ + /*** Four byte table, leaf: 8139f7xx - offset 0x072af ***/ /* 30 */ 0xe39192, 0xe39193, 0xe39194, 0xe39195, /* 34 */ 0xe39196, 0xe39197, 0xe39198, 0xe39199, /* 38 */ 0xe3919a, 0xe3919b, - /*** Four byte table, leaf: 8139f8xx - offset 0x07245 ***/ + /*** Four byte table, leaf: 8139f8xx - offset 0x072b9 ***/ /* 30 */ 0xe3919c, 0xe3919d, 0xe3919e, 0xe3919f, /* 34 */ 0xe391a0, 0xe391a1, 0xe391a2, 0xe391a3, /* 38 */ 0xe391a4, 0xe391a5, - /*** Four byte table, leaf: 8139f9xx - offset 0x0724f ***/ + /*** Four byte table, leaf: 8139f9xx - offset 0x072c3 ***/ /* 30 */ 0xe391a6, 0xe391a7, 0xe391a8, 0xe391a9, /* 34 */ 0xe391aa, 0xe391ab, 0xe391ac, 0xe391ad, /* 38 */ 0xe391ae, 0xe391af, - /*** Four byte table, leaf: 8139faxx - offset 0x07259 ***/ + /*** Four byte table, leaf: 8139faxx - offset 0x072cd ***/ /* 30 */ 0xe391b0, 0xe391b1, 0xe391b2, 0xe391b4, /* 34 */ 0xe391b5, 0xe391b6, 0xe391b7, 0xe391b8, /* 38 */ 0xe391b9, 0xe391ba, - /*** Four byte table, leaf: 8139fbxx - offset 0x07263 ***/ + /*** Four byte table, leaf: 8139fbxx - offset 0x072d7 ***/ /* 30 */ 0xe391bb, 0xe391bc, 0xe391bd, 0xe391be, /* 34 */ 0xe391bf, 0xe39280, 0xe39281, 0xe39282, /* 38 */ 0xe39283, 0xe39284, - /*** Four byte table, leaf: 8139fcxx - offset 0x0726d ***/ + /*** Four byte table, leaf: 8139fcxx - offset 0x072e1 ***/ /* 30 */ 0xe39285, 0xe39286, 0xe39287, 0xe39288, /* 34 */ 0xe39289, 0xe3928a, 0xe3928b, 0xe3928c, /* 38 */ 0xe3928d, 0xe3928e, - /*** Four byte table, leaf: 8139fdxx - offset 0x07277 ***/ + /*** Four byte table, leaf: 8139fdxx - offset 0x072eb ***/ /* 30 */ 0xe3928f, 0xe39290, 0xe39291, 0xe39292, /* 34 */ 0xe39293, 0xe39294, 0xe39295, 0xe39296, /* 38 */ 0xe39297, 0xe39298, - /*** Four byte table, leaf: 8139fexx - offset 0x07281 ***/ + /*** Four byte table, leaf: 8139fexx - offset 0x072f5 ***/ /* 30 */ 0xe39299, 0xe3929a, 0xe3929b, 0xe3929c, /* 34 */ 0xe3929d, 0xe3929e, 0xe3929f, 0xe392a0, /* 38 */ 0xe392a1, 0xe392a2, - /*** Four byte table, leaf: 823081xx - offset 0x0728b ***/ + /*** Four byte table, leaf: 823081xx - offset 0x072ff ***/ /* 30 */ 0xe392a3, 0xe392a4, 0xe392a5, 0xe392a6, /* 34 */ 0xe392a7, 0xe392a8, 0xe392a9, 0xe392aa, /* 38 */ 0xe392ab, 0xe392ac, - /*** Four byte table, leaf: 823082xx - offset 0x07295 ***/ + /*** Four byte table, leaf: 823082xx - offset 0x07309 ***/ /* 30 */ 0xe392ad, 0xe392ae, 0xe392af, 0xe392b0, /* 34 */ 0xe392b1, 0xe392b2, 0xe392b3, 0xe392b4, /* 38 */ 0xe392b5, 0xe392b6, - /*** Four byte table, leaf: 823083xx - offset 0x0729f ***/ + /*** Four byte table, leaf: 823083xx - offset 0x07313 ***/ /* 30 */ 0xe392b7, 0xe392b8, 0xe392b9, 0xe392ba, /* 34 */ 0xe392bb, 0xe392bc, 0xe392bd, 0xe392be, /* 38 */ 0xe392bf, 0xe39380, - /*** Four byte table, leaf: 823084xx - offset 0x072a9 ***/ + /*** Four byte table, leaf: 823084xx - offset 0x0731d ***/ /* 30 */ 0xe39381, 0xe39382, 0xe39383, 0xe39384, /* 34 */ 0xe39385, 0xe39386, 0xe39387, 0xe39388, /* 38 */ 0xe39389, 0xe3938a, - /*** Four byte table, leaf: 823085xx - offset 0x072b3 ***/ + /*** Four byte table, leaf: 823085xx - offset 0x07327 ***/ /* 30 */ 0xe3938b, 0xe3938c, 0xe3938d, 0xe3938e, /* 34 */ 0xe3938f, 0xe39390, 0xe39391, 0xe39392, /* 38 */ 0xe39393, 0xe39394, - /*** Four byte table, leaf: 823086xx - offset 0x072bd ***/ + /*** Four byte table, leaf: 823086xx - offset 0x07331 ***/ /* 30 */ 0xe39395, 0xe39396, 0xe39397, 0xe39398, /* 34 */ 0xe39399, 0xe3939a, 0xe3939b, 0xe3939c, /* 38 */ 0xe3939d, 0xe3939e, - /*** Four byte table, leaf: 823087xx - offset 0x072c7 ***/ + /*** Four byte table, leaf: 823087xx - offset 0x0733b ***/ /* 30 */ 0xe3939f, 0xe393a0, 0xe393a1, 0xe393a2, /* 34 */ 0xe393a3, 0xe393a4, 0xe393a5, 0xe393a6, /* 38 */ 0xe393a7, 0xe393a8, - /*** Four byte table, leaf: 823088xx - offset 0x072d1 ***/ + /*** Four byte table, leaf: 823088xx - offset 0x07345 ***/ /* 30 */ 0xe393a9, 0xe393aa, 0xe393ab, 0xe393ac, /* 34 */ 0xe393ad, 0xe393ae, 0xe393af, 0xe393b0, /* 38 */ 0xe393b1, 0xe393b2, - /*** Four byte table, leaf: 823089xx - offset 0x072db ***/ + /*** Four byte table, leaf: 823089xx - offset 0x0734f ***/ /* 30 */ 0xe393b3, 0xe393b4, 0xe393b5, 0xe393b6, /* 34 */ 0xe393b7, 0xe393b8, 0xe393b9, 0xe393ba, /* 38 */ 0xe393bb, 0xe393bc, - /*** Four byte table, leaf: 82308axx - offset 0x072e5 ***/ + /*** Four byte table, leaf: 82308axx - offset 0x07359 ***/ /* 30 */ 0xe393bd, 0xe393be, 0xe393bf, 0xe39480, /* 34 */ 0xe39481, 0xe39482, 0xe39483, 0xe39484, /* 38 */ 0xe39485, 0xe39486, - /*** Four byte table, leaf: 82308bxx - offset 0x072ef ***/ + /*** Four byte table, leaf: 82308bxx - offset 0x07363 ***/ /* 30 */ 0xe39487, 0xe39488, 0xe39489, 0xe3948a, /* 34 */ 0xe3948b, 0xe3948c, 0xe3948d, 0xe3948e, /* 38 */ 0xe3948f, 0xe39490, - /*** Four byte table, leaf: 82308cxx - offset 0x072f9 ***/ + /*** Four byte table, leaf: 82308cxx - offset 0x0736d ***/ /* 30 */ 0xe39491, 0xe39492, 0xe39493, 0xe39494, /* 34 */ 0xe39495, 0xe39496, 0xe39497, 0xe39498, /* 38 */ 0xe39499, 0xe3949a, - /*** Four byte table, leaf: 82308dxx - offset 0x07303 ***/ + /*** Four byte table, leaf: 82308dxx - offset 0x07377 ***/ /* 30 */ 0xe3949b, 0xe3949c, 0xe3949d, 0xe3949e, /* 34 */ 0xe3949f, 0xe394a0, 0xe394a1, 0xe394a2, /* 38 */ 0xe394a3, 0xe394a4, - /*** Four byte table, leaf: 82308exx - offset 0x0730d ***/ + /*** Four byte table, leaf: 82308exx - offset 0x07381 ***/ /* 30 */ 0xe394a5, 0xe394a6, 0xe394a7, 0xe394a8, /* 34 */ 0xe394a9, 0xe394aa, 0xe394ab, 0xe394ac, /* 38 */ 0xe394ad, 0xe394ae, - /*** Four byte table, leaf: 82308fxx - offset 0x07317 ***/ + /*** Four byte table, leaf: 82308fxx - offset 0x0738b ***/ /* 30 */ 0xe394af, 0xe394b0, 0xe394b1, 0xe394b2, /* 34 */ 0xe394b3, 0xe394b4, 0xe394b5, 0xe394b6, /* 38 */ 0xe394b7, 0xe394b8, - /*** Four byte table, leaf: 823090xx - offset 0x07321 ***/ + /*** Four byte table, leaf: 823090xx - offset 0x07395 ***/ /* 30 */ 0xe394b9, 0xe394ba, 0xe394bb, 0xe394bc, /* 34 */ 0xe394bd, 0xe394be, 0xe394bf, 0xe39580, /* 38 */ 0xe39581, 0xe39582, - /*** Four byte table, leaf: 823091xx - offset 0x0732b ***/ + /*** Four byte table, leaf: 823091xx - offset 0x0739f ***/ /* 30 */ 0xe39583, 0xe39584, 0xe39585, 0xe39586, /* 34 */ 0xe39587, 0xe39588, 0xe39589, 0xe3958a, /* 38 */ 0xe3958b, 0xe3958c, - /*** Four byte table, leaf: 823092xx - offset 0x07335 ***/ + /*** Four byte table, leaf: 823092xx - offset 0x073a9 ***/ /* 30 */ 0xe3958d, 0xe3958e, 0xe3958f, 0xe39590, /* 34 */ 0xe39591, 0xe39592, 0xe39593, 0xe39594, /* 38 */ 0xe39595, 0xe39596, - /*** Four byte table, leaf: 823093xx - offset 0x0733f ***/ + /*** Four byte table, leaf: 823093xx - offset 0x073b3 ***/ /* 30 */ 0xe39597, 0xe39598, 0xe39599, 0xe3959a, /* 34 */ 0xe3959b, 0xe3959c, 0xe3959d, 0xe3959e, /* 38 */ 0xe3959f, 0xe395a0, - /*** Four byte table, leaf: 823094xx - offset 0x07349 ***/ + /*** Four byte table, leaf: 823094xx - offset 0x073bd ***/ /* 30 */ 0xe395a1, 0xe395a2, 0xe395a3, 0xe395a4, /* 34 */ 0xe395a5, 0xe395a6, 0xe395a7, 0xe395a8, /* 38 */ 0xe395a9, 0xe395aa, - /*** Four byte table, leaf: 823095xx - offset 0x07353 ***/ + /*** Four byte table, leaf: 823095xx - offset 0x073c7 ***/ /* 30 */ 0xe395ab, 0xe395ac, 0xe395ad, 0xe395ae, /* 34 */ 0xe395af, 0xe395b0, 0xe395b1, 0xe395b2, /* 38 */ 0xe395b3, 0xe395b4, - /*** Four byte table, leaf: 823096xx - offset 0x0735d ***/ + /*** Four byte table, leaf: 823096xx - offset 0x073d1 ***/ /* 30 */ 0xe395b5, 0xe395b6, 0xe395b7, 0xe395b8, /* 34 */ 0xe395b9, 0xe395ba, 0xe395bb, 0xe395bc, /* 38 */ 0xe395bd, 0xe395be, - /*** Four byte table, leaf: 823097xx - offset 0x07367 ***/ + /*** Four byte table, leaf: 823097xx - offset 0x073db ***/ /* 30 */ 0xe395bf, 0xe39680, 0xe39681, 0xe39682, /* 34 */ 0xe39683, 0xe39684, 0xe39685, 0xe39686, /* 38 */ 0xe39687, 0xe39688, - /*** Four byte table, leaf: 823098xx - offset 0x07371 ***/ + /*** Four byte table, leaf: 823098xx - offset 0x073e5 ***/ /* 30 */ 0xe39689, 0xe3968a, 0xe3968b, 0xe3968c, /* 34 */ 0xe3968d, 0xe3968e, 0xe3968f, 0xe39690, /* 38 */ 0xe39691, 0xe39692, - /*** Four byte table, leaf: 823099xx - offset 0x0737b ***/ + /*** Four byte table, leaf: 823099xx - offset 0x073ef ***/ /* 30 */ 0xe39693, 0xe39694, 0xe39695, 0xe39696, /* 34 */ 0xe39697, 0xe39698, 0xe39699, 0xe3969a, /* 38 */ 0xe3969b, 0xe3969c, - /*** Four byte table, leaf: 82309axx - offset 0x07385 ***/ + /*** Four byte table, leaf: 82309axx - offset 0x073f9 ***/ /* 30 */ 0xe3969d, 0xe3969f, 0xe396a0, 0xe396a1, /* 34 */ 0xe396a2, 0xe396a3, 0xe396a4, 0xe396a5, /* 38 */ 0xe396a6, 0xe396a7, - /*** Four byte table, leaf: 82309bxx - offset 0x0738f ***/ + /*** Four byte table, leaf: 82309bxx - offset 0x07403 ***/ /* 30 */ 0xe396a8, 0xe396a9, 0xe396aa, 0xe396ab, /* 34 */ 0xe396ac, 0xe396ad, 0xe396ae, 0xe396af, /* 38 */ 0xe396b0, 0xe396b1, - /*** Four byte table, leaf: 82309cxx - offset 0x07399 ***/ + /*** Four byte table, leaf: 82309cxx - offset 0x0740d ***/ /* 30 */ 0xe396b2, 0xe396b3, 0xe396b4, 0xe396b5, /* 34 */ 0xe396b6, 0xe396b7, 0xe396b8, 0xe396b9, /* 38 */ 0xe396ba, 0xe396bb, - /*** Four byte table, leaf: 82309dxx - offset 0x073a3 ***/ + /*** Four byte table, leaf: 82309dxx - offset 0x07417 ***/ /* 30 */ 0xe396bc, 0xe396bd, 0xe396be, 0xe396bf, /* 34 */ 0xe39780, 0xe39781, 0xe39782, 0xe39783, /* 38 */ 0xe39784, 0xe39785, - /*** Four byte table, leaf: 82309exx - offset 0x073ad ***/ + /*** Four byte table, leaf: 82309exx - offset 0x07421 ***/ /* 30 */ 0xe39786, 0xe39787, 0xe39788, 0xe39789, /* 34 */ 0xe3978a, 0xe3978b, 0xe3978c, 0xe3978d, /* 38 */ 0xe3978e, 0xe3978f, - /*** Four byte table, leaf: 82309fxx - offset 0x073b7 ***/ + /*** Four byte table, leaf: 82309fxx - offset 0x0742b ***/ /* 30 */ 0xe39790, 0xe39791, 0xe39792, 0xe39793, /* 34 */ 0xe39794, 0xe39795, 0xe39796, 0xe39797, /* 38 */ 0xe39798, 0xe39799, - /*** Four byte table, leaf: 8230a0xx - offset 0x073c1 ***/ + /*** Four byte table, leaf: 8230a0xx - offset 0x07435 ***/ /* 30 */ 0xe3979a, 0xe3979b, 0xe3979c, 0xe3979d, /* 34 */ 0xe3979e, 0xe3979f, 0xe397a0, 0xe397a1, /* 38 */ 0xe397a2, 0xe397a3, - /*** Four byte table, leaf: 8230a1xx - offset 0x073cb ***/ + /*** Four byte table, leaf: 8230a1xx - offset 0x0743f ***/ /* 30 */ 0xe397a4, 0xe397a5, 0xe397a6, 0xe397a7, /* 34 */ 0xe397a8, 0xe397a9, 0xe397aa, 0xe397ab, /* 38 */ 0xe397ac, 0xe397ad, - /*** Four byte table, leaf: 8230a2xx - offset 0x073d5 ***/ + /*** Four byte table, leaf: 8230a2xx - offset 0x07449 ***/ /* 30 */ 0xe397ae, 0xe397af, 0xe397b0, 0xe397b1, /* 34 */ 0xe397b2, 0xe397b3, 0xe397b4, 0xe397b5, /* 38 */ 0xe397b6, 0xe397b7, - /*** Four byte table, leaf: 8230a3xx - offset 0x073df ***/ + /*** Four byte table, leaf: 8230a3xx - offset 0x07453 ***/ /* 30 */ 0xe397b8, 0xe397b9, 0xe397ba, 0xe397bb, /* 34 */ 0xe397bc, 0xe397bd, 0xe397be, 0xe397bf, /* 38 */ 0xe39880, 0xe39881, - /*** Four byte table, leaf: 8230a4xx - offset 0x073e9 ***/ + /*** Four byte table, leaf: 8230a4xx - offset 0x0745d ***/ /* 30 */ 0xe39882, 0xe39883, 0xe39884, 0xe39885, /* 34 */ 0xe39886, 0xe39887, 0xe39888, 0xe39889, /* 38 */ 0xe3988a, 0xe3988b, - /*** Four byte table, leaf: 8230a5xx - offset 0x073f3 ***/ + /*** Four byte table, leaf: 8230a5xx - offset 0x07467 ***/ /* 30 */ 0xe3988c, 0xe3988d, 0xe3988f, 0xe39890, /* 34 */ 0xe39891, 0xe39892, 0xe39893, 0xe39894, /* 38 */ 0xe39895, 0xe39896, - /*** Four byte table, leaf: 8230a6xx - offset 0x073fd ***/ + /*** Four byte table, leaf: 8230a6xx - offset 0x07471 ***/ /* 30 */ 0xe39897, 0xe39898, 0xe39899, /* 7 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8230f2xx - offset 0x07400 ***/ + /*** Four byte table, leaf: 8230f2xx - offset 0x07474 ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0xe3a499, 0xe3a49a, - /*** Four byte table, leaf: 8230f3xx - offset 0x0740a ***/ + /*** Four byte table, leaf: 8230f3xx - offset 0x0747e ***/ /* 30 */ 0xe3a49b, 0xe3a49c, 0xe3a49d, 0xe3a49e, /* 34 */ 0xe3a49f, 0xe3a4a0, 0xe3a4a1, 0xe3a4a2, /* 38 */ 0xe3a4a3, 0xe3a4a4, - /*** Four byte table, leaf: 8230f4xx - offset 0x07414 ***/ + /*** Four byte table, leaf: 8230f4xx - offset 0x07488 ***/ /* 30 */ 0xe3a4a5, 0xe3a4a6, 0xe3a4a7, 0xe3a4a8, /* 34 */ 0xe3a4a9, 0xe3a4aa, 0xe3a4ab, 0xe3a4ac, /* 38 */ 0xe3a4ad, 0xe3a4ae, - /*** Four byte table, leaf: 8230f5xx - offset 0x0741e ***/ + /*** Four byte table, leaf: 8230f5xx - offset 0x07492 ***/ /* 30 */ 0xe3a4af, 0xe3a4b0, 0xe3a4b1, 0xe3a4b2, /* 34 */ 0xe3a4b3, 0xe3a4b4, 0xe3a4b5, 0xe3a4b6, /* 38 */ 0xe3a4b7, 0xe3a4b8, - /*** Four byte table, leaf: 8230f6xx - offset 0x07428 ***/ + /*** Four byte table, leaf: 8230f6xx - offset 0x0749c ***/ /* 30 */ 0xe3a4b9, 0xe3a4ba, 0xe3a4bb, 0xe3a4bc, /* 34 */ 0xe3a4bd, 0xe3a4be, 0xe3a4bf, 0xe3a580, /* 38 */ 0xe3a581, 0xe3a582, - /*** Four byte table, leaf: 8230f7xx - offset 0x07432 ***/ + /*** Four byte table, leaf: 8230f7xx - offset 0x074a6 ***/ /* 30 */ 0xe3a583, 0xe3a584, 0xe3a585, 0xe3a586, /* 34 */ 0xe3a587, 0xe3a588, 0xe3a589, 0xe3a58a, /* 38 */ 0xe3a58b, 0xe3a58c, - /*** Four byte table, leaf: 8230f8xx - offset 0x0743c ***/ + /*** Four byte table, leaf: 8230f8xx - offset 0x074b0 ***/ /* 30 */ 0xe3a58d, 0xe3a58e, 0xe3a58f, 0xe3a590, /* 34 */ 0xe3a591, 0xe3a592, 0xe3a593, 0xe3a594, /* 38 */ 0xe3a595, 0xe3a596, - /*** Four byte table, leaf: 8230f9xx - offset 0x07446 ***/ + /*** Four byte table, leaf: 8230f9xx - offset 0x074ba ***/ /* 30 */ 0xe3a597, 0xe3a598, 0xe3a599, 0xe3a59a, /* 34 */ 0xe3a59b, 0xe3a59c, 0xe3a59d, 0xe3a59e, /* 38 */ 0xe3a59f, 0xe3a5a0, - /*** Four byte table, leaf: 8230faxx - offset 0x07450 ***/ + /*** Four byte table, leaf: 8230faxx - offset 0x074c4 ***/ /* 30 */ 0xe3a5a1, 0xe3a5a2, 0xe3a5a3, 0xe3a5a4, /* 34 */ 0xe3a5a5, 0xe3a5a6, 0xe3a5a7, 0xe3a5a8, /* 38 */ 0xe3a5a9, 0xe3a5aa, - /*** Four byte table, leaf: 8230fbxx - offset 0x0745a ***/ + /*** Four byte table, leaf: 8230fbxx - offset 0x074ce ***/ /* 30 */ 0xe3a5ab, 0xe3a5ac, 0xe3a5ad, 0xe3a5af, /* 34 */ 0xe3a5b0, 0xe3a5b1, 0xe3a5b2, 0xe3a5b3, /* 38 */ 0xe3a5b4, 0xe3a5b5, - /*** Four byte table, leaf: 8230fcxx - offset 0x07464 ***/ + /*** Four byte table, leaf: 8230fcxx - offset 0x074d8 ***/ /* 30 */ 0xe3a5b6, 0xe3a5b7, 0xe3a5b8, 0xe3a5b9, /* 34 */ 0xe3a5ba, 0xe3a5bb, 0xe3a5bc, 0xe3a5bd, /* 38 */ 0xe3a5be, 0xe3a5bf, - /*** Four byte table, leaf: 8230fdxx - offset 0x0746e ***/ + /*** Four byte table, leaf: 8230fdxx - offset 0x074e2 ***/ /* 30 */ 0xe3a680, 0xe3a681, 0xe3a682, 0xe3a683, /* 34 */ 0xe3a684, 0xe3a685, 0xe3a686, 0xe3a687, /* 38 */ 0xe3a688, 0xe3a689, - /*** Four byte table, leaf: 8230fexx - offset 0x07478 ***/ + /*** Four byte table, leaf: 8230fexx - offset 0x074ec ***/ /* 30 */ 0xe3a68a, 0xe3a68b, 0xe3a68c, 0xe3a68d, /* 34 */ 0xe3a68e, 0xe3a68f, 0xe3a690, 0xe3a691, /* 38 */ 0xe3a692, 0xe3a693, - /*** Four byte table, leaf: 823181xx - offset 0x07482 ***/ + /*** Four byte table, leaf: 823181xx - offset 0x074f6 ***/ /* 30 */ 0xe3a694, 0xe3a695, 0xe3a696, 0xe3a697, /* 34 */ 0xe3a698, 0xe3a699, 0xe3a69a, 0xe3a69b, /* 38 */ 0xe3a69c, 0xe3a69d, - /*** Four byte table, leaf: 823182xx - offset 0x0748c ***/ + /*** Four byte table, leaf: 823182xx - offset 0x07500 ***/ /* 30 */ 0xe3a69e, 0xe3a69f, 0xe3a6a0, 0xe3a6a1, /* 34 */ 0xe3a6a2, 0xe3a6a3, 0xe3a6a4, 0xe3a6a5, /* 38 */ 0xe3a6a6, 0xe3a6a7, - /*** Four byte table, leaf: 823183xx - offset 0x07496 ***/ + /*** Four byte table, leaf: 823183xx - offset 0x0750a ***/ /* 30 */ 0xe3a6a8, 0xe3a6a9, 0xe3a6aa, 0xe3a6ab, /* 34 */ 0xe3a6ac, 0xe3a6ad, 0xe3a6ae, 0xe3a6af, /* 38 */ 0xe3a6b0, 0xe3a6b1, - /*** Four byte table, leaf: 823184xx - offset 0x074a0 ***/ + /*** Four byte table, leaf: 823184xx - offset 0x07514 ***/ /* 30 */ 0xe3a6b2, 0xe3a6b3, 0xe3a6b4, 0xe3a6b5, /* 34 */ 0xe3a6b6, 0xe3a6b7, 0xe3a6b8, 0xe3a6b9, /* 38 */ 0xe3a6ba, 0xe3a6bb, - /*** Four byte table, leaf: 823185xx - offset 0x074aa ***/ + /*** Four byte table, leaf: 823185xx - offset 0x0751e ***/ /* 30 */ 0xe3a6bc, 0xe3a6bd, 0xe3a6be, 0xe3a6bf, /* 34 */ 0xe3a780, 0xe3a781, 0xe3a782, 0xe3a783, /* 38 */ 0xe3a784, 0xe3a785, - /*** Four byte table, leaf: 823186xx - offset 0x074b4 ***/ + /*** Four byte table, leaf: 823186xx - offset 0x07528 ***/ /* 30 */ 0xe3a786, 0xe3a787, 0xe3a788, 0xe3a789, /* 34 */ 0xe3a78a, 0xe3a78b, 0xe3a78c, 0xe3a78d, /* 38 */ 0xe3a78e, 0xe3a791, - /*** Four byte table, leaf: 823187xx - offset 0x074be ***/ + /*** Four byte table, leaf: 823187xx - offset 0x07532 ***/ /* 30 */ 0xe3a792, 0xe3a793, 0xe3a794, 0xe3a795, /* 34 */ 0xe3a796, 0xe3a797, 0xe3a798, 0xe3a799, /* 38 */ 0xe3a79a, 0xe3a79b, - /*** Four byte table, leaf: 823188xx - offset 0x074c8 ***/ + /*** Four byte table, leaf: 823188xx - offset 0x0753c ***/ /* 30 */ 0xe3a79c, 0xe3a79d, 0xe3a79e, 0xe3a7a0, /* 34 */ 0xe3a7a1, 0xe3a7a2, 0xe3a7a3, 0xe3a7a4, /* 38 */ 0xe3a7a5, 0xe3a7a6, - /*** Four byte table, leaf: 823189xx - offset 0x074d2 ***/ + /*** Four byte table, leaf: 823189xx - offset 0x07546 ***/ /* 30 */ 0xe3a7a7, 0xe3a7a8, 0xe3a7a9, 0xe3a7aa, /* 34 */ 0xe3a7ab, 0xe3a7ac, 0xe3a7ad, 0xe3a7ae, /* 38 */ 0xe3a7af, 0xe3a7b0, - /*** Four byte table, leaf: 82318axx - offset 0x074dc ***/ + /*** Four byte table, leaf: 82318axx - offset 0x07550 ***/ /* 30 */ 0xe3a7b1, 0xe3a7b2, 0xe3a7b3, 0xe3a7b4, /* 34 */ 0xe3a7b5, 0xe3a7b6, 0xe3a7b7, 0xe3a7b8, /* 38 */ 0xe3a7b9, 0xe3a7ba, - /*** Four byte table, leaf: 82318bxx - offset 0x074e6 ***/ + /*** Four byte table, leaf: 82318bxx - offset 0x0755a ***/ /* 30 */ 0xe3a7bb, 0xe3a7bc, 0xe3a7bd, 0xe3a7be, /* 34 */ 0xe3a7bf, 0xe3a880, 0xe3a881, 0xe3a882, /* 38 */ 0xe3a883, 0xe3a884, - /*** Four byte table, leaf: 82318cxx - offset 0x074f0 ***/ + /*** Four byte table, leaf: 82318cxx - offset 0x07564 ***/ /* 30 */ 0xe3a885, 0xe3a886, 0xe3a887, 0xe3a888, /* 34 */ 0xe3a889, 0xe3a88a, 0xe3a88b, 0xe3a88c, /* 38 */ 0xe3a88d, 0xe3a88e, - /*** Four byte table, leaf: 82318dxx - offset 0x074fa ***/ + /*** Four byte table, leaf: 82318dxx - offset 0x0756e ***/ /* 30 */ 0xe3a88f, 0xe3a890, 0xe3a891, 0xe3a892, /* 34 */ 0xe3a893, 0xe3a894, 0xe3a895, 0xe3a896, /* 38 */ 0xe3a897, 0xe3a898, - /*** Four byte table, leaf: 82318exx - offset 0x07504 ***/ + /*** Four byte table, leaf: 82318exx - offset 0x07578 ***/ /* 30 */ 0xe3a899, 0xe3a89a, 0xe3a89b, 0xe3a89c, /* 34 */ 0xe3a89d, 0xe3a89e, 0xe3a89f, 0xe3a8a0, /* 38 */ 0xe3a8a1, 0xe3a8a2, - /*** Four byte table, leaf: 82318fxx - offset 0x0750e ***/ + /*** Four byte table, leaf: 82318fxx - offset 0x07582 ***/ /* 30 */ 0xe3a8a3, 0xe3a8a4, 0xe3a8a5, 0xe3a8a6, /* 34 */ 0xe3a8a7, 0xe3a8a8, 0xe3a8a9, 0xe3a8aa, /* 38 */ 0xe3a8ab, 0xe3a8ac, - /*** Four byte table, leaf: 823190xx - offset 0x07518 ***/ + /*** Four byte table, leaf: 823190xx - offset 0x0758c ***/ /* 30 */ 0xe3a8ad, 0xe3a8ae, 0xe3a8af, 0xe3a8b0, /* 34 */ 0xe3a8b1, 0xe3a8b2, 0xe3a8b3, 0xe3a8b4, /* 38 */ 0xe3a8b5, 0xe3a8b6, - /*** Four byte table, leaf: 823191xx - offset 0x07522 ***/ + /*** Four byte table, leaf: 823191xx - offset 0x07596 ***/ /* 30 */ 0xe3a8b7, 0xe3a8b8, 0xe3a8b9, 0xe3a8ba, /* 34 */ 0xe3a8bb, 0xe3a8bc, 0xe3a8bd, 0xe3a8be, /* 38 */ 0xe3a8bf, 0xe3a980, - /*** Four byte table, leaf: 823192xx - offset 0x0752c ***/ + /*** Four byte table, leaf: 823192xx - offset 0x075a0 ***/ /* 30 */ 0xe3a981, 0xe3a982, 0xe3a983, 0xe3a984, /* 34 */ 0xe3a985, 0xe3a986, 0xe3a987, 0xe3a988, /* 38 */ 0xe3a989, 0xe3a98a, - /*** Four byte table, leaf: 823193xx - offset 0x07536 ***/ + /*** Four byte table, leaf: 823193xx - offset 0x075aa ***/ /* 30 */ 0xe3a98b, 0xe3a98c, 0xe3a98d, 0xe3a98e, /* 34 */ 0xe3a98f, 0xe3a990, 0xe3a991, 0xe3a992, /* 38 */ 0xe3a993, 0xe3a994, - /*** Four byte table, leaf: 823194xx - offset 0x07540 ***/ + /*** Four byte table, leaf: 823194xx - offset 0x075b4 ***/ /* 30 */ 0xe3a995, 0xe3a996, 0xe3a997, 0xe3a998, /* 34 */ 0xe3a999, 0xe3a99a, 0xe3a99b, 0xe3a99c, /* 38 */ 0xe3a99d, 0xe3a99e, - /*** Four byte table, leaf: 823195xx - offset 0x0754a ***/ + /*** Four byte table, leaf: 823195xx - offset 0x075be ***/ /* 30 */ 0xe3a99f, 0xe3a9a0, 0xe3a9a1, 0xe3a9a2, /* 34 */ 0xe3a9a3, 0xe3a9a4, 0xe3a9a5, 0xe3a9a6, /* 38 */ 0xe3a9a7, 0xe3a9a8, - /*** Four byte table, leaf: 823196xx - offset 0x07554 ***/ + /*** Four byte table, leaf: 823196xx - offset 0x075c8 ***/ /* 30 */ 0xe3a9a9, 0xe3a9aa, 0xe3a9ab, 0xe3a9ac, /* 34 */ 0xe3a9ad, 0xe3a9ae, 0xe3a9af, 0xe3a9b0, /* 38 */ 0xe3a9b1, 0xe3a9b2, - /*** Four byte table, leaf: 823197xx - offset 0x0755e ***/ + /*** Four byte table, leaf: 823197xx - offset 0x075d2 ***/ /* 30 */ 0xe3a9b4, 0xe3a9b5, 0xe3a9b6, 0xe3a9b7, /* 34 */ 0xe3a9b8, 0xe3a9b9, 0xe3a9ba, 0xe3a9bb, /* 38 */ 0xe3a9bc, 0xe3a9bd, - /*** Four byte table, leaf: 823198xx - offset 0x07568 ***/ + /*** Four byte table, leaf: 823198xx - offset 0x075dc ***/ /* 30 */ 0xe3a9be, 0xe3a9bf, 0xe3aa80, 0xe3aa81, /* 34 */ 0xe3aa82, 0xe3aa83, 0xe3aa84, 0xe3aa85, /* 38 */ 0xe3aa86, 0xe3aa87, - /*** Four byte table, leaf: 823199xx - offset 0x07572 ***/ + /*** Four byte table, leaf: 823199xx - offset 0x075e6 ***/ /* 30 */ 0xe3aa88, 0xe3aa89, 0xe3aa8a, 0xe3aa8b, /* 34 */ 0xe3aa8c, 0xe3aa8d, 0xe3aa8e, 0xe3aa8f, /* 38 */ 0xe3aa90, 0xe3aa91, - /*** Four byte table, leaf: 82319axx - offset 0x0757c ***/ + /*** Four byte table, leaf: 82319axx - offset 0x075f0 ***/ /* 30 */ 0xe3aa92, 0xe3aa93, 0xe3aa94, 0xe3aa95, /* 34 */ 0xe3aa96, 0xe3aa97, 0xe3aa98, 0xe3aa99, /* 38 */ 0xe3aa9a, 0xe3aa9b, - /*** Four byte table, leaf: 82319bxx - offset 0x07586 ***/ + /*** Four byte table, leaf: 82319bxx - offset 0x075fa ***/ /* 30 */ 0xe3aa9c, 0xe3aa9d, 0xe3aa9e, 0xe3aa9f, /* 34 */ 0xe3aaa0, 0xe3aaa1, 0xe3aaa2, 0xe3aaa3, /* 38 */ 0xe3aaa4, 0xe3aaa5, - /*** Four byte table, leaf: 82319cxx - offset 0x07590 ***/ + /*** Four byte table, leaf: 82319cxx - offset 0x07604 ***/ /* 30 */ 0xe3aaa6, 0xe3aaa7, 0xe3aaa8, 0xe3aaa9, /* 34 */ 0xe3aaaa, 0xe3aaab, 0xe3aaac, 0xe3aaad, /* 38 */ 0xe3aaae, 0xe3aaaf, - /*** Four byte table, leaf: 82319dxx - offset 0x0759a ***/ + /*** Four byte table, leaf: 82319dxx - offset 0x0760e ***/ /* 30 */ 0xe3aab0, 0xe3aab1, 0xe3aab2, 0xe3aab3, /* 34 */ 0xe3aab4, 0xe3aab5, 0xe3aab6, 0xe3aab7, /* 38 */ 0xe3aab8, 0xe3aab9, - /*** Four byte table, leaf: 82319exx - offset 0x075a4 ***/ + /*** Four byte table, leaf: 82319exx - offset 0x07618 ***/ /* 30 */ 0xe3aaba, 0xe3aabb, 0xe3aabc, 0xe3aabd, /* 34 */ 0xe3aabe, 0xe3aabf, 0xe3ab80, 0xe3ab81, /* 38 */ 0xe3ab82, 0xe3ab83, - /*** Four byte table, leaf: 82319fxx - offset 0x075ae ***/ + /*** Four byte table, leaf: 82319fxx - offset 0x07622 ***/ /* 30 */ 0xe3ab84, 0xe3ab85, 0xe3ab86, 0xe3ab87, /* 34 */ 0xe3ab88, 0xe3ab89, 0xe3ab8a, 0xe3ab8b, /* 38 */ 0xe3ab8c, 0xe3ab8d, - /*** Four byte table, leaf: 8231a0xx - offset 0x075b8 ***/ + /*** Four byte table, leaf: 8231a0xx - offset 0x0762c ***/ /* 30 */ 0xe3ab8e, 0xe3ab8f, 0xe3ab90, 0xe3ab91, /* 34 */ 0xe3ab92, 0xe3ab93, 0xe3ab94, 0xe3ab95, /* 38 */ 0xe3ab96, 0xe3ab97, - /*** Four byte table, leaf: 8231a1xx - offset 0x075c2 ***/ + /*** Four byte table, leaf: 8231a1xx - offset 0x07636 ***/ /* 30 */ 0xe3ab98, 0xe3ab99, 0xe3ab9a, 0xe3ab9b, /* 34 */ 0xe3ab9c, 0xe3ab9d, 0xe3ab9e, 0xe3ab9f, /* 38 */ 0xe3aba0, 0xe3aba1, - /*** Four byte table, leaf: 8231a2xx - offset 0x075cc ***/ + /*** Four byte table, leaf: 8231a2xx - offset 0x07640 ***/ /* 30 */ 0xe3aba2, 0xe3aba3, 0xe3aba4, 0xe3aba5, /* 34 */ 0xe3aba6, 0xe3aba7, 0xe3aba8, 0xe3aba9, /* 38 */ 0xe3abaa, 0xe3abab, - /*** Four byte table, leaf: 8231a3xx - offset 0x075d6 ***/ + /*** Four byte table, leaf: 8231a3xx - offset 0x0764a ***/ /* 30 */ 0xe3abac, 0xe3abad, 0xe3abae, 0xe3abaf, /* 34 */ 0xe3abb0, 0xe3abb1, 0xe3abb2, 0xe3abb3, /* 38 */ 0xe3abb4, 0xe3abb5, - /*** Four byte table, leaf: 8231a4xx - offset 0x075e0 ***/ + /*** Four byte table, leaf: 8231a4xx - offset 0x07654 ***/ /* 30 */ 0xe3abb6, 0xe3abb7, 0xe3abb8, 0xe3abb9, /* 34 */ 0xe3abba, 0xe3abbb, 0xe3abbc, 0xe3abbd, /* 38 */ 0xe3abbe, 0xe3abbf, - /*** Four byte table, leaf: 8231a5xx - offset 0x075ea ***/ + /*** Four byte table, leaf: 8231a5xx - offset 0x0765e ***/ /* 30 */ 0xe3ac80, 0xe3ac81, 0xe3ac82, 0xe3ac83, /* 34 */ 0xe3ac84, 0xe3ac85, 0xe3ac86, 0xe3ac87, /* 38 */ 0xe3ac88, 0xe3ac89, - /*** Four byte table, leaf: 8231a6xx - offset 0x075f4 ***/ + /*** Four byte table, leaf: 8231a6xx - offset 0x07668 ***/ /* 30 */ 0xe3ac8a, 0xe3ac8b, 0xe3ac8c, 0xe3ac8d, /* 34 */ 0xe3ac8e, 0xe3ac8f, 0xe3ac90, 0xe3ac91, /* 38 */ 0xe3ac92, 0xe3ac93, - /*** Four byte table, leaf: 8231a7xx - offset 0x075fe ***/ + /*** Four byte table, leaf: 8231a7xx - offset 0x07672 ***/ /* 30 */ 0xe3ac94, 0xe3ac95, 0xe3ac96, 0xe3ac97, /* 34 */ 0xe3ac98, 0xe3ac99, 0xe3ac9a, 0xe3ac9b, /* 38 */ 0xe3ac9c, 0xe3ac9d, - /*** Four byte table, leaf: 8231a8xx - offset 0x07608 ***/ + /*** Four byte table, leaf: 8231a8xx - offset 0x0767c ***/ /* 30 */ 0xe3ac9e, 0xe3ac9f, 0xe3aca0, 0xe3aca1, /* 34 */ 0xe3aca2, 0xe3aca3, 0xe3aca4, 0xe3aca5, /* 38 */ 0xe3aca6, 0xe3aca7, - /*** Four byte table, leaf: 8231a9xx - offset 0x07612 ***/ + /*** Four byte table, leaf: 8231a9xx - offset 0x07686 ***/ /* 30 */ 0xe3aca8, 0xe3aca9, 0xe3acaa, 0xe3acab, /* 34 */ 0xe3acac, 0xe3acad, 0xe3acae, 0xe3acaf, /* 38 */ 0xe3acb0, 0xe3acb1, - /*** Four byte table, leaf: 8231aaxx - offset 0x0761c ***/ + /*** Four byte table, leaf: 8231aaxx - offset 0x07690 ***/ /* 30 */ 0xe3acb2, 0xe3acb3, 0xe3acb4, 0xe3acb5, /* 34 */ 0xe3acb6, 0xe3acb7, 0xe3acb8, 0xe3acb9, /* 38 */ 0xe3acba, 0xe3acbb, - /*** Four byte table, leaf: 8231abxx - offset 0x07626 ***/ + /*** Four byte table, leaf: 8231abxx - offset 0x0769a ***/ /* 30 */ 0xe3acbc, 0xe3acbd, 0xe3acbe, 0xe3acbf, /* 34 */ 0xe3ad80, 0xe3ad81, 0xe3ad82, 0xe3ad83, /* 38 */ 0xe3ad84, 0xe3ad85, - /*** Four byte table, leaf: 8231acxx - offset 0x07630 ***/ + /*** Four byte table, leaf: 8231acxx - offset 0x076a4 ***/ /* 30 */ 0xe3ad86, 0xe3ad87, 0xe3ad88, 0xe3ad89, /* 34 */ 0xe3ad8a, 0xe3ad8b, 0xe3ad8c, 0xe3ad8d, /* 38 */ 0xe3ad8f, 0xe3ad90, - /*** Four byte table, leaf: 8231adxx - offset 0x0763a ***/ + /*** Four byte table, leaf: 8231adxx - offset 0x076ae ***/ /* 30 */ 0xe3ad91, 0xe3ad92, 0xe3ad93, 0xe3ad94, /* 34 */ 0xe3ad95, 0xe3ad96, 0xe3ad97, 0xe3ad98, /* 38 */ 0xe3ad99, 0xe3ad9a, - /*** Four byte table, leaf: 8231aexx - offset 0x07644 ***/ + /*** Four byte table, leaf: 8231aexx - offset 0x076b8 ***/ /* 30 */ 0xe3ad9b, 0xe3ad9c, 0xe3ad9d, 0xe3ad9e, /* 34 */ 0xe3ad9f, 0xe3ada0, 0xe3ada1, 0xe3ada2, /* 38 */ 0xe3ada3, 0xe3ada4, - /*** Four byte table, leaf: 8231afxx - offset 0x0764e ***/ + /*** Four byte table, leaf: 8231afxx - offset 0x076c2 ***/ /* 30 */ 0xe3ada5, 0xe3ada6, 0xe3ada7, 0xe3ada8, /* 34 */ 0xe3ada9, 0xe3adaa, 0xe3adab, 0xe3adac, /* 38 */ 0xe3adad, 0xe3adae, - /*** Four byte table, leaf: 8231b0xx - offset 0x07658 ***/ + /*** Four byte table, leaf: 8231b0xx - offset 0x076cc ***/ /* 30 */ 0xe3adaf, 0xe3adb0, 0xe3adb1, 0xe3adb2, /* 34 */ 0xe3adb3, 0xe3adb4, 0xe3adb5, 0xe3adb6, /* 38 */ 0xe3adb7, 0xe3adb8, - /*** Four byte table, leaf: 8231b1xx - offset 0x07662 ***/ + /*** Four byte table, leaf: 8231b1xx - offset 0x076d6 ***/ /* 30 */ 0xe3adb9, 0xe3adba, 0xe3adbb, 0xe3adbc, /* 34 */ 0xe3adbd, 0xe3adbe, 0xe3adbf, 0xe3ae80, /* 38 */ 0xe3ae81, 0xe3ae82, - /*** Four byte table, leaf: 8231b2xx - offset 0x0766c ***/ + /*** Four byte table, leaf: 8231b2xx - offset 0x076e0 ***/ /* 30 */ 0xe3ae83, 0xe3ae84, 0xe3ae85, 0xe3ae86, /* 34 */ 0xe3ae87, 0xe3ae88, 0xe3ae89, 0xe3ae8a, /* 38 */ 0xe3ae8b, 0xe3ae8c, - /*** Four byte table, leaf: 8231b3xx - offset 0x07676 ***/ + /*** Four byte table, leaf: 8231b3xx - offset 0x076ea ***/ /* 30 */ 0xe3ae8d, 0xe3ae8e, 0xe3ae8f, 0xe3ae90, /* 34 */ 0xe3ae91, 0xe3ae92, 0xe3ae93, 0xe3ae94, /* 38 */ 0xe3ae95, 0xe3ae96, - /*** Four byte table, leaf: 8231b4xx - offset 0x07680 ***/ + /*** Four byte table, leaf: 8231b4xx - offset 0x076f4 ***/ /* 30 */ 0xe3ae97, 0xe3ae98, 0xe3ae99, 0xe3ae9a, /* 34 */ 0xe3ae9b, 0xe3ae9c, 0xe3ae9d, 0xe3ae9e, /* 38 */ 0xe3ae9f, 0xe3aea0, - /*** Four byte table, leaf: 8231b5xx - offset 0x0768a ***/ + /*** Four byte table, leaf: 8231b5xx - offset 0x076fe ***/ /* 30 */ 0xe3aea1, 0xe3aea2, 0xe3aea3, 0xe3aea4, /* 34 */ 0xe3aea5, 0xe3aea6, 0xe3aea7, 0xe3aea8, /* 38 */ 0xe3aea9, 0xe3aeaa, - /*** Four byte table, leaf: 8231b6xx - offset 0x07694 ***/ + /*** Four byte table, leaf: 8231b6xx - offset 0x07708 ***/ /* 30 */ 0xe3aeab, 0xe3aeac, 0xe3aead, 0xe3aeae, /* 34 */ 0xe3aeaf, 0xe3aeb0, 0xe3aeb1, 0xe3aeb2, /* 38 */ 0xe3aeb3, 0xe3aeb4, - /*** Four byte table, leaf: 8231b7xx - offset 0x0769e ***/ + /*** Four byte table, leaf: 8231b7xx - offset 0x07712 ***/ /* 30 */ 0xe3aeb5, 0xe3aeb6, 0xe3aeb7, 0xe3aeb8, /* 34 */ 0xe3aeb9, 0xe3aeba, 0xe3aebb, 0xe3aebc, /* 38 */ 0xe3aebd, 0xe3aebe, - /*** Four byte table, leaf: 8231b8xx - offset 0x076a8 ***/ + /*** Four byte table, leaf: 8231b8xx - offset 0x0771c ***/ /* 30 */ 0xe3aebf, 0xe3af80, 0xe3af81, 0xe3af82, /* 34 */ 0xe3af83, 0xe3af84, 0xe3af85, 0xe3af86, /* 38 */ 0xe3af87, 0xe3af88, - /*** Four byte table, leaf: 8231b9xx - offset 0x076b2 ***/ + /*** Four byte table, leaf: 8231b9xx - offset 0x07726 ***/ /* 30 */ 0xe3af89, 0xe3af8a, 0xe3af8b, 0xe3af8c, /* 34 */ 0xe3af8d, 0xe3af8e, 0xe3af8f, 0xe3af90, /* 38 */ 0xe3af91, 0xe3af92, - /*** Four byte table, leaf: 8231baxx - offset 0x076bc ***/ + /*** Four byte table, leaf: 8231baxx - offset 0x07730 ***/ /* 30 */ 0xe3af93, 0xe3af94, 0xe3af95, 0xe3af96, /* 34 */ 0xe3af97, 0xe3af98, 0xe3af99, 0xe3af9a, /* 38 */ 0xe3af9b, 0xe3af9c, - /*** Four byte table, leaf: 8231bbxx - offset 0x076c6 ***/ + /*** Four byte table, leaf: 8231bbxx - offset 0x0773a ***/ /* 30 */ 0xe3af9d, 0xe3af9e, 0xe3af9f, 0xe3afa0, /* 34 */ 0xe3afa1, 0xe3afa2, 0xe3afa3, 0xe3afa4, /* 38 */ 0xe3afa5, 0xe3afa6, - /*** Four byte table, leaf: 8231bcxx - offset 0x076d0 ***/ + /*** Four byte table, leaf: 8231bcxx - offset 0x07744 ***/ /* 30 */ 0xe3afa7, 0xe3afa8, 0xe3afa9, 0xe3afaa, /* 34 */ 0xe3afab, 0xe3afac, 0xe3afad, 0xe3afae, /* 38 */ 0xe3afaf, 0xe3afb0, - /*** Four byte table, leaf: 8231bdxx - offset 0x076da ***/ + /*** Four byte table, leaf: 8231bdxx - offset 0x0774e ***/ /* 30 */ 0xe3afb1, 0xe3afb2, 0xe3afb3, 0xe3afb4, /* 34 */ 0xe3afb5, 0xe3afb6, 0xe3afb7, 0xe3afb8, /* 38 */ 0xe3afb9, 0xe3afba, - /*** Four byte table, leaf: 8231bexx - offset 0x076e4 ***/ + /*** Four byte table, leaf: 8231bexx - offset 0x07758 ***/ /* 30 */ 0xe3afbb, 0xe3afbc, 0xe3afbd, 0xe3afbe, /* 34 */ 0xe3afbf, 0xe3b080, 0xe3b081, 0xe3b082, /* 38 */ 0xe3b083, 0xe3b084, - /*** Four byte table, leaf: 8231bfxx - offset 0x076ee ***/ + /*** Four byte table, leaf: 8231bfxx - offset 0x07762 ***/ /* 30 */ 0xe3b085, 0xe3b086, 0xe3b087, 0xe3b088, /* 34 */ 0xe3b089, 0xe3b08a, 0xe3b08b, 0xe3b08c, /* 38 */ 0xe3b08d, 0xe3b08e, - /*** Four byte table, leaf: 8231c0xx - offset 0x076f8 ***/ + /*** Four byte table, leaf: 8231c0xx - offset 0x0776c ***/ /* 30 */ 0xe3b08f, 0xe3b090, 0xe3b091, 0xe3b092, /* 34 */ 0xe3b093, 0xe3b094, 0xe3b095, 0xe3b096, /* 38 */ 0xe3b097, 0xe3b098, - /*** Four byte table, leaf: 8231c1xx - offset 0x07702 ***/ + /*** Four byte table, leaf: 8231c1xx - offset 0x07776 ***/ /* 30 */ 0xe3b099, 0xe3b09a, 0xe3b09b, 0xe3b09c, /* 34 */ 0xe3b09d, 0xe3b09e, 0xe3b09f, 0xe3b0a0, /* 38 */ 0xe3b0a1, 0xe3b0a2, - /*** Four byte table, leaf: 8231c2xx - offset 0x0770c ***/ + /*** Four byte table, leaf: 8231c2xx - offset 0x07780 ***/ /* 30 */ 0xe3b0a3, 0xe3b0a4, 0xe3b0a5, 0xe3b0a6, /* 34 */ 0xe3b0a7, 0xe3b0a8, 0xe3b0a9, 0xe3b0aa, /* 38 */ 0xe3b0ab, 0xe3b0ac, - /*** Four byte table, leaf: 8231c3xx - offset 0x07716 ***/ + /*** Four byte table, leaf: 8231c3xx - offset 0x0778a ***/ /* 30 */ 0xe3b0ad, 0xe3b0ae, 0xe3b0af, 0xe3b0b0, /* 34 */ 0xe3b0b1, 0xe3b0b2, 0xe3b0b3, 0xe3b0b4, /* 38 */ 0xe3b0b5, 0xe3b0b6, - /*** Four byte table, leaf: 8231c4xx - offset 0x07720 ***/ + /*** Four byte table, leaf: 8231c4xx - offset 0x07794 ***/ /* 30 */ 0xe3b0b7, 0xe3b0b8, 0xe3b0b9, 0xe3b0ba, /* 34 */ 0xe3b0bb, 0xe3b0bc, 0xe3b0bd, 0xe3b0be, /* 38 */ 0xe3b0bf, 0xe3b180, - /*** Four byte table, leaf: 8231c5xx - offset 0x0772a ***/ + /*** Four byte table, leaf: 8231c5xx - offset 0x0779e ***/ /* 30 */ 0xe3b181, 0xe3b182, 0xe3b183, 0xe3b184, /* 34 */ 0xe3b185, 0xe3b186, 0xe3b187, 0xe3b188, /* 38 */ 0xe3b189, 0xe3b18a, - /*** Four byte table, leaf: 8231c6xx - offset 0x07734 ***/ + /*** Four byte table, leaf: 8231c6xx - offset 0x077a8 ***/ /* 30 */ 0xe3b18b, 0xe3b18c, 0xe3b18d, 0xe3b18e, /* 34 */ 0xe3b18f, 0xe3b190, 0xe3b191, 0xe3b192, /* 38 */ 0xe3b193, 0xe3b194, - /*** Four byte table, leaf: 8231c7xx - offset 0x0773e ***/ + /*** Four byte table, leaf: 8231c7xx - offset 0x077b2 ***/ /* 30 */ 0xe3b195, 0xe3b196, 0xe3b197, 0xe3b198, /* 34 */ 0xe3b199, 0xe3b19a, 0xe3b19b, 0xe3b19c, /* 38 */ 0xe3b19d, 0xe3b19e, - /*** Four byte table, leaf: 8231c8xx - offset 0x07748 ***/ + /*** Four byte table, leaf: 8231c8xx - offset 0x077bc ***/ /* 30 */ 0xe3b19f, 0xe3b1a0, 0xe3b1a1, 0xe3b1a2, /* 34 */ 0xe3b1a3, 0xe3b1a4, 0xe3b1a5, 0xe3b1a6, /* 38 */ 0xe3b1a7, 0xe3b1a8, - /*** Four byte table, leaf: 8231c9xx - offset 0x07752 ***/ + /*** Four byte table, leaf: 8231c9xx - offset 0x077c6 ***/ /* 30 */ 0xe3b1a9, 0xe3b1aa, 0xe3b1ab, 0xe3b1ac, /* 34 */ 0xe3b1ad, 0xe3b1af, 0xe3b1b0, 0xe3b1b1, /* 38 */ 0xe3b1b2, 0xe3b1b3, - /*** Four byte table, leaf: 8231caxx - offset 0x0775c ***/ + /*** Four byte table, leaf: 8231caxx - offset 0x077d0 ***/ /* 30 */ 0xe3b1b4, 0xe3b1b5, 0xe3b1b6, 0xe3b1b7, /* 34 */ 0xe3b1b8, 0xe3b1b9, 0xe3b1ba, 0xe3b1bb, /* 38 */ 0xe3b1bc, 0xe3b1bd, - /*** Four byte table, leaf: 8231cbxx - offset 0x07766 ***/ + /*** Four byte table, leaf: 8231cbxx - offset 0x077da ***/ /* 30 */ 0xe3b1be, 0xe3b1bf, 0xe3b280, 0xe3b281, /* 34 */ 0xe3b282, 0xe3b283, 0xe3b284, 0xe3b285, /* 38 */ 0xe3b286, 0xe3b287, - /*** Four byte table, leaf: 8231ccxx - offset 0x07770 ***/ + /*** Four byte table, leaf: 8231ccxx - offset 0x077e4 ***/ /* 30 */ 0xe3b288, 0xe3b289, 0xe3b28a, 0xe3b28b, /* 34 */ 0xe3b28c, 0xe3b28d, 0xe3b28e, 0xe3b28f, /* 38 */ 0xe3b290, 0xe3b291, - /*** Four byte table, leaf: 8231cdxx - offset 0x0777a ***/ + /*** Four byte table, leaf: 8231cdxx - offset 0x077ee ***/ /* 30 */ 0xe3b292, 0xe3b293, 0xe3b294, 0xe3b295, /* 34 */ 0xe3b296, 0xe3b297, 0xe3b298, 0xe3b299, /* 38 */ 0xe3b29a, 0xe3b29b, - /*** Four byte table, leaf: 8231cexx - offset 0x07784 ***/ + /*** Four byte table, leaf: 8231cexx - offset 0x077f8 ***/ /* 30 */ 0xe3b29c, 0xe3b29d, 0xe3b29e, 0xe3b29f, /* 34 */ 0xe3b2a0, 0xe3b2a1, 0xe3b2a2, 0xe3b2a3, /* 38 */ 0xe3b2a4, 0xe3b2a5, - /*** Four byte table, leaf: 8231cfxx - offset 0x0778e ***/ + /*** Four byte table, leaf: 8231cfxx - offset 0x07802 ***/ /* 30 */ 0xe3b2a6, 0xe3b2a7, 0xe3b2a8, 0xe3b2a9, /* 34 */ 0xe3b2aa, 0xe3b2ab, 0xe3b2ac, 0xe3b2ad, /* 38 */ 0xe3b2ae, 0xe3b2af, - /*** Four byte table, leaf: 8231d0xx - offset 0x07798 ***/ + /*** Four byte table, leaf: 8231d0xx - offset 0x0780c ***/ /* 30 */ 0xe3b2b0, 0xe3b2b1, 0xe3b2b2, 0xe3b2b3, /* 34 */ 0xe3b2b4, 0xe3b2b5, 0xe3b2b6, 0xe3b2b7, /* 38 */ 0xe3b2b8, 0xe3b2b9, - /*** Four byte table, leaf: 8231d1xx - offset 0x077a2 ***/ + /*** Four byte table, leaf: 8231d1xx - offset 0x07816 ***/ /* 30 */ 0xe3b2ba, 0xe3b2bb, 0xe3b2bc, 0xe3b2bd, /* 34 */ 0xe3b2be, 0xe3b2bf, 0xe3b380, 0xe3b381, /* 38 */ 0xe3b382, 0xe3b383, - /*** Four byte table, leaf: 8231d2xx - offset 0x077ac ***/ + /*** Four byte table, leaf: 8231d2xx - offset 0x07820 ***/ /* 30 */ 0xe3b384, 0xe3b385, 0xe3b386, 0xe3b387, /* 34 */ 0xe3b388, 0xe3b389, 0xe3b38a, 0xe3b38b, /* 38 */ 0xe3b38c, 0xe3b38d, - /*** Four byte table, leaf: 8231d3xx - offset 0x077b6 ***/ + /*** Four byte table, leaf: 8231d3xx - offset 0x0782a ***/ /* 30 */ 0xe3b38e, 0xe3b38f, 0xe3b390, 0xe3b391, /* 34 */ 0xe3b392, 0xe3b393, 0xe3b394, 0xe3b395, /* 38 */ 0xe3b396, 0xe3b397, - /*** Four byte table, leaf: 8231d4xx - offset 0x077c0 ***/ + /*** Four byte table, leaf: 8231d4xx - offset 0x07834 ***/ /* 30 */ 0xe3b398, 0xe3b399, 0xe3b39a, 0xe3b39b, /* 34 */ 0xe3b39c, 0xe3b39d, 0xe3b39e, 0xe3b39f, /* 2 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8232afxx - offset 0x077c8 ***/ + /*** Four byte table, leaf: 8232afxx - offset 0x0783c ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0xe48197, /* 34 */ 0xe48198, 0xe48199, 0xe4819a, 0xe4819b, /* 38 */ 0xe4819c, 0xe4819d, - /*** Four byte table, leaf: 8232b0xx - offset 0x077d2 ***/ + /*** Four byte table, leaf: 8232b0xx - offset 0x07846 ***/ /* 30 */ 0xe4819e, 0xe4819f, 0xe481a0, 0xe481a1, /* 34 */ 0xe481a2, 0xe481a3, 0xe481a4, 0xe481a5, /* 38 */ 0xe481a6, 0xe481a7, - /*** Four byte table, leaf: 8232b1xx - offset 0x077dc ***/ + /*** Four byte table, leaf: 8232b1xx - offset 0x07850 ***/ /* 30 */ 0xe481a8, 0xe481a9, 0xe481aa, 0xe481ab, /* 34 */ 0xe481ac, 0xe481ad, 0xe481ae, 0xe481af, /* 38 */ 0xe481b0, 0xe481b1, - /*** Four byte table, leaf: 8232b2xx - offset 0x077e6 ***/ + /*** Four byte table, leaf: 8232b2xx - offset 0x0785a ***/ /* 30 */ 0xe481b2, 0xe481b3, 0xe481b4, 0xe481b5, /* 34 */ 0xe481b6, 0xe481b7, 0xe481b8, 0xe481b9, /* 38 */ 0xe481ba, 0xe481bb, - /*** Four byte table, leaf: 8232b3xx - offset 0x077f0 ***/ + /*** Four byte table, leaf: 8232b3xx - offset 0x07864 ***/ /* 30 */ 0xe481bc, 0xe481bd, 0xe481be, 0xe481bf, /* 34 */ 0xe48280, 0xe48281, 0xe48282, 0xe48283, /* 38 */ 0xe48284, 0xe48285, - /*** Four byte table, leaf: 8232b4xx - offset 0x077fa ***/ + /*** Four byte table, leaf: 8232b4xx - offset 0x0786e ***/ /* 30 */ 0xe48286, 0xe48287, 0xe48288, 0xe48289, /* 34 */ 0xe4828a, 0xe4828b, 0xe4828c, 0xe4828d, /* 38 */ 0xe4828e, 0xe4828f, - /*** Four byte table, leaf: 8232b5xx - offset 0x07804 ***/ + /*** Four byte table, leaf: 8232b5xx - offset 0x07878 ***/ /* 30 */ 0xe48290, 0xe48291, 0xe48292, 0xe48293, /* 34 */ 0xe48294, 0xe48295, 0xe48296, 0xe48297, /* 38 */ 0xe48298, 0xe48299, - /*** Four byte table, leaf: 8232b6xx - offset 0x0780e ***/ + /*** Four byte table, leaf: 8232b6xx - offset 0x07882 ***/ /* 30 */ 0xe4829a, 0xe4829b, 0xe4829c, 0xe4829d, /* 34 */ 0xe4829e, 0xe4829f, 0xe482a0, 0xe482a1, /* 38 */ 0xe482a2, 0xe482a3, - /*** Four byte table, leaf: 8232b7xx - offset 0x07818 ***/ + /*** Four byte table, leaf: 8232b7xx - offset 0x0788c ***/ /* 30 */ 0xe482a4, 0xe482a5, 0xe482a6, 0xe482a7, /* 34 */ 0xe482a8, 0xe482a9, 0xe482aa, 0xe482ab, /* 38 */ 0xe482ac, 0xe482ad, - /*** Four byte table, leaf: 8232b8xx - offset 0x07822 ***/ + /*** Four byte table, leaf: 8232b8xx - offset 0x07896 ***/ /* 30 */ 0xe482ae, 0xe482af, 0xe482b0, 0xe482b1, /* 34 */ 0xe482b2, 0xe482b3, 0xe482b4, 0xe482b5, /* 38 */ 0xe482b6, 0xe482b7, - /*** Four byte table, leaf: 8232b9xx - offset 0x0782c ***/ + /*** Four byte table, leaf: 8232b9xx - offset 0x078a0 ***/ /* 30 */ 0xe482b8, 0xe482b9, 0xe482ba, 0xe482bb, /* 34 */ 0xe482bc, 0xe482bd, 0xe482be, 0xe482bf, /* 38 */ 0xe48380, 0xe48381, - /*** Four byte table, leaf: 8232baxx - offset 0x07836 ***/ + /*** Four byte table, leaf: 8232baxx - offset 0x078aa ***/ /* 30 */ 0xe48382, 0xe48383, 0xe48384, 0xe48385, /* 34 */ 0xe48386, 0xe48387, 0xe48388, 0xe48389, /* 38 */ 0xe4838a, 0xe4838b, - /*** Four byte table, leaf: 8232bbxx - offset 0x07840 ***/ + /*** Four byte table, leaf: 8232bbxx - offset 0x078b4 ***/ /* 30 */ 0xe4838c, 0xe4838d, 0xe4838e, 0xe4838f, /* 34 */ 0xe48390, 0xe48391, 0xe48392, 0xe48393, /* 38 */ 0xe48394, 0xe48395, - /*** Four byte table, leaf: 8232bcxx - offset 0x0784a ***/ + /*** Four byte table, leaf: 8232bcxx - offset 0x078be ***/ /* 30 */ 0xe48396, 0xe48397, 0xe48398, 0xe48399, /* 34 */ 0xe4839a, 0xe4839b, 0xe4839c, 0xe4839d, /* 38 */ 0xe4839e, 0xe4839f, - /*** Four byte table, leaf: 8232bdxx - offset 0x07854 ***/ + /*** Four byte table, leaf: 8232bdxx - offset 0x078c8 ***/ /* 30 */ 0xe483a0, 0xe483a1, 0xe483a2, 0xe483a3, /* 34 */ 0xe483a4, 0xe483a5, 0xe483a6, 0xe483a7, /* 38 */ 0xe483a8, 0xe483a9, - /*** Four byte table, leaf: 8232bexx - offset 0x0785e ***/ + /*** Four byte table, leaf: 8232bexx - offset 0x078d2 ***/ /* 30 */ 0xe483aa, 0xe483ab, 0xe483ac, 0xe483ad, /* 34 */ 0xe483ae, 0xe483af, 0xe483b0, 0xe483b1, /* 38 */ 0xe483b2, 0xe483b3, - /*** Four byte table, leaf: 8232bfxx - offset 0x07868 ***/ + /*** Four byte table, leaf: 8232bfxx - offset 0x078dc ***/ /* 30 */ 0xe483b4, 0xe483b5, 0xe483b6, 0xe483b7, /* 34 */ 0xe483b8, 0xe483b9, 0xe483ba, 0xe483bb, /* 38 */ 0xe483bc, 0xe483bd, - /*** Four byte table, leaf: 8232c0xx - offset 0x07872 ***/ + /*** Four byte table, leaf: 8232c0xx - offset 0x078e6 ***/ /* 30 */ 0xe483be, 0xe483bf, 0xe48480, 0xe48481, /* 34 */ 0xe48482, 0xe48483, 0xe48484, 0xe48485, /* 38 */ 0xe48486, 0xe48487, - /*** Four byte table, leaf: 8232c1xx - offset 0x0787c ***/ + /*** Four byte table, leaf: 8232c1xx - offset 0x078f0 ***/ /* 30 */ 0xe48488, 0xe48489, 0xe4848a, 0xe4848b, /* 34 */ 0xe4848c, 0xe4848d, 0xe4848e, 0xe4848f, /* 38 */ 0xe48490, 0xe48491, - /*** Four byte table, leaf: 8232c2xx - offset 0x07886 ***/ + /*** Four byte table, leaf: 8232c2xx - offset 0x078fa ***/ /* 30 */ 0xe48492, 0xe48493, 0xe48494, 0xe48495, /* 34 */ 0xe48496, 0xe48497, 0xe48498, 0xe48499, /* 38 */ 0xe4849a, 0xe4849b, - /*** Four byte table, leaf: 8232c3xx - offset 0x07890 ***/ + /*** Four byte table, leaf: 8232c3xx - offset 0x07904 ***/ /* 30 */ 0xe4849c, 0xe4849d, 0xe4849e, 0xe4849f, /* 34 */ 0xe484a0, 0xe484a1, 0xe484a2, 0xe484a3, /* 38 */ 0xe484a4, 0xe484a5, - /*** Four byte table, leaf: 8232c4xx - offset 0x0789a ***/ + /*** Four byte table, leaf: 8232c4xx - offset 0x0790e ***/ /* 30 */ 0xe484a6, 0xe484a7, 0xe484a8, 0xe484a9, /* 34 */ 0xe484aa, 0xe484ab, 0xe484ac, 0xe484ad, /* 38 */ 0xe484ae, 0xe484af, - /*** Four byte table, leaf: 8232c5xx - offset 0x078a4 ***/ + /*** Four byte table, leaf: 8232c5xx - offset 0x07918 ***/ /* 30 */ 0xe484b0, 0xe484b1, 0xe484b2, 0xe484b3, /* 34 */ 0xe484b4, 0xe484b5, 0xe484b6, 0xe484b7, /* 38 */ 0xe484b8, 0xe484b9, - /*** Four byte table, leaf: 8232c6xx - offset 0x078ae ***/ + /*** Four byte table, leaf: 8232c6xx - offset 0x07922 ***/ /* 30 */ 0xe484ba, 0xe484bb, 0xe484bc, 0xe484bd, /* 34 */ 0xe484be, 0xe484bf, 0xe48580, 0xe48581, /* 38 */ 0xe48582, 0xe48583, - /*** Four byte table, leaf: 8232c7xx - offset 0x078b8 ***/ + /*** Four byte table, leaf: 8232c7xx - offset 0x0792c ***/ /* 30 */ 0xe48584, 0xe48585, 0xe48586, 0xe48587, /* 34 */ 0xe48588, 0xe48589, 0xe4858a, 0xe4858b, /* 38 */ 0xe4858c, 0xe4858d, - /*** Four byte table, leaf: 8232c8xx - offset 0x078c2 ***/ + /*** Four byte table, leaf: 8232c8xx - offset 0x07936 ***/ /* 30 */ 0xe4858e, 0xe4858f, 0xe48590, 0xe48591, /* 34 */ 0xe48592, 0xe48593, 0xe48594, 0xe48595, /* 38 */ 0xe48596, 0xe48597, - /*** Four byte table, leaf: 8232c9xx - offset 0x078cc ***/ + /*** Four byte table, leaf: 8232c9xx - offset 0x07940 ***/ /* 30 */ 0xe48598, 0xe48599, 0xe4859a, 0xe4859b, /* 34 */ 0xe4859c, 0xe4859d, 0xe4859e, /* 3 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8232f8xx - offset 0x078d3 ***/ + /*** Four byte table, leaf: 8232f8xx - offset 0x07947 ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0xe48cb8, 0xe48cb9, - /*** Four byte table, leaf: 8232f9xx - offset 0x078dd ***/ + /*** Four byte table, leaf: 8232f9xx - offset 0x07951 ***/ /* 30 */ 0xe48cba, 0xe48cbb, 0xe48cbc, 0xe48cbd, /* 34 */ 0xe48cbe, 0xe48cbf, 0xe48d80, 0xe48d81, /* 38 */ 0xe48d82, 0xe48d83, - /*** Four byte table, leaf: 8232faxx - offset 0x078e7 ***/ + /*** Four byte table, leaf: 8232faxx - offset 0x0795b ***/ /* 30 */ 0xe48d84, 0xe48d85, 0xe48d86, 0xe48d87, /* 34 */ 0xe48d88, 0xe48d89, 0xe48d8a, 0xe48d8b, /* 38 */ 0xe48d8c, 0xe48d8d, - /*** Four byte table, leaf: 8232fbxx - offset 0x078f1 ***/ + /*** Four byte table, leaf: 8232fbxx - offset 0x07965 ***/ /* 30 */ 0xe48d8e, 0xe48d8f, 0xe48d90, 0xe48d91, /* 34 */ 0xe48d92, 0xe48d93, 0xe48d94, 0xe48d95, /* 38 */ 0xe48d96, 0xe48d97, - /*** Four byte table, leaf: 8232fcxx - offset 0x078fb ***/ + /*** Four byte table, leaf: 8232fcxx - offset 0x0796f ***/ /* 30 */ 0xe48d98, 0xe48d99, 0xe48d9a, 0xe48d9b, /* 34 */ 0xe48d9c, 0xe48d9d, 0xe48d9e, 0xe48d9f, /* 38 */ 0xe48da0, 0xe48da1, - /*** Four byte table, leaf: 8232fdxx - offset 0x07905 ***/ + /*** Four byte table, leaf: 8232fdxx - offset 0x07979 ***/ /* 30 */ 0xe48da2, 0xe48da3, 0xe48da4, 0xe48da5, /* 34 */ 0xe48da6, 0xe48da7, 0xe48da8, 0xe48da9, /* 38 */ 0xe48daa, 0xe48dab, - /*** Four byte table, leaf: 8232fexx - offset 0x0790f ***/ + /*** Four byte table, leaf: 8232fexx - offset 0x07983 ***/ /* 30 */ 0xe48dac, 0xe48dad, 0xe48dae, 0xe48daf, /* 34 */ 0xe48db0, 0xe48db1, 0xe48db2, 0xe48db3, /* 38 */ 0xe48db4, 0xe48db5, - /*** Four byte table, leaf: 823381xx - offset 0x07919 ***/ + /*** Four byte table, leaf: 823381xx - offset 0x0798d ***/ /* 30 */ 0xe48db6, 0xe48db7, 0xe48db8, 0xe48db9, /* 34 */ 0xe48dba, 0xe48dbb, 0xe48dbc, 0xe48dbd, /* 38 */ 0xe48dbe, 0xe48dbf, - /*** Four byte table, leaf: 823382xx - offset 0x07923 ***/ + /*** Four byte table, leaf: 823382xx - offset 0x07997 ***/ /* 30 */ 0xe48e80, 0xe48e81, 0xe48e82, 0xe48e83, /* 34 */ 0xe48e84, 0xe48e85, 0xe48e86, 0xe48e87, /* 38 */ 0xe48e88, 0xe48e89, - /*** Four byte table, leaf: 823383xx - offset 0x0792d ***/ + /*** Four byte table, leaf: 823383xx - offset 0x079a1 ***/ /* 30 */ 0xe48e8a, 0xe48e8b, 0xe48e8c, 0xe48e8d, /* 34 */ 0xe48e8e, 0xe48e8f, 0xe48e90, 0xe48e91, /* 38 */ 0xe48e92, 0xe48e93, - /*** Four byte table, leaf: 823384xx - offset 0x07937 ***/ + /*** Four byte table, leaf: 823384xx - offset 0x079ab ***/ /* 30 */ 0xe48e94, 0xe48e95, 0xe48e96, 0xe48e97, /* 34 */ 0xe48e98, 0xe48e99, 0xe48e9a, 0xe48e9b, /* 38 */ 0xe48e9c, 0xe48e9d, - /*** Four byte table, leaf: 823385xx - offset 0x07941 ***/ + /*** Four byte table, leaf: 823385xx - offset 0x079b5 ***/ /* 30 */ 0xe48e9e, 0xe48e9f, 0xe48ea0, 0xe48ea1, /* 34 */ 0xe48ea2, 0xe48ea3, 0xe48ea4, 0xe48ea5, /* 38 */ 0xe48ea6, 0xe48ea7, - /*** Four byte table, leaf: 823386xx - offset 0x0794b ***/ + /*** Four byte table, leaf: 823386xx - offset 0x079bf ***/ /* 30 */ 0xe48ea8, 0xe48ea9, 0xe48eaa, 0xe48eab, /* 34 */ 0xe48ead, 0xe48eae, 0xe48eaf, 0xe48eb0, /* 38 */ 0xe48eb2, 0xe48eb3, - /*** Four byte table, leaf: 823387xx - offset 0x07955 ***/ + /*** Four byte table, leaf: 823387xx - offset 0x079c9 ***/ /* 30 */ 0xe48eb4, 0xe48eb5, 0xe48eb6, 0xe48eb7, /* 34 */ 0xe48eb8, 0xe48eb9, 0xe48eba, 0xe48ebb, /* 38 */ 0xe48ebc, 0xe48ebd, - /*** Four byte table, leaf: 823388xx - offset 0x0795f ***/ + /*** Four byte table, leaf: 823388xx - offset 0x079d3 ***/ /* 30 */ 0xe48ebe, 0xe48ebf, 0xe48f80, 0xe48f81, /* 34 */ 0xe48f82, 0xe48f83, 0xe48f84, 0xe48f85, /* 38 */ 0xe48f86, 0xe48f87, - /*** Four byte table, leaf: 823389xx - offset 0x07969 ***/ + /*** Four byte table, leaf: 823389xx - offset 0x079dd ***/ /* 30 */ 0xe48f88, 0xe48f89, 0xe48f8a, 0xe48f8b, /* 34 */ 0xe48f8c, 0xe48f8d, 0xe48f8e, 0xe48f8f, /* 38 */ 0xe48f90, 0xe48f91, - /*** Four byte table, leaf: 82338axx - offset 0x07973 ***/ + /*** Four byte table, leaf: 82338axx - offset 0x079e7 ***/ /* 30 */ 0xe48f92, 0xe48f93, 0xe48f94, 0xe48f95, /* 34 */ 0xe48f96, 0xe48f97, 0xe48f98, 0xe48f99, /* 38 */ 0xe48f9a, 0xe48f9b, - /*** Four byte table, leaf: 82338bxx - offset 0x0797d ***/ + /*** Four byte table, leaf: 82338bxx - offset 0x079f1 ***/ /* 30 */ 0xe48f9c, 0xe48f9e, 0xe48f9f, 0xe48fa0, /* 34 */ 0xe48fa1, 0xe48fa2, 0xe48fa3, 0xe48fa4, /* 38 */ 0xe48fa5, 0xe48fa6, - /*** Four byte table, leaf: 82338cxx - offset 0x07987 ***/ + /*** Four byte table, leaf: 82338cxx - offset 0x079fb ***/ /* 30 */ 0xe48fa7, 0xe48fa8, 0xe48fa9, 0xe48faa, /* 34 */ 0xe48fab, 0xe48fac, 0xe48fad, 0xe48fae, /* 38 */ 0xe48faf, 0xe48fb0, - /*** Four byte table, leaf: 82338dxx - offset 0x07991 ***/ + /*** Four byte table, leaf: 82338dxx - offset 0x07a05 ***/ /* 30 */ 0xe48fb1, 0xe48fb2, 0xe48fb3, 0xe48fb4, /* 34 */ 0xe48fb5, 0xe48fb6, 0xe48fb7, 0xe48fb8, /* 38 */ 0xe48fb9, 0xe48fba, - /*** Four byte table, leaf: 82338exx - offset 0x0799b ***/ + /*** Four byte table, leaf: 82338exx - offset 0x07a0f ***/ /* 30 */ 0xe48fbb, 0xe48fbc, 0xe48fbd, 0xe48fbe, /* 34 */ 0xe48fbf, 0xe49080, 0xe49081, 0xe49082, /* 38 */ 0xe49083, 0xe49084, - /*** Four byte table, leaf: 82338fxx - offset 0x079a5 ***/ + /*** Four byte table, leaf: 82338fxx - offset 0x07a19 ***/ /* 30 */ 0xe49085, 0xe49086, 0xe49087, 0xe49088, /* 34 */ 0xe49089, 0xe4908a, 0xe4908b, 0xe4908c, /* 38 */ 0xe4908d, 0xe4908e, - /*** Four byte table, leaf: 823390xx - offset 0x079af ***/ + /*** Four byte table, leaf: 823390xx - offset 0x07a23 ***/ /* 30 */ 0xe4908f, 0xe49090, 0xe49091, 0xe49092, /* 34 */ 0xe49093, 0xe49094, 0xe49095, 0xe49096, /* 38 */ 0xe49097, 0xe49098, - /*** Four byte table, leaf: 823391xx - offset 0x079b9 ***/ + /*** Four byte table, leaf: 823391xx - offset 0x07a2d ***/ /* 30 */ 0xe49099, 0xe4909a, 0xe4909b, 0xe4909c, /* 34 */ 0xe4909d, 0xe4909e, 0xe4909f, 0xe490a0, /* 38 */ 0xe490a1, 0xe490a2, - /*** Four byte table, leaf: 823392xx - offset 0x079c3 ***/ + /*** Four byte table, leaf: 823392xx - offset 0x07a37 ***/ /* 30 */ 0xe490a3, 0xe490a4, 0xe490a5, 0xe490a6, /* 34 */ 0xe490a7, 0xe490a8, 0xe490a9, 0xe490aa, /* 38 */ 0xe490ab, 0xe490ac, - /*** Four byte table, leaf: 823393xx - offset 0x079cd ***/ + /*** Four byte table, leaf: 823393xx - offset 0x07a41 ***/ /* 30 */ 0xe490ad, 0xe490ae, 0xe490af, 0xe490b0, /* 34 */ 0xe490b1, 0xe490b2, 0xe490b3, 0xe490b4, /* 38 */ 0xe490b5, 0xe490b6, - /*** Four byte table, leaf: 823394xx - offset 0x079d7 ***/ + /*** Four byte table, leaf: 823394xx - offset 0x07a4b ***/ /* 30 */ 0xe490b7, 0xe490b8, 0xe490b9, 0xe490ba, /* 34 */ 0xe490bb, 0xe490bc, 0xe490bd, 0xe490be, /* 38 */ 0xe490bf, 0xe49180, - /*** Four byte table, leaf: 823395xx - offset 0x079e1 ***/ + /*** Four byte table, leaf: 823395xx - offset 0x07a55 ***/ /* 30 */ 0xe49181, 0xe49182, 0xe49183, 0xe49184, /* 34 */ 0xe49185, 0xe49186, 0xe49187, 0xe49188, /* 38 */ 0xe49189, 0xe4918a, - /*** Four byte table, leaf: 823396xx - offset 0x079eb ***/ + /*** Four byte table, leaf: 823396xx - offset 0x07a5f ***/ /* 30 */ 0xe4918b, 0xe4918c, 0xe4918d, 0xe4918e, /* 34 */ 0xe4918f, 0xe49190, 0xe49191, 0xe49192, /* 38 */ 0xe49193, 0xe49194, - /*** Four byte table, leaf: 823397xx - offset 0x079f5 ***/ + /*** Four byte table, leaf: 823397xx - offset 0x07a69 ***/ /* 30 */ 0xe49195, 0xe49196, 0xe49197, 0xe49198, /* 34 */ 0xe49199, 0xe4919a, 0xe4919b, 0xe4919c, /* 38 */ 0xe4919d, 0xe4919e, - /*** Four byte table, leaf: 823398xx - offset 0x079ff ***/ + /*** Four byte table, leaf: 823398xx - offset 0x07a73 ***/ /* 30 */ 0xe4919f, 0xe491a0, 0xe491a1, 0xe491a2, /* 34 */ 0xe491a3, 0xe491a4, 0xe491a5, 0xe491a6, /* 38 */ 0xe491a7, 0xe491a8, - /*** Four byte table, leaf: 823399xx - offset 0x07a09 ***/ + /*** Four byte table, leaf: 823399xx - offset 0x07a7d ***/ /* 30 */ 0xe491a9, 0xe491aa, 0xe491ab, 0xe491ac, /* 34 */ 0xe491ad, 0xe491ae, 0xe491af, 0xe491b0, /* 38 */ 0xe491b1, 0xe491b2, - /*** Four byte table, leaf: 82339axx - offset 0x07a13 ***/ + /*** Four byte table, leaf: 82339axx - offset 0x07a87 ***/ /* 30 */ 0xe491b3, 0xe491b4, 0xe491b5, 0xe491b6, /* 34 */ 0xe491b7, 0xe491b8, 0xe491b9, 0xe491ba, /* 38 */ 0xe491bb, 0xe491bc, - /*** Four byte table, leaf: 82339bxx - offset 0x07a1d ***/ + /*** Four byte table, leaf: 82339bxx - offset 0x07a91 ***/ /* 30 */ 0xe491bd, 0xe491be, 0xe491bf, 0xe49280, /* 34 */ 0xe49281, 0xe49282, 0xe49283, 0xe49284, /* 38 */ 0xe49285, 0xe49286, - /*** Four byte table, leaf: 82339cxx - offset 0x07a27 ***/ + /*** Four byte table, leaf: 82339cxx - offset 0x07a9b ***/ /* 30 */ 0xe49287, 0xe49288, 0xe49289, 0xe4928a, /* 34 */ 0xe4928b, 0xe4928c, 0xe4928d, 0xe4928e, /* 38 */ 0xe4928f, 0xe49290, - /*** Four byte table, leaf: 82339dxx - offset 0x07a31 ***/ + /*** Four byte table, leaf: 82339dxx - offset 0x07aa5 ***/ /* 30 */ 0xe49291, 0xe49292, 0xe49293, 0xe49294, /* 34 */ 0xe49295, 0xe49296, 0xe49297, 0xe49298, /* 38 */ 0xe49299, 0xe4929a, - /*** Four byte table, leaf: 82339exx - offset 0x07a3b ***/ + /*** Four byte table, leaf: 82339exx - offset 0x07aaf ***/ /* 30 */ 0xe4929b, 0xe4929c, 0xe4929d, 0xe4929e, /* 34 */ 0xe4929f, 0xe492a0, 0xe492a1, 0xe492a2, /* 38 */ 0xe492a3, 0xe492a4, - /*** Four byte table, leaf: 82339fxx - offset 0x07a45 ***/ + /*** Four byte table, leaf: 82339fxx - offset 0x07ab9 ***/ /* 30 */ 0xe492a5, 0xe492a6, 0xe492a7, 0xe492a8, /* 34 */ 0xe492a9, 0xe492aa, 0xe492ab, 0xe492ac, /* 38 */ 0xe492ad, 0xe492ae, - /*** Four byte table, leaf: 8233a0xx - offset 0x07a4f ***/ + /*** Four byte table, leaf: 8233a0xx - offset 0x07ac3 ***/ /* 30 */ 0xe492af, 0xe492b0, 0xe492b1, 0xe492b2, /* 34 */ 0xe492b3, 0xe492b4, 0xe492b5, 0xe492b6, /* 38 */ 0xe492b7, 0xe492b8, - /*** Four byte table, leaf: 8233a1xx - offset 0x07a59 ***/ + /*** Four byte table, leaf: 8233a1xx - offset 0x07acd ***/ /* 30 */ 0xe492b9, 0xe492ba, 0xe492bb, 0xe492bc, /* 34 */ 0xe492bd, 0xe492be, 0xe492bf, 0xe49380, /* 38 */ 0xe49381, 0xe49382, - /*** Four byte table, leaf: 8233a2xx - offset 0x07a63 ***/ + /*** Four byte table, leaf: 8233a2xx - offset 0x07ad7 ***/ /* 30 */ 0xe49383, 0xe49384, 0xe49385, 0xe49386, /* 34 */ 0xe49387, 0xe49388, 0xe49389, 0xe4938a, /* 38 */ 0xe4938b, 0xe4938c, - /*** Four byte table, leaf: 8233a3xx - offset 0x07a6d ***/ + /*** Four byte table, leaf: 8233a3xx - offset 0x07ae1 ***/ /* 30 */ 0xe4938d, 0xe4938e, 0xe4938f, 0xe49390, /* 34 */ 0xe49391, 0xe49392, 0xe49393, 0xe49394, /* 38 */ 0xe49395, /* 1 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8233c9xx - offset 0x07a76 ***/ + /*** Four byte table, leaf: 8233c9xx - offset 0x07aea ***/ /* 30 */ 0x000000, 0x000000, 0xe4998d, 0xe4998e, /* 34 */ 0xe4998f, 0xe49990, 0xe49991, 0xe49992, /* 38 */ 0xe49993, 0xe49994, - /*** Four byte table, leaf: 8233caxx - offset 0x07a80 ***/ + /*** Four byte table, leaf: 8233caxx - offset 0x07af4 ***/ /* 30 */ 0xe49995, 0xe49996, 0xe49997, 0xe49998, /* 34 */ 0xe49999, 0xe4999a, 0xe4999b, 0xe4999c, /* 38 */ 0xe4999d, 0xe4999e, - /*** Four byte table, leaf: 8233cbxx - offset 0x07a8a ***/ + /*** Four byte table, leaf: 8233cbxx - offset 0x07afe ***/ /* 30 */ 0xe4999f, 0xe499a0, 0xe499a2, 0xe499a3, /* 34 */ 0xe499a4, 0xe499a5, 0xe499a6, 0xe499a7, /* 38 */ 0xe499a8, 0xe499a9, - /*** Four byte table, leaf: 8233ccxx - offset 0x07a94 ***/ + /*** Four byte table, leaf: 8233ccxx - offset 0x07b08 ***/ /* 30 */ 0xe499aa, 0xe499ab, 0xe499ac, 0xe499ad, /* 34 */ 0xe499ae, 0xe499af, 0xe499b0, 0xe499b1, /* 38 */ 0xe499b2, 0xe499b3, - /*** Four byte table, leaf: 8233cdxx - offset 0x07a9e ***/ + /*** Four byte table, leaf: 8233cdxx - offset 0x07b12 ***/ /* 30 */ 0xe499b4, 0xe499b5, 0xe499b6, 0xe499b7, /* 34 */ 0xe499b8, 0xe499b9, 0xe499ba, 0xe499bb, /* 38 */ 0xe499bc, 0xe499bd, - /*** Four byte table, leaf: 8233cexx - offset 0x07aa8 ***/ + /*** Four byte table, leaf: 8233cexx - offset 0x07b1c ***/ /* 30 */ 0xe499be, 0xe499bf, 0xe49a80, 0xe49a81, /* 34 */ 0xe49a82, 0xe49a83, 0xe49a84, 0xe49a85, /* 38 */ 0xe49a86, 0xe49a87, - /*** Four byte table, leaf: 8233cfxx - offset 0x07ab2 ***/ + /*** Four byte table, leaf: 8233cfxx - offset 0x07b26 ***/ /* 30 */ 0xe49a88, 0xe49a89, 0xe49a8a, 0xe49a8b, /* 34 */ 0xe49a8c, 0xe49a8d, 0xe49a8e, 0xe49a8f, /* 38 */ 0xe49a90, 0xe49a91, - /*** Four byte table, leaf: 8233d0xx - offset 0x07abc ***/ + /*** Four byte table, leaf: 8233d0xx - offset 0x07b30 ***/ /* 30 */ 0xe49a92, 0xe49a93, 0xe49a94, 0xe49a95, /* 34 */ 0xe49a96, 0xe49a97, 0xe49a98, 0xe49a99, /* 38 */ 0xe49a9a, 0xe49a9b, - /*** Four byte table, leaf: 8233d1xx - offset 0x07ac6 ***/ + /*** Four byte table, leaf: 8233d1xx - offset 0x07b3a ***/ /* 30 */ 0xe49a9c, 0xe49a9d, 0xe49a9e, 0xe49a9f, /* 34 */ 0xe49aa0, 0xe49aa1, 0xe49aa2, 0xe49aa3, /* 38 */ 0xe49aa4, 0xe49aa5, - /*** Four byte table, leaf: 8233d2xx - offset 0x07ad0 ***/ + /*** Four byte table, leaf: 8233d2xx - offset 0x07b44 ***/ /* 30 */ 0xe49aa6, 0xe49aa7, 0xe49aa8, 0xe49aa9, /* 34 */ 0xe49aaa, 0xe49aab, 0xe49aac, 0xe49aad, /* 38 */ 0xe49aae, 0xe49aaf, - /*** Four byte table, leaf: 8233d3xx - offset 0x07ada ***/ + /*** Four byte table, leaf: 8233d3xx - offset 0x07b4e ***/ /* 30 */ 0xe49ab0, 0xe49ab1, 0xe49ab2, 0xe49ab3, /* 34 */ 0xe49ab4, 0xe49ab5, 0xe49ab6, 0xe49ab7, /* 38 */ 0xe49ab8, 0xe49ab9, - /*** Four byte table, leaf: 8233d4xx - offset 0x07ae4 ***/ + /*** Four byte table, leaf: 8233d4xx - offset 0x07b58 ***/ /* 30 */ 0xe49aba, 0xe49abb, 0xe49abc, 0xe49abd, /* 34 */ 0xe49abe, 0xe49abf, 0xe49b80, 0xe49b81, /* 38 */ 0xe49b82, 0xe49b83, - /*** Four byte table, leaf: 8233d5xx - offset 0x07aee ***/ + /*** Four byte table, leaf: 8233d5xx - offset 0x07b62 ***/ /* 30 */ 0xe49b84, 0xe49b85, 0xe49b86, 0xe49b87, /* 34 */ 0xe49b88, 0xe49b89, 0xe49b8a, 0xe49b8b, /* 38 */ 0xe49b8c, 0xe49b8d, - /*** Four byte table, leaf: 8233d6xx - offset 0x07af8 ***/ + /*** Four byte table, leaf: 8233d6xx - offset 0x07b6c ***/ /* 30 */ 0xe49b8e, 0xe49b8f, 0xe49b90, 0xe49b91, /* 34 */ 0xe49b92, 0xe49b93, 0xe49b94, 0xe49b95, /* 38 */ 0xe49b96, 0xe49b97, - /*** Four byte table, leaf: 8233d7xx - offset 0x07b02 ***/ + /*** Four byte table, leaf: 8233d7xx - offset 0x07b76 ***/ /* 30 */ 0xe49b98, 0xe49b99, 0xe49b9a, 0xe49b9b, /* 34 */ 0xe49b9c, 0xe49b9d, 0xe49b9e, 0xe49b9f, /* 38 */ 0xe49ba0, 0xe49ba1, - /*** Four byte table, leaf: 8233d8xx - offset 0x07b0c ***/ + /*** Four byte table, leaf: 8233d8xx - offset 0x07b80 ***/ /* 30 */ 0xe49ba2, 0xe49ba3, 0xe49ba4, 0xe49ba5, /* 34 */ 0xe49ba6, 0xe49ba7, 0xe49ba8, 0xe49ba9, /* 38 */ 0xe49baa, 0xe49bab, - /*** Four byte table, leaf: 8233d9xx - offset 0x07b16 ***/ + /*** Four byte table, leaf: 8233d9xx - offset 0x07b8a ***/ /* 30 */ 0xe49bac, 0xe49bad, 0xe49bae, 0xe49baf, /* 34 */ 0xe49bb0, 0xe49bb1, 0xe49bb2, 0xe49bb3, /* 38 */ 0xe49bb4, 0xe49bb5, - /*** Four byte table, leaf: 8233daxx - offset 0x07b20 ***/ + /*** Four byte table, leaf: 8233daxx - offset 0x07b94 ***/ /* 30 */ 0xe49bb6, 0xe49bb7, 0xe49bb8, 0xe49bb9, /* 34 */ 0xe49bba, 0xe49bbb, 0xe49bbc, 0xe49bbd, /* 38 */ 0xe49bbe, 0xe49bbf, - /*** Four byte table, leaf: 8233dbxx - offset 0x07b2a ***/ + /*** Four byte table, leaf: 8233dbxx - offset 0x07b9e ***/ /* 30 */ 0xe49c80, 0xe49c81, 0xe49c82, 0xe49c83, /* 34 */ 0xe49c84, 0xe49c85, 0xe49c86, 0xe49c87, /* 38 */ 0xe49c88, 0xe49c89, - /*** Four byte table, leaf: 8233dcxx - offset 0x07b34 ***/ + /*** Four byte table, leaf: 8233dcxx - offset 0x07ba8 ***/ /* 30 */ 0xe49c8a, 0xe49c8b, 0xe49c8c, 0xe49c8d, /* 34 */ 0xe49c8e, 0xe49c8f, 0xe49c90, 0xe49c91, /* 38 */ 0xe49c92, 0xe49c93, - /*** Four byte table, leaf: 8233ddxx - offset 0x07b3e ***/ + /*** Four byte table, leaf: 8233ddxx - offset 0x07bb2 ***/ /* 30 */ 0xe49c94, 0xe49c95, 0xe49c96, 0xe49c97, /* 34 */ 0xe49c98, 0xe49c99, 0xe49c9a, 0xe49c9b, /* 38 */ 0xe49c9c, 0xe49c9d, - /*** Four byte table, leaf: 8233dexx - offset 0x07b48 ***/ + /*** Four byte table, leaf: 8233dexx - offset 0x07bbc ***/ /* 30 */ 0xe49c9e, 0xe49c9f, 0xe49ca0, 0xe49ca1, /* 34 */ 0xe49ca2, 0xe49ca4, 0xe49ca5, 0xe49ca6, /* 38 */ 0xe49ca7, 0xe49ca8, - /*** Four byte table, leaf: 8233dfxx - offset 0x07b52 ***/ + /*** Four byte table, leaf: 8233dfxx - offset 0x07bc6 ***/ /* 30 */ 0xe49caa, 0xe49cab, 0xe49cac, 0xe49cad, /* 34 */ 0xe49cae, 0xe49caf, 0xe49cb0, 0xe49cb1, /* 38 */ 0xe49cb2, 0xe49cb3, - /*** Four byte table, leaf: 8233e0xx - offset 0x07b5c ***/ + /*** Four byte table, leaf: 8233e0xx - offset 0x07bd0 ***/ /* 30 */ 0xe49cb4, 0xe49cb5, 0xe49cb6, 0xe49cb7, /* 34 */ 0xe49cb8, 0xe49cb9, 0xe49cba, 0xe49cbb, /* 38 */ 0xe49cbc, 0xe49cbd, - /*** Four byte table, leaf: 8233e1xx - offset 0x07b66 ***/ + /*** Four byte table, leaf: 8233e1xx - offset 0x07bda ***/ /* 30 */ 0xe49cbe, 0xe49cbf, 0xe49d80, 0xe49d81, /* 34 */ 0xe49d82, 0xe49d83, 0xe49d84, 0xe49d85, /* 38 */ 0xe49d86, 0xe49d87, - /*** Four byte table, leaf: 8233e2xx - offset 0x07b70 ***/ + /*** Four byte table, leaf: 8233e2xx - offset 0x07be4 ***/ /* 30 */ 0xe49d88, 0xe49d89, 0xe49d8a, 0xe49d8b, /* 34 */ 0xe49d8c, 0xe49d8d, 0xe49d8e, 0xe49d8f, /* 38 */ 0xe49d90, 0xe49d91, - /*** Four byte table, leaf: 8233e3xx - offset 0x07b7a ***/ + /*** Four byte table, leaf: 8233e3xx - offset 0x07bee ***/ /* 30 */ 0xe49d92, 0xe49d93, 0xe49d94, 0xe49d95, /* 34 */ 0xe49d96, 0xe49d97, 0xe49d98, 0xe49d99, /* 38 */ 0xe49d9a, 0xe49d9b, - /*** Four byte table, leaf: 8233e4xx - offset 0x07b84 ***/ + /*** Four byte table, leaf: 8233e4xx - offset 0x07bf8 ***/ /* 30 */ 0xe49d9c, 0xe49d9d, 0xe49d9e, 0xe49d9f, /* 34 */ 0xe49da0, 0xe49da1, 0xe49da2, 0xe49da3, /* 38 */ 0xe49da4, 0xe49da5, - /*** Four byte table, leaf: 8233e5xx - offset 0x07b8e ***/ + /*** Four byte table, leaf: 8233e5xx - offset 0x07c02 ***/ /* 30 */ 0xe49da6, 0xe49da7, 0xe49da8, 0xe49da9, /* 34 */ 0xe49daa, 0xe49dab, 0xe49dac, 0xe49dad, /* 38 */ 0xe49dae, 0xe49daf, - /*** Four byte table, leaf: 8233e6xx - offset 0x07b98 ***/ + /*** Four byte table, leaf: 8233e6xx - offset 0x07c0c ***/ /* 30 */ 0xe49db0, 0xe49db1, 0xe49db2, 0xe49db3, /* 34 */ 0xe49db4, 0xe49db5, 0xe49db6, 0xe49db7, /* 38 */ 0xe49db8, 0xe49db9, - /*** Four byte table, leaf: 8233e7xx - offset 0x07ba2 ***/ + /*** Four byte table, leaf: 8233e7xx - offset 0x07c16 ***/ /* 30 */ 0xe49dba, 0xe49dbb, 0xe49dbd, 0xe49dbe, /* 34 */ 0xe49dbf, 0xe49e80, 0xe49e81, 0xe49e82, /* 38 */ 0xe49e83, 0xe49e84, - /*** Four byte table, leaf: 8233e8xx - offset 0x07bac ***/ + /*** Four byte table, leaf: 8233e8xx - offset 0x07c20 ***/ /* 30 */ 0xe49e85, 0xe49e86, 0xe49e87, 0xe49e88, /* 34 */ 0xe49e89, 0xe49e8a, 0xe49e8b, 0xe49e8c, /* 2 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 823496xx - offset 0x07bb4 ***/ + /*** Four byte table, leaf: 823496xx - offset 0x07c28 ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0x000000, 0xe4a588, - /*** Four byte table, leaf: 823497xx - offset 0x07bbe ***/ + /*** Four byte table, leaf: 823497xx - offset 0x07c32 ***/ /* 30 */ 0xe4a589, 0xe4a58a, 0xe4a58b, 0xe4a58c, /* 34 */ 0xe4a58d, 0xe4a58e, 0xe4a58f, 0xe4a590, /* 38 */ 0xe4a591, 0xe4a592, - /*** Four byte table, leaf: 823498xx - offset 0x07bc8 ***/ + /*** Four byte table, leaf: 823498xx - offset 0x07c3c ***/ /* 30 */ 0xe4a593, 0xe4a594, 0xe4a595, 0xe4a596, /* 34 */ 0xe4a597, 0xe4a598, 0xe4a599, 0xe4a59a, /* 38 */ 0xe4a59b, 0xe4a59c, - /*** Four byte table, leaf: 823499xx - offset 0x07bd2 ***/ + /*** Four byte table, leaf: 823499xx - offset 0x07c46 ***/ /* 30 */ 0xe4a59d, 0xe4a59e, 0xe4a59f, 0xe4a5a0, /* 34 */ 0xe4a5a1, 0xe4a5a2, 0xe4a5a3, 0xe4a5a4, /* 38 */ 0xe4a5a5, 0xe4a5a6, - /*** Four byte table, leaf: 82349axx - offset 0x07bdc ***/ + /*** Four byte table, leaf: 82349axx - offset 0x07c50 ***/ /* 30 */ 0xe4a5a7, 0xe4a5a8, 0xe4a5a9, 0xe4a5aa, /* 34 */ 0xe4a5ab, 0xe4a5ac, 0xe4a5ad, 0xe4a5ae, /* 38 */ 0xe4a5af, 0xe4a5b0, - /*** Four byte table, leaf: 82349bxx - offset 0x07be6 ***/ + /*** Four byte table, leaf: 82349bxx - offset 0x07c5a ***/ /* 30 */ 0xe4a5b1, 0xe4a5b2, 0xe4a5b3, 0xe4a5b4, /* 34 */ 0xe4a5b5, 0xe4a5b6, 0xe4a5b7, 0xe4a5b8, /* 38 */ 0xe4a5b9, 0xe4a5bb, - /*** Four byte table, leaf: 82349cxx - offset 0x07bf0 ***/ + /*** Four byte table, leaf: 82349cxx - offset 0x07c64 ***/ /* 30 */ 0xe4a5bc, 0xe4a5be, 0xe4a5bf, 0xe4a680, /* 34 */ 0xe4a681, 0xe4a684, 0xe4a687, 0xe4a688, /* 38 */ 0xe4a689, 0xe4a68a, - /*** Four byte table, leaf: 82349dxx - offset 0x07bfa ***/ + /*** Four byte table, leaf: 82349dxx - offset 0x07c6e ***/ /* 30 */ 0xe4a68b, 0xe4a68c, 0xe4a68d, 0xe4a68e, /* 34 */ 0xe4a68f, 0xe4a690, 0xe4a691, 0xe4a692, /* 38 */ 0xe4a693, 0xe4a694, - /*** Four byte table, leaf: 82349exx - offset 0x07c04 ***/ + /*** Four byte table, leaf: 82349exx - offset 0x07c78 ***/ /* 30 */ 0xe4a695, 0xe4a696, 0xe4a697, 0xe4a698, /* 34 */ 0xe4a699, 0xe4a69a, 0xe4a69c, 0xe4a69d, /* 38 */ 0xe4a69e, 0xe4a6a0, - /*** Four byte table, leaf: 82349fxx - offset 0x07c0e ***/ + /*** Four byte table, leaf: 82349fxx - offset 0x07c82 ***/ /* 30 */ 0xe4a6a1, 0xe4a6a2, 0xe4a6a3, 0xe4a6a4, /* 34 */ 0xe4a6a5, 0xe4a6a6, 0xe4a6a7, 0xe4a6a8, /* 38 */ 0xe4a6a9, 0xe4a6aa, - /*** Four byte table, leaf: 8234a0xx - offset 0x07c18 ***/ + /*** Four byte table, leaf: 8234a0xx - offset 0x07c8c ***/ /* 30 */ 0xe4a6ab, 0xe4a6ac, 0xe4a6ad, 0xe4a6ae, /* 34 */ 0xe4a6af, 0xe4a6b0, 0xe4a6b1, 0xe4a6b2, /* 38 */ 0xe4a6b3, 0xe4a6b4, - /*** Four byte table, leaf: 8234a1xx - offset 0x07c22 ***/ + /*** Four byte table, leaf: 8234a1xx - offset 0x07c96 ***/ /* 30 */ 0xe4a6b5, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, /* 4 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8234e7xx - offset 0x07c28 ***/ + /*** Four byte table, leaf: 8234e7xx - offset 0x07c9c ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0xe4b1b8, 0xe4b1b9, 0xe4b1ba, 0xe4b1bb, /* 38 */ 0xe4b1bc, 0xe4b1bd, - /*** Four byte table, leaf: 8234e8xx - offset 0x07c32 ***/ + /*** Four byte table, leaf: 8234e8xx - offset 0x07ca6 ***/ /* 30 */ 0xe4b1be, 0xe4b1bf, 0xe4b280, 0xe4b281, /* 34 */ 0xe4b282, 0xe4b283, 0xe4b284, 0xe4b285, /* 38 */ 0xe4b286, 0xe4b287, - /*** Four byte table, leaf: 8234e9xx - offset 0x07c3c ***/ + /*** Four byte table, leaf: 8234e9xx - offset 0x07cb0 ***/ /* 30 */ 0xe4b288, 0xe4b289, 0xe4b28a, 0xe4b28b, /* 34 */ 0xe4b28c, 0xe4b28d, 0xe4b28e, 0xe4b28f, /* 38 */ 0xe4b290, 0xe4b291, - /*** Four byte table, leaf: 8234eaxx - offset 0x07c46 ***/ + /*** Four byte table, leaf: 8234eaxx - offset 0x07cba ***/ /* 30 */ 0xe4b292, 0xe4b293, 0xe4b294, 0xe4b295, /* 34 */ 0xe4b296, 0xe4b297, 0xe4b298, 0xe4b299, /* 38 */ 0xe4b29a, 0xe4b29b, - /*** Four byte table, leaf: 8234ebxx - offset 0x07c50 ***/ + /*** Four byte table, leaf: 8234ebxx - offset 0x07cc4 ***/ /* 30 */ 0xe4b29c, 0xe4b29d, 0xe4b29e, 0xe4b2a4, /* 34 */ 0xe4b2a5, 0xe4b2a6, 0xe4b2a7, 0xe4b2a8, /* 38 */ 0xe4b2a9, 0xe4b2aa, - /*** Four byte table, leaf: 8234ecxx - offset 0x07c5a ***/ + /*** Four byte table, leaf: 8234ecxx - offset 0x07cce ***/ /* 30 */ 0xe4b2ab, 0xe4b2ac, 0xe4b2ad, 0xe4b2ae, /* 34 */ 0xe4b2af, 0xe4b2b0, 0xe4b2b1, 0xe4b2b2, /* 38 */ 0xe4b2b3, 0xe4b2b4, - /*** Four byte table, leaf: 8234edxx - offset 0x07c64 ***/ + /*** Four byte table, leaf: 8234edxx - offset 0x07cd8 ***/ /* 30 */ 0xe4b2b5, 0xe4b2b6, 0xe4b2b7, 0xe4b2b8, /* 34 */ 0xe4b2b9, 0xe4b2ba, 0xe4b2bb, 0xe4b2bc, /* 38 */ 0xe4b2bd, 0xe4b2be, - /*** Four byte table, leaf: 8234eexx - offset 0x07c6e ***/ + /*** Four byte table, leaf: 8234eexx - offset 0x07ce2 ***/ /* 30 */ 0xe4b2bf, 0xe4b380, 0xe4b381, 0xe4b382, /* 34 */ 0xe4b383, 0xe4b384, 0xe4b385, 0xe4b386, /* 38 */ 0xe4b387, 0xe4b388, - /*** Four byte table, leaf: 8234efxx - offset 0x07c78 ***/ + /*** Four byte table, leaf: 8234efxx - offset 0x07cec ***/ /* 30 */ 0xe4b389, 0xe4b38a, 0xe4b38b, 0xe4b38c, /* 34 */ 0xe4b38d, 0xe4b38e, 0xe4b38f, 0xe4b390, /* 38 */ 0xe4b391, 0xe4b392, - /*** Four byte table, leaf: 8234f0xx - offset 0x07c82 ***/ + /*** Four byte table, leaf: 8234f0xx - offset 0x07cf6 ***/ /* 30 */ 0xe4b393, 0xe4b394, 0xe4b395, 0xe4b396, /* 34 */ 0xe4b397, 0xe4b398, 0xe4b399, 0xe4b39a, /* 38 */ 0xe4b39b, 0xe4b39c, - /*** Four byte table, leaf: 8234f1xx - offset 0x07c8c ***/ + /*** Four byte table, leaf: 8234f1xx - offset 0x07d00 ***/ /* 30 */ 0xe4b39d, 0xe4b39e, 0xe4b39f, 0xe4b3a0, /* 34 */ 0xe4b3a1, 0xe4b3a2, 0xe4b3a3, 0xe4b3a4, /* 38 */ 0xe4b3a5, 0xe4b3a6, - /*** Four byte table, leaf: 8234f2xx - offset 0x07c96 ***/ + /*** Four byte table, leaf: 8234f2xx - offset 0x07d0a ***/ /* 30 */ 0xe4b3a7, 0xe4b3a8, 0xe4b3a9, 0xe4b3aa, /* 34 */ 0xe4b3ab, 0xe4b3ac, 0xe4b3ad, 0xe4b3ae, /* 38 */ 0xe4b3af, 0xe4b3b0, - /*** Four byte table, leaf: 8234f3xx - offset 0x07ca0 ***/ + /*** Four byte table, leaf: 8234f3xx - offset 0x07d14 ***/ /* 30 */ 0xe4b3b1, 0xe4b3b2, 0xe4b3b3, 0xe4b3b4, /* 34 */ 0xe4b3b5, 0xe4b3b6, 0xe4b3b7, 0xe4b3b8, /* 38 */ 0xe4b3b9, 0xe4b3ba, - /*** Four byte table, leaf: 8234f4xx - offset 0x07caa ***/ + /*** Four byte table, leaf: 8234f4xx - offset 0x07d1e ***/ /* 30 */ 0xe4b3bb, 0xe4b3bc, 0xe4b3bd, 0xe4b3be, /* 34 */ 0xe4b3bf, 0xe4b480, 0xe4b481, 0xe4b482, /* 38 */ 0xe4b483, 0xe4b484, - /*** Four byte table, leaf: 8234f5xx - offset 0x07cb4 ***/ + /*** Four byte table, leaf: 8234f5xx - offset 0x07d28 ***/ /* 30 */ 0xe4b485, 0xe4b486, 0xe4b487, 0xe4b488, /* 34 */ 0xe4b489, 0xe4b48a, 0xe4b48b, 0xe4b48c, /* 38 */ 0xe4b48d, 0xe4b48e, - /*** Four byte table, leaf: 8234f6xx - offset 0x07cbe ***/ + /*** Four byte table, leaf: 8234f6xx - offset 0x07d32 ***/ /* 30 */ 0xe4b48f, 0xe4b490, 0xe4b491, 0xe4b492, /* 34 */ 0xe4b49a, 0xe4b49b, 0xe4b49c, 0xe4b49d, /* 38 */ 0xe4b49e, 0xe4b49f, - /*** Four byte table, leaf: 8234f7xx - offset 0x07cc8 ***/ + /*** Four byte table, leaf: 8234f7xx - offset 0x07d3c ***/ /* 30 */ 0xe4b4a0, 0xe4b4a1, 0xe4b4a2, 0xe4b4a3, /* 34 */ 0xe4b4a4, 0xe4b4a5, 0xe4b4a6, 0xe4b4a7, /* 38 */ 0xe4b4a8, 0xe4b4a9, - /*** Four byte table, leaf: 8234f8xx - offset 0x07cd2 ***/ + /*** Four byte table, leaf: 8234f8xx - offset 0x07d46 ***/ /* 30 */ 0xe4b4aa, 0xe4b4ab, 0xe4b4ac, 0xe4b4ad, /* 34 */ 0xe4b4ae, 0xe4b4af, 0xe4b4b0, 0xe4b4b1, /* 38 */ 0xe4b4b2, 0xe4b4b3, - /*** Four byte table, leaf: 8234f9xx - offset 0x07cdc ***/ + /*** Four byte table, leaf: 8234f9xx - offset 0x07d50 ***/ /* 30 */ 0xe4b4b4, 0xe4b4b5, 0xe4b4b6, 0xe4b4b7, /* 34 */ 0xe4b4b8, 0xe4b4b9, 0xe4b4ba, 0xe4b4bb, /* 38 */ 0xe4b4bc, 0xe4b4bd, - /*** Four byte table, leaf: 8234faxx - offset 0x07ce6 ***/ + /*** Four byte table, leaf: 8234faxx - offset 0x07d5a ***/ /* 30 */ 0xe4b4be, 0xe4b4bf, 0xe4b580, 0xe4b581, /* 34 */ 0xe4b582, 0xe4b583, 0xe4b584, 0xe4b585, /* 38 */ 0xe4b586, 0xe4b587, - /*** Four byte table, leaf: 8234fbxx - offset 0x07cf0 ***/ + /*** Four byte table, leaf: 8234fbxx - offset 0x07d64 ***/ /* 30 */ 0xe4b588, 0xe4b589, 0xe4b58a, 0xe4b58b, /* 34 */ 0xe4b58c, 0xe4b58d, 0xe4b58e, 0xe4b58f, /* 38 */ 0xe4b590, 0xe4b591, - /*** Four byte table, leaf: 8234fcxx - offset 0x07cfa ***/ + /*** Four byte table, leaf: 8234fcxx - offset 0x07d6e ***/ /* 30 */ 0xe4b592, 0xe4b593, 0xe4b594, 0xe4b595, /* 34 */ 0xe4b596, 0xe4b597, 0xe4b598, 0xe4b599, /* 38 */ 0xe4b59a, 0xe4b59b, - /*** Four byte table, leaf: 8234fdxx - offset 0x07d04 ***/ + /*** Four byte table, leaf: 8234fdxx - offset 0x07d78 ***/ /* 30 */ 0xe4b59c, 0xe4b59d, 0xe4b59e, 0xe4b59f, /* 34 */ 0xe4b5a0, 0xe4b5a1, 0xe4b5a2, 0xe4b5a3, /* 38 */ 0xe4b5a4, 0xe4b5a5, - /*** Four byte table, leaf: 8234fexx - offset 0x07d0e ***/ + /*** Four byte table, leaf: 8234fexx - offset 0x07d82 ***/ /* 30 */ 0xe4b5a6, 0xe4b5a7, 0xe4b5a8, 0xe4b5a9, /* 34 */ 0xe4b5aa, 0xe4b5ab, 0xe4b5ac, 0xe4b5ad, /* 38 */ 0xe4b5ae, 0xe4b5af, - /*** Four byte table, leaf: 823581xx - offset 0x07d18 ***/ + /*** Four byte table, leaf: 823581xx - offset 0x07d8c ***/ /* 30 */ 0xe4b5b0, 0xe4b5b1, 0xe4b5b2, 0xe4b5b3, /* 34 */ 0xe4b5b4, 0xe4b5b5, 0xe4b5b6, 0xe4b5b7, /* 38 */ 0xe4b5b8, 0xe4b5b9, - /*** Four byte table, leaf: 823582xx - offset 0x07d22 ***/ + /*** Four byte table, leaf: 823582xx - offset 0x07d96 ***/ /* 30 */ 0xe4b5ba, 0xe4b5bb, 0xe4b5bc, 0xe4b5bd, /* 34 */ 0xe4b5be, 0xe4b5bf, 0xe4b680, 0xe4b681, /* 38 */ 0xe4b682, 0xe4b683, - /*** Four byte table, leaf: 823583xx - offset 0x07d2c ***/ + /*** Four byte table, leaf: 823583xx - offset 0x07da0 ***/ /* 30 */ 0xe4b684, 0xe4b685, 0xe4b686, 0xe4b687, /* 34 */ 0xe4b688, 0xe4b689, 0xe4b68a, 0xe4b68b, /* 38 */ 0xe4b68c, 0xe4b68d, - /*** Four byte table, leaf: 823584xx - offset 0x07d36 ***/ + /*** Four byte table, leaf: 823584xx - offset 0x07daa ***/ /* 30 */ 0xe4b68e, 0xe4b68f, 0xe4b690, 0xe4b691, /* 34 */ 0xe4b692, 0xe4b693, 0xe4b694, 0xe4b695, /* 38 */ 0xe4b696, 0xe4b697, - /*** Four byte table, leaf: 823585xx - offset 0x07d40 ***/ + /*** Four byte table, leaf: 823585xx - offset 0x07db4 ***/ /* 30 */ 0xe4b698, 0xe4b699, 0xe4b69a, 0xe4b69b, /* 34 */ 0xe4b69c, 0xe4b69d, 0xe4b69e, 0xe4b69f, /* 38 */ 0xe4b6a0, 0xe4b6a1, - /*** Four byte table, leaf: 823586xx - offset 0x07d4a ***/ + /*** Four byte table, leaf: 823586xx - offset 0x07dbe ***/ /* 30 */ 0xe4b6a2, 0xe4b6a3, 0xe4b6a4, 0xe4b6a5, /* 34 */ 0xe4b6a6, 0xe4b6a7, 0xe4b6a8, 0xe4b6a9, /* 38 */ 0xe4b6aa, 0xe4b6ab, - /*** Four byte table, leaf: 823587xx - offset 0x07d54 ***/ + /*** Four byte table, leaf: 823587xx - offset 0x07dc8 ***/ /* 30 */ 0xe4b6ac, 0xe4b6ad, 0xe4b6af, 0xe4b6b0, /* 34 */ 0xe4b6b1, 0xe4b6b2, 0xe4b6b3, 0xe4b6b4, /* 38 */ 0xe4b6b5, 0xe4b6b6, - /*** Four byte table, leaf: 823588xx - offset 0x07d5e ***/ + /*** Four byte table, leaf: 823588xx - offset 0x07dd2 ***/ /* 30 */ 0xe4b6b7, 0xe4b6b8, 0xe4b6b9, 0xe4b6ba, /* 34 */ 0xe4b6bb, 0xe4b6bc, 0xe4b6bd, 0xe4b6be, /* 38 */ 0xe4b6bf, 0xe4b780, - /*** Four byte table, leaf: 823589xx - offset 0x07d68 ***/ + /*** Four byte table, leaf: 823589xx - offset 0x07ddc ***/ /* 30 */ 0xe4b781, 0xe4b782, 0xe4b783, 0xe4b784, /* 34 */ 0xe4b785, 0xe4b786, 0xe4b787, 0xe4b788, /* 38 */ 0xe4b789, 0xe4b78a, - /*** Four byte table, leaf: 82358axx - offset 0x07d72 ***/ + /*** Four byte table, leaf: 82358axx - offset 0x07de6 ***/ /* 30 */ 0xe4b78b, 0xe4b78c, 0xe4b78d, 0xe4b78e, /* 34 */ 0xe4b78f, 0xe4b790, 0xe4b791, 0xe4b792, /* 38 */ 0xe4b793, 0xe4b794, - /*** Four byte table, leaf: 82358bxx - offset 0x07d7c ***/ + /*** Four byte table, leaf: 82358bxx - offset 0x07df0 ***/ /* 30 */ 0xe4b795, 0xe4b796, 0xe4b797, 0xe4b798, /* 34 */ 0xe4b799, 0xe4b79a, 0xe4b79b, 0xe4b79c, /* 38 */ 0xe4b79d, 0xe4b79e, - /*** Four byte table, leaf: 82358cxx - offset 0x07d86 ***/ + /*** Four byte table, leaf: 82358cxx - offset 0x07dfa ***/ /* 30 */ 0xe4b79f, 0xe4b7a0, 0xe4b7a1, 0xe4b7a2, /* 34 */ 0xe4b7a3, 0xe4b7a4, 0xe4b7a5, 0xe4b7a6, /* 38 */ 0xe4b7a7, 0xe4b7a8, - /*** Four byte table, leaf: 82358dxx - offset 0x07d90 ***/ + /*** Four byte table, leaf: 82358dxx - offset 0x07e04 ***/ /* 30 */ 0xe4b7a9, 0xe4b7aa, 0xe4b7ab, 0xe4b7ac, /* 34 */ 0xe4b7ad, 0xe4b7ae, 0xe4b7af, 0xe4b7b0, /* 38 */ 0xe4b7b1, 0xe4b7b2, - /*** Four byte table, leaf: 82358exx - offset 0x07d9a ***/ + /*** Four byte table, leaf: 82358exx - offset 0x07e0e ***/ /* 30 */ 0xe4b7b3, 0xe4b7b4, 0xe4b7b5, 0xe4b7b6, /* 34 */ 0xe4b7b7, 0xe4b7b8, 0xe4b7b9, 0xe4b7ba, /* 38 */ 0xe4b7bb, 0xe4b7bc, - /*** Four byte table, leaf: 82358fxx - offset 0x07da4 ***/ + /*** Four byte table, leaf: 82358fxx - offset 0x07e18 ***/ /* 30 */ 0xe4b7bd, 0xe4b7be, 0xe4b7bf, /* 7 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 8336c7xx - offset 0x07da7 ***/ + /*** Four byte table, leaf: 8336c7xx - offset 0x07e1b ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0x000000, 0xee9dac, - /*** Four byte table, leaf: 8336c8xx - offset 0x07db1 ***/ + /*** Four byte table, leaf: 8336c8xx - offset 0x07e25 ***/ /* 30 */ 0xee9f88, 0xee9fa7, 0xee9fa8, 0xee9fa9, /* 34 */ 0xee9faa, 0xee9fab, 0xee9fac, 0xee9fad, /* 38 */ 0xee9fae, 0xee9faf, - /*** Four byte table, leaf: 8336c9xx - offset 0x07dbb ***/ + /*** Four byte table, leaf: 8336c9xx - offset 0x07e2f ***/ /* 30 */ 0xee9fb0, 0xee9fb1, 0xee9fb2, 0xee9fb3, /* 34 */ 0xeea095, 0xeea099, 0xeea09a, 0xeea09b, /* 38 */ 0xeea09c, 0xeea09d, - /*** Four byte table, leaf: 8336caxx - offset 0x07dc5 ***/ + /*** Four byte table, leaf: 8336caxx - offset 0x07e39 ***/ /* 30 */ 0xeea09f, 0xeea0a0, 0xeea0a1, 0xeea0a2, /* 34 */ 0xeea0a3, 0xeea0a4, 0xeea0a5, 0xeea0a7, /* 38 */ 0xeea0a8, 0xeea0a9, - /*** Four byte table, leaf: 8336cbxx - offset 0x07dcf ***/ + /*** Four byte table, leaf: 8336cbxx - offset 0x07e43 ***/ /* 30 */ 0xeea0aa, 0xeea0ad, 0xeea0ae, 0xeea0af, /* 34 */ 0xeea0b0, 0xeea0b3, 0xeea0b4, 0xeea0b5, /* 38 */ 0xeea0b6, 0xeea0b7, - /*** Four byte table, leaf: 8336ccxx - offset 0x07dd9 ***/ + /*** Four byte table, leaf: 8336ccxx - offset 0x07e4d ***/ /* 30 */ 0xeea0b8, 0xeea0b9, 0xeea0ba, 0xeea0bc, /* 34 */ 0xeea0bd, 0xeea0be, 0xeea0bf, 0xeea180, /* 38 */ 0xeea181, 0xeea182, - /*** Four byte table, leaf: 8336cdxx - offset 0x07de3 ***/ + /*** Four byte table, leaf: 8336cdxx - offset 0x07e57 ***/ /* 30 */ 0xeea184, 0xeea185, 0xeea186, 0xeea187, /* 34 */ 0xeea188, 0xeea189, 0xeea18a, 0xeea18b, /* 38 */ 0xeea18c, 0xeea18d, - /*** Four byte table, leaf: 8336cexx - offset 0x07ded ***/ + /*** Four byte table, leaf: 8336cexx - offset 0x07e61 ***/ /* 30 */ 0xeea18e, 0xeea18f, 0xeea190, 0xeea191, /* 34 */ 0xeea192, 0xeea193, 0xeea196, 0xeea197, /* 38 */ 0xeea198, 0xeea199, - /*** Four byte table, leaf: 8336cfxx - offset 0x07df7 ***/ + /*** Four byte table, leaf: 8336cfxx - offset 0x07e6b ***/ /* 30 */ 0xeea19a, 0xeea19b, 0xeea19c, 0xeea19d, /* 34 */ 0xeea19e, 0xeea19f, 0xeea1a0, 0xeea1a1, /* 38 */ 0xeea1a2, 0xeea1a3, - /*** Four byte table, leaf: 843085xx - offset 0x07e01 ***/ + /*** Four byte table, leaf: 843085xx - offset 0x07e75 ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0xefa4ad, 0xefa4ae, 0xefa4af, /* 38 */ 0xefa4b0, 0xefa4b1, - /*** Four byte table, leaf: 843086xx - offset 0x07e0b ***/ + /*** Four byte table, leaf: 843086xx - offset 0x07e7f ***/ /* 30 */ 0xefa4b2, 0xefa4b3, 0xefa4b4, 0xefa4b5, /* 34 */ 0xefa4b6, 0xefa4b7, 0xefa4b8, 0xefa4b9, /* 38 */ 0xefa4ba, 0xefa4bb, - /*** Four byte table, leaf: 843087xx - offset 0x07e15 ***/ + /*** Four byte table, leaf: 843087xx - offset 0x07e89 ***/ /* 30 */ 0xefa4bc, 0xefa4bd, 0xefa4be, 0xefa4bf, /* 34 */ 0xefa580, 0xefa581, 0xefa582, 0xefa583, /* 38 */ 0xefa584, 0xefa585, - /*** Four byte table, leaf: 843088xx - offset 0x07e1f ***/ + /*** Four byte table, leaf: 843088xx - offset 0x07e93 ***/ /* 30 */ 0xefa586, 0xefa587, 0xefa588, 0xefa589, /* 34 */ 0xefa58a, 0xefa58b, 0xefa58c, 0xefa58d, /* 38 */ 0xefa58e, 0xefa58f, - /*** Four byte table, leaf: 843089xx - offset 0x07e29 ***/ + /*** Four byte table, leaf: 843089xx - offset 0x07e9d ***/ /* 30 */ 0xefa590, 0xefa591, 0xefa592, 0xefa593, /* 34 */ 0xefa594, 0xefa595, 0xefa596, 0xefa597, /* 38 */ 0xefa598, 0xefa599, - /*** Four byte table, leaf: 84308axx - offset 0x07e33 ***/ + /*** Four byte table, leaf: 84308axx - offset 0x07ea7 ***/ /* 30 */ 0xefa59a, 0xefa59b, 0xefa59c, 0xefa59d, /* 34 */ 0xefa59e, 0xefa59f, 0xefa5a0, 0xefa5a1, /* 38 */ 0xefa5a2, 0xefa5a3, - /*** Four byte table, leaf: 84308bxx - offset 0x07e3d ***/ + /*** Four byte table, leaf: 84308bxx - offset 0x07eb1 ***/ /* 30 */ 0xefa5a4, 0xefa5a5, 0xefa5a6, 0xefa5a7, /* 34 */ 0xefa5a8, 0xefa5a9, 0xefa5aa, 0xefa5ab, /* 38 */ 0xefa5ac, 0xefa5ad, - /*** Four byte table, leaf: 84308cxx - offset 0x07e47 ***/ + /*** Four byte table, leaf: 84308cxx - offset 0x07ebb ***/ /* 30 */ 0xefa5ae, 0xefa5af, 0xefa5b0, 0xefa5b1, /* 34 */ 0xefa5b2, 0xefa5b3, 0xefa5b4, 0xefa5b5, /* 38 */ 0xefa5b6, 0xefa5b7, - /*** Four byte table, leaf: 84308dxx - offset 0x07e51 ***/ + /*** Four byte table, leaf: 84308dxx - offset 0x07ec5 ***/ /* 30 */ 0xefa5b8, 0xefa5ba, 0xefa5bb, 0xefa5bc, /* 34 */ 0xefa5bd, 0xefa5be, 0xefa5bf, 0xefa680, /* 38 */ 0xefa681, 0xefa682, - /*** Four byte table, leaf: 84308exx - offset 0x07e5b ***/ + /*** Four byte table, leaf: 84308exx - offset 0x07ecf ***/ /* 30 */ 0xefa683, 0xefa684, 0xefa685, 0xefa686, /* 34 */ 0xefa687, 0xefa688, 0xefa689, 0xefa68a, /* 38 */ 0xefa68b, 0xefa68c, - /*** Four byte table, leaf: 84308fxx - offset 0x07e65 ***/ + /*** Four byte table, leaf: 84308fxx - offset 0x07ed9 ***/ /* 30 */ 0xefa68d, 0xefa68e, 0xefa68f, 0xefa690, /* 34 */ 0xefa691, 0xefa692, 0xefa693, 0xefa694, /* 38 */ 0xefa696, 0xefa697, - /*** Four byte table, leaf: 843090xx - offset 0x07e6f ***/ + /*** Four byte table, leaf: 843090xx - offset 0x07ee3 ***/ /* 30 */ 0xefa698, 0xefa699, 0xefa69a, 0xefa69b, /* 34 */ 0xefa69c, 0xefa69d, 0xefa69e, 0xefa69f, /* 38 */ 0xefa6a0, 0xefa6a1, - /*** Four byte table, leaf: 843091xx - offset 0x07e79 ***/ + /*** Four byte table, leaf: 843091xx - offset 0x07eed ***/ /* 30 */ 0xefa6a2, 0xefa6a3, 0xefa6a4, 0xefa6a5, /* 34 */ 0xefa6a6, 0xefa6a7, 0xefa6a8, 0xefa6a9, /* 38 */ 0xefa6aa, 0xefa6ab, - /*** Four byte table, leaf: 843092xx - offset 0x07e83 ***/ + /*** Four byte table, leaf: 843092xx - offset 0x07ef7 ***/ /* 30 */ 0xefa6ac, 0xefa6ad, 0xefa6ae, 0xefa6af, /* 34 */ 0xefa6b0, 0xefa6b1, 0xefa6b2, 0xefa6b3, /* 38 */ 0xefa6b4, 0xefa6b5, - /*** Four byte table, leaf: 843093xx - offset 0x07e8d ***/ + /*** Four byte table, leaf: 843093xx - offset 0x07f01 ***/ /* 30 */ 0xefa6b6, 0xefa6b7, 0xefa6b8, 0xefa6b9, /* 34 */ 0xefa6ba, 0xefa6bb, 0xefa6bc, 0xefa6bd, /* 38 */ 0xefa6be, 0xefa6bf, - /*** Four byte table, leaf: 843094xx - offset 0x07e97 ***/ + /*** Four byte table, leaf: 843094xx - offset 0x07f0b ***/ /* 30 */ 0xefa780, 0xefa781, 0xefa782, 0xefa783, /* 34 */ 0xefa784, 0xefa785, 0xefa786, 0xefa787, /* 38 */ 0xefa788, 0xefa789, - /*** Four byte table, leaf: 843095xx - offset 0x07ea1 ***/ + /*** Four byte table, leaf: 843095xx - offset 0x07f15 ***/ /* 30 */ 0xefa78a, 0xefa78b, 0xefa78c, 0xefa78d, /* 34 */ 0xefa78e, 0xefa78f, 0xefa790, 0xefa791, /* 38 */ 0xefa792, 0xefa793, - /*** Four byte table, leaf: 843096xx - offset 0x07eab ***/ + /*** Four byte table, leaf: 843096xx - offset 0x07f1f ***/ /* 30 */ 0xefa794, 0xefa795, 0xefa796, 0xefa797, /* 34 */ 0xefa798, 0xefa799, 0xefa79a, 0xefa79b, /* 38 */ 0xefa79c, 0xefa79d, - /*** Four byte table, leaf: 843097xx - offset 0x07eb5 ***/ + /*** Four byte table, leaf: 843097xx - offset 0x07f29 ***/ /* 30 */ 0xefa79e, 0xefa79f, 0xefa7a0, 0xefa7a1, /* 34 */ 0xefa7a2, 0xefa7a3, 0xefa7a4, 0xefa7a5, /* 38 */ 0xefa7a6, 0xefa7a8, - /*** Four byte table, leaf: 843098xx - offset 0x07ebf ***/ + /*** Four byte table, leaf: 843098xx - offset 0x07f33 ***/ /* 30 */ 0xefa7a9, 0xefa7aa, 0xefa7ab, 0xefa7ac, /* 34 */ 0xefa7ad, 0xefa7ae, 0xefa7af, 0xefa7b0, /* 38 */ 0xefa7b2, 0xefa7b3, - /*** Four byte table, leaf: 843099xx - offset 0x07ec9 ***/ + /*** Four byte table, leaf: 843099xx - offset 0x07f3d ***/ /* 30 */ 0xefa7b4, 0xefa7b5, 0xefa7b6, 0xefa7b7, /* 34 */ 0xefa7b8, 0xefa7b9, 0xefa7ba, 0xefa7bb, /* 38 */ 0xefa7bc, 0xefa7bd, - /*** Four byte table, leaf: 84309axx - offset 0x07ed3 ***/ + /*** Four byte table, leaf: 84309axx - offset 0x07f47 ***/ /* 30 */ 0xefa7be, 0xefa7bf, 0xefa880, 0xefa881, /* 34 */ 0xefa882, 0xefa883, 0xefa884, 0xefa885, /* 38 */ 0xefa886, 0xefa887, - /*** Four byte table, leaf: 84309bxx - offset 0x07edd ***/ + /*** Four byte table, leaf: 84309bxx - offset 0x07f51 ***/ /* 30 */ 0xefa888, 0xefa889, 0xefa88a, 0xefa88b, /* 34 */ 0xefa890, 0xefa892, 0xefa895, 0xefa896, /* 38 */ 0xefa897, 0xefa899, - /*** Four byte table, leaf: 84309cxx - offset 0x07ee7 ***/ + /*** Four byte table, leaf: 84309cxx - offset 0x07f5b ***/ /* 30 */ 0xefa89a, 0xefa89b, 0xefa89c, 0xefa89d, /* 34 */ 0xefa89e, 0xefa8a2, 0xefa8a5, 0xefa8a6, /* 2 trailing zero values shared with next segment */ - /*** Four byte table, leaf: 843185xx - offset 0x07eef ***/ + /*** Four byte table, leaf: 843185xx - offset 0x07f63 ***/ /* 30 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, /* 38 */ 0xefb8b2, 0xefb985, - /*** Four byte table, leaf: 843186xx - offset 0x07ef9 ***/ + /*** Four byte table, leaf: 843186xx - offset 0x07f6d ***/ /* 30 */ 0xefb986, 0xefb987, 0xefb988, 0xefb993, /* 34 */ 0xefb998, 0xefb9a7, 0xefb9ac, 0xefb9ad, /* 38 */ 0xefb9ae, 0xefb9af, - /*** Four byte table, leaf: 843187xx - offset 0x07f03 ***/ + /*** Four byte table, leaf: 843187xx - offset 0x07f77 ***/ /* 30 */ 0xefb9b0, 0xefb9b1, 0xefb9b2, 0xefb9b3, /* 34 */ 0xefb9b4, 0xefb9b5, 0xefb9b6, 0xefb9b7, /* 38 */ 0xefb9b8, 0xefb9b9, - /*** Four byte table, leaf: 843188xx - offset 0x07f0d ***/ + /*** Four byte table, leaf: 843188xx - offset 0x07f81 ***/ /* 30 */ 0xefb9ba, 0xefb9bb, 0xefb9bc, 0xefb9bd, /* 34 */ 0xefb9be, 0xefb9bf, 0xefba80, 0xefba81, /* 38 */ 0xefba82, 0xefba83, - /*** Four byte table, leaf: 843189xx - offset 0x07f17 ***/ + /*** Four byte table, leaf: 843189xx - offset 0x07f8b ***/ /* 30 */ 0xefba84, 0xefba85, 0xefba86, 0xefba87, /* 34 */ 0xefba88, 0xefba89, 0xefba8a, 0xefba8b, /* 38 */ 0xefba8c, 0xefba8d, - /*** Four byte table, leaf: 84318axx - offset 0x07f21 ***/ + /*** Four byte table, leaf: 84318axx - offset 0x07f95 ***/ /* 30 */ 0xefba8e, 0xefba8f, 0xefba90, 0xefba91, /* 34 */ 0xefba92, 0xefba93, 0xefba94, 0xefba95, /* 38 */ 0xefba96, 0xefba97, - /*** Four byte table, leaf: 84318bxx - offset 0x07f2b ***/ + /*** Four byte table, leaf: 84318bxx - offset 0x07f9f ***/ /* 30 */ 0xefba98, 0xefba99, 0xefba9a, 0xefba9b, /* 34 */ 0xefba9c, 0xefba9d, 0xefba9e, 0xefba9f, /* 38 */ 0xefbaa0, 0xefbaa1, - /*** Four byte table, leaf: 84318cxx - offset 0x07f35 ***/ + /*** Four byte table, leaf: 84318cxx - offset 0x07fa9 ***/ /* 30 */ 0xefbaa2, 0xefbaa3, 0xefbaa4, 0xefbaa5, /* 34 */ 0xefbaa6, 0xefbaa7, 0xefbaa8, 0xefbaa9, /* 38 */ 0xefbaaa, 0xefbaab, - /*** Four byte table, leaf: 84318dxx - offset 0x07f3f ***/ + /*** Four byte table, leaf: 84318dxx - offset 0x07fb3 ***/ /* 30 */ 0xefbaac, 0xefbaad, 0xefbaae, 0xefbaaf, /* 34 */ 0xefbab0, 0xefbab1, 0xefbab2, 0xefbab3, /* 38 */ 0xefbab4, 0xefbab5, - /*** Four byte table, leaf: 84318exx - offset 0x07f49 ***/ + /*** Four byte table, leaf: 84318exx - offset 0x07fbd ***/ /* 30 */ 0xefbab6, 0xefbab7, 0xefbab8, 0xefbab9, /* 34 */ 0xefbaba, 0xefbabb, 0xefbabc, 0xefbabd, /* 38 */ 0xefbabe, 0xefbabf, - /*** Four byte table, leaf: 84318fxx - offset 0x07f53 ***/ + /*** Four byte table, leaf: 84318fxx - offset 0x07fc7 ***/ /* 30 */ 0xefbb80, 0xefbb81, 0xefbb82, 0xefbb83, /* 34 */ 0xefbb84, 0xefbb85, 0xefbb86, 0xefbb87, /* 38 */ 0xefbb88, 0xefbb89, - /*** Four byte table, leaf: 843190xx - offset 0x07f5d ***/ + /*** Four byte table, leaf: 843190xx - offset 0x07fd1 ***/ /* 30 */ 0xefbb8a, 0xefbb8b, 0xefbb8c, 0xefbb8d, /* 34 */ 0xefbb8e, 0xefbb8f, 0xefbb90, 0xefbb91, /* 38 */ 0xefbb92, 0xefbb93, - /*** Four byte table, leaf: 843191xx - offset 0x07f67 ***/ + /*** Four byte table, leaf: 843191xx - offset 0x07fdb ***/ /* 30 */ 0xefbb94, 0xefbb95, 0xefbb96, 0xefbb97, /* 34 */ 0xefbb98, 0xefbb99, 0xefbb9a, 0xefbb9b, /* 38 */ 0xefbb9c, 0xefbb9d, - /*** Four byte table, leaf: 843192xx - offset 0x07f71 ***/ + /*** Four byte table, leaf: 843192xx - offset 0x07fe5 ***/ /* 30 */ 0xefbb9e, 0xefbb9f, 0xefbba0, 0xefbba1, /* 34 */ 0xefbba2, 0xefbba3, 0xefbba4, 0xefbba5, /* 38 */ 0xefbba6, 0xefbba7, - /*** Four byte table, leaf: 843193xx - offset 0x07f7b ***/ + /*** Four byte table, leaf: 843193xx - offset 0x07fef ***/ /* 30 */ 0xefbba8, 0xefbba9, 0xefbbaa, 0xefbbab, /* 34 */ 0xefbbac, 0xefbbad, 0xefbbae, 0xefbbaf, /* 38 */ 0xefbbb0, 0xefbbb1, - /*** Four byte table, leaf: 843194xx - offset 0x07f85 ***/ + /*** Four byte table, leaf: 843194xx - offset 0x07ff9 ***/ /* 30 */ 0xefbbb2, 0xefbbb3, 0xefbbb4, 0xefbbb5, /* 34 */ 0xefbbb6, 0xefbbb7, 0xefbbb8, 0xefbbb9, /* 38 */ 0xefbbba, 0xefbbbb, - /*** Four byte table, leaf: 843195xx - offset 0x07f8f ***/ + /*** Four byte table, leaf: 843195xx - offset 0x08003 ***/ /* 30 */ 0xefbbbc, 0xefbbbd, 0xefbbbe, 0xefbbbf, /* 34 */ 0xefbc80, 0xefbd9f, 0xefbda0, 0xefbda1, /* 38 */ 0xefbda2, 0xefbda3, - /*** Four byte table, leaf: 843196xx - offset 0x07f99 ***/ + /*** Four byte table, leaf: 843196xx - offset 0x0800d ***/ /* 30 */ 0xefbda4, 0xefbda5, 0xefbda6, 0xefbda7, /* 34 */ 0xefbda8, 0xefbda9, 0xefbdaa, 0xefbdab, /* 38 */ 0xefbdac, 0xefbdad, - /*** Four byte table, leaf: 843197xx - offset 0x07fa3 ***/ + /*** Four byte table, leaf: 843197xx - offset 0x08017 ***/ /* 30 */ 0xefbdae, 0xefbdaf, 0xefbdb0, 0xefbdb1, /* 34 */ 0xefbdb2, 0xefbdb3, 0xefbdb4, 0xefbdb5, /* 38 */ 0xefbdb6, 0xefbdb7, - /*** Four byte table, leaf: 843198xx - offset 0x07fad ***/ + /*** Four byte table, leaf: 843198xx - offset 0x08021 ***/ /* 30 */ 0xefbdb8, 0xefbdb9, 0xefbdba, 0xefbdbb, /* 34 */ 0xefbdbc, 0xefbdbd, 0xefbdbe, 0xefbdbf, /* 38 */ 0xefbe80, 0xefbe81, - /*** Four byte table, leaf: 843199xx - offset 0x07fb7 ***/ + /*** Four byte table, leaf: 843199xx - offset 0x0802b ***/ /* 30 */ 0xefbe82, 0xefbe83, 0xefbe84, 0xefbe85, /* 34 */ 0xefbe86, 0xefbe87, 0xefbe88, 0xefbe89, /* 38 */ 0xefbe8a, 0xefbe8b, - /*** Four byte table, leaf: 84319axx - offset 0x07fc1 ***/ + /*** Four byte table, leaf: 84319axx - offset 0x08035 ***/ /* 30 */ 0xefbe8c, 0xefbe8d, 0xefbe8e, 0xefbe8f, /* 34 */ 0xefbe90, 0xefbe91, 0xefbe92, 0xefbe93, /* 38 */ 0xefbe94, 0xefbe95, - /*** Four byte table, leaf: 84319bxx - offset 0x07fcb ***/ + /*** Four byte table, leaf: 84319bxx - offset 0x0803f ***/ /* 30 */ 0xefbe96, 0xefbe97, 0xefbe98, 0xefbe99, /* 34 */ 0xefbe9a, 0xefbe9b, 0xefbe9c, 0xefbe9d, /* 38 */ 0xefbe9e, 0xefbe9f, - /*** Four byte table, leaf: 84319cxx - offset 0x07fd5 ***/ + /*** Four byte table, leaf: 84319cxx - offset 0x08049 ***/ /* 30 */ 0xefbea0, 0xefbea1, 0xefbea2, 0xefbea3, /* 34 */ 0xefbea4, 0xefbea5, 0xefbea6, 0xefbea7, /* 38 */ 0xefbea8, 0xefbea9, - /*** Four byte table, leaf: 84319dxx - offset 0x07fdf ***/ + /*** Four byte table, leaf: 84319dxx - offset 0x08053 ***/ /* 30 */ 0xefbeaa, 0xefbeab, 0xefbeac, 0xefbead, /* 34 */ 0xefbeae, 0xefbeaf, 0xefbeb0, 0xefbeb1, /* 38 */ 0xefbeb2, 0xefbeb3, - /*** Four byte table, leaf: 84319exx - offset 0x07fe9 ***/ + /*** Four byte table, leaf: 84319exx - offset 0x0805d ***/ /* 30 */ 0xefbeb4, 0xefbeb5, 0xefbeb6, 0xefbeb7, /* 34 */ 0xefbeb8, 0xefbeb9, 0xefbeba, 0xefbebb, /* 38 */ 0xefbebc, 0xefbebd, - /*** Four byte table, leaf: 84319fxx - offset 0x07ff3 ***/ + /*** Four byte table, leaf: 84319fxx - offset 0x08067 ***/ /* 30 */ 0xefbebe, 0xefbebf, 0xefbf80, 0xefbf81, /* 34 */ 0xefbf82, 0xefbf83, 0xefbf84, 0xefbf85, /* 38 */ 0xefbf86, 0xefbf87, - /*** Four byte table, leaf: 8431a0xx - offset 0x07ffd ***/ + /*** Four byte table, leaf: 8431a0xx - offset 0x08071 ***/ /* 30 */ 0xefbf88, 0xefbf89, 0xefbf8a, 0xefbf8b, /* 34 */ 0xefbf8c, 0xefbf8d, 0xefbf8e, 0xefbf8f, /* 38 */ 0xefbf90, 0xefbf91, - /*** Four byte table, leaf: 8431a1xx - offset 0x08007 ***/ + /*** Four byte table, leaf: 8431a1xx - offset 0x0807b ***/ /* 30 */ 0xefbf92, 0xefbf93, 0xefbf94, 0xefbf95, /* 34 */ 0xefbf96, 0xefbf97, 0xefbf98, 0xefbf99, /* 38 */ 0xefbf9a, 0xefbf9b, - /*** Four byte table, leaf: 8431a2xx - offset 0x08011 ***/ + /*** Four byte table, leaf: 8431a2xx - offset 0x08085 ***/ /* 30 */ 0xefbf9c, 0xefbf9d, 0xefbf9e, 0xefbf9f, /* 34 */ 0x000000, 0x000000, 0x000000, 0x000000, diff --git a/src/backend/utils/mb/Unicode/utf8_to_gb18030.map b/src/backend/utils/mb/Unicode/utf8_to_gb18030.map index 6c6b660bebe87..0b76e72b5832d 100644 --- a/src/backend/utils/mb/Unicode/utf8_to_gb18030.map +++ b/src/backend/utils/mb/Unicode/utf8_to_gb18030.map @@ -1,7 +1,7 @@ /* src/backend/utils/mb/Unicode/utf8_to_gb18030.map */ /* This file is generated by src/backend/utils/mb/Unicode/UCS_to_GB18030.pl */ -static const uint32 gb18030_from_unicode_tree_table[31972]; +static const uint32 gb18030_from_unicode_tree_table[32106]; static const pg_mb_radix_tree gb18030_from_unicode_tree = { @@ -19,7 +19,7 @@ static const pg_mb_radix_tree gb18030_from_unicode_tree = 0xbf, /* b2_2_upper */ 0x0450, /* offset of table for 3-byte inputs */ - 0xe2, /* b3_1_lower */ + 0xe1, /* b3_1_lower */ 0xef, /* b3_1_upper */ 0x80, /* b3_2_lower */ 0xbf, /* b3_2_upper */ @@ -37,7 +37,7 @@ static const pg_mb_radix_tree gb18030_from_unicode_tree = 0x00 /* b4_4_upper */ }; -static const uint32 gb18030_from_unicode_tree_table[31972] = +static const uint32 gb18030_from_unicode_tree_table[32106] = { /*** Dummy map, for invalid values - offset 0x00000 ***/ @@ -371,20 +371,20 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /*** Three byte table, byte #1: xx - offset 0x00450 ***/ - /* e2 */ 0x0000045e, 0x0000049e, 0x000004dd, 0x0000051d, - /* e6 */ 0x0000055d, 0x0000059d, 0x000005dd, 0x0000061d, - /* ea */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* ee */ 0x0000065d, 0x0000067f, + /* e1 */ 0x0000045f, 0x0000049f, 0x000004df, 0x0000051e, + /* e5 */ 0x0000055e, 0x0000059e, 0x000005de, 0x0000061e, + /* e9 */ 0x0000065e, 0x00000000, 0x00000000, 0x00000000, + /* ed */ 0x00000000, 0x0000069e, 0x000006c0, - /*** Three byte table, byte #2: e2xx - offset 0x0045e ***/ + /*** Three byte table, byte #2: e1xx - offset 0x0045f ***/ - /* 80 */ 0x000006bf, 0x000006ff, 0x0000073f, 0x0000077f, - /* 84 */ 0x000007bf, 0x000007ff, 0x0000083f, 0x0000087f, - /* 88 */ 0x000008bf, 0x000008ff, 0x0000093f, 0x0000097f, - /* 8c */ 0x000009bf, 0x000009ff, 0x00000a3f, 0x00000a7f, - /* 90 */ 0x00000abf, 0x00000aff, 0x00000b3f, 0x00000b7f, - /* 94 */ 0x00000bbf, 0x00000bff, 0x00000c3f, 0x00000c7f, - /* 98 */ 0x00000cbf, 0x00000cff, 0x00000000, 0x00000000, + /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 88 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 8c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 90 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 94 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 98 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 9c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* a4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -392,157 +392,195 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* b0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* b4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* b8 */ 0x00000000, 0x00000000, 0x00000d3e, 0x00000d7e, - /* bc */ 0x00000dbe, 0x00000dfe, 0x00000e3e, 0x00000e7e, - - /*** Three byte table, byte #2: e3xx - offset 0x0049e ***/ - - /* 80 */ 0x00000ebe, 0x00000efe, 0x00000f3e, 0x00000f7e, - /* 84 */ 0x00000fbe, 0x00000ffe, 0x0000103e, 0x0000107e, - /* 88 */ 0x000010be, 0x000010fe, 0x0000113e, 0x0000117e, - /* 8c */ 0x000011be, 0x000011fe, 0x0000123e, 0x0000127e, - /* 90 */ 0x000012be, 0x000012fe, 0x0000133e, 0x0000137e, - /* 94 */ 0x000013be, 0x000013fe, 0x0000143e, 0x0000147e, - /* 98 */ 0x000014be, 0x00000000, 0x00000000, 0x00000000, + /* b8 */ 0x00000700, 0x00000000, 0x00000000, 0x00000000, + /* bc */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + + /*** Three byte table, byte #2: e2xx - offset 0x0049f ***/ + + /* 80 */ 0x00000740, 0x00000780, 0x000007c0, 0x00000800, + /* 84 */ 0x00000840, 0x00000880, 0x000008c0, 0x00000900, + /* 88 */ 0x00000940, 0x00000980, 0x000009c0, 0x00000a00, + /* 8c */ 0x00000a40, 0x00000a80, 0x00000ac0, 0x00000b00, + /* 90 */ 0x00000b40, 0x00000b80, 0x00000bc0, 0x00000c00, + /* 94 */ 0x00000c40, 0x00000c80, 0x00000cc0, 0x00000d00, + /* 98 */ 0x00000d40, 0x00000d80, 0x00000000, 0x00000000, /* 9c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* a4 */ 0x000014e6, 0x00001526, 0x00001566, 0x000015a6, - /* a8 */ 0x000015e6, 0x00001626, 0x00001666, 0x000016a6, - /* ac */ 0x000016e6, 0x00001726, 0x00001766, 0x000017a6, - /* b0 */ 0x000017e6, 0x00001826, 0x00001866, 0x000018a6, + /* a4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* a8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b8 */ 0x00000000, 0x00000000, 0x00000dbf, 0x00000dff, + /* bc */ 0x00000e3f, 0x00000e7f, 0x00000ebf, 0x00000eff, + + /*** Three byte table, byte #2: e3xx - offset 0x004df ***/ + + /* 80 */ 0x00000f3f, 0x00000f7f, 0x00000fbf, 0x00000fff, + /* 84 */ 0x0000103f, 0x0000107f, 0x000010bf, 0x000010ff, + /* 88 */ 0x0000113f, 0x0000117f, 0x000011bf, 0x000011ff, + /* 8c */ 0x0000123f, 0x0000127f, 0x000012bf, 0x000012ff, + /* 90 */ 0x0000133f, 0x0000137f, 0x000013bf, 0x000013ff, + /* 94 */ 0x0000143f, 0x0000147f, 0x000014bf, 0x000014ff, + /* 98 */ 0x0000153f, 0x00000000, 0x00000000, 0x00000000, + /* 9c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* a4 */ 0x00001567, 0x000015a7, 0x000015e7, 0x00001627, + /* a8 */ 0x00001667, 0x000016a7, 0x000016e7, 0x00001727, + /* ac */ 0x00001767, 0x000017a7, 0x000017e7, 0x00001827, + /* b0 */ 0x00001867, 0x000018a7, 0x000018e7, 0x00001927, /* b4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* b8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* bc */ 0x00000000, 0x00000000, 0x00000000, /* 1 trailing zero values shared with next segment */ - /*** Three byte table, byte #2: e4xx - offset 0x004dd ***/ + /*** Three byte table, byte #2: e4xx - offset 0x0051e ***/ - /* 80 */ 0x00000000, 0x000018d0, 0x00001910, 0x00001950, - /* 84 */ 0x00001990, 0x000019d0, 0x00000000, 0x00000000, + /* 80 */ 0x00000000, 0x00001951, 0x00001991, 0x000019d1, + /* 84 */ 0x00001a11, 0x00001a51, 0x00000000, 0x00000000, /* 88 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* 8c */ 0x000019f0, 0x00001a30, 0x00001a70, 0x00001ab0, - /* 90 */ 0x00001af0, 0x00001b30, 0x00001b70, 0x00001bb0, + /* 8c */ 0x00001a71, 0x00001ab1, 0x00001af1, 0x00001b31, + /* 90 */ 0x00001b71, 0x00001bb1, 0x00001bf1, 0x00001c31, /* 94 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* 98 */ 0x00000000, 0x00001be4, 0x00001c24, 0x00001c64, - /* 9c */ 0x00001ca4, 0x00001ce4, 0x00001d24, 0x00000000, + /* 98 */ 0x00000000, 0x00001c65, 0x00001ca5, 0x00001ce5, + /* 9c */ 0x00001d25, 0x00001d65, 0x00001da5, 0x00000000, /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* a4 */ 0x00000000, 0x00001d5d, 0x00001d9d, 0x00000000, + /* a4 */ 0x00000000, 0x00001dde, 0x00001e1e, 0x00000000, /* a8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* b0 */ 0x00000000, 0x00001dd5, 0x00001e15, 0x00001e55, - /* b4 */ 0x00001e95, 0x00001ed5, 0x00001f15, 0x00001f55, - /* b8 */ 0x00001f95, 0x00001fd5, 0x00002015, 0x00002055, - /* bc */ 0x00002095, 0x000020d5, 0x00002115, 0x00002155, - - /*** Three byte table, byte #2: e5xx - offset 0x0051d ***/ - - /* 80 */ 0x00002195, 0x000021d5, 0x00002215, 0x00002255, - /* 84 */ 0x00002295, 0x000022d5, 0x00002315, 0x00002355, - /* 88 */ 0x00002395, 0x000023d5, 0x00002415, 0x00002455, - /* 8c */ 0x00002495, 0x000024d5, 0x00002515, 0x00002555, - /* 90 */ 0x00002595, 0x000025d5, 0x00002615, 0x00002655, - /* 94 */ 0x00002695, 0x000026d5, 0x00002715, 0x00002755, - /* 98 */ 0x00002795, 0x000027d5, 0x00002815, 0x00002855, - /* 9c */ 0x00002895, 0x000028d5, 0x00002915, 0x00002955, - /* a0 */ 0x00002995, 0x000029d5, 0x00002a15, 0x00002a55, - /* a4 */ 0x00002a95, 0x00002ad5, 0x00002b15, 0x00002b55, - /* a8 */ 0x00002b95, 0x00002bd5, 0x00002c15, 0x00002c55, - /* ac */ 0x00002c95, 0x00002cd5, 0x00002d15, 0x00002d55, - /* b0 */ 0x00002d95, 0x00002dd5, 0x00002e15, 0x00002e55, - /* b4 */ 0x00002e95, 0x00002ed5, 0x00002f15, 0x00002f55, - /* b8 */ 0x00002f95, 0x00002fd5, 0x00003015, 0x00003055, - /* bc */ 0x00003095, 0x000030d5, 0x00003115, 0x00003155, - - /*** Three byte table, byte #2: e6xx - offset 0x0055d ***/ - - /* 80 */ 0x00003195, 0x000031d5, 0x00003215, 0x00003255, - /* 84 */ 0x00003295, 0x000032d5, 0x00003315, 0x00003355, - /* 88 */ 0x00003395, 0x000033d5, 0x00003415, 0x00003455, - /* 8c */ 0x00003495, 0x000034d5, 0x00003515, 0x00003555, - /* 90 */ 0x00003595, 0x000035d5, 0x00003615, 0x00003655, - /* 94 */ 0x00003695, 0x000036d5, 0x00003715, 0x00003755, - /* 98 */ 0x00003795, 0x000037d5, 0x00003815, 0x00003855, - /* 9c */ 0x00003895, 0x000038d5, 0x00003915, 0x00003955, - /* a0 */ 0x00003995, 0x000039d5, 0x00003a15, 0x00003a55, - /* a4 */ 0x00003a95, 0x00003ad5, 0x00003b15, 0x00003b55, - /* a8 */ 0x00003b95, 0x00003bd5, 0x00003c15, 0x00003c55, - /* ac */ 0x00003c95, 0x00003cd5, 0x00003d15, 0x00003d55, - /* b0 */ 0x00003d95, 0x00003dd5, 0x00003e15, 0x00003e55, - /* b4 */ 0x00003e95, 0x00003ed5, 0x00003f15, 0x00003f55, - /* b8 */ 0x00003f95, 0x00003fd5, 0x00004015, 0x00004055, - /* bc */ 0x00004095, 0x000040d5, 0x00004115, 0x00004155, - - /*** Three byte table, byte #2: e7xx - offset 0x0059d ***/ - - /* 80 */ 0x00004195, 0x000041d5, 0x00004215, 0x00004255, - /* 84 */ 0x00004295, 0x000042d5, 0x00004315, 0x00004355, - /* 88 */ 0x00004395, 0x000043d5, 0x00004415, 0x00004455, - /* 8c */ 0x00004495, 0x000044d5, 0x00004515, 0x00004555, - /* 90 */ 0x00004595, 0x000045d5, 0x00004615, 0x00004655, - /* 94 */ 0x00004695, 0x000046d5, 0x00004715, 0x00004755, - /* 98 */ 0x00004795, 0x000047d5, 0x00004815, 0x00004855, - /* 9c */ 0x00004895, 0x000048d5, 0x00004915, 0x00004955, - /* a0 */ 0x00004995, 0x000049d5, 0x00004a15, 0x00004a55, - /* a4 */ 0x00004a95, 0x00004ad5, 0x00004b15, 0x00004b55, - /* a8 */ 0x00004b95, 0x00004bd5, 0x00004c15, 0x00004c55, - /* ac */ 0x00004c95, 0x00004cd5, 0x00004d15, 0x00004d55, - /* b0 */ 0x00004d95, 0x00004dd5, 0x00004e15, 0x00004e55, - /* b4 */ 0x00004e95, 0x00004ed5, 0x00004f15, 0x00004f55, - /* b8 */ 0x00004f95, 0x00004fd5, 0x00005015, 0x00005055, - /* bc */ 0x00005095, 0x000050d5, 0x00005115, 0x00005155, - - /*** Three byte table, byte #2: e8xx - offset 0x005dd ***/ - - /* 80 */ 0x00005195, 0x000051d5, 0x00005215, 0x00005255, - /* 84 */ 0x00005295, 0x000052d5, 0x00005315, 0x00005355, - /* 88 */ 0x00005395, 0x000053d5, 0x00005415, 0x00005455, - /* 8c */ 0x00005495, 0x000054d5, 0x00005515, 0x00005555, - /* 90 */ 0x00005595, 0x000055d5, 0x00005615, 0x00005655, - /* 94 */ 0x00005695, 0x000056d5, 0x00005715, 0x00005755, - /* 98 */ 0x00005795, 0x000057d5, 0x00005815, 0x00005855, - /* 9c */ 0x00005895, 0x000058d5, 0x00005915, 0x00005955, - /* a0 */ 0x00005995, 0x000059d5, 0x00005a15, 0x00005a55, - /* a4 */ 0x00005a95, 0x00005ad5, 0x00005b15, 0x00005b55, - /* a8 */ 0x00005b95, 0x00005bd5, 0x00005c15, 0x00005c55, - /* ac */ 0x00005c95, 0x00005cd5, 0x00005d15, 0x00005d55, - /* b0 */ 0x00005d95, 0x00005dd5, 0x00005e15, 0x00005e55, - /* b4 */ 0x00005e95, 0x00005ed5, 0x00005f15, 0x00005f55, - /* b8 */ 0x00005f95, 0x00005fd5, 0x00006015, 0x00006055, - /* bc */ 0x00006095, 0x000060d5, 0x00006115, 0x00006155, - - /*** Three byte table, byte #2: e9xx - offset 0x0061d ***/ - - /* 80 */ 0x00006195, 0x000061d5, 0x00006215, 0x00006255, - /* 84 */ 0x00006295, 0x000062d5, 0x00006315, 0x00006355, - /* 88 */ 0x00006395, 0x000063d5, 0x00006415, 0x00006455, - /* 8c */ 0x00006495, 0x000064d5, 0x00006515, 0x00006555, - /* 90 */ 0x00006595, 0x000065d5, 0x00006615, 0x00006655, - /* 94 */ 0x00006695, 0x000066d5, 0x00006715, 0x00006755, - /* 98 */ 0x00006795, 0x000067d5, 0x00006815, 0x00006855, - /* 9c */ 0x00006895, 0x000068d5, 0x00006915, 0x00006955, - /* a0 */ 0x00006995, 0x000069d5, 0x00006a15, 0x00006a55, - /* a4 */ 0x00006a95, 0x00006ad5, 0x00006b15, 0x00006b55, - /* a8 */ 0x00006b95, 0x00006bd5, 0x00006c15, 0x00006c55, - /* ac */ 0x00006c95, 0x00006cd5, 0x00006d15, 0x00006d55, - /* b0 */ 0x00006d95, 0x00006dd5, 0x00006e15, 0x00006e55, - /* b4 */ 0x00006e95, 0x00006ed5, 0x00006f15, 0x00006f55, - /* b8 */ 0x00006f95, 0x00006fd5, 0x00007015, 0x00007055, - /* bc */ 0x00007095, 0x000070d5, 0x00007115, 0x00000000, - - /*** Three byte table, byte #2: eexx - offset 0x0065d ***/ - - /* 80 */ 0x00007155, 0x00007195, 0x000071d5, 0x00007215, - /* 84 */ 0x00007255, 0x00007295, 0x000072d5, 0x00007315, - /* 88 */ 0x00007355, 0x00007395, 0x000073d5, 0x00007415, - /* 8c */ 0x00007455, 0x00007495, 0x000074d5, 0x00007515, - /* 90 */ 0x00007555, 0x00007595, 0x000075d5, 0x00007615, - /* 94 */ 0x00007655, 0x00007695, 0x000076d5, 0x00007715, - /* 98 */ 0x00007755, 0x00007795, 0x000077d5, 0x00007815, - /* 9c */ 0x00007855, 0x00007895, 0x000078d5, 0x00007915, - /* a0 */ 0x00007955, 0x00007995, + /* b0 */ 0x00000000, 0x00001e56, 0x00001e96, 0x00001ed6, + /* b4 */ 0x00001f16, 0x00001f56, 0x00001f96, 0x00001fd6, + /* b8 */ 0x00002016, 0x00002056, 0x00002096, 0x000020d6, + /* bc */ 0x00002116, 0x00002156, 0x00002196, 0x000021d6, + + /*** Three byte table, byte #2: e5xx - offset 0x0055e ***/ + + /* 80 */ 0x00002216, 0x00002256, 0x00002296, 0x000022d6, + /* 84 */ 0x00002316, 0x00002356, 0x00002396, 0x000023d6, + /* 88 */ 0x00002416, 0x00002456, 0x00002496, 0x000024d6, + /* 8c */ 0x00002516, 0x00002556, 0x00002596, 0x000025d6, + /* 90 */ 0x00002616, 0x00002656, 0x00002696, 0x000026d6, + /* 94 */ 0x00002716, 0x00002756, 0x00002796, 0x000027d6, + /* 98 */ 0x00002816, 0x00002856, 0x00002896, 0x000028d6, + /* 9c */ 0x00002916, 0x00002956, 0x00002996, 0x000029d6, + /* a0 */ 0x00002a16, 0x00002a56, 0x00002a96, 0x00002ad6, + /* a4 */ 0x00002b16, 0x00002b56, 0x00002b96, 0x00002bd6, + /* a8 */ 0x00002c16, 0x00002c56, 0x00002c96, 0x00002cd6, + /* ac */ 0x00002d16, 0x00002d56, 0x00002d96, 0x00002dd6, + /* b0 */ 0x00002e16, 0x00002e56, 0x00002e96, 0x00002ed6, + /* b4 */ 0x00002f16, 0x00002f56, 0x00002f96, 0x00002fd6, + /* b8 */ 0x00003016, 0x00003056, 0x00003096, 0x000030d6, + /* bc */ 0x00003116, 0x00003156, 0x00003196, 0x000031d6, + + /*** Three byte table, byte #2: e6xx - offset 0x0059e ***/ + + /* 80 */ 0x00003216, 0x00003256, 0x00003296, 0x000032d6, + /* 84 */ 0x00003316, 0x00003356, 0x00003396, 0x000033d6, + /* 88 */ 0x00003416, 0x00003456, 0x00003496, 0x000034d6, + /* 8c */ 0x00003516, 0x00003556, 0x00003596, 0x000035d6, + /* 90 */ 0x00003616, 0x00003656, 0x00003696, 0x000036d6, + /* 94 */ 0x00003716, 0x00003756, 0x00003796, 0x000037d6, + /* 98 */ 0x00003816, 0x00003856, 0x00003896, 0x000038d6, + /* 9c */ 0x00003916, 0x00003956, 0x00003996, 0x000039d6, + /* a0 */ 0x00003a16, 0x00003a56, 0x00003a96, 0x00003ad6, + /* a4 */ 0x00003b16, 0x00003b56, 0x00003b96, 0x00003bd6, + /* a8 */ 0x00003c16, 0x00003c56, 0x00003c96, 0x00003cd6, + /* ac */ 0x00003d16, 0x00003d56, 0x00003d96, 0x00003dd6, + /* b0 */ 0x00003e16, 0x00003e56, 0x00003e96, 0x00003ed6, + /* b4 */ 0x00003f16, 0x00003f56, 0x00003f96, 0x00003fd6, + /* b8 */ 0x00004016, 0x00004056, 0x00004096, 0x000040d6, + /* bc */ 0x00004116, 0x00004156, 0x00004196, 0x000041d6, + + /*** Three byte table, byte #2: e7xx - offset 0x005de ***/ + + /* 80 */ 0x00004216, 0x00004256, 0x00004296, 0x000042d6, + /* 84 */ 0x00004316, 0x00004356, 0x00004396, 0x000043d6, + /* 88 */ 0x00004416, 0x00004456, 0x00004496, 0x000044d6, + /* 8c */ 0x00004516, 0x00004556, 0x00004596, 0x000045d6, + /* 90 */ 0x00004616, 0x00004656, 0x00004696, 0x000046d6, + /* 94 */ 0x00004716, 0x00004756, 0x00004796, 0x000047d6, + /* 98 */ 0x00004816, 0x00004856, 0x00004896, 0x000048d6, + /* 9c */ 0x00004916, 0x00004956, 0x00004996, 0x000049d6, + /* a0 */ 0x00004a16, 0x00004a56, 0x00004a96, 0x00004ad6, + /* a4 */ 0x00004b16, 0x00004b56, 0x00004b96, 0x00004bd6, + /* a8 */ 0x00004c16, 0x00004c56, 0x00004c96, 0x00004cd6, + /* ac */ 0x00004d16, 0x00004d56, 0x00004d96, 0x00004dd6, + /* b0 */ 0x00004e16, 0x00004e56, 0x00004e96, 0x00004ed6, + /* b4 */ 0x00004f16, 0x00004f56, 0x00004f96, 0x00004fd6, + /* b8 */ 0x00005016, 0x00005056, 0x00005096, 0x000050d6, + /* bc */ 0x00005116, 0x00005156, 0x00005196, 0x000051d6, + + /*** Three byte table, byte #2: e8xx - offset 0x0061e ***/ + + /* 80 */ 0x00005216, 0x00005256, 0x00005296, 0x000052d6, + /* 84 */ 0x00005316, 0x00005356, 0x00005396, 0x000053d6, + /* 88 */ 0x00005416, 0x00005456, 0x00005496, 0x000054d6, + /* 8c */ 0x00005516, 0x00005556, 0x00005596, 0x000055d6, + /* 90 */ 0x00005616, 0x00005656, 0x00005696, 0x000056d6, + /* 94 */ 0x00005716, 0x00005756, 0x00005796, 0x000057d6, + /* 98 */ 0x00005816, 0x00005856, 0x00005896, 0x000058d6, + /* 9c */ 0x00005916, 0x00005956, 0x00005996, 0x000059d6, + /* a0 */ 0x00005a16, 0x00005a56, 0x00005a96, 0x00005ad6, + /* a4 */ 0x00005b16, 0x00005b56, 0x00005b96, 0x00005bd6, + /* a8 */ 0x00005c16, 0x00005c56, 0x00005c96, 0x00005cd6, + /* ac */ 0x00005d16, 0x00005d56, 0x00005d96, 0x00005dd6, + /* b0 */ 0x00005e16, 0x00005e56, 0x00005e96, 0x00005ed6, + /* b4 */ 0x00005f16, 0x00005f56, 0x00005f96, 0x00005fd6, + /* b8 */ 0x00006016, 0x00006056, 0x00006096, 0x000060d6, + /* bc */ 0x00006116, 0x00006156, 0x00006196, 0x000061d6, + + /*** Three byte table, byte #2: e9xx - offset 0x0065e ***/ + + /* 80 */ 0x00006216, 0x00006256, 0x00006296, 0x000062d6, + /* 84 */ 0x00006316, 0x00006356, 0x00006396, 0x000063d6, + /* 88 */ 0x00006416, 0x00006456, 0x00006496, 0x000064d6, + /* 8c */ 0x00006516, 0x00006556, 0x00006596, 0x000065d6, + /* 90 */ 0x00006616, 0x00006656, 0x00006696, 0x000066d6, + /* 94 */ 0x00006716, 0x00006756, 0x00006796, 0x000067d6, + /* 98 */ 0x00006816, 0x00006856, 0x00006896, 0x000068d6, + /* 9c */ 0x00006916, 0x00006956, 0x00006996, 0x000069d6, + /* a0 */ 0x00006a16, 0x00006a56, 0x00006a96, 0x00006ad6, + /* a4 */ 0x00006b16, 0x00006b56, 0x00006b96, 0x00006bd6, + /* a8 */ 0x00006c16, 0x00006c56, 0x00006c96, 0x00006cd6, + /* ac */ 0x00006d16, 0x00006d56, 0x00006d96, 0x00006dd6, + /* b0 */ 0x00006e16, 0x00006e56, 0x00006e96, 0x00006ed6, + /* b4 */ 0x00006f16, 0x00006f56, 0x00006f96, 0x00006fd6, + /* b8 */ 0x00007016, 0x00007056, 0x00007096, 0x000070d6, + /* bc */ 0x00007116, 0x00007156, 0x00007196, 0x00000000, + + /*** Three byte table, byte #2: eexx - offset 0x0069e ***/ + + /* 80 */ 0x000071d6, 0x00007216, 0x00007256, 0x00007296, + /* 84 */ 0x000072d6, 0x00007316, 0x00007356, 0x00007396, + /* 88 */ 0x000073d6, 0x00007416, 0x00007456, 0x00007496, + /* 8c */ 0x000074d6, 0x00007516, 0x00007556, 0x00007596, + /* 90 */ 0x000075d6, 0x00007616, 0x00007656, 0x00007696, + /* 94 */ 0x000076d6, 0x00007716, 0x00007756, 0x00007796, + /* 98 */ 0x000077d6, 0x00007816, 0x00007856, 0x00007896, + /* 9c */ 0x000078d6, 0x00007916, 0x00007956, 0x00007996, + /* a0 */ 0x000079d6, 0x00007a16, /* 30 trailing zero values shared with next segment */ - /*** Three byte table, byte #2: efxx - offset 0x0067f ***/ + /*** Three byte table, byte #2: efxx - offset 0x006c0 ***/ + + /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 88 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 8c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 90 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 94 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 98 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 9c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* a4 */ 0x00007a3a, 0x00007a7a, 0x00007aba, 0x00007afa, + /* a8 */ 0x00007b3a, 0x00000000, 0x00000000, 0x00000000, + /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b8 */ 0x00007b6a, 0x00007baa, 0x00007bea, 0x00007c2a, + /* bc */ 0x00007c6a, 0x00007caa, 0x00007cea, 0x00007d2a, + + /*** Three byte table, leaf: e1b8xx - offset 0x00700 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -553,15 +591,15 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* 98 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 9c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* a4 */ 0x000079ba, 0x000079fa, 0x00007a3a, 0x00007a7a, - /* a8 */ 0x00007aba, 0x00000000, 0x00000000, 0x00000000, + /* a4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* a8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* b0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* b4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* b8 */ 0x00007ae4, 0x00007b24, 0x00007b64, 0x00007ba4, - /* bc */ 0x00007be4, 0x00007c24, 0x00007c64, 0x00007ca4, + /* b8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* bc */ 0x00000000, 0x00000000, 0x00000000, 0x0000a8bc, - /*** Three byte table, leaf: e280xx - offset 0x006bf ***/ + /*** Three byte table, leaf: e280xx - offset 0x00740 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -580,7 +618,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136a737, 0x8136a738, 0x8136a739, 0x0000a1f9, /* bc */ 0x8136a830, 0x8136a831, 0x8136a832, 0x8136a833, - /*** Three byte table, leaf: e281xx - offset 0x006ff ***/ + /*** Three byte table, leaf: e281xx - offset 0x00780 ***/ /* 80 */ 0x8136a834, 0x8136a835, 0x8136a836, 0x8136a837, /* 84 */ 0x8136a838, 0x8136a839, 0x8136a930, 0x8136a931, @@ -599,7 +637,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136ae30, 0x8136ae31, 0x8136ae32, 0x8136ae33, /* bc */ 0x8136ae34, 0x8136ae35, 0x8136ae36, 0x8136ae37, - /*** Three byte table, leaf: e282xx - offset 0x0073f ***/ + /*** Three byte table, leaf: e282xx - offset 0x007c0 ***/ /* 80 */ 0x8136ae38, 0x8136ae39, 0x8136af30, 0x8136af31, /* 84 */ 0x8136af32, 0x8136af33, 0x8136af34, 0x8136af35, @@ -618,7 +656,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136b433, 0x8136b434, 0x8136b435, 0x8136b436, /* bc */ 0x8136b437, 0x8136b438, 0x8136b439, 0x8136b530, - /*** Three byte table, leaf: e283xx - offset 0x0077f ***/ + /*** Three byte table, leaf: e283xx - offset 0x00800 ***/ /* 80 */ 0x8136b531, 0x8136b532, 0x8136b533, 0x8136b534, /* 84 */ 0x8136b535, 0x8136b536, 0x8136b537, 0x8136b538, @@ -637,7 +675,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136ba37, 0x8136ba38, 0x8136ba39, 0x8136bb30, /* bc */ 0x8136bb31, 0x8136bb32, 0x8136bb33, 0x8136bb34, - /*** Three byte table, leaf: e284xx - offset 0x007bf ***/ + /*** Three byte table, leaf: e284xx - offset 0x00840 ***/ /* 80 */ 0x8136bb35, 0x8136bb36, 0x8136bb37, 0x0000a1e6, /* 84 */ 0x8136bb38, 0x0000a847, 0x8136bb39, 0x8136bc30, @@ -656,7 +694,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136c036, 0x8136c037, 0x8136c038, 0x8136c039, /* bc */ 0x8136c130, 0x8136c131, 0x8136c132, 0x8136c133, - /*** Three byte table, leaf: e285xx - offset 0x007ff ***/ + /*** Three byte table, leaf: e285xx - offset 0x00880 ***/ /* 80 */ 0x8136c134, 0x8136c135, 0x8136c136, 0x8136c137, /* 84 */ 0x8136c138, 0x8136c139, 0x8136c230, 0x8136c231, @@ -675,7 +713,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a2a9, 0x0000a2aa, 0x8136c530, 0x8136c531, /* bc */ 0x8136c532, 0x8136c533, 0x8136c534, 0x8136c535, - /*** Three byte table, leaf: e286xx - offset 0x0083f ***/ + /*** Three byte table, leaf: e286xx - offset 0x008c0 ***/ /* 80 */ 0x8136c536, 0x8136c537, 0x8136c538, 0x8136c539, /* 84 */ 0x8136c630, 0x8136c631, 0x8136c632, 0x8136c633, @@ -694,7 +732,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136ca34, 0x8136ca35, 0x8136ca36, 0x8136ca37, /* bc */ 0x8136ca38, 0x8136ca39, 0x8136cb30, 0x8136cb31, - /*** Three byte table, leaf: e287xx - offset 0x0087f ***/ + /*** Three byte table, leaf: e287xx - offset 0x00900 ***/ /* 80 */ 0x8136cb32, 0x8136cb33, 0x8136cb34, 0x8136cb35, /* 84 */ 0x8136cb36, 0x8136cb37, 0x8136cb38, 0x8136cb39, @@ -713,7 +751,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136d038, 0x8136d039, 0x8136d130, 0x8136d131, /* bc */ 0x8136d132, 0x8136d133, 0x8136d134, 0x8136d135, - /*** Three byte table, leaf: e288xx - offset 0x008bf ***/ + /*** Three byte table, leaf: e288xx - offset 0x00940 ***/ /* 80 */ 0x8136d136, 0x8136d137, 0x8136d138, 0x8136d139, /* 84 */ 0x8136d230, 0x8136d231, 0x8136d232, 0x8136d233, @@ -732,7 +770,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136d531, 0x8136d532, 0x8136d533, 0x8136d534, /* bc */ 0x8136d535, 0x0000a1d7, 0x8136d536, 0x8136d537, - /*** Three byte table, leaf: e289xx - offset 0x008ff ***/ + /*** Three byte table, leaf: e289xx - offset 0x00980 ***/ /* 80 */ 0x8136d538, 0x8136d539, 0x8136d630, 0x8136d631, /* 84 */ 0x8136d632, 0x8136d633, 0x8136d634, 0x8136d635, @@ -751,7 +789,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136da33, 0x8136da34, 0x8136da35, 0x8136da36, /* bc */ 0x8136da37, 0x8136da38, 0x8136da39, 0x8136db30, - /*** Three byte table, leaf: e28axx - offset 0x0093f ***/ + /*** Three byte table, leaf: e28axx - offset 0x009c0 ***/ /* 80 */ 0x8136db31, 0x8136db32, 0x8136db33, 0x8136db34, /* 84 */ 0x8136db35, 0x8136db36, 0x8136db37, 0x8136db38, @@ -770,7 +808,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136e034, 0x8136e035, 0x8136e036, 0x8136e037, /* bc */ 0x8136e038, 0x8136e039, 0x8136e130, 0x0000a853, - /*** Three byte table, leaf: e28bxx - offset 0x0097f ***/ + /*** Three byte table, leaf: e28bxx - offset 0x00a00 ***/ /* 80 */ 0x8136e131, 0x8136e132, 0x8136e133, 0x8136e134, /* 84 */ 0x8136e135, 0x8136e136, 0x8136e137, 0x8136e138, @@ -789,7 +827,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136e637, 0x8136e638, 0x8136e639, 0x8136e730, /* bc */ 0x8136e731, 0x8136e732, 0x8136e733, 0x8136e734, - /*** Three byte table, leaf: e28cxx - offset 0x009bf ***/ + /*** Three byte table, leaf: e28cxx - offset 0x00a40 ***/ /* 80 */ 0x8136e735, 0x8136e736, 0x8136e737, 0x8136e738, /* 84 */ 0x8136e739, 0x8136e830, 0x8136e831, 0x8136e832, @@ -808,7 +846,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136ed30, 0x8136ed31, 0x8136ed32, 0x8136ed33, /* bc */ 0x8136ed34, 0x8136ed35, 0x8136ed36, 0x8136ed37, - /*** Three byte table, leaf: e28dxx - offset 0x009ff ***/ + /*** Three byte table, leaf: e28dxx - offset 0x00a80 ***/ /* 80 */ 0x8136ed38, 0x8136ed39, 0x8136ee30, 0x8136ee31, /* 84 */ 0x8136ee32, 0x8136ee33, 0x8136ee34, 0x8136ee35, @@ -827,7 +865,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136f334, 0x8136f335, 0x8136f336, 0x8136f337, /* bc */ 0x8136f338, 0x8136f339, 0x8136f430, 0x8136f431, - /*** Three byte table, leaf: e28exx - offset 0x00a3f ***/ + /*** Three byte table, leaf: e28exx - offset 0x00ac0 ***/ /* 80 */ 0x8136f432, 0x8136f433, 0x8136f434, 0x8136f435, /* 84 */ 0x8136f436, 0x8136f437, 0x8136f438, 0x8136f439, @@ -846,7 +884,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8136f938, 0x8136f939, 0x8136fa30, 0x8136fa31, /* bc */ 0x8136fa32, 0x8136fa33, 0x8136fa34, 0x8136fa35, - /*** Three byte table, leaf: e28fxx - offset 0x00a7f ***/ + /*** Three byte table, leaf: e28fxx - offset 0x00b00 ***/ /* 80 */ 0x8136fa36, 0x8136fa37, 0x8136fa38, 0x8136fa39, /* 84 */ 0x8136fb30, 0x8136fb31, 0x8136fb32, 0x8136fb33, @@ -865,7 +903,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81378232, 0x81378233, 0x81378234, 0x81378235, /* bc */ 0x81378236, 0x81378237, 0x81378238, 0x81378239, - /*** Three byte table, leaf: e290xx - offset 0x00abf ***/ + /*** Three byte table, leaf: e290xx - offset 0x00b40 ***/ /* 80 */ 0x81378330, 0x81378331, 0x81378332, 0x81378333, /* 84 */ 0x81378334, 0x81378335, 0x81378336, 0x81378337, @@ -884,7 +922,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81378836, 0x81378837, 0x81378838, 0x81378839, /* bc */ 0x81378930, 0x81378931, 0x81378932, 0x81378933, - /*** Three byte table, leaf: e291xx - offset 0x00aff ***/ + /*** Three byte table, leaf: e291xx - offset 0x00b80 ***/ /* 80 */ 0x81378934, 0x81378935, 0x81378936, 0x81378937, /* 84 */ 0x81378938, 0x81378939, 0x81378a30, 0x81378a31, @@ -903,7 +941,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a2c9, 0x0000a2ca, 0x0000a2cb, 0x0000a2cc, /* bc */ 0x0000a2cd, 0x0000a2ce, 0x0000a2cf, 0x0000a2d0, - /*** Three byte table, leaf: e292xx - offset 0x00b3f ***/ + /*** Three byte table, leaf: e292xx - offset 0x00bc0 ***/ /* 80 */ 0x0000a2d1, 0x0000a2d2, 0x0000a2d3, 0x0000a2d4, /* 84 */ 0x0000a2d5, 0x0000a2d6, 0x0000a2d7, 0x0000a2d8, @@ -922,7 +960,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81379034, 0x81379035, 0x81379036, 0x81379037, /* bc */ 0x81379038, 0x81379039, 0x81379130, 0x81379131, - /*** Three byte table, leaf: e293xx - offset 0x00b7f ***/ + /*** Three byte table, leaf: e293xx - offset 0x00c00 ***/ /* 80 */ 0x81379132, 0x81379133, 0x81379134, 0x81379135, /* 84 */ 0x81379136, 0x81379137, 0x81379138, 0x81379139, @@ -941,7 +979,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81379638, 0x81379639, 0x81379730, 0x81379731, /* bc */ 0x81379732, 0x81379733, 0x81379734, 0x81379735, - /*** Three byte table, leaf: e294xx - offset 0x00bbf ***/ + /*** Three byte table, leaf: e294xx - offset 0x00c40 ***/ /* 80 */ 0x0000a9a4, 0x0000a9a5, 0x0000a9a6, 0x0000a9a7, /* 84 */ 0x0000a9a8, 0x0000a9a9, 0x0000a9aa, 0x0000a9ab, @@ -960,7 +998,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a9dc, 0x0000a9dd, 0x0000a9de, 0x0000a9df, /* bc */ 0x0000a9e0, 0x0000a9e1, 0x0000a9e2, 0x0000a9e3, - /*** Three byte table, leaf: e295xx - offset 0x00bff ***/ + /*** Three byte table, leaf: e295xx - offset 0x00c80 ***/ /* 80 */ 0x0000a9e4, 0x0000a9e5, 0x0000a9e6, 0x0000a9e7, /* 84 */ 0x0000a9e8, 0x0000a9e9, 0x0000a9ea, 0x0000a9eb, @@ -979,7 +1017,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81379834, 0x81379835, 0x81379836, 0x81379837, /* bc */ 0x81379838, 0x81379839, 0x81379930, 0x81379931, - /*** Three byte table, leaf: e296xx - offset 0x00c3f ***/ + /*** Three byte table, leaf: e296xx - offset 0x00cc0 ***/ /* 80 */ 0x81379932, 0x0000a878, 0x0000a879, 0x0000a87a, /* 84 */ 0x0000a87b, 0x0000a87c, 0x0000a87d, 0x0000a87e, @@ -998,7 +1036,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81379c36, 0x81379c37, 0x81379c38, 0x81379c39, /* bc */ 0x0000a88b, 0x0000a88c, 0x81379d30, 0x81379d31, - /*** Three byte table, leaf: e297xx - offset 0x00c7f ***/ + /*** Three byte table, leaf: e297xx - offset 0x00d00 ***/ /* 80 */ 0x81379d32, 0x81379d33, 0x81379d34, 0x81379d35, /* 84 */ 0x81379d36, 0x81379d37, 0x0000a1f4, 0x0000a1f3, @@ -1017,7 +1055,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8137a139, 0x8137a230, 0x8137a231, 0x8137a232, /* bc */ 0x8137a233, 0x8137a234, 0x8137a235, 0x8137a236, - /*** Three byte table, leaf: e298xx - offset 0x00cbf ***/ + /*** Three byte table, leaf: e298xx - offset 0x00d40 ***/ /* 80 */ 0x8137a237, 0x8137a238, 0x8137a239, 0x8137a330, /* 84 */ 0x8137a331, 0x0000a1ef, 0x0000a1ee, 0x8137a332, @@ -1036,7 +1074,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8137a830, 0x8137a831, 0x8137a832, 0x8137a833, /* bc */ 0x8137a834, 0x8137a835, 0x8137a836, 0x8137a837, - /*** Three byte table, leaf: e299xx - offset 0x00cff ***/ + /*** Three byte table, leaf: e299xx - offset 0x00d80 ***/ /* 80 */ 0x0000a1e2, 0x8137a838, 0x0000a1e1, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -1056,7 +1094,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* bc */ 0x00000000, 0x00000000, 0x00000000, /* 1 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e2baxx - offset 0x00d3e ***/ + /*** Three byte table, leaf: e2baxx - offset 0x00dbf ***/ /* 80 */ 0x00000000, 0x0000fe50, 0x8138fd39, 0x8138fe30, /* 84 */ 0x0000fe54, 0x8138fe31, 0x8138fe32, 0x8138fe33, @@ -1075,7 +1113,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81398432, 0x81398433, 0x81398434, 0x0000fe79, /* bc */ 0x81398435, 0x81398436, 0x81398437, 0x81398438, - /*** Three byte table, leaf: e2bbxx - offset 0x00d7e ***/ + /*** Three byte table, leaf: e2bbxx - offset 0x00dff ***/ /* 80 */ 0x81398439, 0x81398530, 0x81398531, 0x81398532, /* 84 */ 0x81398533, 0x81398534, 0x81398535, 0x81398536, @@ -1094,7 +1132,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81398a34, 0x81398a35, 0x81398a36, 0x81398a37, /* bc */ 0x81398a38, 0x81398a39, 0x81398b30, 0x81398b31, - /*** Three byte table, leaf: e2bcxx - offset 0x00dbe ***/ + /*** Three byte table, leaf: e2bcxx - offset 0x00e3f ***/ /* 80 */ 0x81398b32, 0x81398b33, 0x81398b34, 0x81398b35, /* 84 */ 0x81398b36, 0x81398b37, 0x81398b38, 0x81398b39, @@ -1113,7 +1151,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81399038, 0x81399039, 0x81399130, 0x81399131, /* bc */ 0x81399132, 0x81399133, 0x81399134, 0x81399135, - /*** Three byte table, leaf: e2bdxx - offset 0x00dfe ***/ + /*** Three byte table, leaf: e2bdxx - offset 0x00e7f ***/ /* 80 */ 0x81399136, 0x81399137, 0x81399138, 0x81399139, /* 84 */ 0x81399230, 0x81399231, 0x81399232, 0x81399233, @@ -1132,7 +1170,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81399732, 0x81399733, 0x81399734, 0x81399735, /* bc */ 0x81399736, 0x81399737, 0x81399738, 0x81399739, - /*** Three byte table, leaf: e2bexx - offset 0x00e3e ***/ + /*** Three byte table, leaf: e2bexx - offset 0x00ebf ***/ /* 80 */ 0x81399830, 0x81399831, 0x81399832, 0x81399833, /* 84 */ 0x81399834, 0x81399835, 0x81399836, 0x81399837, @@ -1151,7 +1189,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x81399d36, 0x81399d37, 0x81399d38, 0x81399d39, /* bc */ 0x81399e30, 0x81399e31, 0x81399e32, 0x81399e33, - /*** Three byte table, leaf: e2bfxx - offset 0x00e7e ***/ + /*** Three byte table, leaf: e2bfxx - offset 0x00eff ***/ /* 80 */ 0x81399e34, 0x81399e35, 0x81399e36, 0x81399e37, /* 84 */ 0x81399e38, 0x81399e39, 0x81399f30, 0x81399f31, @@ -1170,7 +1208,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a992, 0x0000a993, 0x0000a994, 0x0000a995, /* bc */ 0x8139a332, 0x8139a333, 0x8139a334, 0x8139a335, - /*** Three byte table, leaf: e380xx - offset 0x00ebe ***/ + /*** Three byte table, leaf: e380xx - offset 0x00f3f ***/ /* 80 */ 0x0000a1a1, 0x0000a1a2, 0x0000a1a3, 0x0000a1a8, /* 84 */ 0x8139a336, 0x0000a1a9, 0x0000a965, 0x0000a996, @@ -1189,7 +1227,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139a538, 0x8139a539, 0x8139a630, 0x8139a631, /* bc */ 0x8139a632, 0x8139a633, 0x0000a989, 0x8139a634, - /*** Three byte table, leaf: e381xx - offset 0x00efe ***/ + /*** Three byte table, leaf: e381xx - offset 0x00f7f ***/ /* 80 */ 0x8139a635, 0x0000a4a1, 0x0000a4a2, 0x0000a4a3, /* 84 */ 0x0000a4a4, 0x0000a4a5, 0x0000a4a6, 0x0000a4a7, @@ -1208,7 +1246,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a4d8, 0x0000a4d9, 0x0000a4da, 0x0000a4db, /* bc */ 0x0000a4dc, 0x0000a4dd, 0x0000a4de, 0x0000a4df, - /*** Three byte table, leaf: e382xx - offset 0x00f3e ***/ + /*** Three byte table, leaf: e382xx - offset 0x00fbf ***/ /* 80 */ 0x0000a4e0, 0x0000a4e1, 0x0000a4e2, 0x0000a4e3, /* 84 */ 0x0000a4e4, 0x0000a4e5, 0x0000a4e6, 0x0000a4e7, @@ -1227,7 +1265,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a5b8, 0x0000a5b9, 0x0000a5ba, 0x0000a5bb, /* bc */ 0x0000a5bc, 0x0000a5bd, 0x0000a5be, 0x0000a5bf, - /*** Three byte table, leaf: e383xx - offset 0x00f7e ***/ + /*** Three byte table, leaf: e383xx - offset 0x00fff ***/ /* 80 */ 0x0000a5c0, 0x0000a5c1, 0x0000a5c2, 0x0000a5c3, /* 84 */ 0x0000a5c4, 0x0000a5c5, 0x0000a5c6, 0x0000a5c7, @@ -1246,7 +1284,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139a736, 0x8139a737, 0x8139a738, 0x8139a739, /* bc */ 0x0000a960, 0x0000a963, 0x0000a964, 0x8139a830, - /*** Three byte table, leaf: e384xx - offset 0x00fbe ***/ + /*** Three byte table, leaf: e384xx - offset 0x0103f ***/ /* 80 */ 0x8139a831, 0x8139a832, 0x8139a833, 0x8139a834, /* 84 */ 0x8139a835, 0x0000a8c5, 0x0000a8c6, 0x0000a8c7, @@ -1265,7 +1303,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139aa30, 0x8139aa31, 0x8139aa32, 0x8139aa33, /* bc */ 0x8139aa34, 0x8139aa35, 0x8139aa36, 0x8139aa37, - /*** Three byte table, leaf: e385xx - offset 0x00ffe ***/ + /*** Three byte table, leaf: e385xx - offset 0x0107f ***/ /* 80 */ 0x8139aa38, 0x8139aa39, 0x8139ab30, 0x8139ab31, /* 84 */ 0x8139ab32, 0x8139ab33, 0x8139ab34, 0x8139ab35, @@ -1284,7 +1322,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139b034, 0x8139b035, 0x8139b036, 0x8139b037, /* bc */ 0x8139b038, 0x8139b039, 0x8139b130, 0x8139b131, - /*** Three byte table, leaf: e386xx - offset 0x0103e ***/ + /*** Three byte table, leaf: e386xx - offset 0x010bf ***/ /* 80 */ 0x8139b132, 0x8139b133, 0x8139b134, 0x8139b135, /* 84 */ 0x8139b136, 0x8139b137, 0x8139b138, 0x8139b139, @@ -1303,7 +1341,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139b638, 0x8139b639, 0x8139b730, 0x8139b731, /* bc */ 0x8139b732, 0x8139b733, 0x8139b734, 0x8139b735, - /*** Three byte table, leaf: e387xx - offset 0x0107e ***/ + /*** Three byte table, leaf: e387xx - offset 0x010ff ***/ /* 80 */ 0x8139b736, 0x8139b737, 0x8139b738, 0x8139b739, /* 84 */ 0x8139b830, 0x8139b831, 0x8139b832, 0x8139b833, @@ -1322,7 +1360,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139bd32, 0x8139bd33, 0x8139bd34, 0x8139bd35, /* bc */ 0x8139bd36, 0x8139bd37, 0x8139bd38, 0x8139bd39, - /*** Three byte table, leaf: e388xx - offset 0x010be ***/ + /*** Three byte table, leaf: e388xx - offset 0x0113f ***/ /* 80 */ 0x8139be30, 0x8139be31, 0x8139be32, 0x8139be33, /* 84 */ 0x8139be34, 0x8139be35, 0x8139be36, 0x8139be37, @@ -1341,7 +1379,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139c235, 0x8139c236, 0x8139c237, 0x8139c238, /* bc */ 0x8139c239, 0x8139c330, 0x8139c331, 0x8139c332, - /*** Three byte table, leaf: e389xx - offset 0x010fe ***/ + /*** Three byte table, leaf: e389xx - offset 0x0117f ***/ /* 80 */ 0x8139c333, 0x8139c334, 0x8139c335, 0x8139c336, /* 84 */ 0x8139c337, 0x8139c338, 0x8139c339, 0x8139c430, @@ -1360,7 +1398,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139c839, 0x8139c930, 0x8139c931, 0x8139c932, /* bc */ 0x8139c933, 0x8139c934, 0x8139c935, 0x8139c936, - /*** Three byte table, leaf: e38axx - offset 0x0113e ***/ + /*** Three byte table, leaf: e38axx - offset 0x011bf ***/ /* 80 */ 0x8139c937, 0x8139c938, 0x8139c939, 0x8139ca30, /* 84 */ 0x8139ca31, 0x8139ca32, 0x8139ca33, 0x8139ca34, @@ -1379,7 +1417,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139cf32, 0x8139cf33, 0x8139cf34, 0x8139cf35, /* bc */ 0x8139cf36, 0x8139cf37, 0x8139cf38, 0x8139cf39, - /*** Three byte table, leaf: e38bxx - offset 0x0117e ***/ + /*** Three byte table, leaf: e38bxx - offset 0x011ff ***/ /* 80 */ 0x8139d030, 0x8139d031, 0x8139d032, 0x8139d033, /* 84 */ 0x8139d034, 0x8139d035, 0x8139d036, 0x8139d037, @@ -1398,7 +1436,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139d536, 0x8139d537, 0x8139d538, 0x8139d539, /* bc */ 0x8139d630, 0x8139d631, 0x8139d632, 0x8139d633, - /*** Three byte table, leaf: e38cxx - offset 0x011be ***/ + /*** Three byte table, leaf: e38cxx - offset 0x0123f ***/ /* 80 */ 0x8139d634, 0x8139d635, 0x8139d636, 0x8139d637, /* 84 */ 0x8139d638, 0x8139d639, 0x8139d730, 0x8139d731, @@ -1417,7 +1455,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139dc30, 0x8139dc31, 0x8139dc32, 0x8139dc33, /* bc */ 0x8139dc34, 0x8139dc35, 0x8139dc36, 0x8139dc37, - /*** Three byte table, leaf: e38dxx - offset 0x011fe ***/ + /*** Three byte table, leaf: e38dxx - offset 0x0127f ***/ /* 80 */ 0x8139dc38, 0x8139dc39, 0x8139dd30, 0x8139dd31, /* 84 */ 0x8139dd32, 0x8139dd33, 0x8139dd34, 0x8139dd35, @@ -1436,7 +1474,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139e234, 0x8139e235, 0x8139e236, 0x8139e237, /* bc */ 0x8139e238, 0x8139e239, 0x8139e330, 0x8139e331, - /*** Three byte table, leaf: e38exx - offset 0x0123e ***/ + /*** Three byte table, leaf: e38exx - offset 0x012bf ***/ /* 80 */ 0x8139e332, 0x8139e333, 0x8139e334, 0x8139e335, /* 84 */ 0x8139e336, 0x8139e337, 0x8139e338, 0x8139e339, @@ -1455,7 +1493,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139e832, 0x8139e833, 0x8139e834, 0x8139e835, /* bc */ 0x8139e836, 0x8139e837, 0x8139e838, 0x8139e839, - /*** Three byte table, leaf: e38fxx - offset 0x0127e ***/ + /*** Three byte table, leaf: e38fxx - offset 0x012ff ***/ /* 80 */ 0x8139e930, 0x8139e931, 0x8139e932, 0x8139e933, /* 84 */ 0x0000a950, 0x8139e934, 0x8139e935, 0x8139e936, @@ -1474,7 +1512,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139ee31, 0x8139ee32, 0x8139ee33, 0x8139ee34, /* bc */ 0x8139ee35, 0x8139ee36, 0x8139ee37, 0x8139ee38, - /*** Three byte table, leaf: e390xx - offset 0x012be ***/ + /*** Three byte table, leaf: e390xx - offset 0x0133f ***/ /* 80 */ 0x8139ee39, 0x8139ef30, 0x8139ef31, 0x8139ef32, /* 84 */ 0x8139ef33, 0x8139ef34, 0x8139ef35, 0x8139ef36, @@ -1493,7 +1531,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139f435, 0x8139f436, 0x8139f437, 0x8139f438, /* bc */ 0x8139f439, 0x8139f530, 0x8139f531, 0x8139f532, - /*** Three byte table, leaf: e391xx - offset 0x012fe ***/ + /*** Three byte table, leaf: e391xx - offset 0x0137f ***/ /* 80 */ 0x8139f533, 0x8139f534, 0x8139f535, 0x8139f536, /* 84 */ 0x8139f537, 0x8139f538, 0x8139f539, 0x0000fe56, @@ -1512,7 +1550,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8139fa37, 0x8139fa38, 0x8139fa39, 0x8139fb30, /* bc */ 0x8139fb31, 0x8139fb32, 0x8139fb33, 0x8139fb34, - /*** Three byte table, leaf: e392xx - offset 0x0133e ***/ + /*** Three byte table, leaf: e392xx - offset 0x013bf ***/ /* 80 */ 0x8139fb35, 0x8139fb36, 0x8139fb37, 0x8139fb38, /* 84 */ 0x8139fb39, 0x8139fc30, 0x8139fc31, 0x8139fc32, @@ -1531,7 +1569,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82308331, 0x82308332, 0x82308333, 0x82308334, /* bc */ 0x82308335, 0x82308336, 0x82308337, 0x82308338, - /*** Three byte table, leaf: e393xx - offset 0x0137e ***/ + /*** Three byte table, leaf: e393xx - offset 0x013ff ***/ /* 80 */ 0x82308339, 0x82308430, 0x82308431, 0x82308432, /* 84 */ 0x82308433, 0x82308434, 0x82308435, 0x82308436, @@ -1550,7 +1588,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82308935, 0x82308936, 0x82308937, 0x82308938, /* bc */ 0x82308939, 0x82308a30, 0x82308a31, 0x82308a32, - /*** Three byte table, leaf: e394xx - offset 0x013be ***/ + /*** Three byte table, leaf: e394xx - offset 0x0143f ***/ /* 80 */ 0x82308a33, 0x82308a34, 0x82308a35, 0x82308a36, /* 84 */ 0x82308a37, 0x82308a38, 0x82308a39, 0x82308b30, @@ -1569,7 +1607,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82308f39, 0x82309030, 0x82309031, 0x82309032, /* bc */ 0x82309033, 0x82309034, 0x82309035, 0x82309036, - /*** Three byte table, leaf: e395xx - offset 0x013fe ***/ + /*** Three byte table, leaf: e395xx - offset 0x0147f ***/ /* 80 */ 0x82309037, 0x82309038, 0x82309039, 0x82309130, /* 84 */ 0x82309131, 0x82309132, 0x82309133, 0x82309134, @@ -1588,7 +1626,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82309633, 0x82309634, 0x82309635, 0x82309636, /* bc */ 0x82309637, 0x82309638, 0x82309639, 0x82309730, - /*** Three byte table, leaf: e396xx - offset 0x0143e ***/ + /*** Three byte table, leaf: e396xx - offset 0x014bf ***/ /* 80 */ 0x82309731, 0x82309732, 0x82309733, 0x82309734, /* 84 */ 0x82309735, 0x82309736, 0x82309737, 0x82309738, @@ -1607,7 +1645,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82309c36, 0x82309c37, 0x82309c38, 0x82309c39, /* bc */ 0x82309d30, 0x82309d31, 0x82309d32, 0x82309d33, - /*** Three byte table, leaf: e397xx - offset 0x0147e ***/ + /*** Three byte table, leaf: e397xx - offset 0x014ff ***/ /* 80 */ 0x82309d34, 0x82309d35, 0x82309d36, 0x82309d37, /* 84 */ 0x82309d38, 0x82309d39, 0x82309e30, 0x82309e31, @@ -1626,7 +1664,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8230a330, 0x8230a331, 0x8230a332, 0x8230a333, /* bc */ 0x8230a334, 0x8230a335, 0x8230a336, 0x8230a337, - /*** Three byte table, leaf: e398xx - offset 0x014be ***/ + /*** Three byte table, leaf: e398xx - offset 0x0153f ***/ /* 80 */ 0x8230a338, 0x8230a339, 0x8230a430, 0x8230a431, /* 84 */ 0x8230a432, 0x8230a433, 0x8230a434, 0x8230a435, @@ -1640,7 +1678,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* a4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 24 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e3a4xx - offset 0x014e6 ***/ + /*** Three byte table, leaf: e3a4xx - offset 0x01567 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -1659,7 +1697,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8230f539, 0x8230f630, 0x8230f631, 0x8230f632, /* bc */ 0x8230f633, 0x8230f634, 0x8230f635, 0x8230f636, - /*** Three byte table, leaf: e3a5xx - offset 0x01526 ***/ + /*** Three byte table, leaf: e3a5xx - offset 0x015a7 ***/ /* 80 */ 0x8230f637, 0x8230f638, 0x8230f639, 0x8230f730, /* 84 */ 0x8230f731, 0x8230f732, 0x8230f733, 0x8230f734, @@ -1678,7 +1716,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8230fc32, 0x8230fc33, 0x8230fc34, 0x8230fc35, /* bc */ 0x8230fc36, 0x8230fc37, 0x8230fc38, 0x8230fc39, - /*** Three byte table, leaf: e3a6xx - offset 0x01566 ***/ + /*** Three byte table, leaf: e3a6xx - offset 0x015e7 ***/ /* 80 */ 0x8230fd30, 0x8230fd31, 0x8230fd32, 0x8230fd33, /* 84 */ 0x8230fd34, 0x8230fd35, 0x8230fd36, 0x8230fd37, @@ -1697,7 +1735,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82318436, 0x82318437, 0x82318438, 0x82318439, /* bc */ 0x82318530, 0x82318531, 0x82318532, 0x82318533, - /*** Three byte table, leaf: e3a7xx - offset 0x015a6 ***/ + /*** Three byte table, leaf: e3a7xx - offset 0x01627 ***/ /* 80 */ 0x82318534, 0x82318535, 0x82318536, 0x82318537, /* 84 */ 0x82318538, 0x82318539, 0x82318630, 0x82318631, @@ -1716,7 +1754,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82318a37, 0x82318a38, 0x82318a39, 0x82318b30, /* bc */ 0x82318b31, 0x82318b32, 0x82318b33, 0x82318b34, - /*** Three byte table, leaf: e3a8xx - offset 0x015e6 ***/ + /*** Three byte table, leaf: e3a8xx - offset 0x01667 ***/ /* 80 */ 0x82318b35, 0x82318b36, 0x82318b37, 0x82318b38, /* 84 */ 0x82318b39, 0x82318c30, 0x82318c31, 0x82318c32, @@ -1735,7 +1773,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82319131, 0x82319132, 0x82319133, 0x82319134, /* bc */ 0x82319135, 0x82319136, 0x82319137, 0x82319138, - /*** Three byte table, leaf: e3a9xx - offset 0x01626 ***/ + /*** Three byte table, leaf: e3a9xx - offset 0x016a7 ***/ /* 80 */ 0x82319139, 0x82319230, 0x82319231, 0x82319232, /* 84 */ 0x82319233, 0x82319234, 0x82319235, 0x82319236, @@ -1754,7 +1792,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82319734, 0x82319735, 0x82319736, 0x82319737, /* bc */ 0x82319738, 0x82319739, 0x82319830, 0x82319831, - /*** Three byte table, leaf: e3aaxx - offset 0x01666 ***/ + /*** Three byte table, leaf: e3aaxx - offset 0x016e7 ***/ /* 80 */ 0x82319832, 0x82319833, 0x82319834, 0x82319835, /* 84 */ 0x82319836, 0x82319837, 0x82319838, 0x82319839, @@ -1773,7 +1811,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82319d38, 0x82319d39, 0x82319e30, 0x82319e31, /* bc */ 0x82319e32, 0x82319e33, 0x82319e34, 0x82319e35, - /*** Three byte table, leaf: e3abxx - offset 0x016a6 ***/ + /*** Three byte table, leaf: e3abxx - offset 0x01727 ***/ /* 80 */ 0x82319e36, 0x82319e37, 0x82319e38, 0x82319e39, /* 84 */ 0x82319f30, 0x82319f31, 0x82319f32, 0x82319f33, @@ -1792,7 +1830,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231a432, 0x8231a433, 0x8231a434, 0x8231a435, /* bc */ 0x8231a436, 0x8231a437, 0x8231a438, 0x8231a439, - /*** Three byte table, leaf: e3acxx - offset 0x016e6 ***/ + /*** Three byte table, leaf: e3acxx - offset 0x01767 ***/ /* 80 */ 0x8231a530, 0x8231a531, 0x8231a532, 0x8231a533, /* 84 */ 0x8231a534, 0x8231a535, 0x8231a536, 0x8231a537, @@ -1811,7 +1849,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231aa36, 0x8231aa37, 0x8231aa38, 0x8231aa39, /* bc */ 0x8231ab30, 0x8231ab31, 0x8231ab32, 0x8231ab33, - /*** Three byte table, leaf: e3adxx - offset 0x01726 ***/ + /*** Three byte table, leaf: e3adxx - offset 0x017a7 ***/ /* 80 */ 0x8231ab34, 0x8231ab35, 0x8231ab36, 0x8231ab37, /* 84 */ 0x8231ab38, 0x8231ab39, 0x8231ac30, 0x8231ac31, @@ -1830,7 +1868,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231b039, 0x8231b130, 0x8231b131, 0x8231b132, /* bc */ 0x8231b133, 0x8231b134, 0x8231b135, 0x8231b136, - /*** Three byte table, leaf: e3aexx - offset 0x01766 ***/ + /*** Three byte table, leaf: e3aexx - offset 0x017e7 ***/ /* 80 */ 0x8231b137, 0x8231b138, 0x8231b139, 0x8231b230, /* 84 */ 0x8231b231, 0x8231b232, 0x8231b233, 0x8231b234, @@ -1849,7 +1887,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231b733, 0x8231b734, 0x8231b735, 0x8231b736, /* bc */ 0x8231b737, 0x8231b738, 0x8231b739, 0x8231b830, - /*** Three byte table, leaf: e3afxx - offset 0x017a6 ***/ + /*** Three byte table, leaf: e3afxx - offset 0x01827 ***/ /* 80 */ 0x8231b831, 0x8231b832, 0x8231b833, 0x8231b834, /* 84 */ 0x8231b835, 0x8231b836, 0x8231b837, 0x8231b838, @@ -1868,7 +1906,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231bd37, 0x8231bd38, 0x8231bd39, 0x8231be30, /* bc */ 0x8231be31, 0x8231be32, 0x8231be33, 0x8231be34, - /*** Three byte table, leaf: e3b0xx - offset 0x017e6 ***/ + /*** Three byte table, leaf: e3b0xx - offset 0x01867 ***/ /* 80 */ 0x8231be35, 0x8231be36, 0x8231be37, 0x8231be38, /* 84 */ 0x8231be39, 0x8231bf30, 0x8231bf31, 0x8231bf32, @@ -1887,7 +1925,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231c431, 0x8231c432, 0x8231c433, 0x8231c434, /* bc */ 0x8231c435, 0x8231c436, 0x8231c437, 0x8231c438, - /*** Three byte table, leaf: e3b1xx - offset 0x01826 ***/ + /*** Three byte table, leaf: e3b1xx - offset 0x018a7 ***/ /* 80 */ 0x8231c439, 0x8231c530, 0x8231c531, 0x8231c532, /* 84 */ 0x8231c533, 0x8231c534, 0x8231c535, 0x8231c536, @@ -1906,7 +1944,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231ca34, 0x8231ca35, 0x8231ca36, 0x8231ca37, /* bc */ 0x8231ca38, 0x8231ca39, 0x8231cb30, 0x8231cb31, - /*** Three byte table, leaf: e3b2xx - offset 0x01866 ***/ + /*** Three byte table, leaf: e3b2xx - offset 0x018e7 ***/ /* 80 */ 0x8231cb32, 0x8231cb33, 0x8231cb34, 0x8231cb35, /* 84 */ 0x8231cb36, 0x8231cb37, 0x8231cb38, 0x8231cb39, @@ -1925,7 +1963,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8231d038, 0x8231d039, 0x8231d130, 0x8231d131, /* bc */ 0x8231d132, 0x8231d133, 0x8231d134, 0x8231d135, - /*** Three byte table, leaf: e3b3xx - offset 0x018a6 ***/ + /*** Three byte table, leaf: e3b3xx - offset 0x01927 ***/ /* 80 */ 0x8231d136, 0x8231d137, 0x8231d138, 0x8231d139, /* 84 */ 0x8231d230, 0x8231d231, 0x8231d232, 0x8231d233, @@ -1940,7 +1978,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* a8 */ 0x00000000, 0x00000000, /* 22 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e481xx - offset 0x018d0 ***/ + /*** Three byte table, leaf: e481xx - offset 0x01951 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -1959,7 +1997,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8232b236, 0x8232b237, 0x8232b238, 0x8232b239, /* bc */ 0x8232b330, 0x8232b331, 0x8232b332, 0x8232b333, - /*** Three byte table, leaf: e482xx - offset 0x01910 ***/ + /*** Three byte table, leaf: e482xx - offset 0x01991 ***/ /* 80 */ 0x8232b334, 0x8232b335, 0x8232b336, 0x8232b337, /* 84 */ 0x8232b338, 0x8232b339, 0x8232b430, 0x8232b431, @@ -1978,7 +2016,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8232b930, 0x8232b931, 0x8232b932, 0x8232b933, /* bc */ 0x8232b934, 0x8232b935, 0x8232b936, 0x8232b937, - /*** Three byte table, leaf: e483xx - offset 0x01950 ***/ + /*** Three byte table, leaf: e483xx - offset 0x019d1 ***/ /* 80 */ 0x8232b938, 0x8232b939, 0x8232ba30, 0x8232ba31, /* 84 */ 0x8232ba32, 0x8232ba33, 0x8232ba34, 0x8232ba35, @@ -1997,7 +2035,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8232bf34, 0x8232bf35, 0x8232bf36, 0x8232bf37, /* bc */ 0x8232bf38, 0x8232bf39, 0x8232c030, 0x8232c031, - /*** Three byte table, leaf: e484xx - offset 0x01990 ***/ + /*** Three byte table, leaf: e484xx - offset 0x01a11 ***/ /* 80 */ 0x8232c032, 0x8232c033, 0x8232c034, 0x8232c035, /* 84 */ 0x8232c036, 0x8232c037, 0x8232c038, 0x8232c039, @@ -2016,7 +2054,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8232c538, 0x8232c539, 0x8232c630, 0x8232c631, /* bc */ 0x8232c632, 0x8232c633, 0x8232c634, 0x8232c635, - /*** Three byte table, leaf: e485xx - offset 0x019d0 ***/ + /*** Three byte table, leaf: e485xx - offset 0x01a51 ***/ /* 80 */ 0x8232c636, 0x8232c637, 0x8232c638, 0x8232c639, /* 84 */ 0x8232c730, 0x8232c731, 0x8232c732, 0x8232c733, @@ -2028,7 +2066,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* 9c */ 0x8232c934, 0x8232c935, 0x8232c936, 0x0000fe70, /* 32 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e48cxx - offset 0x019f0 ***/ + /*** Three byte table, leaf: e48cxx - offset 0x01a71 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -2047,7 +2085,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8232f838, 0x8232f839, 0x8232f930, 0x8232f931, /* bc */ 0x8232f932, 0x8232f933, 0x8232f934, 0x8232f935, - /*** Three byte table, leaf: e48dxx - offset 0x01a30 ***/ + /*** Three byte table, leaf: e48dxx - offset 0x01ab1 ***/ /* 80 */ 0x8232f936, 0x8232f937, 0x8232f938, 0x8232f939, /* 84 */ 0x8232fa30, 0x8232fa31, 0x8232fa32, 0x8232fa33, @@ -2066,7 +2104,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82338132, 0x82338133, 0x82338134, 0x82338135, /* bc */ 0x82338136, 0x82338137, 0x82338138, 0x82338139, - /*** Three byte table, leaf: e48exx - offset 0x01a70 ***/ + /*** Three byte table, leaf: e48exx - offset 0x01af1 ***/ /* 80 */ 0x82338230, 0x82338231, 0x82338232, 0x82338233, /* 84 */ 0x82338234, 0x82338235, 0x82338236, 0x82338237, @@ -2085,7 +2123,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82338734, 0x82338735, 0x82338736, 0x82338737, /* bc */ 0x82338738, 0x82338739, 0x82338830, 0x82338831, - /*** Three byte table, leaf: e48fxx - offset 0x01ab0 ***/ + /*** Three byte table, leaf: e48fxx - offset 0x01b31 ***/ /* 80 */ 0x82338832, 0x82338833, 0x82338834, 0x82338835, /* 84 */ 0x82338836, 0x82338837, 0x82338838, 0x82338839, @@ -2104,7 +2142,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82338d37, 0x82338d38, 0x82338d39, 0x82338e30, /* bc */ 0x82338e31, 0x82338e32, 0x82338e33, 0x82338e34, - /*** Three byte table, leaf: e490xx - offset 0x01af0 ***/ + /*** Three byte table, leaf: e490xx - offset 0x01b71 ***/ /* 80 */ 0x82338e35, 0x82338e36, 0x82338e37, 0x82338e38, /* 84 */ 0x82338e39, 0x82338f30, 0x82338f31, 0x82338f32, @@ -2123,7 +2161,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82339431, 0x82339432, 0x82339433, 0x82339434, /* bc */ 0x82339435, 0x82339436, 0x82339437, 0x82339438, - /*** Three byte table, leaf: e491xx - offset 0x01b30 ***/ + /*** Three byte table, leaf: e491xx - offset 0x01bb1 ***/ /* 80 */ 0x82339439, 0x82339530, 0x82339531, 0x82339532, /* 84 */ 0x82339533, 0x82339534, 0x82339535, 0x82339536, @@ -2142,7 +2180,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82339a35, 0x82339a36, 0x82339a37, 0x82339a38, /* bc */ 0x82339a39, 0x82339b30, 0x82339b31, 0x82339b32, - /*** Three byte table, leaf: e492xx - offset 0x01b70 ***/ + /*** Three byte table, leaf: e492xx - offset 0x01bf1 ***/ /* 80 */ 0x82339b33, 0x82339b34, 0x82339b35, 0x82339b36, /* 84 */ 0x82339b37, 0x82339b38, 0x82339b39, 0x82339c30, @@ -2161,7 +2199,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8233a039, 0x8233a130, 0x8233a131, 0x8233a132, /* bc */ 0x8233a133, 0x8233a134, 0x8233a135, 0x8233a136, - /*** Three byte table, leaf: e493xx - offset 0x01bb0 ***/ + /*** Three byte table, leaf: e493xx - offset 0x01c31 ***/ /* 80 */ 0x8233a137, 0x8233a138, 0x8233a139, 0x8233a230, /* 84 */ 0x8233a231, 0x8233a232, 0x8233a233, 0x8233a234, @@ -2178,7 +2216,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 12 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e499xx - offset 0x01be4 ***/ + /*** Three byte table, leaf: e499xx - offset 0x01c65 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -2197,7 +2235,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8233cd34, 0x8233cd35, 0x8233cd36, 0x8233cd37, /* bc */ 0x8233cd38, 0x8233cd39, 0x8233ce30, 0x8233ce31, - /*** Three byte table, leaf: e49axx - offset 0x01c24 ***/ + /*** Three byte table, leaf: e49axx - offset 0x01ca5 ***/ /* 80 */ 0x8233ce32, 0x8233ce33, 0x8233ce34, 0x8233ce35, /* 84 */ 0x8233ce36, 0x8233ce37, 0x8233ce38, 0x8233ce39, @@ -2216,7 +2254,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8233d338, 0x8233d339, 0x8233d430, 0x8233d431, /* bc */ 0x8233d432, 0x8233d433, 0x8233d434, 0x8233d435, - /*** Three byte table, leaf: e49bxx - offset 0x01c64 ***/ + /*** Three byte table, leaf: e49bxx - offset 0x01ce5 ***/ /* 80 */ 0x8233d436, 0x8233d437, 0x8233d438, 0x8233d439, /* 84 */ 0x8233d530, 0x8233d531, 0x8233d532, 0x8233d533, @@ -2235,7 +2273,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8233da32, 0x8233da33, 0x8233da34, 0x8233da35, /* bc */ 0x8233da36, 0x8233da37, 0x8233da38, 0x8233da39, - /*** Three byte table, leaf: e49cxx - offset 0x01ca4 ***/ + /*** Three byte table, leaf: e49cxx - offset 0x01d25 ***/ /* 80 */ 0x8233db30, 0x8233db31, 0x8233db32, 0x8233db33, /* 84 */ 0x8233db34, 0x8233db35, 0x8233db36, 0x8233db37, @@ -2254,7 +2292,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8233e034, 0x8233e035, 0x8233e036, 0x8233e037, /* bc */ 0x8233e038, 0x8233e039, 0x8233e130, 0x8233e131, - /*** Three byte table, leaf: e49dxx - offset 0x01ce4 ***/ + /*** Three byte table, leaf: e49dxx - offset 0x01d65 ***/ /* 80 */ 0x8233e132, 0x8233e133, 0x8233e134, 0x8233e135, /* 84 */ 0x8233e136, 0x8233e137, 0x8233e138, 0x8233e139, @@ -2273,7 +2311,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8233e638, 0x8233e639, 0x8233e730, 0x8233e731, /* bc */ 0x0000fe82, 0x8233e732, 0x8233e733, 0x8233e734, - /*** Three byte table, leaf: e49exx - offset 0x01d24 ***/ + /*** Three byte table, leaf: e49exx - offset 0x01da5 ***/ /* 80 */ 0x8233e735, 0x8233e736, 0x8233e737, 0x8233e738, /* 84 */ 0x8233e739, 0x8233e830, 0x8233e831, 0x8233e832, @@ -2292,7 +2330,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00000000, /* 7 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e4a5xx - offset 0x01d5d ***/ + /*** Three byte table, leaf: e4a5xx - offset 0x01dde ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x0000fe85, @@ -2311,7 +2349,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82349b37, 0x82349b38, 0x0000fe86, 0x82349b39, /* bc */ 0x82349c30, 0x0000fe87, 0x82349c31, 0x82349c32, - /*** Three byte table, leaf: e4a6xx - offset 0x01d9d ***/ + /*** Three byte table, leaf: e4a6xx - offset 0x01e1e ***/ /* 80 */ 0x82349c33, 0x82349c34, 0x0000fe88, 0x0000fe89, /* 84 */ 0x82349c35, 0x0000fe8a, 0x0000fe8b, 0x82349c36, @@ -2329,7 +2367,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b4 */ 0x8234a039, 0x8234a130, 0x0000fe8f, 0x0000fe8e, /* 8 trailing zero values shared with next segment */ - /*** Three byte table, leaf: e4b1xx - offset 0x01dd5 ***/ + /*** Three byte table, leaf: e4b1xx - offset 0x01e56 ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -2348,7 +2386,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8234e734, 0x8234e735, 0x8234e736, 0x8234e737, /* bc */ 0x8234e738, 0x8234e739, 0x8234e830, 0x8234e831, - /*** Three byte table, leaf: e4b2xx - offset 0x01e15 ***/ + /*** Three byte table, leaf: e4b2xx - offset 0x01e96 ***/ /* 80 */ 0x8234e832, 0x8234e833, 0x8234e834, 0x8234e835, /* 84 */ 0x8234e836, 0x8234e837, 0x8234e838, 0x8234e839, @@ -2367,7 +2405,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8234ed33, 0x8234ed34, 0x8234ed35, 0x8234ed36, /* bc */ 0x8234ed37, 0x8234ed38, 0x8234ed39, 0x8234ee30, - /*** Three byte table, leaf: e4b3xx - offset 0x01e55 ***/ + /*** Three byte table, leaf: e4b3xx - offset 0x01ed6 ***/ /* 80 */ 0x8234ee31, 0x8234ee32, 0x8234ee33, 0x8234ee34, /* 84 */ 0x8234ee35, 0x8234ee36, 0x8234ee37, 0x8234ee38, @@ -2386,7 +2424,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8234f337, 0x8234f338, 0x8234f339, 0x8234f430, /* bc */ 0x8234f431, 0x8234f432, 0x8234f433, 0x8234f434, - /*** Three byte table, leaf: e4b4xx - offset 0x01e95 ***/ + /*** Three byte table, leaf: e4b4xx - offset 0x01f16 ***/ /* 80 */ 0x8234f435, 0x8234f436, 0x8234f437, 0x8234f438, /* 84 */ 0x8234f439, 0x8234f530, 0x8234f531, 0x8234f532, @@ -2405,7 +2443,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x8234f934, 0x8234f935, 0x8234f936, 0x8234f937, /* bc */ 0x8234f938, 0x8234f939, 0x8234fa30, 0x8234fa31, - /*** Three byte table, leaf: e4b5xx - offset 0x01ed5 ***/ + /*** Three byte table, leaf: e4b5xx - offset 0x01f56 ***/ /* 80 */ 0x8234fa32, 0x8234fa33, 0x8234fa34, 0x8234fa35, /* 84 */ 0x8234fa36, 0x8234fa37, 0x8234fa38, 0x8234fa39, @@ -2424,7 +2462,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82358138, 0x82358139, 0x82358230, 0x82358231, /* bc */ 0x82358232, 0x82358233, 0x82358234, 0x82358235, - /*** Three byte table, leaf: e4b6xx - offset 0x01f15 ***/ + /*** Three byte table, leaf: e4b6xx - offset 0x01f96 ***/ /* 80 */ 0x82358236, 0x82358237, 0x82358238, 0x82358239, /* 84 */ 0x82358330, 0x82358331, 0x82358332, 0x82358333, @@ -2443,7 +2481,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82358831, 0x82358832, 0x82358833, 0x82358834, /* bc */ 0x82358835, 0x82358836, 0x82358837, 0x82358838, - /*** Three byte table, leaf: e4b7xx - offset 0x01f55 ***/ + /*** Three byte table, leaf: e4b7xx - offset 0x01fd6 ***/ /* 80 */ 0x82358839, 0x82358930, 0x82358931, 0x82358932, /* 84 */ 0x82358933, 0x82358934, 0x82358935, 0x82358936, @@ -2462,7 +2500,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x82358e35, 0x82358e36, 0x82358e37, 0x82358e38, /* bc */ 0x82358e39, 0x82358f30, 0x82358f31, 0x82358f32, - /*** Three byte table, leaf: e4b8xx - offset 0x01f95 ***/ + /*** Three byte table, leaf: e4b8xx - offset 0x02016 ***/ /* 80 */ 0x0000d2bb, 0x0000b6a1, 0x00008140, 0x0000c6df, /* 84 */ 0x00008141, 0x00008142, 0x00008143, 0x0000cdf2, @@ -2481,7 +2519,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cde8, 0x0000b5a4, 0x0000ceaa, 0x0000d6f7, /* bc */ 0x00008153, 0x0000c0f6, 0x0000bed9, 0x0000d8af, - /*** Three byte table, leaf: e4b9xx - offset 0x01fd5 ***/ + /*** Three byte table, leaf: e4b9xx - offset 0x02056 ***/ /* 80 */ 0x00008154, 0x00008155, 0x00008156, 0x0000c4cb, /* 84 */ 0x00008157, 0x0000bec3, 0x00008158, 0x0000d8b1, @@ -2500,7 +2538,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008170, 0x00008171, 0x00008172, 0x00008173, /* bc */ 0x00008174, 0x00008175, 0x0000c7ac, 0x00008176, - /*** Three byte table, leaf: e4baxx - offset 0x02015 ***/ + /*** Three byte table, leaf: e4baxx - offset 0x02096 ***/ /* 80 */ 0x00008177, 0x00008178, 0x00008179, 0x0000817a, /* 84 */ 0x0000817b, 0x0000817c, 0x0000c1cb, 0x0000817d, @@ -2519,7 +2557,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000818f, 0x00008190, 0x0000c8cb, 0x0000d8e9, /* bc */ 0x00008191, 0x00008192, 0x00008193, 0x0000d2da, - /*** Three byte table, leaf: e4bbxx - offset 0x02055 ***/ + /*** Three byte table, leaf: e4bbxx - offset 0x020d6 ***/ /* 80 */ 0x0000cab2, 0x0000c8ca, 0x0000d8ec, 0x0000d8ea, /* 84 */ 0x0000d8c6, 0x0000bdf6, 0x0000c6cd, 0x0000b3f0, @@ -2538,7 +2576,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000081a6, 0x000081a7, 0x000081a8, 0x0000c8ce, /* bc */ 0x000081a9, 0x0000b7dd, 0x000081aa, 0x0000b7c2, - /*** Three byte table, leaf: e4bcxx - offset 0x02095 ***/ + /*** Three byte table, leaf: e4bcxx - offset 0x02116 ***/ /* 80 */ 0x000081ab, 0x0000c6f3, 0x000081ac, 0x000081ad, /* 84 */ 0x000081ae, 0x000081af, 0x000081b0, 0x000081b1, @@ -2557,7 +2595,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c9ec, 0x000081c7, 0x0000cbc5, 0x000081c8, /* bc */ 0x0000cbc6, 0x0000d9a4, 0x000081c9, 0x000081ca, - /*** Three byte table, leaf: e4bdxx - offset 0x020d5 ***/ + /*** Three byte table, leaf: e4bdxx - offset 0x02156 ***/ /* 80 */ 0x000081cb, 0x000081cc, 0x000081cd, 0x0000b5e8, /* 84 */ 0x000081ce, 0x000081cf, 0x0000b5ab, 0x000081d0, @@ -2576,7 +2614,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000081e5, 0x000081e6, 0x000081e7, 0x0000d9ac, /* bc */ 0x0000d9ae, 0x000081e8, 0x0000d9ab, 0x0000cab9, - /*** Three byte table, leaf: e4bexx - offset 0x02115 ***/ + /*** Three byte table, leaf: e4bexx - offset 0x02196 ***/ /* 80 */ 0x000081e9, 0x000081ea, 0x000081eb, 0x0000d9a9, /* 84 */ 0x0000d6b6, 0x000081ec, 0x000081ed, 0x000081ee, @@ -2595,7 +2633,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000824a, 0x0000824b, 0x0000824c, 0x0000824d, /* bc */ 0x0000824e, 0x0000824f, 0x00008250, 0x0000b1e3, - /*** Three byte table, leaf: e4bfxx - offset 0x02155 ***/ + /*** Three byte table, leaf: e4bfxx - offset 0x021d6 ***/ /* 80 */ 0x00008251, 0x00008252, 0x00008253, 0x0000b4d9, /* 84 */ 0x0000b6ed, 0x0000d9b4, 0x00008254, 0x00008255, @@ -2614,7 +2652,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d9ba, 0x0000826f, 0x0000b0b3, 0x00008270, /* bc */ 0x00008271, 0x00008272, 0x0000d9c2, 0x00008273, - /*** Three byte table, leaf: e580xx - offset 0x02195 ***/ + /*** Three byte table, leaf: e580xx - offset 0x02216 ***/ /* 80 */ 0x00008274, 0x00008275, 0x00008276, 0x00008277, /* 84 */ 0x00008278, 0x00008279, 0x0000827a, 0x0000827b, @@ -2633,7 +2671,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000829a, 0x0000829b, 0x0000d5ae, 0x0000829c, /* bc */ 0x0000d6b5, 0x0000829d, 0x0000c7e3, 0x0000829e, - /*** Three byte table, leaf: e581xx - offset 0x021d5 ***/ + /*** Three byte table, leaf: e581xx - offset 0x02256 ***/ /* 80 */ 0x0000829f, 0x000082a0, 0x000082a1, 0x0000d9c8, /* 84 */ 0x000082a2, 0x000082a3, 0x000082a4, 0x0000bcd9, @@ -2652,7 +2690,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000082ca, 0x000082cb, 0x000082cc, 0x0000d9cd, /* bc */ 0x000082cd, 0x000082ce, 0x0000d9c7, 0x0000b3a5, - /*** Three byte table, leaf: e582xx - offset 0x02215 ***/ + /*** Three byte table, leaf: e582xx - offset 0x02296 ***/ /* 80 */ 0x0000bffe, 0x000082cf, 0x000082d0, 0x000082d1, /* 84 */ 0x000082d2, 0x0000b8b5, 0x000082d3, 0x000082d4, @@ -2671,7 +2709,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000082fc, 0x000082fd, 0x0000d9d1, 0x0000c9b5, /* bc */ 0x000082fe, 0x00008340, 0x00008341, 0x00008342, - /*** Three byte table, leaf: e583xx - offset 0x02255 ***/ + /*** Three byte table, leaf: e583xx - offset 0x022d6 ***/ /* 80 */ 0x00008343, 0x00008344, 0x00008345, 0x00008346, /* 84 */ 0x00008347, 0x00008348, 0x00008349, 0x0000834a, @@ -2690,7 +2728,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008371, 0x00008372, 0x00008373, 0x0000c6a7, /* bc */ 0x00008374, 0x00008375, 0x00008376, 0x00008377, - /*** Three byte table, leaf: e584xx - offset 0x02295 ***/ + /*** Three byte table, leaf: e584xx - offset 0x02316 ***/ /* 80 */ 0x00008378, 0x00008379, 0x0000837a, 0x0000837b, /* 84 */ 0x0000837c, 0x0000837d, 0x0000d9d3, 0x0000d9d8, @@ -2709,7 +2747,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000083ac, 0x000083ad, 0x000083ae, 0x000083af, /* bc */ 0x000083b0, 0x000083b1, 0x000083b2, 0x0000b6f9, - /*** Three byte table, leaf: e585xx - offset 0x022d5 ***/ + /*** Three byte table, leaf: e585xx - offset 0x02356 ***/ /* 80 */ 0x0000d8a3, 0x0000d4ca, 0x000083b3, 0x0000d4aa, /* 84 */ 0x0000d0d6, 0x0000b3e4, 0x0000d5d7, 0x000083b4, @@ -2728,7 +2766,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b5e4, 0x0000d7c8, 0x000083cd, 0x0000d1f8, /* bc */ 0x0000bce6, 0x0000cade, 0x000083ce, 0x000083cf, - /*** Three byte table, leaf: e586xx - offset 0x02315 ***/ + /*** Three byte table, leaf: e586xx - offset 0x02396 ***/ /* 80 */ 0x0000bcbd, 0x0000d9e6, 0x0000d8e7, 0x000083d0, /* 84 */ 0x000083d1, 0x0000c4da, 0x000083d2, 0x000083d3, @@ -2747,7 +2785,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000083eb, 0x000083ec, 0x000083ed, 0x0000b6b3, /* bc */ 0x0000d9fe, 0x0000d9fd, 0x000083ee, 0x000083ef, - /*** Three byte table, leaf: e587xx - offset 0x02355 ***/ + /*** Three byte table, leaf: e587xx - offset 0x023d6 ***/ /* 80 */ 0x0000bebb, 0x000083f0, 0x000083f1, 0x000083f2, /* 84 */ 0x0000c6e0, 0x000083f3, 0x0000d7bc, 0x0000daa1, @@ -2766,7 +2804,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cdb9, 0x0000b0bc, 0x0000b3f6, 0x0000bbf7, /* bc */ 0x0000dbca, 0x0000baaf, 0x00008454, 0x0000d4e4, - /*** Three byte table, leaf: e588xx - offset 0x02395 ***/ + /*** Three byte table, leaf: e588xx - offset 0x02416 ***/ /* 80 */ 0x0000b5b6, 0x0000b5f3, 0x0000d8d6, 0x0000c8d0, /* 84 */ 0x00008455, 0x00008456, 0x0000b7d6, 0x0000c7d0, @@ -2785,7 +2823,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c8af, 0x0000c9b2, 0x0000b4cc, 0x0000bfcc, /* bc */ 0x0000846f, 0x0000b9f4, 0x00008470, 0x0000d8db, - /*** Three byte table, leaf: e589xx - offset 0x023d5 ***/ + /*** Three byte table, leaf: e589xx - offset 0x02456 ***/ /* 80 */ 0x0000d8dc, 0x0000b6e7, 0x0000bcc1, 0x0000ccea, /* 84 */ 0x00008471, 0x00008472, 0x00008473, 0x00008474, @@ -2804,7 +2842,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008496, 0x00008497, 0x00008498, 0x00008499, /* bc */ 0x0000849a, 0x0000d8e2, 0x0000849b, 0x0000bdcb, - /*** Three byte table, leaf: e58axx - offset 0x02415 ***/ + /*** Three byte table, leaf: e58axx - offset 0x02496 ***/ /* 80 */ 0x0000849c, 0x0000d8e4, 0x0000d8e3, 0x0000849d, /* 84 */ 0x0000849e, 0x0000849f, 0x000084a0, 0x000084a1, @@ -2823,7 +2861,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000084be, 0x000084bf, 0x000084c0, 0x000084c1, /* bc */ 0x000084c2, 0x000084c3, 0x0000dbc0, 0x0000cac6, - /*** Three byte table, leaf: e58bxx - offset 0x02455 ***/ + /*** Three byte table, leaf: e58bxx - offset 0x024d6 ***/ /* 80 */ 0x000084c4, 0x000084c5, 0x000084c6, 0x0000b2aa, /* 84 */ 0x000084c7, 0x000084c8, 0x000084c9, 0x0000d3c2, @@ -2842,7 +2880,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000084f1, 0x0000d9e8, 0x0000c9d7, 0x000084f2, /* bc */ 0x000084f3, 0x000084f4, 0x0000b9b4, 0x0000cef0, - /*** Three byte table, leaf: e58cxx - offset 0x02495 ***/ + /*** Three byte table, leaf: e58cxx - offset 0x02516 ***/ /* 80 */ 0x0000d4c8, 0x000084f5, 0x000084f6, 0x000084f7, /* 84 */ 0x000084f8, 0x0000b0fc, 0x0000b4d2, 0x000084f9, @@ -2861,7 +2899,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000855b, 0x0000c6a5, 0x0000c7f8, 0x0000d2bd, /* bc */ 0x0000855c, 0x0000855d, 0x0000d8d2, 0x0000c4e4, - /*** Three byte table, leaf: e58dxx - offset 0x024d5 ***/ + /*** Three byte table, leaf: e58dxx - offset 0x02556 ***/ /* 80 */ 0x0000855e, 0x0000caae, 0x0000855f, 0x0000c7a7, /* 84 */ 0x00008560, 0x0000d8a6, 0x00008561, 0x0000c9fd, @@ -2880,7 +2918,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d0b6, 0x00008572, 0x0000dae1, 0x00008573, /* bc */ 0x00008574, 0x00008575, 0x00008576, 0x0000c7e4, - /*** Three byte table, leaf: e58exx - offset 0x02515 ***/ + /*** Three byte table, leaf: e58exx - offset 0x02596 ***/ /* 80 */ 0x00008577, 0x00008578, 0x0000b3a7, 0x00008579, /* 84 */ 0x0000b6f2, 0x0000ccfc, 0x0000c0fa, 0x0000857a, @@ -2899,7 +2937,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000859b, 0x0000859c, 0x0000859d, 0x0000c8a5, /* bc */ 0x0000859e, 0x0000859f, 0x000085a0, 0x0000cfd8, - /*** Three byte table, leaf: e58fxx - offset 0x02555 ***/ + /*** Three byte table, leaf: e58fxx - offset 0x025d6 ***/ /* 80 */ 0x000085a1, 0x0000c8fe, 0x0000b2ce, 0x000085a2, /* 84 */ 0x000085a3, 0x000085a4, 0x000085a5, 0x000085a6, @@ -2918,7 +2956,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cbbe, 0x0000ccbe, 0x000085b5, 0x0000dfb7, /* bc */ 0x0000b5f0, 0x0000dfb4, 0x000085b6, 0x000085b7, - /*** Three byte table, leaf: e590xx - offset 0x02595 ***/ + /*** Three byte table, leaf: e590xx - offset 0x02616 ***/ /* 80 */ 0x000085b8, 0x0000d3f5, 0x000085b9, 0x0000b3d4, /* 84 */ 0x0000b8f7, 0x000085ba, 0x0000dfba, 0x000085bb, @@ -2937,7 +2975,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cefc, 0x0000b4b5, 0x000085ca, 0x0000cec7, /* bc */ 0x0000baf0, 0x000085cb, 0x0000cee1, 0x000085cc, - /*** Three byte table, leaf: e591xx - offset 0x025d5 ***/ + /*** Three byte table, leaf: e591xx - offset 0x02656 ***/ /* 80 */ 0x0000d1bd, 0x000085cd, 0x000085ce, 0x0000dfc0, /* 84 */ 0x000085cf, 0x000085d0, 0x0000b4f4, 0x000085d1, @@ -2956,7 +2994,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c5de, 0x000085ea, 0x000085eb, 0x0000c9eb, /* bc */ 0x0000baf4, 0x0000c3fc, 0x000085ec, 0x000085ed, - /*** Three byte table, leaf: e592xx - offset 0x02615 ***/ + /*** Three byte table, leaf: e592xx - offset 0x02696 ***/ /* 80 */ 0x0000bed7, 0x000085ee, 0x0000dfc6, 0x000085ef, /* 84 */ 0x0000dfcd, 0x000085f0, 0x0000c5d8, 0x000085f1, @@ -2975,7 +3013,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cfcc, 0x00008648, 0x00008649, 0x0000dfdd, /* bc */ 0x0000864a, 0x0000d1ca, 0x0000864b, 0x0000dfde, - /*** Three byte table, leaf: e593xx - offset 0x02655 ***/ + /*** Three byte table, leaf: e593xx - offset 0x026d6 ***/ /* 80 */ 0x0000b0a7, 0x0000c6b7, 0x0000dfd3, 0x0000864c, /* 84 */ 0x0000bae5, 0x0000864d, 0x0000b6df, 0x0000cddb, @@ -2994,7 +3032,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008661, 0x00008662, 0x0000b2b8, 0x00008663, /* bc */ 0x0000badf, 0x0000dfec, 0x00008664, 0x0000dbc1, - /*** Three byte table, leaf: e594xx - offset 0x02695 ***/ + /*** Three byte table, leaf: e594xx - offset 0x02716 ***/ /* 80 */ 0x00008665, 0x0000d1e4, 0x00008666, 0x00008667, /* 84 */ 0x00008668, 0x00008669, 0x0000cbf4, 0x0000b4bd, @@ -3013,7 +3051,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008688, 0x00008689, 0x0000868a, 0x0000868b, /* bc */ 0x0000dffe, 0x0000868c, 0x0000cdd9, 0x0000dffc, - /*** Three byte table, leaf: e595xx - offset 0x026d5 ***/ + /*** Three byte table, leaf: e595xx - offset 0x02756 ***/ /* 80 */ 0x0000868d, 0x0000dffa, 0x0000868e, 0x0000bfd0, /* 84 */ 0x0000d7c4, 0x0000868f, 0x0000c9cc, 0x00008690, @@ -3032,7 +3070,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d0a5, 0x000086af, 0x000086b0, 0x0000e0b4, /* bc */ 0x0000cce4, 0x000086b1, 0x0000e0b1, 0x000086b2, - /*** Three byte table, leaf: e596xx - offset 0x02715 ***/ + /*** Three byte table, leaf: e596xx - offset 0x02796 ***/ /* 80 */ 0x0000bfa6, 0x0000e0af, 0x0000ceb9, 0x0000e0ab, /* 84 */ 0x0000c9c6, 0x000086b3, 0x000086b4, 0x0000c0ae, @@ -3051,7 +3089,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000086d4, 0x0000e0ad, 0x000086d5, 0x0000d3f7, /* bc */ 0x000086d6, 0x0000e0b6, 0x0000e0b7, 0x000086d7, - /*** Three byte table, leaf: e597xx - offset 0x02755 ***/ + /*** Three byte table, leaf: e597xx - offset 0x027d6 ***/ /* 80 */ 0x000086d8, 0x000086d9, 0x000086da, 0x000086db, /* 84 */ 0x0000e0c4, 0x0000d0e1, 0x000086dc, 0x000086dd, @@ -3070,7 +3108,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000086f5, 0x000086f6, 0x000086f7, 0x000086f8, /* bc */ 0x000086f9, 0x0000cbd4, 0x0000e0d5, 0x000086fa, - /*** Three byte table, leaf: e598xx - offset 0x02795 ***/ + /*** Three byte table, leaf: e598xx - offset 0x02816 ***/ /* 80 */ 0x0000e0d6, 0x0000e0d2, 0x000086fb, 0x000086fc, /* 84 */ 0x000086fd, 0x000086fe, 0x00008740, 0x00008741, @@ -3089,7 +3127,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008760, 0x0000e0da, 0x00008761, 0x0000cefb, /* bc */ 0x00008762, 0x00008763, 0x00008764, 0x0000bad9, - /*** Three byte table, leaf: e599xx - offset 0x027d5 ***/ + /*** Three byte table, leaf: e599xx - offset 0x02856 ***/ /* 80 */ 0x00008765, 0x00008766, 0x00008767, 0x00008768, /* 84 */ 0x00008769, 0x0000876a, 0x0000876b, 0x0000876c, @@ -3108,7 +3146,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000878d, 0x0000878e, 0x0000878f, 0x0000e0e7, /* bc */ 0x0000e0e8, 0x00008790, 0x00008791, 0x00008792, - /*** Three byte table, leaf: e59axx - offset 0x02815 ***/ + /*** Three byte table, leaf: e59axx - offset 0x02896 ***/ /* 80 */ 0x00008793, 0x00008794, 0x00008795, 0x00008796, /* 84 */ 0x00008797, 0x0000e0e9, 0x0000e0e3, 0x00008798, @@ -3127,7 +3165,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000087c3, 0x000087c4, 0x000087c5, 0x000087c6, /* bc */ 0x0000bdc0, 0x000087c7, 0x000087c8, 0x000087c9, - /*** Three byte table, leaf: e59bxx - offset 0x02855 ***/ + /*** Three byte table, leaf: e59bxx - offset 0x028d6 ***/ /* 80 */ 0x000087ca, 0x000087cb, 0x000087cc, 0x000087cd, /* 84 */ 0x000087ce, 0x000087cf, 0x000087d0, 0x000087d1, @@ -3146,7 +3184,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000087f0, 0x0000e0f2, 0x0000b9cc, 0x000087f1, /* bc */ 0x000087f2, 0x0000b9fa, 0x0000cdbc, 0x0000e0f3, - /*** Three byte table, leaf: e59cxx - offset 0x02895 ***/ + /*** Three byte table, leaf: e59cxx - offset 0x02916 ***/ /* 80 */ 0x000087f3, 0x000087f4, 0x000087f5, 0x0000c6d4, /* 84 */ 0x0000e0f4, 0x000087f6, 0x0000d4b2, 0x000087f7, @@ -3165,7 +3203,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000885a, 0x0000dbdb, 0x0000b3a1, 0x0000dbdf, /* bc */ 0x0000885b, 0x0000885c, 0x0000bbf8, 0x0000885d, - /*** Three byte table, leaf: e59dxx - offset 0x028d5 ***/ + /*** Three byte table, leaf: e59dxx - offset 0x02956 ***/ /* 80 */ 0x0000d6b7, 0x0000885e, 0x0000dbe0, 0x0000885f, /* 84 */ 0x00008860, 0x00008861, 0x00008862, 0x0000bef9, @@ -3184,7 +3222,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008878, 0x00008879, 0x0000887a, 0x0000dbe6, /* bc */ 0x0000dbe5, 0x0000887b, 0x0000887c, 0x0000887d, - /*** Three byte table, leaf: e59exx - offset 0x02915 ***/ + /*** Three byte table, leaf: e59exx - offset 0x02996 ***/ /* 80 */ 0x0000887e, 0x00008880, 0x0000b4b9, 0x0000c0ac, /* 84 */ 0x0000c2a2, 0x0000dbe2, 0x0000dbe4, 0x00008881, @@ -3203,7 +3241,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dbf9, 0x000088a0, 0x000088a1, 0x000088a2, /* bc */ 0x000088a3, 0x000088a4, 0x000088a5, 0x000088a6, - /*** Three byte table, leaf: e59fxx - offset 0x02955 ***/ + /*** Three byte table, leaf: e59fxx - offset 0x029d6 ***/ /* 80 */ 0x000088a7, 0x000088a8, 0x0000b9a1, 0x0000b0a3, /* 84 */ 0x000088a9, 0x000088aa, 0x000088ab, 0x000088ac, @@ -3222,7 +3260,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dbfc, 0x0000c5e0, 0x0000bbf9, 0x000088cd, /* bc */ 0x000088ce, 0x0000dca3, 0x000088cf, 0x000088d0, - /*** Three byte table, leaf: e5a0xx - offset 0x02995 ***/ + /*** Three byte table, leaf: e5a0xx - offset 0x02a16 ***/ /* 80 */ 0x0000dca5, 0x000088d1, 0x0000ccc3, 0x000088d2, /* 84 */ 0x000088d3, 0x000088d4, 0x0000b6d1, 0x0000ddc0, @@ -3241,7 +3279,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000088f9, 0x000088fa, 0x000088fb, 0x000088fc, /* bc */ 0x000088fd, 0x000088fe, 0x00008940, 0x00008941, - /*** Three byte table, leaf: e5a1xx - offset 0x029d5 ***/ + /*** Three byte table, leaf: e5a1xx - offset 0x02a56 ***/ /* 80 */ 0x00008942, 0x00008943, 0x00008944, 0x00008945, /* 84 */ 0x0000dca8, 0x00008946, 0x00008947, 0x00008948, @@ -3260,7 +3298,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008970, 0x00008971, 0x00008972, 0x00008973, /* bc */ 0x00008974, 0x00008975, 0x0000dbd3, 0x00008976, - /*** Three byte table, leaf: e5a2xx - offset 0x02a15 ***/ + /*** Three byte table, leaf: e5a2xx - offset 0x02a96 ***/ /* 80 */ 0x0000dcaf, 0x0000dcac, 0x00008977, 0x0000beb3, /* 84 */ 0x00008978, 0x0000cafb, 0x00008979, 0x0000897a, @@ -3279,7 +3317,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000089a3, 0x000089a4, 0x000089a5, 0x000089a6, /* bc */ 0x0000dbd4, 0x000089a7, 0x000089a8, 0x000089a9, - /*** Three byte table, leaf: e5a3xx - offset 0x02a55 ***/ + /*** Three byte table, leaf: e5a3xx - offset 0x02ad6 ***/ /* 80 */ 0x000089aa, 0x0000b1da, 0x000089ab, 0x000089ac, /* 84 */ 0x000089ad, 0x0000dbd5, 0x000089ae, 0x000089af, @@ -3298,7 +3336,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000089d7, 0x0000d2bc, 0x000089d8, 0x000089d9, /* bc */ 0x000089da, 0x000089db, 0x000089dc, 0x000089dd, - /*** Three byte table, leaf: e5a4xx - offset 0x02a95 ***/ + /*** Three byte table, leaf: e5a4xx - offset 0x02b16 ***/ /* 80 */ 0x000089de, 0x000089df, 0x0000e2ba, 0x000089e0, /* 84 */ 0x0000b4a6, 0x000089e1, 0x000089e2, 0x0000b1b8, @@ -3317,7 +3355,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bfe4, 0x0000bcd0, 0x0000b6e1, 0x000089fe, /* bc */ 0x0000dec5, 0x00008a40, 0x00008a41, 0x00008a42, - /*** Three byte table, leaf: e5a5xx - offset 0x02ad5 ***/ + /*** Three byte table, leaf: e5a5xx - offset 0x02b56 ***/ /* 80 */ 0x00008a43, 0x0000dec6, 0x0000dbbc, 0x00008a44, /* 84 */ 0x0000d1d9, 0x00008a45, 0x00008a46, 0x0000c6e6, @@ -3336,7 +3374,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bce9, 0x0000cbfd, 0x00008a65, 0x00008a66, /* bc */ 0x00008a67, 0x0000bac3, 0x00008a68, 0x00008a69, - /*** Three byte table, leaf: e5a6xx - offset 0x02b15 ***/ + /*** Three byte table, leaf: e5a6xx - offset 0x02b96 ***/ /* 80 */ 0x00008a6a, 0x0000e5f9, 0x0000c8e7, 0x0000e5fa, /* 84 */ 0x0000cdfd, 0x00008a6b, 0x0000d7b1, 0x0000b8be, @@ -3355,7 +3393,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008a8a, 0x0000c3c3, 0x00008a8b, 0x0000c6de, /* bc */ 0x00008a8c, 0x00008a8d, 0x0000e6aa, 0x00008a8e, - /*** Three byte table, leaf: e5a7xx - offset 0x02b55 ***/ + /*** Three byte table, leaf: e5a7xx - offset 0x02bd6 ***/ /* 80 */ 0x00008a8f, 0x00008a90, 0x00008a91, 0x00008a92, /* 84 */ 0x00008a93, 0x00008a94, 0x0000c4b7, 0x00008a95, @@ -3374,7 +3412,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008ab6, 0x0000e6b1, 0x00008ab7, 0x0000d2f6, /* bc */ 0x00008ab8, 0x00008ab9, 0x00008aba, 0x0000d7cb, - /*** Three byte table, leaf: e5a8xx - offset 0x02b95 ***/ + /*** Three byte table, leaf: e5a8xx - offset 0x02c16 ***/ /* 80 */ 0x00008abb, 0x0000cdfe, 0x00008abc, 0x0000cdde, /* 84 */ 0x0000c2a6, 0x0000e6ab, 0x0000e6ac, 0x0000bdbf, @@ -3393,7 +3431,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008add, 0x00008ade, 0x00008adf, 0x00008ae0, /* bc */ 0x0000e6bd, 0x00008ae1, 0x00008ae2, 0x00008ae3, - /*** Three byte table, leaf: e5a9xx - offset 0x02bd5 ***/ + /*** Three byte table, leaf: e5a9xx - offset 0x02c56 ***/ /* 80 */ 0x0000e6b9, 0x00008ae4, 0x00008ae5, 0x00008ae6, /* 84 */ 0x00008ae7, 0x00008ae8, 0x0000c6c5, 0x00008ae9, @@ -3412,7 +3450,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008b50, 0x00008b51, 0x0000e6c4, 0x00008b52, /* bc */ 0x00008b53, 0x00008b54, 0x00008b55, 0x0000d0f6, - /*** Three byte table, leaf: e5aaxx - offset 0x02c15 ***/ + /*** Three byte table, leaf: e5aaxx - offset 0x02c96 ***/ /* 80 */ 0x00008b56, 0x00008b57, 0x00008b58, 0x00008b59, /* 84 */ 0x00008b5a, 0x00008b5b, 0x00008b5c, 0x00008b5d, @@ -3431,7 +3469,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e6ca, 0x00008b88, 0x00008b89, 0x00008b8a, /* bc */ 0x00008b8b, 0x00008b8c, 0x0000e6c5, 0x00008b8d, - /*** Three byte table, leaf: e5abxx - offset 0x02c55 ***/ + /*** Three byte table, leaf: e5abxx - offset 0x02cd6 ***/ /* 80 */ 0x00008b8e, 0x0000bcde, 0x0000c9a9, 0x00008b8f, /* 84 */ 0x00008b90, 0x00008b91, 0x00008b92, 0x00008b93, @@ -3450,7 +3488,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008bb6, 0x00008bb7, 0x00008bb8, 0x00008bb9, /* bc */ 0x00008bba, 0x00008bbb, 0x00008bbc, 0x00008bbd, - /*** Three byte table, leaf: e5acxx - offset 0x02c95 ***/ + /*** Three byte table, leaf: e5acxx - offset 0x02d16 ***/ /* 80 */ 0x00008bbe, 0x00008bbf, 0x00008bc0, 0x00008bc1, /* 84 */ 0x00008bc2, 0x00008bc3, 0x00008bc4, 0x00008bc5, @@ -3469,7 +3507,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008bf0, 0x00008bf1, 0x00008bf2, 0x00008bf3, /* bc */ 0x00008bf4, 0x00008bf5, 0x00008bf6, 0x00008bf7, - /*** Three byte table, leaf: e5adxx - offset 0x02cd5 ***/ + /*** Three byte table, leaf: e5adxx - offset 0x02d56 ***/ /* 80 */ 0x0000e6d7, 0x00008bf8, 0x00008bf9, 0x00008bfa, /* 84 */ 0x00008bfb, 0x00008bfc, 0x00008bfd, 0x00008bfe, @@ -3488,7 +3526,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008c57, 0x00008c58, 0x0000c8e6, 0x00008c59, /* bc */ 0x00008c5a, 0x0000c4f5, 0x00008c5b, 0x00008c5c, - /*** Three byte table, leaf: e5aexx - offset 0x02d15 ***/ + /*** Three byte table, leaf: e5aexx - offset 0x02d96 ***/ /* 80 */ 0x0000e5b2, 0x0000c4fe, 0x00008c5d, 0x0000cbfc, /* 84 */ 0x0000e5b3, 0x0000d5ac, 0x00008c5e, 0x0000d3ee, @@ -3507,7 +3545,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e5b7, 0x0000c8dd, 0x00008c72, 0x00008c73, /* bc */ 0x00008c74, 0x0000bfed, 0x0000b1f6, 0x0000cbde, - /*** Three byte table, leaf: e5afxx - offset 0x02d55 ***/ + /*** Three byte table, leaf: e5afxx - offset 0x02dd6 ***/ /* 80 */ 0x00008c75, 0x00008c76, 0x0000bcc5, 0x00008c77, /* 84 */ 0x0000bcc4, 0x0000d2fa, 0x0000c3dc, 0x0000bfdc, @@ -3526,7 +3564,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b4e7, 0x0000b6d4, 0x0000cbc2, 0x0000d1b0, /* bc */ 0x0000b5bc, 0x00008c9c, 0x00008c9d, 0x0000cad9, - /*** Three byte table, leaf: e5b0xx - offset 0x02d95 ***/ + /*** Three byte table, leaf: e5b0xx - offset 0x02e16 ***/ /* 80 */ 0x00008c9e, 0x0000b7e2, 0x00008c9f, 0x00008ca0, /* 84 */ 0x0000c9e4, 0x00008ca1, 0x0000bdab, 0x00008ca2, @@ -3545,7 +3583,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000caac, 0x0000d2fc, 0x0000b3df, 0x0000e5ea, /* bc */ 0x0000c4e1, 0x0000bea1, 0x0000ceb2, 0x0000c4f2, - /*** Three byte table, leaf: e5b1xx - offset 0x02dd5 ***/ + /*** Three byte table, leaf: e5b1xx - offset 0x02e56 ***/ /* 80 */ 0x0000bed6, 0x0000c6a8, 0x0000b2e3, 0x00008cc1, /* 84 */ 0x00008cc2, 0x0000bed3, 0x00008cc3, 0x00008cc4, @@ -3564,7 +3602,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008ce2, 0x0000d2d9, 0x0000e1a8, 0x00008ce3, /* bc */ 0x00008ce4, 0x00008ce5, 0x00008ce6, 0x0000d3ec, - /*** Three byte table, leaf: e5b2xx - offset 0x02e15 ***/ + /*** Three byte table, leaf: e5b2xx - offset 0x02e96 ***/ /* 80 */ 0x00008ce7, 0x0000cbea, 0x0000c6f1, 0x00008ce8, /* 84 */ 0x00008ce9, 0x00008cea, 0x00008ceb, 0x00008cec, @@ -3583,7 +3621,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b0b6, 0x00008d47, 0x00008d48, 0x00008d49, /* bc */ 0x00008d4a, 0x0000e1b4, 0x00008d4b, 0x0000bff9, - /*** Three byte table, leaf: e5b3xx - offset 0x02e55 ***/ + /*** Three byte table, leaf: e5b3xx - offset 0x02ed6 ***/ /* 80 */ 0x00008d4c, 0x0000e1b9, 0x00008d4d, 0x00008d4e, /* 84 */ 0x0000e1bb, 0x00008d4f, 0x00008d50, 0x00008d51, @@ -3602,7 +3640,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008d77, 0x00008d78, 0x00008d79, 0x0000befe, /* bc */ 0x00008d7a, 0x00008d7b, 0x00008d7c, 0x00008d7d, - /*** Three byte table, leaf: e5b4xx - offset 0x02e95 ***/ + /*** Three byte table, leaf: e5b4xx - offset 0x02f16 ***/ /* 80 */ 0x00008d7e, 0x00008d80, 0x0000e1c0, 0x0000e1c1, /* 84 */ 0x00008d81, 0x00008d82, 0x0000e1c7, 0x0000b3e7, @@ -3621,7 +3659,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008da7, 0x00008da8, 0x00008da9, 0x00008daa, /* bc */ 0x00008dab, 0x0000e1cc, 0x0000e1ca, 0x00008dac, - /*** Three byte table, leaf: e5b5xx - offset 0x02ed5 ***/ + /*** Three byte table, leaf: e5b5xx - offset 0x02f56 ***/ /* 80 */ 0x00008dad, 0x00008dae, 0x00008daf, 0x00008db0, /* 84 */ 0x00008db1, 0x00008db2, 0x00008db3, 0x0000effa, @@ -3640,7 +3678,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008dd9, 0x00008dda, 0x00008ddb, 0x00008ddc, /* bc */ 0x00008ddd, 0x00008dde, 0x00008ddf, 0x00008de0, - /*** Three byte table, leaf: e5b6xx - offset 0x02f15 ***/ + /*** Three byte table, leaf: e5b6xx - offset 0x02f96 ***/ /* 80 */ 0x00008de1, 0x00008de2, 0x0000e1d6, 0x00008de3, /* 84 */ 0x00008de4, 0x00008de5, 0x00008de6, 0x00008de7, @@ -3659,7 +3697,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008e56, 0x00008e57, 0x00008e58, 0x00008e59, /* bc */ 0x00008e5a, 0x00008e5b, 0x00008e5c, 0x00008e5d, - /*** Three byte table, leaf: e5b7xx - offset 0x02f55 ***/ + /*** Three byte table, leaf: e5b7xx - offset 0x02fd6 ***/ /* 80 */ 0x00008e5e, 0x00008e5f, 0x00008e60, 0x00008e61, /* 84 */ 0x00008e62, 0x0000e1db, 0x00008e63, 0x00008e64, @@ -3678,7 +3716,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008e83, 0x00008e84, 0x00008e85, 0x00008e86, /* bc */ 0x00008e87, 0x0000d9e3, 0x0000bded, 0x00008e88, - /*** Three byte table, leaf: e5b8xx - offset 0x02f95 ***/ + /*** Three byte table, leaf: e5b8xx - offset 0x03016 ***/ /* 80 */ 0x00008e89, 0x0000b1d2, 0x0000cad0, 0x0000b2bc, /* 84 */ 0x00008e8a, 0x0000cba7, 0x0000b7ab, 0x00008e8b, @@ -3697,7 +3735,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b3a3, 0x00008ea8, 0x00008ea9, 0x0000e0fd, /* bc */ 0x0000e0fe, 0x0000c3b1, 0x00008eaa, 0x00008eab, - /*** Three byte table, leaf: e5b9xx - offset 0x02fd5 ***/ + /*** Three byte table, leaf: e5b9xx - offset 0x03056 ***/ /* 80 */ 0x00008eac, 0x00008ead, 0x0000c3dd, 0x00008eae, /* 84 */ 0x0000e1a2, 0x0000b7f9, 0x00008eaf, 0x00008eb0, @@ -3716,7 +3754,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d0d2, 0x00008ed6, 0x0000e7db, 0x0000bbc3, /* bc */ 0x0000d3d7, 0x0000d3c4, 0x00008ed7, 0x0000b9e3, - /*** Three byte table, leaf: e5baxx - offset 0x03015 ***/ + /*** Three byte table, leaf: e5baxx - offset 0x03096 ***/ /* 80 */ 0x0000e2cf, 0x00008ed8, 0x00008ed9, 0x00008eda, /* 84 */ 0x0000d7af, 0x00008edb, 0x0000c7ec, 0x0000b1d3, @@ -3735,7 +3773,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d3b9, 0x0000e2d5, 0x00008ef4, 0x00008ef5, /* bc */ 0x00008ef6, 0x00008ef7, 0x0000e2d7, 0x00008ef8, - /*** Three byte table, leaf: e5bbxx - offset 0x03055 ***/ + /*** Three byte table, leaf: e5bbxx - offset 0x030d6 ***/ /* 80 */ 0x00008ef9, 0x00008efa, 0x00008efb, 0x00008efc, /* 84 */ 0x00008efd, 0x00008efe, 0x00008f40, 0x00008f41, @@ -3754,7 +3792,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008f66, 0x00008f67, 0x0000bda8, 0x00008f68, /* bc */ 0x00008f69, 0x00008f6a, 0x0000dec3, 0x0000d8a5, - /*** Three byte table, leaf: e5bcxx - offset 0x03095 ***/ + /*** Three byte table, leaf: e5bcxx - offset 0x03116 ***/ /* 80 */ 0x0000bfaa, 0x0000dbcd, 0x0000d2ec, 0x0000c6fa, /* 84 */ 0x0000c5aa, 0x00008f6b, 0x00008f6c, 0x00008f6d, @@ -3773,7 +3811,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008f8b, 0x0000b5af, 0x0000c7bf, 0x00008f8c, /* bc */ 0x0000e5f6, 0x00008f8d, 0x00008f8e, 0x00008f8f, - /*** Three byte table, leaf: e5bdxx - offset 0x030d5 ***/ + /*** Three byte table, leaf: e5bdxx - offset 0x03156 ***/ /* 80 */ 0x0000ecb0, 0x00008f90, 0x00008f91, 0x00008f92, /* 84 */ 0x00008f93, 0x00008f94, 0x00008f95, 0x00008f96, @@ -3792,7 +3830,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008fb3, 0x0000d2db, 0x00008fb4, 0x0000b3b9, /* bc */ 0x0000b1cb, 0x00008fb5, 0x00008fb6, 0x00008fb7, - /*** Three byte table, leaf: e5bexx - offset 0x03115 ***/ + /*** Three byte table, leaf: e5bexx - offset 0x03196 ***/ /* 80 */ 0x0000cdf9, 0x0000d5f7, 0x0000e1de, 0x00008fb8, /* 84 */ 0x0000beb6, 0x0000b4fd, 0x00008fb9, 0x0000e1df, @@ -3811,7 +3849,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00008fd7, 0x00008fd8, 0x00008fd9, 0x00008fda, /* bc */ 0x0000e1e8, 0x0000bbd5, 0x00008fdb, 0x00008fdc, - /*** Three byte table, leaf: e5bfxx - offset 0x03155 ***/ + /*** Three byte table, leaf: e5bfxx - offset 0x031d6 ***/ /* 80 */ 0x00008fdd, 0x00008fde, 0x00008fdf, 0x0000d0c4, /* 84 */ 0x0000e2e0, 0x0000b1d8, 0x0000d2e4, 0x00008fe0, @@ -3830,7 +3868,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e2ee, 0x00008ffb, 0x00008ffc, 0x0000d0c3, /* bc */ 0x00008ffd, 0x0000baf6, 0x0000e2e9, 0x0000b7de, - /*** Three byte table, leaf: e680xx - offset 0x03195 ***/ + /*** Three byte table, leaf: e680xx - offset 0x03216 ***/ /* 80 */ 0x0000bbb3, 0x0000ccac, 0x0000cbcb, 0x0000e2e4, /* 84 */ 0x0000e2e6, 0x0000e2ea, 0x0000e2eb, 0x00008ffe, @@ -3849,7 +3887,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009059, 0x0000905a, 0x0000905b, 0x0000d7dc, /* bc */ 0x0000eda1, 0x0000905c, 0x0000905d, 0x0000e2f8, - /*** Three byte table, leaf: e681xx - offset 0x031d5 ***/ + /*** Three byte table, leaf: e681xx - offset 0x03256 ***/ /* 80 */ 0x0000905e, 0x0000eda5, 0x0000e2fe, 0x0000cad1, /* 84 */ 0x0000905f, 0x00009060, 0x00009061, 0x00009062, @@ -3868,7 +3906,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e2fa, 0x0000e2fb, 0x0000e2fd, 0x0000e2fc, /* bc */ 0x0000c4d5, 0x0000e3a2, 0x0000907d, 0x0000d3c1, - /*** Three byte table, leaf: e682xx - offset 0x03215 ***/ + /*** Three byte table, leaf: e682xx - offset 0x03296 ***/ /* 80 */ 0x0000907e, 0x00009080, 0x00009081, 0x0000e3a7, /* 84 */ 0x0000c7c4, 0x00009082, 0x00009083, 0x00009084, @@ -3887,7 +3925,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bcc2, 0x000090a0, 0x000090a1, 0x0000e3ac, /* bc */ 0x0000b5bf, 0x000090a2, 0x000090a3, 0x000090a4, - /*** Three byte table, leaf: e683xx - offset 0x03255 ***/ + /*** Three byte table, leaf: e683xx - offset 0x032d6 ***/ /* 80 */ 0x000090a5, 0x000090a6, 0x000090a7, 0x000090a8, /* 84 */ 0x000090a9, 0x0000c7e9, 0x0000e3b0, 0x000090aa, @@ -3906,7 +3944,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000090c4, 0x0000c8c7, 0x0000d0ca, 0x000090c5, /* bc */ 0x000090c6, 0x000090c7, 0x000090c8, 0x000090c9, - /*** Three byte table, leaf: e684xx - offset 0x03295 ***/ + /*** Three byte table, leaf: e684xx - offset 0x03316 ***/ /* 80 */ 0x0000e3b8, 0x0000b3ee, 0x000090ca, 0x000090cb, /* 84 */ 0x000090cc, 0x000090cd, 0x0000eda9, 0x000090ce, @@ -3925,7 +3963,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000090f1, 0x000090f2, 0x000090f3, 0x000090f4, /* bc */ 0x000090f5, 0x000090f6, 0x000090f7, 0x0000d4b8, - /*** Three byte table, leaf: e685xx - offset 0x032d5 ***/ + /*** Three byte table, leaf: e685xx - offset 0x03356 ***/ /* 80 */ 0x000090f8, 0x000090f9, 0x000090fa, 0x000090fb, /* 84 */ 0x000090fc, 0x000090fd, 0x000090fe, 0x00009140, @@ -3944,7 +3982,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009164, 0x00009165, 0x00009166, 0x00009167, /* bc */ 0x00009168, 0x00009169, 0x0000916a, 0x0000916b, - /*** Three byte table, leaf: e686xx - offset 0x03315 ***/ + /*** Three byte table, leaf: e686xx - offset 0x03396 ***/ /* 80 */ 0x0000916c, 0x0000916d, 0x0000916e, 0x0000916f, /* 84 */ 0x00009170, 0x00009171, 0x00009172, 0x00009173, @@ -3963,7 +4001,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000919c, 0x0000919d, 0x0000919e, 0x0000919f, /* bc */ 0x000091a0, 0x000091a1, 0x0000bab6, 0x000091a2, - /*** Three byte table, leaf: e687xx - offset 0x03355 ***/ + /*** Three byte table, leaf: e687xx - offset 0x033d6 ***/ /* 80 */ 0x000091a3, 0x000091a4, 0x0000b6ae, 0x000091a5, /* 84 */ 0x000091a6, 0x000091a7, 0x000091a8, 0x000091a9, @@ -3982,7 +4020,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000091d2, 0x000091d3, 0x000091d4, 0x000091d5, /* bc */ 0x000091d6, 0x000091d7, 0x000091d8, 0x0000dcb2, - /*** Three byte table, leaf: e688xx - offset 0x03395 ***/ + /*** Three byte table, leaf: e688xx - offset 0x03416 ***/ /* 80 */ 0x000091d9, 0x000091da, 0x000091db, 0x000091dc, /* 84 */ 0x000091dd, 0x000091de, 0x0000edb0, 0x000091df, @@ -4001,7 +4039,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000091f5, 0x000091f6, 0x000091f7, 0x000091f8, /* bc */ 0x000091f9, 0x0000ece6, 0x0000ece5, 0x0000b7bf, - /*** Three byte table, leaf: e689xx - offset 0x033d5 ***/ + /*** Three byte table, leaf: e689xx - offset 0x03456 ***/ /* 80 */ 0x0000cbf9, 0x0000b1e2, 0x000091fa, 0x0000ece7, /* 84 */ 0x000091fb, 0x000091fc, 0x000091fd, 0x0000c9c8, @@ -4020,7 +4058,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009256, 0x0000c5fa, 0x00009257, 0x00009258, /* bc */ 0x0000b6f3, 0x00009259, 0x0000d5d2, 0x0000b3d0, - /*** Three byte table, leaf: e68axx - offset 0x03415 ***/ + /*** Three byte table, leaf: e68axx - offset 0x03496 ***/ /* 80 */ 0x0000bcbc, 0x0000925a, 0x0000925b, 0x0000925c, /* 84 */ 0x0000b3ad, 0x0000925d, 0x0000925e, 0x0000925f, @@ -4039,7 +4077,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000927a, 0x0000c4a8, 0x0000927b, 0x0000ded3, /* bc */ 0x0000d1ba, 0x0000b3e9, 0x0000927c, 0x0000c3f2, - /*** Three byte table, leaf: e68bxx - offset 0x03455 ***/ + /*** Three byte table, leaf: e68bxx - offset 0x034d6 ***/ /* 80 */ 0x0000927d, 0x0000927e, 0x0000b7f7, 0x00009280, /* 84 */ 0x0000d6f4, 0x0000b5a3, 0x0000b2f0, 0x0000c4b4, @@ -4058,7 +4096,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000928f, 0x00009290, 0x00009291, 0x00009292, /* bc */ 0x0000c6b4, 0x0000d7a7, 0x0000cab0, 0x0000c4c3, - /*** Three byte table, leaf: e68cxx - offset 0x03495 ***/ + /*** Three byte table, leaf: e68cxx - offset 0x03516 ***/ /* 80 */ 0x00009293, 0x0000b3d6, 0x0000b9d2, 0x00009294, /* 84 */ 0x00009295, 0x00009296, 0x00009297, 0x0000d6b8, @@ -4077,7 +4115,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000092b3, 0x0000deda, 0x0000cda6, 0x000092b4, /* bc */ 0x000092b5, 0x0000cdec, 0x000092b6, 0x000092b7, - /*** Three byte table, leaf: e68dxx - offset 0x034d5 ***/ + /*** Three byte table, leaf: e68dxx - offset 0x03556 ***/ /* 80 */ 0x000092b8, 0x000092b9, 0x0000cee6, 0x0000dedc, /* 84 */ 0x000092ba, 0x0000cdb1, 0x0000c0a6, 0x000092bb, @@ -4096,7 +4134,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000092d8, 0x000092d9, 0x0000dee0, 0x0000c4ed, /* bc */ 0x000092da, 0x000092db, 0x000092dc, 0x000092dd, - /*** Three byte table, leaf: e68exx - offset 0x03515 ***/ + /*** Three byte table, leaf: e68exx - offset 0x03596 ***/ /* 80 */ 0x0000cfc6, 0x000092de, 0x0000b5e0, 0x000092df, /* 84 */ 0x000092e0, 0x000092e1, 0x000092e2, 0x0000b6de, @@ -4115,7 +4153,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b5a7, 0x000092fa, 0x0000b2f4, 0x000092fb, /* bc */ 0x0000dee8, 0x000092fc, 0x0000def2, 0x000092fd, - /*** Three byte table, leaf: e68fxx - offset 0x03555 ***/ + /*** Three byte table, leaf: e68fxx - offset 0x035d6 ***/ /* 80 */ 0x000092fe, 0x00009340, 0x00009341, 0x00009342, /* 84 */ 0x0000deed, 0x00009343, 0x0000def1, 0x00009344, @@ -4134,7 +4172,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000deea, 0x00009364, 0x00009365, 0x00009366, /* bc */ 0x00009367, 0x0000c0bf, 0x00009368, 0x0000deec, - /*** Three byte table, leaf: e690xx - offset 0x03595 ***/ + /*** Three byte table, leaf: e690xx - offset 0x03616 ***/ /* 80 */ 0x0000b2f3, 0x0000b8e9, 0x0000c2a7, 0x00009369, /* 84 */ 0x0000936a, 0x0000bdc1, 0x0000936b, 0x0000936c, @@ -4153,7 +4191,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000938e, 0x0000938f, 0x0000d0af, 0x00009390, /* bc */ 0x00009391, 0x0000b2eb, 0x00009392, 0x0000eba1, - /*** Three byte table, leaf: e691xx - offset 0x035d5 ***/ + /*** Three byte table, leaf: e691xx - offset 0x03656 ***/ /* 80 */ 0x00009393, 0x0000def4, 0x00009394, 0x00009395, /* 84 */ 0x0000c9e3, 0x0000def3, 0x0000b0da, 0x0000d2a1, @@ -4172,7 +4210,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c3fe, 0x0000c4a1, 0x0000dfa1, 0x000093bd, /* bc */ 0x000093be, 0x000093bf, 0x000093c0, 0x000093c1, - /*** Three byte table, leaf: e692xx - offset 0x03615 ***/ + /*** Three byte table, leaf: e692xx - offset 0x03696 ***/ /* 80 */ 0x000093c2, 0x000093c3, 0x0000c1cc, 0x000093c4, /* 84 */ 0x0000defc, 0x0000beef, 0x000093c5, 0x0000c6b2, @@ -4191,7 +4229,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dfa3, 0x000093e8, 0x0000dfa5, 0x000093e9, /* bc */ 0x0000bab3, 0x000093ea, 0x000093eb, 0x000093ec, - /*** Three byte table, leaf: e693xx - offset 0x03655 ***/ + /*** Three byte table, leaf: e693xx - offset 0x036d6 ***/ /* 80 */ 0x0000dfa6, 0x000093ed, 0x0000c0de, 0x000093ee, /* 84 */ 0x000093ef, 0x0000c9c3, 0x000093f0, 0x000093f1, @@ -4210,7 +4248,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009459, 0x0000945a, 0x0000945b, 0x0000945c, /* bc */ 0x0000945d, 0x0000945e, 0x0000945f, 0x00009460, - /*** Three byte table, leaf: e694xx - offset 0x03695 ***/ + /*** Three byte table, leaf: e694xx - offset 0x03716 ***/ /* 80 */ 0x0000c5ca, 0x00009461, 0x00009462, 0x00009463, /* 84 */ 0x00009464, 0x00009465, 0x00009466, 0x00009467, @@ -4229,7 +4267,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d8fc, 0x0000b8c4, 0x0000948f, 0x0000b9a5, /* bc */ 0x00009490, 0x00009491, 0x0000b7c5, 0x0000d5fe, - /*** Three byte table, leaf: e695xx - offset 0x036d5 ***/ + /*** Three byte table, leaf: e695xx - offset 0x03756 ***/ /* 80 */ 0x00009492, 0x00009493, 0x00009494, 0x00009495, /* 84 */ 0x00009496, 0x0000b9ca, 0x00009497, 0x00009498, @@ -4248,7 +4286,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000094b5, 0x000094b6, 0x000094b7, 0x000094b8, /* bc */ 0x000094b9, 0x000094ba, 0x000094bb, 0x000094bc, - /*** Three byte table, leaf: e696xx - offset 0x03715 ***/ + /*** Three byte table, leaf: e696xx - offset 0x03796 ***/ /* 80 */ 0x000094bd, 0x000094be, 0x000094bf, 0x000094c0, /* 84 */ 0x000094c1, 0x000094c2, 0x000094c3, 0x0000cec4, @@ -4267,7 +4305,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000094e1, 0x0000b7bd, 0x000094e2, 0x000094e3, /* bc */ 0x0000ecb6, 0x0000caa9, 0x000094e4, 0x000094e5, - /*** Three byte table, leaf: e697xx - offset 0x03755 ***/ + /*** Three byte table, leaf: e697xx - offset 0x037d6 ***/ /* 80 */ 0x000094e6, 0x0000c5d4, 0x000094e7, 0x0000ecb9, /* 84 */ 0x0000ecb8, 0x0000c2c3, 0x0000ecb7, 0x000094e8, @@ -4286,7 +4324,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009544, 0x00009545, 0x0000cdfa, 0x00009546, /* bc */ 0x00009547, 0x00009548, 0x00009549, 0x0000954a, - /*** Three byte table, leaf: e698xx - offset 0x03795 ***/ + /*** Three byte table, leaf: e698xx - offset 0x03816 ***/ /* 80 */ 0x0000eac0, 0x0000954b, 0x0000b0ba, 0x0000eabe, /* 84 */ 0x0000954c, 0x0000954d, 0x0000c0a5, 0x0000954e, @@ -4305,7 +4343,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000956b, 0x0000956c, 0x0000956d, 0x0000956e, /* bc */ 0x0000d6e7, 0x0000956f, 0x0000cfd4, 0x00009570, - /*** Three byte table, leaf: e699xx - offset 0x037d5 ***/ + /*** Three byte table, leaf: e699xx - offset 0x03856 ***/ /* 80 */ 0x00009571, 0x0000eacb, 0x00009572, 0x0000bbce, /* 84 */ 0x00009573, 0x00009574, 0x00009575, 0x00009576, @@ -4324,7 +4362,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009593, 0x00009594, 0x0000d6c7, 0x00009595, /* bc */ 0x00009596, 0x00009597, 0x0000c1c0, 0x00009598, - /*** Three byte table, leaf: e69axx - offset 0x03815 ***/ + /*** Three byte table, leaf: e69axx - offset 0x03896 ***/ /* 80 */ 0x00009599, 0x0000959a, 0x0000d4dd, 0x0000959b, /* 84 */ 0x0000ead1, 0x0000959c, 0x0000959d, 0x0000cfbe, @@ -4343,7 +4381,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000095c5, 0x0000e5df, 0x000095c6, 0x000095c7, /* bc */ 0x000095c8, 0x000095c9, 0x0000ead5, 0x000095ca, - /*** Three byte table, leaf: e69bxx - offset 0x03855 ***/ + /*** Three byte table, leaf: e69bxx - offset 0x038d6 ***/ /* 80 */ 0x000095cb, 0x000095cc, 0x000095cd, 0x000095ce, /* 84 */ 0x000095cf, 0x000095d0, 0x000095d1, 0x000095d2, @@ -4362,7 +4400,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000095f8, 0x0000b2dc, 0x000095f9, 0x000095fa, /* bc */ 0x0000c2fc, 0x000095fb, 0x0000d4f8, 0x0000cce6, - /*** Three byte table, leaf: e69cxx - offset 0x03895 ***/ + /*** Three byte table, leaf: e69cxx - offset 0x03916 ***/ /* 80 */ 0x0000d7ee, 0x000095fc, 0x000095fd, 0x000095fe, /* 84 */ 0x00009640, 0x00009641, 0x00009642, 0x00009643, @@ -4381,7 +4419,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000965e, 0x0000965f, 0x0000bbfa, 0x00009660, /* bc */ 0x00009661, 0x0000d0e0, 0x00009662, 0x00009663, - /*** Three byte table, leaf: e69dxx - offset 0x038d5 ***/ + /*** Three byte table, leaf: e69dxx - offset 0x03956 ***/ /* 80 */ 0x0000c9b1, 0x00009664, 0x0000d4d3, 0x0000c8a8, /* 84 */ 0x00009665, 0x00009666, 0x0000b8cb, 0x00009667, @@ -4400,7 +4438,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009680, 0x00009681, 0x00009682, 0x00009683, /* bc */ 0x0000e8cc, 0x00009684, 0x0000cbc9, 0x0000b0e5, - /*** Three byte table, leaf: e69exx - offset 0x03915 ***/ + /*** Three byte table, leaf: e69exx - offset 0x03996 ***/ /* 80 */ 0x00009685, 0x0000bcab, 0x00009686, 0x00009687, /* 84 */ 0x0000b9b9, 0x00009688, 0x00009689, 0x0000e8c1, @@ -4419,7 +4457,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e8db, 0x000096a2, 0x000096a3, 0x000096a4, /* bc */ 0x000096a5, 0x000096a6, 0x000096a7, 0x000096a8, - /*** Three byte table, leaf: e69fxx - offset 0x03955 ***/ + /*** Three byte table, leaf: e69fxx - offset 0x039d6 ***/ /* 80 */ 0x000096a9, 0x0000e8de, 0x000096aa, 0x0000e8da, /* 84 */ 0x0000b1fa, 0x000096ab, 0x000096ac, 0x000096ad, @@ -4438,7 +4476,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000096c8, 0x000096c9, 0x000096ca, 0x000096cb, /* bc */ 0x000096cc, 0x0000e8df, 0x000096cd, 0x0000cac1, - /*** Three byte table, leaf: e6a0xx - offset 0x03995 ***/ + /*** Three byte table, leaf: e6a0xx - offset 0x03a16 ***/ /* 80 */ 0x0000e8d9, 0x000096ce, 0x000096cf, 0x000096d0, /* 84 */ 0x000096d1, 0x0000d5a4, 0x000096d2, 0x0000b1ea, @@ -4457,7 +4495,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bacb, 0x0000b8f9, 0x000096f1, 0x000096f2, /* bc */ 0x0000b8f1, 0x0000d4d4, 0x0000e8ef, 0x000096f3, - /*** Three byte table, leaf: e6a1xx - offset 0x039d5 ***/ + /*** Three byte table, leaf: e6a1xx - offset 0x03a56 ***/ /* 80 */ 0x0000e8ee, 0x0000e8ec, 0x0000b9f0, 0x0000ccd2, /* 84 */ 0x0000e8e6, 0x0000cea6, 0x0000bff2, 0x000096f4, @@ -4476,7 +4514,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000974e, 0x0000974f, 0x00009750, 0x00009751, /* bc */ 0x00009752, 0x00009753, 0x00009754, 0x00009755, - /*** Three byte table, leaf: e6a2xx - offset 0x03a15 ***/ + /*** Three byte table, leaf: e6a2xx - offset 0x03a96 ***/ /* 80 */ 0x00009756, 0x0000c1ba, 0x00009757, 0x0000e8e8, /* 84 */ 0x00009758, 0x0000c3b7, 0x0000b0f0, 0x00009759, @@ -4495,7 +4533,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000977e, 0x00009780, 0x00009781, 0x00009782, /* bc */ 0x00009783, 0x00009784, 0x00009785, 0x00009786, - /*** Three byte table, leaf: e6a3xx - offset 0x03a55 ***/ + /*** Three byte table, leaf: e6a3xx - offset 0x03ad6 ***/ /* 80 */ 0x0000bcec, 0x00009787, 0x0000e8f9, 0x00009788, /* 84 */ 0x00009789, 0x0000978a, 0x0000978b, 0x0000978c, @@ -4514,7 +4552,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000097b0, 0x0000e8fe, 0x0000b9d7, 0x000097b1, /* bc */ 0x0000e8fb, 0x000097b2, 0x000097b3, 0x000097b4, - /*** Three byte table, leaf: e6a4xx - offset 0x03a95 ***/ + /*** Three byte table, leaf: e6a4xx - offset 0x03b16 ***/ /* 80 */ 0x000097b5, 0x0000e9a4, 0x000097b6, 0x000097b7, /* 84 */ 0x000097b8, 0x0000d2ce, 0x000097b9, 0x000097ba, @@ -4533,7 +4571,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000097e0, 0x0000e9a9, 0x000097e1, 0x000097e2, /* bc */ 0x000097e3, 0x0000b4aa, 0x000097e4, 0x0000b4bb, - /*** Three byte table, leaf: e6a5xx - offset 0x03ad5 ***/ + /*** Three byte table, leaf: e6a5xx - offset 0x03b56 ***/ /* 80 */ 0x000097e5, 0x000097e6, 0x0000e9ab, 0x000097e7, /* 84 */ 0x000097e8, 0x000097e9, 0x000097ea, 0x000097eb, @@ -4552,7 +4590,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e9b1, 0x0000e9ba, 0x00009851, 0x00009852, /* bc */ 0x0000c2a5, 0x00009853, 0x00009854, 0x00009855, - /*** Three byte table, leaf: e6a6xx - offset 0x03b15 ***/ + /*** Three byte table, leaf: e6a6xx - offset 0x03b96 ***/ /* 80 */ 0x0000e9af, 0x00009856, 0x0000b8c5, 0x00009857, /* 84 */ 0x0000e9ad, 0x00009858, 0x0000d3dc, 0x0000e9b4, @@ -4571,7 +4609,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000987a, 0x0000987b, 0x0000987c, 0x0000e9bd, /* bc */ 0x0000987d, 0x0000987e, 0x00009880, 0x00009881, - /*** Three byte table, leaf: e6a7xx - offset 0x03b55 ***/ + /*** Three byte table, leaf: e6a7xx - offset 0x03bd6 ***/ /* 80 */ 0x00009882, 0x0000e9c2, 0x00009883, 0x00009884, /* 84 */ 0x00009885, 0x00009886, 0x00009887, 0x00009888, @@ -4590,7 +4628,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000098af, 0x000098b0, 0x000098b1, 0x000098b2, /* bc */ 0x000098b3, 0x0000b2db, 0x000098b4, 0x0000e9c8, - /*** Three byte table, leaf: e6a8xx - offset 0x03b95 ***/ + /*** Three byte table, leaf: e6a8xx - offset 0x03c16 ***/ /* 80 */ 0x000098b5, 0x000098b6, 0x000098b7, 0x000098b8, /* 84 */ 0x000098b9, 0x000098ba, 0x000098bb, 0x000098bc, @@ -4609,7 +4647,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000098e3, 0x000098e4, 0x000098e5, 0x000098e6, /* bc */ 0x000098e7, 0x0000e9d7, 0x0000e9d0, 0x000098e8, - /*** Three byte table, leaf: e6a9xx - offset 0x03bd5 ***/ + /*** Three byte table, leaf: e6a9xx - offset 0x03c56 ***/ /* 80 */ 0x000098e9, 0x000098ea, 0x000098eb, 0x000098ec, /* 84 */ 0x0000e9cf, 0x000098ed, 0x000098ee, 0x0000c7c1, @@ -4628,7 +4666,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009959, 0x0000e9d6, 0x0000995a, 0x0000995b, /* bc */ 0x0000e9da, 0x0000995c, 0x0000995d, 0x0000995e, - /*** Three byte table, leaf: e6aaxx - offset 0x03c15 ***/ + /*** Three byte table, leaf: e6aaxx - offset 0x03c96 ***/ /* 80 */ 0x0000ccb4, 0x0000995f, 0x00009960, 0x00009961, /* 84 */ 0x0000cfad, 0x00009962, 0x00009963, 0x00009964, @@ -4647,7 +4685,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000998e, 0x0000998f, 0x00009990, 0x00009991, /* bc */ 0x00009992, 0x00009993, 0x00009994, 0x00009995, - /*** Three byte table, leaf: e6abxx - offset 0x03c55 ***/ + /*** Three byte table, leaf: e6abxx - offset 0x03cd6 ***/ /* 80 */ 0x00009996, 0x00009997, 0x00009998, 0x00009999, /* 84 */ 0x0000999a, 0x0000999b, 0x0000999c, 0x0000999d, @@ -4666,7 +4704,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x000099ce, 0x000099cf, 0x000099d0, 0x000099d1, /* bc */ 0x000099d2, 0x000099d3, 0x000099d4, 0x000099d5, - /*** Three byte table, leaf: e6acxx - offset 0x03c95 ***/ + /*** Three byte table, leaf: e6acxx - offset 0x03d16 ***/ /* 80 */ 0x000099d6, 0x000099d7, 0x000099d8, 0x000099d9, /* 84 */ 0x000099da, 0x000099db, 0x000099dc, 0x000099dd, @@ -4685,7 +4723,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009a47, 0x0000eca5, 0x0000c6db, 0x00009a48, /* bc */ 0x00009a49, 0x00009a4a, 0x0000bfee, 0x00009a4b, - /*** Three byte table, leaf: e6adxx - offset 0x03cd5 ***/ + /*** Three byte table, leaf: e6adxx - offset 0x03d56 ***/ /* 80 */ 0x00009a4c, 0x00009a4d, 0x00009a4e, 0x0000eca6, /* 84 */ 0x00009a4f, 0x00009a50, 0x0000eca7, 0x0000d0aa, @@ -4704,7 +4742,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009a77, 0x0000b4f5, 0x00009a78, 0x0000cbc0, /* bc */ 0x0000bcdf, 0x00009a79, 0x00009a7a, 0x00009a7b, - /*** Three byte table, leaf: e6aexx - offset 0x03d15 ***/ + /*** Three byte table, leaf: e6aexx - offset 0x03d96 ***/ /* 80 */ 0x00009a7c, 0x0000e9e2, 0x0000e9e3, 0x0000d1ea, /* 84 */ 0x0000e9e5, 0x00009a7d, 0x0000b4f9, 0x0000e9e4, @@ -4723,7 +4761,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009aa0, 0x00009aa1, 0x00009aa2, 0x00009aa3, /* bc */ 0x00009aa4, 0x00009aa5, 0x00009aa6, 0x0000b5ee, - /*** Three byte table, leaf: e6afxx - offset 0x03d55 ***/ + /*** Three byte table, leaf: e6afxx - offset 0x03dd6 ***/ /* 80 */ 0x00009aa7, 0x0000bbd9, 0x0000ecb1, 0x00009aa8, /* 84 */ 0x00009aa9, 0x0000d2e3, 0x00009aaa, 0x00009aab, @@ -4742,7 +4780,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009acb, 0x0000eba8, 0x00009acc, 0x00009acd, /* bc */ 0x00009ace, 0x0000eba6, 0x00009acf, 0x00009ad0, - /*** Three byte table, leaf: e6b0xx - offset 0x03d95 ***/ + /*** Three byte table, leaf: e6b0xx - offset 0x03e16 ***/ /* 80 */ 0x00009ad1, 0x00009ad2, 0x00009ad3, 0x00009ad4, /* 84 */ 0x00009ad5, 0x0000eba9, 0x0000ebab, 0x0000ebaa, @@ -4761,7 +4799,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d3c0, 0x00009aeb, 0x00009aec, 0x00009aed, /* bc */ 0x00009aee, 0x0000d9db, 0x00009aef, 0x00009af0, - /*** Three byte table, leaf: e6b1xx - offset 0x03dd5 ***/ + /*** Three byte table, leaf: e6b1xx - offset 0x03e56 ***/ /* 80 */ 0x0000cda1, 0x0000d6ad, 0x0000c7f3, 0x00009af1, /* 84 */ 0x00009af2, 0x00009af3, 0x0000d9e0, 0x0000bbe3, @@ -4780,7 +4818,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009b50, 0x0000d0da, 0x00009b51, 0x00009b52, /* bc */ 0x00009b53, 0x0000c6fb, 0x0000b7da, 0x00009b54, - /*** Three byte table, leaf: e6b2xx - offset 0x03e15 ***/ + /*** Three byte table, leaf: e6b2xx - offset 0x03e96 ***/ /* 80 */ 0x00009b55, 0x0000c7df, 0x0000d2ca, 0x0000ced6, /* 84 */ 0x00009b56, 0x0000e3e4, 0x0000e3ec, 0x00009b57, @@ -4799,7 +4837,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b7d0, 0x0000d3cd, 0x00009b70, 0x0000d6ce, /* bc */ 0x0000d5d3, 0x0000b9c1, 0x0000d5b4, 0x0000d1d8, - /*** Three byte table, leaf: e6b3xx - offset 0x03e55 ***/ + /*** Three byte table, leaf: e6b3xx - offset 0x03ed6 ***/ /* 80 */ 0x00009b71, 0x00009b72, 0x00009b73, 0x00009b74, /* 84 */ 0x0000d0b9, 0x0000c7f6, 0x00009b75, 0x00009b76, @@ -4818,7 +4856,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e3f2, 0x00009b8d, 0x0000e3f8, 0x0000d0ba, /* bc */ 0x0000c6c3, 0x0000d4f3, 0x0000e3fe, 0x00009b8e, - /*** Three byte table, leaf: e6b4xx - offset 0x03e95 ***/ + /*** Three byte table, leaf: e6b4xx - offset 0x03f16 ***/ /* 80 */ 0x00009b8f, 0x0000bde0, 0x00009b90, 0x00009b91, /* 84 */ 0x0000e4a7, 0x00009b92, 0x00009b93, 0x0000e4a6, @@ -4837,7 +4875,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009bb2, 0x0000e4a1, 0x00009bb3, 0x0000bbee, /* bc */ 0x0000cddd, 0x0000c7a2, 0x0000c5c9, 0x00009bb4, - /*** Three byte table, leaf: e6b5xx - offset 0x03ed5 ***/ + /*** Three byte table, leaf: e6b5xx - offset 0x03f56 ***/ /* 80 */ 0x00009bb5, 0x0000c1f7, 0x00009bb6, 0x0000e4a4, /* 84 */ 0x00009bb7, 0x0000c7b3, 0x0000bdac, 0x0000bdbd, @@ -4856,7 +4894,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bdfe, 0x00009bd1, 0x00009bd2, 0x00009bd3, /* bc */ 0x0000e4bc, 0x00009bd4, 0x00009bd5, 0x00009bd6, - /*** Three byte table, leaf: e6b6xx - offset 0x03f15 ***/ + /*** Three byte table, leaf: e6b6xx - offset 0x03f96 ***/ /* 80 */ 0x00009bd7, 0x00009bd8, 0x0000cdbf, 0x00009bd9, /* 84 */ 0x00009bda, 0x0000c4f9, 0x00009bdb, 0x00009bdc, @@ -4875,7 +4913,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bad4, 0x00009bf3, 0x00009bf4, 0x00009bf5, /* bc */ 0x00009bf6, 0x00009bf7, 0x00009bf8, 0x0000e4c3, - /*** Three byte table, leaf: e6b7xx - offset 0x03f55 ***/ + /*** Three byte table, leaf: e6b7xx - offset 0x03fd6 ***/ /* 80 */ 0x0000b5ed, 0x00009bf9, 0x00009bfa, 0x00009bfb, /* 84 */ 0x0000d7cd, 0x0000e4c0, 0x0000cffd, 0x0000e4bf, @@ -4894,7 +4932,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009c5b, 0x0000d1cd, 0x00009c5c, 0x0000cced, /* bc */ 0x0000edb5, 0x00009c5d, 0x00009c5e, 0x00009c5f, - /*** Three byte table, leaf: e6b8xx - offset 0x03f95 ***/ + /*** Three byte table, leaf: e6b8xx - offset 0x04016 ***/ /* 80 */ 0x00009c60, 0x00009c61, 0x00009c62, 0x00009c63, /* 84 */ 0x00009c64, 0x0000c7e5, 0x00009c65, 0x00009c66, @@ -4913,7 +4951,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d3ce, 0x00009c82, 0x0000c3ec, 0x00009c83, /* bc */ 0x00009c84, 0x00009c85, 0x00009c86, 0x00009c87, - /*** Three byte table, leaf: e6b9xx - offset 0x03fd5 ***/ + /*** Three byte table, leaf: e6b9xx - offset 0x04056 ***/ /* 80 */ 0x00009c88, 0x00009c89, 0x00009c8a, 0x0000c5c8, /* 84 */ 0x0000e4d8, 0x00009c8b, 0x00009c8c, 0x00009c8d, @@ -4932,7 +4970,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009cb4, 0x00009cb5, 0x00009cb6, 0x00009cb7, /* bc */ 0x00009cb8, 0x00009cb9, 0x0000cde5, 0x0000caaa, - /*** Three byte table, leaf: e6baxx - offset 0x04015 ***/ + /*** Three byte table, leaf: e6baxx - offset 0x04096 ***/ /* 80 */ 0x00009cba, 0x00009cbb, 0x00009cbc, 0x0000c0a3, /* 84 */ 0x00009cbd, 0x0000bda6, 0x0000e4d3, 0x00009cbe, @@ -4951,7 +4989,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009cdf, 0x00009ce0, 0x0000c4e7, 0x0000e4e2, /* bc */ 0x00009ce1, 0x0000e4e1, 0x00009ce2, 0x00009ce3, - /*** Three byte table, leaf: e6bbxx - offset 0x04055 ***/ + /*** Three byte table, leaf: e6bbxx - offset 0x040d6 ***/ /* 80 */ 0x00009ce4, 0x0000b3fc, 0x0000e4e8, 0x00009ce5, /* 84 */ 0x00009ce6, 0x00009ce7, 0x00009ce8, 0x0000b5e1, @@ -4970,7 +5008,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009d47, 0x0000e4ef, 0x00009d48, 0x00009d49, /* bc */ 0x00009d4a, 0x00009d4b, 0x00009d4c, 0x00009d4d, - /*** Three byte table, leaf: e6bcxx - offset 0x04095 ***/ + /*** Three byte table, leaf: e6bcxx - offset 0x04116 ***/ /* 80 */ 0x00009d4e, 0x00009d4f, 0x0000c6af, 0x00009d50, /* 84 */ 0x00009d51, 0x00009d52, 0x0000c6e1, 0x00009d53, @@ -4989,7 +5027,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009d75, 0x00009d76, 0x00009d77, 0x00009d78, /* bc */ 0x00009d79, 0x00009d7a, 0x0000d1fa, 0x00009d7b, - /*** Three byte table, leaf: e6bdxx - offset 0x040d5 ***/ + /*** Three byte table, leaf: e6bdxx - offset 0x04156 ***/ /* 80 */ 0x00009d7c, 0x00009d7d, 0x00009d7e, 0x00009d80, /* 84 */ 0x00009d81, 0x00009d82, 0x0000e4eb, 0x0000e4ec, @@ -5008,7 +5046,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e4fa, 0x00009da8, 0x0000e4fd, 0x00009da9, /* bc */ 0x0000e4fc, 0x00009daa, 0x00009dab, 0x00009dac, - /*** Three byte table, leaf: e6bexx - offset 0x04115 ***/ + /*** Three byte table, leaf: e6bexx - offset 0x04196 ***/ /* 80 */ 0x00009dad, 0x00009dae, 0x00009daf, 0x00009db0, /* 84 */ 0x0000b3ce, 0x00009db1, 0x00009db2, 0x00009db3, @@ -5027,7 +5065,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009dda, 0x0000e5a3, 0x00009ddb, 0x00009ddc, /* bc */ 0x00009ddd, 0x00009dde, 0x00009ddf, 0x00009de0, - /*** Three byte table, leaf: e6bfxx - offset 0x04155 ***/ + /*** Three byte table, leaf: e6bfxx - offset 0x041d6 ***/ /* 80 */ 0x0000bca4, 0x00009de1, 0x0000e5a5, 0x00009de2, /* 84 */ 0x00009de3, 0x00009de4, 0x00009de5, 0x00009de6, @@ -5046,7 +5084,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009e50, 0x00009e51, 0x00009e52, 0x00009e53, /* bc */ 0x00009e54, 0x00009e55, 0x00009e56, 0x00009e57, - /*** Three byte table, leaf: e780xx - offset 0x04195 ***/ + /*** Three byte table, leaf: e780xx - offset 0x04216 ***/ /* 80 */ 0x00009e58, 0x00009e59, 0x00009e5a, 0x00009e5b, /* 84 */ 0x00009e5c, 0x00009e5d, 0x00009e5e, 0x00009e5f, @@ -5065,7 +5103,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009e8c, 0x0000e5ae, 0x00009e8d, 0x00009e8e, /* bc */ 0x00009e8f, 0x00009e90, 0x00009e91, 0x00009e92, - /*** Three byte table, leaf: e781xx - offset 0x041d5 ***/ + /*** Three byte table, leaf: e781xx - offset 0x04256 ***/ /* 80 */ 0x00009e93, 0x00009e94, 0x00009e95, 0x00009e96, /* 84 */ 0x00009e97, 0x00009e98, 0x00009e99, 0x00009e9a, @@ -5084,7 +5122,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bec4, 0x00009ec1, 0x00009ec2, 0x00009ec3, /* bc */ 0x0000d7c6, 0x00009ec4, 0x0000d4d6, 0x0000b2d3, - /*** Three byte table, leaf: e782xx - offset 0x04215 ***/ + /*** Three byte table, leaf: e782xx - offset 0x04296 ***/ /* 80 */ 0x0000ecbe, 0x00009ec5, 0x00009ec6, 0x00009ec7, /* 84 */ 0x00009ec8, 0x0000eac1, 0x00009ec9, 0x00009eca, @@ -5103,7 +5141,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d5a8, 0x0000b5e3, 0x00009ee9, 0x0000ecc2, /* bc */ 0x0000c1b6, 0x0000b3e3, 0x00009eea, 0x00009eeb, - /*** Three byte table, leaf: e783xx - offset 0x04255 ***/ + /*** Three byte table, leaf: e783xx - offset 0x042d6 ***/ /* 80 */ 0x0000ecc3, 0x0000cbb8, 0x0000c0c3, 0x0000ccfe, /* 84 */ 0x00009eec, 0x00009eed, 0x00009eee, 0x00009eef, @@ -5122,7 +5160,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009f51, 0x0000c5eb, 0x00009f52, 0x00009f53, /* bc */ 0x00009f54, 0x0000b7e9, 0x00009f55, 0x00009f56, - /*** Three byte table, leaf: e784xx - offset 0x04295 ***/ + /*** Three byte table, leaf: e784xx - offset 0x04316 ***/ /* 80 */ 0x00009f57, 0x00009f58, 0x00009f59, 0x00009f5a, /* 84 */ 0x00009f5b, 0x00009f5c, 0x00009f5d, 0x00009f5e, @@ -5141,7 +5179,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009f82, 0x00009f83, 0x00009f84, 0x00009f85, /* bc */ 0x00009f86, 0x00009f87, 0x00009f88, 0x00009f89, - /*** Three byte table, leaf: e785xx - offset 0x042d5 ***/ + /*** Three byte table, leaf: e785xx - offset 0x04356 ***/ /* 80 */ 0x00009f8a, 0x00009f8b, 0x00009f8c, 0x00009f8d, /* 84 */ 0x00009f8e, 0x0000ecd1, 0x00009f8f, 0x00009f90, @@ -5160,7 +5198,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ecd4, 0x00009fb5, 0x0000ecd5, 0x00009fb6, /* bc */ 0x00009fb7, 0x0000c9bf, 0x00009fb8, 0x00009fb9, - /*** Three byte table, leaf: e786xx - offset 0x04315 ***/ + /*** Three byte table, leaf: e786xx - offset 0x04396 ***/ /* 80 */ 0x00009fba, 0x00009fbb, 0x00009fbc, 0x00009fbd, /* 84 */ 0x0000cfa8, 0x00009fbe, 0x00009fbf, 0x00009fc0, @@ -5179,7 +5217,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x00009fe6, 0x0000ece4, 0x00009fe7, 0x00009fe8, /* bc */ 0x00009fe9, 0x00009fea, 0x00009feb, 0x00009fec, - /*** Three byte table, leaf: e787xx - offset 0x04355 ***/ + /*** Three byte table, leaf: e787xx - offset 0x043d6 ***/ /* 80 */ 0x00009fed, 0x00009fee, 0x00009fef, 0x0000c8bc, /* 84 */ 0x00009ff0, 0x00009ff1, 0x00009ff2, 0x00009ff3, @@ -5198,7 +5236,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a05e, 0x0000ecde, 0x0000a05f, 0x0000a060, /* bc */ 0x0000a061, 0x0000a062, 0x0000a063, 0x0000a064, - /*** Three byte table, leaf: e788xx - offset 0x04395 ***/ + /*** Three byte table, leaf: e788xx - offset 0x04416 ***/ /* 80 */ 0x0000a065, 0x0000a066, 0x0000a067, 0x0000a068, /* 84 */ 0x0000a069, 0x0000a06a, 0x0000b1ac, 0x0000a06b, @@ -5217,7 +5255,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b0d6, 0x0000b5f9, 0x0000a094, 0x0000d8b3, /* bc */ 0x0000a095, 0x0000cbac, 0x0000a096, 0x0000e3dd, - /*** Three byte table, leaf: e789xx - offset 0x043d5 ***/ + /*** Three byte table, leaf: e789xx - offset 0x04456 ***/ /* 80 */ 0x0000a097, 0x0000a098, 0x0000a099, 0x0000a09a, /* 84 */ 0x0000a09b, 0x0000a09c, 0x0000a09d, 0x0000c6ac, @@ -5236,7 +5274,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a0bc, 0x0000ccd8, 0x0000cefe, 0x0000a0bd, /* bc */ 0x0000a0be, 0x0000a0bf, 0x0000eaf5, 0x0000eaf6, - /*** Three byte table, leaf: e78axx - offset 0x04415 ***/ + /*** Three byte table, leaf: e78axx - offset 0x04496 ***/ /* 80 */ 0x0000cfac, 0x0000c0e7, 0x0000a0c0, 0x0000a0c1, /* 84 */ 0x0000eaf7, 0x0000a0c2, 0x0000a0c3, 0x0000a0c4, @@ -5255,7 +5293,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e1ef, 0x0000d3cc, 0x0000a0e8, 0x0000a0e9, /* bc */ 0x0000a0ea, 0x0000a0eb, 0x0000a0ec, 0x0000a0ed, - /*** Three byte table, leaf: e78bxx - offset 0x04455 ***/ + /*** Three byte table, leaf: e78bxx - offset 0x044d6 ***/ /* 80 */ 0x0000a0ee, 0x0000e1f1, 0x0000bff1, 0x0000e1f0, /* 84 */ 0x0000b5d2, 0x0000a0ef, 0x0000a0f0, 0x0000a0f1, @@ -5274,7 +5312,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c0ea, 0x0000aa4d, 0x0000e1fe, 0x0000e2a1, /* bc */ 0x0000c0c7, 0x0000aa4e, 0x0000aa4f, 0x0000aa50, - /*** Three byte table, leaf: e78cxx - offset 0x04495 ***/ + /*** Three byte table, leaf: e78cxx - offset 0x04516 ***/ /* 80 */ 0x0000aa51, 0x0000e1fb, 0x0000aa52, 0x0000e1fd, /* 84 */ 0x0000aa53, 0x0000aa54, 0x0000aa55, 0x0000aa56, @@ -5293,7 +5331,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e2ad, 0x0000e2aa, 0x0000aa72, 0x0000aa73, /* bc */ 0x0000aa74, 0x0000aa75, 0x0000bbab, 0x0000d4b3, - /*** Three byte table, leaf: e78dxx - offset 0x044d5 ***/ + /*** Three byte table, leaf: e78dxx - offset 0x04556 ***/ /* 80 */ 0x0000aa76, 0x0000aa77, 0x0000aa78, 0x0000aa79, /* 84 */ 0x0000aa7a, 0x0000aa7b, 0x0000aa7c, 0x0000aa7d, @@ -5312,7 +5350,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ab46, 0x0000ab47, 0x0000ab48, 0x0000ab49, /* bc */ 0x0000ab4a, 0x0000ab4b, 0x0000e2b5, 0x0000ab4c, - /*** Three byte table, leaf: e78exx - offset 0x04515 ***/ + /*** Three byte table, leaf: e78exx - offset 0x04596 ***/ /* 80 */ 0x0000ab4d, 0x0000ab4e, 0x0000ab4f, 0x0000ab50, /* 84 */ 0x0000d0fe, 0x0000ab51, 0x0000ab52, 0x0000c2ca, @@ -5331,7 +5369,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ab73, 0x0000ab74, 0x0000e7f4, 0x0000b2a3, /* bc */ 0x0000ab75, 0x0000ab76, 0x0000ab77, 0x0000ab78, - /*** Three byte table, leaf: e78fxx - offset 0x04555 ***/ + /*** Three byte table, leaf: e78fxx - offset 0x045d6 ***/ /* 80 */ 0x0000e7ea, 0x0000ab79, 0x0000e7e6, 0x0000ab7a, /* 84 */ 0x0000ab7b, 0x0000ab7c, 0x0000ab7d, 0x0000ab7e, @@ -5350,7 +5388,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ac40, 0x0000ac41, 0x0000ac42, 0x0000ac43, /* bc */ 0x0000ac44, 0x0000ac45, 0x0000ac46, 0x0000ac47, - /*** Three byte table, leaf: e790xx - offset 0x04595 ***/ + /*** Three byte table, leaf: e790xx - offset 0x04616 ***/ /* 80 */ 0x0000ac48, 0x0000ac49, 0x0000ac4a, 0x0000c7f2, /* 84 */ 0x0000ac4b, 0x0000c0c5, 0x0000c0ed, 0x0000ac4c, @@ -5369,7 +5407,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ac6b, 0x0000ac6c, 0x0000ac6d, 0x0000ac6e, /* bc */ 0x0000c7ed, 0x0000ac6f, 0x0000ac70, 0x0000ac71, - /*** Three byte table, leaf: e791xx - offset 0x045d5 ***/ + /*** Three byte table, leaf: e791xx - offset 0x04656 ***/ /* 80 */ 0x0000ac72, 0x0000e8a3, 0x0000ac73, 0x0000ac74, /* 84 */ 0x0000ac75, 0x0000ac76, 0x0000ac77, 0x0000ac78, @@ -5388,7 +5426,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ac9e, 0x0000ac9f, 0x0000aca0, 0x0000ad40, /* bc */ 0x0000ad41, 0x0000ad42, 0x0000e8aa, 0x0000ad43, - /*** Three byte table, leaf: e792xx - offset 0x04615 ***/ + /*** Three byte table, leaf: e792xx - offset 0x04696 ***/ /* 80 */ 0x0000e8ad, 0x0000e8ae, 0x0000ad44, 0x0000c1a7, /* 84 */ 0x0000ad45, 0x0000ad46, 0x0000ad47, 0x0000e8af, @@ -5407,7 +5445,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ad70, 0x0000ad71, 0x0000e8b7, 0x0000ad72, /* bc */ 0x0000ad73, 0x0000ad74, 0x0000ad75, 0x0000ad76, - /*** Three byte table, leaf: e793xx - offset 0x04655 ***/ + /*** Three byte table, leaf: e793xx - offset 0x046d6 ***/ /* 80 */ 0x0000ad77, 0x0000ad78, 0x0000ad79, 0x0000ad7a, /* 84 */ 0x0000ad7b, 0x0000ad7c, 0x0000ad7d, 0x0000ad7e, @@ -5426,7 +5464,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ae42, 0x0000ae43, 0x0000ae44, 0x0000ae45, /* bc */ 0x0000ae46, 0x0000ae47, 0x0000ae48, 0x0000eab3, - /*** Three byte table, leaf: e794xx - offset 0x04695 ***/ + /*** Three byte table, leaf: e794xx - offset 0x04716 ***/ /* 80 */ 0x0000ae49, 0x0000ae4a, 0x0000ae4b, 0x0000ae4c, /* 84 */ 0x0000d5e7, 0x0000ae4d, 0x0000ae4e, 0x0000ae4f, @@ -5445,7 +5483,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b5e9, 0x0000ae6a, 0x0000eeae, 0x0000bbad, /* bc */ 0x0000ae6b, 0x0000ae6c, 0x0000e7de, 0x0000ae6d, - /*** Three byte table, leaf: e795xx - offset 0x046d5 ***/ + /*** Three byte table, leaf: e795xx - offset 0x04756 ***/ /* 80 */ 0x0000eeaf, 0x0000ae6e, 0x0000ae6f, 0x0000ae70, /* 84 */ 0x0000ae71, 0x0000b3a9, 0x0000ae72, 0x0000ae73, @@ -5464,7 +5502,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bbfb, 0x0000eeb5, 0x0000ae96, 0x0000ae97, /* bc */ 0x0000ae98, 0x0000ae99, 0x0000ae9a, 0x0000e7dc, - /*** Three byte table, leaf: e796xx - offset 0x04715 ***/ + /*** Three byte table, leaf: e796xx - offset 0x04796 ***/ /* 80 */ 0x0000ae9b, 0x0000ae9c, 0x0000ae9d, 0x0000eeb6, /* 84 */ 0x0000ae9e, 0x0000ae9f, 0x0000bdae, 0x0000aea0, @@ -5483,7 +5521,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f0e3, 0x0000d5ee, 0x0000af56, 0x0000af57, /* bc */ 0x0000ccdb, 0x0000bed2, 0x0000bcb2, 0x0000af58, - /*** Three byte table, leaf: e797xx - offset 0x04755 ***/ + /*** Three byte table, leaf: e797xx - offset 0x047d6 ***/ /* 80 */ 0x0000af59, 0x0000af5a, 0x0000f0e8, 0x0000f0e7, /* 84 */ 0x0000f0e4, 0x0000b2a1, 0x0000af5b, 0x0000d6a2, @@ -5502,7 +5540,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000af76, 0x0000b1d4, 0x0000af77, 0x0000af78, /* bc */ 0x0000f0f3, 0x0000af79, 0x0000af7a, 0x0000f0f4, - /*** Three byte table, leaf: e798xx - offset 0x04795 ***/ + /*** Three byte table, leaf: e798xx - offset 0x04816 ***/ /* 80 */ 0x0000f0f6, 0x0000b4e1, 0x0000af7b, 0x0000f0f1, /* 84 */ 0x0000af7c, 0x0000f0f7, 0x0000af7d, 0x0000af7e, @@ -5521,7 +5559,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c8b3, 0x0000af9a, 0x0000af9b, 0x0000af9c, /* bc */ 0x0000f1a2, 0x0000af9d, 0x0000f1ab, 0x0000f1a8, - /*** Three byte table, leaf: e799xx - offset 0x047d5 ***/ + /*** Three byte table, leaf: e799xx - offset 0x04856 ***/ /* 80 */ 0x0000f1a5, 0x0000af9e, 0x0000af9f, 0x0000f1aa, /* 84 */ 0x0000afa0, 0x0000b040, 0x0000b041, 0x0000b042, @@ -5540,7 +5578,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b9ef, 0x0000b06a, 0x0000b06b, 0x0000b5c7, /* bc */ 0x0000b06c, 0x0000b0d7, 0x0000b0d9, 0x0000b06d, - /*** Three byte table, leaf: e79axx - offset 0x04815 ***/ + /*** Three byte table, leaf: e79axx - offset 0x04896 ***/ /* 80 */ 0x0000b06e, 0x0000b06f, 0x0000d4ed, 0x0000b070, /* 84 */ 0x0000b5c4, 0x0000b071, 0x0000bdd4, 0x0000bbca, @@ -5559,7 +5597,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b097, 0x0000b098, 0x0000b099, 0x0000b09a, /* bc */ 0x0000b09b, 0x0000b09c, 0x0000b09d, 0x0000c3f3, - /*** Three byte table, leaf: e79bxx - offset 0x04855 ***/ + /*** Three byte table, leaf: e79bxx - offset 0x048d6 ***/ /* 80 */ 0x0000b09e, 0x0000b09f, 0x0000d3db, 0x0000b0a0, /* 84 */ 0x0000b140, 0x0000d6d1, 0x0000c5e8, 0x0000b141, @@ -5578,7 +5616,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cfe0, 0x0000edef, 0x0000b15e, 0x0000b15f, /* bc */ 0x0000c5ce, 0x0000b160, 0x0000b6dc, 0x0000b161, - /*** Three byte table, leaf: e79cxx - offset 0x04895 ***/ + /*** Three byte table, leaf: e79cxx - offset 0x04916 ***/ /* 80 */ 0x0000b162, 0x0000caa1, 0x0000b163, 0x0000b164, /* 84 */ 0x0000eded, 0x0000b165, 0x0000b166, 0x0000edf0, @@ -5597,7 +5635,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000edf8, 0x0000b187, 0x0000ccf7, 0x0000b188, /* bc */ 0x0000d1db, 0x0000b189, 0x0000b18a, 0x0000b18b, - /*** Three byte table, leaf: e79dxx - offset 0x048d5 ***/ + /*** Three byte table, leaf: e79dxx - offset 0x04956 ***/ /* 80 */ 0x0000d7c5, 0x0000d5f6, 0x0000b18c, 0x0000edfc, /* 84 */ 0x0000b18d, 0x0000b18e, 0x0000b18f, 0x0000edfb, @@ -5616,7 +5654,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b253, 0x0000b6c3, 0x0000b254, 0x0000b255, /* bc */ 0x0000b256, 0x0000eea5, 0x0000d8ba, 0x0000eea3, - /*** Three byte table, leaf: e79exx - offset 0x04915 ***/ + /*** Three byte table, leaf: e79exx - offset 0x04996 ***/ /* 80 */ 0x0000eea6, 0x0000b257, 0x0000b258, 0x0000b259, /* 84 */ 0x0000c3e9, 0x0000b3f2, 0x0000b25a, 0x0000b25b, @@ -5635,7 +5673,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b27c, 0x0000b27d, 0x0000b27e, 0x0000d5b0, /* bc */ 0x0000b280, 0x0000eead, 0x0000b281, 0x0000f6c4, - /*** Three byte table, leaf: e79fxx - offset 0x04955 ***/ + /*** Three byte table, leaf: e79fxx - offset 0x049d6 ***/ /* 80 */ 0x0000b282, 0x0000b283, 0x0000b284, 0x0000b285, /* 84 */ 0x0000b286, 0x0000b287, 0x0000b288, 0x0000b289, @@ -5654,7 +5692,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000edb7, 0x0000b34a, 0x0000b34b, 0x0000b34c, /* bc */ 0x0000b34d, 0x0000cef9, 0x0000b7af, 0x0000bff3, - /*** Three byte table, leaf: e7a0xx - offset 0x04995 ***/ + /*** Three byte table, leaf: e7a0xx - offset 0x04a16 ***/ /* 80 */ 0x0000edb8, 0x0000c2eb, 0x0000c9b0, 0x0000b34e, /* 84 */ 0x0000b34f, 0x0000b350, 0x0000b351, 0x0000b352, @@ -5673,7 +5711,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d4d2, 0x0000edc1, 0x0000edc2, 0x0000edc3, /* bc */ 0x0000edc5, 0x0000b36c, 0x0000c0f9, 0x0000b36d, - /*** Three byte table, leaf: e7a1xx - offset 0x049d5 ***/ + /*** Three byte table, leaf: e7a1xx - offset 0x04a56 ***/ /* 80 */ 0x0000b4a1, 0x0000b36e, 0x0000b36f, 0x0000b370, /* 84 */ 0x0000b371, 0x0000b9e8, 0x0000b372, 0x0000edd0, @@ -5692,7 +5730,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b396, 0x0000b397, 0x0000b398, 0x0000b399, /* bc */ 0x0000c5f0, 0x0000b39a, 0x0000b39b, 0x0000b39c, - /*** Three byte table, leaf: e7a2xx - offset 0x04a15 ***/ + /*** Three byte table, leaf: e7a2xx - offset 0x04a96 ***/ /* 80 */ 0x0000b39d, 0x0000b39e, 0x0000b39f, 0x0000b3a0, /* 84 */ 0x0000b440, 0x0000b441, 0x0000b442, 0x0000edd6, @@ -5711,7 +5749,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b45e, 0x0000eddb, 0x0000b45f, 0x0000b460, /* bc */ 0x0000b461, 0x0000b462, 0x0000c4eb, 0x0000b463, - /*** Three byte table, leaf: e7a3xx - offset 0x04a55 ***/ + /*** Three byte table, leaf: e7a3xx - offset 0x04ad6 ***/ /* 80 */ 0x0000b464, 0x0000b4c5, 0x0000b465, 0x0000b466, /* 84 */ 0x0000b467, 0x0000b0f5, 0x0000b468, 0x0000b469, @@ -5730,7 +5768,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b48f, 0x0000b490, 0x0000bbc7, 0x0000b491, /* bc */ 0x0000b492, 0x0000b493, 0x0000b494, 0x0000b495, - /*** Three byte table, leaf: e7a4xx - offset 0x04a95 ***/ + /*** Three byte table, leaf: e7a4xx - offset 0x04b16 ***/ /* 80 */ 0x0000b496, 0x0000bdb8, 0x0000b497, 0x0000b498, /* 84 */ 0x0000b499, 0x0000ede2, 0x0000b49a, 0x0000b49b, @@ -5749,7 +5787,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b567, 0x0000b568, 0x0000cabe, 0x0000ecea, /* bc */ 0x0000c0f1, 0x0000b569, 0x0000c9e7, 0x0000b56a, - /*** Three byte table, leaf: e7a5xx - offset 0x04ad5 ***/ + /*** Three byte table, leaf: e7a5xx - offset 0x04b56 ***/ /* 80 */ 0x0000eceb, 0x0000c6ee, 0x0000b56b, 0x0000b56c, /* 84 */ 0x0000b56d, 0x0000b56e, 0x0000ecec, 0x0000b56f, @@ -5768,7 +5806,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bbf6, 0x0000b58e, 0x0000ecf7, 0x0000b58f, /* bc */ 0x0000b590, 0x0000b591, 0x0000b592, 0x0000b593, - /*** Three byte table, leaf: e7a6xx - offset 0x04b15 ***/ + /*** Three byte table, leaf: e7a6xx - offset 0x04b96 ***/ /* 80 */ 0x0000d9f7, 0x0000bdfb, 0x0000b594, 0x0000b595, /* 84 */ 0x0000c2bb, 0x0000ecf8, 0x0000b596, 0x0000b597, @@ -5787,7 +5825,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b662, 0x0000d3ed, 0x0000d8ae, 0x0000c0eb, /* bc */ 0x0000b663, 0x0000c7dd, 0x0000bacc, 0x0000b664, - /*** Three byte table, leaf: e7a7xx - offset 0x04b55 ***/ + /*** Three byte table, leaf: e7a7xx - offset 0x04bd6 ***/ /* 80 */ 0x0000d0e3, 0x0000cbbd, 0x0000b665, 0x0000cdba, /* 84 */ 0x0000b666, 0x0000b667, 0x0000b8d1, 0x0000b668, @@ -5806,7 +5844,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bdd5, 0x0000b689, 0x0000b68a, 0x0000d2c6, /* bc */ 0x0000b68b, 0x0000bbe0, 0x0000b68c, 0x0000b68d, - /*** Three byte table, leaf: e7a8xx - offset 0x04b95 ***/ + /*** Three byte table, leaf: e7a8xx - offset 0x04c16 ***/ /* 80 */ 0x0000cfa1, 0x0000b68e, 0x0000effc, 0x0000effb, /* 84 */ 0x0000b68f, 0x0000b690, 0x0000eff9, 0x0000b691, @@ -5825,7 +5863,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b756, 0x0000f0a1, 0x0000b757, 0x0000b5be, /* bc */ 0x0000bcda, 0x0000bbfc, 0x0000b758, 0x0000b8e5, - /*** Three byte table, leaf: e7a9xx - offset 0x04bd5 ***/ + /*** Three byte table, leaf: e7a9xx - offset 0x04c56 ***/ /* 80 */ 0x0000b759, 0x0000b75a, 0x0000b75b, 0x0000b75c, /* 84 */ 0x0000b75d, 0x0000b75e, 0x0000c4c2, 0x0000b75f, @@ -5844,7 +5882,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f1b6, 0x0000f1b7, 0x0000bfd5, 0x0000b78b, /* bc */ 0x0000b78c, 0x0000b78d, 0x0000b78e, 0x0000b4a9, - /*** Three byte table, leaf: e7aaxx - offset 0x04c15 ***/ + /*** Three byte table, leaf: e7aaxx - offset 0x04c96 ***/ /* 80 */ 0x0000f1b8, 0x0000cdbb, 0x0000b78f, 0x0000c7d4, /* 84 */ 0x0000d5ad, 0x0000b790, 0x0000f1b9, 0x0000b791, @@ -5863,7 +5901,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b84f, 0x0000b850, 0x0000b851, 0x0000b852, /* bc */ 0x0000b853, 0x0000b854, 0x0000b855, 0x0000c1fe, - /*** Three byte table, leaf: e7abxx - offset 0x04c55 ***/ + /*** Three byte table, leaf: e7abxx - offset 0x04cd6 ***/ /* 80 */ 0x0000b856, 0x0000b857, 0x0000b858, 0x0000b859, /* 84 */ 0x0000b85a, 0x0000b85b, 0x0000b85c, 0x0000b85d, @@ -5882,7 +5920,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b884, 0x0000d6f1, 0x0000f3c3, 0x0000b885, /* bc */ 0x0000b886, 0x0000f3c4, 0x0000b887, 0x0000b8cd, - /*** Three byte table, leaf: e7acxx - offset 0x04c95 ***/ + /*** Three byte table, leaf: e7acxx - offset 0x04d16 ***/ /* 80 */ 0x0000b888, 0x0000b889, 0x0000b88a, 0x0000f3c6, /* 84 */ 0x0000f3c7, 0x0000b88b, 0x0000b0ca, 0x0000b88c, @@ -5901,7 +5939,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f3cd, 0x0000b947, 0x0000bce3, 0x0000b948, /* bc */ 0x0000c1fd, 0x0000b949, 0x0000f3d6, 0x0000b94a, - /*** Three byte table, leaf: e7adxx - offset 0x04cd5 ***/ + /*** Three byte table, leaf: e7adxx - offset 0x04d56 ***/ /* 80 */ 0x0000b94b, 0x0000b94c, 0x0000b94d, 0x0000b94e, /* 84 */ 0x0000b94f, 0x0000f3da, 0x0000b950, 0x0000f3cc, @@ -5920,7 +5958,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b96d, 0x0000b3ef, 0x0000b96e, 0x0000f3e0, /* bc */ 0x0000b96f, 0x0000b970, 0x0000c7a9, 0x0000b971, - /*** Three byte table, leaf: e7aexx - offset 0x04d15 ***/ + /*** Three byte table, leaf: e7aexx - offset 0x04d96 ***/ /* 80 */ 0x0000bcf2, 0x0000b972, 0x0000b973, 0x0000b974, /* 84 */ 0x0000b975, 0x0000f3eb, 0x0000b976, 0x0000b977, @@ -5939,7 +5977,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f3e7, 0x0000b996, 0x0000b997, 0x0000b998, /* bc */ 0x0000b999, 0x0000b99a, 0x0000b99b, 0x0000b99c, - /*** Three byte table, leaf: e7afxx - offset 0x04d55 ***/ + /*** Three byte table, leaf: e7afxx - offset 0x04dd6 ***/ /* 80 */ 0x0000b99d, 0x0000f3f2, 0x0000b99e, 0x0000b99f, /* 84 */ 0x0000b9a0, 0x0000ba40, 0x0000d7ad, 0x0000c6aa, @@ -5958,7 +5996,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ba64, 0x0000ba65, 0x0000ba66, 0x0000ba67, /* bc */ 0x0000f3fb, 0x0000ba68, 0x0000f3fa, 0x0000ba69, - /*** Three byte table, leaf: e7b0xx - offset 0x04d95 ***/ + /*** Three byte table, leaf: e7b0xx - offset 0x04e16 ***/ /* 80 */ 0x0000ba6a, 0x0000ba6b, 0x0000ba6c, 0x0000ba6d, /* 84 */ 0x0000ba6e, 0x0000ba6f, 0x0000ba70, 0x0000b4d8, @@ -5977,7 +6015,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f4a4, 0x0000ba9a, 0x0000ba9b, 0x0000ba9c, /* bc */ 0x0000ba9d, 0x0000ba9e, 0x0000ba9f, 0x0000b2be, - /*** Three byte table, leaf: e7b1xx - offset 0x04dd5 ***/ + /*** Three byte table, leaf: e7b1xx - offset 0x04e56 ***/ /* 80 */ 0x0000f4a6, 0x0000f4a5, 0x0000baa0, 0x0000bb40, /* 84 */ 0x0000bb41, 0x0000bb42, 0x0000bb43, 0x0000bb44, @@ -5996,7 +6034,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bb72, 0x0000bb73, 0x0000bb74, 0x0000c0e0, /* bc */ 0x0000f4cc, 0x0000d7d1, 0x0000bb75, 0x0000bb76, - /*** Three byte table, leaf: e7b2xx - offset 0x04e15 ***/ + /*** Three byte table, leaf: e7b2xx - offset 0x04e96 ***/ /* 80 */ 0x0000bb77, 0x0000bb78, 0x0000bb79, 0x0000bb7a, /* 84 */ 0x0000bb7b, 0x0000bb7c, 0x0000bb7d, 0x0000bb7e, @@ -6015,7 +6053,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bb9e, 0x0000b4e2, 0x0000bb9f, 0x0000bba0, /* bc */ 0x0000f4d4, 0x0000f4d5, 0x0000beab, 0x0000bc40, - /*** Three byte table, leaf: e7b3xx - offset 0x04e55 ***/ + /*** Three byte table, leaf: e7b3xx - offset 0x04ed6 ***/ /* 80 */ 0x0000bc41, 0x0000f4d6, 0x0000bc42, 0x0000bc43, /* 84 */ 0x0000bc44, 0x0000f4db, 0x0000bc45, 0x0000f4d7, @@ -6034,7 +6072,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f4e9, 0x0000bc69, 0x0000bc6a, 0x0000cfb5, /* bc */ 0x0000bc6b, 0x0000bc6c, 0x0000bc6d, 0x0000bc6e, - /*** Three byte table, leaf: e7b4xx - offset 0x04e95 ***/ + /*** Three byte table, leaf: e7b4xx - offset 0x04f16 ***/ /* 80 */ 0x0000bc6f, 0x0000bc70, 0x0000bc71, 0x0000bc72, /* 84 */ 0x0000bc73, 0x0000bc74, 0x0000bc75, 0x0000bc76, @@ -6053,7 +6091,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bd41, 0x0000bd42, 0x0000bd43, 0x0000bd44, /* bc */ 0x0000bd45, 0x0000bd46, 0x0000bd47, 0x0000bd48, - /*** Three byte table, leaf: e7b5xx - offset 0x04ed5 ***/ + /*** Three byte table, leaf: e7b5xx - offset 0x04f56 ***/ /* 80 */ 0x0000bd49, 0x0000bd4a, 0x0000bd4b, 0x0000bd4c, /* 84 */ 0x0000bd4d, 0x0000bd4e, 0x0000bd4f, 0x0000bd50, @@ -6072,7 +6110,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bd80, 0x0000bd81, 0x0000bd82, 0x0000bd83, /* bc */ 0x0000bd84, 0x0000bd85, 0x0000bd86, 0x0000bd87, - /*** Three byte table, leaf: e7b6xx - offset 0x04f15 ***/ + /*** Three byte table, leaf: e7b6xx - offset 0x04f96 ***/ /* 80 */ 0x0000bd88, 0x0000bd89, 0x0000bd8a, 0x0000bd8b, /* 84 */ 0x0000bd8c, 0x0000bd8d, 0x0000bd8e, 0x0000bd8f, @@ -6091,7 +6129,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000be5d, 0x0000be5e, 0x0000be5f, 0x0000be60, /* bc */ 0x0000be61, 0x0000be62, 0x0000be63, 0x0000be64, - /*** Three byte table, leaf: e7b7xx - offset 0x04f55 ***/ + /*** Three byte table, leaf: e7b7xx - offset 0x04fd6 ***/ /* 80 */ 0x0000be65, 0x0000be66, 0x0000be67, 0x0000be68, /* 84 */ 0x0000be69, 0x0000be6a, 0x0000be6b, 0x0000be6c, @@ -6110,7 +6148,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000be9e, 0x0000be9f, 0x0000bea0, 0x0000bf40, /* bc */ 0x0000bf41, 0x0000bf42, 0x0000bf43, 0x0000bf44, - /*** Three byte table, leaf: e7b8xx - offset 0x04f95 ***/ + /*** Three byte table, leaf: e7b8xx - offset 0x05016 ***/ /* 80 */ 0x0000bf45, 0x0000bf46, 0x0000bf47, 0x0000bf48, /* 84 */ 0x0000bf49, 0x0000bf4a, 0x0000bf4b, 0x0000bf4c, @@ -6129,7 +6167,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bf7d, 0x0000bf7e, 0x0000bf80, 0x0000f7e3, /* bc */ 0x0000bf81, 0x0000bf82, 0x0000bf83, 0x0000bf84, - /*** Three byte table, leaf: e7b9xx - offset 0x04fd5 ***/ + /*** Three byte table, leaf: e7b9xx - offset 0x05056 ***/ /* 80 */ 0x0000bf85, 0x0000b7b1, 0x0000bf86, 0x0000bf87, /* 84 */ 0x0000bf88, 0x0000bf89, 0x0000bf8a, 0x0000f4ed, @@ -6148,7 +6186,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c05a, 0x0000c05b, 0x0000c05c, 0x0000c05d, /* bc */ 0x0000c05e, 0x0000c05f, 0x0000c060, 0x0000c061, - /*** Three byte table, leaf: e7baxx - offset 0x05015 ***/ + /*** Three byte table, leaf: e7baxx - offset 0x05096 ***/ /* 80 */ 0x0000c062, 0x0000c063, 0x0000d7eb, 0x0000c064, /* 84 */ 0x0000c065, 0x0000c066, 0x0000c067, 0x0000c068, @@ -6167,7 +6205,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d6bd, 0x0000cec6, 0x0000b7c4, 0x0000c082, /* bc */ 0x0000c083, 0x0000c5a6, 0x0000e7a3, 0x0000cfdf, - /*** Three byte table, leaf: e7bbxx - offset 0x05055 ***/ + /*** Three byte table, leaf: e7bbxx - offset 0x050d6 ***/ /* 80 */ 0x0000e7a4, 0x0000e7a5, 0x0000e7a6, 0x0000c1b7, /* 84 */ 0x0000d7e9, 0x0000c9f0, 0x0000cfb8, 0x0000d6af, @@ -6186,7 +6224,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b3f1, 0x0000c087, 0x0000e7b8, 0x0000e7b9, /* bc */ 0x0000d7db, 0x0000d5c0, 0x0000e7ba, 0x0000c2cc, - /*** Three byte table, leaf: e7bcxx - offset 0x05095 ***/ + /*** Three byte table, leaf: e7bcxx - offset 0x05116 ***/ /* 80 */ 0x0000d7ba, 0x0000e7bb, 0x0000e7bc, 0x0000e7bd, /* 84 */ 0x0000bcea, 0x0000c3e5, 0x0000c0c2, 0x0000e7be, @@ -6205,7 +6243,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b8d7, 0x0000c08c, 0x0000c8b1, 0x0000c08d, /* bc */ 0x0000c08e, 0x0000c08f, 0x0000c090, 0x0000c091, - /*** Three byte table, leaf: e7bdxx - offset 0x050d5 ***/ + /*** Three byte table, leaf: e7bdxx - offset 0x05156 ***/ /* 80 */ 0x0000c092, 0x0000c093, 0x0000f3bf, 0x0000c094, /* 84 */ 0x0000f3c0, 0x0000f3c1, 0x0000c095, 0x0000c096, @@ -6224,7 +6262,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c155, 0x0000eebe, 0x0000c156, 0x0000c157, /* bc */ 0x0000c158, 0x0000c159, 0x0000eec0, 0x0000c15a, - /*** Three byte table, leaf: e7bexx - offset 0x05115 ***/ + /*** Three byte table, leaf: e7bexx - offset 0x05196 ***/ /* 80 */ 0x0000c15b, 0x0000eebf, 0x0000c15c, 0x0000c15d, /* 84 */ 0x0000c15e, 0x0000c15f, 0x0000c160, 0x0000c161, @@ -6243,7 +6281,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d9fa, 0x0000b8fe, 0x0000c185, 0x0000c186, /* bc */ 0x0000e5f1, 0x0000d3f0, 0x0000c187, 0x0000f4e0, - /*** Three byte table, leaf: e7bfxx - offset 0x05155 ***/ + /*** Three byte table, leaf: e7bfxx - offset 0x051d6 ***/ /* 80 */ 0x0000c188, 0x0000cecc, 0x0000c189, 0x0000c18a, /* 84 */ 0x0000c18b, 0x0000b3e1, 0x0000c18c, 0x0000c18d, @@ -6262,7 +6300,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c24d, 0x0000c24e, 0x0000c24f, 0x0000b7ad, /* bc */ 0x0000d2ed, 0x0000c250, 0x0000c251, 0x0000c252, - /*** Three byte table, leaf: e880xx - offset 0x05195 ***/ + /*** Three byte table, leaf: e880xx - offset 0x05216 ***/ /* 80 */ 0x0000d2ab, 0x0000c0cf, 0x0000c253, 0x0000bfbc, /* 84 */ 0x0000eba3, 0x0000d5df, 0x0000eac8, 0x0000c254, @@ -6281,7 +6319,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cbca, 0x0000c26c, 0x0000c26d, 0x0000b3dc, /* bc */ 0x0000c26e, 0x0000b5a2, 0x0000c26f, 0x0000b9a2, - /*** Three byte table, leaf: e881xx - offset 0x051d5 ***/ + /*** Three byte table, leaf: e881xx - offset 0x05256 ***/ /* 80 */ 0x0000c270, 0x0000c271, 0x0000c4f4, 0x0000f1f5, /* 84 */ 0x0000c272, 0x0000c273, 0x0000f1f6, 0x0000c274, @@ -6300,7 +6338,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c29b, 0x0000c29c, 0x0000c29d, 0x0000c29e, /* bc */ 0x0000c29f, 0x0000c2a0, 0x0000c340, 0x0000edb2, - /*** Three byte table, leaf: e882xx - offset 0x05215 ***/ + /*** Three byte table, leaf: e882xx - offset 0x05296 ***/ /* 80 */ 0x0000edb1, 0x0000c341, 0x0000c342, 0x0000cbe0, /* 84 */ 0x0000d2de, 0x0000c343, 0x0000cbc1, 0x0000d5d8, @@ -6319,7 +6357,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c35a, 0x0000c35b, 0x0000b7ce, 0x0000c35c, /* bc */ 0x0000ebc2, 0x0000ebc4, 0x0000c9f6, 0x0000d6d7, - /*** Three byte table, leaf: e883xx - offset 0x05255 ***/ + /*** Three byte table, leaf: e883xx - offset 0x052d6 ***/ /* 80 */ 0x0000d5cd, 0x0000d0b2, 0x0000ebcf, 0x0000ceb8, /* 84 */ 0x0000ebd0, 0x0000c35d, 0x0000b5a8, 0x0000c35e, @@ -6338,7 +6376,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d0d8, 0x0000c373, 0x0000b0b7, 0x0000c374, /* bc */ 0x0000ebdd, 0x0000c4dc, 0x0000c375, 0x0000c376, - /*** Three byte table, leaf: e884xx - offset 0x05295 ***/ + /*** Three byte table, leaf: e884xx - offset 0x05316 ***/ /* 80 */ 0x0000c377, 0x0000c378, 0x0000d6ac, 0x0000c379, /* 84 */ 0x0000c37a, 0x0000c37b, 0x0000b4e0, 0x0000c37c, @@ -6357,7 +6395,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c1b3, 0x0000c39b, 0x0000c39c, 0x0000c39d, /* bc */ 0x0000c39e, 0x0000c39f, 0x0000c6a2, 0x0000c3a0, - /*** Three byte table, leaf: e885xx - offset 0x052d5 ***/ + /*** Three byte table, leaf: e885xx - offset 0x05356 ***/ /* 80 */ 0x0000c440, 0x0000c441, 0x0000c442, 0x0000c443, /* 84 */ 0x0000c444, 0x0000c445, 0x0000ccf3, 0x0000c446, @@ -6376,7 +6414,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c463, 0x0000b8b9, 0x0000cfd9, 0x0000c4e5, /* bc */ 0x0000ebef, 0x0000ebf0, 0x0000ccda, 0x0000cdc8, - /*** Three byte table, leaf: e886xx - offset 0x05315 ***/ + /*** Three byte table, leaf: e886xx - offset 0x05396 ***/ /* 80 */ 0x0000b0f2, 0x0000c464, 0x0000ebf6, 0x0000c465, /* 84 */ 0x0000c466, 0x0000c467, 0x0000c468, 0x0000c469, @@ -6395,7 +6433,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c48e, 0x0000c48f, 0x0000e2df, 0x0000ebfe, /* bc */ 0x0000c490, 0x0000c491, 0x0000c492, 0x0000c493, - /*** Three byte table, leaf: e887xx - offset 0x05355 ***/ + /*** Three byte table, leaf: e887xx - offset 0x053d6 ***/ /* 80 */ 0x0000cdce, 0x0000eca1, 0x0000b1db, 0x0000d3b7, /* 84 */ 0x0000c494, 0x0000c495, 0x0000d2dc, 0x0000c496, @@ -6414,7 +6452,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c55d, 0x0000c55e, 0x0000c55f, 0x0000d5e9, /* bc */ 0x0000beca, 0x0000c560, 0x0000f4a7, 0x0000c561, - /*** Three byte table, leaf: e888xx - offset 0x05395 ***/ + /*** Three byte table, leaf: e888xx - offset 0x05416 ***/ /* 80 */ 0x0000d2a8, 0x0000f4a8, 0x0000f4a9, 0x0000c562, /* 84 */ 0x0000f4aa, 0x0000becb, 0x0000d3df, 0x0000c563, @@ -6433,7 +6471,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f4b4, 0x0000b4ac, 0x0000c57b, 0x0000f4b5, /* bc */ 0x0000c57c, 0x0000c57d, 0x0000f4b8, 0x0000c57e, - /*** Three byte table, leaf: e889xx - offset 0x053d5 ***/ + /*** Three byte table, leaf: e889xx - offset 0x05456 ***/ /* 80 */ 0x0000c580, 0x0000c581, 0x0000c582, 0x0000c583, /* 84 */ 0x0000f4b9, 0x0000c584, 0x0000c585, 0x0000cda7, @@ -6452,7 +6490,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c648, 0x0000dcb3, 0x0000d2d5, 0x0000c649, /* bc */ 0x0000c64a, 0x0000dcb4, 0x0000b0ac, 0x0000dcb5, - /*** Three byte table, leaf: e88axx - offset 0x05415 ***/ + /*** Three byte table, leaf: e88axx - offset 0x05496 ***/ /* 80 */ 0x0000c64b, 0x0000c64c, 0x0000bdda, 0x0000c64d, /* 84 */ 0x0000dcb9, 0x0000c64e, 0x0000c64f, 0x0000c650, @@ -6471,7 +6509,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dcbf, 0x0000c7db, 0x0000c662, 0x0000c663, /* bc */ 0x0000c664, 0x0000d1bf, 0x0000dcc0, 0x0000c665, - /*** Three byte table, leaf: e88bxx - offset 0x05455 ***/ + /*** Three byte table, leaf: e88bxx - offset 0x054d6 ***/ /* 80 */ 0x0000c666, 0x0000dcca, 0x0000c667, 0x0000c668, /* 84 */ 0x0000dcd0, 0x0000c669, 0x0000c66a, 0x0000cead, @@ -6490,7 +6528,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c67e, 0x0000c6bb, 0x0000c680, 0x0000dcde, /* bc */ 0x0000c681, 0x0000c682, 0x0000c683, 0x0000c684, - /*** Three byte table, leaf: e88cxx - offset 0x05495 ***/ + /*** Three byte table, leaf: e88cxx - offset 0x05516 ***/ /* 80 */ 0x0000c685, 0x0000d7c2, 0x0000c3af, 0x0000b7b6, /* 84 */ 0x0000c7d1, 0x0000c3a9, 0x0000dce2, 0x0000dcd8, @@ -6509,7 +6547,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c8d7, 0x0000c8e3, 0x0000dcfb, 0x0000c69f, /* bc */ 0x0000dced, 0x0000c6a0, 0x0000c740, 0x0000c741, - /*** Three byte table, leaf: e88dxx - offset 0x054d5 ***/ + /*** Three byte table, leaf: e88dxx - offset 0x05556 ***/ /* 80 */ 0x0000dcf7, 0x0000c742, 0x0000c743, 0x0000dcf5, /* 84 */ 0x0000c744, 0x0000c745, 0x0000bea3, 0x0000dcf4, @@ -6528,7 +6566,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dda9, 0x0000c75b, 0x0000c75c, 0x0000ddb6, /* bc */ 0x0000ddb1, 0x0000ddb4, 0x0000c75d, 0x0000c75e, - /*** Three byte table, leaf: e88exx - offset 0x05515 ***/ + /*** Three byte table, leaf: e88exx - offset 0x05596 ***/ /* 80 */ 0x0000c75f, 0x0000c760, 0x0000c761, 0x0000c762, /* 84 */ 0x0000c763, 0x0000ddb0, 0x0000c6ce, 0x0000c764, @@ -6547,7 +6585,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ddb5, 0x0000d3a8, 0x0000ddba, 0x0000c782, /* bc */ 0x0000ddbb, 0x0000c3a7, 0x0000c783, 0x0000c784, - /*** Three byte table, leaf: e88fxx - offset 0x05555 ***/ + /*** Three byte table, leaf: e88fxx - offset 0x055d6 ***/ /* 80 */ 0x0000ddd2, 0x0000ddbc, 0x0000c785, 0x0000c786, /* 84 */ 0x0000c787, 0x0000ddd1, 0x0000c788, 0x0000b9bd, @@ -6566,7 +6604,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ddce, 0x0000ddcf, 0x0000c847, 0x0000c848, /* bc */ 0x0000c849, 0x0000ddc4, 0x0000c84a, 0x0000c84b, - /*** Three byte table, leaf: e890xx - offset 0x05595 ***/ + /*** Three byte table, leaf: e890xx - offset 0x05616 ***/ /* 80 */ 0x0000c84c, 0x0000ddbd, 0x0000c84d, 0x0000ddcd, /* 84 */ 0x0000ccd1, 0x0000c84e, 0x0000ddc9, 0x0000c84f, @@ -6585,7 +6623,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ddc7, 0x0000c871, 0x0000c872, 0x0000c873, /* bc */ 0x0000dde0, 0x0000c2e4, 0x0000c874, 0x0000c875, - /*** Three byte table, leaf: e891xx - offset 0x055d5 ***/ + /*** Three byte table, leaf: e891xx - offset 0x05656 ***/ /* 80 */ 0x0000c876, 0x0000c877, 0x0000c878, 0x0000c879, /* 84 */ 0x0000c87a, 0x0000c87b, 0x0000dde1, 0x0000c87c, @@ -6604,7 +6642,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dddf, 0x0000c89e, 0x0000dddd, 0x0000c89f, /* bc */ 0x0000c8a0, 0x0000c940, 0x0000c941, 0x0000c942, - /*** Three byte table, leaf: e892xx - offset 0x05615 ***/ + /*** Three byte table, leaf: e892xx - offset 0x05696 ***/ /* 80 */ 0x0000c943, 0x0000c944, 0x0000b5d9, 0x0000c945, /* 84 */ 0x0000c946, 0x0000c947, 0x0000c948, 0x0000dddb, @@ -6623,7 +6661,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d5f4, 0x0000ddf3, 0x0000ddf0, 0x0000c96d, /* bc */ 0x0000c96e, 0x0000ddec, 0x0000c96f, 0x0000ddef, - /*** Three byte table, leaf: e893xx - offset 0x05655 ***/ + /*** Three byte table, leaf: e893xx - offset 0x056d6 ***/ /* 80 */ 0x0000c970, 0x0000dde8, 0x0000c971, 0x0000c972, /* 84 */ 0x0000d0ee, 0x0000c973, 0x0000c974, 0x0000c975, @@ -6642,7 +6680,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c998, 0x0000c999, 0x0000c99a, 0x0000c99b, /* bc */ 0x0000dea4, 0x0000c99c, 0x0000c99d, 0x0000dea3, - /*** Three byte table, leaf: e894xx - offset 0x05695 ***/ + /*** Three byte table, leaf: e894xx - offset 0x05716 ***/ /* 80 */ 0x0000c99e, 0x0000c99f, 0x0000c9a0, 0x0000ca40, /* 84 */ 0x0000ca41, 0x0000ca42, 0x0000ca43, 0x0000ca44, @@ -6661,7 +6699,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ddfa, 0x0000ddfc, 0x0000ddfe, 0x0000dea2, /* bc */ 0x0000b0aa, 0x0000b1ce, 0x0000ca6b, 0x0000ca6c, - /*** Three byte table, leaf: e895xx - offset 0x056d5 ***/ + /*** Three byte table, leaf: e895xx - offset 0x05756 ***/ /* 80 */ 0x0000ca6d, 0x0000ca6e, 0x0000ca6f, 0x0000deac, /* 84 */ 0x0000ca70, 0x0000ca71, 0x0000ca72, 0x0000ca73, @@ -6680,7 +6718,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ca9b, 0x0000deb3, 0x0000deaa, 0x0000deae, /* bc */ 0x0000ca9c, 0x0000ca9d, 0x0000c0d9, 0x0000ca9e, - /*** Three byte table, leaf: e896xx - offset 0x05715 ***/ + /*** Three byte table, leaf: e896xx - offset 0x05796 ***/ /* 80 */ 0x0000ca9f, 0x0000caa0, 0x0000cb40, 0x0000cb41, /* 84 */ 0x0000b1a1, 0x0000deb6, 0x0000cb42, 0x0000deb1, @@ -6699,7 +6737,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cb69, 0x0000deb7, 0x0000cb6a, 0x0000cb6b, /* bc */ 0x0000cb6c, 0x0000cb6d, 0x0000cb6e, 0x0000cb6f, - /*** Three byte table, leaf: e897xx - offset 0x05755 ***/ + /*** Three byte table, leaf: e897xx - offset 0x057d6 ***/ /* 80 */ 0x0000cb70, 0x0000debb, 0x0000cb71, 0x0000cb72, /* 84 */ 0x0000cb73, 0x0000cb74, 0x0000cb75, 0x0000cb76, @@ -6718,7 +6756,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cba0, 0x0000cc40, 0x0000cc41, 0x0000d4e5, /* bc */ 0x0000cc42, 0x0000cc43, 0x0000cc44, 0x0000debd, - /*** Three byte table, leaf: e898xx - offset 0x05795 ***/ + /*** Three byte table, leaf: e898xx - offset 0x05816 ***/ /* 80 */ 0x0000cc45, 0x0000cc46, 0x0000cc47, 0x0000cc48, /* 84 */ 0x0000cc49, 0x0000debf, 0x0000cc4a, 0x0000cc4b, @@ -6737,7 +6775,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d5ba, 0x0000cc78, 0x0000cc79, 0x0000cc7a, /* bc */ 0x0000dec2, 0x0000cc7b, 0x0000cc7c, 0x0000cc7d, - /*** Three byte table, leaf: e899xx - offset 0x057d5 ***/ + /*** Three byte table, leaf: e899xx - offset 0x05856 ***/ /* 80 */ 0x0000cc7e, 0x0000cc80, 0x0000cc81, 0x0000cc82, /* 84 */ 0x0000cc83, 0x0000cc84, 0x0000cc85, 0x0000cc86, @@ -6756,7 +6794,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cd49, 0x0000bae7, 0x0000f2b3, 0x0000f2b5, /* bc */ 0x0000f2b4, 0x0000cbe4, 0x0000cfba, 0x0000f2b2, - /*** Three byte table, leaf: e89axx - offset 0x05815 ***/ + /*** Three byte table, leaf: e89axx - offset 0x05896 ***/ /* 80 */ 0x0000cab4, 0x0000d2cf, 0x0000c2ec, 0x0000cd4a, /* 84 */ 0x0000cd4b, 0x0000cd4c, 0x0000cd4d, 0x0000cd4e, @@ -6775,7 +6813,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cd6a, 0x0000cd6b, 0x0000f2c5, 0x0000cd6c, /* bc */ 0x0000cd6d, 0x0000cd6e, 0x0000cd6f, 0x0000cd70, - /*** Three byte table, leaf: e89bxx - offset 0x05855 ***/ + /*** Three byte table, leaf: e89bxx - offset 0x058d6 ***/ /* 80 */ 0x0000d6fb, 0x0000cd71, 0x0000cd72, 0x0000cd73, /* 84 */ 0x0000f2c1, 0x0000cd74, 0x0000c7f9, 0x0000c9df, @@ -6794,7 +6832,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f2d9, 0x0000d3bc, 0x0000cd90, 0x0000cd91, /* bc */ 0x0000cd92, 0x0000cd93, 0x0000b6ea, 0x0000cd94, - /*** Three byte table, leaf: e89cxx - offset 0x05895 ***/ + /*** Three byte table, leaf: e89cxx - offset 0x05916 ***/ /* 80 */ 0x0000caf1, 0x0000cd95, 0x0000b7e4, 0x0000f2d7, /* 84 */ 0x0000cd96, 0x0000cd97, 0x0000cd98, 0x0000f2d8, @@ -6813,7 +6851,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ce53, 0x0000ce54, 0x0000ce55, 0x0000f2df, /* bc */ 0x0000ce56, 0x0000ce57, 0x0000f2e4, 0x0000f2ea, - /*** Three byte table, leaf: e89dxx - offset 0x058d5 ***/ + /*** Three byte table, leaf: e89dxx - offset 0x05956 ***/ /* 80 */ 0x0000ce58, 0x0000ce59, 0x0000ce5a, 0x0000ce5b, /* 84 */ 0x0000ce5c, 0x0000ce5d, 0x0000ce5e, 0x0000d3ac, @@ -6832,7 +6870,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ce81, 0x0000ce82, 0x0000ce83, 0x0000f2ef, /* bc */ 0x0000f2f7, 0x0000f2ed, 0x0000f2ee, 0x0000ce84, - /*** Three byte table, leaf: e89exx - offset 0x05915 ***/ + /*** Three byte table, leaf: e89exx - offset 0x05996 ***/ /* 80 */ 0x0000ce85, 0x0000ce86, 0x0000f2eb, 0x0000f3a6, /* 84 */ 0x0000ce87, 0x0000f3a3, 0x0000ce88, 0x0000ce89, @@ -6851,7 +6889,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cf4c, 0x0000cf4d, 0x0000c2dd, 0x0000cf4e, /* bc */ 0x0000cf4f, 0x0000f3ae, 0x0000cf50, 0x0000cf51, - /*** Three byte table, leaf: e89fxx - offset 0x05955 ***/ + /*** Three byte table, leaf: e89fxx - offset 0x059d6 ***/ /* 80 */ 0x0000f3b0, 0x0000cf52, 0x0000cf53, 0x0000cf54, /* 84 */ 0x0000cf55, 0x0000cf56, 0x0000f3a1, 0x0000cf57, @@ -6870,7 +6908,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cf7e, 0x0000d0b7, 0x0000cf80, 0x0000cf81, /* bc */ 0x0000cf82, 0x0000cf83, 0x0000f3b8, 0x0000cf84, - /*** Three byte table, leaf: e8a0xx - offset 0x05995 ***/ + /*** Three byte table, leaf: e8a0xx - offset 0x05a16 ***/ /* 80 */ 0x0000cf85, 0x0000cf86, 0x0000cf87, 0x0000d9f9, /* 84 */ 0x0000cf88, 0x0000cf89, 0x0000cf8a, 0x0000cf8b, @@ -6889,7 +6927,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d053, 0x0000f3bc, 0x0000d054, 0x0000d055, /* bc */ 0x0000f3bd, 0x0000d056, 0x0000d057, 0x0000d058, - /*** Three byte table, leaf: e8a1xx - offset 0x059d5 ***/ + /*** Three byte table, leaf: e8a1xx - offset 0x05a56 ***/ /* 80 */ 0x0000d1aa, 0x0000d059, 0x0000d05a, 0x0000d05b, /* 84 */ 0x0000f4ac, 0x0000d0c6, 0x0000d05c, 0x0000d05d, @@ -6908,7 +6946,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d07c, 0x0000d07d, 0x0000d07e, 0x0000d080, /* bc */ 0x0000d081, 0x0000f1c5, 0x0000f4c0, 0x0000f1c6, - /*** Three byte table, leaf: e8a2xx - offset 0x05a15 ***/ + /*** Three byte table, leaf: e8a2xx - offset 0x05a96 ***/ /* 80 */ 0x0000d082, 0x0000d4ac, 0x0000f1c7, 0x0000d083, /* 84 */ 0x0000b0c0, 0x0000f4c1, 0x0000d084, 0x0000d085, @@ -6927,7 +6965,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d149, 0x0000d14a, 0x0000d14b, 0x0000d14c, /* bc */ 0x0000f1cb, 0x0000d14d, 0x0000d14e, 0x0000d14f, - /*** Three byte table, leaf: e8a3xx - offset 0x05a55 ***/ + /*** Three byte table, leaf: e8a3xx - offset 0x05ad6 ***/ /* 80 */ 0x0000d150, 0x0000b2c3, 0x0000c1d1, 0x0000d151, /* 84 */ 0x0000d152, 0x0000d7b0, 0x0000f1c9, 0x0000d153, @@ -6946,7 +6984,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c2e3, 0x0000b9fc, 0x0000d173, 0x0000d174, /* bc */ 0x0000f1d3, 0x0000d175, 0x0000f1d5, 0x0000d176, - /*** Three byte table, leaf: e8a4xx - offset 0x05a95 ***/ + /*** Three byte table, leaf: e8a4xx - offset 0x05b16 ***/ /* 80 */ 0x0000d177, 0x0000d178, 0x0000b9d3, 0x0000d179, /* 84 */ 0x0000d17a, 0x0000d17b, 0x0000d17c, 0x0000d17d, @@ -6965,7 +7003,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d240, 0x0000d241, 0x0000d242, 0x0000d243, /* bc */ 0x0000d244, 0x0000d245, 0x0000d246, 0x0000d247, - /*** Three byte table, leaf: e8a5xx - offset 0x05ad5 ***/ + /*** Three byte table, leaf: e8a5xx - offset 0x05b56 ***/ /* 80 */ 0x0000d248, 0x0000f1df, 0x0000d249, 0x0000d24a, /* 84 */ 0x0000cfe5, 0x0000d24b, 0x0000d24c, 0x0000d24d, @@ -6984,7 +7022,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d27b, 0x0000d27c, 0x0000d27d, 0x0000f1e1, /* bc */ 0x0000d27e, 0x0000d280, 0x0000d281, 0x0000cef7, - /*** Three byte table, leaf: e8a6xx - offset 0x05b15 ***/ + /*** Three byte table, leaf: e8a6xx - offset 0x05b96 ***/ /* 80 */ 0x0000d282, 0x0000d2aa, 0x0000d283, 0x0000f1fb, /* 84 */ 0x0000d284, 0x0000d285, 0x0000b8b2, 0x0000d286, @@ -7003,7 +7041,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d356, 0x0000d357, 0x0000d358, 0x0000d359, /* bc */ 0x0000d35a, 0x0000d35b, 0x0000d35c, 0x0000d35d, - /*** Three byte table, leaf: e8a7xx - offset 0x05b55 ***/ + /*** Three byte table, leaf: e8a7xx - offset 0x05bd6 ***/ /* 80 */ 0x0000d35e, 0x0000bcfb, 0x0000b9db, 0x0000d35f, /* 84 */ 0x0000b9e6, 0x0000c3d9, 0x0000cad3, 0x0000eae8, @@ -7022,7 +7060,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d37c, 0x0000d37d, 0x0000d37e, 0x0000d380, /* bc */ 0x0000d381, 0x0000d382, 0x0000d383, 0x0000d384, - /*** Three byte table, leaf: e8a8xx - offset 0x05b95 ***/ + /*** Three byte table, leaf: e8a8xx - offset 0x05c16 ***/ /* 80 */ 0x0000d1d4, 0x0000d385, 0x0000d386, 0x0000d387, /* 84 */ 0x0000d388, 0x0000d389, 0x0000d38a, 0x0000d9ea, @@ -7041,7 +7079,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d45a, 0x0000d45b, 0x0000d45c, 0x0000d45d, /* bc */ 0x0000d45e, 0x0000d45f, 0x0000f6a4, 0x0000d460, - /*** Three byte table, leaf: e8a9xx - offset 0x05bd5 ***/ + /*** Three byte table, leaf: e8a9xx - offset 0x05c56 ***/ /* 80 */ 0x0000d461, 0x0000d462, 0x0000d463, 0x0000d464, /* 84 */ 0x0000d465, 0x0000d466, 0x0000d467, 0x0000d468, @@ -7060,7 +7098,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d499, 0x0000d5b2, 0x0000d49a, 0x0000d49b, /* bc */ 0x0000d49c, 0x0000d49d, 0x0000d49e, 0x0000d49f, - /*** Three byte table, leaf: e8aaxx - offset 0x05c15 ***/ + /*** Three byte table, leaf: e8aaxx - offset 0x05c96 ***/ /* 80 */ 0x0000d4a0, 0x0000d540, 0x0000d541, 0x0000d542, /* 84 */ 0x0000d543, 0x0000d544, 0x0000d545, 0x0000d546, @@ -7079,7 +7117,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d574, 0x0000d575, 0x0000d576, 0x0000d577, /* bc */ 0x0000d578, 0x0000d579, 0x0000d57a, 0x0000d57b, - /*** Three byte table, leaf: e8abxx - offset 0x05c55 ***/ + /*** Three byte table, leaf: e8abxx - offset 0x05cd6 ***/ /* 80 */ 0x0000d57c, 0x0000d57d, 0x0000d57e, 0x0000d580, /* 84 */ 0x0000d581, 0x0000d582, 0x0000d583, 0x0000d584, @@ -7098,7 +7136,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d654, 0x0000d655, 0x0000d656, 0x0000d657, /* bc */ 0x0000d658, 0x0000d659, 0x0000d65a, 0x0000d65b, - /*** Three byte table, leaf: e8acxx - offset 0x05c95 ***/ + /*** Three byte table, leaf: e8acxx - offset 0x05d16 ***/ /* 80 */ 0x0000d65c, 0x0000d65d, 0x0000d65e, 0x0000d65f, /* 84 */ 0x0000d660, 0x0000d661, 0x0000d662, 0x0000e5c0, @@ -7117,7 +7155,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d693, 0x0000d694, 0x0000d695, 0x0000d696, /* bc */ 0x0000d697, 0x0000d698, 0x0000d699, 0x0000d69a, - /*** Three byte table, leaf: e8adxx - offset 0x05cd5 ***/ + /*** Three byte table, leaf: e8adxx - offset 0x05d56 ***/ /* 80 */ 0x0000d69b, 0x0000d69c, 0x0000d69d, 0x0000d69e, /* 84 */ 0x0000d69f, 0x0000d6a0, 0x0000d740, 0x0000d741, @@ -7136,7 +7174,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d770, 0x0000d771, 0x0000d772, 0x0000d773, /* bc */ 0x0000d774, 0x0000d775, 0x0000d776, 0x0000d777, - /*** Three byte table, leaf: e8aexx - offset 0x05d15 ***/ + /*** Three byte table, leaf: e8aexx - offset 0x05d96 ***/ /* 80 */ 0x0000d778, 0x0000d779, 0x0000d77a, 0x0000d77b, /* 84 */ 0x0000d77c, 0x0000d77d, 0x0000d77e, 0x0000d780, @@ -7155,7 +7193,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d0ed, 0x0000b6ef, 0x0000c2db, 0x0000d79b, /* bc */ 0x0000cbcf, 0x0000b7ed, 0x0000c9e8, 0x0000b7c3, - /*** Three byte table, leaf: e8afxx - offset 0x05d55 ***/ + /*** Three byte table, leaf: e8afxx - offset 0x05dd6 ***/ /* 80 */ 0x0000bef7, 0x0000d6a4, 0x0000daac, 0x0000daad, /* 84 */ 0x0000c6c0, 0x0000d7e7, 0x0000cab6, 0x0000d79c, @@ -7174,7 +7212,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d6ee, 0x0000dac1, 0x0000c5b5, 0x0000b6c1, /* bc */ 0x0000dac2, 0x0000b7cc, 0x0000bfce, 0x0000dac3, - /*** Three byte table, leaf: e8b0xx - offset 0x05d95 ***/ + /*** Three byte table, leaf: e8b0xx - offset 0x05e16 ***/ /* 80 */ 0x0000dac4, 0x0000cbad, 0x0000dac5, 0x0000b5f7, /* 84 */ 0x0000dac6, 0x0000c1c2, 0x0000d7bb, 0x0000dac7, @@ -7193,7 +7231,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d840, 0x0000d841, 0x0000d842, 0x0000d843, /* bc */ 0x0000d844, 0x0000d845, 0x0000d846, 0x0000d847, - /*** Three byte table, leaf: e8b1xx - offset 0x05dd5 ***/ + /*** Three byte table, leaf: e8b1xx - offset 0x05e56 ***/ /* 80 */ 0x0000d848, 0x0000bbed, 0x0000d849, 0x0000d84a, /* 84 */ 0x0000d84b, 0x0000d84c, 0x0000b6b9, 0x0000f4f8, @@ -7212,7 +7250,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f5f4, 0x0000b1aa, 0x0000b2f2, 0x0000d874, /* bc */ 0x0000d875, 0x0000d876, 0x0000d877, 0x0000d878, - /*** Three byte table, leaf: e8b2xx - offset 0x05e15 ***/ + /*** Three byte table, leaf: e8b2xx - offset 0x05e96 ***/ /* 80 */ 0x0000d879, 0x0000d87a, 0x0000f5f5, 0x0000d87b, /* 84 */ 0x0000d87c, 0x0000f5f7, 0x0000d87d, 0x0000d87e, @@ -7231,7 +7269,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d94a, 0x0000d94b, 0x0000d94c, 0x0000d94d, /* bc */ 0x0000d94e, 0x0000d94f, 0x0000d950, 0x0000d951, - /*** Three byte table, leaf: e8b3xx - offset 0x05e55 ***/ + /*** Three byte table, leaf: e8b3xx - offset 0x05ed6 ***/ /* 80 */ 0x0000d952, 0x0000d953, 0x0000d954, 0x0000d955, /* 84 */ 0x0000d956, 0x0000d957, 0x0000d958, 0x0000d959, @@ -7250,7 +7288,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d98b, 0x0000d98c, 0x0000d98d, 0x0000d98e, /* bc */ 0x0000d98f, 0x0000d990, 0x0000d991, 0x0000d992, - /*** Three byte table, leaf: e8b4xx - offset 0x05e95 ***/ + /*** Three byte table, leaf: e8b4xx - offset 0x05f16 ***/ /* 80 */ 0x0000d993, 0x0000d994, 0x0000d995, 0x0000d996, /* 84 */ 0x0000d997, 0x0000d998, 0x0000d999, 0x0000d99a, @@ -7269,7 +7307,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000c3b3, 0x0000b7d1, 0x0000bad8, 0x0000eadd, /* bc */ 0x0000d4f4, 0x0000eade, 0x0000bcd6, 0x0000bbdf, - /*** Three byte table, leaf: e8b5xx - offset 0x05ed5 ***/ + /*** Three byte table, leaf: e8b5xx - offset 0x05f56 ***/ /* 80 */ 0x0000eadf, 0x0000c1de, 0x0000c2b8, 0x0000d4df, /* 84 */ 0x0000d7ca, 0x0000eae0, 0x0000eae1, 0x0000eae4, @@ -7288,7 +7326,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000da5d, 0x0000da5e, 0x0000da5f, 0x0000da60, /* bc */ 0x0000da61, 0x0000da62, 0x0000da63, 0x0000da64, - /*** Three byte table, leaf: e8b6xx - offset 0x05f15 ***/ + /*** Three byte table, leaf: e8b6xx - offset 0x05f96 ***/ /* 80 */ 0x0000da65, 0x0000b3c3, 0x0000da66, 0x0000da67, /* 84 */ 0x0000f4f2, 0x0000b3ac, 0x0000da68, 0x0000da69, @@ -7307,7 +7345,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f5bb, 0x0000da91, 0x0000f5c3, 0x0000da92, /* bc */ 0x0000f5c2, 0x0000da93, 0x0000d6ba, 0x0000f5c1, - /*** Three byte table, leaf: e8b7xx - offset 0x05f55 ***/ + /*** Three byte table, leaf: e8b7xx - offset 0x05fd6 ***/ /* 80 */ 0x0000da94, 0x0000da95, 0x0000da96, 0x0000d4be, /* 84 */ 0x0000f5c4, 0x0000da97, 0x0000f5cc, 0x0000da98, @@ -7326,7 +7364,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f5cf, 0x0000f5d1, 0x0000b6e5, 0x0000f5d2, /* bc */ 0x0000db52, 0x0000f5d5, 0x0000db53, 0x0000db54, - /*** Three byte table, leaf: e8b8xx - offset 0x05f95 ***/ + /*** Three byte table, leaf: e8b8xx - offset 0x06016 ***/ /* 80 */ 0x0000db55, 0x0000db56, 0x0000db57, 0x0000db58, /* 84 */ 0x0000db59, 0x0000f5bd, 0x0000db5a, 0x0000db5b, @@ -7345,7 +7383,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000db7b, 0x0000f5df, 0x0000f5dd, 0x0000db7c, /* bc */ 0x0000db7d, 0x0000f5e1, 0x0000db7e, 0x0000db80, - /*** Three byte table, leaf: e8b9xx - offset 0x05fd5 ***/ + /*** Three byte table, leaf: e8b9xx - offset 0x06056 ***/ /* 80 */ 0x0000f5de, 0x0000f5e4, 0x0000f5e5, 0x0000db81, /* 84 */ 0x0000cce3, 0x0000db82, 0x0000db83, 0x0000e5bf, @@ -7364,7 +7402,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dc43, 0x0000dc44, 0x0000dc45, 0x0000dc46, /* bc */ 0x0000f5eb, 0x0000dc47, 0x0000dc48, 0x0000b4da, - /*** Three byte table, leaf: e8baxx - offset 0x06015 ***/ + /*** Three byte table, leaf: e8baxx - offset 0x06096 ***/ /* 80 */ 0x0000dc49, 0x0000d4ea, 0x0000dc4a, 0x0000dc4b, /* 84 */ 0x0000dc4c, 0x0000f5ee, 0x0000dc4d, 0x0000b3f9, @@ -7383,7 +7421,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dc75, 0x0000dc76, 0x0000ccc9, 0x0000dc77, /* bc */ 0x0000dc78, 0x0000dc79, 0x0000dc7a, 0x0000dc7b, - /*** Three byte table, leaf: e8bbxx - offset 0x06055 ***/ + /*** Three byte table, leaf: e8bbxx - offset 0x060d6 ***/ /* 80 */ 0x0000dc7c, 0x0000dc7d, 0x0000dc7e, 0x0000dc80, /* 84 */ 0x0000dc81, 0x0000dc82, 0x0000dc83, 0x0000dc84, @@ -7402,7 +7440,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dd53, 0x0000dd54, 0x0000dd55, 0x0000dd56, /* bc */ 0x0000dd57, 0x0000dd58, 0x0000dd59, 0x0000dd5a, - /*** Three byte table, leaf: e8bcxx - offset 0x06095 ***/ + /*** Three byte table, leaf: e8bcxx - offset 0x06116 ***/ /* 80 */ 0x0000dd5b, 0x0000dd5c, 0x0000dd5d, 0x0000dd5e, /* 84 */ 0x0000dd5f, 0x0000dd60, 0x0000dd61, 0x0000dd62, @@ -7421,7 +7459,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dd94, 0x0000dd95, 0x0000dd96, 0x0000dd97, /* bc */ 0x0000dd98, 0x0000dd99, 0x0000dd9a, 0x0000dd9b, - /*** Three byte table, leaf: e8bdxx - offset 0x060d5 ***/ + /*** Three byte table, leaf: e8bdxx - offset 0x06156 ***/ /* 80 */ 0x0000dd9c, 0x0000dd9d, 0x0000dd9e, 0x0000dd9f, /* 84 */ 0x0000dda0, 0x0000de40, 0x0000de41, 0x0000de42, @@ -7440,7 +7478,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e9f4, 0x0000e9f6, 0x0000e9f7, 0x0000c7e1, /* bc */ 0x0000e9f8, 0x0000d4d8, 0x0000e9f9, 0x0000bdce, - /*** Three byte table, leaf: e8bexx - offset 0x06115 ***/ + /*** Three byte table, leaf: e8bexx - offset 0x06196 ***/ /* 80 */ 0x0000de62, 0x0000e9fa, 0x0000e9fb, 0x0000bdcf, /* 84 */ 0x0000e9fc, 0x0000b8a8, 0x0000c1be, 0x0000e9fd, @@ -7459,7 +7497,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000de77, 0x0000b1df, 0x0000de78, 0x0000de79, /* bc */ 0x0000de7a, 0x0000c1c9, 0x0000b4ef, 0x0000de7b, - /*** Three byte table, leaf: e8bfxx - offset 0x06155 ***/ + /*** Three byte table, leaf: e8bfxx - offset 0x061d6 ***/ /* 80 */ 0x0000de7c, 0x0000c7a8, 0x0000d3d8, 0x0000de7d, /* 84 */ 0x0000c6f9, 0x0000d1b8, 0x0000de7e, 0x0000b9fd, @@ -7478,7 +7516,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b1c5, 0x0000bca3, 0x0000de95, 0x0000de96, /* bc */ 0x0000de97, 0x0000d7b7, 0x0000de98, 0x0000de99, - /*** Three byte table, leaf: e980xx - offset 0x06195 ***/ + /*** Three byte table, leaf: e980xx - offset 0x06216 ***/ /* 80 */ 0x0000cdcb, 0x0000cbcd, 0x0000caca, 0x0000ccd3, /* 84 */ 0x0000e5cc, 0x0000e5cb, 0x0000c4e6, 0x0000de9a, @@ -7497,7 +7535,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d2dd, 0x0000df51, 0x0000df52, 0x0000c2df, /* bc */ 0x0000b1c6, 0x0000df53, 0x0000d3e2, 0x0000df54, - /*** Three byte table, leaf: e981xx - offset 0x061d5 ***/ + /*** Three byte table, leaf: e981xx - offset 0x06256 ***/ /* 80 */ 0x0000df55, 0x0000b6dd, 0x0000cbec, 0x0000df56, /* 84 */ 0x0000e5d7, 0x0000df57, 0x0000df58, 0x0000d3f6, @@ -7516,7 +7554,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000df78, 0x0000df79, 0x0000df7a, 0x0000df7b, /* bc */ 0x0000df7c, 0x0000e5e1, 0x0000df7d, 0x0000b1dc, - /*** Three byte table, leaf: e982xx - offset 0x06215 ***/ + /*** Three byte table, leaf: e982xx - offset 0x06296 ***/ /* 80 */ 0x0000d1fb, 0x0000df7e, 0x0000e5e2, 0x0000e5e4, /* 84 */ 0x0000df80, 0x0000df81, 0x0000df82, 0x0000df83, @@ -7535,7 +7573,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000dba1, 0x0000d7de, 0x0000dafe, 0x0000c1da, /* bc */ 0x0000df9d, 0x0000df9e, 0x0000dba5, 0x0000df9f, - /*** Three byte table, leaf: e983xx - offset 0x06255 ***/ + /*** Three byte table, leaf: e983xx - offset 0x062d6 ***/ /* 80 */ 0x0000dfa0, 0x0000d3f4, 0x0000e040, 0x0000e041, /* 84 */ 0x0000dba7, 0x0000dba4, 0x0000e042, 0x0000dba8, @@ -7554,7 +7592,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b5a6, 0x0000e060, 0x0000e061, 0x0000e062, /* bc */ 0x0000e063, 0x0000b6bc, 0x0000dbb1, 0x0000e064, - /*** Three byte table, leaf: e984xx - offset 0x06295 ***/ + /*** Three byte table, leaf: e984xx - offset 0x06316 ***/ /* 80 */ 0x0000e065, 0x0000e066, 0x0000b6f5, 0x0000e067, /* 84 */ 0x0000dbb2, 0x0000e068, 0x0000e069, 0x0000e06a, @@ -7573,7 +7611,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e096, 0x0000dbb8, 0x0000e097, 0x0000e098, /* bc */ 0x0000e099, 0x0000e09a, 0x0000e09b, 0x0000e09c, - /*** Three byte table, leaf: e985xx - offset 0x062d5 ***/ + /*** Three byte table, leaf: e985xx - offset 0x06356 ***/ /* 80 */ 0x0000e09d, 0x0000e09e, 0x0000e09f, 0x0000dbb9, /* 84 */ 0x0000e0a0, 0x0000e140, 0x0000dbba, 0x0000e141, @@ -7592,7 +7630,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000cbe1, 0x0000f5aa, 0x0000e154, 0x0000e155, /* bc */ 0x0000e156, 0x0000f5a6, 0x0000f5a7, 0x0000c4f0, - /*** Three byte table, leaf: e986xx - offset 0x06315 ***/ + /*** Three byte table, leaf: e986xx - offset 0x06396 ***/ /* 80 */ 0x0000e157, 0x0000e158, 0x0000e159, 0x0000e15a, /* 84 */ 0x0000e15b, 0x0000f5ac, 0x0000e15c, 0x0000b4bc, @@ -7611,7 +7649,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e17c, 0x0000e17d, 0x0000f5b8, 0x0000e17e, /* bc */ 0x0000e180, 0x0000e181, 0x0000e182, 0x0000e183, - /*** Three byte table, leaf: e987xx - offset 0x06355 ***/ + /*** Three byte table, leaf: e987xx - offset 0x063d6 ***/ /* 80 */ 0x0000e184, 0x0000e185, 0x0000e186, 0x0000e187, /* 84 */ 0x0000e188, 0x0000e189, 0x0000e18a, 0x0000b2c9, @@ -7630,7 +7668,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e252, 0x0000e253, 0x0000e254, 0x0000e255, /* bc */ 0x0000e256, 0x0000e257, 0x0000e258, 0x0000e259, - /*** Three byte table, leaf: e988xx - offset 0x06395 ***/ + /*** Three byte table, leaf: e988xx - offset 0x06416 ***/ /* 80 */ 0x0000e25a, 0x0000e25b, 0x0000e25c, 0x0000e25d, /* 84 */ 0x0000e25e, 0x0000e25f, 0x0000e260, 0x0000e261, @@ -7649,7 +7687,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e293, 0x0000e294, 0x0000e295, 0x0000e296, /* bc */ 0x0000e297, 0x0000e298, 0x0000e299, 0x0000e29a, - /*** Three byte table, leaf: e989xx - offset 0x063d5 ***/ + /*** Three byte table, leaf: e989xx - offset 0x06456 ***/ /* 80 */ 0x0000e29b, 0x0000e29c, 0x0000e29d, 0x0000e29e, /* 84 */ 0x0000e29f, 0x0000e2a0, 0x0000e340, 0x0000e341, @@ -7668,7 +7706,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e371, 0x0000e372, 0x0000e373, 0x0000e374, /* bc */ 0x0000e375, 0x0000e376, 0x0000e377, 0x0000e378, - /*** Three byte table, leaf: e98axx - offset 0x06415 ***/ + /*** Three byte table, leaf: e98axx - offset 0x06496 ***/ /* 80 */ 0x0000e379, 0x0000e37a, 0x0000e37b, 0x0000e37c, /* 84 */ 0x0000e37d, 0x0000e37e, 0x0000e380, 0x0000e381, @@ -7687,7 +7725,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e44f, 0x0000e450, 0x0000e451, 0x0000e452, /* bc */ 0x0000e453, 0x0000e454, 0x0000e455, 0x0000e456, - /*** Three byte table, leaf: e98bxx - offset 0x06455 ***/ + /*** Three byte table, leaf: e98bxx - offset 0x064d6 ***/ /* 80 */ 0x0000e457, 0x0000e458, 0x0000e459, 0x0000e45a, /* 84 */ 0x0000e45b, 0x0000e45c, 0x0000e45d, 0x0000e45e, @@ -7706,7 +7744,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e48f, 0x0000e490, 0x0000e491, 0x0000e492, /* bc */ 0x0000e493, 0x0000e494, 0x0000e495, 0x0000e496, - /*** Three byte table, leaf: e98cxx - offset 0x06495 ***/ + /*** Three byte table, leaf: e98cxx - offset 0x06516 ***/ /* 80 */ 0x0000e497, 0x0000e498, 0x0000e499, 0x0000e49a, /* 84 */ 0x0000e49b, 0x0000e49c, 0x0000e49d, 0x0000e49e, @@ -7725,7 +7763,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e56e, 0x0000e56f, 0x0000e570, 0x0000e571, /* bc */ 0x0000e572, 0x0000e573, 0x0000f6c9, 0x0000e574, - /*** Three byte table, leaf: e98dxx - offset 0x064d5 ***/ + /*** Three byte table, leaf: e98dxx - offset 0x06556 ***/ /* 80 */ 0x0000e575, 0x0000e576, 0x0000e577, 0x0000e578, /* 84 */ 0x0000e579, 0x0000e57a, 0x0000e57b, 0x0000e57c, @@ -7744,7 +7782,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e64c, 0x0000e64d, 0x0000e64e, 0x0000e64f, /* bc */ 0x0000e650, 0x0000e651, 0x0000e652, 0x0000e653, - /*** Three byte table, leaf: e98exx - offset 0x06515 ***/ + /*** Three byte table, leaf: e98exx - offset 0x06596 ***/ /* 80 */ 0x0000e654, 0x0000e655, 0x0000e656, 0x0000e657, /* 84 */ 0x0000e658, 0x0000e659, 0x0000e65a, 0x0000e65b, @@ -7763,7 +7801,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e68c, 0x0000e68d, 0x0000e68e, 0x0000e68f, /* bc */ 0x0000e690, 0x0000e691, 0x0000e692, 0x0000e693, - /*** Three byte table, leaf: e98fxx - offset 0x06555 ***/ + /*** Three byte table, leaf: e98fxx - offset 0x065d6 ***/ /* 80 */ 0x0000e694, 0x0000e695, 0x0000e696, 0x0000e697, /* 84 */ 0x0000e698, 0x0000e699, 0x0000e69a, 0x0000e69b, @@ -7782,7 +7820,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e769, 0x0000e76a, 0x0000e76b, 0x0000e76c, /* bc */ 0x0000e76d, 0x0000e76e, 0x0000e76f, 0x0000e770, - /*** Three byte table, leaf: e990xx - offset 0x06595 ***/ + /*** Three byte table, leaf: e990xx - offset 0x06616 ***/ /* 80 */ 0x0000e771, 0x0000e772, 0x0000e773, 0x0000e774, /* 84 */ 0x0000e775, 0x0000e776, 0x0000e777, 0x0000e778, @@ -7801,7 +7839,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e849, 0x0000e84a, 0x0000e84b, 0x0000e84c, /* bc */ 0x0000e84d, 0x0000e84e, 0x0000f6cd, 0x0000e84f, - /*** Three byte table, leaf: e991xx - offset 0x065d5 ***/ + /*** Three byte table, leaf: e991xx - offset 0x06656 ***/ /* 80 */ 0x0000e850, 0x0000e851, 0x0000e852, 0x0000e853, /* 84 */ 0x0000e854, 0x0000e855, 0x0000e856, 0x0000e857, @@ -7820,7 +7858,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e888, 0x0000e889, 0x0000e88a, 0x0000e88b, /* bc */ 0x0000e88c, 0x0000e88d, 0x0000e88e, 0x0000e88f, - /*** Three byte table, leaf: e992xx - offset 0x06615 ***/ + /*** Three byte table, leaf: e992xx - offset 0x06696 ***/ /* 80 */ 0x0000e890, 0x0000e891, 0x0000e892, 0x0000e893, /* 84 */ 0x0000e894, 0x0000eec4, 0x0000eec5, 0x0000eec6, @@ -7839,7 +7877,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000eedf, 0x0000eee0, 0x0000eee1, 0x0000d7ea, /* bc */ 0x0000eee2, 0x0000eee3, 0x0000bcd8, 0x0000eee4, - /*** Three byte table, leaf: e993xx - offset 0x06655 ***/ + /*** Three byte table, leaf: e993xx - offset 0x066d6 ***/ /* 80 */ 0x0000d3cb, 0x0000ccfa, 0x0000b2ac, 0x0000c1e5, /* 84 */ 0x0000eee5, 0x0000c7a6, 0x0000c3ad, 0x0000e898, @@ -7858,7 +7896,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d6fd, 0x0000efa9, 0x0000c6cc, 0x0000e89e, /* bc */ 0x0000efaa, 0x0000efab, 0x0000c1b4, 0x0000efac, - /*** Three byte table, leaf: e994xx - offset 0x06695 ***/ + /*** Three byte table, leaf: e994xx - offset 0x06716 ***/ /* 80 */ 0x0000cffa, 0x0000cbf8, 0x0000efae, 0x0000efad, /* 84 */ 0x0000b3fa, 0x0000b9f8, 0x0000efaf, 0x0000efb0, @@ -7877,7 +7915,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000efca, 0x0000c7c2, 0x0000eff1, 0x0000b6cd, /* bc */ 0x0000efcb, 0x0000e942, 0x0000efcc, 0x0000efcd, - /*** Three byte table, leaf: e995xx - offset 0x066d5 ***/ + /*** Three byte table, leaf: e995xx - offset 0x06756 ***/ /* 80 */ 0x0000b6c6, 0x0000c3be, 0x0000efce, 0x0000e943, /* 84 */ 0x0000efd0, 0x0000efd1, 0x0000efd2, 0x0000d5f2, @@ -7896,7 +7934,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e94d, 0x0000e94e, 0x0000e94f, 0x0000e950, /* bc */ 0x0000e951, 0x0000e952, 0x0000e953, 0x0000b3a4, - /*** Three byte table, leaf: e996xx - offset 0x06715 ***/ + /*** Three byte table, leaf: e996xx - offset 0x06796 ***/ /* 80 */ 0x0000e954, 0x0000e955, 0x0000e956, 0x0000e957, /* 84 */ 0x0000e958, 0x0000e959, 0x0000e95a, 0x0000e95b, @@ -7915,7 +7953,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e98d, 0x0000e98e, 0x0000e98f, 0x0000e990, /* bc */ 0x0000e991, 0x0000e992, 0x0000e993, 0x0000e994, - /*** Three byte table, leaf: e997xx - offset 0x06755 ***/ + /*** Three byte table, leaf: e997xx - offset 0x067d6 ***/ /* 80 */ 0x0000e995, 0x0000e996, 0x0000e997, 0x0000e998, /* 84 */ 0x0000e999, 0x0000e99a, 0x0000e99b, 0x0000e99c, @@ -7934,7 +7972,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000d5a2, 0x0000c4d6, 0x0000b9eb, 0x0000cec5, /* bc */ 0x0000e3cb, 0x0000c3f6, 0x0000e3cc, 0x0000ea5d, - /*** Three byte table, leaf: e998xx - offset 0x06795 ***/ + /*** Three byte table, leaf: e998xx - offset 0x06816 ***/ /* 80 */ 0x0000b7a7, 0x0000b8f3, 0x0000bad2, 0x0000e3cd, /* 84 */ 0x0000e3ce, 0x0000d4c4, 0x0000e3cf, 0x0000ea5e, @@ -7953,7 +7991,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ea71, 0x0000ea72, 0x0000ea73, 0x0000d7e8, /* bc */ 0x0000dae8, 0x0000dae7, 0x0000ea74, 0x0000b0a2, - /*** Three byte table, leaf: e999xx - offset 0x067d5 ***/ + /*** Three byte table, leaf: e999xx - offset 0x06856 ***/ /* 80 */ 0x0000cdd3, 0x0000ea75, 0x0000dae9, 0x0000ea76, /* 84 */ 0x0000b8bd, 0x0000bcca, 0x0000c2bd, 0x0000c2a4, @@ -7972,7 +8010,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ea91, 0x0000ea92, 0x0000ea93, 0x0000ea94, /* bc */ 0x0000ea95, 0x0000ea96, 0x0000ea97, 0x0000ea98, - /*** Three byte table, leaf: e99axx - offset 0x06815 ***/ + /*** Three byte table, leaf: e99axx - offset 0x06896 ***/ /* 80 */ 0x0000ea99, 0x0000ea9a, 0x0000ea9b, 0x0000ea9c, /* 84 */ 0x0000ea9d, 0x0000d3e7, 0x0000c2a1, 0x0000ea9e, @@ -7991,7 +8029,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000eb60, 0x0000f6bf, 0x0000eb61, 0x0000eb62, /* bc */ 0x0000f6c0, 0x0000f6c1, 0x0000c4d1, 0x0000eb63, - /*** Three byte table, leaf: e99bxx - offset 0x06855 ***/ + /*** Three byte table, leaf: e99bxx - offset 0x068d6 ***/ /* 80 */ 0x0000c8b8, 0x0000d1e3, 0x0000eb64, 0x0000eb65, /* 84 */ 0x0000d0db, 0x0000d1c5, 0x0000bcaf, 0x0000b9cd, @@ -8010,7 +8048,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000eb88, 0x0000b1a2, 0x0000eb89, 0x0000eb8a, /* bc */ 0x0000eb8b, 0x0000eb8c, 0x0000ceed, 0x0000eb8d, - /*** Three byte table, leaf: e99cxx - offset 0x06895 ***/ + /*** Three byte table, leaf: e99cxx - offset 0x06916 ***/ /* 80 */ 0x0000d0e8, 0x0000f6ab, 0x0000eb8e, 0x0000eb8f, /* 84 */ 0x0000cff6, 0x0000eb90, 0x0000f6aa, 0x0000d5f0, @@ -8029,7 +8067,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000b0d4, 0x0000c5f9, 0x0000ec53, 0x0000ec54, /* bc */ 0x0000ec55, 0x0000ec56, 0x0000f6b2, 0x0000ec57, - /*** Three byte table, leaf: e99dxx - offset 0x068d5 ***/ + /*** Three byte table, leaf: e99dxx - offset 0x06956 ***/ /* 80 */ 0x0000ec58, 0x0000ec59, 0x0000ec5a, 0x0000ec5b, /* 84 */ 0x0000ec5c, 0x0000ec5d, 0x0000ec5e, 0x0000ec5f, @@ -8048,7 +8086,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ec83, 0x0000ec84, 0x0000ec85, 0x0000ec86, /* bc */ 0x0000f7b0, 0x0000ec87, 0x0000ec88, 0x0000ec89, - /*** Three byte table, leaf: e99exx - offset 0x06915 ***/ + /*** Three byte table, leaf: e99exx - offset 0x06996 ***/ /* 80 */ 0x0000ec8a, 0x0000ec8b, 0x0000ec8c, 0x0000ec8d, /* 84 */ 0x0000ec8e, 0x0000f7b1, 0x0000ec8f, 0x0000ec90, @@ -8067,7 +8105,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ed53, 0x0000ed54, 0x0000ed55, 0x0000ed56, /* bc */ 0x0000ed57, 0x0000ed58, 0x0000ed59, 0x0000ed5a, - /*** Three byte table, leaf: e99fxx - offset 0x06955 ***/ + /*** Three byte table, leaf: e99fxx - offset 0x069d6 ***/ /* 80 */ 0x0000ed5b, 0x0000ed5c, 0x0000ed5d, 0x0000ed5e, /* 84 */ 0x0000ed5f, 0x0000ed60, 0x0000ed61, 0x0000ed62, @@ -8086,7 +8124,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ed8a, 0x0000ed8b, 0x0000ed8c, 0x0000ed8d, /* bc */ 0x0000ed8e, 0x0000ed8f, 0x0000ed90, 0x0000ed91, - /*** Three byte table, leaf: e9a0xx - offset 0x06995 ***/ + /*** Three byte table, leaf: e9a0xx - offset 0x06a16 ***/ /* 80 */ 0x0000ed92, 0x0000ed93, 0x0000ed94, 0x0000ed95, /* 84 */ 0x0000ed96, 0x0000ed97, 0x0000ed98, 0x0000ed99, @@ -8105,7 +8143,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ee69, 0x0000ee6a, 0x0000ee6b, 0x0000ee6c, /* bc */ 0x0000ee6d, 0x0000ee6e, 0x0000ee6f, 0x0000ee70, - /*** Three byte table, leaf: e9a1xx - offset 0x069d5 ***/ + /*** Three byte table, leaf: e9a1xx - offset 0x06a56 ***/ /* 80 */ 0x0000ee71, 0x0000ee72, 0x0000ee73, 0x0000ee74, /* 84 */ 0x0000ee75, 0x0000ee76, 0x0000ee77, 0x0000ee78, @@ -8124,7 +8162,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f1fc, 0x0000cfee, 0x0000cbb3, 0x0000d0eb, /* bc */ 0x0000e7ef, 0x0000cde7, 0x0000b9cb, 0x0000b6d9, - /*** Three byte table, leaf: e9a2xx - offset 0x06a15 ***/ + /*** Three byte table, leaf: e9a2xx - offset 0x06a96 ***/ /* 80 */ 0x0000f1fd, 0x0000b0e4, 0x0000cbcc, 0x0000f1fe, /* 84 */ 0x0000d4a4, 0x0000c2ad, 0x0000c1ec, 0x0000c6c4, @@ -8143,7 +8181,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ef5c, 0x0000ef5d, 0x0000ef5e, 0x0000ef5f, /* bc */ 0x0000ef60, 0x0000ef61, 0x0000ef62, 0x0000ef63, - /*** Three byte table, leaf: e9a3xx - offset 0x06a55 ***/ + /*** Three byte table, leaf: e9a3xx - offset 0x06ad6 ***/ /* 80 */ 0x0000ef64, 0x0000ef65, 0x0000ef66, 0x0000ef67, /* 84 */ 0x0000ef68, 0x0000ef69, 0x0000ef6a, 0x0000ef6b, @@ -8162,7 +8200,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000ef91, 0x0000ef92, 0x0000ef93, 0x0000ef94, /* bc */ 0x0000ef95, 0x0000ef96, 0x0000ef97, 0x0000ef98, - /*** Three byte table, leaf: e9a4xx - offset 0x06a95 ***/ + /*** Three byte table, leaf: e9a4xx - offset 0x06b16 ***/ /* 80 */ 0x0000ef99, 0x0000ef9a, 0x0000ef9b, 0x0000ef9c, /* 84 */ 0x0000ef9d, 0x0000ef9e, 0x0000ef9f, 0x0000efa0, @@ -8181,7 +8219,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f06d, 0x0000f06e, 0x0000f06f, 0x0000f070, /* bc */ 0x0000f071, 0x0000f072, 0x0000f073, 0x0000f074, - /*** Three byte table, leaf: e9a5xx - offset 0x06ad5 ***/ + /*** Three byte table, leaf: e9a5xx - offset 0x06b56 ***/ /* 80 */ 0x0000f075, 0x0000f076, 0x0000f077, 0x0000f078, /* 84 */ 0x0000f079, 0x0000f07a, 0x0000f07b, 0x0000f07c, @@ -8200,7 +8238,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f09a, 0x0000f09b, 0x0000bdc8, 0x0000f09c, /* bc */ 0x0000b1fd, 0x0000e2c4, 0x0000f09d, 0x0000b6f6, - /*** Three byte table, leaf: e9a6xx - offset 0x06b15 ***/ + /*** Three byte table, leaf: e9a6xx - offset 0x06b96 ***/ /* 80 */ 0x0000e2c5, 0x0000c4d9, 0x0000f09e, 0x0000f09f, /* 84 */ 0x0000e2c6, 0x0000cfda, 0x0000b9dd, 0x0000e2c7, @@ -8219,7 +8257,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f15e, 0x0000f15f, 0x0000f160, 0x0000f161, /* bc */ 0x0000f162, 0x0000f163, 0x0000f164, 0x0000f165, - /*** Three byte table, leaf: e9a7xx - offset 0x06b55 ***/ + /*** Three byte table, leaf: e9a7xx - offset 0x06bd6 ***/ /* 80 */ 0x0000f166, 0x0000f167, 0x0000f168, 0x0000f169, /* 84 */ 0x0000f16a, 0x0000f16b, 0x0000f16c, 0x0000f16d, @@ -8238,7 +8276,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f19f, 0x0000f1a0, 0x0000f240, 0x0000f241, /* bc */ 0x0000f242, 0x0000f243, 0x0000f244, 0x0000f245, - /*** Three byte table, leaf: e9a8xx - offset 0x06b95 ***/ + /*** Three byte table, leaf: e9a8xx - offset 0x06c16 ***/ /* 80 */ 0x0000f246, 0x0000f247, 0x0000f248, 0x0000f249, /* 84 */ 0x0000f24a, 0x0000f24b, 0x0000f24c, 0x0000f24d, @@ -8257,7 +8295,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f27e, 0x0000f280, 0x0000f281, 0x0000f282, /* bc */ 0x0000f283, 0x0000f284, 0x0000f285, 0x0000f286, - /*** Three byte table, leaf: e9a9xx - offset 0x06bd5 ***/ + /*** Three byte table, leaf: e9a9xx - offset 0x06c56 ***/ /* 80 */ 0x0000f287, 0x0000f288, 0x0000f289, 0x0000f28a, /* 84 */ 0x0000f28b, 0x0000f28c, 0x0000f28d, 0x0000f28e, @@ -8276,7 +8314,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000e6e2, 0x0000bed4, 0x0000e6e3, 0x0000d7a4, /* bc */ 0x0000cdd5, 0x0000e6e5, 0x0000bcdd, 0x0000e6e4, - /*** Three byte table, leaf: e9aaxx - offset 0x06c15 ***/ + /*** Three byte table, leaf: e9aaxx - offset 0x06c96 ***/ /* 80 */ 0x0000e6e6, 0x0000e6e7, 0x0000c2ee, 0x0000f353, /* 84 */ 0x0000bdbe, 0x0000e6e8, 0x0000c2e6, 0x0000baa7, @@ -8295,7 +8333,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000baa1, 0x0000f366, 0x0000f7bf, 0x0000f367, /* bc */ 0x0000f7c0, 0x0000f368, 0x0000f369, 0x0000f36a, - /*** Three byte table, leaf: e9abxx - offset 0x06c55 ***/ + /*** Three byte table, leaf: e9abxx - offset 0x06cd6 ***/ /* 80 */ 0x0000f7c2, 0x0000f7c1, 0x0000f7c4, 0x0000f36b, /* 84 */ 0x0000f36c, 0x0000f7c3, 0x0000f36d, 0x0000f36e, @@ -8314,7 +8352,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f395, 0x0000f7db, 0x0000f396, 0x0000f7d9, /* bc */ 0x0000f397, 0x0000f398, 0x0000f399, 0x0000f39a, - /*** Three byte table, leaf: e9acxx - offset 0x06c95 ***/ + /*** Three byte table, leaf: e9acxx - offset 0x06d16 ***/ /* 80 */ 0x0000f39b, 0x0000f39c, 0x0000f39d, 0x0000d7d7, /* 84 */ 0x0000f39e, 0x0000f39f, 0x0000f3a0, 0x0000f440, @@ -8333,7 +8371,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f46a, 0x0000f46b, 0x0000f46c, 0x0000e5f7, /* bc */ 0x0000b9ed, 0x0000f46d, 0x0000f46e, 0x0000f46f, - /*** Three byte table, leaf: e9adxx - offset 0x06cd5 ***/ + /*** Three byte table, leaf: e9adxx - offset 0x06d56 ***/ /* 80 */ 0x0000f470, 0x0000bffd, 0x0000bbea, 0x0000f7c9, /* 84 */ 0x0000c6c7, 0x0000f7c8, 0x0000f471, 0x0000f7ca, @@ -8352,7 +8390,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f49d, 0x0000f49e, 0x0000f49f, 0x0000f4a0, /* bc */ 0x0000f540, 0x0000f541, 0x0000f542, 0x0000f543, - /*** Three byte table, leaf: e9aexx - offset 0x06d15 ***/ + /*** Three byte table, leaf: e9aexx - offset 0x06d96 ***/ /* 80 */ 0x0000f544, 0x0000f545, 0x0000f546, 0x0000f547, /* 84 */ 0x0000f548, 0x0000f549, 0x0000f54a, 0x0000f54b, @@ -8371,7 +8409,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f57c, 0x0000f57d, 0x0000f57e, 0x0000f580, /* bc */ 0x0000f581, 0x0000f582, 0x0000f583, 0x0000f584, - /*** Three byte table, leaf: e9afxx - offset 0x06d55 ***/ + /*** Three byte table, leaf: e9afxx - offset 0x06dd6 ***/ /* 80 */ 0x0000f585, 0x0000f586, 0x0000f587, 0x0000f588, /* 84 */ 0x0000f589, 0x0000f58a, 0x0000f58b, 0x0000f58c, @@ -8390,7 +8428,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f65c, 0x0000f65d, 0x0000f65e, 0x0000f65f, /* bc */ 0x0000f660, 0x0000f661, 0x0000f662, 0x0000f663, - /*** Three byte table, leaf: e9b0xx - offset 0x06d95 ***/ + /*** Three byte table, leaf: e9b0xx - offset 0x06e16 ***/ /* 80 */ 0x0000f664, 0x0000f665, 0x0000f666, 0x0000f667, /* 84 */ 0x0000f668, 0x0000f669, 0x0000f66a, 0x0000f66b, @@ -8409,7 +8447,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f69d, 0x0000f69e, 0x0000f69f, 0x0000f6a0, /* bc */ 0x0000f740, 0x0000f741, 0x0000f742, 0x0000f743, - /*** Three byte table, leaf: e9b1xx - offset 0x06dd5 ***/ + /*** Three byte table, leaf: e9b1xx - offset 0x06e56 ***/ /* 80 */ 0x0000f744, 0x0000f745, 0x0000f746, 0x0000f747, /* 84 */ 0x0000f748, 0x0000f749, 0x0000f74a, 0x0000f74b, @@ -8428,7 +8466,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f77c, 0x0000f77d, 0x0000f77e, 0x0000f780, /* bc */ 0x0000d3e3, 0x0000f781, 0x0000f782, 0x0000f6cf, - /*** Three byte table, leaf: e9b2xx - offset 0x06e15 ***/ + /*** Three byte table, leaf: e9b2xx - offset 0x06e96 ***/ /* 80 */ 0x0000f783, 0x0000c2b3, 0x0000f6d0, 0x0000f784, /* 84 */ 0x0000f785, 0x0000f6d1, 0x0000f6d2, 0x0000f6d3, @@ -8447,7 +8485,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000bea8, 0x0000f793, 0x0000f6f5, 0x0000f6f6, /* bc */ 0x0000f6f7, 0x0000f6f8, 0x0000f794, 0x0000f795, - /*** Three byte table, leaf: e9b3xx - offset 0x06e55 ***/ + /*** Three byte table, leaf: e9b3xx - offset 0x06ed6 ***/ /* 80 */ 0x0000f796, 0x0000f797, 0x0000f798, 0x0000c8fa, /* 84 */ 0x0000f6f9, 0x0000f6fa, 0x0000f6fb, 0x0000f6fc, @@ -8466,7 +8504,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f855, 0x0000f856, 0x0000f857, 0x0000f858, /* bc */ 0x0000f859, 0x0000f85a, 0x0000f85b, 0x0000f85c, - /*** Three byte table, leaf: e9b4xx - offset 0x06e95 ***/ + /*** Three byte table, leaf: e9b4xx - offset 0x06f16 ***/ /* 80 */ 0x0000f85d, 0x0000f85e, 0x0000f85f, 0x0000f860, /* 84 */ 0x0000f861, 0x0000f862, 0x0000f863, 0x0000f864, @@ -8485,7 +8523,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f896, 0x0000f897, 0x0000f898, 0x0000f899, /* bc */ 0x0000f89a, 0x0000f89b, 0x0000f89c, 0x0000f89d, - /*** Three byte table, leaf: e9b5xx - offset 0x06ed5 ***/ + /*** Three byte table, leaf: e9b5xx - offset 0x06f56 ***/ /* 80 */ 0x0000f89e, 0x0000f89f, 0x0000f8a0, 0x0000f940, /* 84 */ 0x0000f941, 0x0000f942, 0x0000f943, 0x0000f944, @@ -8504,7 +8542,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f975, 0x0000f976, 0x0000f977, 0x0000f978, /* bc */ 0x0000f979, 0x0000f97a, 0x0000f97b, 0x0000f97c, - /*** Three byte table, leaf: e9b6xx - offset 0x06f15 ***/ + /*** Three byte table, leaf: e9b6xx - offset 0x06f96 ***/ /* 80 */ 0x0000f97d, 0x0000f97e, 0x0000f980, 0x0000f981, /* 84 */ 0x0000f982, 0x0000f983, 0x0000f984, 0x0000f985, @@ -8523,7 +8561,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fa55, 0x0000fa56, 0x0000fa57, 0x0000fa58, /* bc */ 0x0000fa59, 0x0000fa5a, 0x0000fa5b, 0x0000fa5c, - /*** Three byte table, leaf: e9b7xx - offset 0x06f55 ***/ + /*** Three byte table, leaf: e9b7xx - offset 0x06fd6 ***/ /* 80 */ 0x0000fa5d, 0x0000fa5e, 0x0000fa5f, 0x0000fa60, /* 84 */ 0x0000fa61, 0x0000fa62, 0x0000fa63, 0x0000fa64, @@ -8542,7 +8580,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fa96, 0x0000fa97, 0x0000fa98, 0x0000fa99, /* bc */ 0x0000fa9a, 0x0000fa9b, 0x0000fa9c, 0x0000fa9d, - /*** Three byte table, leaf: e9b8xx - offset 0x06f95 ***/ + /*** Three byte table, leaf: e9b8xx - offset 0x07016 ***/ /* 80 */ 0x0000fa9e, 0x0000fa9f, 0x0000faa0, 0x0000fb40, /* 84 */ 0x0000fb41, 0x0000fb42, 0x0000fb43, 0x0000fb44, @@ -8561,7 +8599,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f0b9, 0x0000f0bb, 0x0000f0bc, 0x0000fb61, /* bc */ 0x0000fb62, 0x0000b8eb, 0x0000f0bd, 0x0000bae8, - /*** Three byte table, leaf: e9b9xx - offset 0x06fd5 ***/ + /*** Three byte table, leaf: e9b9xx - offset 0x07056 ***/ /* 80 */ 0x0000fb63, 0x0000f0be, 0x0000f0bf, 0x0000bee9, /* 84 */ 0x0000f0c0, 0x0000b6ec, 0x0000f0c1, 0x0000f0c2, @@ -8580,7 +8618,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fb78, 0x0000fb79, 0x0000fb7a, 0x0000fb7b, /* bc */ 0x0000fb7c, 0x0000fb7d, 0x0000f5ba, 0x0000c2b9, - /*** Three byte table, leaf: e9baxx - offset 0x07015 ***/ + /*** Three byte table, leaf: e9baxx - offset 0x07096 ***/ /* 80 */ 0x0000fb7e, 0x0000fb80, 0x0000f7e4, 0x0000fb81, /* 84 */ 0x0000fb82, 0x0000fb83, 0x0000fb84, 0x0000f7e5, @@ -8599,7 +8637,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f4ef, 0x0000fc4c, 0x0000fc4d, 0x0000c2e9, /* bc */ 0x0000fc4e, 0x0000f7e1, 0x0000f7e2, 0x0000fc4f, - /*** Three byte table, leaf: e9bbxx - offset 0x07055 ***/ + /*** Three byte table, leaf: e9bbxx - offset 0x070d6 ***/ /* 80 */ 0x0000fc50, 0x0000fc51, 0x0000fc52, 0x0000fc53, /* 84 */ 0x0000bbc6, 0x0000fc54, 0x0000fc55, 0x0000fc56, @@ -8618,7 +8656,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fc75, 0x0000ede9, 0x0000fc76, 0x0000edea, /* bc */ 0x0000edeb, 0x0000fc77, 0x0000f6bc, 0x0000fc78, - /*** Three byte table, leaf: e9bcxx - offset 0x07095 ***/ + /*** Three byte table, leaf: e9bcxx - offset 0x07116 ***/ /* 80 */ 0x0000fc79, 0x0000fc7a, 0x0000fc7b, 0x0000fc7c, /* 84 */ 0x0000fc7d, 0x0000fc7e, 0x0000fc80, 0x0000fc81, @@ -8637,7 +8675,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fd45, 0x0000f7fa, 0x0000fd46, 0x0000b1c7, /* bc */ 0x0000fd47, 0x0000f7fc, 0x0000f7fd, 0x0000fd48, - /*** Three byte table, leaf: e9bdxx - offset 0x070d5 ***/ + /*** Three byte table, leaf: e9bdxx - offset 0x07156 ***/ /* 80 */ 0x0000fd49, 0x0000fd4a, 0x0000fd4b, 0x0000fd4c, /* 84 */ 0x0000f7fe, 0x0000fd4d, 0x0000fd4e, 0x0000fd4f, @@ -8656,7 +8694,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fd7e, 0x0000fd80, 0x0000fd81, 0x0000fd82, /* bc */ 0x0000fd83, 0x0000fd84, 0x0000fd85, 0x0000b3dd, - /*** Three byte table, leaf: e9bexx - offset 0x07115 ***/ + /*** Three byte table, leaf: e9bexx - offset 0x07196 ***/ /* 80 */ 0x0000f6b3, 0x0000fd86, 0x0000fd87, 0x0000f6b4, /* 84 */ 0x0000c1e4, 0x0000f6b5, 0x0000f6b6, 0x0000f6b7, @@ -8671,11 +8709,11 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* a8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* b0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* b4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* b8 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* b4 */ 0x0000fe59, 0x0000fe61, 0x0000fe66, 0x0000fe67, + /* b8 */ 0x0000fe6d, 0x0000fe7e, 0x0000fe90, 0x0000fea0, /* bc */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /*** Three byte table, leaf: ee80xx - offset 0x07155 ***/ + /*** Three byte table, leaf: ee80xx - offset 0x071d6 ***/ /* 80 */ 0x0000aaa1, 0x0000aaa2, 0x0000aaa3, 0x0000aaa4, /* 84 */ 0x0000aaa5, 0x0000aaa6, 0x0000aaa7, 0x0000aaa8, @@ -8694,7 +8732,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000aad9, 0x0000aada, 0x0000aadb, 0x0000aadc, /* bc */ 0x0000aadd, 0x0000aade, 0x0000aadf, 0x0000aae0, - /*** Three byte table, leaf: ee81xx - offset 0x07195 ***/ + /*** Three byte table, leaf: ee81xx - offset 0x07216 ***/ /* 80 */ 0x0000aae1, 0x0000aae2, 0x0000aae3, 0x0000aae4, /* 84 */ 0x0000aae5, 0x0000aae6, 0x0000aae7, 0x0000aae8, @@ -8713,7 +8751,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000abbb, 0x0000abbc, 0x0000abbd, 0x0000abbe, /* bc */ 0x0000abbf, 0x0000abc0, 0x0000abc1, 0x0000abc2, - /*** Three byte table, leaf: ee82xx - offset 0x071d5 ***/ + /*** Three byte table, leaf: ee82xx - offset 0x07256 ***/ /* 80 */ 0x0000abc3, 0x0000abc4, 0x0000abc5, 0x0000abc6, /* 84 */ 0x0000abc7, 0x0000abc8, 0x0000abc9, 0x0000abca, @@ -8732,7 +8770,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000abfb, 0x0000abfc, 0x0000abfd, 0x0000abfe, /* bc */ 0x0000aca1, 0x0000aca2, 0x0000aca3, 0x0000aca4, - /*** Three byte table, leaf: ee83xx - offset 0x07215 ***/ + /*** Three byte table, leaf: ee83xx - offset 0x07296 ***/ /* 80 */ 0x0000aca5, 0x0000aca6, 0x0000aca7, 0x0000aca8, /* 84 */ 0x0000aca9, 0x0000acaa, 0x0000acab, 0x0000acac, @@ -8751,7 +8789,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000acdd, 0x0000acde, 0x0000acdf, 0x0000ace0, /* bc */ 0x0000ace1, 0x0000ace2, 0x0000ace3, 0x0000ace4, - /*** Three byte table, leaf: ee84xx - offset 0x07255 ***/ + /*** Three byte table, leaf: ee84xx - offset 0x072d6 ***/ /* 80 */ 0x0000ace5, 0x0000ace6, 0x0000ace7, 0x0000ace8, /* 84 */ 0x0000ace9, 0x0000acea, 0x0000aceb, 0x0000acec, @@ -8770,7 +8808,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000adbf, 0x0000adc0, 0x0000adc1, 0x0000adc2, /* bc */ 0x0000adc3, 0x0000adc4, 0x0000adc5, 0x0000adc6, - /*** Three byte table, leaf: ee85xx - offset 0x07295 ***/ + /*** Three byte table, leaf: ee85xx - offset 0x07316 ***/ /* 80 */ 0x0000adc7, 0x0000adc8, 0x0000adc9, 0x0000adca, /* 84 */ 0x0000adcb, 0x0000adcc, 0x0000adcd, 0x0000adce, @@ -8789,7 +8827,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000aea1, 0x0000aea2, 0x0000aea3, 0x0000aea4, /* bc */ 0x0000aea5, 0x0000aea6, 0x0000aea7, 0x0000aea8, - /*** Three byte table, leaf: ee86xx - offset 0x072d5 ***/ + /*** Three byte table, leaf: ee86xx - offset 0x07356 ***/ /* 80 */ 0x0000aea9, 0x0000aeaa, 0x0000aeab, 0x0000aeac, /* 84 */ 0x0000aead, 0x0000aeae, 0x0000aeaf, 0x0000aeb0, @@ -8808,7 +8846,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000aee1, 0x0000aee2, 0x0000aee3, 0x0000aee4, /* bc */ 0x0000aee5, 0x0000aee6, 0x0000aee7, 0x0000aee8, - /*** Three byte table, leaf: ee87xx - offset 0x07315 ***/ + /*** Three byte table, leaf: ee87xx - offset 0x07396 ***/ /* 80 */ 0x0000aee9, 0x0000aeea, 0x0000aeeb, 0x0000aeec, /* 84 */ 0x0000aeed, 0x0000aeee, 0x0000aeef, 0x0000aef0, @@ -8827,7 +8865,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000afc3, 0x0000afc4, 0x0000afc5, 0x0000afc6, /* bc */ 0x0000afc7, 0x0000afc8, 0x0000afc9, 0x0000afca, - /*** Three byte table, leaf: ee88xx - offset 0x07355 ***/ + /*** Three byte table, leaf: ee88xx - offset 0x073d6 ***/ /* 80 */ 0x0000afcb, 0x0000afcc, 0x0000afcd, 0x0000afce, /* 84 */ 0x0000afcf, 0x0000afd0, 0x0000afd1, 0x0000afd2, @@ -8846,7 +8884,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f8a5, 0x0000f8a6, 0x0000f8a7, 0x0000f8a8, /* bc */ 0x0000f8a9, 0x0000f8aa, 0x0000f8ab, 0x0000f8ac, - /*** Three byte table, leaf: ee89xx - offset 0x07395 ***/ + /*** Three byte table, leaf: ee89xx - offset 0x07416 ***/ /* 80 */ 0x0000f8ad, 0x0000f8ae, 0x0000f8af, 0x0000f8b0, /* 84 */ 0x0000f8b1, 0x0000f8b2, 0x0000f8b3, 0x0000f8b4, @@ -8865,7 +8903,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f8e5, 0x0000f8e6, 0x0000f8e7, 0x0000f8e8, /* bc */ 0x0000f8e9, 0x0000f8ea, 0x0000f8eb, 0x0000f8ec, - /*** Three byte table, leaf: ee8axx - offset 0x073d5 ***/ + /*** Three byte table, leaf: ee8axx - offset 0x07456 ***/ /* 80 */ 0x0000f8ed, 0x0000f8ee, 0x0000f8ef, 0x0000f8f0, /* 84 */ 0x0000f8f1, 0x0000f8f2, 0x0000f8f3, 0x0000f8f4, @@ -8884,7 +8922,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000f9c7, 0x0000f9c8, 0x0000f9c9, 0x0000f9ca, /* bc */ 0x0000f9cb, 0x0000f9cc, 0x0000f9cd, 0x0000f9ce, - /*** Three byte table, leaf: ee8bxx - offset 0x07415 ***/ + /*** Three byte table, leaf: ee8bxx - offset 0x07496 ***/ /* 80 */ 0x0000f9cf, 0x0000f9d0, 0x0000f9d1, 0x0000f9d2, /* 84 */ 0x0000f9d3, 0x0000f9d4, 0x0000f9d5, 0x0000f9d6, @@ -8903,7 +8941,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000faa9, 0x0000faaa, 0x0000faab, 0x0000faac, /* bc */ 0x0000faad, 0x0000faae, 0x0000faaf, 0x0000fab0, - /*** Three byte table, leaf: ee8cxx - offset 0x07455 ***/ + /*** Three byte table, leaf: ee8cxx - offset 0x074d6 ***/ /* 80 */ 0x0000fab1, 0x0000fab2, 0x0000fab3, 0x0000fab4, /* 84 */ 0x0000fab5, 0x0000fab6, 0x0000fab7, 0x0000fab8, @@ -8922,7 +8960,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fae9, 0x0000faea, 0x0000faeb, 0x0000faec, /* bc */ 0x0000faed, 0x0000faee, 0x0000faef, 0x0000faf0, - /*** Three byte table, leaf: ee8dxx - offset 0x07495 ***/ + /*** Three byte table, leaf: ee8dxx - offset 0x07516 ***/ /* 80 */ 0x0000faf1, 0x0000faf2, 0x0000faf3, 0x0000faf4, /* 84 */ 0x0000faf5, 0x0000faf6, 0x0000faf7, 0x0000faf8, @@ -8941,7 +8979,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fbcb, 0x0000fbcc, 0x0000fbcd, 0x0000fbce, /* bc */ 0x0000fbcf, 0x0000fbd0, 0x0000fbd1, 0x0000fbd2, - /*** Three byte table, leaf: ee8exx - offset 0x074d5 ***/ + /*** Three byte table, leaf: ee8exx - offset 0x07556 ***/ /* 80 */ 0x0000fbd3, 0x0000fbd4, 0x0000fbd5, 0x0000fbd6, /* 84 */ 0x0000fbd7, 0x0000fbd8, 0x0000fbd9, 0x0000fbda, @@ -8960,7 +8998,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fcad, 0x0000fcae, 0x0000fcaf, 0x0000fcb0, /* bc */ 0x0000fcb1, 0x0000fcb2, 0x0000fcb3, 0x0000fcb4, - /*** Three byte table, leaf: ee8fxx - offset 0x07515 ***/ + /*** Three byte table, leaf: ee8fxx - offset 0x07596 ***/ /* 80 */ 0x0000fcb5, 0x0000fcb6, 0x0000fcb7, 0x0000fcb8, /* 84 */ 0x0000fcb9, 0x0000fcba, 0x0000fcbb, 0x0000fcbc, @@ -8979,7 +9017,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fced, 0x0000fcee, 0x0000fcef, 0x0000fcf0, /* bc */ 0x0000fcf1, 0x0000fcf2, 0x0000fcf3, 0x0000fcf4, - /*** Three byte table, leaf: ee90xx - offset 0x07555 ***/ + /*** Three byte table, leaf: ee90xx - offset 0x075d6 ***/ /* 80 */ 0x0000fcf5, 0x0000fcf6, 0x0000fcf7, 0x0000fcf8, /* 84 */ 0x0000fcf9, 0x0000fcfa, 0x0000fcfb, 0x0000fcfc, @@ -8998,7 +9036,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fdcf, 0x0000fdd0, 0x0000fdd1, 0x0000fdd2, /* bc */ 0x0000fdd3, 0x0000fdd4, 0x0000fdd5, 0x0000fdd6, - /*** Three byte table, leaf: ee91xx - offset 0x07595 ***/ + /*** Three byte table, leaf: ee91xx - offset 0x07616 ***/ /* 80 */ 0x0000fdd7, 0x0000fdd8, 0x0000fdd9, 0x0000fdda, /* 84 */ 0x0000fddb, 0x0000fddc, 0x0000fddd, 0x0000fdde, @@ -9017,7 +9055,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000feb1, 0x0000feb2, 0x0000feb3, 0x0000feb4, /* bc */ 0x0000feb5, 0x0000feb6, 0x0000feb7, 0x0000feb8, - /*** Three byte table, leaf: ee92xx - offset 0x075d5 ***/ + /*** Three byte table, leaf: ee92xx - offset 0x07656 ***/ /* 80 */ 0x0000feb9, 0x0000feba, 0x0000febb, 0x0000febc, /* 84 */ 0x0000febd, 0x0000febe, 0x0000febf, 0x0000fec0, @@ -9036,7 +9074,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000fef1, 0x0000fef2, 0x0000fef3, 0x0000fef4, /* bc */ 0x0000fef5, 0x0000fef6, 0x0000fef7, 0x0000fef8, - /*** Three byte table, leaf: ee93xx - offset 0x07615 ***/ + /*** Three byte table, leaf: ee93xx - offset 0x07696 ***/ /* 80 */ 0x0000fef9, 0x0000fefa, 0x0000fefb, 0x0000fefc, /* 84 */ 0x0000fefd, 0x0000fefe, 0x0000a140, 0x0000a141, @@ -9055,7 +9093,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a172, 0x0000a173, 0x0000a174, 0x0000a175, /* bc */ 0x0000a176, 0x0000a177, 0x0000a178, 0x0000a179, - /*** Three byte table, leaf: ee94xx - offset 0x07655 ***/ + /*** Three byte table, leaf: ee94xx - offset 0x076d6 ***/ /* 80 */ 0x0000a17a, 0x0000a17b, 0x0000a17c, 0x0000a17d, /* 84 */ 0x0000a17e, 0x0000a180, 0x0000a181, 0x0000a182, @@ -9074,7 +9112,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a252, 0x0000a253, 0x0000a254, 0x0000a255, /* bc */ 0x0000a256, 0x0000a257, 0x0000a258, 0x0000a259, - /*** Three byte table, leaf: ee95xx - offset 0x07695 ***/ + /*** Three byte table, leaf: ee95xx - offset 0x07716 ***/ /* 80 */ 0x0000a25a, 0x0000a25b, 0x0000a25c, 0x0000a25d, /* 84 */ 0x0000a25e, 0x0000a25f, 0x0000a260, 0x0000a261, @@ -9093,7 +9131,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a293, 0x0000a294, 0x0000a295, 0x0000a296, /* bc */ 0x0000a297, 0x0000a298, 0x0000a299, 0x0000a29a, - /*** Three byte table, leaf: ee96xx - offset 0x076d5 ***/ + /*** Three byte table, leaf: ee96xx - offset 0x07756 ***/ /* 80 */ 0x0000a29b, 0x0000a29c, 0x0000a29d, 0x0000a29e, /* 84 */ 0x0000a29f, 0x0000a2a0, 0x0000a340, 0x0000a341, @@ -9112,7 +9150,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a372, 0x0000a373, 0x0000a374, 0x0000a375, /* bc */ 0x0000a376, 0x0000a377, 0x0000a378, 0x0000a379, - /*** Three byte table, leaf: ee97xx - offset 0x07715 ***/ + /*** Three byte table, leaf: ee97xx - offset 0x07796 ***/ /* 80 */ 0x0000a37a, 0x0000a37b, 0x0000a37c, 0x0000a37d, /* 84 */ 0x0000a37e, 0x0000a380, 0x0000a381, 0x0000a382, @@ -9123,7 +9161,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* 98 */ 0x0000a393, 0x0000a394, 0x0000a395, 0x0000a396, /* 9c */ 0x0000a397, 0x0000a398, 0x0000a399, 0x0000a39a, /* a0 */ 0x0000a39b, 0x0000a39c, 0x0000a39d, 0x0000a39e, - /* a4 */ 0x0000a39f, 0x0000a3a0, 0x0000a440, 0x0000a441, + /* a4 */ 0x0000a39f, 0x00000000, 0x0000a440, 0x0000a441, /* a8 */ 0x0000a442, 0x0000a443, 0x0000a444, 0x0000a445, /* ac */ 0x0000a446, 0x0000a447, 0x0000a448, 0x0000a449, /* b0 */ 0x0000a44a, 0x0000a44b, 0x0000a44c, 0x0000a44d, @@ -9131,7 +9169,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a452, 0x0000a453, 0x0000a454, 0x0000a455, /* bc */ 0x0000a456, 0x0000a457, 0x0000a458, 0x0000a459, - /*** Three byte table, leaf: ee98xx - offset 0x07755 ***/ + /*** Three byte table, leaf: ee98xx - offset 0x077d6 ***/ /* 80 */ 0x0000a45a, 0x0000a45b, 0x0000a45c, 0x0000a45d, /* 84 */ 0x0000a45e, 0x0000a45f, 0x0000a460, 0x0000a461, @@ -9150,7 +9188,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a493, 0x0000a494, 0x0000a495, 0x0000a496, /* bc */ 0x0000a497, 0x0000a498, 0x0000a499, 0x0000a49a, - /*** Three byte table, leaf: ee99xx - offset 0x07795 ***/ + /*** Three byte table, leaf: ee99xx - offset 0x07816 ***/ /* 80 */ 0x0000a49b, 0x0000a49c, 0x0000a49d, 0x0000a49e, /* 84 */ 0x0000a49f, 0x0000a4a0, 0x0000a540, 0x0000a541, @@ -9169,7 +9207,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a572, 0x0000a573, 0x0000a574, 0x0000a575, /* bc */ 0x0000a576, 0x0000a577, 0x0000a578, 0x0000a579, - /*** Three byte table, leaf: ee9axx - offset 0x077d5 ***/ + /*** Three byte table, leaf: ee9axx - offset 0x07856 ***/ /* 80 */ 0x0000a57a, 0x0000a57b, 0x0000a57c, 0x0000a57d, /* 84 */ 0x0000a57e, 0x0000a580, 0x0000a581, 0x0000a582, @@ -9188,7 +9226,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a652, 0x0000a653, 0x0000a654, 0x0000a655, /* bc */ 0x0000a656, 0x0000a657, 0x0000a658, 0x0000a659, - /*** Three byte table, leaf: ee9bxx - offset 0x07815 ***/ + /*** Three byte table, leaf: ee9bxx - offset 0x07896 ***/ /* 80 */ 0x0000a65a, 0x0000a65b, 0x0000a65c, 0x0000a65d, /* 84 */ 0x0000a65e, 0x0000a65f, 0x0000a660, 0x0000a661, @@ -9207,7 +9245,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a693, 0x0000a694, 0x0000a695, 0x0000a696, /* bc */ 0x0000a697, 0x0000a698, 0x0000a699, 0x0000a69a, - /*** Three byte table, leaf: ee9cxx - offset 0x07855 ***/ + /*** Three byte table, leaf: ee9cxx - offset 0x078d6 ***/ /* 80 */ 0x0000a69b, 0x0000a69c, 0x0000a69d, 0x0000a69e, /* 84 */ 0x0000a69f, 0x0000a6a0, 0x0000a740, 0x0000a741, @@ -9226,7 +9264,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a772, 0x0000a773, 0x0000a774, 0x0000a775, /* bc */ 0x0000a776, 0x0000a777, 0x0000a778, 0x0000a779, - /*** Three byte table, leaf: ee9dxx - offset 0x07895 ***/ + /*** Three byte table, leaf: ee9dxx - offset 0x07916 ***/ /* 80 */ 0x0000a77a, 0x0000a77b, 0x0000a77c, 0x0000a77d, /* 84 */ 0x0000a77e, 0x0000a780, 0x0000a781, 0x0000a782, @@ -9245,14 +9283,14 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a4fa, 0x0000a4fb, 0x0000a4fc, 0x0000a4fd, /* bc */ 0x0000a4fe, 0x0000a5f7, 0x0000a5f8, 0x0000a5f9, - /*** Three byte table, leaf: ee9exx - offset 0x078d5 ***/ + /*** Three byte table, leaf: ee9exx - offset 0x07956 ***/ /* 80 */ 0x0000a5fa, 0x0000a5fb, 0x0000a5fc, 0x0000a5fd, /* 84 */ 0x0000a5fe, 0x0000a6b9, 0x0000a6ba, 0x0000a6bb, /* 88 */ 0x0000a6bc, 0x0000a6bd, 0x0000a6be, 0x0000a6bf, - /* 8c */ 0x0000a6c0, 0x0000a6d9, 0x0000a6da, 0x0000a6db, - /* 90 */ 0x0000a6dc, 0x0000a6dd, 0x0000a6de, 0x0000a6df, - /* 94 */ 0x0000a6ec, 0x0000a6ed, 0x0000a6f3, 0x0000a6f6, + /* 8c */ 0x0000a6c0, 0x00000000, 0x00000000, 0x00000000, + /* 90 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 94 */ 0x00000000, 0x00000000, 0x00000000, 0x0000a6f6, /* 98 */ 0x0000a6f7, 0x0000a6f8, 0x0000a6f9, 0x0000a6fa, /* 9c */ 0x0000a6fb, 0x0000a6fc, 0x0000a6fd, 0x0000a6fe, /* a0 */ 0x0000a7c2, 0x0000a7c3, 0x0000a7c4, 0x0000a7c5, @@ -9264,10 +9302,10 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a7fb, 0x0000a7fc, 0x0000a7fd, 0x0000a7fe, /* bc */ 0x0000a896, 0x0000a897, 0x0000a898, 0x0000a899, - /*** Three byte table, leaf: ee9fxx - offset 0x07915 ***/ + /*** Three byte table, leaf: ee9fxx - offset 0x07996 ***/ /* 80 */ 0x0000a89a, 0x0000a89b, 0x0000a89c, 0x0000a89d, - /* 84 */ 0x0000a89e, 0x0000a89f, 0x0000a8a0, 0x0000a8bc, + /* 84 */ 0x0000a89e, 0x0000a89f, 0x0000a8a0, 0x8135f437, /* 88 */ 0x8336c830, 0x0000a8c1, 0x0000a8c2, 0x0000a8c3, /* 8c */ 0x0000a8c4, 0x0000a8ea, 0x0000a8eb, 0x0000a8ec, /* 90 */ 0x0000a8ed, 0x0000a8ee, 0x0000a8ef, 0x0000a8f0, @@ -9283,7 +9321,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a99b, 0x0000a99c, 0x0000a99d, 0x0000a99e, /* bc */ 0x0000a99f, 0x0000a9a0, 0x0000a9a1, 0x0000a9a2, - /*** Three byte table, leaf: eea0xx - offset 0x07955 ***/ + /*** Three byte table, leaf: eea0xx - offset 0x079d6 ***/ /* 80 */ 0x0000a9a3, 0x0000a9f0, 0x0000a9f1, 0x0000a9f2, /* 84 */ 0x0000a9f3, 0x0000a9f4, 0x0000a9f5, 0x0000a9f6, @@ -9292,31 +9330,30 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* 90 */ 0x0000d7fa, 0x0000d7fb, 0x0000d7fc, 0x0000d7fd, /* 94 */ 0x0000d7fe, 0x8336c934, 0x0000fe51, 0x0000fe52, /* 98 */ 0x0000fe53, 0x8336c935, 0x8336c936, 0x8336c937, - /* 9c */ 0x8336c938, 0x8336c939, 0x0000fe59, 0x8336ca30, + /* 9c */ 0x8336c938, 0x8336c939, 0x00000000, 0x8336ca30, /* a0 */ 0x8336ca31, 0x8336ca32, 0x8336ca33, 0x8336ca34, - /* a4 */ 0x8336ca35, 0x8336ca36, 0x0000fe61, 0x8336ca37, - /* a8 */ 0x8336ca38, 0x8336ca39, 0x8336cb30, 0x0000fe66, - /* ac */ 0x0000fe67, 0x8336cb31, 0x8336cb32, 0x8336cb33, - /* b0 */ 0x8336cb34, 0x0000fe6c, 0x0000fe6d, 0x8336cb35, + /* a4 */ 0x8336ca35, 0x8336ca36, 0x00000000, 0x8336ca37, + /* a8 */ 0x8336ca38, 0x8336ca39, 0x8336cb30, 0x00000000, + /* ac */ 0x00000000, 0x8336cb31, 0x8336cb32, 0x8336cb33, + /* b0 */ 0x8336cb34, 0x0000fe6c, 0x00000000, 0x8336cb35, /* b4 */ 0x8336cb36, 0x8336cb37, 0x8336cb38, 0x8336cb39, /* b8 */ 0x8336cc30, 0x8336cc31, 0x8336cc32, 0x0000fe76, /* bc */ 0x8336cc33, 0x8336cc34, 0x8336cc35, 0x8336cc36, - /*** Three byte table, leaf: eea1xx - offset 0x07995 ***/ + /*** Three byte table, leaf: eea1xx - offset 0x07a16 ***/ - /* 80 */ 0x8336cc37, 0x8336cc38, 0x8336cc39, 0x0000fe7e, + /* 80 */ 0x8336cc37, 0x8336cc38, 0x8336cc39, 0x00000000, /* 84 */ 0x8336cd30, 0x8336cd31, 0x8336cd32, 0x8336cd33, /* 88 */ 0x8336cd34, 0x8336cd35, 0x8336cd36, 0x8336cd37, /* 8c */ 0x8336cd38, 0x8336cd39, 0x8336ce30, 0x8336ce31, /* 90 */ 0x8336ce32, 0x8336ce33, 0x8336ce34, 0x8336ce35, - /* 94 */ 0x0000fe90, 0x0000fe91, 0x8336ce36, 0x8336ce37, + /* 94 */ 0x00000000, 0x0000fe91, 0x8336ce36, 0x8336ce37, /* 98 */ 0x8336ce38, 0x8336ce39, 0x8336cf30, 0x8336cf31, /* 9c */ 0x8336cf32, 0x8336cf33, 0x8336cf34, 0x8336cf35, /* a0 */ 0x8336cf36, 0x8336cf37, 0x8336cf38, 0x8336cf39, - /* a4 */ 0x0000fea0, - /* 27 trailing zero values shared with next segment */ + /* 28 trailing zero values shared with next segment */ - /*** Three byte table, leaf: efa4xx - offset 0x079ba ***/ + /*** Three byte table, leaf: efa4xx - offset 0x07a3a ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -9335,7 +9372,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84308636, 0x84308637, 0x84308638, 0x84308639, /* bc */ 0x84308730, 0x84308731, 0x84308732, 0x84308733, - /*** Three byte table, leaf: efa5xx - offset 0x079fa ***/ + /*** Three byte table, leaf: efa5xx - offset 0x07a7a ***/ /* 80 */ 0x84308734, 0x84308735, 0x84308736, 0x84308737, /* 84 */ 0x84308738, 0x84308739, 0x84308830, 0x84308831, @@ -9354,7 +9391,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84308d30, 0x0000fd9d, 0x84308d31, 0x84308d32, /* bc */ 0x84308d33, 0x84308d34, 0x84308d35, 0x84308d36, - /*** Three byte table, leaf: efa6xx - offset 0x07a3a ***/ + /*** Three byte table, leaf: efa6xx - offset 0x07aba ***/ /* 80 */ 0x84308d37, 0x84308d38, 0x84308d39, 0x84308e30, /* 84 */ 0x84308e31, 0x84308e32, 0x84308e33, 0x84308e34, @@ -9373,7 +9410,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84309332, 0x84309333, 0x84309334, 0x84309335, /* bc */ 0x84309336, 0x84309337, 0x84309338, 0x84309339, - /*** Three byte table, leaf: efa7xx - offset 0x07a7a ***/ + /*** Three byte table, leaf: efa7xx - offset 0x07afa ***/ /* 80 */ 0x84309430, 0x84309431, 0x84309432, 0x84309433, /* 84 */ 0x84309434, 0x84309435, 0x84309436, 0x84309437, @@ -9392,7 +9429,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84309934, 0x84309935, 0x84309936, 0x84309937, /* bc */ 0x84309938, 0x84309939, 0x84309a30, 0x84309a31, - /*** Three byte table, leaf: efa8xx - offset 0x07aba ***/ + /*** Three byte table, leaf: efa8xx - offset 0x07b3a ***/ /* 80 */ 0x84309a32, 0x84309a33, 0x84309a34, 0x84309a35, /* 84 */ 0x84309a36, 0x84309a37, 0x84309a38, 0x84309a39, @@ -9404,18 +9441,19 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* 9c */ 0x84309c32, 0x84309c33, 0x84309c34, 0x0000fe48, /* a0 */ 0x0000fe49, 0x0000fe4a, 0x84309c35, 0x0000fe4b, /* a4 */ 0x0000fe4c, 0x84309c36, 0x84309c37, 0x0000fe4d, - /* a8 */ 0x0000fe4e, 0x0000fe4f, - /* 22 trailing zero values shared with next segment */ + /* a8 */ 0x0000fe4e, 0x0000fe4f, 0x00000000, 0x00000000, + /* ac */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 16 trailing zero values shared with next segment */ - /*** Three byte table, leaf: efb8xx - offset 0x07ae4 ***/ + /*** Three byte table, leaf: efb8xx - offset 0x07b6a ***/ /* 80 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 84 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 88 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* 8c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* 90 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* 94 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, - /* 98 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, + /* 90 */ 0x0000a6d9, 0x0000a6db, 0x0000a6da, 0x0000a6dc, + /* 94 */ 0x0000a6dd, 0x0000a6de, 0x0000a6df, 0x0000a6ec, + /* 98 */ 0x0000a6ed, 0x0000a6f3, 0x00000000, 0x00000000, /* 9c */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* a0 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, /* a4 */ 0x00000000, 0x00000000, 0x00000000, 0x00000000, @@ -9426,7 +9464,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a6f1, 0x0000a6e2, 0x0000a6e3, 0x0000a6ee, /* bc */ 0x0000a6ef, 0x0000a6e6, 0x0000a6e7, 0x0000a6e4, - /*** Three byte table, leaf: efb9xx - offset 0x07b24 ***/ + /*** Three byte table, leaf: efb9xx - offset 0x07baa ***/ /* 80 */ 0x0000a6e5, 0x0000a6e8, 0x0000a6e9, 0x0000a6ea, /* 84 */ 0x0000a6eb, 0x84318539, 0x84318630, 0x84318631, @@ -9445,7 +9483,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84318738, 0x84318739, 0x84318830, 0x84318831, /* bc */ 0x84318832, 0x84318833, 0x84318834, 0x84318835, - /*** Three byte table, leaf: efbaxx - offset 0x07b64 ***/ + /*** Three byte table, leaf: efbaxx - offset 0x07bea ***/ /* 80 */ 0x84318836, 0x84318837, 0x84318838, 0x84318839, /* 84 */ 0x84318930, 0x84318931, 0x84318932, 0x84318933, @@ -9464,7 +9502,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84318e32, 0x84318e33, 0x84318e34, 0x84318e35, /* bc */ 0x84318e36, 0x84318e37, 0x84318e38, 0x84318e39, - /*** Three byte table, leaf: efbbxx - offset 0x07ba4 ***/ + /*** Three byte table, leaf: efbbxx - offset 0x07c2a ***/ /* 80 */ 0x84318f30, 0x84318f31, 0x84318f32, 0x84318f33, /* 84 */ 0x84318f34, 0x84318f35, 0x84318f36, 0x84318f37, @@ -9483,7 +9521,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84319436, 0x84319437, 0x84319438, 0x84319439, /* bc */ 0x84319530, 0x84319531, 0x84319532, 0x84319533, - /*** Three byte table, leaf: efbcxx - offset 0x07be4 ***/ + /*** Three byte table, leaf: efbcxx - offset 0x07c6a ***/ /* 80 */ 0x84319534, 0x0000a3a1, 0x0000a3a2, 0x0000a3a3, /* 84 */ 0x0000a1e7, 0x0000a3a5, 0x0000a3a6, 0x0000a3a7, @@ -9502,7 +9540,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x0000a3d8, 0x0000a3d9, 0x0000a3da, 0x0000a3db, /* bc */ 0x0000a3dc, 0x0000a3dd, 0x0000a3de, 0x0000a3df, - /*** Three byte table, leaf: efbdxx - offset 0x07c24 ***/ + /*** Three byte table, leaf: efbdxx - offset 0x07caa ***/ /* 80 */ 0x0000a3e0, 0x0000a3e1, 0x0000a3e2, 0x0000a3e3, /* 84 */ 0x0000a3e4, 0x0000a3e5, 0x0000a3e6, 0x0000a3e7, @@ -9521,7 +9559,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84319830, 0x84319831, 0x84319832, 0x84319833, /* bc */ 0x84319834, 0x84319835, 0x84319836, 0x84319837, - /*** Three byte table, leaf: efbexx - offset 0x07c64 ***/ + /*** Three byte table, leaf: efbexx - offset 0x07cea ***/ /* 80 */ 0x84319838, 0x84319839, 0x84319930, 0x84319931, /* 84 */ 0x84319932, 0x84319933, 0x84319934, 0x84319935, @@ -9540,7 +9578,7 @@ static const uint32 gb18030_from_unicode_tree_table[31972] = /* b8 */ 0x84319e34, 0x84319e35, 0x84319e36, 0x84319e37, /* bc */ 0x84319e38, 0x84319e39, 0x84319f30, 0x84319f31, - /*** Three byte table, leaf: efbfxx - offset 0x07ca4 ***/ + /*** Three byte table, leaf: efbfxx - offset 0x07d2a ***/ /* 80 */ 0x84319f32, 0x84319f33, 0x84319f34, 0x84319f35, /* 84 */ 0x84319f36, 0x84319f37, 0x84319f38, 0x84319f39, diff --git a/src/backend/utils/mb/conv.c b/src/backend/utils/mb/conv.c index 4a312ab429b6f..79238bf664725 100644 --- a/src/backend/utils/mb/conv.c +++ b/src/backend/utils/mb/conv.c @@ -2,7 +2,7 @@ * * Utility functions for conversion procs. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -74,244 +74,6 @@ local2local(const unsigned char *l, return l - start; } -/* - * LATINn ---> MIC when the charset's local codes map directly to MIC - * - * l points to the source string of length len - * p is the output area (must be large enough!) - * lc is the mule character set id for the local encoding - * encoding is the PG identifier for the local encoding - * - * Returns the number of input bytes consumed. If noError is true, this can - * be less than 'len'. - */ -int -latin2mic(const unsigned char *l, unsigned char *p, int len, - int lc, int encoding, bool noError) -{ - const unsigned char *start = l; - int c1; - - while (len > 0) - { - c1 = *l; - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(encoding, (const char *) l, len); - } - if (IS_HIGHBIT_SET(c1)) - *p++ = lc; - *p++ = c1; - l++; - len--; - } - *p = '\0'; - - return l - start; -} - -/* - * MIC ---> LATINn when the charset's local codes map directly to MIC - * - * mic points to the source string of length len - * p is the output area (must be large enough!) - * lc is the mule character set id for the local encoding - * encoding is the PG identifier for the local encoding - * - * Returns the number of input bytes consumed. If noError is true, this can - * be less than 'len'. - */ -int -mic2latin(const unsigned char *mic, unsigned char *p, int len, - int lc, int encoding, bool noError) -{ - const unsigned char *start = mic; - int c1; - - while (len > 0) - { - c1 = *mic; - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, (const char *) mic, len); - } - if (!IS_HIGHBIT_SET(c1)) - { - /* easy for ASCII */ - *p++ = c1; - mic++; - len--; - } - else - { - int l = pg_mule_mblen(mic); - - if (len < l) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, (const char *) mic, - len); - } - if (l != 2 || c1 != lc || !IS_HIGHBIT_SET(mic[1])) - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, encoding, - (const char *) mic, len); - } - *p++ = mic[1]; - mic += 2; - len -= 2; - } - } - *p = '\0'; - - return mic - start; -} - - -/* - * latin2mic_with_table: a generic single byte charset encoding - * conversion from a local charset to the mule internal code. - * - * l points to the source string of length len - * p is the output area (must be large enough!) - * lc is the mule character set id for the local encoding - * encoding is the PG identifier for the local encoding - * tab holds conversion entries for the local charset - * starting from 128 (0x80). each entry in the table holds the corresponding - * code point for the mule encoding, or 0 if there is no equivalent code. - * - * Returns the number of input bytes consumed. If noError is true, this can - * be less than 'len'. - */ -int -latin2mic_with_table(const unsigned char *l, - unsigned char *p, - int len, - int lc, - int encoding, - const unsigned char *tab, - bool noError) -{ - const unsigned char *start = l; - unsigned char c1, - c2; - - while (len > 0) - { - c1 = *l; - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(encoding, (const char *) l, len); - } - if (!IS_HIGHBIT_SET(c1)) - *p++ = c1; - else - { - c2 = tab[c1 - HIGHBIT]; - if (c2) - { - *p++ = lc; - *p++ = c2; - } - else - { - if (noError) - break; - report_untranslatable_char(encoding, PG_MULE_INTERNAL, - (const char *) l, len); - } - } - l++; - len--; - } - *p = '\0'; - - return l - start; -} - -/* - * mic2latin_with_table: a generic single byte charset encoding - * conversion from the mule internal code to a local charset. - * - * mic points to the source string of length len - * p is the output area (must be large enough!) - * lc is the mule character set id for the local encoding - * encoding is the PG identifier for the local encoding - * tab holds conversion entries for the mule internal code's second byte, - * starting from 128 (0x80). each entry in the table holds the corresponding - * code point for the local charset, or 0 if there is no equivalent code. - * - * Returns the number of input bytes consumed. If noError is true, this can - * be less than 'len'. - */ -int -mic2latin_with_table(const unsigned char *mic, - unsigned char *p, - int len, - int lc, - int encoding, - const unsigned char *tab, - bool noError) -{ - const unsigned char *start = mic; - unsigned char c1, - c2; - - while (len > 0) - { - c1 = *mic; - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, (const char *) mic, len); - } - if (!IS_HIGHBIT_SET(c1)) - { - /* easy for ASCII */ - *p++ = c1; - mic++; - len--; - } - else - { - int l = pg_mule_mblen(mic); - - if (len < l) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, (const char *) mic, - len); - } - if (l != 2 || c1 != lc || !IS_HIGHBIT_SET(mic[1]) || - (c2 = tab[mic[1] - HIGHBIT]) == 0) - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, encoding, - (const char *) mic, len); - break; /* keep compiler quiet */ - } - *p++ = c2; - mic += 2; - len -= 2; - } - } - *p = '\0'; - - return mic - start; -} - /* * comparison routine for bsearch() * this routine is intended for combined UTF8 -> local code @@ -484,7 +246,7 @@ pg_mb_radix_conv(const pg_mb_radix_tree *rt, * utf: input string in UTF8 encoding (need not be null-terminated) * len: length of input string (in bytes) * iso: pointer to the output area (must be large enough!) - (output string will be null-terminated) + * (output string will be null-terminated) * map: conversion map for single characters * cmap: conversion map for combined characters * (optional, pass NULL if none) @@ -694,7 +456,7 @@ UtfToLocal(const unsigned char *utf, int len, * iso: input string in local encoding (need not be null-terminated) * len: length of input string (in bytes) * utf: pointer to the output area (must be large enough!) - (output string will be null-terminated) + * (output string will be null-terminated) * map: conversion map for single characters * cmap: conversion map for combined characters * (optional, pass NULL if none) diff --git a/src/backend/utils/mb/conversion_procs/Makefile b/src/backend/utils/mb/conversion_procs/Makefile index e1919eff6d1ae..d3c49275a158c 100644 --- a/src/backend/utils/mb/conversion_procs/Makefile +++ b/src/backend/utils/mb/conversion_procs/Makefile @@ -2,7 +2,7 @@ # # Makefile for backend/utils/mb/conversion_procs # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/backend/utils/mb/conversion_procs/Makefile @@ -14,8 +14,8 @@ top_builddir = ../../../../.. include $(top_builddir)/src/Makefile.global SUBDIRS = \ - cyrillic_and_mic euc_cn_and_mic euc_jp_and_sjis \ - euc_kr_and_mic euc_tw_and_big5 latin2_and_win1250 latin_and_mic \ + cyrillic euc_jp_and_sjis \ + euc_tw_and_big5 latin2_and_win1250 \ utf8_and_big5 utf8_and_cyrillic utf8_and_euc_cn \ utf8_and_euc_jp utf8_and_euc_kr utf8_and_euc_tw utf8_and_gb18030 \ utf8_and_gbk utf8_and_iso8859 utf8_and_iso8859_1 utf8_and_johab \ diff --git a/src/backend/utils/mb/conversion_procs/latin_and_mic/Makefile b/src/backend/utils/mb/conversion_procs/cyrillic/Makefile similarity index 57% rename from src/backend/utils/mb/conversion_procs/latin_and_mic/Makefile rename to src/backend/utils/mb/conversion_procs/cyrillic/Makefile index c404738d2f647..1ad04c203009d 100644 --- a/src/backend/utils/mb/conversion_procs/latin_and_mic/Makefile +++ b/src/backend/utils/mb/conversion_procs/cyrillic/Makefile @@ -1,13 +1,13 @@ #------------------------------------------------------------------------- # -# src/backend/utils/mb/conversion_procs/latin_and_mic/Makefile +# src/backend/utils/mb/conversion_procs/cyrillic/Makefile # #------------------------------------------------------------------------- -subdir = src/backend/utils/mb/conversion_procs/latin_and_mic +subdir = src/backend/utils/mb/conversion_procs/cyrillic top_builddir = ../../../../../.. include $(top_builddir)/src/Makefile.global -NAME = latin_and_mic -PGFILEDESC = "latin <-> mic text conversions" +NAME = cyrillic +PGFILEDESC = "cyrillic single-byte conversions" include $(srcdir)/../proc.mk diff --git a/src/backend/utils/mb/conversion_procs/cyrillic_and_mic/cyrillic_and_mic.c b/src/backend/utils/mb/conversion_procs/cyrillic/cyrillic.c similarity index 80% rename from src/backend/utils/mb/conversion_procs/cyrillic_and_mic/cyrillic_and_mic.c rename to src/backend/utils/mb/conversion_procs/cyrillic/cyrillic.c index f00432a698126..16f91c80a7583 100644 --- a/src/backend/utils/mb/conversion_procs/cyrillic_and_mic/cyrillic_and_mic.c +++ b/src/backend/utils/mb/conversion_procs/cyrillic/cyrillic.c @@ -1,12 +1,12 @@ /*------------------------------------------------------------------------- * - * Cyrillic and MULE_INTERNAL + * KOI8R, WIN1251, WIN866 and ISO_8859_5 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/backend/utils/mb/conversion_procs/cyrillic_and_mic/cyrillic_and_mic.c + * src/backend/utils/mb/conversion_procs/cyrillic/cyrillic.c * *------------------------------------------------------------------------- */ @@ -16,18 +16,10 @@ #include "mb/pg_wchar.h" PG_MODULE_MAGIC_EXT( - .name = "cyrillic_and_mic", + .name = "cyrillic", .version = PG_VERSION ); -PG_FUNCTION_INFO_V1(koi8r_to_mic); -PG_FUNCTION_INFO_V1(mic_to_koi8r); -PG_FUNCTION_INFO_V1(iso_to_mic); -PG_FUNCTION_INFO_V1(mic_to_iso); -PG_FUNCTION_INFO_V1(win1251_to_mic); -PG_FUNCTION_INFO_V1(mic_to_win1251); -PG_FUNCTION_INFO_V1(win866_to_mic); -PG_FUNCTION_INFO_V1(mic_to_win866); PG_FUNCTION_INFO_V1(koi8r_to_win1251); PG_FUNCTION_INFO_V1(win1251_to_koi8r); PG_FUNCTION_INFO_V1(koi8r_to_win866); @@ -59,7 +51,7 @@ PG_FUNCTION_INFO_V1(win866_to_iso); * Cyrillic support * currently supported Cyrillic encodings: * - * KOI8-R (this is also the charset for the mule internal code for Cyrillic) + * KOI8-R * ISO-8859-5 * Microsoft's CP1251 (windows-1251) * Alternativny Variant (MS-DOS CP866) @@ -306,134 +298,6 @@ static const unsigned char win8662iso[] = { }; -Datum -koi8r_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_KOI8R, PG_MULE_INTERNAL); - - converted = latin2mic(src, dest, len, LC_KOI8_R, PG_KOI8R, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_koi8r(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_KOI8R); - - converted = mic2latin(src, dest, len, LC_KOI8_R, PG_KOI8R, noError); - - PG_RETURN_INT32(converted); -} - -Datum -iso_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_ISO_8859_5, PG_MULE_INTERNAL); - - converted = latin2mic_with_table(src, dest, len, LC_KOI8_R, PG_ISO_8859_5, iso2koi, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_iso(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_ISO_8859_5); - - converted = mic2latin_with_table(src, dest, len, LC_KOI8_R, PG_ISO_8859_5, koi2iso, noError); - - PG_RETURN_INT32(converted); -} - -Datum -win1251_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_WIN1251, PG_MULE_INTERNAL); - - converted = latin2mic_with_table(src, dest, len, LC_KOI8_R, PG_WIN1251, win12512koi, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_win1251(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_WIN1251); - - converted = mic2latin_with_table(src, dest, len, LC_KOI8_R, PG_WIN1251, koi2win1251, noError); - - PG_RETURN_INT32(converted); -} - -Datum -win866_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_WIN866, PG_MULE_INTERNAL); - - converted = latin2mic_with_table(src, dest, len, LC_KOI8_R, PG_WIN866, win8662koi, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_win866(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_WIN866); - - converted = mic2latin_with_table(src, dest, len, LC_KOI8_R, PG_WIN866, koi2win866, noError); - - PG_RETURN_INT32(converted); -} - Datum koi8r_to_win1251(PG_FUNCTION_ARGS) { diff --git a/src/backend/utils/mb/conversion_procs/cyrillic_and_mic/Makefile b/src/backend/utils/mb/conversion_procs/cyrillic_and_mic/Makefile deleted file mode 100644 index e7cd8e8162a3d..0000000000000 --- a/src/backend/utils/mb/conversion_procs/cyrillic_and_mic/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -#------------------------------------------------------------------------- -# -# src/backend/utils/mb/conversion_procs/cyrillic_and_mic/Makefile -# -#------------------------------------------------------------------------- -subdir = src/backend/utils/mb/conversion_procs/cyrillic_and_mic -top_builddir = ../../../../../.. -include $(top_builddir)/src/Makefile.global - -NAME = cyrillic_and_mic -PGFILEDESC = "cyrillic <-> mic text conversions" - -include $(srcdir)/../proc.mk diff --git a/src/backend/utils/mb/conversion_procs/euc2004_sjis2004/euc2004_sjis2004.c b/src/backend/utils/mb/conversion_procs/euc2004_sjis2004/euc2004_sjis2004.c index 14bd66e16f2b2..756f5a3029d63 100644 --- a/src/backend/utils/mb/conversion_procs/euc2004_sjis2004/euc2004_sjis2004.c +++ b/src/backend/utils/mb/conversion_procs/euc2004_sjis2004/euc2004_sjis2004.c @@ -2,7 +2,7 @@ * * EUC_JIS_2004, SHIFT_JIS_2004 * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/mb/conversion_procs/euc2004_sjis2004/euc2004_sjis2004.c diff --git a/src/backend/utils/mb/conversion_procs/euc_cn_and_mic/Makefile b/src/backend/utils/mb/conversion_procs/euc_cn_and_mic/Makefile deleted file mode 100644 index cb6a83f5841e9..0000000000000 --- a/src/backend/utils/mb/conversion_procs/euc_cn_and_mic/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -#------------------------------------------------------------------------- -# -# src/backend/utils/mb/conversion_procs/euc_cn_and_mic/Makefile -# -#------------------------------------------------------------------------- -subdir = src/backend/utils/mb/conversion_procs/euc_cn_and_mic -top_builddir = ../../../../../.. -include $(top_builddir)/src/Makefile.global - -NAME = euc_cn_and_mic -PGFILEDESC = "euc_cn <-> mic text conversions" - -include $(srcdir)/../proc.mk diff --git a/src/backend/utils/mb/conversion_procs/euc_cn_and_mic/euc_cn_and_mic.c b/src/backend/utils/mb/conversion_procs/euc_cn_and_mic/euc_cn_and_mic.c deleted file mode 100644 index 14e157e14f5b2..0000000000000 --- a/src/backend/utils/mb/conversion_procs/euc_cn_and_mic/euc_cn_and_mic.c +++ /dev/null @@ -1,169 +0,0 @@ -/*------------------------------------------------------------------------- - * - * EUC_CN and MULE_INTERNAL - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/backend/utils/mb/conversion_procs/euc_cn_and_mic/euc_cn_and_mic.c - * - *------------------------------------------------------------------------- - */ - -#include "postgres.h" -#include "fmgr.h" -#include "mb/pg_wchar.h" - -PG_MODULE_MAGIC_EXT( - .name = "euc_cn_and_mic", - .version = PG_VERSION -); - -PG_FUNCTION_INFO_V1(euc_cn_to_mic); -PG_FUNCTION_INFO_V1(mic_to_euc_cn); - -/* ---------- - * conv_proc( - * INTEGER, -- source encoding id - * INTEGER, -- destination encoding id - * CSTRING, -- source string (null terminated C string) - * CSTRING, -- destination string (null terminated C string) - * INTEGER, -- source string length - * BOOL -- if true, don't throw an error if conversion fails - * ) returns INTEGER; - * - * Returns the number of bytes successfully converted. - * ---------- - */ - -static int euc_cn2mic(const unsigned char *euc, unsigned char *p, int len, bool noError); -static int mic2euc_cn(const unsigned char *mic, unsigned char *p, int len, bool noError); - -Datum -euc_cn_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_EUC_CN, PG_MULE_INTERNAL); - - converted = euc_cn2mic(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_euc_cn(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_EUC_CN); - - converted = mic2euc_cn(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -/* - * EUC_CN ---> MIC - */ -static int -euc_cn2mic(const unsigned char *euc, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = euc; - int c1; - - while (len > 0) - { - c1 = *euc; - if (IS_HIGHBIT_SET(c1)) - { - if (len < 2 || !IS_HIGHBIT_SET(euc[1])) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_CN, (const char *) euc, len); - } - *p++ = LC_GB2312_80; - *p++ = c1; - *p++ = euc[1]; - euc += 2; - len -= 2; - } - else - { /* should be ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_CN, (const char *) euc, len); - } - *p++ = c1; - euc++; - len--; - } - } - *p = '\0'; - - return euc - start; -} - -/* - * MIC ---> EUC_CN - */ -static int -mic2euc_cn(const unsigned char *mic, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = mic; - int c1; - - while (len > 0) - { - c1 = *mic; - if (IS_HIGHBIT_SET(c1)) - { - if (c1 != LC_GB2312_80) - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_EUC_CN, - (const char *) mic, len); - } - if (len < 3 || !IS_HIGHBIT_SET(mic[1]) || !IS_HIGHBIT_SET(mic[2])) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - mic++; - *p++ = *mic++; - *p++ = *mic++; - len -= 3; - } - else - { /* should be ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - *p++ = c1; - mic++; - len--; - } - } - *p = '\0'; - - return mic - start; -} diff --git a/src/backend/utils/mb/conversion_procs/euc_jp_and_sjis/euc_jp_and_sjis.c b/src/backend/utils/mb/conversion_procs/euc_jp_and_sjis/euc_jp_and_sjis.c index d2744bd69b290..082c803a8ebd5 100644 --- a/src/backend/utils/mb/conversion_procs/euc_jp_and_sjis/euc_jp_and_sjis.c +++ b/src/backend/utils/mb/conversion_procs/euc_jp_and_sjis/euc_jp_and_sjis.c @@ -1,8 +1,8 @@ /*------------------------------------------------------------------------- * - * EUC_JP, SJIS and MULE_INTERNAL + * EUC_JP and SJIS * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -34,10 +34,6 @@ PG_MODULE_MAGIC_EXT( PG_FUNCTION_INFO_V1(euc_jp_to_sjis); PG_FUNCTION_INFO_V1(sjis_to_euc_jp); -PG_FUNCTION_INFO_V1(euc_jp_to_mic); -PG_FUNCTION_INFO_V1(mic_to_euc_jp); -PG_FUNCTION_INFO_V1(sjis_to_mic); -PG_FUNCTION_INFO_V1(mic_to_sjis); /* ---------- * conv_proc( @@ -53,10 +49,6 @@ PG_FUNCTION_INFO_V1(mic_to_sjis); * ---------- */ -static int sjis2mic(const unsigned char *sjis, unsigned char *p, int len, bool noError); -static int mic2sjis(const unsigned char *mic, unsigned char *p, int len, bool noError); -static int euc_jp2mic(const unsigned char *euc, unsigned char *p, int len, bool noError); -static int mic2euc_jp(const unsigned char *mic, unsigned char *p, int len, bool noError); static int euc_jp2sjis(const unsigned char *euc, unsigned char *p, int len, bool noError); static int sjis2euc_jp(const unsigned char *sjis, unsigned char *p, int len, bool noError); @@ -92,444 +84,6 @@ sjis_to_euc_jp(PG_FUNCTION_ARGS) PG_RETURN_INT32(converted); } -Datum -euc_jp_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_EUC_JP, PG_MULE_INTERNAL); - - converted = euc_jp2mic(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_euc_jp(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_EUC_JP); - - converted = mic2euc_jp(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -sjis_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_SJIS, PG_MULE_INTERNAL); - - converted = sjis2mic(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_sjis(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_SJIS); - - converted = mic2sjis(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -/* - * SJIS ---> MIC - */ -static int -sjis2mic(const unsigned char *sjis, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = sjis; - int c1, - c2, - i, - k, - k2; - - while (len > 0) - { - c1 = *sjis; - if (c1 >= 0xa1 && c1 <= 0xdf) - { - /* JIS X0201 (1 byte kana) */ - *p++ = LC_JISX0201K; - *p++ = c1; - sjis++; - len--; - } - else if (IS_HIGHBIT_SET(c1)) - { - /* - * JIS X0208, X0212, user defined extended characters - */ - if (len < 2 || !ISSJISHEAD(c1) || !ISSJISTAIL(sjis[1])) - { - if (noError) - break; - report_invalid_encoding(PG_SJIS, (const char *) sjis, len); - } - c2 = sjis[1]; - k = (c1 << 8) + c2; - if (k >= 0xed40 && k < 0xf040) - { - /* NEC selection IBM kanji */ - for (i = 0;; i++) - { - k2 = ibmkanji[i].nec; - if (k2 == 0xffff) - break; - if (k2 == k) - { - k = ibmkanji[i].sjis; - c1 = (k >> 8) & 0xff; - c2 = k & 0xff; - } - } - } - - if (k < 0xeb3f) - { - /* JIS X0208 */ - *p++ = LC_JISX0208; - *p++ = ((c1 & 0x3f) << 1) + 0x9f + (c2 > 0x9e); - *p++ = c2 + ((c2 > 0x9e) ? 2 : 0x60) + (c2 < 0x80); - } - else if ((k >= 0xeb40 && k < 0xf040) || (k >= 0xfc4c && k <= 0xfcfc)) - { - /* NEC selection IBM kanji - Other undecided justice */ - *p++ = LC_JISX0208; - *p++ = PGEUCALTCODE >> 8; - *p++ = PGEUCALTCODE & 0xff; - } - else if (k >= 0xf040 && k < 0xf540) - { - /* - * UDC1 mapping to X0208 85 ku - 94 ku JIS code 0x7521 - - * 0x7e7e EUC 0xf5a1 - 0xfefe - */ - *p++ = LC_JISX0208; - c1 -= 0x6f; - *p++ = ((c1 & 0x3f) << 1) + 0xf3 + (c2 > 0x9e); - *p++ = c2 + ((c2 > 0x9e) ? 2 : 0x60) + (c2 < 0x80); - } - else if (k >= 0xf540 && k < 0xfa40) - { - /* - * UDC2 mapping to X0212 85 ku - 94 ku JIS code 0x7521 - - * 0x7e7e EUC 0x8ff5a1 - 0x8ffefe - */ - *p++ = LC_JISX0212; - c1 -= 0x74; - *p++ = ((c1 & 0x3f) << 1) + 0xf3 + (c2 > 0x9e); - *p++ = c2 + ((c2 > 0x9e) ? 2 : 0x60) + (c2 < 0x80); - } - else if (k >= 0xfa40) - { - /* - * mapping IBM kanji to X0208 and X0212 - */ - for (i = 0;; i++) - { - k2 = ibmkanji[i].sjis; - if (k2 == 0xffff) - break; - if (k2 == k) - { - k = ibmkanji[i].euc; - if (k >= 0x8f0000) - { - *p++ = LC_JISX0212; - *p++ = 0x80 | ((k & 0xff00) >> 8); - *p++ = 0x80 | (k & 0xff); - } - else - { - *p++ = LC_JISX0208; - *p++ = 0x80 | (k >> 8); - *p++ = 0x80 | (k & 0xff); - } - } - } - } - sjis += 2; - len -= 2; - } - else - { /* should be ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_SJIS, (const char *) sjis, len); - } - *p++ = c1; - sjis++; - len--; - } - } - *p = '\0'; - - return sjis - start; -} - -/* - * MIC ---> SJIS - */ -static int -mic2sjis(const unsigned char *mic, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = mic; - int c1, - c2, - k, - l; - - while (len > 0) - { - c1 = *mic; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - *p++ = c1; - mic++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_MULE_INTERNAL, (const char *) mic, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - if (c1 == LC_JISX0201K) - *p++ = mic[1]; - else if (c1 == LC_JISX0208) - { - c1 = mic[1]; - c2 = mic[2]; - k = (c1 << 8) | (c2 & 0xff); - if (k >= 0xf5a1) - { - /* UDC1 */ - c1 -= 0x54; - *p++ = ((c1 - 0xa1) >> 1) + ((c1 < 0xdf) ? 0x81 : 0xc1) + 0x6f; - } - else - *p++ = ((c1 - 0xa1) >> 1) + ((c1 < 0xdf) ? 0x81 : 0xc1); - *p++ = c2 - ((c1 & 1) ? ((c2 < 0xe0) ? 0x61 : 0x60) : 2); - } - else if (c1 == LC_JISX0212) - { - int i, - k2; - - c1 = mic[1]; - c2 = mic[2]; - k = c1 << 8 | c2; - if (k >= 0xf5a1) - { - /* UDC2 */ - c1 -= 0x54; - *p++ = ((c1 - 0xa1) >> 1) + ((c1 < 0xdf) ? 0x81 : 0xc1) + 0x74; - *p++ = c2 - ((c1 & 1) ? ((c2 < 0xe0) ? 0x61 : 0x60) : 2); - } - else - { - /* IBM kanji */ - for (i = 0;; i++) - { - k2 = ibmkanji[i].euc & 0xffff; - if (k2 == 0xffff) - { - *p++ = PGSJISALTCODE >> 8; - *p++ = PGSJISALTCODE & 0xff; - break; - } - if (k2 == k) - { - k = ibmkanji[i].sjis; - *p++ = k >> 8; - *p++ = k & 0xff; - break; - } - } - } - } - else - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_SJIS, - (const char *) mic, len); - } - mic += l; - len -= l; - } - *p = '\0'; - - return mic - start; -} - -/* - * EUC_JP ---> MIC - */ -static int -euc_jp2mic(const unsigned char *euc, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = euc; - int c1; - int l; - - while (len > 0) - { - c1 = *euc; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_JP, - (const char *) euc, len); - } - *p++ = c1; - euc++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_EUC_JP, (const char *) euc, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_JP, - (const char *) euc, len); - } - if (c1 == SS2) - { /* 1 byte kana? */ - *p++ = LC_JISX0201K; - *p++ = euc[1]; - } - else if (c1 == SS3) - { /* JIS X0212 kanji? */ - *p++ = LC_JISX0212; - *p++ = euc[1]; - *p++ = euc[2]; - } - else - { /* kanji? */ - *p++ = LC_JISX0208; - *p++ = c1; - *p++ = euc[1]; - } - euc += l; - len -= l; - } - *p = '\0'; - - return euc - start; -} - -/* - * MIC ---> EUC_JP - */ -static int -mic2euc_jp(const unsigned char *mic, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = mic; - int c1; - int l; - - while (len > 0) - { - c1 = *mic; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - *p++ = c1; - mic++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_MULE_INTERNAL, (const char *) mic, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - if (c1 == LC_JISX0201K) - { - *p++ = SS2; - *p++ = mic[1]; - } - else if (c1 == LC_JISX0212) - { - *p++ = SS3; - *p++ = mic[1]; - *p++ = mic[2]; - } - else if (c1 == LC_JISX0208) - { - *p++ = mic[1]; - *p++ = mic[2]; - } - else - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_EUC_JP, - (const char *) mic, len); - } - mic += l; - len -= l; - } - *p = '\0'; - - return mic - start; -} - /* * EUC_JP -> SJIS */ diff --git a/src/backend/utils/mb/conversion_procs/euc_kr_and_mic/Makefile b/src/backend/utils/mb/conversion_procs/euc_kr_and_mic/Makefile deleted file mode 100644 index d43b082bd5fa3..0000000000000 --- a/src/backend/utils/mb/conversion_procs/euc_kr_and_mic/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -#------------------------------------------------------------------------- -# -# src/backend/utils/mb/conversion_procs/euc_kr_and_mic/Makefile -# -#------------------------------------------------------------------------- -subdir = src/backend/utils/mb/conversion_procs/euc_kr_and_mic -top_builddir = ../../../../../.. -include $(top_builddir)/src/Makefile.global - -NAME = euc_kr_and_mic -PGFILEDESC = "euc_kr <-> mic text conversions" - -include $(srcdir)/../proc.mk diff --git a/src/backend/utils/mb/conversion_procs/euc_kr_and_mic/euc_kr_and_mic.c b/src/backend/utils/mb/conversion_procs/euc_kr_and_mic/euc_kr_and_mic.c deleted file mode 100644 index 0213768c452b2..0000000000000 --- a/src/backend/utils/mb/conversion_procs/euc_kr_and_mic/euc_kr_and_mic.c +++ /dev/null @@ -1,177 +0,0 @@ -/*------------------------------------------------------------------------- - * - * EUC_KR and MULE_INTERNAL - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/backend/utils/mb/conversion_procs/euc_kr_and_mic/euc_kr_and_mic.c - * - *------------------------------------------------------------------------- - */ - -#include "postgres.h" -#include "fmgr.h" -#include "mb/pg_wchar.h" - -PG_MODULE_MAGIC_EXT( - .name = "euc_kr_and_mic", - .version = PG_VERSION -); - -PG_FUNCTION_INFO_V1(euc_kr_to_mic); -PG_FUNCTION_INFO_V1(mic_to_euc_kr); - -/* ---------- - * conv_proc( - * INTEGER, -- source encoding id - * INTEGER, -- destination encoding id - * CSTRING, -- source string (null terminated C string) - * CSTRING, -- destination string (null terminated C string) - * INTEGER, -- source string length - * BOOL -- if true, don't throw an error if conversion fails - * ) returns INTEGER; - * - * Returns the number of bytes successfully converted. - * ---------- - */ - -static int euc_kr2mic(const unsigned char *euc, unsigned char *p, int len, bool noError); -static int mic2euc_kr(const unsigned char *mic, unsigned char *p, int len, bool noError); - -Datum -euc_kr_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_EUC_KR, PG_MULE_INTERNAL); - - converted = euc_kr2mic(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_euc_kr(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_EUC_KR); - - converted = mic2euc_kr(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -/* - * EUC_KR ---> MIC - */ -static int -euc_kr2mic(const unsigned char *euc, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = euc; - int c1; - int l; - - while (len > 0) - { - c1 = *euc; - if (IS_HIGHBIT_SET(c1)) - { - l = pg_encoding_verifymbchar(PG_EUC_KR, (const char *) euc, len); - if (l != 2) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_KR, - (const char *) euc, len); - } - *p++ = LC_KS5601; - *p++ = c1; - *p++ = euc[1]; - euc += 2; - len -= 2; - } - else - { /* should be ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_KR, - (const char *) euc, len); - } - *p++ = c1; - euc++; - len--; - } - } - *p = '\0'; - - return euc - start; -} - -/* - * MIC ---> EUC_KR - */ -static int -mic2euc_kr(const unsigned char *mic, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = mic; - int c1; - int l; - - while (len > 0) - { - c1 = *mic; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - *p++ = c1; - mic++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_MULE_INTERNAL, (const char *) mic, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - if (c1 == LC_KS5601) - { - *p++ = mic[1]; - *p++ = mic[2]; - } - else - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_EUC_KR, - (const char *) mic, len); - } - mic += l; - len -= l; - } - *p = '\0'; - - return mic - start; -} diff --git a/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c b/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c index 68f76aa8cb82b..812fab9e6f696 100644 --- a/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c +++ b/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/big5.c @@ -1,6 +1,5 @@ /* - * conversion between BIG5 and Mule Internal Code(CNS 116643-1992 - * plane 1 and plane 2). + * BIG5 support functions (CNS 116643-1992 * plane 1 and plane 2). * This program is partially copied from lv(Multilingual file viewer) * and slightly modified. lv is written and copyrighted by NARITA Tomio * (nrt@web.ad.jp). @@ -370,6 +369,7 @@ CNStoBIG5(unsigned short cns, unsigned char lc) if (b1c4[i][1] == cns) return b1c4[i][0]; } + break; default: break; } diff --git a/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/euc_tw_and_big5.c b/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/euc_tw_and_big5.c index c1834ca41819e..4dac49b3b901d 100644 --- a/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/euc_tw_and_big5.c +++ b/src/backend/utils/mb/conversion_procs/euc_tw_and_big5/euc_tw_and_big5.c @@ -1,8 +1,8 @@ /*------------------------------------------------------------------------- * - * EUC_TW, BIG5 and MULE_INTERNAL + * EUC_TW and BIG5 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,10 +22,6 @@ PG_MODULE_MAGIC_EXT( PG_FUNCTION_INFO_V1(euc_tw_to_big5); PG_FUNCTION_INFO_V1(big5_to_euc_tw); -PG_FUNCTION_INFO_V1(euc_tw_to_mic); -PG_FUNCTION_INFO_V1(mic_to_euc_tw); -PG_FUNCTION_INFO_V1(big5_to_mic); -PG_FUNCTION_INFO_V1(mic_to_big5); /* ---------- * conv_proc( @@ -43,10 +39,6 @@ PG_FUNCTION_INFO_V1(mic_to_big5); static int euc_tw2big5(const unsigned char *euc, unsigned char *p, int len, bool noError); static int big52euc_tw(const unsigned char *big5, unsigned char *p, int len, bool noError); -static int big52mic(const unsigned char *big5, unsigned char *p, int len, bool noError); -static int mic2big5(const unsigned char *mic, unsigned char *p, int len, bool noError); -static int euc_tw2mic(const unsigned char *euc, unsigned char *p, int len, bool noError); -static int mic2euc_tw(const unsigned char *mic, unsigned char *p, int len, bool noError); Datum euc_tw_to_big5(PG_FUNCTION_ARGS) @@ -80,74 +72,6 @@ big5_to_euc_tw(PG_FUNCTION_ARGS) PG_RETURN_INT32(converted); } -Datum -euc_tw_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_EUC_TW, PG_MULE_INTERNAL); - - converted = euc_tw2mic(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_euc_tw(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_EUC_TW); - - converted = mic2euc_tw(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -big5_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_BIG5, PG_MULE_INTERNAL); - - converted = big52mic(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_big5(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_BIG5); - - converted = mic2big5(src, dest, len, noError); - - PG_RETURN_INT32(converted); -} - - -/* - * EUC_TW ---> Big5 - */ static int euc_tw2big5(const unsigned char *euc, unsigned char *p, int len, bool noError) { @@ -303,281 +227,3 @@ big52euc_tw(const unsigned char *big5, unsigned char *p, int len, bool noError) return big5 - start; } - -/* - * EUC_TW ---> MIC - */ -static int -euc_tw2mic(const unsigned char *euc, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = euc; - int c1; - int l; - - while (len > 0) - { - c1 = *euc; - if (IS_HIGHBIT_SET(c1)) - { - l = pg_encoding_verifymbchar(PG_EUC_TW, (const char *) euc, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_TW, - (const char *) euc, len); - } - if (c1 == SS2) - { - c1 = euc[1]; /* plane No. */ - if (c1 == 0xa1) - *p++ = LC_CNS11643_1; - else if (c1 == 0xa2) - *p++ = LC_CNS11643_2; - else - { - /* other planes are MULE private charsets */ - *p++ = LCPRV2_B; - *p++ = c1 - 0xa3 + LC_CNS11643_3; - } - *p++ = euc[2]; - *p++ = euc[3]; - } - else - { /* CNS11643-1 */ - *p++ = LC_CNS11643_1; - *p++ = c1; - *p++ = euc[1]; - } - euc += l; - len -= l; - } - else - { /* should be ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_EUC_TW, - (const char *) euc, len); - } - *p++ = c1; - euc++; - len--; - } - } - *p = '\0'; - - return euc - start; -} - -/* - * MIC ---> EUC_TW - */ -static int -mic2euc_tw(const unsigned char *mic, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = mic; - int c1; - int l; - - while (len > 0) - { - c1 = *mic; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - *p++ = c1; - mic++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_MULE_INTERNAL, (const char *) mic, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - if (c1 == LC_CNS11643_1) - { - *p++ = mic[1]; - *p++ = mic[2]; - } - else if (c1 == LC_CNS11643_2) - { - *p++ = SS2; - *p++ = 0xa2; - *p++ = mic[1]; - *p++ = mic[2]; - } - else if (c1 == LCPRV2_B && - mic[1] >= LC_CNS11643_3 && mic[1] <= LC_CNS11643_7) - { - *p++ = SS2; - *p++ = mic[1] - LC_CNS11643_3 + 0xa3; - *p++ = mic[2]; - *p++ = mic[3]; - } - else - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_EUC_TW, - (const char *) mic, len); - } - mic += l; - len -= l; - } - *p = '\0'; - - return mic - start; -} - -/* - * Big5 ---> MIC - */ -static int -big52mic(const unsigned char *big5, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = big5; - unsigned short c1; - unsigned short big5buf, - cnsBuf; - unsigned char lc; - int l; - - while (len > 0) - { - c1 = *big5; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_BIG5, - (const char *) big5, len); - } - *p++ = c1; - big5++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_BIG5, (const char *) big5, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_BIG5, - (const char *) big5, len); - } - big5buf = (c1 << 8) | big5[1]; - cnsBuf = BIG5toCNS(big5buf, &lc); - if (lc != 0) - { - /* Planes 3 and 4 are MULE private charsets */ - if (lc == LC_CNS11643_3 || lc == LC_CNS11643_4) - *p++ = LCPRV2_B; - *p++ = lc; /* Plane No. */ - *p++ = (cnsBuf >> 8) & 0x00ff; - *p++ = cnsBuf & 0x00ff; - } - else - { - if (noError) - break; - report_untranslatable_char(PG_BIG5, PG_MULE_INTERNAL, - (const char *) big5, len); - } - big5 += l; - len -= l; - } - *p = '\0'; - - return big5 - start; -} - -/* - * MIC ---> Big5 - */ -static int -mic2big5(const unsigned char *mic, unsigned char *p, int len, bool noError) -{ - const unsigned char *start = mic; - unsigned short c1; - unsigned short big5buf, - cnsBuf; - int l; - - while (len > 0) - { - c1 = *mic; - if (!IS_HIGHBIT_SET(c1)) - { - /* ASCII */ - if (c1 == 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - *p++ = c1; - mic++; - len--; - continue; - } - l = pg_encoding_verifymbchar(PG_MULE_INTERNAL, (const char *) mic, len); - if (l < 0) - { - if (noError) - break; - report_invalid_encoding(PG_MULE_INTERNAL, - (const char *) mic, len); - } - if (c1 == LC_CNS11643_1 || c1 == LC_CNS11643_2 || c1 == LCPRV2_B) - { - if (c1 == LCPRV2_B) - { - c1 = mic[1]; /* get plane no. */ - cnsBuf = (mic[2] << 8) | mic[3]; - } - else - { - cnsBuf = (mic[1] << 8) | mic[2]; - } - big5buf = CNStoBIG5(cnsBuf, c1); - if (big5buf == 0) - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_BIG5, - (const char *) mic, len); - } - *p++ = (big5buf >> 8) & 0x00ff; - *p++ = big5buf & 0x00ff; - } - else - { - if (noError) - break; - report_untranslatable_char(PG_MULE_INTERNAL, PG_BIG5, - (const char *) mic, len); - } - mic += l; - len -= l; - } - *p = '\0'; - - return mic - start; -} diff --git a/src/backend/utils/mb/conversion_procs/latin2_and_win1250/latin2_and_win1250.c b/src/backend/utils/mb/conversion_procs/latin2_and_win1250/latin2_and_win1250.c index 803705282643a..29201c9443444 100644 --- a/src/backend/utils/mb/conversion_procs/latin2_and_win1250/latin2_and_win1250.c +++ b/src/backend/utils/mb/conversion_procs/latin2_and_win1250/latin2_and_win1250.c @@ -2,7 +2,7 @@ * * LATIN2 and WIN1250 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -20,10 +20,6 @@ PG_MODULE_MAGIC_EXT( .version = PG_VERSION ); -PG_FUNCTION_INFO_V1(latin2_to_mic); -PG_FUNCTION_INFO_V1(mic_to_latin2); -PG_FUNCTION_INFO_V1(win1250_to_mic); -PG_FUNCTION_INFO_V1(mic_to_win1250); PG_FUNCTION_INFO_V1(latin2_to_win1250); PG_FUNCTION_INFO_V1(win1250_to_latin2); @@ -82,72 +78,6 @@ static const unsigned char iso88592_2_win1250[] = { }; -Datum -latin2_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_LATIN2, PG_MULE_INTERNAL); - - converted = latin2mic(src, dest, len, LC_ISO8859_2, PG_LATIN2, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_latin2(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_LATIN2); - - converted = mic2latin(src, dest, len, LC_ISO8859_2, PG_LATIN2, noError); - - PG_RETURN_INT32(converted); -} - -Datum -win1250_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_WIN1250, PG_MULE_INTERNAL); - - converted = latin2mic_with_table(src, dest, len, LC_ISO8859_2, PG_WIN1250, - win1250_2_iso88592, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_win1250(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_WIN1250); - - converted = mic2latin_with_table(src, dest, len, LC_ISO8859_2, PG_WIN1250, - iso88592_2_win1250, noError); - - PG_RETURN_INT32(converted); -} - Datum latin2_to_win1250(PG_FUNCTION_ARGS) { diff --git a/src/backend/utils/mb/conversion_procs/latin_and_mic/latin_and_mic.c b/src/backend/utils/mb/conversion_procs/latin_and_mic/latin_and_mic.c deleted file mode 100644 index 19757afa2d91f..0000000000000 --- a/src/backend/utils/mb/conversion_procs/latin_and_mic/latin_and_mic.c +++ /dev/null @@ -1,139 +0,0 @@ -/*------------------------------------------------------------------------- - * - * LATINn and MULE_INTERNAL - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/backend/utils/mb/conversion_procs/latin_and_mic/latin_and_mic.c - * - *------------------------------------------------------------------------- - */ - -#include "postgres.h" -#include "fmgr.h" -#include "mb/pg_wchar.h" - -PG_MODULE_MAGIC_EXT( - .name = "latin_and_mic", - .version = PG_VERSION -); - -PG_FUNCTION_INFO_V1(latin1_to_mic); -PG_FUNCTION_INFO_V1(mic_to_latin1); -PG_FUNCTION_INFO_V1(latin3_to_mic); -PG_FUNCTION_INFO_V1(mic_to_latin3); -PG_FUNCTION_INFO_V1(latin4_to_mic); -PG_FUNCTION_INFO_V1(mic_to_latin4); - -/* ---------- - * conv_proc( - * INTEGER, -- source encoding id - * INTEGER, -- destination encoding id - * CSTRING, -- source string (null terminated C string) - * CSTRING, -- destination string (null terminated C string) - * INTEGER, -- source string length - * BOOL -- if true, don't throw an error if conversion fails - * ) returns INTEGER; - * - * Returns the number of bytes successfully converted. - * ---------- - */ - - -Datum -latin1_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_LATIN1, PG_MULE_INTERNAL); - - converted = latin2mic(src, dest, len, LC_ISO8859_1, PG_LATIN1, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_latin1(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_LATIN1); - - converted = mic2latin(src, dest, len, LC_ISO8859_1, PG_LATIN1, noError); - - PG_RETURN_INT32(converted); -} - -Datum -latin3_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_LATIN3, PG_MULE_INTERNAL); - - converted = latin2mic(src, dest, len, LC_ISO8859_3, PG_LATIN3, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_latin3(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_LATIN3); - - converted = mic2latin(src, dest, len, LC_ISO8859_3, PG_LATIN3, noError); - - PG_RETURN_INT32(converted); -} - -Datum -latin4_to_mic(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_LATIN4, PG_MULE_INTERNAL); - - converted = latin2mic(src, dest, len, LC_ISO8859_4, PG_LATIN4, noError); - - PG_RETURN_INT32(converted); -} - -Datum -mic_to_latin4(PG_FUNCTION_ARGS) -{ - unsigned char *src = (unsigned char *) PG_GETARG_CSTRING(2); - unsigned char *dest = (unsigned char *) PG_GETARG_CSTRING(3); - int len = PG_GETARG_INT32(4); - bool noError = PG_GETARG_BOOL(5); - int converted; - - CHECK_ENCODING_CONVERSION_ARGS(PG_MULE_INTERNAL, PG_LATIN4); - - converted = mic2latin(src, dest, len, LC_ISO8859_4, PG_LATIN4, noError); - - PG_RETURN_INT32(converted); -} diff --git a/src/backend/utils/mb/conversion_procs/meson.build b/src/backend/utils/mb/conversion_procs/meson.build index 0e8273e0b615e..6c6fd30a01fe4 100644 --- a/src/backend/utils/mb/conversion_procs/meson.build +++ b/src/backend/utils/mb/conversion_procs/meson.build @@ -1,17 +1,14 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group encodings = { - 'cyrillic_and_mic': ['cyrillic_and_mic/cyrillic_and_mic.c'], + 'cyrillic': ['cyrillic/cyrillic.c'], 'euc2004_sjis2004': ['euc2004_sjis2004/euc2004_sjis2004.c'], - 'euc_cn_and_mic': ['euc_cn_and_mic/euc_cn_and_mic.c'], 'euc_jp_and_sjis': ['euc_jp_and_sjis/euc_jp_and_sjis.c'], - 'euc_kr_and_mic': ['euc_kr_and_mic/euc_kr_and_mic.c'], 'euc_tw_and_big5': [ 'euc_tw_and_big5/euc_tw_and_big5.c', 'euc_tw_and_big5/big5.c', ], 'latin2_and_win1250': ['latin2_and_win1250/latin2_and_win1250.c'], - 'latin_and_mic': ['latin_and_mic/latin_and_mic.c'], 'utf8_and_big5': ['utf8_and_big5/utf8_and_big5.c'], 'utf8_and_cyrillic': ['utf8_and_cyrillic/utf8_and_cyrillic.c'], 'utf8_and_euc2004': ['utf8_and_euc2004/utf8_and_euc2004.c'], diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_big5/utf8_and_big5.c b/src/backend/utils/mb/conversion_procs/utf8_and_big5/utf8_and_big5.c index eae2d2d69f305..e66c6269242aa 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_big5/utf8_and_big5.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_big5/utf8_and_big5.c @@ -2,7 +2,7 @@ * * BIG5 <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_cyrillic/utf8_and_cyrillic.c b/src/backend/utils/mb/conversion_procs/utf8_and_cyrillic/utf8_and_cyrillic.c index 5addade582fd3..3b116cdd83947 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_cyrillic/utf8_and_cyrillic.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_cyrillic/utf8_and_cyrillic.c @@ -2,7 +2,7 @@ * * UTF8 and Cyrillic * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_euc2004/utf8_and_euc2004.c b/src/backend/utils/mb/conversion_procs/utf8_and_euc2004/utf8_and_euc2004.c index 3e660da89b846..f20d80e7196ba 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_euc2004/utf8_and_euc2004.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_euc2004/utf8_and_euc2004.c @@ -2,7 +2,7 @@ * * EUC_JIS_2004 <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_euc_cn/utf8_and_euc_cn.c b/src/backend/utils/mb/conversion_procs/utf8_and_euc_cn/utf8_and_euc_cn.c index 260b75c6bc5f4..f397e61db6d60 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_euc_cn/utf8_and_euc_cn.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_euc_cn/utf8_and_euc_cn.c @@ -2,7 +2,7 @@ * * EUC_CN <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_euc_jp/utf8_and_euc_jp.c b/src/backend/utils/mb/conversion_procs/utf8_and_euc_jp/utf8_and_euc_jp.c index ad11594753deb..6f4787dfc0e87 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_euc_jp/utf8_and_euc_jp.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_euc_jp/utf8_and_euc_jp.c @@ -2,7 +2,7 @@ * * EUC_JP <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_euc_kr/utf8_and_euc_kr.c b/src/backend/utils/mb/conversion_procs/utf8_and_euc_kr/utf8_and_euc_kr.c index e3f953263f3ab..fabd2d13a760d 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_euc_kr/utf8_and_euc_kr.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_euc_kr/utf8_and_euc_kr.c @@ -2,7 +2,7 @@ * * EUC_KR <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_euc_tw/utf8_and_euc_tw.c b/src/backend/utils/mb/conversion_procs/utf8_and_euc_tw/utf8_and_euc_tw.c index 25663bbda5d45..e3dfac553a367 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_euc_tw/utf8_and_euc_tw.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_euc_tw/utf8_and_euc_tw.c @@ -2,7 +2,7 @@ * * EUC_TW <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_gb18030/utf8_and_gb18030.c b/src/backend/utils/mb/conversion_procs/utf8_and_gb18030/utf8_and_gb18030.c index ffc9c58cd130b..8c98fcc91abac 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_gb18030/utf8_and_gb18030.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_gb18030/utf8_and_gb18030.c @@ -2,7 +2,7 @@ * * GB18030 <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -124,7 +124,12 @@ utf8word_to_unicode(uint32 c) /* * Perform mapping of GB18030 ranges to UTF8 * - * The ranges we need to convert are specified in gb-18030-2000.xml. + * General description, and the range we need to convert for U+10000 and up: + * https://htmlpreview.github.io/?https://github.com/unicode-org/icu-data/blob/main/charset/source/gb18030/gb18030.html + * + * Ranges up to U+FFFF: + * https://github.com/unicode-org/icu-data/blob/main/charset/source/gb18030/ranges.txt + * * All are ranges of 4-byte GB18030 codes. */ static uint32 diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_gbk/utf8_and_gbk.c b/src/backend/utils/mb/conversion_procs/utf8_and_gbk/utf8_and_gbk.c index 9adc0ce7d8963..fa9595e14279f 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_gbk/utf8_and_gbk.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_gbk/utf8_and_gbk.c @@ -2,7 +2,7 @@ * * GBK <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c b/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c index 5a15981b2dee3..bcfa368406b9d 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_iso8859/utf8_and_iso8859.c @@ -2,7 +2,7 @@ * * ISO 8859 2-16 <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_iso8859_1/utf8_and_iso8859_1.c b/src/backend/utils/mb/conversion_procs/utf8_and_iso8859_1/utf8_and_iso8859_1.c index c077b986bcdf0..4d829e7eb034c 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_iso8859_1/utf8_and_iso8859_1.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_iso8859_1/utf8_and_iso8859_1.c @@ -2,7 +2,7 @@ * * ISO8859_1 <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_johab/utf8_and_johab.c b/src/backend/utils/mb/conversion_procs/utf8_and_johab/utf8_and_johab.c index 08e38026a408b..52045b039a636 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_johab/utf8_and_johab.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_johab/utf8_and_johab.c @@ -2,7 +2,7 @@ * * JOHAB <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_sjis/utf8_and_sjis.c b/src/backend/utils/mb/conversion_procs/utf8_and_sjis/utf8_and_sjis.c index 911a6342c602e..bca4ea2489828 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_sjis/utf8_and_sjis.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_sjis/utf8_and_sjis.c @@ -2,7 +2,7 @@ * * SJIS <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_sjis2004/utf8_and_sjis2004.c b/src/backend/utils/mb/conversion_procs/utf8_and_sjis2004/utf8_and_sjis2004.c index d0361784a3952..e585377b4f4e5 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_sjis2004/utf8_and_sjis2004.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_sjis2004/utf8_and_sjis2004.c @@ -2,7 +2,7 @@ * * SHIFT_JIS_2004 <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_uhc/utf8_and_uhc.c b/src/backend/utils/mb/conversion_procs/utf8_and_uhc/utf8_and_uhc.c index 891a17014a105..a60319857d302 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_uhc/utf8_and_uhc.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_uhc/utf8_and_uhc.c @@ -2,7 +2,7 @@ * * UHC <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c b/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c index 24c0dd9a5524d..cf56fc16e8dc1 100644 --- a/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c +++ b/src/backend/utils/mb/conversion_procs/utf8_and_win/utf8_and_win.c @@ -2,7 +2,7 @@ * * WIN <--> UTF8 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad87183..bfbe0d28075b3 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -23,7 +23,7 @@ * the result is validly encoded according to the destination encoding. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -38,6 +38,7 @@ #include "catalog/namespace.h" #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" +#include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/relcache.h" #include "varatt.h" @@ -97,6 +98,13 @@ static char *perform_default_encoding_conversion(const char *src, int len, bool is_client_to_server); static int cliplen(const char *str, int len, int limit); +pg_noreturn +static void report_invalid_encoding_int(int encoding, const char *mbstr, + int mblen, int len); + +pg_noreturn +static void report_invalid_encoding_db(const char *mbstr, int mblen, int len); + /* * Prepare for a future call to SetClientEncoding. Success should mean @@ -497,7 +505,8 @@ pg_do_encoding_conversion_buf(Oid proc, * Convert string to encoding encoding_name. The source * encoding is the DB encoding. * - * BYTEA convert_to(TEXT string, NAME encoding_name) */ + * BYTEA convert_to(TEXT string, NAME encoding_name) + */ Datum pg_convert_to(PG_FUNCTION_ARGS) { @@ -522,7 +531,8 @@ pg_convert_to(PG_FUNCTION_ARGS) * Convert string from encoding encoding_name. The destination * encoding is the DB encoding. * - * TEXT convert_from(BYTEA string, NAME encoding_name) */ + * TEXT convert_from(BYTEA string, NAME encoding_name) + */ Datum pg_convert_from(PG_FUNCTION_ARGS) { @@ -862,7 +872,7 @@ perform_default_encoding_conversion(const char *src, int len, * may call this outside any transaction, or in an aborted transaction. */ void -pg_unicode_to_server(pg_wchar c, unsigned char *s) +pg_unicode_to_server(char32_t c, unsigned char *s) { unsigned char c_as_utf8[MAX_MULTIBYTE_CHAR_LEN + 1]; int c_as_utf8_len; @@ -924,7 +934,7 @@ pg_unicode_to_server(pg_wchar c, unsigned char *s) * but simply return false on conversion failure. */ bool -pg_unicode_to_server_noerror(pg_wchar c, unsigned char *s) +pg_unicode_to_server_noerror(char32_t c, unsigned char *s) { unsigned char c_as_utf8[MAX_MULTIBYTE_CHAR_LEN + 1]; int c_as_utf8_len; @@ -1019,11 +1029,128 @@ pg_encoding_wchar2mb_with_len(int encoding, return pg_wchar_table[encoding].wchar2mb_with_len(from, (unsigned char *) to, len); } -/* returns the byte length of a multibyte character */ +/* + * Returns the byte length of a multibyte character sequence in a + * null-terminated string. Raises an illegal byte sequence error if the + * sequence would hit a null terminator. + * + * The caller is expected to have checked for a terminator at *mbstr == 0 + * before calling, but some callers want 1 in that case, so this function + * continues that tradition. + * + * This must only be used for strings that have a null-terminator to enable + * bounds detection. + */ +int +pg_mblen_cstr(const char *mbstr) +{ + int length = pg_wchar_table[DatabaseEncoding->encoding].mblen((const unsigned char *) mbstr); + + /* + * The .mblen functions return 1 when given a pointer to a terminator. + * Some callers depend on that, so we tolerate it for now. Well-behaved + * callers check the leading byte for a terminator *before* calling. + */ + for (int i = 1; i < length; ++i) + if (unlikely(mbstr[i] == 0)) + report_invalid_encoding_db(mbstr, length, i); + + /* + * String should be NUL-terminated, but checking that would make typical + * callers O(N^2), tripling Valgrind check-world time. Unless + * VALGRIND_EXPENSIVE, check 1 byte after each actual character. (If we + * found a character, not a terminator, the next byte must be a terminator + * or the start of the next character.) If the caller iterates the whole + * string, the last call will diagnose a missing terminator. + */ + if (mbstr[0] != '\0') + { +#ifdef VALGRIND_EXPENSIVE + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr, strlen(mbstr)); +#else + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr + length, 1); +#endif + } + + return length; +} + +/* + * Returns the byte length of a multibyte character sequence bounded by a range + * [mbstr, end) of at least one byte in size. Raises an illegal byte sequence + * error if the sequence would exceed the range. + */ +int +pg_mblen_range(const char *mbstr, const char *end) +{ + int length = pg_wchar_table[DatabaseEncoding->encoding].mblen((const unsigned char *) mbstr); + + Assert(end > mbstr); + + if (unlikely(mbstr + length > end)) + report_invalid_encoding_db(mbstr, length, end - mbstr); + +#ifdef VALGRIND_EXPENSIVE + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr, end - mbstr); +#else + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr, length); +#endif + + return length; +} + +/* + * Returns the byte length of a multibyte character sequence bounded by a range + * extending for 'limit' bytes, which must be at least one. Raises an illegal + * byte sequence error if the sequence would exceed the range. + */ +int +pg_mblen_with_len(const char *mbstr, int limit) +{ + int length = pg_wchar_table[DatabaseEncoding->encoding].mblen((const unsigned char *) mbstr); + + Assert(limit >= 1); + + if (unlikely(length > limit)) + report_invalid_encoding_db(mbstr, length, limit); + +#ifdef VALGRIND_EXPENSIVE + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr, limit); +#else + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr, length); +#endif + + return length; +} + + +/* + * Returns the length of a multibyte character sequence, without any + * validation of bounds. + * + * PLEASE NOTE: This function can only be used safely if the caller has + * already verified the input string, since otherwise there is a risk of + * overrunning the buffer if the string is invalid. A prior call to a + * pg_mbstrlen* function suffices. + */ +int +pg_mblen_unbounded(const char *mbstr) +{ + int length = pg_wchar_table[DatabaseEncoding->encoding].mblen((const unsigned char *) mbstr); + + VALGRIND_CHECK_MEM_IS_DEFINED(mbstr, length); + + return length; +} + +/* + * Historical name for pg_mblen_unbounded(). Should not be used and will be + * removed in a later version. + */ int pg_mblen(const char *mbstr) { - return pg_wchar_table[DatabaseEncoding->encoding].mblen((const unsigned char *) mbstr); + return pg_mblen_unbounded(mbstr); } /* returns the display length of a multibyte character */ @@ -1045,14 +1172,14 @@ pg_mbstrlen(const char *mbstr) while (*mbstr) { - mbstr += pg_mblen(mbstr); + mbstr += pg_mblen_cstr(mbstr); len++; } return len; } /* returns the length (counted in wchars) of a multibyte string - * (not necessarily NULL terminated) + * (stops at the first of "limit" or a NUL) */ int pg_mbstrlen_with_len(const char *mbstr, int limit) @@ -1065,7 +1192,7 @@ pg_mbstrlen_with_len(const char *mbstr, int limit) while (limit > 0 && *mbstr) { - int l = pg_mblen(mbstr); + int l = pg_mblen_with_len(mbstr, limit); limit -= l; mbstr += l; @@ -1135,7 +1262,7 @@ pg_mbcharcliplen(const char *mbstr, int len, int limit) while (len > 0 && *mbstr) { - l = pg_mblen(mbstr); + l = pg_mblen_with_len(mbstr, len); nch++; if (nch > limit) break; @@ -1181,8 +1308,7 @@ SetMessageEncoding(int encoding) #ifdef ENABLE_NLS /* * Make one bind_textdomain_codeset() call, translating a pg_enc to a gettext - * codeset. Fails for MULE_INTERNAL, an encoding unknown to gettext; can also - * fail for gettext-internal causes like out-of-memory. + * codeset. Can fail for gettext-internal causes like out-of-memory. */ static bool raw_pg_bind_textdomain_codeset(const char *domainname, int encoding) @@ -1302,8 +1428,7 @@ PG_encoding_to_char(PG_FUNCTION_ARGS) /* * gettext() returns messages in this encoding. This often matches the * database encoding, but it differs for SQL_ASCII databases, for processes - * not attached to a database, and under a database encoding lacking iconv - * support (MULE_INTERNAL). + * not attached to a database. */ int GetMessageEncoding(void) @@ -1374,7 +1499,7 @@ pg_utf8_increment(unsigned char *charptr, int length) charptr[3]++; break; } - /* FALL THRU */ + pg_fallthrough; case 3: a = charptr[2]; if (a < 0xBF) @@ -1382,7 +1507,7 @@ pg_utf8_increment(unsigned char *charptr, int length) charptr[2]++; break; } - /* FALL THRU */ + pg_fallthrough; case 2: a = charptr[1]; switch (*charptr) @@ -1402,7 +1527,7 @@ pg_utf8_increment(unsigned char *charptr, int length) charptr[1]++; break; } - /* FALL THRU */ + pg_fallthrough; case 1: a = *charptr; if (a == 0x7F || a == 0xDF || a == 0xEF || a == 0xF4) @@ -1699,12 +1824,19 @@ void report_invalid_encoding(int encoding, const char *mbstr, int len) { int l = pg_encoding_mblen_or_incomplete(encoding, mbstr, len); + + report_invalid_encoding_int(encoding, mbstr, l, len); +} + +static void +report_invalid_encoding_int(int encoding, const char *mbstr, int mblen, int len) +{ char buf[8 * 5 + 1]; char *p = buf; int j, jlimit; - jlimit = Min(l, len); + jlimit = Min(mblen, len); jlimit = Min(jlimit, 8); /* prevent buffer overrun */ for (j = 0; j < jlimit; j++) @@ -1721,6 +1853,12 @@ report_invalid_encoding(int encoding, const char *mbstr, int len) buf))); } +static void +report_invalid_encoding_db(const char *mbstr, int mblen, int len) +{ + report_invalid_encoding_int(GetDatabaseEncoding(), mbstr, mblen, len); +} + /* * report_untranslatable_char: complain about untranslatable character * @@ -1792,7 +1930,7 @@ pgwin32_message_to_UTF16(const char *str, int len, int *utf16len) */ if (codepage != 0) { - utf16 = (WCHAR *) palloc(sizeof(WCHAR) * (len + 1)); + utf16 = palloc_array(WCHAR, len + 1); dstlen = MultiByteToWideChar(codepage, 0, str, len, utf16, len); utf16[dstlen] = (WCHAR) 0; } @@ -1816,7 +1954,7 @@ pgwin32_message_to_UTF16(const char *str, int len, int *utf16len) else utf8 = (char *) str; - utf16 = (WCHAR *) palloc(sizeof(WCHAR) * (len + 1)); + utf16 = palloc_array(WCHAR, len + 1); dstlen = MultiByteToWideChar(CP_UTF8, 0, utf8, len, utf16, len); utf16[dstlen] = (WCHAR) 0; diff --git a/src/backend/utils/mb/meson.build b/src/backend/utils/mb/meson.build index 3569b39f6265d..79bb89069f0ea 100644 --- a/src/backend/utils/mb/meson.build +++ b/src/backend/utils/mb/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'conv.c', diff --git a/src/backend/utils/mb/stringinfo_mb.c b/src/backend/utils/mb/stringinfo_mb.c index 2f40cf745c413..8ad3fa503fa7b 100644 --- a/src/backend/utils/mb/stringinfo_mb.c +++ b/src/backend/utils/mb/stringinfo_mb.c @@ -8,7 +8,7 @@ * code. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/meson.build b/src/backend/utils/meson.build index 6891195ecb45b..7d0377944febd 100644 --- a/src/backend/utils/meson.build +++ b/src/backend/utils/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group install_data('errcodes.txt', install_dir: dir_data, diff --git a/src/backend/utils/misc/Makefile b/src/backend/utils/misc/Makefile index b362ae437710d..f142d17178bdd 100644 --- a/src/backend/utils/misc/Makefile +++ b/src/backend/utils/misc/Makefile @@ -40,6 +40,9 @@ ifdef krb_srvtab override CPPFLAGS += -DPG_KRB_SRVTAB='"$(krb_srvtab)"' endif +# Force this dependency to be known even without dependency info built: +guc_tables.o: guc_tables.c $(top_builddir)/src/backend/utils/guc_tables.inc.c + include $(top_srcdir)/src/backend/common.mk clean: diff --git a/src/backend/utils/misc/conffiles.c b/src/backend/utils/misc/conffiles.c index 23ebad4749b59..3148aa2cc191b 100644 --- a/src/backend/utils/misc/conffiles.c +++ b/src/backend/utils/misc/conffiles.c @@ -7,7 +7,7 @@ * used by PostgreSQL, be they related to GUCs or authentication. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -108,7 +108,7 @@ GetConfFilesInDir(const char *includedir, const char *calling_file, * them prior to caller processing the contents. */ size_filenames = 32; - filenames = (char **) palloc(size_filenames * sizeof(char *)); + filenames = palloc_array(char *, size_filenames); *num_filenames = 0; while ((de = ReadDir(d, directory)) != NULL) diff --git a/src/backend/utils/misc/gen_guc_tables.pl b/src/backend/utils/misc/gen_guc_tables.pl new file mode 100644 index 0000000000000..ac23e93c395e7 --- /dev/null +++ b/src/backend/utils/misc/gen_guc_tables.pl @@ -0,0 +1,208 @@ +#!/usr/bin/perl +#---------------------------------------------------------------------- +# +# Generate guc_tables.c from guc_parameters.dat. +# +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/backend/utils/misc/gen_guc_tables.pl +# +#---------------------------------------------------------------------- + +use strict; +use warnings FATAL => 'all'; + +use FindBin; +use lib "$FindBin::RealBin/../../catalog"; +use Catalog; + +die "Usage: $0 INPUT_FILE OUTPUT_FILE\n" unless @ARGV == 2; +my ($input_fname, $output_fname) = @ARGV; + +my $parse = Catalog::ParseData($input_fname); + +open my $ofh, '>', $output_fname or die; + +print_boilerplate($ofh, $output_fname, 'GUC tables'); +print_table($ofh); + +close $ofh; + + +# Adds double quotes and escapes as necessary for C strings. +sub dquote +{ + my ($s) = @_; + + return q{"} . $s =~ s/"/\\"/gr . q{"}; +} + +sub validate_guc_entry +{ + my ($entry) = @_; + + my @required_common = + qw(name type context group short_desc variable boot_val); + + my %required_by_type = ( + int => [qw(min max)], + real => [qw(min max)], + enum => [qw(options)], + bool => [], # no extra required fields + string => [], # no extra required fields + ); + + # All fields recognized by the generator. "line_number" is injected + # by Catalog::ParseData and is not a user-facing field. + my %valid_fields = map { $_ => 1 } ( + @required_common, + qw(long_desc flags ifdef min max options + check_hook assign_hook show_hook + line_number)); + + for my $f (sort keys %$entry) + { + unless ($valid_fields{$f}) + { + die sprintf( + qq{%s:%d: error: entry "%s" has unrecognized field "%s"\n}, + $input_fname, $entry->{line_number}, + $entry->{name} // '', $f); + } + } + + for my $f (@required_common) + { + unless (defined $entry->{$f}) + { + die sprintf( + qq{%s:%d: error: entry "%s" is missing required field "%s"\n}, + $input_fname, $entry->{line_number}, + $entry->{name} // '', $f); + } + } + + unless (exists $required_by_type{ $entry->{type} }) + { + die sprintf( + qq{%s:%d: error: entry "%s" has unrecognized GUC type "%s"\n}, + $input_fname, $entry->{line_number}, + $entry->{name}, $entry->{type} // ''); + } + + for my $f (@{ $required_by_type{ $entry->{type} } }) + { + unless (defined $entry->{$f}) + { + die sprintf( + qq{%s:%d: error: entry "%s" of type "%s" is missing required field "%s"\n}, + $input_fname, $entry->{line_number}, $entry->{name}, + $entry->{type}, $f); + } + } +} + +# Print GUC table. +sub print_table +{ + my ($ofh) = @_; + my $prev_name = undef; + + print $ofh "\n\n"; + print $ofh "struct config_generic ConfigureNames[] =\n"; + print $ofh "{\n"; + + foreach my $entry (@{$parse}) + { + validate_guc_entry($entry); + + if (defined($prev_name) && lc($prev_name) eq lc($entry->{name})) + { + die sprintf(qq{%s:%d: error: duplicate entry "%s"\n}, + $input_fname, $entry->{line_number}, $entry->{name}); + } + if (defined($prev_name) && lc($prev_name) gt lc($entry->{name})) + { + die sprintf( + qq{%s:%d: error: entries are not in alphabetical order: "%s", "%s"\n}, + $input_fname, $entry->{line_number}, + $prev_name, $entry->{name}); + } + + print $ofh "#ifdef $entry->{ifdef}\n" if $entry->{ifdef}; + print $ofh "\t{\n"; + printf $ofh "\t\t.name = %s,\n", dquote($entry->{name}); + printf $ofh "\t\t.context = %s,\n", $entry->{context}; + printf $ofh "\t\t.group = %s,\n", $entry->{group}; + printf $ofh + "\t\t/* translator: GUC parameter \"%s\" short description */\n", + $entry->{name}; + printf $ofh "\t\t.short_desc = gettext_noop(%s),\n", + dquote($entry->{short_desc}); + + if ($entry->{long_desc}) + { + printf $ofh + "\t\t/* translator: GUC parameter \"%s\" long description */\n", + $entry->{name}; + printf $ofh "\t\t.long_desc = gettext_noop(%s),\n", + dquote($entry->{long_desc}); + } + printf $ofh "\t\t.flags = %s,\n", $entry->{flags} if $entry->{flags}; + printf $ofh "\t\t.vartype = %s,\n", ('PGC_' . uc($entry->{type})); + printf $ofh "\t\t._%s = {\n", $entry->{type}; + printf $ofh "\t\t\t.variable = &%s,\n", $entry->{variable}; + printf $ofh "\t\t\t.boot_val = %s,\n", $entry->{boot_val}; + printf $ofh "\t\t\t.min = %s,\n", $entry->{min} + if $entry->{type} eq 'int' || $entry->{type} eq 'real'; + printf $ofh "\t\t\t.max = %s,\n", $entry->{max} + if $entry->{type} eq 'int' || $entry->{type} eq 'real'; + printf $ofh "\t\t\t.options = %s,\n", $entry->{options} + if $entry->{type} eq 'enum'; + printf $ofh "\t\t\t.check_hook = %s,\n", $entry->{check_hook} + if $entry->{check_hook}; + printf $ofh "\t\t\t.assign_hook = %s,\n", $entry->{assign_hook} + if $entry->{assign_hook}; + printf $ofh "\t\t\t.show_hook = %s,\n", $entry->{show_hook} + if $entry->{show_hook}; + print $ofh "\t\t},\n"; + print $ofh "\t},\n"; + print $ofh "#endif\n" if $entry->{ifdef}; + print $ofh "\n"; + + $prev_name = $entry->{name}; + } + + print $ofh "\t/* End-of-list marker */\n"; + print $ofh "\t{0}\n"; + print $ofh "};\n"; + + return; +} + +sub print_boilerplate +{ + my ($fh, $fname, $descr) = @_; + printf $fh <name = NULL; item->value = NULL; item->errmsg = pstrdup(errmsg); @@ -482,7 +482,7 @@ ParseConfigFp(FILE *fp, const char *config_file, int depth, int elevel, else { /* ordinary variable, append to list */ - item = palloc(sizeof *item); + item = palloc_object(ConfigVariable); item->name = opt_name; item->value = opt_value; item->errmsg = NULL; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 667df448732f2..c4c3fbc4fe36d 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -14,7 +14,7 @@ * See src/backend/utils/misc/README for more information. * * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * Written by Peter Eisentraut . * * IDENTIFICATION @@ -34,6 +34,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_authid.h" #include "catalog/pg_parameter_acl.h" +#include "catalog/pg_type.h" #include "guc_internal.h" #include "libpq/pqformat.h" #include "libpq/protocol.h" @@ -55,6 +56,7 @@ #define CONFIG_FILENAME "postgresql.conf" #define HBA_FILENAME "pg_hba.conf" #define IDENT_FILENAME "pg_ident.conf" +#define HOSTS_FILENAME "pg_hosts.conf" #ifdef EXEC_BACKEND #define CONFIG_EXEC_PARAMS "global/config_exec_params" @@ -244,11 +246,12 @@ static void ReportGUCOption(struct config_generic *record); static void set_config_sourcefile(const char *name, char *sourcefile, int sourceline); static void reapply_stacked_values(struct config_generic *variable, - struct config_string *pHolder, + struct config_generic *pHolder, GucStack *stack, const char *curvalue, GucContext curscontext, GucSource cursource, Oid cursrole); +static void free_placeholder(struct config_generic *pHolder); static bool validate_option_array_item(const char *name, const char *value, bool skipIfNoPermissions); static void write_auto_conf_file(int fd, const char *filename, ConfigVariable *head); @@ -259,15 +262,15 @@ static bool assignable_custom_variable_name(const char *name, bool skip_errors, int elevel); static void do_serialize(char **destptr, Size *maxbytes, const char *fmt,...) pg_attribute_printf(3, 4); -static bool call_bool_check_hook(struct config_bool *conf, bool *newval, +static bool call_bool_check_hook(const struct config_generic *conf, bool *newval, void **extra, GucSource source, int elevel); -static bool call_int_check_hook(struct config_int *conf, int *newval, +static bool call_int_check_hook(const struct config_generic *conf, int *newval, void **extra, GucSource source, int elevel); -static bool call_real_check_hook(struct config_real *conf, double *newval, +static bool call_real_check_hook(const struct config_generic *conf, double *newval, void **extra, GucSource source, int elevel); -static bool call_string_check_hook(struct config_string *conf, char **newval, +static bool call_string_check_hook(const struct config_generic *conf, char **newval, void **extra, GucSource source, int elevel); -static bool call_enum_check_hook(struct config_enum *conf, int *newval, +static bool call_enum_check_hook(const struct config_generic *conf, int *newval, void **extra, GucSource source, int elevel); @@ -284,8 +287,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) bool error = false; bool applying = false; const char *ConfFileWithError; - ConfigVariable *item, - *head, + ConfigVariable *head, *tail; HASH_SEQ_STATUS status; GUCHashEntry *hentry; @@ -336,7 +338,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) /* * Prune all items except the last "data_directory" from the list. */ - for (item = head; item; item = item->next) + for (ConfigVariable *item = head; item; item = item->next) { if (!item->ignore && strcmp(item->name, "data_directory") == 0) @@ -384,7 +386,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) * variable mentioned in the file; and we detect duplicate entries in the * file and mark the earlier occurrences as ignorable. */ - for (item = head; item; item = item->next) + for (ConfigVariable *item = head; item; item = item->next) { struct config_generic *record; @@ -408,9 +410,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) * avoid the O(N^2) behavior here with some additional state, * but it seems unlikely to be worth the trouble. */ - ConfigVariable *pitem; - - for (pitem = head; pitem != item; pitem = pitem->next) + for (ConfigVariable *pitem = head; pitem != item; pitem = pitem->next) { if (!pitem->ignore && strcmp(pitem->name, item->name) == 0) @@ -454,7 +454,6 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL) { struct config_generic *gconf = hentry->gucvar; - GucStack *stack; if (gconf->reset_source != PGC_S_FILE || (gconf->status & GUC_IS_IN_FILE)) @@ -487,7 +486,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) gconf->reset_source = PGC_S_DEFAULT; if (gconf->source == PGC_S_FILE) set_guc_source(gconf, PGC_S_DEFAULT); - for (stack = gconf->stack; stack; stack = stack->prev) + for (GucStack *stack = gconf->stack; stack; stack = stack->prev) { if (stack->source == PGC_S_FILE) stack->source = PGC_S_DEFAULT; @@ -531,7 +530,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel) /* * Now apply the values from the config file. */ - for (item = head; item; item = item->next) + for (ConfigVariable *item = head; item; item = item->next) { char *pre_value = NULL; int scres; @@ -705,15 +704,13 @@ guc_free(void *ptr) * Detect whether strval is referenced anywhere in a GUC string item */ static bool -string_field_used(struct config_string *conf, char *strval) +string_field_used(struct config_generic *conf, char *strval) { - GucStack *stack; - - if (strval == *(conf->variable) || - strval == conf->reset_val || - strval == conf->boot_val) + if (strval == *(conf->_string.variable) || + strval == conf->_string.reset_val || + strval == conf->_string.boot_val) return true; - for (stack = conf->gen.stack; stack; stack = stack->prev) + for (GucStack *stack = conf->stack; stack; stack = stack->prev) { if (strval == stack->prior.val.stringval || strval == stack->masked.val.stringval) @@ -728,7 +725,7 @@ string_field_used(struct config_string *conf, char *strval) * states). */ static void -set_string_field(struct config_string *conf, char **field, char *newval) +set_string_field(struct config_generic *conf, char **field, char *newval) { char *oldval = *field; @@ -746,34 +743,11 @@ set_string_field(struct config_string *conf, char **field, char *newval) static bool extra_field_used(struct config_generic *gconf, void *extra) { - GucStack *stack; - if (extra == gconf->extra) return true; - switch (gconf->vartype) - { - case PGC_BOOL: - if (extra == ((struct config_bool *) gconf)->reset_extra) - return true; - break; - case PGC_INT: - if (extra == ((struct config_int *) gconf)->reset_extra) - return true; - break; - case PGC_REAL: - if (extra == ((struct config_real *) gconf)->reset_extra) - return true; - break; - case PGC_STRING: - if (extra == ((struct config_string *) gconf)->reset_extra) - return true; - break; - case PGC_ENUM: - if (extra == ((struct config_enum *) gconf)->reset_extra) - return true; - break; - } - for (stack = gconf->stack; stack; stack = stack->prev) + if (extra == gconf->reset_extra) + return true; + for (GucStack *stack = gconf->stack; stack; stack = stack->prev) { if (extra == stack->prior.extra || extra == stack->masked.extra) @@ -814,25 +788,19 @@ set_stack_value(struct config_generic *gconf, config_var_value *val) switch (gconf->vartype) { case PGC_BOOL: - val->val.boolval = - *((struct config_bool *) gconf)->variable; + val->val.boolval = *gconf->_bool.variable; break; case PGC_INT: - val->val.intval = - *((struct config_int *) gconf)->variable; + val->val.intval = *gconf->_int.variable; break; case PGC_REAL: - val->val.realval = - *((struct config_real *) gconf)->variable; + val->val.realval = *gconf->_real.variable; break; case PGC_STRING: - set_string_field((struct config_string *) gconf, - &(val->val.stringval), - *((struct config_string *) gconf)->variable); + set_string_field(gconf, &(val->val.stringval), *gconf->_string.variable); break; case PGC_ENUM: - val->val.enumval = - *((struct config_enum *) gconf)->variable; + val->val.enumval = *gconf->_enum.variable; break; } set_extra_field(gconf, &(val->extra), gconf->extra); @@ -854,7 +822,7 @@ discard_stack_value(struct config_generic *gconf, config_var_value *val) /* no need to do anything */ break; case PGC_STRING: - set_string_field((struct config_string *) gconf, + set_string_field(gconf, &(val->val.stringval), NULL); break; @@ -877,7 +845,7 @@ get_guc_variables(int *num_vars) int i; *num_vars = hash_get_num_entries(guc_hashtab); - result = palloc(sizeof(struct config_generic *) * *num_vars); + result = palloc_array(struct config_generic *, *num_vars); /* Extract pointers from the hash table */ i = 0; @@ -907,7 +875,6 @@ build_guc_variables(void) HASHCTL hash_ctl; GUCHashEntry *hentry; bool found; - int i; /* * Create the memory context that will hold all GUC-related data. @@ -918,48 +885,10 @@ build_guc_variables(void) ALLOCSET_DEFAULT_SIZES); /* - * Count all the built-in variables, and set their vartypes correctly. + * Count all the built-in variables. */ - for (i = 0; ConfigureNamesBool[i].gen.name; i++) - { - struct config_bool *conf = &ConfigureNamesBool[i]; - - /* Rather than requiring vartype to be filled in by hand, do this: */ - conf->gen.vartype = PGC_BOOL; - num_vars++; - } - - for (i = 0; ConfigureNamesInt[i].gen.name; i++) - { - struct config_int *conf = &ConfigureNamesInt[i]; - - conf->gen.vartype = PGC_INT; + for (int i = 0; ConfigureNames[i].name; i++) num_vars++; - } - - for (i = 0; ConfigureNamesReal[i].gen.name; i++) - { - struct config_real *conf = &ConfigureNamesReal[i]; - - conf->gen.vartype = PGC_REAL; - num_vars++; - } - - for (i = 0; ConfigureNamesString[i].gen.name; i++) - { - struct config_string *conf = &ConfigureNamesString[i]; - - conf->gen.vartype = PGC_STRING; - num_vars++; - } - - for (i = 0; ConfigureNamesEnum[i].gen.name; i++) - { - struct config_enum *conf = &ConfigureNamesEnum[i]; - - conf->gen.vartype = PGC_ENUM; - num_vars++; - } /* * Create hash table with 20% slack @@ -976,57 +905,9 @@ build_guc_variables(void) &hash_ctl, HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_CONTEXT); - for (i = 0; ConfigureNamesBool[i].gen.name; i++) + for (int i = 0; ConfigureNames[i].name; i++) { - struct config_generic *gucvar = &ConfigureNamesBool[i].gen; - - hentry = (GUCHashEntry *) hash_search(guc_hashtab, - &gucvar->name, - HASH_ENTER, - &found); - Assert(!found); - hentry->gucvar = gucvar; - } - - for (i = 0; ConfigureNamesInt[i].gen.name; i++) - { - struct config_generic *gucvar = &ConfigureNamesInt[i].gen; - - hentry = (GUCHashEntry *) hash_search(guc_hashtab, - &gucvar->name, - HASH_ENTER, - &found); - Assert(!found); - hentry->gucvar = gucvar; - } - - for (i = 0; ConfigureNamesReal[i].gen.name; i++) - { - struct config_generic *gucvar = &ConfigureNamesReal[i].gen; - - hentry = (GUCHashEntry *) hash_search(guc_hashtab, - &gucvar->name, - HASH_ENTER, - &found); - Assert(!found); - hentry->gucvar = gucvar; - } - - for (i = 0; ConfigureNamesString[i].gen.name; i++) - { - struct config_generic *gucvar = &ConfigureNamesString[i].gen; - - hentry = (GUCHashEntry *) hash_search(guc_hashtab, - &gucvar->name, - HASH_ENTER, - &found); - Assert(!found); - hentry->gucvar = gucvar; - } - - for (i = 0; ConfigureNamesEnum[i].gen.name; i++) - { - struct config_generic *gucvar = &ConfigureNamesEnum[i].gen; + struct config_generic *gucvar = &ConfigureNames[i]; hentry = (GUCHashEntry *) hash_search(guc_hashtab, &gucvar->name, @@ -1176,44 +1057,42 @@ assignable_custom_variable_name(const char *name, bool skip_errors, int elevel) static struct config_generic * add_placeholder_variable(const char *name, int elevel) { - size_t sz = sizeof(struct config_string) + sizeof(char *); - struct config_string *var; - struct config_generic *gen; + size_t sz = sizeof(struct config_generic) + sizeof(char *); + struct config_generic *var; - var = (struct config_string *) guc_malloc(elevel, sz); + var = (struct config_generic *) guc_malloc(elevel, sz); if (var == NULL) return NULL; memset(var, 0, sz); - gen = &var->gen; - gen->name = guc_strdup(elevel, name); - if (gen->name == NULL) + var->name = guc_strdup(elevel, name); + if (var->name == NULL) { guc_free(var); return NULL; } - gen->context = PGC_USERSET; - gen->group = CUSTOM_OPTIONS; - gen->short_desc = "GUC placeholder variable"; - gen->flags = GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE | GUC_CUSTOM_PLACEHOLDER; - gen->vartype = PGC_STRING; + var->context = PGC_USERSET; + var->group = CUSTOM_OPTIONS; + var->short_desc = "GUC placeholder variable"; + var->flags = GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE | GUC_CUSTOM_PLACEHOLDER; + var->vartype = PGC_STRING; /* * The char* is allocated at the end of the struct since we have no * 'static' place to point to. Note that the current value, as well as * the boot and reset values, start out NULL. */ - var->variable = (char **) (var + 1); + var->_string.variable = (char **) (var + 1); - if (!add_guc_variable((struct config_generic *) var, elevel)) + if (!add_guc_variable(var, elevel)) { - guc_free(unconstify(char *, gen->name)); + guc_free(unconstify(char *, var->name)); guc_free(var); return NULL; } - return gen; + return var; } /* @@ -1236,7 +1115,6 @@ find_option(const char *name, bool create_placeholders, bool skip_errors, int elevel) { GUCHashEntry *hentry; - int i; Assert(name); @@ -1253,7 +1131,7 @@ find_option(const char *name, bool create_placeholders, bool skip_errors, * set of supported old names is short enough that a brute-force search is * the best way. */ - for (i = 0; map_old_guc_names[i] != NULL; i += 2) + for (int i = 0; map_old_guc_names[i] != NULL; i += 2) { if (guc_name_compare(name, map_old_guc_names[i]) == 0) return find_option(map_old_guc_names[i + 1], false, @@ -1287,10 +1165,10 @@ find_option(const char *name, bool create_placeholders, bool skip_errors, static int guc_var_compare(const void *a, const void *b) { - const char *namea = **(const char **const *) a; - const char *nameb = **(const char **const *) b; + const struct config_generic *ca = *(const struct config_generic *const *) a; + const struct config_generic *cb = *(const struct config_generic *const *) b; - return guc_name_compare(namea, nameb); + return guc_name_compare(ca->name, cb->name); } /* @@ -1433,69 +1311,69 @@ check_GUC_name_for_parameter_acl(const char *name) */ #ifdef USE_ASSERT_CHECKING static bool -check_GUC_init(struct config_generic *gconf) +check_GUC_init(const struct config_generic *gconf) { /* Checks on values */ switch (gconf->vartype) { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) gconf; + const struct config_bool *conf = &gconf->_bool; if (*conf->variable && !conf->boot_val) { elog(LOG, "GUC (PGC_BOOL) %s, boot_val=%d, C-var=%d", - conf->gen.name, conf->boot_val, *conf->variable); + gconf->name, conf->boot_val, *conf->variable); return false; } break; } case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + const struct config_int *conf = &gconf->_int; if (*conf->variable != 0 && *conf->variable != conf->boot_val) { elog(LOG, "GUC (PGC_INT) %s, boot_val=%d, C-var=%d", - conf->gen.name, conf->boot_val, *conf->variable); + gconf->name, conf->boot_val, *conf->variable); return false; } break; } case PGC_REAL: { - struct config_real *conf = (struct config_real *) gconf; + const struct config_real *conf = &gconf->_real; if (*conf->variable != 0.0 && *conf->variable != conf->boot_val) { elog(LOG, "GUC (PGC_REAL) %s, boot_val=%g, C-var=%g", - conf->gen.name, conf->boot_val, *conf->variable); + gconf->name, conf->boot_val, *conf->variable); return false; } break; } case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + const struct config_string *conf = &gconf->_string; if (*conf->variable != NULL && (conf->boot_val == NULL || strcmp(*conf->variable, conf->boot_val) != 0)) { elog(LOG, "GUC (PGC_STRING) %s, boot_val=%s, C-var=%s", - conf->gen.name, conf->boot_val ? conf->boot_val : "", *conf->variable); + gconf->name, conf->boot_val ? conf->boot_val : "", *conf->variable); return false; } break; } case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + const struct config_enum *conf = &gconf->_enum; if (*conf->variable != conf->boot_val) { elog(LOG, "GUC (PGC_ENUM) %s, boot_val=%d, C-var=%d", - conf->gen.name, conf->boot_val, *conf->variable); + gconf->name, conf->boot_val, *conf->variable); return false; } break; @@ -1627,7 +1505,7 @@ InitializeGUCOptionsFromEnvironment(void) new_limit = 2048; source = PGC_S_DYNAMIC_DEFAULT; } - snprintf(limbuf, sizeof(limbuf), "%d", (int) new_limit); + snprintf(limbuf, sizeof(limbuf), "%zd", new_limit); SetConfigOption("max_stack_depth", limbuf, PGC_POSTMASTER, source); } @@ -1643,6 +1521,8 @@ InitializeGUCOptionsFromEnvironment(void) static void InitializeOneGUCOption(struct config_generic *gconf) { + void *extra = NULL; + gconf->status = 0; gconf->source = PGC_S_DEFAULT; gconf->reset_source = PGC_S_DEFAULT; @@ -1660,61 +1540,54 @@ InitializeOneGUCOption(struct config_generic *gconf) { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) gconf; + struct config_bool *conf = &gconf->_bool; bool newval = conf->boot_val; - void *extra = NULL; - if (!call_bool_check_hook(conf, &newval, &extra, + if (!call_bool_check_hook(gconf, &newval, &extra, PGC_S_DEFAULT, LOG)) elog(FATAL, "failed to initialize %s to %d", - conf->gen.name, (int) newval); + gconf->name, (int) newval); if (conf->assign_hook) conf->assign_hook(newval, extra); *conf->variable = conf->reset_val = newval; - conf->gen.extra = conf->reset_extra = extra; break; } case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + struct config_int *conf = &gconf->_int; int newval = conf->boot_val; - void *extra = NULL; Assert(newval >= conf->min); Assert(newval <= conf->max); - if (!call_int_check_hook(conf, &newval, &extra, + if (!call_int_check_hook(gconf, &newval, &extra, PGC_S_DEFAULT, LOG)) elog(FATAL, "failed to initialize %s to %d", - conf->gen.name, newval); + gconf->name, newval); if (conf->assign_hook) conf->assign_hook(newval, extra); *conf->variable = conf->reset_val = newval; - conf->gen.extra = conf->reset_extra = extra; break; } case PGC_REAL: { - struct config_real *conf = (struct config_real *) gconf; + struct config_real *conf = &gconf->_real; double newval = conf->boot_val; - void *extra = NULL; Assert(newval >= conf->min); Assert(newval <= conf->max); - if (!call_real_check_hook(conf, &newval, &extra, + if (!call_real_check_hook(gconf, &newval, &extra, PGC_S_DEFAULT, LOG)) elog(FATAL, "failed to initialize %s to %g", - conf->gen.name, newval); + gconf->name, newval); if (conf->assign_hook) conf->assign_hook(newval, extra); *conf->variable = conf->reset_val = newval; - conf->gen.extra = conf->reset_extra = extra; break; } case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; char *newval; - void *extra = NULL; /* non-NULL boot_val must always get strdup'd */ if (conf->boot_val != NULL) @@ -1722,33 +1595,32 @@ InitializeOneGUCOption(struct config_generic *gconf) else newval = NULL; - if (!call_string_check_hook(conf, &newval, &extra, + if (!call_string_check_hook(gconf, &newval, &extra, PGC_S_DEFAULT, LOG)) elog(FATAL, "failed to initialize %s to \"%s\"", - conf->gen.name, newval ? newval : ""); + gconf->name, newval ? newval : ""); if (conf->assign_hook) conf->assign_hook(newval, extra); *conf->variable = conf->reset_val = newval; - conf->gen.extra = conf->reset_extra = extra; break; } case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + struct config_enum *conf = &gconf->_enum; int newval = conf->boot_val; - void *extra = NULL; - if (!call_enum_check_hook(conf, &newval, &extra, + if (!call_enum_check_hook(gconf, &newval, &extra, PGC_S_DEFAULT, LOG)) elog(FATAL, "failed to initialize %s to %d", - conf->gen.name, newval); + gconf->name, newval); if (conf->assign_hook) conf->assign_hook(newval, extra); *conf->variable = conf->reset_val = newval; - conf->gen.extra = conf->reset_extra = extra; break; } } + + gconf->extra = gconf->reset_extra = extra; } /* @@ -1787,7 +1659,7 @@ SelectConfigFiles(const char *userDoption, const char *progname) char *fname; bool fname_is_malloced; struct stat stat_buf; - struct config_string *data_directory_rec; + struct config_generic *data_directory_rec; /* configdir is -D option, or $PGDATA if no -D */ if (userDoption) @@ -1802,7 +1674,7 @@ SelectConfigFiles(const char *userDoption, const char *progname) configdir); if (errno == ENOENT) write_stderr("Run initdb or pg_basebackup to initialize a PostgreSQL data directory.\n"); - return false; + goto fail; } /* @@ -1829,7 +1701,7 @@ SelectConfigFiles(const char *userDoption, const char *progname) "You must specify the --config-file or -D invocation " "option or set the PGDATA environment variable.\n", progname); - return false; + goto fail; } /* @@ -1850,8 +1722,7 @@ SelectConfigFiles(const char *userDoption, const char *progname) { write_stderr("%s: could not access the server configuration file \"%s\": %m\n", progname, ConfigFileName); - free(configdir); - return false; + goto fail; } /* @@ -1868,10 +1739,10 @@ SelectConfigFiles(const char *userDoption, const char *progname) * Note: SetDataDir will copy and absolute-ize its argument, so we don't * have to. */ - data_directory_rec = (struct config_string *) + data_directory_rec = find_option("data_directory", false, false, PANIC); - if (*data_directory_rec->variable) - SetDataDir(*data_directory_rec->variable); + if (*data_directory_rec->_string.variable) + SetDataDir(*data_directory_rec->_string.variable); else if (configdir) SetDataDir(configdir); else @@ -1881,7 +1752,7 @@ SelectConfigFiles(const char *userDoption, const char *progname) "or by the -D invocation option, or by the " "PGDATA environment variable.\n", progname, ConfigFileName); - return false; + goto fail; } /* @@ -1933,7 +1804,7 @@ SelectConfigFiles(const char *userDoption, const char *progname) "or by the -D invocation option, or by the " "PGDATA environment variable.\n", progname, ConfigFileName); - return false; + goto fail; } SetConfigOption("hba_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); @@ -1964,10 +1835,41 @@ SelectConfigFiles(const char *userDoption, const char *progname) "or by the -D invocation option, or by the " "PGDATA environment variable.\n", progname, ConfigFileName); - return false; + goto fail; } SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); + if (fname_is_malloced) + free(fname); + else + guc_free(fname); + + /* + * Likewise for pg_hosts.conf. + */ + if (HostsFileName) + { + fname = make_absolute_path(HostsFileName); + fname_is_malloced = true; + } + else if (configdir) + { + fname = guc_malloc(FATAL, + strlen(configdir) + strlen(HOSTS_FILENAME) + 2); + sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME); + fname_is_malloced = false; + } + else + { + write_stderr("%s does not know where to find the \"hosts\" configuration file.\n" + "This can be specified as \"hosts_file\" in \"%s\", " + "or by the -D invocation option, or by the " + "PGDATA environment variable.\n", + progname, ConfigFileName); + goto fail; + } + SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); + if (fname_is_malloced) free(fname); else @@ -1976,6 +1878,11 @@ SelectConfigFiles(const char *userDoption, const char *progname) free(configdir); return true; + +fail: + free(configdir); + + return false; } /* @@ -2028,62 +1935,62 @@ ResetAllOptions(void) { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) gconf; + struct config_bool *conf = &gconf->_bool; if (conf->assign_hook) conf->assign_hook(conf->reset_val, - conf->reset_extra); + gconf->reset_extra); *conf->variable = conf->reset_val; - set_extra_field(&conf->gen, &conf->gen.extra, - conf->reset_extra); + set_extra_field(gconf, &gconf->extra, + gconf->reset_extra); break; } case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + struct config_int *conf = &gconf->_int; if (conf->assign_hook) conf->assign_hook(conf->reset_val, - conf->reset_extra); + gconf->reset_extra); *conf->variable = conf->reset_val; - set_extra_field(&conf->gen, &conf->gen.extra, - conf->reset_extra); + set_extra_field(gconf, &gconf->extra, + gconf->reset_extra); break; } case PGC_REAL: { - struct config_real *conf = (struct config_real *) gconf; + struct config_real *conf = &gconf->_real; if (conf->assign_hook) conf->assign_hook(conf->reset_val, - conf->reset_extra); + gconf->reset_extra); *conf->variable = conf->reset_val; - set_extra_field(&conf->gen, &conf->gen.extra, - conf->reset_extra); + set_extra_field(gconf, &gconf->extra, + gconf->reset_extra); break; } case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; if (conf->assign_hook) conf->assign_hook(conf->reset_val, - conf->reset_extra); - set_string_field(conf, conf->variable, conf->reset_val); - set_extra_field(&conf->gen, &conf->gen.extra, - conf->reset_extra); + gconf->reset_extra); + set_string_field(gconf, conf->variable, conf->reset_val); + set_extra_field(gconf, &gconf->extra, + gconf->reset_extra); break; } case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + struct config_enum *conf = &gconf->_enum; if (conf->assign_hook) conf->assign_hook(conf->reset_val, - conf->reset_extra); + gconf->reset_extra); *conf->variable = conf->reset_val; - set_extra_field(&conf->gen, &conf->gen.extra, - conf->reset_extra); + set_extra_field(gconf, &gconf->extra, + gconf->reset_extra); break; } } @@ -2403,17 +2310,17 @@ AtEOXact_GUC(bool isCommit, int nestLevel) { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) gconf; + struct config_bool *conf = &gconf->_bool; bool newval = newvalue.val.boolval; void *newextra = newvalue.extra; if (*conf->variable != newval || - conf->gen.extra != newextra) + gconf->extra != newextra) { if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(gconf, &gconf->extra, newextra); changed = true; } @@ -2421,17 +2328,17 @@ AtEOXact_GUC(bool isCommit, int nestLevel) } case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + struct config_int *conf = &gconf->_int; int newval = newvalue.val.intval; void *newextra = newvalue.extra; if (*conf->variable != newval || - conf->gen.extra != newextra) + gconf->extra != newextra) { if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(gconf, &gconf->extra, newextra); changed = true; } @@ -2439,17 +2346,17 @@ AtEOXact_GUC(bool isCommit, int nestLevel) } case PGC_REAL: { - struct config_real *conf = (struct config_real *) gconf; + struct config_real *conf = &gconf->_real; double newval = newvalue.val.realval; void *newextra = newvalue.extra; if (*conf->variable != newval || - conf->gen.extra != newextra) + gconf->extra != newextra) { if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(gconf, &gconf->extra, newextra); changed = true; } @@ -2457,17 +2364,17 @@ AtEOXact_GUC(bool isCommit, int nestLevel) } case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; char *newval = newvalue.val.stringval; void *newextra = newvalue.extra; if (*conf->variable != newval || - conf->gen.extra != newextra) + gconf->extra != newextra) { if (conf->assign_hook) conf->assign_hook(newval, newextra); - set_string_field(conf, conf->variable, newval); - set_extra_field(&conf->gen, &conf->gen.extra, + set_string_field(gconf, conf->variable, newval); + set_extra_field(gconf, &gconf->extra, newextra); changed = true; } @@ -2478,23 +2385,23 @@ AtEOXact_GUC(bool isCommit, int nestLevel) * we have type-specific code anyway, might as * well inline it. */ - set_string_field(conf, &stack->prior.val.stringval, NULL); - set_string_field(conf, &stack->masked.val.stringval, NULL); + set_string_field(gconf, &stack->prior.val.stringval, NULL); + set_string_field(gconf, &stack->masked.val.stringval, NULL); break; } case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + struct config_enum *conf = &gconf->_enum; int newval = newvalue.val.enumval; void *newextra = newvalue.extra; if (*conf->variable != newval || - conf->gen.extra != newextra) + gconf->extra != newextra) { if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(gconf, &gconf->extra, newextra); changed = true; } @@ -2674,7 +2581,6 @@ convert_to_base_unit(double value, const char *unit, char unitstr[MAX_UNIT_LEN + 1]; int unitlen; const unit_conversion *table; - int i; /* extract unit string to compare to table entries */ unitlen = 0; @@ -2694,7 +2600,7 @@ convert_to_base_unit(double value, const char *unit, else table = time_unit_conversion_table; - for (i = 0; *table[i].unit; i++) + for (int i = 0; *table[i].unit; i++) { if (base_unit == table[i].base_unit && strcmp(unitstr, table[i].unit) == 0) @@ -2730,7 +2636,6 @@ convert_int_from_base_unit(int64 base_value, int base_unit, int64 *value, const char **unit) { const unit_conversion *table; - int i; *unit = NULL; @@ -2739,7 +2644,7 @@ convert_int_from_base_unit(int64 base_value, int base_unit, else table = time_unit_conversion_table; - for (i = 0; *table[i].unit; i++) + for (int i = 0; *table[i].unit; i++) { if (base_unit == table[i].base_unit) { @@ -2772,7 +2677,6 @@ convert_real_from_base_unit(double base_value, int base_unit, double *value, const char **unit) { const unit_conversion *table; - int i; *unit = NULL; @@ -2781,7 +2685,7 @@ convert_real_from_base_unit(double base_value, int base_unit, else table = time_unit_conversion_table; - for (i = 0; *table[i].unit; i++) + for (int i = 0; *table[i].unit; i++) { if (base_unit == table[i].base_unit) { @@ -3020,18 +2924,16 @@ parse_real(const char *value, double *result, int flags, const char **hintmsg) * allocated for modification. */ const char * -config_enum_lookup_by_value(struct config_enum *record, int val) +config_enum_lookup_by_value(const struct config_generic *record, int val) { - const struct config_enum_entry *entry; - - for (entry = record->options; entry && entry->name; entry++) + for (const struct config_enum_entry *entry = record->_enum.options; entry && entry->name; entry++) { if (entry->val == val) return entry->name; } elog(ERROR, "could not find enum option %d for %s", - val, record->gen.name); + val, record->name); return NULL; /* silence compiler */ } @@ -3043,12 +2945,10 @@ config_enum_lookup_by_value(struct config_enum *record, int val) * true. If it's not found, return false and retval is set to 0. */ bool -config_enum_lookup_by_name(struct config_enum *record, const char *value, +config_enum_lookup_by_name(const struct config_enum *record, const char *value, int *retval) { - const struct config_enum_entry *entry; - - for (entry = record->options; entry && entry->name; entry++) + for (const struct config_enum_entry *entry = record->options; entry && entry->name; entry++) { if (pg_strcasecmp(value, entry->name) == 0) { @@ -3069,10 +2969,9 @@ config_enum_lookup_by_name(struct config_enum *record, const char *value, * If suffix is non-NULL, it is added to the end of the string. */ char * -config_enum_get_options(struct config_enum *record, const char *prefix, +config_enum_get_options(const struct config_enum *record, const char *prefix, const char *suffix, const char *separator) { - const struct config_enum_entry *entry; StringInfoData retstr; int seplen; @@ -3080,7 +2979,7 @@ config_enum_get_options(struct config_enum *record, const char *prefix, appendStringInfoString(&retstr, prefix); seplen = strlen(separator); - for (entry = record->options; entry && entry->name; entry++) + for (const struct config_enum_entry *entry = record->options; entry && entry->name; entry++) { if (!entry->hidden) { @@ -3126,7 +3025,7 @@ config_enum_get_options(struct config_enum *record, const char *prefix, * Returns true if OK, false if not (or throws error, if elevel >= ERROR) */ static bool -parse_and_validate_value(struct config_generic *record, +parse_and_validate_value(const struct config_generic *record, const char *value, GucSource source, int elevel, union config_var_val *newval, void **newextra) @@ -3135,41 +3034,39 @@ parse_and_validate_value(struct config_generic *record, { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) record; - if (!parse_bool(value, &newval->boolval)) { ereport(elevel, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("parameter \"%s\" requires a Boolean value", - conf->gen.name))); + record->name))); return false; } - if (!call_bool_check_hook(conf, &newval->boolval, newextra, + if (!call_bool_check_hook(record, &newval->boolval, newextra, source, elevel)) return false; } break; case PGC_INT: { - struct config_int *conf = (struct config_int *) record; + const struct config_int *conf = &record->_int; const char *hintmsg; if (!parse_int(value, &newval->intval, - conf->gen.flags, &hintmsg)) + record->flags, &hintmsg)) { ereport(elevel, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid value for parameter \"%s\": \"%s\"", - conf->gen.name, value), + record->name, value), hintmsg ? errhint("%s", _(hintmsg)) : 0)); return false; } if (newval->intval < conf->min || newval->intval > conf->max) { - const char *unit = get_config_unit_name(conf->gen.flags); + const char *unit = get_config_unit_name(record->flags); const char *unitspace; if (unit) @@ -3181,36 +3078,36 @@ parse_and_validate_value(struct config_generic *record, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("%d%s%s is outside the valid range for parameter \"%s\" (%d%s%s .. %d%s%s)", newval->intval, unitspace, unit, - conf->gen.name, + record->name, conf->min, unitspace, unit, conf->max, unitspace, unit))); return false; } - if (!call_int_check_hook(conf, &newval->intval, newextra, + if (!call_int_check_hook(record, &newval->intval, newextra, source, elevel)) return false; } break; case PGC_REAL: { - struct config_real *conf = (struct config_real *) record; + const struct config_real *conf = &record->_real; const char *hintmsg; if (!parse_real(value, &newval->realval, - conf->gen.flags, &hintmsg)) + record->flags, &hintmsg)) { ereport(elevel, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid value for parameter \"%s\": \"%s\"", - conf->gen.name, value), + record->name, value), hintmsg ? errhint("%s", _(hintmsg)) : 0)); return false; } if (newval->realval < conf->min || newval->realval > conf->max) { - const char *unit = get_config_unit_name(conf->gen.flags); + const char *unit = get_config_unit_name(record->flags); const char *unitspace; if (unit) @@ -3222,21 +3119,19 @@ parse_and_validate_value(struct config_generic *record, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("%g%s%s is outside the valid range for parameter \"%s\" (%g%s%s .. %g%s%s)", newval->realval, unitspace, unit, - conf->gen.name, + record->name, conf->min, unitspace, unit, conf->max, unitspace, unit))); return false; } - if (!call_real_check_hook(conf, &newval->realval, newextra, + if (!call_real_check_hook(record, &newval->realval, newextra, source, elevel)) return false; } break; case PGC_STRING: { - struct config_string *conf = (struct config_string *) record; - /* * The value passed by the caller could be transient, so we * always strdup it. @@ -3249,12 +3144,12 @@ parse_and_validate_value(struct config_generic *record, * The only built-in "parsing" check we have is to apply * truncation if GUC_IS_NAME. */ - if (conf->gen.flags & GUC_IS_NAME) + if (record->flags & GUC_IS_NAME) truncate_identifier(newval->stringval, strlen(newval->stringval), true); - if (!call_string_check_hook(conf, &newval->stringval, newextra, + if (!call_string_check_hook(record, &newval->stringval, newextra, source, elevel)) { guc_free(newval->stringval); @@ -3265,28 +3160,39 @@ parse_and_validate_value(struct config_generic *record, break; case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) record; + const struct config_enum *conf = &record->_enum; if (!config_enum_lookup_by_name(conf, value, &newval->enumval)) { char *hintmsg; hintmsg = config_enum_get_options(conf, - "Available values: ", - ".", ", "); + _("Available values: "), + + /* + * translator: This is the terminator of a list of entity + * names. + */ + _("."), + + /* + * translator: This is a separator in a list of entity + * names. + */ + _(", ")); ereport(elevel, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid value for parameter \"%s\": \"%s\"", - conf->gen.name, value), - hintmsg ? errhint("%s", _(hintmsg)) : 0)); + record->name, value), + hintmsg ? errhint("%s", hintmsg) : 0)); if (hintmsg) pfree(hintmsg); return false; } - if (!call_enum_check_hook(conf, &newval->enumval, newextra, + if (!call_enum_check_hook(record, &newval->enumval, newextra, source, elevel)) return false; } @@ -3452,9 +3358,15 @@ set_config_with_handle(const char *name, config_handle *handle, * * Also allow normal setting if the GUC is marked GUC_ALLOW_IN_PARALLEL. * - * Other changes might need to affect other workers, so forbid them. + * Other changes might need to affect other workers, so forbid them. Note, + * that parallel autovacuum leader is an exception because cost-based + * delays need to be affected to parallel autovacuum workers. These + * parameters are propagated to its workers during parallel vacuum (see + * vacuumparallel.c for details). All other changes will affect only the + * parallel autovacuum leader. */ - if (IsInParallelMode() && changeVal && action != GUC_ACTION_SAVE && + if (IsInParallelMode() && !AmAutoVacuumWorkerProcess() && changeVal && + action != GUC_ACTION_SAVE && (record->flags & GUC_ALLOW_IN_PARALLEL) == 0) { ereport(elevel, @@ -3541,7 +3453,7 @@ set_config_with_handle(const char *name, config_handle *handle, } } /* fall through to process the same as PGC_BACKEND */ - /* FALLTHROUGH */ + pg_fallthrough; case PGC_BACKEND: if (context == PGC_SIGHUP) { @@ -3704,7 +3616,7 @@ set_config_with_handle(const char *name, config_handle *handle, { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) record; + struct config_bool *conf = &record->_bool; #define newval (newval_union.boolval) @@ -3718,23 +3630,23 @@ set_config_with_handle(const char *name, config_handle *handle, else if (source == PGC_S_DEFAULT) { newval = conf->boot_val; - if (!call_bool_check_hook(conf, &newval, &newextra, + if (!call_bool_check_hook(record, &newval, &newextra, source, elevel)) return 0; } else { newval = conf->reset_val; - newextra = conf->reset_extra; - source = conf->gen.reset_source; - context = conf->gen.reset_scontext; - srole = conf->gen.reset_srole; + newextra = record->reset_extra; + source = record->reset_source; + context = record->reset_scontext; + srole = record->reset_srole; } if (prohibitValueChange) { /* Release newextra, unless it's reset_extra */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); if (*conf->variable != newval) @@ -3743,7 +3655,7 @@ set_config_with_handle(const char *name, config_handle *handle, ereport(elevel, (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), errmsg("parameter \"%s\" cannot be changed without restarting the server", - conf->gen.name))); + record->name))); return 0; } record->status &= ~GUC_PENDING_RESTART; @@ -3754,36 +3666,34 @@ set_config_with_handle(const char *name, config_handle *handle, { /* Save old value to support transaction abort */ if (!makeDefault) - push_old_value(&conf->gen, action); + push_old_value(record, action); if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(record, &record->extra, newextra); - set_guc_source(&conf->gen, source); - conf->gen.scontext = context; - conf->gen.srole = srole; + set_guc_source(record, source); + record->scontext = context; + record->srole = srole; } if (makeDefault) { - GucStack *stack; - - if (conf->gen.reset_source <= source) + if (record->reset_source <= source) { conf->reset_val = newval; - set_extra_field(&conf->gen, &conf->reset_extra, + set_extra_field(record, &record->reset_extra, newextra); - conf->gen.reset_source = source; - conf->gen.reset_scontext = context; - conf->gen.reset_srole = srole; + record->reset_source = source; + record->reset_scontext = context; + record->reset_srole = srole; } - for (stack = conf->gen.stack; stack; stack = stack->prev) + for (GucStack *stack = record->stack; stack; stack = stack->prev) { if (stack->source <= source) { stack->prior.val.boolval = newval; - set_extra_field(&conf->gen, &stack->prior.extra, + set_extra_field(record, &stack->prior.extra, newextra); stack->source = source; stack->scontext = context; @@ -3793,7 +3703,7 @@ set_config_with_handle(const char *name, config_handle *handle, } /* Perhaps we didn't install newextra anywhere */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); break; @@ -3802,7 +3712,7 @@ set_config_with_handle(const char *name, config_handle *handle, case PGC_INT: { - struct config_int *conf = (struct config_int *) record; + struct config_int *conf = &record->_int; #define newval (newval_union.intval) @@ -3816,23 +3726,23 @@ set_config_with_handle(const char *name, config_handle *handle, else if (source == PGC_S_DEFAULT) { newval = conf->boot_val; - if (!call_int_check_hook(conf, &newval, &newextra, + if (!call_int_check_hook(record, &newval, &newextra, source, elevel)) return 0; } else { newval = conf->reset_val; - newextra = conf->reset_extra; - source = conf->gen.reset_source; - context = conf->gen.reset_scontext; - srole = conf->gen.reset_srole; + newextra = record->reset_extra; + source = record->reset_source; + context = record->reset_scontext; + srole = record->reset_srole; } if (prohibitValueChange) { /* Release newextra, unless it's reset_extra */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); if (*conf->variable != newval) @@ -3841,7 +3751,7 @@ set_config_with_handle(const char *name, config_handle *handle, ereport(elevel, (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), errmsg("parameter \"%s\" cannot be changed without restarting the server", - conf->gen.name))); + record->name))); return 0; } record->status &= ~GUC_PENDING_RESTART; @@ -3852,36 +3762,34 @@ set_config_with_handle(const char *name, config_handle *handle, { /* Save old value to support transaction abort */ if (!makeDefault) - push_old_value(&conf->gen, action); + push_old_value(record, action); if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(record, &record->extra, newextra); - set_guc_source(&conf->gen, source); - conf->gen.scontext = context; - conf->gen.srole = srole; + set_guc_source(record, source); + record->scontext = context; + record->srole = srole; } if (makeDefault) { - GucStack *stack; - - if (conf->gen.reset_source <= source) + if (record->reset_source <= source) { conf->reset_val = newval; - set_extra_field(&conf->gen, &conf->reset_extra, + set_extra_field(record, &record->reset_extra, newextra); - conf->gen.reset_source = source; - conf->gen.reset_scontext = context; - conf->gen.reset_srole = srole; + record->reset_source = source; + record->reset_scontext = context; + record->reset_srole = srole; } - for (stack = conf->gen.stack; stack; stack = stack->prev) + for (GucStack *stack = record->stack; stack; stack = stack->prev) { if (stack->source <= source) { stack->prior.val.intval = newval; - set_extra_field(&conf->gen, &stack->prior.extra, + set_extra_field(record, &stack->prior.extra, newextra); stack->source = source; stack->scontext = context; @@ -3891,7 +3799,7 @@ set_config_with_handle(const char *name, config_handle *handle, } /* Perhaps we didn't install newextra anywhere */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); break; @@ -3900,7 +3808,7 @@ set_config_with_handle(const char *name, config_handle *handle, case PGC_REAL: { - struct config_real *conf = (struct config_real *) record; + struct config_real *conf = &record->_real; #define newval (newval_union.realval) @@ -3914,23 +3822,23 @@ set_config_with_handle(const char *name, config_handle *handle, else if (source == PGC_S_DEFAULT) { newval = conf->boot_val; - if (!call_real_check_hook(conf, &newval, &newextra, + if (!call_real_check_hook(record, &newval, &newextra, source, elevel)) return 0; } else { newval = conf->reset_val; - newextra = conf->reset_extra; - source = conf->gen.reset_source; - context = conf->gen.reset_scontext; - srole = conf->gen.reset_srole; + newextra = record->reset_extra; + source = record->reset_source; + context = record->reset_scontext; + srole = record->reset_srole; } if (prohibitValueChange) { /* Release newextra, unless it's reset_extra */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); if (*conf->variable != newval) @@ -3939,7 +3847,7 @@ set_config_with_handle(const char *name, config_handle *handle, ereport(elevel, (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), errmsg("parameter \"%s\" cannot be changed without restarting the server", - conf->gen.name))); + record->name))); return 0; } record->status &= ~GUC_PENDING_RESTART; @@ -3950,36 +3858,34 @@ set_config_with_handle(const char *name, config_handle *handle, { /* Save old value to support transaction abort */ if (!makeDefault) - push_old_value(&conf->gen, action); + push_old_value(record, action); if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(record, &record->extra, newextra); - set_guc_source(&conf->gen, source); - conf->gen.scontext = context; - conf->gen.srole = srole; + set_guc_source(record, source); + record->scontext = context; + record->srole = srole; } if (makeDefault) { - GucStack *stack; - - if (conf->gen.reset_source <= source) + if (record->reset_source <= source) { conf->reset_val = newval; - set_extra_field(&conf->gen, &conf->reset_extra, + set_extra_field(record, &record->reset_extra, newextra); - conf->gen.reset_source = source; - conf->gen.reset_scontext = context; - conf->gen.reset_srole = srole; + record->reset_source = source; + record->reset_scontext = context; + record->reset_srole = srole; } - for (stack = conf->gen.stack; stack; stack = stack->prev) + for (GucStack *stack = record->stack; stack; stack = stack->prev) { if (stack->source <= source) { stack->prior.val.realval = newval; - set_extra_field(&conf->gen, &stack->prior.extra, + set_extra_field(record, &stack->prior.extra, newextra); stack->source = source; stack->scontext = context; @@ -3989,7 +3895,7 @@ set_config_with_handle(const char *name, config_handle *handle, } /* Perhaps we didn't install newextra anywhere */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); break; @@ -3998,7 +3904,7 @@ set_config_with_handle(const char *name, config_handle *handle, case PGC_STRING: { - struct config_string *conf = (struct config_string *) record; + struct config_string *conf = &record->_string; GucContext orig_context = context; GucSource orig_source = source; Oid orig_srole = srole; @@ -4024,7 +3930,7 @@ set_config_with_handle(const char *name, config_handle *handle, else newval = NULL; - if (!call_string_check_hook(conf, &newval, &newextra, + if (!call_string_check_hook(record, &newval, &newextra, source, elevel)) { guc_free(newval); @@ -4038,10 +3944,10 @@ set_config_with_handle(const char *name, config_handle *handle, * guc.c's control */ newval = conf->reset_val; - newextra = conf->reset_extra; - source = conf->gen.reset_source; - context = conf->gen.reset_scontext; - srole = conf->gen.reset_srole; + newextra = record->reset_extra; + source = record->reset_source; + context = record->reset_scontext; + srole = record->reset_srole; } if (prohibitValueChange) @@ -4054,10 +3960,10 @@ set_config_with_handle(const char *name, config_handle *handle, strcmp(*conf->variable, newval) != 0); /* Release newval, unless it's reset_val */ - if (newval && !string_field_used(conf, newval)) + if (newval && !string_field_used(record, newval)) guc_free(newval); /* Release newextra, unless it's reset_extra */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); if (newval_different) @@ -4066,7 +3972,7 @@ set_config_with_handle(const char *name, config_handle *handle, ereport(elevel, (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), errmsg("parameter \"%s\" cannot be changed without restarting the server", - conf->gen.name))); + record->name))); return 0; } record->status &= ~GUC_PENDING_RESTART; @@ -4077,16 +3983,16 @@ set_config_with_handle(const char *name, config_handle *handle, { /* Save old value to support transaction abort */ if (!makeDefault) - push_old_value(&conf->gen, action); + push_old_value(record, action); if (conf->assign_hook) conf->assign_hook(newval, newextra); - set_string_field(conf, conf->variable, newval); - set_extra_field(&conf->gen, &conf->gen.extra, + set_string_field(record, conf->variable, newval); + set_extra_field(record, &record->extra, newextra); - set_guc_source(&conf->gen, source); - conf->gen.scontext = context; - conf->gen.srole = srole; + set_guc_source(record, source); + record->scontext = context; + record->srole = srole; /* * Ugly hack: during SET session_authorization, forcibly @@ -4113,7 +4019,7 @@ set_config_with_handle(const char *name, config_handle *handle, * that. */ if (!is_reload && - strcmp(conf->gen.name, "session_authorization") == 0) + strcmp(record->name, "session_authorization") == 0) (void) set_config_with_handle("role", NULL, value ? "none" : NULL, orig_context, @@ -4129,24 +4035,22 @@ set_config_with_handle(const char *name, config_handle *handle, if (makeDefault) { - GucStack *stack; - - if (conf->gen.reset_source <= source) + if (record->reset_source <= source) { - set_string_field(conf, &conf->reset_val, newval); - set_extra_field(&conf->gen, &conf->reset_extra, + set_string_field(record, &conf->reset_val, newval); + set_extra_field(record, &record->reset_extra, newextra); - conf->gen.reset_source = source; - conf->gen.reset_scontext = context; - conf->gen.reset_srole = srole; + record->reset_source = source; + record->reset_scontext = context; + record->reset_srole = srole; } - for (stack = conf->gen.stack; stack; stack = stack->prev) + for (GucStack *stack = record->stack; stack; stack = stack->prev) { if (stack->source <= source) { - set_string_field(conf, &stack->prior.val.stringval, + set_string_field(record, &stack->prior.val.stringval, newval); - set_extra_field(&conf->gen, &stack->prior.extra, + set_extra_field(record, &stack->prior.extra, newextra); stack->source = source; stack->scontext = context; @@ -4156,10 +4060,10 @@ set_config_with_handle(const char *name, config_handle *handle, } /* Perhaps we didn't install newval anywhere */ - if (newval && !string_field_used(conf, newval)) + if (newval && !string_field_used(record, newval)) guc_free(newval); /* Perhaps we didn't install newextra anywhere */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); break; @@ -4168,7 +4072,7 @@ set_config_with_handle(const char *name, config_handle *handle, case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) record; + struct config_enum *conf = &record->_enum; #define newval (newval_union.enumval) @@ -4182,23 +4086,23 @@ set_config_with_handle(const char *name, config_handle *handle, else if (source == PGC_S_DEFAULT) { newval = conf->boot_val; - if (!call_enum_check_hook(conf, &newval, &newextra, + if (!call_enum_check_hook(record, &newval, &newextra, source, elevel)) return 0; } else { newval = conf->reset_val; - newextra = conf->reset_extra; - source = conf->gen.reset_source; - context = conf->gen.reset_scontext; - srole = conf->gen.reset_srole; + newextra = record->reset_extra; + source = record->reset_source; + context = record->reset_scontext; + srole = record->reset_srole; } if (prohibitValueChange) { /* Release newextra, unless it's reset_extra */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); if (*conf->variable != newval) @@ -4207,7 +4111,7 @@ set_config_with_handle(const char *name, config_handle *handle, ereport(elevel, (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), errmsg("parameter \"%s\" cannot be changed without restarting the server", - conf->gen.name))); + record->name))); return 0; } record->status &= ~GUC_PENDING_RESTART; @@ -4218,36 +4122,34 @@ set_config_with_handle(const char *name, config_handle *handle, { /* Save old value to support transaction abort */ if (!makeDefault) - push_old_value(&conf->gen, action); + push_old_value(record, action); if (conf->assign_hook) conf->assign_hook(newval, newextra); *conf->variable = newval; - set_extra_field(&conf->gen, &conf->gen.extra, + set_extra_field(record, &record->extra, newextra); - set_guc_source(&conf->gen, source); - conf->gen.scontext = context; - conf->gen.srole = srole; + set_guc_source(record, source); + record->scontext = context; + record->srole = srole; } if (makeDefault) { - GucStack *stack; - - if (conf->gen.reset_source <= source) + if (record->reset_source <= source) { conf->reset_val = newval; - set_extra_field(&conf->gen, &conf->reset_extra, + set_extra_field(record, &record->reset_extra, newextra); - conf->gen.reset_source = source; - conf->gen.reset_scontext = context; - conf->gen.reset_srole = srole; + record->reset_source = source; + record->reset_scontext = context; + record->reset_srole = srole; } - for (stack = conf->gen.stack; stack; stack = stack->prev) + for (GucStack *stack = record->stack; stack; stack = stack->prev) { if (stack->source <= source) { stack->prior.val.enumval = newval; - set_extra_field(&conf->gen, &stack->prior.extra, + set_extra_field(record, &stack->prior.extra, newextra); stack->source = source; stack->scontext = context; @@ -4257,7 +4159,7 @@ set_config_with_handle(const char *name, config_handle *handle, } /* Perhaps we didn't install newextra anywhere */ - if (newextra && !extra_field_used(&conf->gen, newextra)) + if (newextra && !extra_field_used(record, newextra)) guc_free(newextra); break; @@ -4371,25 +4273,25 @@ GetConfigOption(const char *name, bool missing_ok, bool restrict_privileged) switch (record->vartype) { case PGC_BOOL: - return *((struct config_bool *) record)->variable ? "on" : "off"; + return *record->_bool.variable ? "on" : "off"; case PGC_INT: snprintf(buffer, sizeof(buffer), "%d", - *((struct config_int *) record)->variable); + *record->_int.variable); return buffer; case PGC_REAL: snprintf(buffer, sizeof(buffer), "%g", - *((struct config_real *) record)->variable); + *record->_real.variable); return buffer; case PGC_STRING: - return *((struct config_string *) record)->variable ? - *((struct config_string *) record)->variable : ""; + return *record->_string.variable ? + *record->_string.variable : ""; case PGC_ENUM: - return config_enum_lookup_by_value((struct config_enum *) record, - *((struct config_enum *) record)->variable); + return config_enum_lookup_by_value(record, + *record->_enum.variable); } return NULL; } @@ -4419,25 +4321,25 @@ GetConfigOptionResetString(const char *name) switch (record->vartype) { case PGC_BOOL: - return ((struct config_bool *) record)->reset_val ? "on" : "off"; + return record->_bool.reset_val ? "on" : "off"; case PGC_INT: snprintf(buffer, sizeof(buffer), "%d", - ((struct config_int *) record)->reset_val); + record->_int.reset_val); return buffer; case PGC_REAL: snprintf(buffer, sizeof(buffer), "%g", - ((struct config_real *) record)->reset_val); + record->_real.reset_val); return buffer; case PGC_STRING: - return ((struct config_string *) record)->reset_val ? - ((struct config_string *) record)->reset_val : ""; + return record->_string.reset_val ? + record->_string.reset_val : ""; case PGC_ENUM: - return config_enum_lookup_by_value((struct config_enum *) record, - ((struct config_enum *) record)->reset_val); + return config_enum_lookup_by_value(record, + record->_enum.reset_val); } return NULL; } @@ -4469,7 +4371,6 @@ static void write_auto_conf_file(int fd, const char *filename, ConfigVariable *head) { StringInfoData buf; - ConfigVariable *item; initStringInfo(&buf); @@ -4489,7 +4390,7 @@ write_auto_conf_file(int fd, const char *filename, ConfigVariable *head) } /* Emit each parameter, properly quoting the value */ - for (item = head; item != NULL; item = item->next) + for (ConfigVariable *item = head; item != NULL; item = item->next) { char *escaped; @@ -4537,7 +4438,7 @@ static void replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **tail_p, const char *name, const char *value) { - ConfigVariable *item, + ConfigVariable *newitem, *next, *prev = NULL; @@ -4546,7 +4447,7 @@ replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **tail_p, * one, but if external tools have modified the config file, there could * be more. */ - for (item = *head_p; item != NULL; item = next) + for (ConfigVariable *item = *head_p; item != NULL; item = next) { next = item->next; if (guc_name_compare(item->name, name) == 0) @@ -4573,21 +4474,21 @@ replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **tail_p, return; /* OK, append a new entry */ - item = palloc(sizeof *item); - item->name = pstrdup(name); - item->value = pstrdup(value); - item->errmsg = NULL; - item->filename = pstrdup(""); /* new item has no location */ - item->sourceline = 0; - item->ignore = false; - item->applied = false; - item->next = NULL; + newitem = palloc_object(ConfigVariable); + newitem->name = pstrdup(name); + newitem->value = pstrdup(value); + newitem->errmsg = NULL; + newitem->filename = pstrdup(""); /* new item has no location */ + newitem->sourceline = 0; + newitem->ignore = false; + newitem->applied = false; + newitem->next = NULL; if (*head_p == NULL) - *head_p = item; + *head_p = newitem; else - (*tail_p)->next = item; - *tail_p = item; + (*tail_p)->next = newitem; + *tail_p = newitem; } @@ -4722,8 +4623,13 @@ AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt) * the config file cannot cause postmaster start to fail, so we * don't have to be too tense about possibly installing a bad * value.) + * + * As an exception, we skip this check if this is a RESET command + * for an unknown custom GUC, else there'd be no way for users to + * remove such settings with reserved prefixes. */ - (void) assignable_custom_variable_name(name, false, ERROR); + if (value || !valid_custom_variable_name(name)) + (void) assignable_custom_variable_name(name, false, ERROR); } /* @@ -4873,8 +4779,7 @@ init_custom_variable(const char *name, const char *long_desc, GucContext context, int flags, - enum config_type type, - size_t sz) + enum config_type type) { struct config_generic *gen; @@ -4910,8 +4815,8 @@ init_custom_variable(const char *name, context = PGC_SUSET; /* As above, an OOM here is FATAL */ - gen = (struct config_generic *) guc_malloc(FATAL, sz); - memset(gen, 0, sz); + gen = (struct config_generic *) guc_malloc(FATAL, sizeof(struct config_generic)); + memset(gen, 0, sizeof(struct config_generic)); gen->name = guc_strdup(FATAL, name); gen->context = context; @@ -4933,7 +4838,7 @@ define_custom_variable(struct config_generic *variable) { const char *name = variable->name; GUCHashEntry *hentry; - struct config_string *pHolder; + struct config_generic *pHolder; /* Check mapping between initial and default value */ Assert(check_GUC_init(variable)); @@ -4965,7 +4870,7 @@ define_custom_variable(struct config_generic *variable) errmsg("attempt to redefine parameter \"%s\"", name))); Assert(hentry->gucvar->vartype == PGC_STRING); - pHolder = (struct config_string *) hentry->gucvar; + pHolder = hentry->gucvar; /* * First, set the variable to its default value. We must do this even @@ -4984,7 +4889,7 @@ define_custom_variable(struct config_generic *variable) /* * Remove the placeholder from any lists it's in, too. */ - RemoveGUCFromLists(&pHolder->gen); + RemoveGUCFromLists(pHolder); /* * Assign the string value(s) stored in the placeholder to the real @@ -4998,36 +4903,28 @@ define_custom_variable(struct config_generic *variable) */ /* First, apply the reset value if any */ - if (pHolder->reset_val) - (void) set_config_option_ext(name, pHolder->reset_val, - pHolder->gen.reset_scontext, - pHolder->gen.reset_source, - pHolder->gen.reset_srole, + if (pHolder->_string.reset_val) + (void) set_config_option_ext(name, pHolder->_string.reset_val, + pHolder->reset_scontext, + pHolder->reset_source, + pHolder->reset_srole, GUC_ACTION_SET, true, WARNING, false); /* That should not have resulted in stacking anything */ Assert(variable->stack == NULL); /* Now, apply current and stacked values, in the order they were stacked */ - reapply_stacked_values(variable, pHolder, pHolder->gen.stack, - *(pHolder->variable), - pHolder->gen.scontext, pHolder->gen.source, - pHolder->gen.srole); + reapply_stacked_values(variable, pHolder, pHolder->stack, + *(pHolder->_string.variable), + pHolder->scontext, pHolder->source, + pHolder->srole); /* Also copy over any saved source-location information */ - if (pHolder->gen.sourcefile) - set_config_sourcefile(name, pHolder->gen.sourcefile, - pHolder->gen.sourceline); - - /* - * Free up as much as we conveniently can of the placeholder structure. - * (This neglects any stack items, so it's possible for some memory to be - * leaked. Since this can only happen once per session per variable, it - * doesn't seem worth spending much code on.) - */ - set_string_field(pHolder, pHolder->variable, NULL); - set_string_field(pHolder, &pHolder->reset_val, NULL); + if (pHolder->sourcefile) + set_config_sourcefile(name, pHolder->sourcefile, + pHolder->sourceline); - guc_free(pHolder); + /* Now we can free the no-longer-referenced placeholder variable */ + free_placeholder(pHolder); } /* @@ -5039,7 +4936,7 @@ define_custom_variable(struct config_generic *variable) */ static void reapply_stacked_values(struct config_generic *variable, - struct config_string *pHolder, + struct config_generic *pHolder, GucStack *stack, const char *curvalue, GucContext curscontext, GucSource cursource, @@ -5109,10 +5006,10 @@ reapply_stacked_values(struct config_generic *variable, * this is to be just a transactional assignment. (We leak the stack * entry.) */ - if (curvalue != pHolder->reset_val || - curscontext != pHolder->gen.reset_scontext || - cursource != pHolder->gen.reset_source || - cursrole != pHolder->gen.reset_srole) + if (curvalue != pHolder->_string.reset_val || + curscontext != pHolder->reset_scontext || + cursource != pHolder->reset_source || + cursrole != pHolder->reset_srole) { (void) set_config_option_ext(name, curvalue, curscontext, cursource, cursrole, @@ -5126,6 +5023,25 @@ reapply_stacked_values(struct config_generic *variable, } } +/* + * Free up a no-longer-referenced placeholder GUC variable. + * + * This neglects any stack items, so it's possible for some memory to be + * leaked. Since this can only happen once per session per variable, it + * doesn't seem worth spending much code on. + */ +static void +free_placeholder(struct config_generic *pHolder) +{ + /* Placeholders are always STRING type, so free their values */ + Assert(pHolder->vartype == PGC_STRING); + set_string_field(pHolder, pHolder->_string.variable, NULL); + set_string_field(pHolder, &pHolder->_string.reset_val, NULL); + + guc_free(unconstify(char *, pHolder->name)); + guc_free(pHolder); +} + /* * Functions for extensions to call to define their custom GUC variables. */ @@ -5141,18 +5057,16 @@ DefineCustomBoolVariable(const char *name, GucBoolAssignHook assign_hook, GucShowHook show_hook) { - struct config_bool *var; - - var = (struct config_bool *) - init_custom_variable(name, short_desc, long_desc, context, flags, - PGC_BOOL, sizeof(struct config_bool)); - var->variable = valueAddr; - var->boot_val = bootValue; - var->reset_val = bootValue; - var->check_hook = check_hook; - var->assign_hook = assign_hook; - var->show_hook = show_hook; - define_custom_variable(&var->gen); + struct config_generic *var; + + var = init_custom_variable(name, short_desc, long_desc, context, flags, PGC_BOOL); + var->_bool.variable = valueAddr; + var->_bool.boot_val = bootValue; + var->_bool.reset_val = bootValue; + var->_bool.check_hook = check_hook; + var->_bool.assign_hook = assign_hook; + var->_bool.show_hook = show_hook; + define_custom_variable(var); } void @@ -5169,20 +5083,18 @@ DefineCustomIntVariable(const char *name, GucIntAssignHook assign_hook, GucShowHook show_hook) { - struct config_int *var; - - var = (struct config_int *) - init_custom_variable(name, short_desc, long_desc, context, flags, - PGC_INT, sizeof(struct config_int)); - var->variable = valueAddr; - var->boot_val = bootValue; - var->reset_val = bootValue; - var->min = minValue; - var->max = maxValue; - var->check_hook = check_hook; - var->assign_hook = assign_hook; - var->show_hook = show_hook; - define_custom_variable(&var->gen); + struct config_generic *var; + + var = init_custom_variable(name, short_desc, long_desc, context, flags, PGC_INT); + var->_int.variable = valueAddr; + var->_int.boot_val = bootValue; + var->_int.reset_val = bootValue; + var->_int.min = minValue; + var->_int.max = maxValue; + var->_int.check_hook = check_hook; + var->_int.assign_hook = assign_hook; + var->_int.show_hook = show_hook; + define_custom_variable(var); } void @@ -5199,20 +5111,18 @@ DefineCustomRealVariable(const char *name, GucRealAssignHook assign_hook, GucShowHook show_hook) { - struct config_real *var; - - var = (struct config_real *) - init_custom_variable(name, short_desc, long_desc, context, flags, - PGC_REAL, sizeof(struct config_real)); - var->variable = valueAddr; - var->boot_val = bootValue; - var->reset_val = bootValue; - var->min = minValue; - var->max = maxValue; - var->check_hook = check_hook; - var->assign_hook = assign_hook; - var->show_hook = show_hook; - define_custom_variable(&var->gen); + struct config_generic *var; + + var = init_custom_variable(name, short_desc, long_desc, context, flags, PGC_REAL); + var->_real.variable = valueAddr; + var->_real.boot_val = bootValue; + var->_real.reset_val = bootValue; + var->_real.min = minValue; + var->_real.max = maxValue; + var->_real.check_hook = check_hook; + var->_real.assign_hook = assign_hook; + var->_real.show_hook = show_hook; + define_custom_variable(var); } void @@ -5227,17 +5137,15 @@ DefineCustomStringVariable(const char *name, GucStringAssignHook assign_hook, GucShowHook show_hook) { - struct config_string *var; - - var = (struct config_string *) - init_custom_variable(name, short_desc, long_desc, context, flags, - PGC_STRING, sizeof(struct config_string)); - var->variable = valueAddr; - var->boot_val = bootValue; - var->check_hook = check_hook; - var->assign_hook = assign_hook; - var->show_hook = show_hook; - define_custom_variable(&var->gen); + struct config_generic *var; + + var = init_custom_variable(name, short_desc, long_desc, context, flags, PGC_STRING); + var->_string.variable = valueAddr; + var->_string.boot_val = bootValue; + var->_string.check_hook = check_hook; + var->_string.assign_hook = assign_hook; + var->_string.show_hook = show_hook; + define_custom_variable(var); } void @@ -5253,19 +5161,17 @@ DefineCustomEnumVariable(const char *name, GucEnumAssignHook assign_hook, GucShowHook show_hook) { - struct config_enum *var; - - var = (struct config_enum *) - init_custom_variable(name, short_desc, long_desc, context, flags, - PGC_ENUM, sizeof(struct config_enum)); - var->variable = valueAddr; - var->boot_val = bootValue; - var->reset_val = bootValue; - var->options = options; - var->check_hook = check_hook; - var->assign_hook = assign_hook; - var->show_hook = show_hook; - define_custom_variable(&var->gen); + struct config_generic *var; + + var = init_custom_variable(name, short_desc, long_desc, context, flags, PGC_ENUM); + var->_enum.variable = valueAddr; + var->_enum.boot_val = bootValue; + var->_enum.reset_val = bootValue; + var->_enum.options = options; + var->_enum.check_hook = check_hook; + var->_enum.assign_hook = assign_hook; + var->_enum.show_hook = show_hook; + define_custom_variable(var); } /* @@ -5286,9 +5192,7 @@ MarkGUCPrefixReserved(const char *className) /* * Check for existing placeholders. We must actually remove invalid - * placeholders, else future parallel worker startups will fail. (We - * don't bother trying to free associated memory, since this shouldn't - * happen often.) + * placeholders, else future parallel worker startups will fail. */ hash_seq_init(&status, guc_hashtab); while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL) @@ -5312,6 +5216,8 @@ MarkGUCPrefixReserved(const char *className) NULL); /* Remove it from any lists it's in, too */ RemoveGUCFromLists(var); + /* And free it */ + free_placeholder(var); } } @@ -5340,7 +5246,7 @@ get_explain_guc_options(int *num) * While only a fraction of all the GUC variables are marked GUC_EXPLAIN, * it doesn't seem worth dynamically resizing this array. */ - result = palloc(sizeof(struct config_generic *) * hash_get_num_entries(guc_hashtab)); + result = palloc_array(struct config_generic *, hash_get_num_entries(guc_hashtab)); /* We need only consider GUCs with source not PGC_S_DEFAULT */ dlist_foreach(iter, &guc_nondef_list) @@ -5364,7 +5270,7 @@ get_explain_guc_options(int *num) { case PGC_BOOL: { - struct config_bool *lconf = (struct config_bool *) conf; + struct config_bool *lconf = &conf->_bool; modified = (lconf->boot_val != *(lconf->variable)); } @@ -5372,7 +5278,7 @@ get_explain_guc_options(int *num) case PGC_INT: { - struct config_int *lconf = (struct config_int *) conf; + struct config_int *lconf = &conf->_int; modified = (lconf->boot_val != *(lconf->variable)); } @@ -5380,7 +5286,7 @@ get_explain_guc_options(int *num) case PGC_REAL: { - struct config_real *lconf = (struct config_real *) conf; + struct config_real *lconf = &conf->_real; modified = (lconf->boot_val != *(lconf->variable)); } @@ -5388,7 +5294,7 @@ get_explain_guc_options(int *num) case PGC_STRING: { - struct config_string *lconf = (struct config_string *) conf; + struct config_string *lconf = &conf->_string; if (lconf->boot_val == NULL && *lconf->variable == NULL) @@ -5403,7 +5309,7 @@ get_explain_guc_options(int *num) case PGC_ENUM: { - struct config_enum *lconf = (struct config_enum *) conf; + struct config_enum *lconf = &conf->_enum; modified = (lconf->boot_val != *(lconf->variable)); } @@ -5463,7 +5369,7 @@ GetConfigOptionByName(const char *name, const char **varname, bool missing_ok) * The result string is palloc'd. */ char * -ShowGUCOption(struct config_generic *record, bool use_units) +ShowGUCOption(const struct config_generic *record, bool use_units) { char buffer[256]; const char *val; @@ -5472,7 +5378,7 @@ ShowGUCOption(struct config_generic *record, bool use_units) { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) record; + const struct config_bool *conf = &record->_bool; if (conf->show_hook) val = conf->show_hook(); @@ -5483,7 +5389,7 @@ ShowGUCOption(struct config_generic *record, bool use_units) case PGC_INT: { - struct config_int *conf = (struct config_int *) record; + const struct config_int *conf = &record->_int; if (conf->show_hook) val = conf->show_hook(); @@ -5512,7 +5418,7 @@ ShowGUCOption(struct config_generic *record, bool use_units) case PGC_REAL: { - struct config_real *conf = (struct config_real *) record; + const struct config_real *conf = &record->_real; if (conf->show_hook) val = conf->show_hook(); @@ -5537,7 +5443,7 @@ ShowGUCOption(struct config_generic *record, bool use_units) case PGC_STRING: { - struct config_string *conf = (struct config_string *) record; + const struct config_string *conf = &record->_string; if (conf->show_hook) val = conf->show_hook(); @@ -5550,12 +5456,12 @@ ShowGUCOption(struct config_generic *record, bool use_units) case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) record; + const struct config_enum *conf = &record->_enum; if (conf->show_hook) val = conf->show_hook(); else - val = config_enum_lookup_by_value(conf, *conf->variable); + val = config_enum_lookup_by_value(record, *conf->variable); } break; @@ -5581,7 +5487,7 @@ ShowGUCOption(struct config_generic *record, bool use_units) * variable sourceline, integer * variable source, integer * variable scontext, integer -* variable srole, OID + * variable srole, OID */ static void write_one_nondefault_variable(FILE *fp, struct config_generic *gconf) @@ -5595,7 +5501,7 @@ write_one_nondefault_variable(FILE *fp, struct config_generic *gconf) { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) gconf; + struct config_bool *conf = &gconf->_bool; if (*conf->variable) fprintf(fp, "true"); @@ -5606,7 +5512,7 @@ write_one_nondefault_variable(FILE *fp, struct config_generic *gconf) case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + struct config_int *conf = &gconf->_int; fprintf(fp, "%d", *conf->variable); } @@ -5614,7 +5520,7 @@ write_one_nondefault_variable(FILE *fp, struct config_generic *gconf) case PGC_REAL: { - struct config_real *conf = (struct config_real *) gconf; + struct config_real *conf = &gconf->_real; fprintf(fp, "%.17g", *conf->variable); } @@ -5622,7 +5528,7 @@ write_one_nondefault_variable(FILE *fp, struct config_generic *gconf) case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; if (*conf->variable) fprintf(fp, "%s", *conf->variable); @@ -5631,10 +5537,10 @@ write_one_nondefault_variable(FILE *fp, struct config_generic *gconf) case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + struct config_enum *conf = &gconf->_enum; fprintf(fp, "%s", - config_enum_lookup_by_value(conf, *conf->variable)); + config_enum_lookup_by_value(gconf, *conf->variable)); } break; } @@ -5869,7 +5775,7 @@ estimate_variable_size(struct config_generic *gconf) case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + struct config_int *conf = &gconf->_int; /* * Instead of getting the exact display length, use max @@ -5898,7 +5804,7 @@ estimate_variable_size(struct config_generic *gconf) case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; /* * If the value is NULL, we transmit it as an empty string. @@ -5914,9 +5820,9 @@ estimate_variable_size(struct config_generic *gconf) case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + struct config_enum *conf = &gconf->_enum; - valsize = strlen(config_enum_lookup_by_value(conf, *conf->variable)); + valsize = strlen(config_enum_lookup_by_value(gconf, *conf->variable)); } break; } @@ -6035,7 +5941,7 @@ serialize_variable(char **destptr, Size *maxbytes, { case PGC_BOOL: { - struct config_bool *conf = (struct config_bool *) gconf; + struct config_bool *conf = &gconf->_bool; do_serialize(destptr, maxbytes, (*conf->variable ? "true" : "false")); @@ -6044,7 +5950,7 @@ serialize_variable(char **destptr, Size *maxbytes, case PGC_INT: { - struct config_int *conf = (struct config_int *) gconf; + struct config_int *conf = &gconf->_int; do_serialize(destptr, maxbytes, "%d", *conf->variable); } @@ -6052,7 +5958,7 @@ serialize_variable(char **destptr, Size *maxbytes, case PGC_REAL: { - struct config_real *conf = (struct config_real *) gconf; + struct config_real *conf = &gconf->_real; do_serialize(destptr, maxbytes, "%.*e", REALTYPE_PRECISION, *conf->variable); @@ -6061,7 +5967,7 @@ serialize_variable(char **destptr, Size *maxbytes, case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; /* NULL becomes empty string, see estimate_variable_size() */ do_serialize(destptr, maxbytes, "%s", @@ -6071,10 +5977,10 @@ serialize_variable(char **destptr, Size *maxbytes, case PGC_ENUM: { - struct config_enum *conf = (struct config_enum *) gconf; + struct config_enum *conf = &gconf->_enum; do_serialize(destptr, maxbytes, "%s", - config_enum_lookup_by_value(conf, *conf->variable)); + config_enum_lookup_by_value(gconf, *conf->variable)); } break; } @@ -6252,49 +6158,23 @@ RestoreGUCState(void *gucstate) switch (gconf->vartype) { case PGC_BOOL: - { - struct config_bool *conf = (struct config_bool *) gconf; - - if (conf->reset_extra && conf->reset_extra != gconf->extra) - guc_free(conf->reset_extra); - break; - } case PGC_INT: - { - struct config_int *conf = (struct config_int *) gconf; - - if (conf->reset_extra && conf->reset_extra != gconf->extra) - guc_free(conf->reset_extra); - break; - } case PGC_REAL: - { - struct config_real *conf = (struct config_real *) gconf; - - if (conf->reset_extra && conf->reset_extra != gconf->extra) - guc_free(conf->reset_extra); - break; - } + case PGC_ENUM: + /* no need to do anything */ + break; case PGC_STRING: { - struct config_string *conf = (struct config_string *) gconf; + struct config_string *conf = &gconf->_string; guc_free(*conf->variable); if (conf->reset_val && conf->reset_val != *conf->variable) guc_free(conf->reset_val); - if (conf->reset_extra && conf->reset_extra != gconf->extra) - guc_free(conf->reset_extra); - break; - } - case PGC_ENUM: - { - struct config_enum *conf = (struct config_enum *) gconf; - - if (conf->reset_extra && conf->reset_extra != gconf->extra) - guc_free(conf->reset_extra); break; } } + if (gconf->reset_extra && gconf->reset_extra != gconf->extra) + guc_free(gconf->reset_extra); /* Remove it from any lists it's in. */ RemoveGUCFromLists(gconf); /* Now we can reset the struct to PGS_S_DEFAULT state. */ @@ -6363,7 +6243,6 @@ void ParseLongOption(const char *string, char **name, char **value) { size_t equal_pos; - char *cp; Assert(string); Assert(name); @@ -6385,7 +6264,7 @@ ParseLongOption(const char *string, char **name, char **value) *value = NULL; } - for (cp = *name; *cp; cp++) + for (char *cp = *name; *cp; cp++) if (*cp == '-') *cp = '_'; } @@ -6399,8 +6278,6 @@ ParseLongOption(const char *string, char **name, char **value) void TransformGUCArray(ArrayType *array, List **names, List **values) { - int i; - Assert(array != NULL); Assert(ARR_ELEMTYPE(array) == TEXTOID); Assert(ARR_NDIM(array) == 1); @@ -6408,7 +6285,7 @@ TransformGUCArray(ArrayType *array, List **names, List **values) *names = NIL; *values = NIL; - for (i = 1; i <= ARR_DIMS(array)[0]; i++) + for (int i = 1; i <= ARR_DIMS(array)[0]; i++) { Datum d; bool isnull; @@ -6512,7 +6389,6 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value) { int index; bool isnull; - int i; Assert(ARR_ELEMTYPE(array) == TEXTOID); Assert(ARR_NDIM(array) == 1); @@ -6520,7 +6396,7 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value) index = ARR_DIMS(array)[0] + 1; /* add after end */ - for (i = 1; i <= ARR_DIMS(array)[0]; i++) + for (int i = 1; i <= ARR_DIMS(array)[0]; i++) { Datum d; char *current; @@ -6568,7 +6444,6 @@ GUCArrayDelete(ArrayType *array, const char *name) { struct config_generic *record; ArrayType *newarray; - int i; int index; Assert(name); @@ -6588,7 +6463,7 @@ GUCArrayDelete(ArrayType *array, const char *name) newarray = NULL; index = 1; - for (i = 1; i <= ARR_DIMS(array)[0]; i++) + for (int i = 1; i <= ARR_DIMS(array)[0]; i++) { Datum d; char *val; @@ -6637,7 +6512,6 @@ ArrayType * GUCArrayReset(ArrayType *array) { ArrayType *newarray; - int i; int index; /* if array is currently null, nothing to do */ @@ -6651,7 +6525,7 @@ GUCArrayReset(ArrayType *array) newarray = NULL; index = 1; - for (i = 1; i <= ARR_DIMS(array)[0]; i++) + for (int i = 1; i <= ARR_DIMS(array)[0]; i++) { Datum d; char *val; @@ -6711,6 +6585,7 @@ validate_option_array_item(const char *name, const char *value, { struct config_generic *gconf; + bool reset_custom; /* * There are three cases to consider: @@ -6729,16 +6604,21 @@ validate_option_array_item(const char *name, const char *value, * it's assumed to be fully validated.) * * name is not known and can't be created as a placeholder. Throw error, - * unless skipIfNoPermissions is true, in which case return false. + * unless skipIfNoPermissions or reset_custom is true. If reset_custom is + * true, this is a RESET or RESET ALL operation for an unknown custom GUC + * with a reserved prefix, in which case we want to fall through to the + * placeholder case described in the preceding paragraph (else there'd be + * no way for users to remove them). Otherwise, return false. */ - gconf = find_option(name, true, skipIfNoPermissions, ERROR); - if (!gconf) + reset_custom = (!value && valid_custom_variable_name(name)); + gconf = find_option(name, true, skipIfNoPermissions || reset_custom, ERROR); + if (!gconf && !reset_custom) { /* not known, failed to make a placeholder */ return false; } - if (gconf->flags & GUC_CUSTOM_PLACEHOLDER) + if (!gconf || gconf->flags & GUC_CUSTOM_PLACEHOLDER) { /* * We cannot do any meaningful check on the value, so only permissions @@ -6796,11 +6676,11 @@ GUC_check_errcode(int sqlerrcode) */ static bool -call_bool_check_hook(struct config_bool *conf, bool *newval, void **extra, +call_bool_check_hook(const struct config_generic *conf, bool *newval, void **extra, GucSource source, int elevel) { /* Quick success if no hook */ - if (!conf->check_hook) + if (!conf->_bool.check_hook) return true; /* Reset variables that might be set by hook */ @@ -6809,19 +6689,19 @@ call_bool_check_hook(struct config_bool *conf, bool *newval, void **extra, GUC_check_errdetail_string = NULL; GUC_check_errhint_string = NULL; - if (!conf->check_hook(newval, extra, source)) + if (!conf->_bool.check_hook(newval, extra, source)) { ereport(elevel, (errcode(GUC_check_errcode_value), GUC_check_errmsg_string ? errmsg_internal("%s", GUC_check_errmsg_string) : errmsg("invalid value for parameter \"%s\": %d", - conf->gen.name, (int) *newval), + conf->name, (int) *newval), GUC_check_errdetail_string ? errdetail_internal("%s", GUC_check_errdetail_string) : 0, GUC_check_errhint_string ? errhint("%s", GUC_check_errhint_string) : 0)); - /* Flush any strings created in ErrorContext */ + /* Flush strings created in ErrorContext (ereport might not have) */ FlushErrorState(); return false; } @@ -6830,11 +6710,11 @@ call_bool_check_hook(struct config_bool *conf, bool *newval, void **extra, } static bool -call_int_check_hook(struct config_int *conf, int *newval, void **extra, +call_int_check_hook(const struct config_generic *conf, int *newval, void **extra, GucSource source, int elevel) { /* Quick success if no hook */ - if (!conf->check_hook) + if (!conf->_int.check_hook) return true; /* Reset variables that might be set by hook */ @@ -6843,19 +6723,19 @@ call_int_check_hook(struct config_int *conf, int *newval, void **extra, GUC_check_errdetail_string = NULL; GUC_check_errhint_string = NULL; - if (!conf->check_hook(newval, extra, source)) + if (!conf->_int.check_hook(newval, extra, source)) { ereport(elevel, (errcode(GUC_check_errcode_value), GUC_check_errmsg_string ? errmsg_internal("%s", GUC_check_errmsg_string) : errmsg("invalid value for parameter \"%s\": %d", - conf->gen.name, *newval), + conf->name, *newval), GUC_check_errdetail_string ? errdetail_internal("%s", GUC_check_errdetail_string) : 0, GUC_check_errhint_string ? errhint("%s", GUC_check_errhint_string) : 0)); - /* Flush any strings created in ErrorContext */ + /* Flush strings created in ErrorContext (ereport might not have) */ FlushErrorState(); return false; } @@ -6864,11 +6744,11 @@ call_int_check_hook(struct config_int *conf, int *newval, void **extra, } static bool -call_real_check_hook(struct config_real *conf, double *newval, void **extra, +call_real_check_hook(const struct config_generic *conf, double *newval, void **extra, GucSource source, int elevel) { /* Quick success if no hook */ - if (!conf->check_hook) + if (!conf->_real.check_hook) return true; /* Reset variables that might be set by hook */ @@ -6877,19 +6757,19 @@ call_real_check_hook(struct config_real *conf, double *newval, void **extra, GUC_check_errdetail_string = NULL; GUC_check_errhint_string = NULL; - if (!conf->check_hook(newval, extra, source)) + if (!conf->_real.check_hook(newval, extra, source)) { ereport(elevel, (errcode(GUC_check_errcode_value), GUC_check_errmsg_string ? errmsg_internal("%s", GUC_check_errmsg_string) : errmsg("invalid value for parameter \"%s\": %g", - conf->gen.name, *newval), + conf->name, *newval), GUC_check_errdetail_string ? errdetail_internal("%s", GUC_check_errdetail_string) : 0, GUC_check_errhint_string ? errhint("%s", GUC_check_errhint_string) : 0)); - /* Flush any strings created in ErrorContext */ + /* Flush strings created in ErrorContext (ereport might not have) */ FlushErrorState(); return false; } @@ -6898,13 +6778,13 @@ call_real_check_hook(struct config_real *conf, double *newval, void **extra, } static bool -call_string_check_hook(struct config_string *conf, char **newval, void **extra, +call_string_check_hook(const struct config_generic *conf, char **newval, void **extra, GucSource source, int elevel) { volatile bool result = true; /* Quick success if no hook */ - if (!conf->check_hook) + if (!conf->_string.check_hook) return true; /* @@ -6920,19 +6800,19 @@ call_string_check_hook(struct config_string *conf, char **newval, void **extra, GUC_check_errdetail_string = NULL; GUC_check_errhint_string = NULL; - if (!conf->check_hook(newval, extra, source)) + if (!conf->_string.check_hook(newval, extra, source)) { ereport(elevel, (errcode(GUC_check_errcode_value), GUC_check_errmsg_string ? errmsg_internal("%s", GUC_check_errmsg_string) : errmsg("invalid value for parameter \"%s\": \"%s\"", - conf->gen.name, *newval ? *newval : ""), + conf->name, *newval ? *newval : ""), GUC_check_errdetail_string ? errdetail_internal("%s", GUC_check_errdetail_string) : 0, GUC_check_errhint_string ? errhint("%s", GUC_check_errhint_string) : 0)); - /* Flush any strings created in ErrorContext */ + /* Flush strings created in ErrorContext (ereport might not have) */ FlushErrorState(); result = false; } @@ -6948,11 +6828,11 @@ call_string_check_hook(struct config_string *conf, char **newval, void **extra, } static bool -call_enum_check_hook(struct config_enum *conf, int *newval, void **extra, +call_enum_check_hook(const struct config_generic *conf, int *newval, void **extra, GucSource source, int elevel) { /* Quick success if no hook */ - if (!conf->check_hook) + if (!conf->_enum.check_hook) return true; /* Reset variables that might be set by hook */ @@ -6961,20 +6841,20 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra, GUC_check_errdetail_string = NULL; GUC_check_errhint_string = NULL; - if (!conf->check_hook(newval, extra, source)) + if (!conf->_enum.check_hook(newval, extra, source)) { ereport(elevel, (errcode(GUC_check_errcode_value), GUC_check_errmsg_string ? errmsg_internal("%s", GUC_check_errmsg_string) : errmsg("invalid value for parameter \"%s\": \"%s\"", - conf->gen.name, + conf->name, config_enum_lookup_by_value(conf, *newval)), GUC_check_errdetail_string ? errdetail_internal("%s", GUC_check_errdetail_string) : 0, GUC_check_errhint_string ? errhint("%s", GUC_check_errhint_string) : 0)); - /* Flush any strings created in ErrorContext */ + /* Flush strings created in ErrorContext (ereport might not have) */ FlushErrorState(); return false; } diff --git a/src/backend/utils/misc/guc_funcs.c b/src/backend/utils/misc/guc_funcs.c index b9e26982abd90..e2c2919484e63 100644 --- a/src/backend/utils/misc/guc_funcs.c +++ b/src/backend/utils/misc/guc_funcs.c @@ -5,7 +5,7 @@ * SQL commands and SQL-accessible functions related to GUC variables. * * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * Written by Peter Eisentraut . * * IDENTIFICATION @@ -22,6 +22,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_authid.h" #include "catalog/pg_parameter_acl.h" +#include "catalog/pg_type_d.h" #include "funcapi.h" #include "guc_internal.h" #include "miscadmin.h" @@ -30,6 +31,7 @@ #include "utils/builtins.h" #include "utils/guc_tables.h" #include "utils/snapmgr.h" +#include "utils/tuplestore.h" static char *flatten_set_variable_args(const char *name, List *args); static void ShowGUCConfigOption(const char *name, DestReceiver *dest); @@ -139,7 +141,7 @@ ExecSetVariableStmt(VariableSetStmt *stmt, bool isTopLevel) case VAR_SET_DEFAULT: if (stmt->is_local) WarnNoTransactionBlock(isTopLevel, "SET LOCAL"); - /* fall through */ + pg_fallthrough; case VAR_RESET: (void) set_config_option(stmt->name, NULL, @@ -210,12 +212,29 @@ flatten_set_variable_args(const char *name, List *args) else flags = 0; - /* Complain if list input and non-list variable */ - if ((flags & GUC_LIST_INPUT) == 0 && - list_length(args) != 1) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("SET %s takes only one argument", name))); + /* + * Handle special cases for list input. + */ + if (flags & GUC_LIST_INPUT) + { + /* NULL represents an empty list. */ + if (list_length(args) == 1) + { + Node *arg = (Node *) linitial(args); + + if (IsA(arg, A_Const) && + ((A_Const *) arg)->isnull) + return pstrdup(""); + } + } + else + { + /* Complain if list input and non-list variable. */ + if (list_length(args) != 1) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("SET %s takes only one argument", name))); + } initStringInfo(&buf); @@ -246,6 +265,12 @@ flatten_set_variable_args(const char *name, List *args) elog(ERROR, "unrecognized node type: %d", (int) nodeTag(arg)); con = (A_Const *) arg; + /* Complain if NULL is used with a non-list variable. */ + if (con->isnull) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("NULL is an invalid value for %s", name))); + switch (nodeTag(&con->val)) { case T_Integer: @@ -269,6 +294,9 @@ flatten_set_variable_args(const char *name, List *args) Datum interval; char *intervalout; + /* gram.y ensures this is only reachable for TIME ZONE */ + Assert(!(flags & GUC_LIST_QUOTE)); + typenameTypeIdAndMod(NULL, typeName, &typoid, &typmod); Assert(typoid == INTERVALOID); @@ -418,6 +446,7 @@ GetPGVariableResultDesc(const char *name) TupleDescInitEntry(tupdesc, (AttrNumber) 1, varname, TEXTOID, -1, 0); } + TupleDescFinalize(tupdesc); return tupdesc; } @@ -439,6 +468,7 @@ ShowGUCConfigOption(const char *name, DestReceiver *dest) tupdesc = CreateTemplateTupleDesc(1); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 1, varname, TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); /* prepare for projection of tuples */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); @@ -473,6 +503,7 @@ ShowAllGUCConfig(DestReceiver *dest) TEXTOID, -1, 0); TupleDescInitBuiltinEntry(tupdesc, (AttrNumber) 3, "description", TEXTOID, -1, 0); + TupleDescFinalize(tupdesc); /* prepare for projection of tuples */ tstate = begin_tup_output_tupdesc(dest, tupdesc, &TTSOpsVirtual); @@ -578,7 +609,7 @@ pg_settings_get_flags(PG_FUNCTION_ARGS) * Return whether or not the GUC variable is visible to the current user. */ bool -ConfigOptionIsVisible(struct config_generic *conf) +ConfigOptionIsVisible(const struct config_generic *conf) { if ((conf->flags & GUC_SUPERUSER_ONLY) && !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_SETTINGS)) @@ -591,7 +622,7 @@ ConfigOptionIsVisible(struct config_generic *conf) * Extract fields to show in pg_settings for given variable. */ static void -GetConfigOptionValues(struct config_generic *conf, const char **values) +GetConfigOptionValues(const struct config_generic *conf, const char **values) { char buffer[256]; @@ -629,7 +660,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values) { case PGC_BOOL: { - struct config_bool *lconf = (struct config_bool *) conf; + const struct config_bool *lconf = &conf->_bool; /* min_val */ values[9] = NULL; @@ -650,7 +681,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values) case PGC_INT: { - struct config_int *lconf = (struct config_int *) conf; + const struct config_int *lconf = &conf->_int; /* min_val */ snprintf(buffer, sizeof(buffer), "%d", lconf->min); @@ -675,7 +706,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values) case PGC_REAL: { - struct config_real *lconf = (struct config_real *) conf; + const struct config_real *lconf = &conf->_real; /* min_val */ snprintf(buffer, sizeof(buffer), "%g", lconf->min); @@ -700,7 +731,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values) case PGC_STRING: { - struct config_string *lconf = (struct config_string *) conf; + const struct config_string *lconf = &conf->_string; /* min_val */ values[9] = NULL; @@ -727,7 +758,7 @@ GetConfigOptionValues(struct config_generic *conf, const char **values) case PGC_ENUM: { - struct config_enum *lconf = (struct config_enum *) conf; + const struct config_enum *lconf = &conf->_enum; /* min_val */ values[9] = NULL; @@ -741,15 +772,15 @@ GetConfigOptionValues(struct config_generic *conf, const char **values) * NOTE! enumvals with double quotes in them are not * supported! */ - values[11] = config_enum_get_options((struct config_enum *) conf, + values[11] = config_enum_get_options(lconf, "{\"", "\"}", "\",\""); /* boot_val */ - values[12] = pstrdup(config_enum_lookup_by_value(lconf, + values[12] = pstrdup(config_enum_lookup_by_value(conf, lconf->boot_val)); /* reset_val */ - values[13] = pstrdup(config_enum_lookup_by_value(lconf, + values[13] = pstrdup(config_enum_lookup_by_value(conf, lconf->reset_val)); } break; @@ -908,6 +939,8 @@ show_all_settings(PG_FUNCTION_ARGS) TupleDescInitEntry(tupdesc, (AttrNumber) 17, "pending_restart", BOOLOID, -1, 0); + TupleDescFinalize(tupdesc); + /* * Generate attribute metadata needed later to produce tuples from raw * C strings @@ -986,7 +1019,6 @@ show_all_file_settings(PG_FUNCTION_ARGS) #define NUM_PG_FILE_SETTINGS_ATTS 7 ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; ConfigVariable *conf; - int seqno; /* Scan the config files using current context as workspace */ conf = ProcessConfigFileInternal(PGC_SIGHUP, false, DEBUG3); @@ -995,7 +1027,7 @@ show_all_file_settings(PG_FUNCTION_ARGS) InitMaterializedSRF(fcinfo, 0); /* Process the results and create a tuplestore */ - for (seqno = 1; conf != NULL; conf = conf->next, seqno++) + for (int seqno = 1; conf != NULL; conf = conf->next, seqno++) { Datum values[NUM_PG_FILE_SETTINGS_ATTS]; bool nulls[NUM_PG_FILE_SETTINGS_ATTS]; diff --git a/src/backend/utils/misc/guc_internal.h b/src/backend/utils/misc/guc_internal.h index 7e4c969989cc0..8efb070051fdf 100644 --- a/src/backend/utils/misc/guc_internal.h +++ b/src/backend/utils/misc/guc_internal.h @@ -4,7 +4,7 @@ * Declarations shared between backend/utils/misc/guc.c and * backend/utils/misc/guc-file.l * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/backend/utils/misc/guc_internal.h *-------------------------------------------------------------------- diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat new file mode 100644 index 0000000000000..83af594d4af4b --- /dev/null +++ b/src/backend/utils/misc/guc_parameters.dat @@ -0,0 +1,3656 @@ +#---------------------------------------------------------------------- +# +# Contents of GUC tables. +# +# See src/backend/utils/misc/README for design notes. +# +# Portions Copyright (c) 2000-2026, PostgreSQL Global Development Group +# +# src/backend/utils/misc/guc_parameters.dat +# +#---------------------------------------------------------------------- + +[ + +# TO ADD AN OPTION: +# +# 1. Declare a global variable of type bool, int, double, or char* and +# make use of it. +# +# 2. Decide at what times it's safe to set the option. See guc.h for +# details. +# +# 3. Decide on a name, a default value, upper and lower bounds (if +# applicable), etc. +# +# 4. Add a record below (in alphabetical order). +# +# 5. Add it to src/backend/utils/misc/postgresql.conf.sample, if +# appropriate. +# +# 6. Don't forget to document the option (at least in config.sgml). +# +# 7. If it's a new GUC_LIST_QUOTE option, you must add it to +# variable_is_guc_list_quote() in src/bin/pg_dump/dumputils.c. + +# This setting itself cannot be set by ALTER SYSTEM to avoid an +# operator turning this setting off by using ALTER SYSTEM, without a +# way to turn it back on. +{ name => 'allow_alter_system', type => 'bool', context => 'PGC_SIGHUP', group => 'COMPAT_OPTIONS_OTHER', + short_desc => 'Allows running the ALTER SYSTEM command.', + long_desc => 'Can be set to off for environments where global configuration changes should be made using a different method.', + flags => 'GUC_DISALLOW_IN_AUTO_FILE', + variable => 'AllowAlterSystem', + boot_val => 'true', +}, + +{ name => 'allow_in_place_tablespaces', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Allows tablespaces directly inside pg_tblspc, for testing.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'allow_in_place_tablespaces', + boot_val => 'false', +}, + +{ name => 'allow_system_table_mods', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Allows modifications of the structure of system tables.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'allowSystemTableMods', + boot_val => 'false', +}, + +{ name => 'application_name', type => 'string', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Sets the application name to be reported in statistics and logs.', + flags => 'GUC_IS_NAME | GUC_REPORT | GUC_NOT_IN_SAMPLE', + variable => 'application_name', + boot_val => '""', + check_hook => 'check_application_name', + assign_hook => 'assign_application_name', +}, + +{ name => 'archive_cleanup_command', type => 'string', context => 'PGC_SIGHUP', group => 'WAL_ARCHIVE_RECOVERY', + short_desc => 'Sets the shell command that will be executed at every restart point.', + variable => 'archiveCleanupCommand', + boot_val => '""', +}, + + +{ name => 'archive_command', type => 'string', context => 'PGC_SIGHUP', group => 'WAL_ARCHIVING', + short_desc => 'Sets the shell command that will be called to archive a WAL file.', + long_desc => 'An empty string means use "archive_library".', + variable => 'XLogArchiveCommand', + boot_val => '""', + show_hook => 'show_archive_command', +}, + +{ name => 'archive_library', type => 'string', context => 'PGC_SIGHUP', group => 'WAL_ARCHIVING', + short_desc => 'Sets the library that will be called to archive a WAL file.', + long_desc => 'An empty string means use "archive_command".', + variable => 'XLogArchiveLibrary', + boot_val => '""', +}, + +{ name => 'archive_mode', type => 'enum', context => 'PGC_POSTMASTER', group => 'WAL_ARCHIVING', + short_desc => 'Allows archiving of WAL files using "archive_command".', + variable => 'XLogArchiveMode', + boot_val => 'ARCHIVE_MODE_OFF', + options => 'archive_mode_options', +}, + +{ name => 'archive_timeout', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_ARCHIVING', + short_desc => 'Sets the amount of time to wait before forcing a switch to the next WAL file.', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_S', + variable => 'XLogArchiveTimeout', + boot_val => '0', + min => '0', + max => 'INT_MAX / 2', +}, + +{ name => 'array_nulls', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'Enables input of NULL elements in arrays.', + long_desc => 'When turned on, unquoted NULL in an array input value means a null value; otherwise it is taken literally.', + variable => 'Array_nulls', + boot_val => 'true', +}, + +{ name => 'authentication_timeout', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', + short_desc => 'Sets the maximum allowed time to complete client authentication.', + flags => 'GUC_UNIT_S', + variable => 'AuthenticationTimeout', + boot_val => '60', + min => '1', + max => '600', +}, + +{ name => 'autovacuum', type => 'bool', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Starts the autovacuum subprocess.', + variable => 'autovacuum_start_daemon', + boot_val => 'true', +}, + +{ name => 'autovacuum_analyze_scale_factor', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Number of tuple inserts, updates, or deletes prior to analyze as a fraction of reltuples.', + variable => 'autovacuum_anl_scale', + boot_val => '0.1', + min => '0.0', + max => '100.0', +}, + +{ name => 'autovacuum_analyze_score_weight', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Scaling factor of analyze score for autovacuum prioritization.', + variable => 'autovacuum_analyze_score_weight', + boot_val => '1.0', + min => '0.0', + max => '10.0', +}, + +{ name => 'autovacuum_analyze_threshold', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Minimum number of tuple inserts, updates, or deletes prior to analyze.', + variable => 'autovacuum_anl_thresh', + boot_val => '50', + min => '0', + max => 'INT_MAX', +}, + +# see varsup.c for why this is PGC_POSTMASTER not PGC_SIGHUP +# see vacuum_failsafe_age if you change the upper-limit value. +{ name => 'autovacuum_freeze_max_age', type => 'int', context => 'PGC_POSTMASTER', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Age at which to autovacuum a table to prevent transaction ID wraparound.', + variable => 'autovacuum_freeze_max_age', + boot_val => '200000000', + min => '100000', + max => '2000000000', +}, + +{ name => 'autovacuum_freeze_score_weight', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Scaling factor of freeze score for autovacuum prioritization.', + variable => 'autovacuum_freeze_score_weight', + boot_val => '1.0', + min => '0.0', + max => '10.0', +}, + +{ name => 'autovacuum_max_parallel_workers', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Maximum number of parallel workers that can be used by a single autovacuum worker.', + variable => 'autovacuum_max_parallel_workers', + boot_val => '0', + min => '0', + max => 'MAX_PARALLEL_WORKER_LIMIT', +}, + +{ name => 'autovacuum_max_workers', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Sets the maximum number of simultaneously running autovacuum worker processes.', + variable => 'autovacuum_max_workers', + boot_val => '3', + min => '1', + max => 'MAX_BACKENDS', +}, + +# see multixact.c for why this is PGC_POSTMASTER not PGC_SIGHUP +{ name => 'autovacuum_multixact_freeze_max_age', type => 'int', context => 'PGC_POSTMASTER', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Multixact age at which to autovacuum a table to prevent multixact wraparound.', + variable => 'autovacuum_multixact_freeze_max_age', + boot_val => '400000000', + min => '10000', + max => '2000000000', +}, + +{ name => 'autovacuum_multixact_freeze_score_weight', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Scaling factor of multixact freeze score for autovacuum prioritization.', + variable => 'autovacuum_multixact_freeze_score_weight', + boot_val => '1.0', + min => '0.0', + max => '10.0', +}, + +{ name => 'autovacuum_naptime', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Time to sleep between autovacuum runs.', + flags => 'GUC_UNIT_S', + variable => 'autovacuum_naptime', + boot_val => '60', + min => '1', + max => 'INT_MAX / 1000', +}, + +{ name => 'autovacuum_vacuum_cost_delay', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Vacuum cost delay in milliseconds, for autovacuum.', + long_desc => '-1 means use "vacuum_cost_delay".', + flags => 'GUC_UNIT_MS', + variable => 'autovacuum_vac_cost_delay', + boot_val => '2', + min => '-1', + max => '100', +}, + +{ name => 'autovacuum_vacuum_cost_limit', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Vacuum cost amount available before napping, for autovacuum.', + long_desc => '-1 means use "vacuum_cost_limit".', + variable => 'autovacuum_vac_cost_limit', + boot_val => '-1', + min => '-1', + max => '10000', +}, + +{ name => 'autovacuum_vacuum_insert_scale_factor', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Number of tuple inserts prior to vacuum as a fraction of reltuples.', + variable => 'autovacuum_vac_ins_scale', + boot_val => '0.2', + min => '0.0', + max => '100.0', +}, + +{ name => 'autovacuum_vacuum_insert_score_weight', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Scaling factor of vacuum insert score for autovacuum prioritization.', + variable => 'autovacuum_vacuum_insert_score_weight', + boot_val => '1.0', + min => '0.0', + max => '10.0', +}, + +{ name => 'autovacuum_vacuum_insert_threshold', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Minimum number of tuple inserts prior to vacuum.', + long_desc => '-1 disables insert vacuums.', + variable => 'autovacuum_vac_ins_thresh', + boot_val => '1000', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'autovacuum_vacuum_max_threshold', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Maximum number of tuple updates or deletes prior to vacuum.', + long_desc => '-1 disables the maximum threshold.', + variable => 'autovacuum_vac_max_thresh', + boot_val => '100000000', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'autovacuum_vacuum_scale_factor', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Number of tuple updates or deletes prior to vacuum as a fraction of reltuples.', + variable => 'autovacuum_vac_scale', + boot_val => '0.2', + min => '0.0', + max => '100.0', +}, + +{ name => 'autovacuum_vacuum_score_weight', type => 'real', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Scaling factor of vacuum score for autovacuum prioritization.', + variable => 'autovacuum_vacuum_score_weight', + boot_val => '1.0', + min => '0.0', + max => '10.0', +}, + +{ name => 'autovacuum_vacuum_threshold', type => 'int', context => 'PGC_SIGHUP', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Minimum number of tuple updates or deletes prior to vacuum.', + variable => 'autovacuum_vac_thresh', + boot_val => '50', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'autovacuum_work_mem', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum memory to be used by each autovacuum worker process.', + long_desc => '-1 means use "maintenance_work_mem".', + flags => 'GUC_UNIT_KB', + variable => 'autovacuum_work_mem', + boot_val => '-1', + min => '-1', + max => 'MAX_KILOBYTES', + check_hook => 'check_autovacuum_work_mem', +}, + +# see max_connections +{ name => 'autovacuum_worker_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'VACUUM_AUTOVACUUM', + short_desc => 'Sets the number of backend slots to allocate for autovacuum workers.', + variable => 'autovacuum_worker_slots', + boot_val => '16', + min => '1', + max => 'MAX_BACKENDS', +}, + +{ name => 'backend_flush_after', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_IO', + short_desc => 'Number of pages after which previously performed writes are flushed to disk.', + long_desc => '0 disables forced writeback.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'backend_flush_after', + boot_val => 'DEFAULT_BACKEND_FLUSH_AFTER', + min => '0', + max => 'WRITEBACK_MAX_PENDING_FLUSHES', +}, + +{ name => 'backslash_quote', type => 'enum', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'Sets whether "\\\\\'" is allowed in string literals.', + variable => 'backslash_quote', + boot_val => 'BACKSLASH_QUOTE_SAFE_ENCODING', + options => 'backslash_quote_options', +}, + +{ name => 'backtrace_functions', type => 'string', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Log backtrace for errors in these functions.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'backtrace_functions', + boot_val => '""', + check_hook => 'check_backtrace_functions', + assign_hook => 'assign_backtrace_functions', +}, + +{ name => 'bgwriter_delay', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_BGWRITER', + short_desc => 'Background writer sleep time between rounds.', + flags => 'GUC_UNIT_MS', + variable => 'BgWriterDelay', + boot_val => '200', + min => '10', + max => '10000', +}, + +{ name => 'bgwriter_flush_after', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_BGWRITER', + short_desc => 'Number of pages after which previously performed writes are flushed to disk.', + long_desc => '0 disables forced writeback.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'bgwriter_flush_after', + boot_val => 'DEFAULT_BGWRITER_FLUSH_AFTER', + min => '0', + max => 'WRITEBACK_MAX_PENDING_FLUSHES', +}, + +# Same upper limit as shared_buffers +{ name => 'bgwriter_lru_maxpages', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_BGWRITER', + short_desc => 'Background writer maximum number of LRU pages to flush per round.', + long_desc => '0 disables background writing.', + variable => 'bgwriter_lru_maxpages', + boot_val => '100', + min => '0', + max => 'INT_MAX / 2', +}, + +{ name => 'bgwriter_lru_multiplier', type => 'real', context => 'PGC_SIGHUP', group => 'RESOURCES_BGWRITER', + short_desc => 'Multiple of the average buffer usage to free per round.', + variable => 'bgwriter_lru_multiplier', + boot_val => '2.0', + min => '0.0', + max => '10.0', +}, + +{ name => 'block_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the size of a disk block.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'block_size', + boot_val => 'BLCKSZ', + min => 'BLCKSZ', + max => 'BLCKSZ', +}, + +{ name => 'bonjour', type => 'bool', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Enables advertising the server via Bonjour.', + variable => 'enable_bonjour', + boot_val => 'false', + check_hook => 'check_bonjour', +}, + +{ name => 'bonjour_name', type => 'string', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the Bonjour service name.', + long_desc => 'An empty string means use the computer name.', + variable => 'bonjour_name', + boot_val => '""', +}, + +{ name => 'bytea_output', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the output format for bytea.', + variable => 'bytea_output', + boot_val => 'BYTEA_OUTPUT_HEX', + options => 'bytea_output_options', +}, + +{ name => 'check_function_bodies', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Check routine bodies during CREATE FUNCTION and CREATE PROCEDURE.', + variable => 'check_function_bodies', + boot_val => 'true', +}, + +{ name => 'checkpoint_completion_target', type => 'real', context => 'PGC_SIGHUP', group => 'WAL_CHECKPOINTS', + short_desc => 'Time spent flushing dirty buffers during checkpoint, as fraction of checkpoint interval.', + variable => 'CheckPointCompletionTarget', + boot_val => '0.9', + min => '0.0', + max => '1.0', + assign_hook => 'assign_checkpoint_completion_target', +}, + +{ name => 'checkpoint_flush_after', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_CHECKPOINTS', + short_desc => 'Number of pages after which previously performed writes are flushed to disk.', + long_desc => '0 disables forced writeback.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'checkpoint_flush_after', + boot_val => 'DEFAULT_CHECKPOINT_FLUSH_AFTER', + min => '0', + max => 'WRITEBACK_MAX_PENDING_FLUSHES', +}, + +{ name => 'checkpoint_timeout', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_CHECKPOINTS', + short_desc => 'Sets the maximum time between automatic WAL checkpoints.', + flags => 'GUC_UNIT_S', + variable => 'CheckPointTimeout', + boot_val => '300', + min => '30', + max => '86400', +}, + +{ name => 'checkpoint_warning', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_CHECKPOINTS', + short_desc => 'Sets the maximum time before warning if checkpoints triggered by WAL volume happen too frequently.', + long_desc => 'Write a message to the server log if checkpoints caused by the filling of WAL segment files happen more frequently than this amount of time. 0 disables the warning.', + flags => 'GUC_UNIT_S', + variable => 'CheckPointWarning', + boot_val => '30', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'client_connection_check_interval', type => 'int', context => 'PGC_USERSET', group => 'CONN_AUTH_TCP', + short_desc => 'Sets the time interval between checks for disconnection while running queries.', + long_desc => '0 disables connection checks.', + flags => 'GUC_UNIT_MS', + variable => 'client_connection_check_interval', + boot_val => '0', + min => '0', + max => 'INT_MAX', + check_hook => 'check_client_connection_check_interval', +}, + +{ name => 'client_encoding', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the client\'s character set encoding.', + flags => 'GUC_IS_NAME | GUC_REPORT', + variable => 'client_encoding_string', + boot_val => '"SQL_ASCII"', + check_hook => 'check_client_encoding', + assign_hook => 'assign_client_encoding', +}, + +{ name => 'client_min_messages', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the message levels that are sent to the client.', + long_desc => 'Each level includes all the levels that follow it. The later the level, the fewer messages are sent.', + variable => 'client_min_messages', + boot_val => 'NOTICE', + options => 'client_message_level_options', +}, + +{ name => 'cluster_name', type => 'string', context => 'PGC_POSTMASTER', group => 'PROCESS_TITLE', + short_desc => 'Sets the name of the cluster, which is included in the process title.', + flags => 'GUC_IS_NAME', + variable => 'cluster_name', + boot_val => '""', + check_hook => 'check_cluster_name', +}, + +# we have no microseconds designation, so can't supply units here +{ name => 'commit_delay', type => 'int', context => 'PGC_SUSET', group => 'WAL_SETTINGS', + short_desc => 'Sets the delay in microseconds between transaction commit and flushing WAL to disk.', + variable => 'CommitDelay', + boot_val => '0', + min => '0', + max => '100000', +}, + +{ name => 'commit_siblings', type => 'int', context => 'PGC_USERSET', group => 'WAL_SETTINGS', + short_desc => 'Sets the minimum number of concurrent open transactions required before performing "commit_delay".', + variable => 'CommitSiblings', + boot_val => '5', + min => '0', + max => '1000', +}, + +{ name => 'commit_timestamp_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the commit timestamp cache.', + long_desc => '0 means use a fraction of "shared_buffers".', + flags => 'GUC_UNIT_BLOCKS', + variable => 'commit_timestamp_buffers', + boot_val => '0', + min => '0', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_commit_ts_buffers', +}, + +{ name => 'compute_query_id', type => 'enum', context => 'PGC_SUSET', group => 'STATS_MONITORING', + short_desc => 'Enables in-core computation of query identifiers.', + variable => 'compute_query_id', + boot_val => 'COMPUTE_QUERY_ID_AUTO', + options => 'compute_query_id_options', +}, + +{ name => 'config_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS', + short_desc => 'Sets the server\'s main configuration file.', + flags => 'GUC_DISALLOW_IN_FILE | GUC_SUPERUSER_ONLY', + variable => 'ConfigFileName', + boot_val => 'NULL', +}, + +{ name => 'constraint_exclusion', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Enables the planner to use constraints to optimize queries.', + long_desc => 'Table scans will be skipped if their constraints guarantee that no rows match the query.', + flags => 'GUC_EXPLAIN', + variable => 'constraint_exclusion', + boot_val => 'CONSTRAINT_EXCLUSION_PARTITION', + options => 'constraint_exclusion_options', +}, + +{ name => 'cpu_index_tuple_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of processing each index entry during an index scan.', + flags => 'GUC_EXPLAIN', + variable => 'cpu_index_tuple_cost', + boot_val => 'DEFAULT_CPU_INDEX_TUPLE_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'cpu_operator_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of processing each operator or function call.', + flags => 'GUC_EXPLAIN', + variable => 'cpu_operator_cost', + boot_val => 'DEFAULT_CPU_OPERATOR_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'cpu_tuple_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of processing each tuple (row).', + flags => 'GUC_EXPLAIN', + variable => 'cpu_tuple_cost', + boot_val => 'DEFAULT_CPU_TUPLE_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'createrole_self_grant', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets whether a CREATEROLE user automatically grants the role to themselves, and with which options.', + long_desc => 'An empty string disables automatic self grants.', + flags => 'GUC_LIST_INPUT', + variable => 'createrole_self_grant', + boot_val => '""', + check_hook => 'check_createrole_self_grant', + assign_hook => 'assign_createrole_self_grant', +}, + +{ name => 'cursor_tuple_fraction', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Sets the planner\'s estimate of the fraction of a cursor\'s rows that will be retrieved.', + flags => 'GUC_EXPLAIN', + variable => 'cursor_tuple_fraction', + boot_val => 'DEFAULT_CURSOR_TUPLE_FRACTION', + min => '0.0', + max => '1.0', +}, + +{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows whether data checksums are turned on for this cluster.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED', + variable => 'data_checksums', + boot_val => 'PG_DATA_CHECKSUM_OFF', + options => 'data_checksums_options', +}, + +# Can't be set by ALTER SYSTEM as it can lead to recursive definition +# of data_directory. +{ name => 'data_directory', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS', + short_desc => 'Sets the server\'s data directory.', + flags => 'GUC_SUPERUSER_ONLY | GUC_DISALLOW_IN_AUTO_FILE', + variable => 'data_directory', + boot_val => 'NULL', +}, + +{ name => 'data_directory_mode', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the mode of the data directory.', + long_desc => 'The parameter value is a numeric mode specification in the form accepted by the chmod and umask system calls. (To use the customary octal format the number must start with a 0 (zero).)', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED', + variable => 'data_directory_mode', + boot_val => '0700', + min => '0000', + max => '0777', + show_hook => 'show_data_directory_mode', +}, + +{ name => 'data_sync_retry', type => 'bool', context => 'PGC_POSTMASTER', group => 'ERROR_HANDLING_OPTIONS', + short_desc => 'Whether to continue running after a failure to sync data files.', + variable => 'data_sync_retry', + boot_val => 'false', +}, + +{ name => 'DateStyle', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the display format for date and time values.', + long_desc => 'Also controls interpretation of ambiguous date inputs.', + flags => 'GUC_LIST_INPUT | GUC_REPORT', + variable => 'datestyle_string', + boot_val => '"ISO, MDY"', + check_hook => 'check_datestyle', + assign_hook => 'assign_datestyle', +}, + +# This is PGC_SUSET to prevent hiding from log_lock_waits. +{ name => 'deadlock_timeout', type => 'int', context => 'PGC_SUSET', group => 'LOCK_MANAGEMENT', + short_desc => 'Sets the time to wait on a lock before checking for deadlock.', + flags => 'GUC_UNIT_MS', + variable => 'DeadlockTimeout', + boot_val => '1000', + min => '1', + max => 'INT_MAX', +}, + +{ name => 'debug_assertions', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows whether the running server has assertion checks enabled.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'assert_enabled', + boot_val => 'DEFAULT_ASSERT_ENABLED', +}, + +{ name => 'debug_copy_parse_plan_trees', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Set this to force all parse and plan trees to be passed through copyObject(), to facilitate catching errors and omissions in copyObject().', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Debug_copy_parse_plan_trees', + boot_val => 'DEFAULT_DEBUG_COPY_PARSE_PLAN_TREES', + ifdef => 'DEBUG_NODE_TESTS_ENABLED', +}, + +{ name => 'debug_deadlocks', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Dumps information about all current locks when a deadlock timeout occurs.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Debug_deadlocks', + boot_val => 'false', + ifdef => 'LOCK_DEBUG', +}, + +{ name => 'debug_discard_caches', type => 'int', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Aggressively flush system caches for debugging purposes.', + long_desc => '0 means use normal caching behavior.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'debug_discard_caches', + boot_val => 'DEFAULT_DEBUG_DISCARD_CACHES', + min => 'MIN_DEBUG_DISCARD_CACHES', + max => 'MAX_DEBUG_DISCARD_CACHES', +}, + +{ name => 'debug_exec_backend', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows whether the running server is built with EXEC_BACKEND enabled.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'exec_backend_enabled', + boot_val => 'EXEC_BACKEND_ENABLED', +}, + +{ name => 'debug_io_direct', type => 'string', context => 'PGC_POSTMASTER', group => 'DEVELOPER_OPTIONS', + short_desc => 'Use direct I/O for file access.', + long_desc => 'An empty string disables direct I/O.', + flags => 'GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE', + variable => 'debug_io_direct_string', + boot_val => '""', + check_hook => 'check_debug_io_direct', + assign_hook => 'assign_debug_io_direct', +}, + +{ name => 'debug_logical_replication_streaming', type => 'enum', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Forces immediate streaming or serialization of changes in large transactions.', + long_desc => 'On the publisher, it allows streaming or serializing each change in logical decoding. On the subscriber, it allows serialization of all changes to files and notifies the parallel apply workers to read and apply them at the end of the transaction.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'debug_logical_replication_streaming', + boot_val => 'DEBUG_LOGICAL_REP_STREAMING_BUFFERED', + options => 'debug_logical_replication_streaming_options', +}, + +{ name => 'debug_parallel_query', type => 'enum', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Forces the planner\'s use parallel query nodes.', + long_desc => 'This can be useful for testing the parallel query infrastructure by forcing the planner to generate plans that contain nodes that perform tuple communication between workers and the main process.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_EXPLAIN', + variable => 'debug_parallel_query', + boot_val => 'DEBUG_PARALLEL_OFF', + options => 'debug_parallel_query_options', +}, + +{ name => 'debug_pretty_print', type => 'bool', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Indents parse and plan tree displays.', + variable => 'Debug_pretty_print', + boot_val => 'true', +}, + +{ name => 'debug_print_parse', type => 'bool', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Logs each query\'s parse tree.', + variable => 'Debug_print_parse', + boot_val => 'false', +}, + +{ name => 'debug_print_plan', type => 'bool', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Logs each query\'s execution plan.', + variable => 'Debug_print_plan', + boot_val => 'false', +}, + +{ name => 'debug_print_raw_parse', type => 'bool', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Logs each query\'s raw parse tree.', + variable => 'Debug_print_raw_parse', + boot_val => 'false', +}, + +{ name => 'debug_print_rewritten', type => 'bool', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Logs each query\'s rewritten parse tree.', + variable => 'Debug_print_rewritten', + boot_val => 'false', +}, + +{ name => 'debug_raw_expression_coverage_test', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Set this to force all raw parse trees for DML statements to be scanned by raw_expression_tree_walker(), to facilitate catching errors and omissions in that function.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Debug_raw_expression_coverage_test', + boot_val => 'DEFAULT_DEBUG_RAW_EXPRESSION_COVERAGE_TEST', + ifdef => 'DEBUG_NODE_TESTS_ENABLED', +}, + +{ name => 'debug_write_read_parse_plan_trees', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Set this to force all parse and plan trees to be passed through outfuncs.c/readfuncs.c, to facilitate catching errors and omissions in those modules.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Debug_write_read_parse_plan_trees', + boot_val => 'DEFAULT_DEBUG_WRITE_READ_PARSE_PLAN_TREES', + ifdef => 'DEBUG_NODE_TESTS_ENABLED', +}, + +{ name => 'default_statistics_target', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Sets the default statistics target.', + long_desc => 'This applies to table columns that have not had a column-specific target set via ALTER TABLE SET STATISTICS.', + variable => 'default_statistics_target', + boot_val => '100', + min => '1', + max => 'MAX_STATISTICS_TARGET', +}, + +{ name => 'default_table_access_method', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the default table access method for new tables.', + flags => 'GUC_IS_NAME', + variable => 'default_table_access_method', + boot_val => 'DEFAULT_TABLE_ACCESS_METHOD', + check_hook => 'check_default_table_access_method', +}, + +{ name => 'default_tablespace', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the default tablespace to create tables and indexes in.', + long_desc => 'An empty string means use the database\'s default tablespace.', + flags => 'GUC_IS_NAME', + variable => 'default_tablespace', + boot_val => '""', + check_hook => 'check_default_tablespace', +}, + +{ name => 'default_text_search_config', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets default text search configuration.', + variable => 'TSCurrentConfig', + boot_val => '"pg_catalog.simple"', + check_hook => 'check_default_text_search_config', + assign_hook => 'assign_default_text_search_config', +}, + +{ name => 'default_toast_compression', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the default compression method for compressible values.', + variable => 'default_toast_compression', + boot_val => 'DEFAULT_TOAST_COMPRESSION', + options => 'default_toast_compression_options', +}, + +{ name => 'default_transaction_deferrable', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the default deferrable status of new transactions.', + variable => 'DefaultXactDeferrable', + boot_val => 'false', +}, + +{ name => 'default_transaction_isolation', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the transaction isolation level of each new transaction.', + variable => 'DefaultXactIsoLevel', + boot_val => 'XACT_READ_COMMITTED', + options => 'isolation_level_options', +}, + +{ name => 'default_transaction_read_only', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the default read-only status of new transactions.', + flags => 'GUC_REPORT', + variable => 'DefaultXactReadOnly', + boot_val => 'false', +}, + +# WITH OIDS support, and consequently default_with_oids, was removed +# in PostgreSQL 12, but we tolerate the parameter being set to false +# to avoid unnecessarily breaking older dump files. +{ name => 'default_with_oids', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'WITH OIDS is no longer supported; this can only be false.', + flags => 'GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE', + variable => 'default_with_oids', + boot_val => 'false', + check_hook => 'check_default_with_oids', +}, + +{ name => 'dynamic_library_path', type => 'string', context => 'PGC_SUSET', group => 'CLIENT_CONN_OTHER', + short_desc => 'Sets the path for dynamically loadable modules.', + long_desc => 'If a dynamically loadable module needs to be opened and the specified name does not have a directory component (i.e., the name does not contain a slash), the system will search this path for the specified file.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'Dynamic_library_path', + boot_val => '"$libdir"', +}, + +{ name => 'dynamic_shared_memory_type', type => 'enum', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Selects the dynamic shared memory implementation used.', + variable => 'dynamic_shared_memory_type', + boot_val => 'DEFAULT_DYNAMIC_SHARED_MEMORY_TYPE', + options => 'dynamic_shared_memory_options', +}, + +{ name => 'effective_cache_size', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s assumption about the total size of the data caches.', + long_desc => 'That is, the total size of the caches (kernel cache and shared buffers) used for PostgreSQL data files. This is measured in disk pages, which are normally 8 kB each.', + flags => 'GUC_UNIT_BLOCKS | GUC_EXPLAIN', + variable => 'effective_cache_size', + boot_val => 'DEFAULT_EFFECTIVE_CACHE_SIZE', + min => '1', + max => 'INT_MAX', +}, + +{ name => 'effective_io_concurrency', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_IO', + short_desc => 'Number of simultaneous requests that can be handled efficiently by the disk subsystem.', + long_desc => '0 disables simultaneous requests.', + flags => 'GUC_EXPLAIN', + variable => 'effective_io_concurrency', + boot_val => 'DEFAULT_EFFECTIVE_IO_CONCURRENCY', + min => '0', + max => 'MAX_IO_CONCURRENCY', +}, + +{ name => 'effective_wal_level', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows effective WAL level.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'effective_wal_level', + boot_val => 'WAL_LEVEL_REPLICA', + options => 'wal_level_options', + show_hook => 'show_effective_wal_level', +}, + +{ name => 'enable_async_append', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of async append plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_async_append', + boot_val => 'true', +}, + +{ name => 'enable_bitmapscan', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of bitmap-scan plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_bitmapscan', + boot_val => 'true', +}, + +{ name => 'enable_distinct_reordering', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables reordering of DISTINCT keys.', + flags => 'GUC_EXPLAIN', + variable => 'enable_distinct_reordering', + boot_val => 'true', +}, + +{ name => 'enable_eager_aggregate', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables eager aggregation.', + flags => 'GUC_EXPLAIN', + variable => 'enable_eager_aggregate', + boot_val => 'true', +}, + +{ name => 'enable_gathermerge', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of gather merge plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_gathermerge', + boot_val => 'true', +}, + +{ name => 'enable_group_by_reordering', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables reordering of GROUP BY keys.', + flags => 'GUC_EXPLAIN', + variable => 'enable_group_by_reordering', + boot_val => 'true', +}, + +{ name => 'enable_hashagg', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of hashed aggregation plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_hashagg', + boot_val => 'true', +}, + +{ name => 'enable_hashjoin', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of hash join plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_hashjoin', + boot_val => 'true', +}, + +{ name => 'enable_incremental_sort', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of incremental sort steps.', + flags => 'GUC_EXPLAIN', + variable => 'enable_incremental_sort', + boot_val => 'true', +}, + +{ name => 'enable_indexonlyscan', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of index-only-scan plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_indexonlyscan', + boot_val => 'true', +}, + +{ name => 'enable_indexscan', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of index-scan plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_indexscan', + boot_val => 'true', +}, + +{ name => 'enable_material', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of materialization.', + flags => 'GUC_EXPLAIN', + variable => 'enable_material', + boot_val => 'true', +}, + +{ name => 'enable_memoize', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of memoization.', + flags => 'GUC_EXPLAIN', + variable => 'enable_memoize', + boot_val => 'true', +}, + +{ name => 'enable_mergejoin', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of merge join plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_mergejoin', + boot_val => 'true', +}, + +{ name => 'enable_nestloop', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of nested-loop join plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_nestloop', + boot_val => 'true', +}, + +{ name => 'enable_parallel_append', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of parallel append plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_parallel_append', + boot_val => 'true', +}, + +{ name => 'enable_parallel_hash', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of parallel hash plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_parallel_hash', + boot_val => 'true', +}, + +{ name => 'enable_partition_pruning', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables plan-time and execution-time partition pruning.', + long_desc => 'Allows the query planner and executor to compare partition bounds to conditions in the query to determine which partitions must be scanned.', + flags => 'GUC_EXPLAIN', + variable => 'enable_partition_pruning', + boot_val => 'true', +}, + +{ name => 'enable_partitionwise_aggregate', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables partitionwise aggregation and grouping.', + flags => 'GUC_EXPLAIN', + variable => 'enable_partitionwise_aggregate', + boot_val => 'false', +}, + +{ name => 'enable_partitionwise_join', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables partitionwise join.', + flags => 'GUC_EXPLAIN', + variable => 'enable_partitionwise_join', + boot_val => 'false', +}, + +{ name => 'enable_presorted_aggregate', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s ability to produce plans that provide presorted input for ORDER BY / DISTINCT aggregate functions.', + long_desc => 'Allows the query planner to build plans that provide presorted input for aggregate functions with an ORDER BY / DISTINCT clause. When disabled, implicit sorts are always performed during execution.', + flags => 'GUC_EXPLAIN', + variable => 'enable_presorted_aggregate', + boot_val => 'true', +}, + +{ name => 'enable_self_join_elimination', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables removal of unique self-joins.', + flags => 'GUC_EXPLAIN', + variable => 'enable_self_join_elimination', + boot_val => 'true', +}, + +{ name => 'enable_seqscan', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of sequential-scan plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_seqscan', + boot_val => 'true', +}, + +{ name => 'enable_sort', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of explicit sort steps.', + flags => 'GUC_EXPLAIN', + variable => 'enable_sort', + boot_val => 'true', +}, + +{ name => 'enable_tidscan', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables the planner\'s use of TID scan plans.', + flags => 'GUC_EXPLAIN', + variable => 'enable_tidscan', + boot_val => 'true', +}, + +{ name => 'event_source', type => 'string', context => 'PGC_POSTMASTER', group => 'LOGGING_WHERE', + short_desc => 'Sets the application name used to identify PostgreSQL messages in the event log.', + variable => 'event_source', + boot_val => 'DEFAULT_EVENT_SOURCE', +}, + +{ name => 'event_triggers', type => 'bool', context => 'PGC_SUSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Enables event triggers.', + long_desc => 'When enabled, event triggers will fire for all applicable statements.', + variable => 'event_triggers', + boot_val => 'true', +}, + +{ name => 'exit_on_error', type => 'bool', context => 'PGC_USERSET', group => 'ERROR_HANDLING_OPTIONS', + short_desc => 'Terminate session on any error.', + variable => 'ExitOnAnyError', + boot_val => 'false', +}, + +{ name => 'extension_control_path', type => 'string', context => 'PGC_SUSET', group => 'CLIENT_CONN_OTHER', + short_desc => 'Sets the path for extension control files.', + long_desc => 'The remaining extension script and secondary control files are then loaded from the same directory where the primary control file was found.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'Extension_control_path', + boot_val => '"$system"', +}, + +{ name => 'external_pid_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS', + short_desc => 'Writes the postmaster PID to the specified file.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'external_pid_file', + boot_val => 'NULL', + check_hook => 'check_canonical_path', +}, + +{ name => 'extra_float_digits', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the number of digits displayed for floating-point values.', + long_desc => 'This affects real, double precision, and geometric data types. A zero or negative parameter value is added to the standard number of digits (FLT_DIG or DBL_DIG as appropriate). Any value greater than zero selects precise output mode.', + variable => 'extra_float_digits', + boot_val => '1', + min => '-15', + max => '3', +}, + +{ name => 'file_copy_method', type => 'enum', context => 'PGC_USERSET', group => 'RESOURCES_DISK', + short_desc => 'Selects the file copy method.', + variable => 'file_copy_method', + boot_val => 'FILE_COPY_METHOD_COPY', + options => 'file_copy_method_options', +}, + +{ name => 'file_extend_method', type => 'enum', context => 'PGC_SIGHUP', group => 'RESOURCES_DISK', + short_desc => 'Selects the method used for extending data files.', + variable => 'file_extend_method', + boot_val => 'DEFAULT_FILE_EXTEND_METHOD', + options => 'file_extend_method_options', +}, + +{ name => 'from_collapse_limit', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Sets the FROM-list size beyond which subqueries are not collapsed.', + long_desc => 'The planner will merge subqueries into upper queries if the resulting FROM list would have no more than this many items.', + flags => 'GUC_EXPLAIN', + variable => 'from_collapse_limit', + boot_val => '8', + min => '1', + max => 'INT_MAX', +}, + +{ name => 'fsync', type => 'bool', context => 'PGC_SIGHUP', group => 'WAL_SETTINGS', + short_desc => 'Forces synchronization of updates to disk.', + long_desc => 'The server will use the fsync() system call in several places to make sure that updates are physically written to disk. This ensures that a database cluster will recover to a consistent state after an operating system or hardware crash.', + variable => 'enableFsync', + boot_val => 'true', +}, + +{ name => 'full_page_writes', type => 'bool', context => 'PGC_SIGHUP', group => 'WAL_SETTINGS', + short_desc => 'Writes full pages to WAL when first modified after a checkpoint.', + long_desc => 'A page write in process during an operating system crash might be only partially written to disk. During recovery, the row changes stored in WAL are not enough to recover. This option writes pages when first modified after a checkpoint to WAL so full recovery is possible.', + variable => 'fullPageWrites', + boot_val => 'true', +}, + +{ name => 'geqo', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'Enables genetic query optimization.', + long_desc => 'This algorithm attempts to do planning without exhaustive searching.', + flags => 'GUC_EXPLAIN', + variable => 'enable_geqo', + boot_val => 'true', +}, + +{ name => 'geqo_effort', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'GEQO: effort is used to set the default for other GEQO parameters.', + flags => 'GUC_EXPLAIN', + variable => 'Geqo_effort', + boot_val => 'DEFAULT_GEQO_EFFORT', + min => 'MIN_GEQO_EFFORT', + max => 'MAX_GEQO_EFFORT', +}, + +{ name => 'geqo_generations', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'GEQO: number of iterations of the algorithm.', + long_desc => '0 means use a suitable default value.', + flags => 'GUC_EXPLAIN', + variable => 'Geqo_generations', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'geqo_pool_size', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'GEQO: number of individuals in the population.', + long_desc => '0 means use a suitable default value.', + flags => 'GUC_EXPLAIN', + variable => 'Geqo_pool_size', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'geqo_seed', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'GEQO: seed for random path selection.', + flags => 'GUC_EXPLAIN', + variable => 'Geqo_seed', + boot_val => '0.0', + min => '0.0', + max => '1.0', +}, + +{ name => 'geqo_selection_bias', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'GEQO: selective pressure within the population.', + flags => 'GUC_EXPLAIN', + variable => 'Geqo_selection_bias', + boot_val => 'DEFAULT_GEQO_SELECTION_BIAS', + min => 'MIN_GEQO_SELECTION_BIAS', + max => 'MAX_GEQO_SELECTION_BIAS', +}, + +{ name => 'geqo_threshold', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_GEQO', + short_desc => 'Sets the threshold of FROM items beyond which GEQO is used.', + flags => 'GUC_EXPLAIN', + variable => 'geqo_threshold', + boot_val => '12', + min => '2', + max => 'INT_MAX', +}, + +{ name => 'gin_fuzzy_search_limit', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_OTHER', + short_desc => 'Sets the maximum allowed result for exact search by GIN.', + long_desc => '0 means no limit.', + variable => 'GinFuzzySearchLimit', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'gin_pending_list_limit', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the maximum size of the pending list for GIN index.', + flags => 'GUC_UNIT_KB', + variable => 'gin_pending_list_limit', + boot_val => '4096', + min => '64', + max => 'MAX_KILOBYTES', +}, + +{ name => 'gss_accept_delegation', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', + short_desc => 'Sets whether GSSAPI delegation should be accepted from the client.', + variable => 'pg_gss_accept_delegation', + boot_val => 'false', +}, + +{ name => 'hash_mem_multiplier', type => 'real', context => 'PGC_USERSET', group => 'RESOURCES_MEM', + short_desc => 'Multiple of "work_mem" to use for hash tables.', + flags => 'GUC_EXPLAIN', + variable => 'hash_mem_multiplier', + boot_val => '2.0', + min => '1.0', + max => '1000.0', +}, + +{ name => 'hba_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS', + short_desc => 'Sets the server\'s "hba" configuration file.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'HbaFileName', + boot_val => 'NULL', +}, + +{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS', + short_desc => 'Sets the server\'s "hosts" configuration file.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'HostsFileName', + boot_val => 'NULL', +}, + +{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY', + short_desc => 'Allows connections and queries during recovery.', + variable => 'EnableHotStandby', + boot_val => 'true', +}, + +{ name => 'hot_standby_feedback', type => 'bool', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Allows feedback from a hot standby to the primary that will avoid query conflicts.', + variable => 'hot_standby_feedback', + boot_val => 'false', +}, + +{ name => 'huge_page_size', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'The size of huge page that should be requested.', + long_desc => '0 means use the system default.', + flags => 'GUC_UNIT_KB', + variable => 'huge_page_size', + boot_val => '0', + min => '0', + max => 'INT_MAX', + check_hook => 'check_huge_page_size', +}, + +{ name => 'huge_pages', type => 'enum', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Use of huge pages on Linux or Windows.', + variable => 'huge_pages', + boot_val => 'HUGE_PAGES_TRY', + options => 'huge_pages_options', +}, + +{ name => 'huge_pages_status', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Indicates the status of huge pages.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'huge_pages_status', + boot_val => 'HUGE_PAGES_UNKNOWN', + options => 'huge_pages_status_options', +}, + +{ name => 'icu_validation_level', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Log level for reporting invalid ICU locale strings.', + variable => 'icu_validation_level', + boot_val => 'WARNING', + options => 'icu_validation_level_options', +}, + +{ name => 'ident_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS', + short_desc => 'Sets the server\'s "ident" configuration file.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'IdentFileName', + boot_val => 'NULL', +}, + +{ name => 'idle_in_transaction_session_timeout', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the maximum allowed idle time between queries, when in a transaction.', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_MS', + variable => 'IdleInTransactionSessionTimeout', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'idle_replication_slot_timeout', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SENDING', + short_desc => 'Sets the duration a replication slot can remain idle before it is invalidated.', + flags => 'GUC_UNIT_S', + variable => 'idle_replication_slot_timeout_secs', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'idle_session_timeout', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the maximum allowed idle time between queries, when not in a transaction.', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_MS', + variable => 'IdleSessionTimeout', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'ignore_checksum_failure', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Continues processing after a checksum failure.', + long_desc => 'Detection of a checksum failure normally causes PostgreSQL to report an error, aborting the current transaction. Setting ignore_checksum_failure to true causes the system to ignore the failure (but still report a warning), and continue processing. This behavior could cause crashes or other serious problems. Only has an effect if checksums are enabled.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'ignore_checksum_failure', + boot_val => 'false', +}, + +{ name => 'ignore_invalid_pages', type => 'bool', context => 'PGC_POSTMASTER', group => 'DEVELOPER_OPTIONS', + short_desc => 'Continues recovery after an invalid pages failure.', + long_desc => 'Detection of WAL records having references to invalid pages during recovery causes PostgreSQL to raise a PANIC-level error, aborting the recovery. Setting "ignore_invalid_pages" to true causes the system to ignore invalid page references in WAL records (but still report a warning), and continue recovery. This behavior may cause crashes, data loss, propagate or hide corruption, or other serious problems. Only has an effect during recovery or in standby mode.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'ignore_invalid_pages', + boot_val => 'false', +}, + +{ name => 'ignore_system_indexes', type => 'bool', context => 'PGC_BACKEND', group => 'DEVELOPER_OPTIONS', + short_desc => 'Disables reading from system indexes.', + long_desc => 'It does not prevent updating the indexes, so it is safe to use. The worst consequence is slowness.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'IgnoreSystemIndexes', + boot_val => 'false', +}, + +{ name => 'in_hot_standby', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows whether hot standby is currently active.', + flags => 'GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'in_hot_standby_guc', + boot_val => 'false', + show_hook => 'show_in_hot_standby', +}, + +{ name => 'integer_datetimes', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows whether datetimes are integer based.', + flags => 'GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'integer_datetimes', + boot_val => 'true', +}, + +{ name => 'IntervalStyle', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the display format for interval values.', + flags => 'GUC_REPORT', + variable => 'IntervalStyle', + boot_val => 'INTSTYLE_POSTGRES', + options => 'intervalstyle_options', +}, + +{ name => 'io_combine_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_IO', + short_desc => 'Limit on the size of data reads and writes.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'io_combine_limit_guc', + boot_val => 'DEFAULT_IO_COMBINE_LIMIT', + min => '1', + max => 'MAX_IO_COMBINE_LIMIT', + assign_hook => 'assign_io_combine_limit', +}, + +{ name => 'io_max_combine_limit', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_IO', + short_desc => 'Server-wide limit that clamps io_combine_limit.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'io_max_combine_limit', + boot_val => 'DEFAULT_IO_COMBINE_LIMIT', + min => '1', + max => 'MAX_IO_COMBINE_LIMIT', + assign_hook => 'assign_io_max_combine_limit', +}, + +{ name => 'io_max_concurrency', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_IO', + short_desc => 'Max number of IOs that one process can execute simultaneously.', + variable => 'io_max_concurrency', + boot_val => '-1', + min => '-1', + max => '1024', + check_hook => 'check_io_max_concurrency', +}, + +{ name => 'io_max_workers', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_IO', + short_desc => 'Maximum number of I/O worker processes, for io_method=worker.', + variable => 'io_max_workers', + boot_val => '8', + min => '1', + max => 'MAX_IO_WORKERS', +}, + +{ name => 'io_method', type => 'enum', context => 'PGC_POSTMASTER', group => 'RESOURCES_IO', + short_desc => 'Selects the method for executing asynchronous I/O.', + variable => 'io_method', + boot_val => 'DEFAULT_IO_METHOD', + options => 'io_method_options', + assign_hook => 'assign_io_method', +}, + +{ name => 'io_min_workers', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_IO', + short_desc => 'Minimum number of I/O worker processes, for io_method=worker.', + variable => 'io_min_workers', + boot_val => '2', + min => '1', + max => 'MAX_IO_WORKERS', +}, + +{ name => 'io_worker_idle_timeout', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_IO', + short_desc => 'Maximum time before idle I/O worker processes time out, for io_method=worker.', + variable => 'io_worker_idle_timeout', + flags => 'GUC_UNIT_MS', + boot_val => '60000', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'io_worker_launch_interval', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_IO', + short_desc => 'Minimum time before launching a new I/O worker process, for io_method=worker.', + variable => 'io_worker_launch_interval', + flags => 'GUC_UNIT_MS', + boot_val => '100', + min => '0', + max => 'INT_MAX', +}, + +# Not for general use --- used by SET SESSION AUTHORIZATION and SET +# ROLE +{ name => 'is_superuser', type => 'bool', context => 'PGC_INTERNAL', group => 'UNGROUPED', + short_desc => 'Shows whether the current user is a superuser.', + flags => 'GUC_REPORT | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_ALLOW_IN_PARALLEL', + variable => 'current_role_is_superuser', + boot_val => 'false', +}, + +{ name => 'jit', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Allow JIT compilation.', + flags => 'GUC_EXPLAIN', + variable => 'jit_enabled', + boot_val => 'false', +}, + +{ name => 'jit_above_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Perform JIT compilation if query is more expensive.', + long_desc => '-1 disables JIT compilation.', + flags => 'GUC_EXPLAIN', + variable => 'jit_above_cost', + boot_val => '100000', + min => '-1', + max => 'DBL_MAX', +}, + +# This is not guaranteed to be available, but given it's a developer +# oriented option, it doesn't seem worth adding code checking +# availability. +{ name => 'jit_debugging_support', type => 'bool', context => 'PGC_SU_BACKEND', group => 'DEVELOPER_OPTIONS', + short_desc => 'Register JIT-compiled functions with debugger.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'jit_debugging_support', + boot_val => 'false', +}, + +{ name => 'jit_dump_bitcode', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Write out LLVM bitcode to facilitate JIT debugging.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'jit_dump_bitcode', + boot_val => 'false', +}, + +{ name => 'jit_expressions', type => 'bool', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Allow JIT compilation of expressions.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'jit_expressions', + boot_val => 'true', +}, + +{ name => 'jit_inline_above_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Perform JIT inlining if query is more expensive.', + long_desc => '-1 disables inlining.', + flags => 'GUC_EXPLAIN', + variable => 'jit_inline_above_cost', + boot_val => '500000', + min => '-1', + max => 'DBL_MAX', +}, + +{ name => 'jit_optimize_above_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Optimize JIT-compiled functions if query is more expensive.', + long_desc => '-1 disables optimization.', + flags => 'GUC_EXPLAIN', + variable => 'jit_optimize_above_cost', + boot_val => '500000', + min => '-1', + max => 'DBL_MAX', +}, + +# This is not guaranteed to be available, but given it's a developer +# oriented option, it doesn't seem worth adding code checking +# availability. +{ name => 'jit_profiling_support', type => 'bool', context => 'PGC_SU_BACKEND', group => 'DEVELOPER_OPTIONS', + short_desc => 'Register JIT-compiled functions with perf profiler.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'jit_profiling_support', + boot_val => 'false', +}, + +{ name => 'jit_provider', type => 'string', context => 'PGC_POSTMASTER', group => 'CLIENT_CONN_PRELOAD', + short_desc => 'JIT provider to use.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'jit_provider', + boot_val => '"llvmjit"', +}, + +{ name => 'jit_tuple_deforming', type => 'bool', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Allow JIT compilation of tuple deforming.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'jit_tuple_deforming', + boot_val => 'true', +}, + +{ name => 'join_collapse_limit', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Sets the FROM-list size beyond which JOIN constructs are not flattened.', + long_desc => 'The planner will flatten explicit JOIN constructs into lists of FROM items whenever a list of no more than this many items would result.', + flags => 'GUC_EXPLAIN', + variable => 'join_collapse_limit', + boot_val => '8', + min => '1', + max => 'INT_MAX', +}, + +{ name => 'krb_caseins_users', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', + short_desc => 'Sets whether Kerberos and GSSAPI user names should be treated as case-insensitive.', + variable => 'pg_krb_caseins_users', + boot_val => 'false', +}, + +{ name => 'krb_server_keyfile', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', + short_desc => 'Sets the location of the Kerberos server key file.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'pg_krb_server_keyfile', + boot_val => 'PG_KRB_SRVTAB', +}, + +{ name => 'lc_messages', type => 'string', context => 'PGC_SUSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the language in which messages are displayed.', + long_desc => 'An empty string means use the operating system setting.', + variable => 'locale_messages', + boot_val => '""', + check_hook => 'check_locale_messages', + assign_hook => 'assign_locale_messages', +}, + +{ name => 'lc_monetary', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the locale for formatting monetary amounts.', + long_desc => 'An empty string means use the operating system setting.', + variable => 'locale_monetary', + boot_val => '"C"', + check_hook => 'check_locale_monetary', + assign_hook => 'assign_locale_monetary', +}, + +{ name => 'lc_numeric', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the locale for formatting numbers.', + long_desc => 'An empty string means use the operating system setting.', + variable => 'locale_numeric', + boot_val => '"C"', + check_hook => 'check_locale_numeric', + assign_hook => 'assign_locale_numeric', +}, + +{ name => 'lc_time', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the locale for formatting date and time values.', + long_desc => 'An empty string means use the operating system setting.', + variable => 'locale_time', + boot_val => '"C"', + check_hook => 'check_locale_time', + assign_hook => 'assign_locale_time', +}, + +{ name => 'listen_addresses', type => 'string', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the host name or IP address(es) to listen to.', + flags => 'GUC_LIST_INPUT', + variable => 'ListenAddresses', + boot_val => '"localhost"', +}, + +{ name => 'lo_compat_privileges', type => 'bool', context => 'PGC_SUSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'Enables backward compatibility mode for privilege checks on large objects.', + long_desc => 'Skips privilege checks when reading or modifying large objects, for compatibility with PostgreSQL releases prior to 9.0.', + variable => 'lo_compat_privileges', + boot_val => 'false', +}, + +{ name => 'local_preload_libraries', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_PRELOAD', + short_desc => 'Lists unprivileged shared libraries to preload into each backend.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE', + variable => 'local_preload_libraries_string', + boot_val => '""', +}, + +{ name => 'lock_timeout', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the maximum allowed duration of any wait for a lock.', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_MS', + variable => 'LockTimeout', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'log_autoanalyze_min_duration', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Sets the minimum execution time above which analyze actions by autovacuum will be logged.', + long_desc => '-1 disables logging analyze actions by autovacuum. 0 means log all analyze actions by autovacuum.', + flags => 'GUC_UNIT_MS', + variable => 'Log_autoanalyze_min_duration', + boot_val => '600000', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'log_autovacuum_min_duration', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Sets the minimum execution time above which vacuum actions by autovacuum will be logged.', + long_desc => '-1 disables logging vacuum actions by autovacuum. 0 means log all vacuum actions by autovacuum.', + flags => 'GUC_UNIT_MS', + variable => 'Log_autovacuum_min_duration', + boot_val => '600000', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'log_btree_build_stats', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Logs system resource usage statistics (memory and CPU) on various B-tree operations.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'log_btree_build_stats', + boot_val => 'false', + ifdef => 'BTREE_BUILD_STATS', +}, + +{ name => 'log_checkpoints', type => 'bool', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Logs each checkpoint.', + variable => 'log_checkpoints', + boot_val => 'true', +}, + +{ name => 'log_connections', type => 'string', context => 'PGC_SU_BACKEND', group => 'LOGGING_WHAT', + short_desc => 'Logs specified aspects of connection establishment and setup.', + flags => 'GUC_LIST_INPUT', + variable => 'log_connections_string', + boot_val => '""', + check_hook => 'check_log_connections', + assign_hook => 'assign_log_connections', +}, + +{ name => 'log_destination', type => 'string', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the destination for server log output.', + long_desc => 'Valid values are combinations of "stderr", "syslog", "csvlog", "jsonlog", and "eventlog", depending on the platform.', + flags => 'GUC_LIST_INPUT', + variable => 'Log_destination_string', + boot_val => '"stderr"', + check_hook => 'check_log_destination', + assign_hook => 'assign_log_destination', +}, + +{ name => 'log_directory', type => 'string', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the destination directory for log files.', + long_desc => 'Can be specified as relative to the data directory or as absolute path.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'Log_directory', + boot_val => '"log"', + check_hook => 'check_canonical_path', +}, + +{ name => 'log_disconnections', type => 'bool', context => 'PGC_SU_BACKEND', group => 'LOGGING_WHAT', + short_desc => 'Logs end of a session, including duration.', + variable => 'Log_disconnections', + boot_val => 'false', +}, + +{ name => 'log_duration', type => 'bool', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Logs the duration of each completed SQL statement.', + variable => 'log_duration', + boot_val => 'false', +}, + +{ name => 'log_error_verbosity', type => 'enum', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Sets the verbosity of logged messages.', + variable => 'Log_error_verbosity', + boot_val => 'PGERROR_DEFAULT', + options => 'log_error_verbosity_options', +}, + +{ name => 'log_executor_stats', type => 'bool', context => 'PGC_SUSET', group => 'STATS_MONITORING', + short_desc => 'Writes executor performance statistics to the server log.', + variable => 'log_executor_stats', + boot_val => 'false', + check_hook => 'check_stage_log_stats', +}, + +{ name => 'log_file_mode', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the file permissions for log files.', + long_desc => 'The parameter value is expected to be a numeric mode specification in the form accepted by the chmod and umask system calls. (To use the customary octal format the number must start with a 0 (zero).)', + variable => 'Log_file_mode', + boot_val => '0600', + min => '0000', + max => '0777', + show_hook => 'show_log_file_mode', +}, + +{ name => 'log_filename', type => 'string', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the file name pattern for log files.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'Log_filename', + boot_val => '"postgresql-%Y-%m-%d_%H%M%S.log"', +}, + +{ name => 'log_hostname', type => 'bool', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Logs the host name in the connection logs.', + long_desc => 'By default, connection logs only show the IP address of the connecting host. If you want them to show the host name you can turn this on, but depending on your host name resolution setup it might impose a non-negligible performance penalty.', + variable => 'log_hostname', + boot_val => 'false', +}, + +{ name => 'log_line_prefix', type => 'string', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Controls information prefixed to each log line.', + long_desc => 'An empty string means no prefix.', + variable => 'Log_line_prefix', + boot_val => '"%m [%p] "', +}, + +{ name => 'log_lock_failures', type => 'bool', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Logs lock failures.', + variable => 'log_lock_failures', + boot_val => 'false', +}, + +{ name => 'log_lock_waits', type => 'bool', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Logs long lock waits.', + variable => 'log_lock_waits', + boot_val => 'true', +}, + +{ name => 'log_min_duration_sample', type => 'int', context => 'PGC_SUSET', group => 'LOGGING_WHEN', + short_desc => 'Sets the minimum execution time above which a sample of statements will be logged. Sampling is determined by "log_statement_sample_rate".', + long_desc => '-1 disables sampling. 0 means sample all statements.', + flags => 'GUC_UNIT_MS', + variable => 'log_min_duration_sample', + boot_val => '-1', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'log_min_duration_statement', type => 'int', context => 'PGC_SUSET', group => 'LOGGING_WHEN', + short_desc => 'Sets the minimum execution time above which all statements will be logged.', + long_desc => '-1 disables logging statement durations. 0 means log all statement durations.', + flags => 'GUC_UNIT_MS', + variable => 'log_min_duration_statement', + boot_val => '-1', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'log_min_error_statement', type => 'enum', context => 'PGC_SUSET', group => 'LOGGING_WHEN', + short_desc => 'Causes all statements generating error at or above this level to be logged.', + long_desc => 'Each level includes all the levels that follow it. The later the level, the fewer messages are sent.', + variable => 'log_min_error_statement', + boot_val => 'ERROR', + options => 'server_message_level_options', +}, + +{ name => 'log_min_messages', type => 'string', context => 'PGC_SUSET', group => 'LOGGING_WHEN', + short_desc => 'Sets the message levels that are logged.', + long_desc => 'Each level includes all the levels that follow it. The later the level, the fewer messages are sent.', + flags => 'GUC_LIST_INPUT', + variable => 'log_min_messages_string', + boot_val => '"warning"', + check_hook => 'check_log_min_messages', + assign_hook => 'assign_log_min_messages', +}, + +{ name => 'log_parameter_max_length', type => 'int', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Sets the maximum length in bytes of data logged for bind parameter values when logging statements.', + long_desc => '-1 means log values in full.', + flags => 'GUC_UNIT_BYTE', + variable => 'log_parameter_max_length', + boot_val => '-1', + min => '-1', + max => 'INT_MAX / 2', +}, + +{ name => 'log_parameter_max_length_on_error', type => 'int', context => 'PGC_USERSET', group => 'LOGGING_WHAT', + short_desc => 'Sets the maximum length in bytes of data logged for bind parameter values when logging statements, on error.', + long_desc => '-1 means log values in full.', + flags => 'GUC_UNIT_BYTE', + variable => 'log_parameter_max_length_on_error', + boot_val => '0', + min => '-1', + max => 'INT_MAX / 2', +}, + +{ name => 'log_parser_stats', type => 'bool', context => 'PGC_SUSET', group => 'STATS_MONITORING', + short_desc => 'Writes parser performance statistics to the server log.', + variable => 'log_parser_stats', + boot_val => 'false', + check_hook => 'check_stage_log_stats', +}, + +{ name => 'log_planner_stats', type => 'bool', context => 'PGC_SUSET', group => 'STATS_MONITORING', + short_desc => 'Writes planner performance statistics to the server log.', + variable => 'log_planner_stats', + boot_val => 'false', + check_hook => 'check_stage_log_stats', +}, + +{ name => 'log_recovery_conflict_waits', type => 'bool', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Logs standby recovery conflict waits.', + variable => 'log_recovery_conflict_waits', + boot_val => 'false', +}, + +{ name => 'log_replication_commands', type => 'bool', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Logs each replication command.', + variable => 'log_replication_commands', + boot_val => 'false', +}, + +{ name => 'log_rotation_age', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the amount of time to wait before forcing log file rotation.', + long_desc => '0 disables time-based creation of new log files.', + flags => 'GUC_UNIT_MIN', + variable => 'Log_RotationAge', + boot_val => 'HOURS_PER_DAY * MINS_PER_HOUR', + min => '0', + max => 'INT_MAX / SECS_PER_MINUTE', +}, + +{ name => 'log_rotation_size', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the maximum size a log file can reach before being rotated.', + long_desc => '0 disables size-based creation of new log files.', + flags => 'GUC_UNIT_KB', + variable => 'Log_RotationSize', + boot_val => '10 * 1024', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'log_startup_progress_interval', type => 'int', context => 'PGC_SIGHUP', group => 'LOGGING_WHEN', + short_desc => 'Time between progress updates for long-running startup operations.', + long_desc => '0 disables progress updates.', + flags => 'GUC_UNIT_MS', + variable => 'log_startup_progress_interval', + boot_val => '10000', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'log_statement', type => 'enum', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Sets the type of statements logged.', + variable => 'log_statement', + boot_val => 'LOGSTMT_NONE', + options => 'log_statement_options', +}, + +{ name => 'log_statement_sample_rate', type => 'real', context => 'PGC_SUSET', group => 'LOGGING_WHEN', + short_desc => 'Fraction of statements exceeding "log_min_duration_sample" to be logged.', + long_desc => 'Use a value between 0.0 (never log) and 1.0 (always log).', + variable => 'log_statement_sample_rate', + boot_val => '1.0', + min => '0.0', + max => '1.0', +}, + +{ name => 'log_statement_stats', type => 'bool', context => 'PGC_SUSET', group => 'STATS_MONITORING', + short_desc => 'Writes cumulative performance statistics to the server log.', + variable => 'log_statement_stats', + boot_val => 'false', + check_hook => 'check_log_stats', +}, + +{ name => 'log_temp_files', type => 'int', context => 'PGC_SUSET', group => 'LOGGING_WHAT', + short_desc => 'Log the use of temporary files larger than this number of kilobytes.', + long_desc => '-1 disables logging temporary files. 0 means log all temporary files.', + flags => 'GUC_UNIT_KB', + variable => 'log_temp_files', + boot_val => '-1', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'log_timezone', type => 'string', context => 'PGC_SIGHUP', group => 'LOGGING_WHAT', + short_desc => 'Sets the time zone to use in log messages.', + variable => 'log_timezone_string', + boot_val => '"GMT"', + check_hook => 'check_log_timezone', + assign_hook => 'assign_log_timezone', + show_hook => 'show_log_timezone', +}, + +{ name => 'log_transaction_sample_rate', type => 'real', context => 'PGC_SUSET', group => 'LOGGING_WHEN', + short_desc => 'Sets the fraction of transactions from which to log all statements.', + long_desc => 'Use a value between 0.0 (never log) and 1.0 (log all statements for all transactions).', + variable => 'log_xact_sample_rate', + boot_val => '0.0', + min => '0.0', + max => '1.0', +}, + +{ name => 'log_truncate_on_rotation', type => 'bool', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Truncate existing log files of same name during log rotation.', + variable => 'Log_truncate_on_rotation', + boot_val => 'false', +}, + +{ name => 'logging_collector', type => 'bool', context => 'PGC_POSTMASTER', group => 'LOGGING_WHERE', + short_desc => 'Start a subprocess to capture stderr, csvlog and/or jsonlog into log files.', + variable => 'Logging_collector', + boot_val => 'false', +}, + +{ name => 'logical_decoding_work_mem', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum memory to be used for logical decoding.', + long_desc => 'This much memory can be used by each internal reorder buffer before spilling to disk.', + flags => 'GUC_UNIT_KB', + variable => 'logical_decoding_work_mem', + boot_val => '65536', + min => '64', + max => 'MAX_KILOBYTES', +}, + +{ name => 'maintenance_io_concurrency', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_IO', + short_desc => 'A variant of "effective_io_concurrency" that is used for maintenance work.', + long_desc => '0 disables simultaneous requests.', + flags => 'GUC_EXPLAIN', + variable => 'maintenance_io_concurrency', + boot_val => 'DEFAULT_MAINTENANCE_IO_CONCURRENCY', + min => '0', + max => 'MAX_IO_CONCURRENCY', + assign_hook => 'assign_maintenance_io_concurrency', +}, + +# Dynamic shared memory has a higher overhead than local memory +# contexts, so when testing low-memory scenarios that could use shared +# memory, the recommended minimum is 1MB. +{ name => 'maintenance_work_mem', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum memory to be used for maintenance operations.', + long_desc => 'This includes operations such as VACUUM and CREATE INDEX.', + flags => 'GUC_UNIT_KB', + variable => 'maintenance_work_mem', + boot_val => '65536', + min => '64', + max => 'MAX_KILOBYTES', +}, + +{ name => 'max_active_replication_origins', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SUBSCRIBERS', + short_desc => 'Sets the maximum number of active replication origins.', + variable => 'max_active_replication_origins', + boot_val => '10', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'max_connections', type => 'int', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the maximum number of concurrent connections.', + variable => 'MaxConnections', + boot_val => '100', + min => '1', + max => 'MAX_BACKENDS', +}, + +{ name => 'max_files_per_process', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_KERNEL', + short_desc => 'Sets the maximum number of files each server process is allowed to open simultaneously.', + variable => 'max_files_per_process', + boot_val => '1000', + min => '64', + max => 'INT_MAX', +}, + +{ name => 'max_function_args', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the maximum number of function arguments.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'max_function_args', + boot_val => 'FUNC_MAX_ARGS', + min => 'FUNC_MAX_ARGS', + max => 'FUNC_MAX_ARGS', +}, + +{ name => 'max_identifier_length', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the maximum identifier length.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'max_identifier_length', + boot_val => 'NAMEDATALEN - 1', + min => 'NAMEDATALEN - 1', + max => 'NAMEDATALEN - 1', +}, + +{ name => 'max_index_keys', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the maximum number of index keys.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'max_index_keys', + boot_val => 'INDEX_MAX_KEYS', + min => 'INDEX_MAX_KEYS', + max => 'INDEX_MAX_KEYS', +}, + +# See also CheckRequiredParameterValues() if this parameter changes +{ name => 'max_locks_per_transaction', type => 'int', context => 'PGC_POSTMASTER', group => 'LOCK_MANAGEMENT', + short_desc => 'Sets the maximum number of locks per transaction.', + long_desc => 'The shared lock table is sized on the assumption that at most "max_locks_per_transaction" objects per server process or prepared transaction will need to be locked at any one time.', + variable => 'max_locks_per_xact', + boot_val => '128', + min => '10', + max => 'INT_MAX', +}, + +{ name => 'max_logical_replication_workers', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SUBSCRIBERS', + short_desc => 'Maximum number of logical replication worker processes.', + variable => 'max_logical_replication_workers', + boot_val => '4', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'max_notify_queue_pages', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_DISK', + short_desc => 'Sets the maximum number of allocated pages for NOTIFY / LISTEN queue.', + variable => 'max_notify_queue_pages', + boot_val => '1048576', + min => '64', + max => 'INT_MAX', +}, + +{ name => 'max_parallel_apply_workers_per_subscription', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SUBSCRIBERS', + short_desc => 'Maximum number of parallel apply workers per subscription.', + variable => 'max_parallel_apply_workers_per_subscription', + boot_val => '2', + min => '0', + max => 'MAX_PARALLEL_WORKER_LIMIT', +}, + +{ name => 'max_parallel_maintenance_workers', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_WORKER_PROCESSES', + short_desc => 'Sets the maximum number of parallel processes per maintenance operation.', + variable => 'max_parallel_maintenance_workers', + boot_val => '2', + min => '0', + max => 'MAX_PARALLEL_WORKER_LIMIT', +}, + +{ name => 'max_parallel_workers', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_WORKER_PROCESSES', + short_desc => 'Sets the maximum number of parallel workers that can be active at one time.', + flags => 'GUC_EXPLAIN', + variable => 'max_parallel_workers', + boot_val => '8', + min => '0', + max => 'MAX_PARALLEL_WORKER_LIMIT', +}, + +{ name => 'max_parallel_workers_per_gather', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_WORKER_PROCESSES', + short_desc => 'Sets the maximum number of parallel processes per executor node.', + flags => 'GUC_EXPLAIN', + variable => 'max_parallel_workers_per_gather', + boot_val => '2', + min => '0', + max => 'MAX_PARALLEL_WORKER_LIMIT', +}, + +{ name => 'max_pred_locks_per_page', type => 'int', context => 'PGC_SIGHUP', group => 'LOCK_MANAGEMENT', + short_desc => 'Sets the maximum number of predicate-locked tuples per page.', + long_desc => 'If more than this number of tuples on the same page are locked by a connection, those locks are replaced by a page-level lock.', + variable => 'max_predicate_locks_per_page', + boot_val => '2', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'max_pred_locks_per_relation', type => 'int', context => 'PGC_SIGHUP', group => 'LOCK_MANAGEMENT', + short_desc => 'Sets the maximum number of predicate-locked pages and tuples per relation.', + long_desc => 'If more than this total of pages and tuples in the same relation are locked by a connection, those locks are replaced by a relation-level lock.', + variable => 'max_predicate_locks_per_relation', + boot_val => '-2', + min => 'INT_MIN', + max => 'INT_MAX', +}, + +{ name => 'max_pred_locks_per_transaction', type => 'int', context => 'PGC_POSTMASTER', group => 'LOCK_MANAGEMENT', + short_desc => 'Sets the maximum number of predicate locks per transaction.', + long_desc => 'The shared predicate lock table is sized on the assumption that at most "max_pred_locks_per_transaction" objects per server process or prepared transaction will need to be locked at any one time.', + variable => 'max_predicate_locks_per_xact', + boot_val => '64', + min => '10', + max => 'INT_MAX', +}, + +# See also CheckRequiredParameterValues() if this parameter changes +{ name => 'max_prepared_transactions', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum number of simultaneously prepared transactions.', + variable => 'max_prepared_xacts', + boot_val => '0', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'max_repack_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum number of replication slots for use by REPACK.', + variable => 'max_repack_replication_slots', + boot_val => '5', + min => '0', + max => 'MAX_BACKENDS', +}, + +/* see max_wal_senders */ +{ name => 'max_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum number of simultaneously defined replication slots.', + variable => 'max_replication_slots', + boot_val => '10', + min => '0', + max => 'MAX_BACKENDS /* XXX? */', +}, + +{ name => 'max_slot_wal_keep_size', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum WAL size that can be reserved by replication slots.', + long_desc => 'Replication slots will be marked as failed, and segments released for deletion or recycling, if this much space is occupied by WAL on disk. -1 means no maximum.', + flags => 'GUC_UNIT_MB', + variable => 'max_slot_wal_keep_size_mb', + boot_val => '-1', + min => '-1', + max => 'MAX_KILOBYTES', +}, + +# We use the hopefully-safely-small value of 100kB as the compiled-in +# default for max_stack_depth. InitializeGUCOptions will increase it +# if possible, depending on the actual platform-specific stack limit. +{ name => 'max_stack_depth', type => 'int', context => 'PGC_SUSET', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum stack depth, in kilobytes.', + flags => 'GUC_UNIT_KB', + variable => 'max_stack_depth', + boot_val => '100', + min => '100', + max => 'MAX_KILOBYTES', + check_hook => 'check_max_stack_depth', + assign_hook => 'assign_max_stack_depth', +}, + +{ name => 'max_standby_archive_delay', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the maximum delay before canceling queries when a hot standby server is processing archived WAL data.', + long_desc => '-1 means wait forever.', + flags => 'GUC_UNIT_MS', + variable => 'max_standby_archive_delay', + boot_val => '30 * 1000', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'max_standby_streaming_delay', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the maximum delay before canceling queries when a hot standby server is processing streamed WAL data.', + long_desc => '-1 means wait forever.', + flags => 'GUC_UNIT_MS', + variable => 'max_standby_streaming_delay', + boot_val => '30 * 1000', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'max_sync_workers_per_subscription', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SUBSCRIBERS', + short_desc => 'Maximum number of workers per subscription for synchronizing tables and sequences.', + variable => 'max_sync_workers_per_subscription', + boot_val => '2', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'max_wal_senders', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum number of simultaneously running WAL sender processes.', + variable => 'max_wal_senders', + boot_val => '10', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'max_wal_size', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_CHECKPOINTS', + short_desc => 'Sets the WAL size that triggers a checkpoint.', + flags => 'GUC_UNIT_MB', + variable => 'max_wal_size_mb', + boot_val => 'DEFAULT_MAX_WAL_SEGS * (DEFAULT_XLOG_SEG_SIZE / (1024 * 1024))', + min => '2', + max => 'MAX_KILOBYTES', + assign_hook => 'assign_max_wal_size', +}, + +{ name => 'max_worker_processes', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_WORKER_PROCESSES', + short_desc => 'Maximum number of concurrent worker processes.', + variable => 'max_worker_processes', + boot_val => '8', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'md5_password_warnings', type => 'bool', context => 'PGC_USERSET', group => 'CONN_AUTH_AUTH', + short_desc => 'Enables deprecation warnings for MD5 passwords.', + variable => 'md5_password_warnings', + boot_val => 'true', +}, + +{ name => 'min_dynamic_shared_memory', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Amount of dynamic shared memory reserved at startup.', + flags => 'GUC_UNIT_MB', + variable => 'min_dynamic_shared_memory', + boot_val => '0', + min => '0', + max => '(int) Min((size_t) INT_MAX, SIZE_MAX / (1024 * 1024))', +}, + +{ name => 'min_eager_agg_group_size', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the minimum average group size required to consider applying eager aggregation.', + flags => 'GUC_EXPLAIN', + variable => 'min_eager_agg_group_size', + boot_val => '8.0', + min => '0.0', + max => 'DBL_MAX', +}, + +{ name => 'min_parallel_index_scan_size', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the minimum amount of index data for a parallel scan.', + long_desc => 'If the planner estimates that it will read a number of index pages too small to reach this limit, a parallel scan will not be considered.', + flags => 'GUC_UNIT_BLOCKS | GUC_EXPLAIN', + variable => 'min_parallel_index_scan_size', + boot_val => '(512 * 1024) / BLCKSZ', + min => '0', + max => 'INT_MAX / 3', +}, + +{ name => 'min_parallel_table_scan_size', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the minimum amount of table data for a parallel scan.', + long_desc => 'If the planner estimates that it will read a number of table pages too small to reach this limit, a parallel scan will not be considered.', + flags => 'GUC_UNIT_BLOCKS | GUC_EXPLAIN', + variable => 'min_parallel_table_scan_size', + boot_val => '(8 * 1024 * 1024) / BLCKSZ', + min => '0', + max => 'INT_MAX / 3', +}, + +{ name => 'min_wal_size', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_CHECKPOINTS', + short_desc => 'Sets the minimum size to shrink the WAL to.', + flags => 'GUC_UNIT_MB', + variable => 'min_wal_size_mb', + boot_val => 'DEFAULT_MIN_WAL_SEGS * (DEFAULT_XLOG_SEG_SIZE / (1024 * 1024))', + min => '2', + max => 'MAX_KILOBYTES', +}, + +{ name => 'multixact_member_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the MultiXact member cache.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'multixact_member_buffers', + boot_val => '32', + min => '16', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_multixact_member_buffers', +}, + +{ name => 'multixact_offset_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the MultiXact offset cache.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'multixact_offset_buffers', + boot_val => '16', + min => '16', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_multixact_offset_buffers', +}, + +{ name => 'notify_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the LISTEN/NOTIFY message cache.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'notify_buffers', + boot_val => '16', + min => '16', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_notify_buffers', +}, + +{ name => 'num_os_semaphores', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the number of semaphores required for the server.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED', + variable => 'num_os_semaphores', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'oauth_validator_libraries', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', + short_desc => 'Lists libraries that may be called to validate OAuth v2 bearer tokens.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', + variable => 'oauth_validator_libraries_string', + boot_val => '""', +}, + +# this is undocumented because not exposed in a standard build +{ name => 'optimize_bounded_sort', type => 'bool', context => 'PGC_USERSET', group => 'QUERY_TUNING_METHOD', + short_desc => 'Enables bounded sorting using heap sort.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_EXPLAIN', + variable => 'optimize_bounded_sort', + boot_val => 'true', + ifdef => 'DEBUG_BOUNDED_SORT', +}, + +{ name => 'parallel_leader_participation', type => 'bool', context => 'PGC_USERSET', group => 'RESOURCES_WORKER_PROCESSES', + short_desc => 'Controls whether Gather and Gather Merge also run subplans.', + long_desc => 'Should gather nodes also run subplans or just gather tuples?', + flags => 'GUC_EXPLAIN', + variable => 'parallel_leader_participation', + boot_val => 'true', +}, + +{ name => 'parallel_setup_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of starting up worker processes for parallel query.', + flags => 'GUC_EXPLAIN', + variable => 'parallel_setup_cost', + boot_val => 'DEFAULT_PARALLEL_SETUP_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'parallel_tuple_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of passing each tuple (row) from worker to leader backend.', + flags => 'GUC_EXPLAIN', + variable => 'parallel_tuple_cost', + boot_val => 'DEFAULT_PARALLEL_TUPLE_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'password_encryption', type => 'enum', context => 'PGC_USERSET', group => 'CONN_AUTH_AUTH', + short_desc => 'Chooses the algorithm for encrypting passwords.', + variable => 'Password_encryption', + boot_val => 'PASSWORD_TYPE_SCRAM_SHA_256', + options => 'password_encryption_options', +}, + +{ name => 'password_expiration_warning_threshold', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', + short_desc => 'Threshold for password expiration warnings.', + long_desc => '0 means not to emit these warnings.', + flags => 'GUC_UNIT_S', + variable => 'password_expiration_warning_threshold', + boot_val => '604800', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Controls the planner\'s selection of custom or generic plan.', + long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better. This can be set to override the default behavior.', + flags => 'GUC_EXPLAIN', + variable => 'plan_cache_mode', + boot_val => 'PLAN_CACHE_MODE_AUTO', + options => 'plan_cache_mode_options', +}, + +{ name => 'port', type => 'int', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the TCP port the server listens on.', + variable => 'PostPortNumber', + boot_val => 'DEF_PGPORT', + min => '1', + max => '65535', +}, + +{ name => 'post_auth_delay', type => 'int', context => 'PGC_BACKEND', group => 'DEVELOPER_OPTIONS', + short_desc => 'Sets the amount of time to wait after authentication on connection startup.', + long_desc => 'This allows attaching a debugger to the process.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_UNIT_S', + variable => 'PostAuthDelay', + boot_val => '0', + min => '0', + max => 'INT_MAX / 1000000', +}, + +# Not for general use +{ name => 'pre_auth_delay', type => 'int', context => 'PGC_SIGHUP', group => 'DEVELOPER_OPTIONS', + short_desc => 'Sets the amount of time to wait before authentication on connection startup.', + long_desc => 'This allows attaching a debugger to the process.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_UNIT_S', + variable => 'PreAuthDelay', + boot_val => '0', + min => '0', + max => '60', +}, + +{ name => 'primary_conninfo', type => 'string', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the connection string to be used to connect to the sending server.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'PrimaryConnInfo', + boot_val => '""', +}, + +{ name => 'primary_slot_name', type => 'string', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the name of the replication slot to use on the sending server.', + variable => 'PrimarySlotName', + boot_val => '""', + check_hook => 'check_primary_slot_name', +}, + +{ name => 'quote_all_identifiers', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'When generating SQL fragments, quote all identifiers.', + variable => 'quote_all_identifiers', + boot_val => 'false', +}, + +{ name => 'random_page_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of a nonsequentially fetched disk page.', + flags => 'GUC_EXPLAIN', + variable => 'random_page_cost', + boot_val => 'DEFAULT_RANDOM_PAGE_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'recovery_end_command', type => 'string', context => 'PGC_SIGHUP', group => 'WAL_ARCHIVE_RECOVERY', + short_desc => 'Sets the shell command that will be executed once at the end of recovery.', + variable => 'recoveryEndCommand', + boot_val => '""', +}, + +{ name => 'recovery_init_sync_method', type => 'enum', context => 'PGC_SIGHUP', group => 'ERROR_HANDLING_OPTIONS', + short_desc => 'Sets the method for synchronizing the data directory before crash recovery.', + variable => 'recovery_init_sync_method', + boot_val => 'DATA_DIR_SYNC_METHOD_FSYNC', + options => 'recovery_init_sync_method_options', +}, + +{ name => 'recovery_min_apply_delay', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the minimum delay for applying changes during recovery.', + flags => 'GUC_UNIT_MS', + variable => 'recovery_min_apply_delay', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'recovery_prefetch', type => 'enum', context => 'PGC_SIGHUP', group => 'WAL_RECOVERY', + short_desc => 'Prefetch referenced blocks during recovery.', + long_desc => 'Look ahead in the WAL to find references to uncached data.', + variable => 'recovery_prefetch', + boot_val => 'RECOVERY_PREFETCH_TRY', + options => 'recovery_prefetch_options', + check_hook => 'check_recovery_prefetch', + assign_hook => 'assign_recovery_prefetch', +}, + +{ name => 'recovery_target', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Set to "immediate" to end recovery as soon as a consistent state is reached.', + variable => 'recovery_target_string', + boot_val => '""', + check_hook => 'check_recovery_target', + assign_hook => 'assign_recovery_target', +}, + +{ name => 'recovery_target_action', type => 'enum', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Sets the action to perform upon reaching the recovery target.', + variable => 'recoveryTargetAction', + boot_val => 'RECOVERY_TARGET_ACTION_PAUSE', + options => 'recovery_target_action_options', +}, + +{ name => 'recovery_target_inclusive', type => 'bool', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Sets whether to include or exclude transaction with recovery target.', + variable => 'recoveryTargetInclusive', + boot_val => 'true', +}, + +{ name => 'recovery_target_lsn', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Sets the LSN of the write-ahead log location up to which recovery will proceed.', + variable => 'recovery_target_lsn_string', + boot_val => '""', + check_hook => 'check_recovery_target_lsn', + assign_hook => 'assign_recovery_target_lsn', +}, + +{ name => 'recovery_target_name', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Sets the named restore point up to which recovery will proceed.', + variable => 'recovery_target_name_string', + boot_val => '""', + check_hook => 'check_recovery_target_name', + assign_hook => 'assign_recovery_target_name', +}, + +{ name => 'recovery_target_time', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Sets the time stamp up to which recovery will proceed.', + variable => 'recovery_target_time_string', + boot_val => '""', + check_hook => 'check_recovery_target_time', + assign_hook => 'assign_recovery_target_time', +}, + +{ name => 'recovery_target_timeline', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Specifies the timeline to recover into.', + variable => 'recovery_target_timeline_string', + boot_val => '"latest"', + check_hook => 'check_recovery_target_timeline', + assign_hook => 'assign_recovery_target_timeline', +}, + +{ name => 'recovery_target_xid', type => 'string', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY_TARGET', + short_desc => 'Sets the transaction ID up to which recovery will proceed.', + variable => 'recovery_target_xid_string', + boot_val => '""', + check_hook => 'check_recovery_target_xid', + assign_hook => 'assign_recovery_target_xid', +}, + +{ name => 'recursive_worktable_factor', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER', + short_desc => 'Sets the planner\'s estimate of the average size of a recursive query\'s working table.', + flags => 'GUC_EXPLAIN', + variable => 'recursive_worktable_factor', + boot_val => 'DEFAULT_RECURSIVE_WORKTABLE_FACTOR', + min => '0.001', + max => '1000000.0', +}, + +{ name => 'remove_temp_files_after_crash', type => 'bool', context => 'PGC_SIGHUP', group => 'DEVELOPER_OPTIONS', + short_desc => 'Remove temporary files after backend crash.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'remove_temp_files_after_crash', + boot_val => 'true', +}, + +{ name => 'reserved_connections', type => 'int', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the number of connection slots reserved for roles with privileges of pg_use_reserved_connections.', + variable => 'ReservedConnections', + boot_val => '0', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'restart_after_crash', type => 'bool', context => 'PGC_SIGHUP', group => 'ERROR_HANDLING_OPTIONS', + short_desc => 'Reinitialize server after backend crash.', + variable => 'restart_after_crash', + boot_val => 'true', +}, + +{ name => 'restore_command', type => 'string', context => 'PGC_SIGHUP', group => 'WAL_ARCHIVE_RECOVERY', + short_desc => 'Sets the shell command that will be called to retrieve an archived WAL file.', + variable => 'recoveryRestoreCommand', + boot_val => '""', +}, + +{ name => 'restrict_nonsystem_relation_kind', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Prohibits access to non-system relations of specified kinds.', + flags => 'GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE', + variable => 'restrict_nonsystem_relation_kind_string', + boot_val => '""', + check_hook => 'check_restrict_nonsystem_relation_kind', + assign_hook => 'assign_restrict_nonsystem_relation_kind', +}, + +# Not for general use --- used by SET ROLE +{ name => 'role', type => 'string', context => 'PGC_USERSET', group => 'UNGROUPED', + short_desc => 'Sets the current role.', + flags => 'GUC_IS_NAME | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NOT_WHILE_SEC_REST', + variable => 'role_string', + boot_val => '"none"', + check_hook => 'check_role', + assign_hook => 'assign_role', + show_hook => 'show_role', +}, + +{ name => 'row_security', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Enables row security.', + long_desc => 'When enabled, row security will be applied to all users.', + variable => 'row_security', + boot_val => 'true', +}, + +{ name => 'scram_iterations', type => 'int', context => 'PGC_USERSET', group => 'CONN_AUTH_AUTH', + short_desc => 'Sets the iteration count for SCRAM secret generation.', + flags => 'GUC_REPORT', + variable => 'scram_sha_256_iterations', + boot_val => 'SCRAM_SHA_256_DEFAULT_ITERATIONS', + min => '1', + max => 'INT_MAX', +}, + +{ name => 'search_path', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the schema search order for names that are not schema-qualified.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_EXPLAIN | GUC_REPORT', + variable => 'namespace_search_path', + boot_val => '"\"$user\", public"', + check_hook => 'check_search_path', + assign_hook => 'assign_search_path', +}, + +{ name => 'seed', type => 'real', context => 'PGC_USERSET', group => 'UNGROUPED', + short_desc => 'Sets the seed for random-number generation.', + flags => 'GUC_NO_SHOW_ALL | GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'phony_random_seed', + boot_val => '0.0', + min => '-1.0', + max => '1.0', + check_hook => 'check_random_seed', + assign_hook => 'assign_random_seed', + show_hook => 'show_random_seed', +}, + +{ name => 'segment_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the number of pages per disk file.', + flags => 'GUC_UNIT_BLOCKS | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'segment_size', + boot_val => 'RELSEG_SIZE', + min => 'RELSEG_SIZE', + max => 'RELSEG_SIZE', +}, + +{ name => 'send_abort_for_crash', type => 'bool', context => 'PGC_SIGHUP', group => 'DEVELOPER_OPTIONS', + short_desc => 'Send SIGABRT not SIGQUIT to child processes after backend crash.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'send_abort_for_crash', + boot_val => 'false', +}, + +{ name => 'send_abort_for_kill', type => 'bool', context => 'PGC_SIGHUP', group => 'DEVELOPER_OPTIONS', + short_desc => 'Send SIGABRT not SIGKILL to stuck child processes.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'send_abort_for_kill', + boot_val => 'false', +}, + + +{ name => 'seq_page_cost', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s estimate of the cost of a sequentially fetched disk page.', + flags => 'GUC_EXPLAIN', + variable => 'seq_page_cost', + boot_val => 'DEFAULT_SEQ_PAGE_COST', + min => '0', + max => 'DBL_MAX', +}, + +{ name => 'serializable_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the serializable transaction cache.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'serializable_buffers', + boot_val => '32', + min => '16', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_serial_buffers', +}, + +# Can't be set in postgresql.conf +{ name => 'server_encoding', type => 'string', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the server (database) character set encoding.', + flags => 'GUC_IS_NAME | GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'server_encoding_string', + boot_val => '"SQL_ASCII"', +}, + +# Can't be set in postgresql.conf +{ name => 'server_version', type => 'string', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the server version.', + flags => 'GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'server_version_string', + boot_val => 'PG_VERSION', +}, + +# Can't be set in postgresql.conf +{ name => 'server_version_num', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the server version as an integer.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'server_version_num', + boot_val => 'PG_VERSION_NUM', + min => 'PG_VERSION_NUM', + max => 'PG_VERSION_NUM', +}, + +# Not for general use --- used by SET SESSION AUTHORIZATION +{ name => 'session_authorization', type => 'string', context => 'PGC_USERSET', group => 'UNGROUPED', + short_desc => 'Sets the session user name.', + flags => 'GUC_IS_NAME | GUC_REPORT | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NOT_WHILE_SEC_REST', + variable => 'session_authorization_string', + boot_val => 'NULL', + check_hook => 'check_session_authorization', + assign_hook => 'assign_session_authorization', +}, + +{ name => 'session_preload_libraries', type => 'string', context => 'PGC_SUSET', group => 'CLIENT_CONN_PRELOAD', + short_desc => 'Lists shared libraries to preload into each backend.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', + variable => 'session_preload_libraries_string', + boot_val => '""', +}, + +{ name => 'session_replication_role', type => 'enum', context => 'PGC_SUSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the session\'s behavior for triggers and rewrite rules.', + variable => 'SessionReplicationRole', + boot_val => 'SESSION_REPLICATION_ROLE_ORIGIN', + options => 'session_replication_role_options', + assign_hook => 'assign_session_replication_role', +}, + +# We sometimes multiply the number of shared buffers by two without +# checking for overflow, so we mustn't allow more than INT_MAX / 2. +{ name => 'shared_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the number of shared memory buffers used by the server.', + flags => 'GUC_UNIT_BLOCKS', + variable => 'NBuffers', + boot_val => '16384', + min => '16', + max => 'INT_MAX / 2', +}, + +{ name => 'shared_memory_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the size of the server\'s main shared memory area (rounded up to the nearest MB).', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_UNIT_MB | GUC_RUNTIME_COMPUTED', + variable => 'shared_memory_size_mb', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'shared_memory_size_in_huge_pages', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the number of huge pages needed for the main shared memory area.', + long_desc => '-1 means huge pages are not supported.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED', + variable => 'shared_memory_size_in_huge_pages', + boot_val => '-1', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'shared_memory_type', type => 'enum', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Selects the shared memory implementation used for the main shared memory region.', + variable => 'shared_memory_type', + boot_val => 'DEFAULT_SHARED_MEMORY_TYPE', + options => 'shared_memory_options', +}, + +{ name => 'shared_preload_libraries', type => 'string', context => 'PGC_POSTMASTER', group => 'CLIENT_CONN_PRELOAD', + short_desc => 'Lists shared libraries to preload into server.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', + variable => 'shared_preload_libraries_string', + boot_val => '""', +}, + +{ name => 'ssl', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Enables SSL connections.', + variable => 'EnableSSL', + boot_val => 'false', + check_hook => 'check_ssl', +}, + +{ name => 'ssl_ca_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Location of the SSL certificate authority file.', + variable => 'ssl_ca_file', + boot_val => '""', +}, + +{ name => 'ssl_cert_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Location of the SSL server certificate file.', + variable => 'ssl_cert_file', + boot_val => '"server.crt"', +}, + +{ name => 'ssl_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Sets the list of allowed TLSv1.2 (and lower) ciphers.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'SSLCipherList', + boot_val => 'DEFAULT_SSL_CIPHERS', +}, + +{ name => 'ssl_crl_dir', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Location of the SSL certificate revocation list directory.', + variable => 'ssl_crl_dir', + boot_val => '""', +}, + +{ name => 'ssl_crl_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Location of the SSL certificate revocation list file.', + variable => 'ssl_crl_file', + boot_val => '""', +}, + +{ name => 'ssl_dh_params_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Location of the SSL DH parameters file.', + long_desc => 'An empty string means use compiled-in default parameters.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'ssl_dh_params_file', + boot_val => '""', +}, + +{ name => 'ssl_groups', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Sets the group(s) to use for Diffie-Hellman key exchange.', + long_desc => 'Multiple groups can be specified using a colon-separated list.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'SSLECDHCurve', + boot_val => 'DEFAULT_SSL_GROUPS', +}, + +{ name => 'ssl_key_file', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Location of the SSL server private key file.', + variable => 'ssl_key_file', + boot_val => '"server.key"', +}, + +{ name => 'ssl_library', type => 'string', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the name of the SSL library.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'ssl_library', + boot_val => 'SSL_LIBRARY', +}, + +{ name => 'ssl_max_protocol_version', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Sets the maximum SSL/TLS protocol version to use.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'ssl_max_protocol_version', + boot_val => 'PG_TLS_ANY', + options => 'ssl_protocol_versions_info', +}, + +{ name => 'ssl_min_protocol_version', type => 'enum', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Sets the minimum SSL/TLS protocol version to use.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'ssl_min_protocol_version', + boot_val => 'PG_TLS1_2_VERSION', + options => 'ssl_protocol_versions_info + 1', # don't allow PG_TLS_ANY +}, + +{ name => 'ssl_passphrase_command', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Command to obtain passphrases for SSL.', + long_desc => 'An empty string means use the built-in prompting mechanism.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'ssl_passphrase_command', + boot_val => '""', +}, + +{ name => 'ssl_passphrase_command_supports_reload', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Controls whether "ssl_passphrase_command" is called during server reload.', + variable => 'ssl_passphrase_command_supports_reload', + boot_val => 'false', +}, + +{ name => 'ssl_prefer_server_ciphers', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Give priority to server ciphersuite order.', + variable => 'SSLPreferServerCiphers', + boot_val => 'true', +}, + +{ name => 'ssl_renegotiation_limit', type => 'int', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'SSL renegotiation is no longer supported; this can only be 0.', + flags => 'GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'ssl_renegotiation_limit', + boot_val => '0', + min => '0', + max => '0', +}, + +{ name => 'ssl_sni', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Sets whether to interpret SNI extensions in SSL connections.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'ssl_sni', + boot_val => 'false', + check_hook => 'check_ssl_sni', +}, + +{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL', + short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.', + long_desc => 'An empty string means use the default cipher suites.', + flags => 'GUC_SUPERUSER_ONLY', + variable => 'SSLCipherSuites', + boot_val => '""', +}, + +# We removed support for non-conforming string literals in PostgreSQL 19, +# but this parameter must be kept indefinitely, since client code might +# consult it or attempt to set it. We allow it to be explicitly set to true. +{ name => 'standard_conforming_strings', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'Nonstandard strings are no longer supported; this can only be true.', + flags => 'GUC_REPORT | GUC_NOT_IN_SAMPLE', + variable => 'standard_conforming_strings', + boot_val => 'true', + check_hook => 'check_standard_conforming_strings', +}, + +{ name => 'statement_timeout', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the maximum allowed duration of any statement.', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_MS', + variable => 'StatementTimeout', + boot_val => '0', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'stats_fetch_consistency', type => 'enum', context => 'PGC_USERSET', group => 'STATS_CUMULATIVE', + short_desc => 'Sets the consistency of accesses to statistics data.', + variable => 'pgstat_fetch_consistency', + boot_val => 'PGSTAT_FETCH_CONSISTENCY_CACHE', + options => 'stats_fetch_consistency', + assign_hook => 'assign_stats_fetch_consistency', +}, + +{ name => 'subtransaction_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the subtransaction cache.', + long_desc => '0 means use a fraction of "shared_buffers".', + flags => 'GUC_UNIT_BLOCKS', + variable => 'subtransaction_buffers', + boot_val => '0', + min => '0', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_subtrans_buffers', +}, + +{ name => 'summarize_wal', type => 'bool', context => 'PGC_SIGHUP', group => 'WAL_SUMMARIZATION', + short_desc => 'Starts the WAL summarizer process to enable incremental backup.', + variable => 'summarize_wal', + boot_val => 'false', +}, + +# see max_connections +{ name => 'superuser_reserved_connections', type => 'int', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the number of connection slots reserved for superusers.', + variable => 'SuperuserReservedConnections', + boot_val => '3', + min => '0', + max => 'MAX_BACKENDS', +}, + +{ name => 'sync_replication_slots', type => 'bool', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Enables a physical standby to synchronize logical failover replication slots from the primary server.', + variable => 'sync_replication_slots', + boot_val => 'false', +}, + +{ name => 'synchronize_seqscans', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_PREVIOUS', + short_desc => 'Enables synchronized sequential scans.', + variable => 'synchronize_seqscans', + boot_val => 'true', +}, + +{ name => 'synchronized_standby_slots', type => 'string', context => 'PGC_SIGHUP', group => 'REPLICATION_PRIMARY', + short_desc => 'Lists streaming replication standby server replication slot names that logical WAL sender processes will wait for.', + long_desc => 'Logical WAL sender processes will send decoded changes to output plugins only after the specified replication slots have confirmed receiving WAL.', + flags => 'GUC_LIST_INPUT', + variable => 'synchronized_standby_slots', + boot_val => '""', + check_hook => 'check_synchronized_standby_slots', + assign_hook => 'assign_synchronized_standby_slots', +}, + +{ name => 'synchronous_commit', type => 'enum', context => 'PGC_USERSET', group => 'WAL_SETTINGS', + short_desc => 'Sets the current transaction\'s synchronization level.', + variable => 'synchronous_commit', + boot_val => 'SYNCHRONOUS_COMMIT_ON', + options => 'synchronous_commit_options', + assign_hook => 'assign_synchronous_commit', +}, + +{ name => 'synchronous_standby_names', type => 'string', context => 'PGC_SIGHUP', group => 'REPLICATION_PRIMARY', + short_desc => 'Number of synchronous standbys and list of names of potential synchronous ones.', + flags => 'GUC_LIST_INPUT', + variable => 'SyncRepStandbyNames', + boot_val => '""', + check_hook => 'check_synchronous_standby_names', + assign_hook => 'assign_synchronous_standby_names', +}, + +{ name => 'syslog_facility', type => 'enum', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the syslog "facility" to be used when syslog enabled.', + variable => 'syslog_facility', + boot_val => 'DEFAULT_SYSLOG_FACILITY', + options => 'syslog_facility_options', + assign_hook => 'assign_syslog_facility', +}, + +{ name => 'syslog_ident', type => 'string', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Sets the program name used to identify PostgreSQL messages in syslog.', + variable => 'syslog_ident_str', + boot_val => '"postgres"', + assign_hook => 'assign_syslog_ident', +}, + +{ name => 'syslog_sequence_numbers', type => 'bool', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Add sequence number to syslog messages to avoid duplicate suppression.', + variable => 'syslog_sequence_numbers', + boot_val => 'true', +}, + +{ name => 'syslog_split_messages', type => 'bool', context => 'PGC_SIGHUP', group => 'LOGGING_WHERE', + short_desc => 'Split messages sent to syslog by lines and to fit into 1024 bytes.', + variable => 'syslog_split_messages', + boot_val => 'true', +}, + +{ name => 'tcp_keepalives_count', type => 'int', context => 'PGC_USERSET', group => 'CONN_AUTH_TCP', + short_desc => 'Maximum number of TCP keepalive retransmits.', + long_desc => 'Number of consecutive keepalive retransmits that can be lost before a connection is considered dead. 0 means use the system default.', + variable => 'tcp_keepalives_count', + boot_val => '0', + min => '0', + max => 'INT_MAX', + assign_hook => 'assign_tcp_keepalives_count', + show_hook => 'show_tcp_keepalives_count', +}, + +{ name => 'tcp_keepalives_idle', type => 'int', context => 'PGC_USERSET', group => 'CONN_AUTH_TCP', + short_desc => 'Time between issuing TCP keepalives.', + long_desc => '0 means use the system default.', + flags => 'GUC_UNIT_S', + variable => 'tcp_keepalives_idle', + boot_val => '0', + min => '0', + max => 'INT_MAX', + assign_hook => 'assign_tcp_keepalives_idle', + show_hook => 'show_tcp_keepalives_idle', +}, + +{ name => 'tcp_keepalives_interval', type => 'int', context => 'PGC_USERSET', group => 'CONN_AUTH_TCP', + short_desc => 'Time between TCP keepalive retransmits.', + long_desc => '0 means use the system default.', + flags => 'GUC_UNIT_S', + variable => 'tcp_keepalives_interval', + boot_val => '0', + min => '0', + max => 'INT_MAX', + assign_hook => 'assign_tcp_keepalives_interval', + show_hook => 'show_tcp_keepalives_interval', +}, + +{ name => 'tcp_user_timeout', type => 'int', context => 'PGC_USERSET', group => 'CONN_AUTH_TCP', + short_desc => 'TCP user timeout.', + long_desc => '0 means use the system default.', + flags => 'GUC_UNIT_MS', + variable => 'tcp_user_timeout', + boot_val => '0', + min => '0', + max => 'INT_MAX', + assign_hook => 'assign_tcp_user_timeout', + show_hook => 'show_tcp_user_timeout', +}, + +{ name => 'temp_buffers', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum number of temporary buffers used by each session.', + flags => 'GUC_UNIT_BLOCKS | GUC_EXPLAIN', + variable => 'num_temp_buffers', + boot_val => '1024', + min => '100', + max => 'INT_MAX / 2', + check_hook => 'check_temp_buffers', +}, + +{ name => 'temp_file_limit', type => 'int', context => 'PGC_SUSET', group => 'RESOURCES_DISK', + short_desc => 'Limits the total size of all temporary files used by each process.', + long_desc => '-1 means no limit.', + flags => 'GUC_UNIT_KB', + variable => 'temp_file_limit', + boot_val => '-1', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'temp_tablespaces', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the tablespace(s) to use for temporary tables and sort files.', + long_desc => 'An empty string means use the database\'s default tablespace.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE', + variable => 'temp_tablespaces', + boot_val => '""', + check_hook => 'check_temp_tablespaces', + assign_hook => 'assign_temp_tablespaces', +}, + +{ name => 'TimeZone', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Sets the time zone for displaying and interpreting time stamps.', + flags => 'GUC_REPORT', + variable => 'timezone_string', + boot_val => '"GMT"', + check_hook => 'check_timezone', + assign_hook => 'assign_timezone', + show_hook => 'show_timezone', +}, + +{ name => 'timezone_abbreviations', type => 'string', context => 'PGC_USERSET', group => 'CLIENT_CONN_LOCALE', + short_desc => 'Selects a file of time zone abbreviations.', + variable => 'timezone_abbreviations_string', + boot_val => 'NULL', + check_hook => 'check_timezone_abbreviations', + assign_hook => 'assign_timezone_abbreviations', +}, + +{ name => 'timing_clock_source', type => 'enum', context => 'PGC_SUSET', group => 'RESOURCES_TIME', + short_desc => 'Controls the clock source used for collecting timing measurements.', + long_desc => 'This enables the use of specialized clock sources, specifically the RDTSC clock source on x86-64 systems (if available), to support timing measurements with lower overhead during EXPLAIN and other instrumentation.', + variable => 'timing_clock_source', + boot_val => 'TIMING_CLOCK_SOURCE_AUTO', + options => 'timing_clock_source_options', + check_hook => 'check_timing_clock_source', + assign_hook => 'assign_timing_clock_source', + show_hook => 'show_timing_clock_source', +}, + +{ name => 'trace_connection_negotiation', type => 'bool', context => 'PGC_POSTMASTER', group => 'DEVELOPER_OPTIONS', + short_desc => 'Logs details of pre-authentication connection handshake.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_connection_negotiation', + boot_val => 'false', +}, + +{ name => 'trace_lock_oidmin', type => 'int', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Sets the minimum OID of tables for tracking locks.', + long_desc => 'Is used to avoid output on system tables.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_lock_oidmin', + boot_val => 'FirstNormalObjectId', + min => '0', + max => 'INT_MAX', + ifdef => 'LOCK_DEBUG', +}, + +{ name => 'trace_lock_table', type => 'int', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Sets the OID of the table with unconditionally lock tracing.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_lock_table', + boot_val => '0', + min => '0', + max => 'INT_MAX', + ifdef => 'LOCK_DEBUG', +}, + +{ name => 'trace_locks', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Emits information about lock usage.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_locks', + boot_val => 'false', + ifdef => 'LOCK_DEBUG', +}, + +{ name => 'trace_lwlocks', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Emits information about lightweight lock usage.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_lwlocks', + boot_val => 'false', + ifdef => 'LOCK_DEBUG', +}, + +{ name => 'trace_notify', type => 'bool', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Generates debugging output for LISTEN and NOTIFY.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_notify', + boot_val => 'false', +}, + +{ name => 'trace_sort', type => 'bool', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Emit information about resource usage in sorting.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'trace_sort', + boot_val => 'false', +}, + +# this is undocumented because not exposed in a standard build +{ name => 'trace_syncscan', type => 'bool', context => 'PGC_USERSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Generate debugging output for synchronized scanning.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'trace_syncscan', + boot_val => 'false', + ifdef => 'TRACE_SYNCSCAN', +}, + +{ name => 'trace_userlocks', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Emits information about user lock usage.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'Trace_userlocks', + boot_val => 'false', + ifdef => 'LOCK_DEBUG', +}, + +{ name => 'track_activities', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects information about executing commands.', + long_desc => 'Enables the collection of information on the currently executing command of each session, along with the time at which that command began execution.', + variable => 'pgstat_track_activities', + boot_val => 'true', +}, + +{ name => 'track_activity_query_size', type => 'int', context => 'PGC_POSTMASTER', group => 'STATS_CUMULATIVE', + short_desc => 'Sets the size reserved for pg_stat_activity.query, in bytes.', + flags => 'GUC_UNIT_BYTE', + variable => 'pgstat_track_activity_query_size', + boot_val => '1024', + min => '100', + max => '1048576', +}, + +{ name => 'track_commit_timestamp', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Collects transaction commit time.', + variable => 'track_commit_timestamp', + boot_val => 'false', +}, + +{ name => 'track_cost_delay_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects timing statistics for cost-based vacuum delay.', + variable => 'track_cost_delay_timing', + boot_val => 'false', +}, + +{ name => 'track_counts', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects statistics on database activity.', + variable => 'pgstat_track_counts', + boot_val => 'true', +}, + +{ name => 'track_functions', type => 'enum', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects function-level statistics on database activity.', + variable => 'pgstat_track_functions', + boot_val => 'TRACK_FUNC_OFF', + options => 'track_function_options', +}, + +{ name => 'track_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects timing statistics for database I/O activity.', + variable => 'track_io_timing', + boot_val => 'false', +}, + +{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE', + short_desc => 'Collects timing statistics for WAL I/O activity.', + variable => 'track_wal_io_timing', + boot_val => 'false', +}, + +{ name => 'transaction_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM', + short_desc => 'Sets the size of the dedicated buffer pool used for the transaction status cache.', + long_desc => '0 means use a fraction of "shared_buffers".', + flags => 'GUC_UNIT_BLOCKS', + variable => 'transaction_buffers', + boot_val => '0', + min => '0', + max => 'SLRU_MAX_ALLOWED_BUFFERS', + check_hook => 'check_transaction_buffers', +}, + +{ name => 'transaction_deferrable', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Whether to defer a read-only serializable transaction until it can be executed with no possible serialization failures.', + flags => 'GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'XactDeferrable', + boot_val => 'false', + check_hook => 'check_transaction_deferrable', +}, + +{ name => 'transaction_isolation', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the current transaction\'s isolation level.', + flags => 'GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'XactIsoLevel', + boot_val => 'XACT_READ_COMMITTED', + options => 'isolation_level_options', + check_hook => 'check_transaction_isolation', +}, + +{ name => 'transaction_read_only', type => 'bool', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the current transaction\'s read-only status.', + flags => 'GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'XactReadOnly', + boot_val => 'false', + check_hook => 'check_transaction_read_only', +}, + +{ name => 'transaction_timeout', type => 'int', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets the maximum allowed duration of any transaction within a session (not a prepared transaction).', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_MS', + variable => 'TransactionTimeout', + boot_val => '0', + min => '0', + max => 'INT_MAX', + assign_hook => 'assign_transaction_timeout', +}, + +{ name => 'transform_null_equals', type => 'bool', context => 'PGC_USERSET', group => 'COMPAT_OPTIONS_OTHER', + short_desc => 'Treats "expr=NULL" as "expr IS NULL".', + long_desc => 'When turned on, expressions of the form expr = NULL (or NULL = expr) are treated as expr IS NULL, that is, they return true if expr evaluates to the null value, and false otherwise. The correct behavior of expr = NULL is to always return null (unknown).', + variable => 'Transform_null_equals', + boot_val => 'false', +}, + +{ name => 'unix_socket_directories', type => 'string', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the directories where Unix-domain sockets will be created.', + flags => 'GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY', + variable => 'Unix_socket_directories', + boot_val => 'DEFAULT_PGSOCKET_DIR', +}, + +{ name => 'unix_socket_group', type => 'string', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the owning group of the Unix-domain socket.', + long_desc => 'The owning user of the socket is always the user that starts the server. An empty string means use the user\'s default group.', + variable => 'Unix_socket_group', + boot_val => '""', +}, + +{ name => 'unix_socket_permissions', type => 'int', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS', + short_desc => 'Sets the access permissions of the Unix-domain socket.', + long_desc => 'Unix-domain sockets use the usual Unix file system permission set. The parameter value is expected to be a numeric mode specification in the form accepted by the chmod and umask system calls. (To use the customary octal format the number must start with a 0 (zero).)', + variable => 'Unix_socket_permissions', + boot_val => '0777', + min => '0000', + max => '0777', + show_hook => 'show_unix_socket_permissions', +}, + +{ name => 'update_process_title', type => 'bool', context => 'PGC_SUSET', group => 'PROCESS_TITLE', + short_desc => 'Updates the process title to show the active SQL command.', + long_desc => 'Enables updating of the process title every time a new SQL command is received by the server.', + variable => 'update_process_title', + boot_val => 'DEFAULT_UPDATE_PROCESS_TITLE', +}, + +{ name => 'vacuum_buffer_usage_limit', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM', + short_desc => 'Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum.', + flags => 'GUC_UNIT_KB', + variable => 'VacuumBufferUsageLimit', + boot_val => '2048', + min => '0', + max => 'MAX_BAS_VAC_RING_SIZE_KB', + check_hook => 'check_vacuum_buffer_usage_limit', +}, + +{ name => 'vacuum_cost_delay', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY', + short_desc => 'Vacuum cost delay in milliseconds.', + flags => 'GUC_UNIT_MS', + variable => 'VacuumCostDelay', + boot_val => '0', + min => '0', + max => '100', +}, + +{ name => 'vacuum_cost_limit', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY', + short_desc => 'Vacuum cost amount available before napping.', + variable => 'VacuumCostLimit', + boot_val => '200', + min => '1', + max => '10000', +}, + +{ name => 'vacuum_cost_page_dirty', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY', + short_desc => 'Vacuum cost for a page dirtied by vacuum.', + variable => 'VacuumCostPageDirty', + boot_val => '20', + min => '0', + max => '10000', +}, + +{ name => 'vacuum_cost_page_hit', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY', + short_desc => 'Vacuum cost for a page found in the buffer cache.', + variable => 'VacuumCostPageHit', + boot_val => '1', + min => '0', + max => '10000', +}, + +{ name => 'vacuum_cost_page_miss', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_COST_DELAY', + short_desc => 'Vacuum cost for a page not found in the buffer cache.', + variable => 'VacuumCostPageMiss', + boot_val => '2', + min => '0', + max => '10000', +}, + +{ name => 'vacuum_failsafe_age', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Age at which VACUUM should trigger failsafe to avoid a wraparound outage.', + variable => 'vacuum_failsafe_age', + boot_val => '1600000000', + min => '0', + max => '2100000000', +}, + +{ name => 'vacuum_freeze_min_age', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Minimum age at which VACUUM should freeze a table row.', + variable => 'vacuum_freeze_min_age', + boot_val => '50000000', + min => '0', + max => '1000000000', +}, + +{ name => 'vacuum_freeze_table_age', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Age at which VACUUM should scan whole table to freeze tuples.', + variable => 'vacuum_freeze_table_age', + boot_val => '150000000', + min => '0', + max => '2000000000', +}, + +{ name => 'vacuum_max_eager_freeze_failure_rate', type => 'real', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.', + long_desc => 'A value of 0.0 disables eager scanning and a value of 1.0 will eagerly scan up to 100 percent of the all-visible pages in the relation. If vacuum successfully freezes these pages, the cap is lower than 100 percent, because the goal is to amortize page freezing across multiple vacuums.', + variable => 'vacuum_max_eager_freeze_failure_rate', + boot_val => '0.03', + min => '0.0', + max => '1.0', +}, + +{ name => 'vacuum_multixact_failsafe_age', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Multixact age at which VACUUM should trigger failsafe to avoid a wraparound outage.', + variable => 'vacuum_multixact_failsafe_age', + boot_val => '1600000000', + min => '0', + max => '2100000000', +}, + +{ name => 'vacuum_multixact_freeze_min_age', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Minimum age at which VACUUM should freeze a MultiXactId in a table row.', + variable => 'vacuum_multixact_freeze_min_age', + boot_val => '5000000', + min => '0', + max => '1000000000', +}, + +{ name => 'vacuum_multixact_freeze_table_age', type => 'int', context => 'PGC_USERSET', group => 'VACUUM_FREEZING', + short_desc => 'Multixact age at which VACUUM should scan whole table to freeze tuples.', + variable => 'vacuum_multixact_freeze_table_age', + boot_val => '150000000', + min => '0', + max => '2000000000', +}, + +{ name => 'vacuum_truncate', type => 'bool', context => 'PGC_USERSET', group => 'VACUUM_DEFAULT', + short_desc => 'Enables vacuum to truncate empty pages at the end of the table.', + variable => 'vacuum_truncate', + boot_val => 'true', +}, + +{ name => 'wal_block_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the block size in the write ahead log.', + flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE', + variable => 'wal_block_size', + boot_val => 'XLOG_BLCKSZ', + min => 'XLOG_BLCKSZ', + max => 'XLOG_BLCKSZ', +}, + +{ name => 'wal_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'WAL_SETTINGS', + short_desc => 'Sets the number of disk-page buffers in shared memory for WAL.', + long_desc => '-1 means use a fraction of "shared_buffers".', + flags => 'GUC_UNIT_XBLOCKS', + variable => 'XLOGbuffers', + boot_val => '-1', + min => '-1', + max => '(INT_MAX / XLOG_BLCKSZ)', + check_hook => 'check_wal_buffers', +}, + +{ name => 'wal_compression', type => 'enum', context => 'PGC_SUSET', group => 'WAL_SETTINGS', + short_desc => 'Compresses full-page writes written in WAL file with specified method.', + variable => 'wal_compression', + boot_val => 'WAL_COMPRESSION_NONE', + options => 'wal_compression_options', +}, + +{ name => 'wal_consistency_checking', type => 'string', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Sets the WAL resource managers for which WAL consistency checks are done.', + long_desc => 'Full-page images will be logged for all data blocks and cross-checked against the results of WAL replay.', + flags => 'GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE', + variable => 'wal_consistency_checking_string', + boot_val => '""', + check_hook => 'check_wal_consistency_checking', + assign_hook => 'assign_wal_consistency_checking', +}, + +{ name => 'wal_debug', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Emit WAL-related debugging output.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'XLOG_DEBUG', + boot_val => 'false', + ifdef => 'WAL_DEBUG', +}, + +{ name => 'wal_decode_buffer_size', type => 'int', context => 'PGC_POSTMASTER', group => 'WAL_RECOVERY', + short_desc => 'Buffer size for reading ahead in the WAL during recovery.', + long_desc => 'Maximum distance to read ahead in the WAL to prefetch referenced data blocks.', + flags => 'GUC_UNIT_BYTE', + variable => 'wal_decode_buffer_size', + boot_val => '512 * 1024', + min => '64 * 1024', + max => 'MaxAllocSize', +}, + +{ name => 'wal_init_zero', type => 'bool', context => 'PGC_SUSET', group => 'WAL_SETTINGS', + short_desc => 'Writes zeroes to new WAL files before first use.', + variable => 'wal_init_zero', + boot_val => 'true', +}, + +{ name => 'wal_keep_size', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SENDING', + short_desc => 'Sets the size of WAL files held for standby servers.', + flags => 'GUC_UNIT_MB', + variable => 'wal_keep_size_mb', + boot_val => '0', + min => '0', + max => 'MAX_KILOBYTES', +}, + +{ name => 'wal_level', type => 'enum', context => 'PGC_POSTMASTER', group => 'WAL_SETTINGS', + short_desc => 'Sets the level of information written to the WAL.', + variable => 'wal_level', + boot_val => 'WAL_LEVEL_REPLICA', + options => 'wal_level_options', +}, + +{ name => 'wal_log_hints', type => 'bool', context => 'PGC_POSTMASTER', group => 'WAL_SETTINGS', + short_desc => 'Writes full pages to WAL when first modified after a checkpoint, even for a non-critical modification.', + variable => 'wal_log_hints', + boot_val => 'false', +}, + +{ name => 'wal_receiver_create_temp_slot', type => 'bool', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets whether a WAL receiver should create a temporary replication slot if no permanent slot is configured.', + variable => 'wal_receiver_create_temp_slot', + boot_val => 'false', +}, + +{ name => 'wal_receiver_status_interval', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the maximum interval between WAL receiver status reports to the sending server.', + flags => 'GUC_UNIT_S', + variable => 'wal_receiver_status_interval', + boot_val => '10', + min => '0', + max => 'INT_MAX / 1000', +}, + +{ name => 'wal_receiver_timeout', type => 'int', context => 'PGC_USERSET', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the maximum wait time to receive data from the sending server.', + long_desc => '0 disables the timeout.', + flags => 'GUC_UNIT_MS', + variable => 'wal_receiver_timeout', + boot_val => '60 * 1000', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'wal_recycle', type => 'bool', context => 'PGC_SUSET', group => 'WAL_SETTINGS', + short_desc => 'Recycles WAL files by renaming them.', + variable => 'wal_recycle', + boot_val => 'true', +}, + +{ name => 'wal_retrieve_retry_interval', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_STANDBY', + short_desc => 'Sets the time to wait before retrying to retrieve WAL after a failed attempt.', + flags => 'GUC_UNIT_MS', + variable => 'wal_retrieve_retry_interval', + boot_val => '5000', + min => '1', + max => 'INT_MAX', +}, + +{ name => 'wal_segment_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS', + short_desc => 'Shows the size of write ahead log segments.', + flags => 'GUC_UNIT_BYTE | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED', + variable => 'wal_segment_size', + boot_val => 'DEFAULT_XLOG_SEG_SIZE', + min => 'WalSegMinSize', + max => 'WalSegMaxSize', + check_hook => 'check_wal_segment_size', +}, + +{ name => 'wal_sender_shutdown_timeout', type => 'int', context => 'PGC_USERSET', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum time the server waits during shutdown for all WAL data to be replicated to the receiver.', + long_desc => '-1 disables the timeout', + flags => 'GUC_UNIT_MS', + variable => 'wal_sender_shutdown_timeout', + boot_val => '-1', + min => '-1', + max => 'INT_MAX', +}, + +{ name => 'wal_sender_timeout', type => 'int', context => 'PGC_USERSET', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum time to wait for WAL replication.', + flags => 'GUC_UNIT_MS', + variable => 'wal_sender_timeout', + boot_val => '60 * 1000', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'wal_skip_threshold', type => 'int', context => 'PGC_USERSET', group => 'WAL_SETTINGS', + short_desc => 'Minimum size of new file to fsync instead of writing WAL.', + flags => 'GUC_UNIT_KB', + variable => 'wal_skip_threshold', + boot_val => '2048', + min => '0', + max => 'MAX_KILOBYTES', +}, + +{ name => 'wal_summary_keep_time', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_SUMMARIZATION', + short_desc => 'Time for which WAL summary files should be kept.', + long_desc => '0 disables automatic summary file deletion.', + flags => 'GUC_UNIT_MIN', + variable => 'wal_summary_keep_time', + boot_val => '10 * HOURS_PER_DAY * MINS_PER_HOUR /* 10 days */', + min => '0', + max => 'INT_MAX / SECS_PER_MINUTE', +}, + +{ name => 'wal_sync_method', type => 'enum', context => 'PGC_SIGHUP', group => 'WAL_SETTINGS', + short_desc => 'Selects the method used for forcing WAL updates to disk.', + variable => 'wal_sync_method', + boot_val => 'DEFAULT_WAL_SYNC_METHOD', + options => 'wal_sync_method_options', + assign_hook => 'assign_wal_sync_method', +}, + +{ name => 'wal_writer_delay', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_SETTINGS', + short_desc => 'Time between WAL flushes performed in the WAL writer.', + flags => 'GUC_UNIT_MS', + variable => 'WalWriterDelay', + boot_val => '200', + min => '1', + max => '10000', +}, + +{ name => 'wal_writer_flush_after', type => 'int', context => 'PGC_SIGHUP', group => 'WAL_SETTINGS', + short_desc => 'Amount of WAL written out by WAL writer that triggers a flush.', + flags => 'GUC_UNIT_XBLOCKS', + variable => 'WalWriterFlushAfter', + boot_val => 'DEFAULT_WAL_WRITER_FLUSH_AFTER', + min => '0', + max => 'INT_MAX', +}, + +{ name => 'work_mem', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_MEM', + short_desc => 'Sets the maximum memory to be used for query workspaces.', + long_desc => 'This much memory can be used by each internal sort operation and hash table before switching to temporary disk files.', + flags => 'GUC_UNIT_KB | GUC_EXPLAIN', + variable => 'work_mem', + boot_val => '4096', + min => '64', + max => 'MAX_KILOBYTES', +}, + +{ name => 'xmlbinary', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets how binary values are to be encoded in XML.', + variable => 'xmlbinary', + boot_val => 'XMLBINARY_BASE64', + options => 'xmlbinary_options', +}, + +{ name => 'xmloption', type => 'enum', context => 'PGC_USERSET', group => 'CLIENT_CONN_STATEMENT', + short_desc => 'Sets whether XML data in implicit parsing and serialization operations is to be considered as documents or content fragments.', + variable => 'xmloption', + boot_val => 'XMLOPTION_CONTENT', + options => 'xmloption_options', +}, + +{ name => 'zero_damaged_pages', type => 'bool', context => 'PGC_SUSET', group => 'DEVELOPER_OPTIONS', + short_desc => 'Continues processing past damaged page headers.', + long_desc => 'Detection of a damaged page header normally causes PostgreSQL to report an error, aborting the current transaction. Setting "zero_damaged_pages" to true causes the system to instead report a warning, zero out the damaged page, and continue processing. This behavior will destroy data, namely all the rows on the damaged page.', + flags => 'GUC_NOT_IN_SAMPLE', + variable => 'zero_damaged_pages', + boot_val => 'false', +}, + +] diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 2f8cbd8675998..290ccbc543e25 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -4,13 +4,13 @@ * * Static tables for the Grand Unified Configuration scheme. * - * Many of these tables are const. However, ConfigureNamesBool[] - * and so on are not, because the structs in those arrays are actually - * the live per-variable state data that guc.c manipulates. While many of - * their fields are intended to be constant, some fields change at runtime. + * Many of these tables are const. However, ConfigureNames[] is not, because + * the structs in it are actually the live per-variable state data that guc.c + * manipulates. While many of their fields are intended to be constant, some + * fields change at runtime. * * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * Written by Peter Eisentraut . * * IDENTIFICATION @@ -80,14 +80,17 @@ #include "storage/bufmgr.h" #include "storage/bufpage.h" #include "storage/copydir.h" +#include "storage/fd.h" #include "storage/io_worker.h" #include "storage/large_object.h" #include "storage/pg_shmem.h" #include "storage/predicate.h" +#include "storage/proc.h" #include "storage/procnumber.h" #include "storage/standby.h" #include "tcop/backend_startup.h" #include "tcop/tcopprot.h" +#include "portability/instr_time.h" #include "tsearch/ts_cache.h" #include "utils/builtins.h" #include "utils/bytea.h" @@ -146,7 +149,7 @@ static const struct config_enum_entry client_message_level_options[] = { {NULL, 0, false} }; -static const struct config_enum_entry server_message_level_options[] = { +const struct config_enum_entry server_message_level_options[] = { {"debug5", DEBUG5, false}, {"debug4", DEBUG4, false}, {"debug3", DEBUG3, false}, @@ -371,6 +374,15 @@ static const struct config_enum_entry huge_pages_options[] = { {NULL, 0, false} }; +static const struct config_enum_entry timing_clock_source_options[] = { + {"auto", TIMING_CLOCK_SOURCE_AUTO, false}, + {"system", TIMING_CLOCK_SOURCE_SYSTEM, false}, +#if PG_INSTR_TSC_CLOCK + {"tsc", TIMING_CLOCK_SOURCE_TSC, false}, +#endif + {NULL, 0, false} +}; + static const struct config_enum_entry huge_pages_status_options[] = { {"off", HUGE_PAGES_OFF, false}, {"on", HUGE_PAGES_ON, false}, @@ -491,6 +503,22 @@ static const struct config_enum_entry file_copy_method_options[] = { {NULL, 0, false} }; +static const struct config_enum_entry file_extend_method_options[] = { +#ifdef HAVE_POSIX_FALLOCATE + {"posix_fallocate", FILE_EXTEND_METHOD_POSIX_FALLOCATE, false}, +#endif + {"write_zeros", FILE_EXTEND_METHOD_WRITE_ZEROS, false}, + {NULL, 0, false} +}; + +static const struct config_enum_entry data_checksums_options[] = { + {"on", PG_DATA_CHECKSUM_VERSION, true}, + {"off", PG_DATA_CHECKSUM_OFF, true}, + {"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON, true}, + {"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF, true}, + {NULL, 0, false} +}; + /* * Options for enum values stored in other modules */ @@ -507,6 +535,7 @@ bool AllowAlterSystem = true; bool log_duration = false; bool Debug_print_plan = false; bool Debug_print_parse = false; +bool Debug_print_raw_parse = false; bool Debug_print_rewritten = false; bool Debug_pretty_print = true; @@ -528,15 +557,14 @@ bool row_security; bool check_function_bodies = true; /* - * This GUC exists solely for backward compatibility, check its definition for - * details. + * These GUCs exist solely for backward compatibility. */ static bool default_with_oids = false; +static bool standard_conforming_strings = true; bool current_role_is_superuser; int log_min_error_statement = ERROR; -int log_min_messages = WARNING; int client_min_messages = NOTICE; int log_min_duration_sample = -1; int log_min_duration_statement = -1; @@ -555,6 +583,7 @@ char *cluster_name = ""; char *ConfigFileName; char *HbaFileName; char *IdentFileName; +char *HostsFileName; char *external_pid_file; char *application_name; @@ -594,6 +623,7 @@ static char *server_version_string; static int server_version_num; static char *debug_io_direct_string; static char *restrict_nonsystem_relation_kind_string; +static char *log_min_messages_string; #ifdef HAVE_SYSLOG #define DEFAULT_SYSLOG_FACILITY LOG_LOCAL0 @@ -616,7 +646,7 @@ static int shared_memory_size_mb; static int shared_memory_size_in_huge_pages; static int wal_block_size; static int num_os_semaphores; -static bool data_checksums; +static int effective_wal_level = WAL_LEVEL_REPLICA; static bool integer_datetimes; #ifdef USE_ASSERT_CHECKING @@ -626,6 +656,13 @@ static bool integer_datetimes; #endif static bool assert_enabled = DEFAULT_ASSERT_ENABLED; +#ifdef EXEC_BACKEND +#define EXEC_BACKEND_ENABLED true +#else +#define EXEC_BACKEND_ENABLED false +#endif +static bool exec_backend_enabled = EXEC_BACKEND_ENABLED; + static char *recovery_target_timeline_string; static char *recovery_target_string; static char *recovery_target_xid_string; @@ -638,6 +675,15 @@ char *role_string; /* should be static, but guc.c needs to get at this */ bool in_hot_standby_guc; +/* + * set default log_min_messages to WARNING for all process types + */ +int log_min_messages[] = { +#define PG_PROCTYPE(bktype, bkcategory, description, main_func, shmem_attach) \ + [bktype] = WARNING, +#include "postmaster/proctypelist.h" +#undef PG_PROCTYPE +}; /* * Displayable names for context types (enum GucContext) @@ -695,6 +741,7 @@ const char *const config_group_names[] = [CONN_AUTH_TCP] = gettext_noop("Connections and Authentication / TCP Settings"), [CONN_AUTH_AUTH] = gettext_noop("Connections and Authentication / Authentication"), [CONN_AUTH_SSL] = gettext_noop("Connections and Authentication / SSL"), + [RESOURCES_TIME] = gettext_noop("Resource Usage / Time"), [RESOURCES_MEM] = gettext_noop("Resource Usage / Memory"), [RESOURCES_DISK] = gettext_noop("Resource Usage / Disk"), [RESOURCES_KERNEL] = gettext_noop("Resource Usage / Kernel Resources"), @@ -760,4666 +807,4 @@ StaticAssertDecl(lengthof(config_type_names) == (PGC_ENUM + 1), "array length mismatch"); -/* - * Contents of GUC tables - * - * See src/backend/utils/misc/README for design notes. - * - * TO ADD AN OPTION: - * - * 1. Declare a global variable of type bool, int, double, or char* - * and make use of it. - * - * 2. Decide at what times it's safe to set the option. See guc.h for - * details. - * - * 3. Decide on a name, a default value, upper and lower bounds (if - * applicable), etc. - * - * 4. Add a record below. - * - * 5. Add it to src/backend/utils/misc/postgresql.conf.sample, if - * appropriate. - * - * 6. Don't forget to document the option (at least in config.sgml). - * - * 7. If it's a new GUC_LIST_QUOTE option, you must add it to - * variable_is_guc_list_quote() in src/bin/pg_dump/dumputils.c. - */ - -struct config_bool ConfigureNamesBool[] = -{ - { - {"enable_seqscan", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of sequential-scan plans."), - NULL, - GUC_EXPLAIN - }, - &enable_seqscan, - true, - NULL, NULL, NULL - }, - { - {"enable_indexscan", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of index-scan plans."), - NULL, - GUC_EXPLAIN - }, - &enable_indexscan, - true, - NULL, NULL, NULL - }, - { - {"enable_indexonlyscan", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of index-only-scan plans."), - NULL, - GUC_EXPLAIN - }, - &enable_indexonlyscan, - true, - NULL, NULL, NULL - }, - { - {"enable_bitmapscan", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of bitmap-scan plans."), - NULL, - GUC_EXPLAIN - }, - &enable_bitmapscan, - true, - NULL, NULL, NULL - }, - { - {"enable_tidscan", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of TID scan plans."), - NULL, - GUC_EXPLAIN - }, - &enable_tidscan, - true, - NULL, NULL, NULL - }, - { - {"enable_sort", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of explicit sort steps."), - NULL, - GUC_EXPLAIN - }, - &enable_sort, - true, - NULL, NULL, NULL - }, - { - {"enable_incremental_sort", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of incremental sort steps."), - NULL, - GUC_EXPLAIN - }, - &enable_incremental_sort, - true, - NULL, NULL, NULL - }, - { - {"enable_hashagg", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of hashed aggregation plans."), - NULL, - GUC_EXPLAIN - }, - &enable_hashagg, - true, - NULL, NULL, NULL - }, - { - {"enable_material", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of materialization."), - NULL, - GUC_EXPLAIN - }, - &enable_material, - true, - NULL, NULL, NULL - }, - { - {"enable_memoize", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of memoization."), - NULL, - GUC_EXPLAIN - }, - &enable_memoize, - true, - NULL, NULL, NULL - }, - { - {"enable_nestloop", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of nested-loop join plans."), - NULL, - GUC_EXPLAIN - }, - &enable_nestloop, - true, - NULL, NULL, NULL - }, - { - {"enable_mergejoin", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of merge join plans."), - NULL, - GUC_EXPLAIN - }, - &enable_mergejoin, - true, - NULL, NULL, NULL - }, - { - {"enable_hashjoin", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of hash join plans."), - NULL, - GUC_EXPLAIN - }, - &enable_hashjoin, - true, - NULL, NULL, NULL - }, - { - {"enable_gathermerge", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of gather merge plans."), - NULL, - GUC_EXPLAIN - }, - &enable_gathermerge, - true, - NULL, NULL, NULL - }, - { - {"enable_partitionwise_join", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables partitionwise join."), - NULL, - GUC_EXPLAIN - }, - &enable_partitionwise_join, - false, - NULL, NULL, NULL - }, - { - {"enable_partitionwise_aggregate", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables partitionwise aggregation and grouping."), - NULL, - GUC_EXPLAIN - }, - &enable_partitionwise_aggregate, - false, - NULL, NULL, NULL - }, - { - {"enable_parallel_append", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of parallel append plans."), - NULL, - GUC_EXPLAIN - }, - &enable_parallel_append, - true, - NULL, NULL, NULL - }, - { - {"enable_parallel_hash", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of parallel hash plans."), - NULL, - GUC_EXPLAIN - }, - &enable_parallel_hash, - true, - NULL, NULL, NULL - }, - { - {"enable_partition_pruning", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables plan-time and execution-time partition pruning."), - gettext_noop("Allows the query planner and executor to compare partition " - "bounds to conditions in the query to determine which " - "partitions must be scanned."), - GUC_EXPLAIN - }, - &enable_partition_pruning, - true, - NULL, NULL, NULL - }, - { - {"enable_presorted_aggregate", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's ability to produce plans that " - "provide presorted input for ORDER BY / DISTINCT aggregate " - "functions."), - gettext_noop("Allows the query planner to build plans that provide " - "presorted input for aggregate functions with an ORDER BY / " - "DISTINCT clause. When disabled, implicit sorts are always " - "performed during execution."), - GUC_EXPLAIN - }, - &enable_presorted_aggregate, - true, - NULL, NULL, NULL - }, - { - {"enable_async_append", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables the planner's use of async append plans."), - NULL, - GUC_EXPLAIN - }, - &enable_async_append, - true, - NULL, NULL, NULL - }, - { - {"enable_self_join_elimination", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables removal of unique self-joins."), - NULL, - GUC_EXPLAIN - }, - &enable_self_join_elimination, - true, - NULL, NULL, NULL - }, - { - {"enable_group_by_reordering", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables reordering of GROUP BY keys."), - NULL, - GUC_EXPLAIN - }, - &enable_group_by_reordering, - true, - NULL, NULL, NULL - }, - { - {"enable_distinct_reordering", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables reordering of DISTINCT pathkeys."), - NULL, - GUC_EXPLAIN - }, - &enable_distinct_reordering, - true, - NULL, NULL, NULL - }, - { - {"geqo", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("Enables genetic query optimization."), - gettext_noop("This algorithm attempts to do planning without " - "exhaustive searching."), - GUC_EXPLAIN - }, - &enable_geqo, - true, - NULL, NULL, NULL - }, - { - /* - * Not for general use --- used by SET SESSION AUTHORIZATION and SET - * ROLE - */ - {"is_superuser", PGC_INTERNAL, UNGROUPED, - gettext_noop("Shows whether the current user is a superuser."), - NULL, - GUC_REPORT | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_ALLOW_IN_PARALLEL - }, - ¤t_role_is_superuser, - false, - NULL, NULL, NULL - }, - { - /* - * This setting itself cannot be set by ALTER SYSTEM to avoid an - * operator turning this setting off by using ALTER SYSTEM, without a - * way to turn it back on. - */ - {"allow_alter_system", PGC_SIGHUP, COMPAT_OPTIONS_OTHER, - gettext_noop("Allows running the ALTER SYSTEM command."), - gettext_noop("Can be set to off for environments where global configuration " - "changes should be made using a different method."), - GUC_DISALLOW_IN_AUTO_FILE - }, - &AllowAlterSystem, - true, - NULL, NULL, NULL - }, - { - {"bonjour", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Enables advertising the server via Bonjour."), - NULL - }, - &enable_bonjour, - false, - check_bonjour, NULL, NULL - }, - { - {"track_commit_timestamp", PGC_POSTMASTER, REPLICATION_SENDING, - gettext_noop("Collects transaction commit time."), - NULL - }, - &track_commit_timestamp, - false, - NULL, NULL, NULL - }, - { - {"ssl", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Enables SSL connections."), - NULL - }, - &EnableSSL, - false, - check_ssl, NULL, NULL - }, - { - {"ssl_passphrase_command_supports_reload", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Controls whether \"ssl_passphrase_command\" is called during server reload."), - NULL - }, - &ssl_passphrase_command_supports_reload, - false, - NULL, NULL, NULL - }, - { - {"ssl_prefer_server_ciphers", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Give priority to server ciphersuite order."), - NULL - }, - &SSLPreferServerCiphers, - true, - NULL, NULL, NULL - }, - { - {"fsync", PGC_SIGHUP, WAL_SETTINGS, - gettext_noop("Forces synchronization of updates to disk."), - gettext_noop("The server will use the fsync() system call in several places to make " - "sure that updates are physically written to disk. This ensures " - "that a database cluster will recover to a consistent state after " - "an operating system or hardware crash.") - }, - &enableFsync, - true, - NULL, NULL, NULL - }, - { - {"ignore_checksum_failure", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Continues processing after a checksum failure."), - gettext_noop("Detection of a checksum failure normally causes PostgreSQL to " - "report an error, aborting the current transaction. Setting " - "ignore_checksum_failure to true causes the system to ignore the failure " - "(but still report a warning), and continue processing. This " - "behavior could cause crashes or other serious problems. Only " - "has an effect if checksums are enabled."), - GUC_NOT_IN_SAMPLE - }, - &ignore_checksum_failure, - false, - NULL, NULL, NULL - }, - { - {"zero_damaged_pages", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Continues processing past damaged page headers."), - gettext_noop("Detection of a damaged page header normally causes PostgreSQL to " - "report an error, aborting the current transaction. Setting " - "\"zero_damaged_pages\" to true causes the system to instead report a " - "warning, zero out the damaged page, and continue processing. This " - "behavior will destroy data, namely all the rows on the damaged page."), - GUC_NOT_IN_SAMPLE - }, - &zero_damaged_pages, - false, - NULL, NULL, NULL - }, - { - {"ignore_invalid_pages", PGC_POSTMASTER, DEVELOPER_OPTIONS, - gettext_noop("Continues recovery after an invalid pages failure."), - gettext_noop("Detection of WAL records having references to " - "invalid pages during recovery causes PostgreSQL to " - "raise a PANIC-level error, aborting the recovery. " - "Setting \"ignore_invalid_pages\" to true causes " - "the system to ignore invalid page references " - "in WAL records (but still report a warning), " - "and continue recovery. This behavior may cause " - "crashes, data loss, propagate or hide corruption, " - "or other serious problems. Only has an effect " - "during recovery or in standby mode."), - GUC_NOT_IN_SAMPLE - }, - &ignore_invalid_pages, - false, - NULL, NULL, NULL - }, - { - {"full_page_writes", PGC_SIGHUP, WAL_SETTINGS, - gettext_noop("Writes full pages to WAL when first modified after a checkpoint."), - gettext_noop("A page write in process during an operating system crash might be " - "only partially written to disk. During recovery, the row changes " - "stored in WAL are not enough to recover. This option writes " - "pages when first modified after a checkpoint to WAL so full recovery " - "is possible.") - }, - &fullPageWrites, - true, - NULL, NULL, NULL - }, - - { - {"wal_log_hints", PGC_POSTMASTER, WAL_SETTINGS, - gettext_noop("Writes full pages to WAL when first modified after a checkpoint, even for a non-critical modification."), - NULL - }, - &wal_log_hints, - false, - NULL, NULL, NULL - }, - - { - {"wal_init_zero", PGC_SUSET, WAL_SETTINGS, - gettext_noop("Writes zeroes to new WAL files before first use."), - NULL - }, - &wal_init_zero, - true, - NULL, NULL, NULL - }, - - { - {"wal_recycle", PGC_SUSET, WAL_SETTINGS, - gettext_noop("Recycles WAL files by renaming them."), - NULL - }, - &wal_recycle, - true, - NULL, NULL, NULL - }, - - { - {"log_checkpoints", PGC_SIGHUP, LOGGING_WHAT, - gettext_noop("Logs each checkpoint."), - NULL - }, - &log_checkpoints, - true, - NULL, NULL, NULL - }, - { - {"trace_connection_negotiation", PGC_POSTMASTER, DEVELOPER_OPTIONS, - gettext_noop("Logs details of pre-authentication connection handshake."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Trace_connection_negotiation, - false, - NULL, NULL, NULL - }, - { - {"log_disconnections", PGC_SU_BACKEND, LOGGING_WHAT, - gettext_noop("Logs end of a session, including duration."), - NULL - }, - &Log_disconnections, - false, - NULL, NULL, NULL - }, - { - {"log_replication_commands", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Logs each replication command."), - NULL - }, - &log_replication_commands, - false, - NULL, NULL, NULL - }, - { - {"debug_assertions", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows whether the running server has assertion checks enabled."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &assert_enabled, - DEFAULT_ASSERT_ENABLED, - NULL, NULL, NULL - }, - - { - {"exit_on_error", PGC_USERSET, ERROR_HANDLING_OPTIONS, - gettext_noop("Terminate session on any error."), - NULL - }, - &ExitOnAnyError, - false, - NULL, NULL, NULL - }, - { - {"restart_after_crash", PGC_SIGHUP, ERROR_HANDLING_OPTIONS, - gettext_noop("Reinitialize server after backend crash."), - NULL - }, - &restart_after_crash, - true, - NULL, NULL, NULL - }, - { - {"remove_temp_files_after_crash", PGC_SIGHUP, DEVELOPER_OPTIONS, - gettext_noop("Remove temporary files after backend crash."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &remove_temp_files_after_crash, - true, - NULL, NULL, NULL - }, - { - {"send_abort_for_crash", PGC_SIGHUP, DEVELOPER_OPTIONS, - gettext_noop("Send SIGABRT not SIGQUIT to child processes after backend crash."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &send_abort_for_crash, - false, - NULL, NULL, NULL - }, - { - {"send_abort_for_kill", PGC_SIGHUP, DEVELOPER_OPTIONS, - gettext_noop("Send SIGABRT not SIGKILL to stuck child processes."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &send_abort_for_kill, - false, - NULL, NULL, NULL - }, - - { - {"log_duration", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Logs the duration of each completed SQL statement."), - NULL - }, - &log_duration, - false, - NULL, NULL, NULL - }, -#ifdef DEBUG_NODE_TESTS_ENABLED - { - {"debug_copy_parse_plan_trees", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Set this to force all parse and plan trees to be passed through " - "copyObject(), to facilitate catching errors and omissions in " - "copyObject()."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Debug_copy_parse_plan_trees, -/* support for legacy compile-time setting */ -#ifdef COPY_PARSE_PLAN_TREES - true, -#else - false, -#endif - NULL, NULL, NULL - }, - { - {"debug_write_read_parse_plan_trees", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Set this to force all parse and plan trees to be passed through " - "outfuncs.c/readfuncs.c, to facilitate catching errors and omissions in " - "those modules."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Debug_write_read_parse_plan_trees, -/* support for legacy compile-time setting */ -#ifdef WRITE_READ_PARSE_PLAN_TREES - true, -#else - false, -#endif - NULL, NULL, NULL - }, - { - {"debug_raw_expression_coverage_test", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Set this to force all raw parse trees for DML statements to be scanned " - "by raw_expression_tree_walker(), to facilitate catching errors and " - "omissions in that function."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Debug_raw_expression_coverage_test, -/* support for legacy compile-time setting */ -#ifdef RAW_EXPRESSION_COVERAGE_TEST - true, -#else - false, -#endif - NULL, NULL, NULL - }, -#endif /* DEBUG_NODE_TESTS_ENABLED */ - { - {"debug_print_parse", PGC_USERSET, LOGGING_WHAT, - gettext_noop("Logs each query's parse tree."), - NULL - }, - &Debug_print_parse, - false, - NULL, NULL, NULL - }, - { - {"debug_print_rewritten", PGC_USERSET, LOGGING_WHAT, - gettext_noop("Logs each query's rewritten parse tree."), - NULL - }, - &Debug_print_rewritten, - false, - NULL, NULL, NULL - }, - { - {"debug_print_plan", PGC_USERSET, LOGGING_WHAT, - gettext_noop("Logs each query's execution plan."), - NULL - }, - &Debug_print_plan, - false, - NULL, NULL, NULL - }, - { - {"debug_pretty_print", PGC_USERSET, LOGGING_WHAT, - gettext_noop("Indents parse and plan tree displays."), - NULL - }, - &Debug_pretty_print, - true, - NULL, NULL, NULL - }, - { - {"log_parser_stats", PGC_SUSET, STATS_MONITORING, - gettext_noop("Writes parser performance statistics to the server log."), - NULL - }, - &log_parser_stats, - false, - check_stage_log_stats, NULL, NULL - }, - { - {"log_planner_stats", PGC_SUSET, STATS_MONITORING, - gettext_noop("Writes planner performance statistics to the server log."), - NULL - }, - &log_planner_stats, - false, - check_stage_log_stats, NULL, NULL - }, - { - {"log_executor_stats", PGC_SUSET, STATS_MONITORING, - gettext_noop("Writes executor performance statistics to the server log."), - NULL - }, - &log_executor_stats, - false, - check_stage_log_stats, NULL, NULL - }, - { - {"log_statement_stats", PGC_SUSET, STATS_MONITORING, - gettext_noop("Writes cumulative performance statistics to the server log."), - NULL - }, - &log_statement_stats, - false, - check_log_stats, NULL, NULL - }, -#ifdef BTREE_BUILD_STATS - { - {"log_btree_build_stats", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Logs system resource usage statistics (memory and CPU) on various B-tree operations."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &log_btree_build_stats, - false, - NULL, NULL, NULL - }, -#endif - - { - {"track_activities", PGC_SUSET, STATS_CUMULATIVE, - gettext_noop("Collects information about executing commands."), - gettext_noop("Enables the collection of information on the currently " - "executing command of each session, along with " - "the time at which that command began execution.") - }, - &pgstat_track_activities, - true, - NULL, NULL, NULL - }, - { - {"track_counts", PGC_SUSET, STATS_CUMULATIVE, - gettext_noop("Collects statistics on database activity."), - NULL - }, - &pgstat_track_counts, - true, - NULL, NULL, NULL - }, - { - {"track_cost_delay_timing", PGC_SUSET, STATS_CUMULATIVE, - gettext_noop("Collects timing statistics for cost-based vacuum delay."), - NULL - }, - &track_cost_delay_timing, - false, - NULL, NULL, NULL - }, - { - {"track_io_timing", PGC_SUSET, STATS_CUMULATIVE, - gettext_noop("Collects timing statistics for database I/O activity."), - NULL - }, - &track_io_timing, - false, - NULL, NULL, NULL - }, - { - {"track_wal_io_timing", PGC_SUSET, STATS_CUMULATIVE, - gettext_noop("Collects timing statistics for WAL I/O activity."), - NULL - }, - &track_wal_io_timing, - false, - NULL, NULL, NULL - }, - - { - {"update_process_title", PGC_SUSET, PROCESS_TITLE, - gettext_noop("Updates the process title to show the active SQL command."), - gettext_noop("Enables updating of the process title every time a new SQL command is received by the server.") - }, - &update_process_title, - DEFAULT_UPDATE_PROCESS_TITLE, - NULL, NULL, NULL - }, - - { - {"autovacuum", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Starts the autovacuum subprocess."), - NULL - }, - &autovacuum_start_daemon, - true, - NULL, NULL, NULL - }, - - { - {"trace_notify", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Generates debugging output for LISTEN and NOTIFY."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Trace_notify, - false, - NULL, NULL, NULL - }, - -#ifdef LOCK_DEBUG - { - {"trace_locks", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Emits information about lock usage."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Trace_locks, - false, - NULL, NULL, NULL - }, - { - {"trace_userlocks", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Emits information about user lock usage."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Trace_userlocks, - false, - NULL, NULL, NULL - }, - { - {"trace_lwlocks", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Emits information about lightweight lock usage."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Trace_lwlocks, - false, - NULL, NULL, NULL - }, - { - {"debug_deadlocks", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Dumps information about all current locks when a deadlock timeout occurs."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Debug_deadlocks, - false, - NULL, NULL, NULL - }, -#endif - - { - {"log_lock_waits", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Logs long lock waits."), - NULL - }, - &log_lock_waits, - false, - NULL, NULL, NULL - }, - { - {"log_lock_failure", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Logs lock failures."), - NULL - }, - &log_lock_failure, - false, - NULL, NULL, NULL - }, - { - {"log_recovery_conflict_waits", PGC_SIGHUP, LOGGING_WHAT, - gettext_noop("Logs standby recovery conflict waits."), - NULL - }, - &log_recovery_conflict_waits, - false, - NULL, NULL, NULL - }, - { - {"log_hostname", PGC_SIGHUP, LOGGING_WHAT, - gettext_noop("Logs the host name in the connection logs."), - gettext_noop("By default, connection logs only show the IP address " - "of the connecting host. If you want them to show the host name you " - "can turn this on, but depending on your host name resolution " - "setup it might impose a non-negligible performance penalty.") - }, - &log_hostname, - false, - NULL, NULL, NULL - }, - { - {"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_OTHER, - gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."), - gettext_noop("When turned on, expressions of the form expr = NULL " - "(or NULL = expr) are treated as expr IS NULL, that is, they " - "return true if expr evaluates to the null value, and false " - "otherwise. The correct behavior of expr = NULL is to always " - "return null (unknown).") - }, - &Transform_null_equals, - false, - NULL, NULL, NULL - }, - { - {"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the default read-only status of new transactions."), - NULL, - GUC_REPORT - }, - &DefaultXactReadOnly, - false, - NULL, NULL, NULL - }, - { - {"transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the current transaction's read-only status."), - NULL, - GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &XactReadOnly, - false, - check_transaction_read_only, NULL, NULL - }, - { - {"default_transaction_deferrable", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the default deferrable status of new transactions."), - NULL - }, - &DefaultXactDeferrable, - false, - NULL, NULL, NULL - }, - { - {"transaction_deferrable", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Whether to defer a read-only serializable transaction until it can be executed with no possible serialization failures."), - NULL, - GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &XactDeferrable, - false, - check_transaction_deferrable, NULL, NULL - }, - { - {"row_security", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Enables row security."), - gettext_noop("When enabled, row security will be applied to all users.") - }, - &row_security, - true, - NULL, NULL, NULL - }, - { - {"check_function_bodies", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Check routine bodies during CREATE FUNCTION and CREATE PROCEDURE."), - NULL - }, - &check_function_bodies, - true, - NULL, NULL, NULL - }, - { - {"array_nulls", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("Enables input of NULL elements in arrays."), - gettext_noop("When turned on, unquoted NULL in an array input " - "value means a null value; " - "otherwise it is taken literally.") - }, - &Array_nulls, - true, - NULL, NULL, NULL - }, - - /* - * WITH OIDS support, and consequently default_with_oids, was removed in - * PostgreSQL 12, but we tolerate the parameter being set to false to - * avoid unnecessarily breaking older dump files. - */ - { - {"default_with_oids", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("WITH OIDS is no longer supported; this can only be false."), - NULL, - GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE - }, - &default_with_oids, - false, - check_default_with_oids, NULL, NULL - }, - { - {"logging_collector", PGC_POSTMASTER, LOGGING_WHERE, - gettext_noop("Start a subprocess to capture stderr, csvlog and/or jsonlog into log files."), - NULL - }, - &Logging_collector, - false, - NULL, NULL, NULL - }, - { - {"log_truncate_on_rotation", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Truncate existing log files of same name during log rotation."), - NULL - }, - &Log_truncate_on_rotation, - false, - NULL, NULL, NULL - }, - - { - {"trace_sort", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Emit information about resource usage in sorting."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &trace_sort, - false, - NULL, NULL, NULL - }, - -#ifdef TRACE_SYNCSCAN - /* this is undocumented because not exposed in a standard build */ - { - {"trace_syncscan", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Generate debugging output for synchronized scanning."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &trace_syncscan, - false, - NULL, NULL, NULL - }, -#endif - -#ifdef DEBUG_BOUNDED_SORT - /* this is undocumented because not exposed in a standard build */ - { - { - "optimize_bounded_sort", PGC_USERSET, QUERY_TUNING_METHOD, - gettext_noop("Enables bounded sorting using heap sort."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_EXPLAIN - }, - &optimize_bounded_sort, - true, - NULL, NULL, NULL - }, -#endif - -#ifdef WAL_DEBUG - { - {"wal_debug", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Emit WAL-related debugging output."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &XLOG_DEBUG, - false, - NULL, NULL, NULL - }, -#endif - - { - {"integer_datetimes", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows whether datetimes are integer based."), - NULL, - GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &integer_datetimes, - true, - NULL, NULL, NULL - }, - - { - {"krb_caseins_users", PGC_SIGHUP, CONN_AUTH_AUTH, - gettext_noop("Sets whether Kerberos and GSSAPI user names should be treated as case-insensitive."), - NULL - }, - &pg_krb_caseins_users, - false, - NULL, NULL, NULL - }, - - { - {"gss_accept_delegation", PGC_SIGHUP, CONN_AUTH_AUTH, - gettext_noop("Sets whether GSSAPI delegation should be accepted from the client."), - NULL - }, - &pg_gss_accept_delegation, - false, - NULL, NULL, NULL - }, - - { - {"escape_string_warning", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("Warn about backslash escapes in ordinary string literals."), - NULL - }, - &escape_string_warning, - true, - NULL, NULL, NULL - }, - - { - {"standard_conforming_strings", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("Causes '...' strings to treat backslashes literally."), - NULL, - GUC_REPORT - }, - &standard_conforming_strings, - true, - NULL, NULL, NULL - }, - - { - {"synchronize_seqscans", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("Enables synchronized sequential scans."), - NULL - }, - &synchronize_seqscans, - true, - NULL, NULL, NULL - }, - - { - {"recovery_target_inclusive", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Sets whether to include or exclude transaction with recovery target."), - NULL - }, - &recoveryTargetInclusive, - true, - NULL, NULL, NULL - }, - - { - {"summarize_wal", PGC_SIGHUP, WAL_SUMMARIZATION, - gettext_noop("Starts the WAL summarizer process to enable incremental backup."), - NULL - }, - &summarize_wal, - false, - NULL, NULL, NULL - }, - - { - {"hot_standby", PGC_POSTMASTER, REPLICATION_STANDBY, - gettext_noop("Allows connections and queries during recovery."), - NULL - }, - &EnableHotStandby, - true, - NULL, NULL, NULL - }, - - { - {"hot_standby_feedback", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Allows feedback from a hot standby to the primary that will avoid query conflicts."), - NULL - }, - &hot_standby_feedback, - false, - NULL, NULL, NULL - }, - - { - {"in_hot_standby", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows whether hot standby is currently active."), - NULL, - GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &in_hot_standby_guc, - false, - NULL, NULL, show_in_hot_standby - }, - - { - {"allow_system_table_mods", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Allows modifications of the structure of system tables."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &allowSystemTableMods, - false, - NULL, NULL, NULL - }, - - { - {"ignore_system_indexes", PGC_BACKEND, DEVELOPER_OPTIONS, - gettext_noop("Disables reading from system indexes."), - gettext_noop("It does not prevent updating the indexes, so it is safe " - "to use. The worst consequence is slowness."), - GUC_NOT_IN_SAMPLE - }, - &IgnoreSystemIndexes, - false, - NULL, NULL, NULL - }, - - { - {"allow_in_place_tablespaces", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Allows tablespaces directly inside pg_tblspc, for testing."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &allow_in_place_tablespaces, - false, - NULL, NULL, NULL - }, - - { - {"lo_compat_privileges", PGC_SUSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("Enables backward compatibility mode for privilege checks on large objects."), - gettext_noop("Skips privilege checks when reading or modifying large objects, " - "for compatibility with PostgreSQL releases prior to 9.0.") - }, - &lo_compat_privileges, - false, - NULL, NULL, NULL - }, - - { - {"quote_all_identifiers", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("When generating SQL fragments, quote all identifiers."), - NULL, - }, - "e_all_identifiers, - false, - NULL, NULL, NULL - }, - - { - {"data_checksums", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows whether data checksums are turned on for this cluster."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED - }, - &data_checksums, - false, - NULL, NULL, NULL - }, - - { - {"syslog_sequence_numbers", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Add sequence number to syslog messages to avoid duplicate suppression."), - NULL - }, - &syslog_sequence_numbers, - true, - NULL, NULL, NULL - }, - - { - {"syslog_split_messages", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Split messages sent to syslog by lines and to fit into 1024 bytes."), - NULL - }, - &syslog_split_messages, - true, - NULL, NULL, NULL - }, - - { - {"parallel_leader_participation", PGC_USERSET, RESOURCES_WORKER_PROCESSES, - gettext_noop("Controls whether Gather and Gather Merge also run subplans."), - gettext_noop("Should gather nodes also run subplans or just gather tuples?"), - GUC_EXPLAIN - }, - ¶llel_leader_participation, - true, - NULL, NULL, NULL - }, - - { - {"jit", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Allow JIT compilation."), - NULL, - GUC_EXPLAIN - }, - &jit_enabled, - true, - NULL, NULL, NULL - }, - - { - {"jit_debugging_support", PGC_SU_BACKEND, DEVELOPER_OPTIONS, - gettext_noop("Register JIT-compiled functions with debugger."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &jit_debugging_support, - false, - - /* - * This is not guaranteed to be available, but given it's a developer - * oriented option, it doesn't seem worth adding code checking - * availability. - */ - NULL, NULL, NULL - }, - - { - {"jit_dump_bitcode", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Write out LLVM bitcode to facilitate JIT debugging."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &jit_dump_bitcode, - false, - NULL, NULL, NULL - }, - - { - {"jit_expressions", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Allow JIT compilation of expressions."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &jit_expressions, - true, - NULL, NULL, NULL - }, - - { - {"jit_profiling_support", PGC_SU_BACKEND, DEVELOPER_OPTIONS, - gettext_noop("Register JIT-compiled functions with perf profiler."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &jit_profiling_support, - false, - - /* - * This is not guaranteed to be available, but given it's a developer - * oriented option, it doesn't seem worth adding code checking - * availability. - */ - NULL, NULL, NULL - }, - - { - {"jit_tuple_deforming", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Allow JIT compilation of tuple deforming."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &jit_tuple_deforming, - true, - NULL, NULL, NULL - }, - - { - {"data_sync_retry", PGC_POSTMASTER, ERROR_HANDLING_OPTIONS, - gettext_noop("Whether to continue running after a failure to sync data files."), - }, - &data_sync_retry, - false, - NULL, NULL, NULL - }, - - { - {"wal_receiver_create_temp_slot", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets whether a WAL receiver should create a temporary replication slot if no permanent slot is configured."), - }, - &wal_receiver_create_temp_slot, - false, - NULL, NULL, NULL - }, - - { - {"event_triggers", PGC_SUSET, CLIENT_CONN_STATEMENT, - gettext_noop("Enables event triggers."), - gettext_noop("When enabled, event triggers will fire for all applicable statements."), - }, - &event_triggers, - true, - NULL, NULL, NULL - }, - - { - {"sync_replication_slots", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Enables a physical standby to synchronize logical failover replication slots from the primary server."), - }, - &sync_replication_slots, - false, - NULL, NULL, NULL - }, - - { - {"md5_password_warnings", PGC_USERSET, CONN_AUTH_AUTH, - gettext_noop("Enables deprecation warnings for MD5 passwords."), - }, - &md5_password_warnings, - true, - NULL, NULL, NULL - }, - - { - {"vacuum_truncate", PGC_USERSET, VACUUM_DEFAULT, - gettext_noop("Enables vacuum to truncate empty pages at the end of the table."), - }, - &vacuum_truncate, - true, - NULL, NULL, NULL - }, - - /* End-of-list marker */ - { - {NULL, 0, 0, NULL, NULL}, NULL, false, NULL, NULL, NULL - } -}; - - -struct config_int ConfigureNamesInt[] = -{ - { - {"archive_timeout", PGC_SIGHUP, WAL_ARCHIVING, - gettext_noop("Sets the amount of time to wait before forcing a " - "switch to the next WAL file."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_S - }, - &XLogArchiveTimeout, - 0, 0, INT_MAX / 2, - NULL, NULL, NULL - }, - { - {"post_auth_delay", PGC_BACKEND, DEVELOPER_OPTIONS, - gettext_noop("Sets the amount of time to wait after " - "authentication on connection startup."), - gettext_noop("This allows attaching a debugger to the process."), - GUC_NOT_IN_SAMPLE | GUC_UNIT_S - }, - &PostAuthDelay, - 0, 0, INT_MAX / 1000000, - NULL, NULL, NULL - }, - { - {"default_statistics_target", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Sets the default statistics target."), - gettext_noop("This applies to table columns that have not had a " - "column-specific target set via ALTER TABLE SET STATISTICS.") - }, - &default_statistics_target, - 100, 1, MAX_STATISTICS_TARGET, - NULL, NULL, NULL - }, - { - {"from_collapse_limit", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Sets the FROM-list size beyond which subqueries " - "are not collapsed."), - gettext_noop("The planner will merge subqueries into upper " - "queries if the resulting FROM list would have no more than " - "this many items."), - GUC_EXPLAIN - }, - &from_collapse_limit, - 8, 1, INT_MAX, - NULL, NULL, NULL - }, - { - {"join_collapse_limit", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Sets the FROM-list size beyond which JOIN " - "constructs are not flattened."), - gettext_noop("The planner will flatten explicit JOIN " - "constructs into lists of FROM items whenever a " - "list of no more than this many items would result."), - GUC_EXPLAIN - }, - &join_collapse_limit, - 8, 1, INT_MAX, - NULL, NULL, NULL - }, - { - {"geqo_threshold", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("Sets the threshold of FROM items beyond which GEQO is used."), - NULL, - GUC_EXPLAIN - }, - &geqo_threshold, - 12, 2, INT_MAX, - NULL, NULL, NULL - }, - { - {"geqo_effort", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("GEQO: effort is used to set the default for other GEQO parameters."), - NULL, - GUC_EXPLAIN - }, - &Geqo_effort, - DEFAULT_GEQO_EFFORT, MIN_GEQO_EFFORT, MAX_GEQO_EFFORT, - NULL, NULL, NULL - }, - { - {"geqo_pool_size", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("GEQO: number of individuals in the population."), - gettext_noop("0 means use a suitable default value."), - GUC_EXPLAIN - }, - &Geqo_pool_size, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - { - {"geqo_generations", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("GEQO: number of iterations of the algorithm."), - gettext_noop("0 means use a suitable default value."), - GUC_EXPLAIN - }, - &Geqo_generations, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - /* This is PGC_SUSET to prevent hiding from log_lock_waits. */ - {"deadlock_timeout", PGC_SUSET, LOCK_MANAGEMENT, - gettext_noop("Sets the time to wait on a lock before checking for deadlock."), - NULL, - GUC_UNIT_MS - }, - &DeadlockTimeout, - 1000, 1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_standby_archive_delay", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the maximum delay before canceling queries when a hot standby server is processing archived WAL data."), - gettext_noop("-1 means wait forever."), - GUC_UNIT_MS - }, - &max_standby_archive_delay, - 30 * 1000, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_standby_streaming_delay", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the maximum delay before canceling queries when a hot standby server is processing streamed WAL data."), - gettext_noop("-1 means wait forever."), - GUC_UNIT_MS - }, - &max_standby_streaming_delay, - 30 * 1000, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"recovery_min_apply_delay", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the minimum delay for applying changes during recovery."), - NULL, - GUC_UNIT_MS - }, - &recovery_min_apply_delay, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"wal_receiver_status_interval", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the maximum interval between WAL receiver status reports to the sending server."), - NULL, - GUC_UNIT_S - }, - &wal_receiver_status_interval, - 10, 0, INT_MAX / 1000, - NULL, NULL, NULL - }, - - { - {"wal_receiver_timeout", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the maximum wait time to receive data from the sending server."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_MS - }, - &wal_receiver_timeout, - 60 * 1000, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_connections", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the maximum number of concurrent connections."), - NULL - }, - &MaxConnections, - 100, 1, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - /* see max_connections */ - {"superuser_reserved_connections", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the number of connection slots reserved for superusers."), - NULL - }, - &SuperuserReservedConnections, - 3, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"reserved_connections", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the number of connection slots reserved for roles " - "with privileges of pg_use_reserved_connections."), - NULL - }, - &ReservedConnections, - 0, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"min_dynamic_shared_memory", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Amount of dynamic shared memory reserved at startup."), - NULL, - GUC_UNIT_MB - }, - &min_dynamic_shared_memory, - 0, 0, (int) Min((size_t) INT_MAX, SIZE_MAX / (1024 * 1024)), - NULL, NULL, NULL - }, - - /* - * We sometimes multiply the number of shared buffers by two without - * checking for overflow, so we mustn't allow more than INT_MAX / 2. - */ - { - {"shared_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the number of shared memory buffers used by the server."), - NULL, - GUC_UNIT_BLOCKS - }, - &NBuffers, - 16384, 16, INT_MAX / 2, - NULL, NULL, NULL - }, - - { - {"vacuum_buffer_usage_limit", PGC_USERSET, RESOURCES_MEM, - gettext_noop("Sets the buffer pool size for VACUUM, ANALYZE, and autovacuum."), - NULL, - GUC_UNIT_KB - }, - &VacuumBufferUsageLimit, - 2048, 0, MAX_BAS_VAC_RING_SIZE_KB, - check_vacuum_buffer_usage_limit, NULL, NULL - }, - - { - {"shared_memory_size", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the size of the server's main shared memory area (rounded up to the nearest MB)."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_UNIT_MB | GUC_RUNTIME_COMPUTED - }, - &shared_memory_size_mb, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"shared_memory_size_in_huge_pages", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the number of huge pages needed for the main shared memory area."), - gettext_noop("-1 means huge pages are not supported."), - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED - }, - &shared_memory_size_in_huge_pages, - -1, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"num_os_semaphores", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the number of semaphores required for the server."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED - }, - &num_os_semaphores, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"commit_timestamp_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the commit timestamp cache."), - gettext_noop("0 means use a fraction of \"shared_buffers\"."), - GUC_UNIT_BLOCKS - }, - &commit_timestamp_buffers, - 0, 0, SLRU_MAX_ALLOWED_BUFFERS, - check_commit_ts_buffers, NULL, NULL - }, - - { - {"multixact_member_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the MultiXact member cache."), - NULL, - GUC_UNIT_BLOCKS - }, - &multixact_member_buffers, - 32, 16, SLRU_MAX_ALLOWED_BUFFERS, - check_multixact_member_buffers, NULL, NULL - }, - - { - {"multixact_offset_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the MultiXact offset cache."), - NULL, - GUC_UNIT_BLOCKS - }, - &multixact_offset_buffers, - 16, 16, SLRU_MAX_ALLOWED_BUFFERS, - check_multixact_offset_buffers, NULL, NULL - }, - - { - {"notify_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the LISTEN/NOTIFY message cache."), - NULL, - GUC_UNIT_BLOCKS - }, - ¬ify_buffers, - 16, 16, SLRU_MAX_ALLOWED_BUFFERS, - check_notify_buffers, NULL, NULL - }, - - { - {"serializable_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the serializable transaction cache."), - NULL, - GUC_UNIT_BLOCKS - }, - &serializable_buffers, - 32, 16, SLRU_MAX_ALLOWED_BUFFERS, - check_serial_buffers, NULL, NULL - }, - - { - {"subtransaction_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the subtransaction cache."), - gettext_noop("0 means use a fraction of \"shared_buffers\"."), - GUC_UNIT_BLOCKS - }, - &subtransaction_buffers, - 0, 0, SLRU_MAX_ALLOWED_BUFFERS, - check_subtrans_buffers, NULL, NULL - }, - - { - {"transaction_buffers", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the size of the dedicated buffer pool used for the transaction status cache."), - gettext_noop("0 means use a fraction of \"shared_buffers\"."), - GUC_UNIT_BLOCKS - }, - &transaction_buffers, - 0, 0, SLRU_MAX_ALLOWED_BUFFERS, - check_transaction_buffers, NULL, NULL - }, - - { - {"temp_buffers", PGC_USERSET, RESOURCES_MEM, - gettext_noop("Sets the maximum number of temporary buffers used by each session."), - NULL, - GUC_UNIT_BLOCKS | GUC_EXPLAIN - }, - &num_temp_buffers, - 1024, 100, INT_MAX / 2, - check_temp_buffers, NULL, NULL - }, - - { - {"port", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the TCP port the server listens on."), - NULL - }, - &PostPortNumber, - DEF_PGPORT, 1, 65535, - NULL, NULL, NULL - }, - - { - {"unix_socket_permissions", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the access permissions of the Unix-domain socket."), - gettext_noop("Unix-domain sockets use the usual Unix file system " - "permission set. The parameter value is expected " - "to be a numeric mode specification in the form " - "accepted by the chmod and umask system calls. " - "(To use the customary octal format the number must " - "start with a 0 (zero).)") - }, - &Unix_socket_permissions, - 0777, 0000, 0777, - NULL, NULL, show_unix_socket_permissions - }, - - { - {"log_file_mode", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the file permissions for log files."), - gettext_noop("The parameter value is expected " - "to be a numeric mode specification in the form " - "accepted by the chmod and umask system calls. " - "(To use the customary octal format the number must " - "start with a 0 (zero).)") - }, - &Log_file_mode, - 0600, 0000, 0777, - NULL, NULL, show_log_file_mode - }, - - - { - {"data_directory_mode", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the mode of the data directory."), - gettext_noop("The parameter value is a numeric mode specification " - "in the form accepted by the chmod and umask system " - "calls. (To use the customary octal format the number " - "must start with a 0 (zero).)"), - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED - }, - &data_directory_mode, - 0700, 0000, 0777, - NULL, NULL, show_data_directory_mode - }, - - { - {"work_mem", PGC_USERSET, RESOURCES_MEM, - gettext_noop("Sets the maximum memory to be used for query workspaces."), - gettext_noop("This much memory can be used by each internal " - "sort operation and hash table before switching to " - "temporary disk files."), - GUC_UNIT_KB | GUC_EXPLAIN - }, - &work_mem, - 4096, 64, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - /* - * Dynamic shared memory has a higher overhead than local memory contexts, - * so when testing low-memory scenarios that could use shared memory, the - * recommended minimum is 1MB. - */ - { - {"maintenance_work_mem", PGC_USERSET, RESOURCES_MEM, - gettext_noop("Sets the maximum memory to be used for maintenance operations."), - gettext_noop("This includes operations such as VACUUM and CREATE INDEX."), - GUC_UNIT_KB - }, - &maintenance_work_mem, - 65536, 64, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - { - {"logical_decoding_work_mem", PGC_USERSET, RESOURCES_MEM, - gettext_noop("Sets the maximum memory to be used for logical decoding."), - gettext_noop("This much memory can be used by each internal " - "reorder buffer before spilling to disk."), - GUC_UNIT_KB - }, - &logical_decoding_work_mem, - 65536, 64, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - /* - * We use the hopefully-safely-small value of 100kB as the compiled-in - * default for max_stack_depth. InitializeGUCOptions will increase it if - * possible, depending on the actual platform-specific stack limit. - */ - { - {"max_stack_depth", PGC_SUSET, RESOURCES_MEM, - gettext_noop("Sets the maximum stack depth, in kilobytes."), - NULL, - GUC_UNIT_KB - }, - &max_stack_depth, - 100, 100, MAX_KILOBYTES, - check_max_stack_depth, assign_max_stack_depth, NULL - }, - - { - {"temp_file_limit", PGC_SUSET, RESOURCES_DISK, - gettext_noop("Limits the total size of all temporary files used by each process."), - gettext_noop("-1 means no limit."), - GUC_UNIT_KB - }, - &temp_file_limit, - -1, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"vacuum_cost_page_hit", PGC_USERSET, VACUUM_COST_DELAY, - gettext_noop("Vacuum cost for a page found in the buffer cache."), - NULL - }, - &VacuumCostPageHit, - 1, 0, 10000, - NULL, NULL, NULL - }, - - { - {"vacuum_cost_page_miss", PGC_USERSET, VACUUM_COST_DELAY, - gettext_noop("Vacuum cost for a page not found in the buffer cache."), - NULL - }, - &VacuumCostPageMiss, - 2, 0, 10000, - NULL, NULL, NULL - }, - - { - {"vacuum_cost_page_dirty", PGC_USERSET, VACUUM_COST_DELAY, - gettext_noop("Vacuum cost for a page dirtied by vacuum."), - NULL - }, - &VacuumCostPageDirty, - 20, 0, 10000, - NULL, NULL, NULL - }, - - { - {"vacuum_cost_limit", PGC_USERSET, VACUUM_COST_DELAY, - gettext_noop("Vacuum cost amount available before napping."), - NULL - }, - &VacuumCostLimit, - 200, 1, 10000, - NULL, NULL, NULL - }, - - { - {"autovacuum_vacuum_cost_limit", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Vacuum cost amount available before napping, for autovacuum."), - gettext_noop("-1 means use \"vacuum_cost_limit\".") - }, - &autovacuum_vac_cost_limit, - -1, -1, 10000, - NULL, NULL, NULL - }, - - { - {"max_files_per_process", PGC_POSTMASTER, RESOURCES_KERNEL, - gettext_noop("Sets the maximum number of files each server process is allowed to open simultaneously."), - NULL - }, - &max_files_per_process, - 1000, 64, INT_MAX, - NULL, NULL, NULL - }, - - /* - * See also CheckRequiredParameterValues() if this parameter changes - */ - { - {"max_prepared_transactions", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Sets the maximum number of simultaneously prepared transactions."), - NULL - }, - &max_prepared_xacts, - 0, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - -#ifdef LOCK_DEBUG - { - {"trace_lock_oidmin", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Sets the minimum OID of tables for tracking locks."), - gettext_noop("Is used to avoid output on system tables."), - GUC_NOT_IN_SAMPLE - }, - &Trace_lock_oidmin, - FirstNormalObjectId, 0, INT_MAX, - NULL, NULL, NULL - }, - { - {"trace_lock_table", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Sets the OID of the table with unconditionally lock tracing."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &Trace_lock_table, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, -#endif - - { - {"statement_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the maximum allowed duration of any statement."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_MS - }, - &StatementTimeout, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"lock_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the maximum allowed duration of any wait for a lock."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_MS - }, - &LockTimeout, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"idle_in_transaction_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the maximum allowed idle time between queries, when in a transaction."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_MS - }, - &IdleInTransactionSessionTimeout, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"transaction_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the maximum allowed duration of any transaction within a session (not a prepared transaction)."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_MS - }, - &TransactionTimeout, - 0, 0, INT_MAX, - NULL, assign_transaction_timeout, NULL - }, - - { - {"idle_session_timeout", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the maximum allowed idle time between queries, when not in a transaction."), - gettext_noop("0 disables the timeout."), - GUC_UNIT_MS - }, - &IdleSessionTimeout, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"vacuum_freeze_min_age", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Minimum age at which VACUUM should freeze a table row."), - NULL - }, - &vacuum_freeze_min_age, - 50000000, 0, 1000000000, - NULL, NULL, NULL - }, - - { - {"vacuum_freeze_table_age", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Age at which VACUUM should scan whole table to freeze tuples."), - NULL - }, - &vacuum_freeze_table_age, - 150000000, 0, 2000000000, - NULL, NULL, NULL - }, - - { - {"vacuum_multixact_freeze_min_age", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Minimum age at which VACUUM should freeze a MultiXactId in a table row."), - NULL - }, - &vacuum_multixact_freeze_min_age, - 5000000, 0, 1000000000, - NULL, NULL, NULL - }, - - { - {"vacuum_multixact_freeze_table_age", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Multixact age at which VACUUM should scan whole table to freeze tuples."), - NULL - }, - &vacuum_multixact_freeze_table_age, - 150000000, 0, 2000000000, - NULL, NULL, NULL - }, - - { - {"vacuum_failsafe_age", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Age at which VACUUM should trigger failsafe to avoid a wraparound outage."), - NULL - }, - &vacuum_failsafe_age, - 1600000000, 0, 2100000000, - NULL, NULL, NULL - }, - { - {"vacuum_multixact_failsafe_age", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Multixact age at which VACUUM should trigger failsafe to avoid a wraparound outage."), - NULL - }, - &vacuum_multixact_failsafe_age, - 1600000000, 0, 2100000000, - NULL, NULL, NULL - }, - - /* - * See also CheckRequiredParameterValues() if this parameter changes - */ - { - {"max_locks_per_transaction", PGC_POSTMASTER, LOCK_MANAGEMENT, - gettext_noop("Sets the maximum number of locks per transaction."), - gettext_noop("The shared lock table is sized on the assumption that at most " - "\"max_locks_per_transaction\" objects per server process or prepared " - "transaction will need to be locked at any one time.") - }, - &max_locks_per_xact, - 64, 10, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_pred_locks_per_transaction", PGC_POSTMASTER, LOCK_MANAGEMENT, - gettext_noop("Sets the maximum number of predicate locks per transaction."), - gettext_noop("The shared predicate lock table is sized on the assumption that " - "at most \"max_pred_locks_per_transaction\" objects per server process " - "or prepared transaction will need to be locked at any one time.") - }, - &max_predicate_locks_per_xact, - 64, 10, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_pred_locks_per_relation", PGC_SIGHUP, LOCK_MANAGEMENT, - gettext_noop("Sets the maximum number of predicate-locked pages and tuples per relation."), - gettext_noop("If more than this total of pages and tuples in the same relation are locked " - "by a connection, those locks are replaced by a relation-level lock.") - }, - &max_predicate_locks_per_relation, - -2, INT_MIN, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_pred_locks_per_page", PGC_SIGHUP, LOCK_MANAGEMENT, - gettext_noop("Sets the maximum number of predicate-locked tuples per page."), - gettext_noop("If more than this number of tuples on the same page are locked " - "by a connection, those locks are replaced by a page-level lock.") - }, - &max_predicate_locks_per_page, - 2, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"authentication_timeout", PGC_SIGHUP, CONN_AUTH_AUTH, - gettext_noop("Sets the maximum allowed time to complete client authentication."), - NULL, - GUC_UNIT_S - }, - &AuthenticationTimeout, - 60, 1, 600, - NULL, NULL, NULL - }, - - { - /* Not for general use */ - {"pre_auth_delay", PGC_SIGHUP, DEVELOPER_OPTIONS, - gettext_noop("Sets the amount of time to wait before " - "authentication on connection startup."), - gettext_noop("This allows attaching a debugger to the process."), - GUC_NOT_IN_SAMPLE | GUC_UNIT_S - }, - &PreAuthDelay, - 0, 0, 60, - NULL, NULL, NULL - }, - - { - {"max_notify_queue_pages", PGC_POSTMASTER, RESOURCES_DISK, - gettext_noop("Sets the maximum number of allocated pages for NOTIFY / LISTEN queue."), - NULL, - }, - &max_notify_queue_pages, - 1048576, 64, INT_MAX, - NULL, NULL, NULL - }, - - { - {"wal_decode_buffer_size", PGC_POSTMASTER, WAL_RECOVERY, - gettext_noop("Buffer size for reading ahead in the WAL during recovery."), - gettext_noop("Maximum distance to read ahead in the WAL to prefetch referenced data blocks."), - GUC_UNIT_BYTE - }, - &wal_decode_buffer_size, - 512 * 1024, 64 * 1024, MaxAllocSize, - NULL, NULL, NULL - }, - - { - {"wal_keep_size", PGC_SIGHUP, REPLICATION_SENDING, - gettext_noop("Sets the size of WAL files held for standby servers."), - NULL, - GUC_UNIT_MB - }, - &wal_keep_size_mb, - 0, 0, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - { - {"min_wal_size", PGC_SIGHUP, WAL_CHECKPOINTS, - gettext_noop("Sets the minimum size to shrink the WAL to."), - NULL, - GUC_UNIT_MB - }, - &min_wal_size_mb, - DEFAULT_MIN_WAL_SEGS * (DEFAULT_XLOG_SEG_SIZE / (1024 * 1024)), - 2, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - { - {"max_wal_size", PGC_SIGHUP, WAL_CHECKPOINTS, - gettext_noop("Sets the WAL size that triggers a checkpoint."), - NULL, - GUC_UNIT_MB - }, - &max_wal_size_mb, - DEFAULT_MAX_WAL_SEGS * (DEFAULT_XLOG_SEG_SIZE / (1024 * 1024)), - 2, MAX_KILOBYTES, - NULL, assign_max_wal_size, NULL - }, - - { - {"checkpoint_timeout", PGC_SIGHUP, WAL_CHECKPOINTS, - gettext_noop("Sets the maximum time between automatic WAL checkpoints."), - NULL, - GUC_UNIT_S - }, - &CheckPointTimeout, - 300, 30, 86400, - NULL, NULL, NULL - }, - - { - {"checkpoint_warning", PGC_SIGHUP, WAL_CHECKPOINTS, - gettext_noop("Sets the maximum time before warning if checkpoints " - "triggered by WAL volume happen too frequently."), - gettext_noop("Write a message to the server log if checkpoints " - "caused by the filling of WAL segment files happen more " - "frequently than this amount of time. " - "0 disables the warning."), - GUC_UNIT_S - }, - &CheckPointWarning, - 30, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"checkpoint_flush_after", PGC_SIGHUP, WAL_CHECKPOINTS, - gettext_noop("Number of pages after which previously performed writes are flushed to disk."), - gettext_noop("0 disables forced writeback."), - GUC_UNIT_BLOCKS - }, - &checkpoint_flush_after, - DEFAULT_CHECKPOINT_FLUSH_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES, - NULL, NULL, NULL - }, - - { - {"wal_buffers", PGC_POSTMASTER, WAL_SETTINGS, - gettext_noop("Sets the number of disk-page buffers in shared memory for WAL."), - gettext_noop("-1 means use a fraction of \"shared_buffers\"."), - GUC_UNIT_XBLOCKS - }, - &XLOGbuffers, - -1, -1, (INT_MAX / XLOG_BLCKSZ), - check_wal_buffers, NULL, NULL - }, - - { - {"wal_writer_delay", PGC_SIGHUP, WAL_SETTINGS, - gettext_noop("Time between WAL flushes performed in the WAL writer."), - NULL, - GUC_UNIT_MS - }, - &WalWriterDelay, - 200, 1, 10000, - NULL, NULL, NULL - }, - - { - {"wal_writer_flush_after", PGC_SIGHUP, WAL_SETTINGS, - gettext_noop("Amount of WAL written out by WAL writer that triggers a flush."), - NULL, - GUC_UNIT_XBLOCKS - }, - &WalWriterFlushAfter, - DEFAULT_WAL_WRITER_FLUSH_AFTER, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"wal_skip_threshold", PGC_USERSET, WAL_SETTINGS, - gettext_noop("Minimum size of new file to fsync instead of writing WAL."), - NULL, - GUC_UNIT_KB - }, - &wal_skip_threshold, - 2048, 0, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - { - {"max_wal_senders", PGC_POSTMASTER, REPLICATION_SENDING, - gettext_noop("Sets the maximum number of simultaneously running WAL sender processes."), - NULL - }, - &max_wal_senders, - 10, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - /* see max_wal_senders */ - {"max_replication_slots", PGC_POSTMASTER, REPLICATION_SENDING, - gettext_noop("Sets the maximum number of simultaneously defined replication slots."), - NULL - }, - &max_replication_slots, - 10, 0, MAX_BACKENDS /* XXX? */ , - NULL, NULL, NULL - }, - - { - {"max_slot_wal_keep_size", PGC_SIGHUP, REPLICATION_SENDING, - gettext_noop("Sets the maximum WAL size that can be reserved by replication slots."), - gettext_noop("Replication slots will be marked as failed, and segments released " - "for deletion or recycling, if this much space is occupied by WAL on disk. " - "-1 means no maximum."), - GUC_UNIT_MB - }, - &max_slot_wal_keep_size_mb, - -1, -1, MAX_KILOBYTES, - check_max_slot_wal_keep_size, NULL, NULL - }, - - { - {"wal_sender_timeout", PGC_USERSET, REPLICATION_SENDING, - gettext_noop("Sets the maximum time to wait for WAL replication."), - NULL, - GUC_UNIT_MS - }, - &wal_sender_timeout, - 60 * 1000, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"idle_replication_slot_timeout", PGC_SIGHUP, REPLICATION_SENDING, - gettext_noop("Sets the duration a replication slot can remain idle before " - "it is invalidated."), - NULL, - GUC_UNIT_MIN - }, - &idle_replication_slot_timeout_mins, - 0, 0, INT_MAX / SECS_PER_MINUTE, - check_idle_replication_slot_timeout, NULL, NULL - }, - - { - {"commit_delay", PGC_SUSET, WAL_SETTINGS, - gettext_noop("Sets the delay in microseconds between transaction commit and " - "flushing WAL to disk."), - NULL - /* we have no microseconds designation, so can't supply units here */ - }, - &CommitDelay, - 0, 0, 100000, - NULL, NULL, NULL - }, - - { - {"commit_siblings", PGC_USERSET, WAL_SETTINGS, - gettext_noop("Sets the minimum number of concurrent open transactions " - "required before performing \"commit_delay\"."), - NULL - }, - &CommitSiblings, - 5, 0, 1000, - NULL, NULL, NULL - }, - - { - {"extra_float_digits", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the number of digits displayed for floating-point values."), - gettext_noop("This affects real, double precision, and geometric data types. " - "A zero or negative parameter value is added to the standard " - "number of digits (FLT_DIG or DBL_DIG as appropriate). " - "Any value greater than zero selects precise output mode.") - }, - &extra_float_digits, - 1, -15, 3, - NULL, NULL, NULL - }, - - { - {"log_min_duration_sample", PGC_SUSET, LOGGING_WHEN, - gettext_noop("Sets the minimum execution time above which " - "a sample of statements will be logged." - " Sampling is determined by \"log_statement_sample_rate\"."), - gettext_noop("-1 disables sampling. 0 means sample all statements."), - GUC_UNIT_MS - }, - &log_min_duration_sample, - -1, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"log_min_duration_statement", PGC_SUSET, LOGGING_WHEN, - gettext_noop("Sets the minimum execution time above which " - "all statements will be logged."), - gettext_noop("-1 disables logging statement durations. 0 means log all statement durations."), - GUC_UNIT_MS - }, - &log_min_duration_statement, - -1, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"log_autovacuum_min_duration", PGC_SIGHUP, LOGGING_WHAT, - gettext_noop("Sets the minimum execution time above which " - "autovacuum actions will be logged."), - gettext_noop("-1 disables logging autovacuum actions. 0 means log all autovacuum actions."), - GUC_UNIT_MS - }, - &Log_autovacuum_min_duration, - 600000, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"log_parameter_max_length", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Sets the maximum length in bytes of data logged for bind " - "parameter values when logging statements."), - gettext_noop("-1 means log values in full."), - GUC_UNIT_BYTE - }, - &log_parameter_max_length, - -1, -1, INT_MAX / 2, - NULL, NULL, NULL - }, - - { - {"log_parameter_max_length_on_error", PGC_USERSET, LOGGING_WHAT, - gettext_noop("Sets the maximum length in bytes of data logged for bind " - "parameter values when logging statements, on error."), - gettext_noop("-1 means log values in full."), - GUC_UNIT_BYTE - }, - &log_parameter_max_length_on_error, - 0, -1, INT_MAX / 2, - NULL, NULL, NULL - }, - - { - {"bgwriter_delay", PGC_SIGHUP, RESOURCES_BGWRITER, - gettext_noop("Background writer sleep time between rounds."), - NULL, - GUC_UNIT_MS - }, - &BgWriterDelay, - 200, 10, 10000, - NULL, NULL, NULL - }, - - { - {"bgwriter_lru_maxpages", PGC_SIGHUP, RESOURCES_BGWRITER, - gettext_noop("Background writer maximum number of LRU pages to flush per round."), - gettext_noop("0 disables background writing.") - }, - &bgwriter_lru_maxpages, - 100, 0, INT_MAX / 2, /* Same upper limit as shared_buffers */ - NULL, NULL, NULL - }, - - { - {"bgwriter_flush_after", PGC_SIGHUP, RESOURCES_BGWRITER, - gettext_noop("Number of pages after which previously performed writes are flushed to disk."), - gettext_noop("0 disables forced writeback."), - GUC_UNIT_BLOCKS - }, - &bgwriter_flush_after, - DEFAULT_BGWRITER_FLUSH_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES, - NULL, NULL, NULL - }, - - { - {"effective_io_concurrency", - PGC_USERSET, - RESOURCES_IO, - gettext_noop("Number of simultaneous requests that can be handled efficiently by the disk subsystem."), - gettext_noop("0 disables simultaneous requests."), - GUC_EXPLAIN - }, - &effective_io_concurrency, - DEFAULT_EFFECTIVE_IO_CONCURRENCY, - 0, MAX_IO_CONCURRENCY, - NULL, NULL, NULL - }, - - { - {"maintenance_io_concurrency", - PGC_USERSET, - RESOURCES_IO, - gettext_noop("A variant of \"effective_io_concurrency\" that is used for maintenance work."), - gettext_noop("0 disables simultaneous requests."), - GUC_EXPLAIN - }, - &maintenance_io_concurrency, - DEFAULT_MAINTENANCE_IO_CONCURRENCY, - 0, MAX_IO_CONCURRENCY, - NULL, assign_maintenance_io_concurrency, - NULL - }, - - { - {"io_max_combine_limit", - PGC_POSTMASTER, - RESOURCES_IO, - gettext_noop("Server-wide limit that clamps io_combine_limit."), - NULL, - GUC_UNIT_BLOCKS - }, - &io_max_combine_limit, - DEFAULT_IO_COMBINE_LIMIT, - 1, MAX_IO_COMBINE_LIMIT, - NULL, assign_io_max_combine_limit, NULL - }, - - { - {"io_combine_limit", - PGC_USERSET, - RESOURCES_IO, - gettext_noop("Limit on the size of data reads and writes."), - NULL, - GUC_UNIT_BLOCKS - }, - &io_combine_limit_guc, - DEFAULT_IO_COMBINE_LIMIT, - 1, MAX_IO_COMBINE_LIMIT, - NULL, assign_io_combine_limit, NULL - }, - - { - {"io_max_concurrency", - PGC_POSTMASTER, - RESOURCES_IO, - gettext_noop("Max number of IOs that one process can execute simultaneously."), - NULL, - }, - &io_max_concurrency, - -1, -1, 1024, - check_io_max_concurrency, NULL, NULL - }, - - { - {"io_workers", - PGC_SIGHUP, - RESOURCES_IO, - gettext_noop("Number of IO worker processes, for io_method=worker."), - NULL, - }, - &io_workers, - 3, 1, MAX_IO_WORKERS, - NULL, NULL, NULL - }, - - { - {"backend_flush_after", PGC_USERSET, RESOURCES_IO, - gettext_noop("Number of pages after which previously performed writes are flushed to disk."), - gettext_noop("0 disables forced writeback."), - GUC_UNIT_BLOCKS - }, - &backend_flush_after, - DEFAULT_BACKEND_FLUSH_AFTER, 0, WRITEBACK_MAX_PENDING_FLUSHES, - NULL, NULL, NULL - }, - - { - {"max_worker_processes", - PGC_POSTMASTER, - RESOURCES_WORKER_PROCESSES, - gettext_noop("Maximum number of concurrent worker processes."), - NULL, - }, - &max_worker_processes, - 8, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"max_logical_replication_workers", - PGC_POSTMASTER, - REPLICATION_SUBSCRIBERS, - gettext_noop("Maximum number of logical replication worker processes."), - NULL, - }, - &max_logical_replication_workers, - 4, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"max_sync_workers_per_subscription", - PGC_SIGHUP, - REPLICATION_SUBSCRIBERS, - gettext_noop("Maximum number of table synchronization workers per subscription."), - NULL, - }, - &max_sync_workers_per_subscription, - 2, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"max_parallel_apply_workers_per_subscription", - PGC_SIGHUP, - REPLICATION_SUBSCRIBERS, - gettext_noop("Maximum number of parallel apply workers per subscription."), - NULL, - }, - &max_parallel_apply_workers_per_subscription, - 2, 0, MAX_PARALLEL_WORKER_LIMIT, - NULL, NULL, NULL - }, - - { - {"max_active_replication_origins", - PGC_POSTMASTER, - REPLICATION_SUBSCRIBERS, - gettext_noop("Sets the maximum number of active replication origins."), - NULL - }, - &max_active_replication_origins, - 10, 0, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"log_rotation_age", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the amount of time to wait before forcing " - "log file rotation."), - gettext_noop("0 disables time-based creation of new log files."), - GUC_UNIT_MIN - }, - &Log_RotationAge, - HOURS_PER_DAY * MINS_PER_HOUR, 0, INT_MAX / SECS_PER_MINUTE, - NULL, NULL, NULL - }, - - { - {"log_rotation_size", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the maximum size a log file can reach before " - "being rotated."), - gettext_noop("0 disables size-based creation of new log files."), - GUC_UNIT_KB - }, - &Log_RotationSize, - 10 * 1024, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"max_function_args", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the maximum number of function arguments."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &max_function_args, - FUNC_MAX_ARGS, FUNC_MAX_ARGS, FUNC_MAX_ARGS, - NULL, NULL, NULL - }, - - { - {"max_index_keys", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the maximum number of index keys."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &max_index_keys, - INDEX_MAX_KEYS, INDEX_MAX_KEYS, INDEX_MAX_KEYS, - NULL, NULL, NULL - }, - - { - {"max_identifier_length", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the maximum identifier length."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &max_identifier_length, - NAMEDATALEN - 1, NAMEDATALEN - 1, NAMEDATALEN - 1, - NULL, NULL, NULL - }, - - { - {"block_size", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the size of a disk block."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &block_size, - BLCKSZ, BLCKSZ, BLCKSZ, - NULL, NULL, NULL - }, - - { - {"segment_size", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the number of pages per disk file."), - NULL, - GUC_UNIT_BLOCKS | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &segment_size, - RELSEG_SIZE, RELSEG_SIZE, RELSEG_SIZE, - NULL, NULL, NULL - }, - - { - {"wal_block_size", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the block size in the write ahead log."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &wal_block_size, - XLOG_BLCKSZ, XLOG_BLCKSZ, XLOG_BLCKSZ, - NULL, NULL, NULL - }, - - { - {"wal_retrieve_retry_interval", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the time to wait before retrying to retrieve WAL " - "after a failed attempt."), - NULL, - GUC_UNIT_MS - }, - &wal_retrieve_retry_interval, - 5000, 1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"wal_segment_size", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the size of write ahead log segments."), - NULL, - GUC_UNIT_BYTE | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED - }, - &wal_segment_size, - DEFAULT_XLOG_SEG_SIZE, - WalSegMinSize, - WalSegMaxSize, - check_wal_segment_size, NULL, NULL - }, - - { - {"wal_summary_keep_time", PGC_SIGHUP, WAL_SUMMARIZATION, - gettext_noop("Time for which WAL summary files should be kept."), - gettext_noop("0 disables automatic summary file deletion."), - GUC_UNIT_MIN, - }, - &wal_summary_keep_time, - 10 * HOURS_PER_DAY * MINS_PER_HOUR, /* 10 days */ - 0, - INT_MAX / SECS_PER_MINUTE, - NULL, NULL, NULL - }, - - { - {"autovacuum_naptime", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Time to sleep between autovacuum runs."), - NULL, - GUC_UNIT_S - }, - &autovacuum_naptime, - 60, 1, INT_MAX / 1000, - NULL, NULL, NULL - }, - { - {"autovacuum_vacuum_threshold", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Minimum number of tuple updates or deletes prior to vacuum."), - NULL - }, - &autovacuum_vac_thresh, - 50, 0, INT_MAX, - NULL, NULL, NULL - }, - { - {"autovacuum_vacuum_max_threshold", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Maximum number of tuple updates or deletes prior to vacuum."), - gettext_noop("-1 disables the maximum threshold.") - }, - &autovacuum_vac_max_thresh, - 100000000, -1, INT_MAX, - NULL, NULL, NULL - }, - { - {"autovacuum_vacuum_insert_threshold", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Minimum number of tuple inserts prior to vacuum."), - gettext_noop("-1 disables insert vacuums.") - }, - &autovacuum_vac_ins_thresh, - 1000, -1, INT_MAX, - NULL, NULL, NULL - }, - { - {"autovacuum_analyze_threshold", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Minimum number of tuple inserts, updates, or deletes prior to analyze."), - NULL - }, - &autovacuum_anl_thresh, - 50, 0, INT_MAX, - NULL, NULL, NULL - }, - { - /* see varsup.c for why this is PGC_POSTMASTER not PGC_SIGHUP */ - {"autovacuum_freeze_max_age", PGC_POSTMASTER, VACUUM_AUTOVACUUM, - gettext_noop("Age at which to autovacuum a table to prevent transaction ID wraparound."), - NULL - }, - &autovacuum_freeze_max_age, - - /* see vacuum_failsafe_age if you change the upper-limit value. */ - 200000000, 100000, 2000000000, - NULL, NULL, NULL - }, - { - /* see multixact.c for why this is PGC_POSTMASTER not PGC_SIGHUP */ - {"autovacuum_multixact_freeze_max_age", PGC_POSTMASTER, VACUUM_AUTOVACUUM, - gettext_noop("Multixact age at which to autovacuum a table to prevent multixact wraparound."), - NULL - }, - &autovacuum_multixact_freeze_max_age, - 400000000, 10000, 2000000000, - NULL, NULL, NULL - }, - { - /* see max_connections */ - {"autovacuum_worker_slots", PGC_POSTMASTER, VACUUM_AUTOVACUUM, - gettext_noop("Sets the number of backend slots to allocate for autovacuum workers."), - NULL - }, - &autovacuum_worker_slots, - 16, 1, MAX_BACKENDS, - NULL, NULL, NULL - }, - { - {"autovacuum_max_workers", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Sets the maximum number of simultaneously running autovacuum worker processes."), - NULL - }, - &autovacuum_max_workers, - 3, 1, MAX_BACKENDS, - NULL, NULL, NULL - }, - - { - {"max_parallel_maintenance_workers", PGC_USERSET, RESOURCES_WORKER_PROCESSES, - gettext_noop("Sets the maximum number of parallel processes per maintenance operation."), - NULL - }, - &max_parallel_maintenance_workers, - 2, 0, MAX_PARALLEL_WORKER_LIMIT, - NULL, NULL, NULL - }, - - { - {"max_parallel_workers_per_gather", PGC_USERSET, RESOURCES_WORKER_PROCESSES, - gettext_noop("Sets the maximum number of parallel processes per executor node."), - NULL, - GUC_EXPLAIN - }, - &max_parallel_workers_per_gather, - 2, 0, MAX_PARALLEL_WORKER_LIMIT, - NULL, NULL, NULL - }, - - { - {"max_parallel_workers", PGC_USERSET, RESOURCES_WORKER_PROCESSES, - gettext_noop("Sets the maximum number of parallel workers that can be active at one time."), - NULL, - GUC_EXPLAIN - }, - &max_parallel_workers, - 8, 0, MAX_PARALLEL_WORKER_LIMIT, - NULL, NULL, NULL - }, - - { - {"autovacuum_work_mem", PGC_SIGHUP, RESOURCES_MEM, - gettext_noop("Sets the maximum memory to be used by each autovacuum worker process."), - gettext_noop("-1 means use \"maintenance_work_mem\"."), - GUC_UNIT_KB - }, - &autovacuum_work_mem, - -1, -1, MAX_KILOBYTES, - check_autovacuum_work_mem, NULL, NULL - }, - - { - {"tcp_keepalives_idle", PGC_USERSET, CONN_AUTH_TCP, - gettext_noop("Time between issuing TCP keepalives."), - gettext_noop("0 means use the system default."), - GUC_UNIT_S - }, - &tcp_keepalives_idle, - 0, 0, INT_MAX, - NULL, assign_tcp_keepalives_idle, show_tcp_keepalives_idle - }, - - { - {"tcp_keepalives_interval", PGC_USERSET, CONN_AUTH_TCP, - gettext_noop("Time between TCP keepalive retransmits."), - gettext_noop("0 means use the system default."), - GUC_UNIT_S - }, - &tcp_keepalives_interval, - 0, 0, INT_MAX, - NULL, assign_tcp_keepalives_interval, show_tcp_keepalives_interval - }, - - { - {"ssl_renegotiation_limit", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("SSL renegotiation is no longer supported; this can only be 0."), - NULL, - GUC_NO_SHOW_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE, - }, - &ssl_renegotiation_limit, - 0, 0, 0, - NULL, NULL, NULL - }, - - { - {"tcp_keepalives_count", PGC_USERSET, CONN_AUTH_TCP, - gettext_noop("Maximum number of TCP keepalive retransmits."), - gettext_noop("Number of consecutive keepalive retransmits that can be " - "lost before a connection is considered dead. " - "0 means use the system default."), - }, - &tcp_keepalives_count, - 0, 0, INT_MAX, - NULL, assign_tcp_keepalives_count, show_tcp_keepalives_count - }, - - { - {"gin_fuzzy_search_limit", PGC_USERSET, CLIENT_CONN_OTHER, - gettext_noop("Sets the maximum allowed result for exact search by GIN."), - gettext_noop("0 means no limit."), - }, - &GinFuzzySearchLimit, - 0, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"effective_cache_size", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's assumption about the total size of the data caches."), - gettext_noop("That is, the total size of the caches (kernel cache and shared buffers) used for PostgreSQL data files. " - "This is measured in disk pages, which are normally 8 kB each."), - GUC_UNIT_BLOCKS | GUC_EXPLAIN, - }, - &effective_cache_size, - DEFAULT_EFFECTIVE_CACHE_SIZE, 1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"min_parallel_table_scan_size", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the minimum amount of table data for a parallel scan."), - gettext_noop("If the planner estimates that it will read a number of table pages too small to reach this limit, a parallel scan will not be considered."), - GUC_UNIT_BLOCKS | GUC_EXPLAIN, - }, - &min_parallel_table_scan_size, - (8 * 1024 * 1024) / BLCKSZ, 0, INT_MAX / 3, - NULL, NULL, NULL - }, - - { - {"min_parallel_index_scan_size", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the minimum amount of index data for a parallel scan."), - gettext_noop("If the planner estimates that it will read a number of index pages too small to reach this limit, a parallel scan will not be considered."), - GUC_UNIT_BLOCKS | GUC_EXPLAIN, - }, - &min_parallel_index_scan_size, - (512 * 1024) / BLCKSZ, 0, INT_MAX / 3, - NULL, NULL, NULL - }, - - { - /* Can't be set in postgresql.conf */ - {"server_version_num", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the server version as an integer."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &server_version_num, - PG_VERSION_NUM, PG_VERSION_NUM, PG_VERSION_NUM, - NULL, NULL, NULL - }, - - { - {"log_temp_files", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Log the use of temporary files larger than this number of kilobytes."), - gettext_noop("-1 disables logging temporary files. 0 means log all temporary files."), - GUC_UNIT_KB - }, - &log_temp_files, - -1, -1, INT_MAX, - NULL, NULL, NULL - }, - - { - {"track_activity_query_size", PGC_POSTMASTER, STATS_CUMULATIVE, - gettext_noop("Sets the size reserved for pg_stat_activity.query, in bytes."), - NULL, - GUC_UNIT_BYTE - }, - &pgstat_track_activity_query_size, - 1024, 100, 1048576, - NULL, NULL, NULL - }, - - { - {"gin_pending_list_limit", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the maximum size of the pending list for GIN index."), - NULL, - GUC_UNIT_KB - }, - &gin_pending_list_limit, - 4096, 64, MAX_KILOBYTES, - NULL, NULL, NULL - }, - - { - {"tcp_user_timeout", PGC_USERSET, CONN_AUTH_TCP, - gettext_noop("TCP user timeout."), - gettext_noop("0 means use the system default."), - GUC_UNIT_MS - }, - &tcp_user_timeout, - 0, 0, INT_MAX, - NULL, assign_tcp_user_timeout, show_tcp_user_timeout - }, - - { - {"huge_page_size", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("The size of huge page that should be requested."), - gettext_noop("0 means use the system default."), - GUC_UNIT_KB - }, - &huge_page_size, - 0, 0, INT_MAX, - check_huge_page_size, NULL, NULL - }, - - { - {"debug_discard_caches", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Aggressively flush system caches for debugging purposes."), - gettext_noop("0 means use normal caching behavior."), - GUC_NOT_IN_SAMPLE - }, - &debug_discard_caches, -#ifdef DISCARD_CACHES_ENABLED - /* Set default based on older compile-time-only cache clobber macros */ -#if defined(CLOBBER_CACHE_RECURSIVELY) - 3, -#elif defined(CLOBBER_CACHE_ALWAYS) - 1, -#else - 0, -#endif - 0, 5, -#else /* not DISCARD_CACHES_ENABLED */ - 0, 0, 0, -#endif /* not DISCARD_CACHES_ENABLED */ - NULL, NULL, NULL - }, - - { - {"client_connection_check_interval", PGC_USERSET, CONN_AUTH_TCP, - gettext_noop("Sets the time interval between checks for disconnection while running queries."), - gettext_noop("0 disables connection checks."), - GUC_UNIT_MS - }, - &client_connection_check_interval, - 0, 0, INT_MAX, - check_client_connection_check_interval, NULL, NULL - }, - - { - {"log_startup_progress_interval", PGC_SIGHUP, LOGGING_WHEN, - gettext_noop("Time between progress updates for " - "long-running startup operations."), - gettext_noop("0 disables progress updates."), - GUC_UNIT_MS, - }, - &log_startup_progress_interval, - 10000, 0, INT_MAX, - NULL, NULL, NULL - }, - - { - {"scram_iterations", PGC_USERSET, CONN_AUTH_AUTH, - gettext_noop("Sets the iteration count for SCRAM secret generation."), - NULL, - GUC_REPORT - }, - &scram_sha_256_iterations, - SCRAM_SHA_256_DEFAULT_ITERATIONS, 1, INT_MAX, - NULL, NULL, NULL - }, - - /* End-of-list marker */ - { - {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL - } -}; - - -struct config_real ConfigureNamesReal[] = -{ - { - {"seq_page_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of a " - "sequentially fetched disk page."), - NULL, - GUC_EXPLAIN - }, - &seq_page_cost, - DEFAULT_SEQ_PAGE_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - { - {"random_page_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of a " - "nonsequentially fetched disk page."), - NULL, - GUC_EXPLAIN - }, - &random_page_cost, - DEFAULT_RANDOM_PAGE_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - { - {"cpu_tuple_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of " - "processing each tuple (row)."), - NULL, - GUC_EXPLAIN - }, - &cpu_tuple_cost, - DEFAULT_CPU_TUPLE_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - { - {"cpu_index_tuple_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of " - "processing each index entry during an index scan."), - NULL, - GUC_EXPLAIN - }, - &cpu_index_tuple_cost, - DEFAULT_CPU_INDEX_TUPLE_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - { - {"cpu_operator_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of " - "processing each operator or function call."), - NULL, - GUC_EXPLAIN - }, - &cpu_operator_cost, - DEFAULT_CPU_OPERATOR_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - { - {"parallel_tuple_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of " - "passing each tuple (row) from worker to leader backend."), - NULL, - GUC_EXPLAIN - }, - ¶llel_tuple_cost, - DEFAULT_PARALLEL_TUPLE_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - { - {"parallel_setup_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Sets the planner's estimate of the cost of " - "starting up worker processes for parallel query."), - NULL, - GUC_EXPLAIN - }, - ¶llel_setup_cost, - DEFAULT_PARALLEL_SETUP_COST, 0, DBL_MAX, - NULL, NULL, NULL - }, - - { - {"jit_above_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Perform JIT compilation if query is more expensive."), - gettext_noop("-1 disables JIT compilation."), - GUC_EXPLAIN - }, - &jit_above_cost, - 100000, -1, DBL_MAX, - NULL, NULL, NULL - }, - - { - {"jit_optimize_above_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Optimize JIT-compiled functions if query is more expensive."), - gettext_noop("-1 disables optimization."), - GUC_EXPLAIN - }, - &jit_optimize_above_cost, - 500000, -1, DBL_MAX, - NULL, NULL, NULL - }, - - { - {"jit_inline_above_cost", PGC_USERSET, QUERY_TUNING_COST, - gettext_noop("Perform JIT inlining if query is more expensive."), - gettext_noop("-1 disables inlining."), - GUC_EXPLAIN - }, - &jit_inline_above_cost, - 500000, -1, DBL_MAX, - NULL, NULL, NULL - }, - - { - {"cursor_tuple_fraction", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Sets the planner's estimate of the fraction of " - "a cursor's rows that will be retrieved."), - NULL, - GUC_EXPLAIN - }, - &cursor_tuple_fraction, - DEFAULT_CURSOR_TUPLE_FRACTION, 0.0, 1.0, - NULL, NULL, NULL - }, - - { - {"recursive_worktable_factor", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Sets the planner's estimate of the average size " - "of a recursive query's working table."), - NULL, - GUC_EXPLAIN - }, - &recursive_worktable_factor, - DEFAULT_RECURSIVE_WORKTABLE_FACTOR, 0.001, 1000000.0, - NULL, NULL, NULL - }, - - { - {"geqo_selection_bias", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("GEQO: selective pressure within the population."), - NULL, - GUC_EXPLAIN - }, - &Geqo_selection_bias, - DEFAULT_GEQO_SELECTION_BIAS, - MIN_GEQO_SELECTION_BIAS, MAX_GEQO_SELECTION_BIAS, - NULL, NULL, NULL - }, - { - {"geqo_seed", PGC_USERSET, QUERY_TUNING_GEQO, - gettext_noop("GEQO: seed for random path selection."), - NULL, - GUC_EXPLAIN - }, - &Geqo_seed, - 0.0, 0.0, 1.0, - NULL, NULL, NULL - }, - - { - {"hash_mem_multiplier", PGC_USERSET, RESOURCES_MEM, - gettext_noop("Multiple of \"work_mem\" to use for hash tables."), - NULL, - GUC_EXPLAIN - }, - &hash_mem_multiplier, - 2.0, 1.0, 1000.0, - NULL, NULL, NULL - }, - - { - {"bgwriter_lru_multiplier", PGC_SIGHUP, RESOURCES_BGWRITER, - gettext_noop("Multiple of the average buffer usage to free per round."), - NULL - }, - &bgwriter_lru_multiplier, - 2.0, 0.0, 10.0, - NULL, NULL, NULL - }, - - { - {"seed", PGC_USERSET, UNGROUPED, - gettext_noop("Sets the seed for random-number generation."), - NULL, - GUC_NO_SHOW_ALL | GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &phony_random_seed, - 0.0, -1.0, 1.0, - check_random_seed, assign_random_seed, show_random_seed - }, - - { - {"vacuum_cost_delay", PGC_USERSET, VACUUM_COST_DELAY, - gettext_noop("Vacuum cost delay in milliseconds."), - NULL, - GUC_UNIT_MS - }, - &VacuumCostDelay, - 0, 0, 100, - NULL, NULL, NULL - }, - - { - {"autovacuum_vacuum_cost_delay", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Vacuum cost delay in milliseconds, for autovacuum."), - gettext_noop("-1 means use \"vacuum_cost_delay\"."), - GUC_UNIT_MS - }, - &autovacuum_vac_cost_delay, - 2, -1, 100, - NULL, NULL, NULL - }, - - { - {"autovacuum_vacuum_scale_factor", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Number of tuple updates or deletes prior to vacuum as a fraction of reltuples."), - NULL - }, - &autovacuum_vac_scale, - 0.2, 0.0, 100.0, - NULL, NULL, NULL - }, - - { - {"autovacuum_vacuum_insert_scale_factor", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Number of tuple inserts prior to vacuum as a fraction of reltuples."), - NULL - }, - &autovacuum_vac_ins_scale, - 0.2, 0.0, 100.0, - NULL, NULL, NULL - }, - - { - {"autovacuum_analyze_scale_factor", PGC_SIGHUP, VACUUM_AUTOVACUUM, - gettext_noop("Number of tuple inserts, updates, or deletes prior to analyze as a fraction of reltuples."), - NULL - }, - &autovacuum_anl_scale, - 0.1, 0.0, 100.0, - NULL, NULL, NULL - }, - - { - {"checkpoint_completion_target", PGC_SIGHUP, WAL_CHECKPOINTS, - gettext_noop("Time spent flushing dirty buffers during checkpoint, as fraction of checkpoint interval."), - NULL - }, - &CheckPointCompletionTarget, - 0.9, 0.0, 1.0, - NULL, assign_checkpoint_completion_target, NULL - }, - - { - {"log_statement_sample_rate", PGC_SUSET, LOGGING_WHEN, - gettext_noop("Fraction of statements exceeding \"log_min_duration_sample\" to be logged."), - gettext_noop("Use a value between 0.0 (never log) and 1.0 (always log).") - }, - &log_statement_sample_rate, - 1.0, 0.0, 1.0, - NULL, NULL, NULL - }, - - { - {"log_transaction_sample_rate", PGC_SUSET, LOGGING_WHEN, - gettext_noop("Sets the fraction of transactions from which to log all statements."), - gettext_noop("Use a value between 0.0 (never log) and 1.0 (log all " - "statements for all transactions).") - }, - &log_xact_sample_rate, - 0.0, 0.0, 1.0, - NULL, NULL, NULL - }, - - { - {"vacuum_max_eager_freeze_failure_rate", PGC_USERSET, VACUUM_FREEZING, - gettext_noop("Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning."), - gettext_noop("A value of 0.0 disables eager scanning and a value of 1.0 will eagerly scan up to 100 percent of the all-visible pages in the relation. If vacuum successfully freezes these pages, the cap is lower than 100 percent, because the goal is to amortize page freezing across multiple vacuums.") - }, - &vacuum_max_eager_freeze_failure_rate, - 0.03, 0.0, 1.0, - NULL, NULL, NULL - }, - - /* End-of-list marker */ - { - {NULL, 0, 0, NULL, NULL}, NULL, 0.0, 0.0, 0.0, NULL, NULL, NULL - } -}; - - -struct config_string ConfigureNamesString[] = -{ - { - {"archive_command", PGC_SIGHUP, WAL_ARCHIVING, - gettext_noop("Sets the shell command that will be called to archive a WAL file."), - gettext_noop("An empty string means use \"archive_library\".") - }, - &XLogArchiveCommand, - "", - NULL, NULL, show_archive_command - }, - - { - {"archive_library", PGC_SIGHUP, WAL_ARCHIVING, - gettext_noop("Sets the library that will be called to archive a WAL file."), - gettext_noop("An empty string means use \"archive_command\".") - }, - &XLogArchiveLibrary, - "", - NULL, NULL, NULL - }, - - { - {"restore_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, - gettext_noop("Sets the shell command that will be called to retrieve an archived WAL file."), - NULL - }, - &recoveryRestoreCommand, - "", - NULL, NULL, NULL - }, - - { - {"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, - gettext_noop("Sets the shell command that will be executed at every restart point."), - NULL - }, - &archiveCleanupCommand, - "", - NULL, NULL, NULL - }, - - { - {"recovery_end_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, - gettext_noop("Sets the shell command that will be executed once at the end of recovery."), - NULL - }, - &recoveryEndCommand, - "", - NULL, NULL, NULL - }, - - { - {"recovery_target_timeline", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Specifies the timeline to recover into."), - NULL - }, - &recovery_target_timeline_string, - "latest", - check_recovery_target_timeline, assign_recovery_target_timeline, NULL - }, - - { - {"recovery_target", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Set to \"immediate\" to end recovery as soon as a consistent state is reached."), - NULL - }, - &recovery_target_string, - "", - check_recovery_target, assign_recovery_target, NULL - }, - { - {"recovery_target_xid", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Sets the transaction ID up to which recovery will proceed."), - NULL - }, - &recovery_target_xid_string, - "", - check_recovery_target_xid, assign_recovery_target_xid, NULL - }, - { - {"recovery_target_time", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Sets the time stamp up to which recovery will proceed."), - NULL - }, - &recovery_target_time_string, - "", - check_recovery_target_time, assign_recovery_target_time, NULL - }, - { - {"recovery_target_name", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Sets the named restore point up to which recovery will proceed."), - NULL - }, - &recovery_target_name_string, - "", - check_recovery_target_name, assign_recovery_target_name, NULL - }, - { - {"recovery_target_lsn", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Sets the LSN of the write-ahead log location up to which recovery will proceed."), - NULL - }, - &recovery_target_lsn_string, - "", - check_recovery_target_lsn, assign_recovery_target_lsn, NULL - }, - - { - {"primary_conninfo", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the connection string to be used to connect to the sending server."), - NULL, - GUC_SUPERUSER_ONLY - }, - &PrimaryConnInfo, - "", - NULL, NULL, NULL - }, - - { - {"primary_slot_name", PGC_SIGHUP, REPLICATION_STANDBY, - gettext_noop("Sets the name of the replication slot to use on the sending server."), - NULL - }, - &PrimarySlotName, - "", - check_primary_slot_name, NULL, NULL - }, - - { - {"client_encoding", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the client's character set encoding."), - NULL, - GUC_IS_NAME | GUC_REPORT - }, - &client_encoding_string, - "SQL_ASCII", - check_client_encoding, assign_client_encoding, NULL - }, - - { - {"log_line_prefix", PGC_SIGHUP, LOGGING_WHAT, - gettext_noop("Controls information prefixed to each log line."), - gettext_noop("An empty string means no prefix.") - }, - &Log_line_prefix, - "%m [%p] ", - NULL, NULL, NULL - }, - - { - {"log_timezone", PGC_SIGHUP, LOGGING_WHAT, - gettext_noop("Sets the time zone to use in log messages."), - NULL - }, - &log_timezone_string, - "GMT", - check_log_timezone, assign_log_timezone, show_log_timezone - }, - - { - {"DateStyle", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the display format for date and time values."), - gettext_noop("Also controls interpretation of ambiguous " - "date inputs."), - GUC_LIST_INPUT | GUC_REPORT - }, - &datestyle_string, - "ISO, MDY", - check_datestyle, assign_datestyle, NULL - }, - - { - {"default_table_access_method", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the default table access method for new tables."), - NULL, - GUC_IS_NAME - }, - &default_table_access_method, - DEFAULT_TABLE_ACCESS_METHOD, - check_default_table_access_method, NULL, NULL - }, - - { - {"default_tablespace", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the default tablespace to create tables and indexes in."), - gettext_noop("An empty string means use the database's default tablespace."), - GUC_IS_NAME - }, - &default_tablespace, - "", - check_default_tablespace, NULL, NULL - }, - - { - {"temp_tablespaces", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the tablespace(s) to use for temporary tables and sort files."), - gettext_noop("An empty string means use the database's default tablespace."), - GUC_LIST_INPUT | GUC_LIST_QUOTE - }, - &temp_tablespaces, - "", - check_temp_tablespaces, assign_temp_tablespaces, NULL - }, - - { - {"createrole_self_grant", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets whether a CREATEROLE user automatically grants " - "the role to themselves, and with which options."), - gettext_noop("An empty string disables automatic self grants."), - GUC_LIST_INPUT - }, - &createrole_self_grant, - "", - check_createrole_self_grant, assign_createrole_self_grant, NULL - }, - - { - {"dynamic_library_path", PGC_SUSET, CLIENT_CONN_OTHER, - gettext_noop("Sets the path for dynamically loadable modules."), - gettext_noop("If a dynamically loadable module needs to be opened and " - "the specified name does not have a directory component (i.e., the " - "name does not contain a slash), the system will search this path for " - "the specified file."), - GUC_SUPERUSER_ONLY - }, - &Dynamic_library_path, - "$libdir", - NULL, NULL, NULL - }, - - { - {"extension_control_path", PGC_SUSET, CLIENT_CONN_OTHER, - gettext_noop("Sets the path for extension control files."), - gettext_noop("The remaining extension script and secondary control files are then loaded " - "from the same directory where the primary control file was found."), - GUC_SUPERUSER_ONLY - }, - &Extension_control_path, - "$system", - NULL, NULL, NULL - }, - - { - {"krb_server_keyfile", PGC_SIGHUP, CONN_AUTH_AUTH, - gettext_noop("Sets the location of the Kerberos server key file."), - NULL, - GUC_SUPERUSER_ONLY - }, - &pg_krb_server_keyfile, - PG_KRB_SRVTAB, - NULL, NULL, NULL - }, - - { - {"bonjour_name", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the Bonjour service name."), - gettext_noop("An empty string means use the computer name.") - }, - &bonjour_name, - "", - NULL, NULL, NULL - }, - - { - {"lc_messages", PGC_SUSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the language in which messages are displayed."), - gettext_noop("An empty string means use the operating system setting.") - }, - &locale_messages, - "", - check_locale_messages, assign_locale_messages, NULL - }, - - { - {"lc_monetary", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the locale for formatting monetary amounts."), - gettext_noop("An empty string means use the operating system setting.") - }, - &locale_monetary, - "C", - check_locale_monetary, assign_locale_monetary, NULL - }, - - { - {"lc_numeric", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the locale for formatting numbers."), - gettext_noop("An empty string means use the operating system setting.") - }, - &locale_numeric, - "C", - check_locale_numeric, assign_locale_numeric, NULL - }, - - { - {"lc_time", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the locale for formatting date and time values."), - gettext_noop("An empty string means use the operating system setting.") - }, - &locale_time, - "C", - check_locale_time, assign_locale_time, NULL - }, - - { - {"session_preload_libraries", PGC_SUSET, CLIENT_CONN_PRELOAD, - gettext_noop("Lists shared libraries to preload into each backend."), - NULL, - GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY - }, - &session_preload_libraries_string, - "", - NULL, NULL, NULL - }, - - { - {"shared_preload_libraries", PGC_POSTMASTER, CLIENT_CONN_PRELOAD, - gettext_noop("Lists shared libraries to preload into server."), - NULL, - GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY - }, - &shared_preload_libraries_string, - "", - NULL, NULL, NULL - }, - - { - {"local_preload_libraries", PGC_USERSET, CLIENT_CONN_PRELOAD, - gettext_noop("Lists unprivileged shared libraries to preload into each backend."), - NULL, - GUC_LIST_INPUT | GUC_LIST_QUOTE - }, - &local_preload_libraries_string, - "", - NULL, NULL, NULL - }, - - { - {"search_path", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the schema search order for names that are not schema-qualified."), - NULL, - GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_EXPLAIN | GUC_REPORT - }, - &namespace_search_path, - "\"$user\", public", - check_search_path, assign_search_path, NULL - }, - - { - /* Can't be set in postgresql.conf */ - {"server_encoding", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the server (database) character set encoding."), - NULL, - GUC_IS_NAME | GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &server_encoding_string, - "SQL_ASCII", - NULL, NULL, NULL - }, - - { - /* Can't be set in postgresql.conf */ - {"server_version", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the server version."), - NULL, - GUC_REPORT | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &server_version_string, - PG_VERSION, - NULL, NULL, NULL - }, - - { - /* Not for general use --- used by SET ROLE */ - {"role", PGC_USERSET, UNGROUPED, - gettext_noop("Sets the current role."), - NULL, - GUC_IS_NAME | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NOT_WHILE_SEC_REST - }, - &role_string, - "none", - check_role, assign_role, show_role - }, - - { - /* Not for general use --- used by SET SESSION AUTHORIZATION */ - {"session_authorization", PGC_USERSET, UNGROUPED, - gettext_noop("Sets the session user name."), - NULL, - GUC_IS_NAME | GUC_REPORT | GUC_NO_SHOW_ALL | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_NOT_WHILE_SEC_REST - }, - &session_authorization_string, - NULL, - check_session_authorization, assign_session_authorization, NULL - }, - - { - {"log_destination", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the destination for server log output."), - gettext_noop("Valid values are combinations of \"stderr\", " - "\"syslog\", \"csvlog\", \"jsonlog\", and \"eventlog\", " - "depending on the platform."), - GUC_LIST_INPUT - }, - &Log_destination_string, - "stderr", - check_log_destination, assign_log_destination, NULL - }, - { - {"log_directory", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the destination directory for log files."), - gettext_noop("Can be specified as relative to the data directory " - "or as absolute path."), - GUC_SUPERUSER_ONLY - }, - &Log_directory, - "log", - check_canonical_path, NULL, NULL - }, - { - {"log_filename", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the file name pattern for log files."), - NULL, - GUC_SUPERUSER_ONLY - }, - &Log_filename, - "postgresql-%Y-%m-%d_%H%M%S.log", - NULL, NULL, NULL - }, - - { - {"syslog_ident", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the program name used to identify PostgreSQL " - "messages in syslog."), - NULL - }, - &syslog_ident_str, - "postgres", - NULL, assign_syslog_ident, NULL - }, - - { - {"event_source", PGC_POSTMASTER, LOGGING_WHERE, - gettext_noop("Sets the application name used to identify " - "PostgreSQL messages in the event log."), - NULL - }, - &event_source, - DEFAULT_EVENT_SOURCE, - NULL, NULL, NULL - }, - - { - {"TimeZone", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the time zone for displaying and interpreting time stamps."), - NULL, - GUC_REPORT - }, - &timezone_string, - "GMT", - check_timezone, assign_timezone, show_timezone - }, - { - {"timezone_abbreviations", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Selects a file of time zone abbreviations."), - NULL - }, - &timezone_abbreviations_string, - NULL, - check_timezone_abbreviations, assign_timezone_abbreviations, NULL - }, - - { - {"unix_socket_group", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the owning group of the Unix-domain socket."), - gettext_noop("The owning user of the socket is always the user that starts the server. " - "An empty string means use the user's default group.") - }, - &Unix_socket_group, - "", - NULL, NULL, NULL - }, - - { - {"unix_socket_directories", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the directories where Unix-domain sockets will be created."), - NULL, - GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY - }, - &Unix_socket_directories, - DEFAULT_PGSOCKET_DIR, - NULL, NULL, NULL - }, - - { - {"listen_addresses", PGC_POSTMASTER, CONN_AUTH_SETTINGS, - gettext_noop("Sets the host name or IP address(es) to listen to."), - NULL, - GUC_LIST_INPUT - }, - &ListenAddresses, - "localhost", - NULL, NULL, NULL - }, - - { - /* - * Can't be set by ALTER SYSTEM as it can lead to recursive definition - * of data_directory. - */ - {"data_directory", PGC_POSTMASTER, FILE_LOCATIONS, - gettext_noop("Sets the server's data directory."), - NULL, - GUC_SUPERUSER_ONLY | GUC_DISALLOW_IN_AUTO_FILE - }, - &data_directory, - NULL, - NULL, NULL, NULL - }, - - { - {"config_file", PGC_POSTMASTER, FILE_LOCATIONS, - gettext_noop("Sets the server's main configuration file."), - NULL, - GUC_DISALLOW_IN_FILE | GUC_SUPERUSER_ONLY - }, - &ConfigFileName, - NULL, - NULL, NULL, NULL - }, - - { - {"hba_file", PGC_POSTMASTER, FILE_LOCATIONS, - gettext_noop("Sets the server's \"hba\" configuration file."), - NULL, - GUC_SUPERUSER_ONLY - }, - &HbaFileName, - NULL, - NULL, NULL, NULL - }, - - { - {"ident_file", PGC_POSTMASTER, FILE_LOCATIONS, - gettext_noop("Sets the server's \"ident\" configuration file."), - NULL, - GUC_SUPERUSER_ONLY - }, - &IdentFileName, - NULL, - NULL, NULL, NULL - }, - - { - {"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS, - gettext_noop("Writes the postmaster PID to the specified file."), - NULL, - GUC_SUPERUSER_ONLY - }, - &external_pid_file, - NULL, - check_canonical_path, NULL, NULL - }, - - { - {"ssl_library", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Shows the name of the SSL library."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &ssl_library, -#ifdef USE_SSL - "OpenSSL", -#else - "", -#endif - NULL, NULL, NULL - }, - - { - {"ssl_cert_file", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Location of the SSL server certificate file."), - NULL - }, - &ssl_cert_file, - "server.crt", - NULL, NULL, NULL - }, - - { - {"ssl_key_file", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Location of the SSL server private key file."), - NULL - }, - &ssl_key_file, - "server.key", - NULL, NULL, NULL - }, - - { - {"ssl_ca_file", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Location of the SSL certificate authority file."), - NULL - }, - &ssl_ca_file, - "", - NULL, NULL, NULL - }, - - { - {"ssl_crl_file", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Location of the SSL certificate revocation list file."), - NULL - }, - &ssl_crl_file, - "", - NULL, NULL, NULL - }, - - { - {"ssl_crl_dir", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Location of the SSL certificate revocation list directory."), - NULL - }, - &ssl_crl_dir, - "", - NULL, NULL, NULL - }, - - { - {"synchronous_standby_names", PGC_SIGHUP, REPLICATION_PRIMARY, - gettext_noop("Number of synchronous standbys and list of names of potential synchronous ones."), - NULL, - GUC_LIST_INPUT - }, - &SyncRepStandbyNames, - "", - check_synchronous_standby_names, assign_synchronous_standby_names, NULL - }, - - { - {"default_text_search_config", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets default text search configuration."), - NULL - }, - &TSCurrentConfig, - "pg_catalog.simple", - check_default_text_search_config, assign_default_text_search_config, NULL - }, - - { - {"ssl_tls13_ciphers", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Sets the list of allowed TLSv1.3 cipher suites."), - gettext_noop("An empty string means use the default cipher suites."), - GUC_SUPERUSER_ONLY - }, - &SSLCipherSuites, - "", - NULL, NULL, NULL - }, - - { - {"ssl_ciphers", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Sets the list of allowed TLSv1.2 (and lower) ciphers."), - NULL, - GUC_SUPERUSER_ONLY - }, - &SSLCipherList, -#ifdef USE_OPENSSL - "HIGH:MEDIUM:+3DES:!aNULL", -#else - "none", -#endif - NULL, NULL, NULL - }, - - { - {"ssl_groups", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Sets the group(s) to use for Diffie-Hellman key exchange."), - gettext_noop("Multiple groups can be specified using colon-separated list."), - GUC_SUPERUSER_ONLY - }, - &SSLECDHCurve, -#ifdef USE_SSL - "X25519:prime256v1", -#else - "none", -#endif - NULL, NULL, NULL - }, - - { - {"ssl_dh_params_file", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Location of the SSL DH parameters file."), - gettext_noop("An empty string means use compiled-in default parameters."), - GUC_SUPERUSER_ONLY - }, - &ssl_dh_params_file, - "", - NULL, NULL, NULL - }, - - { - {"ssl_passphrase_command", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Command to obtain passphrases for SSL."), - gettext_noop("An empty string means use the built-in prompting mechanism."), - GUC_SUPERUSER_ONLY - }, - &ssl_passphrase_command, - "", - NULL, NULL, NULL - }, - - { - {"application_name", PGC_USERSET, LOGGING_WHAT, - gettext_noop("Sets the application name to be reported in statistics and logs."), - NULL, - GUC_IS_NAME | GUC_REPORT | GUC_NOT_IN_SAMPLE - }, - &application_name, - "", - check_application_name, assign_application_name, NULL - }, - - { - {"cluster_name", PGC_POSTMASTER, PROCESS_TITLE, - gettext_noop("Sets the name of the cluster, which is included in the process title."), - NULL, - GUC_IS_NAME - }, - &cluster_name, - "", - check_cluster_name, NULL, NULL - }, - - { - {"wal_consistency_checking", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Sets the WAL resource managers for which WAL consistency checks are done."), - gettext_noop("Full-page images will be logged for all data blocks and cross-checked against the results of WAL replay."), - GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE - }, - &wal_consistency_checking_string, - "", - check_wal_consistency_checking, assign_wal_consistency_checking, NULL - }, - - { - {"jit_provider", PGC_POSTMASTER, CLIENT_CONN_PRELOAD, - gettext_noop("JIT provider to use."), - NULL, - GUC_SUPERUSER_ONLY - }, - &jit_provider, - "llvmjit", - NULL, NULL, NULL - }, - - { - {"backtrace_functions", PGC_SUSET, DEVELOPER_OPTIONS, - gettext_noop("Log backtrace for errors in these functions."), - NULL, - GUC_NOT_IN_SAMPLE - }, - &backtrace_functions, - "", - check_backtrace_functions, assign_backtrace_functions, NULL - }, - - { - {"debug_io_direct", PGC_POSTMASTER, DEVELOPER_OPTIONS, - gettext_noop("Use direct I/O for file access."), - gettext_noop("An empty string disables direct I/O."), - GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE - }, - &debug_io_direct_string, - "", - check_debug_io_direct, assign_debug_io_direct, NULL - }, - - { - {"synchronized_standby_slots", PGC_SIGHUP, REPLICATION_PRIMARY, - gettext_noop("Lists streaming replication standby server replication slot " - "names that logical WAL sender processes will wait for."), - gettext_noop("Logical WAL sender processes will send decoded " - "changes to output plugins only after the specified " - "replication slots have confirmed receiving WAL."), - GUC_LIST_INPUT - }, - &synchronized_standby_slots, - "", - check_synchronized_standby_slots, assign_synchronized_standby_slots, NULL - }, - - { - {"restrict_nonsystem_relation_kind", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Prohibits access to non-system relations of specified kinds."), - NULL, - GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE - }, - &restrict_nonsystem_relation_kind_string, - "", - check_restrict_nonsystem_relation_kind, assign_restrict_nonsystem_relation_kind, NULL - }, - - { - {"oauth_validator_libraries", PGC_SIGHUP, CONN_AUTH_AUTH, - gettext_noop("Lists libraries that may be called to validate OAuth v2 bearer tokens."), - NULL, - GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY - }, - &oauth_validator_libraries_string, - "", - NULL, NULL, NULL - }, - - { - {"log_connections", PGC_SU_BACKEND, LOGGING_WHAT, - gettext_noop("Logs specified aspects of connection establishment and setup."), - NULL, - GUC_LIST_INPUT - }, - &log_connections_string, - "", - check_log_connections, assign_log_connections, NULL - }, - - - /* End-of-list marker */ - { - {NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL - } -}; - - -struct config_enum ConfigureNamesEnum[] = -{ - { - {"backslash_quote", PGC_USERSET, COMPAT_OPTIONS_PREVIOUS, - gettext_noop("Sets whether \"\\'\" is allowed in string literals."), - NULL - }, - &backslash_quote, - BACKSLASH_QUOTE_SAFE_ENCODING, backslash_quote_options, - NULL, NULL, NULL - }, - - { - {"bytea_output", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the output format for bytea."), - NULL - }, - &bytea_output, - BYTEA_OUTPUT_HEX, bytea_output_options, - NULL, NULL, NULL - }, - - { - {"client_min_messages", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the message levels that are sent to the client."), - gettext_noop("Each level includes all the levels that follow it. The later" - " the level, the fewer messages are sent.") - }, - &client_min_messages, - NOTICE, client_message_level_options, - NULL, NULL, NULL - }, - - { - {"compute_query_id", PGC_SUSET, STATS_MONITORING, - gettext_noop("Enables in-core computation of query identifiers."), - NULL - }, - &compute_query_id, - COMPUTE_QUERY_ID_AUTO, compute_query_id_options, - NULL, NULL, NULL - }, - - { - {"constraint_exclusion", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Enables the planner to use constraints to optimize queries."), - gettext_noop("Table scans will be skipped if their constraints" - " guarantee that no rows match the query."), - GUC_EXPLAIN - }, - &constraint_exclusion, - CONSTRAINT_EXCLUSION_PARTITION, constraint_exclusion_options, - NULL, NULL, NULL - }, - - { - {"default_toast_compression", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the default compression method for compressible values."), - NULL - }, - &default_toast_compression, - TOAST_PGLZ_COMPRESSION, - default_toast_compression_options, - NULL, NULL, NULL - }, - - { - {"default_transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the transaction isolation level of each new transaction."), - NULL - }, - &DefaultXactIsoLevel, - XACT_READ_COMMITTED, isolation_level_options, - NULL, NULL, NULL - }, - - { - {"transaction_isolation", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the current transaction's isolation level."), - NULL, - GUC_NO_RESET | GUC_NO_RESET_ALL | GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &XactIsoLevel, - XACT_READ_COMMITTED, isolation_level_options, - check_transaction_isolation, NULL, NULL - }, - - { - {"IntervalStyle", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Sets the display format for interval values."), - NULL, - GUC_REPORT - }, - &IntervalStyle, - INTSTYLE_POSTGRES, intervalstyle_options, - NULL, NULL, NULL - }, - - { - {"icu_validation_level", PGC_USERSET, CLIENT_CONN_LOCALE, - gettext_noop("Log level for reporting invalid ICU locale strings."), - NULL - }, - &icu_validation_level, - WARNING, icu_validation_level_options, - NULL, NULL, NULL - }, - - { - {"log_error_verbosity", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Sets the verbosity of logged messages."), - NULL - }, - &Log_error_verbosity, - PGERROR_DEFAULT, log_error_verbosity_options, - NULL, NULL, NULL - }, - - { - {"log_min_messages", PGC_SUSET, LOGGING_WHEN, - gettext_noop("Sets the message levels that are logged."), - gettext_noop("Each level includes all the levels that follow it. The later" - " the level, the fewer messages are sent.") - }, - &log_min_messages, - WARNING, server_message_level_options, - NULL, NULL, NULL - }, - - { - {"log_min_error_statement", PGC_SUSET, LOGGING_WHEN, - gettext_noop("Causes all statements generating error at or above this level to be logged."), - gettext_noop("Each level includes all the levels that follow it. The later" - " the level, the fewer messages are sent.") - }, - &log_min_error_statement, - ERROR, server_message_level_options, - NULL, NULL, NULL - }, - - { - {"log_statement", PGC_SUSET, LOGGING_WHAT, - gettext_noop("Sets the type of statements logged."), - NULL - }, - &log_statement, - LOGSTMT_NONE, log_statement_options, - NULL, NULL, NULL - }, - - { - {"syslog_facility", PGC_SIGHUP, LOGGING_WHERE, - gettext_noop("Sets the syslog \"facility\" to be used when syslog enabled."), - NULL - }, - &syslog_facility, - DEFAULT_SYSLOG_FACILITY, - syslog_facility_options, - NULL, assign_syslog_facility, NULL - }, - - { - {"session_replication_role", PGC_SUSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets the session's behavior for triggers and rewrite rules."), - NULL - }, - &SessionReplicationRole, - SESSION_REPLICATION_ROLE_ORIGIN, session_replication_role_options, - NULL, assign_session_replication_role, NULL - }, - - { - {"synchronous_commit", PGC_USERSET, WAL_SETTINGS, - gettext_noop("Sets the current transaction's synchronization level."), - NULL - }, - &synchronous_commit, - SYNCHRONOUS_COMMIT_ON, synchronous_commit_options, - NULL, assign_synchronous_commit, NULL - }, - - { - {"archive_mode", PGC_POSTMASTER, WAL_ARCHIVING, - gettext_noop("Allows archiving of WAL files using \"archive_command\"."), - NULL - }, - &XLogArchiveMode, - ARCHIVE_MODE_OFF, archive_mode_options, - NULL, NULL, NULL - }, - - { - {"recovery_target_action", PGC_POSTMASTER, WAL_RECOVERY_TARGET, - gettext_noop("Sets the action to perform upon reaching the recovery target."), - NULL - }, - &recoveryTargetAction, - RECOVERY_TARGET_ACTION_PAUSE, recovery_target_action_options, - NULL, NULL, NULL - }, - - { - {"track_functions", PGC_SUSET, STATS_CUMULATIVE, - gettext_noop("Collects function-level statistics on database activity."), - NULL - }, - &pgstat_track_functions, - TRACK_FUNC_OFF, track_function_options, - NULL, NULL, NULL - }, - - - { - {"stats_fetch_consistency", PGC_USERSET, STATS_CUMULATIVE, - gettext_noop("Sets the consistency of accesses to statistics data."), - NULL - }, - &pgstat_fetch_consistency, - PGSTAT_FETCH_CONSISTENCY_CACHE, stats_fetch_consistency, - NULL, assign_stats_fetch_consistency, NULL - }, - - { - {"wal_compression", PGC_SUSET, WAL_SETTINGS, - gettext_noop("Compresses full-page writes written in WAL file with specified method."), - NULL - }, - &wal_compression, - WAL_COMPRESSION_NONE, wal_compression_options, - NULL, NULL, NULL - }, - - { - {"wal_level", PGC_POSTMASTER, WAL_SETTINGS, - gettext_noop("Sets the level of information written to the WAL."), - NULL - }, - &wal_level, - WAL_LEVEL_REPLICA, wal_level_options, - NULL, NULL, NULL - }, - - { - {"dynamic_shared_memory_type", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Selects the dynamic shared memory implementation used."), - NULL - }, - &dynamic_shared_memory_type, - DEFAULT_DYNAMIC_SHARED_MEMORY_TYPE, dynamic_shared_memory_options, - NULL, NULL, NULL - }, - - { - {"shared_memory_type", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Selects the shared memory implementation used for the main shared memory region."), - NULL - }, - &shared_memory_type, - DEFAULT_SHARED_MEMORY_TYPE, shared_memory_options, - NULL, NULL, NULL - }, - - { - {"file_copy_method", PGC_USERSET, RESOURCES_DISK, - gettext_noop("Selects the file copy method."), - NULL - }, - &file_copy_method, - FILE_COPY_METHOD_COPY, file_copy_method_options, - NULL, NULL, NULL - }, - - { - {"wal_sync_method", PGC_SIGHUP, WAL_SETTINGS, - gettext_noop("Selects the method used for forcing WAL updates to disk."), - NULL - }, - &wal_sync_method, - DEFAULT_WAL_SYNC_METHOD, wal_sync_method_options, - NULL, assign_wal_sync_method, NULL - }, - - { - {"xmlbinary", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets how binary values are to be encoded in XML."), - NULL - }, - &xmlbinary, - XMLBINARY_BASE64, xmlbinary_options, - NULL, NULL, NULL - }, - - { - {"xmloption", PGC_USERSET, CLIENT_CONN_STATEMENT, - gettext_noop("Sets whether XML data in implicit parsing and serialization " - "operations is to be considered as documents or content fragments."), - NULL - }, - &xmloption, - XMLOPTION_CONTENT, xmloption_options, - NULL, NULL, NULL - }, - - { - {"huge_pages", PGC_POSTMASTER, RESOURCES_MEM, - gettext_noop("Use of huge pages on Linux or Windows."), - NULL - }, - &huge_pages, - HUGE_PAGES_TRY, huge_pages_options, - NULL, NULL, NULL - }, - - { - {"huge_pages_status", PGC_INTERNAL, PRESET_OPTIONS, - gettext_noop("Indicates the status of huge pages."), - NULL, - GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE - }, - &huge_pages_status, - HUGE_PAGES_UNKNOWN, huge_pages_status_options, - NULL, NULL, NULL - }, - - { - {"recovery_prefetch", PGC_SIGHUP, WAL_RECOVERY, - gettext_noop("Prefetch referenced blocks during recovery."), - gettext_noop("Look ahead in the WAL to find references to uncached data.") - }, - &recovery_prefetch, - RECOVERY_PREFETCH_TRY, recovery_prefetch_options, - check_recovery_prefetch, assign_recovery_prefetch, NULL - }, - - { - {"debug_parallel_query", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Forces the planner's use parallel query nodes."), - gettext_noop("This can be useful for testing the parallel query infrastructure " - "by forcing the planner to generate plans that contain nodes " - "that perform tuple communication between workers and the main process."), - GUC_NOT_IN_SAMPLE | GUC_EXPLAIN - }, - &debug_parallel_query, - DEBUG_PARALLEL_OFF, debug_parallel_query_options, - NULL, NULL, NULL - }, - - { - {"password_encryption", PGC_USERSET, CONN_AUTH_AUTH, - gettext_noop("Chooses the algorithm for encrypting passwords."), - NULL - }, - &Password_encryption, - PASSWORD_TYPE_SCRAM_SHA_256, password_encryption_options, - NULL, NULL, NULL - }, - - { - {"plan_cache_mode", PGC_USERSET, QUERY_TUNING_OTHER, - gettext_noop("Controls the planner's selection of custom or generic plan."), - gettext_noop("Prepared statements can have custom and generic plans, and the planner " - "will attempt to choose which is better. This can be set to override " - "the default behavior."), - GUC_EXPLAIN - }, - &plan_cache_mode, - PLAN_CACHE_MODE_AUTO, plan_cache_mode_options, - NULL, NULL, NULL - }, - - { - {"ssl_min_protocol_version", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Sets the minimum SSL/TLS protocol version to use."), - NULL, - GUC_SUPERUSER_ONLY - }, - &ssl_min_protocol_version, - PG_TLS1_2_VERSION, - ssl_protocol_versions_info + 1, /* don't allow PG_TLS_ANY */ - NULL, NULL, NULL - }, - - { - {"ssl_max_protocol_version", PGC_SIGHUP, CONN_AUTH_SSL, - gettext_noop("Sets the maximum SSL/TLS protocol version to use."), - NULL, - GUC_SUPERUSER_ONLY - }, - &ssl_max_protocol_version, - PG_TLS_ANY, - ssl_protocol_versions_info, - NULL, NULL, NULL - }, - - { - {"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS, - gettext_noop("Sets the method for synchronizing the data directory before crash recovery."), - }, - &recovery_init_sync_method, - DATA_DIR_SYNC_METHOD_FSYNC, recovery_init_sync_method_options, - NULL, NULL, NULL - }, - - { - {"debug_logical_replication_streaming", PGC_USERSET, DEVELOPER_OPTIONS, - gettext_noop("Forces immediate streaming or serialization of changes in large transactions."), - gettext_noop("On the publisher, it allows streaming or serializing each change in logical decoding. " - "On the subscriber, it allows serialization of all changes to files and notifies the " - "parallel apply workers to read and apply them at the end of the transaction."), - GUC_NOT_IN_SAMPLE - }, - &debug_logical_replication_streaming, - DEBUG_LOGICAL_REP_STREAMING_BUFFERED, debug_logical_replication_streaming_options, - NULL, NULL, NULL - }, - - { - {"io_method", PGC_POSTMASTER, RESOURCES_IO, - gettext_noop("Selects the method for executing asynchronous I/O."), - NULL - }, - &io_method, - DEFAULT_IO_METHOD, io_method_options, - NULL, assign_io_method, NULL - }, - - /* End-of-list marker */ - { - {NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL - } -}; +#include "utils/guc_tables.inc.c" diff --git a/src/backend/utils/misc/help_config.c b/src/backend/utils/misc/help_config.c index 55c36ddf051d5..f7bf70d675cbb 100644 --- a/src/backend/utils/misc/help_config.c +++ b/src/backend/utils/misc/help_config.c @@ -7,7 +7,7 @@ * or GUC_DISALLOW_IN_FILE are not displayed, unless the user specifically * requests that variable by name * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/misc/help_config.c @@ -23,40 +23,24 @@ #include "utils/help_config.h" -/* - * This union allows us to mix the numerous different types of structs - * that we are organizing. - */ -typedef union -{ - struct config_generic generic; - struct config_bool _bool; - struct config_real real; - struct config_int integer; - struct config_string string; - struct config_enum _enum; -} mixedStruct; - - -static void printMixedStruct(mixedStruct *structToPrint); -static bool displayStruct(mixedStruct *structToDisplay); +static void printMixedStruct(const struct config_generic *structToPrint); +static bool displayStruct(const struct config_generic *structToDisplay); void GucInfoMain(void) { struct config_generic **guc_vars; - int numOpts, - i; + int numOpts; /* Initialize the GUC hash table */ build_guc_variables(); guc_vars = get_guc_variables(&numOpts); - for (i = 0; i < numOpts; i++) + for (int i = 0; i < numOpts; i++) { - mixedStruct *var = (mixedStruct *) guc_vars[i]; + const struct config_generic *var = guc_vars[i]; if (displayStruct(var)) printMixedStruct(var); @@ -71,11 +55,11 @@ GucInfoMain(void) * should be displayed to the user. */ static bool -displayStruct(mixedStruct *structToDisplay) +displayStruct(const struct config_generic *structToDisplay) { - return !(structToDisplay->generic.flags & (GUC_NO_SHOW_ALL | - GUC_NOT_IN_SAMPLE | - GUC_DISALLOW_IN_FILE)); + return !(structToDisplay->flags & (GUC_NO_SHOW_ALL | + GUC_NOT_IN_SAMPLE | + GUC_DISALLOW_IN_FILE)); } @@ -84,14 +68,14 @@ displayStruct(mixedStruct *structToDisplay) * a different format, depending on what the user wants to see. */ static void -printMixedStruct(mixedStruct *structToPrint) +printMixedStruct(const struct config_generic *structToPrint) { printf("%s\t%s\t%s\t", - structToPrint->generic.name, - GucContext_Names[structToPrint->generic.context], - _(config_group_names[structToPrint->generic.group])); + structToPrint->name, + GucContext_Names[structToPrint->context], + _(config_group_names[structToPrint->group])); - switch (structToPrint->generic.vartype) + switch (structToPrint->vartype) { case PGC_BOOL: @@ -102,26 +86,26 @@ printMixedStruct(mixedStruct *structToPrint) case PGC_INT: printf("INTEGER\t%d\t%d\t%d\t", - structToPrint->integer.reset_val, - structToPrint->integer.min, - structToPrint->integer.max); + structToPrint->_int.reset_val, + structToPrint->_int.min, + structToPrint->_int.max); break; case PGC_REAL: printf("REAL\t%g\t%g\t%g\t", - structToPrint->real.reset_val, - structToPrint->real.min, - structToPrint->real.max); + structToPrint->_real.reset_val, + structToPrint->_real.min, + structToPrint->_real.max); break; case PGC_STRING: printf("STRING\t%s\t\t\t", - structToPrint->string.boot_val ? structToPrint->string.boot_val : ""); + structToPrint->_string.boot_val ? structToPrint->_string.boot_val : ""); break; case PGC_ENUM: printf("ENUM\t%s\t\t\t", - config_enum_lookup_by_value(&structToPrint->_enum, + config_enum_lookup_by_value(structToPrint, structToPrint->_enum.boot_val)); break; @@ -131,6 +115,6 @@ printMixedStruct(mixedStruct *structToPrint) } printf("%s\t%s\n", - (structToPrint->generic.short_desc == NULL) ? "" : _(structToPrint->generic.short_desc), - (structToPrint->generic.long_desc == NULL) ? "" : _(structToPrint->generic.long_desc)); + (structToPrint->short_desc == NULL) ? "" : _(structToPrint->short_desc), + (structToPrint->long_desc == NULL) ? "" : _(structToPrint->long_desc)); } diff --git a/src/backend/utils/misc/injection_point.c b/src/backend/utils/misc/injection_point.c index f58ebc8ee522d..272ef5e578ad0 100644 --- a/src/backend/utils/misc/injection_point.c +++ b/src/backend/utils/misc/injection_point.c @@ -6,7 +6,7 @@ * Injection points can be used to run arbitrary code by attaching callbacks * that would be executed in place of the named injection point. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -28,6 +28,7 @@ #include "storage/fd.h" #include "storage/lwlock.h" #include "storage/shmem.h" +#include "storage/subsystems.h" #include "utils/hsearch.h" #include "utils/memutils.h" @@ -109,6 +110,9 @@ typedef struct InjectionPointCacheEntry static HTAB *InjectionPointCache = NULL; +static void InjectionPointShmemRequest(void *arg); +static void InjectionPointShmemInit(void *arg); + /* * injection_point_cache_add * @@ -186,7 +190,7 @@ injection_point_cache_load(InjectionPointEntry *entry, int slot_idx, uint64 gene elog(ERROR, "could not find library \"%s\" for injection point \"%s\"", path, entry->name); - injection_callback_local = (void *) + injection_callback_local = load_external_function(path, entry->function, false, NULL); if (injection_callback_local == NULL) @@ -224,47 +228,32 @@ injection_point_cache_get(const char *name) return NULL; } -#endif /* USE_INJECTION_POINTS */ + +const ShmemCallbacks InjectionPointShmemCallbacks = { + .request_fn = InjectionPointShmemRequest, + .init_fn = InjectionPointShmemInit, +}; /* - * Return the space for dynamic shared hash table. + * Reserve space for the dynamic shared hash table */ -Size -InjectionPointShmemSize(void) +static void +InjectionPointShmemRequest(void *arg) { -#ifdef USE_INJECTION_POINTS - Size sz = 0; - - sz = add_size(sz, sizeof(InjectionPointsCtl)); - return sz; -#else - return 0; -#endif + ShmemRequestStruct(.name = "InjectionPoint hash", + .size = sizeof(InjectionPointsCtl), + .ptr = (void **) &ActiveInjectionPoints, + ); } -/* - * Allocate shmem space for dynamic shared hash. - */ -void -InjectionPointShmemInit(void) +static void +InjectionPointShmemInit(void *arg) { -#ifdef USE_INJECTION_POINTS - bool found; - - ActiveInjectionPoints = ShmemInitStruct("InjectionPoint hash", - sizeof(InjectionPointsCtl), - &found); - if (!IsUnderPostmaster) - { - Assert(!found); - pg_atomic_init_u32(&ActiveInjectionPoints->max_inuse, 0); - for (int i = 0; i < MAX_INJECTION_POINTS; i++) - pg_atomic_init_u64(&ActiveInjectionPoints->entries[i].generation, 0); - } - else - Assert(found); -#endif + pg_atomic_init_u32(&ActiveInjectionPoints->max_inuse, 0); + for (int i = 0; i < MAX_INJECTION_POINTS; i++) + pg_atomic_init_u64(&ActiveInjectionPoints->entries[i].generation, 0); } +#endif /* USE_INJECTION_POINTS */ /* * Attach a new injection point. @@ -283,16 +272,16 @@ InjectionPointAttach(const char *name, int free_idx; if (strlen(name) >= INJ_NAME_MAXLEN) - elog(ERROR, "injection point name %s too long (maximum of %u)", - name, INJ_NAME_MAXLEN); + elog(ERROR, "injection point name %s too long (maximum of %u characters)", + name, INJ_NAME_MAXLEN - 1); if (strlen(library) >= INJ_LIB_MAXLEN) - elog(ERROR, "injection point library %s too long (maximum of %u)", - library, INJ_LIB_MAXLEN); + elog(ERROR, "injection point library %s too long (maximum of %u characters)", + library, INJ_LIB_MAXLEN - 1); if (strlen(function) >= INJ_FUNC_MAXLEN) - elog(ERROR, "injection point function %s too long (maximum of %u)", - function, INJ_FUNC_MAXLEN); - if (private_data_size >= INJ_PRIVATE_MAXLEN) - elog(ERROR, "injection point data too long (maximum of %u)", + elog(ERROR, "injection point function %s too long (maximum of %u characters)", + function, INJ_FUNC_MAXLEN - 1); + if (private_data_size > INJ_PRIVATE_MAXLEN) + elog(ERROR, "injection point data too long (maximum of %u bytes)", INJ_PRIVATE_MAXLEN); /* @@ -331,11 +320,9 @@ InjectionPointAttach(const char *name, /* Save the entry */ strlcpy(entry->name, name, sizeof(entry->name)); - entry->name[INJ_NAME_MAXLEN - 1] = '\0'; strlcpy(entry->library, library, sizeof(entry->library)); - entry->library[INJ_LIB_MAXLEN - 1] = '\0'; strlcpy(entry->function, function, sizeof(entry->function)); - entry->function[INJ_FUNC_MAXLEN - 1] = '\0'; + memset(entry->private_data, 0, INJ_PRIVATE_MAXLEN); if (private_data != NULL) memcpy(entry->private_data, private_data, private_data_size); @@ -584,3 +571,49 @@ IsInjectionPointAttached(const char *name) return false; /* silence compiler */ #endif } + +/* + * Retrieve a list of all the injection points currently attached. + * + * This list is palloc'd in the current memory context. + */ +List * +InjectionPointList(void) +{ +#ifdef USE_INJECTION_POINTS + List *inj_points = NIL; + uint32 max_inuse; + + LWLockAcquire(InjectionPointLock, LW_SHARED); + + max_inuse = pg_atomic_read_u32(&ActiveInjectionPoints->max_inuse); + + for (uint32 idx = 0; idx < max_inuse; idx++) + { + InjectionPointEntry *entry; + InjectionPointData *inj_point; + uint64 generation; + + entry = &ActiveInjectionPoints->entries[idx]; + generation = pg_atomic_read_u64(&entry->generation); + + /* skip free slots */ + if (generation % 2 == 0) + continue; + + inj_point = palloc0_object(InjectionPointData); + inj_point->name = pstrdup(entry->name); + inj_point->library = pstrdup(entry->library); + inj_point->function = pstrdup(entry->function); + inj_points = lappend(inj_points, inj_point); + } + + LWLockRelease(InjectionPointLock); + + return inj_points; + +#else + elog(ERROR, "Injection points are not supported by this build"); + return NIL; /* keep compiler quiet */ +#endif +} diff --git a/src/backend/utils/misc/meson.build b/src/backend/utils/misc/meson.build index 9e389a00d0576..232e74d0af90f 100644 --- a/src/backend/utils/misc/meson.build +++ b/src/backend/utils/misc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'conffiles.c', diff --git a/src/backend/utils/misc/pg_config.c b/src/backend/utils/misc/pg_config.c index e64e6758c9319..1d9d7985cf1ee 100644 --- a/src/backend/utils/misc/pg_config.c +++ b/src/backend/utils/misc/pg_config.c @@ -3,7 +3,7 @@ * pg_config.c * Expose same output as pg_config except as an SRF * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -18,6 +18,7 @@ #include "funcapi.h" #include "miscadmin.h" #include "utils/builtins.h" +#include "utils/tuplestore.h" Datum pg_config(PG_FUNCTION_ARGS) diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c index 6d036e3bf3280..c6d9cbb1577fa 100644 --- a/src/backend/utils/misc/pg_controldata.c +++ b/src/backend/utils/misc/pg_controldata.c @@ -5,7 +5,7 @@ * Routines to expose the contents of the control data file via * a set of SQL functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/misc/pg_rusage.c b/src/backend/utils/misc/pg_rusage.c index feac22b37a9ca..c8cf9d47872a1 100644 --- a/src/backend/utils/misc/pg_rusage.c +++ b/src/backend/utils/misc/pg_rusage.c @@ -4,7 +4,7 @@ * Resource usage measurement support routines. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 63f991c4f9305..ac38cddaaf9a6 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -39,16 +39,18 @@ # The default values of these variables are driven from the -D command-line # option or PGDATA environment variable, represented here as ConfigDir. -#data_directory = 'ConfigDir' # use data in another directory - # (change requires restart) -#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file - # (change requires restart) -#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file - # (change requires restart) +#data_directory = 'ConfigDir' # use data in another directory + # (change requires restart) +#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file + # (change requires restart) +#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file + # (change requires restart) +#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file + # (change requires restart) # If external_pid_file is not explicitly set, no extra PID file is written. -#external_pid_file = '' # write an extra PID file - # (change requires restart) +#external_pid_file = '' # write an extra PID file + # (change requires restart) #------------------------------------------------------------------------------ @@ -57,47 +59,48 @@ # - Connection Settings - -#listen_addresses = 'localhost' # what IP address(es) to listen on; - # comma-separated list of addresses; - # defaults to 'localhost'; use '*' for all - # (change requires restart) -#port = 5432 # (change requires restart) -#max_connections = 100 # (change requires restart) -#reserved_connections = 0 # (change requires restart) -#superuser_reserved_connections = 3 # (change requires restart) -#unix_socket_directories = '/tmp' # comma-separated list of directories - # (change requires restart) -#unix_socket_group = '' # (change requires restart) -#unix_socket_permissions = 0777 # begin with 0 to use octal notation - # (change requires restart) -#bonjour = off # advertise server via Bonjour - # (change requires restart) -#bonjour_name = '' # defaults to the computer name - # (change requires restart) +#listen_addresses = 'localhost' # what IP address(es) to listen on; + # comma-separated list of addresses; + # defaults to 'localhost'; use '*' for all + # (change requires restart) +#port = 5432 # (change requires restart) +#max_connections = 100 # (change requires restart) +#reserved_connections = 0 # (change requires restart) +#superuser_reserved_connections = 3 # (change requires restart) +#unix_socket_directories = '/tmp' # comma-separated list of directories + # (change requires restart) +#unix_socket_group = '' # (change requires restart) +#unix_socket_permissions = 0777 # begin with 0 to use octal notation + # (change requires restart) +#bonjour = off # advertise server via Bonjour + # (change requires restart) +#bonjour_name = '' # defaults to the computer name + # (change requires restart) # - TCP settings - # see "man tcp" for details -#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; - # 0 selects the system default -#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; - # 0 selects the system default -#tcp_keepalives_count = 0 # TCP_KEEPCNT; - # 0 selects the system default -#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; - # 0 selects the system default +#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; + # 0 selects the system default +#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; + # 0 selects the system default +#tcp_keepalives_count = 0 # TCP_KEEPCNT; + # 0 selects the system default +#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; + # 0 selects the system default -#client_connection_check_interval = 0 # time between checks for client - # disconnection while running queries; - # 0 for never +#client_connection_check_interval = 0 # time between checks for client + # disconnection while running queries; + # 0 for never # - Authentication - -#authentication_timeout = 1min # 1s-600s -#password_encryption = scram-sha-256 # scram-sha-256 or md5 +#authentication_timeout = 1min # 1s-600s +#password_encryption = scram-sha-256 # scram-sha-256 or (deprecated) md5 #scram_iterations = 4096 -#md5_password_warnings = on -#oauth_validator_libraries = '' # comma-separated list of trusted validator modules +#password_expiration_warning_threshold = 7d # threshold for expiration warnings +#md5_password_warnings = on # display md5 deprecation warnings? +#oauth_validator_libraries = '' # comma-separated list of trusted validator modules # GSSAPI using Kerberos #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab' @@ -112,8 +115,8 @@ #ssl_crl_file = '' #ssl_crl_dir = '' #ssl_key_file = 'server.key' -#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed TLSv1.2 ciphers -#ssl_tls13_ciphers = '' # allowed TLSv1.3 cipher suites, blank for default +#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed TLSv1.2 ciphers +#ssl_tls13_ciphers = '' # allowed TLSv1.3 cipher suites, blank for default #ssl_prefer_server_ciphers = on #ssl_groups = 'X25519:prime256v1' #ssl_min_protocol_version = 'TLSv1.2' @@ -121,6 +124,7 @@ #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = off +#ssl_sni = off #------------------------------------------------------------------------------ @@ -129,98 +133,108 @@ # - Memory - -#shared_buffers = 128MB # min 128kB - # (change requires restart) -#huge_pages = try # on, off, or try - # (change requires restart) -#huge_page_size = 0 # zero for system default - # (change requires restart) -#temp_buffers = 8MB # min 800kB -#max_prepared_transactions = 0 # zero disables the feature - # (change requires restart) +#shared_buffers = 128MB # min 128kB + # (change requires restart) +#huge_pages = try # on, off, or try + # (change requires restart) +#huge_page_size = 0 # zero for system default + # (change requires restart) +#temp_buffers = 8MB # min 800kB +#max_prepared_transactions = 0 # zero disables the feature + # (change requires restart) # Caution: it is not advisable to set max_prepared_transactions nonzero unless # you actively intend to use prepared transactions. -#work_mem = 4MB # min 64kB -#hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem -#maintenance_work_mem = 64MB # min 64kB -#autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem -#logical_decoding_work_mem = 64MB # min 64kB -#max_stack_depth = 2MB # min 100kB -#shared_memory_type = mmap # the default is the first option - # supported by the operating system: - # mmap - # sysv - # windows - # (change requires restart) -#dynamic_shared_memory_type = posix # the default is usually the first option - # supported by the operating system: - # posix - # sysv - # windows - # mmap - # (change requires restart) -#min_dynamic_shared_memory = 0MB # (change requires restart) -#vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring; - # 0 to disable vacuum buffer access strategy; - # range 128kB to 16GB +#work_mem = 4MB # min 64kB +#hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem +#maintenance_work_mem = 64MB # min 64kB +#autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem +#logical_decoding_work_mem = 64MB # min 64kB +#max_stack_depth = 2MB # min 100kB +#shared_memory_type = mmap # the default is the first option + # supported by the operating system: + # mmap + # sysv + # windows + # (change requires restart) +#dynamic_shared_memory_type = posix # the default is usually the first option + # supported by the operating system: + # posix + # sysv + # windows + # mmap + # (change requires restart) +#min_dynamic_shared_memory = 0MB # (change requires restart) +#vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring; + # 0 to disable vacuum buffer access strategy; + # range 128kB to 16GB # SLRU buffers (change requires restart) -#commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto) -#multixact_offset_buffers = 16 # memory for pg_multixact/offsets -#multixact_member_buffers = 32 # memory for pg_multixact/members -#notify_buffers = 16 # memory for pg_notify -#serializable_buffers = 32 # memory for pg_serial -#subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto) -#transaction_buffers = 0 # memory for pg_xact (0 = auto) +#commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto) +#multixact_offset_buffers = 16 # memory for pg_multixact/offsets +#multixact_member_buffers = 32 # memory for pg_multixact/members +#notify_buffers = 16 # memory for pg_notify +#serializable_buffers = 32 # memory for pg_serial +#subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto) +#transaction_buffers = 0 # memory for pg_xact (0 = auto) # - Disk - -#temp_file_limit = -1 # limits per-process temp file space - # in kilobytes, or -1 for no limit +#temp_file_limit = -1 # limits per-process temp file space + # in kilobytes, or -1 for no limit -#max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated - # for NOTIFY / LISTEN queue +#file_copy_method = copy # copy, clone (if supported by OS) +#file_extend_method = posix_fallocate # the default is the first option supported + # by the operating system: + # posix_fallocate (most Unix-like systems) + # write_zeros -#file_copy_method = copy # the default is the first option - # copy - # clone (if system support is available) +#max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated + # for NOTIFY / LISTEN queue # - Kernel Resources - -#max_files_per_process = 1000 # min 64 - # (change requires restart) +#max_files_per_process = 1000 # min 64 + # (change requires restart) + +# - Time - + +#timing_clock_source = auto # auto, system, tsc (if supported) # - Background Writer - -#bgwriter_delay = 200ms # 10-10000ms between rounds -#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables -#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round -#bgwriter_flush_after = 0 # measured in pages, 0 disables +#bgwriter_delay = 200ms # 10-10000ms between rounds +#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables +#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round +#bgwriter_flush_after = 0 # measured in pages, 0 disables # - I/O - -#backend_flush_after = 0 # measured in pages, 0 disables -#effective_io_concurrency = 16 # 1-1000; 0 disables issuing multiple simultaneous IO requests -#maintenance_io_concurrency = 16 # 1-1000; same as effective_io_concurrency -#io_max_combine_limit = 128kB # usually 1-128 blocks (depends on OS) - # (change requires restart) -#io_combine_limit = 128kB # usually 1-128 blocks (depends on OS) - -#io_method = worker # worker, io_uring, sync - # (change requires restart) -#io_max_concurrency = -1 # Max number of IOs that one process - # can execute simultaneously - # -1 sets based on shared_buffers - # (change requires restart) -#io_workers = 3 # 1-32; +#backend_flush_after = 0 # measured in pages, 0 disables +#effective_io_concurrency = 16 # 1-1000; 0 disables issuing multiple simultaneous IO requests +#maintenance_io_concurrency = 16 # 1-1000; same as effective_io_concurrency +#io_max_combine_limit = 128kB # usually 1-128 blocks (depends on OS) + # (change requires restart) +#io_combine_limit = 128kB # usually 1-128 blocks (depends on OS) + +#io_method = worker # worker, io_uring, sync + # (change requires restart) +#io_max_concurrency = -1 # Max number of IOs that one process + # can execute simultaneously + # -1 sets based on shared_buffers + # (change requires restart) + +#io_min_workers = 2 # 1-32 +#io_max_workers = 8 # 1-32 +#io_worker_idle_timeout = 60s +#io_worker_launch_interval = 100ms # - Worker Processes - -#max_worker_processes = 8 # (change requires restart) -#max_parallel_workers_per_gather = 2 # limited by max_parallel_workers -#max_parallel_maintenance_workers = 2 # limited by max_parallel_workers -#max_parallel_workers = 8 # number of max_worker_processes that - # can be used in parallel operations +#max_worker_processes = 8 # (change requires restart) +#max_parallel_workers_per_gather = 2 # limited by max_parallel_workers +#max_parallel_maintenance_workers = 2 # limited by max_parallel_workers +#max_parallel_workers = 8 # number of max_worker_processes that + # can be used in parallel operations #parallel_leader_participation = on @@ -230,104 +244,104 @@ # - Settings - -#wal_level = replica # minimal, replica, or logical - # (change requires restart) -#fsync = on # flush data to disk for crash safety - # (turning this off can cause - # unrecoverable data corruption) -#synchronous_commit = on # synchronization level; - # off, local, remote_write, remote_apply, or on -#wal_sync_method = fsync # the default is the first option - # supported by the operating system: - # open_datasync - # fdatasync (default on Linux and FreeBSD) - # fsync - # fsync_writethrough - # open_sync -#full_page_writes = on # recover from partial page writes -#wal_log_hints = off # also do full page writes of non-critical updates - # (change requires restart) -#wal_compression = off # enables compression of full-page writes; - # off, pglz, lz4, zstd, or on -#wal_init_zero = on # zero-fill new WAL files -#wal_recycle = on # recycle WAL files -#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers - # (change requires restart) -#wal_writer_delay = 200ms # 1-10000 milliseconds -#wal_writer_flush_after = 1MB # measured in pages, 0 disables +#wal_level = replica # minimal, replica, or logical + # (change requires restart) +#fsync = on # flush data to disk for crash safety + # (turning this off can cause + # unrecoverable data corruption) +#synchronous_commit = on # synchronization level; + # off, local, remote_write, remote_apply, or on +#wal_sync_method = fsync # the default is the first option + # supported by the operating system: + # open_datasync + # fdatasync (default on Linux and FreeBSD) + # fsync + # fsync_writethrough + # open_sync +#full_page_writes = on # recover from partial page writes +#wal_log_hints = off # also do full page writes of non-critical updates + # (change requires restart) +#wal_compression = off # enables compression of full-page writes; + # off, pglz, lz4, zstd, or on +#wal_init_zero = on # zero-fill new WAL files +#wal_recycle = on # recycle WAL files +#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers + # (change requires restart) +#wal_writer_delay = 200ms # 1-10000 milliseconds +#wal_writer_flush_after = 1MB # measured in pages, 0 disables #wal_skip_threshold = 2MB -#commit_delay = 0 # range 0-100000, in microseconds -#commit_siblings = 5 # range 1-1000 +#commit_delay = 0 # range 0-100000, in microseconds +#commit_siblings = 5 # range 0-1000 # - Checkpoints - -#checkpoint_timeout = 5min # range 30s-1d -#checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0 -#checkpoint_flush_after = 0 # measured in pages, 0 disables -#checkpoint_warning = 30s # 0 disables +#checkpoint_timeout = 5min # range 30s-1d +#checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0 +#checkpoint_flush_after = 0 # measured in pages, 0 disables +#checkpoint_warning = 30s # 0 disables #max_wal_size = 1GB #min_wal_size = 80MB # - Prefetching during recovery - -#recovery_prefetch = try # prefetch pages referenced in the WAL? -#wal_decode_buffer_size = 512kB # lookahead window used for prefetching - # (change requires restart) +#recovery_prefetch = try # prefetch pages referenced in the WAL? +#wal_decode_buffer_size = 512kB # lookahead window used for prefetching + # (change requires restart) # - Archiving - -#archive_mode = off # enables archiving; off, on, or always - # (change requires restart) -#archive_library = '' # library to use to archive a WAL file - # (empty string indicates archive_command should - # be used) -#archive_command = '' # command to use to archive a WAL file - # placeholders: %p = path of file to archive - # %f = file name only - # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' -#archive_timeout = 0 # force a WAL file switch after this - # number of seconds; 0 disables +#archive_mode = off # enables archiving; off, on, or always + # (change requires restart) +#archive_library = '' # library to use to archive a WAL file + # (empty string indicates archive_command should + # be used) +#archive_command = '' # command to use to archive a WAL file + # placeholders: %p = path of file to archive + # %f = file name only + # e.g. 'test ! -f "/mnt/server/archivedir/%f" && cp "%p" "/mnt/server/archivedir/%f"' +#archive_timeout = 0 # force a WAL file switch after this + # number of seconds; 0 disables # - Archive Recovery - # These are only used in recovery mode. -#restore_command = '' # command to use to restore an archived WAL file - # placeholders: %p = path of file to restore - # %f = file name only - # e.g. 'cp /mnt/server/archivedir/%f %p' -#archive_cleanup_command = '' # command to execute at every restartpoint -#recovery_end_command = '' # command to execute at completion of recovery +#restore_command = '' # command to use to restore an archived WAL file + # placeholders: %p = path of file to restore + # %f = file name only + # e.g. 'cp "/mnt/server/archivedir/%f" "%p"' +#archive_cleanup_command = '' # command to execute at every restartpoint +#recovery_end_command = '' # command to execute at completion of recovery # - Recovery Target - # Set these only when performing a targeted recovery. -#recovery_target = '' # 'immediate' to end recovery as soon as a - # consistent state is reached - # (change requires restart) -#recovery_target_name = '' # the named restore point to which recovery will proceed - # (change requires restart) -#recovery_target_time = '' # the time stamp up to which recovery will proceed - # (change requires restart) -#recovery_target_xid = '' # the transaction ID up to which recovery will proceed - # (change requires restart) -#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed - # (change requires restart) -#recovery_target_inclusive = on # Specifies whether to stop: - # just after the specified recovery target (on) - # just before the recovery target (off) - # (change requires restart) -#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID - # (change requires restart) -#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' - # (change requires restart) +#recovery_target = '' # 'immediate' to end recovery as soon as a + # consistent state is reached + # (change requires restart) +#recovery_target_name = '' # the named restore point to which recovery will proceed + # (change requires restart) +#recovery_target_time = '' # the time stamp up to which recovery will proceed + # (change requires restart) +#recovery_target_xid = '' # the transaction ID up to which recovery will proceed + # (change requires restart) +#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed + # (change requires restart) +#recovery_target_inclusive = on # Specifies whether to stop: + # just after the specified recovery target (on) + # just before the recovery target (off) + # (change requires restart) +#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID + # (change requires restart) +#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' + # (change requires restart) # - WAL Summarization - -#summarize_wal = off # run WAL summarizer process? -#wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never +#summarize_wal = off # run WAL summarizer process? +#wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never #------------------------------------------------------------------------------ @@ -338,66 +352,69 @@ # Set these on the primary and on any standby that will send replication data. -#max_wal_senders = 10 # max number of walsender processes - # (change requires restart) -#max_replication_slots = 10 # max number of replication slots - # (change requires restart) -#wal_keep_size = 0 # in megabytes; 0 disables -#max_slot_wal_keep_size = -1 # in megabytes; -1 disables -#idle_replication_slot_timeout = 0 # in minutes; 0 disables -#wal_sender_timeout = 60s # in milliseconds; 0 disables -#track_commit_timestamp = off # collect timestamp of transaction commit - # (change requires restart) +#max_wal_senders = 10 # max number of walsender processes + # (change requires restart) +#max_replication_slots = 10 # max number of replication slots + # (change requires restart) +#max_repack_replication_slots = 5 # max number of replication slots for REPACK + # (change requires restart) +#wal_keep_size = 0 # in megabytes; 0 disables +#max_slot_wal_keep_size = -1 # in megabytes; -1 disables +#idle_replication_slot_timeout = 0 # in seconds; 0 disables +#wal_sender_timeout = 60s # in milliseconds; 0 disables +#wal_sender_shutdown_timeout = -1 # in milliseconds; -1 disables +#track_commit_timestamp = off # collect timestamp of transaction commit + # (change requires restart) # - Primary Server - # These settings are ignored on a standby server. -#synchronous_standby_names = '' # standby servers that provide sync rep - # method to choose sync standbys, number of sync standbys, - # and comma-separated list of application_name - # from standby(s); '*' = all -#synchronized_standby_slots = '' # streaming replication standby server slot - # names that logical walsender processes will wait for +#synchronous_standby_names = '' # standby servers that provide sync rep + # method to choose sync standbys, number of sync standbys, + # and comma-separated list of application_name + # from standby(s); '*' = all +#synchronized_standby_slots = '' # streaming replication standby server slot + # names that logical walsender processes will wait for # - Standby Servers - # These settings are ignored on a primary server. -#primary_conninfo = '' # connection string to sending server -#primary_slot_name = '' # replication slot on sending server -#hot_standby = on # "off" disallows queries during recovery - # (change requires restart) -#max_standby_archive_delay = 30s # max delay before canceling queries - # when reading WAL from archive; - # -1 allows indefinite delay -#max_standby_streaming_delay = 30s # max delay before canceling queries - # when reading streaming WAL; - # -1 allows indefinite delay -#wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name - # is not set -#wal_receiver_status_interval = 10s # send replies at least this often - # 0 disables -#hot_standby_feedback = off # send info from standby to prevent - # query conflicts -#wal_receiver_timeout = 60s # time that receiver waits for - # communication from primary - # in milliseconds; 0 disables -#wal_retrieve_retry_interval = 5s # time to wait before retrying to - # retrieve WAL after a failed attempt -#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery -#sync_replication_slots = off # enables slot synchronization on the physical standby from the primary +#primary_conninfo = '' # connection string to sending server +#primary_slot_name = '' # replication slot on sending server +#hot_standby = on # "off" disallows queries during recovery + # (change requires restart) +#max_standby_archive_delay = 30s # max delay before canceling queries + # when reading WAL from archive; + # -1 allows indefinite delay +#max_standby_streaming_delay = 30s # max delay before canceling queries + # when reading streaming WAL; + # -1 allows indefinite delay +#wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name + # is not set +#wal_receiver_status_interval = 10s # send replies at least this often + # 0 disables +#hot_standby_feedback = off # send info from standby to prevent + # query conflicts +#wal_receiver_timeout = 60s # time that receiver waits for + # communication from primary + # in milliseconds; 0 disables +#wal_retrieve_retry_interval = 5s # time to wait before retrying to + # retrieve WAL after a failed attempt +#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery +#sync_replication_slots = off # enables slot synchronization on the physical standby from the primary # - Subscribers - # These settings are ignored on a publisher. -#max_active_replication_origins = 10 # max number of active replication origins - # (change requires restart) -#max_logical_replication_workers = 4 # taken from max_worker_processes - # (change requires restart) -#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers -#max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers +#max_active_replication_origins = 10 # max number of active replication origins + # (change requires restart) +#max_logical_replication_workers = 4 # taken from max_worker_processes + # (change requires restart) +#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers +#max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers #------------------------------------------------------------------------------ @@ -430,51 +447,53 @@ #enable_group_by_reordering = on #enable_distinct_reordering = on #enable_self_join_elimination = on +#enable_eager_aggregate = on # - Planner Cost Constants - -#seq_page_cost = 1.0 # measured on an arbitrary scale -#random_page_cost = 4.0 # same scale as above -#cpu_tuple_cost = 0.01 # same scale as above -#cpu_index_tuple_cost = 0.005 # same scale as above -#cpu_operator_cost = 0.0025 # same scale as above -#parallel_setup_cost = 1000.0 # same scale as above -#parallel_tuple_cost = 0.1 # same scale as above +#seq_page_cost = 1.0 # measured on an arbitrary scale +#random_page_cost = 4.0 # same scale as above +#cpu_tuple_cost = 0.01 # same scale as above +#cpu_index_tuple_cost = 0.005 # same scale as above +#cpu_operator_cost = 0.0025 # same scale as above +#parallel_setup_cost = 1000.0 # same scale as above +#parallel_tuple_cost = 0.1 # same scale as above #min_parallel_table_scan_size = 8MB #min_parallel_index_scan_size = 512kB #effective_cache_size = 4GB +#min_eager_agg_group_size = 8.0 -#jit_above_cost = 100000 # perform JIT compilation if available - # and query more expensive than this; - # -1 disables -#jit_inline_above_cost = 500000 # inline small functions if query is - # more expensive than this; -1 disables -#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if - # query is more expensive than this; - # -1 disables +#jit_above_cost = 100000 # perform JIT compilation if available + # and query more expensive than this; + # -1 disables +#jit_inline_above_cost = 500000 # inline small functions if query is + # more expensive than this; -1 disables +#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if + # query is more expensive than this; + # -1 disables # - Genetic Query Optimizer - #geqo = on #geqo_threshold = 12 -#geqo_effort = 5 # range 1-10 -#geqo_pool_size = 0 # selects default based on effort -#geqo_generations = 0 # selects default based on effort -#geqo_selection_bias = 2.0 # range 1.5-2.0 -#geqo_seed = 0.0 # range 0.0-1.0 +#geqo_effort = 5 # range 1-10 +#geqo_pool_size = 0 # selects default based on effort +#geqo_generations = 0 # selects default based on effort +#geqo_selection_bias = 2.0 # range 1.5-2.0 +#geqo_seed = 0.0 # range 0.0-1.0 # - Other Planner Options - -#default_statistics_target = 100 # range 1-10000 -#constraint_exclusion = partition # on, off, or partition -#cursor_tuple_fraction = 0.1 # range 0.0-1.0 +#default_statistics_target = 100 # range 1-10000 +#constraint_exclusion = partition # on, off, or partition +#cursor_tuple_fraction = 0.1 # range 0.0-1.0 #from_collapse_limit = 8 -#jit = on # allow JIT compilation -#join_collapse_limit = 8 # 1 disables collapsing of explicit - # JOIN clauses -#plan_cache_mode = auto # auto, force_generic_plan or - # force_custom_plan -#recursive_worktable_factor = 10.0 # range 0.001-1000000 +#jit = off # allow JIT compilation +#join_collapse_limit = 8 # 1 disables collapsing of explicit + # JOIN clauses +#plan_cache_mode = auto # auto, force_generic_plan or + # force_custom_plan +#recursive_worktable_factor = 10.0 # range 0.001-1000000 #------------------------------------------------------------------------------ @@ -483,38 +502,38 @@ # - Where to Log - -#log_destination = 'stderr' # Valid values are combinations of - # stderr, csvlog, jsonlog, syslog, and - # eventlog, depending on platform. - # csvlog and jsonlog require - # logging_collector to be on. +#log_destination = 'stderr' # Valid values are combinations of + # stderr, csvlog, jsonlog, syslog, and + # eventlog, depending on platform. + # csvlog and jsonlog require + # logging_collector to be on. # This is used when logging to stderr: -#logging_collector = off # Enable capturing of stderr, jsonlog, - # and csvlog into log files. Required - # to be on for csvlogs and jsonlogs. - # (change requires restart) +#logging_collector = off # Enable capturing of stderr, jsonlog, + # and csvlog into log files. Required + # to be on for csvlogs and jsonlogs. + # (change requires restart) # These are only used if logging_collector is on: -#log_directory = 'log' # directory where log files are written, - # can be absolute or relative to PGDATA -#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, - # can include strftime() escapes -#log_file_mode = 0600 # creation mode for log files, - # begin with 0 to use octal notation -#log_rotation_age = 1d # Automatic rotation of logfiles will - # happen after that time. 0 disables. -#log_rotation_size = 10MB # Automatic rotation of logfiles will - # happen after that much log output. - # 0 disables. -#log_truncate_on_rotation = off # If on, an existing log file with the - # same name as the new log file will be - # truncated rather than appended to. - # But such truncation only occurs on - # time-driven rotation, not on restarts - # or size-driven rotation. Default is - # off, meaning append to existing files - # in all cases. +#log_directory = 'log' # directory where log files are written, + # can be absolute or relative to PGDATA +#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, + # can include strftime() escapes +#log_file_mode = 0600 # creation mode for log files, + # begin with 0 to use octal notation +#log_rotation_age = 1d # Automatic rotation of logfiles will + # happen after that time. 0 disables. +#log_rotation_size = 10MB # Automatic rotation of logfiles will + # happen after that much log output. + # 0 disables. +#log_truncate_on_rotation = off # If on, an existing log file with the + # same name as the new log file will be + # truncated rather than appended to. + # But such truncation only occurs on + # time-driven rotation, not on restarts + # or size-driven rotation. Default is + # off, meaning append to existing files + # in all cases. # These are relevant when logging to syslog: #syslog_facility = 'LOCAL0' @@ -528,124 +547,144 @@ # - When to Log - -#log_min_messages = warning # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic - -#log_min_error_statement = error # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # info - # notice - # warning - # error - # log - # fatal - # panic (effectively off) - -#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements - # and their durations, > 0 logs only - # statements running at least this number - # of milliseconds - -#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements - # and their durations, > 0 logs only a sample of - # statements running at least this number - # of milliseconds; - # sample fraction is determined by log_statement_sample_rate - -#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding - # log_min_duration_sample to be logged; - # 1.0 logs all such statements, 0.0 never logs - - -#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements - # are logged regardless of their duration; 1.0 logs all - # statements from all transactions, 0.0 never logs - -#log_startup_progress_interval = 10s # Time between progress updates for - # long-running startup operations. - # 0 disables the feature, > 0 indicates - # the interval in milliseconds. +#log_min_messages = 'warning' # comma-separated list of + # process_type:level entries, plus + # one freestanding level as default. + # Valid process types are: + # archiver autovacuum + # backend bgworker + # bgwriter checkpointer + # checksums ioworker + # postmaster slotsyncworker + # startup syslogger + # walreceiver walsummarizer + # walwriter walsender + # + # Level values in order of decreasing + # detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic + +#log_min_error_statement = error # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic (effectively off) + +#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements + # and their durations, > 0 logs only + # statements running at least this number + # of milliseconds + +#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements + # and their durations, > 0 logs only a sample of + # statements running at least this number + # of milliseconds; + # sample fraction is determined by log_statement_sample_rate + +#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding + # log_min_duration_sample to be logged; + # 1.0 logs all such statements, 0.0 never logs + + +#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements + # are logged regardless of their duration; 1.0 logs all + # statements from all transactions, 0.0 never logs + +#log_startup_progress_interval = 10s # Time between progress updates for + # long-running startup operations. + # 0 disables the feature, > 0 indicates + # the interval in milliseconds. # - What to Log - +#debug_print_raw_parse = off #debug_print_parse = off #debug_print_rewritten = off #debug_print_plan = off #debug_pretty_print = on -#log_autovacuum_min_duration = 10min # log autovacuum activity; - # -1 disables, 0 logs all actions and - # their durations, > 0 logs only - # actions running at least this number - # of milliseconds. +#log_autovacuum_min_duration = 10min # log vacuum activity by autovacuum; + # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#log_autoanalyze_min_duration = 10min # log analyze activity by autovacuum; + # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. #log_checkpoints = on #log_connections = '' # log aspects of connection setup # options include receipt, authentication, authorization, # setup_durations, and all to log all of these aspects #log_disconnections = off #log_duration = off # log statement duration -#log_error_verbosity = default # terse, default, or verbose messages +#log_error_verbosity = default # terse, default, or verbose messages #log_hostname = off -#log_line_prefix = '%m [%p] ' # special values: - # %a = application name - # %u = user name - # %d = database name - # %r = remote host and port - # %h = remote host - # %L = local address - # %b = backend type - # %p = process ID - # %P = process ID of parallel group leader - # %t = timestamp without milliseconds - # %m = timestamp with milliseconds - # %n = timestamp with milliseconds (as a Unix epoch) - # %Q = query ID (0 if none or not computed) - # %i = command tag - # %e = SQL state - # %c = session ID - # %l = session line number - # %s = session start timestamp - # %v = virtual transaction ID - # %x = transaction ID (0 if none) - # %q = stop here in non-session - # processes - # %% = '%' - # e.g. '<%u%%%d> ' -#log_lock_waits = off # log lock waits >= deadlock_timeout -#log_lock_failure = off # log lock failures -#log_recovery_conflict_waits = off # log standby recovery conflict waits - # >= deadlock_timeout -#log_parameter_max_length = -1 # when logging statements, limit logged - # bind-parameter values to N bytes; - # -1 means print in full, 0 disables -#log_parameter_max_length_on_error = 0 # when logging an error, limit logged - # bind-parameter values to N bytes; - # -1 means print in full, 0 disables -#log_statement = 'none' # none, ddl, mod, all +#log_line_prefix = '%m [%p] ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %L = local address + # %b = backend type + # %p = process ID + # %P = process ID of parallel group leader + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %Q = query ID (0 if none or not computed) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = on # log lock waits >= deadlock_timeout +#log_lock_failures = off # log lock failures +#log_recovery_conflict_waits = off # log standby recovery conflict waits + # >= deadlock_timeout +#log_parameter_max_length = -1 # when logging statements, limit logged + # bind-parameter values to N bytes; + # -1 means print in full, 0 disables +#log_parameter_max_length_on_error = 0 # when logging an error, limit logged + # bind-parameter values to N bytes; + # -1 means print in full, 0 disables +#log_statement = 'none' # none, ddl, mod, all #log_replication_commands = off -#log_temp_files = -1 # log temporary files equal or larger - # than the specified size in kilobytes; - # -1 disables, 0 logs all temp files +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files #log_timezone = 'GMT' # - Process Title - -#cluster_name = '' # added to process titles if nonempty - # (change requires restart) +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) #update_process_title = on @@ -656,13 +695,13 @@ # - Cumulative Query and Index Statistics - #track_activities = on -#track_activity_query_size = 1024 # (change requires restart) +#track_activity_query_size = 1024 # (change requires restart) #track_counts = on #track_cost_delay_timing = off #track_io_timing = off #track_wal_io_timing = off -#track_functions = none # none, pl, all -#stats_fetch_consistency = cache # cache, none, snapshot +#track_functions = none # none, pl, all +#stats_fetch_consistency = cache # cache, none, snapshot # - Monitoring - @@ -680,49 +719,55 @@ # - Automatic Vacuuming - -#autovacuum = on # Enable autovacuum subprocess? 'on' - # requires track_counts to also be on. -autovacuum_worker_slots = 16 # autovacuum worker slots to allocate - # (change requires restart) -#autovacuum_max_workers = 3 # max number of autovacuum subprocesses -#autovacuum_naptime = 1min # time between autovacuum runs -#autovacuum_vacuum_threshold = 50 # min number of row updates before - # vacuum -#autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts - # before vacuum; -1 disables insert - # vacuums -#autovacuum_analyze_threshold = 50 # min number of row updates before - # analyze -#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum -#autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of unfrozen pages +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#autovacuum_worker_slots = 16 # autovacuum worker slots to allocate + # (change requires restart) +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses +#autovacuum_max_parallel_workers = 0 # limited by max_parallel_workers +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts + # before vacuum; -1 disables insert + # vacuums +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of unfrozen pages # before insert vacuum -#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze #autovacuum_vacuum_max_threshold = 100000000 # max number of row updates - # before vacuum; -1 disables max - # threshold -#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum - # (change requires restart) -#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age - # before forced vacuum - # (change requires restart) -#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for - # autovacuum, in milliseconds; - # -1 means use vacuum_cost_delay -#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for - # autovacuum, -1 means use - # vacuum_cost_limit + # before vacuum; -1 disables max + # threshold +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_freeze_score_weight = 1.0 # range 0.0-10.0 +#autovacuum_multixact_freeze_score_weight = 1.0 # range 0.0-10.0 +#autovacuum_vacuum_score_weight = 1.0 # range 0.0-10.0 +#autovacuum_vacuum_insert_score_weight = 1.0 # range 0.0-10.0 +#autovacuum_analyze_score_weight = 1.0 # range 0.0-10.0 +#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit # - Cost-Based Vacuum Delay - -#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) -#vacuum_cost_page_hit = 1 # 0-10000 credits -#vacuum_cost_page_miss = 2 # 0-10000 credits -#vacuum_cost_page_dirty = 20 # 0-10000 credits -#vacuum_cost_limit = 200 # 1-10000 credits +#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) +#vacuum_cost_page_hit = 1 # 0-10000 credits +#vacuum_cost_page_miss = 2 # 0-10000 credits +#vacuum_cost_page_dirty = 20 # 0-10000 credits +#vacuum_cost_limit = 200 # 1-10000 credits # - Default Behavior - -#vacuum_truncate = on # enable truncation after vacuum +#vacuum_truncate = on # enable truncation after vacuum # - Freezing - @@ -740,38 +785,38 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Statement Behavior - -#client_min_messages = notice # values in order of decreasing detail: - # debug5 - # debug4 - # debug3 - # debug2 - # debug1 - # log - # notice - # warning - # error -#search_path = '"$user", public' # schema names +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names #row_security = on #default_table_access_method = 'heap' -#default_tablespace = '' # a tablespace name, '' uses the default -#default_toast_compression = 'pglz' # 'pglz' or 'lz4' -#temp_tablespaces = '' # a list of tablespace names, '' uses - # only default tablespace +#default_tablespace = '' # a tablespace name, '' uses the default +#default_toast_compression = pglz # pglz or lz4 +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace #check_function_bodies = on #default_transaction_isolation = 'read committed' #default_transaction_read_only = off #default_transaction_deferrable = off #session_replication_role = 'origin' -#statement_timeout = 0 # in milliseconds, 0 is disabled -#transaction_timeout = 0 # in milliseconds, 0 is disabled -#lock_timeout = 0 # in milliseconds, 0 is disabled -#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled -#idle_session_timeout = 0 # in milliseconds, 0 is disabled -#bytea_output = 'hex' # hex, escape +#statement_timeout = 0 # in milliseconds, 0 is disabled +#transaction_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#idle_session_timeout = 0 # in milliseconds, 0 is disabled +#bytea_output = 'hex' # hex, escape #xmlbinary = 'base64' #xmloption = 'content' #gin_pending_list_limit = 4MB -#createrole_self_grant = '' # set and/or inherit +#createrole_self_grant = '' # set and/or inherit #event_triggers = on # - Locale and Formatting - @@ -779,27 +824,27 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate #datestyle = 'iso, mdy' #intervalstyle = 'postgres' #timezone = 'GMT' -#timezone_abbreviations = 'Default' # Select the set of available time zone - # abbreviations. Currently, there are - # Default - # Australia (historical usage) - # India - # You can create your own file in - # share/timezonesets/. -#extra_float_digits = 1 # min -15, max 3; any value >0 actually - # selects precise output mode -#client_encoding = sql_ascii # actually, defaults to database - # encoding +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 1 # min -15, max 3; any value >0 actually + # selects precise output mode +#client_encoding = sql_ascii # actually, defaults to database + # encoding # These settings are initialized by initdb, but they can be changed. -#lc_messages = '' # locale for system error message - # strings -#lc_monetary = 'C' # locale for monetary formatting -#lc_numeric = 'C' # locale for number formatting -#lc_time = 'C' # locale for time formatting +#lc_messages = '' # locale for system error message + # strings +#lc_monetary = 'C' # locale for monetary formatting +#lc_numeric = 'C' # locale for number formatting +#lc_time = 'C' # locale for time formatting -#icu_validation_level = warning # report ICU locale validation - # errors at the given level +#icu_validation_level = warning # report ICU locale validation + # errors at the given level # default configuration for text search #default_text_search_config = 'pg_catalog.simple' @@ -808,8 +853,8 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate #local_preload_libraries = '' #session_preload_libraries = '' -#shared_preload_libraries = '' # (change requires restart) -#jit_provider = 'llvmjit' # JIT library to use +#shared_preload_libraries = '' # (change requires restart) +#jit_provider = 'llvmjit' # JIT library to use # - Other Defaults - @@ -823,14 +868,14 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate #------------------------------------------------------------------------------ #deadlock_timeout = 1s -#max_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_transaction = 64 # min 10 - # (change requires restart) -#max_pred_locks_per_relation = -2 # negative values mean - # (max_pred_locks_per_transaction - # / -max_pred_locks_per_relation) - 1 -#max_pred_locks_per_page = 2 # min 0 +#max_locks_per_transaction = 128 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 #------------------------------------------------------------------------------ @@ -840,11 +885,9 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # - Previous PostgreSQL Versions - #array_nulls = on -#backslash_quote = safe_encoding # on, off, or safe_encoding -#escape_string_warning = on +#backslash_quote = safe_encoding # on, off, or safe_encoding #lo_compat_privileges = off #quote_all_identifiers = off -#standard_conforming_strings = on #synchronize_seqscans = on # - Other Platforms and Clients - @@ -857,12 +900,12 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # ERROR HANDLING #------------------------------------------------------------------------------ -#exit_on_error = off # terminate session on any error? -#restart_after_crash = on # reinitialize after backend crash? -#data_sync_retry = off # retry or panic on failure to fsync - # data? - # (change requires restart) -#recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+) +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) +#recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+) #------------------------------------------------------------------------------ @@ -873,10 +916,10 @@ autovacuum_worker_slots = 16 # autovacuum worker slots to allocate # default postgresql.conf. Note that these are directives, not variable # assignments, so they can usefully be given more than once. -#include_dir = '...' # include files ending in '.conf' from - # a directory, e.g., 'conf.d' -#include_if_exists = '...' # include file only if it exists -#include = '...' # include file +#include_dir = '...' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '...' # include file only if it exists +#include = '...' # include file #------------------------------------------------------------------------------ diff --git a/src/backend/utils/misc/ps_status.c b/src/backend/utils/misc/ps_status.c index e08b26e8c14f2..cde10dd59d2fe 100644 --- a/src/backend/utils/misc/ps_status.c +++ b/src/backend/utils/misc/ps_status.c @@ -7,7 +7,7 @@ * * src/backend/utils/misc/ps_status.c * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * various details abducted from various places *-------------------------------------------------------------------- */ @@ -23,7 +23,7 @@ #include "utils/guc.h" #include "utils/ps_status.h" -#if !defined(WIN32) || defined(_MSC_VER) +#if !defined(WIN32) extern char **environ; #endif @@ -52,7 +52,7 @@ bool update_process_title = DEFAULT_UPDATE_PROCESS_TITLE; #define PS_USE_SETPROCTITLE_FAST #elif defined(HAVE_SETPROCTITLE) #define PS_USE_SETPROCTITLE -#elif defined(__linux__) || defined(__sun) || defined(__darwin__) +#elif defined(__linux__) || defined(_AIX) || defined(__sun) || defined(__darwin__) || defined(__GNU__) #define PS_USE_CLOBBER_ARGV #elif defined(WIN32) #define PS_USE_WIN32 @@ -62,7 +62,7 @@ bool update_process_title = DEFAULT_UPDATE_PROCESS_TITLE; /* Different systems want the buffer padded differently */ -#if defined(__linux__) || defined(__darwin__) +#if defined(_AIX) || defined(__linux__) || defined(__darwin__) || defined(__GNU__) #define PS_PADDING '\0' #else #define PS_PADDING ' ' @@ -100,6 +100,17 @@ static void flush_ps_display(void); static int save_argc; static char **save_argv; +/* + * Valgrind seems not to consider the global "environ" variable as a valid + * root pointer; so when we allocate a new environment array, it claims that + * data is leaked. To fix that, keep our own statically-allocated copy of the + * pointer. (Oddly, this doesn't seem to be a problem for "argv".) + */ +#if defined(PS_USE_CLOBBER_ARGV) && defined(USE_VALGRIND) +extern char **ps_status_new_environ; +char **ps_status_new_environ; +#endif + /* * Call this early in startup to save the original argc/argv values. @@ -206,6 +217,11 @@ save_ps_display_args(int argc, char **argv) } new_environ[i] = NULL; environ = new_environ; + + /* See notes about Valgrind above. */ +#ifdef USE_VALGRIND + ps_status_new_environ = new_environ; +#endif } /* @@ -218,7 +234,8 @@ save_ps_display_args(int argc, char **argv) * into the argv array, and will get horribly confused when it is * re-called to analyze a subprocess' argument string if the argv storage * has been clobbered meanwhile. Other platforms have other dependencies - * on argv[]. + * on argv[]. (We use custom pg_getopt_start/next() functions nowadays + * that don't do that, but those other dependencies might still exist.) */ { char **new_argv; diff --git a/src/backend/utils/misc/queryenvironment.c b/src/backend/utils/misc/queryenvironment.c index 7bc72dabe6797..46ac154fb15df 100644 --- a/src/backend/utils/misc/queryenvironment.c +++ b/src/backend/utils/misc/queryenvironment.c @@ -11,7 +11,7 @@ * on callers, since this is an opaque structure. This is the reason to * require a create function. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -38,7 +38,7 @@ struct QueryEnvironment QueryEnvironment * create_queryEnv(void) { - return (QueryEnvironment *) palloc0(sizeof(QueryEnvironment)); + return palloc0_object(QueryEnvironment); } EphemeralNamedRelationMetadata diff --git a/src/backend/utils/misc/rls.c b/src/backend/utils/misc/rls.c index f5b4e45b84a96..9ce7abb66dc45 100644 --- a/src/backend/utils/misc/rls.c +++ b/src/backend/utils/misc/rls.c @@ -3,7 +3,7 @@ * rls.c * RLS-related utility functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/misc/sampling.c b/src/backend/utils/misc/sampling.c index a26835ed20d6f..ea121b4c388cf 100644 --- a/src/backend/utils/misc/sampling.c +++ b/src/backend/utils/misc/sampling.c @@ -3,7 +3,7 @@ * sampling.c * Relation block sampling routines. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/misc/stack_depth.c b/src/backend/utils/misc/stack_depth.c index 8f7cf531fbc5f..61a07cf824e9f 100644 --- a/src/backend/utils/misc/stack_depth.c +++ b/src/backend/utils/misc/stack_depth.c @@ -3,7 +3,7 @@ * stack_depth.c * Functions for monitoring and limiting process stack depth * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/misc/superuser.c b/src/backend/utils/misc/superuser.c index 5858b0af64cdf..b9c3a0ceaa867 100644 --- a/src/backend/utils/misc/superuser.c +++ b/src/backend/utils/misc/superuser.c @@ -9,7 +9,7 @@ * the single-user case works. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -36,7 +36,8 @@ static Oid last_roleid = InvalidOid; /* InvalidOid == cache not valid */ static bool last_roleid_is_super = false; static bool roleid_callback_registered = false; -static void RoleidCallback(Datum arg, int cacheid, uint32 hashvalue); +static void RoleidCallback(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); /* @@ -100,7 +101,7 @@ superuser_arg(Oid roleid) * Syscache inval callback function */ static void -RoleidCallback(Datum arg, int cacheid, uint32 hashvalue) +RoleidCallback(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) { /* Invalidate our local cache in case role's superuserness changed */ last_roleid = InvalidOid; diff --git a/src/backend/utils/misc/timeout.c b/src/backend/utils/misc/timeout.c index d92efa12550ae..ddba5dc607c86 100644 --- a/src/backend/utils/misc/timeout.c +++ b/src/backend/utils/misc/timeout.c @@ -3,7 +3,7 @@ * timeout.c * Routines to multiplex SIGALRM interrupts for multiple timeout reasons. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/backend/utils/misc/tzparser.c b/src/backend/utils/misc/tzparser.c index 6aaf7395ba852..8129cf44c4f10 100644 --- a/src/backend/utils/misc/tzparser.c +++ b/src/backend/utils/misc/tzparser.c @@ -11,7 +11,7 @@ * PG_TRY if necessary. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -466,7 +466,7 @@ load_tzoffsets(const char *filename) /* Initialize array at a reasonable size */ arraysize = 128; - array = (tzEntry *) palloc(arraysize * sizeof(tzEntry)); + array = palloc_array(tzEntry, arraysize); /* Parse the file(s) */ n = ParseTzFile(filename, 0, &array, &arraysize, 0); diff --git a/src/backend/utils/mmgr/alignedalloc.c b/src/backend/utils/mmgr/alignedalloc.c index 7eea695de62c5..3208f8f256e1f 100644 --- a/src/backend/utils/mmgr/alignedalloc.c +++ b/src/backend/utils/mmgr/alignedalloc.c @@ -8,7 +8,7 @@ * operations such as pfree() and repalloc() to work correctly on a memory * chunk that was allocated by palloc_aligned(). * - * Portions Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/mmgr/alignedalloc.c @@ -23,8 +23,8 @@ /* * AlignedAllocFree -* Frees allocated memory; memory is removed from its owning context. -*/ + * Frees allocated memory; memory is removed from its owning context. + */ void AlignedAllocFree(void *pointer) { @@ -45,6 +45,15 @@ AlignedAllocFree(void *pointer) GetMemoryChunkContext(unaligned)->name, chunk); #endif + /* + * Create a dummy vchunk covering the start of the unaligned chunk, but + * not overlapping the aligned chunk. This will be freed while pfree'ing + * the unaligned chunk, keeping Valgrind happy. Then when we return to + * the outer pfree, that will clean up the vchunk for the aligned chunk. + */ + VALGRIND_MEMPOOL_ALLOC(GetMemoryChunkContext(unaligned), unaligned, + (char *) pointer - (char *) unaligned); + /* Recursively pfree the unaligned chunk */ pfree(unaligned); } @@ -123,6 +132,15 @@ AlignedAllocRealloc(void *pointer, Size size, int flags) VALGRIND_MAKE_MEM_DEFINED(pointer, old_size); memcpy(newptr, pointer, Min(size, old_size)); + /* + * Create a dummy vchunk covering the start of the old unaligned chunk, + * but not overlapping the aligned chunk. This will be freed while + * pfree'ing the old unaligned chunk, keeping Valgrind happy. Then when + * we return to repalloc, it will move the vchunk for the aligned chunk. + */ + VALGRIND_MEMPOOL_ALLOC(ctx, unaligned, + (char *) pointer - (char *) unaligned); + pfree(unaligned); return newptr; diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c index 666ecd8f78d0e..6a9ea3671074a 100644 --- a/src/backend/utils/mmgr/aset.c +++ b/src/backend/utils/mmgr/aset.c @@ -7,7 +7,7 @@ * type. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -87,6 +87,10 @@ #define ALLOC_CHUNK_FRACTION 4 /* We allow chunks to be at most 1/4 of maxBlockSize (less overhead) */ +/* ALLOC_CHUNK_LIMIT must be equal to ALLOCSET_SEPARATE_THRESHOLD */ +StaticAssertDecl(ALLOC_CHUNK_LIMIT == ALLOCSET_SEPARATE_THRESHOLD, + "ALLOC_CHUNK_LIMIT != ALLOCSET_SEPARATE_THRESHOLD"); + /*-------------------- * The first block allocated for an allocset has size initBlockSize. * Each time we have to allocate another block, we double the block size @@ -103,6 +107,8 @@ #define ALLOC_BLOCKHDRSZ MAXALIGN(sizeof(AllocBlockData)) #define ALLOC_CHUNKHDRSZ sizeof(MemoryChunk) +#define FIRST_BLOCKHDRSZ (MAXALIGN(sizeof(AllocSetContext)) + \ + ALLOC_BLOCKHDRSZ) typedef struct AllocBlockData *AllocBlock; /* forward reference */ @@ -187,25 +193,19 @@ typedef struct AllocBlockData char *endptr; /* end of space in this block */ } AllocBlockData; -/* - * AllocPointerIsValid - * True iff pointer is valid allocation pointer. - */ -#define AllocPointerIsValid(pointer) PointerIsValid(pointer) - /* * AllocSetIsValid * True iff set is valid allocation set. */ #define AllocSetIsValid(set) \ - (PointerIsValid(set) && IsA(set, AllocSetContext)) + ((set) && IsA(set, AllocSetContext)) /* * AllocBlockIsValid * True iff block is valid block of allocation set. */ #define AllocBlockIsValid(block) \ - (PointerIsValid(block) && AllocSetIsValid((block)->aset)) + ((block) && AllocSetIsValid((block)->aset)) /* * We always store external chunks on a dedicated block. This makes fetching @@ -458,6 +458,21 @@ AllocSetContextCreateInternal(MemoryContext parent, * we'd leak the header/initial block if we ereport in this stretch. */ + /* Create a vpool associated with the context */ + VALGRIND_CREATE_MEMPOOL(set, 0, false); + + /* + * Create a vchunk covering both the AllocSetContext struct and the keeper + * block's header. (Perhaps it would be more sensible for these to be two + * separate vchunks, but doing that seems to tickle bugs in some versions + * of Valgrind.) We must have these vchunks, and also a vchunk for each + * subsequently-added block header, so that Valgrind considers the + * pointers within them while checking for leaked memory. Note that + * Valgrind doesn't distinguish between these vchunks and those created by + * mcxt.c for the user-accessible-data chunks we allocate. + */ + VALGRIND_MEMPOOL_ALLOC(set, set, FIRST_BLOCKHDRSZ); + /* Fill in the initial block's block header */ block = KeeperBlock(set); block->aset = set; @@ -490,12 +505,6 @@ AllocSetContextCreateInternal(MemoryContext parent, * requests that are all the maximum chunk size we will waste at most * 1/8th of the allocated space. * - * Also, allocChunkLimit must not exceed ALLOCSET_SEPARATE_THRESHOLD. - */ - StaticAssertStmt(ALLOC_CHUNK_LIMIT == ALLOCSET_SEPARATE_THRESHOLD, - "ALLOC_CHUNK_LIMIT != ALLOCSET_SEPARATE_THRESHOLD"); - - /* * Determine the maximum size that a chunk can be before we allocate an * entire AllocBlock dedicated for that chunk. We set the absolute limit * of that size as ALLOC_CHUNK_LIMIT but we reduce it further so that we @@ -585,6 +594,14 @@ AllocSetReset(MemoryContext context) #ifdef CLOBBER_FREED_MEMORY wipe_mem(block, block->freeptr - ((char *) block)); #endif + + /* + * We need to free the block header's vchunk explicitly, although + * the user-data vchunks within will go away in the TRIM below. + * Otherwise Valgrind complains about leaked allocations. + */ + VALGRIND_MEMPOOL_FREE(set, block); + free(block); } block = next; @@ -592,6 +609,14 @@ AllocSetReset(MemoryContext context) Assert(context->mem_allocated == keepersize); + /* + * Instruct Valgrind to throw away all the vchunks associated with this + * context, except for the one covering the AllocSetContext and + * keeper-block header. This gets rid of the vchunks for whatever user + * data is getting discarded by the context reset. + */ + VALGRIND_MEMPOOL_TRIM(set, set, FIRST_BLOCKHDRSZ); + /* Reset block size allocation sequence, too */ set->nextBlockSize = set->initBlockSize; } @@ -648,6 +673,9 @@ AllocSetDelete(MemoryContext context) freelist->first_free = (AllocSetContext *) oldset->header.nextchild; freelist->num_free--; + /* Destroy the context's vpool --- see notes below */ + VALGRIND_DESTROY_MEMPOOL(oldset); + /* All that remains is to free the header/initial block */ free(oldset); } @@ -675,13 +703,24 @@ AllocSetDelete(MemoryContext context) #endif if (!IsKeeperBlock(set, block)) + { + /* As in AllocSetReset, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(set, block); free(block); + } block = next; } Assert(context->mem_allocated == keepersize); + /* + * Destroy the vpool. We don't seem to need to explicitly free the + * initial block's header vchunk, nor any user-data vchunks that Valgrind + * still knows about; they'll all go away automatically. + */ + VALGRIND_DESTROY_MEMPOOL(set); + /* Finally, free the context header, including the keeper block */ free(set); } @@ -716,6 +755,9 @@ AllocSetAllocLarge(MemoryContext context, Size size, int flags) if (block == NULL) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(set, block, ALLOC_BLOCKHDRSZ); + context->mem_allocated += blksize; block->aset = set; @@ -922,6 +964,9 @@ AllocSetAllocFromNewBlock(MemoryContext context, Size size, int flags, if (block == NULL) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(set, block, ALLOC_BLOCKHDRSZ); + context->mem_allocated += blksize; block->aset = set; @@ -1104,6 +1149,10 @@ AllocSetFree(void *pointer) #ifdef CLOBBER_FREED_MEMORY wipe_mem(block, block->freeptr - ((char *) block)); #endif + + /* As in AllocSetReset, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(set, block); + free(block); } else @@ -1125,7 +1174,26 @@ AllocSetFree(void *pointer) Assert(FreeListIdxIsValid(fidx)); link = GetFreeListLink(chunk); + /* + * It might seem odd that we use elevel ERROR for double-pfree but + * only WARNING for write-past-chunk-end. But the two conditions are + * not very comparable. In the double-pfree case we can prevent + * corruption before it happens; while if we let it go through, the + * result would be a corrupted freelist that allows this chunk to get + * re-allocated twice. Thus the original bug could cascade into + * hard-to-understand misbehavior that might manifest far away from + * the actual source of the problem. On the other hand, a write past + * chunk end can be relatively benign if just a few bytes too many + * were written: often, only padding or unused space gets affected. + * Moreover, whatever damage was done is already done, and we're just + * reporting after the fact with no ability to clean it up. So just + * warn, like AllocSetCheck would do if the chunk didn't get freed. + */ #ifdef MEMORY_CONTEXT_CHECKING + /* Test for previously-freed chunk */ + if (unlikely(chunk->requested_size == InvalidAllocSize)) + elog(ERROR, "detected double pfree in %s %p", + set->header.name, chunk); /* Test for someone scribbling on unused space in chunk */ if (chunk->requested_size < GetChunkSizeFromFreeListIdx(fidx)) if (!sentinel_ok(pointer, chunk->requested_size)) @@ -1184,6 +1252,7 @@ AllocSetRealloc(void *pointer, Size size, int flags) * realloc() to make the containing block bigger, or smaller, with * minimum space wastage. */ + AllocBlock newblock; Size chksize; Size blksize; Size oldblksize; @@ -1223,14 +1292,21 @@ AllocSetRealloc(void *pointer, Size size, int flags) blksize = chksize + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ; oldblksize = block->endptr - ((char *) block); - block = (AllocBlock) realloc(block, blksize); - if (block == NULL) + newblock = (AllocBlock) realloc(block, blksize); + if (newblock == NULL) { /* Disallow access to the chunk header. */ VALGRIND_MAKE_MEM_NOACCESS(chunk, ALLOC_CHUNKHDRSZ); return MemoryContextAllocationFailure(&set->header, size, flags); } + /* + * Move the block-header vchunk explicitly. (mcxt.c will take care of + * moving the vchunk for the user data.) + */ + VALGRIND_MEMPOOL_CHANGE(set, block, newblock, ALLOC_BLOCKHDRSZ); + block = newblock; + /* updated separately, not to underflow when (oldblksize > blksize) */ set->header.mem_allocated -= oldblksize; set->header.mem_allocated += blksize; @@ -1294,7 +1370,7 @@ AllocSetRealloc(void *pointer, Size size, int flags) /* Ensure any padding bytes are marked NOACCESS. */ VALGRIND_MAKE_MEM_NOACCESS((char *) pointer + size, chksize - size); - /* Disallow access to the chunk header . */ + /* Disallow access to the chunk header. */ VALGRIND_MAKE_MEM_NOACCESS(chunk, ALLOC_CHUNKHDRSZ); return pointer; @@ -1316,6 +1392,11 @@ AllocSetRealloc(void *pointer, Size size, int flags) oldchksize = GetChunkSizeFromFreeListIdx(fidx); #ifdef MEMORY_CONTEXT_CHECKING + /* See comments in AllocSetFree about uses of ERROR and WARNING here */ + /* Test for previously-freed chunk */ + if (unlikely(chunk->requested_size == InvalidAllocSize)) + elog(ERROR, "detected realloc of freed chunk in %s %p", + set->header.name, chunk); /* Test for someone scribbling on unused space in chunk */ if (chunk->requested_size < oldchksize) if (!sentinel_ok(pointer, chunk->requested_size)) @@ -1609,9 +1690,9 @@ AllocSetCheck(MemoryContext context) prevblock = block, block = block->next) { char *bpoz = ((char *) block) + ALLOC_BLOCKHDRSZ; - long blk_used = block->freeptr - bpoz; - long blk_data = 0; - long nchunks = 0; + Size blk_used = block->freeptr - bpoz; + Size blk_data = 0; + Size nchunks = 0; bool has_external_chunk = false; if (IsKeeperBlock(set, block)) diff --git a/src/backend/utils/mmgr/bump.c b/src/backend/utils/mmgr/bump.c index f7a37d1b3e86c..bfb5a114147d0 100644 --- a/src/backend/utils/mmgr/bump.c +++ b/src/backend/utils/mmgr/bump.c @@ -12,7 +12,7 @@ * only way to release memory allocated by this context type is to reset or * delete the context. * - * Portions Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/mmgr/bump.c @@ -45,7 +45,9 @@ #include "utils/memutils_memorychunk.h" #include "utils/memutils_internal.h" -#define Bump_BLOCKHDRSZ MAXALIGN(sizeof(BumpBlock)) +#define Bump_BLOCKHDRSZ MAXALIGN(sizeof(BumpBlock)) +#define FIRST_BLOCKHDRSZ (MAXALIGN(sizeof(BumpContext)) + \ + Bump_BLOCKHDRSZ) /* No chunk header unless built with MEMORY_CONTEXT_CHECKING */ #ifdef MEMORY_CONTEXT_CHECKING @@ -98,7 +100,7 @@ struct BumpBlock * True iff set is valid bump context. */ #define BumpIsValid(set) \ - (PointerIsValid(set) && IsA(set, BumpContext)) + ((set) && IsA(set, BumpContext)) /* * We always store external chunks on a dedicated block. This makes fetching @@ -189,6 +191,12 @@ BumpContextCreate(MemoryContext parent, const char *name, Size minContextSize, * Avoid writing code that can fail between here and MemoryContextCreate; * we'd leak the header and initial block if we ereport in this stretch. */ + + /* See comments about Valgrind interactions in aset.c */ + VALGRIND_CREATE_MEMPOOL(set, 0, false); + /* This vchunk covers the BumpContext and the keeper block header */ + VALGRIND_MEMPOOL_ALLOC(set, set, FIRST_BLOCKHDRSZ); + dlist_init(&set->blocks); /* Fill in the initial block's block header */ @@ -262,6 +270,14 @@ BumpReset(MemoryContext context) BumpBlockFree(set, block); } + /* + * Instruct Valgrind to throw away all the vchunks associated with this + * context, except for the one covering the BumpContext and keeper-block + * header. This gets rid of the vchunks for whatever user data is getting + * discarded by the context reset. + */ + VALGRIND_MEMPOOL_TRIM(set, set, FIRST_BLOCKHDRSZ); + /* Reset block size allocation sequence, too */ set->nextBlockSize = set->initBlockSize; @@ -279,6 +295,10 @@ BumpDelete(MemoryContext context) { /* Reset to release all releasable BumpBlocks */ BumpReset(context); + + /* Destroy the vpool -- see notes in aset.c */ + VALGRIND_DESTROY_MEMPOOL(context); + /* And free the context header and keeper block */ free(context); } @@ -318,6 +338,9 @@ BumpAllocLarge(MemoryContext context, Size size, int flags) if (block == NULL) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(set, block, Bump_BLOCKHDRSZ); + context->mem_allocated += blksize; /* the block is completely full */ @@ -384,7 +407,7 @@ BumpAllocChunkFromBlock(MemoryContext context, BumpBlock *block, Size size, #ifdef MEMORY_CONTEXT_CHECKING chunk = (MemoryChunk *) block->freeptr; #else - ptr = (void *) block->freeptr; + ptr = block->freeptr; #endif /* point the freeptr beyond this chunk */ @@ -455,6 +478,9 @@ BumpAllocFromNewBlock(MemoryContext context, Size size, int flags, if (block == NULL) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(set, block, Bump_BLOCKHDRSZ); + context->mem_allocated += blksize; /* initialize the new block */ @@ -606,6 +632,9 @@ BumpBlockFree(BumpContext *set, BumpBlock *block) wipe_mem(block, ((char *) block->endptr - (char *) block)); #endif + /* As in aset.c, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(set, block); + free(block); } diff --git a/src/backend/utils/mmgr/dsa.c b/src/backend/utils/mmgr/dsa.c index 17d4f7a7a06e1..4b4f1e1965ba3 100644 --- a/src/backend/utils/mmgr/dsa.c +++ b/src/backend/utils/mmgr/dsa.c @@ -39,7 +39,7 @@ * empty and be returned to the free page manager, and whole segments can * become empty and be returned to the operating system. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -531,6 +531,21 @@ dsa_attach(dsa_handle handle) return area; } +/* + * Returns whether the area with the given handle was already attached by the + * current process. The area must have been created with dsa_create (not + * dsa_create_in_place). + */ +bool +dsa_is_attached(dsa_handle handle) +{ + /* + * An area handle is really a DSM segment handle for the first segment, so + * we can just search for that. + */ + return dsm_find_mapping(handle) != NULL; +} + /* * Attach to an area that was created with dsa_create_in_place. The caller * must somehow know the location in memory that was used when the area was @@ -1028,13 +1043,48 @@ dsa_get_total_size(dsa_area *area) { size_t size; - LWLockAcquire(DSA_AREA_LOCK(area), LW_EXCLUSIVE); + LWLockAcquire(DSA_AREA_LOCK(area), LW_SHARED); size = area->control->total_segment_size; LWLockRelease(DSA_AREA_LOCK(area)); return size; } +/* + * Same as dsa_get_total_size(), but accepts a DSA handle. The area must have + * been created with dsa_create (not dsa_create_in_place). + */ +size_t +dsa_get_total_size_from_handle(dsa_handle handle) +{ + size_t size; + bool already_attached; + dsm_segment *segment; + dsa_area_control *control; + + already_attached = dsa_is_attached(handle); + if (already_attached) + segment = dsm_find_mapping(handle); + else + segment = dsm_attach(handle); + + if (segment == NULL) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("could not attach to dynamic shared area"))); + + control = (dsa_area_control *) dsm_segment_address(segment); + + LWLockAcquire(&control->lock, LW_SHARED); + size = control->total_segment_size; + LWLockRelease(&control->lock); + + if (!already_attached) + dsm_detach(segment); + + return size; +} + /* * Aggressively free all spare memory in the hope of returning DSM segments to * the operating system. @@ -1280,7 +1330,7 @@ create_internal(void *place, size_t size, * area. Other backends will need to obtain their own dsa_area object by * attaching. */ - area = palloc(sizeof(dsa_area)); + area = palloc_object(dsa_area); area->control = control; area->resowner = CurrentResourceOwner; memset(area->segment_maps, 0, sizeof(dsa_segment_map) * DSA_MAX_SEGMENTS); @@ -1336,7 +1386,7 @@ attach_internal(void *place, dsm_segment *segment, dsa_handle handle) (DSA_SEGMENT_HEADER_MAGIC ^ handle ^ 0)); /* Build the backend-local area object. */ - area = palloc(sizeof(dsa_area)); + area = palloc_object(dsa_area); area->control = control; area->resowner = CurrentResourceOwner; memset(&area->segment_maps[0], 0, @@ -2146,6 +2196,8 @@ make_new_segment(dsa_area *area, size_t requested_pages) /* See if that is enough... */ if (requested_pages > usable_pages) { + size_t total_requested_pages PG_USED_FOR_ASSERTS_ONLY; + /* * We'll make an odd-sized segment, working forward from the requested * number of pages. @@ -2156,10 +2208,37 @@ make_new_segment(dsa_area *area, size_t requested_pages) MAXALIGN(sizeof(FreePageManager)) + usable_pages * sizeof(dsa_pointer); + /* + * We must also account for pagemap entries needed to cover the + * metadata pages themselves. The pagemap must track all pages in the + * segment, including the pages occupied by metadata. + * + * This formula uses integer ceiling division to compute the exact + * number of additional entries needed. The divisor (FPM_PAGE_SIZE - + * sizeof(dsa_pointer)) accounts for the fact that each metadata page + * consumes one pagemap entry of sizeof(dsa_pointer) bytes, leaving + * only (FPM_PAGE_SIZE - sizeof(dsa_pointer)) net bytes per metadata + * page. + */ + metadata_bytes += + ((metadata_bytes + (FPM_PAGE_SIZE - sizeof(dsa_pointer)) - 1) / + (FPM_PAGE_SIZE - sizeof(dsa_pointer))) * + sizeof(dsa_pointer); + /* Add padding up to next page boundary. */ if (metadata_bytes % FPM_PAGE_SIZE != 0) metadata_bytes += FPM_PAGE_SIZE - (metadata_bytes % FPM_PAGE_SIZE); total_size = metadata_bytes + usable_pages * FPM_PAGE_SIZE; + total_requested_pages = total_size / FPM_PAGE_SIZE; + + /* + * Verify that we allocated enough pagemap entries for metadata and + * usable pages. This reverse-engineers the new calculation of + * "metadata_bytes" done based on the new "requested_pages" for an + * odd-sized segment. + */ + Assert((metadata_bytes - MAXALIGN(sizeof(dsa_segment_header)) - + MAXALIGN(sizeof(FreePageManager))) / sizeof(dsa_pointer) >= total_requested_pages); /* Is that too large for dsa_pointer's addressing scheme? */ if (total_size > DSA_MAX_SEGMENT_SIZE) diff --git a/src/backend/utils/mmgr/freepage.c b/src/backend/utils/mmgr/freepage.c index 52fa78dc58612..d7195685f69d2 100644 --- a/src/backend/utils/mmgr/freepage.c +++ b/src/backend/utils/mmgr/freepage.c @@ -42,7 +42,7 @@ * where memory fragmentation is very severe, only a tiny fraction of * the pages under management are consumed by this btree. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -894,14 +894,14 @@ FreePageBtreeGetRecycled(FreePageManager *fpm) } /* - * Insert an item into an internal page. + * Insert an item into an internal page (there must be room). */ static void FreePageBtreeInsertInternal(char *base, FreePageBtree *btp, Size index, Size first_page, FreePageBtree *child) { Assert(btp->hdr.magic == FREE_PAGE_INTERNAL_MAGIC); - Assert(btp->hdr.nused <= FPM_ITEMS_PER_INTERNAL_PAGE); + Assert(btp->hdr.nused < FPM_ITEMS_PER_INTERNAL_PAGE); Assert(index <= btp->hdr.nused); memmove(&btp->u.internal_key[index + 1], &btp->u.internal_key[index], sizeof(FreePageBtreeInternalKey) * (btp->hdr.nused - index)); @@ -911,14 +911,14 @@ FreePageBtreeInsertInternal(char *base, FreePageBtree *btp, Size index, } /* - * Insert an item into a leaf page. + * Insert an item into a leaf page (there must be room). */ static void FreePageBtreeInsertLeaf(FreePageBtree *btp, Size index, Size first_page, Size npages) { Assert(btp->hdr.magic == FREE_PAGE_LEAF_MAGIC); - Assert(btp->hdr.nused <= FPM_ITEMS_PER_LEAF_PAGE); + Assert(btp->hdr.nused < FPM_ITEMS_PER_LEAF_PAGE); Assert(index <= btp->hdr.nused); memmove(&btp->u.leaf_key[index + 1], &btp->u.leaf_key[index], sizeof(FreePageBtreeLeafKey) * (btp->hdr.nused - index)); diff --git a/src/backend/utils/mmgr/generation.c b/src/backend/utils/mmgr/generation.c index 18679ad4f1e41..609c9bdc9a6b5 100644 --- a/src/backend/utils/mmgr/generation.c +++ b/src/backend/utils/mmgr/generation.c @@ -6,7 +6,7 @@ * Generation is a custom MemoryContext implementation designed for cases of * chunks with similar lifespan. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/mmgr/generation.c @@ -45,6 +45,8 @@ #define Generation_BLOCKHDRSZ MAXALIGN(sizeof(GenerationBlock)) #define Generation_CHUNKHDRSZ sizeof(MemoryChunk) +#define FIRST_BLOCKHDRSZ (MAXALIGN(sizeof(GenerationContext)) + \ + Generation_BLOCKHDRSZ) #define Generation_CHUNK_FRACTION 8 @@ -100,14 +102,14 @@ struct GenerationBlock * True iff set is valid generation set. */ #define GenerationIsValid(set) \ - (PointerIsValid(set) && IsA(set, GenerationContext)) + ((set) && IsA(set, GenerationContext)) /* * GenerationBlockIsValid * True iff block is valid block of generation set. */ #define GenerationBlockIsValid(block) \ - (PointerIsValid(block) && GenerationIsValid((block)->context)) + ((block) && GenerationIsValid((block)->context)) /* * GenerationBlockIsEmpty @@ -221,6 +223,12 @@ GenerationContextCreate(MemoryContext parent, * Avoid writing code that can fail between here and MemoryContextCreate; * we'd leak the header if we ereport in this stretch. */ + + /* See comments about Valgrind interactions in aset.c */ + VALGRIND_CREATE_MEMPOOL(set, 0, false); + /* This vchunk covers the GenerationContext and the keeper block header */ + VALGRIND_MEMPOOL_ALLOC(set, set, FIRST_BLOCKHDRSZ); + dlist_init(&set->blocks); /* Fill in the initial block's block header */ @@ -309,6 +317,14 @@ GenerationReset(MemoryContext context) GenerationBlockFree(set, block); } + /* + * Instruct Valgrind to throw away all the vchunks associated with this + * context, except for the one covering the GenerationContext and + * keeper-block header. This gets rid of the vchunks for whatever user + * data is getting discarded by the context reset. + */ + VALGRIND_MEMPOOL_TRIM(set, set, FIRST_BLOCKHDRSZ); + /* set it so new allocations to make use of the keeper block */ set->block = KeeperBlock(set); @@ -329,6 +345,10 @@ GenerationDelete(MemoryContext context) { /* Reset to release all releasable GenerationBlocks */ GenerationReset(context); + + /* Destroy the vpool -- see notes in aset.c */ + VALGRIND_DESTROY_MEMPOOL(context); + /* And free the context header and keeper block */ free(context); } @@ -365,6 +385,9 @@ GenerationAllocLarge(MemoryContext context, Size size, int flags) if (block == NULL) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(set, block, Generation_BLOCKHDRSZ); + context->mem_allocated += blksize; /* block with a single (used) chunk */ @@ -487,6 +510,9 @@ GenerationAllocFromNewBlock(MemoryContext context, Size size, int flags, if (block == NULL) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(set, block, Generation_BLOCKHDRSZ); + context->mem_allocated += blksize; /* initialize the new block */ @@ -677,6 +703,9 @@ GenerationBlockFree(GenerationContext *set, GenerationBlock *block) wipe_mem(block, block->blksize); #endif + /* As in aset.c, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(set, block); + free(block); } @@ -733,6 +762,11 @@ GenerationFree(void *pointer) } #ifdef MEMORY_CONTEXT_CHECKING + /* See comments in AllocSetFree about uses of ERROR and WARNING here */ + /* Test for previously-freed chunk */ + if (unlikely(chunk->requested_size == InvalidAllocSize)) + elog(ERROR, "detected double pfree in %s %p", + ((MemoryContext) block->context)->name, chunk); /* Test for someone scribbling on unused space in chunk */ Assert(chunk->requested_size < chunksize); if (!sentinel_ok(pointer, chunk->requested_size)) @@ -838,6 +872,11 @@ GenerationRealloc(void *pointer, Size size, int flags) set = block->context; #ifdef MEMORY_CONTEXT_CHECKING + /* See comments in AllocSetFree about uses of ERROR and WARNING here */ + /* Test for previously-freed chunk */ + if (unlikely(chunk->requested_size == InvalidAllocSize)) + elog(ERROR, "detected realloc of freed chunk in %s %p", + ((MemoryContext) set)->name, chunk); /* Test for someone scribbling on unused space in chunk */ Assert(chunk->requested_size < oldsize); if (!sentinel_ok(pointer, chunk->requested_size)) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 15fa4d0a55eeb..073bdb35d2acf 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -8,8 +8,25 @@ * context-type-specific operations via the function pointers in a * context's MemoryContextMethods struct. * + * A note about Valgrind support: when USE_VALGRIND is defined, we provide + * support for memory leak tracking at the allocation-unit level. Valgrind + * does leak detection by tracking allocated "chunks", which can be grouped + * into "pools". The "chunk" terminology is overloaded, since we use that + * word for our allocation units, and it's sometimes important to distinguish + * those from the Valgrind objects that describe them. To reduce confusion, + * let's use the terms "vchunk" and "vpool" for the Valgrind objects. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * We use a separate vpool for each memory context. The context-type-specific + * code is responsible for creating and deleting the vpools, and also for + * creating vchunks to cover its management data structures such as block + * headers. (There must be a vchunk that includes every pointer we want + * Valgrind to consider for leak-tracking purposes.) This module creates + * and deletes the vchunks that cover the caller-visible allocated chunks. + * However, the context-type-specific code must handle cleaning up those + * vchunks too during memory context reset operations. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -157,6 +174,9 @@ MemoryContext CurTransactionContext = NULL; /* This is a transient link to the active portal's memory context: */ MemoryContext PortalContext = NULL; +/* Is memory context logging currently in progress? */ +static bool LogMemoryContextInProgress = false; + static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, @@ -418,8 +438,6 @@ MemoryContextResetOnly(MemoryContext context) context->methods->reset(context); context->isReset = true; - VALGRIND_DESTROY_MEMPOOL(context); - VALGRIND_CREATE_MEMPOOL(context, 0, false); } } @@ -526,8 +544,6 @@ MemoryContextDeleteOnly(MemoryContext context) context->ident = NULL; context->methods->delete_context(context); - - VALGRIND_DESTROY_MEMPOOL(context); } /* @@ -560,9 +576,7 @@ MemoryContextDeleteChildren(MemoryContext context) * the specified context, since that means it will automatically be freed * when no longer needed. * - * There is no API for deregistering a callback once registered. If you - * want it to not do anything anymore, adjust the state pointed to by its - * "arg" to indicate that. + * Note that callers can assume this cannot fail. */ void MemoryContextRegisterResetCallback(MemoryContext context, @@ -577,6 +591,41 @@ MemoryContextRegisterResetCallback(MemoryContext context, context->isReset = false; } +/* + * MemoryContextUnregisterResetCallback + * Undo the effects of MemoryContextRegisterResetCallback. + * + * This can be used if a callback's effects are no longer required + * at some point before the context has been reset/deleted. It is the + * caller's responsibility to pfree the callback struct (if needed). + * + * An assertion failure occurs if the callback was not registered. + * We could alternatively define that case as a no-op, but that seems too + * likely to mask programming errors such as passing the wrong context. + */ +void +MemoryContextUnregisterResetCallback(MemoryContext context, + MemoryContextCallback *cb) +{ + MemoryContextCallback *prev, + *cur; + + Assert(MemoryContextIsValid(context)); + + for (prev = NULL, cur = context->reset_cbs; cur != NULL; + prev = cur, cur = cur->next) + { + if (cur != cb) + continue; + if (prev) + prev->next = cur->next; + else + context->reset_cbs = cur->next; + return; + } + Assert(false); +} + /* * MemoryContextCallResetCallbacks * Internal function to call all registered callbacks for context. @@ -1137,8 +1186,6 @@ MemoryContextCreate(MemoryContext node, node->nextchild = NULL; node->allowInCritSection = false; } - - VALGRIND_CREATE_MEMPOOL(node, 0, false); } /* @@ -1295,26 +1342,45 @@ ProcessLogMemoryContextInterrupt(void) LogMemoryContextPending = false; /* - * Use LOG_SERVER_ONLY to prevent this message from being sent to the - * connected client. + * Exit immediately if memory context logging is already in progress. This + * prevents recursive calls, which could occur if logging is requested + * repeatedly and rapidly, potentially leading to infinite recursion and a + * crash. */ - ereport(LOG_SERVER_ONLY, - (errhidestmt(true), - errhidecontext(true), - errmsg("logging memory contexts of PID %d", MyProcPid))); + if (LogMemoryContextInProgress) + return; + LogMemoryContextInProgress = true; - /* - * When a backend process is consuming huge memory, logging all its memory - * contexts might overrun available disk space. To prevent this, we limit - * the depth of the hierarchy, as well as the number of child contexts to - * log per parent to 100. - * - * As with MemoryContextStats(), we suppose that practical cases where the - * dump gets long will typically be huge numbers of siblings under the - * same parent context; while the additional debugging value from seeing - * details about individual siblings beyond 100 will not be large. - */ - MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); + PG_TRY(); + { + /* + * Use LOG_SERVER_ONLY to prevent this message from being sent to the + * connected client. + */ + ereport(LOG_SERVER_ONLY, + (errhidestmt(true), + errhidecontext(true), + errmsg("logging memory contexts of PID %d", MyProcPid))); + + /* + * When a backend process is consuming huge memory, logging all its + * memory contexts might overrun available disk space. To prevent + * this, we limit the depth of the hierarchy, as well as the number of + * child contexts to log per parent to 100. + * + * As with MemoryContextStats(), we suppose that practical cases where + * the dump gets long will typically be huge numbers of siblings under + * the same parent context; while the additional debugging value from + * seeing details about individual siblings beyond 100 will not be + * large. + */ + MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); + } + PG_FINALLY(); + { + LogMemoryContextInProgress = false; + } + PG_END_TRY(); } void * @@ -1421,7 +1487,13 @@ MemoryContextAllocAligned(MemoryContext context, void *unaligned; void *aligned; - /* wouldn't make much sense to waste that much space */ + /* + * Restrict alignto to ensure that it can fit into the "value" field of + * the redirection MemoryChunk, and that the distance back to the start of + * the unaligned chunk will fit into the space available for that. This + * isn't a limitation in practice, since it wouldn't make much sense to + * waste that much space. + */ Assert(alignto < (128 * 1024 * 1024)); /* ensure alignto is a power of 2 */ @@ -1458,10 +1530,15 @@ MemoryContextAllocAligned(MemoryContext context, alloc_size += 1; #endif - /* perform the actual allocation */ - unaligned = MemoryContextAllocExtended(context, alloc_size, flags); + /* + * Perform the actual allocation, but do not pass down MCXT_ALLOC_ZERO. + * This ensures that wasted bytes beyond the aligned chunk do not become + * DEFINED. + */ + unaligned = MemoryContextAllocExtended(context, alloc_size, + flags & ~MCXT_ALLOC_ZERO); - /* set the aligned pointer */ + /* compute the aligned pointer */ aligned = (void *) TYPEALIGN(alignto, (char *) unaligned + sizeof(MemoryChunk)); @@ -1489,12 +1566,23 @@ MemoryContextAllocAligned(MemoryContext context, set_sentinel(aligned, size); #endif - /* Mark the bytes before the redirection header as noaccess */ - VALGRIND_MAKE_MEM_NOACCESS(unaligned, - (char *) alignedchunk - (char *) unaligned); + /* + * MemoryContextAllocExtended marked the whole unaligned chunk as a + * vchunk. Undo that, instead making just the aligned chunk be a vchunk. + * This prevents Valgrind from complaining that the vchunk is possibly + * leaked, since only pointers to the aligned chunk will exist. + * + * After these calls, the aligned chunk will be marked UNDEFINED, and all + * the rest of the unaligned chunk (the redirection chunk header, the + * padding bytes before it, and any wasted trailing bytes) will be marked + * NOACCESS, which is what we want. + */ + VALGRIND_MEMPOOL_FREE(context, unaligned); + VALGRIND_MEMPOOL_ALLOC(context, aligned, size); - /* Disallow access to the redirection chunk header. */ - VALGRIND_MAKE_MEM_NOACCESS(alignedchunk, sizeof(MemoryChunk)); + /* Now zero (and make DEFINED) just the aligned chunk, if requested */ + if ((flags & MCXT_ALLOC_ZERO) != 0) + MemSetAligned(aligned, 0, size); return aligned; } @@ -1528,16 +1616,12 @@ void pfree(void *pointer) { #ifdef USE_VALGRIND - MemoryContextMethodID method = GetMemoryChunkMethodID(pointer); MemoryContext context = GetMemoryChunkContext(pointer); #endif MCXT_METHOD(pointer, free_p) (pointer); -#ifdef USE_VALGRIND - if (method != MCTX_ALIGNED_REDIRECT_ID) - VALGRIND_MEMPOOL_FREE(context, pointer); -#endif + VALGRIND_MEMPOOL_FREE(context, pointer); } /* @@ -1547,9 +1631,6 @@ pfree(void *pointer) void * repalloc(void *pointer, Size size) { -#ifdef USE_VALGRIND - MemoryContextMethodID method = GetMemoryChunkMethodID(pointer); -#endif #if defined(USE_ASSERT_CHECKING) || defined(USE_VALGRIND) MemoryContext context = GetMemoryChunkContext(pointer); #endif @@ -1572,10 +1653,7 @@ repalloc(void *pointer, Size size) */ ret = MCXT_METHOD(pointer, realloc) (pointer, size, 0); -#ifdef USE_VALGRIND - if (method != MCTX_ALIGNED_REDIRECT_ID) - VALGRIND_MEMPOOL_CHANGE(context, pointer, ret, size); -#endif + VALGRIND_MEMPOOL_CHANGE(context, pointer, ret, size); return ret; } diff --git a/src/backend/utils/mmgr/memdebug.c b/src/backend/utils/mmgr/memdebug.c index 42f19aa8b21b9..d6f0dc718c2f1 100644 --- a/src/backend/utils/mmgr/memdebug.c +++ b/src/backend/utils/mmgr/memdebug.c @@ -5,7 +5,7 @@ * public API of the memory management subsystem. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/utils/mmgr/memdebug.c diff --git a/src/backend/utils/mmgr/meson.build b/src/backend/utils/mmgr/meson.build index 2addccf00def1..61837e9f91b94 100644 --- a/src/backend/utils/mmgr/meson.build +++ b/src/backend/utils/mmgr/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'alignedalloc.c', diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c index 0be1c2b0fff85..493f9b0ee1912 100644 --- a/src/backend/utils/mmgr/portalmem.c +++ b/src/backend/utils/mmgr/portalmem.c @@ -8,7 +8,7 @@ * doesn't actually run the executor for them. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -24,9 +24,11 @@ #include "miscadmin.h" #include "storage/ipc.h" #include "utils/builtins.h" +#include "utils/hsearch.h" #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/timestamp.h" +#include "utils/tuplestore.h" /* * Estimate of the maximum number of open portals a user would have, @@ -131,7 +133,7 @@ GetPortalByName(const char *name) { Portal portal; - if (PointerIsValid(name)) + if (name) PortalHashTableLookup(name, portal); else portal = NULL; @@ -176,7 +178,7 @@ CreatePortal(const char *name, bool allowDup, bool dupSilent) { Portal portal; - Assert(PointerIsValid(name)); + Assert(name); portal = GetPortalByName(name); if (PortalIsValid(portal)) @@ -294,9 +296,8 @@ PortalDefineQuery(Portal portal, portal->prepStmtName = prepStmtName; portal->sourceText = sourceText; - portal->qc.commandTag = commandTag; - portal->qc.nprocessed = 0; portal->commandTag = commandTag; + SetQueryCompletion(&portal->qc, commandTag, 0); portal->stmts = stmts; portal->cplan = cplan; portal->status = PORTAL_DEFINED; @@ -425,7 +426,7 @@ MarkPortalDone(Portal portal) * aborted transaction, this is necessary, or we'd reach AtCleanup_Portals * with the cleanup hook still unexecuted. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { portal->cleanup(portal); portal->cleanup = NULL; @@ -453,7 +454,7 @@ MarkPortalFailed(Portal portal) * is necessary, or we'd reach AtCleanup_Portals with the cleanup hook * still unexecuted. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { portal->cleanup(portal); portal->cleanup = NULL; @@ -497,7 +498,7 @@ PortalDrop(Portal portal, bool isTopCommit) * Note: in most paths of control, this will have been done already in * MarkPortalDone or MarkPortalFailed. We're just making sure. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { portal->cleanup(portal); portal->cleanup = NULL; @@ -823,7 +824,7 @@ AtAbort_Portals(void) * Allow portalcmds.c to clean up the state it knows about, if we * haven't already. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { portal->cleanup(portal); portal->cleanup = NULL; @@ -853,7 +854,8 @@ AtAbort_Portals(void) /* * Post-abort cleanup for portals. * - * Delete all portals not held over from prior transactions. */ + * Delete all portals not held over from prior transactions. + */ void AtCleanup_Portals(void) { @@ -896,7 +898,7 @@ AtCleanup_Portals(void) * We had better not call any user-defined code during cleanup, so if * the cleanup hook hasn't been run yet, too bad; we'll just skip it. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { elog(WARNING, "skipping cleanup for portal \"%s\"", portal->name); portal->cleanup = NULL; @@ -1056,7 +1058,7 @@ AtSubAbort_Portals(SubTransactionId mySubid, * Allow portalcmds.c to clean up the state it knows about, if we * haven't already. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { portal->cleanup(portal); portal->cleanup = NULL; @@ -1115,7 +1117,7 @@ AtSubCleanup_Portals(SubTransactionId mySubid) * We had better not call any user-defined code during cleanup, so if * the cleanup hook hasn't been run yet, too bad; we'll just skip it. */ - if (PointerIsValid(portal->cleanup)) + if (portal->cleanup) { elog(WARNING, "skipping cleanup for portal \"%s\"", portal->name); portal->cleanup = NULL; diff --git a/src/backend/utils/mmgr/slab.c b/src/backend/utils/mmgr/slab.c index d32c0d318fbf4..dd1db9566d154 100644 --- a/src/backend/utils/mmgr/slab.c +++ b/src/backend/utils/mmgr/slab.c @@ -8,7 +8,7 @@ * with minimal memory wastage and fragmentation. * * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/mmgr/slab.c @@ -193,14 +193,14 @@ typedef struct SlabBlock * SlabIsValid * True iff set is a valid slab allocation set. */ -#define SlabIsValid(set) (PointerIsValid(set) && IsA(set, SlabContext)) +#define SlabIsValid(set) ((set) && IsA(set, SlabContext)) /* * SlabBlockIsValid * True iff block is a valid block of slab allocation set. */ #define SlabBlockIsValid(block) \ - (PointerIsValid(block) && SlabIsValid((block)->slab)) + ((block) && SlabIsValid((block)->slab)) /* * SlabBlocklistIndex @@ -377,6 +377,11 @@ SlabContextCreate(MemoryContext parent, * we'd leak the header if we ereport in this stretch. */ + /* See comments about Valgrind interactions in aset.c */ + VALGRIND_CREATE_MEMPOOL(slab, 0, false); + /* This vchunk covers the SlabContext only */ + VALGRIND_MEMPOOL_ALLOC(slab, slab, sizeof(SlabContext)); + /* Fill in SlabContext-specific header fields */ slab->chunkSize = (uint32) chunkSize; slab->fullChunkSize = (uint32) fullChunkSize; @@ -451,6 +456,10 @@ SlabReset(MemoryContext context) #ifdef CLOBBER_FREED_MEMORY wipe_mem(block, slab->blockSize); #endif + + /* As in aset.c, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(slab, block); + free(block); context->mem_allocated -= slab->blockSize; } @@ -467,11 +476,23 @@ SlabReset(MemoryContext context) #ifdef CLOBBER_FREED_MEMORY wipe_mem(block, slab->blockSize); #endif + + /* As in aset.c, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(slab, block); + free(block); context->mem_allocated -= slab->blockSize; } } + /* + * Instruct Valgrind to throw away all the vchunks associated with this + * context, except for the one covering the SlabContext. This gets rid of + * the vchunks for whatever user data is getting discarded by the context + * reset. + */ + VALGRIND_MEMPOOL_TRIM(slab, slab, sizeof(SlabContext)); + slab->curBlocklistIndex = 0; Assert(context->mem_allocated == 0); @@ -486,6 +507,10 @@ SlabDelete(MemoryContext context) { /* Reset to release all the SlabBlocks */ SlabReset(context); + + /* Destroy the vpool -- see notes in aset.c */ + VALGRIND_DESTROY_MEMPOOL(context); + /* And free the context header */ free(context); } @@ -514,6 +539,7 @@ SlabAllocSetupNewChunk(MemoryContext context, SlabBlock *block, MemoryChunkSetHdrMask(chunk, block, MAXALIGN(slab->chunkSize), MCTX_SLAB_ID); #ifdef MEMORY_CONTEXT_CHECKING + chunk->requested_size = size; /* slab mark to catch clobber of "unused" space */ Assert(slab->chunkSize < (slab->fullChunkSize - Slab_CHUNKHDRSZ)); set_sentinel(MemoryChunkGetPointer(chunk), size); @@ -567,6 +593,9 @@ SlabAllocFromNewBlock(MemoryContext context, Size size, int flags) if (unlikely(block == NULL)) return MemoryContextAllocationFailure(context, size, flags); + /* Make a vchunk covering the new block's header */ + VALGRIND_MEMPOOL_ALLOC(slab, block, Slab_BLOCKHDRSZ); + block->slab = slab; context->mem_allocated += slab->blockSize; @@ -600,8 +629,8 @@ SlabAllocFromNewBlock(MemoryContext context, Size size, int flags) * to setup the stack frame in SlabAlloc. For performance reasons, we * want to avoid that. */ -pg_noinline pg_noreturn +pg_noinline static void SlabAllocInvalidSize(MemoryContext context, Size size) { @@ -720,11 +749,18 @@ SlabFree(void *pointer) slab = block->slab; #ifdef MEMORY_CONTEXT_CHECKING + /* See comments in AllocSetFree about uses of ERROR and WARNING here */ + /* Test for previously-freed chunk */ + if (unlikely(chunk->requested_size == InvalidAllocSize)) + elog(ERROR, "detected double pfree in %s %p", + slab->header.name, chunk); /* Test for someone scribbling on unused space in chunk */ Assert(slab->chunkSize < (slab->fullChunkSize - Slab_CHUNKHDRSZ)); if (!sentinel_ok(pointer, slab->chunkSize)) elog(WARNING, "detected write past chunk end in %s %p", slab->header.name, chunk); + /* Reset requested_size to InvalidAllocSize in free chunks */ + chunk->requested_size = InvalidAllocSize; #endif /* push this chunk onto the head of the block's free list */ @@ -795,6 +831,10 @@ SlabFree(void *pointer) #ifdef CLOBBER_FREED_MEMORY wipe_mem(block, slab->blockSize); #endif + + /* As in aset.c, free block-header vchunks explicitly */ + VALGRIND_MEMPOOL_FREE(slab, block); + free(block); slab->header.mem_allocated -= slab->blockSize; } diff --git a/src/backend/utils/postprocess_dtrace.sed b/src/backend/utils/postprocess_dtrace.sed index 842eb17baad3d..7ff3b35930ae7 100644 --- a/src/backend/utils/postprocess_dtrace.sed +++ b/src/backend/utils/postprocess_dtrace.sed @@ -1,7 +1,7 @@ #------------------------------------------------------------------------- # sed script to postprocess dtrace output # -# Copyright (c) 2008-2025, PostgreSQL Global Development Group +# Copyright (c) 2008-2026, PostgreSQL Global Development Group # # src/backend/utils/postprocess_dtrace.sed #------------------------------------------------------------------------- diff --git a/src/backend/utils/probes.d b/src/backend/utils/probes.d index e9e413477ba46..8b65353ea37fc 100644 --- a/src/backend/utils/probes.d +++ b/src/backend/utils/probes.d @@ -1,7 +1,7 @@ /* ---------- * DTrace probes for PostgreSQL backend * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * src/backend/utils/probes.d * ---------- @@ -91,4 +91,7 @@ provider postgresql { probe wal__switch(); probe wal__buffer__write__dirty__start(); probe wal__buffer__write__dirty__done(); + + probe wait__event__start(unsigned int); + probe wait__event__end(); }; diff --git a/src/backend/utils/resowner/meson.build b/src/backend/utils/resowner/meson.build index 3d7f914825f33..b1d254314331e 100644 --- a/src/backend/utils/resowner/meson.build +++ b/src/backend/utils/resowner/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'resowner.c' diff --git a/src/backend/utils/resowner/resowner.c b/src/backend/utils/resowner/resowner.c index d39f3e1b655cd..06e1121c5ffdd 100644 --- a/src/backend/utils/resowner/resowner.c +++ b/src/backend/utils/resowner/resowner.c @@ -34,7 +34,7 @@ * or reassigning locks from a resource owner to its parent. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -231,11 +231,8 @@ hash_resource_elem(Datum value, const ResourceOwnerDesc *kind) * 'kind' into the hash. Just add it with hash_combine(), it perturbs the * result enough for our purposes. */ -#if SIZEOF_DATUM == 8 - return hash_combine64(murmurhash64((uint64) value), (uint64) kind); -#else - return hash_combine(murmurhash32((uint32) value), (uint32) kind); -#endif + return hash_combine64(murmurhash64((uint64) value), + (uint64) (uintptr_t) kind); } /* diff --git a/src/backend/utils/sort/logtape.c b/src/backend/utils/sort/logtape.c index e529ceb8260bb..ebe2861cf5780 100644 --- a/src/backend/utils/sort/logtape.c +++ b/src/backend/utils/sort/logtape.c @@ -66,7 +66,7 @@ * There will always be the same number of runs as input tapes, and the same * number of input tapes as participants (worker Tuplesortstates). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -437,7 +437,7 @@ ltsGetPreallocBlock(LogicalTapeSet *lts, LogicalTape *lt) if (lt->prealloc == NULL) { lt->prealloc_size = TAPE_WRITE_PREALLOC_MIN; - lt->prealloc = (int64 *) palloc(sizeof(int64) * lt->prealloc_size); + lt->prealloc = palloc_array(int64, lt->prealloc_size); } else if (lt->prealloc_size < TAPE_WRITE_PREALLOC_MAX) { @@ -560,7 +560,7 @@ LogicalTapeSetCreate(bool preallocate, SharedFileSet *fileset, int worker) /* * Create top-level struct including per-tape LogicalTape structs. */ - lts = (LogicalTapeSet *) palloc(sizeof(LogicalTapeSet)); + lts = palloc_object(LogicalTapeSet); lts->nBlocksAllocated = 0L; lts->nBlocksWritten = 0L; lts->nHoleBlocks = 0L; @@ -681,7 +681,7 @@ LogicalTapeCreate(LogicalTapeSet *lts) { /* * The only thing that currently prevents creating new tapes in leader is - * the fact that BufFiles opened using BufFileOpenShared() are read-only + * the fact that BufFiles opened using BufFileOpenFileSet() are read-only * by definition, but that could be changed if it seemed worthwhile. For * now, writing to the leader tape will raise a "Bad file descriptor" * error, so tuplesort must avoid writing to the leader tape altogether. @@ -700,7 +700,7 @@ ltsCreateTape(LogicalTapeSet *lts) /* * Create per-tape struct. Note we allocate the I/O buffer lazily. */ - lt = palloc(sizeof(LogicalTape)); + lt = palloc_object(LogicalTape); lt->tapeSet = lts; lt->writing = true; lt->frozen = false; diff --git a/src/backend/utils/sort/meson.build b/src/backend/utils/sort/meson.build index 0743c56726252..fc1feec12416a 100644 --- a/src/backend/utils/sort/meson.build +++ b/src/backend/utils/sort/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'logtape.c', diff --git a/src/backend/utils/sort/sharedtuplestore.c b/src/backend/utils/sort/sharedtuplestore.c index 2f031c329094a..04189f708fa47 100644 --- a/src/backend/utils/sort/sharedtuplestore.c +++ b/src/backend/utils/sort/sharedtuplestore.c @@ -10,7 +10,7 @@ * scan where each backend reads an arbitrary subset of the tuples that were * written. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -88,7 +88,6 @@ struct SharedTuplestoreAccessor /* State for writing. */ SharedTuplestoreChunk *write_chunk; /* Buffer for writing. */ BufFile *write_file; /* The current file to write to. */ - BlockNumber write_page; /* The next page to write to. */ char *write_pointer; /* Current write pointer within chunk. */ char *write_end; /* One past the end of the current chunk. */ }; @@ -161,7 +160,7 @@ sts_initialize(SharedTuplestore *sts, int participants, sts->participants[i].writing = false; } - accessor = palloc0(sizeof(SharedTuplestoreAccessor)); + accessor = palloc0_object(SharedTuplestoreAccessor); accessor->participant = my_participant_number; accessor->sts = sts; accessor->fileset = fileset; @@ -183,7 +182,7 @@ sts_attach(SharedTuplestore *sts, Assert(my_participant_number < sts->nparticipants); - accessor = palloc0(sizeof(SharedTuplestoreAccessor)); + accessor = palloc0_object(SharedTuplestoreAccessor); accessor->participant = my_participant_number; accessor->sts = sts; accessor->fileset = fileset; @@ -324,7 +323,8 @@ sts_puttuple(SharedTuplestoreAccessor *accessor, void *meta_data, /* Do we have space? */ size = accessor->sts->meta_data_size + tuple->t_len; - if (accessor->write_pointer + size > accessor->write_end) + if (accessor->write_pointer == NULL || + accessor->write_pointer + size > accessor->write_end) { if (accessor->write_chunk == NULL) { diff --git a/src/backend/utils/sort/sortsupport.c b/src/backend/utils/sort/sortsupport.c index e0f500b9aa29c..c6f7f60760a5b 100644 --- a/src/backend/utils/sort/sortsupport.c +++ b/src/backend/utils/sort/sortsupport.c @@ -4,7 +4,7 @@ * Support routines for accelerated sorting. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -57,7 +57,7 @@ comparison_shim(Datum x, Datum y, SortSupport ssup) if (extra->fcinfo.isnull) elog(ERROR, "function %u returned NULL", extra->flinfo.fn_oid); - return result; + return DatumGetInt32(result); } /* diff --git a/src/backend/utils/sort/tuplesort.c b/src/backend/utils/sort/tuplesort.c index 65ab83fff8b26..72c2c2995d8da 100644 --- a/src/backend/utils/sort/tuplesort.c +++ b/src/backend/utils/sort/tuplesort.c @@ -7,8 +7,8 @@ * applied to different kinds of sortable objects. Implementation of * the particular sorting variants is given in tuplesortvariants.c. * This module works efficiently for both small and large amounts - * of data. Small amounts are sorted in-memory using qsort(). Large - * amounts are sorted using temporary files and a standard external sort + * of data. Small amounts are sorted in-memory. Large amounts are + * sorted using temporary files and a standard external sort * algorithm. * * See Knuth, volume 3, for more than you want to know about external @@ -26,16 +26,16 @@ * Historically, we divided the input into sorted runs using replacement * selection, in the form of a priority tree implemented as a heap * (essentially Knuth's Algorithm 5.2.3H), but now we always use quicksort - * for run generation. + * or radix sort for run generation. * * The approximate amount of memory allowed for any one sort operation * is specified in kilobytes by the caller (most pass work_mem). Initially, * we absorb tuples and simply store them in an unsorted array as long as * we haven't exceeded workMem. If we reach the end of the input without - * exceeding workMem, we sort the array using qsort() and subsequently return + * exceeding workMem, we sort the array in memory and subsequently return * tuples just by scanning the tuple array sequentially. If we do exceed * workMem, we begin to emit tuples into sorted runs in temporary tapes. - * When tuples are dumped in batch after quicksorting, we begin a new run + * When tuples are dumped in batch after in-memory sorting, we begin a new run * with a new output tape. If we reach the max number of tapes, we write * subsequent runs on the existing tapes in a round-robin fashion. We will * need multiple merge passes to finish the merge in that case. After the @@ -88,7 +88,7 @@ * produce exactly one output run from their partial input. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -104,6 +104,7 @@ #include "commands/tablespace.h" #include "miscadmin.h" #include "pg_trace.h" +#include "port/pg_bitutils.h" #include "storage/shmem.h" #include "utils/guc.h" #include "utils/memutils.h" @@ -111,11 +112,9 @@ #include "utils/tuplesort.h" /* - * Initial size of memtuples array. We're trying to select this size so that - * array doesn't exceed ALLOCSET_SEPARATE_THRESHOLD and so that the overhead of - * allocation might possibly be lowered. However, we don't consider array sizes - * less than 1024. - * + * Initial size of memtuples array. This must be more than + * ALLOCSET_SEPARATE_THRESHOLD; see comments in grow_memtuples(). Clamp at + * 1024 elements to avoid excessive reallocs. */ #define INITIAL_MEMTUPSIZE Max(1024, \ ALLOCSET_SEPARATE_THRESHOLD / sizeof(SortTuple) + 1) @@ -201,9 +200,8 @@ struct Tuplesortstate * pass */ int64 maxSpace; /* maximum amount of space occupied among sort * of groups, either in-memory or on-disk */ - bool isMaxSpaceDisk; /* true when maxSpace is value for on-disk - * space, false when its value for in-memory - * space */ + bool isMaxSpaceDisk; /* true when maxSpace tracks on-disk space, + * false means in-memory */ TupSortStatus maxSpaceStatus; /* sort status when maxSpace was reached */ LogicalTapeSet *tapeset; /* logtape.c object for tapes in a temp file */ @@ -479,125 +477,15 @@ static void free_sort_tuple(Tuplesortstate *state, SortTuple *stup); static void tuplesort_free(Tuplesortstate *state); static void tuplesort_updatemax(Tuplesortstate *state); -/* - * Specialized comparators that we can inline into specialized sorts. The goal - * is to try to sort two tuples without having to follow the pointers to the - * comparator or the tuple. - * - * XXX: For now, there is no specialization for cases where datum1 is - * authoritative and we don't even need to fall back to a callback at all (that - * would be true for types like int4/int8/timestamp/date, but not true for - * abbreviations of text or multi-key sorts. There could be! Is it worth it? - */ - -/* Used if first key's comparator is ssup_datum_unsigned_cmp */ -static pg_attribute_always_inline int -qsort_tuple_unsigned_compare(SortTuple *a, SortTuple *b, Tuplesortstate *state) -{ - int compare; - - compare = ApplyUnsignedSortComparator(a->datum1, a->isnull1, - b->datum1, b->isnull1, - &state->base.sortKeys[0]); - if (compare != 0) - return compare; - - /* - * No need to waste effort calling the tiebreak function when there are no - * other keys to sort on. - */ - if (state->base.onlyKey != NULL) - return 0; - - return state->base.comparetup_tiebreak(a, b, state); -} - -#if SIZEOF_DATUM >= 8 -/* Used if first key's comparator is ssup_datum_signed_cmp */ -static pg_attribute_always_inline int -qsort_tuple_signed_compare(SortTuple *a, SortTuple *b, Tuplesortstate *state) -{ - int compare; - - compare = ApplySignedSortComparator(a->datum1, a->isnull1, - b->datum1, b->isnull1, - &state->base.sortKeys[0]); - - if (compare != 0) - return compare; - - /* - * No need to waste effort calling the tiebreak function when there are no - * other keys to sort on. - */ - if (state->base.onlyKey != NULL) - return 0; - - return state->base.comparetup_tiebreak(a, b, state); -} -#endif - -/* Used if first key's comparator is ssup_datum_int32_cmp */ -static pg_attribute_always_inline int -qsort_tuple_int32_compare(SortTuple *a, SortTuple *b, Tuplesortstate *state) -{ - int compare; - - compare = ApplyInt32SortComparator(a->datum1, a->isnull1, - b->datum1, b->isnull1, - &state->base.sortKeys[0]); - - if (compare != 0) - return compare; - - /* - * No need to waste effort calling the tiebreak function when there are no - * other keys to sort on. - */ - if (state->base.onlyKey != NULL) - return 0; - - return state->base.comparetup_tiebreak(a, b, state); -} /* * Special versions of qsort just for SortTuple objects. qsort_tuple() sorts * any variant of SortTuples, using the appropriate comparetup function. * qsort_ssup() is specialized for the case where the comparetup function * reduces to ApplySortComparator(), that is single-key MinimalTuple sorts - * and Datum sorts. qsort_tuple_{unsigned,signed,int32} are specialized for - * common comparison functions on pass-by-value leading datums. + * and Datum sorts. */ -#define ST_SORT qsort_tuple_unsigned -#define ST_ELEMENT_TYPE SortTuple -#define ST_COMPARE(a, b, state) qsort_tuple_unsigned_compare(a, b, state) -#define ST_COMPARE_ARG_TYPE Tuplesortstate -#define ST_CHECK_FOR_INTERRUPTS -#define ST_SCOPE static -#define ST_DEFINE -#include "lib/sort_template.h" - -#if SIZEOF_DATUM >= 8 -#define ST_SORT qsort_tuple_signed -#define ST_ELEMENT_TYPE SortTuple -#define ST_COMPARE(a, b, state) qsort_tuple_signed_compare(a, b, state) -#define ST_COMPARE_ARG_TYPE Tuplesortstate -#define ST_CHECK_FOR_INTERRUPTS -#define ST_SCOPE static -#define ST_DEFINE -#include "lib/sort_template.h" -#endif - -#define ST_SORT qsort_tuple_int32 -#define ST_ELEMENT_TYPE SortTuple -#define ST_COMPARE(a, b, state) qsort_tuple_int32_compare(a, b, state) -#define ST_COMPARE_ARG_TYPE Tuplesortstate -#define ST_CHECK_FOR_INTERRUPTS -#define ST_SCOPE static -#define ST_DEFINE -#include "lib/sort_template.h" - #define ST_SORT qsort_tuple #define ST_ELEMENT_TYPE SortTuple #define ST_COMPARE_RUNTIME_POINTER @@ -619,6 +507,23 @@ qsort_tuple_int32_compare(SortTuple *a, SortTuple *b, Tuplesortstate *state) #define ST_DEFINE #include "lib/sort_template.h" +/* state for radix sort */ +typedef struct RadixSortInfo +{ + union + { + size_t count; + size_t offset; + }; + size_t next_offset; +} RadixSortInfo; + +/* + * Threshold below which qsort_tuple() is generally faster than a radix sort. + */ +#define QSORT_THRESHOLD 40 + + /* * tuplesort_begin_xxx * @@ -677,7 +582,7 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate, int sortopt) */ oldcontext = MemoryContextSwitchTo(maincontext); - state = (Tuplesortstate *) palloc0(sizeof(Tuplesortstate)); + state = palloc0_object(Tuplesortstate); if (trace_sort) pg_rusage_init(&state->ru_start); @@ -696,10 +601,6 @@ tuplesort_begin_common(int workMem, SortCoordinate coordinate, int sortopt) state->base.sortcontext = sortcontext; state->base.maincontext = maincontext; - /* - * Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD; - * see comments in grow_memtuples(). - */ state->memtupsize = INITIAL_MEMTUPSIZE; state->memtuples = NULL; @@ -788,10 +689,6 @@ tuplesort_begin_batch(Tuplesortstate *state) state->memtupcount = 0; - /* - * Initial size of array must be more than ALLOCSET_SEPARATE_THRESHOLD; - * see comments in grow_memtuples(). - */ state->growmemtuples = true; state->slabAllocatorUsed = false; if (state->memtuples != NULL && state->memtupsize != INITIAL_MEMTUPSIZE) @@ -1378,7 +1275,7 @@ tuplesort_performsort(Tuplesortstate *state) */ if (SERIAL(state)) { - /* Just qsort 'em and we're done */ + /* Sort in memory and we're done */ tuplesort_sort_memtuples(state); state->status = TSS_SORTEDINMEM; } @@ -2352,7 +2249,7 @@ dumptuples(Tuplesortstate *state, bool alltuples) /* * Sort all tuples accumulated within the allowed amount of memory for - * this run using quicksort + * this run. */ tuplesort_sort_memtuples(state); @@ -2667,45 +2564,456 @@ sort_bounded_heap(Tuplesortstate *state) state->boundUsed = true; } + +/* radix sort routines */ + /* - * Sort all memtuples using specialized qsort() routines. - * - * Quicksort is used for small in-memory sorts, and external sort runs. + * Retrieve byte from datum, indexed by 'level': 0 for MSB, 7 for LSB + */ +static inline uint8 +current_byte(Datum key, int level) +{ + int shift = (sizeof(Datum) - 1 - level) * BITS_PER_BYTE; + + return (key >> shift) & 0xFF; +} + +/* + * Normalize datum such that unsigned comparison is order-preserving, + * taking ASC/DESC into account as well. + */ +static inline Datum +normalize_datum(Datum orig, SortSupport ssup) +{ + Datum norm_datum1; + + if (ssup->comparator == ssup_datum_signed_cmp) + { + norm_datum1 = orig + ((uint64) PG_INT64_MAX) + 1; + } + else if (ssup->comparator == ssup_datum_int32_cmp) + { + /* + * First truncate to uint32. Technically, we don't need to do this, + * but it forces the upper half of the datum to be zero regardless of + * sign. + */ + uint32 u32 = DatumGetUInt32(orig) + ((uint32) PG_INT32_MAX) + 1; + + norm_datum1 = UInt32GetDatum(u32); + } + else + { + Assert(ssup->comparator == ssup_datum_unsigned_cmp); + norm_datum1 = orig; + } + + if (ssup->ssup_reverse) + norm_datum1 = ~norm_datum1; + + return norm_datum1; +} + +/* + * radix_sort_recursive + * + * Radix sort by (pass-by-value) datum1, diverting to qsort_tuple() + * for tiebreaks. + * + * This is a modification of + * ska_byte_sort() from https://github.com/skarupke/ska_sort + * The original copyright notice follows: + * + * Copyright Malte Skarupke 2016. + * Distributed under the Boost Software License, Version 1.0. + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. */ static void -tuplesort_sort_memtuples(Tuplesortstate *state) +radix_sort_recursive(SortTuple *begin, size_t n_elems, int level, Tuplesortstate *state) { - Assert(!LEADER(state)); + RadixSortInfo partitions[256] = {0}; + uint8 remaining_partitions[256]; + size_t total = 0; + int num_partitions = 0; + int num_remaining; + SortSupport ssup = &state->base.sortKeys[0]; + Datum ref_datum; + Datum common_upper_bits = 0; + size_t start_offset = 0; + SortTuple *partition_begin = begin; + int next_level; + + /* count number of occurrences of each byte */ + ref_datum = normalize_datum(begin[0].datum1, ssup); + for (SortTuple *st = begin; st < begin + n_elems; st++) + { + Datum this_datum; + uint8 this_partition; - if (state->memtupcount > 1) + this_datum = normalize_datum(st->datum1, ssup); + /* accumulate bits different from the reference datum */ + common_upper_bits |= ref_datum ^ this_datum; + + /* extract the byte for this level from the normalized datum */ + this_partition = current_byte(this_datum, level); + + /* save it for the permutation step */ + st->curbyte = this_partition; + + partitions[this_partition].count++; + + CHECK_FOR_INTERRUPTS(); + } + + /* compute partition offsets */ + for (int i = 0; i < 256; i++) + { + size_t count = partitions[i].count; + + if (count != 0) + { + partitions[i].offset = total; + total += count; + remaining_partitions[num_partitions] = i; + num_partitions++; + } + partitions[i].next_offset = total; + } + + /* + * Swap tuples to correct partition. + * + * In traditional American flag sort, a swap sends the current element to + * the correct partition, but the array pointer only advances if the + * partner of the swap happens to be an element that belongs in the + * current partition. That only requires one pass through the array, but + * the disadvantage is we don't know if the pointer can advance until the + * swap completes. Here lies the most interesting innovation from the + * upstream ska_byte_sort: After initiating the swap, we immediately + * proceed to the next element. This makes better use of CPU pipelining, + * but also means that we will often need multiple iterations of this + * loop. ska_byte_sort() maintains a separate list of which partitions + * haven't finished, which is updated every loop iteration. Here we simply + * check each partition during every iteration. + * + * If we started with a single partition, there is nothing to do. If a + * previous loop iteration results in only one partition that hasn't been + * counted as sorted, we know it's actually sorted and can exit the loop. + */ + num_remaining = num_partitions; + while (num_remaining > 1) + { + /* start the count over */ + num_remaining = num_partitions; + + for (int i = 0; i < num_partitions; i++) + { + uint8 idx = remaining_partitions[i]; + + for (SortTuple *st = begin + partitions[idx].offset; + st < begin + partitions[idx].next_offset; + st++) + { + size_t offset = partitions[st->curbyte].offset++; + SortTuple tmp; + + /* swap current tuple with destination position */ + Assert(offset < n_elems); + tmp = *st; + *st = begin[offset]; + begin[offset] = tmp; + + CHECK_FOR_INTERRUPTS(); + }; + + /* Is this partition sorted? */ + if (partitions[idx].offset == partitions[idx].next_offset) + num_remaining--; + } + } + + /* recurse */ + + if (num_partitions == 1) { /* - * Do we have the leading column's value or abbreviation in datum1, - * and is there a specialization for its comparator? + * There is only one distinct byte at the current level. It can happen + * that some subsequent bytes are also the same for all input values, + * such as the upper bytes of small integers. To skip unproductive + * passes for that case, we compute the level where the input has more + * than one distinct byte, so that the next recursion can start there. */ - if (state->base.haveDatum1 && state->base.sortKeys) + if (common_upper_bits == 0) + next_level = sizeof(Datum); + else + { + int diffpos; + + /* + * The upper bits of common_upper_bits are zero where all datums + * have the same bits. + */ + diffpos = pg_leftmost_one_pos64(DatumGetUInt64(common_upper_bits)); + next_level = sizeof(Datum) - 1 - (diffpos / BITS_PER_BYTE); + } + } + else + next_level = level + 1; + + for (uint8 *rp = remaining_partitions; + rp < remaining_partitions + num_partitions; + rp++) + { + size_t end_offset = partitions[*rp].next_offset; + SortTuple *partition_end = begin + end_offset; + size_t num_elements = end_offset - start_offset; + + if (num_elements > 1) { - if (state->base.sortKeys[0].comparator == ssup_datum_unsigned_cmp) + if (next_level < sizeof(Datum)) { - qsort_tuple_unsigned(state->memtuples, - state->memtupcount, - state); - return; + if (num_elements < QSORT_THRESHOLD) + { + qsort_tuple(partition_begin, + num_elements, + state->base.comparetup, + state); + } + else + { + radix_sort_recursive(partition_begin, + num_elements, + next_level, + state); + } } -#if SIZEOF_DATUM >= 8 - else if (state->base.sortKeys[0].comparator == ssup_datum_signed_cmp) + else if (state->base.onlyKey == NULL) { - qsort_tuple_signed(state->memtuples, - state->memtupcount, - state); - return; + /* + * We've finished radix sort on all bytes of the pass-by-value + * datum (possibly abbreviated), now sort using the tiebreak + * comparator. + */ + qsort_tuple(partition_begin, + num_elements, + state->base.comparetup_tiebreak, + state); } + } + + start_offset = end_offset; + partition_begin = partition_end; + } +} + +/* + * Entry point for radix_sort_recursive + * + * Partition tuples by isnull1, then sort both partitions, using + * radix sort on the NOT NULL partition if it's large enough. + */ +static void +radix_sort_tuple(SortTuple *data, size_t n, Tuplesortstate *state) +{ + bool nulls_first = state->base.sortKeys[0].ssup_nulls_first; + SortTuple *null_start; + SortTuple *not_null_start; + size_t d1 = 0, + d2, + null_count, + not_null_count; + + /* + * Find the first NOT NULL if NULLS FIRST, or first NULL if NULLS LAST. + * This also serves as a quick check for the common case where all tuples + * are NOT NULL in the first sort key. + */ + while (d1 < n && data[d1].isnull1 == nulls_first) + { + d1++; + CHECK_FOR_INTERRUPTS(); + } + + /* + * If we have more than one tuple left after the quick check, partition + * the remainder using branchless cyclic permutation, based on + * https://orlp.net/blog/branchless-lomuto-partitioning/ + */ + Assert(n > 0); + if (d1 < n - 1) + { + size_t i = d1, + j = d1; + SortTuple tmp = data[d1]; /* create gap at front */ + + while (j < n - 1) + { + /* gap is at j, move i's element to gap */ + data[j] = data[i]; + /* advance j to the first unknown element */ + j += 1; + /* move the first unknown element back to i */ + data[i] = data[j]; + /* advance i if this element belongs in the left partition */ + i += (data[i].isnull1 == nulls_first); + + CHECK_FOR_INTERRUPTS(); + } + + /* place gap between left and right partitions */ + data[j] = data[i]; + /* restore the saved element */ + data[i] = tmp; + /* assign it to the correct partition */ + i += (data[i].isnull1 == nulls_first); + + /* d1 is now the number of elements in the left partition */ + d1 = i; + } + + d2 = n - d1; + + /* set pointers and counts for each partition */ + if (nulls_first) + { + null_start = data; + null_count = d1; + not_null_start = data + d1; + not_null_count = d2; + } + else + { + not_null_start = data; + not_null_count = d1; + null_start = data + d1; + null_count = d2; + } + + for (SortTuple *st = null_start; + st < null_start + null_count; + st++) + Assert(st->isnull1 == true); + for (SortTuple *st = not_null_start; + st < not_null_start + not_null_count; + st++) + Assert(st->isnull1 == false); + + /* + * Sort the NULL partition using tiebreak comparator, if necessary. + */ + if (state->base.onlyKey == NULL && null_count > 1) + { + qsort_tuple(null_start, + null_count, + state->base.comparetup_tiebreak, + state); + } + + /* + * Sort the NOT NULL partition, using radix sort if large enough, + * otherwise fall back to quicksort. + */ + if (not_null_count < QSORT_THRESHOLD) + { + qsort_tuple(not_null_start, + not_null_count, + state->base.comparetup, + state); + } + else + { + bool presorted = true; + + for (SortTuple *st = not_null_start + 1; + st < not_null_start + not_null_count; + st++) + { + if (COMPARETUP(state, st - 1, st) > 0) + { + presorted = false; + break; + } + + CHECK_FOR_INTERRUPTS(); + } + + if (presorted) + return; + else + { + radix_sort_recursive(not_null_start, + not_null_count, + 0, + state); + } + } +} + +/* Verify in-memory sort using standard comparator. */ +static void +verify_memtuples_sorted(Tuplesortstate *state) +{ +#ifdef USE_ASSERT_CHECKING + for (SortTuple *st = state->memtuples + 1; + st < state->memtuples + state->memtupcount; + st++) + Assert(COMPARETUP(state, st - 1, st) <= 0); #endif - else if (state->base.sortKeys[0].comparator == ssup_datum_int32_cmp) +} + +/* + * Sort all memtuples using specialized routines. + * + * Quicksort or radix sort is used for small in-memory sorts, + * and external sort runs. + */ +static void +tuplesort_sort_memtuples(Tuplesortstate *state) +{ + Assert(!LEADER(state)); + + if (state->memtupcount > 1) + { + /* + * Do we have the leading column's value or abbreviation in datum1? + */ + if (state->base.haveDatum1 && state->base.sortKeys) + { + SortSupport ssup = &state->base.sortKeys[0]; + + /* Does it compare as an integer? */ + if (state->memtupcount >= QSORT_THRESHOLD && + (ssup->comparator == ssup_datum_unsigned_cmp || + ssup->comparator == ssup_datum_signed_cmp || + ssup->comparator == ssup_datum_int32_cmp)) { - qsort_tuple_int32(state->memtuples, - state->memtupcount, - state); + radix_sort_tuple(state->memtuples, + state->memtupcount, + state); + verify_memtuples_sorted(state); return; } } @@ -3146,7 +3454,6 @@ ssup_datum_unsigned_cmp(Datum x, Datum y, SortSupport ssup) return 0; } -#if SIZEOF_DATUM >= 8 int ssup_datum_signed_cmp(Datum x, Datum y, SortSupport ssup) { @@ -3160,7 +3467,6 @@ ssup_datum_signed_cmp(Datum x, Datum y, SortSupport ssup) else return 0; } -#endif int ssup_datum_int32_cmp(Datum x, Datum y, SortSupport ssup) diff --git a/src/backend/utils/sort/tuplesortvariants.c b/src/backend/utils/sort/tuplesortvariants.c index 5f70e8dddac57..2509ac3e3a4d6 100644 --- a/src/backend/utils/sort/tuplesortvariants.c +++ b/src/backend/utils/sort/tuplesortvariants.c @@ -9,7 +9,7 @@ * could be easily added here, another module, or even an extension. * * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/backend/utils/sort/tuplesortvariants.c @@ -20,6 +20,7 @@ #include "postgres.h" #include "access/brin_tuple.h" +#include "access/gin.h" #include "access/gin_tuple.h" #include "access/hash.h" #include "access/htup_details.h" @@ -28,9 +29,11 @@ #include "catalog/pg_collation.h" #include "executor/executor.h" #include "pg_trace.h" +#include "utils/builtins.h" #include "utils/datum.h" #include "utils/guc.h" #include "utils/lsyscache.h" +#include "utils/rel.h" #include "utils/tuplesort.h" @@ -264,7 +267,7 @@ tuplesort_begin_cluster(TupleDesc tupDesc, Assert(indexRel->rd_rel->relam == BTREE_AM_OID); oldcontext = MemoryContextSwitchTo(base->maincontext); - arg = (TuplesortClusterArg *) palloc0(sizeof(TuplesortClusterArg)); + arg = palloc0_object(TuplesortClusterArg); if (trace_sort) elog(LOG, @@ -371,7 +374,7 @@ tuplesort_begin_index_btree(Relation heapRel, int i; oldcontext = MemoryContextSwitchTo(base->maincontext); - arg = (TuplesortIndexBTreeArg *) palloc(sizeof(TuplesortIndexBTreeArg)); + arg = palloc_object(TuplesortIndexBTreeArg); if (trace_sort) elog(LOG, @@ -452,7 +455,7 @@ tuplesort_begin_index_hash(Relation heapRel, TuplesortIndexHashArg *arg; oldcontext = MemoryContextSwitchTo(base->maincontext); - arg = (TuplesortIndexHashArg *) palloc(sizeof(TuplesortIndexHashArg)); + arg = palloc_object(TuplesortIndexHashArg); if (trace_sort) elog(LOG, @@ -501,7 +504,7 @@ tuplesort_begin_index_gist(Relation heapRel, int i; oldcontext = MemoryContextSwitchTo(base->maincontext); - arg = (TuplesortIndexBTreeArg *) palloc(sizeof(TuplesortIndexBTreeArg)); + arg = palloc_object(TuplesortIndexBTreeArg); if (trace_sort) elog(LOG, @@ -614,7 +617,7 @@ tuplesort_begin_index_gin(Relation heapRel, { SortSupport sortKey = base->sortKeys + i; Form_pg_attribute att = TupleDescAttr(desc, i); - TypeCacheEntry *typentry; + Oid cmpFunc; sortKey->ssup_cxt = CurrentMemoryContext; sortKey->ssup_collation = indexRel->rd_indcollation[i]; @@ -628,11 +631,26 @@ tuplesort_begin_index_gin(Relation heapRel, sortKey->ssup_collation = DEFAULT_COLLATION_OID; /* - * Look for a ordering for the index key data type, and then the sort - * support function. + * If the compare proc isn't specified in the opclass definition, look + * up the index key type's default btree comparator. */ - typentry = lookup_type_cache(att->atttypid, TYPECACHE_LT_OPR); - PrepareSortSupportFromOrderingOp(typentry->lt_opr, sortKey); + cmpFunc = index_getprocid(indexRel, i + 1, GIN_COMPARE_PROC); + if (cmpFunc == InvalidOid) + { + TypeCacheEntry *typentry; + + typentry = lookup_type_cache(att->atttypid, + TYPECACHE_CMP_PROC_FINFO); + if (!OidIsValid(typentry->cmp_proc_finfo.fn_oid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_FUNCTION), + errmsg("could not identify a comparison function for type %s", + format_type_be(att->atttypid)))); + + cmpFunc = typentry->cmp_proc_finfo.fn_oid; + } + + PrepareSortSupportComparisonShim(cmpFunc, sortKey); } base->removeabbrev = removeabbrev_index_gin; @@ -661,7 +679,7 @@ tuplesort_begin_datum(Oid datumType, Oid sortOperator, Oid sortCollation, bool typbyval; oldcontext = MemoryContextSwitchTo(base->maincontext); - arg = (TuplesortDatumArg *) palloc(sizeof(TuplesortDatumArg)); + arg = palloc_object(TuplesortDatumArg); if (trace_sort) elog(LOG, @@ -693,7 +711,7 @@ tuplesort_begin_datum(Oid datumType, Oid sortOperator, Oid sortCollation, base->tuples = !typbyval; /* Prepare SortSupport data */ - base->sortKeys = (SortSupport) palloc0(sizeof(SortSupportData)); + base->sortKeys = palloc0_object(SortSupportData); base->sortKeys->ssup_cxt = CurrentMemoryContext; base->sortKeys->ssup_collation = sortCollation; @@ -815,7 +833,7 @@ tuplesort_putheaptuple(Tuplesortstate *state, HeapTuple tup) */ void tuplesort_putindextuplevalues(Tuplesortstate *state, Relation rel, - ItemPointer self, const Datum *values, + const ItemPointerData *self, const Datum *values, const bool *isnull) { SortTuple stup; @@ -865,7 +883,7 @@ tuplesort_putbrintuple(Tuplesortstate *state, BrinTuple *tuple, Size size) memcpy(&bstup->tuple, tuple, size); stup.tuple = bstup; - stup.datum1 = tuple->bt_blkno; + stup.datum1 = UInt32GetDatum(tuple->bt_blkno); stup.isnull1 = false; /* GetMemoryChunkSpace is not supported for bump contexts */ @@ -1131,7 +1149,6 @@ tuplesort_getgintuple(Tuplesortstate *state, Size *len, bool forward) * efficient, but only safe for callers that are prepared to have any * subsequent manipulation of the tuplesort's state invalidate slot contents. * For byval Datums, the value of the 'copy' parameter has no effect. - */ bool tuplesort_getdatum(Tuplesortstate *state, bool forward, bool copy, @@ -1836,7 +1853,7 @@ removeabbrev_index_brin(Tuplesortstate *state, SortTuple *stups, int count) BrinSortTuple *tuple; tuple = stups[i].tuple; - stups[i].datum1 = tuple->tuple.bt_blkno; + stups[i].datum1 = UInt32GetDatum(tuple->tuple.bt_blkno); } } @@ -1893,7 +1910,7 @@ readtup_index_brin(Tuplesortstate *state, SortTuple *stup, stup->tuple = tuple; /* set up first-column key value, which is block number */ - stup->datum1 = tuple->tuple.bt_blkno; + stup->datum1 = UInt32GetDatum(tuple->tuple.bt_blkno); } /* @@ -1953,7 +1970,7 @@ readtup_index_gin(Tuplesortstate *state, SortTuple *stup, LogicalTapeReadExact(tape, tuple, tuplen); if (base->sortopt & TUPLESORT_RANDOMACCESS) /* need trailing length word? */ LogicalTapeReadExact(tape, &tuplen, sizeof(tuplen)); - stup->tuple = (void *) tuple; + stup->tuple = tuple; /* no abbreviations (FIXME maybe use attrnum for this?) */ stup->datum1 = (Datum) 0; diff --git a/src/backend/utils/sort/tuplestore.c b/src/backend/utils/sort/tuplestore.c index c9aecab8d66cb..f9e2d95186a62 100644 --- a/src/backend/utils/sort/tuplestore.c +++ b/src/backend/utils/sort/tuplestore.c @@ -43,7 +43,7 @@ * before switching to the other state or activating a different read pointer. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -63,6 +63,7 @@ #include "storage/buffile.h" #include "utils/memutils.h" #include "utils/resowner.h" +#include "utils/tuplestore.h" /* @@ -94,7 +95,7 @@ typedef struct bool eof_reached; /* read has reached EOF */ int current; /* next array index to read */ int file; /* temp file# */ - off_t offset; /* byte offset in file */ + pgoff_t offset; /* byte offset in file */ } TSReadPointer; /* @@ -179,7 +180,7 @@ struct Tuplestorestate int readptrsize; /* allocated length of readptrs array */ int writepos_file; /* file# (valid if READFILE state) */ - off_t writepos_offset; /* offset (valid if READFILE state) */ + pgoff_t writepos_offset; /* offset (valid if READFILE state) */ }; #define COPYTUP(state,tup) ((*(state)->copytup) (state, tup)) @@ -257,7 +258,7 @@ tuplestore_begin_common(int eflags, bool interXact, int maxKBytes) { Tuplestorestate *state; - state = (Tuplestorestate *) palloc0(sizeof(Tuplestorestate)); + state = palloc0_object(Tuplestorestate); state->status = TSS_INMEM; state->eflags = eflags; @@ -707,10 +708,10 @@ grow_memtuples(Tuplestorestate *state) /* OK, do it */ FREEMEM(state, GetMemoryChunkSpace(state->memtuples)); - state->memtupsize = newmemtupsize; state->memtuples = (void **) repalloc_huge(state->memtuples, - state->memtupsize * sizeof(void *)); + newmemtupsize * sizeof(void *)); + state->memtupsize = newmemtupsize; USEMEM(state, GetMemoryChunkSpace(state->memtuples)); if (LACKMEM(state)) elog(ERROR, "unexpected out-of-memory situation in tuplestore"); @@ -1024,7 +1025,7 @@ tuplestore_gettuple(Tuplestorestate *state, bool forward, (errcode_for_file_access(), errmsg("could not seek in tuplestore temporary file"))); state->status = TSS_READFILE; - /* FALLTHROUGH */ + pg_fallthrough; case TSS_READFILE: *should_free = true; @@ -1051,7 +1052,7 @@ tuplestore_gettuple(Tuplestorestate *state, bool forward, * Back up to fetch previously-returned tuple's ending length * word. If seek fails, assume we are at start of file. */ - if (BufFileSeek(state->myfile, 0, -(long) sizeof(unsigned int), + if (BufFileSeek(state->myfile, 0, -(pgoff_t) sizeof(unsigned int), SEEK_CUR) != 0) { /* even a failed backwards fetch gets you out of eof state */ @@ -1072,7 +1073,7 @@ tuplestore_gettuple(Tuplestorestate *state, bool forward, * Back up to get ending length word of tuple before it. */ if (BufFileSeek(state->myfile, 0, - -(long) (tuplen + 2 * sizeof(unsigned int)), + -(pgoff_t) (tuplen + 2 * sizeof(unsigned int)), SEEK_CUR) != 0) { /* @@ -1082,7 +1083,7 @@ tuplestore_gettuple(Tuplestorestate *state, bool forward, * what in-memory case does). */ if (BufFileSeek(state->myfile, 0, - -(long) (tuplen + sizeof(unsigned int)), + -(pgoff_t) (tuplen + sizeof(unsigned int)), SEEK_CUR) != 0) ereport(ERROR, (errcode_for_file_access(), @@ -1099,7 +1100,7 @@ tuplestore_gettuple(Tuplestorestate *state, bool forward, * length word of the tuple, so back up to that point. */ if (BufFileSeek(state->myfile, 0, - -(long) tuplen, + -(pgoff_t) tuplen, SEEK_CUR) != 0) ereport(ERROR, (errcode_for_file_access(), @@ -1152,6 +1153,38 @@ tuplestore_gettupleslot(Tuplestorestate *state, bool forward, } } +/* + * tuplestore_gettupleslot_force - exported function to fetch a tuple + * + * This is identical to tuplestore_gettupleslot except the given slot can be + * any kind of slot; it need not be one that will accept a MinimalTuple. + */ +bool +tuplestore_gettupleslot_force(Tuplestorestate *state, bool forward, + bool copy, TupleTableSlot *slot) +{ + MinimalTuple tuple; + bool should_free; + + tuple = (MinimalTuple) tuplestore_gettuple(state, forward, &should_free); + + if (tuple) + { + if (copy && !should_free) + { + tuple = heap_copy_minimal_tuple(tuple, 0); + should_free = true; + } + ExecForceStoreMinimalTuple(tuple, slot, should_free); + return true; + } + else + { + ExecClearTuple(slot); + return false; + } +} + /* * tuplestore_advance - exported function to adjust position without fetching * @@ -1273,7 +1306,19 @@ dumptuples(Tuplestorestate *state) if (i >= state->memtupcount) break; WRITETUP(state, state->memtuples[i]); + + /* + * Increase memtupdeleted to track the fact that we just deleted that + * tuple. Think not to remove this on the grounds that we'll reset + * memtupdeleted to zero below. We might not reach that if some later + * WRITETUP fails (e.g. due to overrunning temp_file_limit). If so, + * we'd error out leaving an effectively-corrupt tuplestore, which + * would be quite bad if it's a persistent data structure such as a + * Portal's holdStore. + */ + state->memtupdeleted++; } + /* Now we can reset memtupdeleted along with memtupcount */ state->memtupdeleted = 0; state->memtupcount = 0; } @@ -1463,8 +1508,10 @@ tuplestore_trim(Tuplestorestate *state) FREEMEM(state, GetMemoryChunkSpace(state->memtuples[i])); pfree(state->memtuples[i]); state->memtuples[i] = NULL; + /* As in dumptuples(), increment memtupdeleted synchronously */ + state->memtupdeleted++; } - state->memtupdeleted = nremove; + Assert(state->memtupdeleted == nremove); /* mark tuplestore as truncated (used for Assert crosschecks only) */ state->truncated = true; diff --git a/src/backend/utils/time/combocid.c b/src/backend/utils/time/combocid.c index 1e81557157090..614b7c1006bf7 100644 --- a/src/backend/utils/time/combocid.c +++ b/src/backend/utils/time/combocid.c @@ -30,7 +30,7 @@ * destroyed at the end of each transaction. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/backend/utils/time/meson.build b/src/backend/utils/time/meson.build index be40f43ab9450..bce856a4e423b 100644 --- a/src/backend/utils/time/meson.build +++ b/src/backend/utils/time/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group backend_sources += files( 'combocid.c', diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c index ea35f30f49457..10fe18df2e7a4 100644 --- a/src/backend/utils/time/snapmgr.c +++ b/src/backend/utils/time/snapmgr.c @@ -94,7 +94,7 @@ * stack is empty. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -119,6 +119,7 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/builtins.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/resowner.h" #include "utils/snapmgr.h" @@ -271,12 +272,23 @@ Snapshot GetTransactionSnapshot(void) { /* - * This should not be called while doing logical decoding. Historic - * snapshots are only usable for catalog access, not for general-purpose - * queries. + * Return historic snapshot if doing logical decoding. + * + * Historic snapshots are only usable for catalog access, not for + * general-purpose queries. The caller is responsible for ensuring that + * the snapshot is used correctly! (PostgreSQL code never calls this + * during logical decoding, but extensions can do it.) */ if (HistoricSnapshotActive()) - elog(ERROR, "cannot take query snapshot during logical decoding"); + { + /* + * We'll never need a non-historic transaction snapshot in this + * (sub-)transaction, so there's no need to be careful to set one up + * for later calls to GetTransactionSnapshot(). + */ + Assert(!FirstSnapshotSet); + return HistoricSnapshot; + } /* First call in transaction? */ if (!FirstSnapshotSet) @@ -447,6 +459,7 @@ InvalidateCatalogSnapshot(void) pairingheap_remove(&RegisteredSnapshots, &CatalogSnapshot->ph_node); CatalogSnapshot = NULL; SnapshotResetXmin(); + INJECTION_POINT("invalidate-catalog-snapshot-end", NULL); } } @@ -1166,7 +1179,7 @@ ExportSnapshot(Snapshot snapshot) snapshot = CopySnapshot(snapshot); oldcxt = MemoryContextSwitchTo(TopTransactionContext); - esnap = (ExportedSnapshot *) palloc(sizeof(ExportedSnapshot)); + esnap = palloc_object(ExportedSnapshot); esnap->snapfile = pstrdup(path); esnap->snapshot = snapshot; exportedSnapshots = lappend(exportedSnapshots, esnap); @@ -1722,7 +1735,7 @@ EstimateSnapshotSpace(Snapshot snapshot) void SerializeSnapshot(Snapshot snapshot, char *start_address) { - SerializedSnapshotData serialized_snapshot; + SerializedSnapshotData serialized_snapshot = {0}; Assert(snapshot->subxcnt >= 0); @@ -1835,12 +1848,9 @@ RestoreSnapshot(char *start_address) /* * Install a restored snapshot as the transaction snapshot. - * - * The second argument is of type void * so that snapmgr.h need not include - * the declaration for PGPROC. */ void -RestoreTransactionSnapshot(Snapshot snapshot, void *source_pgproc) +RestoreTransactionSnapshot(Snapshot snapshot, PGPROC *source_pgproc) { SetTransactionSnapshot(snapshot, NULL, InvalidPid, source_pgproc); } diff --git a/src/bin/Makefile b/src/bin/Makefile index ac50cf93cb67e..538af88a5237f 100644 --- a/src/bin/Makefile +++ b/src/bin/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin (client programs) # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/Makefile diff --git a/src/bin/initdb/Makefile b/src/bin/initdb/Makefile index 997e0a013e956..21b755025ad61 100644 --- a/src/bin/initdb/Makefile +++ b/src/bin/initdb/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/initdb # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/initdb/Makefile @@ -20,7 +20,7 @@ include $(top_builddir)/src/Makefile.global # from libpq, else we have risks of version skew if we run with a libpq # shared library from a different PG version. Define # USE_PRIVATE_ENCODING_FUNCS to ensure that that happens. -override CPPFLAGS := -DUSE_PRIVATE_ENCODING_FUNCS -I$(libpq_srcdir) -I$(top_srcdir)/src/timezone $(ICU_CFLAGS) $(CPPFLAGS) +override CPPFLAGS := -DUSE_PRIVATE_ENCODING_FUNCS -I$(libpq_srcdir) -I$(top_srcdir)/src/timezone $(CPPFLAGS) $(ICU_CFLAGS) # We need libpq only because fe_utils does. LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) $(ICU_LIBS) diff --git a/src/bin/initdb/findtimezone.c b/src/bin/initdb/findtimezone.c index 2b2ae39adf34f..910b97a570b57 100644 --- a/src/bin/initdb/findtimezone.c +++ b/src/bin/initdb/findtimezone.c @@ -3,7 +3,7 @@ * findtimezone.c * Functions for determining the default timezone to use. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/initdb/findtimezone.c diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 62bbd08d9f658..14cb79c26be04 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -38,7 +38,7 @@ * * This code is released under the terms of the PostgreSQL License. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/initdb/initdb.c @@ -177,6 +177,7 @@ static int encodingid; static char *bki_file; static char *hba_file; static char *ident_file; +static char *hosts_file; static char *conf_file; static char *dictionary_file; static char *info_schema_file; @@ -444,7 +445,7 @@ escape_quotes_bki(const char *src) static void add_stringlist_item(_stringlist **listhead, const char *str) { - _stringlist *newentry = pg_malloc(sizeof(_stringlist)); + _stringlist *newentry = pg_malloc_object(_stringlist); _stringlist *oldentry; newentry->str = pg_strdup(str); @@ -461,7 +462,8 @@ add_stringlist_item(_stringlist **listhead, const char *str) /* * Modify the array of lines, replacing "token" by "replacement" - * the first time it occurs on each line. + * the first time it occurs on each line. To prevent false matches, the + * occurrence of "token" must be surrounded by whitespace or line start/end. * * The array must be a malloc'd array of individually malloc'd strings. * We free any discarded strings. @@ -483,6 +485,7 @@ replace_token(char **lines, const char *token, const char *replacement) for (int i = 0; lines[i]; i++) { char *where; + char *endwhere; char *newline; int pre; @@ -490,6 +493,17 @@ replace_token(char **lines, const char *token, const char *replacement) if ((where = strstr(lines[i], token)) == NULL) continue; + /* + * Reject false match. Note a blind spot: we don't check for a valid + * match following a false match. That case can't occur at present, + * so not worth complicating this code for it. + */ + if (!(where == lines[i] || isspace((unsigned char) where[-1]))) + continue; + endwhere = where + strlen(token); + if (!(*endwhere == '\0' || isspace((unsigned char) *endwhere))) + continue; + /* if we get here a change is needed - set up new line */ newline = (char *) pg_malloc(strlen(lines[i]) + diff + 1); @@ -687,7 +701,7 @@ readfile(const char *path) initStringInfo(&line); maxlines = 1024; - result = (char **) pg_malloc(maxlines * sizeof(char *)); + result = pg_malloc_array(char *, maxlines); n = 0; while (pg_get_line_buf(infile, &line)) @@ -696,7 +710,7 @@ readfile(const char *path) if (n >= maxlines - 1) { maxlines *= 2; - result = (char **) pg_realloc(result, maxlines * sizeof(char *)); + result = pg_realloc_array(result, char *, maxlines); } result[n++] = pg_strdup(line.data); @@ -910,6 +924,8 @@ static const struct tsearch_config_match tsearch_config_languages[] = {"nepali", "Nepali"}, {"norwegian", "no"}, {"norwegian", "Norwegian"}, + {"polish", "pl"}, + {"polish", "Polish"}, {"portuguese", "pt"}, {"portuguese", "Portuguese"}, {"romanian", "ro"}, @@ -1424,6 +1440,11 @@ setup_config(void) "0640", false); } +#if USE_LZ4 + conflines = replace_guc_value(conflines, "default_toast_compression", + "lz4", true); +#endif + /* * Now replace anything that's overridden via -c switches. */ @@ -1461,9 +1482,6 @@ setup_config(void) conflines = readfile(hba_file); - conflines = replace_token(conflines, "@remove-line-for-nolocal@", ""); - - /* * Probe to see if there is really any platform support for IPv6, and * comment out the relevant pg_hba line if not. This avoids runtime @@ -1497,11 +1515,11 @@ setup_config(void) getaddrinfo("::1", NULL, &hints, &gai_result) != 0) { conflines = replace_token(conflines, - "host all all ::1", - "#host all all ::1"); + "host all all ::1/128", + "#host all all ::1/128"); conflines = replace_token(conflines, - "host replication all ::1", - "#host replication all ::1"); + "host replication all ::1/128", + "#host replication all ::1/128"); } } @@ -1530,6 +1548,14 @@ setup_config(void) snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data); + writefile(path, conflines); + if (chmod(path, pg_file_create_mode) != 0) + pg_fatal("could not change permissions of \"%s\": %m", path); + + /* pg_hosts.conf */ + conflines = readfile(hosts_file); + snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data); + writefile(path, conflines); if (chmod(path, pg_file_create_mode) != 0) pg_fatal("could not change permissions of \"%s\": %m", path); @@ -1580,9 +1606,6 @@ bootstrap_template1(void) bki_lines = replace_token(bki_lines, "ALIGNOF_POINTER", (sizeof(Pointer) == 4) ? "i" : "d"); - bki_lines = replace_token(bki_lines, "FLOAT8PASSBYVAL", - FLOAT8PASSBYVAL ? "true" : "false"); - bki_lines = replace_token(bki_lines, "POSTGRES", escape_quotes_bki(username)); @@ -2380,7 +2403,7 @@ icu_validate_locale(const char *loc_str) /* validate that we can extract the language */ status = U_ZERO_ERROR; uloc_getLanguage(loc_str, lang, ULOC_LANG_CAPACITY, &status); - if (U_FAILURE(status)) + if (U_FAILURE(status) || status == U_STRING_NOT_TERMINATED_WARNING) { pg_fatal("could not get language from locale \"%s\": %s", loc_str, u_errorName(status)); @@ -2400,7 +2423,7 @@ icu_validate_locale(const char *loc_str) status = U_ZERO_ERROR; uloc_getLanguage(otherloc, otherlang, ULOC_LANG_CAPACITY, &status); - if (U_FAILURE(status)) + if (U_FAILURE(status) || status == U_STRING_NOT_TERMINATED_WARNING) continue; if (strcmp(lang, otherlang) == 0) @@ -2794,6 +2817,7 @@ setup_data_file_paths(void) set_input(&bki_file, "postgres.bki"); set_input(&hba_file, "pg_hba.conf.sample"); set_input(&ident_file, "pg_ident.conf.sample"); + set_input(&hosts_file, "pg_hosts.conf.sample"); set_input(&conf_file, "postgresql.conf.sample"); set_input(&dictionary_file, "snowball_create.sql"); set_input(&info_schema_file, "information_schema.sql"); @@ -2809,12 +2833,12 @@ setup_data_file_paths(void) "PGDATA=%s\nshare_path=%s\nPGPATH=%s\n" "POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n" "POSTGRESQL_CONF_SAMPLE=%s\n" - "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n", + "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n", PG_VERSION, pg_data, share_path, bin_path, username, bki_file, conf_file, - hba_file, ident_file); + hba_file, ident_file, hosts_file); if (show_setting) exit(0); } @@ -2822,6 +2846,7 @@ setup_data_file_paths(void) check_input(bki_file); check_input(hba_file); check_input(ident_file); + check_input(hosts_file); check_input(conf_file); check_input(dictionary_file); check_input(info_schema_file); @@ -2878,10 +2903,10 @@ setup_signals(void) pqsignal(SIGQUIT, trapsig); /* Ignore SIGPIPE when writing to backend, so we can clean up */ - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); /* Prevent SIGSYS so we can probe for kernel calls that might not work */ - pqsignal(SIGSYS, SIG_IGN); + pqsignal(SIGSYS, PG_SIG_IGN); #endif } diff --git a/src/bin/initdb/meson.build b/src/bin/initdb/meson.build index 06958e370f320..bc6eb2e085ca4 100644 --- a/src/bin/initdb/meson.build +++ b/src/bin/initdb/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group initdb_sources = files( 'findtimezone.c', diff --git a/src/bin/initdb/po/meson.build b/src/bin/initdb/po/meson.build index 0eeeaca8cc1eb..57f98da904e32 100644 --- a/src/bin/initdb/po/meson.build +++ b/src/bin/initdb/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('initdb-' + pg_version_major.to_string())] diff --git a/src/bin/initdb/t/001_initdb.pl b/src/bin/initdb/t/001_initdb.pl index 15dd10ce40a31..081535a22e41b 100644 --- a/src/bin/initdb/t/001_initdb.pl +++ b/src/bin/initdb/t/001_initdb.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # To test successful data directory creation with an additional feature, first # try to elaborate the "successful creation" test instead of adding a test. @@ -76,7 +76,8 @@ 'checksums are enabled in control file'); command_ok([ 'initdb', '--sync-only', $datadir ], 'sync only'); -command_ok([ 'initdb', '--sync-only', '--no-sync-data-files', $datadir ], '--no-sync-data-files'); +command_ok([ 'initdb', '--sync-only', '--no-sync-data-files', $datadir ], + '--no-sync-data-files'); command_fails([ 'initdb', $datadir ], 'existing data directory'); if ($supports_syncfs) @@ -307,9 +308,9 @@ 'multiple --set options with different case'); my $conf = slurp_file("$tempdir/dataY/postgresql.conf"); -ok($conf !~ qr/^WORK_MEM = /m, "WORK_MEM should not be configured"); -ok($conf !~ qr/^Work_Mem = /m, "Work_Mem should not be configured"); -ok($conf =~ qr/^work_mem = 512/m, "work_mem should be in config"); +unlike($conf, qr/^WORK_MEM = /m, "WORK_MEM should not be configured"); +unlike($conf, qr/^Work_Mem = /m, "Work_Mem should not be configured"); +like($conf, qr/^work_mem = 512/m, "work_mem should be in config"); # Test the no-data-checksums flag my $datadir_nochecksums = "$tempdir/data_no_checksums"; diff --git a/src/bin/meson.build b/src/bin/meson.build index b33cb6c75bf88..bf765381d8964 100644 --- a/src/bin/meson.build +++ b/src/bin/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('initdb') subdir('pg_amcheck') diff --git a/src/bin/pg_amcheck/Makefile b/src/bin/pg_amcheck/Makefile index fa6071f97c186..f7a9640033bce 100644 --- a/src/bin/pg_amcheck/Makefile +++ b/src/bin/pg_amcheck/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_amcheck # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/pg_amcheck/Makefile diff --git a/src/bin/pg_amcheck/meson.build b/src/bin/pg_amcheck/meson.build index 316ea0d40b8c2..592cef74ecb9a 100644 --- a/src/bin/pg_amcheck/meson.build +++ b/src/bin/pg_amcheck/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_amcheck_sources = files( 'pg_amcheck.c', diff --git a/src/bin/pg_amcheck/pg_amcheck.c b/src/bin/pg_amcheck/pg_amcheck.c index 2b1fd566c353f..09ba0596400b5 100644 --- a/src/bin/pg_amcheck/pg_amcheck.c +++ b/src/bin/pg_amcheck/pg_amcheck.c @@ -3,7 +3,7 @@ * pg_amcheck.c * Detects corruption within database relations. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_amcheck/pg_amcheck.c @@ -1338,7 +1338,7 @@ extend_pattern_info_array(PatternInfoArray *pia) PatternInfo *result; pia->len++; - pia->data = (PatternInfo *) pg_realloc(pia->data, pia->len * sizeof(PatternInfo)); + pia->data = pg_realloc_array(pia->data, PatternInfo, pia->len); result = &pia->data[pia->len - 1]; memset(result, 0, sizeof(*result)); @@ -1593,7 +1593,7 @@ compile_database_list(PGconn *conn, SimplePtrList *databases, if (initial_dbname) { - DatabaseInfo *dat = (DatabaseInfo *) pg_malloc0(sizeof(DatabaseInfo)); + DatabaseInfo *dat = pg_malloc0_object(DatabaseInfo); /* This database is included. Add to list */ if (opts.verbose) @@ -1738,7 +1738,7 @@ compile_database_list(PGconn *conn, SimplePtrList *databases, if (opts.verbose) pg_log_info("including database \"%s\"", datname); - dat = (DatabaseInfo *) pg_malloc0(sizeof(DatabaseInfo)); + dat = pg_malloc0_object(DatabaseInfo); dat->datname = pstrdup(datname); simple_ptr_list_append(databases, dat); } @@ -2202,7 +2202,7 @@ compile_relation_list_one_db(PGconn *conn, SimplePtrList *relations, { /* Current record pertains to a relation */ - RelationInfo *rel = (RelationInfo *) pg_malloc0(sizeof(RelationInfo)); + RelationInfo *rel = pg_malloc0_object(RelationInfo); Assert(OidIsValid(oid)); Assert((is_heap && !is_btree) || (is_btree && !is_heap)); diff --git a/src/bin/pg_amcheck/po/meson.build b/src/bin/pg_amcheck/po/meson.build index d1bfa58cd795e..649108fccbfa4 100644 --- a/src/bin/pg_amcheck/po/meson.build +++ b/src/bin/pg_amcheck/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_amcheck-' + pg_version_major.to_string())] diff --git a/src/bin/pg_amcheck/t/001_basic.pl b/src/bin/pg_amcheck/t/001_basic.pl index 462793977da80..6fd5d634eb68c 100644 --- a/src/bin/pg_amcheck/t/001_basic.pl +++ b/src/bin/pg_amcheck/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_amcheck/t/002_nonesuch.pl b/src/bin/pg_amcheck/t/002_nonesuch.pl index f23368abeab3b..ee4a2dc3b0268 100644 --- a/src/bin/pg_amcheck/t/002_nonesuch.pl +++ b/src/bin/pg_amcheck/t/002_nonesuch.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_amcheck/t/003_check.pl b/src/bin/pg_amcheck/t/003_check.pl index 881854da254b1..ee714c3fc8fa3 100644 --- a/src/bin/pg_amcheck/t/003_check.pl +++ b/src/bin/pg_amcheck/t/003_check.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl index 2a3af2666f52a..95f1f34c90dc6 100644 --- a/src/bin/pg_amcheck/t/004_verify_heapam.pl +++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -529,7 +529,7 @@ sub header $tup->{t_infomask2} |= HEAP_NATTS_MASK; push @expected, - qr/${$header}number of attributes 2047 exceeds maximum expected for table 3/; + qr/${$header}number of attributes 2047 exceeds maximum 3 expected for table/; } elsif ($offnum == 10) { @@ -552,7 +552,7 @@ sub header $tup->{t_hoff} = 32; push @expected, - qr/${$header}number of attributes 67 exceeds maximum expected for table 3/; + qr/${$header}number of attributes 67 exceeds maximum 3 expected for table/; } elsif ($offnum == 12) { diff --git a/src/bin/pg_amcheck/t/005_opclass_damage.pl b/src/bin/pg_amcheck/t/005_opclass_damage.pl index 775014aabdc45..7bee1073bc330 100644 --- a/src/bin/pg_amcheck/t/005_opclass_damage.pl +++ b/src/bin/pg_amcheck/t/005_opclass_damage.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This regression test checks the behavior of the btree validation in the # presence of breaking sort order changes. diff --git a/src/bin/pg_archivecleanup/meson.build b/src/bin/pg_archivecleanup/meson.build index 7590cecee3442..4527a3816b35c 100644 --- a/src/bin/pg_archivecleanup/meson.build +++ b/src/bin/pg_archivecleanup/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_archivecleanup_sources = files( 'pg_archivecleanup.c', diff --git a/src/bin/pg_archivecleanup/pg_archivecleanup.c b/src/bin/pg_archivecleanup/pg_archivecleanup.c index c25348bcb85dd..ab686b4748ca4 100644 --- a/src/bin/pg_archivecleanup/pg_archivecleanup.c +++ b/src/bin/pg_archivecleanup/pg_archivecleanup.c @@ -375,6 +375,10 @@ main(int argc, char **argv) exit(2); } + if (dryrun) + pg_log_info("Executing in dry-run mode.\n" + "No files will be removed."); + /* * Check archive exists and other initialization if required. */ diff --git a/src/bin/pg_archivecleanup/po/meson.build b/src/bin/pg_archivecleanup/po/meson.build index 0d65690246d44..3670e26ba6a3b 100644 --- a/src/bin/pg_archivecleanup/po/meson.build +++ b/src/bin/pg_archivecleanup/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_archivecleanup-' + pg_version_major.to_string())] diff --git a/src/bin/pg_archivecleanup/t/010_pg_archivecleanup.pl b/src/bin/pg_archivecleanup/t/010_pg_archivecleanup.pl index c6148cda7fc2f..6fb67d581d25f 100644 --- a/src/bin/pg_archivecleanup/t/010_pg_archivecleanup.pl +++ b/src/bin/pg_archivecleanup/t/010_pg_archivecleanup.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile index a9557c0789f6f..df94fc27d0293 100644 --- a/src/bin/pg_basebackup/Makefile +++ b/src/bin/pg_basebackup/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_basebackup # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/pg_basebackup/Makefile diff --git a/src/bin/pg_basebackup/astreamer_inject.c b/src/bin/pg_basebackup/astreamer_inject.c index 15334e458ad1e..a5dff0ac1c6d1 100644 --- a/src/bin/pg_basebackup/astreamer_inject.c +++ b/src/bin/pg_basebackup/astreamer_inject.c @@ -2,7 +2,7 @@ * * astreamer_inject.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/astreamer_inject.c @@ -68,7 +68,7 @@ astreamer_recovery_injector_new(astreamer *next, { astreamer_recovery_injector *streamer; - streamer = palloc0(sizeof(astreamer_recovery_injector)); + streamer = palloc0_object(astreamer_recovery_injector); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_recovery_injector_ops; streamer->base.bbs_next = next; @@ -224,8 +224,9 @@ astreamer_inject_file(astreamer *streamer, char *pathname, char *data, strlcpy(member.pathname, pathname, MAXPGPATH); member.size = len; member.mode = pg_file_create_mode; + member.is_regular = true; member.is_directory = false; - member.is_link = false; + member.is_symlink = false; member.linktarget[0] = '\0'; /* diff --git a/src/bin/pg_basebackup/astreamer_inject.h b/src/bin/pg_basebackup/astreamer_inject.h index cd11685588423..153558a371d5b 100644 --- a/src/bin/pg_basebackup/astreamer_inject.h +++ b/src/bin/pg_basebackup/astreamer_inject.h @@ -2,7 +2,7 @@ * * astreamer_inject.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/astreamer_inject.h diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build index 8a1c96b4f5c84..d70ce5786a261 100644 --- a/src/bin/pg_basebackup/meson.build +++ b/src/bin/pg_basebackup/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group common_sources = files( 'astreamer_inject.c', @@ -93,9 +93,9 @@ tests += { 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { - 'env': {'GZIP_PROGRAM': gzip.found() ? gzip.path() : '', - 'TAR': tar.found() ? tar.path() : '', - 'LZ4': program_lz4.found() ? program_lz4.path() : '', + 'env': {'GZIP_PROGRAM': gzip.found() ? gzip.full_path() : '', + 'TAR': tar.found() ? tar.full_path() : '', + 'LZ4': program_lz4.found() ? program_lz4.full_path() : '', }, 'tests': [ 't/010_pg_basebackup.pl', diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c index eb7354200bcee..c1a4672aa6fd7 100644 --- a/src/bin/pg_basebackup/pg_basebackup.c +++ b/src/bin/pg_basebackup/pg_basebackup.c @@ -4,7 +4,7 @@ * * Author: Magnus Hagander * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/pg_basebackup.c @@ -35,6 +35,7 @@ #include "fe_utils/option_utils.h" #include "fe_utils/recovery_gen.h" #include "getopt_long.h" +#include "libpq/protocol.h" #include "receivelog.h" #include "streamutil.h" @@ -319,7 +320,7 @@ kill_bgchild_atexit(void) static void tablespace_list_append(const char *arg) { - TablespaceListCell *cell = (TablespaceListCell *) pg_malloc0(sizeof(TablespaceListCell)); + TablespaceListCell *cell = pg_malloc0_object(TablespaceListCell); char *dst; char *dst_ptr; const char *arg_ptr; @@ -487,7 +488,7 @@ reached_end_position(XLogRecPtr segendpos, uint32 timeline, if (r < 0) pg_fatal("could not read from ready pipe: %m"); - if (sscanf(xlogend, "%X/%X", &hi, &lo) != 2) + if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2) pg_fatal("could not parse write-ahead log location \"%s\"", xlogend); xlogendptr = ((uint64) hi) << 32 | lo; @@ -622,14 +623,14 @@ StartLogStreamer(char *startpos, uint32 timeline, char *sysidentifier, lo; char statusdir[MAXPGPATH]; - param = pg_malloc0(sizeof(logstreamer_param)); + param = pg_malloc0_object(logstreamer_param); param->timeline = timeline; param->sysidentifier = sysidentifier; param->wal_compress_algorithm = wal_compress_algorithm; param->wal_compress_level = wal_compress_level; /* Convert the starting position */ - if (sscanf(startpos, "%X/%X", &hi, &lo) != 2) + if (sscanf(startpos, "%X/%08X", &hi, &lo) != 2) pg_fatal("could not parse write-ahead log location \"%s\"", startpos); param->startptr = ((uint64) hi) << 32 | lo; @@ -1069,12 +1070,9 @@ CreateBackupStreamer(char *archive_name, char *spclocation, astreamer *manifest_inject_streamer = NULL; bool inject_manifest; bool is_tar, - is_tar_gz, - is_tar_lz4, - is_tar_zstd, is_compressed_tar; + pg_compress_algorithm compressed_tar_algorithm; bool must_parse_archive; - int archive_name_len = strlen(archive_name); /* * Normally, we emit the backup manifest as a separate file, but when @@ -1083,24 +1081,13 @@ CreateBackupStreamer(char *archive_name, char *spclocation, */ inject_manifest = (format == 't' && strcmp(basedir, "-") == 0 && manifest); - /* Is this a tar archive? */ - is_tar = (archive_name_len > 4 && - strcmp(archive_name + archive_name_len - 4, ".tar") == 0); - - /* Is this a .tar.gz archive? */ - is_tar_gz = (archive_name_len > 7 && - strcmp(archive_name + archive_name_len - 7, ".tar.gz") == 0); - - /* Is this a .tar.lz4 archive? */ - is_tar_lz4 = (archive_name_len > 8 && - strcmp(archive_name + archive_name_len - 8, ".tar.lz4") == 0); - - /* Is this a .tar.zst archive? */ - is_tar_zstd = (archive_name_len > 8 && - strcmp(archive_name + archive_name_len - 8, ".tar.zst") == 0); + /* Check whether it is a tar archive and its compression type */ + is_tar = parse_tar_compress_algorithm(archive_name, + &compressed_tar_algorithm); /* Is this any kind of compressed tar? */ - is_compressed_tar = is_tar_gz || is_tar_lz4 || is_tar_zstd; + is_compressed_tar = (is_tar && + compressed_tar_algorithm != PG_COMPRESSION_NONE); /* * Injecting the manifest into a compressed tar file would be possible if @@ -1127,7 +1114,7 @@ CreateBackupStreamer(char *archive_name, char *spclocation, (spclocation == NULL && writerecoveryconf)); /* At present, we only know how to parse tar archives. */ - if (must_parse_archive && !is_tar && !is_compressed_tar) + if (must_parse_archive && !is_tar) { pg_log_error("cannot parse archive \"%s\"", archive_name); pg_log_error_detail("Only tar archives can be parsed."); @@ -1262,13 +1249,13 @@ CreateBackupStreamer(char *archive_name, char *spclocation, * If the user has requested a server compressed archive along with * archive extraction at client then we need to decompress it. */ - if (format == 'p') + if (format == 'p' && is_compressed_tar) { - if (is_tar_gz) + if (compressed_tar_algorithm == PG_COMPRESSION_GZIP) streamer = astreamer_gzip_decompressor_new(streamer); - else if (is_tar_lz4) + else if (compressed_tar_algorithm == PG_COMPRESSION_LZ4) streamer = astreamer_lz4_decompressor_new(streamer); - else if (is_tar_zstd) + else if (compressed_tar_algorithm == PG_COMPRESSION_ZSTD) streamer = astreamer_zstd_decompressor_new(streamer); } @@ -1338,7 +1325,7 @@ ReceiveArchiveStreamChunk(size_t r, char *copybuf, void *callback_data) /* Each CopyData message begins with a type byte. */ switch (GetCopyDataByte(r, copybuf, &cursor)) { - case 'n': + case PqBackupMsg_NewArchive: { /* New archive. */ char *archive_name; @@ -1410,7 +1397,7 @@ ReceiveArchiveStreamChunk(size_t r, char *copybuf, void *callback_data) break; } - case 'd': + case PqMsg_CopyData: { /* Archive or manifest data. */ if (state->manifest_buffer != NULL) @@ -1446,7 +1433,7 @@ ReceiveArchiveStreamChunk(size_t r, char *copybuf, void *callback_data) break; } - case 'p': + case PqBackupMsg_ProgressReport: { /* * Progress report. @@ -1465,7 +1452,7 @@ ReceiveArchiveStreamChunk(size_t r, char *copybuf, void *callback_data) break; } - case 'm': + case PqBackupMsg_Manifest: { /* * Manifest data will be sent next. This message is not @@ -2255,7 +2242,7 @@ BaseBackup(char *compression_algorithm, char *compression_detail, * value directly in the variable, and then set the flag that says * it's there. */ - if (sscanf(xlogend, "%X/%X", &hi, &lo) != 2) + if (sscanf(xlogend, "%X/%08X", &hi, &lo) != 2) pg_fatal("could not parse write-ahead log location \"%s\"", xlogend); xlogendptr = ((uint64) hi) << 32 | lo; diff --git a/src/bin/pg_basebackup/pg_createsubscriber.c b/src/bin/pg_basebackup/pg_createsubscriber.c index f65acc7cb1141..15e06e5686e27 100644 --- a/src/bin/pg_basebackup/pg_createsubscriber.c +++ b/src/bin/pg_basebackup/pg_createsubscriber.c @@ -3,7 +3,7 @@ * pg_createsubscriber.c * Create a new logical replica from a standby server * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/pg_createsubscriber.c @@ -20,21 +20,44 @@ #include "common/connect.h" #include "common/controldata_utils.h" +#include "common/file_perm.h" +#include "common/file_utils.h" #include "common/logging.h" #include "common/pg_prng.h" #include "common/restricted_token.h" +#include "datatype/timestamp.h" #include "fe_utils/recovery_gen.h" #include "fe_utils/simple_list.h" #include "fe_utils/string_utils.h" +#include "fe_utils/version.h" #include "getopt_long.h" #define DEFAULT_SUB_PORT "50432" #define OBJECTTYPE_PUBLICATIONS 0x0001 +/* + * Configuration files for recovery parameters. + * + * The recovery parameters are set in INCLUDED_CONF_FILE, itself loaded by + * the server through an include_if_exists in postgresql.auto.conf. + * + * INCLUDED_CONF_FILE is renamed to INCLUDED_CONF_FILE_DISABLED when exiting, + * so as the recovery parameters set by this tool never take effect on node + * restart. The contents of INCLUDED_CONF_FILE_DISABLED can be useful for + * debugging. + */ +#define PG_AUTOCONF_FILENAME "postgresql.auto.conf" +#define INCLUDED_CONF_FILE "pg_createsubscriber.conf" +#define INCLUDED_CONF_FILE_DISABLED INCLUDED_CONF_FILE ".disabled" + +#define SERVER_LOG_FILE_NAME "pg_createsubscriber_server.log" +#define INTERNAL_LOG_FILE_NAME "pg_createsubscriber_internal.log" + /* Command-line options */ struct CreateSubscriberOptions { char *config_file; /* configuration file */ + char *log_dir; /* log directory name */ char *pub_conninfo_str; /* publisher connection string */ char *socket_dir; /* directory for Unix-domain socket, if any */ char *sub_port; /* subscriber port number */ @@ -46,7 +69,7 @@ struct CreateSubscriberOptions SimpleStringList replslot_names; /* list of replication slot names */ int recovery_timeout; /* stop recovery after this time */ bool all_dbs; /* all option */ - SimpleStringList objecttypes_to_remove; /* list of object types to remove */ + SimpleStringList objecttypes_to_clean; /* list of object types to cleanup */ }; /* per-database publication/subscription info */ @@ -71,12 +94,12 @@ struct LogicalRepInfos { struct LogicalRepInfo *dbinfo; bool two_phase; /* enable-two-phase option */ - bits32 objecttypes_to_remove; /* flags indicating which object types - * to remove on subscriber */ + uint32 objecttypes_to_clean; /* flags indicating which object types + * to clean up on subscriber */ }; static void cleanup_objects_atexit(void); -static void usage(); +static void usage(void); static char *get_base_conninfo(const char *conninfo, char **dbname); static char *get_sub_conninfo(const struct CreateSubscriberOptions *opt); static char *get_exec_path(const char *argv0, const char *progname); @@ -114,6 +137,7 @@ static void stop_standby_server(const char *datadir); static void wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt); static void create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo); +static bool find_publication(PGconn *conn, const char *pubname, const char *dbname); static void drop_publication(PGconn *conn, const char *pubname, const char *dbname, bool *made_publication); static void check_and_drop_publications(PGconn *conn, struct LogicalRepInfo *dbinfo); @@ -123,12 +147,11 @@ static void set_replication_progress(PGconn *conn, const struct LogicalRepInfo * static void enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo); static void check_and_drop_existing_subscriptions(PGconn *conn, const struct LogicalRepInfo *dbinfo); -static void drop_existing_subscriptions(PGconn *conn, const char *subname, - const char *dbname); +static void drop_existing_subscription(PGconn *conn, const char *subname, + const char *dbname); static void get_publisher_databases(struct CreateSubscriberOptions *opt, bool dbnamespecified); -#define USEC_PER_SEC 1000000 #define WAIT_INTERVAL 1 /* 1 second */ static const char *progname; @@ -149,33 +172,52 @@ static pg_prng_state prng_state; static char *pg_ctl_path = NULL; static char *pg_resetwal_path = NULL; +static char *logdir = NULL; /* Subdirectory of the user specified logdir + * where the log files are written (if + * specified) */ + /* standby / subscriber data directory */ static char *subscriber_dir = NULL; static bool recovery_ended = false; static bool standby_running = false; - -enum WaitPMResult -{ - POSTMASTER_READY, - POSTMASTER_STILL_STARTING -}; +static bool recovery_params_set = false; /* - * Cleanup objects that were created by pg_createsubscriber if there is an - * error. + * Clean up objects created by pg_createsubscriber. * - * Publications and replication slots are created on primary. Depending on the - * step it failed, it should remove the already created objects if it is - * possible (sometimes it won't work due to a connection issue). - * There is no cleanup on the target server. The steps on the target server are - * executed *after* promotion, hence, at this point, a failure means recreate - * the physical replica and start again. + * Publications and replication slots are created on the primary. Depending + * on the step where it failed, already-created objects should be removed if + * possible (sometimes this won't work due to a connection issue). + * There is no cleanup on the target server *after* its promotion, because any + * failure at this point means recreating the physical replica and starting + * again. + * + * The recovery configuration is always removed, by renaming the included + * configuration file out of the way. */ static void cleanup_objects_atexit(void) { + /* Rename the included configuration file, if necessary. */ + if (recovery_params_set) + { + char conf_filename[MAXPGPATH]; + char conf_filename_disabled[MAXPGPATH]; + + snprintf(conf_filename, MAXPGPATH, "%s/%s", subscriber_dir, + INCLUDED_CONF_FILE); + snprintf(conf_filename_disabled, MAXPGPATH, "%s/%s", subscriber_dir, + INCLUDED_CONF_FILE_DISABLED); + + if (durable_rename(conf_filename, conf_filename_disabled) != 0) + { + /* durable_rename() has already logged something. */ + pg_log_warning_hint("A manual removal of the recovery parameters may be required."); + } + } + if (success) return; @@ -247,19 +289,20 @@ usage(void) printf(_(" %s [OPTION]...\n"), progname); printf(_("\nOptions:\n")); printf(_(" -a, --all create subscriptions for all databases except template\n" - " databases or databases that don't allow connections\n")); + " databases and databases that don't allow connections\n")); printf(_(" -d, --database=DBNAME database in which to create a subscription\n")); printf(_(" -D, --pgdata=DATADIR location for the subscriber data directory\n")); + printf(_(" -l, --logdir=LOGDIR location for the log directory\n")); printf(_(" -n, --dry-run dry run, just show what would be done\n")); printf(_(" -p, --subscriber-port=PORT subscriber port number (default %s)\n"), DEFAULT_SUB_PORT); printf(_(" -P, --publisher-server=CONNSTR publisher connection string\n")); - printf(_(" -R, --remove=OBJECTTYPE remove all objects of the specified type from specified\n" - " databases on the subscriber; accepts: publications\n")); printf(_(" -s, --socketdir=DIR socket directory to use (default current dir.)\n")); printf(_(" -t, --recovery-timeout=SECS seconds to wait for recovery to end\n")); printf(_(" -T, --enable-two-phase enable two-phase commit for all subscriptions\n")); printf(_(" -U, --subscriber-username=NAME user name for subscriber connection\n")); printf(_(" -v, --verbose output verbose messages\n")); + printf(_(" --clean=OBJECTTYPE drop all objects of the specified type from specified\n" + " databases on the subscriber; accepts: \"%s\"\n"), "publications"); printf(_(" --config-file=FILENAME use specified main server configuration\n" " file when running target cluster\n")); printf(_(" --publication=NAME publication name\n")); @@ -407,7 +450,8 @@ static void check_data_directory(const char *datadir) { struct stat statbuf; - char versionfile[MAXPGPATH]; + uint32 major_version; + char *version_str; pg_log_info("checking if directory \"%s\" is a cluster data directory", datadir); @@ -420,11 +464,18 @@ check_data_directory(const char *datadir) pg_fatal("could not access directory \"%s\": %m", datadir); } - snprintf(versionfile, MAXPGPATH, "%s/PG_VERSION", datadir); - if (stat(versionfile, &statbuf) != 0 && errno == ENOENT) + /* + * Retrieve the contents of this cluster's PG_VERSION. We require + * compatibility with the same major version as the one this tool is + * compiled with. + */ + major_version = GET_PG_MAJORVERSION_NUM(get_pg_version(datadir, &version_str)); + if (major_version != PG_MAJORVERSION_NUM) { - pg_fatal("directory \"%s\" is not a database cluster directory", - datadir); + pg_log_error("data directory is of wrong version"); + pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".", + "PG_VERSION", version_str, PG_MAJORVERSION); + exit(1); } } @@ -661,6 +712,7 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt) bool crc_ok; struct timeval tv; + char *out_file; char *cmd_str; pg_log_info("modifying system identifier of subscriber"); @@ -679,16 +731,35 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt) cf->system_identifier |= ((uint64) tv.tv_usec) << 12; cf->system_identifier |= getpid() & 0xFFF; - if (!dry_run) + if (dry_run) + pg_log_info("dry-run: would set system identifier to %" PRIu64 " on subscriber", + cf->system_identifier); + else + { update_controlfile(subscriber_dir, cf, true); + pg_log_info("system identifier is %" PRIu64 " on subscriber", + cf->system_identifier); + } - pg_log_info("system identifier is %" PRIu64 " on subscriber", - cf->system_identifier); + if (dry_run) + pg_log_info("dry-run: would run pg_resetwal on the subscriber"); + else + pg_log_info("running pg_resetwal on the subscriber"); - pg_log_info("running pg_resetwal on the subscriber"); + /* + * Redirecting the output to the logfile if specified. Since the output + * would be very short, around one line, we do not provide a separate file + * for it; it's done as a part of the server log. + */ + if (opt->log_dir) + out_file = psprintf("%s/%s", logdir, SERVER_LOG_FILE_NAME); + else + out_file = DEVNULL; - cmd_str = psprintf("\"%s\" -D \"%s\" > \"%s\"", pg_resetwal_path, - subscriber_dir, DEVNULL); + cmd_str = psprintf("\"%s\" -D \"%s\" >> \"%s\"", pg_resetwal_path, + subscriber_dir, out_file); + if (opt->log_dir) + pg_free(out_file); pg_log_debug("pg_resetwal command is: %s", cmd_str); @@ -697,12 +768,13 @@ modify_subscriber_sysid(const struct CreateSubscriberOptions *opt) int rc = system(cmd_str); if (rc == 0) - pg_log_info("subscriber successfully changed the system identifier"); + pg_log_info("successfully reset WAL on the subscriber"); else - pg_fatal("could not change system identifier of subscriber: %s", wait_result_to_str(rc)); + pg_fatal("could not reset WAL on subscriber: %s", wait_result_to_str(rc)); } pg_free(cf); + pg_free(cmd_str); } /* @@ -753,6 +825,39 @@ generate_object_name(PGconn *conn) return objname; } +/* + * Does the publication exist in the specified database? + */ +static bool +find_publication(PGconn *conn, const char *pubname, const char *dbname) +{ + PQExpBuffer str = createPQExpBuffer(); + PGresult *res; + bool found = false; + char *pubname_esc = PQescapeLiteral(conn, pubname, strlen(pubname)); + + appendPQExpBuffer(str, + "SELECT 1 FROM pg_catalog.pg_publication " + "WHERE pubname = %s", + pubname_esc); + res = PQexec(conn, str->data); + if (PQresultStatus(res) != PGRES_TUPLES_OK) + { + pg_log_error("could not find publication \"%s\" in database \"%s\": %s", + pubname, dbname, PQerrorMessage(conn)); + disconnect_database(conn, true); + } + + if (PQntuples(res) == 1) + found = true; + + PQclear(res); + PQfreemem(pubname_esc); + destroyPQExpBuffer(str); + + return found; +} + /* * Create the publications and replication slots in preparation for logical * replication. Returns the LSN from latest replication slot. It will be the @@ -789,22 +894,31 @@ setup_publisher(struct LogicalRepInfo *dbinfo) if (num_replslots == 0) dbinfo[i].replslotname = pg_strdup(dbinfo[i].subname); - /* - * Create publication on publisher. This step should be executed - * *before* promoting the subscriber to avoid any transactions between - * consistent LSN and the new publication rows (such transactions - * wouldn't see the new publication rows resulting in an error). - */ - create_publication(conn, &dbinfo[i]); + if (find_publication(conn, dbinfo[i].pubname, dbinfo[i].dbname)) + { + /* Reuse existing publication on publisher. */ + pg_log_info("use existing publication \"%s\" in database \"%s\"", + dbinfo[i].pubname, dbinfo[i].dbname); + /* Don't remove pre-existing publication if an error occurs. */ + dbinfo[i].made_publication = false; + } + else + { + /* + * Create publication on publisher. This step should be executed + * *before* promoting the subscriber to avoid any transactions + * between consistent LSN and the new publication rows (such + * transactions wouldn't see the new publication rows resulting in + * an error). + */ + create_publication(conn, &dbinfo[i]); + } /* Create replication slot on publisher */ if (lsn) pg_free(lsn); lsn = create_logical_replication_slot(conn, &dbinfo[i]); - if (lsn != NULL || dry_run) - pg_log_info("create replication slot \"%s\" on publisher", - dbinfo[i].replslotname); - else + if (lsn == NULL && !dry_run) exit(1); /* @@ -861,6 +975,38 @@ server_is_in_recovery(PGconn *conn) return ret == 0; } +static void +make_output_dirs(const char *log_basedir) +{ + char timestamp[128]; + struct timeval tval; + time_t now; + struct tm tmbuf; + + /* Generate timestamp */ + gettimeofday(&tval, NULL); + now = tval.tv_sec; + + strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%S", + localtime_r(&now, &tmbuf)); + + /* Append milliseconds */ + snprintf(timestamp + strlen(timestamp), + sizeof(timestamp) - strlen(timestamp), ".%03u", + (unsigned int) (tval.tv_usec / 1000)); + + /* Build timestamp directory path */ + logdir = psprintf("%s/%s", log_basedir, timestamp); + + /* Create base directory (ignore if exists) */ + if (mkdir(log_basedir, pg_dir_create_mode) < 0 && errno != EEXIST) + pg_fatal("could not create directory \"%s\": %m", log_basedir); + + /* Create a timestamp-named subdirectory under the base directory */ + if (mkdir(logdir, pg_dir_create_mode) < 0) + pg_fatal("could not create directory \"%s\": %m", logdir); +} + /* * Is the primary server ready for logical replication? * @@ -900,7 +1046,7 @@ check_publisher(const struct LogicalRepInfo *dbinfo) * Since these parameters are not a requirement for physical replication, * we should check it to make sure it won't fail. * - * - wal_level = logical + * - wal_level >= replica * - max_replication_slots >= current + number of dbs to be converted * - max_wal_senders >= current + number of dbs to be converted * - max_slot_wal_keep_size = -1 (to prevent deletion of required WAL files) @@ -944,9 +1090,9 @@ check_publisher(const struct LogicalRepInfo *dbinfo) disconnect_database(conn, false); - if (strcmp(wal_level, "logical") != 0) + if (strcmp(wal_level, "minimal") == 0) { - pg_log_error("publisher requires \"wal_level\" >= \"logical\""); + pg_log_error("publisher requires \"wal_level\" >= \"replica\""); failed = true; } @@ -973,13 +1119,13 @@ check_publisher(const struct LogicalRepInfo *dbinfo) pg_log_warning("two_phase option will not be enabled for replication slots"); pg_log_warning_detail("Subscriptions will be created with the two_phase option disabled. " "Prepared transactions will be replicated at COMMIT PREPARED."); - pg_log_warning_hint("You can use --enable-two-phase switch to enable two_phase."); + pg_log_warning_hint("You can use the command-line option --enable-two-phase to enable two_phase."); } /* - * Validate 'max_slot_wal_keep_size'. If this parameter is set to a - * non-default value, it may cause replication failures due to required - * WAL files being prematurely removed. + * In dry-run mode, validate 'max_slot_wal_keep_size'. If this parameter + * is set to a non-default value, it may cause replication failures due to + * required WAL files being prematurely removed. */ if (dry_run && (strcmp(max_slot_wal_keep_size, "-1") != 0)) { @@ -1013,7 +1159,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo) bool failed = false; int max_lrworkers; - int max_reporigins; + int max_replorigins; int max_wprocs; pg_log_info("checking settings on subscriber"); @@ -1052,7 +1198,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo) disconnect_database(conn, true); } - max_reporigins = atoi(PQgetvalue(res, 0, 0)); + max_replorigins = atoi(PQgetvalue(res, 0, 0)); max_lrworkers = atoi(PQgetvalue(res, 1, 0)); max_wprocs = atoi(PQgetvalue(res, 2, 0)); if (strcmp(PQgetvalue(res, 3, 0), "") != 0) @@ -1060,7 +1206,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo) pg_log_debug("subscriber: max_logical_replication_workers: %d", max_lrworkers); - pg_log_debug("subscriber: max_active_replication_origins: %d", max_reporigins); + pg_log_debug("subscriber: max_active_replication_origins: %d", max_replorigins); pg_log_debug("subscriber: max_worker_processes: %d", max_wprocs); if (primary_slot_name) pg_log_debug("subscriber: primary_slot_name: %s", primary_slot_name); @@ -1069,10 +1215,10 @@ check_subscriber(const struct LogicalRepInfo *dbinfo) disconnect_database(conn, false); - if (max_reporigins < num_dbs) + if (max_replorigins < num_dbs) { pg_log_error("subscriber requires %d active replication origins, but only %d remain", - num_dbs, max_reporigins); + num_dbs, max_replorigins); pg_log_error_hint("Increase the configuration parameter \"%s\" to at least %d.", "max_active_replication_origins", num_dbs); failed = true; @@ -1107,7 +1253,7 @@ check_subscriber(const struct LogicalRepInfo *dbinfo) * node. */ static void -drop_existing_subscriptions(PGconn *conn, const char *subname, const char *dbname) +drop_existing_subscription(PGconn *conn, const char *subname, const char *dbname) { PQExpBuffer query = createPQExpBuffer(); PGresult *res; @@ -1124,11 +1270,14 @@ drop_existing_subscriptions(PGconn *conn, const char *subname, const char *dbnam subname); appendPQExpBuffer(query, " DROP SUBSCRIPTION %s;", subname); - pg_log_info("dropping subscription \"%s\" in database \"%s\"", - subname, dbname); - - if (!dry_run) + if (dry_run) + pg_log_info("dry-run: would drop subscription \"%s\" in database \"%s\"", + subname, dbname); + else { + pg_log_info("dropping subscription \"%s\" in database \"%s\"", + subname, dbname); + res = PQexec(conn, query->data); if (PQresultStatus(res) != PGRES_COMMAND_OK) @@ -1174,8 +1323,8 @@ check_and_drop_existing_subscriptions(PGconn *conn, } for (int i = 0; i < PQntuples(res); i++) - drop_existing_subscriptions(conn, PQgetvalue(res, i, 0), - dbinfo->dbname); + drop_existing_subscription(conn, PQgetvalue(res, i, 0), + dbinfo->dbname); PQclear(res); destroyPQExpBuffer(query); @@ -1250,8 +1399,17 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c appendPQExpBufferStr(recoveryconfcontents, "recovery_target = ''\n"); appendPQExpBufferStr(recoveryconfcontents, "recovery_target_timeline = 'latest'\n"); + + /* + * Set recovery_target_inclusive = false to avoid reapplying the + * transaction committed at 'lsn' after subscription is enabled. This is + * because the provided 'lsn' is also used as the replication start point + * for the subscription. So, the server can send the transaction committed + * at that 'lsn' after replication is started which can lead to applying + * the same transaction twice if we keep recovery_target_inclusive = true. + */ appendPQExpBufferStr(recoveryconfcontents, - "recovery_target_inclusive = true\n"); + "recovery_target_inclusive = false\n"); appendPQExpBufferStr(recoveryconfcontents, "recovery_target_action = promote\n"); appendPQExpBufferStr(recoveryconfcontents, "recovery_target_name = ''\n"); @@ -1260,20 +1418,45 @@ setup_recovery(const struct LogicalRepInfo *dbinfo, const char *datadir, const c if (dry_run) { - appendPQExpBufferStr(recoveryconfcontents, "# dry run mode"); + appendPQExpBufferStr(recoveryconfcontents, "# dry run mode\n"); appendPQExpBuffer(recoveryconfcontents, - "recovery_target_lsn = '%X/%X'\n", + "recovery_target_lsn = '%X/%08X'\n", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr)); } else { appendPQExpBuffer(recoveryconfcontents, "recovery_target_lsn = '%s'\n", lsn); - WriteRecoveryConfig(conn, datadir, recoveryconfcontents); } - disconnect_database(conn, false); pg_log_debug("recovery parameters:\n%s", recoveryconfcontents->data); + + if (!dry_run) + { + char conf_filename[MAXPGPATH]; + FILE *fd; + + /* Write the recovery parameters to INCLUDED_CONF_FILE */ + snprintf(conf_filename, MAXPGPATH, "%s/%s", datadir, + INCLUDED_CONF_FILE); + fd = fopen(conf_filename, "w"); + if (fd == NULL) + pg_fatal("could not open file \"%s\": %m", conf_filename); + + if (fwrite(recoveryconfcontents->data, recoveryconfcontents->len, 1, fd) != 1) + pg_fatal("could not write to file \"%s\": %m", conf_filename); + + fclose(fd); + recovery_params_set = true; + + /* Include conditionally the recovery parameters. */ + resetPQExpBuffer(recoveryconfcontents); + appendPQExpBufferStr(recoveryconfcontents, + "include_if_exists '" INCLUDED_CONF_FILE "'\n"); + WriteRecoveryConfig(conn, datadir, recoveryconfcontents); + } + + disconnect_database(conn, false); } /* @@ -1366,8 +1549,12 @@ create_logical_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo) Assert(conn != NULL); - pg_log_info("creating the replication slot \"%s\" in database \"%s\"", - slot_name, dbinfo->dbname); + if (dry_run) + pg_log_info("dry-run: would create the replication slot \"%s\" in database \"%s\" on publisher", + slot_name, dbinfo->dbname); + else + pg_log_info("creating the replication slot \"%s\" in database \"%s\" on publisher", + slot_name, dbinfo->dbname); slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name)); @@ -1415,8 +1602,12 @@ drop_replication_slot(PGconn *conn, struct LogicalRepInfo *dbinfo, Assert(conn != NULL); - pg_log_info("dropping the replication slot \"%s\" in database \"%s\"", - slot_name, dbinfo->dbname); + if (dry_run) + pg_log_info("dry-run: would drop the replication slot \"%s\" in database \"%s\"", + slot_name, dbinfo->dbname); + else + pg_log_info("dropping the replication slot \"%s\" in database \"%s\"", + slot_name, dbinfo->dbname); slot_name_esc = PQescapeLiteral(conn, slot_name, strlen(slot_name)); @@ -1515,6 +1706,9 @@ start_standby_server(const struct CreateSubscriberOptions *opt, bool restricted_ if (restrict_logical_worker) appendPQExpBufferStr(pg_ctl_cmd, " -o \"-c max_logical_replication_workers=0\""); + if (opt->log_dir) + appendPQExpBuffer(pg_ctl_cmd, " -l \"%s/%s\"", logdir, SERVER_LOG_FILE_NAME); + pg_log_debug("pg_ctl command is: %s", pg_ctl_cmd->data); rc = system(pg_ctl_cmd->data); pg_ctl_status(pg_ctl_cmd->data, rc); @@ -1551,7 +1745,7 @@ static void wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions *opt) { PGconn *conn; - int status = POSTMASTER_STILL_STARTING; + bool ready = false; int timer = 0; pg_log_info("waiting for the target server to reach the consistent state"); @@ -1560,15 +1754,10 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions for (;;) { - bool in_recovery = server_is_in_recovery(conn); - - /* - * Does the recovery process finish? In dry run mode, there is no - * recovery mode. Bail out as the recovery process has ended. - */ - if (!in_recovery || dry_run) + /* Did the recovery process finish? We're done if so. */ + if (dry_run || !server_is_in_recovery(conn)) { - status = POSTMASTER_READY; + ready = true; recovery_ended = true; break; } @@ -1582,14 +1771,13 @@ wait_for_end_recovery(const char *conninfo, const struct CreateSubscriberOptions } /* Keep waiting */ - pg_usleep(WAIT_INTERVAL * USEC_PER_SEC); - + pg_usleep(WAIT_INTERVAL * USECS_PER_SEC); timer += WAIT_INTERVAL; } disconnect_database(conn, false); - if (status == POSTMASTER_STILL_STARTING) + if (!ready) pg_fatal("server did not end recovery"); pg_log_info("target server reached the consistent state"); @@ -1642,8 +1830,12 @@ create_publication(PGconn *conn, struct LogicalRepInfo *dbinfo) PQclear(res); resetPQExpBuffer(str); - pg_log_info("creating publication \"%s\" in database \"%s\"", - dbinfo->pubname, dbinfo->dbname); + if (dry_run) + pg_log_info("dry-run: would create publication \"%s\" in database \"%s\"", + dbinfo->pubname, dbinfo->dbname); + else + pg_log_info("creating publication \"%s\" in database \"%s\"", + dbinfo->pubname, dbinfo->dbname); appendPQExpBuffer(str, "CREATE PUBLICATION %s FOR ALL TABLES", ipubname_esc); @@ -1685,8 +1877,12 @@ drop_publication(PGconn *conn, const char *pubname, const char *dbname, pubname_esc = PQescapeIdentifier(conn, pubname, strlen(pubname)); - pg_log_info("dropping publication \"%s\" in database \"%s\"", - pubname, dbname); + if (dry_run) + pg_log_info("dry-run: would drop publication \"%s\" in database \"%s\"", + pubname, dbname); + else + pg_log_info("dropping publication \"%s\" in database \"%s\"", + pubname, dbname); appendPQExpBuffer(str, "DROP PUBLICATION %s", pubname_esc); @@ -1720,17 +1916,16 @@ drop_publication(PGconn *conn, const char *pubname, const char *dbname, /* * Retrieve and drop the publications. * - * Since the publications were created before the consistent LSN, they - * remain on the subscriber even after the physical replica is - * promoted. Remove these publications from the subscriber because - * they have no use. Additionally, if requested, drop all pre-existing - * publications. + * Publications copied during physical replication remain on the subscriber + * after promotion. If --clean=publications is specified, drop all existing + * publications in the subscriber database. Otherwise, only drop publications + * that were created by pg_createsubscriber during this operation. */ static void check_and_drop_publications(PGconn *conn, struct LogicalRepInfo *dbinfo) { PGresult *res; - bool drop_all_pubs = dbinfos.objecttypes_to_remove & OBJECTTYPE_PUBLICATIONS; + bool drop_all_pubs = dbinfos.objecttypes_to_clean & OBJECTTYPE_PUBLICATIONS; Assert(conn != NULL); @@ -1756,14 +1951,24 @@ check_and_drop_publications(PGconn *conn, struct LogicalRepInfo *dbinfo) PQclear(res); } - - /* - * In dry-run mode, we don't create publications, but we still try to drop - * those to provide necessary information to the user. - */ - if (!drop_all_pubs || dry_run) - drop_publication(conn, dbinfo->pubname, dbinfo->dbname, - &dbinfo->made_publication); + else + { + /* Drop publication only if it was created by this tool */ + if (dbinfo->made_publication) + { + drop_publication(conn, dbinfo->pubname, dbinfo->dbname, + &dbinfo->made_publication); + } + else + { + if (dry_run) + pg_log_info("dry-run: would preserve existing publication \"%s\" in database \"%s\"", + dbinfo->pubname, dbinfo->dbname); + else + pg_log_info("preserve existing publication \"%s\" in database \"%s\"", + dbinfo->pubname, dbinfo->dbname); + } + } } /* @@ -1794,8 +1999,12 @@ create_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo) pubconninfo_esc = PQescapeLiteral(conn, dbinfo->pubconninfo, strlen(dbinfo->pubconninfo)); replslotname_esc = PQescapeLiteral(conn, dbinfo->replslotname, strlen(dbinfo->replslotname)); - pg_log_info("creating subscription \"%s\" in database \"%s\"", - dbinfo->subname, dbinfo->dbname); + if (dry_run) + pg_log_info("dry-run: would create subscription \"%s\" in database \"%s\"", + dbinfo->subname, dbinfo->dbname); + else + pg_log_info("creating subscription \"%s\" in database \"%s\"", + dbinfo->subname, dbinfo->dbname); appendPQExpBuffer(str, "CREATE SUBSCRIPTION %s CONNECTION %s PUBLICATION %s " @@ -1876,7 +2085,7 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons if (dry_run) { suboid = InvalidOid; - lsnstr = psprintf("%X/%X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr)); + lsnstr = psprintf("%X/%08X", LSN_FORMAT_ARGS((XLogRecPtr) InvalidXLogRecPtr)); } else { @@ -1892,8 +2101,12 @@ set_replication_progress(PGconn *conn, const struct LogicalRepInfo *dbinfo, cons */ originname = psprintf("pg_%u", suboid); - pg_log_info("setting the replication progress (node name \"%s\", LSN %s) in database \"%s\"", - originname, lsnstr, dbinfo->dbname); + if (dry_run) + pg_log_info("dry-run: would set the replication progress (node name \"%s\", LSN %s) in database \"%s\"", + originname, lsnstr, dbinfo->dbname); + else + pg_log_info("setting the replication progress (node name \"%s\", LSN %s) in database \"%s\"", + originname, lsnstr, dbinfo->dbname); resetPQExpBuffer(str); appendPQExpBuffer(str, @@ -1938,8 +2151,12 @@ enable_subscription(PGconn *conn, const struct LogicalRepInfo *dbinfo) subname = PQescapeIdentifier(conn, dbinfo->subname, strlen(dbinfo->subname)); - pg_log_info("enabling subscription \"%s\" in database \"%s\"", - dbinfo->subname, dbinfo->dbname); + if (dry_run) + pg_log_info("dry-run: would enable subscription \"%s\" in database \"%s\"", + dbinfo->subname, dbinfo->dbname); + else + pg_log_info("enabling subscription \"%s\" in database \"%s\"", + dbinfo->subname, dbinfo->dbname); appendPQExpBuffer(str, "ALTER SUBSCRIPTION %s ENABLE", subname); @@ -2023,10 +2240,10 @@ main(int argc, char **argv) {"all", no_argument, NULL, 'a'}, {"database", required_argument, NULL, 'd'}, {"pgdata", required_argument, NULL, 'D'}, + {"logdir", required_argument, NULL, 'l'}, {"dry-run", no_argument, NULL, 'n'}, {"subscriber-port", required_argument, NULL, 'p'}, {"publisher-server", required_argument, NULL, 'P'}, - {"remove", required_argument, NULL, 'R'}, {"socketdir", required_argument, NULL, 's'}, {"recovery-timeout", required_argument, NULL, 't'}, {"enable-two-phase", no_argument, NULL, 'T'}, @@ -2038,6 +2255,7 @@ main(int argc, char **argv) {"publication", required_argument, NULL, 2}, {"replication-slot", required_argument, NULL, 3}, {"subscription", required_argument, NULL, 4}, + {"clean", required_argument, NULL, 5}, {NULL, 0, NULL, 0} }; @@ -2081,6 +2299,7 @@ main(int argc, char **argv) /* Default settings */ subscriber_dir = NULL; opt.config_file = NULL; + opt.log_dir = NULL; opt.pub_conninfo_str = NULL; opt.socket_dir = NULL; opt.sub_port = DEFAULT_SUB_PORT; @@ -2109,7 +2328,7 @@ main(int argc, char **argv) get_restricted_token(); - while ((c = getopt_long(argc, argv, "ad:D:np:P:R:s:t:TU:v", + while ((c = getopt_long(argc, argv, "ad:D:l:np:P:s:t:TU:v", long_options, &option_index)) != -1) { switch (c) @@ -2130,6 +2349,10 @@ main(int argc, char **argv) subscriber_dir = pg_strdup(optarg); canonicalize_path(subscriber_dir); break; + case 'l': + opt.log_dir = pg_strdup(optarg); + canonicalize_path(opt.log_dir); + break; case 'n': dry_run = true; break; @@ -2139,12 +2362,6 @@ main(int argc, char **argv) case 'P': opt.pub_conninfo_str = pg_strdup(optarg); break; - case 'R': - if (!simple_string_list_member(&opt.objecttypes_to_remove, optarg)) - simple_string_list_append(&opt.objecttypes_to_remove, optarg); - else - pg_fatal("object type \"%s\" is specified more than once for -R/--remove", optarg); - break; case 's': opt.socket_dir = pg_strdup(optarg); canonicalize_path(opt.socket_dir); @@ -2191,6 +2408,12 @@ main(int argc, char **argv) else pg_fatal("subscription \"%s\" specified more than once for --subscription", optarg); break; + case 5: + if (!simple_string_list_member(&opt.objecttypes_to_clean, optarg)) + simple_string_list_append(&opt.objecttypes_to_clean, optarg); + else + pg_fatal("object type \"%s\" specified more than once for --clean", optarg); + break; default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -2214,7 +2437,8 @@ main(int argc, char **argv) if (bad_switch) { - pg_log_error("%s cannot be used with -a/--all", bad_switch); + pg_log_error("options %s and %s cannot be used together", + bad_switch, "-a/--all"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } @@ -2264,6 +2488,41 @@ main(int argc, char **argv) pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } + + if (opt.log_dir != NULL) + { + char *internal_log_file; + FILE *internal_log_file_fp; + + umask(PG_MODE_MASK_OWNER); + + /* + * Set mask based on PGDATA permissions, needed for the creation of + * the output directories with correct permissions, similar with + * pg_ctl and pg_upgrade. + * + * Don't error here if the data directory cannot be stat'd. Upcoming + * checks for the data directory would raise the fatal error later. + */ + if (GetDataDirectoryCreatePerm(subscriber_dir)) + umask(pg_mode_mask); + + make_output_dirs(opt.log_dir); + internal_log_file = psprintf("%s/%s", logdir, INTERNAL_LOG_FILE_NAME); + + internal_log_file_fp = fopen(internal_log_file, "a"); + if (!internal_log_file_fp) + pg_fatal("could not open log file \"%s\": %m", internal_log_file); + + pg_free(internal_log_file); + + pg_logging_set_logfile(internal_log_file_fp); + } + + if (dry_run) + pg_log_info("Executing in dry-run mode.\n" + "The target directory will not be modified."); + pg_log_info("validating publisher connection string"); pub_base_conninfo = get_base_conninfo(opt.pub_conninfo_str, &dbname_conninfo); @@ -2334,14 +2593,15 @@ main(int argc, char **argv) } /* Verify the object types specified for removal from the subscriber */ - for (SimpleStringListCell *cell = opt.objecttypes_to_remove.head; cell; cell = cell->next) + for (SimpleStringListCell *cell = opt.objecttypes_to_clean.head; cell; cell = cell->next) { if (pg_strcasecmp(cell->val, "publications") == 0) - dbinfos.objecttypes_to_remove |= OBJECTTYPE_PUBLICATIONS; + dbinfos.objecttypes_to_clean |= OBJECTTYPE_PUBLICATIONS; else { - pg_log_error("invalid object type \"%s\" specified for -R/--remove", cell->val); - pg_log_error_hint("The valid option is: \"publications\""); + pg_log_error("invalid object type \"%s\" specified for %s", + cell->val, "--clean"); + pg_log_error_hint("The valid value is: \"%s\"", "publications"); exit(1); } } diff --git a/src/bin/pg_basebackup/pg_receivewal.c b/src/bin/pg_basebackup/pg_receivewal.c index e816cf58101fb..ddfec298fb7a1 100644 --- a/src/bin/pg_basebackup/pg_receivewal.c +++ b/src/bin/pg_basebackup/pg_receivewal.c @@ -5,7 +5,7 @@ * * Author: Magnus Hagander * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/pg_receivewal.c @@ -188,14 +188,14 @@ stop_streaming(XLogRecPtr xlogpos, uint32 timeline, bool segment_finished) /* we assume that we get called once at the end of each segment */ if (verbose && segment_finished) - pg_log_info("finished segment at %X/%X (timeline %u)", + pg_log_info("finished segment at %X/%08X (timeline %u)", LSN_FORMAT_ARGS(xlogpos), timeline); - if (!XLogRecPtrIsInvalid(endpos) && endpos < xlogpos) + if (XLogRecPtrIsValid(endpos) && endpos < xlogpos) { if (verbose) - pg_log_info("stopped log streaming at %X/%X (timeline %u)", + pg_log_info("stopped log streaming at %X/%08X (timeline %u)", LSN_FORMAT_ARGS(xlogpos), timeline); time_to_stop = true; @@ -211,7 +211,7 @@ stop_streaming(XLogRecPtr xlogpos, uint32 timeline, bool segment_finished) * timeline, but it's close enough for reporting purposes. */ if (verbose && prevtimeline != 0 && prevtimeline != timeline) - pg_log_info("switched to timeline %u at %X/%X", + pg_log_info("switched to timeline %u at %X/%08X", timeline, LSN_FORMAT_ARGS(prevpos)); @@ -535,7 +535,7 @@ StreamLog(void) * Figure out where to start streaming. First scan the local directory. */ stream.startpos = FindStreamingStart(&stream.timeline); - if (stream.startpos == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(stream.startpos)) { /* * Try to get the starting point from the slot if any. This is @@ -556,14 +556,14 @@ StreamLog(void) * If it the starting point is still not known, use the current WAL * flush value as last resort. */ - if (stream.startpos == InvalidXLogRecPtr) + if (!XLogRecPtrIsValid(stream.startpos)) { stream.startpos = serverpos; stream.timeline = servertli; } } - Assert(stream.startpos != InvalidXLogRecPtr && + Assert(XLogRecPtrIsValid(stream.startpos) && stream.timeline != 0); /* @@ -575,7 +575,7 @@ StreamLog(void) * Start the replication */ if (verbose) - pg_log_info("starting log streaming at %X/%X (timeline %u)", + pg_log_info("starting log streaming at %X/%08X (timeline %u)", LSN_FORMAT_ARGS(stream.startpos), stream.timeline); @@ -689,7 +689,7 @@ main(int argc, char **argv) basedir = pg_strdup(optarg); break; case 'E': - if (sscanf(optarg, "%X/%X", &hi, &lo) != 2) + if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2) pg_fatal("could not parse end position \"%s\"", optarg); endpos = ((uint64) hi) << 32 | lo; break; @@ -770,7 +770,7 @@ main(int argc, char **argv) if (replication_slot == NULL && (do_drop_slot || do_create_slot)) { - /* translator: second %s is an option name */ + /* translator: %s is an option name */ pg_log_error("%s needs a slot to be specified using --slot", do_drop_slot ? "--drop-slot" : "--create-slot"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); diff --git a/src/bin/pg_basebackup/pg_recvlogical.c b/src/bin/pg_basebackup/pg_recvlogical.c index e6810efe5f0d7..be71783b370e1 100644 --- a/src/bin/pg_basebackup/pg_recvlogical.c +++ b/src/bin/pg_basebackup/pg_recvlogical.c @@ -3,7 +3,7 @@ * pg_recvlogical.c - receive data from a logical decoding slot in a streaming * fashion and write it to a local file. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/pg_recvlogical.c @@ -24,6 +24,7 @@ #include "getopt_long.h" #include "libpq-fe.h" #include "libpq/pqsignal.h" +#include "libpq/protocol.h" #include "pqexpbuffer.h" #include "streamutil.h" @@ -41,8 +42,8 @@ typedef enum /* Global Options */ static char *outfile = NULL; static int verbose = 0; -static bool two_phase = false; -static bool failover = false; +static bool two_phase = false; /* enable-two-phase option */ +static bool failover = false; /* enable-failover option */ static int noloop = 0; static int standby_message_timeout = 10 * 1000; /* 10 sec = default */ static int fsync_interval = 10 * 1000; /* 10 sec = default */ @@ -89,9 +90,9 @@ usage(void) printf(_(" --drop-slot drop the replication slot (for the slot's name see --slot)\n")); printf(_(" --start start streaming in a replication slot (for the slot's name see --slot)\n")); printf(_("\nOptions:\n")); + printf(_(" --enable-failover enable replication slot synchronization to standby servers when\n" + " creating a replication slot\n")); printf(_(" -E, --endpos=LSN exit after receiving the specified LSN\n")); - printf(_(" --failover enable replication slot synchronization to standby servers when\n" - " creating a slot\n")); printf(_(" -f, --file=FILE receive log into this file, - for stdout\n")); printf(_(" -F --fsync-interval=SECS\n" " time between fsyncs to the output file (default: %d)\n"), (fsync_interval / 1000)); @@ -105,7 +106,8 @@ usage(void) printf(_(" -s, --status-interval=SECS\n" " time between status packets sent to server (default: %d)\n"), (standby_message_timeout / 1000)); printf(_(" -S, --slot=SLOTNAME name of the logical replication slot\n")); - printf(_(" -t, --two-phase enable decoding of prepared transactions when creating a slot\n")); + printf(_(" -t, --enable-two-phase enable decoding of prepared transactions when creating a slot\n")); + printf(_(" --two-phase (same as --enable-two-phase, deprecated)\n")); printf(_(" -v, --verbose output verbose messages\n")); printf(_(" -V, --version output version information, then exit\n")); printf(_(" -?, --help show this help, then exit\n")); @@ -143,12 +145,12 @@ sendFeedback(PGconn *conn, TimestampTz now, bool force, bool replyRequested) return true; if (verbose) - pg_log_info("confirming write up to %X/%X, flush to %X/%X (slot %s)", + pg_log_info("confirming write up to %X/%08X, flush to %X/%08X (slot %s)", LSN_FORMAT_ARGS(output_written_lsn), LSN_FORMAT_ARGS(output_fsync_lsn), replication_slot); - replybuf[len] = 'r'; + replybuf[len] = PqReplMsg_StandbyStatusUpdate; len += 1; fe_sendint64(output_written_lsn, &replybuf[len]); /* write */ len += 8; @@ -182,29 +184,34 @@ disconnect_atexit(void) PQfinish(conn); } -static bool +static void OutputFsync(TimestampTz now) { output_last_fsync = now; output_fsync_lsn = output_written_lsn; + /* + * Save the last flushed position as the replication start point. On + * reconnect, replication resumes from there to avoid re-sending flushed + * data. + */ + startpos = output_fsync_lsn; + if (fsync_interval <= 0) - return true; + return; if (!output_needs_fsync) - return true; + return; output_needs_fsync = false; /* can only fsync if it's a regular file */ if (!output_isfile) - return true; + return; if (fsync(outfd) != 0) pg_fatal("could not fsync file \"%s\": %m", outfile); - - return true; } /* @@ -220,8 +227,6 @@ StreamLogicalLog(void) PQExpBuffer query; XLogRecPtr cur_record_lsn; - output_written_lsn = InvalidXLogRecPtr; - output_fsync_lsn = InvalidXLogRecPtr; cur_record_lsn = InvalidXLogRecPtr; /* @@ -237,13 +242,13 @@ StreamLogicalLog(void) * Start the replication */ if (verbose) - pg_log_info("starting log streaming at %X/%X (slot %s)", + pg_log_info("starting log streaming at %X/%08X (slot %s)", LSN_FORMAT_ARGS(startpos), replication_slot); /* Initiate the replication stream at specified location */ query = createPQExpBuffer(); - appendPQExpBuffer(query, "START_REPLICATION SLOT \"%s\" LOGICAL %X/%X", + appendPQExpBuffer(query, "START_REPLICATION SLOT \"%s\" LOGICAL %X/%08X", replication_slot, LSN_FORMAT_ARGS(startpos)); /* print options if there are any */ @@ -305,10 +310,7 @@ StreamLogicalLog(void) if (outfd != -1 && feTimestampDifferenceExceeds(output_last_fsync, now, fsync_interval)) - { - if (!OutputFsync(now)) - goto error; - } + OutputFsync(now); if (standby_message_timeout > 0 && feTimestampDifferenceExceeds(last_status, now, @@ -325,8 +327,7 @@ StreamLogicalLog(void) if (outfd != -1 && output_reopen && strcmp(outfile, "-") != 0) { now = feGetCurrentTimestamp(); - if (!OutputFsync(now)) - goto error; + OutputFsync(now); close(outfd); outfd = -1; } @@ -453,7 +454,7 @@ StreamLogicalLog(void) } /* Check the message type. */ - if (copybuf[0] == 'k') + if (copybuf[0] == PqReplMsg_Keepalive) { int pos; bool replyRequested; @@ -465,7 +466,7 @@ StreamLogicalLog(void) * We just check if the server requested a reply, and ignore the * rest. */ - pos = 1; /* skip msgtype 'k' */ + pos = 1; /* skip msgtype PqReplMsg_Keepalive */ walEnd = fe_recvint64(©buf[pos]); output_written_lsn = Max(walEnd, output_written_lsn); @@ -480,7 +481,7 @@ StreamLogicalLog(void) } replyRequested = copybuf[pos]; - if (endpos != InvalidXLogRecPtr && walEnd >= endpos) + if (XLogRecPtrIsValid(endpos) && walEnd >= endpos) { /* * If there's nothing to read on the socket until a keepalive @@ -508,7 +509,7 @@ StreamLogicalLog(void) continue; } - else if (copybuf[0] != 'w') + else if (copybuf[0] != PqReplMsg_WALData) { pg_log_error("unrecognized streaming header: \"%c\"", copybuf[0]); @@ -516,11 +517,11 @@ StreamLogicalLog(void) } /* - * Read the header of the XLogData message, enclosed in the CopyData + * Read the header of the WALData message, enclosed in the CopyData * message. We only need the WAL location field (dataStart), the rest * of the header is ignored. */ - hdr_len = 1; /* msgtype 'w' */ + hdr_len = 1; /* msgtype PqReplMsg_WALData */ hdr_len += 8; /* dataStart */ hdr_len += 8; /* walEnd */ hdr_len += 8; /* sendTime */ @@ -533,7 +534,7 @@ StreamLogicalLog(void) /* Extract WAL location for this block */ cur_record_lsn = fe_recvint64(©buf[1]); - if (endpos != InvalidXLogRecPtr && cur_record_lsn > endpos) + if (XLogRecPtrIsValid(endpos) && cur_record_lsn > endpos) { /* * We've read past our endpoint, so prepare to go away being @@ -581,7 +582,7 @@ StreamLogicalLog(void) goto error; } - if (endpos != InvalidXLogRecPtr && cur_record_lsn == endpos) + if (XLogRecPtrIsValid(endpos) && cur_record_lsn == endpos) { /* endpos was exactly the record we just processed, we're done */ if (!flushAndSendFeedback(conn, &now)) @@ -604,7 +605,7 @@ StreamLogicalLog(void) /* * We're doing a client-initiated clean exit and have sent CopyDone to * the server. Drain any messages, so we don't miss a last-minute - * ErrorResponse. The walsender stops generating XLogData records once + * ErrorResponse. The walsender stops generating WALData records once * it sees CopyDone, so expect this to finish quickly. After CopyDone, * it's too late for sendFeedback(), even if this were to take a long * time. Hence, use synchronous-mode PQgetCopyData(). @@ -645,9 +646,7 @@ StreamLogicalLog(void) { TimestampTz t = feGetCurrentTimestamp(); - /* no need to jump to error on failure here, we're finishing anyway */ OutputFsync(t); - if (close(outfd) != 0) pg_log_error("could not close file \"%s\": %m", outfile); } @@ -698,9 +697,10 @@ main(int argc, char **argv) {"file", required_argument, NULL, 'f'}, {"fsync-interval", required_argument, NULL, 'F'}, {"no-loop", no_argument, NULL, 'n'}, - {"failover", no_argument, NULL, 5}, + {"enable-failover", no_argument, NULL, 5}, + {"enable-two-phase", no_argument, NULL, 't'}, + {"two-phase", no_argument, NULL, 't'}, /* deprecated */ {"verbose", no_argument, NULL, 'v'}, - {"two-phase", no_argument, NULL, 't'}, {"version", no_argument, NULL, 'V'}, {"help", no_argument, NULL, '?'}, /* connection options */ @@ -798,12 +798,12 @@ main(int argc, char **argv) break; /* replication options */ case 'I': - if (sscanf(optarg, "%X/%X", &hi, &lo) != 2) + if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2) pg_fatal("could not parse start position \"%s\"", optarg); startpos = ((uint64) hi) << 32 | lo; break; case 'E': - if (sscanf(optarg, "%X/%X", &hi, &lo) != 2) + if (sscanf(optarg, "%X/%08X", &hi, &lo) != 2) pg_fatal("could not parse end position \"%s\"", optarg); endpos = ((uint64) hi) << 32 | lo; break; @@ -820,7 +820,7 @@ main(int argc, char **argv) } noptions += 1; - options = pg_realloc(options, sizeof(char *) * noptions * 2); + options = pg_realloc_array(options, char *, noptions * 2); options[(noptions - 1) * 2] = data; options[(noptions - 1) * 2 + 1] = val; @@ -910,14 +910,14 @@ main(int argc, char **argv) exit(1); } - if (startpos != InvalidXLogRecPtr && (do_create_slot || do_drop_slot)) + if (XLogRecPtrIsValid(startpos) && (do_create_slot || do_drop_slot)) { pg_log_error("cannot use --create-slot or --drop-slot together with --startpos"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (endpos != InvalidXLogRecPtr && !do_start_slot) + if (XLogRecPtrIsValid(endpos) && !do_start_slot) { pg_log_error("--endpos may only be specified with --start"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -928,14 +928,14 @@ main(int argc, char **argv) { if (two_phase) { - pg_log_error("--two-phase may only be specified with --create-slot"); + pg_log_error("%s may only be specified with --create-slot", "--enable-two-phase"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } if (failover) { - pg_log_error("--failover may only be specified with --create-slot"); + pg_log_error("%s may only be specified with --create-slot", "--enable-failover"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } @@ -1022,8 +1022,18 @@ main(int argc, char **argv) */ exit(0); } - else if (noloop) + + /* + * Ensure all written data is flushed to disk before exiting or + * starting a new replication. + */ + if (outfd != -1) + OutputFsync(feGetCurrentTimestamp()); + + if (noloop) + { pg_fatal("disconnected"); + } else { /* translator: check source for value for %d */ @@ -1045,8 +1055,7 @@ static bool flushAndSendFeedback(PGconn *conn, TimestampTz *now) { /* flush data to disk, so that we send a recent flush pointer */ - if (!OutputFsync(*now)) - return false; + OutputFsync(*now); *now = feGetCurrentTimestamp(); if (!sendFeedback(conn, *now, true, false)) return false; @@ -1073,12 +1082,12 @@ prepareToTerminate(PGconn *conn, XLogRecPtr endpos, StreamStopReason reason, pg_log_info("received interrupt signal, exiting"); break; case STREAM_STOP_KEEPALIVE: - pg_log_info("end position %X/%X reached by keepalive", + pg_log_info("end position %X/%08X reached by keepalive", LSN_FORMAT_ARGS(endpos)); break; case STREAM_STOP_END_OF_WAL: - Assert(!XLogRecPtrIsInvalid(lsn)); - pg_log_info("end position %X/%X reached by WAL record at %X/%X", + Assert(XLogRecPtrIsValid(lsn)); + pg_log_info("end position %X/%08X reached by WAL record at %X/%08X", LSN_FORMAT_ARGS(endpos), LSN_FORMAT_ARGS(lsn)); break; case STREAM_STOP_NONE: diff --git a/src/bin/pg_basebackup/po/meson.build b/src/bin/pg_basebackup/po/meson.build index bab2cd32646fd..200a10bf34ef1 100644 --- a/src/bin/pg_basebackup/po/meson.build +++ b/src/bin/pg_basebackup/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_basebackup-' + pg_version_major.to_string())] diff --git a/src/bin/pg_basebackup/receivelog.c b/src/bin/pg_basebackup/receivelog.c index 6b6e32dfbdf56..5ce8f2ba2871c 100644 --- a/src/bin/pg_basebackup/receivelog.c +++ b/src/bin/pg_basebackup/receivelog.c @@ -5,7 +5,7 @@ * * Author: Magnus Hagander * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/receivelog.c @@ -21,6 +21,7 @@ #include "access/xlog_internal.h" #include "common/logging.h" #include "libpq-fe.h" +#include "libpq/protocol.h" #include "receivelog.h" #include "streamutil.h" @@ -38,8 +39,8 @@ static int CopyStreamReceive(PGconn *conn, long timeout, pgsocket stop_socket, char **buffer); static bool ProcessKeepaliveMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, XLogRecPtr blockpos, TimestampTz *last_status); -static bool ProcessXLogDataMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, - XLogRecPtr *blockpos); +static bool ProcessWALDataMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, + XLogRecPtr *blockpos); static PGresult *HandleEndOfCopyStream(PGconn *conn, StreamCtl *stream, char *copybuf, XLogRecPtr blockpos, XLogRecPtr *stoppos); static bool CheckCopyStreamStop(PGconn *conn, StreamCtl *stream, XLogRecPtr blockpos); @@ -338,7 +339,7 @@ sendFeedback(PGconn *conn, XLogRecPtr blockpos, TimestampTz now, bool replyReque char replybuf[1 + 8 + 8 + 8 + 8 + 1]; int len = 0; - replybuf[len] = 'r'; + replybuf[len] = PqReplMsg_StandbyStatusUpdate; len += 1; fe_sendint64(blockpos, &replybuf[len]); /* write */ len += 8; @@ -571,7 +572,7 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) return true; /* Initiate the replication stream at specified location */ - snprintf(query, sizeof(query), "START_REPLICATION %s%X/%X TIMELINE %u", + snprintf(query, sizeof(query), "START_REPLICATION %s%X/%08X TIMELINE %u", slotcmd, LSN_FORMAT_ARGS(stream->startpos), stream->timeline); @@ -628,7 +629,7 @@ ReceiveXlogStream(PGconn *conn, StreamCtl *stream) } if (stream->startpos > stoppos) { - pg_log_error("server stopped streaming timeline %u at %X/%X, but reported next timeline %u to begin at %X/%X", + pg_log_error("server stopped streaming timeline %u at %X/%08X, but reported next timeline %u to begin at %X/%08X", stream->timeline, LSN_FORMAT_ARGS(stoppos), newtimeline, LSN_FORMAT_ARGS(stream->startpos)); goto error; @@ -720,7 +721,7 @@ ReadEndOfStreamingResult(PGresult *res, XLogRecPtr *startpos, uint32 *timeline) } *timeline = atoi(PQgetvalue(res, 0, 0)); - if (sscanf(PQgetvalue(res, 0, 1), "%X/%X", &startpos_xlogid, + if (sscanf(PQgetvalue(res, 0, 1), "%X/%08X", &startpos_xlogid, &startpos_xrecoff) != 2) { pg_log_error("could not parse next timeline's starting point \"%s\"", @@ -823,15 +824,15 @@ HandleCopyStream(PGconn *conn, StreamCtl *stream, } /* Check the message type. */ - if (copybuf[0] == 'k') + if (copybuf[0] == PqReplMsg_Keepalive) { if (!ProcessKeepaliveMsg(conn, stream, copybuf, r, blockpos, &last_status)) goto error; } - else if (copybuf[0] == 'w') + else if (copybuf[0] == PqReplMsg_WALData) { - if (!ProcessXLogDataMsg(conn, stream, copybuf, r, &blockpos)) + if (!ProcessWALDataMsg(conn, stream, copybuf, r, &blockpos)) goto error; /* @@ -1001,7 +1002,7 @@ ProcessKeepaliveMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, * Parse the keepalive message, enclosed in the CopyData message. We just * check if the server requested a reply, and ignore the rest. */ - pos = 1; /* skip msgtype 'k' */ + pos = 1; /* skip msgtype PqReplMsg_Keepalive */ pos += 8; /* skip walEnd */ pos += 8; /* skip sendTime */ @@ -1041,11 +1042,11 @@ ProcessKeepaliveMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, } /* - * Process XLogData message. + * Process WALData message. */ static bool -ProcessXLogDataMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, - XLogRecPtr *blockpos) +ProcessWALDataMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, + XLogRecPtr *blockpos) { int xlogoff; int bytes_left; @@ -1054,17 +1055,17 @@ ProcessXLogDataMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, /* * Once we've decided we don't want to receive any more, just ignore any - * subsequent XLogData messages. + * subsequent WALData messages. */ if (!(still_sending)) return true; /* - * Read the header of the XLogData message, enclosed in the CopyData + * Read the header of the WALData message, enclosed in the CopyData * message. We only need the WAL location field (dataStart), the rest of * the header is ignored. */ - hdr_len = 1; /* msgtype 'w' */ + hdr_len = 1; /* msgtype PqReplMsg_WALData */ hdr_len += 8; /* dataStart */ hdr_len += 8; /* walEnd */ hdr_len += 8; /* sendTime */ @@ -1162,7 +1163,7 @@ ProcessXLogDataMsg(PGconn *conn, StreamCtl *stream, char *copybuf, int len, return false; } still_sending = false; - return true; /* ignore the rest of this XLogData packet */ + return true; /* ignore the rest of this WALData packet */ } } } diff --git a/src/bin/pg_basebackup/receivelog.h b/src/bin/pg_basebackup/receivelog.h index 36beddcea459f..0caa904da6a8f 100644 --- a/src/bin/pg_basebackup/receivelog.h +++ b/src/bin/pg_basebackup/receivelog.h @@ -2,7 +2,7 @@ * * receivelog.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/receivelog.h diff --git a/src/bin/pg_basebackup/streamutil.c b/src/bin/pg_basebackup/streamutil.c index c7b8a4c3a4b6a..76abdfa2ae6ce 100644 --- a/src/bin/pg_basebackup/streamutil.c +++ b/src/bin/pg_basebackup/streamutil.c @@ -5,7 +5,7 @@ * * Author: Magnus Hagander * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/streamutil.c @@ -94,8 +94,8 @@ GetConnection(void) argcount++; } - keywords = pg_malloc0((argcount + 1) * sizeof(*keywords)); - values = pg_malloc0((argcount + 1) * sizeof(*values)); + keywords = pg_malloc0_array(const char *, argcount + 1); + values = pg_malloc0_array(const char *, argcount + 1); /* * Set dbname here already, so it can be overridden by a dbname in the @@ -117,8 +117,8 @@ GetConnection(void) } else { - keywords = pg_malloc0((argcount + 1) * sizeof(*keywords)); - values = pg_malloc0((argcount + 1) * sizeof(*values)); + keywords = pg_malloc0_array(const char *, argcount + 1); + values = pg_malloc0_array(const char *, argcount + 1); keywords[i] = "dbname"; values[i] = (dbname == NULL) ? "replication" : dbname; i++; @@ -445,7 +445,7 @@ RunIdentifySystem(PGconn *conn, char **sysid, TimeLineID *starttli, /* Get LSN start position if necessary */ if (startpos != NULL) { - if (sscanf(PQgetvalue(res, 0, 2), "%X/%X", &hi, &lo) != 2) + if (sscanf(PQgetvalue(res, 0, 2), "%X/%08X", &hi, &lo) != 2) { pg_log_error("could not parse write-ahead log location \"%s\"", PQgetvalue(res, 0, 2)); @@ -551,7 +551,7 @@ GetSlotInformation(PGconn *conn, const char *slot_name, uint32 hi, lo; - if (sscanf(PQgetvalue(res, 0, 1), "%X/%X", &hi, &lo) != 2) + if (sscanf(PQgetvalue(res, 0, 1), "%X/%08X", &hi, &lo) != 2) { pg_log_error("could not parse restart_lsn \"%s\" for replication slot \"%s\"", PQgetvalue(res, 0, 1), slot_name); diff --git a/src/bin/pg_basebackup/streamutil.h b/src/bin/pg_basebackup/streamutil.h index 017b227303c83..15afef3a9c8e4 100644 --- a/src/bin/pg_basebackup/streamutil.h +++ b/src/bin/pg_basebackup/streamutil.h @@ -2,7 +2,7 @@ * * streamutil.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/streamutil.h diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl index 7cdd444275524..cfcfdb8b58051 100644 --- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl +++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_basebackup/t/011_in_place_tablespace.pl b/src/bin/pg_basebackup/t/011_in_place_tablespace.pl index ec942e54eee87..a644d0b5f9af8 100644 --- a/src/bin/pg_basebackup/t/011_in_place_tablespace.pl +++ b/src/bin/pg_basebackup/t/011_in_place_tablespace.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_basebackup/t/020_pg_receivewal.pl b/src/bin/pg_basebackup/t/020_pg_receivewal.pl index 4be96affd7b7a..8da7cc86bae20 100644 --- a/src/bin/pg_basebackup/t/020_pg_receivewal.pl +++ b/src/bin/pg_basebackup/t/020_pg_receivewal.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_basebackup/t/030_pg_recvlogical.pl b/src/bin/pg_basebackup/t/030_pg_recvlogical.pl index c82e78847b382..063ad96b9be51 100644 --- a/src/bin/pg_basebackup/t/030_pg_recvlogical.pl +++ b/src/bin/pg_basebackup/t/030_pg_recvlogical.pl @@ -1,11 +1,12 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Utils; use PostgreSQL::Test::Cluster; use Test::More; +use Config; program_help_ok('pg_recvlogical'); program_version_ok('pg_recvlogical'); @@ -110,7 +111,7 @@ '--dbname' => $node->connstr('postgres'), '--start', '--endpos' => $nextlsn, - '--two-phase', '--no-loop', + '--enable-two-phase', '--no-loop', '--file' => '-', ], 'incorrect usage'); @@ -142,12 +143,106 @@ '--slot' => 'test', '--dbname' => $node->connstr('postgres'), '--create-slot', - '--failover', + '--enable-failover', ], 'slot with failover created'); my $result = $node->safe_psql('postgres', - "SELECT failover FROM pg_catalog.pg_replication_slots WHERE slot_name = 'test'"); + "SELECT failover FROM pg_catalog.pg_replication_slots WHERE slot_name = 'test'" +); is($result, 't', "failover is enabled for the new slot"); +# Test that when pg_recvlogical reconnects, it does not write duplicate +# records to the output file +my $outfile = $node->basedir . '/reconnect.out'; + +$node->command_ok( + [ + 'pg_recvlogical', + '--slot' => 'reconnect_test', + '--dbname' => $node->connstr('postgres'), + '--create-slot', + ], + 'slot created for reconnection test'); + +# Insert the first record for this test +$node->safe_psql('postgres', 'INSERT INTO test_table VALUES (1)'); + +my @pg_recvlogical_cmd = ( + 'pg_recvlogical', + '--slot' => 'reconnect_test', + '--dbname' => $node->connstr('postgres'), + '--start', + '--file' => $outfile, + '--fsync-interval' => '1', + '--status-interval' => '100', + '--verbose'); + +# On Windows, specify --endpos so pg_recvlogical can terminate, since +# signals cannot be used. Use the current LSN plus 32MB as endpos, which +# would be sufficient to cover the WAL generated by the test INSERTs. +if ($Config{osname} eq 'MSWin32') +{ + $nextlsn = $node->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn() + pg_size_bytes('32MB')"); + chomp($nextlsn); + push(@pg_recvlogical_cmd, '--endpos' => $nextlsn); +} + +my ($stdout, $stderr); +my $recv = IPC::Run::start( + [@pg_recvlogical_cmd], + '>' => \$stdout, + '2>' => \$stderr); + +# Wait for pg_recvlogical to receive and write the first INSERT +my $first_ins = wait_for_file($outfile, qr/INSERT/); + +# Terminate the walsender to force pg_recvlogical to reconnect +my $backend_pid = $node->safe_psql('postgres', + "SELECT active_pid FROM pg_replication_slots WHERE slot_name = 'reconnect_test'" +); +$node->safe_psql('postgres', "SELECT pg_terminate_backend($backend_pid)"); + +# Wait for pg_recvlogical to reconnect +$node->poll_query_until('postgres', + "SELECT active_pid IS NOT NULL AND active_pid != $backend_pid FROM pg_replication_slots WHERE slot_name = 'reconnect_test'" +) or die "Timed out while waiting for pg_recvlogical to reconnect"; + +# Insert the second record for this test +$node->safe_psql('postgres', 'INSERT INTO test_table VALUES (2)'); + +# Wait for pg_recvlogical to receive and write the second INSERT +wait_for_file($outfile, qr/INSERT/, $first_ins); + +# Terminate pg_recvlogical by generating WAL until the current position +# reaches the specified --endpos on Windows, or by sending a TERM signal +# on other platforms. +if ($Config{osname} eq 'MSWin32') +{ + $node->poll_query_until('postgres', + "SELECT pg_switch_wal() >= '$nextlsn' FROM pg_logical_emit_message(false, 'test', 'test')" + ) or die "Timed out while waiting for pg_recvlogical to end"; +} +else +{ + $recv->signal('TERM'); +} + +$recv->finish(); + +my $outfiledata = slurp_file("$outfile"); +my $count = (() = $outfiledata =~ /INSERT/g); +cmp_ok($count, '==', 2, + 'pg_recvlogical has received and written two INSERTs'); + +$node->command_ok( + [ + 'pg_recvlogical', + '--slot' => 'reconnect_test', + '--dbname' => $node->connstr('postgres'), + '--drop-slot' + ], + 'reconnect_test slot dropped'); + done_testing(); diff --git a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl index 2d532fee567dd..858082c70dfdd 100644 --- a/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl +++ b/src/bin/pg_basebackup/t/040_pg_createsubscriber.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # # Test using a standby server as the subscriber. @@ -14,6 +14,7 @@ program_options_handling_ok('pg_createsubscriber'); my $datadir = PostgreSQL::Test::Utils::tempdir; +my $logdir = PostgreSQL::Test::Utils::tempdir; # Generate a database with a name made of a range of ASCII characters. # Extracted from 002_pg_upgrade.pl. @@ -160,6 +161,7 @@ sub generate_db primary_conninfo = '$pconnstr dbname=postgres' hot_standby_feedback = on ]); +my $sconnstr = $node_s->connstr; $node_s->set_standby_mode(); $node_s->start; @@ -240,7 +242,6 @@ sub generate_db # Check some unmet conditions on node P $node_p->append_conf( 'postgresql.conf', q{ -wal_level = replica max_replication_slots = 1 max_wal_senders = 1 max_worker_processes = 2 @@ -265,7 +266,6 @@ sub generate_db # standby settings should not be a lower setting than on the primary. $node_p->append_conf( 'postgresql.conf', q{ -wal_level = logical max_replication_slots = 10 max_wal_senders = 10 max_worker_processes = 8 @@ -331,7 +331,7 @@ sub generate_db $node_p->wait_for_replay_catchup($node_s); # Create user-defined publications, wait for streaming replication to sync them -# to the standby, then verify that '--remove' +# to the standby, then verify that '--clean' # removes them. $node_p->safe_psql( $db1, qq( @@ -341,8 +341,8 @@ sub generate_db $node_p->wait_for_replay_catchup($node_s); -ok($node_s->safe_psql($db1, "SELECT COUNT(*) = 2 FROM pg_publication"), - 'two pre-existing publications on subscriber'); +is($node_s->safe_psql($db1, "SELECT COUNT(*) FROM pg_publication"), + '2', 'two pre-existing publications on subscriber'); $node_s->stop; @@ -363,9 +363,35 @@ sub generate_db '--subscription' => 'sub2', '--database' => $db1, '--database' => $db2, + '--logdir' => $logdir, ], 'run pg_createsubscriber --dry-run on node S'); +# Check that the log files were created +my @server_log_files = glob "$logdir/*/pg_createsubscriber_server.log"; +is(scalar(@server_log_files), + 1, "pg_createsubscriber_server.log file was created"); +my $server_log_file_size = -s $server_log_files[0]; +isnt($server_log_file_size, 0, + "pg_createsubscriber_server.log file not empty"); +my $server_log = slurp_file($server_log_files[0]); +like( + $server_log, + qr/consistent recovery state reached/, + "server reached consistent recovery state"); + +my @internal_log_files = glob "$logdir/*/pg_createsubscriber_internal.log"; +is(scalar(@internal_log_files), + 1, "pg_createsubscriber_internal.log file was created"); +my $internal_log_file_size = -s $internal_log_files[0]; +isnt($internal_log_file_size, 0, + "pg_createsubscriber_internal.log file not empty"); +my $internal_log = slurp_file($internal_log_files[0]); +like( + $internal_log, + qr/target server reached the consistent state/, + "log shows consistent state reached"); + # Check if node S is still a standby $node_s->start; is($node_s->safe_psql('postgres', 'SELECT pg_catalog.pg_is_in_recovery()'), @@ -399,7 +425,7 @@ sub generate_db '--database' => $db1, '--all', ], - qr/--database cannot be used with -a\/--all/, + qr/options --database and -a\/--all cannot be used together/, 'fail if --database is used with --all'); # run pg_createsubscriber with '--publication' and '--all' and verify @@ -416,7 +442,7 @@ sub generate_db '--all', '--publication' => 'pub1', ], - qr/--publication cannot be used with -a\/--all/, + qr/options --publication and -a\/--all cannot be used together/, 'fail if --publication is used with --all'); # run pg_createsubscriber with '--all' option @@ -436,17 +462,25 @@ sub generate_db # Verify that the required logical replication objects are output. # The expected count 3 refers to postgres, $db1 and $db2 databases. -is(scalar(() = $stderr =~ /creating publication/g), +is(scalar(() = $stderr =~ /would create publication/g), 3, "verify publications are created for all databases"); -is(scalar(() = $stderr =~ /creating the replication slot/g), +is(scalar(() = $stderr =~ /would create the replication slot/g), 3, "verify replication slots are created for all databases"); -is(scalar(() = $stderr =~ /creating subscription/g), +is(scalar(() = $stderr =~ /would create subscription/g), 3, "verify subscriptions are created for all databases"); +# Create a user-defined publication, and a table that is not a member of that +# publication. +$node_p->safe_psql( + $db1, qq( + CREATE PUBLICATION test_pub3 FOR TABLE tbl1; + CREATE TABLE not_replicated (a int); +)); + # Run pg_createsubscriber on node S. --verbose is used twice # to show more information. -# In passing, also test the --enable-two-phase option and -# --remove option +# +# Test two phase and clean options. Use pre-existing publication. command_ok( [ 'pg_createsubscriber', @@ -456,17 +490,22 @@ sub generate_db '--publisher-server' => $node_p->connstr($db1), '--socketdir' => $node_s->host, '--subscriber-port' => $node_s->port, - '--publication' => 'pub1', + '--publication' => 'test_pub3', '--publication' => 'pub2', '--replication-slot' => 'replslot1', '--replication-slot' => 'replslot2', '--database' => $db1, '--database' => $db2, '--enable-two-phase', - '--remove' => 'publications', + '--clean' => 'publications', ], 'run pg_createsubscriber on node S'); +# Check that included file is renamed after success. +my $node_s_datadir = $node_s->data_dir; +ok( -f "$node_s_datadir/pg_createsubscriber.conf.disabled", + "pg_createsubscriber.conf.disabled exists in node S"); + # Confirm the physical replication slot has been removed $result = $node_p->safe_psql($db1, "SELECT count(*) FROM pg_replication_slots WHERE slot_name = '$slotname'" @@ -478,13 +517,16 @@ sub generate_db # Insert rows on P $node_p->safe_psql($db1, "INSERT INTO tbl1 VALUES('third row')"); $node_p->safe_psql($db2, "INSERT INTO tbl2 VALUES('row 1')"); +$node_p->safe_psql($db1, "INSERT INTO not_replicated VALUES(0)"); # Start subscriber $node_s->start; # Confirm publications are removed from the subscriber node -is($node_s->safe_psql($db1, "SELECT COUNT(*) FROM pg_publication;"), - '0', 'all publications on subscriber have been removed'); +is($node_s->safe_psql($db1, 'SELECT COUNT(*) FROM pg_publication'), + '0', 'all publications were removed from db1'); +is($node_s->safe_psql($db2, 'SELECT COUNT(*) FROM pg_publication'), + '0', 'all publications were removed from db2'); # Verify that all subtwophase states are pending or enabled, # e.g. there are no subscriptions where subtwophase is disabled ('d') @@ -525,6 +567,8 @@ sub generate_db second row third row), "logical replication works in database $db1"); +$result = $node_s->safe_psql($db1, 'SELECT * FROM not_replicated'); +is($result, qq(), "table is not replicated in database $db1"); # Check result in database $db2 $result = $node_s->safe_psql($db2, 'SELECT * FROM tbl2'); @@ -535,7 +579,84 @@ sub generate_db 'SELECT system_identifier FROM pg_control_system()'); my $sysid_s = $node_s->safe_psql('postgres', 'SELECT system_identifier FROM pg_control_system()'); -ok($sysid_p != $sysid_s, 'system identifier was changed'); +isnt($sysid_p, $sysid_s, 'system identifier was changed'); + +# Verify that pub2 was created in $db2 +is( $node_p->safe_psql( + $db2, "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'pub2'"), + '1', + "publication pub2 was created in $db2"); + +# Get subscription and publication names +$result = $node_s->safe_psql( + 'postgres', qq( + SELECT subname, subpublications FROM pg_subscription WHERE subname ~ '^pg_createsubscriber_' + ORDER BY subpublications; +)); +like( + $result, + qr/^pg_createsubscriber_\d+_[0-9a-f]+ \|\{pub2\}\n + pg_createsubscriber_\d+_[0-9a-f]+ \|\{test_pub3\}$/x, + 'subscription and publication names are ok'); + +# Verify that the correct publications are being used +$result = $node_s->safe_psql( + 'postgres', qq( + SELECT d.datname, s.subpublications + FROM pg_subscription s + JOIN pg_database d ON d.oid = s.subdbid + WHERE subname ~ '^pg_createsubscriber_' + ORDER BY s.subdbid + ) +); + +is( $result, qq($db1|{test_pub3} +$db2|{pub2}), + "subscriptions use the correct publications"); + +# Verify that node K, set as a standby, is able to start correctly without +# the recovery configuration written by pg_createsubscriber interfering. +# This node is created from node S, where pg_createsubscriber has been run. + +# Create a physical standby from the promoted subscriber +$node_s->safe_psql('postgres', + "SELECT pg_create_physical_replication_slot('$slotname');"); + +# Create backup from promoted subscriber +$node_s->backup('backup_3'); + +# Initialize new physical standby +my $node_k = PostgreSQL::Test::Cluster->new('node_k'); +$node_k->init_from_backup($node_s, 'backup_3', has_streaming => 1); + +my $node_k_datadir = $node_k->data_dir; +ok( -f "$node_k_datadir/pg_createsubscriber.conf.disabled", + "pg_createsubscriber.conf.disabled exists in node K"); + +# Configure the new standby +$node_k->append_conf( + 'postgresql.conf', qq[ +primary_slot_name = '$slotname' +primary_conninfo = '$sconnstr dbname=postgres' +hot_standby_feedback = on +]); + +$node_k->set_standby_mode(); +my $node_k_name = $node_s->name; +command_ok( + [ + 'pg_ctl', '--wait', + '--pgdata' => $node_k->data_dir, + '--log' => $node_k->logfile, + '--options' => "--cluster-name=$node_k_name", + 'start' + ], + "node K has started"); + +# Note that this uses a direct pg_ctl command rather than a teardown(), +# because $node->stop() would not work due to the node's postmaster PID +# not being tracked, something that is set within $node->start(). +system_log('pg_ctl', 'stop', '--pgdata', $node_k->data_dir); # clean up $node_p->teardown_node; diff --git a/src/bin/pg_basebackup/walmethods.c b/src/bin/pg_basebackup/walmethods.c index eaaabc5f3745a..3a6b3b5f45bef 100644 --- a/src/bin/pg_basebackup/walmethods.c +++ b/src/bin/pg_basebackup/walmethods.c @@ -2,7 +2,7 @@ * * walmethods.c - implementations of different ways to write received wal * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/walmethods.c @@ -102,7 +102,7 @@ static char * dir_get_file_name(WalWriteMethod *wwmethod, const char *pathname, const char *temp_suffix) { - char *filename = pg_malloc0(MAXPGPATH * sizeof(char)); + char *filename = pg_malloc0_array(char, MAXPGPATH); snprintf(filename, MAXPGPATH, "%s%s%s", pathname, @@ -275,7 +275,7 @@ dir_open_for_write(WalWriteMethod *wwmethod, const char *pathname, } } - f = pg_malloc0(sizeof(DirectoryMethodFile)); + f = pg_malloc0_object(DirectoryMethodFile); #ifdef HAVE_LIBZ if (wwmethod->compression_algorithm == PG_COMPRESSION_GZIP) f->gzfp = gzfp; @@ -359,7 +359,7 @@ dir_write(Walfile *f, const void *buf, size_t count) return -1; } - inbuf = ((char *) inbuf) + chunk; + inbuf = ((const char *) inbuf) + chunk; } /* Our caller keeps track of the uncompressed size. */ @@ -643,7 +643,7 @@ CreateWalDirectoryMethod(const char *basedir, { DirectoryMethodData *wwmethod; - wwmethod = pg_malloc0(sizeof(DirectoryMethodData)); + wwmethod = pg_malloc0_object(DirectoryMethodData); *((const WalWriteMethodOps **) &wwmethod->base.ops) = &WalDirectoryMethodOps; wwmethod->base.compression_algorithm = compression_algorithm; @@ -825,7 +825,7 @@ static char * tar_get_file_name(WalWriteMethod *wwmethod, const char *pathname, const char *temp_suffix) { - char *filename = pg_malloc0(MAXPGPATH * sizeof(char)); + char *filename = pg_malloc0_array(char, MAXPGPATH); snprintf(filename, MAXPGPATH, "%s%s", pathname, temp_suffix ? temp_suffix : ""); @@ -859,7 +859,7 @@ tar_open_for_write(WalWriteMethod *wwmethod, const char *pathname, #ifdef HAVE_LIBZ if (wwmethod->compression_algorithm == PG_COMPRESSION_GZIP) { - tar_data->zp = (z_streamp) pg_malloc(sizeof(z_stream)); + tar_data->zp = pg_malloc_object(z_stream); tar_data->zp->zalloc = Z_NULL; tar_data->zp->zfree = Z_NULL; tar_data->zp->opaque = Z_NULL; @@ -893,7 +893,7 @@ tar_open_for_write(WalWriteMethod *wwmethod, const char *pathname, return NULL; } - tar_data->currentfile = pg_malloc0(sizeof(TarMethodFile)); + tar_data->currentfile = pg_malloc0_object(TarMethodFile); tar_data->currentfile->base.wwmethod = wwmethod; tmppath = tar_get_file_name(wwmethod, pathname, temp_suffix); @@ -1360,7 +1360,7 @@ CreateWalTarMethod(const char *tarbase, const char *suffix = (compression_algorithm == PG_COMPRESSION_GZIP) ? ".tar.gz" : ".tar"; - wwmethod = pg_malloc0(sizeof(TarMethodData)); + wwmethod = pg_malloc0_object(TarMethodData); *((const WalWriteMethodOps **) &wwmethod->base.ops) = &WalTarMethodOps; wwmethod->base.compression_algorithm = compression_algorithm; diff --git a/src/bin/pg_basebackup/walmethods.h b/src/bin/pg_basebackup/walmethods.h index f7a6dc18439ff..f296a4e43abbf 100644 --- a/src/bin/pg_basebackup/walmethods.h +++ b/src/bin/pg_basebackup/walmethods.h @@ -2,7 +2,7 @@ * * walmethods.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_basebackup/walmethods.h diff --git a/src/bin/pg_checksums/Makefile b/src/bin/pg_checksums/Makefile index a7f6d9c7c5cc9..b16cfafa0bfa4 100644 --- a/src/bin/pg_checksums/Makefile +++ b/src/bin/pg_checksums/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_checksums # -# Copyright (c) 1998-2025, PostgreSQL Global Development Group +# Copyright (c) 1998-2026, PostgreSQL Global Development Group # # src/bin/pg_checksums/Makefile # diff --git a/src/bin/pg_checksums/meson.build b/src/bin/pg_checksums/meson.build index 52b5cafa441fd..7b2401cb31b76 100644 --- a/src/bin/pg_checksums/meson.build +++ b/src/bin/pg_checksums/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_checksums_sources = files( 'pg_checksums.c', diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c index f20be82862a2b..cfacd1300fc1d 100644 --- a/src/bin/pg_checksums/pg_checksums.c +++ b/src/bin/pg_checksums/pg_checksums.c @@ -4,7 +4,7 @@ * Checks, enables or disables page level checksums for an offline * cluster * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_checksums/pg_checksums.c @@ -25,6 +25,7 @@ #include "common/logging.h" #include "common/relpath.h" #include "fe_utils/option_utils.h" +#include "fe_utils/version.h" #include "getopt_long.h" #include "pg_getopt.h" #include "storage/bufpage.h" @@ -448,6 +449,8 @@ main(int argc, char *argv[]) int c; int option_index; bool crc_ok; + uint32 major_version; + char *version_str; pg_logging_init(argv[0]); set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_checksums")); @@ -543,6 +546,20 @@ main(int argc, char *argv[]) exit(1); } + /* + * Retrieve the contents of this cluster's PG_VERSION. We require + * compatibility with the same major version as the one this tool is + * compiled with. + */ + major_version = GET_PG_MAJORVERSION_NUM(get_pg_version(DataDir, &version_str)); + if (major_version != PG_MAJORVERSION_NUM) + { + pg_log_error("data directory is of wrong version"); + pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".", + "PG_VERSION", version_str, PG_MAJORVERSION); + exit(1); + } + /* Read the control file and check compatibility */ ControlFile = get_controlfile(DataDir, &crc_ok); if (!crc_ok) @@ -568,15 +585,15 @@ main(int argc, char *argv[]) ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY) pg_fatal("cluster must be shut down"); - if (ControlFile->data_checksum_version == 0 && + if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION && mode == PG_MODE_CHECK) pg_fatal("data checksums are not enabled in cluster"); - if (ControlFile->data_checksum_version == 0 && + if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_OFF && mode == PG_MODE_DISABLE) pg_fatal("data checksums are already disabled in cluster"); - if (ControlFile->data_checksum_version > 0 && + if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION && mode == PG_MODE_ENABLE) pg_fatal("data checksums are already enabled in cluster"); @@ -628,7 +645,7 @@ main(int argc, char *argv[]) if (mode == PG_MODE_ENABLE || mode == PG_MODE_DISABLE) { ControlFile->data_checksum_version = - (mode == PG_MODE_ENABLE) ? PG_DATA_CHECKSUM_VERSION : 0; + (mode == PG_MODE_ENABLE) ? PG_DATA_CHECKSUM_VERSION : PG_DATA_CHECKSUM_OFF; if (do_sync) { diff --git a/src/bin/pg_checksums/po/meson.build b/src/bin/pg_checksums/po/meson.build index f654700f91ee8..f8bd34ae6c3c1 100644 --- a/src/bin/pg_checksums/po/meson.build +++ b/src/bin/pg_checksums/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_checksums-' + pg_version_major.to_string())] diff --git a/src/bin/pg_checksums/t/001_basic.pl b/src/bin/pg_checksums/t/001_basic.pl index 23730b1833ea3..fc5d431c2b00c 100644 --- a/src/bin/pg_checksums/t/001_basic.pl +++ b/src/bin/pg_checksums/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_checksums/t/002_actions.pl b/src/bin/pg_checksums/t/002_actions.pl index 339c1537a4606..94a11a534e98b 100644 --- a/src/bin/pg_checksums/t/002_actions.pl +++ b/src/bin/pg_checksums/t/002_actions.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Do basic sanity checks supported by pg_checksums using # an initialized cluster. diff --git a/src/bin/pg_combinebackup/Makefile b/src/bin/pg_combinebackup/Makefile index 33a1f4483bfbd..0d0089472e8cb 100644 --- a/src/bin/pg_combinebackup/Makefile +++ b/src/bin/pg_combinebackup/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_combinebackup # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/pg_combinebackup/Makefile diff --git a/src/bin/pg_combinebackup/backup_label.c b/src/bin/pg_combinebackup/backup_label.c index e89d4603f09dc..c7f51f0f8c73e 100644 --- a/src/bin/pg_combinebackup/backup_label.c +++ b/src/bin/pg_combinebackup/backup_label.c @@ -2,7 +2,7 @@ * * Read and manipulate backup label files * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/backup_label.c @@ -247,7 +247,7 @@ parse_lsn(char *s, char *e, XLogRecPtr *lsn, char **c) unsigned lo; *e = '\0'; - success = (sscanf(s, "%X/%X%n", &hi, &lo, &nchars) == 2); + success = (sscanf(s, "%X/%08X%n", &hi, &lo, &nchars) == 2); *e = save; if (success) diff --git a/src/bin/pg_combinebackup/backup_label.h b/src/bin/pg_combinebackup/backup_label.h index cbb56dc557b97..5fbb18bfe7de1 100644 --- a/src/bin/pg_combinebackup/backup_label.h +++ b/src/bin/pg_combinebackup/backup_label.h @@ -2,7 +2,7 @@ * * Read and manipulate backup label files * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/backup_label.h diff --git a/src/bin/pg_combinebackup/copy_file.c b/src/bin/pg_combinebackup/copy_file.c index db6c86223bbda..0287d6e87df5e 100644 --- a/src/bin/pg_combinebackup/copy_file.c +++ b/src/bin/pg_combinebackup/copy_file.c @@ -1,10 +1,10 @@ /* * Copy entire files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * src/bin/pg_combinebackup/copy_file.h + * src/bin/pg_combinebackup/copy_file.c * *------------------------------------------------------------------------- */ @@ -210,7 +210,7 @@ copy_file_blocks(const char *src, const char *dst, } if (rb < 0) - pg_fatal("could not read from file \"%s\": %m", dst); + pg_fatal("could not read from file \"%s\": %m", src); pg_free(buffer); close(src_fd); diff --git a/src/bin/pg_combinebackup/copy_file.h b/src/bin/pg_combinebackup/copy_file.h index 3779edd567793..f74fcb6c6899a 100644 --- a/src/bin/pg_combinebackup/copy_file.h +++ b/src/bin/pg_combinebackup/copy_file.h @@ -1,7 +1,7 @@ /* * Copy entire files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/copy_file.h diff --git a/src/bin/pg_combinebackup/load_manifest.c b/src/bin/pg_combinebackup/load_manifest.c index 8e0d04a26a6a7..2e50b7af4d217 100644 --- a/src/bin/pg_combinebackup/load_manifest.c +++ b/src/bin/pg_combinebackup/load_manifest.c @@ -2,7 +2,7 @@ * * Load data from a backup manifest into memory. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/load_manifest.c @@ -85,7 +85,7 @@ load_backup_manifests(int n_backups, char **backup_directories) manifest_data **result; int i; - result = pg_malloc(sizeof(manifest_data *) * n_backups); + result = pg_malloc_array(manifest_data *, n_backups); for (i = 0; i < n_backups; ++i) result[i] = load_backup_manifest(backup_directories[i]); @@ -139,7 +139,7 @@ load_backup_manifest(char *backup_directory) /* Create the hash table. */ ht = manifest_files_create(initial_size, NULL); - result = pg_malloc0(sizeof(manifest_data)); + result = pg_malloc0_object(manifest_data); result->files = ht; context.private_data = result; context.version_cb = combinebackup_version_cb; @@ -298,7 +298,7 @@ combinebackup_per_wal_range_cb(JsonManifestParseContext *context, manifest_wal_range *range; /* Allocate and initialize a struct describing this WAL range. */ - range = palloc(sizeof(manifest_wal_range)); + range = palloc_object(manifest_wal_range); range->tli = tli; range->start_lsn = start_lsn; range->end_lsn = end_lsn; diff --git a/src/bin/pg_combinebackup/load_manifest.h b/src/bin/pg_combinebackup/load_manifest.h index 9bd3c694b34aa..25edda3d30538 100644 --- a/src/bin/pg_combinebackup/load_manifest.h +++ b/src/bin/pg_combinebackup/load_manifest.h @@ -2,7 +2,7 @@ * * Load data from a backup manifest into memory. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/load_manifest.h diff --git a/src/bin/pg_combinebackup/meson.build b/src/bin/pg_combinebackup/meson.build index e80a4756a7f4f..a35b86f3f5987 100644 --- a/src/bin/pg_combinebackup/meson.build +++ b/src/bin/pg_combinebackup/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_combinebackup_sources = files( 'pg_combinebackup.c', @@ -38,6 +38,7 @@ tests += { 't/008_promote.pl', 't/009_no_full_file.pl', 't/010_hardlink.pl', + 't/011_ib_truncation.pl', ], } } diff --git a/src/bin/pg_combinebackup/pg_combinebackup.c b/src/bin/pg_combinebackup/pg_combinebackup.c index 28e58cd8ef458..d13bf63eb1e5f 100644 --- a/src/bin/pg_combinebackup/pg_combinebackup.c +++ b/src/bin/pg_combinebackup/pg_combinebackup.c @@ -3,7 +3,7 @@ * pg_combinebackup.c * Combine incremental backups with prior backups. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_combinebackup/pg_combinebackup.c @@ -34,6 +34,7 @@ #include "common/relpath.h" #include "copy_file.h" #include "fe_utils/option_utils.h" +#include "fe_utils/version.h" #include "getopt_long.h" #include "lib/stringinfo.h" #include "load_manifest.h" @@ -117,7 +118,6 @@ static void process_directory_recursively(Oid tsoid, manifest_data **manifests, manifest_writer *mwriter, cb_options *opt); -static int read_pg_version_file(char *directory); static void remember_to_cleanup_directory(char *target_path, bool rmtopdir); static void reset_directory_cleanup_list(void); static cb_tablespace *scan_for_existing_tablespaces(char *pathname, @@ -153,7 +153,7 @@ main(int argc, char *argv[]) int c; int n_backups; int n_prior_backups; - int version; + uint32 version; uint64 system_identifier; char **prior_backup_dirs; cb_options opt; @@ -162,6 +162,7 @@ main(int argc, char *argv[]) StringInfo last_backup_label; manifest_data **manifests; manifest_writer *mwriter; + char *pgdata; pg_logging_init(argv[0]); progname = get_progname(argv[0]); @@ -241,6 +242,10 @@ main(int argc, char *argv[]) if (opt.no_manifest) opt.manifest_checksums = CHECKSUM_TYPE_NONE; + if (opt.dry_run) + pg_log_info("Executing in dry-run mode.\n" + "The target directory will not be modified."); + /* Check that the platform supports the requested copy method. */ if (opt.copy_method == COPY_METHOD_CLONE) { @@ -271,7 +276,12 @@ main(int argc, char *argv[]) } /* Read the server version from the final backup. */ - version = read_pg_version_file(argv[argc - 1]); + pgdata = argv[argc - 1]; + version = get_pg_version(pgdata, NULL); + if (GET_PG_MAJORVERSION_NUM(version) < 10) + pg_fatal("server version too old"); + pg_log_debug("read server version %u from file \"%s/%s\"", + GET_PG_MAJORVERSION_NUM(version), pgdata, "PG_VERSION"); /* Sanity-check control files. */ n_backups = argc - optind; @@ -425,7 +435,7 @@ main(int argc, char *argv[]) else { pg_log_debug("recursively fsyncing \"%s\"", opt.output); - sync_pgdata(opt.output, version * 10000, opt.sync_method, true); + sync_pgdata(opt.output, version, opt.sync_method, true); } } @@ -445,7 +455,7 @@ main(int argc, char *argv[]) static void add_tablespace_mapping(cb_options *opt, char *arg) { - cb_tablespace_mapping *tsmap = pg_malloc0(sizeof(cb_tablespace_mapping)); + cb_tablespace_mapping *tsmap = pg_malloc0_object(cb_tablespace_mapping); char *dst; char *dst_ptr; char *arg_ptr; @@ -491,7 +501,7 @@ add_tablespace_mapping(cb_options *opt, char *arg) tsmap->old_dir); if (!is_absolute_path(tsmap->new_dir)) - pg_fatal("old directory is not an absolute path in tablespace mapping: %s", + pg_fatal("new directory is not an absolute path in tablespace mapping: %s", tsmap->new_dir); /* Canonicalize paths to avoid spurious failures when comparing. */ @@ -569,7 +579,7 @@ check_backup_label_files(int n_backups, char **backup_dirs) pg_fatal("backup at \"%s\" starts on timeline %u, but expected %u", backup_dirs[i], start_tli, check_tli); if (i < n_backups - 1 && start_lsn != check_lsn) - pg_fatal("backup at \"%s\" starts at LSN %X/%X, but expected %X/%X", + pg_fatal("backup at \"%s\" starts at LSN %X/%08X, but expected %X/%08X", backup_dirs[i], LSN_FORMAT_ARGS(start_lsn), LSN_FORMAT_ARGS(check_lsn)); @@ -605,7 +615,7 @@ check_control_files(int n_backups, char **backup_dirs) { int i; uint64 system_identifier = 0; /* placate compiler */ - uint32 data_checksum_version = 0; /* placate compiler */ + uint32 data_checksum_version = PG_DATA_CHECKSUM_OFF; /* placate compiler */ bool data_checksum_mismatch = false; /* Try to read each control file in turn, last to first. */ @@ -642,7 +652,7 @@ check_control_files(int n_backups, char **backup_dirs) */ if (i == n_backups - 1) data_checksum_version = control_file->data_checksum_version; - else if (data_checksum_version != 0 && + else if (data_checksum_version != PG_DATA_CHECKSUM_OFF && data_checksum_version != control_file->data_checksum_version) data_checksum_mismatch = true; @@ -1155,66 +1165,13 @@ process_directory_recursively(Oid tsoid, closedir(dir); } -/* - * Read the version number from PG_VERSION and convert it to the usual server - * version number format. (e.g. If PG_VERSION contains "14\n" this function - * will return 140000) - */ -static int -read_pg_version_file(char *directory) -{ - char filename[MAXPGPATH]; - StringInfoData buf; - int fd; - int version; - char *ep; - - /* Construct pathname. */ - snprintf(filename, MAXPGPATH, "%s/PG_VERSION", directory); - - /* Open file. */ - if ((fd = open(filename, O_RDONLY, 0)) < 0) - pg_fatal("could not open file \"%s\": %m", filename); - - /* Read into memory. Length limit of 128 should be more than generous. */ - initStringInfo(&buf); - slurp_file(fd, filename, &buf, 128); - - /* Close the file. */ - if (close(fd) != 0) - pg_fatal("could not close file \"%s\": %m", filename); - - /* Convert to integer. */ - errno = 0; - version = strtoul(buf.data, &ep, 10); - if (errno != 0 || *ep != '\n') - { - /* - * Incremental backup is not relevant to very old server versions that - * used multi-part version number (e.g. 9.6, or 8.4). So if we see - * what looks like the beginning of such a version number, just bail - * out. - */ - if (version < 10 && *ep == '.') - pg_fatal("%s: server version too old", filename); - pg_fatal("%s: could not parse version number", filename); - } - - /* Debugging output. */ - pg_log_debug("read server version %d from file \"%s\"", version, filename); - - /* Release memory and return result. */ - pfree(buf.data); - return version * 10000; -} - /* * Add a directory to the list of output directories to clean up. */ static void remember_to_cleanup_directory(char *target_path, bool rmtopdir) { - cb_cleanup_dir *dir = pg_malloc(sizeof(cb_cleanup_dir)); + cb_cleanup_dir *dir = pg_malloc_object(cb_cleanup_dir); dir->target_path = target_path; dir->rmtopdir = rmtopdir; @@ -1302,7 +1259,7 @@ scan_for_existing_tablespaces(char *pathname, cb_options *opt) } /* Create a new tablespace object. */ - ts = pg_malloc0(sizeof(cb_tablespace)); + ts = pg_malloc0_object(cb_tablespace); ts->oid = oid; /* diff --git a/src/bin/pg_combinebackup/po/meson.build b/src/bin/pg_combinebackup/po/meson.build index 29f7049ab0861..73fb1feeef9ee 100644 --- a/src/bin/pg_combinebackup/po/meson.build +++ b/src/bin/pg_combinebackup/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_combinebackup-' + pg_version_major.to_string())] diff --git a/src/bin/pg_combinebackup/reconstruct.c b/src/bin/pg_combinebackup/reconstruct.c index 8acaa54ff38b4..3349aa2441d29 100644 --- a/src/bin/pg_combinebackup/reconstruct.c +++ b/src/bin/pg_combinebackup/reconstruct.c @@ -3,7 +3,7 @@ * reconstruct.c * Reconstruct full file from incremental file and backup chain. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_combinebackup/reconstruct.c @@ -120,7 +120,7 @@ reconstruct_from_incremental_file(char *input_filename, * Every block must come either from the latest version of the file or * from one of the prior backups. */ - source = pg_malloc0(sizeof(rfile *) * (1 + n_prior_backups)); + source = pg_malloc0_array(rfile *, 1 + n_prior_backups); /* * Use the information from the latest incremental file to figure out how @@ -135,8 +135,8 @@ reconstruct_from_incremental_file(char *input_filename, * need to obtain it and at what offset in that file it's stored. * sourcemap gives us the first of these things, and offsetmap the latter. */ - sourcemap = pg_malloc0(sizeof(rfile *) * block_length); - offsetmap = pg_malloc0(sizeof(off_t) * block_length); + sourcemap = pg_malloc0_array(rfile *, block_length); + offsetmap = pg_malloc0_array(off_t, block_length); /* * Every block that is present in the newest incremental file should be @@ -370,6 +370,7 @@ reconstruct_from_incremental_file(char *input_filename, if (s->relative_block_numbers != NULL) pfree(s->relative_block_numbers); pg_free(s->filename); + pg_free(s); } pfree(sourcemap); pfree(offsetmap); @@ -482,7 +483,7 @@ make_incremental_rfile(char *filename) if (rf->num_blocks > 0) { rf->relative_block_numbers = - pg_malloc0(sizeof(BlockNumber) * rf->num_blocks); + pg_malloc0_array(BlockNumber, rf->num_blocks); read_bytes(rf, rf->relative_block_numbers, sizeof(BlockNumber) * rf->num_blocks); } @@ -511,12 +512,13 @@ make_rfile(char *filename, bool missing_ok) { rfile *rf; - rf = pg_malloc0(sizeof(rfile)); + rf = pg_malloc0_object(rfile); rf->filename = pstrdup(filename); if ((rf->fd = open(filename, O_RDONLY | PG_BINARY, 0)) < 0) { if (missing_ok && errno == ENOENT) { + pg_free(rf->filename); pg_free(rf); return NULL; } diff --git a/src/bin/pg_combinebackup/reconstruct.h b/src/bin/pg_combinebackup/reconstruct.h index c605fd7efdde0..2c42d586aad0e 100644 --- a/src/bin/pg_combinebackup/reconstruct.h +++ b/src/bin/pg_combinebackup/reconstruct.h @@ -3,7 +3,7 @@ * reconstruct.h * Reconstruct full file from incremental file and backup chain. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_combinebackup/reconstruct.h diff --git a/src/bin/pg_combinebackup/t/001_basic.pl b/src/bin/pg_combinebackup/t/001_basic.pl index 123bf4a21fb28..88b231cd1671b 100644 --- a/src/bin/pg_combinebackup/t/001_basic.pl +++ b/src/bin/pg_combinebackup/t/001_basic.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_combinebackup/t/002_compare_backups.pl b/src/bin/pg_combinebackup/t/002_compare_backups.pl index 2c7ca89b92f7f..b509296a94a3f 100644 --- a/src/bin/pg_combinebackup/t/002_compare_backups.pl +++ b/src/bin/pg_combinebackup/t/002_compare_backups.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -174,6 +174,7 @@ $pitr1->command_ok( [ 'pg_dumpall', + '--restrict-key' => 'test', '--no-sync', '--no-unlogged-table-data', '--file' => $dump1, @@ -183,6 +184,7 @@ $pitr2->command_ok( [ 'pg_dumpall', + '--restrict-key' => 'test', '--no-sync', '--no-unlogged-table-data', '--file' => $dump2, diff --git a/src/bin/pg_combinebackup/t/003_timeline.pl b/src/bin/pg_combinebackup/t/003_timeline.pl index 0205a59f927ef..37a18c5fe96c6 100644 --- a/src/bin/pg_combinebackup/t/003_timeline.pl +++ b/src/bin/pg_combinebackup/t/003_timeline.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # This test aims to validate that restoring an incremental backup works # properly even when the reference backup is on a different timeline. diff --git a/src/bin/pg_combinebackup/t/004_manifest.pl b/src/bin/pg_combinebackup/t/004_manifest.pl index 2a69d4d9b9ca0..b3fa9101cd67f 100644 --- a/src/bin/pg_combinebackup/t/004_manifest.pl +++ b/src/bin/pg_combinebackup/t/004_manifest.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # This test aims to validate that pg_combinebackup works in the degenerate # case where it is invoked on a single full backup and that it can produce diff --git a/src/bin/pg_combinebackup/t/005_integrity.pl b/src/bin/pg_combinebackup/t/005_integrity.pl index cfacf5ad7a001..9e1af2a7a7fc8 100644 --- a/src/bin/pg_combinebackup/t/005_integrity.pl +++ b/src/bin/pg_combinebackup/t/005_integrity.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # This test aims to validate that an incremental backup can be combined # with a valid prior backup and that it cannot be combined with an invalid diff --git a/src/bin/pg_combinebackup/t/006_db_file_copy.pl b/src/bin/pg_combinebackup/t/006_db_file_copy.pl index 65dd4e2d46074..f04ffb41fef7f 100644 --- a/src/bin/pg_combinebackup/t/006_db_file_copy.pl +++ b/src/bin/pg_combinebackup/t/006_db_file_copy.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_combinebackup/t/007_wal_level_minimal.pl b/src/bin/pg_combinebackup/t/007_wal_level_minimal.pl index be24e0558922a..99688fa54ddec 100644 --- a/src/bin/pg_combinebackup/t/007_wal_level_minimal.pl +++ b/src/bin/pg_combinebackup/t/007_wal_level_minimal.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # This test aims to validate that taking an incremental backup fails when # wal_level has been changed to minimal between the full backup and the diff --git a/src/bin/pg_combinebackup/t/008_promote.pl b/src/bin/pg_combinebackup/t/008_promote.pl index 3a15983f4a131..f6d7a45967641 100644 --- a/src/bin/pg_combinebackup/t/008_promote.pl +++ b/src/bin/pg_combinebackup/t/008_promote.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test whether WAL summaries are complete such that incremental backup # can be performed after promoting a standby at an arbitrary LSN. diff --git a/src/bin/pg_combinebackup/t/009_no_full_file.pl b/src/bin/pg_combinebackup/t/009_no_full_file.pl index abe9e9a6a8113..667db3228028a 100644 --- a/src/bin/pg_combinebackup/t/009_no_full_file.pl +++ b/src/bin/pg_combinebackup/t/009_no_full_file.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_combinebackup/t/010_hardlink.pl b/src/bin/pg_combinebackup/t/010_hardlink.pl index a0ee419090cf6..b6e8a9128af64 100644 --- a/src/bin/pg_combinebackup/t/010_hardlink.pl +++ b/src/bin/pg_combinebackup/t/010_hardlink.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group # # This test aims to validate that hard links are created as expected in the # output directory, when running pg_combinebackup with --link mode. @@ -56,7 +56,7 @@ '--pgdata' => $backup1path, '--no-sync', '--checkpoint' => 'fast', - '--wal-method' => 'none' + '--wal-method' => 'none' ], "full backup"); @@ -74,7 +74,7 @@ '--pgdata' => $backup2path, '--no-sync', '--checkpoint' => 'fast', - '--wal-method' => 'none', + '--wal-method' => 'none', '--incremental' => $backup1path . '/backup_manifest' ], "incremental backup"); @@ -112,45 +112,45 @@ # of the given data file. sub check_data_file { - my ($data_file, $last_segment_nlinks) = @_; - - my @data_file_segments = ($data_file); - - # Start checking for additional segments - my $segment_number = 1; - - while (1) - { - my $next_segment = $data_file . '.' . $segment_number; - - # If the file exists and is a regular file, add it to the list - if (-f $next_segment) - { - push @data_file_segments, $next_segment; - $segment_number++; - } - # Stop the loop if the file doesn't exist - else - { - last; - } - } - - # All segments of the given data file should contain 2 hard links, except - # for the last one, which should match the given number of links. - my $last_segment = pop @data_file_segments; - - for my $segment (@data_file_segments) - { - # Get the file's stat information of each segment - my $nlink_count = get_hard_link_count($segment); - ok($nlink_count == 2, "File '$segment' has 2 hard links"); - } - - # Get the file's stat information of the last segment - my $nlink_count = get_hard_link_count($last_segment); - ok($nlink_count == $last_segment_nlinks, - "File '$last_segment' has $last_segment_nlinks hard link(s)"); + my ($data_file, $last_segment_nlinks) = @_; + + my @data_file_segments = ($data_file); + + # Start checking for additional segments + my $segment_number = 1; + + while (1) + { + my $next_segment = $data_file . '.' . $segment_number; + + # If the file exists and is a regular file, add it to the list + if (-f $next_segment) + { + push @data_file_segments, $next_segment; + $segment_number++; + } + # Stop the loop if the file doesn't exist + else + { + last; + } + } + + # All segments of the given data file should contain 2 hard links, except + # for the last one, which should match the given number of links. + my $last_segment = pop @data_file_segments; + + for my $segment (@data_file_segments) + { + # Get the file's stat information of each segment + my $nlink_count = get_hard_link_count($segment); + is($nlink_count, 2, "File '$segment' has 2 hard links"); + } + + # Get the file's stat information of the last segment + my $nlink_count = get_hard_link_count($last_segment); + is($nlink_count, $last_segment_nlinks, + "File '$last_segment' has $last_segment_nlinks hard link(s)"); } @@ -159,11 +159,11 @@ sub check_data_file # that file. sub get_hard_link_count { - my ($file) = @_; + my ($file) = @_; - # Get file stats - my @stats = stat($file); - my $nlink = $stats[3]; # Number of hard links + # Get file stats + my @stats = stat($file); + my $nlink = $stats[3]; # Number of hard links - return $nlink; + return $nlink; } diff --git a/src/bin/pg_combinebackup/t/011_ib_truncation.pl b/src/bin/pg_combinebackup/t/011_ib_truncation.pl new file mode 100644 index 0000000000000..e1ce45215582c --- /dev/null +++ b/src/bin/pg_combinebackup/t/011_ib_truncation.pl @@ -0,0 +1,120 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# This test aims to validate two things: (1) that the calculated truncation +# block never exceeds the segment size and (2) that the correct limit block +# length is calculated for the VM fork. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize primary node +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(has_archiving => 1, allows_streaming => 1); +$primary->append_conf('postgresql.conf', 'summarize_wal = on'); +$primary->start; + +# Backup locations +my $backup_path = $primary->backup_dir; +my $full_backup = "$backup_path/full"; + +# To avoid using up lots of disk space in the CI/buildfarm environment, this +# test will only find the issue when run with a small RELSEG_SIZE. As of this +# writing, one of the CI runs is configured using --with-segsize-blocks=6, and +# we aim to have this test check for the issue only in that configuration. +my $target_blocks = 6; +my $block_size = $primary->safe_psql('postgres', + "SELECT current_setting('block_size')::int;"); + +# We'll have two blocks more than the target number of blocks (one will +# survive the subsequent truncation). +my $target_rows = int($target_blocks + 2); +my $rows_after_truncation = int($target_rows - 1); + +# Create a test table. STORAGE PLAIN prevents compression and TOASTing of +# repetitive data, ensuring predictable row sizes. +$primary->safe_psql( + 'postgres', q{ + CREATE TABLE t ( + id int, + data text STORAGE PLAIN + ) WITH (autovacuum_enabled = false); +}); + +# The tuple size should be enough to prevent two tuples from being on the same +# page. Since the template string has a length of 32 bytes, it's enough to +# repeat it (block_size / (2*32)) times. +$primary->safe_psql( + 'postgres', + "INSERT INTO t + SELECT i, + repeat('0123456789ABCDEF0123456789ABCDEF', ($block_size / (2*32))) + FROM generate_series(1, $target_rows) i;" +); + +# Make sure hint bits are set. +$primary->safe_psql('postgres', 'VACUUM t;'); + +# Verify that the relation is as large as was desired. +my $t_blocks = $primary->safe_psql('postgres', + "SELECT pg_relation_size('t') / current_setting('block_size')::int;"); +cmp_ok($t_blocks, '>', $target_blocks, 'target block size exceeded'); + +# Take a full base backup +$primary->backup('full'); + +# Delete rows at the logical end of the table, creating removable pages. +$primary->safe_psql('postgres', + "DELETE FROM t WHERE id > ($rows_after_truncation);"); + +# VACUUM the table. TRUNCATE is enabled by default, and is just mentioned here +# for emphasis. +$primary->safe_psql('postgres', 'VACUUM (TRUNCATE) t;'); + +# Verify expected length after truncation. +$t_blocks = $primary->safe_psql('postgres', + "SELECT pg_relation_size('t') / current_setting('block_size')::int;"); +is($t_blocks, $rows_after_truncation, 'post-truncation row count as expected'); +cmp_ok($t_blocks, '>', $target_blocks, + 'post-truncation block count as expected'); + +# Take an incremental backup based on the full backup manifest +$primary->backup('incr', + backup_options => [ '--incremental', "$full_backup/backup_manifest" ]); + +# We used to have a bug where the wrong limit block was calculated for the +# VM fork, so verify that the WAL summary records the correct VM fork +# truncation limit. We can't just check whether the restored VM fork is +# the right size on disk, because it's so small that the incremental backup +# code will send the entire file. +my $relfilenode = $primary->safe_psql('postgres', + "SELECT pg_relation_filenode('t');"); +my $vm_limits = $primary->safe_psql('postgres', + "SELECT string_agg(relblocknumber::text, ',') + FROM pg_available_wal_summaries() s, + pg_wal_summary_contents(s.tli, s.start_lsn, s.end_lsn) c + WHERE c.relfilenode = $relfilenode + AND c.relforknumber = 2 + AND c.is_limit_block;"); +is($vm_limits, '1', + 'WAL summary has correct VM fork truncation limit'); + +# Combine full and incremental backups. Before the fix, this failed because +# the INCREMENTAL file header contained an incorrect truncation_block_length +# value. +my $restored = PostgreSQL::Test::Cluster->new('node2'); +$restored->init_from_backup($primary, 'incr', combine_with_prior => ['full']); +$restored->start(); + +# Check that the restored table contains the correct number of rows +my $restored_count = + $restored->safe_psql('postgres', "SELECT count(*) FROM t;"); +is($restored_count, $rows_after_truncation, + 'Restored backup has correct row count'); + +$primary->stop; +$restored->stop; + +done_testing(); diff --git a/src/bin/pg_combinebackup/write_manifest.c b/src/bin/pg_combinebackup/write_manifest.c index 313f8929df509..715286043b587 100644 --- a/src/bin/pg_combinebackup/write_manifest.c +++ b/src/bin/pg_combinebackup/write_manifest.c @@ -2,7 +2,7 @@ * * Write a new backup manifest. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/write_manifest.c @@ -47,7 +47,7 @@ static size_t hex_encode(const uint8 *src, size_t len, char *dst); manifest_writer * create_manifest_writer(char *directory, uint64 system_identifier) { - manifest_writer *mwriter = pg_malloc(sizeof(manifest_writer)); + manifest_writer *mwriter = pg_malloc_object(manifest_writer); snprintf(mwriter->pathname, MAXPGPATH, "%s/backup_manifest", directory); mwriter->fd = -1; @@ -155,7 +155,7 @@ finalize_manifest(manifest_writer *mwriter, for (wal_range = first_wal_range; wal_range != NULL; wal_range = wal_range->next) appendStringInfo(&mwriter->buf, - "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%X\", \"End-LSN\": \"%X/%X\" }", + "%s{ \"Timeline\": %u, \"Start-LSN\": \"%X/%08X\", \"End-LSN\": \"%X/%08X\" }", wal_range == first_wal_range ? "" : ",\n", wal_range->tli, LSN_FORMAT_ARGS(wal_range->start_lsn), @@ -259,8 +259,8 @@ flush_manifest(manifest_writer *mwriter) if (wb < 0) pg_fatal("could not write file \"%s\": %m", mwriter->pathname); else - pg_fatal("could not write file \"%s\": wrote %d of %d", - mwriter->pathname, (int) wb, mwriter->buf.len); + pg_fatal("could not write file \"%s\": wrote %zd of %d", + mwriter->pathname, wb, mwriter->buf.len); } if (mwriter->still_checksumming && diff --git a/src/bin/pg_combinebackup/write_manifest.h b/src/bin/pg_combinebackup/write_manifest.h index 3bb9c04ec299f..f5fa8fc745881 100644 --- a/src/bin/pg_combinebackup/write_manifest.h +++ b/src/bin/pg_combinebackup/write_manifest.h @@ -2,7 +2,7 @@ * * Write a new backup manifest. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_combinebackup/write_manifest.h diff --git a/src/bin/pg_config/Makefile b/src/bin/pg_config/Makefile index 38169b0ded5b0..ce78a14d38f37 100644 --- a/src/bin/pg_config/Makefile +++ b/src/bin/pg_config/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_config # -# Copyright (c) 1998-2025, PostgreSQL Global Development Group +# Copyright (c) 1998-2026, PostgreSQL Global Development Group # # src/bin/pg_config/Makefile # diff --git a/src/bin/pg_config/meson.build b/src/bin/pg_config/meson.build index a245e752cbd99..cbdfe8e5a4c0f 100644 --- a/src/bin/pg_config/meson.build +++ b/src/bin/pg_config/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_config_sources = files( 'pg_config.c', diff --git a/src/bin/pg_config/pg_config.c b/src/bin/pg_config/pg_config.c index 28db024263dbd..9d924c7f7a422 100644 --- a/src/bin/pg_config/pg_config.c +++ b/src/bin/pg_config/pg_config.c @@ -15,7 +15,7 @@ * * This code is released under the terms of the PostgreSQL License. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/bin/pg_config/pg_config.c * diff --git a/src/bin/pg_config/po/meson.build b/src/bin/pg_config/po/meson.build index b407175884072..e01c545a99584 100644 --- a/src/bin/pg_config/po/meson.build +++ b/src/bin/pg_config/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_config-' + pg_version_major.to_string())] diff --git a/src/bin/pg_config/t/001_pg_config.pl b/src/bin/pg_config/t/001_pg_config.pl index 1dfc4c3f87633..be8e507631124 100644 --- a/src/bin/pg_config/t/001_pg_config.pl +++ b/src/bin/pg_config/t/001_pg_config.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_controldata/Makefile b/src/bin/pg_controldata/Makefile index 029370afb13d5..28709aeae1189 100644 --- a/src/bin/pg_controldata/Makefile +++ b/src/bin/pg_controldata/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_controldata # -# Copyright (c) 1998-2025, PostgreSQL Global Development Group +# Copyright (c) 1998-2026, PostgreSQL Global Development Group # # src/bin/pg_controldata/Makefile # diff --git a/src/bin/pg_controldata/meson.build b/src/bin/pg_controldata/meson.build index a7701ff675431..c587bb5bfd917 100644 --- a/src/bin/pg_controldata/meson.build +++ b/src/bin/pg_controldata/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_controldata_sources = files( 'pg_controldata.c', diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c index 7bb801bb88612..fe5fc5ec133b7 100644 --- a/src/bin/pg_controldata/pg_controldata.c +++ b/src/bin/pg_controldata/pg_controldata.c @@ -167,7 +167,14 @@ main(int argc, char *argv[]) /* get a copy of the control file */ ControlFile = get_controlfile(DataDir, &crc_ok); - if (!crc_ok) + if (ControlFile->pg_control_version != PG_CONTROL_VERSION) + { + pg_log_warning("control file version (%u) does not match the version understood by this program (%u)", + ControlFile->pg_control_version, PG_CONTROL_VERSION); + pg_log_warning_detail("Either the control file has been created with a different version of PostgreSQL, " + "or it is corrupt. The results below are untrustworthy."); + } + else if (!crc_ok) { pg_log_warning("calculated CRC checksum does not match value stored in control file"); pg_log_warning_detail("Either the control file is corrupt, or it has a different layout than this program " @@ -245,9 +252,9 @@ main(int argc, char *argv[]) dbState(ControlFile->state)); printf(_("pg_control last modified: %s\n"), pgctime_str); - printf(_("Latest checkpoint location: %X/%X\n"), + printf(_("Latest checkpoint location: %X/%08X\n"), LSN_FORMAT_ARGS(ControlFile->checkPoint)); - printf(_("Latest checkpoint's REDO location: %X/%X\n"), + printf(_("Latest checkpoint's REDO location: %X/%08X\n"), LSN_FORMAT_ARGS(ControlFile->checkPointCopy.redo)); printf(_("Latest checkpoint's REDO WAL file: %s\n"), xlogfilename); @@ -264,7 +271,7 @@ main(int argc, char *argv[]) ControlFile->checkPointCopy.nextOid); printf(_("Latest checkpoint's NextMultiXactId: %u\n"), ControlFile->checkPointCopy.nextMulti); - printf(_("Latest checkpoint's NextMultiOffset: %u\n"), + printf(_("Latest checkpoint's NextMultiOffset: %" PRIu64 "\n"), ControlFile->checkPointCopy.nextMultiOffset); printf(_("Latest checkpoint's oldestXID: %u\n"), ControlFile->checkPointCopy.oldestXid); @@ -280,17 +287,19 @@ main(int argc, char *argv[]) ControlFile->checkPointCopy.oldestCommitTsXid); printf(_("Latest checkpoint's newestCommitTsXid:%u\n"), ControlFile->checkPointCopy.newestCommitTsXid); + printf(_("Latest checkpoint's data_checksum_version:%u\n"), + ControlFile->checkPointCopy.dataChecksumState); printf(_("Time of latest checkpoint: %s\n"), ckpttime_str); - printf(_("Fake LSN counter for unlogged rels: %X/%X\n"), + printf(_("Fake LSN counter for unlogged rels: %X/%08X\n"), LSN_FORMAT_ARGS(ControlFile->unloggedLSN)); - printf(_("Minimum recovery ending location: %X/%X\n"), + printf(_("Minimum recovery ending location: %X/%08X\n"), LSN_FORMAT_ARGS(ControlFile->minRecoveryPoint)); printf(_("Min recovery ending loc's timeline: %u\n"), ControlFile->minRecoveryPointTLI); - printf(_("Backup start location: %X/%X\n"), + printf(_("Backup start location: %X/%08X\n"), LSN_FORMAT_ARGS(ControlFile->backupStartPoint)); - printf(_("Backup end location: %X/%X\n"), + printf(_("Backup end location: %X/%08X\n"), LSN_FORMAT_ARGS(ControlFile->backupEndPoint)); printf(_("End-of-backup record required: %s\n"), ControlFile->backupEndRequired ? _("yes") : _("no")); @@ -317,6 +326,8 @@ main(int argc, char *argv[]) ControlFile->blcksz); printf(_("Blocks per segment of large relation: %u\n"), ControlFile->relseg_size); + printf(_("Pages per SLRU segment: %u\n"), + ControlFile->slru_pages_per_segment); printf(_("WAL block size: %u\n"), ControlFile->xlog_blcksz); printf(_("Bytes per WAL segment: %u\n"), diff --git a/src/bin/pg_controldata/po/meson.build b/src/bin/pg_controldata/po/meson.build index 608ad5d6257db..c36e9b4e08d86 100644 --- a/src/bin/pg_controldata/po/meson.build +++ b/src/bin/pg_controldata/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_controldata-' + pg_version_major.to_string())] diff --git a/src/bin/pg_controldata/t/001_pg_controldata.pl b/src/bin/pg_controldata/t/001_pg_controldata.pl index 4aea00d6d5af4..561322e0856c9 100644 --- a/src/bin/pg_controldata/t/001_pg_controldata.pl +++ b/src/bin/pg_controldata/t/001_pg_controldata.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -21,16 +21,22 @@ qr/checkpoint/, 'pg_controldata produces output'); -# check with a corrupted pg_control +# Check with a corrupted pg_control +# +# To corrupt it, overwrite most of it with zeros. We leave the +# beginning portion that contains the pg_control version number (first +# 16 bytes) unmodified because otherwise you get an error about the +# version number, instead of checksum mismatch. my $pg_control = $node->data_dir . '/global/pg_control'; my $size = -s $pg_control; -open my $fh, '>', $pg_control or BAIL_OUT($!); +open my $fh, '+<', $pg_control or BAIL_OUT($!); binmode $fh; -# fill file with zeros -print $fh pack("x[$size]"); +my ($overwrite_off, $overwrite_len) = (16, $size - 16); +seek $fh, $overwrite_off, 0 or BAIL_OUT($!); +print $fh pack("x[$overwrite_len]"); close $fh; command_checks_all( diff --git a/src/bin/pg_ctl/Makefile b/src/bin/pg_ctl/Makefile index 1c4bb324714dd..5c2d4180980e8 100644 --- a/src/bin/pg_ctl/Makefile +++ b/src/bin/pg_ctl/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_ctl # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/pg_ctl/Makefile diff --git a/src/bin/pg_ctl/meson.build b/src/bin/pg_ctl/meson.build index e92ba50f8a3e6..69fa7a2842716 100644 --- a/src/bin/pg_ctl/meson.build +++ b/src/bin/pg_ctl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_ctl_sources = files( 'pg_ctl.c', diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c index 8a405ff122c71..5539eb8ebef61 100644 --- a/src/bin/pg_ctl/pg_ctl.c +++ b/src/bin/pg_ctl/pg_ctl.c @@ -2,7 +2,7 @@ * * pg_ctl --- start/stops/restarts the PostgreSQL server * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/bin/pg_ctl/pg_ctl.c * @@ -26,6 +26,7 @@ #include "common/file_perm.h" #include "common/logging.h" #include "common/string.h" +#include "datatype/timestamp.h" #include "getopt_long.h" #include "utils/pidfile.h" @@ -68,9 +69,9 @@ typedef enum #define DEFAULT_WAIT 60 -#define USEC_PER_SEC 1000000 - -#define WAITS_PER_SEC 10 /* should divide USEC_PER_SEC evenly */ +#define WAITS_PER_SEC 10 +StaticAssertDecl(USECS_PER_SEC % WAITS_PER_SEC == 0, + "WAITS_PER_SEC must divide USECS_PER_SEC evenly"); static bool do_wait = true; static int wait_seconds = DEFAULT_WAIT; @@ -345,7 +346,7 @@ readfile(const char *path, int *numlines) { /* empty file */ close(fd); - result = (char **) pg_malloc(sizeof(char *)); + result = pg_malloc_object(char *); *result = NULL; return result; } @@ -373,7 +374,7 @@ readfile(const char *path, int *numlines) } /* set up the result buffer */ - result = (char **) pg_malloc((nlines + 1) * sizeof(char *)); + result = pg_malloc_array(char *, nlines + 1); *numlines = nlines; /* now split the buffer into lines */ @@ -563,7 +564,7 @@ start_postmaster(void) if (!CreateRestrictedProcess(cmd, &pi, false)) { write_stderr(_("%s: could not start server: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); exit(1); } /* Don't close command process handle here; caller must do so */ @@ -699,7 +700,7 @@ wait_for_postmaster_start(pid_t pm_pid, bool do_checkpoint) print_msg("."); } - pg_usleep(USEC_PER_SEC / WAITS_PER_SEC); + pg_usleep(USECS_PER_SEC / WAITS_PER_SEC); } /* out of patience; report that postmaster is still starting up */ @@ -738,7 +739,7 @@ wait_for_postmaster_stop(void) if (cnt % WAITS_PER_SEC == 0) print_msg("."); - pg_usleep(USEC_PER_SEC / WAITS_PER_SEC); + pg_usleep(USECS_PER_SEC / WAITS_PER_SEC); } return false; /* timeout reached */ } @@ -771,7 +772,7 @@ wait_for_postmaster_promote(void) if (cnt % WAITS_PER_SEC == 0) print_msg("."); - pg_usleep(USEC_PER_SEC / WAITS_PER_SEC); + pg_usleep(USECS_PER_SEC / WAITS_PER_SEC); } return false; /* timeout reached */ } @@ -867,7 +868,7 @@ trap_sigint_during_startup(SIGNAL_ARGS) * Clear the signal handler, and send the signal again, to terminate the * process as normal. */ - pqsignal(postgres_signal_arg, SIG_DFL); + pqsignal(postgres_signal_arg, PG_SIG_DFL); raise(postgres_signal_arg); } @@ -1536,7 +1537,7 @@ pgwin32_doRegister(void) CloseServiceHandle(hSCM); write_stderr(_("%s: could not register service \"%s\": error code %lu\n"), progname, register_servicename, - (unsigned long) GetLastError()); + GetLastError()); exit(1); } CloseServiceHandle(hService); @@ -1566,7 +1567,7 @@ pgwin32_doUnregister(void) CloseServiceHandle(hSCM); write_stderr(_("%s: could not open service \"%s\": error code %lu\n"), progname, register_servicename, - (unsigned long) GetLastError()); + GetLastError()); exit(1); } if (!DeleteService(hService)) @@ -1575,7 +1576,7 @@ pgwin32_doUnregister(void) CloseServiceHandle(hSCM); write_stderr(_("%s: could not unregister service \"%s\": error code %lu\n"), progname, register_servicename, - (unsigned long) GetLastError()); + GetLastError()); exit(1); } CloseServiceHandle(hService); @@ -1724,7 +1725,7 @@ pgwin32_doRunAsService(void) { write_stderr(_("%s: could not start service \"%s\": error code %lu\n"), progname, register_servicename, - (unsigned long) GetLastError()); + GetLastError()); exit(1); } } @@ -1796,7 +1797,7 @@ CreateRestrictedProcess(char *cmd, PROCESS_INFORMATION *processInfo, bool as_ser * it doesn't cast DWORD before printing. */ write_stderr(_("%s: could not open process token: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); return 0; } @@ -1810,7 +1811,7 @@ CreateRestrictedProcess(char *cmd, PROCESS_INFORMATION *processInfo, bool as_ser 0, &dropSids[1].Sid)) { write_stderr(_("%s: could not allocate SIDs: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); return 0; } @@ -1836,7 +1837,7 @@ CreateRestrictedProcess(char *cmd, PROCESS_INFORMATION *processInfo, bool as_ser if (!b) { write_stderr(_("%s: could not create restricted token: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); return 0; } @@ -1855,8 +1856,7 @@ CreateRestrictedProcess(char *cmd, PROCESS_INFORMATION *processInfo, bool as_ser HANDLE job; char jobname[128]; - sprintf(jobname, "PostgreSQL_%lu", - (unsigned long) processInfo->dwProcessId); + sprintf(jobname, "PostgreSQL_%lu", processInfo->dwProcessId); job = CreateJobObject(NULL, jobname); if (job) @@ -1918,7 +1918,7 @@ GetPrivilegesToDelete(HANDLE hToken) !LookupPrivilegeValue(NULL, SE_CHANGE_NOTIFY_NAME, &luidChangeNotify)) { write_stderr(_("%s: could not get LUIDs for privileges: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); return NULL; } @@ -1926,7 +1926,7 @@ GetPrivilegesToDelete(HANDLE hToken) GetLastError() != ERROR_INSUFFICIENT_BUFFER) { write_stderr(_("%s: could not get token information: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); return NULL; } @@ -1941,7 +1941,7 @@ GetPrivilegesToDelete(HANDLE hToken) if (!GetTokenInformation(hToken, TokenPrivileges, tokenPrivs, length, &length)) { write_stderr(_("%s: could not get token information: error code %lu\n"), - progname, (unsigned long) GetLastError()); + progname, GetLastError()); free(tokenPrivs); return NULL; } diff --git a/src/bin/pg_ctl/po/meson.build b/src/bin/pg_ctl/po/meson.build index dedb45b480840..dcc89b933fd1d 100644 --- a/src/bin/pg_ctl/po/meson.build +++ b/src/bin/pg_ctl/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_ctl-' + pg_version_major.to_string())] diff --git a/src/bin/pg_ctl/t/001_start_stop.pl b/src/bin/pg_ctl/t/001_start_stop.pl index 8c86faf3ad5f1..4a25b35ed9ca4 100644 --- a/src/bin/pg_ctl/t/001_start_stop.pl +++ b/src/bin/pg_ctl/t/001_start_stop.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -112,7 +112,7 @@ ok(check_mode_recursive("$tempdir/data", 0750, 0640)); } -command_ok([ 'pg_ctl', 'restart', '--pgdata' => "$tempdir/data" ], +command_ok([ 'pg_ctl', 'restart', '--pgdata' => "$tempdir/data", '--log' => $logFileName ], 'pg_ctl restart with server running'); system_or_bail 'pg_ctl', 'stop', '--pgdata' => "$tempdir/data"; diff --git a/src/bin/pg_ctl/t/002_status.pl b/src/bin/pg_ctl/t/002_status.pl index 346f6919ac69c..1d7f9a1776cfb 100644 --- a/src/bin/pg_ctl/t/002_status.pl +++ b/src/bin/pg_ctl/t/002_status.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_ctl/t/003_promote.pl b/src/bin/pg_ctl/t/003_promote.pl index 43a9bbac2ac59..bc3d1163aeee4 100644 --- a/src/bin/pg_ctl/t/003_promote.pl +++ b/src/bin/pg_ctl/t/003_promote.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_ctl/t/004_logrotate.pl b/src/bin/pg_ctl/t/004_logrotate.pl index d78360e6d1ae1..7b19f86467313 100644 --- a/src/bin/pg_ctl/t/004_logrotate.pl +++ b/src/bin/pg_ctl/t/004_logrotate.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_dump/Makefile b/src/bin/pg_dump/Makefile index fa795883e9f30..79073b0a0ead8 100644 --- a/src/bin/pg_dump/Makefile +++ b/src/bin/pg_dump/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_dump # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/pg_dump/Makefile diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c index aa1589e3331d2..d1431c5c24c05 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -4,7 +4,7 @@ * Catalog routines used by pg_dump; long ago these were shared * by another dump tool, but not anymore. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -17,6 +17,7 @@ #include +#include "catalog/pg_am_d.h" #include "catalog/pg_class_d.h" #include "catalog/pg_collation_d.h" #include "catalog/pg_extension_d.h" @@ -243,8 +244,8 @@ getSchemaData(Archive *fout, int *numTablesPtr) pg_log_info("reading subscriptions"); getSubscriptions(fout); - pg_log_info("reading subscription membership of tables"); - getSubscriptionTables(fout); + pg_log_info("reading subscription membership of relations"); + getSubscriptionRelations(fout); free(inhinfo); /* not needed any longer */ @@ -352,7 +353,7 @@ flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables, tblinfo[i].numParents, tblinfo[i].dobj.name); - attachinfo = (TableAttachInfo *) palloc(sizeof(TableAttachInfo)); + attachinfo = palloc_object(TableAttachInfo); attachinfo->dobj.objType = DO_TABLE_ATTACH; attachinfo->dobj.catId.tableoid = 0; attachinfo->dobj.catId.oid = 0; @@ -496,7 +497,8 @@ flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, int numTables /* Some kinds never have parents */ if (tbinfo->relkind == RELKIND_SEQUENCE || tbinfo->relkind == RELKIND_VIEW || - tbinfo->relkind == RELKIND_MATVIEW) + tbinfo->relkind == RELKIND_MATVIEW || + tbinfo->relkind == RELKIND_PROPGRAPH) continue; /* Don't bother computing anything for non-target tables, either */ @@ -944,6 +946,24 @@ findOprByOid(Oid oid) return (OprInfo *) dobj; } +/* + * findAccessMethodByOid + * finds the DumpableObject for the access method with the given oid + * returns NULL if not found + */ +AccessMethodInfo * +findAccessMethodByOid(Oid oid) +{ + CatalogId catId; + DumpableObject *dobj; + + catId.tableoid = AccessMethodRelationId; + catId.oid = oid; + dobj = findObjectByCatalogId(catId); + Assert(dobj == NULL || dobj->objType == DO_ACCESS_METHOD); + return (AccessMethodInfo *) dobj; +} + /* * findCollationByOid * finds the DumpableObject for the collation with the given oid diff --git a/src/bin/pg_dump/compress_gzip.c b/src/bin/pg_dump/compress_gzip.c index 5a30ebf9bf5b5..60c553ba25ada 100644 --- a/src/bin/pg_dump/compress_gzip.c +++ b/src/bin/pg_dump/compress_gzip.c @@ -3,7 +3,7 @@ * compress_gzip.c * Routines for archivers to read or write a gzip compressed data stream. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -20,6 +20,15 @@ #ifdef HAVE_LIBZ #include +/* + * We don't use the gzgetc() macro, because zlib's configuration logic is not + * robust enough to guarantee that the macro will have the same ideas about + * struct field layout as the library itself does; see for example + * https://gnats.netbsd.org/cgi-bin/query-pr-single.pl?number=59711 + * Instead, #undef the macro and fall back to the underlying function. + */ +#undef gzgetc + /*---------------------- * Compressor API *---------------------- @@ -48,8 +57,8 @@ DeflateCompressorInit(CompressorState *cs) GzipCompressorState *gzipcs; z_streamp zp; - gzipcs = (GzipCompressorState *) pg_malloc0(sizeof(GzipCompressorState)); - zp = gzipcs->zp = (z_streamp) pg_malloc(sizeof(z_stream)); + gzipcs = pg_malloc0_object(GzipCompressorState); + zp = gzipcs->zp = pg_malloc_object(z_stream); zp->zalloc = Z_NULL; zp->zfree = Z_NULL; zp->opaque = Z_NULL; @@ -169,7 +178,7 @@ ReadDataFromArchiveGzip(ArchiveHandle *AH, CompressorState *cs) char *buf; size_t buflen; - zp = (z_streamp) pg_malloc(sizeof(z_stream)); + zp = pg_malloc_object(z_stream); zp->zalloc = Z_NULL; zp->zfree = Z_NULL; zp->opaque = Z_NULL; @@ -251,34 +260,53 @@ InitCompressorGzip(CompressorState *cs, *---------------------- */ -static bool -Gzip_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH) +static size_t +Gzip_read(void *ptr, size_t size, CompressFileHandle *CFH) { gzFile gzfp = (gzFile) CFH->private_data; int gzret; + /* Reading zero bytes must be a no-op */ + if (size == 0) + return 0; + gzret = gzread(gzfp, ptr, size); - if (gzret <= 0 && !gzeof(gzfp)) + + /* + * gzread returns zero on EOF as well as some error conditions, and less + * than zero on other error conditions, so we need to inspect for EOF on + * zero. + */ + if (gzret <= 0) { int errnum; - const char *errmsg = gzerror(gzfp, &errnum); + const char *errmsg; + + if (gzret == 0 && gzeof(gzfp)) + return 0; + + errmsg = gzerror(gzfp, &errnum); pg_fatal("could not read from input file: %s", errnum == Z_ERRNO ? strerror(errno) : errmsg); } - if (rsize) - *rsize = (size_t) gzret; - - return true; + return (size_t) gzret; } -static bool +static void Gzip_write(const void *ptr, size_t size, CompressFileHandle *CFH) { gzFile gzfp = (gzFile) CFH->private_data; + int errnum; + const char *errmsg; - return gzwrite(gzfp, ptr, size) > 0; + if (gzwrite(gzfp, ptr, size) != size) + { + errmsg = gzerror(gzfp, &errnum); + pg_fatal("could not write to file: %s", + errnum == Z_ERRNO ? strerror(errno) : errmsg); + } } static int @@ -358,12 +386,24 @@ Gzip_open(const char *path, int fd, const char *mode, CompressFileHandle *CFH) strcpy(mode_compression, mode); if (fd >= 0) - gzfp = gzdopen(dup(fd), mode_compression); + { + int dup_fd = dup(fd); + + if (dup_fd < 0) + return false; + gzfp = gzdopen(dup_fd, mode_compression); + if (gzfp == NULL) + { + close(dup_fd); + return false; + } + } else + { gzfp = gzopen(path, mode_compression); - - if (gzfp == NULL) - return false; + if (gzfp == NULL) + return false; + } CFH->private_data = gzfp; diff --git a/src/bin/pg_dump/compress_gzip.h b/src/bin/pg_dump/compress_gzip.h index 3bef0d5b1b802..af1a2a3445edd 100644 --- a/src/bin/pg_dump/compress_gzip.h +++ b/src/bin/pg_dump/compress_gzip.h @@ -3,7 +3,7 @@ * compress_gzip.h * GZIP interface to compress_io.c routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/compress_io.c b/src/bin/pg_dump/compress_io.c index 8c3d9c911c47b..52652b0d979da 100644 --- a/src/bin/pg_dump/compress_io.c +++ b/src/bin/pg_dump/compress_io.c @@ -4,7 +4,7 @@ * Routines for archivers to write an uncompressed or compressed data * stream. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * This file includes two APIs for dealing with compressed data. The first @@ -125,7 +125,7 @@ AllocateCompressor(const pg_compress_specification compression_spec, { CompressorState *cs; - cs = (CompressorState *) pg_malloc0(sizeof(CompressorState)); + cs = pg_malloc0_object(CompressorState); cs->readF = readF; cs->writeF = writeF; @@ -195,7 +195,7 @@ InitCompressFileHandle(const pg_compress_specification compression_spec) { CompressFileHandle *CFH; - CFH = pg_malloc0(sizeof(CompressFileHandle)); + CFH = pg_malloc0_object(CompressFileHandle); if (compression_spec.algorithm == PG_COMPRESSION_NONE) InitCompressFileHandleNone(CFH, compression_spec); @@ -269,6 +269,7 @@ InitDiscoverCompressFileHandle(const char *path, const char *mode) } CFH = InitCompressFileHandle(compression_spec); + errno = 0; if (!CFH->open_func(fname, -1, mode, CFH)) { free_keep_errno(CFH); @@ -289,6 +290,7 @@ EndCompressFileHandle(CompressFileHandle *CFH) { bool ret = false; + errno = 0; if (CFH->private_data) ret = CFH->close_func(CFH); diff --git a/src/bin/pg_dump/compress_io.h b/src/bin/pg_dump/compress_io.h index db9b38744c8e2..ed7b14f096302 100644 --- a/src/bin/pg_dump/compress_io.h +++ b/src/bin/pg_dump/compress_io.h @@ -3,7 +3,7 @@ * compress_io.h * Interface to compress_io.c routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,9 +22,9 @@ * * When changing this value, it's necessary to check the relevant test cases * still exercise all the branches. This applies especially if the value is - * increased, in which case the overflow buffer may not be needed. + * increased, in which case some loops may not get iterated. */ -#define DEFAULT_IO_BUFFER_SIZE 4096 +#define DEFAULT_IO_BUFFER_SIZE (128 * 1024) extern char *supports_compression(const pg_compress_specification compression_spec); @@ -123,21 +123,22 @@ struct CompressFileHandle CompressFileHandle *CFH); /* - * Read 'size' bytes of data from the file and store them into 'ptr'. - * Optionally it will store the number of bytes read in 'rsize'. + * Read up to 'size' bytes of data from the file and store them into + * 'ptr'. * - * Returns true on success and throws an internal error otherwise. + * Returns number of bytes read (this might be less than 'size' if EOF was + * reached). Exits via pg_fatal for all error conditions. */ - bool (*read_func) (void *ptr, size_t size, size_t *rsize, + size_t (*read_func) (void *ptr, size_t size, CompressFileHandle *CFH); /* * Write 'size' bytes of data into the file from 'ptr'. * - * Returns true on success and false on error. + * Returns nothing, exits via pg_fatal for all error conditions. */ - bool (*write_func) (const void *ptr, size_t size, - struct CompressFileHandle *CFH); + void (*write_func) (const void *ptr, size_t size, + CompressFileHandle *CFH); /* * Read at most size - 1 characters from the compress file handle into diff --git a/src/bin/pg_dump/compress_lz4.c b/src/bin/pg_dump/compress_lz4.c index e99f0cad71fcb..0a7872116e763 100644 --- a/src/bin/pg_dump/compress_lz4.c +++ b/src/bin/pg_dump/compress_lz4.c @@ -3,7 +3,7 @@ * compress_lz4.c * Routines for archivers to write a LZ4 compressed data stream. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -12,6 +12,7 @@ *------------------------------------------------------------------------- */ #include "postgres_fe.h" +#include #include "compress_lz4.h" #include "pg_backup_utils.h" @@ -59,27 +60,17 @@ typedef struct LZ4State bool compressing; /* - * Used by the Compressor API to mark if the compression headers have been - * written after initialization. + * I/O buffer area. */ - bool needs_header_flush; - - size_t buflen; - char *buffer; - - /* - * Used by the Stream API to store already uncompressed data that the - * caller has not consumed. - */ - size_t overflowalloclen; - size_t overflowlen; - char *overflowbuf; - - /* - * Used by both APIs to keep track of the compressed data length stored in - * the buffer. - */ - size_t compressedlen; + char *buffer; /* buffer for compressed data */ + size_t buflen; /* allocated size of buffer */ + size_t bufdata; /* amount of valid data currently in buffer */ + /* These fields are used only while decompressing: */ + size_t bufnext; /* next buffer position to decompress */ + char *outbuf; /* buffer for decompressed data */ + size_t outbuflen; /* allocated size of outbuf */ + size_t outbufdata; /* amount of valid data currently in outbuf */ + size_t outbufnext; /* next outbuf position to return */ /* * Used by both APIs to keep track of error codes. @@ -102,8 +93,22 @@ LZ4State_compression_init(LZ4State *state) { size_t status; + /* + * Compute size needed for buffer, assuming we will present at most + * DEFAULT_IO_BUFFER_SIZE input bytes at a time. + */ state->buflen = LZ4F_compressBound(DEFAULT_IO_BUFFER_SIZE, &state->prefs); + /* + * Add some slop to ensure we're not forced to flush every time. + * + * The present slop factor of 50% is chosen so that the typical output + * block size is about 128K when DEFAULT_IO_BUFFER_SIZE = 128K. We might + * need a different slop factor to maintain that equivalence if + * DEFAULT_IO_BUFFER_SIZE is changed dramatically. + */ + state->buflen += state->buflen / 2; + /* * LZ4F_compressBegin requires a buffer that is greater or equal to * LZ4F_HEADER_SIZE_MAX. Verify that the requirement is met. @@ -119,6 +124,10 @@ LZ4State_compression_init(LZ4State *state) } state->buffer = pg_malloc(state->buflen); + + /* + * Insert LZ4 header into buffer. + */ status = LZ4F_compressBegin(state->ctx, state->buffer, state->buflen, &state->prefs); @@ -128,7 +137,7 @@ LZ4State_compression_init(LZ4State *state) return false; } - state->compressedlen = status; + state->bufdata = status; return true; } @@ -157,8 +166,8 @@ ReadDataFromArchiveLZ4(ArchiveHandle *AH, CompressorState *cs) pg_fatal("could not create LZ4 decompression context: %s", LZ4F_getErrorName(status)); - outbuf = pg_malloc0(DEFAULT_IO_BUFFER_SIZE); - readbuf = pg_malloc0(DEFAULT_IO_BUFFER_SIZE); + outbuf = pg_malloc(DEFAULT_IO_BUFFER_SIZE); + readbuf = pg_malloc(DEFAULT_IO_BUFFER_SIZE); readbuflen = DEFAULT_IO_BUFFER_SIZE; while ((r = cs->readF(AH, &readbuf, &readbuflen)) > 0) { @@ -173,7 +182,6 @@ ReadDataFromArchiveLZ4(ArchiveHandle *AH, CompressorState *cs) size_t out_size = DEFAULT_IO_BUFFER_SIZE; size_t read_size = readend - readp; - memset(outbuf, 0, DEFAULT_IO_BUFFER_SIZE); status = LZ4F_decompress(ctx, outbuf, &out_size, readp, &read_size, &dec_opt); if (LZ4F_isError(status)) @@ -200,36 +208,37 @@ WriteDataToArchiveLZ4(ArchiveHandle *AH, CompressorState *cs, { LZ4State *state = (LZ4State *) cs->private_data; size_t remaining = dLen; - size_t status; - size_t chunk; - - /* Write the header if not yet written. */ - if (state->needs_header_flush) - { - cs->writeF(AH, state->buffer, state->compressedlen); - state->needs_header_flush = false; - } while (remaining > 0) { + size_t chunk; + size_t required; + size_t status; - if (remaining > DEFAULT_IO_BUFFER_SIZE) - chunk = DEFAULT_IO_BUFFER_SIZE; - else - chunk = remaining; + /* We don't try to present more than DEFAULT_IO_BUFFER_SIZE bytes */ + chunk = Min(remaining, (size_t) DEFAULT_IO_BUFFER_SIZE); + + /* If not enough space, must flush buffer */ + required = LZ4F_compressBound(chunk, &state->prefs); + if (required > state->buflen - state->bufdata) + { + cs->writeF(AH, state->buffer, state->bufdata); + state->bufdata = 0; + } - remaining -= chunk; status = LZ4F_compressUpdate(state->ctx, - state->buffer, state->buflen, + state->buffer + state->bufdata, + state->buflen - state->bufdata, data, chunk, NULL); if (LZ4F_isError(status)) pg_fatal("could not compress data: %s", LZ4F_getErrorName(status)); - cs->writeF(AH, state->buffer, status); + state->bufdata += status; - data = ((char *) data) + chunk; + data = ((const char *) data) + chunk; + remaining -= chunk; } } @@ -237,29 +246,32 @@ static void EndCompressorLZ4(ArchiveHandle *AH, CompressorState *cs) { LZ4State *state = (LZ4State *) cs->private_data; + size_t required; size_t status; /* Nothing needs to be done */ if (!state) return; - /* - * Write the header if not yet written. The caller is not required to call - * writeData if the relation does not contain any data. Thus it is - * possible to reach here without having flushed the header. Do it before - * ending the compression. - */ - if (state->needs_header_flush) - cs->writeF(AH, state->buffer, state->compressedlen); + /* We might need to flush the buffer to make room for LZ4F_compressEnd */ + required = LZ4F_compressBound(0, &state->prefs); + if (required > state->buflen - state->bufdata) + { + cs->writeF(AH, state->buffer, state->bufdata); + state->bufdata = 0; + } status = LZ4F_compressEnd(state->ctx, - state->buffer, state->buflen, + state->buffer + state->bufdata, + state->buflen - state->bufdata, NULL); if (LZ4F_isError(status)) pg_fatal("could not end compression: %s", LZ4F_getErrorName(status)); + state->bufdata += status; - cs->writeF(AH, state->buffer, status); + /* Write the final bufferload */ + cs->writeF(AH, state->buffer, state->bufdata); status = LZ4F_freeCompressionContext(state->ctx); if (LZ4F_isError(status)) @@ -293,7 +305,7 @@ InitCompressorLZ4(CompressorState *cs, const pg_compress_specification compressi if (cs->readF) return; - state = pg_malloc0(sizeof(*state)); + state = pg_malloc0_object(LZ4State); if (cs->compression_spec.level >= 0) state->prefs.compressionLevel = cs->compression_spec.level; @@ -301,8 +313,6 @@ InitCompressorLZ4(CompressorState *cs, const pg_compress_specification compressi pg_fatal("could not initialize LZ4 compression: %s", LZ4F_getErrorName(state->errcode)); - /* Remember that the header has not been written. */ - state->needs_header_flush = true; cs->private_data = state; } @@ -314,15 +324,16 @@ InitCompressorLZ4(CompressorState *cs, const pg_compress_specification compressi /* * LZ4 equivalent to feof() or gzeof(). Return true iff there is no - * decompressed output in the overflow buffer and the end of the backing file - * is reached. + * more buffered data and the end of the input file has been reached. */ static bool LZ4Stream_eof(CompressFileHandle *CFH) { LZ4State *state = (LZ4State *) CFH->private_data; - return state->overflowlen == 0 && feof(state->fp); + return state->outbufnext >= state->outbufdata && + state->bufnext >= state->bufdata && + feof(state->fp); } static const char * @@ -344,13 +355,15 @@ LZ4Stream_get_error(CompressFileHandle *CFH) * * Creates the necessary contexts for either compression or decompression. When * compressing data (indicated by compressing=true), it additionally writes the - * LZ4 header in the output stream. + * LZ4 header in the output buffer. + * + * It's expected that a not-yet-initialized LZ4State will be zero-filled. * * Returns true on success. In case of a failure returns false, and stores the * error code in state->errcode. */ static bool -LZ4Stream_init(LZ4State *state, int size, bool compressing) +LZ4Stream_init(LZ4State *state, bool compressing) { size_t status; @@ -358,20 +371,11 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing) return true; state->compressing = compressing; - state->inited = true; - /* When compressing, write LZ4 header to the output stream. */ if (state->compressing) { - if (!LZ4State_compression_init(state)) return false; - - if (fwrite(state->buffer, 1, state->compressedlen, state->fp) != state->compressedlen) - { - errno = (errno) ? errno : ENOSPC; - return false; - } } else { @@ -382,65 +386,22 @@ LZ4Stream_init(LZ4State *state, int size, bool compressing) return false; } - state->buflen = Max(size, DEFAULT_IO_BUFFER_SIZE); + state->buflen = DEFAULT_IO_BUFFER_SIZE; state->buffer = pg_malloc(state->buflen); - - state->overflowalloclen = state->buflen; - state->overflowbuf = pg_malloc(state->overflowalloclen); - state->overflowlen = 0; + state->outbuflen = DEFAULT_IO_BUFFER_SIZE; + state->outbuf = pg_malloc(state->outbuflen); } + state->inited = true; return true; } -/* - * Read already decompressed content from the overflow buffer into 'ptr' up to - * 'size' bytes, if available. If the eol_flag is set, then stop at the first - * occurrence of the newline char prior to 'size' bytes. - * - * Any unread content in the overflow buffer is moved to the beginning. - * - * Returns the number of bytes read from the overflow buffer (and copied into - * the 'ptr' buffer), or 0 if the overflow buffer is empty. - */ -static int -LZ4Stream_read_overflow(LZ4State *state, void *ptr, int size, bool eol_flag) -{ - char *p; - int readlen = 0; - - if (state->overflowlen == 0) - return 0; - - if (state->overflowlen >= size) - readlen = size; - else - readlen = state->overflowlen; - - if (eol_flag && (p = memchr(state->overflowbuf, '\n', readlen))) - /* Include the line terminating char */ - readlen = p - state->overflowbuf + 1; - - memcpy(ptr, state->overflowbuf, readlen); - state->overflowlen -= readlen; - - if (state->overflowlen > 0) - memmove(state->overflowbuf, state->overflowbuf + readlen, state->overflowlen); - - return readlen; -} - /* * The workhorse for reading decompressed content out of an LZ4 compressed * stream. * * It will read up to 'ptrsize' decompressed content, or up to the new line - * char if found first when the eol_flag is set. It is possible that the - * decompressed output generated by reading any compressed input via the - * LZ4F API, exceeds 'ptrsize'. Any exceeding decompressed content is stored - * at an overflow buffer within LZ4State. Of course, when the function is - * called, it will first try to consume any decompressed content already - * present in the overflow buffer, before decompressing new content. + * char if one is found first when the eol_flag is set. * * Returns the number of bytes of decompressed data copied into the ptr * buffer, or -1 in case of error. @@ -449,108 +410,97 @@ static int LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag) { int dsize = 0; - int rsize; - int size = ptrsize; - bool eol_found = false; - - void *readbuf; + int remaining = ptrsize; /* Lazy init */ - if (!LZ4Stream_init(state, size, false /* decompressing */ )) - return -1; - - /* No work needs to be done for a zero-sized output buffer */ - if (size <= 0) - return 0; - - /* Verify that there is enough space in the outbuf */ - if (size > state->buflen) + if (!LZ4Stream_init(state, false /* decompressing */ )) { - state->buflen = size; - state->buffer = pg_realloc(state->buffer, size); + pg_log_error("unable to initialize LZ4 library: %s", + LZ4F_getErrorName(state->errcode)); + return -1; } - /* use already decompressed content if available */ - dsize = LZ4Stream_read_overflow(state, ptr, size, eol_flag); - if (dsize == size || (eol_flag && memchr(ptr, '\n', dsize))) - return dsize; - - readbuf = pg_malloc(size); - - do + /* Loop until postcondition is satisfied */ + while (remaining > 0) { - char *rp; - char *rend; - - rsize = fread(readbuf, 1, size, state->fp); - if (rsize < size && !feof(state->fp)) - return -1; - - rp = (char *) readbuf; - rend = (char *) readbuf + rsize; - - while (rp < rend) + /* + * If we already have some decompressed data, return that. + */ + if (state->outbufnext < state->outbufdata) { - size_t status; - size_t outlen = state->buflen; - size_t read_remain = rend - rp; - - memset(state->buffer, 0, outlen); - status = LZ4F_decompress(state->dtx, state->buffer, &outlen, - rp, &read_remain, NULL); - if (LZ4F_isError(status)) + char *outptr = state->outbuf + state->outbufnext; + size_t readlen = state->outbufdata - state->outbufnext; + bool eol_found = false; + + if (readlen > remaining) + readlen = remaining; + /* If eol_flag is set, don't read beyond a newline */ + if (eol_flag) { - state->errcode = status; - return -1; - } + char *eolptr = memchr(outptr, '\n', readlen); - rp += read_remain; - - /* - * fill in what space is available in ptr if the eol flag is set, - * either skip if one already found or fill up to EOL if present - * in the outbuf - */ - if (outlen > 0 && dsize < size && eol_found == false) - { - char *p; - size_t lib = (!eol_flag) ? size - dsize : size - 1 - dsize; - size_t len = outlen < lib ? outlen : lib; - - if (eol_flag && - (p = memchr(state->buffer, '\n', outlen)) && - (size_t) (p - state->buffer + 1) <= len) + if (eolptr) { - len = p - state->buffer + 1; + readlen = eolptr - outptr + 1; eol_found = true; } + } + memcpy(ptr, outptr, readlen); + ptr = ((char *) ptr) + readlen; + state->outbufnext += readlen; + dsize += readlen; + remaining -= readlen; + if (eol_found || remaining == 0) + break; + /* We must have emptied outbuf */ + Assert(state->outbufnext >= state->outbufdata); + } - memcpy((char *) ptr + dsize, state->buffer, len); - dsize += len; + /* + * If we don't have any pending compressed data, load more into + * state->buffer. + */ + if (state->bufnext >= state->bufdata) + { + size_t rsize; - /* move what did not fit, if any, at the beginning of the buf */ - if (len < outlen) - memmove(state->buffer, state->buffer + len, outlen - len); - outlen -= len; + rsize = fread(state->buffer, 1, state->buflen, state->fp); + if (rsize < state->buflen && !feof(state->fp)) + { + pg_log_error("could not read from input file: %m"); + return -1; } + if (rsize == 0) + break; /* must be EOF */ + state->bufdata = rsize; + state->bufnext = 0; + } - /* if there is available output, save it */ - if (outlen > 0) + /* + * Decompress some data into state->outbuf. + */ + { + size_t status; + size_t outlen = state->outbuflen; + size_t inlen = state->bufdata - state->bufnext; + + status = LZ4F_decompress(state->dtx, + state->outbuf, &outlen, + state->buffer + state->bufnext, + &inlen, + NULL); + if (LZ4F_isError(status)) { - while (state->overflowlen + outlen > state->overflowalloclen) - { - state->overflowalloclen *= 2; - state->overflowbuf = pg_realloc(state->overflowbuf, - state->overflowalloclen); - } - - memcpy(state->overflowbuf + state->overflowlen, state->buffer, outlen); - state->overflowlen += outlen; + state->errcode = status; + pg_log_error("could not read from input file: %s", + LZ4F_getErrorName(state->errcode)); + return -1; } + state->bufnext += inlen; + state->outbufdata = outlen; + state->outbufnext = 0; } - } while (rsize == size && dsize < size && eol_found == false); - - pg_free(readbuf); + } return dsize; } @@ -558,48 +508,57 @@ LZ4Stream_read_internal(LZ4State *state, void *ptr, int ptrsize, bool eol_flag) /* * Compress size bytes from ptr and write them to the stream. */ -static bool +static void LZ4Stream_write(const void *ptr, size_t size, CompressFileHandle *CFH) { LZ4State *state = (LZ4State *) CFH->private_data; - size_t status; - int remaining = size; + size_t remaining = size; /* Lazy init */ - if (!LZ4Stream_init(state, size, true)) - return false; + if (!LZ4Stream_init(state, true)) + pg_fatal("unable to initialize LZ4 library: %s", + LZ4F_getErrorName(state->errcode)); while (remaining > 0) { - int chunk = Min(remaining, DEFAULT_IO_BUFFER_SIZE); + size_t chunk; + size_t required; + size_t status; - remaining -= chunk; + /* We don't try to present more than DEFAULT_IO_BUFFER_SIZE bytes */ + chunk = Min(remaining, (size_t) DEFAULT_IO_BUFFER_SIZE); - status = LZ4F_compressUpdate(state->ctx, state->buffer, state->buflen, - ptr, chunk, NULL); - if (LZ4F_isError(status)) + /* If not enough space, must flush buffer */ + required = LZ4F_compressBound(chunk, &state->prefs); + if (required > state->buflen - state->bufdata) { - state->errcode = status; - return false; + errno = 0; + if (fwrite(state->buffer, 1, state->bufdata, state->fp) != state->bufdata) + { + errno = (errno) ? errno : ENOSPC; + pg_fatal("error during writing: %m"); + } + state->bufdata = 0; } - if (fwrite(state->buffer, 1, status, state->fp) != status) - { - errno = (errno) ? errno : ENOSPC; - return false; - } + status = LZ4F_compressUpdate(state->ctx, + state->buffer + state->bufdata, + state->buflen - state->bufdata, + ptr, chunk, NULL); + if (LZ4F_isError(status)) + pg_fatal("error during writing: %s", LZ4F_getErrorName(status)); + state->bufdata += status; ptr = ((const char *) ptr) + chunk; + remaining -= chunk; } - - return true; } /* * fread() equivalent implementation for LZ4 compressed files. */ -static bool -LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH) +static size_t +LZ4Stream_read(void *ptr, size_t size, CompressFileHandle *CFH) { LZ4State *state = (LZ4State *) CFH->private_data; int ret; @@ -607,10 +566,7 @@ LZ4Stream_read(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH) if ((ret = LZ4Stream_read_internal(state, ptr, size, false)) < 0) pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH)); - if (rsize) - *rsize = (size_t) ret; - - return true; + return (size_t) ret; } /* @@ -643,11 +599,13 @@ LZ4Stream_gets(char *ptr, int size, CompressFileHandle *CFH) int ret; ret = LZ4Stream_read_internal(state, ptr, size - 1, true); - if (ret < 0 || (ret == 0 && !LZ4Stream_eof(CFH))) - pg_fatal("could not read from input file: %s", LZ4Stream_get_error(CFH)); - /* Done reading */ - if (ret == 0) + /* + * LZ4Stream_read_internal returning 0 or -1 means that it was either an + * EOF or an error, but gets_func is defined to return NULL in either case + * so we can treat both the same here. + */ + if (ret <= 0) return NULL; /* @@ -668,64 +626,121 @@ LZ4Stream_close(CompressFileHandle *CFH) { FILE *fp; LZ4State *state = (LZ4State *) CFH->private_data; + size_t required; size_t status; + int ret; + bool success = true; fp = state->fp; if (state->inited) { if (state->compressing) { - status = LZ4F_compressEnd(state->ctx, state->buffer, state->buflen, NULL); + /* We might need to flush the buffer to make room */ + required = LZ4F_compressBound(0, &state->prefs); + if (required > state->buflen - state->bufdata) + { + errno = 0; + if (fwrite(state->buffer, 1, state->bufdata, state->fp) != state->bufdata) + { + errno = (errno) ? errno : ENOSPC; + pg_log_error("could not write to output file: %m"); + success = false; + } + state->bufdata = 0; + } + + status = LZ4F_compressEnd(state->ctx, + state->buffer + state->bufdata, + state->buflen - state->bufdata, + NULL); if (LZ4F_isError(status)) - pg_fatal("could not end compression: %s", - LZ4F_getErrorName(status)); - else if (fwrite(state->buffer, 1, status, state->fp) != status) + { + pg_log_error("could not end compression: %s", + LZ4F_getErrorName(status)); + success = false; + } + else + state->bufdata += status; + + errno = 0; + if (fwrite(state->buffer, 1, state->bufdata, state->fp) != state->bufdata) { errno = (errno) ? errno : ENOSPC; - WRITE_ERROR_EXIT; + pg_log_error("could not write to output file: %m"); + success = false; } status = LZ4F_freeCompressionContext(state->ctx); if (LZ4F_isError(status)) - pg_fatal("could not end compression: %s", - LZ4F_getErrorName(status)); + { + pg_log_error("could not end compression: %s", + LZ4F_getErrorName(status)); + success = false; + } } else { status = LZ4F_freeDecompressionContext(state->dtx); if (LZ4F_isError(status)) - pg_fatal("could not end decompression: %s", - LZ4F_getErrorName(status)); - pg_free(state->overflowbuf); + { + pg_log_error("could not end decompression: %s", + LZ4F_getErrorName(status)); + success = false; + } + pg_free(state->outbuf); } pg_free(state->buffer); } pg_free(state); + CFH->private_data = NULL; - return fclose(fp) == 0; + errno = 0; + ret = fclose(fp); + if (ret != 0) + { + pg_log_error("could not close file: %m"); + success = false; + } + + return success; } static bool LZ4Stream_open(const char *path, int fd, const char *mode, CompressFileHandle *CFH) { - FILE *fp; LZ4State *state = (LZ4State *) CFH->private_data; if (fd >= 0) - fp = fdopen(fd, mode); + { + int dup_fd = dup(fd); + + if (dup_fd < 0) + { + state->errcode = errno; + return false; + } + state->fp = fdopen(dup_fd, mode); + if (state->fp == NULL) + { + state->errcode = errno; + close(dup_fd); + return false; + } + } else - fp = fopen(path, mode); - if (fp == NULL) { - state->errcode = errno; - return false; + state->fp = fopen(path, mode); + if (state->fp == NULL) + { + state->errcode = errno; + return false; + } } - state->fp = fp; - return true; } @@ -766,7 +781,7 @@ InitCompressFileHandleLZ4(CompressFileHandle *CFH, CFH->get_error_func = LZ4Stream_get_error; CFH->compression_spec = compression_spec; - state = pg_malloc0(sizeof(*state)); + state = pg_malloc0_object(LZ4State); if (CFH->compression_spec.level >= 0) state->prefs.compressionLevel = CFH->compression_spec.level; diff --git a/src/bin/pg_dump/compress_lz4.h b/src/bin/pg_dump/compress_lz4.h index 7f7216cc6489f..7360a469fc070 100644 --- a/src/bin/pg_dump/compress_lz4.h +++ b/src/bin/pg_dump/compress_lz4.h @@ -3,7 +3,7 @@ * compress_lz4.h * LZ4 interface to compress_io.c routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/compress_none.c b/src/bin/pg_dump/compress_none.c index 3fc89c9985461..743e2ce94b55b 100644 --- a/src/bin/pg_dump/compress_none.c +++ b/src/bin/pg_dump/compress_none.c @@ -3,7 +3,7 @@ * compress_none.c * Routines for archivers to read or write an uncompressed stream. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,6 +22,18 @@ *---------------------- */ +/* + * We buffer outgoing data, just to ensure that data blocks written to the + * archive file are of reasonable size. The read side could use this struct, + * but there's no need because it does not retain data across calls. + */ +typedef struct NoneCompressorState +{ + char *buffer; /* buffer for unwritten data */ + size_t buflen; /* allocated size of buffer */ + size_t bufdata; /* amount of valid data currently in buffer */ +} NoneCompressorState; + /* * Private routines */ @@ -49,13 +61,45 @@ static void WriteDataToArchiveNone(ArchiveHandle *AH, CompressorState *cs, const void *data, size_t dLen) { - cs->writeF(AH, data, dLen); + NoneCompressorState *nonecs = (NoneCompressorState *) cs->private_data; + size_t remaining = dLen; + + while (remaining > 0) + { + size_t chunk; + + /* Dump buffer if full */ + if (nonecs->bufdata >= nonecs->buflen) + { + cs->writeF(AH, nonecs->buffer, nonecs->bufdata); + nonecs->bufdata = 0; + } + /* And fill it */ + chunk = nonecs->buflen - nonecs->bufdata; + if (chunk > remaining) + chunk = remaining; + memcpy(nonecs->buffer + nonecs->bufdata, data, chunk); + nonecs->bufdata += chunk; + data = ((const char *) data) + chunk; + remaining -= chunk; + } } static void EndCompressorNone(ArchiveHandle *AH, CompressorState *cs) { - /* no op */ + NoneCompressorState *nonecs = (NoneCompressorState *) cs->private_data; + + if (nonecs) + { + /* Dump buffer if nonempty */ + if (nonecs->bufdata > 0) + cs->writeF(AH, nonecs->buffer, nonecs->bufdata); + /* Free working state */ + pg_free(nonecs->buffer); + pg_free(nonecs); + cs->private_data = NULL; + } } /* @@ -71,6 +115,22 @@ InitCompressorNone(CompressorState *cs, cs->end = EndCompressorNone; cs->compression_spec = compression_spec; + + /* + * If the caller has defined a write function, prepare the necessary + * buffer. + */ + if (cs->writeF) + { + NoneCompressorState *nonecs; + + nonecs = pg_malloc_object(NoneCompressorState); + nonecs->buflen = DEFAULT_IO_BUFFER_SIZE; + nonecs->buffer = pg_malloc(nonecs->buflen); + nonecs->bufdata = 0; + + cs->private_data = nonecs; + } } @@ -83,35 +143,31 @@ InitCompressorNone(CompressorState *cs, * Private routines */ -static bool -read_none(void *ptr, size_t size, size_t *rsize, CompressFileHandle *CFH) +static size_t +read_none(void *ptr, size_t size, CompressFileHandle *CFH) { FILE *fp = (FILE *) CFH->private_data; size_t ret; - if (size == 0) - return true; - ret = fread(ptr, 1, size, fp); - if (ret != size && !feof(fp)) + if (ferror(fp)) pg_fatal("could not read from input file: %m"); - if (rsize) - *rsize = ret; - - return true; + return ret; } -static bool +static void write_none(const void *ptr, size_t size, CompressFileHandle *CFH) { size_t ret; + errno = 0; ret = fwrite(ptr, 1, size, (FILE *) CFH->private_data); if (ret != size) - return false; - - return true; + { + errno = (errno) ? errno : ENOSPC; + pg_fatal("could not write to file: %m"); + } } static const char * @@ -153,7 +209,12 @@ close_none(CompressFileHandle *CFH) CFH->private_data = NULL; if (fp) + { + errno = 0; ret = fclose(fp); + if (ret != 0) + pg_log_error("could not close file: %m"); + } return ret == 0; } @@ -170,12 +231,24 @@ open_none(const char *path, int fd, const char *mode, CompressFileHandle *CFH) Assert(CFH->private_data == NULL); if (fd >= 0) - CFH->private_data = fdopen(dup(fd), mode); + { + int dup_fd = dup(fd); + + if (dup_fd < 0) + return false; + CFH->private_data = fdopen(dup_fd, mode); + if (CFH->private_data == NULL) + { + close(dup_fd); + return false; + } + } else + { CFH->private_data = fopen(path, mode); - - if (CFH->private_data == NULL) - return false; + if (CFH->private_data == NULL) + return false; + } return true; } diff --git a/src/bin/pg_dump/compress_none.h b/src/bin/pg_dump/compress_none.h index f927f196c36ae..5134f012ee970 100644 --- a/src/bin/pg_dump/compress_none.h +++ b/src/bin/pg_dump/compress_none.h @@ -3,7 +3,7 @@ * compress_none.h * Uncompressed interface to compress_io.c routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/compress_zstd.c b/src/bin/pg_dump/compress_zstd.c index cb595b10c2d32..68f1d8159171f 100644 --- a/src/bin/pg_dump/compress_zstd.c +++ b/src/bin/pg_dump/compress_zstd.c @@ -3,7 +3,7 @@ * compress_zstd.c * Routines for archivers to write a Zstd compressed data stream. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -13,6 +13,7 @@ */ #include "postgres_fe.h" +#include #include "compress_zstd.h" #include "pg_backup_utils.h" @@ -97,24 +98,22 @@ _ZstdWriteCommon(ArchiveHandle *AH, CompressorState *cs, bool flush) ZSTD_outBuffer *output = &zstdcs->output; /* Loop while there's any input or until flushed */ - while (input->pos != input->size || flush) + while (input->pos < input->size || flush) { size_t res; - output->pos = 0; res = ZSTD_compressStream2(zstdcs->cstream, output, input, flush ? ZSTD_e_end : ZSTD_e_continue); if (ZSTD_isError(res)) pg_fatal("could not compress data: %s", ZSTD_getErrorName(res)); - /* - * Extra paranoia: avoid zero-length chunks, since a zero length chunk - * is the EOF marker in the custom format. This should never happen - * but... - */ - if (output->pos > 0) + /* Dump output buffer if full, or if we're told to flush */ + if (output->pos >= output->size || flush) + { cs->writeF(AH, output->dst, output->pos); + output->pos = 0; + } if (res == 0) break; /* End of frame or all input consumed */ @@ -220,7 +219,7 @@ InitCompressorZstd(CompressorState *cs, cs->compression_spec = compression_spec; - zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs)); + zstdcs = pg_malloc0_object(ZstdCompressorState); cs->private_data = zstdcs; /* We expect that exactly one of readF/writeF is specified */ @@ -258,8 +257,8 @@ InitCompressorZstd(CompressorState *cs, * Compressed stream API */ -static bool -Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH) +static size_t +Zstd_read_internal(void *ptr, size_t size, CompressFileHandle *CFH, bool exit_on_error) { ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data; ZSTD_inBuffer *input = &zstdcs->input; @@ -268,11 +267,27 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH) size_t res, cnt; + /* + * If this is the first call to the reading function, initialize the + * required datastructures. + */ + if (zstdcs->dstream == NULL) + { + zstdcs->input.src = pg_malloc0(input_allocated_size); + zstdcs->dstream = ZSTD_createDStream(); + if (zstdcs->dstream == NULL) + { + if (exit_on_error) + pg_fatal("could not initialize compression library"); + return -1; + } + } + output->size = size; output->dst = ptr; output->pos = 0; - for (;;) + while (output->pos < output->size) { Assert(input->pos <= input->size); Assert(input->size <= input_allocated_size); @@ -292,6 +307,13 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH) if (input->pos == input->size) { cnt = fread(unconstify(void *, input->src), 1, input_allocated_size, zstdcs->fp); + if (ferror(zstdcs->fp)) + { + if (exit_on_error) + pg_fatal("could not read from input file: %m"); + return -1; + } + input->size = cnt; Assert(cnt <= input_allocated_size); @@ -307,7 +329,11 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH) res = ZSTD_decompressStream(zstdcs->dstream, output, input); if (ZSTD_isError(res)) - pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res)); + { + if (exit_on_error) + pg_fatal("could not decompress data: %s", ZSTD_getErrorName(res)); + return -1; + } if (output->pos == output->size) break; /* No more room for output */ @@ -315,18 +341,12 @@ Zstd_read(void *ptr, size_t size, size_t *rdsize, CompressFileHandle *CFH) if (res == 0) break; /* End of frame */ } - - if (output->pos == output->size) - break; /* We read all the data that fits */ } - if (rdsize != NULL) - *rdsize = output->pos; - - return true; + return output->pos; } -static bool +static void Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH) { ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data; @@ -339,41 +359,45 @@ Zstd_write(const void *ptr, size_t size, CompressFileHandle *CFH) input->size = size; input->pos = 0; + if (zstdcs->cstream == NULL) + { + zstdcs->output.size = ZSTD_CStreamOutSize(); + zstdcs->output.dst = pg_malloc(zstdcs->output.size); + zstdcs->output.pos = 0; + zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec); + if (zstdcs->cstream == NULL) + pg_fatal("could not initialize compression library"); + } + /* Consume all input, to be flushed later */ - while (input->pos != input->size) + while (input->pos < input->size) { - output->pos = 0; res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_continue); if (ZSTD_isError(res)) - { - zstdcs->zstderror = ZSTD_getErrorName(res); - return false; - } + pg_fatal("could not write to file: %s", ZSTD_getErrorName(res)); - cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp); - if (cnt != output->pos) + /* Dump output buffer if full */ + if (output->pos >= output->size) { - zstdcs->zstderror = strerror(errno); - return false; + errno = 0; + cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp); + if (cnt != output->pos) + { + errno = (errno) ? errno : ENOSPC; + pg_fatal("could not write to file: %m"); + } + output->pos = 0; } } - - return size; } static int Zstd_getc(CompressFileHandle *CFH) { - ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data; - int ret; + unsigned char ret; - if (CFH->read_func(&ret, 1, NULL, CFH) != 1) - { - if (feof(zstdcs->fp)) - pg_fatal("could not read from input file: end of file"); - else - pg_fatal("could not read from input file: %m"); - } + if (CFH->read_func(&ret, 1, CFH) != 1) + pg_fatal("could not read from input file: end of file"); return ret; } @@ -390,11 +414,7 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH) */ for (i = 0; i < len - 1; ++i) { - size_t readsz; - - if (!CFH->read_func(&buf[i], 1, &readsz, CFH)) - break; - if (readsz != 1) + if (Zstd_read_internal(&buf[i], 1, CFH, false) != 1) break; if (buf[i] == '\n') { @@ -406,10 +426,17 @@ Zstd_gets(char *buf, int len, CompressFileHandle *CFH) return i > 0 ? buf : NULL; } +static size_t +Zstd_read(void *ptr, size_t size, CompressFileHandle *CFH) +{ + return Zstd_read_internal(ptr, size, CFH, true); +} + static bool Zstd_close(CompressFileHandle *CFH) { ZstdCompressorState *zstdcs = (ZstdCompressorState *) CFH->private_data; + bool success = true; if (zstdcs->cstream) { @@ -421,20 +448,24 @@ Zstd_close(CompressFileHandle *CFH) /* Loop until the compression buffers are fully consumed */ for (;;) { - output->pos = 0; res = ZSTD_compressStream2(zstdcs->cstream, output, input, ZSTD_e_end); if (ZSTD_isError(res)) { zstdcs->zstderror = ZSTD_getErrorName(res); - return false; + success = false; + break; } + errno = 0; cnt = fwrite(output->dst, 1, output->pos, zstdcs->fp); if (cnt != output->pos) { + errno = (errno) ? errno : ENOSPC; zstdcs->zstderror = strerror(errno); - return false; + success = false; + break; } + output->pos = 0; if (res == 0) break; /* End of frame */ @@ -450,11 +481,16 @@ Zstd_close(CompressFileHandle *CFH) pg_free(unconstify(void *, zstdcs->input.src)); } + errno = 0; if (fclose(zstdcs->fp) != 0) - return false; + { + zstdcs->zstderror = strerror(errno); + success = false; + } pg_free(zstdcs); - return true; + CFH->private_data = NULL; + return success; } static bool @@ -472,35 +508,49 @@ Zstd_open(const char *path, int fd, const char *mode, FILE *fp; ZstdCompressorState *zstdcs; - if (fd >= 0) - fp = fdopen(fd, mode); - else - fp = fopen(path, mode); + /* + * Clear state storage to avoid having the fd point to non-NULL memory on + * error return. + */ + CFH->private_data = NULL; - if (fp == NULL) + zstdcs = (ZstdCompressorState *) pg_malloc_extended(sizeof(*zstdcs), + MCXT_ALLOC_NO_OOM | MCXT_ALLOC_ZERO); + if (!zstdcs) + { + errno = ENOMEM; return false; + } - zstdcs = (ZstdCompressorState *) pg_malloc0(sizeof(*zstdcs)); - CFH->private_data = zstdcs; - zstdcs->fp = fp; - - if (mode[0] == 'r') + if (fd >= 0) { - zstdcs->input.src = pg_malloc0(ZSTD_DStreamInSize()); - zstdcs->dstream = ZSTD_createDStream(); - if (zstdcs->dstream == NULL) - pg_fatal("could not initialize compression library"); + int dup_fd = dup(fd); + + if (dup_fd < 0) + { + pg_free(zstdcs); + return false; + } + fp = fdopen(dup_fd, mode); + if (fp == NULL) + { + close(dup_fd); + pg_free(zstdcs); + return false; + } } - else if (mode[0] == 'w' || mode[0] == 'a') + else { - zstdcs->output.size = ZSTD_CStreamOutSize(); - zstdcs->output.dst = pg_malloc0(zstdcs->output.size); - zstdcs->cstream = _ZstdCStreamParams(CFH->compression_spec); - if (zstdcs->cstream == NULL) - pg_fatal("could not initialize compression library"); + fp = fopen(path, mode); + if (fp == NULL) + { + pg_free(zstdcs); + return false; + } } - else - pg_fatal("unhandled mode \"%s\"", mode); + + zstdcs->fp = fp; + CFH->private_data = zstdcs; return true; } diff --git a/src/bin/pg_dump/compress_zstd.h b/src/bin/pg_dump/compress_zstd.h index af21db48ded34..1222d7107d99a 100644 --- a/src/bin/pg_dump/compress_zstd.h +++ b/src/bin/pg_dump/compress_zstd.h @@ -3,7 +3,7 @@ * compress_zstd.h * Zstd interface to compress_io.c routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/connectdb.c b/src/bin/pg_dump/connectdb.c index d55d53dbeeab9..f3ce8b1cfb10e 100644 --- a/src/bin/pg_dump/connectdb.c +++ b/src/bin/pg_dump/connectdb.c @@ -3,7 +3,7 @@ * connectdb.c * This is a common file connection to the database. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -89,8 +89,8 @@ ConnectDatabase(const char *dbname, const char *connection_string, argcount++; } - keywords = pg_malloc0((argcount + 1) * sizeof(*keywords)); - values = pg_malloc0((argcount + 1) * sizeof(*values)); + keywords = pg_malloc0_array(const char *, (argcount + 1)); + values = pg_malloc0_array(const char *, (argcount + 1)); for (conn_opt = conn_opts; conn_opt->keyword != NULL; conn_opt++) { @@ -105,8 +105,8 @@ ConnectDatabase(const char *dbname, const char *connection_string, } else { - keywords = pg_malloc0((argcount + 1) * sizeof(*keywords)); - values = pg_malloc0((argcount + 1) * sizeof(*values)); + keywords = pg_malloc0_array(const char *, (argcount + 1)); + values = pg_malloc0_array(const char *, (argcount + 1)); } if (pghost) diff --git a/src/bin/pg_dump/connectdb.h b/src/bin/pg_dump/connectdb.h index 6c1e1954769ff..67813853e653d 100644 --- a/src/bin/pg_dump/connectdb.h +++ b/src/bin/pg_dump/connectdb.h @@ -3,7 +3,7 @@ * connectdb.h * Common header file for connection to the database. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 73ce34346b278..dfb1f603a43e9 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -5,7 +5,7 @@ * Basically this is stuff that is useful in both pg_dump and pg_dumpall. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_dump/dumputils.c @@ -21,6 +21,7 @@ #include "dumputils.h" #include "fe_utils/string_utils.h" +static const char restrict_chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; static bool parseAclItem(const char *item, const char *type, const char *name, const char *subname, int remoteVersion, @@ -31,6 +32,43 @@ static void AddAcl(PQExpBuffer aclbuf, const char *keyword, const char *subname); +/* + * Sanitize a string to be included in an SQL comment or TOC listing, by + * replacing any newlines with spaces. This ensures each logical output line + * is in fact one physical output line, to prevent corruption of the dump + * (which could, in the worst case, present an SQL injection vulnerability + * if someone were to incautiously load a dump containing objects with + * maliciously crafted names). + * + * The result is a freshly malloc'd string. If the input string is NULL, + * return a malloc'ed empty string, unless want_hyphen, in which case return a + * malloc'ed hyphen. + * + * Note that we currently don't bother to quote names, meaning that the name + * fields aren't automatically parseable. "pg_restore -L" doesn't care because + * it only examines the dumpId field, but someday we might want to try harder. + */ +char * +sanitize_line(const char *str, bool want_hyphen) +{ + char *result; + char *s; + + if (!str) + return pg_strdup(want_hyphen ? "-" : ""); + + result = pg_strdup(str); + + for (s = result; *s != '\0'; s++) + { + if (*s == '\n' || *s == '\r') + *s = ' '; + } + + return result; +} + + /* * Build GRANT/REVOKE command(s) for an object. * @@ -122,7 +160,7 @@ buildACLCommands(const char *name, const char *subname, const char *nspname, * Besides, a false mismatch will just cause the output to be a little * more verbose than it really needed to be. */ - grantitems = (char **) pg_malloc(naclitems * sizeof(char *)); + grantitems = pg_malloc_array(char *, naclitems); for (i = 0; i < naclitems; i++) { bool found = false; @@ -138,7 +176,7 @@ buildACLCommands(const char *name, const char *subname, const char *nspname, if (!found) grantitems[ngrantitems++] = aclitems[i]; } - revokeitems = (char **) pg_malloc(nbaseitems * sizeof(char *)); + revokeitems = pg_malloc_array(char *, nbaseitems); for (i = 0; i < nbaseitems; i++) { bool found = false; @@ -472,6 +510,9 @@ do { \ /* UPDATE */ CONVERT_PRIV('w', "UPDATE"); } + else if (strcmp(type, "PROPERTY GRAPH") == 0 || + strcmp(type, "PROPERTY GRAPHS") == 0) + CONVERT_PRIV('r', "SELECT"); else if (strcmp(type, "FUNCTION") == 0 || strcmp(type, "FUNCTIONS") == 0) CONVERT_PRIV('X', "EXECUTE"); @@ -686,12 +727,13 @@ emitShSecLabels(PGconn *conn, PGresult *res, PQExpBuffer buffer, * currently known to guc.c, so that it'd be unsafe for extensions to declare * GUC_LIST_QUOTE variables anyway. Lacking a solution for that, it doesn't * seem worth the work to do more than have this list, which must be kept in - * sync with the variables actually marked GUC_LIST_QUOTE in guc_tables.c. + * sync with the variables actually marked GUC_LIST_QUOTE in guc_parameters.dat. */ bool variable_is_guc_list_quote(const char *name) { if (pg_strcasecmp(name, "local_preload_libraries") == 0 || + pg_strcasecmp(name, "oauth_validator_libraries") == 0 || pg_strcasecmp(name, "search_path") == 0 || pg_strcasecmp(name, "session_preload_libraries") == 0 || pg_strcasecmp(name, "shared_preload_libraries") == 0 || @@ -735,15 +777,15 @@ SplitGUCList(char *rawstring, char separator, * overestimate of the number of pointers we could need. Allow one for * list terminator. */ - *namelist = nextptr = (char **) - pg_malloc((strlen(rawstring) / 2 + 2) * sizeof(char *)); + *namelist = nextptr = + pg_malloc_array(char *, (strlen(rawstring) / 2 + 2)); *nextptr = NULL; while (isspace((unsigned char) *nextp)) nextp++; /* skip leading whitespace */ if (*nextp == '\0') - return true; /* allow empty string */ + return true; /* empty string represents empty list */ /* At the top of the loop, we are at start of a new identifier. */ do @@ -855,6 +897,7 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem, * elements as string literals. (The elements may be double-quoted as-is, * but we can't just feed them to the SQL parser; it would do the wrong * thing with elements that are zero-length or longer than NAMEDATALEN.) + * Also, we need a special case for empty lists. * * Variables that are not so marked should just be emitted as simple * string literals. If the variable is not known to @@ -870,6 +913,9 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem, /* this shouldn't fail really */ if (SplitGUCList(pos, ',', &namelist)) { + /* Special case: represent an empty list as NULL */ + if (*namelist == NULL) + appendPQExpBufferStr(buf, "NULL"); for (nameptr = namelist; *nameptr; nameptr++) { if (nameptr != namelist) @@ -920,3 +966,40 @@ create_or_open_dir(const char *dirname) pg_fatal("directory \"%s\" is not empty", dirname); } } + +/* + * Generates a valid restrict key (i.e., an alphanumeric string) for use with + * psql's \restrict and \unrestrict meta-commands. For safety, the value is + * chosen at random. + */ +char * +generate_restrict_key(void) +{ + uint8 buf[64]; + char *ret = palloc(sizeof(buf)); + + if (!pg_strong_random(buf, sizeof(buf))) + return NULL; + + for (int i = 0; i < sizeof(buf) - 1; i++) + { + uint8 idx = buf[i] % strlen(restrict_chars); + + ret[i] = restrict_chars[idx]; + } + ret[sizeof(buf) - 1] = '\0'; + + return ret; +} + +/* + * Checks that a given restrict key (intended for use with psql's \restrict and + * \unrestrict meta-commands) contains only alphanumeric characters. + */ +bool +valid_restrict_key(const char *restrict_key) +{ + return restrict_key != NULL && + restrict_key[0] != '\0' && + strspn(restrict_key, restrict_chars) == strlen(restrict_key); +} diff --git a/src/bin/pg_dump/dumputils.h b/src/bin/pg_dump/dumputils.h index 91c6e612e282e..d231ce1d6546c 100644 --- a/src/bin/pg_dump/dumputils.h +++ b/src/bin/pg_dump/dumputils.h @@ -5,7 +5,7 @@ * Basically this is stuff that is useful in both pg_dump and pg_dumpall. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_dump/dumputils.h @@ -25,9 +25,10 @@ * We don't print the timezone on Windows, because the names are long and * localized, which means they may contain characters in various random * encodings; this has been seen to cause encoding errors when reading the - * dump script. Think not to get around that by using %z, because - * (1) %z is not portable to pre-C99 systems, and - * (2) %z doesn't actually act differently from %Z on Windows anyway. + * dump script. One could now possibly get around that by using %z, but %z + * was previously not portable to pre-C99 systems, and also previously %z + * didn't actually act differently from %Z on Windows. But of these problems + * might be obsolete now. */ #ifndef WIN32 #define PGDUMP_STRFTIME_FMT "%Y-%m-%d %H:%M:%S %Z" @@ -36,6 +37,7 @@ #endif +extern char *sanitize_line(const char *str, bool want_hyphen); extern bool buildACLCommands(const char *name, const char *subname, const char *nspname, const char *type, const char *acls, const char *baseacls, const char *owner, const char *prefix, int remoteVersion, @@ -64,4 +66,7 @@ extern void makeAlterConfigCommand(PGconn *conn, const char *configitem, PQExpBuffer buf); extern void create_or_open_dir(const char *dirname); +extern char *generate_restrict_key(void); +extern bool valid_restrict_key(const char *restrict_key); + #endif /* DUMPUTILS_H */ diff --git a/src/bin/pg_dump/filter.c b/src/bin/pg_dump/filter.c index 7214d51413771..fbff69d4f7ef5 100644 --- a/src/bin/pg_dump/filter.c +++ b/src/bin/pg_dump/filter.c @@ -3,7 +3,7 @@ * filter.c * Implementation of simple filter file parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -171,9 +171,8 @@ pg_log_filter_error(FilterStateData *fstate, const char *fmt,...) /* * filter_get_keyword - read the next filter keyword from buffer * - * Search for keywords (limited to ascii alphabetic characters) in - * the passed in line buffer. Returns NULL when the buffer is empty or the first - * char is not alpha. The char '_' is allowed, except as the first character. + * Search for keywords (strings of non-whitespace characters) in the passed + * in line buffer. Returns NULL when the buffer is empty or no keyword exists. * The length of the found keyword is returned in the size parameter. */ static const char * @@ -182,6 +181,9 @@ filter_get_keyword(const char **line, int *size) const char *ptr = *line; const char *result = NULL; + /* The passed buffer must not be NULL */ + Assert(*line != NULL); + /* Set returned length preemptively in case no keyword is found */ *size = 0; @@ -189,11 +191,12 @@ filter_get_keyword(const char **line, int *size) while (isspace((unsigned char) *ptr)) ptr++; - if (isalpha((unsigned char) *ptr)) + /* Grab one keyword that's the string of non-whitespace characters */ + if (*ptr != '\0' && !isspace((unsigned char) *ptr)) { result = ptr++; - while (isalpha((unsigned char) *ptr) || *ptr == '_') + while (*ptr != '\0' && !isspace((unsigned char) *ptr)) ptr++; *size = ptr - result; diff --git a/src/bin/pg_dump/filter.h b/src/bin/pg_dump/filter.h index f35b7b8d0c1c4..b87cd6dec58ae 100644 --- a/src/bin/pg_dump/filter.h +++ b/src/bin/pg_dump/filter.h @@ -3,7 +3,7 @@ * filter.h * Common header file for the parser of filter file * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/meson.build b/src/bin/pg_dump/meson.build index d8e9e101254b1..7c9a475963b5c 100644 --- a/src/bin/pg_dump/meson.build +++ b/src/bin/pg_dump/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_dump_common_sources = files( 'compress_gzip.c', @@ -91,9 +91,9 @@ tests += { 'bd': meson.current_build_dir(), 'tap': { 'env': { - 'GZIP_PROGRAM': gzip.found() ? gzip.path() : '', - 'LZ4': program_lz4.found() ? program_lz4.path() : '', - 'ZSTD': program_zstd.found() ? program_zstd.path() : '', + 'GZIP_PROGRAM': gzip.found() ? gzip.full_path() : '', + 'LZ4': program_lz4.found() ? program_lz4.full_path() : '', + 'ZSTD': program_zstd.found() ? program_zstd.full_path() : '', 'with_icu': icu.found() ? 'yes' : 'no', }, 'tests': [ @@ -102,7 +102,8 @@ tests += { 't/003_pg_dump_with_server.pl', 't/004_pg_dump_parallel.pl', 't/005_pg_dump_filterfile.pl', - 't/006_pg_dumpall.pl', + 't/006_pg_dump_compress.pl', + 't/007_pg_dumpall.pl', 't/010_dump_connstr.pl', ], }, diff --git a/src/bin/pg_dump/parallel.c b/src/bin/pg_dump/parallel.c index 5974d6706fd57..a7bed5ecccf3f 100644 --- a/src/bin/pg_dump/parallel.c +++ b/src/bin/pg_dump/parallel.c @@ -4,7 +4,7 @@ * * Parallel support for pg_dump and pg_restore * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -334,8 +334,12 @@ on_exit_close_archive(Archive *AHX) } /* - * When pg_restore restores multiple databases, then update already added entry - * into array for cleanup. + * Update the archive handle in the on_exit callback registered by + * on_exit_close_archive(). When pg_restore processes a pg_dumpall archive + * containing multiple databases, each database is restored from a separate + * archive. After closing one archive and opening the next, we update the + * shutdown_info to reference the new archive handle so the cleanup callback + * will close the correct archive on exit. */ void replace_on_exit_close_archive(Archive *AHX) @@ -479,7 +483,7 @@ WaitForTerminatingWorkers(ParallelState *pstate) } #else /* WIN32 */ /* On Windows, we must use WaitForMultipleObjects() */ - HANDLE *lpHandles = pg_malloc(sizeof(HANDLE) * pstate->numWorkers); + HANDLE *lpHandles = pg_malloc_array(HANDLE, pstate->numWorkers); int nrun = 0; DWORD ret; uintptr_t hThread; @@ -564,9 +568,9 @@ sigTermHandler(SIGNAL_ARGS) * signal handler. That could muck up our attempt to send PQcancel, so * disable the signals that set_cancel_handler enabled. */ - pqsignal(SIGINT, SIG_IGN); - pqsignal(SIGTERM, SIG_IGN); - pqsignal(SIGQUIT, SIG_IGN); + pqsignal(SIGINT, PG_SIG_IGN); + pqsignal(SIGTERM, PG_SIG_IGN); + pqsignal(SIGQUIT, PG_SIG_IGN); /* * If we're in the leader, forward signal to all workers. (It seems best @@ -913,7 +917,7 @@ ParallelBackupStart(ArchiveHandle *AH) Assert(AH->public.numWorkers > 0); - pstate = (ParallelState *) pg_malloc(sizeof(ParallelState)); + pstate = pg_malloc_object(ParallelState); pstate->numWorkers = AH->public.numWorkers; pstate->te = NULL; @@ -923,10 +927,10 @@ ParallelBackupStart(ArchiveHandle *AH) return pstate; /* Create status arrays, being sure to initialize all fields to 0 */ - pstate->te = (TocEntry **) - pg_malloc0(pstate->numWorkers * sizeof(TocEntry *)); - pstate->parallelSlot = (ParallelSlot *) - pg_malloc0(pstate->numWorkers * sizeof(ParallelSlot)); + pstate->te = + pg_malloc0_array(TocEntry *, pstate->numWorkers); + pstate->parallelSlot = + pg_malloc0_array(ParallelSlot, pstate->numWorkers); #ifdef WIN32 /* Make fmtId() and fmtQualifiedId() use thread-local storage */ @@ -979,7 +983,7 @@ ParallelBackupStart(ArchiveHandle *AH) #ifdef WIN32 /* Create transient structure to pass args to worker function */ - wi = (WorkerInfo *) pg_malloc(sizeof(WorkerInfo)); + wi = pg_malloc_object(WorkerInfo); wi->AH = AH; wi->slot = slot; @@ -1045,7 +1049,7 @@ ParallelBackupStart(ArchiveHandle *AH) * the workers to inherit this setting, though. */ #ifndef WIN32 - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); #endif /* diff --git a/src/bin/pg_dump/parallel.h b/src/bin/pg_dump/parallel.h index beddfaa6d3192..f7557cd089c0f 100644 --- a/src/bin/pg_dump/parallel.h +++ b/src/bin/pg_dump/parallel.h @@ -4,7 +4,7 @@ * * Parallel support for pg_dump and pg_restore * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index af0007fb6d2f1..fda912ba0a91f 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -68,6 +68,7 @@ enum _dumpPreparedQueries PREPQUERY_DUMPCOMPOSITETYPE, PREPQUERY_DUMPDOMAIN, PREPQUERY_DUMPENUMTYPE, + PREPQUERY_DUMPEXTSTATSOBJSTATS, PREPQUERY_DUMPFUNC, PREPQUERY_DUMPOPR, PREPQUERY_DUMPRANGETYPE, @@ -163,6 +164,8 @@ typedef struct _restoreOptions bool dumpSchema; bool dumpData; bool dumpStatistics; + + char *restrict_key; } RestoreOptions; typedef struct _dumpOptions @@ -213,6 +216,8 @@ typedef struct _dumpOptions bool dumpSchema; bool dumpData; bool dumpStatistics; + + char *restrict_key; } DumpOptions; /* diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index afa42337b110f..fecf6f2d1ce21 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -31,6 +31,8 @@ #endif #include "catalog/pg_class_d.h" +#include "catalog/pg_largeobject_metadata_d.h" +#include "catalog/pg_shdepend_d.h" #include "common/string.h" #include "compress_io.h" #include "dumputils.h" @@ -42,6 +44,7 @@ #include "pg_backup_archiver.h" #include "pg_backup_db.h" #include "pg_backup_utils.h" +#include "pgtar.h" #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n" #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n" @@ -57,7 +60,6 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt, DataDirSyncMethod sync_method); static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te); static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx); -static char *sanitize_line(const char *str, bool want_hyphen); static void _doSetFixedOutputState(ArchiveHandle *AH); static void _doSetSessionAuth(ArchiveHandle *AH, const char *user); static void _reconnectToDB(ArchiveHandle *AH, const char *dbname); @@ -133,7 +135,7 @@ static void StrictNamesCheck(RestoreOptions *ropt); DumpOptions * NewDumpOptions(void) { - DumpOptions *opts = (DumpOptions *) pg_malloc(sizeof(DumpOptions)); + DumpOptions *opts = pg_malloc_object(DumpOptions); InitDumpOptions(opts); return opts; @@ -152,7 +154,7 @@ InitDumpOptions(DumpOptions *opts) opts->dumpSections = DUMP_UNSECTIONED; opts->dumpSchema = true; opts->dumpData = true; - opts->dumpStatistics = true; + opts->dumpStatistics = false; } /* @@ -196,6 +198,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt) dopt->include_everything = ropt->include_everything; dopt->enable_row_security = ropt->enable_row_security; dopt->sequence_data = ropt->sequence_data; + dopt->restrict_key = ropt->restrict_key ? pg_strdup(ropt->restrict_key) : NULL; return dopt; } @@ -465,6 +468,17 @@ RestoreArchive(Archive *AHX, bool append_data) ahprintf(AH, "--\n-- PostgreSQL database dump\n--\n\n"); + /* + * If generating plain-text output, enter restricted mode to block any + * unexpected psql meta-commands. A malicious source might try to inject + * a variety of things via bogus responses to queries. While we cannot + * prevent such sources from affecting the destination at restore time, we + * can block psql meta-commands so that the client machine that runs psql + * with the dump output remains unaffected. + */ + if (ropt->restrict_key) + ahprintf(AH, "\\restrict %s\n\n", ropt->restrict_key); + if (AH->archiveRemoteVersion) ahprintf(AH, "-- Dumped from database version %s\n", AH->archiveRemoteVersion); @@ -753,6 +767,19 @@ RestoreArchive(Archive *AHX, bool append_data) if ((te->reqs & (REQ_SCHEMA | REQ_DATA | REQ_STATS)) == 0) continue; /* ignore if not to be dumped at all */ + /* Skip if no-tablespace is given. */ + if (ropt->noTablespace && te && te->desc && + (strcmp(te->desc, "TABLESPACE") == 0)) + continue; + + /* + * Skip DROP DATABASE/ROLES/TABLESPACE if we didn't specify + * --clean + */ + if (!ropt->dropSchema && te && te->desc && + strcmp(te->desc, "DROP_GLOBAL") == 0) + continue; + switch (_tocEntryRestorePass(te)) { case RESTORE_PASS_MAIN: @@ -805,6 +832,14 @@ RestoreArchive(Archive *AHX, bool append_data) ahprintf(AH, "--\n-- PostgreSQL database dump complete\n--\n\n"); + /* + * If generating plain-text output, exit restricted mode at the very end + * of the script. This is not pro forma; in particular, pg_dumpall + * requires this when transitioning from one database to another. + */ + if (ropt->restrict_key) + ahprintf(AH, "\\unrestrict %s\n\n", ropt->restrict_key); + /* * Clean up & we're done. */ @@ -1091,7 +1126,7 @@ NewRestoreOptions(void) { RestoreOptions *opts; - opts = (RestoreOptions *) pg_malloc0(sizeof(RestoreOptions)); + opts = pg_malloc0_object(RestoreOptions); /* set any fields that shouldn't default to zeroes */ opts->format = archUnknown; @@ -1228,7 +1263,7 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId, ArchiveHandle *AH = (ArchiveHandle *) AHX; TocEntry *newToc; - newToc = (TocEntry *) pg_malloc0(sizeof(TocEntry)); + newToc = pg_malloc0_object(TocEntry); AH->tocCount++; if (dumpId > AH->maxDumpId) @@ -1256,7 +1291,7 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId, if (opts->nDeps > 0) { - newToc->dependencies = (DumpId *) pg_malloc(opts->nDeps * sizeof(DumpId)); + newToc->dependencies = pg_malloc_array(DumpId, opts->nDeps); memcpy(newToc->dependencies, opts->deps, opts->nDeps * sizeof(DumpId)); newToc->nDeps = opts->nDeps; } @@ -1330,8 +1365,8 @@ PrintTOCSummary(Archive *AHX) ahprintf(AH, "; Dump Version: %d.%d-%d\n", ARCHIVE_MAJOR(AH->version), ARCHIVE_MINOR(AH->version), ARCHIVE_REV(AH->version)); ahprintf(AH, "; Format: %s\n", fmtName); - ahprintf(AH, "; Integer: %d bytes\n", (int) AH->intSize); - ahprintf(AH, "; Offset: %d bytes\n", (int) AH->offSize); + ahprintf(AH, "; Integer: %zu bytes\n", AH->intSize); + ahprintf(AH, "; Offset: %zu bytes\n", AH->offSize); if (AH->archiveRemoteVersion) ahprintf(AH, "; Dumped from database version: %s\n", AH->archiveRemoteVersion); @@ -1559,7 +1594,7 @@ SortTocFromFile(Archive *AHX) StringInfoData linebuf; /* Allocate space for the 'wanted' array, and init it */ - ropt->idWanted = (bool *) pg_malloc0(sizeof(bool) * AH->maxDumpId); + ropt->idWanted = pg_malloc0_array(bool, AH->maxDumpId); /* Setup the file */ fh = fopen(ropt->tocFile, PG_BINARY_R); @@ -1675,6 +1710,9 @@ archprintf(Archive *AH, const char *fmt,...) /******************************* * Stuff below here should be 'private' to the archiver routines + * + * If append_data is set, then append data into file as we are restoring dump + * of multiple databases which was taken by pg_dumpall. *******************************/ static void @@ -1868,8 +1906,8 @@ ahwrite(const void *ptr, size_t size, size_t nmemb, ArchiveHandle *AH) { CompressFileHandle *CFH = (CompressFileHandle *) AH->OF; - if (CFH->write_func(ptr, size * nmemb, CFH)) - bytes_written = size * nmemb; + CFH->write_func(ptr, size * nmemb, CFH); + bytes_written = size * nmemb; } if (bytes_written != size * nmemb) @@ -1975,8 +2013,8 @@ buildTocEntryArrays(ArchiveHandle *AH) DumpId maxDumpId = AH->maxDumpId; TocEntry *te; - AH->tocsByDumpId = (TocEntry **) pg_malloc0((maxDumpId + 1) * sizeof(TocEntry *)); - AH->tableDataId = (DumpId *) pg_malloc0((maxDumpId + 1) * sizeof(DumpId)); + AH->tocsByDumpId = pg_malloc0_array(TocEntry *, (maxDumpId + 1)); + AH->tableDataId = pg_malloc0_array(DumpId, (maxDumpId + 1)); for (te = AH->toc->next; te != AH->toc; te = te->next) { @@ -2052,7 +2090,7 @@ WriteOffset(ArchiveHandle *AH, pgoff_t o, int wasSet) } int -ReadOffset(ArchiveHandle *AH, pgoff_t * o) +ReadOffset(ArchiveHandle *AH, pgoff_t *o) { int i; int off; @@ -2292,8 +2330,7 @@ _discoverArchiveFormat(ArchiveHandle *AH) if (ferror(fh)) pg_fatal("could not read input file: %m"); else - pg_fatal("input file is too short (read %lu, expected 5)", - (unsigned long) cnt); + pg_fatal("input file is too short (read %zu, expected 5)", cnt); } /* Save it, just in case we need it later */ @@ -2336,7 +2373,7 @@ _discoverArchiveFormat(ArchiveHandle *AH) } if (!isValidTarHeader(AH->lookahead)) - pg_fatal("input file does not appear to be a valid archive"); + pg_fatal("input file does not appear to be a valid tar archive"); AH->format = archTar; } @@ -2371,13 +2408,13 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt, pg_log_debug("allocating AH for %s, format %d", FileSpec ? FileSpec : "(stdio)", fmt); - AH = (ArchiveHandle *) pg_malloc0(sizeof(ArchiveHandle)); + AH = pg_malloc0_object(ArchiveHandle); AH->version = K_VERS_SELF; /* initialize for backwards compatible string processing */ AH->public.encoding = 0; /* PG_SQL_ASCII */ - AH->public.std_strings = false; + AH->public.std_strings = true; /* sql error handling */ AH->public.exit_on_error = true; @@ -2408,7 +2445,7 @@ _allocAH(const char *FileSpec, const ArchiveFormat fmt, AH->currTablespace = NULL; /* ditto */ AH->currTableAm = NULL; /* ditto */ - AH->toc = (TocEntry *) pg_malloc0(sizeof(TocEntry)); + AH->toc = pg_malloc0_object(TocEntry); AH->toc->next = AH->toc; AH->toc->prev = AH->toc; @@ -2495,7 +2532,7 @@ WriteDataChunks(ArchiveHandle *AH, ParallelState *pstate) TocEntry **tes; int ntes; - tes = (TocEntry **) pg_malloc(AH->tocCount * sizeof(TocEntry *)); + tes = pg_malloc_array(TocEntry *, AH->tocCount); ntes = 0; for (te = AH->toc->next; te != AH->toc; te = te->next) { @@ -2655,7 +2692,7 @@ WriteToc(ArchiveHandle *AH) pg_fatal("unexpected TOC entry in WriteToc(): %d %s %s", te->dumpId, te->desc, te->tag); - if (fseeko(AH->FH, te->defnLen, SEEK_CUR != 0)) + if (fseeko(AH->FH, te->defnLen, SEEK_CUR) != 0) pg_fatal("error during file seek: %m"); } else if (te->defnDumper) @@ -2706,7 +2743,7 @@ ReadToc(ArchiveHandle *AH) for (i = 0; i < AH->tocCount; i++) { - te = (TocEntry *) pg_malloc0(sizeof(TocEntry)); + te = pg_malloc0_object(TocEntry); te->dumpId = ReadInt(AH); if (te->dumpId > AH->maxDumpId) @@ -2803,7 +2840,7 @@ ReadToc(ArchiveHandle *AH) if (AH->version >= K_VERS_1_5) { depSize = 100; - deps = (DumpId *) pg_malloc(sizeof(DumpId) * depSize); + deps = pg_malloc_array(DumpId, depSize); depIdx = 0; for (;;) { @@ -2813,7 +2850,7 @@ ReadToc(ArchiveHandle *AH) if (depIdx >= depSize) { depSize *= 2; - deps = (DumpId *) pg_realloc(deps, sizeof(DumpId) * depSize); + deps = pg_realloc_array(deps, DumpId, depSize); } sscanf(tmp, "%d", &deps[depIdx]); free(tmp); @@ -2822,7 +2859,7 @@ ReadToc(ArchiveHandle *AH) if (depIdx > 0) /* We have a non-null entry */ { - deps = (DumpId *) pg_realloc(deps, sizeof(DumpId) * depIdx); + deps = pg_realloc_array(deps, DumpId, depIdx); te->dependencies = deps; te->nDeps = depIdx; } @@ -2974,13 +3011,24 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) int res = REQ_SCHEMA | REQ_DATA; RestoreOptions *ropt = AH->public.ropt; + /* + * For binary upgrade mode, dump pg_largeobject_metadata and the + * associated pg_shdepend rows. This is faster to restore than the + * equivalent set of large object commands. + */ + if (ropt->binary_upgrade && strcmp(te->desc, "TABLE DATA") == 0 && + (te->catalogId.oid == LargeObjectMetadataRelationId || + te->catalogId.oid == SharedDependRelationId)) + return REQ_DATA; + /* These items are treated specially */ if (strcmp(te->desc, "ENCODING") == 0 || strcmp(te->desc, "STDSTRINGS") == 0 || strcmp(te->desc, "SEARCHPATH") == 0) return REQ_SPECIAL; - if (strcmp(te->desc, "STATISTICS DATA") == 0) + if ((strcmp(te->desc, "STATISTICS DATA") == 0) || + (strcmp(te->desc, "EXTENDED STATISTICS DATA") == 0)) { if (!ropt->dumpStatistics) return 0; @@ -3002,6 +3050,16 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) return 0; } + /* + * Global object TOC entries (e.g., ROLEs or TABLESPACEs) must not be + * ignored. + */ + if (strcmp(te->desc, "ROLE") == 0 || + strcmp(te->desc, "ROLE PROPERTIES") == 0 || + strcmp(te->desc, "TABLESPACE") == 0 || + strcmp(te->desc, "DROP_GLOBAL") == 0) + return REQ_SCHEMA; + /* * Process exclusions that affect certain classes of TOC entries. */ @@ -3020,6 +3078,33 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) strcmp(te->desc, "ROW SECURITY") == 0)) return 0; + /* + * If it's a comment on a policy, a publication, or a subscription, maybe + * ignore it. + */ + if (strcmp(te->desc, "COMMENT") == 0) + { + if (ropt->no_policies && + strncmp(te->tag, "POLICY", strlen("POLICY")) == 0) + return 0; + + if (ropt->no_publications && + strncmp(te->tag, "PUBLICATION", strlen("PUBLICATION")) == 0) + return 0; + + if (ropt->no_subscriptions && + strncmp(te->tag, "SUBSCRIPTION", strlen("SUBSCRIPTION")) == 0) + return 0; + + /* + * Comments on global objects (ROLEs or TABLESPACEs) should not be + * skipped, since global objects themselves are never skipped. + */ + if (strncmp(te->tag, "ROLE", strlen("ROLE")) == 0 || + strncmp(te->tag, "TABLESPACE", strlen("TABLESPACE")) == 0) + return REQ_SCHEMA; + } + /* * If it's a publication or a table part of a publication, maybe ignore * it. @@ -3034,6 +3119,29 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH) if (ropt->no_security_labels && strcmp(te->desc, "SECURITY LABEL") == 0) return 0; + /* + * If it's a security label on a publication or a subscription, maybe + * ignore it. + */ + if (strcmp(te->desc, "SECURITY LABEL") == 0) + { + if (ropt->no_publications && + strncmp(te->tag, "PUBLICATION", strlen("PUBLICATION")) == 0) + return 0; + + if (ropt->no_subscriptions && + strncmp(te->tag, "SUBSCRIPTION", strlen("SUBSCRIPTION")) == 0) + return 0; + + /* + * Security labels on global objects (ROLEs or TABLESPACEs) should not + * be skipped, since global objects themselves are never skipped. + */ + if (strncmp(te->tag, "ROLE", strlen("ROLE")) == 0 || + strncmp(te->tag, "TABLESPACE", strlen("TABLESPACE")) == 0) + return REQ_SCHEMA; + } + /* If it's a subscription, maybe ignore it */ if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0) return 0; @@ -3278,12 +3386,14 @@ _tocEntryRestorePass(TocEntry *te) return RESTORE_PASS_POST_ACL; /* - * Comments need to be emitted in the same pass as their parent objects. - * ACLs haven't got comments, and neither do matview data objects, but - * event triggers do. (Fortunately, event triggers haven't got ACLs, or - * we'd need yet another weird special case.) + * Comments and security labels need to be emitted in the same pass as + * their parent objects. ACLs haven't got comments and security labels, + * and neither do matview data objects, but event triggers do. + * (Fortunately, event triggers haven't got ACLs, or we'd need yet another + * weird special case.) */ - if (strcmp(te->desc, "COMMENT") == 0 && + if ((strcmp(te->desc, "COMMENT") == 0 || + strcmp(te->desc, "SECURITY LABEL") == 0) && strncmp(te->tag, "EVENT TRIGGER ", 14) == 0) return RESTORE_PASS_POST_ACL; @@ -3363,8 +3473,6 @@ _doSetFixedOutputState(ArchiveHandle *AH) /* Avoid annoying notices etc */ ahprintf(AH, "SET client_min_messages = warning;\n"); - if (!AH->public.std_strings) - ahprintf(AH, "SET escape_string_warning = off;\n"); /* Adjust row-security state */ if (ropt && ropt->enable_row_security) @@ -3444,11 +3552,21 @@ _reconnectToDB(ArchiveHandle *AH, const char *dbname) else { PQExpBufferData connectbuf; + RestoreOptions *ropt = AH->public.ropt; + + /* + * We must temporarily exit restricted mode for \connect, etc. + * Anything added between this line and the following \restrict must + * be careful to avoid any possible meta-command injection vectors. + */ + ahprintf(AH, "\\unrestrict %s\n", ropt->restrict_key); initPQExpBuffer(&connectbuf); appendPsqlMetaConnect(&connectbuf, dbname); - ahprintf(AH, "%s\n", connectbuf.data); + ahprintf(AH, "%s", connectbuf.data); termPQExpBuffer(&connectbuf); + + ahprintf(AH, "\\restrict %s\n\n", ropt->restrict_key); } /* @@ -3736,6 +3854,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) strcmp(type, "DOMAIN") == 0 || strcmp(type, "FOREIGN TABLE") == 0 || strcmp(type, "MATERIALIZED VIEW") == 0 || + strcmp(type, "PROPERTY GRAPH") == 0 || strcmp(type, "SEQUENCE") == 0 || strcmp(type, "STATISTICS") == 0 || strcmp(type, "TABLE") == 0 || @@ -3796,6 +3915,9 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te) else if (strcmp(type, "CAST") == 0 || strcmp(type, "CHECK CONSTRAINT") == 0 || strcmp(type, "CONSTRAINT") == 0 || + strcmp(type, "DROP_GLOBAL") == 0 || + strcmp(type, "ROLE PROPERTIES") == 0 || + strcmp(type, "ROLE") == 0 || strcmp(type, "DATABASE PROPERTIES") == 0 || strcmp(type, "DEFAULT") == 0 || strcmp(type, "FK CONSTRAINT") == 0 || @@ -4041,42 +4163,6 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx) } } -/* - * Sanitize a string to be included in an SQL comment or TOC listing, by - * replacing any newlines with spaces. This ensures each logical output line - * is in fact one physical output line, to prevent corruption of the dump - * (which could, in the worst case, present an SQL injection vulnerability - * if someone were to incautiously load a dump containing objects with - * maliciously crafted names). - * - * The result is a freshly malloc'd string. If the input string is NULL, - * return a malloc'ed empty string, unless want_hyphen, in which case return a - * malloc'ed hyphen. - * - * Note that we currently don't bother to quote names, meaning that the name - * fields aren't automatically parseable. "pg_restore -L" doesn't care because - * it only examines the dumpId field, but someday we might want to try harder. - */ -static char * -sanitize_line(const char *str, bool want_hyphen) -{ - char *result; - char *s; - - if (!str) - return pg_strdup(want_hyphen ? "-" : ""); - - result = pg_strdup(str); - - for (s = result; *s != '\0'; s++) - { - if (*s == '\n' || *s == '\r') - *s = ' '; - } - - return result; -} - /* * Write the file header for a custom-format archive */ @@ -4147,8 +4233,7 @@ ReadHead(ArchiveHandle *AH) AH->intSize = AH->ReadBytePtr(AH); if (AH->intSize > 32) - pg_fatal("sanity check on integer size (%lu) failed", - (unsigned long) AH->intSize); + pg_fatal("sanity check on integer size (%zu) failed", AH->intSize); if (AH->intSize > sizeof(int)) pg_log_warning("archive was made on a machine with larger integers, some operations might fail"); @@ -4847,7 +4932,7 @@ fix_dependencies(ArchiveHandle *AH) { if (strcmp(te2->desc, "BLOBS") == 0) { - te->dependencies = (DumpId *) pg_malloc(sizeof(DumpId)); + te->dependencies = pg_malloc_object(DumpId); te->dependencies[0] = te2->dumpId; te->nDeps++; te->depCount++; @@ -4890,7 +4975,7 @@ fix_dependencies(ArchiveHandle *AH) for (te = AH->toc->next; te != AH->toc; te = te->next) { if (te->nRevDeps > 0) - te->revDeps = (DumpId *) pg_malloc(te->nRevDeps * sizeof(DumpId)); + te->revDeps = pg_malloc_array(DumpId, te->nRevDeps); te->nRevDeps = 0; } @@ -5005,7 +5090,7 @@ identify_locking_dependencies(ArchiveHandle *AH, TocEntry *te) * difference between a dependency on a table and a dependency on its * data, so that closer analysis would be needed here. */ - lockids = (DumpId *) pg_malloc(te->nDeps * sizeof(DumpId)); + lockids = pg_malloc_array(DumpId, te->nDeps); nlockids = 0; for (i = 0; i < te->nDeps; i++) { @@ -5023,7 +5108,7 @@ identify_locking_dependencies(ArchiveHandle *AH, TocEntry *te) return; } - te->lockDeps = pg_realloc(lockids, nlockids * sizeof(DumpId)); + te->lockDeps = pg_realloc_array(lockids, DumpId, nlockids); te->nLockDeps = nlockids; } @@ -5113,11 +5198,11 @@ CloneArchive(ArchiveHandle *AH) ArchiveHandle *clone; /* Make a "flat" copy */ - clone = (ArchiveHandle *) pg_malloc(sizeof(ArchiveHandle)); + clone = pg_malloc_object(ArchiveHandle); memcpy(clone, AH, sizeof(ArchiveHandle)); /* Likewise flat-copy the RestoreOptions, so we can alter them locally */ - clone->public.ropt = (RestoreOptions *) pg_malloc(sizeof(RestoreOptions)); + clone->public.ropt = pg_malloc_object(RestoreOptions); memcpy(clone->public.ropt, AH->public.ropt, sizeof(RestoreOptions)); /* Handle format-independent fields */ diff --git a/src/bin/pg_dump/pg_backup_archiver.h b/src/bin/pg_dump/pg_backup_archiver.h index 365073b3eae45..9c3aca6543aa9 100644 --- a/src/bin/pg_dump/pg_backup_archiver.h +++ b/src/bin/pg_dump/pg_backup_archiver.h @@ -465,8 +465,6 @@ extern void InitArchiveFmt_Null(ArchiveHandle *AH); extern void InitArchiveFmt_Directory(ArchiveHandle *AH); extern void InitArchiveFmt_Tar(ArchiveHandle *AH); -extern bool isValidTarHeader(char *header); - extern void ReconnectToServer(ArchiveHandle *AH, const char *dbname); extern void IssueCommandPerBlob(ArchiveHandle *AH, TocEntry *te, const char *cmdBegin, const char *cmdEnd); diff --git a/src/bin/pg_dump/pg_backup_custom.c b/src/bin/pg_dump/pg_backup_custom.c index f7c3af56304ce..529906209400f 100644 --- a/src/bin/pg_dump/pg_backup_custom.c +++ b/src/bin/pg_dump/pg_backup_custom.c @@ -136,7 +136,7 @@ InitArchiveFmt_Custom(ArchiveHandle *AH) AH->WorkerJobRestorePtr = _WorkerJobRestoreCustom; /* Set up a private area. */ - ctx = (lclContext *) pg_malloc0(sizeof(lclContext)); + ctx = pg_malloc0_object(lclContext); AH->formatData = ctx; /* @@ -199,7 +199,7 @@ _ArchiveEntry(ArchiveHandle *AH, TocEntry *te) { lclTocEntry *ctx; - ctx = (lclTocEntry *) pg_malloc0(sizeof(lclTocEntry)); + ctx = pg_malloc0_object(lclTocEntry); if (te->dataDumper) ctx->dataState = K_OFFSET_POS_NOT_SET; else @@ -240,7 +240,7 @@ _ReadExtraToc(ArchiveHandle *AH, TocEntry *te) if (ctx == NULL) { - ctx = (lclTocEntry *) pg_malloc0(sizeof(lclTocEntry)); + ctx = pg_malloc0_object(lclTocEntry); te->formatData = ctx; } @@ -624,12 +624,19 @@ _skipData(ArchiveHandle *AH) lclContext *ctx = (lclContext *) AH->formatData; size_t blkLen; char *buf = NULL; - int buflen = 0; + size_t buflen = 0; blkLen = ReadInt(AH); while (blkLen != 0) { - if (ctx->hasSeek) + /* + * Seeks of less than stdio's buffer size are less efficient than just + * reading the data, at least on common platforms. We don't know the + * buffer size for sure, but 4kB is the usual value. (While pg_dump + * currently tries to avoid producing such short data blocks, older + * dump files often contain them.) + */ + if (ctx->hasSeek && blkLen >= 4 * 1024) { if (fseeko(AH->FH, blkLen, SEEK_CUR) != 0) pg_fatal("error during file seek: %m"); @@ -639,8 +646,8 @@ _skipData(ArchiveHandle *AH) if (blkLen > buflen) { free(buf); - buf = (char *) pg_malloc(blkLen); - buflen = blkLen; + buflen = Max(blkLen, 4 * 1024); + buf = (char *) pg_malloc(buflen); } if (fread(buf, 1, blkLen, AH->FH) != blkLen) { @@ -886,7 +893,7 @@ _Clone(ArchiveHandle *AH) /* * Each thread must have private lclContext working state. */ - AH->formatData = (lclContext *) pg_malloc(sizeof(lclContext)); + AH->formatData = pg_malloc_object(lclContext); memcpy(AH->formatData, ctx, sizeof(lclContext)); ctx = (lclContext *) AH->formatData; diff --git a/src/bin/pg_dump/pg_backup_directory.c b/src/bin/pg_dump/pg_backup_directory.c index 21b00792a8a48..d6a1428c67a63 100644 --- a/src/bin/pg_dump/pg_backup_directory.c +++ b/src/bin/pg_dump/pg_backup_directory.c @@ -19,7 +19,7 @@ * sync. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * Portions Copyright (c) 2000, Philip Warner * @@ -140,7 +140,7 @@ InitArchiveFmt_Directory(ArchiveHandle *AH) AH->WorkerJobDumpPtr = _WorkerJobDumpDirectory; /* Set up our private context */ - ctx = (lclContext *) pg_malloc0(sizeof(lclContext)); + ctx = pg_malloc0_object(lclContext); AH->formatData = ctx; ctx->dataFH = NULL; @@ -200,7 +200,7 @@ _ArchiveEntry(ArchiveHandle *AH, TocEntry *te) lclTocEntry *tctx; char fn[MAXPGPATH]; - tctx = (lclTocEntry *) pg_malloc0(sizeof(lclTocEntry)); + tctx = pg_malloc0_object(lclTocEntry); if (strcmp(te->desc, "BLOBS") == 0) { snprintf(fn, MAXPGPATH, "blobs_%d.toc", te->dumpId); @@ -252,7 +252,7 @@ _ReadExtraToc(ArchiveHandle *AH, TocEntry *te) if (tctx == NULL) { - tctx = (lclTocEntry *) pg_malloc0(sizeof(lclTocEntry)); + tctx = pg_malloc0_object(lclTocEntry); te->formatData = tctx; } @@ -316,15 +316,9 @@ _WriteData(ArchiveHandle *AH, const void *data, size_t dLen) lclContext *ctx = (lclContext *) AH->formatData; CompressFileHandle *CFH = ctx->dataFH; - errno = 0; - if (dLen > 0 && !CFH->write_func(data, dLen, CFH)) - { - /* if write didn't set errno, assume problem is no disk space */ - if (errno == 0) - errno = ENOSPC; - pg_fatal("could not write to output file: %s", - CFH->get_error_func(CFH)); - } + if (dLen <= 0) + return; + CFH->write_func(data, dLen, CFH); } /* @@ -351,7 +345,7 @@ _EndData(ArchiveHandle *AH, TocEntry *te) static void _PrintFileData(ArchiveHandle *AH, char *filename) { - size_t cnt = 0; + size_t cnt; char *buf; size_t buflen; CompressFileHandle *CFH; @@ -366,7 +360,7 @@ _PrintFileData(ArchiveHandle *AH, char *filename) buflen = DEFAULT_IO_BUFFER_SIZE; buf = pg_malloc(buflen); - while (CFH->read_func(buf, buflen, &cnt, CFH) && cnt > 0) + while ((cnt = CFH->read_func(buf, buflen, CFH)) > 0) { ahwrite(buf, 1, cnt, AH); } @@ -412,10 +406,15 @@ _LoadLOs(ArchiveHandle *AH, TocEntry *te) /* * Note: before archive v16, there was always only one BLOBS TOC entry, - * now there can be multiple. We don't need to worry what version we are - * reading though, because tctx->filename should be correct either way. + * now there can be multiple. Furthermore, although the actual filename + * was always "blobs.toc" before v16, the value of tctx->filename did not + * match that before commit 548e50976 fixed it. For simplicity we assume + * it must be "blobs.toc" in all archives before v16. */ - setFilePath(AH, tocfname, tctx->filename); + if (AH->version < K_VERS_1_16) + setFilePath(AH, tocfname, "blobs.toc"); + else + setFilePath(AH, tocfname, tctx->filename); CFH = ctx->LOsTocFH = InitDiscoverCompressFileHandle(tocfname, PG_BINARY_R); @@ -465,16 +464,7 @@ _WriteByte(ArchiveHandle *AH, const int i) lclContext *ctx = (lclContext *) AH->formatData; CompressFileHandle *CFH = ctx->dataFH; - errno = 0; - if (!CFH->write_func(&c, 1, CFH)) - { - /* if write didn't set errno, assume problem is no disk space */ - if (errno == 0) - errno = ENOSPC; - pg_fatal("could not write to output file: %s", - CFH->get_error_func(CFH)); - } - + CFH->write_func(&c, 1, CFH); return 1; } @@ -503,15 +493,7 @@ _WriteBuf(ArchiveHandle *AH, const void *buf, size_t len) lclContext *ctx = (lclContext *) AH->formatData; CompressFileHandle *CFH = ctx->dataFH; - errno = 0; - if (!CFH->write_func(buf, len, CFH)) - { - /* if write didn't set errno, assume problem is no disk space */ - if (errno == 0) - errno = ENOSPC; - pg_fatal("could not write to output file: %s", - CFH->get_error_func(CFH)); - } + CFH->write_func(buf, len, CFH); } /* @@ -526,10 +508,10 @@ _ReadBuf(ArchiveHandle *AH, void *buf, size_t len) CompressFileHandle *CFH = ctx->dataFH; /* - * If there was an I/O error, we already exited in readF(), so here we - * exit on short reads. + * We do not expect a short read, so fail if we get one. The read_func + * already dealt with any outright I/O error. */ - if (!CFH->read_func(buf, len, NULL, CFH)) + if (CFH->read_func(buf, len, CFH) != len) pg_fatal("could not read from input file: end of file"); } @@ -672,14 +654,7 @@ _EndLO(ArchiveHandle *AH, TocEntry *te, Oid oid) /* register the LO in blobs_NNN.toc */ len = snprintf(buf, sizeof(buf), "%u blob_%u.dat\n", oid, oid); - if (!CFH->write_func(buf, len, CFH)) - { - /* if write didn't set errno, assume problem is no disk space */ - if (errno == 0) - errno = ENOSPC; - pg_fatal("could not write to LOs TOC file: %s", - CFH->get_error_func(CFH)); - } + CFH->write_func(buf, len, CFH); } /* @@ -794,7 +769,7 @@ _Clone(ArchiveHandle *AH) { lclContext *ctx = (lclContext *) AH->formatData; - AH->formatData = (lclContext *) pg_malloc(sizeof(lclContext)); + AH->formatData = pg_malloc_object(lclContext); memcpy(AH->formatData, ctx, sizeof(lclContext)); ctx = (lclContext *) AH->formatData; diff --git a/src/bin/pg_dump/pg_backup_tar.c b/src/bin/pg_dump/pg_backup_tar.c index d94d0de2a5d17..a3879410c946a 100644 --- a/src/bin/pg_dump/pg_backup_tar.c +++ b/src/bin/pg_dump/pg_backup_tar.c @@ -984,31 +984,6 @@ tarPrintf(TAR_MEMBER *th, const char *fmt,...) return (int) cnt; } -bool -isValidTarHeader(char *header) -{ - int sum; - int chk = tarChecksum(header); - - sum = read_tar_number(&header[TAR_OFFSET_CHECKSUM], 8); - - if (sum != chk) - return false; - - /* POSIX tar format */ - if (memcmp(&header[TAR_OFFSET_MAGIC], "ustar\0", 6) == 0 && - memcmp(&header[TAR_OFFSET_VERSION], "00", 2) == 0) - return true; - /* GNU tar format */ - if (memcmp(&header[TAR_OFFSET_MAGIC], "ustar \0", 8) == 0) - return true; - /* not-quite-POSIX format written by pre-9.3 pg_dump */ - if (memcmp(&header[TAR_OFFSET_MAGIC], "ustar00\0", 8) == 0) - return true; - - return false; -} - /* Given the member, write the TAR header & copy the file */ static void _tarAddFile(ArchiveHandle *AH, TAR_MEMBER *th) diff --git a/src/bin/pg_dump/pg_backup_utils.c b/src/bin/pg_dump/pg_backup_utils.c index 79aec5f515825..0368f7623a757 100644 --- a/src/bin/pg_dump/pg_backup_utils.c +++ b/src/bin/pg_dump/pg_backup_utils.c @@ -4,7 +4,7 @@ * Utility routines shared by pg_dump and pg_restore * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_dump/pg_backup_utils.c diff --git a/src/bin/pg_dump/pg_backup_utils.h b/src/bin/pg_dump/pg_backup_utils.h index ba042016879d4..9e98ed1161910 100644 --- a/src/bin/pg_dump/pg_backup_utils.h +++ b/src/bin/pg_dump/pg_backup_utils.h @@ -4,7 +4,7 @@ * Utility routines shared by pg_dump and pg_restore. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_dump/pg_backup_utils.h diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 37432e66efd7c..d56dcc701ce8d 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -4,7 +4,7 @@ * pg_dump is a utility for dumping out a postgres database * into a script file. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * pg_dump will read the system catalogs in a database and dump out a @@ -47,10 +47,13 @@ #include "catalog/pg_authid_d.h" #include "catalog/pg_cast_d.h" #include "catalog/pg_class_d.h" +#include "catalog/pg_constraint_d.h" #include "catalog/pg_default_acl_d.h" #include "catalog/pg_largeobject_d.h" +#include "catalog/pg_largeobject_metadata_d.h" #include "catalog/pg_proc_d.h" #include "catalog/pg_publication_d.h" +#include "catalog/pg_shdepend_d.h" #include "catalog/pg_subscription_d.h" #include "catalog/pg_type_d.h" #include "common/connect.h" @@ -68,6 +71,7 @@ #include "pg_backup_db.h" #include "pg_backup_utils.h" #include "pg_dump.h" +#include "statistics/statistics_format.h" #include "storage/block.h" typedef struct @@ -134,6 +138,7 @@ typedef struct int64 cache; /* cache size */ int64 last_value; /* last value of sequence */ bool is_called; /* whether nextval advances before returning */ + bool null_seqtuple; /* did pg_get_sequence_data return nulls? */ } SequenceItem; typedef enum OidOptions @@ -315,6 +320,7 @@ static void dumpSequenceData(Archive *fout, const TableDataInfo *tdinfo); static void dumpIndex(Archive *fout, const IndxInfo *indxinfo); static void dumpIndexAttach(Archive *fout, const IndexAttachInfo *attachinfo); static void dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo); +static void dumpStatisticsExtStats(Archive *fout, const StatsExtInfo *statsextinfo); static void dumpConstraint(Archive *fout, const ConstraintInfo *coninfo); static void dumpTableConstraintComment(Archive *fout, const ConstraintInfo *coninfo); static void dumpTSParser(Archive *fout, const TSParserInfo *prsinfo); @@ -350,7 +356,9 @@ static void buildMatViewRefreshDependencies(Archive *fout); static void getTableDataFKConstraints(void); static void determineNotNullFlags(Archive *fout, PGresult *res, int r, TableInfo *tbinfo, int j, - int i_notnull_name, int i_notnull_invalidoid, + int i_notnull_name, + int i_notnull_comment, + int i_notnull_invalidoid, int i_notnull_noinherit, int i_notnull_islocal, PQExpBuffer *invalidnotnulloids); @@ -438,8 +446,6 @@ main(int argc, char **argv) bool data_only = false; bool schema_only = false; bool statistics_only = false; - bool with_data = false; - bool with_schema = false; bool with_statistics = false; bool no_data = false; bool no_schema = false; @@ -503,6 +509,7 @@ main(int argc, char **argv) {"section", required_argument, NULL, 5}, {"serializable-deferrable", no_argument, &dopt.serializable_deferrable, 1}, {"snapshot", required_argument, NULL, 6}, + {"statistics", no_argument, NULL, 22}, {"statistics-only", no_argument, NULL, 18}, {"strict-names", no_argument, &strict_names, 1}, {"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1}, @@ -517,9 +524,6 @@ main(int argc, char **argv) {"no-toast-compression", no_argument, &dopt.no_toast_compression, 1}, {"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1}, {"no-sync", no_argument, NULL, 7}, - {"with-data", no_argument, NULL, 22}, - {"with-schema", no_argument, NULL, 23}, - {"with-statistics", no_argument, NULL, 24}, {"on-conflict-do-nothing", no_argument, &dopt.do_nothing, 1}, {"rows-per-insert", required_argument, NULL, 10}, {"include-foreign-data", required_argument, NULL, 11}, @@ -530,6 +534,7 @@ main(int argc, char **argv) {"filter", required_argument, NULL, 16}, {"exclude-extension", required_argument, NULL, 17}, {"sequence-data", no_argument, &dopt.sequence_data, 1}, + {"restrict-key", required_argument, NULL, 25}, {NULL, 0, NULL, 0} }; @@ -787,15 +792,11 @@ main(int argc, char **argv) break; case 22: - with_data = true; - break; - - case 23: - with_schema = true; + with_statistics = true; break; - case 24: - with_statistics = true; + case 25: + dopt.restrict_key = pg_strdup(optarg); break; default: @@ -825,53 +826,53 @@ main(int argc, char **argv) if (dopt.column_inserts && dopt.dump_inserts == 0) dopt.dump_inserts = DUMP_DEFAULT_ROWS_PER_INSERT; - /* reject conflicting "-only" options */ - if (data_only && schema_only) - pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together"); - if (schema_only && statistics_only) - pg_fatal("options -s/--schema-only and --statistics-only cannot be used together"); - if (data_only && statistics_only) - pg_fatal("options -a/--data-only and --statistics-only cannot be used together"); - - /* reject conflicting "-only" and "no-" options */ - if (data_only && no_data) - pg_fatal("options -a/--data-only and --no-data cannot be used together"); - if (schema_only && no_schema) - pg_fatal("options -s/--schema-only and --no-schema cannot be used together"); - if (statistics_only && no_statistics) - pg_fatal("options --statistics-only and --no-statistics cannot be used together"); - - /* reject conflicting "with-" and "no-" options */ - if (with_data && no_data) - pg_fatal("options --with-data and --no-data cannot be used together"); - if (with_schema && no_schema) - pg_fatal("options --with-schema and --no-schema cannot be used together"); - if (with_statistics && no_statistics) - pg_fatal("options --with-statistics and --no-statistics cannot be used together"); - - if (schema_only && foreign_servers_include_patterns.head != NULL) - pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together"); + /* *-only options are incompatible with each other */ + check_mut_excl_opts(data_only, "-a/--data-only", + schema_only, "-s/--schema-only", + statistics_only, "--statistics-only"); + + /* --no-* and *-only for same thing are incompatible */ + check_mut_excl_opts(data_only, "-a/--data-only", + no_data, "--no-data"); + check_mut_excl_opts(schema_only, "-s/--schema-only", + no_schema, "--no-schema"); + check_mut_excl_opts(statistics_only, "--statistics-only", + no_statistics, "--no-statistics"); + + /* --statistics and --no-statistics are incompatible */ + check_mut_excl_opts(with_statistics, "--statistics", + no_statistics, "--no-statistics"); + + /* --statistics is incompatible with *-only (except --statistics-only) */ + check_mut_excl_opts(with_statistics, "--statistics", + data_only, "-a/--data-only", + schema_only, "-s/--schema-only"); + + /* --include-foreign-data is incompatible with --schema-only */ + check_mut_excl_opts(foreign_servers_include_patterns.head, "--include-foreign-data", + schema_only, "-s/--schema-only"); if (numWorkers > 1 && foreign_servers_include_patterns.head != NULL) - pg_fatal("option --include-foreign-data is not supported with parallel backup"); + pg_fatal("option %s is not supported with parallel backup", + "--include-foreign-data"); - if (data_only && dopt.outputClean) - pg_fatal("options -c/--clean and -a/--data-only cannot be used together"); + /* --clean is incompatible with --data-only */ + check_mut_excl_opts(dopt.outputClean, "-c/--clean", + data_only, "-a/--data-only"); if (dopt.if_exists && !dopt.outputClean) - pg_fatal("option --if-exists requires option -c/--clean"); + pg_fatal("option %s requires option %s", + "--if-exists", "-c/--clean"); /* - * Set derivative flags. An "-only" option may be overridden by an - * explicit "with-" option; e.g. "--schema-only --with-statistics" will - * include schema and statistics. Other ambiguous or nonsensical - * combinations, e.g. "--schema-only --no-schema", will have already - * caused an error in one of the checks above. + * Set derivative flags. Ambiguous or nonsensical combinations, e.g. + * "--schema-only --no-schema", will have already caused an error in one + * of the checks above. */ dopt.dumpData = ((dopt.dumpData && !schema_only && !statistics_only) || - (data_only || with_data)) && !no_data; + data_only) && !no_data; dopt.dumpSchema = ((dopt.dumpSchema && !data_only && !statistics_only) || - (schema_only || with_schema)) && !no_schema; + schema_only) && !no_schema; dopt.dumpStatistics = ((dopt.dumpStatistics && !schema_only && !data_only) || (statistics_only || with_statistics)) && !no_statistics; @@ -881,15 +882,32 @@ main(int argc, char **argv) * --rows-per-insert were specified. */ if (dopt.do_nothing && dopt.dump_inserts == 0) - pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts"); + pg_fatal("option %s requires option %s, %s, or %s", + "--on-conflict-do-nothing", + "--inserts", "--rows-per-insert", "--column-inserts"); /* Identify archive format to emit */ archiveFormat = parseArchiveFormat(format, &archiveMode); /* archiveFormat specific setup */ if (archiveFormat == archNull) + { plainText = 1; + /* + * If you don't provide a restrict key, one will be appointed for you. + */ + if (!dopt.restrict_key) + dopt.restrict_key = generate_restrict_key(); + if (!dopt.restrict_key) + pg_fatal("could not generate restrict key"); + if (!valid_restrict_key(dopt.restrict_key)) + pg_fatal("invalid restrict key"); + } + else if (dopt.restrict_key) + pg_fatal("option %s can only be used with %s", + "--restrict-key", "--format=plain"); + /* * Custom and directory formats are compressed by default with gzip when * available, not the others. If gzip is not available, no compression is @@ -1083,6 +1101,42 @@ main(int argc, char **argv) if (!dopt.dumpData && dopt.sequence_data) getTableData(&dopt, tblinfo, numTables, RELKIND_SEQUENCE); + /* + * For binary upgrade mode, dump the pg_shdepend rows for large objects + * and maybe even pg_largeobject_metadata (see comment below for details). + * This is faster to restore than the equivalent set of large object + * commands. + */ + if (dopt.binary_upgrade) + { + TableInfo *shdepend; + + shdepend = findTableByOid(SharedDependRelationId); + makeTableDataInfo(&dopt, shdepend); + + /* + * Only dump large object shdepend rows for this database. + */ + shdepend->dataObj->filtercond = "WHERE classid = 'pg_largeobject'::regclass " + "AND dbid = (SELECT oid FROM pg_database " + " WHERE datname = current_database())"; + + /* + * For binary upgrades from v16 and newer versions, we can copy + * pg_largeobject_metadata's files from the old cluster, so we don't + * need to dump its contents. pg_upgrade can't copy/link the files + * from older versions because aclitem (needed by + * pg_largeobject_metadata.lomacl) changed its storage format in v16. + */ + if (fout->remoteVersion < 160000) + { + TableInfo *lo_metadata; + + lo_metadata = findTableByOid(LargeObjectMetadataRelationId); + makeTableDataInfo(&dopt, lo_metadata); + } + } + /* * In binary-upgrade mode, we do not have to worry about the actual LO * data or the associated metadata that resides in the pg_largeobject and @@ -1198,6 +1252,7 @@ main(int argc, char **argv) ropt->enable_row_security = dopt.enable_row_security; ropt->sequence_data = dopt.sequence_data; ropt->binary_upgrade = dopt.binary_upgrade; + ropt->restrict_key = dopt.restrict_key ? pg_strdup(dopt.restrict_key) : NULL; ropt->compression_spec = compression_spec; @@ -1235,7 +1290,7 @@ main(int argc, char **argv) static void help(const char *progname) { - printf(_("%s dumps a database as a text file or to other formats.\n\n"), progname); + printf(_("%s exports a PostgreSQL database as an SQL script or to other formats.\n\n"), progname); printf(_("Usage:\n")); printf(_(" %s [OPTION]... [DBNAME]\n"), progname); @@ -1309,11 +1364,13 @@ help(const char *progname) printf(_(" --no-unlogged-table-data do not dump unlogged table data\n")); printf(_(" --on-conflict-do-nothing add ON CONFLICT DO NOTHING to INSERT commands\n")); printf(_(" --quote-all-identifiers quote all identifiers, even if not key words\n")); + printf(_(" --restrict-key=RESTRICT_KEY use provided string as psql \\restrict key\n")); printf(_(" --rows-per-insert=NROWS number of rows per INSERT; implies --inserts\n")); printf(_(" --section=SECTION dump named section (pre-data, data, or post-data)\n")); printf(_(" --sequence-data include sequence data in dump\n")); printf(_(" --serializable-deferrable wait until the dump can run without anomalies\n")); printf(_(" --snapshot=SNAPSHOT use given snapshot for the dump\n")); + printf(_(" --statistics dump the statistics\n")); printf(_(" --statistics-only dump only the statistics, not schema or data\n")); printf(_(" --strict-names require table and/or schema include patterns to\n" " match at least one entity each\n")); @@ -1322,9 +1379,6 @@ help(const char *progname) printf(_(" --use-set-session-authorization\n" " use SET SESSION AUTHORIZATION commands instead of\n" " ALTER OWNER commands to set ownership\n")); - printf(_(" --with-data dump the data\n")); - printf(_(" --with-schema dump the schema\n")); - printf(_(" --with-statistics dump the statistics\n")); printf(_("\nConnection options:\n")); printf(_(" -d, --dbname=DBNAME database to dump\n")); @@ -1347,7 +1401,6 @@ setup_connection(Archive *AH, const char *dumpencoding, { DumpOptions *dopt = AH->dopt; PGconn *conn = GetConnection(AH); - const char *std_strings; PQclear(ExecuteSqlQueryForSingleRow(AH, ALWAYS_SECURE_SEARCH_PATH_SQL)); @@ -1362,15 +1415,27 @@ setup_connection(Archive *AH, const char *dumpencoding, } /* - * Get the active encoding and the standard_conforming_strings setting, so - * we know how to escape strings. + * Force standard_conforming_strings on, just in case we are dumping from + * an old server that has it disabled. Without this, literals in views, + * expressions, etc, would be incorrect for modern servers. + */ + ExecuteSqlStatement(AH, "SET standard_conforming_strings = on"); + + /* + * And reflect that to AH->std_strings. You might think that we should + * just delete that variable and the code that checks it, but that would + * be problematic for pg_restore, which at least for now should still cope + * with archives containing the other setting (cf. processStdStringsEntry + * in pg_backup_archiver.c). + */ + AH->std_strings = true; + + /* + * Get the active encoding, so we know how to escape strings. */ AH->encoding = PQclientEncoding(conn); setFmtEncoding(AH->encoding); - std_strings = PQparameterStatus(conn, "standard_conforming_strings"); - AH->std_strings = (std_strings && strcmp(std_strings, "on") == 0); - /* * Set the role if requested. In a parallel dump worker, we'll be passed * use_role == NULL, but AH->use_role is already set (if user specified it @@ -1461,7 +1526,7 @@ setup_connection(Archive *AH, const char *dumpencoding, * Initialize prepared-query state to "nothing prepared". We do this here * so that a parallel dump worker will have its own state. */ - AH->is_prepared = (bool *) pg_malloc0(NUM_PREP_QUERIES * sizeof(bool)); + AH->is_prepared = pg_malloc0_array(bool, NUM_PREP_QUERIES); /* * Start transaction-snapshot mode transaction to dump consistent data. @@ -1786,10 +1851,10 @@ expand_table_name_patterns(Archive *fout, "\n LEFT JOIN pg_catalog.pg_namespace n" "\n ON n.oid OPERATOR(pg_catalog.=) c.relnamespace" "\nWHERE c.relkind OPERATOR(pg_catalog.=) ANY" - "\n (array['%c', '%c', '%c', '%c', '%c', '%c'])\n", + "\n (array['%c', '%c', '%c', '%c', '%c', '%c', '%c'])\n", RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW, RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE, - RELKIND_PARTITIONED_TABLE); + RELKIND_PARTITIONED_TABLE, RELKIND_PROPGRAPH); initPQExpBuffer(&dbbuf); processSQLNamePattern(GetConnection(fout), query, cell->val, true, false, "n.nspname", "c.relname", NULL, @@ -2115,7 +2180,7 @@ selectDumpableCast(CastInfo *cast, Archive *fout) * This would be DUMP_COMPONENT_ACL for from-initdb casts, but they do not * support ACLs currently. */ - if (cast->dobj.catId.oid <= (Oid) g_last_builtin_oid) + if (cast->dobj.catId.oid <= g_last_builtin_oid) cast->dobj.dump = DUMP_COMPONENT_NONE; else cast->dobj.dump = fout->dopt->include_everything ? @@ -2147,7 +2212,7 @@ selectDumpableProcLang(ProcLangInfo *plang, Archive *fout) plang->dobj.dump = DUMP_COMPONENT_NONE; else { - if (plang->dobj.catId.oid <= (Oid) g_last_builtin_oid) + if (plang->dobj.catId.oid <= g_last_builtin_oid) plang->dobj.dump = fout->remoteVersion < 90600 ? DUMP_COMPONENT_NONE : DUMP_COMPONENT_ACL; else @@ -2166,6 +2231,13 @@ selectDumpableProcLang(ProcLangInfo *plang, Archive *fout) static void selectDumpableAccessMethod(AccessMethodInfo *method, Archive *fout) { + /* see getAccessMethods() comment about v9.6. */ + if (fout->remoteVersion < 90600) + { + method->dobj.dump = DUMP_COMPONENT_NONE; + return; + } + if (checkExtensionMembership(&method->dobj, fout)) return; /* extension membership overrides all else */ @@ -2173,7 +2245,7 @@ selectDumpableAccessMethod(AccessMethodInfo *method, Archive *fout) * This would be DUMP_COMPONENT_ACL for from-initdb access methods, but * they do not support ACLs currently. */ - if (method->dobj.catId.oid <= (Oid) g_last_builtin_oid) + if (method->dobj.catId.oid <= g_last_builtin_oid) method->dobj.dump = DUMP_COMPONENT_NONE; else method->dobj.dump = fout->dopt->include_everything ? @@ -2199,7 +2271,7 @@ selectDumpableExtension(ExtensionInfo *extinfo, DumpOptions *dopt) * change permissions on their member objects, if they wish to, and have * those changes preserved. */ - if (extinfo->dobj.catId.oid <= (Oid) g_last_builtin_oid) + if (extinfo->dobj.catId.oid <= g_last_builtin_oid) extinfo->dobj.dump = extinfo->dobj.dump_contains = DUMP_COMPONENT_ACL; else { @@ -2291,8 +2363,8 @@ selectDumpableObject(DumpableObject *dobj, Archive *fout) static int dumpTableData_copy(Archive *fout, const void *dcontext) { - TableDataInfo *tdinfo = (TableDataInfo *) dcontext; - TableInfo *tbinfo = tdinfo->tdtable; + const TableDataInfo *tdinfo = dcontext; + const TableInfo *tbinfo = tdinfo->tdtable; const char *classname = tbinfo->dobj.name; PQExpBuffer q = createPQExpBuffer(); @@ -2319,11 +2391,14 @@ dumpTableData_copy(Archive *fout, const void *dcontext) column_list = fmtCopyColumnList(tbinfo, clistBuf); /* - * Use COPY (SELECT ...) TO when dumping a foreign table's data, and when - * a filter condition was specified. For other cases a simple COPY - * suffices. + * Use COPY (SELECT ...) TO when dumping a foreign table's data, when a + * filter condition was specified, and when in binary upgrade mode and + * dumping an old pg_largeobject_metadata defined WITH OIDS. For other + * cases a simple COPY suffices. */ - if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE) + if (tdinfo->filtercond || tbinfo->relkind == RELKIND_FOREIGN_TABLE || + (fout->dopt->binary_upgrade && fout->remoteVersion < 120000 && + tbinfo->dobj.catId.oid == LargeObjectMetadataRelationId)) { /* Temporary allows to access to foreign tables to dump data */ if (tbinfo->relkind == RELKIND_FOREIGN_TABLE) @@ -2459,8 +2534,8 @@ dumpTableData_copy(Archive *fout, const void *dcontext) static int dumpTableData_insert(Archive *fout, const void *dcontext) { - TableDataInfo *tdinfo = (TableDataInfo *) dcontext; - TableInfo *tbinfo = tdinfo->tdtable; + const TableDataInfo *tdinfo = dcontext; + const TableInfo *tbinfo = tdinfo->tdtable; DumpOptions *dopt = fout->dopt; PQExpBuffer q = createPQExpBuffer(); PQExpBuffer insertStmt = NULL; @@ -2482,7 +2557,7 @@ dumpTableData_insert(Archive *fout, const void *dcontext) * actual column value --- but we can save a few cycles by fetching nulls * rather than the uninteresting-to-us value. */ - attgenerated = (char *) pg_malloc(tbinfo->numatts * sizeof(char)); + attgenerated = pg_malloc_array(char, tbinfo->numatts); appendPQExpBufferStr(q, "DECLARE _pg_dump_cursor CURSOR FOR SELECT "); nfields = 0; for (i = 0; i < tbinfo->numatts; i++) @@ -2530,7 +2605,7 @@ dumpTableData_insert(Archive *fout, const void *dcontext) */ if (insertStmt == NULL) { - TableInfo *targettab; + const TableInfo *targettab; insertStmt = createPQExpBuffer(); @@ -2782,7 +2857,7 @@ static void dumpTableData(Archive *fout, const TableDataInfo *tdinfo) { DumpOptions *dopt = fout->dopt; - TableInfo *tbinfo = tdinfo->tdtable; + const TableInfo *tbinfo = tdinfo->tdtable; PQExpBuffer copyBuf = createPQExpBuffer(); PQExpBuffer clistBuf = createPQExpBuffer(); DataDumperPtr dumpFn; @@ -2803,12 +2878,15 @@ dumpTableData(Archive *fout, const TableDataInfo *tdinfo) (dopt->load_via_partition_root || forcePartitionRootLoad(tbinfo))) { - TableInfo *parentTbinfo; + const TableInfo *parentTbinfo; + char *sanitized; parentTbinfo = getRootTableInfo(tbinfo); copyFrom = fmtQualifiedDumpable(parentTbinfo); + sanitized = sanitize_line(copyFrom, true); printfPQExpBuffer(copyBuf, "-- load via partition root %s", - copyFrom); + sanitized); + free(sanitized); tdDefn = pg_strdup(copyBuf->data); } else @@ -2956,6 +3034,9 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo) if (tbinfo->dataObj != NULL) return; + /* Skip property graphs (no data to dump) */ + if (tbinfo->relkind == RELKIND_PROPGRAPH) + return; /* Skip VIEWs (no data to dump) */ if (tbinfo->relkind == RELKIND_VIEW) return; @@ -2980,7 +3061,7 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo) return; /* OK, let's dump it */ - tdinfo = (TableDataInfo *) pg_malloc(sizeof(TableDataInfo)); + tdinfo = pg_malloc_object(TableDataInfo); if (tbinfo->relkind == RELKIND_MATVIEW) tdinfo->dobj.objType = DO_REFRESH_MATVIEW; @@ -3569,26 +3650,32 @@ dumpDatabase(Archive *fout) /* * pg_largeobject comes from the old system intact, so set its * relfrozenxids, relminmxids and relfilenode. + * + * pg_largeobject_metadata also comes from the old system intact for + * upgrades from v16 and newer, so set its relfrozenxids, relminmxids, and + * relfilenode, too. pg_upgrade can't copy/link the files from older + * versions because aclitem (needed by pg_largeobject_metadata.lomacl) + * changed its storage format in v16. */ if (dopt->binary_upgrade) { PGresult *lo_res; PQExpBuffer loFrozenQry = createPQExpBuffer(); PQExpBuffer loOutQry = createPQExpBuffer(); + PQExpBuffer lomOutQry = createPQExpBuffer(); PQExpBuffer loHorizonQry = createPQExpBuffer(); + PQExpBuffer lomHorizonQry = createPQExpBuffer(); int ii_relfrozenxid, ii_relfilenode, ii_oid, ii_relminmxid; - /* - * pg_largeobject - */ if (fout->remoteVersion >= 90300) appendPQExpBuffer(loFrozenQry, "SELECT relfrozenxid, relminmxid, relfilenode, oid\n" "FROM pg_catalog.pg_class\n" - "WHERE oid IN (%u, %u);\n", - LargeObjectRelationId, LargeObjectLOidPNIndexId); + "WHERE oid IN (%u, %u, %u, %u);\n", + LargeObjectRelationId, LargeObjectLOidPNIndexId, + LargeObjectMetadataRelationId, LargeObjectMetadataOidIndexId); else appendPQExpBuffer(loFrozenQry, "SELECT relfrozenxid, 0 AS relminmxid, relfilenode, oid\n" "FROM pg_catalog.pg_class\n" @@ -3603,35 +3690,57 @@ dumpDatabase(Archive *fout) ii_oid = PQfnumber(lo_res, "oid"); appendPQExpBufferStr(loHorizonQry, "\n-- For binary upgrade, set pg_largeobject relfrozenxid and relminmxid\n"); + appendPQExpBufferStr(lomHorizonQry, "\n-- For binary upgrade, set pg_largeobject_metadata relfrozenxid and relminmxid\n"); appendPQExpBufferStr(loOutQry, "\n-- For binary upgrade, preserve pg_largeobject and index relfilenodes\n"); + appendPQExpBufferStr(lomOutQry, "\n-- For binary upgrade, preserve pg_largeobject_metadata and index relfilenodes\n"); for (int i = 0; i < PQntuples(lo_res); ++i) { Oid oid; RelFileNumber relfilenumber; + PQExpBuffer horizonQry; + PQExpBuffer outQry; + + oid = atooid(PQgetvalue(lo_res, i, ii_oid)); + relfilenumber = atooid(PQgetvalue(lo_res, i, ii_relfilenode)); - appendPQExpBuffer(loHorizonQry, "UPDATE pg_catalog.pg_class\n" + if (oid == LargeObjectRelationId || + oid == LargeObjectLOidPNIndexId) + { + horizonQry = loHorizonQry; + outQry = loOutQry; + } + else + { + horizonQry = lomHorizonQry; + outQry = lomOutQry; + } + + appendPQExpBuffer(horizonQry, "UPDATE pg_catalog.pg_class\n" "SET relfrozenxid = '%u', relminmxid = '%u'\n" "WHERE oid = %u;\n", atooid(PQgetvalue(lo_res, i, ii_relfrozenxid)), atooid(PQgetvalue(lo_res, i, ii_relminmxid)), atooid(PQgetvalue(lo_res, i, ii_oid))); - oid = atooid(PQgetvalue(lo_res, i, ii_oid)); - relfilenumber = atooid(PQgetvalue(lo_res, i, ii_relfilenode)); - - if (oid == LargeObjectRelationId) - appendPQExpBuffer(loOutQry, + if (oid == LargeObjectRelationId || + oid == LargeObjectMetadataRelationId) + appendPQExpBuffer(outQry, "SELECT pg_catalog.binary_upgrade_set_next_heap_relfilenode('%u'::pg_catalog.oid);\n", relfilenumber); - else if (oid == LargeObjectLOidPNIndexId) - appendPQExpBuffer(loOutQry, + else if (oid == LargeObjectLOidPNIndexId || + oid == LargeObjectMetadataOidIndexId) + appendPQExpBuffer(outQry, "SELECT pg_catalog.binary_upgrade_set_next_index_relfilenode('%u'::pg_catalog.oid);\n", relfilenumber); } appendPQExpBufferStr(loOutQry, "TRUNCATE pg_catalog.pg_largeobject;\n"); + appendPQExpBufferStr(lomOutQry, + "TRUNCATE pg_catalog.pg_largeobject_metadata;\n"); + appendPQExpBufferStr(loOutQry, loHorizonQry->data); + appendPQExpBufferStr(lomOutQry, lomHorizonQry->data); ArchiveEntry(fout, nilCatalogId, createDumpId(), ARCHIVE_OPTS(.tag = "pg_largeobject", @@ -3639,11 +3748,20 @@ dumpDatabase(Archive *fout) .section = SECTION_PRE_DATA, .createStmt = loOutQry->data)); + if (fout->remoteVersion >= 160000) + ArchiveEntry(fout, nilCatalogId, createDumpId(), + ARCHIVE_OPTS(.tag = "pg_largeobject_metadata", + .description = "pg_largeobject_metadata", + .section = SECTION_PRE_DATA, + .createStmt = lomOutQry->data)); + PQclear(lo_res); destroyPQExpBuffer(loFrozenQry); destroyPQExpBuffer(loHorizonQry); + destroyPQExpBuffer(lomHorizonQry); destroyPQExpBuffer(loOutQry); + destroyPQExpBuffer(lomOutQry); } PQclear(res); @@ -3837,7 +3955,24 @@ getLOs(Archive *fout) appendPQExpBufferStr(loQry, "SELECT oid, lomowner, lomacl, " "acldefault('L', lomowner) AS acldefault " - "FROM pg_largeobject_metadata " + "FROM pg_largeobject_metadata "); + + /* + * For binary upgrades, we transfer pg_largeobject_metadata via COPY or by + * copying/linking its files from the old cluster. On such upgrades, we + * only need to consider large objects that have comments or security + * labels, since we still restore those objects via COMMENT/SECURITY LABEL + * commands. + */ + if (dopt->binary_upgrade) + appendPQExpBufferStr(loQry, + "WHERE oid IN " + "(SELECT objoid FROM pg_description " + "WHERE classoid = " CppAsString2(LargeObjectRelationId) " " + "UNION SELECT objoid FROM pg_seclabel " + "WHERE classoid = " CppAsString2(LargeObjectRelationId) ") "); + + appendPQExpBufferStr(loQry, "ORDER BY lomowner, lomacl::pg_catalog.text, oid"); res = ExecuteSqlQuery(fout, loQry->data, PGRES_TUPLES_OK); @@ -3918,27 +4053,26 @@ getLOs(Archive *fout) loinfo->dobj.components |= DUMP_COMPONENT_ACL; /* - * In binary-upgrade mode for LOs, we do *not* dump out the LO data, - * as it will be copied by pg_upgrade, which simply copies the - * pg_largeobject table. We *do* however dump out anything but the - * data, as pg_upgrade copies just pg_largeobject, but not - * pg_largeobject_metadata, after the dump is restored. + * In binary upgrade mode, pg_largeobject and pg_largeobject_metadata + * are transferred via COPY or by copying/linking the files from the + * old cluster. Thus, we do not need to dump LO data, definitions, or + * ACLs. */ if (dopt->binary_upgrade) - loinfo->dobj.dump &= ~DUMP_COMPONENT_DATA; + loinfo->dobj.dump &= ~(DUMP_COMPONENT_DATA | DUMP_COMPONENT_ACL | DUMP_COMPONENT_DEFINITION); /* * Create a "BLOBS" data item for the group, too. This is just a * placeholder for sorting; it carries no data now. */ - lodata = (DumpableObject *) pg_malloc(sizeof(DumpableObject)); + lodata = pg_malloc_object(DumpableObject); lodata->objType = DO_LARGE_OBJECT_DATA; lodata->catId = nilCatalogId; AssignDumpId(lodata); lodata->name = pg_strdup(namebuf); lodata->components |= DUMP_COMPONENT_DATA; /* Set up explicit dependency from data to metadata */ - lodata->dependencies = (DumpId *) pg_malloc(sizeof(DumpId)); + lodata->dependencies = pg_malloc_object(DumpId); lodata->dependencies[0] = loinfo->dobj.dumpId; lodata->nDeps = lodata->allocDeps = 1; } @@ -4154,7 +4288,7 @@ getPolicies(Archive *fout, TableInfo tblinfo[], int numTables) * Note: use tableoid 0 so that this object won't be mistaken for * something that pg_depend entries apply to. */ - polinfo = pg_malloc(sizeof(PolicyInfo)); + polinfo = pg_malloc_object(PolicyInfo); polinfo->dobj.objType = DO_POLICY; polinfo->dobj.catId.tableoid = 0; polinfo->dobj.catId.oid = tbinfo->dobj.catId.oid; @@ -4210,7 +4344,7 @@ getPolicies(Archive *fout, TableInfo tblinfo[], int numTables) i_polqual = PQfnumber(res, "polqual"); i_polwithcheck = PQfnumber(res, "polwithcheck"); - polinfo = pg_malloc(ntups * sizeof(PolicyInfo)); + polinfo = pg_malloc_array(PolicyInfo, ntups); for (j = 0; j < ntups; j++) { @@ -4390,6 +4524,7 @@ getPublications(Archive *fout) int i_pubname; int i_pubowner; int i_puballtables; + int i_puballsequences; int i_pubinsert; int i_pubupdate; int i_pubdelete; @@ -4420,9 +4555,14 @@ getPublications(Archive *fout) appendPQExpBufferStr(query, "false AS pubviaroot, "); if (fout->remoteVersion >= 180000) - appendPQExpBufferStr(query, "p.pubgencols "); + appendPQExpBufferStr(query, "p.pubgencols, "); + else + appendPQExpBuffer(query, "'%c' AS pubgencols, ", PUBLISH_GENCOLS_NONE); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, "p.puballsequences "); else - appendPQExpBuffer(query, "'%c' AS pubgencols ", PUBLISH_GENCOLS_NONE); + appendPQExpBufferStr(query, "false AS puballsequences "); appendPQExpBufferStr(query, "FROM pg_publication p"); @@ -4438,6 +4578,7 @@ getPublications(Archive *fout) i_pubname = PQfnumber(res, "pubname"); i_pubowner = PQfnumber(res, "pubowner"); i_puballtables = PQfnumber(res, "puballtables"); + i_puballsequences = PQfnumber(res, "puballsequences"); i_pubinsert = PQfnumber(res, "pubinsert"); i_pubupdate = PQfnumber(res, "pubupdate"); i_pubdelete = PQfnumber(res, "pubdelete"); @@ -4445,7 +4586,7 @@ getPublications(Archive *fout) i_pubviaroot = PQfnumber(res, "pubviaroot"); i_pubgencols = PQfnumber(res, "pubgencols"); - pubinfo = pg_malloc(ntups * sizeof(PublicationInfo)); + pubinfo = pg_malloc_array(PublicationInfo, ntups); for (i = 0; i < ntups; i++) { @@ -4458,6 +4599,8 @@ getPublications(Archive *fout) pubinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_pubowner)); pubinfo[i].puballtables = (strcmp(PQgetvalue(res, i, i_puballtables), "t") == 0); + pubinfo[i].puballsequences = + (strcmp(PQgetvalue(res, i, i_puballsequences), "t") == 0); pubinfo[i].pubinsert = (strcmp(PQgetvalue(res, i, i_pubinsert), "t") == 0); pubinfo[i].pubupdate = @@ -4470,9 +4613,59 @@ getPublications(Archive *fout) (strcmp(PQgetvalue(res, i, i_pubviaroot), "t") == 0); pubinfo[i].pubgencols_type = *(PQgetvalue(res, i, i_pubgencols)); + pubinfo[i].except_tables = (SimplePtrList) + { + NULL, NULL + }; /* Decide whether we want to dump it */ selectDumpableObject(&(pubinfo[i].dobj), fout); + + /* + * Get the list of tables for publications specified in the EXCEPT + * TABLE clause. + * + * Although individual table entries in EXCEPT list could be stored in + * PublicationRelInfo, dumpPublicationTable cannot be used to emit + * them, because there is no ALTER PUBLICATION ... ADD command to add + * individual table entries to the EXCEPT list. + * + * Therefore, the approach is to dump the complete EXCEPT list in a + * single CREATE PUBLICATION statement. PublicationInfo is used to + * collect this information, which is then emitted by + * dumpPublication(). + */ + if (fout->remoteVersion >= 190000) + { + int ntbls; + PGresult *res_tbls; + + resetPQExpBuffer(query); + appendPQExpBuffer(query, + "SELECT prrelid\n" + "FROM pg_catalog.pg_publication_rel\n" + "WHERE prpubid = %u AND prexcept", + pubinfo[i].dobj.catId.oid); + + res_tbls = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntbls = PQntuples(res_tbls); + + for (int j = 0; j < ntbls; j++) + { + Oid prrelid; + TableInfo *tbinfo; + + prrelid = atooid(PQgetvalue(res_tbls, j, 0)); + + tbinfo = findTableByOid(prrelid); + + if (tbinfo != NULL) + simple_ptr_list_append(&pubinfo[i].except_tables, tbinfo); + } + + PQclear(res_tbls); + } } cleanup: @@ -4510,8 +4703,31 @@ dumpPublication(Archive *fout, const PublicationInfo *pubinfo) qpubname); if (pubinfo->puballtables) + { + int n_except = 0; + appendPQExpBufferStr(query, " FOR ALL TABLES"); + /* Include EXCEPT (TABLE) clause if there are except_tables. */ + for (SimplePtrListCell *cell = pubinfo->except_tables.head; cell; cell = cell->next) + { + TableInfo *tbinfo = (TableInfo *) cell->ptr; + + if (++n_except == 1) + appendPQExpBufferStr(query, " EXCEPT ("); + else + appendPQExpBufferStr(query, ", "); + appendPQExpBuffer(query, "TABLE ONLY %s", fmtQualifiedDumpable(tbinfo)); + } + if (n_except > 0) + appendPQExpBufferChar(query, ')'); + + if (pubinfo->puballsequences) + appendPQExpBufferStr(query, ", ALL SEQUENCES"); + } + else if (pubinfo->puballsequences) + appendPQExpBufferStr(query, " FOR ALL SEQUENCES"); + appendPQExpBufferStr(query, " WITH (publish = '"); if (pubinfo->pubinsert) { @@ -4618,7 +4834,7 @@ getPublicationNamespaces(Archive *fout) i_pnnspid = PQfnumber(res, "pnnspid"); /* this allocation may be more than we need */ - pubsinfo = pg_malloc(ntups * sizeof(PublicationSchemaInfo)); + pubsinfo = pg_malloc_array(PublicationSchemaInfo, ntups); j = 0; for (i = 0; i < ntups; i++) @@ -4688,6 +4904,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) /* Collect all publication membership info. */ if (fout->remoteVersion >= 150000) + { appendPQExpBufferStr(query, "SELECT tableoid, oid, prpubid, prrelid, " "pg_catalog.pg_get_expr(prqual, prrelid) AS prrelqual, " @@ -4700,6 +4917,9 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) " WHERE attrelid = pr.prrelid AND attnum = prattrs[s])\n" " ELSE NULL END) prattrs " "FROM pg_catalog.pg_publication_rel pr"); + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, " WHERE NOT pr.prexcept"); + } else appendPQExpBufferStr(query, "SELECT tableoid, oid, prpubid, prrelid, " @@ -4717,7 +4937,7 @@ getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables) i_prattrs = PQfnumber(res, "prattrs"); /* this allocation may be more than we need */ - pubrinfo = pg_malloc(ntups * sizeof(PublicationRelInfo)); + pubrinfo = pg_malloc_array(PublicationRelInfo, ntups); j = 0; for (i = 0; i < ntups; i++) @@ -4952,14 +5172,18 @@ getSubscriptions(Archive *fout) int i_subdisableonerr; int i_subpasswordrequired; int i_subrunasowner; + int i_subservername; int i_subconninfo; int i_subslotname; int i_subsynccommit; + int i_subwalrcvtimeout; int i_subpublications; int i_suborigin; int i_suboriginremotelsn; int i_subenabled; int i_subfailover; + int i_subretaindeadtuples; + int i_submaxretention; int i, ntups; @@ -5032,14 +5256,44 @@ getSubscriptions(Archive *fout) if (fout->remoteVersion >= 170000) appendPQExpBufferStr(query, - " s.subfailover\n"); + " s.subfailover,\n"); + else + appendPQExpBufferStr(query, + " false AS subfailover,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + " s.subretaindeadtuples,\n"); + else + appendPQExpBufferStr(query, + " false AS subretaindeadtuples,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + " s.submaxretention,\n"); + else + appendPQExpBufferStr(query, " 0 AS submaxretention,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + " s.subwalrcvtimeout,\n"); else appendPQExpBufferStr(query, - " false AS subfailover\n"); + " '-1' AS subwalrcvtimeout,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, " fs.srvname AS subservername\n"); + else + appendPQExpBufferStr(query, " NULL AS subservername\n"); appendPQExpBufferStr(query, "FROM pg_subscription s\n"); + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + "LEFT JOIN pg_catalog.pg_foreign_server fs \n" + " ON fs.oid = s.subserver \n"); + if (dopt->binary_upgrade && fout->remoteVersion >= 170000) appendPQExpBufferStr(query, "LEFT JOIN pg_catalog.pg_replication_origin_status o \n" @@ -5069,14 +5323,18 @@ getSubscriptions(Archive *fout) i_subpasswordrequired = PQfnumber(res, "subpasswordrequired"); i_subrunasowner = PQfnumber(res, "subrunasowner"); i_subfailover = PQfnumber(res, "subfailover"); + i_subretaindeadtuples = PQfnumber(res, "subretaindeadtuples"); + i_submaxretention = PQfnumber(res, "submaxretention"); + i_subservername = PQfnumber(res, "subservername"); i_subconninfo = PQfnumber(res, "subconninfo"); i_subslotname = PQfnumber(res, "subslotname"); i_subsynccommit = PQfnumber(res, "subsynccommit"); + i_subwalrcvtimeout = PQfnumber(res, "subwalrcvtimeout"); i_subpublications = PQfnumber(res, "subpublications"); i_suborigin = PQfnumber(res, "suborigin"); i_suboriginremotelsn = PQfnumber(res, "suboriginremotelsn"); - subinfo = pg_malloc(ntups * sizeof(SubscriptionInfo)); + subinfo = pg_malloc_array(SubscriptionInfo, ntups); for (i = 0; i < ntups; i++) { @@ -5090,6 +5348,10 @@ getSubscriptions(Archive *fout) subinfo[i].subenabled = (strcmp(PQgetvalue(res, i, i_subenabled), "t") == 0); + if (PQgetisnull(res, i, i_subservername)) + subinfo[i].subservername = NULL; + else + subinfo[i].subservername = pg_strdup(PQgetvalue(res, i, i_subservername)); subinfo[i].subbinary = (strcmp(PQgetvalue(res, i, i_subbinary), "t") == 0); subinfo[i].substream = *(PQgetvalue(res, i, i_substream)); @@ -5102,8 +5364,15 @@ getSubscriptions(Archive *fout) (strcmp(PQgetvalue(res, i, i_subrunasowner), "t") == 0); subinfo[i].subfailover = (strcmp(PQgetvalue(res, i, i_subfailover), "t") == 0); - subinfo[i].subconninfo = - pg_strdup(PQgetvalue(res, i, i_subconninfo)); + subinfo[i].subretaindeadtuples = + (strcmp(PQgetvalue(res, i, i_subretaindeadtuples), "t") == 0); + subinfo[i].submaxretention = + atoi(PQgetvalue(res, i, i_submaxretention)); + if (PQgetisnull(res, i, i_subconninfo)) + subinfo[i].subconninfo = NULL; + else + subinfo[i].subconninfo = + pg_strdup(PQgetvalue(res, i, i_subconninfo)); if (PQgetisnull(res, i, i_subslotname)) subinfo[i].subslotname = NULL; else @@ -5111,6 +5380,8 @@ getSubscriptions(Archive *fout) pg_strdup(PQgetvalue(res, i, i_subslotname)); subinfo[i].subsynccommit = pg_strdup(PQgetvalue(res, i, i_subsynccommit)); + subinfo[i].subwalrcvtimeout = + pg_strdup(PQgetvalue(res, i, i_subwalrcvtimeout)); subinfo[i].subpublications = pg_strdup(PQgetvalue(res, i, i_subpublications)); subinfo[i].suborigin = pg_strdup(PQgetvalue(res, i, i_suborigin)); @@ -5129,12 +5400,12 @@ getSubscriptions(Archive *fout) } /* - * getSubscriptionTables - * Get information about subscription membership for dumpable tables. This + * getSubscriptionRelations + * Get information about subscription membership for dumpable relations. This * will be used only in binary-upgrade mode for PG17 or later versions. */ void -getSubscriptionTables(Archive *fout) +getSubscriptionRelations(Archive *fout) { DumpOptions *dopt = fout->dopt; SubscriptionInfo *subinfo = NULL; @@ -5166,7 +5437,7 @@ getSubscriptionTables(Archive *fout) i_srsubstate = PQfnumber(res, "srsubstate"); i_srsublsn = PQfnumber(res, "srsublsn"); - subrinfo = pg_malloc(ntups * sizeof(SubRelInfo)); + subrinfo = pg_malloc_array(SubRelInfo, ntups); for (int i = 0; i < ntups; i++) { Oid cur_srsubid = atooid(PQgetvalue(res, i, i_srsubid)); @@ -5188,7 +5459,7 @@ getSubscriptionTables(Archive *fout) tblinfo = findTableByOid(relid); if (tblinfo == NULL) - pg_fatal("failed sanity check, table with OID %u not found", + pg_fatal("failed sanity check, relation with OID %u not found", relid); /* OK, make a DumpableObject for this relationship */ @@ -5196,7 +5467,9 @@ getSubscriptionTables(Archive *fout) subrinfo[i].dobj.catId.tableoid = relid; subrinfo[i].dobj.catId.oid = cur_srsubid; AssignDumpId(&subrinfo[i].dobj); - subrinfo[i].dobj.name = pg_strdup(subinfo->dobj.name); + subrinfo[i].dobj.namespace = tblinfo->dobj.namespace; + subrinfo[i].dobj.name = tblinfo->dobj.name; + subrinfo[i].subinfo = subinfo; subrinfo[i].tblinfo = tblinfo; subrinfo[i].srsubstate = PQgetvalue(res, i, i_srsubstate)[0]; if (PQgetisnull(res, i, i_srsublsn)) @@ -5204,8 +5477,6 @@ getSubscriptionTables(Archive *fout) else subrinfo[i].srsublsn = pg_strdup(PQgetvalue(res, i, i_srsublsn)); - subrinfo[i].subinfo = subinfo; - /* Decide whether we want to dump it */ selectDumpableObject(&(subrinfo[i].dobj), fout); } @@ -5233,7 +5504,7 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo) Assert(fout->dopt->binary_upgrade && fout->remoteVersion >= 170000); - tag = psprintf("%s %s", subinfo->dobj.name, subrinfo->dobj.name); + tag = psprintf("%s %s", subinfo->dobj.name, subrinfo->tblinfo->dobj.name); query = createPQExpBuffer(); @@ -5248,7 +5519,7 @@ dumpSubscriptionTable(Archive *fout, const SubRelInfo *subrinfo) "\n-- For binary upgrade, must preserve the subscriber table.\n"); appendPQExpBufferStr(query, "SELECT pg_catalog.binary_upgrade_add_sub_rel_state("); - appendStringLiteralAH(query, subrinfo->dobj.name, fout); + appendStringLiteralAH(query, subinfo->dobj.name, fout); appendPQExpBuffer(query, ", %u, '%c'", subrinfo->tblinfo->dobj.catId.oid, @@ -5312,9 +5583,17 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) appendPQExpBuffer(delq, "DROP SUBSCRIPTION %s;\n", qsubname); - appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s CONNECTION ", + appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s ", qsubname); - appendStringLiteralAH(query, subinfo->subconninfo, fout); + if (subinfo->subservername) + { + appendPQExpBuffer(query, "SERVER %s", fmtId(subinfo->subservername)); + } + else + { + appendPQExpBufferStr(query, "CONNECTION "); + appendStringLiteralAH(query, subinfo->subconninfo, fout); + } /* Build list of quoted publications and append them to query. */ if (!parsePGArray(subinfo->subpublications, &pubnames, &npubnames)) @@ -5360,9 +5639,18 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) if (subinfo->subfailover) appendPQExpBufferStr(query, ", failover = true"); + if (subinfo->subretaindeadtuples) + appendPQExpBufferStr(query, ", retain_dead_tuples = true"); + + if (subinfo->submaxretention) + appendPQExpBuffer(query, ", max_retention_duration = %d", subinfo->submaxretention); + if (strcmp(subinfo->subsynccommit, "off") != 0) appendPQExpBuffer(query, ", synchronous_commit = %s", fmtId(subinfo->subsynccommit)); + if (strcmp(subinfo->subwalrcvtimeout, "-1") != 0) + appendPQExpBuffer(query, ", wal_receiver_timeout = %s", fmtId(subinfo->subwalrcvtimeout)); + if (pg_strcasecmp(subinfo->suborigin, LOGICALREP_ORIGIN_ANY) != 0) appendPQExpBuffer(query, ", origin = %s", subinfo->suborigin); @@ -5640,8 +5928,8 @@ collectBinaryUpgradeClassOids(Archive *fout) res = ExecuteSqlQuery(fout, query, PGRES_TUPLES_OK); nbinaryUpgradeClassOids = PQntuples(res); - binaryUpgradeClassOids = (BinaryUpgradeClassOidItem *) - pg_malloc(nbinaryUpgradeClassOids * sizeof(BinaryUpgradeClassOidItem)); + binaryUpgradeClassOids = + pg_malloc_array(BinaryUpgradeClassOidItem, nbinaryUpgradeClassOids); for (int i = 0; i < nbinaryUpgradeClassOids; i++) { @@ -5822,7 +6110,7 @@ getNamespaces(Archive *fout) ntups = PQntuples(res); - nsinfo = (NamespaceInfo *) pg_malloc(ntups * sizeof(NamespaceInfo)); + nsinfo = pg_malloc_array(NamespaceInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -5954,7 +6242,7 @@ getExtensions(Archive *fout, int *numExtensions) if (ntups == 0) goto cleanup; - extinfo = (ExtensionInfo *) pg_malloc(ntups * sizeof(ExtensionInfo)); + extinfo = pg_malloc_array(ExtensionInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6053,7 +6341,7 @@ getTypes(Archive *fout) ntups = PQntuples(res); - tyinfo = (TypeInfo *) pg_malloc(ntups * sizeof(TypeInfo)); + tyinfo = pg_malloc_array(TypeInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6120,6 +6408,7 @@ getTypes(Archive *fout) */ tyinfo[i].nDomChecks = 0; tyinfo[i].domChecks = NULL; + tyinfo[i].notnull = NULL; if ((tyinfo[i].dobj.dump & DUMP_COMPONENT_DEFINITION) && tyinfo[i].typtype == TYPTYPE_DOMAIN) getDomainConstraints(fout, &(tyinfo[i])); @@ -6138,7 +6427,7 @@ getTypes(Archive *fout) (tyinfo[i].typtype == TYPTYPE_BASE || tyinfo[i].typtype == TYPTYPE_RANGE)) { - stinfo = (ShellTypeInfo *) pg_malloc(sizeof(ShellTypeInfo)); + stinfo = pg_malloc_object(ShellTypeInfo); stinfo->dobj.objType = DO_SHELL_TYPE; stinfo->dobj.catId = nilCatalogId; AssignDumpId(&stinfo->dobj); @@ -6179,6 +6468,8 @@ getOperators(Archive *fout) int i_oprnamespace; int i_oprowner; int i_oprkind; + int i_oprleft; + int i_oprright; int i_oprcode; /* @@ -6190,6 +6481,8 @@ getOperators(Archive *fout) "oprnamespace, " "oprowner, " "oprkind, " + "oprleft, " + "oprright, " "oprcode::oid AS oprcode " "FROM pg_operator"); @@ -6197,7 +6490,7 @@ getOperators(Archive *fout) ntups = PQntuples(res); - oprinfo = (OprInfo *) pg_malloc(ntups * sizeof(OprInfo)); + oprinfo = pg_malloc_array(OprInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6205,6 +6498,8 @@ getOperators(Archive *fout) i_oprnamespace = PQfnumber(res, "oprnamespace"); i_oprowner = PQfnumber(res, "oprowner"); i_oprkind = PQfnumber(res, "oprkind"); + i_oprleft = PQfnumber(res, "oprleft"); + i_oprright = PQfnumber(res, "oprright"); i_oprcode = PQfnumber(res, "oprcode"); for (i = 0; i < ntups; i++) @@ -6218,6 +6513,8 @@ getOperators(Archive *fout) findNamespace(atooid(PQgetvalue(res, i, i_oprnamespace))); oprinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_oprowner)); oprinfo[i].oprkind = (PQgetvalue(res, i, i_oprkind))[0]; + oprinfo[i].oprleft = atooid(PQgetvalue(res, i, i_oprleft)); + oprinfo[i].oprright = atooid(PQgetvalue(res, i, i_oprright)); oprinfo[i].oprcode = atooid(PQgetvalue(res, i, i_oprcode)); /* Decide whether we want to dump it */ @@ -6246,6 +6543,7 @@ getCollations(Archive *fout) int i_collname; int i_collnamespace; int i_collowner; + int i_collencoding; query = createPQExpBuffer(); @@ -6256,20 +6554,22 @@ getCollations(Archive *fout) appendPQExpBufferStr(query, "SELECT tableoid, oid, collname, " "collnamespace, " - "collowner " + "collowner, " + "collencoding " "FROM pg_collation"); res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); ntups = PQntuples(res); - collinfo = (CollInfo *) pg_malloc(ntups * sizeof(CollInfo)); + collinfo = pg_malloc_array(CollInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); i_collname = PQfnumber(res, "collname"); i_collnamespace = PQfnumber(res, "collnamespace"); i_collowner = PQfnumber(res, "collowner"); + i_collencoding = PQfnumber(res, "collencoding"); for (i = 0; i < ntups; i++) { @@ -6281,6 +6581,7 @@ getCollations(Archive *fout) collinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_collnamespace))); collinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_collowner)); + collinfo[i].collencoding = atoi(PQgetvalue(res, i, i_collencoding)); /* Decide whether we want to dump it */ selectDumpableObject(&(collinfo[i].dobj), fout); @@ -6325,7 +6626,7 @@ getConversions(Archive *fout) ntups = PQntuples(res); - convinfo = (ConvInfo *) pg_malloc(ntups * sizeof(ConvInfo)); + convinfo = pg_malloc_array(ConvInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6371,22 +6672,34 @@ getAccessMethods(Archive *fout) int i_amhandler; int i_amtype; - /* Before 9.6, there are no user-defined access methods */ - if (fout->remoteVersion < 90600) - return; - query = createPQExpBuffer(); - /* Select all access methods from pg_am table */ - appendPQExpBufferStr(query, "SELECT tableoid, oid, amname, amtype, " - "amhandler::pg_catalog.regproc AS amhandler " - "FROM pg_am"); + /* + * Select all access methods from pg_am table. v9.6 introduced CREATE + * ACCESS METHOD, so earlier versions usually have only built-in access + * methods. v9.6 also changed the access method API, replacing dozens of + * pg_am columns with amhandler. Even if a user created an access method + * by "INSERT INTO pg_am", we have no way to translate pre-v9.6 pg_am + * columns to a v9.6+ CREATE ACCESS METHOD. Hence, before v9.6, read + * pg_am just to facilitate findAccessMethodByOid() providing the + * OID-to-name mapping. + */ + appendPQExpBufferStr(query, "SELECT tableoid, oid, amname, "); + if (fout->remoteVersion >= 90600) + appendPQExpBufferStr(query, + "amtype, " + "amhandler::pg_catalog.regproc AS amhandler "); + else + appendPQExpBufferStr(query, + "'i'::pg_catalog.\"char\" AS amtype, " + "'-'::pg_catalog.regproc AS amhandler "); + appendPQExpBufferStr(query, "FROM pg_am"); res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); ntups = PQntuples(res); - aminfo = (AccessMethodInfo *) pg_malloc(ntups * sizeof(AccessMethodInfo)); + aminfo = pg_malloc_array(AccessMethodInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6429,6 +6742,7 @@ getOpclasses(Archive *fout) OpclassInfo *opcinfo; int i_tableoid; int i_oid; + int i_opcmethod; int i_opcname; int i_opcnamespace; int i_opcowner; @@ -6438,7 +6752,7 @@ getOpclasses(Archive *fout) * system-defined opclasses at dump-out time. */ - appendPQExpBufferStr(query, "SELECT tableoid, oid, opcname, " + appendPQExpBufferStr(query, "SELECT tableoid, oid, opcmethod, opcname, " "opcnamespace, " "opcowner " "FROM pg_opclass"); @@ -6447,10 +6761,11 @@ getOpclasses(Archive *fout) ntups = PQntuples(res); - opcinfo = (OpclassInfo *) pg_malloc(ntups * sizeof(OpclassInfo)); + opcinfo = pg_malloc_array(OpclassInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); + i_opcmethod = PQfnumber(res, "opcmethod"); i_opcname = PQfnumber(res, "opcname"); i_opcnamespace = PQfnumber(res, "opcnamespace"); i_opcowner = PQfnumber(res, "opcowner"); @@ -6464,6 +6779,7 @@ getOpclasses(Archive *fout) opcinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_opcname)); opcinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_opcnamespace))); + opcinfo[i].opcmethod = atooid(PQgetvalue(res, i, i_opcmethod)); opcinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_opcowner)); /* Decide whether we want to dump it */ @@ -6489,6 +6805,7 @@ getOpfamilies(Archive *fout) OpfamilyInfo *opfinfo; int i_tableoid; int i_oid; + int i_opfmethod; int i_opfname; int i_opfnamespace; int i_opfowner; @@ -6500,7 +6817,7 @@ getOpfamilies(Archive *fout) * system-defined opfamilies at dump-out time. */ - appendPQExpBufferStr(query, "SELECT tableoid, oid, opfname, " + appendPQExpBufferStr(query, "SELECT tableoid, oid, opfmethod, opfname, " "opfnamespace, " "opfowner " "FROM pg_opfamily"); @@ -6509,11 +6826,12 @@ getOpfamilies(Archive *fout) ntups = PQntuples(res); - opfinfo = (OpfamilyInfo *) pg_malloc(ntups * sizeof(OpfamilyInfo)); + opfinfo = pg_malloc_array(OpfamilyInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); i_opfname = PQfnumber(res, "opfname"); + i_opfmethod = PQfnumber(res, "opfmethod"); i_opfnamespace = PQfnumber(res, "opfnamespace"); i_opfowner = PQfnumber(res, "opfowner"); @@ -6526,6 +6844,7 @@ getOpfamilies(Archive *fout) opfinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_opfname)); opfinfo[i].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_opfnamespace))); + opfinfo[i].opfmethod = atooid(PQgetvalue(res, i, i_opfmethod)); opfinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_opfowner)); /* Decide whether we want to dump it */ @@ -6625,7 +6944,7 @@ getAggregates(Archive *fout) ntups = PQntuples(res); - agginfo = (AggInfo *) pg_malloc(ntups * sizeof(AggInfo)); + agginfo = pg_malloc_array(AggInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6658,7 +6977,7 @@ getAggregates(Archive *fout) agginfo[i].aggfn.argtypes = NULL; else { - agginfo[i].aggfn.argtypes = (Oid *) pg_malloc(agginfo[i].aggfn.nargs * sizeof(Oid)); + agginfo[i].aggfn.argtypes = pg_malloc_array(Oid, agginfo[i].aggfn.nargs); parseOidArray(PQgetvalue(res, i, i_proargtypes), agginfo[i].aggfn.argtypes, agginfo[i].aggfn.nargs); @@ -6816,7 +7135,7 @@ getFuncs(Archive *fout) ntups = PQntuples(res); - finfo = (FuncInfo *) pg_malloc0(ntups * sizeof(FuncInfo)); + finfo = pg_malloc0_array(FuncInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -6851,7 +7170,7 @@ getFuncs(Archive *fout) finfo[i].argtypes = NULL; else { - finfo[i].argtypes = (Oid *) pg_malloc(finfo[i].nargs * sizeof(Oid)); + finfo[i].argtypes = pg_malloc_array(Oid, finfo[i].nargs); parseOidArray(PQgetvalue(res, i, i_proargtypes), finfo[i].argtypes, finfo[i].nargs); } @@ -6890,22 +7209,24 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages, (relkind == RELKIND_PARTITIONED_TABLE) || (relkind == RELKIND_INDEX) || (relkind == RELKIND_PARTITIONED_INDEX) || - (relkind == RELKIND_MATVIEW)) + (relkind == RELKIND_MATVIEW || + relkind == RELKIND_FOREIGN_TABLE)) { - RelStatsInfo *info = pg_malloc0(sizeof(RelStatsInfo)); + RelStatsInfo *info = pg_malloc0_object(RelStatsInfo); DumpableObject *dobj = &info->dobj; dobj->objType = DO_REL_STATS; dobj->catId.tableoid = 0; dobj->catId.oid = 0; AssignDumpId(dobj); - dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId)); + dobj->dependencies = pg_malloc_object(DumpId); dobj->dependencies[0] = rel->dumpId; dobj->nDeps = 1; dobj->allocDeps = 1; dobj->components |= DUMP_COMPONENT_STATISTICS; dobj->name = pg_strdup(rel->name); dobj->namespace = rel->namespace; + info->relid = rel->catId.oid; info->relpages = relpages; info->reltuples = pstrdup(reltuples); info->relallvisible = relallvisible; @@ -6929,6 +7250,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages, case RELKIND_RELATION: case RELKIND_PARTITIONED_TABLE: case RELKIND_MATVIEW: + case RELKIND_FOREIGN_TABLE: info->section = SECTION_DATA; break; case RELKIND_INDEX: @@ -6936,7 +7258,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages, info->section = SECTION_POST_DATA; break; default: - pg_fatal("cannot dump statistics for relation kind '%c'", + pg_fatal("cannot dump statistics for relation kind \"%c\"", info->relkind); } @@ -7165,7 +7487,8 @@ getTables(Archive *fout, int *numTables) CppAsString2(RELKIND_COMPOSITE_TYPE) ", " CppAsString2(RELKIND_MATVIEW) ", " CppAsString2(RELKIND_FOREIGN_TABLE) ", " - CppAsString2(RELKIND_PARTITIONED_TABLE) ")\n" + CppAsString2(RELKIND_PARTITIONED_TABLE) ", " + CppAsString2(RELKIND_PROPGRAPH) ")\n" "ORDER BY c.oid"); res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); @@ -7183,7 +7506,7 @@ getTables(Archive *fout, int *numTables) * only one, because we don't yet know which tables might be inheritance * ancestors of the target table. */ - tblinfo = (TableInfo *) pg_malloc0(ntups * sizeof(TableInfo)); + tblinfo = pg_malloc0_array(TableInfo, ntups); i_reltableoid = PQfnumber(res, "tableoid"); i_reloid = PQfnumber(res, "oid"); @@ -7515,7 +7838,7 @@ getInherits(Archive *fout, int *numInherits) *numInherits = ntups; - inhinfo = (InhInfo *) pg_malloc(ntups * sizeof(InhInfo)); + inhinfo = pg_malloc_array(InhInfo, ntups); i_inhrelid = PQfnumber(res, "inhrelid"); i_inhparent = PQfnumber(res, "inhparent"); @@ -7827,7 +8150,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) i_indstatcols = PQfnumber(res, "indstatcols"); i_indstatvals = PQfnumber(res, "indstatvals"); - indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo)); + indxinfo = pg_malloc_array(IndxInfo, ntups); /* * Outer loop iterates once per table, not once per row. Incrementing of @@ -7893,7 +8216,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) indxinfo[j].indreloptions = pg_strdup(PQgetvalue(res, j, i_indreloptions)); indxinfo[j].indstatcols = pg_strdup(PQgetvalue(res, j, i_indstatcols)); indxinfo[j].indstatvals = pg_strdup(PQgetvalue(res, j, i_indstatvals)); - indxinfo[j].indkeys = (Oid *) pg_malloc(indxinfo[j].indnattrs * sizeof(Oid)); + indxinfo[j].indkeys = pg_malloc_array(Oid, indxinfo[j].indnattrs); parseOidArray(PQgetvalue(res, j, i_indkey), indxinfo[j].indkeys, indxinfo[j].indnattrs); indxinfo[j].indisclustered = (PQgetvalue(res, j, i_indisclustered)[0] == 't'); @@ -7931,7 +8254,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) */ ConstraintInfo *constrinfo; - constrinfo = (ConstraintInfo *) pg_malloc(sizeof(ConstraintInfo)); + constrinfo = pg_malloc_object(ConstraintInfo); constrinfo->dobj.objType = DO_CONSTRAINT; constrinfo->dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_contableoid)); constrinfo->dobj.catId.oid = atooid(PQgetvalue(res, j, i_conoid)); @@ -8022,7 +8345,7 @@ getExtendedStatistics(Archive *fout) i_stxrelid = PQfnumber(res, "stxrelid"); i_stattarget = PQfnumber(res, "stxstattarget"); - statsextinfo = (StatsExtInfo *) pg_malloc(ntups * sizeof(StatsExtInfo)); + statsextinfo = pg_malloc_array(StatsExtInfo, ntups); for (i = 0; i < ntups; i++) { @@ -8043,6 +8366,9 @@ getExtendedStatistics(Archive *fout) /* Decide whether we want to dump it */ selectDumpableStatisticsObject(&(statsextinfo[i]), fout); + + if (fout->dopt->dumpStatistics) + statsextinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS; } PQclear(res); @@ -8130,7 +8456,7 @@ getConstraints(Archive *fout, TableInfo tblinfo[], int numTables) i_conindid = PQfnumber(res, "conindid"); i_condef = PQfnumber(res, "condef"); - constrinfo = (ConstraintInfo *) pg_malloc(ntups * sizeof(ConstraintInfo)); + constrinfo = pg_malloc_array(ConstraintInfo, ntups); curtblindx = -1; for (int j = 0; j < ntups; j++) @@ -8243,27 +8569,33 @@ addConstrChildIdxDeps(DumpableObject *dobj, const IndxInfo *refidx) static void getDomainConstraints(Archive *fout, TypeInfo *tyinfo) { - int i; ConstraintInfo *constrinfo; PQExpBuffer query = createPQExpBuffer(); PGresult *res; int i_tableoid, i_oid, i_conname, - i_consrc; + i_consrc, + i_convalidated, + i_contype; int ntups; if (!fout->is_prepared[PREPQUERY_GETDOMAINCONSTRAINTS]) { - /* Set up query for constraint-specific details */ - appendPQExpBufferStr(query, - "PREPARE getDomainConstraints(pg_catalog.oid) AS\n" - "SELECT tableoid, oid, conname, " - "pg_catalog.pg_get_constraintdef(oid) AS consrc, " - "convalidated " - "FROM pg_catalog.pg_constraint " - "WHERE contypid = $1 AND contype = 'c' " - "ORDER BY conname"); + /* + * Set up query for constraint-specific details. For servers 17 and + * up, domains have constraints of type 'n' as well as 'c', otherwise + * just the latter. + */ + appendPQExpBuffer(query, + "PREPARE getDomainConstraints(pg_catalog.oid) AS\n" + "SELECT tableoid, oid, conname, " + "pg_catalog.pg_get_constraintdef(oid) AS consrc, " + "convalidated, contype " + "FROM pg_catalog.pg_constraint " + "WHERE contypid = $1 AND contype IN (%s) " + "ORDER BY conname", + fout->remoteVersion < 170000 ? "'c'" : "'c', 'n'"); ExecuteSqlStatement(fout, query->data); @@ -8282,33 +8614,50 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo) i_oid = PQfnumber(res, "oid"); i_conname = PQfnumber(res, "conname"); i_consrc = PQfnumber(res, "consrc"); + i_convalidated = PQfnumber(res, "convalidated"); + i_contype = PQfnumber(res, "contype"); - constrinfo = (ConstraintInfo *) pg_malloc(ntups * sizeof(ConstraintInfo)); - - tyinfo->nDomChecks = ntups; + constrinfo = pg_malloc_array(ConstraintInfo, ntups); tyinfo->domChecks = constrinfo; - for (i = 0; i < ntups; i++) + /* 'i' tracks result rows; 'j' counts CHECK constraints */ + for (int i = 0, j = 0; i < ntups; i++) { - bool validated = PQgetvalue(res, i, 4)[0] == 't'; - - constrinfo[i].dobj.objType = DO_CONSTRAINT; - constrinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid)); - constrinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); - AssignDumpId(&constrinfo[i].dobj); - constrinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_conname)); - constrinfo[i].dobj.namespace = tyinfo->dobj.namespace; - constrinfo[i].contable = NULL; - constrinfo[i].condomain = tyinfo; - constrinfo[i].contype = 'c'; - constrinfo[i].condef = pg_strdup(PQgetvalue(res, i, i_consrc)); - constrinfo[i].confrelid = InvalidOid; - constrinfo[i].conindex = 0; - constrinfo[i].condeferrable = false; - constrinfo[i].condeferred = false; - constrinfo[i].conislocal = true; - - constrinfo[i].separate = !validated; + bool validated = PQgetvalue(res, i, i_convalidated)[0] == 't'; + char contype = (PQgetvalue(res, i, i_contype))[0]; + ConstraintInfo *constraint; + + if (contype == CONSTRAINT_CHECK) + { + constraint = &constrinfo[j++]; + tyinfo->nDomChecks++; + } + else + { + Assert(contype == CONSTRAINT_NOTNULL); + Assert(tyinfo->notnull == NULL); + /* use last item in array for the not-null constraint */ + tyinfo->notnull = &(constrinfo[ntups - 1]); + constraint = tyinfo->notnull; + } + + constraint->dobj.objType = DO_CONSTRAINT; + constraint->dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid)); + constraint->dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid)); + AssignDumpId(&(constraint->dobj)); + constraint->dobj.name = pg_strdup(PQgetvalue(res, i, i_conname)); + constraint->dobj.namespace = tyinfo->dobj.namespace; + constraint->contable = NULL; + constraint->condomain = tyinfo; + constraint->contype = contype; + constraint->condef = pg_strdup(PQgetvalue(res, i, i_consrc)); + constraint->confrelid = InvalidOid; + constraint->conindex = 0; + constraint->condeferrable = false; + constraint->condeferred = false; + constraint->conislocal = true; + + constraint->separate = !validated; /* * Make the domain depend on the constraint, ensuring it won't be @@ -8317,8 +8666,7 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo) * anyway, so this doesn't matter. */ if (validated) - addObjectDependency(&tyinfo->dobj, - constrinfo[i].dobj.dumpId); + addObjectDependency(&tyinfo->dobj, constraint->dobj.dumpId); } PQclear(res); @@ -8357,7 +8705,7 @@ getRules(Archive *fout) ntups = PQntuples(res); - ruleinfo = (RuleInfo *) pg_malloc(ntups * sizeof(RuleInfo)); + ruleinfo = pg_malloc_array(RuleInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -8563,7 +8911,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables) i_tgispartition = PQfnumber(res, "tgispartition"); i_tgdef = PQfnumber(res, "tgdef"); - tginfo = (TriggerInfo *) pg_malloc(ntups * sizeof(TriggerInfo)); + tginfo = pg_malloc_array(TriggerInfo, ntups); /* * Outer loop iterates once per table, not once per row. Incrementing of @@ -8660,7 +9008,7 @@ getEventTriggers(Archive *fout) ntups = PQntuples(res); - evtinfo = (EventTriggerInfo *) pg_malloc(ntups * sizeof(EventTriggerInfo)); + evtinfo = pg_malloc_array(EventTriggerInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -8734,7 +9082,7 @@ getProcLangs(Archive *fout) ntups = PQntuples(res); - planginfo = (ProcLangInfo *) pg_malloc(ntups * sizeof(ProcLangInfo)); + planginfo = pg_malloc_array(ProcLangInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -8826,7 +9174,7 @@ getCasts(Archive *fout) ntups = PQntuples(res); - castinfo = (CastInfo *) pg_malloc(ntups * sizeof(CastInfo)); + castinfo = pg_malloc_array(CastInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -8925,7 +9273,7 @@ getTransforms(Archive *fout) ntups = PQntuples(res); - transforminfo = (TransformInfo *) pg_malloc(ntups * sizeof(TransformInfo)); + transforminfo = pg_malloc_array(TransformInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -9004,6 +9352,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) int i_attalign; int i_attislocal; int i_notnull_name; + int i_notnull_comment; int i_notnull_noinherit; int i_notnull_islocal; int i_notnull_invalidoid; @@ -9034,8 +9383,18 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) if (tbinfo->relkind == RELKIND_SEQUENCE) continue; - /* Don't bother with uninteresting tables, either */ - if (!tbinfo->interesting) + /* + * Don't bother with uninteresting tables, either. For binary + * upgrades, this is bypassed for pg_largeobject_metadata and + * pg_shdepend so that the columns names are collected for the + * corresponding COPY commands. Restoring the data for those catalogs + * is faster than restoring the equivalent set of large object + * commands. + */ + if (!tbinfo->interesting && + !(fout->dopt->binary_upgrade && + (tbinfo->dobj.catId.oid == LargeObjectMetadataRelationId || + tbinfo->dobj.catId.oid == SharedDependRelationId))) continue; /* OK, we need info for this table */ @@ -9087,7 +9446,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) /* * Find out any NOT NULL markings for each column. In 18 and up we read - * pg_constraint to obtain the constraint name. notnull_noinherit is set + * pg_constraint to obtain the constraint name, and for valid constraints + * also pg_description to obtain its comment. notnull_noinherit is set * according to the NO INHERIT property. For versions prior to 18, we * store an empty string as the name when a constraint is marked as * attnotnull (this cues dumpTableSchema to print the NOT NULL clause @@ -9095,16 +9455,18 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) * * For invalid constraints, we need to store their OIDs for processing * elsewhere, so we bring the pg_constraint.oid value when the constraint - * is invalid, and NULL otherwise. + * is invalid, and NULL otherwise. Their comments are handled not here + * but by collectComments, because they're their own dumpable object. * * We track in notnull_islocal whether the constraint was defined directly * in this table or via an ancestor, for binary upgrade. flagInhAttrs - * might modify this later; that routine is also in charge of determining - * the correct inhcount. + * might modify this later. */ if (fout->remoteVersion >= 180000) appendPQExpBufferStr(q, "co.conname AS notnull_name,\n" + "CASE WHEN co.convalidated THEN pt.description" + " ELSE NULL END AS notnull_comment,\n" "CASE WHEN NOT co.convalidated THEN co.oid " "ELSE NULL END AS notnull_invalidoid,\n" "co.connoinherit AS notnull_noinherit,\n" @@ -9112,9 +9474,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) else appendPQExpBufferStr(q, "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n" + "NULL AS notnull_comment,\n" "NULL AS notnull_invalidoid,\n" "false AS notnull_noinherit,\n" - "a.attislocal AS notnull_islocal,\n"); + "CASE WHEN a.attislocal THEN true\n" + " WHEN a.attnotnull AND NOT a.attislocal THEN true\n" + " ELSE false\n" + "END AS notnull_islocal,\n"); if (fout->remoteVersion >= 140000) appendPQExpBufferStr(q, @@ -9155,18 +9521,30 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) /* * In versions 18 and up, we need pg_constraint for explicit NOT NULL - * entries. Also, we need to know if the NOT NULL for each column is - * backing a primary key. + * entries and pg_description to get their comments. */ if (fout->remoteVersion >= 180000) appendPQExpBufferStr(q, " LEFT JOIN pg_catalog.pg_constraint co ON " "(a.attrelid = co.conrelid\n" " AND co.contype = 'n' AND " - "co.conkey = array[a.attnum])\n"); + "co.conkey = array[a.attnum])\n" + " LEFT JOIN pg_catalog.pg_description pt ON " + "(pt.classoid = co.tableoid AND pt.objoid = co.oid)\n"); + + appendPQExpBufferStr(q, + "WHERE a.attnum > 0::pg_catalog.int2\n"); + + /* + * For binary upgrades from dopt->binary_upgrade && fout->remoteVersion < 120000) + appendPQExpBufferStr(q, + "OR (a.attnum = -2::pg_catalog.int2 AND src.tbloid = " + CppAsString2(LargeObjectMetadataRelationId) ")\n"); appendPQExpBufferStr(q, - "WHERE a.attnum > 0::pg_catalog.int2\n" "ORDER BY a.attrelid, a.attnum"); res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); @@ -9187,6 +9565,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) i_attalign = PQfnumber(res, "attalign"); i_attislocal = PQfnumber(res, "attislocal"); i_notnull_name = PQfnumber(res, "notnull_name"); + i_notnull_comment = PQfnumber(res, "notnull_comment"); i_notnull_invalidoid = PQfnumber(res, "notnull_invalidoid"); i_notnull_noinherit = PQfnumber(res, "notnull_noinherit"); i_notnull_islocal = PQfnumber(res, "notnull_islocal"); @@ -9232,38 +9611,44 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) pg_fatal("unrecognized table OID %u", attrelid); /* cross-check that we only got requested tables */ if (tbinfo->relkind == RELKIND_SEQUENCE || - !tbinfo->interesting) + (!tbinfo->interesting && + !(fout->dopt->binary_upgrade && + (tbinfo->dobj.catId.oid == LargeObjectMetadataRelationId || + tbinfo->dobj.catId.oid == SharedDependRelationId)))) pg_fatal("unexpected column data for table \"%s\"", tbinfo->dobj.name); /* Save data for this table */ tbinfo->numatts = numatts; - tbinfo->attnames = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->atttypnames = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->attstattarget = (int *) pg_malloc(numatts * sizeof(int)); - tbinfo->attstorage = (char *) pg_malloc(numatts * sizeof(char)); - tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char)); - tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char)); - tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char)); - tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int)); - tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char)); - tbinfo->attislocal = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->attoptions = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->attcollation = (Oid *) pg_malloc(numatts * sizeof(Oid)); - tbinfo->attcompression = (char *) pg_malloc(numatts * sizeof(char)); - tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *)); - tbinfo->notnull_invalid = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->notnull_islocal = (bool *) pg_malloc(numatts * sizeof(bool)); - tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *)); + tbinfo->attnames = pg_malloc_array(char *, numatts); + tbinfo->atttypnames = pg_malloc_array(char *, numatts); + tbinfo->attstattarget = pg_malloc_array(int, numatts); + tbinfo->attstorage = pg_malloc_array(char, numatts); + tbinfo->typstorage = pg_malloc_array(char, numatts); + tbinfo->attidentity = pg_malloc_array(char, numatts); + tbinfo->attgenerated = pg_malloc_array(char, numatts); + tbinfo->attisdropped = pg_malloc_array(bool, numatts); + tbinfo->attlen = pg_malloc_array(int, numatts); + tbinfo->attalign = pg_malloc_array(char, numatts); + tbinfo->attislocal = pg_malloc_array(bool, numatts); + tbinfo->attoptions = pg_malloc_array(char *, numatts); + tbinfo->attcollation = pg_malloc_array(Oid, numatts); + tbinfo->attcompression = pg_malloc_array(char, numatts); + tbinfo->attfdwoptions = pg_malloc_array(char *, numatts); + tbinfo->attmissingval = pg_malloc_array(char *, numatts); + tbinfo->notnull_constrs = pg_malloc_array(char *, numatts); + tbinfo->notnull_comment = pg_malloc_array(char *, numatts); + tbinfo->notnull_invalid = pg_malloc_array(bool, numatts); + tbinfo->notnull_noinh = pg_malloc_array(bool, numatts); + tbinfo->notnull_islocal = pg_malloc_array(bool, numatts); + tbinfo->attrdefs = pg_malloc_array(AttrDefInfo *, numatts); hasdefaults = false; for (int j = 0; j < numatts; j++, r++) { - if (j + 1 != atoi(PQgetvalue(res, r, i_attnum))) + if (j + 1 != atoi(PQgetvalue(res, r, i_attnum)) && + !(fout->dopt->binary_upgrade && fout->remoteVersion < 120000 && + tbinfo->dobj.catId.oid == LargeObjectMetadataRelationId)) pg_fatal("invalid column numbering in table \"%s\"", tbinfo->dobj.name); tbinfo->attnames[j] = pg_strdup(PQgetvalue(res, r, i_attname)); @@ -9286,11 +9671,14 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) determineNotNullFlags(fout, res, r, tbinfo, j, i_notnull_name, + i_notnull_comment, i_notnull_invalidoid, i_notnull_noinherit, i_notnull_islocal, &invalidnotnulloids); + tbinfo->notnull_comment[j] = PQgetisnull(res, r, i_notnull_comment) ? + NULL : pg_strdup(PQgetvalue(res, r, i_notnull_comment)); tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions)); tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation)); tbinfo->attcompression[j] = *(PQgetvalue(res, r, i_attcompression)); @@ -9340,7 +9728,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); numDefaults = PQntuples(res); - attrdefs = (AttrDefInfo *) pg_malloc(numDefaults * sizeof(AttrDefInfo)); + attrdefs = pg_malloc_array(AttrDefInfo, numDefaults); curtblindx = -1; for (int j = 0; j < numDefaults; j++) @@ -9461,7 +9849,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) int i_consrc; int i_conislocal; - pg_log_info("finding invalid not null constraints"); + pg_log_info("finding invalid not-null constraints"); resetPQExpBuffer(q); appendPQExpBuffer(q, @@ -9476,7 +9864,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); numConstrs = PQntuples(res); - constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo)); + constrs = pg_malloc_array(ConstraintInfo, numConstrs); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -9575,7 +9963,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); numConstrs = PQntuples(res); - constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo)); + constrs = pg_malloc_array(ConstraintInfo, numConstrs); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -9702,8 +10090,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) * 4) The column has a constraint with a known name; in that case * notnull_constrs carries that name and dumpTableSchema will print * "CONSTRAINT the_name NOT NULL". However, if the name is the default - * (table_column_not_null), there's no need to print that name in the dump, - * so notnull_constrs is set to the empty string and it behaves as case 2. + * (table_column_not_null) and there's no comment on the constraint, + * there's no need to print that name in the dump, so notnull_constrs + * is set to the empty string and it behaves as case 2. * * In a child table that inherits from a parent already containing NOT NULL * constraints and the columns in the child don't have their own NOT NULL @@ -9730,6 +10119,7 @@ static void determineNotNullFlags(Archive *fout, PGresult *res, int r, TableInfo *tbinfo, int j, int i_notnull_name, + int i_notnull_comment, int i_notnull_invalidoid, int i_notnull_noinherit, int i_notnull_islocal, @@ -9803,11 +10193,13 @@ determineNotNullFlags(Archive *fout, PGresult *res, int r, { /* * In binary upgrade of inheritance child tables, must have a - * constraint name that we can UPDATE later. + * constraint name that we can UPDATE later; same if there's a + * comment on the constraint. */ - if (dopt->binary_upgrade && - !tbinfo->ispartition && - !tbinfo->notnull_islocal) + if ((dopt->binary_upgrade && + !tbinfo->ispartition && + !tbinfo->notnull_islocal[j]) || + !PQgetisnull(res, r, i_notnull_comment)) { tbinfo->notnull_constrs[j] = pstrdup(PQgetvalue(res, r, i_notnull_name)); @@ -9899,7 +10291,7 @@ getTSParsers(Archive *fout) ntups = PQntuples(res); - prsinfo = (TSParserInfo *) pg_malloc(ntups * sizeof(TSParserInfo)); + prsinfo = pg_malloc_array(TSParserInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -9966,7 +10358,7 @@ getTSDictionaries(Archive *fout) ntups = PQntuples(res); - dictinfo = (TSDictInfo *) pg_malloc(ntups * sizeof(TSDictInfo)); + dictinfo = pg_malloc_array(TSDictInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -10030,7 +10422,7 @@ getTSTemplates(Archive *fout) ntups = PQntuples(res); - tmplinfo = (TSTemplateInfo *) pg_malloc(ntups * sizeof(TSTemplateInfo)); + tmplinfo = pg_malloc_array(TSTemplateInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -10089,7 +10481,7 @@ getTSConfigurations(Archive *fout) ntups = PQntuples(res); - cfginfo = (TSConfigInfo *) pg_malloc(ntups * sizeof(TSConfigInfo)); + cfginfo = pg_malloc_array(TSConfigInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -10137,6 +10529,7 @@ getForeignDataWrappers(Archive *fout) int i_fdwowner; int i_fdwhandler; int i_fdwvalidator; + int i_fdwconnection; int i_fdwacl; int i_acldefault; int i_fdwoptions; @@ -10146,7 +10539,14 @@ getForeignDataWrappers(Archive *fout) appendPQExpBufferStr(query, "SELECT tableoid, oid, fdwname, " "fdwowner, " "fdwhandler::pg_catalog.regproc, " - "fdwvalidator::pg_catalog.regproc, " + "fdwvalidator::pg_catalog.regproc, "); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, "fdwconnection::pg_catalog.regproc, "); + else + appendPQExpBufferStr(query, "'-' AS fdwconnection, "); + + appendPQExpBufferStr(query, "fdwacl, " "acldefault('F', fdwowner) AS acldefault, " "array_to_string(ARRAY(" @@ -10161,7 +10561,7 @@ getForeignDataWrappers(Archive *fout) ntups = PQntuples(res); - fdwinfo = (FdwInfo *) pg_malloc(ntups * sizeof(FdwInfo)); + fdwinfo = pg_malloc_array(FdwInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -10169,6 +10569,7 @@ getForeignDataWrappers(Archive *fout) i_fdwowner = PQfnumber(res, "fdwowner"); i_fdwhandler = PQfnumber(res, "fdwhandler"); i_fdwvalidator = PQfnumber(res, "fdwvalidator"); + i_fdwconnection = PQfnumber(res, "fdwconnection"); i_fdwacl = PQfnumber(res, "fdwacl"); i_acldefault = PQfnumber(res, "acldefault"); i_fdwoptions = PQfnumber(res, "fdwoptions"); @@ -10188,6 +10589,7 @@ getForeignDataWrappers(Archive *fout) fdwinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_fdwowner)); fdwinfo[i].fdwhandler = pg_strdup(PQgetvalue(res, i, i_fdwhandler)); fdwinfo[i].fdwvalidator = pg_strdup(PQgetvalue(res, i, i_fdwvalidator)); + fdwinfo[i].fdwconnection = pg_strdup(PQgetvalue(res, i, i_fdwconnection)); fdwinfo[i].fdwoptions = pg_strdup(PQgetvalue(res, i, i_fdwoptions)); /* Decide whether we want to dump it */ @@ -10244,7 +10646,7 @@ getForeignServers(Archive *fout) ntups = PQntuples(res); - srvinfo = (ForeignServerInfo *) pg_malloc(ntups * sizeof(ForeignServerInfo)); + srvinfo = pg_malloc_array(ForeignServerInfo, ntups); i_tableoid = PQfnumber(res, "tableoid"); i_oid = PQfnumber(res, "oid"); @@ -10342,7 +10744,7 @@ getDefaultACLs(Archive *fout) ntups = PQntuples(res); - daclinfo = (DefaultACLInfo *) pg_malloc(ntups * sizeof(DefaultACLInfo)); + daclinfo = pg_malloc_array(DefaultACLInfo, ntups); i_oid = PQfnumber(res, "oid"); i_tableoid = PQfnumber(res, "tableoid"); @@ -10441,7 +10843,7 @@ collectRoleNames(Archive *fout) nrolenames = PQntuples(res); - rolenames = (RoleNameItem *) pg_malloc(nrolenames * sizeof(RoleNameItem)); + rolenames = pg_malloc_array(RoleNameItem, nrolenames); for (i = 0; i < nrolenames; i++) { @@ -10730,6 +11132,7 @@ static PGresult * fetchAttributeStats(Archive *fout) { ArchiveHandle *AH = (ArchiveHandle *) fout; + PQExpBuffer relids = createPQExpBuffer(); PQExpBuffer nspnames = createPQExpBuffer(); PQExpBuffer relnames = createPQExpBuffer(); int count = 0; @@ -10765,6 +11168,7 @@ fetchAttributeStats(Archive *fout) restarted = true; } + appendPQExpBufferChar(relids, '{'); appendPQExpBufferChar(nspnames, '{'); appendPQExpBufferChar(relnames, '{'); @@ -10776,15 +11180,28 @@ fetchAttributeStats(Archive *fout) */ for (; te != AH->toc && count < max_rels; te = te->next) { - if ((te->reqs & REQ_STATS) != 0 && - strcmp(te->desc, "STATISTICS DATA") == 0) + if ((te->reqs & REQ_STATS) == 0 || + strcmp(te->desc, "STATISTICS DATA") != 0) + continue; + + if (fout->remoteVersion >= 190000) + { + const RelStatsInfo *rsinfo = (const RelStatsInfo *) te->defnDumperArg; + char relid[32]; + + sprintf(relid, "%u", rsinfo->relid); + appendPGArray(relids, relid); + } + else { appendPGArray(nspnames, te->namespace); appendPGArray(relnames, te->tag); - count++; } + + count++; } + appendPQExpBufferChar(relids, '}'); appendPQExpBufferChar(nspnames, '}'); appendPQExpBufferChar(relnames, '}'); @@ -10794,14 +11211,25 @@ fetchAttributeStats(Archive *fout) PQExpBuffer query = createPQExpBuffer(); appendPQExpBufferStr(query, "EXECUTE getAttributeStats("); - appendStringLiteralAH(query, nspnames->data, fout); - appendPQExpBufferStr(query, "::pg_catalog.name[],"); - appendStringLiteralAH(query, relnames->data, fout); - appendPQExpBufferStr(query, "::pg_catalog.name[])"); + + if (fout->remoteVersion >= 190000) + { + appendStringLiteralAH(query, relids->data, fout); + appendPQExpBufferStr(query, "::pg_catalog.oid[])"); + } + else + { + appendStringLiteralAH(query, nspnames->data, fout); + appendPQExpBufferStr(query, "::pg_catalog.name[],"); + appendStringLiteralAH(query, relnames->data, fout); + appendPQExpBufferStr(query, "::pg_catalog.name[])"); + } + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); destroyPQExpBuffer(query); } + destroyPQExpBuffer(relids); destroyPQExpBuffer(nspnames); destroyPQExpBuffer(relnames); return res; @@ -10817,7 +11245,7 @@ fetchAttributeStats(Archive *fout) static char * dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te) { - const RelStatsInfo *rsinfo = (RelStatsInfo *) userArg; + const RelStatsInfo *rsinfo = userArg; static PGresult *res; static int rownum; PQExpBuffer query; @@ -10855,15 +11283,21 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te) expected_te = expected_te->next; if (te != expected_te) - pg_fatal("stats dumped out of order (current: %d %s %s) (expected: %d %s %s)", + pg_fatal("statistics dumped out of order (current: %d %s %s, expected: %d %s %s)", te->dumpId, te->desc, te->tag, expected_te->dumpId, expected_te->desc, expected_te->tag); query = createPQExpBuffer(); if (!fout->is_prepared[PREPQUERY_GETATTRIBUTESTATS]) { + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + "PREPARE getAttributeStats(pg_catalog.oid[]) AS\n"); + else + appendPQExpBufferStr(query, + "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n"); + appendPQExpBufferStr(query, - "PREPARE getAttributeStats(pg_catalog.name[], pg_catalog.name[]) AS\n" "SELECT s.schemaname, s.tablename, s.attname, s.inherited, " "s.null_frac, s.avg_width, s.n_distinct, " "s.most_common_vals, s.most_common_freqs, " @@ -10885,17 +11319,25 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te) /* * The results must be in the order of the relations supplied in the * parameters to ensure we remain in sync as we walk through the TOC. - * The redundant filter clause on s.tablename = ANY(...) seems - * sufficient to convince the planner to use + * + * For v9.4 through v18, the redundant filter clause on s.tablename = + * ANY(...) seems sufficient to convince the planner to use * pg_class_relname_nsp_index, which avoids a full scan of pg_stats. - * This may not work for all versions. + * In newer versions, pg_stats returns the table OIDs, eliminating the + * need for that hack. * * Our query for retrieving statistics for multiple relations uses * WITH ORDINALITY and multi-argument UNNEST(), both of which were * introduced in v9.4. For older versions, we resort to gathering * statistics for a single relation at a time. */ - if (fout->remoteVersion >= 90400) + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + "FROM pg_catalog.pg_stats s " + "JOIN unnest($1) WITH ORDINALITY AS u (tableid, ord) " + "ON s.tableid = u.tableid " + "ORDER BY u.ord, s.attname, s.inherited"); + else if (fout->remoteVersion >= 90400) appendPQExpBufferStr(query, "FROM pg_catalog.pg_stats s " "JOIN unnest($1, $2) WITH ORDINALITY AS u (schemaname, tablename, ord) " @@ -10996,7 +11438,7 @@ dumpRelationStats_dumper(Archive *fout, const void *userArg, const TocEntry *te) appendStringLiteralAH(out, rsinfo->dobj.name, fout); if (PQgetisnull(res, rownum, i_attname)) - pg_fatal("attname cannot be NULL"); + pg_fatal("unexpected null attname"); attname = PQgetvalue(res, rownum, i_attname); /* @@ -11318,7 +11760,7 @@ collectComments(Archive *fout) ntups = PQntuples(res); - comments = (CommentItem *) pg_malloc(ntups * sizeof(CommentItem)); + comments = pg_malloc_array(CommentItem, ntups); ncomments = 0; dobj = NULL; @@ -11442,6 +11884,7 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) break; case DO_STATSEXT: dumpStatisticsExt(fout, (const StatsExtInfo *) dobj); + dumpStatisticsExtStats(fout, (const StatsExtInfo *) dobj); break; case DO_REFRESH_MATVIEW: refreshMatViewData(fout, (const TableDataInfo *) dobj); @@ -11753,17 +12196,12 @@ dumpExtension(Archive *fout, const ExtensionInfo *extinfo) .createStmt = q->data, .dropStmt = delq->data)); - /* Dump Extension Comments and Security Labels */ + /* Dump Extension Comments */ if (extinfo->dobj.dump & DUMP_COMPONENT_COMMENT) dumpComment(fout, "EXTENSION", qextname, NULL, "", extinfo->dobj.catId, 0, extinfo->dobj.dumpId); - if (extinfo->dobj.dump & DUMP_COMPONENT_SECLABEL) - dumpSecLabel(fout, "EXTENSION", qextname, - NULL, "", - extinfo->dobj.catId, 0, extinfo->dobj.dumpId); - free(qextname); destroyPQExpBuffer(q); @@ -12497,8 +12935,36 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo) appendPQExpBuffer(q, " COLLATE %s", fmtQualifiedDumpable(coll)); } + /* + * Print a not-null constraint if there's one. In servers older than 17 + * these don't have names, so just print it unadorned; in newer ones they + * do, but most of the time it's going to be the standard generated one, + * so omit the name in that case also. + */ if (typnotnull[0] == 't') - appendPQExpBufferStr(q, " NOT NULL"); + { + if (fout->remoteVersion < 170000 || tyinfo->notnull == NULL) + appendPQExpBufferStr(q, " NOT NULL"); + else + { + ConstraintInfo *notnull = tyinfo->notnull; + + if (!notnull->separate) + { + char *default_name; + + /* XXX should match ChooseConstraintName better */ + default_name = psprintf("%s_not_null", tyinfo->dobj.name); + + if (strcmp(default_name, notnull->dobj.name) == 0) + appendPQExpBufferStr(q, " NOT NULL"); + else + appendPQExpBuffer(q, " CONSTRAINT %s %s", + fmtId(notnull->dobj.name), notnull->condef); + free(default_name); + } + } + } if (typdefault != NULL) { @@ -12518,7 +12984,7 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo) { ConstraintInfo *domcheck = &(tyinfo->domChecks[i]); - if (!domcheck->separate) + if (!domcheck->separate && domcheck->contype == 'c') appendPQExpBuffer(q, "\n\tCONSTRAINT %s %s", fmtId(domcheck->dobj.name), domcheck->condef); } @@ -12563,8 +13029,13 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo) for (i = 0; i < tyinfo->nDomChecks; i++) { ConstraintInfo *domcheck = &(tyinfo->domChecks[i]); - PQExpBuffer conprefix = createPQExpBuffer(); + PQExpBuffer conprefix; + /* but only if the constraint itself was dumped here */ + if (domcheck->separate) + continue; + + conprefix = createPQExpBuffer(); appendPQExpBuffer(conprefix, "CONSTRAINT %s ON DOMAIN", fmtId(domcheck->dobj.name)); @@ -12577,6 +13048,25 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo) destroyPQExpBuffer(conprefix); } + /* + * And a comment on the not-null constraint, if there's one -- but only if + * the constraint itself was dumped here + */ + if (tyinfo->notnull != NULL && !tyinfo->notnull->separate) + { + PQExpBuffer conprefix = createPQExpBuffer(); + + appendPQExpBuffer(conprefix, "CONSTRAINT %s ON DOMAIN", + fmtId(tyinfo->notnull->dobj.name)); + + if (tyinfo->notnull->dobj.dump & DUMP_COMPONENT_COMMENT) + dumpComment(fout, conprefix->data, qtypname, + tyinfo->dobj.namespace->dobj.name, + tyinfo->rolname, + tyinfo->notnull->dobj.catId, 0, tyinfo->dobj.dumpId); + destroyPQExpBuffer(conprefix); + } + destroyPQExpBuffer(q); destroyPQExpBuffer(delq); destroyPQExpBuffer(query); @@ -13347,7 +13837,7 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) if (*protrftypes) { - Oid *typeids = pg_malloc(FUNC_MAX_ARGS * sizeof(Oid)); + Oid *typeids = pg_malloc_array(Oid, FUNC_MAX_ARGS); int i; appendPQExpBufferStr(q, " TRANSFORM "); @@ -13447,7 +13937,8 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) * and then quote the elements as string literals. (The elements may * be double-quoted as-is, but we can't just feed them to the SQL * parser; it would do the wrong thing with elements that are - * zero-length or longer than NAMEDATALEN.) + * zero-length or longer than NAMEDATALEN.) Also, we need a special + * case for empty lists. * * Variables that are not so marked should just be emitted as simple * string literals. If the variable is not known to @@ -13463,6 +13954,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) /* this shouldn't fail really */ if (SplitGUCList(pos, ',', &namelist)) { + /* Special case: represent an empty list as NULL */ + if (*namelist == NULL) + appendPQExpBufferStr(q, "NULL"); for (nameptr = namelist; *nameptr; nameptr++) { if (nameptr != namelist) @@ -15739,6 +16233,9 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo) if (strcmp(fdwinfo->fdwvalidator, "-") != 0) appendPQExpBuffer(q, " VALIDATOR %s", fdwinfo->fdwvalidator); + if (strcmp(fdwinfo->fdwconnection, "-") != 0) + appendPQExpBuffer(q, " CONNECTION %s", fdwinfo->fdwconnection); + if (strlen(fdwinfo->fdwoptions) > 0) appendPQExpBuffer(q, " OPTIONS (\n %s\n)", fdwinfo->fdwoptions); @@ -16456,7 +16953,7 @@ collectSecLabels(Archive *fout) appendPQExpBufferStr(query, "SELECT label, provider, classoid, objoid, objsubid " - "FROM pg_catalog.pg_seclabel " + "FROM pg_catalog.pg_seclabels " "ORDER BY classoid, objoid, objsubid"); res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); @@ -16470,7 +16967,7 @@ collectSecLabels(Archive *fout) ntups = PQntuples(res); - seclabels = (SecLabelItem *) pg_malloc(ntups * sizeof(SecLabelItem)); + seclabels = pg_malloc_array(SecLabelItem, ntups); nseclabels = 0; dobj = NULL; @@ -16547,8 +17044,20 @@ dumpTable(Archive *fout, const TableInfo *tbinfo) namecopy = pg_strdup(fmtId(tbinfo->dobj.name)); if (tbinfo->dobj.dump & DUMP_COMPONENT_ACL) { - const char *objtype = - (tbinfo->relkind == RELKIND_SEQUENCE) ? "SEQUENCE" : "TABLE"; + const char *objtype; + + switch (tbinfo->relkind) + { + case RELKIND_SEQUENCE: + objtype = "SEQUENCE"; + break; + case RELKIND_PROPGRAPH: + objtype = "PROPERTY GRAPH"; + break; + default: + objtype = "TABLE"; + break; + } tableAclDumpId = dumpACL(fout, tbinfo->dobj.dumpId, InvalidDumpId, @@ -16794,8 +17303,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) reltypename = "VIEW"; - appendPQExpBuffer(delq, "DROP VIEW %s;\n", qualrelname); - if (dopt->binary_upgrade) binary_upgrade_set_pg_class_oids(fout, q, tbinfo->dobj.catId.oid); @@ -16821,6 +17328,47 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) appendPQExpBuffer(q, "\n WITH %s CHECK OPTION", tbinfo->checkoption); appendPQExpBufferStr(q, ";\n"); } + else if (tbinfo->relkind == RELKIND_PROPGRAPH) + { + PQExpBuffer query = createPQExpBuffer(); + PGresult *res; + int len; + + reltypename = "PROPERTY GRAPH"; + + if (dopt->binary_upgrade) + binary_upgrade_set_pg_class_oids(fout, q, + tbinfo->dobj.catId.oid); + + appendPQExpBuffer(query, + "SELECT pg_catalog.pg_get_propgraphdef('%u'::pg_catalog.oid) AS pgdef", + tbinfo->dobj.catId.oid); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + if (PQntuples(res) != 1) + { + if (PQntuples(res) < 1) + pg_fatal("query to obtain definition of property graph \"%s\" returned no data", + tbinfo->dobj.name); + else + pg_fatal("query to obtain definition of property graph \"%s\" returned more than one definition", + tbinfo->dobj.name); + } + + len = PQgetlength(res, 0, 0); + + if (len == 0) + pg_fatal("definition of property graph \"%s\" appears to be empty (length zero)", + tbinfo->dobj.name); + + appendPQExpBufferStr(q, PQgetvalue(res, 0, 0)); + + PQclear(res); + destroyPQExpBuffer(query); + + appendPQExpBufferStr(q, ";\n"); + } else { char *partkeydef = NULL; @@ -16896,8 +17444,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) numParents = tbinfo->numParents; parents = tbinfo->parents; - appendPQExpBuffer(delq, "DROP %s %s;\n", reltypename, qualrelname); - if (dopt->binary_upgrade) binary_upgrade_set_pg_class_oids(fout, q, tbinfo->dobj.catId.oid); @@ -17068,6 +17614,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) appendPQExpBuffer(q, "CONSTRAINT %s NOT NULL %s", tbinfo->notnull_constrs[j], fmtId(tbinfo->attnames[j])); + + if (tbinfo->notnull_noinh[j]) + appendPQExpBufferStr(q, " NO INHERIT"); } } @@ -17638,6 +18187,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) appendPQExpBuffer(q, "\nALTER TABLE ONLY %s FORCE ROW LEVEL SECURITY;\n", qualrelname); + appendPQExpBuffer(delq, "DROP %s %s;\n", reltypename, qualrelname); + if (dopt->binary_upgrade) binary_upgrade_extension_member(q, &tbinfo->dobj, reltypename, qrelname, @@ -17684,6 +18235,56 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) if (tbinfo->dobj.dump & DUMP_COMPONENT_SECLABEL) dumpTableSecLabel(fout, tbinfo, reltypename); + /* + * Dump comments for not-null constraints that aren't to be dumped + * separately (those are processed by collectComments/dumpComment). + */ + if (!fout->dopt->no_comments && dopt->dumpSchema && + fout->remoteVersion >= 180000) + { + PQExpBuffer comment = NULL; + PQExpBuffer tag = NULL; + + for (j = 0; j < tbinfo->numatts; j++) + { + if (tbinfo->notnull_constrs[j] != NULL && + tbinfo->notnull_comment[j] != NULL) + { + if (comment == NULL) + { + comment = createPQExpBuffer(); + tag = createPQExpBuffer(); + } + else + { + resetPQExpBuffer(comment); + resetPQExpBuffer(tag); + } + + appendPQExpBuffer(comment, "COMMENT ON CONSTRAINT %s ON %s IS ", + fmtId(tbinfo->notnull_constrs[j]), qualrelname); + appendStringLiteralAH(comment, tbinfo->notnull_comment[j], fout); + appendPQExpBufferStr(comment, ";\n"); + + appendPQExpBuffer(tag, "CONSTRAINT %s ON %s", + fmtId(tbinfo->notnull_constrs[j]), qrelname); + + ArchiveEntry(fout, nilCatalogId, createDumpId(), + ARCHIVE_OPTS(.tag = tag->data, + .namespace = tbinfo->dobj.namespace->dobj.name, + .owner = tbinfo->rolname, + .description = "COMMENT", + .section = SECTION_NONE, + .createStmt = comment->data, + .deps = &(tbinfo->dobj.dumpId), + .nDeps = 1)); + } + } + + destroyPQExpBuffer(comment); + destroyPQExpBuffer(tag); + } + /* Dump comments on inlined table constraints */ for (j = 0; j < tbinfo->ncheck; j++) { @@ -18143,6 +18744,286 @@ dumpStatisticsExt(Archive *fout, const StatsExtInfo *statsextinfo) free(qstatsextname); } +/* + * dumpStatisticsExtStats + * write out to fout the stats for an extended statistics object + */ +static void +dumpStatisticsExtStats(Archive *fout, const StatsExtInfo *statsextinfo) +{ + DumpOptions *dopt = fout->dopt; + PQExpBuffer query; + PGresult *res; + int nstats; + + /* Do nothing if not dumping statistics */ + if (!dopt->dumpStatistics) + return; + + if (!fout->is_prepared[PREPQUERY_DUMPEXTSTATSOBJSTATS]) + { + PQExpBuffer pq = createPQExpBuffer(); + + /*--------- + * Set up query for details about extended statistics objects. + * + * The query depends on the backend version: + * - In v19 and newer versions, query directly the pg_stats_ext* + * catalogs. + * - In v18 and older versions, ndistinct and dependencies have a + * different format that needs translation. + * - In v14 and older versions, inherited does not exist. + * - In v11 and older versions, there is no pg_stats_ext, hence + * the logic joins pg_statistic_ext and pg_namespace. + *--------- + */ + + appendPQExpBufferStr(pq, + "PREPARE getExtStatsStats(pg_catalog.name, pg_catalog.name) AS\n" + "SELECT "); + + /* + * Versions 15 and newer have inherited stats. + * + * Create this column in all versions because we need to order by it + * later. + */ + if (fout->remoteVersion >= 150000) + appendPQExpBufferStr(pq, "e.inherited, "); + else + appendPQExpBufferStr(pq, "false AS inherited, "); + + /*-------- + * The ndistinct and dependencies formats changed in v19, so + * everything before that needs to be translated. + * + * The ndistinct translation converts this kind of data: + * {"3, 4": 11, "3, 6": 11, "4, 6": 11, "3, 4, 6": 11} + * + * to this: + * [ {"attributes": [3,4], "ndistinct": 11}, + * {"attributes": [3,6], "ndistinct": 11}, + * {"attributes": [4,6], "ndistinct": 11}, + * {"attributes": [3,4,6], "ndistinct": 11} ] + * + * The dependencies translation converts this kind of data: + * {"3 => 4": 1.000000, "3 => 6": 1.000000, + * "4 => 6": 1.000000, "3, 4 => 6": 1.000000, + * "3, 6 => 4": 1.000000} + * + * to this: + * [ {"attributes": [3], "dependency": 4, "degree": 1.000000}, + * {"attributes": [3], "dependency": 6, "degree": 1.000000}, + * {"attributes": [4], "dependency": 6, "degree": 1.000000}, + * {"attributes": [3,4], "dependency": 6, "degree": 1.000000}, + * {"attributes": [3,6], "dependency": 4, "degree": 1.000000} ] + *-------- + */ + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(pq, "e.n_distinct, e.dependencies, "); + else + appendPQExpBufferStr(pq, + "( " + "SELECT json_agg( " + " json_build_object( " + " '" PG_NDISTINCT_KEY_ATTRIBUTES "', " + " string_to_array(kv.key, ', ')::integer[], " + " '" PG_NDISTINCT_KEY_NDISTINCT "', " + " kv.value::bigint )) " + "FROM json_each_text(e.n_distinct::text::json) AS kv" + ") AS n_distinct, " + "( " + "SELECT json_agg( " + " json_build_object( " + " '" PG_DEPENDENCIES_KEY_ATTRIBUTES "', " + " string_to_array( " + " split_part(kv.key, ' => ', 1), " + " ', ')::integer[], " + " '" PG_DEPENDENCIES_KEY_DEPENDENCY "', " + " split_part(kv.key, ' => ', 2)::integer, " + " '" PG_DEPENDENCIES_KEY_DEGREE "', " + " kv.value::double precision )) " + "FROM json_each_text(e.dependencies::text::json) AS kv " + ") AS dependencies, "); + + /* MCV was introduced v13 */ + if (fout->remoteVersion >= 130000) + appendPQExpBufferStr(pq, + "e.most_common_vals, e.most_common_freqs, " + "e.most_common_base_freqs, "); + else + appendPQExpBufferStr(pq, + "NULL AS most_common_vals, NULL AS most_common_freqs, " + "NULL AS most_common_base_freqs, "); + + /* Expressions were introduced in v14 */ + if (fout->remoteVersion >= 140000) + { + /* + * There is no ordering column in pg_stats_ext_exprs. However, we + * can rely on the unnesting of pg_statistic_ext_data.stxdexpr to + * maintain the desired order of expression elements. + */ + appendPQExpBufferStr(pq, + "( " + "SELECT jsonb_pretty(jsonb_agg(" + "nullif(j.obj, '{}'::jsonb))) " + "FROM pg_stats_ext_exprs AS ee " + "CROSS JOIN LATERAL jsonb_strip_nulls(" + " jsonb_build_object( " + " 'null_frac', ee.null_frac::text, " + " 'avg_width', ee.avg_width::text, " + " 'n_distinct', ee.n_distinct::text, " + " 'most_common_vals', ee.most_common_vals::text, " + " 'most_common_freqs', ee.most_common_freqs::text, " + " 'histogram_bounds', ee.histogram_bounds::text, " + " 'correlation', ee.correlation::text, " + " 'most_common_elems', ee.most_common_elems::text, " + " 'most_common_elem_freqs', ee.most_common_elem_freqs::text, " + " 'elem_count_histogram', ee.elem_count_histogram::text"); + + /* These three have been added to pg_stats_ext_exprs in v19. */ + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(pq, + ", " + " 'range_length_histogram', ee.range_length_histogram::text, " + " 'range_empty_frac', ee.range_empty_frac::text, " + " 'range_bounds_histogram', ee.range_bounds_histogram::text"); + + appendPQExpBufferStr(pq, + " )) AS j(obj)" + "WHERE ee.statistics_schemaname = $1 " + "AND ee.statistics_name = $2 "); + /* Inherited expressions introduced in v15 */ + if (fout->remoteVersion >= 150000) + appendPQExpBufferStr(pq, "AND ee.inherited = e.inherited"); + + appendPQExpBufferStr(pq, ") AS exprs "); + } + else + appendPQExpBufferStr(pq, "NULL AS exprs "); + + /* pg_stats_ext introduced in v12 */ + if (fout->remoteVersion >= 120000) + appendPQExpBufferStr(pq, + "FROM pg_catalog.pg_stats_ext AS e " + "WHERE e.statistics_schemaname = $1 " + "AND e.statistics_name = $2 "); + else + appendPQExpBufferStr(pq, + "FROM ( " + "SELECT s.stxndistinct AS n_distinct, " + " s.stxdependencies AS dependencies " + "FROM pg_catalog.pg_statistic_ext AS s " + "JOIN pg_catalog.pg_namespace AS n " + "ON n.oid = s.stxnamespace " + "WHERE n.nspname = $1 " + "AND s.stxname = $2 " + ") AS e "); + + /* we always have an inherited column, but it may be a constant */ + appendPQExpBufferStr(pq, "ORDER BY inherited"); + + ExecuteSqlStatement(fout, pq->data); + + fout->is_prepared[PREPQUERY_DUMPEXTSTATSOBJSTATS] = true; + + destroyPQExpBuffer(pq); + } + + query = createPQExpBuffer(); + + appendPQExpBufferStr(query, "EXECUTE getExtStatsStats("); + appendStringLiteralAH(query, statsextinfo->dobj.namespace->dobj.name, fout); + appendPQExpBufferStr(query, "::pg_catalog.name, "); + appendStringLiteralAH(query, statsextinfo->dobj.name, fout); + appendPQExpBufferStr(query, "::pg_catalog.name)"); + + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + destroyPQExpBuffer(query); + + nstats = PQntuples(res); + + if (nstats > 0) + { + PQExpBuffer out = createPQExpBuffer(); + + int i_inherited = PQfnumber(res, "inherited"); + int i_ndistinct = PQfnumber(res, "n_distinct"); + int i_dependencies = PQfnumber(res, "dependencies"); + int i_mcv = PQfnumber(res, "most_common_vals"); + int i_mcf = PQfnumber(res, "most_common_freqs"); + int i_mcbf = PQfnumber(res, "most_common_base_freqs"); + int i_exprs = PQfnumber(res, "exprs"); + + for (int i = 0; i < nstats; i++) + { + TableInfo *tbinfo = statsextinfo->stattable; + + if (PQgetisnull(res, i, i_inherited)) + pg_fatal("inherited cannot be NULL"); + + appendPQExpBufferStr(out, + "SELECT * FROM pg_catalog.pg_restore_extended_stats(\n"); + appendPQExpBuffer(out, "\t'version', '%d'::integer,\n", + fout->remoteVersion); + + /* Relation information */ + appendPQExpBufferStr(out, "\t'schemaname', "); + appendStringLiteralAH(out, tbinfo->dobj.namespace->dobj.name, fout); + appendPQExpBufferStr(out, ",\n\t'relname', "); + appendStringLiteralAH(out, tbinfo->dobj.name, fout); + + /* Extended statistics information */ + appendPQExpBufferStr(out, ",\n\t'statistics_schemaname', "); + appendStringLiteralAH(out, statsextinfo->dobj.namespace->dobj.name, fout); + appendPQExpBufferStr(out, ",\n\t'statistics_name', "); + appendStringLiteralAH(out, statsextinfo->dobj.name, fout); + appendNamedArgument(out, fout, "inherited", "boolean", + PQgetvalue(res, i, i_inherited)); + + if (!PQgetisnull(res, i, i_ndistinct)) + appendNamedArgument(out, fout, "n_distinct", "pg_ndistinct", + PQgetvalue(res, i, i_ndistinct)); + + if (!PQgetisnull(res, i, i_dependencies)) + appendNamedArgument(out, fout, "dependencies", "pg_dependencies", + PQgetvalue(res, i, i_dependencies)); + + if (!PQgetisnull(res, i, i_mcv)) + appendNamedArgument(out, fout, "most_common_vals", "text[]", + PQgetvalue(res, i, i_mcv)); + + if (!PQgetisnull(res, i, i_mcf)) + appendNamedArgument(out, fout, "most_common_freqs", "double precision[]", + PQgetvalue(res, i, i_mcf)); + + if (!PQgetisnull(res, i, i_mcbf)) + appendNamedArgument(out, fout, "most_common_base_freqs", "double precision[]", + PQgetvalue(res, i, i_mcbf)); + + if (!PQgetisnull(res, i, i_exprs)) + appendNamedArgument(out, fout, "exprs", "jsonb", + PQgetvalue(res, i, i_exprs)); + + appendPQExpBufferStr(out, "\n);\n"); + } + + ArchiveEntry(fout, nilCatalogId, createDumpId(), + ARCHIVE_OPTS(.tag = statsextinfo->dobj.name, + .namespace = statsextinfo->dobj.namespace->dobj.name, + .owner = statsextinfo->rolname, + .description = "EXTENDED STATISTICS DATA", + .section = SECTION_POST_DATA, + .createStmt = out->data, + .deps = &statsextinfo->dobj.dumpId, + .nDeps = 1)); + destroyPQExpBuffer(out); + } + PQclear(res); +} + /* * dumpConstraint * write out to fout a user-defined constraint @@ -18388,14 +19269,23 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo) .dropStmt = delq->data)); } } - else if (coninfo->contype == 'c' && tbinfo == NULL) + else if (tbinfo == NULL) { - /* CHECK constraint on a domain */ + /* CHECK, NOT NULL constraint on a domain */ TypeInfo *tyinfo = coninfo->condomain; + Assert(coninfo->contype == 'c' || coninfo->contype == 'n'); + /* Ignore if not to be dumped separately */ if (coninfo->separate) { + const char *keyword; + + if (coninfo->contype == 'c') + keyword = "CHECK CONSTRAINT"; + else + keyword = "CONSTRAINT"; + appendPQExpBuffer(q, "ALTER DOMAIN %s\n", fmtQualifiedDumpable(tyinfo)); appendPQExpBuffer(q, " ADD CONSTRAINT %s %s;\n", @@ -18414,10 +19304,26 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo) ARCHIVE_OPTS(.tag = tag, .namespace = tyinfo->dobj.namespace->dobj.name, .owner = tyinfo->rolname, - .description = "CHECK CONSTRAINT", + .description = keyword, .section = SECTION_POST_DATA, .createStmt = q->data, .dropStmt = delq->data)); + + if (coninfo->dobj.dump & DUMP_COMPONENT_COMMENT) + { + PQExpBuffer conprefix = createPQExpBuffer(); + char *qtypname = pg_strdup(fmtId(tyinfo->dobj.name)); + + appendPQExpBuffer(conprefix, "CONSTRAINT %s ON DOMAIN", + fmtId(coninfo->dobj.name)); + + dumpComment(fout, conprefix->data, qtypname, + tyinfo->dobj.namespace->dobj.name, + tyinfo->rolname, + coninfo->dobj.catId, 0, coninfo->dobj.dumpId); + destroyPQExpBuffer(conprefix); + free(qtypname); + } } } else @@ -18535,7 +19441,7 @@ collectSequences(Archive *fout) res = ExecuteSqlQuery(fout, query, PGRES_TUPLES_OK); nsequences = PQntuples(res); - sequences = (SequenceItem *) pg_malloc(nsequences * sizeof(SequenceItem)); + sequences = pg_malloc_array(SequenceItem, nsequences); for (int i = 0; i < nsequences; i++) { @@ -18549,6 +19455,7 @@ collectSequences(Archive *fout) sequences[i].cycled = (strcmp(PQgetvalue(res, i, 7), "t") == 0); sequences[i].last_value = strtoi64(PQgetvalue(res, i, 8), NULL, 10); sequences[i].is_called = (strcmp(PQgetvalue(res, i, 9), "t") == 0); + sequences[i].null_seqtuple = (PQgetisnull(res, i, 8) || PQgetisnull(res, i, 9)); } PQclear(res); @@ -18612,7 +19519,7 @@ dumpSequence(Archive *fout, const TableInfo *tbinfo) PQntuples(res)), tbinfo->dobj.name, PQntuples(res)); - seq = pg_malloc0(sizeof(SequenceItem)); + seq = pg_malloc0_object(SequenceItem); seq->seqtype = parse_sequence_type(PQgetvalue(res, 0, 0)); seq->startv = strtoi64(PQgetvalue(res, 0, 1), NULL, 10); seq->incby = strtoi64(PQgetvalue(res, 0, 2), NULL, 10); @@ -18818,7 +19725,13 @@ dumpSequenceData(Archive *fout, const TableDataInfo *tdinfo) TableInfo *tbinfo = tdinfo->tdtable; int64 last; bool called; - PQExpBuffer query = createPQExpBuffer(); + PQExpBuffer query; + + /* needn't bother if not dumping sequence data */ + if (!fout->dopt->dumpData && !fout->dopt->sequence_data) + return; + + query = createPQExpBuffer(); /* * For versions >= 18, the sequence information is gathered in the sorted @@ -18861,6 +19774,12 @@ dumpSequenceData(Archive *fout, const TableDataInfo *tdinfo) entry = bsearch(&key, sequences, nsequences, sizeof(SequenceItem), SequenceItemCmp); + if (entry->null_seqtuple) + pg_fatal("failed to get data for sequence \"%s\"; user may lack " + "SELECT privilege on the sequence or the sequence may " + "have been concurrently dropped", + tbinfo->dobj.name); + last = entry->last_value; called = entry->is_called; } @@ -19091,6 +20010,11 @@ dumpEventTrigger(Archive *fout, const EventTriggerInfo *evtinfo) NULL, evtinfo->evtowner, evtinfo->dobj.catId, 0, evtinfo->dobj.dumpId); + if (evtinfo->dobj.dump & DUMP_COMPONENT_SECLABEL) + dumpSecLabel(fout, "EVENT TRIGGER", qevtname, + NULL, evtinfo->evtowner, + evtinfo->dobj.catId, 0, evtinfo->dobj.dumpId); + destroyPQExpBuffer(query); destroyPQExpBuffer(delqry); free(qevtname); @@ -19598,6 +20522,17 @@ getDependencies(Archive *fout) "classid = 'pg_amproc'::regclass AND objid = p.oid " "AND NOT (refclassid = 'pg_opfamily'::regclass AND amprocfamily = refobjid)\n"); + /* + * Translate dependencies of pg_propgraph_element entries into + * dependencies of their parent pg_class entry. + */ + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, "UNION ALL\n" + "SELECT 'pg_class'::regclass AS classid, pgepgid AS objid, refclassid, refobjid, deptype " + "FROM pg_depend d, pg_propgraph_element pge " + "WHERE deptype NOT IN ('p', 'e', 'i') AND " + "classid = 'pg_propgraph_element'::regclass AND objid = pge.oid\n"); + /* Sort the output for efficiency below */ appendPQExpBufferStr(query, "ORDER BY 1,2"); @@ -19699,7 +20634,7 @@ createBoundaryObjects(void) { DumpableObject *dobjs; - dobjs = (DumpableObject *) pg_malloc(2 * sizeof(DumpableObject)); + dobjs = pg_malloc_array(DumpableObject, 2); dobjs[0].objType = DO_PRE_DATA_BOUNDARY; dobjs[0].catId = nilCatalogId; @@ -19873,7 +20808,7 @@ BuildArchiveDependencies(Archive *fout) continue; /* Set up work array */ allocDeps = 64; - dependencies = (DumpId *) pg_malloc(allocDeps * sizeof(DumpId)); + dependencies = pg_malloc_array(DumpId, allocDeps); nDeps = 0; /* Recursively find all dumpable dependencies */ findDumpableDependencies(AH, dobj, @@ -19881,8 +20816,7 @@ BuildArchiveDependencies(Archive *fout) /* And save 'em ... */ if (nDeps > 0) { - dependencies = (DumpId *) pg_realloc(dependencies, - nDeps * sizeof(DumpId)); + dependencies = pg_realloc_array(dependencies, DumpId, nDeps); te->dependencies = dependencies; te->nDeps = nDeps; } @@ -19916,8 +20850,7 @@ findDumpableDependencies(ArchiveHandle *AH, const DumpableObject *dobj, if (*nDeps >= *allocDeps) { *allocDeps *= 2; - *dependencies = (DumpId *) pg_realloc(*dependencies, - *allocDeps * sizeof(DumpId)); + *dependencies = pg_realloc_array(*dependencies, DumpId, *allocDeps); } (*dependencies)[*nDeps] = depid; (*nDeps)++; diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 7417eab6aefa6..5a6726d8b12e2 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -3,7 +3,7 @@ * pg_dump.h * Common header file for the pg_dump utility * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_dump/pg_dump.h @@ -222,7 +222,9 @@ typedef struct _typeInfo bool isDefined; /* true if typisdefined */ /* If needed, we'll create a "shell type" entry for it; link that here: */ struct _shellTypeInfo *shellType; /* shell-type entry, or NULL */ - /* If it's a domain, we store links to its constraints here: */ + /* If it's a domain, its not-null constraint is here: */ + struct _constraintInfo *notnull; + /* If it's a domain, we store links to its CHECK constraints here: */ int nDomChecks; struct _constraintInfo *domChecks; } TypeInfo; @@ -258,6 +260,8 @@ typedef struct _oprInfo DumpableObject dobj; const char *rolname; char oprkind; + Oid oprleft; + Oid oprright; Oid oprcode; } OprInfo; @@ -271,12 +275,14 @@ typedef struct _accessMethodInfo typedef struct _opclassInfo { DumpableObject dobj; + Oid opcmethod; const char *rolname; } OpclassInfo; typedef struct _opfamilyInfo { DumpableObject dobj; + Oid opfmethod; const char *rolname; } OpfamilyInfo; @@ -284,6 +290,7 @@ typedef struct _collInfo { DumpableObject dobj; const char *rolname; + int collencoding; } CollInfo; typedef struct _convInfo @@ -365,6 +372,7 @@ typedef struct _tableInfo * there isn't one on this column. If * empty string, unnamed constraint * (pre-v17) */ + char **notnull_comment; /* comment thereof */ bool *notnull_invalid; /* true for NOT NULL NOT VALID */ bool *notnull_noinh; /* NOT NULL is NO INHERIT */ bool *notnull_islocal; /* true if NOT NULL has local definition */ @@ -440,6 +448,7 @@ typedef struct _indexAttachInfo typedef struct _relStatsInfo { DumpableObject dobj; + Oid relid; int32 relpages; char *reltuples; int32 relallvisible; @@ -596,6 +605,7 @@ typedef struct _fdwInfo const char *rolname; char *fdwhandler; char *fdwvalidator; + char *fdwconnection; char *fdwoptions; } FdwInfo; @@ -661,12 +671,14 @@ typedef struct _PublicationInfo DumpableObject dobj; const char *rolname; bool puballtables; + bool puballsequences; bool pubinsert; bool pubupdate; bool pubdelete; bool pubtruncate; bool pubviaroot; PublishGencolsType pubgencols_type; + SimplePtrList except_tables; } PublicationInfo; /* @@ -708,9 +720,13 @@ typedef struct _SubscriptionInfo bool subpasswordrequired; bool subrunasowner; bool subfailover; + bool subretaindeadtuples; + int submaxretention; + char *subservername; char *subconninfo; char *subslotname; char *subsynccommit; + char *subwalrcvtimeout; char *subpublications; char *suborigin; char *suboriginremotelsn; @@ -756,6 +772,7 @@ extern TableInfo *findTableByOid(Oid oid); extern TypeInfo *findTypeByOid(Oid oid); extern FuncInfo *findFuncByOid(Oid oid); extern OprInfo *findOprByOid(Oid oid); +extern AccessMethodInfo *findAccessMethodByOid(Oid oid); extern CollInfo *findCollationByOid(Oid oid); extern NamespaceInfo *findNamespaceByOid(Oid oid); extern ExtensionInfo *findExtensionByOid(Oid oid); @@ -817,6 +834,6 @@ extern void getPublicationNamespaces(Archive *fout); extern void getPublicationTables(Archive *fout, TableInfo tblinfo[], int numTables); extern void getSubscriptions(Archive *fout); -extern void getSubscriptionTables(Archive *fout); +extern void getSubscriptionRelations(Archive *fout); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 0b0977788f13d..03e5c1c1116c0 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -4,7 +4,7 @@ * Sort the items of a dump into a safe order for dumping * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -76,10 +76,10 @@ enum dbObjectTypePriorities PRIO_TABLE_ATTACH, PRIO_DUMMY_TYPE, PRIO_ATTRDEF, - PRIO_LARGE_OBJECT, PRIO_PRE_DATA_BOUNDARY, /* boundary! */ PRIO_TABLE_DATA, PRIO_SEQUENCE_SET, + PRIO_LARGE_OBJECT, PRIO_LARGE_OBJECT_DATA, PRIO_STATISTICS_DATA_DATA, PRIO_POST_DATA_BOUNDARY, /* boundary! */ @@ -162,6 +162,8 @@ static DumpId postDataBoundId; static int DOTypeNameCompare(const void *p1, const void *p2); +static int pgTypeNameCompare(Oid typid1, Oid typid2); +static int accessMethodNameCompare(Oid am1, Oid am2); static bool TopoSort(DumpableObject **objs, int numObjs, DumpableObject **ordering, @@ -228,12 +230,39 @@ DOTypeNameCompare(const void *p1, const void *p2) else if (obj2->namespace) return 1; - /* Sort by name */ + /* + * Sort by name. With a few exceptions, names here are single catalog + * columns. To get a fuller picture, grep pg_dump.c for "dobj.name = ". + * Names here don't match "Name:" in plain format output, which is a + * _tocEntry.tag. For example, DumpableObject.name of a constraint is + * pg_constraint.conname, but _tocEntry.tag of a constraint is relname and + * conname joined with a space. + */ cmpval = strcmp(obj1->name, obj2->name); if (cmpval != 0) return cmpval; - /* To have a stable sort order, break ties for some object types */ + /* + * Sort by type. This helps types that share a type priority without + * sharing a unique name constraint, e.g. opclass and opfamily. + */ + cmpval = obj1->objType - obj2->objType; + if (cmpval != 0) + return cmpval; + + /* + * To have a stable sort order, break ties for some object types. Most + * catalogs have a natural key, e.g. pg_proc_proname_args_nsp_index. Where + * the above "namespace" and "name" comparisons don't cover all natural + * key columns, compare the rest here. + * + * The natural key usually refers to other catalogs by surrogate keys. + * Hence, this translates each of those references to the natural key of + * the referenced catalog. That may descend through multiple levels of + * catalog references. For example, to sort by pg_proc.proargtypes, + * descend to each pg_type and then further to its pg_namespace, for an + * overall sort by (nspname, typname). + */ if (obj1->objType == DO_FUNC || obj1->objType == DO_AGG) { FuncInfo *fobj1 = *(FuncInfo *const *) p1; @@ -246,22 +275,10 @@ DOTypeNameCompare(const void *p1, const void *p2) return cmpval; for (i = 0; i < fobj1->nargs; i++) { - TypeInfo *argtype1 = findTypeByOid(fobj1->argtypes[i]); - TypeInfo *argtype2 = findTypeByOid(fobj2->argtypes[i]); - - if (argtype1 && argtype2) - { - if (argtype1->dobj.namespace && argtype2->dobj.namespace) - { - cmpval = strcmp(argtype1->dobj.namespace->dobj.name, - argtype2->dobj.namespace->dobj.name); - if (cmpval != 0) - return cmpval; - } - cmpval = strcmp(argtype1->dobj.name, argtype2->dobj.name); - if (cmpval != 0) - return cmpval; - } + cmpval = pgTypeNameCompare(fobj1->argtypes[i], + fobj2->argtypes[i]); + if (cmpval != 0) + return cmpval; } } else if (obj1->objType == DO_OPERATOR) @@ -273,6 +290,57 @@ DOTypeNameCompare(const void *p1, const void *p2) cmpval = (oobj2->oprkind - oobj1->oprkind); if (cmpval != 0) return cmpval; + /* Within an oprkind, sort by argument type names */ + cmpval = pgTypeNameCompare(oobj1->oprleft, oobj2->oprleft); + if (cmpval != 0) + return cmpval; + cmpval = pgTypeNameCompare(oobj1->oprright, oobj2->oprright); + if (cmpval != 0) + return cmpval; + } + else if (obj1->objType == DO_OPCLASS) + { + OpclassInfo *opcobj1 = *(OpclassInfo *const *) p1; + OpclassInfo *opcobj2 = *(OpclassInfo *const *) p2; + + /* Sort by access method name, per pg_opclass_am_name_nsp_index */ + cmpval = accessMethodNameCompare(opcobj1->opcmethod, + opcobj2->opcmethod); + if (cmpval != 0) + return cmpval; + } + else if (obj1->objType == DO_OPFAMILY) + { + OpfamilyInfo *opfobj1 = *(OpfamilyInfo *const *) p1; + OpfamilyInfo *opfobj2 = *(OpfamilyInfo *const *) p2; + + /* Sort by access method name, per pg_opfamily_am_name_nsp_index */ + cmpval = accessMethodNameCompare(opfobj1->opfmethod, + opfobj2->opfmethod); + if (cmpval != 0) + return cmpval; + } + else if (obj1->objType == DO_COLLATION) + { + CollInfo *cobj1 = *(CollInfo *const *) p1; + CollInfo *cobj2 = *(CollInfo *const *) p2; + + /* + * Sort by encoding, per pg_collation_name_enc_nsp_index. Technically, + * this is not necessary, because wherever this changes dump order, + * restoring the dump fails anyway. CREATE COLLATION can't create a + * tie for this to break, because it imposes restrictions to make + * (nspname, collname) uniquely identify a collation within a given + * DatabaseEncoding. While pg_import_system_collations() can create a + * tie, pg_dump+restore fails after + * pg_import_system_collations('my_schema') does so. However, there's + * little to gain by ignoring one natural key column on the basis of + * those limitations elsewhere, so respect the full natural key like + * we do for other object types. + */ + cmpval = cobj1->collencoding - cobj2->collencoding; + if (cmpval != 0) + return cmpval; } else if (obj1->objType == DO_ATTRDEF) { @@ -317,11 +385,168 @@ DOTypeNameCompare(const void *p1, const void *p2) if (cmpval != 0) return cmpval; } + else if (obj1->objType == DO_CONSTRAINT || + obj1->objType == DO_FK_CONSTRAINT) + { + ConstraintInfo *robj1 = *(ConstraintInfo *const *) p1; + ConstraintInfo *robj2 = *(ConstraintInfo *const *) p2; + + /* + * Sort domain constraints before table constraints, for consistency + * with our decision to sort CREATE DOMAIN before CREATE TABLE. + */ + if (robj1->condomain) + { + if (robj2->condomain) + { + /* Sort by domain name (domain namespace was considered) */ + cmpval = strcmp(robj1->condomain->dobj.name, + robj2->condomain->dobj.name); + if (cmpval != 0) + return cmpval; + } + else + return PRIO_TYPE - PRIO_TABLE; + } + else if (robj2->condomain) + return PRIO_TABLE - PRIO_TYPE; + else + { + /* Sort by table name (table namespace was considered already) */ + cmpval = strcmp(robj1->contable->dobj.name, + robj2->contable->dobj.name); + if (cmpval != 0) + return cmpval; + } + } + else if (obj1->objType == DO_DEFAULT_ACL) + { + DefaultACLInfo *daclobj1 = *(DefaultACLInfo *const *) p1; + DefaultACLInfo *daclobj2 = *(DefaultACLInfo *const *) p2; + + /* + * Sort by defaclrole, per pg_default_acl_role_nsp_obj_index. The + * (namespace, name) match (defaclnamespace, defaclobjtype). + */ + cmpval = strcmp(daclobj1->defaclrole, daclobj2->defaclrole); + if (cmpval != 0) + return cmpval; + } + else if (obj1->objType == DO_PUBLICATION_REL) + { + PublicationRelInfo *probj1 = *(PublicationRelInfo *const *) p1; + PublicationRelInfo *probj2 = *(PublicationRelInfo *const *) p2; - /* Usually shouldn't get here, but if we do, sort by OID */ + /* Sort by publication name, since (namespace, name) match the rel */ + cmpval = strcmp(probj1->publication->dobj.name, + probj2->publication->dobj.name); + if (cmpval != 0) + return cmpval; + } + else if (obj1->objType == DO_PUBLICATION_TABLE_IN_SCHEMA) + { + PublicationSchemaInfo *psobj1 = *(PublicationSchemaInfo *const *) p1; + PublicationSchemaInfo *psobj2 = *(PublicationSchemaInfo *const *) p2; + + /* Sort by publication name, since ->name is just nspname */ + cmpval = strcmp(psobj1->publication->dobj.name, + psobj2->publication->dobj.name); + if (cmpval != 0) + return cmpval; + } + else if (obj1->objType == DO_SUBSCRIPTION_REL) + { + SubRelInfo *srobj1 = *(SubRelInfo *const *) p1; + SubRelInfo *srobj2 = *(SubRelInfo *const *) p2; + + /* Sort by subscription name, since (namespace, name) match the rel */ + cmpval = strcmp(srobj1->subinfo->dobj.name, + srobj2->subinfo->dobj.name); + if (cmpval != 0) + return cmpval; + } + + /* + * Shouldn't get here except after catalog corruption, but if we do, sort + * by OID. This may make logically-identical databases differ in the + * order of objects in dump output. Users will get spurious schema diffs. + * Expect flaky failures of 002_pg_upgrade.pl test 'dump outputs from + * original and restored regression databases match' if the regression + * database contains objects allowing that test to reach here. That's a + * consequence of the test using "pg_restore -j", which doesn't fully + * constrain OID assignment order. + */ + Assert(false); return oidcmp(obj1->catId.oid, obj2->catId.oid); } +/* Compare two OID-identified pg_type values by nspname, then by typname. */ +static int +pgTypeNameCompare(Oid typid1, Oid typid2) +{ + TypeInfo *typobj1; + TypeInfo *typobj2; + int cmpval; + + if (typid1 == typid2) + return 0; + + typobj1 = findTypeByOid(typid1); + typobj2 = findTypeByOid(typid2); + + if (!typobj1 || !typobj2) + { + /* + * getTypes() didn't find some OID. Assume catalog corruption, e.g. + * an oprright value without the corresponding OID in a pg_type row. + * Report as "equal", so the caller uses the next available basis for + * comparison, e.g. the next function argument. + * + * Unary operators have InvalidOid in oprleft (if oprkind='r') or in + * oprright (if oprkind='l'). Caller already sorted by oprkind, + * calling us only for like-kind operators. Hence, "typid1 == typid2" + * took care of InvalidOid. (v14 removed postfix operator support. + * Hence, when dumping from v14+, only oprleft can be InvalidOid.) + */ + Assert(false); + return 0; + } + + if (!typobj1->dobj.namespace || !typobj2->dobj.namespace) + Assert(false); /* catalog corruption */ + else + { + cmpval = strcmp(typobj1->dobj.namespace->dobj.name, + typobj2->dobj.namespace->dobj.name); + if (cmpval != 0) + return cmpval; + } + return strcmp(typobj1->dobj.name, typobj2->dobj.name); +} + +/* Compare two OID-identified pg_am values by amname. */ +static int +accessMethodNameCompare(Oid am1, Oid am2) +{ + AccessMethodInfo *amobj1; + AccessMethodInfo *amobj2; + + if (am1 == am2) + return 0; + + amobj1 = findAccessMethodByOid(am1); + amobj2 = findAccessMethodByOid(am2); + + if (!amobj1 || !amobj2) + { + /* catalog corruption: handle like pgTypeNameCompare() does */ + Assert(false); + return 0; + } + + return strcmp(amobj1->dobj.name, amobj2->dobj.name); +} + /* * Sort the given objects into a safe dump order using dependency @@ -347,7 +572,7 @@ sortDumpableObjects(DumpableObject **objs, int numObjs, preDataBoundId = preBoundaryId; postDataBoundId = postBoundaryId; - ordering = (DumpableObject **) pg_malloc(numObjs * sizeof(DumpableObject *)); + ordering = pg_malloc_array(DumpableObject *, numObjs); while (!TopoSort(objs, numObjs, ordering, &nOrdering)) findDependencyLoops(ordering, nOrdering, numObjs); @@ -426,8 +651,8 @@ TopoSort(DumpableObject **objs, * We also make a map showing the input-order index of the item with * dumpId j. */ - beforeConstraints = (int *) pg_malloc0((maxDumpId + 1) * sizeof(int)); - idMap = (int *) pg_malloc((maxDumpId + 1) * sizeof(int)); + beforeConstraints = pg_malloc0_array(int, (maxDumpId + 1)); + idMap = pg_malloc_array(int, (maxDumpId + 1)); for (i = 0; i < numObjs; i++) { obj = objs[i]; @@ -562,9 +787,9 @@ findDependencyLoops(DumpableObject **objs, int nObjs, int totObjs) bool fixedloop; int i; - processed = (bool *) pg_malloc0((getMaxDumpId() + 1) * sizeof(bool)); - searchFailed = (DumpId *) pg_malloc0((getMaxDumpId() + 1) * sizeof(DumpId)); - workspace = (DumpableObject **) pg_malloc(totObjs * sizeof(DumpableObject *)); + processed = pg_malloc0_array(bool, (getMaxDumpId() + 1)); + searchFailed = pg_malloc0_array(DumpId, (getMaxDumpId() + 1)); + workspace = pg_malloc_array(DumpableObject *, totObjs); fixedloop = false; for (i = 0; i < nObjs; i++) @@ -907,7 +1132,7 @@ repairTableAttrDefMultiLoop(DumpableObject *tableobj, } /* - * CHECK constraints on domains work just like those on tables ... + * CHECK, NOT NULL constraints on domains work just like those on tables ... */ static void repairDomainConstraintLoop(DumpableObject *domainobj, @@ -1173,11 +1398,12 @@ repairDependencyLoop(DumpableObject **loop, } } - /* Domain and CHECK constraint */ + /* Domain and CHECK or NOT NULL constraint */ if (nLoop == 2 && loop[0]->objType == DO_TYPE && loop[1]->objType == DO_CONSTRAINT && - ((ConstraintInfo *) loop[1])->contype == 'c' && + (((ConstraintInfo *) loop[1])->contype == 'c' || + ((ConstraintInfo *) loop[1])->contype == 'n') && ((ConstraintInfo *) loop[1])->condomain == (TypeInfo *) loop[0]) { repairDomainConstraintLoop(loop[0], loop[1]); @@ -1186,14 +1412,15 @@ repairDependencyLoop(DumpableObject **loop, if (nLoop == 2 && loop[1]->objType == DO_TYPE && loop[0]->objType == DO_CONSTRAINT && - ((ConstraintInfo *) loop[0])->contype == 'c' && + (((ConstraintInfo *) loop[0])->contype == 'c' || + ((ConstraintInfo *) loop[0])->contype == 'n') && ((ConstraintInfo *) loop[0])->condomain == (TypeInfo *) loop[1]) { repairDomainConstraintLoop(loop[1], loop[0]); return; } - /* Indirect loop involving domain and CHECK constraint */ + /* Indirect loop involving domain and CHECK or NOT NULL constraint */ if (nLoop > 2) { for (i = 0; i < nLoop; i++) @@ -1203,7 +1430,8 @@ repairDependencyLoop(DumpableObject **loop, for (j = 0; j < nLoop; j++) { if (loop[j]->objType == DO_CONSTRAINT && - ((ConstraintInfo *) loop[j])->contype == 'c' && + (((ConstraintInfo *) loop[j])->contype == 'c' || + ((ConstraintInfo *) loop[j])->contype == 'n') && ((ConstraintInfo *) loop[j])->condomain == (TypeInfo *) loop[i]) { repairDomainConstraintMultiLoop(loop[i], loop[j]); diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c index 7f9c302b719ec..9e904f76baa3f 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -1,12 +1,19 @@ /*------------------------------------------------------------------------- * * pg_dumpall.c + * pg_dumpall dumps all databases and global objects (roles and + * tablespaces) from a PostgreSQL cluster. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California + * For text format output, globals are written directly and pg_dump is + * invoked for each database, with all output going to stdout or a file. + * + * For non-text formats (custom, directory, tar), a directory is created + * containing a toc.glo file with global objects, a map.dat file mapping + * database OIDs to names, and a databases/ subdirectory with individual + * pg_dump archives for each database. * - * pg_dumpall forces all pg_dump output to be text, since it also outputs - * text into the same output stream. + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_dump/pg_dumpall.c * @@ -27,9 +34,11 @@ #include "common/string.h" #include "connectdb.h" #include "dumputils.h" +#include "fe_utils/option_utils.h" #include "fe_utils/string_utils.h" #include "filter.h" #include "getopt_long.h" +#include "pg_backup_archiver.h" /* version string we expect back from pg_dump */ #define PGDUMP_VERSIONSTR "pg_dump (PostgreSQL) " PG_VERSION "\n" @@ -65,19 +74,21 @@ static void dropTablespaces(PGconn *conn); static void dumpTablespaces(PGconn *conn); static void dropDBs(PGconn *conn); static void dumpUserConfig(PGconn *conn, const char *username); -static void dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat); +static void dumpDatabases(PGconn *conn); static void dumpTimestamp(const char *msg); -static int runPgDump(const char *dbname, const char *create_opts, - char *dbfile, ArchiveFormat archDumpFormat); +static int runPgDump(const char *dbname, const char *create_opts, char *dbfile); static void buildShSecLabels(PGconn *conn, const char *catalog_name, Oid objectId, const char *objtype, const char *objname, PQExpBuffer buffer); static void executeCommand(PGconn *conn, const char *query); +static void check_for_invalid_global_names(PGconn *conn, + SimpleStringList *database_exclude_names); static void expand_dbname_patterns(PGconn *conn, SimpleStringList *patterns, SimpleStringList *names); static void read_dumpall_filters(const char *filename, SimpleStringList *pattern); static ArchiveFormat parseDumpFormat(const char *format); +static int createDumpId(void); static char pg_dump_bin[MAXPGPATH]; static PQExpBuffer pgdumpopts; @@ -107,8 +118,6 @@ static int no_subscriptions = 0; static int no_toast_compression = 0; static int no_unlogged_table_data = 0; static int no_role_passwords = 0; -static int with_data = 0; -static int with_schema = 0; static int with_statistics = 0; static int server_version; static int load_via_partition_root = 0; @@ -126,6 +135,12 @@ static char *filename = NULL; static SimpleStringList database_exclude_patterns = {NULL, NULL}; static SimpleStringList database_exclude_names = {NULL, NULL}; +static char *restrict_key; +static Archive *fout = NULL; +static int dumpIdVal = 0; +static ArchiveFormat archDumpFormat = archNull; +static const CatalogId nilCatalogId = {0, 0}; + int main(int argc, char *argv[]) { @@ -183,14 +198,13 @@ main(int argc, char *argv[]) {"no-sync", no_argument, NULL, 4}, {"no-toast-compression", no_argument, &no_toast_compression, 1}, {"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1}, - {"with-data", no_argument, &with_data, 1}, - {"with-schema", no_argument, &with_schema, 1}, - {"with-statistics", no_argument, &with_statistics, 1}, {"on-conflict-do-nothing", no_argument, &on_conflict_do_nothing, 1}, {"rows-per-insert", required_argument, NULL, 7}, + {"statistics", no_argument, &with_statistics, 1}, {"statistics-only", no_argument, &statistics_only, 1}, {"filter", required_argument, NULL, 8}, {"sequence-data", no_argument, &sequence_data, 1}, + {"restrict-key", required_argument, NULL, 9}, {NULL, 0, NULL, 0} }; @@ -201,19 +215,19 @@ main(int argc, char *argv[]) char *pgdb = NULL; char *use_role = NULL; const char *dumpencoding = NULL; - ArchiveFormat archDumpFormat = archNull; - const char *formatName = "p"; + const char *format_name = "p"; trivalue prompt_password = TRI_DEFAULT; bool data_only = false; bool globals_only = false; bool roles_only = false; + bool schema_only = false; bool tablespaces_only = false; PGconn *conn; int encoding; - const char *std_strings; int c, ret; int optindex; + DumpOptions dopt; pg_logging_init(argv[0]); pg_logging_set_level(PG_LOG_WARNING); @@ -251,6 +265,7 @@ main(int argc, char *argv[]) } pgdumpopts = createPQExpBuffer(); + InitDumpOptions(&dopt); while ((c = getopt_long(argc, argv, "acd:E:f:F:gh:l:Op:rsS:tU:vwWx", long_options, &optindex)) != -1) { @@ -281,7 +296,7 @@ main(int argc, char *argv[]) appendShellString(pgdumpopts, filename); break; case 'F': - formatName = pg_strdup(optarg); + format_name = pg_strdup(optarg); break; case 'g': globals_only = true; @@ -308,6 +323,7 @@ main(int argc, char *argv[]) break; case 's': + schema_only = true; appendPQExpBufferStr(pgdumpopts, " -s"); break; @@ -322,6 +338,7 @@ main(int argc, char *argv[]) case 'U': pguser = pg_strdup(optarg); + dopt.cparams.username = pg_strdup(optarg); break; case 'v': @@ -382,6 +399,12 @@ main(int argc, char *argv[]) read_dumpall_filters(optarg, &database_exclude_patterns); break; + case 9: + restrict_key = pg_strdup(optarg); + appendPQExpBufferStr(pgdumpopts, " --restrict-key "); + appendShellString(pgdumpopts, optarg); + break; + default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -398,41 +421,50 @@ main(int argc, char *argv[]) exit_nicely(1); } - if (database_exclude_patterns.head != NULL && - (globals_only || roles_only || tablespaces_only)) - { - pg_log_error("option --exclude-database cannot be used together with -g/--globals-only, -r/--roles-only, or -t/--tablespaces-only"); - pg_log_error_hint("Try \"%s --help\" for more information.", progname); - exit_nicely(1); - } - - /* Make sure the user hasn't specified a mix of globals-only options */ - if (globals_only && roles_only) - { - pg_log_error("options -g/--globals-only and -r/--roles-only cannot be used together"); - pg_log_error_hint("Try \"%s --help\" for more information.", progname); - exit_nicely(1); - } - - if (globals_only && tablespaces_only) - { - pg_log_error("options -g/--globals-only and -t/--tablespaces-only cannot be used together"); - pg_log_error_hint("Try \"%s --help\" for more information.", progname); - exit_nicely(1); - } + /* --exclude-database is incompatible with global *-only options */ + check_mut_excl_opts(database_exclude_patterns.head, "--exclude-database", + globals_only, "-g/--globals-only", + roles_only, "-r/--roles-only", + tablespaces_only, "-t/--tablespaces-only"); + + /* *-only options are incompatible with each other */ + check_mut_excl_opts(data_only, "-a/--data-only", + globals_only, "-g/--globals-only", + roles_only, "-r/--roles-only", + schema_only, "-s/--schema-only", + statistics_only, "--statistics-only", + tablespaces_only, "-t/--tablespaces-only"); + + /* --no-* and *-only for same thing are incompatible */ + check_mut_excl_opts(data_only, "-a/--data-only", + no_data, "--no-data"); + check_mut_excl_opts(schema_only, "-s/--schema-only", + no_schema, "--no-schema"); + check_mut_excl_opts(statistics_only, "--statistics-only", + no_statistics, "--no-statistics"); + + /* --statistics and --no-statistics are incompatible */ + check_mut_excl_opts(with_statistics, "--statistics", + no_statistics, "--no-statistics"); + + /* --statistics is incompatible with *-only (except --statistics-only) */ + check_mut_excl_opts(with_statistics, "--statistics", + data_only, "-a/--data-only", + globals_only, "-g/--globals-only", + roles_only, "-r/--roles-only", + schema_only, "-s/--schema-only", + tablespaces_only, "-t/--tablespaces-only"); + + /* --clean and --data-only are incompatible */ + check_mut_excl_opts(output_clean, "-c/--clean", + data_only, "-a/--data-only"); if (if_exists && !output_clean) - pg_fatal("option --if-exists requires option -c/--clean"); - - if (roles_only && tablespaces_only) - { - pg_log_error("options -r/--roles-only and -t/--tablespaces-only cannot be used together"); - pg_log_error_hint("Try \"%s --help\" for more information.", progname); - exit_nicely(1); - } + pg_fatal("option %s requires option %s", + "--if-exists", "-c/--clean"); /* Get format for dump. */ - archDumpFormat = parseDumpFormat(formatName); + archDumpFormat = parseDumpFormat(format_name); /* * If a non-plain format is specified, a file name is also required as the @@ -441,11 +473,22 @@ main(int argc, char *argv[]) if (archDumpFormat != archNull && (!filename || strcmp(filename, "") == 0)) { - pg_log_error("option -F/--format=d|c|t requires option -f/--file"); + pg_log_error("option %s=d|c|t requires option %s", + "-F/--format", "-f/--file"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit_nicely(1); } + /* restrict-key is only supported with --format=plain */ + if (archDumpFormat != archNull && restrict_key) + pg_fatal("option %s can only be used with %s=plain", + "--restrict-key", "--format"); + + /* --clean and -g/--globals-only cannot be used together in non-text dump */ + if (archDumpFormat != archNull && output_clean && globals_only) + pg_fatal("options %s and %s cannot be used together in non-text dump", + "--clean", "-g/--globals-only"); + /* * If password values are not required in the dump, switch to using * pg_roles which is equally useful, just more likely to have unrestricted @@ -497,12 +540,8 @@ main(int argc, char *argv[]) appendPQExpBufferStr(pgdumpopts, " --no-toast-compression"); if (no_unlogged_table_data) appendPQExpBufferStr(pgdumpopts, " --no-unlogged-table-data"); - if (with_data) - appendPQExpBufferStr(pgdumpopts, " --with-data"); - if (with_schema) - appendPQExpBufferStr(pgdumpopts, " --with-schema"); if (with_statistics) - appendPQExpBufferStr(pgdumpopts, " --with-statistics"); + appendPQExpBufferStr(pgdumpopts, " --statistics"); if (on_conflict_do_nothing) appendPQExpBufferStr(pgdumpopts, " --on-conflict-do-nothing"); if (statistics_only) @@ -512,20 +551,14 @@ main(int argc, char *argv[]) /* * Open the output file if required, otherwise use stdout. If required, - * then create new directory and global.dat file. + * then create new directory. */ if (archDumpFormat != archNull) { - char global_path[MAXPGPATH]; + Assert(filename); /* Create new directory or accept the empty existing directory. */ create_or_open_dir(filename); - - snprintf(global_path, MAXPGPATH, "%s/global.dat", filename); - - OPF = fopen(global_path, PG_BINARY_W); - if (!OPF) - pg_fatal("could not open \"%s\": %m", global_path); } else if (filename) { @@ -537,6 +570,16 @@ main(int argc, char *argv[]) else OPF = stdout; + /* + * If you don't provide a restrict key, one will be appointed for you. + */ + if (!restrict_key) + restrict_key = generate_restrict_key(); + if (!restrict_key) + pg_fatal("could not generate restrict key"); + if (!valid_restrict_key(restrict_key)) + pg_fatal("invalid restrict key"); + /* * If there was a database specified on the command line, use that, * otherwise try to connect to database "postgres", and failing that @@ -587,14 +630,17 @@ main(int argc, char *argv[]) } /* - * Get the active encoding and the standard_conforming_strings setting, so - * we know how to escape strings. + * Force standard_conforming_strings on, just in case we are dumping from + * an old server that has it disabled. Without this, literals in views, + * expressions, etc, would be incorrect for modern servers. + */ + executeCommand(conn, "SET standard_conforming_strings = on"); + + /* + * Get the active encoding, so we know how to escape strings. */ encoding = PQclientEncoding(conn); setFmtEncoding(encoding); - std_strings = PQparameterStatus(conn, "standard_conforming_strings"); - if (!std_strings) - std_strings = "off"; /* Set the role if requested */ if (use_role) @@ -610,37 +656,137 @@ main(int argc, char *argv[]) if (quote_all_identifiers) executeCommand(conn, "SET quote_all_identifiers = true"); - fprintf(OPF, "--\n-- PostgreSQL database cluster dump\n--\n\n"); - if (verbose) - dumpTimestamp("Started on"); + /* create a archive file for global commands. */ + if (archDumpFormat != archNull) + { + PQExpBuffer qry = createPQExpBuffer(); + char global_path[MAXPGPATH]; + const char *encname; + pg_compress_specification compression_spec = {0}; - /* - * We used to emit \connect postgres here, but that served no purpose - * other than to break things for installations without a postgres - * database. Everything we're restoring here is a global, so whichever - * database we're connected to at the moment is fine. - */ + /* + * Check that no global object names contain newlines or carriage + * returns, which would break the map.dat file format. This is only + * needed for servers older than v19, which started prohibiting such + * names. + */ + if (server_version < 190000) + check_for_invalid_global_names(conn, &database_exclude_names); + + /* Set file path for global sql commands. */ + snprintf(global_path, MAXPGPATH, "%s/toc.glo", filename); + + /* Open the output file */ + fout = CreateArchive(global_path, archCustom, compression_spec, + dosync, archModeWrite, NULL, DATA_DIR_SYNC_METHOD_FSYNC); + + /* Make dump options accessible right away */ + SetArchiveOptions(fout, &dopt, NULL); + + ((ArchiveHandle *) fout)->connection = conn; + ((ArchiveHandle *) fout)->public.numWorkers = 1; + + /* Register the cleanup hook */ + on_exit_close_archive(fout); + + /* Let the archiver know how noisy to be */ + fout->verbose = verbose; + + /* + * We allow the server to be back to 9.2, and up to any minor release + * of our own major version. (See also version check in + * pg_dumpall.c.) + */ + fout->minRemoteVersion = 90200; + fout->maxRemoteVersion = (PG_VERSION_NUM / 100) * 100 + 99; + fout->numWorkers = 1; + + /* Dump default_transaction_read_only. */ + appendPQExpBufferStr(qry, "SET default_transaction_read_only = off;\n\n"); + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = "default_transaction_read_only", + .description = "default_transaction_read_only", + .section = SECTION_PRE_DATA, + .createStmt = qry->data)); + resetPQExpBuffer(qry); + + /* Put the correct encoding into the archive */ + encname = pg_encoding_to_char(encoding); + + appendPQExpBufferStr(qry, "SET client_encoding = "); + appendStringLiteralAH(qry, encname, fout); + appendPQExpBufferStr(qry, ";\n"); + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = "client_encoding", + .description = "client_encoding", + .section = SECTION_PRE_DATA, + .createStmt = qry->data)); + resetPQExpBuffer(qry); + + /* Put the correct escape string behavior into the archive. */ + appendPQExpBufferStr(qry, "SET standard_conforming_strings = 'on';\n"); + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = "standard_conforming_strings", + .description = "standard_conforming_strings", + .section = SECTION_PRE_DATA, + .createStmt = qry->data)); + destroyPQExpBuffer(qry); + } + else + { + fprintf(OPF, "--\n-- PostgreSQL database cluster dump\n--\n\n"); + + if (verbose) + dumpTimestamp("Started on"); + + /* + * Enter restricted mode to block any unexpected psql meta-commands. A + * malicious source might try to inject a variety of things via bogus + * responses to queries. While we cannot prevent such sources from + * affecting the destination at restore time, we can block psql + * meta-commands so that the client machine that runs psql with the + * dump output remains unaffected. + */ + fprintf(OPF, "\\restrict %s\n\n", restrict_key); - /* Restore will need to write to the target cluster */ - fprintf(OPF, "SET default_transaction_read_only = off;\n\n"); + /* + * We used to emit \connect postgres here, but that served no purpose + * other than to break things for installations without a postgres + * database. Everything we're restoring here is a global, so + * whichever database we're connected to at the moment is fine. + */ - /* Replicate encoding and std_strings in output */ - fprintf(OPF, "SET client_encoding = '%s';\n", - pg_encoding_to_char(encoding)); - fprintf(OPF, "SET standard_conforming_strings = %s;\n", std_strings); - if (strcmp(std_strings, "off") == 0) - fprintf(OPF, "SET escape_string_warning = off;\n"); - fprintf(OPF, "\n"); + /* Restore will need to write to the target cluster */ + fprintf(OPF, "SET default_transaction_read_only = off;\n\n"); + + /* Replicate encoding and standard_conforming_strings in output */ + fprintf(OPF, "SET client_encoding = '%s';\n", + pg_encoding_to_char(encoding)); + fprintf(OPF, "SET standard_conforming_strings = on;\n"); + fprintf(OPF, "\n"); + } - if (!data_only) + if (!data_only && !statistics_only && !no_schema) { /* * If asked to --clean, do that first. We can avoid detailed * dependency analysis because databases never depend on each other, * and tablespaces never depend on each other. Roles could have * grants to each other, but DROP ROLE will clean those up silently. + * + * For non-text formats, pg_dumpall unconditionally process --clean + * option. In contrast, pg_restore only applies it if the user + * explicitly provides the flag. This discrepancy resolves corner + * cases where pg_restore requires cleanup instructions that may be + * missing from a standard pg_dumpall output. */ - if (output_clean) + if (output_clean || archDumpFormat != archNull) { if (!globals_only && !roles_only && !tablespaces_only) dropDBs(conn); @@ -674,22 +820,45 @@ main(int argc, char *argv[]) dumpTablespaces(conn); } + if (archDumpFormat == archNull) + { + /* + * Exit restricted mode just before dumping the databases. pg_dump + * will handle entering restricted mode again as appropriate. + */ + fprintf(OPF, "\\unrestrict %s\n\n", restrict_key); + } + if (!globals_only && !roles_only && !tablespaces_only) - dumpDatabases(conn, archDumpFormat); + dumpDatabases(conn); - PQfinish(conn); + if (archDumpFormat == archNull) + { + PQfinish(conn); - if (verbose) - dumpTimestamp("Completed on"); - fprintf(OPF, "--\n-- PostgreSQL database cluster dump complete\n--\n\n"); + if (verbose) + dumpTimestamp("Completed on"); + fprintf(OPF, "--\n-- PostgreSQL database cluster dump complete\n--\n\n"); - if (filename) + if (filename) + { + fclose(OPF); + + /* sync the resulting file, errors are not fatal */ + if (dosync) + (void) fsync_fname(filename, false); + } + } + else { - fclose(OPF); + RestoreOptions *ropt; + + ropt = NewRestoreOptions(); + SetArchiveOptions(fout, &dopt, ropt); - /* sync the resulting file, errors are not fatal */ - if (dosync && (archDumpFormat == archNull)) - (void) fsync_fname(filename, false); + /* Mark which entries should be output */ + ProcessArchiveRestoreOptions(fout); + CloseArchive(fout); } exit_nicely(0); @@ -699,7 +868,7 @@ main(int argc, char *argv[]) static void help(void) { - printf(_("%s extracts a PostgreSQL database cluster based on specified dump format.\n\n"), progname); + printf(_("%s exports a PostgreSQL database cluster as an SQL script or to other formats.\n\n"), progname); printf(_("Usage:\n")); printf(_(" %s [OPTION]...\n"), progname); @@ -748,15 +917,14 @@ help(void) printf(_(" --no-unlogged-table-data do not dump unlogged table data\n")); printf(_(" --on-conflict-do-nothing add ON CONFLICT DO NOTHING to INSERT commands\n")); printf(_(" --quote-all-identifiers quote all identifiers, even if not key words\n")); + printf(_(" --restrict-key=RESTRICT_KEY use provided string as psql \\restrict key\n")); printf(_(" --rows-per-insert=NROWS number of rows per INSERT; implies --inserts\n")); printf(_(" --sequence-data include sequence data in dump\n")); + printf(_(" --statistics dump the statistics\n")); printf(_(" --statistics-only dump only the statistics, not schema or data\n")); printf(_(" --use-set-session-authorization\n" " use SET SESSION AUTHORIZATION commands instead of\n" " ALTER OWNER commands to set ownership\n")); - printf(_(" --with-data dump the data\n")); - printf(_(" --with-schema dump the schema\n")); - printf(_(" --with-statistics dump the statistics\n")); printf(_("\nConnection options:\n")); printf(_(" -d, --dbname=CONNSTR connect using connection string\n")); @@ -802,24 +970,45 @@ dropRoles(PGconn *conn) i_rolname = PQfnumber(res, "rolname"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Drop roles\n--\n\n"); for (i = 0; i < PQntuples(res); i++) { const char *rolename; + PQExpBuffer delQry = createPQExpBuffer(); rolename = PQgetvalue(res, i, i_rolname); - fprintf(OPF, "DROP ROLE %s%s;\n", - if_exists ? "IF EXISTS " : "", - fmtId(rolename)); + if (archDumpFormat == archNull) + { + appendPQExpBuffer(delQry, "DROP ROLE %s%s;\n", + if_exists ? "IF EXISTS " : "", + fmtId(rolename)); + fprintf(OPF, "%s", delQry->data); + } + else + { + appendPQExpBuffer(delQry, "DROP ROLE IF EXISTS %s;\n", + fmtId(rolename)); + + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = psprintf("ROLE %s", fmtId(rolename)), + .description = "DROP_GLOBAL", + .section = SECTION_PRE_DATA, + .createStmt = delQry->data)); + } + + destroyPQExpBuffer(delQry); } PQclear(res); destroyPQExpBuffer(buf); - fprintf(OPF, "\n\n"); + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); } /* @@ -829,6 +1018,8 @@ static void dumpRoles(PGconn *conn) { PQExpBuffer buf = createPQExpBuffer(); + PQExpBuffer comment_buf = createPQExpBuffer(); + PQExpBuffer seclabel_buf = createPQExpBuffer(); PGresult *res; int i_oid, i_rolname, @@ -900,7 +1091,7 @@ dumpRoles(PGconn *conn) i_rolcomment = PQfnumber(res, "rolcomment"); i_is_current_user = PQfnumber(res, "is_current_user"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Roles\n--\n\n"); for (i = 0; i < PQntuples(res); i++) @@ -919,6 +1110,8 @@ dumpRoles(PGconn *conn) } resetPQExpBuffer(buf); + resetPQExpBuffer(comment_buf); + resetPQExpBuffer(seclabel_buf); if (binary_upgrade) { @@ -995,17 +1188,53 @@ dumpRoles(PGconn *conn) if (!no_comments && !PQgetisnull(res, i, i_rolcomment)) { - appendPQExpBuffer(buf, "COMMENT ON ROLE %s IS ", fmtId(rolename)); - appendStringLiteralConn(buf, PQgetvalue(res, i, i_rolcomment), conn); - appendPQExpBufferStr(buf, ";\n"); + appendPQExpBuffer(comment_buf, "COMMENT ON ROLE %s IS ", fmtId(rolename)); + appendStringLiteralConn(comment_buf, PQgetvalue(res, i, i_rolcomment), conn); + appendPQExpBufferStr(comment_buf, ";\n"); } if (!no_security_labels) buildShSecLabels(conn, "pg_authid", auth_oid, "ROLE", rolename, - buf); + seclabel_buf); + + if (archDumpFormat == archNull) + { + fprintf(OPF, "%s", buf->data); + fprintf(OPF, "%s", comment_buf->data); - fprintf(OPF, "%s", buf->data); + if (seclabel_buf->data[0] != '\0') + fprintf(OPF, "%s", seclabel_buf->data); + } + else + { + char *tag = psprintf("ROLE %s", fmtId(rolename)); + + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = tag, + .description = "ROLE", + .section = SECTION_PRE_DATA, + .createStmt = buf->data)); + if (comment_buf->data[0] != '\0') + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = tag, + .description = "COMMENT", + .section = SECTION_PRE_DATA, + .createStmt = comment_buf->data)); + + if (seclabel_buf->data[0] != '\0') + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = tag, + .description = "SECURITY LABEL", + .section = SECTION_PRE_DATA, + .createStmt = seclabel_buf->data)); + } } /* @@ -1013,14 +1242,20 @@ dumpRoles(PGconn *conn) * We do it this way because config settings for roles could mention the * names of other roles. */ + if (PQntuples(res) > 0 && archDumpFormat == archNull) + fprintf(OPF, "\n--\n-- User Configurations\n--\n"); + for (i = 0; i < PQntuples(res); i++) dumpUserConfig(conn, PQgetvalue(res, i, i_rolname)); PQclear(res); - fprintf(OPF, "\n\n"); + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); destroyPQExpBuffer(buf); + destroyPQExpBuffer(comment_buf); + destroyPQExpBuffer(seclabel_buf); } @@ -1034,6 +1269,7 @@ static void dumpRoleMembership(PGconn *conn) { PQExpBuffer buf = createPQExpBuffer(); + PQExpBuffer querybuf = createPQExpBuffer(); PQExpBuffer optbuf = createPQExpBuffer(); PGresult *res; int start = 0, @@ -1062,7 +1298,7 @@ dumpRoleMembership(PGconn *conn) * that no longer exist. If we find such cases, print a warning and skip * the entry. */ - dump_grantors = (PQserverVersion(conn) >= 160000); + dump_grantors = (server_version >= 160000); /* * Previous versions of PostgreSQL also did not have grant-level options. @@ -1096,7 +1332,7 @@ dumpRoleMembership(PGconn *conn) i_inherit_option = PQfnumber(res, "inherit_option"); i_set_option = PQfnumber(res, "set_option"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Role memberships\n--\n\n"); /* @@ -1127,7 +1363,7 @@ dumpRoleMembership(PGconn *conn) if (PQgetisnull(res, start, i_role)) { /* translator: %s represents a numeric role OID */ - pg_log_warning("found orphaned pg_auth_members entry for role %s", + pg_log_warning("ignoring role grant for missing role with OID %s", PQgetvalue(res, start, i_roleid)); break; } @@ -1143,7 +1379,12 @@ dumpRoleMembership(PGconn *conn) } remaining = end - start; - done = pg_malloc0(remaining * sizeof(bool)); + done = pg_malloc0_array(bool, remaining); + + /* + * We use a hashtable to track the member names that have been granted + * admin option. Usually a hashtable is overkill, but sometimes not. + */ ht = rolename_create(remaining, NULL); /* @@ -1171,50 +1412,56 @@ dumpRoleMembership(PGconn *conn) for (i = start; i < end; ++i) { char *member; - char *admin_option; char *grantorid; - char *grantor; + char *grantor = NULL; + bool dump_this_grantor = dump_grantors; char *set_option = "true"; + char *admin_option; bool found; /* If we already did this grant, don't do it again. */ if (done[i - start]) continue; - /* Complain about, then ignore, entries with orphaned OIDs. */ + /* Complain about, then ignore, entries for unknown members. */ if (PQgetisnull(res, i, i_member)) { /* translator: %s represents a numeric role OID */ - pg_log_warning("found orphaned pg_auth_members entry for role %s", + pg_log_warning("ignoring role grant to missing role with OID %s", PQgetvalue(res, i, i_memberid)); done[i - start] = true; --remaining; continue; } - if (PQgetisnull(res, i, i_grantor)) + member = PQgetvalue(res, i, i_member); + + /* If the grantor is unknown, complain and dump without it. */ + grantorid = PQgetvalue(res, i, i_grantorid); + if (dump_this_grantor) { - /* translator: %s represents a numeric role OID */ - pg_log_warning("found orphaned pg_auth_members entry for role %s", - PQgetvalue(res, i, i_grantorid)); - done[i - start] = true; - --remaining; - continue; + if (PQgetisnull(res, i, i_grantor)) + { + /* translator: %s represents a numeric role OID */ + pg_log_warning("grant of role \"%s\" to \"%s\" has invalid grantor OID %s", + role, member, grantorid); + pg_log_warning_detail("This grant will be dumped without GRANTED BY."); + dump_this_grantor = false; + } + else + grantor = PQgetvalue(res, i, i_grantor); } - member = PQgetvalue(res, i, i_member); - grantor = PQgetvalue(res, i, i_grantor); - grantorid = PQgetvalue(res, i, i_grantorid); admin_option = PQgetvalue(res, i, i_admin_option); if (dump_grant_options) set_option = PQgetvalue(res, i, i_set_option); /* - * If we're not dumping grantors or if the grantor is the + * If we're not dumping the grantor or if the grantor is the * bootstrap superuser, it's fine to dump this now. Otherwise, * it's got to be someone who has already been granted ADMIN * OPTION. */ - if (dump_grantors && + if (dump_this_grantor && atooid(grantorid) != BOOTSTRAP_SUPERUSERID && rolename_lookup(ht, grantor) == NULL) continue; @@ -1232,8 +1479,9 @@ dumpRoleMembership(PGconn *conn) /* Generate the actual GRANT statement. */ resetPQExpBuffer(optbuf); - fprintf(OPF, "GRANT %s", fmtId(role)); - fprintf(OPF, " TO %s", fmtId(member)); + resetPQExpBuffer(querybuf); + appendPQExpBuffer(querybuf, "GRANT %s", fmtId(role)); + appendPQExpBuffer(querybuf, " TO %s", fmtId(member)); if (*admin_option == 't') appendPQExpBufferStr(optbuf, "ADMIN OPTION"); if (dump_grant_options) @@ -1254,10 +1502,21 @@ dumpRoleMembership(PGconn *conn) appendPQExpBufferStr(optbuf, "SET FALSE"); } if (optbuf->data[0] != '\0') - fprintf(OPF, " WITH %s", optbuf->data); - if (dump_grantors) - fprintf(OPF, " GRANTED BY %s", fmtId(grantor)); - fprintf(OPF, ";\n"); + appendPQExpBuffer(querybuf, " WITH %s", optbuf->data); + if (dump_this_grantor) + appendPQExpBuffer(querybuf, " GRANTED BY %s", fmtId(grantor)); + appendPQExpBufferStr(querybuf, ";\n"); + + if (archDumpFormat == archNull) + fprintf(OPF, "%s", querybuf->data); + else + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = psprintf("ROLE %s", fmtId(role)), + .description = "ROLE PROPERTIES", + .section = SECTION_PRE_DATA, + .createStmt = querybuf->data)); } } @@ -1268,8 +1527,11 @@ dumpRoleMembership(PGconn *conn) PQclear(res); destroyPQExpBuffer(buf); + destroyPQExpBuffer(querybuf); + destroyPQExpBuffer(optbuf); - fprintf(OPF, "\n\n"); + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); } @@ -1296,7 +1558,7 @@ dumpRoleGUCPrivs(PGconn *conn) "FROM pg_catalog.pg_parameter_acl " "ORDER BY 1"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Role privileges on configuration parameters\n--\n\n"); for (i = 0; i < PQntuples(res); i++) @@ -1321,14 +1583,25 @@ dumpRoleGUCPrivs(PGconn *conn) exit_nicely(1); } - fprintf(OPF, "%s", buf->data); + if (archDumpFormat == archNull) + fprintf(OPF, "%s", buf->data); + else + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = psprintf("ROLE %s", fmtId(parowner)), + .description = "ROLE PROPERTIES", + .section = SECTION_PRE_DATA, + .createStmt = buf->data)); free(fparname); destroyPQExpBuffer(buf); } PQclear(res); - fprintf(OPF, "\n\n"); + + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); } @@ -1350,21 +1623,41 @@ dropTablespaces(PGconn *conn) "WHERE spcname !~ '^pg_' " "ORDER BY 1"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Drop tablespaces\n--\n\n"); for (i = 0; i < PQntuples(res); i++) { char *spcname = PQgetvalue(res, i, 0); + PQExpBuffer delQry = createPQExpBuffer(); + + if (archDumpFormat == archNull) + { + appendPQExpBuffer(delQry, "DROP TABLESPACE %s%s;\n", + if_exists ? "IF EXISTS " : "", + fmtId(spcname)); + fprintf(OPF, "%s", delQry->data); + } + else + { + appendPQExpBuffer(delQry, "DROP TABLESPACE IF EXISTS %s;\n", + fmtId(spcname)); + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = psprintf("TABLESPACE %s", fmtId(spcname)), + .description = "DROP_GLOBAL", + .section = SECTION_PRE_DATA, + .createStmt = delQry->data)); + } - fprintf(OPF, "DROP TABLESPACE %s%s;\n", - if_exists ? "IF EXISTS " : "", - fmtId(spcname)); + destroyPQExpBuffer(delQry); } PQclear(res); - fprintf(OPF, "\n\n"); + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); } /* @@ -1374,6 +1667,8 @@ static void dumpTablespaces(PGconn *conn) { PGresult *res; + PQExpBuffer comment_buf = createPQExpBuffer(); + PQExpBuffer seclabel_buf = createPQExpBuffer(); int i; /* @@ -1390,7 +1685,7 @@ dumpTablespaces(PGconn *conn) "WHERE spcname !~ '^pg_' " "ORDER BY 1"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Tablespaces\n--\n\n"); for (i = 0; i < PQntuples(res); i++) @@ -1409,6 +1704,9 @@ dumpTablespaces(PGconn *conn) /* needed for buildACLCommands() */ fspcname = pg_strdup(fmtId(spcname)); + resetPQExpBuffer(comment_buf); + resetPQExpBuffer(seclabel_buf); + if (binary_upgrade) { appendPQExpBufferStr(buf, "\n-- For binary upgrade, must preserve pg_tablespace oid\n"); @@ -1450,24 +1748,67 @@ dumpTablespaces(PGconn *conn) if (!no_comments && spccomment && spccomment[0] != '\0') { - appendPQExpBuffer(buf, "COMMENT ON TABLESPACE %s IS ", fspcname); - appendStringLiteralConn(buf, spccomment, conn); - appendPQExpBufferStr(buf, ";\n"); + appendPQExpBuffer(comment_buf, "COMMENT ON TABLESPACE %s IS ", fspcname); + appendStringLiteralConn(comment_buf, spccomment, conn); + appendPQExpBufferStr(comment_buf, ";\n"); } if (!no_security_labels) buildShSecLabels(conn, "pg_tablespace", spcoid, "TABLESPACE", spcname, - buf); + seclabel_buf); - fprintf(OPF, "%s", buf->data); + if (archDumpFormat == archNull) + { + fprintf(OPF, "%s", buf->data); + + if (comment_buf->data[0] != '\0') + fprintf(OPF, "%s", comment_buf->data); + + if (seclabel_buf->data[0] != '\0') + fprintf(OPF, "%s", seclabel_buf->data); + } + else + { + char *tag = psprintf("TABLESPACE %s", fmtId(fspcname)); + + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = tag, + .description = "TABLESPACE", + .section = SECTION_PRE_DATA, + .createStmt = buf->data)); + + if (comment_buf->data[0] != '\0') + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = tag, + .description = "COMMENT", + .section = SECTION_PRE_DATA, + .createStmt = comment_buf->data)); + + if (seclabel_buf->data[0] != '\0') + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = tag, + .description = "SECURITY LABEL", + .section = SECTION_PRE_DATA, + .createStmt = seclabel_buf->data)); + } free(fspcname); destroyPQExpBuffer(buf); } PQclear(res); - fprintf(OPF, "\n\n"); + destroyPQExpBuffer(comment_buf); + destroyPQExpBuffer(seclabel_buf); + + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); } @@ -1490,7 +1831,7 @@ dropDBs(PGconn *conn) "WHERE datallowconn AND datconnlimit != -2 " "ORDER BY datname"); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Drop databases (except postgres and template1)\n--\n\n"); for (i = 0; i < PQntuples(res); i++) @@ -1506,15 +1847,33 @@ dropDBs(PGconn *conn) strcmp(dbname, "template0") != 0 && strcmp(dbname, "postgres") != 0) { - fprintf(OPF, "DROP DATABASE %s%s;\n", - if_exists ? "IF EXISTS " : "", - fmtId(dbname)); + if (archDumpFormat == archNull) + { + fprintf(OPF, "DROP DATABASE %s%s;\n", + if_exists ? "IF EXISTS " : "", + fmtId(dbname)); + } + else + { + char *stmt = psprintf("DROP DATABASE IF EXISTS %s;\n", + fmtId(dbname)); + + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = psprintf("DATABASE %s", fmtId(dbname)), + .description = "DROP_GLOBAL", + .section = SECTION_PRE_DATA, + .createStmt = stmt)); + pg_free(stmt); + } } } PQclear(res); - fprintf(OPF, "\n\n"); + if (archDumpFormat == archNull) + fprintf(OPF, "\n\n"); } @@ -1526,7 +1885,6 @@ dumpUserConfig(PGconn *conn, const char *username) { PQExpBuffer buf = createPQExpBuffer(); PGresult *res; - static bool header_done = false; printfPQExpBuffer(buf, "SELECT unnest(setconfig) FROM pg_db_role_setting " "WHERE setdatabase = 0 AND setrole = " @@ -1537,13 +1895,13 @@ dumpUserConfig(PGconn *conn, const char *username) res = executeQuery(conn, buf->data); - if (PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) { - if (!header_done) - fprintf(OPF, "\n--\n-- User Configurations\n--\n"); - header_done = true; + char *sanitized; - fprintf(OPF, "\n--\n-- User Config \"%s\"\n--\n\n", username); + sanitized = sanitize_line(username, true); + fprintf(OPF, "\n--\n-- User Config \"%s\"\n--\n\n", sanitized); + free(sanitized); } for (int i = 0; i < PQntuples(res); i++) @@ -1552,7 +1910,17 @@ dumpUserConfig(PGconn *conn, const char *username) makeAlterConfigCommand(conn, PQgetvalue(res, i, 0), "ROLE", username, NULL, NULL, buf); - fprintf(OPF, "%s", buf->data); + + if (archDumpFormat == archNull) + fprintf(OPF, "%s", buf->data); + else + ArchiveEntry(fout, + nilCatalogId, /* catalog ID */ + createDumpId(), /* dump ID */ + ARCHIVE_OPTS(.tag = psprintf("ROLE %s", fmtId(username)), + .description = "ROLE PROPERTIES", + .section = SECTION_PRE_DATA, + .createStmt = buf->data)); } PQclear(res); @@ -1618,7 +1986,7 @@ expand_dbname_patterns(PGconn *conn, * Dump contents of databases. */ static void -dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat) +dumpDatabases(PGconn *conn) { PGresult *res; int i; @@ -1643,7 +2011,7 @@ dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat) "WHERE datallowconn AND datconnlimit != -2 " "ORDER BY (datname <> 'template1'), datname"); - if (archDumpFormat == archNull && PQntuples(res) > 0) + if (PQntuples(res) > 0 && archDumpFormat == archNull) fprintf(OPF, "--\n-- Databases\n--\n\n"); /* @@ -1659,19 +2027,32 @@ dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat) /* Create a subdirectory with 'databases' name under main directory. */ if (mkdir(db_subdir, pg_dir_create_mode) != 0) - pg_fatal("could not create subdirectory \"%s\": %m", db_subdir); + pg_fatal("could not create directory \"%s\": %m", db_subdir); snprintf(map_file_path, MAXPGPATH, "%s/map.dat", filename); /* Create a map file (to store dboid and dbname) */ map_file = fopen(map_file_path, PG_BINARY_W); if (!map_file) - pg_fatal("could not open map file: %s", strerror(errno)); + pg_fatal("could not open file \"%s\": %m", map_file_path); + + fprintf(map_file, + "#################################################################\n" + "# map.dat\n" + "#\n" + "# This file maps oids to database names\n" + "#\n" + "# pg_restore will restore all the databases listed here, unless\n" + "# otherwise excluded. You can also inhibit restoration of a\n" + "# database by removing the line or commenting out the line with\n" + "# a # mark.\n" + "#################################################################\n"); } for (i = 0; i < PQntuples(res); i++) { char *dbname = PQgetvalue(res, i, 0); + char *sanitized; char *oid = PQgetvalue(res, i, 1); const char *create_opts = ""; int ret; @@ -1687,27 +2068,14 @@ dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat) continue; } - /* - * If this is not a plain format dump, then append dboid and dbname to - * the map.dat file. - */ - if (archDumpFormat != archNull) - { - if (archDumpFormat == archCustom) - snprintf(dbfilepath, MAXPGPATH, "\"%s\"/\"%s\".dmp", db_subdir, oid); - else if (archDumpFormat == archTar) - snprintf(dbfilepath, MAXPGPATH, "\"%s\"/\"%s\".tar", db_subdir, oid); - else - snprintf(dbfilepath, MAXPGPATH, "\"%s\"/\"%s\"", db_subdir, oid); - - /* Put one line entry for dboid and dbname in map file. */ - fprintf(map_file, "%s %s\n", oid, dbname); - } - pg_log_info("dumping database \"%s\"", dbname); + sanitized = sanitize_line(dbname, true); + if (archDumpFormat == archNull) - fprintf(OPF, "--\n-- Database \"%s\" dump\n--\n\n", dbname); + fprintf(OPF, "--\n-- Database \"%s\" dump\n--\n\n", sanitized); + + free(sanitized); /* * We assume that "template1" and "postgres" already exist in the @@ -1724,30 +2092,42 @@ dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat) /* Since pg_dump won't emit a \connect command, we must */ else if (archDumpFormat == archNull) fprintf(OPF, "\\connect %s\n\n", dbname); + else + create_opts = ""; } else create_opts = "--create"; - if (filename) + if (filename && archDumpFormat == archNull) fclose(OPF); - ret = runPgDump(dbname, create_opts, dbfilepath, archDumpFormat); + /* + * If this is not a plain format dump, then append dboid and dbname to + * the map.dat file. + */ + if (archDumpFormat != archNull) + { + if (archDumpFormat == archCustom) + snprintf(dbfilepath, MAXPGPATH, "\"%s\"/\"%s\".dmp", db_subdir, oid); + else if (archDumpFormat == archTar) + snprintf(dbfilepath, MAXPGPATH, "\"%s\"/\"%s\".tar", db_subdir, oid); + else + snprintf(dbfilepath, MAXPGPATH, "\"%s\"/\"%s\"", db_subdir, oid); + + /* Put one line entry for dboid and dbname in map file. */ + fprintf(map_file, "%s %s\n", oid, dbname); + } + + ret = runPgDump(dbname, create_opts, dbfilepath); if (ret != 0) pg_fatal("pg_dump failed on database \"%s\", exiting", dbname); - if (filename) + if (filename && archDumpFormat == archNull) { - char global_path[MAXPGPATH]; - - if (archDumpFormat != archNull) - snprintf(global_path, MAXPGPATH, "%s/global.dat", filename); - else - snprintf(global_path, MAXPGPATH, "%s", filename); - - OPF = fopen(global_path, PG_BINARY_A); + OPF = fopen(filename, PG_BINARY_A); if (!OPF) pg_fatal("could not re-open the output file \"%s\": %m", - global_path); + filename); } } @@ -1764,8 +2144,7 @@ dumpDatabases(PGconn *conn, ArchiveFormat archDumpFormat) * Run pg_dump on dbname, with specified options. */ static int -runPgDump(const char *dbname, const char *create_opts, char *dbfile, - ArchiveFormat archDumpFormat) +runPgDump(const char *dbname, const char *create_opts, char *dbfile) { PQExpBufferData connstrbuf; PQExpBufferData cmd; @@ -1780,8 +2159,8 @@ runPgDump(const char *dbname, const char *create_opts, char *dbfile, */ if (archDumpFormat != archNull) { - printfPQExpBuffer(&cmd, "\"%s\" -f %s %s", pg_dump_bin, - dbfile, create_opts); + printfPQExpBuffer(&cmd, "\"%s\" %s -f %s %s", pg_dump_bin, + pgdumpopts->data, dbfile, create_opts); if (archDumpFormat == archDirectory) appendPQExpBufferStr(&cmd, " --format=directory "); @@ -1877,6 +2256,76 @@ executeCommand(PGconn *conn, const char *query) } +/* + * check_for_invalid_global_names + * + * Check that no database, role, or tablespace name contains a newline or + * carriage return character. Such characters in database names would break + * the map.dat file format used for non-plain-text dumps. Role and tablespace + * names are also checked because such characters were forbidden starting in + * v19. + * + * Excluded databases are skipped since they won't appear in map.dat. + */ +static void +check_for_invalid_global_names(PGconn *conn, + SimpleStringList *database_exclude_names) +{ + PGresult *res; + int i; + PQExpBuffer names; + int count = 0; + + res = executeQuery(conn, + "SELECT datname AS objname, 'database' AS objtype " + "FROM pg_catalog.pg_database " + "WHERE datallowconn AND datconnlimit != -2 " + "UNION ALL " + "SELECT rolname AS objname, 'role' AS objtype " + "FROM pg_catalog.pg_roles " + "UNION ALL " + "SELECT spcname AS objname, 'tablespace' AS objtype " + "FROM pg_catalog.pg_tablespace"); + + names = createPQExpBuffer(); + + for (i = 0; i < PQntuples(res); i++) + { + char *objname = PQgetvalue(res, i, 0); + char *objtype = PQgetvalue(res, i, 1); + + /* Skip excluded databases since they won't be in map.dat */ + if (strcmp(objtype, "database") == 0 && + simple_string_list_member(database_exclude_names, objname)) + continue; + + if (strpbrk(objname, "\n\r")) + { + appendPQExpBuffer(names, " %s: \"", objtype); + for (char *p = objname; *p; p++) + { + if (*p == '\n') + appendPQExpBufferStr(names, "\\n"); + else if (*p == '\r') + appendPQExpBufferStr(names, "\\r"); + else + appendPQExpBufferChar(names, *p); + } + appendPQExpBufferStr(names, "\"\n"); + count++; + } + } + + PQclear(res); + + if (count > 0) + pg_fatal("database, role, or tablespace names contain a newline or carriage return character, which is not supported in non-plain-text dumps:\n%s", + names->data); + + destroyPQExpBuffer(names); +} + + /* * dumpTimestamp */ @@ -1976,8 +2425,19 @@ parseDumpFormat(const char *format) else if (pg_strcasecmp(format, "tar") == 0) archDumpFormat = archTar; else - pg_fatal("unrecognized archive format \"%s\"; please specify \"c\", \"d\", \"p\", or \"t\"", + pg_fatal("unrecognized output format \"%s\"; please specify \"c\", \"d\", \"p\", or \"t\"", format); return archDumpFormat; } + +/* + * createDumpId + * + * Return the next dumpId. + */ +static int +createDumpId(void) +{ + return ++dumpIdVal; +} diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c index f2182e9182560..95f4ac110b93a 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -48,6 +48,7 @@ #include "common/string.h" #include "connectdb.h" +#include "dumputils.h" #include "fe_utils/option_utils.h" #include "fe_utils/string_utils.h" #include "filter.h" @@ -59,13 +60,11 @@ static void usage(const char *progname); static void read_restore_filters(const char *filename, RestoreOptions *opts); static bool file_exists_in_directory(const char *dir, const char *filename); static int restore_one_database(const char *inputFileSpec, RestoreOptions *opts, - int numWorkers, bool append_data, int num); -static int read_one_statement(StringInfo inBuf, FILE *pfile); -static int restore_all_databases(PGconn *conn, const char *dumpdirpath, + int numWorkers, bool append_data); +static int restore_global_objects(const char *inputFileSpec, RestoreOptions *opts); + +static int restore_all_databases(const char *inputFileSpec, SimpleStringList db_exclude_patterns, RestoreOptions *opts, int numWorkers); -static int process_global_sql_commands(PGconn *conn, const char *dumpdirpath, - const char *outfile); -static void copy_or_print_global_file(const char *outfile, FILE *pfile); static int get_dbnames_list_to_restore(PGconn *conn, SimplePtrList *dbname_oid_list, SimpleStringList db_exclude_patterns); @@ -108,11 +107,10 @@ main(int argc, char **argv) static int no_schema = 0; static int no_security_labels = 0; static int no_statistics = 0; + static int no_globals = 0; static int no_subscriptions = 0; static int strict_names = 0; static int statistics_only = 0; - static int with_data = 0; - static int with_schema = 0; static int with_statistics = 0; struct option cmdopts[] = { @@ -167,14 +165,14 @@ main(int argc, char **argv) {"no-publications", no_argument, &no_publications, 1}, {"no-schema", no_argument, &no_schema, 1}, {"no-security-labels", no_argument, &no_security_labels, 1}, + {"no-globals", no_argument, &no_globals, 1}, {"no-subscriptions", no_argument, &no_subscriptions, 1}, {"no-statistics", no_argument, &no_statistics, 1}, - {"with-data", no_argument, &with_data, 1}, - {"with-schema", no_argument, &with_schema, 1}, - {"with-statistics", no_argument, &with_statistics, 1}, + {"statistics", no_argument, &with_statistics, 1}, {"statistics-only", no_argument, &statistics_only, 1}, {"filter", required_argument, NULL, 4}, - {"exclude-database", required_argument, NULL, 6}, + {"restrict-key", required_argument, NULL, 6}, + {"exclude-database", required_argument, NULL, 7}, {NULL, 0, NULL, 0} }; @@ -227,16 +225,14 @@ main(int argc, char **argv) opts->filename = pg_strdup(optarg); break; case 'F': - if (strlen(optarg) != 0) - opts->formatName = pg_strdup(optarg); + opts->formatName = pg_strdup(optarg); break; case 'g': - /* restore only global.dat file from directory */ + /* restore only global sql commands. */ globals_only = true; break; case 'h': - if (strlen(optarg) != 0) - opts->cparams.pghost = pg_strdup(optarg); + opts->cparams.pghost = pg_strdup(optarg); break; case 'j': /* number of restore jobs */ if (!option_parse_int(optarg, "-j/--jobs", 1, @@ -266,8 +262,7 @@ main(int argc, char **argv) break; case 'p': - if (strlen(optarg) != 0) - opts->cparams.pgport = pg_strdup(optarg); + opts->cparams.pgport = pg_strdup(optarg); break; case 'R': /* no-op, still accepted for backwards compatibility */ @@ -352,7 +347,12 @@ main(int argc, char **argv) exit(1); opts->exit_on_error = true; break; - case 6: /* database patterns to skip */ + + case 6: + opts->restrict_key = pg_strdup(optarg); + break; + + case 7: /* database patterns to skip */ simple_string_list_append(&db_exclude_patterns, optarg); break; @@ -382,77 +382,95 @@ main(int argc, char **argv) if (!opts->cparams.dbname && !opts->filename && !opts->tocSummary) pg_fatal("one of -d/--dbname and -f/--file must be specified"); - if (db_exclude_patterns.head != NULL && globals_only) - { - pg_log_error("option --exclude-database cannot be used together with -g/--globals-only"); - pg_log_error_hint("Try \"%s --help\" for more information.", progname); - exit_nicely(1); - } + /* --exclude-database and --globals-only are incompatible */ + check_mut_excl_opts(db_exclude_patterns.head, "--exclude-database", + globals_only, "-g/--globals-only"); /* Should get at most one of -d and -f, else user is confused */ + check_mut_excl_opts(opts->cparams.dbname, "-d/--dbname", + opts->filename, "-f/--file"); + + /* --dbname and --restrict-key are incompatible */ + check_mut_excl_opts(opts->cparams.dbname, "-d/--dbname", + opts->restrict_key, "--restrict-key"); + if (opts->cparams.dbname) - { - if (opts->filename) - { - pg_log_error("options -d/--dbname and -f/--file cannot be used together"); - pg_log_error_hint("Try \"%s --help\" for more information.", progname); - exit_nicely(1); - } opts->useDB = 1; + else + { + /* + * If you don't provide a restrict key, one will be appointed for you. + */ + if (!opts->restrict_key) + opts->restrict_key = generate_restrict_key(); + if (!opts->restrict_key) + pg_fatal("could not generate restrict key"); + if (!valid_restrict_key(opts->restrict_key)) + pg_fatal("invalid restrict key"); } - /* reject conflicting "-only" options */ - if (data_only && schema_only) - pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together"); - if (schema_only && statistics_only) - pg_fatal("options -s/--schema-only and --statistics-only cannot be used together"); - if (data_only && statistics_only) - pg_fatal("options -a/--data-only and --statistics-only cannot be used together"); - - /* reject conflicting "-only" and "no-" options */ - if (data_only && no_data) - pg_fatal("options -a/--data-only and --no-data cannot be used together"); - if (schema_only && no_schema) - pg_fatal("options -s/--schema-only and --no-schema cannot be used together"); - if (statistics_only && no_statistics) - pg_fatal("options --statistics-only and --no-statistics cannot be used together"); - - /* reject conflicting "with-" and "no-" options */ - if (with_data && no_data) - pg_fatal("options --with-data and --no-data cannot be used together"); - if (with_schema && no_schema) - pg_fatal("options --with-schema and --no-schema cannot be used together"); - if (with_statistics && no_statistics) - pg_fatal("options --with-statistics and --no-statistics cannot be used together"); - - if (data_only && opts->dropSchema) - pg_fatal("options -c/--clean and -a/--data-only cannot be used together"); - - if (opts->single_txn && opts->txn_size > 0) - pg_fatal("options -1/--single-transaction and --transaction-size cannot be used together"); + /* *-only options are incompatible with each other */ + check_mut_excl_opts(data_only, "-a/--data-only", + globals_only, "-g/--globals-only", + schema_only, "-s/--schema-only", + statistics_only, "--statistics-only"); + + /* --no-* and *-only for same thing are incompatible */ + check_mut_excl_opts(data_only, "-a/--data-only", + no_data, "--no-data"); + check_mut_excl_opts(globals_only, "-g/--globals-only", + no_globals, "--no-globals"); + check_mut_excl_opts(schema_only, "-s/--schema-only", + no_schema, "--no-schema"); + check_mut_excl_opts(statistics_only, "--statistics-only", + no_statistics, "--no-statistics"); + + /* --statistics and --no-statistics are incompatible */ + check_mut_excl_opts(with_statistics, "--statistics", + no_statistics, "--no-statistics"); + + /* --statistics is incompatible with *-only (except --statistics-only) */ + check_mut_excl_opts(with_statistics, "--statistics", + data_only, "-a/--data-only", + globals_only, "-g/--globals-only", + schema_only, "-s/--schema-only"); + + /* --clean and --data-only are incompatible */ + check_mut_excl_opts(opts->dropSchema, "-c/--clean", + data_only, "-a/--data-only"); + + /* + * --globals-only, --single-transaction, and --transaction-size are + * incompatible. + */ + check_mut_excl_opts(globals_only, "-g/--globals-only", + opts->single_txn, "-1/--single-transaction", + opts->txn_size, "--transaction-size"); + + /* --exit-on-error and --globals-only are incompatible */ + check_mut_excl_opts(opts->exit_on_error, "--exit-on-error", + globals_only, "-g/--globals-only"); /* * -C is not compatible with -1, because we can't create a database inside * a transaction block. */ - if (opts->createDB && opts->single_txn) - pg_fatal("options -C/--create and -1/--single-transaction cannot be used together"); + check_mut_excl_opts(opts->createDB, "-C/--create", + opts->single_txn, "-1/--single-transaction"); /* Can't do single-txn mode with multiple connections */ if (opts->single_txn && numWorkers > 1) pg_fatal("cannot specify both --single-transaction and multiple jobs"); /* - * Set derivative flags. An "-only" option may be overridden by an - * explicit "with-" option; e.g. "--schema-only --with-statistics" will - * include schema and statistics. Other ambiguous or nonsensical - * combinations, e.g. "--schema-only --no-schema", will have already - * caused an error in one of the checks above. + * Set derivative flags. Ambiguous or nonsensical combinations, e.g. + * "--schema-only --no-schema", will have already caused an error in one + * of the checks above. */ opts->dumpData = ((opts->dumpData && !schema_only && !statistics_only) || - (data_only || with_data)) && !no_data; + data_only) && !no_data; opts->dumpSchema = ((opts->dumpSchema && !data_only && !statistics_only) || - (schema_only || with_schema)) && !no_schema; + schema_only) && !no_schema; opts->dumpStatistics = ((opts->dumpStatistics && !schema_only && !data_only) || (statistics_only || with_statistics)) && !no_statistics; @@ -469,7 +487,8 @@ main(int argc, char **argv) opts->no_subscriptions = no_subscriptions; if (if_exists && !opts->dropSchema) - pg_fatal("option --if-exists requires option -c/--clean"); + pg_fatal("option %s requires option %s", + "--if-exists", "-c/--clean"); opts->if_exists = if_exists; opts->strict_names = strict_names; @@ -497,87 +516,117 @@ main(int argc, char **argv) } /* - * If toc.dat file is not present in the current path, then check for - * global.dat. If global.dat file is present, then restore all the - * databases from map.dat (if it exists), but skip restoring those - * matching --exclude-database patterns. + * If toc.glo file is present, then restore all the databases from + * map.dat, but skip restoring those matching --exclude-database patterns. */ - if (inputFileSpec != NULL && !file_exists_in_directory(inputFileSpec, "toc.dat") && - file_exists_in_directory(inputFileSpec, "global.dat")) + if (inputFileSpec != NULL && + (file_exists_in_directory(inputFileSpec, "toc.glo"))) { - PGconn *conn = NULL; /* Connection to restore global sql - * commands. */ + char global_path[MAXPGPATH]; + RestoreOptions *tmpopts = pg_malloc0_object(RestoreOptions); + + opts->format = archUnknown; + + memcpy(tmpopts, opts, sizeof(RestoreOptions)); /* * Can only use --list or --use-list options with a single database * dump. */ if (opts->tocSummary) - pg_fatal("option -l/--list cannot be used when restoring an archive created by pg_dumpall"); - else if (opts->tocFile) - pg_fatal("option -L/--use-list cannot be used when restoring an archive created by pg_dumpall"); + pg_fatal("option %s cannot be used when restoring an archive created by pg_dumpall", + "-l/--list"); + if (opts->tocFile) + pg_fatal("option %s cannot be used when restoring an archive created by pg_dumpall", + "-L/--use-list"); + + if (opts->strict_names) + pg_fatal("option %s cannot be used when restoring an archive created by pg_dumpall", + "--strict-names"); + if (globals_only && opts->dropSchema) + pg_fatal("options %s and %s cannot be used together when restoring an archive created by pg_dumpall", + "--clean", "-g/--globals-only"); + + /* + * For pg_dumpall archives, --clean implies --if-exists since global + * objects may not exist in the target cluster. + */ + if (opts->dropSchema && !opts->if_exists) + { + opts->if_exists = 1; + pg_log_info("--if-exists is implied by --clean for pg_dumpall archives"); + } + + if (no_schema) + pg_fatal("option %s cannot be used when restoring an archive created by pg_dumpall", + "--no-schema"); + + if (data_only) + pg_fatal("option %s cannot be used when restoring an archive created by pg_dumpall", + "-a/--data-only"); + + if (statistics_only) + pg_fatal("option %s cannot be used when restoring an archive created by pg_dumpall", + "--statistics-only"); + + if (!(opts->dumpSections & DUMP_PRE_DATA)) + pg_fatal("option %s cannot exclude %s when restoring a pg_dumpall archive", + "--section", "--pre-data"); /* * To restore from a pg_dumpall archive, -C (create database) option - * must be specified unless we are only restoring globals. + * must be specified unless we are only restoring globals or we are + * skipping globals. */ - if (!globals_only && opts->createDB != 1) + if (!no_globals && !globals_only && opts->createDB != 1) { - pg_log_error("-C/--create option should be specified when restoring an archive created by pg_dumpall"); + pg_log_error("option %s must be specified when restoring an archive created by pg_dumpall", + "-C/--create"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); pg_log_error_hint("Individual databases can be restored using their specific archives."); exit_nicely(1); } /* - * Connect to the database to execute global sql commands from - * global.dat file. + * Restore global objects, even if --exclude-database results in zero + * databases to process. If 'globals-only' is set, exit immediately. */ - if (opts->cparams.dbname) - { - conn = ConnectDatabase(opts->cparams.dbname, NULL, opts->cparams.pghost, - opts->cparams.pgport, opts->cparams.username, TRI_DEFAULT, - false, progname, NULL, NULL, NULL, NULL); + snprintf(global_path, MAXPGPATH, "%s/toc.glo", inputFileSpec); + if (!no_globals) + n_errors = restore_global_objects(global_path, tmpopts); + else + pg_log_info("skipping restore of global objects because %s was specified", + "--no-globals"); - if (!conn) - pg_fatal("could not connect to database \"%s\"", opts->cparams.dbname); - } - - /* If globals-only, then return from here. */ if (globals_only) - { - /* - * Open global.dat file and execute/append all the global sql - * commands. - */ - n_errors = process_global_sql_commands(conn, inputFileSpec, - opts->filename); - - if (conn) - PQfinish(conn); - - pg_log_info("database restoring skipped as -g/--globals-only option was specified"); - } + pg_log_info("database restoring skipped because option %s was specified", + "-g/--globals-only"); else { /* Now restore all the databases from map.dat */ - n_errors = restore_all_databases(conn, inputFileSpec, db_exclude_patterns, - opts, numWorkers); + n_errors = n_errors + restore_all_databases(inputFileSpec, db_exclude_patterns, + opts, numWorkers); } /* Free db pattern list. */ simple_string_list_destroy(&db_exclude_patterns); } - else /* process if global.dat file does not exist. */ + else { if (db_exclude_patterns.head != NULL) - pg_fatal("option --exclude-database can be used only when restoring an archive created by pg_dumpall"); + { + simple_string_list_destroy(&db_exclude_patterns); + pg_fatal("option %s can be used only when restoring an archive created by pg_dumpall", + "--exclude-database"); + } if (globals_only) - pg_fatal("option -g/--globals-only can be used only when restoring an archive created by pg_dumpall"); + pg_fatal("option %s can be used only when restoring an archive created by pg_dumpall", + "-g/--globals-only"); - n_errors = restore_one_database(inputFileSpec, opts, numWorkers, false, 0); + /* Process if toc.glo file does not exist. */ + n_errors = restore_one_database(inputFileSpec, opts, numWorkers, false); } /* Done, print a summary of ignored errors during restore. */ @@ -590,6 +639,49 @@ main(int argc, char **argv) return 0; } +/* + * restore_global_objects + * + * This restore all global objects. + */ +static int +restore_global_objects(const char *inputFileSpec, RestoreOptions *opts) +{ + Archive *AH; + int nerror = 0; + + /* Set format as custom so that toc.glo file can be read. */ + opts->format = archCustom; + opts->txn_size = 0; + + AH = OpenArchive(inputFileSpec, opts->format); + + SetArchiveOptions(AH, NULL, opts); + + on_exit_close_archive(AH); + + /* Let the archiver know how noisy to be */ + AH->verbose = opts->verbose; + + /* Don't output TOC entry comments when restoring globals */ + ((ArchiveHandle *) AH)->noTocComments = 1; + + AH->exit_on_error = false; + + /* Parallel execution is not supported for global object restoration. */ + AH->numWorkers = 1; + + ProcessArchiveRestoreOptions(AH); + RestoreArchive(AH, false); + + nerror = AH->n_errors; + + /* AH may be freed in CloseArchive? */ + CloseArchive(AH); + + return nerror; +} + /* * restore_one_database * @@ -599,7 +691,7 @@ main(int argc, char **argv) */ static int restore_one_database(const char *inputFileSpec, RestoreOptions *opts, - int numWorkers, bool append_data, int num) + int numWorkers, bool append_data) { Archive *AH; int n_errors; @@ -612,11 +704,11 @@ restore_one_database(const char *inputFileSpec, RestoreOptions *opts, * We don't have a connection yet but that doesn't matter. The connection * is initialized to NULL and if we terminate through exit_nicely() while * it's still NULL, the cleanup function will just be a no-op. If we are - * restoring multiple databases, then only update AX handle for cleanup as + * restoring multiple databases, then only update AH handle for cleanup as * the previous entry was already in the array and we had closed previous * connection, so we can use the same array slot. */ - if (!append_data || num == 0) + if (!append_data) on_exit_close_archive(AH); else replace_on_exit_close_archive(AH); @@ -702,9 +794,12 @@ usage(const char *progname) printf(_(" --no-security-labels do not restore security labels\n")); printf(_(" --no-statistics do not restore statistics\n")); printf(_(" --no-subscriptions do not restore subscriptions\n")); + printf(_(" --no-globals do not restore global objects (roles and tablespaces)\n")); printf(_(" --no-table-access-method do not restore table access methods\n")); printf(_(" --no-tablespaces do not restore tablespace assignments\n")); + printf(_(" --restrict-key=RESTRICT_KEY use provided string as psql \\restrict key\n")); printf(_(" --section=SECTION restore named section (pre-data, data, or post-data)\n")); + printf(_(" --statistics restore the statistics\n")); printf(_(" --statistics-only restore only the statistics, not schema or data\n")); printf(_(" --strict-names require table and/or schema include patterns to\n" " match at least one entity each\n")); @@ -712,9 +807,6 @@ usage(const char *progname) printf(_(" --use-set-session-authorization\n" " use SET SESSION AUTHORIZATION commands instead of\n" " ALTER OWNER commands to set ownership\n")); - printf(_(" --with-data dump the data\n")); - printf(_(" --with-schema dump the schema\n")); - printf(_(" --with-statistics dump the statistics\n")); printf(_("\nConnection options:\n")); printf(_(" -h, --host=HOSTNAME database server host or socket directory\n")); @@ -725,8 +817,8 @@ usage(const char *progname) printf(_(" --role=ROLENAME do SET ROLE before restore\n")); printf(_("\n" - "The options -I, -n, -N, -P, -t, -T, --section, and --exclude-database can be combined\n" - "and specified multiple times to select multiple objects.\n")); + "The options -I, -n, -N, -P, -t, -T, --section, and --exclude-database can be\n" + "combined and specified multiple times to select multiple objects.\n")); printf(_("\nIf no input file name is supplied, then standard input is used.\n\n")); printf(_("Report bugs to <%s>.\n"), PACKAGE_BUGREPORT); printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); @@ -849,82 +941,6 @@ file_exists_in_directory(const char *dir, const char *filename) return (stat(buf, &st) == 0 && S_ISREG(st.st_mode)); } -/* - * read_one_statement - * - * This will start reading from passed file pointer using fgetc and read till - * semicolon(sql statement terminator for global.dat file) - * - * EOF is returned if end-of-file input is seen; time to shut down. - */ - -static int -read_one_statement(StringInfo inBuf, FILE *pfile) -{ - int c; /* character read from getc() */ - int m; - - StringInfoData q; - - initStringInfo(&q); - - resetStringInfo(inBuf); - - /* - * Read characters until EOF or the appropriate delimiter is seen. - */ - while ((c = fgetc(pfile)) != EOF) - { - if (c != '\'' && c != '"' && c != '\n' && c != ';') - { - appendStringInfoChar(inBuf, (char) c); - while ((c = fgetc(pfile)) != EOF) - { - if (c != '\'' && c != '"' && c != ';' && c != '\n') - appendStringInfoChar(inBuf, (char) c); - else - break; - } - } - - if (c == '\'' || c == '"') - { - appendStringInfoChar(&q, (char) c); - m = c; - - while ((c = fgetc(pfile)) != EOF) - { - appendStringInfoChar(&q, (char) c); - - if (c == m) - { - appendStringInfoString(inBuf, q.data); - resetStringInfo(&q); - break; - } - } - } - - if (c == ';') - { - appendStringInfoChar(inBuf, (char) ';'); - break; - } - - if (c == '\n') - appendStringInfoChar(inBuf, (char) '\n'); - } - - pg_free(q.data); - - /* No input before EOF signal means time to quit. */ - if (c == EOF && inBuf->len == 0) - return EOF; - - /* return something that's not EOF */ - return 'Q'; -} - /* * get_dbnames_list_to_restore * @@ -941,12 +957,11 @@ get_dbnames_list_to_restore(PGconn *conn, { int count_db = 0; PQExpBuffer query; + PQExpBuffer db_lit; PGresult *res; query = createPQExpBuffer(); - - if (!conn) - pg_log_info("considering PATTERN as NAME for --exclude-database option as no db connection while doing pg_restore."); + db_lit = createPQExpBuffer(); /* * Process one by one all dbnames and if specified to skip restoring, then @@ -957,7 +972,9 @@ get_dbnames_list_to_restore(PGconn *conn, { DbOidName *dbidname = (DbOidName *) db_cell->ptr; bool skip_db_restore = false; - PQExpBuffer db_lit = createPQExpBuffer(); + + resetPQExpBuffer(query); + resetPQExpBuffer(db_lit); appendStringLiteralConn(db_lit, dbidname->str, conn); @@ -970,7 +987,7 @@ get_dbnames_list_to_restore(PGconn *conn, if (pg_strcasecmp(dbidname->str, pat_cell->val) == 0) skip_db_restore = true; /* Otherwise, try a pattern match if there is a connection */ - else if (conn) + else { int dotcnt; @@ -989,10 +1006,10 @@ get_dbnames_list_to_restore(PGconn *conn, res = executeQuery(conn, query->data); - if ((PQresultStatus(res) == PGRES_TUPLES_OK) && PQntuples(res)) + if (PQntuples(res)) { skip_db_restore = true; - pg_log_info("database \"%s\" matches exclude pattern: \"%s\"", dbidname->str, pat_cell->val); + pg_log_info("database name \"%s\" matches --exclude-database pattern \"%s\"", dbidname->str, pat_cell->val); } PQclear(res); @@ -1003,8 +1020,6 @@ get_dbnames_list_to_restore(PGconn *conn, break; } - destroyPQExpBuffer(db_lit); - /* * Mark db to be skipped or increment the counter of dbs to be * restored @@ -1015,12 +1030,11 @@ get_dbnames_list_to_restore(PGconn *conn, dbidname->oid = InvalidOid; } else - { count_db++; - } } destroyPQExpBuffer(query); + destroyPQExpBuffer(db_lit); return count_db; } @@ -1034,21 +1048,21 @@ get_dbnames_list_to_restore(PGconn *conn, * Returns, total number of database names in map.dat file. */ static int -get_dbname_oid_list_from_mfile(const char *dumpdirpath, SimplePtrList *dbname_oid_list) +get_dbname_oid_list_from_mfile(const char *dumpdirpath, + SimplePtrList *dbname_oid_list) { StringInfoData linebuf; FILE *pfile; char map_file_path[MAXPGPATH]; int count = 0; - /* - * If there is only global.dat file in dump, then return from here as - * there is no database to restore. + * If there is no map.dat file in the dump, then return from here as there + * is no database to restore. */ if (!file_exists_in_directory(dumpdirpath, "map.dat")) { - pg_log_info("database restoring is skipped as \"map.dat\" is not present in \"%s\"", dumpdirpath); + pg_log_info("database restoring is skipped because file \"%s\" does not exist in directory \"%s\"", "map.dat", dumpdirpath); return 0; } @@ -1058,7 +1072,7 @@ get_dbname_oid_list_from_mfile(const char *dumpdirpath, SimplePtrList *dbname_oi pfile = fopen(map_file_path, PG_BINARY_R); if (pfile == NULL) - pg_fatal("could not open \"%s\": %m", map_file_path); + pg_fatal("could not open file \"%s\": %m", map_file_path); initStringInfo(&linebuf); @@ -1071,10 +1085,15 @@ get_dbname_oid_list_from_mfile(const char *dumpdirpath, SimplePtrList *dbname_oi int namelen; char *p = linebuf.data; - /* Extract dboid. */ + /* look for the dboid. */ while (isdigit((unsigned char) *p)) p++; - if (p > linebuf.data && *p == ' ') + + /* ignore lines that don't begin with a digit */ + if (p == linebuf.data) + continue; + + if (*p == ' ') { sscanf(linebuf.data, "%u", &db_oid); p++; @@ -1084,17 +1103,21 @@ get_dbname_oid_list_from_mfile(const char *dumpdirpath, SimplePtrList *dbname_oi dbname = p; namelen = strlen(dbname); + /* Strip trailing newline */ + if (namelen > 0 && dbname[namelen - 1] == '\n') + dbname[--namelen] = '\0'; + /* Report error and exit if the file has any corrupted data. */ - if (!OidIsValid(db_oid) || namelen <= 1) - pg_fatal("invalid entry in \"%s\" at line: %d", map_file_path, + if (!OidIsValid(db_oid) || namelen < 1) + pg_fatal("invalid entry in file \"%s\" on line %d", map_file_path, count + 1); - pg_log_info("found database \"%s\" (OID: %u) in \"%s\"", - dbname, db_oid, map_file_path); - dbidname = pg_malloc(offsetof(DbOidName, str) + namelen + 1); dbidname->oid = db_oid; - strlcpy(dbidname->str, dbname, namelen); + strlcpy(dbidname->str, dbname, namelen + 1); + + pg_log_info("found database \"%s\" (OID: %u) in file \"%s\"", + dbidname->str, db_oid, map_file_path); simple_ptr_list_append(dbname_oid_list, dbidname); count++; @@ -1103,6 +1126,8 @@ get_dbname_oid_list_from_mfile(const char *dumpdirpath, SimplePtrList *dbname_oi /* Close map.dat file. */ fclose(pfile); + pfree(linebuf.data); + return count; } @@ -1118,69 +1143,90 @@ get_dbname_oid_list_from_mfile(const char *dumpdirpath, SimplePtrList *dbname_oi * returns, number of errors while doing restore. */ static int -restore_all_databases(PGconn *conn, const char *dumpdirpath, +restore_all_databases(const char *inputFileSpec, SimpleStringList db_exclude_patterns, RestoreOptions *opts, int numWorkers) { SimplePtrList dbname_oid_list = {NULL, NULL}; int num_db_restore = 0; int num_total_db; - int n_errors_total; - int count = 0; + int n_errors_total = 0; char *connected_db = NULL; - bool dumpData = opts->dumpData; - bool dumpSchema = opts->dumpSchema; - bool dumpStatistics = opts->dumpSchema; + PGconn *conn = NULL; + RestoreOptions *original_opts = pg_malloc0_object(RestoreOptions); + RestoreOptions *tmpopts = pg_malloc0_object(RestoreOptions); + + memcpy(original_opts, opts, sizeof(RestoreOptions)); /* Save db name to reuse it for all the database. */ if (opts->cparams.dbname) connected_db = opts->cparams.dbname; - num_total_db = get_dbname_oid_list_from_mfile(dumpdirpath, &dbname_oid_list); + num_total_db = get_dbname_oid_list_from_mfile(inputFileSpec, &dbname_oid_list); - /* If map.dat has no entries, return after processing global.dat */ - if (dbname_oid_list.head == NULL) - return process_global_sql_commands(conn, dumpdirpath, opts->filename); + pg_log_info(ngettext("found %d database name in \"%s\"", + "found %d database names in \"%s\"", + num_total_db), + num_total_db, "map.dat"); - pg_log_info("found %d database names in \"map.dat\"", num_total_db); - - if (!conn) + /* + * If exclude-patterns is given, connect to the database to process them. + */ + if (db_exclude_patterns.head != NULL) { - pg_log_info("trying to connect database \"postgres\""); + if (opts->cparams.dbname) + { + conn = ConnectDatabase(opts->cparams.dbname, NULL, opts->cparams.pghost, + opts->cparams.pgport, opts->cparams.username, TRI_DEFAULT, + false, progname, NULL, NULL, NULL, NULL); - conn = ConnectDatabase("postgres", NULL, opts->cparams.pghost, - opts->cparams.pgport, opts->cparams.username, TRI_DEFAULT, - false, progname, NULL, NULL, NULL, NULL); + if (!conn) + pg_fatal("could not connect to database \"%s\"", opts->cparams.dbname); + } - /* Try with template1. */ if (!conn) { - pg_log_info("trying to connect database \"template1\""); + pg_log_info("trying to connect to database \"%s\"", "postgres"); - conn = ConnectDatabase("template1", NULL, opts->cparams.pghost, + conn = ConnectDatabase("postgres", NULL, opts->cparams.pghost, opts->cparams.pgport, opts->cparams.username, TRI_DEFAULT, false, progname, NULL, NULL, NULL, NULL); - } - } - /* - * filter the db list according to the exclude patterns - */ - num_db_restore = get_dbnames_list_to_restore(conn, &dbname_oid_list, - db_exclude_patterns); + /* Try with template1. */ + if (!conn) + { + pg_log_info("trying to connect to database \"%s\"", "template1"); - /* Open global.dat file and execute/append all the global sql commands. */ - n_errors_total = process_global_sql_commands(conn, dumpdirpath, opts->filename); + conn = ConnectDatabase("template1", NULL, opts->cparams.pghost, + opts->cparams.pgport, opts->cparams.username, TRI_DEFAULT, + false, progname, NULL, NULL, NULL, NULL); + if (!conn) + { + pg_log_error("could not connect to databases \"postgres\" or \"template1\"\n" + "Please specify an alternative database."); + pg_log_error_hint("Try \"%s --help\" for more information.", progname); + exit_nicely(1); + } + } + } - /* Close the db connection as we are done with globals and patterns. */ - if (conn) + /* Filter the db list according to the exclude patterns. */ + num_db_restore = get_dbnames_list_to_restore(conn, &dbname_oid_list, + db_exclude_patterns); PQfinish(conn); + } + else + num_db_restore = num_total_db; /* Exit if no db needs to be restored. */ - if (dbname_oid_list.head == NULL || num_db_restore == 0) + if (num_db_restore == 0) { - pg_log_info("no database needs to restore out of %d databases", num_total_db); - return n_errors_total; + pg_log_info(ngettext("no database needs restoring out of %d database", + "no database needs restoring out of %d databases", num_total_db), + num_total_db); + pg_free(original_opts); + pg_free(tmpopts); + return 0; } pg_log_info("need to restore %d databases out of %d databases", num_db_restore, num_total_db); @@ -1203,16 +1249,13 @@ restore_all_databases(PGconn *conn, const char *dumpdirpath, continue; /* - * We need to reset override_dbname so that objects can be restored - * into an already created database. (used with -d/--dbname option) + * Since pg_backup_archiver.c may modify RestoreOptions during the + * previous restore, we must provide a fresh copy of the original + * "opts" for each call to restore_one_database. */ - if (opts->cparams.override_dbname) - { - pfree(opts->cparams.override_dbname); - opts->cparams.override_dbname = NULL; - } + memcpy(tmpopts, original_opts, sizeof(RestoreOptions)); - snprintf(subdirdbpath, MAXPGPATH, "%s/databases", dumpdirpath); + snprintf(subdirdbpath, MAXPGPATH, "%s/databases", inputFileSpec); /* * Look for the database dump file/dir. If there is an {oid}.tar or @@ -1221,62 +1264,58 @@ restore_all_databases(PGconn *conn, const char *dumpdirpath, */ snprintf(dbfilename, MAXPGPATH, "%u.tar", dbidname->oid); if (file_exists_in_directory(subdirdbpath, dbfilename)) - snprintf(subdirpath, MAXPGPATH, "%s/databases/%u.tar", dumpdirpath, dbidname->oid); + snprintf(subdirpath, MAXPGPATH, "%s/databases/%u.tar", inputFileSpec, dbidname->oid); else { snprintf(dbfilename, MAXPGPATH, "%u.dmp", dbidname->oid); if (file_exists_in_directory(subdirdbpath, dbfilename)) - snprintf(subdirpath, MAXPGPATH, "%s/databases/%u.dmp", dumpdirpath, dbidname->oid); + snprintf(subdirpath, MAXPGPATH, "%s/databases/%u.dmp", inputFileSpec, dbidname->oid); else - snprintf(subdirpath, MAXPGPATH, "%s/databases/%u", dumpdirpath, dbidname->oid); + snprintf(subdirpath, MAXPGPATH, "%s/databases/%u", inputFileSpec, dbidname->oid); } pg_log_info("restoring database \"%s\"", dbidname->str); /* If database is already created, then don't set createDB flag. */ - if (opts->cparams.dbname) + if (tmpopts->cparams.dbname) { PGconn *test_conn; - test_conn = ConnectDatabase(dbidname->str, NULL, opts->cparams.pghost, - opts->cparams.pgport, opts->cparams.username, TRI_DEFAULT, + test_conn = ConnectDatabase(dbidname->str, NULL, tmpopts->cparams.pghost, + tmpopts->cparams.pgport, tmpopts->cparams.username, TRI_DEFAULT, false, progname, NULL, NULL, NULL, NULL); if (test_conn) { PQfinish(test_conn); /* Use already created database for connection. */ - opts->createDB = 0; - opts->cparams.dbname = dbidname->str; + tmpopts->createDB = 0; + tmpopts->cparams.dbname = dbidname->str; } else { - /* we'll have to create it */ - opts->createDB = 1; - opts->cparams.dbname = connected_db; + if (!tmpopts->createDB) + { + pg_log_info("skipping restore of database \"%s\": database does not exist and %s was not specified", + dbidname->str, "-C/--create"); + continue; + } + + /* We'll have to create it */ + tmpopts->createDB = 1; + tmpopts->cparams.dbname = connected_db; } } - /* - * Reset flags - might have been reset in pg_backup_archiver.c by the - * previous restore. - */ - opts->dumpData = dumpData; - opts->dumpSchema = dumpSchema; - opts->dumpStatistics = dumpStatistics; - /* Restore the single database. */ - n_errors = restore_one_database(subdirpath, opts, numWorkers, true, count); + n_errors = restore_one_database(subdirpath, tmpopts, numWorkers, true); + + n_errors_total += n_errors; /* Print a summary of ignored errors during single database restore. */ if (n_errors) - { - n_errors_total += n_errors; pg_log_warning("errors ignored on database \"%s\" restore: %d", dbidname->str, n_errors); - } - - count++; } /* Log number of processed databases. */ @@ -1285,124 +1324,8 @@ restore_all_databases(PGconn *conn, const char *dumpdirpath, /* Free dbname and dboid list. */ simple_ptr_list_destroy(&dbname_oid_list); - return n_errors_total; -} - -/* - * process_global_sql_commands - * - * Open global.dat and execute or copy the sql commands one by one. - * - * If outfile is not NULL, copy all sql commands into outfile rather than - * executing them. - * - * Returns the number of errors while processing global.dat - */ -static int -process_global_sql_commands(PGconn *conn, const char *dumpdirpath, const char *outfile) -{ - char global_file_path[MAXPGPATH]; - PGresult *result; - StringInfoData sqlstatement, - user_create; - FILE *pfile; - int n_errors = 0; - - snprintf(global_file_path, MAXPGPATH, "%s/global.dat", dumpdirpath); - - /* Open global.dat file. */ - pfile = fopen(global_file_path, PG_BINARY_R); - - if (pfile == NULL) - pg_fatal("could not open \"%s\": %m", global_file_path); - - /* - * If outfile is given, then just copy all global.dat file data into - * outfile. - */ - if (outfile) - { - copy_or_print_global_file(outfile, pfile); - return 0; - } - - /* Init sqlstatement to append commands. */ - initStringInfo(&sqlstatement); + pg_free(original_opts); + pg_free(tmpopts); - /* creation statement for our current role */ - initStringInfo(&user_create); - appendStringInfoString(&user_create, "CREATE ROLE "); - /* should use fmtId here, but we don't know the encoding */ - appendStringInfoString(&user_create, PQuser(conn)); - appendStringInfoChar(&user_create, ';'); - - /* Process file till EOF and execute sql statements. */ - while (read_one_statement(&sqlstatement, pfile) != EOF) - { - /* don't try to create the role we are connected as */ - if (strstr(sqlstatement.data, user_create.data)) - continue; - - pg_log_info("executing query: %s", sqlstatement.data); - result = PQexec(conn, sqlstatement.data); - - switch (PQresultStatus(result)) - { - case PGRES_COMMAND_OK: - case PGRES_TUPLES_OK: - case PGRES_EMPTY_QUERY: - break; - default: - n_errors++; - pg_log_error("could not execute query: \"%s\" \nCommand was: \"%s\"", PQerrorMessage(conn), sqlstatement.data); - } - PQclear(result); - } - - /* Print a summary of ignored errors during global.dat. */ - if (n_errors) - pg_log_warning("ignored %d errors in \"%s\"", n_errors, global_file_path); - - fclose(pfile); - - return n_errors; -} - -/* - * copy_or_print_global_file - * - * Copy global.dat into the output file. If "-" is used as outfile, - * then print commands to stdout. - */ -static void -copy_or_print_global_file(const char *outfile, FILE *pfile) -{ - char out_file_path[MAXPGPATH]; - FILE *OPF; - int c; - - /* "-" is used for stdout. */ - if (strcmp(outfile, "-") == 0) - OPF = stdout; - else - { - snprintf(out_file_path, MAXPGPATH, "%s", outfile); - OPF = fopen(out_file_path, PG_BINARY_W); - - if (OPF == NULL) - { - fclose(pfile); - pg_fatal("could not open file: \"%s\"", outfile); - } - } - - /* Append global.dat into output file or print to stdout. */ - while ((c = fgetc(pfile)) != EOF) - fputc(c, OPF); - - fclose(pfile); - - /* Close output file. */ - if (strcmp(outfile, "-") != 0) - fclose(OPF); + return n_errors_total; } diff --git a/src/bin/pg_dump/po/meson.build b/src/bin/pg_dump/po/meson.build index 7e350c5b819a5..2eaedcdf2180b 100644 --- a/src/bin/pg_dump/po/meson.build +++ b/src/bin/pg_dump/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_dump-' + pg_version_major.to_string())] diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl index 84ca25e17d636..509f4f9ce7dfc 100644 --- a/src/bin/pg_dump/t/001_basic.pl +++ b/src/bin/pg_dump/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -46,8 +46,8 @@ command_fails_like( [ 'pg_dump', '-s', '-a' ], - qr/\Qpg_dump: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/, - 'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together' + qr/\Qpg_dump: error: options -a\/--data-only and -s\/--schema-only cannot be used together\E/, + 'pg_dump: options -a/--data-only and -s/--schema-only cannot be used together' ); command_fails_like( @@ -64,8 +64,8 @@ command_fails_like( [ 'pg_dump', '-s', '--include-foreign-data=xxx' ], - qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/, - 'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together' + qr/\Qpg_dump: error: options --include-foreign-data and -s\/--schema-only cannot be used together\E/, + 'pg_dump: options --include-foreign-data and -s/--schema-only cannot be used together' ); command_fails_like( @@ -87,8 +87,8 @@ command_fails_like( [ 'pg_restore', '-s', '-a', '-f -' ], - qr/\Qpg_restore: error: options -s\/--schema-only and -a\/--data-only cannot be used together\E/, - 'pg_restore: options -s/--schema-only and -a/--data-only cannot be used together' + qr/\Qpg_restore: error: options -a\/--data-only and -s\/--schema-only cannot be used together\E/, + 'pg_restore: options -a/--data-only and -s/--schema-only cannot be used together' ); command_fails_like( @@ -101,6 +101,11 @@ qr/\Qpg_dump: error: options -c\/--clean and -a\/--data-only cannot be used together\E/, 'pg_dump: options -c/--clean and -a/--data-only cannot be used together'); +command_fails_like( + [ 'pg_dumpall', '-c', '-a' ], + qr/\Qpg_dumpall: error: options -c\/--clean and -a\/--data-only cannot be used together\E/, + 'pg_dumpall: options -c/--clean and -a/--data-only cannot be used together'); + command_fails_like( [ 'pg_restore', '-c', '-a', '-f -' ], qr/\Qpg_restore: error: options -c\/--clean and -a\/--data-only cannot be used together\E/, @@ -199,7 +204,12 @@ command_fails_like( [ 'pg_restore', '-f -', '-F', 'garbage' ], qr/\Qpg_restore: error: unrecognized archive format "garbage";\E/, - 'pg_dump: unrecognized archive format'); + 'pg_restore: unrecognized archive format'); + +command_fails_like( + [ 'pg_restore', '-f -', '-F', '' ], + qr/\Qpg_restore: error: unrecognized archive format "";\E/, + 'pg_restore: empty archive format'); command_fails_like( [ 'pg_dump', '--on-conflict-do-nothing' ], @@ -237,30 +247,108 @@ 'pg_restore: options -C\/--create and -1\/--single-transaction cannot be used together' ); +# also fails for -r and -t, but it seems pointless to add more tests for those. +command_fails_like( + [ 'pg_dumpall', '--exclude-database=foo', '--globals-only' ], + qr/\Qpg_dumpall: error: options --exclude-database and -g\/--globals-only cannot be used together\E/, + 'pg_dumpall: options --exclude-database and -g/--globals-only cannot be used together' +); + +command_fails_like( + [ 'pg_dumpall', '-a', '--no-data' ], + qr/\Qpg_dumpall: error: options -a\/--data-only and --no-data cannot be used together\E/, + 'pg_dumpall: options -a\/--data-only and --no-data cannot be used together' +); + +command_fails_like( + [ 'pg_dumpall', '-s', '--no-schema' ], + qr/\Qpg_dumpall: error: options -s\/--schema-only and --no-schema cannot be used together\E/, + 'pg_dumpall: options -s\/--schema-only and --no-schema cannot be used together' +); + +command_fails_like( + [ 'pg_dumpall', '--statistics-only', '--no-statistics' ], + qr/\Qpg_dumpall: error: options --statistics-only and --no-statistics cannot be used together\E/, + 'pg_dumpall: options --statistics-only and --no-statistics cannot be used together' +); + +command_fails_like( + [ 'pg_dumpall', '--statistics', '--no-statistics' ], + qr/\Qpg_dumpall: error: options --statistics and --no-statistics cannot be used together\E/, + 'pg_dumpall: options --statistics-only and --no-statistics cannot be used together' +); + +command_fails_like( + [ 'pg_dumpall', '--statistics', '--tablespaces-only' ], + qr/\Qpg_dumpall: error: options --statistics and -t\/--tablespaces-only cannot be used together\E/, + 'pg_dumpall: options --statistics and -t\/--tablespaces-only cannot be used together' +); + +command_fails_like( + [ 'pg_dumpall', '--format', 'x' ], + qr/\Qpg_dumpall: error: unrecognized output format "x";\E/, + 'pg_dumpall: unrecognized output format'); + +command_fails_like( + [ 'pg_dumpall', '--format', 'd', '--restrict-key=uu', '-f dumpfile' ], + qr/\Qpg_dumpall: error: option --restrict-key can only be used with --format=plain\E/, + 'pg_dumpall: --restrict-key can only be used with plain dump format'); + +command_fails_like( + [ + 'pg_dumpall', '--format', 'd', '--globals-only', + '--clean', '-f', 'dumpfile' + ], + qr/\Qpg_dumpall: error: options --clean and -g\/--globals-only cannot be used together in non-text dump\E/, + 'pg_dumpall: --clean and -g/--globals-only cannot be used together in non-text dump' +); + +command_fails_like( + [ 'pg_dumpall', '--format', 'd' ], + qr/\Qpg_dumpall: error: option -F\/--format=d|c|t requires option -f\/--file\E/, + 'pg_dumpall: non-plain format requires --file option'); + command_fails_like( [ 'pg_restore', '--exclude-database=foo', '--globals-only', '-d', 'xxx' ], - qr/\Qpg_restore: error: option --exclude-database cannot be used together with -g\/--globals-only\E/, - 'pg_restore: option --exclude-database cannot be used together with -g/--globals-only'); + qr/\Qpg_restore: error: options --exclude-database and -g\/--globals-only cannot be used together\E/, + 'pg_restore: options --exclude-database and -g/--globals-only cannot be used together' +); + +command_fails_like( + [ 'pg_restore', '--data-only', '--globals-only', '-d', 'xxx' ], + qr/\Qpg_restore: error: options -a\/--data-only and -g\/--globals-only cannot be used together\E/, + 'pg_restore: error: options -a/--data-only and -g/--globals-only cannot be used together' +); + +command_fails_like( + [ 'pg_restore', '--schema-only', '--globals-only', '-d', 'xxx' ], + qr/\Qpg_restore: error: options -g\/--globals-only and -s\/--schema-only cannot be used together\E/, + 'pg_restore: error: options -g/--globals-only and -s/--schema-only cannot be used together' +); + +command_fails_like( + [ 'pg_restore', '--statistics-only', '--globals-only', '-d', 'xxx' ], + qr/\Qpg_restore: error: options -g\/--globals-only and --statistics-only cannot be used together\E/, + 'pg_restore: error: options -g/--globals-only and --statistics-only cannot be used together' +); command_fails_like( [ 'pg_restore', '--exclude-database=foo', '-d', 'xxx', 'dumpdir' ], qr/\Qpg_restore: error: option --exclude-database can be used only when restoring an archive created by pg_dumpall\E/, - 'When option --exclude-database is used in pg_restore with dump of pg_dump'); + 'When option --exclude-database is used in pg_restore with dump of pg_dump' +); command_fails_like( [ 'pg_restore', '--globals-only', '-d', 'xxx', 'dumpdir' ], qr/\Qpg_restore: error: option -g\/--globals-only can be used only when restoring an archive created by pg_dumpall\E/, - 'When option --globals-only is not used in pg_restore with dump of pg_dump'); - -# also fails for -r and -t, but it seems pointless to add more tests for those. -command_fails_like( - [ 'pg_dumpall', '--exclude-database=foo', '--globals-only' ], - qr/\Qpg_dumpall: error: option --exclude-database cannot be used together with -g\/--globals-only\E/, - 'pg_dumpall: option --exclude-database cannot be used together with -g/--globals-only' + 'When option --globals-only is used in pg_restore with the dump of pg_dump' ); command_fails_like( - [ 'pg_dumpall', '--format', 'x' ], - qr/\Qpg_dumpall: error: unrecognized archive format "x";\E/, - 'pg_dumpall: unrecognized archive format'); + [ + 'pg_restore', '--globals-only', '--no-globals', '-d', 'xxx', + 'dumpdir' + ], + qr/\Qpg_restore: error: options -g\/--globals-only and --no-globals cannot be used together\E/, + 'options --no-globals and --globals-only cannot be used together'); done_testing(); diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index cf34f71ea1196..3bc8e51561d3d 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -20,22 +20,12 @@ # test_key indicates that a given run should simply use the same # set of like/unlike tests as another run, and which run that is. # -# compile_option indicates if the commands run depend on a compilation -# option, if any. This can be used to control if tests should be -# skipped when a build dependency is not satisfied. -# # dump_cmd is the pg_dump command to run, which is an array of # the full command and arguments to run. Note that this is run # using $node->command_ok(), so the port does not need to be # specified and is pulled from $PGPORT, which is set by the # PostgreSQL::Test::Cluster system. # -# compress_cmd is the utility command for (de)compression, if any. -# Note that this should generally be used on pg_dump's output -# either to generate a text file to run the through the tests, or -# to test pg_restore's ability to parse manually compressed files -# that otherwise pg_dump does not compress on its own (e.g. *.toc). -# # glob_patterns is an optional array consisting of strings compilable # with glob() to check the files generated after a dump. # @@ -55,8 +45,6 @@ my $supports_icu = ($ENV{with_icu} eq 'yes'); my $supports_gzip = check_pg_config("#define HAVE_LIBZ 1"); -my $supports_lz4 = check_pg_config("#define USE_LZ4 1"); -my $supports_zstd = check_pg_config("#define USE_ZSTD 1"); my %pgdump_runs = ( binary_upgrade => { @@ -68,6 +56,7 @@ '--no-data', '--sequence-data', '--binary-upgrade', + '--statistics', '--dbname' => 'postgres', # alternative way to specify database ], restore_cmd => [ @@ -75,241 +64,17 @@ '--format' => 'custom', '--verbose', '--file' => "$tempdir/binary_upgrade.sql", + '--statistics', "$tempdir/binary_upgrade.dump", ], }, - # Do not use --no-sync to give test coverage for data sync. - compression_gzip_custom => { - test_key => 'compression', - compile_option => 'gzip', - dump_cmd => [ - 'pg_dump', - '--format' => 'custom', - '--compress' => '1', - '--file' => "$tempdir/compression_gzip_custom.dump", - 'postgres', - ], - restore_cmd => [ - 'pg_restore', - '--file' => "$tempdir/compression_gzip_custom.sql", - "$tempdir/compression_gzip_custom.dump", - ], - command_like => { - command => [ - 'pg_restore', '--list', - "$tempdir/compression_gzip_custom.dump", - ], - expected => qr/Compression: gzip/, - name => 'data content is gzip-compressed' - }, - }, - - # Do not use --no-sync to give test coverage for data sync. - compression_gzip_dir => { - test_key => 'compression', - compile_option => 'gzip', - dump_cmd => [ - 'pg_dump', - '--jobs' => '2', - '--format' => 'directory', - '--compress' => 'gzip:1', - '--file' => "$tempdir/compression_gzip_dir", - 'postgres', - ], - # Give coverage for manually compressed blobs.toc files during - # restore. - compress_cmd => { - program => $ENV{'GZIP_PROGRAM'}, - args => [ '-f', "$tempdir/compression_gzip_dir/blobs_*.toc", ], - }, - # Verify that only data files were compressed - glob_patterns => [ - "$tempdir/compression_gzip_dir/toc.dat", - "$tempdir/compression_gzip_dir/*.dat.gz", - ], - restore_cmd => [ - 'pg_restore', - '--jobs' => '2', - '--file' => "$tempdir/compression_gzip_dir.sql", - "$tempdir/compression_gzip_dir", - ], - }, - - compression_gzip_plain => { - test_key => 'compression', - compile_option => 'gzip', - dump_cmd => [ - 'pg_dump', - '--format' => 'plain', - '--compress' => '1', - '--file' => "$tempdir/compression_gzip_plain.sql.gz", - 'postgres', - ], - # Decompress the generated file to run through the tests. - compress_cmd => { - program => $ENV{'GZIP_PROGRAM'}, - args => [ '-d', "$tempdir/compression_gzip_plain.sql.gz", ], - }, - }, - - # Do not use --no-sync to give test coverage for data sync. - compression_lz4_custom => { - test_key => 'compression', - compile_option => 'lz4', - dump_cmd => [ - 'pg_dump', - '--format' => 'custom', - '--compress' => 'lz4', - '--file' => "$tempdir/compression_lz4_custom.dump", - 'postgres', - ], - restore_cmd => [ - 'pg_restore', - '--file' => "$tempdir/compression_lz4_custom.sql", - "$tempdir/compression_lz4_custom.dump", - ], - command_like => { - command => [ - 'pg_restore', '--list', - "$tempdir/compression_lz4_custom.dump", - ], - expected => qr/Compression: lz4/, - name => 'data content is lz4 compressed' - }, - }, - - # Do not use --no-sync to give test coverage for data sync. - compression_lz4_dir => { - test_key => 'compression', - compile_option => 'lz4', - dump_cmd => [ - 'pg_dump', - '--jobs' => '2', - '--format' => 'directory', - '--compress' => 'lz4:1', - '--file' => "$tempdir/compression_lz4_dir", - 'postgres', - ], - # Verify that data files were compressed - glob_patterns => [ - "$tempdir/compression_lz4_dir/toc.dat", - "$tempdir/compression_lz4_dir/*.dat.lz4", - ], - restore_cmd => [ - 'pg_restore', - '--jobs' => '2', - '--file' => "$tempdir/compression_lz4_dir.sql", - "$tempdir/compression_lz4_dir", - ], - }, - - compression_lz4_plain => { - test_key => 'compression', - compile_option => 'lz4', - dump_cmd => [ - 'pg_dump', - '--format' => 'plain', - '--compress' => 'lz4', - '--file' => "$tempdir/compression_lz4_plain.sql.lz4", - 'postgres', - ], - # Decompress the generated file to run through the tests. - compress_cmd => { - program => $ENV{'LZ4'}, - args => [ - '-d', '-f', - "$tempdir/compression_lz4_plain.sql.lz4", - "$tempdir/compression_lz4_plain.sql", - ], - }, - }, - - compression_zstd_custom => { - test_key => 'compression', - compile_option => 'zstd', - dump_cmd => [ - 'pg_dump', - '--format' => 'custom', - '--compress' => 'zstd', - '--file' => "$tempdir/compression_zstd_custom.dump", - 'postgres', - ], - restore_cmd => [ - 'pg_restore', - '--file' => "$tempdir/compression_zstd_custom.sql", - "$tempdir/compression_zstd_custom.dump", - ], - command_like => { - command => [ - 'pg_restore', '--list', - "$tempdir/compression_zstd_custom.dump", - ], - expected => qr/Compression: zstd/, - name => 'data content is zstd compressed' - }, - }, - - compression_zstd_dir => { - test_key => 'compression', - compile_option => 'zstd', - dump_cmd => [ - 'pg_dump', - '--jobs' => '2', - '--format' => 'directory', - '--compress' => 'zstd:1', - '--file' => "$tempdir/compression_zstd_dir", - 'postgres', - ], - # Give coverage for manually compressed blobs.toc files during - # restore. - compress_cmd => { - program => $ENV{'ZSTD'}, - args => [ - '-z', '-f', - '--rm', "$tempdir/compression_zstd_dir/blobs_*.toc", - ], - }, - # Verify that data files were compressed - glob_patterns => [ - "$tempdir/compression_zstd_dir/toc.dat", - "$tempdir/compression_zstd_dir/*.dat.zst", - ], - restore_cmd => [ - 'pg_restore', - '--jobs' => '2', - '--file' => "$tempdir/compression_zstd_dir.sql", - "$tempdir/compression_zstd_dir", - ], - }, - - # Exercise long mode for test coverage - compression_zstd_plain => { - test_key => 'compression', - compile_option => 'zstd', - dump_cmd => [ - 'pg_dump', - '--format' => 'plain', - '--compress' => 'zstd:long', - '--file' => "$tempdir/compression_zstd_plain.sql.zst", - 'postgres', - ], - # Decompress the generated file to run through the tests. - compress_cmd => { - program => $ENV{'ZSTD'}, - args => [ - '-d', '-f', - "$tempdir/compression_zstd_plain.sql.zst", "-o", - "$tempdir/compression_zstd_plain.sql", - ], - }, - }, - clean => { dump_cmd => [ 'pg_dump', '--no-sync', '--file' => "$tempdir/clean.sql", '--clean', + '--statistics', '--dbname' => 'postgres', # alternative way to specify database ], }, @@ -320,6 +85,7 @@ '--clean', '--if-exists', '--encoding' => 'UTF8', # no-op, just for testing + '--statistics', 'postgres', ], }, @@ -338,6 +104,7 @@ '--create', '--no-reconnect', # no-op, just for testing '--verbose', + '--statistics', 'postgres', ], }, @@ -348,7 +115,7 @@ '--data-only', '--superuser' => 'test_superuser', '--disable-triggers', - '--verbose', # no-op, just make sure it works + '--verbose', # no-op, just make sure it works 'postgres', ], }, @@ -356,6 +123,7 @@ dump_cmd => [ 'pg_dump', '--no-sync', '--file' => "$tempdir/defaults.sql", + '--statistics', 'postgres', ], }, @@ -364,6 +132,7 @@ dump_cmd => [ 'pg_dump', '--no-sync', '--file' => "$tempdir/defaults_no_public.sql", + '--statistics', 'regress_pg_dump_test', ], }, @@ -373,6 +142,7 @@ 'pg_dump', '--no-sync', '--clean', '--file' => "$tempdir/defaults_no_public_clean.sql", + '--statistics', 'regress_pg_dump_test', ], }, @@ -381,6 +151,7 @@ dump_cmd => [ 'pg_dump', '--no-sync', '--file' => "$tempdir/defaults_public_owner.sql", + '--statistics', 'regress_public_owner', ], }, @@ -395,12 +166,14 @@ 'pg_dump', '--format' => 'custom', '--file' => "$tempdir/defaults_custom_format.dump", + '--statistics', 'postgres', ], restore_cmd => [ 'pg_restore', '--format' => 'custom', '--file' => "$tempdir/defaults_custom_format.sql", + '--statistics', "$tempdir/defaults_custom_format.dump", ], command_like => { @@ -425,12 +198,14 @@ 'pg_dump', '--format' => 'directory', '--file' => "$tempdir/defaults_dir_format", + '--statistics', 'postgres', ], restore_cmd => [ 'pg_restore', '--format' => 'directory', '--file' => "$tempdir/defaults_dir_format.sql", + '--statistics', "$tempdir/defaults_dir_format", ], command_like => { @@ -456,11 +231,13 @@ '--format' => 'directory', '--jobs' => 2, '--file' => "$tempdir/defaults_parallel", + '--statistics', 'postgres', ], restore_cmd => [ 'pg_restore', '--file' => "$tempdir/defaults_parallel.sql", + '--statistics', "$tempdir/defaults_parallel", ], }, @@ -472,12 +249,14 @@ 'pg_dump', '--format' => 'tar', '--file' => "$tempdir/defaults_tar_format.tar", + '--statistics', 'postgres', ], restore_cmd => [ 'pg_restore', '--format' => 'tar', '--file' => "$tempdir/defaults_tar_format.sql", + '--statistics', "$tempdir/defaults_tar_format.tar", ], }, @@ -486,6 +265,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/exclude_dump_test_schema.sql", '--exclude-schema' => 'dump_test', + '--statistics', 'postgres', ], }, @@ -494,6 +274,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/exclude_test_table.sql", '--exclude-table' => 'dump_test.test_table', + '--statistics', 'postgres', ], }, @@ -502,6 +283,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/exclude_measurement.sql", '--exclude-table-and-children' => 'dump_test.measurement', + '--statistics', 'postgres', ], }, @@ -511,6 +293,7 @@ '--file' => "$tempdir/exclude_measurement_data.sql", '--exclude-table-data-and-children' => 'dump_test.measurement', '--no-unlogged-table-data', + '--statistics', 'postgres', ], }, @@ -520,6 +303,7 @@ '--file' => "$tempdir/exclude_test_table_data.sql", '--exclude-table-data' => 'dump_test.test_table', '--no-unlogged-table-data', + '--statistics', 'postgres', ], }, @@ -553,6 +337,7 @@ dump_cmd => [ 'pg_dumpall', '--no-sync', '--file' => "$tempdir/pg_dumpall_dbprivs.sql", + '--statistics', ], }, pg_dumpall_exclude => { @@ -562,6 +347,7 @@ '--file' => "$tempdir/pg_dumpall_exclude.sql", '--exclude-database' => '*dump_test*', '--no-sync', + '--statistics', ], }, no_toast_compression => { @@ -569,6 +355,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/no_toast_compression.sql", '--no-toast-compression', + '--statistics', 'postgres', ], }, @@ -577,6 +364,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/no_large_objects.sql", '--no-large-objects', + '--statistics', 'postgres', ], }, @@ -585,14 +373,33 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/no_policies.sql", '--no-policies', + '--statistics', 'postgres', ], }, + no_policies_restore => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/no_policies_restore.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/no_policies_restore.sql", + '--no-policies', + '--statistics', + "$tempdir/no_policies_restore.dump", + ], + }, no_privs => { dump_cmd => [ 'pg_dump', '--no-sync', '--file' => "$tempdir/no_privs.sql", '--no-privileges', + '--statistics', 'postgres', ], }, @@ -601,14 +408,42 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/no_owner.sql", '--no-owner', + '--statistics', + 'postgres', + ], + }, + no_subscriptions => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--file' => "$tempdir/no_subscriptions.sql", + '--no-subscriptions', + '--statistics', + 'postgres', + ], + }, + no_subscriptions_restore => { + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--file' => "$tempdir/no_subscriptions_restore.dump", + '--statistics', 'postgres', ], + restore_cmd => [ + 'pg_restore', + '--format' => 'custom', + '--file' => "$tempdir/no_subscriptions_restore.sql", + '--no-subscriptions', + '--statistics', + "$tempdir/no_subscriptions_restore.dump", + ], }, no_table_access_method => { dump_cmd => [ 'pg_dump', '--no-sync', '--file' => "$tempdir/no_table_access_method.sql", '--no-table-access-method', + '--statistics', 'postgres', ], }, @@ -617,6 +452,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/only_dump_test_schema.sql", '--schema' => 'dump_test', + '--statistics', 'postgres', ], }, @@ -627,6 +463,7 @@ '--table' => 'dump_test.test_table', '--lock-wait-timeout' => (1000 * $PostgreSQL::Test::Utils::timeout_default), + '--statistics', 'postgres', ], }, @@ -637,6 +474,7 @@ '--table-and-children' => 'dump_test.measurement', '--lock-wait-timeout' => (1000 * $PostgreSQL::Test::Utils::timeout_default), + '--statistics', 'postgres', ], }, @@ -646,6 +484,7 @@ '--file' => "$tempdir/role.sql", '--role' => 'regress_dump_test_role', '--schema' => 'dump_test_second_schema', + '--statistics', 'postgres', ], }, @@ -658,11 +497,13 @@ '--file' => "$tempdir/role_parallel", '--role' => 'regress_dump_test_role', '--schema' => 'dump_test_second_schema', + '--statistics', 'postgres', ], restore_cmd => [ 'pg_restore', '--file' => "$tempdir/role_parallel.sql", + '--statistics', "$tempdir/role_parallel", ], }, @@ -691,6 +532,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/section_pre_data.sql", '--section' => 'pre-data', + '--statistics', 'postgres', ], }, @@ -699,6 +541,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/section_data.sql", '--section' => 'data', + '--statistics', 'postgres', ], }, @@ -707,6 +550,7 @@ 'pg_dump', '--no-sync', '--file' => "$tempdir/section_post_data.sql", '--section' => 'post-data', + '--statistics', 'postgres', ], }, @@ -717,6 +561,7 @@ '--schema' => 'dump_test', '--large-objects', '--no-large-objects', + '--statistics', 'postgres', ], }, @@ -732,6 +577,7 @@ 'pg_dump', '--no-sync', "--file=$tempdir/no_data_no_schema.sql", '--no-data', '--no-schema', 'postgres', + '--statistics', ], }, statistics_only => { @@ -741,18 +587,11 @@ 'postgres', ], }, - schema_only_with_statistics => { - dump_cmd => [ - 'pg_dump', '--no-sync', - "--file=$tempdir/schema_only_with_statistics.sql", - '--schema-only', '--with-statistics', 'postgres', - ], - }, no_schema => { dump_cmd => [ 'pg_dump', '--no-sync', "--file=$tempdir/no_schema.sql", '--no-schema', - 'postgres', + '--statistics', 'postgres', ], },); @@ -788,10 +627,6 @@ # of the pg_dump runs happening. This is what "seeds" the # system with objects to be dumped out. # -# There can be a flag called 'lz4', which can be set if the test -# case depends on LZ4. Tests marked with this flag are skipped if -# the build used does not support LZ4. -# # Building of this hash takes a bit of time as all of the regexps # included in it are compiled. This greatly improves performance # as the regexps are used for each run the test applies to. @@ -808,7 +643,6 @@ binary_upgrade => 1, clean => 1, clean_if_exists => 1, - compression => 1, createdb => 1, defaults => 1, exclude_dump_test_schema => 1, @@ -820,8 +654,11 @@ no_large_objects => 1, no_owner => 1, no_policies => 1, + no_policies_restore => 1, no_privs => 1, no_statistics => 1, + no_subscriptions => 1, + no_subscriptions_restore => 1, no_table_access_method => 1, pg_dumpall_dbprivs => 1, pg_dumpall_exclude => 1, @@ -830,6 +667,16 @@ # This is where the actual tests are defined. my %tests = ( + 'restrict' => { + all_runs => 1, + regexp => qr/^\\restrict [a-zA-Z0-9]+$/m, + }, + + 'unrestrict' => { + all_runs => 1, + regexp => qr/^\\unrestrict [a-zA-Z0-9]+$/m, + }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT' => { create_order => 14, create_sql => 'ALTER DEFAULT PRIVILEGES @@ -1029,6 +876,7 @@ test_schema_plus_large_objects => 1, }, unlike => { + binary_upgrade => 1, no_large_objects => 1, no_owner => 1, schema_only => 1, @@ -1132,7 +980,9 @@ ) INHERITS (dump_test.test_table_nn, dump_test.test_table_nn_2); ALTER TABLE dump_test.test_table_nn ADD CONSTRAINT nn NOT NULL col1 NOT VALID; ALTER TABLE dump_test.test_table_nn_chld1 VALIDATE CONSTRAINT nn; - ALTER TABLE dump_test.test_table_nn_chld2 VALIDATE CONSTRAINT nn;', + ALTER TABLE dump_test.test_table_nn_chld2 VALIDATE CONSTRAINT nn; + COMMENT ON CONSTRAINT nn ON dump_test.test_table_nn IS \'nn comment is valid\'; + COMMENT ON CONSTRAINT nn ON dump_test.test_table_nn_chld2 IS \'nn_chld2 comment is valid\';', regexp => qr/^ \QALTER TABLE dump_test.test_table_nn\E \n^\s+ \QADD CONSTRAINT nn NOT NULL col1 NOT VALID;\E @@ -1146,6 +996,34 @@ }, }, + # This constraint is invalid therefore it goes in SECTION_POST_DATA + 'COMMENT ON CONSTRAINT ON test_table_nn' => { + regexp => qr/^ + \QCOMMENT ON CONSTRAINT nn ON dump_test.test_table_nn IS\E + /xm, + like => { + %full_runs, %dump_test_schema_runs, section_post_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + # This constraint is valid therefore it goes in SECTION_PRE_DATA + 'COMMENT ON CONSTRAINT ON test_table_chld2' => { + regexp => qr/^ + \QCOMMENT ON CONSTRAINT nn ON dump_test.test_table_nn_chld2 IS\E + /xm, + like => { + %full_runs, %dump_test_schema_runs, section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CONSTRAINT NOT NULL / NOT VALID (child1)' => { regexp => qr/^ \QCREATE TABLE dump_test.test_table_nn_chld1 (\E\n @@ -1190,6 +1068,43 @@ }, }, + 'CONSTRAINT NOT NULL / NO INHERIT' => { + create_sql => 'CREATE TABLE dump_test.test_table_nonn ( + col1 int NOT NULL NO INHERIT, + col2 int); + CREATE TABLE dump_test.test_table_nonn_chld1 ( + CONSTRAINT nn NOT NULL col2 NO INHERIT) + INHERITS (dump_test.test_table_nonn); ', + regexp => qr/^ + \QCREATE TABLE dump_test.test_table_nonn (\E \n^\s+ + \Qcol1 integer NOT NULL NO INHERIT\E + /xm, + like => { + %full_runs, %dump_test_schema_runs, + section_pre_data => 1, + binary_upgrade => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CONSTRAINT NOT NULL / NO INHERIT (child1)' => { + regexp => qr/^ + \QCREATE TABLE dump_test.test_table_nonn_chld1 (\E \n^\s+ + \QCONSTRAINT nn NOT NULL col2 NO INHERIT\E + /xm, + like => { + %full_runs, %dump_test_schema_runs, section_pre_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + binary_upgrade => 1, + }, + }, + 'CONSTRAINT PRIMARY KEY / WITHOUT OVERLAPS' => { create_sql => 'CREATE TABLE dump_test.test_table_tpk ( col1 int4range, @@ -1420,6 +1335,7 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, @@ -1517,6 +1433,7 @@ test_schema_plus_large_objects => 1, }, unlike => { + binary_upgrade => 1, schema_only => 1, schema_only_with_statistics => 1, no_large_objects => 1, @@ -1737,6 +1654,27 @@ }, }, + 'COMMENT ON POLICY p1' => { + create_order => 55, + create_sql => 'COMMENT ON POLICY p1 ON dump_test.test_table + IS \'comment on policy\';', + regexp => + qr/^COMMENT ON POLICY p1 ON dump_test.test_table IS 'comment on policy';/m, + like => { + %full_runs, + %dump_test_schema_runs, + only_dump_test_table => 1, + section_post_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + exclude_test_table => 1, + no_policies => 1, + no_policies_restore => 1, + only_dump_measurement => 1, + }, + }, + 'COMMENT ON PUBLICATION pub1' => { create_order => 55, create_sql => 'COMMENT ON PUBLICATION pub1 @@ -1753,6 +1691,10 @@ regexp => qr/^COMMENT ON SUBSCRIPTION sub1 IS 'comment on subscription';/m, like => { %full_runs, section_post_data => 1, }, + unlike => { + no_subscriptions => 1, + no_subscriptions_restore => 1, + }, }, 'COMMENT ON TEXT SEARCH CONFIGURATION dump_test.alt_ts_conf1' => { @@ -2141,6 +2083,22 @@ }, }, + 'newline of table name in comment' => { + create_sql => qq{-- meet getPartitioningInfo() "unsafe" condition + CREATE TYPE pp_colors AS + ENUM ('green', 'blue', 'black'); + CREATE TABLE pp_enumpart (a pp_colors) + PARTITION BY HASH (a); + CREATE TABLE pp_enumpart1 PARTITION OF pp_enumpart + FOR VALUES WITH (MODULUS 2, REMAINDER 0); + CREATE TABLE pp_enumpart2 PARTITION OF pp_enumpart + FOR VALUES WITH (MODULUS 2, REMAINDER 1); + ALTER TABLE pp_enumpart + RENAME TO "pp_enumpart\nattack";}, + regexp => qr/\n--[^\n]*\nattack/s, + like => {}, + }, + 'CREATE TABLESPACE regress_dump_tablespace' => { create_order => 2, create_sql => q( @@ -2289,17 +2247,19 @@ create_sql => 'CREATE DOMAIN dump_test.us_postal_code AS TEXT COLLATE "C" DEFAULT \'10014\' + CONSTRAINT nn NOT NULL CHECK(VALUE ~ \'^\d{5}$\' OR VALUE ~ \'^\d{5}-\d{4}$\'); + COMMENT ON CONSTRAINT nn + ON DOMAIN dump_test.us_postal_code IS \'not null\'; COMMENT ON CONSTRAINT us_postal_code_check ON DOMAIN dump_test.us_postal_code IS \'check it\';', regexp => qr/^ - \QCREATE DOMAIN dump_test.us_postal_code AS text COLLATE pg_catalog."C" DEFAULT '10014'::text\E\n\s+ + \QCREATE DOMAIN dump_test.us_postal_code AS text COLLATE pg_catalog."C" CONSTRAINT nn NOT NULL DEFAULT '10014'::text\E\n\s+ \QCONSTRAINT us_postal_code_check CHECK \E \Q(((VALUE ~ '^\d{5}\E \$\Q'::text) OR (VALUE ~ '^\d{5}-\d{4}\E\$ \Q'::text)));\E(.|\n)* - \QCOMMENT ON CONSTRAINT us_postal_code_check ON DOMAIN dump_test.us_postal_code IS 'check it';\E /xm, like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, @@ -2309,6 +2269,30 @@ }, }, + 'COMMENT ON CONSTRAINT ON DOMAIN (1)' => { + regexp => qr/^ + \QCOMMENT ON CONSTRAINT nn ON DOMAIN dump_test.us_postal_code IS 'not null';\E + /xm, + like => + { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'COMMENT ON CONSTRAINT ON DOMAIN (2)' => { + regexp => qr/^ + \QCOMMENT ON CONSTRAINT us_postal_code_check ON DOMAIN dump_test.us_postal_code IS 'check it';\E + /xm, + like => + { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'CREATE FUNCTION dump_test.pltestlang_call_handler' => { create_order => 17, create_sql => 'CREATE FUNCTION dump_test.pltestlang_call_handler() @@ -2989,31 +2973,6 @@ }, }, - 'CREATE MATERIALIZED VIEW matview_compression' => { - create_order => 20, - create_sql => 'CREATE MATERIALIZED VIEW - dump_test.matview_compression (col2) AS - SELECT col2 FROM dump_test.test_table; - ALTER MATERIALIZED VIEW dump_test.matview_compression - ALTER COLUMN col2 SET COMPRESSION lz4;', - regexp => qr/^ - \QCREATE MATERIALIZED VIEW dump_test.matview_compression AS\E - \n\s+\QSELECT col2\E - \n\s+\QFROM dump_test.test_table\E - \n\s+\QWITH NO DATA;\E - .* - \QALTER TABLE ONLY dump_test.matview_compression ALTER COLUMN col2 SET COMPRESSION lz4;\E\n - /xms, - lz4 => 1, - like => - { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, - unlike => { - exclude_dump_test_schema => 1, - no_toast_compression => 1, - only_dump_measurement => 1, - }, - }, - 'Check ordering of a matview that depends on a primary key' => { create_order => 42, create_sql => ' @@ -3052,6 +3011,7 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, @@ -3074,6 +3034,7 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, @@ -3096,6 +3057,7 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, @@ -3118,6 +3080,7 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, @@ -3140,6 +3103,7 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, @@ -3162,10 +3126,23 @@ exclude_dump_test_schema => 1, exclude_test_table => 1, no_policies => 1, + no_policies_restore => 1, only_dump_measurement => 1, }, }, + 'CREATE PROPERTY GRAPH propgraph' => { + create_order => 20, + create_sql => 'CREATE PROPERTY GRAPH dump_test.propgraph;', + regexp => qr/^ + \QCREATE PROPERTY GRAPH dump_test.propgraph\E; + /xm, + like => + { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, + unlike => + { exclude_dump_test_schema => 1, only_dump_measurement => 1, }, + }, + 'CREATE PUBLICATION pub1' => { create_order => 50, create_sql => 'CREATE PUBLICATION pub1;', @@ -3214,6 +3191,57 @@ like => { %full_runs, section_post_data => 1, }, }, + 'CREATE PUBLICATION pub6' => { + create_order => 50, + create_sql => 'CREATE PUBLICATION pub6 + FOR ALL SEQUENCES;', + regexp => qr/^ + \QCREATE PUBLICATION pub6 FOR ALL SEQUENCES WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub7' => { + create_order => 50, + create_sql => 'CREATE PUBLICATION pub7 + FOR ALL SEQUENCES, ALL TABLES + WITH (publish = \'\');', + regexp => qr/^ + \QCREATE PUBLICATION pub7 FOR ALL TABLES, ALL SEQUENCES WITH (publish = '');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub8' => { + create_order => 50, + create_sql => + 'CREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (TABLE dump_test.test_table);', + regexp => qr/^ + \QCREATE PUBLICATION pub8 FOR ALL TABLES EXCEPT (TABLE ONLY dump_test.test_table) WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub9' => { + create_order => 50, + create_sql => + 'CREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT (TABLE dump_test.test_table, dump_test.test_second_table);', + regexp => qr/^ + \QCREATE PUBLICATION pub9 FOR ALL TABLES EXCEPT (TABLE ONLY dump_test.test_table, TABLE ONLY dump_test.test_second_table) WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + + 'CREATE PUBLICATION pub10' => { + create_order => 92, + create_sql => + 'CREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT (TABLE dump_test.test_inheritance_parent);', + regexp => qr/^ + \QCREATE PUBLICATION pub10 FOR ALL TABLES EXCEPT (TABLE ONLY dump_test.test_inheritance_parent, TABLE ONLY dump_test.test_inheritance_child) WITH (publish = 'insert, update, delete, truncate');\E + /xm, + like => { %full_runs, section_post_data => 1, }, + }, + 'CREATE SUBSCRIPTION sub1' => { create_order => 50, create_sql => 'CREATE SUBSCRIPTION sub1 @@ -3223,6 +3251,10 @@ \QCREATE SUBSCRIPTION sub1 CONNECTION 'dbname=doesnotexist' PUBLICATION pub1 WITH (connect = false, slot_name = 'sub1', streaming = parallel);\E /xm, like => { %full_runs, section_post_data => 1, }, + unlike => { + no_subscriptions => 1, + no_subscriptions_restore => 1, + }, }, 'CREATE SUBSCRIPTION sub2' => { @@ -3234,6 +3266,10 @@ \QCREATE SUBSCRIPTION sub2 CONNECTION 'dbname=doesnotexist' PUBLICATION pub1 WITH (connect = false, slot_name = 'sub2', streaming = off, origin = none);\E /xm, like => { %full_runs, section_post_data => 1, }, + unlike => { + no_subscriptions => 1, + no_subscriptions_restore => 1, + }, }, 'CREATE SUBSCRIPTION sub3' => { @@ -3245,6 +3281,10 @@ \QCREATE SUBSCRIPTION sub3 CONNECTION 'dbname=doesnotexist' PUBLICATION pub1 WITH (connect = false, slot_name = 'sub3', streaming = on);\E /xm, like => { %full_runs, section_post_data => 1, }, + unlike => { + no_subscriptions => 1, + no_subscriptions_restore => 1, + }, }, @@ -3431,51 +3471,6 @@ }, }, - 'CREATE TABLE test_compression_method' => { - create_order => 110, - create_sql => 'CREATE TABLE dump_test.test_compression_method ( - col1 text - );', - regexp => qr/^ - \QCREATE TABLE dump_test.test_compression_method (\E\n - \s+\Qcol1 text\E\n - \Q);\E - /xm, - like => { - %full_runs, %dump_test_schema_runs, section_pre_data => 1, - }, - unlike => { - exclude_dump_test_schema => 1, - only_dump_measurement => 1, - }, - }, - - # Insert enough data to surpass DEFAULT_IO_BUFFER_SIZE during - # (de)compression operations - 'COPY test_compression_method' => { - create_order => 111, - create_sql => 'INSERT INTO dump_test.test_compression_method (col1) ' - . 'SELECT string_agg(a::text, \'\') FROM generate_series(1,4096) a;', - regexp => qr/^ - \QCOPY dump_test.test_compression_method (col1) FROM stdin;\E - \n(?:\d{15277}\n){1}\\\.\n - /xm, - like => { - %full_runs, - data_only => 1, - no_schema => 1, - section_data => 1, - only_dump_test_schema => 1, - test_schema_plus_large_objects => 1, - }, - unlike => { - binary_upgrade => 1, - exclude_dump_test_schema => 1, - schema_only => 1, - schema_only_with_statistics => 1, - }, - }, - 'CREATE TABLE fk_reference_test_table' => { create_order => 21, create_sql => 'CREATE TABLE dump_test.fk_reference_test_table ( @@ -3514,30 +3509,6 @@ }, }, - 'CREATE TABLE test_compression' => { - create_order => 3, - create_sql => 'CREATE TABLE dump_test.test_compression ( - col1 int, - col2 text COMPRESSION lz4 - );', - regexp => qr/^ - \QCREATE TABLE dump_test.test_compression (\E\n - \s+\Qcol1 integer,\E\n - \s+\Qcol2 text\E\n - \);\n - .* - \QALTER TABLE ONLY dump_test.test_compression ALTER COLUMN col2 SET COMPRESSION lz4;\E\n - /xms, - lz4 => 1, - like => - { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, - unlike => { - exclude_dump_test_schema => 1, - no_toast_compression => 1, - only_dump_measurement => 1, - }, - }, - 'CREATE TABLE measurement PARTITIONED BY' => { create_order => 90, create_sql => 'CREATE TABLE dump_test.measurement ( @@ -4109,7 +4080,6 @@ }, 'ALTER TABLE measurement PRIMARY KEY' => { - all_runs => 1, catch_all => 'CREATE ... commands', create_order => 93, create_sql => @@ -4161,7 +4131,6 @@ }, 'ALTER INDEX ... ATTACH PARTITION (primary key)' => { - all_runs => 1, catch_all => 'CREATE ... commands', regexp => qr/^ \QALTER INDEX dump_test.measurement_pkey ATTACH PARTITION dump_test_second_schema.measurement_y2006m2_pkey\E @@ -4524,9 +4493,9 @@ no_schema => 1, section_data => 1, test_schema_plus_large_objects => 1, - binary_upgrade => 1, }, unlike => { + binary_upgrade => 1, no_large_objects => 1, no_privs => 1, schema_only => 1, @@ -4551,6 +4520,22 @@ }, }, + 'GRANT SELECT ON PROPERTY GRAPH propgraph' => { + create_order => 21, + create_sql => + 'GRANT SELECT ON PROPERTY GRAPH dump_test.propgraph TO regress_dump_test_role;', + regexp => qr/^ + \QGRANT ALL ON PROPERTY GRAPH dump_test.propgraph TO regress_dump_test_role;\E + /xm, + like => + { %full_runs, %dump_test_schema_runs, section_pre_data => 1, }, + unlike => { + exclude_dump_test_schema => 1, + no_privs => 1, + only_dump_measurement => 1, + }, + }, + 'GRANT EXECUTE ON FUNCTION pg_sleep() TO regress_dump_test_role' => { create_order => 16, create_sql => 'GRANT EXECUTE ON FUNCTION pg_sleep(float8) @@ -4875,6 +4860,34 @@ }, }, + # + # EXTENDED stats will end up in SECTION_POST_DATA. + # + 'extended_statistics_import' => { + create_sql => ' + CREATE TABLE dump_test.has_ext_stats + AS SELECT g.g AS x, g.g / 2 AS y FROM generate_series(1,100) AS g(g); + CREATE STATISTICS dump_test.es1 ON x, (y % 2) FROM dump_test.has_ext_stats; + ANALYZE dump_test.has_ext_stats;', + regexp => qr/^ + \QSELECT * FROM pg_catalog.pg_restore_extended_stats(\E\s+/xm, + like => { + %full_runs, + %dump_test_schema_runs, + no_data_no_schema => 1, + no_schema => 1, + section_post_data => 1, + statistics_only => 1, + schema_only_with_statistics => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + no_statistics => 1, + only_dump_measurement => 1, + schema_only => 1, + }, + }, + # # While attribute stats (aka pg_statistic stats) only appear for tables # that have been analyzed, all tables will have relation stats because @@ -5022,13 +5035,6 @@ next; } - # Skip tests specific to LZ4 if this build does not support - # this option. - if (!$supports_lz4 && defined($tests{$test}->{lz4})) - { - next; - } - # Normalize command ending: strip all line endings, add # semicolon if missing, add two newlines. my $create_sql = $tests{$test}->{create_sql}; @@ -5091,6 +5097,17 @@ qr/\Qpg_dump: error: no matching schemas were found for pattern\E/, 'no matching schemas'); +command_fails_like( + [ + 'pg_dump', + '--port' => $port, + '--strict-names', + '--schema-only', + '--statistics', + ], + qr/\Qpg_dump: error: options --statistics and -s\/--schema-only cannot be used together\E/, + 'cannot use --statistics and --schema-only together'); + command_fails_like( [ 'pg_dump', @@ -5118,7 +5135,12 @@ # Test dumping pg_catalog (for research -- cannot be reloaded) $node->command_ok( - [ 'pg_dump', '--port' => $port, '--schema' => 'pg_catalog' ], + [ + 'pg_dump', + '--port' => $port, + '--schema' => 'pg_catalog', + '--file' => "$tempdir/pgdump_pgcatalog.dmp" + ], 'pg_dump: option -n pg_catalog'); ######################################### @@ -5128,7 +5150,8 @@ [ 'pg_dumpall', '--port' => $port, - '--exclude-database' => '"myhost.mydb"' + '--exclude-database' => '"myhost.mydb"', + '--file' => "$tempdir/pgdumpall.dmp" ], 'pg_dumpall: option --exclude-database handles database names with embedded dots' ); @@ -5194,50 +5217,21 @@ my $test_key = $run; my $run_db = 'postgres'; - # Skip command-level tests for gzip/lz4/zstd if the tool is not supported - if ($pgdump_runs{$run}->{compile_option} - && (($pgdump_runs{$run}->{compile_option} eq 'gzip' - && !$supports_gzip) - || ($pgdump_runs{$run}->{compile_option} eq 'lz4' - && !$supports_lz4) - || ($pgdump_runs{$run}->{compile_option} eq 'zstd' - && !$supports_zstd))) - { - note - "$run: skipped due to no $pgdump_runs{$run}->{compile_option} support"; - next; - } - $node->command_ok(\@{ $pgdump_runs{$run}->{dump_cmd} }, "$run: pg_dump runs"); - if ($pgdump_runs{$run}->{compress_cmd}) - { - my ($compress_cmd) = $pgdump_runs{$run}->{compress_cmd}; - my $compress_program = $compress_cmd->{program}; - - # Skip the rest of the test if the compression program is - # not defined. - next if (!defined($compress_program) || $compress_program eq ''); - - # Arguments may require globbing. - my @full_compress_cmd = ($compress_program); - foreach my $arg (@{ $compress_cmd->{args} }) - { - push @full_compress_cmd, glob($arg); - } - - command_ok(\@full_compress_cmd, "$run: compression commands"); - } - if ($pgdump_runs{$run}->{glob_patterns}) { my $glob_patterns = $pgdump_runs{$run}->{glob_patterns}; foreach my $glob_pattern (@{$glob_patterns}) { my @glob_output = glob($glob_pattern); - is(scalar(@glob_output) > 0, - 1, "$run: glob check for $glob_pattern"); + my $ok = 0; + # certainly found some files if glob() returned multiple matches + $ok = 1 if (scalar(@glob_output) > 1); + # if just one match, we need to check if it's real + $ok = 1 if (scalar(@glob_output) == 1 && -f $glob_output[0]); + is($ok, 1, "$run: glob check for $glob_pattern"); } } @@ -5283,9 +5277,10 @@ # Check for proper test definitions # - # There should be a "like" list, even if it is empty. (This - # makes the test more self-documenting.) - if (!defined($tests{$test}->{like})) + # Either "all_runs" should be set or there should be a "like" list, + # even if it is empty. (This makes the test more self-documenting.) + if (!defined($tests{$test}->{all_runs}) + && !defined($tests{$test}->{like})) { die "missing \"like\" in test \"$test\""; } @@ -5309,24 +5304,19 @@ next; } - # Skip tests specific to LZ4 if this build does not support - # this option. - if (!$supports_lz4 && defined($tests{$test}->{lz4})) - { - next; - } - if ($run_db ne $test_db) { next; } - # Run the test listed as a like, unless it is specifically noted - # as an unlike (generally due to an explicit exclusion or similar). - if ($tests{$test}->{like}->{$test_key} + # Run the test if all_runs is set or if listed as a like, unless it is + # specifically noted as an unlike (generally due to an explicit + # exclusion or similar). + if (($tests{$test}->{like}->{$test_key} || $tests{$test}->{all_runs}) && !defined($tests{$test}->{unlike}->{$test_key})) { - if (!ok($output_file =~ $tests{$test}->{regexp}, + if (!like( + $output_file, $tests{$test}->{regexp}, "$run: should dump $test")) { diag("Review $run results in $tempdir"); @@ -5334,7 +5324,8 @@ } else { - if (!ok($output_file !~ $tests{$test}->{regexp}, + if (!unlike( + $output_file, $tests{$test}->{regexp}, "$run: should not dump $test")) { diag("Review $run results in $tempdir"); diff --git a/src/bin/pg_dump/t/003_pg_dump_with_server.pl b/src/bin/pg_dump/t/003_pg_dump_with_server.pl index 8dc014ed6ed34..349add67d5043 100644 --- a/src/bin/pg_dump/t/003_pg_dump_with_server.pl +++ b/src/bin/pg_dump/t/003_pg_dump_with_server.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_dump/t/004_pg_dump_parallel.pl b/src/bin/pg_dump/t/004_pg_dump_parallel.pl index fcbe74ec8e981..738f34b1c1b86 100644 --- a/src/bin/pg_dump/t/004_pg_dump_parallel.pl +++ b/src/bin/pg_dump/t/004_pg_dump_parallel.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_dump/t/005_pg_dump_filterfile.pl b/src/bin/pg_dump/t/005_pg_dump_filterfile.pl index f05e8a20e0559..b2630ef289733 100644 --- a/src/bin/pg_dump/t/005_pg_dump_filterfile.pl +++ b/src/bin/pg_dump/t/005_pg_dump_filterfile.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -100,10 +100,12 @@ my $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.table_one/m, "table one dumped"); -ok($dump =~ qr/^CREATE TABLE public\.table_two/m, "table two dumped"); -ok($dump =~ qr/^CREATE TABLE public\.table_three/m, "table three dumped"); -ok($dump =~ qr/^CREATE TABLE public\.table_three_one/m, +like($dump, qr/^CREATE TABLE public\.table_one/m, "table one dumped"); +like($dump, qr/^CREATE TABLE public\.table_two/m, "table two dumped"); +like($dump, qr/^CREATE TABLE public\.table_three/m, "table three dumped"); +like( + $dump, + qr/^CREATE TABLE public\.table_three_one/m, "table three one dumped"); # Test various combinations of whitespace, comments and correct filters @@ -130,14 +132,21 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.table_one/m, "dumped table one"); -ok($dump =~ qr/^CREATE TABLE public\.table_two/m, "dumped table two"); -ok($dump !~ qr/^CREATE TABLE public\.table_three/m, "table three not dumped"); -ok($dump !~ qr/^CREATE TABLE public\.table_three_one/m, +like($dump, qr/^CREATE TABLE public\.table_one/m, "dumped table one"); +like($dump, qr/^CREATE TABLE public\.table_two/m, "dumped table two"); +unlike( + $dump, + qr/^CREATE TABLE public\.table_three/m, + "table three not dumped"); +unlike( + $dump, + qr/^CREATE TABLE public\.table_three_one/m, "table three_one not dumped"); -ok( $dump !~ qr/^COPY public\.table_one/m, +unlike( + $dump, + qr/^COPY public\.table_one/m, "content of table one is not included"); -ok($dump =~ qr/^COPY public\.table_two/m, "content of table two is included"); +like($dump, qr/^COPY public\.table_two/m, "content of table two is included"); # Test dumping tables specified by qualified names open $inputfile, '>', "$tempdir/inputfile.txt" @@ -159,9 +168,9 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.table_one/m, "dumped table one"); -ok($dump =~ qr/^CREATE TABLE public\.table_two/m, "dumped table two"); -ok($dump =~ qr/^CREATE TABLE public\.table_three/m, "dumped table three"); +like($dump, qr/^CREATE TABLE public\.table_one/m, "dumped table one"); +like($dump, qr/^CREATE TABLE public\.table_two/m, "dumped table two"); +like($dump, qr/^CREATE TABLE public\.table_three/m, "dumped table three"); # Test dumping all tables except one open $inputfile, '>', "$tempdir/inputfile.txt" @@ -181,10 +190,12 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE public\.table_one/m, "table one not dumped"); -ok($dump =~ qr/^CREATE TABLE public\.table_two/m, "dumped table two"); -ok($dump =~ qr/^CREATE TABLE public\.table_three/m, "dumped table three"); -ok($dump =~ qr/^CREATE TABLE public\.table_three_one/m, +unlike($dump, qr/^CREATE TABLE public\.table_one/m, "table one not dumped"); +like($dump, qr/^CREATE TABLE public\.table_two/m, "dumped table two"); +like($dump, qr/^CREATE TABLE public\.table_three/m, "dumped table three"); +like( + $dump, + qr/^CREATE TABLE public\.table_three_one/m, "dumped table three_one"); # Test dumping tables with a wildcard pattern @@ -205,10 +216,12 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE public\.table_one/m, "table one not dumped"); -ok($dump !~ qr/^CREATE TABLE public\.table_two/m, "table two not dumped"); -ok($dump =~ qr/^CREATE TABLE public\.table_three/m, "dumped table three"); -ok($dump =~ qr/^CREATE TABLE public\.table_three_one/m, +unlike($dump, qr/^CREATE TABLE public\.table_one/m, "table one not dumped"); +unlike($dump, qr/^CREATE TABLE public\.table_two/m, "table two not dumped"); +like($dump, qr/^CREATE TABLE public\.table_three/m, "dumped table three"); +like( + $dump, + qr/^CREATE TABLE public\.table_three_one/m, "dumped table three_one"); # Test dumping table with multiline quoted tablename @@ -230,7 +243,9 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public.\"strange aaa/m, +like( + $dump, + qr/^CREATE TABLE public.\"strange aaa/m, "dump table with new line in name"); # Test excluding multiline quoted tablename from dump @@ -251,7 +266,9 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE public.\"strange aaa/m, +unlike( + $dump, + qr/^CREATE TABLE public.\"strange aaa/m, "dump table with new line in name"); # Test excluding an entire schema @@ -272,7 +289,7 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE/m, "no table dumped"); +unlike($dump, qr/^CREATE TABLE/m, "no table dumped"); # Test including and excluding an entire schema by multiple filterfiles open $inputfile, '>', "$tempdir/inputfile.txt" @@ -298,7 +315,7 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE/m, "no table dumped"); +unlike($dump, qr/^CREATE TABLE/m, "no table dumped"); # Test dumping a table with a single leading newline on a row open $inputfile, '>', "$tempdir/inputfile.txt" @@ -321,7 +338,9 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public.\"\nt\nt\n\" \($/ms, +like( + $dump, + qr/^CREATE TABLE public.\"\nt\nt\n\" \($/ms, "dump table with multiline strange name"); open $inputfile, '>', "$tempdir/inputfile.txt" @@ -341,7 +360,9 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public.\"\nt\nt\n\" \($/ms, +like( + $dump, + qr/^CREATE TABLE public.\"\nt\nt\n\" \($/ms, "dump table with multiline strange name"); ######################################### @@ -380,7 +401,7 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE SERVER dummyserver/m, "dump foreign server"); +like($dump, qr/^CREATE SERVER dummyserver/m, "dump foreign server"); open $inputfile, '>', "$tempdir/inputfile.txt" or die "unable to open filterfile for writing"; @@ -418,10 +439,16 @@ qr/invalid filter command/, "invalid syntax: incorrect filter command"); -# Test invalid object type +# Test invalid object type. +# +# This test also verifies that keywords are correctly recognized as strings of +# non-whitespace characters. If the parser incorrectly treats non-whitespace +# delimiters (like hyphens) as keyword boundaries, "table-data" might be +# misread as the valid object type "table". To catch such issues, +# "table-data" is used here as an intentionally invalid object type. open $inputfile, '>', "$tempdir/inputfile.txt" or die "unable to open filterfile for writing"; -print $inputfile "include xxx"; +print $inputfile "exclude table-data one"; close $inputfile; command_fails_like( @@ -432,8 +459,8 @@ '--filter' => "$tempdir/inputfile.txt", 'postgres' ], - qr/unsupported filter object type: "xxx"/, - "invalid syntax: invalid object type specified, should be table, schema, foreign_data or data" + qr/unsupported filter object type: "table-data"/, + "invalid syntax: invalid object type specified" ); # Test missing object identifier pattern @@ -491,7 +518,7 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.table_one/m, "no table dumped"); +like($dump, qr/^CREATE TABLE public\.table_one/m, "no table dumped"); # Now append a pattern to the filter file which doesn't resolve open $inputfile, '>>', "$tempdir/inputfile.txt" @@ -531,10 +558,10 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^\\connect postgres/m, "database postgres is not dumped"); -ok($dump =~ qr/^\\connect template1/m, "database template1 is dumped"); +unlike($dump, qr/^\\connect postgres/m, "database postgres is not dumped"); +like($dump, qr/^\\connect template1/m, "database template1 is dumped"); -# Make sure this option dont break the existing limitation of using +# Make sure this option doesn't break the existing limitation of using # --globals-only with exclusions command_fails_like( [ @@ -544,8 +571,8 @@ '--filter' => "$tempdir/inputfile.txt", '--globals-only' ], - qr/\Qpg_dumpall: error: option --exclude-database cannot be used together with -g\/--globals-only\E/, - 'pg_dumpall: option --exclude-database cannot be used together with -g/--globals-only' + qr/\Qpg_dumpall: error: options --exclude-database and -g\/--globals-only cannot be used together\E/, + 'pg_dumpall: options --exclude-database and -g/--globals-only cannot be used together' ); # Test invalid filter command @@ -626,8 +653,10 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.table_two/m, "wanted table restored"); -ok($dump !~ qr/^CREATE TABLE public\.table_one/m, +like($dump, qr/^CREATE TABLE public\.table_two/m, "wanted table restored"); +unlike( + $dump, + qr/^CREATE TABLE public\.table_one/m, "unwanted table is not restored"); open $inputfile, '>', "$tempdir/inputfile.txt" @@ -721,8 +750,10 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE FUNCTION public\.foo1/m, "wanted function restored"); -ok( $dump !~ qr/^CREATE TABLE public\.foo2/m, +like($dump, qr/^CREATE FUNCTION public\.foo1/m, "wanted function restored"); +unlike( + $dump, + qr/^CREATE TABLE public\.foo2/m, "unwanted function is not restored"); # this should be white space tolerant (against the -P argument) @@ -745,7 +776,7 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE FUNCTION public\.foo3/m, "wanted function restored"); +like($dump, qr/^CREATE FUNCTION public\.foo3/m, "wanted function restored"); open $inputfile, '>', "$tempdir/inputfile.txt" or die "unable to open filterfile for writing"; @@ -769,10 +800,10 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE INDEX t1_idx1/m, "wanted index restored"); -ok($dump !~ qr/^CREATE INDEX t2_idx2/m, "unwanted index are not restored"); -ok($dump =~ qr/^CREATE TRIGGER trg1/m, "wanted trigger restored"); -ok($dump !~ qr/^CREATE TRIGGER trg2/m, "unwanted trigger is not restored"); +like($dump, qr/^CREATE INDEX t1_idx1/m, "wanted index restored"); +unlike($dump, qr/^CREATE INDEX t2_idx2/m, "unwanted index are not restored"); +like($dump, qr/^CREATE TRIGGER trg1/m, "wanted trigger restored"); +unlike($dump, qr/^CREATE TRIGGER trg2/m, "unwanted trigger is not restored"); open $inputfile, '>', "$tempdir/inputfile.txt" or die "unable to open filterfile for writing"; @@ -792,10 +823,12 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE s1\.t1/m, "wanted table from schema restored"); -ok( $dump =~ qr/^CREATE SEQUENCE s1\.s1/m, +like($dump, qr/^CREATE TABLE s1\.t1/m, "wanted table from schema restored"); +like( + $dump, + qr/^CREATE SEQUENCE s1\.s1/m, "wanted sequence from schema restored"); -ok($dump !~ qr/^CREATE TABLE s2\t2/m, "unwanted table is not restored"); +unlike($dump, qr/^CREATE TABLE s2\t2/m, "unwanted table is not restored"); open $inputfile, '>', "$tempdir/inputfile.txt" or die "unable to open filterfile for writing"; @@ -815,12 +848,16 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE s1\.t1/m, +unlike( + $dump, + qr/^CREATE TABLE s1\.t1/m, "unwanted table from schema is not restored"); -ok($dump !~ qr/^CREATE SEQUENCE s1\.s1/m, +unlike( + $dump, + qr/^CREATE SEQUENCE s1\.s1/m, "unwanted sequence from schema is not restored"); -ok($dump =~ qr/^CREATE TABLE s2\.t2/m, "wanted table restored"); -ok($dump =~ qr/^CREATE TABLE public\.t1/m, "wanted table restored"); +like($dump, qr/^CREATE TABLE s2\.t2/m, "wanted table restored"); +like($dump, qr/^CREATE TABLE public\.t1/m, "wanted table restored"); ######################################### # test of supported syntax @@ -843,7 +880,7 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.bootab/m, "dumped children table"); +like($dump, qr/^CREATE TABLE public\.bootab/m, "dumped children table"); open $inputfile, '>', "$tempdir/inputfile.txt" or die "unable to open filterfile for writing"; @@ -863,7 +900,9 @@ $dump = slurp_file($plainfile); -ok($dump !~ qr/^CREATE TABLE public\.bootab/m, +unlike( + $dump, + qr/^CREATE TABLE public\.bootab/m, "exclude dumped children table"); open $inputfile, '>', "$tempdir/inputfile.txt" @@ -884,8 +923,8 @@ $dump = slurp_file($plainfile); -ok($dump =~ qr/^CREATE TABLE public\.bootab/m, "dumped children table"); -ok($dump !~ qr/^COPY public\.bootab/m, "exclude dumped children table"); +like($dump, qr/^CREATE TABLE public\.bootab/m, "dumped children table"); +unlike($dump, qr/^COPY public\.bootab/m, "exclude dumped children table"); ######################################### # Test extension diff --git a/src/bin/pg_dump/t/006_pg_dump_compress.pl b/src/bin/pg_dump/t/006_pg_dump_compress.pl new file mode 100644 index 0000000000000..d4ce6b180771d --- /dev/null +++ b/src/bin/pg_dump/t/006_pg_dump_compress.pl @@ -0,0 +1,639 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +############################################################### +# This test script uses essentially the same structure as +# 002_pg_dump.pl, but is specialized to deal with compression +# concerns. As such, some of the test cases here are large +# and would contribute undue amounts of runtime if they were +# included in 002_pg_dump.pl. +############################################################### + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $tempdir = PostgreSQL::Test::Utils::tempdir; + +############################################################### +# Definition of the pg_dump runs to make. +# +# In addition to the facilities explained in 002_pg_dump.pl, +# these entries can include: +# +# compile_option indicates if the test depends on a compilation +# option, if any. This can be used to control if tests should be +# skipped when a build dependency is not satisfied. +# +# compress_cmd is the utility command for (de)compression, if any. +# Note that this should generally be used on pg_dump's output +# either to generate a text file to run the through the tests, or +# to test pg_restore's ability to parse manually compressed files +# that otherwise pg_dump does not compress on its own (e.g. *.toc). + +my $supports_gzip = check_pg_config("#define HAVE_LIBZ 1"); +my $supports_lz4 = check_pg_config("#define USE_LZ4 1"); +my $supports_zstd = check_pg_config("#define USE_ZSTD 1"); + +my %pgdump_runs = ( + compression_none_custom => { + test_key => 'compression', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--compress' => 'none', + '--file' => "$tempdir/compression_none_custom.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--file' => "$tempdir/compression_none_custom.sql", + '--statistics', + "$tempdir/compression_none_custom.dump", + ], + }, + + compression_gzip_custom => { + test_key => 'compression', + compile_option => 'gzip', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--compress' => '1', + '--file' => "$tempdir/compression_gzip_custom.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--file' => "$tempdir/compression_gzip_custom.sql", + '--statistics', + "$tempdir/compression_gzip_custom.dump", + ], + command_like => { + command => [ + 'pg_restore', '--list', + "$tempdir/compression_gzip_custom.dump", + ], + expected => qr/Compression: gzip/, + name => 'data content is gzip-compressed' + }, + }, + + compression_gzip_dir => { + test_key => 'compression', + compile_option => 'gzip', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--jobs' => '2', + '--format' => 'directory', + '--compress' => 'gzip:1', + '--file' => "$tempdir/compression_gzip_dir", + '--statistics', + 'postgres', + ], + # Give coverage for manually-compressed TOC files during restore. + compress_cmd => { + program => $ENV{'GZIP_PROGRAM'}, + args => [ + '-f', + "$tempdir/compression_gzip_dir/toc.dat", + "$tempdir/compression_gzip_dir/blobs_*.toc", + ], + }, + # Verify that TOC and data files were compressed + glob_patterns => [ + "$tempdir/compression_gzip_dir/toc.dat.gz", + "$tempdir/compression_gzip_dir/*.dat.gz", + ], + restore_cmd => [ + 'pg_restore', + '--jobs' => '2', + '--file' => "$tempdir/compression_gzip_dir.sql", + '--statistics', + "$tempdir/compression_gzip_dir", + ], + }, + + compression_gzip_plain => { + test_key => 'compression', + compile_option => 'gzip', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'plain', + '--compress' => '1', + '--file' => "$tempdir/compression_gzip_plain.sql.gz", + '--statistics', + 'postgres', + ], + # Decompress the generated file to run through the tests. + compress_cmd => { + program => $ENV{'GZIP_PROGRAM'}, + args => [ '-d', "$tempdir/compression_gzip_plain.sql.gz", ], + }, + }, + + compression_lz4_custom => { + test_key => 'compression', + compile_option => 'lz4', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--compress' => 'lz4', + '--file' => "$tempdir/compression_lz4_custom.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--file' => "$tempdir/compression_lz4_custom.sql", + '--statistics', + "$tempdir/compression_lz4_custom.dump", + ], + command_like => { + command => [ + 'pg_restore', '--list', + "$tempdir/compression_lz4_custom.dump", + ], + expected => qr/Compression: lz4/, + name => 'data content is lz4 compressed' + }, + }, + + compression_lz4_dir => { + test_key => 'compression', + compile_option => 'lz4', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--jobs' => '2', + '--format' => 'directory', + '--compress' => 'lz4:1', + '--file' => "$tempdir/compression_lz4_dir", + '--statistics', + 'postgres', + ], + # Give coverage for manually-compressed TOC files during restore. + compress_cmd => { + program => $ENV{'LZ4'}, + args => [ + '-z', '-f', '-m', '--rm', + "$tempdir/compression_lz4_dir/toc.dat", + "$tempdir/compression_lz4_dir/blobs_*.toc", + ], + }, + # Verify that TOC and data files were compressed + glob_patterns => [ + "$tempdir/compression_lz4_dir/toc.dat.lz4", + "$tempdir/compression_lz4_dir/*.dat.lz4", + ], + restore_cmd => [ + 'pg_restore', + '--jobs' => '2', + '--file' => "$tempdir/compression_lz4_dir.sql", + '--statistics', + "$tempdir/compression_lz4_dir", + ], + }, + + compression_lz4_plain => { + test_key => 'compression', + compile_option => 'lz4', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'plain', + '--compress' => 'lz4', + '--file' => "$tempdir/compression_lz4_plain.sql.lz4", + '--statistics', + 'postgres', + ], + # Decompress the generated file to run through the tests. + compress_cmd => { + program => $ENV{'LZ4'}, + args => [ + '-d', '-f', + "$tempdir/compression_lz4_plain.sql.lz4", + "$tempdir/compression_lz4_plain.sql", + ], + }, + }, + + compression_zstd_custom => { + test_key => 'compression', + compile_option => 'zstd', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'custom', + '--compress' => 'zstd', + '--file' => "$tempdir/compression_zstd_custom.dump", + '--statistics', + 'postgres', + ], + restore_cmd => [ + 'pg_restore', + '--file' => "$tempdir/compression_zstd_custom.sql", + '--statistics', + "$tempdir/compression_zstd_custom.dump", + ], + command_like => { + command => [ + 'pg_restore', '--list', + "$tempdir/compression_zstd_custom.dump", + ], + expected => qr/Compression: zstd/, + name => 'data content is zstd compressed' + }, + }, + + compression_zstd_dir => { + test_key => 'compression', + compile_option => 'zstd', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--jobs' => '2', + '--format' => 'directory', + '--compress' => 'zstd:1', + '--file' => "$tempdir/compression_zstd_dir", + '--statistics', + 'postgres', + ], + # Give coverage for manually-compressed TOC files during restore. + compress_cmd => { + program => $ENV{'ZSTD'}, + args => [ + '-z', '-f', '--rm', + "$tempdir/compression_zstd_dir/toc.dat", + "$tempdir/compression_zstd_dir/blobs_*.toc", + ], + }, + # Verify that TOC and data files were compressed + glob_patterns => [ + "$tempdir/compression_zstd_dir/toc.dat.zst", + "$tempdir/compression_zstd_dir/*.dat.zst", + ], + restore_cmd => [ + 'pg_restore', + '--jobs' => '2', + '--file' => "$tempdir/compression_zstd_dir.sql", + '--statistics', + "$tempdir/compression_zstd_dir", + ], + }, + + # Exercise long mode for test coverage + compression_zstd_plain => { + test_key => 'compression', + compile_option => 'zstd', + dump_cmd => [ + 'pg_dump', '--no-sync', + '--format' => 'plain', + '--compress' => 'zstd:long', + '--file' => "$tempdir/compression_zstd_plain.sql.zst", + '--statistics', + 'postgres', + ], + # Decompress the generated file to run through the tests. + compress_cmd => { + program => $ENV{'ZSTD'}, + args => [ + '-d', '-f', + "$tempdir/compression_zstd_plain.sql.zst", "-o", + "$tempdir/compression_zstd_plain.sql", + ], + }, + },); + +############################################################### +# Definition of the tests to run. +# +# In addition to the facilities explained in 002_pg_dump.pl, +# these entries can include: +# +# compile_option indicates if the test depends on a compilation +# option, if any. This can be used to control if tests should be +# skipped when a build dependency is not satisfied. + +# Tests which are considered 'full' dumps by pg_dump, but there +# are flags used to exclude specific items (ACLs, LOs, etc). +my %full_runs = (compression => 1,); + +# This is where the actual tests are defined. +my %tests = ( + 'CREATE MATERIALIZED VIEW matview_compression_lz4' => { + create_order => 20, + create_sql => 'CREATE MATERIALIZED VIEW + matview_compression_lz4 (col2) AS + SELECT repeat(\'xyzzy\', 10000); + ALTER MATERIALIZED VIEW matview_compression_lz4 + ALTER COLUMN col2 SET COMPRESSION lz4;', + regexp => qr/^ + \QCREATE MATERIALIZED VIEW public.matview_compression_lz4 AS\E + \n\s+\QSELECT repeat('xyzzy'::text, 10000) AS col2\E + \n\s+\QWITH NO DATA;\E + .* + \QALTER TABLE ONLY public.matview_compression_lz4 ALTER COLUMN col2 SET COMPRESSION lz4;\E\n + /xms, + compile_option => 'lz4', + like => {%full_runs}, + }, + + 'CREATE TABLE test_compression_method' => { + create_order => 110, + create_sql => 'CREATE TABLE test_compression_method ( + col1 text + );', + regexp => qr/^ + \QCREATE TABLE public.test_compression_method (\E\n + \s+\Qcol1 text\E\n + \Q);\E + /xm, + like => { %full_runs, }, + }, + + # Insert enough data to surpass DEFAULT_IO_BUFFER_SIZE during + # (de)compression operations. The weird regex is because Perl + # restricts us to repeat counts of less than 32K. + 'COPY test_compression_method' => { + create_order => 111, + create_sql => 'INSERT INTO test_compression_method (col1) ' + . 'SELECT string_agg(a::text, \'\') FROM generate_series(1,65536) a;', + regexp => qr/^ + \QCOPY public.test_compression_method (col1) FROM stdin;\E + \n(?:(?:\d\d\d\d\d\d\d\d\d\d){31657}\d\d\d\d\n){1}\\\.\n + /xm, + like => { %full_runs, }, + }, + + 'CREATE TABLE test_compression' => { + create_order => 3, + create_sql => 'CREATE TABLE test_compression ( + col1 int, + col2 text COMPRESSION lz4 + );', + regexp => qr/^ + \QCREATE TABLE public.test_compression (\E\n + \s+\Qcol1 integer,\E\n + \s+\Qcol2 text\E\n + \);\n + .* + \QALTER TABLE ONLY public.test_compression ALTER COLUMN col2 SET COMPRESSION lz4;\E\n + /xms, + compile_option => 'lz4', + like => {%full_runs}, + }, + + # Create a large object so we can test compression of blobs.toc + 'LO create (using lo_from_bytea)' => { + create_order => 50, + create_sql => + 'SELECT pg_catalog.lo_from_bytea(0, \'\\x310a320a330a340a350a360a370a380a390a\');', + regexp => qr/^SELECT pg_catalog\.lo_create\('\d+'\);/m, + like => { %full_runs, }, + }, + + 'LO load (using lo_from_bytea)' => { + regexp => qr/^ + \QSELECT pg_catalog.lo_open\E \('\d+',\ \d+\);\n + \QSELECT pg_catalog.lowrite(0, \E + \Q'\x310a320a330a340a350a360a370a380a390a');\E\n + \QSELECT pg_catalog.lo_close(0);\E + /xm, + like => { %full_runs, }, + },); + +######################################### +# Create a PG instance to test actually dumping from + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +my $port = $node->port; + +######################################### +# Set up schemas, tables, etc, to be dumped. + +# Build up the create statements +my %create_sql = (); + +foreach my $test ( + sort { + if ($tests{$a}->{create_order} and $tests{$b}->{create_order}) + { + $tests{$a}->{create_order} <=> $tests{$b}->{create_order}; + } + elsif ($tests{$a}->{create_order}) + { + -1; + } + elsif ($tests{$b}->{create_order}) + { + 1; + } + else + { + 0; + } + } keys %tests) +{ + my $test_db = 'postgres'; + + if (defined($tests{$test}->{database})) + { + $test_db = $tests{$test}->{database}; + } + + # Skip tests that require an unsupported compile option + if ($tests{$test}->{compile_option} + && (($tests{$test}->{compile_option} eq 'gzip' && !$supports_gzip) + || ($tests{$test}->{compile_option} eq 'lz4' + && !$supports_lz4) + || ($tests{$test}->{compile_option} eq 'zstd' + && !$supports_zstd))) + { + next; + } + + if ($tests{$test}->{create_sql}) + { + # Normalize command ending: strip all line endings, add + # semicolon if missing, add two newlines. + my $create_sql = $tests{$test}->{create_sql}; + chomp $create_sql; + $create_sql .= ';' unless substr($create_sql, -1) eq ';'; + $create_sql{$test_db} .= $create_sql . "\n\n"; + } +} + +# Send the combined set of commands to psql +foreach my $db (sort keys %create_sql) +{ + $node->safe_psql($db, $create_sql{$db}); +} + +######################################### +# Run all runs + +foreach my $run (sort keys %pgdump_runs) +{ + my $test_key = $run; + my $run_db = 'postgres'; + + # Skip runs that require an unsupported compile option + if ($pgdump_runs{$run}->{compile_option} + && (($pgdump_runs{$run}->{compile_option} eq 'gzip' + && !$supports_gzip) + || ($pgdump_runs{$run}->{compile_option} eq 'lz4' + && !$supports_lz4) + || ($pgdump_runs{$run}->{compile_option} eq 'zstd' + && !$supports_zstd))) + { + note + "$run: skipped due to no $pgdump_runs{$run}->{compile_option} support"; + next; + } + + $node->command_ok(\@{ $pgdump_runs{$run}->{dump_cmd} }, + "$run: pg_dump runs"); + + if ($pgdump_runs{$run}->{compress_cmd}) + { + my ($compress_cmd) = $pgdump_runs{$run}->{compress_cmd}; + my $compress_program = $compress_cmd->{program}; + + # Skip the rest of the test if the compression program is + # not defined. + next if (!defined($compress_program) || $compress_program eq ''); + + # Arguments may require globbing. + my @full_compress_cmd = ($compress_program); + foreach my $arg (@{ $compress_cmd->{args} }) + { + push @full_compress_cmd, glob($arg); + } + + command_ok(\@full_compress_cmd, "$run: compression commands"); + } + + if ($pgdump_runs{$run}->{glob_patterns}) + { + my $glob_patterns = $pgdump_runs{$run}->{glob_patterns}; + foreach my $glob_pattern (@{$glob_patterns}) + { + my @glob_output = glob($glob_pattern); + my $ok = 0; + # certainly found some files if glob() returned multiple matches + $ok = 1 if (scalar(@glob_output) > 1); + # if just one match, we need to check if it's real + $ok = 1 if (scalar(@glob_output) == 1 && -f $glob_output[0]); + is($ok, 1, "$run: glob check for $glob_pattern"); + } + } + + if ($pgdump_runs{$run}->{command_like}) + { + my $cmd_like = $pgdump_runs{$run}->{command_like}; + $node->command_like( + \@{ $cmd_like->{command} }, + $cmd_like->{expected}, + "$run: " . $cmd_like->{name}); + } + + if ($pgdump_runs{$run}->{restore_cmd}) + { + $node->command_ok(\@{ $pgdump_runs{$run}->{restore_cmd} }, + "$run: pg_restore runs"); + } + + if ($pgdump_runs{$run}->{test_key}) + { + $test_key = $pgdump_runs{$run}->{test_key}; + } + + my $output_file = slurp_file("$tempdir/${run}.sql"); + + ######################################### + # Run all tests where this run is included + # as either a 'like' or 'unlike' test. + + foreach my $test (sort keys %tests) + { + my $test_db = 'postgres'; + + if (defined($pgdump_runs{$run}->{database})) + { + $run_db = $pgdump_runs{$run}->{database}; + } + + if (defined($tests{$test}->{database})) + { + $test_db = $tests{$test}->{database}; + } + + # Check for proper test definitions + # + # Either "all_runs" should be set or there should be a "like" list, + # even if it is empty. (This makes the test more self-documenting.) + if ( !defined($tests{$test}->{all_runs}) + && !defined($tests{$test}->{like})) + { + die "missing \"like\" in test \"$test\""; + } + # Check for useless entries in "unlike" list. Runs that are + # not listed in "like" don't need to be excluded in "unlike". + if ($tests{$test}->{unlike}->{$test_key} + && !defined($tests{$test}->{like}->{$test_key})) + { + die "useless \"unlike\" entry \"$test_key\" in test \"$test\""; + } + + # Skip tests that require an unsupported compile option + if ($tests{$test}->{compile_option} + && (($tests{$test}->{compile_option} eq 'gzip' && !$supports_gzip) + || ($tests{$test}->{compile_option} eq 'lz4' + && !$supports_lz4) + || ($tests{$test}->{compile_option} eq 'zstd' + && !$supports_zstd))) + { + next; + } + + if ($run_db ne $test_db) + { + next; + } + + # Run the test if all_runs is set or if listed as a like, unless it is + # specifically noted as an unlike (generally due to an explicit + # exclusion or similar). + if (($tests{$test}->{like}->{$test_key} || $tests{$test}->{all_runs}) + && !defined($tests{$test}->{unlike}->{$test_key})) + { + if (!like( + $output_file, $tests{$test}->{regexp}, + "$run: should dump $test")) + { + diag("Review $run results in $tempdir"); + } + } + else + { + if (!unlike( + $output_file, $tests{$test}->{regexp}, + "$run: should not dump $test")) + { + diag("Review $run results in $tempdir"); + } + } + } +} + +######################################### +# Stop the database instance, which will be removed at the end of the tests. + +$node->stop('fast'); + +done_testing(); diff --git a/src/bin/pg_dump/t/006_pg_dumpall.pl b/src/bin/pg_dump/t/007_pg_dumpall.pl similarity index 50% rename from src/bin/pg_dump/t/006_pg_dumpall.pl rename to src/bin/pg_dump/t/007_pg_dumpall.pl index 5acd49f1559d2..22f11a13a9a68 100644 --- a/src/bin/pg_dump/t/006_pg_dumpall.pl +++ b/src/bin/pg_dump/t/007_pg_dumpall.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -17,7 +17,8 @@ mkdir($tablespace1) || die "mkdir $tablespace1 $!"; mkdir($tablespace2) || die "mkdir $tablespace2 $!"; -# Scape tablespace locations on Windows. +# escape tablespace locations on Windows. +my $tablespace2_orig = $tablespace2; $tablespace1 = $windows_os ? ($tablespace1 =~ s/\\/\\\\/gr) : $tablespace1; $tablespace2 = $windows_os ? ($tablespace2 =~ s/\\/\\\\/gr) : $tablespace2; @@ -62,9 +63,6 @@ "$tempdir/restore_roles", ], like => qr/ - ^\s*\QCREATE ROLE dumpall;\E\s*\n - \s*\QALTER ROLE dumpall WITH SUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS PASSWORD 'SCRAM-SHA-256\E - [^']+';\s*\n \s*\QCREATE ROLE dumpall2;\E \s*\QALTER ROLE dumpall2 WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN REPLICATION NOBYPASSRLS CONNECTION LIMIT 10;\E /xm @@ -89,8 +87,7 @@ # Match "E" as optional since it is added on LOCATION when running on # Windows. like => qr/^ - \n\QCREATE TABLESPACE tbl1 OWNER tap LOCATION \E(?:E)?\Q'$tablespace1';\E - \n\QCREATE TABLESPACE tbl2 OWNER tap LOCATION \E(?:E)?\Q'$tablespace2';\E + \n\QCREATE TABLESPACE tbl2 OWNER tap LOCATION \E(?:E)?\Q'$tablespace2_orig';\E \n\QALTER TABLESPACE tbl2 SET (seq_page_cost=1.0);\E /xm, }, @@ -138,8 +135,6 @@ "$tempdir/restore_grants", ], like => qr/^ - \n\QGRANT super TO grant7 WITH INHERIT TRUE GRANTED BY\E - (.*\n)* \n\QGRANT ALL ON SCHEMA private TO grant5;\E (.*\n)* \n\QGRANT ALL ON FUNCTION public.fn() TO grant8;\E @@ -294,17 +289,35 @@ '--format' => 'directory', '--globals-only', '--file' => "$tempdir/dump_globals_only", - ], - restore_cmd => [ - 'pg_restore', '-C', '--globals-only', - '--format' => 'directory', - '--file' => "$tempdir/dump_globals_only.sql", - "$tempdir/dump_globals_only", - ], - like => qr/ + ], + restore_cmd => [ + 'pg_restore', '-C', '--globals-only', + '--format' => 'directory', + '--file' => "$tempdir/dump_globals_only.sql", + "$tempdir/dump_globals_only", + ], + like => qr/ ^\s*\QCREATE ROLE dumpall;\E\s*\n /xm - }, ); + }, + + restore_no_globals => { + setup_sql => "CREATE TABLE no_globals_test(a int, b text); + INSERT INTO no_globals_test VALUES (1, 'hello'), (2, 'world');", + dump_cmd => [ + 'pg_dumpall', + '--format' => 'directory', + '--file' => "$tempdir/restore_no_globals", + ], + restore_cmd => [ + 'pg_restore', '-C', '--no-globals', + '--format' => 'directory', + '--file' => "$tempdir/restore_no_globals.sql", + "$tempdir/restore_no_globals", + ], + like => qr/^\n\QCOPY public.no_globals_test (a, b) FROM stdin;\E/xm, + unlike => qr/^\QCREATE ROLE dumpall;\E/xm, + },); # First execute the setup_sql foreach my $run (sort keys %pgdumpall_runs) @@ -339,7 +352,8 @@ # pg_restore --file output file. my $output_file = slurp_file("$tempdir/${run}.sql"); - if (!($pgdumpall_runs{$run}->{like}) && !($pgdumpall_runs{$run}->{unlike})) + if ( !($pgdumpall_runs{$run}->{like}) + && !($pgdumpall_runs{$run}->{unlike})) { die "missing \"like\" or \"unlike\" in test \"$run\""; } @@ -356,35 +370,291 @@ $pgdumpall_runs{$run}->{unlike}, "should not dump $run"); } + + $target_node->stop; + $target_node->clean_node; } # Some negative test case with dump of pg_dumpall and restore using pg_restore -# test case 1: when -C is not used in pg_restore with dump of pg_dumpall +# report an error when -C is not used in pg_restore with dump of pg_dumpall $node->command_fails_like( - [ 'pg_restore', - "$tempdir/format_custom", - '--format' => 'custom', - '--file' => "$tempdir/error_test.sql", ], - qr/\Qpg_restore: error: -C\/--create option should be specified when restoring an archive created by pg_dumpall\E/, - 'When -C is not used in pg_restore with dump of pg_dumpall'); - -# test case 2: When --list option is used with dump of pg_dumpall + [ + 'pg_restore', + "$tempdir/format_custom", + '--format' => 'custom', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option -C\/--create must be specified when restoring an archive created by pg_dumpall\E/, + 'When -C is not used in pg_restore with dump of pg_dumpall'); + +# report an error when \l/--list option is used with dump of pg_dumpall $node->command_fails_like( - [ 'pg_restore', + [ + 'pg_restore', "$tempdir/format_custom", '-C', - '--format' => 'custom', '--list', - '--file' => "$tempdir/error_test.sql", ], + '--format' => 'custom', + '--list', + '--file' => "$tempdir/error_test.sql", + ], qr/\Qpg_restore: error: option -l\/--list cannot be used when restoring an archive created by pg_dumpall\E/, 'When --list is used in pg_restore with dump of pg_dumpall'); -# test case 3: When non-exist database is given with -d option +# report an error when -L/--use-list option is used with dump of pg_dumpall +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--use-list' => 'use', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option -L\/--use-list cannot be used when restoring an archive created by pg_dumpall\E/, + 'When -L/--use-list is used in pg_restore with dump of pg_dumpall'); + +# report an error when --strict-names option is used with dump of pg_dumpall +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--strict-names', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option --strict-names cannot be used when restoring an archive created by pg_dumpall\E/, + 'When --strict-names is used in pg_restore with dump of pg_dumpall'); + +# report an error when --clean and -g/--globals-only are used in pg_restore with dump of pg_dumpall $node->command_fails_like( - [ 'pg_restore', + [ + 'pg_restore', "$tempdir/format_custom", '-C', '--format' => 'custom', - '-d' => 'dbpq', ], - qr/\Qpg_restore: error: could not connect to database "dbpq"\E/, - 'When non-existent database is given with -d option in pg_restore with dump of pg_dumpall'); + '--clean', + '--globals-only', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options --clean and -g\/--globals-only cannot be used together when restoring an archive created by pg_dumpall\E/, + 'When --clean and -g/--globals-only are used in pg_restore with dump of pg_dumpall' +); + +# report an error when non-exist database is given with -d option +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '-d' => 'dbpq', + ], + qr/\QFATAL: database "dbpq" does not exist\E/, + 'When non-existent database is given with -d option in pg_restore with dump of pg_dumpall' +); + +# report an error when --no-schema is used with dump of pg_dumpall +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--no-schema', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option --no-schema cannot be used when restoring an archive created by pg_dumpall\E/, + 'When --no-schema is used in pg_restore with dump of pg_dumpall'); + +# report an error when --data-only is used with dump of pg_dumpall +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--data-only', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option -a\/--data-only cannot be used when restoring an archive created by pg_dumpall\E/, + 'When --data-only is used in pg_restore with dump of pg_dumpall'); + +# report an error when --statistics-only is used with dump of pg_dumpall +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--statistics-only', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option --statistics-only cannot be used when restoring an archive created by pg_dumpall\E/, + 'When --statistics-only is used in pg_restore with dump of pg_dumpall'); + +# report an error when --section excludes pre-data with dump of pg_dumpall +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--section' => 'post-data', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: option --section cannot exclude --pre-data when restoring a pg_dumpall archive\E/, + 'When --section=post-data is used in pg_restore with dump of pg_dumpall'); + +# report an error when --globals-only and --data-only are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--data-only', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options -a\/--data-only and -g\/--globals-only cannot be used together\E/, + 'When --globals-only and --data-only are used together'); + +# report an error when --globals-only and --schema-only are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--schema-only', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options -g\/--globals-only and -s\/--schema-only cannot be used together\E/, + 'When --globals-only and --schema-only are used together'); + +# report an error when --globals-only and --statistics-only are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--statistics-only', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options -g\/--globals-only and --statistics-only cannot be used together\E/, + 'When --globals-only and --statistics-only are used together'); + +# report an error when --globals-only and --statistics are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--statistics', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options --statistics and -g\/--globals-only cannot be used together\E/, + 'When --globals-only and --statistics are used together'); + +# report an error when --globals-only and --exit-on-error are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--exit-on-error', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options --exit-on-error and -g\/--globals-only cannot be used together\E/, + 'When --globals-only and --exit-on-error are used together'); + +# report an error when --globals-only and --single-transaction are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--single-transaction', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options -g\/--globals-only and -1\/--single-transaction cannot be used together\E/, + 'When --globals-only and --single-transaction are used together'); + +# report an error when --globals-only and --transaction-size are used together +$node->command_fails_like( + [ + 'pg_restore', + "$tempdir/format_custom", '-C', + '--format' => 'custom', + '--globals-only', + '--transaction-size' => '100', + '--file' => "$tempdir/error_test.sql", + ], + qr/\Qpg_restore: error: options -g\/--globals-only and --transaction-size cannot be used together\E/, + 'When --globals-only and --transaction-size are used together'); + +# verify map.dat preamble exists +my $map_dat_content = slurp_file("$tempdir/format_directory/map.dat"); +like( + $map_dat_content, + qr/^# map\.dat\n.*# This file maps oids to database names/ms, + 'map.dat contains expected preamble'); + +# verify commenting out a line in map.dat skips that database +$node->safe_psql( + $run_db, 'CREATE DATABASE comment_test_db; +\c comment_test_db +CREATE TABLE comment_test_table (id int);'); + +$node->command_ok( + [ + 'pg_dumpall', + '--format' => 'directory', + '--file' => "$tempdir/comment_test", + ], + 'pg_dumpall for comment test'); + +# Modify map.dat to comment out the comment_test_db entry +my $map_content = slurp_file("$tempdir/comment_test/map.dat"); +$map_content =~ s/^(\d+ comment_test_db)$/# $1/m; +open(my $fh, '>', "$tempdir/comment_test/map.dat") + or die "Cannot open map.dat: $!"; +print $fh $map_content; +close($fh); + +# Create a target node and restore - commented db should be skipped +my $target_comment = PostgreSQL::Test::Cluster->new("target_comment"); +$target_comment->init; +$target_comment->start; + +$node->command_ok( + [ + 'pg_restore', '-C', + '--format' => 'directory', + '--file' => "$tempdir/comment_test_restore.sql", + '--host', $target_comment->host, + '--port', $target_comment->port, + "$tempdir/comment_test", + ], + 'pg_restore with commented out database in map.dat'); + +my $restore_output = slurp_file("$tempdir/comment_test_restore.sql"); +unlike( + $restore_output, + qr/CREATE DATABASE comment_test_db/, + 'commented out database in map.dat is not restored'); + +# Test that --clean implies --if-exists for pg_dumpall archives +$node->command_ok( + [ + 'pg_restore', '-C', + '--format' => 'custom', + '--clean', + '--file' => "$tempdir/clean_test.sql", + "$tempdir/format_custom", + ], + 'pg_restore with --clean on pg_dumpall archive'); + +my $clean_output = slurp_file("$tempdir/clean_test.sql"); +like( + $clean_output, + qr/DROP ROLE IF EXISTS/, + '--clean implies --if-exists: DROP ROLE IF EXISTS in output'); $node->stop('fast'); diff --git a/src/bin/pg_dump/t/010_dump_connstr.pl b/src/bin/pg_dump/t/010_dump_connstr.pl index bde6096c60d62..bf2c3b6d00bd4 100644 --- a/src/bin/pg_dump/t/010_dump_connstr.pl +++ b/src/bin/pg_dump/t/010_dump_connstr.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -153,20 +153,6 @@ ], 'pg_dumpall --dbname accepts connection string'); -$node->run_log( - [ 'createdb', '--username' => $src_bootstrap_super, "foo\n\rbar" ]); - -# not sufficient to use --roles-only here -$node->command_fails( - [ - 'pg_dumpall', '--no-sync', - '--username' => $src_bootstrap_super, - '--file' => $discard, - ], - 'pg_dumpall with \n\r in database name'); -$node->run_log( - [ 'dropdb', '--username' => $src_bootstrap_super, "foo\n\rbar" ]); - # make a table, so the parallel worker has something to dump $node->safe_psql( diff --git a/src/bin/pg_resetwal/Makefile b/src/bin/pg_resetwal/Makefile index 82bea06dee5a3..7113acbef2fb6 100644 --- a/src/bin/pg_resetwal/Makefile +++ b/src/bin/pg_resetwal/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_resetwal # -# Copyright (c) 1998-2025, PostgreSQL Global Development Group +# Copyright (c) 1998-2026, PostgreSQL Global Development Group # # src/bin/pg_resetwal/Makefile # diff --git a/src/bin/pg_resetwal/meson.build b/src/bin/pg_resetwal/meson.build index 290832b22996f..c2607767b511f 100644 --- a/src/bin/pg_resetwal/meson.build +++ b/src/bin/pg_resetwal/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_resetwal_sources = files( 'pg_resetwal.c', diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c index e876f35f38ed4..f8d25afed9de4 100644 --- a/src/bin/pg_resetwal/pg_resetwal.c +++ b/src/bin/pg_resetwal/pg_resetwal.c @@ -19,7 +19,7 @@ * step 2 ... * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_resetwal/pg_resetwal.c @@ -55,6 +55,7 @@ #include "common/restricted_token.h" #include "common/string.h" #include "fe_utils/option_utils.h" +#include "fe_utils/version.h" #include "getopt_long.h" #include "pg_getopt.h" #include "storage/large_object.h" @@ -63,19 +64,43 @@ static ControlFileData ControlFile; /* pg_control values */ static XLogSegNo newXlogSegNo; /* new XLOG segment # */ static bool guessed = false; /* T if we had to guess at any values */ static const char *progname; -static uint32 set_xid_epoch = (uint32) -1; -static TransactionId set_oldest_xid = 0; -static TransactionId set_xid = 0; -static TransactionId set_oldest_commit_ts_xid = 0; -static TransactionId set_newest_commit_ts_xid = 0; -static Oid set_oid = 0; -static MultiXactId set_mxid = 0; -static MultiXactOffset set_mxoff = (MultiXactOffset) -1; + +/* + * New values given on the command-line + */ +static bool next_xid_epoch_given = false; +static uint32 next_xid_epoch_val; + +static bool oldest_xid_given = false; +static TransactionId oldest_xid_val; + +static bool next_xid_given = false; +static TransactionId next_xid_val; + +static bool commit_ts_xids_given = false; +static TransactionId oldest_commit_ts_xid_val; +static TransactionId newest_commit_ts_xid_val; + +static bool next_oid_given = false; +static Oid next_oid_val; + +static bool mxids_given = false; +static MultiXactId next_mxid_val; +static MultiXactId oldest_mxid_val = 0; + +static bool next_mxoff_given = false; +static MultiXactOffset next_mxoff_val; + +static bool wal_segsize_given = false; +static int wal_segsize_val; + +static bool char_signedness_given = false; +static bool char_signedness_val; + + static TimeLineID minXlogTli = 0; static XLogSegNo minXlogSegNo = 0; static int WalSegSz; -static int set_wal_segsize; -static int set_char_signedness = -1; static void CheckDataVersion(void); static bool read_controlfile(void); @@ -89,6 +114,8 @@ static void KillExistingArchiveStatus(void); static void KillExistingWALSummaries(void); static void WriteEmptyXLOG(void); static void usage(void); +static uint32 strtouint32_strict(const char *restrict s, char **restrict endptr, int base); +static uint64 strtouint64_strict(const char *restrict s, char **restrict endptr, int base); int @@ -114,7 +141,6 @@ main(int argc, char *argv[]) int c; bool force = false; bool noupdate = false; - MultiXactId set_oldestmxid = 0; char *endptr; char *endptr2; char *DataDir = NULL; @@ -158,55 +184,56 @@ main(int argc, char *argv[]) case 'e': errno = 0; - set_xid_epoch = strtoul(optarg, &endptr, 0); + next_xid_epoch_val = strtouint32_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != '\0' || errno != 0) { /*------ - translator: the second %s is a command line argument (-e, etc) */ + translator: %s is a command line argument (-e, etc) */ pg_log_error("invalid argument for option %s", "-e"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (set_xid_epoch == -1) - pg_fatal("transaction ID epoch (-e) must not be -1"); + next_xid_epoch_given = true; break; case 'u': errno = 0; - set_oldest_xid = strtoul(optarg, &endptr, 0); + oldest_xid_val = strtouint32_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != '\0' || errno != 0) { pg_log_error("invalid argument for option %s", "-u"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (!TransactionIdIsNormal(set_oldest_xid)) + if (!TransactionIdIsNormal(oldest_xid_val)) pg_fatal("oldest transaction ID (-u) must be greater than or equal to %u", FirstNormalTransactionId); + oldest_xid_given = true; break; case 'x': errno = 0; - set_xid = strtoul(optarg, &endptr, 0); + next_xid_val = strtouint32_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != '\0' || errno != 0) { pg_log_error("invalid argument for option %s", "-x"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (!TransactionIdIsNormal(set_xid)) + if (!TransactionIdIsNormal(next_xid_val)) pg_fatal("transaction ID (-x) must be greater than or equal to %u", FirstNormalTransactionId); + next_xid_given = true; break; case 'c': errno = 0; - set_oldest_commit_ts_xid = strtoul(optarg, &endptr, 0); + oldest_commit_ts_xid_val = strtouint32_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != ',' || errno != 0) { pg_log_error("invalid argument for option %s", "-c"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - set_newest_commit_ts_xid = strtoul(endptr + 1, &endptr2, 0); + newest_commit_ts_xid_val = strtoul(endptr + 1, &endptr2, 0); if (endptr2 == endptr + 1 || *endptr2 != '\0' || errno != 0) { pg_log_error("invalid argument for option %s", "-c"); @@ -214,31 +241,33 @@ main(int argc, char *argv[]) exit(1); } - if (set_oldest_commit_ts_xid < FirstNormalTransactionId && - set_oldest_commit_ts_xid != InvalidTransactionId) + if (oldest_commit_ts_xid_val < FirstNormalTransactionId && + oldest_commit_ts_xid_val != InvalidTransactionId) pg_fatal("transaction ID (-c) must be either %u or greater than or equal to %u", InvalidTransactionId, FirstNormalTransactionId); - if (set_newest_commit_ts_xid < FirstNormalTransactionId && - set_newest_commit_ts_xid != InvalidTransactionId) + if (newest_commit_ts_xid_val < FirstNormalTransactionId && + newest_commit_ts_xid_val != InvalidTransactionId) pg_fatal("transaction ID (-c) must be either %u or greater than or equal to %u", InvalidTransactionId, FirstNormalTransactionId); + commit_ts_xids_given = true; break; case 'o': errno = 0; - set_oid = strtoul(optarg, &endptr, 0); + next_oid_val = strtouint32_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != '\0' || errno != 0) { pg_log_error("invalid argument for option %s", "-o"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (set_oid == 0) + if (next_oid_val == 0) pg_fatal("OID (-o) must not be 0"); + next_oid_given = true; break; case 'm': errno = 0; - set_mxid = strtoul(optarg, &endptr, 0); + next_mxid_val = strtouint32_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != ',' || errno != 0) { pg_log_error("invalid argument for option %s", "-m"); @@ -246,39 +275,39 @@ main(int argc, char *argv[]) exit(1); } - set_oldestmxid = strtoul(endptr + 1, &endptr2, 0); + oldest_mxid_val = strtouint32_strict(endptr + 1, &endptr2, 0); if (endptr2 == endptr + 1 || *endptr2 != '\0' || errno != 0) { pg_log_error("invalid argument for option %s", "-m"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (set_mxid == 0) - pg_fatal("multitransaction ID (-m) must not be 0"); /* * XXX It'd be nice to have more sanity checks here, e.g. so * that oldest is not wrapped around w.r.t. nextMulti. */ - if (set_oldestmxid == 0) + if (next_mxid_val == 0) + pg_fatal("next multitransaction ID (-m) must not be 0"); + if (oldest_mxid_val == 0) pg_fatal("oldest multitransaction ID (-m) must not be 0"); + mxids_given = true; break; case 'O': errno = 0; - set_mxoff = strtoul(optarg, &endptr, 0); + next_mxoff_val = strtouint64_strict(optarg, &endptr, 0); if (endptr == optarg || *endptr != '\0' || errno != 0) { pg_log_error("invalid argument for option %s", "-O"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } - if (set_mxoff == -1) - pg_fatal("multitransaction offset (-O) must not be -1"); + next_mxoff_given = true; break; case 'l': - if (strspn(optarg, "01234567890ABCDEFabcdef") != XLOG_FNAME_LEN) + if (strspn(optarg, "0123456789ABCDEFabcdef") != XLOG_FNAME_LEN) { pg_log_error("invalid argument for option %s", "-l"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -298,9 +327,10 @@ main(int argc, char *argv[]) if (!option_parse_int(optarg, "--wal-segsize", 1, 1024, &wal_segsize_mb)) exit(1); - set_wal_segsize = wal_segsize_mb * 1024 * 1024; - if (!IsValidWalSegSize(set_wal_segsize)) + wal_segsize_val = wal_segsize_mb * 1024 * 1024; + if (!IsValidWalSegSize(wal_segsize_val)) pg_fatal("argument of %s must be a power of two between 1 and 1024", "--wal-segsize"); + wal_segsize_given = true; break; } @@ -309,15 +339,16 @@ main(int argc, char *argv[]) errno = 0; if (pg_strcasecmp(optarg, "signed") == 0) - set_char_signedness = 1; + char_signedness_val = true; else if (pg_strcasecmp(optarg, "unsigned") == 0) - set_char_signedness = 0; + char_signedness_val = false; else { pg_log_error("invalid argument for option %s", "--char-signedness"); pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit(1); } + char_signedness_given = true; break; } @@ -405,8 +436,8 @@ main(int argc, char *argv[]) /* * If no new WAL segment size was specified, use the control file value. */ - if (set_wal_segsize != 0) - WalSegSz = set_wal_segsize; + if (wal_segsize_given) + WalSegSz = wal_segsize_val; else WalSegSz = ControlFile.xlog_seg_size; @@ -429,42 +460,43 @@ main(int argc, char *argv[]) * Adjust fields if required by switches. (Do this now so that printout, * if any, includes these values.) */ - if (set_xid_epoch != -1) + if (next_xid_epoch_given) ControlFile.checkPointCopy.nextXid = - FullTransactionIdFromEpochAndXid(set_xid_epoch, + FullTransactionIdFromEpochAndXid(next_xid_epoch_val, XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid)); - if (set_oldest_xid != 0) + if (oldest_xid_given) { - ControlFile.checkPointCopy.oldestXid = set_oldest_xid; + ControlFile.checkPointCopy.oldestXid = oldest_xid_val; ControlFile.checkPointCopy.oldestXidDB = InvalidOid; } - if (set_xid != 0) + if (next_xid_given) ControlFile.checkPointCopy.nextXid = FullTransactionIdFromEpochAndXid(EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid), - set_xid); + next_xid_val); - if (set_oldest_commit_ts_xid != 0) - ControlFile.checkPointCopy.oldestCommitTsXid = set_oldest_commit_ts_xid; - if (set_newest_commit_ts_xid != 0) - ControlFile.checkPointCopy.newestCommitTsXid = set_newest_commit_ts_xid; + if (commit_ts_xids_given) + { + ControlFile.checkPointCopy.oldestCommitTsXid = oldest_commit_ts_xid_val; + ControlFile.checkPointCopy.newestCommitTsXid = newest_commit_ts_xid_val; + } - if (set_oid != 0) - ControlFile.checkPointCopy.nextOid = set_oid; + if (next_oid_given) + ControlFile.checkPointCopy.nextOid = next_oid_val; - if (set_mxid != 0) + if (mxids_given) { - ControlFile.checkPointCopy.nextMulti = set_mxid; + ControlFile.checkPointCopy.nextMulti = next_mxid_val; - ControlFile.checkPointCopy.oldestMulti = set_oldestmxid; + ControlFile.checkPointCopy.oldestMulti = oldest_mxid_val; if (ControlFile.checkPointCopy.oldestMulti < FirstMultiXactId) ControlFile.checkPointCopy.oldestMulti += FirstMultiXactId; ControlFile.checkPointCopy.oldestMultiDB = InvalidOid; } - if (set_mxoff != -1) - ControlFile.checkPointCopy.nextMultiOffset = set_mxoff; + if (next_mxoff_given) + ControlFile.checkPointCopy.nextMultiOffset = next_mxoff_val; if (minXlogTli > ControlFile.checkPointCopy.ThisTimeLineID) { @@ -472,11 +504,11 @@ main(int argc, char *argv[]) ControlFile.checkPointCopy.PrevTimeLineID = minXlogTli; } - if (set_wal_segsize != 0) + if (wal_segsize_given) ControlFile.xlog_seg_size = WalSegSz; - if (set_char_signedness != -1) - ControlFile.default_char_signedness = (set_char_signedness == 1); + if (char_signedness_given) + ControlFile.default_char_signedness = char_signedness_val; if (minXlogSegNo > newXlogSegNo) newXlogSegNo = minXlogSegNo; @@ -539,35 +571,18 @@ main(int argc, char *argv[]) static void CheckDataVersion(void) { - const char *ver_file = "PG_VERSION"; - FILE *ver_fd; - char rawline[64]; - - if ((ver_fd = fopen(ver_file, "r")) == NULL) - pg_fatal("could not open file \"%s\" for reading: %m", - ver_file); + char *version_str; + uint32 version = get_pg_version(".", &version_str); - /* version number has to be the first line read */ - if (!fgets(rawline, sizeof(rawline), ver_fd)) - { - if (!ferror(ver_fd)) - pg_fatal("unexpected empty file \"%s\"", ver_file); - else - pg_fatal("could not read file \"%s\": %m", ver_file); - } - - /* strip trailing newline and carriage return */ - (void) pg_strip_crlf(rawline); - - if (strcmp(rawline, PG_MAJORVERSION) != 0) + if (GET_PG_MAJORVERSION_NUM(version) != PG_MAJORVERSION_NUM) { pg_log_error("data directory is of wrong version"); pg_log_error_detail("File \"%s\" contains \"%s\", which is not compatible with this program's version \"%s\".", - ver_file, rawline, PG_MAJORVERSION); + "PG_VERSION", + version_str, + PG_MAJORVERSION); exit(1); } - - fclose(ver_fd); } @@ -707,19 +722,20 @@ GuessControlValues(void) ControlFile.max_wal_senders = 10; ControlFile.max_worker_processes = 8; ControlFile.max_prepared_xacts = 0; - ControlFile.max_locks_per_xact = 64; + ControlFile.max_locks_per_xact = 128; ControlFile.maxAlign = MAXIMUM_ALIGNOF; ControlFile.floatFormat = FLOATFORMAT_VALUE; ControlFile.blcksz = BLCKSZ; ControlFile.relseg_size = RELSEG_SIZE; + ControlFile.slru_pages_per_segment = SLRU_PAGES_PER_SEGMENT; ControlFile.xlog_blcksz = XLOG_BLCKSZ; ControlFile.xlog_seg_size = DEFAULT_XLOG_SEG_SIZE; ControlFile.nameDataLen = NAMEDATALEN; ControlFile.indexMaxKeys = INDEX_MAX_KEYS; ControlFile.toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE; ControlFile.loblksize = LOBLKSIZE; - ControlFile.float8ByVal = FLOAT8PASSBYVAL; + ControlFile.float8ByVal = true; /* vestigial */ /* * XXX eventually, should try to grovel through old XLOG to develop more @@ -759,7 +775,7 @@ PrintControlValues(bool guessed) ControlFile.checkPointCopy.nextOid); printf(_("Latest checkpoint's NextMultiXactId: %u\n"), ControlFile.checkPointCopy.nextMulti); - printf(_("Latest checkpoint's NextMultiOffset: %u\n"), + printf(_("Latest checkpoint's NextMultiOffset: %" PRIu64 "\n"), ControlFile.checkPointCopy.nextMultiOffset); printf(_("Latest checkpoint's oldestXID: %u\n"), ControlFile.checkPointCopy.oldestXid); @@ -782,6 +798,8 @@ PrintControlValues(bool guessed) ControlFile.blcksz); printf(_("Blocks per segment of large relation: %u\n"), ControlFile.relseg_size); + printf(_("Pages per SLRU segment: %u\n"), + ControlFile.slru_pages_per_segment); printf(_("WAL block size: %u\n"), ControlFile.xlog_blcksz); printf(_("Bytes per WAL segment: %u\n"), @@ -821,7 +839,7 @@ PrintNewControlValues(void) newXlogSegNo, WalSegSz); printf(_("First log segment after reset: %s\n"), fname); - if (set_mxid != 0) + if (mxids_given) { printf(_("NextMultiXactId: %u\n"), ControlFile.checkPointCopy.nextMulti); @@ -831,46 +849,47 @@ PrintNewControlValues(void) ControlFile.checkPointCopy.oldestMultiDB); } - if (set_mxoff != -1) + if (next_mxoff_given) { - printf(_("NextMultiOffset: %u\n"), + printf(_("NextMultiOffset: %" PRIu64 "\n"), ControlFile.checkPointCopy.nextMultiOffset); } - if (set_oid != 0) + if (next_oid_given) { printf(_("NextOID: %u\n"), ControlFile.checkPointCopy.nextOid); } - if (set_xid != 0) + if (next_xid_given) { printf(_("NextXID: %u\n"), XidFromFullTransactionId(ControlFile.checkPointCopy.nextXid)); + } + + if (oldest_xid_given) + { printf(_("OldestXID: %u\n"), ControlFile.checkPointCopy.oldestXid); printf(_("OldestXID's DB: %u\n"), ControlFile.checkPointCopy.oldestXidDB); } - if (set_xid_epoch != -1) + if (next_xid_epoch_given) { printf(_("NextXID epoch: %u\n"), EpochFromFullTransactionId(ControlFile.checkPointCopy.nextXid)); } - if (set_oldest_commit_ts_xid != 0) + if (commit_ts_xids_given) { printf(_("oldestCommitTsXid: %u\n"), ControlFile.checkPointCopy.oldestCommitTsXid); - } - if (set_newest_commit_ts_xid != 0) - { printf(_("newestCommitTsXid: %u\n"), ControlFile.checkPointCopy.newestCommitTsXid); } - if (set_wal_segsize != 0) + if (wal_segsize_given) { printf(_("Bytes per WAL segment: %u\n"), ControlFile.xlog_seg_size); @@ -894,10 +913,10 @@ RewriteControlFile(void) ControlFile.state = DB_SHUTDOWNED; ControlFile.checkPoint = ControlFile.checkPointCopy.redo; - ControlFile.minRecoveryPoint = 0; + ControlFile.minRecoveryPoint = InvalidXLogRecPtr; ControlFile.minRecoveryPointTLI = 0; - ControlFile.backupStartPoint = 0; - ControlFile.backupEndPoint = 0; + ControlFile.backupStartPoint = InvalidXLogRecPtr; + ControlFile.backupEndPoint = InvalidXLogRecPtr; ControlFile.backupEndRequired = false; /* @@ -912,7 +931,7 @@ RewriteControlFile(void) ControlFile.max_wal_senders = 10; ControlFile.max_worker_processes = 8; ControlFile.max_prepared_xacts = 0; - ControlFile.max_locks_per_xact = 64; + ControlFile.max_locks_per_xact = 128; /* The control file gets flushed here. */ update_controlfile(".", &ControlFile, true); @@ -1058,6 +1077,8 @@ KillExistingArchiveStatus(void) if (closedir(xldir)) pg_fatal("could not close directory \"%s\": %m", ARCHSTATDIR); + +#undef ARCHSTATDIR } /* @@ -1092,7 +1113,10 @@ KillExistingWALSummaries(void) pg_fatal("could not read directory \"%s\": %m", WALSUMMARYDIR); if (closedir(xldir)) - pg_fatal("could not close directory \"%s\": %m", ARCHSTATDIR); + pg_fatal("could not close directory \"%s\": %m", WALSUMMARYDIR); + +#undef WALSUMMARY_NHEXCHARS +#undef WALSUMMARYDIR } /* @@ -1128,7 +1152,7 @@ WriteEmptyXLOG(void) /* Insert the initial checkpoint record */ recptr = (char *) page + SizeOfXLogLongPHD; record = (XLogRecord *) recptr; - record->xl_prev = 0; + record->xl_prev = InvalidXLogRecPtr; record->xl_xid = InvalidTransactionId; record->xl_tot_len = SizeOfXLogRecord + SizeOfXLogRecordDataHeaderShort + sizeof(CheckPoint); record->xl_info = XLOG_CHECKPOINT_SHUTDOWN; @@ -1218,3 +1242,76 @@ usage(void) printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); } + +/* + * strtouint32_strict -- like strtoul(), but returns uint32 and doesn't accept + * negative values + */ +static uint32 +strtouint32_strict(const char *restrict s, char **restrict endptr, int base) +{ + unsigned long val; + bool is_neg; + + /* skip leading whitespace */ + while (isspace((unsigned char) *s)) + s++; + + /* + * Is it negative? We still call strtoul() if it was, to set 'endptr'. + * (The current callers don't care though.) + */ + is_neg = (*s == '-'); + + val = strtoul(s, endptr, base); + + /* reject if it was negative */ + if (errno == 0 && is_neg) + { + errno = ERANGE; + val = 0; + } + + /* + * reject values larger than UINT32_MAX on platforms where long is 64 bits + * wide. + */ + if (errno == 0 && val != (uint32) val) + { + errno = ERANGE; + val = UINT32_MAX; + } + + return (uint32) val; +} + +/* + * strtouint64_strict -- like strtou64(), but doesn't accept negative values + */ +static uint64 +strtouint64_strict(const char *restrict s, char **restrict endptr, int base) +{ + uint64 val; + bool is_neg; + + /* skip leading whitespace */ + while (isspace((unsigned char) *s)) + s++; + + /* + * Is it negative? We still call strtou64() if it was, to set 'endptr'. + * (The current callers don't care though.) + */ + is_neg = (*s == '-'); + + val = strtou64(s, endptr, base); + + /* reject if it was negative */ + if (errno == 0 && is_neg) + { + errno = ERANGE; + val = 0; + } + + return val; +} diff --git a/src/bin/pg_resetwal/po/meson.build b/src/bin/pg_resetwal/po/meson.build index 017c2c60004ec..89cd05e05fa78 100644 --- a/src/bin/pg_resetwal/po/meson.build +++ b/src/bin/pg_resetwal/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_resetwal-' + pg_version_major.to_string())] diff --git a/src/bin/pg_resetwal/t/001_basic.pl b/src/bin/pg_resetwal/t/001_basic.pl index d6bbbd0ceda38..d686584eb9674 100644 --- a/src/bin/pg_resetwal/t/001_basic.pl +++ b/src/bin/pg_resetwal/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -103,7 +103,7 @@ 'fails with incorrect -e option'); command_fails_like( [ 'pg_resetwal', '-e' => '-1', $node->data_dir ], - qr/must not be -1/, + qr/error: invalid argument for option -e/, 'fails with -e value -1'); # -l command_fails_like( @@ -122,11 +122,11 @@ command_fails_like( [ 'pg_resetwal', '-m' => '0,10', $node->data_dir ], qr/must not be 0/, - 'fails with -m value 0 part 1'); + 'fails with -m value 0 in the first part'); command_fails_like( [ 'pg_resetwal', '-m' => '10,0', $node->data_dir ], qr/must not be 0/, - 'fails with -m value 0 part 2'); + 'fails with -m value 0 in the second part'); # -o command_fails_like( [ 'pg_resetwal', '-o' => 'foo', $node->data_dir ], @@ -143,7 +143,7 @@ 'fails with incorrect -O option'); command_fails_like( [ 'pg_resetwal', '-O' => '-1', $node->data_dir ], - qr/must not be -1/, + qr/error: invalid argument for option -O/, 'fails with -O value -1'); # --wal-segsize command_fails_like( @@ -173,6 +173,21 @@ qr/must be greater than/, 'fails with -x value too small'); +# Check out of range values with -x. These are forbidden for all other +# 32-bit values too, but we use just -x to exercise the parsing. +command_fails_like( + [ 'pg_resetwal', '-x' => '-1', $node->data_dir ], + qr/error: invalid argument for option -x/, + 'fails with -x value -1'); +command_fails_like( + [ 'pg_resetwal', '-x' => '-100', $node->data_dir ], + qr/error: invalid argument for option -x/, + 'fails with negative -x value'); +command_fails_like( + [ 'pg_resetwal', '-x' => '10000000000', $node->data_dir ], + qr/error: invalid argument for option -x/, + 'fails with -x value too large'); + # --char-signedness command_fails_like( [ 'pg_resetwal', '--char-signedness', 'foo', $node->data_dir ], @@ -213,7 +228,7 @@ sub get_slru_files sprintf("%d,%d", hex($files[0]) == 0 ? 3 : hex($files[0]), hex($files[-1])); @files = get_slru_files('pg_multixact/offsets'); -$mult = 32 * $blcksz / 4; +$mult = 32 * $blcksz / 8; # --multixact-ids argument is "new,old" push @cmd, '--multixact-ids' => sprintf("%d,%d", diff --git a/src/bin/pg_resetwal/t/002_corrupted.pl b/src/bin/pg_resetwal/t/002_corrupted.pl index 3c80f6309c9b6..dcd06913778ee 100644 --- a/src/bin/pg_resetwal/t/002_corrupted.pl +++ b/src/bin/pg_resetwal/t/002_corrupted.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for handling a corrupted pg_control diff --git a/src/bin/pg_rewind/Makefile b/src/bin/pg_rewind/Makefile index cbd000b686dba..32a35c5761215 100644 --- a/src/bin/pg_rewind/Makefile +++ b/src/bin/pg_rewind/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_rewind # -# Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group # # src/bin/pg_rewind/Makefile # diff --git a/src/bin/pg_rewind/datapagemap.c b/src/bin/pg_rewind/datapagemap.c index a0645afc83b1c..8e8cdda50050f 100644 --- a/src/bin/pg_rewind/datapagemap.c +++ b/src/bin/pg_rewind/datapagemap.c @@ -5,7 +5,7 @@ * * This is a fairly simple bitmap. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -76,7 +76,7 @@ datapagemap_iterate(datapagemap_t *map) { datapagemap_iterator_t *iter; - iter = pg_malloc(sizeof(datapagemap_iterator_t)); + iter = pg_malloc0_object(datapagemap_iterator_t); iter->map = map; iter->nextblkno = 0; diff --git a/src/bin/pg_rewind/datapagemap.h b/src/bin/pg_rewind/datapagemap.h index a23872231e822..ea85275307ac9 100644 --- a/src/bin/pg_rewind/datapagemap.h +++ b/src/bin/pg_rewind/datapagemap.h @@ -2,7 +2,7 @@ * * datapagemap.h * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ diff --git a/src/bin/pg_rewind/file_ops.c b/src/bin/pg_rewind/file_ops.c index 55659ce201f4c..5cfb676f41f87 100644 --- a/src/bin/pg_rewind/file_ops.c +++ b/src/bin/pg_rewind/file_ops.c @@ -8,7 +8,7 @@ * do nothing if it's enabled. You should avoid accessing the target files * directly but if you do, make sure you honor the --dry-run mode! * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -327,7 +327,7 @@ slurpFile(const char *datadir, const char *path, size_t *filesize) fullpath); if (fstat(fd, &statbuf) < 0) - pg_fatal("could not open file \"%s\" for reading: %m", + pg_fatal("could not stat file \"%s\": %m", fullpath); len = statbuf.st_size; diff --git a/src/bin/pg_rewind/file_ops.h b/src/bin/pg_rewind/file_ops.h index ee0d01df1ae51..b955de0f48c3c 100644 --- a/src/bin/pg_rewind/file_ops.h +++ b/src/bin/pg_rewind/file_ops.h @@ -3,7 +3,7 @@ * file_ops.h * Helper functions for operating on files * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c index c933871ca9fda..b79c47f925266 100644 --- a/src/bin/pg_rewind/filemap.c +++ b/src/bin/pg_rewind/filemap.c @@ -16,7 +16,7 @@ * for each file. Finally, it sorts the array to the final order that the * actions should be executed in. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -55,7 +55,7 @@ static filehash_hash *filehash; -static bool isRelDataFile(const char *path); +static file_content_type_t getFileContentType(const char *path); static char *datasegpath(RelFileLocator rlocator, ForkNumber forknum, BlockNumber segno); @@ -210,7 +210,7 @@ insert_filehash_entry(const char *path) if (!found) { entry->path = pg_strdup(path); - entry->isrelfile = isRelDataFile(path); + entry->content_type = getFileContentType(path); entry->target_exists = false; entry->target_type = FILE_TYPE_UNDEFINED; @@ -294,7 +294,7 @@ process_source_file(const char *path, file_type_t type, size_t size, * sanity check: a filename that looks like a data file better be a * regular file */ - if (type != FILE_TYPE_REGULAR && isRelDataFile(path)) + if (type != FILE_TYPE_REGULAR && getFileContentType(path) == FILE_CONTENT_TYPE_RELATION) pg_fatal("data file \"%s\" in source is not a regular file", path); /* Remember this source file */ @@ -383,7 +383,7 @@ process_target_wal_block_change(ForkNumber forknum, RelFileLocator rlocator, */ if (entry) { - Assert(entry->isrelfile); + Assert(entry->content_type == FILE_CONTENT_TYPE_RELATION); if (entry->target_exists) { @@ -546,7 +546,9 @@ print_filemap(filemap_t *filemap) for (i = 0; i < filemap->nentries; i++) { entry = filemap->entries[i]; + if (entry->action != FILE_ACTION_NONE || + entry->content_type == FILE_CONTENT_TYPE_WAL || entry->target_pages_to_overwrite.bitmapsize > 0) { pg_log_debug("%s (%s)", entry->path, @@ -560,22 +562,35 @@ print_filemap(filemap_t *filemap) } /* - * Does it look like a relation data file? - * - * For our purposes, only files belonging to the main fork are considered - * relation files. Other forks are always copied in toto, because we cannot - * reliably track changes to them, because WAL only contains block references - * for the main fork. + * Determine what kind of file this one looks like. */ -static bool -isRelDataFile(const char *path) +static file_content_type_t +getFileContentType(const char *path) { RelFileLocator rlocator; unsigned int segNo; int nmatch; - bool matched; + file_content_type_t result = FILE_CONTENT_TYPE_OTHER; + + /* Check if it is a WAL file. */ + if (strncmp("pg_wal/", path, 7) == 0) + { + const char *filename = path + 7; /* Skip "pg_wal/" */ + + if (IsXLogFileName(filename)) + return FILE_CONTENT_TYPE_WAL; + else + return FILE_CONTENT_TYPE_OTHER; + } /*---- + * Does it look like a relation data file? + * + * For our purposes, only files belonging to the main fork are considered + * relation files. Other forks are always copied in toto, because we + * cannot reliably track changes to them, because WAL only contains block + * references for the main fork. + * * Relation data files can be in one of the following directories: * * global/ @@ -598,14 +613,14 @@ isRelDataFile(const char *path) rlocator.dbOid = InvalidOid; rlocator.relNumber = InvalidRelFileNumber; segNo = 0; - matched = false; + result = FILE_CONTENT_TYPE_OTHER; nmatch = sscanf(path, "global/%u.%u", &rlocator.relNumber, &segNo); if (nmatch == 1 || nmatch == 2) { rlocator.spcOid = GLOBALTABLESPACE_OID; rlocator.dbOid = 0; - matched = true; + result = FILE_CONTENT_TYPE_RELATION; } else { @@ -614,7 +629,7 @@ isRelDataFile(const char *path) if (nmatch == 2 || nmatch == 3) { rlocator.spcOid = DEFAULTTABLESPACE_OID; - matched = true; + result = FILE_CONTENT_TYPE_RELATION; } else { @@ -622,7 +637,7 @@ isRelDataFile(const char *path) &rlocator.spcOid, &rlocator.dbOid, &rlocator.relNumber, &segNo); if (nmatch == 3 || nmatch == 4) - matched = true; + result = FILE_CONTENT_TYPE_RELATION; } } @@ -632,17 +647,17 @@ isRelDataFile(const char *path) * creates the exact same filename, when passed the RelFileLocator * information we extracted from the filename. */ - if (matched) + if (result == FILE_CONTENT_TYPE_RELATION) { char *check_path = datasegpath(rlocator, MAIN_FORKNUM, segNo); if (strcmp(check_path, path) != 0) - matched = false; + result = FILE_CONTENT_TYPE_OTHER; pfree(check_path); } - return matched; + return result; } /* @@ -679,8 +694,8 @@ datasegpath(RelFileLocator rlocator, ForkNumber forknum, BlockNumber segno) static int final_filemap_cmp(const void *a, const void *b) { - file_entry_t *fa = *((file_entry_t **) a); - file_entry_t *fb = *((file_entry_t **) b); + file_entry_t *fa = *((file_entry_t *const *) a); + file_entry_t *fb = *((file_entry_t *const *) b); if (fa->action > fb->action) return 1; @@ -693,11 +708,45 @@ final_filemap_cmp(const void *a, const void *b) return strcmp(fa->path, fb->path); } +/* + * Decide what to do with a WAL segment file based on its position + * relative to the point of divergence. + * + * Caller is responsible for ensuring that the file exists on both + * source and target servers. + */ +static file_action_t +decide_wal_file_action(const char *fname, XLogSegNo last_common_segno, + size_t source_size, size_t target_size) +{ + TimeLineID file_tli; + XLogSegNo file_segno; + + /* Get current WAL segment number given current segment file name */ + XLogFromFileName(fname, &file_tli, &file_segno, WalSegSz); + + /* + * Avoid copying files before the last common segment. + * + * These files exist on the source and the target servers, so they should + * be identical and located strictly before the segment that contains the + * LSN where target and source servers have diverged. + * + * While we are on it, double-check the size of each file and copy the + * file if they do not match, in case. + */ + if (file_segno < last_common_segno && + source_size == target_size) + return FILE_ACTION_NONE; + + return FILE_ACTION_COPY; +} + /* * Decide what action to perform to a file. */ static file_action_t -decide_file_action(file_entry_t *entry) +decide_file_action(file_entry_t *entry, XLogSegNo last_common_segno) { const char *path = entry->path; @@ -799,7 +848,21 @@ decide_file_action(file_entry_t *entry) return FILE_ACTION_NONE; case FILE_TYPE_REGULAR: - if (!entry->isrelfile) + if (entry->content_type == FILE_CONTENT_TYPE_WAL) + { + /* Handle WAL segment file */ + const char *filename = last_dir_separator(entry->path); + + if (filename == NULL) + filename = entry->path; + else + filename++; /* Skip the separator */ + + return decide_wal_file_action(filename, last_common_segno, + entry->source_size, + entry->target_size); + } + else if (entry->content_type != FILE_CONTENT_TYPE_RELATION) { /* * It's a non-data file that we have no special processing @@ -858,7 +921,7 @@ decide_file_action(file_entry_t *entry) * should be executed. */ filemap_t * -decide_file_actions(void) +decide_file_actions(XLogSegNo last_common_segno) { int i; filehash_iterator it; @@ -868,7 +931,7 @@ decide_file_actions(void) filehash_start_iterate(filehash, &it); while ((entry = filehash_iterate(filehash, &it)) != NULL) { - entry->action = decide_file_action(entry); + entry->action = decide_file_action(entry, last_common_segno); } /* diff --git a/src/bin/pg_rewind/filemap.h b/src/bin/pg_rewind/filemap.h index df78a02e3da03..4c6dd8740d724 100644 --- a/src/bin/pg_rewind/filemap.h +++ b/src/bin/pg_rewind/filemap.h @@ -2,7 +2,7 @@ * * filemap.h * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group *------------------------------------------------------------------------- */ #ifndef FILEMAP_H @@ -11,6 +11,7 @@ #include "datapagemap.h" #include "storage/block.h" #include "storage/relfilelocator.h" +#include "access/xlogdefs.h" /* these enum values are sorted in the order we want actions to be processed */ typedef enum @@ -36,6 +37,13 @@ typedef enum FILE_TYPE_SYMLINK, } file_type_t; +typedef enum +{ + FILE_CONTENT_TYPE_OTHER = 0, + FILE_CONTENT_TYPE_RELATION, + FILE_CONTENT_TYPE_WAL +} file_content_type_t; + /* * For every file found in the local or remote system, we have a file entry * that contains information about the file on both systems. For relation @@ -51,7 +59,7 @@ typedef struct file_entry_t uint32 status; /* hash status */ const char *path; - bool isrelfile; /* is it a relation data file? */ + file_content_type_t content_type; /* * Status of the file in the target. @@ -106,7 +114,7 @@ extern void process_target_wal_block_change(ForkNumber forknum, RelFileLocator rlocator, BlockNumber blkno); -extern filemap_t *decide_file_actions(void); +extern filemap_t *decide_file_actions(XLogSegNo last_common_segno); extern void calculate_totals(filemap_t *filemap); extern void print_filemap(filemap_t *filemap); diff --git a/src/bin/pg_rewind/libpq_source.c b/src/bin/pg_rewind/libpq_source.c index 56c2ad55d4a67..6955bc575eaa2 100644 --- a/src/bin/pg_rewind/libpq_source.c +++ b/src/bin/pg_rewind/libpq_source.c @@ -3,7 +3,7 @@ * libpq_source.c * Functions for fetching files from a remote server via libpq. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -84,7 +84,7 @@ init_libpq_source(PGconn *conn) init_libpq_conn(conn); - src = pg_malloc0(sizeof(libpq_source)); + src = pg_malloc0_object(libpq_source); src->common.traverse_files = libpq_traverse_files; src->common.fetch_file = libpq_fetch_file; @@ -215,7 +215,7 @@ libpq_get_current_wal_insert_lsn(rewind_source *source) val = run_simple_query(conn, "SELECT pg_current_wal_insert_lsn()"); - if (sscanf(val, "%X/%X", &hi, &lo) != 2) + if (sscanf(val, "%X/%08X", &hi, &lo) != 2) pg_fatal("unrecognized result \"%s\" for current WAL insert location", val); result = ((uint64) hi) << 32 | lo; @@ -459,7 +459,7 @@ process_queued_fetch_requests(libpq_source *src) appendArrayEscapedString(&src->paths, rq->path); appendStringInfo(&src->offsets, INT64_FORMAT, (int64) rq->offset); - appendStringInfo(&src->lengths, INT64_FORMAT, (int64) rq->length); + appendStringInfo(&src->lengths, "%zu", rq->length); } appendStringInfoChar(&src->paths, '}'); appendStringInfoChar(&src->offsets, '}'); diff --git a/src/bin/pg_rewind/local_source.c b/src/bin/pg_rewind/local_source.c index 5a6e805c15833..4841cf01fb700 100644 --- a/src/bin/pg_rewind/local_source.c +++ b/src/bin/pg_rewind/local_source.c @@ -3,7 +3,7 @@ * local_source.c * Functions for using a local data directory as the source. * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -39,7 +39,7 @@ init_local_source(const char *datadir) { local_source *src; - src = pg_malloc0(sizeof(local_source)); + src = pg_malloc0_object(local_source); src->common.traverse_files = local_traverse_files; src->common.fetch_file = local_fetch_file; @@ -112,8 +112,8 @@ local_queue_fetch_file(rewind_source *source, const char *path, size_t len) * check that the size of the file matches our earlier expectation. */ if (written_len != len) - pg_fatal("size of source file \"%s\" changed concurrently: %d bytes expected, %d copied", - srcpath, (int) len, (int) written_len); + pg_fatal("size of source file \"%s\" changed concurrently: %zu bytes expected, %zu copied", + srcpath, len, written_len); if (close(srcfd) != 0) pg_fatal("could not close file \"%s\": %m", srcpath); diff --git a/src/bin/pg_rewind/meson.build b/src/bin/pg_rewind/meson.build index 36171600ccaf4..52a6ab0a51503 100644 --- a/src/bin/pg_rewind/meson.build +++ b/src/bin/pg_rewind/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_rewind_sources = files( 'datapagemap.c', @@ -44,6 +44,7 @@ tests += { 't/008_min_recovery_point.pl', 't/009_growing_files.pl', 't/010_keep_recycled_wals.pl', + 't/011_wal_copy.pl', ], }, } diff --git a/src/bin/pg_rewind/parsexlog.c b/src/bin/pg_rewind/parsexlog.c index 2cd44625ca368..db7a7e73042fb 100644 --- a/src/bin/pg_rewind/parsexlog.c +++ b/src/bin/pg_rewind/parsexlog.c @@ -3,7 +3,7 @@ * parsexlog.c * Functions for reading Write-Ahead-Log * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- @@ -89,11 +89,11 @@ extractPageMap(const char *datadir, XLogRecPtr startpoint, int tliIndex, XLogRecPtr errptr = xlogreader->EndRecPtr; if (errormsg) - pg_fatal("could not read WAL record at %X/%X: %s", + pg_fatal("could not read WAL record at %X/%08X: %s", LSN_FORMAT_ARGS(errptr), errormsg); else - pg_fatal("could not read WAL record at %X/%X", + pg_fatal("could not read WAL record at %X/%08X", LSN_FORMAT_ARGS(errptr)); } @@ -105,7 +105,7 @@ extractPageMap(const char *datadir, XLogRecPtr startpoint, int tliIndex, * messed up. */ if (xlogreader->EndRecPtr != endpoint) - pg_fatal("end pointer %X/%X is not a valid end point; expected %X/%X", + pg_fatal("end pointer %X/%08X is not a valid end point; expected %X/%08X", LSN_FORMAT_ARGS(endpoint), LSN_FORMAT_ARGS(xlogreader->EndRecPtr)); XLogReaderFree(xlogreader); @@ -143,10 +143,10 @@ readOneRecord(const char *datadir, XLogRecPtr ptr, int tliIndex, if (record == NULL) { if (errormsg) - pg_fatal("could not read WAL record at %X/%X: %s", + pg_fatal("could not read WAL record at %X/%08X: %s", LSN_FORMAT_ARGS(ptr), errormsg); else - pg_fatal("could not read WAL record at %X/%X", + pg_fatal("could not read WAL record at %X/%08X", LSN_FORMAT_ARGS(ptr)); } endptr = xlogreader->EndRecPtr; @@ -211,11 +211,11 @@ findLastCheckpoint(const char *datadir, XLogRecPtr forkptr, int tliIndex, if (record == NULL) { if (errormsg) - pg_fatal("could not find previous WAL record at %X/%X: %s", + pg_fatal("could not find previous WAL record at %X/%08X: %s", LSN_FORMAT_ARGS(searchptr), errormsg); else - pg_fatal("could not find previous WAL record at %X/%X", + pg_fatal("could not find previous WAL record at %X/%08X", LSN_FORMAT_ARGS(searchptr)); } @@ -458,8 +458,8 @@ extractPageInfo(XLogReaderState *record) * we don't recognize the type. That's bad - we don't know how to * track that change. */ - pg_fatal("WAL record modifies a relation, but record type is not recognized: " - "lsn: %X/%X, rmid: %d, rmgr: %s, info: %02X", + pg_fatal("WAL record modifies a relation, but record type is not recognized:\n" + "lsn: %X/%08X, rmid: %d, rmgr: %s, info: %02X", LSN_FORMAT_ARGS(record->ReadRecPtr), rmid, RmgrName(rmid), info); } diff --git a/src/bin/pg_rewind/pg_rewind.c b/src/bin/pg_rewind/pg_rewind.c index 9d16c1e6b4757..9d745d4b25b46 100644 --- a/src/bin/pg_rewind/pg_rewind.c +++ b/src/bin/pg_rewind/pg_rewind.c @@ -3,7 +3,7 @@ * pg_rewind.c * Synchronizes a PostgreSQL data directory to a new timeline * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -147,6 +147,7 @@ main(int argc, char **argv) TimeLineID source_tli; TimeLineID target_tli; XLogRecPtr target_wal_endrec; + XLogSegNo last_common_segno; size_t size; char *buffer; bool no_ensure_shutdown = false; @@ -299,10 +300,12 @@ main(int argc, char **argv) atexit(disconnect_atexit); - /* - * Ok, we have all the options and we're ready to start. First, connect to - * remote server. - */ + /* Ok, we have all the options and we're ready to start. */ + if (dry_run) + pg_log_info("Executing in dry-run mode.\n" + "The target directory will not be modified."); + + /* First, connect to remote server. */ if (connstr_source) { conn = PQconnectdb(connstr_source); @@ -374,7 +377,7 @@ main(int argc, char **argv) { pg_log_info("source and target cluster are on the same timeline"); rewind_needed = false; - target_wal_endrec = 0; + target_wal_endrec = InvalidXLogRecPtr; } else { @@ -393,10 +396,16 @@ main(int argc, char **argv) targetHistory, targetNentries, &divergerec, &lastcommontliIndex); - pg_log_info("servers diverged at WAL location %X/%X on timeline %u", + pg_log_info("servers diverged at WAL location %X/%08X on timeline %u", LSN_FORMAT_ARGS(divergerec), targetHistory[lastcommontliIndex].tli); + /* + * Convert the divergence LSN to a segment number, that will be used + * to decide how WAL segments should be processed. + */ + XLByteToSeg(divergerec, last_common_segno, ControlFile_target.xlog_seg_size); + /* * Don't need the source history anymore. The target history is still * needed by the routines in parsexlog.c, when we read the target WAL. @@ -461,7 +470,7 @@ main(int argc, char **argv) findLastCheckpoint(datadir_target, divergerec, lastcommontliIndex, &chkptrec, &chkpttli, &chkptredo, restore_command); - pg_log_info("rewinding from last common checkpoint at %X/%X on timeline %u", + pg_log_info("rewinding from last common checkpoint at %X/%08X on timeline %u", LSN_FORMAT_ARGS(chkptrec), chkpttli); /* Initialize the hash table to track the status of each file */ @@ -492,7 +501,7 @@ main(int argc, char **argv) * We have collected all information we need from both systems. Decide * what to do with each file. */ - filemap = decide_file_actions(); + filemap = decide_file_actions(last_common_segno); if (showprogress) calculate_totals(filemap); @@ -505,9 +514,9 @@ main(int argc, char **argv) */ if (showprogress) { - pg_log_info("need to copy %lu MB (total source directory size is %lu MB)", - (unsigned long) (filemap->fetch_size / (1024 * 1024)), - (unsigned long) (filemap->total_size / (1024 * 1024))); + pg_log_info("need to copy %" PRIu64 " MB (total source directory size is %" PRIu64 " MB)", + filemap->fetch_size / (1024 * 1024), + filemap->total_size / (1024 * 1024)); fetch_size = filemap->fetch_size; fetch_done = 0; @@ -843,9 +852,9 @@ progress_report(bool finished) static XLogRecPtr MinXLogRecPtr(XLogRecPtr a, XLogRecPtr b) { - if (XLogRecPtrIsInvalid(a)) + if (!XLogRecPtrIsValid(a)) return b; - else if (XLogRecPtrIsInvalid(b)) + else if (!XLogRecPtrIsValid(b)) return a; else return Min(a, b); @@ -865,7 +874,7 @@ getTimelineHistory(TimeLineID tli, bool is_source, int *nentries) */ if (tli == 1) { - history = (TimeLineHistoryEntry *) pg_malloc(sizeof(TimeLineHistoryEntry)); + history = pg_malloc_object(TimeLineHistoryEntry); history->tli = tli; history->begin = history->end = InvalidXLogRecPtr; *nentries = 1; @@ -902,7 +911,7 @@ getTimelineHistory(TimeLineID tli, bool is_source, int *nentries) TimeLineHistoryEntry *entry; entry = &history[i]; - pg_log_debug("%u: %X/%X - %X/%X", entry->tli, + pg_log_debug("%u: %X/%08X - %X/%08X", entry->tli, LSN_FORMAT_ARGS(entry->begin), LSN_FORMAT_ARGS(entry->end)); } @@ -981,8 +990,8 @@ createBackupLabel(XLogRecPtr startpoint, TimeLineID starttli, XLogRecPtr checkpo strftime(strfbuf, sizeof(strfbuf), "%Y-%m-%d %H:%M:%S %Z", tmp); len = snprintf(buf, sizeof(buf), - "START WAL LOCATION: %X/%X (file %s)\n" - "CHECKPOINT LOCATION: %X/%X\n" + "START WAL LOCATION: %X/%08X (file %s)\n" + "CHECKPOINT LOCATION: %X/%08X\n" "BACKUP METHOD: pg_rewind\n" "BACKUP FROM: standby\n" "START TIME: %s\n", @@ -1026,8 +1035,8 @@ digestControlFile(ControlFileData *ControlFile, const char *content, size_t size) { if (size != PG_CONTROL_FILE_SIZE) - pg_fatal("unexpected control file size %d, expected %d", - (int) size, PG_CONTROL_FILE_SIZE); + pg_fatal("unexpected control file size %zu, expected %d", + size, PG_CONTROL_FILE_SIZE); memcpy(ControlFile, content, sizeof(ControlFileData)); diff --git a/src/bin/pg_rewind/pg_rewind.h b/src/bin/pg_rewind/pg_rewind.h index 9cea144d2b241..9a981f7f24682 100644 --- a/src/bin/pg_rewind/pg_rewind.h +++ b/src/bin/pg_rewind/pg_rewind.h @@ -3,7 +3,7 @@ * pg_rewind.h * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- diff --git a/src/bin/pg_rewind/po/meson.build b/src/bin/pg_rewind/po/meson.build index 6ff9790289423..bb2a6d2b5728e 100644 --- a/src/bin/pg_rewind/po/meson.build +++ b/src/bin/pg_rewind/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_rewind-' + pg_version_major.to_string())] diff --git a/src/bin/pg_rewind/rewind_source.h b/src/bin/pg_rewind/rewind_source.h index 8b619a2db40f3..f2e9b84a7329c 100644 --- a/src/bin/pg_rewind/rewind_source.h +++ b/src/bin/pg_rewind/rewind_source.h @@ -8,7 +8,7 @@ * operations to fetch data from the source system, so that the rest of * the code doesn't need to care what kind of a source its dealing with. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ diff --git a/src/bin/pg_rewind/t/001_basic.pl b/src/bin/pg_rewind/t/001_basic.pl index 031060db84211..8d6ab3484b817 100644 --- a/src/bin/pg_rewind/t/001_basic.pl +++ b/src/bin/pg_rewind/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_rewind/t/002_databases.pl b/src/bin/pg_rewind/t/002_databases.pl index 031c545faa02f..0740dfd1c84e7 100644 --- a/src/bin/pg_rewind/t/002_databases.pl +++ b/src/bin/pg_rewind/t/002_databases.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_rewind/t/003_extrafiles.pl b/src/bin/pg_rewind/t/003_extrafiles.pl index 823bcabeb8247..52aa5d05bd0f5 100644 --- a/src/bin/pg_rewind/t/003_extrafiles.pl +++ b/src/bin/pg_rewind/t/003_extrafiles.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test how pg_rewind reacts to extra files and directories in the data dirs. diff --git a/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl b/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl index dd577c0aa09b9..0c7398ebeea2f 100644 --- a/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl +++ b/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test pg_rewind when the target's pg_wal directory is a symlink. diff --git a/src/bin/pg_rewind/t/005_same_timeline.pl b/src/bin/pg_rewind/t/005_same_timeline.pl index 500b326ead462..95a40c3b27081 100644 --- a/src/bin/pg_rewind/t/005_same_timeline.pl +++ b/src/bin/pg_rewind/t/005_same_timeline.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test that running pg_rewind with the source and target clusters diff --git a/src/bin/pg_rewind/t/006_options.pl b/src/bin/pg_rewind/t/006_options.pl index b7a84ac4d7afb..011f879e6f405 100644 --- a/src/bin/pg_rewind/t/006_options.pl +++ b/src/bin/pg_rewind/t/006_options.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test checking options of pg_rewind. diff --git a/src/bin/pg_rewind/t/007_standby_source.pl b/src/bin/pg_rewind/t/007_standby_source.pl index 583e551a3e895..f2cb675830214 100644 --- a/src/bin/pg_rewind/t/007_standby_source.pl +++ b/src/bin/pg_rewind/t/007_standby_source.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test using a standby server as the source. diff --git a/src/bin/pg_rewind/t/008_min_recovery_point.pl b/src/bin/pg_rewind/t/008_min_recovery_point.pl index 28496afe350e8..6843ae00ab22a 100644 --- a/src/bin/pg_rewind/t/008_min_recovery_point.pl +++ b/src/bin/pg_rewind/t/008_min_recovery_point.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test situation where a target data directory contains diff --git a/src/bin/pg_rewind/t/009_growing_files.pl b/src/bin/pg_rewind/t/009_growing_files.pl index afe68c8bf0d32..9f728647663c2 100644 --- a/src/bin/pg_rewind/t/009_growing_files.pl +++ b/src/bin/pg_rewind/t/009_growing_files.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_rewind/t/010_keep_recycled_wals.pl b/src/bin/pg_rewind/t/010_keep_recycled_wals.pl index 55bf046d9c10c..ce0defb6ffa69 100644 --- a/src/bin/pg_rewind/t/010_keep_recycled_wals.pl +++ b/src/bin/pg_rewind/t/010_keep_recycled_wals.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Test situation where a target data directory contains # WAL files that were already recycled by the new primary. diff --git a/src/bin/pg_rewind/t/011_wal_copy.pl b/src/bin/pg_rewind/t/011_wal_copy.pl new file mode 100644 index 0000000000000..1de593a3c6fe6 --- /dev/null +++ b/src/bin/pg_rewind/t/011_wal_copy.pl @@ -0,0 +1,122 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# Check how the copy of WAL segments is handled from the source to +# the target server. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use Test::More; +use File::stat qw(stat); + +use FindBin; +use lib $FindBin::RealBin; +use RewindTest; + +RewindTest::setup_cluster(); +RewindTest::start_primary(); +RewindTest::create_standby(); + +# Advance WAL on primary +RewindTest::primary_psql("CREATE TABLE t(a int)"); +RewindTest::primary_psql("INSERT INTO t VALUES(0)"); + +# Segment that is not copied from the source to the target, being +# generated before the servers have diverged. +my $wal_seg_skipped = $node_primary->safe_psql('postgres', + 'SELECT pg_walfile_name(pg_current_wal_lsn())'); + +RewindTest::primary_psql("SELECT pg_switch_wal()"); + +# Follow-up segment, that will include corrupted contents, and will be +# copied from the source to the target even if generated before the point +# of divergence. +RewindTest::primary_psql("INSERT INTO t VALUES(0)"); +my $corrupt_wal_seg = $node_primary->safe_psql('postgres', + 'SELECT pg_walfile_name(pg_current_wal_lsn())'); +RewindTest::primary_psql("SELECT pg_switch_wal()"); + +RewindTest::primary_psql("CHECKPOINT"); +RewindTest::promote_standby; + +# New segment on a new timeline, expected to be copied. +my $new_timeline_wal_seg = $node_standby->safe_psql('postgres', + 'SELECT pg_walfile_name(pg_current_wal_lsn())'); + +# Corrupt a WAL segment on target that has been generated before the +# divergence point. We will check that it is copied from the source. +my $corrupt_wal_seg_in_target_path = + $node_primary->data_dir . '/pg_wal/' . $corrupt_wal_seg; +open my $fh, ">>", $corrupt_wal_seg_in_target_path + or die "could not open $corrupt_wal_seg_in_target_path"; + +print $fh 'a'; +close $fh; + +my $corrupt_wal_seg_stat_before_rewind = + stat($corrupt_wal_seg_in_target_path); +ok(defined($corrupt_wal_seg_stat_before_rewind), + "segment $corrupt_wal_seg exists in target before rewind"); + +# Verify that the WAL segment on the new timeline does not exist in target +# before the rewind. +my $new_timeline_wal_seg_path = + $node_primary->data_dir . '/pg_wal/' . $new_timeline_wal_seg; +my $new_timeline_wal_seg_stat = stat($new_timeline_wal_seg_path); +ok(!defined($new_timeline_wal_seg_stat), + "segment $new_timeline_wal_seg does not exist in target before rewind"); + +$node_standby->stop(); +$node_primary->stop(); + +# Cross-check how WAL segments are handled: +# - The "corrupted" segment generated before the point of divergence is +# copied. +# - The "clean" segment generated before the point of divergence is skipped. +# - The segment of the new timeline is copied. +command_checks_all( + [ + 'pg_rewind', '--debug', + '--source-pgdata' => $node_standby->data_dir, + '--target-pgdata' => $node_primary->data_dir, + '--no-sync', + ], + 0, + [qr//], + [ + qr/pg_wal\/$wal_seg_skipped \(NONE\)/, + qr/pg_wal\/$corrupt_wal_seg \(COPY\)/, + qr/pg_wal\/$new_timeline_wal_seg \(COPY\)/, + ], + 'run pg_rewind'); + +# Verify that the first WAL segment of the new timeline now exists in +# target. +$new_timeline_wal_seg_stat = stat($new_timeline_wal_seg_path); +ok(defined($new_timeline_wal_seg_stat), + "new timeline segment $new_timeline_wal_seg exists in target after rewind" +); + +# Validate that the WAL segment with the same file name as the +# corrupted WAL segment in target has been copied from source +# where it was still intact. +my $corrupt_wal_seg_in_source_path = + $node_standby->data_dir . '/pg_wal/' . $corrupt_wal_seg; +my $corrupt_wal_seg_source_stat = stat($corrupt_wal_seg_in_source_path); +ok(defined($corrupt_wal_seg_source_stat), + "corrupted $corrupt_wal_seg exists in source after rewind"); + +my $corrupt_wal_seg_stat_after_rewind = stat($corrupt_wal_seg_in_target_path); +ok(defined($corrupt_wal_seg_stat_after_rewind), + "corrupted $corrupt_wal_seg exists in target after rewind"); +isnt( + $corrupt_wal_seg_stat_before_rewind->size, + $corrupt_wal_seg_source_stat->size, + "different size of corrupted $corrupt_wal_seg in source vs target before rewind" +); +is( $corrupt_wal_seg_stat_after_rewind->size, + $corrupt_wal_seg_source_stat->size, + "same size of corrupted $corrupt_wal_seg in source and target after rewind" +); + +done_testing(); diff --git a/src/bin/pg_rewind/t/RewindTest.pm b/src/bin/pg_rewind/t/RewindTest.pm index 3efab8317978a..32aeca80f131a 100644 --- a/src/bin/pg_rewind/t/RewindTest.pm +++ b/src/bin/pg_rewind/t/RewindTest.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group package RewindTest; @@ -285,7 +285,7 @@ sub run_pg_rewind # Check that pg_rewind with dbname and --write-recovery-conf # wrote the dbname in the generated primary_conninfo value. like(slurp_file("$primary_pgdata/postgresql.auto.conf"), - qr/dbname=postgres/m, 'recovery conf file sets dbname'); + qr/dbname=postgres/m, 'recovery conf file sets dbname'); # Check that standby.signal is here as recovery configuration # was requested. diff --git a/src/bin/pg_rewind/timeline.c b/src/bin/pg_rewind/timeline.c index 4d9f0d8301bf2..dda06eaa0bc6b 100644 --- a/src/bin/pg_rewind/timeline.c +++ b/src/bin/pg_rewind/timeline.c @@ -3,7 +3,7 @@ * timeline.c * timeline-related functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -66,7 +66,7 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries) if (*ptr == '\0' || *ptr == '#') continue; - nfields = sscanf(fline, "%u\t%X/%X", &tli, &switchpoint_hi, &switchpoint_lo); + nfields = sscanf(fline, "%u\t%X/%08X", &tli, &switchpoint_hi, &switchpoint_lo); if (nfields < 1) { @@ -91,7 +91,7 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries) lasttli = tli; nlines++; - entries = pg_realloc(entries, nlines * sizeof(TimeLineHistoryEntry)); + entries = pg_realloc_array(entries, TimeLineHistoryEntry, nlines); entry = &entries[nlines - 1]; entry->tli = tli; @@ -115,9 +115,9 @@ rewind_parseTimeLineHistory(char *buffer, TimeLineID targetTLI, int *nentries) */ nlines++; if (entries) - entries = pg_realloc(entries, nlines * sizeof(TimeLineHistoryEntry)); + entries = pg_realloc_array(entries, TimeLineHistoryEntry, nlines); else - entries = pg_malloc(1 * sizeof(TimeLineHistoryEntry)); + entries = pg_malloc_array(TimeLineHistoryEntry, 1); entry = &entries[nlines - 1]; entry->tli = targetTLI; diff --git a/src/bin/pg_test_fsync/meson.build b/src/bin/pg_test_fsync/meson.build index 15f3439f99b5b..f14793d665ab0 100644 --- a/src/bin/pg_test_fsync/meson.build +++ b/src/bin/pg_test_fsync/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_fsync_sources = files( 'pg_test_fsync.c', diff --git a/src/bin/pg_test_fsync/pg_test_fsync.c b/src/bin/pg_test_fsync/pg_test_fsync.c index 0060ea15902cb..4b84f86e7d7b7 100644 --- a/src/bin/pg_test_fsync/pg_test_fsync.c +++ b/src/bin/pg_test_fsync/pg_test_fsync.c @@ -2,7 +2,7 @@ * * pg_test_fsync --- tests all supported fsync() methods * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/bin/pg_test_fsync/pg_test_fsync.c * @@ -68,9 +68,8 @@ static const char *progname; static unsigned int secs_per_test = 5; static int needs_unlink = 0; -static char full_buf[DEFAULT_XLOG_SEG_SIZE], - *buf, - *filename = FSYNC_FILENAME; +alignas(PGAlignedXLogBlock) static char buf[DEFAULT_XLOG_SEG_SIZE]; +static char *filename = FSYNC_FILENAME; static struct timeval start_t, stop_t; static sig_atomic_t alarm_triggered = false; @@ -232,9 +231,7 @@ prepare_buf(void) /* write random data into buffer */ for (ops = 0; ops < DEFAULT_XLOG_SEG_SIZE; ops++) - full_buf[ops] = (char) pg_prng_int32(&pg_global_prng_state); - - buf = (char *) TYPEALIGN(XLOG_BLCKSZ, full_buf); + buf[ops] = (char) pg_prng_int32(&pg_global_prng_state); } static void @@ -248,7 +245,7 @@ test_open(void) if ((tmpfile = open(filename, O_RDWR | O_CREAT | PG_BINARY, S_IRUSR | S_IWUSR)) == -1) die("could not open output file"); needs_unlink = 1; - if (write(tmpfile, full_buf, DEFAULT_XLOG_SEG_SIZE) != + if (write(tmpfile, buf, DEFAULT_XLOG_SEG_SIZE) != DEFAULT_XLOG_SEG_SIZE) die("write failed"); diff --git a/src/bin/pg_test_fsync/po/meson.build b/src/bin/pg_test_fsync/po/meson.build index 08b2e396c8123..c9889e07cdc87 100644 --- a/src/bin/pg_test_fsync/po/meson.build +++ b/src/bin/pg_test_fsync/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_test_fsync-' + pg_version_major.to_string())] diff --git a/src/bin/pg_test_fsync/t/001_basic.pl b/src/bin/pg_test_fsync/t/001_basic.pl index 7eed51233c481..ed1dfa9d8b4db 100644 --- a/src/bin/pg_test_fsync/t/001_basic.pl +++ b/src/bin/pg_test_fsync/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_test_timing/meson.build b/src/bin/pg_test_timing/meson.build index 64550f5458f08..89f31fa95299c 100644 --- a/src/bin/pg_test_timing/meson.build +++ b/src/bin/pg_test_timing/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_test_timing_sources = files( 'pg_test_timing.c' diff --git a/src/bin/pg_test_timing/pg_test_timing.c b/src/bin/pg_test_timing/pg_test_timing.c index ce7aad4b25a4b..2afb0e6a41034 100644 --- a/src/bin/pg_test_timing/pg_test_timing.c +++ b/src/bin/pg_test_timing/pg_test_timing.c @@ -9,32 +9,50 @@ #include #include "getopt_long.h" +#include "port/pg_bitutils.h" #include "portability/instr_time.h" static const char *progname; static unsigned int test_duration = 3; +static double max_rprct = 99.99; + +/* record duration in powers of 2 nanoseconds */ +static long long int histogram[32]; + +/* record counts of first 10K durations directly */ +#define NUM_DIRECT 10000 +static long long int direct_histogram[NUM_DIRECT]; + +/* separately record highest observed duration */ +static int32 largest_diff; +static long long int largest_diff_count; + static void handle_args(int argc, char *argv[]); -static uint64 test_timing(unsigned int duration); +static void test_system_timing(void); +#if PG_INSTR_TSC_CLOCK +static void test_tsc_timing(void); +#endif +static uint64 test_timing(unsigned int duration, TimingClockSourceType source, bool fast_timing); static void output(uint64 loop_count); -/* record duration in powers of 2 microseconds */ -static long long int histogram[32]; - int main(int argc, char *argv[]) { - uint64 loop_count; - set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_test_timing")); progname = get_progname(argv[0]); handle_args(argc, argv); - loop_count = test_timing(test_duration); + /* initialize timing infrastructure (required for INSTR_* calls) */ + pg_initialize_timing(); - output(loop_count); + test_system_timing(); + +#if PG_INSTR_TSC_CLOCK + test_tsc_timing(); +#endif return 0; } @@ -44,6 +62,7 @@ handle_args(int argc, char *argv[]) { static struct option long_options[] = { {"duration", required_argument, NULL, 'd'}, + {"cutoff", required_argument, NULL, 'c'}, {NULL, 0, NULL, 0} }; @@ -56,7 +75,7 @@ handle_args(int argc, char *argv[]) { if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0) { - printf(_("Usage: %s [-d DURATION]\n"), progname); + printf(_("Usage: %s [-d DURATION] [-c CUTOFF]\n"), progname); exit(0); } if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0) @@ -66,7 +85,7 @@ handle_args(int argc, char *argv[]) } } - while ((option = getopt_long(argc, argv, "d:", + while ((option = getopt_long(argc, argv, "d:c:", long_options, &optindex)) != -1) { switch (option) @@ -93,6 +112,26 @@ handle_args(int argc, char *argv[]) } break; + case 'c': + errno = 0; + max_rprct = strtod(optarg, &endptr); + + if (endptr == optarg || *endptr != '\0' || errno != 0) + { + fprintf(stderr, _("%s: invalid argument for option %s\n"), + progname, "--cutoff"); + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); + exit(1); + } + + if (max_rprct < 0 || max_rprct > 100) + { + fprintf(stderr, _("%s: %s must be in range %u..%u\n"), + progname, "--cutoff", 0, 100); + exit(1); + } + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); @@ -111,42 +150,135 @@ handle_args(int argc, char *argv[]) exit(1); } - - printf(ngettext("Testing timing overhead for %u second.\n", - "Testing timing overhead for %u seconds.\n", + printf(ngettext("Testing timing overhead for %u second.\n\n", + "Testing timing overhead for %u seconds.\n\n", test_duration), test_duration); } +/* + * This tests default (non-fast) timing code. A clock source for that is + * always available. Hence, we can unconditionally output the result. + */ +static void +test_system_timing(void) +{ + uint64 loop_count; + + loop_count = test_timing(test_duration, TIMING_CLOCK_SOURCE_SYSTEM, false); + output(loop_count); +} + +/* + * If on a supported architecture, test the TSC clock source. This clock + * source is not always available. In that case we print an informational + * message indicating as such. + * + * We first emit "slow" timings (RDTSCP on x86), which are used for higher + * precision measurements when the TSC clock source is enabled. We emit + * "fast" timings second (RDTSC on x86), which is used for faster timing + * measurements with lower precision. + */ +#if PG_INSTR_TSC_CLOCK +static void +test_tsc_timing(void) +{ + uint64 loop_count; + uint32 calibrated_freq; + + printf("\n"); + loop_count = test_timing(test_duration, TIMING_CLOCK_SOURCE_TSC, false); + if (loop_count > 0) + { + output(loop_count); + printf("\n"); + + /* Now, emit fast timing measurements */ + loop_count = test_timing(test_duration, TIMING_CLOCK_SOURCE_TSC, true); + output(loop_count); + printf("\n"); + + printf(_("TSC frequency in use: %u kHz\n"), timing_tsc_frequency_khz); + + calibrated_freq = pg_tsc_calibrate_frequency(); + if (calibrated_freq > 0) + printf(_("TSC frequency from calibration: %u kHz\n"), calibrated_freq); + else + printf(_("TSC calibration did not converge\n")); + + pg_set_timing_clock_source(TIMING_CLOCK_SOURCE_AUTO); + if (pg_current_timing_clock_source() == TIMING_CLOCK_SOURCE_TSC) + printf(_("TSC clock source will be used by default, unless timing_clock_source is set to 'system'.\n")); + else + printf(_("TSC clock source will not be used by default, unless timing_clock_source is set to 'tsc'.\n")); + } + else + printf(_("TSC clock source is not usable. Likely unable to determine TSC frequency. Are you running in an unsupported virtualized environment?\n")); +} +#endif + static uint64 -test_timing(unsigned int duration) +test_timing(unsigned int duration, TimingClockSourceType source, bool fast_timing) { - uint64 total_time; - int64 time_elapsed = 0; uint64 loop_count = 0; - uint64 prev, - cur; instr_time start_time, end_time, - temp; - - total_time = duration > 0 ? duration * INT64CONST(1000000) : 0; + prev, + cur; + const char *time_source = NULL; + + if (!pg_set_timing_clock_source(source)) + return 0; + + time_source = PG_INSTR_SYSTEM_CLOCK_NAME; + +#if PG_INSTR_TSC_CLOCK + if (pg_current_timing_clock_source() == TIMING_CLOCK_SOURCE_TSC) + time_source = fast_timing ? PG_INSTR_TSC_CLOCK_NAME_FAST : PG_INSTR_TSC_CLOCK_NAME; +#endif + + if (fast_timing) + printf(_("Fast clock source: %s\n"), time_source); + else if (source == TIMING_CLOCK_SOURCE_SYSTEM) + printf(_("System clock source: %s\n"), time_source); + else + printf(_("Clock source: %s\n"), time_source); + + /* + * Pre-zero the statistics data structures. They're already zero by + * default, but this helps bring them into processor cache and avoid + * possible timing glitches due to COW behavior. + */ + memset(direct_histogram, 0, sizeof(direct_histogram)); + memset(histogram, 0, sizeof(histogram)); + largest_diff = 0; + largest_diff_count = 0; INSTR_TIME_SET_CURRENT(start_time); - cur = INSTR_TIME_GET_MICROSEC(start_time); + cur = start_time; - while (time_elapsed < total_time) + end_time = start_time; + INSTR_TIME_ADD_NANOSEC(end_time, duration * NS_PER_S); + + while (INSTR_TIME_GT(end_time, cur)) { int32 diff, - bits = 0; + bits; + instr_time diff_time; prev = cur; - INSTR_TIME_SET_CURRENT(temp); - cur = INSTR_TIME_GET_MICROSEC(temp); - diff = cur - prev; + + if (fast_timing) + INSTR_TIME_SET_CURRENT_FAST(cur); + else + INSTR_TIME_SET_CURRENT(cur); + + diff_time = cur; + INSTR_TIME_SUBTRACT(diff_time, prev); + diff = INSTR_TIME_GET_NANOSEC(diff_time); /* Did time go backwards? */ - if (diff < 0) + if (unlikely(diff < 0)) { fprintf(stderr, _("Detected clock going backwards in time.\n")); fprintf(stderr, _("Time warp: %d ms\n"), diff); @@ -154,25 +286,36 @@ test_timing(unsigned int duration) } /* What is the highest bit in the time diff? */ - while (diff) - { - diff >>= 1; - bits++; - } + if (diff > 0) + bits = pg_leftmost_one_pos32(diff) + 1; + else + bits = 0; /* Update appropriate duration bucket */ histogram[bits]++; + /* Update direct histogram of time diffs */ + if (diff < NUM_DIRECT) + direct_histogram[diff]++; + + /* Also track the largest observed duration, even if >= NUM_DIRECT */ + if (diff > largest_diff) + { + largest_diff = diff; + largest_diff_count = 1; + } + else if (diff == largest_diff) + largest_diff_count++; + loop_count++; - INSTR_TIME_SUBTRACT(temp, start_time); - time_elapsed = INSTR_TIME_GET_MICROSEC(temp); } + /* Refresh end time to be the actual time spent (vs the target end time) */ INSTR_TIME_SET_CURRENT(end_time); INSTR_TIME_SUBTRACT(end_time, start_time); - printf(_("Per loop time including overhead: %0.2f ns\n"), + printf(_("Average loop time including overhead: %0.2f ns\n"), INSTR_TIME_GET_DOUBLE(end_time) * 1e9 / loop_count); return loop_count; @@ -181,28 +324,95 @@ test_timing(unsigned int duration) static void output(uint64 loop_count) { - int64 max_bit = 31, - i; - char *header1 = _("< us"); - char *header2 = /* xgettext:no-c-format */ _("% of total"); - char *header3 = _("count"); + int max_bit = 31; + const char *header1 = _("<= ns"); + const char *header1b = _("ns"); + const char *header2 = /* xgettext:no-c-format */ _("% of total"); + const char *header3 = /* xgettext:no-c-format */ _("running %"); + const char *header4 = _("count"); int len1 = strlen(header1); int len2 = strlen(header2); int len3 = strlen(header3); + int len4 = strlen(header4); + double rprct; + bool stopped = false; /* find highest bit value */ while (max_bit > 0 && histogram[max_bit] == 0) max_bit--; + /* set minimum column widths */ + len1 = Max(8, len1); + len2 = Max(10, len2); + len3 = Max(10, len3); + len4 = Max(10, len4); + printf(_("Histogram of timing durations:\n")); - printf("%*s %*s %*s\n", - Max(6, len1), header1, - Max(10, len2), header2, - Max(10, len3), header3); - - for (i = 0; i <= max_bit; i++) - printf("%*ld %*.5f %*lld\n", - Max(6, len1), 1l << i, - Max(10, len2) - 1, (double) histogram[i] * 100 / loop_count, - Max(10, len3), histogram[i]); + printf("%*s %*s %*s %*s\n", + len1, header1, + len2, header2, + len3, header3, + len4, header4); + + rprct = 0; + for (int i = 0; i <= max_bit; i++) + { + double prct = (double) histogram[i] * 100 / loop_count; + + rprct += prct; + printf("%*ld %*.4f %*.4f %*lld\n", + len1, (1L << i) - 1, + len2, prct, + len3, rprct, + len4, histogram[i]); + } + + printf(_("\nObserved timing durations up to %.4f%%:\n"), max_rprct); + printf("%*s %*s %*s %*s\n", + len1, header1b, + len2, header2, + len3, header3, + len4, header4); + + rprct = 0; + for (int i = 0; i < NUM_DIRECT; i++) + { + if (direct_histogram[i]) + { + double prct = (double) direct_histogram[i] * 100 / loop_count; + bool print_it = !stopped; + + rprct += prct; + + /* if largest diff is < NUM_DIRECT, be sure we print it */ + if (i == largest_diff) + { + if (stopped) + printf("...\n"); + print_it = true; + } + + if (print_it) + printf("%*d %*.4f %*.4f %*lld\n", + len1, i, + len2, prct, + len3, rprct, + len4, direct_histogram[i]); + if (rprct >= max_rprct) + stopped = true; + } + } + + /* print largest diff when it's outside the array range */ + if (largest_diff >= NUM_DIRECT) + { + double prct = (double) largest_diff_count * 100 / loop_count; + + printf("...\n"); + printf("%*d %*.4f %*.4f %*lld\n", + len1, largest_diff, + len2, prct, + len3, 100.0, + len4, largest_diff_count); + } } diff --git a/src/bin/pg_test_timing/po/meson.build b/src/bin/pg_test_timing/po/meson.build index 1de9f09083718..ec33106db87c8 100644 --- a/src/bin/pg_test_timing/po/meson.build +++ b/src/bin/pg_test_timing/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_test_timing-' + pg_version_major.to_string())] diff --git a/src/bin/pg_test_timing/t/001_basic.pl b/src/bin/pg_test_timing/t/001_basic.pl index 6554cd981af8f..430d1ed817f16 100644 --- a/src/bin/pg_test_timing/t/001_basic.pl +++ b/src/bin/pg_test_timing/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -25,5 +25,31 @@ [ 'pg_test_timing', '--duration' => '0' ], qr/\Qpg_test_timing: --duration must be in range 1..4294967295\E/, 'pg_test_timing: --duration must be in range'); +command_fails_like( + [ 'pg_test_timing', '--cutoff' => '101' ], + qr/\Qpg_test_timing: --cutoff must be in range 0..100\E/, + 'pg_test_timing: --cutoff must be in range'); + +######################################### +# We obviously can't check for specific output, but we can +# do a simple run and make sure it produces something. +# Also, note the output in the log for data collection purposes. + +my $cmd = [ 'pg_test_timing', '--duration' => '1' ]; +my ($stdout, $stderr); +print("# Running: " . join(" ", @{$cmd}) . "\n"); +my $result = IPC::Run::run $cmd, '>' => \$stdout, '2>' => \$stderr; +is($result, 1, "pg_test_timing: exit code 0"); +is($stderr, '', "pg_test_timing: no stderr"); +like( + $stdout, + qr/ +\QTesting timing overhead for 1 second.\E.* +\QHistogram of timing durations:\E.* +\QObserved timing durations up to 99.9900%:\E +/sx, + 'pg_test_timing: stdout passes sanity check'); + +note "pg_test_timing results:\n$stdout\n"; done_testing(); diff --git a/src/bin/pg_upgrade/Makefile b/src/bin/pg_upgrade/Makefile index f83d2b5d30955..771addb675a6d 100644 --- a/src/bin/pg_upgrade/Makefile +++ b/src/bin/pg_upgrade/Makefile @@ -3,8 +3,7 @@ PGFILEDESC = "pg_upgrade - an in-place binary upgrade utility" PGAPPICON = win32 -# required for 003_upgrade_logical_replication_slots.pl -EXTRA_INSTALL=contrib/test_decoding +EXTRA_INSTALL=contrib/test_decoding src/test/modules/dummy_seclabel src/test/modules/test_extensions subdir = src/bin/pg_upgrade top_builddir = ../../.. @@ -19,11 +18,14 @@ OBJS = \ file.o \ function.o \ info.o \ + multixact_read_v18.o \ + multixact_rewrite.o \ option.o \ parallel.o \ pg_upgrade.o \ relfilenumber.o \ server.o \ + slru_io.o \ tablespace.o \ task.o \ util.o \ @@ -36,6 +38,10 @@ LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX) export REGRESS_SHLIB +# required for 008_extension_control_path.pl +TEST_EXT_LIB=$(abs_top_builddir)/src/test/modules/test_extensions/test_ext$(DLSUFFIX) +export TEST_EXT_LIB + all: pg_upgrade pg_upgrade: $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c index 940fc77fc2e8c..5a7afe62eab2d 100644 --- a/src/bin/pg_upgrade/check.c +++ b/src/bin/pg_upgrade/check.c @@ -3,34 +3,40 @@ * * server checks and output routines * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/check.c */ #include "postgres_fe.h" +#include "catalog/pg_am_d.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_class_d.h" #include "fe_utils/string_utils.h" +#include "mb/pg_wchar.h" #include "pg_upgrade.h" #include "common/unicode_version.h" static void check_new_cluster_is_empty(void); static void check_is_install_user(ClusterInfo *cluster); +static void check_for_unsupported_encodings(ClusterInfo *cluster); static void check_for_connection_status(ClusterInfo *cluster); static void check_for_prepared_transactions(ClusterInfo *cluster); static void check_for_isn_and_int8_passing_mismatch(ClusterInfo *cluster); static void check_for_user_defined_postfix_ops(ClusterInfo *cluster); static void check_for_incompatible_polymorphics(ClusterInfo *cluster); static void check_for_tables_with_oids(ClusterInfo *cluster); +static void check_for_not_null_inheritance(ClusterInfo *cluster); +static void check_for_gist_inet_ops(ClusterInfo *cluster); static void check_for_pg_role_prefix(ClusterInfo *cluster); static void check_for_new_tablespace_dir(void); static void check_for_user_defined_encoding_conversions(ClusterInfo *cluster); static void check_for_unicode_update(ClusterInfo *cluster); -static void check_new_cluster_logical_replication_slots(void); +static void check_new_cluster_replication_slots(void); static void check_new_cluster_subscription_configuration(void); static void check_old_cluster_for_valid_slots(void); static void check_old_cluster_subscription_state(void); +static void check_old_cluster_global_names(ClusterInfo *cluster); /* * DataTypesUsageChecks - definitions of data type checks for the old cluster @@ -168,6 +174,7 @@ static DataTypesUsageChecks data_types_usage_checks[] = /* pg_class.oid is preserved, so 'regclass' is OK */ " 'regcollation', " " 'regconfig', " + /* pg_database.oid is preserved, so 'regdatabase' is OK */ " 'regdictionary', " " 'regnamespace', " " 'regoper', " @@ -396,8 +403,6 @@ process_data_type_check(DbInfo *dbinfo, PGresult *res, void *arg) int i_attname = PQfnumber(res, "attname"); FILE *script = NULL; - AssertVariableIsOfType(&process_data_type_check, UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -419,7 +424,7 @@ process_data_type_check(DbInfo *dbinfo, PGresult *res, void *arg) if (!state->result) { pg_log(PG_REPORT, "failed check: %s", _(state->check->status)); - appendPQExpBuffer(*state->report, "\n%s\n%s %s\n", + appendPQExpBuffer(*state->report, "\n%s\n%s\n %s\n", _(state->check->report_text), _("A list of the problem columns is in the file:"), output_path); @@ -479,8 +484,8 @@ check_for_data_types_usage(ClusterInfo *cluster) } /* Allocate memory for queries and for task states */ - queries = pg_malloc0(sizeof(char *) * n_data_types_usage_checks); - states = pg_malloc0(sizeof(struct data_type_check_state) * n_data_types_usage_checks); + queries = pg_malloc0_array(char *, n_data_types_usage_checks); + states = pg_malloc0_array(struct data_type_check_state, n_data_types_usage_checks); for (int i = 0; i < n_data_types_usage_checks; i++) { @@ -598,6 +603,19 @@ check_and_dump_old_cluster(void) */ check_for_connection_status(&old_cluster); + /* + * Check for encodings that are no longer supported. + */ + check_for_unsupported_encodings(&old_cluster); + + /* + * Validate database, user, role and tablespace names from the old + * cluster. No need to check in 19 or newer as newline and carriage return + * are not allowed at the creation time of the object. + */ + if (GET_MAJOR_VERSION(old_cluster.major_version) < 1900) + check_old_cluster_global_names(&old_cluster); + /* * Extract a list of databases, tables, and logical replication slots from * the old cluster. @@ -620,7 +638,7 @@ check_and_dump_old_cluster(void) { /* * Logical replication slots can be migrated since PG17. See comments - * atop get_old_cluster_logical_slot_infos(). + * in get_db_rel_and_slot_infos(). */ check_old_cluster_for_valid_slots(); @@ -629,7 +647,7 @@ check_and_dump_old_cluster(void) * Before that the logical slots are not upgraded, so we will not be * able to upgrade the logical replication clusters completely. */ - get_subscription_count(&old_cluster); + get_subscription_info(&old_cluster); check_old_cluster_subscription_state(); } @@ -671,6 +689,29 @@ check_and_dump_old_cluster(void) if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1100) check_for_tables_with_oids(&old_cluster); + /* + * Pre-PG 18 allowed child tables to omit not-null constraints that their + * parents columns have, but schema restore fails for them. Verify there + * are none, iff applicable. + */ + if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1800) + check_for_not_null_inheritance(&old_cluster); + + /* + * The btree_gist extension contains gist_inet_ops and gist_cidr_ops + * opclasses that do not reliably give correct answers. We want to + * deprecate and eventually remove those, and as a first step v19 marks + * them not-opcdefault and instead marks the replacement in-core opclass + * "inet_ops" as opcdefault. That creates a problem for pg_upgrade: in + * versions where those opclasses were marked opcdefault, pg_dump will + * dump indexes using them with no explicit opclass specification, so that + * restore would create them using the inet_ops opclass. That would be + * incompatible with what's actually in the on-disk files. So refuse to + * upgrade if there are any such indexes. + */ + if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1800) + check_for_gist_inet_ops(&old_cluster); + /* * Pre-PG 10 allowed tables with 'unknown' type columns and non WAL logged * hash indexes @@ -754,7 +795,7 @@ check_new_cluster(void) check_for_new_tablespace_dir(); - check_new_cluster_logical_replication_slots(); + check_new_cluster_replication_slots(); check_new_cluster_subscription_configuration(); } @@ -885,7 +926,7 @@ check_cluster_versions(void) */ if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1800 && user_opts.char_signedness != -1) - pg_fatal("%s option cannot be used to upgrade from PostgreSQL %s and later.", + pg_fatal("The option %s cannot be used for upgrades from PostgreSQL %s and later.", "--set-char-signedness", "18"); check_ok(); @@ -946,12 +987,12 @@ check_for_new_tablespace_dir(void) prep_status("Checking for new cluster tablespace directories"); - for (tblnum = 0; tblnum < os_info.num_old_tablespaces; tblnum++) + for (tblnum = 0; tblnum < new_cluster.num_tablespaces; tblnum++) { struct stat statbuf; snprintf(new_tablespace_dir, MAXPGPATH, "%s%s", - os_info.old_tablespaces[tblnum], + new_cluster.tablespaces[tblnum], new_cluster.tablespace_suffix); if (stat(new_tablespace_dir, &statbuf) == 0 || errno != ENOENT) @@ -1003,17 +1044,17 @@ create_script_for_old_cluster_deletion(char **deletion_script_file_name) * directory. We can't create a proper old cluster delete script in that * case. */ - for (tblnum = 0; tblnum < os_info.num_old_tablespaces; tblnum++) + for (tblnum = 0; tblnum < new_cluster.num_tablespaces; tblnum++) { - char old_tablespace_dir[MAXPGPATH]; + char new_tablespace_dir[MAXPGPATH]; - strlcpy(old_tablespace_dir, os_info.old_tablespaces[tblnum], MAXPGPATH); - canonicalize_path(old_tablespace_dir); - if (path_is_prefix_of_path(old_cluster_pgdata, old_tablespace_dir)) + strlcpy(new_tablespace_dir, new_cluster.tablespaces[tblnum], MAXPGPATH); + canonicalize_path(new_tablespace_dir); + if (path_is_prefix_of_path(old_cluster_pgdata, new_tablespace_dir)) { /* reproduce warning from CREATE TABLESPACE that is in the log */ pg_log(PG_WARNING, - "\nWARNING: user-defined tablespace locations should not be inside the data directory, i.e. %s", old_tablespace_dir); + "\nWARNING: user-defined tablespace locations should not be inside the data directory, i.e. %s", new_tablespace_dir); /* Unlink file in case it is left over from a previous run. */ unlink(*deletion_script_file_name); @@ -1041,9 +1082,9 @@ create_script_for_old_cluster_deletion(char **deletion_script_file_name) /* delete old cluster's alternate tablespaces */ old_tblspc_suffix = pg_strdup(old_cluster.tablespace_suffix); fix_path_separator(old_tblspc_suffix); - for (tblnum = 0; tblnum < os_info.num_old_tablespaces; tblnum++) + for (tblnum = 0; tblnum < old_cluster.num_tablespaces; tblnum++) fprintf(script, RMDIR_CMD " %c%s%s%c\n", PATH_QUOTE, - fix_path_separator(os_info.old_tablespaces[tblnum]), + fix_path_separator(old_cluster.tablespaces[tblnum]), old_tblspc_suffix, PATH_QUOTE); pfree(old_tblspc_suffix); @@ -1205,6 +1246,64 @@ check_for_connection_status(ClusterInfo *cluster) } +/* + * check_for_unsupported_encodings() + */ +static void +check_for_unsupported_encodings(ClusterInfo *cluster) +{ + int i_datname; + int i_encoding; + int ntups; + PGresult *res; + PGconn *conn; + FILE *script = NULL; + char output_path[MAXPGPATH]; + + prep_status("Checking for unsupported encodings"); + + snprintf(output_path, sizeof(output_path), "%s/%s", + log_opts.basedir, + "databases_unsupported_encoding.txt"); + + conn = connectToServer(cluster, "template1"); + + res = executeQueryOrDie(conn, + "SELECT datname, encoding " + "FROM pg_catalog.pg_database"); + ntups = PQntuples(res); + i_datname = PQfnumber(res, "datname"); + i_encoding = PQfnumber(res, "encoding"); + for (int rowno = 0; rowno < ntups; rowno++) + { + char *datname = PQgetvalue(res, rowno, i_datname); + int encoding = atoi(PQgetvalue(res, rowno, i_encoding)); + + if (!PG_VALID_BE_ENCODING(encoding)) + { + if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", output_path); + + fprintf(script, "%s\n", datname); + } + } + PQclear(res); + PQfinish(conn); + + if (script) + { + fclose(script); + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains databases using encodings that are\n" + "no longer supported. Consider dumping and restoring with UTF8.\n" + "A list of databases with unsupported encodings is in the file:\n" + " %s", output_path); + } + else + check_ok(); +} + + /* * check_for_prepared_transactions() * @@ -1252,9 +1351,6 @@ process_isn_and_int8_passing_mismatch(DbInfo *dbinfo, PGresult *res, void *arg) int i_proname = PQfnumber(res, "proname"); UpgradeTaskReport *report = (UpgradeTaskReport *) arg; - AssertVariableIsOfType(&process_isn_and_int8_passing_mismatch, - UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -1341,9 +1437,6 @@ process_user_defined_postfix_ops(DbInfo *dbinfo, PGresult *res, void *arg) int i_typnsp = PQfnumber(res, "typnsp"); int i_typname = PQfnumber(res, "typname"); - AssertVariableIsOfType(&process_user_defined_postfix_ops, - UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -1432,9 +1525,6 @@ process_incompat_polymorphics(DbInfo *dbinfo, PGresult *res, void *arg) int i_objkind = PQfnumber(res, "objkind"); int i_objname = PQfnumber(res, "objname"); - AssertVariableIsOfType(&process_incompat_polymorphics, - UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -1565,8 +1655,6 @@ process_with_oids_check(DbInfo *dbinfo, PGresult *res, void *arg) int i_nspname = PQfnumber(res, "nspname"); int i_relname = PQfnumber(res, "relname"); - AssertVariableIsOfType(&process_with_oids_check, UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -1623,6 +1711,164 @@ check_for_tables_with_oids(ClusterInfo *cluster) check_ok(); } +/* + * Callback function for processing results of query for + * check_for_not_null_inheritance. + */ +static void +process_inconsistent_notnull(DbInfo *dbinfo, PGresult *res, void *arg) +{ + UpgradeTaskReport *report = (UpgradeTaskReport *) arg; + int ntups = PQntuples(res); + int i_nspname = PQfnumber(res, "nspname"); + int i_relname = PQfnumber(res, "relname"); + int i_attname = PQfnumber(res, "attname"); + + if (ntups == 0) + return; + + if (report->file == NULL && + (report->file = fopen_priv(report->path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", report->path); + + fprintf(report->file, "In database: %s\n", dbinfo->db_name); + + for (int rowno = 0; rowno < ntups; rowno++) + { + fprintf(report->file, " %s.%s.%s\n", + PQgetvalue(res, rowno, i_nspname), + PQgetvalue(res, rowno, i_relname), + PQgetvalue(res, rowno, i_attname)); + } +} + +/* + * check_for_not_null_inheritance() + * + * An attempt to create child tables lacking not-null constraints that are + * present in their parents errors out. This can no longer occur since 18, + * but previously there were various ways for that to happen. Check that + * the cluster to be upgraded doesn't have any of those problems. + */ +static void +check_for_not_null_inheritance(ClusterInfo *cluster) +{ + UpgradeTaskReport report; + UpgradeTask *task; + const char *query; + + prep_status("Checking for not-null constraint inconsistencies"); + + report.file = NULL; + snprintf(report.path, sizeof(report.path), "%s/%s", + log_opts.basedir, + "not_null_inconsistent_columns.txt"); + + query = "SELECT nspname, cc.relname, ac.attname " + "FROM pg_catalog.pg_inherits i, pg_catalog.pg_attribute ac, " + " pg_catalog.pg_attribute ap, pg_catalog.pg_class cc, " + " pg_catalog.pg_namespace nc " + "WHERE cc.oid = ac.attrelid AND i.inhrelid = ac.attrelid " + " AND i.inhparent = ap.attrelid AND ac.attname = ap.attname " + " AND cc.relnamespace = nc.oid " + " AND ap.attnum > 0 and ap.attnotnull AND NOT ac.attnotnull"; + + task = upgrade_task_create(); + upgrade_task_add_step(task, query, + process_inconsistent_notnull, + true, &report); + upgrade_task_run(task, cluster); + upgrade_task_free(task); + + if (report.file) + { + fclose(report.file); + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains inconsistent NOT NULL constraints.\n" + "If the parent column(s) are NOT NULL, then the child column must\n" + "also be marked NOT NULL, or the upgrade will fail.\n" + "You can fix this by running\n" + " ALTER TABLE tablename ALTER column SET NOT NULL;\n" + "on each column listed in the file:\n" + " %s", report.path); + } + else + check_ok(); +} + +/* + * Callback function for processing results of query for + * check_for_gist_inet_ops()'s UpgradeTask. If the query returned any rows + * (i.e., the check failed), write the details to the report file. + */ +static void +process_gist_inet_ops_check(DbInfo *dbinfo, PGresult *res, void *arg) +{ + UpgradeTaskReport *report = (UpgradeTaskReport *) arg; + int ntups = PQntuples(res); + int i_nspname = PQfnumber(res, "nspname"); + int i_relname = PQfnumber(res, "relname"); + + if (ntups == 0) + return; + + if (report->file == NULL && + (report->file = fopen_priv(report->path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", report->path); + + fprintf(report->file, "In database: %s\n", dbinfo->db_name); + + for (int rowno = 0; rowno < ntups; rowno++) + fprintf(report->file, " %s.%s\n", + PQgetvalue(res, rowno, i_nspname), + PQgetvalue(res, rowno, i_relname)); +} + +/* + * Verify that no indexes use gist_inet_ops/gist_cidr_ops, unless the + * opclasses have been changed to not-opcdefault (which would allow + * the old server to dump the index definitions with explicit opclasses). + */ +static void +check_for_gist_inet_ops(ClusterInfo *cluster) +{ + UpgradeTaskReport report; + UpgradeTask *task = upgrade_task_create(); + const char *query = "SELECT nc.nspname, cc.relname " + "FROM pg_catalog.pg_opclass oc, pg_catalog.pg_index i, " + " pg_catalog.pg_class cc, pg_catalog.pg_namespace nc " + "WHERE oc.opcmethod = " CppAsString2(GIST_AM_OID) + " AND oc.opcname IN ('gist_inet_ops', 'gist_cidr_ops')" + " AND oc.opcdefault" + " AND oc.oid = any(i.indclass)" + " AND i.indexrelid = cc.oid AND cc.relnamespace = nc.oid"; + + prep_status("Checking for uses of gist_inet_ops/gist_cidr_ops"); + + report.file = NULL; + snprintf(report.path, sizeof(report.path), "%s/%s", + log_opts.basedir, + "gist_inet_ops.txt"); + + upgrade_task_add_step(task, query, process_gist_inet_ops_check, + true, &report); + upgrade_task_run(task, cluster); + upgrade_task_free(task); + + if (report.file) + { + fclose(report.file); + pg_log(PG_REPORT, "fatal"); + pg_fatal("Your installation contains indexes that use btree_gist's\n" + "gist_inet_ops or gist_cidr_ops opclasses,\n" + "which cannot be binary-upgraded. Replace them with indexes\n" + "that use the built-in GiST inet_ops opclass.\n" + "A list of indexes with the problem is in the file:\n" + " %s", report.path); + } + else + check_ok(); +} /* * check_for_pg_role_prefix() @@ -1696,9 +1942,6 @@ process_user_defined_encoding_conversions(DbInfo *dbinfo, PGresult *res, void *a int i_conname = PQfnumber(res, "conname"); int i_nspname = PQfnumber(res, "nspname"); - AssertVariableIsOfType(&process_user_defined_encoding_conversions, - UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -1874,14 +2117,19 @@ check_for_unicode_update(ClusterInfo *cluster) " SELECT oper.oid, oper.oprcode, collid FROM pg_operator oper, collations " " WHERE oprname IN ('~', '~*', '!~', '!~*', '~~*', '!~~*') AND " " oprnamespace='pg_catalog'::regnamespace AND " - " oprright='text'::regtype " + " oprright='pg_catalog.text'::pg_catalog.regtype " "), " /* functions that use the input collation for character semantics */ "coll_functions(procid, collid) AS ( " " SELECT proc.oid, collid FROM pg_proc proc, collations " - " WHERE proname IN ('lower','initcap','upper') AND " - " pronamespace='pg_catalog'::regnamespace AND " - " proargtypes[0] = 'text'::regtype " + " WHERE pronamespace='pg_catalog'::regnamespace AND " + " ((proname IN ('lower','initcap','upper','casefold') AND " + " pronargs = 1 AND " + " proargtypes[0] = 'pg_catalog.text'::pg_catalog.regtype) OR " + " (proname = 'substring' AND pronargs = 2 AND " + " proargtypes[0] = 'pg_catalog.text'::pg_catalog.regtype AND " + " proargtypes[1] = 'pg_catalog.text'::pg_catalog.regtype) OR " + " proname LIKE 'regexp_%') " /* include functions behind the operators listed above */ " UNION " " SELECT procid, collid FROM coll_operators " @@ -1934,7 +2182,7 @@ check_for_unicode_update(ClusterInfo *cluster) { fclose(report.file); report_status(PG_WARNING, "warning"); - pg_log(PG_WARNING, "Your installation contains relations that may be affected by a new version of Unicode.\n" + pg_log(PG_WARNING, "Your installation contains relations that might be affected by a new version of Unicode.\n" "A list of potentially-affected relations is in the file:\n" " %s", report.path); } @@ -1943,48 +2191,80 @@ check_for_unicode_update(ClusterInfo *cluster) } /* - * check_new_cluster_logical_replication_slots() + * check_new_cluster_replication_slots() * - * Verify that there are no logical replication slots on the new cluster and - * that the parameter settings necessary for creating slots are sufficient. + * Validate the new cluster's readiness for migrating replication slots: + * - Ensures no existing logical replication slots on the new cluster when + * migrating logical slots. + * - Ensure conflict detection slot does not exist on the new cluster when + * migrating subscriptions with retain_dead_tuples enabled. + * - Ensure that the parameter settings on the new cluster necessary for + * creating slots are sufficient. */ static void -check_new_cluster_logical_replication_slots(void) +check_new_cluster_replication_slots(void) { PGresult *res; PGconn *conn; int nslots_on_old; int nslots_on_new; + int rdt_slot_on_new; int max_replication_slots; char *wal_level; + int i_nslots_on_new; + int i_rdt_slot_on_new; - /* Logical slots can be migrated since PG17. */ + /* + * Logical slots can be migrated since PG17 and a physical slot + * CONFLICT_DETECTION_SLOT can be migrated since PG19. + */ if (GET_MAJOR_VERSION(old_cluster.major_version) <= 1600) return; nslots_on_old = count_old_cluster_logical_slots(); - /* Quick return if there are no logical slots to be migrated. */ - if (nslots_on_old == 0) + /* + * Quick return if there are no slots to be migrated and no subscriptions + * have the retain_dead_tuples option enabled. + */ + if (nslots_on_old == 0 && !old_cluster.sub_retain_dead_tuples) return; conn = connectToServer(&new_cluster, "template1"); - prep_status("Checking for new cluster logical replication slots"); + prep_status("Checking for new cluster replication slots"); - res = executeQueryOrDie(conn, "SELECT count(*) " - "FROM pg_catalog.pg_replication_slots " - "WHERE slot_type = 'logical' AND " - "temporary IS FALSE;"); + res = executeQueryOrDie(conn, "SELECT %s AS nslots_on_new, %s AS rdt_slot_on_new " + "FROM pg_catalog.pg_replication_slots", + nslots_on_old > 0 + ? "COUNT(*) FILTER (WHERE slot_type = 'logical' AND temporary IS FALSE)" + : "0", + old_cluster.sub_retain_dead_tuples + ? "COUNT(*) FILTER (WHERE slot_name = 'pg_conflict_detection')" + : "0"); if (PQntuples(res) != 1) - pg_fatal("could not count the number of logical replication slots"); + pg_fatal("could not count the number of replication slots"); - nslots_on_new = atoi(PQgetvalue(res, 0, 0)); + i_nslots_on_new = PQfnumber(res, "nslots_on_new"); + i_rdt_slot_on_new = PQfnumber(res, "rdt_slot_on_new"); + + nslots_on_new = atoi(PQgetvalue(res, 0, i_nslots_on_new)); if (nslots_on_new) + { + Assert(nslots_on_old); pg_fatal("expected 0 logical replication slots but found %d", nslots_on_new); + } + + rdt_slot_on_new = atoi(PQgetvalue(res, 0, i_rdt_slot_on_new)); + + if (rdt_slot_on_new) + { + Assert(old_cluster.sub_retain_dead_tuples); + pg_fatal("The replication slot \"pg_conflict_detection\" already exists on the new cluster"); + } PQclear(res); @@ -1997,12 +2277,20 @@ check_new_cluster_logical_replication_slots(void) wal_level = PQgetvalue(res, 0, 0); - if (strcmp(wal_level, "logical") != 0) - pg_fatal("\"wal_level\" must be \"logical\" but is set to \"%s\"", + if ((nslots_on_old > 0 || old_cluster.sub_retain_dead_tuples) && + strcmp(wal_level, "minimal") == 0) + pg_fatal("\"wal_level\" must be \"replica\" or \"logical\" but is set to \"%s\"", wal_level); max_replication_slots = atoi(PQgetvalue(res, 1, 0)); + if (old_cluster.sub_retain_dead_tuples && + nslots_on_old + 1 > max_replication_slots) + pg_fatal("\"max_replication_slots\" (%d) must be greater than or equal to the number of " + "logical replication slots on the old cluster plus one additional slot required " + "for retaining conflict detection information (%d)", + max_replication_slots, nslots_on_old + 1); + if (nslots_on_old > max_replication_slots) pg_fatal("\"max_replication_slots\" (%d) must be greater than or equal to the number of " "logical replication slots (%d) on the old cluster", @@ -2114,6 +2402,22 @@ check_old_cluster_for_valid_slots(void) "The slot \"%s\" has not consumed the WAL yet\n", slot->slotname); } + + /* + * The name "pg_conflict_detection" (defined as + * CONFLICT_DETECTION_SLOT) has been reserved for logical + * replication conflict detection slot since PG19. + */ + if (strcmp(slot->slotname, "pg_conflict_detection") == 0) + { + if (script == NULL && + (script = fopen_priv(output_path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", output_path); + + fprintf(script, + "The slot name \"%s\" is reserved\n", + slot->slotname); + } } } @@ -2147,8 +2451,6 @@ process_old_sub_state_check(DbInfo *dbinfo, PGresult *res, void *arg) int i_nspname = PQfnumber(res, "nspname"); int i_relname = PQfnumber(res, "relname"); - AssertVariableIsOfType(&process_old_sub_state_check, UpgradeTaskProcessCB); - if (ntups == 0) return; @@ -2225,9 +2527,9 @@ check_old_cluster_subscription_state(void) * states listed below are not supported: * * a) SUBREL_STATE_DATASYNC: A relation upgraded while in this state would - * retain a replication slot, which could not be dropped by the sync - * worker spawned after the upgrade because the subscription ID used for - * the slot name won't match anymore. + * retain a replication slot and origin. The sync worker spawned after the + * upgrade cannot drop them because the subscription ID used for the slot + * and origin name no longer matches. * * b) SUBREL_STATE_SYNCDONE: A relation upgraded while in this state would * retain the replication origin when there is a failure in tablesync @@ -2272,3 +2574,73 @@ check_old_cluster_subscription_state(void) else check_ok(); } + +/* + * check_old_cluster_global_names() + * + * Raise an error if any database, role, or tablespace name contains a newline + * or carriage return character. Such names are not allowed in v19 and later. + */ +static void +check_old_cluster_global_names(ClusterInfo *cluster) +{ + int i; + PGconn *conn_template1; + PGresult *res; + int ntups; + FILE *script = NULL; + char output_path[MAXPGPATH]; + int count = 0; + + prep_status("Checking names of databases, roles and tablespaces"); + + snprintf(output_path, sizeof(output_path), "%s/%s", + log_opts.basedir, + "db_role_tablespace_invalid_names.txt"); + + conn_template1 = connectToServer(cluster, "template1"); + + /* + * Get database, user/role and tablespace names from cluster. Can't use + * pg_authid because only superusers can view it. + */ + res = executeQueryOrDie(conn_template1, + "SELECT datname AS objname, 'database' AS objtype " + "FROM pg_catalog.pg_database UNION ALL " + "SELECT rolname AS objname, 'role' AS objtype " + "FROM pg_catalog.pg_roles UNION ALL " + "SELECT spcname AS objname, 'tablespace' AS objtype " + "FROM pg_catalog.pg_tablespace ORDER BY 2 "); + + ntups = PQntuples(res); + for (i = 0; i < ntups; i++) + { + char *objname = PQgetvalue(res, i, 0); + char *objtype = PQgetvalue(res, i, 1); + + /* If name has \n or \r, then report it. */ + if (strpbrk(objname, "\n\r")) + { + if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL) + pg_fatal("could not open file \"%s\": %m", output_path); + + fprintf(script, "%d : %s name = \"%s\"\n", ++count, objtype, objname); + } + } + + PQclear(res); + PQfinish(conn_template1); + + if (script) + { + fclose(script); + pg_log(PG_REPORT, "fatal"); + pg_fatal("All the database, role and tablespace names should have only valid characters. A newline or \n" + "carriage return character is not allowed in these object names. To fix this, please \n" + "rename these names with valid names. \n" + "To see all %d invalid object names, refer db_role_tablespace_invalid_names.txt file. \n" + " %s", count, output_path); + } + else + check_ok(); +} diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c index 90cef0864de7c..cffcd4b0ebabe 100644 --- a/src/bin/pg_upgrade/controldata.c +++ b/src/bin/pg_upgrade/controldata.c @@ -3,7 +3,7 @@ * * controldata functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/controldata.c */ @@ -15,6 +15,7 @@ #include "access/xlog_internal.h" #include "common/string.h" #include "pg_upgrade.h" +#include "storage/checksum.h" /* @@ -205,7 +206,7 @@ get_control_data(ClusterInfo *cluster) /* Only in <= 9.2 */ if (GET_MAJOR_VERSION(cluster->major_version) <= 902) { - cluster->controldata.data_checksum_version = 0; + cluster->controldata.data_checksum_version = PG_DATA_CHECKSUM_OFF; got_data_checksum_version = true; } @@ -360,7 +361,7 @@ get_control_data(ClusterInfo *cluster) p = strchr(p, ':'); if (p == NULL || strlen(p) <= 1) pg_fatal("%d: controldata retrieval problem", __LINE__); - p = strpbrk(p, "01234567890ABCDEF"); + p = strpbrk(p, "0123456789ABCDEF"); if (p == NULL || strlen(p) <= 1) pg_fatal("%d: controldata retrieval problem", __LINE__); @@ -736,15 +737,23 @@ check_control_data(ControlData *oldctrl, * check_for_isn_and_int8_passing_mismatch(). */ + /* + * If data checksums are in any in-progress state then disallow the + * upgrade. The user should either let the process finish, or turn off + * data checksums, before retrying. + */ + if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION) + pg_fatal("checksums are being enabled in the old cluster"); + /* * We might eventually allow upgrades from checksum to no-checksum * clusters. */ - if (oldctrl->data_checksum_version == 0 && - newctrl->data_checksum_version != 0) + if (oldctrl->data_checksum_version == PG_DATA_CHECKSUM_OFF && + newctrl->data_checksum_version != PG_DATA_CHECKSUM_OFF) pg_fatal("old cluster does not use data checksums but the new one does"); - else if (oldctrl->data_checksum_version != 0 && - newctrl->data_checksum_version == 0) + else if (oldctrl->data_checksum_version != PG_DATA_CHECKSUM_OFF && + newctrl->data_checksum_version == PG_DATA_CHECKSUM_OFF) pg_fatal("old cluster uses data checksums but the new one does not"); else if (oldctrl->data_checksum_version != newctrl->data_checksum_version) pg_fatal("old and new cluster pg_controldata checksum versions do not match"); diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c index 23cb08e83476e..f47c8d06211f9 100644 --- a/src/bin/pg_upgrade/dump.c +++ b/src/bin/pg_upgrade/dump.c @@ -3,7 +3,7 @@ * * dump functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/dump.c */ @@ -21,9 +21,10 @@ generate_old_dump(void) /* run new pg_dumpall binary for globals */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, - "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers " + "\"%s/pg_dumpall\" %s%s --globals-only --quote-all-identifiers " "--binary-upgrade %s --no-sync -f \"%s/%s\"", new_cluster.bindir, cluster_conn_opts(&old_cluster), + protocol_negotiation_supported(&old_cluster) ? "" : " -d \"max_protocol_version=3.0\"", log_opts.verbose ? "--verbose" : "", log_opts.dumpdir, GLOBALS_DUMP_FILE); @@ -43,6 +44,9 @@ generate_old_dump(void) initPQExpBuffer(&connstr); appendPQExpBufferStr(&connstr, "dbname="); appendConnStrVal(&connstr, old_db->db_name); + if (!protocol_negotiation_supported(&old_cluster)) + appendPQExpBufferStr(&connstr, " max_protocol_version=3.0"); + initPQExpBuffer(&escaped_connstr); appendShellString(&escaped_connstr, connstr.data); termPQExpBuffer(&connstr); @@ -58,7 +62,7 @@ generate_old_dump(void) (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ? "" : "--sequence-data", log_opts.verbose ? "--verbose" : "", - user_opts.do_statistics ? "" : "--no-statistics", + user_opts.do_statistics ? "--statistics" : "--no-statistics", log_opts.dumpdir, sql_file_name, escaped_connstr.data); diff --git a/src/bin/pg_upgrade/exec.c b/src/bin/pg_upgrade/exec.c index 63f2815a7cd10..e1de61f36eee1 100644 --- a/src/bin/pg_upgrade/exec.c +++ b/src/bin/pg_upgrade/exec.c @@ -3,7 +3,7 @@ * * execution functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/exec.c */ @@ -12,6 +12,7 @@ #include #include "common/string.h" +#include "fe_utils/version.h" #include "pg_upgrade.h" static void check_data_dir(ClusterInfo *cluster); @@ -343,8 +344,8 @@ check_data_dir(ClusterInfo *cluster) const char *pg_data = cluster->pgdata; /* get the cluster version */ - cluster->major_version = get_major_server_version(cluster); - + cluster->major_version = get_pg_version(cluster->pgdata, + &cluster->major_version_str); check_single_dir(pg_data, ""); check_single_dir(pg_data, "base"); check_single_dir(pg_data, "global"); diff --git a/src/bin/pg_upgrade/file.c b/src/bin/pg_upgrade/file.c index 91ed16acb088f..5b2760086146d 100644 --- a/src/bin/pg_upgrade/file.c +++ b/src/bin/pg_upgrade/file.c @@ -3,7 +3,7 @@ * * file system operations * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/file.c */ @@ -331,7 +331,7 @@ rewriteVisibilityMap(const char *fromfile, const char *tofile, break; /* Set new checksum for visibility map page, if enabled */ - if (new_cluster.controldata.data_checksum_version != 0) + if (new_cluster.controldata.data_checksum_version != PG_DATA_CHECKSUM_OFF) ((PageHeader) new_vmbuf.data)->pd_checksum = pg_checksum_page(new_vmbuf.data, new_blkno); diff --git a/src/bin/pg_upgrade/function.c b/src/bin/pg_upgrade/function.c index 9ad53caeebc9d..bc7e8006d825e 100644 --- a/src/bin/pg_upgrade/function.c +++ b/src/bin/pg_upgrade/function.c @@ -3,7 +3,7 @@ * * server-side function support * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/function.c */ @@ -61,8 +61,6 @@ process_loadable_libraries(DbInfo *dbinfo, PGresult *res, void *arg) { struct loadable_libraries_state *state = (struct loadable_libraries_state *) arg; - AssertVariableIsOfType(&process_loadable_libraries, UpgradeTaskProcessCB); - state->ress[dbinfo - old_cluster.dbarr.dbs] = res; state->totaltups += PQntuples(res); } @@ -85,7 +83,7 @@ get_loadable_libraries(void) struct loadable_libraries_state state; char *query; - state.ress = (PGresult **) pg_malloc(old_cluster.dbarr.ndbs * sizeof(PGresult *)); + state.ress = pg_malloc_array(PGresult *, old_cluster.dbarr.ndbs); state.totaltups = 0; query = psprintf("SELECT DISTINCT probin " @@ -107,7 +105,7 @@ get_loadable_libraries(void) * plugins. */ n_libinfos = state.totaltups + count_old_cluster_logical_slots(); - os_info.libraries = (LibraryInfo *) pg_malloc(sizeof(LibraryInfo) * n_libinfos); + os_info.libraries = pg_malloc_array(LibraryInfo, n_libinfos); totaltups = 0; for (dbnum = 0; dbnum < old_cluster.dbarr.ndbs; dbnum++) @@ -122,6 +120,14 @@ get_loadable_libraries(void) { char *lib = PQgetvalue(res, rowno, 0); + /* + * Starting with version 19, for extensions with hardcoded + * '$libdir/' library names, we strip the prefix to allow the + * library search path to be used. + */ + if (strncmp(lib, "$libdir/", 8) == 0) + lib += 8; + os_info.libraries[totaltups].name = pg_strdup(lib); os_info.libraries[totaltups].dbnum = dbnum; diff --git a/src/bin/pg_upgrade/info.c b/src/bin/pg_upgrade/info.c index 4b7a56f5b3be4..37fff93892f17 100644 --- a/src/bin/pg_upgrade/info.c +++ b/src/bin/pg_upgrade/info.c @@ -3,7 +3,7 @@ * * information support functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/info.c */ @@ -29,7 +29,7 @@ static void free_rel_infos(RelInfoArr *rel_arr); static void print_db_infos(DbInfoArr *db_arr); static void print_rel_infos(RelInfoArr *rel_arr); static void print_slot_infos(LogicalSlotInfoArr *slot_arr); -static char *get_old_cluster_logical_slot_infos_query(void); +static const char *get_old_cluster_logical_slot_infos_query(ClusterInfo *cluster); static void process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg); @@ -53,8 +53,7 @@ gen_db_file_maps(DbInfo *old_db, DbInfo *new_db, bool all_matched = true; /* There will certainly not be more mappings than there are old rels */ - maps = (FileNameMap *) pg_malloc(sizeof(FileNameMap) * - old_db->rel_arr.nrels); + maps = pg_malloc_array(FileNameMap, old_db->rel_arr.nrels); /* * Each of the RelInfo arrays should be sorted by OID. Scan through them @@ -281,7 +280,6 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster) { UpgradeTask *task = upgrade_task_create(); char *rel_infos_query = NULL; - char *logical_slot_infos_query = NULL; if (cluster->dbarr.dbs != NULL) free_db_and_rel_infos(&cluster->dbarr); @@ -306,20 +304,15 @@ get_db_rel_and_slot_infos(ClusterInfo *cluster) */ if (cluster == &old_cluster && GET_MAJOR_VERSION(cluster->major_version) > 1600) - { - logical_slot_infos_query = get_old_cluster_logical_slot_infos_query(); upgrade_task_add_step(task, - logical_slot_infos_query, + get_old_cluster_logical_slot_infos_query(cluster), process_old_cluster_logical_slot_infos, true, NULL); - } upgrade_task_run(task, cluster); upgrade_task_free(task); pg_free(rel_infos_query); - if (logical_slot_infos_query) - pg_free(logical_slot_infos_query); if (cluster == &old_cluster) pg_log(PG_VERBOSE, "\nsource databases:"); @@ -370,7 +363,7 @@ get_template0_info(ClusterInfo *cluster) if (PQntuples(dbres) != 1) pg_fatal("template0 not found"); - locale = pg_malloc(sizeof(DbLocaleInfo)); + locale = pg_malloc_object(DbLocaleInfo); i_datencoding = PQfnumber(dbres, "encoding"); i_datlocprovider = PQfnumber(dbres, "datlocprovider"); @@ -439,14 +432,30 @@ get_db_infos(ClusterInfo *cluster) i_spclocation = PQfnumber(res, "spclocation"); ntups = PQntuples(res); - dbinfos = (DbInfo *) pg_malloc0(sizeof(DbInfo) * ntups); + dbinfos = pg_malloc0_array(DbInfo, ntups); for (tupnum = 0; tupnum < ntups; tupnum++) { + char *spcloc = PQgetvalue(res, tupnum, i_spclocation); + bool inplace = spcloc[0] && !is_absolute_path(spcloc); + dbinfos[tupnum].db_oid = atooid(PQgetvalue(res, tupnum, i_oid)); dbinfos[tupnum].db_name = pg_strdup(PQgetvalue(res, tupnum, i_datname)); - snprintf(dbinfos[tupnum].db_tablespace, sizeof(dbinfos[tupnum].db_tablespace), "%s", - PQgetvalue(res, tupnum, i_spclocation)); + + /* + * The tablespace location might be "", meaning the cluster default + * location, i.e. pg_default or pg_global. For in-place tablespaces, + * pg_tablespace_location() returns a path relative to the data + * directory. + */ + if (inplace) + snprintf(dbinfos[tupnum].db_tablespace, + sizeof(dbinfos[tupnum].db_tablespace), + "%s/%s", cluster->pgdata, spcloc); + else + snprintf(dbinfos[tupnum].db_tablespace, + sizeof(dbinfos[tupnum].db_tablespace), + "%s", spcloc); } PQclear(res); @@ -482,7 +491,10 @@ get_rel_infos_query(void) * * pg_largeobject contains user data that does not appear in pg_dump * output, so we have to copy that system table. It's easiest to do that - * by treating it as a user table. + * by treating it as a user table. We can do the same for + * pg_largeobject_metadata for upgrades from v16 and newer. pg_upgrade + * can't copy/link the files from older versions because aclitem (needed + * by pg_largeobject_metadata.lomacl) changed its storage format in v16. */ appendPQExpBuffer(&query, "WITH regular_heap (reloid, indtable, toastheap) AS ( " @@ -498,10 +510,12 @@ get_rel_infos_query(void) " 'binary_upgrade', 'pg_toast') AND " " c.oid >= %u::pg_catalog.oid) OR " " (n.nspname = 'pg_catalog' AND " - " relname IN ('pg_largeobject') ))), ", + " relname IN ('pg_largeobject'%s) ))), ", (user_opts.transfer_mode == TRANSFER_MODE_SWAP) ? ", " CppAsString2(RELKIND_SEQUENCE) : "", - FirstNormalObjectId); + FirstNormalObjectId, + (GET_MAJOR_VERSION(old_cluster.major_version) >= 1600) ? + ", 'pg_largeobject_metadata'" : ""); /* * Add a CTE that collects OIDs of toast tables belonging to the tables @@ -564,7 +578,7 @@ static void process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg) { int ntups = PQntuples(res); - RelInfo *relinfos = (RelInfo *) pg_malloc(sizeof(RelInfo) * ntups); + RelInfo *relinfos = pg_malloc_array(RelInfo, ntups); int i_reloid = PQfnumber(res, "reloid"); int i_indtable = PQfnumber(res, "indtable"); int i_toastheap = PQfnumber(res, "toastheap"); @@ -580,8 +594,6 @@ process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg) char *last_namespace = NULL; char *last_tablespace = NULL; - AssertVariableIsOfType(&process_rel_infos, UpgradeTaskProcessCB); - for (int relnum = 0; relnum < ntups; relnum++) { RelInfo *curr = &relinfos[num_rels++]; @@ -616,11 +628,21 @@ process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg) /* Is the tablespace oid non-default? */ if (atooid(PQgetvalue(res, relnum, i_reltablespace)) != 0) { + char *spcloc = PQgetvalue(res, relnum, i_spclocation); + bool inplace = spcloc[0] && !is_absolute_path(spcloc); + /* * The tablespace location might be "", meaning the cluster - * default location, i.e. pg_default or pg_global. + * default location, i.e. pg_default or pg_global. For in-place + * tablespaces, pg_tablespace_location() returns a path relative + * to the data directory. */ - tablespace = PQgetvalue(res, relnum, i_spclocation); + if (inplace) + tablespace = psprintf("%s/%s", + os_info.running_cluster->pgdata, + spcloc); + else + tablespace = spcloc; /* Can we reuse the previous string allocation? */ if (last_tablespace && strcmp(tablespace, last_tablespace) == 0) @@ -630,6 +652,10 @@ process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg) last_tablespace = curr->tablespace = pg_strdup(tablespace); curr->tblsp_alloc = true; } + + /* Free palloc'd string for in-place tablespaces. */ + if (inplace) + pfree(tablespace); } else /* A zero reltablespace oid indicates the database tablespace. */ @@ -648,17 +674,15 @@ process_rel_infos(DbInfo *dbinfo, PGresult *res, void *arg) * get_db_rel_and_slot_infos()'s UpgradeTask. The status of each logical slot * is checked in check_old_cluster_for_valid_slots(). */ -static char * -get_old_cluster_logical_slot_infos_query(void) +static const char * +get_old_cluster_logical_slot_infos_query(ClusterInfo *cluster) { /* * Fetch the logical replication slot information. The check whether the * slot is considered caught up is done by an upgrade function. This * regards the slot as caught up if we don't find any decodable changes. - * See binary_upgrade_logical_slot_has_caught_up(). - * - * Note that we can't ensure whether the slot is caught up during - * live_check as the new WAL records could be generated. + * The implementation of this check varies depending on the server + * version. * * We intentionally skip checking the WALs for invalidated slots as the * corresponding WALs could have been removed for such slots. @@ -668,21 +692,81 @@ get_old_cluster_logical_slot_infos_query(void) * started and stopped several times causing any temporary slots to be * removed. */ - return psprintf("SELECT slot_name, plugin, two_phase, failover, " - "%s as caught_up, invalidation_reason IS NOT NULL as invalid " - "FROM pg_catalog.pg_replication_slots " - "WHERE slot_type = 'logical' AND " - "database = current_database() AND " - "temporary IS FALSE;", - user_opts.live_check ? "FALSE" : - "(CASE WHEN invalidation_reason IS NOT NULL THEN FALSE " - "ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) " - "END)"); + + if (user_opts.live_check) + { + /* + * We skip the caught-up check during live_check. We cannot verify + * whether the slot is caught up in this mode, as new WAL records + * could be generated concurrently. + */ + return "SELECT slot_name, plugin, two_phase, failover, " + "FALSE as caught_up, " + "invalidation_reason IS NOT NULL as invalid " + "FROM pg_catalog.pg_replication_slots " + "WHERE slot_type = 'logical' AND " + "database = current_database() AND " + "temporary IS FALSE"; + } + else if (GET_MAJOR_VERSION(cluster->major_version) >= 1900) + { + /* + * For PG19 and later, we optimize the slot caught-up check to avoid + * reading the same WAL stream multiple times: execute the caught-up + * check only for the slot with the minimum confirmed_flush_lsn, and + * apply the same result to all other slots in the same database. This + * limits the check to at most one logical slot per database. We also + * use the maximum confirmed_flush_lsn among all logical slots on the + * database as an early scan cutoff; finding a decodable WAL record + * beyond this point implies that no slot has caught up. + * + * Note that we don't distinguish slots based on their output plugin. + * If a plugin applies replication origin filters, we might get a + * false positive (i.e., erroneously considering a slot caught up). + * However, such cases are very rare, and the impact of a false + * positive is minimal. + */ + return "WITH check_caught_up AS ( " + " SELECT pg_catalog.binary_upgrade_check_logical_slot_pending_wal(slot_name, " + " MAX(confirmed_flush_lsn) OVER ()) as last_pending_wal " + " FROM pg_replication_slots " + " WHERE slot_type = 'logical' AND " + " database = current_database() AND " + " temporary IS FALSE AND " + " invalidation_reason IS NULL " + " ORDER BY confirmed_flush_lsn ASC " + " LIMIT 1 " + ") " + "SELECT slot_name, plugin, two_phase, failover, " + "CASE WHEN invalidation_reason IS NOT NULL THEN FALSE " + "ELSE last_pending_wal IS NULL OR " + " confirmed_flush_lsn > last_pending_wal " + "END as caught_up, " + "invalidation_reason IS NOT NULL as invalid " + "FROM pg_catalog.pg_replication_slots " + "LEFT JOIN check_caught_up ON true " + "WHERE slot_type = 'logical' AND " + "database = current_database() AND " + "temporary IS FALSE "; + } + + /* + * For PG18 and earlier, we call + * binary_upgrade_logical_slot_has_caught_up() for each logical slot. + */ + return "SELECT slot_name, plugin, two_phase, failover, " + "CASE WHEN invalidation_reason IS NOT NULL THEN FALSE " + "ELSE (SELECT pg_catalog.binary_upgrade_logical_slot_has_caught_up(slot_name)) " + "END as caught_up, " + "invalidation_reason IS NOT NULL as invalid " + "FROM pg_catalog.pg_replication_slots " + "WHERE slot_type = 'logical' AND " + "database = current_database() AND " + "temporary IS FALSE "; } /* - * Callback function for processing results of the query returned by - * get_old_cluster_logical_slot_infos_query(), which is used for + * Callback function for processing results of the query, which is used for * get_db_rel_and_slot_infos()'s UpgradeTask. This function stores the logical * slot information for later use. */ @@ -692,9 +776,6 @@ process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg) LogicalSlotInfo *slotinfos = NULL; int num_slots = PQntuples(res); - AssertVariableIsOfType(&process_old_cluster_logical_slot_infos, - UpgradeTaskProcessCB); - if (num_slots) { int i_slotname; @@ -704,7 +785,7 @@ process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg) int i_caught_up; int i_invalid; - slotinfos = (LogicalSlotInfo *) pg_malloc(sizeof(LogicalSlotInfo) * num_slots); + slotinfos = pg_malloc_array(LogicalSlotInfo, num_slots); i_slotname = PQfnumber(res, "slot_name"); i_plugin = PQfnumber(res, "plugin"); @@ -738,7 +819,7 @@ process_old_cluster_logical_slot_infos(DbInfo *dbinfo, PGresult *res, void *arg) * * Note: this function always returns 0 if the old_cluster is PG16 and prior * because we gather slot information only for cluster versions greater than or - * equal to PG17. See get_old_cluster_logical_slot_infos(). + * equal to PG17. See get_db_rel_and_slot_infos(). */ int count_old_cluster_logical_slots(void) @@ -752,20 +833,33 @@ count_old_cluster_logical_slots(void) } /* - * get_subscription_count() + * get_subscription_info() * - * Gets the number of subscriptions in the cluster. + * Gets the information of subscriptions in the cluster. */ void -get_subscription_count(ClusterInfo *cluster) +get_subscription_info(ClusterInfo *cluster) { PGconn *conn; PGresult *res; + int i_nsub; + int i_retain_dead_tuples; conn = connectToServer(cluster, "template1"); - res = executeQueryOrDie(conn, "SELECT count(*) " - "FROM pg_catalog.pg_subscription"); - cluster->nsubs = atoi(PQgetvalue(res, 0, 0)); + if (GET_MAJOR_VERSION(cluster->major_version) >= 1900) + res = executeQueryOrDie(conn, "SELECT count(*) AS nsub," + "COUNT(CASE WHEN subretaindeadtuples THEN 1 END) > 0 AS retain_dead_tuples " + "FROM pg_catalog.pg_subscription"); + else + res = executeQueryOrDie(conn, "SELECT count(*) AS nsub," + "'f' AS retain_dead_tuples " + "FROM pg_catalog.pg_subscription"); + + i_nsub = PQfnumber(res, "nsub"); + i_retain_dead_tuples = PQfnumber(res, "retain_dead_tuples"); + + cluster->nsubs = atoi(PQgetvalue(res, 0, i_nsub)); + cluster->sub_retain_dead_tuples = (strcmp(PQgetvalue(res, 0, i_retain_dead_tuples), "t") == 0); PQclear(res); PQfinish(conn); diff --git a/src/bin/pg_upgrade/meson.build b/src/bin/pg_upgrade/meson.build index ac992f0d14b1d..ffbf6ae8d759b 100644 --- a/src/bin/pg_upgrade/meson.build +++ b/src/bin/pg_upgrade/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_upgrade_sources = files( 'check.c', @@ -8,11 +8,14 @@ pg_upgrade_sources = files( 'file.c', 'function.c', 'info.c', + 'multixact_read_v18.c', + 'multixact_rewrite.c', 'option.c', 'parallel.c', 'pg_upgrade.c', 'relfilenumber.c', 'server.c', + 'slru_io.c', 'tablespace.c', 'task.c', 'util.c', @@ -33,13 +36,30 @@ pg_upgrade = executable('pg_upgrade', ) bin_targets += pg_upgrade +test_ext_sources = files( + '../../test/modules/test_extensions/test_ext.c' +) + +if host_system == 'windows' + test_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_ext', + '--FILEDESC', 'test_ext - test C extension for pg_upgrade',]) +endif + +test_ext = shared_module('test_ext', + test_ext_sources, + kwargs: pg_test_mod_args, +) tests += { 'name': 'pg_upgrade', 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { - 'env': {'with_icu': icu.found() ? 'yes' : 'no'}, + 'env': { + 'with_icu': icu.found() ? 'yes' : 'no', + 'TEST_EXT_LIB': test_ext.full_path(), + }, 'tests': [ 't/001_basic.pl', 't/002_pg_upgrade.pl', @@ -47,7 +67,10 @@ tests += { 't/004_subscription.pl', 't/005_char_signedness.pl', 't/006_transfer_modes.pl', + 't/007_multixact_conversion.pl', + 't/008_extension_control_path.pl', ], + 'deps': [test_ext], 'test_kwargs': {'priority': 40}, # pg_upgrade tests are slow }, } diff --git a/src/bin/pg_upgrade/multixact_read_v18.c b/src/bin/pg_upgrade/multixact_read_v18.c new file mode 100644 index 0000000000000..c92b977530a80 --- /dev/null +++ b/src/bin/pg_upgrade/multixact_read_v18.c @@ -0,0 +1,363 @@ +/* + * multixact_read_v18.c + * + * Functions to read multixact SLRUs from clusters of PostgreSQL version 18 + * and older. In version 19, the multixid offsets were expanded from 32 to 64 + * bits. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * src/bin/pg_upgrade/multixact_read_v18.c + */ + +#include "postgres_fe.h" + +#include "multixact_read_v18.h" +#include "pg_upgrade.h" + +/* + * NOTE: below are a bunch of definitions that are copy-pasted from + * multixact.c from version 18. It's important that this file doesn't + * #include the new definitions with same names from "multixact_internal.h"! + * + * To further avoid confusion in the functions exposed outside this source + * file, we use MultiXactOffset32 to represent the old-style 32-bit multixid + * offsets. The new 64-bit MultiXactOffset should not be used anywhere in + * this file. + */ +#ifdef MULTIXACT_INTERNAL_H +#error multixact_internal.h should not be included in multixact_read_v18.c +#endif +#define MultiXactOffset should_not_be_used + +/* We need four bytes per offset and 8 bytes per base for each page. */ +#define MULTIXACT_OFFSETS_PER_PAGE (BLCKSZ / sizeof(MultiXactOffset32)) + +static inline int64 +MultiXactIdToOffsetPage(MultiXactId multi) +{ + return multi / MULTIXACT_OFFSETS_PER_PAGE; +} + +static inline int +MultiXactIdToOffsetEntry(MultiXactId multi) +{ + return multi % MULTIXACT_OFFSETS_PER_PAGE; +} + +/* + * The situation for members is a bit more complex: we store one byte of + * additional flag bits for each TransactionId. To do this without getting + * into alignment issues, we store four bytes of flags, and then the + * corresponding 4 Xids. Each such 5-word (20-byte) set we call a "group", and + * are stored as a whole in pages. Thus, with 8kB BLCKSZ, we keep 409 groups + * per page. This wastes 12 bytes per page, but that's OK -- simplicity (and + * performance) trumps space efficiency here. + * + * Note that the "offset" macros work with byte offset, not array indexes, so + * arithmetic must be done using "char *" pointers. + */ +/* We need eight bits per xact, so one xact fits in a byte */ +#define MXACT_MEMBER_BITS_PER_XACT 8 +#define MXACT_MEMBER_FLAGS_PER_BYTE 1 +#define MXACT_MEMBER_XACT_BITMASK ((1 << MXACT_MEMBER_BITS_PER_XACT) - 1) + +/* how many full bytes of flags are there in a group? */ +#define MULTIXACT_FLAGBYTES_PER_GROUP 4 +#define MULTIXACT_MEMBERS_PER_MEMBERGROUP \ + (MULTIXACT_FLAGBYTES_PER_GROUP * MXACT_MEMBER_FLAGS_PER_BYTE) +/* size in bytes of a complete group */ +#define MULTIXACT_MEMBERGROUP_SIZE \ + (sizeof(TransactionId) * MULTIXACT_MEMBERS_PER_MEMBERGROUP + MULTIXACT_FLAGBYTES_PER_GROUP) +#define MULTIXACT_MEMBERGROUPS_PER_PAGE (BLCKSZ / MULTIXACT_MEMBERGROUP_SIZE) +#define MULTIXACT_MEMBERS_PER_PAGE \ + (MULTIXACT_MEMBERGROUPS_PER_PAGE * MULTIXACT_MEMBERS_PER_MEMBERGROUP) + +/* page in which a member is to be found */ +static inline int64 +MXOffsetToMemberPage(MultiXactOffset32 offset) +{ + return offset / MULTIXACT_MEMBERS_PER_PAGE; +} + +/* Location (byte offset within page) of flag word for a given member */ +static inline int +MXOffsetToFlagsOffset(MultiXactOffset32 offset) +{ + MultiXactOffset32 group = offset / MULTIXACT_MEMBERS_PER_MEMBERGROUP; + int grouponpg = group % MULTIXACT_MEMBERGROUPS_PER_PAGE; + int byteoff = grouponpg * MULTIXACT_MEMBERGROUP_SIZE; + + return byteoff; +} + +/* Location (byte offset within page) of TransactionId of given member */ +static inline int +MXOffsetToMemberOffset(MultiXactOffset32 offset) +{ + int member_in_group = offset % MULTIXACT_MEMBERS_PER_MEMBERGROUP; + + return MXOffsetToFlagsOffset(offset) + + MULTIXACT_FLAGBYTES_PER_GROUP + + member_in_group * sizeof(TransactionId); +} + +static inline int +MXOffsetToFlagsBitShift(MultiXactOffset32 offset) +{ + int member_in_group = offset % MULTIXACT_MEMBERS_PER_MEMBERGROUP; + int bshift = member_in_group * MXACT_MEMBER_BITS_PER_XACT; + + return bshift; +} + +/* + * Construct reader of old multixacts. + * + * Returns the malloced memory used by the all other calls in this module. + */ +OldMultiXactReader * +AllocOldMultiXactRead(char *pgdata, MultiXactId nextMulti, + MultiXactOffset32 nextOffset) +{ + OldMultiXactReader *state = pg_malloc_object(OldMultiXactReader); + char dir[MAXPGPATH] = {0}; + + state->nextMXact = nextMulti; + state->nextOffset = nextOffset; + + pg_sprintf(dir, "%s/pg_multixact/offsets", pgdata); + state->offset = AllocSlruRead(dir, false); + + pg_sprintf(dir, "%s/pg_multixact/members", pgdata); + state->members = AllocSlruRead(dir, false); + + return state; +} + +/* + * This is a simplified version of the GetMultiXactIdMembers() server + * function: + * + * - Only return the updating member, if any. Upgrade only cares about the + * updaters. If there is no updating member, return somewhat arbitrarily + * the first locking-only member, because we don't have any way to represent + * "no members". + * + * - Because there's no concurrent activity, we don't need to worry about + * locking and some corner cases. + * + * - Don't bail out on invalid entries that could've been left behind after a + * server crash. Such multixids won't appear anywhere else on disk, so the + * server will never try to read them. During upgrade, however, we scan + * through all multixids in order, and will encounter such invalid but + * unreferenced multixids too. We try to distinguish between entries that + * are invalid because of missed disk writes, like entries with zeros in + * offsets or members, and entries that look corrupt in other ways that + * should not happen even on a server crash. + * + * Returns true on success, false if the multixact was invalid. + */ +bool +GetOldMultiXactIdSingleMember(OldMultiXactReader *state, MultiXactId multi, + MultiXactMember *member) +{ + MultiXactId nextMXact, + nextOffset, + tmpMXact; + int64 pageno, + prev_pageno; + int entryno, + length; + char *buf; + MultiXactOffset32 *offptr, + offset; + MultiXactOffset32 nextMXOffset; + TransactionId result_xid = InvalidTransactionId; + MultiXactStatus result_status = 0; + + nextMXact = state->nextMXact; + nextOffset = state->nextOffset; + + /* + * Comment copied from GetMultiXactIdMembers in PostgreSQL v18 + * multixact.c: + * + * Find out the offset at which we need to start reading MultiXactMembers + * and the number of members in the multixact. We determine the latter as + * the difference between this multixact's starting offset and the next + * one's. However, there are some corner cases to worry about: + * + * 1. This multixact may be the latest one created, in which case there is + * no next one to look at. The next multixact's offset should be set + * already, as we set it in RecordNewMultiXact(), but we used to not do + * that in older minor versions. To cope with that case, if this + * multixact is the latest one created, use the nextOffset value we read + * above as the endpoint. + * + * 2. Because GetNewMultiXactId skips over offset zero, to reserve zero + * for to mean "unset", there is an ambiguity near the point of offset + * wraparound. If we see next multixact's offset is one, is that our + * multixact's actual endpoint, or did it end at zero with a subsequent + * increment? We handle this using the knowledge that if the zero'th + * member slot wasn't filled, it'll contain zero, and zero isn't a valid + * transaction ID so it can't be a multixact member. Therefore, if we + * read a zero from the members array, just ignore it. + */ + + pageno = MultiXactIdToOffsetPage(multi); + entryno = MultiXactIdToOffsetEntry(multi); + + buf = SlruReadSwitchPage(state->offset, pageno); + offptr = (MultiXactOffset32 *) buf; + offptr += entryno; + offset = *offptr; + + if (offset == 0) + { + /* Invalid entry. These can be left behind on a server crash. */ + return false; + } + + /* + * Use the same increment rule as GetNewMultiXactId(), that is, don't + * handle wraparound explicitly until needed. + */ + tmpMXact = multi + 1; + + if (nextMXact == tmpMXact) + { + /* Corner case 1: there is no next multixact */ + nextMXOffset = nextOffset; + } + else + { + /* handle wraparound if needed */ + if (tmpMXact < FirstMultiXactId) + tmpMXact = FirstMultiXactId; + + prev_pageno = pageno; + + pageno = MultiXactIdToOffsetPage(tmpMXact); + entryno = MultiXactIdToOffsetEntry(tmpMXact); + + if (pageno != prev_pageno) + buf = SlruReadSwitchPage(state->offset, pageno); + + offptr = (MultiXactOffset32 *) buf; + offptr += entryno; + nextMXOffset = *offptr; + } + + if (nextMXOffset == 0) + { + /* Invalid entry. These can be left behind on a server crash. */ + return false; + } + length = nextMXOffset - offset; + + if (length < 0) + { + /* + * This entry is corrupt. We should not see these even after a server + * crash. + */ + pg_fatal("multixact %u has an invalid length (%d)", multi, length); + } + if (length == 0) + { + /* + * Invalid entry. The server never writes multixids with zero + * members, but it's not clear if a server crash or using pg_resetwal + * could leave them behind. Seems best to accept them. + */ + return false; + } + + /* read the members */ + prev_pageno = -1; + for (int i = 0; i < length; i++, offset++) + { + TransactionId *xactptr; + uint32 *flagsptr; + int flagsoff; + int bshift; + int memberoff; + MultiXactStatus status; + + pageno = MXOffsetToMemberPage(offset); + memberoff = MXOffsetToMemberOffset(offset); + + if (pageno != prev_pageno) + { + buf = SlruReadSwitchPage(state->members, pageno); + prev_pageno = pageno; + } + + xactptr = (TransactionId *) (buf + memberoff); + if (!TransactionIdIsValid(*xactptr)) + { + /* + * Corner case 2: offset must have wrapped around to unused slot + * zero. + */ + if (offset == 0) + continue; + + /* + * Otherwise this is an invalid entry that should not be + * referenced from anywhere in the heap. These can be left behind + * on a server crash. We could return 'false' here, but we prefer + * to continue reading the members and converting them the best we + * can, to preserve evidence in case this is corruption that + * should not have happened. + */ + } + + flagsoff = MXOffsetToFlagsOffset(offset); + bshift = MXOffsetToFlagsBitShift(offset); + flagsptr = (uint32 *) (buf + flagsoff); + + status = (*flagsptr >> bshift) & MXACT_MEMBER_XACT_BITMASK; + + /* + * Remember the updating XID among the members, or first locking XID + * if no updating XID. + */ + if (ISUPDATE_from_mxstatus(status)) + { + /* sanity check */ + if (ISUPDATE_from_mxstatus(result_status)) + { + /* + * We don't expect to see more than one updating member, even + * if the server had crashed. + */ + pg_fatal("multixact %u has more than one updating member", + multi); + } + result_xid = *xactptr; + result_status = status; + } + else if (!TransactionIdIsValid(result_xid)) + { + result_xid = *xactptr; + result_status = status; + } + } + + member->xid = result_xid; + member->status = result_status; + return true; +} + +/* + * Frees the malloced reader. + */ +void +FreeOldMultiXactReader(OldMultiXactReader *state) +{ + FreeSlruRead(state->offset); + FreeSlruRead(state->members); + + pfree(state); +} diff --git a/src/bin/pg_upgrade/multixact_read_v18.h b/src/bin/pg_upgrade/multixact_read_v18.h new file mode 100644 index 0000000000000..88b52b9d2e892 --- /dev/null +++ b/src/bin/pg_upgrade/multixact_read_v18.h @@ -0,0 +1,37 @@ +/* + * multixact_read_v18.h + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * src/bin/pg_upgrade/multixact_read_v18.h + */ +#ifndef MULTIXACT_READ_V18_H +#define MULTIXACT_READ_V18_H + +#include "access/multixact.h" +#include "slru_io.h" + +/* + * MultiXactOffset changed from uint32 to uint64 between versions 18 and 19. + * MultiXactOffset32 is used to represent a 32-bit offset from the old + * cluster. + */ +typedef uint32 MultiXactOffset32; + +typedef struct OldMultiXactReader +{ + MultiXactId nextMXact; + MultiXactOffset32 nextOffset; + + SlruSegState *offset; + SlruSegState *members; +} OldMultiXactReader; + +extern OldMultiXactReader *AllocOldMultiXactRead(char *pgdata, + MultiXactId nextMulti, + MultiXactOffset32 nextOffset); +extern bool GetOldMultiXactIdSingleMember(OldMultiXactReader *state, + MultiXactId multi, + MultiXactMember *member); +extern void FreeOldMultiXactReader(OldMultiXactReader *state); + +#endif /* MULTIXACT_READ_V18_H */ diff --git a/src/bin/pg_upgrade/multixact_rewrite.c b/src/bin/pg_upgrade/multixact_rewrite.c new file mode 100644 index 0000000000000..823984ec8f337 --- /dev/null +++ b/src/bin/pg_upgrade/multixact_rewrite.c @@ -0,0 +1,191 @@ +/* + * multixact_rewrite.c + * + * Functions to convert multixact SLRUs from the pre-v19 format to the current + * format with 64-bit MultiXactOffsets. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * src/bin/pg_upgrade/multixact_rewrite.c + */ + +#include "postgres_fe.h" + +#include "access/multixact_internal.h" +#include "multixact_read_v18.h" +#include "pg_upgrade.h" + +static void RecordMultiXactOffset(SlruSegState *offsets_writer, MultiXactId multi, + MultiXactOffset offset); +static void RecordMultiXactMembers(SlruSegState *members_writer, + MultiXactOffset offset, + int nmembers, MultiXactMember *members); + +/* + * Convert pg_multixact/offset and /members from the old pre-v19 format with + * 32-bit offsets to the current format. + * + * Multixids in the range [from_multi, to_multi) are read from the old + * cluster, and written in the new format. An important edge case is that if + * from_multi == to_multi, this initializes the new pg_multixact files in the + * new format without trying to open any old files. (We rely on that when + * upgrading from PostgreSQL version 9.2 or below.) + * + * Returns the new nextOffset value; the caller should set it in the new + * control file. The new members always start from offset 1, regardless of + * the offset range used in the old cluster. + */ +MultiXactOffset +rewrite_multixacts(MultiXactId from_multi, MultiXactId to_multi) +{ + MultiXactOffset next_offset; + SlruSegState *offsets_writer; + SlruSegState *members_writer; + char dir[MAXPGPATH] = {0}; + bool prev_multixid_valid = false; + + /* + * The range of valid multi XIDs is unchanged by the conversion (they are + * referenced from the heap tables), but the members SLRU is rewritten to + * start from offset 1. + */ + next_offset = 1; + + /* Prepare to write the new SLRU files */ + pg_sprintf(dir, "%s/pg_multixact/offsets", new_cluster.pgdata); + offsets_writer = AllocSlruWrite(dir, false); + SlruWriteSwitchPage(offsets_writer, MultiXactIdToOffsetPage(from_multi)); + + pg_sprintf(dir, "%s/pg_multixact/members", new_cluster.pgdata); + members_writer = AllocSlruWrite(dir, true /* use long segment names */ ); + SlruWriteSwitchPage(members_writer, MXOffsetToMemberPage(next_offset)); + + /* + * Convert old multixids, if needed, by reading them one-by-one from the + * old cluster. + */ + if (to_multi != from_multi) + { + OldMultiXactReader *old_reader; + + old_reader = AllocOldMultiXactRead(old_cluster.pgdata, + old_cluster.controldata.chkpnt_nxtmulti, + old_cluster.controldata.chkpnt_nxtmxoff); + + for (MultiXactId multi = from_multi; multi != to_multi;) + { + MultiXactMember member; + bool multixid_valid; + + /* + * Read this multixid's members. + * + * Locking-only XIDs that may be part of multi-xids don't matter + * after upgrade, as there can be no transactions running across + * upgrade. So as a small optimization, we only read one member + * from each multixid: the one updating one, or if there was no + * update, arbitrarily the first locking xid. + */ + multixid_valid = GetOldMultiXactIdSingleMember(old_reader, multi, &member); + + /* + * Write the new offset to pg_multixact/offsets. + * + * Even if this multixid is invalid, we still need to write its + * offset if the *previous* multixid was valid. That's because + * when reading a multixid, the number of members is calculated + * from the difference between the two offsets. + */ + RecordMultiXactOffset(offsets_writer, multi, + (multixid_valid || prev_multixid_valid) ? next_offset : 0); + + /* Write the members */ + if (multixid_valid) + { + RecordMultiXactMembers(members_writer, next_offset, 1, &member); + next_offset += 1; + } + + /* Advance to next multixid, handling wraparound */ + multi++; + if (multi < FirstMultiXactId) + multi = FirstMultiXactId; + prev_multixid_valid = multixid_valid; + } + + FreeOldMultiXactReader(old_reader); + } + + /* Write the final 'next' offset to the last SLRU page */ + RecordMultiXactOffset(offsets_writer, to_multi, + prev_multixid_valid ? next_offset : 0); + + /* Flush the last SLRU pages */ + FreeSlruWrite(offsets_writer); + FreeSlruWrite(members_writer); + + return next_offset; +} + + +/* + * Write one offset to the offset SLRU + */ +static void +RecordMultiXactOffset(SlruSegState *offsets_writer, MultiXactId multi, + MultiXactOffset offset) +{ + int64 pageno; + int entryno; + char *buf; + MultiXactOffset *offptr; + + pageno = MultiXactIdToOffsetPage(multi); + entryno = MultiXactIdToOffsetEntry(multi); + + buf = SlruWriteSwitchPage(offsets_writer, pageno); + offptr = (MultiXactOffset *) buf; + offptr[entryno] = offset; +} + +/* + * Write the members for one multixid in the members SLRU + * + * (Currently, this is only ever called with nmembers == 1) + */ +static void +RecordMultiXactMembers(SlruSegState *members_writer, + MultiXactOffset offset, + int nmembers, MultiXactMember *members) +{ + for (int i = 0; i < nmembers; i++, offset++) + { + int64 pageno; + char *buf; + TransactionId *memberptr; + uint32 *flagsptr; + uint32 flagsval; + int bshift; + int flagsoff; + int memberoff; + + Assert(members[i].status <= MultiXactStatusUpdate); + + pageno = MXOffsetToMemberPage(offset); + memberoff = MXOffsetToMemberOffset(offset); + flagsoff = MXOffsetToFlagsOffset(offset); + bshift = MXOffsetToFlagsBitShift(offset); + + buf = SlruWriteSwitchPage(members_writer, pageno); + + memberptr = (TransactionId *) (buf + memberoff); + + *memberptr = members[i].xid; + + flagsptr = (uint32 *) (buf + flagsoff); + + flagsval = *flagsptr; + flagsval &= ~(((1 << MXACT_MEMBER_BITS_PER_XACT) - 1) << bshift); + flagsval |= (members[i].status << bshift); + *flagsptr = flagsval; + } +} diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c index 7fd7f1d33fcb8..f01d2f92d9548 100644 --- a/src/bin/pg_upgrade/option.c +++ b/src/bin/pg_upgrade/option.c @@ -3,7 +3,7 @@ * * options functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/option.c */ diff --git a/src/bin/pg_upgrade/parallel.c b/src/bin/pg_upgrade/parallel.c index 056aa2edaee3f..f0406de84ee31 100644 --- a/src/bin/pg_upgrade/parallel.c +++ b/src/bin/pg_upgrade/parallel.c @@ -3,7 +3,7 @@ * * multi-process support * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/parallel.c */ @@ -40,6 +40,7 @@ typedef struct char *old_pgdata; char *new_pgdata; char *old_tablespace; + char *new_tablespace; } transfer_thread_arg; static exec_thread_arg **exec_thread_args; @@ -84,13 +85,13 @@ parallel_exec_prog(const char *log_file, const char *opt_log_file, /* parallel */ #ifdef WIN32 if (thread_handles == NULL) - thread_handles = pg_malloc(user_opts.jobs * sizeof(HANDLE)); + thread_handles = pg_malloc_array(HANDLE, user_opts.jobs); if (exec_thread_args == NULL) { int i; - exec_thread_args = pg_malloc(user_opts.jobs * sizeof(exec_thread_arg *)); + exec_thread_args = pg_malloc_array(exec_thread_arg *, user_opts.jobs); /* * For safety and performance, we keep the args allocated during @@ -98,7 +99,7 @@ parallel_exec_prog(const char *log_file, const char *opt_log_file, * thread different from the one that allocated it. */ for (i = 0; i < user_opts.jobs; i++) - exec_thread_args[i] = pg_malloc0(sizeof(exec_thread_arg)); + exec_thread_args[i] = pg_malloc0_object(exec_thread_arg); } cur_thread_args = (void **) exec_thread_args; @@ -171,7 +172,7 @@ win32_exec_prog(exec_thread_arg *args) void parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, char *old_pgdata, char *new_pgdata, - char *old_tablespace) + char *old_tablespace, char *new_tablespace) { #ifndef WIN32 pid_t child; @@ -181,19 +182,19 @@ parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, #endif if (user_opts.jobs <= 1) - transfer_all_new_dbs(old_db_arr, new_db_arr, old_pgdata, new_pgdata, NULL); + transfer_all_new_dbs(old_db_arr, new_db_arr, old_pgdata, new_pgdata, NULL, NULL); else { /* parallel */ #ifdef WIN32 if (thread_handles == NULL) - thread_handles = pg_malloc(user_opts.jobs * sizeof(HANDLE)); + thread_handles = pg_malloc_array(HANDLE, user_opts.jobs); if (transfer_thread_args == NULL) { int i; - transfer_thread_args = pg_malloc(user_opts.jobs * sizeof(transfer_thread_arg *)); + transfer_thread_args = pg_malloc_array(transfer_thread_arg *, user_opts.jobs); /* * For safety and performance, we keep the args allocated during @@ -201,7 +202,7 @@ parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, * thread different from the one that allocated it. */ for (i = 0; i < user_opts.jobs; i++) - transfer_thread_args[i] = pg_malloc0(sizeof(transfer_thread_arg)); + transfer_thread_args[i] = pg_malloc0_object(transfer_thread_arg); } cur_thread_args = (void **) transfer_thread_args; @@ -225,7 +226,7 @@ parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, if (child == 0) { transfer_all_new_dbs(old_db_arr, new_db_arr, old_pgdata, new_pgdata, - old_tablespace); + old_tablespace, new_tablespace); /* if we take another exit path, it will be non-zero */ /* use _exit to skip atexit() functions */ _exit(0); @@ -246,6 +247,7 @@ parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, new_arg->new_pgdata = pg_strdup(new_pgdata); pg_free(new_arg->old_tablespace); new_arg->old_tablespace = old_tablespace ? pg_strdup(old_tablespace) : NULL; + new_arg->new_tablespace = new_tablespace ? pg_strdup(new_tablespace) : NULL; child = (HANDLE) _beginthreadex(NULL, 0, (void *) win32_transfer_all_new_dbs, new_arg, 0, NULL); @@ -263,7 +265,8 @@ DWORD win32_transfer_all_new_dbs(transfer_thread_arg *args) { transfer_all_new_dbs(args->old_db_arr, args->new_db_arr, args->old_pgdata, - args->new_pgdata, args->old_tablespace); + args->new_pgdata, args->old_tablespace, + args->new_tablespace); /* terminates thread */ return 0; diff --git a/src/bin/pg_upgrade/pg_upgrade.c b/src/bin/pg_upgrade/pg_upgrade.c index 536e49d26168b..2127d297bfe86 100644 --- a/src/bin/pg_upgrade/pg_upgrade.c +++ b/src/bin/pg_upgrade/pg_upgrade.c @@ -3,7 +3,7 @@ * * main source file * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/pg_upgrade.c */ @@ -29,9 +29,9 @@ * We control all assignments of pg_enum.oid because these oids are stored * in user tables as enum values. * - * We control all assignments of pg_authid.oid for historical reasons (the - * oids used to be stored in pg_largeobject_metadata, which is now copied via - * SQL commands), that might change at some point in the future. + * We control all assignments of pg_authid.oid because the oids are stored in + * pg_largeobject_metadata, which is copied via file transfer for upgrades + * from v16 and newer. * * We control all assignments of pg_database.oid because we want the directory * names to match between the old and new cluster. @@ -43,6 +43,7 @@ #include +#include "access/multixact.h" #include "catalog/pg_class_d.h" #include "common/file_perm.h" #include "common/logging.h" @@ -67,6 +68,7 @@ static void set_frozenxids(bool minmxid_only); static void make_outputdirs(char *pgdata); static void setup(char *argv0); static void create_logical_replication_slots(void); +static void create_conflict_detection_slot(void); ClusterInfo old_cluster, new_cluster; @@ -88,6 +90,7 @@ int main(int argc, char **argv) { char *deletion_script_file_name = NULL; + bool migrate_logical_slots; /* * pg_upgrade doesn't currently use common/logging.c, but initialize it @@ -198,18 +201,39 @@ main(int argc, char **argv) new_cluster.pgdata); check_ok(); + migrate_logical_slots = count_old_cluster_logical_slots(); + /* - * Migrate the logical slots to the new cluster. Note that we need to do - * this after resetting WAL because otherwise the required WAL would be - * removed and slots would become unusable. There is a possibility that - * background processes might generate some WAL before we could create the - * slots in the new cluster but we can ignore that WAL as that won't be - * required downstream. + * Migrate replication slots to the new cluster. + * + * Note that we must migrate logical slots after resetting WAL because + * otherwise the required WAL would be removed and slots would become + * unusable. There is a possibility that background processes might + * generate some WAL before we could create the slots in the new cluster + * but we can ignore that WAL as that won't be required downstream. + * + * The conflict detection slot is not affected by concerns related to WALs + * as it only retains the dead tuples. It is created here for consistency. + * Note that the new conflict detection slot uses the latest transaction + * ID as xmin, so it cannot protect dead tuples that existed before the + * upgrade. Additionally, commit timestamps and origin data are not + * preserved during the upgrade. So, even after creating the slot, the + * upgraded subscriber may be unable to detect conflicts or log relevant + * commit timestamps and origins when applying changes from the publisher + * occurred before the upgrade especially if those changes were not + * replicated. It can only protect tuples that might be deleted after the + * new cluster starts. */ - if (count_old_cluster_logical_slots()) + if (migrate_logical_slots || old_cluster.sub_retain_dead_tuples) { start_postmaster(&new_cluster, true); - create_logical_replication_slots(); + + if (migrate_logical_slots) + create_logical_replication_slots(); + + if (old_cluster.sub_retain_dead_tuples) + create_conflict_detection_slot(); + stop_postmaster(false); } @@ -784,15 +808,15 @@ copy_xact_xlog_xid(void) new_cluster.pgdata); check_ok(); - /* - * If the old server is before the MULTIXACT_FORMATCHANGE_CAT_VER change - * (see pg_upgrade.h) and the new server is after, then we don't copy - * pg_multixact files, but we need to reset pg_control so that the new - * server doesn't attempt to read multis older than the cutoff value. - */ - if (old_cluster.controldata.cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER && - new_cluster.controldata.cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER) + /* Copy or convert pg_multixact files */ + Assert(new_cluster.controldata.cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER); + Assert(new_cluster.controldata.cat_ver >= MULTIXACTOFFSET_FORMATCHANGE_CAT_VER); + if (old_cluster.controldata.cat_ver >= MULTIXACTOFFSET_FORMATCHANGE_CAT_VER) { + /* No change in multixact format, just copy the files */ + MultiXactId new_nxtmulti = old_cluster.controldata.chkpnt_nxtmulti; + MultiXactOffset new_nxtmxoff = old_cluster.controldata.chkpnt_nxtmxoff; + copy_subdir_files("pg_multixact/offsets", "pg_multixact/offsets"); copy_subdir_files("pg_multixact/members", "pg_multixact/members"); @@ -803,38 +827,67 @@ copy_xact_xlog_xid(void) * counters here and the oldest multi present on system. */ exec_prog(UTILITY_LOG_FILE, NULL, true, true, - "\"%s/pg_resetwal\" -O %u -m %u,%u \"%s\"", - new_cluster.bindir, - old_cluster.controldata.chkpnt_nxtmxoff, - old_cluster.controldata.chkpnt_nxtmulti, + "\"%s/pg_resetwal\" -O %" PRIu64 " -m %u,%u \"%s\"", + new_cluster.bindir, new_nxtmxoff, new_nxtmulti, old_cluster.controldata.chkpnt_oldstMulti, new_cluster.pgdata); check_ok(); } - else if (new_cluster.controldata.cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER) + else { + /* Conversion is needed */ + MultiXactId nxtmulti; + MultiXactId oldstMulti; + MultiXactOffset nxtmxoff; + /* - * Remove offsets/0000 file created by initdb that no longer matches - * the new multi-xid value. "members" starts at zero so no need to - * remove it. + * Determine the range of multixacts to convert. */ - remove_new_subdir("pg_multixact/offsets", false); + nxtmulti = old_cluster.controldata.chkpnt_nxtmulti; + if (old_cluster.controldata.cat_ver >= MULTIXACT_FORMATCHANGE_CAT_VER) + { + /* Versions 9.3 - 18: convert all multixids */ + oldstMulti = old_cluster.controldata.chkpnt_oldstMulti; + } + else + { + /* + * In PostgreSQL 9.2 and below, multitransactions were only used + * for row locking, and as such don't need to be preserved during + * upgrade. In that case, we utilize rewrite_multixacts() just to + * initialize new, empty files in the new format. + * + * It's important that the oldest multi is set to the latest value + * used by the old system, so that multixact.c returns the empty + * set for multis that might be present on disk. + */ + oldstMulti = nxtmulti; + } + /* handle wraparound */ + if (nxtmulti < FirstMultiXactId) + nxtmulti = FirstMultiXactId; + if (oldstMulti < FirstMultiXactId) + oldstMulti = FirstMultiXactId; - prep_status("Setting oldest multixact ID in new cluster"); + /* + * Remove the files created by initdb in the new cluster. + * rewrite_multixacts() will create new ones. + */ + remove_new_subdir("pg_multixact/members", false); + remove_new_subdir("pg_multixact/offsets", false); /* - * We don't preserve files in this case, but it's important that the - * oldest multi is set to the latest value used by the old system, so - * that multixact.c returns the empty set for multis that might be - * present on disk. We set next multi to the value following that; it - * might end up wrapped around (i.e. 0) if the old cluster had - * next=MaxMultiXactId, but multixact.c can cope with that just fine. + * Create new pg_multixact files, converting old ones if needed. */ + prep_status("Converting pg_multixact files"); + nxtmxoff = rewrite_multixacts(oldstMulti, nxtmulti); + check_ok(); + + prep_status("Setting next multixact ID and offset for new cluster"); exec_prog(UTILITY_LOG_FILE, NULL, true, true, - "\"%s/pg_resetwal\" -m %u,%u \"%s\"", + "\"%s/pg_resetwal\" -O %" PRIu64 " -m %u,%u \"%s\"", new_cluster.bindir, - old_cluster.controldata.chkpnt_nxtmulti + 1, - old_cluster.controldata.chkpnt_nxtmulti, + nxtmxoff, nxtmulti, oldstMulti, new_cluster.pgdata); check_ok(); } @@ -1025,3 +1078,24 @@ create_logical_replication_slots(void) return; } + +/* + * create_conflict_detection_slot() + * + * Create a replication slot to retain information necessary for conflict + * detection such as dead tuples, commit timestamps, and origins, for migrated + * subscriptions with retain_dead_tuples enabled. + */ +static void +create_conflict_detection_slot(void) +{ + PGconn *conn_new_template1; + + prep_status("Creating the replication conflict detection slot"); + + conn_new_template1 = connectToServer(&new_cluster, "template1"); + PQclear(executeQueryOrDie(conn_new_template1, "SELECT pg_catalog.binary_upgrade_create_conflict_detection_slot()")); + PQfinish(conn_new_template1); + + check_ok(); +} diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h index 69c965bb7d09a..1d767bbda2df8 100644 --- a/src/bin/pg_upgrade/pg_upgrade.h +++ b/src/bin/pg_upgrade/pg_upgrade.h @@ -1,7 +1,7 @@ /* * pg_upgrade.h * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/pg_upgrade.h */ @@ -114,6 +114,13 @@ extern char *output_files[]; */ #define MULTIXACT_FORMATCHANGE_CAT_VER 201301231 +/* + * MultiXactOffset was changed from 32-bit to 64-bit in version 19, at this + * catalog version. pg_multixact files need to be converted when upgrading + * across this version. + */ +#define MULTIXACTOFFSET_FORMATCHANGE_CAT_VER 202512091 + /* * large object chunk size added to pg_controldata, * commit 5f93c37805e7485488480916b4585e098d3cc883 @@ -235,7 +242,7 @@ typedef struct uint32 chkpnt_nxtepoch; uint32 chkpnt_nxtoid; uint32 chkpnt_nxtmulti; - uint32 chkpnt_nxtmxoff; + uint64 chkpnt_nxtmxoff; uint32 chkpnt_oldstMulti; uint32 chkpnt_oldstxid; uint32 align; @@ -298,10 +305,14 @@ typedef struct char *sockdir; /* directory for Unix Domain socket, if any */ unsigned short port; /* port number where postmaster is waiting */ uint32 major_version; /* PG_VERSION of cluster */ - char major_version_str[64]; /* string PG_VERSION of cluster */ + char *major_version_str; /* string PG_VERSION of cluster */ uint32 bin_version; /* version returned from pg_ctl */ + char **tablespaces; /* tablespace directories */ + int num_tablespaces; const char *tablespace_suffix; /* directory specification */ int nsubs; /* number of subscriptions */ + bool sub_retain_dead_tuples; /* whether a subscription enables + * retain_dead_tuples. */ } ClusterInfo; @@ -354,8 +365,6 @@ typedef struct const char *progname; /* complete pathname for this program */ char *user; /* username for clusters */ bool user_specified; /* user specified on command-line */ - char **old_tablespaces; /* tablespaces */ - int num_old_tablespaces; LibraryInfo *libraries; /* loadable libraries */ int num_libraries; ClusterInfo *running_cluster; @@ -441,7 +450,7 @@ FileNameMap *gen_db_file_maps(DbInfo *old_db, const char *new_pgdata); void get_db_rel_and_slot_infos(ClusterInfo *cluster); int count_old_cluster_logical_slots(void); -void get_subscription_count(ClusterInfo *cluster); +void get_subscription_info(ClusterInfo *cluster); /* option.c */ @@ -455,7 +464,7 @@ void transfer_all_new_tablespaces(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, char *old_pgdata, char *new_pgdata); void transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, char *old_pgdata, char *new_pgdata, - char *old_tablespace); + char *old_tablespace, char *new_tablespace); /* tablespace.c */ @@ -471,7 +480,6 @@ char *cluster_conn_opts(ClusterInfo *cluster); bool start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error); void stop_postmaster(bool in_atexit); -uint32 get_major_server_version(ClusterInfo *cluster); void check_pghost_envvar(void); @@ -493,17 +501,21 @@ unsigned int str2uint(const char *str); /* version.c */ bool jsonb_9_4_check_applicable(ClusterInfo *cluster); +bool protocol_negotiation_supported(const ClusterInfo *cluster); void old_9_6_invalidate_hash_indexes(ClusterInfo *cluster, bool check_mode); void report_extension_updates(ClusterInfo *cluster); +/* multixact_rewrite.c */ +MultiXactOffset rewrite_multixacts(MultiXactId from_multi, MultiXactId to_multi); + /* parallel.c */ void parallel_exec_prog(const char *log_file, const char *opt_log_file, const char *fmt,...) pg_attribute_printf(3, 4); void parallel_transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, char *old_pgdata, char *new_pgdata, - char *old_tablespace); + char *old_tablespace, char *new_tablespace); bool reap_child(bool wait_for_child); /* task.c */ diff --git a/src/bin/pg_upgrade/po/meson.build b/src/bin/pg_upgrade/po/meson.build index 2b2c004bb2dfb..1648aaaf94ba7 100644 --- a/src/bin/pg_upgrade/po/meson.build +++ b/src/bin/pg_upgrade/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_upgrade-' + pg_version_major.to_string())] diff --git a/src/bin/pg_upgrade/relfilenumber.c b/src/bin/pg_upgrade/relfilenumber.c index 2959c07f0b8d1..d5088447e0d06 100644 --- a/src/bin/pg_upgrade/relfilenumber.c +++ b/src/bin/pg_upgrade/relfilenumber.c @@ -3,7 +3,7 @@ * * relfilenumber functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/relfilenumber.c */ @@ -17,7 +17,7 @@ #include "common/logging.h" #include "pg_upgrade.h" -static void transfer_single_new_db(FileNameMap *maps, int size, char *old_tablespace); +static void transfer_single_new_db(FileNameMap *maps, int size, char *old_tablespace, char *new_tablespace); static void transfer_relfile(FileNameMap *map, const char *type_suffix, bool vm_must_add_frozenbit); /* @@ -136,21 +136,22 @@ transfer_all_new_tablespaces(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, */ if (user_opts.jobs <= 1) parallel_transfer_all_new_dbs(old_db_arr, new_db_arr, old_pgdata, - new_pgdata, NULL); + new_pgdata, NULL, NULL); else { int tblnum; /* transfer default tablespace */ parallel_transfer_all_new_dbs(old_db_arr, new_db_arr, old_pgdata, - new_pgdata, old_pgdata); + new_pgdata, old_pgdata, new_pgdata); - for (tblnum = 0; tblnum < os_info.num_old_tablespaces; tblnum++) + for (tblnum = 0; tblnum < old_cluster.num_tablespaces; tblnum++) parallel_transfer_all_new_dbs(old_db_arr, new_db_arr, old_pgdata, new_pgdata, - os_info.old_tablespaces[tblnum]); + old_cluster.tablespaces[tblnum], + new_cluster.tablespaces[tblnum]); /* reap all children */ while (reap_child(true) == true) ; @@ -169,7 +170,8 @@ transfer_all_new_tablespaces(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, */ void transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, - char *old_pgdata, char *new_pgdata, char *old_tablespace) + char *old_pgdata, char *new_pgdata, + char *old_tablespace, char *new_tablespace) { int old_dbnum, new_dbnum; @@ -204,7 +206,7 @@ transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, new_pgdata); if (n_maps) { - transfer_single_new_db(mappings, n_maps, old_tablespace); + transfer_single_new_db(mappings, n_maps, old_tablespace, new_tablespace); } /* We allocate something even for n_maps == 0 */ pg_free(mappings); @@ -234,10 +236,10 @@ transfer_all_new_dbs(DbInfoArr *old_db_arr, DbInfoArr *new_db_arr, * moved_db_dir: Destination for the pg_restore-generated database directory. */ static bool -prepare_for_swap(const char *old_tablespace, Oid db_oid, - char *old_catalog_dir, char *new_db_dir, char *moved_db_dir) +prepare_for_swap(const char *old_tablespace, const char *new_tablespace, + Oid db_oid, char *old_catalog_dir, char *new_db_dir, + char *moved_db_dir) { - const char *new_tablespace; const char *old_tblspc_suffix; const char *new_tblspc_suffix; char old_tblspc[MAXPGPATH]; @@ -247,24 +249,14 @@ prepare_for_swap(const char *old_tablespace, Oid db_oid, struct stat st; if (strcmp(old_tablespace, old_cluster.pgdata) == 0) - { - new_tablespace = new_cluster.pgdata; - new_tblspc_suffix = "/base"; old_tblspc_suffix = "/base"; - } else - { - /* - * XXX: The below line is a hack to deal with the fact that we - * presently don't have an easy way to find the corresponding new - * tablespace's path. This will need to be fixed if/when we add - * pg_upgrade support for in-place tablespaces. - */ - new_tablespace = old_tablespace; + old_tblspc_suffix = old_cluster.tablespace_suffix; + if (strcmp(new_tablespace, new_cluster.pgdata) == 0) + new_tblspc_suffix = "/base"; + else new_tblspc_suffix = new_cluster.tablespace_suffix; - old_tblspc_suffix = old_cluster.tablespace_suffix; - } /* Old and new cluster paths. */ snprintf(old_tblspc, sizeof(old_tblspc), "%s%s", old_tablespace, old_tblspc_suffix); @@ -290,19 +282,19 @@ prepare_for_swap(const char *old_tablespace, Oid db_oid, /* Create directory for stuff that is moved aside. */ if (pg_mkdir_p(moved_tblspc, pg_dir_create_mode) != 0 && errno != EEXIST) - pg_fatal("could not create directory \"%s\"", moved_tblspc); + pg_fatal("could not create directory \"%s\": %m", moved_tblspc); /* Create directory for old catalog files. */ if (pg_mkdir_p(old_catalog_dir, pg_dir_create_mode) != 0) - pg_fatal("could not create directory \"%s\"", old_catalog_dir); + pg_fatal("could not create directory \"%s\": %m", old_catalog_dir); /* Move the new cluster's database directory aside. */ if (rename(new_db_dir, moved_db_dir) != 0) - pg_fatal("could not rename \"%s\" to \"%s\"", new_db_dir, moved_db_dir); + pg_fatal("could not rename directory \"%s\" to \"%s\": %m", new_db_dir, moved_db_dir); /* Move the old cluster's database directory into place. */ if (rename(old_db_dir, new_db_dir) != 0) - pg_fatal("could not rename \"%s\" to \"%s\"", old_db_dir, new_db_dir); + pg_fatal("could not rename directory \"%s\" to \"%s\": %m", old_db_dir, new_db_dir); return true; } @@ -390,7 +382,7 @@ swap_catalog_files(FileNameMap *maps, int size, const char *old_catalog_dir, snprintf(dest, sizeof(dest), "%s/%s", old_catalog_dir, de->d_name); if (rename(path, dest) != 0) - pg_fatal("could not rename \"%s\" to \"%s\": %m", path, dest); + pg_fatal("could not rename file \"%s\" to \"%s\": %m", path, dest); } if (errno) pg_fatal("could not read directory \"%s\": %m", new_db_dir); @@ -417,7 +409,7 @@ swap_catalog_files(FileNameMap *maps, int size, const char *old_catalog_dir, snprintf(dest, sizeof(dest), "%s/%s", new_db_dir, de->d_name); if (rename(path, dest) != 0) - pg_fatal("could not rename \"%s\" to \"%s\": %m", path, dest); + pg_fatal("could not rename file \"%s\" to \"%s\": %m", path, dest); /* * We don't fsync() the database files in the file synchronization @@ -450,7 +442,7 @@ swap_catalog_files(FileNameMap *maps, int size, const char *old_catalog_dir, * during pg_restore. */ static void -do_swap(FileNameMap *maps, int size, char *old_tablespace) +do_swap(FileNameMap *maps, int size, char *old_tablespace, char *new_tablespace) { char old_catalog_dir[MAXPGPATH]; char new_db_dir[MAXPGPATH]; @@ -470,21 +462,23 @@ do_swap(FileNameMap *maps, int size, char *old_tablespace) */ if (old_tablespace) { - if (prepare_for_swap(old_tablespace, maps[0].db_oid, + if (prepare_for_swap(old_tablespace, new_tablespace, maps[0].db_oid, old_catalog_dir, new_db_dir, moved_db_dir)) swap_catalog_files(maps, size, old_catalog_dir, new_db_dir, moved_db_dir); } else { - if (prepare_for_swap(old_cluster.pgdata, maps[0].db_oid, + if (prepare_for_swap(old_cluster.pgdata, new_cluster.pgdata, maps[0].db_oid, old_catalog_dir, new_db_dir, moved_db_dir)) swap_catalog_files(maps, size, old_catalog_dir, new_db_dir, moved_db_dir); - for (int tblnum = 0; tblnum < os_info.num_old_tablespaces; tblnum++) + for (int tblnum = 0; tblnum < old_cluster.num_tablespaces; tblnum++) { - if (prepare_for_swap(os_info.old_tablespaces[tblnum], maps[0].db_oid, + if (prepare_for_swap(old_cluster.tablespaces[tblnum], + new_cluster.tablespaces[tblnum], + maps[0].db_oid, old_catalog_dir, new_db_dir, moved_db_dir)) swap_catalog_files(maps, size, old_catalog_dir, new_db_dir, moved_db_dir); @@ -498,7 +492,8 @@ do_swap(FileNameMap *maps, int size, char *old_tablespace) * create links for mappings stored in "maps" array. */ static void -transfer_single_new_db(FileNameMap *maps, int size, char *old_tablespace) +transfer_single_new_db(FileNameMap *maps, int size, + char *old_tablespace, char *new_tablespace) { int mapnum; bool vm_must_add_frozenbit = false; @@ -520,7 +515,7 @@ transfer_single_new_db(FileNameMap *maps, int size, char *old_tablespace) */ Assert(!vm_must_add_frozenbit); - do_swap(maps, size, old_tablespace); + do_swap(maps, size, old_tablespace, new_tablespace); return; } diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c index 873e5b5117bf9..1eb8bc97c051e 100644 --- a/src/bin/pg_upgrade/server.c +++ b/src/bin/pg_upgrade/server.c @@ -3,7 +3,7 @@ * * database server functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/server.c */ @@ -71,6 +71,8 @@ get_db_conn(ClusterInfo *cluster, const char *db_name) appendPQExpBufferStr(&conn_opts, " host="); appendConnStrVal(&conn_opts, cluster->sockdir); } + if (!protocol_negotiation_supported(cluster)) + appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0"); conn = PQconnectdb(conn_opts.data); termPQExpBuffer(&conn_opts); @@ -148,45 +150,6 @@ executeQueryOrDie(PGconn *conn, const char *fmt,...) } -/* - * get_major_server_version() - * - * gets the version (in unsigned int form) for the given datadir. Assumes - * that datadir is an absolute path to a valid pgdata directory. The version - * is retrieved by reading the PG_VERSION file. - */ -uint32 -get_major_server_version(ClusterInfo *cluster) -{ - FILE *version_fd; - char ver_filename[MAXPGPATH]; - int v1 = 0, - v2 = 0; - - snprintf(ver_filename, sizeof(ver_filename), "%s/PG_VERSION", - cluster->pgdata); - if ((version_fd = fopen(ver_filename, "r")) == NULL) - pg_fatal("could not open version file \"%s\": %m", ver_filename); - - if (fscanf(version_fd, "%63s", cluster->major_version_str) == 0 || - sscanf(cluster->major_version_str, "%d.%d", &v1, &v2) < 1) - pg_fatal("could not parse version file \"%s\"", ver_filename); - - fclose(version_fd); - - if (v1 < 10) - { - /* old style, e.g. 9.6.1 */ - return v1 * 10000 + v2 * 100; - } - else - { - /* new style, e.g. 10.1 */ - return v1 * 10000; - } -} - - static void stop_postmaster_atexit(void) { @@ -241,24 +204,6 @@ start_postmaster(ClusterInfo *cluster, bool report_and_exit_on_error) if (cluster == &new_cluster) appendPQExpBufferStr(&pgoptions, " -c synchronous_commit=off -c fsync=off -c full_page_writes=off"); - /* - * Use max_slot_wal_keep_size as -1 to prevent the WAL removal by the - * checkpointer process. If WALs required by logical replication slots - * are removed, the slots are unusable. This setting prevents the - * invalidation of slots during the upgrade. We set this option when - * cluster is PG17 or later because logical replication slots can only be - * migrated since then. Besides, max_slot_wal_keep_size is added in PG13. - */ - if (GET_MAJOR_VERSION(cluster->major_version) >= 1700) - appendPQExpBufferStr(&pgoptions, " -c max_slot_wal_keep_size=-1"); - - /* - * Use idle_replication_slot_timeout=0 to prevent slot invalidation due to - * idle_timeout by checkpointer process during upgrade. - */ - if (GET_MAJOR_VERSION(cluster->major_version) >= 1800) - appendPQExpBufferStr(&pgoptions, " -c idle_replication_slot_timeout=0"); - /* * Use -b to disable autovacuum and logical replication launcher * (effective in PG17 or later for the latter). diff --git a/src/bin/pg_upgrade/slru_io.c b/src/bin/pg_upgrade/slru_io.c new file mode 100644 index 0000000000000..2188a287850d8 --- /dev/null +++ b/src/bin/pg_upgrade/slru_io.c @@ -0,0 +1,269 @@ +/* + * slru_io.c + * + * Routines for reading and writing SLRU files during upgrade. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * src/bin/pg_upgrade/slru_io.c + */ + +#include "postgres_fe.h" + +#include + +#include "common/fe_memutils.h" +#include "common/file_perm.h" +#include "common/file_utils.h" +#include "pg_upgrade.h" +#include "port/pg_iovec.h" +#include "slru_io.h" + +static SlruSegState *AllocSlruSegState(const char *dir); +static char *SlruFileName(SlruSegState *state, int64 segno); +static void SlruFlush(SlruSegState *state); + +/* common parts of AllocSlruRead and AllocSlruWrite */ +static SlruSegState * +AllocSlruSegState(const char *dir) +{ + SlruSegState *state = pg_malloc_object(SlruSegState); + + state->dir = pstrdup(dir); + state->fn = NULL; + state->fd = -1; + state->segno = -1; + state->pageno = 0; + + /* state->writing and state->long_segment_names must be set by caller! */ + + return state; +} + +/* similar to the backend function with the same name */ +static char * +SlruFileName(SlruSegState *state, int64 segno) +{ + if (state->long_segment_names) + { + Assert(segno >= 0 && segno <= INT64CONST(0xFFFFFFFFFFFFFFF)); + return psprintf("%s/%015" PRIX64, state->dir, segno); + } + else + { + Assert(segno >= 0 && segno <= INT64CONST(0xFFFFFF)); + return psprintf("%s/%04X", state->dir, (unsigned int) segno); + } +} + +/* + * Create SLRU reader for dir. + */ +SlruSegState * +AllocSlruRead(const char *dir, bool long_segment_names) +{ + SlruSegState *state = AllocSlruSegState(dir); + + state->writing = false; + state->long_segment_names = long_segment_names; + + return state; +} + +/* + * Read the given page into memory buffer. + * + * Reading can be done in random order. + * + * If the file containing 'pageno' does not exist, a fatal error is raised. + * If the file exists but is shorter than expected, the missing part is read + * as zeros and a warning is logged. That is reasonable behavior for current + * callers. + * + * This is the slow path of the inlineable SlruReadSwitchPage() function. + */ +char * +SlruReadSwitchPageSlow(SlruSegState *state, uint64 pageno) +{ + int64 segno; + off_t offset; + ssize_t bytes_read; + + Assert(!state->writing); /* read only mode */ + + if (state->segno != -1 && pageno == state->pageno) + return state->buf.data; + + /* If the new page is on a different SLRU segment, open the new segment */ + segno = pageno / SLRU_PAGES_PER_SEGMENT; + if (segno != state->segno) + { + if (state->segno != -1) + { + close(state->fd); + state->fd = -1; + + pg_free(state->fn); + state->fn = NULL; + + state->segno = -1; + } + + state->fn = SlruFileName(state, segno); + if ((state->fd = open(state->fn, O_RDONLY | PG_BINARY, 0)) < 0) + pg_fatal("could not open file \"%s\": %m", state->fn); + state->segno = segno; + } + + offset = (pageno % SLRU_PAGES_PER_SEGMENT) * BLCKSZ; + bytes_read = 0; + while (bytes_read < BLCKSZ) + { + ssize_t rc; + + rc = pg_pread(state->fd, + &state->buf.data[bytes_read], + BLCKSZ - bytes_read, + offset); + if (rc < 0) + { + if (errno == EINTR) + continue; + pg_fatal("could not read file \"%s\": %m", state->fn); + } + if (rc == 0) + { + /* unexpected EOF */ + pg_log(PG_WARNING, "unexpected EOF reading file \"%s\" at offset %u, reading as zeros", + state->fn, (unsigned int) offset); + memset(&state->buf.data[bytes_read], 0, BLCKSZ - bytes_read); + break; + } + bytes_read += rc; + offset += rc; + } + state->pageno = pageno; + + return state->buf.data; +} + +/* + * Free the reader. + */ +void +FreeSlruRead(SlruSegState *state) +{ + Assert(!state->writing); /* read only mode */ + + if (state->fd != -1) + close(state->fd); + pg_free(state); +} + +/* + * Create SLRU writer for dir. + */ +SlruSegState * +AllocSlruWrite(const char *dir, bool long_segment_names) +{ + SlruSegState *state = AllocSlruSegState(dir); + + state->writing = true; + state->long_segment_names = long_segment_names; + + return state; +} + +/* + * Open the given page for writing. + * + * NOTE: This uses O_EXCL when stepping to a new segment, so this assumes that + * each segment is written in full before moving on to the next one. This + * limitation would be easy to lift if needed, but it fits the usage pattern + * of current callers. + * + * This is the slow path of the inlineable SlruWriteSwitchPage() function. + */ +char * +SlruWriteSwitchPageSlow(SlruSegState *state, uint64 pageno) +{ + int64 segno; + off_t offset; + + Assert(state->writing); + + if (state->segno != -1 && pageno == state->pageno) + return state->buf.data; + + segno = pageno / SLRU_PAGES_PER_SEGMENT; + offset = (pageno % SLRU_PAGES_PER_SEGMENT) * BLCKSZ; + + SlruFlush(state); + memset(state->buf.data, 0, BLCKSZ); + + if (segno != state->segno) + { + if (state->segno != -1) + { + close(state->fd); + state->fd = -1; + + pg_free(state->fn); + state->fn = NULL; + + state->segno = -1; + } + + /* Create the segment */ + state->fn = SlruFileName(state, segno); + if ((state->fd = open(state->fn, O_RDWR | O_CREAT | O_EXCL | PG_BINARY, + pg_file_create_mode)) < 0) + { + pg_fatal("could not create file \"%s\": %m", state->fn); + } + + state->segno = segno; + + if (offset > 0) + { + if (pg_pwrite_zeros(state->fd, offset, 0) < 0) + pg_fatal("could not write file \"%s\": %m", state->fn); + } + } + + state->pageno = pageno; + + return state->buf.data; +} + +static void +SlruFlush(SlruSegState *state) +{ + struct iovec iovec = { + .iov_base = &state->buf, + .iov_len = BLCKSZ, + }; + off_t offset; + + if (state->segno == -1) + return; + + offset = (state->pageno % SLRU_PAGES_PER_SEGMENT) * BLCKSZ; + + if (pg_pwritev_with_retry(state->fd, &iovec, 1, offset) < 0) + pg_fatal("could not write file \"%s\": %m", state->fn); +} + +/* + * Free the writer. + */ +void +FreeSlruWrite(SlruSegState *state) +{ + Assert(state->writing); + + SlruFlush(state); + + if (state->fd != -1) + close(state->fd); + pg_free(state); +} diff --git a/src/bin/pg_upgrade/slru_io.h b/src/bin/pg_upgrade/slru_io.h new file mode 100644 index 0000000000000..0be83b286159d --- /dev/null +++ b/src/bin/pg_upgrade/slru_io.h @@ -0,0 +1,52 @@ +/* + * slru_io.h + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * src/bin/pg_upgrade/slru_io.h + */ + +#ifndef SLRU_IO_H +#define SLRU_IO_H + +/* + * State for reading or writing an SLRU, with a one page buffer. + */ +typedef struct SlruSegState +{ + bool writing; + bool long_segment_names; + + char *dir; + char *fn; + int fd; + int64 segno; + uint64 pageno; + + PGAlignedBlock buf; +} SlruSegState; + +extern SlruSegState *AllocSlruRead(const char *dir, bool long_segment_names); +extern char *SlruReadSwitchPageSlow(SlruSegState *state, uint64 pageno); +extern void FreeSlruRead(SlruSegState *state); + +static inline char * +SlruReadSwitchPage(SlruSegState *state, uint64 pageno) +{ + if (state->segno != -1 && pageno == state->pageno) + return state->buf.data; + return SlruReadSwitchPageSlow(state, pageno); +} + +extern SlruSegState *AllocSlruWrite(const char *dir, bool long_segment_names); +extern char *SlruWriteSwitchPageSlow(SlruSegState *state, uint64 pageno); +extern void FreeSlruWrite(SlruSegState *state); + +static inline char * +SlruWriteSwitchPage(SlruSegState *state, uint64 pageno) +{ + if (state->segno != -1 && pageno == state->pageno) + return state->buf.data; + return SlruWriteSwitchPageSlow(state, pageno); +} + +#endif /* SLRU_IO_H */ diff --git a/src/bin/pg_upgrade/t/001_basic.pl b/src/bin/pg_upgrade/t/001_basic.pl index 1eec35d436deb..4dc0c2d4d377b 100644 --- a/src/bin/pg_upgrade/t/001_basic.pl +++ b/src/bin/pg_upgrade/t/001_basic.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_upgrade/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl index 7d82593879d57..0a4121fdc4d9f 100644 --- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl +++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Set of tests for pg_upgrade, including cross-version checks. use strict; @@ -86,6 +86,7 @@ sub get_dump_for_comparison $node->run_log( [ 'pg_dump', '--no-sync', + '--restrict-key' => 'test', '-d' => $node->connstr($db), '-f' => $dumpfile ]); @@ -224,6 +225,10 @@ sub get_dump_for_comparison # Override log_statement=all set by Cluster.pm. This avoids large amounts # of log traffic that slow this test down even more when run under valgrind. $oldnode->append_conf('postgresql.conf', 'log_statement = none'); + +# Set wal_level = replica to run the regression tests in the same +# wal_level as when 'make check' runs. +$oldnode->append_conf('postgresql.conf', 'wal_level = replica'); $oldnode->start; my $result; @@ -275,32 +280,20 @@ sub get_dump_for_comparison # --inputdir points to the path of the input files. my $inputdir = "$srcdir/src/test/regress"; - note 'running regression tests in old instance'; - my $rc = - system($ENV{PG_REGRESS} - . " $extra_opts " - . "--dlpath=\"$dlpath\" " - . "--bindir= " - . "--host=" - . $oldnode->host . " " - . "--port=" - . $oldnode->port . " " - . "--schedule=$srcdir/src/test/regress/parallel_schedule " - . "--max-concurrent-tests=20 " - . "--inputdir=\"$inputdir\" " - . "--outputdir=\"$outputdir\""); - if ($rc != 0) - { - # Dump out the regression diffs file, if there is one - my $diffs = "$outputdir/regression.diffs"; - if (-e $diffs) - { - print "=== dumping $diffs ===\n"; - print slurp_file($diffs); - print "=== EOF ===\n"; - } - } - is($rc, 0, 'regression tests pass'); + command_ok( + [ + $ENV{PG_REGRESS}, + split(' ', $extra_opts), + "--dlpath=$dlpath", + '--bindir=', + '--host=' . $oldnode->host, + '--port=' . $oldnode->port, + "--schedule=$srcdir/src/test/regress/parallel_schedule", + '--max-concurrent-tests=20', + "--inputdir=$inputdir", + "--outputdir=$outputdir" + ], + 'regression tests in old instance'); } # Initialize a new node for the upgrade. @@ -375,6 +368,9 @@ sub get_dump_for_comparison { my $dstnode = PostgreSQL::Test::Cluster->new('dst_node'); + skip "regress_dump_restore not enabled in PG_TEST_EXTRA" + if (!$ENV{PG_TEST_EXTRA} + || $ENV{PG_TEST_EXTRA} !~ /\bregress_dump_restore\b/); skip "different Postgres versions" if ($oldnode->pg_version != $dstnode->pg_version); skip "source node not using default install" @@ -424,6 +420,7 @@ sub get_dump_for_comparison # that we need to use pg_dumpall from the new node here. my @dump_command = ( 'pg_dumpall', '--no-sync', + '--restrict-key' => 'test', '--dbname' => $oldnode->connstr('postgres'), '--file' => $dump1_file); # --extra-float-digits is needed when upgrading from a version older than 11. @@ -621,6 +618,7 @@ sub get_dump_for_comparison # Second dump from the upgraded instance. @dump_command = ( 'pg_dumpall', '--no-sync', + '--restrict-key' => 'test', '--dbname' => $newnode->connstr('postgres'), '--file' => $dump2_file); # --extra-float-digits is needed when upgrading from a version older than 11. diff --git a/src/bin/pg_upgrade/t/003_logical_slots.pl b/src/bin/pg_upgrade/t/003_logical_slots.pl index 75db4911730be..15e6d267f2f11 100644 --- a/src/bin/pg_upgrade/t/003_logical_slots.pl +++ b/src/bin/pg_upgrade/t/003_logical_slots.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Tests for upgrading logical replication slots @@ -64,6 +64,7 @@ 'postgres', qq[ SELECT pg_create_logical_replication_slot('test_slot1', 'test_decoding'); SELECT pg_create_logical_replication_slot('test_slot2', 'test_decoding'); + SELECT pg_create_logical_replication_slot('test_slot3', 'test_decoding'); ]); $oldpub->stop(); @@ -77,7 +78,7 @@ [@pg_upgrade_cmd], 1, [ - qr/"max_replication_slots" \(1\) must be greater than or equal to the number of logical replication slots \(2\) on the old cluster/ + qr/"max_replication_slots" \(1\) must be greater than or equal to the number of logical replication slots \(3\) on the old cluster/ ], [qr//], 'run of pg_upgrade where the new cluster has insufficient "max_replication_slots"' @@ -85,29 +86,31 @@ ok(-d $newpub->data_dir . "/pg_upgrade_output.d", "pg_upgrade_output.d/ not removed after pg_upgrade failure"); -# Set 'max_replication_slots' to match the number of slots (2) present on the +# Set 'max_replication_slots' to match the number of slots (3) present on the # old cluster. Both slots will be used for subsequent tests. -$newpub->append_conf('postgresql.conf', "max_replication_slots = 2"); +$newpub->append_conf('postgresql.conf', "max_replication_slots = 3"); # ------------------------------ # TEST: Confirm pg_upgrade fails when the slot still has unconsumed WAL records # Preparations for the subsequent test: -# 1. Generate extra WAL records. At this point neither test_slot1 nor -# test_slot2 has consumed them. +# 1. Generate extra WAL records. At this point none of the slots has consumed them. # # 2. Advance the slot test_slot2 up to the current WAL location, but test_slot1 # still has unconsumed WAL records. # # 3. Emit a non-transactional message. This will cause test_slot2 to detect the # unconsumed WAL record. +# +# 4. Advance the slot test_slots3 up to the current WAL location. $oldpub->start; $oldpub->safe_psql( 'postgres', qq[ CREATE TABLE tbl AS SELECT generate_series(1, 10) AS a; SELECT pg_replication_slot_advance('test_slot2', pg_current_wal_lsn()); - SELECT count(*) FROM pg_logical_emit_message('false', 'prefix', 'This is a non-transactional message'); + SELECT count(*) FROM pg_logical_emit_message('false', 'prefix', 'This is a non-transactional message', true); + SELECT pg_replication_slot_advance('test_slot3', pg_current_wal_lsn()); ]); $oldpub->stop; @@ -138,8 +141,9 @@ }, $newpub->data_dir . "/pg_upgrade_output.d"); -# Check the file content. Both slots should be reporting that they have -# unconsumed WAL records. +# Check the file content. While both test_slot1 and test_slot2 should be reporting +# that they have unconsumed WAL records, test_slot3 should not be reported as +# it has caught up. like( slurp_file($slots_filename), qr/The slot \"test_slot1\" has not consumed the WAL yet/m, @@ -148,6 +152,10 @@ slurp_file($slots_filename), qr/The slot \"test_slot2\" has not consumed the WAL yet/m, 'the previous test failed due to unconsumed WALs'); +unlike( + slurp_file($slots_filename), + qr/test_slot3/m, + 'caught-up slot is not reported'); # ------------------------------ @@ -162,6 +170,7 @@ 'postgres', qq[ SELECT * FROM pg_drop_replication_slot('test_slot1'); SELECT * FROM pg_drop_replication_slot('test_slot2'); + SELECT * FROM pg_drop_replication_slot('test_slot3'); CREATE PUBLICATION regress_pub FOR ALL TABLES; ]); diff --git a/src/bin/pg_upgrade/t/004_subscription.pl b/src/bin/pg_upgrade/t/004_subscription.pl index c545abf65816e..f68821df2a30e 100644 --- a/src/bin/pg_upgrade/t/004_subscription.pl +++ b/src/bin/pg_upgrade/t/004_subscription.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Test for pg_upgrade of logical subscription. Note that after the successful # upgrade test, we can't use the old cluster to prevent failing in --link mode. @@ -22,13 +22,13 @@ # Initialize the old subscriber node my $old_sub = PostgreSQL::Test::Cluster->new('old_sub'); -$old_sub->init; +$old_sub->init(allows_streaming => 'physical'); $old_sub->start; my $oldbindir = $old_sub->config_data('--bindir'); # Initialize the new subscriber my $new_sub = PostgreSQL::Test::Cluster->new('new_sub'); -$new_sub->init; +$new_sub->init(allows_streaming => 'physical'); my $newbindir = $new_sub->config_data('--bindir'); # In a VPATH build, we'll be started in the source directory, but we want @@ -53,7 +53,8 @@ $old_sub->stop; -$new_sub->append_conf('postgresql.conf', "max_active_replication_origins = 0"); +$new_sub->append_conf('postgresql.conf', + "max_active_replication_origins = 0"); # pg_upgrade will fail because the new cluster has insufficient # max_active_replication_origins. @@ -80,7 +81,56 @@ ); # Reset max_active_replication_origins -$new_sub->append_conf('postgresql.conf', "max_active_replication_origins = 10"); +$new_sub->append_conf('postgresql.conf', + "max_active_replication_origins = 10"); + +# Cleanup +$publisher->safe_psql('postgres', "DROP PUBLICATION regress_pub1"); +$old_sub->start; +$old_sub->safe_psql('postgres', "DROP SUBSCRIPTION regress_sub1;"); + +# ------------------------------------------------------ +# Check that pg_upgrade fails when max_replication_slots configured in the new +# cluster is less than the number of logical slots in the old cluster + 1 when +# subscription's retain_dead_tuples option is enabled. +# ------------------------------------------------------ +# It is sufficient to use disabled subscription to test upgrade failure. + +$publisher->safe_psql('postgres', "CREATE PUBLICATION regress_pub1"); +$old_sub->safe_psql('postgres', + "CREATE SUBSCRIPTION regress_sub1 CONNECTION '$connstr' PUBLICATION regress_pub1 WITH (enabled = false, retain_dead_tuples = true)" +); + +$old_sub->stop; + +$new_sub->append_conf('postgresql.conf', 'max_replication_slots = 0'); + +# pg_upgrade will fail because the new cluster has insufficient +# max_replication_slots. +command_checks_all( + [ + 'pg_upgrade', + '--no-sync', + '--old-datadir' => $old_sub->data_dir, + '--new-datadir' => $new_sub->data_dir, + '--old-bindir' => $oldbindir, + '--new-bindir' => $newbindir, + '--socketdir' => $new_sub->host, + '--old-port' => $old_sub->port, + '--new-port' => $new_sub->port, + $mode, + '--check', + ], + 1, + [ + qr/"max_replication_slots" \(0\) must be greater than or equal to the number of logical replication slots on the old cluster plus one additional slot required for retaining conflict detection information \(1\)/ + ], + [qr//], + 'run of pg_upgrade where the new cluster has insufficient max_replication_slots' +); + +# Reset max_replication_slots +$new_sub->append_conf('postgresql.conf', 'max_replication_slots = 10'); # Cleanup $publisher->safe_psql('postgres', "DROP PUBLICATION regress_pub1"); @@ -125,9 +175,9 @@ ); my $sub_oid = $old_sub->safe_psql('postgres', "SELECT oid FROM pg_subscription WHERE subname = 'regress_sub3'"); -my $reporigin = 'pg_' . qq($sub_oid); +my $replorigin = 'pg_' . qq($sub_oid); $old_sub->safe_psql('postgres', - "SELECT pg_replication_origin_drop('$reporigin')"); + "SELECT pg_replication_origin_drop('$replorigin')"); $old_sub->stop; @@ -198,23 +248,27 @@ rmtree($new_sub->data_dir . "/pg_upgrade_output.d"); # Verify that the upgrade should be successful with tables in 'ready'/'init' -# state along with retaining the replication origin's remote lsn, subscription's -# running status, and failover option. +# state along with retaining the replication origin's remote lsn, +# subscription's running status, failover option, and retain_dead_tuples +# option. Use multiple tables to verify deterministic pg_dump ordering +# of subscription relations during --binary-upgrade. $publisher->safe_psql( 'postgres', qq[ + CREATE TABLE tab_upgraded(id int); CREATE TABLE tab_upgraded1(id int); - CREATE PUBLICATION regress_pub4 FOR TABLE tab_upgraded1; + CREATE PUBLICATION regress_pub4 FOR TABLE tab_upgraded, tab_upgraded1; ]); $old_sub->safe_psql( 'postgres', qq[ + CREATE TABLE tab_upgraded(id int); CREATE TABLE tab_upgraded1(id int); - CREATE SUBSCRIPTION regress_sub4 CONNECTION '$connstr' PUBLICATION regress_pub4 WITH (failover = true); + CREATE SUBSCRIPTION regress_sub4 CONNECTION '$connstr' PUBLICATION regress_pub4 WITH (failover = true, retain_dead_tuples = true); ]); -# Wait till the table tab_upgraded1 reaches 'ready' state +# Wait till the tables tab_upgraded and tab_upgraded1 reach 'ready' state my $synced_query = - "SELECT count(1) = 1 FROM pg_subscription_rel WHERE srsubstate = 'r'"; + "SELECT count(1) = 2 FROM pg_subscription_rel WHERE srsubstate = 'r'"; $old_sub->poll_query_until('postgres', $synced_query) or die "Timed out while waiting for the table to reach ready state"; @@ -252,6 +306,8 @@ # Have the subscription in disabled state before upgrade $old_sub->safe_psql('postgres', "ALTER SUBSCRIPTION regress_sub5 DISABLE"); +my $tab_upgraded_oid = $old_sub->safe_psql('postgres', + "SELECT oid FROM pg_class WHERE relname = 'tab_upgraded'"); my $tab_upgraded1_oid = $old_sub->safe_psql('postgres', "SELECT oid FROM pg_class WHERE relname = 'tab_upgraded1'"); my $tab_upgraded2_oid = $old_sub->safe_psql('postgres', @@ -266,9 +322,10 @@ # ------------------------------------------------------ # Check that pg_upgrade is successful when all tables are in ready or in -# init state (tab_upgraded1 table is in ready state and tab_upgraded2 table is -# in init state) along with retaining the replication origin's remote lsn, -# subscription's running status, and failover option. +# init state (tab_upgraded and tab_upgraded1 tables are in ready state and +# tab_upgraded2 table is in init state) along with retaining the replication +# origin's remote lsn, subscription's running status, failover option, and +# retain_dead_tuples option. # ------------------------------------------------------ command_ok( [ @@ -291,7 +348,8 @@ # ------------------------------------------------------ # Check that the data inserted to the publisher when the new subscriber is down # will be replicated once it is started. Also check that the old subscription -# states and relations origins are all preserved. +# states and relations origins are all preserved, and that the conflict +# detection slot is created. # ------------------------------------------------------ $publisher->safe_psql( 'postgres', qq[ @@ -301,23 +359,25 @@ $new_sub->start; -# The subscription's running status and failover option should be preserved -# in the upgraded instance. So regress_sub4 should still have subenabled and -# subfailover set to true, while regress_sub5 should have both set to false. +# The subscription's running status, failover option, and retain_dead_tuples +# option should be preserved in the upgraded instance. So regress_sub4 should +# still have subenabled, subfailover, and subretaindeadtuples set to true, +# while regress_sub5 should have both set to false. $result = $new_sub->safe_psql('postgres', - "SELECT subname, subenabled, subfailover FROM pg_subscription ORDER BY subname" + "SELECT subname, subenabled, subfailover, subretaindeadtuples FROM pg_subscription ORDER BY subname" ); -is( $result, qq(regress_sub4|t|t -regress_sub5|f|f), - "check that the subscription's running status and failover are preserved" +is( $result, qq(regress_sub4|t|t|t +regress_sub5|f|f|f), + "check that the subscription's running status, failover, and retain_dead_tuples are preserved" ); # Subscription relations should be preserved $result = $new_sub->safe_psql('postgres', "SELECT srrelid, srsubstate FROM pg_subscription_rel ORDER BY srrelid"); -is( $result, qq($tab_upgraded1_oid|r +is( $result, qq($tab_upgraded_oid|r +$tab_upgraded1_oid|r $tab_upgraded2_oid|i), - "there should be 2 rows in pg_subscription_rel(representing tab_upgraded1 and tab_upgraded2)" + "there should be 3 rows in pg_subscription_rel(representing tab_upgraded, tab_upgraded1 and tab_upgraded2)" ); # The replication origin's remote_lsn should be preserved @@ -328,6 +388,11 @@ ); is($result, qq($remote_lsn), "remote_lsn should have been preserved"); +# The conflict detection slot should be created +$result = $new_sub->safe_psql('postgres', + "SELECT xmin IS NOT NULL from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'"); +is($result, qq(t), "conflict detection slot exists"); + # Resume the initial sync and wait until all tables of subscription # 'regress_sub5' are synchronized $new_sub->append_conf('postgresql.conf', diff --git a/src/bin/pg_upgrade/t/005_char_signedness.pl b/src/bin/pg_upgrade/t/005_char_signedness.pl index 17fa0d48b15c1..8f55dc523e86b 100644 --- a/src/bin/pg_upgrade/t/005_char_signedness.pl +++ b/src/bin/pg_upgrade/t/005_char_signedness.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group # Tests for handling the default char signedness during upgrade. @@ -65,7 +65,7 @@ $mode ], 1, - [qr/--set-char-signedness option cannot be used/], + [qr/option --set-char-signedness cannot be used/], [], '--set-char-signedness option cannot be used for upgrading from v18 or later' ); diff --git a/src/bin/pg_upgrade/t/006_transfer_modes.pl b/src/bin/pg_upgrade/t/006_transfer_modes.pl index 550a63fdf7d47..d317159bf1283 100644 --- a/src/bin/pg_upgrade/t/006_transfer_modes.pl +++ b/src/bin/pg_upgrade/t/006_transfer_modes.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group # Tests for file transfer modes @@ -13,7 +13,8 @@ sub test_mode { my ($mode) = @_; - my $old = PostgreSQL::Test::Cluster->new('old', install_path => $ENV{oldinstall}); + my $old = + PostgreSQL::Test::Cluster->new('old', install_path => $ENV{oldinstall}); my $new = PostgreSQL::Test::Cluster->new('new'); # --swap can't be used to upgrade from versions older than 10, so just skip @@ -37,24 +38,89 @@ sub test_mode } $new->init(); + # allow_in_place_tablespaces is available as far back as v10. + if ($old->pg_version >= 10) + { + $new->append_conf('postgresql.conf', "allow_in_place_tablespaces = true"); + $old->append_conf('postgresql.conf', "allow_in_place_tablespaces = true"); + } + + # We can only test security labels if both the old and new installations + # have dummy_seclabel. + my $test_seclabel = 1; + $old->start; + if (!$old->check_extension('dummy_seclabel')) + { + $test_seclabel = 0; + } + $old->stop; + $new->start; + if (!$new->check_extension('dummy_seclabel')) + { + $test_seclabel = 0; + } + $new->stop; + # Create a small variety of simple test objects on the old cluster. We'll # check that these reach the new version after upgrading. $old->start; - $old->safe_psql('postgres', "CREATE TABLE test1 AS SELECT generate_series(1, 100)"); + $old->safe_psql('postgres', + "CREATE TABLE test1 AS SELECT generate_series(1, 100)"); $old->safe_psql('postgres', "CREATE DATABASE testdb1"); - $old->safe_psql('testdb1', "CREATE TABLE test2 AS SELECT generate_series(200, 300)"); + $old->safe_psql('testdb1', + "CREATE TABLE test2 AS SELECT generate_series(200, 300)"); $old->safe_psql('testdb1', "VACUUM FULL test2"); $old->safe_psql('testdb1', "CREATE SEQUENCE testseq START 5432"); - # For cross-version tests, we can also check that pg_upgrade handles - # tablespaces. + # If an old installation is provided, we can test non-in-place tablespaces. if (defined($ENV{oldinstall})) { my $tblspc = PostgreSQL::Test::Utils::tempdir_short(); - $old->safe_psql('postgres', "CREATE TABLESPACE test_tblspc LOCATION '$tblspc'"); - $old->safe_psql('postgres', "CREATE DATABASE testdb2 TABLESPACE test_tblspc"); - $old->safe_psql('postgres', "CREATE TABLE test3 TABLESPACE test_tblspc AS SELECT generate_series(300, 401)"); - $old->safe_psql('testdb2', "CREATE TABLE test4 AS SELECT generate_series(400, 502)"); + $old->safe_psql('postgres', + "CREATE TABLESPACE test_tblspc LOCATION '$tblspc'"); + $old->safe_psql('postgres', + "CREATE DATABASE testdb2 TABLESPACE test_tblspc"); + $old->safe_psql('postgres', + "CREATE TABLE test3 TABLESPACE test_tblspc AS SELECT generate_series(300, 401)" + ); + $old->safe_psql('testdb2', + "CREATE TABLE test4 AS SELECT generate_series(400, 502)"); + } + + # If the old cluster is >= v10, we can test in-place tablespaces. + if ($old->pg_version >= 10) + { + $old->safe_psql('postgres', + "CREATE TABLESPACE inplc_tblspc LOCATION ''"); + $old->safe_psql('postgres', + "CREATE DATABASE testdb3 TABLESPACE inplc_tblspc"); + $old->safe_psql('postgres', + "CREATE TABLE test5 TABLESPACE inplc_tblspc AS SELECT generate_series(503, 606)"); + $old->safe_psql('testdb3', + "CREATE TABLE test6 AS SELECT generate_series(607, 711)"); + } + + # While we are here, test handling of large objects. + $old->safe_psql('postgres', q| + CREATE ROLE regress_lo_1; + CREATE ROLE regress_lo_2; + + SELECT lo_from_bytea(4532, '\xffffff00'); + COMMENT ON LARGE OBJECT 4532 IS 'test'; + + SELECT lo_from_bytea(4533, '\x0f0f0f0f'); + ALTER LARGE OBJECT 4533 OWNER TO regress_lo_1; + GRANT SELECT ON LARGE OBJECT 4533 TO regress_lo_2; + |); + + if ($test_seclabel) + { + $old->safe_psql('postgres', q| + CREATE EXTENSION dummy_seclabel; + + SELECT lo_from_bytea(4534, '\x00ffffff'); + SECURITY LABEL ON LARGE OBJECT 4534 IS 'classified'; + |); } $old->stop; @@ -86,15 +152,53 @@ sub test_mode $result = $new->safe_psql('testdb1', "SELECT nextval('testseq')"); is($result, '5432', "sequence data after pg_upgrade $mode"); - # For cross-version tests, we should have some objects in a non-default - # tablespace. + # Tests for non-in-place tablespaces. if (defined($ENV{oldinstall})) { - $result = $new->safe_psql('postgres', "SELECT COUNT(*) FROM test3"); + $result = + $new->safe_psql('postgres', "SELECT COUNT(*) FROM test3"); is($result, '102', "test3 data after pg_upgrade $mode"); - $result = $new->safe_psql('testdb2', "SELECT COUNT(*) FROM test4"); + $result = + $new->safe_psql('testdb2', "SELECT COUNT(*) FROM test4"); is($result, '103', "test4 data after pg_upgrade $mode"); } + + # Tests for in-place tablespaces. + if ($old->pg_version >= 10) + { + $result = $new->safe_psql('postgres', "SELECT COUNT(*) FROM test5"); + is($result, '104', "test5 data after pg_upgrade $mode"); + $result = $new->safe_psql('testdb3', "SELECT COUNT(*) FROM test6"); + is($result, '105', "test6 data after pg_upgrade $mode"); + } + + # Tests for large objects + $result = $new->safe_psql('postgres', "SELECT lo_get(4532)"); + is($result, '\xffffff00', "LO contents after upgrade"); + $result = $new->safe_psql('postgres', + "SELECT obj_description(4532, 'pg_largeobject')"); + is($result, 'test', "comment on LO after pg_upgrade"); + + $result = $new->safe_psql('postgres', "SELECT lo_get(4533)"); + is($result, '\x0f0f0f0f', "LO contents after upgrade"); + $result = $new->safe_psql('postgres', + "SELECT lomowner::regrole FROM pg_largeobject_metadata WHERE oid = 4533"); + is($result, 'regress_lo_1', "LO owner after upgrade"); + $result = $new->safe_psql('postgres', + "SELECT lomacl FROM pg_largeobject_metadata WHERE oid = 4533"); + is($result, '{regress_lo_1=rw/regress_lo_1,regress_lo_2=r/regress_lo_1}', + "LO ACL after upgrade"); + + if ($test_seclabel) + { + $result = $new->safe_psql('postgres', "SELECT lo_get(4534)"); + is($result, '\x00ffffff', "LO contents after upgrade"); + $result = $new->safe_psql('postgres', q| + SELECT label FROM pg_seclabel WHERE objoid = 4534 + AND classoid = 'pg_largeobject'::regclass + |); + is($result, 'classified', "seclabel on LO after pg_upgrade"); + } $new->stop; } diff --git a/src/bin/pg_upgrade/t/007_multixact_conversion.pl b/src/bin/pg_upgrade/t/007_multixact_conversion.pl new file mode 100644 index 0000000000000..867a062315316 --- /dev/null +++ b/src/bin/pg_upgrade/t/007_multixact_conversion.pl @@ -0,0 +1,443 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Version 19 expanded MultiXactOffset from 32 to 64 bits. Upgrading +# across that requires rewriting the SLRU files to the new format. +# This file contains tests for the conversion. +# +# To run, set 'oldinstall' ENV variable to point to a pre-v19 +# installation. If it's not set, or if it points to a v19 or above +# installation, this still performs a very basic test, upgrading a +# cluster with some multixacts. It's not very interesting, however, +# because there's no conversion involved in that case. + +use strict; +use warnings FATAL => 'all'; + +use Math::BigInt; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Temp dir for a dumps. +my $tempdir = PostgreSQL::Test::Utils::tempdir; + +# A workload that consumes multixids. The purpose of this is to +# generate some multixids in the old cluster, so that we can test +# upgrading them. The workload is a mix of KEY SHARE locking queries +# and UPDATEs, and commits and aborts, to generate a mix of multixids +# with different statuses. It consumes around 3000 multixids with +# 60000 members in total. That's enough to span more than one +# multixids 'offsets' page, and more than one 'members' segment with +# the default block size. +# +# The workload leaves behind a table called 'mxofftest' containing a +# small number of rows referencing some of the generated multixids. +# +# Because this function is used to generate test data on the old +# installation, it needs to work with older PostgreSQL server +# versions. +# +# The first argument is the cluster to connect to, the second argument +# is a cluster using the new version. We need the 'psql' binary from +# the new version, the new cluster is otherwise unused. (We need to +# use the new 'psql' because some of the more advanced background psql +# perl module features depend on a fairly recent psql version.) +sub mxact_workload +{ + my $node = shift; # Cluster to connect to + my $binnode = shift; # Use the psql binary from this cluster + + my $connstr = $node->connstr('postgres'); + + $node->start; + $node->safe_psql( + 'postgres', qq[ + CREATE TABLE mxofftest (id INT PRIMARY KEY, n_updated INT) + WITH (AUTOVACUUM_ENABLED=FALSE); + INSERT INTO mxofftest SELECT G, 0 FROM GENERATE_SERIES(1, 50) G; + ]); + + my $nclients = 20; + my $update_every = 13; + my $abort_every = 11; + my @connections = (); + + # Silence the logging of the statements we run to avoid + # unnecessarily bloating the test logs. This runs before the + # upgrade we're testing, so the details should not be very + # interesting for debugging. But if needed, you can make it more + # verbose by setting this. + my $verbose = 0; + + # Bump the timeout on the connections to avoid false negatives on + # slow test systems. The timeout covers the whole duration that + # the connections are open rather than the individual queries. + my $connection_timeout_secs = + 4 * $PostgreSQL::Test::Utils::timeout_default; + + # Open multiple connections to the database. Start a transaction + # in each connection. + for (0 .. $nclients) + { + # Use the psql binary from the new installation. The + # BackgroundPsql functionality doesn't work with older psql + # versions. + my $conn = $binnode->background_psql( + '', + connstr => $node->connstr('postgres'), + timeout => $connection_timeout_secs); + + $conn->query_safe("SET log_statement=none", verbose => $verbose) + unless $verbose; + $conn->query_safe("SET enable_seqscan=off", verbose => $verbose); + $conn->query_safe("BEGIN", verbose => $verbose); + + push(@connections, $conn); + } + + # Run queries using cycling through the connections in a + # round-robin fashion. We keep a transaction open in each + # connection at all times, and lock/update the rows. With 20 + # connections, each SELECT FOR KEY SHARE query generates a new + # multixid, containing the XIDs of all the transactions running at + # the time, ie. around 20 XIDs. + for (my $i = 0; $i < 3000; $i++) + { + note "generating multixids $i / 3000\n" if ($i % 100 == 0); + + my $conn = $connections[ $i % $nclients ]; + + my $sql = ($i % $abort_every == 0) ? "ABORT" : "COMMIT"; + $conn->query_safe($sql, verbose => $verbose); + + $conn->query_safe("BEGIN", verbose => $verbose); + if ($i % $update_every == 0) + { + $sql = qq[ + UPDATE mxofftest SET n_updated = n_updated + 1 WHERE id = ${i} % 50; + ]; + } + else + { + my $threshold = int($i / 3000 * 50); + $sql = qq[ + select count(*) from ( + SELECT * FROM mxofftest WHERE id >= $threshold FOR KEY SHARE + ) as x + ]; + } + $conn->query_safe($sql, verbose => $verbose); + } + + for my $conn (@connections) + { + $conn->quit(); + } + + $node->stop; + return; +} + +# Return contents of the 'mxofftest' table, created by mxact_workload +sub get_test_table_contents +{ + my ($node, $filename) = @_; + + my $contents = $node->safe_psql('postgres', + "SELECT ctid, xmin, xmax, * FROM mxofftest"); + + my $path = $tempdir . '/' . $filename; + open(my $fh, '>', $path) + || die "could not open $path for writing $!"; + print $fh $contents; + close($fh); + + return $path; +} + +# Return the members of all updating multixids in the given range +sub get_updating_multixact_members +{ + my ($node, $from, $to, $filename) = @_; + + my $path = $tempdir . '/' . $filename; + open(my $fh, '>', $path) + || die "could not open $path for writing $!"; + + if ($to >= $from) + { + my $res = $node->safe_psql( + 'postgres', qq[ + SELECT multi, mode, xid + FROM generate_series($from, $to - 1) as multi, + pg_get_multixact_members(multi::text::xid) + WHERE mode not in ('keysh', 'sh'); + ]); + print $fh $res; + } + else + { + # Multixids wrapped around. Split the query into two parts, + # before and after the wraparound. + my $res = $node->safe_psql( + 'postgres', qq[ + SELECT multi, mode, xid + FROM generate_series($from, 4294967295) as multi, + pg_get_multixact_members(multi::text::xid) + WHERE mode not in ('keysh', 'sh'); + ]); + print $fh $res; + $res = $node->safe_psql( + 'postgres', qq[ + SELECT multi, mode, xid + FROM generate_series(1, $to - 1) as multi, + pg_get_multixact_members(multi::text::xid) + WHERE mode not in ('keysh', 'sh'); + ]); + print $fh $res; + } + + close($fh); + return $path; +} + +# Read multixid related fields from the control file +# +# Note: This is used on both the old and the new installation, so the +# command arguments and the output parsing used here must work with +# all PostgreSQL versions supported by the test. +sub read_multixid_fields +{ + my $node = shift; + + my $pg_controldata_path = $node->installed_command('pg_controldata'); + my ($stdout, $stderr) = + run_command([ $pg_controldata_path, $node->data_dir ]); + $stdout =~ /^Latest checkpoint's oldestMultiXid:\s*(.*)$/m + or die "could not read oldestMultiXid from pg_controldata"; + my $oldest_multi_xid = $1; + $stdout =~ /^Latest checkpoint's NextMultiXactId:\s*(.*)$/m + or die "could not read NextMultiXactId from pg_controldata"; + my $next_multi_xid = $1; + $stdout =~ /^Latest checkpoint's NextMultiOffset:\s*(.*)$/m + or die "could not read NextMultiOffset from pg_controldata"; + my $next_multi_offset = $1; + + return ($oldest_multi_xid, $next_multi_xid, $next_multi_offset); +} + +# Reset a cluster's next multixid and mxoffset to given values. +# +# Note: This is used on the old installation, so the command arguments +# and the output parsing used here must work with all pre-v19 +# PostgreSQL versions supported by the test. +sub reset_mxid_mxoffset_pre_v19 +{ + my $node = shift; + my $mxid = shift; + my $mxoffset = shift; + + my $pg_resetwal_path = $node->installed_command('pg_resetwal'); + # Get block size + my ($out, $err) = + run_command([ $pg_resetwal_path, '--dry-run', $node->data_dir ]); + $out =~ /^Database block size: *(\d+)$/m or die; + + # Verify that no multixids are currently in use. Resetting would + # destroy them. (A freshly initialized cluster has no multixids.) + $out =~ /^Latest checkpoint's NextMultiXactId: *(\d+)$/m or die; + my $next_mxid = $1; + $out =~ /^Latest checkpoint's oldestMultiXid: *(\d+)$/m or die; + my $oldest_mxid = $1; + die "cluster has some multixids in use" unless $next_mxid == $oldest_mxid; + + # Extract a few other values from pg_resetwal --dry-run output + # that we need for the calculations below + $out =~ /^Database block size: *(\d+)$/m or die; + my $blcksz = $1; + # SLRU_PAGES_PER_SEGMENT is always 32 on pre-19 versions + my $slru_pages_per_segment = 32; + + # Do the reset + my @cmd = ( + $pg_resetwal_path, + '--pgdata' => $node->data_dir, + '--multixact-offset' => $mxoffset, + '--multixact-ids' => "$mxid,$mxid"); + command_ok(\@cmd, 'reset multixids and offset'); + + # pg_resetwal just updates the control file. The cluster will + # refuse to start up, if the SLRU segments corresponding to the + # next multixid and offset does not exist. Create a segments that + # covers the given values, filled with zeros. But first remove + # any old segments. + unlink glob $node->data_dir . "/pg_multixact/offsets/*"; + unlink glob $node->data_dir . "/pg_multixact/members/*"; + + # Initialize the 'offsets' SLRU file containing the new next multixid + # with zeros + # + # sizeof(MultiXactOffset) == 4 in PostgreSQL versions before 19 + my $multixact_offsets_per_page = $blcksz / 4; + my $segno = + int($mxid / $multixact_offsets_per_page / $slru_pages_per_segment); + my $path = + sprintf('%s/pg_multixact/offsets/%04X', $node->data_dir, $segno); + open my $fh, ">", $path + or die "could not open \"$path\": $!"; + binmode $fh; + my $bytes_per_seg = $slru_pages_per_segment * $blcksz; + syswrite($fh, "\0" x $bytes_per_seg) == $bytes_per_seg + or die "could not write to \"$path\": $!"; + close $fh; + + # Same for the 'members' SLRU + my $multixact_members_per_page = int($blcksz / 20) * 4; + $segno = + int($mxoffset / $multixact_members_per_page / $slru_pages_per_segment); + $path = sprintf "%s/pg_multixact/members/%04X", $node->data_dir, $segno; + open $fh, ">", $path + or die "could not open \"$path\": $!"; + binmode $fh; + syswrite($fh, "\0" x $bytes_per_seg) == $bytes_per_seg + or die "could not write to \"$path\": $!"; + close($fh); +} + +# Main test workhorse routine. Dump data on old version, run +# pg_upgrade, compare data after upgrade. +sub upgrade_and_compare +{ + my $tag = shift; + my $oldnode = shift; + my $newnode = shift; + + command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $oldnode->data_dir, + '--new-datadir' => $newnode->data_dir, + '--old-bindir' => $oldnode->config_data('--bindir'), + '--new-bindir' => $newnode->config_data('--bindir'), + '--socketdir' => $newnode->host, + '--old-port' => $oldnode->port, + '--new-port' => $newnode->port, + ], + 'run of pg_upgrade for new instance'); + + # Dump contents of the test table, and the status of all updating + # multixids from the old cluster. (Locking-only multixids don't + # need to be preserved so we ignore those) + # + # Note: we do this *after* running pg_upgrade, to ensure that we + # don't set all the hint bits before upgrade by doing the SELECT + # on the table. + my ($multixids_start, $multixids_end, undef) = + read_multixid_fields($oldnode); + $oldnode->start; + my $old_table_contents = + get_test_table_contents($oldnode, "oldnode_${tag}_table_contents"); + my $old_multixacts = + get_updating_multixact_members($oldnode, $multixids_start, + $multixids_end, "oldnode_${tag}_multixacts"); + $oldnode->stop; + + # Compare them with the upgraded cluster + $newnode->start; + my $new_table_contents = + get_test_table_contents($newnode, "newnode_${tag}_table_contents"); + my $new_multixacts = + get_updating_multixact_members($newnode, $multixids_start, + $multixids_end, "newnode_${tag}_multixacts"); + $newnode->stop; + + compare_files($old_table_contents, $new_table_contents, + 'test table contents from original and upgraded clusters match'); + compare_files($old_multixacts, $new_multixacts, + 'multixact members from original and upgraded clusters match'); +} + +# In a VPATH build, we'll be started in the source directory, but we want +# to run pg_upgrade in the build directory so that any files generated finish +# in it, like delete_old_cluster.{sh,bat}. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +my $old_version; + +# Basic scenario: Create a cluster using old installation, run +# multixid-creating workload on it, then upgrade. +# +# This works even even if the old and new version is the same, +# although it's not very interesting as the conversion routines only +# run when upgrading from a pre-v19 cluster. +{ + my $tag = 'basic'; + my $old = + PostgreSQL::Test::Cluster->new("${tag}_oldnode", + install_path => $ENV{oldinstall}); + my $new = PostgreSQL::Test::Cluster->new("${tag}_newnode"); + + $old->init(extra => ['-k']); + + $old_version = $old->pg_version; + note "old installation is version $old_version\n"; + + # Run the workload + my (undef, $start_mxid, $start_mxoff) = read_multixid_fields($old); + mxact_workload($old, $new); + my (undef, $finish_mxid, $finish_mxoff) = read_multixid_fields($old); + + note "Testing upgrade, ${tag} scenario\n" + . " mxid from ${start_mxid} to ${finish_mxid}\n" + . " oldnode mxoff from ${start_mxoff} to ${finish_mxoff}\n"; + + $new->init; + upgrade_and_compare($tag, $old, $new); +} + +# Wraparound scenario: This is the same as the basic scenario, but the +# old cluster goes through multixid and offset wraparound. +# +# This requires the old installation to be version 18 or older, +# because the hacks we use to reset the old cluster to a state just +# before the wraparound rely on the pre-v19 file format. If the old +# cluster is of v19 or above, multixact SLRU conversion is not needed +# anyway. +SKIP: +{ + skip + "skipping mxoffset conversion tests because upgrading from the old version does not require conversion" + if ($old_version >= '19devel'); + + my $tag = 'wraparound'; + my $old = + PostgreSQL::Test::Cluster->new("${tag}_oldnode", + install_path => $ENV{oldinstall}); + my $new = PostgreSQL::Test::Cluster->new("${tag}_newnode"); + + $old->init(extra => ['-k']); + + # Reset the old cluster to just before multixid and 32-bit offset + # wraparound. + reset_mxid_mxoffset_pre_v19($old, 0xFFFFFA00, 0xFFFFEC00); + + # Run the workload. This crosses multixid and offset wraparound. + my (undef, $start_mxid, $start_mxoff) = read_multixid_fields($old); + mxact_workload($old, $new); + my (undef, $finish_mxid, $finish_mxoff) = read_multixid_fields($old); + + note "Testing upgrade, ${tag} scenario\n" + . " mxid from ${start_mxid} to ${finish_mxid}\n" + . " oldnode mxoff from ${start_mxoff} to ${finish_mxoff}\n"; + + # Verify that wraparounds happened. + cmp_ok($finish_mxid, '<', $start_mxid, + "multixid wrapped around in old cluster"); + cmp_ok($finish_mxoff, '<', $start_mxoff, + "mxoff wrapped around in old cluster"); + + $new->init; + upgrade_and_compare($tag, $old, $new); +} + +done_testing(); diff --git a/src/bin/pg_upgrade/t/008_extension_control_path.pl b/src/bin/pg_upgrade/t/008_extension_control_path.pl new file mode 100644 index 0000000000000..383d7dc08d2c7 --- /dev/null +++ b/src/bin/pg_upgrade/t/008_extension_control_path.pl @@ -0,0 +1,126 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test pg_upgrade with the extension_control_path GUC active. + +use strict; +use warnings FATAL => 'all'; + +use File::Copy; +use File::Path; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Make sure the extension file .so path is provided +my $ext_lib_so = $ENV{TEST_EXT_LIB} + or die "couldn't get the extension so path"; + +# Create the custom extension directory layout: +# $ext_dir/extension/ -- .control and .sql files +# $ext_dir/lib/ -- .so file +my $ext_dir = PostgreSQL::Test::Utils::tempdir(); +mkpath("$ext_dir/extension"); +mkpath("$ext_dir/lib"); +my $ext_lib = $ext_dir . '/lib'; + +# Copy the .so file into the lib/ subdirectory. +copy($ext_lib_so, $ext_lib) + or die "could not copy '$ext_lib_so' to '$ext_lib': $!"; + +create_extension_files('test_ext', $ext_dir); + +my $sep = $windows_os ? ";" : ":"; +my $ext_path = $windows_os ? ($ext_dir =~ s/\\/\\\\/gr) : $ext_dir; +my $ext_lib_path = $windows_os ? ($ext_lib =~ s/\\/\\\\/gr) : $ext_lib; + +my $extension_control_path_conf = qq( +extension_control_path = '\$system$sep$ext_path' +dynamic_library_path = '\$libdir$sep$ext_lib_path' +); + +my $old = + PostgreSQL::Test::Cluster->new('old', install_path => $ENV{oldinstall}); +$old->init; + +# Configure extension_control_path so the .control file is found in our +# extension/ directory, and dynamic_library_path so the .so is found in lib/. +$old->append_conf('postgresql.conf', $extension_control_path_conf); + +$old->start; + +# CREATE EXTENSION 'test_ext' +$old->safe_psql('postgres', 'CREATE EXTENSION test_ext'); + +# Verify the extension works before the upgrade. +my ($code, $stdout, $stderr) = $old->psql('postgres', 'SELECT test_ext()'); +is($code, 0, 'extension works before upgrade'); +like($stderr, qr/NOTICE: running successful/, 'extension working'); + +$old->stop; + +my $new = PostgreSQL::Test::Cluster->new('new'); +$new->init; + +# Pre-configure the new cluster with dynamic_library_path and +# extension_control_path before running pg_upgrade. +$new->append_conf('postgresql.conf', $extension_control_path_conf); + +# In a VPATH build, we'll be started in the source directory, but we want +# to run pg_upgrade in the build directory so that any files generated finish +# in it, like delete_old_cluster.{sh,bat}. +chdir ${PostgreSQL::Test::Utils::tmp_check}; + +command_ok( + [ + 'pg_upgrade', '--no-sync', + '--old-datadir' => $old->data_dir, + '--new-datadir' => $new->data_dir, + '--old-bindir' => $old->config_data('--bindir'), + '--new-bindir' => $new->config_data('--bindir'), + '--socketdir' => $new->host, + '--old-port' => $old->port, + '--new-port' => $new->port, + '--copy', + ], + 'pg_upgrade succeeds with extension installed via extension_control_path' +); + +$new->start; + +# Verify the extension still works after the upgrade. +($code, $stdout, $stderr) = $new->psql('postgres', 'SELECT test_ext()'); +is($code, 0, 'extension works after upgrade'); +like($stderr, qr/NOTICE: running successful/, 'extension working'); + +$new->stop; + +# Write .control and .sql files into $ext_dir/extension/ +# `module_pathname` contains the `$libdir/` to simulate most of the extensions +# that use it as a prefix in the `module_pathname` by default +sub create_extension_files +{ + my ($ext_name, $ext_dir) = @_; + + open my $cf, '>', "$ext_dir/extension/$ext_name.control" + or die "could not create control file: $!"; + print $cf + "comment = 'Test C extension for pg_upgrade + extension_control_path'\n"; + print $cf "default_version = '1.0'\n"; + print $cf "module_pathname = '\$libdir/$ext_name'\n"; + print $cf "relocatable = true\n"; + close $cf; + + open my $sqlf, '>', "$ext_dir/extension/$ext_name--1.0.sql" + or die "could not create SQL file: $!"; + print $sqlf "/* $ext_name--1.0.sql */\n"; + print $sqlf + "-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n"; + print $sqlf + qq'\\echo Use "CREATE EXTENSION $ext_name" to load this file. \\quit\n'; + print $sqlf "CREATE FUNCTION test_ext()\n"; + print $sqlf "RETURNS void AS 'MODULE_PATHNAME'\n"; + print $sqlf "LANGUAGE C;\n"; + close $sqlf; +} + +done_testing(); diff --git a/src/bin/pg_upgrade/tablespace.c b/src/bin/pg_upgrade/tablespace.c index 3520a75ba317d..95ea7819457bc 100644 --- a/src/bin/pg_upgrade/tablespace.c +++ b/src/bin/pg_upgrade/tablespace.c @@ -3,7 +3,7 @@ * * tablespace functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/tablespace.c */ @@ -23,10 +23,20 @@ init_tablespaces(void) set_tablespace_directory_suffix(&old_cluster); set_tablespace_directory_suffix(&new_cluster); - if (os_info.num_old_tablespaces > 0 && + if (old_cluster.num_tablespaces > 0 && strcmp(old_cluster.tablespace_suffix, new_cluster.tablespace_suffix) == 0) - pg_fatal("Cannot upgrade to/from the same system catalog version when\n" - "using tablespaces."); + { + for (int i = 0; i < old_cluster.num_tablespaces; i++) + { + /* + * In-place tablespaces are okay for same-version upgrades because + * their paths will differ between clusters. + */ + if (strcmp(old_cluster.tablespaces[i], new_cluster.tablespaces[i]) == 0) + pg_fatal("Cannot upgrade to/from the same system catalog version when\n" + "using tablespaces."); + } + } } @@ -53,19 +63,48 @@ get_tablespace_paths(void) res = executeQueryOrDie(conn, "%s", query); - if ((os_info.num_old_tablespaces = PQntuples(res)) != 0) - os_info.old_tablespaces = - (char **) pg_malloc(os_info.num_old_tablespaces * sizeof(char *)); + old_cluster.num_tablespaces = PQntuples(res); + new_cluster.num_tablespaces = PQntuples(res); + + if (PQntuples(res) != 0) + { + old_cluster.tablespaces = + pg_malloc_array(char *, old_cluster.num_tablespaces); + new_cluster.tablespaces = + pg_malloc_array(char *, new_cluster.num_tablespaces); + } else - os_info.old_tablespaces = NULL; + { + old_cluster.tablespaces = NULL; + new_cluster.tablespaces = NULL; + } i_spclocation = PQfnumber(res, "spclocation"); - for (tblnum = 0; tblnum < os_info.num_old_tablespaces; tblnum++) + for (tblnum = 0; tblnum < old_cluster.num_tablespaces; tblnum++) { struct stat statBuf; + char *spcloc = PQgetvalue(res, tblnum, i_spclocation); - os_info.old_tablespaces[tblnum] = pg_strdup(PQgetvalue(res, tblnum, i_spclocation)); + /* + * For now, we do not expect non-in-place tablespaces to move during + * upgrade. If that changes, it will likely become necessary to run + * the above query on the new cluster, too. + * + * pg_tablespace_location() returns absolute paths for non-in-place + * tablespaces and relative paths for in-place ones, so we use + * is_absolute_path() to distinguish between them. + */ + if (is_absolute_path(PQgetvalue(res, tblnum, i_spclocation))) + { + old_cluster.tablespaces[tblnum] = pg_strdup(spcloc); + new_cluster.tablespaces[tblnum] = old_cluster.tablespaces[tblnum]; + } + else + { + old_cluster.tablespaces[tblnum] = psprintf("%s/%s", old_cluster.pgdata, spcloc); + new_cluster.tablespaces[tblnum] = psprintf("%s/%s", new_cluster.pgdata, spcloc); + } /* * Check that the tablespace path exists and is a directory. @@ -76,21 +115,21 @@ get_tablespace_paths(void) * that contains user tablespaces is moved as part of pg_upgrade * preparation and the symbolic links are not updated. */ - if (stat(os_info.old_tablespaces[tblnum], &statBuf) != 0) + if (stat(old_cluster.tablespaces[tblnum], &statBuf) != 0) { if (errno == ENOENT) report_status(PG_FATAL, "tablespace directory \"%s\" does not exist", - os_info.old_tablespaces[tblnum]); + old_cluster.tablespaces[tblnum]); else report_status(PG_FATAL, "could not stat tablespace directory \"%s\": %m", - os_info.old_tablespaces[tblnum]); + old_cluster.tablespaces[tblnum]); } if (!S_ISDIR(statBuf.st_mode)) report_status(PG_FATAL, "tablespace path \"%s\" is not a directory", - os_info.old_tablespaces[tblnum]); + old_cluster.tablespaces[tblnum]); } PQclear(res); diff --git a/src/bin/pg_upgrade/task.c b/src/bin/pg_upgrade/task.c index a48d56913908d..b6eb29e1f3a63 100644 --- a/src/bin/pg_upgrade/task.c +++ b/src/bin/pg_upgrade/task.c @@ -38,7 +38,7 @@ * words, it only ever initiates one connection to each database in the * cluster for a given run. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/task.c */ @@ -116,7 +116,7 @@ typedef struct UpgradeTaskSlot UpgradeTask * upgrade_task_create(void) { - UpgradeTask *task = pg_malloc0(sizeof(UpgradeTask)); + UpgradeTask *task = pg_malloc0_object(UpgradeTask); task->queries = createPQExpBuffer(); @@ -154,8 +154,8 @@ upgrade_task_add_step(UpgradeTask *task, const char *query, { UpgradeTaskStep *new_step; - task->steps = pg_realloc(task->steps, - ++task->num_steps * sizeof(UpgradeTaskStep)); + task->steps = pg_realloc_array(task->steps, UpgradeTaskStep, + ++task->num_steps); new_step = &task->steps[task->num_steps - 1]; new_step->process_cb = process_cb; @@ -188,12 +188,13 @@ start_conn(const ClusterInfo *cluster, UpgradeTaskSlot *slot) appendPQExpBufferStr(&conn_opts, " host="); appendConnStrVal(&conn_opts, cluster->sockdir); } + if (!protocol_negotiation_supported(cluster)) + appendPQExpBufferStr(&conn_opts, " max_protocol_version=3.0"); slot->conn = PQconnectStart(conn_opts.data); if (!slot->conn) - pg_fatal("failed to create connection with connection string: \"%s\"", - conn_opts.data); + pg_fatal("out of memory"); termPQExpBuffer(&conn_opts); } @@ -402,7 +403,7 @@ wait_on_slots(UpgradeTaskSlot *slots, int numslots) * If we found socket(s) to wait on, wait. */ if (select_loop(maxFd, &input, &output) == -1) - pg_fatal("select() failed: %m"); + pg_fatal("%s() failed: %m", "select"); /* * Mark which sockets appear to be ready. @@ -420,7 +421,7 @@ void upgrade_task_run(const UpgradeTask *task, const ClusterInfo *cluster) { int jobs = Max(1, user_opts.jobs); - UpgradeTaskSlot *slots = pg_malloc0(sizeof(UpgradeTaskSlot) * jobs); + UpgradeTaskSlot *slots = pg_malloc0_array(UpgradeTaskSlot, jobs); dbs_complete = 0; dbs_processing = 0; diff --git a/src/bin/pg_upgrade/util.c b/src/bin/pg_upgrade/util.c index de0673f11d69a..0f47ed771d552 100644 --- a/src/bin/pg_upgrade/util.c +++ b/src/bin/pg_upgrade/util.c @@ -3,7 +3,7 @@ * * utility functions * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/util.c */ diff --git a/src/bin/pg_upgrade/version.c b/src/bin/pg_upgrade/version.c index 3ad5a991a30ea..047670d4acbfc 100644 --- a/src/bin/pg_upgrade/version.c +++ b/src/bin/pg_upgrade/version.c @@ -3,7 +3,7 @@ * * Postgres-version-specific routines * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * src/bin/pg_upgrade/version.c */ @@ -28,6 +28,24 @@ jsonb_9_4_check_applicable(ClusterInfo *cluster) return false; } +/* + * Older servers can't support newer protocol versions, so their connection + * strings will need to lock max_protocol_version to 3.0. + */ +bool +protocol_negotiation_supported(const ClusterInfo *cluster) +{ + /* + * The February 2018 patch release (9.3.21, 9.4.16, 9.5.11, 9.6.7, and + * 10.2) added support for NegotiateProtocolVersion. But ClusterInfo only + * has information about the major version number. To ensure we can still + * upgrade older unpatched servers, just assume anything prior to PG11 + * can't negotiate. It's not possible for those servers to make use of + * newer protocols anyway, so nothing is lost. + */ + return (GET_MAJOR_VERSION(cluster->major_version) >= 1100); +} + /* * old_9_6_invalidate_hash_indexes() * 9.6 -> 10 @@ -152,8 +170,6 @@ process_extension_updates(DbInfo *dbinfo, PGresult *res, void *arg) UpgradeTaskReport *report = (UpgradeTaskReport *) arg; PQExpBufferData connectbuf; - AssertVariableIsOfType(&process_extension_updates, UpgradeTaskProcessCB); - if (ntups == 0) return; diff --git a/src/bin/pg_verifybackup/astreamer_verify.c b/src/bin/pg_verifybackup/astreamer_verify.c index 33cf67670a70d..99bd69ce7c9ef 100644 --- a/src/bin/pg_verifybackup/astreamer_verify.c +++ b/src/bin/pg_verifybackup/astreamer_verify.c @@ -5,7 +5,7 @@ * Archive streamer for verification of a tar format backup (including * compressed tar format backups). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/bin/pg_verifybackup/astreamer_verify.c * @@ -69,7 +69,7 @@ astreamer_verify_content_new(astreamer *next, verifier_context *context, { astreamer_verify *streamer; - streamer = palloc0(sizeof(astreamer_verify)); + streamer = palloc0_object(astreamer_verify); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_verify_ops; @@ -79,7 +79,7 @@ astreamer_verify_content_new(astreamer *next, verifier_context *context, streamer->tblspc_oid = tblspc_oid; if (!context->skip_checksums) - streamer->checksum_ctx = pg_malloc(sizeof(pg_checksum_context)); + streamer->checksum_ctx = pg_malloc_object(pg_checksum_context); return &streamer->base; } @@ -165,7 +165,7 @@ member_verify_header(astreamer *streamer, astreamer_member *member) char pathname[MAXPGPATH]; /* We are only interested in normal files. */ - if (member->is_directory || member->is_link) + if (!member->is_regular) return; /* @@ -268,7 +268,7 @@ member_compute_checksum(astreamer *streamer, astreamer_member *member, mystreamer->checksum_bytes += len; /* Feed these bytes to the checksum calculation. */ - if (pg_checksum_update(checksum_ctx, (uint8 *) data, len) < 0) + if (pg_checksum_update(checksum_ctx, (const uint8 *) data, len) < 0) { report_backup_error(mystreamer->context, "could not update checksum of file \"%s\"", diff --git a/src/bin/pg_verifybackup/meson.build b/src/bin/pg_verifybackup/meson.build index 9567d55500d0a..0b21db9f1b53c 100644 --- a/src/bin/pg_verifybackup/meson.build +++ b/src/bin/pg_verifybackup/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_verifybackup_sources = files( 'astreamer_verify.c', @@ -23,10 +23,10 @@ tests += { 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { - 'env': {'GZIP_PROGRAM': gzip.found() ? gzip.path() : '', - 'TAR': tar.found() ? tar.path() : '', - 'LZ4': program_lz4.found() ? program_lz4.path() : '', - 'ZSTD': program_zstd.found() ? program_zstd.path() : ''}, + 'env': {'GZIP_PROGRAM': gzip.found() ? gzip.full_path() : '', + 'TAR': tar.found() ? tar.full_path() : '', + 'LZ4': program_lz4.found() ? program_lz4.full_path() : '', + 'ZSTD': program_zstd.found() ? program_zstd.full_path() : ''}, 'tests': [ 't/001_basic.pl', 't/002_algorithm.pl', diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c index 48994ef9bc6fc..b60ab8739d5f1 100644 --- a/src/bin/pg_verifybackup/pg_verifybackup.c +++ b/src/bin/pg_verifybackup/pg_verifybackup.c @@ -3,7 +3,7 @@ * pg_verifybackup.c * Verify a backup against a backup manifest. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_verifybackup/pg_verifybackup.c @@ -74,7 +74,9 @@ pg_noreturn static void report_manifest_error(JsonManifestParseContext *context, const char *fmt,...) pg_attribute_printf(2, 3); -static void verify_tar_backup(verifier_context *context, DIR *dir); +static void verify_tar_backup(verifier_context *context, DIR *dir, + char **base_archive_path, + char **wal_archive_path); static void verify_plain_backup_directory(verifier_context *context, char *relpath, char *fullpath, DIR *dir); @@ -83,7 +85,9 @@ static void verify_plain_backup_file(verifier_context *context, char *relpath, static void verify_control_file(const char *controlpath, uint64 manifest_system_identifier); static void precheck_tar_backup_file(verifier_context *context, char *relpath, - char *fullpath, SimplePtrList *tarfiles); + char *fullpath, SimplePtrList *tarfiles, + char **base_archive_path, + char **wal_archive_path); static void verify_tar_file(verifier_context *context, char *relpath, char *fullpath, astreamer *streamer); static void report_extra_backup_files(verifier_context *context); @@ -93,7 +97,7 @@ static void verify_file_checksum(verifier_context *context, uint8 *buffer); static void parse_required_wal(verifier_context *context, char *pg_waldump_path, - char *wal_directory); + char *wal_path); static astreamer *create_archive_verifier(verifier_context *context, char *archive_name, Oid tblspc_oid, @@ -126,7 +130,8 @@ main(int argc, char **argv) {"progress", no_argument, NULL, 'P'}, {"quiet", no_argument, NULL, 'q'}, {"skip-checksums", no_argument, NULL, 's'}, - {"wal-directory", required_argument, NULL, 'w'}, + {"wal-path", required_argument, NULL, 'w'}, + {"wal-directory", required_argument, NULL, 'w'}, /* deprecated */ {NULL, 0, NULL, 0} }; @@ -135,7 +140,9 @@ main(int argc, char **argv) char *manifest_path = NULL; bool no_parse_wal = false; bool quiet = false; - char *wal_directory = NULL; + char *wal_path = NULL; + char *base_archive_path = NULL; + char *wal_archive_path = NULL; char *pg_waldump_path = NULL; DIR *dir; @@ -221,8 +228,8 @@ main(int argc, char **argv) context.skip_checksums = true; break; case 'w': - wal_directory = pstrdup(optarg); - canonicalize_path(wal_directory); + wal_path = pstrdup(optarg); + canonicalize_path(wal_path); break; default: /* getopt_long already emitted a complaint */ @@ -285,10 +292,6 @@ main(int argc, char **argv) manifest_path = psprintf("%s/backup_manifest", context.backup_directory); - /* By default, look for the WAL in the backup directory, too. */ - if (wal_directory == NULL) - wal_directory = psprintf("%s/pg_wal", context.backup_directory); - /* * Try to read the manifest. We treat any errors encountered while parsing * the manifest as fatal; there doesn't seem to be much point in trying to @@ -331,17 +334,6 @@ main(int argc, char **argv) pfree(path); } - /* - * XXX: In the future, we should consider enhancing pg_waldump to read WAL - * files from an archive. - */ - if (!no_parse_wal && context.format == 't') - { - pg_log_error("pg_waldump cannot read tar files"); - pg_log_error_hint("You must use -n/--no-parse-wal when verifying a tar-format backup."); - exit(1); - } - /* * Perform the appropriate type of verification appropriate based on the * backup format. This will close 'dir'. @@ -350,7 +342,7 @@ main(int argc, char **argv) verify_plain_backup_directory(&context, NULL, context.backup_directory, dir); else - verify_tar_backup(&context, dir); + verify_tar_backup(&context, dir, &base_archive_path, &wal_archive_path); /* * The "matched" flag should now be set on every entry in the hash table. @@ -368,12 +360,35 @@ main(int argc, char **argv) if (context.format == 'p' && !context.skip_checksums) verify_backup_checksums(&context); + /* + * By default, WAL files are expected to be found in the backup directory + * for plain-format backups. In the case of tar-format backups, if a + * separate WAL archive is not found, the WAL files are most likely + * included within the main data directory archive. + */ + if (wal_path == NULL) + { + if (context.format == 'p') + wal_path = psprintf("%s/pg_wal", context.backup_directory); + else if (wal_archive_path) + wal_path = wal_archive_path; + else if (base_archive_path) + wal_path = base_archive_path; + else + { + pg_log_error("WAL archive not found"); + pg_log_error_hint("Specify the correct path using the option -w/--wal-path. " + "Or you must use -n/--no-parse-wal when verifying a tar-format backup."); + exit(1); + } + } + /* * Try to parse the required ranges of WAL records, unless we were told * not to do so. */ if (!no_parse_wal) - parse_required_wal(&context, pg_waldump_path, wal_directory); + parse_required_wal(&context, pg_waldump_path, wal_path); /* * If everything looks OK, tell the user this, unless we were asked to @@ -418,7 +433,7 @@ parse_manifest_file(char *manifest_path) /* Create the hash table. */ ht = manifest_files_create(initial_size, NULL); - result = pg_malloc0(sizeof(manifest_data)); + result = pg_malloc0_object(manifest_data); result->files = ht; context.private_data = result; context.version_cb = verifybackup_version_cb; @@ -584,7 +599,7 @@ verifybackup_per_wal_range_cb(JsonManifestParseContext *context, manifest_wal_range *range; /* Allocate and initialize a struct describing this WAL range. */ - range = palloc(sizeof(manifest_wal_range)); + range = palloc_object(manifest_wal_range); range->tli = tli; range->start_lsn = start_lsn; range->end_lsn = end_lsn; @@ -787,7 +802,8 @@ verify_control_file(const char *controlpath, uint64 manifest_system_identifier) * close when we're done with it. */ static void -verify_tar_backup(verifier_context *context, DIR *dir) +verify_tar_backup(verifier_context *context, DIR *dir, char **base_archive_path, + char **wal_archive_path) { struct dirent *dirent; SimplePtrList tarfiles = {NULL, NULL}; @@ -816,7 +832,8 @@ verify_tar_backup(verifier_context *context, DIR *dir) char *fullpath; fullpath = psprintf("%s/%s", context->backup_directory, filename); - precheck_tar_backup_file(context, filename, fullpath, &tarfiles); + precheck_tar_backup_file(context, filename, fullpath, &tarfiles, + base_archive_path, wal_archive_path); pfree(fullpath); } } @@ -875,17 +892,21 @@ verify_tar_backup(verifier_context *context, DIR *dir) * * The arguments to this function are mostly the same as the * verify_plain_backup_file. The additional argument outputs a list of valid - * tar files. + * tar files, along with the full paths to the main archive and the WAL + * directory archive. */ static void precheck_tar_backup_file(verifier_context *context, char *relpath, - char *fullpath, SimplePtrList *tarfiles) + char *fullpath, SimplePtrList *tarfiles, + char **base_archive_path, char **wal_archive_path) { struct stat sb; Oid tblspc_oid = InvalidOid; pg_compress_algorithm compress_algorithm; tar_file *tar; char *suffix = NULL; + bool is_base_archive = false; + bool is_wal_archive = false; /* Should be tar format backup */ Assert(context->format == 't'); @@ -918,9 +939,15 @@ precheck_tar_backup_file(verifier_context *context, char *relpath, * extension such as .gz, .lz4, or .zst. */ if (strncmp("base", relpath, 4) == 0) + { suffix = relpath + 4; + is_base_archive = true; + } else if (strncmp("pg_wal", relpath, 6) == 0) + { suffix = relpath + 6; + is_wal_archive = true; + } else { /* Expected a .tar file here. */ @@ -941,17 +968,7 @@ precheck_tar_backup_file(verifier_context *context, char *relpath, } /* Now, check the compression type of the tar */ - if (strcmp(suffix, ".tar") == 0) - compress_algorithm = PG_COMPRESSION_NONE; - else if (strcmp(suffix, ".tgz") == 0) - compress_algorithm = PG_COMPRESSION_GZIP; - else if (strcmp(suffix, ".tar.gz") == 0) - compress_algorithm = PG_COMPRESSION_GZIP; - else if (strcmp(suffix, ".tar.lz4") == 0) - compress_algorithm = PG_COMPRESSION_LZ4; - else if (strcmp(suffix, ".tar.zst") == 0) - compress_algorithm = PG_COMPRESSION_ZSTD; - else + if (!parse_tar_compress_algorithm(suffix, &compress_algorithm)) { report_backup_error(context, "file \"%s\" is not expected in a tar format backup", @@ -963,14 +980,19 @@ precheck_tar_backup_file(verifier_context *context, char *relpath, * Ignore WALs, as reading and verification will be handled through * pg_waldump. */ - if (strncmp("pg_wal", relpath, 6) == 0) + if (is_wal_archive) + { + *wal_archive_path = pstrdup(fullpath); return; + } + else if (is_base_archive) + *base_archive_path = pstrdup(fullpath); /* * Append the information to the list for complete verification at a later * stage. */ - tar = pg_malloc(sizeof(tar_file)); + tar = pg_malloc_object(tar_file); tar->relpath = pstrdup(relpath); tar->tblspc_oid = tblspc_oid; tar->compress_algorithm = compress_algorithm; @@ -1065,7 +1087,7 @@ verify_backup_checksums(verifier_context *context) progress_report(false); - buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8)); + buffer = pg_malloc_array(uint8, READ_CHUNK_SIZE); manifest_files_start_iterate(manifest->files, &it); while ((m = manifest_files_iterate(manifest->files, &it)) != NULL) @@ -1198,7 +1220,7 @@ verify_file_checksum(verifier_context *context, manifest_file *m, */ static void parse_required_wal(verifier_context *context, char *pg_waldump_path, - char *wal_directory) + char *wal_path) { manifest_data *manifest = context->manifest; manifest_wal_range *this_wal_range = manifest->first_wal_range; @@ -1207,8 +1229,8 @@ parse_required_wal(verifier_context *context, char *pg_waldump_path, { char *pg_waldump_cmd; - pg_waldump_cmd = psprintf("\"%s\" --quiet --path=\"%s\" --timeline=%u --start=%X/%X --end=%X/%X\n", - pg_waldump_path, wal_directory, this_wal_range->tli, + pg_waldump_cmd = psprintf("\"%s\" --quiet --path=\"%s\" --timeline=%u --start=%X/%08X --end=%X/%08X\n", + pg_waldump_path, wal_path, this_wal_range->tli, LSN_FORMAT_ARGS(this_wal_range->start_lsn), LSN_FORMAT_ARGS(this_wal_range->end_lsn)); fflush(NULL); @@ -1376,7 +1398,7 @@ usage(void) printf(_(" -P, --progress show progress information\n")); printf(_(" -q, --quiet do not print any output, except for errors\n")); printf(_(" -s, --skip-checksums skip checksum verification\n")); - printf(_(" -w, --wal-directory=PATH use specified path for WAL files\n")); + printf(_(" -w, --wal-path=PATH use specified path for WAL files\n")); printf(_(" -V, --version output version information, then exit\n")); printf(_(" -?, --help show this help, then exit\n")); printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT); diff --git a/src/bin/pg_verifybackup/pg_verifybackup.h b/src/bin/pg_verifybackup/pg_verifybackup.h index 8cb6f9c53ad85..a99d9bfd58171 100644 --- a/src/bin/pg_verifybackup/pg_verifybackup.h +++ b/src/bin/pg_verifybackup/pg_verifybackup.h @@ -3,7 +3,7 @@ * pg_verifybackup.h * Verify a backup against a backup manifest. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pg_verifybackup/pg_verifybackup.h diff --git a/src/bin/pg_verifybackup/po/meson.build b/src/bin/pg_verifybackup/po/meson.build index 7e6ddbbc9dce8..5c96ab273fbdf 100644 --- a/src/bin/pg_verifybackup/po/meson.build +++ b/src/bin/pg_verifybackup/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_verifybackup-' + pg_version_major.to_string())] diff --git a/src/bin/pg_verifybackup/t/001_basic.pl b/src/bin/pg_verifybackup/t/001_basic.pl index ded3011df5f42..de86f7a2aed4c 100644 --- a/src/bin/pg_verifybackup/t/001_basic.pl +++ b/src/bin/pg_verifybackup/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_verifybackup/t/002_algorithm.pl b/src/bin/pg_verifybackup/t/002_algorithm.pl index ae16c11bc4dd1..edc515d5904e2 100644 --- a/src/bin/pg_verifybackup/t/002_algorithm.pl +++ b/src/bin/pg_verifybackup/t/002_algorithm.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify that we can take and verify backups with various checksum types. @@ -30,10 +30,6 @@ sub test_checksums { # Add switch to get a tar-format backup push @backup, ('--format' => 'tar'); - - # Add switch to skip WAL verification, which is not yet supported for - # tar-format backups - push @verify, ('--no-parse-wal'); } # A backup with a bogus algorithm should fail. diff --git a/src/bin/pg_verifybackup/t/003_corruption.pl b/src/bin/pg_verifybackup/t/003_corruption.pl index 1dd60f709cf66..405ea793b6831 100644 --- a/src/bin/pg_verifybackup/t/003_corruption.pl +++ b/src/bin/pg_verifybackup/t/003_corruption.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify that various forms of corruption are detected by pg_verifybackup. @@ -13,6 +13,7 @@ use Test::More; my $tar = $ENV{TAR}; +my @tar_p_flags = tar_portability_options($tar); my $primary = PostgreSQL::Test::Cluster->new('primary'); $primary->init(allows_streaming => 1); @@ -154,8 +155,8 @@ # have a TAR program available. Note that this destructively modifies # the backup directory. if ( !$scenario->{'needs_unix_permissions'} - || !defined $tar - || $tar eq '') + && defined $tar + && $tar ne '') { my $tar_backup_path = $primary->backup_dir . '/tar_' . $name; mkdir($tar_backup_path) || die "mkdir $tar_backup_path: $!"; @@ -171,14 +172,23 @@ chdir($tspath) || die "chdir: $!"; command_ok( - [ $tar, '-cf', "$tar_backup_path/$tsoid.tar", '.' ]); + [ + $tar, @tar_p_flags, + '-cf' => "$tar_backup_path/$tsoid.tar", + '.' + ]); chdir($cwd) || die "chdir: $!"; rmtree($tspath); } # tar and remove pg_wal chdir($backup_path . '/pg_wal') || die "chdir: $!"; - command_ok([ $tar, '-cf', "$tar_backup_path/pg_wal.tar", '.' ]); + command_ok( + [ + $tar, @tar_p_flags, + '-cf' => "$tar_backup_path/pg_wal.tar", + '.' + ]); chdir($cwd) || die "chdir: $!"; rmtree($backup_path . '/pg_wal'); @@ -190,13 +200,16 @@ # Construct base.tar with what's left. chdir($backup_path) || die "chdir: $!"; - command_ok([ $tar, '-cf' => "$tar_backup_path/base.tar", '.' ]); + command_ok( + [ + $tar, @tar_p_flags, + '-cf' => "$tar_backup_path/base.tar", + '.' + ]); chdir($cwd) || die "chdir: $!"; - # Now check that the backup no longer verifies. We must use -n - # here, because pg_waldump can't yet read WAL from a tarfile. command_fails_like( - [ 'pg_verifybackup', '--no-parse-wal', $tar_backup_path ], + [ 'pg_verifybackup', $tar_backup_path ], $scenario->{'fails_like'}, "corrupt backup fails verification: $name"); diff --git a/src/bin/pg_verifybackup/t/004_options.pl b/src/bin/pg_verifybackup/t/004_options.pl index 3cdbe816862e1..18b3ca1570813 100644 --- a/src/bin/pg_verifybackup/t/004_options.pl +++ b/src/bin/pg_verifybackup/t/004_options.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify the behavior of assorted pg_verifybackup options. diff --git a/src/bin/pg_verifybackup/t/005_bad_manifest.pl b/src/bin/pg_verifybackup/t/005_bad_manifest.pl index d373829645402..0413fea02c35e 100644 --- a/src/bin/pg_verifybackup/t/005_bad_manifest.pl +++ b/src/bin/pg_verifybackup/t/005_bad_manifest.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test the behavior of pg_verifybackup when the backup manifest has # problems. diff --git a/src/bin/pg_verifybackup/t/006_encoding.pl b/src/bin/pg_verifybackup/t/006_encoding.pl index c243153c5f940..19483c9928a64 100644 --- a/src/bin/pg_verifybackup/t/006_encoding.pl +++ b/src/bin/pg_verifybackup/t/006_encoding.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify that pg_verifybackup handles hex-encoded filenames correctly. diff --git a/src/bin/pg_verifybackup/t/007_wal.pl b/src/bin/pg_verifybackup/t/007_wal.pl index babc4f0a86b85..0e0377bfaccaa 100644 --- a/src/bin/pg_verifybackup/t/007_wal.pl +++ b/src/bin/pg_verifybackup/t/007_wal.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test pg_verifybackup's WAL verification. @@ -42,10 +42,10 @@ command_ok( [ 'pg_verifybackup', - '--wal-directory' => $relocated_pg_wal, + '--wal-path' => $relocated_pg_wal, $backup_path ], - '--wal-directory can be used to specify WAL directory'); + '--wal-path can be used to specify WAL directory'); # Move directory back to original location. rename($relocated_pg_wal, $original_pg_wal) || die "rename pg_wal back: $!"; @@ -90,4 +90,20 @@ [ 'pg_verifybackup', $backup_path2 ], 'valid base backup with timeline > 1'); +# Test WAL verification for a tar-format backup with a separate pg_wal.tar, +# as produced by pg_basebackup --format=tar --wal-method=stream. +my $backup_path3 = $primary->backup_dir . '/test_tar_wal'; +$primary->command_ok( + [ + 'pg_basebackup', + '--pgdata' => $backup_path3, + '--no-sync', + '--format' => 'tar', + '--checkpoint' => 'fast' + ], + "tar backup with separate pg_wal.tar"); +command_ok( + [ 'pg_verifybackup', $backup_path3 ], + 'WAL verification succeeds with separate pg_wal.tar'); + done_testing(); diff --git a/src/bin/pg_verifybackup/t/008_untar.pl b/src/bin/pg_verifybackup/t/008_untar.pl index deed3ec247d2d..161c08c190d1f 100644 --- a/src/bin/pg_verifybackup/t/008_untar.pl +++ b/src/bin/pg_verifybackup/t/008_untar.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This test case aims to verify that server-side backups and server-side # backup compression work properly, and it also aims to verify that @@ -16,6 +16,22 @@ $primary->init(allows_streaming => 1); $primary->start; +# Create file with some random data and an arbitrary size, useful to check +# the solidity of the compression and decompression logic. The size of the +# file is chosen to be around 640kB. This has proven to be large enough to +# detect some issues related to LZ4, and low enough to not impact the runtime +# of the test significantly. +my $junk_data = $primary->safe_psql( + 'postgres', qq( + SELECT string_agg(encode(sha256(i::bytea), 'hex'), '') + FROM generate_series(1, 10240) s(i);)); +my $data_dir = $primary->data_dir; +my $junk_file = "$data_dir/junk"; +open my $jf, '>', $junk_file + or die "Could not create junk file: $!"; +print $jf $junk_data; +close $jf; + # Create a tablespace directory. my $source_ts_path = PostgreSQL::Test::Utils::tempdir_short(); @@ -31,7 +47,6 @@ SELECT oid FROM pg_tablespace WHERE spcname = 'regress_ts1')); my $backup_path = $primary->backup_dir . '/server-backup'; -my $extract_path = $primary->backup_dir . '/extracted-backup'; my @test_configuration = ( { @@ -52,6 +67,12 @@ 'backup_archive' => [ 'base.tar.lz4', "$tsoid.tar.lz4" ], 'enabled' => check_pg_config("#define USE_LZ4 1") }, + { + 'compression_method' => 'lz4', + 'backup_flags' => [ '--compress', 'server-lz4:5' ], + 'backup_archive' => [ 'base.tar.lz4', "$tsoid.tar.lz4" ], + 'enabled' => check_pg_config("#define USE_LZ4 1") + }, { 'compression_method' => 'zstd', 'backup_flags' => [ '--compress', 'server-zstd' ], @@ -101,14 +122,12 @@ # Verify tar backup. $primary->command_ok( [ - 'pg_verifybackup', '--no-parse-wal', - '--exit-on-error', $backup_path, + 'pg_verifybackup', '--exit-on-error', $backup_path, ], "verify backup, compression $method"); # Cleanup. rmtree($backup_path); - rmtree($extract_path); } } diff --git a/src/bin/pg_verifybackup/t/009_extract.pl b/src/bin/pg_verifybackup/t/009_extract.pl index 2560529121700..91c3b32bdab56 100644 --- a/src/bin/pg_verifybackup/t/009_extract.pl +++ b/src/bin/pg_verifybackup/t/009_extract.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This test aims to verify that the client can decompress and extract # a backup which was compressed by the server. diff --git a/src/bin/pg_verifybackup/t/010_client_untar.pl b/src/bin/pg_verifybackup/t/010_client_untar.pl index d8d2b06c7ee86..9670fbe4fda33 100644 --- a/src/bin/pg_verifybackup/t/010_client_untar.pl +++ b/src/bin/pg_verifybackup/t/010_client_untar.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This test case aims to verify that client-side backup compression work # properly, and it also aims to verify that pg_verifybackup can verify a base @@ -15,8 +15,23 @@ $primary->init(allows_streaming => 1); $primary->start; +# Create file with some random data and an arbitrary size, useful to check +# the solidity of the compression and decompression logic. The size of the +# file is chosen to be around 640kB. This has proven to be large enough to +# detect some issues related to LZ4, and low enough to not impact the runtime +# of the test significantly. +my $junk_data = $primary->safe_psql( + 'postgres', qq( + SELECT string_agg(encode(sha256(i::bytea), 'hex'), '') + FROM generate_series(1, 10240) s(i);)); +my $data_dir = $primary->data_dir; +my $junk_file = "$data_dir/junk"; +open my $jf, '>', $junk_file + or die "Could not create junk file: $!"; +print $jf $junk_data; +close $jf; + my $backup_path = $primary->backup_dir . '/client-backup'; -my $extract_path = $primary->backup_dir . '/extracted-backup'; my @test_configuration = ( { @@ -37,6 +52,12 @@ 'backup_archive' => 'base.tar.lz4', 'enabled' => check_pg_config("#define USE_LZ4 1") }, + { + 'compression_method' => 'lz4', + 'backup_flags' => [ '--compress', 'client-lz4:1' ], + 'backup_archive' => 'base.tar.lz4', + 'enabled' => check_pg_config("#define USE_LZ4 1") + }, { 'compression_method' => 'zstd', 'backup_flags' => [ '--compress', 'client-zstd:5' ], @@ -115,13 +136,11 @@ # Verify tar backup. $primary->command_ok( [ - 'pg_verifybackup', '--no-parse-wal', - '--exit-on-error', $backup_path, + 'pg_verifybackup', '--exit-on-error', $backup_path, ], "verify backup, compression $method"); # Cleanup. - rmtree($extract_path); rmtree($backup_path); } } diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile index 4c1ee649501f4..aabb87566a2dc 100644 --- a/src/bin/pg_waldump/Makefile +++ b/src/bin/pg_waldump/Makefile @@ -3,6 +3,9 @@ PGFILEDESC = "pg_waldump - decode and display WAL" PGAPPICON=win32 +# make these available to TAP test scripts +export TAR + subdir = src/bin/pg_waldump top_builddir = ../../.. include $(top_builddir)/src/Makefile.global @@ -10,13 +13,15 @@ include $(top_builddir)/src/Makefile.global OBJS = \ $(RMGRDESCOBJS) \ $(WIN32RES) \ + archive_waldump.o \ compat.o \ pg_waldump.o \ rmgrdesc.o \ xlogreader.o \ xlogstats.o -override CPPFLAGS := -DFRONTEND $(CPPFLAGS) +override CPPFLAGS := -DFRONTEND -I$(libpq_srcdir) $(CPPFLAGS) +LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils RMGRDESCSOURCES = $(sort $(notdir $(wildcard $(top_srcdir)/src/backend/access/rmgrdesc/*desc*.c))) RMGRDESCOBJS = $(patsubst %.c,%.o,$(RMGRDESCSOURCES)) diff --git a/src/bin/pg_waldump/archive_waldump.c b/src/bin/pg_waldump/archive_waldump.c new file mode 100644 index 0000000000000..0f44ebfeb2001 --- /dev/null +++ b/src/bin/pg_waldump/archive_waldump.c @@ -0,0 +1,856 @@ +/*------------------------------------------------------------------------- + * + * archive_waldump.c + * A generic facility for reading WAL data from tar archives via archive + * streamer. + * + * Portions Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/bin/pg_waldump/archive_waldump.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include + +#include "access/xlog_internal.h" +#include "common/file_perm.h" +#include "common/hashfn.h" +#include "common/logging.h" +#include "fe_utils/simple_list.h" +#include "pg_waldump.h" + +/* + * How many bytes should we try to read from a file at once? + */ +#define READ_CHUNK_SIZE (128 * 1024) + +/* Temporary directory for spilled WAL segment files */ +char *TmpWalSegDir = NULL; + +/* + * Check if the start segment number is zero; this indicates a request to read + * any WAL file. + */ +#define READ_ANY_WAL(privateInfo) ((privateInfo)->start_segno == 0) + +/* + * Hash entry representing a WAL segment retrieved from the archive. + * + * While WAL segments are typically read sequentially, individual entries + * maintain their own buffers for the following reasons: + * + * 1. Boundary Handling: The archive streamer provides a continuous byte + * stream. A single streaming chunk may contain the end of one WAL segment + * and the start of the next. Separate buffers allow us to easily + * partition and track these bytes by their respective segments. + * + * 2. Out-of-Order Support: Dedicated buffers simplify logic when segments + * are archived or retrieved out of sequence. + * + * To minimize the memory footprint, entries and their associated buffers are + * freed once consumed. Since pg_waldump does not request the same bytes + * twice (after it's located the point at which it should start decoding), + * a segment can be discarded as soon as pg_waldump moves past it. Moreover, + * if we read a segment that won't be needed till later, we spill its data to + * a temporary file instead of retaining it in memory. This ensures that + * pg_waldump can process even very large tar archives without needing more + * than a few WAL segments' worth of memory space. + */ +typedef struct ArchivedWALFile +{ + uint32 status; /* hash status */ + const char *fname; /* hash key: WAL segment name */ + + StringInfo buf; /* holds WAL bytes read from archive */ + bool spilled; /* true if the WAL data was spilled to a + * temporary file */ + + int read_len; /* total bytes received from archive for this + * segment (same as buf->len, unless we have + * spilled the data to a temp file) */ +} ArchivedWALFile; + +static uint32 hash_string_pointer(const char *s); +#define SH_PREFIX ArchivedWAL +#define SH_ELEMENT_TYPE ArchivedWALFile +#define SH_KEY_TYPE const char * +#define SH_KEY fname +#define SH_HASH_KEY(tb, key) hash_string_pointer(key) +#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0) +#define SH_SCOPE static inline +#define SH_RAW_ALLOCATOR pg_malloc0 +#define SH_DECLARE +#define SH_DEFINE +#include "lib/simplehash.h" + +typedef struct astreamer_waldump +{ + astreamer base; + XLogDumpPrivate *privateInfo; +} astreamer_waldump; + +static ArchivedWALFile *get_archive_wal_entry(const char *fname, + XLogDumpPrivate *privateInfo); +static bool read_archive_file(XLogDumpPrivate *privateInfo); +static void setup_tmpwal_dir(const char *waldir); + +static FILE *prepare_tmp_write(const char *fname, XLogDumpPrivate *privateInfo); +static void perform_tmp_write(const char *fname, StringInfo buf, FILE *file); + +static astreamer *astreamer_waldump_new(XLogDumpPrivate *privateInfo); +static void astreamer_waldump_content(astreamer *streamer, + astreamer_member *member, + const char *data, int len, + astreamer_archive_context context); +static void astreamer_waldump_finalize(astreamer *streamer); +static void astreamer_waldump_free(astreamer *streamer); + +static bool member_is_wal_file(astreamer_waldump *mystreamer, + astreamer_member *member, + char **fname); + +static const astreamer_ops astreamer_waldump_ops = { + .content = astreamer_waldump_content, + .finalize = astreamer_waldump_finalize, + .free = astreamer_waldump_free +}; + +/* + * Initializes the tar archive reader: opens the archive, builds a hash table + * for WAL entries, reads ahead until a full WAL page header is available to + * determine the WAL segment size, and computes start/end segment numbers for + * filtering. + */ +void +init_archive_reader(XLogDumpPrivate *privateInfo, + pg_compress_algorithm compression) +{ + int fd; + astreamer *streamer; + ArchivedWALFile *entry = NULL; + XLogLongPageHeader longhdr; + ArchivedWAL_iterator iter; + + /* Open tar archive and store its file descriptor */ + fd = open_file_in_directory(privateInfo->archive_dir, + privateInfo->archive_name); + + if (fd < 0) + pg_fatal("could not open file \"%s\"", privateInfo->archive_name); + + privateInfo->archive_fd = fd; + privateInfo->archive_fd_eof = false; + + streamer = astreamer_waldump_new(privateInfo); + + /* We must first parse the tar archive. */ + streamer = astreamer_tar_parser_new(streamer); + + /* If the archive is compressed, decompress before parsing. */ + if (compression == PG_COMPRESSION_GZIP) + streamer = astreamer_gzip_decompressor_new(streamer); + else if (compression == PG_COMPRESSION_LZ4) + streamer = astreamer_lz4_decompressor_new(streamer); + else if (compression == PG_COMPRESSION_ZSTD) + streamer = astreamer_zstd_decompressor_new(streamer); + + privateInfo->archive_streamer = streamer; + + /* + * Allocate a buffer for reading the archive file to begin content + * decoding. + */ + privateInfo->archive_read_buf = pg_malloc(READ_CHUNK_SIZE); + privateInfo->archive_read_buf_size = READ_CHUNK_SIZE; + + /* + * Hash table storing WAL entries read from the archive with an arbitrary + * initial size. + */ + privateInfo->archive_wal_htab = ArchivedWAL_create(8, NULL); + + /* + * Read until we have at least one WAL segment with enough data to extract + * the WAL segment size from the long page header. + * + * We must not rely on cur_file here, because it can become NULL if a + * member trailer is processed during a read_archive_file() call. Instead, + * scan the hash table after each read to find any entry with sufficient + * data. + */ + while (entry == NULL) + { + if (!read_archive_file(privateInfo)) + pg_fatal("could not find WAL in archive \"%s\"", + privateInfo->archive_name); + + ArchivedWAL_start_iterate(privateInfo->archive_wal_htab, &iter); + while ((entry = ArchivedWAL_iterate(privateInfo->archive_wal_htab, + &iter)) != NULL) + { + if (entry->read_len >= sizeof(XLogLongPageHeaderData)) + break; + } + } + + /* Extract the WAL segment size from the long page header */ + longhdr = (XLogLongPageHeader) entry->buf->data; + + if (!IsValidWalSegSize(longhdr->xlp_seg_size)) + { + pg_log_error(ngettext("invalid WAL segment size in WAL file from archive \"%s\" (%u byte)", + "invalid WAL segment size in WAL file from archive \"%s\" (%u bytes)", + longhdr->xlp_seg_size), + privateInfo->archive_name, longhdr->xlp_seg_size); + pg_log_error_detail("The WAL segment size must be a power of two between 1 MB and 1 GB."); + exit(1); + } + + privateInfo->segsize = longhdr->xlp_seg_size; + + /* + * With the WAL segment size available, we can now initialize the + * dependent start and end segment numbers. + */ + Assert(XLogRecPtrIsValid(privateInfo->startptr)); + XLByteToSeg(privateInfo->startptr, privateInfo->start_segno, + privateInfo->segsize); + + if (XLogRecPtrIsValid(privateInfo->endptr)) + XLByteToSeg(privateInfo->endptr, privateInfo->end_segno, + privateInfo->segsize); + + /* + * Now that we have initialized the filtering parameters (start_segno and + * end_segno), we can discard any already-loaded WAL hash table entries + * for segments we don't actually need. Subsequent WAL will be filtered + * automatically by the archive streamer using the updated start_segno and + * end_segno values. + */ + ArchivedWAL_start_iterate(privateInfo->archive_wal_htab, &iter); + while ((entry = ArchivedWAL_iterate(privateInfo->archive_wal_htab, + &iter)) != NULL) + { + XLogSegNo segno; + TimeLineID timeline; + + XLogFromFileName(entry->fname, &timeline, &segno, privateInfo->segsize); + if (privateInfo->timeline != timeline || + privateInfo->start_segno > segno || + privateInfo->end_segno < segno) + free_archive_wal_entry(entry->fname, privateInfo); + } +} + +/* + * Release the archive streamer chain and close the archive file. + */ +void +free_archive_reader(XLogDumpPrivate *privateInfo) +{ + /* + * NB: Normally, astreamer_finalize() is called before astreamer_free() to + * flush any remaining buffered data or to ensure the end of the tar + * archive is reached. read_archive_file() may have done so. However, + * when decoding WAL we can stop once we hit the end LSN, so we may never + * have read all of the input file. In that case any remaining buffered + * data or unread portion of the archive can be safely ignored. + */ + astreamer_free(privateInfo->archive_streamer); + + /* Free any remaining hash table entries and their buffers. */ + if (privateInfo->archive_wal_htab != NULL) + { + ArchivedWAL_iterator iter; + ArchivedWALFile *entry; + + ArchivedWAL_start_iterate(privateInfo->archive_wal_htab, &iter); + while ((entry = ArchivedWAL_iterate(privateInfo->archive_wal_htab, + &iter)) != NULL) + { + if (entry->buf != NULL) + destroyStringInfo(entry->buf); + } + ArchivedWAL_destroy(privateInfo->archive_wal_htab); + privateInfo->archive_wal_htab = NULL; + } + + /* Free the reusable read buffer. */ + if (privateInfo->archive_read_buf != NULL) + { + pg_free(privateInfo->archive_read_buf); + privateInfo->archive_read_buf = NULL; + } + + /* Close the file. */ + if (close(privateInfo->archive_fd) != 0) + pg_log_error("could not close file \"%s\": %m", + privateInfo->archive_name); +} + +/* + * Copies the requested WAL data from the hash entry's buffer into readBuff. + * If the buffer does not yet contain the needed bytes, fetches more data from + * the tar archive via the archive streamer. + */ +int +read_archive_wal_page(XLogDumpPrivate *privateInfo, XLogRecPtr targetPagePtr, + size_t count, char *readBuff) +{ + char *p = readBuff; + size_t nbytes = count; + XLogRecPtr recptr = targetPagePtr; + int segsize = privateInfo->segsize; + XLogSegNo segno; + char fname[MAXFNAMELEN]; + ArchivedWALFile *entry; + + /* Identify the segment and locate its entry in the archive hash */ + XLByteToSeg(targetPagePtr, segno, segsize); + XLogFileName(fname, privateInfo->timeline, segno, segsize); + entry = get_archive_wal_entry(fname, privateInfo); + Assert(!entry->spilled); + + while (nbytes > 0) + { + char *buf = entry->buf->data; + int bufLen = entry->buf->len; + XLogRecPtr endPtr; + XLogRecPtr startPtr; + + /* + * Calculate the LSN range currently residing in the buffer. + * + * read_len tracks total bytes received for this segment, so endPtr is + * the LSN just past the last buffered byte, and startPtr is the LSN + * of the first buffered byte. + */ + XLogSegNoOffsetToRecPtr(segno, entry->read_len, segsize, endPtr); + startPtr = endPtr - bufLen; + + /* + * Copy the requested WAL record if it exists in the buffer. + */ + if (bufLen > 0 && startPtr <= recptr && recptr < endPtr) + { + int copyBytes; + int offset = recptr - startPtr; + + /* + * Given startPtr <= recptr < endPtr and a total buffer size + * 'bufLen', the offset (recptr - startPtr) will always be less + * than 'bufLen'. + */ + Assert(offset < bufLen); + + copyBytes = Min(nbytes, bufLen - offset); + memcpy(p, buf + offset, copyBytes); + + /* Update state for read */ + recptr += copyBytes; + nbytes -= copyBytes; + p += copyBytes; + } + else + { + /* + * We evidently need to fetch more data. Raise an error if the + * archive streamer has moved past our segment (meaning the WAL + * file in the archive is shorter than expected) or if reading the + * archive reached EOF. + */ + if (privateInfo->cur_file != entry) + pg_fatal("WAL segment \"%s\" in archive \"%s\" is too short: read %zu of %zu bytes", + fname, privateInfo->archive_name, + (count - nbytes), count); + if (!read_archive_file(privateInfo)) + pg_fatal("unexpected end of archive \"%s\" while reading \"%s\": read %zu of %zu bytes", + privateInfo->archive_name, fname, + (count - nbytes), count); + + /* + * Loading more data may have moved hash table entries, so we must + * re-look-up the one we are reading from. + */ + entry = ArchivedWAL_lookup(privateInfo->archive_wal_htab, fname); + /* ... it had better still be there */ + Assert(entry != NULL); + } + } + + /* + * Should have successfully read all the requested bytes or reported a + * failure before this point. + */ + Assert(nbytes == 0); + + /* + * Return count unchanged; the caller expects this convention, matching + * the routine that reads WAL pages from physical files. + */ + return count; +} + +/* + * Releases the buffer of a WAL entry that is no longer needed, preventing the + * accumulation of irrelevant WAL data. Also removes any associated temporary + * file and clears privateInfo->cur_file if it points to this entry, so the + * archive streamer skips subsequent data for it. + */ +void +free_archive_wal_entry(const char *fname, XLogDumpPrivate *privateInfo) +{ + ArchivedWALFile *entry; + const char *oldfname; + + entry = ArchivedWAL_lookup(privateInfo->archive_wal_htab, fname); + + if (entry == NULL) + return; + + /* Destroy the buffer */ + destroyStringInfo(entry->buf); + entry->buf = NULL; + + /* Remove temporary file if any */ + if (entry->spilled) + { + char fpath[MAXPGPATH]; + + snprintf(fpath, MAXPGPATH, "%s/%s", TmpWalSegDir, fname); + + if (unlink(fpath) == 0) + pg_log_debug("removed file \"%s\"", fpath); + } + + /* Clear cur_file if it points to the entry being freed */ + if (privateInfo->cur_file == entry) + privateInfo->cur_file = NULL; + + /* + * ArchivedWAL_delete_item may cause other hash table entries to move. + * Therefore, if cur_file isn't NULL now, we have to be prepared to look + * that entry up again after the deletion. Fortunately, the entry's fname + * string won't move. + */ + oldfname = privateInfo->cur_file ? privateInfo->cur_file->fname : NULL; + + ArchivedWAL_delete_item(privateInfo->archive_wal_htab, entry); + + if (oldfname) + { + privateInfo->cur_file = ArchivedWAL_lookup(privateInfo->archive_wal_htab, + oldfname); + /* ... it had better still be there */ + Assert(privateInfo->cur_file != NULL); + } +} + +/* + * Returns the archived WAL entry from the hash table if it already exists. + * Otherwise, reads more data from the archive until the requested entry is + * found. If the archive streamer reads a WAL file from the archive that + * is not currently needed, that data is spilled to a temporary file for later + * retrieval. + * + * Note that the returned entry might not have been completely read from + * the archive yet. + */ +static ArchivedWALFile * +get_archive_wal_entry(const char *fname, XLogDumpPrivate *privateInfo) +{ + while (1) + { + ArchivedWALFile *entry; + ArchivedWAL_iterator iter; + + /* + * Search the hash table first. If the entry is found, return it. + * Otherwise, the requested WAL entry hasn't been read from the + * archive yet; we must invoke the archive streamer to fetch it. + */ + entry = ArchivedWAL_lookup(privateInfo->archive_wal_htab, fname); + + if (entry != NULL) + return entry; + + /* + * Before loading more data, scan the hash table to see if we have + * loaded any files we don't need yet. If so, spill their data to + * disk to conserve memory space. But don't try to spill a + * partially-read file; it's not worth the complication. + */ + ArchivedWAL_start_iterate(privateInfo->archive_wal_htab, &iter); + while ((entry = ArchivedWAL_iterate(privateInfo->archive_wal_htab, + &iter)) != NULL) + { + FILE *write_fp; + + /* OK to spill? */ + if (entry->spilled) + continue; /* already spilled */ + if (entry == privateInfo->cur_file) + continue; /* still being read */ + + /* Write out the completed WAL file contents to a temp file. */ + write_fp = prepare_tmp_write(entry->fname, privateInfo); + perform_tmp_write(entry->fname, entry->buf, write_fp); + if (fclose(write_fp) != 0) + pg_fatal("could not close file \"%s/%s\": %m", + TmpWalSegDir, entry->fname); + + /* resetStringInfo won't release storage, so delete/recreate. */ + destroyStringInfo(entry->buf); + entry->buf = makeStringInfo(); + entry->spilled = true; + } + + /* + * Read more data. If we reach EOF, the desired file is not present. + */ + if (!read_archive_file(privateInfo)) + pg_fatal("could not find WAL \"%s\" in archive \"%s\"", + fname, privateInfo->archive_name); + } +} + +/* + * Reads a chunk from the archive file and passes it through the streamer + * pipeline for decompression (if needed) and tar member extraction. + * + * Returns true if successful, false if there is no more data. + * + * Callers must be aware that a single call may trigger multiple callbacks + * in astreamer_waldump_content, so privateInfo->cur_file can change value + * (or become NULL) during a call. In particular, cur_file is set to NULL + * when the ASTREAMER_MEMBER_TRAILER callback fires at the end of a tar + * member; it is then set to a new entry when the next WAL member's + * ASTREAMER_MEMBER_HEADER callback fires, which may or may not happen + * within the same call. + */ +static bool +read_archive_file(XLogDumpPrivate *privateInfo) +{ + int rc; + + /* Fail if we already reached EOF in a prior call. */ + if (privateInfo->archive_fd_eof) + return false; + + /* Try to read some more data. */ + rc = read(privateInfo->archive_fd, privateInfo->archive_read_buf, + privateInfo->archive_read_buf_size); + if (rc < 0) + pg_fatal("could not read file \"%s\": %m", + privateInfo->archive_name); + + /* + * Decompress (if required), and then parse the previously read contents + * of the tar file. + */ + if (rc > 0) + astreamer_content(privateInfo->archive_streamer, NULL, + privateInfo->archive_read_buf, rc, + ASTREAMER_UNKNOWN); + else + { + /* + * We reached EOF, but there is probably still data queued in the + * astreamer pipeline's buffers. Flush it out to ensure that we + * process everything. + */ + astreamer_finalize(privateInfo->archive_streamer); + /* Set flag to ensure we don't finalize more than once. */ + privateInfo->archive_fd_eof = true; + } + + return true; +} + +/* + * Set up a temporary directory to temporarily store WAL segments. + */ +static void +setup_tmpwal_dir(const char *waldir) +{ + const char *tmpdir = getenv("TMPDIR"); + char *template; + + Assert(TmpWalSegDir == NULL); + + /* + * Use the directory specified by the TMPDIR environment variable. If it's + * not set, fall back to the provided WAL directory to store WAL files + * temporarily. + */ + template = psprintf("%s/waldump_tmp-XXXXXX", + tmpdir ? tmpdir : waldir); + TmpWalSegDir = mkdtemp(template); + + if (TmpWalSegDir == NULL) + pg_fatal("could not create directory \"%s\": %m", template); + + canonicalize_path(TmpWalSegDir); + + pg_log_debug("created directory \"%s\"", TmpWalSegDir); +} + +/* + * Open a file in the temporary spill directory for writing an out-of-order + * WAL segment, creating the directory if not already done. + * Returns the open file handle. + */ +static FILE * +prepare_tmp_write(const char *fname, XLogDumpPrivate *privateInfo) +{ + char fpath[MAXPGPATH]; + FILE *file; + + /* Setup temporary directory to store WAL segments, if we didn't already */ + if (unlikely(TmpWalSegDir == NULL)) + setup_tmpwal_dir(privateInfo->archive_dir); + + snprintf(fpath, MAXPGPATH, "%s/%s", TmpWalSegDir, fname); + + /* Open the spill file for writing */ + file = fopen(fpath, PG_BINARY_W); + if (file == NULL) + pg_fatal("could not create file \"%s\": %m", fpath); + +#ifndef WIN32 + if (chmod(fpath, pg_file_create_mode)) + pg_fatal("could not set permissions on file \"%s\": %m", + fpath); +#endif + + pg_log_debug("spilling to temporary file \"%s\"", fpath); + + return file; +} + +/* + * Write buffer data to the given file handle. + */ +static void +perform_tmp_write(const char *fname, StringInfo buf, FILE *file) +{ + Assert(file); + + errno = 0; + if (buf->len > 0 && fwrite(buf->data, buf->len, 1, file) != 1) + { + /* + * If write didn't set errno, assume problem is no disk space + */ + if (errno == 0) + errno = ENOSPC; + pg_fatal("could not write to file \"%s/%s\": %m", TmpWalSegDir, fname); + } +} + +/* + * Create an astreamer that can read WAL from tar file. + */ +static astreamer * +astreamer_waldump_new(XLogDumpPrivate *privateInfo) +{ + astreamer_waldump *streamer; + + streamer = palloc0_object(astreamer_waldump); + *((const astreamer_ops **) &streamer->base.bbs_ops) = + &astreamer_waldump_ops; + + streamer->privateInfo = privateInfo; + + return &streamer->base; +} + +/* + * Main entry point of the archive streamer for reading WAL data from a tar + * file. If a member is identified as a valid WAL file, a hash entry is created + * for it, and its contents are copied into that entry's buffer, making them + * accessible to the decoding routine. + */ +static void +astreamer_waldump_content(astreamer *streamer, astreamer_member *member, + const char *data, int len, + astreamer_archive_context context) +{ + astreamer_waldump *mystreamer = (astreamer_waldump *) streamer; + XLogDumpPrivate *privateInfo = mystreamer->privateInfo; + + Assert(context != ASTREAMER_UNKNOWN); + + switch (context) + { + case ASTREAMER_MEMBER_HEADER: + { + char *fname = NULL; + ArchivedWALFile *entry; + bool found; + + /* Shouldn't see MEMBER_HEADER in the middle of a file */ + Assert(privateInfo->cur_file == NULL); + + pg_log_debug("reading \"%s\"", member->pathname); + + if (!member_is_wal_file(mystreamer, member, &fname)) + break; + + /* + * Skip range filtering during initial startup, before the WAL + * segment size and segment number bounds are known. + */ + if (!READ_ANY_WAL(privateInfo)) + { + XLogSegNo segno; + TimeLineID timeline; + + /* + * Skip the segment if the timeline does not match, if it + * falls outside the caller-specified range. + */ + XLogFromFileName(fname, &timeline, &segno, privateInfo->segsize); + if (privateInfo->timeline != timeline || + privateInfo->start_segno > segno || + privateInfo->end_segno < segno) + { + pfree(fname); + break; + } + } + + /* + * Note: ArchivedWAL_insert may cause existing hash table + * entries to move. While cur_file is known to be NULL right + * now, read_archive_wal_page may have a live hash entry + * pointer, which it needs to take care to update after + * read_archive_file completes. + */ + entry = ArchivedWAL_insert(privateInfo->archive_wal_htab, + fname, &found); + + /* + * Shouldn't happen, but if it does, simply ignore the + * duplicate WAL file. + */ + if (found) + { + pg_log_warning("ignoring duplicate WAL \"%s\" found in archive \"%s\"", + member->pathname, privateInfo->archive_name); + pfree(fname); + break; + } + + entry->buf = makeStringInfo(); + entry->spilled = false; + entry->read_len = 0; + privateInfo->cur_file = entry; + } + break; + + case ASTREAMER_MEMBER_CONTENTS: + if (privateInfo->cur_file) + { + appendBinaryStringInfo(privateInfo->cur_file->buf, data, len); + privateInfo->cur_file->read_len += len; + } + break; + + case ASTREAMER_MEMBER_TRAILER: + + /* + * End of this tar member; mark cur_file NULL so subsequent + * content callbacks (if any) know no WAL file is currently + * active. + */ + privateInfo->cur_file = NULL; + break; + + case ASTREAMER_ARCHIVE_TRAILER: + break; + + default: + /* Shouldn't happen. */ + pg_fatal("unexpected state while parsing tar file"); + } +} + +/* + * End-of-stream processing for an astreamer_waldump stream. This is a + * terminal streamer so it must have no successor. + */ +static void +astreamer_waldump_finalize(astreamer *streamer) +{ + Assert(streamer->bbs_next == NULL); +} + +/* + * Free memory associated with an astreamer_waldump stream. + */ +static void +astreamer_waldump_free(astreamer *streamer) +{ + Assert(streamer->bbs_next == NULL); + pfree(streamer); +} + +/* + * Returns true if the archive member name matches the WAL naming format. If + * successful, it also outputs the WAL segment name. + */ +static bool +member_is_wal_file(astreamer_waldump *mystreamer, astreamer_member *member, + char **fname) +{ + int pathlen; + char pathname[MAXPGPATH]; + char *filename; + + /* We are only interested in normal files */ + if (!member->is_regular) + return false; + + if (strlen(member->pathname) < XLOG_FNAME_LEN) + return false; + + /* + * For a correct comparison, we must remove any '.' or '..' components + * from the member pathname. Similar to member_verify_header(), we prepend + * './' to the path so that canonicalize_path() can properly resolve and + * strip these references from the tar member name. + */ + snprintf(pathname, MAXPGPATH, "./%s", member->pathname); + canonicalize_path(pathname); + pathlen = strlen(pathname); + + /* Skip files in subdirectories other than pg_wal/ */ + if (pathlen > XLOG_FNAME_LEN && + strncmp(pathname, XLOGDIR, strlen(XLOGDIR)) != 0) + return false; + + /* WAL file may appear with a full path (e.g., pg_wal/) */ + filename = pathname + (pathlen - XLOG_FNAME_LEN); + if (!IsXLogFileName(filename)) + return false; + + *fname = pnstrdup(filename, XLOG_FNAME_LEN); + + return true; +} + +/* + * Helper function for WAL file hash table. + */ +static uint32 +hash_string_pointer(const char *s) +{ + const unsigned char *ss = (const unsigned char *) s; + + return hash_bytes(ss, strlen(s)); +} diff --git a/src/bin/pg_waldump/compat.c b/src/bin/pg_waldump/compat.c index 2529d3e6d177d..fc16186e66a9b 100644 --- a/src/bin/pg_waldump/compat.c +++ b/src/bin/pg_waldump/compat.c @@ -3,7 +3,7 @@ * compat.c * Reimplementations of various backend functions. * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_waldump/compat.c diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build index 937e0d688414e..5296f21b82c7f 100644 --- a/src/bin/pg_waldump/meson.build +++ b/src/bin/pg_waldump/meson.build @@ -1,6 +1,7 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_waldump_sources = files( + 'archive_waldump.c', 'compat.c', 'pg_waldump.c', 'rmgrdesc.c', @@ -18,7 +19,7 @@ endif pg_waldump = executable('pg_waldump', pg_waldump_sources, - dependencies: [frontend_code, lz4, zstd], + dependencies: [frontend_code, libpq, lz4, zstd], c_args: ['-DFRONTEND'], # needed for xlogreader et al kwargs: default_bin_args, ) @@ -29,6 +30,7 @@ tests += { 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { + 'env': {'TAR': tar.found() ? tar.full_path() : ''}, 'tests': [ 't/001_basic.pl', 't/002_save_fullpage.pl', diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c index 51fb76efc489e..c777e6763e5cf 100644 --- a/src/bin/pg_waldump/pg_waldump.c +++ b/src/bin/pg_waldump/pg_waldump.c @@ -2,7 +2,7 @@ * * pg_waldump.c - decode and display WAL * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_waldump/pg_waldump.c @@ -29,6 +29,7 @@ #include "common/logging.h" #include "common/relpath.h" #include "getopt_long.h" +#include "pg_waldump.h" #include "rmgrdesc.h" #include "storage/bufpage.h" @@ -39,18 +40,11 @@ static const char *progname; -static int WalSegSz; static volatile sig_atomic_t time_to_stop = false; -static const RelFileLocator emptyRelFileLocator = {0, 0, 0}; +static XLogReaderState *xlogreader_state_cleanup = NULL; -typedef struct XLogDumpPrivate -{ - TimeLineID timeline; - XLogRecPtr startptr; - XLogRecPtr endptr; - bool endptr_reached; -} XLogDumpPrivate; +static const RelFileLocator emptyRelFileLocator = {0, 0, 0}; typedef struct XLogDumpConfig { @@ -160,7 +154,7 @@ create_fullpage_directory(char *path) static void split_path(const char *path, char **dir, char **fname) { - char *sep; + const char *sep; /* split filepath into directory & filename */ sep = strrchr(path, '/'); @@ -184,7 +178,7 @@ split_path(const char *path, char **dir, char **fname) * * return a read only fd */ -static int +int open_file_in_directory(const char *directory, const char *fname) { int fd = -1; @@ -203,11 +197,11 @@ open_file_in_directory(const char *directory, const char *fname) /* * Try to find fname in the given directory. Returns true if it is found, * false otherwise. If fname is NULL, search the complete directory for any - * file with a valid WAL file name. If file is successfully opened, set the - * wal segment size. + * file with a valid WAL file name. If file is successfully opened, set + * *WaSegSz to the WAL segment size. */ static bool -search_directory(const char *directory, const char *fname) +search_directory(const char *directory, const char *fname, int *WalSegSz) { int fd = -1; DIR *xldir; @@ -249,17 +243,17 @@ search_directory(const char *directory, const char *fname) { XLogLongPageHeader longhdr = (XLogLongPageHeader) buf.data; - WalSegSz = longhdr->xlp_seg_size; - - if (!IsValidWalSegSize(WalSegSz)) + if (!IsValidWalSegSize(longhdr->xlp_seg_size)) { - pg_log_error(ngettext("invalid WAL segment size in WAL file \"%s\" (%d byte)", - "invalid WAL segment size in WAL file \"%s\" (%d bytes)", - WalSegSz), - fname, WalSegSz); + pg_log_error(ngettext("invalid WAL segment size in WAL file \"%s\" (%u byte)", + "invalid WAL segment size in WAL file \"%s\" (%u bytes)", + longhdr->xlp_seg_size), + fname, longhdr->xlp_seg_size); pg_log_error_detail("The WAL segment size must be a power of two between 1 MB and 1 GB."); exit(1); } + + *WalSegSz = longhdr->xlp_seg_size; } else if (r < 0) pg_fatal("could not read file \"%s\": %m", @@ -286,21 +280,22 @@ search_directory(const char *directory, const char *fname) * XLOGDIR / * $PGDATA / XLOGDIR / * - * The valid target directory is returned. + * The valid target directory is returned, and *WalSegSz is set to the + * size of the WAL segment found in that directory. */ static char * -identify_target_directory(char *directory, char *fname) +identify_target_directory(char *directory, char *fname, int *WalSegSz) { char fpath[MAXPGPATH]; if (directory != NULL) { - if (search_directory(directory, fname)) + if (search_directory(directory, fname, WalSegSz)) return pg_strdup(directory); /* directory / XLOGDIR */ snprintf(fpath, MAXPGPATH, "%s/%s", directory, XLOGDIR); - if (search_directory(fpath, fname)) + if (search_directory(fpath, fname, WalSegSz)) return pg_strdup(fpath); } else @@ -308,10 +303,10 @@ identify_target_directory(char *directory, char *fname) const char *datadir; /* current directory */ - if (search_directory(".", fname)) + if (search_directory(".", fname, WalSegSz)) return pg_strdup("."); /* XLOGDIR */ - if (search_directory(XLOGDIR, fname)) + if (search_directory(XLOGDIR, fname, WalSegSz)) return pg_strdup(XLOGDIR); datadir = getenv("PGDATA"); @@ -319,7 +314,7 @@ identify_target_directory(char *directory, char *fname) if (datadir != NULL) { snprintf(fpath, MAXPGPATH, "%s/%s", datadir, XLOGDIR); - if (search_directory(fpath, fname)) + if (search_directory(fpath, fname, WalSegSz)) return pg_strdup(fpath); } } @@ -333,6 +328,32 @@ identify_target_directory(char *directory, char *fname) return NULL; /* not reached */ } +/* + * Returns the number of bytes to read for the given page. Returns -1 if + * the requested range has already been reached or exceeded. + */ +static inline int +required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr, + int reqLen) +{ + int count = XLOG_BLCKSZ; + + if (XLogRecPtrIsValid(private->endptr)) + { + if (targetPagePtr + XLOG_BLCKSZ <= private->endptr) + count = XLOG_BLCKSZ; + else if (targetPagePtr + reqLen <= private->endptr) + count = private->endptr - targetPagePtr; + else + { + private->endptr_reached = true; + return -1; + } + } + + return count; +} + /* pg_waldump's XLogReaderRoutine->segment_open callback */ static void WALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo, @@ -390,21 +411,12 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, XLogRecPtr targetPtr, char *readBuff) { XLogDumpPrivate *private = state->private_data; - int count = XLOG_BLCKSZ; + int count = required_read_len(private, targetPagePtr, reqLen); WALReadError errinfo; - if (private->endptr != InvalidXLogRecPtr) - { - if (targetPagePtr + XLOG_BLCKSZ <= private->endptr) - count = XLOG_BLCKSZ; - else if (targetPagePtr + reqLen <= private->endptr) - count = private->endptr - targetPagePtr; - else - { - private->endptr_reached = true; - return -1; - } - } + /* Bail out if the end of the requested range has already been reached */ + if (count < 0) + return -1; if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline, &errinfo)) @@ -430,6 +442,110 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, return count; } +/* + * pg_waldump's XLogReaderRoutine->segment_open callback to support dumping WAL + * files from tar archives. Segment tracking is handled by + * TarWALDumpReadPage, so no action is needed here. + */ +static void +TarWALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo, + TimeLineID *tli_p) +{ + /* No action needed */ +} + +/* + * pg_waldump's XLogReaderRoutine->segment_close callback to support dumping + * WAL files from tar archives. Same as wal_segment_close. + */ +static void +TarWALDumpCloseSegment(XLogReaderState *state) +{ + close(state->seg.ws_file); + /* need to check errno? */ + state->seg.ws_file = -1; +} + +/* + * pg_waldump's XLogReaderRoutine->page_read callback to support dumping WAL + * files from tar archives. + */ +static int +TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen, + XLogRecPtr targetPtr, char *readBuff) +{ + XLogDumpPrivate *private = state->private_data; + int count = required_read_len(private, targetPagePtr, reqLen); + int segsize = state->segcxt.ws_segsize; + XLogSegNo curSegNo; + + /* Bail out if the end of the requested range has already been reached */ + if (count < 0) + return -1; + + /* + * If the target page is in a different segment, release the hash entry + * buffer and remove any spilled temporary file for the previous segment. + * Since pg_waldump never requests the same WAL bytes twice, moving to a + * new segment means the previous segment's data will not be needed again. + * + * Afterward, check whether the next required WAL segment was already + * spilled to the temporary directory before invoking the archive + * streamer. + */ + curSegNo = state->seg.ws_segno; + if (!XLByteInSeg(targetPagePtr, curSegNo, segsize)) + { + char fname[MAXFNAMELEN]; + XLogSegNo nextSegNo; + + /* + * Calculate the next WAL segment to be decoded from the given page + * pointer. + */ + XLByteToSeg(targetPagePtr, nextSegNo, segsize); + state->seg.ws_tli = private->timeline; + state->seg.ws_segno = nextSegNo; + + /* Close the WAL segment file if it is currently open */ + if (state->seg.ws_file >= 0) + { + close(state->seg.ws_file); + state->seg.ws_file = -1; + } + + /* + * If in pre-reading mode (prior to actual decoding), do not delete + * any entries that might be requested again once the decoding loop + * starts. For more details, see the comments in + * read_archive_wal_page(). + */ + if (private->decoding_started && curSegNo < nextSegNo) + { + XLogFileName(fname, state->seg.ws_tli, curSegNo, segsize); + free_archive_wal_entry(fname, private); + } + + /* + * If the next segment exists in the temporary spill directory, open + * it and continue reading from there. + */ + if (TmpWalSegDir != NULL) + { + XLogFileName(fname, state->seg.ws_tli, nextSegNo, segsize); + state->seg.ws_file = open_file_in_directory(TmpWalSegDir, fname); + } + } + + /* Continue reading from the open WAL segment, if any */ + if (state->seg.ws_file >= 0) + return WALDumpReadPage(state, targetPagePtr, count, targetPtr, + readBuff); + + /* Otherwise, read the WAL page from the archive streamer */ + return read_archive_wal_page(private, targetPagePtr, count, readBuff); +} + /* * Boolean to return whether the given WAL record matches a specific relation * and optionally block. @@ -637,7 +753,7 @@ XLogDumpDisplayStats(XLogDumpConfig *config, XLogStats *stats) /* * Leave if no stats have been computed yet, as tracked by the end LSN. */ - if (XLogRecPtrIsInvalid(stats->endptr)) + if (!XLogRecPtrIsValid(stats->endptr)) return; /* @@ -656,7 +772,7 @@ XLogDumpDisplayStats(XLogDumpConfig *config, XLogStats *stats) } total_len = total_rec_len + total_fpi_len; - printf("WAL statistics between %X/%X and %X/%X:\n", + printf("WAL statistics between %X/%08X and %X/%08X:\n", LSN_FORMAT_ARGS(stats->startptr), LSN_FORMAT_ARGS(stats->endptr)); /* @@ -752,6 +868,27 @@ XLogDumpDisplayStats(XLogDumpConfig *config, XLogStats *stats) total_len, "[100%]"); } +/* + * Remove temporary directory at exit, if any. + */ +static void +cleanup_tmpwal_dir_atexit(void) +{ + /* + * Before calling rmtree, we must close any open file we have in the temp + * directory; else rmdir fails on Windows. + */ + if (xlogreader_state_cleanup != NULL && + xlogreader_state_cleanup->seg.ws_file >= 0) + WALDumpCloseSegment(xlogreader_state_cleanup); + + if (TmpWalSegDir != NULL) + { + rmtree(TmpWalSegDir, true); + TmpWalSegDir = NULL; + } +} + static void usage(void) { @@ -767,8 +904,8 @@ usage(void) printf(_(" -F, --fork=FORK only show records that modify blocks in fork FORK;\n" " valid names are main, fsm, vm, init\n")); printf(_(" -n, --limit=N number of records to display\n")); - printf(_(" -p, --path=PATH directory in which to find WAL segment files or a\n" - " directory with a ./pg_wal that contains such files\n" + printf(_(" -p, --path=PATH a tar archive or a directory in which to find WAL segment files or\n" + " a directory with a pg_wal subdirectory containing such files\n" " (default: current directory, ./pg_wal, $PGDATA/pg_wal)\n")); printf(_(" -q, --quiet do not print any output, except for errors\n")); printf(_(" -r, --rmgr=RMGR only show records generated by resource manager RMGR;\n" @@ -801,6 +938,7 @@ main(int argc, char **argv) XLogRecPtr first_record; char *waldir = NULL; char *errormsg; + pg_compress_algorithm compression = PG_COMPRESSION_NONE; static struct option long_options[] = { {"bkp-details", no_argument, NULL, 'b'}, @@ -854,9 +992,14 @@ main(int argc, char **argv) memset(&stats, 0, sizeof(XLogStats)); private.timeline = 1; + private.segsize = 0; private.startptr = InvalidXLogRecPtr; private.endptr = InvalidXLogRecPtr; private.endptr_reached = false; + private.decoding_started = false; + private.archive_name = NULL; + private.start_segno = 0; + private.end_segno = UINT64_MAX; config.quiet = false; config.bkp_details = false; @@ -904,7 +1047,7 @@ main(int argc, char **argv) config.filter_by_extended = true; break; case 'e': - if (sscanf(optarg, "%X/%X", &xlogid, &xrecoff) != 2) + if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2) { pg_log_error("invalid WAL location: \"%s\"", optarg); @@ -1002,7 +1145,7 @@ main(int argc, char **argv) config.filter_by_extended = true; break; case 's': - if (sscanf(optarg, "%X/%X", &xlogid, &xrecoff) != 2) + if (sscanf(optarg, "%X/%08X", &xlogid, &xrecoff) != 2) { pg_log_error("invalid WAL location: \"%s\"", optarg); @@ -1098,8 +1241,13 @@ main(int argc, char **argv) if (waldir != NULL) { - /* validate path points to directory */ - if (!verify_directory(waldir)) + /* Check whether the path looks like a tar archive by its extension */ + if (parse_tar_compress_algorithm(waldir, &compression)) + { + split_path(waldir, &private.archive_dir, &private.archive_name); + } + /* Otherwise it must be a directory */ + else if (!verify_directory(waldir)) { pg_log_error("could not open directory \"%s\": %m", waldir); goto bad_argument; @@ -1117,6 +1265,17 @@ main(int argc, char **argv) int fd; XLogSegNo segno; + /* + * If a tar archive is passed using the --path option, all other + * arguments become unnecessary. + */ + if (private.archive_name) + { + pg_log_error("unnecessary command-line arguments specified with tar archive (first is \"%s\")", + argv[optind]); + goto bad_argument; + } + split_path(argv[optind], &directory, &fname); if (waldir == NULL && directory != NULL) @@ -1127,95 +1286,147 @@ main(int argc, char **argv) pg_fatal("could not open directory \"%s\": %m", waldir); } - waldir = identify_target_directory(waldir, fname); - fd = open_file_in_directory(waldir, fname); - if (fd < 0) - pg_fatal("could not open file \"%s\"", fname); - close(fd); - - /* parse position from file */ - XLogFromFileName(fname, &private.timeline, &segno, WalSegSz); - - if (XLogRecPtrIsInvalid(private.startptr)) - XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr); - else if (!XLByteInSeg(private.startptr, segno, WalSegSz)) + if (fname != NULL && parse_tar_compress_algorithm(fname, &compression)) { - pg_log_error("start WAL location %X/%X is not inside file \"%s\"", - LSN_FORMAT_ARGS(private.startptr), - fname); - goto bad_argument; + private.archive_dir = waldir; + private.archive_name = fname; } - - /* no second file specified, set end position */ - if (!(optind + 1 < argc) && XLogRecPtrIsInvalid(private.endptr)) - XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr); - - /* parse ENDSEG if passed */ - if (optind + 1 < argc) + else { - XLogSegNo endsegno; - - /* ignore directory, already have that */ - split_path(argv[optind + 1], &directory, &fname); - + waldir = identify_target_directory(waldir, fname, &private.segsize); fd = open_file_in_directory(waldir, fname); if (fd < 0) pg_fatal("could not open file \"%s\"", fname); close(fd); /* parse position from file */ - XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz); + XLogFromFileName(fname, &private.timeline, &segno, private.segsize); - if (endsegno < segno) - pg_fatal("ENDSEG %s is before STARTSEG %s", - argv[optind + 1], argv[optind]); + if (!XLogRecPtrIsValid(private.startptr)) + XLogSegNoOffsetToRecPtr(segno, 0, private.segsize, private.startptr); + else if (!XLByteInSeg(private.startptr, segno, private.segsize)) + { + pg_log_error("start WAL location %X/%08X is not inside file \"%s\"", + LSN_FORMAT_ARGS(private.startptr), + fname); + goto bad_argument; + } - if (XLogRecPtrIsInvalid(private.endptr)) - XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz, - private.endptr); + /* no second file specified, set end position */ + if (!(optind + 1 < argc) && !XLogRecPtrIsValid(private.endptr)) + XLogSegNoOffsetToRecPtr(segno + 1, 0, private.segsize, private.endptr); - /* set segno to endsegno for check of --end */ - segno = endsegno; - } + /* parse ENDSEG if passed */ + if (optind + 1 < argc) + { + XLogSegNo endsegno; + /* ignore directory, already have that */ + split_path(argv[optind + 1], &directory, &fname); - if (!XLByteInSeg(private.endptr, segno, WalSegSz) && - private.endptr != (segno + 1) * WalSegSz) - { - pg_log_error("end WAL location %X/%X is not inside file \"%s\"", - LSN_FORMAT_ARGS(private.endptr), - argv[argc - 1]); - goto bad_argument; + fd = open_file_in_directory(waldir, fname); + if (fd < 0) + pg_fatal("could not open file \"%s\"", fname); + close(fd); + + /* parse position from file */ + XLogFromFileName(fname, &private.timeline, &endsegno, private.segsize); + + if (endsegno < segno) + pg_fatal("ENDSEG %s is before STARTSEG %s", + argv[optind + 1], argv[optind]); + + if (!XLogRecPtrIsValid(private.endptr)) + XLogSegNoOffsetToRecPtr(endsegno + 1, 0, private.segsize, + private.endptr); + + /* set segno to endsegno for check of --end */ + segno = endsegno; + } + + if (!XLByteInSeg(private.endptr, segno, private.segsize) && + private.endptr != (segno + 1) * private.segsize) + { + pg_log_error("end WAL location %X/%08X is not inside file \"%s\"", + LSN_FORMAT_ARGS(private.endptr), + argv[argc - 1]); + goto bad_argument; + } } } - else - waldir = identify_target_directory(waldir, NULL); + else if (!private.archive_name) + waldir = identify_target_directory(waldir, NULL, &private.segsize); /* we don't know what to print */ - if (XLogRecPtrIsInvalid(private.startptr)) + if (!XLogRecPtrIsValid(private.startptr)) { pg_log_error("no start WAL location given"); goto bad_argument; } + /* --follow is not supported with tar archives */ + if (config.follow && private.archive_name) + { + pg_log_error("--follow is not supported when reading from a tar archive"); + goto bad_argument; + } + /* done with argument parsing, do the actual work */ /* we have everything we need, start reading */ - xlogreader_state = - XLogReaderAllocate(WalSegSz, waldir, - XL_ROUTINE(.page_read = WALDumpReadPage, - .segment_open = WALDumpOpenSegment, - .segment_close = WALDumpCloseSegment), - &private); + if (private.archive_name) + { + /* + * A NULL directory indicates that the archive file is located in the + * current working directory. + */ + if (private.archive_dir == NULL) + private.archive_dir = pg_strdup("."); + + /* Set up for reading tar file */ + init_archive_reader(&private, compression); + + /* Routine to decode WAL files in tar archive */ + xlogreader_state = + XLogReaderAllocate(private.segsize, private.archive_dir, + XL_ROUTINE(.page_read = TarWALDumpReadPage, + .segment_open = TarWALDumpOpenSegment, + .segment_close = TarWALDumpCloseSegment), + &private); + } + else + { + xlogreader_state = + XLogReaderAllocate(private.segsize, waldir, + XL_ROUTINE(.page_read = WALDumpReadPage, + .segment_open = WALDumpOpenSegment, + .segment_close = WALDumpCloseSegment), + &private); + } + if (!xlogreader_state) pg_fatal("out of memory while allocating a WAL reading processor"); + /* + * Set up atexit cleanup of temporary directory. This must happen before + * archive_waldump.c could possibly create the temporary directory. Also + * arm the callback to cleanup the xlogreader state. + */ + atexit(cleanup_tmpwal_dir_atexit); + xlogreader_state_cleanup = xlogreader_state; + /* first find a valid recptr to start from */ - first_record = XLogFindNextRecord(xlogreader_state, private.startptr); + first_record = XLogFindNextRecord(xlogreader_state, private.startptr, &errormsg); - if (first_record == InvalidXLogRecPtr) - pg_fatal("could not find a valid record after %X/%X", - LSN_FORMAT_ARGS(private.startptr)); + if (!XLogRecPtrIsValid(first_record)) + { + if (errormsg) + pg_fatal("could not find a valid record after %X/%08X: %s", + LSN_FORMAT_ARGS(private.startptr), errormsg); + else + pg_fatal("could not find a valid record after %X/%08X", + LSN_FORMAT_ARGS(private.startptr)); + } /* * Display a message that we're skipping data if `from` wasn't a pointer @@ -1223,9 +1434,9 @@ main(int argc, char **argv) * a segment (e.g. we were used in file mode). */ if (first_record != private.startptr && - XLogSegmentOffset(private.startptr, WalSegSz) != 0) - pg_log_info(ngettext("first record is after %X/%X, at %X/%X, skipping over %u byte", - "first record is after %X/%X, at %X/%X, skipping over %u bytes", + XLogSegmentOffset(private.startptr, private.segsize) != 0) + pg_log_info(ngettext("first record is after %X/%08X, at %X/%08X, skipping over %u byte", + "first record is after %X/%08X, at %X/%08X, skipping over %u bytes", (first_record - private.startptr)), LSN_FORMAT_ARGS(private.startptr), LSN_FORMAT_ARGS(first_record), @@ -1234,6 +1445,9 @@ main(int argc, char **argv) if (config.stats == true && !config.quiet) stats.startptr = first_record; + /* Flag indicating that the decoding loop has been entered */ + private.decoding_started = true; + for (;;) { if (time_to_stop) @@ -1309,12 +1523,21 @@ main(int argc, char **argv) exit(0); if (errormsg) - pg_fatal("error in WAL record at %X/%X: %s", + pg_fatal("error in WAL record at %X/%08X: %s", LSN_FORMAT_ARGS(xlogreader_state->ReadRecPtr), errormsg); + /* + * Disarm atexit cleanup of open WAL file; XLogReaderFree will close it, + * and we don't want the atexit callback trying to touch freed memory. + */ + xlogreader_state_cleanup = NULL; + XLogReaderFree(xlogreader_state); + if (private.archive_name) + free_archive_reader(&private); + return EXIT_SUCCESS; bad_argument: diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h new file mode 100644 index 0000000000000..beb61ec5892ae --- /dev/null +++ b/src/bin/pg_waldump/pg_waldump.h @@ -0,0 +1,82 @@ +/*------------------------------------------------------------------------- + * + * pg_waldump.h - decode and display WAL + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/bin/pg_waldump/pg_waldump.h + *------------------------------------------------------------------------- + */ +#ifndef PG_WALDUMP_H +#define PG_WALDUMP_H + +#include "access/xlogdefs.h" +#include "fe_utils/astreamer.h" + +/* Forward declaration */ +struct ArchivedWALFile; +struct ArchivedWAL_hash; + +/* Temporary directory for spilling out-of-order WAL segments from archives */ +extern char *TmpWalSegDir; + +/* Contains the necessary information to drive WAL decoding */ +typedef struct XLogDumpPrivate +{ + TimeLineID timeline; + int segsize; + XLogRecPtr startptr; + XLogRecPtr endptr; + bool endptr_reached; + bool decoding_started; + + /* Fields required to read WAL from archive */ + char *archive_dir; + char *archive_name; /* Tar archive filename */ + int archive_fd; /* File descriptor for the open tar file */ + bool archive_fd_eof; /* Have we reached EOF on archive_fd? */ + + astreamer *archive_streamer; + char *archive_read_buf; /* Reusable read buffer for archive I/O */ + size_t archive_read_buf_size; + + /* + * The buffer for the WAL file the archive streamer is currently reading, + * or NULL if none. It is quite risky to examine this anywhere except in + * astreamer_waldump_content(), since it can change multiple times during + * a single read_archive_file() call. However, it is safe to assume that + * if cur_file is different from a particular ArchivedWALFile of interest, + * then the archive streamer has finished reading that file. + */ + struct ArchivedWALFile *cur_file; + + /* + * Hash table of WAL segments currently buffered from the archive, + * including any segment currently being streamed. Entries are removed + * once consumed, so this does not accumulate all segments ever read. + */ + struct ArchivedWAL_hash *archive_wal_htab; + + /* + * Pre-computed segment numbers derived from startptr and endptr. Caching + * them avoids repeated XLByteToSeg() calls when filtering each archive + * member against the requested WAL range. end_segno is initialized to + * UINT64_MAX when no end limit is requested. + */ + XLogSegNo start_segno; + XLogSegNo end_segno; +} XLogDumpPrivate; + +extern int open_file_in_directory(const char *directory, const char *fname); + +extern void init_archive_reader(XLogDumpPrivate *privateInfo, + pg_compress_algorithm compression); +extern void free_archive_reader(XLogDumpPrivate *privateInfo); +extern int read_archive_wal_page(XLogDumpPrivate *privateInfo, + XLogRecPtr targetPagePtr, + size_t count, char *readBuff); +extern void free_archive_wal_entry(const char *fname, + XLogDumpPrivate *privateInfo); + +#endif /* PG_WALDUMP_H */ diff --git a/src/bin/pg_waldump/po/meson.build b/src/bin/pg_waldump/po/meson.build index b69469699e959..59c30f9cfac1f 100644 --- a/src/bin/pg_waldump/po/meson.build +++ b/src/bin/pg_waldump/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_waldump-' + pg_version_major.to_string())] diff --git a/src/bin/pg_waldump/rmgrdesc.c b/src/bin/pg_waldump/rmgrdesc.c index fac509ed134e5..931ab8b979e23 100644 --- a/src/bin/pg_waldump/rmgrdesc.c +++ b/src/bin/pg_waldump/rmgrdesc.c @@ -24,7 +24,7 @@ #include "access/xlog_internal.h" #include "catalog/storage_xlog.h" #include "commands/dbcommands_xlog.h" -#include "commands/sequence.h" +#include "commands/sequence_xlog.h" #include "commands/tablespace.h" #include "replication/message.h" #include "replication/origin.h" diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl index f26d75e01cfd3..7dd1c3dd63e71 100644 --- a/src/bin/pg_waldump/t/001_basic.pl +++ b/src/bin/pg_waldump/t/001_basic.pl @@ -1,11 +1,17 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; +use Cwd; +use File::Copy; use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; use Test::More; +use List::Util qw(shuffle); + +my $tar = $ENV{TAR}; +my @tar_p_flags = tar_portability_options($tar); program_help_ok('pg_waldump'); program_version_ok('pg_waldump'); @@ -73,7 +79,8 @@ CommitTs ReplicationOrigin Generic -LogicalMessage$/, +LogicalMessage +XLOG2$/, 'rmgr list'); @@ -162,6 +169,42 @@ DROP TABLESPACE ts1; }); +# Test: Decode a continuation record (contrecord) that spans multiple WAL +# segments. +# +# Now consume all remaining room in the current WAL segment, leaving +# space enough only for the start of a largish record. +$node->safe_psql( + 'postgres', q{ +DO $$ +DECLARE + wal_segsize int := setting::int FROM pg_settings WHERE name = 'wal_segment_size'; + remain int; + iters int := 0; +BEGIN + LOOP + INSERT into t1(b) + select repeat(encode(sha256(g::text::bytea), 'hex'), (random() * 15 + 1)::int) + from generate_series(1, 10) g; + + remain := wal_segsize - (pg_current_wal_insert_lsn() - '0/0') % wal_segsize; + IF remain < 2 * setting::int from pg_settings where name = 'block_size' THEN + RAISE log 'exiting after % iterations, % bytes to end of WAL segment', iters, remain; + EXIT; + END IF; + iters := iters + 1; + END LOOP; +END +$$; +}); + +my $contrecord_lsn = $node->safe_psql('postgres', + 'SELECT pg_current_wal_insert_lsn()'); +# Generate contrecord record +$node->safe_psql('postgres', + qq{SELECT pg_logical_emit_message(true, 'test 026', repeat('xyzxz', 123456))} +); + my ($end_lsn, $end_walfile) = split /\|/, $node->safe_psql('postgres', q{SELECT pg_current_wal_insert_lsn(), pg_walfile_name(pg_current_wal_insert_lsn())} @@ -198,51 +241,50 @@ ], qr/./, 'runs with start and end segment specified'); -command_fails_like( - [ 'pg_waldump', '--path' => $node->data_dir ], - qr/error: no start WAL location given/, - 'path option requires start location'); -command_like( - [ - 'pg_waldump', - '--path' => $node->data_dir, - '--start' => $start_lsn, - '--end' => $end_lsn, - ], - qr/./, - 'runs with path option and start and end locations'); -command_fails_like( - [ - 'pg_waldump', - '--path' => $node->data_dir, - '--start' => $start_lsn, - ], - qr/error: error in WAL record at/, - 'falling off the end of the WAL results in an error'); - command_like( [ - 'pg_waldump', '--quiet', - $node->data_dir . '/pg_wal/' . $start_walfile + 'pg_waldump', '--quiet', '--path', + $node->data_dir . '/pg_wal/', $start_walfile ], qr/^$/, 'no output with --quiet option'); -command_fails_like( - [ - 'pg_waldump', '--quiet', - '--path' => $node->data_dir, - '--start' => $start_lsn - ], - qr/error: error in WAL record at/, - 'errors are shown with --quiet'); +# Test that pg_waldump reports a detailed error message when dumping +# a WAL file with an invalid magic number (0000). +# +# The broken WAL file is created by copying a valid WAL file and +# overwriting its magic number with 0000. +my $broken_wal_dir = PostgreSQL::Test::Utils::tempdir_short(); +my $broken_wal = "$broken_wal_dir/$start_walfile"; +copy($node->data_dir . '/pg_wal/' . $start_walfile, $broken_wal) + || die "copying $start_walfile $!"; + +my $fh; +open($fh, '+<', $broken_wal) + or BAIL_OUT("open failed: $!"); +binmode $fh; + +sysseek($fh, 0, 0) + or BAIL_OUT("sysseek failed: $!"); +syswrite($fh, pack("S", 0)) + or BAIL_OUT("syswrite failed: $!"); +close($fh) + or BAIL_OUT("close failed: $!"); + +command_fails_like( + [ 'pg_waldump', $broken_wal ], + qr/invalid magic number 0000/i, + 'detailed error message shown for invalid WAL page magic'); # Test for: Display a message that we're skipping data if `from` # wasn't a pointer to the start of a record. +sub test_pg_waldump_skip_bytes { + my ($path, $startlsn, $endlsn) = @_; + # Construct a new LSN that is one byte past the original # start_lsn. - my ($part1, $part2) = split qr{/}, $start_lsn; + my ($part1, $part2) = split qr{/}, $startlsn; my $lsn2 = hex $part2; $lsn2++; my $new_start = sprintf("%s/%X", $part1, $lsn2); @@ -252,7 +294,8 @@ my $result = IPC::Run::run [ 'pg_waldump', '--start' => $new_start, - $node->data_dir . '/pg_wal/' . $start_walfile + '--end' => $endlsn, + '--path' => $path, ], '>' => \$stdout, '2>' => \$stderr; @@ -266,15 +309,15 @@ sub test_pg_waldump { local $Test::Builder::Level = $Test::Builder::Level + 1; - my @opts = @_; + my ($path, $startlsn, $endlsn, @opts) = @_; my ($stdout, $stderr); my $result = IPC::Run::run [ 'pg_waldump', - '--path' => $node->data_dir, - '--start' => $start_lsn, - '--end' => $end_lsn, + '--start' => $startlsn, + '--end' => $endlsn, + '--path' => $path, @opts ], '>' => \$stdout, @@ -286,40 +329,145 @@ sub test_pg_waldump return @lines; } -my @lines; - -@lines = test_pg_waldump; -is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines'); - -@lines = test_pg_waldump('--limit' => 6); -is(@lines, 6, 'limit option observed'); - -@lines = test_pg_waldump('--fullpage'); -is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW'); - -@lines = test_pg_waldump('--stats'); -like($lines[0], qr/WAL statistics/, "statistics on stdout"); -is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); - -@lines = test_pg_waldump('--stats=record'); -like($lines[0], qr/WAL statistics/, "statistics on stdout"); -is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); - -@lines = test_pg_waldump('--rmgr' => 'Btree'); -is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines'); - -@lines = test_pg_waldump('--fork' => 'init'); -is(grep(!/fork init/, @lines), 0, 'only init fork lines'); - -@lines = test_pg_waldump( - '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_t1_oid"); -is(grep(!/rel $default_ts_oid\/$postgres_db_oid\/$rel_t1_oid/, @lines), - 0, 'only lines for selected relation'); - -@lines = test_pg_waldump( - '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid", - '--block' => 1); -is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block'); +# Create a tar archive, shuffle the file order +sub generate_archive +{ + my ($archive, $directory, $compression_flags) = @_; + + my @files; + opendir my $dh, $directory or die "opendir: $!"; + while (my $entry = readdir $dh) { + # Skip '.' and '..' + next if $entry eq '.' || $entry eq '..'; + push @files, $entry; + } + closedir $dh; + + @files = shuffle @files; + + # move into the WAL directory before archiving files + my $cwd = getcwd; + chdir($directory) || die "chdir: $!"; + command_ok([ $tar, @tar_p_flags, $compression_flags, $archive, @files ]); + chdir($cwd) || die "chdir: $!"; +} +my $tmp_dir = PostgreSQL::Test::Utils::tempdir_short(); + +my @scenarios = ( + { + 'path' => $node->data_dir, + 'is_archive' => 0, + 'enabled' => 1 + }, + { + 'path' => "$tmp_dir/pg_wal.tar", + 'compression_method' => 'none', + 'compression_flags' => '-cf', + 'is_archive' => 1, + 'enabled' => 1 + }, + { + 'path' => "$tmp_dir/pg_wal.tar.gz", + 'compression_method' => 'gzip', + 'compression_flags' => '-czf', + 'is_archive' => 1, + 'enabled' => check_pg_config("#define HAVE_LIBZ 1") + }); + +for my $scenario (@scenarios) +{ + my $path = $scenario->{'path'}; + + SKIP: + { + skip "tar command is not available", 56 + if (!defined $tar || $tar eq '') && $scenario->{'is_archive'}; + skip "$scenario->{'compression_method'} compression not supported by this build", 56 + if !$scenario->{'enabled'} && $scenario->{'is_archive'}; + + # create pg_wal archive + if ($scenario->{'is_archive'}) + { + generate_archive($path, + $node->data_dir . '/pg_wal', + $scenario->{'compression_flags'}); + } + + command_fails_like( + [ 'pg_waldump', '--path' => $path ], + qr/error: no start WAL location given/, + 'path option requires start location'); + command_like( + [ + 'pg_waldump', + '--path' => $path, + '--start' => $start_lsn, + '--end' => $end_lsn, + ], + qr/./, + 'runs with path option and start and end locations'); + command_fails_like( + [ + 'pg_waldump', + '--path' => $path, + '--start' => $start_lsn, + ], + qr/error: error in WAL record at/, + 'falling off the end of the WAL results in an error'); + + command_fails_like( + [ + 'pg_waldump', '--quiet', + '--path' => $path, + '--start' => $start_lsn + ], + qr/error: error in WAL record at/, + 'errors are shown with --quiet'); + + test_pg_waldump_skip_bytes($path, $start_lsn, $end_lsn); + + my @lines = test_pg_waldump($path, $start_lsn, $end_lsn); + is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines'); + + @lines = test_pg_waldump($path, $contrecord_lsn, $end_lsn); + is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines'); + + test_pg_waldump_skip_bytes($path, $contrecord_lsn, $end_lsn); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--limit' => 6); + is(@lines, 6, 'limit option observed'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--fullpage'); + is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--stats'); + like($lines[0], qr/WAL statistics/, "statistics on stdout"); + is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--stats=record'); + like($lines[0], qr/WAL statistics/, "statistics on stdout"); + is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--rmgr' => 'Btree'); + is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--fork' => 'init'); + is(grep(!/fork init/, @lines), 0, 'only init fork lines'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, + '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_t1_oid"); + is(grep(!/rel $default_ts_oid\/$postgres_db_oid\/$rel_t1_oid/, @lines), + 0, 'only lines for selected relation'); + + @lines = test_pg_waldump($path, $start_lsn, $end_lsn, + '--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid", + '--block' => 1); + is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block'); + + # Cleanup. + unlink $path if $scenario->{'is_archive'}; + } +} done_testing(); diff --git a/src/bin/pg_waldump/t/002_save_fullpage.pl b/src/bin/pg_waldump/t/002_save_fullpage.pl index 17b15e3a649ec..c38054a4c7bbe 100644 --- a/src/bin/pg_waldump/t/002_save_fullpage.pl +++ b/src/bin/pg_waldump/t/002_save_fullpage.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -105,8 +105,9 @@ sub get_block_lsn my ($hi_lsn_bk, $lo_lsn_bk) = get_block_lsn($fullpath, $blocksize); # The LSN on the block comes before the file's LSN. - ok( $hi_lsn_fn . $lo_lsn_fn gt $hi_lsn_bk . $lo_lsn_bk, - 'LSN stored in the file precedes the one stored in the block'); + ok( $hi_lsn_fn . $lo_lsn_fn ge $hi_lsn_bk . $lo_lsn_bk, + "LSN stored in the file $hi_lsn_fn/$lo_lsn_fn precedes the one stored in the block $hi_lsn_bk/$lo_lsn_bk" + ); } ok($file_count > 0, 'verify that at least one block has been saved'); diff --git a/src/bin/pg_walsummary/Makefile b/src/bin/pg_walsummary/Makefile index 1bc620572d279..7563c243c1b71 100644 --- a/src/bin/pg_walsummary/Makefile +++ b/src/bin/pg_walsummary/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pg_walsummary # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/pg_walsummary/Makefile diff --git a/src/bin/pg_walsummary/meson.build b/src/bin/pg_walsummary/meson.build index fe9af967b123b..d012275402bba 100644 --- a/src/bin/pg_walsummary/meson.build +++ b/src/bin/pg_walsummary/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_walsummary_sources = files( 'pg_walsummary.c', diff --git a/src/bin/pg_walsummary/pg_walsummary.c b/src/bin/pg_walsummary/pg_walsummary.c index cd7a6b147b289..aa214b8616de3 100644 --- a/src/bin/pg_walsummary/pg_walsummary.c +++ b/src/bin/pg_walsummary/pg_walsummary.c @@ -3,7 +3,7 @@ * pg_walsummary.c * Prints the contents of WAL summary files. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/bin/pg_walsummary/pg_walsummary.c @@ -217,8 +217,8 @@ dump_one_relation(ws_options *opt, RelFileLocator *rlocator, static int compare_block_numbers(const void *a, const void *b) { - BlockNumber aa = *(BlockNumber *) a; - BlockNumber bb = *(BlockNumber *) b; + BlockNumber aa = *(const BlockNumber *) a; + BlockNumber bb = *(const BlockNumber *) b; return pg_cmp_u32(aa, bb); } diff --git a/src/bin/pg_walsummary/po/meson.build b/src/bin/pg_walsummary/po/meson.build index 40ed4cdde42b4..299a56bc87353 100644 --- a/src/bin/pg_walsummary/po/meson.build +++ b/src/bin/pg_walsummary/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pg_walsummary-' + pg_version_major.to_string())] diff --git a/src/bin/pg_walsummary/t/001_basic.pl b/src/bin/pg_walsummary/t/001_basic.pl index 65509eef61ad8..7d4759228b7cd 100644 --- a/src/bin/pg_walsummary/t/001_basic.pl +++ b/src/bin/pg_walsummary/t/001_basic.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/pg_walsummary/t/002_blocks.pl b/src/bin/pg_walsummary/t/002_blocks.pl index 270332780a453..f5fe94f9f15a4 100644 --- a/src/bin/pg_walsummary/t/002_blocks.pl +++ b/src/bin/pg_walsummary/t/002_blocks.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -47,11 +47,12 @@ ok($result, "WAL summarization caught up after insert"); # The WAL summarizer should have generated some IO statistics. -my $stats_reads = $node1->safe_psql( +$node1->poll_query_until( 'postgres', - qq{SELECT sum(reads) > 0 FROM pg_stat_io - WHERE backend_type = 'walsummarizer' AND object = 'wal'}); -is($stats_reads, 't', "WAL summarizer generates statistics for WAL reads"); + q{SELECT sum(reads) > 0 FROM pg_stat_io + WHERE backend_type = 'walsummarizer' AND object = 'wal'}) + or die + "Timed out while waiting for WAL summarizer to generate statistics for WAL reads"; # Find the highest LSN that is summarized on disk. my $summarized_lsn = $node1->safe_psql('postgres', <etype = ENODE_CONSTANT; expr->u.constant.type = PGBT_NULL; @@ -178,7 +178,7 @@ make_null_constant(void) static PgBenchExpr * make_integer_constant(int64 ival) { - PgBenchExpr *expr = pg_malloc(sizeof(PgBenchExpr)); + PgBenchExpr *expr = pg_malloc_object(PgBenchExpr); expr->etype = ENODE_CONSTANT; expr->u.constant.type = PGBT_INT; @@ -189,7 +189,7 @@ make_integer_constant(int64 ival) static PgBenchExpr * make_double_constant(double dval) { - PgBenchExpr *expr = pg_malloc(sizeof(PgBenchExpr)); + PgBenchExpr *expr = pg_malloc_object(PgBenchExpr); expr->etype = ENODE_CONSTANT; expr->u.constant.type = PGBT_DOUBLE; @@ -200,7 +200,7 @@ make_double_constant(double dval) static PgBenchExpr * make_boolean_constant(bool bval) { - PgBenchExpr *expr = pg_malloc(sizeof(PgBenchExpr)); + PgBenchExpr *expr = pg_malloc_object(PgBenchExpr); expr->etype = ENODE_CONSTANT; expr->u.constant.type = PGBT_BOOLEAN; @@ -211,7 +211,7 @@ make_boolean_constant(bool bval) static PgBenchExpr * make_variable(char *varname) { - PgBenchExpr *expr = pg_malloc(sizeof(PgBenchExpr)); + PgBenchExpr *expr = pg_malloc_object(PgBenchExpr); expr->etype = ENODE_VARIABLE; expr->u.variable.varname = varname; @@ -415,12 +415,12 @@ make_elist(PgBenchExpr *expr, PgBenchExprList *list) if (list == NULL) { - list = pg_malloc(sizeof(PgBenchExprList)); + list = pg_malloc_object(PgBenchExprList); list->head = NULL; list->tail = NULL; } - cons = pg_malloc(sizeof(PgBenchExprLink)); + cons = pg_malloc_object(PgBenchExprLink); cons->expr = expr; cons->next = NULL; @@ -453,7 +453,7 @@ make_func(yyscan_t yyscanner, int fnumber, PgBenchExprList *args) { int len = elist_length(args); - PgBenchExpr *expr = pg_malloc(sizeof(PgBenchExpr)); + PgBenchExpr *expr = pg_malloc_object(PgBenchExpr); Assert(fnumber >= 0); diff --git a/src/bin/pgbench/exprscan.l b/src/bin/pgbench/exprscan.l index 5747af38cb2ca..f5e0c883c46c7 100644 --- a/src/bin/pgbench/exprscan.l +++ b/src/bin/pgbench/exprscan.l @@ -15,7 +15,7 @@ * * Note that this lexer operates within the framework created by psqlscan.l, * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/pgbench/exprscan.l diff --git a/src/bin/pgbench/meson.build b/src/bin/pgbench/meson.build index a2a909a8442b1..12e895796c12e 100644 --- a/src/bin/pgbench/meson.build +++ b/src/bin/pgbench/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgbench_sources = files( 'pgbench.c', diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c index 497a936c141f3..c969afab3a595 100644 --- a/src/bin/pgbench/pgbench.c +++ b/src/bin/pgbench/pgbench.c @@ -5,7 +5,7 @@ * Originally written by Tatsuo Ishii and enhanced by many contributors. * * src/bin/pgbench/pgbench.c - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * ALL RIGHTS RESERVED; * * Permission to use, copy, modify, and distribute this software and its @@ -402,14 +402,15 @@ typedef struct StatsData * directly successful transactions (they were successfully completed on * the first try). * - * A failed transaction is defined as unsuccessfully retried transactions. - * It can be one of two types: - * - * failed (the number of failed transactions) = + * 'failed' (the number of failed transactions) = * 'serialization_failures' (they got a serialization error and were not - * successfully retried) + + * successfully retried) + * 'deadlock_failures' (they got a deadlock error and were not - * successfully retried). + * successfully retried) + + * 'other_sql_failures' (they failed on the first try or after retries + * due to a SQL error other than serialization or + * deadlock; they are counted as a failed transaction + * only when --continue-on-error is specified). * * If the transaction was retried after a serialization or a deadlock * error this does not guarantee that this retry was successful. Thus @@ -421,7 +422,7 @@ typedef struct StatsData * * 'retried' (number of all retried transactions) = * successfully retried transactions + - * failed transactions. + * unsuccessful retried transactions. *---------- */ int64 cnt; /* number of successful transactions, not @@ -440,6 +441,11 @@ typedef struct StatsData int64 deadlock_failures; /* number of transactions that were not * successfully retried after a deadlock * error */ + int64 other_sql_failures; /* number of failed transactions for + * reasons other than + * serialization/deadlock failure, which + * is counted if --continue-on-error is + * specified */ SimpleStats latency; SimpleStats lag; } StatsData; @@ -457,6 +463,7 @@ typedef enum EStatus { ESTATUS_NO_ERROR = 0, ESTATUS_META_COMMAND_ERROR, + ESTATUS_CONN_ERROR, /* SQL errors */ ESTATUS_SERIALIZATION_ERROR, @@ -770,6 +777,7 @@ static int64 total_weight = 0; static bool verbose_errors = false; /* print verbose messages of all errors */ static bool exit_on_abort = false; /* exit when any client is aborted */ +static bool continue_on_error = false; /* continue after errors */ /* Builtin test scripts */ typedef struct BuiltinScript @@ -949,6 +957,7 @@ usage(void) " -T, --time=NUM duration of benchmark test in seconds\n" " -v, --vacuum-all vacuum all four standard tables before tests\n" " --aggregate-interval=NUM aggregate data over NUM seconds\n" + " --continue-on-error continue running after an SQL error\n" " --exit-on-abort exit when any client is aborted\n" " --failures-detailed report the failures grouped by basic types\n" " --log-prefix=PREFIX prefix for transaction time log file\n" @@ -973,13 +982,17 @@ usage(void) progname, progname, PACKAGE_BUGREPORT, PACKAGE_NAME, PACKAGE_URL); } -/* return whether str matches "^\s*[-+]?[0-9]+$" */ +/* + * Return whether str matches "^\s*[-+]?[0-9]+$" + * + * This should agree with strtoint64() on what's accepted, ignoring overflows. + */ static bool is_an_int(const char *str) { const char *ptr = str; - /* skip leading spaces; cast is consistent with strtoint64 */ + /* skip leading spaces */ while (*ptr && isspace((unsigned char) *ptr)) ptr++; @@ -1003,9 +1016,6 @@ is_an_int(const char *str) /* * strtoint64 -- convert a string to 64-bit integer * - * This function is a slightly modified version of pg_strtoint64() from - * src/backend/utils/adt/numutils.c. - * * The function returns whether the conversion worked, and if so * "*result" is set to the result. * @@ -1014,71 +1024,25 @@ is_an_int(const char *str) bool strtoint64(const char *str, bool errorOK, int64 *result) { - const char *ptr = str; - int64 tmp = 0; - bool neg = false; - - /* - * Do our own scan, rather than relying on sscanf which might be broken - * for long long. - * - * As INT64_MIN can't be stored as a positive 64 bit integer, accumulate - * value as a negative number. - */ - - /* skip leading spaces */ - while (*ptr && isspace((unsigned char) *ptr)) - ptr++; - - /* handle sign */ - if (*ptr == '-') - { - ptr++; - neg = true; - } - else if (*ptr == '+') - ptr++; + char *end; - /* require at least one digit */ - if (unlikely(!isdigit((unsigned char) *ptr))) - goto invalid_syntax; + errno = 0; + *result = strtoi64(str, &end, 10); - /* process digits */ - while (*ptr && isdigit((unsigned char) *ptr)) + if (unlikely(errno == ERANGE)) { - int8 digit = (*ptr++ - '0'); - - if (unlikely(pg_mul_s64_overflow(tmp, 10, &tmp)) || - unlikely(pg_sub_s64_overflow(tmp, digit, &tmp))) - goto out_of_range; + if (!errorOK) + pg_log_error("value \"%s\" is out of range for type bigint", str); + return false; } - /* allow trailing whitespace, but not other trailing chars */ - while (*ptr != '\0' && isspace((unsigned char) *ptr)) - ptr++; - - if (unlikely(*ptr != '\0')) - goto invalid_syntax; - - if (!neg) + if (unlikely(errno != 0 || end == str || *end != '\0')) { - if (unlikely(tmp == PG_INT64_MIN)) - goto out_of_range; - tmp = -tmp; + if (!errorOK) + pg_log_error("invalid input syntax for type bigint: \"%s\"", str); + return false; } - - *result = tmp; return true; - -out_of_range: - if (!errorOK) - pg_log_error("value \"%s\" is out of range for type bigint", str); - return false; - -invalid_syntax: - if (!errorOK) - pg_log_error("invalid input syntax for type bigint: \"%s\"", str); - return false; } /* convert string to double, detecting overflows/underflows */ @@ -1090,14 +1054,14 @@ strtodouble(const char *str, bool errorOK, double *dv) errno = 0; *dv = strtod(str, &end); - if (unlikely(errno != 0)) + if (unlikely(errno == ERANGE)) { if (!errorOK) pg_log_error("value \"%s\" is out of range for type double", str); return false; } - if (unlikely(end == str || *end != '\0')) + if (unlikely(errno != 0 || end == str || *end != '\0')) { if (!errorOK) pg_log_error("invalid input syntax for type double: \"%s\"", str); @@ -1467,6 +1431,7 @@ initStats(StatsData *sd, pg_time_usec_t start) sd->retried = 0; sd->serialization_failures = 0; sd->deadlock_failures = 0; + sd->other_sql_failures = 0; initSimpleStats(&sd->latency); initSimpleStats(&sd->lag); } @@ -1516,6 +1481,9 @@ accumStats(StatsData *stats, bool skipped, double lat, double lag, case ESTATUS_DEADLOCK_ERROR: stats->deadlock_failures++; break; + case ESTATUS_OTHER_SQL_ERROR: + stats->other_sql_failures++; + break; default: /* internal error which should never occur */ pg_fatal("unexpected error status: %d", estatus); @@ -1806,7 +1774,7 @@ enlargeVariables(Variables *variables, int needed) { variables->max_vars = needed + VARIABLES_ALLOC_MARGIN; variables->vars = (Variable *) - pg_realloc(variables->vars, variables->max_vars * sizeof(Variable)); + pg_realloc_array(variables->vars, Variable, variables->max_vars); } } @@ -3059,7 +3027,14 @@ commandFailed(CState *st, const char *cmd, const char *message) static void commandError(CState *st, const char *message) { - Assert(sql_script[st->use_file].commands[st->command]->type == SQL_COMMAND); + /* + * Errors should only be detected during an SQL command or the + * \endpipeline meta command. Any other case triggers an assertion + * failure. + */ + Assert(sql_script[st->use_file].commands[st->command]->type == SQL_COMMAND || + sql_script[st->use_file].commands[st->command]->meta == META_ENDPIPELINE); + pg_log_info("client %d got an error in command %d (SQL) of script %d; %s", st->id, st->command, st->use_file, message); } @@ -3092,7 +3067,7 @@ allocCStatePrepared(CState *st) { Assert(st->prepared == NULL); - st->prepared = pg_malloc(sizeof(bool *) * num_scripts); + st->prepared = pg_malloc_array(bool *, num_scripts); for (int i = 0; i < num_scripts; i++) { ParsedScript *script = &sql_script[i]; @@ -3100,7 +3075,7 @@ allocCStatePrepared(CState *st) for (numcmds = 0; script->commands[numcmds] != NULL; numcmds++) ; - st->prepared[i] = pg_malloc0(sizeof(bool) * numcmds); + st->prepared[i] = pg_malloc0_array(bool, numcmds); } } @@ -3224,11 +3199,43 @@ sendCommand(CState *st, Command *command) } /* - * Get the error status from the error code. + * Read and discard all available results from the connection. + */ +static void +discardAvailableResults(CState *st) +{ + PGresult *res = NULL; + + for (;;) + { + res = PQgetResult(st->con); + + /* + * Read and discard results until PQgetResult() returns NULL (no more + * results) or a connection failure is detected. If the pipeline + * status is PQ_PIPELINE_ABORTED, more results may still be available + * even after PQgetResult() returns NULL, so continue reading in that + * case. + */ + if ((res == NULL && PQpipelineStatus(st->con) != PQ_PIPELINE_ABORTED) || + PQstatus(st->con) == CONNECTION_BAD) + break; + + PQclear(res); + } + PQclear(res); +} + +/* + * Determine the error status based on the connection status and error code. */ static EStatus -getSQLErrorStatus(const char *sqlState) +getSQLErrorStatus(CState *st, const char *sqlState) { + discardAvailableResults(st); + if (PQstatus(st->con) == CONNECTION_BAD) + return ESTATUS_CONN_ERROR; + if (sqlState != NULL) { if (strcmp(sqlState, ERRCODE_T_R_SERIALIZATION_FAILURE) == 0) @@ -3250,6 +3257,17 @@ canRetryError(EStatus estatus) estatus == ESTATUS_DEADLOCK_ERROR); } +/* + * Returns true if --continue-on-error is specified and this error allows + * processing to continue. + */ +static bool +canContinueOnError(EStatus estatus) +{ + return (continue_on_error && + estatus == ESTATUS_OTHER_SQL_ERROR); +} + /* * Process query response from the backend. * @@ -3349,26 +3367,40 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix) st->num_syncs--; if (st->num_syncs == 0 && PQexitPipelineMode(st->con) != 1) pg_log_error("client %d failed to exit pipeline mode: %s", st->id, - PQerrorMessage(st->con)); + PQresultErrorMessage(res)); break; + case PGRES_COPY_IN: + case PGRES_COPY_OUT: + case PGRES_COPY_BOTH: + pg_log_error("COPY is not supported in pgbench, aborting"); + + /* + * We need to exit the copy state. Otherwise, PQgetResult() + * will always return an empty PGresult as an effect of + * getCopyResult(), leading to an infinite loop in the error + * cleanup done below. + */ + PQendcopy(st->con); + goto error; + case PGRES_NONFATAL_ERROR: case PGRES_FATAL_ERROR: - st->estatus = getSQLErrorStatus(PQresultErrorField(res, - PG_DIAG_SQLSTATE)); - if (canRetryError(st->estatus)) + st->estatus = getSQLErrorStatus(st, PQresultErrorField(res, + PG_DIAG_SQLSTATE)); + if (canRetryError(st->estatus) || canContinueOnError(st->estatus)) { if (verbose_errors) - commandError(st, PQerrorMessage(st->con)); + commandError(st, PQresultErrorMessage(res)); goto error; } - /* fall through */ + pg_fallthrough; default: /* anything else is unexpected */ pg_log_error("client %d script %d aborted in command %d query %d: %s", st->id, st->use_file, st->command, qrynum, - PQerrorMessage(st->con)); + PQresultErrorMessage(res)); goto error; } @@ -3388,11 +3420,7 @@ readCommandResponse(CState *st, MetaCommand meta, char *varprefix) error: PQclear(res); PQclear(next_res); - do - { - res = PQgetResult(st->con); - PQclear(res); - } while (res); + discardAvailableResults(st); return false; } @@ -3490,12 +3518,18 @@ doRetry(CState *st, pg_time_usec_t *now) } /* - * Read results and discard it until a sync point. + * Read and discard results until the last sync point. */ static int discardUntilSync(CState *st) { - /* send a sync */ + bool received_sync = false; + + /* + * Send a Sync message to ensure at least one PGRES_PIPELINE_SYNC is + * received and to avoid an infinite loop, since all earlier ones may have + * already been received. + */ if (!PQpipelineSync(st->con)) { pg_log_error("client %d aborted: failed to send a pipeline sync", @@ -3503,18 +3537,42 @@ discardUntilSync(CState *st) return 0; } - /* receive PGRES_PIPELINE_SYNC and null following it */ + /* + * Continue reading results until the last sync point, i.e., until + * reaching null just after PGRES_PIPELINE_SYNC. + */ for (;;) { PGresult *res = PQgetResult(st->con); - if (PQresultStatus(res) == PGRES_PIPELINE_SYNC) + if (PQstatus(st->con) == CONNECTION_BAD) { + pg_log_error("client %d aborted while rolling back the transaction after an error; perhaps the backend died while processing", + st->id); PQclear(res); - res = PQgetResult(st->con); - Assert(res == NULL); + return 0; + } + + if (PQresultStatus(res) == PGRES_PIPELINE_SYNC) + received_sync = true; + else if (received_sync && res == NULL) + { + /* + * Reset ongoing sync count to 0 since all PGRES_PIPELINE_SYNC + * results have been discarded. + */ + st->num_syncs = 0; break; } + else + { + /* + * If a PGRES_PIPELINE_SYNC is followed by something other than + * PGRES_PIPELINE_SYNC or NULL, another PGRES_PIPELINE_SYNC will + * appear later. Reset received_sync to false to wait for it. + */ + received_sync = false; + } PQclear(res); } @@ -3549,7 +3607,7 @@ getTransactionStatus(PGconn *con) /* PQTRANS_UNKNOWN is expected given a broken connection */ if (PQstatus(con) == CONNECTION_BAD) return TSTATUS_CONN_ERROR; - /* fall through */ + pg_fallthrough; case PQTRANS_ACTIVE: default: @@ -4007,7 +4065,7 @@ advanceConnectionState(TState *thread, CState *st, StatsData *agg) if (PQpipelineStatus(st->con) != PQ_PIPELINE_ON) st->state = CSTATE_END_COMMAND; } - else if (canRetryError(st->estatus)) + else if (canRetryError(st->estatus) || canContinueOnError(st->estatus)) st->state = CSTATE_ERROR; else st->state = CSTATE_ABORTED; @@ -4528,7 +4586,8 @@ static int64 getFailures(const StatsData *stats) { return (stats->serialization_failures + - stats->deadlock_failures); + stats->deadlock_failures + + stats->other_sql_failures); } /* @@ -4548,6 +4607,8 @@ getResultString(bool skipped, EStatus estatus) return "serialization"; case ESTATUS_DEADLOCK_ERROR: return "deadlock"; + case ESTATUS_OTHER_SQL_ERROR: + return "other"; default: /* internal error which should never occur */ pg_fatal("unexpected error status: %d", estatus); @@ -4603,6 +4664,7 @@ doLog(TState *thread, CState *st, int64 skipped = 0; int64 serialization_failures = 0; int64 deadlock_failures = 0; + int64 other_sql_failures = 0; int64 retried = 0; int64 retries = 0; @@ -4643,10 +4705,12 @@ doLog(TState *thread, CState *st, { serialization_failures = agg->serialization_failures; deadlock_failures = agg->deadlock_failures; + other_sql_failures = agg->other_sql_failures; } - fprintf(logfile, " " INT64_FORMAT " " INT64_FORMAT, + fprintf(logfile, " " INT64_FORMAT " " INT64_FORMAT " " INT64_FORMAT, serialization_failures, - deadlock_failures); + deadlock_failures, + other_sql_failures); fputc('\n', logfile); @@ -5586,7 +5650,7 @@ skip_sql_comments(char *sql_command) * struct. */ static Command * -create_sql_command(PQExpBuffer buf, const char *source) +create_sql_command(PQExpBuffer buf) { Command *my_command; char *p = skip_sql_comments(buf->data); @@ -5595,7 +5659,7 @@ create_sql_command(PQExpBuffer buf, const char *source) return NULL; /* Allocate and initialize Command structure */ - my_command = (Command *) pg_malloc(sizeof(Command)); + my_command = pg_malloc0_object(Command); initPQExpBuffer(&my_command->lines); appendPQExpBufferStr(&my_command->lines, p); my_command->first_line = NULL; /* this is set later */ @@ -5656,7 +5720,7 @@ postprocess_sql_command(Command *my_command) break; case QUERY_PREPARED: my_command->prepname = psprintf("P_%d", prepnum++); - /* fall through */ + pg_fallthrough; case QUERY_EXTENDED: if (!parseQuery(my_command)) exit(1); @@ -5691,7 +5755,7 @@ process_backslash_command(PsqlScanState sstate, const char *source, } /* Allocate and initialize Command structure */ - my_command = (Command *) pg_malloc0(sizeof(Command)); + my_command = pg_malloc0_object(Command); my_command->type = META_COMMAND; my_command->argc = 0; initSimpleStats(&my_command->stats); @@ -5947,7 +6011,7 @@ ParseScript(const char *script, const char *desc, int weight) /* Initialize all fields of ps */ ps.desc = desc; ps.weight = weight; - ps.commands = (Command **) pg_malloc(sizeof(Command *) * alloc_num); + ps.commands = pg_malloc_array(Command *, alloc_num); initStats(&ps.stats, 0); /* Prepare to parse script */ @@ -5979,7 +6043,7 @@ ParseScript(const char *script, const char *desc, int weight) sr = psql_scan(sstate, &line_buf, &prompt); /* If we collected a new SQL command, process that */ - command = create_sql_command(&line_buf, desc); + command = create_sql_command(&line_buf); /* store new command */ if (command) @@ -6050,7 +6114,7 @@ ParseScript(const char *script, const char *desc, int weight) { alloc_num += COMMANDS_ALLOC_NUM; ps.commands = (Command **) - pg_realloc(ps.commands, sizeof(Command *) * alloc_num); + pg_realloc_array(ps.commands, Command *, alloc_num); } /* Done if we reached EOF */ @@ -6190,7 +6254,7 @@ findBuiltin(const char *name) static int parseScriptWeight(const char *option, char **script) { - char *sep; + const char *sep; int weight; if ((sep = strrchr(option, WSEP))) @@ -6210,8 +6274,8 @@ parseScriptWeight(const char *option, char **script) if (errno != 0 || badp == sep + 1 || *badp != '\0') pg_fatal("invalid weight specification: %s", sep); if (wtmp > INT_MAX || wtmp < 0) - pg_fatal("weight specification out of range (0 .. %d): %lld", - INT_MAX, (long long) wtmp); + pg_fatal("weight specification out of range (0 .. %d): %ld", + INT_MAX, wtmp); weight = wtmp; } else @@ -6285,6 +6349,7 @@ printProgressReport(TState *threads, int64 test_start, pg_time_usec_t now, cur.serialization_failures += threads[i].stats.serialization_failures; cur.deadlock_failures += threads[i].stats.deadlock_failures; + cur.other_sql_failures += threads[i].stats.other_sql_failures; } /* we count only actually executed transactions */ @@ -6427,7 +6492,8 @@ printResults(StatsData *total, /* * Remaining stats are nonsensical if we failed to execute any xacts due - * to others than serialization or deadlock errors + * to other than serialization or deadlock errors and --continue-on-error + * is not set. */ if (total_cnt <= 0) return; @@ -6443,6 +6509,9 @@ printResults(StatsData *total, printf("number of deadlock failures: " INT64_FORMAT " (%.3f%%)\n", total->deadlock_failures, 100.0 * total->deadlock_failures / total_cnt); + printf("number of other failures: " INT64_FORMAT " (%.3f%%)\n", + total->other_sql_failures, + 100.0 * total->other_sql_failures / total_cnt); } /* it can be non-zero only if max_tries is not equal to one */ @@ -6546,6 +6615,10 @@ printResults(StatsData *total, sstats->deadlock_failures, (100.0 * sstats->deadlock_failures / script_total_cnt)); + printf(" - number of other failures: " INT64_FORMAT " (%.3f%%)\n", + sstats->other_sql_failures, + (100.0 * sstats->other_sql_failures / + script_total_cnt)); } /* @@ -6705,6 +6778,7 @@ main(int argc, char **argv) {"verbose-errors", no_argument, NULL, 15}, {"exit-on-abort", no_argument, NULL, 16}, {"debug", no_argument, NULL, 17}, + {"continue-on-error", no_argument, NULL, 18}, {NULL, 0, NULL, 0} }; @@ -6746,6 +6820,9 @@ main(int argc, char **argv) int exit_code = 0; struct timeval tv; + /* initialize timing infrastructure (required for INSTR_* calls) */ + pg_initialize_timing(); + /* * Record difference between Unix time and instr_time time. We'll use * this for logging and aggregation. @@ -6770,7 +6847,7 @@ main(int argc, char **argv) } } - state = (CState *) pg_malloc0(sizeof(CState)); + state = pg_malloc0_object(CState); /* set random seed early, because it may be used while parsing scripts. */ if (!set_random_seed(getenv("PGBENCH_RANDOM_SEED"))) @@ -7058,6 +7135,10 @@ main(int argc, char **argv) case 17: /* debug */ pg_logging_increase_verbosity(); break; + case 18: /* continue-on-error */ + benchmarking_option_set = true; + continue_on_error = true; + break; default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -7220,7 +7301,7 @@ main(int argc, char **argv) if (nclients > 1) { - state = (CState *) pg_realloc(state, sizeof(CState) * nclients); + state = pg_realloc_array(state, CState, nclients); memset(state + 1, 0, sizeof(CState) * (nclients - 1)); /* copy any -D switch values to all clients */ @@ -7334,7 +7415,7 @@ main(int argc, char **argv) PQfinish(con); /* set up thread data structures */ - threads = (TState *) pg_malloc(sizeof(TState) * nthreads); + threads = pg_malloc_array(TState, nthreads); nclients_dealt = 0; for (i = 0; i < nthreads; i++) @@ -7413,6 +7494,7 @@ main(int argc, char **argv) stats.retried += thread->stats.retried; stats.serialization_failures += thread->stats.serialization_failures; stats.deadlock_failures += thread->stats.deadlock_failures; + stats.other_sql_failures += thread->stats.other_sql_failures; latency_late += thread->latency_late; conn_total_duration += thread->conn_duration; @@ -7914,7 +7996,7 @@ socket_has_input(socket_set *sa, int fd, int idx) static socket_set * alloc_socket_set(int count) { - return (socket_set *) pg_malloc0(sizeof(socket_set)); + return pg_malloc0_object(socket_set); } static void diff --git a/src/bin/pgbench/pgbench.h b/src/bin/pgbench/pgbench.h index e053c9e2eb63d..4a8f7d2fecf0e 100644 --- a/src/bin/pgbench/pgbench.h +++ b/src/bin/pgbench/pgbench.h @@ -2,7 +2,7 @@ * * pgbench.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- @@ -16,11 +16,11 @@ /* * This file is included outside exprscan.l, in places where we can't see * flex's definition of typedef yyscan_t. Fortunately, it's documented as - * being "void *", so we can use a macro to keep the function declarations + * being "void *", so we can use typedef to keep the function declarations * here looking like the definitions in exprscan.l. exprparse.y and * pgbench.c also use this to be able to declare things as "yyscan_t". */ -#define yyscan_t void * +typedef void *yyscan_t; /* * Likewise, we can't see exprparse.y's definition of union YYSTYPE here, diff --git a/src/bin/pgbench/t/001_pgbench_with_server.pl b/src/bin/pgbench/t/001_pgbench_with_server.pl index 7dd7894030032..b7685ea5d20fc 100644 --- a/src/bin/pgbench/t/001_pgbench_with_server.pl +++ b/src/bin/pgbench/t/001_pgbench_with_server.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -753,15 +753,23 @@ sub check_data_state 'SELECT seed, rand, val, COUNT(*) FROM seeded_random GROUP BY seed, rand, val' ); -ok($ret == 0, "psql seeded_random count ok"); -ok($err eq '', "psql seeded_random count stderr is empty"); -ok($out =~ /\b$seed\|uniform\|1\d\d\d\|2/, +is($ret, 0, "psql seeded_random count ok"); +is($err, '', "psql seeded_random count stderr is empty"); +like( + $out, + qr/\b$seed\|uniform\|1\d\d\d\|2/, "psql seeded_random count uniform"); -ok( $out =~ /\b$seed\|exponential\|2\d\d\d\|2/, +like( + $out, + qr/\b$seed\|exponential\|2\d\d\d\|2/, "psql seeded_random count exponential"); -ok( $out =~ /\b$seed\|gaussian\|3\d\d\d\|2/, +like( + $out, + qr/\b$seed\|gaussian\|3\d\d\d\|2/, "psql seeded_random count gaussian"); -ok($out =~ /\b$seed\|zipfian\|4\d\d\d\|2/, +like( + $out, + qr/\b$seed\|zipfian\|4\d\d\d\|2/, "psql seeded_random count zipfian"); $node->safe_psql('postgres', 'DROP TABLE seeded_random;'); @@ -1521,8 +1529,9 @@ sub check_pgbench_logs # $prefix is simple enough, thus does not need escaping my @logs = list_files($dir, qr{^$prefix\..*$}); - ok(@logs == $nb, "number of log files"); - ok(grep(/\/$prefix\.\d+(\.\d+)?$/, @logs) == $nb, "file name format"); + is(scalar(@logs), $nb, "number of log files"); + is(scalar(grep(/\/$prefix\.\d+(\.\d+)?$/, @logs)), + $nb, "file name format"); my $log_number = 0; for my $log (sort @logs) @@ -1532,10 +1541,12 @@ sub check_pgbench_logs my @contents = split(/\n/, $contents_raw); my $clen = @contents; - ok( $min <= $clen && $clen <= $max, - "transaction count for $log ($clen)"); + cmp_ok($clen, '>=', $min, + "transaction count for $log ($clen) is above min"); + cmp_ok($clen, '<=', $max, + "transaction count for $log ($clen) is below max"); my $clen_match = grep(/$re/, @contents); - ok($clen_match == $clen, "transaction format for $prefix"); + is($clen_match, $clen, "transaction format for $prefix"); # Show more information if some logs don't match # to help with debugging. @@ -1810,9 +1821,42 @@ BEGIN } }); +# Test copy in pgbench +$node->pgbench( + '-t 10', + 2, + [], + [ qr{COPY is not supported in pgbench, aborting} ], + 'Test copy in script', + { + '001_copy' => q{ COPY pgbench_accounts FROM stdin } + }); + # Clean up $node->safe_psql('postgres', 'DROP TABLE counter;'); +# Test --continue-on-error +$node->safe_psql('postgres', + 'CREATE TABLE unique_table(i int unique);'); + +$node->pgbench( + '-n -t 10 --continue-on-error --failures-detailed', + 0, + [ + qr{processed: 1/10\b}, + qr{other failures: 9\b} + ], + [], + 'test --continue-on-error', + { + '001_continue_on_error' => q{ + INSERT INTO unique_table VALUES(0); + } + }); + +# Clean up +$node->safe_psql('postgres', 'DROP TABLE unique_table;'); + # done $node->safe_psql('postgres', 'DROP TABLESPACE regress_pgbench_tap_1_ts'); $node->stop; diff --git a/src/bin/pgbench/t/002_pgbench_no_server.pl b/src/bin/pgbench/t/002_pgbench_no_server.pl index f975c73dd758a..e694e9ef0f83b 100644 --- a/src/bin/pgbench/t/002_pgbench_no_server.pl +++ b/src/bin/pgbench/t/002_pgbench_no_server.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # pgbench tests which do not need a server @@ -233,21 +233,9 @@ sub pgbench_scripts 'pgbench option error: ' . $name); } -# Help -pgbench( - '--help', 0, - [ - qr{benchmarking tool for PostgreSQL}, - qr{Usage}, - qr{Initialization options:}, - qr{Common options:}, - qr{Report bugs to} - ], - [qr{^$}], - 'pgbench help'); - -# Version -pgbench('-V', 0, [qr{^pgbench .PostgreSQL. }], [qr{^$}], 'pgbench version'); +program_help_ok('pgbench'); +program_version_ok('pgbench'); +program_options_handling_ok('pgbench'); # list of builtins pgbench( diff --git a/src/bin/pgevent/Makefile b/src/bin/pgevent/Makefile index f2adcce990f69..2a607ff586e39 100644 --- a/src/bin/pgevent/Makefile +++ b/src/bin/pgevent/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/pgevent # -# Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Copyright (c) 1996-2026, PostgreSQL Global Development Group # #------------------------------------------------------------------------- diff --git a/src/bin/pgevent/meson.build b/src/bin/pgevent/meson.build index 6ec04e9548d39..df850e4904034 100644 --- a/src/bin/pgevent/meson.build +++ b/src/bin/pgevent/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if host_system != 'windows' subdir_done() diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile index 5b1545d9948ad..be0032652cd78 100644 --- a/src/bin/psql/Makefile +++ b/src/bin/psql/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/psql # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/psql/Makefile diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 81a5ba844ba0f..493400f90900b 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/command.c */ @@ -67,8 +67,8 @@ static backslashResult exec_command_C(PsqlScanState scan_state, bool active_bran static backslashResult exec_command_connect(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_cd(PsqlScanState scan_state, bool active_branch, const char *cmd); -static backslashResult exec_command_close(PsqlScanState scan_state, bool active_branch, - const char *cmd); +static backslashResult exec_command_close_prepared(PsqlScanState scan_state, + bool active_branch, const char *cmd); static backslashResult exec_command_conninfo(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_copy(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_copyright(PsqlScanState scan_state, bool active_branch); @@ -130,6 +130,8 @@ static backslashResult exec_command_pset(PsqlScanState scan_state, bool active_b static backslashResult exec_command_quit(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_reset(PsqlScanState scan_state, bool active_branch, PQExpBuffer query_buf); +static backslashResult exec_command_restrict(PsqlScanState scan_state, bool active_branch, + const char *cmd); static backslashResult exec_command_s(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_sendpipeline(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_set(PsqlScanState scan_state, bool active_branch); @@ -142,6 +144,8 @@ static backslashResult exec_command_syncpipeline(PsqlScanState scan_state, bool static backslashResult exec_command_t(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_T(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_timing(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_unrestrict(PsqlScanState scan_state, bool active_branch, + const char *cmd); static backslashResult exec_command_unset(PsqlScanState scan_state, bool active_branch, const char *cmd); static backslashResult exec_command_write(PsqlScanState scan_state, bool active_branch, @@ -192,6 +196,8 @@ static char *pset_value_string(const char *param, printQueryOpt *popt); static void checkWin32Codepage(void); #endif +static bool restricted; +static char *restrict_key; /*---------- @@ -237,8 +243,19 @@ HandleSlashCmds(PsqlScanState scan_state, /* Parse off the command name */ cmd = psql_scan_slash_command(scan_state); - /* And try to execute it */ - status = exec_command(cmd, scan_state, cstack, query_buf, previous_buf); + /* + * And try to execute it. + * + * If we are in "restricted" mode, the only allowable backslash command is + * \unrestrict (to exit restricted mode). + */ + if (restricted && strcmp(cmd, "unrestrict") != 0) + { + pg_log_error("backslash commands are restricted; only \\unrestrict is allowed"); + status = PSQL_CMD_ERROR; + } + else + status = exec_command(cmd, scan_state, cstack, query_buf, previous_buf); if (status == PSQL_CMD_UNKNOWN) { @@ -330,8 +347,8 @@ exec_command(const char *cmd, status = exec_command_connect(scan_state, active_branch); else if (strcmp(cmd, "cd") == 0) status = exec_command_cd(scan_state, active_branch, cmd); - else if (strcmp(cmd, "close") == 0) - status = exec_command_close(scan_state, active_branch, cmd); + else if (strcmp(cmd, "close_prepared") == 0) + status = exec_command_close_prepared(scan_state, active_branch, cmd); else if (strcmp(cmd, "conninfo") == 0) status = exec_command_conninfo(scan_state, active_branch); else if (pg_strcasecmp(cmd, "copy") == 0) @@ -416,6 +433,8 @@ exec_command(const char *cmd, status = exec_command_quit(scan_state, active_branch); else if (strcmp(cmd, "r") == 0 || strcmp(cmd, "reset") == 0) status = exec_command_reset(scan_state, active_branch, query_buf); + else if (strcmp(cmd, "restrict") == 0) + status = exec_command_restrict(scan_state, active_branch, cmd); else if (strcmp(cmd, "s") == 0) status = exec_command_s(scan_state, active_branch); else if (strcmp(cmd, "sendpipeline") == 0) @@ -438,6 +457,8 @@ exec_command(const char *cmd, status = exec_command_T(scan_state, active_branch); else if (strcmp(cmd, "timing") == 0) status = exec_command_timing(scan_state, active_branch); + else if (strcmp(cmd, "unrestrict") == 0) + status = exec_command_unrestrict(scan_state, active_branch, cmd); else if (strcmp(cmd, "unset") == 0) status = exec_command_unset(scan_state, active_branch, cmd); else if (strcmp(cmd, "w") == 0 || strcmp(cmd, "write") == 0) @@ -728,10 +749,10 @@ exec_command_cd(PsqlScanState scan_state, bool active_branch, const char *cmd) } /* - * \close -- close a previously prepared statement + * \close_prepared -- close a previously prepared statement */ static backslashResult -exec_command_close(PsqlScanState scan_state, bool active_branch, const char *cmd) +exec_command_close_prepared(PsqlScanState scan_state, bool active_branch, const char *cmd) { backslashResult status = PSQL_CMD_SKIP_LINE; @@ -778,6 +799,7 @@ exec_command_conninfo(PsqlScanState scan_state, bool active_branch) int ssl_in_use, password_used, gssapi_used; + int version_num; char *paramval; if (!active_branch) @@ -793,7 +815,9 @@ exec_command_conninfo(PsqlScanState scan_state, bool active_branch) /* Get values for the parameters */ host = PQhost(pset.db); hostaddr = PQhostaddr(pset.db); - protocol_version = psprintf("%d", PQprotocolVersion(pset.db)); + version_num = PQfullProtocolVersion(pset.db); + protocol_version = psprintf("%d.%d", version_num / 10000, + version_num % 10000); ssl_in_use = PQsslInUse(pset.db); password_used = PQconnectionUsedPassword(pset.db); gssapi_used = PQconnectionUsedGSSAPI(pset.db); @@ -874,11 +898,11 @@ exec_command_conninfo(PsqlScanState scan_state, bool active_branch) printTableAddCell(&cont, _("Backend PID"), false, false); printTableAddCell(&cont, backend_pid, false, false); - /* TLS Connection */ - printTableAddCell(&cont, _("TLS Connection"), false, false); + /* SSL Connection */ + printTableAddCell(&cont, _("SSL Connection"), false, false); printTableAddCell(&cont, ssl_in_use ? _("true") : _("false"), false, false); - /* TLS Information */ + /* SSL Information */ if (ssl_in_use) { char *library, @@ -895,19 +919,19 @@ exec_command_conninfo(PsqlScanState scan_state, bool active_branch) compression = (char *) PQsslAttribute(pset.db, "compression"); alpn = (char *) PQsslAttribute(pset.db, "alpn"); - printTableAddCell(&cont, _("TLS Library"), false, false); + printTableAddCell(&cont, _("SSL Library"), false, false); printTableAddCell(&cont, library ? library : _("unknown"), false, false); - printTableAddCell(&cont, _("TLS Protocol"), false, false); + printTableAddCell(&cont, _("SSL Protocol"), false, false); printTableAddCell(&cont, protocol ? protocol : _("unknown"), false, false); - printTableAddCell(&cont, _("TLS Key Bits"), false, false); + printTableAddCell(&cont, _("SSL Key Bits"), false, false); printTableAddCell(&cont, key_bits ? key_bits : _("unknown"), false, false); - printTableAddCell(&cont, _("TLS Cipher"), false, false); + printTableAddCell(&cont, _("SSL Cipher"), false, false); printTableAddCell(&cont, cipher ? cipher : _("unknown"), false, false); - printTableAddCell(&cont, _("TLS Compression"), false, false); + printTableAddCell(&cont, _("SSL Compression"), false, false); printTableAddCell(&cont, (compression && strcmp(compression, "off") != 0) ? _("true") : _("false"), false, false); @@ -1032,7 +1056,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) success = describeTableDetails(pattern, show_verbose, show_system); else /* standard listing of interesting things */ - success = listTables("tvmsE", NULL, show_verbose, show_system); + success = listTables("tvmsEG", NULL, show_verbose, show_system); break; case 'A': { @@ -1166,6 +1190,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) case 'i': case 's': case 'E': + case 'G': success = listTables(&cmd[1], pattern, show_verbose, show_system); break; case 'r': @@ -1253,7 +1278,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) success = listExtensions(pattern); break; case 'X': /* Extended Statistics */ - success = listExtendedStats(pattern); + success = listExtendedStats(pattern, show_verbose); break; case 'y': /* Event Triggers */ success = listEventTriggers(pattern, show_verbose); @@ -1946,7 +1971,7 @@ exec_command_gexec(PsqlScanState scan_state, bool active_branch) { if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF) { - pg_log_error("\\gexec not allowed in pipeline mode"); + pg_log_error("\\%s not allowed in pipeline mode", "gexec"); clean_extended_state(); return PSQL_CMD_ERROR; } @@ -1972,7 +1997,7 @@ exec_command_gset(PsqlScanState scan_state, bool active_branch) if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF) { - pg_log_error("\\gset not allowed in pipeline mode"); + pg_log_error("\\%s not allowed in pipeline mode", "gset"); clean_extended_state(); return PSQL_CMD_ERROR; } @@ -2685,7 +2710,8 @@ exec_command_pset(PsqlScanState scan_state, bool active_branch) int i; static const char *const my_list[] = { - "border", "columns", "csv_fieldsep", "expanded", "fieldsep", + "border", "columns", "csv_fieldsep", + "display_false", "display_true", "expanded", "fieldsep", "fieldsep_zero", "footer", "format", "linestyle", "null", "numericlocale", "pager", "pager_min_lines", "recordsep", "recordsep_zero", @@ -2751,6 +2777,35 @@ exec_command_reset(PsqlScanState scan_state, bool active_branch, return PSQL_CMD_SKIP_LINE; } +/* + * \restrict -- enter "restricted mode" with the provided key + */ +static backslashResult +exec_command_restrict(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + if (active_branch) + { + char *opt; + + Assert(!restricted); + + opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); + if (opt == NULL || opt[0] == '\0') + { + pg_log_error("\\%s: missing required argument", cmd); + return PSQL_CMD_ERROR; + } + + restrict_key = pstrdup(opt); + restricted = true; + } + else + ignore_slash_options(scan_state); + + return PSQL_CMD_SKIP_LINE; +} + /* * \s -- save history in a file or show it on the screen */ @@ -3132,6 +3187,46 @@ exec_command_timing(PsqlScanState scan_state, bool active_branch) return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; } +/* + * \unrestrict -- exit "restricted mode" if provided key matches + */ +static backslashResult +exec_command_unrestrict(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + if (active_branch) + { + char *opt; + + opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); + if (opt == NULL || opt[0] == '\0') + { + pg_log_error("\\%s: missing required argument", cmd); + return PSQL_CMD_ERROR; + } + + if (!restricted) + { + pg_log_error("\\%s: not currently in restricted mode", cmd); + return PSQL_CMD_ERROR; + } + else if (strcmp(opt, restrict_key) == 0) + { + pfree(restrict_key); + restricted = false; + } + else + { + pg_log_error("\\%s: wrong key", cmd); + return PSQL_CMD_ERROR; + } + } + else + ignore_slash_options(scan_state); + + return PSQL_CMD_SKIP_LINE; +} + /* * \unset -- unset variable */ @@ -3284,7 +3379,7 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch, if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF) { - pg_log_error("\\watch not allowed in pipeline mode"); + pg_log_error("\\%s not allowed in pipeline mode", "watch"); clean_extended_state(); success = false; } @@ -4074,8 +4169,8 @@ do_connect(enum trivalue reuse_previous_specification, /* Loop till we have a connection or fail, which we might've already */ while (success) { - const char **keywords = pg_malloc((nconnopts + 1) * sizeof(*keywords)); - const char **values = pg_malloc((nconnopts + 1) * sizeof(*values)); + const char **keywords = pg_malloc_array(const char *, nconnopts + 1); + const char **values = pg_malloc_array(const char *, nconnopts + 1); int paramnum = 0; PQconninfoOption *ci; @@ -4477,6 +4572,8 @@ SyncVariables(void) { char vbuf[32]; const char *server_version; + char *service_name; + char *service_file; /* get stuff from connection */ pset.encoding = PQclientEncoding(pset.db); @@ -4486,12 +4583,21 @@ SyncVariables(void) setFmtEncoding(pset.encoding); SetVariable(pset.vars, "DBNAME", PQdb(pset.db)); - SetVariable(pset.vars, "SERVICE", PQservice(pset.db)); SetVariable(pset.vars, "USER", PQuser(pset.db)); SetVariable(pset.vars, "HOST", PQhost(pset.db)); SetVariable(pset.vars, "PORT", PQport(pset.db)); SetVariable(pset.vars, "ENCODING", pg_encoding_to_char(pset.encoding)); + service_name = get_conninfo_value("service"); + SetVariable(pset.vars, "SERVICE", service_name); + if (service_name) + pg_free(service_name); + + service_file = get_conninfo_value("servicefile"); + SetVariable(pset.vars, "SERVICEFILE", service_file); + if (service_file) + pg_free(service_file); + /* this bit should match connection_warnings(): */ /* Try to get full text form of version, might include "devel" etc */ server_version = PQparameterStatus(pset.db, "server_version"); @@ -4521,6 +4627,7 @@ UnsyncVariables(void) { SetVariable(pset.vars, "DBNAME", NULL); SetVariable(pset.vars, "SERVICE", NULL); + SetVariable(pset.vars, "SERVICEFILE", NULL); SetVariable(pset.vars, "USER", NULL); SetVariable(pset.vars, "HOST", NULL); SetVariable(pset.vars, "PORT", NULL); @@ -5195,6 +5302,26 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) } } + /* 'false' display */ + else if (strcmp(param, "display_false") == 0) + { + if (value) + { + free(popt->falsePrint); + popt->falsePrint = pg_strdup(value); + } + } + + /* 'true' display */ + else if (strcmp(param, "display_true") == 0) + { + if (value) + { + free(popt->truePrint); + popt->truePrint = pg_strdup(value); + } + } + /* field separator for unaligned text */ else if (strcmp(param, "fieldsep") == 0) { @@ -5369,6 +5496,20 @@ printPsetInfo(const char *param, printQueryOpt *popt) popt->topt.csvFieldSep); } + /* show boolean 'false' display */ + else if (strcmp(param, "display_false") == 0) + { + printf(_("Boolean false display is \"%s\".\n"), + popt->falsePrint ? popt->falsePrint : "f"); + } + + /* show boolean 'true' display */ + else if (strcmp(param, "display_true") == 0) + { + printf(_("Boolean true display is \"%s\".\n"), + popt->truePrint ? popt->truePrint : "t"); + } + /* show field separator for unaligned text */ else if (strcmp(param, "fieldsep") == 0) { @@ -5525,7 +5666,7 @@ savePsetInfo(const printQueryOpt *popt) { printQueryOpt *save; - save = (printQueryOpt *) pg_malloc(sizeof(printQueryOpt)); + save = pg_malloc_object(printQueryOpt); /* Flat-copy all the scalar fields, then duplicate sub-structures. */ memcpy(save, popt, sizeof(printQueryOpt)); @@ -5638,6 +5779,10 @@ pset_value_string(const char *param, printQueryOpt *popt) return psprintf("%d", popt->topt.columns); else if (strcmp(param, "csv_fieldsep") == 0) return pset_quoted_string(popt->topt.csvFieldSep); + else if (strcmp(param, "display_false") == 0) + return pset_quoted_string(popt->falsePrint ? popt->falsePrint : "f"); + else if (strcmp(param, "display_true") == 0) + return pset_quoted_string(popt->truePrint ? popt->truePrint : "t"); else if (strcmp(param, "expanded") == 0) return pstrdup(popt->topt.expanded == 2 ? "auto" @@ -6013,14 +6158,14 @@ echo_hidden_command(const char *query) { if (pset.echo_hidden != PSQL_ECHO_HIDDEN_OFF) { - printf(_("/******** QUERY *********/\n" + printf(_("/**** INTERNAL QUERY ****/\n" "%s\n" "/************************/\n\n"), query); fflush(stdout); if (pset.logfile) { fprintf(pset.logfile, - _("/******** QUERY *********/\n" + _("/**** INTERNAL QUERY ****/\n" "%s\n" "/************************/\n\n"), query); fflush(pset.logfile); @@ -6057,6 +6202,7 @@ lookup_object_oid(EditableObjectType obj_type, const char *desc, * query to retrieve the function's OID using a cast to regproc or * regprocedure (as appropriate). */ + printfPQExpBuffer(query, "/* %s */\n", _("Get function's OID")); appendPQExpBufferStr(query, "SELECT "); appendStringLiteralConn(query, desc, pset.db); appendPQExpBuffer(query, "::pg_catalog.%s::pg_catalog.oid", @@ -6070,6 +6216,7 @@ lookup_object_oid(EditableObjectType obj_type, const char *desc, * this code doesn't check if the relation is actually a view. * We'll detect that in get_create_object_cmd(). */ + printfPQExpBuffer(query, "/* %s */\n", _("Get view's OID")); appendPQExpBufferStr(query, "SELECT "); appendStringLiteralConn(query, desc, pset.db); appendPQExpBufferStr(query, "::pg_catalog.regclass::pg_catalog.oid"); @@ -6111,7 +6258,8 @@ get_create_object_cmd(EditableObjectType obj_type, Oid oid, switch (obj_type) { case EditableFunction: - printfPQExpBuffer(query, + printfPQExpBuffer(query, "/* %s */\n", _("Get function's definition")); + appendPQExpBuffer(query, "SELECT pg_catalog.pg_get_functiondef(%u)", oid); break; @@ -6130,9 +6278,10 @@ get_create_object_cmd(EditableObjectType obj_type, Oid oid, * separately. Materialized views (introduced in 9.3) may have * arbitrary storage parameter reloptions. */ + printfPQExpBuffer(query, "/* %s */\n", _("Get view's definition and details")); if (pset.sversion >= 90400) { - printfPQExpBuffer(query, + appendPQExpBuffer(query, "SELECT nspname, relname, relkind, " "pg_catalog.pg_get_viewdef(c.oid, true), " "pg_catalog.array_remove(pg_catalog.array_remove(c.reloptions,'check_option=local'),'check_option=cascaded') AS reloptions, " @@ -6145,7 +6294,7 @@ get_create_object_cmd(EditableObjectType obj_type, Oid oid, } else { - printfPQExpBuffer(query, + appendPQExpBuffer(query, "SELECT nspname, relname, relkind, " "pg_catalog.pg_get_viewdef(c.oid, true), " "c.reloptions AS reloptions, " diff --git a/src/bin/psql/command.h b/src/bin/psql/command.h index ae2d66ea81774..7d75d6a116543 100644 --- a/src/bin/psql/command.h +++ b/src/bin/psql/command.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/command.h */ diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 3e4e444f3fd93..476e7fe673768 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/common.c */ @@ -664,14 +664,14 @@ PSQLexec(const char *query) if (pset.echo_hidden != PSQL_ECHO_HIDDEN_OFF) { - printf(_("/******** QUERY *********/\n" + printf(_("/**** INTERNAL QUERY ****/\n" "%s\n" "/************************/\n\n"), query); fflush(stdout); if (pset.logfile) { fprintf(pset.logfile, - _("/******** QUERY *********/\n" + _("/**** INTERNAL QUERY ****/\n" "%s\n" "/************************/\n\n"), query); fflush(pset.logfile); @@ -1021,7 +1021,7 @@ PrintQueryStatus(PGresult *result, FILE *printQueryFout) if (pset.logfile) fprintf(pset.logfile, "%s\n", cmdstatus); - snprintf(buf, sizeof(buf), "%u", (unsigned int) PQoidValue(result)); + snprintf(buf, sizeof(buf), "%u", PQoidValue(result)); SetVariable(pset.vars, "LASTOID", buf); } @@ -1867,6 +1867,33 @@ ExecQueryAndProcessResults(const char *query, { FILE *copy_stream = NULL; + if (PQpipelineStatus(pset.db) != PQ_PIPELINE_OFF) + { + /* + * Running COPY within a pipeline can break the protocol + * synchronisation in multiple ways, and psql shows its limits + * when it comes to tracking this information. + * + * While in COPY mode, the backend process ignores additional + * Sync messages and will not send the matching ReadyForQuery + * expected by the frontend. + * + * Additionally, libpq automatically sends a Sync with the + * Copy message, creating an unexpected synchronisation point. + * A failure during COPY would leave the pipeline in an + * aborted state while the backend would be in a clean state, + * ready to process commands. + * + * Improving those issues would require modifications in how + * libpq handles pipelines and COPY. Hence, for the time + * being, we forbid the use of COPY within a pipeline, + * aborting the connection to avoid an inconsistent state on + * psql side if trying to use a COPY command. + */ + pg_log_info("COPY in a pipeline is not supported, aborting connection"); + exit(EXIT_BADCONN); + } + /* * For COPY OUT, direct the output to the default place (probably * a pager pipe) for \watch, or to pset.copyStream for \copy, @@ -2504,6 +2531,41 @@ session_username(void) return PQuser(pset.db); } +/* + * Return the value of option for keyword in the current connection. + * + * The caller is responsible for freeing the result value allocated. + */ +char * +get_conninfo_value(const char *keyword) +{ + PQconninfoOption *opts; + PQconninfoOption *serviceopt = NULL; + char *res = NULL; + + if (pset.db == NULL) + return NULL; + + opts = PQconninfo(pset.db); + if (opts == NULL) + return NULL; + + for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt) + { + if (strcmp(opt->keyword, keyword) == 0) + { + serviceopt = opt; + break; + } + } + + /* Take a copy of the value, as it is freed by PQconninfoFree(). */ + if (serviceopt && serviceopt->val != NULL) + res = pg_strdup(serviceopt->val); + PQconninfoFree(opts); + + return res; +} /* expand_tilde * @@ -2601,7 +2663,7 @@ clean_extended_state(void) switch (pset.send_mode) { - case PSQL_SEND_EXTENDED_CLOSE: /* \close */ + case PSQL_SEND_EXTENDED_CLOSE: /* \close_prepared */ free(pset.stmtName); break; case PSQL_SEND_EXTENDED_PARSE: /* \parse */ diff --git a/src/bin/psql/common.h b/src/bin/psql/common.h index 7f1a23de1e82d..b8a5a2ab6a178 100644 --- a/src/bin/psql/common.h +++ b/src/bin/psql/common.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/common.h */ @@ -39,6 +39,7 @@ extern bool SendQuery(const char *query); extern bool is_superuser(void); extern bool standard_strings(void); extern const char *session_username(void); +extern char *get_conninfo_value(const char *keyword); extern void expand_tilde(char **filename); extern void clean_extended_state(void); diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c index 92c955b637a43..6a8a9792e7d65 100644 --- a/src/bin/psql/copy.c +++ b/src/bin/psql/copy.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/copy.c */ @@ -33,7 +33,7 @@ * \copy ( query stmt ) to filename [options] * * where 'filename' can be one of the following: - * '' | PROGRAM '' | stdin | stdout | pstdout | pstdout + * '' | PROGRAM '' | stdin | stdout | pstdin | pstdout * and 'query' can be one of the following: * SELECT | UPDATE | INSERT | DELETE * @@ -99,7 +99,7 @@ parse_slash_copy(const char *args) return NULL; } - result = pg_malloc0(sizeof(struct copy_options)); + result = pg_malloc0_object(struct copy_options); result->before_tofrom = pg_strdup(""); /* initialize for appending */ diff --git a/src/bin/psql/copy.h b/src/bin/psql/copy.h index 2a3e593d4eaa8..85d4236c091bb 100644 --- a/src/bin/psql/copy.h +++ b/src/bin/psql/copy.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/copy.h */ diff --git a/src/bin/psql/create_help.pl b/src/bin/psql/create_help.pl index 5964b49798345..12931c17b691e 100644 --- a/src/bin/psql/create_help.pl +++ b/src/bin/psql/create_help.pl @@ -3,7 +3,7 @@ ################################################################# # create_help.pl -- converts SGML docs to internal psql help # -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group # # src/bin/psql/create_help.pl ################################################################# diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c index 3e5cd0a96d4fa..111e8823bdbe7 100644 --- a/src/bin/psql/crosstabview.c +++ b/src/bin/psql/crosstabview.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/crosstabview.c */ @@ -245,11 +245,9 @@ PrintResultInCrosstab(const PGresult *res) num_columns = piv_columns.count; num_rows = piv_rows.count; - array_columns = (pivot_field *) - pg_malloc(sizeof(pivot_field) * num_columns); + array_columns = pg_malloc_array(pivot_field, num_columns); - array_rows = (pivot_field *) - pg_malloc(sizeof(pivot_field) * num_rows); + array_rows = pg_malloc_array(pivot_field, num_rows); avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0); avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0); @@ -312,7 +310,7 @@ printCrosstab(const PGresult *result, * map associating each piv_columns[].rank to its index in piv_columns. * This avoids an O(N^2) loop later. */ - horiz_map = (int *) pg_malloc(sizeof(int) * num_columns); + horiz_map = pg_malloc_array(int, num_columns); for (i = 0; i < num_columns; i++) horiz_map[piv_columns[i].rank] = i; @@ -437,7 +435,7 @@ printCrosstab(const PGresult *result, static void avlInit(avl_tree *tree) { - tree->end = (avl_node *) pg_malloc0(sizeof(avl_node)); + tree->end = pg_malloc0_object(avl_node); tree->end->children[0] = tree->end->children[1] = tree->end; tree->count = 0; tree->root = tree->end; @@ -532,8 +530,7 @@ avlInsertNode(avl_tree *tree, avl_node **node, pivot_field field) if (current == tree->end) { - avl_node *new_node = (avl_node *) - pg_malloc(sizeof(avl_node)); + avl_node *new_node = pg_malloc_object(avl_node); new_node->height = 1; new_node->field = field; @@ -591,7 +588,7 @@ rankSort(int num_columns, pivot_field *piv_columns) * every header entry] */ int i; - hmap = (int *) pg_malloc(sizeof(int) * num_columns * 2); + hmap = pg_malloc_array(int, num_columns * 2); for (i = 0; i < num_columns; i++) { char *val = piv_columns[i].sort_value; diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h index a3b96d28a9f23..8c59ffbe874a2 100644 --- a/src/bin/psql/crosstabview.h +++ b/src/bin/psql/crosstabview.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/crosstabview.h */ diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 1d08268393e3c..dd1179ef927be 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -7,7 +7,7 @@ * information for an old server, but not to fail outright. (But failing * against a pre-9.2 server is allowed.) * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/describe.c */ @@ -24,6 +24,7 @@ #include "catalog/pg_constraint_d.h" #include "catalog/pg_default_acl_d.h" #include "catalog/pg_proc_d.h" +#include "catalog/pg_propgraph_element_d.h" #include "catalog/pg_publication_d.h" #include "catalog/pg_statistic_ext_d.h" #include "catalog/pg_subscription_d.h" @@ -83,7 +84,8 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching aggregates")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " p.proname AS \"%s\",\n" " pg_catalog.format_type(p.prorettype, NULL) AS \"%s\",\n" @@ -164,7 +166,8 @@ describeAccessMethods(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching access methods")); + appendPQExpBuffer(&buf, "SELECT amname AS \"%s\",\n" " CASE amtype" " WHEN " CppAsString2(AMTYPE_INDEX) " THEN '%s'" @@ -227,7 +230,8 @@ describeTablespaces(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching tablespaces")); + appendPQExpBuffer(&buf, "SELECT spcname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(spcowner) AS \"%s\",\n" " pg_catalog.pg_tablespace_location(oid) AS \"%s\"", @@ -296,6 +300,7 @@ describeFunctions(const char *functypes, const char *func_pattern, char **arg_patterns, int num_arg_patterns, bool verbose, bool showSystem) { + const char *df_options = "anptwSx+"; bool showAggregate = strchr(functypes, 'a') != NULL; bool showNormal = strchr(functypes, 'n') != NULL; bool showProcedure = strchr(functypes, 'p') != NULL; @@ -310,9 +315,9 @@ describeFunctions(const char *functypes, const char *func_pattern, /* No "Parallel" column before 9.6 */ static const bool translate_columns_pre_96[] = {false, false, false, false, true, true, false, true, true, false, false, false, false}; - if (strlen(functypes) != strspn(functypes, "anptwSx+")) + if (strlen(functypes) != strspn(functypes, df_options)) { - pg_log_error("\\df only takes [anptwSx+] as options"); + pg_log_error("\\df only takes [%s] as options", df_options); return true; } @@ -336,7 +341,8 @@ describeFunctions(const char *functypes, const char *func_pattern, initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching functions")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " p.proname as \"%s\",\n", gettext_noop("Schema"), @@ -643,7 +649,8 @@ describeTypes(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching types")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " pg_catalog.format_type(t.oid, NULL) AS \"%s\",\n", gettext_noop("Schema"), @@ -817,7 +824,8 @@ describeOperators(const char *oper_pattern, * to pre-v14 servers. */ - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching operators")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " o.oprname AS \"%s\",\n" " CASE WHEN o.oprkind='l' THEN NULL ELSE pg_catalog.format_type(o.oprleft, NULL) END AS \"%s\",\n" @@ -950,7 +958,8 @@ listAllDbs(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching databases")); + appendPQExpBuffer(&buf, "SELECT\n" " d.datname as \"%s\",\n" " pg_catalog.pg_get_userbyid(d.datdba) as \"%s\",\n" @@ -1058,7 +1067,9 @@ permissionsList(const char *pattern, bool showSystem) /* * we ignore indexes and toast tables since they have no meaningful rights */ - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get access privileges of matching relations")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " c.relname as \"%s\",\n" " CASE c.relkind" @@ -1067,6 +1078,7 @@ permissionsList(const char *pattern, bool showSystem) " WHEN " CppAsString2(RELKIND_MATVIEW) " THEN '%s'" " WHEN " CppAsString2(RELKIND_SEQUENCE) " THEN '%s'" " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'" + " WHEN " CppAsString2(RELKIND_PROPGRAPH) " THEN '%s'" " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'" " END as \"%s\",\n" " ", @@ -1077,6 +1089,7 @@ permissionsList(const char *pattern, bool showSystem) gettext_noop("materialized view"), gettext_noop("sequence"), gettext_noop("foreign table"), + gettext_noop("property graph"), gettext_noop("partitioned table"), gettext_noop("Type")); @@ -1168,6 +1181,7 @@ permissionsList(const char *pattern, bool showSystem) CppAsString2(RELKIND_MATVIEW) "," CppAsString2(RELKIND_SEQUENCE) "," CppAsString2(RELKIND_FOREIGN_TABLE) "," + CppAsString2(RELKIND_PROPGRAPH) "," CppAsString2(RELKIND_PARTITIONED_TABLE) ")\n"); if (!showSystem && !pattern) @@ -1219,7 +1233,8 @@ listDefaultACLs(const char *pattern) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching default ACLs")); + appendPQExpBuffer(&buf, "SELECT pg_catalog.pg_get_userbyid(d.defaclrole) AS \"%s\",\n" " n.nspname AS \"%s\",\n" " CASE d.defaclobjtype " @@ -1300,6 +1315,7 @@ objectDescription(const char *pattern, bool showSystem) initPQExpBuffer(&buf); + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching object comments")); appendPQExpBuffer(&buf, "SELECT DISTINCT tt.nspname AS \"%s\", tt.name AS \"%s\", tt.object AS \"%s\", d.description AS \"%s\"\n" "FROM (\n", @@ -1492,12 +1508,14 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, - "SELECT c.oid,\n" - " n.nspname,\n" - " c.relname\n" - "FROM pg_catalog.pg_class c\n" - " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n"); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get matching relations to describe")); + appendPQExpBufferStr(&buf, + "SELECT c.oid,\n" + " n.nspname,\n" + " c.relname\n" + "FROM pg_catalog.pg_class c\n" + " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n"); if (!showSystem && !pattern) appendPQExpBufferStr(&buf, "WHERE n.nspname <> 'pg_catalog'\n" @@ -1628,9 +1646,11 @@ describeOneTableDetails(const char *schemaname, initPQExpBuffer(&tmpbuf); /* Get general table info */ + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get general information about one relation")); if (pset.sversion >= 120000) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, " "false AS relhasoids, c.relispartition, %s, c.reltablespace, " @@ -1648,7 +1668,7 @@ describeOneTableDetails(const char *schemaname, } else if (pset.sversion >= 100000) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, " "c.relhasoids, c.relispartition, %s, c.reltablespace, " @@ -1665,7 +1685,7 @@ describeOneTableDetails(const char *schemaname, } else if (pset.sversion >= 90500) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " "c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, " "c.relhasoids, false as relispartition, %s, c.reltablespace, " @@ -1682,7 +1702,7 @@ describeOneTableDetails(const char *schemaname, } else if (pset.sversion >= 90400) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " "c.relhastriggers, false, false, c.relhasoids, " "false as relispartition, %s, c.reltablespace, " @@ -1699,7 +1719,7 @@ describeOneTableDetails(const char *schemaname, } else { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " "c.relhastriggers, false, false, c.relhasoids, " "false as relispartition, %s, c.reltablespace, " @@ -1758,11 +1778,12 @@ describeOneTableDetails(const char *schemaname, { PGresult *result = NULL; printQueryOpt myopt = pset.popt; - char *footers[2] = {NULL, NULL}; + char *footers[3] = {NULL, NULL, NULL}; + printfPQExpBuffer(&buf, "/* %s */\n", _("Get sequence information")); if (pset.sversion >= 100000) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT pg_catalog.format_type(seqtypid, NULL) AS \"%s\",\n" " seqstart AS \"%s\",\n" " seqmin AS \"%s\",\n" @@ -1786,7 +1807,7 @@ describeOneTableDetails(const char *schemaname, } else { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT 'bigint' AS \"%s\",\n" " start_value AS \"%s\",\n" " min_value AS \"%s\",\n" @@ -1813,7 +1834,9 @@ describeOneTableDetails(const char *schemaname, goto error_return; /* Get the column that owns this sequence */ - printfPQExpBuffer(&buf, "SELECT pg_catalog.quote_ident(nspname) || '.' ||" + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get the column that owns this sequence")); + appendPQExpBuffer(&buf, "SELECT pg_catalog.quote_ident(nspname) || '.' ||" "\n pg_catalog.quote_ident(relname) || '.' ||" "\n pg_catalog.quote_ident(attname)," "\n d.deptype" @@ -1854,6 +1877,41 @@ describeOneTableDetails(const char *schemaname, } PQclear(result); + /* Print any publications */ + if (pset.sversion >= 190000) + { + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get publications containing this sequence")); + appendPQExpBuffer(&buf, "SELECT pubname FROM pg_catalog.pg_publication p" + "\nWHERE p.puballsequences" + "\n AND pg_catalog.pg_relation_is_publishable('%s')" + "\nORDER BY 1", + oid); + + result = PSQLexec(buf.data); + if (result) + { + int nrows = PQntuples(result); + + if (nrows > 0) + { + printfPQExpBuffer(&tmpbuf, _("Publications:")); + for (i = 0; i < nrows; i++) + appendPQExpBuffer(&tmpbuf, "\n \"%s\"", PQgetvalue(result, i, 0)); + + /* Store in the first available footer slot */ + if (footers[0] == NULL) + footers[0] = pg_strdup(tmpbuf.data); + else + footers[1] = pg_strdup(tmpbuf.data); + + resetPQExpBuffer(&tmpbuf); + } + + PQclear(result); + } + } + if (tableinfo.relpersistence == RELPERSISTENCE_UNLOGGED) printfPQExpBuffer(&title, _("Unlogged sequence \"%s.%s\""), schemaname, relationname); @@ -1869,6 +1927,81 @@ describeOneTableDetails(const char *schemaname, printQuery(res, &myopt, pset.queryFout, false, pset.logfile); free(footers[0]); + free(footers[1]); + + retval = true; + goto error_return; /* not an error, just return early */ + } + + /* + * If it's a property graph, deal with it here separately. + */ + if (tableinfo.relkind == RELKIND_PROPGRAPH) + { + printQueryOpt myopt = pset.popt; + char *footers[3] = {NULL, NULL, NULL}; + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get property graph information")); + appendPQExpBuffer(&buf, + "SELECT e.pgealias AS \"%s\"," + "\n pg_catalog.quote_ident(n.nspname) || '.' ||" + "\n pg_catalog.quote_ident(c.relname) AS \"%s\"," + "\n case e.pgekind when " CppAsString2(PGEKIND_VERTEX) " then 'vertex'" + "\n when " CppAsString2(PGEKIND_EDGE) " then 'edge' end AS \"%s\"," + "\n s.pgealias as \"%s\"," + "\n d.pgealias as \"%s\"" + "\n FROM pg_propgraph_element e" + "\n INNER JOIN pg_class c ON c.oid = e.pgerelid" + "\n INNER JOIN pg_namespace n ON c.relnamespace = n.oid" + "\n LEFT JOIN pg_propgraph_element s ON e.pgesrcvertexid = s.oid" + "\n LEFT JOIN pg_propgraph_element d ON e.pgedestvertexid = d.oid" + "\n WHERE e.pgepgid = '%s'" + "\n ORDER BY e.pgealias", + gettext_noop("Element Alias"), + gettext_noop("Element Table"), + gettext_noop("Element Kind"), + gettext_noop("Source Vertex Alias"), + gettext_noop("Destination Vertex Alias"), + oid); + + res = PSQLexec(buf.data); + if (!res) + goto error_return; + + printfPQExpBuffer(&title, _("Property Graph \"%s.%s\""), + schemaname, relationname); + + /* Add property graph definition in verbose mode */ + if (verbose) + { + PGresult *result; + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get property graph definition")); + appendPQExpBuffer(&buf, + "SELECT pg_catalog.pg_get_propgraphdef('%s'::pg_catalog.oid);", + oid); + result = PSQLexec(buf.data); + + if (result) + { + if (PQntuples(result) > 0) + { + footers[0] = pg_strdup(_("Property graph definition:")); + footers[1] = pg_strdup(PQgetvalue(result, 0, 0)); + } + PQclear(result); + } + } + + myopt.footers = footers; + myopt.topt.default_footer = false; + myopt.title = title.data; + myopt.translate_header = true; + + printQuery(res, &myopt, pset.queryFout, false, pset.logfile); + + free(footers[0]); + free(footers[1]); retval = true; goto error_return; /* not an error, just return early */ @@ -1892,7 +2025,9 @@ describeOneTableDetails(const char *schemaname, * duplicative test logic below. */ cols = 0; - printfPQExpBuffer(&buf, "SELECT a.attname"); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get per-column information for one relation")); + appendPQExpBufferStr(&buf, "SELECT a.attname"); attname_col = cols++; appendPQExpBufferStr(&buf, ",\n pg_catalog.format_type(a.atttypid, a.atttypmod)"); atttype_col = cols++; @@ -2194,9 +2329,11 @@ describeOneTableDetails(const char *schemaname, /* Footer information for a partition child table */ PGresult *result; - printfPQExpBuffer(&buf, - "SELECT inhparent::pg_catalog.regclass,\n" - " pg_catalog.pg_get_expr(c.relpartbound, c.oid),\n "); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get partitioning information for this partition")); + appendPQExpBufferStr(&buf, + "SELECT inhparent::pg_catalog.regclass,\n" + " pg_catalog.pg_get_expr(c.relpartbound, c.oid),\n "); appendPQExpBufferStr(&buf, pset.sversion >= 140000 ? "inhdetachpending" : @@ -2249,7 +2386,9 @@ describeOneTableDetails(const char *schemaname, /* Footer information for a partitioned table (partitioning parent) */ PGresult *result; - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get partitioning information for this table")); + appendPQExpBuffer(&buf, "SELECT pg_catalog.pg_get_partkeydef('%s'::pg_catalog.oid);", oid); result = PSQLexec(buf.data); @@ -2271,7 +2410,9 @@ describeOneTableDetails(const char *schemaname, /* For a TOAST table, print name of owning table */ PGresult *result; - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get the table that owns this TOAST table")); + appendPQExpBuffer(&buf, "SELECT n.nspname, c.relname\n" "FROM pg_catalog.pg_class c" " JOIN pg_catalog.pg_namespace n" @@ -2299,25 +2440,26 @@ describeOneTableDetails(const char *schemaname, /* Footer information about an index */ PGresult *result; - printfPQExpBuffer(&buf, - "SELECT i.indisunique, i.indisprimary, i.indisclustered, " - "i.indisvalid,\n" - " (NOT i.indimmediate) AND " - "EXISTS (SELECT 1 FROM pg_catalog.pg_constraint " - "WHERE conrelid = i.indrelid AND " - "conindid = i.indexrelid AND " - "contype IN (" CppAsString2(CONSTRAINT_PRIMARY) "," - CppAsString2(CONSTRAINT_UNIQUE) "," - CppAsString2(CONSTRAINT_EXCLUSION) ") AND " - "condeferrable) AS condeferrable,\n" - " (NOT i.indimmediate) AND " - "EXISTS (SELECT 1 FROM pg_catalog.pg_constraint " - "WHERE conrelid = i.indrelid AND " - "conindid = i.indexrelid AND " - "contype IN (" CppAsString2(CONSTRAINT_PRIMARY) "," - CppAsString2(CONSTRAINT_UNIQUE) "," - CppAsString2(CONSTRAINT_EXCLUSION) ") AND " - "condeferred) AS condeferred,\n"); + printfPQExpBuffer(&buf, "/* %s */\n", _("Get index details")); + appendPQExpBufferStr(&buf, + "SELECT i.indisunique, i.indisprimary, i.indisclustered, " + "i.indisvalid,\n" + " (NOT i.indimmediate) AND " + "EXISTS (SELECT 1 FROM pg_catalog.pg_constraint " + "WHERE conrelid = i.indrelid AND " + "conindid = i.indexrelid AND " + "contype IN (" CppAsString2(CONSTRAINT_PRIMARY) "," + CppAsString2(CONSTRAINT_UNIQUE) "," + CppAsString2(CONSTRAINT_EXCLUSION) ") AND " + "condeferrable) AS condeferrable,\n" + " (NOT i.indimmediate) AND " + "EXISTS (SELECT 1 FROM pg_catalog.pg_constraint " + "WHERE conrelid = i.indrelid AND " + "conindid = i.indexrelid AND " + "contype IN (" CppAsString2(CONSTRAINT_PRIMARY) "," + CppAsString2(CONSTRAINT_UNIQUE) "," + CppAsString2(CONSTRAINT_EXCLUSION) ") AND " + "condeferred) AS condeferred,\n"); if (pset.sversion >= 90400) appendPQExpBufferStr(&buf, "i.indisreplident,\n"); @@ -2420,12 +2562,13 @@ describeOneTableDetails(const char *schemaname, /* print indexes */ if (tableinfo.hasindex) { - printfPQExpBuffer(&buf, - "SELECT c2.relname, i.indisprimary, i.indisunique, " - "i.indisclustered, i.indisvalid, " - "pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),\n " - "pg_catalog.pg_get_constraintdef(con.oid, true), " - "contype, condeferrable, condeferred"); + printfPQExpBuffer(&buf, "/* %s */\n", _("Get indexes for this table")); + appendPQExpBufferStr(&buf, + "SELECT c2.relname, i.indisprimary, i.indisunique, " + "i.indisclustered, i.indisvalid, " + "pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),\n " + "pg_catalog.pg_get_constraintdef(con.oid, true), " + "contype, condeferrable, condeferred"); if (pset.sversion >= 90400) appendPQExpBufferStr(&buf, ", i.indisreplident"); else @@ -2524,7 +2667,9 @@ describeOneTableDetails(const char *schemaname, /* print table (and column) check constraints */ if (tableinfo.checks) { - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get check constraints for this table")); + appendPQExpBuffer(&buf, "SELECT r.conname, " "pg_catalog.pg_get_constraintdef(r.oid, true)\n" "FROM pg_catalog.pg_constraint r\n" @@ -2555,6 +2700,8 @@ describeOneTableDetails(const char *schemaname, } /* Print foreign-key constraints */ + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get foreign key constraints for this table")); if (pset.sversion >= 120000 && (tableinfo.ispartition || tableinfo.relkind == RELKIND_PARTITIONED_TABLE)) { @@ -2562,7 +2709,7 @@ describeOneTableDetails(const char *schemaname, * Put the constraints defined in this table first, followed by * the constraints defined in ancestor partitioned tables. */ - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT conrelid = '%s'::pg_catalog.regclass AS sametable,\n" " conname,\n" " pg_catalog.pg_get_constraintdef(oid, true) AS condef,\n" @@ -2575,7 +2722,7 @@ describeOneTableDetails(const char *schemaname, } else { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT true as sametable, conname,\n" " pg_catalog.pg_get_constraintdef(r.oid, true) as condef,\n" " conrelid::pg_catalog.regclass AS ontable\n" @@ -2625,9 +2772,11 @@ describeOneTableDetails(const char *schemaname, PQclear(result); /* print incoming foreign-key references */ + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get foreign keys referencing this table")); if (pset.sversion >= 120000) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n" " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n" " FROM pg_catalog.pg_constraint c\n" @@ -2639,7 +2788,7 @@ describeOneTableDetails(const char *schemaname, } else { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT conname, conrelid::pg_catalog.regclass AS ontable,\n" " pg_catalog.pg_get_constraintdef(oid, true) AS condef\n" " FROM pg_catalog.pg_constraint\n" @@ -2676,7 +2825,9 @@ describeOneTableDetails(const char *schemaname, /* print any row-level policies */ if (pset.sversion >= 90500) { - printfPQExpBuffer(&buf, "SELECT pol.polname,"); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get row-level policies for this table")); + appendPQExpBufferStr(&buf, "SELECT pol.polname,"); if (pset.sversion >= 100000) appendPQExpBufferStr(&buf, " pol.polpermissive,\n"); @@ -2758,7 +2909,9 @@ describeOneTableDetails(const char *schemaname, /* print any extended statistics */ if (pset.sversion >= 140000) { - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get extended statistics for this table")); + appendPQExpBuffer(&buf, "SELECT oid, " "stxrelid::pg_catalog.regclass, " "stxnamespace::pg_catalog.regnamespace::pg_catalog.text AS nsp, " @@ -2856,18 +3009,20 @@ describeOneTableDetails(const char *schemaname, } else if (pset.sversion >= 100000) { - printfPQExpBuffer(&buf, - "SELECT oid, " - "stxrelid::pg_catalog.regclass, " - "stxnamespace::pg_catalog.regnamespace AS nsp, " - "stxname,\n" - " (SELECT pg_catalog.string_agg(pg_catalog.quote_ident(attname),', ')\n" - " FROM pg_catalog.unnest(stxkeys) s(attnum)\n" - " JOIN pg_catalog.pg_attribute a ON (stxrelid = a.attrelid AND\n" - " a.attnum = s.attnum AND NOT attisdropped)) AS columns,\n" - " " CppAsString2(STATS_EXT_NDISTINCT) " = any(stxkind) AS ndist_enabled,\n" - " " CppAsString2(STATS_EXT_DEPENDENCIES) " = any(stxkind) AS deps_enabled,\n" - " " CppAsString2(STATS_EXT_MCV) " = any(stxkind) AS mcv_enabled,\n"); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get extended statistics for this table")); + appendPQExpBufferStr(&buf, + "SELECT oid, " + "stxrelid::pg_catalog.regclass, " + "stxnamespace::pg_catalog.regnamespace AS nsp, " + "stxname,\n" + " (SELECT pg_catalog.string_agg(pg_catalog.quote_ident(attname),', ')\n" + " FROM pg_catalog.unnest(stxkeys) s(attnum)\n" + " JOIN pg_catalog.pg_attribute a ON (stxrelid = a.attrelid AND\n" + " a.attnum = s.attnum AND NOT attisdropped)) AS columns,\n" + " " CppAsString2(STATS_EXT_NDISTINCT) " = any(stxkind) AS ndist_enabled,\n" + " " CppAsString2(STATS_EXT_DEPENDENCIES) " = any(stxkind) AS deps_enabled,\n" + " " CppAsString2(STATS_EXT_MCV) " = any(stxkind) AS mcv_enabled,\n"); if (pset.sversion >= 130000) appendPQExpBufferStr(&buf, " stxstattarget\n"); @@ -2935,7 +3090,9 @@ describeOneTableDetails(const char *schemaname, /* print rules */ if (tableinfo.hasrules && tableinfo.relkind != RELKIND_MATVIEW) { - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get rules for this relation")); + appendPQExpBuffer(&buf, "SELECT r.rulename, trim(trailing ';' from pg_catalog.pg_get_ruledef(r.oid, true)), " "ev_enabled\n" "FROM pg_catalog.pg_rewrite r\n" @@ -3018,9 +3175,11 @@ describeOneTableDetails(const char *schemaname, /* print any publications */ if (pset.sversion >= 100000) { + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get publications that publish this table")); if (pset.sversion >= 150000) { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT pubname\n" " , NULL\n" " , NULL\n" @@ -3040,19 +3199,47 @@ describeOneTableDetails(const char *schemaname, "FROM pg_catalog.pg_publication p\n" " JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n" " JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid\n" - "WHERE pr.prrelid = '%s'\n" - "UNION\n" - "SELECT pubname\n" - " , NULL\n" - " , NULL\n" - "FROM pg_catalog.pg_publication p\n" - "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n" - "ORDER BY 1;", - oid, oid, oid, oid); + "WHERE pr.prrelid = '%s'\n", + oid, oid, oid); + + if (pset.sversion >= 190000) + { + /* + * Skip entries where this relation appears in the + * publication's EXCEPT list. + */ + appendPQExpBuffer(&buf, + " AND NOT pr.prexcept\n" + "UNION\n" + "SELECT pubname\n" + " , NULL\n" + " , NULL\n" + "FROM pg_catalog.pg_publication p\n" + "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n" + " AND NOT EXISTS (\n" + " SELECT 1\n" + " FROM pg_catalog.pg_publication_rel pr\n" + " WHERE pr.prpubid = p.oid AND\n" + " (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s')))\n" + "ORDER BY 1;", + oid, oid, oid); + } + else + { + appendPQExpBuffer(&buf, + "UNION\n" + "SELECT pubname\n" + " , NULL\n" + " , NULL\n" + "FROM pg_catalog.pg_publication p\n" + "WHERE p.puballtables AND pg_catalog.pg_relation_is_publishable('%s')\n" + "ORDER BY 1;", + oid); + } } else { - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT pubname\n" " , NULL\n" " , NULL\n" @@ -3099,12 +3286,46 @@ describeOneTableDetails(const char *schemaname, PQclear(result); } + /* Print publications where the table is in the EXCEPT clause */ + if (pset.sversion >= 190000) + { + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get publications that exclude this table")); + appendPQExpBuffer(&buf, + "SELECT pubname\n" + "FROM pg_catalog.pg_publication p\n" + "JOIN pg_catalog.pg_publication_rel pr ON p.oid = pr.prpubid\n" + "WHERE (pr.prrelid = '%s' OR pr.prrelid = pg_catalog.pg_partition_root('%s'))\n" + "AND pr.prexcept\n" + "ORDER BY 1;", oid, oid); + + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + printTableAddFooter(&cont, _("Except publications:")); + + /* Might be an empty set - that's ok */ + for (i = 0; i < tuples; i++) + { + printfPQExpBuffer(&buf, " \"%s\"", PQgetvalue(result, i, 0)); + + printTableAddFooter(&cont, buf.data); + } + PQclear(result); + } + /* * If verbose, print NOT NULL constraints. */ if (verbose) { - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get not-null constraints for this table")); + appendPQExpBuffer(&buf, "SELECT c.conname, a.attname, c.connoinherit,\n" " c.conislocal, c.coninhcount <> 0,\n" " c.convalidated\n" @@ -3153,7 +3374,8 @@ describeOneTableDetails(const char *schemaname, { PGresult *result; - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get view's definition")); + appendPQExpBuffer(&buf, "SELECT pg_catalog.pg_get_viewdef('%s'::pg_catalog.oid, true);", oid); result = PSQLexec(buf.data); @@ -3177,7 +3399,8 @@ describeOneTableDetails(const char *schemaname, /* print rules */ if (tableinfo.hasrules) { - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get rules for this view")); + appendPQExpBuffer(&buf, "SELECT r.rulename, trim(trailing ';' from pg_catalog.pg_get_ruledef(r.oid, true))\n" "FROM pg_catalog.pg_rewrite r\n" "WHERE r.ev_class = '%s' AND r.rulename != '_RETURN' ORDER BY 1;", @@ -3214,10 +3437,12 @@ describeOneTableDetails(const char *schemaname, PGresult *result; int tuples; - printfPQExpBuffer(&buf, - "SELECT t.tgname, " - "pg_catalog.pg_get_triggerdef(t.oid, true), " - "t.tgenabled, t.tgisinternal,\n"); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get triggers for this relation")); + appendPQExpBufferStr(&buf, + "SELECT t.tgname, " + "pg_catalog.pg_get_triggerdef(t.oid, true), " + "t.tgenabled, t.tgisinternal,\n"); /* * Detect whether each trigger is inherited, and if so, get the name @@ -3397,7 +3622,9 @@ describeOneTableDetails(const char *schemaname, char *ftoptions; /* Footer information about foreign table */ - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get foreign server for this table")); + appendPQExpBuffer(&buf, "SELECT s.srvname,\n" " pg_catalog.array_to_string(ARRAY(\n" " SELECT pg_catalog.quote_ident(option_name)" @@ -3432,7 +3659,9 @@ describeOneTableDetails(const char *schemaname, } /* print tables inherited from (exclude partitioned parents) */ - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get inheritance parent tables")); + appendPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass\n" "FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\n" "WHERE c.oid = i.inhparent AND i.inhrelid = '%s'\n" @@ -3447,21 +3676,18 @@ describeOneTableDetails(const char *schemaname, else { const char *s = _("Inherits"); - int sw = pg_wcswidth(s, strlen(s), pset.encoding); tuples = PQntuples(result); - for (i = 0; i < tuples; i++) + if (tuples > 0) { - if (i == 0) - printfPQExpBuffer(&buf, "%s: %s", - s, PQgetvalue(result, i, 0)); - else - printfPQExpBuffer(&buf, "%*s %s", - sw, "", PQgetvalue(result, i, 0)); - if (i < tuples - 1) - appendPQExpBufferChar(&buf, ','); + printfPQExpBuffer(&buf, "%s:", s); + printTableAddFooter(&cont, buf.data); + } + for (i = 0; i < tuples; i++) + { + printfPQExpBuffer(&buf, " %s", PQgetvalue(result, i, 0)); printTableAddFooter(&cont, buf.data); } @@ -3469,8 +3695,9 @@ describeOneTableDetails(const char *schemaname, } /* print child tables (with additional info if partitions) */ + printfPQExpBuffer(&buf, "/* %s */\n", _("Get child tables")); if (pset.sversion >= 140000) - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass, c.relkind," " inhdetachpending," " pg_catalog.pg_get_expr(c.relpartbound, c.oid)\n" @@ -3480,7 +3707,7 @@ describeOneTableDetails(const char *schemaname, " c.oid::pg_catalog.regclass::pg_catalog.text;", oid); else if (pset.sversion >= 100000) - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass, c.relkind," " false AS inhdetachpending," " pg_catalog.pg_get_expr(c.relpartbound, c.oid)\n" @@ -3490,7 +3717,7 @@ describeOneTableDetails(const char *schemaname, " c.oid::pg_catalog.regclass::pg_catalog.text;", oid); else - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT c.oid::pg_catalog.regclass, c.relkind," " false AS inhdetachpending, NULL\n" "FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i\n" @@ -3530,18 +3757,18 @@ describeOneTableDetails(const char *schemaname, { /* display the list of child tables */ const char *ct = is_partitioned ? _("Partitions") : _("Child tables"); - int ctw = pg_wcswidth(ct, strlen(ct), pset.encoding); + + if (tuples > 0) + { + printfPQExpBuffer(&buf, "%s:", ct); + printTableAddFooter(&cont, buf.data); + } for (i = 0; i < tuples; i++) { char child_relkind = *PQgetvalue(result, i, 1); - if (i == 0) - printfPQExpBuffer(&buf, "%s: %s", - ct, PQgetvalue(result, i, 0)); - else - printfPQExpBuffer(&buf, "%*s %s", - ctw, "", PQgetvalue(result, i, 0)); + printfPQExpBuffer(&buf, " %s", PQgetvalue(result, i, 0)); if (!PQgetisnull(result, i, 3)) appendPQExpBuffer(&buf, " %s", PQgetvalue(result, i, 3)); if (child_relkind == RELKIND_PARTITIONED_TABLE || @@ -3551,8 +3778,6 @@ describeOneTableDetails(const char *schemaname, appendPQExpBufferStr(&buf, ", FOREIGN"); if (strcmp(PQgetvalue(result, i, 2), "t") == 0) appendPQExpBufferStr(&buf, " (DETACH PENDING)"); - if (i < tuples - 1) - appendPQExpBufferChar(&buf, ','); printTableAddFooter(&cont, buf.data); } @@ -3664,7 +3889,9 @@ add_tablespace_footer(printTableContent *const cont, char relkind, PQExpBufferData buf; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get tablespace information for this relation")); + appendPQExpBuffer(&buf, "SELECT spcname FROM pg_catalog.pg_tablespace\n" "WHERE oid = '%u';", tablespace); result = PSQLexec(buf.data); @@ -3725,10 +3952,11 @@ describeRoles(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, - "SELECT r.rolname, r.rolsuper, r.rolinherit,\n" - " r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\n" - " r.rolconnlimit, r.rolvaliduntil"); + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching roles")); + appendPQExpBufferStr(&buf, + "SELECT r.rolname, r.rolsuper, r.rolinherit,\n" + " r.rolcreaterole, r.rolcreatedb, r.rolcanlogin,\n" + " r.rolconnlimit, r.rolvaliduntil"); if (verbose) { @@ -3762,7 +3990,7 @@ describeRoles(const char *pattern, bool verbose, bool showSystem) return false; nrows = PQntuples(res); - attr = pg_malloc0((nrows + 1) * sizeof(*attr)); + attr = pg_malloc0_array(char *, nrows + 1); printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows); @@ -3864,7 +4092,8 @@ listDbRoleSettings(const char *pattern, const char *pattern2) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, "SELECT rolname AS \"%s\", datname AS \"%s\",\n" + printfPQExpBuffer(&buf, "/* %s */\n", _("Get per-database and per-role settings")); + appendPQExpBuffer(&buf, "SELECT rolname AS \"%s\", datname AS \"%s\",\n" "pg_catalog.array_to_string(setconfig, E'\\n') AS \"%s\"\n" "FROM pg_catalog.pg_db_role_setting s\n" "LEFT JOIN pg_catalog.pg_database d ON d.oid = setdatabase\n" @@ -3931,7 +4160,8 @@ describeRoleGrants(const char *pattern, bool showSystem) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching role grants")); + appendPQExpBuffer(&buf, "SELECT m.rolname AS \"%s\", r.rolname AS \"%s\",\n" " pg_catalog.concat_ws(', ',\n", gettext_noop("Role name"), @@ -4000,6 +4230,7 @@ describeRoleGrants(const char *pattern, bool showSystem) * m - materialized views * s - sequences * E - foreign table (Note: different from 'f', the relkind value) + * G - property graphs * (any order of the above is fine) */ bool @@ -4011,6 +4242,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys bool showMatViews = strchr(tabtypes, 'm') != NULL; bool showSeq = strchr(tabtypes, 's') != NULL; bool showForeign = strchr(tabtypes, 'E') != NULL; + bool showPropGraphs = strchr(tabtypes, 'G') != NULL; int ntypes; PQExpBufferData buf; @@ -4021,14 +4253,15 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys /* Count the number of explicitly-requested relation types */ ntypes = showTables + showIndexes + showViews + showMatViews + - showSeq + showForeign; - /* If none, we default to \dtvmsE (but see also command.c) */ + showSeq + showForeign + showPropGraphs; + /* If none, we default to \dtvmsEG (but see also command.c) */ if (ntypes == 0) - showTables = showViews = showMatViews = showSeq = showForeign = true; + showTables = showViews = showMatViews = showSeq = showForeign = showPropGraphs = true; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching relations")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " c.relname as \"%s\",\n" " CASE c.relkind" @@ -4041,6 +4274,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'" " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'" " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'" + " WHEN " CppAsString2(RELKIND_PROPGRAPH) " THEN '%s'" " END as \"%s\",\n" " pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"", gettext_noop("Schema"), @@ -4054,6 +4288,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys gettext_noop("foreign table"), gettext_noop("partitioned table"), gettext_noop("partitioned index"), + gettext_noop("property graph"), gettext_noop("Type"), gettext_noop("Owner")); cols_so_far = 4; @@ -4141,6 +4376,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys appendPQExpBufferStr(&buf, "'s',"); /* was RELKIND_SPECIAL */ if (showForeign) appendPQExpBufferStr(&buf, CppAsString2(RELKIND_FOREIGN_TABLE) ","); + if (showPropGraphs) + appendPQExpBufferStr(&buf, CppAsString2(RELKIND_PROPGRAPH) ","); appendPQExpBufferStr(&buf, "''"); /* dummy */ appendPQExpBufferStr(&buf, ")\n"); @@ -4196,6 +4433,9 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys else if (showForeign) pg_log_error("Did not find any foreign tables named \"%s\".", pattern); + else if (showPropGraphs) + pg_log_error("Did not find any property graphs named \"%s\".", + pattern); else /* should not get here */ pg_log_error_internal("Did not find any ??? named \"%s\".", pattern); @@ -4216,6 +4456,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys pg_log_error("Did not find any sequences."); else if (showForeign) pg_log_error("Did not find any foreign tables."); + else if (showPropGraphs) + pg_log_error("Did not find any property graphs."); else /* should not get here */ pg_log_error_internal("Did not find any ??? relations."); } @@ -4230,6 +4472,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys (showMatViews) ? _("List of materialized views") : (showSeq) ? _("List of sequences") : (showForeign) ? _("List of foreign tables") : + (showPropGraphs) ? _("List of property graphs") : "List of ???"; /* should not get here */ myopt.translate_header = true; myopt.translate_columns = translate_columns; @@ -4301,7 +4544,9 @@ listPartitionedTables(const char *reltypes, const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get matching partitioned relations")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " c.relname as \"%s\",\n" " pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"", @@ -4476,7 +4721,8 @@ listLanguages(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching procedural languages")); + appendPQExpBuffer(&buf, "SELECT l.lanname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(l.lanowner) as \"%s\",\n" " l.lanpltrusted AS \"%s\"", @@ -4552,7 +4798,8 @@ listDomains(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching domains")); + appendPQExpBuffer(&buf, "SELECT n.nspname as \"%s\",\n" " t.typname as \"%s\",\n" " pg_catalog.format_type(t.typbasetype, t.typtypmod) as \"%s\",\n" @@ -4637,7 +4884,8 @@ listConversions(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching conversions")); + appendPQExpBuffer(&buf, "SELECT n.nspname AS \"%s\",\n" " c.conname AS \"%s\",\n" " pg_catalog.pg_encoding_to_char(c.conforencoding) AS \"%s\",\n" @@ -4715,7 +4963,10 @@ describeConfigurationParameters(const char *pattern, bool verbose, printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get matching configuration parameters")); + appendPQExpBuffer(&buf, "SELECT s.name AS \"%s\", " "pg_catalog.current_setting(s.name) AS \"%s\"", gettext_noop("Parameter"), @@ -4795,7 +5046,8 @@ listEventTriggers(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching event triggers")); + appendPQExpBuffer(&buf, "SELECT evtname as \"%s\", " "evtevent as \"%s\", " "pg_catalog.pg_get_userbyid(e.evtowner) as \"%s\",\n" @@ -4855,7 +5107,7 @@ listEventTriggers(const char *pattern, bool verbose) * Describes extended statistics. */ bool -listExtendedStats(const char *pattern) +listExtendedStats(const char *pattern, bool verbose) { PQExpBufferData buf; PGresult *res; @@ -4872,7 +5124,9 @@ listExtendedStats(const char *pattern) } initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching extended statistics")); + appendPQExpBuffer(&buf, "SELECT \n" "es.stxnamespace::pg_catalog.regnamespace::pg_catalog.text AS \"%s\", \n" "es.stxname AS \"%s\", \n", @@ -4916,6 +5170,11 @@ listExtendedStats(const char *pattern) gettext_noop("MCV")); } + if (verbose) + appendPQExpBuffer(&buf, + ", \npg_catalog.obj_description(oid, 'pg_statistic_ext') AS \"%s\"\n", + gettext_noop("Description")); + appendPQExpBufferStr(&buf, " \nFROM pg_catalog.pg_statistic_ext es \n"); @@ -4960,7 +5219,8 @@ listCasts(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching casts")); + appendPQExpBuffer(&buf, "SELECT pg_catalog.format_type(castsource, NULL) AS \"%s\",\n" " pg_catalog.format_type(casttarget, NULL) AS \"%s\",\n", gettext_noop("Source type"), @@ -5084,7 +5344,8 @@ listCollations(const char *pattern, bool verbose, bool showSystem) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching collations")); + appendPQExpBuffer(&buf, "SELECT\n" " n.nspname AS \"%s\",\n" " c.collname AS \"%s\",\n", @@ -5207,7 +5468,9 @@ listSchemas(const char *pattern, bool verbose, bool showSystem) char **footers = NULL; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching schemas")); + appendPQExpBuffer(&buf, "SELECT n.nspname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(n.nspowner) AS \"%s\"", gettext_noop("Name"), @@ -5250,7 +5513,9 @@ listSchemas(const char *pattern, bool verbose, bool showSystem) PGresult *result; int i; - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get publications that publish this schema")); + appendPQExpBuffer(&buf, "SELECT pubname \n" "FROM pg_catalog.pg_publication p\n" " JOIN pg_catalog.pg_publication_namespace pn ON p.oid = pn.pnpubid\n" @@ -5271,7 +5536,7 @@ listSchemas(const char *pattern, bool verbose, bool showSystem) * storing "Publications:" string) + publication schema mapping * count + 1 (for storing NULL). */ - footers = (char **) pg_malloc((1 + pub_schema_tuples + 1) * sizeof(char *)); + footers = pg_malloc_array(char *, 1 + pub_schema_tuples + 1); footers[0] = pg_strdup(_("Publications:")); /* Might be an empty set - that's ok */ @@ -5330,7 +5595,8 @@ listTSParsers(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching text search parsers")); + appendPQExpBuffer(&buf, "SELECT\n" " n.nspname as \"%s\",\n" " p.prsname as \"%s\",\n" @@ -5379,12 +5645,13 @@ listTSParsersVerbose(const char *pattern) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, - "SELECT p.oid,\n" - " n.nspname,\n" - " p.prsname\n" - "FROM pg_catalog.pg_ts_parser p\n" - "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.prsnamespace\n" + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching text search parsers")); + appendPQExpBufferStr(&buf, + "SELECT p.oid,\n" + " n.nspname,\n" + " p.prsname\n" + "FROM pg_catalog.pg_ts_parser p\n" + "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.prsnamespace\n" ); if (!validateSQLNamePattern(&buf, pattern, false, false, @@ -5456,7 +5723,8 @@ describeOneTSParser(const char *oid, const char *nspname, const char *prsname) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get text search parser details")); + appendPQExpBuffer(&buf, "SELECT '%s' AS \"%s\",\n" " p.prsstart::pg_catalog.regproc AS \"%s\",\n" " pg_catalog.obj_description(p.prsstart, 'pg_proc') as \"%s\"\n" @@ -5524,7 +5792,9 @@ describeOneTSParser(const char *oid, const char *nspname, const char *prsname) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get text search parser token types")); + appendPQExpBuffer(&buf, "SELECT t.alias as \"%s\",\n" " t.description as \"%s\"\n" "FROM pg_catalog.ts_token_type( '%s'::pg_catalog.oid ) as t\n" @@ -5574,7 +5844,8 @@ listTSDictionaries(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching text search dictionaries")); + appendPQExpBuffer(&buf, "SELECT\n" " n.nspname as \"%s\",\n" " d.dictname as \"%s\",\n", @@ -5639,8 +5910,9 @@ listTSTemplates(const char *pattern, bool verbose) initPQExpBuffer(&buf); + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching text search templates")); if (verbose) - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT\n" " n.nspname AS \"%s\",\n" " t.tmplname AS \"%s\",\n" @@ -5653,7 +5925,7 @@ listTSTemplates(const char *pattern, bool verbose) gettext_noop("Lexize"), gettext_noop("Description")); else - printfPQExpBuffer(&buf, + appendPQExpBuffer(&buf, "SELECT\n" " n.nspname AS \"%s\",\n" " t.tmplname AS \"%s\",\n" @@ -5707,7 +5979,8 @@ listTSConfigs(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching text search configurations")); + appendPQExpBuffer(&buf, "SELECT\n" " n.nspname as \"%s\",\n" " c.cfgname as \"%s\",\n" @@ -5753,16 +6026,17 @@ listTSConfigsVerbose(const char *pattern) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, - "SELECT c.oid, c.cfgname,\n" - " n.nspname,\n" - " p.prsname,\n" - " np.nspname as pnspname\n" - "FROM pg_catalog.pg_ts_config c\n" - " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace,\n" - " pg_catalog.pg_ts_parser p\n" - " LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.prsnamespace\n" - "WHERE p.oid = c.cfgparser\n" + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching text search configurations")); + appendPQExpBufferStr(&buf, + "SELECT c.oid, c.cfgname,\n" + " n.nspname,\n" + " p.prsname,\n" + " np.nspname as pnspname\n" + "FROM pg_catalog.pg_ts_config c\n" + " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.cfgnamespace,\n" + " pg_catalog.pg_ts_parser p\n" + " LEFT JOIN pg_catalog.pg_namespace np ON np.oid = p.prsnamespace\n" + "WHERE p.oid = c.cfgparser\n" ); if (!validateSQLNamePattern(&buf, pattern, true, false, @@ -5839,7 +6113,8 @@ describeOneTSConfig(const char *oid, const char *nspname, const char *cfgname, initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get text search configuration details")); + appendPQExpBuffer(&buf, "SELECT\n" " ( SELECT t.alias FROM\n" " pg_catalog.ts_token_type(c.cfgparser) AS t\n" @@ -5907,7 +6182,9 @@ listForeignDataWrappers(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching foreign-data wrappers")); + appendPQExpBuffer(&buf, "SELECT fdw.fdwname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(fdw.fdwowner) AS \"%s\",\n" " fdw.fdwhandler::pg_catalog.regproc AS \"%s\",\n" @@ -5978,7 +6255,9 @@ listForeignServers(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching foreign servers")); + appendPQExpBuffer(&buf, "SELECT s.srvname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(s.srvowner) AS \"%s\",\n" " f.fdwname AS \"%s\"", @@ -6054,7 +6333,9 @@ listUserMappings(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching user mappings")); + appendPQExpBuffer(&buf, "SELECT um.srvname AS \"%s\",\n" " um.usename AS \"%s\"", gettext_noop("Server"), @@ -6109,7 +6390,9 @@ listForeignTables(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching foreign tables")); + appendPQExpBuffer(&buf, "SELECT n.nspname AS \"%s\",\n" " c.relname AS \"%s\",\n" " s.srvname AS \"%s\"", @@ -6181,15 +6464,17 @@ listExtensions(const char *pattern) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching installed extensions")); + appendPQExpBuffer(&buf, "SELECT e.extname AS \"%s\", " "e.extversion AS \"%s\", ae.default_version AS \"%s\"," "n.nspname AS \"%s\", d.description AS \"%s\"\n" "FROM pg_catalog.pg_extension e " "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace " "LEFT JOIN pg_catalog.pg_description d ON d.objoid = e.oid " - "LEFT JOIN pg_catalog.pg_available_extensions() ae(name, default_version, comment) ON ae.name = e.extname " - "AND d.classoid = 'pg_catalog.pg_extension'::pg_catalog.regclass\n", + "AND d.classoid = 'pg_catalog.pg_extension'::pg_catalog.regclass " + "LEFT JOIN pg_catalog.pg_available_extensions() ae(name, default_version, comment) ON ae.name = e.extname\n", gettext_noop("Name"), gettext_noop("Version"), gettext_noop("Default version"), @@ -6235,9 +6520,11 @@ listExtensionContents(const char *pattern) int i; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, - "SELECT e.extname, e.oid\n" - "FROM pg_catalog.pg_extension e\n"); + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching installed extensions")); + appendPQExpBufferStr(&buf, + "SELECT e.extname, e.oid\n" + "FROM pg_catalog.pg_extension e\n"); if (!validateSQLNamePattern(&buf, pattern, false, false, @@ -6303,7 +6590,9 @@ listOneExtensionContents(const char *extname, const char *oid) printQueryOpt myopt = pset.popt; initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + + printfPQExpBuffer(&buf, "/* %s */\n", _("Get installed extension's contents")); + appendPQExpBuffer(&buf, "SELECT pg_catalog.pg_describe_object(classid, objid, 0) AS \"%s\"\n" "FROM pg_catalog.pg_depend\n" "WHERE refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND refobjid = '%s' AND deptype = 'e'\n" @@ -6397,7 +6686,7 @@ listPublications(const char *pattern) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false}; + static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false}; if (pset.sversion < 100000) { @@ -6411,16 +6700,24 @@ listPublications(const char *pattern) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching publications")); + appendPQExpBuffer(&buf, "SELECT pubname AS \"%s\",\n" " pg_catalog.pg_get_userbyid(pubowner) AS \"%s\",\n" - " puballtables AS \"%s\",\n" - " pubinsert AS \"%s\",\n" - " pubupdate AS \"%s\",\n" - " pubdelete AS \"%s\"", + " puballtables AS \"%s\"", gettext_noop("Name"), gettext_noop("Owner"), - gettext_noop("All tables"), + gettext_noop("All tables")); + + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ",\n puballsequences AS \"%s\"", + gettext_noop("All sequences")); + + appendPQExpBuffer(&buf, + ",\n pubinsert AS \"%s\",\n" + " pubupdate AS \"%s\",\n" + " pubdelete AS \"%s\"", gettext_noop("Inserts"), gettext_noop("Updates"), gettext_noop("Deletes")); @@ -6531,6 +6828,9 @@ describePublications(const char *pattern) bool has_pubtruncate; bool has_pubgencols; bool has_pubviaroot; + bool has_pubsequence; + int ncols = 6; + int nrows = 1; PQExpBufferData title; printTableContent cont; @@ -6545,16 +6845,29 @@ describePublications(const char *pattern) return true; } + has_pubsequence = (pset.sversion >= 190000); has_pubtruncate = (pset.sversion >= 110000); has_pubgencols = (pset.sversion >= 180000); has_pubviaroot = (pset.sversion >= 130000); initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, - "SELECT oid, pubname,\n" - " pg_catalog.pg_get_userbyid(pubowner) AS owner,\n" - " puballtables, pubinsert, pubupdate, pubdelete"); + printfPQExpBuffer(&buf, "/* %s */\n", _("Get details about matching publications")); + appendPQExpBufferStr(&buf, + "SELECT oid, pubname,\n" + " pg_catalog.pg_get_userbyid(pubowner) AS owner,\n" + " puballtables"); + + if (has_pubsequence) + appendPQExpBufferStr(&buf, + ", puballsequences"); + else + appendPQExpBufferStr(&buf, + ", false AS puballsequences"); + + appendPQExpBufferStr(&buf, + ", pubinsert, pubupdate, pubdelete"); + if (has_pubtruncate) appendPQExpBufferStr(&buf, ", pubtruncate"); @@ -6582,6 +6895,9 @@ describePublications(const char *pattern) appendPQExpBufferStr(&buf, ", false AS pubviaroot"); + appendPQExpBufferStr(&buf, + ", pg_catalog.obj_description(oid, 'pg_publication')"); + appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_publication\n"); @@ -6619,29 +6935,31 @@ describePublications(const char *pattern) return false; } + if (has_pubsequence) + ncols++; + if (has_pubtruncate) + ncols++; + if (has_pubgencols) + ncols++; + if (has_pubviaroot) + ncols++; + for (i = 0; i < PQntuples(res); i++) { const char align = 'l'; - int ncols = 5; - int nrows = 1; char *pubid = PQgetvalue(res, i, 0); char *pubname = PQgetvalue(res, i, 1); bool puballtables = strcmp(PQgetvalue(res, i, 3), "t") == 0; printTableOpt myopt = pset.popt.topt; - if (has_pubtruncate) - ncols++; - if (has_pubgencols) - ncols++; - if (has_pubviaroot) - ncols++; - initPQExpBuffer(&title); printfPQExpBuffer(&title, _("Publication %s"), pubname); printTableInit(&cont, &myopt, title.data, ncols, nrows); printTableAddHeader(&cont, gettext_noop("Owner"), true, align); printTableAddHeader(&cont, gettext_noop("All tables"), true, align); + if (has_pubsequence) + printTableAddHeader(&cont, gettext_noop("All sequences"), true, align); printTableAddHeader(&cont, gettext_noop("Inserts"), true, align); printTableAddHeader(&cont, gettext_noop("Updates"), true, align); printTableAddHeader(&cont, gettext_noop("Deletes"), true, align); @@ -6651,24 +6969,29 @@ describePublications(const char *pattern) printTableAddHeader(&cont, gettext_noop("Generated columns"), true, align); if (has_pubviaroot) printTableAddHeader(&cont, gettext_noop("Via root"), true, align); + printTableAddHeader(&cont, gettext_noop("Description"), true, align); printTableAddCell(&cont, PQgetvalue(res, i, 2), false, false); printTableAddCell(&cont, PQgetvalue(res, i, 3), false, false); - printTableAddCell(&cont, PQgetvalue(res, i, 4), false, false); + if (has_pubsequence) + printTableAddCell(&cont, PQgetvalue(res, i, 4), false, false); printTableAddCell(&cont, PQgetvalue(res, i, 5), false, false); printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 7), false, false); if (has_pubtruncate) - printTableAddCell(&cont, PQgetvalue(res, i, 7), false, false); - if (has_pubgencols) printTableAddCell(&cont, PQgetvalue(res, i, 8), false, false); - if (has_pubviaroot) + if (has_pubgencols) printTableAddCell(&cont, PQgetvalue(res, i, 9), false, false); + if (has_pubviaroot) + printTableAddCell(&cont, PQgetvalue(res, i, 10), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 11), false, false); if (!puballtables) { /* Get the tables for the specified publication */ - printfPQExpBuffer(&buf, - "SELECT n.nspname, c.relname"); + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get tables published by this publication")); + appendPQExpBufferStr(&buf, "SELECT n.nspname, c.relname"); if (pset.sversion >= 150000) { appendPQExpBufferStr(&buf, @@ -6692,15 +7015,21 @@ describePublications(const char *pattern) " pg_catalog.pg_publication_rel pr\n" "WHERE c.relnamespace = n.oid\n" " AND c.oid = pr.prrelid\n" - " AND pr.prpubid = '%s'\n" - "ORDER BY 1,2", pubid); + " AND pr.prpubid = '%s'\n", pubid); + + if (pset.sversion >= 190000) + appendPQExpBufferStr(&buf, " AND NOT pr.prexcept\n"); + + appendPQExpBufferStr(&buf, "ORDER BY 1,2"); if (!addFooterToPublicationDesc(&buf, _("Tables:"), false, &cont)) goto error_return; if (pset.sversion >= 150000) { /* Get the schemas for the specified publication */ - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get schemas published by this publication")); + appendPQExpBuffer(&buf, "SELECT n.nspname\n" "FROM pg_catalog.pg_namespace n\n" " JOIN pg_catalog.pg_publication_namespace pn ON n.oid = pn.pnnspid\n" @@ -6711,6 +7040,25 @@ describePublications(const char *pattern) goto error_return; } } + else + { + if (pset.sversion >= 190000) + { + /* Get tables in the EXCEPT clause for this publication */ + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get tables excluded by this publication")); + appendPQExpBuffer(&buf, + "SELECT n.nspname || '.' || c.relname\n" + "FROM pg_catalog.pg_class c\n" + " JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" + " JOIN pg_catalog.pg_publication_rel pr ON c.oid = pr.prrelid\n" + "WHERE pr.prpubid = '%s' AND pr.prexcept\n" + "ORDER BY 1", pubid); + if (!addFooterToPublicationDesc(&buf, _("Except tables:"), + true, &cont)) + goto error_return; + } + } printTable(&cont, pset.queryFout, false, pset.logfile); printTableCleanup(&cont); @@ -6745,7 +7093,7 @@ describeSubscriptions(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false}; + false, false, false, false, false, false, false}; if (pset.sversion < 100000) { @@ -6759,7 +7107,8 @@ describeSubscriptions(const char *pattern, bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching subscriptions")); + appendPQExpBuffer(&buf, "SELECT subname AS \"%s\"\n" ", pg_catalog.pg_get_userbyid(subowner) AS \"%s\"\n" ", subenabled AS \"%s\"\n" @@ -6813,6 +7162,24 @@ describeSubscriptions(const char *pattern, bool verbose) appendPQExpBuffer(&buf, ", subfailover AS \"%s\"\n", gettext_noop("Failover")); + if (pset.sversion >= 190000) + { + appendPQExpBuffer(&buf, + ", (select srvname from pg_foreign_server where oid=subserver) AS \"%s\"\n", + gettext_noop("Server")); + + appendPQExpBuffer(&buf, + ", subretaindeadtuples AS \"%s\"\n", + gettext_noop("Retain dead tuples")); + + appendPQExpBuffer(&buf, + ", submaxretention AS \"%s\"\n", + gettext_noop("Max retention duration")); + + appendPQExpBuffer(&buf, + ", subretentionactive AS \"%s\"\n", + gettext_noop("Retention active")); + } appendPQExpBuffer(&buf, ", subsynccommit AS \"%s\"\n" @@ -6820,11 +7187,20 @@ describeSubscriptions(const char *pattern, bool verbose) gettext_noop("Synchronous commit"), gettext_noop("Conninfo")); + if (pset.sversion >= 190000) + appendPQExpBuffer(&buf, + ", subwalrcvtimeout AS \"%s\"\n", + gettext_noop("Receiver timeout")); + /* Skip LSN is only supported in v15 and higher */ if (pset.sversion >= 150000) appendPQExpBuffer(&buf, ", subskiplsn AS \"%s\"\n", gettext_noop("Skip LSN")); + + appendPQExpBuffer(&buf, + ", pg_catalog.obj_description(oid, 'pg_subscription') AS \"%s\"\n", + gettext_noop("Description")); } /* Only display subscriptions in current database. */ @@ -6901,7 +7277,8 @@ listOperatorClasses(const char *access_method_pattern, initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching operator classes")); + appendPQExpBuffer(&buf, "SELECT\n" " am.amname AS \"%s\",\n" " pg_catalog.format_type(c.opcintype, NULL) AS \"%s\",\n" @@ -7002,7 +7379,8 @@ listOperatorFamilies(const char *access_method_pattern, initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get matching operator families")); + appendPQExpBuffer(&buf, "SELECT\n" " am.amname AS \"%s\",\n" " CASE\n" @@ -7092,7 +7470,8 @@ listOpFamilyOperators(const char *access_method_pattern, initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get operators of matching operator families")); + appendPQExpBuffer(&buf, "SELECT\n" " am.amname AS \"%s\",\n" " CASE\n" @@ -7198,7 +7577,9 @@ listOpFamilyFunctions(const char *access_method_pattern, initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", + _("Get support functions of matching operator families")); + appendPQExpBuffer(&buf, "SELECT\n" " am.amname AS \"%s\",\n" " CASE\n" @@ -7284,7 +7665,8 @@ listLargeObjects(bool verbose) initPQExpBuffer(&buf); - printfPQExpBuffer(&buf, + printfPQExpBuffer(&buf, "/* %s */\n", _("Get large objects")); + appendPQExpBuffer(&buf, "SELECT oid as \"%s\",\n" " pg_catalog.pg_get_userbyid(lomowner) as \"%s\",\n ", gettext_noop("ID"), diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 18ecaa60949d6..47fae5ceafb33 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/describe.h */ @@ -114,7 +114,7 @@ extern bool listExtensions(const char *pattern); extern bool listExtensionContents(const char *pattern); /* \dX */ -extern bool listExtendedStats(const char *pattern); +extern bool listExtendedStats(const char *pattern, bool verbose); /* \dy */ extern bool listEventTriggers(const char *pattern, bool verbose); diff --git a/src/bin/psql/gen_tabcomplete.pl b/src/bin/psql/gen_tabcomplete.pl index 37899dd02bc19..b2a90fa541d23 100644 --- a/src/bin/psql/gen_tabcomplete.pl +++ b/src/bin/psql/gen_tabcomplete.pl @@ -40,7 +40,7 @@ # The tab-completion data line must appear before BEGIN GEN_TABCOMPLETE. # # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/psql/gen_tabcomplete.pl @@ -77,7 +77,7 @@ * tab-complete.c * Preprocessed tab-completion code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 403b51325a72c..5e0d8f3aae1fd 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/help.c */ @@ -171,6 +171,10 @@ slashUsage(unsigned short int pager) HELP0(" \\gset [PREFIX] execute query and store result in psql variables\n"); HELP0(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n"); HELP0(" \\q quit psql\n"); + HELP0(" \\restrict RESTRICT_KEY\n" + " enter restricted mode with provided key\n"); + HELP0(" \\unrestrict RESTRICT_KEY\n" + " exit restricted mode if key matches\n"); HELP0(" \\watch [[i=]SEC] [c=N] [m=MIN]\n" " execute query every SEC seconds, up to N times,\n" " stop if less than MIN rows are returned\n"); @@ -215,8 +219,8 @@ slashUsage(unsigned short int pager) HELP0("Informational\n"); HELP0(" (options: S = show system objects, x = expanded mode, + = additional detail)\n"); - HELP0(" \\d[Sx+] list tables, views, and sequences\n"); - HELP0(" \\d[S+] NAME describe table, view, sequence, or index\n"); + HELP0(" \\d[Sx+] list tables, views, sequences, and property graphs\n"); + HELP0(" \\d[S+] NAME describe table, view, sequence, index, or property graph\n"); HELP0(" \\da[Sx] [PATTERN] list aggregates\n"); HELP0(" \\dA[x+] [PATTERN] list access methods\n"); HELP0(" \\dAc[x+] [AMPTRN [TYPEPTRN]] list operator classes\n"); @@ -242,6 +246,7 @@ slashUsage(unsigned short int pager) HELP0(" \\dFp[x+] [PATTERN] list text search parsers\n"); HELP0(" \\dFt[x+] [PATTERN] list text search templates\n"); HELP0(" \\dg[Sx+] [PATTERN] list roles\n"); + HELP0(" \\dG[Sx+] [PATTERN] list property graphs\n"); HELP0(" \\di[Sx+] [PATTERN] list indexes\n"); HELP0(" \\dl[x+] list large objects, same as \\lo_list\n"); HELP0(" \\dL[Sx+] [PATTERN] list procedural languages\n"); @@ -252,7 +257,8 @@ slashUsage(unsigned short int pager) HELP0(" \\dO[Sx+] [PATTERN] list collations\n"); HELP0(" \\dp[Sx] [PATTERN] list table, view, and sequence access privileges\n"); HELP0(" \\dP[itnx+] [PATTERN] list [only index/table] partitioned relations [n=nested]\n"); - HELP0(" \\drds[x] [ROLEPTRN [DBPTRN]] list per-database role settings\n"); + HELP0(" \\drds[x] [ROLEPTRN [DBPTRN]]\n" + " list per-database role settings\n"); HELP0(" \\drg[Sx] [PATTERN] list role grants\n"); HELP0(" \\dRp[x+] [PATTERN] list replication publications\n"); HELP0(" \\dRs[x+] [PATTERN] list replication subscriptions\n"); @@ -262,7 +268,7 @@ slashUsage(unsigned short int pager) HELP0(" \\du[Sx+] [PATTERN] list roles\n"); HELP0(" \\dv[Sx+] [PATTERN] list views\n"); HELP0(" \\dx[x+] [PATTERN] list extensions\n"); - HELP0(" \\dX[x] [PATTERN] list extended statistics\n"); + HELP0(" \\dX[x+] [PATTERN] list extended statistics\n"); HELP0(" \\dy[x+] [PATTERN] list event triggers\n"); HELP0(" \\l[x+] [PATTERN] list databases\n"); HELP0(" \\sf[+] FUNCNAME show a function's definition\n"); @@ -285,12 +291,7 @@ slashUsage(unsigned short int pager) HELPN(" \\H toggle HTML output mode (currently %s)\n", ON(pset.popt.topt.format == PRINT_HTML)); HELP0(" \\pset [NAME [VALUE]] set table output option\n" - " (border|columns|csv_fieldsep|expanded|fieldsep|\n" - " fieldsep_zero|footer|format|linestyle|null|\n" - " numericlocale|pager|pager_min_lines|recordsep|\n" - " recordsep_zero|tableattr|title|tuples_only|\n" - " unicode_border_linestyle|unicode_column_linestyle|\n" - " unicode_header_linestyle|xheader_width)\n"); + " see \"\\? variables\" for valid options\n"); HELPN(" \\t [on|off] show only rows (currently %s)\n", ON(pset.popt.topt.tuples_only)); HELP0(" \\T [STRING] set HTML
tag attributes, or unset if none\n"); @@ -330,12 +331,12 @@ slashUsage(unsigned short int pager) HELP0(" \\bind [PARAM]... set query parameters\n"); HELP0(" \\bind_named STMT_NAME [PARAM]...\n" " set query parameters for an existing prepared statement\n"); - HELP0(" \\close STMT_NAME close an existing prepared statement\n"); + HELP0(" \\close_prepared STMT_NAME\n" + " close an existing prepared statement\n"); HELP0(" \\endpipeline exit pipeline mode\n"); HELP0(" \\flush flush output data to the server\n"); HELP0(" \\flushrequest send request to the server to flush its output buffer\n"); - HELP0(" \\getresults [NUM_RES] read NUM_RES pending results. All pending results are\n" - " read if no argument is provided\n"); + HELP0(" \\getresults [NUM_RES] read NUM_RES pending results, or all if no argument\n"); HELP0(" \\parse STMT_NAME create a prepared statement\n"); HELP0(" \\sendpipeline send an extended query to an ongoing pipeline\n"); HELP0(" \\startpipeline enter pipeline mode\n"); @@ -463,8 +464,9 @@ helpVariables(unsigned short int pager) " VERSION_NAME\n" " VERSION_NUM\n" " psql's version (in verbose string, short string, or numeric format)\n"); - HELP0(" WATCH_INTERVAL\n" - " if set to a number, overrides the default two second \\watch interval\n"); + HELPN(" WATCH_INTERVAL\n" + " number of seconds \\watch waits between executions (default %s)\n", + DEFAULT_WATCH_INTERVAL); HELP0("\nDisplay settings:\n"); HELP0("Usage:\n"); @@ -474,6 +476,13 @@ helpVariables(unsigned short int pager) " border style (number)\n"); HELP0(" columns\n" " target width for the wrapped format\n"); + HELPN(" csv_fieldsep\n" + " field separator for CSV output format (default \"%c\")\n", + DEFAULT_CSV_FIELD_SEP); + HELP0(" display_false\n" + " set the string to be printed in place of a boolean 'false'\n"); + HELP0(" display_true\n" + " set the string to be printed in place of a boolean 'true'\n"); HELP0(" expanded (or x)\n" " expanded output [on, off, auto]\n"); HELPN(" fieldsep\n" @@ -746,8 +755,8 @@ void print_copyright(void) { puts("PostgreSQL Database Management System\n" - "(formerly known as Postgres, then as Postgres95)\n\n" - "Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group\n\n" + "(also known as Postgres, formerly known as Postgres95)\n\n" + "Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group\n\n" "Portions Copyright (c) 1994, The Regents of the University of California\n\n" "Permission to use, copy, modify, and distribute this software and its\n" "documentation for any purpose, without fee, and without a written agreement\n" diff --git a/src/bin/psql/help.h b/src/bin/psql/help.h index 4a6c566d45f9f..a57024422d139 100644 --- a/src/bin/psql/help.h +++ b/src/bin/psql/help.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/help.h */ diff --git a/src/bin/psql/input.c b/src/bin/psql/input.c index d0156ac4de21b..9f9c1cbbf502d 100644 --- a/src/bin/psql/input.c +++ b/src/bin/psql/input.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/input.c */ diff --git a/src/bin/psql/input.h b/src/bin/psql/input.h index 6b2f84e0d0cf4..2a47166347e14 100644 --- a/src/bin/psql/input.h +++ b/src/bin/psql/input.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/input.h */ @@ -17,6 +17,15 @@ #ifdef HAVE_LIBREADLINE #define USE_READLINE 1 +/* + * Readline headers trigger a lot of warnings with our preferred compiler flags + * (at least -Wstrict-prototypes is known to be problematic). The system_header + * pragma hides warnings from within the rest of this file, if supported. + */ +#ifdef HAVE_PRAGMA_GCC_SYSTEM_HEADER +#pragma GCC system_header +#endif + #if defined(HAVE_READLINE_READLINE_H) #include #if defined(HAVE_READLINE_HISTORY_H) diff --git a/src/bin/psql/large_obj.c b/src/bin/psql/large_obj.c index 28f8c414b672c..021f78e0f78e3 100644 --- a/src/bin/psql/large_obj.c +++ b/src/bin/psql/large_obj.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/large_obj.c */ diff --git a/src/bin/psql/large_obj.h b/src/bin/psql/large_obj.h index c6467f80f5a1a..001a3f98ac05c 100644 --- a/src/bin/psql/large_obj.h +++ b/src/bin/psql/large_obj.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/large_obj.h */ diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c index 0e0baa3540b14..e9abda07161d9 100644 --- a/src/bin/psql/mainloop.c +++ b/src/bin/psql/mainloop.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/mainloop.c */ diff --git a/src/bin/psql/mainloop.h b/src/bin/psql/mainloop.h index 075bbfed41b4f..29ac1cac7bab7 100644 --- a/src/bin/psql/mainloop.h +++ b/src/bin/psql/mainloop.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/mainloop.h */ diff --git a/src/bin/psql/meson.build b/src/bin/psql/meson.build index f795ff282711c..922b28452672f 100644 --- a/src/bin/psql/meson.build +++ b/src/bin/psql/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group psql_sources = files( 'command.c', @@ -77,6 +77,7 @@ tests += { 't/001_basic.pl', 't/010_tab_completion.pl', 't/020_cancel.pl', + 't/030_pager.pl', ], }, } diff --git a/src/bin/psql/po/meson.build b/src/bin/psql/po/meson.build index 6b5abd0e49276..2d995e0b7f84a 100644 --- a/src/bin/psql/po/meson.build +++ b/src/bin/psql/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('psql-' + pg_version_major.to_string())] diff --git a/src/bin/psql/prompt.c b/src/bin/psql/prompt.c index 3aa7d2d06c80e..9725d53dfe784 100644 --- a/src/bin/psql/prompt.c +++ b/src/bin/psql/prompt.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/prompt.c */ @@ -34,6 +34,7 @@ * %P - pipeline status: on, off or abort * %> - database server port number * %n - database user name + * %S - search_path * %s - service * %/ - current database * %~ - like %/ but "~" when database name equals user name @@ -43,6 +44,8 @@ * or a ! if session is not connected to a database; * in prompt2 -, *, ', or "; * in prompt3 nothing + * %i - "standby" or "primary" depending on the server's in_hot_standby + * status, or "?" if unavailable (empty if unknown) * %x - transaction status: empty, *, !, ? (unknown or no connection) * %l - The line number inside the current statement, starting from 1. * %? - the error code of the last query (not yet implemented) @@ -167,10 +170,24 @@ get_prompt(promptStatus_t status, ConditionalStack cstack) if (pset.db) strlcpy(buf, session_username(), sizeof(buf)); break; + /* search_path */ + case 'S': + if (pset.db) + { + const char *sp = PQparameterStatus(pset.db, "search_path"); + + /* Use ? for versions that don't report search_path. */ + strlcpy(buf, sp ? sp : "?", sizeof(buf)); + } + break; /* service name */ case 's': - if (pset.db && PQservice(pset.db)) - strlcpy(buf, PQservice(pset.db), sizeof(buf)); + { + const char *service_name = GetVariable(pset.vars, "SERVICE"); + + if (service_name) + strlcpy(buf, service_name, sizeof(buf)); + } break; /* backend pid */ case 'p': @@ -184,6 +201,7 @@ get_prompt(promptStatus_t status, ConditionalStack cstack) break; /* pipeline status */ case 'P': + if (pset.db) { PGpipelineStatus status = PQpipelineStatus(pset.db); @@ -193,9 +211,8 @@ get_prompt(promptStatus_t status, ConditionalStack cstack) strlcpy(buf, "abort", sizeof(buf)); else strlcpy(buf, "off", sizeof(buf)); - break; } - + break; case '0': case '1': case '2': @@ -243,7 +260,23 @@ get_prompt(promptStatus_t status, ConditionalStack cstack) break; } break; + case 'i': + if (pset.db) + { + const char *hs = PQparameterStatus(pset.db, "in_hot_standby"); + if (hs) + { + if (strcmp(hs, "on") == 0) + strlcpy(buf, "standby", sizeof(buf)); + else + strlcpy(buf, "primary", sizeof(buf)); + } + /* Use ? for versions that don't report in_hot_standby */ + else + buf[0] = '?'; + } + break; case 'x': if (!pset.db) buf[0] = '?'; diff --git a/src/bin/psql/prompt.h b/src/bin/psql/prompt.h index f1f6ca51aba0a..3227cb6eeb742 100644 --- a/src/bin/psql/prompt.h +++ b/src/bin/psql/prompt.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/prompt.h */ diff --git a/src/bin/psql/psqlscanslash.h b/src/bin/psql/psqlscanslash.h index 497e7e2c9634a..d95a6ff2d1ebb 100644 --- a/src/bin/psql/psqlscanslash.h +++ b/src/bin/psql/psqlscanslash.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/psqlscanslash.h */ diff --git a/src/bin/psql/psqlscanslash.l b/src/bin/psql/psqlscanslash.l index ae7602a61dfc1..2d7557523745d 100644 --- a/src/bin/psql/psqlscanslash.l +++ b/src/bin/psql/psqlscanslash.l @@ -8,7 +8,7 @@ * * See fe_utils/psqlscan_int.h for additional commentary. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index fd82303f776c4..a3d088d80cd73 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/settings.h */ diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 249b6aa516902..69d044d405d5b 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/startup.c */ @@ -24,6 +24,7 @@ #include "help.h" #include "input.h" #include "mainloop.h" +#include "portability/instr_time.h" #include "settings.h" /* @@ -327,6 +328,9 @@ main(int argc, char *argv[]) PQsetNoticeProcessor(pset.db, NoticeProcessor, NULL); + /* initialize timing infrastructure (required for INSTR_* calls) */ + pg_initialize_timing(); + SyncVariables(); if (options.list_dbs) diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c index f613413553e29..ad7562271186a 100644 --- a/src/bin/psql/stringutils.c +++ b/src/bin/psql/stringutils.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/stringutils.c */ diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h index 5b91e96410344..01c2f79e597c5 100644 --- a/src/bin/psql/stringutils.h +++ b/src/bin/psql/stringutils.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/stringutils.h */ diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index 4050f9a5e3e11..7c21204c1f2f8 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -141,13 +141,12 @@ sub psql_fails_like is($ret, 2, 'server crash: psql exit code'); like($out, qr/before/, 'server crash: output before crash'); -ok($out !~ qr/AFTER/, 'server crash: no output after crash'); -is( $err, - 'psql::2: FATAL: terminating connection due to administrator command -psql::2: server closed the connection unexpectedly +unlike($out, qr/AFTER/, 'server crash: no output after crash'); +like( $err, qr/psql::2: FATAL: terminating connection due to administrator command +(?:DETAIL: Signal sent by PID \d+, UID \d+\.\n)?psql::2: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. -psql::2: error: connection to server was lost', +psql::2: error: connection to server was lost/, 'server crash: error message'); # test \errverbose @@ -483,8 +482,8 @@ sub psql_fails_like my $c4 = slurp_file($g_file); like($c4, qr/foo.*bar/s); -# Tests with pipelines. These trigger FATAL failures in the backend, -# so they cannot be tested via SQL. +# Test COPY within pipelines. These abort the connection from +# the frontend so they cannot be tested via SQL. $node->safe_psql('postgres', 'CREATE TABLE psql_pipeline()'); my $log_location = -s $node->logfile; psql_fails_like( @@ -493,35 +492,48 @@ sub psql_fails_like COPY psql_pipeline FROM STDIN; SELECT 'val1'; \\syncpipeline -\\getresults \\endpipeline}, - qr/server closed the connection unexpectedly/, - 'protocol sync loss in pipeline: direct COPY, SELECT, sync and getresult' -); + qr/COPY in a pipeline is not supported, aborting connection/, + 'COPY FROM in pipeline: fails'); $node->wait_for_log( qr/FATAL: .*terminating connection because protocol synchronization was lost/, $log_location); +# Remove \syncpipeline here. psql_fails_like( $node, qq{\\startpipeline -COPY psql_pipeline FROM STDIN \\bind \\sendpipeline -SELECT 'val1' \\bind \\sendpipeline -\\syncpipeline -\\getresults +COPY psql_pipeline TO STDOUT; +SELECT 'val1'; \\endpipeline}, - qr/server closed the connection unexpectedly/, - 'protocol sync loss in pipeline: bind COPY, SELECT, sync and getresult'); + qr/COPY in a pipeline is not supported, aborting connection/, + 'COPY TO in pipeline: fails'); -# This time, test without the \getresults. psql_fails_like( $node, qq{\\startpipeline -COPY psql_pipeline FROM STDIN; +\\copy psql_pipeline from stdin; SELECT 'val1'; \\syncpipeline \\endpipeline}, - qr/server closed the connection unexpectedly/, - 'protocol sync loss in pipeline: COPY, SELECT and sync'); + qr/COPY in a pipeline is not supported, aborting connection/, + '\copy from in pipeline: fails'); + +# Sync attempt after a COPY TO/FROM. +psql_fails_like( + $node, + qq{\\startpipeline +\\copy psql_pipeline to stdout; +\\syncpipeline +\\endpipeline}, + qr/COPY in a pipeline is not supported, aborting connection/, + '\copy to in pipeline: fails'); + +psql_fails_like( + $node, + qq{\\restrict test +\\! should_fail}, + qr/backslash commands are restricted; only \\unrestrict is allowed/, + 'meta-command in restrict mode fails'); done_testing(); diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl index fa7427af8c58d..1d2e5f5b92a7c 100644 --- a/src/bin/psql/t/010_tab_completion.pl +++ b/src/bin/psql/t/010_tab_completion.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -77,8 +77,10 @@ # for possible debugging purposes. my $historyfile = "${PostgreSQL::Test::Utils::log_path}/010_psql_history.txt"; -# fire up an interactive psql session +# fire up an interactive psql session and configure it such that each query +# restarts the timer my $h = $node->interactive_psql('postgres', history_file => $historyfile); +$h->set_query_timer_restart(); # Simple test case: type something and see if psql responds as expected sub check_completion @@ -88,9 +90,6 @@ sub check_completion # report test failures from caller location local $Test::Builder::Level = $Test::Builder::Level + 1; - # restart per-command timer - $h->{timeout}->start($PostgreSQL::Test::Utils::timeout_default); - # send the data to be sent and wait for its result my $out = $h->query_until($pattern, $send); my $okay = ($out =~ $pattern && !$h->{timeout}->is_expired); diff --git a/src/bin/psql/t/020_cancel.pl b/src/bin/psql/t/020_cancel.pl index 154a24bca98c7..08cfb95153713 100644 --- a/src/bin/psql/t/020_cancel.pl +++ b/src/bin/psql/t/020_cancel.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/psql/t/030_pager.pl b/src/bin/psql/t/030_pager.pl new file mode 100644 index 0000000000000..d3f964639d3ae --- /dev/null +++ b/src/bin/psql/t/030_pager.pl @@ -0,0 +1,138 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use Data::Dumper; + +# If we don't have IO::Pty, forget it, because IPC::Run depends on that +# to support pty connections +eval { require IO::Pty; }; +if ($@) +{ + plan skip_all => 'IO::Pty is needed to run this test'; +} + +# Check that "wc -l" does what we expect, else forget it +my $wcstdin = "foo bar\nbaz\n"; +my ($wcstdout, $wcstderr); +my $result = IPC::Run::run [ 'wc', '-l' ], + '<' => \$wcstdin, + '>' => \$wcstdout, + '2>' => \$wcstderr; +chomp $wcstdout; +if ($wcstdout !~ /^ *2$/ || $wcstderr ne '') +{ + note "wc stdout = '$wcstdout'\n"; + note "wc stderr = '$wcstderr'\n"; + plan skip_all => '"wc -l" is needed to run this test'; +} + +# We set up "wc -l" as the pager so we can tell whether psql used the pager +$ENV{PSQL_PAGER} = "wc -l"; + +# start a new server +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + +# create a view we'll use below +$node->safe_psql( + 'postgres', 'create view public.view_030_pager as select +1 as a, +2 as b, +3 as c, +4 as d, +5 as e, +6 as f, +7 as g, +8 as h, +9 as i, +10 as j, +11 as k, +12 as l, +13 as m, +14 as n, +15 as o, +16 as p, +17 as q, +18 as r, +19 as s, +20 as t, +21 as u, +22 as v, +23 as w, +24 as x, +25 as y, +26 as z'); + +# fire up an interactive psql session and configure it such that each query +# restarts the timer +my $h = $node->interactive_psql('postgres'); +$h->set_query_timer_restart(); + +# set the pty's window size to known values +# (requires undesirable chumminess with the innards of IPC::Run) +for my $pty (values %{ $h->{run}->{PTYS} }) +{ + $pty->set_winsize(24, 80); +} + +# Simple test case: type something and see if psql responds as expected +sub do_command +{ + my ($send, $pattern, $annotation) = @_; + + # report test failures from caller location + local $Test::Builder::Level = $Test::Builder::Level + 1; + + # send the data to be sent and wait for its result + my $out = $h->query_until($pattern, $send); + my $okay = ($out =~ $pattern && !$h->{timeout}->is_expired); + ok($okay, $annotation); + # for debugging, log actual output if it didn't match + local $Data::Dumper::Terse = 1; + local $Data::Dumper::Useqq = 1; + diag 'Actual output was ' . Dumper($out) . "Did not match \"$pattern\"\n" + if !$okay; + return; +} + +# Test invocation of the pager +# +# Note that interactive_psql starts psql with --no-align --tuples-only, +# and that the output string will include psql's prompts and command echo. +# So we have to test for patterns that can't match the command itself, +# and we can't assume the match will extend across a whole line (there +# might be a prompt ahead of it in the output). + +do_command( + "SELECT 'test' AS t FROM generate_series(1,23);\n", + qr/test\r?$/m, + "execute SELECT query that needs no pagination"); + +do_command( + "SELECT 'test' AS t FROM generate_series(1,24);\n", + qr/24\r?$/m, + "execute SELECT query that needs pagination"); + +do_command( + "\\pset expanded\nSELECT generate_series(1,20) as g;\n", + qr/39\r?$/m, + "execute SELECT query that needs pagination in expanded mode"); + +do_command( + "\\pset tuples_only off\n\\d+ public.view_030_pager\n", + qr/55\r?$/m, + "execute command with footer that needs pagination"); + +# send psql an explicit \q to shut it down, else pty won't close properly +$h->quit or die "psql returned $?"; + +# done +$node->stop; +done_testing(); diff --git a/src/bin/psql/tab-complete.h b/src/bin/psql/tab-complete.h index 8d24e71f1bc62..fda737bfcaa79 100644 --- a/src/bin/psql/tab-complete.h +++ b/src/bin/psql/tab-complete.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/tab-complete.h */ diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index ec65ab79fecba..db65d130fcbbf 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/tab-complete.in.c * @@ -443,6 +443,26 @@ do { \ matches = rl_completion_matches(text, complete_from_schema_query); \ } while (0) +#define COMPLETE_WITH_FILES_LIST(escape, force_quote, list) \ +do { \ + completion_charp = escape; \ + completion_charpp = list; \ + completion_force_quote = force_quote; \ + matches = rl_completion_matches(text, complete_from_files); \ +} while (0) + +#define COMPLETE_WITH_FILES(escape, force_quote) \ + COMPLETE_WITH_FILES_LIST(escape, force_quote, NULL) + +#define COMPLETE_WITH_FILES_PLUS(escape, force_quote, ...) \ +do { \ + static const char *const list[] = { __VA_ARGS__, NULL }; \ + COMPLETE_WITH_FILES_LIST(escape, force_quote, list); \ +} while (0) + +#define COMPLETE_WITH_GENERATOR(generator) \ + matches = rl_completion_matches(text, generator) + /* * Assembly instructions for schema queries * @@ -795,6 +815,14 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = { .result = "c.relname", }; +static const SchemaQuery Query_for_list_of_propgraphs = { + .catname = "pg_catalog.pg_class c", + .selcondition = "c.relkind IN (" CppAsString2(RELKIND_PROPGRAPH) ")", + .viscondition = "pg_catalog.pg_table_is_visible(c.oid)", + .namespace = "c.relnamespace", + .result = "pg_catalog.quote_ident(c.relname)", +}; + /* All relations */ static const SchemaQuery Query_for_list_of_relations = { @@ -889,6 +917,14 @@ static const SchemaQuery Query_for_list_of_analyzables = { .result = "c.relname", }; +/* + * Relations supporting COPY TO/FROM are currently almost the same as + * those supporting ANALYZE. Although views with INSTEAD OF INSERT triggers + * can be used with COPY FROM, they are rarely used for this purpose, + * so plain views are intentionally excluded from this tab completion. + */ +#define Query_for_list_of_tables_for_copy Query_for_list_of_analyzables + /* Relations supporting index creation */ static const SchemaQuery Query_for_list_of_indexables = { .catname = "pg_catalog.pg_class c", @@ -1002,7 +1038,7 @@ static const SchemaQuery Query_for_trigger_of_table = { #define Query_for_list_of_database_vars \ "SELECT conf FROM ("\ -" SELECT setdatabase, pg_catalog.split_part(unnest(setconfig),'=',1) conf"\ +" SELECT setdatabase, pg_catalog.split_part(pg_catalog.unnest(setconfig),'=',1) conf"\ " FROM pg_db_role_setting "\ " ) s, pg_database d "\ " WHERE s.setdatabase = d.oid "\ @@ -1078,9 +1114,12 @@ Keywords_for_list_of_owner_roles, "PUBLIC" " WHERE usename LIKE '%s'" #define Query_for_list_of_user_vars \ -" SELECT pg_catalog.split_part(pg_catalog.unnest(rolconfig),'=',1) "\ -" FROM pg_catalog.pg_roles "\ -" WHERE rolname LIKE '%s'" +"SELECT conf FROM ("\ +" SELECT rolname, pg_catalog.split_part(pg_catalog.unnest(rolconfig),'=',1) conf"\ +" FROM pg_catalog.pg_roles"\ +" ) s"\ +" WHERE s.conf like '%s' "\ +" AND s.rolname LIKE '%s'" #define Query_for_list_of_access_methods \ " SELECT amname "\ @@ -1190,6 +1229,19 @@ Alter_procedure_options, "COST", "IMMUTABLE", "LEAKPROOF", "NOT LEAKPROOF", \ Alter_routine_options, "CALLED ON NULL INPUT", "RETURNS NULL ON NULL INPUT", \ "STRICT", "SUPPORT" +/* COPY options shared between FROM and TO */ +#define Copy_common_options \ +"DELIMITER", "ENCODING", "ESCAPE", "FORMAT", "HEADER", "NULL", "QUOTE" + +/* COPY FROM options */ +#define Copy_from_options \ +Copy_common_options, "DEFAULT", "FORCE_NOT_NULL", "FORCE_NULL", "FREEZE", \ +"LOG_VERBOSITY", "ON_ERROR", "REJECT_LIMIT" + +/* COPY TO options */ +#define Copy_to_options \ +Copy_common_options, "FORCE_QUOTE", "FORCE_ARRAY" + /* * These object types were introduced later than our support cutoff of * server version 9.2. We use the VersionedQuery infrastructure so that @@ -1223,10 +1275,11 @@ static const char *const sql_commands[] = { "DELETE FROM", "DISCARD", "DO", "DROP", "END", "EXECUTE", "EXPLAIN", "FETCH", "GRANT", "IMPORT FOREIGN SCHEMA", "INSERT INTO", "LISTEN", "LOAD", "LOCK", "MERGE INTO", "MOVE", "NOTIFY", "PREPARE", - "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", + "REASSIGN", "REFRESH MATERIALIZED VIEW", "REINDEX", "RELEASE", "REPACK", "RESET", "REVOKE", "ROLLBACK", "SAVEPOINT", "SECURITY LABEL", "SELECT", "SET", "SHOW", "START", - "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", "WITH", + "TABLE", "TRUNCATE", "UNLISTEN", "UPDATE", "VACUUM", "VALUES", + "WAIT FOR", "WITH", NULL }; @@ -1243,7 +1296,7 @@ typedef struct const VersionedQuery *vquery; /* versioned query, or NULL */ const SchemaQuery *squery; /* schema query, or NULL */ const char *const *keywords; /* keywords to be offered as well */ - const bits32 flags; /* visibility flags, see below */ + const uint32 flags; /* visibility flags, see below */ } pgsql_thing_t; #define THING_NO_CREATE (1 << 0) /* should not show up after CREATE */ @@ -1291,6 +1344,7 @@ static const pgsql_thing_t words_after_create[] = { {"PARSER", NULL, NULL, &Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW}, {"POLICY", NULL, NULL, NULL}, {"PROCEDURE", NULL, NULL, Query_for_list_of_procedures}, + {"PROPERTY GRAPH", NULL, NULL, &Query_for_list_of_propgraphs}, {"PUBLICATION", NULL, Query_for_list_of_publications}, {"ROLE", Query_for_list_of_roles}, {"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE}, @@ -1378,6 +1432,7 @@ static const char *const table_storage_parameters[] = { "autovacuum_multixact_freeze_max_age", "autovacuum_multixact_freeze_min_age", "autovacuum_multixact_freeze_table_age", + "autovacuum_parallel_workers", "autovacuum_vacuum_cost_delay", "autovacuum_vacuum_cost_limit", "autovacuum_vacuum_insert_scale_factor", @@ -1387,6 +1442,7 @@ static const char *const table_storage_parameters[] = { "autovacuum_vacuum_threshold", "fillfactor", "log_autovacuum_min_duration", + "log_autoanalyze_min_duration", "parallel_workers", "toast.autovacuum_enabled", "toast.autovacuum_freeze_max_age", @@ -1450,6 +1506,7 @@ static void append_variable_names(char ***varnames, int *nvars, static char **complete_from_variables(const char *text, const char *prefix, const char *suffix, bool need_value); static char *complete_from_files(const char *text, int state); +static char *_complete_from_files(const char *text, int state); static char *pg_strdup_keyword_case(const char *s, const char *ref); static char *escape_string(const char *text); @@ -1875,7 +1932,7 @@ psql_completion(const char *text, int start, int end) static const char *const backslash_commands[] = { "\\a", "\\bind", "\\bind_named", - "\\connect", "\\conninfo", "\\C", "\\cd", "\\close", "\\copy", + "\\connect", "\\conninfo", "\\C", "\\cd", "\\close_prepared", "\\copy", "\\copyright", "\\crosstabview", "\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp", "\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD", @@ -1894,11 +1951,11 @@ psql_completion(const char *text, int start, int end) "\\out", "\\parse", "\\password", "\\print", "\\prompt", "\\pset", "\\qecho", "\\quit", - "\\reset", + "\\reset", "\\restrict", "\\s", "\\sendpipeline", "\\set", "\\setenv", "\\sf", "\\startpipeline", "\\sv", "\\syncpipeline", "\\t", "\\T", "\\timing", - "\\unset", + "\\unrestrict", "\\unset", "\\x", "\\warn", "\\watch", "\\write", "\\z", @@ -2154,11 +2211,15 @@ match_previous_words(int pattern_id, { /* only some object types can be created as part of CREATE SCHEMA */ if (HeadMatches("CREATE", "SCHEMA")) - COMPLETE_WITH("TABLE", "VIEW", "INDEX", "SEQUENCE", "TRIGGER", + COMPLETE_WITH("AGGREGATE", "COLLATION", "DOMAIN", "FUNCTION", + "INDEX", "OPERATOR", "PROCEDURE", "SEQUENCE", "TABLE", + "TEXT SEARCH CONFIGURATION", "TEXT SEARCH DICTIONARY", + "TEXT SEARCH PARSER", "TEXT SEARCH TEMPLATE", + "TRIGGER", "TYPE", "VIEW", /* for INDEX and TABLE/SEQUENCE, respectively */ "UNIQUE", "UNLOGGED"); else - matches = rl_completion_matches(text, create_command_generator); + COMPLETE_WITH_GENERATOR(create_command_generator); } /* complete with something you can create or replace */ else if (TailMatches("CREATE", "OR", "REPLACE")) @@ -2168,7 +2229,7 @@ match_previous_words(int pattern_id, /* DROP, but not DROP embedded in other commands */ /* complete with something you can drop */ else if (Matches("DROP")) - matches = rl_completion_matches(text, drop_command_generator); + COMPLETE_WITH_GENERATOR(drop_command_generator); /* ALTER */ @@ -2179,7 +2240,7 @@ match_previous_words(int pattern_id, /* ALTER something */ else if (Matches("ALTER")) - matches = rl_completion_matches(text, alter_command_generator); + COMPLETE_WITH_GENERATOR(alter_command_generator); /* ALTER TABLE,INDEX,MATERIALIZED VIEW ALL IN TABLESPACE xxx */ else if (TailMatches("ALL", "IN", "TABLESPACE", MatchAny)) COMPLETE_WITH("SET TABLESPACE", "OWNED BY"); @@ -2273,7 +2334,22 @@ match_previous_words(int pattern_id, COMPLETE_WITH("TABLES IN SCHEMA", "TABLE"); /* ALTER PUBLICATION SET */ else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET")) - COMPLETE_WITH("(", "TABLES IN SCHEMA", "TABLE"); + COMPLETE_WITH("(", "ALL SEQUENCES", "ALL TABLES", "TABLES IN SCHEMA", "TABLE"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL")) + COMPLETE_WITH("SEQUENCES", "TABLES"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL", "TABLES")) + COMPLETE_WITH("EXCEPT ( TABLE"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL", "TABLES", "EXCEPT")) + COMPLETE_WITH("( TABLE"); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL", "TABLES", "EXCEPT", "(")) + COMPLETE_WITH("TABLE"); + /* Complete "ALTER PUBLICATION FOR TABLE" with "
, ..." */ + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL", "TABLES", "EXCEPT", "(", "TABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL", "TABLES", "EXCEPT", "(", "TABLE", MatchAnyN) && ends_with(prev_wd, ',')) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "ALL", "TABLES", "EXCEPT", "(", "TABLE", MatchAnyN) && !ends_with(prev_wd, ',')) + COMPLETE_WITH(")"); else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|DROP|SET", "TABLES", "IN", "SCHEMA")) COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_schemas " AND nspname NOT LIKE E'pg\\\\_%%'", @@ -2284,11 +2360,13 @@ match_previous_words(int pattern_id, /* ALTER SUBSCRIPTION */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny)) COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO", - "RENAME TO", "REFRESH PUBLICATION", "SET", "SKIP (", - "ADD PUBLICATION", "DROP PUBLICATION"); - /* ALTER SUBSCRIPTION REFRESH PUBLICATION */ - else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "REFRESH", "PUBLICATION")) - COMPLETE_WITH("WITH ("); + "RENAME TO", "REFRESH PUBLICATION", "REFRESH SEQUENCES", + "SERVER", "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); + else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SERVER")) + COMPLETE_WITH_QUERY(Query_for_list_of_servers); + /* ALTER SUBSCRIPTION REFRESH */ + else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "REFRESH")) + COMPLETE_WITH("PUBLICATION", "SEQUENCES"); /* ALTER SUBSCRIPTION REFRESH PUBLICATION WITH ( */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "REFRESH", "PUBLICATION", "WITH", "(")) COMPLETE_WITH("copy_data"); @@ -2297,9 +2375,11 @@ match_previous_words(int pattern_id, COMPLETE_WITH("(", "PUBLICATION"); /* ALTER SUBSCRIPTION SET ( */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "SET", "(")) - COMPLETE_WITH("binary", "disable_on_error", "failover", "origin", - "password_required", "run_as_owner", "slot_name", - "streaming", "synchronous_commit", "two_phase"); + COMPLETE_WITH("binary", "disable_on_error", "failover", + "max_retention_duration", "origin", + "password_required", "retain_dead_tuples", + "run_as_owner", "slot_name", "streaming", + "synchronous_commit", "two_phase"); /* ALTER SUBSCRIPTION SKIP ( */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "SKIP", "(")) COMPLETE_WITH("lsn"); @@ -2400,10 +2480,10 @@ match_previous_words(int pattern_id, /* ALTER FOREIGN DATA WRAPPER */ else if (Matches("ALTER", "FOREIGN", "DATA", "WRAPPER", MatchAny)) - COMPLETE_WITH("HANDLER", "VALIDATOR", "NO", - "OPTIONS", "OWNER TO", "RENAME TO"); + COMPLETE_WITH("CONNECTION", "HANDLER", "NO", + "OPTIONS", "OWNER TO", "RENAME TO", "VALIDATOR"); else if (Matches("ALTER", "FOREIGN", "DATA", "WRAPPER", MatchAny, "NO")) - COMPLETE_WITH("HANDLER", "VALIDATOR"); + COMPLETE_WITH("CONNECTION", "HANDLER", "VALIDATOR"); /* ALTER FOREIGN TABLE */ else if (Matches("ALTER", "FOREIGN", "TABLE", MatchAny)) @@ -2487,15 +2567,30 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "USER|ROLE", MatchAny) && !TailMatches("USER", "MAPPING")) COMPLETE_WITH("BYPASSRLS", "CONNECTION LIMIT", "CREATEDB", "CREATEROLE", - "ENCRYPTED PASSWORD", "INHERIT", "LOGIN", "NOBYPASSRLS", + "ENCRYPTED PASSWORD", "IN", "INHERIT", "LOGIN", "NOBYPASSRLS", "NOCREATEDB", "NOCREATEROLE", "NOINHERIT", "NOLOGIN", "NOREPLICATION", "NOSUPERUSER", "PASSWORD", "RENAME TO", "REPLICATION", "RESET", "SET", "SUPERUSER", "VALID UNTIL", "WITH"); - + /* ALTER USER,ROLE IN */ + else if (Matches("ALTER", "USER|ROLE", MatchAny, "IN")) + COMPLETE_WITH("DATABASE"); + /* ALTER USER,ROLE IN DATABASE */ + else if (Matches("ALTER", "USER|ROLE", MatchAny, "IN", "DATABASE")) + COMPLETE_WITH_QUERY(Query_for_list_of_databases); + /* ALTER USER,ROLE IN DATABASE */ + else if (Matches("ALTER", "USER|ROLE", MatchAny, "IN", "DATABASE", MatchAny)) + COMPLETE_WITH("SET", "RESET"); + /* ALTER USER,ROLE IN DATABASE SET */ + else if (Matches("ALTER", "USER|ROLE", MatchAny, "IN", "DATABASE", MatchAny, "SET")) + COMPLETE_WITH_QUERY(Query_for_list_of_set_vars); + /* XXX missing support for ALTER ROLE IN DATABASE RESET */ /* ALTER USER,ROLE RESET */ else if (Matches("ALTER", "USER|ROLE", MatchAny, "RESET")) + { + set_completion_reference(prev2_wd); COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_user_vars, "ALL"); + } /* ALTER USER,ROLE WITH */ else if (Matches("ALTER", "USER|ROLE", MatchAny, "WITH")) @@ -2675,6 +2770,20 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "WITH", "CHECK")) COMPLETE_WITH("("); + /* ALTER PROPERTY GRAPH */ + else if (Matches("ALTER", "PROPERTY", "GRAPH")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs); + else if (Matches("ALTER", "PROPERTY", "GRAPH", MatchAny)) + COMPLETE_WITH("ADD", "ALTER", "DROP", "OWNER TO", "RENAME TO", "SET SCHEMA"); + else if (Matches("ALTER", "PROPERTY", "GRAPH", MatchAny, "ADD|ALTER|DROP")) + COMPLETE_WITH("VERTEX", "EDGE"); + else if (Matches("ALTER", "PROPERTY", "GRAPH", MatchAny, "ADD|DROP", "VERTEX|EDGE")) + COMPLETE_WITH("TABLES"); + else if (HeadMatches("ALTER", "PROPERTY", "GRAPH", MatchAny, "ADD") && TailMatches("EDGE")) + COMPLETE_WITH("TABLES"); + else if (Matches("ALTER", "PROPERTY", "GRAPH", MatchAny, "ALTER", "VERTEX|EDGE")) + COMPLETE_WITH("TABLE"); + /* ALTER RULE , add ON */ else if (Matches("ALTER", "RULE", MatchAny)) COMPLETE_WITH("ON"); @@ -2721,21 +2830,29 @@ match_previous_words(int pattern_id, "OWNER TO", "SET", "VALIDATE CONSTRAINT", "REPLICA IDENTITY", "ATTACH PARTITION", "DETACH PARTITION", "FORCE ROW LEVEL SECURITY", + "SPLIT PARTITION", "MERGE PARTITIONS (", "OF", "NOT OF"); /* ALTER TABLE xxx ADD */ else if (Matches("ALTER", "TABLE", MatchAny, "ADD")) { - /* make sure to keep this list and the !Matches() below in sync */ - COMPLETE_WITH("COLUMN", "CONSTRAINT", "CHECK", "UNIQUE", "PRIMARY KEY", - "EXCLUDE", "FOREIGN KEY"); + /* + * make sure to keep this list and the MatchAnyExcept() below in sync + */ + COMPLETE_WITH("COLUMN", "CONSTRAINT", "CHECK (", "NOT NULL", "UNIQUE", + "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY"); } /* ALTER TABLE xxx ADD [COLUMN] yyy */ else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "COLUMN", MatchAny) || - Matches("ALTER", "TABLE", MatchAny, "ADD", MatchAnyExcept("COLUMN|CONSTRAINT|CHECK|UNIQUE|PRIMARY|EXCLUDE|FOREIGN"))) + Matches("ALTER", "TABLE", MatchAny, "ADD", MatchAnyExcept("COLUMN|CONSTRAINT|CHECK|UNIQUE|PRIMARY|NOT|EXCLUDE|FOREIGN"))) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); /* ALTER TABLE xxx ADD CONSTRAINT yyy */ else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny)) - COMPLETE_WITH("CHECK", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY"); + COMPLETE_WITH("CHECK (", "NOT NULL", "UNIQUE", "PRIMARY KEY", "EXCLUDE", "FOREIGN KEY"); + /* ALTER TABLE xxx ADD NOT NULL */ + else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "NOT", "NULL")) + COMPLETE_WITH_ATTR(prev4_wd); + else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "CONSTRAINT", MatchAny, "NOT", "NULL")) + COMPLETE_WITH_ATTR(prev6_wd); /* ALTER TABLE xxx ADD [CONSTRAINT yyy] (PRIMARY KEY|UNIQUE) */ else if (Matches("ALTER", "TABLE", MatchAny, "ADD", "PRIMARY", "KEY") || Matches("ALTER", "TABLE", MatchAny, "ADD", "UNIQUE") || @@ -2976,10 +3093,10 @@ match_previous_words(int pattern_id, COMPLETE_WITH("FROM (", "IN (", "WITH ("); /* - * If we have ALTER TABLE DETACH PARTITION, provide a list of + * If we have ALTER TABLE DETACH|SPLIT PARTITION, provide a list of * partitions of . */ - else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION")) + else if (Matches("ALTER", "TABLE", MatchAny, "DETACH|SPLIT", "PARTITION")) { set_completion_reference(prev3_wd); COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table); @@ -2987,6 +3104,19 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "TABLE", MatchAny, "DETACH", "PARTITION", MatchAny)) COMPLETE_WITH("CONCURRENTLY", "FINALIZE"); + /* ALTER TABLE SPLIT PARTITION */ + else if (Matches("ALTER", "TABLE", MatchAny, "SPLIT", "PARTITION", MatchAny)) + COMPLETE_WITH("INTO ( PARTITION"); + + /* ALTER TABLE MERGE PARTITIONS ( */ + else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "(")) + { + set_completion_reference(prev4_wd); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_partition_of_table); + } + else if (Matches("ALTER", "TABLE", MatchAny, "MERGE", "PARTITIONS", "(*)")) + COMPLETE_WITH("INTO"); + /* ALTER TABLE OF */ else if (Matches("ALTER", "TABLE", MatchAny, "OF")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_composite_datatypes); @@ -3097,6 +3227,9 @@ match_previous_words(int pattern_id, else if (TailMatches("VERBOSE|SKIP_LOCKED")) COMPLETE_WITH("ON", "OFF"); } + else if (Matches("ANALYZE", "(*)")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_analyzables, + "ONLY"); else if (Matches("ANALYZE", MatchAnyN, "(")) /* "ANALYZE (" should be caught above, so assume we want columns */ COMPLETE_WITH_ATTR(prev2_wd); @@ -3125,6 +3258,22 @@ match_previous_words(int pattern_id, COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures); else if (Matches("CALL", MatchAny)) COMPLETE_WITH("("); +/* CHECKPOINT */ + else if (Matches("CHECKPOINT")) + COMPLETE_WITH("("); + else if (HeadMatches("CHECKPOINT", "(*") && + !HeadMatches("CHECKPOINT", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list as + * one word, so the above test is correct. + */ + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH("MODE", "FLUSH_UNLOGGED"); + else if (TailMatches("MODE")) + COMPLETE_WITH("FAST", "SPREAD"); + } /* CLOSE */ else if (Matches("CLOSE")) COMPLETE_WITH_QUERY_PLUS(Query_for_list_of_cursors, @@ -3171,7 +3320,7 @@ match_previous_words(int pattern_id, "FOREIGN DATA WRAPPER", "FOREIGN TABLE", "FUNCTION", "INDEX", "LANGUAGE", "LARGE OBJECT", "MATERIALIZED VIEW", "OPERATOR", "POLICY", - "PROCEDURE", "PROCEDURAL LANGUAGE", "PUBLICATION", "ROLE", + "PROCEDURE", "PROCEDURAL LANGUAGE", "PROPERTY GRAPH", "PUBLICATION", "ROLE", "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER", "STATISTICS", "SUBSCRIPTION", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR", @@ -3209,6 +3358,8 @@ match_previous_words(int pattern_id, } else if (Matches("COMMENT", "ON", "PROCEDURAL", "LANGUAGE")) COMPLETE_WITH_QUERY(Query_for_list_of_languages); + else if (Matches("COMMENT", "ON", "PROPERTY", "GRAPH")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs); else if (Matches("COMMENT", "ON", "RULE", MatchAny)) COMPLETE_WITH("ON"); else if (Matches("COMMENT", "ON", "RULE", MatchAny, "ON")) @@ -3255,56 +3406,97 @@ match_previous_words(int pattern_id, * backslash command). */ else if (Matches("COPY|\\copy")) - COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables, "("); + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_tables_for_copy, "("); /* Complete COPY ( with legal query commands */ else if (Matches("COPY|\\copy", "(")) COMPLETE_WITH("SELECT", "TABLE", "VALUES", "INSERT INTO", "UPDATE", "DELETE FROM", "MERGE INTO", "WITH"); /* Complete COPY */ else if (Matches("COPY|\\copy", MatchAny)) COMPLETE_WITH("FROM", "TO"); - /* Complete COPY FROM|TO with filename */ - else if (Matches("COPY", MatchAny, "FROM|TO")) - { - completion_charp = ""; - completion_force_quote = true; /* COPY requires quoted filename */ - matches = rl_completion_matches(text, complete_from_files); - } - else if (Matches("\\copy", MatchAny, "FROM|TO")) + /* Complete COPY|\copy FROM|TO with filename or STDIN/STDOUT/PROGRAM */ + else if (Matches("COPY|\\copy", MatchAny, "FROM|TO")) { - completion_charp = ""; - completion_force_quote = false; - matches = rl_completion_matches(text, complete_from_files); + if (HeadMatches("COPY")) + { + /* COPY requires quoted filename */ + if (TailMatches("FROM")) + COMPLETE_WITH_FILES_PLUS("", true, "STDIN", "PROGRAM"); + else + COMPLETE_WITH_FILES_PLUS("", true, "STDOUT", "PROGRAM"); + } + else + { + /* \copy supports pstdin and pstdout */ + if (TailMatches("FROM")) + COMPLETE_WITH_FILES_PLUS("", false, "STDIN", "PSTDIN", "PROGRAM"); + else + COMPLETE_WITH_FILES_PLUS("", false, "STDOUT", "PSTDOUT", "PROGRAM"); + } } - /* Complete COPY TO */ - else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAny)) + /* Complete COPY|\copy FROM|TO PROGRAM */ + else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM")) + COMPLETE_WITH_FILES("", HeadMatches("COPY")); /* COPY requires quoted + * filename */ + + /* Complete COPY TO [PROGRAM] */ + else if (Matches("COPY|\\copy", MatchAny, "TO", MatchAnyExcept("PROGRAM")) || + Matches("COPY|\\copy", MatchAny, "TO", "PROGRAM", MatchAny)) COMPLETE_WITH("WITH ("); - /* Complete COPY FROM */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny)) + /* Complete COPY FROM [PROGRAM] */ + else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM")) || + Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny)) COMPLETE_WITH("WITH (", "WHERE"); - /* Complete COPY FROM|TO filename WITH ( */ - else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(")) - COMPLETE_WITH("FORMAT", "FREEZE", "DELIMITER", "NULL", - "HEADER", "QUOTE", "ESCAPE", "FORCE_QUOTE", - "FORCE_NOT_NULL", "FORCE_NULL", "ENCODING", "DEFAULT", - "ON_ERROR", "LOG_VERBOSITY"); + /* Complete COPY FROM|TO [PROGRAM] filename WITH ( */ + else if (HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*") || + HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*")) + { + if (!HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", MatchAnyExcept("PROGRAM"), "WITH", "(*)") && + !HeadMatches("COPY|\\copy", MatchAny, "FROM|TO", "PROGRAM", MatchAny, "WITH", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list + * as one word, so the above tests are correct. + */ + + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + { + if (HeadMatches("COPY|\\copy", MatchAny, "FROM")) + COMPLETE_WITH(Copy_from_options); + else + COMPLETE_WITH(Copy_to_options); + } + + /* Complete COPY FROM|TO filename WITH (FORMAT */ + else if (TailMatches("FORMAT")) + COMPLETE_WITH("binary", "csv", "text", "json"); + + /* Complete COPY FROM|TO filename WITH (FREEZE */ + else if (TailMatches("FREEZE")) + COMPLETE_WITH("true", "false"); - /* Complete COPY FROM|TO filename WITH (FORMAT */ - else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "FORMAT")) - COMPLETE_WITH("binary", "csv", "text"); + /* Complete COPY FROM|TO filename WITH (HEADER */ + else if (TailMatches("HEADER")) + COMPLETE_WITH("true", "false", "MATCH"); - /* Complete COPY FROM filename WITH (ON_ERROR */ - else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "ON_ERROR")) - COMPLETE_WITH("stop", "ignore"); + /* Complete COPY FROM filename WITH (ON_ERROR */ + else if (TailMatches("ON_ERROR")) + COMPLETE_WITH("stop", "ignore", "set_null"); - /* Complete COPY FROM filename WITH (LOG_VERBOSITY */ - else if (Matches("COPY|\\copy", MatchAny, "FROM|TO", MatchAny, "WITH", "(", "LOG_VERBOSITY")) - COMPLETE_WITH("silent", "default", "verbose"); + /* Complete COPY FROM filename WITH (LOG_VERBOSITY */ + else if (TailMatches("LOG_VERBOSITY")) + COMPLETE_WITH("silent", "default", "verbose"); + } - /* Complete COPY FROM WITH () */ - else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAny, "WITH", MatchAny)) + /* A completed parenthesized option list should be caught below */ + } + + /* Complete COPY FROM [PROGRAM] WITH () */ + else if (Matches("COPY|\\copy", MatchAny, "FROM", MatchAnyExcept("PROGRAM"), "WITH", MatchAny) || + Matches("COPY|\\copy", MatchAny, "FROM", "PROGRAM", MatchAny, "WITH", MatchAny)) COMPLETE_WITH("WHERE"); /* CREATE ACCESS METHOD */ @@ -3347,15 +3539,15 @@ match_previous_words(int pattern_id, else if (Matches("CREATE", "DATABASE", MatchAny, "STRATEGY")) COMPLETE_WITH("WAL_LOG", "FILE_COPY"); - /* CREATE DOMAIN */ - else if (Matches("CREATE", "DOMAIN", MatchAny)) + /* CREATE DOMAIN --- is allowed inside CREATE SCHEMA, so use TailMatches */ + else if (TailMatches("CREATE", "DOMAIN", MatchAny)) COMPLETE_WITH("AS"); - else if (Matches("CREATE", "DOMAIN", MatchAny, "AS")) + else if (TailMatches("CREATE", "DOMAIN", MatchAny, "AS")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_datatypes); - else if (Matches("CREATE", "DOMAIN", MatchAny, "AS", MatchAny)) + else if (TailMatches("CREATE", "DOMAIN", MatchAny, "AS", MatchAny)) COMPLETE_WITH("COLLATE", "DEFAULT", "CONSTRAINT", "NOT NULL", "NULL", "CHECK ("); - else if (Matches("CREATE", "DOMAIN", MatchAny, "COLLATE")) + else if (TailMatches("CREATE", "DOMAIN", MatchAny, "COLLATE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_collations); /* CREATE EXTENSION */ @@ -3378,7 +3570,7 @@ match_previous_words(int pattern_id, /* CREATE FOREIGN DATA WRAPPER */ else if (Matches("CREATE", "FOREIGN", "DATA", "WRAPPER", MatchAny)) - COMPLETE_WITH("HANDLER", "VALIDATOR", "OPTIONS"); + COMPLETE_WITH("CONNECTION", "HANDLER", "OPTIONS", "VALIDATOR"); /* CREATE FOREIGN TABLE */ else if (Matches("CREATE", "FOREIGN", "TABLE", MatchAny)) @@ -3527,16 +3719,45 @@ match_previous_words(int pattern_id, else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "AS", MatchAny, "USING")) COMPLETE_WITH("("); +/* CREATE PROPERTY GRAPH */ + else if (Matches("CREATE", "PROPERTY")) + COMPLETE_WITH("GRAPH"); + else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny)) + COMPLETE_WITH("VERTEX"); + else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE")) + COMPLETE_WITH("TABLES"); + else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES")) + COMPLETE_WITH("("); + else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES", "(")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES", "(*)")) + COMPLETE_WITH("EDGE"); + else if (HeadMatches("CREATE", "PROPERTY", "GRAPH") && TailMatches("EDGE|RELATIONSHIP")) + COMPLETE_WITH("TABLES"); + else if (HeadMatches("CREATE", "PROPERTY", "GRAPH") && TailMatches("EDGE|RELATIONSHIP", "TABLES")) + COMPLETE_WITH("("); + else if (HeadMatches("CREATE", "PROPERTY", "GRAPH") && TailMatches("EDGE|RELATIONSHIP", "TABLES", "(")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); /* CREATE PUBLICATION */ else if (Matches("CREATE", "PUBLICATION", MatchAny)) - COMPLETE_WITH("FOR TABLE", "FOR ALL TABLES", "FOR TABLES IN SCHEMA", "WITH ("); + COMPLETE_WITH("FOR TABLE", "FOR TABLES IN SCHEMA", "FOR ALL TABLES", "FOR ALL SEQUENCES", "WITH ("); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR")) - COMPLETE_WITH("TABLE", "ALL TABLES", "TABLES IN SCHEMA"); + COMPLETE_WITH("TABLE", "TABLES IN SCHEMA", "ALL TABLES", "ALL SEQUENCES"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL")) - COMPLETE_WITH("TABLES"); + COMPLETE_WITH("TABLES", "SEQUENCES"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES")) - COMPLETE_WITH("WITH ("); + COMPLETE_WITH("EXCEPT ( TABLE", "WITH ("); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT")) + COMPLETE_WITH("( TABLE"); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "(")) + COMPLETE_WITH("TABLE"); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "(", "TABLE")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "(", "TABLE", MatchAnyN) && ends_with(prev_wd, ',')) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); + else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "ALL", "TABLES", "EXCEPT", "(", "TABLE", MatchAnyN) && !ends_with(prev_wd, ',')) + COMPLETE_WITH(")"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLES")) COMPLETE_WITH("IN SCHEMA"); else if (Matches("CREATE", "PUBLICATION", MatchAny, "FOR", "TABLE", MatchAny) && !ends_with(prev_wd, ',')) @@ -3664,9 +3885,10 @@ match_previous_words(int pattern_id, TailMatches("CREATE", "TEMP|TEMPORARY|UNLOGGED", "TABLE", MatchAny, "(*)", "AS")) COMPLETE_WITH("EXECUTE", "SELECT", "TABLE", "VALUES", "WITH"); /* Complete CREATE TABLE name (...) with supported options */ - else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)") || - TailMatches("CREATE", "UNLOGGED", "TABLE", MatchAny, "(*)")) + else if (TailMatches("CREATE", "TABLE", MatchAny, "(*)")) COMPLETE_WITH("AS", "INHERITS (", "PARTITION BY", "USING", "TABLESPACE", "WITH ("); + else if (TailMatches("CREATE", "UNLOGGED", "TABLE", MatchAny, "(*)")) + COMPLETE_WITH("AS", "INHERITS (", "USING", "TABLESPACE", "WITH ("); else if (TailMatches("CREATE", "TEMP|TEMPORARY", "TABLE", MatchAny, "(*)")) COMPLETE_WITH("AS", "INHERITS (", "ON COMMIT", "PARTITION BY", "USING", "TABLESPACE", "WITH ("); @@ -3689,10 +3911,10 @@ match_previous_words(int pattern_id, else if (Matches("CREATE", "TABLESPACE", MatchAny, "OWNER", MatchAny)) COMPLETE_WITH("LOCATION"); -/* CREATE TEXT SEARCH */ - else if (Matches("CREATE", "TEXT", "SEARCH")) +/* CREATE TEXT SEARCH --- is allowed inside CREATE SCHEMA, so use TailMatches */ + else if (TailMatches("CREATE", "TEXT", "SEARCH")) COMPLETE_WITH("CONFIGURATION", "DICTIONARY", "PARSER", "TEMPLATE"); - else if (Matches("CREATE", "TEXT", "SEARCH", "CONFIGURATION|DICTIONARY|PARSER|TEMPLATE", MatchAny)) + else if (TailMatches("CREATE", "TEXT", "SEARCH", "CONFIGURATION|DICTIONARY|PARSER|TEMPLATE", MatchAny)) COMPLETE_WITH("("); /* CREATE TRANSFORM */ @@ -3714,9 +3936,18 @@ match_previous_words(int pattern_id, /* CREATE SUBSCRIPTION */ else if (Matches("CREATE", "SUBSCRIPTION", MatchAny)) - COMPLETE_WITH("CONNECTION"); + COMPLETE_WITH("CONNECTION", "SERVER"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER")) + COMPLETE_WITH_QUERY(Query_for_list_of_servers); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", MatchAny)) + COMPLETE_WITH("PUBLICATION"); else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny)) COMPLETE_WITH("PUBLICATION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", + MatchAny, "PUBLICATION")) + { + /* complete with nothing here as this refers to remote publications */ + } else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny, "PUBLICATION")) { @@ -3727,9 +3958,11 @@ match_previous_words(int pattern_id, /* Complete "CREATE SUBSCRIPTION ... WITH ( " */ else if (Matches("CREATE", "SUBSCRIPTION", MatchAnyN, "WITH", "(")) COMPLETE_WITH("binary", "connect", "copy_data", "create_slot", - "disable_on_error", "enabled", "failover", "origin", - "password_required", "run_as_owner", "slot_name", - "streaming", "synchronous_commit", "two_phase"); + "disable_on_error", "enabled", "failover", + "max_retention_duration", "origin", + "password_required", "retain_dead_tuples", + "run_as_owner", "slot_name", "streaming", + "synchronous_commit", "two_phase"); /* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */ @@ -4130,7 +4363,9 @@ match_previous_words(int pattern_id, /* Complete DELETE FROM
*/ else if (TailMatches("DELETE", "FROM", MatchAny)) COMPLETE_WITH("USING", "WHERE"); - /* XXX: implement tab completion for DELETE ... USING */ + /* Complete DELETE FROM
USING with relations supporting SELECT */ + else if (TailMatches("DELETE", "FROM", MatchAny, "USING")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_selectables); /* DISCARD */ else if (Matches("DISCARD")) @@ -4236,6 +4471,12 @@ match_previous_words(int pattern_id, else if (Matches("DROP", "POLICY", MatchAny, "ON", MatchAny)) COMPLETE_WITH("CASCADE", "RESTRICT"); + /* DROP PROPERTY GRAPH */ + else if (Matches("DROP", "PROPERTY")) + COMPLETE_WITH("GRAPH"); + else if (Matches("DROP", "PROPERTY", "GRAPH")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs); + /* DROP RULE */ else if (Matches("DROP", "RULE", MatchAny)) COMPLETE_WITH("ON"); @@ -4480,6 +4721,7 @@ match_previous_words(int pattern_id, "LARGE OBJECT", "PARAMETER", "PROCEDURE", + "PROPERTY GRAPH", "ROUTINE", "SCHEMA", "SEQUENCE", @@ -4494,13 +4736,10 @@ match_previous_words(int pattern_id, "ROUTINES IN SCHEMA", "SEQUENCES IN SCHEMA", "TABLES IN SCHEMA"); - else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN") || - TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN")) - COMPLETE_WITH("DATA WRAPPER", "SERVER"); /* * Complete "GRANT/REVOKE * ON DATABASE/DOMAIN/..." with a list of - * appropriate objects. + * appropriate objects or keywords. * * Complete "GRANT/REVOKE * ON *" with "TO/FROM". */ @@ -4513,8 +4752,17 @@ match_previous_words(int pattern_id, COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_domains); else if (TailMatches("FUNCTION")) COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_functions); + else if (TailMatches("FOREIGN")) + COMPLETE_WITH("DATA WRAPPER", "SERVER"); else if (TailMatches("LANGUAGE")) COMPLETE_WITH_QUERY(Query_for_list_of_languages); + else if (TailMatches("LARGE")) + { + if (HeadMatches("ALTER", "DEFAULT", "PRIVILEGES")) + COMPLETE_WITH("OBJECTS"); + else + COMPLETE_WITH("OBJECT"); + } else if (TailMatches("PROCEDURE")) COMPLETE_WITH_VERSIONED_SCHEMA_QUERY(Query_for_list_of_procedures); else if (TailMatches("ROUTINE")) @@ -4573,10 +4821,14 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "DEFAULT", "PRIVILEGES", MatchAnyN, "TO", MatchAny)) COMPLETE_WITH("WITH GRANT OPTION"); /* Complete "GRANT/REVOKE ... ON * *" with TO/FROM */ - else if (Matches("GRANT", MatchAnyN, "ON", MatchAny, MatchAny)) - COMPLETE_WITH("TO"); - else if (Matches("REVOKE", MatchAnyN, "ON", MatchAny, MatchAny)) - COMPLETE_WITH("FROM"); + else if (Matches("GRANT|REVOKE", MatchAnyN, "ON", MatchAny, MatchAny) && + !TailMatches("FOREIGN", "SERVER") && !TailMatches("LARGE", "OBJECT")) + { + if (Matches("GRANT", MatchAnyN, "ON", MatchAny, MatchAny)) + COMPLETE_WITH("TO"); + else + COMPLETE_WITH("FROM"); + } /* Complete "GRANT/REVOKE * ON ALL * IN SCHEMA *" with TO/FROM */ else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "ALL", MatchAny, "IN", "SCHEMA", MatchAny) || @@ -4608,6 +4860,34 @@ match_previous_words(int pattern_id, COMPLETE_WITH("FROM"); } + /* Complete "GRANT/REVOKE * ON LARGE OBJECT *" with TO/FROM */ + else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "LARGE", "OBJECT", MatchAny) || + TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "LARGE", "OBJECT", MatchAny)) + { + if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny)) + COMPLETE_WITH("TO"); + else + COMPLETE_WITH("FROM"); + } + + /* Complete "GRANT/REVOKE * ON LARGE OBJECTS" with TO/FROM */ + else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "LARGE", "OBJECTS") || + TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "LARGE", "OBJECTS")) + { + if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny)) + COMPLETE_WITH("TO"); + else + COMPLETE_WITH("FROM"); + } + +/* GRAPH_TABLE */ + else if (TailMatches("GRAPH_TABLE")) + COMPLETE_WITH("("); + else if (TailMatches("GRAPH_TABLE", "(")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs); + else if (TailMatches("GRAPH_TABLE", "(", MatchAny)) + COMPLETE_WITH("MATCH"); + /* GROUP BY */ else if (TailMatches("FROM", MatchAny, "GROUP")) COMPLETE_WITH("BY"); @@ -4620,7 +4900,7 @@ match_previous_words(int pattern_id, else if (Matches("IMPORT", "FOREIGN", "SCHEMA", MatchAny)) COMPLETE_WITH("EXCEPT (", "FROM SERVER", "LIMIT TO ("); else if (TailMatches("LIMIT", "TO", "(*)") || - TailMatches("EXCEPT", "(*)")) + Matches("IMPORT", "FOREIGN", "SCHEMA", MatchAny, "EXCEPT", "(*)")) COMPLETE_WITH("FROM SERVER"); else if (TailMatches("FROM", "SERVER", MatchAny)) COMPLETE_WITH("INTO"); @@ -4831,7 +5111,8 @@ match_previous_words(int pattern_id, /* PREPARE xx AS */ else if (Matches("PREPARE", MatchAny, "AS")) - COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM"); + COMPLETE_WITH("SELECT", "UPDATE", "INSERT INTO", "DELETE FROM", + "MERGE INTO", "VALUES", "WITH", "TABLE"); /* * PREPARE TRANSACTION is missing on purpose. It's intended for transaction @@ -4919,6 +5200,47 @@ match_previous_words(int pattern_id, COMPLETE_WITH_QUERY(Query_for_list_of_tablespaces); } +/* REPACK */ + else if (Matches("REPACK")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables, + "(", "USING INDEX"); + else if (Matches("REPACK", "(*)")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_clusterables, + "USING INDEX"); + else if (Matches("REPACK", MatchAnyExcept("("))) + COMPLETE_WITH("USING INDEX"); + else if (Matches("REPACK", "(*)", MatchAnyExcept("("))) + COMPLETE_WITH("USING INDEX"); + else if (Matches("REPACK", MatchAny, "USING", "INDEX") || + Matches("REPACK", "(*)", MatchAny, "USING", "INDEX")) + { + set_completion_reference(prev3_wd); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table); + } + + /* + * Complete ... [ (*) ] USING INDEX, with a list of indexes for + * . + */ + else if (TailMatches(MatchAny, "USING", "INDEX")) + { + set_completion_reference(prev3_wd); + COMPLETE_WITH_SCHEMA_QUERY(Query_for_index_of_table); + } + else if (HeadMatches("REPACK", "(*") && + !HeadMatches("REPACK", "(*)")) + { + /* + * This fires if we're in an unfinished parenthesized option list. + * get_previous_words treats a completed parenthesized option list as + * one word, so the above test is correct. + */ + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH("ANALYZE", "CONCURRENTLY", "VERBOSE"); + else if (TailMatches("ANALYZE", "CONCURRENTLY", "VERBOSE")) + COMPLETE_WITH("ON", "OFF"); + } + /* SECURITY LABEL */ else if (Matches("SECURITY")) COMPLETE_WITH("LABEL"); @@ -4931,8 +5253,10 @@ match_previous_words(int pattern_id, COMPLETE_WITH("TABLE", "COLUMN", "AGGREGATE", "DATABASE", "DOMAIN", "EVENT TRIGGER", "FOREIGN TABLE", "FUNCTION", "LARGE OBJECT", "MATERIALIZED VIEW", "LANGUAGE", - "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA", + "PROPERTY GRAPH", "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA", "SEQUENCE", "SUBSCRIPTION", "TABLESPACE", "TYPE", "VIEW"); + else if (Matches("SECURITY", "LABEL", "ON", "PROPERTY", "GRAPH")) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs); else if (Matches("SECURITY", "LABEL", "ON", MatchAny, MatchAny)) COMPLETE_WITH("IS"); @@ -4943,7 +5267,7 @@ match_previous_words(int pattern_id, /* Complete with a variable name */ else if (TailMatches("SET|RESET") && !TailMatches("UPDATE", MatchAny, "SET") && - !TailMatches("ALTER", "DATABASE", MatchAny, "RESET")) + !TailMatches("ALTER", "DATABASE|USER|ROLE", MatchAny, "RESET")) COMPLETE_WITH_QUERY_VERBATIM_PLUS(Query_for_list_of_set_vars, "CONSTRAINTS", "TRANSACTION", @@ -5148,24 +5472,6 @@ match_previous_words(int pattern_id, "VERBOSE", "ANALYZE", "ONLY"); - else if (Matches("VACUUM", "FULL")) - COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, - "FREEZE", - "VERBOSE", - "ANALYZE", - "ONLY"); - else if (Matches("VACUUM", MatchAnyN, "FREEZE")) - COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, - "VERBOSE", - "ANALYZE", - "ONLY"); - else if (Matches("VACUUM", MatchAnyN, "VERBOSE")) - COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, - "ANALYZE", - "ONLY"); - else if (Matches("VACUUM", MatchAnyN, "ANALYZE")) - COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, - "ONLY"); else if (HeadMatches("VACUUM", "(*") && !HeadMatches("VACUUM", "(*)")) { @@ -5185,12 +5491,73 @@ match_previous_words(int pattern_id, else if (TailMatches("INDEX_CLEANUP")) COMPLETE_WITH("AUTO", "ON", "OFF"); } + else if (Matches("VACUUM", "(*)")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, + "ONLY"); + else if (Matches("VACUUM", "FULL")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, + "FREEZE", + "VERBOSE", + "ANALYZE", + "ONLY"); + else if (Matches("VACUUM", MatchAnyN, "FREEZE")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, + "VERBOSE", + "ANALYZE", + "ONLY"); + else if (Matches("VACUUM", MatchAnyN, "VERBOSE")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, + "ANALYZE", + "ONLY"); + else if (Matches("VACUUM", MatchAnyN, "ANALYZE")) + COMPLETE_WITH_SCHEMA_QUERY_PLUS(Query_for_list_of_vacuumables, + "ONLY"); else if (Matches("VACUUM", MatchAnyN, "(")) /* "VACUUM (" should be caught above, so assume we want columns */ COMPLETE_WITH_ATTR(prev2_wd); else if (HeadMatches("VACUUM")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_vacuumables); +/* + * WAIT FOR LSN '' [ WITH ( option [, ...] ) ] + * where option can be: + * MODE '' + * TIMEOUT '' + * NO_THROW + * and mode can be: + * standby_replay | standby_write | standby_flush | primary_flush + */ + else if (Matches("WAIT")) + COMPLETE_WITH("FOR"); + else if (Matches("WAIT", "FOR")) + COMPLETE_WITH("LSN"); + else if (Matches("WAIT", "FOR", "LSN")) + /* No completion for LSN value - user must provide manually */ + ; + else if (Matches("WAIT", "FOR", "LSN", MatchAny)) + COMPLETE_WITH("WITH"); + else if (Matches("WAIT", "FOR", "LSN", MatchAny, "WITH")) + COMPLETE_WITH("("); + + /* + * Handle parenthesized option list. This fires when we're in an + * unfinished parenthesized option list. get_previous_words treats a + * completed parenthesized option list as one word, so the above test is + * correct. + * + * 'mode' takes a string value (one of the listed above), 'timeout' takes + * a string value, and 'no_throw' takes no value. We do not offer + * completions for the *values* of 'timeout' or 'no_throw'. + */ + else if (HeadMatches("WAIT", "FOR", "LSN", MatchAny, "WITH", "(*") && + !HeadMatches("WAIT", "FOR", "LSN", MatchAny, "WITH", "(*)")) + { + if (ends_with(prev_wd, '(') || ends_with(prev_wd, ',')) + COMPLETE_WITH("mode", "timeout", "no_throw"); + else if (TailMatches("mode")) + COMPLETE_WITH("'standby_replay'", "'standby_write'", "'standby_flush'", "'primary_flush'"); + } + /* WITH [RECURSIVE] */ /* @@ -5347,9 +5714,9 @@ match_previous_words(int pattern_id, else if (TailMatchesCS("\\h|\\help", MatchAny)) { if (TailMatches("DROP")) - matches = rl_completion_matches(text, drop_command_generator); + COMPLETE_WITH_GENERATOR(drop_command_generator); else if (TailMatches("ALTER")) - matches = rl_completion_matches(text, alter_command_generator); + COMPLETE_WITH_GENERATOR(alter_command_generator); /* * CREATE is recognized by tail match elsewhere, so doesn't need to be @@ -5370,6 +5737,8 @@ match_previous_words(int pattern_id, COMPLETE_WITH("OBJECT"); else if (TailMatches("CREATE|ALTER|DROP", "MATERIALIZED")) COMPLETE_WITH("VIEW"); + else if (TailMatches("CREATE|ALTER|DROP", "PROPERTY")) + COMPLETE_WITH("GRAPH"); else if (TailMatches("CREATE|ALTER|DROP", "TEXT")) COMPLETE_WITH("SEARCH"); else if (TailMatches("CREATE|ALTER|DROP", "USER")) @@ -5389,7 +5758,8 @@ match_previous_words(int pattern_id, else if (TailMatchesCS("\\password")) COMPLETE_WITH_QUERY(Query_for_list_of_roles); else if (TailMatchesCS("\\pset")) - COMPLETE_WITH_CS("border", "columns", "csv_fieldsep", "expanded", + COMPLETE_WITH_CS("border", "columns", "csv_fieldsep", + "display_false", "display_true", "expanded", "fieldsep", "fieldsep_zero", "footer", "format", "linestyle", "null", "numericlocale", "pager", "pager_min_lines", @@ -5448,12 +5818,9 @@ match_previous_words(int pattern_id, COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_views); else if (TailMatchesCS("\\cd|\\e|\\edit|\\g|\\gx|\\i|\\include|" "\\ir|\\include_relative|\\o|\\out|" - "\\s|\\w|\\write|\\lo_import")) - { - completion_charp = "\\"; - completion_force_quote = false; - matches = rl_completion_matches(text, complete_from_files); - } + "\\s|\\w|\\write|\\lo_import") || + TailMatchesCS("\\lo_export", MatchAny)) + COMPLETE_WITH_FILES("\\", false); /* gen_tabcomplete.pl ends special processing here */ /* END GEN_TABCOMPLETE */ @@ -5481,7 +5848,7 @@ match_previous_words(int pattern_id, * Entries that have 'excluded' flags are not returned. */ static char * -create_or_drop_command_generator(const char *text, int state, bits32 excluded) +create_or_drop_command_generator(const char *text, int state, uint32 excluded) { static int list_index, string_length; @@ -6116,8 +6483,7 @@ append_variable_names(char ***varnames, int *nvars, if (*nvars >= *maxvars) { *maxvars *= 2; - *varnames = (char **) pg_realloc(*varnames, - ((*maxvars) + 1) * sizeof(char *)); + *varnames = pg_realloc_array(*varnames, char *, (*maxvars) + 1); } (*varnames)[(*nvars)++] = psprintf("%s%s%s", prefix, varname, suffix); @@ -6142,7 +6508,7 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix int i; struct _variable *ptr; - varnames = (char **) pg_malloc((maxvars + 1) * sizeof(char *)); + varnames = pg_malloc_array(char *, maxvars + 1); for (ptr = pset.vars->next; ptr; ptr = ptr->next) { @@ -6163,6 +6529,59 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix } +/* + * This function returns in order one of a fixed, NULL pointer terminated list + * of string that matches file names or optionally specified list of keywords. + * + * If completion_charpp is set to a null-terminated array of literal keywords, + * those keywords are added to the completion results alongside filenames if + * they case-insensitively match the current input. + */ +static char * +complete_from_files(const char *text, int state) +{ + static int list_index; + static bool files_done; + const char *item; + + /* Initialization */ + if (state == 0) + { + list_index = 0; + files_done = false; + } + + if (!files_done) + { + char *result = _complete_from_files(text, state); + + /* Return a filename that matches */ + if (result) + return result; + + /* There are no more matching files */ + files_done = true; + } + + if (!completion_charpp) + return NULL; + + /* + * Check for hard-wired keywords. These will only be returned if they + * match the input-so-far, ignoring case. + */ + while ((item = completion_charpp[list_index++])) + { + if (pg_strncasecmp(text, item, strlen(text)) == 0) + { + completion_force_quote = false; + return pg_strdup_keyword_case(item, text); + } + } + + return NULL; +} + /* * This function wraps rl_filename_completion_function() to strip quotes from * the input before searching for matches and to quote any matches for which @@ -6177,7 +6596,7 @@ complete_from_variables(const char *text, const char *prefix, const char *suffix * quotes around the result. (The SQL COPY command requires that.) */ static char * -complete_from_files(const char *text, int state) +_complete_from_files(const char *text, int state) { #ifdef USE_FILENAME_QUOTING_FUNCTIONS @@ -6667,7 +7086,7 @@ get_previous_words(int point, char **buffer, int *nwords) * This is usually much more space than we need, but it's cheaper than * doing a separate malloc() for each word. */ - previous_words = (char **) pg_malloc(point * sizeof(char *)); + previous_words = pg_malloc_array(char *, point); *buffer = outptr = (char *) pg_malloc(point * 2); /* diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c index ae2d0e5ed3f47..f2a28bc9820a0 100644 --- a/src/bin/psql/variables.c +++ b/src/bin/psql/variables.c @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/bin/psql/variables.c */ @@ -54,7 +54,7 @@ CreateVariableSpace(void) { struct _variable *ptr; - ptr = pg_malloc(sizeof *ptr); + ptr = pg_malloc_object(struct _variable); ptr->name = NULL; ptr->value = NULL; ptr->substitute_hook = NULL; @@ -204,7 +204,7 @@ ParseVariableDouble(const char *value, const char *name, double *result, double if ((value == NULL) || (*value == '\0')) { if (name) - pg_log_error("invalid input syntax for \"%s\"", name); + pg_log_error("invalid input syntax for variable \"%s\"", name); return false; } @@ -215,14 +215,14 @@ ParseVariableDouble(const char *value, const char *name, double *result, double if (dblval < min) { if (name) - pg_log_error("invalid value \"%s\" for \"%s\": must be greater than %.2f", + pg_log_error("invalid value \"%s\" for variable \"%s\": must be greater than %.2f", value, name, min); return false; } else if (dblval > max) { if (name) - pg_log_error("invalid value \"%s\" for \"%s\": must be less than %.2f", + pg_log_error("invalid value \"%s\" for variable \"%s\": must be less than %.2f", value, name, max); } *result = dblval; @@ -238,13 +238,13 @@ ParseVariableDouble(const char *value, const char *name, double *result, double (dblval == 0.0 || dblval >= HUGE_VAL || dblval <= -HUGE_VAL)) { if (name) - pg_log_error("\"%s\" is out of range for \"%s\"", value, name); + pg_log_error("value \"%s\" is out of range for variable \"%s\"", value, name); return false; } else { if (name) - pg_log_error("invalid value \"%s\" for \"%s\"", value, name); + pg_log_error("invalid value \"%s\" for variable \"%s\"", value, name); return false; } } @@ -353,7 +353,7 @@ SetVariable(VariableSpace space, const char *name, const char *value) /* not present, make new entry ... unless we were asked to delete */ if (value) { - current = pg_malloc(sizeof *current); + current = pg_malloc_object(struct _variable); current->name = pg_strdup(name); current->value = pg_strdup(value); current->substitute_hook = NULL; @@ -416,7 +416,7 @@ SetVariableHooks(VariableSpace space, const char *name, } /* not present, make new entry */ - current = pg_malloc(sizeof *current); + current = pg_malloc_object(struct _variable); current->name = pg_strdup(name); current->value = NULL; current->substitute_hook = shook; diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h index df23ccb987d72..63e5f6cc7babd 100644 --- a/src/bin/psql/variables.h +++ b/src/bin/psql/variables.h @@ -1,7 +1,7 @@ /* * psql - the PostgreSQL interactive terminal * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * This implements a sort of variable repository. One could also think of it * as a cheap version of an associative array. Each variable has a string diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile index f6b4d40810b45..e6cd9ef4af57f 100644 --- a/src/bin/scripts/Makefile +++ b/src/bin/scripts/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/bin/scripts # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/bin/scripts/Makefile @@ -16,7 +16,15 @@ subdir = src/bin/scripts top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -PROGRAMS = createdb createuser dropdb dropuser clusterdb vacuumdb reindexdb pg_isready +PROGRAMS = \ + clusterdb \ + createdb \ + createuser \ + dropdb \ + dropuser \ + pg_isready \ + reindexdb \ + vacuumdb override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) @@ -28,7 +36,7 @@ createuser: createuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport dropdb: dropdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils dropuser: dropuser.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils clusterdb: clusterdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils -vacuumdb: vacuumdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils +vacuumdb: vacuumdb.o vacuuming.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils reindexdb: reindexdb.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils pg_isready: pg_isready.o common.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils @@ -50,7 +58,7 @@ uninstall: clean distclean: rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS)) - rm -f common.o $(WIN32RES) + rm -f common.o vacuuming.o $(WIN32RES) rm -rf tmp_check export with_icu diff --git a/src/bin/scripts/clusterdb.c b/src/bin/scripts/clusterdb.c index a0aa950b784e1..53bbb42c8832c 100644 --- a/src/bin/scripts/clusterdb.c +++ b/src/bin/scripts/clusterdb.c @@ -2,7 +2,7 @@ * * clusterdb * - * Portions Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/bin/scripts/clusterdb.c * diff --git a/src/bin/scripts/common.c b/src/bin/scripts/common.c index 7b8f7d4c52661..917a54386e489 100644 --- a/src/bin/scripts/common.c +++ b/src/bin/scripts/common.c @@ -4,7 +4,7 @@ * Common support routines for bin/scripts/ * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/scripts/common.c diff --git a/src/bin/scripts/common.h b/src/bin/scripts/common.h index 7fab1fb6e038f..4fc0889ba083a 100644 --- a/src/bin/scripts/common.h +++ b/src/bin/scripts/common.h @@ -2,7 +2,7 @@ * common.h * Common support routines for bin/scripts/ * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/bin/scripts/common.h */ diff --git a/src/bin/scripts/createdb.c b/src/bin/scripts/createdb.c index 7c0cf32d6a11b..53afbcb237232 100644 --- a/src/bin/scripts/createdb.c +++ b/src/bin/scripts/createdb.c @@ -2,7 +2,7 @@ * * createdb * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/scripts/createdb.c diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c index 81e6abfc46e70..b3ab607afa787 100644 --- a/src/bin/scripts/createuser.c +++ b/src/bin/scripts/createuser.c @@ -2,7 +2,7 @@ * * createuser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/scripts/createuser.c diff --git a/src/bin/scripts/dropdb.c b/src/bin/scripts/dropdb.c index 0b63081827713..d6c1b273de383 100644 --- a/src/bin/scripts/dropdb.c +++ b/src/bin/scripts/dropdb.c @@ -2,7 +2,7 @@ * * dropdb * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/scripts/dropdb.c diff --git a/src/bin/scripts/dropuser.c b/src/bin/scripts/dropuser.c index 39bb16861736f..50952b46c452f 100644 --- a/src/bin/scripts/dropuser.c +++ b/src/bin/scripts/dropuser.c @@ -2,7 +2,7 @@ * * dropuser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/scripts/dropuser.c diff --git a/src/bin/scripts/meson.build b/src/bin/scripts/meson.build index 80df7c332572a..c083ec38099c7 100644 --- a/src/bin/scripts/meson.build +++ b/src/bin/scripts/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group scripts_common = static_library('libscripts_common', files('common.c'), @@ -12,7 +12,6 @@ binaries = [ 'createuser', 'dropuser', 'clusterdb', - 'vacuumdb', 'reindexdb', 'pg_isready', ] @@ -35,6 +34,33 @@ foreach binary : binaries bin_targets += binary endforeach +vacuuming_common = static_library('libvacuuming_common', + files('common.c', 'vacuuming.c'), + dependencies: [frontend_code, libpq], + kwargs: internal_lib_args, +) + +binaries = [ + 'vacuumdb', +] +foreach binary : binaries + binary_sources = files('@0@.c'.format(binary)) + + if host_system == 'windows' + binary_sources += rc_bin_gen.process(win32ver_rc, extra_args: [ + '--NAME', binary, + '--FILEDESC', '@0@ - PostgreSQL utility'.format(binary),]) + endif + + binary = executable(binary, + binary_sources, + link_with: [vacuuming_common], + dependencies: [frontend_code, libpq], + kwargs: default_bin_args, + ) + bin_targets += binary +endforeach + tests += { 'name': 'scripts', 'sd': meson.current_source_dir(), diff --git a/src/bin/scripts/nls.mk b/src/bin/scripts/nls.mk index 4b358da018911..e49b04ce5b02c 100644 --- a/src/bin/scripts/nls.mk +++ b/src/bin/scripts/nls.mk @@ -7,6 +7,7 @@ GETTEXT_FILES = $(FRONTEND_COMMON_GETTEXT_FILES) \ dropuser.c \ clusterdb.c \ vacuumdb.c \ + vacuuming.c \ reindexdb.c \ pg_isready.c \ common.c \ diff --git a/src/bin/scripts/pg_isready.c b/src/bin/scripts/pg_isready.c index 7b4ea3f39a9c5..eb8af08c21a9f 100644 --- a/src/bin/scripts/pg_isready.c +++ b/src/bin/scripts/pg_isready.c @@ -2,7 +2,7 @@ * * pg_isready --- checks the status of the PostgreSQL server * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * src/bin/scripts/pg_isready.c * diff --git a/src/bin/scripts/po/meson.build b/src/bin/scripts/po/meson.build index e317f7ea9fbde..d4469a0f66290 100644 --- a/src/bin/scripts/po/meson.build +++ b/src/bin/scripts/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pgscripts-' + pg_version_major.to_string())] diff --git a/src/bin/scripts/reindexdb.c b/src/bin/scripts/reindexdb.c index 8ddcc5312f754..d7fb16d3c85db 100644 --- a/src/bin/scripts/reindexdb.c +++ b/src/bin/scripts/reindexdb.c @@ -2,7 +2,7 @@ * * reindexdb * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/bin/scripts/reindexdb.c * @@ -322,7 +322,7 @@ reindex_one_database(ConnParams *cparams, ReindexType type, * database itself, so build a list with a single entry. */ Assert(user_list == NULL); - process_list = pg_malloc0(sizeof(SimpleStringList)); + process_list = pg_malloc0_object(SimpleStringList); simple_string_list_append(process_list, PQdb(conn)); break; @@ -340,7 +340,7 @@ reindex_one_database(ConnParams *cparams, ReindexType type, { case REINDEX_SCHEMA: Assert(user_list != NULL); - /* fall through */ + pg_fallthrough; case REINDEX_DATABASE: @@ -713,7 +713,7 @@ get_parallel_tables_list(PGconn *conn, ReindexType type, return NULL; } - tables = pg_malloc0(sizeof(SimpleStringList)); + tables = pg_malloc0_object(SimpleStringList); /* Build qualified identifiers for each table */ for (int i = 0; i < ntups; i++) @@ -809,7 +809,7 @@ get_parallel_tabidx_list(PGconn *conn, return; } - *table_list = pg_malloc0(sizeof(SimpleOidList)); + *table_list = pg_malloc0_object(SimpleOidList); /* * Build two lists, one with table OIDs and the other with fully-qualified diff --git a/src/bin/scripts/t/010_clusterdb.pl b/src/bin/scripts/t/010_clusterdb.pl index 65a32b6c4c52e..56e6881244b3e 100644 --- a/src/bin/scripts/t/010_clusterdb.pl +++ b/src/bin/scripts/t/010_clusterdb.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/011_clusterdb_all.pl b/src/bin/scripts/t/011_clusterdb_all.pl index cf06c8c1f8e08..a1afbe6a3ef1c 100644 --- a/src/bin/scripts/t/011_clusterdb_all.pl +++ b/src/bin/scripts/t/011_clusterdb_all.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/020_createdb.pl b/src/bin/scripts/t/020_createdb.pl index a8293390edea2..a0995868363c4 100644 --- a/src/bin/scripts/t/020_createdb.pl +++ b/src/bin/scripts/t/020_createdb.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -241,6 +241,18 @@ ], 'fails for invalid locale provider'); +$node->command_fails_like( + [ 'createdb', "invalid \n dbname" ], + qr(contains a newline or carriage return character), + 'fails if database name contains a newline character in name' +); + +$node->command_fails_like( + [ 'createdb', "invalid \r dbname" ], + qr(contains a newline or carriage return character), + 'fails if database name contains a carriage return character in name' +); + # Check use of templates with shared dependencies copied from the template. my ($ret, $stdout, $stderr) = $node->psql( 'foobar2', @@ -351,9 +363,9 @@ 'create database with owner role_foobar'); ($ret, $stdout, $stderr) = $node->psql('foobar2', 'DROP OWNED BY role_foobar;', on_error_die => 1,); -ok($ret == 0, "DROP OWNED BY role_foobar"); +is($ret, 0, "DROP OWNED BY role_foobar"); ($ret, $stdout, $stderr) = $node->psql('foobar2', 'DROP DATABASE foobar8;', on_error_die => 1,); -ok($ret == 0, "DROP DATABASE foobar8"); +is($ret, 0, "DROP DATABASE foobar8"); done_testing(); diff --git a/src/bin/scripts/t/040_createuser.pl b/src/bin/scripts/t/040_createuser.pl index 54af43401bb05..0fbb91ce330ab 100644 --- a/src/bin/scripts/t/040_createuser.pl +++ b/src/bin/scripts/t/040_createuser.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/050_dropdb.pl b/src/bin/scripts/t/050_dropdb.pl index d0bf4924ce48b..f935052c6f53a 100644 --- a/src/bin/scripts/t/050_dropdb.pl +++ b/src/bin/scripts/t/050_dropdb.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/070_dropuser.pl b/src/bin/scripts/t/070_dropuser.pl index 5f100978446b8..6b2e8711d7c6d 100644 --- a/src/bin/scripts/t/070_dropuser.pl +++ b/src/bin/scripts/t/070_dropuser.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/080_pg_isready.pl b/src/bin/scripts/t/080_pg_isready.pl index f184bd77388eb..5657e92317049 100644 --- a/src/bin/scripts/t/080_pg_isready.pl +++ b/src/bin/scripts/t/080_pg_isready.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl index 1bd32d9426c96..ae7d3724464c6 100644 --- a/src/bin/scripts/t/090_reindexdb.pl +++ b/src/bin/scripts/t/090_reindexdb.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/091_reindexdb_all.pl b/src/bin/scripts/t/091_reindexdb_all.pl index 6a75946b2b9ff..55b96f155a9fc 100644 --- a/src/bin/scripts/t/091_reindexdb_all.pl +++ b/src/bin/scripts/t/091_reindexdb_all.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl index 75ac24a7a5539..cf0b853e9bfab 100644 --- a/src/bin/scripts/t/100_vacuumdb.pl +++ b/src/bin/scripts/t/100_vacuumdb.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -169,6 +169,10 @@ [ 'vacuumdb', '--schema' => '"Foo"', 'postgres' ], qr/VACUUM \(SKIP_DATABASE_STATS\) "Foo".bar/, 'vacuumdb --schema'); +$node->issues_sql_unlike( + [ 'vacuumdb', '--schema' => '"Foo"', 'postgres', '--dry-run' ], + qr/VACUUM \(SKIP_DATABASE_STATS\) "Foo".bar/, + 'vacuumdb --dry-run'); $node->issues_sql_like( [ 'vacuumdb', '--schema' => '"Foo"', '--schema' => '"Bar"', 'postgres' ], qr/VACUUM\ \(SKIP_DATABASE_STATS\)\ "Foo".bar @@ -237,64 +241,133 @@ qr/cannot vacuum all databases and a specific one at the same time/, 'cannot use option --all and a dbname as argument at the same time'); -$node->safe_psql('postgres', - 'CREATE TABLE regression_vacuumdb_test AS select generate_series(1, 10) a, generate_series(2, 11) b;'); +$node->safe_psql('postgres', q| + CREATE TABLE regression_vacuumdb_test AS select generate_series(1, 10) a, generate_series(2, 11) b; + ALTER TABLE regression_vacuumdb_test ADD COLUMN c INT GENERATED ALWAYS AS (a + b); +|); +$node->issues_sql_unlike( + [ + 'vacuumdb', '--analyze-only', '--dry-run', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], + qr/statement:\ ANALYZE/sx, + '--missing-stats-only --dry-run'); $node->issues_sql_like( - [ 'vacuumdb', '--analyze-only', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-only', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with missing stats'); $node->issues_sql_unlike( - [ 'vacuumdb', '--analyze-only', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-only', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with no missing stats'); $node->safe_psql('postgres', - 'CREATE INDEX regression_vacuumdb_test_idx ON regression_vacuumdb_test (mod(a, 2));'); + 'CREATE INDEX regression_vacuumdb_test_idx ON regression_vacuumdb_test (mod(a, 2));' +); $node->issues_sql_like( - [ 'vacuumdb', '--analyze-in-stages', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-in-stages', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with missing index expression stats'); $node->issues_sql_unlike( - [ 'vacuumdb', '--analyze-in-stages', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-in-stages', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with no missing index expression stats'); $node->safe_psql('postgres', - 'CREATE STATISTICS regression_vacuumdb_test_stat ON a, b FROM regression_vacuumdb_test;'); + 'CREATE STATISTICS regression_vacuumdb_test_stat ON a, b FROM regression_vacuumdb_test;' +); $node->issues_sql_like( - [ 'vacuumdb', '--analyze-only', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-only', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with missing extended stats'); $node->issues_sql_unlike( - [ 'vacuumdb', '--analyze-only', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-only', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with no missing extended stats'); $node->safe_psql('postgres', "CREATE TABLE regression_vacuumdb_child (a INT) INHERITS (regression_vacuumdb_test);\n" - . "INSERT INTO regression_vacuumdb_child VALUES (1, 2);\n" - . "ANALYZE regression_vacuumdb_child;\n"); + . "INSERT INTO regression_vacuumdb_child VALUES (1, 2);\n" + . "ANALYZE regression_vacuumdb_child;\n"); $node->issues_sql_like( - [ 'vacuumdb', '--analyze-in-stages', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-in-stages', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with missing inherited stats'); $node->issues_sql_unlike( - [ 'vacuumdb', '--analyze-in-stages', '--missing-stats-only', '-t', 'regression_vacuumdb_test', 'postgres' ], + [ + 'vacuumdb', '--analyze-in-stages', + '--missing-stats-only', '-t', + 'regression_vacuumdb_test', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with no missing inherited stats'); $node->safe_psql('postgres', "CREATE TABLE regression_vacuumdb_parted (a INT) PARTITION BY LIST (a);\n" - . "CREATE TABLE regression_vacuumdb_part1 PARTITION OF regression_vacuumdb_parted FOR VALUES IN (1);\n" - . "INSERT INTO regression_vacuumdb_parted VALUES (1);\n" - . "ANALYZE regression_vacuumdb_part1;\n"); + . "CREATE TABLE regression_vacuumdb_part1 PARTITION OF regression_vacuumdb_parted FOR VALUES IN (1);\n" + . "INSERT INTO regression_vacuumdb_parted VALUES (1);\n" + . "ANALYZE regression_vacuumdb_part1;\n"); $node->issues_sql_like( - [ 'vacuumdb', '--analyze-only', '--missing-stats-only', '-t', 'regression_vacuumdb_parted', 'postgres' ], + [ + 'vacuumdb', '--analyze-only', + '--missing-stats-only', '-t', + 'regression_vacuumdb_parted', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with missing partition stats'); $node->issues_sql_unlike( - [ 'vacuumdb', '--analyze-only', '--missing-stats-only', '-t', 'regression_vacuumdb_parted', 'postgres' ], + [ + 'vacuumdb', '--analyze-only', + '--missing-stats-only', '-t', + 'regression_vacuumdb_parted', 'postgres' + ], qr/statement:\ ANALYZE/sx, '--missing-stats-only with no missing partition stats'); +$node->safe_psql('postgres', + "CREATE TABLE parent_table (a INT) PARTITION BY LIST (a);\n" + . "CREATE TABLE child_table PARTITION OF parent_table FOR VALUES IN (1);\n" + . "INSERT INTO parent_table VALUES (1);\n"); +$node->issues_sql_like( + [ + 'vacuumdb', '--analyze-only', 'postgres' + ], + qr/statement: ANALYZE public.parent_table/s, + '--analyze-only updates statistics for partitioned tables'); +$node->issues_sql_unlike( + [ + 'vacuumdb', '--analyze-only', 'postgres' + ], + qr/statement:\ VACUUM/sx, + '--analyze-only does not run vacuum'); + done_testing(); diff --git a/src/bin/scripts/t/101_vacuumdb_all.pl b/src/bin/scripts/t/101_vacuumdb_all.pl index 74cb22dc3416c..c91c3328643c5 100644 --- a/src/bin/scripts/t/101_vacuumdb_all.pl +++ b/src/bin/scripts/t/101_vacuumdb_all.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/102_vacuumdb_stages.pl b/src/bin/scripts/t/102_vacuumdb_stages.pl index 984c8d06de60b..bb8ec330bd354 100644 --- a/src/bin/scripts/t/102_vacuumdb_stages.pl +++ b/src/bin/scripts/t/102_vacuumdb_stages.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/t/200_connstr.pl b/src/bin/scripts/t/200_connstr.pl index 4d61f09347fc0..9e73120fa5d98 100644 --- a/src/bin/scripts/t/200_connstr.pl +++ b/src/bin/scripts/t/200_connstr.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c index 79b1096eb08c4..ccc7f88a2917a 100644 --- a/src/bin/scripts/vacuumdb.c +++ b/src/bin/scripts/vacuumdb.c @@ -2,7 +2,7 @@ * * vacuumdb * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/bin/scripts/vacuumdb.c @@ -14,90 +14,13 @@ #include -#include "catalog/pg_class_d.h" #include "common.h" -#include "common/connect.h" #include "common/logging.h" -#include "fe_utils/cancel.h" #include "fe_utils/option_utils.h" -#include "fe_utils/parallel_slot.h" -#include "fe_utils/query_utils.h" -#include "fe_utils/simple_list.h" -#include "fe_utils/string_utils.h" - - -/* vacuum options controlled by user flags */ -typedef struct vacuumingOptions -{ - bool analyze_only; - bool verbose; - bool and_analyze; - bool full; - bool freeze; - bool disable_page_skipping; - bool skip_locked; - int min_xid_age; - int min_mxid_age; - int parallel_workers; /* >= 0 indicates user specified the - * parallel degree, otherwise -1 */ - bool no_index_cleanup; - bool force_index_cleanup; - bool do_truncate; - bool process_main; - bool process_toast; - bool skip_database_stats; - char *buffer_usage_limit; - bool missing_stats_only; -} vacuumingOptions; - -/* object filter options */ -typedef enum -{ - OBJFILTER_NONE = 0, /* no filter used */ - OBJFILTER_ALL_DBS = (1 << 0), /* -a | --all */ - OBJFILTER_DATABASE = (1 << 1), /* -d | --dbname */ - OBJFILTER_TABLE = (1 << 2), /* -t | --table */ - OBJFILTER_SCHEMA = (1 << 3), /* -n | --schema */ - OBJFILTER_SCHEMA_EXCLUDE = (1 << 4), /* -N | --exclude-schema */ -} VacObjFilter; - -static VacObjFilter objfilter = OBJFILTER_NONE; - -static SimpleStringList *retrieve_objects(PGconn *conn, - vacuumingOptions *vacopts, - SimpleStringList *objects, - bool echo); - -static void vacuum_one_database(ConnParams *cparams, - vacuumingOptions *vacopts, - int stage, - SimpleStringList *objects, - SimpleStringList **found_objs, - int concurrentCons, - const char *progname, bool echo, bool quiet); - -static void vacuum_all_databases(ConnParams *cparams, - vacuumingOptions *vacopts, - bool analyze_in_stages, - SimpleStringList *objects, - int concurrentCons, - const char *progname, bool echo, bool quiet); - -static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion, - vacuumingOptions *vacopts, const char *table); - -static void run_vacuum_command(PGconn *conn, const char *sql, bool echo, - const char *table); +#include "vacuuming.h" static void help(const char *progname); - -void check_objfilter(void); - -static char *escape_quotes(const char *src); - -/* For analyze-in-stages mode */ -#define ANALYZE_NO_STAGE -1 -#define ANALYZE_NUM_STAGES 3 +static void check_objfilter(uint32 objfilter); int @@ -136,6 +59,7 @@ main(int argc, char *argv[]) {"no-process-main", no_argument, NULL, 12}, {"buffer-usage-limit", required_argument, NULL, 13}, {"missing-stats-only", no_argument, NULL, 14}, + {"dry-run", no_argument, NULL, 15}, {NULL, 0, NULL, 0} }; @@ -144,48 +68,44 @@ main(int argc, char *argv[]) int c; const char *dbname = NULL; const char *maintenance_db = NULL; - char *host = NULL; - char *port = NULL; - char *username = NULL; - enum trivalue prompt_password = TRI_DEFAULT; ConnParams cparams; - bool echo = false; - bool quiet = false; vacuumingOptions vacopts; - bool analyze_in_stages = false; SimpleStringList objects = {NULL, NULL}; int concurrentCons = 1; - int tbl_count = 0; + unsigned int tbl_count = 0; + int ret; /* initialize options */ memset(&vacopts, 0, sizeof(vacopts)); vacopts.parallel_workers = -1; - vacopts.buffer_usage_limit = NULL; - vacopts.no_index_cleanup = false; - vacopts.force_index_cleanup = false; vacopts.do_truncate = true; vacopts.process_main = true; vacopts.process_toast = true; + /* the same for connection parameters */ + memset(&cparams, 0, sizeof(cparams)); + cparams.prompt_password = TRI_DEFAULT; + pg_logging_init(argv[0]); progname = get_progname(argv[0]); set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts")); handle_help_version_opts(argc, argv, "vacuumdb", help); - while ((c = getopt_long(argc, argv, "ad:efFh:j:n:N:p:P:qt:U:vwWzZ", long_options, &optindex)) != -1) + while ((c = getopt_long(argc, argv, "ad:efFh:j:n:N:p:P:qt:U:vwWzZ", + long_options, &optindex)) != -1) { switch (c) { case 'a': - objfilter |= OBJFILTER_ALL_DBS; + vacopts.objfilter |= OBJFILTER_ALL_DBS; break; case 'd': - objfilter |= OBJFILTER_DATABASE; + vacopts.objfilter |= OBJFILTER_DATABASE; dbname = pg_strdup(optarg); break; case 'e': - echo = true; + vacopts.echo = true; break; case 'f': vacopts.full = true; @@ -194,7 +114,7 @@ main(int argc, char *argv[]) vacopts.freeze = true; break; case 'h': - host = pg_strdup(optarg); + cparams.pghost = pg_strdup(optarg); break; case 'j': if (!option_parse_int(optarg, "-j/--jobs", 1, INT_MAX, @@ -202,15 +122,15 @@ main(int argc, char *argv[]) exit(1); break; case 'n': - objfilter |= OBJFILTER_SCHEMA; + vacopts.objfilter |= OBJFILTER_SCHEMA; simple_string_list_append(&objects, optarg); break; case 'N': - objfilter |= OBJFILTER_SCHEMA_EXCLUDE; + vacopts.objfilter |= OBJFILTER_SCHEMA_EXCLUDE; simple_string_list_append(&objects, optarg); break; case 'p': - port = pg_strdup(optarg); + cparams.pgport = pg_strdup(optarg); break; case 'P': if (!option_parse_int(optarg, "-P/--parallel", 0, INT_MAX, @@ -218,36 +138,38 @@ main(int argc, char *argv[]) exit(1); break; case 'q': - quiet = true; + vacopts.quiet = true; break; case 't': - objfilter |= OBJFILTER_TABLE; + vacopts.objfilter |= OBJFILTER_TABLE; simple_string_list_append(&objects, optarg); tbl_count++; break; case 'U': - username = pg_strdup(optarg); + cparams.pguser = pg_strdup(optarg); break; case 'v': vacopts.verbose = true; break; case 'w': - prompt_password = TRI_NO; + cparams.prompt_password = TRI_NO; break; case 'W': - prompt_password = TRI_YES; + cparams.prompt_password = TRI_YES; break; case 'z': vacopts.and_analyze = true; break; case 'Z': - vacopts.analyze_only = true; + /* if analyze-in-stages is given, don't override it */ + if (vacopts.mode != MODE_ANALYZE_IN_STAGES) + vacopts.mode = MODE_ANALYZE; break; case 2: maintenance_db = pg_strdup(optarg); break; case 3: - analyze_in_stages = vacopts.analyze_only = true; + vacopts.mode = MODE_ANALYZE_IN_STAGES; break; case 4: vacopts.disable_page_skipping = true; @@ -286,6 +208,9 @@ main(int argc, char *argv[]) case 14: vacopts.missing_stats_only = true; break; + case 15: + vacopts.dry_run = true; + break; default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -299,7 +224,7 @@ main(int argc, char *argv[]) */ if (optind < argc && dbname == NULL) { - objfilter |= OBJFILTER_DATABASE; + vacopts.objfilter |= OBJFILTER_DATABASE; dbname = argv[optind]; optind++; } @@ -316,9 +241,10 @@ main(int argc, char *argv[]) * Validate the combination of filters specified in the command-line * options. */ - check_objfilter(); + check_objfilter(vacopts.objfilter); - if (vacopts.analyze_only) + if (vacopts.mode == MODE_ANALYZE || + vacopts.mode == MODE_ANALYZE_IN_STAGES) { if (vacopts.full) pg_fatal("cannot use the \"%s\" option when performing only analyze", @@ -350,7 +276,8 @@ main(int argc, char *argv[]) /* Prohibit full and analyze_only options with parallel option */ if (vacopts.parallel_workers >= 0) { - if (vacopts.analyze_only) + if (vacopts.mode == MODE_ANALYZE || + vacopts.mode == MODE_ANALYZE_IN_STAGES) pg_fatal("cannot use the \"%s\" option when performing only analyze", "parallel"); if (vacopts.full) @@ -375,78 +302,27 @@ main(int argc, char *argv[]) * Prohibit --missing-stats-only without --analyze-only or * --analyze-in-stages. */ - if (vacopts.missing_stats_only && !vacopts.analyze_only) + if (vacopts.missing_stats_only && (vacopts.mode != MODE_ANALYZE && + vacopts.mode != MODE_ANALYZE_IN_STAGES)) pg_fatal("cannot use the \"%s\" option without \"%s\" or \"%s\"", "missing-stats-only", "analyze-only", "analyze-in-stages"); - /* fill cparams except for dbname, which is set below */ - cparams.pghost = host; - cparams.pgport = port; - cparams.pguser = username; - cparams.prompt_password = prompt_password; - cparams.override_dbname = NULL; - - setup_cancel_handler(NULL); - - /* Avoid opening extra connections. */ - if (tbl_count && (concurrentCons > tbl_count)) - concurrentCons = tbl_count; + if (vacopts.dry_run && !vacopts.quiet) + pg_log_info("Executing in dry-run mode.\n" + "No commands will be sent to the server."); - if (objfilter & OBJFILTER_ALL_DBS) - { - cparams.dbname = maintenance_db; - - vacuum_all_databases(&cparams, &vacopts, - analyze_in_stages, - &objects, - concurrentCons, - progname, echo, quiet); - } - else - { - if (dbname == NULL) - { - if (getenv("PGDATABASE")) - dbname = getenv("PGDATABASE"); - else if (getenv("PGUSER")) - dbname = getenv("PGUSER"); - else - dbname = get_user_name_or_exit(progname); - } - - cparams.dbname = dbname; - - if (analyze_in_stages) - { - int stage; - SimpleStringList *found_objs = NULL; - - for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++) - { - vacuum_one_database(&cparams, &vacopts, - stage, - &objects, - vacopts.missing_stats_only ? &found_objs : NULL, - concurrentCons, - progname, echo, quiet); - } - } - else - vacuum_one_database(&cparams, &vacopts, - ANALYZE_NO_STAGE, - &objects, NULL, - concurrentCons, - progname, echo, quiet); - } - - exit(0); + ret = vacuuming_main(&cparams, dbname, maintenance_db, &vacopts, + &objects, tbl_count, + concurrentCons, + progname); + exit(ret); } /* * Verify that the filters used at command line are compatible. */ void -check_objfilter(void) +check_objfilter(uint32 objfilter) { if ((objfilter & OBJFILTER_ALL_DBS) && (objfilter & OBJFILTER_DATABASE)) @@ -465,865 +341,6 @@ check_objfilter(void) pg_fatal("cannot vacuum all tables in schema(s) and exclude schema(s) at the same time"); } -/* - * Returns a newly malloc'd version of 'src' with escaped single quotes and - * backslashes. - */ -static char * -escape_quotes(const char *src) -{ - char *result = escape_single_quotes_ascii(src); - - if (!result) - pg_fatal("out of memory"); - return result; -} - -/* - * vacuum_one_database - * - * Process tables in the given database. - * - * There are two ways to specify the list of objects to process: - * - * 1) The "found_objs" parameter is a double pointer to a fully qualified list - * of objects to process, as returned by a previous call to - * vacuum_one_database(). - * - * a) If both "found_objs" (the double pointer) and "*found_objs" (the - * once-dereferenced double pointer) are not NULL, this list takes - * priority, and anything specified in "objects" is ignored. - * - * b) If "found_objs" (the double pointer) is not NULL but "*found_objs" - * (the once-dereferenced double pointer) _is_ NULL, the "objects" - * parameter takes priority, and the results of the catalog query - * described in (2) are stored in "found_objs". - * - * c) If "found_objs" (the double pointer) is NULL, the "objects" - * parameter again takes priority, and the results of the catalog query - * are not saved. - * - * 2) The "objects" parameter is a user-specified list of objects to process. - * When (1b) or (1c) applies, this function performs a catalog query to - * retrieve a fully qualified list of objects to process, as described - * below. - * - * a) If "objects" is not NULL, the catalog query gathers only the objects - * listed in "objects". - * - * b) If "objects" is NULL, all tables in the database are gathered. - * - * Note that this function is only concerned with running exactly one stage - * when in analyze-in-stages mode; caller must iterate on us if necessary. - * - * If concurrentCons is > 1, multiple connections are used to vacuum tables - * in parallel. - */ -static void -vacuum_one_database(ConnParams *cparams, - vacuumingOptions *vacopts, - int stage, - SimpleStringList *objects, - SimpleStringList **found_objs, - int concurrentCons, - const char *progname, bool echo, bool quiet) -{ - PQExpBufferData sql; - PGconn *conn; - SimpleStringListCell *cell; - ParallelSlotArray *sa; - int ntups = 0; - bool failed = false; - const char *initcmd; - SimpleStringList *ret = NULL; - const char *stage_commands[] = { - "SET default_statistics_target=1; SET vacuum_cost_delay=0;", - "SET default_statistics_target=10; RESET vacuum_cost_delay;", - "RESET default_statistics_target;" - }; - const char *stage_messages[] = { - gettext_noop("Generating minimal optimizer statistics (1 target)"), - gettext_noop("Generating medium optimizer statistics (10 targets)"), - gettext_noop("Generating default (full) optimizer statistics") - }; - - Assert(stage == ANALYZE_NO_STAGE || - (stage >= 0 && stage < ANALYZE_NUM_STAGES)); - - conn = connectDatabase(cparams, progname, echo, false, true); - - if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "disable-page-skipping", "9.6"); - } - - if (vacopts->no_index_cleanup && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-index-cleanup", "12"); - } - - if (vacopts->force_index_cleanup && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "force-index-cleanup", "12"); - } - - if (!vacopts->do_truncate && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-truncate", "12"); - } - - if (!vacopts->process_main && PQserverVersion(conn) < 160000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-process-main", "16"); - } - - if (!vacopts->process_toast && PQserverVersion(conn) < 140000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "no-process-toast", "14"); - } - - if (vacopts->skip_locked && PQserverVersion(conn) < 120000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "skip-locked", "12"); - } - - if (vacopts->min_xid_age != 0 && PQserverVersion(conn) < 90600) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--min-xid-age", "9.6"); - } - - if (vacopts->min_mxid_age != 0 && PQserverVersion(conn) < 90600) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--min-mxid-age", "9.6"); - } - - if (vacopts->parallel_workers >= 0 && PQserverVersion(conn) < 130000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--parallel", "13"); - } - - if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--buffer-usage-limit", "16"); - } - - if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000) - { - PQfinish(conn); - pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", - "--missing-stats-only", "15"); - } - - /* skip_database_stats is used automatically if server supports it */ - vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000); - - if (!quiet) - { - if (stage != ANALYZE_NO_STAGE) - printf(_("%s: processing database \"%s\": %s\n"), - progname, PQdb(conn), _(stage_messages[stage])); - else - printf(_("%s: vacuuming database \"%s\"\n"), - progname, PQdb(conn)); - fflush(stdout); - } - - /* - * If the caller provided the results of a previous catalog query, just - * use that. Otherwise, run the catalog query ourselves and set the - * return variable if provided. - */ - if (found_objs && *found_objs) - ret = *found_objs; - else - { - ret = retrieve_objects(conn, vacopts, objects, echo); - if (found_objs) - *found_objs = ret; - } - - /* - * Count the number of objects in the catalog query result. If there are - * none, we are done. - */ - for (cell = ret ? ret->head : NULL; cell; cell = cell->next) - ntups++; - - if (ntups == 0) - { - PQfinish(conn); - return; - } - - /* - * Ensure concurrentCons is sane. If there are more connections than - * vacuumable relations, we don't need to use them all. - */ - if (concurrentCons > ntups) - concurrentCons = ntups; - if (concurrentCons <= 0) - concurrentCons = 1; - - /* - * All slots need to be prepared to run the appropriate analyze stage, if - * caller requested that mode. We have to prepare the initial connection - * ourselves before setting up the slots. - */ - if (stage == ANALYZE_NO_STAGE) - initcmd = NULL; - else - { - initcmd = stage_commands[stage]; - executeCommand(conn, initcmd, echo); - } - - /* - * Setup the database connections. We reuse the connection we already have - * for the first slot. If not in parallel mode, the first slot in the - * array contains the connection. - */ - sa = ParallelSlotsSetup(concurrentCons, cparams, progname, echo, initcmd); - ParallelSlotsAdoptConn(sa, conn); - - initPQExpBuffer(&sql); - - cell = ret->head; - do - { - const char *tabname = cell->val; - ParallelSlot *free_slot; - - if (CancelRequested) - { - failed = true; - goto finish; - } - - free_slot = ParallelSlotsGetIdle(sa, NULL); - if (!free_slot) - { - failed = true; - goto finish; - } - - prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection), - vacopts, tabname); - - /* - * Execute the vacuum. All errors are handled in processQueryResult - * through ParallelSlotsGetIdle. - */ - ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); - run_vacuum_command(free_slot->connection, sql.data, - echo, tabname); - - cell = cell->next; - } while (cell != NULL); - - if (!ParallelSlotsWaitCompletion(sa)) - { - failed = true; - goto finish; - } - - /* If we used SKIP_DATABASE_STATS, mop up with ONLY_DATABASE_STATS */ - if (vacopts->skip_database_stats && stage == ANALYZE_NO_STAGE) - { - const char *cmd = "VACUUM (ONLY_DATABASE_STATS);"; - ParallelSlot *free_slot = ParallelSlotsGetIdle(sa, NULL); - - if (!free_slot) - { - failed = true; - goto finish; - } - - ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); - run_vacuum_command(free_slot->connection, cmd, echo, NULL); - - if (!ParallelSlotsWaitCompletion(sa)) - failed = true; - } - -finish: - ParallelSlotsTerminate(sa); - pg_free(sa); - - termPQExpBuffer(&sql); - - if (failed) - exit(1); -} - -/* - * Prepare the list of tables to process by querying the catalogs. - * - * Since we execute the constructed query with the default search_path (which - * could be unsafe), everything in this query MUST be fully qualified. - * - * First, build a WITH clause for the catalog query if any tables were - * specified, with a set of values made of relation names and their optional - * set of columns. This is used to match any provided column lists with the - * generated qualified identifiers and to filter for the tables provided via - * --table. If a listed table does not exist, the catalog query will fail. - */ -static SimpleStringList * -retrieve_objects(PGconn *conn, vacuumingOptions *vacopts, - SimpleStringList *objects, bool echo) -{ - PQExpBufferData buf; - PQExpBufferData catalog_query; - PGresult *res; - SimpleStringListCell *cell; - SimpleStringList *found_objs = palloc0(sizeof(SimpleStringList)); - bool objects_listed = false; - - initPQExpBuffer(&catalog_query); - for (cell = objects ? objects->head : NULL; cell; cell = cell->next) - { - char *just_table = NULL; - const char *just_columns = NULL; - - if (!objects_listed) - { - appendPQExpBufferStr(&catalog_query, - "WITH listed_objects (object_oid, column_list) " - "AS (\n VALUES ("); - objects_listed = true; - } - else - appendPQExpBufferStr(&catalog_query, ",\n ("); - - if (objfilter & (OBJFILTER_SCHEMA | OBJFILTER_SCHEMA_EXCLUDE)) - { - appendStringLiteralConn(&catalog_query, cell->val, conn); - appendPQExpBufferStr(&catalog_query, "::pg_catalog.regnamespace, "); - } - - if (objfilter & OBJFILTER_TABLE) - { - /* - * Split relation and column names given by the user, this is used - * to feed the CTE with values on which are performed pre-run - * validity checks as well. For now these happen only on the - * relation name. - */ - splitTableColumnsSpec(cell->val, PQclientEncoding(conn), - &just_table, &just_columns); - - appendStringLiteralConn(&catalog_query, just_table, conn); - appendPQExpBufferStr(&catalog_query, "::pg_catalog.regclass, "); - } - - if (just_columns && just_columns[0] != '\0') - appendStringLiteralConn(&catalog_query, just_columns, conn); - else - appendPQExpBufferStr(&catalog_query, "NULL"); - - appendPQExpBufferStr(&catalog_query, "::pg_catalog.text)"); - - pg_free(just_table); - } - - /* Finish formatting the CTE */ - if (objects_listed) - appendPQExpBufferStr(&catalog_query, "\n)\n"); - - appendPQExpBufferStr(&catalog_query, "SELECT c.relname, ns.nspname"); - - if (objects_listed) - appendPQExpBufferStr(&catalog_query, ", listed_objects.column_list"); - - appendPQExpBufferStr(&catalog_query, - " FROM pg_catalog.pg_class c\n" - " JOIN pg_catalog.pg_namespace ns" - " ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n" - " CROSS JOIN LATERAL (SELECT c.relkind IN (" - CppAsString2(RELKIND_PARTITIONED_TABLE) ", " - CppAsString2(RELKIND_PARTITIONED_INDEX) ")) as p (inherited)\n" - " LEFT JOIN pg_catalog.pg_class t" - " ON c.reltoastrelid OPERATOR(pg_catalog.=) t.oid\n"); - - /* - * Used to match the tables or schemas listed by the user, completing the - * JOIN clause. - */ - if (objects_listed) - { - appendPQExpBufferStr(&catalog_query, " LEFT JOIN listed_objects" - " ON listed_objects.object_oid" - " OPERATOR(pg_catalog.=) "); - - if (objfilter & OBJFILTER_TABLE) - appendPQExpBufferStr(&catalog_query, "c.oid\n"); - else - appendPQExpBufferStr(&catalog_query, "ns.oid\n"); - } - - /* - * Exclude temporary tables, beginning the WHERE clause. - */ - appendPQExpBufferStr(&catalog_query, - " WHERE c.relpersistence OPERATOR(pg_catalog.!=) " - CppAsString2(RELPERSISTENCE_TEMP) "\n"); - - /* - * Used to match the tables or schemas listed by the user, for the WHERE - * clause. - */ - if (objects_listed) - { - if (objfilter & OBJFILTER_SCHEMA_EXCLUDE) - appendPQExpBufferStr(&catalog_query, - " AND listed_objects.object_oid IS NULL\n"); - else - appendPQExpBufferStr(&catalog_query, - " AND listed_objects.object_oid IS NOT NULL\n"); - } - - /* - * If no tables were listed, filter for the relevant relation types. If - * tables were given via --table, don't bother filtering by relation type. - * Instead, let the server decide whether a given relation can be - * processed in which case the user will know about it. - */ - if ((objfilter & OBJFILTER_TABLE) == 0) - { - appendPQExpBufferStr(&catalog_query, - " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" - CppAsString2(RELKIND_RELATION) ", " - CppAsString2(RELKIND_MATVIEW) "])\n"); - } - - /* - * For --min-xid-age and --min-mxid-age, the age of the relation is the - * greatest of the ages of the main relation and its associated TOAST - * table. The commands generated by vacuumdb will also process the TOAST - * table for the relation if necessary, so it does not need to be - * considered separately. - */ - if (vacopts->min_xid_age != 0) - { - appendPQExpBuffer(&catalog_query, - " AND GREATEST(pg_catalog.age(c.relfrozenxid)," - " pg_catalog.age(t.relfrozenxid)) " - " OPERATOR(pg_catalog.>=) '%d'::pg_catalog.int4\n" - " AND c.relfrozenxid OPERATOR(pg_catalog.!=)" - " '0'::pg_catalog.xid\n", - vacopts->min_xid_age); - } - - if (vacopts->min_mxid_age != 0) - { - appendPQExpBuffer(&catalog_query, - " AND GREATEST(pg_catalog.mxid_age(c.relminmxid)," - " pg_catalog.mxid_age(t.relminmxid)) OPERATOR(pg_catalog.>=)" - " '%d'::pg_catalog.int4\n" - " AND c.relminmxid OPERATOR(pg_catalog.!=)" - " '0'::pg_catalog.xid\n", - vacopts->min_mxid_age); - } - - if (vacopts->missing_stats_only) - { - appendPQExpBufferStr(&catalog_query, " AND (\n"); - - /* regular stats */ - appendPQExpBufferStr(&catalog_query, - " EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" - " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" - " AND NOT a.attisdropped\n" - " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" - " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); - - /* extended stats */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" - " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" - " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" - " AND d.stxdinherit OPERATOR(pg_catalog.=) p.inherited))\n"); - - /* expression indexes */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" - " JOIN pg_catalog.pg_index i" - " ON i.indexrelid OPERATOR(pg_catalog.=) a.attrelid\n" - " WHERE i.indrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND i.indkey[a.attnum OPERATOR(pg_catalog.-) 1::pg_catalog.int2]" - " OPERATOR(pg_catalog.=) 0::pg_catalog.int2\n" - " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" - " AND NOT a.attisdropped\n" - " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" - " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); - - /* inheritance and regular stats */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" - " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" - " AND NOT a.attisdropped\n" - " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND c.relhassubclass\n" - " AND NOT p.inherited\n" - " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" - " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" - " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" - " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" - " AND s.stainherit))\n"); - - /* inheritance and extended stats */ - appendPQExpBufferStr(&catalog_query, - " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" - " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" - " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" - " AND c.relhassubclass\n" - " AND NOT p.inherited\n" - " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" - " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" - " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" - " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" - " AND d.stxdinherit))\n"); - - appendPQExpBufferStr(&catalog_query, " )\n"); - } - - /* - * Execute the catalog query. We use the default search_path for this - * query for consistency with table lookups done elsewhere by the user. - */ - appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;"); - executeCommand(conn, "RESET search_path;", echo); - res = executeQuery(conn, catalog_query.data, echo); - termPQExpBuffer(&catalog_query); - PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo)); - - /* - * Build qualified identifiers for each table, including the column list - * if given. - */ - initPQExpBuffer(&buf); - for (int i = 0; i < PQntuples(res); i++) - { - appendPQExpBufferStr(&buf, - fmtQualifiedIdEnc(PQgetvalue(res, i, 1), - PQgetvalue(res, i, 0), - PQclientEncoding(conn))); - - if (objects_listed && !PQgetisnull(res, i, 2)) - appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2)); - - simple_string_list_append(found_objs, buf.data); - resetPQExpBuffer(&buf); - } - termPQExpBuffer(&buf); - PQclear(res); - - return found_objs; -} - -/* - * Vacuum/analyze all connectable databases. - * - * In analyze-in-stages mode, we process all databases in one stage before - * moving on to the next stage. That ensure minimal stats are available - * quickly everywhere before generating more detailed ones. - */ -static void -vacuum_all_databases(ConnParams *cparams, - vacuumingOptions *vacopts, - bool analyze_in_stages, - SimpleStringList *objects, - int concurrentCons, - const char *progname, bool echo, bool quiet) -{ - PGconn *conn; - PGresult *result; - int stage; - int i; - - conn = connectMaintenanceDatabase(cparams, progname, echo); - result = executeQuery(conn, - "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", - echo); - PQfinish(conn); - - if (analyze_in_stages) - { - SimpleStringList **found_objs = NULL; - - if (vacopts->missing_stats_only) - found_objs = palloc0(PQntuples(result) * sizeof(SimpleStringList *)); - - /* - * When analyzing all databases in stages, we analyze them all in the - * fastest stage first, so that initial statistics become available - * for all of them as soon as possible. - * - * This means we establish several times as many connections, but - * that's a secondary consideration. - */ - for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++) - { - for (i = 0; i < PQntuples(result); i++) - { - cparams->override_dbname = PQgetvalue(result, i, 0); - - vacuum_one_database(cparams, vacopts, - stage, - objects, - vacopts->missing_stats_only ? &found_objs[i] : NULL, - concurrentCons, - progname, echo, quiet); - } - } - } - else - { - for (i = 0; i < PQntuples(result); i++) - { - cparams->override_dbname = PQgetvalue(result, i, 0); - - vacuum_one_database(cparams, vacopts, - ANALYZE_NO_STAGE, - objects, NULL, - concurrentCons, - progname, echo, quiet); - } - } - - PQclear(result); -} - -/* - * Construct a vacuum/analyze command to run based on the given options, in the - * given string buffer, which may contain previous garbage. - * - * The table name used must be already properly quoted. The command generated - * depends on the server version involved and it is semicolon-terminated. - */ -static void -prepare_vacuum_command(PQExpBuffer sql, int serverVersion, - vacuumingOptions *vacopts, const char *table) -{ - const char *paren = " ("; - const char *comma = ", "; - const char *sep = paren; - - resetPQExpBuffer(sql); - - if (vacopts->analyze_only) - { - appendPQExpBufferStr(sql, "ANALYZE"); - - /* parenthesized grammar of ANALYZE is supported since v11 */ - if (serverVersion >= 110000) - { - if (vacopts->skip_locked) - { - /* SKIP_LOCKED is supported since v12 */ - Assert(serverVersion >= 120000); - appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); - sep = comma; - } - if (vacopts->verbose) - { - appendPQExpBuffer(sql, "%sVERBOSE", sep); - sep = comma; - } - if (vacopts->buffer_usage_limit) - { - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, - vacopts->buffer_usage_limit); - sep = comma; - } - if (sep != paren) - appendPQExpBufferChar(sql, ')'); - } - else - { - if (vacopts->verbose) - appendPQExpBufferStr(sql, " VERBOSE"); - } - } - else - { - appendPQExpBufferStr(sql, "VACUUM"); - - /* parenthesized grammar of VACUUM is supported since v9.0 */ - if (serverVersion >= 90000) - { - if (vacopts->disable_page_skipping) - { - /* DISABLE_PAGE_SKIPPING is supported since v9.6 */ - Assert(serverVersion >= 90600); - appendPQExpBuffer(sql, "%sDISABLE_PAGE_SKIPPING", sep); - sep = comma; - } - if (vacopts->no_index_cleanup) - { - /* "INDEX_CLEANUP FALSE" has been supported since v12 */ - Assert(serverVersion >= 120000); - Assert(!vacopts->force_index_cleanup); - appendPQExpBuffer(sql, "%sINDEX_CLEANUP FALSE", sep); - sep = comma; - } - if (vacopts->force_index_cleanup) - { - /* "INDEX_CLEANUP TRUE" has been supported since v12 */ - Assert(serverVersion >= 120000); - Assert(!vacopts->no_index_cleanup); - appendPQExpBuffer(sql, "%sINDEX_CLEANUP TRUE", sep); - sep = comma; - } - if (!vacopts->do_truncate) - { - /* TRUNCATE is supported since v12 */ - Assert(serverVersion >= 120000); - appendPQExpBuffer(sql, "%sTRUNCATE FALSE", sep); - sep = comma; - } - if (!vacopts->process_main) - { - /* PROCESS_MAIN is supported since v16 */ - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sPROCESS_MAIN FALSE", sep); - sep = comma; - } - if (!vacopts->process_toast) - { - /* PROCESS_TOAST is supported since v14 */ - Assert(serverVersion >= 140000); - appendPQExpBuffer(sql, "%sPROCESS_TOAST FALSE", sep); - sep = comma; - } - if (vacopts->skip_database_stats) - { - /* SKIP_DATABASE_STATS is supported since v16 */ - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sSKIP_DATABASE_STATS", sep); - sep = comma; - } - if (vacopts->skip_locked) - { - /* SKIP_LOCKED is supported since v12 */ - Assert(serverVersion >= 120000); - appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); - sep = comma; - } - if (vacopts->full) - { - appendPQExpBuffer(sql, "%sFULL", sep); - sep = comma; - } - if (vacopts->freeze) - { - appendPQExpBuffer(sql, "%sFREEZE", sep); - sep = comma; - } - if (vacopts->verbose) - { - appendPQExpBuffer(sql, "%sVERBOSE", sep); - sep = comma; - } - if (vacopts->and_analyze) - { - appendPQExpBuffer(sql, "%sANALYZE", sep); - sep = comma; - } - if (vacopts->parallel_workers >= 0) - { - /* PARALLEL is supported since v13 */ - Assert(serverVersion >= 130000); - appendPQExpBuffer(sql, "%sPARALLEL %d", sep, - vacopts->parallel_workers); - sep = comma; - } - if (vacopts->buffer_usage_limit) - { - Assert(serverVersion >= 160000); - appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, - vacopts->buffer_usage_limit); - sep = comma; - } - if (sep != paren) - appendPQExpBufferChar(sql, ')'); - } - else - { - if (vacopts->full) - appendPQExpBufferStr(sql, " FULL"); - if (vacopts->freeze) - appendPQExpBufferStr(sql, " FREEZE"); - if (vacopts->verbose) - appendPQExpBufferStr(sql, " VERBOSE"); - if (vacopts->and_analyze) - appendPQExpBufferStr(sql, " ANALYZE"); - } - } - - appendPQExpBuffer(sql, " %s;", table); -} - -/* - * Send a vacuum/analyze command to the server, returning after sending the - * command. - * - * Any errors during command execution are reported to stderr. - */ -static void -run_vacuum_command(PGconn *conn, const char *sql, bool echo, - const char *table) -{ - bool status; - - if (echo) - printf("%s\n", sql); - - status = PQsendQuery(conn, sql) == 1; - - if (!status) - { - if (table) - pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s", - table, PQdb(conn), PQerrorMessage(conn)); - else - pg_log_error("vacuuming of database \"%s\" failed: %s", - PQdb(conn), PQerrorMessage(conn)); - } -} static void help(const char *progname) @@ -1336,6 +353,7 @@ help(const char *progname) printf(_(" --buffer-usage-limit=SIZE size of ring buffer used for vacuum\n")); printf(_(" -d, --dbname=DBNAME database to vacuum\n")); printf(_(" --disable-page-skipping disable all page-skipping behavior\n")); + printf(_(" --dry-run show the commands that would be sent to the server\n")); printf(_(" -e, --echo show the commands being sent to the server\n")); printf(_(" -f, --full do full vacuuming\n")); printf(_(" -F, --freeze freeze row transaction information\n")); diff --git a/src/bin/scripts/vacuuming.c b/src/bin/scripts/vacuuming.c new file mode 100644 index 0000000000000..faac9089a0128 --- /dev/null +++ b/src/bin/scripts/vacuuming.c @@ -0,0 +1,1050 @@ +/*------------------------------------------------------------------------- + * vacuuming.c + * Helper routines for vacuumdb + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/bin/scripts/vacuuming.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "catalog/pg_attribute_d.h" +#include "catalog/pg_class_d.h" +#include "common/connect.h" +#include "common/logging.h" +#include "fe_utils/cancel.h" +#include "fe_utils/option_utils.h" +#include "fe_utils/parallel_slot.h" +#include "fe_utils/query_utils.h" +#include "fe_utils/string_utils.h" +#include "vacuuming.h" + + +static int vacuum_one_database(ConnParams *cparams, + vacuumingOptions *vacopts, + int stage, + SimpleStringList *objects, + SimpleStringList **found_objs, + int concurrentCons, + const char *progname); +static int vacuum_all_databases(ConnParams *cparams, + vacuumingOptions *vacopts, + SimpleStringList *objects, + int concurrentCons, + const char *progname); +static SimpleStringList *retrieve_objects(PGconn *conn, + vacuumingOptions *vacopts, + SimpleStringList *objects); +static void free_retrieved_objects(SimpleStringList *list); +static void prepare_vacuum_command(PGconn *conn, PQExpBuffer sql, + vacuumingOptions *vacopts, const char *table); +static void run_vacuum_command(ParallelSlot *free_slot, + vacuumingOptions *vacopts, const char *sql, + const char *table); + +/* + * Executes vacuum/analyze as indicated. Returns 0 if the plan is carried + * to completion, or -1 in case of certain errors (which should hopefully + * been already reported.) Other errors are reported via pg_fatal(). + */ +int +vacuuming_main(ConnParams *cparams, const char *dbname, + const char *maintenance_db, vacuumingOptions *vacopts, + SimpleStringList *objects, + unsigned int tbl_count, int concurrentCons, + const char *progname) +{ + setup_cancel_handler(NULL); + + /* Avoid opening extra connections. */ + if (tbl_count > 0 && (concurrentCons > tbl_count)) + concurrentCons = tbl_count; + + if (vacopts->objfilter & OBJFILTER_ALL_DBS) + { + cparams->dbname = maintenance_db; + + return vacuum_all_databases(cparams, vacopts, + objects, + concurrentCons, + progname); + } + else + { + if (dbname == NULL) + { + if (getenv("PGDATABASE")) + dbname = getenv("PGDATABASE"); + else if (getenv("PGUSER")) + dbname = getenv("PGUSER"); + else + dbname = get_user_name_or_exit(progname); + } + + cparams->dbname = dbname; + + if (vacopts->mode == MODE_ANALYZE_IN_STAGES) + { + SimpleStringList *found_objs = NULL; + + for (int stage = 0; stage < ANALYZE_NUM_STAGES; stage++) + { + int ret; + + ret = vacuum_one_database(cparams, vacopts, + stage, + objects, + vacopts->missing_stats_only ? &found_objs : NULL, + concurrentCons, + progname); + if (ret != 0) + { + free_retrieved_objects(found_objs); + return ret; + } + } + + free_retrieved_objects(found_objs); + return EXIT_SUCCESS; + } + else + return vacuum_one_database(cparams, vacopts, + ANALYZE_NO_STAGE, + objects, NULL, + concurrentCons, + progname); + } +} + +/* + * vacuum_one_database + * + * Process tables in the given database. + * + * There are two ways to specify the list of objects to process: + * + * 1) The "found_objs" parameter is a double pointer to a fully qualified list + * of objects to process, as returned by a previous call to + * vacuum_one_database(). + * + * a) If both "found_objs" (the double pointer) and "*found_objs" (the + * once-dereferenced double pointer) are not NULL, this list takes + * priority, and anything specified in "objects" is ignored. + * + * b) If "found_objs" (the double pointer) is not NULL but "*found_objs" + * (the once-dereferenced double pointer) _is_ NULL, the "objects" + * parameter takes priority, and the results of the catalog query + * described in (2) are stored in "found_objs". + * + * c) If "found_objs" (the double pointer) is NULL, the "objects" + * parameter again takes priority, and the results of the catalog query + * are not saved. + * + * 2) The "objects" parameter is a user-specified list of objects to process. + * When (1b) or (1c) applies, this function performs a catalog query to + * retrieve a fully qualified list of objects to process, as described + * below. + * + * a) If "objects" is not NULL, the catalog query gathers only the objects + * listed in "objects". + * + * b) If "objects" is NULL, all tables in the database are gathered. + * + * Note that this function is only concerned with running exactly one stage + * when in analyze-in-stages mode; caller must iterate on us if necessary. + * + * If concurrentCons is > 1, multiple connections are used to vacuum tables + * in parallel. + */ +static int +vacuum_one_database(ConnParams *cparams, + vacuumingOptions *vacopts, + int stage, + SimpleStringList *objects, + SimpleStringList **found_objs, + int concurrentCons, + const char *progname) +{ + PQExpBufferData sql; + PGconn *conn; + SimpleStringListCell *cell; + ParallelSlotArray *sa; + int ntups = 0; + const char *initcmd; + SimpleStringList *retobjs = NULL; + bool free_retobjs = false; + int ret = EXIT_SUCCESS; + const char *stage_commands[] = { + "SET default_statistics_target=1; SET vacuum_cost_delay=0;", + "SET default_statistics_target=10; RESET vacuum_cost_delay;", + "RESET default_statistics_target;" + }; + const char *stage_messages[] = { + gettext_noop("Generating minimal optimizer statistics (1 target)"), + gettext_noop("Generating medium optimizer statistics (10 targets)"), + gettext_noop("Generating default (full) optimizer statistics") + }; + + Assert(stage == ANALYZE_NO_STAGE || + (stage >= 0 && stage < ANALYZE_NUM_STAGES)); + + conn = connectDatabase(cparams, progname, vacopts->echo, false, true); + + if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "disable-page-skipping", "9.6"); + } + + if (vacopts->no_index_cleanup && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-index-cleanup", "12"); + } + + if (vacopts->force_index_cleanup && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "force-index-cleanup", "12"); + } + + if (!vacopts->do_truncate && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-truncate", "12"); + } + + if (!vacopts->process_main && PQserverVersion(conn) < 160000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-process-main", "16"); + } + + if (!vacopts->process_toast && PQserverVersion(conn) < 140000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "no-process-toast", "14"); + } + + if (vacopts->skip_locked && PQserverVersion(conn) < 120000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "skip-locked", "12"); + } + + if (vacopts->min_xid_age != 0 && PQserverVersion(conn) < 90600) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--min-xid-age", "9.6"); + } + + if (vacopts->min_mxid_age != 0 && PQserverVersion(conn) < 90600) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--min-mxid-age", "9.6"); + } + + if (vacopts->parallel_workers >= 0 && PQserverVersion(conn) < 130000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--parallel", "13"); + } + + if (vacopts->buffer_usage_limit && PQserverVersion(conn) < 160000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--buffer-usage-limit", "16"); + } + + if (vacopts->missing_stats_only && PQserverVersion(conn) < 150000) + { + PQfinish(conn); + pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s", + "--missing-stats-only", "15"); + } + + /* skip_database_stats is used automatically if server supports it */ + vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000); + + if (!vacopts->quiet) + { + if (vacopts->mode == MODE_ANALYZE_IN_STAGES) + printf(_("%s: processing database \"%s\": %s\n"), + progname, PQdb(conn), _(stage_messages[stage])); + else + printf(_("%s: vacuuming database \"%s\"\n"), + progname, PQdb(conn)); + fflush(stdout); + } + + /* + * If the caller provided the results of a previous catalog query, just + * use that. Otherwise, run the catalog query ourselves and set the + * return variable if provided. (If it is, then freeing the string list + * becomes the caller's responsibility.) + */ + if (found_objs && *found_objs) + retobjs = *found_objs; + else + { + retobjs = retrieve_objects(conn, vacopts, objects); + if (found_objs) + *found_objs = retobjs; + else + free_retobjs = true; + } + + /* + * Count the number of objects in the catalog query result. If there are + * none, we are done. + */ + for (cell = retobjs->head; cell; cell = cell->next) + ntups++; + + if (ntups == 0) + { + PQfinish(conn); + if (free_retobjs) + free_retrieved_objects(retobjs); + return EXIT_SUCCESS; + } + + /* + * Ensure concurrentCons is sane. If there are more connections than + * vacuumable relations, we don't need to use them all. + */ + if (concurrentCons > ntups) + concurrentCons = ntups; + if (concurrentCons <= 0) + concurrentCons = 1; + + /* + * All slots need to be prepared to run the appropriate analyze stage, if + * caller requested that mode. We have to prepare the initial connection + * ourselves before setting up the slots. + */ + if (vacopts->mode == MODE_ANALYZE_IN_STAGES) + { + initcmd = stage_commands[stage]; + + if (vacopts->dry_run) + printf("%s\n", initcmd); + else + executeCommand(conn, initcmd, vacopts->echo); + } + else + initcmd = NULL; + + /* + * Setup the database connections. We reuse the connection we already have + * for the first slot. If not in parallel mode, the first slot in the + * array contains the connection. + */ + sa = ParallelSlotsSetup(concurrentCons, cparams, progname, + vacopts->echo, initcmd); + ParallelSlotsAdoptConn(sa, conn); + + initPQExpBuffer(&sql); + + cell = retobjs->head; + do + { + const char *tabname = cell->val; + ParallelSlot *free_slot; + + if (CancelRequested) + { + ret = EXIT_FAILURE; + goto finish; + } + + free_slot = ParallelSlotsGetIdle(sa, NULL); + if (!free_slot) + { + ret = EXIT_FAILURE; + goto finish; + } + + prepare_vacuum_command(free_slot->connection, &sql, + vacopts, tabname); + + /* + * Execute the vacuum. All errors are handled in processQueryResult + * through ParallelSlotsGetIdle. + */ + ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); + run_vacuum_command(free_slot, vacopts, sql.data, tabname); + + cell = cell->next; + } while (cell != NULL); + + if (!ParallelSlotsWaitCompletion(sa)) + { + ret = EXIT_FAILURE; + goto finish; + } + + /* If we used SKIP_DATABASE_STATS, mop up with ONLY_DATABASE_STATS */ + if (vacopts->mode == MODE_VACUUM && vacopts->skip_database_stats) + { + const char *cmd = "VACUUM (ONLY_DATABASE_STATS);"; + ParallelSlot *free_slot = ParallelSlotsGetIdle(sa, NULL); + + if (!free_slot) + { + ret = EXIT_FAILURE; + goto finish; + } + + ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL); + run_vacuum_command(free_slot, vacopts, cmd, NULL); + + if (!ParallelSlotsWaitCompletion(sa)) + ret = EXIT_FAILURE; /* error already reported by handler */ + } + +finish: + ParallelSlotsTerminate(sa); + pg_free(sa); + termPQExpBuffer(&sql); + if (free_retobjs) + free_retrieved_objects(retobjs); + + return ret; +} + +/* + * Vacuum/analyze all connectable databases. + * + * In analyze-in-stages mode, we process all databases in one stage before + * moving on to the next stage. That ensure minimal stats are available + * quickly everywhere before generating more detailed ones. + */ +static int +vacuum_all_databases(ConnParams *cparams, + vacuumingOptions *vacopts, + SimpleStringList *objects, + int concurrentCons, + const char *progname) +{ + int ret = EXIT_SUCCESS; + PGconn *conn; + PGresult *result; + int numdbs; + + conn = connectMaintenanceDatabase(cparams, progname, vacopts->echo); + result = executeQuery(conn, + "SELECT datname FROM pg_database WHERE datallowconn AND datconnlimit <> -2 ORDER BY 1;", + vacopts->echo); + numdbs = PQntuples(result); + PQfinish(conn); + + if (vacopts->mode == MODE_ANALYZE_IN_STAGES) + { + SimpleStringList **found_objs = NULL; + + if (vacopts->missing_stats_only) + found_objs = palloc0(numdbs * sizeof(SimpleStringList *)); + + /* + * When analyzing all databases in stages, we analyze them all in the + * fastest stage first, so that initial statistics become available + * for all of them as soon as possible. + * + * This means we establish several times as many connections, but + * that's a secondary consideration. + */ + for (int stage = 0; stage < ANALYZE_NUM_STAGES; stage++) + { + for (int i = 0; i < numdbs; i++) + { + cparams->override_dbname = PQgetvalue(result, i, 0); + ret = vacuum_one_database(cparams, vacopts, stage, + objects, + vacopts->missing_stats_only ? &found_objs[i] : NULL, + concurrentCons, + progname); + if (ret != EXIT_SUCCESS) + break; + } + if (ret != EXIT_SUCCESS) + break; + } + + if (vacopts->missing_stats_only) + { + for (int i = 0; i < numdbs; i++) + free_retrieved_objects(found_objs[i]); + pg_free(found_objs); + } + } + else + { + for (int i = 0; i < numdbs; i++) + { + cparams->override_dbname = PQgetvalue(result, i, 0); + ret = vacuum_one_database(cparams, vacopts, + ANALYZE_NO_STAGE, + objects, + NULL, + concurrentCons, + progname); + if (ret != EXIT_SUCCESS) + break; + } + } + + PQclear(result); + + return ret; +} + +/* + * Prepare the list of tables to process by querying the catalogs. + * + * Since we execute the constructed query with the default search_path (which + * could be unsafe), everything in this query MUST be fully qualified. + * + * First, build a WITH clause for the catalog query if any tables were + * specified, with a set of values made of relation names and their optional + * set of columns. This is used to match any provided column lists with the + * generated qualified identifiers and to filter for the tables provided via + * --table. If a listed table does not exist, the catalog query will fail. + */ +static SimpleStringList * +retrieve_objects(PGconn *conn, vacuumingOptions *vacopts, + SimpleStringList *objects) +{ + PQExpBufferData buf; + PQExpBufferData catalog_query; + PGresult *res; + SimpleStringListCell *cell; + SimpleStringList *found_objs = palloc0_object(SimpleStringList); + bool objects_listed = false; + + initPQExpBuffer(&catalog_query); + for (cell = objects ? objects->head : NULL; cell; cell = cell->next) + { + char *just_table = NULL; + const char *just_columns = NULL; + + if (!objects_listed) + { + appendPQExpBufferStr(&catalog_query, + "WITH listed_objects (object_oid, column_list) AS (\n" + " VALUES ("); + objects_listed = true; + } + else + appendPQExpBufferStr(&catalog_query, ",\n ("); + + if (vacopts->objfilter & (OBJFILTER_SCHEMA | OBJFILTER_SCHEMA_EXCLUDE)) + { + appendStringLiteralConn(&catalog_query, cell->val, conn); + appendPQExpBufferStr(&catalog_query, "::pg_catalog.regnamespace, "); + } + + if (vacopts->objfilter & OBJFILTER_TABLE) + { + /* + * Split relation and column names given by the user, this is used + * to feed the CTE with values on which are performed pre-run + * validity checks as well. For now these happen only on the + * relation name. + */ + splitTableColumnsSpec(cell->val, PQclientEncoding(conn), + &just_table, &just_columns); + + appendStringLiteralConn(&catalog_query, just_table, conn); + appendPQExpBufferStr(&catalog_query, "::pg_catalog.regclass, "); + } + + if (just_columns && just_columns[0] != '\0') + appendStringLiteralConn(&catalog_query, just_columns, conn); + else + appendPQExpBufferStr(&catalog_query, "NULL"); + + appendPQExpBufferStr(&catalog_query, "::pg_catalog.text)"); + + pg_free(just_table); + } + + /* Finish formatting the CTE */ + if (objects_listed) + appendPQExpBufferStr(&catalog_query, "\n)\n"); + + appendPQExpBufferStr(&catalog_query, "SELECT c.relname, ns.nspname"); + + if (objects_listed) + appendPQExpBufferStr(&catalog_query, ", listed_objects.column_list"); + + appendPQExpBufferStr(&catalog_query, + " FROM pg_catalog.pg_class c\n" + " JOIN pg_catalog.pg_namespace ns" + " ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n" + " CROSS JOIN LATERAL (SELECT c.relkind IN (" + CppAsString2(RELKIND_PARTITIONED_TABLE) ", " + CppAsString2(RELKIND_PARTITIONED_INDEX) ")) as p (inherited)\n" + " LEFT JOIN pg_catalog.pg_class t" + " ON c.reltoastrelid OPERATOR(pg_catalog.=) t.oid\n"); + + /* + * Used to match the tables or schemas listed by the user, completing the + * JOIN clause. + */ + if (objects_listed) + { + appendPQExpBufferStr(&catalog_query, " LEFT JOIN listed_objects" + " ON listed_objects.object_oid" + " OPERATOR(pg_catalog.=) "); + + if (vacopts->objfilter & OBJFILTER_TABLE) + appendPQExpBufferStr(&catalog_query, "c.oid\n"); + else + appendPQExpBufferStr(&catalog_query, "ns.oid\n"); + } + + /* + * Exclude temporary tables, beginning the WHERE clause. + */ + appendPQExpBufferStr(&catalog_query, + " WHERE c.relpersistence OPERATOR(pg_catalog.!=) " + CppAsString2(RELPERSISTENCE_TEMP) "\n"); + + /* + * Used to match the tables or schemas listed by the user, for the WHERE + * clause. + */ + if (objects_listed) + { + if (vacopts->objfilter & OBJFILTER_SCHEMA_EXCLUDE) + appendPQExpBufferStr(&catalog_query, + " AND listed_objects.object_oid IS NULL\n"); + else + appendPQExpBufferStr(&catalog_query, + " AND listed_objects.object_oid IS NOT NULL\n"); + } + + /* + * If no tables were listed, filter for the relevant relation types. If + * tables were given via --table, don't bother filtering by relation type. + * Instead, let the server decide whether a given relation can be + * processed in which case the user will know about it. + */ + if ((vacopts->objfilter & OBJFILTER_TABLE) == 0) + { + /* + * vacuumdb should generally follow the behavior of the underlying + * VACUUM and ANALYZE commands. In MODE_ANALYZE mode, process regular + * tables, materialized views, and partitioned tables, just like + * ANALYZE (with no specific target tables) does. Otherwise, process + * only regular tables and materialized views, since VACUUM skips + * partitioned tables when no target tables are specified. + */ + if (vacopts->mode == MODE_ANALYZE) + appendPQExpBufferStr(&catalog_query, + " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" + CppAsString2(RELKIND_RELATION) ", " + CppAsString2(RELKIND_MATVIEW) ", " + CppAsString2(RELKIND_PARTITIONED_TABLE) "])\n"); + else + appendPQExpBufferStr(&catalog_query, + " AND c.relkind OPERATOR(pg_catalog.=) ANY (array[" + CppAsString2(RELKIND_RELATION) ", " + CppAsString2(RELKIND_MATVIEW) "])\n"); + } + + /* + * For --min-xid-age and --min-mxid-age, the age of the relation is the + * greatest of the ages of the main relation and its associated TOAST + * table. The commands generated by vacuumdb will also process the TOAST + * table for the relation if necessary, so it does not need to be + * considered separately. + */ + if (vacopts->min_xid_age != 0) + { + appendPQExpBuffer(&catalog_query, + " AND GREATEST(pg_catalog.age(c.relfrozenxid)," + " pg_catalog.age(t.relfrozenxid)) " + " OPERATOR(pg_catalog.>=) '%d'::pg_catalog.int4\n" + " AND c.relfrozenxid OPERATOR(pg_catalog.!=)" + " '0'::pg_catalog.xid\n", + vacopts->min_xid_age); + } + + if (vacopts->min_mxid_age != 0) + { + appendPQExpBuffer(&catalog_query, + " AND GREATEST(pg_catalog.mxid_age(c.relminmxid)," + " pg_catalog.mxid_age(t.relminmxid)) OPERATOR(pg_catalog.>=)" + " '%d'::pg_catalog.int4\n" + " AND c.relminmxid OPERATOR(pg_catalog.!=)" + " '0'::pg_catalog.xid\n", + vacopts->min_mxid_age); + } + + if (vacopts->missing_stats_only) + { + appendPQExpBufferStr(&catalog_query, " AND (\n"); + + /* regular stats */ + appendPQExpBufferStr(&catalog_query, + " EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" + " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" + " AND NOT a.attisdropped\n" + " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND a.attgenerated OPERATOR(pg_catalog.<>) " + CppAsString2(ATTRIBUTE_GENERATED_VIRTUAL) "\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" + " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" + " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); + + /* extended stats */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" + " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" + " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" + " AND d.stxdinherit OPERATOR(pg_catalog.=) p.inherited))\n"); + + /* expression indexes */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" + " JOIN pg_catalog.pg_index i" + " ON i.indexrelid OPERATOR(pg_catalog.=) a.attrelid\n" + " WHERE i.indrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND i.indkey[a.attnum OPERATOR(pg_catalog.-) 1::pg_catalog.int2]" + " OPERATOR(pg_catalog.=) 0::pg_catalog.int2\n" + " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" + " AND NOT a.attisdropped\n" + " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" + " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" + " AND s.stainherit OPERATOR(pg_catalog.=) p.inherited))\n"); + + /* inheritance and regular stats */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_attribute a\n" + " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND a.attnum OPERATOR(pg_catalog.>) 0::pg_catalog.int2\n" + " AND NOT a.attisdropped\n" + " AND a.attstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND a.attgenerated OPERATOR(pg_catalog.<>) " + CppAsString2(ATTRIBUTE_GENERATED_VIRTUAL) "\n" + " AND c.relhassubclass\n" + " AND NOT p.inherited\n" + " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" + " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic s\n" + " WHERE s.starelid OPERATOR(pg_catalog.=) a.attrelid\n" + " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum\n" + " AND s.stainherit))\n"); + + /* inheritance and extended stats */ + appendPQExpBufferStr(&catalog_query, + " OR EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext e\n" + " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n" + " AND e.stxstattarget IS DISTINCT FROM 0::pg_catalog.int2\n" + " AND c.relhassubclass\n" + " AND NOT p.inherited\n" + " AND EXISTS (SELECT NULL FROM pg_catalog.pg_inherits h\n" + " WHERE h.inhparent OPERATOR(pg_catalog.=) c.oid)\n" + " AND NOT EXISTS (SELECT NULL FROM pg_catalog.pg_statistic_ext_data d\n" + " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid\n" + " AND d.stxdinherit))\n"); + + appendPQExpBufferStr(&catalog_query, " )\n"); + } + + /* + * Execute the catalog query. We use the default search_path for this + * query for consistency with table lookups done elsewhere by the user. + */ + appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;"); + executeCommand(conn, "RESET search_path;", vacopts->echo); + res = executeQuery(conn, catalog_query.data, vacopts->echo); + termPQExpBuffer(&catalog_query); + PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, vacopts->echo)); + + /* + * Build qualified identifiers for each table, including the column list + * if given. + */ + initPQExpBuffer(&buf); + for (int i = 0; i < PQntuples(res); i++) + { + appendPQExpBufferStr(&buf, + fmtQualifiedIdEnc(PQgetvalue(res, i, 1), + PQgetvalue(res, i, 0), + PQclientEncoding(conn))); + + if (objects_listed && !PQgetisnull(res, i, 2)) + appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2)); + + simple_string_list_append(found_objs, buf.data); + resetPQExpBuffer(&buf); + } + termPQExpBuffer(&buf); + PQclear(res); + + return found_objs; +} + +/* + * Free the results of retrieve_objects(). + * + * For caller convenience, we allow the argument to be NULL, + * although retrieve_objects() will never return that. + */ +static void +free_retrieved_objects(SimpleStringList *list) +{ + if (list) + { + simple_string_list_destroy(list); + pg_free(list); + } +} + +/* + * Construct a vacuum/analyze command to run based on the given + * options, in the given string buffer, which may contain previous garbage. + * + * The table name used must be already properly quoted. The command generated + * depends on the server version involved and it is semicolon-terminated. + */ +static void +prepare_vacuum_command(PGconn *conn, PQExpBuffer sql, + vacuumingOptions *vacopts, const char *table) +{ + int serverVersion = PQserverVersion(conn); + const char *paren = " ("; + const char *comma = ", "; + const char *sep = paren; + + resetPQExpBuffer(sql); + + if (vacopts->mode == MODE_ANALYZE || + vacopts->mode == MODE_ANALYZE_IN_STAGES) + { + appendPQExpBufferStr(sql, "ANALYZE"); + + /* parenthesized grammar of ANALYZE is supported since v11 */ + if (serverVersion >= 110000) + { + if (vacopts->skip_locked) + { + /* SKIP_LOCKED is supported since v12 */ + Assert(serverVersion >= 120000); + appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); + sep = comma; + } + if (vacopts->verbose) + { + appendPQExpBuffer(sql, "%sVERBOSE", sep); + sep = comma; + } + if (vacopts->buffer_usage_limit) + { + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, + vacopts->buffer_usage_limit); + sep = comma; + } + if (sep != paren) + appendPQExpBufferChar(sql, ')'); + } + else + { + if (vacopts->verbose) + appendPQExpBufferStr(sql, " VERBOSE"); + } + } + else + { + appendPQExpBufferStr(sql, "VACUUM"); + + /* parenthesized grammar of VACUUM is supported since v9.0 */ + if (serverVersion >= 90000) + { + if (vacopts->disable_page_skipping) + { + /* DISABLE_PAGE_SKIPPING is supported since v9.6 */ + Assert(serverVersion >= 90600); + appendPQExpBuffer(sql, "%sDISABLE_PAGE_SKIPPING", sep); + sep = comma; + } + if (vacopts->no_index_cleanup) + { + /* "INDEX_CLEANUP FALSE" has been supported since v12 */ + Assert(serverVersion >= 120000); + Assert(!vacopts->force_index_cleanup); + appendPQExpBuffer(sql, "%sINDEX_CLEANUP FALSE", sep); + sep = comma; + } + if (vacopts->force_index_cleanup) + { + /* "INDEX_CLEANUP TRUE" has been supported since v12 */ + Assert(serverVersion >= 120000); + Assert(!vacopts->no_index_cleanup); + appendPQExpBuffer(sql, "%sINDEX_CLEANUP TRUE", sep); + sep = comma; + } + if (!vacopts->do_truncate) + { + /* TRUNCATE is supported since v12 */ + Assert(serverVersion >= 120000); + appendPQExpBuffer(sql, "%sTRUNCATE FALSE", sep); + sep = comma; + } + if (!vacopts->process_main) + { + /* PROCESS_MAIN is supported since v16 */ + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sPROCESS_MAIN FALSE", sep); + sep = comma; + } + if (!vacopts->process_toast) + { + /* PROCESS_TOAST is supported since v14 */ + Assert(serverVersion >= 140000); + appendPQExpBuffer(sql, "%sPROCESS_TOAST FALSE", sep); + sep = comma; + } + if (vacopts->skip_database_stats) + { + /* SKIP_DATABASE_STATS is supported since v16 */ + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sSKIP_DATABASE_STATS", sep); + sep = comma; + } + if (vacopts->skip_locked) + { + /* SKIP_LOCKED is supported since v12 */ + Assert(serverVersion >= 120000); + appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep); + sep = comma; + } + if (vacopts->full) + { + appendPQExpBuffer(sql, "%sFULL", sep); + sep = comma; + } + if (vacopts->freeze) + { + appendPQExpBuffer(sql, "%sFREEZE", sep); + sep = comma; + } + if (vacopts->verbose) + { + appendPQExpBuffer(sql, "%sVERBOSE", sep); + sep = comma; + } + if (vacopts->and_analyze) + { + appendPQExpBuffer(sql, "%sANALYZE", sep); + sep = comma; + } + if (vacopts->parallel_workers >= 0) + { + /* PARALLEL is supported since v13 */ + Assert(serverVersion >= 130000); + appendPQExpBuffer(sql, "%sPARALLEL %d", sep, + vacopts->parallel_workers); + sep = comma; + } + if (vacopts->buffer_usage_limit) + { + Assert(serverVersion >= 160000); + appendPQExpBuffer(sql, "%sBUFFER_USAGE_LIMIT '%s'", sep, + vacopts->buffer_usage_limit); + sep = comma; + } + if (sep != paren) + appendPQExpBufferChar(sql, ')'); + } + else + { + if (vacopts->full) + appendPQExpBufferStr(sql, " FULL"); + if (vacopts->freeze) + appendPQExpBufferStr(sql, " FREEZE"); + if (vacopts->verbose) + appendPQExpBufferStr(sql, " VERBOSE"); + if (vacopts->and_analyze) + appendPQExpBufferStr(sql, " ANALYZE"); + } + } + + appendPQExpBuffer(sql, " %s;", table); +} + +/* + * Send a vacuum/analyze command to the server, returning after sending the + * command. If dry_run is true, the command is printed but not sent to the + * server. + * + * Any errors during command execution are reported to stderr. + */ +static void +run_vacuum_command(ParallelSlot *free_slot, vacuumingOptions *vacopts, + const char *sql, const char *table) +{ + bool status = true; + PGconn *conn = free_slot->connection; + + if (vacopts->echo || vacopts->dry_run) + printf("%s\n", sql); + + if (vacopts->dry_run) + ParallelSlotSetIdle(free_slot); + else + status = PQsendQuery(conn, sql) == 1; + + if (!status) + { + if (table) + { + pg_log_error("vacuuming of table \"%s\" in database \"%s\" failed: %s", + table, PQdb(conn), PQerrorMessage(conn)); + } + else + { + pg_log_error("vacuuming of database \"%s\" failed: %s", + PQdb(conn), PQerrorMessage(conn)); + } + } +} + +/* + * Returns a newly malloc'd version of 'src' with escaped single quotes and + * backslashes. + */ +char * +escape_quotes(const char *src) +{ + char *result = escape_single_quotes_ascii(src); + + if (!result) + pg_fatal("out of memory"); + return result; +} diff --git a/src/bin/scripts/vacuuming.h b/src/bin/scripts/vacuuming.h new file mode 100644 index 0000000000000..5a491db2526d7 --- /dev/null +++ b/src/bin/scripts/vacuuming.h @@ -0,0 +1,75 @@ +/*------------------------------------------------------------------------- + * + * vacuuming.h + * Common declarations for vacuuming.c + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/bin/scripts/vacuuming.h + * + *------------------------------------------------------------------------- + */ +#ifndef VACUUMING_H +#define VACUUMING_H + +#include "common.h" +#include "fe_utils/connect_utils.h" +#include "fe_utils/simple_list.h" + +typedef enum +{ + MODE_VACUUM, + MODE_ANALYZE, + MODE_ANALYZE_IN_STAGES +} RunMode; + +/* For analyze-in-stages mode */ +#define ANALYZE_NO_STAGE -1 +#define ANALYZE_NUM_STAGES 3 + +/* vacuum options controlled by user flags */ +typedef struct vacuumingOptions +{ + RunMode mode; + uint32 objfilter; + bool verbose; + bool and_analyze; + bool full; + bool freeze; + bool disable_page_skipping; + bool skip_locked; + int min_xid_age; + int min_mxid_age; + int parallel_workers; /* >= 0 indicates user specified the + * parallel degree, otherwise -1 */ + bool no_index_cleanup; + bool force_index_cleanup; + bool do_truncate; + bool process_main; + bool process_toast; + bool skip_database_stats; + char *buffer_usage_limit; + bool missing_stats_only; + bool echo; + bool quiet; + bool dry_run; +} vacuumingOptions; + +/* Valid values for vacuumingOptions->objfilter */ +#define OBJFILTER_ALL_DBS 0x01 /* --all */ +#define OBJFILTER_DATABASE 0x02 /* --dbname */ +#define OBJFILTER_TABLE 0x04 /* --table */ +#define OBJFILTER_SCHEMA 0x08 /* --schema */ +#define OBJFILTER_SCHEMA_EXCLUDE 0x10 /* --exclude-schema */ + +extern int vacuuming_main(ConnParams *cparams, const char *dbname, + const char *maintenance_db, vacuumingOptions *vacopts, + SimpleStringList *objects, + unsigned int tbl_count, + int concurrentCons, + const char *progname); + +extern char *escape_quotes(const char *src); + +#endif /* VACUUMING_H */ diff --git a/src/common/Makefile b/src/common/Makefile index 1e2b91c83c4c4..1a2fbbe887f22 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -59,6 +59,7 @@ OBJS_COMMON = \ file_perm.o \ file_utils.o \ hashfn.o \ + instr_time.o \ ip.o \ jsonapi.o \ keywords.o \ @@ -163,7 +164,7 @@ libpgcommon_shlib.a: $(OBJS_SHLIB) # The JSON API normally exits on out-of-memory; disable that behavior for shared # library builds. This requires libpq's pqexpbuffer.h. jsonapi_shlib.o: override CPPFLAGS += -DJSONAPI_USE_PQEXPBUFFER -jsonapi_shlib.o: override CPPFLAGS += -I$(libpq_srcdir) +jsonapi_shlib.o: override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) # Because this uses its own compilation rule, it doesn't use the # dependency tracking logic from Makefile.global. To make sure that diff --git a/src/common/archive.c b/src/common/archive.c index 325f6d7b7f3aa..43b11638faecb 100644 --- a/src/common/archive.c +++ b/src/common/archive.c @@ -3,7 +3,7 @@ * archive.c * Common WAL archive routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/base64.c b/src/common/base64.c index 240461fe1e6ab..aaaefc2921ace 100644 --- a/src/common/base64.c +++ b/src/common/base64.c @@ -3,7 +3,7 @@ * base64.c * Encoding and decoding routines for base64 without whitespace. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/common/binaryheap.c b/src/common/binaryheap.c index b13816f6f8495..78823ff00f804 100644 --- a/src/common/binaryheap.c +++ b/src/common/binaryheap.c @@ -3,7 +3,7 @@ * binaryheap.c * A simple binary heap implementation * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/binaryheap.c @@ -17,8 +17,6 @@ #include "postgres.h" #endif -#include - #ifdef FRONTEND #include "common/logging.h" #endif diff --git a/src/common/blkreftable.c b/src/common/blkreftable.c index b935baf9ad4bb..37a9d14b00eeb 100644 --- a/src/common/blkreftable.c +++ b/src/common/blkreftable.c @@ -18,7 +18,7 @@ * later be marked as modified again; if that happens, it means the relation * was re-extended. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/common/blkreftable.c * @@ -234,7 +234,7 @@ static void BlockRefTableFileTerminate(BlockRefTableBuffer *buffer); BlockRefTable * CreateEmptyBlockRefTable(void) { - BlockRefTable *brtab = palloc(sizeof(BlockRefTable)); + BlockRefTable *brtab = palloc_object(BlockRefTable); /* * Even completely empty database has a few hundred relation forks, so it @@ -497,7 +497,7 @@ WriteBlockRefTable(BlockRefTable *brtab, /* Extract entries into serializable format and sort them. */ sdata = - palloc(brtab->hash->members * sizeof(BlockRefTableSerializedEntry)); + palloc_array(BlockRefTableSerializedEntry, brtab->hash->members); blockreftable_start_iterate(brtab->hash, &it); while ((brtentry = blockreftable_iterate(brtab->hash, &it)) != NULL) { @@ -584,7 +584,7 @@ CreateBlockRefTableReader(io_callback_fn read_callback, uint32 magic; /* Initialize data structure. */ - reader = palloc0(sizeof(BlockRefTableReader)); + reader = palloc0_object(BlockRefTableReader); reader->buffer.io_callback = read_callback; reader->buffer.io_callback_arg = read_callback_arg; reader->error_filename = error_filename; @@ -660,7 +660,7 @@ BlockRefTableReaderNextRelation(BlockRefTableReader *reader, /* Read chunk size array. */ if (reader->chunk_size != NULL) pfree(reader->chunk_size); - reader->chunk_size = palloc(sentry.nchunks * sizeof(uint16)); + reader->chunk_size = palloc_array(uint16, sentry.nchunks); BlockRefTableRead(reader, reader->chunk_size, sentry.nchunks * sizeof(uint16)); @@ -794,7 +794,7 @@ CreateBlockRefTableWriter(io_callback_fn write_callback, uint32 magic = BLOCKREFTABLE_MAGIC; /* Prepare buffer and CRC check and save callbacks. */ - writer = palloc0(sizeof(BlockRefTableWriter)); + writer = palloc0_object(BlockRefTableWriter); writer->buffer.io_callback = write_callback; writer->buffer.io_callback_arg = write_callback_arg; INIT_CRC32C(writer->buffer.crc); @@ -874,7 +874,7 @@ DestroyBlockRefTableWriter(BlockRefTableWriter *writer) BlockRefTableEntry * CreateBlockRefTableEntry(RelFileLocator rlocator, ForkNumber forknum) { - BlockRefTableEntry *entry = palloc0(sizeof(BlockRefTableEntry)); + BlockRefTableEntry *entry = palloc0_object(BlockRefTableEntry); memcpy(&entry->key.rlocator, &rlocator, sizeof(RelFileLocator)); entry->key.forknum = forknum; @@ -997,10 +997,9 @@ BlockRefTableEntryMarkBlockModified(BlockRefTableEntry *entry, if (entry->nchunks == 0) { - entry->chunk_size = palloc0(sizeof(uint16) * max_chunks); - entry->chunk_usage = palloc0(sizeof(uint16) * max_chunks); - entry->chunk_data = - palloc0(sizeof(BlockRefTableChunk) * max_chunks); + entry->chunk_size = palloc0_array(uint16, max_chunks); + entry->chunk_usage = palloc0_array(uint16, max_chunks); + entry->chunk_data = palloc0_array(BlockRefTableChunk, max_chunks); } else { @@ -1029,7 +1028,7 @@ BlockRefTableEntryMarkBlockModified(BlockRefTableEntry *entry, if (entry->chunk_size[chunkno] == 0) { entry->chunk_data[chunkno] = - palloc(sizeof(uint16) * INITIAL_ENTRIES_PER_CHUNK); + palloc_array(uint16, INITIAL_ENTRIES_PER_CHUNK); entry->chunk_size[chunkno] = INITIAL_ENTRIES_PER_CHUNK; entry->chunk_data[chunkno][0] = chunkoffset; entry->chunk_usage[chunkno] = 1; diff --git a/src/common/checksum_helper.c b/src/common/checksum_helper.c index 716a481cea5df..2c6e2fc03fc79 100644 --- a/src/common/checksum_helper.c +++ b/src/common/checksum_helper.c @@ -3,7 +3,7 @@ * checksum_helper.c * Compute a checksum of any of various types using common routines * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/checksum_helper.c diff --git a/src/common/compression.c b/src/common/compression.c index 4c3c9fd7b507b..ae2089d9406d6 100644 --- a/src/common/compression.c +++ b/src/common/compression.c @@ -14,7 +14,7 @@ * * Currently, the supported keywords are "level", "long", and "workers". * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/compression.c @@ -41,6 +41,36 @@ static int expect_integer_value(char *keyword, char *value, static bool expect_boolean_value(char *keyword, char *value, pg_compress_specification *result); +/* + * Look up a compression algorithm by archive file extension. Returns true and + * sets *algorithm if the extension is recognized. Otherwise returns false. + */ +bool +parse_tar_compress_algorithm(const char *fname, pg_compress_algorithm *algorithm) +{ + size_t fname_len = strlen(fname); + + if (fname_len >= 4 && + strcmp(fname + fname_len - 4, ".tar") == 0) + *algorithm = PG_COMPRESSION_NONE; + else if (fname_len >= 4 && + strcmp(fname + fname_len - 4, ".tgz") == 0) + *algorithm = PG_COMPRESSION_GZIP; + else if (fname_len >= 7 && + strcmp(fname + fname_len - 7, ".tar.gz") == 0) + *algorithm = PG_COMPRESSION_GZIP; + else if (fname_len >= 8 && + strcmp(fname + fname_len - 8, ".tar.lz4") == 0) + *algorithm = PG_COMPRESSION_LZ4; + else if (fname_len >= 8 && + strcmp(fname + fname_len - 8, ".tar.zst") == 0) + *algorithm = PG_COMPRESSION_ZSTD; + else + return false; + + return true; +} + /* * Look up a compression algorithm by name. Returns true and sets *algorithm * if the name is recognized. Otherwise returns false. @@ -425,7 +455,7 @@ validate_compress_specification(pg_compress_specification *spec) void parse_compress_options(const char *option, char **algorithm, char **detail) { - char *sep; + const char *sep; char *endp; long result; diff --git a/src/common/config_info.c b/src/common/config_info.c index 113a8a6eb474c..572ae3a465175 100644 --- a/src/common/config_info.c +++ b/src/common/config_info.c @@ -4,7 +4,7 @@ * Common code for pg_config output * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/controldata_utils.c b/src/common/controldata_utils.c index fa375dc91293b..4ab116afcdef6 100644 --- a/src/common/controldata_utils.c +++ b/src/common/controldata_utils.c @@ -4,7 +4,7 @@ * Common code for control data file output. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -37,6 +37,7 @@ #ifndef FRONTEND #include "pgstat.h" #include "storage/fd.h" +#include "utils/wait_event.h" #endif /* diff --git a/src/common/cryptohash.c b/src/common/cryptohash.c index 533e4aaea36df..db5358ab5d7fc 100644 --- a/src/common/cryptohash.c +++ b/src/common/cryptohash.c @@ -6,7 +6,7 @@ * This is the set of in-core functions used when there are no other * alternative options like OpenSSL. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/cryptohash_openssl.c b/src/common/cryptohash_openssl.c index c4f15bee563af..51b7e0409333d 100644 --- a/src/common/cryptohash_openssl.c +++ b/src/common/cryptohash_openssl.c @@ -6,7 +6,7 @@ * * This should only be used if code is compiled with OpenSSL support. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/d2s.c b/src/common/d2s.c index 5322c465d53df..34e7cd33bf9f5 100644 --- a/src/common/d2s.c +++ b/src/common/d2s.c @@ -2,7 +2,7 @@ * * Ryu floating-point output for double precision. * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/d2s.c diff --git a/src/common/d2s_full_table.h b/src/common/d2s_full_table.h index c6ce63567b50f..e9613d4affeba 100644 --- a/src/common/d2s_full_table.h +++ b/src/common/d2s_full_table.h @@ -2,7 +2,7 @@ * * Ryu floating-point output for double precision. * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/d2s_full_table.h diff --git a/src/common/d2s_intrinsics.h b/src/common/d2s_intrinsics.h index 0c6313e0df6b9..a01af8a22bbb5 100644 --- a/src/common/d2s_intrinsics.h +++ b/src/common/d2s_intrinsics.h @@ -2,7 +2,7 @@ * * Ryu floating-point output for double precision. * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/d2s_intrinsics.h diff --git a/src/common/encnames.c b/src/common/encnames.c index dd468af204060..959b991dde44d 100644 --- a/src/common/encnames.c +++ b/src/common/encnames.c @@ -3,7 +3,7 @@ * encnames.c * Encoding names and routines for working with them. * - * Portions Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2001-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/encnames.c @@ -61,8 +61,9 @@ static const pg_encname pg_encname_tbl[] = * Japanese, standard OSF */ { "euckr", PG_EUC_KR - }, /* EUC-KR; Extended Unix Code for Korean , KS - * X 1001 standard */ + }, /* EUC-KR; Extended Unix Code for Korean + * precomposed (Wansung) encoding, standard KS + * X 1001 */ { "euctw", PG_EUC_TW }, /* EUC-TW; Extended Unix Code for @@ -119,8 +120,8 @@ static const pg_encname pg_encname_tbl[] = }, /* ISO-8859-9; RFC1345,KXS2 */ { "johab", PG_JOHAB - }, /* JOHAB; Extended Unix Code for simplified - * Chinese */ + }, /* JOHAB; Korean combining (Johab) encoding, + * standard KS X 1001 annex 3 */ { "koi8", PG_KOI8R }, /* _dirty_ alias for KOI8-R (backward @@ -164,9 +165,6 @@ static const pg_encname pg_encname_tbl[] = { "mskanji", PG_SJIS }, /* alias for Shift_JIS */ - { - "muleinternal", PG_MULE_INTERNAL - }, { "shiftjis", PG_SJIS }, /* Shift_JIS; JIS X 0202-1991 */ @@ -189,7 +187,9 @@ static const pg_encname pg_encname_tbl[] = }, /* alias for WIN1258 */ { "uhc", PG_UHC - }, /* UHC; Korean Windows CodePage 949 */ + }, /* UHC; Unified Hangul Code, Microsoft Windows + * CodePage 949; superset of EUC-KR covering + * all 11,172 precomposed Hangul syllables */ { "unicode", PG_UTF8 }, /* alias for UTF8 */ @@ -314,7 +314,6 @@ const pg_enc2name pg_enc2name_tbl[] = [PG_EUC_TW] = DEF_ENC2NAME(EUC_TW, 0), [PG_EUC_JIS_2004] = DEF_ENC2NAME(EUC_JIS_2004, 20932), [PG_UTF8] = DEF_ENC2NAME(UTF8, 65001), - [PG_MULE_INTERNAL] = DEF_ENC2NAME(MULE_INTERNAL, 0), [PG_LATIN1] = DEF_ENC2NAME(LATIN1, 28591), [PG_LATIN2] = DEF_ENC2NAME(LATIN2, 28592), [PG_LATIN3] = DEF_ENC2NAME(LATIN3, 28593), @@ -353,15 +352,12 @@ const pg_enc2name pg_enc2name_tbl[] = /* ---------- * These are encoding names for gettext. - * - * This covers all encodings except MULE_INTERNAL, which is alien to gettext. * ---------- */ const char *pg_enc2gettext_tbl[] = { [PG_SQL_ASCII] = "US-ASCII", [PG_UTF8] = "UTF-8", - [PG_MULE_INTERNAL] = NULL, [PG_LATIN1] = "LATIN1", [PG_LATIN2] = "LATIN2", [PG_LATIN3] = "LATIN3", @@ -420,7 +416,6 @@ static const char *const pg_enc2icu_tbl[] = [PG_EUC_TW] = "EUC-TW", [PG_EUC_JIS_2004] = NULL, [PG_UTF8] = "UTF-8", - [PG_MULE_INTERNAL] = NULL, [PG_LATIN1] = "ISO-8859-1", [PG_LATIN2] = "ISO-8859-2", [PG_LATIN3] = "ISO-8859-3", diff --git a/src/common/exec.c b/src/common/exec.c index 8b690a1018501..2881aa92ca61e 100644 --- a/src/common/exec.c +++ b/src/common/exec.c @@ -4,7 +4,7 @@ * Functions for finding and validating executable files * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -46,7 +46,8 @@ /* Inhibit mingw CRT's auto-globbing of command line arguments */ #if defined(WIN32) && !defined(_MSC_VER) -extern int _CRT_glob = 0; /* 0 turns off globbing; 1 turns it on */ +extern int _CRT_glob; +int _CRT_glob = 0; /* 0 turns off globbing; 1 turns it on */ #endif /* diff --git a/src/common/f2s.c b/src/common/f2s.c index a239a8765b07b..d81a3a43d0e41 100644 --- a/src/common/f2s.c +++ b/src/common/f2s.c @@ -2,7 +2,7 @@ * * Ryu floating-point output for single precision. * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/f2s.c diff --git a/src/common/fe_memutils.c b/src/common/fe_memutils.c index 4ba793a6132bb..d24a5f6770413 100644 --- a/src/common/fe_memutils.c +++ b/src/common/fe_memutils.c @@ -3,7 +3,7 @@ * fe_memutils.c * memory management support for frontend code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/file_perm.c b/src/common/file_perm.c index a65a34d4d3f93..bcd0b5ec666b8 100644 --- a/src/common/file_perm.c +++ b/src/common/file_perm.c @@ -3,7 +3,7 @@ * File and directory permission routines * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/common/file_perm.c diff --git a/src/common/file_utils.c b/src/common/file_utils.c index 7b62687a2aa75..390e60bd01f35 100644 --- a/src/common/file_utils.c +++ b/src/common/file_utils.c @@ -5,7 +5,7 @@ * Assorted utility functions to work on files. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/common/file_utils.c @@ -656,7 +656,7 @@ compute_remaining_iovec(struct iovec *destination, * error is returned, it is unspecified how much has been written. */ ssize_t -pg_pwritev_with_retry(int fd, const struct iovec *iov, int iovcnt, off_t offset) +pg_pwritev_with_retry(int fd, const struct iovec *iov, int iovcnt, pgoff_t offset) { struct iovec iov_copy[PG_IOV_MAX]; ssize_t sum = 0; @@ -706,7 +706,7 @@ pg_pwritev_with_retry(int fd, const struct iovec *iov, int iovcnt, off_t offset) * is returned with errno set. */ ssize_t -pg_pwrite_zeros(int fd, size_t size, off_t offset) +pg_pwrite_zeros(int fd, size_t size, pgoff_t offset) { static const PGIOAlignedBlock zbuffer = {0}; /* worth BLCKSZ */ void *zerobuf_addr = unconstify(PGIOAlignedBlock *, &zbuffer)->data; diff --git a/src/common/hashfn.c b/src/common/hashfn.c index 8a6bd8114efa3..c7a0626f96f62 100644 --- a/src/common/hashfn.c +++ b/src/common/hashfn.c @@ -5,7 +5,7 @@ * hashtables * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -178,13 +178,13 @@ hash_bytes(const unsigned char *k, int keylen) { case 11: c += ((uint32) k[10] << 8); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 24); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += ka[1]; @@ -192,22 +192,22 @@ hash_bytes(const unsigned char *k, int keylen) break; case 7: b += ((uint32) k[6] << 8); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 16); - /* fall through */ + pg_fallthrough; case 5: b += ((uint32) k[4] << 24); - /* fall through */ + pg_fallthrough; case 4: a += ka[0]; break; case 3: a += ((uint32) k[2] << 8); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 16); - /* fall through */ + pg_fallthrough; case 1: a += ((uint32) k[0] << 24); /* case 0: nothing left to add */ @@ -217,13 +217,13 @@ hash_bytes(const unsigned char *k, int keylen) { case 11: c += ((uint32) k[10] << 24); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 8); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += ka[1]; @@ -231,22 +231,22 @@ hash_bytes(const unsigned char *k, int keylen) break; case 7: b += ((uint32) k[6] << 16); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 8); - /* fall through */ + pg_fallthrough; case 5: b += k[4]; - /* fall through */ + pg_fallthrough; case 4: a += ka[0]; break; case 3: a += ((uint32) k[2] << 16); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 8); - /* fall through */ + pg_fallthrough; case 1: a += k[0]; /* case 0: nothing left to add */ @@ -280,35 +280,35 @@ hash_bytes(const unsigned char *k, int keylen) { case 11: c += ((uint32) k[10] << 8); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 24); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += k[7]; - /* fall through */ + pg_fallthrough; case 7: b += ((uint32) k[6] << 8); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 16); - /* fall through */ + pg_fallthrough; case 5: b += ((uint32) k[4] << 24); - /* fall through */ + pg_fallthrough; case 4: a += k[3]; - /* fall through */ + pg_fallthrough; case 3: a += ((uint32) k[2] << 8); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 16); - /* fall through */ + pg_fallthrough; case 1: a += ((uint32) k[0] << 24); /* case 0: nothing left to add */ @@ -318,35 +318,35 @@ hash_bytes(const unsigned char *k, int keylen) { case 11: c += ((uint32) k[10] << 24); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 8); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += ((uint32) k[7] << 24); - /* fall through */ + pg_fallthrough; case 7: b += ((uint32) k[6] << 16); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 8); - /* fall through */ + pg_fallthrough; case 5: b += k[4]; - /* fall through */ + pg_fallthrough; case 4: a += ((uint32) k[3] << 24); - /* fall through */ + pg_fallthrough; case 3: a += ((uint32) k[2] << 16); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 8); - /* fall through */ + pg_fallthrough; case 1: a += k[0]; /* case 0: nothing left to add */ @@ -417,13 +417,13 @@ hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed) { case 11: c += ((uint32) k[10] << 8); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 24); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += ka[1]; @@ -431,22 +431,22 @@ hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed) break; case 7: b += ((uint32) k[6] << 8); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 16); - /* fall through */ + pg_fallthrough; case 5: b += ((uint32) k[4] << 24); - /* fall through */ + pg_fallthrough; case 4: a += ka[0]; break; case 3: a += ((uint32) k[2] << 8); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 16); - /* fall through */ + pg_fallthrough; case 1: a += ((uint32) k[0] << 24); /* case 0: nothing left to add */ @@ -456,13 +456,13 @@ hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed) { case 11: c += ((uint32) k[10] << 24); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 8); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += ka[1]; @@ -470,22 +470,22 @@ hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed) break; case 7: b += ((uint32) k[6] << 16); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 8); - /* fall through */ + pg_fallthrough; case 5: b += k[4]; - /* fall through */ + pg_fallthrough; case 4: a += ka[0]; break; case 3: a += ((uint32) k[2] << 16); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 8); - /* fall through */ + pg_fallthrough; case 1: a += k[0]; /* case 0: nothing left to add */ @@ -519,35 +519,35 @@ hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed) { case 11: c += ((uint32) k[10] << 8); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 24); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += k[7]; - /* fall through */ + pg_fallthrough; case 7: b += ((uint32) k[6] << 8); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 16); - /* fall through */ + pg_fallthrough; case 5: b += ((uint32) k[4] << 24); - /* fall through */ + pg_fallthrough; case 4: a += k[3]; - /* fall through */ + pg_fallthrough; case 3: a += ((uint32) k[2] << 8); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 16); - /* fall through */ + pg_fallthrough; case 1: a += ((uint32) k[0] << 24); /* case 0: nothing left to add */ @@ -557,35 +557,35 @@ hash_bytes_extended(const unsigned char *k, int keylen, uint64 seed) { case 11: c += ((uint32) k[10] << 24); - /* fall through */ + pg_fallthrough; case 10: c += ((uint32) k[9] << 16); - /* fall through */ + pg_fallthrough; case 9: c += ((uint32) k[8] << 8); - /* fall through */ + pg_fallthrough; case 8: /* the lowest byte of c is reserved for the length */ b += ((uint32) k[7] << 24); - /* fall through */ + pg_fallthrough; case 7: b += ((uint32) k[6] << 16); - /* fall through */ + pg_fallthrough; case 6: b += ((uint32) k[5] << 8); - /* fall through */ + pg_fallthrough; case 5: b += k[4]; - /* fall through */ + pg_fallthrough; case 4: a += ((uint32) k[3] << 24); - /* fall through */ + pg_fallthrough; case 3: a += ((uint32) k[2] << 16); - /* fall through */ + pg_fallthrough; case 2: a += ((uint32) k[1] << 8); - /* fall through */ + pg_fallthrough; case 1: a += k[0]; /* case 0: nothing left to add */ diff --git a/src/common/hmac.c b/src/common/hmac.c index 9b85910672842..ce747947e5b8a 100644 --- a/src/common/hmac.c +++ b/src/common/hmac.c @@ -5,7 +5,7 @@ * * Fallback implementation of HMAC, as specified in RFC 2104. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/hmac_openssl.c b/src/common/hmac_openssl.c index 31cd188904384..7990822854c2b 100644 --- a/src/common/hmac_openssl.c +++ b/src/common/hmac_openssl.c @@ -5,7 +5,7 @@ * * This should only be used if code is compiled with OpenSSL support. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/instr_time.c b/src/common/instr_time.c new file mode 100644 index 0000000000000..fc6e1852c30b2 --- /dev/null +++ b/src/common/instr_time.c @@ -0,0 +1,372 @@ +/*------------------------------------------------------------------------- + * + * instr_time.c + * Non-inline parts of the portable high-precision interval timing + * implementation + * + * Portions Copyright (c) 2026, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/common/instr_time.c + * + *------------------------------------------------------------------------- + */ +#ifndef FRONTEND +#include "postgres.h" +#else +#include "postgres_fe.h" +#endif + +#include + +#include "port/pg_cpu.h" +#include "portability/instr_time.h" + +/* + * Stores what the number of ticks needs to be multiplied with to end up + * with nanoseconds using integer math. + * + * In certain cases (TSC on x86-64, and QueryPerformanceCounter on Windows) + * the ticks to nanoseconds conversion requires floating point math because: + * + * sec = ticks / frequency_hz + * ns = ticks / frequency_hz * 1,000,000,000 + * ns = ticks * (1,000,000,000 / frequency_hz) + * ns = ticks * (1,000,000 / frequency_khz) <-- now in kilohertz + * + * Here, 'ns' is usually a floating point number. For example for a 2.5 GHz CPU + * the scaling factor becomes 1,000,000 / 2,500,000 = 0.4. + * + * To be able to use integer math we work around the lack of precision. We + * first scale the integer up (left shift by TICKS_TO_NS_SHIFT) and after the + * multiplication by the number of ticks in pg_ticks_to_ns() we shift right by + * the same amount. + * + * We remember the maximum number of ticks that can be multiplied by the scale + * factor without overflowing so we can check via a * b > max <=> a > max / b. + * + * However, as this is meant for interval measurements, it is unlikely that the + * overflow path is actually taken in typical scenarios, since overflows would + * only occur for intervals longer than 6.5 days. + * + * Note we utilize unsigned integers even though ticks are stored as a signed + * value to encourage compilers to generate better assembly, since we can be + * sure these values are not negative. + * + * In all other cases we are using clock_gettime(), which uses nanoseconds + * as ticks. Hence, we set the multiplier to zero, which causes pg_ticks_to_ns + * to return the original value. + */ +uint64 ticks_per_ns_scaled = 0; +uint64 max_ticks_no_overflow = 0; +bool timing_initialized = false; +int timing_clock_source = TIMING_CLOCK_SOURCE_AUTO; + +bool timing_tsc_enabled = false; +int32 timing_tsc_frequency_khz = -1; + +static void set_ticks_per_ns(void); +static void set_ticks_per_ns_system(void); + +#if PG_INSTR_TSC_CLOCK +static bool tsc_use_by_default(void); +static void set_ticks_per_ns_for_tsc(void); +#endif + +/* + * Initializes timing infrastructure. Must be called before making any use + * of INSTR* macros. + */ +void +pg_initialize_timing(void) +{ + if (timing_initialized) + return; + + set_ticks_per_ns_system(); + timing_initialized = true; +} + +bool +pg_set_timing_clock_source(TimingClockSourceType source) +{ + Assert(timing_initialized); + +#if PG_INSTR_TSC_CLOCK + pg_initialize_timing_tsc(); + + switch (source) + { + case TIMING_CLOCK_SOURCE_AUTO: + timing_tsc_enabled = (timing_tsc_frequency_khz > 0) && tsc_use_by_default(); + break; + case TIMING_CLOCK_SOURCE_SYSTEM: + timing_tsc_enabled = false; + break; + case TIMING_CLOCK_SOURCE_TSC: + /* Tell caller TSC is not usable */ + if (timing_tsc_frequency_khz <= 0) + return false; + timing_tsc_enabled = true; + break; + } +#endif + + set_ticks_per_ns(); + timing_clock_source = source; + return true; +} + +static void +set_ticks_per_ns(void) +{ +#if PG_INSTR_TSC_CLOCK + if (timing_tsc_enabled) + { + set_ticks_per_ns_for_tsc(); + return; + } +#endif + set_ticks_per_ns_system(); +} + +#ifndef WIN32 + +static void +set_ticks_per_ns_system(void) +{ + ticks_per_ns_scaled = 0; + max_ticks_no_overflow = 0; +} + +#else /* WIN32 */ + +/* GetTimerFrequency returns counts per second */ +static inline double +GetTimerFrequency(void) +{ + LARGE_INTEGER f; + + QueryPerformanceFrequency(&f); + return (double) f.QuadPart; +} + +static void +set_ticks_per_ns_system(void) +{ + ticks_per_ns_scaled = (NS_PER_S << TICKS_TO_NS_SHIFT) / GetTimerFrequency(); + max_ticks_no_overflow = PG_INT64_MAX / ticks_per_ns_scaled; +} + +#endif /* WIN32 */ + +/* TSC specific logic */ + +#if PG_INSTR_TSC_CLOCK + +static void tsc_detect_frequency(void); + +/* + * Initialize the TSC clock source by determining its usability and frequency. + * + * This can be called multiple times without causing repeated work, as + * timing_tsc_frequency_khz will be set to 0 if a prior call determined the + * TSC is not usable. On EXEC_BACKEND (Windows), the TSC frequency may also be + * set by restore_backend_variables. + */ +void +pg_initialize_timing_tsc(void) +{ + if (timing_tsc_frequency_khz < 0) + tsc_detect_frequency(); +} + +static void +set_ticks_per_ns_for_tsc(void) +{ + ticks_per_ns_scaled = ((NS_PER_S / 1000) << TICKS_TO_NS_SHIFT) / timing_tsc_frequency_khz; + max_ticks_no_overflow = PG_INT64_MAX / ticks_per_ns_scaled; +} + +/* + * Detect the TSC frequency and whether RDTSCP is available on x86-64. + * + * This can't be reliably determined at compile time, since the + * availability of an "invariant" TSC (that is not affected by CPU + * frequency changes) is dependent on the CPU architecture. Additionally, + * there are cases where TSC availability is impacted by virtualization, + * where a simple cpuid feature check would not be enough. + */ +static void +tsc_detect_frequency(void) +{ + timing_tsc_frequency_khz = 0; + + /* We require RDTSCP support and an invariant TSC, bail if not available */ + if (!x86_feature_available(PG_RDTSCP) || !x86_feature_available(PG_TSC_INVARIANT)) + return; + + /* Determine speed at which the TSC advances */ + timing_tsc_frequency_khz = x86_tsc_frequency_khz(); + if (timing_tsc_frequency_khz > 0) + return; + + /* + * CPUID did not give us the TSC frequency. We can instead measure the + * frequency by comparing ticks against walltime in a calibration loop. + */ + timing_tsc_frequency_khz = pg_tsc_calibrate_frequency(); +} + +/* + * Decides whether to use the TSC clock source if the user did not specify it + * one way or the other, and it is available (checked separately). + * + * Inspired by the Linux kernel's clocksource watchdog disable logic as updated + * in 2021 to reflect the reliability of the TSC on Intel platforms, see + * check_system_tsc_reliable() in arch/x86/kernel/tsc.c, as well as discussion + * in https://lore.kernel.org/lkml/87eekfk8bd.fsf@nanos.tec.linutronix.de/ + * and https://lore.kernel.org/lkml/87a6pimt1f.ffs@nanos.tec.linutronix.de/ + * for reference. + * + * When tsc_detect_frequency determines the TSC is viable (invariant, etc.), and + * we're on an Intel platform (determined via TSC_ADJUST), we consider the TSC + * trustworthy by default, matching the Linux kernel. + * + * On other CPU platforms (e.g. AMD), or in some virtual machines, we don't have + * an easy way to determine the TSC's reliability. If on Linux, we can check if + * TSC is the active clocksource, based on it having run the watchdog logic to + * monitor TSC correctness. For other platforms the user must explicitly enable + * it via GUC instead. + */ +static bool +tsc_use_by_default(void) +{ + if (x86_feature_available(PG_TSC_ADJUST)) + return true; + +#if defined(__linux__) + { + FILE *fp; + char buf[128]; + + fp = fopen("/sys/devices/system/clocksource/clocksource0/current_clocksource", "r"); + if (fp) + { + bool is_tsc = (fgets(buf, sizeof(buf), fp) != NULL && + strcmp(buf, "tsc\n") == 0); + + fclose(fp); + if (is_tsc) + return true; + } + } +#endif + + return false; +} + +/* + * Calibrate the TSC frequency by comparing TSC ticks against walltime. + * + * Takes initial TSC and system clock snapshots, then loops, recomputing the + * frequency each TSC_CALIBRATION_SKIPS iterations from cumulative TSC + * ticks divided by elapsed time. + * + * Once the frequency estimate stabilizes (consecutive iterations agree), we + * consider it converged and the frequency in KHz is returned. If either too + * many iterations or a time limit passes without convergence, 0 is returned. + */ +#define TSC_CALIBRATION_MAX_NS (50 * NS_PER_MS) +#define TSC_CALIBRATION_ITERATIONS 1000000 +#define TSC_CALIBRATION_SKIPS 100 +#define TSC_CALIBRATION_STABLE_CYCLES 10 +uint32 +pg_tsc_calibrate_frequency(void) +{ + instr_time initial_wall; + int64 initial_tsc; + double freq_khz = 0; + double prev_freq_khz = 0; + int stable_count = 0; + int64 prev_tsc; + int saved_clock_source = timing_clock_source; + + /* + * Frequency must be initialized to avoid recursion via + * pg_set_timing_clock_source. + */ + Assert(timing_tsc_frequency_khz >= 0); + + /* Ensure INSTR_* calls below work on system time */ + pg_set_timing_clock_source(TIMING_CLOCK_SOURCE_SYSTEM); + + INSTR_TIME_SET_CURRENT(initial_wall); + + initial_tsc = pg_rdtscp(); + prev_tsc = initial_tsc; + + for (int i = 0; i < TSC_CALIBRATION_ITERATIONS; i++) + { + instr_time now_wall; + int64 now_tsc; + int64 elapsed_ns; + int64 elapsed_ticks; + + INSTR_TIME_SET_CURRENT(now_wall); + + now_tsc = pg_rdtscp(); + + INSTR_TIME_SUBTRACT(now_wall, initial_wall); + elapsed_ns = INSTR_TIME_GET_NANOSEC(now_wall); + + /* Safety: bail out if we've taken too long */ + if (elapsed_ns >= TSC_CALIBRATION_MAX_NS) + break; + + elapsed_ticks = now_tsc - initial_tsc; + + /* + * Skip if TSC hasn't advanced, or we walked backwards for some + * reason. + */ + if (now_tsc == prev_tsc || elapsed_ns <= 0 || elapsed_ticks <= 0) + continue; + + /* + * We only measure frequency every TSC_CALIBRATION_SKIPS to avoid + * stabilizing based on just a handful of RDTSC instructions. + */ + if (i % TSC_CALIBRATION_SKIPS != 0) + continue; + + freq_khz = ((double) elapsed_ticks / elapsed_ns) * 1000 * 1000; + + /* + * Once freq_khz / prev_freq_khz is small, check if it stays that way. + * If it does for long enough, we've got a winner frequency. + */ + if (prev_freq_khz != 0 && fabs(1 - freq_khz / prev_freq_khz) < 0.0001) + { + stable_count++; + if (stable_count >= TSC_CALIBRATION_STABLE_CYCLES) + break; + } + else + stable_count = 0; + + prev_tsc = now_tsc; + prev_freq_khz = freq_khz; + } + + /* Restore the previous clock source */ + pg_set_timing_clock_source(saved_clock_source); + + if (stable_count < TSC_CALIBRATION_STABLE_CYCLES) + return 0; /* did not converge */ + + return (uint32) freq_khz; +} + +#endif /* PG_INSTR_TSC_CLOCK */ diff --git a/src/common/ip.c b/src/common/ip.c index 0e7897a5c8fe8..bdcb52b81414f 100644 --- a/src/common/ip.c +++ b/src/common/ip.c @@ -3,7 +3,7 @@ * ip.c * IPv6-aware network access. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -48,6 +48,9 @@ static int getnameinfo_unix(const struct sockaddr_un *sa, int salen, /* * pg_getaddrinfo_all - get address info for Unix, IPv4 and IPv6 sockets + * + * The API of this routine differs from the standard getaddrinfo() definition + * in that it requires a valid hintp, a null pointer is not allowed. */ int pg_getaddrinfo_all(const char *hostname, const char *servname, diff --git a/src/common/jsonapi.c b/src/common/jsonapi.c index 7dad4da65f636..12e40f2d564fd 100644 --- a/src/common/jsonapi.c +++ b/src/common/jsonapi.c @@ -3,7 +3,7 @@ * jsonapi.c * JSON parser and lexer interfaces * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -1670,9 +1670,31 @@ json_lex(JsonLexContext *lex) if (c == '-' || (c >= '0' && c <= '9')) { - /* for numbers look for possible numeric continuations */ - + /* + * Accumulate numeric continuations, respecting JSON number + * grammar: -? int [frac] [exp] + * + * We must track what parts of the number we've already seen + * so we don't over-consume. '.' is valid only once and not + * after 'e'/'E'; 'e'/'E' is valid only once; '+'/'-' are + * valid only immediately after 'e'/'E'. + */ bool numend = false; + bool seen_dot = false; + bool seen_exp = false; + char prev; + + /* Scan existing partial token for state */ + for (int j = 0; j < ptok->len; j++) + { + char pc = ptok->data[j]; + + if (pc == '.') + seen_dot = true; + else if (pc == 'e' || pc == 'E') + seen_exp = true; + } + prev = ptok->data[ptok->len - 1]; for (size_t i = 0; i < lex->input_length && !numend; i++) { @@ -1682,8 +1704,35 @@ json_lex(JsonLexContext *lex) { case '+': case '-': + if (prev != 'e' && prev != 'E') + { + numend = true; + break; + } + jsonapi_appendStringInfoCharMacro(ptok, cc); + added++; + break; + case '.': + if (seen_dot || seen_exp) + { + numend = true; + break; + } + seen_dot = true; + jsonapi_appendStringInfoCharMacro(ptok, cc); + added++; + break; case 'e': case 'E': + if (seen_exp) + { + numend = true; + break; + } + seen_exp = true; + jsonapi_appendStringInfoCharMacro(ptok, cc); + added++; + break; case '0': case '1': case '2': @@ -1694,14 +1743,14 @@ json_lex(JsonLexContext *lex) case '7': case '8': case '9': - { - jsonapi_appendStringInfoCharMacro(ptok, cc); - added++; - } + jsonapi_appendStringInfoCharMacro(ptok, cc); + added++; break; default: numend = true; } + if (!numend) + prev = cc; } } @@ -2167,9 +2216,9 @@ json_lex_string(JsonLexContext *lex) * can batch calls to jsonapi_appendBinaryStringInfo. */ while (p < end - sizeof(Vector8) && - !pg_lfind8('\\', (uint8 *) p, sizeof(Vector8)) && - !pg_lfind8('"', (uint8 *) p, sizeof(Vector8)) && - !pg_lfind8_le(31, (uint8 *) p, sizeof(Vector8))) + !pg_lfind8('\\', (const uint8 *) p, sizeof(Vector8)) && + !pg_lfind8('"', (const uint8 *) p, sizeof(Vector8)) && + !pg_lfind8_le(31, (const uint8 *) p, sizeof(Vector8))) p += sizeof(Vector8); for (; p < end; p++) diff --git a/src/common/keywords.c b/src/common/keywords.c index 8c5639e220e25..92180f383bc82 100644 --- a/src/common/keywords.c +++ b/src/common/keywords.c @@ -4,7 +4,7 @@ * PostgreSQL's list of SQL keywords * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/kwlookup.c b/src/common/kwlookup.c index 76b20c5bb4d58..1ba2e2e477e93 100644 --- a/src/common/kwlookup.c +++ b/src/common/kwlookup.c @@ -4,7 +4,7 @@ * Key word lookup for PostgreSQL * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/link-canary.c b/src/common/link-canary.c index 5fdc4858e2d19..0ae3280c680b8 100644 --- a/src/common/link-canary.c +++ b/src/common/link-canary.c @@ -2,7 +2,7 @@ * link-canary.c * Detect whether src/common functions came from frontend or backend. * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/link-canary.c diff --git a/src/common/logging.c b/src/common/logging.c index 125a172af80cf..4a69d96281b29 100644 --- a/src/common/logging.c +++ b/src/common/logging.c @@ -1,7 +1,7 @@ /*------------------------------------------------------------------------- * Logging framework for frontend programs * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * src/common/logging.c * @@ -26,6 +26,8 @@ static int log_flags; static void (*log_pre_callback) (void); static void (*log_locus_callback) (const char **, uint64 *); +static FILE *log_logfile; + static const char *sgr_error = NULL; static const char *sgr_warning = NULL; static const char *sgr_note = NULL; @@ -204,6 +206,18 @@ pg_logging_set_locus_callback(void (*cb) (const char **filename, uint64 *lineno) log_locus_callback = cb; } +void +pg_logging_set_logfile(FILE *logfile) +{ + log_logfile = logfile; +} + +void +pg_logging_unset_logfile(void) +{ + log_logfile = NULL; +} + void pg_log_generic(enum pg_log_level level, enum pg_log_part part, const char *pg_restrict fmt,...) @@ -277,6 +291,8 @@ pg_log_generic_v(enum pg_log_level level, enum pg_log_part part, if (sgr_error) fprintf(stderr, ANSI_ESCAPE_FMT, sgr_error); fprintf(stderr, _("error: ")); + if (log_logfile) + fprintf(log_logfile, _("error: ")); if (sgr_error) fprintf(stderr, ANSI_ESCAPE_RESET); break; @@ -284,6 +300,8 @@ pg_log_generic_v(enum pg_log_level level, enum pg_log_part part, if (sgr_warning) fprintf(stderr, ANSI_ESCAPE_FMT, sgr_warning); fprintf(stderr, _("warning: ")); + if (log_logfile) + fprintf(log_logfile, _("warning: ")); if (sgr_warning) fprintf(stderr, ANSI_ESCAPE_RESET); break; @@ -295,6 +313,8 @@ pg_log_generic_v(enum pg_log_level level, enum pg_log_part part, if (sgr_note) fprintf(stderr, ANSI_ESCAPE_FMT, sgr_note); fprintf(stderr, _("detail: ")); + if (log_logfile) + fprintf(log_logfile, _("detail: ")); if (sgr_note) fprintf(stderr, ANSI_ESCAPE_RESET); break; @@ -302,6 +322,8 @@ pg_log_generic_v(enum pg_log_level level, enum pg_log_part part, if (sgr_note) fprintf(stderr, ANSI_ESCAPE_FMT, sgr_note); fprintf(stderr, _("hint: ")); + if (log_logfile) + fprintf(log_logfile, _("hint: ")); if (sgr_note) fprintf(stderr, ANSI_ESCAPE_RESET); break; @@ -332,6 +354,11 @@ pg_log_generic_v(enum pg_log_level level, enum pg_log_part part, buf[required_len - 2] = '\0'; fprintf(stderr, "%s\n", buf); + if (log_logfile) + { + fprintf(log_logfile, "%s\n", buf); + fflush(log_logfile); + } free(buf); } diff --git a/src/common/md5.c b/src/common/md5.c index c8b4a204964b6..3962c80592eb4 100644 --- a/src/common/md5.c +++ b/src/common/md5.c @@ -7,7 +7,7 @@ * implementation is a simple one, in that it needs every input byte * to be buffered before doing any calculations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/md5_common.c b/src/common/md5_common.c index 057ae7a449f1c..8a9f6b7f5e9b5 100644 --- a/src/common/md5_common.c +++ b/src/common/md5_common.c @@ -6,7 +6,7 @@ * * Sverre H. Huseby * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/md5_int.h b/src/common/md5_int.h index 2706836188cbd..df59d875d0147 100644 --- a/src/common/md5_int.h +++ b/src/common/md5_int.h @@ -3,7 +3,7 @@ * md5_int.h * Internal headers for fallback implementation of MD5 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/meson.build b/src/common/meson.build index 1540ba67cca41..9bd55cda95b10 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group common_sources = files( 'archive.c', @@ -13,6 +13,7 @@ common_sources = files( 'file_perm.c', 'file_utils.c', 'hashfn.c', + 'instr_time.c', 'ip.c', 'jsonapi.c', 'keywords.c', @@ -192,6 +193,7 @@ foreach name, opts : pgcommon_variants opts.get('include_directories', []), ], 'dependencies': opts['dependencies'] + [ssl], + 'install': install_internal_static_lib and name != '_srv', } ) pgcommon += {name: lib} diff --git a/src/common/parse_manifest.c b/src/common/parse_manifest.c index 71973af199b90..5065c9bf39c03 100644 --- a/src/common/parse_manifest.c +++ b/src/common/parse_manifest.c @@ -3,7 +3,7 @@ * parse_manifest.c * Parse a backup manifest in JSON format. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/common/parse_manifest.c @@ -132,8 +132,8 @@ json_parse_manifest_incremental_init(JsonManifestParseContext *context) JsonManifestParseState *parse; pg_cryptohash_ctx *manifest_ctx; - incstate = palloc(sizeof(JsonManifestParseIncrementalState)); - parse = palloc(sizeof(JsonManifestParseState)); + incstate = palloc_object(JsonManifestParseIncrementalState); + parse = palloc_object(JsonManifestParseState); parse->context = context; parse->state = JM_EXPECT_TOPLEVEL_START; @@ -942,7 +942,7 @@ parse_xlogrecptr(XLogRecPtr *result, char *input) uint32 hi; uint32 lo; - if (sscanf(input, "%X/%X", &hi, &lo) != 2) + if (sscanf(input, "%X/%08X", &hi, &lo) != 2) return false; *result = ((uint64) hi) << 32 | lo; return true; diff --git a/src/common/percentrepl.c b/src/common/percentrepl.c index 8225a281d7d0c..525a5ba6cf095 100644 --- a/src/common/percentrepl.c +++ b/src/common/percentrepl.c @@ -3,7 +3,7 @@ * percentrepl.c * Common routines to replace percent placeholders in strings * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/pg_get_line.c b/src/common/pg_get_line.c index 8d28a0ae6dbce..5cece9f93959d 100644 --- a/src/common/pg_get_line.c +++ b/src/common/pg_get_line.c @@ -3,7 +3,7 @@ * pg_get_line.c * fgets() with an expansible result buffer * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/pg_lzcompress.c b/src/common/pg_lzcompress.c index 86354d660fdf5..c8e9ef9aa3b30 100644 --- a/src/common/pg_lzcompress.c +++ b/src/common/pg_lzcompress.c @@ -172,7 +172,7 @@ * * Jan Wieck * - * Copyright (c) 1999-2025, PostgreSQL Global Development Group + * Copyright (c) 1999-2026, PostgreSQL Global Development Group * * src/common/pg_lzcompress.c * ---------- @@ -727,22 +727,33 @@ pglz_decompress(const char *source, int32 slen, char *dest, int32 len; int32 off; + /* + * A match tag is at least 2 bytes; if the length nibble is + * 0x0f the tag is 3 bytes (extended length). Verify we have + * enough source data before reading them. + */ + if (unlikely(sp + 2 > srcend)) + return -1; + len = (sp[0] & 0x0f) + 3; off = ((sp[0] & 0xf0) << 4) | sp[1]; sp += 2; if (len == 18) + { + if (unlikely(sp >= srcend)) + return -1; len += *sp++; + } /* - * Check for corrupt data: if we fell off the end of the - * source, or if we obtained off = 0, or if off is more than - * the distance back to the buffer start, we have problems. - * (We must check for off = 0, else we risk an infinite loop - * below in the face of corrupt data. Likewise, the upper - * limit on off prevents accessing outside the buffer - * boundaries.) + * Check for corrupt data: if we obtained off = 0, or if off + * is more than the distance back to the buffer start, we have + * problems. (We must check for off = 0, else we risk an + * infinite loop below in the face of corrupt data. Likewise, + * the upper limit on off prevents accessing outside the + * buffer boundaries.) */ - if (unlikely(sp > srcend || off == 0 || + if (unlikely(off == 0 || off > (dp - (unsigned char *) dest))) return -1; diff --git a/src/common/pg_prng.c b/src/common/pg_prng.c index 370d81b31673d..8583cb89cb4a3 100644 --- a/src/common/pg_prng.c +++ b/src/common/pg_prng.c @@ -10,7 +10,7 @@ * About these generators: https://prng.di.unimi.it/ * See also https://en.wikipedia.org/wiki/List_of_random_number_generators * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * * src/common/pg_prng.c * diff --git a/src/common/pgfnames.c b/src/common/pgfnames.c index 8fb79105714b6..399238e4027a2 100644 --- a/src/common/pgfnames.c +++ b/src/common/pgfnames.c @@ -3,7 +3,7 @@ * pgfnames.c * directory handling functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -49,7 +49,7 @@ pgfnames(const char *path) return NULL; } - filenames = (char **) palloc(fnsize * sizeof(char *)); + filenames = palloc_array(char *, fnsize); while (errno = 0, (file = readdir(dir)) != NULL) { @@ -58,8 +58,7 @@ pgfnames(const char *path) if (numnames + 1 >= fnsize) { fnsize *= 2; - filenames = (char **) repalloc(filenames, - fnsize * sizeof(char *)); + filenames = repalloc_array(filenames, char *, fnsize); } filenames[numnames++] = pstrdup(file->d_name); } diff --git a/src/common/psprintf.c b/src/common/psprintf.c index 15e68599dc55f..6f006e0262f4c 100644 --- a/src/common/psprintf.c +++ b/src/common/psprintf.c @@ -4,7 +4,7 @@ * sprintf into an allocated-on-demand buffer * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/relpath.c b/src/common/relpath.c index 7dcf987afcd01..8fb3bed7873ab 100644 --- a/src/common/relpath.c +++ b/src/common/relpath.c @@ -4,7 +4,7 @@ * * This module also contains some logic associated with fork names. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/restricted_token.c b/src/common/restricted_token.c index 61884f59dea8c..5790c86b2b2ba 100644 --- a/src/common/restricted_token.c +++ b/src/common/restricted_token.c @@ -4,7 +4,7 @@ * helper routine to ensure restricted token on Windows * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/rmtree.c b/src/common/rmtree.c index 2f364f84ae5b2..e35b5e7ba73e6 100644 --- a/src/common/rmtree.c +++ b/src/common/rmtree.c @@ -2,7 +2,7 @@ * * rmtree.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -64,7 +64,7 @@ rmtree(const char *path, bool rmtopdir) return false; } - dirnames = (char **) palloc(sizeof(char *) * dirnames_capacity); + dirnames = palloc_array(char *, dirnames_capacity); while (errno = 0, (de = readdir(dir))) { diff --git a/src/common/ryu_common.h b/src/common/ryu_common.h index bb1389bac3774..6436e5ac3c96c 100644 --- a/src/common/ryu_common.h +++ b/src/common/ryu_common.h @@ -2,7 +2,7 @@ * * Common routines for Ryu floating-point output. * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/ryu_common.h diff --git a/src/common/saslprep.c b/src/common/saslprep.c index 97beb47940bfe..38d50dd823c41 100644 --- a/src/common/saslprep.c +++ b/src/common/saslprep.c @@ -12,7 +12,7 @@ * http://www.ietf.org/rfc/rfc4013.txt * * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/saslprep.c @@ -47,7 +47,7 @@ /* Prototypes for local functions */ static int codepoint_range_cmp(const void *a, const void *b); -static bool is_code_in_table(pg_wchar code, const pg_wchar *map, int mapsize); +static bool is_code_in_table(char32_t code, const char32_t *map, int mapsize); static int pg_utf8_string_len(const char *source); /* @@ -64,7 +64,7 @@ static int pg_utf8_string_len(const char *source); * * These are all mapped to the ASCII space character (U+00A0). */ -static const pg_wchar non_ascii_space_ranges[] = +static const char32_t non_ascii_space_ranges[] = { 0x00A0, 0x00A0, 0x1680, 0x1680, @@ -79,7 +79,7 @@ static const pg_wchar non_ascii_space_ranges[] = * * If any of these appear in the input, they are removed. */ -static const pg_wchar commonly_mapped_to_nothing_ranges[] = +static const char32_t commonly_mapped_to_nothing_ranges[] = { 0x00AD, 0x00AD, 0x034F, 0x034F, @@ -114,7 +114,7 @@ static const pg_wchar commonly_mapped_to_nothing_ranges[] = * tables, so one code might originate from multiple source tables. * Adjacent ranges have also been merged together, to save space. */ -static const pg_wchar prohibited_output_ranges[] = +static const char32_t prohibited_output_ranges[] = { 0x0000, 0x001F, /* C.2.1 */ 0x007F, 0x00A0, /* C.1.2, C.2.1, C.2.2 */ @@ -155,7 +155,7 @@ static const pg_wchar prohibited_output_ranges[] = }; /* A.1 Unassigned code points in Unicode 3.2 */ -static const pg_wchar unassigned_codepoint_ranges[] = +static const char32_t unassigned_codepoint_ranges[] = { 0x0221, 0x0221, 0x0234, 0x024F, @@ -556,7 +556,7 @@ static const pg_wchar unassigned_codepoint_ranges[] = }; /* D.1 Characters with bidirectional property "R" or "AL" */ -static const pg_wchar RandALCat_codepoint_ranges[] = +static const char32_t RandALCat_codepoint_ranges[] = { 0x05BE, 0x05BE, 0x05C0, 0x05C0, @@ -595,7 +595,7 @@ static const pg_wchar RandALCat_codepoint_ranges[] = }; /* D.2 Characters with bidirectional property "L" */ -static const pg_wchar LCat_codepoint_ranges[] = +static const char32_t LCat_codepoint_ranges[] = { 0x0041, 0x005A, 0x0061, 0x007A, @@ -968,8 +968,8 @@ static const pg_wchar LCat_codepoint_ranges[] = static int codepoint_range_cmp(const void *a, const void *b) { - const pg_wchar *key = (const pg_wchar *) a; - const pg_wchar *range = (const pg_wchar *) b; + const char32_t *key = (const char32_t *) a; + const char32_t *range = (const char32_t *) b; if (*key < range[0]) return -1; /* less than lower bound */ @@ -980,14 +980,14 @@ codepoint_range_cmp(const void *a, const void *b) } static bool -is_code_in_table(pg_wchar code, const pg_wchar *map, int mapsize) +is_code_in_table(char32_t code, const char32_t *map, int mapsize) { Assert(mapsize % 2 == 0); if (code < map[0] || code > map[mapsize - 1]) return false; - if (bsearch(&code, map, mapsize / 2, sizeof(pg_wchar) * 2, + if (bsearch(&code, map, mapsize / 2, sizeof(char32_t) * 2, codepoint_range_cmp)) return true; else @@ -1046,32 +1046,21 @@ pg_utf8_string_len(const char *source) pg_saslprep_rc pg_saslprep(const char *input, char **output) { - pg_wchar *input_chars = NULL; - pg_wchar *output_chars = NULL; + char32_t *input_chars = NULL; + char32_t *output_chars = NULL; int input_size; char *result; int result_size; int count; int i; bool contains_RandALCat; - unsigned char *p; - pg_wchar *wp; + const unsigned char *p; + unsigned char *outp; + char32_t *wp; /* Ensure we return *output as NULL on failure */ *output = NULL; - /* - * Quick check if the input is pure ASCII. An ASCII string requires no - * further processing. - */ - if (pg_is_ascii(input)) - { - *output = STRDUP(input); - if (!(*output)) - goto oom; - return SASLPREP_SUCCESS; - } - /* * Convert the input from UTF-8 to an array of Unicode codepoints. * @@ -1080,20 +1069,20 @@ pg_saslprep(const char *input, char **output) input_size = pg_utf8_string_len(input); if (input_size < 0) return SASLPREP_INVALID_UTF8; - if (input_size >= MaxAllocSize / sizeof(pg_wchar)) + if (input_size >= MaxAllocSize / sizeof(char32_t)) goto oom; - input_chars = ALLOC((input_size + 1) * sizeof(pg_wchar)); + input_chars = ALLOC((input_size + 1) * sizeof(char32_t)); if (!input_chars) goto oom; - p = (unsigned char *) input; + p = (const unsigned char *) input; for (i = 0; i < input_size; i++) { input_chars[i] = utf8_to_unicode(p); p += pg_utf_mblen(p); } - input_chars[i] = (pg_wchar) '\0'; + input_chars[i] = (char32_t) '\0'; /* * The steps below correspond to the steps listed in [RFC3454], Section @@ -1107,7 +1096,7 @@ pg_saslprep(const char *input, char **output) count = 0; for (i = 0; i < input_size; i++) { - pg_wchar code = input_chars[i]; + char32_t code = input_chars[i]; if (IS_CODE_IN_TABLE(code, non_ascii_space_ranges)) input_chars[count++] = 0x0020; @@ -1118,7 +1107,7 @@ pg_saslprep(const char *input, char **output) else input_chars[count++] = code; } - input_chars[count] = (pg_wchar) '\0'; + input_chars[count] = (char32_t) '\0'; input_size = count; if (input_size == 0) @@ -1138,7 +1127,7 @@ pg_saslprep(const char *input, char **output) */ for (i = 0; i < input_size; i++) { - pg_wchar code = input_chars[i]; + char32_t code = input_chars[i]; if (IS_CODE_IN_TABLE(code, prohibited_output_ranges)) goto prohibited; @@ -1170,7 +1159,7 @@ pg_saslprep(const char *input, char **output) contains_RandALCat = false; for (i = 0; i < input_size; i++) { - pg_wchar code = input_chars[i]; + char32_t code = input_chars[i]; if (IS_CODE_IN_TABLE(code, RandALCat_codepoint_ranges)) { @@ -1181,12 +1170,12 @@ pg_saslprep(const char *input, char **output) if (contains_RandALCat) { - pg_wchar first = input_chars[0]; - pg_wchar last = input_chars[input_size - 1]; + char32_t first = input_chars[0]; + char32_t last = input_chars[input_size - 1]; for (i = 0; i < input_size; i++) { - pg_wchar code = input_chars[i]; + char32_t code = input_chars[i]; if (IS_CODE_IN_TABLE(code, LCat_codepoint_ranges)) goto prohibited; @@ -1217,14 +1206,14 @@ pg_saslprep(const char *input, char **output) * There are no error exits below here, so the error exit paths don't need * to worry about possibly freeing "result". */ - p = (unsigned char *) result; + outp = (unsigned char *) result; for (wp = output_chars; *wp; wp++) { - unicode_to_utf8(*wp, p); - p += pg_utf_mblen(p); + unicode_to_utf8(*wp, outp); + outp += pg_utf_mblen(outp); } - Assert((char *) p == result + result_size); - *p = '\0'; + Assert((char *) outp == result + result_size); + *outp = '\0'; FREE(input_chars); FREE(output_chars); diff --git a/src/common/scram-common.c b/src/common/scram-common.c index e47a6ebbaab0f..259fa5554b602 100644 --- a/src/common/scram-common.c +++ b/src/common/scram-common.c @@ -6,7 +6,7 @@ * backend, for implement the Salted Challenge Response Authentication * Mechanism (SCRAM), per IETF's RFC 5802. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/scram-common.c @@ -61,7 +61,7 @@ scram_SaltedPassword(const char *password, */ /* First iteration */ - if (pg_hmac_init(hmac_ctx, (uint8 *) password, password_len) < 0 || + if (pg_hmac_init(hmac_ctx, (const uint8 *) password, password_len) < 0 || pg_hmac_update(hmac_ctx, salt, saltlen) < 0 || pg_hmac_update(hmac_ctx, (uint8 *) &one, sizeof(uint32)) < 0 || pg_hmac_final(hmac_ctx, Ui_prev, key_length) < 0) @@ -84,7 +84,7 @@ scram_SaltedPassword(const char *password, CHECK_FOR_INTERRUPTS(); #endif - if (pg_hmac_init(hmac_ctx, (uint8 *) password, password_len) < 0 || + if (pg_hmac_init(hmac_ctx, (const uint8 *) password, password_len) < 0 || pg_hmac_update(hmac_ctx, (uint8 *) Ui_prev, key_length) < 0 || pg_hmac_final(hmac_ctx, Ui, key_length) < 0) { diff --git a/src/common/sha1.c b/src/common/sha1.c index f29866f8359bc..d8989229fb609 100644 --- a/src/common/sha1.c +++ b/src/common/sha1.c @@ -5,7 +5,7 @@ * * Fallback implementation of SHA1, as specified in RFC 3174. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -277,7 +277,7 @@ sha1_result(uint8 *digest0, pg_sha1_ctx *ctx) { uint8 *digest; - digest = (uint8 *) digest0; + digest = digest0; #ifdef WORDS_BIGENDIAN memmove(digest, &ctx->h.b8[0], 20); @@ -337,7 +337,7 @@ pg_sha1_update(pg_sha1_ctx *ctx, const uint8 *data, size_t len) size_t off; size_t copysiz; - input = (const uint8 *) data; + input = data; off = 0; while (off < len) diff --git a/src/common/sha1_int.h b/src/common/sha1_int.h index a7a72fe1deb10..3b260c0d13d5b 100644 --- a/src/common/sha1_int.h +++ b/src/common/sha1_int.h @@ -3,7 +3,7 @@ * sha1_int.h * Internal headers for fallback implementation of SHA1 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/sha2.c b/src/common/sha2.c index e7c163dba561a..275d796bb97e2 100644 --- a/src/common/sha2.c +++ b/src/common/sha2.c @@ -6,7 +6,7 @@ * This includes the fallback implementation for SHA2 cryptographic * hashes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/sha2_int.h b/src/common/sha2_int.h index f9c77b856edaf..b580df077a94c 100644 --- a/src/common/sha2_int.h +++ b/src/common/sha2_int.h @@ -3,7 +3,7 @@ * sha2_int.h * Internal headers for fallback implementation of SHA{224,256,384,512} * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/sha2_int.h diff --git a/src/common/sprompt.c b/src/common/sprompt.c index 06e1b5335a4bb..257c8fe7b5674 100644 --- a/src/common/sprompt.c +++ b/src/common/sprompt.c @@ -3,7 +3,7 @@ * sprompt.c * simple_prompt() routine * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/string.c b/src/common/string.c index d8a3129c3ba8d..41c74a1502dfb 100644 --- a/src/common/string.c +++ b/src/common/string.c @@ -4,7 +4,7 @@ * string handling helpers * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/stringinfo.c b/src/common/stringinfo.c index 22d03807697ec..ae39540e4685a 100644 --- a/src/common/stringinfo.c +++ b/src/common/stringinfo.c @@ -7,7 +7,7 @@ * (null-terminated text) or arbitrary binary data. All storage is allocated * with palloc() (falling back to malloc in frontend code). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/common/stringinfo.c @@ -57,7 +57,7 @@ initStringInfoInternal(StringInfo str, int initsize) static inline StringInfo makeStringInfoInternal(int initsize) { - StringInfo res = (StringInfo) palloc(sizeof(StringInfoData)); + StringInfo res = palloc_object(StringInfoData); initStringInfoInternal(res, initsize); return res; diff --git a/src/common/unicode/README b/src/common/unicode/README index 4974c3c885df8..fdb8b66fa58b8 100644 --- a/src/common/unicode/README +++ b/src/common/unicode/README @@ -10,7 +10,7 @@ Update Unicode Version ---------------------- Edit src/Makefile.global.in and src/common/unicode/meson.build -to update the UNICODE_VERSION. +to update the UNICODE_VERSION and the CLDR_VERSION. Then, generate the new header files with: diff --git a/src/common/unicode/case_test.c b/src/common/unicode/case_test.c index fdfb62e855286..099530c1eade5 100644 --- a/src/common/unicode/case_test.c +++ b/src/common/unicode/case_test.c @@ -2,7 +2,7 @@ * case_test.c * Program to test Unicode case mapping functions. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/unicode/case_test.c @@ -24,12 +24,13 @@ #include "common/unicode_case.h" #include "common/unicode_category.h" #include "common/unicode_version.h" +#include "mb/pg_wchar.h" /* enough to hold largest source or result string, including NUL */ #define BUFSZ 256 #ifdef USE_ICU -static UCaseMap * casemap = NULL; +static UCaseMap *casemap = NULL; #endif typedef size_t (*TestFunc) (char *dst, size_t dstsize, const char *src, @@ -54,7 +55,7 @@ initcap_wbnext(void *state) while (wbstate->offset < wbstate->len && wbstate->str[wbstate->offset] != '\0') { - pg_wchar u = utf8_to_unicode((unsigned char *) wbstate->str + + char32_t u = utf8_to_unicode((const unsigned char *) wbstate->str + wbstate->offset); bool curr_alnum = pg_u_isalnum(u, wbstate->posix); @@ -77,16 +78,16 @@ initcap_wbnext(void *state) #ifdef USE_ICU static void -icu_test_simple(pg_wchar code) +icu_test_simple(char32_t code) { - pg_wchar lower = unicode_lowercase_simple(code); - pg_wchar title = unicode_titlecase_simple(code); - pg_wchar upper = unicode_uppercase_simple(code); - pg_wchar fold = unicode_casefold_simple(code); - pg_wchar iculower = u_tolower(code); - pg_wchar icutitle = u_totitle(code); - pg_wchar icuupper = u_toupper(code); - pg_wchar icufold = u_foldCase(code, U_FOLD_CASE_DEFAULT); + char32_t lower = unicode_lowercase_simple(code); + char32_t title = unicode_titlecase_simple(code); + char32_t upper = unicode_uppercase_simple(code); + char32_t fold = unicode_casefold_simple(code); + char32_t iculower = u_tolower(code); + char32_t icutitle = u_totitle(code); + char32_t icuupper = u_toupper(code); + char32_t icufold = u_foldCase(code, U_FOLD_CASE_DEFAULT); if (lower != iculower || title != icutitle || upper != icuupper || fold != icufold) @@ -172,7 +173,7 @@ test_icu(void) int successful = 0; int skipped_mismatch = 0; - for (pg_wchar code = 0; code <= 0x10ffff; code++) + for (char32_t code = 0; code <= 0x10ffff; code++) { pg_unicode_category category = unicode_category(code); @@ -303,7 +304,7 @@ tfunc_title(char *dst, size_t dstsize, const char *src, { struct WordBoundaryState wbstate = { .str = src, - .len = srclen, + .len = (srclen < 0) ? strlen(src) : srclen, .offset = 0, .init = false, .prev_alnum = false, @@ -328,7 +329,7 @@ tfunc_fold(char *dst, size_t dstsize, const char *src, } static void -test_convert_case() +test_convert_case(void) { /* test string with no case changes */ test_convert(tfunc_lower, "√∞", "√∞"); diff --git a/src/common/unicode/category_test.c b/src/common/unicode/category_test.c index 5d37ba391968e..ffb9d42d7044f 100644 --- a/src/common/unicode/category_test.c +++ b/src/common/unicode/category_test.c @@ -2,7 +2,7 @@ * category_test.c * Program to test Unicode general category and character properties. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/unicode/category_test.c @@ -22,6 +22,7 @@ #include "common/unicode_category.h" #include "common/unicode_version.h" +#include "mb/pg_wchar.h" static int pg_unicode_version = 0; #ifdef USE_ICU @@ -53,13 +54,13 @@ parse_unicode_version(const char *version) * White_Space, and Hex_Digit. */ static void -icu_test() +icu_test(void) { int successful = 0; int pg_skipped_codepoints = 0; int icu_skipped_codepoints = 0; - for (pg_wchar code = 0; code <= 0x10ffff; code++) + for (char32_t code = 0; code <= 0x10ffff; code++) { uint8_t pg_category = unicode_category(code); uint8_t icu_category = u_charType(code); diff --git a/src/common/unicode/generate-norm_test_table.pl b/src/common/unicode/generate-norm_test_table.pl index 1b401be940931..b6ce4681a3fd1 100644 --- a/src/common/unicode/generate-norm_test_table.pl +++ b/src/common/unicode/generate-norm_test_table.pl @@ -5,7 +5,7 @@ # # NormalizationTest.txt is part of the Unicode Character Database. # -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -30,7 +30,7 @@ * norm_test_table.h * Test strings for Unicode normalization. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/common/unicode/norm_test_table.h @@ -47,8 +47,8 @@ typedef struct { int linenum; - pg_wchar input[50]; - pg_wchar output[4][50]; + char32_t input[50]; + char32_t output[4][50]; } pg_unicode_test; /* test table */ diff --git a/src/common/unicode/generate-unicode_case_table.pl b/src/common/unicode/generate-unicode_case_table.pl index 5d9ddd628038c..c1b94f47f5630 100644 --- a/src/common/unicode/generate-unicode_case_table.pl +++ b/src/common/unicode/generate-unicode_case_table.pl @@ -6,7 +6,7 @@ # Input: CaseFolding.txt SpecialCasing.txt UnicodeData.txt # Output: unicode_case_table.h # -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -255,7 +255,7 @@ * unicode_case_table.h * Case mapping and information table. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_case_table.h @@ -270,7 +270,6 @@ */ #include "common/unicode_case.h" -#include "mb/pg_wchar.h" /* * The maximum number of codepoints that can result from case mapping @@ -297,7 +296,7 @@ typedef struct { int16 conditions; - pg_wchar map[NCaseKind][MAX_CASE_EXPANSION]; + char32_t map[NCaseKind][MAX_CASE_EXPANSION]; } pg_special_case; /* @@ -430,7 +429,7 @@ sub get_hash_key * The entry case_map_${kind}[case_index(codepoint)] is the mapping for the * given codepoint. */ -static const pg_wchar case_map_$kind\[$index\] = +static const char32_t case_map_$kind\[$index\] = { EOS @@ -502,7 +501,7 @@ sub get_hash_key * the offset into the mapping tables. */ static inline uint16 -case_index(pg_wchar cp) +case_index(char32_t cp) { /* Fast path for codepoints < $fastpath_limit */ if (cp < $fastpath_limit) diff --git a/src/common/unicode/generate-unicode_category_table.pl b/src/common/unicode/generate-unicode_category_table.pl index abab5cd96968c..8a524f76aef76 100644 --- a/src/common/unicode/generate-unicode_category_table.pl +++ b/src/common/unicode/generate-unicode_category_table.pl @@ -6,7 +6,7 @@ # Input: UnicodeData.txt # Output: unicode_category_table.h # -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -349,7 +349,7 @@ * unicode_category_table.h * Category table for Unicode character classification. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_category_table.h @@ -366,15 +366,15 @@ */ typedef struct { - uint32 first; /* Unicode codepoint */ - uint32 last; /* Unicode codepoint */ + char32_t first; /* Unicode codepoint */ + char32_t last; /* Unicode codepoint */ uint8 category; /* General Category */ } pg_category_range; typedef struct { - uint32 first; /* Unicode codepoint */ - uint32 last; /* Unicode codepoint */ + char32_t first; /* Unicode codepoint */ + char32_t last; /* Unicode codepoint */ } pg_unicode_range; typedef struct diff --git a/src/common/unicode/generate-unicode_east_asian_fw_table.pl b/src/common/unicode/generate-unicode_east_asian_fw_table.pl index f35a645f78557..01a7294e3538d 100644 --- a/src/common/unicode/generate-unicode_east_asian_fw_table.pl +++ b/src/common/unicode/generate-unicode_east_asian_fw_table.pl @@ -4,7 +4,7 @@ # and East Asian Fullwidth (F) characters, using Unicode data files as input. # Pass EastAsianWidth.txt as argument. The output is on stdout. # -# Copyright (c) 2019-2025, PostgreSQL Global Development Group +# Copyright (c) 2019-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/common/unicode/generate-unicode_nonspacing_table.pl b/src/common/unicode/generate-unicode_nonspacing_table.pl index 790cde33736ec..024e0a3a80c80 100644 --- a/src/common/unicode/generate-unicode_nonspacing_table.pl +++ b/src/common/unicode/generate-unicode_nonspacing_table.pl @@ -4,7 +4,7 @@ # characters, using Unicode data files as input. Pass UnicodeData.txt # as argument. The output is on stdout. # -# Copyright (c) 2019-2025, PostgreSQL Global Development Group +# Copyright (c) 2019-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/common/unicode/generate-unicode_norm_table.pl b/src/common/unicode/generate-unicode_norm_table.pl index 5e4330e458f73..4c1d105b6c5ea 100644 --- a/src/common/unicode/generate-unicode_norm_table.pl +++ b/src/common/unicode/generate-unicode_norm_table.pl @@ -6,7 +6,7 @@ # Input: UnicodeData.txt and CompositionExclusions.txt # Output: unicode_norm_table.h and unicode_norm_hashfunc.h # -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -88,7 +88,7 @@ * unicode_norm_table.h * Composition table used for Unicode normalization * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_norm_table.h @@ -131,7 +131,7 @@ * unicode_norm_hashfunc.h * Perfect hash functions used for Unicode normalization * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_norm_hashfunc.h diff --git a/src/common/unicode/generate-unicode_normprops_table.pl b/src/common/unicode/generate-unicode_normprops_table.pl index 7064abfba2581..73a6a3123ba25 100644 --- a/src/common/unicode/generate-unicode_normprops_table.pl +++ b/src/common/unicode/generate-unicode_normprops_table.pl @@ -4,7 +4,7 @@ # (see UAX #15). Pass DerivedNormalizationProps.txt as argument. The # output is on stdout. # -# Copyright (c) 2020-2025, PostgreSQL Global Development Group +# Copyright (c) 2020-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/common/unicode/generate-unicode_version.pl b/src/common/unicode/generate-unicode_version.pl index 6c3b7243c6cbe..1a772ce32de6e 100644 --- a/src/common/unicode/generate-unicode_version.pl +++ b/src/common/unicode/generate-unicode_version.pl @@ -4,7 +4,7 @@ # # Output: unicode_version.h # -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -35,7 +35,7 @@ * unicode_version.h * Unicode version used by Postgres. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_version.h diff --git a/src/common/unicode/meson.build b/src/common/unicode/meson.build index c6a4715ccc553..8a31fe40f6c0f 100644 --- a/src/common/unicode/meson.build +++ b/src/common/unicode/meson.build @@ -1,6 +1,7 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group -UNICODE_VERSION = '16.0.0' +UNICODE_VERSION = '17.0.0' +CLDR_VERSION = '48.2' unicode_data = {} unicode_baseurl = 'https://www.unicode.org/Public/@0@/ucd/@1@' @@ -22,9 +23,9 @@ foreach f : ['CompositionExclusions.txt', 'CaseFolding.txt', 'DerivedCorePropert endforeach -update_unicode_targets = [] +update_unicode_common_targets = [] -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_case_table.h', input: [unicode_data['CaseFolding.txt'], unicode_data['SpecialCasing.txt'], unicode_data['UnicodeData.txt']], output: ['unicode_case_table.h'], @@ -34,7 +35,7 @@ update_unicode_targets += \ build_by_default: false, ) -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_category_table.h', input: [unicode_data['UnicodeData.txt'], unicode_data['DerivedCoreProperties.txt'], unicode_data['PropList.txt']], output: ['unicode_category_table.h'], @@ -44,7 +45,7 @@ update_unicode_targets += \ build_by_default: false, ) -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_east_asian_fw_table.h', input: [unicode_data['EastAsianWidth.txt']], output: ['unicode_east_asian_fw_table.h'], @@ -53,7 +54,7 @@ update_unicode_targets += \ capture: true, ) -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_nonspacing_table.h', input: [unicode_data['UnicodeData.txt']], output: ['unicode_nonspacing_table.h'], @@ -63,7 +64,7 @@ update_unicode_targets += \ capture: true, ) -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_norm_table.h', input: [unicode_data['UnicodeData.txt'], unicode_data['CompositionExclusions.txt']], output: ['unicode_norm_table.h', 'unicode_norm_hashfunc.h'], @@ -74,7 +75,7 @@ update_unicode_targets += \ build_by_default: false, ) -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_normprops_table.h', input: [unicode_data['DerivedNormalizationProps.txt']], output: ['unicode_normprops_table.h'], @@ -84,7 +85,7 @@ update_unicode_targets += \ capture: true, ) -update_unicode_targets += \ +update_unicode_common_targets += \ custom_target('unicode_version.h', output: ['unicode_version.h'], command: [ @@ -140,7 +141,7 @@ update_unicode_dep = [] if not meson.is_cross_build() update_unicode_dep += custom_target('case_test.run', output: 'case_test.run', - input: update_unicode_targets, + input: update_unicode_common_targets, command: [case_test, UNICODE_VERSION], build_by_default: false, build_always_stale: true, @@ -150,7 +151,7 @@ endif if not meson.is_cross_build() update_unicode_dep += custom_target('category_test.run', output: 'category_test.run', - input: update_unicode_targets, + input: update_unicode_common_targets, command: [category_test, UNICODE_VERSION], build_by_default: false, build_always_stale: true, @@ -160,7 +161,7 @@ endif if not meson.is_cross_build() update_unicode_dep += custom_target('norm_test.run', output: 'norm_test.run', - input: update_unicode_targets, + input: update_unicode_common_targets, command: [norm_test], build_by_default: false, build_always_stale: true, @@ -170,13 +171,13 @@ endif # Use a custom target, as run targets serialize the output, making this harder # to debug, and don't deal well with targets with multiple outputs. -update_unicode = custom_target('update-unicode', +update_unicode_common = custom_target('update-unicode', depends: update_unicode_dep, output: ['dont-exist'], - input: update_unicode_targets, + input: update_unicode_common_targets, command: [cp, '@INPUT@', '@SOURCE_ROOT@/src/include/common/'], build_by_default: false, build_always_stale: true, ) -alias_target('update-unicode', update_unicode) +update_unicode_targets += update_unicode_common diff --git a/src/common/unicode/norm_test.c b/src/common/unicode/norm_test.c index 25bc59463f24d..a7630f805740b 100644 --- a/src/common/unicode/norm_test.c +++ b/src/common/unicode/norm_test.c @@ -2,7 +2,7 @@ * norm_test.c * Program to test Unicode normalization functions. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/unicode/norm_test.c @@ -20,7 +20,7 @@ #include "norm_test_table.h" static char * -print_wchar_str(const pg_wchar *s) +print_wchar_str(const char32_t *s) { #define BUF_DIGITS 50 static char buf[BUF_DIGITS * 11 + 1]; @@ -41,7 +41,7 @@ print_wchar_str(const pg_wchar *s) } static int -pg_wcscmp(const pg_wchar *s1, const pg_wchar *s2) +pg_wcscmp(const char32_t *s1, const char32_t *s2) { for (;;) { @@ -65,7 +65,7 @@ main(int argc, char **argv) { for (int form = 0; form < 4; form++) { - pg_wchar *result; + char32_t *result; result = unicode_normalize(form, test->input); diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c index 073faf6a0d58b..0b8d3ffc0b4dd 100644 --- a/src/common/unicode_case.c +++ b/src/common/unicode_case.c @@ -2,7 +2,7 @@ * unicode_case.c * Unicode case mapping and case conversion. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/unicode_case.c @@ -30,7 +30,7 @@ enum CaseMapResult /* * Map for each case kind. */ -static const pg_wchar *const casekind_map[NCaseKind] = +static const char32_t *const casekind_map[NCaseKind] = { [CaseLower] = case_map_lower, [CaseTitle] = case_map_title, @@ -38,42 +38,42 @@ static const pg_wchar *const casekind_map[NCaseKind] = [CaseFold] = case_map_fold, }; -static pg_wchar find_case_map(pg_wchar ucs, const pg_wchar *map); +static char32_t find_case_map(char32_t ucs, const char32_t *map); static size_t convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen, CaseKind str_casekind, bool full, WordBoundaryNext wbnext, void *wbstate); -static enum CaseMapResult casemap(pg_wchar u1, CaseKind casekind, bool full, +static enum CaseMapResult casemap(char32_t u1, CaseKind casekind, bool full, const char *src, size_t srclen, size_t srcoff, - pg_wchar *simple, const pg_wchar **special); + char32_t *simple, const char32_t **special); -pg_wchar -unicode_lowercase_simple(pg_wchar code) +char32_t +unicode_lowercase_simple(char32_t code) { - pg_wchar cp = find_case_map(code, case_map_lower); + char32_t cp = find_case_map(code, case_map_lower); return cp != 0 ? cp : code; } -pg_wchar -unicode_titlecase_simple(pg_wchar code) +char32_t +unicode_titlecase_simple(char32_t code) { - pg_wchar cp = find_case_map(code, case_map_title); + char32_t cp = find_case_map(code, case_map_title); return cp != 0 ? cp : code; } -pg_wchar -unicode_uppercase_simple(pg_wchar code) +char32_t +unicode_uppercase_simple(char32_t code) { - pg_wchar cp = find_case_map(code, case_map_upper); + char32_t cp = find_case_map(code, case_map_upper); return cp != 0 ? cp : code; } -pg_wchar -unicode_casefold_simple(pg_wchar code) +char32_t +unicode_casefold_simple(char32_t code) { - pg_wchar cp = find_case_map(code, case_map_fold); + char32_t cp = find_case_map(code, case_map_fold); return cp != 0 ? cp : code; } @@ -231,10 +231,10 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen, while ((srclen < 0 || srcoff < srclen) && src[srcoff] != '\0') { - pg_wchar u1 = utf8_to_unicode((unsigned char *) src + srcoff); + char32_t u1 = utf8_to_unicode((const unsigned char *) src + srcoff); int u1len = unicode_utf8len(u1); - pg_wchar simple = 0; - const pg_wchar *special = NULL; + char32_t simple = 0; + const char32_t *special = NULL; enum CaseMapResult casemap_result; if (str_casekind == CaseTitle) @@ -265,8 +265,8 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen, case CASEMAP_SIMPLE: { /* replace with single character */ - pg_wchar u2 = simple; - pg_wchar u2len = unicode_utf8len(u2); + char32_t u2 = simple; + char32_t u2len = unicode_utf8len(u2); Assert(special == NULL); if (result_len + u2len <= dstsize) @@ -280,7 +280,7 @@ convert_case(char *dst, size_t dstsize, const char *src, ssize_t srclen, Assert(simple == 0); for (int i = 0; i < MAX_CASE_EXPANSION && special[i]; i++) { - pg_wchar u2 = special[i]; + char32_t u2 = special[i]; size_t u2len = unicode_utf8len(u2); if (result_len + u2len <= dstsize) @@ -320,7 +320,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) { if ((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0) { - pg_wchar curr = utf8_to_unicode(str + i); + char32_t curr = utf8_to_unicode(str + i); if (pg_u_prop_case_ignorable(curr)) continue; @@ -344,7 +344,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset) { if ((str[i] & 0x80) == 0 || (str[i] & 0xC0) == 0xC0) { - pg_wchar curr = utf8_to_unicode(str + i); + char32_t curr = utf8_to_unicode(str + i); if (pg_u_prop_case_ignorable(curr)) continue; @@ -373,7 +373,7 @@ check_special_conditions(int conditions, const char *str, size_t len, if (conditions == 0) return true; else if (conditions == PG_U_FINAL_SIGMA) - return check_final_sigma((unsigned char *) str, len, offset); + return check_final_sigma((const unsigned char *) str, len, offset); /* no other conditions supported */ Assert(false); @@ -394,9 +394,9 @@ check_special_conditions(int conditions, const char *str, size_t len, * character without modification. */ static enum CaseMapResult -casemap(pg_wchar u1, CaseKind casekind, bool full, +casemap(char32_t u1, CaseKind casekind, bool full, const char *src, size_t srclen, size_t srcoff, - pg_wchar *simple, const pg_wchar **special) + char32_t *simple, const char32_t **special) { uint16 idx; @@ -434,8 +434,8 @@ casemap(pg_wchar u1, CaseKind casekind, bool full, * Find entry in simple case map. * If the entry does not exist, 0 will be returned. */ -static pg_wchar -find_case_map(pg_wchar ucs, const pg_wchar *map) +static char32_t +find_case_map(char32_t ucs, const char32_t *map) { /* Fast path for codepoints < 0x80 */ if (ucs < 0x80) diff --git a/src/common/unicode_category.c b/src/common/unicode_category.c index 4136c4d4f926f..0d63e07314f6b 100644 --- a/src/common/unicode_category.c +++ b/src/common/unicode_category.c @@ -1,10 +1,10 @@ /*------------------------------------------------------------------------- * unicode_category.c * Determine general category and character properties of Unicode - * characters. Encoding must be UTF8, where we assume that the pg_wchar + * characters. Encoding must be UTF8, where we assume that the char32_t * representation is a code point. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/unicode_category.c @@ -76,13 +76,13 @@ #define PG_U_CHARACTER_TAB 0x09 static bool range_search(const pg_unicode_range *tbl, size_t size, - pg_wchar code); + char32_t code); /* * Unicode general category for the given codepoint. */ pg_unicode_category -unicode_category(pg_wchar code) +unicode_category(char32_t code) { int min = 0; int mid; @@ -108,7 +108,7 @@ unicode_category(pg_wchar code) } bool -pg_u_prop_alphabetic(pg_wchar code) +pg_u_prop_alphabetic(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_ALPHABETIC; @@ -119,7 +119,7 @@ pg_u_prop_alphabetic(pg_wchar code) } bool -pg_u_prop_lowercase(pg_wchar code) +pg_u_prop_lowercase(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_LOWERCASE; @@ -130,7 +130,7 @@ pg_u_prop_lowercase(pg_wchar code) } bool -pg_u_prop_uppercase(pg_wchar code) +pg_u_prop_uppercase(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_UPPERCASE; @@ -141,7 +141,7 @@ pg_u_prop_uppercase(pg_wchar code) } bool -pg_u_prop_cased(pg_wchar code) +pg_u_prop_cased(char32_t code) { uint32 category_mask; @@ -156,7 +156,7 @@ pg_u_prop_cased(pg_wchar code) } bool -pg_u_prop_case_ignorable(pg_wchar code) +pg_u_prop_case_ignorable(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_CASE_IGNORABLE; @@ -167,7 +167,7 @@ pg_u_prop_case_ignorable(pg_wchar code) } bool -pg_u_prop_white_space(pg_wchar code) +pg_u_prop_white_space(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_WHITE_SPACE; @@ -178,7 +178,7 @@ pg_u_prop_white_space(pg_wchar code) } bool -pg_u_prop_hex_digit(pg_wchar code) +pg_u_prop_hex_digit(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_HEX_DIGIT; @@ -189,7 +189,7 @@ pg_u_prop_hex_digit(pg_wchar code) } bool -pg_u_prop_join_control(pg_wchar code) +pg_u_prop_join_control(char32_t code) { if (code < 0x80) return unicode_opt_ascii[code].properties & PG_U_PROP_JOIN_CONTROL; @@ -208,7 +208,7 @@ pg_u_prop_join_control(pg_wchar code) */ bool -pg_u_isdigit(pg_wchar code, bool posix) +pg_u_isdigit(char32_t code, bool posix) { if (posix) return ('0' <= code && code <= '9'); @@ -217,19 +217,19 @@ pg_u_isdigit(pg_wchar code, bool posix) } bool -pg_u_isalpha(pg_wchar code) +pg_u_isalpha(char32_t code) { return pg_u_prop_alphabetic(code); } bool -pg_u_isalnum(pg_wchar code, bool posix) +pg_u_isalnum(char32_t code, bool posix) { return pg_u_isalpha(code) || pg_u_isdigit(code, posix); } bool -pg_u_isword(pg_wchar code) +pg_u_isword(char32_t code) { uint32 category_mask = PG_U_CATEGORY_MASK(unicode_category(code)); @@ -240,32 +240,32 @@ pg_u_isword(pg_wchar code) } bool -pg_u_isupper(pg_wchar code) +pg_u_isupper(char32_t code) { return pg_u_prop_uppercase(code); } bool -pg_u_islower(pg_wchar code) +pg_u_islower(char32_t code) { return pg_u_prop_lowercase(code); } bool -pg_u_isblank(pg_wchar code) +pg_u_isblank(char32_t code) { return code == PG_U_CHARACTER_TAB || unicode_category(code) == PG_U_SPACE_SEPARATOR; } bool -pg_u_iscntrl(pg_wchar code) +pg_u_iscntrl(char32_t code) { return unicode_category(code) == PG_U_CONTROL; } bool -pg_u_isgraph(pg_wchar code) +pg_u_isgraph(char32_t code) { uint32 category_mask = PG_U_CATEGORY_MASK(unicode_category(code)); @@ -276,7 +276,7 @@ pg_u_isgraph(pg_wchar code) } bool -pg_u_isprint(pg_wchar code) +pg_u_isprint(char32_t code) { pg_unicode_category category = unicode_category(code); @@ -287,7 +287,7 @@ pg_u_isprint(pg_wchar code) } bool -pg_u_ispunct(pg_wchar code, bool posix) +pg_u_ispunct(char32_t code, bool posix) { uint32 category_mask; @@ -308,13 +308,13 @@ pg_u_ispunct(pg_wchar code, bool posix) } bool -pg_u_isspace(pg_wchar code) +pg_u_isspace(char32_t code) { return pg_u_prop_white_space(code); } bool -pg_u_isxdigit(pg_wchar code, bool posix) +pg_u_isxdigit(char32_t code, bool posix) { if (posix) return (('0' <= code && code <= '9') || @@ -478,7 +478,7 @@ unicode_category_abbrev(pg_unicode_category category) * given table. */ static bool -range_search(const pg_unicode_range *tbl, size_t size, pg_wchar code) +range_search(const pg_unicode_range *tbl, size_t size, char32_t code) { int min = 0; int mid; diff --git a/src/common/unicode_norm.c b/src/common/unicode_norm.c index 6654b4cbc49cf..238002e6a3352 100644 --- a/src/common/unicode_norm.c +++ b/src/common/unicode_norm.c @@ -5,7 +5,7 @@ * This implements Unicode normalization, per the documentation at * https://www.unicode.org/reports/tr15/. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/unicode_norm.c @@ -69,7 +69,7 @@ conv_compare(const void *p1, const void *p2) * lookup, while the frontend version uses a binary search. */ static const pg_unicode_decomposition * -get_code_entry(pg_wchar code) +get_code_entry(char32_t code) { #ifndef FRONTEND int h; @@ -109,12 +109,12 @@ get_code_entry(pg_wchar code) * Get the combining class of the given codepoint. */ static uint8 -get_canonical_class(pg_wchar code) +get_canonical_class(char32_t code) { const pg_unicode_decomposition *entry = get_code_entry(code); /* - * If no entries are found, the character used is either an Hangul + * If no entries are found, the character used is either a Hangul * character or a character with a class of 0 and no decompositions. */ if (!entry) @@ -130,15 +130,15 @@ get_canonical_class(pg_wchar code) * Note: the returned pointer can point to statically allocated buffer, and * is only valid until next call to this function! */ -static const pg_wchar * +static const char32_t * get_code_decomposition(const pg_unicode_decomposition *entry, int *dec_size) { - static pg_wchar x; + static char32_t x; if (DECOMPOSITION_IS_INLINE(entry)) { Assert(DECOMPOSITION_SIZE(entry) == 1); - x = (pg_wchar) entry->dec_index; + x = (char32_t) entry->dec_index; *dec_size = 1; return &x; } @@ -156,7 +156,7 @@ get_code_decomposition(const pg_unicode_decomposition *entry, int *dec_size) * are, in turn, decomposable. */ static int -get_decomposed_size(pg_wchar code, bool compat) +get_decomposed_size(char32_t code, bool compat) { const pg_unicode_decomposition *entry; int size = 0; @@ -318,7 +318,7 @@ recompose_code(uint32 start, uint32 code, uint32 *result) * in the array result. */ static void -decompose_code(pg_wchar code, bool compat, pg_wchar **result, int *current) +decompose_code(char32_t code, bool compat, char32_t **result, int *current) { const pg_unicode_decomposition *entry; int i; @@ -337,7 +337,7 @@ decompose_code(pg_wchar code, bool compat, pg_wchar **result, int *current) v, tindex, sindex; - pg_wchar *res = *result; + char32_t *res = *result; sindex = code - SBASE; l = LBASE + sindex / (VCOUNT * TCOUNT); @@ -369,7 +369,7 @@ decompose_code(pg_wchar code, bool compat, pg_wchar **result, int *current) if (entry == NULL || DECOMPOSITION_SIZE(entry) == 0 || (!compat && DECOMPOSITION_IS_COMPAT(entry))) { - pg_wchar *res = *result; + char32_t *res = *result; res[*current] = code; (*current)++; @@ -382,7 +382,7 @@ decompose_code(pg_wchar code, bool compat, pg_wchar **result, int *current) decomp = get_code_decomposition(entry, &dec_size); for (i = 0; i < dec_size; i++) { - pg_wchar lcode = (pg_wchar) decomp[i]; + char32_t lcode = (char32_t) decomp[i]; /* Leave if no more decompositions */ decompose_code(lcode, compat, result, current); @@ -398,17 +398,17 @@ decompose_code(pg_wchar code, bool compat, pg_wchar **result, int *current) * malloc. Or NULL if we run out of memory. In backend, the returned * string is palloc'd instead, and OOM is reported with ereport(). */ -pg_wchar * -unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) +char32_t * +unicode_normalize(UnicodeNormalizationForm form, const char32_t *input) { bool compat = (form == UNICODE_NFKC || form == UNICODE_NFKD); bool recompose = (form == UNICODE_NFC || form == UNICODE_NFKC); - pg_wchar *decomp_chars; - pg_wchar *recomp_chars; + char32_t *decomp_chars; + char32_t *recomp_chars; int decomp_size, current_size; int count; - const pg_wchar *p; + const char32_t *p; /* variables for recomposition */ int last_class; @@ -425,7 +425,7 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) for (p = input; *p; p++) decomp_size += get_decomposed_size(*p, compat); - decomp_chars = (pg_wchar *) ALLOC((decomp_size + 1) * sizeof(pg_wchar)); + decomp_chars = (char32_t *) ALLOC((decomp_size + 1) * sizeof(char32_t)); if (decomp_chars == NULL) return NULL; @@ -448,9 +448,9 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) */ for (count = 1; count < decomp_size; count++) { - pg_wchar prev = decomp_chars[count - 1]; - pg_wchar next = decomp_chars[count]; - pg_wchar tmp; + char32_t prev = decomp_chars[count - 1]; + char32_t next = decomp_chars[count]; + char32_t tmp; const uint8 prevClass = get_canonical_class(prev); const uint8 nextClass = get_canonical_class(next); @@ -487,7 +487,7 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) * longer than the decomposed one, so make the allocation of the output * string based on that assumption. */ - recomp_chars = (pg_wchar *) ALLOC((decomp_size + 1) * sizeof(pg_wchar)); + recomp_chars = (char32_t *) ALLOC((decomp_size + 1) * sizeof(char32_t)); if (!recomp_chars) { FREE(decomp_chars); @@ -501,9 +501,9 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) for (count = 1; count < decomp_size; count++) { - pg_wchar ch = decomp_chars[count]; + char32_t ch = decomp_chars[count]; int ch_class = get_canonical_class(ch); - pg_wchar composite; + char32_t composite; if (last_class < ch_class && recompose_code(starter_ch, ch, &composite)) @@ -524,7 +524,7 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) recomp_chars[target_pos++] = ch; } } - recomp_chars[target_pos] = (pg_wchar) '\0'; + recomp_chars[target_pos] = (char32_t) '\0'; FREE(decomp_chars); @@ -540,7 +540,7 @@ unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input) #ifndef FRONTEND static const pg_unicode_normprops * -qc_hash_lookup(pg_wchar ch, const pg_unicode_norminfo *norminfo) +qc_hash_lookup(char32_t ch, const pg_unicode_norminfo *norminfo) { int h; uint32 hashkey; @@ -571,7 +571,7 @@ qc_hash_lookup(pg_wchar ch, const pg_unicode_norminfo *norminfo) * Look up the normalization quick check character property */ static UnicodeNormalizationQC -qc_is_allowed(UnicodeNormalizationForm form, pg_wchar ch) +qc_is_allowed(UnicodeNormalizationForm form, char32_t ch) { const pg_unicode_normprops *found = NULL; @@ -595,7 +595,7 @@ qc_is_allowed(UnicodeNormalizationForm form, pg_wchar ch) } UnicodeNormalizationQC -unicode_is_normalized_quickcheck(UnicodeNormalizationForm form, const pg_wchar *input) +unicode_is_normalized_quickcheck(UnicodeNormalizationForm form, const char32_t *input) { uint8 lastCanonicalClass = 0; UnicodeNormalizationQC result = UNICODE_NORM_QC_YES; @@ -610,9 +610,9 @@ unicode_is_normalized_quickcheck(UnicodeNormalizationForm form, const pg_wchar * if (form == UNICODE_NFD || form == UNICODE_NFKD) return UNICODE_NORM_QC_MAYBE; - for (const pg_wchar *p = input; *p; p++) + for (const char32_t *p = input; *p; p++) { - pg_wchar ch = *p; + char32_t ch = *p; uint8 canonicalClass; UnicodeNormalizationQC check; diff --git a/src/common/username.c b/src/common/username.c index ae5f02d96b3f7..66ddf3e68c40c 100644 --- a/src/common/username.c +++ b/src/common/username.c @@ -3,7 +3,7 @@ * username.c * get user name * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/common/wait_error.c b/src/common/wait_error.c index 1477887dee844..dd1bc3f03f1e7 100644 --- a/src/common/wait_error.c +++ b/src/common/wait_error.c @@ -4,7 +4,7 @@ * Convert a wait/waitpid(2) result code to a human-readable string * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/common/wchar.c b/src/common/wchar.c index a4bc29921dedd..4c77e3e1dc81c 100644 --- a/src/common/wchar.c +++ b/src/common/wchar.c @@ -3,7 +3,7 @@ * wchar.c * Functions for working with multibyte characters in various encodings. * - * Portions Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1998-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/wchar.c @@ -26,9 +26,9 @@ * this pair specifically. Byte pair range constraints, in encoding * originator documentation, always excluded this pair. No core conversion * could translate it. However, longstanding verifychar implementations - * accepted any non-NUL byte. big5_to_euc_tw and big5_to_mic even translate - * pairs not valid per encoding originator documentation. To avoid tightening - * core or non-core conversions in a security patch, we sought this one pair. + * accepted any non-NUL byte. big5_to_euc_tw even translates pairs not + * valid per encoding originator documentation. To avoid tightening core + * or non-core conversions in a security patch, we sought this one pair. * * PQescapeString() historically used spaces for BYTE1; many other values * could suffice for BYTE1. @@ -63,6 +63,9 @@ * subset to the ASCII routines to ensure consistency. */ +/* No error-reporting facility. Ignore incomplete trailing byte sequence. */ +#define MB2CHAR_NEED_AT_LEAST(len, need) if ((len) < (need)) break + /* * SQL/ASCII */ @@ -108,22 +111,24 @@ pg_euc2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) while (len > 0 && *from) { - if (*from == SS2 && len >= 2) /* JIS X 0201 (so called "1 byte - * KANA") */ + if (*from == SS2) /* JIS X 0201 (so called "1 byte KANA") */ { + MB2CHAR_NEED_AT_LEAST(len, 2); from++; *to = (SS2 << 8) | *from++; len -= 2; } - else if (*from == SS3 && len >= 3) /* JIS X 0212 KANJI */ + else if (*from == SS3) /* JIS X 0212 KANJI */ { + MB2CHAR_NEED_AT_LEAST(len, 3); from++; *to = (SS3 << 16) | (*from++ << 8); *to |= *from++; len -= 3; } - else if (IS_HIGHBIT_SET(*from) && len >= 2) /* JIS X 0208 KANJI */ + else if (IS_HIGHBIT_SET(*from)) /* JIS X 0208 KANJI */ { + MB2CHAR_NEED_AT_LEAST(len, 2); *to = *from++ << 8; *to |= *from++; len -= 2; @@ -235,22 +240,25 @@ pg_euccn2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) while (len > 0 && *from) { - if (*from == SS2 && len >= 3) /* code set 2 (unused?) */ + if (*from == SS2) /* code set 2 (unused?) */ { + MB2CHAR_NEED_AT_LEAST(len, 3); from++; *to = (SS2 << 16) | (*from++ << 8); *to |= *from++; len -= 3; } - else if (*from == SS3 && len >= 3) /* code set 3 (unused ?) */ + else if (*from == SS3) /* code set 3 (unused ?) */ { + MB2CHAR_NEED_AT_LEAST(len, 3); from++; *to = (SS3 << 16) | (*from++ << 8); *to |= *from++; len -= 3; } - else if (IS_HIGHBIT_SET(*from) && len >= 2) /* code set 1 */ + else if (IS_HIGHBIT_SET(*from)) /* code set 1 */ { + MB2CHAR_NEED_AT_LEAST(len, 2); *to = *from++ << 8; *to |= *from++; len -= 2; @@ -267,12 +275,22 @@ pg_euccn2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) return cnt; } +/* + * mbverifychar does not accept SS2 or SS3 (CS2 and CS3 are not defined for + * EUC_CN), but mb2wchar_with_len does. Tell a coherent story for code that + * relies on agreement between mb2wchar_with_len and mblen. Invalid text + * datums (e.g. from shared catalogs) reach this. + */ static int pg_euccn_mblen(const unsigned char *s) { int len; - if (IS_HIGHBIT_SET(*s)) + if (*s == SS2) + len = 3; + else if (*s == SS3) + len = 3; + else if (IS_HIGHBIT_SET(*s)) len = 2; else len = 1; @@ -302,23 +320,26 @@ pg_euctw2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) while (len > 0 && *from) { - if (*from == SS2 && len >= 4) /* code set 2 */ + if (*from == SS2) /* code set 2 */ { + MB2CHAR_NEED_AT_LEAST(len, 4); from++; *to = (((uint32) SS2) << 24) | (*from++ << 16); *to |= *from++ << 8; *to |= *from++; len -= 4; } - else if (*from == SS3 && len >= 3) /* code set 3 (unused?) */ + else if (*from == SS3) /* code set 3 (unused?) */ { + MB2CHAR_NEED_AT_LEAST(len, 3); from++; *to = (SS3 << 16) | (*from++ << 8); *to |= *from++; len -= 3; } - else if (IS_HIGHBIT_SET(*from) && len >= 2) /* code set 2 */ + else if (IS_HIGHBIT_SET(*from)) /* code set 2 */ { + MB2CHAR_NEED_AT_LEAST(len, 2); *to = *from++ << 8; *to |= *from++; len -= 2; @@ -455,8 +476,7 @@ pg_utf2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) } else if ((*from & 0xe0) == 0xc0) { - if (len < 2) - break; /* drop trailing incomplete char */ + MB2CHAR_NEED_AT_LEAST(len, 2); c1 = *from++ & 0x1f; c2 = *from++ & 0x3f; *to = (c1 << 6) | c2; @@ -464,8 +484,7 @@ pg_utf2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) } else if ((*from & 0xf0) == 0xe0) { - if (len < 3) - break; /* drop trailing incomplete char */ + MB2CHAR_NEED_AT_LEAST(len, 3); c1 = *from++ & 0x0f; c2 = *from++ & 0x3f; c3 = *from++ & 0x3f; @@ -474,8 +493,7 @@ pg_utf2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) } else if ((*from & 0xf8) == 0xf0) { - if (len < 4) - break; /* drop trailing incomplete char */ + MB2CHAR_NEED_AT_LEAST(len, 4); c1 = *from++ & 0x07; c2 = *from++ & 0x3f; c3 = *from++ & 0x3f; @@ -664,174 +682,6 @@ pg_utf_dsplen(const unsigned char *s) return ucs_wcwidth(utf8_to_unicode(s)); } -/* - * convert mule internal code to pg_wchar - * caller should allocate enough space for "to" - * len: length of from. - * "from" not necessarily null terminated. - */ -static int -pg_mule2wchar_with_len(const unsigned char *from, pg_wchar *to, int len) -{ - int cnt = 0; - - while (len > 0 && *from) - { - if (IS_LC1(*from) && len >= 2) - { - *to = *from++ << 16; - *to |= *from++; - len -= 2; - } - else if (IS_LCPRV1(*from) && len >= 3) - { - from++; - *to = *from++ << 16; - *to |= *from++; - len -= 3; - } - else if (IS_LC2(*from) && len >= 3) - { - *to = *from++ << 16; - *to |= *from++ << 8; - *to |= *from++; - len -= 3; - } - else if (IS_LCPRV2(*from) && len >= 4) - { - from++; - *to = *from++ << 16; - *to |= *from++ << 8; - *to |= *from++; - len -= 4; - } - else - { /* assume ASCII */ - *to = (unsigned char) *from++; - len--; - } - to++; - cnt++; - } - *to = 0; - return cnt; -} - -/* - * convert pg_wchar to mule internal code - * caller should allocate enough space for "to" - * len: length of from. - * "from" not necessarily null terminated. - */ -static int -pg_wchar2mule_with_len(const pg_wchar *from, unsigned char *to, int len) -{ - int cnt = 0; - - while (len > 0 && *from) - { - unsigned char lb; - - lb = (*from >> 16) & 0xff; - if (IS_LC1(lb)) - { - *to++ = lb; - *to++ = *from & 0xff; - cnt += 2; - } - else if (IS_LC2(lb)) - { - *to++ = lb; - *to++ = (*from >> 8) & 0xff; - *to++ = *from & 0xff; - cnt += 3; - } - else if (IS_LCPRV1_A_RANGE(lb)) - { - *to++ = LCPRV1_A; - *to++ = lb; - *to++ = *from & 0xff; - cnt += 3; - } - else if (IS_LCPRV1_B_RANGE(lb)) - { - *to++ = LCPRV1_B; - *to++ = lb; - *to++ = *from & 0xff; - cnt += 3; - } - else if (IS_LCPRV2_A_RANGE(lb)) - { - *to++ = LCPRV2_A; - *to++ = lb; - *to++ = (*from >> 8) & 0xff; - *to++ = *from & 0xff; - cnt += 4; - } - else if (IS_LCPRV2_B_RANGE(lb)) - { - *to++ = LCPRV2_B; - *to++ = lb; - *to++ = (*from >> 8) & 0xff; - *to++ = *from & 0xff; - cnt += 4; - } - else - { - *to++ = *from & 0xff; - cnt += 1; - } - from++; - len--; - } - *to = 0; - return cnt; -} - -/* exported for direct use by conv.c */ -int -pg_mule_mblen(const unsigned char *s) -{ - int len; - - if (IS_LC1(*s)) - len = 2; - else if (IS_LCPRV1(*s)) - len = 3; - else if (IS_LC2(*s)) - len = 3; - else if (IS_LCPRV2(*s)) - len = 4; - else - len = 1; /* assume ASCII */ - return len; -} - -static int -pg_mule_dsplen(const unsigned char *s) -{ - int len; - - /* - * Note: it's not really appropriate to assume that all multibyte charsets - * are double-wide on screen. But this seems an okay approximation for - * the MULE charsets we currently support. - */ - - if (IS_LC1(*s)) - len = 1; - else if (IS_LCPRV1(*s)) - len = 1; - else if (IS_LC2(*s)) - len = 2; - else if (IS_LCPRV2(*s)) - len = 2; - else - len = 1; /* assume ASCII */ - - return len; -} - /* * ISO8859-1 */ @@ -1356,56 +1206,6 @@ pg_johab_verifystr(const unsigned char *s, int len) return s - start; } -static int -pg_mule_verifychar(const unsigned char *s, int len) -{ - int l, - mbl; - unsigned char c; - - l = mbl = pg_mule_mblen(s); - - if (len < l) - return -1; - - while (--l > 0) - { - c = *++s; - if (!IS_HIGHBIT_SET(c)) - return -1; - } - return mbl; -} - -static int -pg_mule_verifystr(const unsigned char *s, int len) -{ - const unsigned char *start = s; - - while (len > 0) - { - int l; - - /* fast path for ASCII-subset characters */ - if (!IS_HIGHBIT_SET(*s)) - { - if (*s == '\0') - break; - l = 1; - } - else - { - l = pg_mule_verifychar(s, len); - if (l == -1) - break; - } - s += l; - len -= l; - } - - return s - start; -} - static int pg_latin1_verifychar(const unsigned char *s, int len) { @@ -1999,12 +1799,12 @@ pg_utf8_islegal(const unsigned char *source, int length) a = source[3]; if (a < 0x80 || a > 0xBF) return false; - /* FALL THRU */ + pg_fallthrough; case 3: a = source[2]; if (a < 0x80 || a > 0xBF) return false; - /* FALL THRU */ + pg_fallthrough; case 2: a = source[1]; switch (*source) @@ -2030,7 +1830,7 @@ pg_utf8_islegal(const unsigned char *source, int length) return false; break; } - /* FALL THRU */ + pg_fallthrough; case 1: a = *source; if (a >= 0x80 && a < 0xC2) @@ -2064,12 +1864,11 @@ pg_encoding_set_invalid(int encoding, char *dst) const pg_wchar_tbl pg_wchar_table[] = { [PG_SQL_ASCII] = {pg_ascii2wchar_with_len, pg_wchar2single_with_len, pg_ascii_mblen, pg_ascii_dsplen, pg_ascii_verifychar, pg_ascii_verifystr, 1}, [PG_EUC_JP] = {pg_eucjp2wchar_with_len, pg_wchar2euc_with_len, pg_eucjp_mblen, pg_eucjp_dsplen, pg_eucjp_verifychar, pg_eucjp_verifystr, 3}, - [PG_EUC_CN] = {pg_euccn2wchar_with_len, pg_wchar2euc_with_len, pg_euccn_mblen, pg_euccn_dsplen, pg_euccn_verifychar, pg_euccn_verifystr, 2}, + [PG_EUC_CN] = {pg_euccn2wchar_with_len, pg_wchar2euc_with_len, pg_euccn_mblen, pg_euccn_dsplen, pg_euccn_verifychar, pg_euccn_verifystr, 3}, [PG_EUC_KR] = {pg_euckr2wchar_with_len, pg_wchar2euc_with_len, pg_euckr_mblen, pg_euckr_dsplen, pg_euckr_verifychar, pg_euckr_verifystr, 3}, [PG_EUC_TW] = {pg_euctw2wchar_with_len, pg_wchar2euc_with_len, pg_euctw_mblen, pg_euctw_dsplen, pg_euctw_verifychar, pg_euctw_verifystr, 4}, [PG_EUC_JIS_2004] = {pg_eucjp2wchar_with_len, pg_wchar2euc_with_len, pg_eucjp_mblen, pg_eucjp_dsplen, pg_eucjp_verifychar, pg_eucjp_verifystr, 3}, [PG_UTF8] = {pg_utf2wchar_with_len, pg_wchar2utf_with_len, pg_utf_mblen, pg_utf_dsplen, pg_utf8_verifychar, pg_utf8_verifystr, 4}, - [PG_MULE_INTERNAL] = {pg_mule2wchar_with_len, pg_wchar2mule_with_len, pg_mule_mblen, pg_mule_dsplen, pg_mule_verifychar, pg_mule_verifystr, 4}, [PG_LATIN1] = {pg_latin12wchar_with_len, pg_wchar2single_with_len, pg_latin1_mblen, pg_latin1_dsplen, pg_latin1_verifychar, pg_latin1_verifystr, 1}, [PG_LATIN2] = {pg_latin12wchar_with_len, pg_wchar2single_with_len, pg_latin1_mblen, pg_latin1_dsplen, pg_latin1_verifychar, pg_latin1_verifystr, 1}, [PG_LATIN3] = {pg_latin12wchar_with_len, pg_wchar2single_with_len, pg_latin1_mblen, pg_latin1_dsplen, pg_latin1_verifychar, pg_latin1_verifystr, 1}, diff --git a/src/fe_utils/Makefile b/src/fe_utils/Makefile index 28196ce0f6ae6..cbfbf93ac69fe 100644 --- a/src/fe_utils/Makefile +++ b/src/fe_utils/Makefile @@ -5,7 +5,7 @@ # This makefile generates a static library, libpgfeutils.a, # for use by client applications # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # IDENTIFICATION @@ -37,7 +37,8 @@ OBJS = \ query_utils.o \ recovery_gen.o \ simple_list.o \ - string_utils.o + string_utils.o \ + version.o ifeq ($(PORTNAME), win32) override CPPFLAGS += -DFD_SETSIZE=1024 diff --git a/src/fe_utils/archive.c b/src/fe_utils/archive.c index 5de3617cb2988..712030e941203 100644 --- a/src/fe_utils/archive.c +++ b/src/fe_utils/archive.c @@ -3,7 +3,7 @@ * archive.c * Routines to access WAL archives from frontend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/fe_utils/astreamer_file.c b/src/fe_utils/astreamer_file.c index c685628508636..0fca70a4f867d 100644 --- a/src/fe_utils/astreamer_file.c +++ b/src/fe_utils/astreamer_file.c @@ -6,7 +6,7 @@ * the whole archive to a single file, and astreamer_extractor writes * each archive member to a separate file in a given directory. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/fe_utils/astreamer_file.c @@ -82,7 +82,7 @@ astreamer_plain_writer_new(char *pathname, FILE *file) { astreamer_plain_writer *streamer; - streamer = palloc0(sizeof(astreamer_plain_writer)); + streamer = palloc0_object(astreamer_plain_writer); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_plain_writer_ops; @@ -189,7 +189,7 @@ astreamer_extractor_new(const char *basepath, { astreamer_extractor *streamer; - streamer = palloc0(sizeof(astreamer_extractor)); + streamer = palloc0_object(astreamer_extractor); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_extractor_ops; streamer->basepath = pstrdup(basepath); @@ -228,9 +228,13 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member, mystreamer->filename[fnamelen - 1] = '\0'; /* Dispatch based on file type. */ - if (member->is_directory) + if (member->is_regular) + mystreamer->file = + create_file_for_extract(mystreamer->filename, + member->mode); + else if (member->is_directory) extract_directory(mystreamer->filename, member->mode); - else if (member->is_link) + else if (member->is_symlink) { const char *linktarget = member->linktarget; @@ -238,10 +242,6 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member, linktarget = mystreamer->link_map(linktarget); extract_link(mystreamer->filename, linktarget); } - else - mystreamer->file = - create_file_for_extract(mystreamer->filename, - member->mode); /* Report output file change. */ if (mystreamer->report_output_file) @@ -266,7 +266,9 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member, case ASTREAMER_MEMBER_TRAILER: if (mystreamer->file == NULL) break; - fclose(mystreamer->file); + if (fclose(mystreamer->file) != 0) + pg_fatal("could not close file \"%s\": %m", + mystreamer->filename); mystreamer->file = NULL; break; diff --git a/src/fe_utils/astreamer_gzip.c b/src/fe_utils/astreamer_gzip.c index a395f57edcd70..bc3d53076e1db 100644 --- a/src/fe_utils/astreamer_gzip.c +++ b/src/fe_utils/astreamer_gzip.c @@ -17,7 +17,7 @@ * the same APIs that astreamer_gzip_writer now uses, and it didn't seem * necessary to change anything at the time. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/fe_utils/astreamer_gzip.c @@ -102,7 +102,7 @@ astreamer_gzip_writer_new(char *pathname, FILE *file, #ifdef HAVE_LIBZ astreamer_gzip_writer *streamer; - streamer = palloc0(sizeof(astreamer_gzip_writer)); + streamer = palloc0_object(astreamer_gzip_writer); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_gzip_writer_ops; @@ -241,12 +241,14 @@ astreamer_gzip_decompressor_new(astreamer *next) Assert(next != NULL); - streamer = palloc0(sizeof(astreamer_gzip_decompressor)); + streamer = palloc0_object(astreamer_gzip_decompressor); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_gzip_decompressor_ops; streamer->base.bbs_next = next; initStringInfo(&streamer->base.bbs_buffer); + /* Use a buffer size comparable to the other decompressors */ + enlargeStringInfo(&streamer->base.bbs_buffer, 256 * 1024 - 1); /* Initialize internal stream state for decompression */ zs = &streamer->zstream; @@ -316,8 +318,9 @@ astreamer_gzip_decompressor_content(astreamer *streamer, */ res = inflate(zs, Z_NO_FLUSH); - if (res == Z_STREAM_ERROR) - pg_log_error("could not decompress data: %s", zs->msg); + if (res != Z_OK && res != Z_STREAM_END && res != Z_BUF_ERROR) + pg_fatal("could not decompress data: %s", + zs->msg ? zs->msg : "unknown error"); mystreamer->bytes_written = mystreamer->base.bbs_buffer.maxlen - zs->avail_out; @@ -347,10 +350,11 @@ astreamer_gzip_decompressor_finalize(astreamer *streamer) * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. */ - astreamer_content(mystreamer->base.bbs_next, NULL, - mystreamer->base.bbs_buffer.data, - mystreamer->base.bbs_buffer.maxlen, - ASTREAMER_UNKNOWN); + if (mystreamer->bytes_written > 0) + astreamer_content(mystreamer->base.bbs_next, NULL, + mystreamer->base.bbs_buffer.data, + mystreamer->bytes_written, + ASTREAMER_UNKNOWN); astreamer_finalize(mystreamer->base.bbs_next); } @@ -361,7 +365,12 @@ astreamer_gzip_decompressor_finalize(astreamer *streamer) static void astreamer_gzip_decompressor_free(astreamer *streamer) { + astreamer_gzip_decompressor *mystreamer; + + mystreamer = (astreamer_gzip_decompressor *) streamer; + astreamer_free(streamer->bbs_next); + inflateEnd(&mystreamer->zstream); pfree(streamer->bbs_buffer.data); pfree(streamer); } diff --git a/src/fe_utils/astreamer_lz4.c b/src/fe_utils/astreamer_lz4.c index 781aaf99f38fe..12dfde2c83702 100644 --- a/src/fe_utils/astreamer_lz4.c +++ b/src/fe_utils/astreamer_lz4.c @@ -6,7 +6,7 @@ * astreamer_lz4_compressor applies lz4 compression to the input stream, * and astreamer_lz4_decompressor does the reverse. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/fe_utils/astreamer_lz4.c @@ -78,7 +78,7 @@ astreamer_lz4_compressor_new(astreamer *next, pg_compress_specification *compres Assert(next != NULL); - streamer = palloc0(sizeof(astreamer_lz4_frame)); + streamer = palloc0_object(astreamer_lz4_frame); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_lz4_compressor_ops; @@ -94,8 +94,8 @@ astreamer_lz4_compressor_new(astreamer *next, pg_compress_specification *compres ctxError = LZ4F_createCompressionContext(&streamer->cctx, LZ4F_VERSION); if (LZ4F_isError(ctxError)) - pg_log_error("could not create lz4 compression context: %s", - LZ4F_getErrorName(ctxError)); + pg_fatal("could not create lz4 compression context: %s", + LZ4F_getErrorName(ctxError)); return &streamer->base; #else @@ -121,14 +121,14 @@ astreamer_lz4_compressor_content(astreamer *streamer, astreamer_archive_context context) { astreamer_lz4_frame *mystreamer; - uint8 *next_in, - *next_out; + const uint8 *next_in; + uint8 *next_out; size_t out_bound, compressed_size, avail_out; mystreamer = (astreamer_lz4_frame *) streamer; - next_in = (uint8 *) data; + next_in = (const uint8 *) data; /* Write header before processing the first input chunk. */ if (!mystreamer->header_written) @@ -139,8 +139,8 @@ astreamer_lz4_compressor_content(astreamer *streamer, &mystreamer->prefs); if (LZ4F_isError(compressed_size)) - pg_log_error("could not write lz4 header: %s", - LZ4F_getErrorName(compressed_size)); + pg_fatal("could not write lz4 header: %s", + LZ4F_getErrorName(compressed_size)); mystreamer->bytes_written += compressed_size; mystreamer->header_written = true; @@ -188,8 +188,8 @@ astreamer_lz4_compressor_content(astreamer *streamer, next_in, len, NULL); if (LZ4F_isError(compressed_size)) - pg_log_error("could not compress data: %s", - LZ4F_getErrorName(compressed_size)); + pg_fatal("could not compress data: %s", + LZ4F_getErrorName(compressed_size)); mystreamer->bytes_written += compressed_size; } @@ -240,8 +240,8 @@ astreamer_lz4_compressor_finalize(astreamer *streamer) next_out, avail_out, NULL); if (LZ4F_isError(compressed_size)) - pg_log_error("could not end lz4 compression: %s", - LZ4F_getErrorName(compressed_size)); + pg_fatal("could not end lz4 compression: %s", + LZ4F_getErrorName(compressed_size)); mystreamer->bytes_written += compressed_size; @@ -282,12 +282,14 @@ astreamer_lz4_decompressor_new(astreamer *next) Assert(next != NULL); - streamer = palloc0(sizeof(astreamer_lz4_frame)); + streamer = palloc0_object(astreamer_lz4_frame); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_lz4_decompressor_ops; streamer->base.bbs_next = next; initStringInfo(&streamer->base.bbs_buffer); + /* Use a buffer size comparable to the compressor's */ + enlargeStringInfo(&streamer->base.bbs_buffer, 256 * 1024 - 1); /* Initialize internal stream state for decompression */ ctxError = LZ4F_createDecompressionContext(&streamer->dctx, LZ4F_VERSION); @@ -315,16 +317,16 @@ astreamer_lz4_decompressor_content(astreamer *streamer, astreamer_archive_context context) { astreamer_lz4_frame *mystreamer; - uint8 *next_in, - *next_out; + const uint8 *next_in; + uint8 *next_out; size_t avail_in, avail_out; mystreamer = (astreamer_lz4_frame *) streamer; - next_in = (uint8 *) data; - next_out = (uint8 *) mystreamer->base.bbs_buffer.data; + next_in = (const uint8 *) data; + next_out = (uint8 *) mystreamer->base.bbs_buffer.data + mystreamer->bytes_written; avail_in = len; - avail_out = mystreamer->base.bbs_buffer.maxlen; + avail_out = mystreamer->base.bbs_buffer.maxlen - mystreamer->bytes_written; while (avail_in > 0) { @@ -353,18 +355,21 @@ astreamer_lz4_decompressor_content(astreamer *streamer, next_in, &read_size, NULL); if (LZ4F_isError(ret)) - pg_log_error("could not decompress data: %s", - LZ4F_getErrorName(ret)); + pg_fatal("could not decompress data: %s", + LZ4F_getErrorName(ret)); /* Update input buffer based on number of bytes consumed */ avail_in -= read_size; next_in += read_size; + /* Update output buffer based on number of bytes produced */ + avail_out -= out_size; + next_out += out_size; mystreamer->bytes_written += out_size; /* * If output buffer is full then forward the content to next streamer - * and update the output buffer. + * and reset the output buffer. */ if (mystreamer->bytes_written >= mystreamer->base.bbs_buffer.maxlen) { @@ -374,13 +379,8 @@ astreamer_lz4_decompressor_content(astreamer *streamer, context); avail_out = mystreamer->base.bbs_buffer.maxlen; - mystreamer->bytes_written = 0; next_out = (uint8 *) mystreamer->base.bbs_buffer.data; - } - else - { - avail_out = mystreamer->base.bbs_buffer.maxlen - mystreamer->bytes_written; - next_out += mystreamer->bytes_written; + mystreamer->bytes_written = 0; } } } @@ -399,10 +399,11 @@ astreamer_lz4_decompressor_finalize(astreamer *streamer) * End of the stream, if there is some pending data in output buffers then * we must forward it to next streamer. */ - astreamer_content(mystreamer->base.bbs_next, NULL, - mystreamer->base.bbs_buffer.data, - mystreamer->base.bbs_buffer.maxlen, - ASTREAMER_UNKNOWN); + if (mystreamer->bytes_written > 0) + astreamer_content(mystreamer->base.bbs_next, NULL, + mystreamer->base.bbs_buffer.data, + mystreamer->bytes_written, + ASTREAMER_UNKNOWN); astreamer_finalize(mystreamer->base.bbs_next); } diff --git a/src/fe_utils/astreamer_tar.c b/src/fe_utils/astreamer_tar.c index 088e2357920c5..f584665057dab 100644 --- a/src/fe_utils/astreamer_tar.c +++ b/src/fe_utils/astreamer_tar.c @@ -12,7 +12,7 @@ * just adds two blocks of NUL bytes to the end of the file, since older * server versions produce files with this terminator omitted. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/fe_utils/astreamer_tar.c @@ -94,7 +94,7 @@ astreamer_tar_parser_new(astreamer *next) { astreamer_tar_parser *streamer; - streamer = palloc0(sizeof(astreamer_tar_parser)); + streamer = palloc0_object(astreamer_tar_parser); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_tar_parser_ops; streamer->base.bbs_next = next; @@ -224,7 +224,8 @@ astreamer_tar_parser_content(astreamer *streamer, astreamer_member *member, /* OK, now we can send it. */ astreamer_content(mystreamer->base.bbs_next, &mystreamer->member, - data, mystreamer->pad_bytes_expected, + mystreamer->base.bbs_buffer.data, + mystreamer->pad_bytes_expected, ASTREAMER_MEMBER_TRAILER); /* Expect next file header. */ @@ -236,12 +237,16 @@ astreamer_tar_parser_content(astreamer *streamer, astreamer_member *member, /* * We've seen an end-of-archive indicator, so anything more is - * buffered and sent as part of the archive trailer. But we - * don't expect more than 2 blocks. + * buffered and sent as part of the archive trailer. + * + * Per POSIX, the last physical block of a tar archive is + * always full-sized, so there may be undefined data after the + * two zero blocks that mark end-of-archive. GNU tar, for + * example, zero-pads to a 10kB boundary by default. We just + * buffer whatever we receive and pass it along at finalize + * time. */ astreamer_buffer_bytes(streamer, &data, &len, len); - if (len > 2 * TAR_BLOCK_SIZE) - pg_fatal("tar file trailer exceeds 2 blocks"); return; default: @@ -255,7 +260,8 @@ astreamer_tar_parser_content(astreamer *streamer, astreamer_member *member, * Parse a file header within a tar stream. * * The return value is true if we found a file header and passed it on to the - * next astreamer; it is false if we have reached the archive trailer. + * next astreamer; it is false if we have found the archive trailer. + * We throw error if we see invalid data. */ static bool astreamer_tar_header(astreamer_tar_parser *mystreamer) @@ -267,6 +273,9 @@ astreamer_tar_header(astreamer_tar_parser *mystreamer) Assert(mystreamer->base.bbs_buffer.len == TAR_BLOCK_SIZE); + /* Zero out fields of *member, just for consistency. */ + memset(member, 0, sizeof(astreamer_member)); + /* Check whether we've got a block of all zero bytes. */ for (i = 0; i < TAR_BLOCK_SIZE; ++i) { @@ -284,6 +293,12 @@ astreamer_tar_header(astreamer_tar_parser *mystreamer) if (!has_nonzero_byte) return false; + /* + * Verify that we have a reasonable-looking header. + */ + if (!isValidTarHeader(buffer)) + pg_fatal("input file does not appear to be a valid tar archive"); + /* * Parse key fields out of the header. */ @@ -294,12 +309,28 @@ astreamer_tar_header(astreamer_tar_parser *mystreamer) member->mode = read_tar_number(&buffer[TAR_OFFSET_MODE], 8); member->uid = read_tar_number(&buffer[TAR_OFFSET_UID], 8); member->gid = read_tar_number(&buffer[TAR_OFFSET_GID], 8); - member->is_directory = - (buffer[TAR_OFFSET_TYPEFLAG] == TAR_FILETYPE_DIRECTORY); - member->is_link = - (buffer[TAR_OFFSET_TYPEFLAG] == TAR_FILETYPE_SYMLINK); - if (member->is_link) - strlcpy(member->linktarget, &buffer[TAR_OFFSET_LINKNAME], 100); + + switch (buffer[TAR_OFFSET_TYPEFLAG]) + { + case TAR_FILETYPE_PLAIN: + case TAR_FILETYPE_PLAIN_OLD: + member->is_regular = true; + break; + case TAR_FILETYPE_DIRECTORY: + member->is_directory = true; + break; + case TAR_FILETYPE_SYMLINK: + member->is_symlink = true; + strlcpy(member->linktarget, &buffer[TAR_OFFSET_LINKNAME], 100); + break; + case TAR_FILETYPE_PAX_EXTENDED: + case TAR_FILETYPE_PAX_EXTENDED_GLOBAL: + pg_fatal("pax extensions to tar format are not supported"); + break; + default: + /* For special filetypes, set none of the three is_xxx flags */ + break; + } /* Compute number of padding bytes. */ mystreamer->pad_bytes_expected = tarPaddingBytesRequired(member->size); @@ -342,6 +373,7 @@ astreamer_tar_parser_free(astreamer *streamer) { pfree(streamer->bbs_buffer.data); astreamer_free(streamer->bbs_next); + pfree(streamer); } /* @@ -357,7 +389,7 @@ astreamer_tar_archiver_new(astreamer *next) { astreamer_tar_archiver *streamer; - streamer = palloc0(sizeof(astreamer_tar_archiver)); + streamer = palloc0_object(astreamer_tar_archiver); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_tar_archiver_ops; streamer->base.bbs_next = next; @@ -463,7 +495,7 @@ astreamer_tar_terminator_new(astreamer *next) { astreamer *streamer; - streamer = palloc0(sizeof(astreamer)); + streamer = palloc0_object(astreamer); *((const astreamer_ops **) &streamer->bbs_ops) = &astreamer_tar_terminator_ops; streamer->bbs_next = next; diff --git a/src/fe_utils/astreamer_zstd.c b/src/fe_utils/astreamer_zstd.c index bacdcc150c40f..98e8a700efef3 100644 --- a/src/fe_utils/astreamer_zstd.c +++ b/src/fe_utils/astreamer_zstd.c @@ -3,10 +3,10 @@ * astreamer_zstd.c * * Archive streamers that deal with data compressed using zstd. - * astreamer_zstd_compressor applies lz4 compression to the input stream, + * astreamer_zstd_compressor applies zstd compression to the input stream, * and astreamer_zstd_decompressor does the reverse. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/fe_utils/astreamer_zstd.c @@ -75,14 +75,14 @@ astreamer_zstd_compressor_new(astreamer *next, pg_compress_specification *compre Assert(next != NULL); - streamer = palloc0(sizeof(astreamer_zstd_frame)); + streamer = palloc0_object(astreamer_zstd_frame); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_zstd_compressor_ops; streamer->base.bbs_next = next; initStringInfo(&streamer->base.bbs_buffer); - enlargeStringInfo(&streamer->base.bbs_buffer, ZSTD_DStreamOutSize()); + enlargeStringInfo(&streamer->base.bbs_buffer, ZSTD_CStreamOutSize()); streamer->cctx = ZSTD_createCCtx(); if (!streamer->cctx) @@ -116,11 +116,8 @@ astreamer_zstd_compressor_new(astreamer *next, pg_compress_specification *compre ZSTD_c_enableLongDistanceMatching, compress->long_distance); if (ZSTD_isError(ret)) - { - pg_log_error("could not enable long-distance mode: %s", - ZSTD_getErrorName(ret)); - exit(1); - } + pg_fatal("could not enable long-distance mode: %s", + ZSTD_getErrorName(ret)); } /* Initialize the ZSTD output buffer. */ @@ -182,8 +179,8 @@ astreamer_zstd_compressor_content(astreamer *streamer, &inBuf, ZSTD_e_continue); if (ZSTD_isError(yet_to_flush)) - pg_log_error("could not compress data: %s", - ZSTD_getErrorName(yet_to_flush)); + pg_fatal("could not compress data: %s", + ZSTD_getErrorName(yet_to_flush)); } } @@ -224,8 +221,8 @@ astreamer_zstd_compressor_finalize(astreamer *streamer) &in, ZSTD_e_end); if (ZSTD_isError(yet_to_flush)) - pg_log_error("could not compress data: %s", - ZSTD_getErrorName(yet_to_flush)); + pg_fatal("could not compress data: %s", + ZSTD_getErrorName(yet_to_flush)); } while (yet_to_flush > 0); @@ -266,7 +263,7 @@ astreamer_zstd_decompressor_new(astreamer *next) Assert(next != NULL); - streamer = palloc0(sizeof(astreamer_zstd_frame)); + streamer = palloc0_object(astreamer_zstd_frame); *((const astreamer_ops **) &streamer->base.bbs_ops) = &astreamer_zstd_decompressor_ops; @@ -330,8 +327,8 @@ astreamer_zstd_decompressor_content(astreamer *streamer, &mystreamer->zstd_outBuf, &inBuf); if (ZSTD_isError(ret)) - pg_log_error("could not decompress data: %s", - ZSTD_getErrorName(ret)); + pg_fatal("could not decompress data: %s", + ZSTD_getErrorName(ret)); } } @@ -350,7 +347,7 @@ astreamer_zstd_decompressor_finalize(astreamer *streamer) if (mystreamer->zstd_outBuf.pos > 0) astreamer_content(mystreamer->base.bbs_next, NULL, mystreamer->base.bbs_buffer.data, - mystreamer->base.bbs_buffer.maxlen, + mystreamer->zstd_outBuf.pos, ASTREAMER_UNKNOWN); astreamer_finalize(mystreamer->base.bbs_next); diff --git a/src/fe_utils/cancel.c b/src/fe_utils/cancel.c index f434718eac802..e6b75439f56f0 100644 --- a/src/fe_utils/cancel.c +++ b/src/fe_utils/cancel.c @@ -6,7 +6,7 @@ * handler for SIGINT. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/cancel.c diff --git a/src/fe_utils/conditional.c b/src/fe_utils/conditional.c index 9e1ad687f08c8..537c76ed3cd4a 100644 --- a/src/fe_utils/conditional.c +++ b/src/fe_utils/conditional.c @@ -1,7 +1,7 @@ /*------------------------------------------------------------------------- * A stack of automaton states to handle nested conditionals. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/fe_utils/conditional.c * @@ -17,7 +17,7 @@ ConditionalStack conditional_stack_create(void) { - ConditionalStack cstack = pg_malloc(sizeof(ConditionalStackData)); + ConditionalStack cstack = pg_malloc_object(ConditionalStackData); cstack->head = NULL; return cstack; @@ -52,7 +52,7 @@ conditional_stack_destroy(ConditionalStack cstack) void conditional_stack_push(ConditionalStack cstack, ifState new_state) { - IfStackElem *p = (IfStackElem *) pg_malloc(sizeof(IfStackElem)); + IfStackElem *p = pg_malloc_object(IfStackElem); p->if_state = new_state; p->query_len = -1; diff --git a/src/fe_utils/connect_utils.c b/src/fe_utils/connect_utils.c index cda1c11099789..b117718513c95 100644 --- a/src/fe_utils/connect_utils.c +++ b/src/fe_utils/connect_utils.c @@ -2,7 +2,7 @@ * * Facilities for frontend code to connect to and disconnect from databases. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/connect_utils.c diff --git a/src/fe_utils/mbprint.c b/src/fe_utils/mbprint.c index eb3eeee9925cb..dbfa1cab597b9 100644 --- a/src/fe_utils/mbprint.c +++ b/src/fe_utils/mbprint.c @@ -3,7 +3,7 @@ * Multibyte character printing support for frontend code * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/mbprint.c @@ -49,20 +49,20 @@ pg_get_utf8_id(void) * * No error checks here, c must point to a long-enough string. */ -static pg_wchar +static char32_t utf8_to_unicode(const unsigned char *c) { if ((*c & 0x80) == 0) - return (pg_wchar) c[0]; + return (char32_t) c[0]; else if ((*c & 0xe0) == 0xc0) - return (pg_wchar) (((c[0] & 0x1f) << 6) | + return (char32_t) (((c[0] & 0x1f) << 6) | (c[1] & 0x3f)); else if ((*c & 0xf0) == 0xe0) - return (pg_wchar) (((c[0] & 0x0f) << 12) | + return (char32_t) (((c[0] & 0x0f) << 12) | ((c[1] & 0x3f) << 6) | (c[2] & 0x3f)); else if ((*c & 0xf8) == 0xf0) - return (pg_wchar) (((c[0] & 0x07) << 18) | + return (char32_t) (((c[0] & 0x07) << 18) | ((c[1] & 0x3f) << 12) | ((c[2] & 0x3f) << 6) | (c[3] & 0x3f)); diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build index a18cbc939e412..86befca192e66 100644 --- a/src/fe_utils/meson.build +++ b/src/fe_utils/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group fe_utils_sources = files( 'archive.c', @@ -18,6 +18,7 @@ fe_utils_sources = files( 'recovery_gen.c', 'simple_list.c', 'string_utils.c', + 'version.c', ) psqlscan = custom_target('psqlscan', @@ -29,10 +30,12 @@ generated_sources += psqlscan fe_utils_sources += psqlscan fe_utils = static_library('libpgfeutils', - fe_utils_sources + generated_headers, + fe_utils_sources, c_pch: pch_postgres_fe_h, include_directories: [postgres_inc, libpq_inc], c_args: host_system == 'windows' ? ['-DFD_SETSIZE=1024'] : [], dependencies: frontend_common_code, - kwargs: default_lib_args, + kwargs: default_lib_args + { + 'install': install_internal_static_lib, + }, ) diff --git a/src/fe_utils/option_utils.c b/src/fe_utils/option_utils.c index 6cb4ac7ffeb06..8d0659c1164d3 100644 --- a/src/fe_utils/option_utils.c +++ b/src/fe_utils/option_utils.c @@ -2,7 +2,7 @@ * * Command line option processing facilities for frontend code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/option_utils.c @@ -109,3 +109,38 @@ parse_sync_method(const char *optarg, DataDirSyncMethod *sync_method) return true; } + +/* + * Fail with appropriate error if 2 or more of the specified options are set. + * The first parameter is the number of arguments. Following that is an + * arbitrary number of bool/string pairs. The bool indicates whether the + * option is set, and the string is used for the error message. Note that only + * the first pair of enabled options we find are reported. + * + * Don't call this directly. Use the check_mut_excl_opts() macro in + * option_utils.h, which is the exact same except it doesn't take the first + * parameter (it discovers the number of arguments automagically). + */ +void +check_mut_excl_opts_internal(int n,...) +{ + char *first = NULL; + va_list args; + + Assert(n % 2 == 0); + + va_start(args, n); + for (int i = 0; i < n; i += 2) + { + bool set = va_arg(args, int); + char *opt = va_arg(args, char *); + + if (set && first) + pg_fatal("options %s and %s cannot be used together", + first, opt); + + if (set) + first = opt; + } + va_end(args); +} diff --git a/src/fe_utils/parallel_slot.c b/src/fe_utils/parallel_slot.c index 253a840865e87..fb9e6cc4ec134 100644 --- a/src/fe_utils/parallel_slot.c +++ b/src/fe_utils/parallel_slot.c @@ -4,7 +4,7 @@ * Parallel support for front-end parallel database connections * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/parallel_slot.c @@ -269,8 +269,7 @@ wait_on_slots(ParallelSlotArray *sa) else { /* This connection has become idle */ - sa->slots[i].inUse = false; - ParallelSlotClearHandler(&sa->slots[i]); + ParallelSlotSetIdle(&sa->slots[i]); break; } } @@ -509,8 +508,7 @@ ParallelSlotsWaitCompletion(ParallelSlotArray *sa) if (!consumeQueryResult(&sa->slots[i])) return false; /* Mark connection as idle */ - sa->slots[i].inUse = false; - ParallelSlotClearHandler(&sa->slots[i]); + ParallelSlotSetIdle(&sa->slots[i]); } return true; diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c index 4af0f32f2fc05..f2dd52003c184 100644 --- a/src/fe_utils/print.c +++ b/src/fe_utils/print.c @@ -8,7 +8,7 @@ * pager open/close functions, all that stuff came with it. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/print.c @@ -33,6 +33,11 @@ #include "fe_utils/mbprint.h" #include "fe_utils/print.h" +/* Presently, count_table_lines() is only used within #ifdef TIOCGWINSZ */ +#ifdef TIOCGWINSZ +#define NEED_COUNT_TABLE_LINES +#endif + /* * If the calling program doesn't have any mechanism for setting * cancel_pressed, it will have no effect. @@ -266,9 +271,20 @@ static const unicodeStyleFormat unicode_style = { /* Local functions */ static int strlen_max_width(unsigned char *str, int *target_width, int encoding); -static void IsPagerNeeded(const printTableContent *cont, int extra_lines, bool expanded, +static FILE *PageOutputInternal(int lines, const printTableOpt *topt, + const printTableContent *cont, + const unsigned int *width_wrap, + bool vertical); +static void IsPagerNeeded(const printTableContent *cont, + const unsigned int *width_wrap, + bool vertical, FILE **fout, bool *is_pager); - +#ifdef NEED_COUNT_TABLE_LINES +static int count_table_lines(const printTableContent *cont, + const unsigned int *width_wrap, + bool vertical, + int threshold); +#endif static void print_aligned_vertical(const printTableContent *cont, FILE *fout, bool is_pager); @@ -328,7 +344,7 @@ format_numeric_locale(const char *my_str) return pg_strdup(my_str); new_len = strlen(my_str) + additional_numeric_locale_len(my_str); - new_str = pg_malloc(new_len + 1); + new_str = pg_malloc_array(char, (new_len + 1)); new_str_pos = 0; int_len = integer_digits(my_str); @@ -656,8 +672,6 @@ print_aligned_text(const printTableContent *cont, FILE *fout, bool is_pager) unsigned char **format_buf; unsigned int width_total; unsigned int total_header_width; - unsigned int extra_row_output_lines = 0; - unsigned int extra_output_lines = 0; const char *const *ptr; @@ -678,18 +692,18 @@ print_aligned_text(const printTableContent *cont, FILE *fout, bool is_pager) if (cont->ncolumns > 0) { col_count = cont->ncolumns; - width_header = pg_malloc0(col_count * sizeof(*width_header)); - width_average = pg_malloc0(col_count * sizeof(*width_average)); - max_width = pg_malloc0(col_count * sizeof(*max_width)); - width_wrap = pg_malloc0(col_count * sizeof(*width_wrap)); - max_nl_lines = pg_malloc0(col_count * sizeof(*max_nl_lines)); - curr_nl_line = pg_malloc0(col_count * sizeof(*curr_nl_line)); - col_lineptrs = pg_malloc0(col_count * sizeof(*col_lineptrs)); - max_bytes = pg_malloc0(col_count * sizeof(*max_bytes)); - format_buf = pg_malloc0(col_count * sizeof(*format_buf)); - header_done = pg_malloc0(col_count * sizeof(*header_done)); - bytes_output = pg_malloc0(col_count * sizeof(*bytes_output)); - wrap = pg_malloc0(col_count * sizeof(*wrap)); + width_header = pg_malloc0_array(unsigned int, col_count); + width_average = pg_malloc0_array(unsigned int, col_count); + max_width = pg_malloc0_array(unsigned int, col_count); + width_wrap = pg_malloc0_array(unsigned int, col_count); + max_nl_lines = pg_malloc0_array(unsigned int, col_count); + curr_nl_line = pg_malloc0_array(unsigned int, col_count); + col_lineptrs = pg_malloc0_array(struct lineptr *, col_count); + max_bytes = pg_malloc0_array(unsigned int, col_count); + format_buf = pg_malloc0_array(unsigned char *, col_count); + header_done = pg_malloc0_array(bool, col_count); + bytes_output = pg_malloc0_array(int, col_count); + wrap = pg_malloc0_array(printTextLineWrap, col_count); } else { @@ -722,17 +736,12 @@ print_aligned_text(const printTableContent *cont, FILE *fout, bool is_pager) max_nl_lines[i] = nl_lines; if (bytes_required > max_bytes[i]) max_bytes[i] = bytes_required; - if (nl_lines > extra_row_output_lines) - extra_row_output_lines = nl_lines; width_header[i] = width; } - /* Add height of tallest header column */ - extra_output_lines += extra_row_output_lines; - extra_row_output_lines = 0; /* scan all cells, find maximum width, compute cell_count */ - for (i = 0, ptr = cont->cells; *ptr; ptr++, i++, cell_count++) + for (i = 0, ptr = cont->cells; *ptr; ptr++, cell_count++) { int width, nl_lines, @@ -741,14 +750,18 @@ print_aligned_text(const printTableContent *cont, FILE *fout, bool is_pager) pg_wcssize((const unsigned char *) *ptr, strlen(*ptr), encoding, &width, &nl_lines, &bytes_required); - if (width > max_width[i % col_count]) - max_width[i % col_count] = width; - if (nl_lines > max_nl_lines[i % col_count]) - max_nl_lines[i % col_count] = nl_lines; - if (bytes_required > max_bytes[i % col_count]) - max_bytes[i % col_count] = bytes_required; + if (width > max_width[i]) + max_width[i] = width; + if (nl_lines > max_nl_lines[i]) + max_nl_lines[i] = nl_lines; + if (bytes_required > max_bytes[i]) + max_bytes[i] = bytes_required; + + width_average[i] += width; - width_average[i % col_count] += width; + /* i is the current column number: increment with wrap */ + if (++i >= col_count) + i = 0; } /* If we have rows, compute average */ @@ -785,10 +798,10 @@ print_aligned_text(const printTableContent *cont, FILE *fout, bool is_pager) for (i = 0; i < col_count; i++) { /* Add entry for ptr == NULL array termination */ - col_lineptrs[i] = pg_malloc0((max_nl_lines[i] + 1) * - sizeof(**col_lineptrs)); + col_lineptrs[i] = pg_malloc0_array(struct lineptr, + (max_nl_lines[i] + 1)); - format_buf[i] = pg_malloc(max_bytes[i] + 1); + format_buf[i] = pg_malloc_array(unsigned char, (max_bytes[i] + 1)); col_lineptrs[i]->ptr = format_buf[i]; } @@ -889,43 +902,10 @@ print_aligned_text(const printTableContent *cont, FILE *fout, bool is_pager) is_pager = is_local_pager = true; } - /* Check if newlines or our wrapping now need the pager */ - if (!is_pager && fout == stdout) + /* Check if there are enough lines to require the pager */ + if (!is_pager) { - /* scan all cells, find maximum width, compute cell_count */ - for (i = 0, ptr = cont->cells; *ptr; ptr++, cell_count++) - { - int width, - nl_lines, - bytes_required; - - pg_wcssize((const unsigned char *) *ptr, strlen(*ptr), encoding, - &width, &nl_lines, &bytes_required); - - /* - * A row can have both wrapping and newlines that cause it to - * display across multiple lines. We check for both cases below. - */ - if (width > 0 && width_wrap[i]) - { - unsigned int extra_lines; - - /* don't count the first line of nl_lines - it's not "extra" */ - extra_lines = ((width - 1) / width_wrap[i]) + nl_lines - 1; - if (extra_lines > extra_row_output_lines) - extra_row_output_lines = extra_lines; - } - - /* i is the current column number: increment with wrap */ - if (++i >= col_count) - { - i = 0; - /* At last column of each row, add tallest column height */ - extra_output_lines += extra_row_output_lines; - extra_row_output_lines = 0; - } - } - IsPagerNeeded(cont, extra_output_lines, false, &fout, &is_pager); + IsPagerNeeded(cont, width_wrap, false, &fout, &is_pager); is_local_pager = is_pager; } @@ -1351,6 +1331,11 @@ print_aligned_vertical(const printTableContent *cont, if (opt_border > 2) opt_border = 2; + /* + * Kluge for totally empty table: use the default footer even though + * vertical modes normally don't. Otherwise we'd print nothing at all, + * which isn't terribly friendly. Assume pager will not be needed. + */ if (cont->cells[0] == NULL && cont->opt->start_table && cont->opt->stop_table) { @@ -1376,7 +1361,7 @@ print_aligned_vertical(const printTableContent *cont, */ if (!is_pager) { - IsPagerNeeded(cont, 0, true, &fout, &is_pager); + IsPagerNeeded(cont, NULL, true, &fout, &is_pager); is_local_pager = is_pager; } @@ -1424,8 +1409,8 @@ print_aligned_vertical(const printTableContent *cont, * We now have all the information we need to setup the formatting * structures */ - dlineptr = pg_malloc((sizeof(*dlineptr)) * (dheight + 1)); - hlineptr = pg_malloc((sizeof(*hlineptr)) * (hheight + 1)); + dlineptr = pg_malloc_array(struct lineptr, (dheight + 1)); + hlineptr = pg_malloc_array(struct lineptr, (hheight + 1)); dlineptr->ptr = pg_malloc(dformatsize); hlineptr->ptr = pg_malloc(hformatsize); @@ -3039,7 +3024,7 @@ void disable_sigpipe_trap(void) { #ifndef WIN32 - pqsignal(SIGPIPE, SIG_IGN); + pqsignal(SIGPIPE, PG_SIG_IGN); #endif } @@ -3062,7 +3047,7 @@ void restore_sigpipe_trap(void) { #ifndef WIN32 - pqsignal(SIGPIPE, always_ignore_sigpipe ? SIG_IGN : SIG_DFL); + pqsignal(SIGPIPE, always_ignore_sigpipe ? PG_SIG_IGN : PG_SIG_DFL); #endif } @@ -3081,28 +3066,62 @@ set_sigpipe_trap_state(bool ignore) /* * PageOutput * - * Tests if pager is needed and returns appropriate FILE pointer. + * Tests if pager is needed and returns appropriate FILE pointer + * (either a pipe, or stdout if we don't need the pager). + * + * lines: number of lines that will be printed + * topt: print formatting options * * If the topt argument is NULL no pager is used. */ FILE * PageOutput(int lines, const printTableOpt *topt) +{ + return PageOutputInternal(lines, topt, NULL, NULL, false); +} + +/* + * Private version that allows for line-counting to be avoided when + * not needed. If "cont" is not null then the input value of "lines" + * is ignored and we count lines based on cont + width_wrap + vertical + * (see count_table_lines). + */ +static FILE * +PageOutputInternal(int lines, const printTableOpt *topt, + const printTableContent *cont, + const unsigned int *width_wrap, + bool vertical) { /* check whether we need / can / are supposed to use pager */ if (topt && topt->pager && isatty(fileno(stdin)) && isatty(fileno(stdout))) { + /* without TIOCGWINSZ, pager == 1 acts the same as pager > 1 */ #ifdef TIOCGWINSZ unsigned short int pager = topt->pager; int min_lines = topt->pager_min_lines; - int result; - struct winsize screen_size; - result = ioctl(fileno(stdout), TIOCGWINSZ, &screen_size); + if (pager == 1) + { + int result; + struct winsize screen_size; - /* >= accounts for a one-line prompt */ - if (result == -1 - || (lines >= screen_size.ws_row && lines >= min_lines) - || pager > 1) + result = ioctl(fileno(stdout), TIOCGWINSZ, &screen_size); + if (result < 0) + pager = 2; /* force use of pager */ + else + { + int threshold = Max(screen_size.ws_row, min_lines); + + if (cont) /* caller wants us to calculate lines */ + lines = count_table_lines(cont, width_wrap, vertical, + threshold); + /* >= accounts for a one-line prompt */ + if (lines >= threshold) + pager = 2; + } + } + + if (pager > 1) #endif { const char *pagerprog; @@ -3179,7 +3198,7 @@ printTableInit(printTableContent *const content, const printTableOpt *opt, content->ncolumns = ncolumns; content->nrows = nrows; - content->headers = pg_malloc0((ncolumns + 1) * sizeof(*content->headers)); + content->headers = pg_malloc0_array(const char *, (ncolumns + 1)); total_cells = (uint64) ncolumns * nrows; /* Catch possible overflow. Using >= here allows adding 1 below */ @@ -3190,12 +3209,12 @@ printTableInit(printTableContent *const content, const printTableOpt *opt, SIZE_MAX / sizeof(*content->cells)); exit(EXIT_FAILURE); } - content->cells = pg_malloc0((total_cells + 1) * sizeof(*content->cells)); + content->cells = pg_malloc0_array(const char *, (total_cells + 1)); content->cellmustfree = NULL; content->footers = NULL; - content->aligns = pg_malloc0((ncolumns + 1) * sizeof(*content->align)); + content->aligns = pg_malloc0_array(char, (ncolumns + 1)); content->header = content->headers; content->cell = content->cells; @@ -3286,7 +3305,7 @@ printTableAddCell(printTableContent *const content, char *cell, { if (content->cellmustfree == NULL) content->cellmustfree = - pg_malloc0((total_cells + 1) * sizeof(bool)); + pg_malloc0_array(bool, (total_cells + 1)); content->cellmustfree[content->cellsadded] = true; } @@ -3311,7 +3330,7 @@ printTableAddFooter(printTableContent *const content, const char *footer) { printTableFooter *f; - f = pg_malloc0(sizeof(*f)); + f = pg_malloc0_object(printTableFooter); f->data = pg_strdup(footer); if (content->footers == NULL) @@ -3398,38 +3417,212 @@ printTableCleanup(printTableContent *const content) * IsPagerNeeded * * Setup pager if required + * + * cont: table data to be printed + * width_wrap[]: per-column maximum width, or NULL if caller will not wrap + * vertical: vertical mode? + * fout: where to print to (in/out argument) + * is_pager: output argument + * + * If we decide pager is needed, *fout is modified and *is_pager is set true */ static void -IsPagerNeeded(const printTableContent *cont, int extra_lines, bool expanded, +IsPagerNeeded(const printTableContent *cont, const unsigned int *width_wrap, + bool vertical, FILE **fout, bool *is_pager) { if (*fout == stdout) { - int lines; + *fout = PageOutputInternal(0, cont->opt, cont, width_wrap, vertical); + *is_pager = (*fout != stdout); + } + else + *is_pager = false; +} - if (expanded) - lines = (cont->ncolumns + 1) * cont->nrows; - else - lines = cont->nrows + 1; +/* + * Count the number of lines needed to print the given table. + * + * cont: table data to be printed + * width_wrap[]: per-column maximum width, or NULL if caller will not wrap + * vertical: vertical mode? + * threshold: we can stop counting once we pass this many lines + * + * The result is currently only fully accurate for ALIGNED/WRAPPED and + * UNALIGNED formats; otherwise it's an approximation. + * + * Note: while cont->opt will tell us most formatting details, we need the + * separate "vertical" flag because of the possibility of a dynamic switch + * from aligned_text to aligned_vertical format. + * + * The point of the threshold parameter is that when the table is very long, + * we'll typically be able to stop scanning after not many rows. + */ +#ifdef NEED_COUNT_TABLE_LINES +static int +count_table_lines(const printTableContent *cont, + const unsigned int *width_wrap, + bool vertical, + int threshold) +{ + int *header_height; + int lines = 0, + max_lines = 0, + nl_lines, + i; + int encoding = cont->opt->encoding; + const char *const *cell; - if (!cont->opt->tuples_only) - { - printTableFooter *f; + /* + * Scan all column headers and determine their heights. Cache the values + * since vertical mode repeats the headers for every record. + */ + header_height = pg_malloc_array(int, cont->ncolumns); + for (i = 0; i < cont->ncolumns; i++) + { + pg_wcssize((const unsigned char *) cont->headers[i], + strlen(cont->headers[i]), encoding, + NULL, &header_height[i], NULL); + } + + /* + * Account for separator lines (if used), as well as the trailing blank + * line that most formats emit. + */ + switch (cont->opt->format) + { + case PRINT_ALIGNED: + case PRINT_WRAPPED: /* - * FIXME -- this is slightly bogus: it counts the number of - * footers, not the number of lines in them. + * Vertical mode writes one separator line per record. Normal + * mode writes a single separator line between header and rows. */ - for (f = cont->footers; f; f = f->next) - lines++; + lines = vertical ? cont->nrows : 1; + /* Both modes add a blank line at the end */ + lines++; + break; + case PRINT_UNALIGNED: + + /* + * Vertical mode writes a separator (here assumed to be a newline) + * between records. Normal mode writes nothing extra. + */ + if (vertical) + lines = Max(cont->nrows - 1, 0); + break; + case PRINT_CSV: + /* Nothing extra is added */ + break; + case PRINT_HTML: + case PRINT_ASCIIDOC: + case PRINT_LATEX: + case PRINT_LATEX_LONGTABLE: + case PRINT_TROFF_MS: + + /* + * These formats aren't really meant for interactive consumption, + * so for now we won't work hard on them. Treat them like aligned + * mode. + */ + lines = vertical ? cont->nrows : 1; + lines++; + break; + case PRINT_NOTHING: + /* Shouldn't get here... */ + break; + } + + /* Scan all cells to count their lines */ + for (i = 0, cell = cont->cells; *cell; cell++) + { + int width; + + /* Count the original line breaks */ + pg_wcssize((const unsigned char *) *cell, strlen(*cell), encoding, + &width, &nl_lines, NULL); + + /* Count extra lines due to wrapping */ + if (width > 0 && width_wrap && width_wrap[i]) + nl_lines += (width - 1) / width_wrap[i]; + + if (vertical) + { + /* Pick the height of the header or cell, whichever is taller */ + if (nl_lines > header_height[i]) + lines += nl_lines; + else + lines += header_height[i]; + } + else + { + /* Remember max height in the current row */ + if (nl_lines > max_lines) + max_lines = nl_lines; } - *fout = PageOutput(lines + extra_lines, cont->opt); - *is_pager = (*fout != stdout); + /* i is the current column number: increment with wrap */ + if (++i >= cont->ncolumns) + { + i = 0; + if (!vertical) + { + /* At last column of each row, add tallest column height */ + lines += max_lines; + max_lines = 0; + } + /* Stop scanning table body once we pass threshold */ + if (lines > threshold) + break; + } } - else - *is_pager = false; + + /* Account for header and footer decoration */ + if (!cont->opt->tuples_only && lines <= threshold) + { + printTableFooter *f; + + if (cont->title) + { + /* Add height of title */ + pg_wcssize((const unsigned char *) cont->title, strlen(cont->title), + encoding, NULL, &nl_lines, NULL); + lines += nl_lines; + } + + if (!vertical) + { + /* Add height of tallest header column */ + max_lines = 0; + for (i = 0; i < cont->ncolumns; i++) + { + if (header_height[i] > max_lines) + max_lines = header_height[i]; + } + lines += max_lines; + } + + /* + * Add all footer lines. Vertical mode does not use the default + * footer, but we must include that in normal mode. + */ + for (f = (vertical ? cont->footers : footers_with_default(cont)); + f != NULL; f = f->next) + { + pg_wcssize((const unsigned char *) f->data, strlen(f->data), + encoding, NULL, &nl_lines, NULL); + lines += nl_lines; + /* Stop scanning footers once we pass threshold */ + if (lines > threshold) + break; + } + } + + free(header_height); + + return lines; } +#endif /* NEED_COUNT_TABLE_LINES */ /* * Use this to print any table in the supported formats. @@ -3456,7 +3649,7 @@ printTable(const printTableContent *cont, cont->opt->format != PRINT_ALIGNED && cont->opt->format != PRINT_WRAPPED) { - IsPagerNeeded(cont, 0, (cont->opt->expanded == 1), &fout, &is_pager); + IsPagerNeeded(cont, NULL, (cont->opt->expanded == 1), &fout, &is_pager); is_local_pager = is_pager; } @@ -3464,10 +3657,6 @@ printTable(const printTableContent *cont, clearerr(fout); /* print the stuff */ - - if (flog) - print_aligned_text(cont, flog, false); - switch (cont->opt->format) { case PRINT_UNALIGNED: @@ -3534,6 +3723,10 @@ printTable(const printTableContent *cont, if (is_local_pager) ClosePager(fout); + + /* also produce log output if wanted */ + if (flog) + print_aligned_text(cont, flog, false); } /* @@ -3582,6 +3775,10 @@ printQuery(const PGresult *result, const printQueryOpt *opt, if (PQgetisnull(result, r, c)) cell = opt->nullPrint ? opt->nullPrint : ""; + else if (PQftype(result, c) == BOOLOID) + cell = (PQgetvalue(result, r, c)[0] == 't' ? + (opt->truePrint ? opt->truePrint : "t") : + (opt->falsePrint ? opt->falsePrint : "f")); else { cell = PQgetvalue(result, r, c); @@ -3624,6 +3821,7 @@ column_type_alignment(Oid ftype) case FLOAT8OID: case NUMERICOID: case OIDOID: + case OID8OID: case XIDOID: case XID8OID: case CIDOID: diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l index fc5bfa597bb92..d29dda4d8e1e9 100644 --- a/src/fe_utils/psqlscan.l +++ b/src/fe_utils/psqlscan.l @@ -24,7 +24,7 @@ * See psqlscan_int.h for additional commentary. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -293,6 +293,8 @@ less_equals "<=" greater_equals ">=" less_greater "<>" not_equals "!=" +/* Note there is no need for left_arrow, since "<-" is not a single operator. */ +right_arrow "->" /* * "self" is the set of chars that should be returned as single-character @@ -304,7 +306,7 @@ not_equals "!=" * If you change either set, adjust the character lists appearing in the * rule for "operator"! */ -self [,()\[\].;\:\+\-\*\/\%\^\<\>\=] +self [,()\[\].;\:\|\+\-\*\/\%\^\<\>\=] op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=] operator {op_chars}+ @@ -652,6 +654,10 @@ other . ECHO; } +{right_arrow} { + ECHO; + } + /* * These rules are specific to psql --- they implement parenthesis * counting and detection of command-ending semicolon. These must @@ -922,19 +928,24 @@ other . * writing a full parser here, the following heuristic * should work. First, we track whether the beginning of * the statement matches CREATE [OR REPLACE] - * {FUNCTION|PROCEDURE} + * {FUNCTION|PROCEDURE|SCHEMA}. (Allowing this in + * CREATE SCHEMA, without tracking whether we're within a + * CREATE FUNCTION/PROCEDURE subcommand, is a bit shaky + * but should be okay with the present set of valid + * subcommands.) */ if (cur_state->identifier_count == 0) memset(cur_state->identifiers, 0, sizeof(cur_state->identifiers)); - if (pg_strcasecmp(yytext, "create") == 0 || - pg_strcasecmp(yytext, "function") == 0 || - pg_strcasecmp(yytext, "procedure") == 0 || - pg_strcasecmp(yytext, "or") == 0 || - pg_strcasecmp(yytext, "replace") == 0) + if (cur_state->identifier_count < sizeof(cur_state->identifiers)) { - if (cur_state->identifier_count < sizeof(cur_state->identifiers)) + if (pg_strcasecmp(yytext, "create") == 0 || + pg_strcasecmp(yytext, "function") == 0 || + pg_strcasecmp(yytext, "procedure") == 0 || + pg_strcasecmp(yytext, "or") == 0 || + pg_strcasecmp(yytext, "replace") == 0 || + pg_strcasecmp(yytext, "schema") == 0) cur_state->identifiers[cur_state->identifier_count] = pg_tolower((unsigned char) yytext[0]); } @@ -943,7 +954,8 @@ other . if (cur_state->identifiers[0] == 'c' && (cur_state->identifiers[1] == 'f' || cur_state->identifiers[1] == 'p' || (cur_state->identifiers[1] == 'o' && cur_state->identifiers[2] == 'r' && - (cur_state->identifiers[3] == 'f' || cur_state->identifiers[3] == 'p'))) && + (cur_state->identifiers[3] == 'f' || cur_state->identifiers[3] == 'p')) || + cur_state->identifiers[1] == 's') && cur_state->paren_depth == 0) { if (pg_strcasecmp(yytext, "begin") == 0) @@ -1002,7 +1014,7 @@ psql_scan_create(const PsqlScanCallbacks *callbacks) { PsqlScanState state; - state = (PsqlScanStateData *) pg_malloc0(sizeof(PsqlScanStateData)); + state = pg_malloc0_object(PsqlScanStateData); state->callbacks = callbacks; @@ -1376,7 +1388,7 @@ psqlscan_push_new_buffer(PsqlScanState state, const char *newstr, { StackElem *stackelem; - stackelem = (StackElem *) pg_malloc(sizeof(StackElem)); + stackelem = pg_malloc_object(StackElem); /* * In current usage, the passed varname points at the current flex input @@ -1479,7 +1491,7 @@ psqlscan_prepare_buffer(PsqlScanState state, const char *txt, int len, char *newtxt; /* Flex wants two \0 characters after the actual data */ - newtxt = pg_malloc(len + 2); + newtxt = pg_malloc_array(char, (len + 2)); *txtcopy = newtxt; newtxt[len] = newtxt[len + 1] = YY_END_OF_BUFFER_CHAR; @@ -1548,7 +1560,7 @@ psqlscan_emit(PsqlScanState state, const char *txt, int len) char * psqlscan_extract_substring(PsqlScanState state, const char *txt, int len) { - char *result = (char *) pg_malloc(len + 1); + char *result = pg_malloc_array(char, (len + 1)); if (state->safe_encoding) memcpy(result, txt, len); diff --git a/src/fe_utils/query_utils.c b/src/fe_utils/query_utils.c index 1fa727a81c853..c05fd9c21df9d 100644 --- a/src/fe_utils/query_utils.c +++ b/src/fe_utils/query_utils.c @@ -2,7 +2,7 @@ * * Facilities for frontend code to query a databases. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/query_utils.c diff --git a/src/fe_utils/recovery_gen.c b/src/fe_utils/recovery_gen.c index e9023584768d5..f352652c785e7 100644 --- a/src/fe_utils/recovery_gen.c +++ b/src/fe_utils/recovery_gen.c @@ -3,7 +3,7 @@ * recovery_gen.c * Generator for recovery configuration * - * Portions Copyright (c) 2011-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2011-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ diff --git a/src/fe_utils/simple_list.c b/src/fe_utils/simple_list.c index 483d5455594b3..03c11bae7c8de 100644 --- a/src/fe_utils/simple_list.c +++ b/src/fe_utils/simple_list.c @@ -7,7 +7,7 @@ * it's all we need in, eg, pg_dump. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/simple_list.c @@ -27,7 +27,7 @@ simple_oid_list_append(SimpleOidList *list, Oid val) { SimpleOidListCell *cell; - cell = (SimpleOidListCell *) pg_malloc(sizeof(SimpleOidListCell)); + cell = pg_malloc_object(SimpleOidListCell); cell->next = NULL; cell->val = val; @@ -163,7 +163,7 @@ simple_ptr_list_append(SimplePtrList *list, void *ptr) { SimplePtrListCell *cell; - cell = (SimplePtrListCell *) pg_malloc(sizeof(SimplePtrListCell)); + cell = pg_malloc_object(SimplePtrListCell); cell->next = NULL; cell->ptr = ptr; diff --git a/src/fe_utils/string_utils.c b/src/fe_utils/string_utils.c index 130d1020d504d..38fffbd036bf7 100644 --- a/src/fe_utils/string_utils.c +++ b/src/fe_utils/string_utils.c @@ -6,7 +6,7 @@ * and interpreting backend output. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/fe_utils/string_utils.c @@ -449,9 +449,9 @@ appendStringLiteralConn(PQExpBuffer buf, const char *str, PGconn *conn) /* * XXX This is a kluge to silence escape_string_warning in our utility - * programs. It should go away someday. + * programs. It can go away once pre-v19 servers are out of support. */ - if (strchr(str, '\\') != NULL && PQserverVersion(conn) >= 80100) + if (strchr(str, '\\') != NULL && PQserverVersion(conn) < 190000) { /* ensure we are not adjacent to an identifier */ if (buf->len > 0 && buf->data[buf->len - 1] != ' ') @@ -568,12 +568,6 @@ appendByteaLiteral(PQExpBuffer buf, const unsigned char *str, size_t length, * Append the given string to the shell command being built in the buffer, * with shell-style quoting as needed to create exactly one argument. * - * Forbid LF or CR characters, which have scant practical use beyond designing - * security breaches. The Windows command shell is unusable as a conduit for - * arguments containing LF or CR characters. A future major release should - * reject those characters in CREATE ROLE and CREATE DATABASE, because use - * there eventually leads to errors here. - * * appendShellString() simply prints an error and dies if LF or CR appears. * appendShellStringNoError() omits those characters from the result, and * returns false if there were any. diff --git a/src/fe_utils/version.c b/src/fe_utils/version.c new file mode 100644 index 0000000000000..c9ea114afd052 --- /dev/null +++ b/src/fe_utils/version.c @@ -0,0 +1,86 @@ +/*------------------------------------------------------------------------- + * + * version.c + * Routine to retrieve information of PG_VERSION + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/fe_utils/version.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include + +#include "common/logging.h" +#include "fe_utils/version.h" + +/* + * Assumed maximum size of PG_VERSION. This should be more than enough for + * any version numbers that need to be handled. + */ +#define PG_VERSION_MAX_SIZE 64 + +/* + * get_pg_version + * + * Retrieve the major version number of the given data folder, from + * PG_VERSION. The result returned is a version number, that can be used + * for comparisons based on PG_VERSION_NUM. For example, if PG_VERSION + * contains "18\n", this function returns 180000. + * + * This supports both the pre-v10 and the post-v10 version numbering. + * + * Optionally, "version_str" can be specified to store the contents + * retrieved from PG_VERSION. It is allocated by this routine; the + * caller is responsible for pg_free()-ing it. + */ +uint32 +get_pg_version(const char *datadir, char **version_str) +{ + FILE *version_fd; + char ver_filename[MAXPGPATH]; + char buf[PG_VERSION_MAX_SIZE]; + int v1 = 0, + v2 = 0; + struct stat st; + + snprintf(ver_filename, sizeof(ver_filename), "%s/PG_VERSION", + datadir); + + if ((version_fd = fopen(ver_filename, "r")) == NULL) + pg_fatal("could not open version file \"%s\": %m", ver_filename); + + if (fstat(fileno(version_fd), &st) != 0) + pg_fatal("could not stat file \"%s\": %m", ver_filename); + if (st.st_size > PG_VERSION_MAX_SIZE) + pg_fatal("file \"%s\" is too large", ver_filename); + + if (fscanf(version_fd, "%63s", buf) == 0 || + sscanf(buf, "%d.%d", &v1, &v2) < 1) + pg_fatal("could not parse version file \"%s\"", ver_filename); + + fclose(version_fd); + + if (version_str) + { + *version_str = pg_malloc(PG_VERSION_MAX_SIZE); + memcpy(*version_str, buf, st.st_size); + } + + if (v1 < 10) + { + /* pre-v10 style, e.g. 9.6.1 */ + return v1 * 10000 + v2 * 100; + } + else + { + /* post-v10 style, e.g. 10.1 */ + return v1 * 10000; + } +} diff --git a/src/include/Makefile b/src/include/Makefile index 3f94543f3270b..ac673f4cf17b7 100644 --- a/src/include/Makefile +++ b/src/include/Makefile @@ -17,14 +17,46 @@ all: pg_config.h pg_config_os.h # Subdirectories containing installable headers -SUBDIRS = access archive bootstrap catalog commands common datatype \ - executor fe_utils foreign jit \ - lib libpq mb nodes optimizer parser partitioning postmaster \ - regex replication rewrite \ - statistics storage tcop snowball snowball/libstemmer tsearch \ - tsearch/dicts utils port port/atomics port/win32 port/win32_msvc \ - port/win32_msvc/sys port/win32/arpa port/win32/netinet \ - port/win32/sys portability +SUBDIRS = \ + access \ + archive \ + bootstrap \ + catalog \ + commands \ + common \ + datatype \ + executor \ + fe_utils \ + foreign \ + jit \ + lib \ + libpq \ + mb \ + nodes \ + optimizer \ + parser \ + partitioning \ + postmaster \ + regex \ + replication \ + rewrite \ + statistics \ + storage \ + tcop \ + snowball \ + snowball/libstemmer \ + tsearch \ + tsearch/dicts \ + utils \ + port \ + port/atomics \ + port/win32 \ + port/win32_msvc \ + port/win32_msvc/sys \ + port/win32/arpa \ + port/win32/netinet \ + port/win32/sys \ + portability # Install all headers install: all installdirs @@ -72,7 +104,8 @@ uninstall: clean: - rm -f utils/fmgroids.h utils/fmgrprotos.h utils/errcodes.h utils/header-stamp + rm -f utils/fmgroids.h utils/fmgrprotos.h utils/guc_tables.inc.c utils/errcodes.h utils/header-stamp + rm -f utils/pgstat_wait_event.c utils/wait_event_funcs_data.c rm -f storage/lwlocknames.h utils/probes.h utils/wait_event_types.h rm -f nodes/nodetags.h nodes/header-stamp $(MAKE) -C catalog clean diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h index 52916bab7a31f..ecfbd017d66dc 100644 --- a/src/include/access/amapi.h +++ b/src/include/access/amapi.h @@ -3,7 +3,7 @@ * amapi.h * API for Postgres index access methods. * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * src/include/access/amapi.h * @@ -15,17 +15,19 @@ #include "access/cmptype.h" #include "access/genam.h" #include "access/stratnum.h" +#include "nodes/nodes.h" +#include "nodes/pg_list.h" /* * We don't wish to include planner header files here, since most of an index * AM's implementation isn't concerned with those data structures. To allow * declaring amcostestimate_function here, use forward struct references. */ -struct PlannerInfo; -struct IndexPath; +typedef struct PlannerInfo PlannerInfo; +typedef struct IndexPath IndexPath; /* Likewise, this file shouldn't depend on execnodes.h. */ -struct IndexInfo; +typedef struct IndexInfo IndexInfo; /* @@ -110,7 +112,7 @@ typedef StrategyNumber (*amtranslate_cmptype_function) (CompareType cmptype, Oid /* build new index */ typedef IndexBuildResult *(*ambuild_function) (Relation heapRelation, Relation indexRelation, - struct IndexInfo *indexInfo); + IndexInfo *indexInfo); /* build empty index */ typedef void (*ambuildempty_function) (Relation indexRelation); @@ -123,11 +125,11 @@ typedef bool (*aminsert_function) (Relation indexRelation, Relation heapRelation, IndexUniqueCheck checkUnique, bool indexUnchanged, - struct IndexInfo *indexInfo); + IndexInfo *indexInfo); /* cleanup after insert */ typedef void (*aminsertcleanup_function) (Relation indexRelation, - struct IndexInfo *indexInfo); + IndexInfo *indexInfo); /* bulk delete */ typedef IndexBulkDeleteResult *(*ambulkdelete_function) (IndexVacuumInfo *info, @@ -143,8 +145,8 @@ typedef IndexBulkDeleteResult *(*amvacuumcleanup_function) (IndexVacuumInfo *inf typedef bool (*amcanreturn_function) (Relation indexRelation, int attno); /* estimate cost of an indexscan */ -typedef void (*amcostestimate_function) (struct PlannerInfo *root, - struct IndexPath *path, +typedef void (*amcostestimate_function) (PlannerInfo *root, + IndexPath *path, double loop_count, Cost *indexStartupCost, Cost *indexTotalCost, @@ -224,8 +226,8 @@ typedef void (*aminitparallelscan_function) (void *target); typedef void (*amparallelrescan_function) (IndexScanDesc scan); /* - * API struct for an index AM. Note this must be stored in a single palloc'd - * chunk of memory. + * API struct for an index AM. Note we expect index AMs to allocate these + * structs statically; the core code never copies nor frees them. */ typedef struct IndexAmRoutine { @@ -293,7 +295,7 @@ typedef struct IndexAmRoutine ambuild_function ambuild; ambuildempty_function ambuildempty; aminsert_function aminsert; - aminsertcleanup_function aminsertcleanup; + aminsertcleanup_function aminsertcleanup; /* can be NULL */ ambulkdelete_function ambulkdelete; amvacuumcleanup_function amvacuumcleanup; amcanreturn_function amcanreturn; /* can be NULL */ @@ -324,8 +326,8 @@ typedef struct IndexAmRoutine /* Functions in access/index/amapi.c */ -extern IndexAmRoutine *GetIndexAmRoutine(Oid amhandler); -extern IndexAmRoutine *GetIndexAmRoutineByAmId(Oid amoid, bool noerror); +extern const IndexAmRoutine *GetIndexAmRoutine(Oid amhandler); +extern const IndexAmRoutine *GetIndexAmRoutineByAmId(Oid amoid, bool noerror); extern CompareType IndexAmTranslateStrategy(StrategyNumber strategy, Oid amoid, Oid opfamily, bool missing_ok); extern StrategyNumber IndexAmTranslateCompareType(CompareType cmptype, Oid amoid, Oid opfamily, bool missing_ok); diff --git a/src/include/access/amvalidate.h b/src/include/access/amvalidate.h index 43b1692b07937..b82998778ac25 100644 --- a/src/include/access/amvalidate.h +++ b/src/include/access/amvalidate.h @@ -4,7 +4,7 @@ * Support routines for index access methods' amvalidate and * amadjustmembers functions. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/access/amvalidate.h * diff --git a/src/include/access/attmap.h b/src/include/access/attmap.h index 8f9218826c992..daf1502db6f98 100644 --- a/src/include/access/attmap.h +++ b/src/include/access/attmap.h @@ -4,7 +4,7 @@ * Definitions for PostgreSQL attribute mappings * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/attmap.h diff --git a/src/include/access/attnum.h b/src/include/access/attnum.h index 06c91672018be..f0fe73048ecbc 100644 --- a/src/include/access/attnum.h +++ b/src/include/access/attnum.h @@ -4,7 +4,7 @@ * POSTGRES attribute number definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/attnum.h diff --git a/src/include/access/brin.h b/src/include/access/brin.h index 821f1e02806d8..a5b453468d11d 100644 --- a/src/include/access/brin.h +++ b/src/include/access/brin.h @@ -1,7 +1,7 @@ /* * AM-callable functions for BRIN indexes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -10,7 +10,8 @@ #ifndef BRIN_H #define BRIN_H -#include "nodes/execnodes.h" +#include "storage/block.h" +#include "storage/dsm.h" #include "storage/shm_toc.h" #include "utils/relcache.h" diff --git a/src/include/access/brin_internal.h b/src/include/access/brin_internal.h index d093a0bf1307a..e17c7fee51150 100644 --- a/src/include/access/brin_internal.h +++ b/src/include/access/brin_internal.h @@ -2,7 +2,7 @@ * brin_internal.h * internal declarations for BRIN indexes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -89,14 +89,14 @@ typedef struct BrinDesc extern BrinDesc *brin_build_desc(Relation rel); extern void brin_free_desc(BrinDesc *bdesc); extern IndexBuildResult *brinbuild(Relation heap, Relation index, - struct IndexInfo *indexInfo); + IndexInfo *indexInfo); extern void brinbuildempty(Relation index); extern bool brininsert(Relation idxRel, Datum *values, bool *nulls, ItemPointer heaptid, Relation heapRel, IndexUniqueCheck checkUnique, bool indexUnchanged, - struct IndexInfo *indexInfo); -extern void brininsertcleanup(Relation index, struct IndexInfo *indexInfo); + IndexInfo *indexInfo); +extern void brininsertcleanup(Relation index, IndexInfo *indexInfo); extern IndexScanDesc brinbeginscan(Relation r, int nkeys, int norderbys); extern int64 bringetbitmap(IndexScanDesc scan, TIDBitmap *tbm); extern void brinrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys, diff --git a/src/include/access/brin_page.h b/src/include/access/brin_page.h index 06742a3c6dd7b..3297934c1779e 100644 --- a/src/include/access/brin_page.h +++ b/src/include/access/brin_page.h @@ -2,7 +2,7 @@ * brin_page.h * Prototypes and definitions for BRIN page layouts * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/access/brin_pageops.h b/src/include/access/brin_pageops.h index be343767027fb..b22ddec215bec 100644 --- a/src/include/access/brin_pageops.h +++ b/src/include/access/brin_pageops.h @@ -2,7 +2,7 @@ * brin_pageops.h * Prototypes for operating on BRIN pages. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -23,7 +23,7 @@ extern bool brin_can_do_samepage_update(Buffer buffer, Size origsz, Size newsz); extern OffsetNumber brin_doinsert(Relation idxrel, BlockNumber pagesPerRange, BrinRevmap *revmap, Buffer *buffer, BlockNumber heapBlk, - BrinTuple *tup, Size itemsz); + const BrinTuple *tup, Size itemsz); extern void brin_page_init(Page page, uint16 type); extern void brin_metapage_init(Page page, BlockNumber pagesPerRange, diff --git a/src/include/access/brin_revmap.h b/src/include/access/brin_revmap.h index b88473c9860db..a0f1971660762 100644 --- a/src/include/access/brin_revmap.h +++ b/src/include/access/brin_revmap.h @@ -2,7 +2,7 @@ * brin_revmap.h * Prototypes for BRIN reverse range maps * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/access/brin_tuple.h b/src/include/access/brin_tuple.h index 010ba4ea3c0ee..c818286e52370 100644 --- a/src/include/access/brin_tuple.h +++ b/src/include/access/brin_tuple.h @@ -2,7 +2,7 @@ * brin_tuple.h * Declarations for dealing with BRIN-specific tuples. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/access/brin_xlog.h b/src/include/access/brin_xlog.h index 992b057f4600a..1c60dc8adb9c5 100644 --- a/src/include/access/brin_xlog.h +++ b/src/include/access/brin_xlog.h @@ -4,7 +4,7 @@ * POSTGRES BRIN access XLOG definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/brin_xlog.h diff --git a/src/include/access/bufmask.h b/src/include/access/bufmask.h index b6632cf62732f..1e3f2006735c2 100644 --- a/src/include/access/bufmask.h +++ b/src/include/access/bufmask.h @@ -7,7 +7,7 @@ * individual rmgr, but we make things easier by providing some * common routines to handle cases which occur in multiple rmgrs. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/access/bufmask.h * diff --git a/src/include/access/clog.h b/src/include/access/clog.h index 7eb3ffede4c1f..7894998c76369 100644 --- a/src/include/access/clog.h +++ b/src/include/access/clog.h @@ -3,7 +3,7 @@ * * PostgreSQL transaction-commit-log manager * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/clog.h @@ -40,8 +40,6 @@ extern void TransactionIdSetTreeStatus(TransactionId xid, int nsubxids, TransactionId *subxids, XidStatus status, XLogRecPtr lsn); extern XidStatus TransactionIdGetStatus(TransactionId xid, XLogRecPtr *lsn); -extern Size CLOGShmemSize(void); -extern void CLOGShmemInit(void); extern void BootStrapCLOG(void); extern void StartupCLOG(void); extern void TrimCLOG(void); diff --git a/src/include/access/cmptype.h b/src/include/access/cmptype.h index ed6da1eada12b..1125b19e3df67 100644 --- a/src/include/access/cmptype.h +++ b/src/include/access/cmptype.h @@ -3,7 +3,7 @@ * cmptype.h * POSTGRES compare type definitions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/cmptype.h diff --git a/src/include/access/commit_ts.h b/src/include/access/commit_ts.h index b8294e41b978e..825ccda90eda1 100644 --- a/src/include/access/commit_ts.h +++ b/src/include/access/commit_ts.h @@ -3,7 +3,7 @@ * * PostgreSQL commit timestamp manager * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/commit_ts.h @@ -21,14 +21,12 @@ extern PGDLLIMPORT bool track_commit_timestamp; extern void TransactionTreeSetCommitTsData(TransactionId xid, int nsubxids, TransactionId *subxids, TimestampTz timestamp, - RepOriginId nodeid); + ReplOriginId nodeid); extern bool TransactionIdGetCommitTsData(TransactionId xid, - TimestampTz *ts, RepOriginId *nodeid); + TimestampTz *ts, ReplOriginId *nodeid); extern TransactionId GetLatestCommitTsData(TimestampTz *ts, - RepOriginId *nodeid); + ReplOriginId *nodeid); -extern Size CommitTsShmemSize(void); -extern void CommitTsShmemInit(void); extern void BootStrapCommitTs(void); extern void StartupCommitTs(void); extern void CommitTsParameterChange(bool newvalue, bool oldvalue); @@ -46,17 +44,6 @@ extern int committssyncfiletag(const FileTag *ftag, char *path); #define COMMIT_TS_ZEROPAGE 0x00 #define COMMIT_TS_TRUNCATE 0x10 -typedef struct xl_commit_ts_set -{ - TimestampTz timestamp; - RepOriginId nodeid; - TransactionId mainxid; - /* subxact Xids follow */ -} xl_commit_ts_set; - -#define SizeOfCommitTsSet (offsetof(xl_commit_ts_set, mainxid) + \ - sizeof(TransactionId)) - typedef struct xl_commit_ts_truncate { int64 pageno; diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h index e603a2276c381..fbd98181a3a18 100644 --- a/src/include/access/detoast.h +++ b/src/include/access/detoast.h @@ -3,7 +3,7 @@ * detoast.h * Access to compressed and external varlena values. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/include/access/detoast.h * @@ -14,7 +14,7 @@ /* * Macro to fetch the possibly-unaligned contents of an EXTERNAL datum - * into a local "struct varatt_external" toast pointer. This should be + * into a local "varatt_external" toast pointer. This should be * just a memcpy, but some versions of gcc seem to produce broken code * that assumes the datum contents are aligned. Introducing an explicit * intermediate "varattrib_1b_e *" variable seems to fix it. @@ -41,7 +41,7 @@ do { \ * in compressed format. * ---------- */ -extern struct varlena *detoast_external_attr(struct varlena *attr); +extern varlena *detoast_external_attr(varlena *attr); /* ---------- * detoast_attr() - @@ -50,7 +50,7 @@ extern struct varlena *detoast_external_attr(struct varlena *attr); * it as needed. * ---------- */ -extern struct varlena *detoast_attr(struct varlena *attr); +extern varlena *detoast_attr(varlena *attr); /* ---------- * detoast_attr_slice() - @@ -59,9 +59,9 @@ extern struct varlena *detoast_attr(struct varlena *attr); * (Handles all cases for attribute storage) * ---------- */ -extern struct varlena *detoast_attr_slice(struct varlena *attr, - int32 sliceoffset, - int32 slicelength); +extern varlena *detoast_attr_slice(varlena *attr, + int32 sliceoffset, + int32 slicelength); /* ---------- * toast_raw_datum_size - diff --git a/src/include/access/genam.h b/src/include/access/genam.h index 5b2ab181b5f8d..68bfe405db3a0 100644 --- a/src/include/access/genam.h +++ b/src/include/access/genam.h @@ -4,7 +4,7 @@ * POSTGRES generalized index access method definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/genam.h @@ -17,35 +17,20 @@ #include "access/htup.h" #include "access/sdir.h" #include "access/skey.h" -#include "nodes/tidbitmap.h" +#include "executor/instrument_node.h" #include "storage/buf.h" #include "storage/lockdefs.h" -#include "utils/relcache.h" #include "utils/snapshot.h" -/* We don't want this file to depend on execnodes.h. */ -struct IndexInfo; /* - * Struct for statistics maintained by amgettuple and amgetbitmap - * - * Note: IndexScanInstrumentation can't contain any pointers, since it is - * copied into a SharedIndexScanInstrumentation during parallel scans + * forward references in this file */ -typedef struct IndexScanInstrumentation -{ - /* Index search count (incremented with pgstat_count_index_scan call) */ - uint64 nsearches; -} IndexScanInstrumentation; +typedef struct IndexInfo IndexInfo; +typedef struct RelationData *Relation; +typedef struct TIDBitmap TIDBitmap; +typedef struct TupleTableSlot TupleTableSlot; -/* - * Struct for every worker's IndexScanInstrumentation, stored in shared memory - */ -typedef struct SharedIndexScanInstrumentation -{ - int num_workers; - IndexScanInstrumentation winstrument[FLEXIBLE_ARRAY_MEMBER]; -} SharedIndexScanInstrumentation; /* * Struct for statistics returned by ambuild @@ -155,12 +140,6 @@ typedef struct IndexOrderByDistance * generalized index_ interface routines (in indexam.c) */ -/* - * IndexScanIsValid - * True iff the index scan is valid. - */ -#define IndexScanIsValid(scan) PointerIsValid(scan) - extern Relation index_open(Oid relationId, LOCKMODE lockmode); extern Relation try_index_open(Oid relationId, LOCKMODE lockmode); extern void index_close(Relation relation, LOCKMODE lockmode); @@ -171,15 +150,16 @@ extern bool index_insert(Relation indexRelation, Relation heapRelation, IndexUniqueCheck checkUnique, bool indexUnchanged, - struct IndexInfo *indexInfo); + IndexInfo *indexInfo); extern void index_insert_cleanup(Relation indexRelation, - struct IndexInfo *indexInfo); + IndexInfo *indexInfo); extern IndexScanDesc index_beginscan(Relation heapRelation, Relation indexRelation, Snapshot snapshot, IndexScanInstrumentation *instrument, - int nkeys, int norderbys); + int nkeys, int norderbys, + uint32 flags); extern IndexScanDesc index_beginscan_bitmap(Relation indexRelation, Snapshot snapshot, IndexScanInstrumentation *instrument, @@ -191,27 +171,22 @@ extern void index_endscan(IndexScanDesc scan); extern void index_markpos(IndexScanDesc scan); extern void index_restrpos(IndexScanDesc scan); extern Size index_parallelscan_estimate(Relation indexRelation, - int nkeys, int norderbys, Snapshot snapshot, - bool instrument, bool parallel_aware, - int nworkers); + int nkeys, int norderbys, Snapshot snapshot); extern void index_parallelscan_initialize(Relation heapRelation, Relation indexRelation, Snapshot snapshot, - bool instrument, bool parallel_aware, - int nworkers, - SharedIndexScanInstrumentation **sharedinfo, ParallelIndexScanDesc target); extern void index_parallelrescan(IndexScanDesc scan); extern IndexScanDesc index_beginscan_parallel(Relation heaprel, Relation indexrel, IndexScanInstrumentation *instrument, int nkeys, int norderbys, - ParallelIndexScanDesc pscan); + ParallelIndexScanDesc pscan, + uint32 flags); extern ItemPointer index_getnext_tid(IndexScanDesc scan, ScanDirection direction); -struct TupleTableSlot; -extern bool index_fetch_heap(IndexScanDesc scan, struct TupleTableSlot *slot); +extern bool index_fetch_heap(IndexScanDesc scan, TupleTableSlot *slot); extern bool index_getnext_slot(IndexScanDesc scan, ScanDirection direction, - struct TupleTableSlot *slot); + TupleTableSlot *slot); extern int64 index_getbitmap(IndexScanDesc scan, TIDBitmap *bitmap); extern IndexBulkDeleteResult *index_bulk_delete(IndexVacuumInfo *info, diff --git a/src/include/access/generic_xlog.h b/src/include/access/generic_xlog.h index 4d1cbab55e1dd..93d93f73ddcb0 100644 --- a/src/include/access/generic_xlog.h +++ b/src/include/access/generic_xlog.h @@ -4,7 +4,7 @@ * Generic xlog API definition. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/generic_xlog.h diff --git a/src/include/access/gin.h b/src/include/access/gin.h index 2e1076a04992b..fa1a3b20e099e 100644 --- a/src/include/access/gin.h +++ b/src/include/access/gin.h @@ -2,7 +2,7 @@ * gin.h * Public header file for Generalized Inverted Index access method. * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * src/include/access/gin.h *-------------------------------------------------------------------------- @@ -12,9 +12,9 @@ #include "access/xlogreader.h" #include "lib/stringinfo.h" -#include "nodes/execnodes.h" -#include "storage/shm_toc.h" #include "storage/block.h" +#include "storage/dsm.h" +#include "storage/shm_toc.h" #include "utils/relcache.h" diff --git a/src/include/access/gin_private.h b/src/include/access/gin_private.h index aee1f70c22eee..6725ee2839f6e 100644 --- a/src/include/access/gin_private.h +++ b/src/include/access/gin_private.h @@ -2,7 +2,7 @@ * gin_private.h * header file for postgres inverted index access method implementation. * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * src/include/access/gin_private.h *-------------------------------------------------------------------------- @@ -13,11 +13,13 @@ #include "access/amapi.h" #include "access/gin.h" #include "access/ginblock.h" +#include "access/htup_details.h" #include "access/itup.h" #include "common/int.h" #include "catalog/pg_am_d.h" #include "fmgr.h" #include "lib/rbtree.h" +#include "nodes/tidbitmap.h" #include "storage/bufmgr.h" /* @@ -96,15 +98,9 @@ extern Buffer GinNewBuffer(Relation index); extern void GinInitBuffer(Buffer b, uint32 f); extern void GinInitPage(Page page, uint32 f, Size pageSize); extern void GinInitMetabuffer(Buffer b); -extern int ginCompareEntries(GinState *ginstate, OffsetNumber attnum, - Datum a, GinNullCategory categorya, - Datum b, GinNullCategory categoryb); -extern int ginCompareAttEntries(GinState *ginstate, - OffsetNumber attnuma, Datum a, GinNullCategory categorya, - OffsetNumber attnumb, Datum b, GinNullCategory categoryb); extern Datum *ginExtractEntries(GinState *ginstate, OffsetNumber attnum, Datum value, bool isNull, - int32 *nentries, GinNullCategory **categories); + int32 *nentries_p, GinNullCategory **categories_p); extern OffsetNumber gintuple_get_attrnum(GinState *ginstate, IndexTuple tuple); extern Datum gintuple_get_key(GinState *ginstate, IndexTuple tuple, @@ -332,7 +328,7 @@ typedef struct GinScanKeyData bool curItemMatches; bool recheckCurItem; bool isFinished; -} GinScanKeyData; +} GinScanKeyData; typedef struct GinScanEntryData { @@ -372,7 +368,7 @@ typedef struct GinScanEntryData bool reduceResult; uint32 predictNumberResult; GinBtreeData btree; -} GinScanEntryData; +} GinScanEntryData; typedef struct GinScanOpaqueData { @@ -477,7 +473,7 @@ extern void ginInsertCleanup(GinState *ginstate, bool full_clean, /* ginpostinglist.c */ -extern GinPostingList *ginCompressPostingList(const ItemPointer ipd, int nipd, +extern GinPostingList *ginCompressPostingList(const ItemPointerData *ipd, int nipd, int maxsize, int *nwritten); extern int ginPostingListDecodeAllSegmentsToTbm(GinPostingList *ptr, int len, TIDBitmap *tbm); @@ -493,7 +489,7 @@ extern ItemPointer ginMergeItemPointers(ItemPointerData *a, uint32 na, * so we want this to be inlined. */ static inline int -ginCompareItemPointers(ItemPointer a, ItemPointer b) +ginCompareItemPointers(const ItemPointerData *a, const ItemPointerData *b) { uint64 ia = (uint64) GinItemPointerGetBlockNumber(a) << 32 | GinItemPointerGetOffsetNumber(a); uint64 ib = (uint64) GinItemPointerGetBlockNumber(b) << 32 | GinItemPointerGetOffsetNumber(b); @@ -501,6 +497,44 @@ ginCompareItemPointers(ItemPointer a, ItemPointer b) return pg_cmp_u64(ia, ib); } +/* + * Compare two keys of the same index column + */ +static inline int +ginCompareEntries(GinState *ginstate, OffsetNumber attnum, + Datum a, GinNullCategory categorya, + Datum b, GinNullCategory categoryb) +{ + /* if not of same null category, sort by that first */ + if (categorya != categoryb) + return (categorya < categoryb) ? -1 : 1; + + /* all null items in same category are equal */ + if (categorya != GIN_CAT_NORM_KEY) + return 0; + + /* both not null, so safe to call the compareFn */ + return DatumGetInt32(FunctionCall2Coll(&ginstate->compareFn[attnum - 1], + ginstate->supportCollation[attnum - 1], + a, b)); +} + +/* + * Compare two keys of possibly different index columns + */ +static inline int +ginCompareAttEntries(GinState *ginstate, + OffsetNumber attnuma, Datum a, GinNullCategory categorya, + OffsetNumber attnumb, Datum b, GinNullCategory categoryb) +{ + /* attribute number is the first sort key */ + if (attnuma != attnumb) + return (attnuma < attnumb) ? -1 : 1; + + return ginCompareEntries(ginstate, attnuma, a, categorya, b, categoryb); + +} + extern int ginTraverseLock(Buffer buffer, bool searchMode); #endif /* GIN_PRIVATE_H */ diff --git a/src/include/access/gin_tuple.h b/src/include/access/gin_tuple.h index 702f7d12889d8..7bde05e2de4b1 100644 --- a/src/include/access/gin_tuple.h +++ b/src/include/access/gin_tuple.h @@ -1,10 +1,10 @@ /*-------------------------------------------------------------------------- - * gin.h + * gin_tuple.h * Public header file for Generalized Inverted Index access method. * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * - * src/include/access/gin.h + * src/include/access/gin_tuple.h *-------------------------------------------------------------------------- */ #ifndef GIN_TUPLE_H @@ -15,7 +15,9 @@ #include "utils/sortsupport.h" /* - * Data for one key in a GIN index. + * Data for one key in a GIN index. (This is not the permanent in-index + * representation, but just a convenient format to use during the tuplesort + * stage of building a new GIN index.) */ typedef struct GinTuple { diff --git a/src/include/access/ginblock.h b/src/include/access/ginblock.h index 4c1681068db2a..2d75023179a28 100644 --- a/src/include/access/ginblock.h +++ b/src/include/access/ginblock.h @@ -2,7 +2,7 @@ * ginblock.h * details of structures stored in GIN index blocks * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * src/include/access/ginblock.h *-------------------------------------------------------------------------- diff --git a/src/include/access/ginxlog.h b/src/include/access/ginxlog.h index f8c671c2e0d8e..74fdf9b3d7ed4 100644 --- a/src/include/access/ginxlog.h +++ b/src/include/access/ginxlog.h @@ -2,7 +2,7 @@ * ginxlog.h * header file for postgres inverted index xlog implementation. * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * src/include/access/ginxlog.h *-------------------------------------------------------------------------- @@ -179,6 +179,9 @@ typedef struct ginxlogUpdateMeta #define XLOG_GIN_INSERT_LISTPAGE 0x70 +/* + * Backup Blk 0: list page with inserted tuples + */ typedef struct ginxlogInsertListPage { BlockNumber rightlink; diff --git a/src/include/access/gist.h b/src/include/access/gist.h index db78e60eeab0b..9b385b13a8895 100644 --- a/src/include/access/gist.h +++ b/src/include/access/gist.h @@ -6,7 +6,7 @@ * changes should be made with care. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/gist.h @@ -40,7 +40,7 @@ #define GIST_FETCH_PROC 9 #define GIST_OPTIONS_PROC 10 #define GIST_SORTSUPPORT_PROC 11 -#define GIST_STRATNUM_PROC 12 +#define GIST_TRANSLATE_CMPTYPE_PROC 12 #define GISTNProcs 12 /* @@ -186,8 +186,8 @@ typedef struct GISTENTRY #define GistMarkFollowRight(page) ( GistPageGetOpaque(page)->flags |= F_FOLLOW_RIGHT) #define GistClearFollowRight(page) ( GistPageGetOpaque(page)->flags &= ~F_FOLLOW_RIGHT) -#define GistPageGetNSN(page) ( PageXLogRecPtrGet(GistPageGetOpaque(page)->nsn)) -#define GistPageSetNSN(page, val) ( PageXLogRecPtrSet(GistPageGetOpaque(page)->nsn, val)) +#define GistPageGetNSN(page) ( PageXLogRecPtrGet(&GistPageGetOpaque(page)->nsn)) +#define GistPageSetNSN(page, val) ( PageXLogRecPtrSet(&GistPageGetOpaque(page)->nsn, val)) /* diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h index 39404ec7cdb6d..44514f1cb8d81 100644 --- a/src/include/access/gist_private.h +++ b/src/include/access/gist_private.h @@ -4,7 +4,7 @@ * private declarations for GiST -- declarations related to the * internal implementation of GiST, not the public API * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/gist_private.h @@ -457,8 +457,6 @@ extern XLogRecPtr gistXLogSplit(bool page_is_leaf, BlockNumber origrlink, GistNSN orignsn, Buffer leftchildbuf, bool markfollowright); -extern XLogRecPtr gistXLogAssignLSN(void); - /* gistget.c */ extern bool gistgettuple(IndexScanDesc scan, ScanDirection dir); extern int64 gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm); @@ -531,8 +529,6 @@ extern void gistMakeUnionKey(GISTSTATE *giststate, int attno, GISTENTRY *entry2, bool isnull2, Datum *dst, bool *dstisnull); -extern XLogRecPtr gistGetFakeLSN(Relation rel); - /* gistvacuum.c */ extern IndexBulkDeleteResult *gistbulkdelete(IndexVacuumInfo *info, IndexBulkDeleteResult *stats, diff --git a/src/include/access/gistscan.h b/src/include/access/gistscan.h index 518034c36d54f..f0b216a86eb09 100644 --- a/src/include/access/gistscan.h +++ b/src/include/access/gistscan.h @@ -4,7 +4,7 @@ * routines defined in access/gist/gistscan.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/gistscan.h diff --git a/src/include/access/gistxlog.h b/src/include/access/gistxlog.h index d551c0a19d41e..1c2cf6e813afe 100644 --- a/src/include/access/gistxlog.h +++ b/src/include/access/gistxlog.h @@ -3,7 +3,7 @@ * gistxlog.h * gist xlog routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/gistxlog.h @@ -26,7 +26,7 @@ /* #define XLOG_GIST_INSERT_COMPLETE 0x40 */ /* not used anymore */ /* #define XLOG_GIST_CREATE_INDEX 0x50 */ /* not used anymore */ #define XLOG_GIST_PAGE_DELETE 0x60 -#define XLOG_GIST_ASSIGN_LSN 0x70 /* nop, assign new LSN */ + /* #define XLOG_GIST_ASSIGN_LSN 0x70 */ /* not used anymore */ /* * Backup Blk 0: updated page. diff --git a/src/include/access/hash.h b/src/include/access/hash.h index 073ad29b19b92..a8702f0e5ea13 100644 --- a/src/include/access/hash.h +++ b/src/include/access/hash.h @@ -4,7 +4,7 @@ * header file for postgres hash access method implementation * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/hash.h @@ -453,7 +453,7 @@ typedef struct HSpool HSpool; /* opaque struct in hashsort.c */ extern HSpool *_h_spoolinit(Relation heap, Relation index, uint32 num_buckets); extern void _h_spooldestroy(HSpool *hspool); -extern void _h_spool(HSpool *hspool, ItemPointer self, +extern void _h_spool(HSpool *hspool, const ItemPointerData *self, const Datum *values, const bool *isnull); extern void _h_indexbuild(HSpool *hspool, Relation heapRel); @@ -468,7 +468,7 @@ extern uint32 _hash_get_totalbuckets(uint32 splitpoint_phase); extern void _hash_checkpage(Relation rel, Buffer buf, int flags); extern uint32 _hash_get_indextuple_hashkey(IndexTuple itup); extern bool _hash_convert_tuple(Relation index, - Datum *user_values, bool *user_isnull, + const Datum *user_values, const bool *user_isnull, Datum *index_values, bool *index_isnull); extern OffsetNumber _hash_binsearch(Page page, uint32 hash_value); extern OffsetNumber _hash_binsearch_last(Page page, uint32 hash_value); diff --git a/src/include/access/hash_xlog.h b/src/include/access/hash_xlog.h index 6fe97de4d66f1..149aed69dc5c9 100644 --- a/src/include/access/hash_xlog.h +++ b/src/include/access/hash_xlog.h @@ -4,7 +4,7 @@ * header file for Postgres hash AM implementation * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/hash_xlog.h @@ -129,7 +129,7 @@ typedef struct xl_hash_split_complete * * This data record is used for XLOG_HASH_MOVE_PAGE_CONTENTS * - * Backup Blk 0: bucket page + * Backup Blk 0: primary bucket page * Backup Blk 1: page containing moved tuples * Backup Blk 2: page from which tuples will be removed */ @@ -149,12 +149,13 @@ typedef struct xl_hash_move_page_contents * * This data record is used for XLOG_HASH_SQUEEZE_PAGE * - * Backup Blk 0: page containing tuples moved from freed overflow page - * Backup Blk 1: freed overflow page - * Backup Blk 2: page previous to the freed overflow page - * Backup Blk 3: page next to the freed overflow page - * Backup Blk 4: bitmap page containing info of freed overflow page - * Backup Blk 5: meta page + * Backup Blk 0: primary bucket page + * Backup Blk 1: page containing tuples moved from freed overflow page + * Backup Blk 2: freed overflow page + * Backup Blk 3: page previous to the freed overflow page + * Backup Blk 4: page next to the freed overflow page + * Backup Blk 5: bitmap page containing info of freed overflow page + * Backup Blk 6: meta page */ typedef struct xl_hash_squeeze_page { @@ -245,7 +246,7 @@ typedef struct xl_hash_init_bitmap_page * * This data record is used for XLOG_HASH_VACUUM_ONE_PAGE * - * Backup Blk 0: bucket page + * Backup Blk 0: primary bucket page * Backup Blk 1: meta page */ typedef struct xl_hash_vacuum_one_page diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index e48fe434cd393..5176478c29583 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -4,7 +4,7 @@ * POSTGRES heap access method definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/heapam.h @@ -41,10 +41,14 @@ /* "options" flag bits for heap_page_prune_and_freeze */ #define HEAP_PAGE_PRUNE_MARK_UNUSED_NOW (1 << 0) #define HEAP_PAGE_PRUNE_FREEZE (1 << 1) +#define HEAP_PAGE_PRUNE_ALLOW_FAST_PATH (1 << 2) +#define HEAP_PAGE_PRUNE_SET_VM (1 << 3) typedef struct BulkInsertStateData *BulkInsertState; -struct TupleTableSlot; -struct VacuumCutoffs; +typedef struct GlobalVisState GlobalVisState; +typedef struct TupleTableSlot TupleTableSlot; +typedef struct VacuumCutoffs VacuumCutoffs; +typedef struct VacuumParams VacuumParams; #define MaxLockTupleMode LockTupleExclusive @@ -92,11 +96,14 @@ typedef struct HeapScanDescData */ ParallelBlockTableScanWorkerData *rs_parallelworkerdata; + /* Current heap block's corresponding page in the visibility map */ + Buffer rs_vmbuffer; + /* these fields only used in page-at-a-time mode and for bitmap scans */ uint32 rs_cindex; /* current tuple's index in vistuples */ uint32 rs_ntuples; /* number of visible tuples on page */ OffsetNumber rs_vistuples[MaxHeapTuplesPerPage]; /* their offsets */ -} HeapScanDescData; +} HeapScanDescData; typedef struct HeapScanDescData *HeapScanDesc; typedef struct BitmapHeapScanDescData @@ -104,7 +111,7 @@ typedef struct BitmapHeapScanDescData HeapScanDescData rs_heap_base; /* Holds no data */ -} BitmapHeapScanDescData; +} BitmapHeapScanDescData; typedef struct BitmapHeapScanDescData *BitmapHeapScanDesc; /* @@ -114,8 +121,15 @@ typedef struct IndexFetchHeapData { IndexFetchTableData xs_base; /* AM independent part of the descriptor */ - Buffer xs_cbuf; /* current heap buffer in scan, if any */ - /* NB: if xs_cbuf is not InvalidBuffer, we hold a pin on that buffer */ + /* + * Current heap buffer in scan (and its block number), if any. NB: if + * xs_blk is not InvalidBlockNumber, we hold a pin in xs_cbuf. + */ + Buffer xs_cbuf; + BlockNumber xs_blk; + + /* Current heap block's corresponding page in the visibility map */ + Buffer xs_vmbuffer; } IndexFetchHeapData; /* Result codes for HeapTupleSatisfiesVacuum */ @@ -206,6 +220,18 @@ typedef struct HeapPageFreeze TransactionId FreezePageRelfrozenXid; MultiXactId FreezePageRelminMxid; + /* + * Newest XID that this page's freeze actions will remove from tuple + * visibility metadata (currently xmin and/or xvac). It is used to derive + * the snapshot conflict horizon for a WAL record that freezes tuples. On + * a standby, we must not replay that change while any snapshot could + * still treat that XID as running. + * + * It's only used if we execute freeze plans for this page, so there is no + * corresponding "no freeze" tracker. + */ + TransactionId FreezePageConflictXid; + /* * "No freeze" NewRelfrozenXid/NewRelminMxid trackers. * @@ -219,6 +245,62 @@ typedef struct HeapPageFreeze } HeapPageFreeze; + +/* 'reason' codes for heap_page_prune_and_freeze() */ +typedef enum +{ + PRUNE_ON_ACCESS, /* on-access pruning */ + PRUNE_VACUUM_SCAN, /* VACUUM 1st heap pass */ + PRUNE_VACUUM_CLEANUP, /* VACUUM 2nd heap pass */ +} PruneReason; + +/* + * Input parameters to heap_page_prune_and_freeze() + */ +typedef struct PruneFreezeParams +{ + Relation relation; /* relation containing buffer to be pruned */ + Buffer buffer; /* buffer to be pruned */ + + /* + * Callers should provide a pinned vmbuffer corresponding to the heap + * block in buffer. We will check for and repair any corruption in the VM + * and set the VM after pruning if the page is all-visible/all-frozen. + */ + Buffer vmbuffer; + + /* + * The reason pruning was performed. It is used to set the WAL record + * opcode which is used for debugging and analysis purposes. + */ + PruneReason reason; + + /* + * Contains flag bits: + * + * HEAP_PAGE_PRUNE_MARK_UNUSED_NOW indicates that dead items can be set + * LP_UNUSED during pruning. + * + * HEAP_PAGE_PRUNE_FREEZE indicates that we will also freeze tuples. + */ + int options; + + /* + * vistest is used to distinguish whether tuples are DEAD or RECENTLY_DEAD + * (see heap_prune_satisfies_vacuum). + */ + GlobalVisState *vistest; + + /* + * Contains the cutoffs used for freezing. They are required if the + * HEAP_PAGE_PRUNE_FREEZE option is set. cutoffs->OldestXmin is also used + * to determine if dead tuples are HEAPTUPLE_RECENTLY_DEAD or + * HEAPTUPLE_DEAD. Currently only vacuum passes in cutoffs. Vacuum + * calculates them once, at the beginning of vacuuming the relation. + */ + VacuumCutoffs *cutoffs; +} PruneFreezeParams; + /* * Per-page state returned by heap_page_prune_and_freeze() */ @@ -233,19 +315,12 @@ typedef struct PruneFreezeResult int recently_dead_tuples; /* - * all_visible and all_frozen indicate if the all-visible and all-frozen - * bits in the visibility map can be set for this page, after pruning. - * - * vm_conflict_horizon is the newest xmin of live tuples on the page. The - * caller can use it as the conflict horizon when setting the VM bits. It - * is only valid if we froze some tuples (nfrozen > 0), and all_frozen is - * true. - * - * These are only set if the HEAP_PRUNE_FREEZE option is set. + * Whether or not the page was newly set all-visible and all-frozen during + * phase I of vacuuming. */ - bool all_visible; - bool all_frozen; - TransactionId vm_conflict_horizon; + bool newly_all_visible; + bool newly_all_visible_frozen; + bool newly_all_frozen; /* * Whether or not the page makes rel truncation unsafe. This is set to @@ -262,13 +337,6 @@ typedef struct PruneFreezeResult OffsetNumber deadoffsets[MaxHeapTuplesPerPage]; } PruneFreezeResult; -/* 'reason' codes for heap_page_prune_and_freeze() */ -typedef enum -{ - PRUNE_ON_ACCESS, /* on-access pruning */ - PRUNE_VACUUM_SCAN, /* VACUUM 1st heap pass */ - PRUNE_VACUUM_CLEANUP, /* VACUUM 2nd heap pass */ -} PruneReason; /* ---------------- * function prototypes for heap access method @@ -279,12 +347,6 @@ typedef enum */ -/* - * HeapScanIsValid - * True iff the heap scan is valid. - */ -#define HeapScanIsValid(scan) PointerIsValid(scan) - extern TableScanDesc heap_beginscan(Relation relation, Snapshot snapshot, int nkeys, ScanKey key, ParallelTableScanDesc parallel_scan, @@ -297,7 +359,7 @@ extern void heap_rescan(TableScanDesc sscan, ScanKey key, bool set_params, extern void heap_endscan(TableScanDesc sscan); extern HeapTuple heap_getnext(TableScanDesc sscan, ScanDirection direction); extern bool heap_getnextslot(TableScanDesc sscan, - ScanDirection direction, struct TupleTableSlot *slot); + ScanDirection direction, TupleTableSlot *slot); extern void heap_set_tidrange(TableScanDesc sscan, ItemPointer mintid, ItemPointer maxtid); extern bool heap_getnextslot_tidrange(TableScanDesc sscan, @@ -305,9 +367,6 @@ extern bool heap_getnextslot_tidrange(TableScanDesc sscan, TupleTableSlot *slot); extern bool heap_fetch(Relation relation, Snapshot snapshot, HeapTuple tuple, Buffer *userbuf, bool keep_buf); -extern bool heap_hot_search_buffer(ItemPointer tid, Relation relation, - Buffer buffer, Snapshot snapshot, HeapTuple heapTuple, - bool *all_dead, bool first_call); extern void heap_get_latest_tid(TableScanDesc sscan, ItemPointer tid); @@ -316,24 +375,25 @@ extern void FreeBulkInsertState(BulkInsertState); extern void ReleaseBulkInsertStatePin(BulkInsertState bistate); extern void heap_insert(Relation relation, HeapTuple tup, CommandId cid, - int options, BulkInsertState bistate); -extern void heap_multi_insert(Relation relation, struct TupleTableSlot **slots, - int ntuples, CommandId cid, int options, + uint32 options, BulkInsertState bistate); +extern void heap_multi_insert(Relation relation, TupleTableSlot **slots, + int ntuples, CommandId cid, uint32 options, BulkInsertState bistate); -extern TM_Result heap_delete(Relation relation, ItemPointer tid, - CommandId cid, Snapshot crosscheck, bool wait, - struct TM_FailureData *tmfd, bool changingPart); -extern void heap_finish_speculative(Relation relation, ItemPointer tid); -extern void heap_abort_speculative(Relation relation, ItemPointer tid); -extern TM_Result heap_update(Relation relation, ItemPointer otid, +extern TM_Result heap_delete(Relation relation, const ItemPointerData *tid, + CommandId cid, uint32 options, Snapshot crosscheck, + bool wait, TM_FailureData *tmfd); +extern void heap_finish_speculative(Relation relation, const ItemPointerData *tid); +extern void heap_abort_speculative(Relation relation, const ItemPointerData *tid); +extern TM_Result heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup, - CommandId cid, Snapshot crosscheck, bool wait, - struct TM_FailureData *tmfd, LockTupleMode *lockmode, + CommandId cid, uint32 options, + Snapshot crosscheck, bool wait, + TM_FailureData *tmfd, LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes); extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple, CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy, bool follow_updates, - Buffer *buffer, struct TM_FailureData *tmfd); + Buffer *buffer, TM_FailureData *tmfd); extern bool heap_inplace_lock(Relation relation, HeapTuple oldtup_ptr, Buffer buffer, @@ -344,7 +404,7 @@ extern void heap_inplace_update_and_unlock(Relation relation, extern void heap_inplace_unlock(Relation relation, HeapTuple oldtup, Buffer buffer); extern bool heap_prepare_freeze_tuple(HeapTupleHeader tuple, - const struct VacuumCutoffs *cutoffs, + const VacuumCutoffs *cutoffs, HeapPageFreeze *pagefrz, HeapTupleFreeze *frz, bool *totally_frozen); @@ -356,28 +416,36 @@ extern bool heap_freeze_tuple(HeapTupleHeader tuple, TransactionId relfrozenxid, TransactionId relminmxid, TransactionId FreezeLimit, TransactionId MultiXactCutoff); extern bool heap_tuple_should_freeze(HeapTupleHeader tuple, - const struct VacuumCutoffs *cutoffs, + const VacuumCutoffs *cutoffs, TransactionId *NoFreezePageRelfrozenXid, MultiXactId *NoFreezePageRelminMxid); extern bool heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple); extern void simple_heap_insert(Relation relation, HeapTuple tup); -extern void simple_heap_delete(Relation relation, ItemPointer tid); -extern void simple_heap_update(Relation relation, ItemPointer otid, +extern void simple_heap_delete(Relation relation, const ItemPointerData *tid); +extern void simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup, TU_UpdateIndexes *update_indexes); extern TransactionId heap_index_delete_tuples(Relation rel, TM_IndexDeleteOp *delstate); +/* in heap/heapam_indexscan.c */ +extern IndexFetchTableData *heapam_index_fetch_begin(Relation rel, uint32 flags); +extern void heapam_index_fetch_reset(IndexFetchTableData *scan); +extern void heapam_index_fetch_end(IndexFetchTableData *scan); +extern bool heap_hot_search_buffer(ItemPointer tid, Relation relation, + Buffer buffer, Snapshot snapshot, HeapTuple heapTuple, + bool *all_dead, bool first_call); +extern bool heapam_index_fetch_tuple(struct IndexFetchTableData *scan, + ItemPointer tid, Snapshot snapshot, + TupleTableSlot *slot, bool *heap_continue, + bool *all_dead); + /* in heap/pruneheap.c */ -struct GlobalVisState; -extern void heap_page_prune_opt(Relation relation, Buffer buffer); -extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer, - struct GlobalVisState *vistest, - int options, - struct VacuumCutoffs *cutoffs, +extern void heap_page_prune_opt(Relation relation, Buffer buffer, + Buffer *vmbuffer, bool rel_read_only); +extern void heap_page_prune_and_freeze(PruneFreezeParams *params, PruneFreezeResult *presult, - PruneReason reason, OffsetNumber *off_loc, TransactionId *new_relfrozen_xid, MultiXactId *new_relmin_mxid); @@ -387,6 +455,7 @@ extern void heap_page_prune_execute(Buffer buffer, bool lp_truncate_only, OffsetNumber *nowunused, int nunused); extern void heap_get_root_tuples(Page page, OffsetNumber *root_offsets); extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer, + Buffer vmbuffer, uint8 vmflags, TransactionId conflict_xid, bool cleanup_lock, PruneReason reason, @@ -396,9 +465,15 @@ extern void log_heap_prune_and_freeze(Relation relation, Buffer buffer, OffsetNumber *unused, int nunused); /* in heap/vacuumlazy.c */ -struct VacuumParams; extern void heap_vacuum_rel(Relation rel, - struct VacuumParams *params, BufferAccessStrategy bstrategy); + const VacuumParams *params, BufferAccessStrategy bstrategy); +#ifdef USE_ASSERT_CHECKING +extern bool heap_page_is_all_visible(Relation rel, Buffer buf, + GlobalVisState *vistest, + bool *all_frozen, + TransactionId *newest_live_xid, + OffsetNumber *logging_offnum); +#endif /* in heap/heapam_visibility.c */ extern bool HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, @@ -413,7 +488,24 @@ extern void HeapTupleSetHintBits(HeapTupleHeader tuple, Buffer buffer, uint16 infomask, TransactionId xid); extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple); extern bool HeapTupleIsSurelyDead(HeapTuple htup, - struct GlobalVisState *vistest); + GlobalVisState *vistest); + +/* + * Some of the input/output to/from HeapTupleSatisfiesMVCCBatch() is passed + * via this struct, as otherwise the increased number of arguments to + * HeapTupleSatisfiesMVCCBatch() leads to on-stack argument passing on x86-64, + * which causes a small regression. + */ +typedef struct BatchMVCCState +{ + HeapTupleData tuples[MaxHeapTuplesPerPage]; + bool visible[MaxHeapTuplesPerPage]; +} BatchMVCCState; + +extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer, + int ntups, + BatchMVCCState *batchmvcc, + OffsetNumber *vistuples_dense); /* * To avoid leaking too much knowledge about reorderbuffer implementation diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 277df6b3cf0b3..fdca7d821c87c 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -4,7 +4,7 @@ * POSTGRES heap access XLOG definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/heapam_xlog.h @@ -60,7 +60,7 @@ #define XLOG_HEAP2_PRUNE_ON_ACCESS 0x10 #define XLOG_HEAP2_PRUNE_VACUUM_SCAN 0x20 #define XLOG_HEAP2_PRUNE_VACUUM_CLEANUP 0x30 -#define XLOG_HEAP2_VISIBLE 0x40 +/* 0x40 was XLOG_HEAP2_VISIBLE */ #define XLOG_HEAP2_MULTI_INSERT 0x50 #define XLOG_HEAP2_LOCK_UPDATED 0x60 #define XLOG_HEAP2_NEW_CID 0x70 @@ -104,6 +104,8 @@ #define XLH_DELETE_CONTAINS_OLD_KEY (1<<2) #define XLH_DELETE_IS_SUPER (1<<3) #define XLH_DELETE_IS_PARTITION_MOVE (1<<4) +/* See heap_delete() */ +#define XLH_DELETE_NO_LOGICAL (1<<5) /* convenience macro for checking whether any form of old tuple was logged */ #define XLH_DELETE_CONTAINS_OLD \ @@ -249,7 +251,7 @@ typedef struct xl_heap_update * Main data section: * * xl_heap_prune - * uint8 flags + * uint16 flags * TransactionId snapshot_conflict_horizon * * Block 0 data section: @@ -284,8 +286,7 @@ typedef struct xl_heap_update */ typedef struct xl_heap_prune { - uint8 reason; - uint8 flags; + uint16 flags; /* * If XLHP_HAS_CONFLICT_HORIZON is set, the conflict horizon XID follows, @@ -293,7 +294,7 @@ typedef struct xl_heap_prune */ } xl_heap_prune; -#define SizeOfHeapPrune (offsetof(xl_heap_prune, flags) + sizeof(uint8)) +#define SizeOfHeapPrune (offsetof(xl_heap_prune, flags) + sizeof(uint16)) /* to handle recovery conflict during logical decoding on standby */ #define XLHP_IS_CATALOG_REL (1 << 1) @@ -331,6 +332,15 @@ typedef struct xl_heap_prune #define XLHP_HAS_DEAD_ITEMS (1 << 6) #define XLHP_HAS_NOW_UNUSED_ITEMS (1 << 7) +/* + * The xl_heap_prune record's flags may also contain which VM bits to set. + * xl_heap_prune should always use the XLHP_VM_ALL_VISIBLE and + * XLHP_VM_ALL_FROZEN flags and translate them to their visibilitymapdefs.h + * equivalents, VISIBILITYMAP_ALL_VISIBLE and VISIBILITYMAP_ALL_FROZEN. + */ +#define XLHP_VM_ALL_VISIBLE (1 << 8) +#define XLHP_VM_ALL_FROZEN (1 << 9) + /* * xlhp_freeze_plan describes how to freeze a group of one or more heap tuples * (appears in xl_heap_prune's xlhp_freeze_plans sub-record) @@ -435,20 +445,6 @@ typedef struct xl_heap_inplace #define MinSizeOfHeapInplace (offsetof(xl_heap_inplace, nmsgs) + sizeof(int)) -/* - * This is what we need to know about setting a visibility map bit - * - * Backup blk 0: visibility map buffer - * Backup blk 1: heap buffer - */ -typedef struct xl_heap_visible -{ - TransactionId snapshotConflictHorizon; - uint8 flags; -} xl_heap_visible; - -#define SizeOfHeapVisible (offsetof(xl_heap_visible, flags) + sizeof(uint8)) - typedef struct xl_heap_new_cid { /* @@ -492,13 +488,8 @@ extern void heap2_desc(StringInfo buf, XLogReaderState *record); extern const char *heap2_identify(uint8 info); extern void heap_xlog_logical_rewrite(XLogReaderState *r); -extern XLogRecPtr log_heap_visible(Relation rel, Buffer heap_buffer, - Buffer vm_buffer, - TransactionId snapshotConflictHorizon, - uint8 vmflags); - /* in heapdesc.c, so it can be shared between frontend/backend code */ -extern void heap_xlog_deserialize_prune_and_freeze(char *cursor, uint8 flags, +extern void heap_xlog_deserialize_prune_and_freeze(char *cursor, uint16 flags, int *nplans, xlhp_freeze_plan **plans, OffsetNumber **frz_offsets, int *nredirected, OffsetNumber **redirected, diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h index 6385a27caf83e..631cb1836b962 100644 --- a/src/include/access/heaptoast.h +++ b/src/include/access/heaptoast.h @@ -4,7 +4,7 @@ * Heap-specific definitions for external and compressed storage * of variable size attributes. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/include/access/heaptoast.h * @@ -95,7 +95,7 @@ * ---------- */ extern HeapTuple heap_toast_insert_or_update(Relation rel, HeapTuple newtup, - HeapTuple oldtup, int options); + HeapTuple oldtup, uint32 options); /* ---------- * heap_toast_delete - @@ -133,8 +133,8 @@ extern Datum toast_flatten_tuple_to_datum(HeapTupleHeader tup, * ---------- */ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc, - Datum *values, - bool *isnull); + const Datum *values, + const bool *isnull); /* ---------- * heap_fetch_toast_slice @@ -144,6 +144,6 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc, */ extern void heap_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize, int32 sliceoffset, - int32 slicelength, struct varlena *result); + int32 slicelength, varlena *result); #endif /* HEAPTOAST_H */ diff --git a/src/include/access/hio.h b/src/include/access/hio.h index 60b802647fe56..60cfc375fd523 100644 --- a/src/include/access/hio.h +++ b/src/include/access/hio.h @@ -4,7 +4,7 @@ * POSTGRES heap access method input/output definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/hio.h @@ -54,7 +54,7 @@ typedef struct BulkInsertStateData extern void RelationPutHeapTuple(Relation relation, Buffer buffer, HeapTuple tuple, bool token); extern Buffer RelationGetBufferForTuple(Relation relation, Size len, - Buffer otherBuffer, int options, + Buffer otherBuffer, uint32 options, BulkInsertStateData *bistate, Buffer *vmbuffer, Buffer *vmbuffer_other, int num_pages); diff --git a/src/include/access/htup.h b/src/include/access/htup.h index f0e3aa87dc306..cfc4e1cbc58c0 100644 --- a/src/include/access/htup.h +++ b/src/include/access/htup.h @@ -4,7 +4,7 @@ * POSTGRES heap tuple definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/htup.h @@ -75,7 +75,7 @@ typedef HeapTupleData *HeapTuple; /* * Accessor macros to be used with HeapTuple pointers. */ -#define HeapTupleIsValid(tuple) PointerIsValid(tuple) +#define HeapTupleIsValid(tuple) ((tuple) != NULL) /* HeapTupleHeader functions implemented in utils/time/combocid.c */ extern CommandId HeapTupleHeaderGetCmin(const HeapTupleHeaderData *tup); diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h index aa957cf3b0165..77a6c48fd711a 100644 --- a/src/include/access/htup_details.h +++ b/src/include/access/htup_details.h @@ -4,7 +4,7 @@ * POSTGRES heap tuple header definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/htup_details.h @@ -175,7 +175,7 @@ struct HeapTupleHeaderData /* ^ - 23 bytes - ^ */ #define FIELDNO_HEAPTUPLEHEADERDATA_BITS 5 - bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */ + uint8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */ /* MORE DATA FOLLOWS AT END OF STRUCT */ }; @@ -264,19 +264,19 @@ HEAP_LOCKED_UPGRADED(uint16 infomask) * Use these to test whether a particular lock is applied to a tuple */ static inline bool -HEAP_XMAX_IS_SHR_LOCKED(int16 infomask) +HEAP_XMAX_IS_SHR_LOCKED(uint16 infomask) { return (infomask & HEAP_LOCK_MASK) == HEAP_XMAX_SHR_LOCK; } static inline bool -HEAP_XMAX_IS_EXCL_LOCKED(int16 infomask) +HEAP_XMAX_IS_EXCL_LOCKED(uint16 infomask) { return (infomask & HEAP_LOCK_MASK) == HEAP_XMAX_EXCL_LOCK; } static inline bool -HEAP_XMAX_IS_KEYSHR_LOCKED(int16 infomask) +HEAP_XMAX_IS_KEYSHR_LOCKED(uint16 infomask) { return (infomask & HEAP_LOCK_MASK) == HEAP_XMAX_KEYSHR_LOCK; } @@ -357,20 +357,6 @@ HeapTupleHeaderXminFrozen(const HeapTupleHeaderData *tup) return (tup->t_infomask & HEAP_XMIN_FROZEN) == HEAP_XMIN_FROZEN; } -static inline void -HeapTupleHeaderSetXminCommitted(HeapTupleHeaderData *tup) -{ - Assert(!HeapTupleHeaderXminInvalid(tup)); - tup->t_infomask |= HEAP_XMIN_COMMITTED; -} - -static inline void -HeapTupleHeaderSetXminInvalid(HeapTupleHeaderData *tup) -{ - Assert(!HeapTupleHeaderXminCommitted(tup)); - tup->t_infomask |= HEAP_XMIN_INVALID; -} - static inline void HeapTupleHeaderSetXminFrozen(HeapTupleHeaderData *tup) { @@ -634,7 +620,7 @@ BITMAPLEN(int NATTS) * MaxAttrSize is a somewhat arbitrary upper limit on the declared size of * data fields of char(n) and similar types. It need not have anything * directly to do with the *actual* upper limit of varlena values, which - * is currently 1Gb (see TOAST structures in postgres.h). I've set it + * is currently 1Gb (see TOAST structures in varatt.h). I've set it * at 10Mb which seems like a reasonable number --- tgl 8/6/00. */ #define MaxAttrSize (10 * 1024 * 1024) @@ -694,7 +680,7 @@ struct MinimalTupleData /* ^ - 23 bytes - ^ */ - bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */ + uint8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* bitmap of NULLs */ /* MORE DATA FOLLOWS AT END OF STRUCT */ }; @@ -811,7 +797,7 @@ extern Size heap_compute_data_size(TupleDesc tupleDesc, extern void heap_fill_tuple(TupleDesc tupleDesc, const Datum *values, const bool *isnull, char *data, Size data_size, - uint16 *infomask, bits8 *bit); + uint16 *infomask, uint8 *bit); extern bool heap_attisnull(HeapTuple tup, int attnum, TupleDesc tupleDesc); extern Datum nocachegetattr(HeapTuple tup, int attnum, TupleDesc tupleDesc); @@ -884,7 +870,7 @@ fastgetattr(HeapTuple tup, int attnum, TupleDesc tupleDesc, bool *isnull) if (att_isnull(attnum - 1, tup->t_data->t_bits)) { *isnull = true; - return (Datum) NULL; + return (Datum) 0; } else return nocachegetattr(tup, attnum, tupleDesc); diff --git a/src/include/access/itup.h b/src/include/access/itup.h index 7066c2a2868b3..5758bbdf6eb4c 100644 --- a/src/include/access/itup.h +++ b/src/include/access/itup.h @@ -4,7 +4,7 @@ * POSTGRES index tuple definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/itup.h @@ -54,7 +54,7 @@ typedef IndexTupleData *IndexTuple; typedef struct IndexAttributeBitMapData { - bits8 bits[(INDEX_MAX_KEYS + 8 - 1) / 8]; + uint8 bits[(INDEX_MAX_KEYS + 8 - 1) / 8]; } IndexAttributeBitMapData; typedef IndexAttributeBitMapData * IndexAttributeBitMap; @@ -99,7 +99,7 @@ extern void index_deform_tuple(IndexTuple tup, TupleDesc tupleDescriptor, Datum *values, bool *isnull); extern void index_deform_tuple_internal(TupleDesc tupleDescriptor, Datum *values, bool *isnull, - char *tp, bits8 *bp, int hasnulls); + char *tp, uint8 *bp, int hasnulls); extern IndexTuple CopyIndexTuple(IndexTuple source); extern IndexTuple index_truncate_tuple(TupleDesc sourceDescriptor, IndexTuple source, int leavenatts); @@ -131,7 +131,7 @@ IndexInfoFindDataOffset(unsigned short t_info) static inline Datum index_getattr(IndexTuple tup, int attnum, TupleDesc tupleDesc, bool *isnull) { - Assert(PointerIsValid(isnull)); + Assert(isnull); Assert(attnum > 0); *isnull = false; @@ -151,10 +151,10 @@ index_getattr(IndexTuple tup, int attnum, TupleDesc tupleDesc, bool *isnull) } else { - if (att_isnull(attnum - 1, (bits8 *) tup + sizeof(IndexTupleData))) + if (att_isnull(attnum - 1, (uint8 *) tup + sizeof(IndexTupleData))) { *isnull = true; - return (Datum) NULL; + return (Datum) 0; } else return nocache_index_getattr(tup, attnum, tupleDesc); diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h index 4e6b0eec2ff4e..6be5299ab6877 100644 --- a/src/include/access/multixact.h +++ b/src/include/access/multixact.h @@ -3,7 +3,7 @@ * * PostgreSQL multi-transaction-log manager * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/multixact.h @@ -11,6 +11,7 @@ #ifndef MULTIXACT_H #define MULTIXACT_H +#include "access/transam.h" #include "access/xlogreader.h" #include "lib/stringinfo.h" #include "storage/sync.h" @@ -27,8 +28,6 @@ #define MultiXactIdIsValid(multi) ((multi) != InvalidMultiXactId) -#define MaxMultiXactOffset ((MultiXactOffset) 0xFFFFFFFF) - /* * Possible multixact lock modes ("status"). The first four modes are for * tuple locks (FOR KEY SHARE, FOR SHARE, FOR NO KEY UPDATE, FOR UPDATE); the @@ -84,13 +83,11 @@ typedef struct xl_multixact_truncate { Oid oldestMultiDB; - /* to-be-truncated range of multixact offsets */ - MultiXactId startTruncOff; /* just for completeness' sake */ - MultiXactId endTruncOff; + /* truncate multixact offsets older than this */ + MultiXactId oldestMulti; - /* to-be-truncated range of multixact members */ - MultiXactOffset startTruncMemb; - MultiXactOffset endTruncMemb; + /* truncate multixact members older than this */ + MultiXactOffset oldestOffset; } xl_multixact_truncate; #define SizeOfMultiXactTruncate (sizeof(xl_multixact_truncate)) @@ -110,6 +107,9 @@ extern bool MultiXactIdIsRunning(MultiXactId multi, bool isLockOnly); extern void MultiXactIdSetOldestMember(void); extern int GetMultiXactIdMembers(MultiXactId multi, MultiXactMember **members, bool from_pgupgrade, bool isLockOnly); +extern void GetMultiXactInfo(uint32 *multixacts, MultiXactOffset *nextOffset, + MultiXactId *oldestMultiXactId, + MultiXactOffset *oldestOffset); extern bool MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2); extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2); @@ -119,16 +119,13 @@ extern int multixactmemberssyncfiletag(const FileTag *ftag, char *path); extern void AtEOXact_MultiXact(void); extern void AtPrepare_MultiXact(void); -extern void PostPrepare_MultiXact(TransactionId xid); +extern void PostPrepare_MultiXact(FullTransactionId fxid); -extern Size MultiXactShmemSize(void); -extern void MultiXactShmemInit(void); extern void BootStrapMultiXact(void); extern void StartupMultiXact(void); extern void TrimMultiXact(void); extern void SetMultiXactIdLimit(MultiXactId oldest_datminmxid, - Oid oldest_datoid, - bool is_startup); + Oid oldest_datoid); extern void MultiXactGetCheckptMulti(bool is_shutdown, MultiXactId *nextMulti, MultiXactOffset *nextMultiOffset, @@ -145,11 +142,11 @@ extern void MultiXactAdvanceNextMXact(MultiXactId minMulti, extern void MultiXactAdvanceOldest(MultiXactId oldestMulti, Oid oldestMultiDB); extern int MultiXactMemberFreezeThreshold(void); -extern void multixact_twophase_recover(TransactionId xid, uint16 info, +extern void multixact_twophase_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); -extern void multixact_twophase_postcommit(TransactionId xid, uint16 info, +extern void multixact_twophase_postcommit(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); -extern void multixact_twophase_postabort(TransactionId xid, uint16 info, +extern void multixact_twophase_postabort(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); extern void multixact_redo(XLogReaderState *record); @@ -157,5 +154,6 @@ extern void multixact_desc(StringInfo buf, XLogReaderState *record); extern const char *multixact_identify(uint8 info); extern char *mxid_to_string(MultiXactId multi, int nmembers, MultiXactMember *members); +extern char *mxstatus_to_string(MultiXactStatus status); #endif /* MULTIXACT_H */ diff --git a/src/include/access/multixact_internal.h b/src/include/access/multixact_internal.h new file mode 100644 index 0000000000000..82349ea0d32b9 --- /dev/null +++ b/src/include/access/multixact_internal.h @@ -0,0 +1,134 @@ +/* + * multixact_internal.h + * + * PostgreSQL multi-transaction-log manager internal declarations + * + * These functions and definitions are for dealing with pg_multixact SLRU + * pages. They are internal to multixact.c, but they are exported here to + * allow pg_upgrade to write pg_multixact files directly. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/access/multixact_internal.h + */ +#ifndef MULTIXACT_INTERNAL_H + +/* + * Note: This is not only to prevent including this file twice. + * MULTIXACT_INTERNAL_H is checked explicitly in multixact_read_v18.c. + */ +#define MULTIXACT_INTERNAL_H + +#include "access/multixact.h" + + +/* + * Defines for MultiXactOffset page sizes. A page is the same BLCKSZ as is + * used everywhere else in Postgres. + */ + +/* We need 8 bytes per offset */ +#define MULTIXACT_OFFSETS_PER_PAGE (BLCKSZ / sizeof(MultiXactOffset)) + +static inline int64 +MultiXactIdToOffsetPage(MultiXactId multi) +{ + return multi / MULTIXACT_OFFSETS_PER_PAGE; +} + +static inline int +MultiXactIdToOffsetEntry(MultiXactId multi) +{ + return multi % MULTIXACT_OFFSETS_PER_PAGE; +} + +static inline int64 +MultiXactIdToOffsetSegment(MultiXactId multi) +{ + return MultiXactIdToOffsetPage(multi) / SLRU_PAGES_PER_SEGMENT; +} + +/* + * The situation for members is a bit more complex: we store one byte of + * additional flag bits for each TransactionId. To do this without getting + * into alignment issues, we store four bytes of flags, and then the + * corresponding 4 Xids. Each such 5-word (20-byte) set we call a "group", and + * are stored as a whole in pages. Thus, with 8kB BLCKSZ, we keep 409 groups + * per page. This wastes 12 bytes per page, but that's OK -- simplicity (and + * performance) trumps space efficiency here. + * + * Note that the "offset" macros work with byte offset, not array indexes, so + * arithmetic must be done using "char *" pointers. + */ +/* We need eight bits per xact, so one xact fits in a byte */ +#define MXACT_MEMBER_BITS_PER_XACT 8 +#define MXACT_MEMBER_FLAGS_PER_BYTE 1 +#define MXACT_MEMBER_XACT_BITMASK ((1 << MXACT_MEMBER_BITS_PER_XACT) - 1) + +/* how many full bytes of flags are there in a group? */ +#define MULTIXACT_FLAGBYTES_PER_GROUP 4 +#define MULTIXACT_MEMBERS_PER_MEMBERGROUP \ + (MULTIXACT_FLAGBYTES_PER_GROUP * MXACT_MEMBER_FLAGS_PER_BYTE) +/* size in bytes of a complete group */ +#define MULTIXACT_MEMBERGROUP_SIZE \ + (sizeof(TransactionId) * MULTIXACT_MEMBERS_PER_MEMBERGROUP + MULTIXACT_FLAGBYTES_PER_GROUP) +#define MULTIXACT_MEMBERGROUPS_PER_PAGE (BLCKSZ / MULTIXACT_MEMBERGROUP_SIZE) +#define MULTIXACT_MEMBERS_PER_PAGE \ + (MULTIXACT_MEMBERGROUPS_PER_PAGE * MULTIXACT_MEMBERS_PER_MEMBERGROUP) + +/* page in which a member is to be found */ +static inline int64 +MXOffsetToMemberPage(MultiXactOffset offset) +{ + return offset / MULTIXACT_MEMBERS_PER_PAGE; +} + +static inline int64 +MXOffsetToMemberSegment(MultiXactOffset offset) +{ + return MXOffsetToMemberPage(offset) / SLRU_PAGES_PER_SEGMENT; +} + +/* Location (byte offset within page) of flag word for a given member */ +static inline int +MXOffsetToFlagsOffset(MultiXactOffset offset) +{ + MultiXactOffset group = offset / MULTIXACT_MEMBERS_PER_MEMBERGROUP; + int grouponpg = group % MULTIXACT_MEMBERGROUPS_PER_PAGE; + int byteoff = grouponpg * MULTIXACT_MEMBERGROUP_SIZE; + + return byteoff; +} + +static inline int +MXOffsetToFlagsBitShift(MultiXactOffset offset) +{ + int member_in_group = offset % MULTIXACT_MEMBERS_PER_MEMBERGROUP; + int bshift = member_in_group * MXACT_MEMBER_BITS_PER_XACT; + + return bshift; +} + +/* Location (byte offset within page) of TransactionId of given member */ +static inline int +MXOffsetToMemberOffset(MultiXactOffset offset) +{ + int member_in_group = offset % MULTIXACT_MEMBERS_PER_MEMBERGROUP; + + return MXOffsetToFlagsOffset(offset) + + MULTIXACT_FLAGBYTES_PER_GROUP + + member_in_group * sizeof(TransactionId); +} + +/* Storage space consumed by a range of offsets, in bytes */ +static inline uint64 +MultiXactOffsetStorageSize(MultiXactOffset new_offset, + MultiXactOffset old_offset) +{ + Assert(new_offset >= old_offset); + return (uint64) ((new_offset - old_offset) / MULTIXACT_MEMBERS_PER_MEMBERGROUP) * + MULTIXACT_MEMBERGROUP_SIZE; +} + +#endif /* MULTIXACT_INTERNAL_H */ diff --git a/src/include/access/nbtree.h b/src/include/access/nbtree.h index ebca02588d3e2..3097e9bb1af9b 100644 --- a/src/include/access/nbtree.h +++ b/src/include/access/nbtree.h @@ -4,7 +4,7 @@ * header file for postgres btree access method implementation. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/nbtree.h @@ -17,12 +17,12 @@ #include "access/amapi.h" #include "access/itup.h" #include "access/sdir.h" -#include "access/tableam.h" -#include "access/xlogreader.h" #include "catalog/pg_am_d.h" +#include "catalog/pg_class.h" #include "catalog/pg_index.h" #include "lib/stringinfo.h" #include "storage/bufmgr.h" +#include "storage/dsm.h" #include "storage/shm_toc.h" #include "utils/skipsupport.h" @@ -939,7 +939,7 @@ typedef BTVacuumPostingData *BTVacuumPosting; * processing. This approach minimizes lock/unlock traffic. We must always * drop the lock to make it okay for caller to process the returned items. * Whether or not we can also release the pin during this window will vary. - * We drop the pin eagerly (when safe) to avoid blocking progress by VACUUM + * We drop the pin (when so->dropPin) to avoid blocking progress by VACUUM * (see nbtree/README section about making concurrent TID recycling safe). * We'll always release both the lock and the pin on the current page before * moving on to its sibling page. @@ -967,7 +967,7 @@ typedef struct BTScanPosData BlockNumber currPage; /* page referenced by items array */ BlockNumber prevPage; /* currPage's left link */ BlockNumber nextPage; /* currPage's right link */ - XLogRecPtr lsn; /* currPage's LSN */ + XLogRecPtr lsn; /* currPage's LSN (when so->dropPin) */ /* scan direction for the saved position's call to _bt_readpage */ ScanDirection dir; @@ -1070,6 +1070,7 @@ typedef struct BTScanOpaqueData /* info about killed items if any (killedItems is NULL if never used) */ int *killedItems; /* currPos.items indexes of killed items */ int numKilled; /* number of currently stored items */ + bool dropPin; /* drop leaf pin before btgettuple returns? */ /* * If we are doing an index-only scan, these are the tuple storage @@ -1095,37 +1096,6 @@ typedef struct BTScanOpaqueData typedef BTScanOpaqueData *BTScanOpaque; -/* - * _bt_readpage state used across _bt_checkkeys calls for a page - */ -typedef struct BTReadPageState -{ - /* Input parameters, set by _bt_readpage for _bt_checkkeys */ - OffsetNumber minoff; /* Lowest non-pivot tuple's offset */ - OffsetNumber maxoff; /* Highest non-pivot tuple's offset */ - IndexTuple finaltup; /* Needed by scans with array keys */ - Page page; /* Page being read */ - bool firstpage; /* page is first for primitive scan? */ - bool forcenonrequired; /* treat all keys as nonrequired? */ - int startikey; /* start comparisons from this scan key */ - - /* Per-tuple input parameters, set by _bt_readpage for _bt_checkkeys */ - OffsetNumber offnum; /* current tuple's page offset number */ - - /* Output parameters, set by _bt_checkkeys for _bt_readpage */ - OffsetNumber skip; /* Array keys "look ahead" skip offnum */ - bool continuescan; /* Terminate ongoing (primitive) index scan? */ - - /* - * Private _bt_checkkeys state used to manage "look ahead" optimization - * and primscan scheduling (only used during scans with array keys) - */ - int16 rechecks; - int16 targetdistance; - int16 nskipadvances; - -} BTReadPageState; - /* * We use some private sk_flags bits in preprocessed scan keys. We're allowed * to use bits 16-31 (see skey.h). The uppermost bits are copied from the @@ -1233,7 +1203,7 @@ extern void _bt_dedup_start_pending(BTDedupState state, IndexTuple base, OffsetNumber baseoff); extern bool _bt_dedup_save_htid(BTDedupState state, IndexTuple itup); extern Size _bt_dedup_finish_pending(Page newpage, BTDedupState state); -extern IndexTuple _bt_form_posting(IndexTuple base, ItemPointer htids, +extern IndexTuple _bt_form_posting(IndexTuple base, const ItemPointerData *htids, int nhtids); extern void _bt_update_posting(BTVacuumPosting vacposting); extern IndexTuple _bt_swap_posting(IndexTuple newitem, IndexTuple oposting, @@ -1284,9 +1254,10 @@ extern void _bt_pageinit(Page page, Size size); extern void _bt_delitems_vacuum(Relation rel, Buffer buf, OffsetNumber *deletable, int ndeletable, BTVacuumPosting *updatable, int nupdatable); +struct TM_IndexDeleteOp; /* avoid including tableam.h here */ extern void _bt_delitems_delete_check(Relation rel, Buffer buf, Relation heapRel, - TM_IndexDeleteOp *delstate); + struct TM_IndexDeleteOp *delstate); extern void _bt_pagedel(Relation rel, Buffer leafbuf, BTVacState *vstate); extern void _bt_pendingfsm_init(Relation rel, BTVacState *vstate, bool cleanuponly); @@ -1297,11 +1268,23 @@ extern void _bt_pendingfsm_finalize(Relation rel, BTVacState *vstate); */ extern void _bt_preprocess_keys(IndexScanDesc scan); +/* + * prototypes for functions in nbtreadpage.c + */ +extern bool _bt_readpage(IndexScanDesc scan, ScanDirection dir, + OffsetNumber offnum, bool firstpage); +extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir); +extern int _bt_binsrch_array_skey(FmgrInfo *orderproc, + bool cur_elem_trig, ScanDirection dir, + Datum tupdatum, bool tupnull, + BTArrayKeyInfo *array, ScanKey cur, + int32 *set_elem_result); + /* * prototypes for functions in nbtsearch.c */ extern BTStack _bt_search(Relation rel, Relation heaprel, BTScanInsert key, - Buffer *bufP, int access); + Buffer *bufP, int access, bool returnstack); extern OffsetNumber _bt_binsrch_insert(Relation rel, BTInsertState insertstate); extern int32 _bt_compare(Relation rel, BTScanInsert key, Page page, OffsetNumber offnum); extern bool _bt_first(IndexScanDesc scan, ScanDirection dir); @@ -1312,26 +1295,11 @@ extern Buffer _bt_get_endpoint(Relation rel, uint32 level, bool rightmost); * prototypes for functions in nbtutils.c */ extern BTScanInsert _bt_mkscankey(Relation rel, IndexTuple itup); -extern void _bt_freestack(BTStack stack); -extern bool _bt_start_prim_scan(IndexScanDesc scan, ScanDirection dir); -extern int _bt_binsrch_array_skey(FmgrInfo *orderproc, - bool cur_elem_trig, ScanDirection dir, - Datum tupdatum, bool tupnull, - BTArrayKeyInfo *array, ScanKey cur, - int32 *set_elem_result); -extern void _bt_start_array_keys(IndexScanDesc scan, ScanDirection dir); -extern bool _bt_checkkeys(IndexScanDesc scan, BTReadPageState *pstate, bool arrayKeys, - IndexTuple tuple, int tupnatts); -extern bool _bt_scanbehind_checkkeys(IndexScanDesc scan, ScanDirection dir, - IndexTuple finaltup); -extern void _bt_set_startikey(IndexScanDesc scan, BTReadPageState *pstate); extern void _bt_killitems(IndexScanDesc scan); extern BTCycleId _bt_vacuum_cycleid(Relation rel); extern BTCycleId _bt_start_vacuum(Relation rel); extern void _bt_end_vacuum(Relation rel); extern void _bt_end_vacuum_callback(int code, Datum arg); -extern Size BTreeShmemSize(void); -extern void BTreeShmemInit(void); extern bytea *btoptions(Datum reloptions, bool validate); extern bool btproperty(Oid index_oid, int attno, IndexAMProperty prop, const char *propname, diff --git a/src/include/access/nbtxlog.h b/src/include/access/nbtxlog.h index fbbe115c77126..3a78ec27fe8ce 100644 --- a/src/include/access/nbtxlog.h +++ b/src/include/access/nbtxlog.h @@ -3,7 +3,7 @@ * nbtxlog.h * header file for postgres btree xlog routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/nbtxlog.h diff --git a/src/include/access/parallel.h b/src/include/access/parallel.h index f37be6d56909b..60f857675e05a 100644 --- a/src/include/access/parallel.h +++ b/src/include/access/parallel.h @@ -3,7 +3,7 @@ * parallel.h * Infrastructure for launching parallel workers * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/parallel.h @@ -14,6 +14,8 @@ #ifndef PARALLEL_H #define PARALLEL_H +#include + #include "access/xlogdefs.h" #include "lib/ilist.h" #include "postmaster/bgworker.h" diff --git a/src/include/access/printsimple.h b/src/include/access/printsimple.h index 760d1482aa6fa..1ef1c387477db 100644 --- a/src/include/access/printsimple.h +++ b/src/include/access/printsimple.h @@ -3,7 +3,7 @@ * printsimple.h * print simple tuples without catalog access * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/printsimple.h diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h index b9bca20d718fc..2af9b8cf8df31 100644 --- a/src/include/access/printtup.h +++ b/src/include/access/printtup.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/printtup.h diff --git a/src/include/access/relation.h b/src/include/access/relation.h index 79b0198283e61..1c51542284f81 100644 --- a/src/include/access/relation.h +++ b/src/include/access/relation.h @@ -4,7 +4,7 @@ * Generic relation related routines. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/relation.h diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h index dfbb4c854606c..e8cb7f7a6277e 100644 --- a/src/include/access/reloptions.h +++ b/src/include/access/reloptions.h @@ -9,7 +9,7 @@ * into a lot of low-level code. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/reloptions.h @@ -23,12 +23,12 @@ #include "access/htup.h" #include "access/tupdesc.h" #include "nodes/pg_list.h" -#include "storage/lock.h" /* types supported by reloptions */ typedef enum relopt_type { RELOPT_TYPE_BOOL, + RELOPT_TYPE_TERNARY, /* on, off, unset */ RELOPT_TYPE_INT, RELOPT_TYPE_REAL, RELOPT_TYPE_ENUM, @@ -66,7 +66,7 @@ typedef struct relopt_gen const char *name; /* must be first (used as list termination * marker) */ const char *desc; - bits32 kinds; + uint32 kinds; LOCKMODE lockmode; int namelen; relopt_type type; @@ -80,11 +80,12 @@ typedef struct relopt_value union { bool bool_val; + pg_ternary ternary_val; int int_val; double real_val; int enum_val; char *string_val; /* allocated separately */ - } values; + }; } relopt_value; /* reloptions records for specific variable types */ @@ -94,6 +95,12 @@ typedef struct relopt_bool bool default_val; } relopt_bool; +typedef struct relopt_ternary +{ + relopt_gen gen; + /* ternaries have no default_val: otherwise they'd just be bools */ +} relopt_ternary; + typedef struct relopt_int { relopt_gen gen; @@ -152,19 +159,6 @@ typedef struct const char *optname; /* option's name */ relopt_type opttype; /* option's datatype */ int offset; /* offset of field in result struct */ - - /* - * isset_offset is an optional offset of a field in the result struct that - * stores whether the option is explicitly set for the relation or if it - * just picked up the default value. In most cases, this can be - * accomplished by giving the reloption a special out-of-range default - * value (e.g., some integer reloptions use -2), but this isn't always - * possible. For example, a Boolean reloption cannot be given an - * out-of-range default, so we need another way to discover the source of - * its value. This offset is only used if given a value greater than - * zero. - */ - int isset_offset; } relopt_parse_elt; /* Local reloption definition */ @@ -193,18 +187,20 @@ typedef struct local_relopts (char *)(optstruct) + (optstruct)->member) extern relopt_kind add_reloption_kind(void); -extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc, +extern void add_bool_reloption(uint32 kinds, const char *name, const char *desc, bool default_val, LOCKMODE lockmode); -extern void add_int_reloption(bits32 kinds, const char *name, const char *desc, +extern void add_ternary_reloption(uint32 kinds, const char *name, + const char *desc, LOCKMODE lockmode); +extern void add_int_reloption(uint32 kinds, const char *name, const char *desc, int default_val, int min_val, int max_val, LOCKMODE lockmode); -extern void add_real_reloption(bits32 kinds, const char *name, const char *desc, +extern void add_real_reloption(uint32 kinds, const char *name, const char *desc, double default_val, double min_val, double max_val, LOCKMODE lockmode); -extern void add_enum_reloption(bits32 kinds, const char *name, const char *desc, +extern void add_enum_reloption(uint32 kinds, const char *name, const char *desc, relopt_enum_elt_def *members, int default_val, const char *detailmsg, LOCKMODE lockmode); -extern void add_string_reloption(bits32 kinds, const char *name, const char *desc, +extern void add_string_reloption(uint32 kinds, const char *name, const char *desc, const char *default_val, validate_string_relopt validator, LOCKMODE lockmode); @@ -214,6 +210,9 @@ extern void register_reloptions_validator(local_relopts *relopts, extern void add_local_bool_reloption(local_relopts *relopts, const char *name, const char *desc, bool default_val, int offset); +extern void add_local_ternary_reloption(local_relopts *relopts, + const char *name, const char *desc, + int offset); extern void add_local_int_reloption(local_relopts *relopts, const char *name, const char *desc, int default_val, int min_val, int max_val, int offset); @@ -233,7 +232,7 @@ extern void add_local_string_reloption(local_relopts *relopts, const char *name, fill_string_relopt filler, int offset); extern Datum transformRelOptions(Datum oldOptions, List *defList, - const char *namspace, const char *const validnsps[], + const char *nameSpace, const char *const validnsps[], bool acceptOidsOff, bool isReset); extern List *untransformRelOptions(Datum options); extern bytea *extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h index b5e0fb386c0aa..2ea06a67a6346 100644 --- a/src/include/access/relscan.h +++ b/src/include/access/relscan.h @@ -4,7 +4,7 @@ * POSTGRES relation scan descriptor definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/relscan.h @@ -18,13 +18,13 @@ #include "access/itup.h" #include "nodes/tidbitmap.h" #include "port/atomics.h" -#include "storage/buf.h" #include "storage/relfilelocator.h" #include "storage/spin.h" #include "utils/relcache.h" struct ParallelTableScanDescData; +struct TableScanInstrumentation; /* * Generic descriptor for table scans. This is the base-class for table scans, @@ -65,6 +65,11 @@ typedef struct TableScanDescData struct ParallelTableScanDescData *rs_parallel; /* parallel scan * information */ + + /* + * Instrumentation counters maintained by all table AMs. + */ + struct TableScanInstrumentation *rs_instrument; } TableScanDescData; typedef struct TableScanDescData *TableScanDesc; @@ -96,6 +101,8 @@ typedef struct ParallelBlockTableScanDescData BlockNumber phs_nblocks; /* # blocks in relation at start of scan */ slock_t phs_mutex; /* mutual exclusion for setting startblock */ BlockNumber phs_startblock; /* starting block number */ + BlockNumber phs_numblock; /* # blocks to scan, or InvalidBlockNumber if + * no limit */ pg_atomic_uint64 phs_nallocated; /* number of blocks allocated to * workers so far. */ } ParallelBlockTableScanDescData; @@ -121,6 +128,12 @@ typedef struct ParallelBlockTableScanWorkerData *ParallelBlockTableScanWorker; typedef struct IndexFetchTableData { Relation rel; + + /* + * Bitmask of ScanOptions affecting the relation. No SO_INTERNAL_FLAGS are + * permitted. + */ + uint32 flags; } IndexFetchTableData; struct IndexScanInstrumentation; @@ -189,14 +202,13 @@ typedef struct IndexScanDescData /* parallel index scan information, in shared memory */ struct ParallelIndexScanDescData *parallel_scan; -} IndexScanDescData; +} IndexScanDescData; /* Generic structure for parallel scans */ typedef struct ParallelIndexScanDescData { RelFileLocator ps_locator; /* physical table relation to scan */ RelFileLocator ps_indexlocator; /* physical index relation to scan */ - Size ps_offset_ins; /* Offset to SharedIndexScanInstrumentation */ Size ps_offset_am; /* Offset to am-specific structure */ char ps_snapshot_data[FLEXIBLE_ARRAY_MEMBER]; } ParallelIndexScanDescData; @@ -212,6 +224,6 @@ typedef struct SysScanDescData struct IndexScanDescData *iscan; /* only valid in index-scan case */ struct SnapshotData *snapshot; /* snapshot to unregister at end of scan */ struct TupleTableSlot *slot; -} SysScanDescData; +} SysScanDescData; #endif /* RELSCAN_H */ diff --git a/src/include/access/rewriteheap.h b/src/include/access/rewriteheap.h index 99c3f362adc62..6ccf7b45c04e7 100644 --- a/src/include/access/rewriteheap.h +++ b/src/include/access/rewriteheap.h @@ -3,7 +3,7 @@ * rewriteheap.h * Declarations for heap rewrite support functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * src/include/access/rewriteheap.h diff --git a/src/include/access/rmgrdesc_utils.h b/src/include/access/rmgrdesc_utils.h index cda2ee55311ee..da846a335ef1b 100644 --- a/src/include/access/rmgrdesc_utils.h +++ b/src/include/access/rmgrdesc_utils.h @@ -3,7 +3,7 @@ * rmgrdesc_utils.h * Support functions for rmgrdesc routines * - * Copyright (c) 2023-2025, PostgreSQL Global Development Group + * Copyright (c) 2023-2026, PostgreSQL Global Development Group * * src/include/access/rmgrdesc_utils.h * diff --git a/src/include/access/rmgrlist.h b/src/include/access/rmgrlist.h index 8e7fc9db87786..ae32ef16d67b6 100644 --- a/src/include/access/rmgrlist.h +++ b/src/include/access/rmgrlist.h @@ -6,7 +6,7 @@ * by the PG_RMGR macro, which is not defined in this file; it can be * defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/rmgrlist.h @@ -47,3 +47,4 @@ PG_RMGR(RM_COMMIT_TS_ID, "CommitTs", commit_ts_redo, commit_ts_desc, commit_ts_i PG_RMGR(RM_REPLORIGIN_ID, "ReplicationOrigin", replorigin_redo, replorigin_desc, replorigin_identify, NULL, NULL, NULL, NULL) PG_RMGR(RM_GENERIC_ID, "Generic", generic_redo, generic_desc, generic_identify, NULL, NULL, generic_mask, NULL) PG_RMGR(RM_LOGICALMSG_ID, "LogicalMessage", logicalmsg_redo, logicalmsg_desc, logicalmsg_identify, NULL, NULL, NULL, logicalmsg_decode) +PG_RMGR(RM_XLOG2_ID, "XLOG2", xlog2_redo, xlog2_desc, xlog2_identify, NULL, NULL, NULL, xlog2_decode) diff --git a/src/include/access/sdir.h b/src/include/access/sdir.h index 6544d24ed4e7e..9a774fa24a00f 100644 --- a/src/include/access/sdir.h +++ b/src/include/access/sdir.h @@ -4,7 +4,7 @@ * POSTGRES scan direction definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/sdir.h diff --git a/src/include/access/sequence.h b/src/include/access/sequence.h index 27dde29542a83..cbe93775b1bcc 100644 --- a/src/include/access/sequence.h +++ b/src/include/access/sequence.h @@ -4,7 +4,7 @@ * Generic routines for sequence-related code. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/sequence.h diff --git a/src/include/access/session.h b/src/include/access/session.h index b4946278aec2d..96999fe9085bb 100644 --- a/src/include/access/session.h +++ b/src/include/access/session.h @@ -3,7 +3,7 @@ * session.h * Encapsulation of user session. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/access/session.h * diff --git a/src/include/access/skey.h b/src/include/access/skey.h index e650c2e7baf66..64e7a802f3e12 100644 --- a/src/include/access/skey.h +++ b/src/include/access/skey.h @@ -4,7 +4,7 @@ * POSTGRES scan key definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/skey.h diff --git a/src/include/access/slru.h b/src/include/access/slru.h index e142800aab216..b4adb1789c76f 100644 --- a/src/include/access/slru.h +++ b/src/include/access/slru.h @@ -3,7 +3,7 @@ * slru.h * Simple LRU buffering for transaction status logfiles * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/slru.h @@ -13,8 +13,10 @@ #ifndef SLRU_H #define SLRU_H +#include "access/transam.h" #include "access/xlogdefs.h" #include "storage/lwlock.h" +#include "storage/shmem.h" #include "storage/sync.h" /* @@ -23,21 +25,6 @@ */ #define SLRU_MAX_ALLOWED_BUFFERS ((1024 * 1024 * 1024) / BLCKSZ) -/* - * Define SLRU segment size. A page is the same BLCKSZ as is used everywhere - * else in Postgres. The segment size can be chosen somewhat arbitrarily; - * we make it 32 pages by default, or 256Kb, i.e. 1M transactions for CLOG - * or 64K transactions for SUBTRANS. - * - * Note: because TransactionIds are 32 bits and wrap around at 0xFFFFFFFF, - * page numbering also wraps around at 0xFFFFFFFF/xxxx_XACTS_PER_PAGE (where - * xxxx is CLOG or SUBTRANS, respectively), and segment numbering at - * 0xFFFFFFFF/xxxx_XACTS_PER_PAGE/SLRU_PAGES_PER_SEGMENT. We need - * take no explicit notice of that fact in slru.c, except when comparing - * segment and page numbers in SimpleLruTruncate (see PagePrecedes()). - */ -#define SLRU_PAGES_PER_SEGMENT 32 - /* * Page status codes. Note that these do not include the "dirty" bit. * page_dirty can be true only in the VALID or WRITE_IN_PROGRESS states; @@ -55,7 +42,7 @@ typedef enum /* * Shared-memory state * - * ControlLock is used to protect access to the other fields, except + * SLRU bank locks are used to protect access to the other fields, except * latest_page_number, which uses atomics; see comment in slru.c. */ typedef struct SlruSharedData @@ -120,23 +107,32 @@ typedef struct SlruSharedData typedef SlruSharedData *SlruShared; +typedef struct SlruDesc SlruDesc; + /* - * SlruCtlData is an unshared structure that points to the active information - * in shared memory. + * Options for SimpleLruRequest() */ -typedef struct SlruCtlData +typedef struct SlruOpts { - SlruShared shared; + /* Options for allocating the underlying shmem area; do not touch directly */ + ShmemStructOpts base; - /* Number of banks in this SLRU. */ - uint16 nbanks; + /* + * name of SLRU. (This is user-visible, pick with care!) + */ + const char *name; /* - * If true, use long segment file names. Otherwise, use short file names. - * - * For details about the file name format, see SlruFileName(). + * Pointer to a backend-private handle for the SLRU. It is initialized + * when the SLRU is initialized or attached to. */ - bool long_segment_names; + SlruDesc *desc; + + /* number of page slots to use. */ + int nslots; + + /* number of LSN groups per page (set to zero if not relevant). */ + int nlsns; /* * Which sync handler function to use when handing sync requests over to @@ -144,6 +140,19 @@ typedef struct SlruCtlData */ SyncRequestHandler sync_handler; + /* + * PGDATA-relative subdirectory that will contain the files. + */ + const char *Dir; + + /* + * If true, use long segment file names. Otherwise, use short file names. + * + * For details about the file name format, see SlruFileName(). + */ + bool long_segment_names; + + /* * Decide whether a page is "older" for truncation and as a hint for * evicting pages in LRU order. Return true if every entry of the first @@ -157,62 +166,90 @@ typedef struct SlruCtlData bool (*PagePrecedes) (int64, int64); /* - * Dir is set during SimpleLruInit and does not change thereafter. Since - * it's always the same, it doesn't need to be in shared memory. + * Callback to provide more details on an I/O error. This is called as + * part of ereport(), and the callback function is expected to call + * errdetail() to provide more context on the SLRU access. + * + * The opaque_data argument here is the argument that was passed to the + * SimpleLruReadPage() function. */ - char Dir[64]; -} SlruCtlData; + int (*errdetail_for_io_error) (const void *opaque_data); + + /* + * Tranche IDs to use for the SLRU's per-buffer and per-bank LWLocks. If + * these are left as zeros, new tranches will be assigned dynamically. + */ + int buffer_tranche_id; + int bank_tranche_id; +} SlruOpts; + +/* + * SlruDesc is an unshared structure that points to the active information + * in shared memory. + */ +typedef struct SlruDesc +{ + SlruOpts options; -typedef SlruCtlData *SlruCtl; + SlruShared shared; + + /* Number of banks in this SLRU. */ + uint16 nbanks; +} SlruDesc; /* - * Get the SLRU bank lock for given SlruCtl and the pageno. + * Get the SLRU bank lock for the given pageno. * * This lock needs to be acquired to access the slru buffer slots in the * respective bank. */ static inline LWLock * -SimpleLruGetBankLock(SlruCtl ctl, int64 pageno) +SimpleLruGetBankLock(SlruDesc *ctl, int64 pageno) { int bankno; + Assert(ctl->nbanks != 0); bankno = pageno % ctl->nbanks; return &(ctl->shared->bank_locks[bankno].lock); } -extern Size SimpleLruShmemSize(int nslots, int nlsns); +extern void SimpleLruRequestWithOpts(const SlruOpts *options); + +#define SimpleLruRequest(...) \ + SimpleLruRequestWithOpts(&(SlruOpts){__VA_ARGS__}) + extern int SimpleLruAutotuneBuffers(int divisor, int max); -extern void SimpleLruInit(SlruCtl ctl, const char *name, int nslots, int nlsns, - const char *subdir, int buffer_tranche_id, - int bank_tranche_id, SyncRequestHandler sync_handler, - bool long_segment_names); -extern int SimpleLruZeroPage(SlruCtl ctl, int64 pageno); -extern int SimpleLruReadPage(SlruCtl ctl, int64 pageno, bool write_ok, - TransactionId xid); -extern int SimpleLruReadPage_ReadOnly(SlruCtl ctl, int64 pageno, - TransactionId xid); -extern void SimpleLruWritePage(SlruCtl ctl, int slotno); -extern void SimpleLruWriteAll(SlruCtl ctl, bool allow_redirtied); +extern int SimpleLruZeroPage(SlruDesc *ctl, int64 pageno); +extern void SimpleLruZeroAndWritePage(SlruDesc *ctl, int64 pageno); +extern int SimpleLruReadPage(SlruDesc *ctl, int64 pageno, bool write_ok, + const void *opaque_data); +extern int SimpleLruReadPage_ReadOnly(SlruDesc *ctl, int64 pageno, + const void *opaque_data); +extern void SimpleLruWritePage(SlruDesc *ctl, int slotno); +extern void SimpleLruWriteAll(SlruDesc *ctl, bool allow_redirtied); #ifdef USE_ASSERT_CHECKING -extern void SlruPagePrecedesUnitTests(SlruCtl ctl, int per_page); +extern void SlruPagePrecedesUnitTests(SlruDesc *ctl, int per_page); #else #define SlruPagePrecedesUnitTests(ctl, per_page) do {} while (0) #endif -extern void SimpleLruTruncate(SlruCtl ctl, int64 cutoffPage); -extern bool SimpleLruDoesPhysicalPageExist(SlruCtl ctl, int64 pageno); +extern void SimpleLruTruncate(SlruDesc *ctl, int64 cutoffPage); +extern bool SimpleLruDoesPhysicalPageExist(SlruDesc *ctl, int64 pageno); -typedef bool (*SlruScanCallback) (SlruCtl ctl, char *filename, int64 segpage, +typedef bool (*SlruScanCallback) (SlruDesc *ctl, char *filename, int64 segpage, void *data); -extern bool SlruScanDirectory(SlruCtl ctl, SlruScanCallback callback, void *data); -extern void SlruDeleteSegment(SlruCtl ctl, int64 segno); +extern bool SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *data); +extern void SlruDeleteSegment(SlruDesc *ctl, int64 segno); -extern int SlruSyncFileTag(SlruCtl ctl, const FileTag *ftag, char *path); +extern int SlruSyncFileTag(SlruDesc *ctl, const FileTag *ftag, char *path); /* SlruScanDirectory public callbacks */ -extern bool SlruScanDirCbReportPresence(SlruCtl ctl, char *filename, +extern bool SlruScanDirCbReportPresence(SlruDesc *ctl, char *filename, int64 segpage, void *data); -extern bool SlruScanDirCbDeleteAll(SlruCtl ctl, char *filename, int64 segpage, +extern bool SlruScanDirCbDeleteAll(SlruDesc *ctl, char *filename, int64 segpage, void *data); extern bool check_slru_buffers(const char *name, int *newval); +extern void shmem_slru_init(void *location, ShmemStructOpts *base_options); +extern void shmem_slru_attach(void *location, ShmemStructOpts *base_options); + #endif /* SLRU_H */ diff --git a/src/include/access/spgist.h b/src/include/access/spgist.h index cbe9b347d8fa4..083d93f8ffdc0 100644 --- a/src/include/access/spgist.h +++ b/src/include/access/spgist.h @@ -4,7 +4,7 @@ * Public header file for SP-GiST access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/spgist.h diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h index cb43a278f4667..ec6d6f5f74d27 100644 --- a/src/include/access/spgist_private.h +++ b/src/include/access/spgist_private.h @@ -4,7 +4,7 @@ * Private declarations for SP-GiST access method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/spgist_private.h @@ -285,10 +285,12 @@ typedef struct SpGistCache * If the prefix datum is of a pass-by-value type, it is stored in its * Datum representation, that is its on-disk representation is of length * sizeof(Datum). This is a fairly unfortunate choice, because in no other - * place does Postgres use Datum as an on-disk representation; it creates - * an unnecessary incompatibility between 32-bit and 64-bit builds. But the - * compatibility loss is mostly theoretical since MAXIMUM_ALIGNOF typically - * differs between such builds, too. Anyway we're stuck with it now. + * place does Postgres use Datum as an on-disk representation. Formerly it + * meant an unnecessary incompatibility between 32-bit and 64-bit builds, and + * as of v19 it instead creates a hazard for binary upgrades on 32-bit builds. + * Fortunately, that hazard seems mostly theoretical for lack of affected + * opclasses. Going forward, we will be using a fixed size of Datum so that + * there's no longer any pressing reason to change this. */ typedef struct SpGistInnerTupleData { @@ -377,8 +379,8 @@ typedef SpGistNodeTupleData *SpGistNodeTuple; * * size must be a multiple of MAXALIGN; also, it must be at least SGDTSIZE * so that the tuple can be converted to REDIRECT status later. (This - * restriction only adds bytes for a NULL leaf datum stored on a 32-bit - * machine; otherwise alignment restrictions force it anyway.) + * restriction only adds bytes for a NULL leaf datum; otherwise alignment + * restrictions force it anyway.) */ typedef struct SpGistLeafTupleData { @@ -509,7 +511,7 @@ extern unsigned int SpGistGetInnerTypeSize(SpGistTypeDesc *att, Datum datum); extern Size SpGistGetLeafTupleSize(TupleDesc tupleDescriptor, const Datum *datums, const bool *isnulls); extern SpGistLeafTuple spgFormLeafTuple(SpGistState *state, - ItemPointer heapPtr, + const ItemPointerData *heapPtr, const Datum *datums, const bool *isnulls); extern SpGistNodeTuple spgFormNodeTuple(SpGistState *state, Datum label, bool isnull); @@ -524,7 +526,7 @@ extern void spgDeformLeafTuple(SpGistLeafTuple tup, TupleDesc tupleDescriptor, extern Datum *spgExtractNodeLabels(SpGistState *state, SpGistInnerTuple innerTuple); extern OffsetNumber SpGistPageAddNewItem(SpGistState *state, Page page, - Item item, Size size, + const void *item, Size size, OffsetNumber *startOffset, bool errorOK); extern bool spgproperty(Oid index_oid, int attno, @@ -539,7 +541,7 @@ extern void spgPageIndexMultiDelete(SpGistState *state, Page page, int firststate, int reststate, BlockNumber blkno, OffsetNumber offnum); extern bool spgdoinsert(Relation index, SpGistState *state, - ItemPointer heapPtr, Datum *datums, bool *isnulls); + const ItemPointerData *heapPtr, const Datum *datums, const bool *isnulls); /* spgproc.c */ extern double *spg_key_orderbys_distances(Datum key, bool isLeaf, diff --git a/src/include/access/spgxlog.h b/src/include/access/spgxlog.h index 695b01421b20b..c257408be669e 100644 --- a/src/include/access/spgxlog.h +++ b/src/include/access/spgxlog.h @@ -3,7 +3,7 @@ * spgxlog.h * xlog declarations for SP-GiST access method. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/spgxlog.h diff --git a/src/include/access/stratnum.h b/src/include/access/stratnum.h index ee036d2b62fdc..812ecd2ef8a63 100644 --- a/src/include/access/stratnum.h +++ b/src/include/access/stratnum.h @@ -4,7 +4,7 @@ * POSTGRES strategy number definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/stratnum.h diff --git a/src/include/access/subtrans.h b/src/include/access/subtrans.h index c1193a4dd475f..d986cd9e802ba 100644 --- a/src/include/access/subtrans.h +++ b/src/include/access/subtrans.h @@ -3,7 +3,7 @@ * * PostgreSQL subtransaction-log manager * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/subtrans.h @@ -15,8 +15,6 @@ extern void SubTransSetParent(TransactionId xid, TransactionId parent); extern TransactionId SubTransGetParent(TransactionId xid); extern TransactionId SubTransGetTopmostTransaction(TransactionId xid); -extern Size SUBTRANSShmemSize(void); -extern void SUBTRANSShmemInit(void); extern void BootStrapSUBTRANS(void); extern void StartupSUBTRANS(TransactionId oldestActiveXID); extern void CheckPointSUBTRANS(void); diff --git a/src/include/access/syncscan.h b/src/include/access/syncscan.h index 397bacab05562..32f8332aaeea9 100644 --- a/src/include/access/syncscan.h +++ b/src/include/access/syncscan.h @@ -4,7 +4,7 @@ * POSTGRES synchronous scan support functions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/syncscan.h @@ -24,7 +24,5 @@ extern PGDLLIMPORT bool trace_syncscan; extern void ss_report_location(Relation rel, BlockNumber location); extern BlockNumber ss_get_location(Relation rel, BlockNumber relnblocks); -extern void SyncScanShmemInit(void); -extern Size SyncScanShmemSize(void); #endif diff --git a/src/include/access/sysattr.h b/src/include/access/sysattr.h index 47104f3721c68..c29445c8f8649 100644 --- a/src/include/access/sysattr.h +++ b/src/include/access/sysattr.h @@ -4,7 +4,7 @@ * POSTGRES system attribute definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/sysattr.h diff --git a/src/include/access/table.h b/src/include/access/table.h index f530b06089dbf..80dd188dbb2a0 100644 --- a/src/include/access/table.h +++ b/src/include/access/table.h @@ -4,7 +4,7 @@ * Generic routines for table related code. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/table.h diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index 8713e12cbfb99..f2c36696bcad0 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -4,7 +4,7 @@ * POSTGRES table access method definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/tableam.h @@ -33,17 +33,21 @@ extern PGDLLIMPORT char *default_table_access_method; extern PGDLLIMPORT bool synchronize_seqscans; -struct BulkInsertStateData; -struct IndexInfo; -struct SampleScanState; -struct VacuumParams; -struct ValidateIndexState; +/* forward references in this file */ +typedef struct BulkInsertStateData BulkInsertStateData; +typedef struct IndexInfo IndexInfo; +typedef struct SampleScanState SampleScanState; +typedef struct ScanKeyData ScanKeyData; +typedef struct ValidateIndexState ValidateIndexState; +typedef struct VacuumParams VacuumParams; /* * Bitmask values for the flags argument to the scan_begin callback. */ typedef enum ScanOptions { + SO_NONE = 0, + /* one of SO_TYPE_* may be specified */ SO_TYPE_SEQSCAN = 1 << 0, SO_TYPE_BITMAPSCAN = 1 << 1, @@ -62,8 +66,27 @@ typedef enum ScanOptions /* unregister snapshot at scan end? */ SO_TEMP_SNAPSHOT = 1 << 9, + + /* set if the query doesn't modify the relation */ + SO_HINT_REL_READ_ONLY = 1 << 10, + + /* collect scan instrumentation */ + SO_SCAN_INSTRUMENT = 1 << 11, } ScanOptions; +/* + * Mask of flags that are set internally by the table scan functions and + * shouldn't be passed by callers. Some of these are effectively set by callers + * through parameters to table scan functions (e.g. SO_ALLOW_STRAT/allow_strat), + * however, for now, retain tight control over them and don't allow users to + * pass these themselves to table scan functions. + */ +#define SO_INTERNAL_FLAGS \ + (SO_TYPE_SEQSCAN | SO_TYPE_BITMAPSCAN | SO_TYPE_SAMPLESCAN | \ + SO_TYPE_TIDSCAN | SO_TYPE_TIDRANGESCAN | SO_TYPE_ANALYZE | \ + SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE | \ + SO_TEMP_SNAPSHOT) + /* * Result codes for table_{update,delete,lock_tuple}, and for visibility * routines inside table AMs. @@ -121,7 +144,9 @@ typedef enum TU_UpdateIndexes /* * When table_tuple_update, table_tuple_delete, or table_tuple_lock fail * because the target tuple is already outdated, they fill in this struct to - * provide information to the caller about what happened. + * provide information to the caller about what happened. When those functions + * succeed, the contents of this struct should not be relied upon, except for + * `traversed`, which may be set in both success and failure cases. * * ctid is the target's ctid link: it is the same as the target's TID if the * target was deleted, or the location of the replacement tuple if the target @@ -137,6 +162,9 @@ typedef enum TU_UpdateIndexes * tuple); otherwise cmax is zero. (We make this restriction because * HeapTupleHeaderGetCmax doesn't work for tuples outdated in other * transactions.) + * + * traversed indicates if an update chain was followed in order to try to lock + * the target tuple. (This may be set in both success and failure cases.) */ typedef struct TM_FailureData { @@ -248,12 +276,22 @@ typedef struct TM_IndexDeleteOp TM_IndexStatus *status; } TM_IndexDeleteOp; -/* "options" flag bits for table_tuple_insert */ +/* + * "options" flag bits for table_tuple_insert. Access methods may define + * their own bits for internal use, as long as they don't collide with these. + */ /* TABLE_INSERT_SKIP_WAL was 0x0001; RelationNeedsWAL() now governs */ #define TABLE_INSERT_SKIP_FSM 0x0002 #define TABLE_INSERT_FROZEN 0x0004 #define TABLE_INSERT_NO_LOGICAL 0x0008 +/* "options" flag bits for table_tuple_delete */ +#define TABLE_DELETE_CHANGING_PARTITION (1 << 0) +#define TABLE_DELETE_NO_LOGICAL (1 << 1) + +/* "options" flag bits for table_tuple_update */ +#define TABLE_UPDATE_NO_LOGICAL (1 << 0) + /* flag bits for table_tuple_lock */ /* Follow tuples whose update is in progress if lock modes don't conflict */ #define TUPLE_LOCK_FLAG_LOCK_UPDATE_IN_PROGRESS (1 << 0) @@ -315,12 +353,13 @@ typedef struct TableAmRoutine * `flags` is a bitmask indicating the type of scan (ScanOptions's * SO_TYPE_*, currently only one may be specified), options controlling * the scan's behaviour (ScanOptions's SO_ALLOW_*, several may be - * specified, an AM may ignore unsupported ones) and whether the snapshot - * needs to be deallocated at scan_end (ScanOptions's SO_TEMP_SNAPSHOT). + * specified, an AM may ignore unsupported ones), whether the snapshot + * needs to be deallocated at scan_end (ScanOptions's SO_TEMP_SNAPSHOT), + * and any number of the other ScanOptions values. */ TableScanDesc (*scan_begin) (Relation rel, Snapshot snapshot, - int nkeys, struct ScanKeyData *key, + int nkeys, ScanKeyData *key, ParallelTableScanDesc pscan, uint32 flags); @@ -334,7 +373,7 @@ typedef struct TableAmRoutine * Restart relation scan. If set_params is set to true, allow_{strat, * sync, pagemode} (see scan_begin) changes should be taken into account. */ - void (*scan_rescan) (TableScanDesc scan, struct ScanKeyData *key, + void (*scan_rescan) (TableScanDesc scan, ScanKeyData *key, bool set_params, bool allow_strat, bool allow_sync, bool allow_pagemode); @@ -412,9 +451,12 @@ typedef struct TableAmRoutine * IndexFetchTableData, which the AM will typically embed in a larger * structure with additional information. * + * flags is a bitmask of ScanOptions affecting underlying table scan + * behavior. See scan_begin() for more information on passing these. + * * Tuples for an index scan can then be fetched via index_fetch_tuple. */ - struct IndexFetchTableData *(*index_fetch_begin) (Relation rel); + struct IndexFetchTableData *(*index_fetch_begin) (Relation rel, uint32 flags); /* * Reset index fetch. Typically this will release cross index fetch @@ -502,15 +544,15 @@ typedef struct TableAmRoutine /* see table_tuple_insert() for reference about parameters */ void (*tuple_insert) (Relation rel, TupleTableSlot *slot, - CommandId cid, int options, - struct BulkInsertStateData *bistate); + CommandId cid, uint32 options, + BulkInsertStateData *bistate); /* see table_tuple_insert_speculative() for reference about parameters */ void (*tuple_insert_speculative) (Relation rel, TupleTableSlot *slot, CommandId cid, - int options, - struct BulkInsertStateData *bistate, + uint32 options, + BulkInsertStateData *bistate, uint32 specToken); /* see table_tuple_complete_speculative() for reference about parameters */ @@ -521,23 +563,24 @@ typedef struct TableAmRoutine /* see table_multi_insert() for reference about parameters */ void (*multi_insert) (Relation rel, TupleTableSlot **slots, int nslots, - CommandId cid, int options, struct BulkInsertStateData *bistate); + CommandId cid, uint32 options, BulkInsertStateData *bistate); /* see table_tuple_delete() for reference about parameters */ TM_Result (*tuple_delete) (Relation rel, ItemPointer tid, CommandId cid, + uint32 options, Snapshot snapshot, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, - bool changingPart); + TM_FailureData *tmfd); /* see table_tuple_update() for reference about parameters */ TM_Result (*tuple_update) (Relation rel, ItemPointer otid, TupleTableSlot *slot, CommandId cid, + uint32 options, Snapshot snapshot, Snapshot crosscheck, bool wait, @@ -568,7 +611,7 @@ typedef struct TableAmRoutine * * Optional callback. */ - void (*finish_bulk_insert) (Relation rel, int options); + void (*finish_bulk_insert) (Relation rel, uint32 options); /* ------------------------------------------------------------------------ @@ -623,6 +666,7 @@ typedef struct TableAmRoutine Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -645,7 +689,7 @@ typedef struct TableAmRoutine * integrate with autovacuum's scheduling. */ void (*relation_vacuum) (Relation rel, - struct VacuumParams *params, + const VacuumParams *params, BufferAccessStrategy bstrategy); /* @@ -677,7 +721,6 @@ typedef struct TableAmRoutine * callback). */ bool (*scan_analyze_next_tuple) (TableScanDesc scan, - TransactionId OldestXmin, double *liverows, double *deadrows, TupleTableSlot *slot); @@ -685,7 +728,7 @@ typedef struct TableAmRoutine /* see table_index_build_range_scan for reference about parameters */ double (*index_build_range_scan) (Relation table_rel, Relation index_rel, - struct IndexInfo *index_info, + IndexInfo *index_info, bool allow_sync, bool anyvisible, bool progress, @@ -698,9 +741,9 @@ typedef struct TableAmRoutine /* see table_index_validate_scan for reference about parameters */ void (*index_validate_scan) (Relation table_rel, Relation index_rel, - struct IndexInfo *index_info, + IndexInfo *index_info, Snapshot snapshot, - struct ValidateIndexState *state); + ValidateIndexState *state); /* ------------------------------------------------------------------------ @@ -744,7 +787,7 @@ typedef struct TableAmRoutine int32 attrsize, int32 sliceoffset, int32 slicelength, - struct varlena *result); + varlena *result); /* ------------------------------------------------------------------------ @@ -816,7 +859,7 @@ typedef struct TableAmRoutine * scans. If infeasible to implement, the AM may raise an error. */ bool (*scan_sample_next_block) (TableScanDesc scan, - struct SampleScanState *scanstate); + SampleScanState *scanstate); /* * This callback, only called after scan_sample_next_block has returned @@ -832,7 +875,7 @@ typedef struct TableAmRoutine * assumption somehow. */ bool (*scan_sample_next_tuple) (TableScanDesc scan, - struct SampleScanState *scanstate, + SampleScanState *scanstate, TupleTableSlot *slot); } TableAmRoutine; @@ -862,18 +905,49 @@ extern TupleTableSlot *table_slot_create(Relation relation, List **reglist); * ---------------------------------------------------------------------------- */ +/* + * A wrapper around the Table Access Method scan_begin callback, to centralize + * error checking. All calls to ->scan_begin() should go through this + * function. + * + * The caller-provided user_flags are validated against SO_INTERNAL_FLAGS to + * catch callers that accidentally pass scan-type or other internal flags. + */ +static TableScanDesc +table_beginscan_common(Relation rel, Snapshot snapshot, int nkeys, + ScanKeyData *key, ParallelTableScanDesc pscan, + uint32 flags, uint32 user_flags) +{ + Assert((user_flags & SO_INTERNAL_FLAGS) == 0); + Assert((flags & ~SO_INTERNAL_FLAGS) == 0); + flags |= user_flags; + + /* + * We don't allow scans to be started while CheckXidAlive is set, except + * via systable_beginscan() et al. See detailed comments in xact.c where + * these variables are declared. + */ + if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) + elog(ERROR, "scan started during logical decoding"); + + return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, pscan, flags); +} + /* * Start a scan of `rel`. Returned tuples pass a visibility test of * `snapshot`, and if nkeys != 0, the results are filtered by those scan keys. + * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. */ static inline TableScanDesc table_beginscan(Relation rel, Snapshot snapshot, - int nkeys, struct ScanKeyData *key) + int nkeys, ScanKeyData *key, uint32 flags) { - uint32 flags = SO_TYPE_SEQSCAN | + uint32 internal_flags = SO_TYPE_SEQSCAN | SO_ALLOW_STRAT | SO_ALLOW_SYNC | SO_ALLOW_PAGEMODE; - return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, NULL, flags); + return table_beginscan_common(rel, snapshot, nkeys, key, NULL, + internal_flags, flags); } /* @@ -881,7 +955,7 @@ table_beginscan(Relation rel, Snapshot snapshot, * snapshot appropriate for scanning catalog relations. */ extern TableScanDesc table_beginscan_catalog(Relation relation, int nkeys, - struct ScanKeyData *key); + ScanKeyData *key); /* * Like table_beginscan(), but table_beginscan_strat() offers an extended API @@ -892,7 +966,7 @@ extern TableScanDesc table_beginscan_catalog(Relation relation, int nkeys, */ static inline TableScanDesc table_beginscan_strat(Relation rel, Snapshot snapshot, - int nkeys, struct ScanKeyData *key, + int nkeys, ScanKeyData *key, bool allow_strat, bool allow_sync) { uint32 flags = SO_TYPE_SEQSCAN | SO_ALLOW_PAGEMODE; @@ -902,7 +976,8 @@ table_beginscan_strat(Relation rel, Snapshot snapshot, if (allow_sync) flags |= SO_ALLOW_SYNC; - return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, NULL, flags); + return table_beginscan_common(rel, snapshot, nkeys, key, NULL, + flags, SO_NONE); } /* @@ -910,15 +985,17 @@ table_beginscan_strat(Relation rel, Snapshot snapshot, * TableScanDesc for a bitmap heap scan. Although that scan technology is * really quite unlike a standard seqscan, there is just enough commonality to * make it worth using the same data structure. + * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. */ static inline TableScanDesc table_beginscan_bm(Relation rel, Snapshot snapshot, - int nkeys, struct ScanKeyData *key) + int nkeys, ScanKeyData *key, uint32 flags) { - uint32 flags = SO_TYPE_BITMAPSCAN | SO_ALLOW_PAGEMODE; + uint32 internal_flags = SO_TYPE_BITMAPSCAN | SO_ALLOW_PAGEMODE; - return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, - NULL, flags); + return table_beginscan_common(rel, snapshot, nkeys, key, NULL, + internal_flags, flags); } /* @@ -927,23 +1004,26 @@ table_beginscan_bm(Relation rel, Snapshot snapshot, * using the same data structure although the behavior is rather different. * In addition to the options offered by table_beginscan_strat, this call * also allows control of whether page-mode visibility checking is used. + * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. */ static inline TableScanDesc table_beginscan_sampling(Relation rel, Snapshot snapshot, - int nkeys, struct ScanKeyData *key, + int nkeys, ScanKeyData *key, bool allow_strat, bool allow_sync, - bool allow_pagemode) + bool allow_pagemode, uint32 flags) { - uint32 flags = SO_TYPE_SAMPLESCAN; + uint32 internal_flags = SO_TYPE_SAMPLESCAN; if (allow_strat) - flags |= SO_ALLOW_STRAT; + internal_flags |= SO_ALLOW_STRAT; if (allow_sync) - flags |= SO_ALLOW_SYNC; + internal_flags |= SO_ALLOW_SYNC; if (allow_pagemode) - flags |= SO_ALLOW_PAGEMODE; + internal_flags |= SO_ALLOW_PAGEMODE; - return rel->rd_tableam->scan_begin(rel, snapshot, nkeys, key, NULL, flags); + return table_beginscan_common(rel, snapshot, nkeys, key, NULL, + internal_flags, flags); } /* @@ -956,7 +1036,8 @@ table_beginscan_tid(Relation rel, Snapshot snapshot) { uint32 flags = SO_TYPE_TIDSCAN; - return rel->rd_tableam->scan_begin(rel, snapshot, 0, NULL, NULL, flags); + return table_beginscan_common(rel, snapshot, 0, NULL, NULL, + flags, SO_NONE); } /* @@ -969,7 +1050,8 @@ table_beginscan_analyze(Relation rel) { uint32 flags = SO_TYPE_ANALYZE; - return rel->rd_tableam->scan_begin(rel, NULL, 0, NULL, NULL, flags); + return table_beginscan_common(rel, NULL, 0, NULL, NULL, + flags, SO_NONE); } /* @@ -985,8 +1067,7 @@ table_endscan(TableScanDesc scan) * Restart a relation scan. */ static inline void -table_rescan(TableScanDesc scan, - struct ScanKeyData *key) +table_rescan(TableScanDesc scan, ScanKeyData *key) { scan->rs_rd->rd_tableam->scan_rescan(scan, key, false, false, false, false); } @@ -1000,7 +1081,7 @@ table_rescan(TableScanDesc scan, * previously selected startblock will be kept. */ static inline void -table_rescan_set_params(TableScanDesc scan, struct ScanKeyData *key, +table_rescan_set_params(TableScanDesc scan, ScanKeyData *key, bool allow_strat, bool allow_sync, bool allow_pagemode) { scan->rs_rd->rd_tableam->scan_rescan(scan, key, true, @@ -1020,14 +1101,6 @@ table_scan_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableS Assert(direction == ForwardScanDirection || direction == BackwardScanDirection); - /* - * We don't expect direct calls to table_scan_getnextslot with valid - * CheckXidAlive for catalog or regular tables. See detailed comments in - * xact.c where these variables are declared. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected table_scan_getnextslot call during logical decoding"); - return sscan->rs_rd->rd_tableam->scan_getnextslot(sscan, direction, slot); } @@ -1039,16 +1112,19 @@ table_scan_getnextslot(TableScanDesc sscan, ScanDirection direction, TupleTableS /* * table_beginscan_tidrange is the entry point for setting up a TableScanDesc * for a TID range scan. + * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. */ static inline TableScanDesc table_beginscan_tidrange(Relation rel, Snapshot snapshot, ItemPointer mintid, - ItemPointer maxtid) + ItemPointer maxtid, uint32 flags) { TableScanDesc sscan; - uint32 flags = SO_TYPE_TIDRANGESCAN | SO_ALLOW_PAGEMODE; + uint32 internal_flags = SO_TYPE_TIDRANGESCAN | SO_ALLOW_PAGEMODE; - sscan = rel->rd_tableam->scan_begin(rel, snapshot, 0, NULL, NULL, flags); + sscan = table_beginscan_common(rel, snapshot, 0, NULL, NULL, + internal_flags, flags); /* Set the range of TIDs to scan */ sscan->rs_rd->rd_tableam->scan_set_tidrange(sscan, mintid, maxtid); @@ -1120,10 +1196,26 @@ extern void table_parallelscan_initialize(Relation rel, * table_parallelscan_initialize(), for the same relation. The initialization * does not need to have happened in this backend. * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. + * * Caller must hold a suitable lock on the relation. */ extern TableScanDesc table_beginscan_parallel(Relation relation, - ParallelTableScanDesc pscan); + ParallelTableScanDesc pscan, + uint32 flags); + +/* + * Begin a parallel tid range scan. `pscan` needs to have been initialized + * with table_parallelscan_initialize(), for the same relation. The + * initialization does not need to have happened in this backend. + * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. + * + * Caller must hold a suitable lock on the relation. + */ +extern TableScanDesc table_beginscan_parallel_tidrange(Relation relation, + ParallelTableScanDesc pscan, + uint32 flags); /* * Restart a parallel scan. Call this in the leader process. Caller is @@ -1146,12 +1238,24 @@ table_parallelscan_reinitialize(Relation rel, ParallelTableScanDesc pscan) * Prepare to fetch tuples from the relation, as needed when fetching tuples * for an index scan. * + * flags is a bitmask of ScanOptions. No SO_INTERNAL_FLAGS are permitted. + * * Tuples for an index scan can then be fetched via table_index_fetch_tuple(). */ static inline IndexFetchTableData * -table_index_fetch_begin(Relation rel) +table_index_fetch_begin(Relation rel, uint32 flags) { - return rel->rd_tableam->index_fetch_begin(rel); + Assert((flags & SO_INTERNAL_FLAGS) == 0); + + /* + * We don't allow scans to be started while CheckXidAlive is set, except + * via systable_beginscan() et al. See detailed comments in xact.c where + * these variables are declared. + */ + if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) + elog(ERROR, "scan started during logical decoding"); + + return rel->rd_tableam->index_fetch_begin(rel, flags); } /* @@ -1204,14 +1308,6 @@ table_index_fetch_tuple(struct IndexFetchTableData *scan, TupleTableSlot *slot, bool *call_again, bool *all_dead) { - /* - * We don't expect direct calls to table_index_fetch_tuple with valid - * CheckXidAlive for catalog or regular tables. See detailed comments in - * xact.c where these variables are declared. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected table_index_fetch_tuple call during logical decoding"); - return scan->rel->rd_tableam->index_fetch_tuple(scan, tid, snapshot, slot, call_again, all_dead); @@ -1360,7 +1456,7 @@ table_index_delete_tuples(Relation rel, TM_IndexDeleteOp *delstate) */ static inline void table_tuple_insert(Relation rel, TupleTableSlot *slot, CommandId cid, - int options, struct BulkInsertStateData *bistate) + uint32 options, BulkInsertStateData *bistate) { rel->rd_tableam->tuple_insert(rel, slot, cid, options, bistate); @@ -1379,8 +1475,8 @@ table_tuple_insert(Relation rel, TupleTableSlot *slot, CommandId cid, */ static inline void table_tuple_insert_speculative(Relation rel, TupleTableSlot *slot, - CommandId cid, int options, - struct BulkInsertStateData *bistate, + CommandId cid, uint32 options, + BulkInsertStateData *bistate, uint32 specToken) { rel->rd_tableam->tuple_insert_speculative(rel, slot, cid, options, @@ -1415,7 +1511,7 @@ table_tuple_complete_speculative(Relation rel, TupleTableSlot *slot, */ static inline void table_multi_insert(Relation rel, TupleTableSlot **slots, int nslots, - CommandId cid, int options, struct BulkInsertStateData *bistate) + CommandId cid, uint32 options, BulkInsertStateData *bistate) { rel->rd_tableam->multi_insert(rel, slots, nslots, cid, options, bistate); @@ -1428,16 +1524,18 @@ table_multi_insert(Relation rel, TupleTableSlot **slots, int nslots, * concurrent-update conditions. Use simple_table_tuple_delete instead. * * Input parameters: - * relation - table to be modified (caller must hold suitable lock) + * rel - table to be modified (caller must hold suitable lock) * tid - TID of tuple to be deleted * cid - delete command ID (used for visibility test, and stored into * cmax if successful) + * options - bitmask of options. Supported values: + * TABLE_DELETE_CHANGING_PARTITION: the tuple is being moved to another + * partition table due to an update of the partition key. * crosscheck - if not InvalidSnapshot, also check tuple against this * wait - true if should wait for any conflicting update to commit/abort + * * Output parameters: * tmfd - filled in failure cases (see below) - * changingPart - true iff the tuple is being moved to another partition - * table due to an update of the partition key. Otherwise, false. * * Normal, successful return value is TM_Ok, which means we did actually * delete it. Failure return codes are TM_SelfModified, TM_Updated, and @@ -1449,12 +1547,12 @@ table_multi_insert(Relation rel, TupleTableSlot **slots, int nslots, */ static inline TM_Result table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid, - Snapshot snapshot, Snapshot crosscheck, bool wait, - TM_FailureData *tmfd, bool changingPart) + uint32 options, Snapshot snapshot, Snapshot crosscheck, + bool wait, TM_FailureData *tmfd) { - return rel->rd_tableam->tuple_delete(rel, tid, cid, + return rel->rd_tableam->tuple_delete(rel, tid, cid, options, snapshot, crosscheck, - wait, tmfd, changingPart); + wait, tmfd); } /* @@ -1464,18 +1562,23 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid, * concurrent-update conditions. Use simple_table_tuple_update instead. * * Input parameters: - * relation - table to be modified (caller must hold suitable lock) + * rel - table to be modified (caller must hold suitable lock) * otid - TID of old tuple to be replaced - * slot - newly constructed tuple data to store * cid - update command ID (used for visibility test, and stored into * cmax/cmin if successful) + * options - bitmask of options. No values are currently recognized. * crosscheck - if not InvalidSnapshot, also check old tuple against this - * wait - true if should wait for any conflicting update to commit/abort + * options - These allow the caller to specify options that may change the + * behavior of the AM. The AM will ignore options that it does not support. + * TABLE_UPDATE_NO_LOGICAL -- force-disables the emitting of logical + * decoding information for the tuple. + * * Output parameters: + * slot - newly constructed tuple data to store * tmfd - filled in failure cases (see below) * lockmode - filled with lock mode acquired on tuple - * update_indexes - in success cases this is set to true if new index entries - * are required for this tuple + * update_indexes - in success cases this is set if new index entries + * are required for this tuple; see TU_UpdateIndexes * * Normal, successful return value is TM_Ok, which means we did actually * update it. Failure return codes are TM_SelfModified, TM_Updated, and @@ -1493,12 +1596,13 @@ table_tuple_delete(Relation rel, ItemPointer tid, CommandId cid, */ static inline TM_Result table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot, - CommandId cid, Snapshot snapshot, Snapshot crosscheck, + CommandId cid, uint32 options, + Snapshot snapshot, Snapshot crosscheck, bool wait, TM_FailureData *tmfd, LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes) { return rel->rd_tableam->tuple_update(rel, otid, slot, - cid, snapshot, crosscheck, + cid, options, snapshot, crosscheck, wait, tmfd, lockmode, update_indexes); } @@ -1507,8 +1611,8 @@ table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot, * Lock a tuple in the specified mode. * * Input parameters: - * relation: relation containing tuple (caller must hold suitable lock) - * tid: TID of tuple to lock + * rel: relation containing tuple (caller must hold suitable lock) + * tid: TID of tuple to lock (updated if an update chain was followed) * snapshot: snapshot to use for visibility determinations * cid: current command ID (used for visibility test, and stored into * tuple's cmax if lock is successful) @@ -1533,8 +1637,10 @@ table_tuple_update(Relation rel, ItemPointer otid, TupleTableSlot *slot, * TM_WouldBlock: lock couldn't be acquired and wait_policy is skip * * In the failure cases other than TM_Invisible and TM_Deleted, the routine - * fills *tmfd with the tuple's t_ctid, t_xmax, and, if possible, t_cmax. See - * comments for struct TM_FailureData for additional info. + * fills *tmfd with the tuple's t_ctid, t_xmax, and, if possible, t_cmax. + * Additionally, in both success and failure cases, tmfd->traversed is set if + * an update chain was followed. See comments for struct TM_FailureData for + * additional info. */ static inline TM_Result table_tuple_lock(Relation rel, ItemPointer tid, Snapshot snapshot, @@ -1552,7 +1658,7 @@ table_tuple_lock(Relation rel, ItemPointer tid, Snapshot snapshot, * tuple_insert and multi_insert with a BulkInsertState specified. */ static inline void -table_finish_bulk_insert(Relation rel, int options) +table_finish_bulk_insert(Relation rel, uint32 options) { /* optional callback */ if (rel->rd_tableam && rel->rd_tableam->finish_bulk_insert) @@ -1627,6 +1733,8 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator) * not needed for the relation's AM * - *xid_cutoff - ditto * - *multi_cutoff - ditto + * - snapshot - if != NULL, ignore data changes done by transactions that this + * (MVCC) snapshot considers still in-progress or in the future. * * Output parameters: * - *xid_cutoff - rel's new relfrozenxid value, may be invalid @@ -1639,6 +1747,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, Relation OldIndex, bool use_sort, TransactionId OldestXmin, + Snapshot snapshot, TransactionId *xid_cutoff, MultiXactId *multi_cutoff, double *num_tuples, @@ -1647,6 +1756,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, { OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex, use_sort, OldestXmin, + snapshot, xid_cutoff, multi_cutoff, num_tuples, tups_vacuumed, tups_recently_dead); @@ -1664,7 +1774,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable, * routine, even if (for ANALYZE) it is part of the same VACUUM command. */ static inline void -table_relation_vacuum(Relation rel, struct VacuumParams *params, +table_relation_vacuum(Relation rel, const VacuumParams *params, BufferAccessStrategy bstrategy) { rel->rd_tableam->relation_vacuum(rel, params, bstrategy); @@ -1695,11 +1805,11 @@ table_scan_analyze_next_block(TableScanDesc scan, ReadStream *stream) * tuples. */ static inline bool -table_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin, +table_scan_analyze_next_tuple(TableScanDesc scan, double *liverows, double *deadrows, TupleTableSlot *slot) { - return scan->rs_rd->rd_tableam->scan_analyze_next_tuple(scan, OldestXmin, + return scan->rs_rd->rd_tableam->scan_analyze_next_tuple(scan, liverows, deadrows, slot); } @@ -1734,7 +1844,7 @@ table_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin, static inline double table_index_build_scan(Relation table_rel, Relation index_rel, - struct IndexInfo *index_info, + IndexInfo *index_info, bool allow_sync, bool progress, IndexBuildCallback callback, @@ -1767,7 +1877,7 @@ table_index_build_scan(Relation table_rel, static inline double table_index_build_range_scan(Relation table_rel, Relation index_rel, - struct IndexInfo *index_info, + IndexInfo *index_info, bool allow_sync, bool anyvisible, bool progress, @@ -1798,9 +1908,9 @@ table_index_build_range_scan(Relation table_rel, static inline void table_index_validate_scan(Relation table_rel, Relation index_rel, - struct IndexInfo *index_info, + IndexInfo *index_info, Snapshot snapshot, - struct ValidateIndexState *state) + ValidateIndexState *state) { table_rel->rd_tableam->index_validate_scan(table_rel, index_rel, @@ -1875,7 +1985,7 @@ table_relation_toast_am(Relation rel) static inline void table_relation_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize, int32 sliceoffset, - int32 slicelength, struct varlena *result) + int32 slicelength, varlena *result) { toastrel->rd_tableam->relation_fetch_toast_slice(toastrel, valueid, attrsize, @@ -1928,14 +2038,6 @@ table_scan_bitmap_next_tuple(TableScanDesc scan, uint64 *lossy_pages, uint64 *exact_pages) { - /* - * We don't expect direct calls to table_scan_bitmap_next_tuple with valid - * CheckXidAlive for catalog or regular tables. See detailed comments in - * xact.c where these variables are declared. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected table_scan_bitmap_next_tuple call during logical decoding"); - return scan->rs_rd->rd_tableam->scan_bitmap_next_tuple(scan, slot, recheck, @@ -1954,15 +2056,8 @@ table_scan_bitmap_next_tuple(TableScanDesc scan, */ static inline bool table_scan_sample_next_block(TableScanDesc scan, - struct SampleScanState *scanstate) + SampleScanState *scanstate) { - /* - * We don't expect direct calls to table_scan_sample_next_block with valid - * CheckXidAlive for catalog or regular tables. See detailed comments in - * xact.c where these variables are declared. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected table_scan_sample_next_block call during logical decoding"); return scan->rs_rd->rd_tableam->scan_sample_next_block(scan, scanstate); } @@ -1976,16 +2071,9 @@ table_scan_sample_next_block(TableScanDesc scan, */ static inline bool table_scan_sample_next_tuple(TableScanDesc scan, - struct SampleScanState *scanstate, + SampleScanState *scanstate, TupleTableSlot *slot) { - /* - * We don't expect direct calls to table_scan_sample_next_tuple with valid - * CheckXidAlive for catalog or regular tables. See detailed comments in - * xact.c where these variables are declared. - */ - if (unlikely(TransactionIdIsValid(CheckXidAlive) && !bsysscan)) - elog(ERROR, "unexpected table_scan_sample_next_tuple call during logical decoding"); return scan->rs_rd->rd_tableam->scan_sample_next_tuple(scan, scanstate, slot); } @@ -2019,7 +2107,9 @@ extern BlockNumber table_block_parallelscan_nextpage(Relation rel, ParallelBlockTableScanDesc pbscan); extern void table_block_parallelscan_startblock_init(Relation rel, ParallelBlockTableScanWorker pbscanwork, - ParallelBlockTableScanDesc pbscan); + ParallelBlockTableScanDesc pbscan, + BlockNumber startblock, + BlockNumber numblocks); /* ---------------------------------------------------------------------------- diff --git a/src/include/access/tidstore.h b/src/include/access/tidstore.h index 041091df2786d..6713ba55c87e3 100644 --- a/src/include/access/tidstore.h +++ b/src/include/access/tidstore.h @@ -4,7 +4,7 @@ * TidStore interface. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/tidstore.h @@ -40,7 +40,7 @@ extern void TidStoreUnlock(TidStore *ts); extern void TidStoreDestroy(TidStore *ts); extern void TidStoreSetBlockOffsets(TidStore *ts, BlockNumber blkno, OffsetNumber *offsets, int num_offsets); -extern bool TidStoreIsMember(TidStore *ts, ItemPointer tid); +extern bool TidStoreIsMember(TidStore *ts, const ItemPointerData *tid); extern TidStoreIter *TidStoreBeginIterate(TidStore *ts); extern TidStoreIterResult *TidStoreIterateNext(TidStoreIter *iter); extern int TidStoreGetBlockOffsets(TidStoreIterResult *result, diff --git a/src/include/access/timeline.h b/src/include/access/timeline.h index 219b580b00865..97f1d619c35d8 100644 --- a/src/include/access/timeline.h +++ b/src/include/access/timeline.h @@ -3,7 +3,7 @@ * * Functions for reading and writing timeline history files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/timeline.h diff --git a/src/include/access/toast_compression.h b/src/include/access/toast_compression.h index 13c4612ceedcf..3265f10b734f4 100644 --- a/src/include/access/toast_compression.h +++ b/src/include/access/toast_compression.h @@ -3,7 +3,7 @@ * toast_compression.h * Functions for toast compression. * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * * src/include/access/toast_compression.h * @@ -52,21 +52,30 @@ typedef enum ToastCompressionId #define CompressionMethodIsValid(cm) ((cm) != InvalidCompressionMethod) +/* + * Choose an appropriate default toast compression method. If lz4 is + * compiled-in, use it, otherwise use pglz. + */ +#ifdef USE_LZ4 +#define DEFAULT_TOAST_COMPRESSION TOAST_LZ4_COMPRESSION +#else +#define DEFAULT_TOAST_COMPRESSION TOAST_PGLZ_COMPRESSION +#endif /* pglz compression/decompression routines */ -extern struct varlena *pglz_compress_datum(const struct varlena *value); -extern struct varlena *pglz_decompress_datum(const struct varlena *value); -extern struct varlena *pglz_decompress_datum_slice(const struct varlena *value, - int32 slicelength); +extern varlena *pglz_compress_datum(const varlena *value); +extern varlena *pglz_decompress_datum(const varlena *value); +extern varlena *pglz_decompress_datum_slice(const varlena *value, + int32 slicelength); /* lz4 compression/decompression routines */ -extern struct varlena *lz4_compress_datum(const struct varlena *value); -extern struct varlena *lz4_decompress_datum(const struct varlena *value); -extern struct varlena *lz4_decompress_datum_slice(const struct varlena *value, - int32 slicelength); +extern varlena *lz4_compress_datum(const varlena *value); +extern varlena *lz4_decompress_datum(const varlena *value); +extern varlena *lz4_decompress_datum_slice(const varlena *value, + int32 slicelength); /* other stuff */ -extern ToastCompressionId toast_get_compression_id(struct varlena *attr); +extern ToastCompressionId toast_get_compression_id(varlena *attr); extern char CompressionNameToMethod(const char *compression); extern const char *GetCompressionMethodName(char method); diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h index e6ab8afffb67c..2ec92397f2676 100644 --- a/src/include/access/toast_helper.h +++ b/src/include/access/toast_helper.h @@ -4,7 +4,7 @@ * Helper functions for table AMs implementing compressed or * out-of-line storage of varlena attributes. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/include/access/toast_helper.h * @@ -29,7 +29,7 @@ */ typedef struct { - struct varlena *tai_oldexternal; + varlena *tai_oldexternal; int32 tai_size; uint8 tai_colflags; char tai_compression; @@ -107,7 +107,7 @@ extern int toast_tuple_find_biggest_attribute(ToastTupleContext *ttc, bool check_main); extern void toast_tuple_try_compression(ToastTupleContext *ttc, int attribute); extern void toast_tuple_externalize(ToastTupleContext *ttc, int attribute, - int options); + uint32 options); extern void toast_tuple_cleanup(ToastTupleContext *ttc); extern void toast_delete_external(Relation rel, const Datum *values, const bool *isnull, diff --git a/src/include/access/toast_internals.h b/src/include/access/toast_internals.h index 06ae8583c1e1e..bf45889a64280 100644 --- a/src/include/access/toast_internals.h +++ b/src/include/access/toast_internals.h @@ -3,7 +3,7 @@ * toast_internals.h * Internal definitions for the TOAST system. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/include/access/toast_internals.h * @@ -50,7 +50,7 @@ extern Oid toast_get_valid_index(Oid toastoid, LOCKMODE lock); extern void toast_delete_datum(Relation rel, Datum value, bool is_speculative); extern Datum toast_save_datum(Relation rel, Datum value, - struct varlena *oldexternal, int options); + varlena *oldexternal, uint32 options); extern int toast_open_indexes(Relation toastrel, LOCKMODE lock, diff --git a/src/include/access/transam.h b/src/include/access/transam.h index 7d82cd2eb5621..55a4ab26b3488 100644 --- a/src/include/access/transam.h +++ b/src/include/access/transam.h @@ -4,7 +4,7 @@ * postgres transaction access method support code * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/transam.h @@ -255,6 +255,72 @@ typedef struct TransamVariablesData } TransamVariablesData; + +/* + * TransactionIdPrecedes --- is id1 logically < id2? + */ +static inline bool +TransactionIdPrecedes(TransactionId id1, TransactionId id2) +{ + /* + * If either ID is a permanent XID then we can just do unsigned + * comparison. If both are normal, do a modulo-2^32 comparison. + */ + int32 diff; + + if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) + return (id1 < id2); + + diff = (int32) (id1 - id2); + return (diff < 0); +} + +/* + * TransactionIdPrecedesOrEquals --- is id1 logically <= id2? + */ +static inline bool +TransactionIdPrecedesOrEquals(TransactionId id1, TransactionId id2) +{ + int32 diff; + + if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) + return (id1 <= id2); + + diff = (int32) (id1 - id2); + return (diff <= 0); +} + +/* + * TransactionIdFollows --- is id1 logically > id2? + */ +static inline bool +TransactionIdFollows(TransactionId id1, TransactionId id2) +{ + int32 diff; + + if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) + return (id1 > id2); + + diff = (int32) (id1 - id2); + return (diff > 0); +} + +/* + * TransactionIdFollowsOrEquals --- is id1 logically >= id2? + */ +static inline bool +TransactionIdFollowsOrEquals(TransactionId id1, TransactionId id2) +{ + int32 diff; + + if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2)) + return (id1 >= id2); + + diff = (int32) (id1 - id2); + return (diff >= 0); +} + + /* ---------------- * extern declarations * ---------------- @@ -274,17 +340,11 @@ extern bool TransactionIdDidAbort(TransactionId transactionId); extern void TransactionIdCommitTree(TransactionId xid, int nxids, TransactionId *xids); extern void TransactionIdAsyncCommitTree(TransactionId xid, int nxids, TransactionId *xids, XLogRecPtr lsn); extern void TransactionIdAbortTree(TransactionId xid, int nxids, TransactionId *xids); -extern bool TransactionIdPrecedes(TransactionId id1, TransactionId id2); -extern bool TransactionIdPrecedesOrEquals(TransactionId id1, TransactionId id2); -extern bool TransactionIdFollows(TransactionId id1, TransactionId id2); -extern bool TransactionIdFollowsOrEquals(TransactionId id1, TransactionId id2); extern TransactionId TransactionIdLatest(TransactionId mainxid, int nxids, const TransactionId *xids); extern XLogRecPtr TransactionIdGetCommitLSN(TransactionId xid); /* in transam/varsup.c */ -extern Size VarsupShmemSize(void); -extern void VarsupShmemInit(void); extern FullTransactionId GetNewTransactionId(bool isSubXact); extern void AdvanceNextFullTransactionIdPastXid(TransactionId xid); extern FullTransactionId ReadNextFullTransactionId(void); diff --git a/src/include/access/tsmapi.h b/src/include/access/tsmapi.h index 94d230bf8edf2..e7f95905bf3cc 100644 --- a/src/include/access/tsmapi.h +++ b/src/include/access/tsmapi.h @@ -3,7 +3,7 @@ * tsmapi.h * API for tablesample methods * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * src/include/access/tsmapi.h * diff --git a/src/include/access/tupconvert.h b/src/include/access/tupconvert.h index 2ab3936f22ea8..ef013c811f50c 100644 --- a/src/include/access/tupconvert.h +++ b/src/include/access/tupconvert.h @@ -4,7 +4,7 @@ * Tuple conversion support. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/tupconvert.h diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h index a25b94ba42333..d26287271e937 100644 --- a/src/include/access/tupdesc.h +++ b/src/include/access/tupdesc.h @@ -4,7 +4,7 @@ * POSTGRES tuple descriptor definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/tupdesc.h @@ -55,7 +55,7 @@ typedef struct TupleConstr * directly after the FormData_pg_attribute struct is populated or * altered in any way. * - * Currently, this struct is 16 bytes. Any code changes which enlarge this + * Currently, this struct is 8 bytes. Any code changes which enlarge this * struct should be considered very carefully. * * Code which must access a TupleDesc's attribute data should always make use @@ -67,17 +67,17 @@ typedef struct TupleConstr */ typedef struct CompactAttribute { - int32 attcacheoff; /* fixed offset into tuple, if known, or -1 */ + int16 attcacheoff; /* fixed offset into tuple, if known, or -1 */ int16 attlen; /* attr len in bytes or -1 = varlen, -2 = * cstring */ bool attbyval; /* as FormData_pg_attribute.attbyval */ - bool attispackable; /* FormData_pg_attribute.attstorage != - * TYPSTORAGE_PLAIN */ - bool atthasmissing; /* as FormData_pg_attribute.atthasmissing */ - bool attisdropped; /* as FormData_pg_attribute.attisdropped */ - bool attgenerated; /* FormData_pg_attribute.attgenerated != '\0' */ - char attnullability; /* status of not-null constraint, see below */ uint8 attalignby; /* alignment requirement in bytes */ + bool attispackable:1; /* FormData_pg_attribute.attstorage != + * TYPSTORAGE_PLAIN */ + bool atthasmissing:1; /* as FormData_pg_attribute.atthasmissing */ + bool attisdropped:1; /* as FormData_pg_attribute.attisdropped */ + bool attgenerated:1; /* FormData_pg_attribute.attgenerated != '\0' */ + char attnullability; /* status of not-null constraint, see below */ } CompactAttribute; /* Valid values for CompactAttribute->attnullability */ @@ -131,6 +131,19 @@ typedef struct CompactAttribute * Any code making changes manually to and fields in the FormData_pg_attribute * array must subsequently call populate_compact_attribute() to flush the * changes out to the corresponding 'compact_attrs' element. + * + * firstNonCachedOffsetAttr stores the index into the compact_attrs array for + * the first attribute that we don't have a known attcacheoff for. + * + * firstNonGuaranteedAttr stores the index to into the compact_attrs array for + * the first attribute that is either NULLable, missing, or !attbyval. This + * can be used in locations as a guarantee that attributes before this will + * always exist in tuples. The !attbyval part isn't required for this, but + * including this allows various tuple deforming routines to forego any checks + * for !attbyval. + * + * Once a TupleDesc has been populated, before it is used for any purpose, + * TupleDescFinalize() must be called on it. */ typedef struct TupleDescData { @@ -138,6 +151,11 @@ typedef struct TupleDescData Oid tdtypeid; /* composite type ID for tuple type */ int32 tdtypmod; /* typmod for tuple type */ int tdrefcount; /* reference count, or -1 if not counting */ + int firstNonCachedOffsetAttr; /* index of first compact_attrs + * element without an attcacheoff */ + int firstNonGuaranteedAttr; /* index of the first nullable, + * missing, dropped, or !attbyval + * compact_attrs element. */ TupleConstr *constr; /* constraints, or NULL if none */ /* compact_attrs[N] is the compact metadata of Attribute Number N+1 */ CompactAttribute compact_attrs[FLEXIBLE_ARRAY_MEMBER]; @@ -161,6 +179,8 @@ TupleDescAttr(TupleDesc tupdesc, int i) { FormData_pg_attribute *attrs = TupleDescAttrAddress(tupdesc); + Assert(i >= 0 && i < tupdesc->natts); + return &attrs[i]; } @@ -205,6 +225,7 @@ extern void TupleDescCopy(TupleDesc dst, TupleDesc src); extern void TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno, TupleDesc src, AttrNumber srcAttno); +extern void TupleDescFinalize(TupleDesc tupdesc); extern void FreeTupleDesc(TupleDesc tupdesc); extern void IncrTupleDescRefCount(TupleDesc tupdesc); diff --git a/src/include/access/tupdesc_details.h b/src/include/access/tupdesc_details.h index 8c0e1a12ece9d..317e1dbe6a067 100644 --- a/src/include/access/tupdesc_details.h +++ b/src/include/access/tupdesc_details.h @@ -4,7 +4,7 @@ * POSTGRES tuple descriptor definitions we can't include everywhere * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/tupdesc_details.h diff --git a/src/include/access/tupmacs.h b/src/include/access/tupmacs.h index 6240ec930e7a9..fa76d0f2eaca1 100644 --- a/src/include/access/tupmacs.h +++ b/src/include/access/tupmacs.h @@ -4,7 +4,7 @@ * Tuple macros used by both index tuples and heap tuples. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/tupmacs.h @@ -15,7 +15,9 @@ #define TUPMACS_H #include "catalog/pg_type_d.h" /* for TYPALIGN macros */ - +#include "port/pg_bitutils.h" +#include "port/pg_bswap.h" +#include "varatt.h" /* * Check a tuple's null bitmap to determine whether the attribute is null. @@ -23,11 +25,67 @@ * non-null. */ static inline bool -att_isnull(int ATT, const bits8 *BITS) +att_isnull(int ATT, const uint8 *BITS) { return !(BITS[ATT >> 3] & (1 << (ATT & 0x07))); } +/* + * populate_isnull_array + * Transform a tuple's null bitmap into a boolean array. + * + * Caller must ensure that the isnull array is sized so it contains + * at least as many elements as there are bits in the 'bits' array. + * Callers should be aware that isnull is populated 8 elements at a time, + * effectively as if natts is rounded up to the next multiple of 8. + */ +static inline void +populate_isnull_array(const uint8 *bits, int natts, bool *isnull) +{ + int nbytes = (natts + 7) >> 3; + + /* + * Multiplying the inverted NULL bitmap byte by this value results in the + * lowest bit in each byte being set the same as each bit of the inverted + * byte. We perform this as 2 32-bit operations rather than a single + * 64-bit operation as multiplying by the required value to do this in + * 64-bits would result in overflowing a uint64 in some cases. + * + * XXX if we ever require BMI2 (-march=x86-64-v3), then this could be done + * more efficiently on most X86-64 CPUs with the PDEP instruction. Beware + * that some chips (e.g. AMD's Zen2) are horribly inefficient at PDEP. + */ +#define SPREAD_BITS_MULTIPLIER_32 0x204081U + + for (int i = 0; i < nbytes; i++, isnull += 8) + { + uint64 isnull_8; + uint8 nullbyte = ~bits[i]; + + /* Convert the lower 4 bits of NULL bitmap word into a 64 bit int */ + isnull_8 = (nullbyte & 0xf) * SPREAD_BITS_MULTIPLIER_32; + + /* + * Convert the upper 4 bits of NULL bitmap word into a 64 bit int, + * shift into the upper 32 bit and bitwise-OR with the result of the + * lower 4 bits. + */ + isnull_8 |= ((uint64) ((nullbyte >> 4) * SPREAD_BITS_MULTIPLIER_32)) << 32; + + /* Mask out all other bits apart from the lowest bit of each byte. */ + isnull_8 &= UINT64CONST(0x0101010101010101); + +#ifdef WORDS_BIGENDIAN + + /* + * Fix byte order on big-endian machines before copying to the array. + */ + isnull_8 = pg_bswap64(isnull_8); +#endif + memcpy(isnull, &isnull_8, sizeof(uint64)); + } +} + #ifndef FRONTEND /* * Given an attbyval and an attlen from either a Form_pg_attribute or @@ -39,9 +97,6 @@ att_isnull(int ATT, const bits8 *BITS) * return the correct number of bytes fetched from the data area and extended * to Datum form. * - * On machines where Datum is 8 bytes, we support fetching 8-byte byval - * attributes; otherwise, only 1, 2, and 4-byte values are supported. - * * Note that T must already be properly aligned for this to work correctly. */ #define fetchatt(A,T) fetch_att(T, (A)->attbyval, (A)->attlen) @@ -62,10 +117,8 @@ fetch_att(const void *T, bool attbyval, int attlen) return Int16GetDatum(*((const int16 *) T)); case sizeof(int32): return Int32GetDatum(*((const int32 *) T)); -#if SIZEOF_DATUM == 8 - case sizeof(Datum): - return *((const Datum *) T); -#endif + case sizeof(int64): + return Int64GetDatum(*((const int64 *) T)); default: elog(ERROR, "unsupported byval length: %d", attlen); return 0; @@ -74,8 +127,209 @@ fetch_att(const void *T, bool attbyval, int attlen) else return PointerGetDatum(T); } + +/* + * Same as fetch_att, but no error checking for invalid attlens for byval + * types. This is safe to use when attlen comes from CompactAttribute as we + * validate the length when populating that struct. + */ +static inline Datum +fetch_att_noerr(const void *T, bool attbyval, int attlen) +{ + if (attbyval) + { + switch (attlen) + { + case sizeof(int32): + return Int32GetDatum(*((const int32 *) T)); + case sizeof(int16): + return Int16GetDatum(*((const int16 *) T)); + case sizeof(char): + return CharGetDatum(*((const char *) T)); + default: + Assert(attlen == sizeof(int64)); + return Int64GetDatum(*((const int64 *) T)); + } + } + else + return PointerGetDatum(T); +} + + +/* + * align_fetch_then_add + * Applies all the functionality of att_pointer_alignby(), + * fetch_att_noerr() and att_addlength_pointer(), resulting in the *off + * pointer to the perhaps unaligned number of bytes into 'tupptr', ready + * to deform the next attribute. + * + * tupptr: pointer to the beginning of the tuple, after the header and any + * NULL bitmask. + * off: offset in bytes for reading tuple data, possibly unaligned. + * attbyval, attlen and attalignby are values from CompactAttribute. + */ +static inline Datum +align_fetch_then_add(const char *tupptr, uint32 *off, bool attbyval, int attlen, + uint8 attalignby) +{ + Datum res; + + if (attlen > 0) + { + const char *offset_ptr; + + *off = TYPEALIGN(attalignby, *off); + offset_ptr = tupptr + *off; + *off += attlen; + if (attbyval) + { + switch (attlen) + { + case sizeof(char): + return CharGetDatum(*((const char *) offset_ptr)); + case sizeof(int16): + return Int16GetDatum(*((const int16 *) offset_ptr)); + case sizeof(int32): + return Int32GetDatum(*((const int32 *) offset_ptr)); + default: + + /* + * populate_compact_attribute_internal() should have + * checked + */ + Assert(attlen == sizeof(int64)); + return Int64GetDatum(*((const int64 *) offset_ptr)); + } + } + return PointerGetDatum(offset_ptr); + } + else if (attlen == -1) + { + if (!VARATT_IS_SHORT(tupptr + *off)) + *off = TYPEALIGN(attalignby, *off); + + res = PointerGetDatum(tupptr + *off); + *off += VARSIZE_ANY(DatumGetPointer(res)); + return res; + } + else + { + Assert(attlen == -2); + *off = TYPEALIGN(attalignby, *off); + res = PointerGetDatum(tupptr + *off); + *off += strlen(tupptr + *off) + 1; + return res; + } +} + +/* + * first_null_attr + * Inspect a NULL bitmap from a tuple and return the 0-based attnum of the + * first NULL attribute. Returns natts if no NULLs were found. + * + * This is coded to expect that 'bits' contains at least one 0 bit somewhere + * in the array, but not necessarily < natts. Note that natts may be passed + * as a value lower than the number of bits physically stored in the tuple's + * NULL bitmap, in which case we may not find a NULL and return natts. + * + * The reason we require at least one 0 bit somewhere in the NULL bitmap is + * that the for loop that checks 0xFF bytes would loop to the last byte in + * the array if all bytes were 0xFF, and the subsequent code that finds the + * right-most 0 bit would access the first byte beyond the bitmap. Provided + * we find a 0 bit before then, that won't happen. Since tuples which have no + * NULLs don't have a NULL bitmap, this function won't get called for that + * case. + */ +static inline int +first_null_attr(const uint8 *bits, int natts) +{ + int nattByte = natts >> 3; + int bytenum; + int res; + +#ifdef USE_ASSERT_CHECKING + int firstnull_check = natts; + + /* Do it the slow way and check we get the same answer. */ + for (int i = 0; i < natts; i++) + { + if (att_isnull(i, bits)) + { + firstnull_check = i; + break; + } + } +#endif + + /* Process all bytes up to just before the byte for the natts attribute */ + for (bytenum = 0; bytenum < nattByte; bytenum++) + { + /* break if there's any NULL attrs (a 0 bit) */ + if (bits[bytenum] != 0xFF) + break; + } + + /* + * Look for the highest 0-bit in the 'bytenum' element. To do this, we + * promote the uint8 to uint32 before performing the bitwise NOT and + * looking for the first 1-bit. This works even when the byte is 0xFF, as + * the bitwise NOT of 0xFF in 32 bits is 0xFFFFFF00, in which case + * pg_rightmost_one_pos32() will return 8. We may end up with a value + * higher than natts here, but we'll fix that with the Min() below. + */ + res = bytenum << 3; + res += pg_rightmost_one_pos32(~((uint32) bits[bytenum])); + + /* + * Since we did no masking to mask out bits beyond the natts'th bit, we + * may have found a bit higher than natts, so we must cap res to natts + */ + res = Min(res, natts); + + /* Ensure we got the same answer as the att_isnull() loop got */ + Assert(res == firstnull_check); + + return res; +} #endif /* FRONTEND */ +/* + * typalign_to_alignby: map a TYPALIGN_xxx value to the numeric alignment + * value it represents. (We store TYPALIGN_xxx codes not the real alignment + * values mainly so that initial catalog contents can be machine-independent.) + */ +static inline uint8 +typalign_to_alignby(char typalign) +{ + uint8 alignby; + + switch (typalign) + { + case TYPALIGN_CHAR: + alignby = sizeof(char); + break; + case TYPALIGN_SHORT: + alignby = ALIGNOF_SHORT; + break; + case TYPALIGN_INT: + alignby = ALIGNOF_INT; + break; + case TYPALIGN_DOUBLE: + alignby = ALIGNOF_DOUBLE; + break; + default: +#ifndef FRONTEND + elog(ERROR, "invalid typalign value: %c", typalign); +#else + fprintf(stderr, "invalid typalign value: %c\n", typalign); + exit(1); +#endif + alignby = 0; + break; + } + return alignby; +} + /* * att_align_datum aligns the given offset as needed for a datum of alignment * requirement attalign and typlen attlen. attdatum is the Datum variable @@ -144,19 +398,11 @@ fetch_att(const void *T, bool attbyval, int attlen) * * within arrays and multiranges, we unconditionally align varlenas (XXX this * should be revisited, probably). * - * The attalign cases are tested in what is hopefully something like their - * frequency of occurrence. + * In performance-critical loops, avoid using this macro; instead use + * att_nominal_alignby with a pre-computed alignby value. */ #define att_align_nominal(cur_offset, attalign) \ -( \ - ((attalign) == TYPALIGN_INT) ? INTALIGN(cur_offset) : \ - (((attalign) == TYPALIGN_CHAR) ? (uintptr_t) (cur_offset) : \ - (((attalign) == TYPALIGN_DOUBLE) ? DOUBLEALIGN(cur_offset) : \ - ( \ - AssertMacro((attalign) == TYPALIGN_SHORT), \ - SHORTALIGN(cur_offset) \ - ))) \ -) + att_nominal_alignby(cur_offset, typalign_to_alignby(attalign)) /* * Similar to att_align_nominal, but accepts a number of bytes, typically from @@ -195,7 +441,7 @@ fetch_att(const void *T, bool attbyval, int attlen) : \ ( \ AssertMacro((attlen) == -2), \ - (cur_offset) + (strlen((char *) (attptr)) + 1) \ + (cur_offset) + (strlen((const char *) (attptr)) + 1) \ )) \ ) @@ -221,11 +467,9 @@ store_att_byval(void *T, Datum newdatum, int attlen) case sizeof(int32): *(int32 *) T = DatumGetInt32(newdatum); break; -#if SIZEOF_DATUM == 8 - case sizeof(Datum): - *(Datum *) T = newdatum; + case sizeof(int64): + *(int64 *) T = DatumGetInt64(newdatum); break; -#endif default: elog(ERROR, "unsupported byval length: %d", attlen); } diff --git a/src/include/access/twophase.h b/src/include/access/twophase.h index 9fa8235503375..1d2ff42c9b72f 100644 --- a/src/include/access/twophase.h +++ b/src/include/access/twophase.h @@ -4,7 +4,7 @@ * Two-phase-commit related declarations. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/twophase.h @@ -17,7 +17,12 @@ #include "access/xact.h" #include "access/xlogdefs.h" #include "datatype/timestamp.h" -#include "storage/lock.h" + +/* + * forward references in this file + */ +typedef struct PGPROC PGPROC; +typedef struct VirtualTransactionId VirtualTransactionId; /* * GlobalTransactionData is defined in twophase.c; other places have no @@ -28,18 +33,15 @@ typedef struct GlobalTransactionData *GlobalTransaction; /* GUC variable */ extern PGDLLIMPORT int max_prepared_xacts; -extern Size TwoPhaseShmemSize(void); -extern void TwoPhaseShmemInit(void); - extern void AtAbort_Twophase(void); extern void PostPrepare_Twophase(void); extern TransactionId TwoPhaseGetXidByVirtualXID(VirtualTransactionId vxid, bool *have_more); -extern PGPROC *TwoPhaseGetDummyProc(TransactionId xid, bool lock_held); -extern int TwoPhaseGetDummyProcNumber(TransactionId xid, bool lock_held); +extern PGPROC *TwoPhaseGetDummyProc(FullTransactionId fxid, bool lock_held); +extern int TwoPhaseGetDummyProcNumber(FullTransactionId fxid, bool lock_held); -extern GlobalTransaction MarkAsPreparing(TransactionId xid, const char *gid, +extern GlobalTransaction MarkAsPreparing(FullTransactionId fxid, const char *gid, TimestampTz prepared_at, Oid owner, Oid databaseid); @@ -56,8 +58,9 @@ extern void CheckPointTwoPhase(XLogRecPtr redo_horizon); extern void FinishPreparedTransaction(const char *gid, bool isCommit); -extern void PrepareRedoAdd(char *buf, XLogRecPtr start_lsn, - XLogRecPtr end_lsn, RepOriginId origin_id); +extern void PrepareRedoAdd(FullTransactionId fxid, char *buf, + XLogRecPtr start_lsn, XLogRecPtr end_lsn, + ReplOriginId origin_id); extern void PrepareRedoRemove(TransactionId xid, bool giveWarning); extern void restoreTwoPhaseData(void); extern bool LookupGXact(const char *gid, XLogRecPtr prepare_end_lsn, @@ -67,4 +70,6 @@ extern void TwoPhaseTransactionGid(Oid subid, TransactionId xid, char *gid_res, int szgid); extern bool LookupGXactBySubid(Oid subid); +extern TransactionId TwoPhaseGetOldestXidInCommit(void); + #endif /* TWOPHASE_H */ diff --git a/src/include/access/twophase_rmgr.h b/src/include/access/twophase_rmgr.h index 3ed154bb23127..8927f369c39b5 100644 --- a/src/include/access/twophase_rmgr.h +++ b/src/include/access/twophase_rmgr.h @@ -4,7 +4,7 @@ * Two-phase-commit resource managers definition * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/twophase_rmgr.h @@ -14,7 +14,9 @@ #ifndef TWOPHASE_RMGR_H #define TWOPHASE_RMGR_H -typedef void (*TwoPhaseCallback) (TransactionId xid, uint16 info, +#include "access/transam.h" + +typedef void (*TwoPhaseCallback) (FullTransactionId fxid, uint16 info, void *recdata, uint32 len); typedef uint8 TwoPhaseRmgrId; diff --git a/src/include/access/valid.h b/src/include/access/valid.h index 8b33089dac4e3..4206ddf5707ad 100644 --- a/src/include/access/valid.h +++ b/src/include/access/valid.h @@ -4,7 +4,7 @@ * POSTGRES tuple qualification validity definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/valid.h diff --git a/src/include/access/visibilitymap.h b/src/include/access/visibilitymap.h index be21c6dd1a306..e4e0cfa989e14 100644 --- a/src/include/access/visibilitymap.h +++ b/src/include/access/visibilitymap.h @@ -4,7 +4,7 @@ * visibility map interface * * - * Portions Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2007-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/visibilitymap.h @@ -15,9 +15,9 @@ #define VISIBILITYMAP_H #include "access/visibilitymapdefs.h" -#include "access/xlogdefs.h" #include "storage/block.h" #include "storage/buf.h" +#include "storage/relfilelocator.h" #include "utils/relcache.h" /* Macros for visibilitymap test */ @@ -31,15 +31,13 @@ extern bool visibilitymap_clear(Relation rel, BlockNumber heapBlk, extern void visibilitymap_pin(Relation rel, BlockNumber heapBlk, Buffer *vmbuf); extern bool visibilitymap_pin_ok(BlockNumber heapBlk, Buffer vmbuf); -extern uint8 visibilitymap_set(Relation rel, - BlockNumber heapBlk, Buffer heapBuf, - XLogRecPtr recptr, - Buffer vmBuf, - TransactionId cutoff_xid, - uint8 flags); +extern void visibilitymap_set(BlockNumber heapBlk, + Buffer vmBuf, uint8 flags, + const RelFileLocator rlocator); extern uint8 visibilitymap_get_status(Relation rel, BlockNumber heapBlk, Buffer *vmbuf); extern void visibilitymap_count(Relation rel, BlockNumber *all_visible, BlockNumber *all_frozen); extern BlockNumber visibilitymap_prepare_truncate(Relation rel, BlockNumber nheapblocks); +extern BlockNumber visibilitymap_truncation_length(BlockNumber nheapblocks); #endif /* VISIBILITYMAP_H */ diff --git a/src/include/access/visibilitymapdefs.h b/src/include/access/visibilitymapdefs.h index 5ad5c0208779c..e5794c8559ed7 100644 --- a/src/include/access/visibilitymapdefs.h +++ b/src/include/access/visibilitymapdefs.h @@ -4,7 +4,7 @@ * macros for accessing contents of visibility map pages * * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * * src/include/access/visibilitymapdefs.h * @@ -21,14 +21,5 @@ #define VISIBILITYMAP_ALL_FROZEN 0x02 #define VISIBILITYMAP_VALID_BITS 0x03 /* OR of all valid visibilitymap * flags bits */ -/* - * To detect recovery conflicts during logical decoding on a standby, we need - * to know if a table is a user catalog table. For that we add an additional - * bit into xl_heap_visible.flags, in addition to the above. - * - * NB: VISIBILITYMAP_XLOG_* may not be passed to visibilitymap_set(). - */ -#define VISIBILITYMAP_XLOG_CATALOG_REL 0x04 -#define VISIBILITYMAP_XLOG_VALID_BITS (VISIBILITYMAP_VALID_BITS | VISIBILITYMAP_XLOG_CATALOG_REL) #endif /* VISIBILITYMAPDEFS_H */ diff --git a/src/include/access/xact.h b/src/include/access/xact.h index b2bc10ee04196..a8cbdf247c866 100644 --- a/src/include/access/xact.h +++ b/src/include/access/xact.h @@ -4,7 +4,7 @@ * postgres transaction system definitions * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xact.h @@ -43,10 +43,11 @@ extern PGDLLIMPORT int XactIsoLevel; /* * We implement three isolation levels internally. - * The two stronger ones use one snapshot per database transaction; - * the others use one snapshot per statement. - * Serializable uses predicate locks in addition to snapshots. - * These macros should be used to check which isolation level is selected. + * The weakest uses one snapshot per statement; + * the two stronger levels use one snapshot per database transaction. + * Serializable uses predicate locks in addition to the snapshot. + * These macros can be used to determine which implementation to use + * depending on the prevailing serialization level. */ #define IsolationUsesXactSnapshot() (XactIsoLevel >= XACT_REPEATABLE_READ) #define IsolationIsSerializable() (XactIsoLevel == XACT_SERIALIZABLE) @@ -458,6 +459,7 @@ extern TimestampTz GetCurrentTransactionStopTimestamp(void); extern void SetCurrentStatementStartTimestamp(void); extern int GetCurrentTransactionNestLevel(void); extern bool TransactionIdIsCurrentTransactionId(TransactionId xid); +extern int GetTopReadOnlyTransactionNestLevel(void); extern void CommandCounterIncrement(void); extern void ForceSyncCommit(void); extern void StartTransactionCommand(void); diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h index d313099c027f0..437b4f32349ee 100644 --- a/src/include/access/xlog.h +++ b/src/include/access/xlog.h @@ -3,7 +3,7 @@ * * PostgreSQL write-ahead log manager * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xlog.h @@ -13,6 +13,7 @@ #include "access/xlogbackup.h" #include "access/xlogdefs.h" +#include "replication/logicalctl.h" #include "datatype/timestamp.h" #include "lib/stringinfo.h" #include "nodes/pg_list.h" @@ -56,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay; extern PGDLLIMPORT int CommitSiblings; extern PGDLLIMPORT bool track_wal_io_timing; extern PGDLLIMPORT int wal_decode_buffer_size; +extern PGDLLIMPORT int data_checksums; extern PGDLLIMPORT int CheckPointSegments; @@ -94,6 +96,7 @@ typedef enum RecoveryState } RecoveryState; extern PGDLLIMPORT int wal_level; +extern PGDLLIMPORT bool XLogLogicalInfo; /* Is WAL archiving enabled (always or only while server is running normally)? */ #define XLogArchivingActive() \ @@ -117,13 +120,22 @@ extern PGDLLIMPORT int wal_level; * of the bits make it to disk, but the checksum wouldn't match. Also WAL-log * them if forced by wal_log_hints=on. */ -#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints) +#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite()) /* Do we need to WAL-log information required only for Hot Standby and logical replication? */ #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA) -/* Do we need to WAL-log information required only for logical replication? */ -#define XLogLogicalInfoActive() (wal_level >= WAL_LEVEL_LOGICAL) +/* + * Do we need to WAL-log information required only for logical replication? + * + * When XLogLogicalInfoActive() returns true, it enables logical-decoding-related + * WAL logging as if wal_level were set to 'logical', even if it's actually set + * to 'replica'. Note that XLogLogicalInfo is a process-local cache and can + * change until an XID is assigned to the transaction. In other words, it + * ensures that the same result is returned within an XID-assigned transaction. + */ +#define XLogLogicalInfoActive() \ + (wal_level >= WAL_LEVEL_LOGICAL || XLogLogicalInfo) #ifdef WAL_DEBUG extern PGDLLIMPORT bool XLOG_DEBUG; @@ -139,10 +151,9 @@ extern PGDLLIMPORT bool XLOG_DEBUG; #define CHECKPOINT_IS_SHUTDOWN 0x0001 /* Checkpoint is for shutdown */ #define CHECKPOINT_END_OF_RECOVERY 0x0002 /* Like shutdown checkpoint, but * issued at end of WAL recovery */ -#define CHECKPOINT_IMMEDIATE 0x0004 /* Do it without delays */ +#define CHECKPOINT_FAST 0x0004 /* Do it without delays */ #define CHECKPOINT_FORCE 0x0008 /* Force even if no activity */ -#define CHECKPOINT_FLUSH_ALL 0x0010 /* Flush all pages, including those - * belonging to unlogged tables */ +#define CHECKPOINT_FLUSH_UNLOGGED 0x0010 /* Flush unlogged tables */ /* These are important to RequestCheckpoint */ #define CHECKPOINT_WAIT 0x0020 /* Wait for completion */ #define CHECKPOINT_REQUESTED 0x0040 /* Checkpoint request has been made */ @@ -203,6 +214,7 @@ extern XLogRecPtr XLogInsertRecord(struct XLogRecData *rdata, XLogRecPtr fpw_lsn, uint8 flags, int num_fpi, + uint64 fpi_bytes, bool topxid_included); extern void XLogFlush(XLogRecPtr record); extern bool XLogBackgroundFlush(void); @@ -215,10 +227,14 @@ extern XLogSegNo XLogGetLastRemovedSegno(void); extern XLogSegNo XLogGetOldestSegno(TimeLineID tli); extern void XLogSetAsyncXactLSN(XLogRecPtr asyncXactLSN); extern void XLogSetReplicationSlotMinimumLSN(XLogRecPtr lsn); +extern XLogRecPtr XLogGetReplicationSlotMinimumLSN(void); extern void xlog_redo(struct XLogReaderState *record); +extern void xlog2_redo(struct XLogReaderState *record); extern void xlog_desc(StringInfo buf, struct XLogReaderState *record); +extern void xlog2_desc(StringInfo buf, struct XLogReaderState *record); extern const char *xlog_identify(uint8 info); +extern const char *xlog2_identify(uint8 info); extern void issue_xlog_fsync(int fd, XLogSegNo segno, TimeLineID tli); @@ -226,15 +242,23 @@ extern bool RecoveryInProgress(void); extern RecoveryState GetRecoveryState(void); extern bool XLogInsertAllowed(void); extern XLogRecPtr GetXLogInsertRecPtr(void); +extern XLogRecPtr GetXLogInsertEndRecPtr(void); extern XLogRecPtr GetXLogWriteRecPtr(void); extern uint64 GetSystemIdentifier(void); extern char *GetMockAuthenticationNonce(void); -extern bool DataChecksumsEnabled(void); +extern bool DataChecksumsNeedWrite(void); +extern bool DataChecksumsNeedVerify(void); +extern bool DataChecksumsInProgressOn(void); +extern void SetDataChecksumsOnInProgress(void); +extern void SetDataChecksumsOn(void); +extern void SetDataChecksumsOff(void); +extern const char *show_data_checksums(void); +extern const char *get_checksum_state_string(uint32 state); +extern void InitLocalDataChecksumState(void); +extern void SetLocalDataChecksumState(uint32 data_checksum_version); extern bool GetDefaultCharSignedness(void); extern XLogRecPtr GetFakeLSNForUnloggedRel(void); -extern Size XLOGShmemSize(void); -extern void XLOGShmemInit(void); extern void BootStrapXLOG(uint32 data_checksum_version); extern void InitializeWalConsistencyChecking(void); extern void LocalProcessControlFile(bool reset); @@ -246,6 +270,7 @@ extern bool CreateRestartPoint(int flags); extern WALAvailability GetWALAvailability(XLogRecPtr targetLSN); extern void XLogPutNextOid(Oid nextOid); extern XLogRecPtr XLogRestorePoint(const char *rpName); +extern XLogRecPtr XLogAssignLSN(void); extern void UpdateFullPageWrites(void); extern void GetFullPageWriteInfo(XLogRecPtr *RedoRecPtr_p, bool *doPageWrites_p); extern XLogRecPtr GetRedoRecPtr(void); @@ -257,6 +282,8 @@ extern XLogRecPtr GetLastImportantRecPtr(void); extern void SetWalWriterSleeping(bool sleeping); +extern void WakeupCheckpointer(void); + extern Size WALReadFromBuffers(char *dstbuf, XLogRecPtr startptr, Size count, TimeLineID tli); @@ -269,6 +296,7 @@ extern void SwitchIntoArchiveRecovery(XLogRecPtr EndRecPtr, TimeLineID replayTLI extern void ReachedEndOfBackup(XLogRecPtr EndRecPtr, TimeLineID tli); extern void SetInstallXLogFileSegmentActive(void); extern bool IsInstallXLogFileSegmentActive(void); +extern void ResetInstallXLogFileSegmentActive(void); extern void XLogShutdownWalRcv(void); /* diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h index 2cf8d55d706d1..13ae3ad4fbbb3 100644 --- a/src/include/access/xlog_internal.h +++ b/src/include/access/xlog_internal.h @@ -11,7 +11,7 @@ * Note: This file must be includable in both frontend and backend contexts, * to allow stand-alone tools like pg_receivewal to deal with WAL files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xlog_internal.h @@ -25,13 +25,14 @@ #include "lib/stringinfo.h" #include "pgtime.h" #include "storage/block.h" +#include "storage/checksum.h" #include "storage/relfilelocator.h" /* * Each page of XLOG file has a header like this: */ -#define XLOG_PAGE_MAGIC 0xD118 /* can be used as WAL version indicator */ +#define XLOG_PAGE_MAGIC 0xD11F /* can be used as WAL version indicator */ typedef struct XLogPageHeaderData { @@ -74,12 +75,10 @@ typedef XLogLongPageHeaderData *XLogLongPageHeader; #define XLP_FIRST_IS_CONTRECORD 0x0001 /* This flag indicates a "long" page header */ #define XLP_LONG_HEADER 0x0002 -/* This flag indicates backup blocks starting in this page are optional */ -#define XLP_BKP_REMOVABLE 0x0004 /* Replaces a missing contrecord; see CreateOverwriteContrecordRecord */ -#define XLP_FIRST_IS_OVERWRITE_CONTRECORD 0x0008 +#define XLP_FIRST_IS_OVERWRITE_CONTRECORD 0x0004 /* All defined flag bits in xlp_info (used for validity checking of header) */ -#define XLP_ALL_FLAGS 0x000F +#define XLP_ALL_FLAGS 0x0007 #define XLogPageHeaderSize(hdr) \ (((hdr)->xlp_info & XLP_LONG_HEADER) ? SizeOfXLogLongPHD : SizeOfXLogShortPHD) @@ -289,6 +288,12 @@ typedef struct xl_restore_point char rp_name[MAXFNAMELEN]; } xl_restore_point; +/* Information logged when data checksum level is changed */ +typedef struct xl_checksum_state +{ + ChecksumStateType new_checksum_state; +} xl_checksum_state; + /* Overwrite of prior contrecord */ typedef struct xl_overwrite_contrecord { @@ -305,6 +310,13 @@ typedef struct xl_end_of_recovery int wal_level; } xl_end_of_recovery; +/* checkpoint redo */ +typedef struct xl_checkpoint_redo +{ + int wal_level; + uint32 data_checksum_version; +} xl_checkpoint_redo; + /* * The functions in xloginsert.c construct a chain of XLogRecData structs * to represent the final WAL record. @@ -316,16 +328,6 @@ typedef struct XLogRecData uint32 len; /* length of rmgr data to include */ } XLogRecData; -/* - * Recovery target action. - */ -typedef enum -{ - RECOVERY_TARGET_ACTION_PAUSE, - RECOVERY_TARGET_ACTION_PROMOTE, - RECOVERY_TARGET_ACTION_SHUTDOWN, -} RecoveryTargetAction; - struct LogicalDecodingContext; struct XLogRecordBuffer; diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h index 6ed47e99f12fb..c8a97372e3522 100644 --- a/src/include/access/xlogarchive.h +++ b/src/include/access/xlogarchive.h @@ -3,7 +3,7 @@ * xlogarchive.h * Prototypes for WAL archives in the backend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/access/xlogbackup.h b/src/include/access/xlogbackup.h index 6b828ce2cac98..2cc2f85d9f0a8 100644 --- a/src/include/access/xlogbackup.h +++ b/src/include/access/xlogbackup.h @@ -3,7 +3,7 @@ * xlogbackup.h * Definitions for internals of base backups. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/access/xlogdefs.h b/src/include/access/xlogdefs.h index 9e41c9f6e8446..d77b894cb6507 100644 --- a/src/include/access/xlogdefs.h +++ b/src/include/access/xlogdefs.h @@ -4,7 +4,7 @@ * Postgres write-ahead log manager record pointer and * timeline number definitions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xlogdefs.h @@ -25,7 +25,8 @@ typedef uint64 XLogRecPtr; * WAL segment, initializing the first WAL page at WAL segment size, so no XLOG * record can begin at zero. */ -#define InvalidXLogRecPtr 0 +#define InvalidXLogRecPtr 0 +#define XLogRecPtrIsValid(r) ((r) != InvalidXLogRecPtr) #define XLogRecPtrIsInvalid(r) ((r) == InvalidXLogRecPtr) /* @@ -38,9 +39,12 @@ typedef uint64 XLogRecPtr; /* * Handy macro for printing XLogRecPtr in conventional format, e.g., * - * printf("%X/%X", LSN_FORMAT_ARGS(lsn)); + * printf("%X/%08X", LSN_FORMAT_ARGS(lsn)); + * + * To avoid breaking translatable messages, we're directly applying the + * LSN format instead of using a macro. */ -#define LSN_FORMAT_ARGS(lsn) (AssertVariableIsOfTypeMacro((lsn), XLogRecPtr), (uint32) ((lsn) >> 32)), ((uint32) (lsn)) +#define LSN_FORMAT_ARGS(lsn) (StaticAssertVariableIsOfTypeMacro((lsn), XLogRecPtr), (uint32) ((lsn) >> 32)), ((uint32) (lsn)) /* * XLogSegNo - physical log file sequence number. @@ -62,7 +66,7 @@ typedef uint32 TimeLineID; * Replication origin id - this is located in this file to avoid having to * include origin.h in a bunch of xlog related places. */ -typedef uint16 RepOriginId; +typedef uint16 ReplOriginId; /* * This chunk of hackery attempts to determine which file sync methods diff --git a/src/include/access/xloginsert.h b/src/include/access/xloginsert.h index cf057f033a281..91dfbd5627f50 100644 --- a/src/include/access/xloginsert.h +++ b/src/include/access/xloginsert.h @@ -3,7 +3,7 @@ * * Functions for generating WAL records * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xloginsert.h @@ -44,6 +44,7 @@ extern void XLogBeginInsert(void); extern void XLogSetRecordFlags(uint8 flags); extern XLogRecPtr XLogInsert(RmgrId rmid, uint8 info); +extern XLogRecPtr XLogSimpleInsertInt64(RmgrId rmid, uint8 info, int64 value); extern void XLogEnsureRecordSpace(int max_block_id, int ndatas); extern void XLogRegisterData(const void *data, uint32 len); extern void XLogRegisterBuffer(uint8 block_id, Buffer buffer, uint8 flags); @@ -63,6 +64,8 @@ extern void log_newpage_range(Relation rel, ForkNumber forknum, BlockNumber startblk, BlockNumber endblk, bool page_std); extern XLogRecPtr XLogSaveBufferForHint(Buffer buffer, bool buffer_std); +extern XLogRecPtr XLogGetFakeLSN(Relation rel); + extern void InitXLogInsert(void); #endif /* XLOGINSERT_H */ diff --git a/src/include/access/xlogprefetcher.h b/src/include/access/xlogprefetcher.h index 50b39c1fb0d77..56a81676d9231 100644 --- a/src/include/access/xlogprefetcher.h +++ b/src/include/access/xlogprefetcher.h @@ -3,7 +3,7 @@ * xlogprefetcher.h * Declarations for the recovery prefetching module. * - * Portions Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2022-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -34,9 +34,6 @@ typedef struct XLogPrefetcher XLogPrefetcher; extern void XLogPrefetchReconfigure(void); -extern size_t XLogPrefetchShmemSize(void); -extern void XLogPrefetchShmemInit(void); - extern void XLogPrefetchResetStats(void); extern XLogPrefetcher *XLogPrefetcherAllocate(XLogReaderState *reader); diff --git a/src/include/access/xlogreader.h b/src/include/access/xlogreader.h index 9738462d3c9f1..97eae2c1daba8 100644 --- a/src/include/access/xlogreader.h +++ b/src/include/access/xlogreader.h @@ -3,7 +3,7 @@ * xlogreader.h * Definitions for the generic XLog reading facility * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/access/xlogreader.h @@ -145,7 +145,6 @@ typedef struct bool has_data; char *data; uint16 data_len; - uint16 data_bufsz; } DecodedBkpBlock; /* @@ -164,7 +163,7 @@ typedef struct DecodedXLogRecord XLogRecPtr lsn; /* location */ XLogRecPtr next_lsn; /* location of next record */ XLogRecord header; /* header */ - RepOriginId record_origin; + ReplOriginId record_origin; TransactionId toplevel_xid; /* XID of top-level transaction */ char *main_data; /* record's main data portion */ uint32 main_data_len; /* main data portion's length */ @@ -343,7 +342,8 @@ extern void XLogReaderSetDecodeBuffer(XLogReaderState *state, /* Position the XLogReader to given record */ extern void XLogBeginRead(XLogReaderState *state, XLogRecPtr RecPtr); -extern XLogRecPtr XLogFindNextRecord(XLogReaderState *state, XLogRecPtr RecPtr); +extern XLogRecPtr XLogFindNextRecord(XLogReaderState *state, XLogRecPtr RecPtr, + char **errormsg); /* Return values from XLogPageReadCB. */ typedef enum XLogPageReadResult diff --git a/src/include/access/xlogrecord.h b/src/include/access/xlogrecord.h index a06833ce0a31f..e8999d3fe91f7 100644 --- a/src/include/access/xlogrecord.h +++ b/src/include/access/xlogrecord.h @@ -3,7 +3,7 @@ * * Definitions for the WAL record format. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xlogrecord.h diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h index 91446303024ae..ba7750dca0b45 100644 --- a/src/include/access/xlogrecovery.h +++ b/src/include/access/xlogrecovery.h @@ -3,7 +3,7 @@ * * Functions for WAL recovery and standby mode * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xlogrecovery.h @@ -14,6 +14,8 @@ #include "access/xlogreader.h" #include "catalog/pg_control.h" #include "lib/stringinfo.h" +#include "storage/condition_variable.h" +#include "storage/latch.h" #include "utils/timestamp.h" /* @@ -40,6 +42,16 @@ typedef enum RECOVERY_TARGET_TIMELINE_NUMERIC, } RecoveryTargetTimeLineGoal; +/* + * Recovery target action. + */ +typedef enum +{ + RECOVERY_TARGET_ACTION_PAUSE, + RECOVERY_TARGET_ACTION_PROMOTE, + RECOVERY_TARGET_ACTION_SHUTDOWN, +} RecoveryTargetAction; + /* Recovery pause states */ typedef enum RecoveryPauseState { @@ -48,6 +60,71 @@ typedef enum RecoveryPauseState RECOVERY_PAUSED, /* recovery is paused */ } RecoveryPauseState; +/* + * Shared-memory state for WAL recovery. + */ +typedef struct XLogRecoveryCtlData +{ + /* + * SharedHotStandbyActive indicates if we allow hot standby queries to be + * run. Protected by info_lck. + */ + bool SharedHotStandbyActive; + + /* + * SharedPromoteIsTriggered indicates if a standby promotion has been + * triggered. Protected by info_lck. + */ + bool SharedPromoteIsTriggered; + + /* + * recoveryWakeupLatch is used to wake up the startup process to continue + * WAL replay, if it is waiting for WAL to arrive or promotion to be + * requested. + * + * Note that the startup process also uses another latch, its procLatch, + * to wait for recovery conflict. If we get rid of recoveryWakeupLatch for + * signaling the startup process in favor of using its procLatch, which + * comports better with possible generic signal handlers using that latch. + * But we should not do that because the startup process doesn't assume + * that it's waken up by walreceiver process or SIGHUP signal handler + * while it's waiting for recovery conflict. The separate latches, + * recoveryWakeupLatch and procLatch, should be used for inter-process + * communication for WAL replay and recovery conflict, respectively. + */ + Latch recoveryWakeupLatch; + + /* + * Last record successfully replayed. + */ + XLogRecPtr lastReplayedReadRecPtr; /* start position */ + XLogRecPtr lastReplayedEndRecPtr; /* end+1 position */ + TimeLineID lastReplayedTLI; /* timeline */ + + /* + * When we're currently replaying a record, ie. in a redo function, + * replayEndRecPtr points to the end+1 of the record being replayed, + * otherwise it's equal to lastReplayedEndRecPtr. + */ + XLogRecPtr replayEndRecPtr; + TimeLineID replayEndTLI; + /* timestamp of last COMMIT/ABORT record replayed (or being replayed) */ + TimestampTz recoveryLastXTime; + + /* + * timestamp of when we started replaying the current chunk of WAL data, + * only relevant for replication or archive recovery + */ + TimestampTz currentChunkStartTime; + /* Recovery pause state */ + RecoveryPauseState recoveryPauseState; + ConditionVariable recoveryNotPausedCV; + + slock_t info_lck; /* locks shared variables shown above */ +} XLogRecoveryCtlData; + +extern PGDLLIMPORT XLogRecoveryCtlData *XLogRecoveryCtl; + /* User-settable GUC parameters */ extern PGDLLIMPORT bool recoveryTargetInclusive; extern PGDLLIMPORT int recoveryTargetAction; @@ -76,9 +153,6 @@ extern PGDLLIMPORT bool reachedConsistency; /* Are we currently in standby mode? */ extern PGDLLIMPORT bool StandbyMode; -extern Size XLogRecoveryShmemSize(void); -extern void XLogRecoveryShmemInit(void); - extern void InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr, bool *haveBackupLabel_ptr, bool *haveTblspcMap_ptr); diff --git a/src/include/access/xlogstats.h b/src/include/access/xlogstats.h index 528a640524a27..6d6842379e5d5 100644 --- a/src/include/access/xlogstats.h +++ b/src/include/access/xlogstats.h @@ -1,9 +1,9 @@ /*------------------------------------------------------------------------- * * xlogstats.h - * Definitions for WAL Statitstics + * Definitions for WAL Statistics * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/access/xlogstats.h diff --git a/src/include/access/xlogutils.h b/src/include/access/xlogutils.h index a1870d8e5aa17..b97387c6d4c43 100644 --- a/src/include/access/xlogutils.h +++ b/src/include/access/xlogutils.h @@ -3,7 +3,7 @@ * * Utilities for replaying WAL records. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/access/xlogutils.h diff --git a/src/include/access/xlogwait.h b/src/include/access/xlogwait.h new file mode 100644 index 0000000000000..07157f220eae0 --- /dev/null +++ b/src/include/access/xlogwait.h @@ -0,0 +1,109 @@ +/*------------------------------------------------------------------------- + * + * xlogwait.h + * Declarations for WAL flush, write, and replay waiting routines. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * src/include/access/xlogwait.h + * + *------------------------------------------------------------------------- + */ +#ifndef XLOG_WAIT_H +#define XLOG_WAIT_H + +#include "access/xlogdefs.h" +#include "lib/pairingheap.h" +#include "port/atomics.h" +#include "storage/procnumber.h" +#include "storage/spin.h" +#include "tcop/dest.h" + +/* + * Result statuses for WaitForLSN(). + */ +typedef enum +{ + WAIT_LSN_RESULT_SUCCESS, /* Target LSN is reached */ + WAIT_LSN_RESULT_NOT_IN_RECOVERY, /* Recovery ended before or during our + * wait */ + WAIT_LSN_RESULT_TIMEOUT /* Timeout occurred */ +} WaitLSNResult; + +/* + * LSN type for waiting facility. + */ +typedef enum WaitLSNType +{ + /* Standby wait types (walreceiver/startup wakes) */ + WAIT_LSN_TYPE_STANDBY_REPLAY, + WAIT_LSN_TYPE_STANDBY_WRITE, + WAIT_LSN_TYPE_STANDBY_FLUSH, + + /* Primary wait types (WAL writer/backends wake) */ + WAIT_LSN_TYPE_PRIMARY_FLUSH, +} WaitLSNType; + +#define WAIT_LSN_TYPE_COUNT (WAIT_LSN_TYPE_PRIMARY_FLUSH + 1) + +/* + * WaitLSNProcInfo - the shared memory structure representing information + * about the single process, which may wait for LSN operations. An item of + * waitLSNState->procInfos array. + */ +typedef struct WaitLSNProcInfo +{ + /* LSN, which this process is waiting for */ + XLogRecPtr waitLSN; + + /* The type of LSN to wait */ + WaitLSNType lsnType; + + /* Process to wake up once the waitLSN is reached */ + ProcNumber procno; + + /* + * Heap membership flag. A process can wait for only one LSN type at a + * time, so a single flag suffices (tracked by the lsnType field). + */ + bool inHeap; + + /* Pairing heap node for the waiters' heap (one per process) */ + pairingheap_node heapNode; +} WaitLSNProcInfo; + +/* + * WaitLSNState - the shared memory state for the LSN waiting facility. + */ +typedef struct WaitLSNState +{ + /* + * The minimum LSN values some process is waiting for. Used for the + * fast-path checking if we need to wake up any waiters after replaying a + * WAL record. Could be read lock-less. Update protected by WaitLSNLock. + */ + pg_atomic_uint64 minWaitedLSN[WAIT_LSN_TYPE_COUNT]; + + /* + * A pairing heaps of waiting processes ordered by LSN values (least LSN + * is on top). Protected by WaitLSNLock. + */ + pairingheap waitersHeap[WAIT_LSN_TYPE_COUNT]; + + /* + * An array with per-process information, indexed by the process number. + * Protected by WaitLSNLock. + */ + WaitLSNProcInfo procInfos[FLEXIBLE_ARRAY_MEMBER]; +} WaitLSNState; + + +extern PGDLLIMPORT WaitLSNState *waitLSNState; + +extern XLogRecPtr GetCurrentLSNForWaitType(WaitLSNType lsnType); +extern void WaitLSNWakeup(WaitLSNType lsnType, XLogRecPtr currentLSN); +extern void WaitLSNCleanup(void); +extern WaitLSNResult WaitForLSN(WaitLSNType lsnType, XLogRecPtr targetLSN, + int64 timeout); + +#endif /* XLOG_WAIT_H */ diff --git a/src/include/archive/archive_module.h b/src/include/archive/archive_module.h index 3b17ca7eecad4..146275558c5d9 100644 --- a/src/include/archive/archive_module.h +++ b/src/include/archive/archive_module.h @@ -3,7 +3,7 @@ * archive_module.h * Exports for archive modules. * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * src/include/archive/archive_module.h * diff --git a/src/include/archive/shell_archive.h b/src/include/archive/shell_archive.h index cf131f5dc99fb..e7cbf79215779 100644 --- a/src/include/archive/shell_archive.h +++ b/src/include/archive/shell_archive.h @@ -3,7 +3,7 @@ * shell_archive.h * Exports for archiving via shell. * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * src/include/archive/shell_archive.h * diff --git a/src/include/backup/backup_manifest.h b/src/include/backup/backup_manifest.h index 6db019782608b..de3ab20d01631 100644 --- a/src/include/backup/backup_manifest.h +++ b/src/include/backup/backup_manifest.h @@ -3,7 +3,7 @@ * backup_manifest.h * Routines for generating a backup manifest. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/backup/backup_manifest.h * diff --git a/src/include/backup/basebackup.h b/src/include/backup/basebackup.h index ff88ff7207031..bc776ec3b769e 100644 --- a/src/include/backup/basebackup.h +++ b/src/include/backup/basebackup.h @@ -3,7 +3,7 @@ * basebackup.h * Exports from replication/basebackup.c. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/backup/basebackup.h * diff --git a/src/include/backup/basebackup_incremental.h b/src/include/backup/basebackup_incremental.h index 1c3d0c925b153..e6eb5439cde44 100644 --- a/src/include/backup/basebackup_incremental.h +++ b/src/include/backup/basebackup_incremental.h @@ -3,7 +3,7 @@ * basebackup_incremental.h * API for incremental backup support * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/backup/basebackup_incremental.h * diff --git a/src/include/backup/basebackup_sink.h b/src/include/backup/basebackup_sink.h index 8a5ee996a45ed..bbf4de0cbdcb3 100644 --- a/src/include/backup/basebackup_sink.h +++ b/src/include/backup/basebackup_sink.h @@ -17,7 +17,7 @@ * single task e.g. command progress reporting, throttling, or * communication with the client. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/backup/basebackup_sink.h * @@ -287,7 +287,8 @@ extern bbsink *bbsink_copystream_new(bool send_to_client); extern bbsink *bbsink_gzip_new(bbsink *next, pg_compress_specification *); extern bbsink *bbsink_lz4_new(bbsink *next, pg_compress_specification *); extern bbsink *bbsink_zstd_new(bbsink *next, pg_compress_specification *); -extern bbsink *bbsink_progress_new(bbsink *next, bool estimate_backup_size); +extern bbsink *bbsink_progress_new(bbsink *next, bool estimate_backup_size, + bool incremental); extern bbsink *bbsink_server_new(bbsink *next, char *pathname); extern bbsink *bbsink_throttle_new(bbsink *next, uint32 maxrate); diff --git a/src/include/backup/basebackup_target.h b/src/include/backup/basebackup_target.h index 0903df0d28256..87e8892cf8e9f 100644 --- a/src/include/backup/basebackup_target.h +++ b/src/include/backup/basebackup_target.h @@ -3,7 +3,7 @@ * basebackup_target.h * Extensibility framework for adding base backup targets. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/backup/basebackup_target.h * diff --git a/src/include/backup/walsummary.h b/src/include/backup/walsummary.h index a3927983f8dcb..60157e0ac5048 100644 --- a/src/include/backup/walsummary.h +++ b/src/include/backup/walsummary.h @@ -3,7 +3,7 @@ * walsummary.h * WAL summary management * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/backup/walsummary.h * diff --git a/src/include/bootstrap/bootstrap.h b/src/include/bootstrap/bootstrap.h index befc4fa1b3d87..c0bba03a5ee94 100644 --- a/src/include/bootstrap/bootstrap.h +++ b/src/include/bootstrap/bootstrap.h @@ -4,7 +4,7 @@ * include file for the bootstrapping code * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/bootstrap/bootstrap.h @@ -14,6 +14,7 @@ #ifndef BOOTSTRAP_H #define BOOTSTRAP_H +#include "catalog/pg_attribute.h" #include "nodes/execnodes.h" #include "nodes/parsenodes.h" @@ -53,13 +54,13 @@ extern void boot_get_type_io_data(Oid typid, char *typdelim, Oid *typioparam, Oid *typinput, - Oid *typoutput); + Oid *typoutput, + Oid *typcollation); + +extern Oid boot_get_role_oid(const char *rolname); union YYSTYPE; -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif extern int boot_yyparse(yyscan_t yyscanner); extern int boot_yylex_init(yyscan_t *yyscannerp); diff --git a/src/include/c.h b/src/include/c.h index 8cdc16a0f4a9b..97ed8c63f5ea8 100644 --- a/src/include/c.h +++ b/src/include/c.h @@ -9,7 +9,7 @@ * polluting the namespace with lots of stuff... * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/c.h @@ -59,7 +59,9 @@ #include "pg_config_os.h" /* config from include/port/PORTNAME.h */ /* System header files that should be available everywhere in Postgres */ +#include #include +#include #include #include #include @@ -80,6 +82,14 @@ #endif #ifdef ENABLE_NLS #include +#endif + +#ifdef __cplusplus +extern "C++" +{ +/* This header is used in the definition of various C++ things below. */ +#include +} #endif /* Pull in fundamental symbols that we also expose to applications */ @@ -93,8 +103,6 @@ /* ---------------------------------------------------------------- * Section 1: compiler characteristics - * - * type prefixes (const, signed, volatile, inline) are handled in pg_config.h. * ---------------------------------------------------------------- */ @@ -111,35 +119,55 @@ /* * Attribute macros * + * C23: https://en.cppreference.com/w/c/language/attributes.html + * C++: https://en.cppreference.com/w/cpp/language/attributes.html * GCC: https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html * GCC: https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html * Clang: https://clang.llvm.org/docs/AttributeReference.html - * Sunpro: https://docs.oracle.com/cd/E18659_01/html/821-1384/gjzke.html */ /* * For compilers which don't support __has_attribute, we just define * __has_attribute(x) to 0 so that we can define macros for various * __attribute__s more easily below. + * + * Note that __has_attribute only tells about GCC-style attributes, not C23 or + * C++ attributes. */ #ifndef __has_attribute #define __has_attribute(attribute) 0 #endif -/* only GCC supports the unused attribute */ -#ifdef __GNUC__ +/* + * pg_attribute_unused() suppresses compiler warnings on unused entities. + */ +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201703L) +#define pg_attribute_unused() [[maybe_unused]] +#elif defined(__GNUC__) #define pg_attribute_unused() __attribute__((unused)) #else #define pg_attribute_unused() #endif +/* + * pg_fallthrough indicates that the fall through from the previous case is + * intentional. + */ +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201703L) +#define pg_fallthrough [[fallthrough]] +#elif __has_attribute(fallthrough) +#define pg_fallthrough __attribute__((fallthrough)) +#else +#define pg_fallthrough +#endif + /* * pg_nodiscard means the compiler should warn if the result of a function - * call is ignored. The name "nodiscard" is chosen in alignment with the C23 - * standard attribute with the same name. For maximum forward compatibility, - * place it before the declaration. + * call is ignored. */ -#ifdef __GNUC__ +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201703L) +#define pg_nodiscard [[nodiscard]] +#elif defined(__GNUC__) #define pg_nodiscard __attribute__((warn_unused_result)) #else #define pg_nodiscard @@ -151,18 +179,15 @@ * uses __attribute__((noreturn)) in headers, which would get confused if * "noreturn" is defined to "_Noreturn", as is done by . * - * In a declaration, function specifiers go before the function name. The - * common style is to put them before the return type. (The MSVC fallback has - * the same requirement. The GCC fallback is more flexible.) + * C23 attributes must be placed at the start of a declaration or statement. + * C11 function specifiers go before the function name in a declaration, but + * it is common style (and required for C23 compatibility) to put them before + * the return type. */ -#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L -#define pg_noreturn _Noreturn -#elif defined(__GNUC__) || defined(__SUNPRO_C) -#define pg_noreturn __attribute__((noreturn)) -#elif defined(_MSC_VER) -#define pg_noreturn __declspec(noreturn) +#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || defined(__cplusplus) +#define pg_noreturn [[noreturn]] #else -#define pg_noreturn +#define pg_noreturn _Noreturn #endif /* @@ -224,6 +249,16 @@ #define PG_USED_FOR_ASSERTS_ONLY pg_attribute_unused() #endif +/* + * Our C and C++ compilers may have different ideas about which printf + * archetype best represents what src/port/snprintf.c can do. + */ +#ifndef __cplusplus +#define PG_PRINTF_ATTRIBUTE PG_C_PRINTF_ATTRIBUTE +#else +#define PG_PRINTF_ATTRIBUTE PG_CXX_PRINTF_ATTRIBUTE +#endif + /* GCC supports format attributes */ #if defined(__GNUC__) #define pg_attribute_format_arg(a) __attribute__((format_arg(a))) @@ -233,8 +268,8 @@ #define pg_attribute_printf(f,a) #endif -/* GCC and Sunpro support aligned and packed */ -#if defined(__GNUC__) || defined(__SUNPRO_C) +/* GCC supports aligned and packed */ +#if defined(__GNUC__) #define pg_attribute_aligned(a) __attribute__((aligned(a))) #define pg_attribute_packed() __attribute__((packed)) #elif defined(_MSC_VER) @@ -259,8 +294,8 @@ * choose not to. But, if possible, don't force inlining in unoptimized * debug builds. */ -#if (defined(__GNUC__) && __GNUC__ > 3 && defined(__OPTIMIZE__)) || defined(__SUNPRO_C) -/* GCC > 3 and Sunpro support always_inline via __attribute__ */ +#if defined(__GNUC__) && defined(__OPTIMIZE__) +/* GCC supports always_inline via __attribute__ */ #define pg_attribute_always_inline __attribute__((always_inline)) inline #elif defined(_MSC_VER) /* MSVC has a special keyword for this */ @@ -276,8 +311,8 @@ * for proper cost attribution. Note that unlike the pg_attribute_XXX macros * above, this should be placed before the function's return type and name. */ -/* GCC and Sunpro support noinline via __attribute__ */ -#if (defined(__GNUC__) && __GNUC__ > 2) || defined(__SUNPRO_C) +/* GCC supports noinline via __attribute__ */ +#if defined(__GNUC__) #define pg_noinline __attribute__((noinline)) /* msvc via declspec */ #elif defined(_MSC_VER) @@ -332,6 +367,62 @@ #define pg_unreachable() abort() #endif +/* + * Define a compiler-independent macro for determining if an expression is a + * compile-time integer const. We don't define this macro to return 0 when + * unsupported due to the risk of users of the macro misbehaving if we return + * 0 when the expression *is* an integer constant. Callers may check if this + * macro is defined by checking if HAVE_PG_INTEGER_CONSTANT_P is defined. + */ +#if defined(HAVE__BUILTIN_CONSTANT_P) + +/* When __builtin_constant_p() is available, use it. */ +#define pg_integer_constant_p(x) __builtin_constant_p(x) +#define HAVE_PG_INTEGER_CONSTANT_P +#elif defined(_MSC_VER) && defined(__STDC_VERSION__) + +/* + * With MSVC we can use a trick with _Generic to make this work. This has + * been borrowed from: + * https://stackoverflow.com/questions/49480442/detecting-integer-constant-expressions-in-macros + * and only works with integer constants. Compilation will fail if given a + * constant or variable of any type other than an integer. + */ +#define pg_integer_constant_p(x) \ + _Generic((1 ? ((void *) ((x) * (uintptr_t) 0)) : &(int) {1}), int *: 1, void *: 0) +#define HAVE_PG_INTEGER_CONSTANT_P +#endif + +/* + * pg_assume(expr) states that we assume `expr` to evaluate to true. In assert + * enabled builds pg_assume() is turned into an assertion, in optimized builds + * we try to clue the compiler into the fact that `expr` is true. + * + * This is useful for two purposes: + * + * 1) Avoid compiler warnings by telling the compiler about assumptions the + * code makes. This is particularly useful when building with optimizations + * and w/o assertions. + * + * 2) Help the compiler to generate more efficient code + * + * It is unspecified whether `expr` is evaluated, therefore it better be + * side-effect free. + */ +#if defined(USE_ASSERT_CHECKING) +#define pg_assume(expr) Assert(expr) +#elif defined(HAVE__BUILTIN_UNREACHABLE) +#define pg_assume(expr) \ + do { \ + if (!(expr)) \ + __builtin_unreachable(); \ + } while (0) +#elif defined(_MSC_VER) +#define pg_assume(expr) __assume(expr) +#else +#define pg_assume(expr) ((void) 0) +#endif + /* * Hints to the compiler about the likelihood of a branch. Both likely() and * unlikely() return the boolean value of the contained expression. @@ -339,7 +430,7 @@ * These should only be used sparingly, in very hot code paths. It's very easy * to mis-estimate likelihoods. */ -#if __GNUC__ >= 3 +#ifdef __GNUC__ #define likely(x) __builtin_expect((x) != 0, 1) #define unlikely(x) __builtin_expect((x) != 0, 0) #else @@ -347,6 +438,58 @@ #define unlikely(x) ((x) != 0) #endif +/* + * When we call clang to generate bitcode, we might be using configure results + * from a different compiler, which might not be fully compatible with the + * clang we are using. The fully correct solution would be to run a separate + * set of configure tests for that clang-for-bitcode, but that could be very + * difficult to implement, and in practice clang supports most things other + * compilers support. So this section just contains some hardcoded ugliness + * to override some configure results where it is necessary. + */ +#if defined(__clang__) +#if __clang_major__ < 19 +#undef HAVE_TYPEOF_UNQUAL +#else +#undef typeof_unqual +#define typeof_unqual __typeof_unqual__ +#endif +#endif /* __clang__ */ + +/* + * Provide typeof in C++ for C++ compilers that don't support typeof natively. + * It might be spelled __typeof__ instead of typeof, in which case + * pg_cxx_typeof provides that mapping. If neither is supported, we can use + * decltype, but to make it equivalent to C's typeof, we need to remove + * references from the result [1]. Also ensure HAVE_TYPEOF is set so that + * typeof-dependent code is always enabled in C++ mode. + * + * [1]: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2927.htm#existing-decltype + */ +#if defined(__cplusplus) +#undef typeof +#ifdef pg_cxx_typeof +#define typeof(x) pg_cxx_typeof(x) +#elif !defined(HAVE_CXX_TYPEOF) +#define typeof(x) std::remove_reference::type +#endif +#ifndef HAVE_TYPEOF +#define HAVE_TYPEOF 1 +#endif +/* + * and analogously for typeof_unqual + */ +#undef typeof_unqual +#ifdef pg_cxx_typeof_unqual +#define typeof_unqual(x) pg_cxx_typeof_unqual(x) +#elif !defined(HAVE_CXX_TYPEOF_UNQUAL) +#define typeof_unqual(x) std::remove_cv::type>::type +#endif +#ifndef HAVE_TYPEOF_UNQUAL +#define HAVE_TYPEOF_UNQUAL 1 +#endif +#endif /* __cplusplus */ + /* * CppAsString * Convert the argument to a string, using the C preprocessor. @@ -376,25 +519,7 @@ * pretty trivial: VA_ARGS_NARGS_() returns its 64th argument, and we set up * the call so that that is the appropriate one of the list of constants. * This idea is due to Laurent Deniau. - * - * MSVC has an implementation of __VA_ARGS__ that doesn't conform to the - * standard unless you use the /Zc:preprocessor compiler flag, but that - * isn't available before Visual Studio 2019. For now, use a different - * definition that also works on older compilers. */ -#ifdef _MSC_VER -#define EXPAND(args) args -#define VA_ARGS_NARGS(...) \ - VA_ARGS_NARGS_ EXPAND((__VA_ARGS__, \ - 63,62,61,60, \ - 59,58,57,56,55,54,53,52,51,50, \ - 49,48,47,46,45,44,43,42,41,40, \ - 39,38,37,36,35,34,33,32,31,30, \ - 29,28,27,26,25,24,23,22,21,20, \ - 19,18,17,16,15,14,13,12,11,10, \ - 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)) -#else - #define VA_ARGS_NARGS(...) \ VA_ARGS_NARGS_(__VA_ARGS__, \ 63,62,61,60, \ @@ -404,7 +529,6 @@ 29,28,27,26,25,24,23,22,21,20, \ 19,18,17,16,15,14,13,12,11,10, \ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) -#endif #define VA_ARGS_NARGS_( \ _01,_02,_03,_04,_05,_06,_07,_08,_09,_10, \ @@ -486,11 +610,9 @@ typedef void (*pg_funcptr_t) (void); /* * Pointer * Variable holding address of any memory resident object. - * - * XXX Pointer arithmetic is done with this, so it can't be void * - * under "true" ANSI compilers. + * (obsolescent; use void * or char *) */ -typedef char *Pointer; +typedef void *Pointer; /* Historical names for types in . */ typedef int8_t int8; @@ -502,14 +624,6 @@ typedef uint16_t uint16; typedef uint32_t uint32; typedef uint64_t uint64; -/* - * bitsN - * Unit of bitwise operation, AT LEAST N BITS IN SIZE. - */ -typedef uint8 bits8; /* >= 8 bits */ -typedef uint16 bits16; /* >= 16 bits */ -typedef uint32 bits32; /* >= 32 bits */ - /* * 64-bit integers */ @@ -519,8 +633,7 @@ typedef uint32 bits32; /* >= 32 bits */ /* snprintf format strings to use for 64-bit integers */ #define INT64_FORMAT "%" PRId64 #define UINT64_FORMAT "%" PRIu64 -#define INT64_HEX_FORMAT "%" PRIx64 -#define UINT64_HEX_FORMAT "%" PRIx64 +#define OID8_FORMAT "%" PRIu64 /* * 128-bit signed and unsigned integers @@ -600,14 +713,14 @@ typedef signed int Offset; typedef float float4; typedef double float8; -#ifdef USE_FLOAT8_BYVAL +/* + * float8, int8, and related datatypes are now always pass-by-value. + * We keep this symbol to avoid breaking extension code that may use it. + */ #define FLOAT8PASSBYVAL true -#else -#define FLOAT8PASSBYVAL false -#endif /* - * Oid, RegProcedure, TransactionId, SubTransactionId, MultiXactId, + * Oid, Oid8, RegProcedure, TransactionId, SubTransactionId, MultiXactId, * CommandId */ @@ -632,16 +745,21 @@ typedef uint32 SubTransactionId; /* MultiXactId must be equivalent to TransactionId, to fit in t_xmax */ typedef TransactionId MultiXactId; -typedef uint32 MultiXactOffset; +typedef uint64 MultiXactOffset; typedef uint32 CommandId; #define FirstCommandId ((CommandId) 0) #define InvalidCommandId (~(CommandId)0) +/* 8-byte Object ID */ +typedef uint64 Oid8; + +#define InvalidOid8 ((Oid8) 0) +#define OID8_MAX UINT64_MAX /* ---------------- - * Variable-length datatypes all share the 'struct varlena' header. + * Variable-length datatypes all share the 'varlena' header. * * NOTE: for TOASTable types, this is an oversimplification, since the value * may be compressed or moved out-of-line. However datatype-specific routines @@ -651,14 +769,14 @@ typedef uint32 CommandId; * representation is no longer convenient. It's recommended that code always * use macros VARDATA_ANY, VARSIZE_ANY, VARSIZE_ANY_EXHDR, VARDATA, VARSIZE, * and SET_VARSIZE instead of relying on direct mentions of the struct fields. - * See postgres.h for details of the TOASTed form. + * See varatt.h for details of the TOASTed form. * ---------------- */ -struct varlena +typedef struct varlena { char vl_len_[4]; /* Do not touch this field directly! */ char vl_dat[FLEXIBLE_ARRAY_MEMBER]; /* Data content is here */ -}; +} varlena; #define VARHDRSZ ((int32) sizeof(int32)) @@ -667,10 +785,10 @@ struct varlena * There is no terminating null or anything like that --- the data length is * always VARSIZE_ANY_EXHDR(ptr). */ -typedef struct varlena bytea; -typedef struct varlena text; -typedef struct varlena BpChar; /* blank-padded char, ie SQL char(n) */ -typedef struct varlena VarChar; /* var-length char, ie SQL varchar(n) */ +typedef varlena bytea; +typedef varlena text; +typedef varlena BpChar; /* blank-padded char, ie SQL char(n) */ +typedef varlena VarChar; /* var-length char, ie SQL varchar(n) */ /* * Specialized array types. These are physically laid out just the same @@ -727,12 +845,6 @@ typedef NameData *Name; */ #define BoolIsValid(boolean) ((boolean) == false || (boolean) == true) -/* - * PointerIsValid - * True iff pointer is valid. - */ -#define PointerIsValid(pointer) ((const void*)(pointer) != NULL) - /* * PointerIsAligned * True iff pointer is properly aligned to point to the given type. @@ -745,6 +857,8 @@ typedef NameData *Name; #define OidIsValid(objectId) ((bool) ((objectId) != InvalidOid)) +#define Oid8IsValid(objectId) ((bool) ((objectId) != InvalidOid8)) + #define RegProcedureIsValid(p) OidIsValid(p) @@ -777,7 +891,7 @@ typedef NameData *Name; #define SHORTALIGN(LEN) TYPEALIGN(ALIGNOF_SHORT, (LEN)) #define INTALIGN(LEN) TYPEALIGN(ALIGNOF_INT, (LEN)) -#define LONGALIGN(LEN) TYPEALIGN(ALIGNOF_LONG, (LEN)) +#define INT64ALIGN(LEN) TYPEALIGN(ALIGNOF_INT64_T, (LEN)) #define DOUBLEALIGN(LEN) TYPEALIGN(ALIGNOF_DOUBLE, (LEN)) #define MAXALIGN(LEN) TYPEALIGN(MAXIMUM_ALIGNOF, (LEN)) /* MAXALIGN covers only built-in types, not buffers */ @@ -789,7 +903,7 @@ typedef NameData *Name; #define SHORTALIGN_DOWN(LEN) TYPEALIGN_DOWN(ALIGNOF_SHORT, (LEN)) #define INTALIGN_DOWN(LEN) TYPEALIGN_DOWN(ALIGNOF_INT, (LEN)) -#define LONGALIGN_DOWN(LEN) TYPEALIGN_DOWN(ALIGNOF_LONG, (LEN)) +#define INT64ALIGN_DOWN(LEN) TYPEALIGN_DOWN(ALIGNOF_INT64_T, (LEN)) #define DOUBLEALIGN_DOWN(LEN) TYPEALIGN_DOWN(ALIGNOF_DOUBLE, (LEN)) #define MAXALIGN_DOWN(LEN) TYPEALIGN_DOWN(MAXIMUM_ALIGNOF, (LEN)) #define BUFFERALIGN_DOWN(LEN) TYPEALIGN_DOWN(ALIGNOF_BUFFER, (LEN)) @@ -831,7 +945,6 @@ typedef NameData *Name; #elif defined(FRONTEND) -#include #define Assert(p) assert(p) #define AssertMacro(p) ((void) assert(p)) @@ -881,78 +994,81 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName, * * If the "condition" (a compile-time-constant expression) evaluates to false, * throw a compile error using the "errmessage" (a string literal). - * - * C11 has _Static_assert(), and most C99 compilers already support that. For - * portability, we wrap it into StaticAssertDecl(). _Static_assert() is a - * "declaration", and so it must be placed where for example a variable + */ + +/* + * We require C11 and C++11, so static_assert() is expected to be there. + * StaticAssertDecl() was previously used for portability, but it's now just a + * plain wrapper and doesn't need to be used in new code. static_assert() is + * a "declaration", and so it must be placed where for example a variable * declaration would be valid. As long as we compile with * -Wno-declaration-after-statement, that also means it cannot be placed after - * statements in a function. Macros StaticAssertStmt() and StaticAssertExpr() - * make it safe to use as a statement or in an expression, respectively. - * - * For compilers without _Static_assert(), we fall back on a kluge that - * assumes the compiler will complain about a negative width for a struct - * bit-field. This will not include a helpful error message, but it beats not - * getting an error at all. + * statements in a function. */ -#ifndef __cplusplus -#ifdef HAVE__STATIC_ASSERT -#define StaticAssertDecl(condition, errmessage) \ - _Static_assert(condition, errmessage) -#define StaticAssertStmt(condition, errmessage) \ - do { _Static_assert(condition, errmessage); } while(0) -#define StaticAssertExpr(condition, errmessage) \ - ((void) ({ StaticAssertStmt(condition, errmessage); true; })) -#else /* !HAVE__STATIC_ASSERT */ -#define StaticAssertDecl(condition, errmessage) \ - extern void static_assert_func(int static_assert_failure[(condition) ? 1 : -1]) -#define StaticAssertStmt(condition, errmessage) \ - ((void) sizeof(struct { int static_assert_failure : (condition) ? 1 : -1; })) -#define StaticAssertExpr(condition, errmessage) \ - StaticAssertStmt(condition, errmessage) -#endif /* HAVE__STATIC_ASSERT */ -#else /* C++ */ -#if defined(__cpp_static_assert) && __cpp_static_assert >= 200410 #define StaticAssertDecl(condition, errmessage) \ static_assert(condition, errmessage) + +/* + * StaticAssertStmt() was previously used to make static assertions work as a + * statement, but its use is now deprecated. + */ #define StaticAssertStmt(condition, errmessage) \ - static_assert(condition, errmessage) + do { static_assert(condition, errmessage); } while(0) + +/* + * StaticAssertExpr() is for use in an expression. + * + * See for some + * rationale for the precise behavior of this implementation. See + * about the C++ + * implementation. + * + * For compilers that don't support this, we fall back on a kluge that assumes + * the compiler will complain about a negative width for a struct bit-field. + * This will not include a helpful error message, but it beats not getting an + * error at all. + */ +#ifndef __cplusplus +#if !defined(_MSC_VER) || _MSC_VER >= 1933 #define StaticAssertExpr(condition, errmessage) \ - ({ static_assert(condition, errmessage); }) -#else /* !__cpp_static_assert */ -#define StaticAssertDecl(condition, errmessage) \ - extern void static_assert_func(int static_assert_failure[(condition) ? 1 : -1]) -#define StaticAssertStmt(condition, errmessage) \ - do { struct static_assert_struct { int static_assert_failure : (condition) ? 1 : -1; }; } while(0) + ((void) sizeof(struct {static_assert(condition, errmessage); char a;})) +#else /* _MSC_VER < 1933 */ +/* + * This compiler is buggy and fails to compile the previous variant; use a + * fallback implementation. + */ #define StaticAssertExpr(condition, errmessage) \ - ((void) ({ StaticAssertStmt(condition, errmessage); })) -#endif /* __cpp_static_assert */ -#endif /* C++ */ + ((void) sizeof(struct { int static_assert_failure : (condition) ? 1 : -1; })) +#endif /* _MSC_VER < 1933 */ +#else /* __cplusplus */ +#define StaticAssertExpr(condition, errmessage) \ + ([]{static_assert(condition, errmessage);}) +#endif /* * Compile-time checks that a variable (or expression) has the specified type. * - * AssertVariableIsOfType() can be used as a statement. - * AssertVariableIsOfTypeMacro() is intended for use in macros, eg - * #define foo(x) (AssertVariableIsOfTypeMacro(x, int), bar(x)) + * StaticAssertVariableIsOfType() can be used as a declaration. + * StaticAssertVariableIsOfTypeMacro() is intended for use in macros, eg + * #define foo(x) (StaticAssertVariableIsOfTypeMacro(x, int), bar(x)) * * If we don't have __builtin_types_compatible_p, we can still assert that * the types have the same size. This is far from ideal (especially on 32-bit * platforms) but it provides at least some coverage. */ #ifdef HAVE__BUILTIN_TYPES_COMPATIBLE_P -#define AssertVariableIsOfType(varname, typename) \ - StaticAssertStmt(__builtin_types_compatible_p(__typeof__(varname), typename), \ +#define StaticAssertVariableIsOfType(varname, typename) \ + StaticAssertDecl(__builtin_types_compatible_p(typeof(varname), typename), \ CppAsString(varname) " does not have type " CppAsString(typename)) -#define AssertVariableIsOfTypeMacro(varname, typename) \ - (StaticAssertExpr(__builtin_types_compatible_p(__typeof__(varname), typename), \ +#define StaticAssertVariableIsOfTypeMacro(varname, typename) \ + (StaticAssertExpr(__builtin_types_compatible_p(typeof(varname), typename), \ CppAsString(varname) " does not have type " CppAsString(typename))) #else /* !HAVE__BUILTIN_TYPES_COMPATIBLE_P */ -#define AssertVariableIsOfType(varname, typename) \ - StaticAssertStmt(sizeof(varname) == sizeof(typename), \ +#define StaticAssertVariableIsOfType(varname, typename) \ + StaticAssertDecl(sizeof(varname) == sizeof(typename), \ CppAsString(varname) " does not have type " CppAsString(typename)) -#define AssertVariableIsOfTypeMacro(varname, typename) \ +#define StaticAssertVariableIsOfTypeMacro(varname, typename) \ (StaticAssertExpr(sizeof(varname) == sizeof(typename), \ CppAsString(varname) " does not have type " CppAsString(typename))) #endif /* HAVE__BUILTIN_TYPES_COMPATIBLE_P */ @@ -975,29 +1091,29 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName, #define Min(x, y) ((x) < (y) ? (x) : (y)) -/* Get a bit mask of the bits set in non-long aligned addresses */ -#define LONG_ALIGN_MASK (sizeof(long) - 1) +/* Get a bit mask of the bits set in non-size_t aligned addresses */ +#define SIZE_T_ALIGN_MASK (sizeof(size_t) - 1) /* * MemSet * Exactly the same as standard library function memset(), but considerably - * faster for zeroing small word-aligned structures (such as parsetree nodes). - * This has to be a macro because the main point is to avoid function-call - * overhead. However, we have also found that the loop is faster than - * native libc memset() on some platforms, even those with assembler - * memset() functions. More research needs to be done, perhaps with - * MEMSET_LOOP_LIMIT tests in configure. + * faster for zeroing small size_t-aligned structures (such as parsetree + * nodes). This has to be a macro because the main point is to avoid + * function-call overhead. However, we have also found that the loop is + * faster than native libc memset() on some platforms, even those with + * assembler memset() functions. More research needs to be done, perhaps + * with MEMSET_LOOP_LIMIT tests in configure. */ #define MemSet(start, val, len) \ do \ { \ - /* must be void* because we don't know if it is integer aligned yet */ \ + /* must be void* because we don't know if it is size_t aligned yet */ \ void *_vstart = (void *) (start); \ int _val = (val); \ Size _len = (len); \ \ - if ((((uintptr_t) _vstart) & LONG_ALIGN_MASK) == 0 && \ - (_len & LONG_ALIGN_MASK) == 0 && \ + if ((((uintptr_t) _vstart) & SIZE_T_ALIGN_MASK) == 0 && \ + (_len & SIZE_T_ALIGN_MASK) == 0 && \ _val == 0 && \ _len <= MEMSET_LOOP_LIMIT && \ /* \ @@ -1006,8 +1122,8 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName, */ \ MEMSET_LOOP_LIMIT != 0) \ { \ - long *_start = (long *) _vstart; \ - long *_stop = (long *) ((char *) _start + _len); \ + size_t *_start = (size_t *) _vstart; \ + size_t *_stop = (size_t *) ((char *) _start + _len); \ while (_start < _stop) \ *_start++ = 0; \ } \ @@ -1017,23 +1133,23 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName, /* * MemSetAligned is the same as MemSet except it omits the test to see if - * "start" is word-aligned. This is okay to use if the caller knows a-priori - * that the pointer is suitably aligned (typically, because he just got it - * from palloc(), which always delivers a max-aligned pointer). + * "start" is size_t-aligned. This is okay to use if the caller knows + * a-priori that the pointer is suitably aligned (typically, because he just + * got it from palloc(), which always delivers a max-aligned pointer). */ #define MemSetAligned(start, val, len) \ do \ { \ - long *_start = (long *) (start); \ + size_t *_start = (size_t *) (start); \ int _val = (val); \ Size _len = (len); \ \ - if ((_len & LONG_ALIGN_MASK) == 0 && \ + if ((_len & SIZE_T_ALIGN_MASK) == 0 && \ _val == 0 && \ _len <= MEMSET_LOOP_LIMIT && \ MEMSET_LOOP_LIMIT != 0) \ { \ - long *_stop = (long *) ((char *) _start + _len); \ + size_t *_stop = (size_t *) ((char *) _start + _len); \ while (_start < _stop) \ *_start++ = 0; \ } \ @@ -1084,17 +1200,21 @@ pg_noreturn extern void ExceptionalCondition(const char *conditionName, * Use this, not "char buf[BLCKSZ]", to declare a field or local variable * holding a page buffer, if that page might be accessed as a page. Otherwise * the variable might be under-aligned, causing problems on alignment-picky - * hardware. We include both "double" and "int64" in the union to ensure that - * the compiler knows the value must be MAXALIGN'ed (cf. configure's - * computation of MAXIMUM_ALIGNOF). + * hardware. */ -typedef union PGAlignedBlock +typedef struct PGAlignedBlock { - char data[BLCKSZ]; - double force_align_d; - int64 force_align_i64; + alignas(MAXIMUM_ALIGNOF) char data[BLCKSZ]; } PGAlignedBlock; +/* + * alignas with extended alignments is buggy in g++ < 9. As a simple + * workaround, we disable these definitions in that case. + * + * + */ +#if !(defined(__cplusplus) && defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 9) + /* * Use this to declare a field or local variable holding a page buffer, if that * page might be accessed as a page or passed to an SMgr I/O function. If @@ -1103,27 +1223,25 @@ typedef union PGAlignedBlock * for I/O in general, but may be strictly required on some platforms when * using direct I/O. */ -typedef union PGIOAlignedBlock +typedef struct PGIOAlignedBlock { -#ifdef pg_attribute_aligned - pg_attribute_aligned(PG_IO_ALIGN_SIZE) -#endif - char data[BLCKSZ]; - double force_align_d; - int64 force_align_i64; + alignas(PG_IO_ALIGN_SIZE) char data[BLCKSZ]; } PGIOAlignedBlock; /* Same, but for an XLOG_BLCKSZ-sized buffer */ -typedef union PGAlignedXLogBlock +typedef struct PGAlignedXLogBlock { -#ifdef pg_attribute_aligned - pg_attribute_aligned(PG_IO_ALIGN_SIZE) -#endif - char data[XLOG_BLCKSZ]; - double force_align_d; - int64 force_align_i64; + alignas(PG_IO_ALIGN_SIZE) char data[XLOG_BLCKSZ]; } PGAlignedXLogBlock; +#else /* (g++ < 9) */ + +/* Allow these types to be used as abstract types when using old g++ */ +typedef struct PGIOAlignedBlock PGIOAlignedBlock; +typedef struct PGAlignedXLogBlock PGAlignedXLogBlock; + +#endif /* !(g++ < 9) */ + /* msb for char */ #define HIGHBIT (0x80) #define IS_HIGHBIT_SET(ch) ((unsigned char)(ch) & HIGHBIT) @@ -1206,20 +1324,32 @@ typedef union PGAlignedXLogBlock #if defined(__cplusplus) #define unconstify(underlying_type, expr) const_cast(expr) #define unvolatize(underlying_type, expr) const_cast(expr) -#elif defined(HAVE__BUILTIN_TYPES_COMPATIBLE_P) +#else #define unconstify(underlying_type, expr) \ - (StaticAssertExpr(__builtin_types_compatible_p(__typeof(expr), const underlying_type), \ - "wrong cast"), \ + (StaticAssertVariableIsOfTypeMacro(expr, const underlying_type), \ (underlying_type) (expr)) #define unvolatize(underlying_type, expr) \ - (StaticAssertExpr(__builtin_types_compatible_p(__typeof(expr), volatile underlying_type), \ - "wrong cast"), \ + (StaticAssertVariableIsOfTypeMacro(expr, volatile underlying_type), \ (underlying_type) (expr)) -#else -#define unconstify(underlying_type, expr) \ - ((underlying_type) (expr)) -#define unvolatize(underlying_type, expr) \ - ((underlying_type) (expr)) +#endif + +/* + * SSE2 instructions are part of the spec for the 64-bit x86 ISA. We assume + * that compilers targeting this architecture understand SSE2 intrinsics. + */ +#if (defined(__x86_64__) || defined(_M_AMD64)) +#define USE_SSE2 + +/* + * We use the Neon instructions if the compiler provides access to them (as + * indicated by __ARM_NEON) and we are on aarch64. While Neon support is + * technically optional for aarch64, it appears that all available 64-bit + * hardware does have it. Neon exists in some 32-bit hardware too, but we + * could not realistically use it there without a run-time check, which seems + * not worth the trouble for now. + */ +#elif defined(__aarch64__) && defined(__ARM_NEON) +#define USE_NEON #endif /* ---------------------------------------------------------------- @@ -1256,7 +1386,7 @@ typedef union PGAlignedXLogBlock */ #if !HAVE_DECL_FDATASYNC -extern int fdatasync(int fildes); +extern int fdatasync(int fd); #endif /* @@ -1311,17 +1441,28 @@ extern int fdatasync(int fildes); #endif /* - * The following is used as the arg list for signal handlers. Any ports - * that take something other than an int argument should override this in - * their pg_config_os.h file. Note that variable names are required - * because it is used in both the prototypes as well as the definitions. - * Note also the long name. We expect that this won't collide with - * other names causing compiler warnings. + * Platform independent struct representing additional information about the + * received signal. If the system does not support the extended information, + * or a field does not apply to the signal, the value is instead reset to the + * documented default value. */ -#ifndef SIGNAL_ARGS -#define SIGNAL_ARGS int postgres_signal_arg -#endif +typedef struct pg_signal_info +{ + uint32_t pid; /* pid of sending process or 0 if unknown */ + uint32_t uid; /* uid of sending process; only meaningful + * when pid is not 0 */ +} pg_signal_info; + +/* + * The following is used as the arg list for signal handlers. These days we + * use the same argument to all signal handlers and hide the difference + * between platforms in wrapper functions. + * + * SIGNAL_ARGS just exists separately from the pqsignal() definition for + * historical reasons. + */ +#define SIGNAL_ARGS int postgres_signal_arg, const pg_signal_info *pg_siginfo /* * When there is no sigsetjmp, its functionality is provided by plain @@ -1344,6 +1485,29 @@ typedef intptr_t sigjmp_buf[5]; /* /port compatibility functions */ #include "port.h" +/* + * char16_t and char32_t + * Unicode code points. + * + * uchar.h should always be available in C11, but it's not available on + * Mac. However, these types are keywords in C++11, so when using C++, we + * can't redefine the types. + * + * XXX: when uchar.h is available everywhere, we can remove this check and + * just include uchar.h unconditionally. + * + * XXX: this section is out of place because uchar.h needs to be included + * after port.h, due to an interaction with win32_port.h in some cases. + */ +#ifdef HAVE_UCHAR_H +#include +#else +#ifndef __cplusplus +typedef uint16_t char16_t; +typedef uint32_t char32_t; +#endif +#endif + /* IWYU pragma: end_exports */ #endif /* C_H */ diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile index 2bbc7805fe37d..bab57372b8876 100644 --- a/src/include/catalog/Makefile +++ b/src/include/catalog/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/include/catalog # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/Makefile @@ -81,7 +81,12 @@ CATALOG_HEADERS := \ pg_publication_namespace.h \ pg_publication_rel.h \ pg_subscription.h \ - pg_subscription_rel.h + pg_subscription_rel.h \ + pg_propgraph_element.h \ + pg_propgraph_element_label.h \ + pg_propgraph_label.h \ + pg_propgraph_label_property.h \ + pg_propgraph_property.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) @@ -94,6 +99,7 @@ POSTGRES_BKI_DATA = \ pg_amop.dat \ pg_amproc.dat \ pg_authid.dat \ + pg_auth_members.dat \ pg_cast.dat \ pg_class.dat \ pg_collation.dat \ @@ -149,6 +155,7 @@ install: all installdirs ifeq ($(vpath_build),yes) $(INSTALL_DATA) schemapg.h '$(DESTDIR)$(includedir_server)'/catalog/schemapg.h $(INSTALL_DATA) syscache_ids.h '$(DESTDIR)$(includedir_server)'/catalog/syscache_ids.h + $(INSTALL_DATA) syscache_info.h '$(DESTDIR)$(includedir_server)'/catalog/syscache_info.h $(INSTALL_DATA) system_fk_info.h '$(DESTDIR)$(includedir_server)'/catalog/system_fk_info.h for file in $(GENERATED_HEADERS); do \ $(INSTALL_DATA) $$file '$(DESTDIR)$(includedir_server)'/catalog/$$file || exit; \ @@ -160,7 +167,7 @@ installdirs: uninstall: rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql) - rm -f $(addprefix '$(DESTDIR)$(includedir_server)'/catalog/, schemapg.h syscache_ids.h system_fk_info.h $(GENERATED_HEADERS)) + rm -f $(addprefix '$(DESTDIR)$(includedir_server)'/catalog/, schemapg.h syscache_ids.h syscache_info.h system_fk_info.h $(GENERATED_HEADERS)) clean: rm -f bki-stamp $(GENBKI_OUTPUT_FILES) diff --git a/src/include/catalog/README b/src/include/catalog/README new file mode 100644 index 0000000000000..7e0e5d7e70c87 --- /dev/null +++ b/src/include/catalog/README @@ -0,0 +1,2 @@ +See about the +files in this directory. diff --git a/src/include/catalog/binary_upgrade.h b/src/include/catalog/binary_upgrade.h index 6fcc59edebd86..7bf7ae4438593 100644 --- a/src/include/catalog/binary_upgrade.h +++ b/src/include/catalog/binary_upgrade.h @@ -4,7 +4,7 @@ * variables used for binary upgrades * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/binary_upgrade.h diff --git a/src/include/catalog/catalog.h b/src/include/catalog/catalog.h index 9b5e750b7e408..a9d6e8ea98663 100644 --- a/src/include/catalog/catalog.h +++ b/src/include/catalog/catalog.h @@ -4,7 +4,7 @@ * prototypes for functions in backend/catalog/catalog.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/catalog.h diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 82988d2443390..1602962dbe135 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -38,7 +38,7 @@ * parsenodes.h will warrant a catversion update. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202505071 +#define CATALOG_VERSION_NO 202604061 #endif diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 0ea7ccf524301..2f3c1eae3c71d 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -4,7 +4,7 @@ * Routines to support inter-object dependencies. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/dependency.h @@ -107,6 +107,8 @@ extern void ReleaseDeletionLock(const ObjectAddress *object); extern void performDeletion(const ObjectAddress *object, DropBehavior behavior, int flags); +extern void performDeletionCheck(const ObjectAddress *object, + DropBehavior behavior, int flags); extern void performMultipleDeletions(const ObjectAddresses *objects, DropBehavior behavior, int flags); @@ -114,12 +116,21 @@ extern void recordDependencyOnExpr(const ObjectAddress *depender, Node *expr, List *rtable, DependencyType behavior); +extern void collectDependenciesOfExpr(ObjectAddresses *addrs, + Node *expr, List *rtable); + extern void recordDependencyOnSingleRelExpr(const ObjectAddress *depender, Node *expr, Oid relId, DependencyType behavior, DependencyType self_behavior, bool reverse_self); +extern bool find_temp_object(const ObjectAddresses *addrs, + bool local_temp_okay, + ObjectAddress *foundobj); + +extern bool query_uses_temp_object(Query *query, ObjectAddress *temp_object); + extern ObjectAddresses *new_object_addresses(void); extern void add_exact_object_address(const ObjectAddress *object, @@ -175,6 +186,8 @@ extern long changeDependenciesOn(Oid refClassId, Oid oldRefObjectId, extern Oid getExtensionOfObject(Oid classId, Oid objectId); extern List *getAutoExtensionsOfObject(Oid classId, Oid objectId); +extern Oid getExtensionType(Oid extensionOid, const char *typname); + extern bool sequenceIsOwned(Oid seqId, char deptype, Oid *tableId, int32 *colId); extern List *getOwnedSequences(Oid relid); extern Oid getIdentitySequence(Relation rel, AttrNumber attnum, bool missing_ok); diff --git a/src/include/catalog/duplicate_oids b/src/include/catalog/duplicate_oids index b8ab4ed80bf29..11ff0b844f142 100755 --- a/src/include/catalog/duplicate_oids +++ b/src/include/catalog/duplicate_oids @@ -9,7 +9,7 @@ # the same catalog, our project policy is that manually assigned OIDs # should be globally unique, to avoid confusion. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/duplicate_oids diff --git a/src/include/catalog/genbki.h b/src/include/catalog/genbki.h index 26e205529dca7..12d2a3e295bee 100644 --- a/src/include/catalog/genbki.h +++ b/src/include/catalog/genbki.h @@ -9,7 +9,7 @@ * bootstrap file from these header files.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/genbki.h @@ -19,6 +19,25 @@ #ifndef GENBKI_H #define GENBKI_H +/* + * These macros should be written before and after each catalog structure + * definition. On most platforms they do nothing, but on some platforms + * we need special hacks to coax the compiler into laying out the catalog + * struct compatibly with our tuple forming/deforming rules. + * + * On AIX, where ALIGNOF_DOUBLE < ALIGNOF_INT64_T, we need to coerce int64 + * catalog fields to be aligned on just 4-byte boundaries. Ideally we'd + * write this like pack(push,ALIGNOF_DOUBLE), but gcc seems unwilling + * to take anything but a plain string literal as the argument of _Pragma. + */ +#if ALIGNOF_DOUBLE < ALIGNOF_INT64_T +#define BEGIN_CATALOG_STRUCT _Pragma("pack(push,4)") +#define END_CATALOG_STRUCT _Pragma("pack(pop)") +#else +#define BEGIN_CATALOG_STRUCT +#define END_CATALOG_STRUCT +#endif + /* Introduces a catalog's structure definition */ #define CATALOG(name,oid,oidmacro) typedef struct CppConcat(FormData_,name) diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index dbd339e9df4f2..6c9ac812aa02b 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -4,7 +4,7 @@ * prototypes for functions in backend/catalog/heap.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/heap.h @@ -117,7 +117,8 @@ extern List *AddRelationNewConstraints(Relation rel, const char *queryString); extern List *AddRelationNotNullConstraints(Relation rel, List *constraints, - List *old_notnulls); + List *old_notnulls, + List *existing_constraints); extern void RelationClearMissing(Relation rel); diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index 4daa8bef5eeab..9aee822634781 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -4,7 +4,7 @@ * prototypes for catalog/index.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/index.h @@ -18,6 +18,12 @@ #include "nodes/execnodes.h" +/* + * forward references in this file + */ +typedef struct AttrMap AttrMap; + + #define DEFAULT_INDEX_TYPE "btree" /* Action code for index_set_state_flags */ @@ -32,7 +38,7 @@ typedef enum /* options for REINDEX */ typedef struct ReindexParams { - bits32 options; /* bitmask of REINDEXOPT_* */ + uint32 options; /* bitmask of REINDEXOPT_* */ Oid tablespaceOid; /* New tablespace to move indexes to. * InvalidOid to do nothing. */ } ReindexParams; @@ -65,6 +71,7 @@ extern void index_check_primary_key(Relation heapRel, #define INDEX_CREATE_IF_NOT_EXISTS (1 << 4) #define INDEX_CREATE_PARTITIONED (1 << 5) #define INDEX_CREATE_INVALID (1 << 6) +#define INDEX_CREATE_SUPPRESS_PROGRESS (1 << 7) extern Oid index_create(Relation heapRelation, const char *indexRelationName, @@ -82,8 +89,8 @@ extern Oid index_create(Relation heapRelation, const int16 *coloptions, const NullableDatum *stattargets, Datum reloptions, - bits16 flags, - bits16 constr_flags, + uint16 flags, + uint16 constr_flags, bool allow_system_table_mods, bool is_internal, Oid *constraintId); @@ -95,10 +102,9 @@ extern Oid index_create(Relation heapRelation, #define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4) #define INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS (1 << 5) -extern Oid index_concurrently_create_copy(Relation heapRelation, - Oid oldIndexId, - Oid tablespaceOid, - const char *newName); +extern Oid index_create_copy(Relation heapRelation, uint16 flags, + Oid oldIndexId, Oid tablespaceOid, + const char *newName); extern void index_concurrently_build(Oid heapRelationId, Oid indexRelationId); @@ -116,7 +122,7 @@ extern ObjectAddress index_constraint_create(Relation heapRelation, const IndexInfo *indexInfo, const char *constraintName, char constraintType, - bits16 constr_flags, + uint16 constr_flags, bool allow_system_table_mods, bool is_internal); @@ -143,7 +149,8 @@ extern void index_build(Relation heapRelation, Relation indexRelation, IndexInfo *indexInfo, bool isreindex, - bool parallel); + bool parallel, + bool progress); extern void validate_index(Oid heapId, Oid indexId, Snapshot snapshot); @@ -187,7 +194,7 @@ extern void IndexSetParentIndex(Relation partitionIdx, Oid parentOid); * As noted in validate_index(), this can be significantly faster. */ static inline int64 -itemptr_encode(ItemPointer itemptr) +itemptr_encode(const ItemPointerData *itemptr) { BlockNumber block = ItemPointerGetBlockNumber(itemptr); OffsetNumber offset = ItemPointerGetOffsetNumber(itemptr); diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h index 667aca7ace801..58fc185d0d8cb 100644 --- a/src/include/catalog/indexing.h +++ b/src/include/catalog/indexing.h @@ -5,7 +5,7 @@ * on system catalogs * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/indexing.h @@ -16,7 +16,7 @@ #define INDEXING_H #include "access/htup.h" -#include "nodes/execnodes.h" +#include "executor/tuptable.h" #include "utils/relcache.h" /* @@ -44,11 +44,11 @@ extern void CatalogTuplesMultiInsertWithInfo(Relation heapRel, TupleTableSlot **slot, int ntuples, CatalogIndexState indstate); -extern void CatalogTupleUpdate(Relation heapRel, ItemPointer otid, +extern void CatalogTupleUpdate(Relation heapRel, const ItemPointerData *otid, HeapTuple tup); extern void CatalogTupleUpdateWithInfo(Relation heapRel, - ItemPointer otid, HeapTuple tup, + const ItemPointerData *otid, HeapTuple tup, CatalogIndexState indstate); -extern void CatalogTupleDelete(Relation heapRel, ItemPointer tid); +extern void CatalogTupleDelete(Relation heapRel, const ItemPointerData *tid); #endif /* INDEXING_H */ diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build index ec1cf467f6fa0..fa836e4ee253d 100644 --- a/src/include/catalog/meson.build +++ b/src/include/catalog/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Note: the order of this list determines the order in which the catalog # header files are assembled into postgres.bki. BKI_BOOTSTRAP catalogs @@ -69,6 +69,11 @@ catalog_headers = [ 'pg_publication_rel.h', 'pg_subscription.h', 'pg_subscription_rel.h', + 'pg_propgraph_element.h', + 'pg_propgraph_element_label.h', + 'pg_propgraph_label.h', + 'pg_propgraph_label_property.h', + 'pg_propgraph_property.h', ] # The .dat files we need can just be listed alphabetically. @@ -78,6 +83,7 @@ bki_data = [ 'pg_amop.dat', 'pg_amproc.dat', 'pg_authid.dat', + 'pg_auth_members.dat', 'pg_cast.dat', 'pg_class.dat', 'pg_collation.dat', @@ -115,7 +121,7 @@ output_install = [ dir_data, dir_include_server / 'catalog', dir_include_server / 'catalog', - false, + dir_include_server / 'catalog', dir_include_server / 'catalog', ] diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h index 8c7ccc69a3c3d..9453a3e493297 100644 --- a/src/include/catalog/namespace.h +++ b/src/include/catalog/namespace.h @@ -4,7 +4,7 @@ * prototypes for functions in backend/catalog/namespace.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/namespace.h @@ -15,7 +15,7 @@ #define NAMESPACE_H #include "nodes/primnodes.h" -#include "storage/lock.h" +#include "storage/lockdefs.h" #include "storage/procnumber.h" @@ -39,6 +39,24 @@ typedef struct _FuncCandidateList Oid args[FLEXIBLE_ARRAY_MEMBER]; /* arg types */ } *FuncCandidateList; +/* + * FuncnameGetCandidates also returns a bitmask containing these flags, + * which report on what it found or didn't find. They can help callers + * produce better error reports after a function lookup failure. + */ +#define FGC_SCHEMA_GIVEN 0x0001 /* Func name includes a schema */ +#define FGC_SCHEMA_EXISTS 0x0002 /* Found the explicitly-specified schema */ +#define FGC_NAME_EXISTS 0x0004 /* Found a routine by that name */ +#define FGC_NAME_VISIBLE 0x0008 /* Found a routine name/schema match */ +#define FGC_ARGCOUNT_MATCH 0x0010 /* Found a func with right # of args */ +/* These bits relate only to calls using named or mixed arguments: */ +#define FGC_ARGNAMES_MATCH 0x0020 /* Found a func matching all argnames */ +#define FGC_ARGNAMES_NONDUP 0x0040 /* argnames don't overlap positional args */ +#define FGC_ARGNAMES_ALL 0x0080 /* Found a func with no missing args */ +#define FGC_ARGNAMES_VALID 0x0100 /* Found a fully-valid use of argnames */ +/* These bits are actually filled by func_get_detail: */ +#define FGC_VARIADIC_FAIL 0x0200 /* Disallowed VARIADIC with named args */ + /* * Result of checkTempNamespaceStatus */ @@ -102,12 +120,14 @@ extern FuncCandidateList FuncnameGetCandidates(List *names, bool expand_variadic, bool expand_defaults, bool include_out_arguments, - bool missing_ok); + bool missing_ok, + int *fgc_flags); extern bool FunctionIsVisible(Oid funcid); extern Oid OpernameGetOprid(List *names, Oid oprleft, Oid oprright); extern FuncCandidateList OpernameGetCandidates(List *names, char oprkind, - bool missing_schema_ok); + bool missing_schema_ok, + int *fgc_flags); extern bool OperatorIsVisible(Oid oprid); extern Oid OpclassnameGetOpcid(Oid amid, const char *opcname); diff --git a/src/include/catalog/objectaccess.h b/src/include/catalog/objectaccess.h index 126d9c96aef0f..d6f4f167479fc 100644 --- a/src/include/catalog/objectaccess.h +++ b/src/include/catalog/objectaccess.h @@ -3,7 +3,7 @@ * * Object access hooks. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California */ diff --git a/src/include/catalog/objectaddress.h b/src/include/catalog/objectaddress.h index 630434b73cf1a..1f965e1faef41 100644 --- a/src/include/catalog/objectaddress.h +++ b/src/include/catalog/objectaddress.h @@ -3,7 +3,7 @@ * objectaddress.h * functions for working with object addresses * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/objectaddress.h @@ -14,6 +14,7 @@ #define OBJECTADDRESS_H #include "access/htup.h" +#include "catalog/syscache_ids.h" #include "nodes/parsenodes.h" #include "storage/lockdefs.h" #include "utils/relcache.h" @@ -57,8 +58,8 @@ extern Oid get_object_namespace(const ObjectAddress *address); extern bool is_objectclass_supported(Oid class_id); extern const char *get_object_class_descr(Oid class_id); extern Oid get_object_oid_index(Oid class_id); -extern int get_object_catcache_oid(Oid class_id); -extern int get_object_catcache_name(Oid class_id); +extern SysCacheIdentifier get_object_catcache_oid(Oid class_id); +extern SysCacheIdentifier get_object_catcache_name(Oid class_id); extern AttrNumber get_object_attnum_oid(Oid class_id); extern AttrNumber get_object_attnum_name(Oid class_id); extern AttrNumber get_object_attnum_namespace(Oid class_id); diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h index 520374af90337..a93cf081dd2f1 100644 --- a/src/include/catalog/partition.h +++ b/src/include/catalog/partition.h @@ -4,7 +4,7 @@ * Header file for structures and utility functions related to * partitioning * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/include/catalog/partition.h * diff --git a/src/include/catalog/pg_aggregate.dat b/src/include/catalog/pg_aggregate.dat index d6aa1f6ec4787..75d4604a0b212 100644 --- a/src/include/catalog/pg_aggregate.dat +++ b/src/include/catalog/pg_aggregate.dat @@ -3,7 +3,7 @@ # pg_aggregate.dat # Initial contents of the pg_aggregate system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_aggregate.dat @@ -104,6 +104,9 @@ { aggfnoid => 'max(oid)', aggtransfn => 'oidlarger', aggcombinefn => 'oidlarger', aggsortop => '>(oid,oid)', aggtranstype => 'oid' }, +{ aggfnoid => 'max(oid8)', aggtransfn => 'oid8larger', + aggcombinefn => 'oid8larger', aggsortop => '>(oid8,oid8)', + aggtranstype => 'oid8' }, { aggfnoid => 'max(float4)', aggtransfn => 'float4larger', aggcombinefn => 'float4larger', aggsortop => '>(float4,float4)', aggtranstype => 'float4' }, @@ -178,6 +181,9 @@ { aggfnoid => 'min(oid)', aggtransfn => 'oidsmaller', aggcombinefn => 'oidsmaller', aggsortop => '<(oid,oid)', aggtranstype => 'oid' }, +{ aggfnoid => 'min(oid8)', aggtransfn => 'oid8smaller', + aggcombinefn => 'oid8smaller', aggsortop => '<(oid8,oid8)', + aggtranstype => 'oid8' }, { aggfnoid => 'min(float4)', aggtransfn => 'float4smaller', aggcombinefn => 'float4smaller', aggsortop => '<(float4,float4)', aggtranstype => 'float4' }, @@ -475,37 +481,37 @@ aggcombinefn => 'int8pl', aggtranstype => 'int8', agginitval => '0' }, { aggfnoid => 'regr_sxx', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_sxx', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_syy', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_syy', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_sxy', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_sxy', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_avgx', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_avgx', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_avgy', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_avgy', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_r2', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_r2', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_slope', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_slope', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'regr_intercept', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_regr_intercept', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'covar_pop', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_covar_pop', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'covar_samp', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_covar_samp', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, { aggfnoid => 'corr', aggtransfn => 'float8_regr_accum', aggfinalfn => 'float8_corr', aggcombinefn => 'float8_regr_combine', - aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0}' }, + aggtranstype => '_float8', agginitval => '{0,0,0,0,0,0,0,0}' }, # boolean-and and boolean-or { aggfnoid => 'bool_and', aggtransfn => 'booland_statefunc', @@ -558,26 +564,28 @@ aggfinalfn => 'array_agg_finalfn', aggcombinefn => 'array_agg_combine', aggserialfn => 'array_agg_serialize', aggdeserialfn => 'array_agg_deserialize', aggfinalextra => 't', - aggtranstype => 'internal' }, + aggtranstype => 'internal', aggtransspace => '-1' }, { aggfnoid => 'array_agg(anyarray)', aggtransfn => 'array_agg_array_transfn', aggfinalfn => 'array_agg_array_finalfn', aggcombinefn => 'array_agg_array_combine', aggserialfn => 'array_agg_array_serialize', aggdeserialfn => 'array_agg_array_deserialize', aggfinalextra => 't', - aggtranstype => 'internal' }, + aggtranstype => 'internal', aggtransspace => '-1' }, # text { aggfnoid => 'string_agg(text,text)', aggtransfn => 'string_agg_transfn', aggfinalfn => 'string_agg_finalfn', aggcombinefn => 'string_agg_combine', aggserialfn => 'string_agg_serialize', - aggdeserialfn => 'string_agg_deserialize', aggtranstype => 'internal' }, + aggdeserialfn => 'string_agg_deserialize', + aggtranstype => 'internal', aggtransspace => '-1' }, # bytea { aggfnoid => 'string_agg(bytea,bytea)', aggtransfn => 'bytea_string_agg_transfn', aggfinalfn => 'bytea_string_agg_finalfn', aggcombinefn => 'string_agg_combine', aggserialfn => 'string_agg_serialize', - aggdeserialfn => 'string_agg_deserialize', aggtranstype => 'internal' }, + aggdeserialfn => 'string_agg_deserialize', + aggtranstype => 'internal', aggtransspace => '-1' }, # range { aggfnoid => 'range_intersect_agg(anyrange)', diff --git a/src/include/catalog/pg_aggregate.h b/src/include/catalog/pg_aggregate.h index c15f8d2c9fc74..2b4f5dae5f291 100644 --- a/src/include/catalog/pg_aggregate.h +++ b/src/include/catalog/pg_aggregate.h @@ -4,7 +4,7 @@ * definition of the "aggregate" system catalog (pg_aggregate) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_aggregate.h @@ -29,6 +29,8 @@ * cpp turns this into typedef struct FormData_pg_aggregate * ---------------------------------------------------------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_aggregate,2600,AggregateRelationId) { /* pg_proc OID of the aggregate itself */ @@ -101,6 +103,8 @@ CATALOG(pg_aggregate,2600,AggregateRelationId) #endif } FormData_pg_aggregate; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_aggregate corresponds to a pointer to a tuple with * the format of pg_aggregate relation. diff --git a/src/include/catalog/pg_am.dat b/src/include/catalog/pg_am.dat index 26d15928a1557..46d361047fe67 100644 --- a/src/include/catalog/pg_am.dat +++ b/src/include/catalog/pg_am.dat @@ -3,7 +3,7 @@ # pg_am.dat # Initial contents of the pg_am system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_am.dat diff --git a/src/include/catalog/pg_am.h b/src/include/catalog/pg_am.h index 6e98a0930c271..62bc3fb820664 100644 --- a/src/include/catalog/pg_am.h +++ b/src/include/catalog/pg_am.h @@ -4,7 +4,7 @@ * definition of the "access method" system catalog (pg_am) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_am.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_am * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_am,2601,AccessMethodRelationId) { Oid oid; /* oid */ @@ -40,6 +42,8 @@ CATALOG(pg_am,2601,AccessMethodRelationId) char amtype; } FormData_pg_am; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_am corresponds to a pointer to a tuple with * the format of pg_am relation. diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat index 2a693cfc31c6f..8d5a0004a478a 100644 --- a/src/include/catalog/pg_amop.dat +++ b/src/include/catalog/pg_amop.dat @@ -3,7 +3,7 @@ # pg_amop.dat # Initial contents of the pg_amop system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_amop.dat @@ -180,6 +180,24 @@ { amopfamily => 'btree/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid', amopstrategy => '5', amopopr => '>(oid,oid)', amopmethod => 'btree' }, +# btree oid8_ops + +{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8', + amoprighttype => 'oid8', amopstrategy => '1', amopopr => '<(oid8,oid8)', + amopmethod => 'btree' }, +{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8', + amoprighttype => 'oid8', amopstrategy => '2', amopopr => '<=(oid8,oid8)', + amopmethod => 'btree' }, +{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8', + amoprighttype => 'oid8', amopstrategy => '3', amopopr => '=(oid8,oid8)', + amopmethod => 'btree' }, +{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8', + amoprighttype => 'oid8', amopstrategy => '4', amopopr => '>=(oid8,oid8)', + amopmethod => 'btree' }, +{ amopfamily => 'btree/oid8_ops', amoplefttype => 'oid8', + amoprighttype => 'oid8', amopstrategy => '5', amopopr => '>(oid8,oid8)', + amopmethod => 'btree' }, + # btree xid8_ops { amopfamily => 'btree/xid8_ops', amoplefttype => 'xid8', @@ -974,6 +992,11 @@ { amopfamily => 'hash/oid_ops', amoplefttype => 'oid', amoprighttype => 'oid', amopstrategy => '1', amopopr => '=(oid,oid)', amopmethod => 'hash' }, +# oid8_ops +{ amopfamily => 'hash/oid8_ops', amoplefttype => 'oid8', + amoprighttype => 'oid8', amopstrategy => '1', amopopr => '=(oid8,oid8)', + amopmethod => 'hash' }, + # oidvector_ops { amopfamily => 'hash/oidvector_ops', amoplefttype => 'oidvector', amoprighttype => 'oidvector', amopstrategy => '1', diff --git a/src/include/catalog/pg_amop.h b/src/include/catalog/pg_amop.h index cc71cab498b7a..ebc774d73fb37 100644 --- a/src/include/catalog/pg_amop.h +++ b/src/include/catalog/pg_amop.h @@ -29,7 +29,7 @@ * intentional denormalization of the catalogs to buy lookup speed. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_amop.h @@ -51,6 +51,8 @@ * typedef struct FormData_pg_amop * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_amop,2602,AccessMethodOperatorRelationId) { Oid oid; /* oid */ @@ -80,6 +82,8 @@ CATALOG(pg_amop,2602,AccessMethodOperatorRelationId) Oid amopsortfamily BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_opfamily); } FormData_pg_amop; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_amop corresponds to a pointer to a tuple with * the format of pg_amop relation. diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat index 9250514899844..4a1efdbc89986 100644 --- a/src/include/catalog/pg_amproc.dat +++ b/src/include/catalog/pg_amproc.dat @@ -3,7 +3,7 @@ # pg_amproc.dat # Initial contents of the pg_amproc system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_amproc.dat @@ -213,6 +213,14 @@ amprocrighttype => 'oid', amprocnum => '4', amproc => 'btequalimage' }, { amprocfamily => 'btree/oid_ops', amproclefttype => 'oid', amprocrighttype => 'oid', amprocnum => '6', amproc => 'btoidskipsupport' }, +{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8', + amprocrighttype => 'oid8', amprocnum => '1', amproc => 'btoid8cmp' }, +{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8', + amprocrighttype => 'oid8', amprocnum => '2', amproc => 'btoid8sortsupport' }, +{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8', + amprocrighttype => 'oid8', amprocnum => '4', amproc => 'btequalimage' }, +{ amprocfamily => 'btree/oid8_ops', amproclefttype => 'oid8', + amprocrighttype => 'oid8', amprocnum => '6', amproc => 'btoid8skipsupport' }, { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector', amprocrighttype => 'oidvector', amprocnum => '1', amproc => 'btoidvectorcmp' }, @@ -432,6 +440,10 @@ amprocrighttype => 'xid8', amprocnum => '1', amproc => 'hashxid8' }, { amprocfamily => 'hash/xid8_ops', amproclefttype => 'xid8', amprocrighttype => 'xid8', amprocnum => '2', amproc => 'hashxid8extended' }, +{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8', + amprocrighttype => 'oid8', amprocnum => '1', amproc => 'hashoid8' }, +{ amprocfamily => 'hash/oid8_ops', amproclefttype => 'oid8', + amprocrighttype => 'oid8', amprocnum => '2', amproc => 'hashoid8extended' }, { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid', amprocrighttype => 'cid', amprocnum => '1', amproc => 'hashcid' }, { amprocfamily => 'hash/cid_ops', amproclefttype => 'cid', @@ -533,7 +545,7 @@ amprocrighttype => 'box', amprocnum => '8', amproc => 'gist_box_distance' }, { amprocfamily => 'gist/box_ops', amproclefttype => 'any', amprocrighttype => 'any', amprocnum => '12', - amproc => 'gist_stratnum_common' }, + amproc => 'gist_translate_cmptype_common' }, { amprocfamily => 'gist/poly_ops', amproclefttype => 'polygon', amprocrighttype => 'polygon', amprocnum => '1', amproc => 'gist_poly_consistent' }, @@ -555,7 +567,7 @@ amproc => 'gist_poly_distance' }, { amprocfamily => 'gist/poly_ops', amproclefttype => 'any', amprocrighttype => 'any', amprocnum => '12', - amproc => 'gist_stratnum_common' }, + amproc => 'gist_translate_cmptype_common' }, { amprocfamily => 'gist/circle_ops', amproclefttype => 'circle', amprocrighttype => 'circle', amprocnum => '1', amproc => 'gist_circle_consistent' }, @@ -576,7 +588,7 @@ amproc => 'gist_circle_distance' }, { amprocfamily => 'gist/circle_ops', amproclefttype => 'any', amprocrighttype => 'any', amprocnum => '12', - amproc => 'gist_stratnum_common' }, + amproc => 'gist_translate_cmptype_common' }, { amprocfamily => 'gist/tsvector_ops', amproclefttype => 'tsvector', amprocrighttype => 'tsvector', amprocnum => '1', amproc => 'gtsvector_consistent(internal,tsvector,int2,oid,internal)' }, @@ -636,7 +648,7 @@ amproc => 'range_sortsupport' }, { amprocfamily => 'gist/range_ops', amproclefttype => 'any', amprocrighttype => 'any', amprocnum => '12', - amproc => 'gist_stratnum_common' }, + amproc => 'gist_translate_cmptype_common' }, { amprocfamily => 'gist/network_ops', amproclefttype => 'inet', amprocrighttype => 'inet', amprocnum => '1', amproc => 'inet_gist_consistent' }, @@ -655,7 +667,7 @@ amprocrighttype => 'inet', amprocnum => '9', amproc => 'inet_gist_fetch' }, { amprocfamily => 'gist/network_ops', amproclefttype => 'any', amprocrighttype => 'any', amprocnum => '12', - amproc => 'gist_stratnum_common' }, + amproc => 'gist_translate_cmptype_common' }, { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange', amprocrighttype => 'anymultirange', amprocnum => '1', amproc => 'multirange_gist_consistent' }, @@ -676,7 +688,7 @@ amproc => 'range_gist_same' }, { amprocfamily => 'gist/multirange_ops', amproclefttype => 'any', amprocrighttype => 'any', amprocnum => '12', - amproc => 'gist_stratnum_common' }, + amproc => 'gist_translate_cmptype_common' }, # gin { amprocfamily => 'gin/array_ops', amproclefttype => 'anyarray', diff --git a/src/include/catalog/pg_amproc.h b/src/include/catalog/pg_amproc.h index bf8de4618c751..3a3869e75a970 100644 --- a/src/include/catalog/pg_amproc.h +++ b/src/include/catalog/pg_amproc.h @@ -18,7 +18,7 @@ * some don't pay attention to non-default functions at all. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_amproc.h @@ -40,6 +40,8 @@ * typedef struct FormData_pg_amproc * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_amproc,2603,AccessMethodProcedureRelationId) { Oid oid; /* oid */ @@ -60,6 +62,8 @@ CATALOG(pg_amproc,2603,AccessMethodProcedureRelationId) regproc amproc BKI_LOOKUP(pg_proc); } FormData_pg_amproc; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_amproc corresponds to a pointer to a tuple with * the format of pg_amproc relation. diff --git a/src/include/catalog/pg_attrdef.h b/src/include/catalog/pg_attrdef.h index 3067a8357c038..8bbc3b8882769 100644 --- a/src/include/catalog/pg_attrdef.h +++ b/src/include/catalog/pg_attrdef.h @@ -4,7 +4,7 @@ * definition of the "attribute defaults" system catalog (pg_attrdef) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_attrdef.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_attrdef * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_attrdef,2604,AttrDefaultRelationId) { Oid oid; /* oid */ @@ -41,6 +43,8 @@ CATALOG(pg_attrdef,2604,AttrDefaultRelationId) #endif } FormData_pg_attrdef; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_attrdef corresponds to a pointer to a tuple with * the format of pg_attrdef relation. diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h index c612aed620f63..f33a57573f26d 100644 --- a/src/include/catalog/pg_attribute.h +++ b/src/include/catalog/pg_attribute.h @@ -8,7 +8,7 @@ * relations need be included. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_attribute.h @@ -34,6 +34,8 @@ * You may need to change catalog/genbki.pl as well. * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,AttributeRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid attrelid BKI_LOOKUP(pg_class); /* OID of relation containing @@ -185,6 +187,8 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75, #endif } FormData_pg_attribute; +END_CATALOG_STRUCT + /* * ATTRIBUTE_FIXED_PART_SIZE is the size of the fixed-layout, * guaranteed-not-null part of a pg_attribute row. This is in fact as much diff --git a/src/include/catalog/pg_auth_members.dat b/src/include/catalog/pg_auth_members.dat new file mode 100644 index 0000000000000..246373251d431 --- /dev/null +++ b/src/include/catalog/pg_auth_members.dat @@ -0,0 +1,20 @@ +#---------------------------------------------------------------------- +# +# pg_auth_members.dat +# Initial contents of the pg_auth_members system catalog. +# +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/include/catalog/pg_auth_members.dat +# +#---------------------------------------------------------------------- + +[ + +# We grant these roles to pg_monitor. +{ roleid => 'pg_read_all_settings', member => 'pg_monitor' }, +{ roleid => 'pg_read_all_stats', member => 'pg_monitor' }, +{ roleid => 'pg_stat_scan_tables', member => 'pg_monitor' }, + +] diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h index 387316e44f079..b029cc575a5cc 100644 --- a/src/include/catalog/pg_auth_members.h +++ b/src/include/catalog/pg_auth_members.h @@ -5,7 +5,7 @@ * (pg_auth_members). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_auth_members.h @@ -27,17 +27,34 @@ * typedef struct FormData_pg_auth_members * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO { - Oid oid; /* oid */ - Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */ - Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */ - Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */ - bool admin_option; /* granted with admin option? */ - bool inherit_option; /* exercise privileges without SET ROLE? */ - bool set_option; /* use SET ROLE to the target role? */ + /* OID for this record (needed for dependencies) */ + Oid oid; + + /* ID of a role */ + Oid roleid BKI_LOOKUP(pg_authid); + + /* ID of a member of that role */ + Oid member BKI_LOOKUP(pg_authid); + + /* who granted the membership */ + Oid grantor BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); + + /* granted with admin option? */ + bool admin_option BKI_DEFAULT(f); + + /* exercise privileges without SET ROLE? */ + bool inherit_option BKI_DEFAULT(t); + + /* use SET ROLE to the target role? */ + bool set_option BKI_DEFAULT(t); } FormData_pg_auth_members; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_auth_members corresponds to a pointer to a tuple with * the format of pg_auth_members relation. diff --git a/src/include/catalog/pg_authid.dat b/src/include/catalog/pg_authid.dat index eb4dab5c6aa77..2960707c7293a 100644 --- a/src/include/catalog/pg_authid.dat +++ b/src/include/catalog/pg_authid.dat @@ -3,7 +3,7 @@ # pg_authid.dat # Initial contents of the pg_authid system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_authid.dat @@ -16,8 +16,10 @@ # so make sure every entry has an oid_symbol value. # The bootstrap superuser is named POSTGRES according to this data and -# according to BKI_DEFAULT entries in other catalogs. However, initdb -# will replace that at database initialization time. +# according to BKI_DEFAULT entries and ACL entries in other catalogs. +# initdb will replace that at database initialization time for standalone +# references (the rolname below and various owner-OID columns), while +# boot_get_role_oid() deals with it for ACL entries. { oid => '10', oid_symbol => 'BOOTSTRAP_SUPERUSERID', rolname => 'POSTGRES', rolsuper => 't', rolinherit => 't', @@ -99,7 +101,7 @@ rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', rolpassword => '_null_', rolvaliduntil => '_null_' }, -{ oid => '8916', oid_symbol => 'ROLE_PG_SIGNAL_AUTOVACUUM_WORKER', +{ oid => '6392', oid_symbol => 'ROLE_PG_SIGNAL_AUTOVACUUM_WORKER', rolname => 'pg_signal_autovacuum_worker', rolsuper => 'f', rolinherit => 't', rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f', rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1', diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h index b2f3e9d01eec9..baf5b09979790 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -6,7 +6,7 @@ * pg_shadow and pg_group are now views on pg_authid. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_authid.h @@ -28,6 +28,8 @@ * typedef struct FormData_pg_authid * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2842,AuthIdRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid oid; /* oid */ @@ -48,6 +50,8 @@ CATALOG(pg_authid,1260,AuthIdRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(284 #endif } FormData_pg_authid; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_authid corresponds to a pointer to a tuple with * the format of pg_authid relation. diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat index ab46be606f03d..a7b6d812c5ac9 100644 --- a/src/include/catalog/pg_cast.dat +++ b/src/include/catalog/pg_cast.dat @@ -3,7 +3,7 @@ # pg_cast.dat # Initial contents of the pg_cast system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_cast.dat @@ -281,6 +281,34 @@ castcontext => 'a', castmethod => 'f' }, { castsource => 'regnamespace', casttarget => 'int4', castfunc => '0', castcontext => 'a', castmethod => 'b' }, +{ castsource => 'oid', casttarget => 'regdatabase', castfunc => '0', + castcontext => 'i', castmethod => 'b' }, +{ castsource => 'regdatabase', casttarget => 'oid', castfunc => '0', + castcontext => 'i', castmethod => 'b' }, +{ castsource => 'int8', casttarget => 'regdatabase', castfunc => 'oid', + castcontext => 'i', castmethod => 'f' }, +{ castsource => 'int2', casttarget => 'regdatabase', castfunc => 'int4(int2)', + castcontext => 'i', castmethod => 'f' }, +{ castsource => 'int4', casttarget => 'regdatabase', castfunc => '0', + castcontext => 'i', castmethod => 'b' }, +{ castsource => 'regdatabase', casttarget => 'int8', castfunc => 'int8(oid)', + castcontext => 'a', castmethod => 'f' }, +{ castsource => 'regdatabase', casttarget => 'int4', castfunc => '0', + castcontext => 'a', castmethod => 'b' }, + +# OID8 category: allow implicit conversion from any integral type (including +# int8), as well as assignment coercion to int8. +{ castsource => 'int8', casttarget => 'oid8', castfunc => '0', + castcontext => 'i', castmethod => 'b' }, +{ castsource => 'int2', casttarget => 'oid8', castfunc => 'int8(int2)', + castcontext => 'i', castmethod => 'f' }, +{ castsource => 'int4', casttarget => 'oid8', castfunc => 'int8(int4)', + castcontext => 'i', castmethod => 'f' }, +{ castsource => 'oid8', casttarget => 'int8', castfunc => '0', + castcontext => 'a', castmethod => 'b' }, +# Assignment coercion from oid to oid8. +{ castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)', + castcontext => 'a', castmethod => 'f' }, # String category { castsource => 'text', casttarget => 'bpchar', castfunc => '0', @@ -334,6 +362,12 @@ { castsource => 'bytea', casttarget => 'int8', castfunc => 'int8(bytea)', castcontext => 'e', castmethod => 'f' }, +# Allow explicit coercions between bytea and uuid type +{ castsource => 'bytea', casttarget => 'uuid', castfunc => 'uuid(bytea)', + castcontext => 'e', castmethod => 'f' }, +{ castsource => 'uuid', casttarget => 'bytea', castfunc => 'bytea(uuid)', + castcontext => 'e', castmethod => 'f' }, + # Allow explicit coercions between int4 and "char" { castsource => 'char', casttarget => 'int4', castfunc => 'int4(char)', castcontext => 'e', castmethod => 'f' }, diff --git a/src/include/catalog/pg_cast.h b/src/include/catalog/pg_cast.h index 6a0ca3371534e..d81314946eeb6 100644 --- a/src/include/catalog/pg_cast.h +++ b/src/include/catalog/pg_cast.h @@ -6,7 +6,7 @@ * As of Postgres 8.0, pg_cast describes not only type coercion functions * but also length coercion functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_cast.h @@ -29,6 +29,8 @@ * typedef struct FormData_pg_cast * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_cast,2605,CastRelationId) { Oid oid; /* oid */ @@ -49,6 +51,8 @@ CATALOG(pg_cast,2605,CastRelationId) char castmethod; } FormData_pg_cast; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_cast corresponds to a pointer to a tuple with * the format of pg_cast relation. diff --git a/src/include/catalog/pg_class.dat b/src/include/catalog/pg_class.dat index 8c89ad1bc2ea8..438a84e1edcde 100644 --- a/src/include/catalog/pg_class.dat +++ b/src/include/catalog/pg_class.dat @@ -3,7 +3,7 @@ # pg_class.dat # Initial contents of the pg_class system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_class.dat diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h index 07d182da796a0..c4af599dc906d 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -4,7 +4,7 @@ * definition of the "relation" system catalog (pg_class) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_class.h @@ -29,6 +29,8 @@ * BKI_BOOTSTRAP catalogs, since only those rows appear in pg_class.dat. * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,RelationRelation_Rowtype_Id) BKI_SCHEMA_MACRO { /* oid */ @@ -144,6 +146,8 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat #endif } FormData_pg_class; +END_CATALOG_STRUCT + /* Size of fixed part of pg_class tuples, not counting var-length fields */ #define CLASS_TUPLE_SIZE \ (offsetof(FormData_pg_class,relminmxid) + sizeof(TransactionId)) @@ -174,6 +178,7 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128); #define RELKIND_FOREIGN_TABLE 'f' /* foreign table */ #define RELKIND_PARTITIONED_TABLE 'p' /* partitioned table */ #define RELKIND_PARTITIONED_INDEX 'I' /* partitioned index */ +#define RELKIND_PROPGRAPH 'g' /* property graph */ #define RELPERSISTENCE_PERMANENT 'p' /* regular table */ #define RELPERSISTENCE_UNLOGGED 'u' /* unlogged permanent table */ diff --git a/src/include/catalog/pg_collation.dat b/src/include/catalog/pg_collation.dat index fb76c421931ea..0a3fc1c9a52aa 100644 --- a/src/include/catalog/pg_collation.dat +++ b/src/include/catalog/pg_collation.dat @@ -3,7 +3,7 @@ # pg_collation.dat # Initial contents of the pg_collation system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_collation.dat @@ -33,7 +33,8 @@ descr => 'sorts by Unicode code point; Unicode and POSIX character semantics', collname => 'pg_c_utf8', collprovider => 'b', collencoding => '6', colllocale => 'C.UTF-8', collversion => '1' }, -{ oid => '9535', descr => 'sorts by Unicode code point; Unicode character semantics', +{ oid => '6411', + descr => 'sorts by Unicode code point; Unicode character semantics', collname => 'pg_unicode_fast', collprovider => 'b', collencoding => '6', colllocale => 'PG_UNICODE_FAST', collversion => '1' }, diff --git a/src/include/catalog/pg_collation.h b/src/include/catalog/pg_collation.h index 7c5d5b11300b9..8b1917ee99fe9 100644 --- a/src/include/catalog/pg_collation.h +++ b/src/include/catalog/pg_collation.h @@ -4,7 +4,7 @@ * definition of the "collation" system catalog (pg_collation) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_collation.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_collation * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_collation,3456,CollationRelationId) { Oid oid; /* oid */ @@ -50,6 +52,8 @@ CATALOG(pg_collation,3456,CollationRelationId) #endif } FormData_pg_collation; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_collation corresponds to a pointer to a row with * the format of pg_collation relation. diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 4afceb5c692d2..1b7fedf1750e5 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -4,7 +4,7 @@ * definition of the "constraint" system catalog (pg_constraint) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_constraint.h @@ -28,6 +28,8 @@ * typedef struct FormData_pg_constraint * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_constraint,2606,ConstraintRelationId) { Oid oid; /* oid */ @@ -167,6 +169,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) #endif } FormData_pg_constraint; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_constraint corresponds to a pointer to a tuple with * the format of pg_constraint relation. @@ -263,7 +267,7 @@ extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum); extern HeapTuple findNotNullConstraint(Oid relid, const char *colname); extern HeapTuple findDomainNotNullConstraint(Oid typid); extern AttrNumber extractNotNullColumn(HeapTuple constrTup); -extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum, +extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum, const char *new_conname, bool is_local, bool is_no_inherit, bool is_notvalid); extern List *RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh); diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h index 63e834a6ce477..80b3a730e035a 100644 --- a/src/include/catalog/pg_control.h +++ b/src/include/catalog/pg_control.h @@ -5,7 +5,7 @@ * However, we define it here so that the format is documented. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_control.h @@ -22,7 +22,7 @@ /* Version identifier for this pg_control format */ -#define PG_CONTROL_VERSION 1800 +#define PG_CONTROL_VERSION 1902 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 @@ -41,6 +41,7 @@ typedef struct CheckPoint * timeline (equals ThisTimeLineID otherwise) */ bool fullPageWrites; /* current full_page_writes */ int wal_level; /* current wal_level */ + bool logicalDecodingEnabled; /* current logical decoding status */ FullTransactionId nextXid; /* next free transaction ID */ Oid nextOid; /* next free OID */ MultiXactId nextMulti; /* next free MultiXactId */ @@ -62,6 +63,9 @@ typedef struct CheckPoint * set to InvalidTransactionId. */ TransactionId oldestActiveXid; + + /* data checksums state at the time of the checkpoint */ + uint32 dataChecksumState; } CheckPoint; /* XLOG info values for XLOG rmgr */ @@ -77,9 +81,13 @@ typedef struct CheckPoint #define XLOG_END_OF_RECOVERY 0x90 #define XLOG_FPI_FOR_HINT 0xA0 #define XLOG_FPI 0xB0 -/* 0xC0 is used in Postgres 9.5-11 */ +#define XLOG_ASSIGN_LSN 0xC0 #define XLOG_OVERWRITE_CONTRECORD 0xD0 #define XLOG_CHECKPOINT_REDO 0xE0 +#define XLOG_LOGICAL_DECODING_STATUS_CHANGE 0xF0 + +/* XLOG info values for XLOG2 rmgr */ +#define XLOG2_CHECKSUMS 0x00 /* @@ -207,6 +215,8 @@ typedef struct ControlFileData uint32 blcksz; /* data block size for this DB */ uint32 relseg_size; /* blocks per segment of large relation */ + uint32 slru_pages_per_segment; /* size of each SLRU segment */ + uint32 xlog_blcksz; /* block size within WAL files */ uint32 xlog_seg_size; /* size of each WAL segment */ diff --git a/src/include/catalog/pg_conversion.dat b/src/include/catalog/pg_conversion.dat index 0fb716cdc0ecc..4243c8006a7fb 100644 --- a/src/include/catalog/pg_conversion.dat +++ b/src/include/catalog/pg_conversion.dat @@ -3,7 +3,7 @@ # pg_conversion.dat # Initial contents of the pg_conversion system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_conversion.dat @@ -15,30 +15,6 @@ [ -{ oid => '4402', descr => 'conversion for KOI8R to MULE_INTERNAL', - conname => 'koi8_r_to_mic', conforencoding => 'PG_KOI8R', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'koi8r_to_mic' }, -{ oid => '4403', descr => 'conversion for MULE_INTERNAL to KOI8R', - conname => 'mic_to_koi8_r', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_KOI8R', conproc => 'mic_to_koi8r' }, -{ oid => '4404', descr => 'conversion for ISO-8859-5 to MULE_INTERNAL', - conname => 'iso_8859_5_to_mic', conforencoding => 'PG_ISO_8859_5', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'iso_to_mic' }, -{ oid => '4405', descr => 'conversion for MULE_INTERNAL to ISO-8859-5', - conname => 'mic_to_iso_8859_5', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_ISO_8859_5', conproc => 'mic_to_iso' }, -{ oid => '4406', descr => 'conversion for WIN1251 to MULE_INTERNAL', - conname => 'windows_1251_to_mic', conforencoding => 'PG_WIN1251', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'win1251_to_mic' }, -{ oid => '4407', descr => 'conversion for MULE_INTERNAL to WIN1251', - conname => 'mic_to_windows_1251', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_WIN1251', conproc => 'mic_to_win1251' }, -{ oid => '4408', descr => 'conversion for WIN866 to MULE_INTERNAL', - conname => 'windows_866_to_mic', conforencoding => 'PG_WIN866', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'win866_to_mic' }, -{ oid => '4409', descr => 'conversion for MULE_INTERNAL to WIN866', - conname => 'mic_to_windows_866', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_WIN866', conproc => 'mic_to_win866' }, { oid => '4410', descr => 'conversion for KOI8R to WIN1251', conname => 'koi8_r_to_windows_1251', conforencoding => 'PG_KOI8R', contoencoding => 'PG_WIN1251', conproc => 'koi8r_to_win1251' }, @@ -75,90 +51,24 @@ { oid => '4421', descr => 'conversion for WIN866 to ISO-8859-5', conname => 'windows_866_to_iso_8859_5', conforencoding => 'PG_WIN866', contoencoding => 'PG_ISO_8859_5', conproc => 'win866_to_iso' }, -{ oid => '4422', descr => 'conversion for EUC_CN to MULE_INTERNAL', - conname => 'euc_cn_to_mic', conforencoding => 'PG_EUC_CN', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'euc_cn_to_mic' }, -{ oid => '4423', descr => 'conversion for MULE_INTERNAL to EUC_CN', - conname => 'mic_to_euc_cn', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_EUC_CN', conproc => 'mic_to_euc_cn' }, { oid => '4424', descr => 'conversion for EUC_JP to SJIS', conname => 'euc_jp_to_sjis', conforencoding => 'PG_EUC_JP', contoencoding => 'PG_SJIS', conproc => 'euc_jp_to_sjis' }, { oid => '4425', descr => 'conversion for SJIS to EUC_JP', conname => 'sjis_to_euc_jp', conforencoding => 'PG_SJIS', contoencoding => 'PG_EUC_JP', conproc => 'sjis_to_euc_jp' }, -{ oid => '4426', descr => 'conversion for EUC_JP to MULE_INTERNAL', - conname => 'euc_jp_to_mic', conforencoding => 'PG_EUC_JP', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'euc_jp_to_mic' }, -{ oid => '4427', descr => 'conversion for SJIS to MULE_INTERNAL', - conname => 'sjis_to_mic', conforencoding => 'PG_SJIS', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'sjis_to_mic' }, -{ oid => '4428', descr => 'conversion for MULE_INTERNAL to EUC_JP', - conname => 'mic_to_euc_jp', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_EUC_JP', conproc => 'mic_to_euc_jp' }, -{ oid => '4429', descr => 'conversion for MULE_INTERNAL to SJIS', - conname => 'mic_to_sjis', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_SJIS', conproc => 'mic_to_sjis' }, -{ oid => '4430', descr => 'conversion for EUC_KR to MULE_INTERNAL', - conname => 'euc_kr_to_mic', conforencoding => 'PG_EUC_KR', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'euc_kr_to_mic' }, -{ oid => '4431', descr => 'conversion for MULE_INTERNAL to EUC_KR', - conname => 'mic_to_euc_kr', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_EUC_KR', conproc => 'mic_to_euc_kr' }, { oid => '4432', descr => 'conversion for EUC_TW to BIG5', conname => 'euc_tw_to_big5', conforencoding => 'PG_EUC_TW', contoencoding => 'PG_BIG5', conproc => 'euc_tw_to_big5' }, { oid => '4433', descr => 'conversion for BIG5 to EUC_TW', conname => 'big5_to_euc_tw', conforencoding => 'PG_BIG5', contoencoding => 'PG_EUC_TW', conproc => 'big5_to_euc_tw' }, -{ oid => '4434', descr => 'conversion for EUC_TW to MULE_INTERNAL', - conname => 'euc_tw_to_mic', conforencoding => 'PG_EUC_TW', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'euc_tw_to_mic' }, -{ oid => '4435', descr => 'conversion for BIG5 to MULE_INTERNAL', - conname => 'big5_to_mic', conforencoding => 'PG_BIG5', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'big5_to_mic' }, -{ oid => '4436', descr => 'conversion for MULE_INTERNAL to EUC_TW', - conname => 'mic_to_euc_tw', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_EUC_TW', conproc => 'mic_to_euc_tw' }, -{ oid => '4437', descr => 'conversion for MULE_INTERNAL to BIG5', - conname => 'mic_to_big5', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_BIG5', conproc => 'mic_to_big5' }, -{ oid => '4438', descr => 'conversion for LATIN2 to MULE_INTERNAL', - conname => 'iso_8859_2_to_mic', conforencoding => 'PG_LATIN2', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'latin2_to_mic' }, -{ oid => '4439', descr => 'conversion for MULE_INTERNAL to LATIN2', - conname => 'mic_to_iso_8859_2', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_LATIN2', conproc => 'mic_to_latin2' }, -{ oid => '4440', descr => 'conversion for WIN1250 to MULE_INTERNAL', - conname => 'windows_1250_to_mic', conforencoding => 'PG_WIN1250', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'win1250_to_mic' }, -{ oid => '4441', descr => 'conversion for MULE_INTERNAL to WIN1250', - conname => 'mic_to_windows_1250', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_WIN1250', conproc => 'mic_to_win1250' }, { oid => '4442', descr => 'conversion for LATIN2 to WIN1250', conname => 'iso_8859_2_to_windows_1250', conforencoding => 'PG_LATIN2', contoencoding => 'PG_WIN1250', conproc => 'latin2_to_win1250' }, { oid => '4443', descr => 'conversion for WIN1250 to LATIN2', conname => 'windows_1250_to_iso_8859_2', conforencoding => 'PG_WIN1250', contoencoding => 'PG_LATIN2', conproc => 'win1250_to_latin2' }, -{ oid => '4444', descr => 'conversion for LATIN1 to MULE_INTERNAL', - conname => 'iso_8859_1_to_mic', conforencoding => 'PG_LATIN1', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'latin1_to_mic' }, -{ oid => '4445', descr => 'conversion for MULE_INTERNAL to LATIN1', - conname => 'mic_to_iso_8859_1', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_LATIN1', conproc => 'mic_to_latin1' }, -{ oid => '4446', descr => 'conversion for LATIN3 to MULE_INTERNAL', - conname => 'iso_8859_3_to_mic', conforencoding => 'PG_LATIN3', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'latin3_to_mic' }, -{ oid => '4447', descr => 'conversion for MULE_INTERNAL to LATIN3', - conname => 'mic_to_iso_8859_3', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_LATIN3', conproc => 'mic_to_latin3' }, -{ oid => '4448', descr => 'conversion for LATIN4 to MULE_INTERNAL', - conname => 'iso_8859_4_to_mic', conforencoding => 'PG_LATIN4', - contoencoding => 'PG_MULE_INTERNAL', conproc => 'latin4_to_mic' }, -{ oid => '4449', descr => 'conversion for MULE_INTERNAL to LATIN4', - conname => 'mic_to_iso_8859_4', conforencoding => 'PG_MULE_INTERNAL', - contoencoding => 'PG_LATIN4', conproc => 'mic_to_latin4' }, { oid => '4452', descr => 'conversion for BIG5 to UTF8', conname => 'big5_to_utf8', conforencoding => 'PG_BIG5', contoencoding => 'PG_UTF8', conproc => 'big5_to_utf8' }, diff --git a/src/include/catalog/pg_conversion.h b/src/include/catalog/pg_conversion.h index 477b8d09ae5ec..71f5de904973d 100644 --- a/src/include/catalog/pg_conversion.h +++ b/src/include/catalog/pg_conversion.h @@ -3,7 +3,7 @@ * pg_conversion.h * definition of the "conversion" system catalog (pg_conversion) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_conversion.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_conversion * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_conversion,2607,ConversionRelationId) { /* oid */ @@ -53,6 +55,8 @@ CATALOG(pg_conversion,2607,ConversionRelationId) bool condefault BKI_DEFAULT(t); } FormData_pg_conversion; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_conversion corresponds to a pointer to a tuple with * the format of pg_conversion relation. diff --git a/src/include/catalog/pg_database.dat b/src/include/catalog/pg_database.dat index 1a7f3fed81a1a..4ce20dbe81ff8 100644 --- a/src/include/catalog/pg_database.dat +++ b/src/include/catalog/pg_database.dat @@ -3,7 +3,7 @@ # pg_database.dat # Initial contents of the pg_database system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_database.dat diff --git a/src/include/catalog/pg_database.h b/src/include/catalog/pg_database.h index 54f0d38c9c9e1..8a495e96eed35 100644 --- a/src/include/catalog/pg_database.h +++ b/src/include/catalog/pg_database.h @@ -4,7 +4,7 @@ * definition of the "database" system catalog (pg_database) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_database.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_database * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(1248,DatabaseRelation_Rowtype_Id) BKI_SCHEMA_MACRO { /* oid */ @@ -88,6 +90,8 @@ CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID #endif } FormData_pg_database; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_database corresponds to a pointer to a tuple with * the format of pg_database relation. @@ -123,6 +127,7 @@ DECLARE_OID_DEFINING_MACRO(PostgresDbOid, 5); */ #define DATCONNLIMIT_INVALID_DB -2 +extern Oid get_database_oid(const char *dbname, bool missing_ok); extern bool database_is_invalid_form(Form_pg_database datform); extern bool database_is_invalid_oid(Oid dboid); diff --git a/src/include/catalog/pg_db_role_setting.h b/src/include/catalog/pg_db_role_setting.h index be5cde022862c..f1cbba020b229 100644 --- a/src/include/catalog/pg_db_role_setting.h +++ b/src/include/catalog/pg_db_role_setting.h @@ -5,7 +5,7 @@ * configuration settings (pg_db_role_setting) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_db_role_setting.h @@ -31,6 +31,8 @@ * typedef struct FormData_pg_db_role_setting * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_db_role_setting,2964,DbRoleSettingRelationId) BKI_SHARED_RELATION { /* database, or 0 for a role-specific setting */ @@ -44,6 +46,8 @@ CATALOG(pg_db_role_setting,2964,DbRoleSettingRelationId) BKI_SHARED_RELATION #endif } FormData_pg_db_role_setting; +END_CATALOG_STRUCT + typedef FormData_pg_db_role_setting * Form_pg_db_role_setting; DECLARE_TOAST_WITH_MACRO(pg_db_role_setting, 2966, 2967, PgDbRoleSettingToastTable, PgDbRoleSettingToastIndex); diff --git a/src/include/catalog/pg_default_acl.h b/src/include/catalog/pg_default_acl.h index ce6e5098eaf02..dc1722f7a68fb 100644 --- a/src/include/catalog/pg_default_acl.h +++ b/src/include/catalog/pg_default_acl.h @@ -5,7 +5,7 @@ * (pg_default_acl) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_default_acl.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_default_acl * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_default_acl,826,DefaultAclRelationId) { Oid oid; /* oid */ @@ -42,6 +44,8 @@ CATALOG(pg_default_acl,826,DefaultAclRelationId) #endif } FormData_pg_default_acl; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_default_acl corresponds to a pointer to a tuple with * the format of pg_default_acl relation. diff --git a/src/include/catalog/pg_depend.h b/src/include/catalog/pg_depend.h index 8d0daa9bb7a73..af1423ade4489 100644 --- a/src/include/catalog/pg_depend.h +++ b/src/include/catalog/pg_depend.h @@ -17,7 +17,7 @@ * convenient to find from the contents of other catalogs. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_depend.h @@ -39,6 +39,8 @@ * typedef struct FormData_pg_depend * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_depend,2608,DependRelationId) { /* @@ -64,6 +66,8 @@ CATALOG(pg_depend,2608,DependRelationId) char deptype; /* see codes in dependency.h */ } FormData_pg_depend; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_depend corresponds to a pointer to a row with * the format of pg_depend relation. diff --git a/src/include/catalog/pg_description.h b/src/include/catalog/pg_description.h index 4cd2bf904306a..98971d8ca1171 100644 --- a/src/include/catalog/pg_description.h +++ b/src/include/catalog/pg_description.h @@ -23,7 +23,7 @@ * for example). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_description.h @@ -45,6 +45,8 @@ * typedef struct FormData_pg_description * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_description,2609,DescriptionRelationId) { Oid objoid; /* OID of object itself */ @@ -56,6 +58,8 @@ CATALOG(pg_description,2609,DescriptionRelationId) #endif } FormData_pg_description; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_description corresponds to a pointer to a tuple with * the format of pg_description relation. diff --git a/src/include/catalog/pg_enum.h b/src/include/catalog/pg_enum.h index 7cb69ae013b5d..a1c73817fffb2 100644 --- a/src/include/catalog/pg_enum.h +++ b/src/include/catalog/pg_enum.h @@ -4,7 +4,7 @@ * definition of the "enum" system catalog (pg_enum) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_enum.h @@ -28,6 +28,8 @@ * typedef struct FormData_pg_enum * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_enum,3501,EnumRelationId) { Oid oid; /* oid */ @@ -36,6 +38,8 @@ CATALOG(pg_enum,3501,EnumRelationId) NameData enumlabel; /* text representation of enum value */ } FormData_pg_enum; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_enum corresponds to a pointer to a tuple with * the format of pg_enum relation. diff --git a/src/include/catalog/pg_event_trigger.h b/src/include/catalog/pg_event_trigger.h index bcb516c4f2dd5..eaacaaf2e2cfc 100644 --- a/src/include/catalog/pg_event_trigger.h +++ b/src/include/catalog/pg_event_trigger.h @@ -4,7 +4,7 @@ * definition of the "event trigger" system catalog (pg_event_trigger) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_event_trigger.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_event_trigger * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_event_trigger,3466,EventTriggerRelationId) { Oid oid; /* oid */ @@ -42,6 +44,8 @@ CATALOG(pg_event_trigger,3466,EventTriggerRelationId) #endif } FormData_pg_event_trigger; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_event_trigger corresponds to a pointer to a tuple with * the format of pg_event_trigger relation. diff --git a/src/include/catalog/pg_extension.h b/src/include/catalog/pg_extension.h index 9214ebedafa3c..19ec291ad7ea8 100644 --- a/src/include/catalog/pg_extension.h +++ b/src/include/catalog/pg_extension.h @@ -4,7 +4,7 @@ * definition of the "extension" system catalog (pg_extension) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_extension.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_extension * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_extension,3079,ExtensionRelationId) { Oid oid; /* oid */ @@ -44,6 +46,8 @@ CATALOG(pg_extension,3079,ExtensionRelationId) #endif } FormData_pg_extension; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_extension corresponds to a pointer to a tuple with * the format of pg_extension relation. diff --git a/src/include/catalog/pg_foreign_data_wrapper.h b/src/include/catalog/pg_foreign_data_wrapper.h index d03ab5a4f28bf..3d8389de65e18 100644 --- a/src/include/catalog/pg_foreign_data_wrapper.h +++ b/src/include/catalog/pg_foreign_data_wrapper.h @@ -4,7 +4,7 @@ * definition of the "foreign-data wrapper" system catalog (pg_foreign_data_wrapper) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_foreign_data_wrapper.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_foreign_data_wrapper * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_foreign_data_wrapper,2328,ForeignDataWrapperRelationId) { Oid oid; /* oid */ @@ -36,6 +38,9 @@ CATALOG(pg_foreign_data_wrapper,2328,ForeignDataWrapperRelationId) Oid fdwvalidator BKI_LOOKUP_OPT(pg_proc); /* option validation * function, or 0 if * none */ + Oid fdwconnection BKI_LOOKUP_OPT(pg_proc); /* connection string + * function, or 0 if + * none */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ aclitem fdwacl[1]; /* access permissions */ @@ -43,6 +48,8 @@ CATALOG(pg_foreign_data_wrapper,2328,ForeignDataWrapperRelationId) #endif } FormData_pg_foreign_data_wrapper; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_foreign_data_wrapper corresponds to a pointer to a tuple with * the format of pg_foreign_data_wrapper relation. diff --git a/src/include/catalog/pg_foreign_server.h b/src/include/catalog/pg_foreign_server.h index d53f0974c688e..cac0b9faafed5 100644 --- a/src/include/catalog/pg_foreign_server.h +++ b/src/include/catalog/pg_foreign_server.h @@ -3,7 +3,7 @@ * pg_foreign_server.h * definition of the "foreign server" system catalog (pg_foreign_server) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_foreign_server.h @@ -25,6 +25,8 @@ * typedef struct FormData_pg_foreign_server * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_foreign_server,1417,ForeignServerRelationId) { Oid oid; /* oid */ @@ -40,6 +42,8 @@ CATALOG(pg_foreign_server,1417,ForeignServerRelationId) #endif } FormData_pg_foreign_server; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_foreign_server corresponds to a pointer to a tuple with * the format of pg_foreign_server relation. diff --git a/src/include/catalog/pg_foreign_table.h b/src/include/catalog/pg_foreign_table.h index aea94aa3faa6a..601115c183d74 100644 --- a/src/include/catalog/pg_foreign_table.h +++ b/src/include/catalog/pg_foreign_table.h @@ -3,7 +3,7 @@ * pg_foreign_table.h * definition of the "foreign table" system catalog (pg_foreign_table) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_foreign_table.h @@ -25,6 +25,8 @@ * typedef struct FormData_pg_foreign_table * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_foreign_table,3118,ForeignTableRelationId) { Oid ftrelid BKI_LOOKUP(pg_class); /* OID of foreign table */ @@ -35,6 +37,8 @@ CATALOG(pg_foreign_table,3118,ForeignTableRelationId) #endif } FormData_pg_foreign_table; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_foreign_table corresponds to a pointer to a tuple with * the format of pg_foreign_table relation. diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h index 4392b9d221d5a..d722efe49b4c0 100644 --- a/src/include/catalog/pg_index.h +++ b/src/include/catalog/pg_index.h @@ -4,7 +4,7 @@ * definition of the "index" system catalog (pg_index) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_index.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_index. * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO { Oid indexrelid BKI_LOOKUP(pg_class); /* OID of the index */ @@ -62,6 +64,8 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO #endif } FormData_pg_index; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_index corresponds to a pointer to a tuple with * the format of pg_index relation. @@ -69,7 +73,7 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO */ typedef FormData_pg_index *Form_pg_index; -DECLARE_TOAST_WITH_MACRO(pg_index, 8149, 8150, PgIndexToastTable, PgIndexToastIndex); +DECLARE_TOAST_WITH_MACRO(pg_index, 6351, 6352, PgIndexToastTable, PgIndexToastIndex); DECLARE_INDEX(pg_index_indrelid_index, 2678, IndexIndrelidIndexId, pg_index, btree(indrelid oid_ops)); DECLARE_UNIQUE_INDEX_PKEY(pg_index_indexrelid_index, 2679, IndexRelidIndexId, pg_index, btree(indexrelid oid_ops)); diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h index 1d6765ae9112c..cc874abaabb56 100644 --- a/src/include/catalog/pg_inherits.h +++ b/src/include/catalog/pg_inherits.h @@ -4,7 +4,7 @@ * definition of the "inherits" system catalog (pg_inherits) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_inherits.h @@ -22,13 +22,15 @@ #include "catalog/pg_inherits_d.h" /* IWYU pragma: export */ #include "nodes/pg_list.h" -#include "storage/lock.h" +#include "storage/lockdefs.h" /* ---------------- * pg_inherits definition. cpp turns this into * typedef struct FormData_pg_inherits * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_inherits,2611,InheritsRelationId) { Oid inhrelid BKI_LOOKUP(pg_class); @@ -37,6 +39,8 @@ CATALOG(pg_inherits,2611,InheritsRelationId) bool inhdetachpending; } FormData_pg_inherits; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_inherits corresponds to a pointer to a tuple with * the format of pg_inherits relation. diff --git a/src/include/catalog/pg_init_privs.h b/src/include/catalog/pg_init_privs.h index 594efffca8f90..44c7e50d470e4 100644 --- a/src/include/catalog/pg_init_privs.h +++ b/src/include/catalog/pg_init_privs.h @@ -21,7 +21,7 @@ * are loaded near the end of initdb. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_init_privs.h @@ -43,6 +43,8 @@ * typedef struct FormData_pg_init_privs * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_init_privs,3394,InitPrivsRelationId) { Oid objoid; /* OID of object itself */ @@ -56,6 +58,8 @@ CATALOG(pg_init_privs,3394,InitPrivsRelationId) #endif } FormData_pg_init_privs; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_init_privs corresponds to a pointer to a tuple with * the format of pg_init_privs relation. diff --git a/src/include/catalog/pg_language.dat b/src/include/catalog/pg_language.dat index 84a958e2ef194..705c03ac14bae 100644 --- a/src/include/catalog/pg_language.dat +++ b/src/include/catalog/pg_language.dat @@ -3,7 +3,7 @@ # pg_language.dat # Initial contents of the pg_language system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_language.dat diff --git a/src/include/catalog/pg_language.h b/src/include/catalog/pg_language.h index 3558b99858af9..d64e4525547fc 100644 --- a/src/include/catalog/pg_language.h +++ b/src/include/catalog/pg_language.h @@ -4,7 +4,7 @@ * definition of the "language" system catalog (pg_language) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_language.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_language * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_language,2612,LanguageRelationId) { Oid oid; /* oid */ @@ -57,6 +59,8 @@ CATALOG(pg_language,2612,LanguageRelationId) #endif } FormData_pg_language; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_language corresponds to a pointer to a tuple with * the format of pg_language relation. diff --git a/src/include/catalog/pg_largeobject.h b/src/include/catalog/pg_largeobject.h index 42971bf3bbb6a..4f3c14c7edab8 100644 --- a/src/include/catalog/pg_largeobject.h +++ b/src/include/catalog/pg_largeobject.h @@ -4,7 +4,7 @@ * definition of the "large object" system catalog (pg_largeobject) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_largeobject.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_largeobject * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_largeobject,2613,LargeObjectRelationId) { Oid loid BKI_LOOKUP(pg_largeobject_metadata); /* Identifier of large @@ -38,6 +40,8 @@ CATALOG(pg_largeobject,2613,LargeObjectRelationId) * zero-length) */ } FormData_pg_largeobject; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_largeobject corresponds to a pointer to a tuple with * the format of pg_largeobject relation. diff --git a/src/include/catalog/pg_largeobject_metadata.h b/src/include/catalog/pg_largeobject_metadata.h index e618cd51e031e..86f369255d417 100644 --- a/src/include/catalog/pg_largeobject_metadata.h +++ b/src/include/catalog/pg_largeobject_metadata.h @@ -5,7 +5,7 @@ * (pg_largeobject_metadata) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_largeobject_metadata.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_largeobject_metadata * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_largeobject_metadata,2995,LargeObjectMetadataRelationId) { Oid oid; /* oid */ @@ -39,6 +41,8 @@ CATALOG(pg_largeobject_metadata,2995,LargeObjectMetadataRelationId) #endif } FormData_pg_largeobject_metadata; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_largeobject_metadata corresponds to a pointer to a tuple * with the format of pg_largeobject_metadata relation. diff --git a/src/include/catalog/pg_namespace.dat b/src/include/catalog/pg_namespace.dat index 008373728f338..3075e142c7356 100644 --- a/src/include/catalog/pg_namespace.dat +++ b/src/include/catalog/pg_namespace.dat @@ -3,7 +3,7 @@ # pg_namespace.dat # Initial contents of the pg_namespace system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_namespace.dat diff --git a/src/include/catalog/pg_namespace.h b/src/include/catalog/pg_namespace.h index affb36f11407e..474f4c574e07a 100644 --- a/src/include/catalog/pg_namespace.h +++ b/src/include/catalog/pg_namespace.h @@ -4,7 +4,7 @@ * definition of the "namespace" system catalog (pg_namespace) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_namespace.h @@ -32,6 +32,8 @@ * nspacl access privilege list * ---------------------------------------------------------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_namespace,2615,NamespaceRelationId) { Oid oid; /* oid */ @@ -44,6 +46,8 @@ CATALOG(pg_namespace,2615,NamespaceRelationId) #endif } FormData_pg_namespace; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_namespace corresponds to a pointer to a tuple with * the format of pg_namespace relation. diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat index 4a9624802aa55..df170b80840bb 100644 --- a/src/include/catalog/pg_opclass.dat +++ b/src/include/catalog/pg_opclass.dat @@ -3,7 +3,7 @@ # pg_opclass.dat # Initial contents of the pg_opclass system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_opclass.dat @@ -57,7 +57,7 @@ { opcmethod => 'hash', opcname => 'inet_ops', opcfamily => 'hash/network_ops', opcintype => 'inet' }, { opcmethod => 'gist', opcname => 'inet_ops', opcfamily => 'gist/network_ops', - opcintype => 'inet', opcdefault => 'f' }, + opcintype => 'inet' }, { opcmethod => 'spgist', opcname => 'inet_ops', opcfamily => 'spgist/network_ops', opcintype => 'inet' }, { oid => '1979', oid_symbol => 'INT2_BTREE_OPS_OID', @@ -177,6 +177,10 @@ opcintype => 'xid8' }, { opcmethod => 'btree', opcname => 'xid8_ops', opcfamily => 'btree/xid8_ops', opcintype => 'xid8' }, +{ opcmethod => 'hash', opcname => 'oid8_ops', opcfamily => 'hash/oid8_ops', + opcintype => 'oid8' }, +{ opcmethod => 'btree', opcname => 'oid8_ops', opcfamily => 'btree/oid8_ops', + opcintype => 'oid8' }, { opcmethod => 'hash', opcname => 'cid_ops', opcfamily => 'hash/cid_ops', opcintype => 'cid' }, { opcmethod => 'hash', opcname => 'tid_ops', opcfamily => 'hash/tid_ops', diff --git a/src/include/catalog/pg_opclass.h b/src/include/catalog/pg_opclass.h index cb63bd9ced629..46170c6c3c3bb 100644 --- a/src/include/catalog/pg_opclass.h +++ b/src/include/catalog/pg_opclass.h @@ -24,7 +24,7 @@ * AMs support this. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_opclass.h @@ -46,6 +46,8 @@ * typedef struct FormData_pg_opclass * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_opclass,2616,OperatorClassRelationId) { Oid oid; /* oid */ @@ -75,6 +77,8 @@ CATALOG(pg_opclass,2616,OperatorClassRelationId) Oid opckeytype BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type); } FormData_pg_opclass; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_opclass corresponds to a pointer to a tuple with * the format of pg_opclass relation. diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat index 6d9dc1528d6eb..1465f13120ac5 100644 --- a/src/include/catalog/pg_operator.dat +++ b/src/include/catalog/pg_operator.dat @@ -3,7 +3,7 @@ # pg_operator.dat # Initial contents of the pg_operator system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_operator.dat @@ -3460,4 +3460,30 @@ oprcode => 'multirange_after_multirange', oprrest => 'multirangesel', oprjoin => 'scalargtjoinsel' }, +{ oid => '8262', descr => 'equal', + oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'oid8', + oprright => 'oid8', oprresult => 'bool', oprcom => '=(oid8,oid8)', + oprnegate => '<>(oid8,oid8)', oprcode => 'oid8eq', oprrest => 'eqsel', + oprjoin => 'eqjoinsel' }, +{ oid => '8263', descr => 'not equal', + oprname => '<>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool', + oprcom => '<>(oid8,oid8)', oprnegate => '=(oid8,oid8)', oprcode => 'oid8ne', + oprrest => 'neqsel', oprjoin => 'neqjoinsel' }, +{ oid => '8264', descr => 'less than', + oprname => '<', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool', + oprcom => '>(oid8,oid8)', oprnegate => '>=(oid8,oid8)', oprcode => 'oid8lt', + oprrest => 'scalarltsel', oprjoin => 'scalarltjoinsel' }, +{ oid => '8265', descr => 'greater than', + oprname => '>', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool', + oprcom => '<(oid8,oid8)', oprnegate => '<=(oid8,oid8)', oprcode => 'oid8gt', + oprrest => 'scalargtsel', oprjoin => 'scalargtjoinsel' }, +{ oid => '8266', descr => 'less than or equal', + oprname => '<=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool', + oprcom => '>=(oid8,oid8)', oprnegate => '>(oid8,oid8)', oprcode => 'oid8le', + oprrest => 'scalarlesel', oprjoin => 'scalarlejoinsel' }, +{ oid => '8267', descr => 'greater than or equal', + oprname => '>=', oprleft => 'oid8', oprright => 'oid8', oprresult => 'bool', + oprcom => '<=(oid8,oid8)', oprnegate => '<(oid8,oid8)', oprcode => 'oid8ge', + oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' }, + ] diff --git a/src/include/catalog/pg_operator.h b/src/include/catalog/pg_operator.h index c3ddfb28fa4f0..f5b4d04783a79 100644 --- a/src/include/catalog/pg_operator.h +++ b/src/include/catalog/pg_operator.h @@ -4,7 +4,7 @@ * definition of the "operator" system catalog (pg_operator) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_operator.h @@ -28,6 +28,8 @@ * typedef struct FormData_pg_operator * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_operator,2617,OperatorRelationId) { Oid oid; /* oid */ @@ -75,6 +77,8 @@ CATALOG(pg_operator,2617,OperatorRelationId) regproc oprjoin BKI_DEFAULT(-) BKI_LOOKUP_OPT(pg_proc); } FormData_pg_operator; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_operator corresponds to a pointer to a tuple with * the format of pg_operator relation. diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat index f7dcb96b43ce5..7a027c4810ee0 100644 --- a/src/include/catalog/pg_opfamily.dat +++ b/src/include/catalog/pg_opfamily.dat @@ -3,7 +3,7 @@ # pg_opfamily.dat # Initial contents of the pg_opfamily system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_opfamily.dat @@ -116,6 +116,10 @@ opfmethod => 'hash', opfname => 'xid8_ops' }, { oid => '5067', opfmethod => 'btree', opfname => 'xid8_ops' }, +{ oid => '8278', + opfmethod => 'hash', opfname => 'oid8_ops' }, +{ oid => '8279', + opfmethod => 'btree', opfname => 'oid8_ops' }, { oid => '2226', opfmethod => 'hash', opfname => 'cid_ops' }, { oid => '2227', diff --git a/src/include/catalog/pg_opfamily.h b/src/include/catalog/pg_opfamily.h index 7472e7e9cfa2e..563703f0f22bb 100644 --- a/src/include/catalog/pg_opfamily.h +++ b/src/include/catalog/pg_opfamily.h @@ -4,7 +4,7 @@ * definition of the "operator family" system catalog (pg_opfamily) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_opfamily.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_opfamily * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_opfamily,2753,OperatorFamilyRelationId) { Oid oid; /* oid */ @@ -43,6 +45,8 @@ CATALOG(pg_opfamily,2753,OperatorFamilyRelationId) Oid opfowner BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid); } FormData_pg_opfamily; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_opfamily corresponds to a pointer to a tuple with * the format of pg_opfamily relation. diff --git a/src/include/catalog/pg_parameter_acl.h b/src/include/catalog/pg_parameter_acl.h index ae4049ba75608..a26b05a9bf2e4 100644 --- a/src/include/catalog/pg_parameter_acl.h +++ b/src/include/catalog/pg_parameter_acl.h @@ -5,7 +5,7 @@ * (pg_parameter_acl). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_parameter_acl.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_parameter_acl * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_parameter_acl,6243,ParameterAclRelationId) BKI_SHARED_RELATION { Oid oid; /* oid */ @@ -40,6 +42,8 @@ CATALOG(pg_parameter_acl,6243,ParameterAclRelationId) BKI_SHARED_RELATION #endif } FormData_pg_parameter_acl; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_parameter_acl corresponds to a pointer to a tuple with diff --git a/src/include/catalog/pg_partitioned_table.h b/src/include/catalog/pg_partitioned_table.h index 0527f347690ca..db47795101805 100644 --- a/src/include/catalog/pg_partitioned_table.h +++ b/src/include/catalog/pg_partitioned_table.h @@ -5,7 +5,7 @@ * (pg_partitioned_table) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_partitioned_table.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_partitioned_table * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_partitioned_table,3350,PartitionedRelationId) { Oid partrelid BKI_LOOKUP(pg_class); /* partitioned table oid */ @@ -57,6 +59,8 @@ CATALOG(pg_partitioned_table,3350,PartitionedRelationId) #endif } FormData_pg_partitioned_table; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_partitioned_table corresponds to a pointer to a tuple with * the format of pg_partitioned_table relation. diff --git a/src/include/catalog/pg_policy.h b/src/include/catalog/pg_policy.h index 3c2498cdf11f6..5bcaf0cd8961d 100644 --- a/src/include/catalog/pg_policy.h +++ b/src/include/catalog/pg_policy.h @@ -4,7 +4,7 @@ * definition of the "policy" system catalog (pg_policy) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_policy.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_policy * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_policy,3256,PolicyRelationId) { Oid oid; /* oid */ @@ -43,6 +45,8 @@ CATALOG(pg_policy,3256,PolicyRelationId) #endif } FormData_pg_policy; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_policy corresponds to a pointer to a row with * the format of pg_policy relation. diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 37a484147a8f2..fa9ae79082b83 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3,7 +3,7 @@ # pg_proc.dat # Initial contents of the pg_proc system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_proc.dat @@ -1004,7 +1004,7 @@ { oid => '3129', descr => 'sort support', proname => 'btint2sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btint2sortsupport' }, -{ oid => '9290', descr => 'skip support', +{ oid => '6402', descr => 'skip support', proname => 'btint2skipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btint2skipsupport' }, { oid => '351', descr => 'less-equal-greater', @@ -1013,7 +1013,7 @@ { oid => '3130', descr => 'sort support', proname => 'btint4sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btint4sortsupport' }, -{ oid => '9291', descr => 'skip support', +{ oid => '6403', descr => 'skip support', proname => 'btint4skipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btint4skipsupport' }, { oid => '842', descr => 'less-equal-greater', @@ -1022,7 +1022,7 @@ { oid => '3131', descr => 'sort support', proname => 'btint8sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btint8sortsupport' }, -{ oid => '9292', descr => 'skip support', +{ oid => '6404', descr => 'skip support', proname => 'btint8skipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btint8skipsupport' }, { oid => '354', descr => 'less-equal-greater', @@ -1043,16 +1043,25 @@ { oid => '3134', descr => 'sort support', proname => 'btoidsortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btoidsortsupport' }, -{ oid => '9293', descr => 'skip support', +{ oid => '6405', descr => 'skip support', proname => 'btoidskipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btoidskipsupport' }, +{ oid => '8282', descr => 'less-equal-greater', + proname => 'btoid8cmp', proleakproof => 't', prorettype => 'int4', + proargtypes => 'oid8 oid8', prosrc => 'btoid8cmp' }, +{ oid => '8283', descr => 'sort support', + proname => 'btoid8sortsupport', prorettype => 'void', + proargtypes => 'internal', prosrc => 'btoid8sortsupport' }, +{ oid => '8284', descr => 'skip support', + proname => 'btoid8skipsupport', prorettype => 'void', + proargtypes => 'internal', prosrc => 'btoid8skipsupport' }, { oid => '404', descr => 'less-equal-greater', proname => 'btoidvectorcmp', proleakproof => 't', prorettype => 'int4', proargtypes => 'oidvector oidvector', prosrc => 'btoidvectorcmp' }, { oid => '358', descr => 'less-equal-greater', proname => 'btcharcmp', proleakproof => 't', prorettype => 'int4', proargtypes => 'char char', prosrc => 'btcharcmp' }, -{ oid => '9294', descr => 'skip support', +{ oid => '6406', descr => 'skip support', proname => 'btcharskipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btcharskipsupport' }, { oid => '359', descr => 'less-equal-greater', @@ -1180,24 +1189,31 @@ proname => 'name', proleakproof => 't', prorettype => 'name', proargtypes => 'bpchar', prosrc => 'bpchar_name' }, -{ oid => '8577', descr => 'convert int2 to bytea', +{ oid => '6367', descr => 'convert int2 to bytea', proname => 'bytea', proleakproof => 't', prorettype => 'bytea', proargtypes => 'int2', prosrc => 'int2_bytea' }, -{ oid => '8578', descr => 'convert int4 to bytea', +{ oid => '6368', descr => 'convert int4 to bytea', proname => 'bytea', proleakproof => 't', prorettype => 'bytea', proargtypes => 'int4', prosrc => 'int4_bytea' }, -{ oid => '8579', descr => 'convert int8 to bytea', +{ oid => '6369', descr => 'convert int8 to bytea', proname => 'bytea', proleakproof => 't', prorettype => 'bytea', proargtypes => 'int8', prosrc => 'int8_bytea' }, -{ oid => '8580', descr => 'convert bytea to int2', - proname => 'int2', prorettype => 'int2', - proargtypes => 'bytea', prosrc => 'bytea_int2' }, -{ oid => '8581', descr => 'convert bytea to int4', - proname => 'int4', prorettype => 'int4', - proargtypes => 'bytea', prosrc => 'bytea_int4' }, -{ oid => '8582', descr => 'convert bytea to int8', - proname => 'int8', prorettype => 'int8', - proargtypes => 'bytea', prosrc => 'bytea_int8' }, +{ oid => '6370', descr => 'convert bytea to int2', + proname => 'int2', prorettype => 'int2', proargtypes => 'bytea', + prosrc => 'bytea_int2' }, +{ oid => '6371', descr => 'convert bytea to int4', + proname => 'int4', prorettype => 'int4', proargtypes => 'bytea', + prosrc => 'bytea_int4' }, +{ oid => '6372', descr => 'convert bytea to int8', + proname => 'int8', prorettype => 'int8', proargtypes => 'bytea', + prosrc => 'bytea_int8' }, + +{ oid => '9880', descr => 'convert uuid to bytea', + proname => 'bytea', prorettype => 'bytea', proargtypes => 'uuid', + prosrc => 'uuid_bytea' }, +{ oid => '9881', descr => 'convert bytea to uuid', + proname => 'uuid', prorettype => 'uuid', proargtypes => 'bytea', + prosrc => 'bytea_uuid' }, { oid => '449', descr => 'hash', proname => 'hashint2', prorettype => 'int4', proargtypes => 'int2', @@ -1259,10 +1275,10 @@ { oid => '772', descr => 'hash', proname => 'hashvarlenaextended', prorettype => 'int8', proargtypes => 'internal int8', prosrc => 'hashvarlenaextended' }, -{ oid => '9708', descr => 'hash', +{ oid => '6413', descr => 'hash', proname => 'hashbytea', prorettype => 'int4', proargtypes => 'bytea', prosrc => 'hashbytea' }, -{ oid => '9709', descr => 'hash', +{ oid => '6414', descr => 'hash', proname => 'hashbyteaextended', prorettype => 'int8', proargtypes => 'bytea int8', prosrc => 'hashbyteaextended' }, { oid => '457', descr => 'hash', @@ -1301,34 +1317,34 @@ { oid => '781', descr => 'hash', proname => 'hashmacaddr8extended', prorettype => 'int8', proargtypes => 'macaddr8 int8', prosrc => 'hashmacaddr8extended' }, -{ oid => '9710', descr => 'hash', +{ oid => '6415', descr => 'hash', proname => 'hashdate', prorettype => 'int4', proargtypes => 'date', prosrc => 'hashdate' }, -{ oid => '9711', descr => 'hash', +{ oid => '6416', descr => 'hash', proname => 'hashdateextended', prorettype => 'int8', proargtypes => 'date int8', prosrc => 'hashdateextended' }, -{ oid => '9712', descr => 'hash', +{ oid => '6417', descr => 'hash', proname => 'hashbool', prorettype => 'int4', proargtypes => 'bool', prosrc => 'hashbool' }, -{ oid => '9713', descr => 'hash', +{ oid => '6418', descr => 'hash', proname => 'hashboolextended', prorettype => 'int8', proargtypes => 'bool int8', prosrc => 'hashboolextended' }, -{ oid => '9714', descr => 'hash', +{ oid => '6419', descr => 'hash', proname => 'hashxid', prorettype => 'int4', proargtypes => 'xid', prosrc => 'hashxid' }, -{ oid => '9715', descr => 'hash', +{ oid => '6420', descr => 'hash', proname => 'hashxidextended', prorettype => 'int8', proargtypes => 'xid int8', prosrc => 'hashxidextended' }, -{ oid => '9716', descr => 'hash', +{ oid => '6421', descr => 'hash', proname => 'hashxid8', prorettype => 'int4', proargtypes => 'xid8', prosrc => 'hashxid8' }, -{ oid => '9717', descr => 'hash', +{ oid => '6422', descr => 'hash', proname => 'hashxid8extended', prorettype => 'int8', proargtypes => 'xid8 int8', prosrc => 'hashxid8extended' }, -{ oid => '9718', descr => 'hash', +{ oid => '6423', descr => 'hash', proname => 'hashcid', prorettype => 'int4', proargtypes => 'cid', prosrc => 'hashcid' }, -{ oid => '9719', descr => 'hash', +{ oid => '6424', descr => 'hash', proname => 'hashcidextended', prorettype => 'int8', proargtypes => 'cid int8', prosrc => 'hashcidextended' }, @@ -1348,10 +1364,10 @@ proname => 'text_smaller', proleakproof => 't', prorettype => 'text', proargtypes => 'text text', prosrc => 'text_smaller' }, -{ oid => '8920', descr => 'larger of two', +{ oid => '6393', descr => 'larger of two', proname => 'bytea_larger', proleakproof => 't', prorettype => 'bytea', proargtypes => 'bytea bytea', prosrc => 'bytea_larger' }, -{ oid => '8921', descr => 'smaller of two', +{ oid => '6394', descr => 'smaller of two', proname => 'bytea_smaller', proleakproof => 't', prorettype => 'bytea', proargtypes => 'bytea bytea', prosrc => 'bytea_smaller' }, @@ -1533,7 +1549,7 @@ { oid => '6163', descr => 'number of set bits', proname => 'bit_count', prorettype => 'int8', proargtypes => 'bytea', prosrc => 'bytea_bit_count' }, -{ oid => '8694', descr => 'reverse bytea', +{ oid => '6382', descr => 'reverse bytea', proname => 'reverse', prorettype => 'bytea', proargtypes => 'bytea', prosrc => 'bytea_reverse' }, @@ -1638,7 +1654,7 @@ proname => 'array_append', prosupport => 'array_append_support', proisstrict => 'f', prorettype => 'anycompatiblearray', proargtypes => 'anycompatiblearray anycompatible', prosrc => 'array_append' }, -{ oid => '8680', descr => 'planner support for array_append', +{ oid => '6378', descr => 'planner support for array_append', proname => 'array_append_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'array_append_support' }, { oid => '379', descr => 'prepend element onto front of array', @@ -1646,7 +1662,7 @@ proisstrict => 'f', prorettype => 'anycompatiblearray', proargtypes => 'anycompatible anycompatiblearray', prosrc => 'array_prepend' }, -{ oid => '8681', descr => 'planner support for array_prepend', +{ oid => '6379', descr => 'planner support for array_prepend', proname => 'array_prepend_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'array_prepend_support' }, { oid => '383', @@ -1784,17 +1800,17 @@ { oid => '6216', descr => 'take samples from array', proname => 'array_sample', provolatile => 'v', prorettype => 'anyarray', proargtypes => 'anyarray int4', prosrc => 'array_sample' }, -{ oid => '8686', descr => 'reverse array', +{ oid => '6381', descr => 'reverse array', proname => 'array_reverse', prorettype => 'anyarray', proargtypes => 'anyarray', prosrc => 'array_reverse' }, -{ oid => '8810', descr => 'sort array', +{ oid => '6388', descr => 'sort array', proname => 'array_sort', prorettype => 'anyarray', proargtypes => 'anyarray', prosrc => 'array_sort' }, -{ oid => '8811', descr => 'sort array', +{ oid => '6389', descr => 'sort array', proname => 'array_sort', prorettype => 'anyarray', proargtypes => 'anyarray bool', proargnames => '{array,descending}', prosrc => 'array_sort_order' }, -{ oid => '8812', descr => 'sort array', +{ oid => '6390', descr => 'sort array', proname => 'array_sort', prorettype => 'anyarray', proargtypes => 'anyarray bool bool', proargnames => '{array,descending,nulls_first}', @@ -1813,14 +1829,17 @@ { oid => '764', descr => 'large object import', proname => 'lo_import', provolatile => 'v', proparallel => 'u', - prorettype => 'oid', proargtypes => 'text', prosrc => 'be_lo_import' }, + prorettype => 'oid', proargtypes => 'text', prosrc => 'be_lo_import', + proacl => '{POSTGRES=X}' }, { oid => '767', descr => 'large object import', proname => 'lo_import', provolatile => 'v', proparallel => 'u', prorettype => 'oid', proargtypes => 'text oid', - prosrc => 'be_lo_import_with_oid' }, + prosrc => 'be_lo_import_with_oid', + proacl => '{POSTGRES=X}' }, { oid => '765', descr => 'large object export', proname => 'lo_export', provolatile => 'v', proparallel => 'u', - prorettype => 'int4', proargtypes => 'oid text', prosrc => 'be_lo_export' }, + prorettype => 'int4', proargtypes => 'oid text', prosrc => 'be_lo_export', + proacl => '{POSTGRES=X}' }, { oid => '766', descr => 'increment', proname => 'int4inc', prorettype => 'int4', proargtypes => 'int4', @@ -2315,7 +2334,7 @@ { oid => '3136', descr => 'sort support', proname => 'date_sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'date_sortsupport' }, -{ oid => '9295', descr => 'skip support', +{ oid => '6407', descr => 'skip support', proname => 'date_skipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'date_skipsupport' }, { oid => '4133', descr => 'window RANGE support', @@ -2738,6 +2757,12 @@ { oid => '2796', descr => 'smaller of two', proname => 'tidsmaller', prorettype => 'tid', proargtypes => 'tid tid', prosrc => 'tidsmaller' }, +{ oid => '9951', descr => 'extract block number from tid', + proname => 'tid_block', proleakproof => 't', prorettype => 'int8', + proargtypes => 'tid', prosrc => 'tid_block' }, +{ oid => '9952', descr => 'extract offset number from tid', + proname => 'tid_offset', proleakproof => 't', prorettype => 'int4', + proargtypes => 'tid', prosrc => 'tid_offset' }, { oid => '2233', descr => 'hash', proname => 'hashtid', prorettype => 'int4', proargtypes => 'tid', prosrc => 'hashtid' }, @@ -3371,8 +3396,8 @@ proname => 'center', prorettype => 'point', proargtypes => 'circle', prosrc => 'circle_center' }, { oid => '1544', descr => 'convert circle to 12-vertex polygon', - proname => 'polygon', prolang => 'sql', prorettype => 'polygon', - proargtypes => 'circle', prosrc => 'see system_functions.sql' }, + proname => 'polygon', prorettype => 'polygon', + proargtypes => 'circle', prosrc => 'circle_to_poly' }, { oid => '1545', descr => 'number of points', proname => 'npoints', prorettype => 'int4', proargtypes => 'path', prosrc => 'path_npoints' }, @@ -3433,11 +3458,11 @@ proname => 'pg_sequence_last_value', provolatile => 'v', proparallel => 'u', prorettype => 'int8', proargtypes => 'regclass', prosrc => 'pg_sequence_last_value' }, -{ oid => '9876', descr => 'return sequence tuple, for use by pg_dump', +{ oid => '6427', descr => 'return sequence tuple, for use by pg_dump and sequence synchronization', proname => 'pg_get_sequence_data', provolatile => 'v', proparallel => 'u', prorettype => 'record', proargtypes => 'regclass', - proallargtypes => '{regclass,int8,bool}', proargmodes => '{i,o,o}', - proargnames => '{sequence_oid,last_value,is_called}', + proallargtypes => '{regclass,int8,bool,pg_lsn}', proargmodes => '{i,o,o,o}', + proargnames => '{sequence_oid,last_value,is_called,page_lsn}', prosrc => 'pg_get_sequence_data' }, { oid => '275', descr => 'return the next oid for a system table', @@ -3490,6 +3515,7 @@ { oid => '6212', descr => 'random value from normal distribution', proname => 'random_normal', provolatile => 'v', proparallel => 'r', prorettype => 'float8', proargtypes => 'float8 float8', + proargnames => '{mean,stddev}', proargdefaults => '{0,1}', prosrc => 'drandom_normal' }, { oid => '6339', descr => 'random integer in range', proname => 'random', provolatile => 'v', proparallel => 'r', @@ -3503,6 +3529,18 @@ proname => 'random', provolatile => 'v', proparallel => 'r', prorettype => 'numeric', proargtypes => 'numeric numeric', proargnames => '{min,max}', prosrc => 'numeric_random' }, +{ oid => '6431', descr => 'random date in range', + proname => 'random', provolatile => 'v', proparallel => 'r', + prorettype => 'date', proargtypes => 'date date', + proargnames => '{min,max}', prosrc => 'date_random' }, +{ oid => '6432', descr => 'random timestamp in range', + proname => 'random', provolatile => 'v', proparallel => 'r', + prorettype => 'timestamp', proargtypes => 'timestamp timestamp', + proargnames => '{min,max}', prosrc => 'timestamp_random' }, +{ oid => '6433', descr => 'random timestamptz in range', + proname => 'random', provolatile => 'v', proparallel => 'r', + prorettype => 'timestamptz', proargtypes => 'timestamptz timestamptz', + proargnames => '{min,max}', prosrc => 'timestamptz_random' }, { oid => '1599', descr => 'set random seed', proname => 'setseed', provolatile => 'v', proparallel => 'r', prorettype => 'void', proargtypes => 'float8', prosrc => 'setseed' }, @@ -3594,10 +3632,11 @@ proname => 'erfc', prorettype => 'float8', proargtypes => 'float8', prosrc => 'derfc' }, -{ oid => '8702', descr => 'gamma function', +{ oid => '6383', descr => 'gamma function', proname => 'gamma', prorettype => 'float8', proargtypes => 'float8', prosrc => 'dgamma' }, -{ oid => '8703', descr => 'natural logarithm of absolute value of gamma function', +{ oid => '6384', + descr => 'natural logarithm of absolute value of gamma function', proname => 'lgamma', prorettype => 'float8', proargtypes => 'float8', prosrc => 'dlgamma' }, @@ -3688,7 +3727,7 @@ { oid => '872', descr => 'capitalize each word', proname => 'initcap', prorettype => 'text', proargtypes => 'text', prosrc => 'initcap' }, -{ oid => '9569', descr => 'fold case', +{ oid => '6412', descr => 'fold case', proname => 'casefold', prorettype => 'text', proargtypes => 'text', prosrc => 'casefold' }, { oid => '873', descr => 'left-pad string to length', @@ -3939,6 +3978,9 @@ proargtypes => 'oid oid', prosrc => 'oidge' }, # System-view support functions +{ oid => '8302', descr => 'source text of a property graph', + proname => 'pg_get_propgraphdef', provolatile => 's', prorettype => 'text', + proargtypes => 'oid', prosrc => 'pg_get_propgraphdef' }, { oid => '1573', descr => 'source text of a rule', proname => 'pg_get_ruledef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_ruledef' }, @@ -4515,7 +4557,7 @@ { oid => '1693', descr => 'less-equal-greater', proname => 'btboolcmp', proleakproof => 't', prorettype => 'int4', proargtypes => 'bool bool', prosrc => 'btboolcmp' }, -{ oid => '9296', descr => 'skip support', +{ oid => '6408', descr => 'skip support', proname => 'btboolskipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'btboolskipsupport' }, @@ -5450,17 +5492,17 @@ prorettype => 'bool', proargtypes => 'oid text', prosrc => 'has_any_column_privilege_id' }, -{ oid => '8048', +{ oid => '6348', descr => 'user privilege on large object by username, large object oid', proname => 'has_largeobject_privilege', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'name oid text', prosrc => 'has_largeobject_privilege_name_id' }, -{ oid => '8049', +{ oid => '6349', descr => 'current user privilege on large object by large object oid', proname => 'has_largeobject_privilege', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid text', prosrc => 'has_largeobject_privilege_id' }, -{ oid => '8050', +{ oid => '6350', descr => 'user privilege on large object by user oid, large object oid', proname => 'has_largeobject_privilege', procost => '10', provolatile => 's', prorettype => 'bool', proargtypes => 'oid oid text', @@ -5524,6 +5566,10 @@ proname => 'pg_stat_get_lastscan', provolatile => 's', proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid', prosrc => 'pg_stat_get_lastscan' }, +{ oid => '9127', descr => 'statistics: last reset for a relation', + proname => 'pg_stat_get_stat_reset_time', provolatile => 's', + proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid', + prosrc => 'pg_stat_get_stat_reset_time' }, { oid => '1929', descr => 'statistics: number of tuples read by seqscan', proname => 'pg_stat_get_tuples_returned', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', @@ -5611,22 +5657,29 @@ proname => 'pg_stat_get_autoanalyze_count', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_stat_get_autoanalyze_count' }, -{ oid => '8406', descr => 'total vacuum time, in milliseconds', +{ oid => '6358', descr => 'total vacuum time, in milliseconds', proname => 'pg_stat_get_total_vacuum_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_total_vacuum_time' }, -{ oid => '8407', descr => 'total autovacuum time, in milliseconds', +{ oid => '6359', descr => 'total autovacuum time, in milliseconds', proname => 'pg_stat_get_total_autovacuum_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_total_autovacuum_time' }, -{ oid => '8408', descr => 'total analyze time, in milliseconds', +{ oid => '6360', descr => 'total analyze time, in milliseconds', proname => 'pg_stat_get_total_analyze_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_total_analyze_time' }, -{ oid => '8409', descr => 'total autoanalyze time, in milliseconds', +{ oid => '6361', descr => 'total autoanalyze time, in milliseconds', proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_total_autoanalyze_time' }, +{ oid => '8409', descr => 'autovacuum scores', + proname => 'pg_stat_get_autovacuum_scores', prorows => '100', proretset => 't', + provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => '', + proallargtypes => '{oid,float8,float8,float8,float8,float8,float8,bool,bool,bool}', + proargmodes => '{o,o,o,o,o,o,o,o,o,o}', + proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}', + prosrc => 'pg_stat_get_autovacuum_scores' }, { oid => '1936', descr => 'statistics: currently active backend IDs', proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'int4', @@ -5671,25 +5724,33 @@ proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{pid,status,receive_start_lsn,receive_start_tli,written_lsn,flushed_lsn,received_tli,last_msg_send_time,last_msg_receipt_time,latest_end_lsn,latest_end_time,slot_name,sender_host,sender_port,conninfo}', prosrc => 'pg_stat_get_wal_receiver' }, +{ oid => '9949', descr => 'statistics: information about WAL recovery', + proname => 'pg_stat_get_recovery', proisstrict => 'f', provolatile => 's', + proparallel => 'r', prorettype => 'record', proargtypes => '', + proallargtypes => '{bool,pg_lsn,pg_lsn,int4,pg_lsn,int4,timestamptz,timestamptz,text}', + proargmodes => '{o,o,o,o,o,o,o,o,o}', + proargnames => '{promote_triggered,last_replayed_read_lsn,last_replayed_end_lsn,last_replayed_tli,replay_end_lsn,replay_end_tli,recovery_last_xact_time,current_chunk_start_time,pause_state}', + prosrc => 'pg_stat_get_recovery' }, { oid => '6169', descr => 'statistics: information about replication slot', proname => 'pg_stat_get_replication_slot', provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => 'text', - proallargtypes => '{text,text,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}', - proargmodes => '{i,o,o,o,o,o,o,o,o,o,o}', - proargnames => '{slot_name,slot_name,spill_txns,spill_count,spill_bytes,stream_txns,stream_count,stream_bytes,total_txns,total_bytes,stats_reset}', + proallargtypes => '{text,text,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz,timestamptz}', + proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{slot_name,slot_name,spill_txns,spill_count,spill_bytes,stream_txns,stream_count,stream_bytes,mem_exceeded_count,total_txns,total_bytes,slotsync_skip_count,slotsync_last_skip,stats_reset}', prosrc => 'pg_stat_get_replication_slot' }, { oid => '6230', descr => 'statistics: check if a stats object exists', proname => 'pg_stat_have_stats', provolatile => 'v', proparallel => 'r', prorettype => 'bool', proargtypes => 'text oid int8', - prosrc => 'pg_stat_have_stats' }, + prosrc => 'pg_stat_have_stats', + proacl => '{POSTGRES=X}' }, { oid => '6231', descr => 'statistics: information about subscription stats', proname => 'pg_stat_get_subscription_stats', provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => 'oid', - proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}', - proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}', - proargnames => '{subid,subid,apply_error_count,sync_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset}', + proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}', + proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{subid,subid,apply_error_count,sync_seq_error_count,sync_table_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_deleted,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset}', prosrc => 'pg_stat_get_subscription_stats' }, { oid => '6118', descr => 'statistics: information about subscription', proname => 'pg_stat_get_subscription', prorows => '10', proisstrict => 'f', @@ -5900,12 +5961,12 @@ proname => 'pg_stat_get_db_sessions_killed', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_stat_get_db_sessions_killed' }, -{ oid => '8403', +{ oid => '6355', descr => 'statistics: number of parallel workers planned to be launched by queries', proname => 'pg_stat_get_db_parallel_workers_to_launch', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', prosrc => 'pg_stat_get_db_parallel_workers_to_launch' }, -{ oid => '8404', +{ oid => '6356', descr => 'statistics: number of parallel workers effectively launched by queries', proname => 'pg_stat_get_db_parallel_workers_launched', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => 'oid', @@ -5927,7 +5988,7 @@ proname => 'pg_stat_get_checkpointer_num_requested', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_checkpointer_num_requested' }, -{ oid => '8599', +{ oid => '6377', descr => 'statistics: number of checkpoints performed by the checkpointer', proname => 'pg_stat_get_checkpointer_num_performed', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => '', @@ -5954,7 +6015,7 @@ proname => 'pg_stat_get_checkpointer_buffers_written', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_checkpointer_buffers_written' }, -{ oid => '8573', +{ oid => '6366', descr => 'statistics: number of SLRU buffers written during checkpoints and restartpoints', proname => 'pg_stat_get_checkpointer_slru_written', provolatile => 's', proparallel => 'r', prorettype => 'int8', proargtypes => '', @@ -6000,7 +6061,15 @@ proargnames => '{backend_type,object,context,reads,read_bytes,read_time,writes,write_bytes,write_time,writebacks,writeback_time,extends,extend_bytes,extend_time,hits,evictions,reuses,fsyncs,fsync_time,stats_reset}', prosrc => 'pg_stat_get_io' }, -{ oid => '8806', descr => 'statistics: backend IO statistics', +{ oid => '9375', descr => 'statistics: per lock type statistics', + proname => 'pg_stat_get_lock', prorows => '10', proretset => 't', + provolatile => 'v', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{text,int8,int8,int8,timestamptz}', + proargmodes => '{o,o,o,o,o}', + proargnames => '{locktype,waits,wait_time,fastpath_exceeded,stats_reset}', + prosrc => 'pg_stat_get_lock' }, + +{ oid => '6386', descr => 'statistics: backend IO statistics', proname => 'pg_stat_get_backend_io', prorows => '5', proretset => 't', provolatile => 'v', proparallel => 'r', prorettype => 'record', proargtypes => 'int4', @@ -6012,16 +6081,16 @@ { oid => '1136', descr => 'statistics: information about WAL activity', proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => '', - proallargtypes => '{int8,int8,numeric,int8,timestamptz}', - proargmodes => '{o,o,o,o,o}', - proargnames => '{wal_records,wal_fpi,wal_bytes,wal_buffers_full,stats_reset}', + proallargtypes => '{int8,int8,numeric,numeric,int8,timestamptz}', + proargmodes => '{o,o,o,o,o,o}', + proargnames => '{wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}', prosrc => 'pg_stat_get_wal' }, -{ oid => '8037', descr => 'statistics: backend WAL activity', +{ oid => '6313', descr => 'statistics: backend WAL activity', proname => 'pg_stat_get_backend_wal', provolatile => 'v', proparallel => 'r', prorettype => 'record', proargtypes => 'int4', - proallargtypes => '{int4,int8,int8,numeric,int8,timestamptz}', - proargmodes => '{i,o,o,o,o,o}', - proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_buffers_full,stats_reset}', + proallargtypes => '{int4,int8,int8,numeric,numeric,int8,timestamptz}', + proargmodes => '{i,o,o,o,o,o,o}', + proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}', prosrc => 'pg_stat_get_backend_wal' }, { oid => '6248', descr => 'statistics: information about WAL prefetching', proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't', @@ -6054,6 +6123,10 @@ proname => 'pg_stat_get_function_self_time', provolatile => 's', proparallel => 'r', prorettype => 'float8', proargtypes => 'oid', prosrc => 'pg_stat_get_function_self_time' }, +{ oid => '8745', descr => 'statistics: last reset for a function', + proname => 'pg_stat_get_function_stat_reset_time', provolatile => 's', + proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid', + prosrc => 'pg_stat_get_function_stat_reset_time' }, { oid => '3037', descr => 'statistics: number of scans done for table/index in current transaction', @@ -6139,41 +6212,51 @@ { oid => '2274', descr => 'statistics: reset collected statistics for current database', proname => 'pg_stat_reset', proisstrict => 'f', provolatile => 'v', - prorettype => 'void', proargtypes => '', prosrc => 'pg_stat_reset' }, + prorettype => 'void', proargtypes => '', prosrc => 'pg_stat_reset', + proacl => '{POSTGRES=X}' }, { oid => '3775', descr => 'statistics: reset collected statistics shared across the cluster', proname => 'pg_stat_reset_shared', proisstrict => 'f', provolatile => 'v', prorettype => 'void', proargtypes => 'text', - prosrc => 'pg_stat_reset_shared' }, + proargnames => '{target}', proargdefaults => '{NULL}', + prosrc => 'pg_stat_reset_shared', + proacl => '{POSTGRES=X}' }, { oid => '3776', descr => 'statistics: reset collected statistics for a single table or index in the current database or shared across all databases in the cluster', proname => 'pg_stat_reset_single_table_counters', provolatile => 'v', prorettype => 'void', proargtypes => 'oid', - prosrc => 'pg_stat_reset_single_table_counters' }, + prosrc => 'pg_stat_reset_single_table_counters', + proacl => '{POSTGRES=X}' }, { oid => '3777', descr => 'statistics: reset collected statistics for a single function in the current database', proname => 'pg_stat_reset_single_function_counters', provolatile => 'v', prorettype => 'void', proargtypes => 'oid', - prosrc => 'pg_stat_reset_single_function_counters' }, -{ oid => '8807', descr => 'statistics: reset statistics for a single backend', + prosrc => 'pg_stat_reset_single_function_counters', + proacl => '{POSTGRES=X}' }, +{ oid => '6387', descr => 'statistics: reset statistics for a single backend', proname => 'pg_stat_reset_backend_stats', provolatile => 'v', prorettype => 'void', proargtypes => 'int4', - prosrc => 'pg_stat_reset_backend_stats' }, + prosrc => 'pg_stat_reset_backend_stats', + proacl => '{POSTGRES=X}' }, { oid => '2307', descr => 'statistics: reset collected statistics for a single SLRU', proname => 'pg_stat_reset_slru', proisstrict => 'f', provolatile => 'v', prorettype => 'void', proargtypes => 'text', proargnames => '{target}', - prosrc => 'pg_stat_reset_slru' }, + proargdefaults => '{NULL}', + prosrc => 'pg_stat_reset_slru', + proacl => '{POSTGRES=X}' }, { oid => '6170', descr => 'statistics: reset collected statistics for a single replication slot', proname => 'pg_stat_reset_replication_slot', proisstrict => 'f', provolatile => 'v', prorettype => 'void', proargtypes => 'text', - prosrc => 'pg_stat_reset_replication_slot' }, + prosrc => 'pg_stat_reset_replication_slot', + proacl => '{POSTGRES=X}' }, { oid => '6232', descr => 'statistics: reset collected statistics for a single subscription', proname => 'pg_stat_reset_subscription_stats', proisstrict => 'f', provolatile => 'v', prorettype => 'void', proargtypes => 'oid', - prosrc => 'pg_stat_reset_subscription_stats' }, + prosrc => 'pg_stat_reset_subscription_stats', + proacl => '{POSTGRES=X}' }, { oid => '3163', descr => 'current trigger depth', proname => 'pg_trigger_depth', provolatile => 's', proparallel => 'r', @@ -6369,10 +6452,10 @@ { oid => '3411', descr => 'hash', proname => 'timestamp_hash_extended', prorettype => 'int8', proargtypes => 'timestamp int8', prosrc => 'timestamp_hash_extended' }, -{ oid => '9720', descr => 'hash', +{ oid => '6425', descr => 'hash', proname => 'timestamptz_hash', prorettype => 'int4', proargtypes => 'timestamptz', prosrc => 'timestamptz_hash' }, -{ oid => '9721', descr => 'hash', +{ oid => '6426', descr => 'hash', proname => 'timestamptz_hash_extended', prorettype => 'int8', proargtypes => 'timestamptz int8', prosrc => 'timestamptz_hash_extended' }, { oid => '2041', descr => 'intervals overlap?', @@ -6397,7 +6480,7 @@ { oid => '3137', descr => 'sort support', proname => 'timestamp_sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'timestamp_sortsupport' }, -{ oid => '9297', descr => 'skip support', +{ oid => '6409', descr => 'skip support', proname => 'timestamp_skipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'timestamp_skipsupport' }, @@ -6520,21 +6603,24 @@ proallargtypes => '{text,int4,int4,text,text,bool,text}', proargmodes => '{o,o,o,o,o,o,o}', proargnames => '{sourcefile,sourceline,seqno,name,setting,applied,error}', - prosrc => 'show_all_file_settings' }, + prosrc => 'show_all_file_settings', + proacl => '{POSTGRES=X}' }, { oid => '3401', descr => 'show pg_hba.conf rules', proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}', proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}', - prosrc => 'pg_hba_file_rules' }, + prosrc => 'pg_hba_file_rules', + proacl => '{POSTGRES=X}' }, { oid => '6250', descr => 'show pg_ident.conf mappings', proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{int4,text,int4,text,text,text,text}', proargmodes => '{o,o,o,o,o,o,o}', proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}', - prosrc => 'pg_ident_file_mappings' }, + prosrc => 'pg_ident_file_mappings', + proacl => '{POSTGRES=X}' }, { oid => '1371', descr => 'view system lock information', proname => 'pg_lock_status', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', @@ -6568,6 +6654,13 @@ proallargtypes => '{xid,xid,text}', proargmodes => '{i,o,o}', proargnames => '{multixid,xid,mode}', prosrc => 'pg_get_multixact_members' }, +{ oid => '9001', descr => 'get current multixact usage statistics', + proname => 'pg_get_multixact_stats', provolatile => 'v', + prorettype => 'record', proargtypes => '', + proallargtypes => '{int8,int8,int8,xid}', proargmodes => '{o,o,o,o}', + proargnames => '{num_mxids,num_members,members_size,oldest_multixact}', + prosrc => 'pg_get_multixact_stats' }, + { oid => '3581', descr => 'get commit timestamp of a transaction', proname => 'pg_xact_commit_timestamp', provolatile => 'v', prorettype => 'timestamptz', proargtypes => 'xid', @@ -6593,7 +6686,7 @@ proname => 'pg_describe_object', provolatile => 's', prorettype => 'text', proargtypes => 'oid oid int4', prosrc => 'pg_describe_object' }, -{ oid => '8730', descr => 'get ACL for SQL object', +{ oid => '6385', descr => 'get ACL for SQL object', proname => 'pg_get_acl', provolatile => 's', prorettype => '_aclitem', proargtypes => 'oid oid int4', proargnames => '{classid,objid,objsubid}', prosrc => 'pg_get_acl' }, @@ -6691,32 +6784,42 @@ { oid => '2096', descr => 'terminate a server process', proname => 'pg_terminate_backend', provolatile => 'v', prorettype => 'bool', proargtypes => 'int4 int8', proargnames => '{pid,timeout}', + proargdefaults => '{0}', prosrc => 'pg_terminate_backend' }, { oid => '2172', descr => 'prepare for taking an online backup', proname => 'pg_backup_start', provolatile => 'v', proparallel => 'r', prorettype => 'pg_lsn', proargtypes => 'text bool', - prosrc => 'pg_backup_start' }, + proargnames => '{label,fast}', proargdefaults => '{false}', + prosrc => 'pg_backup_start', + proacl => '{POSTGRES=X}' }, { oid => '2739', descr => 'finish taking an online backup', proname => 'pg_backup_stop', provolatile => 'v', proparallel => 'r', prorettype => 'record', proargtypes => 'bool', proallargtypes => '{bool,pg_lsn,text,text}', proargmodes => '{i,o,o,o}', proargnames => '{wait_for_archive,lsn,labelfile,spcmapfile}', - prosrc => 'pg_backup_stop' }, + proargdefaults => '{true}', + prosrc => 'pg_backup_stop', + proacl => '{POSTGRES=X}' }, { oid => '3436', descr => 'promote standby server', proname => 'pg_promote', provolatile => 'v', prorettype => 'bool', proargtypes => 'bool int4', proargnames => '{wait,wait_seconds}', - prosrc => 'pg_promote' }, + proargdefaults => '{true,60}', + prosrc => 'pg_promote', + proacl => '{POSTGRES=X}' }, { oid => '2848', descr => 'switch to new wal file', proname => 'pg_switch_wal', provolatile => 'v', prorettype => 'pg_lsn', - proargtypes => '', prosrc => 'pg_switch_wal' }, + proargtypes => '', prosrc => 'pg_switch_wal', + proacl => '{POSTGRES=X}' }, { oid => '6305', descr => 'log details of the current snapshot to WAL', proname => 'pg_log_standby_snapshot', provolatile => 'v', prorettype => 'pg_lsn', proargtypes => '', - prosrc => 'pg_log_standby_snapshot' }, + prosrc => 'pg_log_standby_snapshot', + proacl => '{POSTGRES=X}' }, { oid => '3098', descr => 'create a named restore point', proname => 'pg_create_restore_point', provolatile => 'v', prorettype => 'pg_lsn', proargtypes => 'text', - prosrc => 'pg_create_restore_point' }, + prosrc => 'pg_create_restore_point', + proacl => '{POSTGRES=X}' }, { oid => '2849', descr => 'current wal write location', proname => 'pg_current_wal_lsn', provolatile => 'v', prorettype => 'pg_lsn', proargtypes => '', prosrc => 'pg_current_wal_lsn' }, @@ -6772,10 +6875,12 @@ { oid => '3071', descr => 'pause wal replay', proname => 'pg_wal_replay_pause', provolatile => 'v', prorettype => 'void', - proargtypes => '', prosrc => 'pg_wal_replay_pause' }, + proargtypes => '', prosrc => 'pg_wal_replay_pause', + proacl => '{POSTGRES=X}' }, { oid => '3072', descr => 'resume wal replay, if it was paused', proname => 'pg_wal_replay_resume', provolatile => 'v', prorettype => 'void', - proargtypes => '', prosrc => 'pg_wal_replay_resume' }, + proargtypes => '', prosrc => 'pg_wal_replay_resume', + proacl => '{POSTGRES=X}' }, { oid => '3073', descr => 'true if wal replay is paused', proname => 'pg_is_wal_replay_paused', provolatile => 'v', prorettype => 'bool', proargtypes => '', @@ -6792,7 +6897,7 @@ proargnames => '{rm_id, rm_name, rm_builtin}', prosrc => 'pg_get_wal_resource_managers' }, -{ oid => '8303', descr => 'get info about loaded modules', +{ oid => '6353', descr => 'get info about loaded modules', proname => 'pg_get_loaded_modules', prorows => '10', proretset => 't', provolatile => 'v', proparallel => 'r', prorettype => 'record', proargtypes => '', proallargtypes => '{text,text,text}', @@ -6801,17 +6906,21 @@ { oid => '2621', descr => 'reload configuration files', proname => 'pg_reload_conf', provolatile => 'v', prorettype => 'bool', - proargtypes => '', prosrc => 'pg_reload_conf' }, + proargtypes => '', prosrc => 'pg_reload_conf', + proacl => '{POSTGRES=X}' }, { oid => '2622', descr => 'rotate log file', proname => 'pg_rotate_logfile', provolatile => 'v', prorettype => 'bool', - proargtypes => '', prosrc => 'pg_rotate_logfile' }, + proargtypes => '', prosrc => 'pg_rotate_logfile', + proacl => '{POSTGRES=X}' }, { oid => '3800', descr => 'current logging collector file location', proname => 'pg_current_logfile', proisstrict => 'f', provolatile => 'v', - prorettype => 'text', proargtypes => '', prosrc => 'pg_current_logfile' }, + prorettype => 'text', proargtypes => '', prosrc => 'pg_current_logfile', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '3801', descr => 'current logging collector file location', proname => 'pg_current_logfile', proisstrict => 'f', provolatile => 'v', prorettype => 'text', proargtypes => 'text', - prosrc => 'pg_current_logfile_1arg' }, + prosrc => 'pg_current_logfile_1arg', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '2623', descr => 'get information about file', proname => 'pg_stat_file', provolatile => 'v', prorettype => 'record', @@ -6819,48 +6928,60 @@ proallargtypes => '{text,int8,timestamptz,timestamptz,timestamptz,timestamptz,bool}', proargmodes => '{i,o,o,o,o,o,o}', proargnames => '{filename,size,access,modification,change,creation,isdir}', - prosrc => 'pg_stat_file_1arg' }, + prosrc => 'pg_stat_file_1arg', + proacl => '{POSTGRES=X}' }, { oid => '3307', descr => 'get information about file', proname => 'pg_stat_file', provolatile => 'v', prorettype => 'record', proargtypes => 'text bool', proallargtypes => '{text,bool,int8,timestamptz,timestamptz,timestamptz,timestamptz,bool}', proargmodes => '{i,i,o,o,o,o,o,o}', proargnames => '{filename,missing_ok,size,access,modification,change,creation,isdir}', - prosrc => 'pg_stat_file' }, + prosrc => 'pg_stat_file', + proacl => '{POSTGRES=X}' }, { oid => '2624', descr => 'read text from a file', proname => 'pg_read_file', provolatile => 'v', prorettype => 'text', - proargtypes => 'text int8 int8', prosrc => 'pg_read_file_off_len' }, + proargtypes => 'text int8 int8', prosrc => 'pg_read_file_off_len', + proacl => '{POSTGRES=X}' }, { oid => '3293', descr => 'read text from a file', proname => 'pg_read_file', provolatile => 'v', prorettype => 'text', proargtypes => 'text int8 int8 bool', - prosrc => 'pg_read_file_off_len_missing' }, + prosrc => 'pg_read_file_off_len_missing', + proacl => '{POSTGRES=X}' }, { oid => '3826', descr => 'read text from a file', proname => 'pg_read_file', provolatile => 'v', prorettype => 'text', - proargtypes => 'text', prosrc => 'pg_read_file_all' }, + proargtypes => 'text', prosrc => 'pg_read_file_all', + proacl => '{POSTGRES=X}' }, { oid => '6208', descr => 'read text from a file', proname => 'pg_read_file', provolatile => 'v', prorettype => 'text', - proargtypes => 'text bool', prosrc => 'pg_read_file_all_missing' }, + proargtypes => 'text bool', prosrc => 'pg_read_file_all_missing', + proacl => '{POSTGRES=X}' }, { oid => '3827', descr => 'read bytea from a file', proname => 'pg_read_binary_file', provolatile => 'v', prorettype => 'bytea', - proargtypes => 'text int8 int8', prosrc => 'pg_read_binary_file_off_len' }, + proargtypes => 'text int8 int8', prosrc => 'pg_read_binary_file_off_len', + proacl => '{POSTGRES=X}' }, { oid => '3295', descr => 'read bytea from a file', proname => 'pg_read_binary_file', provolatile => 'v', prorettype => 'bytea', proargtypes => 'text int8 int8 bool', - prosrc => 'pg_read_binary_file_off_len_missing' }, + prosrc => 'pg_read_binary_file_off_len_missing', + proacl => '{POSTGRES=X}' }, { oid => '3828', descr => 'read bytea from a file', proname => 'pg_read_binary_file', provolatile => 'v', prorettype => 'bytea', - proargtypes => 'text', prosrc => 'pg_read_binary_file_all' }, + proargtypes => 'text', prosrc => 'pg_read_binary_file_all', + proacl => '{POSTGRES=X}' }, { oid => '6209', descr => 'read bytea from a file', proname => 'pg_read_binary_file', provolatile => 'v', prorettype => 'bytea', - proargtypes => 'text bool', prosrc => 'pg_read_binary_file_all_missing' }, + proargtypes => 'text bool', prosrc => 'pg_read_binary_file_all_missing', + proacl => '{POSTGRES=X}' }, { oid => '2625', descr => 'list all files in a directory', proname => 'pg_ls_dir', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'text', proargtypes => 'text', - prosrc => 'pg_ls_dir_1arg' }, + prosrc => 'pg_ls_dir_1arg', + proacl => '{POSTGRES=X}' }, { oid => '3297', descr => 'list all files in a directory', proname => 'pg_ls_dir', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'text', proargtypes => 'text bool bool', - prosrc => 'pg_ls_dir' }, + prosrc => 'pg_ls_dir', + proacl => '{POSTGRES=X}' }, { oid => '2626', descr => 'sleep for the specified time in seconds', proname => 'pg_sleep', provolatile => 'v', prorettype => 'void', proargtypes => 'float8', prosrc => 'pg_sleep' }, @@ -6992,7 +7113,7 @@ proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'anyarray', proargtypes => 'anyarray', prosrc => 'aggregate_dummy' }, -{ oid => '8595', descr => 'maximum value of all record input values', +{ oid => '6373', descr => 'maximum value of all record input values', proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'record', proargtypes => 'record', prosrc => 'aggregate_dummy' }, { oid => '2244', descr => 'maximum value of all bpchar input values', @@ -7010,7 +7131,7 @@ { oid => '5099', descr => 'maximum value of all xid8 input values', proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'xid8', proargtypes => 'xid8', prosrc => 'aggregate_dummy' }, -{ oid => '8922', descr => 'maximum value of all bytea input values', +{ oid => '6395', descr => 'maximum value of all bytea input values', proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'bytea', proargtypes => 'bytea', prosrc => 'aggregate_dummy' }, @@ -7068,7 +7189,7 @@ proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'anyarray', proargtypes => 'anyarray', prosrc => 'aggregate_dummy' }, -{ oid => '8596', descr => 'minimum value of all record input values', +{ oid => '6374', descr => 'minimum value of all record input values', proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'record', proargtypes => 'record', prosrc => 'aggregate_dummy' }, { oid => '2245', descr => 'minimum value of all bpchar input values', @@ -7086,7 +7207,7 @@ { oid => '5100', descr => 'minimum value of all xid8 input values', proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'xid8', proargtypes => 'xid8', prosrc => 'aggregate_dummy' }, -{ oid => '8923', descr => 'minimum value of all bytea input values', +{ oid => '6396', descr => 'minimum value of all bytea input values', proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'bytea', proargtypes => 'bytea', prosrc => 'aggregate_dummy' }, @@ -7454,6 +7575,17 @@ prorettype => 'regnamespace', proargtypes => 'text', prosrc => 'to_regnamespace' }, +{ oid => '8321', descr => 'I/O', + proname => 'regdatabasein', provolatile => 's', prorettype => 'regdatabase', + proargtypes => 'cstring', prosrc => 'regdatabasein' }, +{ oid => '8322', descr => 'I/O', + proname => 'regdatabaseout', provolatile => 's', prorettype => 'cstring', + proargtypes => 'regdatabase', prosrc => 'regdatabaseout' }, +{ oid => '8323', descr => 'convert database name to regdatabase', + proname => 'to_regdatabase', provolatile => 's', + prorettype => 'regdatabase', proargtypes => 'text', + prosrc => 'to_regdatabase' }, + { oid => '6210', descr => 'test whether string is valid input for data type', proname => 'pg_input_is_valid', provolatile => 's', prorettype => 'bool', proargtypes => 'text text', prosrc => 'pg_input_is_valid' }, @@ -7469,7 +7601,8 @@ { oid => '1268', descr => 'parse qualified identifier to array of identifiers', proname => 'parse_ident', prorettype => '_text', proargtypes => 'text bool', - proargnames => '{str,strict}', prosrc => 'parse_ident' }, + proargnames => '{str,strict}', proargdefaults => '{true}', + prosrc => 'parse_ident' }, { oid => '2246', descr => '(internal)', proname => 'fmgr_internal_validator', provolatile => 's', @@ -7949,10 +8082,10 @@ proargtypes => 'internal', prosrc => 'tsm_system_handler' }, # CRC variants -{ oid => '8571', descr => 'CRC-32 value', +{ oid => '6364', descr => 'CRC-32 value', proname => 'crc32', proleakproof => 't', prorettype => 'int8', proargtypes => 'bytea', prosrc => 'crc32_bytea' }, -{ oid => '8572', descr => 'CRC-32C value', +{ oid => '6365', descr => 'CRC-32C value', proname => 'crc32c', proleakproof => 't', prorettype => 'int8', proargtypes => 'bytea', prosrc => 'crc32c_bytea' }, @@ -8312,6 +8445,12 @@ { oid => '4088', descr => 'I/O', proname => 'regnamespacesend', prorettype => 'bytea', proargtypes => 'regnamespace', prosrc => 'regnamespacesend' }, +{ oid => '8324', descr => 'I/O', + proname => 'regdatabaserecv', prorettype => 'regdatabase', + proargtypes => 'internal', prosrc => 'regdatabaserecv' }, +{ oid => '8325', descr => 'I/O', + proname => 'regdatabasesend', prorettype => 'bytea', + proargtypes => 'regdatabase', prosrc => 'regdatabasesend' }, { oid => '2456', descr => 'I/O', proname => 'bit_recv', prorettype => 'bit', proargtypes => 'internal oid int4', prosrc => 'bit_recv' }, @@ -8477,6 +8616,38 @@ { oid => '2508', descr => 'constraint description with pretty-print option', proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' }, +{ oid => '8760', descr => 'get DDL to recreate a role', + proname => 'pg_get_role_ddl', provariadic => 'text', proisstrict => 'f', + provolatile => 's', proretset => 't', prorows => '10', prorettype => 'text', + proargtypes => 'regrole text', + proargmodes => '{i,v}', + proallargtypes => '{regrole,text}', + pronargdefaults => '1', proargdefaults => '{NULL}', + prosrc => 'pg_get_role_ddl' }, +{ oid => '8758', descr => 'get DDL to recreate a tablespace', + proname => 'pg_get_tablespace_ddl', provariadic => 'text', proisstrict => 'f', + provolatile => 's', proretset => 't', prorows => '10', prorettype => 'text', + proargtypes => 'oid text', + proargmodes => '{i,v}', + proallargtypes => '{oid,text}', + pronargdefaults => '1', proargdefaults => '{NULL}', + prosrc => 'pg_get_tablespace_ddl_oid' }, +{ oid => '8759', descr => 'get DDL to recreate a tablespace', + proname => 'pg_get_tablespace_ddl', provariadic => 'text', proisstrict => 'f', + provolatile => 's', proretset => 't', prorows => '10', prorettype => 'text', + proargtypes => 'name text', + proargmodes => '{i,v}', + proallargtypes => '{name,text}', + pronargdefaults => '1', proargdefaults => '{NULL}', + prosrc => 'pg_get_tablespace_ddl_name' }, +{ oid => '8762', descr => 'get DDL to recreate a database', + proname => 'pg_get_database_ddl', provariadic => 'text', proisstrict => 'f', + provolatile => 's', proretset => 't', prorows => '10', prorettype => 'text', + proargtypes => 'regdatabase text', + proargmodes => '{i,v}', + proallargtypes => '{regdatabase,text}', + pronargdefaults => '1', proargdefaults => '{NULL}', + prosrc => 'pg_get_database_ddl' }, { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', @@ -8496,7 +8667,7 @@ proargmodes => '{o,o,o,o,o,o}', proargnames => '{name,statement,is_holdable,is_binary,is_scrollable,creation_time}', prosrc => 'pg_cursor' }, -{ oid => '9221', descr => 'get abbreviations from current timezone', +{ oid => '6401', descr => 'get abbreviations from current timezone', proname => 'pg_timezone_abbrevs_zone', prorows => '10', proretset => 't', provolatile => 's', prorettype => 'record', proargtypes => '', proallargtypes => '{text,interval,bool}', proargmodes => '{o,o,o}', @@ -8540,7 +8711,8 @@ provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,int8,int8}', proargmodes => '{o,o,o,o}', proargnames => '{name,off,size,allocated_size}', - prosrc => 'pg_get_shmem_allocations' }, + prosrc => 'pg_get_shmem_allocations', + proacl => '{POSTGRES=X,pg_read_all_stats=X}' }, { oid => '4099', descr => 'Is NUMA support available?', proname => 'pg_numa_available', provolatile => 's', prorettype => 'bool', @@ -8552,7 +8724,17 @@ provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int4,int8}', proargmodes => '{o,o,o}', proargnames => '{name,numa_node,size}', - prosrc => 'pg_get_shmem_allocations_numa' }, + prosrc => 'pg_get_shmem_allocations_numa', + proacl => '{POSTGRES=X,pg_read_all_stats=X}' }, + +{ oid => '9314', + descr => 'shared memory allocations tracked in the DSM registry', + proname => 'pg_get_dsm_registry_allocations', prorows => '50', + proretset => 't', provolatile => 'v', prorettype => 'record', + proargtypes => '', proallargtypes => '{text,text,int8}', + proargmodes => '{o,o,o}', proargnames => '{name,type,size}', + prosrc => 'pg_get_dsm_registry_allocations', + proacl => '{POSTGRES=X,pg_read_all_stats=X}' }, # memory context of local backend { oid => '2282', @@ -8563,13 +8745,15 @@ proallargtypes => '{text,text,text,int4,_int4,int8,int8,int8,int8,int8}', proargmodes => '{o,o,o,o,o,o,o,o,o,o}', proargnames => '{name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes}', - prosrc => 'pg_get_backend_memory_contexts' }, + prosrc => 'pg_get_backend_memory_contexts', + proacl => '{POSTGRES=X,pg_read_all_stats=X}' }, # logging memory contexts of the specified backend { oid => '4543', descr => 'log memory contexts of the specified backend', proname => 'pg_log_backend_memory_contexts', provolatile => 'v', prorettype => 'bool', proargtypes => 'int4', - prosrc => 'pg_log_backend_memory_contexts' }, + prosrc => 'pg_log_backend_memory_contexts', + proacl => '{POSTGRES=X}' }, # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', @@ -8608,7 +8792,7 @@ prosupport => 'generate_series_numeric_support', proretset => 't', prorettype => 'numeric', proargtypes => 'numeric numeric', prosrc => 'generate_series_numeric' }, -{ oid => '8405', descr => 'planner support for generate_series', +{ oid => '6357', descr => 'planner support for generate_series', proname => 'generate_series_numeric_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'generate_series_numeric_support' }, { oid => '938', descr => 'non-persistent series generator', @@ -8628,7 +8812,7 @@ prorettype => 'timestamptz', proargtypes => 'timestamptz timestamptz interval text', prosrc => 'generate_series_timestamptz_at_zone' }, -{ oid => '8402', descr => 'planner support for generate_series', +{ oid => '6354', descr => 'planner support for generate_series', proname => 'generate_series_timestamp_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'generate_series_timestamp_support' }, @@ -9360,7 +9544,9 @@ proname => 'to_json', provolatile => 's', prorettype => 'json', proargtypes => 'anyelement', prosrc => 'to_json' }, { oid => '3261', descr => 'remove object fields with null values from json', - proname => 'json_strip_nulls', prorettype => 'json', proargtypes => 'json bool', + proname => 'json_strip_nulls', prorettype => 'json', + proargtypes => 'json bool', + proargnames => '{target,strip_in_arrays}', proargdefaults => '{false}', prosrc => 'json_strip_nulls' }, { oid => '3947', @@ -9418,12 +9604,17 @@ { oid => '3960', descr => 'get record fields from a json object', proname => 'json_populate_record', proisstrict => 'f', provolatile => 's', prorettype => 'anyelement', proargtypes => 'anyelement json bool', + proargnames => '{base,from_json,use_json_as_text}', + proargdefaults => '{false}', prosrc => 'json_populate_record' }, { oid => '3961', descr => 'get set of records with fields from a json array of objects', proname => 'json_populate_recordset', prorows => '100', proisstrict => 'f', proretset => 't', provolatile => 's', prorettype => 'anyelement', - proargtypes => 'anyelement json bool', prosrc => 'json_populate_recordset' }, + proargtypes => 'anyelement json bool', + proargnames => '{base,from_json,use_json_as_text}', + proargdefaults => '{false}', + prosrc => 'json_populate_recordset' }, { oid => '3204', descr => 'get record fields from a json object', proname => 'json_to_record', provolatile => 's', prorettype => 'record', proargtypes => 'json', prosrc => 'json_to_record' }, @@ -9467,7 +9658,7 @@ { oid => '3300', descr => 'sort support', proname => 'uuid_sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'uuid_sortsupport' }, -{ oid => '9298', descr => 'skip support', +{ oid => '6410', descr => 'skip support', proname => 'uuid_skipsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'uuid_skipsupport' }, { oid => '2961', descr => 'I/O', @@ -9483,17 +9674,19 @@ proname => 'uuid_hash_extended', prorettype => 'int8', proargtypes => 'uuid int8', prosrc => 'uuid_hash_extended' }, { oid => '3432', descr => 'generate random UUID', - proname => 'gen_random_uuid', provolatile => 'v', - prorettype => 'uuid', proargtypes => '', prosrc => 'gen_random_uuid' }, -{ oid => '9895', descr => 'generate UUID version 4', - proname => 'uuidv4', provolatile => 'v', - prorettype => 'uuid', proargtypes => '', prosrc => 'gen_random_uuid' }, -{ oid => '9896', descr => 'generate UUID version 7', - proname => 'uuidv7', provolatile => 'v', - prorettype => 'uuid', proargtypes => '', prosrc => 'uuidv7' }, -{ oid => '9897', descr => 'generate UUID version 7 with a timestamp shifted by specified interval', - proname => 'uuidv7', provolatile => 'v', proargnames => '{shift}', - prorettype => 'uuid', proargtypes => 'interval', prosrc => 'uuidv7_interval' }, + proname => 'gen_random_uuid', provolatile => 'v', prorettype => 'uuid', + proargtypes => '', prosrc => 'gen_random_uuid' }, +{ oid => '6428', descr => 'generate UUID version 4', + proname => 'uuidv4', provolatile => 'v', prorettype => 'uuid', + proargtypes => '', prosrc => 'gen_random_uuid' }, +{ oid => '6429', descr => 'generate UUID version 7', + proname => 'uuidv7', provolatile => 'v', prorettype => 'uuid', + proargtypes => '', prosrc => 'uuidv7' }, +{ oid => '6430', + descr => 'generate UUID version 7 with a timestamp shifted by specified interval', + proname => 'uuidv7', provolatile => 'v', prorettype => 'uuid', + proargtypes => 'interval', proargnames => '{shift}', + prosrc => 'uuidv7_interval' }, { oid => '6342', descr => 'extract timestamp from UUID', proname => 'uuid_extract_timestamp', proleakproof => 't', prorettype => 'timestamptz', proargtypes => 'uuid', @@ -10299,7 +10492,9 @@ prorettype => 'jsonb', proargtypes => '', prosrc => 'jsonb_build_object_noargs' }, { oid => '3262', descr => 'remove object fields with null values from jsonb', - proname => 'jsonb_strip_nulls', prorettype => 'jsonb', proargtypes => 'jsonb bool', + proname => 'jsonb_strip_nulls', prorettype => 'jsonb', + proargtypes => 'jsonb bool', + proargnames => '{target,strip_in_arrays}', proargdefaults => '{false}', prosrc => 'jsonb_strip_nulls' }, { oid => '3478', @@ -10474,16 +10669,25 @@ proargtypes => 'jsonb _text', prosrc => 'jsonb_delete_path' }, { oid => '5054', descr => 'Set part of a jsonb, handle NULL value', proname => 'jsonb_set_lax', proisstrict => 'f', prorettype => 'jsonb', - proargtypes => 'jsonb _text jsonb bool text', prosrc => 'jsonb_set_lax' }, + proargtypes => 'jsonb _text jsonb bool text', + proargnames => '{jsonb_in,path,replacement,create_if_missing,null_value_treatment}', + proargdefaults => '{true,use_json_null}', + prosrc => 'jsonb_set_lax' }, { oid => '3305', descr => 'Set part of a jsonb', proname => 'jsonb_set', prorettype => 'jsonb', - proargtypes => 'jsonb _text jsonb bool', prosrc => 'jsonb_set' }, + proargtypes => 'jsonb _text jsonb bool', + proargnames => '{jsonb_in,path,replacement,create_if_missing}', + proargdefaults => '{true}', + prosrc => 'jsonb_set' }, { oid => '3306', descr => 'Indented text from jsonb', proname => 'jsonb_pretty', prorettype => 'text', proargtypes => 'jsonb', prosrc => 'jsonb_pretty' }, { oid => '3579', descr => 'Insert value into a jsonb', proname => 'jsonb_insert', prorettype => 'jsonb', - proargtypes => 'jsonb _text jsonb bool', prosrc => 'jsonb_insert' }, + proargtypes => 'jsonb _text jsonb bool', + proargnames => '{jsonb_in,path,replacement,insert_after}', + proargdefaults => '{false}', + prosrc => 'jsonb_insert' }, # jsonpath { oid => '4001', descr => 'I/O', @@ -10501,42 +10705,66 @@ { oid => '4005', descr => 'jsonpath exists test', proname => 'jsonb_path_exists', prorettype => 'bool', - proargtypes => 'jsonb jsonpath jsonb bool', prosrc => 'jsonb_path_exists' }, + proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', + prosrc => 'jsonb_path_exists' }, { oid => '4006', descr => 'jsonpath query', proname => 'jsonb_path_query', prorows => '1000', proretset => 't', prorettype => 'jsonb', proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', prosrc => 'jsonb_path_query' }, { oid => '4007', descr => 'jsonpath query wrapped into array', proname => 'jsonb_path_query_array', prorettype => 'jsonb', proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', prosrc => 'jsonb_path_query_array' }, { oid => '4008', descr => 'jsonpath query first item', proname => 'jsonb_path_query_first', prorettype => 'jsonb', proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', prosrc => 'jsonb_path_query_first' }, { oid => '4009', descr => 'jsonpath match', proname => 'jsonb_path_match', prorettype => 'bool', - proargtypes => 'jsonb jsonpath jsonb bool', prosrc => 'jsonb_path_match' }, + proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', + prosrc => 'jsonb_path_match' }, { oid => '1177', descr => 'jsonpath exists test with timezone', proname => 'jsonb_path_exists_tz', provolatile => 's', prorettype => 'bool', proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', prosrc => 'jsonb_path_exists_tz' }, { oid => '1179', descr => 'jsonpath query with timezone', proname => 'jsonb_path_query_tz', prorows => '1000', proretset => 't', provolatile => 's', prorettype => 'jsonb', - proargtypes => 'jsonb jsonpath jsonb bool', prosrc => 'jsonb_path_query_tz' }, + proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', + prosrc => 'jsonb_path_query_tz' }, { oid => '1180', descr => 'jsonpath query wrapped into array with timezone', proname => 'jsonb_path_query_array_tz', provolatile => 's', prorettype => 'jsonb', proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', prosrc => 'jsonb_path_query_array_tz' }, { oid => '2023', descr => 'jsonpath query first item with timezone', proname => 'jsonb_path_query_first_tz', provolatile => 's', prorettype => 'jsonb', proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', prosrc => 'jsonb_path_query_first_tz' }, { oid => '2030', descr => 'jsonpath match with timezone', proname => 'jsonb_path_match_tz', provolatile => 's', prorettype => 'bool', - proargtypes => 'jsonb jsonpath jsonb bool', prosrc => 'jsonb_path_match_tz' }, + proargtypes => 'jsonb jsonpath jsonb bool', + proargnames => '{target,path,vars,silent}', + proargdefaults => '{"{}",false}', + prosrc => 'jsonb_path_match_tz' }, { oid => '4010', descr => 'implementation of @? operator', proname => 'jsonb_path_exists_opr', prorettype => 'bool', @@ -10651,10 +10879,10 @@ { oid => '2987', descr => 'less-equal-greater', proname => 'btrecordcmp', prorettype => 'int4', proargtypes => 'record record', prosrc => 'btrecordcmp' }, -{ oid => '8597', descr => 'larger of two', +{ oid => '6375', descr => 'larger of two', proname => 'record_larger', prorettype => 'record', proargtypes => 'record record', prosrc => 'record_larger' }, -{ oid => '8598', descr => 'smaller of two', +{ oid => '6376', descr => 'smaller of two', proname => 'record_smaller', prorettype => 'record', proargtypes => 'record record', prosrc => 'record_smaller' }, @@ -10695,16 +10923,16 @@ { oid => '3082', descr => 'list available extensions', proname => 'pg_available_extensions', procost => '10', prorows => '100', proretset => 't', provolatile => 's', prorettype => 'record', - proargtypes => '', proallargtypes => '{name,text,text}', - proargmodes => '{o,o,o}', proargnames => '{name,default_version,comment}', + proargtypes => '', proallargtypes => '{name,text,text,text}', + proargmodes => '{o,o,o,o}', proargnames => '{name,default_version,location,comment}', prosrc => 'pg_available_extensions' }, { oid => '3083', descr => 'list available extension versions', proname => 'pg_available_extension_versions', procost => '10', prorows => '100', proretset => 't', provolatile => 's', prorettype => 'record', proargtypes => '', - proallargtypes => '{name,text,bool,bool,bool,name,_name,text}', - proargmodes => '{o,o,o,o,o,o,o,o}', - proargnames => '{name,version,superuser,trusted,relocatable,schema,requires,comment}', + proallargtypes => '{name,text,bool,bool,bool,name,_name,text,text}', + proargmodes => '{o,o,o,o,o,o,o,o,o}', + proargnames => '{name,version,superuser,trusted,relocatable,schema,requires,location,comment}', prosrc => 'pg_available_extension_versions' }, { oid => '3084', descr => 'list an extension\'s version update paths', proname => 'pg_extension_update_paths', procost => '10', prorows => '100', @@ -10891,10 +11119,14 @@ { oid => '3869', proname => 'range_minus', prorettype => 'anyrange', proargtypes => 'anyrange anyrange', prosrc => 'range_minus' }, +{ oid => '8412', descr => 'remove portion from range', + proname => 'range_minus_multi', prorows => '2', + proretset => 't', prorettype => 'anyrange', + proargtypes => 'anyrange anyrange', prosrc => 'range_minus_multi' }, { oid => '3870', descr => 'less-equal-greater', proname => 'range_cmp', prorettype => 'int4', proargtypes => 'anyrange anyrange', prosrc => 'range_cmp' }, -{ oid => '8849', descr => 'sort support', +{ oid => '6391', descr => 'sort support', proname => 'range_sortsupport', prorettype => 'void', proargtypes => 'internal', prosrc => 'range_sortsupport' }, { oid => '3871', @@ -11181,6 +11413,10 @@ { oid => '4271', proname => 'multirange_minus', prorettype => 'anymultirange', proargtypes => 'anymultirange anymultirange', prosrc => 'multirange_minus' }, +{ oid => '8411', descr => 'remove portion from multirange', + proname => 'multirange_minus_multi', prorows => '1', + proretset => 't', prorettype => 'anymultirange', + proargtypes => 'anymultirange anymultirange', prosrc => 'multirange_minus_multi' }, { oid => '4272', proname => 'multirange_intersect', prorettype => 'anymultirange', proargtypes => 'anymultirange anymultirange', @@ -11339,6 +11575,7 @@ proname => 'make_interval', prorettype => 'interval', proargtypes => 'int4 int4 int4 int4 int4 int4 float8', proargnames => '{years,months,weeks,days,hours,mins,secs}', + proargdefaults => '{0,0,0,0,0,0,0.0}', prosrc => 'make_interval' }, # spgist opclasses @@ -11439,6 +11676,7 @@ proallargtypes => '{name,bool,bool,name,pg_lsn}', proargmodes => '{i,i,i,o,o}', proargnames => '{slot_name,immediately_reserve,temporary,slot_name,lsn}', + proargdefaults => '{false,false}', prosrc => 'pg_create_physical_replication_slot' }, { oid => '4220', descr => 'copy a physical replication slot, changing temporality', @@ -11463,9 +11701,9 @@ proname => 'pg_get_replication_slots', prorows => '10', proisstrict => 'f', proretset => 't', provolatile => 's', prorettype => 'record', proargtypes => '', - proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,pg_lsn,timestamptz,bool,text,bool,bool}', - proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}', - proargnames => '{slot_name,plugin,slot_type,datoid,temporary,active,active_pid,xmin,catalog_xmin,restart_lsn,confirmed_flush_lsn,wal_status,safe_wal_size,two_phase,two_phase_at,inactive_since,conflicting,invalidation_reason,failover,synced}', + proallargtypes => '{name,name,text,oid,bool,bool,int4,xid,xid,pg_lsn,pg_lsn,text,int8,bool,pg_lsn,timestamptz,bool,text,bool,bool,text}', + proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{slot_name,plugin,slot_type,datoid,temporary,active,active_pid,xmin,catalog_xmin,restart_lsn,confirmed_flush_lsn,wal_status,safe_wal_size,two_phase,two_phase_at,inactive_since,conflicting,invalidation_reason,failover,synced,slotsync_skip_reason}', prosrc => 'pg_get_replication_slots' }, { oid => '3786', descr => 'set up a logical replication slot', proname => 'pg_create_logical_replication_slot', provolatile => 'v', @@ -11474,6 +11712,7 @@ proallargtypes => '{name,name,bool,bool,bool,name,pg_lsn}', proargmodes => '{i,i,i,i,i,o,o}', proargnames => '{slot_name,plugin,temporary,twophase,failover,slot_name,lsn}', + proargdefaults => '{false,false,false}', prosrc => 'pg_create_logical_replication_slot' }, { oid => '4222', descr => 'copy a logical replication slot, changing temporality and plugin', @@ -11506,6 +11745,7 @@ proallargtypes => '{name,pg_lsn,int4,_text,pg_lsn,xid,text}', proargmodes => '{i,i,i,v,o,o,o}', proargnames => '{slot_name,upto_lsn,upto_nchanges,options,lsn,xid,data}', + proargdefaults => '{"{}"}', prosrc => 'pg_logical_slot_get_changes' }, { oid => '3783', descr => 'get binary changes from replication slot', proname => 'pg_logical_slot_get_binary_changes', procost => '1000', @@ -11515,6 +11755,7 @@ proallargtypes => '{name,pg_lsn,int4,_text,pg_lsn,xid,bytea}', proargmodes => '{i,i,i,v,o,o,o}', proargnames => '{slot_name,upto_lsn,upto_nchanges,options,lsn,xid,data}', + proargdefaults => '{"{}"}', prosrc => 'pg_logical_slot_get_binary_changes' }, { oid => '3784', descr => 'peek at changes from replication slot', proname => 'pg_logical_slot_peek_changes', procost => '1000', @@ -11524,6 +11765,7 @@ proallargtypes => '{name,pg_lsn,int4,_text,pg_lsn,xid,text}', proargmodes => '{i,i,i,v,o,o,o}', proargnames => '{slot_name,upto_lsn,upto_nchanges,options,lsn,xid,data}', + proargdefaults => '{"{}"}', prosrc => 'pg_logical_slot_peek_changes' }, { oid => '3785', descr => 'peek at binary changes from replication slot', proname => 'pg_logical_slot_peek_binary_changes', procost => '1000', @@ -11533,6 +11775,7 @@ proallargtypes => '{name,pg_lsn,int4,_text,pg_lsn,xid,bytea}', proargmodes => '{i,i,i,v,o,o,o}', proargnames => '{slot_name,upto_lsn,upto_nchanges,options,lsn,xid,data}', + proargdefaults => '{"{}"}', prosrc => 'pg_logical_slot_peek_binary_changes' }, { oid => '3878', descr => 'advance logical replication slot', proname => 'pg_replication_slot_advance', provolatile => 'v', @@ -11543,10 +11786,14 @@ { oid => '3577', descr => 'emit a textual logical decoding message', proname => 'pg_logical_emit_message', provolatile => 'v', proparallel => 'u', prorettype => 'pg_lsn', proargtypes => 'bool text text bool', + proargnames => '{transactional,prefix,message,flush}', + proargdefaults => '{false}', prosrc => 'pg_logical_emit_message_text' }, { oid => '3578', descr => 'emit a binary logical decoding message', proname => 'pg_logical_emit_message', provolatile => 'v', proparallel => 'u', prorettype => 'pg_lsn', proargtypes => 'bool text bytea bool', + proargnames => '{transactional,prefix,message,flush}', + proargdefaults => '{false}', prosrc => 'pg_logical_emit_message_bytea' }, { oid => '6344', descr => 'sync replication slots from the primary to the standby', @@ -11760,9 +12007,9 @@ proparallel => 'u', prorettype => 'void', proargtypes => 'oid', prosrc => 'binary_upgrade_set_next_pg_tablespace_oid' }, { oid => '6312', descr => 'for use by pg_upgrade', - proname => 'binary_upgrade_logical_slot_has_caught_up', provolatile => 'v', - proparallel => 'u', prorettype => 'bool', proargtypes => 'name', - prosrc => 'binary_upgrade_logical_slot_has_caught_up' }, + proname => 'binary_upgrade_check_logical_slot_pending_wal', provolatile => 'v', + proparallel => 'u', prorettype => 'pg_lsn', proargtypes => 'name pg_lsn', + prosrc => 'binary_upgrade_check_logical_slot_pending_wal' }, { oid => '6319', descr => 'for use by pg_upgrade (relation for pg_subscription_rel)', proname => 'binary_upgrade_add_sub_rel_state', proisstrict => 'f', @@ -11773,114 +12020,68 @@ proname => 'binary_upgrade_replorigin_advance', proisstrict => 'f', provolatile => 'v', proparallel => 'u', prorettype => 'void', proargtypes => 'text pg_lsn', prosrc => 'binary_upgrade_replorigin_advance' }, +{ oid => '9159', descr => 'for use by pg_upgrade (conflict detection slot)', + proname => 'binary_upgrade_create_conflict_detection_slot', proisstrict => 'f', + provolatile => 'v', proparallel => 'u', prorettype => 'void', + proargtypes => '', prosrc => 'binary_upgrade_create_conflict_detection_slot' }, # conversion functions -{ oid => '4302', - descr => 'internal conversion function for KOI8R to MULE_INTERNAL', - proname => 'koi8r_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'koi8r_to_mic', probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4303', - descr => 'internal conversion function for MULE_INTERNAL to KOI8R', - proname => 'mic_to_koi8r', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_koi8r', probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4304', - descr => 'internal conversion function for ISO-8859-5 to MULE_INTERNAL', - proname => 'iso_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', prosrc => 'iso_to_mic', - probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4305', - descr => 'internal conversion function for MULE_INTERNAL to ISO-8859-5', - proname => 'mic_to_iso', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', prosrc => 'mic_to_iso', - probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4306', - descr => 'internal conversion function for WIN1251 to MULE_INTERNAL', - proname => 'win1251_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win1251_to_mic', probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4307', - descr => 'internal conversion function for MULE_INTERNAL to WIN1251', - proname => 'mic_to_win1251', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_win1251', probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4308', - descr => 'internal conversion function for WIN866 to MULE_INTERNAL', - proname => 'win866_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win866_to_mic', probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4309', - descr => 'internal conversion function for MULE_INTERNAL to WIN866', - proname => 'mic_to_win866', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_win866', probin => '$libdir/cyrillic_and_mic' }, { oid => '4310', descr => 'internal conversion function for KOI8R to WIN1251', proname => 'koi8r_to_win1251', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'koi8r_to_win1251', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'koi8r_to_win1251', probin => '$libdir/cyrillic' }, { oid => '4311', descr => 'internal conversion function for WIN1251 to KOI8R', proname => 'win1251_to_koi8r', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win1251_to_koi8r', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'win1251_to_koi8r', probin => '$libdir/cyrillic' }, { oid => '4312', descr => 'internal conversion function for KOI8R to WIN866', proname => 'koi8r_to_win866', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'koi8r_to_win866', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'koi8r_to_win866', probin => '$libdir/cyrillic' }, { oid => '4313', descr => 'internal conversion function for WIN866 to KOI8R', proname => 'win866_to_koi8r', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win866_to_koi8r', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'win866_to_koi8r', probin => '$libdir/cyrillic' }, { oid => '4314', descr => 'internal conversion function for WIN866 to WIN1251', proname => 'win866_to_win1251', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win866_to_win1251', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'win866_to_win1251', probin => '$libdir/cyrillic' }, { oid => '4315', descr => 'internal conversion function for WIN1251 to WIN866', proname => 'win1251_to_win866', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win1251_to_win866', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'win1251_to_win866', probin => '$libdir/cyrillic' }, { oid => '4316', descr => 'internal conversion function for ISO-8859-5 to KOI8R', proname => 'iso_to_koi8r', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'iso_to_koi8r', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'iso_to_koi8r', probin => '$libdir/cyrillic' }, { oid => '4317', descr => 'internal conversion function for KOI8R to ISO-8859-5', proname => 'koi8r_to_iso', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'koi8r_to_iso', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'koi8r_to_iso', probin => '$libdir/cyrillic' }, { oid => '4318', descr => 'internal conversion function for ISO-8859-5 to WIN1251', proname => 'iso_to_win1251', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'iso_to_win1251', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'iso_to_win1251', probin => '$libdir/cyrillic' }, { oid => '4319', descr => 'internal conversion function for WIN1251 to ISO-8859-5', proname => 'win1251_to_iso', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win1251_to_iso', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'win1251_to_iso', probin => '$libdir/cyrillic' }, { oid => '4320', descr => 'internal conversion function for ISO-8859-5 to WIN866', proname => 'iso_to_win866', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'iso_to_win866', probin => '$libdir/cyrillic_and_mic' }, + prosrc => 'iso_to_win866', probin => '$libdir/cyrillic' }, { oid => '4321', descr => 'internal conversion function for WIN866 to ISO-8859-5', proname => 'win866_to_iso', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win866_to_iso', probin => '$libdir/cyrillic_and_mic' }, -{ oid => '4322', - descr => 'internal conversion function for EUC_CN to MULE_INTERNAL', - proname => 'euc_cn_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'euc_cn_to_mic', probin => '$libdir/euc_cn_and_mic' }, -{ oid => '4323', - descr => 'internal conversion function for MULE_INTERNAL to EUC_CN', - proname => 'mic_to_euc_cn', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_euc_cn', probin => '$libdir/euc_cn_and_mic' }, + prosrc => 'win866_to_iso', probin => '$libdir/cyrillic' }, { oid => '4324', descr => 'internal conversion function for EUC_JP to SJIS', proname => 'euc_jp_to_sjis', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', @@ -11889,36 +12090,6 @@ proname => 'sjis_to_euc_jp', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', prosrc => 'sjis_to_euc_jp', probin => '$libdir/euc_jp_and_sjis' }, -{ oid => '4326', - descr => 'internal conversion function for EUC_JP to MULE_INTERNAL', - proname => 'euc_jp_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'euc_jp_to_mic', probin => '$libdir/euc_jp_and_sjis' }, -{ oid => '4327', - descr => 'internal conversion function for SJIS to MULE_INTERNAL', - proname => 'sjis_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'sjis_to_mic', probin => '$libdir/euc_jp_and_sjis' }, -{ oid => '4328', - descr => 'internal conversion function for MULE_INTERNAL to EUC_JP', - proname => 'mic_to_euc_jp', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_euc_jp', probin => '$libdir/euc_jp_and_sjis' }, -{ oid => '4329', - descr => 'internal conversion function for MULE_INTERNAL to SJIS', - proname => 'mic_to_sjis', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_sjis', probin => '$libdir/euc_jp_and_sjis' }, -{ oid => '4330', - descr => 'internal conversion function for EUC_KR to MULE_INTERNAL', - proname => 'euc_kr_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'euc_kr_to_mic', probin => '$libdir/euc_kr_and_mic' }, -{ oid => '4331', - descr => 'internal conversion function for MULE_INTERNAL to EUC_KR', - proname => 'mic_to_euc_kr', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_euc_kr', probin => '$libdir/euc_kr_and_mic' }, { oid => '4332', descr => 'internal conversion function for EUC_TW to BIG5', proname => 'euc_tw_to_big5', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', @@ -11927,46 +12098,6 @@ proname => 'big5_to_euc_tw', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', prosrc => 'big5_to_euc_tw', probin => '$libdir/euc_tw_and_big5' }, -{ oid => '4334', - descr => 'internal conversion function for EUC_TW to MULE_INTERNAL', - proname => 'euc_tw_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'euc_tw_to_mic', probin => '$libdir/euc_tw_and_big5' }, -{ oid => '4335', - descr => 'internal conversion function for BIG5 to MULE_INTERNAL', - proname => 'big5_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'big5_to_mic', probin => '$libdir/euc_tw_and_big5' }, -{ oid => '4336', - descr => 'internal conversion function for MULE_INTERNAL to EUC_TW', - proname => 'mic_to_euc_tw', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_euc_tw', probin => '$libdir/euc_tw_and_big5' }, -{ oid => '4337', - descr => 'internal conversion function for MULE_INTERNAL to BIG5', - proname => 'mic_to_big5', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_big5', probin => '$libdir/euc_tw_and_big5' }, -{ oid => '4338', - descr => 'internal conversion function for LATIN2 to MULE_INTERNAL', - proname => 'latin2_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'latin2_to_mic', probin => '$libdir/latin2_and_win1250' }, -{ oid => '4339', - descr => 'internal conversion function for MULE_INTERNAL to LATIN2', - proname => 'mic_to_latin2', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_latin2', probin => '$libdir/latin2_and_win1250' }, -{ oid => '4340', - descr => 'internal conversion function for WIN1250 to MULE_INTERNAL', - proname => 'win1250_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'win1250_to_mic', probin => '$libdir/latin2_and_win1250' }, -{ oid => '4341', - descr => 'internal conversion function for MULE_INTERNAL to WIN1250', - proname => 'mic_to_win1250', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_win1250', probin => '$libdir/latin2_and_win1250' }, { oid => '4342', descr => 'internal conversion function for LATIN2 to WIN1250', proname => 'latin2_to_win1250', prolang => 'c', prorettype => 'int4', @@ -11977,36 +12108,6 @@ proname => 'win1250_to_latin2', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', prosrc => 'win1250_to_latin2', probin => '$libdir/latin2_and_win1250' }, -{ oid => '4344', - descr => 'internal conversion function for LATIN1 to MULE_INTERNAL', - proname => 'latin1_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'latin1_to_mic', probin => '$libdir/latin_and_mic' }, -{ oid => '4345', - descr => 'internal conversion function for MULE_INTERNAL to LATIN1', - proname => 'mic_to_latin1', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_latin1', probin => '$libdir/latin_and_mic' }, -{ oid => '4346', - descr => 'internal conversion function for LATIN3 to MULE_INTERNAL', - proname => 'latin3_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'latin3_to_mic', probin => '$libdir/latin_and_mic' }, -{ oid => '4347', - descr => 'internal conversion function for MULE_INTERNAL to LATIN3', - proname => 'mic_to_latin3', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_latin3', probin => '$libdir/latin_and_mic' }, -{ oid => '4348', - descr => 'internal conversion function for LATIN4 to MULE_INTERNAL', - proname => 'latin4_to_mic', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'latin4_to_mic', probin => '$libdir/latin_and_mic' }, -{ oid => '4349', - descr => 'internal conversion function for MULE_INTERNAL to LATIN4', - proname => 'mic_to_latin4', prolang => 'c', prorettype => 'int4', - proargtypes => 'int4 int4 cstring internal int4 bool', - prosrc => 'mic_to_latin4', probin => '$libdir/latin_and_mic' }, { oid => '4352', descr => 'internal conversion function for BIG5 to UTF8', proname => 'big5_to_utf8', prolang => 'c', prorettype => 'int4', proargtypes => 'int4 int4 cstring internal int4 bool', @@ -12175,62 +12276,74 @@ { oid => '6003', descr => 'create a replication origin', proname => 'pg_replication_origin_create', provolatile => 'v', proparallel => 'u', prorettype => 'oid', proargtypes => 'text', - prosrc => 'pg_replication_origin_create' }, + prosrc => 'pg_replication_origin_create', + proacl => '{POSTGRES=X}' }, { oid => '6004', descr => 'drop replication origin identified by its name', proname => 'pg_replication_origin_drop', provolatile => 'v', proparallel => 'u', prorettype => 'void', proargtypes => 'text', - prosrc => 'pg_replication_origin_drop' }, + prosrc => 'pg_replication_origin_drop', + proacl => '{POSTGRES=X}' }, { oid => '6005', descr => 'translate the replication origin\'s name to its id', proname => 'pg_replication_origin_oid', provolatile => 's', prorettype => 'oid', proargtypes => 'text', - prosrc => 'pg_replication_origin_oid' }, + prosrc => 'pg_replication_origin_oid', + proacl => '{POSTGRES=X}' }, { oid => '6006', descr => 'configure session to maintain replication progress tracking for the passed in origin', proname => 'pg_replication_origin_session_setup', provolatile => 'v', - proparallel => 'u', prorettype => 'void', proargtypes => 'text', - prosrc => 'pg_replication_origin_session_setup' }, + proparallel => 'u', prorettype => 'void', proargtypes => 'text int4', + proargnames => '{node_name,pid}', proargdefaults => '{0}', + prosrc => 'pg_replication_origin_session_setup', + proacl => '{POSTGRES=X}' }, { oid => '6007', descr => 'teardown configured replication progress tracking', proname => 'pg_replication_origin_session_reset', provolatile => 'v', proparallel => 'u', prorettype => 'void', proargtypes => '', - prosrc => 'pg_replication_origin_session_reset' }, + prosrc => 'pg_replication_origin_session_reset', + proacl => '{POSTGRES=X}' }, { oid => '6008', descr => 'is a replication origin configured in this session', proname => 'pg_replication_origin_session_is_setup', provolatile => 'v', proparallel => 'r', prorettype => 'bool', proargtypes => '', - prosrc => 'pg_replication_origin_session_is_setup' }, + prosrc => 'pg_replication_origin_session_is_setup', + proacl => '{POSTGRES=X}' }, { oid => '6009', descr => 'get the replication progress of the current session', proname => 'pg_replication_origin_session_progress', provolatile => 'v', proparallel => 'u', prorettype => 'pg_lsn', proargtypes => 'bool', - prosrc => 'pg_replication_origin_session_progress' }, + prosrc => 'pg_replication_origin_session_progress', + proacl => '{POSTGRES=X}' }, { oid => '6010', descr => 'setup the transaction\'s origin lsn and timestamp', proname => 'pg_replication_origin_xact_setup', provolatile => 'v', proparallel => 'r', prorettype => 'void', proargtypes => 'pg_lsn timestamptz', - prosrc => 'pg_replication_origin_xact_setup' }, + prosrc => 'pg_replication_origin_xact_setup', + proacl => '{POSTGRES=X}' }, { oid => '6011', descr => 'reset the transaction\'s origin lsn and timestamp', proname => 'pg_replication_origin_xact_reset', provolatile => 'v', proparallel => 'r', prorettype => 'void', proargtypes => '', - prosrc => 'pg_replication_origin_xact_reset' }, + prosrc => 'pg_replication_origin_xact_reset', + proacl => '{POSTGRES=X}' }, { oid => '6012', descr => 'advance replication origin to specific location', proname => 'pg_replication_origin_advance', provolatile => 'v', proparallel => 'u', prorettype => 'void', proargtypes => 'text pg_lsn', - prosrc => 'pg_replication_origin_advance' }, + prosrc => 'pg_replication_origin_advance', + proacl => '{POSTGRES=X}' }, { oid => '6013', descr => 'get an individual replication origin\'s replication progress', proname => 'pg_replication_origin_progress', provolatile => 'v', proparallel => 'u', prorettype => 'pg_lsn', proargtypes => 'text bool', - prosrc => 'pg_replication_origin_progress' }, + prosrc => 'pg_replication_origin_progress', + proacl => '{POSTGRES=X}' }, { oid => '6014', descr => 'get progress for all replication origins', proname => 'pg_show_replication_origin_status', prorows => '100', @@ -12238,7 +12351,8 @@ prorettype => 'record', proargtypes => '', proallargtypes => '{oid,text,pg_lsn,pg_lsn}', proargmodes => '{o,o,o,o}', proargnames => '{local_id, external_id, remote_lsn, local_lsn}', - prosrc => 'pg_show_replication_origin_status' }, + prosrc => 'pg_show_replication_origin_status', + proacl => '{POSTGRES=X}' }, # publications { oid => '6119', @@ -12248,8 +12362,22 @@ prorettype => 'record', proargtypes => '_text', proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}', proargmodes => '{v,o,o,o,o}', - proargnames => '{pubname,pubid,relid,attrs,qual}', - prosrc => 'pg_get_publication_tables' }, + proargnames => '{pubnames,pubid,relid,attrs,qual}', + prosrc => 'pg_get_publication_tables_a' }, +{ oid => '8060', + descr => 'get information of the specified table that is part of the specified publications', + proname => 'pg_get_publication_tables', prorows => '10', + proretset => 't', provolatile => 's', + prorettype => 'record', proargtypes => '_text oid', + proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}', + proargmodes => '{i,i,o,o,o,o}', + proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}', + prosrc => 'pg_get_publication_tables_b' }, +{ oid => '8052', descr => 'get OIDs of sequences in a publication', + proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't', + provolatile => 's', prorettype => 'oid', proargtypes => 'text', + proallargtypes => '{text,oid}', proargmodes => '{i,o}', + proargnames => '{pubname,relid}', prosrc => 'pg_get_publication_sequences' }, { oid => '6121', descr => 'returns whether a relation can be part of a publication', proname => 'pg_relation_is_publishable', provolatile => 's', @@ -12271,7 +12399,8 @@ proname => 'pg_config', prorows => '23', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => '', proallargtypes => '{text,text}', proargmodes => '{o,o}', - proargnames => '{name,setting}', prosrc => 'pg_config' }, + proargnames => '{name,setting}', prosrc => 'pg_config', + proacl => '{POSTGRES=X}' }, # pg_controldata related functions { oid => '3441', @@ -12313,7 +12442,7 @@ proname => 'array_subscript_handler', prosupport => 'array_subscript_handler_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'array_subscript_handler' }, -{ oid => '8682', descr => 'planner support for array_subscript_handler', +{ oid => '6380', descr => 'planner support for array_subscript_handler', proname => 'array_subscript_handler_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'array_subscript_handler_support' }, { oid => '6180', descr => 'raw array subscripting support', @@ -12324,6 +12453,20 @@ proname => 'jsonb_subscript_handler', prorettype => 'internal', proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' }, +# data checksum management functions +{ oid => '9258', + descr => 'disable data checksums', + proname => 'pg_disable_data_checksums', provolatile => 'v', prorettype => 'void', + proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '', + proacl => '{POSTGRES=X}'}, +{ oid => '9257', + descr => 'enable data checksums', + proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void', + proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}', + proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}', + proargdefaults => '{0,100}', prosrc => 'enable_data_checksums', + proacl => '{POSTGRES=X}'}, + # collation management functions { oid => '3445', descr => 'import collations from operating system', proname => 'pg_import_system_collations', procost => '100', @@ -12346,49 +12489,57 @@ proname => 'pg_ls_logdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', - proargnames => '{name,size,modification}', prosrc => 'pg_ls_logdir' }, + proargnames => '{name,size,modification}', prosrc => 'pg_ls_logdir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '3354', descr => 'list of files in the WAL directory', proname => 'pg_ls_waldir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', - proargnames => '{name,size,modification}', prosrc => 'pg_ls_waldir' }, -{ oid => '9220', descr => 'list of files in the pg_wal/summaries directory', + proargnames => '{name,size,modification}', prosrc => 'pg_ls_waldir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, +{ oid => '6400', descr => 'list of files in the pg_wal/summaries directory', proname => 'pg_ls_summariesdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', proargnames => '{name,size,modification}', - prosrc => 'pg_ls_summariesdir' }, + prosrc => 'pg_ls_summariesdir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '5031', descr => 'list of files in the archive_status directory', proname => 'pg_ls_archive_statusdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', proargnames => '{name,size,modification}', - prosrc => 'pg_ls_archive_statusdir' }, + prosrc => 'pg_ls_archive_statusdir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '5029', descr => 'list files in the pgsql_tmp directory', proname => 'pg_ls_tmpdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', - proargnames => '{name,size,modification}', prosrc => 'pg_ls_tmpdir_noargs' }, + proargnames => '{name,size,modification}', prosrc => 'pg_ls_tmpdir_noargs', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '5030', descr => 'list files in the pgsql_tmp directory', proname => 'pg_ls_tmpdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => 'oid', proallargtypes => '{oid,text,int8,timestamptz}', proargmodes => '{i,o,o,o}', proargnames => '{tablespace,name,size,modification}', - prosrc => 'pg_ls_tmpdir_1arg' }, + prosrc => 'pg_ls_tmpdir_1arg', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '6270', descr => 'list of files in the pg_logical/snapshots directory', proname => 'pg_ls_logicalsnapdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', proargnames => '{name,size,modification}', - prosrc => 'pg_ls_logicalsnapdir' }, + prosrc => 'pg_ls_logicalsnapdir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '6271', descr => 'list of files in the pg_logical/mappings directory', proname => 'pg_ls_logicalmapdir', procost => '10', prorows => '20', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', proallargtypes => '{text,int8,timestamptz}', proargmodes => '{o,o,o}', proargnames => '{name,size,modification}', - prosrc => 'pg_ls_logicalmapdir' }, + prosrc => 'pg_ls_logicalmapdir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, { oid => '6272', descr => 'list of files in the pg_replslot/slot_name directory', proname => 'pg_ls_replslotdir', procost => '10', prorows => '20', @@ -12396,7 +12547,8 @@ proargtypes => 'text', proallargtypes => '{text,text,int8,timestamptz}', proargmodes => '{i,o,o,o}', proargnames => '{slot_name,name,size,modification}', - prosrc => 'pg_ls_replslotdir' }, + prosrc => 'pg_ls_replslotdir', + proacl => '{POSTGRES=X,pg_monitor=X}' }, # hash partitioning constraint function { oid => '5028', descr => 'hash partition CHECK constraint', @@ -12437,10 +12589,12 @@ { oid => '4350', descr => 'Unicode normalization', proname => 'normalize', prorettype => 'text', proargtypes => 'text text', + proargdefaults => '{NFC}', prosrc => 'unicode_normalize_func' }, { oid => '4351', descr => 'check Unicode normalization', proname => 'is_normalized', prorettype => 'bool', proargtypes => 'text text', + proargdefaults => '{NFC}', prosrc => 'unicode_is_normalized' }, { oid => '6198', descr => 'unescape Unicode characters', @@ -12486,6 +12640,9 @@ { oid => '6292', descr => 'aggregate transition function', proname => 'any_value_transfn', prorettype => 'anyelement', proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' }, +{ oid => '8488', descr => 'check if input is the null value', + proname => 'error_on_null', proisstrict => 'f', prorettype => 'anyelement', + proargtypes => 'anyelement', prosrc => 'pg_error_on_null' }, { oid => '6321', descr => 'list of available WAL summary files', proname => 'pg_available_wal_summaries', prorows => '100', proretset => 't', @@ -12508,52 +12665,108 @@ proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}', prosrc => 'pg_get_wal_summarizer_state' }, # Statistics Import -{ oid => '8459', - descr => 'restore statistics on relation', - proname => 'pg_restore_relation_stats', provolatile => 'v', proisstrict => 'f', - provariadic => 'any', - proparallel => 'u', prorettype => 'bool', - proargtypes => 'any', - proargnames => '{kwargs}', - proargmodes => '{v}', - prosrc => 'pg_restore_relation_stats' }, -{ oid => '9160', - descr => 'clear statistics on relation', - proname => 'pg_clear_relation_stats', provolatile => 'v', proisstrict => 'f', - proparallel => 'u', prorettype => 'void', - proargtypes => 'text text', - proargnames => '{schemaname,relname}', - prosrc => 'pg_clear_relation_stats' }, -{ oid => '8461', - descr => 'restore statistics on attribute', - proname => 'pg_restore_attribute_stats', provolatile => 'v', proisstrict => 'f', - provariadic => 'any', - proparallel => 'u', prorettype => 'bool', - proargtypes => 'any', - proargnames => '{kwargs}', - proargmodes => '{v}', - prosrc => 'pg_restore_attribute_stats' }, -{ oid => '9162', - descr => 'clear statistics on attribute', - proname => 'pg_clear_attribute_stats', provolatile => 'v', proisstrict => 'f', +{ oid => '6362', descr => 'restore statistics on relation', + proname => 'pg_restore_relation_stats', provariadic => 'any', + proisstrict => 'f', provolatile => 'v', proparallel => 'u', + prorettype => 'bool', proargtypes => 'any', proargmodes => '{v}', + proargnames => '{kwargs}', prosrc => 'pg_restore_relation_stats' }, +{ oid => '6397', descr => 'clear statistics on relation', + proname => 'pg_clear_relation_stats', proisstrict => 'f', provolatile => 'v', + proparallel => 'u', prorettype => 'void', proargtypes => 'text text', + proargnames => '{schemaname,relname}', prosrc => 'pg_clear_relation_stats' }, +{ oid => '6363', descr => 'restore statistics on attribute', + proname => 'pg_restore_attribute_stats', provariadic => 'any', + proisstrict => 'f', provolatile => 'v', proparallel => 'u', + prorettype => 'bool', proargtypes => 'any', proargmodes => '{v}', + proargnames => '{kwargs}', prosrc => 'pg_restore_attribute_stats' }, +{ oid => '6398', descr => 'clear statistics on attribute', + proname => 'pg_clear_attribute_stats', proisstrict => 'f', provolatile => 'v', proparallel => 'u', prorettype => 'void', proargtypes => 'text text text bool', proargnames => '{schemaname,relname,attname,inherited}', prosrc => 'pg_clear_attribute_stats' }, # GiST stratnum implementations -{ oid => '8047', descr => 'GiST support', - proname => 'gist_stratnum_common', prorettype => 'int2', - proargtypes => 'int4', - prosrc => 'gist_stratnum_common' }, +{ oid => '6347', descr => 'GiST support', + proname => 'gist_translate_cmptype_common', prorettype => 'int2', + proargtypes => 'int4', prosrc => 'gist_translate_cmptype_common' }, + +# Extended Statistics functions +{ oid => '9947', descr => 'restore statistics on extended statistics object', + proname => 'pg_restore_extended_stats', provariadic => 'any', + proisstrict => 'f', provolatile => 'v', proparallel => 'u', + prorettype => 'bool', proargtypes => 'any', proargmodes => '{v}', + proargnames => '{kwargs}', prosrc => 'pg_restore_extended_stats' }, +{ oid => '9948', descr => 'clear statistics on extended statistics object', + proname => 'pg_clear_extended_stats', proisstrict => 'f', provolatile => 'v', + proparallel => 'u', prorettype => 'void', proargtypes => 'text text text text bool', + proargnames => '{schemaname,relname,statistics_schemaname,statistics_name,inherited}', + prosrc => 'pg_clear_extended_stats' }, # AIO related functions -{ oid => '9200', descr => 'information about in-progress asynchronous IOs', +{ oid => '6399', descr => 'information about in-progress asynchronous IOs', proname => 'pg_get_aios', prorows => '100', proretset => 't', - provolatile => 'v', proparallel => 'r', prorettype => 'record', proargtypes => '', + provolatile => 'v', proparallel => 'r', prorettype => 'record', + proargtypes => '', proallargtypes => '{int4,int4,int8,text,text,int8,int8,text,int2,int4,text,text,bool,bool,bool}', proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', - prosrc => 'pg_get_aios' }, + prosrc => 'pg_get_aios', + proacl => '{POSTGRES=X,pg_read_all_stats=X}' }, + +# oid8 related functions +{ oid => '8255', descr => 'convert oid to oid8', + proname => 'oid8', prorettype => 'oid8', proargtypes => 'oid', + prosrc => 'oidtooid8' }, +{ oid => '8257', descr => 'I/O', + proname => 'oid8in', prorettype => 'oid8', proargtypes => 'cstring', + prosrc => 'oid8in' }, +{ oid => '8258', descr => 'I/O', + proname => 'oid8out', prorettype => 'cstring', proargtypes => 'oid8', + prosrc => 'oid8out' }, +{ oid => '8259', descr => 'I/O', + proname => 'oid8recv', prorettype => 'oid8', proargtypes => 'internal', + prosrc => 'oid8recv' }, +{ oid => '8260', descr => 'I/O', + proname => 'oid8send', prorettype => 'bytea', proargtypes => 'oid8', + prosrc => 'oid8send' }, +# Comparators +{ oid => '8268', + proname => 'oid8eq', proleakproof => 't', prorettype => 'bool', + proargtypes => 'oid8 oid8', prosrc => 'oid8eq' }, +{ oid => '8269', + proname => 'oid8ne', proleakproof => 't', prorettype => 'bool', + proargtypes => 'oid8 oid8', prosrc => 'oid8ne' }, +{ oid => '8270', + proname => 'oid8lt', proleakproof => 't', prorettype => 'bool', + proargtypes => 'oid8 oid8', prosrc => 'oid8lt' }, +{ oid => '8271', + proname => 'oid8le', proleakproof => 't', prorettype => 'bool', + proargtypes => 'oid8 oid8', prosrc => 'oid8le' }, +{ oid => '8272', + proname => 'oid8gt', proleakproof => 't', prorettype => 'bool', + proargtypes => 'oid8 oid8', prosrc => 'oid8gt' }, +{ oid => '8273', + proname => 'oid8ge', proleakproof => 't', prorettype => 'bool', + proargtypes => 'oid8 oid8', prosrc => 'oid8ge' }, +# Aggregates +{ oid => '8274', descr => 'larger of two', + proname => 'oid8larger', prorettype => 'oid8', proargtypes => 'oid8 oid8', + prosrc => 'oid8larger' }, +{ oid => '8275', descr => 'smaller of two', + proname => 'oid8smaller', prorettype => 'oid8', proargtypes => 'oid8 oid8', + prosrc => 'oid8smaller' }, +{ oid => '8276', descr => 'maximum value of all oid8 input values', + proname => 'max', prokind => 'a', proisstrict => 'f', prorettype => 'oid8', + proargtypes => 'oid8', prosrc => 'aggregate_dummy' }, +{ oid => '8277', descr => 'minimum value of all oid8 input values', + proname => 'min', prokind => 'a', proisstrict => 'f', prorettype => 'oid8', + proargtypes => 'oid8', prosrc => 'aggregate_dummy' }, +{ oid => '8280', descr => 'hash', + proname => 'hashoid8', prorettype => 'int4', proargtypes => 'oid8', + prosrc => 'hashoid8' }, +{ oid => '8281', descr => 'hash', + proname => 'hashoid8extended', prorettype => 'int8', + proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' }, ] diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index d7353e7a088ff..2f9e0b695e26b 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -3,7 +3,7 @@ * pg_proc.h * definition of the "procedure" system catalog (pg_proc) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_proc.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_proc * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,ProcedureRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid oid; /* oid */ @@ -128,6 +130,8 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce #endif } FormData_pg_proc; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_proc corresponds to a pointer to a tuple with * the format of pg_proc relation. diff --git a/src/include/catalog/pg_propgraph_element.h b/src/include/catalog/pg_propgraph_element.h new file mode 100644 index 0000000000000..2f8af537691b1 --- /dev/null +++ b/src/include/catalog/pg_propgraph_element.h @@ -0,0 +1,118 @@ +/*------------------------------------------------------------------------- + * + * pg_propgraph_element.h + * definition of the "property graph elements" system catalog (pg_propgraph_element) + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_propgraph_element.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PROPGRAPH_ELEMENT_H +#define PG_PROPGRAPH_ELEMENT_H + +#include "catalog/genbki.h" +#include "catalog/pg_propgraph_element_d.h" + +/* ---------------- + * pg_propgraph_element definition. cpp turns this into + * typedef struct FormData_pg_propgraph_element + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_propgraph_element,8299,PropgraphElementRelationId) +{ + Oid oid; + + /* OID of the property graph relation */ + Oid pgepgid BKI_LOOKUP(pg_class); + + /* OID of the element table */ + Oid pgerelid BKI_LOOKUP(pg_class); + + /* element alias */ + NameData pgealias; + + /* vertex or edge? -- see PGEKIND_* below */ + char pgekind; + + /* for edges: source vertex */ + Oid pgesrcvertexid BKI_LOOKUP_OPT(pg_propgraph_element); + + /* for edges: destination vertex */ + Oid pgedestvertexid BKI_LOOKUP_OPT(pg_propgraph_element); + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + /* element key (column numbers in pgerelid relation) */ + int16 pgekey[1] BKI_FORCE_NOT_NULL; + + /* + * for edges: source vertex key (column numbers in pgerelid relation) + */ + int16 pgesrckey[1]; + + /* + * for edges: source vertex table referenced columns (column numbers in + * relation reached via pgesrcvertexid) + */ + int16 pgesrcref[1]; + + /* + * for edges: Oids of the equality operators for comparing source keys + */ + Oid pgesrceqop[1]; + + /* + * for edges: destination vertex key (column numbers in pgerelid relation) + */ + int16 pgedestkey[1]; + + /* + * for edges: destination vertex table referenced columns (column numbers + * in relation reached via pgedestvertexid) + */ + int16 pgedestref[1]; + + /* + * for edges: Oids of the equality operators for comparing destination + * keys + */ + Oid pgedesteqop[1]; +#endif +} FormData_pg_propgraph_element; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_propgraph_element corresponds to a pointer to a tuple with + * the format of pg_propgraph_element relation. + * ---------------- + */ +typedef FormData_pg_propgraph_element *Form_pg_propgraph_element; + +DECLARE_TOAST(pg_propgraph_element, 8315, 8316); + +DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_element_oid_index, 8300, PropgraphElementObjectIndexId, pg_propgraph_element, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_propgraph_element_alias_index, 8301, PropgraphElementAliasIndexId, pg_propgraph_element, btree(pgepgid oid_ops, pgealias name_ops)); + +MAKE_SYSCACHE(PROPGRAPHELOID, pg_propgraph_element_oid_index, 128); +MAKE_SYSCACHE(PROPGRAPHELALIAS, pg_propgraph_element_alias_index, 128); + +#ifdef EXPOSE_TO_CLIENT_CODE + +/* + * Symbolic values for pgekind column + */ +#define PGEKIND_VERTEX 'v' +#define PGEKIND_EDGE 'e' + +#endif /* EXPOSE_TO_CLIENT_CODE */ + +#endif /* PG_PROPGRAPH_ELEMENT_H */ diff --git a/src/include/catalog/pg_propgraph_element_label.h b/src/include/catalog/pg_propgraph_element_label.h new file mode 100644 index 0000000000000..afd1533cadd6c --- /dev/null +++ b/src/include/catalog/pg_propgraph_element_label.h @@ -0,0 +1,55 @@ +/*------------------------------------------------------------------------- + * + * pg_propgraph_element_label.h + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_propgraph_element_label.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PROPGRAPH_ELEMENT_LABEL_H +#define PG_PROPGRAPH_ELEMENT_LABEL_H + +#include "catalog/genbki.h" +#include "catalog/pg_propgraph_element_label_d.h" + +/* ---------------- + * pg_propgraph_element_label definition. cpp turns this into + * typedef struct FormData_pg_propgraph_element_label + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_propgraph_element_label,8305,PropgraphElementLabelRelationId) +{ + Oid oid; + + /* OID of the label */ + Oid pgellabelid BKI_LOOKUP(pg_propgraph_label); + + /* OID of the property graph element */ + Oid pgelelid BKI_LOOKUP(pg_propgraph_element); +} FormData_pg_propgraph_element_label; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_propgraph_element_label corresponds to a pointer to a tuple with + * the format of pg_propgraph_element_label relation. + * ---------------- + */ +typedef FormData_pg_propgraph_element_label *Form_pg_propgraph_element_label; + +DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_element_label_oid_index, 8312, PropgraphElementLabelObjectIndexId, pg_propgraph_element_label, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_propgraph_element_label_element_label_index, 8313, PropgraphElementLabelElementLabelIndexId, pg_propgraph_element_label, btree(pgelelid oid_ops, pgellabelid oid_ops)); +DECLARE_INDEX(pg_propgraph_element_label_label_index, 8317, PropgraphElementLabelLabelIndexId, pg_propgraph_element_label, btree(pgellabelid oid_ops)); + +MAKE_SYSCACHE(PROPGRAPHELEMENTLABELELEMENTLABEL, pg_propgraph_element_label_element_label_index, 128); + +#endif /* PG_PROPGRAPH_ELEMENT_LABEL_H */ diff --git a/src/include/catalog/pg_propgraph_label.h b/src/include/catalog/pg_propgraph_label.h new file mode 100644 index 0000000000000..5c78bfa1b1925 --- /dev/null +++ b/src/include/catalog/pg_propgraph_label.h @@ -0,0 +1,55 @@ +/*------------------------------------------------------------------------- + * + * pg_propgraph_label.h + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_propgraph_label.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PROPGRAPH_LABEL_H +#define PG_PROPGRAPH_LABEL_H + +#include "catalog/genbki.h" +#include "catalog/pg_propgraph_label_d.h" + +/* ---------------- + * pg_propgraph_label definition. cpp turns this into + * typedef struct FormData_pg_propgraph_label + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_propgraph_label,8303,PropgraphLabelRelationId) +{ + Oid oid; + + /* OID of the property graph relation */ + Oid pglpgid BKI_LOOKUP(pg_class); + + /* label name */ + NameData pgllabel; +} FormData_pg_propgraph_label; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_propgraph_label corresponds to a pointer to a tuple with + * the format of pg_propgraph_label relation. + * ---------------- + */ +typedef FormData_pg_propgraph_label *Form_pg_propgraph_label; + +DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_label_oid_index, 8304, PropgraphLabelObjectIndexId, pg_propgraph_label, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_propgraph_label_graph_name_index, 8314, PropgraphLabelGraphNameIndexId, pg_propgraph_label, btree(pglpgid oid_ops, pgllabel name_ops)); + +MAKE_SYSCACHE(PROPGRAPHLABELOID, pg_propgraph_label_oid_index, 128); +MAKE_SYSCACHE(PROPGRAPHLABELNAME, pg_propgraph_label_graph_name_index, 128); + +#endif /* PG_PROPGRAPH_LABEL_H */ diff --git a/src/include/catalog/pg_propgraph_label_property.h b/src/include/catalog/pg_propgraph_label_property.h new file mode 100644 index 0000000000000..39d55bf68a4d1 --- /dev/null +++ b/src/include/catalog/pg_propgraph_label_property.h @@ -0,0 +1,63 @@ +/*------------------------------------------------------------------------- + * + * pg_propgraph_label_property.h + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_propgraph_label_property.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PROPGRAPH_LABEL_PROPERTY_H +#define PG_PROPGRAPH_LABEL_PROPERTY_H + +#include "catalog/genbki.h" +#include "catalog/pg_propgraph_label_property_d.h" + +/* ---------------- + * pg_propgraph_label_property definition. cpp turns this into + * typedef struct FormData_pg_propgraph_label_property + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_propgraph_label_property,8318,PropgraphLabelPropertyRelationId) +{ + Oid oid; + + /* OID of the property */ + Oid plppropid BKI_LOOKUP(pg_propgraph_property); + + /* OID of the element label */ + Oid plpellabelid BKI_LOOKUP(pg_propgraph_element_label); + +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + + /* property expression */ + pg_node_tree plpexpr BKI_FORCE_NOT_NULL; + +#endif +} FormData_pg_propgraph_label_property; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_propgraph_label_property corresponds to a pointer to a tuple with + * the format of pg_propgraph_label_property relation. + * ---------------- + */ +typedef FormData_pg_propgraph_label_property *Form_pg_propgraph_label_property; + +DECLARE_TOAST(pg_propgraph_label_property, 8319, 8320); + +DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_label_property_oid_index, 8328, PropgraphLabelPropertyObjectIndexId, pg_propgraph_label_property, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_propgraph_label_property_label_prop_index, 8329, PropgraphLabelPropertyLabelPropIndexId, pg_propgraph_label_property, btree(plpellabelid oid_ops, plppropid oid_ops)); + +MAKE_SYSCACHE(PROPGRAPHLABELPROP, pg_propgraph_label_property_label_prop_index, 128); + +#endif /* PG_PROPGRAPH_LABEL_PROPERTY_H */ diff --git a/src/include/catalog/pg_propgraph_property.h b/src/include/catalog/pg_propgraph_property.h new file mode 100644 index 0000000000000..72beafbbf15c9 --- /dev/null +++ b/src/include/catalog/pg_propgraph_property.h @@ -0,0 +1,64 @@ +/*------------------------------------------------------------------------- + * + * pg_propgraph_property.h + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_propgraph_property.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PROPGRAPH_PROPERTY_H +#define PG_PROPGRAPH_PROPERTY_H + +#include "catalog/genbki.h" +#include "catalog/pg_propgraph_property_d.h" + +/* ---------------- + * pg_propgraph_property definition. cpp turns this into + * typedef struct FormData_pg_propgraph_property + * ---------------- + */ +BEGIN_CATALOG_STRUCT + +CATALOG(pg_propgraph_property,8306,PropgraphPropertyRelationId) +{ + Oid oid; + + /* OID of the property graph relation */ + Oid pgppgid BKI_LOOKUP(pg_class); + + /* property name */ + NameData pgpname; + + /* data type of the property */ + Oid pgptypid BKI_LOOKUP_OPT(pg_type); + + /* typemod of the property */ + int32 pgptypmod; + + /* collation of the property */ + Oid pgpcollation BKI_LOOKUP_OPT(pg_collation); +} FormData_pg_propgraph_property; + +END_CATALOG_STRUCT + +/* ---------------- + * Form_pg_propgraph_property corresponds to a pointer to a tuple with + * the format of pg_propgraph_property relation. + * ---------------- + */ +typedef FormData_pg_propgraph_property *Form_pg_propgraph_property; + +DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_property_oid_index, 8307, PropgraphPropertyObjectIndexId, pg_propgraph_property, btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_propgraph_property_name_index, 8308, PropgraphPropertyNameIndexId, pg_propgraph_property, btree(pgppgid oid_ops, pgpname name_ops)); + +MAKE_SYSCACHE(PROPGRAPHPROPOID, pg_propgraph_property_oid_index, 128); +MAKE_SYSCACHE(PROPGRAPHPROPNAME, pg_propgraph_property_name_index, 128); + +#endif /* PG_PROPGRAPH_PROPERTY_H */ diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h index 48c7d1a861527..89b4bb14f6288 100644 --- a/src/include/catalog/pg_publication.h +++ b/src/include/catalog/pg_publication.h @@ -3,7 +3,7 @@ * pg_publication.h * definition of the "publication" system catalog (pg_publication) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_publication.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_publication * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_publication,6104,PublicationRelationId) { Oid oid; /* oid */ @@ -40,6 +42,12 @@ CATALOG(pg_publication,6104,PublicationRelationId) */ bool puballtables; + /* + * indicates that this is special publication which should encompass all + * sequences in the database (except for the unlogged and temp ones) + */ + bool puballsequences; + /* true if inserts are published */ bool pubinsert; @@ -62,6 +70,8 @@ CATALOG(pg_publication,6104,PublicationRelationId) char pubgencols; } FormData_pg_publication; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_publication corresponds to a pointer to a tuple with * the format of pg_publication relation. @@ -129,6 +139,7 @@ typedef struct Publication Oid oid; char *name; bool alltables; + bool allsequences; bool pubviaroot; PublishGencolsType pubgencols_type; PublicationActions pubactions; @@ -139,16 +150,19 @@ typedef struct PublicationRelInfo Relation relation; Node *whereClause; List *columns; + bool except; } PublicationRelInfo; extern Publication *GetPublication(Oid pubid); extern Publication *GetPublicationByName(const char *pubname, bool missing_ok); -extern List *GetRelationPublications(Oid relid); +extern List *GetRelationIncludedPublications(Oid relid); +extern List *GetRelationExcludedPublications(Oid relid); /*--------- - * Expected values for pub_partopt parameter of GetRelationPublications(), - * which allows callers to specify which partitions of partitioned tables - * mentioned in the publication they expect to see. + * Expected values for pub_partopt parameter of + * GetIncludedPublicationRelations(), which allows callers to specify which + * partitions of partitioned tables mentioned in the publication they expect to + * see. * * ROOT: only the table explicitly mentioned in the publication * LEAF: only leaf partitions in given tree @@ -161,9 +175,12 @@ typedef enum PublicationPartOpt PUBLICATION_PART_ALL, } PublicationPartOpt; -extern List *GetPublicationRelations(Oid pubid, PublicationPartOpt pub_partopt); +extern List *GetIncludedPublicationRelations(Oid pubid, + PublicationPartOpt pub_partopt); +extern List *GetExcludedPublicationTables(Oid pubid, + PublicationPartOpt pub_partopt); extern List *GetAllTablesPublications(void); -extern List *GetAllTablesPublicationRelations(bool pubviaroot); +extern List *GetAllPublicationRelations(Oid pubid, char relkind, bool pubviaroot); extern List *GetPublicationSchemas(Oid pubid); extern List *GetSchemaPublications(Oid schemaid); extern List *GetSchemaPublicationRelations(Oid schemaid, @@ -178,10 +195,12 @@ extern Oid GetTopMostAncestorInPublication(Oid puboid, List *ancestors, extern bool is_publishable_relation(Relation rel); extern bool is_schema_publication(Oid pubid); +extern bool is_table_publication(Oid pubid); extern bool check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt, Bitmapset **cols); extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri, - bool if_not_exists); + bool if_not_exists, + AlterPublicationStmt *alter_stmt); extern Bitmapset *pub_collist_validate(Relation targetrel, List *columns); extern ObjectAddress publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists); diff --git a/src/include/catalog/pg_publication_namespace.h b/src/include/catalog/pg_publication_namespace.h index 3bf308d98bd3e..6c21b248db2b2 100644 --- a/src/include/catalog/pg_publication_namespace.h +++ b/src/include/catalog/pg_publication_namespace.h @@ -4,7 +4,7 @@ * definition of the system catalog for mappings between schemas and * publications (pg_publication_namespace) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_publication_namespace.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_publication_namespace * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_publication_namespace,6237,PublicationNamespaceRelationId) { Oid oid; /* oid */ @@ -34,6 +36,8 @@ CATALOG(pg_publication_namespace,6237,PublicationNamespaceRelationId) Oid pnnspid BKI_LOOKUP(pg_namespace); /* Oid of the schema */ } FormData_pg_publication_namespace; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_publication_namespace corresponds to a pointer to a tuple with * the format of pg_publication_namespace relation. diff --git a/src/include/catalog/pg_publication_rel.h b/src/include/catalog/pg_publication_rel.h index 92cc36dfdf698..cefc38c9ed872 100644 --- a/src/include/catalog/pg_publication_rel.h +++ b/src/include/catalog/pg_publication_rel.h @@ -4,7 +4,7 @@ * definition of the system catalog for mappings between relations and * publications (pg_publication_rel) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_publication_rel.h @@ -26,11 +26,14 @@ * typedef struct FormData_pg_publication_rel * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_publication_rel,6106,PublicationRelRelationId) { Oid oid; /* oid */ Oid prpubid BKI_LOOKUP(pg_publication); /* Oid of the publication */ Oid prrelid BKI_LOOKUP(pg_class); /* Oid of the relation */ + bool prexcept BKI_DEFAULT(f); /* relation is not published */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ pg_node_tree prqual; /* qualifications */ @@ -38,6 +41,8 @@ CATALOG(pg_publication_rel,6106,PublicationRelRelationId) #endif } FormData_pg_publication_rel; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_publication_rel corresponds to a pointer to a tuple with * the format of pg_publication_rel relation. diff --git a/src/include/catalog/pg_range.dat b/src/include/catalog/pg_range.dat index 1d423dc7caf37..fa5e6ff0c3e90 100644 --- a/src/include/catalog/pg_range.dat +++ b/src/include/catalog/pg_range.dat @@ -3,7 +3,7 @@ # pg_range.dat # Initial contents of the pg_range system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_range.dat @@ -14,21 +14,33 @@ { rngtypid => 'int4range', rngsubtype => 'int4', rngmultitypid => 'int4multirange', rngsubopc => 'btree/int4_ops', + rngconstruct2 => 'int4range(int4,int4)', rngconstruct3 => 'int4range(int4,int4,text)', + rngmltconstruct0 => 'int4multirange()', rngmltconstruct1 => 'int4multirange(int4range)', rngmltconstruct2 => 'int4multirange(_int4range)', rngcanonical => 'int4range_canonical', rngsubdiff => 'int4range_subdiff' }, { rngtypid => 'numrange', rngsubtype => 'numeric', rngmultitypid => 'nummultirange', rngsubopc => 'btree/numeric_ops', + rngconstruct2 => 'numrange(numeric,numeric)', rngconstruct3 => 'numrange(numeric,numeric,text)', + rngmltconstruct0 => 'nummultirange()', rngmltconstruct1 => 'nummultirange(numrange)', rngmltconstruct2 => 'nummultirange(_numrange)', rngcanonical => '-', rngsubdiff => 'numrange_subdiff' }, { rngtypid => 'tsrange', rngsubtype => 'timestamp', rngmultitypid => 'tsmultirange', rngsubopc => 'btree/timestamp_ops', + rngconstruct2 => 'tsrange(timestamp,timestamp)', rngconstruct3 => 'tsrange(timestamp,timestamp,text)', + rngmltconstruct0 => 'tsmultirange()', rngmltconstruct1 => 'tsmultirange(tsrange)', rngmltconstruct2 => 'tsmultirange(_tsrange)', rngcanonical => '-', rngsubdiff => 'tsrange_subdiff' }, { rngtypid => 'tstzrange', rngsubtype => 'timestamptz', rngmultitypid => 'tstzmultirange', rngsubopc => 'btree/timestamptz_ops', + rngconstruct2 => 'tstzrange(timestamptz,timestamptz)', rngconstruct3 => 'tstzrange(timestamptz,timestamptz,text)', + rngmltconstruct0 => 'tstzmultirange()', rngmltconstruct1 => 'tstzmultirange(tstzrange)', rngmltconstruct2 => 'tstzmultirange(_tstzrange)', rngcanonical => '-', rngsubdiff => 'tstzrange_subdiff' }, { rngtypid => 'daterange', rngsubtype => 'date', rngmultitypid => 'datemultirange', rngsubopc => 'btree/date_ops', + rngconstruct2 => 'daterange(date,date)', rngconstruct3 => 'daterange(date,date,text)', + rngmltconstruct0 => 'datemultirange()', rngmltconstruct1 => 'datemultirange(daterange)', rngmltconstruct2 => 'datemultirange(_daterange)', rngcanonical => 'daterange_canonical', rngsubdiff => 'daterange_subdiff' }, { rngtypid => 'int8range', rngsubtype => 'int8', rngmultitypid => 'int8multirange', rngsubopc => 'btree/int8_ops', + rngconstruct2 => 'int8range(int8,int8)', rngconstruct3 => 'int8range(int8,int8,text)', + rngmltconstruct0 => 'int8multirange()', rngmltconstruct1 => 'int8multirange(int8range)', rngmltconstruct2 => 'int8multirange(_int8range)', rngcanonical => 'int8range_canonical', rngsubdiff => 'int8range_subdiff' }, ] diff --git a/src/include/catalog/pg_range.h b/src/include/catalog/pg_range.h index 3bba61627821f..ee87ed3bf4219 100644 --- a/src/include/catalog/pg_range.h +++ b/src/include/catalog/pg_range.h @@ -4,7 +4,7 @@ * definition of the "range type" system catalog (pg_range) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_range.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_range * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_range,3541,RangeRelationId) { /* OID of owning range type */ @@ -43,6 +45,15 @@ CATALOG(pg_range,3541,RangeRelationId) /* subtype's btree opclass */ Oid rngsubopc BKI_LOOKUP(pg_opclass); + /* range constructor functions */ + regproc rngconstruct2 BKI_LOOKUP(pg_proc); + regproc rngconstruct3 BKI_LOOKUP(pg_proc); + + /* multirange constructor functions */ + regproc rngmltconstruct0 BKI_LOOKUP(pg_proc); + regproc rngmltconstruct1 BKI_LOOKUP(pg_proc); + regproc rngmltconstruct2 BKI_LOOKUP(pg_proc); + /* canonicalize range, or 0 */ regproc rngcanonical BKI_LOOKUP_OPT(pg_proc); @@ -50,6 +61,8 @@ CATALOG(pg_range,3541,RangeRelationId) regproc rngsubdiff BKI_LOOKUP_OPT(pg_proc); } FormData_pg_range; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_range corresponds to a pointer to a tuple with * the format of pg_range relation. @@ -69,7 +82,9 @@ MAKE_SYSCACHE(RANGEMULTIRANGE, pg_range_rngmultitypid_index, 4); extern void RangeCreate(Oid rangeTypeOid, Oid rangeSubType, Oid rangeCollation, Oid rangeSubOpclass, RegProcedure rangeCanonical, - RegProcedure rangeSubDiff, Oid multirangeTypeOid); + RegProcedure rangeSubDiff, Oid multirangeTypeOid, + RegProcedure rangeConstruct2, RegProcedure rangeConstruct3, + RegProcedure mltrngConstruct0, RegProcedure mltrngConstruct1, RegProcedure mltrngConstruct2); extern void RangeDelete(Oid rangeTypeOid); #endif /* PG_RANGE_H */ diff --git a/src/include/catalog/pg_replication_origin.h b/src/include/catalog/pg_replication_origin.h index 7ade8bbda3964..565d71ad0b376 100644 --- a/src/include/catalog/pg_replication_origin.h +++ b/src/include/catalog/pg_replication_origin.h @@ -4,7 +4,7 @@ * definition of the "replication origin" system catalog * (pg_replication_origin) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_replication_origin.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_replication_origin * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_replication_origin,6000,ReplicationOriginRelationId) BKI_SHARED_RELATION { /* @@ -52,6 +54,8 @@ CATALOG(pg_replication_origin,6000,ReplicationOriginRelationId) BKI_SHARED_RELAT #endif } FormData_pg_replication_origin; +END_CATALOG_STRUCT + typedef FormData_pg_replication_origin *Form_pg_replication_origin; DECLARE_UNIQUE_INDEX_PKEY(pg_replication_origin_roiident_index, 6001, ReplicationOriginIdentIndex, pg_replication_origin, btree(roident oid_ops)); diff --git a/src/include/catalog/pg_rewrite.h b/src/include/catalog/pg_rewrite.h index c70bc45d1b3e8..fe82e84be4e21 100644 --- a/src/include/catalog/pg_rewrite.h +++ b/src/include/catalog/pg_rewrite.h @@ -7,7 +7,7 @@ * --- ie, rule names are only unique among the rules of a given table. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_rewrite.h @@ -29,6 +29,8 @@ * typedef struct FormData_pg_rewrite * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_rewrite,2618,RewriteRelationId) { Oid oid; /* oid */ @@ -44,6 +46,8 @@ CATALOG(pg_rewrite,2618,RewriteRelationId) #endif } FormData_pg_rewrite; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_rewrite corresponds to a pointer to a tuple with * the format of pg_rewrite relation. diff --git a/src/include/catalog/pg_seclabel.h b/src/include/catalog/pg_seclabel.h index 5b1b433600fba..8f5788ca663c1 100644 --- a/src/include/catalog/pg_seclabel.h +++ b/src/include/catalog/pg_seclabel.h @@ -3,7 +3,7 @@ * pg_seclabel.h * definition of the "security label" system catalog (pg_seclabel) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_seclabel.h @@ -25,6 +25,8 @@ * typedef struct FormData_pg_seclabel * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_seclabel,3596,SecLabelRelationId) { Oid objoid; /* OID of the object itself */ @@ -38,6 +40,8 @@ CATALOG(pg_seclabel,3596,SecLabelRelationId) #endif } FormData_pg_seclabel; +END_CATALOG_STRUCT + DECLARE_TOAST(pg_seclabel, 3598, 3599); DECLARE_UNIQUE_INDEX_PKEY(pg_seclabel_object_index, 3597, SecLabelObjectIndexId, pg_seclabel, btree(objoid oid_ops, classoid oid_ops, objsubid int4_ops, provider text_ops)); diff --git a/src/include/catalog/pg_sequence.h b/src/include/catalog/pg_sequence.h index 580c7f2cbe6f9..59820a86b8be7 100644 --- a/src/include/catalog/pg_sequence.h +++ b/src/include/catalog/pg_sequence.h @@ -3,7 +3,7 @@ * pg_sequence.h * definition of the "sequence" system catalog (pg_sequence) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_sequence.h @@ -20,6 +20,8 @@ #include "catalog/genbki.h" #include "catalog/pg_sequence_d.h" /* IWYU pragma: export */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_sequence,2224,SequenceRelationId) { Oid seqrelid BKI_LOOKUP(pg_class); @@ -32,6 +34,8 @@ CATALOG(pg_sequence,2224,SequenceRelationId) bool seqcycle; } FormData_pg_sequence; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_sequence corresponds to a pointer to a tuple with * the format of pg_sequence relation. diff --git a/src/include/catalog/pg_shdepend.h b/src/include/catalog/pg_shdepend.h index 62df9f757f6db..80d5eb0255e96 100644 --- a/src/include/catalog/pg_shdepend.h +++ b/src/include/catalog/pg_shdepend.h @@ -13,7 +13,7 @@ * from a relation to its database. Currently, only dependencies on roles * are explicitly stored in pg_shdepend. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_shdepend.h @@ -35,6 +35,8 @@ * typedef struct FormData_pg_shdepend * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_shdepend,1214,SharedDependRelationId) BKI_SHARED_RELATION { /* @@ -65,6 +67,8 @@ CATALOG(pg_shdepend,1214,SharedDependRelationId) BKI_SHARED_RELATION char deptype; /* see codes in dependency.h */ } FormData_pg_shdepend; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_shdepend corresponds to a pointer to a row with * the format of pg_shdepend relation. diff --git a/src/include/catalog/pg_shdescription.h b/src/include/catalog/pg_shdescription.h index ccd1f2ecb4cc5..f53d9f5dcd670 100644 --- a/src/include/catalog/pg_shdescription.h +++ b/src/include/catalog/pg_shdescription.h @@ -16,7 +16,7 @@ * across tables. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_shdescription.h @@ -38,6 +38,8 @@ * typedef struct FormData_pg_shdescription * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_shdescription,2396,SharedDescriptionRelationId) BKI_SHARED_RELATION { Oid objoid; /* OID of object itself */ @@ -48,6 +50,8 @@ CATALOG(pg_shdescription,2396,SharedDescriptionRelationId) BKI_SHARED_RELATION #endif } FormData_pg_shdescription; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_shdescription corresponds to a pointer to a tuple with * the format of pg_shdescription relation. diff --git a/src/include/catalog/pg_shseclabel.h b/src/include/catalog/pg_shseclabel.h index 523d6058b2440..273c7790194e2 100644 --- a/src/include/catalog/pg_shseclabel.h +++ b/src/include/catalog/pg_shseclabel.h @@ -3,7 +3,7 @@ * pg_shseclabel.h * definition of the "shared security label" system catalog (pg_shseclabel) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_shseclabel.h @@ -25,6 +25,8 @@ * typedef struct FormData_pg_shseclabel * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_shseclabel,3592,SharedSecLabelRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4066,SharedSecLabelRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid objoid; /* OID of the shared object itself */ @@ -37,6 +39,8 @@ CATALOG(pg_shseclabel,3592,SharedSecLabelRelationId) BKI_SHARED_RELATION BKI_ROW #endif } FormData_pg_shseclabel; +END_CATALOG_STRUCT + typedef FormData_pg_shseclabel * Form_pg_shseclabel; DECLARE_TOAST_WITH_MACRO(pg_shseclabel, 4060, 4061, PgShseclabelToastTable, PgShseclabelToastIndex); diff --git a/src/include/catalog/pg_statistic.h b/src/include/catalog/pg_statistic.h index 4216e27a8a4f7..032bf177b95eb 100644 --- a/src/include/catalog/pg_statistic.h +++ b/src/include/catalog/pg_statistic.h @@ -4,7 +4,7 @@ * definition of the "statistics" system catalog (pg_statistic) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_statistic.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_statistic * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_statistic,2619,StatisticRelationId) { /* These fields form the unique key for the entry: */ @@ -124,6 +126,8 @@ CATALOG(pg_statistic,2619,StatisticRelationId) #endif } FormData_pg_statistic; +END_CATALOG_STRUCT + #define STATISTIC_NUM_SLOTS 5 @@ -240,6 +244,10 @@ DECLARE_FOREIGN_KEY((starelid, staattnum), pg_attribute, (attrelid, attnum)); * the fraction of non-null rows that contain at least one null element). If * this member is omitted, the column is presumed to contain no null elements. * + * Starting in v19, the first extra member can be smaller than the smallest + * frequency of any stored MCE, indicating that it's known that no element + * not present in the MCE array has frequency greater than that value. + * * Note: in current usage for tsvector columns, the stavalues elements are of * type text, even though their representation within tsvector is not * exactly text. diff --git a/src/include/catalog/pg_statistic_ext.h b/src/include/catalog/pg_statistic_ext.h index d476095029c4d..e4a0cb4d41cc9 100644 --- a/src/include/catalog/pg_statistic_ext.h +++ b/src/include/catalog/pg_statistic_ext.h @@ -8,7 +8,7 @@ * objects, created by CREATE STATISTICS, but not the actual statistical data, * created by running ANALYZE. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_statistic_ext.h @@ -30,6 +30,8 @@ * typedef struct FormData_pg_statistic_ext * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_statistic_ext,3381,StatisticExtRelationId) { Oid oid; /* oid */ @@ -61,6 +63,8 @@ CATALOG(pg_statistic_ext,3381,StatisticExtRelationId) } FormData_pg_statistic_ext; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_statistic_ext corresponds to a pointer to a tuple with * the format of pg_statistic_ext relation. diff --git a/src/include/catalog/pg_statistic_ext_data.h b/src/include/catalog/pg_statistic_ext_data.h index 809b525199454..dbc4acc7d1a55 100644 --- a/src/include/catalog/pg_statistic_ext_data.h +++ b/src/include/catalog/pg_statistic_ext_data.h @@ -6,7 +6,7 @@ * * This catalog stores the statistical data for extended statistics objects. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_statistic_ext_data.h @@ -28,6 +28,8 @@ * typedef struct FormData_pg_statistic_ext_data * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_statistic_ext_data,3429,StatisticExtDataRelationId) { Oid stxoid BKI_LOOKUP(pg_statistic_ext); /* statistics object @@ -45,6 +47,8 @@ CATALOG(pg_statistic_ext_data,3429,StatisticExtDataRelationId) } FormData_pg_statistic_ext_data; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_statistic_ext_data corresponds to a pointer to a tuple with * the format of pg_statistic_ext_data relation. diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index 20fc329992dc5..a6a2ad1e49c24 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -3,7 +3,7 @@ * pg_subscription.h * definition of the "subscription" system catalog (pg_subscription) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_subscription.h @@ -40,6 +40,8 @@ * here, be sure to update that (or, if the new column is not to be publicly * readable, update associated comments and catalogs.sgml instead). */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(6101,SubscriptionRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid oid; /* oid */ @@ -78,9 +80,24 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW * slots) in the upstream database are enabled * to be synchronized to the standbys. */ + bool subretaindeadtuples; /* True if dead tuples useful for + * conflict detection are retained */ + + int32 submaxretention; /* The maximum duration (in milliseconds) + * for which information useful for + * conflict detection can be retained */ + + bool subretentionactive; /* True if retain_dead_tuples is enabled + * and the retention duration has not + * exceeded max_retention_duration, when + * defined */ + + Oid subserver BKI_LOOKUP_OPT(pg_foreign_server); /* If connection uses + * server */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* Connection string to the publisher */ - text subconninfo BKI_FORCE_NOT_NULL; + text subconninfo; /* Set if connecting with connection string */ /* Slot name on publisher */ NameData subslotname BKI_FORCE_NULL; @@ -88,6 +105,9 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW /* Synchronous commit setting for worker */ text subsynccommit BKI_FORCE_NOT_NULL; + /* wal_receiver_timeout setting for worker */ + text subwalrcvtimeout BKI_FORCE_NOT_NULL; + /* List of publications subscribed to */ text subpublications[1] BKI_FORCE_NOT_NULL; @@ -96,6 +116,8 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW #endif } FormData_pg_subscription; +END_CATALOG_STRUCT + typedef FormData_pg_subscription *Form_pg_subscription; DECLARE_TOAST_WITH_MACRO(pg_subscription, 4183, 4184, PgSubscriptionToastTable, PgSubscriptionToastIndex); @@ -108,6 +130,8 @@ MAKE_SYSCACHE(SUBSCRIPTIONNAME, pg_subscription_subname_index, 4); typedef struct Subscription { + MemoryContext cxt; /* mem cxt containing this subscription */ + Oid oid; /* Oid of the subscription */ Oid dbid; /* Oid of the database which subscription is * in */ @@ -131,9 +155,19 @@ typedef struct Subscription * (i.e. the main slot and the table sync * slots) in the upstream database are enabled * to be synchronized to the standbys. */ + bool retaindeadtuples; /* True if dead tuples useful for conflict + * detection are retained */ + int32 maxretention; /* The maximum duration (in milliseconds) for + * which information useful for conflict + * detection can be retained */ + bool retentionactive; /* True if retain_dead_tuples is enabled + * and the retention duration has not + * exceeded max_retention_duration, when + * defined */ char *conninfo; /* Connection string to the publisher */ char *slotname; /* Name of the replication slot */ char *synccommit; /* Synchronous commit setting for worker */ + char *walrcvtimeout; /* wal_receiver_timeout setting for worker */ List *publications; /* List of publication names to subscribe to */ char *origin; /* Only publish data originating from the * specified origin */ @@ -178,8 +212,8 @@ typedef struct Subscription #endif /* EXPOSE_TO_CLIENT_CODE */ -extern Subscription *GetSubscription(Oid subid, bool missing_ok); -extern void FreeSubscription(Subscription *sub); +extern Subscription *GetSubscription(Oid subid, bool missing_ok, + bool aclcheck); extern void DisableSubscription(Oid subid); extern int CountDBSubscriptions(Oid dbid); diff --git a/src/include/catalog/pg_subscription_rel.h b/src/include/catalog/pg_subscription_rel.h index c91797c869c24..502640d301882 100644 --- a/src/include/catalog/pg_subscription_rel.h +++ b/src/include/catalog/pg_subscription_rel.h @@ -4,7 +4,7 @@ * definition of the system catalog containing the state for each * replicated table in each subscription (pg_subscription_rel) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_subscription_rel.h @@ -28,6 +28,8 @@ * typedef struct FormData_pg_subscription_rel * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_subscription_rel,6102,SubscriptionRelRelationId) { Oid srsubid BKI_LOOKUP(pg_subscription); /* Oid of subscription */ @@ -47,6 +49,8 @@ CATALOG(pg_subscription_rel,6102,SubscriptionRelRelationId) #endif } FormData_pg_subscription_rel; +END_CATALOG_STRUCT + typedef FormData_pg_subscription_rel *Form_pg_subscription_rel; DECLARE_UNIQUE_INDEX_PKEY(pg_subscription_rel_srrelid_srsubid_index, 6117, SubscriptionRelSrrelidSrsubidIndexId, pg_subscription_rel, btree(srrelid oid_ops, srsubid oid_ops)); @@ -82,14 +86,40 @@ typedef struct SubscriptionRelState char state; } SubscriptionRelState; +/* + * Holds local sequence identity and corresponding publisher values used during + * sequence synchronization. + */ +typedef struct LogicalRepSequenceInfo +{ + /* Sequence information retrieved from the local node */ + char *seqname; + char *nspname; + Oid localrelid; + + /* Sequence information retrieved from the publisher node */ + XLogRecPtr page_lsn; + int64 last_value; + bool is_called; + + /* + * True if the sequence identified by nspname + seqname exists on the + * publisher. + */ + bool found_on_pub; +} LogicalRepSequenceInfo; + extern void AddSubscriptionRelState(Oid subid, Oid relid, char state, XLogRecPtr sublsn, bool retain_lock); extern void UpdateSubscriptionRelState(Oid subid, Oid relid, char state, - XLogRecPtr sublsn); + XLogRecPtr sublsn, bool already_locked); extern char GetSubscriptionRelState(Oid subid, Oid relid, XLogRecPtr *sublsn); extern void RemoveSubscriptionRel(Oid subid, Oid relid); -extern bool HasSubscriptionRelations(Oid subid); -extern List *GetSubscriptionRelations(Oid subid, bool not_ready); +extern bool HasSubscriptionTables(Oid subid); +extern List *GetSubscriptionRelations(Oid subid, bool tables, bool sequences, + bool not_ready); + +extern void UpdateDeadTupleRetentionStatus(Oid subid, bool active); #endif /* PG_SUBSCRIPTION_REL_H */ diff --git a/src/include/catalog/pg_tablespace.dat b/src/include/catalog/pg_tablespace.dat index 1302a3d75cd2f..c4cde41521932 100644 --- a/src/include/catalog/pg_tablespace.dat +++ b/src/include/catalog/pg_tablespace.dat @@ -3,7 +3,7 @@ # pg_tablespace.dat # Initial contents of the pg_tablespace system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_tablespace.dat diff --git a/src/include/catalog/pg_tablespace.h b/src/include/catalog/pg_tablespace.h index 5293488c63091..3bd4a74f003fd 100644 --- a/src/include/catalog/pg_tablespace.h +++ b/src/include/catalog/pg_tablespace.h @@ -4,7 +4,7 @@ * definition of the "tablespace" system catalog (pg_tablespace) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_tablespace.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_tablespace * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_tablespace,1213,TableSpaceRelationId) BKI_SHARED_RELATION { Oid oid; /* oid */ @@ -40,6 +42,8 @@ CATALOG(pg_tablespace,1213,TableSpaceRelationId) BKI_SHARED_RELATION #endif } FormData_pg_tablespace; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_tablespace corresponds to a pointer to a tuple with * the format of pg_tablespace relation. @@ -54,4 +58,6 @@ DECLARE_UNIQUE_INDEX(pg_tablespace_spcname_index, 2698, TablespaceNameIndexId, p MAKE_SYSCACHE(TABLESPACEOID, pg_tablespace_oid_index, 4); +extern char *get_tablespace_location(Oid tablespaceOid); + #endif /* PG_TABLESPACE_H */ diff --git a/src/include/catalog/pg_transform.h b/src/include/catalog/pg_transform.h index bc3d943230f41..115608de43c26 100644 --- a/src/include/catalog/pg_transform.h +++ b/src/include/catalog/pg_transform.h @@ -4,7 +4,7 @@ * definition of the "transform" system catalog (pg_transform) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_transform.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_transform * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_transform,3576,TransformRelationId) { Oid oid; /* oid */ @@ -35,6 +37,8 @@ CATALOG(pg_transform,3576,TransformRelationId) regproc trftosql BKI_LOOKUP_OPT(pg_proc); } FormData_pg_transform; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_transform corresponds to a pointer to a tuple with * the format of pg_transform relation. diff --git a/src/include/catalog/pg_trigger.h b/src/include/catalog/pg_trigger.h index 24a9152806140..2377e2f3167bc 100644 --- a/src/include/catalog/pg_trigger.h +++ b/src/include/catalog/pg_trigger.h @@ -4,7 +4,7 @@ * definition of the "trigger" system catalog (pg_trigger) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_trigger.h @@ -31,6 +31,8 @@ * to be associated with a deferrable constraint. * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_trigger,2620,TriggerRelationId) { Oid oid; /* oid */ @@ -72,6 +74,8 @@ CATALOG(pg_trigger,2620,TriggerRelationId) #endif } FormData_pg_trigger; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_trigger corresponds to a pointer to a tuple with * the format of pg_trigger relation. diff --git a/src/include/catalog/pg_ts_config.dat b/src/include/catalog/pg_ts_config.dat index 6bd03f7f4ef3c..dfad62ffe54c8 100644 --- a/src/include/catalog/pg_ts_config.dat +++ b/src/include/catalog/pg_ts_config.dat @@ -3,7 +3,7 @@ # pg_ts_config.dat # Initial contents of the pg_ts_config system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_ts_config.dat diff --git a/src/include/catalog/pg_ts_config.h b/src/include/catalog/pg_ts_config.h index 0f5df37f0974d..0bbcb3249dc64 100644 --- a/src/include/catalog/pg_ts_config.h +++ b/src/include/catalog/pg_ts_config.h @@ -5,7 +5,7 @@ * (pg_ts_config) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_ts_config.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_ts_config * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_ts_config,3602,TSConfigRelationId) { /* oid */ @@ -45,6 +47,8 @@ CATALOG(pg_ts_config,3602,TSConfigRelationId) Oid cfgparser BKI_LOOKUP(pg_ts_parser); } FormData_pg_ts_config; +END_CATALOG_STRUCT + typedef FormData_pg_ts_config *Form_pg_ts_config; DECLARE_UNIQUE_INDEX(pg_ts_config_cfgname_index, 3608, TSConfigNameNspIndexId, pg_ts_config, btree(cfgname name_ops, cfgnamespace oid_ops)); diff --git a/src/include/catalog/pg_ts_config_map.dat b/src/include/catalog/pg_ts_config_map.dat index 0a376b0dd8c04..cb3f1950e1c18 100644 --- a/src/include/catalog/pg_ts_config_map.dat +++ b/src/include/catalog/pg_ts_config_map.dat @@ -3,7 +3,7 @@ # pg_ts_config_map.dat # Initial contents of the pg_ts_config_map system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_ts_config_map.dat diff --git a/src/include/catalog/pg_ts_config_map.h b/src/include/catalog/pg_ts_config_map.h index 38c3d4426c64d..51feb1b9e8f1c 100644 --- a/src/include/catalog/pg_ts_config_map.h +++ b/src/include/catalog/pg_ts_config_map.h @@ -5,7 +5,7 @@ * (pg_ts_config_map) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_ts_config_map.h @@ -27,6 +27,8 @@ * typedef struct FormData_pg_ts_config_map * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_ts_config_map,3603,TSConfigMapRelationId) { /* OID of configuration owning this entry */ @@ -42,6 +44,8 @@ CATALOG(pg_ts_config_map,3603,TSConfigMapRelationId) Oid mapdict BKI_LOOKUP(pg_ts_dict); } FormData_pg_ts_config_map; +END_CATALOG_STRUCT + typedef FormData_pg_ts_config_map *Form_pg_ts_config_map; DECLARE_UNIQUE_INDEX_PKEY(pg_ts_config_map_index, 3609, TSConfigMapIndexId, pg_ts_config_map, btree(mapcfg oid_ops, maptokentype int4_ops, mapseqno int4_ops)); diff --git a/src/include/catalog/pg_ts_dict.dat b/src/include/catalog/pg_ts_dict.dat index f5c9cd8398467..c48b8d4ecac0b 100644 --- a/src/include/catalog/pg_ts_dict.dat +++ b/src/include/catalog/pg_ts_dict.dat @@ -3,7 +3,7 @@ # pg_ts_dict.dat # Initial contents of the pg_ts_dict system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_ts_dict.dat diff --git a/src/include/catalog/pg_ts_dict.h b/src/include/catalog/pg_ts_dict.h index 86102c73a6bf4..d47b05ae5cca5 100644 --- a/src/include/catalog/pg_ts_dict.h +++ b/src/include/catalog/pg_ts_dict.h @@ -4,7 +4,7 @@ * definition of the "text search dictionary" system catalog (pg_ts_dict) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_ts_dict.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_ts_dict * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_ts_dict,3600,TSDictionaryRelationId) { /* oid */ @@ -49,6 +51,8 @@ CATALOG(pg_ts_dict,3600,TSDictionaryRelationId) #endif } FormData_pg_ts_dict; +END_CATALOG_STRUCT + typedef FormData_pg_ts_dict *Form_pg_ts_dict; DECLARE_TOAST(pg_ts_dict, 4169, 4170); diff --git a/src/include/catalog/pg_ts_parser.dat b/src/include/catalog/pg_ts_parser.dat index 28e33d4819d93..3c6975477abf2 100644 --- a/src/include/catalog/pg_ts_parser.dat +++ b/src/include/catalog/pg_ts_parser.dat @@ -3,7 +3,7 @@ # pg_ts_parser.dat # Initial contents of the pg_ts_parser system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_ts_parser.dat diff --git a/src/include/catalog/pg_ts_parser.h b/src/include/catalog/pg_ts_parser.h index f3a40e5d2936e..181e869ac7557 100644 --- a/src/include/catalog/pg_ts_parser.h +++ b/src/include/catalog/pg_ts_parser.h @@ -4,7 +4,7 @@ * definition of the "text search parser" system catalog (pg_ts_parser) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_ts_parser.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_ts_parser * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_ts_parser,3601,TSParserRelationId) { Oid oid; /* oid */ @@ -52,6 +54,8 @@ CATALOG(pg_ts_parser,3601,TSParserRelationId) regproc prslextype BKI_LOOKUP(pg_proc); } FormData_pg_ts_parser; +END_CATALOG_STRUCT + typedef FormData_pg_ts_parser *Form_pg_ts_parser; DECLARE_UNIQUE_INDEX(pg_ts_parser_prsname_index, 3606, TSParserNameNspIndexId, pg_ts_parser, btree(prsname name_ops, prsnamespace oid_ops)); diff --git a/src/include/catalog/pg_ts_template.dat b/src/include/catalog/pg_ts_template.dat index dd571aa33f195..e8b39de44cc23 100644 --- a/src/include/catalog/pg_ts_template.dat +++ b/src/include/catalog/pg_ts_template.dat @@ -3,7 +3,7 @@ # pg_ts_template.dat # Initial contents of the pg_ts_template system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_ts_template.dat diff --git a/src/include/catalog/pg_ts_template.h b/src/include/catalog/pg_ts_template.h index aa06b7a840a82..5c28991e27b43 100644 --- a/src/include/catalog/pg_ts_template.h +++ b/src/include/catalog/pg_ts_template.h @@ -4,7 +4,7 @@ * definition of the "text search template" system catalog (pg_ts_template) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_ts_template.h @@ -26,6 +26,8 @@ * typedef struct FormData_pg_ts_template * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_ts_template,3764,TSTemplateRelationId) { Oid oid; /* oid */ @@ -43,6 +45,8 @@ CATALOG(pg_ts_template,3764,TSTemplateRelationId) regproc tmpllexize BKI_LOOKUP(pg_proc); } FormData_pg_ts_template; +END_CATALOG_STRUCT + typedef FormData_pg_ts_template *Form_pg_ts_template; DECLARE_UNIQUE_INDEX(pg_ts_template_tmplname_index, 3766, TSTemplateNameNspIndexId, pg_ts_template, btree(tmplname name_ops, tmplnamespace oid_ops)); diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat index 6dca77e0a22f7..a1a753d17978c 100644 --- a/src/include/catalog/pg_type.dat +++ b/src/include/catalog/pg_type.dat @@ -3,7 +3,7 @@ # pg_type.dat # Initial contents of the pg_type system catalog. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/pg_type.dat @@ -54,7 +54,7 @@ typcollation => 'C' }, { oid => '20', array_type_oid => '1016', descr => '~18 digit integer, 8-byte storage', - typname => 'int8', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'int8', typlen => '8', typbyval => 't', typcategory => 'N', typinput => 'int8in', typoutput => 'int8out', typreceive => 'int8recv', typsend => 'int8send', typalign => 'd' }, { oid => '21', array_type_oid => '1005', @@ -172,7 +172,7 @@ typoutput => 'pg_ddl_command_out', typreceive => 'pg_ddl_command_recv', typsend => 'pg_ddl_command_send', typalign => 'ALIGNOF_POINTER' }, { oid => '5069', array_type_oid => '271', descr => 'full transaction id', - typname => 'xid8', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'xid8', typlen => '8', typbyval => 't', typcategory => 'U', typinput => 'xid8in', typoutput => 'xid8out', typreceive => 'xid8recv', typsend => 'xid8send', typalign => 'd' }, @@ -222,7 +222,7 @@ typsend => 'float4send', typalign => 'i' }, { oid => '701', array_type_oid => '1022', descr => 'double-precision floating point number, 8-byte storage', - typname => 'float8', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'float8', typlen => '8', typbyval => 't', typcategory => 'N', typispreferred => 't', typinput => 'float8in', typoutput => 'float8out', typreceive => 'float8recv', typsend => 'float8send', typalign => 'd' }, @@ -237,7 +237,7 @@ typreceive => 'circle_recv', typsend => 'circle_send', typalign => 'd' }, { oid => '790', array_type_oid => '791', descr => 'monetary amounts, $d,ddd.cc', - typname => 'money', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'money', typlen => '8', typbyval => 't', typcategory => 'N', typinput => 'cash_in', typoutput => 'cash_out', typreceive => 'cash_recv', typsend => 'cash_send', typalign => 'd' }, @@ -290,7 +290,7 @@ typinput => 'date_in', typoutput => 'date_out', typreceive => 'date_recv', typsend => 'date_send', typalign => 'i' }, { oid => '1083', array_type_oid => '1183', descr => 'time of day', - typname => 'time', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'time', typlen => '8', typbyval => 't', typcategory => 'D', typinput => 'time_in', typoutput => 'time_out', typreceive => 'time_recv', typsend => 'time_send', typmodin => 'timetypmodin', typmodout => 'timetypmodout', typalign => 'd' }, @@ -298,14 +298,14 @@ # OIDS 1100 - 1199 { oid => '1114', array_type_oid => '1115', descr => 'date and time', - typname => 'timestamp', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'timestamp', typlen => '8', typbyval => 't', typcategory => 'D', typinput => 'timestamp_in', typoutput => 'timestamp_out', typreceive => 'timestamp_recv', typsend => 'timestamp_send', typmodin => 'timestamptypmodin', typmodout => 'timestamptypmodout', typalign => 'd' }, { oid => '1184', array_type_oid => '1185', descr => 'date and time with time zone', - typname => 'timestamptz', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'timestamptz', typlen => '8', typbyval => 't', typcategory => 'D', typispreferred => 't', typinput => 'timestamptz_in', typoutput => 'timestamptz_out', typreceive => 'timestamptz_recv', typsend => 'timestamptz_send', typmodin => 'timestamptztypmodin', @@ -399,6 +399,11 @@ typinput => 'regnamespacein', typoutput => 'regnamespaceout', typreceive => 'regnamespacerecv', typsend => 'regnamespacesend', typalign => 'i' }, +{ oid => '8326', array_type_oid => '8327', descr => 'registered database', + typname => 'regdatabase', typlen => '4', typbyval => 't', typcategory => 'N', + typinput => 'regdatabasein', typoutput => 'regdatabaseout', + typreceive => 'regdatabaserecv', typsend => 'regdatabasesend', + typalign => 'i' }, # uuid { oid => '2950', array_type_oid => '2951', descr => 'UUID', @@ -408,7 +413,7 @@ # pg_lsn { oid => '3220', array_type_oid => '3221', descr => 'PostgreSQL LSN', - typname => 'pg_lsn', typlen => '8', typbyval => 'FLOAT8PASSBYVAL', + typname => 'pg_lsn', typlen => '8', typbyval => 't', typcategory => 'U', typinput => 'pg_lsn_in', typoutput => 'pg_lsn_out', typreceive => 'pg_lsn_recv', typsend => 'pg_lsn_send', typalign => 'd' }, @@ -695,4 +700,9 @@ typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send', typalign => 'i', typstorage => 'x', typcollation => 'default' }, +{ oid => '8256', array_type_oid => '8261', + descr => 'object identifier(oid8), 8 bytes', + typname => 'oid8', typlen => '8', typbyval => 't', typcategory => 'N', + typinput => 'oid8in', typoutput => 'oid8out', typreceive => 'oid8recv', + typsend => 'oid8send', typalign => 'd' }, ] diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h index ff666711a54f8..74183ec5a2e43 100644 --- a/src/include/catalog/pg_type.h +++ b/src/include/catalog/pg_type.h @@ -4,7 +4,7 @@ * definition of the "type" system catalog (pg_type) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_type.h @@ -33,6 +33,8 @@ * See struct FormData_pg_attribute for details. * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_type,1247,TypeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(71,TypeRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid oid; /* oid */ @@ -253,6 +255,8 @@ CATALOG(pg_type,1247,TypeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(71,TypeRelati #endif } FormData_pg_type; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_type corresponds to a pointer to a row with * the format of pg_type relation. diff --git a/src/include/catalog/pg_user_mapping.h b/src/include/catalog/pg_user_mapping.h index 7a0465c4d9745..921a9ec009d77 100644 --- a/src/include/catalog/pg_user_mapping.h +++ b/src/include/catalog/pg_user_mapping.h @@ -3,7 +3,7 @@ * pg_user_mapping.h * definition of the "user mapping" system catalog (pg_user_mapping) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/pg_user_mapping.h @@ -25,6 +25,8 @@ * typedef struct FormData_pg_user_mapping * ---------------- */ +BEGIN_CATALOG_STRUCT + CATALOG(pg_user_mapping,1418,UserMappingRelationId) { Oid oid; /* oid */ @@ -40,6 +42,8 @@ CATALOG(pg_user_mapping,1418,UserMappingRelationId) #endif } FormData_pg_user_mapping; +END_CATALOG_STRUCT + /* ---------------- * Form_pg_user_mapping corresponds to a pointer to a tuple with * the format of pg_user_mapping relation. diff --git a/src/include/catalog/reformat_dat_file.pl b/src/include/catalog/reformat_dat_file.pl index 0e2e5a278de53..5cfe5ef6abf9b 100755 --- a/src/include/catalog/reformat_dat_file.pl +++ b/src/include/catalog/reformat_dat_file.pl @@ -10,7 +10,7 @@ # in the same order as the columns of the corresponding catalog. # Comments and blank lines are preserved. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/reformat_dat_file.pl diff --git a/src/include/catalog/renumber_oids.pl b/src/include/catalog/renumber_oids.pl index 1f93da50b6d88..f6e25d0f1e1c6 100755 --- a/src/include/catalog/renumber_oids.pl +++ b/src/include/catalog/renumber_oids.pl @@ -8,7 +8,7 @@ # Note: This does not reformat the .dat files, so you may want # to run reformat_dat_file.pl afterwards. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/renumber_oids.pl diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h index ba99225b0a37f..70f619a6d6f3f 100644 --- a/src/include/catalog/storage.h +++ b/src/include/catalog/storage.h @@ -4,7 +4,7 @@ * prototypes for functions in backend/catalog/storage.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/storage.h diff --git a/src/include/catalog/storage_xlog.h b/src/include/catalog/storage_xlog.h index e807c442cfe39..c1b2f73666974 100644 --- a/src/include/catalog/storage_xlog.h +++ b/src/include/catalog/storage_xlog.h @@ -4,7 +4,7 @@ * prototypes for XLog support for backend/catalog/storage.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/storage_xlog.h diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h index 0950a07e7c46f..0bc61a8fee986 100644 --- a/src/include/catalog/toasting.h +++ b/src/include/catalog/toasting.h @@ -4,7 +4,7 @@ * This file provides some definitions to support creation of toast tables * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/catalog/toasting.h @@ -14,7 +14,7 @@ #ifndef TOASTING_H #define TOASTING_H -#include "storage/lock.h" +#include "storage/lockdefs.h" /* * toasting.c prototypes diff --git a/src/include/catalog/unused_oids b/src/include/catalog/unused_oids index 763322f52aea8..147fba5e20946 100755 --- a/src/include/catalog/unused_oids +++ b/src/include/catalog/unused_oids @@ -11,7 +11,7 @@ # to take over what was intended as expansion space for something # else. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/include/catalog/unused_oids diff --git a/src/include/commands/alter.h b/src/include/commands/alter.h index b1e4630388a64..ccff3d3a855c7 100644 --- a/src/include/commands/alter.h +++ b/src/include/commands/alter.h @@ -4,7 +4,7 @@ * prototypes for commands/alter.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/alter.h diff --git a/src/include/commands/async.h b/src/include/commands/async.h index f75c3df9556b3..202e4aa5e7452 100644 --- a/src/include/commands/async.h +++ b/src/include/commands/async.h @@ -3,7 +3,7 @@ * async.h * Asynchronous notification: NOTIFY, LISTEN, UNLISTEN * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/async.h @@ -19,9 +19,6 @@ extern PGDLLIMPORT bool Trace_notify; extern PGDLLIMPORT int max_notify_queue_pages; extern PGDLLIMPORT volatile sig_atomic_t notifyInterruptPending; -extern Size AsyncShmemSize(void); -extern void AsyncShmemInit(void); - extern void NotifyMyFrontEnd(const char *channel, const char *payload, int32 srcPid); @@ -46,4 +43,7 @@ extern void HandleNotifyInterrupt(void); /* process interrupts */ extern void ProcessNotifyInterrupt(bool flush); +/* freeze old transaction IDs in notify queue (called by VACUUM) */ +extern void AsyncNotifyFreezeXids(TransactionId newFrozenXid); + #endif /* ASYNC_H */ diff --git a/src/include/commands/collationcmds.h b/src/include/commands/collationcmds.h index c228ac72df4fb..3f84f68868a1a 100644 --- a/src/include/commands/collationcmds.h +++ b/src/include/commands/collationcmds.h @@ -4,7 +4,7 @@ * prototypes for collationcmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/collationcmds.h diff --git a/src/include/commands/comment.h b/src/include/commands/comment.h index f083ea3a3864f..ed7c744caac07 100644 --- a/src/include/commands/comment.h +++ b/src/include/commands/comment.h @@ -7,7 +7,7 @@ * * Prototypes for functions in commands/comment.c * - * Copyright (c) 1999-2025, PostgreSQL Global Development Group + * Copyright (c) 1999-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ diff --git a/src/include/commands/conversioncmds.h b/src/include/commands/conversioncmds.h index 95cd84c833e6b..4f849dfb72145 100644 --- a/src/include/commands/conversioncmds.h +++ b/src/include/commands/conversioncmds.h @@ -4,7 +4,7 @@ * prototypes for conversioncmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/conversioncmds.h diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h index 06dfdfef7210c..abecfe510981e 100644 --- a/src/include/commands/copy.h +++ b/src/include/commands/copy.h @@ -4,7 +4,7 @@ * Definitions for using the POSTGRES copy command. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/copy.h @@ -20,15 +20,12 @@ #include "tcop/dest.h" /* - * Represents whether a header line should be present, and whether it must - * match the actual names (which implies "true"). + * Represents whether a header line must match the actual names + * (which implies "true"), and whether it should be present. */ -typedef enum CopyHeaderChoice -{ - COPY_HEADER_FALSE = 0, - COPY_HEADER_TRUE, - COPY_HEADER_MATCH, -} CopyHeaderChoice; +#define COPY_HEADER_MATCH -1 +#define COPY_HEADER_FALSE 0 +#define COPY_HEADER_TRUE 1 /* * Represents where to save input processing errors. More values to be added @@ -38,6 +35,7 @@ typedef enum CopyOnErrorChoice { COPY_ON_ERROR_STOP = 0, /* immediately throw errors, default */ COPY_ON_ERROR_IGNORE, /* ignore errors */ + COPY_ON_ERROR_SET_NULL, /* set error field to null */ } CopyOnErrorChoice; /* @@ -51,6 +49,17 @@ typedef enum CopyLogVerbosityChoice COPY_LOG_VERBOSITY_VERBOSE, /* logs additional messages */ } CopyLogVerbosityChoice; +/* + * Represents the format of the COPY operation. + */ +typedef enum CopyFormat +{ + COPY_FORMAT_TEXT = 0, + COPY_FORMAT_BINARY, + COPY_FORMAT_CSV, + COPY_FORMAT_JSON, +} CopyFormat; + /* * A struct to hold COPY options, in a parsed form. All of these are related * to formatting, except for 'freeze', which doesn't really belong here, but @@ -61,10 +70,10 @@ typedef struct CopyFormatOptions /* parameters from the COPY command */ int file_encoding; /* file or remote side's character encoding, * -1 if not specified */ - bool binary; /* binary format? */ + CopyFormat format; /* format of the COPY operation */ bool freeze; /* freeze rows on loading? */ - bool csv_mode; /* Comma Separated Value format? */ - CopyHeaderChoice header_line; /* header line? */ + int header_line; /* number of lines to skip or COPY_HEADER_XXX + * value (see the above) */ char *null_print; /* NULL marker string (server encoding!) */ int null_print_len; /* length of same */ char *null_print_client; /* same converted to file encoding */ @@ -79,6 +88,7 @@ typedef struct CopyFormatOptions List *force_notnull; /* list of column names */ bool force_notnull_all; /* FORCE_NOT_NULL *? */ bool *force_notnull_flags; /* per-column CSV FNN flags */ + bool force_array; /* add JSON array decorations */ List *force_null; /* list of column names */ bool force_null_all; /* FORCE_NULL *? */ bool *force_null_flags; /* per-column CSV FN flags */ diff --git a/src/include/commands/copyapi.h b/src/include/commands/copyapi.h index 2a2d2f9876baf..398e7a78bb3e2 100644 --- a/src/include/commands/copyapi.h +++ b/src/include/commands/copyapi.h @@ -4,7 +4,7 @@ * API for COPY TO/FROM handlers * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/copyapi.h diff --git a/src/include/commands/copyfrom_internal.h b/src/include/commands/copyfrom_internal.h index c8b22af22d852..9d3e244ee550c 100644 --- a/src/include/commands/copyfrom_internal.h +++ b/src/include/commands/copyfrom_internal.h @@ -4,7 +4,7 @@ * Internal definitions for COPY FROM command. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/copyfrom_internal.h @@ -108,6 +108,14 @@ typedef struct CopyFromStateData * att */ bool *defaults; /* if DEFAULT marker was found for * corresponding att */ + bool simd_enabled; /* use SIMD to scan for special chars? */ + + /* + * True if the corresponding attribute's is a constrained domain. This + * will be populated only when ON_ERROR is SET_NULL, otherwise NULL. + */ + bool *domain_with_constraint; + bool volatile_defexprs; /* is any of defexprs volatile? */ List *range_table; /* single element list of RangeTblEntry */ List *rteperminfos; /* single element list of RTEPermissionInfo */ diff --git a/src/include/commands/createas.h b/src/include/commands/createas.h index 90612ebbb0e17..f895c67c0c25e 100644 --- a/src/include/commands/createas.h +++ b/src/include/commands/createas.h @@ -4,7 +4,7 @@ * prototypes for createas.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/createas.h diff --git a/src/include/commands/dbcommands.h b/src/include/commands/dbcommands.h index 524ac6d97e898..aa459eebc4698 100644 --- a/src/include/commands/dbcommands.h +++ b/src/include/commands/dbcommands.h @@ -4,7 +4,7 @@ * Database management commands (create/drop database). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/dbcommands.h @@ -14,9 +14,7 @@ #ifndef DBCOMMANDS_H #define DBCOMMANDS_H -#include "access/xlogreader.h" #include "catalog/objectaddress.h" -#include "lib/stringinfo.h" #include "parser/parse_node.h" extern Oid createdb(ParseState *pstate, const CreatedbStmt *stmt); @@ -28,8 +26,6 @@ extern ObjectAddress AlterDatabaseRefreshColl(AlterDatabaseRefreshCollStmt *stmt extern Oid AlterDatabaseSet(AlterDatabaseSetStmt *stmt); extern ObjectAddress AlterDatabaseOwner(const char *dbname, Oid newOwnerId); -extern Oid get_database_oid(const char *dbname, bool missing_ok); -extern char *get_database_name(Oid dbid); extern bool have_createdb_privilege(void); extern void check_encoding_locale_matches(int encoding, const char *collate, const char *ctype); diff --git a/src/include/commands/dbcommands_xlog.h b/src/include/commands/dbcommands_xlog.h index c4202c3156e04..4ff3d0c82c9bf 100644 --- a/src/include/commands/dbcommands_xlog.h +++ b/src/include/commands/dbcommands_xlog.h @@ -4,7 +4,7 @@ * Database resource manager XLOG definitions (create/drop database). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/dbcommands_xlog.h diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index dd22b5efdfd91..d080ad59b71ff 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -4,7 +4,7 @@ * POSTGRES define and remove utility definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/defrem.h @@ -25,8 +25,9 @@ extern void RemoveObjects(DropStmt *stmt); /* commands/indexcmds.c */ -extern ObjectAddress DefineIndex(Oid tableId, - IndexStmt *stmt, +extern ObjectAddress DefineIndex(ParseState *pstate, + Oid tableId, + const IndexStmt *stmt, Oid indexRelationId, Oid parentIndexId, Oid parentConstraintId, @@ -85,7 +86,7 @@ extern void RemoveOperatorById(Oid operOid); extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt); /* commands/statscmds.c */ -extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt); +extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights); extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt); extern void RemoveStatisticsById(Oid statsOid); extern void RemoveStatisticsDataById(Oid statsOid, bool inh); diff --git a/src/include/commands/discard.h b/src/include/commands/discard.h index fac63e9e720c0..592d591efc44a 100644 --- a/src/include/commands/discard.h +++ b/src/include/commands/discard.h @@ -4,7 +4,7 @@ * prototypes for discard.c. * * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/commands/discard.h * diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h index 4c67dd862e305..27340655061f0 100644 --- a/src/include/commands/event_trigger.h +++ b/src/include/commands/event_trigger.h @@ -3,7 +3,7 @@ * event_trigger.h * Declarations for command trigger handling. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/event_trigger.h @@ -75,23 +75,23 @@ extern void EventTriggerUndoInhibitCommandCollection(void); extern void EventTriggerCollectSimpleCommand(ObjectAddress address, ObjectAddress secondaryObject, - Node *parsetree); + const Node *parsetree); -extern void EventTriggerAlterTableStart(Node *parsetree); +extern void EventTriggerAlterTableStart(const Node *parsetree); extern void EventTriggerAlterTableRelid(Oid objectId); -extern void EventTriggerCollectAlterTableSubcmd(Node *subcmd, +extern void EventTriggerCollectAlterTableSubcmd(const Node *subcmd, ObjectAddress address); extern void EventTriggerAlterTableEnd(void); extern void EventTriggerCollectGrant(InternalGrant *istmt); -extern void EventTriggerCollectAlterOpFam(AlterOpFamilyStmt *stmt, +extern void EventTriggerCollectAlterOpFam(const AlterOpFamilyStmt *stmt, Oid opfamoid, List *operators, List *procedures); -extern void EventTriggerCollectCreateOpClass(CreateOpClassStmt *stmt, +extern void EventTriggerCollectCreateOpClass(const CreateOpClassStmt *stmt, Oid opcoid, List *operators, List *procedures); -extern void EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, +extern void EventTriggerCollectAlterTSConfig(const AlterTSConfigurationStmt *stmt, Oid cfgId, Oid *dictIds, int ndicts); -extern void EventTriggerCollectAlterDefPrivs(AlterDefaultPrivilegesStmt *stmt); +extern void EventTriggerCollectAlterDefPrivs(const AlterDefaultPrivilegesStmt *stmt); #endif /* EVENT_TRIGGER_H */ diff --git a/src/include/commands/explain.h b/src/include/commands/explain.h index 3b122f79ed848..472e141bba3ff 100644 --- a/src/include/commands/explain.h +++ b/src/include/commands/explain.h @@ -3,7 +3,7 @@ * explain.h * prototypes for explain.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * src/include/commands/explain.h @@ -14,15 +14,16 @@ #define EXPLAIN_H #include "executor/executor.h" +#include "executor/instrument.h" #include "parser/parse_node.h" -struct ExplainState; /* defined in explain_state.h */ +typedef struct ExplainState ExplainState; /* defined in explain_state.h */ /* Hook for plugins to get control in ExplainOneQuery() */ typedef void (*ExplainOneQuery_hook_type) (Query *query, int cursorOptions, IntoClause *into, - struct ExplainState *es, + ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv); @@ -31,7 +32,7 @@ extern PGDLLIMPORT ExplainOneQuery_hook_type ExplainOneQuery_hook; /* Hook for EXPLAIN plugins to print extra information for each plan */ typedef void (*explain_per_plan_hook_type) (PlannedStmt *plannedstmt, IntoClause *into, - struct ExplainState *es, + ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv); @@ -42,7 +43,7 @@ typedef void (*explain_per_node_hook_type) (PlanState *planstate, List *ancestors, const char *relationship, const char *plan_name, - struct ExplainState *es); + ExplainState *es); extern PGDLLIMPORT explain_per_node_hook_type explain_per_node_hook; /* Hook for plugins to get control in explain_get_index_name() */ @@ -53,32 +54,32 @@ extern PGDLLIMPORT explain_get_index_name_hook_type explain_get_index_name_hook; extern void ExplainQuery(ParseState *pstate, ExplainStmt *stmt, ParamListInfo params, DestReceiver *dest); extern void standard_ExplainOneQuery(Query *query, int cursorOptions, - IntoClause *into, struct ExplainState *es, + IntoClause *into, ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv); extern TupleDesc ExplainResultDesc(ExplainStmt *stmt); extern void ExplainOneUtility(Node *utilityStmt, IntoClause *into, - struct ExplainState *es, ParseState *pstate, + ExplainState *es, ParseState *pstate, ParamListInfo params); extern void ExplainOnePlan(PlannedStmt *plannedstmt, IntoClause *into, - struct ExplainState *es, const char *queryString, + ExplainState *es, const char *queryString, ParamListInfo params, QueryEnvironment *queryEnv, const instr_time *planduration, const BufferUsage *bufusage, const MemoryContextCounters *mem_counters); -extern void ExplainPrintPlan(struct ExplainState *es, QueryDesc *queryDesc); -extern void ExplainPrintTriggers(struct ExplainState *es, +extern void ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc); +extern void ExplainPrintTriggers(ExplainState *es, QueryDesc *queryDesc); -extern void ExplainPrintJITSummary(struct ExplainState *es, +extern void ExplainPrintJITSummary(ExplainState *es, QueryDesc *queryDesc); -extern void ExplainQueryText(struct ExplainState *es, QueryDesc *queryDesc); -extern void ExplainQueryParameters(struct ExplainState *es, +extern void ExplainQueryText(ExplainState *es, QueryDesc *queryDesc); +extern void ExplainQueryParameters(ExplainState *es, ParamListInfo params, int maxlen); #endif /* EXPLAIN_H */ diff --git a/src/include/commands/explain_dr.h b/src/include/commands/explain_dr.h index 55da63d66bdf6..f98eaae186457 100644 --- a/src/include/commands/explain_dr.h +++ b/src/include/commands/explain_dr.h @@ -3,7 +3,7 @@ * explain_dr.h * prototypes for explain_dr.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * src/include/commands/explain_dr.h @@ -16,7 +16,8 @@ #include "executor/instrument.h" #include "tcop/dest.h" -struct ExplainState; /* avoid including explain.h here */ +/* avoid including explain_state.h here */ +typedef struct ExplainState ExplainState; /* Instrumentation data for EXPLAIN's SERIALIZE option */ typedef struct SerializeMetrics @@ -26,7 +27,7 @@ typedef struct SerializeMetrics BufferUsage bufferUsage; /* buffers accessed during serialization */ } SerializeMetrics; -extern DestReceiver *CreateExplainSerializeDestReceiver(struct ExplainState *es); +extern DestReceiver *CreateExplainSerializeDestReceiver(ExplainState *es); extern SerializeMetrics GetSerializationMetrics(DestReceiver *dest); #endif diff --git a/src/include/commands/explain_format.h b/src/include/commands/explain_format.h index 05045bf8cb4af..dacc5e98c591b 100644 --- a/src/include/commands/explain_format.h +++ b/src/include/commands/explain_format.h @@ -3,7 +3,7 @@ * explain_format.h * prototypes for explain_format.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * src/include/commands/explain_format.h @@ -15,44 +15,45 @@ #include "nodes/pg_list.h" -struct ExplainState; /* avoid including explain.h here */ +/* avoid including explain_state.h here */ +typedef struct ExplainState ExplainState; extern void ExplainPropertyList(const char *qlabel, List *data, - struct ExplainState *es); + ExplainState *es); extern void ExplainPropertyListNested(const char *qlabel, List *data, - struct ExplainState *es); + ExplainState *es); extern void ExplainPropertyText(const char *qlabel, const char *value, - struct ExplainState *es); + ExplainState *es); extern void ExplainPropertyInteger(const char *qlabel, const char *unit, - int64 value, struct ExplainState *es); + int64 value, ExplainState *es); extern void ExplainPropertyUInteger(const char *qlabel, const char *unit, - uint64 value, struct ExplainState *es); + uint64 value, ExplainState *es); extern void ExplainPropertyFloat(const char *qlabel, const char *unit, double value, int ndigits, - struct ExplainState *es); + ExplainState *es); extern void ExplainPropertyBool(const char *qlabel, bool value, - struct ExplainState *es); + ExplainState *es); extern void ExplainOpenGroup(const char *objtype, const char *labelname, - bool labeled, struct ExplainState *es); + bool labeled, ExplainState *es); extern void ExplainCloseGroup(const char *objtype, const char *labelname, - bool labeled, struct ExplainState *es); + bool labeled, ExplainState *es); extern void ExplainOpenSetAsideGroup(const char *objtype, const char *labelname, bool labeled, int depth, - struct ExplainState *es); -extern void ExplainSaveGroup(struct ExplainState *es, int depth, + ExplainState *es); +extern void ExplainSaveGroup(ExplainState *es, int depth, int *state_save); -extern void ExplainRestoreGroup(struct ExplainState *es, int depth, +extern void ExplainRestoreGroup(ExplainState *es, int depth, int *state_save); extern void ExplainDummyGroup(const char *objtype, const char *labelname, - struct ExplainState *es); + ExplainState *es); -extern void ExplainBeginOutput(struct ExplainState *es); -extern void ExplainEndOutput(struct ExplainState *es); -extern void ExplainSeparatePlans(struct ExplainState *es); +extern void ExplainBeginOutput(ExplainState *es); +extern void ExplainEndOutput(ExplainState *es); +extern void ExplainSeparatePlans(ExplainState *es); -extern void ExplainIndentText(struct ExplainState *es); +extern void ExplainIndentText(ExplainState *es); #endif diff --git a/src/include/commands/explain_state.h b/src/include/commands/explain_state.h index 32728f5d1a175..97bc7ed49f66b 100644 --- a/src/include/commands/explain_state.h +++ b/src/include/commands/explain_state.h @@ -3,7 +3,7 @@ * explain_state.h * prototypes for explain_state.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * * src/include/commands/explain_state.h @@ -16,6 +16,7 @@ #include "nodes/parsenodes.h" #include "nodes/plannodes.h" #include "parser/parse_node.h" +#include "port/pg_bitutils.h" typedef enum ExplainSerializeOption { @@ -54,6 +55,7 @@ typedef struct ExplainState bool summary; /* print total planning and execution timing */ bool memory; /* print planner's memory usage information */ bool settings; /* print modified settings */ + bool io; /* print info about IO (prefetch, ...) */ bool generic; /* generate a generic plan */ ExplainSerializeOption serialize; /* serialize the query's output? */ ExplainFormat format; /* output format */ @@ -77,9 +79,12 @@ typedef struct ExplainState } ExplainState; typedef void (*ExplainOptionHandler) (ExplainState *, DefElem *, ParseState *); +typedef bool (*ExplainOptionGUCCheckHandler) (const char *option_name, + const char *option_value, + NodeTag option_type); /* Hook to perform additional EXPLAIN options validation */ -typedef void (*explain_validate_options_hook_type) (struct ExplainState *es, List *options, +typedef void (*explain_validate_options_hook_type) (ExplainState *es, List *options, ParseState *pstate); extern PGDLLIMPORT explain_validate_options_hook_type explain_validate_options_hook; @@ -93,8 +98,16 @@ extern void SetExplainExtensionState(ExplainState *es, int extension_id, void *opaque); extern void RegisterExtensionExplainOption(const char *option_name, - ExplainOptionHandler handler); + ExplainOptionHandler handler, + ExplainOptionGUCCheckHandler guc_check_handler); extern bool ApplyExtensionExplainOption(ExplainState *es, DefElem *opt, ParseState *pstate); +extern bool GUCCheckExplainExtensionOption(const char *option_name, + const char *option_value, + NodeTag option_type); + +extern bool GUCCheckBooleanExplainOption(const char *option_name, + const char *option_value, + NodeTag option_type); #endif /* EXPLAIN_STATE_H */ diff --git a/src/include/commands/extension.h b/src/include/commands/extension.h index 24419bfb5c902..7a76bdebcfa7f 100644 --- a/src/include/commands/extension.h +++ b/src/include/commands/extension.h @@ -4,7 +4,7 @@ * Extension management commands (create/drop extension). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/extension.h @@ -52,6 +52,8 @@ extern char *get_extension_name(Oid ext_oid); extern Oid get_extension_schema(Oid ext_oid); extern bool extension_file_exists(const char *extensionName); +extern Oid get_function_sibling_type(Oid funcoid, const char *typname); + extern ObjectAddress AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *oldschema); diff --git a/src/include/commands/lockcmds.h b/src/include/commands/lockcmds.h index 8a0025142ec92..ab116878b15be 100644 --- a/src/include/commands/lockcmds.h +++ b/src/include/commands/lockcmds.h @@ -4,7 +4,7 @@ * prototypes for lockcmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/lockcmds.h diff --git a/src/include/commands/matview.h b/src/include/commands/matview.h index 750bb10ddca45..738c731c1a98c 100644 --- a/src/include/commands/matview.h +++ b/src/include/commands/matview.h @@ -4,7 +4,7 @@ * prototypes for matview.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/matview.h diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h index f06aa1df43946..63c38100fd2e6 100644 --- a/src/include/commands/policy.h +++ b/src/include/commands/policy.h @@ -4,7 +4,7 @@ * prototypes for policy.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/policy.h diff --git a/src/include/commands/portalcmds.h b/src/include/commands/portalcmds.h index baef23d4215c9..ef5c10d640463 100644 --- a/src/include/commands/portalcmds.h +++ b/src/include/commands/portalcmds.h @@ -4,7 +4,7 @@ * prototypes for portalcmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/portalcmds.h diff --git a/src/include/commands/prepare.h b/src/include/commands/prepare.h index 08daac8c92665..5c98dbb9c33c0 100644 --- a/src/include/commands/prepare.h +++ b/src/include/commands/prepare.h @@ -4,7 +4,7 @@ * PREPARE, EXECUTE and DEALLOCATE commands, and prepared-stmt storage * * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/include/commands/prepare.h * diff --git a/src/include/commands/proclang.h b/src/include/commands/proclang.h index fc89282c3656c..68009b27548dc 100644 --- a/src/include/commands/proclang.h +++ b/src/include/commands/proclang.h @@ -3,7 +3,7 @@ * proclang.h * prototypes for proclang.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/proclang.h diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index 7c736e7b03bcf..2a12920c75f2c 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -7,7 +7,7 @@ * constants, you probably also need to update the views based on them * in system_views.sql. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/progress.h @@ -29,6 +29,8 @@ #define PROGRESS_VACUUM_INDEXES_TOTAL 8 #define PROGRESS_VACUUM_INDEXES_PROCESSED 9 #define PROGRESS_VACUUM_DELAY_TIME 10 +#define PROGRESS_VACUUM_MODE 11 +#define PROGRESS_VACUUM_STARTED_BY 12 /* Phases of vacuum (as advertised via PROGRESS_VACUUM_PHASE) */ #define PROGRESS_VACUUM_PHASE_SCAN_HEAP 1 @@ -38,6 +40,16 @@ #define PROGRESS_VACUUM_PHASE_TRUNCATE 5 #define PROGRESS_VACUUM_PHASE_FINAL_CLEANUP 6 +/* Modes of vacuum (as advertised via PROGRESS_VACUUM_MODE) */ +#define PROGRESS_VACUUM_MODE_NORMAL 1 +#define PROGRESS_VACUUM_MODE_AGGRESSIVE 2 +#define PROGRESS_VACUUM_MODE_FAILSAFE 3 + +/* Reasons for vacuum (as advertised via PROGRESS_VACUUM_STARTED_BY) */ +#define PROGRESS_VACUUM_STARTED_BY_MANUAL 1 +#define PROGRESS_VACUUM_STARTED_BY_AUTOVACUUM 2 +#define PROGRESS_VACUUM_STARTED_BY_AUTOVACUUM_WRAPAROUND 3 + /* Progress parameters for analyze */ #define PROGRESS_ANALYZE_PHASE 0 #define PROGRESS_ANALYZE_BLOCKS_TOTAL 1 @@ -48,6 +60,7 @@ #define PROGRESS_ANALYZE_CHILD_TABLES_DONE 6 #define PROGRESS_ANALYZE_CURRENT_CHILD_TABLE_RELID 7 #define PROGRESS_ANALYZE_DELAY_TIME 8 +#define PROGRESS_ANALYZE_STARTED_BY 9 /* Phases of analyze (as advertised via PROGRESS_ANALYZE_PHASE) */ #define PROGRESS_ANALYZE_PHASE_ACQUIRE_SAMPLE_ROWS 1 @@ -56,28 +69,41 @@ #define PROGRESS_ANALYZE_PHASE_COMPUTE_EXT_STATS 4 #define PROGRESS_ANALYZE_PHASE_FINALIZE_ANALYZE 5 -/* Progress parameters for cluster */ -#define PROGRESS_CLUSTER_COMMAND 0 -#define PROGRESS_CLUSTER_PHASE 1 -#define PROGRESS_CLUSTER_INDEX_RELID 2 -#define PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED 3 -#define PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN 4 -#define PROGRESS_CLUSTER_TOTAL_HEAP_BLKS 5 -#define PROGRESS_CLUSTER_HEAP_BLKS_SCANNED 6 -#define PROGRESS_CLUSTER_INDEX_REBUILD_COUNT 7 - -/* Phases of cluster (as advertised via PROGRESS_CLUSTER_PHASE) */ -#define PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP 1 -#define PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP 2 -#define PROGRESS_CLUSTER_PHASE_SORT_TUPLES 3 -#define PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP 4 -#define PROGRESS_CLUSTER_PHASE_SWAP_REL_FILES 5 -#define PROGRESS_CLUSTER_PHASE_REBUILD_INDEX 6 -#define PROGRESS_CLUSTER_PHASE_FINAL_CLEANUP 7 - -/* Commands of PROGRESS_CLUSTER */ -#define PROGRESS_CLUSTER_COMMAND_CLUSTER 1 -#define PROGRESS_CLUSTER_COMMAND_VACUUM_FULL 2 +/* Reasons for analyze (as advertised via PROGRESS_ANALYZE_STARTED_BY) */ +#define PROGRESS_ANALYZE_STARTED_BY_MANUAL 1 +#define PROGRESS_ANALYZE_STARTED_BY_AUTOVACUUM 2 + +/* + * Progress parameters for REPACK. + * + * Values for PROGRESS_REPACK_COMMAND are as in RepackCommand. + * + * Note: Since REPACK shares code with CLUSTER, these values are also + * used by CLUSTER. (CLUSTER being now deprecated, it makes little sense to + * maintain a separate set of constants.) + */ +#define PROGRESS_REPACK_COMMAND 0 +#define PROGRESS_REPACK_PHASE 1 +#define PROGRESS_REPACK_INDEX_RELID 2 +#define PROGRESS_REPACK_HEAP_TUPLES_SCANNED 3 +#define PROGRESS_REPACK_HEAP_TUPLES_INSERTED 4 +#define PROGRESS_REPACK_HEAP_TUPLES_UPDATED 5 +#define PROGRESS_REPACK_HEAP_TUPLES_DELETED 6 +#define PROGRESS_REPACK_TOTAL_HEAP_BLKS 7 +#define PROGRESS_REPACK_HEAP_BLKS_SCANNED 8 +#define PROGRESS_REPACK_INDEX_REBUILD_COUNT 9 + +/* + * Phases of repack (as advertised via PROGRESS_REPACK_PHASE). + */ +#define PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP 1 +#define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP 2 +#define PROGRESS_REPACK_PHASE_SORT_TUPLES 3 +#define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP 4 +#define PROGRESS_REPACK_PHASE_CATCH_UP 5 +#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES 6 +#define PROGRESS_REPACK_PHASE_REBUILD_INDEX 7 +#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP 8 /* Progress parameters for CREATE INDEX */ /* 3, 4 and 5 reserved for "waitfor" metrics */ @@ -130,6 +156,7 @@ #define PROGRESS_BASEBACKUP_BACKUP_STREAMED 2 #define PROGRESS_BASEBACKUP_TBLSPC_TOTAL 3 #define PROGRESS_BASEBACKUP_TBLSPC_STREAMED 4 +#define PROGRESS_BASEBACKUP_BACKUP_TYPE 5 /* Phases of pg_basebackup (as advertised via PROGRESS_BASEBACKUP_PHASE) */ #define PROGRESS_BASEBACKUP_PHASE_WAIT_CHECKPOINT 1 @@ -138,6 +165,10 @@ #define PROGRESS_BASEBACKUP_PHASE_WAIT_WAL_ARCHIVE 4 #define PROGRESS_BASEBACKUP_PHASE_TRANSFER_WAL 5 +/* Types of pg_basebackup (as advertised via PROGRESS_BASEBACKUP_BACKUP_TYPE) */ +#define PROGRESS_BASEBACKUP_BACKUP_TYPE_FULL 1 +#define PROGRESS_BASEBACKUP_BACKUP_TYPE_INCREMENTAL 2 + /* Progress parameters for PROGRESS_COPY */ #define PROGRESS_COPY_BYTES_PROCESSED 0 #define PROGRESS_COPY_BYTES_TOTAL 1 @@ -157,4 +188,20 @@ #define PROGRESS_COPY_TYPE_PIPE 3 #define PROGRESS_COPY_TYPE_CALLBACK 4 +/* Progress parameters for PROGRESS_DATACHECKSUMS */ +#define PROGRESS_DATACHECKSUMS_PHASE 0 +#define PROGRESS_DATACHECKSUMS_DBS_TOTAL 1 +#define PROGRESS_DATACHECKSUMS_DBS_DONE 2 +#define PROGRESS_DATACHECKSUMS_RELS_TOTAL 3 +#define PROGRESS_DATACHECKSUMS_RELS_DONE 4 +#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL 5 +#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE 6 + +/* Phases of datachecksumsworker operation */ +#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING 0 +#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING 1 +#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL 2 +#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER 3 +#define PROGRESS_DATACHECKSUMS_PHASE_DONE 4 + #endif diff --git a/src/include/commands/propgraphcmds.h b/src/include/commands/propgraphcmds.h new file mode 100644 index 0000000000000..1bf7d9ea217d9 --- /dev/null +++ b/src/include/commands/propgraphcmds.h @@ -0,0 +1,23 @@ +/*------------------------------------------------------------------------- + * + * propgraphcmds.h + * prototypes for propgraphcmds.c. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/propgraphcmds.h + * + *------------------------------------------------------------------------- + */ + +#ifndef PROPGRAPHCMDS_H +#define PROPGRAPHCMDS_H + +#include "catalog/objectaddress.h" +#include "parser/parse_node.h" + +extern ObjectAddress CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt); +extern ObjectAddress AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt); + +#endif /* PROPGRAPHCMDS_H */ diff --git a/src/include/commands/publicationcmds.h b/src/include/commands/publicationcmds.h index f90cf1ef896ee..4cf45c17cc561 100644 --- a/src/include/commands/publicationcmds.h +++ b/src/include/commands/publicationcmds.h @@ -4,7 +4,7 @@ * prototypes for publicationcmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/publicationcmds.h diff --git a/src/include/commands/cluster.h b/src/include/commands/repack.h similarity index 58% rename from src/include/commands/cluster.h rename to src/include/commands/repack.h index 60088a64cbb6e..45e5440a31155 100644 --- a/src/include/commands/cluster.h +++ b/src/include/commands/repack.h @@ -1,21 +1,23 @@ /*------------------------------------------------------------------------- * - * cluster.h - * header file for postgres cluster command stuff + * repack.h + * header file for the REPACK command * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994-5, Regents of the University of California * - * src/include/commands/cluster.h + * src/include/commands/repack.h * *------------------------------------------------------------------------- */ -#ifndef CLUSTER_H -#define CLUSTER_H +#ifndef REPACK_H +#define REPACK_H + +#include #include "nodes/parsenodes.h" #include "parser/parse_node.h" -#include "storage/lock.h" +#include "storage/lockdefs.h" #include "utils/relcache.h" @@ -24,15 +26,22 @@ #define CLUOPT_RECHECK 0x02 /* recheck relation state */ #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for * indisclustered */ +#define CLUOPT_ANALYZE 0x08 /* do an ANALYZE */ +#define CLUOPT_CONCURRENT 0x10 /* allow concurrent data changes */ /* options for CLUSTER */ typedef struct ClusterParams { - bits32 options; /* bitmask of CLUOPT_* */ + uint32 options; /* bitmask of CLUOPT_* */ } ClusterParams; -extern void cluster(ParseState *pstate, ClusterStmt *stmt, bool isTopLevel); -extern void cluster_rel(Relation OldHeap, Oid indexOid, ClusterParams *params); +extern PGDLLIMPORT volatile sig_atomic_t RepackMessagePending; + + +extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel); + +extern void cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid, + ClusterParams *params, bool isTopLevel); extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid, LOCKMODE lockmode); extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal); @@ -44,8 +53,16 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap, bool swap_toast_by_content, bool check_constraints, bool is_internal, + bool reindex, TransactionId frozenXid, MultiXactId cutoffMulti, char newrelpersistence); -#endif /* CLUSTER_H */ +extern void HandleRepackMessageInterrupt(void); +extern void ProcessRepackMessages(void); + +/* in repack_worker.c */ +extern void RepackWorkerMain(Datum main_arg); +extern bool AmRepackWorker(void); + +#endif /* REPACK_H */ diff --git a/src/include/commands/repack_internal.h b/src/include/commands/repack_internal.h new file mode 100644 index 0000000000000..6a85cee891012 --- /dev/null +++ b/src/include/commands/repack_internal.h @@ -0,0 +1,124 @@ +/*------------------------------------------------------------------------- + * + * repack_internal.h + * header for REPACK internals + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * src/include/commands/repack_internal.h + * + *------------------------------------------------------------------------- + */ +#ifndef REPACK_INTERNAL_H +#define REPACK_INTERNAL_H + +#include "nodes/execnodes.h" +#include "replication/decode.h" +#include "postmaster/bgworker.h" +#include "replication/logical.h" +#include "storage/buffile.h" +#include "storage/sharedfileset.h" +#include "storage/shm_mq.h" +#include "utils/resowner.h" + +/* + * The type of a change stored in the output files. + */ +typedef char ConcurrentChangeKind; + +#define CHANGE_INSERT 'i' +#define CHANGE_UPDATE_OLD 'u' +#define CHANGE_UPDATE_NEW 'U' +#define CHANGE_DELETE 'd' + +/* + * Logical decoding state. + * + * The output plugin uses it to store the data changes that it decodes from + * WAL while the table contents is being copied to a new storage. + */ +typedef struct RepackDecodingState +{ +#ifdef USE_ASSERT_CHECKING + /* The relation whose changes we're decoding. */ + Oid relid; +#endif + + /* Per-change memory context. */ + MemoryContext change_cxt; + + /* A tuple slot used to pass tuples back and forth */ + TupleTableSlot *slot; + + /* + * Memory context and resource owner of the decoding worker's transaction. + */ + MemoryContext worker_cxt; + ResourceOwner worker_resowner; + + /* The current output file. */ + BufFile *file; +} RepackDecodingState; + +/* + * Shared memory used for communication between the backend running REPACK and + * the worker that performs logical decoding of data changes. + */ +typedef struct DecodingWorkerShared +{ + /* Is the decoding initialized? */ + bool initialized; + + /* + * Once the worker has reached this LSN, it should close the current + * output file and either create a new one or exit, according to the field + * 'done'. If the value is InvalidXLogRecPtr, the worker should decode all + * the WAL available and keep checking this field. It is ok if the worker + * had already decoded records whose LSN is >= lsn_upto before this field + * has been set. + */ + XLogRecPtr lsn_upto; + + /* Exit after closing the current file? */ + bool done; + + /* The output is stored here. */ + SharedFileSet sfs; + + /* Number of the last file exported by the worker. */ + int last_exported; + + /* Synchronize access to the fields above. */ + slock_t mutex; + + /* Database to connect to. */ + Oid dbid; + + /* Role to connect as. */ + Oid roleid; + + /* Relation from which data changes to decode. */ + Oid relid; + + /* CV the backend waits on */ + ConditionVariable cv; + + /* Info to signal the backend. */ + PGPROC *backend_proc; + pid_t backend_pid; + ProcNumber backend_proc_number; + + /* + * Memory the queue is located in. + * + * For considerations on the value see the comments of + * PARALLEL_ERROR_QUEUE_SIZE. + */ +#define REPACK_ERROR_QUEUE_SIZE 16384 + char error_queue[FLEXIBLE_ARRAY_MEMBER]; +} DecodingWorkerShared; + +extern void DecodingWorkerFileName(char *fname, Oid relid, uint32 seq); + + +#endif /* REPACK_INTERNAL_H */ diff --git a/src/include/commands/schemacmds.h b/src/include/commands/schemacmds.h index 8557176b66aa1..b4348436dd38b 100644 --- a/src/include/commands/schemacmds.h +++ b/src/include/commands/schemacmds.h @@ -4,7 +4,7 @@ * prototypes for schemacmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/schemacmds.h @@ -16,12 +16,11 @@ #define SCHEMACMDS_H #include "catalog/objectaddress.h" -#include "nodes/parsenodes.h" +#include "parser/parse_node.h" -extern Oid CreateSchemaCommand(CreateSchemaStmt *stmt, - const char *queryString, +extern Oid CreateSchemaCommand(ParseState *pstate, + CreateSchemaStmt *stmt, int stmt_location, int stmt_len); - extern ObjectAddress RenameSchema(const char *oldname, const char *newname); extern ObjectAddress AlterSchemaOwner(const char *name, Oid newOwnerId); extern void AlterSchemaOwner_oid(Oid schemaoid, Oid newOwnerId); diff --git a/src/include/commands/seclabel.h b/src/include/commands/seclabel.h index 4550be94bc965..f1edeeca0b94f 100644 --- a/src/include/commands/seclabel.h +++ b/src/include/commands/seclabel.h @@ -3,7 +3,7 @@ * * Prototypes for functions in commands/seclabel.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California */ #ifndef SECLABEL_H diff --git a/src/include/commands/sequence.h b/src/include/commands/sequence.h index 9ac0b67683d3c..2c3c4a3f074b9 100644 --- a/src/include/commands/sequence.h +++ b/src/include/commands/sequence.h @@ -3,7 +3,7 @@ * sequence.h * prototypes for sequence.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/sequence.h @@ -13,14 +13,10 @@ #ifndef SEQUENCE_H #define SEQUENCE_H -#include "access/xlogreader.h" #include "catalog/objectaddress.h" #include "fmgr.h" -#include "lib/stringinfo.h" #include "nodes/parsenodes.h" #include "parser/parse_node.h" -#include "storage/relfilelocator.h" - typedef struct FormData_pg_sequence_data { @@ -42,15 +38,6 @@ typedef FormData_pg_sequence_data *Form_pg_sequence_data; #define SEQ_COL_FIRSTCOL SEQ_COL_LASTVAL #define SEQ_COL_LASTCOL SEQ_COL_CALLED -/* XLOG stuff */ -#define XLOG_SEQ_LOG 0x00 - -typedef struct xl_seq_rec -{ - RelFileLocator locator; - /* SEQUENCE TUPLE DATA FOLLOWS AT THE END */ -} xl_seq_rec; - extern int64 nextval_internal(Oid relid, bool check_permissions); extern Datum nextval(PG_FUNCTION_ARGS); extern List *sequence_options(Oid relid); @@ -60,11 +47,7 @@ extern ObjectAddress AlterSequence(ParseState *pstate, AlterSeqStmt *stmt); extern void SequenceChangePersistence(Oid relid, char newrelpersistence); extern void DeleteSequenceTuple(Oid relid); extern void ResetSequence(Oid seq_relid); +extern void SetSequence(Oid relid, int64 next, bool iscalled); extern void ResetSequenceCaches(void); -extern void seq_redo(XLogReaderState *record); -extern void seq_desc(StringInfo buf, XLogReaderState *record); -extern const char *seq_identify(uint8 info); -extern void seq_mask(char *page, BlockNumber blkno); - #endif /* SEQUENCE_H */ diff --git a/src/include/commands/sequence_xlog.h b/src/include/commands/sequence_xlog.h new file mode 100644 index 0000000000000..b0495f41b43d7 --- /dev/null +++ b/src/include/commands/sequence_xlog.h @@ -0,0 +1,45 @@ +/*------------------------------------------------------------------------- + * + * sequence_xlog.h + * Sequence WAL definitions. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/sequence_xlog.h + * + *------------------------------------------------------------------------- + */ + +#ifndef SEQUENCE_XLOG_H +#define SEQUENCE_XLOG_H + +#include "access/xlogreader.h" +#include "lib/stringinfo.h" + +/* Record identifier */ +#define XLOG_SEQ_LOG 0x00 + +/* + * The "special area" of a sequence's buffer page looks like this. + */ +#define SEQ_MAGIC 0x1717 + +typedef struct sequence_magic +{ + uint32 magic; +} sequence_magic; + +/* Sequence WAL record */ +typedef struct xl_seq_rec +{ + RelFileLocator locator; + /* SEQUENCE TUPLE DATA FOLLOWS AT THE END */ +} xl_seq_rec; + +extern void seq_redo(XLogReaderState *record); +extern void seq_desc(StringInfo buf, XLogReaderState *record); +extern const char *seq_identify(uint8 info); +extern void seq_mask(char *page, BlockNumber blkno); + +#endif /* SEQUENCE_XLOG_H */ diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h index c2262e46a7f5c..63504232a1481 100644 --- a/src/include/commands/subscriptioncmds.h +++ b/src/include/commands/subscriptioncmds.h @@ -4,7 +4,7 @@ * prototypes for subscriptioncmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/subscriptioncmds.h @@ -28,4 +28,12 @@ extern void AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId); extern char defGetStreamingMode(DefElem *def); +extern ObjectAddress AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, bool isTopLevel); + +extern void CheckSubDeadTupleRetention(bool check_guc, bool sub_disabled, + int elevel_for_sub_disabled, + bool retain_dead_tuples, + bool retention_active, + bool max_retention_set); + #endif /* SUBSCRIPTIONCMDS_H */ diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 6832470d38729..c3d8518cb6254 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -4,7 +4,7 @@ * prototypes for tablecmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/tablecmds.h @@ -18,10 +18,11 @@ #include "catalog/dependency.h" #include "catalog/objectaddress.h" #include "nodes/parsenodes.h" -#include "storage/lock.h" +#include "storage/lockdefs.h" #include "utils/relcache.h" -struct AlterTableUtilityContext; /* avoid including tcop/utility.h here */ +typedef struct AlterTableUtilityContext AlterTableUtilityContext; /* avoid including + * tcop/utility.h here */ extern ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, @@ -34,7 +35,7 @@ extern void RemoveRelations(DropStmt *drop); extern Oid AlterTableLookupRelation(AlterTableStmt *stmt, LOCKMODE lockmode); extern void AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode, - struct AlterTableUtilityContext *context); + AlterTableUtilityContext *context); extern LOCKMODE AlterTableGetLockLevel(List *cmds); diff --git a/src/include/commands/tablespace.h b/src/include/commands/tablespace.h index 4e8bf4dc0debd..4fa85c2906b01 100644 --- a/src/include/commands/tablespace.h +++ b/src/include/commands/tablespace.h @@ -4,7 +4,7 @@ * Tablespace management commands (create/drop tablespace). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/tablespace.h diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 2ed2c4bb3784b..1d9869973c094 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -3,7 +3,7 @@ * trigger.h * Declarations for trigger handling. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/trigger.h @@ -78,7 +78,9 @@ typedef struct TransitionCaptureState /* * Private data including the tuplestore(s) into which to insert tuples. */ - struct AfterTriggersTableData *tcs_private; + struct AfterTriggersTableData *tcs_insert_private; + struct AfterTriggersTableData *tcs_update_private; + struct AfterTriggersTableData *tcs_delete_private; } TransitionCaptureState; /* @@ -151,11 +153,11 @@ extern PGDLLIMPORT int SessionReplicationRole; #define TRIGGER_FIRES_ON_REPLICA 'R' #define TRIGGER_DISABLED 'D' -extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString, +extern ObjectAddress CreateTrigger(const CreateTrigStmt *stmt, const char *queryString, Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid, Oid funcoid, Oid parentTriggerOid, Node *whenClause, bool isInternal, bool in_partition); -extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, +extern ObjectAddress CreateTriggerFiringOn(const CreateTrigStmt *stmt, const char *queryString, Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid, Oid funcoid, Oid parentTriggerOid, Node *whenClause, bool isInternal, bool in_partition, @@ -213,7 +215,8 @@ extern bool ExecBRDeleteTriggers(EState *estate, HeapTuple fdw_trigtuple, TupleTableSlot **epqslot, TM_Result *tmresult, - TM_FailureData *tmfd); + TM_FailureData *tmfd, + bool is_merge_delete); extern void ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo, ItemPointer tupleid, @@ -235,7 +238,8 @@ extern bool ExecBRUpdateTriggers(EState *estate, HeapTuple fdw_trigtuple, TupleTableSlot *newslot, TM_Result *tmresult, - TM_FailureData *tmfd); + TM_FailureData *tmfd, + bool is_merge_update); extern void ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, ResultRelInfo *src_partinfo, @@ -285,4 +289,25 @@ extern void RI_PartitionRemove_Check(Trigger *trigger, Relation fk_rel, extern int RI_FKey_trigger_type(Oid tgfoid); +/* + * Callback type for end-of-trigger-batch callbacks. + * + * Currently used by ri_triggers.c to flush fast-path FK batches and + * clean up associated resources. + * + * Registered via RegisterAfterTriggerBatchCallback(). Invoked when + * the current trigger-firing batch completes: + * - AfterTriggerEndQuery() (immediate constraints) + * - AfterTriggerFireDeferred() (deferred constraints at COMMIT) + * - AfterTriggerSetState() (SET CONSTRAINTS IMMEDIATE) + * + * The callback list is cleared after each batch. Callers must + * re-register if they need to be called again in a subsequent batch. + */ +typedef void (*AfterTriggerBatchCallback) (void *arg); + +extern void RegisterAfterTriggerBatchCallback(AfterTriggerBatchCallback callback, + void *arg); +extern bool AfterTriggerIsActive(void); + #endif /* TRIGGER_H */ diff --git a/src/include/commands/typecmds.h b/src/include/commands/typecmds.h index 6cc387e333712..f0c5c11132631 100644 --- a/src/include/commands/typecmds.h +++ b/src/include/commands/typecmds.h @@ -4,7 +4,7 @@ * prototypes for typecmds.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/typecmds.h diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index bc37a80dc74fa..956d9cea36da6 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -4,7 +4,7 @@ * header file for postgres vacuum cleaner and statistics analyzer * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/vacuum.h @@ -23,7 +23,6 @@ #include "catalog/pg_type.h" #include "parser/parse_node.h" #include "storage/buf.h" -#include "storage/lock.h" #include "utils/relcache.h" /* @@ -216,7 +215,7 @@ typedef enum VacOptValue */ typedef struct VacuumParams { - bits32 options; /* bitmask of VACOPT_* */ + uint32 options; /* bitmask of VACOPT_* */ int freeze_min_age; /* min freeze age, -1 to use default */ int freeze_table_age; /* age at which to scan whole table */ int multixact_freeze_min_age; /* min multixact freeze age, -1 to @@ -224,9 +223,14 @@ typedef struct VacuumParams int multixact_freeze_table_age; /* multixact age at which to scan * whole table */ bool is_wraparound; /* force a for-wraparound vacuum */ - int log_min_duration; /* minimum execution threshold in ms at - * which autovacuum is logged, -1 to use - * default */ + int log_vacuum_min_duration; /* minimum execution threshold in + * ms at which vacuum by + * autovacuum is logged, -1 to use + * default */ + int log_analyze_min_duration; /* minimum execution threshold in + * ms at which analyze by + * autovacuum is logged, -1 to use + * default */ VacOptValue index_cleanup; /* Do index vacuum and cleanup */ VacOptValue truncate; /* Truncate empty pages at the end */ Oid toast_parent; /* for privilege checks when recursing */ @@ -295,6 +299,28 @@ typedef struct VacDeadItemsInfo int64 num_items; /* current # of entries */ } VacDeadItemsInfo; +/* + * Statistics for parallel vacuum workers (planned vs. actual) + */ +typedef struct PVWorkerStats +{ + /* Number of parallel workers planned to launch */ + int nplanned; + + /* Number of parallel workers that were successfully launched */ + int nlaunched; +} PVWorkerStats; + +/* + * PVWorkerUsage stores information about total number of launched and + * planned workers during parallel vacuum (both for index vacuum and cleanup). + */ +typedef struct PVWorkerUsage +{ + PVWorkerStats vacuum; + PVWorkerStats cleanup; +} PVWorkerUsage; + /* GUC parameters */ extern PGDLLIMPORT int default_statistics_target; /* PGDLLIMPORT for PostGIS */ extern PGDLLIMPORT int vacuum_freeze_min_age; @@ -336,7 +362,7 @@ extern PGDLLIMPORT int64 parallel_vacuum_worker_delay_ns; /* in commands/vacuum.c */ extern void ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel); -extern void vacuum(List *relations, VacuumParams *params, +extern void vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrategy, MemoryContext vac_context, bool isTopLevel); extern void vac_open_indexes(Relation relation, LOCKMODE lockmode, @@ -363,9 +389,9 @@ extern bool vacuum_xid_failsafe_check(const struct VacuumCutoffs *cutoffs); extern void vac_update_datfrozenxid(void); extern void vacuum_delay_point(bool is_analyze); extern bool vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple, - bits32 options); + uint32 options); extern Relation vacuum_open_relation(Oid relid, RangeVar *relation, - bits32 options, bool verbose, + uint32 options, bool verbose, LOCKMODE lmode); extern IndexBulkDeleteResult *vac_bulkdel_one_index(IndexVacuumInfo *ivinfo, IndexBulkDeleteResult *istat, @@ -389,17 +415,23 @@ extern TidStore *parallel_vacuum_get_dead_items(ParallelVacuumState *pvs, extern void parallel_vacuum_reset_dead_items(ParallelVacuumState *pvs); extern void parallel_vacuum_bulkdel_all_indexes(ParallelVacuumState *pvs, long num_table_tuples, - int num_index_scans); + int num_index_scans, + PVWorkerStats *wstats); extern void parallel_vacuum_cleanup_all_indexes(ParallelVacuumState *pvs, long num_table_tuples, int num_index_scans, - bool estimated_count); + bool estimated_count, + PVWorkerStats *wstats); +extern void parallel_vacuum_update_shared_delay_params(void); +extern void parallel_vacuum_propagate_shared_delay_params(void); extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc); /* in commands/analyze.c */ extern void analyze_rel(Oid relid, RangeVar *relation, - VacuumParams *params, List *va_cols, bool in_outer_xact, + const VacuumParams *params, List *va_cols, bool in_outer_xact, BufferAccessStrategy bstrategy); +extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr, + int *p_attstattarget); extern bool std_typanalyze(VacAttrStats *stats); /* in utils/misc/sampling.c --- duplicate of declarations in utils/sampling.h */ diff --git a/src/include/commands/view.h b/src/include/commands/view.h index c41f51b161c95..c261aec400a69 100644 --- a/src/include/commands/view.h +++ b/src/include/commands/view.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/commands/view.h diff --git a/src/include/commands/wait.h b/src/include/commands/wait.h new file mode 100644 index 0000000000000..a563579695c95 --- /dev/null +++ b/src/include/commands/wait.h @@ -0,0 +1,23 @@ +/*------------------------------------------------------------------------- + * + * wait.h + * prototypes for commands/wait.c + * + * Portions Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * src/include/commands/wait.h + * + *------------------------------------------------------------------------- + */ +#ifndef WAIT_H +#define WAIT_H + +#include "nodes/parsenodes.h" +#include "parser/parse_node.h" +#include "tcop/dest.h" + +extern void ExecWaitStmt(ParseState *pstate, WaitStmt *stmt, bool isTopLevel, + DestReceiver *dest); +extern TupleDesc WaitStmtResultDesc(WaitStmt *stmt); + +#endif /* WAIT_H */ diff --git a/src/include/common/archive.h b/src/include/common/archive.h index 6fed2a3a6c8b6..144074d8f767c 100644 --- a/src/include/common/archive.h +++ b/src/include/common/archive.h @@ -3,7 +3,7 @@ * archive.h * Common WAL archive routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/archive.h diff --git a/src/include/common/base64.h b/src/include/common/base64.h index 66cb57b017fb9..2ea5ffeeb77c0 100644 --- a/src/include/common/base64.h +++ b/src/include/common/base64.h @@ -3,7 +3,7 @@ * Encoding and decoding routines for base64 without whitespace * support. * - * Portions Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/common/base64.h */ diff --git a/src/include/common/blkreftable.h b/src/include/common/blkreftable.h index 67630d7460439..b5b5517f6605c 100644 --- a/src/include/common/blkreftable.h +++ b/src/include/common/blkreftable.h @@ -14,7 +14,7 @@ * the limit block number should be set to the length in blocks to * which it was truncated. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/common/blkreftable.h * diff --git a/src/include/common/checksum_helper.h b/src/include/common/checksum_helper.h index af423c30d7be0..0e3ed960e56c0 100644 --- a/src/include/common/checksum_helper.h +++ b/src/include/common/checksum_helper.h @@ -3,7 +3,7 @@ * checksum_helper.h * Compute a checksum of any of various types using common routines * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/common/checksum_helper.h diff --git a/src/include/common/compression.h b/src/include/common/compression.h index 640b42040483b..f99c747cdd3fa 100644 --- a/src/include/common/compression.h +++ b/src/include/common/compression.h @@ -4,7 +4,7 @@ * * Shared definitions for compression methods and specifications. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/common/compression.h @@ -41,6 +41,8 @@ typedef struct pg_compress_specification extern void parse_compress_options(const char *option, char **algorithm, char **detail); +extern bool parse_tar_compress_algorithm(const char *fname, + pg_compress_algorithm *algorithm); extern bool parse_compress_algorithm(char *name, pg_compress_algorithm *algorithm); extern const char *get_compress_algorithm_name(pg_compress_algorithm algorithm); diff --git a/src/include/common/config_info.h b/src/include/common/config_info.h index 860277410b952..7c23664dfe707 100644 --- a/src/include/common/config_info.h +++ b/src/include/common/config_info.h @@ -2,7 +2,7 @@ * config_info.h * Common code for pg_config output * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/common/config_info.h */ diff --git a/src/include/common/connect.h b/src/include/common/connect.h index e7413c1bd8153..bc4155af7d1e4 100644 --- a/src/include/common/connect.h +++ b/src/include/common/connect.h @@ -3,7 +3,7 @@ * Interfaces in support of FE/BE connections. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/connect.h diff --git a/src/include/common/controldata_utils.h b/src/include/common/controldata_utils.h index 778eba1fb6abf..6dd0999f80563 100644 --- a/src/include/common/controldata_utils.h +++ b/src/include/common/controldata_utils.h @@ -2,7 +2,7 @@ * controldata_utils.h * Common code for pg_controldata output * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/controldata_utils.h diff --git a/src/include/common/cryptohash.h b/src/include/common/cryptohash.h index ca103efbc3f18..60ebf83905f00 100644 --- a/src/include/common/cryptohash.h +++ b/src/include/common/cryptohash.h @@ -3,7 +3,7 @@ * cryptohash.h * Generic headers for cryptographic hash functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/common/fe_memutils.h b/src/include/common/fe_memutils.h index cd2052878618e..eccdfcab0866c 100644 --- a/src/include/common/fe_memutils.h +++ b/src/include/common/fe_memutils.h @@ -2,7 +2,7 @@ * fe_memutils.h * memory management support for frontend code * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/common/fe_memutils.h */ diff --git a/src/include/common/file_perm.h b/src/include/common/file_perm.h index c621d82511efe..d454c1afcd876 100644 --- a/src/include/common/file_perm.h +++ b/src/include/common/file_perm.h @@ -3,7 +3,7 @@ * File and directory permission definitions * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/file_perm.h diff --git a/src/include/common/file_utils.h b/src/include/common/file_utils.h index 9fd88953e43b1..d6415424b602c 100644 --- a/src/include/common/file_utils.h +++ b/src/include/common/file_utils.h @@ -3,7 +3,7 @@ * Assorted utility functions to work on files. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/file_utils.h @@ -55,9 +55,9 @@ extern int compute_remaining_iovec(struct iovec *destination, extern ssize_t pg_pwritev_with_retry(int fd, const struct iovec *iov, int iovcnt, - off_t offset); + pgoff_t offset); -extern ssize_t pg_pwrite_zeros(int fd, size_t size, off_t offset); +extern ssize_t pg_pwrite_zeros(int fd, size_t size, pgoff_t offset); /* Filename components */ #define PG_TEMP_FILES_DIR "pgsql_tmp" diff --git a/src/include/common/hashfn.h b/src/include/common/hashfn.h index 4430fb6983740..a40434f798f07 100644 --- a/src/include/common/hashfn.h +++ b/src/include/common/hashfn.h @@ -1,7 +1,7 @@ /* * Utilities for working with hash values. * - * Portions Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2017-2026, PostgreSQL Global Development Group */ #ifndef HASHFN_H diff --git a/src/include/common/hashfn_unstable.h b/src/include/common/hashfn_unstable.h index 8818a0d360b69..06bdf6d58669c 100644 --- a/src/include/common/hashfn_unstable.h +++ b/src/include/common/hashfn_unstable.h @@ -7,7 +7,7 @@ * indexes or other on-disk structures. See hashfn.h if you need stability. * * - * Portions Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2024-2026, PostgreSQL Global Development Group * * src/include/common/hashfn_unstable.h */ @@ -50,9 +50,7 @@ /* * fasthash as implemented here has two interfaces: * - * 1) Standalone functions, e.g. fasthash32() for a single value with a - * known length. These return the same hash code as the original, at - * least on little-endian machines. + * 1) Standalone functions that take a single input. * * 2) Incremental interface. This can used for incorporating multiple * inputs. First, initialize the hash state (here with a zero seed): @@ -60,6 +58,7 @@ * fasthash_state hs; * fasthash_init(&hs, 0); * + * Next, accumulate input into the hash state. * If the inputs are of types that can be trivially cast to uint64, it's * sufficient to do: * @@ -73,20 +72,28 @@ * flexible, but more verbose method. The standalone functions use this * internally, so see fasthash64() for an example of this. * - * After all inputs have been mixed in, finalize the hash: + * After all inputs have been mixed in, finalize the hash and optionally + * reduce to 32 bits. If all inputs are fixed-length, it's sufficient + * to pass zero for the tweak: * * hashcode = fasthash_final32(&hs, 0); * + * For variable length input, experimentation has found that SMHasher + * fails unless we pass the length for the tweak. When accumulating + * multiple varlen values, it's probably safest to calculate a tweak + * such that the bits of all individual lengths are present, for example: + * + * lengths = len1 + (len2 << 10) + (len3 << 20); + * hashcode = fasthash_final32(&hs, lengths); + * * The incremental interface allows an optimization for NUL-terminated * C strings: * * len = fasthash_accum_cstring(&hs, str); * hashcode = fasthash_final32(&hs, len); * - * By handling the terminator on-the-fly, we can avoid needing a strlen() - * call to tell us how many bytes to hash. Experimentation has found that - * SMHasher fails unless we incorporate the length, so it is passed to - * the finalizer as a tweak. + * By computing the length on-the-fly, we can avoid needing a strlen() + * call to tell us how many bytes to hash. */ @@ -151,23 +158,23 @@ fasthash_accum(fasthash_state *hs, const char *k, size_t len) break; case 7: hs->accum |= (uint64) k[6] << 8; - /* FALLTHROUGH */ + pg_fallthrough; case 6: hs->accum |= (uint64) k[5] << 16; - /* FALLTHROUGH */ + pg_fallthrough; case 5: hs->accum |= (uint64) k[4] << 24; - /* FALLTHROUGH */ + pg_fallthrough; case 4: memcpy(&lower_four, k, sizeof(lower_four)); hs->accum |= (uint64) lower_four << 32; break; case 3: hs->accum |= (uint64) k[2] << 40; - /* FALLTHROUGH */ + pg_fallthrough; case 2: hs->accum |= (uint64) k[1] << 48; - /* FALLTHROUGH */ + pg_fallthrough; case 1: hs->accum |= (uint64) k[0] << 56; break; @@ -182,23 +189,23 @@ fasthash_accum(fasthash_state *hs, const char *k, size_t len) break; case 7: hs->accum |= (uint64) k[6] << 48; - /* FALLTHROUGH */ + pg_fallthrough; case 6: hs->accum |= (uint64) k[5] << 40; - /* FALLTHROUGH */ + pg_fallthrough; case 5: hs->accum |= (uint64) k[4] << 32; - /* FALLTHROUGH */ + pg_fallthrough; case 4: memcpy(&lower_four, k, sizeof(lower_four)); hs->accum |= lower_four; break; case 3: hs->accum |= (uint64) k[2] << 16; - /* FALLTHROUGH */ + pg_fallthrough; case 2: hs->accum |= (uint64) k[1] << 8; - /* FALLTHROUGH */ + pg_fallthrough; case 1: hs->accum |= (uint64) k[0]; break; @@ -264,7 +271,7 @@ fasthash_accum_cstring_aligned(fasthash_state *hs, const char *str) */ for (;;) { - uint64 chunk = *(uint64 *) str; + uint64 chunk = *(const uint64 *) str; zero_byte_low = haszero64(chunk); if (zero_byte_low) @@ -350,9 +357,13 @@ fasthash_final32(fasthash_state *hs, uint64 tweak) return fasthash_reduce32(fasthash_final64(hs, tweak)); } + +/* Standalone functions */ + /* * The original fasthash64 function, re-implemented using the incremental - * interface. Returns a 64-bit hashcode. 'len' controls not only how + * interface. Returns the same 64-bit hashcode as the original, + * at least on little-endian machines. 'len' controls not only how * many bytes to hash, but also modifies the internal seed. * 'seed' can be zero. */ @@ -374,6 +385,11 @@ fasthash64(const char *k, size_t len, uint64 seed) } fasthash_accum(&hs, k, len); + + /* + * Since we already mixed the input length into the seed, we can just pass + * zero here. This matches upstream behavior as well. + */ return fasthash_final64(&hs, 0); } @@ -386,6 +402,9 @@ fasthash32(const char *k, size_t len, uint64 seed) /* * Convenience function for hashing NUL-terminated strings + * + * Note: This is faster than, and computes a different result from, + * "fasthash32(s, strlen(s))" */ static inline uint32 hash_string(const char *s) diff --git a/src/include/common/hmac.h b/src/include/common/hmac.h index c4d069e49a13e..8ed99905d102c 100644 --- a/src/include/common/hmac.h +++ b/src/include/common/hmac.h @@ -3,7 +3,7 @@ * hmac.h * Generic headers for HMAC * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/common/int.h b/src/include/common/int.h index 3973f13379d86..6b4e0f16eda39 100644 --- a/src/include/common/int.h +++ b/src/include/common/int.h @@ -11,7 +11,7 @@ * the 64 bit cases can be considerably faster with intrinsics. In case no * intrinsics are available 128 bit math is used where available. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/common/int.h * @@ -601,6 +601,73 @@ pg_neg_u64_overflow(uint64 a, int64 *result) #endif } +/* + * size_t + */ +static inline bool +pg_add_size_overflow(size_t a, size_t b, size_t *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_add_overflow(a, b, result); +#else + size_t res = a + b; + + if (res < a) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = res; + return false; +#endif +} + +static inline bool +pg_sub_size_overflow(size_t a, size_t b, size_t *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_sub_overflow(a, b, result); +#else + if (b > a) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = a - b; + return false; +#endif +} + +static inline bool +pg_mul_size_overflow(size_t a, size_t b, size_t *result) +{ +#if defined(HAVE__BUILTIN_OP_OVERFLOW) + return __builtin_mul_overflow(a, b, result); +#else + size_t res = a * b; + + if (a != 0 && b != res / a) + { + *result = 0x5EED; /* to avoid spurious warnings */ + return true; + } + *result = res; + return false; +#endif +} + +/* + * pg_neg_size_overflow is currently omitted, to avoid having to reason about + * the portability of SSIZE_MIN/_MAX before a use case exists. + */ +/* + * static inline bool + * pg_neg_size_overflow(size_t a, ssize_t *result) + * { + * ... + * } + */ + /*------------------------------------------------------------------------ * * Comparison routines for integer types. diff --git a/src/include/common/int128.h b/src/include/common/int128.h index a50f5709c2988..7db8aff94d35c 100644 --- a/src/include/common/int128.h +++ b/src/include/common/int128.h @@ -6,9 +6,9 @@ * We make use of the native int128 type if there is one, otherwise * implement things the hard way based on two int64 halves. * - * See src/tools/testint128.c for a simple test harness for this file. + * See src/test/modules/test_int128 for a simple test harness for this file. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/common/int128.h * @@ -29,146 +29,172 @@ #endif #endif - +/* + * If native int128 support is enabled, INT128 is just int128. Otherwise, it + * is a structure with separate 64-bit high and low parts. + * + * We lay out the INT128 structure with the same content and byte ordering + * that a native int128 type would (probably) have. This makes no difference + * for ordinary use of INT128, but allows union'ing INT128 with int128 for + * testing purposes. + * + * PG_INT128_HI_INT64 and PG_INT128_LO_UINT64 allow the (signed) high and + * (unsigned) low 64-bit integer parts to be extracted portably on all + * platforms. + */ #if USE_NATIVE_INT128 typedef int128 INT128; -/* - * Add an unsigned int64 value into an INT128 variable. - */ -static inline void -int128_add_uint64(INT128 *i128, uint64 v) +#define PG_INT128_HI_INT64(i128) ((int64) ((i128) >> 64)) +#define PG_INT128_LO_UINT64(i128) ((uint64) (i128)) + +#else + +typedef struct { - *i128 += v; -} +#ifdef WORDS_BIGENDIAN + int64 hi; /* most significant 64 bits, including sign */ + uint64 lo; /* least significant 64 bits, without sign */ +#else + uint64 lo; /* least significant 64 bits, without sign */ + int64 hi; /* most significant 64 bits, including sign */ +#endif +} INT128; + +#define PG_INT128_HI_INT64(i128) ((i128).hi) +#define PG_INT128_LO_UINT64(i128) ((i128).lo) + +#endif /* - * Add a signed int64 value into an INT128 variable. + * Construct an INT128 from (signed) high and (unsigned) low 64-bit integer + * parts. */ -static inline void -int128_add_int64(INT128 *i128, int64 v) +static inline INT128 +make_int128(int64 hi, uint64 lo) { - *i128 += v; +#if USE_NATIVE_INT128 + return (((int128) hi) << 64) + lo; +#else + INT128 val; + + val.hi = hi; + val.lo = lo; + return val; +#endif } /* - * Add the 128-bit product of two int64 values into an INT128 variable. - * - * XXX with a stupid compiler, this could actually be less efficient than - * the other implementation; maybe we should do it by hand always? + * Add an unsigned int64 value into an INT128 variable. */ static inline void -int128_add_int64_mul_int64(INT128 *i128, int64 x, int64 y) +int128_add_uint64(INT128 *i128, uint64 v) { - *i128 += (int128) x * (int128) y; -} +#if USE_NATIVE_INT128 + *i128 += v; +#else + /* + * First add the value to the .lo part, then check to see if a carry needs + * to be propagated into the .hi part. Since this is unsigned integer + * arithmetic, which is just modular arithmetic, a carry is needed if the + * new .lo part is less than the old .lo part (i.e., if modular + * wrap-around occurred). Writing this in the form below, rather than + * using an "if" statement causes modern compilers to produce branchless + * machine code identical to the native code. + */ + uint64 oldlo = i128->lo; -/* - * Compare two INT128 values, return -1, 0, or +1. - */ -static inline int -int128_compare(INT128 x, INT128 y) -{ - if (x < y) - return -1; - if (x > y) - return 1; - return 0; + i128->lo += v; + i128->hi += (i128->lo < oldlo); +#endif } /* - * Widen int64 to INT128. + * Add a signed int64 value into an INT128 variable. */ -static inline INT128 -int64_to_int128(int64 v) +static inline void +int128_add_int64(INT128 *i128, int64 v) { - return (INT128) v; -} +#if USE_NATIVE_INT128 + *i128 += v; +#else + /* + * This is much like the above except that the carry logic differs for + * negative v -- we need to subtract 1 from the .hi part if the new .lo + * value is greater than the old .lo value. That can be achieved without + * any branching by adding the sign bit from v (v >> 63 = 0 or -1) to the + * previous result (for negative v, if the new .lo value is less than the + * old .lo value, the two terms cancel and we leave the .hi part + * unchanged, otherwise we subtract 1 from the .hi part). With modern + * compilers this often produces machine code identical to the native + * code. + */ + uint64 oldlo = i128->lo; -/* - * Convert INT128 to int64 (losing any high-order bits). - * This also works fine for casting down to uint64. - */ -static inline int64 -int128_to_int64(INT128 val) -{ - return (int64) val; + i128->lo += v; + i128->hi += (i128->lo < oldlo) + (v >> 63); +#endif } -#else /* !USE_NATIVE_INT128 */ - /* - * We lay out the INT128 structure with the same content and byte ordering - * that a native int128 type would (probably) have. This makes no difference - * for ordinary use of INT128, but allows union'ing INT128 with int128 for - * testing purposes. + * Add an INT128 value into an INT128 variable. */ -typedef struct +static inline void +int128_add_int128(INT128 *i128, INT128 v) { -#ifdef WORDS_BIGENDIAN - int64 hi; /* most significant 64 bits, including sign */ - uint64 lo; /* least significant 64 bits, without sign */ +#if USE_NATIVE_INT128 + *i128 += v; #else - uint64 lo; /* least significant 64 bits, without sign */ - int64 hi; /* most significant 64 bits, including sign */ + int128_add_uint64(i128, v.lo); + i128->hi += v.hi; #endif -} INT128; +} /* - * Add an unsigned int64 value into an INT128 variable. + * Subtract an unsigned int64 value from an INT128 variable. */ static inline void -int128_add_uint64(INT128 *i128, uint64 v) +int128_sub_uint64(INT128 *i128, uint64 v) { +#if USE_NATIVE_INT128 + *i128 -= v; +#else /* - * First add the value to the .lo part, then check to see if a carry needs - * to be propagated into the .hi part. A carry is needed if both inputs - * have high bits set, or if just one input has high bit set while the new - * .lo part doesn't. Remember that .lo part is unsigned; we cast to - * signed here just as a cheap way to check the high bit. + * This is like int128_add_uint64(), except we must propagate a borrow to + * (subtract 1 from) the .hi part if the new .lo part is greater than the + * old .lo part. */ uint64 oldlo = i128->lo; - i128->lo += v; - if (((int64) v < 0 && (int64) oldlo < 0) || - (((int64) v < 0 || (int64) oldlo < 0) && (int64) i128->lo >= 0)) - i128->hi++; + i128->lo -= v; + i128->hi -= (i128->lo > oldlo); +#endif } /* - * Add a signed int64 value into an INT128 variable. + * Subtract a signed int64 value from an INT128 variable. */ static inline void -int128_add_int64(INT128 *i128, int64 v) +int128_sub_int64(INT128 *i128, int64 v) { - /* - * This is much like the above except that the carry logic differs for - * negative v. Ordinarily we'd need to subtract 1 from the .hi part - * (corresponding to adding the sign-extended bits of v to it); but if - * there is a carry out of the .lo part, that cancels and we do nothing. - */ +#if USE_NATIVE_INT128 + *i128 -= v; +#else + /* Like int128_add_int64() with the sign of v inverted */ uint64 oldlo = i128->lo; - i128->lo += v; - if (v >= 0) - { - if ((int64) oldlo < 0 && (int64) i128->lo >= 0) - i128->hi++; - } - else - { - if (!((int64) oldlo < 0 || (int64) i128->lo >= 0)) - i128->hi--; - } + i128->lo -= v; + i128->hi -= (i128->lo > oldlo) + (v >> 63); +#endif } /* - * INT64_AU32 extracts the most significant 32 bits of int64 as int64, while - * INT64_AL32 extracts the least significant 32 bits as uint64. + * INT64_HI_INT32 extracts the most significant 32 bits of int64 as int32. + * INT64_LO_UINT32 extracts the least significant 32 bits as uint32. */ -#define INT64_AU32(i64) ((i64) >> 32) -#define INT64_AL32(i64) ((i64) & UINT64CONST(0xFFFFFFFF)) +#define INT64_HI_INT32(i64) ((int32) ((i64) >> 32)) +#define INT64_LO_UINT32(i64) ((uint32) (i64)) /* * Add the 128-bit product of two int64 values into an INT128 variable. @@ -176,7 +202,14 @@ int128_add_int64(INT128 *i128, int64 v) static inline void int128_add_int64_mul_int64(INT128 *i128, int64 x, int64 y) { - /* INT64_AU32 must use arithmetic right shift */ +#if USE_NATIVE_INT128 + /* + * XXX with a stupid compiler, this could actually be less efficient than + * the non-native implementation; maybe we should do it by hand always? + */ + *i128 += (int128) x * (int128) y; +#else + /* INT64_HI_INT32 must use arithmetic right shift */ StaticAssertDecl(((int64) -1 >> 1) == (int64) -1, "arithmetic right shift is needed"); @@ -201,34 +234,188 @@ int128_add_int64_mul_int64(INT128 *i128, int64 x, int64 y) /* No need to work hard if product must be zero */ if (x != 0 && y != 0) { - int64 x_u32 = INT64_AU32(x); - uint64 x_l32 = INT64_AL32(x); - int64 y_u32 = INT64_AU32(y); - uint64 y_l32 = INT64_AL32(y); + int32 x_hi = INT64_HI_INT32(x); + uint32 x_lo = INT64_LO_UINT32(x); + int32 y_hi = INT64_HI_INT32(y); + uint32 y_lo = INT64_LO_UINT32(y); int64 tmp; /* the first term */ - i128->hi += x_u32 * y_u32; - - /* the second term: sign-extend it only if x is negative */ - tmp = x_u32 * y_l32; - if (x < 0) - i128->hi += INT64_AU32(tmp); - else - i128->hi += ((uint64) tmp) >> 32; - int128_add_uint64(i128, ((uint64) INT64_AL32(tmp)) << 32); - - /* the third term: sign-extend it only if y is negative */ - tmp = x_l32 * y_u32; - if (y < 0) - i128->hi += INT64_AU32(tmp); - else - i128->hi += ((uint64) tmp) >> 32; - int128_add_uint64(i128, ((uint64) INT64_AL32(tmp)) << 32); + i128->hi += (int64) x_hi * (int64) y_hi; + + /* the second term: sign-extended with the sign of x */ + tmp = (int64) x_hi * (int64) y_lo; + i128->hi += INT64_HI_INT32(tmp); + int128_add_uint64(i128, ((uint64) INT64_LO_UINT32(tmp)) << 32); + + /* the third term: sign-extended with the sign of y */ + tmp = (int64) x_lo * (int64) y_hi; + i128->hi += INT64_HI_INT32(tmp); + int128_add_uint64(i128, ((uint64) INT64_LO_UINT32(tmp)) << 32); /* the fourth term: always unsigned */ - int128_add_uint64(i128, x_l32 * y_l32); + int128_add_uint64(i128, (uint64) x_lo * (uint64) y_lo); } +#endif +} + +/* + * Subtract the 128-bit product of two int64 values from an INT128 variable. + */ +static inline void +int128_sub_int64_mul_int64(INT128 *i128, int64 x, int64 y) +{ +#if USE_NATIVE_INT128 + *i128 -= (int128) x * (int128) y; +#else + /* As above, except subtract the 128-bit product */ + if (x != 0 && y != 0) + { + int32 x_hi = INT64_HI_INT32(x); + uint32 x_lo = INT64_LO_UINT32(x); + int32 y_hi = INT64_HI_INT32(y); + uint32 y_lo = INT64_LO_UINT32(y); + int64 tmp; + + /* the first term */ + i128->hi -= (int64) x_hi * (int64) y_hi; + + /* the second term: sign-extended with the sign of x */ + tmp = (int64) x_hi * (int64) y_lo; + i128->hi -= INT64_HI_INT32(tmp); + int128_sub_uint64(i128, ((uint64) INT64_LO_UINT32(tmp)) << 32); + + /* the third term: sign-extended with the sign of y */ + tmp = (int64) x_lo * (int64) y_hi; + i128->hi -= INT64_HI_INT32(tmp); + int128_sub_uint64(i128, ((uint64) INT64_LO_UINT32(tmp)) << 32); + + /* the fourth term: always unsigned */ + int128_sub_uint64(i128, (uint64) x_lo * (uint64) y_lo); + } +#endif +} + +/* + * Divide an INT128 variable by a signed int32 value, returning the quotient + * and remainder. The remainder will have the same sign as *i128. + * + * Note: This provides no protection against dividing by 0, or dividing + * INT128_MIN by -1, which overflows. It is the caller's responsibility to + * guard against those. + */ +static inline void +int128_div_mod_int32(INT128 *i128, int32 v, int32 *remainder) +{ +#if USE_NATIVE_INT128 + int128 old_i128 = *i128; + + *i128 /= v; + *remainder = (int32) (old_i128 - *i128 * v); +#else + /* + * To avoid any intermediate values overflowing (as happens if INT64_MIN + * is divided by -1), we first compute the quotient abs(*i128) / abs(v) + * using unsigned 64-bit arithmetic, and then fix the signs up at the end. + * + * The quotient is computed using the short division algorithm described + * in Knuth volume 2, section 4.3.1 exercise 16 (cf. div_var_int() in + * numeric.c). Since the absolute value of the divisor is known to be at + * most 2^31, the remainder carried from one digit to the next is at most + * 2^31 - 1, and so there is no danger of overflow when this is combined + * with the next digit (a 32-bit unsigned integer). + */ + uint64 n_hi; + uint64 n_lo; + uint32 d; + uint64 q; + uint64 r; + uint64 tmp; + + /* numerator: absolute value of *i128 */ + if (i128->hi < 0) + { + n_hi = 0 - ((uint64) i128->hi); + n_lo = 0 - i128->lo; + if (n_lo != 0) + n_hi--; + } + else + { + n_hi = i128->hi; + n_lo = i128->lo; + } + + /* denominator: absolute value of v */ + d = abs(v); + + /* quotient and remainder of high 64 bits */ + q = n_hi / d; + r = n_hi % d; + n_hi = q; + + /* quotient and remainder of next 32 bits (upper half of n_lo) */ + tmp = (r << 32) + (n_lo >> 32); + q = tmp / d; + r = tmp % d; + + /* quotient and remainder of last 32 bits (lower half of n_lo) */ + tmp = (r << 32) + (uint32) n_lo; + n_lo = q << 32; + q = tmp / d; + r = tmp % d; + n_lo += q; + + /* final remainder should have the same sign as *i128 */ + *remainder = i128->hi < 0 ? (int32) (0 - r) : (int32) r; + + /* store the quotient in *i128, negating it if necessary */ + if ((i128->hi < 0) != (v < 0)) + { + n_hi = 0 - n_hi; + n_lo = 0 - n_lo; + if (n_lo != 0) + n_hi--; + } + i128->hi = (int64) n_hi; + i128->lo = n_lo; +#endif +} + +/* + * Test if an INT128 value is zero. + */ +static inline bool +int128_is_zero(INT128 x) +{ +#if USE_NATIVE_INT128 + return x == 0; +#else + return x.hi == 0 && x.lo == 0; +#endif +} + +/* + * Return the sign of an INT128 value (returns -1, 0, or +1). + */ +static inline int +int128_sign(INT128 x) +{ +#if USE_NATIVE_INT128 + if (x < 0) + return -1; + if (x > 0) + return 1; + return 0; +#else + if (x.hi < 0) + return -1; + if (x.hi > 0) + return 1; + if (x.lo > 0) + return 1; + return 0; +#endif } /* @@ -237,6 +424,13 @@ int128_add_int64_mul_int64(INT128 *i128, int64 x, int64 y) static inline int int128_compare(INT128 x, INT128 y) { +#if USE_NATIVE_INT128 + if (x < y) + return -1; + if (x > y) + return 1; + return 0; +#else if (x.hi < y.hi) return -1; if (x.hi > y.hi) @@ -246,6 +440,7 @@ int128_compare(INT128 x, INT128 y) if (x.lo > y.lo) return 1; return 0; +#endif } /* @@ -254,11 +449,15 @@ int128_compare(INT128 x, INT128 y) static inline INT128 int64_to_int128(int64 v) { +#if USE_NATIVE_INT128 + return (INT128) v; +#else INT128 val; val.lo = (uint64) v; val.hi = (v < 0) ? -INT64CONST(1) : INT64CONST(0); return val; +#endif } /* @@ -268,9 +467,11 @@ int64_to_int128(int64 v) static inline int64 int128_to_int64(INT128 val) { +#if USE_NATIVE_INT128 + return (int64) val; +#else return (int64) val.lo; +#endif } -#endif /* USE_NATIVE_INT128 */ - #endif /* INT128_H */ diff --git a/src/include/common/ip.h b/src/include/common/ip.h index 8eeae215435be..12444b79e2805 100644 --- a/src/include/common/ip.h +++ b/src/include/common/ip.h @@ -5,7 +5,7 @@ * * These definitions are used by both frontend and backend code. * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/common/ip.h * diff --git a/src/include/common/jsonapi.h b/src/include/common/jsonapi.h index 5d1a3ef3833bd..85cc9a11d9744 100644 --- a/src/include/common/jsonapi.h +++ b/src/include/common/jsonapi.h @@ -3,7 +3,7 @@ * jsonapi.h * Declarations for JSON API support. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/jsonapi.h @@ -108,7 +108,7 @@ typedef struct JsonLexContext bool incremental; JsonTokenType token_type; int lex_level; - bits32 flags; + uint32 flags; int line_number; /* line number, starting from 1 */ const char *line_start; /* where that line starts within input */ JsonParserStack *pstack; diff --git a/src/include/common/keywords.h b/src/include/common/keywords.h index 8a35cc8395633..1572df5296bd8 100644 --- a/src/include/common/keywords.h +++ b/src/include/common/keywords.h @@ -4,7 +4,7 @@ * PostgreSQL's list of SQL keywords * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/keywords.h diff --git a/src/include/common/kwlookup.h b/src/include/common/kwlookup.h index bcc2a018bb454..c710011815357 100644 --- a/src/include/common/kwlookup.h +++ b/src/include/common/kwlookup.h @@ -4,7 +4,7 @@ * Key word lookup for PostgreSQL * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/kwlookup.h diff --git a/src/include/common/link-canary.h b/src/include/common/link-canary.h index 699d3a0baa17f..919fc6fa3e843 100644 --- a/src/include/common/link-canary.h +++ b/src/include/common/link-canary.h @@ -3,7 +3,7 @@ * link-canary.h * Detect whether src/common functions came from frontend or backend. * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * src/include/common/link-canary.h * diff --git a/src/include/common/logging.h b/src/include/common/logging.h index 81529ee8f2926..06c202dbe2de3 100644 --- a/src/include/common/logging.h +++ b/src/include/common/logging.h @@ -1,7 +1,7 @@ /*------------------------------------------------------------------------- * Logging framework for frontend programs * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * src/include/common/logging.h * @@ -91,6 +91,8 @@ void pg_logging_set_level(enum pg_log_level new_level); void pg_logging_increase_verbosity(void); void pg_logging_set_pre_callback(void (*cb) (void)); void pg_logging_set_locus_callback(void (*cb) (const char **filename, uint64 *lineno)); +void pg_logging_set_logfile(FILE *logfile); +void pg_logging_unset_logfile(void); void pg_log_generic(enum pg_log_level level, enum pg_log_part part, const char *pg_restrict fmt,...) diff --git a/src/include/common/md5.h b/src/include/common/md5.h index 0c9ae4888f2d8..66f45a9f700fc 100644 --- a/src/include/common/md5.h +++ b/src/include/common/md5.h @@ -6,7 +6,7 @@ * These definitions are needed by both frontend and backend code to work * with MD5-encrypted passwords. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/md5.h diff --git a/src/include/common/oauth-common.h b/src/include/common/oauth-common.h index 5fb559d84b2bd..1fb312fcec8f5 100644 --- a/src/include/common/oauth-common.h +++ b/src/include/common/oauth-common.h @@ -3,7 +3,7 @@ * oauth-common.h * Declarations for helper functions used for OAuth/OIDC authentication * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/oauth-common.h diff --git a/src/include/common/openssl.h b/src/include/common/openssl.h index eab2c14cc37ea..07e8bfbe4f070 100644 --- a/src/include/common/openssl.h +++ b/src/include/common/openssl.h @@ -3,7 +3,7 @@ * openssl.h * OpenSSL supporting functionality shared between frontend and backend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/common/parse_manifest.h b/src/include/common/parse_manifest.h index 6172d1d52244e..35a3b5ca66c1f 100644 --- a/src/include/common/parse_manifest.h +++ b/src/include/common/parse_manifest.h @@ -3,7 +3,7 @@ * parse_manifest.h * Parse a backup manifest in JSON format. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/parse_manifest.h diff --git a/src/include/common/percentrepl.h b/src/include/common/percentrepl.h index a4786b9224e5b..9c0322dd446e8 100644 --- a/src/include/common/percentrepl.h +++ b/src/include/common/percentrepl.h @@ -3,7 +3,7 @@ * percentrepl.h * Common routines to replace percent placeholders in strings * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/percentrepl.h diff --git a/src/include/common/pg_prng.h b/src/include/common/pg_prng.h index 085c5ad3dfdd5..259848fe9e34e 100644 --- a/src/include/common/pg_prng.h +++ b/src/include/common/pg_prng.h @@ -2,7 +2,7 @@ * * Pseudo-Random Number Generator * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * * src/include/common/pg_prng.h * diff --git a/src/include/common/relpath.h b/src/include/common/relpath.h index 6f2fce216f8c5..9772125be7398 100644 --- a/src/include/common/relpath.h +++ b/src/include/common/relpath.h @@ -3,7 +3,7 @@ * relpath.h * Declarations for GetRelationPath() and friends * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/relpath.h diff --git a/src/include/common/restricted_token.h b/src/include/common/restricted_token.h index 3b8e3d6ca6cc1..67b1880c998d7 100644 --- a/src/include/common/restricted_token.h +++ b/src/include/common/restricted_token.h @@ -2,7 +2,7 @@ * restricted_token.h * helper routine to ensure restricted token on Windows * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/restricted_token.h diff --git a/src/include/common/saslprep.h b/src/include/common/saslprep.h index f4b006dfd9bb4..92a70f887c27e 100644 --- a/src/include/common/saslprep.h +++ b/src/include/common/saslprep.h @@ -5,7 +5,7 @@ * * These definitions are used by both frontend and backend code. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/common/saslprep.h * diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h index 5ce055e0e274d..52545561bd33e 100644 --- a/src/include/common/scram-common.h +++ b/src/include/common/scram-common.h @@ -3,7 +3,7 @@ * scram-common.h * Declarations for helper functions used for SCRAM authentication * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/scram-common.h diff --git a/src/include/common/sha1.h b/src/include/common/sha1.h index 27abde143ba63..c4b24c8cf899f 100644 --- a/src/include/common/sha1.h +++ b/src/include/common/sha1.h @@ -3,7 +3,7 @@ * sha1.h * Constants related to SHA1. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/sha1.h diff --git a/src/include/common/sha2.h b/src/include/common/sha2.h index 5a0fe1b481ee7..5f4d8ab870509 100644 --- a/src/include/common/sha2.h +++ b/src/include/common/sha2.h @@ -3,7 +3,7 @@ * sha2.h * Constants related to SHA224, 256, 384 AND 512. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/common/shortest_dec.h b/src/include/common/shortest_dec.h index 9ca909ef5616f..642665ac0bd3b 100644 --- a/src/include/common/shortest_dec.h +++ b/src/include/common/shortest_dec.h @@ -2,7 +2,7 @@ * * Ryu floating-point output. * - * Portions Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/common/shortest_dec.h diff --git a/src/include/common/string.h b/src/include/common/string.h index ffe5ed51c5db6..2a7c31ea74e66 100644 --- a/src/include/common/string.h +++ b/src/include/common/string.h @@ -2,7 +2,7 @@ * string.h * string handling helpers * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/string.h @@ -12,7 +12,8 @@ #include -struct StringInfoData; /* avoid including stringinfo.h here */ +typedef struct StringInfoData *StringInfo; /* avoid including stringinfo.h + * here */ typedef struct PromptInterruptContext { @@ -32,8 +33,8 @@ extern bool pg_is_ascii(const char *str); /* functions in src/common/pg_get_line.c */ extern char *pg_get_line(FILE *stream, PromptInterruptContext *prompt_ctx); -extern bool pg_get_line_buf(FILE *stream, struct StringInfoData *buf); -extern bool pg_get_line_append(FILE *stream, struct StringInfoData *buf, +extern bool pg_get_line_buf(FILE *stream, StringInfo buf); +extern bool pg_get_line_append(FILE *stream, StringInfo buf, PromptInterruptContext *prompt_ctx); /* functions in src/common/sprompt.c */ diff --git a/src/include/common/unicode_case.h b/src/include/common/unicode_case.h index 41e2c1f4b33f5..2737c1382d4b7 100644 --- a/src/include/common/unicode_case.h +++ b/src/include/common/unicode_case.h @@ -5,7 +5,7 @@ * * These definitions can be used by both frontend and backend code. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/common/unicode_case.h * @@ -14,14 +14,12 @@ #ifndef UNICODE_CASE_H #define UNICODE_CASE_H -#include "mb/pg_wchar.h" - typedef size_t (*WordBoundaryNext) (void *wbstate); -pg_wchar unicode_lowercase_simple(pg_wchar code); -pg_wchar unicode_titlecase_simple(pg_wchar code); -pg_wchar unicode_uppercase_simple(pg_wchar code); -pg_wchar unicode_casefold_simple(pg_wchar code); +char32_t unicode_lowercase_simple(char32_t code); +char32_t unicode_titlecase_simple(char32_t code); +char32_t unicode_uppercase_simple(char32_t code); +char32_t unicode_casefold_simple(char32_t code); size_t unicode_strlower(char *dst, size_t dstsize, const char *src, ssize_t srclen, bool full); size_t unicode_strtitle(char *dst, size_t dstsize, const char *src, diff --git a/src/include/common/unicode_case_table.h b/src/include/common/unicode_case_table.h index d53117865820b..b2274627c4f33 100644 --- a/src/include/common/unicode_case_table.h +++ b/src/include/common/unicode_case_table.h @@ -3,7 +3,7 @@ * unicode_case_table.h * Case mapping and information table. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_case_table.h @@ -18,7 +18,6 @@ */ #include "common/unicode_case.h" -#include "mb/pg_wchar.h" /* * The maximum number of codepoints that can result from case mapping @@ -45,7 +44,7 @@ typedef enum typedef struct { int16 conditions; - pg_wchar map[NCaseKind][MAX_CASE_EXPANSION]; + char32_t map[NCaseKind][MAX_CASE_EXPANSION]; } pg_special_case; /* @@ -166,7 +165,7 @@ static const pg_special_case special_case[106] = * The entry case_map_lower[case_index(codepoint)] is the mapping for the * given codepoint. */ -static const pg_wchar case_map_lower[1704] = +static const char32_t case_map_lower[1732] = { 0x000000, /* reserved */ 0x000000, /* U+000000 */ @@ -1547,7 +1546,10 @@ static const pg_wchar case_map_lower[1704] = 0x00a7c8, /* U+00a7c7 */ 0x00a7ca, /* U+00a7c9 */ 0x00a7cd, /* U+00a7cc */ + 0x00a7cf, /* U+00a7ce */ 0x00a7d1, /* U+00a7d0 */ + 0x00a7d3, /* U+00a7d2 */ + 0x00a7d5, /* U+00a7d4 */ 0x00a7d7, /* U+00a7d6 */ 0x00a7d9, /* U+00a7d8 */ 0x00a7db, /* U+00a7da */ @@ -1838,6 +1840,31 @@ static const pg_wchar case_map_lower[1704] = 0x016e7d, /* U+016e5d */ 0x016e7e, /* U+016e5e */ 0x016e7f, /* U+016e5f */ + 0x016ebb, /* U+016ea0 */ + 0x016ebc, /* U+016ea1 */ + 0x016ebd, /* U+016ea2 */ + 0x016ebe, /* U+016ea3 */ + 0x016ebf, /* U+016ea4 */ + 0x016ec0, /* U+016ea5 */ + 0x016ec1, /* U+016ea6 */ + 0x016ec2, /* U+016ea7 */ + 0x016ec3, /* U+016ea8 */ + 0x016ec4, /* U+016ea9 */ + 0x016ec5, /* U+016eaa */ + 0x016ec6, /* U+016eab */ + 0x016ec7, /* U+016eac */ + 0x016ec8, /* U+016ead */ + 0x016ec9, /* U+016eae */ + 0x016eca, /* U+016eaf */ + 0x016ecb, /* U+016eb0 */ + 0x016ecc, /* U+016eb1 */ + 0x016ecd, /* U+016eb2 */ + 0x016ece, /* U+016eb3 */ + 0x016ecf, /* U+016eb4 */ + 0x016ed0, /* U+016eb5 */ + 0x016ed1, /* U+016eb6 */ + 0x016ed2, /* U+016eb7 */ + 0x016ed3, /* U+016eb8 */ 0x01e922, /* U+01e900 */ 0x01e923, /* U+01e901 */ 0x01e924, /* U+01e902 */ @@ -1879,7 +1906,7 @@ static const pg_wchar case_map_lower[1704] = * The entry case_map_title[case_index(codepoint)] is the mapping for the * given codepoint. */ -static const pg_wchar case_map_title[1704] = +static const char32_t case_map_title[1732] = { 0x000000, /* reserved */ 0x000000, /* U+000000 */ @@ -3260,7 +3287,10 @@ static const pg_wchar case_map_title[1704] = 0x00a7c7, /* U+00a7c7 */ 0x00a7c9, /* U+00a7c9 */ 0x00a7cc, /* U+00a7cc */ + 0x00a7ce, /* U+00a7ce */ 0x00a7d0, /* U+00a7d0 */ + 0x00a7d2, /* U+00a7d2 */ + 0x00a7d4, /* U+00a7d4 */ 0x00a7d6, /* U+00a7d6 */ 0x00a7d8, /* U+00a7d8 */ 0x00a7da, /* U+00a7da */ @@ -3551,6 +3581,31 @@ static const pg_wchar case_map_title[1704] = 0x016e5d, /* U+016e5d */ 0x016e5e, /* U+016e5e */ 0x016e5f, /* U+016e5f */ + 0x016ea0, /* U+016ea0 */ + 0x016ea1, /* U+016ea1 */ + 0x016ea2, /* U+016ea2 */ + 0x016ea3, /* U+016ea3 */ + 0x016ea4, /* U+016ea4 */ + 0x016ea5, /* U+016ea5 */ + 0x016ea6, /* U+016ea6 */ + 0x016ea7, /* U+016ea7 */ + 0x016ea8, /* U+016ea8 */ + 0x016ea9, /* U+016ea9 */ + 0x016eaa, /* U+016eaa */ + 0x016eab, /* U+016eab */ + 0x016eac, /* U+016eac */ + 0x016ead, /* U+016ead */ + 0x016eae, /* U+016eae */ + 0x016eaf, /* U+016eaf */ + 0x016eb0, /* U+016eb0 */ + 0x016eb1, /* U+016eb1 */ + 0x016eb2, /* U+016eb2 */ + 0x016eb3, /* U+016eb3 */ + 0x016eb4, /* U+016eb4 */ + 0x016eb5, /* U+016eb5 */ + 0x016eb6, /* U+016eb6 */ + 0x016eb7, /* U+016eb7 */ + 0x016eb8, /* U+016eb8 */ 0x01e900, /* U+01e900 */ 0x01e901, /* U+01e901 */ 0x01e902, /* U+01e902 */ @@ -3592,7 +3647,7 @@ static const pg_wchar case_map_title[1704] = * The entry case_map_upper[case_index(codepoint)] is the mapping for the * given codepoint. */ -static const pg_wchar case_map_upper[1704] = +static const char32_t case_map_upper[1732] = { 0x000000, /* reserved */ 0x000000, /* U+000000 */ @@ -4973,7 +5028,10 @@ static const pg_wchar case_map_upper[1704] = 0x00a7c7, /* U+00a7c7 */ 0x00a7c9, /* U+00a7c9 */ 0x00a7cc, /* U+00a7cc */ + 0x00a7ce, /* U+00a7ce */ 0x00a7d0, /* U+00a7d0 */ + 0x00a7d2, /* U+00a7d2 */ + 0x00a7d4, /* U+00a7d4 */ 0x00a7d6, /* U+00a7d6 */ 0x00a7d8, /* U+00a7d8 */ 0x00a7da, /* U+00a7da */ @@ -5264,6 +5322,31 @@ static const pg_wchar case_map_upper[1704] = 0x016e5d, /* U+016e5d */ 0x016e5e, /* U+016e5e */ 0x016e5f, /* U+016e5f */ + 0x016ea0, /* U+016ea0 */ + 0x016ea1, /* U+016ea1 */ + 0x016ea2, /* U+016ea2 */ + 0x016ea3, /* U+016ea3 */ + 0x016ea4, /* U+016ea4 */ + 0x016ea5, /* U+016ea5 */ + 0x016ea6, /* U+016ea6 */ + 0x016ea7, /* U+016ea7 */ + 0x016ea8, /* U+016ea8 */ + 0x016ea9, /* U+016ea9 */ + 0x016eaa, /* U+016eaa */ + 0x016eab, /* U+016eab */ + 0x016eac, /* U+016eac */ + 0x016ead, /* U+016ead */ + 0x016eae, /* U+016eae */ + 0x016eaf, /* U+016eaf */ + 0x016eb0, /* U+016eb0 */ + 0x016eb1, /* U+016eb1 */ + 0x016eb2, /* U+016eb2 */ + 0x016eb3, /* U+016eb3 */ + 0x016eb4, /* U+016eb4 */ + 0x016eb5, /* U+016eb5 */ + 0x016eb6, /* U+016eb6 */ + 0x016eb7, /* U+016eb7 */ + 0x016eb8, /* U+016eb8 */ 0x01e900, /* U+01e900 */ 0x01e901, /* U+01e901 */ 0x01e902, /* U+01e902 */ @@ -5305,7 +5388,7 @@ static const pg_wchar case_map_upper[1704] = * The entry case_map_fold[case_index(codepoint)] is the mapping for the * given codepoint. */ -static const pg_wchar case_map_fold[1704] = +static const char32_t case_map_fold[1732] = { 0x000000, /* reserved */ 0x000000, /* U+000000 */ @@ -6686,7 +6769,10 @@ static const pg_wchar case_map_fold[1704] = 0x00a7c8, /* U+00a7c7 */ 0x00a7ca, /* U+00a7c9 */ 0x00a7cd, /* U+00a7cc */ + 0x00a7cf, /* U+00a7ce */ 0x00a7d1, /* U+00a7d0 */ + 0x00a7d3, /* U+00a7d2 */ + 0x00a7d5, /* U+00a7d4 */ 0x00a7d7, /* U+00a7d6 */ 0x00a7d9, /* U+00a7d8 */ 0x00a7db, /* U+00a7da */ @@ -6977,6 +7063,31 @@ static const pg_wchar case_map_fold[1704] = 0x016e7d, /* U+016e5d */ 0x016e7e, /* U+016e5e */ 0x016e7f, /* U+016e5f */ + 0x016ebb, /* U+016ea0 */ + 0x016ebc, /* U+016ea1 */ + 0x016ebd, /* U+016ea2 */ + 0x016ebe, /* U+016ea3 */ + 0x016ebf, /* U+016ea4 */ + 0x016ec0, /* U+016ea5 */ + 0x016ec1, /* U+016ea6 */ + 0x016ec2, /* U+016ea7 */ + 0x016ec3, /* U+016ea8 */ + 0x016ec4, /* U+016ea9 */ + 0x016ec5, /* U+016eaa */ + 0x016ec6, /* U+016eab */ + 0x016ec7, /* U+016eac */ + 0x016ec8, /* U+016ead */ + 0x016ec9, /* U+016eae */ + 0x016eca, /* U+016eaf */ + 0x016ecb, /* U+016eb0 */ + 0x016ecc, /* U+016eb1 */ + 0x016ecd, /* U+016eb2 */ + 0x016ece, /* U+016eb3 */ + 0x016ecf, /* U+016eb4 */ + 0x016ed0, /* U+016eb5 */ + 0x016ed1, /* U+016eb6 */ + 0x016ed2, /* U+016eb7 */ + 0x016ed3, /* U+016eb8 */ 0x01e922, /* U+01e900 */ 0x01e923, /* U+01e901 */ 0x01e924, /* U+01e902 */ @@ -7018,7 +7129,7 @@ static const pg_wchar case_map_fold[1704] = * The entry case_map_special[case_index(codepoint)] is the index in * special_case for that codepoint, or 0 if no special case mapping exists. */ -static const uint8 case_map_special[1704] = +static const uint8 case_map_special[1732] = { 0, /* reserved */ 0, /* U+000000 */ @@ -8399,7 +8510,10 @@ static const uint8 case_map_special[1704] = 0, /* U+00a7c7 */ 0, /* U+00a7c9 */ 0, /* U+00a7cc */ + 0, /* U+00a7ce */ 0, /* U+00a7d0 */ + 0, /* U+00a7d2 */ + 0, /* U+00a7d4 */ 0, /* U+00a7d6 */ 0, /* U+00a7d8 */ 0, /* U+00a7da */ @@ -8690,6 +8804,31 @@ static const uint8 case_map_special[1704] = 0, /* U+016e5d */ 0, /* U+016e5e */ 0, /* U+016e5f */ + 0, /* U+016ea0 */ + 0, /* U+016ea1 */ + 0, /* U+016ea2 */ + 0, /* U+016ea3 */ + 0, /* U+016ea4 */ + 0, /* U+016ea5 */ + 0, /* U+016ea6 */ + 0, /* U+016ea7 */ + 0, /* U+016ea8 */ + 0, /* U+016ea9 */ + 0, /* U+016eaa */ + 0, /* U+016eab */ + 0, /* U+016eac */ + 0, /* U+016ead */ + 0, /* U+016eae */ + 0, /* U+016eaf */ + 0, /* U+016eb0 */ + 0, /* U+016eb1 */ + 0, /* U+016eb2 */ + 0, /* U+016eb3 */ + 0, /* U+016eb4 */ + 0, /* U+016eb5 */ + 0, /* U+016eb6 */ + 0, /* U+016eb7 */ + 0, /* U+016eb8 */ 0, /* U+01e900 */ 0, /* U+01e901 */ 0, /* U+01e902 */ @@ -8732,7 +8871,7 @@ static const uint8 case_map_special[1704] = * of the following arrays: case_map_lower, case_map_title, case_map_upper, * case_map_fold. */ -static const uint16 case_map[4778] = +static const uint16 case_map[4862] = { 1, /* U+000000 */ 2, /* U+000001 */ @@ -12377,20 +12516,20 @@ static const uint16 case_map[4778] = 339, /* U+00A7CB */ 1378, /* U+00A7CC */ 1378, /* U+00A7CD */ - 0, /* U+00A7CE */ - 0, /* U+00A7CF */ - 1379, /* U+00A7D0 */ - 1379, /* U+00A7D1 */ - 0, /* U+00A7D2 */ - 0, /* U+00A7D3 */ - 0, /* U+00A7D4 */ - 0, /* U+00A7D5 */ - 1380, /* U+00A7D6 */ - 1380, /* U+00A7D7 */ - 1381, /* U+00A7D8 */ - 1381, /* U+00A7D9 */ - 1382, /* U+00A7DA */ - 1382, /* U+00A7DB */ + 1379, /* U+00A7CE */ + 1379, /* U+00A7CF */ + 1380, /* U+00A7D0 */ + 1380, /* U+00A7D1 */ + 1381, /* U+00A7D2 */ + 1381, /* U+00A7D3 */ + 1382, /* U+00A7D4 */ + 1382, /* U+00A7D5 */ + 1383, /* U+00A7D6 */ + 1383, /* U+00A7D7 */ + 1384, /* U+00A7D8 */ + 1384, /* U+00A7D9 */ + 1385, /* U+00A7DA */ + 1385, /* U+00A7DB */ 247, /* U+00A7DC */ 0, /* U+00A7DD */ 0, /* U+00A7DE */ @@ -12416,8 +12555,8 @@ static const uint16 case_map[4778] = 0, /* U+00A7F2 */ 0, /* U+00A7F3 */ 0, /* U+00A7F4 */ - 1383, /* U+00A7F5 */ - 1383, /* U+00A7F6 */ + 1386, /* U+00A7F5 */ + 1386, /* U+00A7F6 */ 1367, /* U+00AB53 */ 0, /* U+00AB54 */ 0, /* U+00AB55 */ @@ -12527,13 +12666,13 @@ static const uint16 case_map[4778] = 770, /* U+00ABBD */ 771, /* U+00ABBE */ 772, /* U+00ABBF */ - 1384, /* U+00FB00 */ - 1385, /* U+00FB01 */ - 1386, /* U+00FB02 */ - 1387, /* U+00FB03 */ - 1388, /* U+00FB04 */ - 1389, /* U+00FB05 */ - 1390, /* U+00FB06 */ + 1387, /* U+00FB00 */ + 1388, /* U+00FB01 */ + 1389, /* U+00FB02 */ + 1390, /* U+00FB03 */ + 1391, /* U+00FB04 */ + 1392, /* U+00FB05 */ + 1393, /* U+00FB06 */ 0, /* U+00FB07 */ 0, /* U+00FB08 */ 0, /* U+00FB09 */ @@ -12546,149 +12685,149 @@ static const uint16 case_map[4778] = 0, /* U+00FB10 */ 0, /* U+00FB11 */ 0, /* U+00FB12 */ - 1391, /* U+00FB13 */ - 1392, /* U+00FB14 */ - 1393, /* U+00FB15 */ - 1394, /* U+00FB16 */ - 1395, /* U+00FB17 */ - 1396, /* U+00FF21 */ - 1397, /* U+00FF22 */ - 1398, /* U+00FF23 */ - 1399, /* U+00FF24 */ - 1400, /* U+00FF25 */ - 1401, /* U+00FF26 */ - 1402, /* U+00FF27 */ - 1403, /* U+00FF28 */ - 1404, /* U+00FF29 */ - 1405, /* U+00FF2A */ - 1406, /* U+00FF2B */ - 1407, /* U+00FF2C */ - 1408, /* U+00FF2D */ - 1409, /* U+00FF2E */ - 1410, /* U+00FF2F */ - 1411, /* U+00FF30 */ - 1412, /* U+00FF31 */ - 1413, /* U+00FF32 */ - 1414, /* U+00FF33 */ - 1415, /* U+00FF34 */ - 1416, /* U+00FF35 */ - 1417, /* U+00FF36 */ - 1418, /* U+00FF37 */ - 1419, /* U+00FF38 */ - 1420, /* U+00FF39 */ - 1421, /* U+00FF3A */ + 1394, /* U+00FB13 */ + 1395, /* U+00FB14 */ + 1396, /* U+00FB15 */ + 1397, /* U+00FB16 */ + 1398, /* U+00FB17 */ + 1399, /* U+00FF21 */ + 1400, /* U+00FF22 */ + 1401, /* U+00FF23 */ + 1402, /* U+00FF24 */ + 1403, /* U+00FF25 */ + 1404, /* U+00FF26 */ + 1405, /* U+00FF27 */ + 1406, /* U+00FF28 */ + 1407, /* U+00FF29 */ + 1408, /* U+00FF2A */ + 1409, /* U+00FF2B */ + 1410, /* U+00FF2C */ + 1411, /* U+00FF2D */ + 1412, /* U+00FF2E */ + 1413, /* U+00FF2F */ + 1414, /* U+00FF30 */ + 1415, /* U+00FF31 */ + 1416, /* U+00FF32 */ + 1417, /* U+00FF33 */ + 1418, /* U+00FF34 */ + 1419, /* U+00FF35 */ + 1420, /* U+00FF36 */ + 1421, /* U+00FF37 */ + 1422, /* U+00FF38 */ + 1423, /* U+00FF39 */ + 1424, /* U+00FF3A */ 0, /* U+00FF3B */ 0, /* U+00FF3C */ 0, /* U+00FF3D */ 0, /* U+00FF3E */ 0, /* U+00FF3F */ 0, /* U+00FF40 */ - 1396, /* U+00FF41 */ - 1397, /* U+00FF42 */ - 1398, /* U+00FF43 */ - 1399, /* U+00FF44 */ - 1400, /* U+00FF45 */ - 1401, /* U+00FF46 */ - 1402, /* U+00FF47 */ - 1403, /* U+00FF48 */ - 1404, /* U+00FF49 */ - 1405, /* U+00FF4A */ - 1406, /* U+00FF4B */ - 1407, /* U+00FF4C */ - 1408, /* U+00FF4D */ - 1409, /* U+00FF4E */ - 1410, /* U+00FF4F */ - 1411, /* U+00FF50 */ - 1412, /* U+00FF51 */ - 1413, /* U+00FF52 */ - 1414, /* U+00FF53 */ - 1415, /* U+00FF54 */ - 1416, /* U+00FF55 */ - 1417, /* U+00FF56 */ - 1418, /* U+00FF57 */ - 1419, /* U+00FF58 */ - 1420, /* U+00FF59 */ - 1421, /* U+00FF5A */ - 1422, /* U+010400 */ - 1423, /* U+010401 */ - 1424, /* U+010402 */ - 1425, /* U+010403 */ - 1426, /* U+010404 */ - 1427, /* U+010405 */ - 1428, /* U+010406 */ - 1429, /* U+010407 */ - 1430, /* U+010408 */ - 1431, /* U+010409 */ - 1432, /* U+01040A */ - 1433, /* U+01040B */ - 1434, /* U+01040C */ - 1435, /* U+01040D */ - 1436, /* U+01040E */ - 1437, /* U+01040F */ - 1438, /* U+010410 */ - 1439, /* U+010411 */ - 1440, /* U+010412 */ - 1441, /* U+010413 */ - 1442, /* U+010414 */ - 1443, /* U+010415 */ - 1444, /* U+010416 */ - 1445, /* U+010417 */ - 1446, /* U+010418 */ - 1447, /* U+010419 */ - 1448, /* U+01041A */ - 1449, /* U+01041B */ - 1450, /* U+01041C */ - 1451, /* U+01041D */ - 1452, /* U+01041E */ - 1453, /* U+01041F */ - 1454, /* U+010420 */ - 1455, /* U+010421 */ - 1456, /* U+010422 */ - 1457, /* U+010423 */ - 1458, /* U+010424 */ - 1459, /* U+010425 */ - 1460, /* U+010426 */ - 1461, /* U+010427 */ - 1422, /* U+010428 */ - 1423, /* U+010429 */ - 1424, /* U+01042A */ - 1425, /* U+01042B */ - 1426, /* U+01042C */ - 1427, /* U+01042D */ - 1428, /* U+01042E */ - 1429, /* U+01042F */ - 1430, /* U+010430 */ - 1431, /* U+010431 */ - 1432, /* U+010432 */ - 1433, /* U+010433 */ - 1434, /* U+010434 */ - 1435, /* U+010435 */ - 1436, /* U+010436 */ - 1437, /* U+010437 */ - 1438, /* U+010438 */ - 1439, /* U+010439 */ - 1440, /* U+01043A */ - 1441, /* U+01043B */ - 1442, /* U+01043C */ - 1443, /* U+01043D */ - 1444, /* U+01043E */ - 1445, /* U+01043F */ - 1446, /* U+010440 */ - 1447, /* U+010441 */ - 1448, /* U+010442 */ - 1449, /* U+010443 */ - 1450, /* U+010444 */ - 1451, /* U+010445 */ - 1452, /* U+010446 */ - 1453, /* U+010447 */ - 1454, /* U+010448 */ - 1455, /* U+010449 */ - 1456, /* U+01044A */ - 1457, /* U+01044B */ - 1458, /* U+01044C */ - 1459, /* U+01044D */ - 1460, /* U+01044E */ - 1461, /* U+01044F */ + 1399, /* U+00FF41 */ + 1400, /* U+00FF42 */ + 1401, /* U+00FF43 */ + 1402, /* U+00FF44 */ + 1403, /* U+00FF45 */ + 1404, /* U+00FF46 */ + 1405, /* U+00FF47 */ + 1406, /* U+00FF48 */ + 1407, /* U+00FF49 */ + 1408, /* U+00FF4A */ + 1409, /* U+00FF4B */ + 1410, /* U+00FF4C */ + 1411, /* U+00FF4D */ + 1412, /* U+00FF4E */ + 1413, /* U+00FF4F */ + 1414, /* U+00FF50 */ + 1415, /* U+00FF51 */ + 1416, /* U+00FF52 */ + 1417, /* U+00FF53 */ + 1418, /* U+00FF54 */ + 1419, /* U+00FF55 */ + 1420, /* U+00FF56 */ + 1421, /* U+00FF57 */ + 1422, /* U+00FF58 */ + 1423, /* U+00FF59 */ + 1424, /* U+00FF5A */ + 1425, /* U+010400 */ + 1426, /* U+010401 */ + 1427, /* U+010402 */ + 1428, /* U+010403 */ + 1429, /* U+010404 */ + 1430, /* U+010405 */ + 1431, /* U+010406 */ + 1432, /* U+010407 */ + 1433, /* U+010408 */ + 1434, /* U+010409 */ + 1435, /* U+01040A */ + 1436, /* U+01040B */ + 1437, /* U+01040C */ + 1438, /* U+01040D */ + 1439, /* U+01040E */ + 1440, /* U+01040F */ + 1441, /* U+010410 */ + 1442, /* U+010411 */ + 1443, /* U+010412 */ + 1444, /* U+010413 */ + 1445, /* U+010414 */ + 1446, /* U+010415 */ + 1447, /* U+010416 */ + 1448, /* U+010417 */ + 1449, /* U+010418 */ + 1450, /* U+010419 */ + 1451, /* U+01041A */ + 1452, /* U+01041B */ + 1453, /* U+01041C */ + 1454, /* U+01041D */ + 1455, /* U+01041E */ + 1456, /* U+01041F */ + 1457, /* U+010420 */ + 1458, /* U+010421 */ + 1459, /* U+010422 */ + 1460, /* U+010423 */ + 1461, /* U+010424 */ + 1462, /* U+010425 */ + 1463, /* U+010426 */ + 1464, /* U+010427 */ + 1425, /* U+010428 */ + 1426, /* U+010429 */ + 1427, /* U+01042A */ + 1428, /* U+01042B */ + 1429, /* U+01042C */ + 1430, /* U+01042D */ + 1431, /* U+01042E */ + 1432, /* U+01042F */ + 1433, /* U+010430 */ + 1434, /* U+010431 */ + 1435, /* U+010432 */ + 1436, /* U+010433 */ + 1437, /* U+010434 */ + 1438, /* U+010435 */ + 1439, /* U+010436 */ + 1440, /* U+010437 */ + 1441, /* U+010438 */ + 1442, /* U+010439 */ + 1443, /* U+01043A */ + 1444, /* U+01043B */ + 1445, /* U+01043C */ + 1446, /* U+01043D */ + 1447, /* U+01043E */ + 1448, /* U+01043F */ + 1449, /* U+010440 */ + 1450, /* U+010441 */ + 1451, /* U+010442 */ + 1452, /* U+010443 */ + 1453, /* U+010444 */ + 1454, /* U+010445 */ + 1455, /* U+010446 */ + 1456, /* U+010447 */ + 1457, /* U+010448 */ + 1458, /* U+010449 */ + 1459, /* U+01044A */ + 1460, /* U+01044B */ + 1461, /* U+01044C */ + 1462, /* U+01044D */ + 1463, /* U+01044E */ + 1464, /* U+01044F */ 0, /* U+010450 */ 0, /* U+010451 */ 0, /* U+010452 */ @@ -12785,82 +12924,82 @@ static const uint16 case_map[4778] = 0, /* U+0104AD */ 0, /* U+0104AE */ 0, /* U+0104AF */ - 1462, /* U+0104B0 */ - 1463, /* U+0104B1 */ - 1464, /* U+0104B2 */ - 1465, /* U+0104B3 */ - 1466, /* U+0104B4 */ - 1467, /* U+0104B5 */ - 1468, /* U+0104B6 */ - 1469, /* U+0104B7 */ - 1470, /* U+0104B8 */ - 1471, /* U+0104B9 */ - 1472, /* U+0104BA */ - 1473, /* U+0104BB */ - 1474, /* U+0104BC */ - 1475, /* U+0104BD */ - 1476, /* U+0104BE */ - 1477, /* U+0104BF */ - 1478, /* U+0104C0 */ - 1479, /* U+0104C1 */ - 1480, /* U+0104C2 */ - 1481, /* U+0104C3 */ - 1482, /* U+0104C4 */ - 1483, /* U+0104C5 */ - 1484, /* U+0104C6 */ - 1485, /* U+0104C7 */ - 1486, /* U+0104C8 */ - 1487, /* U+0104C9 */ - 1488, /* U+0104CA */ - 1489, /* U+0104CB */ - 1490, /* U+0104CC */ - 1491, /* U+0104CD */ - 1492, /* U+0104CE */ - 1493, /* U+0104CF */ - 1494, /* U+0104D0 */ - 1495, /* U+0104D1 */ - 1496, /* U+0104D2 */ - 1497, /* U+0104D3 */ + 1465, /* U+0104B0 */ + 1466, /* U+0104B1 */ + 1467, /* U+0104B2 */ + 1468, /* U+0104B3 */ + 1469, /* U+0104B4 */ + 1470, /* U+0104B5 */ + 1471, /* U+0104B6 */ + 1472, /* U+0104B7 */ + 1473, /* U+0104B8 */ + 1474, /* U+0104B9 */ + 1475, /* U+0104BA */ + 1476, /* U+0104BB */ + 1477, /* U+0104BC */ + 1478, /* U+0104BD */ + 1479, /* U+0104BE */ + 1480, /* U+0104BF */ + 1481, /* U+0104C0 */ + 1482, /* U+0104C1 */ + 1483, /* U+0104C2 */ + 1484, /* U+0104C3 */ + 1485, /* U+0104C4 */ + 1486, /* U+0104C5 */ + 1487, /* U+0104C6 */ + 1488, /* U+0104C7 */ + 1489, /* U+0104C8 */ + 1490, /* U+0104C9 */ + 1491, /* U+0104CA */ + 1492, /* U+0104CB */ + 1493, /* U+0104CC */ + 1494, /* U+0104CD */ + 1495, /* U+0104CE */ + 1496, /* U+0104CF */ + 1497, /* U+0104D0 */ + 1498, /* U+0104D1 */ + 1499, /* U+0104D2 */ + 1500, /* U+0104D3 */ 0, /* U+0104D4 */ 0, /* U+0104D5 */ 0, /* U+0104D6 */ 0, /* U+0104D7 */ - 1462, /* U+0104D8 */ - 1463, /* U+0104D9 */ - 1464, /* U+0104DA */ - 1465, /* U+0104DB */ - 1466, /* U+0104DC */ - 1467, /* U+0104DD */ - 1468, /* U+0104DE */ - 1469, /* U+0104DF */ - 1470, /* U+0104E0 */ - 1471, /* U+0104E1 */ - 1472, /* U+0104E2 */ - 1473, /* U+0104E3 */ - 1474, /* U+0104E4 */ - 1475, /* U+0104E5 */ - 1476, /* U+0104E6 */ - 1477, /* U+0104E7 */ - 1478, /* U+0104E8 */ - 1479, /* U+0104E9 */ - 1480, /* U+0104EA */ - 1481, /* U+0104EB */ - 1482, /* U+0104EC */ - 1483, /* U+0104ED */ - 1484, /* U+0104EE */ - 1485, /* U+0104EF */ - 1486, /* U+0104F0 */ - 1487, /* U+0104F1 */ - 1488, /* U+0104F2 */ - 1489, /* U+0104F3 */ - 1490, /* U+0104F4 */ - 1491, /* U+0104F5 */ - 1492, /* U+0104F6 */ - 1493, /* U+0104F7 */ - 1494, /* U+0104F8 */ - 1495, /* U+0104F9 */ - 1496, /* U+0104FA */ - 1497, /* U+0104FB */ + 1465, /* U+0104D8 */ + 1466, /* U+0104D9 */ + 1467, /* U+0104DA */ + 1468, /* U+0104DB */ + 1469, /* U+0104DC */ + 1470, /* U+0104DD */ + 1471, /* U+0104DE */ + 1472, /* U+0104DF */ + 1473, /* U+0104E0 */ + 1474, /* U+0104E1 */ + 1475, /* U+0104E2 */ + 1476, /* U+0104E3 */ + 1477, /* U+0104E4 */ + 1478, /* U+0104E5 */ + 1479, /* U+0104E6 */ + 1480, /* U+0104E7 */ + 1481, /* U+0104E8 */ + 1482, /* U+0104E9 */ + 1483, /* U+0104EA */ + 1484, /* U+0104EB */ + 1485, /* U+0104EC */ + 1486, /* U+0104ED */ + 1487, /* U+0104EE */ + 1488, /* U+0104EF */ + 1489, /* U+0104F0 */ + 1490, /* U+0104F1 */ + 1491, /* U+0104F2 */ + 1492, /* U+0104F3 */ + 1493, /* U+0104F4 */ + 1494, /* U+0104F5 */ + 1495, /* U+0104F6 */ + 1496, /* U+0104F7 */ + 1497, /* U+0104F8 */ + 1498, /* U+0104F9 */ + 1499, /* U+0104FA */ + 1500, /* U+0104FB */ 0, /* U+0104FC */ 0, /* U+0104FD */ 0, /* U+0104FE */ @@ -12977,134 +13116,134 @@ static const uint16 case_map[4778] = 0, /* U+01056D */ 0, /* U+01056E */ 0, /* U+01056F */ - 1498, /* U+010570 */ - 1499, /* U+010571 */ - 1500, /* U+010572 */ - 1501, /* U+010573 */ - 1502, /* U+010574 */ - 1503, /* U+010575 */ - 1504, /* U+010576 */ - 1505, /* U+010577 */ - 1506, /* U+010578 */ - 1507, /* U+010579 */ - 1508, /* U+01057A */ + 1501, /* U+010570 */ + 1502, /* U+010571 */ + 1503, /* U+010572 */ + 1504, /* U+010573 */ + 1505, /* U+010574 */ + 1506, /* U+010575 */ + 1507, /* U+010576 */ + 1508, /* U+010577 */ + 1509, /* U+010578 */ + 1510, /* U+010579 */ + 1511, /* U+01057A */ 0, /* U+01057B */ - 1509, /* U+01057C */ - 1510, /* U+01057D */ - 1511, /* U+01057E */ - 1512, /* U+01057F */ - 1513, /* U+010580 */ - 1514, /* U+010581 */ - 1515, /* U+010582 */ - 1516, /* U+010583 */ - 1517, /* U+010584 */ - 1518, /* U+010585 */ - 1519, /* U+010586 */ - 1520, /* U+010587 */ - 1521, /* U+010588 */ - 1522, /* U+010589 */ - 1523, /* U+01058A */ + 1512, /* U+01057C */ + 1513, /* U+01057D */ + 1514, /* U+01057E */ + 1515, /* U+01057F */ + 1516, /* U+010580 */ + 1517, /* U+010581 */ + 1518, /* U+010582 */ + 1519, /* U+010583 */ + 1520, /* U+010584 */ + 1521, /* U+010585 */ + 1522, /* U+010586 */ + 1523, /* U+010587 */ + 1524, /* U+010588 */ + 1525, /* U+010589 */ + 1526, /* U+01058A */ 0, /* U+01058B */ - 1524, /* U+01058C */ - 1525, /* U+01058D */ - 1526, /* U+01058E */ - 1527, /* U+01058F */ - 1528, /* U+010590 */ - 1529, /* U+010591 */ - 1530, /* U+010592 */ + 1527, /* U+01058C */ + 1528, /* U+01058D */ + 1529, /* U+01058E */ + 1530, /* U+01058F */ + 1531, /* U+010590 */ + 1532, /* U+010591 */ + 1533, /* U+010592 */ 0, /* U+010593 */ - 1531, /* U+010594 */ - 1532, /* U+010595 */ + 1534, /* U+010594 */ + 1535, /* U+010595 */ 0, /* U+010596 */ - 1498, /* U+010597 */ - 1499, /* U+010598 */ - 1500, /* U+010599 */ - 1501, /* U+01059A */ - 1502, /* U+01059B */ - 1503, /* U+01059C */ - 1504, /* U+01059D */ - 1505, /* U+01059E */ - 1506, /* U+01059F */ - 1507, /* U+0105A0 */ - 1508, /* U+0105A1 */ + 1501, /* U+010597 */ + 1502, /* U+010598 */ + 1503, /* U+010599 */ + 1504, /* U+01059A */ + 1505, /* U+01059B */ + 1506, /* U+01059C */ + 1507, /* U+01059D */ + 1508, /* U+01059E */ + 1509, /* U+01059F */ + 1510, /* U+0105A0 */ + 1511, /* U+0105A1 */ 0, /* U+0105A2 */ - 1509, /* U+0105A3 */ - 1510, /* U+0105A4 */ - 1511, /* U+0105A5 */ - 1512, /* U+0105A6 */ - 1513, /* U+0105A7 */ - 1514, /* U+0105A8 */ - 1515, /* U+0105A9 */ - 1516, /* U+0105AA */ - 1517, /* U+0105AB */ - 1518, /* U+0105AC */ - 1519, /* U+0105AD */ - 1520, /* U+0105AE */ - 1521, /* U+0105AF */ - 1522, /* U+0105B0 */ - 1523, /* U+0105B1 */ + 1512, /* U+0105A3 */ + 1513, /* U+0105A4 */ + 1514, /* U+0105A5 */ + 1515, /* U+0105A6 */ + 1516, /* U+0105A7 */ + 1517, /* U+0105A8 */ + 1518, /* U+0105A9 */ + 1519, /* U+0105AA */ + 1520, /* U+0105AB */ + 1521, /* U+0105AC */ + 1522, /* U+0105AD */ + 1523, /* U+0105AE */ + 1524, /* U+0105AF */ + 1525, /* U+0105B0 */ + 1526, /* U+0105B1 */ 0, /* U+0105B2 */ - 1524, /* U+0105B3 */ - 1525, /* U+0105B4 */ - 1526, /* U+0105B5 */ - 1527, /* U+0105B6 */ - 1528, /* U+0105B7 */ - 1529, /* U+0105B8 */ - 1530, /* U+0105B9 */ + 1527, /* U+0105B3 */ + 1528, /* U+0105B4 */ + 1529, /* U+0105B5 */ + 1530, /* U+0105B6 */ + 1531, /* U+0105B7 */ + 1532, /* U+0105B8 */ + 1533, /* U+0105B9 */ 0, /* U+0105BA */ - 1531, /* U+0105BB */ - 1532, /* U+0105BC */ - 1533, /* U+010C80 */ - 1534, /* U+010C81 */ - 1535, /* U+010C82 */ - 1536, /* U+010C83 */ - 1537, /* U+010C84 */ - 1538, /* U+010C85 */ - 1539, /* U+010C86 */ - 1540, /* U+010C87 */ - 1541, /* U+010C88 */ - 1542, /* U+010C89 */ - 1543, /* U+010C8A */ - 1544, /* U+010C8B */ - 1545, /* U+010C8C */ - 1546, /* U+010C8D */ - 1547, /* U+010C8E */ - 1548, /* U+010C8F */ - 1549, /* U+010C90 */ - 1550, /* U+010C91 */ - 1551, /* U+010C92 */ - 1552, /* U+010C93 */ - 1553, /* U+010C94 */ - 1554, /* U+010C95 */ - 1555, /* U+010C96 */ - 1556, /* U+010C97 */ - 1557, /* U+010C98 */ - 1558, /* U+010C99 */ - 1559, /* U+010C9A */ - 1560, /* U+010C9B */ - 1561, /* U+010C9C */ - 1562, /* U+010C9D */ - 1563, /* U+010C9E */ - 1564, /* U+010C9F */ - 1565, /* U+010CA0 */ - 1566, /* U+010CA1 */ - 1567, /* U+010CA2 */ - 1568, /* U+010CA3 */ - 1569, /* U+010CA4 */ - 1570, /* U+010CA5 */ - 1571, /* U+010CA6 */ - 1572, /* U+010CA7 */ - 1573, /* U+010CA8 */ - 1574, /* U+010CA9 */ - 1575, /* U+010CAA */ - 1576, /* U+010CAB */ - 1577, /* U+010CAC */ - 1578, /* U+010CAD */ - 1579, /* U+010CAE */ - 1580, /* U+010CAF */ - 1581, /* U+010CB0 */ - 1582, /* U+010CB1 */ - 1583, /* U+010CB2 */ + 1534, /* U+0105BB */ + 1535, /* U+0105BC */ + 1536, /* U+010C80 */ + 1537, /* U+010C81 */ + 1538, /* U+010C82 */ + 1539, /* U+010C83 */ + 1540, /* U+010C84 */ + 1541, /* U+010C85 */ + 1542, /* U+010C86 */ + 1543, /* U+010C87 */ + 1544, /* U+010C88 */ + 1545, /* U+010C89 */ + 1546, /* U+010C8A */ + 1547, /* U+010C8B */ + 1548, /* U+010C8C */ + 1549, /* U+010C8D */ + 1550, /* U+010C8E */ + 1551, /* U+010C8F */ + 1552, /* U+010C90 */ + 1553, /* U+010C91 */ + 1554, /* U+010C92 */ + 1555, /* U+010C93 */ + 1556, /* U+010C94 */ + 1557, /* U+010C95 */ + 1558, /* U+010C96 */ + 1559, /* U+010C97 */ + 1560, /* U+010C98 */ + 1561, /* U+010C99 */ + 1562, /* U+010C9A */ + 1563, /* U+010C9B */ + 1564, /* U+010C9C */ + 1565, /* U+010C9D */ + 1566, /* U+010C9E */ + 1567, /* U+010C9F */ + 1568, /* U+010CA0 */ + 1569, /* U+010CA1 */ + 1570, /* U+010CA2 */ + 1571, /* U+010CA3 */ + 1572, /* U+010CA4 */ + 1573, /* U+010CA5 */ + 1574, /* U+010CA6 */ + 1575, /* U+010CA7 */ + 1576, /* U+010CA8 */ + 1577, /* U+010CA9 */ + 1578, /* U+010CAA */ + 1579, /* U+010CAB */ + 1580, /* U+010CAC */ + 1581, /* U+010CAD */ + 1582, /* U+010CAE */ + 1583, /* U+010CAF */ + 1584, /* U+010CB0 */ + 1585, /* U+010CB1 */ + 1586, /* U+010CB2 */ 0, /* U+010CB3 */ 0, /* U+010CB4 */ 0, /* U+010CB5 */ @@ -13118,57 +13257,57 @@ static const uint16 case_map[4778] = 0, /* U+010CBD */ 0, /* U+010CBE */ 0, /* U+010CBF */ - 1533, /* U+010CC0 */ - 1534, /* U+010CC1 */ - 1535, /* U+010CC2 */ - 1536, /* U+010CC3 */ - 1537, /* U+010CC4 */ - 1538, /* U+010CC5 */ - 1539, /* U+010CC6 */ - 1540, /* U+010CC7 */ - 1541, /* U+010CC8 */ - 1542, /* U+010CC9 */ - 1543, /* U+010CCA */ - 1544, /* U+010CCB */ - 1545, /* U+010CCC */ - 1546, /* U+010CCD */ - 1547, /* U+010CCE */ - 1548, /* U+010CCF */ - 1549, /* U+010CD0 */ - 1550, /* U+010CD1 */ - 1551, /* U+010CD2 */ - 1552, /* U+010CD3 */ - 1553, /* U+010CD4 */ - 1554, /* U+010CD5 */ - 1555, /* U+010CD6 */ - 1556, /* U+010CD7 */ - 1557, /* U+010CD8 */ - 1558, /* U+010CD9 */ - 1559, /* U+010CDA */ - 1560, /* U+010CDB */ - 1561, /* U+010CDC */ - 1562, /* U+010CDD */ - 1563, /* U+010CDE */ - 1564, /* U+010CDF */ - 1565, /* U+010CE0 */ - 1566, /* U+010CE1 */ - 1567, /* U+010CE2 */ - 1568, /* U+010CE3 */ - 1569, /* U+010CE4 */ - 1570, /* U+010CE5 */ - 1571, /* U+010CE6 */ - 1572, /* U+010CE7 */ - 1573, /* U+010CE8 */ - 1574, /* U+010CE9 */ - 1575, /* U+010CEA */ - 1576, /* U+010CEB */ - 1577, /* U+010CEC */ - 1578, /* U+010CED */ - 1579, /* U+010CEE */ - 1580, /* U+010CEF */ - 1581, /* U+010CF0 */ - 1582, /* U+010CF1 */ - 1583, /* U+010CF2 */ + 1536, /* U+010CC0 */ + 1537, /* U+010CC1 */ + 1538, /* U+010CC2 */ + 1539, /* U+010CC3 */ + 1540, /* U+010CC4 */ + 1541, /* U+010CC5 */ + 1542, /* U+010CC6 */ + 1543, /* U+010CC7 */ + 1544, /* U+010CC8 */ + 1545, /* U+010CC9 */ + 1546, /* U+010CCA */ + 1547, /* U+010CCB */ + 1548, /* U+010CCC */ + 1549, /* U+010CCD */ + 1550, /* U+010CCE */ + 1551, /* U+010CCF */ + 1552, /* U+010CD0 */ + 1553, /* U+010CD1 */ + 1554, /* U+010CD2 */ + 1555, /* U+010CD3 */ + 1556, /* U+010CD4 */ + 1557, /* U+010CD5 */ + 1558, /* U+010CD6 */ + 1559, /* U+010CD7 */ + 1560, /* U+010CD8 */ + 1561, /* U+010CD9 */ + 1562, /* U+010CDA */ + 1563, /* U+010CDB */ + 1564, /* U+010CDC */ + 1565, /* U+010CDD */ + 1566, /* U+010CDE */ + 1567, /* U+010CDF */ + 1568, /* U+010CE0 */ + 1569, /* U+010CE1 */ + 1570, /* U+010CE2 */ + 1571, /* U+010CE3 */ + 1572, /* U+010CE4 */ + 1573, /* U+010CE5 */ + 1574, /* U+010CE6 */ + 1575, /* U+010CE7 */ + 1576, /* U+010CE8 */ + 1577, /* U+010CE9 */ + 1578, /* U+010CEA */ + 1579, /* U+010CEB */ + 1580, /* U+010CEC */ + 1581, /* U+010CED */ + 1582, /* U+010CEE */ + 1583, /* U+010CEF */ + 1584, /* U+010CF0 */ + 1585, /* U+010CF1 */ + 1586, /* U+010CF2 */ 0, /* U+010CF3 */ 0, /* U+010CF4 */ 0, /* U+010CF5 */ @@ -13262,28 +13401,28 @@ static const uint16 case_map[4778] = 0, /* U+010D4D */ 0, /* U+010D4E */ 0, /* U+010D4F */ - 1584, /* U+010D50 */ - 1585, /* U+010D51 */ - 1586, /* U+010D52 */ - 1587, /* U+010D53 */ - 1588, /* U+010D54 */ - 1589, /* U+010D55 */ - 1590, /* U+010D56 */ - 1591, /* U+010D57 */ - 1592, /* U+010D58 */ - 1593, /* U+010D59 */ - 1594, /* U+010D5A */ - 1595, /* U+010D5B */ - 1596, /* U+010D5C */ - 1597, /* U+010D5D */ - 1598, /* U+010D5E */ - 1599, /* U+010D5F */ - 1600, /* U+010D60 */ - 1601, /* U+010D61 */ - 1602, /* U+010D62 */ - 1603, /* U+010D63 */ - 1604, /* U+010D64 */ - 1605, /* U+010D65 */ + 1587, /* U+010D50 */ + 1588, /* U+010D51 */ + 1589, /* U+010D52 */ + 1590, /* U+010D53 */ + 1591, /* U+010D54 */ + 1592, /* U+010D55 */ + 1593, /* U+010D56 */ + 1594, /* U+010D57 */ + 1595, /* U+010D58 */ + 1596, /* U+010D59 */ + 1597, /* U+010D5A */ + 1598, /* U+010D5B */ + 1599, /* U+010D5C */ + 1600, /* U+010D5D */ + 1601, /* U+010D5E */ + 1602, /* U+010D5F */ + 1603, /* U+010D60 */ + 1604, /* U+010D61 */ + 1605, /* U+010D62 */ + 1606, /* U+010D63 */ + 1607, /* U+010D64 */ + 1608, /* U+010D65 */ 0, /* U+010D66 */ 0, /* U+010D67 */ 0, /* U+010D68 */ @@ -13294,224 +13433,308 @@ static const uint16 case_map[4778] = 0, /* U+010D6D */ 0, /* U+010D6E */ 0, /* U+010D6F */ - 1584, /* U+010D70 */ - 1585, /* U+010D71 */ - 1586, /* U+010D72 */ - 1587, /* U+010D73 */ - 1588, /* U+010D74 */ - 1589, /* U+010D75 */ - 1590, /* U+010D76 */ - 1591, /* U+010D77 */ - 1592, /* U+010D78 */ - 1593, /* U+010D79 */ - 1594, /* U+010D7A */ - 1595, /* U+010D7B */ - 1596, /* U+010D7C */ - 1597, /* U+010D7D */ - 1598, /* U+010D7E */ - 1599, /* U+010D7F */ - 1600, /* U+010D80 */ - 1601, /* U+010D81 */ - 1602, /* U+010D82 */ - 1603, /* U+010D83 */ - 1604, /* U+010D84 */ - 1605, /* U+010D85 */ - 1606, /* U+0118A0 */ - 1607, /* U+0118A1 */ - 1608, /* U+0118A2 */ - 1609, /* U+0118A3 */ - 1610, /* U+0118A4 */ - 1611, /* U+0118A5 */ - 1612, /* U+0118A6 */ - 1613, /* U+0118A7 */ - 1614, /* U+0118A8 */ - 1615, /* U+0118A9 */ - 1616, /* U+0118AA */ - 1617, /* U+0118AB */ - 1618, /* U+0118AC */ - 1619, /* U+0118AD */ - 1620, /* U+0118AE */ - 1621, /* U+0118AF */ - 1622, /* U+0118B0 */ - 1623, /* U+0118B1 */ - 1624, /* U+0118B2 */ - 1625, /* U+0118B3 */ - 1626, /* U+0118B4 */ - 1627, /* U+0118B5 */ - 1628, /* U+0118B6 */ - 1629, /* U+0118B7 */ - 1630, /* U+0118B8 */ - 1631, /* U+0118B9 */ - 1632, /* U+0118BA */ - 1633, /* U+0118BB */ - 1634, /* U+0118BC */ - 1635, /* U+0118BD */ - 1636, /* U+0118BE */ - 1637, /* U+0118BF */ - 1606, /* U+0118C0 */ - 1607, /* U+0118C1 */ - 1608, /* U+0118C2 */ - 1609, /* U+0118C3 */ - 1610, /* U+0118C4 */ - 1611, /* U+0118C5 */ - 1612, /* U+0118C6 */ - 1613, /* U+0118C7 */ - 1614, /* U+0118C8 */ - 1615, /* U+0118C9 */ - 1616, /* U+0118CA */ - 1617, /* U+0118CB */ - 1618, /* U+0118CC */ - 1619, /* U+0118CD */ - 1620, /* U+0118CE */ - 1621, /* U+0118CF */ - 1622, /* U+0118D0 */ - 1623, /* U+0118D1 */ - 1624, /* U+0118D2 */ - 1625, /* U+0118D3 */ - 1626, /* U+0118D4 */ - 1627, /* U+0118D5 */ - 1628, /* U+0118D6 */ - 1629, /* U+0118D7 */ - 1630, /* U+0118D8 */ - 1631, /* U+0118D9 */ - 1632, /* U+0118DA */ - 1633, /* U+0118DB */ - 1634, /* U+0118DC */ - 1635, /* U+0118DD */ - 1636, /* U+0118DE */ - 1637, /* U+0118DF */ - 1638, /* U+016E40 */ - 1639, /* U+016E41 */ - 1640, /* U+016E42 */ - 1641, /* U+016E43 */ - 1642, /* U+016E44 */ - 1643, /* U+016E45 */ - 1644, /* U+016E46 */ - 1645, /* U+016E47 */ - 1646, /* U+016E48 */ - 1647, /* U+016E49 */ - 1648, /* U+016E4A */ - 1649, /* U+016E4B */ - 1650, /* U+016E4C */ - 1651, /* U+016E4D */ - 1652, /* U+016E4E */ - 1653, /* U+016E4F */ - 1654, /* U+016E50 */ - 1655, /* U+016E51 */ - 1656, /* U+016E52 */ - 1657, /* U+016E53 */ - 1658, /* U+016E54 */ - 1659, /* U+016E55 */ - 1660, /* U+016E56 */ - 1661, /* U+016E57 */ - 1662, /* U+016E58 */ - 1663, /* U+016E59 */ - 1664, /* U+016E5A */ - 1665, /* U+016E5B */ - 1666, /* U+016E5C */ - 1667, /* U+016E5D */ - 1668, /* U+016E5E */ - 1669, /* U+016E5F */ - 1638, /* U+016E60 */ - 1639, /* U+016E61 */ - 1640, /* U+016E62 */ - 1641, /* U+016E63 */ - 1642, /* U+016E64 */ - 1643, /* U+016E65 */ - 1644, /* U+016E66 */ - 1645, /* U+016E67 */ - 1646, /* U+016E68 */ - 1647, /* U+016E69 */ - 1648, /* U+016E6A */ - 1649, /* U+016E6B */ - 1650, /* U+016E6C */ - 1651, /* U+016E6D */ - 1652, /* U+016E6E */ - 1653, /* U+016E6F */ - 1654, /* U+016E70 */ - 1655, /* U+016E71 */ - 1656, /* U+016E72 */ - 1657, /* U+016E73 */ - 1658, /* U+016E74 */ - 1659, /* U+016E75 */ - 1660, /* U+016E76 */ - 1661, /* U+016E77 */ - 1662, /* U+016E78 */ - 1663, /* U+016E79 */ - 1664, /* U+016E7A */ - 1665, /* U+016E7B */ - 1666, /* U+016E7C */ - 1667, /* U+016E7D */ - 1668, /* U+016E7E */ - 1669, /* U+016E7F */ - 1670, /* U+01E900 */ - 1671, /* U+01E901 */ - 1672, /* U+01E902 */ - 1673, /* U+01E903 */ - 1674, /* U+01E904 */ - 1675, /* U+01E905 */ - 1676, /* U+01E906 */ - 1677, /* U+01E907 */ - 1678, /* U+01E908 */ - 1679, /* U+01E909 */ - 1680, /* U+01E90A */ - 1681, /* U+01E90B */ - 1682, /* U+01E90C */ - 1683, /* U+01E90D */ - 1684, /* U+01E90E */ - 1685, /* U+01E90F */ - 1686, /* U+01E910 */ - 1687, /* U+01E911 */ - 1688, /* U+01E912 */ - 1689, /* U+01E913 */ - 1690, /* U+01E914 */ - 1691, /* U+01E915 */ - 1692, /* U+01E916 */ - 1693, /* U+01E917 */ - 1694, /* U+01E918 */ - 1695, /* U+01E919 */ - 1696, /* U+01E91A */ - 1697, /* U+01E91B */ - 1698, /* U+01E91C */ - 1699, /* U+01E91D */ - 1700, /* U+01E91E */ - 1701, /* U+01E91F */ - 1702, /* U+01E920 */ - 1703, /* U+01E921 */ - 1670, /* U+01E922 */ - 1671, /* U+01E923 */ - 1672, /* U+01E924 */ - 1673, /* U+01E925 */ - 1674, /* U+01E926 */ - 1675, /* U+01E927 */ - 1676, /* U+01E928 */ - 1677, /* U+01E929 */ - 1678, /* U+01E92A */ - 1679, /* U+01E92B */ - 1680, /* U+01E92C */ - 1681, /* U+01E92D */ - 1682, /* U+01E92E */ - 1683, /* U+01E92F */ - 1684, /* U+01E930 */ - 1685, /* U+01E931 */ - 1686, /* U+01E932 */ - 1687, /* U+01E933 */ - 1688, /* U+01E934 */ - 1689, /* U+01E935 */ - 1690, /* U+01E936 */ - 1691, /* U+01E937 */ - 1692, /* U+01E938 */ - 1693, /* U+01E939 */ - 1694, /* U+01E93A */ - 1695, /* U+01E93B */ - 1696, /* U+01E93C */ - 1697, /* U+01E93D */ - 1698, /* U+01E93E */ - 1699, /* U+01E93F */ - 1700, /* U+01E940 */ - 1701, /* U+01E941 */ - 1702, /* U+01E942 */ - 1703, /* U+01E943 */ + 1587, /* U+010D70 */ + 1588, /* U+010D71 */ + 1589, /* U+010D72 */ + 1590, /* U+010D73 */ + 1591, /* U+010D74 */ + 1592, /* U+010D75 */ + 1593, /* U+010D76 */ + 1594, /* U+010D77 */ + 1595, /* U+010D78 */ + 1596, /* U+010D79 */ + 1597, /* U+010D7A */ + 1598, /* U+010D7B */ + 1599, /* U+010D7C */ + 1600, /* U+010D7D */ + 1601, /* U+010D7E */ + 1602, /* U+010D7F */ + 1603, /* U+010D80 */ + 1604, /* U+010D81 */ + 1605, /* U+010D82 */ + 1606, /* U+010D83 */ + 1607, /* U+010D84 */ + 1608, /* U+010D85 */ + 1609, /* U+0118A0 */ + 1610, /* U+0118A1 */ + 1611, /* U+0118A2 */ + 1612, /* U+0118A3 */ + 1613, /* U+0118A4 */ + 1614, /* U+0118A5 */ + 1615, /* U+0118A6 */ + 1616, /* U+0118A7 */ + 1617, /* U+0118A8 */ + 1618, /* U+0118A9 */ + 1619, /* U+0118AA */ + 1620, /* U+0118AB */ + 1621, /* U+0118AC */ + 1622, /* U+0118AD */ + 1623, /* U+0118AE */ + 1624, /* U+0118AF */ + 1625, /* U+0118B0 */ + 1626, /* U+0118B1 */ + 1627, /* U+0118B2 */ + 1628, /* U+0118B3 */ + 1629, /* U+0118B4 */ + 1630, /* U+0118B5 */ + 1631, /* U+0118B6 */ + 1632, /* U+0118B7 */ + 1633, /* U+0118B8 */ + 1634, /* U+0118B9 */ + 1635, /* U+0118BA */ + 1636, /* U+0118BB */ + 1637, /* U+0118BC */ + 1638, /* U+0118BD */ + 1639, /* U+0118BE */ + 1640, /* U+0118BF */ + 1609, /* U+0118C0 */ + 1610, /* U+0118C1 */ + 1611, /* U+0118C2 */ + 1612, /* U+0118C3 */ + 1613, /* U+0118C4 */ + 1614, /* U+0118C5 */ + 1615, /* U+0118C6 */ + 1616, /* U+0118C7 */ + 1617, /* U+0118C8 */ + 1618, /* U+0118C9 */ + 1619, /* U+0118CA */ + 1620, /* U+0118CB */ + 1621, /* U+0118CC */ + 1622, /* U+0118CD */ + 1623, /* U+0118CE */ + 1624, /* U+0118CF */ + 1625, /* U+0118D0 */ + 1626, /* U+0118D1 */ + 1627, /* U+0118D2 */ + 1628, /* U+0118D3 */ + 1629, /* U+0118D4 */ + 1630, /* U+0118D5 */ + 1631, /* U+0118D6 */ + 1632, /* U+0118D7 */ + 1633, /* U+0118D8 */ + 1634, /* U+0118D9 */ + 1635, /* U+0118DA */ + 1636, /* U+0118DB */ + 1637, /* U+0118DC */ + 1638, /* U+0118DD */ + 1639, /* U+0118DE */ + 1640, /* U+0118DF */ + 1641, /* U+016E40 */ + 1642, /* U+016E41 */ + 1643, /* U+016E42 */ + 1644, /* U+016E43 */ + 1645, /* U+016E44 */ + 1646, /* U+016E45 */ + 1647, /* U+016E46 */ + 1648, /* U+016E47 */ + 1649, /* U+016E48 */ + 1650, /* U+016E49 */ + 1651, /* U+016E4A */ + 1652, /* U+016E4B */ + 1653, /* U+016E4C */ + 1654, /* U+016E4D */ + 1655, /* U+016E4E */ + 1656, /* U+016E4F */ + 1657, /* U+016E50 */ + 1658, /* U+016E51 */ + 1659, /* U+016E52 */ + 1660, /* U+016E53 */ + 1661, /* U+016E54 */ + 1662, /* U+016E55 */ + 1663, /* U+016E56 */ + 1664, /* U+016E57 */ + 1665, /* U+016E58 */ + 1666, /* U+016E59 */ + 1667, /* U+016E5A */ + 1668, /* U+016E5B */ + 1669, /* U+016E5C */ + 1670, /* U+016E5D */ + 1671, /* U+016E5E */ + 1672, /* U+016E5F */ + 1641, /* U+016E60 */ + 1642, /* U+016E61 */ + 1643, /* U+016E62 */ + 1644, /* U+016E63 */ + 1645, /* U+016E64 */ + 1646, /* U+016E65 */ + 1647, /* U+016E66 */ + 1648, /* U+016E67 */ + 1649, /* U+016E68 */ + 1650, /* U+016E69 */ + 1651, /* U+016E6A */ + 1652, /* U+016E6B */ + 1653, /* U+016E6C */ + 1654, /* U+016E6D */ + 1655, /* U+016E6E */ + 1656, /* U+016E6F */ + 1657, /* U+016E70 */ + 1658, /* U+016E71 */ + 1659, /* U+016E72 */ + 1660, /* U+016E73 */ + 1661, /* U+016E74 */ + 1662, /* U+016E75 */ + 1663, /* U+016E76 */ + 1664, /* U+016E77 */ + 1665, /* U+016E78 */ + 1666, /* U+016E79 */ + 1667, /* U+016E7A */ + 1668, /* U+016E7B */ + 1669, /* U+016E7C */ + 1670, /* U+016E7D */ + 1671, /* U+016E7E */ + 1672, /* U+016E7F */ + 0, /* U+016E80 */ + 0, /* U+016E81 */ + 0, /* U+016E82 */ + 0, /* U+016E83 */ + 0, /* U+016E84 */ + 0, /* U+016E85 */ + 0, /* U+016E86 */ + 0, /* U+016E87 */ + 0, /* U+016E88 */ + 0, /* U+016E89 */ + 0, /* U+016E8A */ + 0, /* U+016E8B */ + 0, /* U+016E8C */ + 0, /* U+016E8D */ + 0, /* U+016E8E */ + 0, /* U+016E8F */ + 0, /* U+016E90 */ + 0, /* U+016E91 */ + 0, /* U+016E92 */ + 0, /* U+016E93 */ + 0, /* U+016E94 */ + 0, /* U+016E95 */ + 0, /* U+016E96 */ + 0, /* U+016E97 */ + 0, /* U+016E98 */ + 0, /* U+016E99 */ + 0, /* U+016E9A */ + 0, /* U+016E9B */ + 0, /* U+016E9C */ + 0, /* U+016E9D */ + 0, /* U+016E9E */ + 0, /* U+016E9F */ + 1673, /* U+016EA0 */ + 1674, /* U+016EA1 */ + 1675, /* U+016EA2 */ + 1676, /* U+016EA3 */ + 1677, /* U+016EA4 */ + 1678, /* U+016EA5 */ + 1679, /* U+016EA6 */ + 1680, /* U+016EA7 */ + 1681, /* U+016EA8 */ + 1682, /* U+016EA9 */ + 1683, /* U+016EAA */ + 1684, /* U+016EAB */ + 1685, /* U+016EAC */ + 1686, /* U+016EAD */ + 1687, /* U+016EAE */ + 1688, /* U+016EAF */ + 1689, /* U+016EB0 */ + 1690, /* U+016EB1 */ + 1691, /* U+016EB2 */ + 1692, /* U+016EB3 */ + 1693, /* U+016EB4 */ + 1694, /* U+016EB5 */ + 1695, /* U+016EB6 */ + 1696, /* U+016EB7 */ + 1697, /* U+016EB8 */ + 0, /* U+016EB9 */ + 0, /* U+016EBA */ + 1673, /* U+016EBB */ + 1674, /* U+016EBC */ + 1675, /* U+016EBD */ + 1676, /* U+016EBE */ + 1677, /* U+016EBF */ + 1678, /* U+016EC0 */ + 1679, /* U+016EC1 */ + 1680, /* U+016EC2 */ + 1681, /* U+016EC3 */ + 1682, /* U+016EC4 */ + 1683, /* U+016EC5 */ + 1684, /* U+016EC6 */ + 1685, /* U+016EC7 */ + 1686, /* U+016EC8 */ + 1687, /* U+016EC9 */ + 1688, /* U+016ECA */ + 1689, /* U+016ECB */ + 1690, /* U+016ECC */ + 1691, /* U+016ECD */ + 1692, /* U+016ECE */ + 1693, /* U+016ECF */ + 1694, /* U+016ED0 */ + 1695, /* U+016ED1 */ + 1696, /* U+016ED2 */ + 1697, /* U+016ED3 */ + 1698, /* U+01E900 */ + 1699, /* U+01E901 */ + 1700, /* U+01E902 */ + 1701, /* U+01E903 */ + 1702, /* U+01E904 */ + 1703, /* U+01E905 */ + 1704, /* U+01E906 */ + 1705, /* U+01E907 */ + 1706, /* U+01E908 */ + 1707, /* U+01E909 */ + 1708, /* U+01E90A */ + 1709, /* U+01E90B */ + 1710, /* U+01E90C */ + 1711, /* U+01E90D */ + 1712, /* U+01E90E */ + 1713, /* U+01E90F */ + 1714, /* U+01E910 */ + 1715, /* U+01E911 */ + 1716, /* U+01E912 */ + 1717, /* U+01E913 */ + 1718, /* U+01E914 */ + 1719, /* U+01E915 */ + 1720, /* U+01E916 */ + 1721, /* U+01E917 */ + 1722, /* U+01E918 */ + 1723, /* U+01E919 */ + 1724, /* U+01E91A */ + 1725, /* U+01E91B */ + 1726, /* U+01E91C */ + 1727, /* U+01E91D */ + 1728, /* U+01E91E */ + 1729, /* U+01E91F */ + 1730, /* U+01E920 */ + 1731, /* U+01E921 */ + 1698, /* U+01E922 */ + 1699, /* U+01E923 */ + 1700, /* U+01E924 */ + 1701, /* U+01E925 */ + 1702, /* U+01E926 */ + 1703, /* U+01E927 */ + 1704, /* U+01E928 */ + 1705, /* U+01E929 */ + 1706, /* U+01E92A */ + 1707, /* U+01E92B */ + 1708, /* U+01E92C */ + 1709, /* U+01E92D */ + 1710, /* U+01E92E */ + 1711, /* U+01E92F */ + 1712, /* U+01E930 */ + 1713, /* U+01E931 */ + 1714, /* U+01E932 */ + 1715, /* U+01E933 */ + 1716, /* U+01E934 */ + 1717, /* U+01E935 */ + 1718, /* U+01E936 */ + 1719, /* U+01E937 */ + 1720, /* U+01E938 */ + 1721, /* U+01E939 */ + 1722, /* U+01E93A */ + 1723, /* U+01E93B */ + 1724, /* U+01E93C */ + 1725, /* U+01E93D */ + 1726, /* U+01E93E */ + 1727, /* U+01E93F */ + 1728, /* U+01E940 */ + 1729, /* U+01E941 */ + 1730, /* U+01E942 */ + 1731, /* U+01E943 */ }; @@ -13522,7 +13745,7 @@ static const uint16 case_map[4778] = * the offset into the mapping tables. */ static inline uint16 -case_index(pg_wchar cp) +case_index(char32_t cp) { /* Fast path for codepoints < 0x0588 */ if (cp < 0x0588) @@ -13605,7 +13828,7 @@ case_index(pg_wchar cp) } else if (cp >= 0x118A0) { - if (cp < 0x16E80) + if (cp < 0x16ED4) { if (cp < 0x118E0) { @@ -13620,7 +13843,7 @@ case_index(pg_wchar cp) { if (cp < 0x1E944) { - return case_map[cp - 0x1E900 + 4710]; + return case_map[cp - 0x1E900 + 4794]; } } } diff --git a/src/include/common/unicode_category.h b/src/include/common/unicode_category.h index 8fd8b67a416e6..d435322f88d37 100644 --- a/src/include/common/unicode_category.h +++ b/src/include/common/unicode_category.h @@ -5,7 +5,7 @@ * * These definitions can be used by both frontend and backend code. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/common/unicode_category.h * @@ -14,8 +14,6 @@ #ifndef UNICODE_CATEGORY_H #define UNICODE_CATEGORY_H -#include "mb/pg_wchar.h" - /* * Unicode General Category Values * @@ -61,31 +59,31 @@ typedef enum pg_unicode_category PG_U_FINAL_PUNCTUATION = 29 /* Pf */ } pg_unicode_category; -extern pg_unicode_category unicode_category(pg_wchar code); +extern pg_unicode_category unicode_category(char32_t code); extern const char *unicode_category_string(pg_unicode_category category); extern const char *unicode_category_abbrev(pg_unicode_category category); -extern bool pg_u_prop_alphabetic(pg_wchar code); -extern bool pg_u_prop_lowercase(pg_wchar code); -extern bool pg_u_prop_uppercase(pg_wchar code); -extern bool pg_u_prop_cased(pg_wchar code); -extern bool pg_u_prop_case_ignorable(pg_wchar code); -extern bool pg_u_prop_white_space(pg_wchar code); -extern bool pg_u_prop_hex_digit(pg_wchar code); -extern bool pg_u_prop_join_control(pg_wchar code); +extern bool pg_u_prop_alphabetic(char32_t code); +extern bool pg_u_prop_lowercase(char32_t code); +extern bool pg_u_prop_uppercase(char32_t code); +extern bool pg_u_prop_cased(char32_t code); +extern bool pg_u_prop_case_ignorable(char32_t code); +extern bool pg_u_prop_white_space(char32_t code); +extern bool pg_u_prop_hex_digit(char32_t code); +extern bool pg_u_prop_join_control(char32_t code); -extern bool pg_u_isdigit(pg_wchar code, bool posix); -extern bool pg_u_isalpha(pg_wchar code); -extern bool pg_u_isalnum(pg_wchar code, bool posix); -extern bool pg_u_isword(pg_wchar code); -extern bool pg_u_isupper(pg_wchar code); -extern bool pg_u_islower(pg_wchar code); -extern bool pg_u_isblank(pg_wchar code); -extern bool pg_u_iscntrl(pg_wchar code); -extern bool pg_u_isgraph(pg_wchar code); -extern bool pg_u_isprint(pg_wchar code); -extern bool pg_u_ispunct(pg_wchar code, bool posix); -extern bool pg_u_isspace(pg_wchar code); -extern bool pg_u_isxdigit(pg_wchar code, bool posix); +extern bool pg_u_isdigit(char32_t code, bool posix); +extern bool pg_u_isalpha(char32_t code); +extern bool pg_u_isalnum(char32_t code, bool posix); +extern bool pg_u_isword(char32_t code); +extern bool pg_u_isupper(char32_t code); +extern bool pg_u_islower(char32_t code); +extern bool pg_u_isblank(char32_t code); +extern bool pg_u_iscntrl(char32_t code); +extern bool pg_u_isgraph(char32_t code); +extern bool pg_u_isprint(char32_t code); +extern bool pg_u_ispunct(char32_t code, bool posix); +extern bool pg_u_isspace(char32_t code); +extern bool pg_u_isxdigit(char32_t code, bool posix); #endif /* UNICODE_CATEGORY_H */ diff --git a/src/include/common/unicode_category_table.h b/src/include/common/unicode_category_table.h index 95a1c65da7e6f..b45df500d083c 100644 --- a/src/include/common/unicode_category_table.h +++ b/src/include/common/unicode_category_table.h @@ -3,7 +3,7 @@ * unicode_category_table.h * Category table for Unicode character classification. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_category_table.h @@ -20,15 +20,15 @@ */ typedef struct { - uint32 first; /* Unicode codepoint */ - uint32 last; /* Unicode codepoint */ + char32_t first; /* Unicode codepoint */ + char32_t last; /* Unicode codepoint */ uint8 category; /* General Category */ } pg_category_range; typedef struct { - uint32 first; /* Unicode codepoint */ - uint32 last; /* Unicode codepoint */ + char32_t first; /* Unicode codepoint */ + char32_t last; /* Unicode codepoint */ } pg_unicode_range; typedef struct @@ -696,7 +696,7 @@ static const pg_unicode_properties unicode_opt_ascii[128] = }; /* table of Unicode codepoint ranges and their categories */ -static const pg_category_range unicode_categories[3368] = +static const pg_category_range unicode_categories[3409] = { {0x000000, 0x00001f, PG_U_CONTROL}, {0x000020, 0x000020, PG_U_SPACE_SEPARATOR}, @@ -1046,8 +1046,8 @@ static const pg_category_range unicode_categories[3368] = {0x00024d, 0x00024d, PG_U_LOWERCASE_LETTER}, {0x00024e, 0x00024e, PG_U_UPPERCASE_LETTER}, {0x00024f, 0x000293, PG_U_LOWERCASE_LETTER}, - {0x000294, 0x000294, PG_U_OTHER_LETTER}, - {0x000295, 0x0002af, PG_U_LOWERCASE_LETTER}, + {0x000294, 0x000295, PG_U_OTHER_LETTER}, + {0x000296, 0x0002af, PG_U_LOWERCASE_LETTER}, {0x0002b0, 0x0002c1, PG_U_MODIFIER_LETTER}, {0x0002c2, 0x0002c5, PG_U_MODIFIER_SYMBOL}, {0x0002c6, 0x0002d1, PG_U_MODIFIER_LETTER}, @@ -1406,7 +1406,7 @@ static const pg_category_range unicode_categories[3368] = {0x000860, 0x00086a, PG_U_OTHER_LETTER}, {0x000870, 0x000887, PG_U_OTHER_LETTER}, {0x000888, 0x000888, PG_U_MODIFIER_SYMBOL}, - {0x000889, 0x00088e, PG_U_OTHER_LETTER}, + {0x000889, 0x00088f, PG_U_OTHER_LETTER}, {0x000890, 0x000891, PG_U_FORMAT}, {0x000897, 0x00089f, PG_U_NONSPACING_MARK}, {0x0008a0, 0x0008c8, PG_U_OTHER_LETTER}, @@ -1574,7 +1574,7 @@ static const pg_category_range unicode_categories[3368] = {0x000c4a, 0x000c4d, PG_U_NONSPACING_MARK}, {0x000c55, 0x000c56, PG_U_NONSPACING_MARK}, {0x000c58, 0x000c5a, PG_U_OTHER_LETTER}, - {0x000c5d, 0x000c5d, PG_U_OTHER_LETTER}, + {0x000c5c, 0x000c5d, PG_U_OTHER_LETTER}, {0x000c60, 0x000c61, PG_U_OTHER_LETTER}, {0x000c62, 0x000c63, PG_U_NONSPACING_MARK}, {0x000c66, 0x000c6f, PG_U_DECIMAL_NUMBER}, @@ -1600,7 +1600,7 @@ static const pg_category_range unicode_categories[3368] = {0x000cca, 0x000ccb, PG_U_SPACING_MARK}, {0x000ccc, 0x000ccd, PG_U_NONSPACING_MARK}, {0x000cd5, 0x000cd6, PG_U_SPACING_MARK}, - {0x000cdd, 0x000cde, PG_U_OTHER_LETTER}, + {0x000cdc, 0x000cde, PG_U_OTHER_LETTER}, {0x000ce0, 0x000ce1, PG_U_OTHER_LETTER}, {0x000ce2, 0x000ce3, PG_U_NONSPACING_MARK}, {0x000ce6, 0x000cef, PG_U_DECIMAL_NUMBER}, @@ -1874,7 +1874,8 @@ static const pg_category_range unicode_categories[3368] = {0x001aa8, 0x001aad, PG_U_OTHER_PUNCTUATION}, {0x001ab0, 0x001abd, PG_U_NONSPACING_MARK}, {0x001abe, 0x001abe, PG_U_ENCLOSING_MARK}, - {0x001abf, 0x001ace, PG_U_NONSPACING_MARK}, + {0x001abf, 0x001add, PG_U_NONSPACING_MARK}, + {0x001ae0, 0x001aeb, PG_U_NONSPACING_MARK}, {0x001b00, 0x001b03, PG_U_NONSPACING_MARK}, {0x001b04, 0x001b04, PG_U_SPACING_MARK}, {0x001b05, 0x001b33, PG_U_OTHER_LETTER}, @@ -2293,7 +2294,7 @@ static const pg_category_range unicode_categories[3368] = {0x00208d, 0x00208d, PG_U_OPEN_PUNCTUATION}, {0x00208e, 0x00208e, PG_U_CLOSE_PUNCTUATION}, {0x002090, 0x00209c, PG_U_MODIFIER_LETTER}, - {0x0020a0, 0x0020c0, PG_U_CURRENCY_SYMBOL}, + {0x0020a0, 0x0020c1, PG_U_CURRENCY_SYMBOL}, {0x0020d0, 0x0020dc, PG_U_NONSPACING_MARK}, {0x0020dd, 0x0020e0, PG_U_ENCLOSING_MARK}, {0x0020e1, 0x0020e1, PG_U_NONSPACING_MARK}, @@ -2464,8 +2465,7 @@ static const pg_category_range unicode_categories[3368] = {0x002b45, 0x002b46, PG_U_OTHER_SYMBOL}, {0x002b47, 0x002b4c, PG_U_MATH_SYMBOL}, {0x002b4d, 0x002b73, PG_U_OTHER_SYMBOL}, - {0x002b76, 0x002b95, PG_U_OTHER_SYMBOL}, - {0x002b97, 0x002bff, PG_U_OTHER_SYMBOL}, + {0x002b76, 0x002bff, PG_U_OTHER_SYMBOL}, {0x002c00, 0x002c2f, PG_U_UPPERCASE_LETTER}, {0x002c30, 0x002c5f, PG_U_LOWERCASE_LETTER}, {0x002c60, 0x002c60, PG_U_UPPERCASE_LETTER}, @@ -2988,9 +2988,13 @@ static const pg_category_range unicode_categories[3368] = {0x00a7ca, 0x00a7ca, PG_U_LOWERCASE_LETTER}, {0x00a7cb, 0x00a7cc, PG_U_UPPERCASE_LETTER}, {0x00a7cd, 0x00a7cd, PG_U_LOWERCASE_LETTER}, + {0x00a7ce, 0x00a7ce, PG_U_UPPERCASE_LETTER}, + {0x00a7cf, 0x00a7cf, PG_U_LOWERCASE_LETTER}, {0x00a7d0, 0x00a7d0, PG_U_UPPERCASE_LETTER}, {0x00a7d1, 0x00a7d1, PG_U_LOWERCASE_LETTER}, + {0x00a7d2, 0x00a7d2, PG_U_UPPERCASE_LETTER}, {0x00a7d3, 0x00a7d3, PG_U_LOWERCASE_LETTER}, + {0x00a7d4, 0x00a7d4, PG_U_UPPERCASE_LETTER}, {0x00a7d5, 0x00a7d5, PG_U_LOWERCASE_LETTER}, {0x00a7d6, 0x00a7d6, PG_U_UPPERCASE_LETTER}, {0x00a7d7, 0x00a7d7, PG_U_LOWERCASE_LETTER}, @@ -2999,7 +3003,7 @@ static const pg_category_range unicode_categories[3368] = {0x00a7da, 0x00a7da, PG_U_UPPERCASE_LETTER}, {0x00a7db, 0x00a7db, PG_U_LOWERCASE_LETTER}, {0x00a7dc, 0x00a7dc, PG_U_UPPERCASE_LETTER}, - {0x00a7f2, 0x00a7f4, PG_U_MODIFIER_LETTER}, + {0x00a7f1, 0x00a7f4, PG_U_MODIFIER_LETTER}, {0x00a7f5, 0x00a7f5, PG_U_UPPERCASE_LETTER}, {0x00a7f6, 0x00a7f6, PG_U_LOWERCASE_LETTER}, {0x00a7f7, 0x00a7f7, PG_U_OTHER_LETTER}, @@ -3150,13 +3154,15 @@ static const pg_category_range unicode_categories[3368] = {0x00fb43, 0x00fb44, PG_U_OTHER_LETTER}, {0x00fb46, 0x00fbb1, PG_U_OTHER_LETTER}, {0x00fbb2, 0x00fbc2, PG_U_MODIFIER_SYMBOL}, + {0x00fbc3, 0x00fbd2, PG_U_OTHER_SYMBOL}, {0x00fbd3, 0x00fd3d, PG_U_OTHER_LETTER}, {0x00fd3e, 0x00fd3e, PG_U_CLOSE_PUNCTUATION}, {0x00fd3f, 0x00fd3f, PG_U_OPEN_PUNCTUATION}, {0x00fd40, 0x00fd4f, PG_U_OTHER_SYMBOL}, {0x00fd50, 0x00fd8f, PG_U_OTHER_LETTER}, + {0x00fd90, 0x00fd91, PG_U_OTHER_SYMBOL}, {0x00fd92, 0x00fdc7, PG_U_OTHER_LETTER}, - {0x00fdcf, 0x00fdcf, PG_U_OTHER_SYMBOL}, + {0x00fdc8, 0x00fdcf, PG_U_OTHER_SYMBOL}, {0x00fdf0, 0x00fdfb, PG_U_OTHER_LETTER}, {0x00fdfc, 0x00fdfc, PG_U_CURRENCY_SYMBOL}, {0x00fdfd, 0x00fdff, PG_U_OTHER_SYMBOL}, @@ -3342,6 +3348,7 @@ static const pg_category_range unicode_categories[3368] = {0x01091f, 0x01091f, PG_U_OTHER_PUNCTUATION}, {0x010920, 0x010939, PG_U_OTHER_LETTER}, {0x01093f, 0x01093f, PG_U_OTHER_PUNCTUATION}, + {0x010940, 0x010959, PG_U_OTHER_LETTER}, {0x010980, 0x0109b7, PG_U_OTHER_LETTER}, {0x0109bc, 0x0109bd, PG_U_OTHER_NUMBER}, {0x0109be, 0x0109bf, PG_U_OTHER_LETTER}, @@ -3401,7 +3408,11 @@ static const pg_category_range unicode_categories[3368] = {0x010ead, 0x010ead, PG_U_DASH_PUNCTUATION}, {0x010eb0, 0x010eb1, PG_U_OTHER_LETTER}, {0x010ec2, 0x010ec4, PG_U_OTHER_LETTER}, - {0x010efc, 0x010eff, PG_U_NONSPACING_MARK}, + {0x010ec5, 0x010ec5, PG_U_MODIFIER_LETTER}, + {0x010ec6, 0x010ec7, PG_U_OTHER_LETTER}, + {0x010ed0, 0x010ed0, PG_U_OTHER_PUNCTUATION}, + {0x010ed1, 0x010ed8, PG_U_OTHER_SYMBOL}, + {0x010efa, 0x010eff, PG_U_NONSPACING_MARK}, {0x010f00, 0x010f1c, PG_U_OTHER_LETTER}, {0x010f1d, 0x010f26, PG_U_OTHER_NUMBER}, {0x010f27, 0x010f27, PG_U_OTHER_LETTER}, @@ -3670,6 +3681,12 @@ static const pg_category_range unicode_categories[3368] = {0x011a9e, 0x011aa2, PG_U_OTHER_PUNCTUATION}, {0x011ab0, 0x011af8, PG_U_OTHER_LETTER}, {0x011b00, 0x011b09, PG_U_OTHER_PUNCTUATION}, + {0x011b60, 0x011b60, PG_U_NONSPACING_MARK}, + {0x011b61, 0x011b61, PG_U_SPACING_MARK}, + {0x011b62, 0x011b64, PG_U_NONSPACING_MARK}, + {0x011b65, 0x011b65, PG_U_SPACING_MARK}, + {0x011b66, 0x011b66, PG_U_NONSPACING_MARK}, + {0x011b67, 0x011b67, PG_U_SPACING_MARK}, {0x011bc0, 0x011be0, PG_U_OTHER_LETTER}, {0x011be1, 0x011be1, PG_U_OTHER_PUNCTUATION}, {0x011bf0, 0x011bf9, PG_U_DECIMAL_NUMBER}, @@ -3714,6 +3731,10 @@ static const pg_category_range unicode_categories[3368] = {0x011d97, 0x011d97, PG_U_NONSPACING_MARK}, {0x011d98, 0x011d98, PG_U_OTHER_LETTER}, {0x011da0, 0x011da9, PG_U_DECIMAL_NUMBER}, + {0x011db0, 0x011dd8, PG_U_OTHER_LETTER}, + {0x011dd9, 0x011dd9, PG_U_MODIFIER_LETTER}, + {0x011dda, 0x011ddb, PG_U_OTHER_LETTER}, + {0x011de0, 0x011de9, PG_U_DECIMAL_NUMBER}, {0x011ee0, 0x011ef2, PG_U_OTHER_LETTER}, {0x011ef3, 0x011ef4, PG_U_NONSPACING_MARK}, {0x011ef5, 0x011ef6, PG_U_SPACING_MARK}, @@ -3785,6 +3806,8 @@ static const pg_category_range unicode_categories[3368] = {0x016e60, 0x016e7f, PG_U_LOWERCASE_LETTER}, {0x016e80, 0x016e96, PG_U_OTHER_NUMBER}, {0x016e97, 0x016e9a, PG_U_OTHER_PUNCTUATION}, + {0x016ea0, 0x016eb8, PG_U_UPPERCASE_LETTER}, + {0x016ebb, 0x016ed3, PG_U_LOWERCASE_LETTER}, {0x016f00, 0x016f4a, PG_U_OTHER_LETTER}, {0x016f4f, 0x016f4f, PG_U_NONSPACING_MARK}, {0x016f50, 0x016f50, PG_U_OTHER_LETTER}, @@ -3796,9 +3819,11 @@ static const pg_category_range unicode_categories[3368] = {0x016fe3, 0x016fe3, PG_U_MODIFIER_LETTER}, {0x016fe4, 0x016fe4, PG_U_NONSPACING_MARK}, {0x016ff0, 0x016ff1, PG_U_SPACING_MARK}, - {0x017000, 0x0187f7, PG_U_OTHER_LETTER}, - {0x018800, 0x018cd5, PG_U_OTHER_LETTER}, - {0x018cff, 0x018d08, PG_U_OTHER_LETTER}, + {0x016ff2, 0x016ff3, PG_U_MODIFIER_LETTER}, + {0x016ff4, 0x016ff6, PG_U_LETTER_NUMBER}, + {0x017000, 0x018cd5, PG_U_OTHER_LETTER}, + {0x018cff, 0x018d1e, PG_U_OTHER_LETTER}, + {0x018d80, 0x018df2, PG_U_OTHER_LETTER}, {0x01aff0, 0x01aff3, PG_U_MODIFIER_LETTER}, {0x01aff5, 0x01affb, PG_U_MODIFIER_LETTER}, {0x01affd, 0x01affe, PG_U_MODIFIER_LETTER}, @@ -3818,7 +3843,11 @@ static const pg_category_range unicode_categories[3368] = {0x01bca0, 0x01bca3, PG_U_FORMAT}, {0x01cc00, 0x01ccef, PG_U_OTHER_SYMBOL}, {0x01ccf0, 0x01ccf9, PG_U_DECIMAL_NUMBER}, + {0x01ccfa, 0x01ccfc, PG_U_OTHER_SYMBOL}, {0x01cd00, 0x01ceb3, PG_U_OTHER_SYMBOL}, + {0x01ceba, 0x01ced0, PG_U_OTHER_SYMBOL}, + {0x01cee0, 0x01ceef, PG_U_OTHER_SYMBOL}, + {0x01cef0, 0x01cef0, PG_U_MATH_SYMBOL}, {0x01cf00, 0x01cf2d, PG_U_NONSPACING_MARK}, {0x01cf30, 0x01cf46, PG_U_NONSPACING_MARK}, {0x01cf50, 0x01cfc3, PG_U_OTHER_SYMBOL}, @@ -3957,6 +3986,17 @@ static const pg_category_range unicode_categories[3368] = {0x01e5f0, 0x01e5f0, PG_U_OTHER_LETTER}, {0x01e5f1, 0x01e5fa, PG_U_DECIMAL_NUMBER}, {0x01e5ff, 0x01e5ff, PG_U_OTHER_PUNCTUATION}, + {0x01e6c0, 0x01e6de, PG_U_OTHER_LETTER}, + {0x01e6e0, 0x01e6e2, PG_U_OTHER_LETTER}, + {0x01e6e3, 0x01e6e3, PG_U_NONSPACING_MARK}, + {0x01e6e4, 0x01e6e5, PG_U_OTHER_LETTER}, + {0x01e6e6, 0x01e6e6, PG_U_NONSPACING_MARK}, + {0x01e6e7, 0x01e6ed, PG_U_OTHER_LETTER}, + {0x01e6ee, 0x01e6ef, PG_U_NONSPACING_MARK}, + {0x01e6f0, 0x01e6f4, PG_U_OTHER_LETTER}, + {0x01e6f5, 0x01e6f5, PG_U_NONSPACING_MARK}, + {0x01e6fe, 0x01e6fe, PG_U_OTHER_LETTER}, + {0x01e6ff, 0x01e6ff, PG_U_MODIFIER_LETTER}, {0x01e7e0, 0x01e7e6, PG_U_OTHER_LETTER}, {0x01e7e8, 0x01e7eb, PG_U_OTHER_LETTER}, {0x01e7ed, 0x01e7ee, PG_U_OTHER_LETTER}, @@ -4027,11 +4067,10 @@ static const pg_category_range unicode_categories[3368] = {0x01f260, 0x01f265, PG_U_OTHER_SYMBOL}, {0x01f300, 0x01f3fa, PG_U_OTHER_SYMBOL}, {0x01f3fb, 0x01f3ff, PG_U_MODIFIER_SYMBOL}, - {0x01f400, 0x01f6d7, PG_U_OTHER_SYMBOL}, + {0x01f400, 0x01f6d8, PG_U_OTHER_SYMBOL}, {0x01f6dc, 0x01f6ec, PG_U_OTHER_SYMBOL}, {0x01f6f0, 0x01f6fc, PG_U_OTHER_SYMBOL}, - {0x01f700, 0x01f776, PG_U_OTHER_SYMBOL}, - {0x01f77b, 0x01f7d9, PG_U_OTHER_SYMBOL}, + {0x01f700, 0x01f7d9, PG_U_OTHER_SYMBOL}, {0x01f7e0, 0x01f7eb, PG_U_OTHER_SYMBOL}, {0x01f7f0, 0x01f7f0, PG_U_OTHER_SYMBOL}, {0x01f800, 0x01f80b, PG_U_OTHER_SYMBOL}, @@ -4041,26 +4080,28 @@ static const pg_category_range unicode_categories[3368] = {0x01f890, 0x01f8ad, PG_U_OTHER_SYMBOL}, {0x01f8b0, 0x01f8bb, PG_U_OTHER_SYMBOL}, {0x01f8c0, 0x01f8c1, PG_U_OTHER_SYMBOL}, - {0x01f900, 0x01fa53, PG_U_OTHER_SYMBOL}, + {0x01f8d0, 0x01f8d8, PG_U_MATH_SYMBOL}, + {0x01f900, 0x01fa57, PG_U_OTHER_SYMBOL}, {0x01fa60, 0x01fa6d, PG_U_OTHER_SYMBOL}, {0x01fa70, 0x01fa7c, PG_U_OTHER_SYMBOL}, - {0x01fa80, 0x01fa89, PG_U_OTHER_SYMBOL}, - {0x01fa8f, 0x01fac6, PG_U_OTHER_SYMBOL}, - {0x01face, 0x01fadc, PG_U_OTHER_SYMBOL}, - {0x01fadf, 0x01fae9, PG_U_OTHER_SYMBOL}, - {0x01faf0, 0x01faf8, PG_U_OTHER_SYMBOL}, + {0x01fa80, 0x01fa8a, PG_U_OTHER_SYMBOL}, + {0x01fa8e, 0x01fac6, PG_U_OTHER_SYMBOL}, + {0x01fac8, 0x01fac8, PG_U_OTHER_SYMBOL}, + {0x01facd, 0x01fadc, PG_U_OTHER_SYMBOL}, + {0x01fadf, 0x01faea, PG_U_OTHER_SYMBOL}, + {0x01faef, 0x01faf8, PG_U_OTHER_SYMBOL}, {0x01fb00, 0x01fb92, PG_U_OTHER_SYMBOL}, {0x01fb94, 0x01fbef, PG_U_OTHER_SYMBOL}, {0x01fbf0, 0x01fbf9, PG_U_DECIMAL_NUMBER}, + {0x01fbfa, 0x01fbfa, PG_U_OTHER_SYMBOL}, {0x020000, 0x02a6df, PG_U_OTHER_LETTER}, - {0x02a700, 0x02b739, PG_U_OTHER_LETTER}, - {0x02b740, 0x02b81d, PG_U_OTHER_LETTER}, - {0x02b820, 0x02cea1, PG_U_OTHER_LETTER}, + {0x02a700, 0x02b81d, PG_U_OTHER_LETTER}, + {0x02b820, 0x02cead, PG_U_OTHER_LETTER}, {0x02ceb0, 0x02ebe0, PG_U_OTHER_LETTER}, {0x02ebf0, 0x02ee5d, PG_U_OTHER_LETTER}, {0x02f800, 0x02fa1d, PG_U_OTHER_LETTER}, {0x030000, 0x03134a, PG_U_OTHER_LETTER}, - {0x031350, 0x0323af, PG_U_OTHER_LETTER}, + {0x031350, 0x033479, PG_U_OTHER_LETTER}, {0x0e0001, 0x0e0001, PG_U_FORMAT}, {0x0e0020, 0x0e007f, PG_U_FORMAT}, {0x0e0100, 0x0e01ef, PG_U_NONSPACING_MARK}, @@ -4069,7 +4110,7 @@ static const pg_category_range unicode_categories[3368] = }; /* table of Unicode codepoint ranges of Alphabetic characters */ -static const pg_unicode_range unicode_alphabetic[1179] = +static const pg_unicode_range unicode_alphabetic[1202] = { {0x000041, 0x00005a}, {0x000061, 0x00007a}, @@ -4083,8 +4124,8 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x0001bc, 0x0001bf}, {0x0001c0, 0x0001c3}, {0x0001c4, 0x000293}, - {0x000294, 0x000294}, - {0x000295, 0x0002af}, + {0x000294, 0x000295}, + {0x000296, 0x0002af}, {0x0002b0, 0x0002c1}, {0x0002c6, 0x0002d1}, {0x0002e0, 0x0002e4}, @@ -4154,7 +4195,7 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x000840, 0x000858}, {0x000860, 0x00086a}, {0x000870, 0x000887}, - {0x000889, 0x00088e}, + {0x000889, 0x00088f}, {0x000897, 0x000897}, {0x0008a0, 0x0008c8}, {0x0008c9, 0x0008c9}, @@ -4287,7 +4328,7 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x000c4a, 0x000c4c}, {0x000c55, 0x000c56}, {0x000c58, 0x000c5a}, - {0x000c5d, 0x000c5d}, + {0x000c5c, 0x000c5d}, {0x000c60, 0x000c61}, {0x000c62, 0x000c63}, {0x000c80, 0x000c80}, @@ -4307,7 +4348,7 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x000cca, 0x000ccb}, {0x000ccc, 0x000ccc}, {0x000cd5, 0x000cd6}, - {0x000cdd, 0x000cde}, + {0x000cdc, 0x000cde}, {0x000ce0, 0x000ce1}, {0x000ce2, 0x000ce3}, {0x000cf1, 0x000cf2}, @@ -4643,11 +4684,8 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x00a788, 0x00a788}, {0x00a78b, 0x00a78e}, {0x00a78f, 0x00a78f}, - {0x00a790, 0x00a7cd}, - {0x00a7d0, 0x00a7d1}, - {0x00a7d3, 0x00a7d3}, - {0x00a7d5, 0x00a7dc}, - {0x00a7f2, 0x00a7f4}, + {0x00a790, 0x00a7dc}, + {0x00a7f1, 0x00a7f4}, {0x00a7f5, 0x00a7f6}, {0x00a7f7, 0x00a7f7}, {0x00a7f8, 0x00a7f9}, @@ -4830,6 +4868,7 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x0108f4, 0x0108f5}, {0x010900, 0x010915}, {0x010920, 0x010939}, + {0x010940, 0x010959}, {0x010980, 0x0109b7}, {0x0109be, 0x0109bf}, {0x010a00, 0x010a00}, @@ -4863,7 +4902,9 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x010eab, 0x010eac}, {0x010eb0, 0x010eb1}, {0x010ec2, 0x010ec4}, - {0x010efc, 0x010efc}, + {0x010ec5, 0x010ec5}, + {0x010ec6, 0x010ec7}, + {0x010efa, 0x010efc}, {0x010f00, 0x010f1c}, {0x010f27, 0x010f27}, {0x010f30, 0x010f45}, @@ -5049,6 +5090,12 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x011a97, 0x011a97}, {0x011a9d, 0x011a9d}, {0x011ab0, 0x011af8}, + {0x011b60, 0x011b60}, + {0x011b61, 0x011b61}, + {0x011b62, 0x011b64}, + {0x011b65, 0x011b65}, + {0x011b66, 0x011b66}, + {0x011b67, 0x011b67}, {0x011bc0, 0x011be0}, {0x011c00, 0x011c08}, {0x011c0a, 0x011c2e}, @@ -5084,6 +5131,9 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x011d95, 0x011d95}, {0x011d96, 0x011d96}, {0x011d98, 0x011d98}, + {0x011db0, 0x011dd8}, + {0x011dd9, 0x011dd9}, + {0x011dda, 0x011ddb}, {0x011ee0, 0x011ef2}, {0x011ef3, 0x011ef4}, {0x011ef5, 0x011ef6}, @@ -5121,6 +5171,8 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x016d43, 0x016d6a}, {0x016d6b, 0x016d6c}, {0x016e40, 0x016e7f}, + {0x016ea0, 0x016eb8}, + {0x016ebb, 0x016ed3}, {0x016f00, 0x016f4a}, {0x016f4f, 0x016f4f}, {0x016f50, 0x016f50}, @@ -5130,9 +5182,11 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x016fe0, 0x016fe1}, {0x016fe3, 0x016fe3}, {0x016ff0, 0x016ff1}, - {0x017000, 0x0187f7}, - {0x018800, 0x018cd5}, - {0x018cff, 0x018d08}, + {0x016ff2, 0x016ff3}, + {0x016ff4, 0x016ff6}, + {0x017000, 0x018cd5}, + {0x018cff, 0x018d1e}, + {0x018d80, 0x018df2}, {0x01aff0, 0x01aff3}, {0x01aff5, 0x01affb}, {0x01affd, 0x01affe}, @@ -5197,6 +5251,17 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x01e4eb, 0x01e4eb}, {0x01e5d0, 0x01e5ed}, {0x01e5f0, 0x01e5f0}, + {0x01e6c0, 0x01e6de}, + {0x01e6e0, 0x01e6e2}, + {0x01e6e3, 0x01e6e3}, + {0x01e6e4, 0x01e6e5}, + {0x01e6e6, 0x01e6e6}, + {0x01e6e7, 0x01e6ed}, + {0x01e6ee, 0x01e6ef}, + {0x01e6f0, 0x01e6f4}, + {0x01e6f5, 0x01e6f5}, + {0x01e6fe, 0x01e6fe}, + {0x01e6ff, 0x01e6ff}, {0x01e7e0, 0x01e7e6}, {0x01e7e8, 0x01e7eb}, {0x01e7ed, 0x01e7ee}, @@ -5242,18 +5307,17 @@ static const pg_unicode_range unicode_alphabetic[1179] = {0x01f150, 0x01f169}, {0x01f170, 0x01f189}, {0x020000, 0x02a6df}, - {0x02a700, 0x02b739}, - {0x02b740, 0x02b81d}, - {0x02b820, 0x02cea1}, + {0x02a700, 0x02b81d}, + {0x02b820, 0x02cead}, {0x02ceb0, 0x02ebe0}, {0x02ebf0, 0x02ee5d}, {0x02f800, 0x02fa1d}, {0x030000, 0x03134a}, - {0x031350, 0x0323af}, + {0x031350, 0x033479}, }; /* table of Unicode codepoint ranges of Lowercase characters */ -static const pg_unicode_range unicode_lowercase[690] = +static const pg_unicode_range unicode_lowercase[692] = { {0x000061, 0x00007a}, {0x0000aa, 0x0000aa}, @@ -5402,7 +5466,7 @@ static const pg_unicode_range unicode_lowercase[690] = {0x00024b, 0x00024b}, {0x00024d, 0x00024d}, {0x00024f, 0x000293}, - {0x000295, 0x0002af}, + {0x000296, 0x0002af}, {0x0002b0, 0x0002b8}, {0x0002c0, 0x0002c1}, {0x0002e0, 0x0002e4}, @@ -5880,13 +5944,14 @@ static const pg_unicode_range unicode_lowercase[690] = {0x00a7c8, 0x00a7c8}, {0x00a7ca, 0x00a7ca}, {0x00a7cd, 0x00a7cd}, + {0x00a7cf, 0x00a7cf}, {0x00a7d1, 0x00a7d1}, {0x00a7d3, 0x00a7d3}, {0x00a7d5, 0x00a7d5}, {0x00a7d7, 0x00a7d7}, {0x00a7d9, 0x00a7d9}, {0x00a7db, 0x00a7db}, - {0x00a7f2, 0x00a7f4}, + {0x00a7f1, 0x00a7f4}, {0x00a7f6, 0x00a7f6}, {0x00a7f8, 0x00a7f9}, {0x00a7fa, 0x00a7fa}, @@ -5912,6 +5977,7 @@ static const pg_unicode_range unicode_lowercase[690] = {0x010d70, 0x010d85}, {0x0118c0, 0x0118df}, {0x016e60, 0x016e7f}, + {0x016ebb, 0x016ed3}, {0x01d41a, 0x01d433}, {0x01d44e, 0x01d454}, {0x01d456, 0x01d467}, @@ -5948,7 +6014,7 @@ static const pg_unicode_range unicode_lowercase[690] = }; /* table of Unicode codepoint ranges of Uppercase characters */ -static const pg_unicode_range unicode_uppercase[656] = +static const pg_unicode_range unicode_uppercase[660] = { {0x000041, 0x00005a}, {0x0000c0, 0x0000d6}, @@ -6554,7 +6620,10 @@ static const pg_unicode_range unicode_uppercase[656] = {0x00a7c4, 0x00a7c7}, {0x00a7c9, 0x00a7c9}, {0x00a7cb, 0x00a7cc}, + {0x00a7ce, 0x00a7ce}, {0x00a7d0, 0x00a7d0}, + {0x00a7d2, 0x00a7d2}, + {0x00a7d4, 0x00a7d4}, {0x00a7d6, 0x00a7d6}, {0x00a7d8, 0x00a7d8}, {0x00a7da, 0x00a7da}, @@ -6571,6 +6640,7 @@ static const pg_unicode_range unicode_uppercase[656] = {0x010d50, 0x010d65}, {0x0118a0, 0x0118bf}, {0x016e40, 0x016e5f}, + {0x016ea0, 0x016eb8}, {0x01d400, 0x01d419}, {0x01d434, 0x01d44d}, {0x01d468, 0x01d481}, @@ -6609,7 +6679,7 @@ static const pg_unicode_range unicode_uppercase[656] = }; /* table of Unicode codepoint ranges of Case_Ignorable characters */ -static const pg_unicode_range unicode_case_ignorable[506] = +static const pg_unicode_range unicode_case_ignorable[518] = { {0x000027, 0x000027}, {0x00002e, 0x00002e}, @@ -6807,7 +6877,8 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x001aa7, 0x001aa7}, {0x001ab0, 0x001abd}, {0x001abe, 0x001abe}, - {0x001abf, 0x001ace}, + {0x001abf, 0x001add}, + {0x001ae0, 0x001aeb}, {0x001b00, 0x001b03}, {0x001b34, 0x001b34}, {0x001b36, 0x001b3a}, @@ -6887,7 +6958,7 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x00a770, 0x00a770}, {0x00a788, 0x00a788}, {0x00a789, 0x00a78a}, - {0x00a7f2, 0x00a7f4}, + {0x00a7f1, 0x00a7f4}, {0x00a7f8, 0x00a7f9}, {0x00a802, 0x00a802}, {0x00a806, 0x00a806}, @@ -6963,7 +7034,8 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x010d69, 0x010d6d}, {0x010d6f, 0x010d6f}, {0x010eab, 0x010eac}, - {0x010efc, 0x010eff}, + {0x010ec5, 0x010ec5}, + {0x010efa, 0x010eff}, {0x010f46, 0x010f50}, {0x010f82, 0x010f85}, {0x011001, 0x011001}, @@ -7040,6 +7112,9 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x011a59, 0x011a5b}, {0x011a8a, 0x011a96}, {0x011a98, 0x011a99}, + {0x011b60, 0x011b60}, + {0x011b62, 0x011b64}, + {0x011b66, 0x011b66}, {0x011c30, 0x011c36}, {0x011c38, 0x011c3d}, {0x011c3f, 0x011c3f}, @@ -7055,6 +7130,7 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x011d90, 0x011d91}, {0x011d95, 0x011d95}, {0x011d97, 0x011d97}, + {0x011dd9, 0x011dd9}, {0x011ef3, 0x011ef4}, {0x011f00, 0x011f01}, {0x011f36, 0x011f3a}, @@ -7077,6 +7153,7 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x016fe0, 0x016fe1}, {0x016fe3, 0x016fe3}, {0x016fe4, 0x016fe4}, + {0x016ff2, 0x016ff3}, {0x01aff0, 0x01aff3}, {0x01aff5, 0x01affb}, {0x01affd, 0x01affe}, @@ -7110,6 +7187,11 @@ static const pg_unicode_range unicode_case_ignorable[506] = {0x01e4eb, 0x01e4eb}, {0x01e4ec, 0x01e4ef}, {0x01e5ee, 0x01e5ef}, + {0x01e6e3, 0x01e6e3}, + {0x01e6e6, 0x01e6e6}, + {0x01e6ee, 0x01e6ef}, + {0x01e6f5, 0x01e6f5}, + {0x01e6ff, 0x01e6ff}, {0x01e8d0, 0x01e8d6}, {0x01e944, 0x01e94a}, {0x01e94b, 0x01e94b}, diff --git a/src/include/common/unicode_east_asian_fw_table.h b/src/include/common/unicode_east_asian_fw_table.h index db8bd0ad89779..48c239f28b1b7 100644 --- a/src/include/common/unicode_east_asian_fw_table.h +++ b/src/include/common/unicode_east_asian_fw_table.h @@ -61,10 +61,10 @@ static const struct mbinterval east_asian_fw[] = { {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4}, - {0x16FF0, 0x16FF1}, - {0x17000, 0x187F7}, - {0x18800, 0x18CD5}, - {0x18CFF, 0x18D08}, + {0x16FF0, 0x16FF6}, + {0x17000, 0x18CD5}, + {0x18CFF, 0x18D1E}, + {0x18D80, 0x18DF2}, {0x1AFF0, 0x1AFF3}, {0x1AFF5, 0x1AFFB}, {0x1AFFD, 0x1AFFE}, @@ -106,7 +106,7 @@ static const struct mbinterval east_asian_fw[] = { {0x1F680, 0x1F6C5}, {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, - {0x1F6D5, 0x1F6D7}, + {0x1F6D5, 0x1F6D8}, {0x1F6DC, 0x1F6DF}, {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, @@ -116,11 +116,12 @@ static const struct mbinterval east_asian_fw[] = { {0x1F93C, 0x1F945}, {0x1F947, 0x1F9FF}, {0x1FA70, 0x1FA7C}, - {0x1FA80, 0x1FA89}, - {0x1FA8F, 0x1FAC6}, - {0x1FACE, 0x1FADC}, - {0x1FADF, 0x1FAE9}, - {0x1FAF0, 0x1FAF8}, + {0x1FA80, 0x1FA8A}, + {0x1FA8E, 0x1FAC6}, + {0x1FAC8, 0x1FAC8}, + {0x1FACD, 0x1FADC}, + {0x1FADF, 0x1FAEA}, + {0x1FAEF, 0x1FAF8}, {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD}, }; diff --git a/src/include/common/unicode_nonspacing_table.h b/src/include/common/unicode_nonspacing_table.h index d67f5b3f281d3..dec117c0c258d 100644 --- a/src/include/common/unicode_nonspacing_table.h +++ b/src/include/common/unicode_nonspacing_table.h @@ -212,7 +212,7 @@ static const struct mbinterval nonspacing[] = { {0x10D24, 0x10D27}, {0x10D69, 0x10D6D}, {0x10EAB, 0x10EAC}, - {0x10EFC, 0x10EFF}, + {0x10EFA, 0x10EFF}, {0x10F46, 0x10F50}, {0x10F82, 0x10F85}, {0x11001, 0x11001}, @@ -286,6 +286,9 @@ static const struct mbinterval nonspacing[] = { {0x11A59, 0x11A5B}, {0x11A8A, 0x11A96}, {0x11A98, 0x11A99}, + {0x11B60, 0x11B60}, + {0x11B62, 0x11B64}, + {0x11B66, 0x11B66}, {0x11C30, 0x11C3D}, {0x11C3F, 0x11C3F}, {0x11C92, 0x11CA7}, @@ -332,6 +335,10 @@ static const struct mbinterval nonspacing[] = { {0x1E2EC, 0x1E2EF}, {0x1E4EC, 0x1E4EF}, {0x1E5EE, 0x1E5EF}, + {0x1E6E3, 0x1E6E3}, + {0x1E6E6, 0x1E6E6}, + {0x1E6EE, 0x1E6EF}, + {0x1E6F5, 0x1E6F5}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A}, {0xE0001, 0xE01EF}, diff --git a/src/include/common/unicode_norm.h b/src/include/common/unicode_norm.h index 5bc3b79e78e08..46e162618a373 100644 --- a/src/include/common/unicode_norm.h +++ b/src/include/common/unicode_norm.h @@ -5,7 +5,7 @@ * * These definitions are used by both frontend and backend code. * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/common/unicode_norm.h * @@ -14,8 +14,6 @@ #ifndef UNICODE_NORM_H #define UNICODE_NORM_H -#include "mb/pg_wchar.h" - typedef enum { UNICODE_NFC = 0, @@ -32,8 +30,8 @@ typedef enum UNICODE_NORM_QC_MAYBE = -1, } UnicodeNormalizationQC; -extern pg_wchar *unicode_normalize(UnicodeNormalizationForm form, const pg_wchar *input); +extern char32_t *unicode_normalize(UnicodeNormalizationForm form, const char32_t *input); -extern UnicodeNormalizationQC unicode_is_normalized_quickcheck(UnicodeNormalizationForm form, const pg_wchar *input); +extern UnicodeNormalizationQC unicode_is_normalized_quickcheck(UnicodeNormalizationForm form, const char32_t *input); #endif /* UNICODE_NORM_H */ diff --git a/src/include/common/unicode_norm_hashfunc.h b/src/include/common/unicode_norm_hashfunc.h index 89e04c744d913..a0b0e99adc067 100644 --- a/src/include/common/unicode_norm_hashfunc.h +++ b/src/include/common/unicode_norm_hashfunc.h @@ -3,7 +3,7 @@ * unicode_norm_hashfunc.h * Perfect hash functions used for Unicode normalization * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_norm_hashfunc.h @@ -41,344 +41,730 @@ typedef struct static int Decomp_hash_func(const void *key) { - static const int16 h[13687] = { + static const int16 h[13757] = { + 45, 32767, 157, 158, 159, 160, 161, 2883, + 52, 53, 54, 0, 412, -233, -232, -231, + -230, 60, 0, 0, -238, -238, -238, -238, + -238, 240, 241, 242, -342, -342, 822, 822, + 822, 822, 822, 822, 0, 0, 824, 824, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 267, 32767, 32767, 0, + 0, 32767, 0, 0, 32767, 32767, 32767, 4434, + 4435, -300, -300, -300, -300, -300, -300, 278, + 279, 280, 281, 282, 283, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 32767, 32767, 32767, 32767, 0, - 0, 0, 0, 32767, 0, 0, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 0, 0, 0, 32767, 0, 32767, 0, 32767, - 0, 32767, 0, 32767, 0, 32767, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1443, 1444, 1445, + -340, 1446, -340, 1447, 0, 0, 0, 32767, + 0, 0, 0, 0, 0, 0, 32767, 32767, + 216, 217, 218, 219, 0, -1058, 222, -363, + 0, 0, -259, 227, 228, 2942, 2943, 2944, + 32767, 32767, 232, 233, 234, 235, 236, 237, + -7938, 2075, 2076, 2077, 2078, 2079, 2080, 2081, + -4642, -4642, -4642, 2085, 2086, -4643, -4643, -4643, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -323, -323, + 14, 15, 321, -322, -322, -322, -322, -322, + -322, 22, 23, -322, 0, -321, -321, -321, + 0, 29, 30, -320, -320, -320, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + -112, 325, -112, -112, -112, 0, 0, -111, + -111, -111, -111, -111, -2832, 0, 0, 0, + 0, -357, 289, 289, 289, 289, 0, 0, + 0, 299, 300, 301, 302, 303, 0, -175, + 0, -278, -278, -278, -3885, -3885, -3885, -3885, + -3885, -3885, -3885, -3885, -3885, -3885, -3885, -3885, + -3885, 32767, 0, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, -4544, 99, 100, 101, 102, 0, + 0, 103, 103, 105, 104, 107, 108, 109, + 110, 108, 32767, 112, 109, 109, 109, 109, + 117, 32767, 110, 110, 110, 121, 111, 111, + -212, 125, 429, 430, 431, -212, -212, -212, + 130, -211, -211, 32767, 32767, -213, 109, -212, + -212, -212, 138, 32767, 32767, -212, -212, -212, + 108, 143, 144, 110, 110, 110, 110, 110, + 150, 151, 0, 437, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, -2721, 111, + 111, 111, 166, -245, 401, 401, 401, 401, + 112, 173, 174, 413, 414, 415, 416, 417, + -60, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 4492, 449, 0, 32767, 451, 452, 0, 32767, + 32767, 4490, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 0, 0, 0, 0, 0, 32767, + 32767, 181, 182, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + -97, -97, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 185, 186, 187, + 188, 189, 190, 191, 192, 193, 194, 195, + 196, 197, 198, 199, -1243, -1243, -1243, 543, + -1242, 545, -1241, 207, 208, 209, 32767, 210, + 211, 212, 213, 214, 215, 32767, 32767, 0, + 0, 0, 0, 220, 1279, 0, 586, 224, + 225, 485, 0, 0, -2713, -2713, -2713, 32767, + 32767, 0, 0, 0, 0, 0, 0, 8176, + -1836, 0, 0, 0, 0, 585, 586, -577, + -576, -575, -574, -573, -572, 251, 252, -571, + -570, 255, 256, 257, 258, 259, 260, 261, + 262, 263, 264, 265, 266, 0, 32767, 32767, + 268, 269, 32767, 32767, 32767, 7164, -515, -6053, + -4164, -4164, 572, 573, 574, 575, 576, 577, + 0, 0, 0, 0, 0, 0, 0, 578, + 579, 580, 577, 578, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 32767, 32767, 32767, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 32767, 32767, 0, 32767, 32767, 0, 0, 0, - 0, 32767, 32767, 32767, 0, 0, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 32767, 0, 32767, 0, + 0, 32767, -379, -379, 32767, 0, 32767, 32767, + 28, 29, 30, 31, 3174, 33, 34, 5578, + 36, 37, 38, 39, 40, 41, 42, 43, + 1899, 1900, 1901, 47, 48, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 4693, 4694, 4695, 4696, 50, 32767, 32767, + 32767, 32767, 51, 52, 53, 54, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 627, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 262, 262, 608, 287, 609, 610, + 611, 291, 263, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 628, 629, 630, 631, + 632, 633, 634, 635, 636, 637, 638, 639, + 640, 641, 642, 643, 644, 645, 646, 647, + 648, 649, 650, 5, 6, 7, 8, 298, + 32767, 32767, 0, 0, 0, 0, 0, 0, + -239, 0, 32767, 608, 609, 610, 611, 612, + 2970, 614, -1770, -1770, 615, 616, 617, 618, + 619, 620, -1770, -1770, 621, 622, -1770, 623, + 624, 625, 626, 8242, -1770, -1770, -1770, -1770, + -1770, -1770, -1770, 4954, 4955, 4956, -1770, -1770, + 4960, 4961, 4962, 320, 321, 322, 323, 324, + 325, 326, 327, 328, 329, 330, 331, 332, + 333, 334, 335, 336, 337, 338, 339, 340, + 341, 665, 666, 330, 330, 25, 669, 670, + 671, 672, 673, 674, 331, 331, 677, 356, + 678, 679, 680, 360, 332, 332, 683, 684, + 685, 366, 367, 368, 369, 370, 371, 372, + 373, 374, 375, 488, 52, 490, 491, 492, + 381, 382, 32767, 494, 495, 496, 497, 3219, + 388, 389, 390, 391, 749, 104, 105, 106, + 107, 397, 398, 399, 101, 101, 101, 101, + 101, 405, 581, 407, 686, 687, 688, 4296, + 4297, 4298, 4299, 32767, 32767, 32767, 32767, 4300, + 32767, 32767, 32767, 32767, 32767, 416, 32767, 655, + 656, 331, 658, 659, 660, 661, 662, 326, + 326, 326, 326, 326, 326, 4968, 32767, 325, + 32767, 324, 427, 428, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, -3708, 0, 0, 0, 0, 0, + 644, 645, 646, 305, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, -4570, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 0, 32767, 32767, 0, 32767, + 32767, 32767, 32767, 32767, 32767, 436, 0, 438, + 439, 440, 32767, 441, 442, 443, 444, 445, + 446, 3168, 32767, 32767, 32767, 0, -4059, -4059, + -4059, -4059, -4059, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 3570, -4045, 5968, -4044, 0, 450, 0, 0, + 0, 0, 0, 0, -4037, 0, 0, -4035, + 0, 0, 0, 0, 367, 367, 0, 366, + 0, 4702, 4703, 363, 0, 0, 0, 0, + 359, 359, 359, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 351, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 6024, 6025, 6026, 6027, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 674, 32767, 32767, + 32767, 32767, 32767, 32767, 776, 32767, 32767, 0, + 32767, 32767, 32767, 32767, 32767, 0, 0, 32767, + 32767, 32767, 32767, 32767, 463, 464, 32767, 465, + 3185, 3186, 32767, 3187, 3188, -1553, 0, 0, + 712, 67, 68, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 0, 0, 0, 32767, 0, 0, + 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 18, 471, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 472, + 473, 474, 475, 476, 32767, 32767, 32767, 32767, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 4709, 4710, 4711, + 4712, 4713, 4714, 4715, 9329, 4717, 4718, 4719, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 1719, -67, 32767, + 32767, 32767, 32767, 32767, 0, 0, 32767, 32767, + 32767, 32767, 32767, 32767, 479, 480, 481, 482, + 0, 32767, 483, -102, 0, 0, 0, 486, + 487, 3201, 3202, 3203, 3204, 32767, 491, 492, + 493, 494, 495, 496, 0, 0, 497, 498, + 499, 500, -84, -84, 1080, 1080, 1080, 1080, + 1080, 1080, 1080, 1080, 1080, 1080, 32767, 32767, + 32767, 1077, 1077, 1077, 1077, 1077, 1077, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1069, + 1069, 1069, 1069, 1069, 1069, 1069, 1069, 32767, + 32767, 1950, 1067, 1067, 1067, 1067, 1067, 1067, + 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 6028, + 4805, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 1037, 1037, + 1037, 1037, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 798, 32767, 32767, 32767, 1024, + 1024, 1024, 1024, 1024, 1024, 895, 895, 799, + 32767, 32767, 0, 0, 800, 0, 32767, 32767, + 32767, 32767, 32767, 32767, 801, -373, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 1, 32767, 2, 32767, 32767, 32767, 32767, 2792, - 32767, 32767, 4, 5, 6, 7, 32767, 32767, - 8, 9, 10, 32767, 11, 12, 13, 1734, - 14, -822, -822, 17, 18, 3020, 1739, 3021, - 3022, 3023, 1110, 24, 3024, 3025, 27, -3019, - 32767, 29, 30, 3026, 3027, 33, 34, 5016, - 1754, -4252, 36, 37, -4254, -4254, -4254, 32767, - 40, 41, 42, 43, 44, 45, 1912, 46, - 47, 48, 49, 50, 51, 52, 53, 54, - 1770, 55, 606, 57, 58, 59, 60, 1776, - 1777, 61, 62, 63, 64, 65, 32767, 66, - 32767, -1290, 6662, 3534, 3534, 3534, -172, 3322, - -170, -169, -168, -167, 3331, 1788, 0, 1790, - 1791, 1792, 1793, 1794, 1795, 1796, 1797, 1798, - 1799, 3354, 1801, 1802, -964, -964, -964, -964, - 1807, 4976, 4976, 4976, 4976, 4976, 4976, 1814, - 1815, 4974, 2192, 11244, 2194, 11245, 0, 0, - 1605, 11251, 0, 11252, 0, 0, 3571, 3571, - -47, -47, 0, 0, -124, -123, -122, -4877, - -4877, -119, 32767, 32767, -118, 0, -116, -11032, - -114, -113, -112, 32767, 32767, -887, -110, -109, - -885, -107, 5320, 32767, 32767, -105, -882, 5328, - -102, -101, -100, -99, -4221, -4221, -4221, -95, - -94, -93, -92, -4221, -4221, -4221, -4221, 32767, - 32767, -4223, -4223, -4223, -4223, -4223, -4223, -4223, - -4223, -5481, -4223, -4223, -4223, -4223, -4223, -4223, - -4223, -4223, -4223, -4223, -4223, -4223, -4223, -4223, - -5495, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 1839, 1840, 1841, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 4663, 4664, 32767, - 32767, -4255, -4255, 32767, 0, 0, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - -4268, -4268, 5881, 5882, 4722, 5884, 5885, 5886, - 5887, 32767, 5888, 5889, 5890, 5891, 5892, 5893, - 5894, 5895, 5896, 5897, 5898, 185, 186, -3976, - -3976, -3976, -3976, 6000, 6001, 6002, 6003, 6004, - 6005, 6006, 198, 5911, -3968, -3968, -826, -3968, - -3968, -3968, -3968, -3968, 208, 209, 32767, -3967, - -3967, -3032, -3967, -3967, -3030, 5922, 5923, -813, - 5925, 5926, 219, 220, 5929, 5930, 5931, 224, - -4290, -4290, -4290, -4290, -4290, -4290, -4290, 5940, - 5941, -4292, -4292, -4292, -4292, -4292, -4292, -4292, - -4292, 5950, 5951, 0, 0, 0, 0, 245, - 0, 0, 0, 0, 0, 0, 0, 253, - 254, 255, 256, -635, 258, 259, 260, 261, - 262, 263, 0, 265, 266, 267, 0, 0, - 268, 269, 32767, 32767, 32767, 32767, 32767, 32767, - 270, 271, 272, 273, 274, 275, 276, 277, - 278, 279, 280, 281, 282, 283, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 5981, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 511, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 6565, 4677, + 4678, -57, -57, -57, -57, -57, -57, 521, + 522, 523, 524, 525, 526, 527, -50, -50, + -50, -46, -46, 533, 534, 535, 536, 537, + 538, 539, 540, 541, 542, 543, 544, 545, + 546, 547, 548, 549, 550, 551, 552, 553, + 554, 555, 556, 32767, 557, 32767, 558, 559, + 32767, 939, 940, -3772, 562, 32767, 32767, -3771, + 4725, 32767, 32767, 32767, 32767, 32767, -3771, -3770, + -5249, -5249, 2900, -5249, -3771, 32767, 2997, -1310, + 32767, 32767, 32767, 2998, 2999, 3000, 3001, 3002, + 3003, 3004, 3005, 3006, 3007, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 4727, 32767, 4728, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 2763, - 975, 32767, 32767, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 4729, 3009, 3010, 3011, 32767, + 32767, 6740, -4, 4734, -4, 6744, -6601, 1079, + 6618, 4730, 4731, -4, -4, -4, -4, -4, + -4, -4, -4, -321, -4, -4, -4, -4, + -4, -4, -4, 0, 0, 0, 0, 0, + 551, 551, 551, 551, 551, 551, 551, 551, + 551, 551, 551, 551, 551, 551, 551, 551, + 551, 551, 551, 5295, 5296, -9309, -1629, 551, + 551, 551, 551, 551, 551, 551, 551, 551, + 551, 551, 551, 551, 551, -2591, 551, 551, + -4992, 551, 551, 551, 551, 551, 551, 551, + 551, -1304, -1304, -1304, 551, 551, 32767, 550, + 550, 550, 550, 550, 550, 550, 550, 550, + 550, 550, 550, 550, 550, 550, 550, 550, + 550, 550, 550, 550, 550, 550, 550, 550, + 550, 550, 550, 550, 550, 550, 550, 550, + 550, 550, 550, 550, 550, 550, 550, 550, + 550, 8699, 550, 6744, 0, 4738, 0, 6748, + -6597, 1083, 6622, 4734, 4735, 0, 0, 0, + 0, 0, 0, 0, 0, -317, 0, 0, + 0, 0, 0, 0, 0, 550, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 5343, 0, 0, 5344, + 0, 0, 0, 0, 0, 0, -40, 0, + -41, 0, 542, 0, 0, 0, 0, 845, + 0, 0, 0, 0, 0, 833, 0, 835, + 836, 846, 0, 0, 0, 0, 0, 0, + 0, -2357, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 289, + 0, 0, 0, 0, 0, 0, 0, 931, + 586, 586, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 4382, 4382, 4382, 0, 7408, - 4383, 4383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -4027, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -1642, -1641, -1640, -1639, - -1638, 3694, -1636, 3697, 3698, 3699, 0, 0, - -1627, 0, 0, -1622, -1621, 0, 6198, -1616, - -1615, -1614, -1613, 0, -1610, -1609, -1608, 0, - 0, 0, 0, 0, 0, 9558, -10546,0, - 6270, 6271, 6272, 6273, 6274, 6275, -3369, 6277, - 6545, 6279, 9583, 6281, -1402, -1402, 6284, 6285, - 304, 1947, 1947, 1947, 1947, 1947, -3384, 1947, - -3385, -3385, -3385, 315, 316, 1944, 318, 319, - 1942, 1942, 322, -5875, 1940, 1940, 1940, 1940, - 328, 1939, 1939, 1939, 332, 333, 334, 335, - 336, 337, -9220, 10885, 340, -5929, -5929, -5929, - -5929, -5929, -5929, 3716, -5929, -6196, -5929, -9232, - -5929, 1755, 1756, -5929, -5929, 53, -1589, -1588, - -1587, -1586, -1585, 3747, -1583, 3750, 3751, 3752, - 53, 53, -1574, 53, 53, -1569, -1568, 53, - 6251, -1563, -1562, -1561, -1560, 53, -1557, 5101, - -1556, 52, 52, 52, 52, 52, 52, 9610, - -10494,52, 6322, 6323, 6324, 6325, 6326, 6327, - -3317, 6329, 6597, 6331, 9635, 6333, -1350, -1350, - 6336, 6337, 356, 1999, 1999, 1999, 1999, 1999, - 0, 0, -2173, -3888, -3337, -5616, 0, 0, - 432, -2164, 1985, 0, 0, 0, 1980, 0, - 0, 0, 0, -1319, -4683, 1975, 368, 369, - 370, 371, 372, 0, -9185, 0, 374, -5895, - -5895, 0, 0, 0, 0, 864, 437, -16, - -16, 440, 32767, 441, 442, 443, -13, 445, - 446, 447, 32767, 32767, 32767, 32767, 32767, 6042, - 6043, 6044, 6045, 430, 431, 0, 2597, -1551, - 435, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 448, 449, 450, 32767, 451, - 452, 32767, 32767, 32767, 453, 0, 0, 0, - 0, 0, -428, 0, 454, 455, 0, 456, - 0, 0, 0, 457, 0, 0, 0, 0, - 458, 459, 460, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 646, 646, + 646, 646, 646, 646, 646, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 3733, 3734, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 462, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 463, 464, 0, 465, - 0, 0, 0, 466, 0, 0, 0, 0, - 467, 0, 469, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 587, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 5493, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 5530, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -4339, -4339, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -7353, -7353, -7353, -5439, - 0, 0, -7353, -4354, 0, 0, 485, 486, - 487, 488, 489, 490, 0, 0, 491, 492, - 493, 494, -2541, -5542, 0, 0, -5544, -5544, - -3630, -2543, -5542, -5542, -2543, 504, -2543, -2543, - -2543, -5538, 0, 0, -2545, -2545, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 326, 0, 0, 0, + 0, 0, 337, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 340, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 337, 338, 0, 0, 0, 0, 0, 0, + 0, 344, 345, 0, 0, 0, 0, 0, + 0, 349, 350, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 3784, 3784, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 3033, 3034, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 6047, 6048, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 4395, 4396, 4397, + 690, 691, 692, 693, 694, 51, 51, 51, + 32767, 32767, 32767, 32767, 32767, 32767, 4748, 4749, + 4750, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 3045, 32767, 32767, + 32767, 4751, 4752, 4753, 4754, 11, 11, 14617, + 32767, 32767, 698, 4758, 4759, 4760, 4761, 4762, + 704, 705, 706, 707, 708, 709, 710, 711, + 712, 713, 714, 715, 716, -2853, 4763, -5249, + 4764, 721, 32767, 722, 723, 724, 725, 726, + 727, 4765, 729, 730, 4766, 732, 733, 734, + 735, 369, 370, 738, 373, 740, -3961, -3961, + 380, 744, 745, 746, 747, 389, 390, 391, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 8673, 32767, 32767, + 5317, 32767, 5318, 5319, 32767, 32767, 32767, 32767, + 32767, 5320, 5321, 5322, 32767, 32767, 32767, 32767, + 32767, 32767, 752, 32767, 32767, 753, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 754, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 4814, 32767, + 32767, 32767, 756, 757, 758, 759, 32767, 32767, + 32767, 760, 761, 762, 763, 764, 765, -2804, + 4812, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, -1299, 32767, + 32767, 32767, 5324, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3448, - 3448, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 5320, 32767, 32767, 768, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2322, 770, 771, 32767, + 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 772, 773, 32767, + 774, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 775, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, -3213, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 2540, 2540, - 2540, 2540, 2540, 0, 4830, 4831, -307, -307, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, -3373, + 4777, 32767, -1417, 8191, 32767, 8547, 11926, 4247, + 32767, 32767, 8550, 8551, 32767, 32767, 32767, 32767, + 32767, 8552, 8553, 32767, 8554, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, + 777, 32767, 32767, 32767, 32767, 32767, 778, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 0, 0, 0, 0, 0, 779, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 780, 781, 782, 32767, 32767, + 783, 8555, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 887, 888, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 2537, 2537, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 889, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 784, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 8556, 8557, 8558, 8559, 32767, 32767, 32767, 785, + 32767, 32767, 32767, 32767, 32767, 8560, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4439, + 4785, 32767, 32767, 32767, 4821, -1372, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 786, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 787, 32767, 32767, 890, 789, + 790, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 891, 32767, 32767, 32767, 32767, 32767, 791, + 792, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 793, + 32767, 32767, 32767, 0, 32767, -3326, 32767, 32767, + 32767, 32767, 32767, 32767, 892, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 893, 894, 895, 0, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 794, 795, -4, + 797, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 0, 0, + 0, 0, 0, 0, 32767, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 32767, + 0, 0, 32767, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1171, 1171, 1171, 0, 0, + 0, 0, 1175, 5650, 5650, 5650, 2043, 2043, + 2043, 2043, 2043, 2043, 2043, 2043, 2043, 2043, + 2043, 2043, 2043, 2043, 2043, 0, 2044, 2044, + 2044, 2044, 2044, 2044, 2044, 2044, 2044, 2044, + 2044, 2044, 2044, 2044, 2044, 2044, 2044, 2044, + 2044, 32767, 2043, 2043, 2043, 2043, 2043, 2043, + 2043, 2043, 2043, 32767, 0, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 5425, 5426, 5427, 4564, + 6340, 5429, 5430, 6343, 6344, 4849, 4567, 5436, + 5437, 4567, 6350, 6351, 4569, 4569, 4569, 6355, + 4570, 6357, 4571, 6359, 5443, 6361, 4573, 4573, + 4573, 4573, 6366, 4574, 4574, 4574, 6370, 4575, + 4575, 4575, 4575, 5448, 4575, 4575, 6378, 4576, + 5450, 4576, 4576, 4576, 4576, 6385, 6056, -2092, + 6388, 4580, 4580, 4580, 4580, 4580, 6394, 6395, + 6396, 6397, 6398, 6399, 4586, 4586, 4586, 4586, + 4586, 4586, 4586, 4586, 4586, 4586, 4586, 4817, + 4586, 4586, 4586, 4586, 4586, 4586, 4586, 4586, + 4586, 4586, 4586, 4586, 10620, 4586, 4586, 10621, + 4586, 4586, 4586, 4586, 4586, 4586, 4586, 10067, + 10068, 4586, 4586, 4586, 4586, 4586, 10069, 4586, + 4586, 4586, 4586, 4586, 6448, 6449, 6450, 6451, + 6452, 6453, 6454, 6455, 6456, 9393, 6458, 6459, + 6460, 6461, 6462, 5983, 6464, 5984, 6466, 5985, + -129, -129, -129, 0, -129, -129, 2253, 4611, + 2255, -129, -129, 2256, 2257, 2258, 2259, 2260, + 2261, -129, -129, 2262, 2263, -129, 2264, 2265, + 2266, 2267, 9883, -129, -129, -129, -129, -129, + -129, -129, 6595, 6596, 6597, -129, -129, 6601, + 6602, 6603, -129, -129, -129, 5679, 6025, 6061, + 6062, -2086, 6064, -129, -129, 1880, 6068, -129, + 13217, 5538, 0, 5539, 0, 138, -129, 139, + 140, 141, -129, 6038, 6081, 5319, 5319, 5319, + 5319, -129, 0, 0, -129, -129, -129, 6048, + 6049, 6050, 6051, -129, -129, -129, -129, -129, + -129, -129, -129, -129, -129, -129, -129, -129, + -129, -129, -129, -129, -129, -129, -129, -129, + -129, -129, -129, -129, -129, -129, -129, -129, + -129, 4712, -129, -129, -129, -129, 4716, -129, + -129, 4718, -1776, -1737, 32767, 6112, 6593, 6113, + 6595, 6114, 0, 0, 0, 129, 0, 0, + 2382, 4740, 2384, 0, 0, 2385, 2386, 2387, + 2388, 2389, 2390, 0, 0, 2391, 2392, 0, + 2393, 2394, 2395, 2396, 10012, 0, 0, 0, + 0, 0, 0, 0, 6724, 6725, 6726, 0, + 0, 6730, 6731, 6732, 0, 0, 0, 5808, + 6154, 6190, 6191, -1957, 6193, 0, 0, 2009, + 6197, 0, 13346, 5667, 129, 5668, 129, 267, + 0, 268, 269, 270, 0, 6167, 6210, 5448, + 5448, 5448, 5448, 0, 129, 129, 0, 0, + 0, 6177, 6178, 6179, 6180, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 4841, 0, 0, 0, 0, + 4845, 0, 0, 4847, -1647, -1608, -129, -129, + -129, -129, -1612, -129, -129, -129, -129, -129, + -129, -129, -129, -129, -129, -129, -129, -129, + -129, -129, 257, 257, -129, 4871, -129, 700, + -129, 270, 6749, 6395, 6356, 6752, 10131, 2452, + 273, 6756, 6757, 6758, 6759, 6760, 6761, 6762, + 6763, 6764, 6765, 6766, 6767, 6768, 3416, 275, + 276, 5820, 6773, 6414, 6775, 6776, 6416, 6778, + 6417, 6780, 6781, 6782, 6783, 6784, 6785, 6786, + 6787, 6788, 6789, 6790, 6791, 6792, 6793, 6794, + 6795, 6796, 6797, 6798, 6799, 6800, 6801, 6439, + 6803, 6440, 6805, 6806, 6807, -743, 6809, 6810, + 6811, 6812, 6813, 6814, 11191, 6450, 6450, 6450, + 6450, 6820, 6821, 6452, 6452, 6824, 6825, 6454, + 6454, 6828, 6829, 6830, 6831, 6832, 6833, 11259, + 6460, 6836, 6837, 6462, 6462, 6840, 6841, 4945, + 4945, 6844, 6845, 6846, 6847, 0, -1518, -1479, + 0, 0, 0, 0, -1483, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 386, 386, 0, 5000, + 0, 829, 0, 399, 0, 0, 830, 871, + 832, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 834, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 837, 838, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 7436, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 840, + 1158, 0, 0, 0, 0, 0, 0, 1164, + 1164, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 4067, 4067, 4067, 4067, 4067, + 0, 0, 0, 0, 842, 0, 843, 0, + 844, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 847, 0, 0, 0, 0, 848, 0, + 0, 0, 0, 849, 0, 0, 0, 1224, + 1224, 0, 0, 0, 0, 0, 0, 0, + 850, 0, 0, 0, -84, 0, 0, 0, + 851, 852, 853, 854, 855, 856, 857, 858, + 859, 860, 861, 862, 863, 0, 0, 864, + 865, 866, -64, 282, 0, 869, 870, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 871, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 872, 0, 0, 0, + 0, 873, 0, 0, 0, 0, 874, 0, + 0, 0, 0, 875, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 876, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 231, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 6034, 0, 0, 6035, 0, 0, + 0, 0, 0, 0, 0, 5481, 5482, 0, + 0, 0, 0, 0, 5483, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 4798, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2358, 0, 0, 0, + 0, 0, 0, 0, 0, -4613, 0, 0, + 0, 0, 0, 0, 0, 1519, 1480, 1480, + 5254, -2425, 1480, 1480, 1480, 1519, 1519, 1519, + 1519, 1519, 1519, 1519, 1519, 1519, 878, 1519, + 1519, 1519, -4617, 1519, 1519, 1519, 1519, 1519, + 1519, 4024, 1519, 1519, 1519, 1519, 1519, 879, + -4659, 880, 881, 1519, 1519, 1519, 1519, 1519, + 6261, 6262, 1519, 1519, 1519, 1519, 1519, 1519, + 1519, 1519, 1519, 1519, 6259, 1519, 1519, 1519, + 1519, 1519, 1519, 1519, 1519, 1519, 6260, 1519, + 1519, 1519, 1519, 1519, 1519, 1519, 1519, 1519, + 1519, 1519, 1519, 1519, 1519, 1519, 1519, 1519, + 1519, 6318, 1519, 1519, 1519, 1519, 1519, 1519, + 1519, 0, 0, 0, 0, 0, 0, 0, + 0, 1440, 1440, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 4774, 0, 0, + 0, 0, 0, 0, 0, 882, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 4166, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 6042, 6043, 6044, 6045, 6046, 0, 0, 0, + 0, 0, 0, 0, 0, 6035, 6036, 6037, + 6038, 6039, 6040, 6041, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -5, 0, 0, + 0, 0, 0, 0, 0, -1164, -1164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 4185, 4186, 0, 0, + 0, 4737, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 883, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -796, -795, - -794, -793, -792, -791, 1061, 1062, 1063, 1064, - 1065, 0, 0, 0, 0, 0, 0, 0, - -785, -784, -783, -782, -4016, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 4739, 0, 0, 0, 0, 0, 0, + 0, 0, -96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -39, -39, 3735, -3944, -39, -39, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, -1097, -1096, -1095, -5270, -5270, -1094, -1093, - -1092, -2026, -1090, -1089, -2025, -2024, -765, -4241, - -10978,-10978,-5270, -5270, -10978,-10978,0, 0, + -641, 0, 0, 0, -6136, 0, 0, 0, + 0, 0, 0, 2505, 0, 0, 0, 0, + 0, -640, -6178, -639, -638, 0, 0, 0, + 0, 0, 4742, 4743, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 4740, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 3849, 0, - 0, 0, 0, 3854, 2311, 523, 2313, 0, + 4741, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 4799, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 5994, 4146, 4146, 5997, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3774, -3905, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 32767, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 350, + 350, 0, 32767, 0, 0, 0, 0, 32767, + 32767, 32767, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 4744, 4745, + -9860, -2180, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + -3142, 0, 0, -5543, 0, 0, 0, 0, + 0, 0, 0, 0, -1855, -1855, -1855, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 8149, 0, 6194, -550, + 4188, -550, 6198, -7147, 533, 6072, 4184, 4185, + -550, -550, -550, -550, -550, -550, -550, -550, + -867, -550, -550, -550, -550, -550, -550, -550, + 0, 0, -549, -549, 0, 0, -547, 0, + 0, -545, -545, 0, 0, -543, -543, 4800, + -543, 4801, 4802, -542, -542, -542, -542, -542, + -542, -582, -542, -583, -542, 0, 0, -541, + 0, -540, 305, -540, -540, -540, -540, -540, + 0, -539, 296, 297, 307, -539, -539, -539, + -539, -539, -539, -539, -2896, -539, -539, -539, + -539, -539, -539, -539, -539, -539, -539, -539, + -539, 32767, 32767, 32767, -542, -542, -542, -542, + -542, -542, 32767, 32767, -544, -544, -544, -544, + -544, -544, 32767, 32767, -546, -546, -546, -546, + -546, -546, 6181, 32767, -548, -548, -548, 6185, + 6186, 6187, 380, 35, 0, 0, 8149, 0, + 6194, 6195, 4187, 0, 6198, -7147, 533, 6072, + 534, 6074, 5937, 6205, 5938, 5938, 5938, 32767, + 42, 0, 32767, 763, 32767, 32767, 6212, 32767, + 6084, 6214, 6215, 6216, 40, 40, 40, 40, + 6221, 6222, 32767, 6223, 6224, 6225, 6226, 32767, + 6227, 32767, 6228, 32767, 32767, 32767, 32767, 32767, + 32767, 6229, 32767, 32767, 32767, 32767, 6230, 32767, + 6231, 32767, 6232, 32767, 6233, 6234, 1394, 32767, + 6236, 6237, 32767, 1393, 32767, 32767, 1392, 32767, + 7848, 32767, 0, 32767, 0, 32767, 0, 32767, + 6115, 6116, 32767, 6117, 32767, 32767, 1378, 3735, + 6120, 6121, 32767, 3736, 3736, 3736, 3736, 3736, + 6127, 6128, 32767, 3737, 6130, 3738, 3738, 32767, + 3737, -3878, 6135, 6136, 32767, 6137, 32767, 6138, + 6139, -584, -584, -584, 6143, 6144, -585, -585, + -585, 32767, 6148, 6149, 342, -3, -38, -38, + 8111, -38, 6156, 6157, 4149, -38, 6160, -7185, + 495, 6034, 496, 32767, 32767, 32767, 32767, 32767, + 5895, 6166, 0, 32767, 720, 721, 722, 723, + 6172, 32767, 6044, 6174, 6175, 6176, 0, 0, + 0, 0, 6181, 6182, 6183, 6184, 6185, 6186, + 6187, 6188, 6189, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 2970, 32767, 32767, 32767, 32767, 1264, 1264, 1264, - 2215, 2216, 2217, 2218, 2219, 2220, 2221, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 5792, 32767, - 362, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -3774, 32767, 32767, -3776, -3776, -3776, -3776, - -5034, -3776, -3776, 32767, 32767, -3778, -3778, -3778, - -3778, -3778, -3778, 32767, 32767, -3780, -3780, 32767, - -5053, -3781, -3781, -3781, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, -157, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 627, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 4695, 32767, - 32767, 2985, 2986, 32767, 32767, 32767, 32767, 628, - 629, 630, 631, 632, 633, -5911, 635, 636, - 637, 638, 639, 640, 641, 642, 643, 644, - 645, 646, 647, 648, 649, 650, 651, 652, - 653, 654, 32767, 32767, 32767, 32767, 32767, 32767, - -2208, 32767, 8339, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 4699, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4700, - 4701, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 2745, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 32767, 4786, + 4787, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 5998, 0, 32767, - 32767, 32767, 32767, 0, 0, 32767, 32767, 32767, - 32767, 0, 2987, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -1459, - -1459, -1459, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -1496, -2931, 32767, - 32767, 32767, 655, 656, 657, 658, 659, 660, - 661, 662, 663, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 664, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 4743, 32767, 32767, 32767, -3717, -3716, - 667, -6740, 32767, -3714, 670, 671, 672, 673, - 674, 675, 676, 677, 32767, 678, 679, 680, - 32767, 681, 4709, 683, 684, 685, 2990, 32767, - 32767, 32767, 2991, 2992, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 538, 32767, 32767, 32767, 32767, -1583, 32767, + 32767, 32767, 32767, 32767, 32767, -3270, -3270, -3270, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 4671, 4672, 4673, -68, + -68, 4676, 4677, 4678, 4679, 4680, 4681, 32767, + 71, 6566, 32767, 32767, 4682, 4683, 4684, 4685, + 4686, 4687, 4688, 4689, 4690, -50, 4692, 4693, + 4694, 4695, 4696, 4697, 4698, 4699, 4700, 4701, + 4702, 4703, 4704, 4705, 4706, 4707, 4708, 4709, + -89, 4711, 4712, 32767, 4713, 4714, 4715, 4716, + 6236, 6237, 6238, 6239, 6240, 6241, 6242, 6243, + 4804, 4805, 6246, 6247, 6248, 6249, 6250, 6251, + 6252, 6253, 6254, 6255, 1482, 6257, 6258, 6259, + 6260, 6261, 6262, 6263, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, -3370, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 686, -5583, -5583, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -1622, + 32767, 32767, 32767, 32767, 32767, 32767, 6264, 6265, + 6266, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4710, + 32767, 32767, 32767, 6268, 6269, 6270, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, -2053, 32767, + 32767, 32767, 32767, 6310, 6272, 6273, 6274, 6275, + 6276, 6277, 6278, 6279, 6280, 6922, 6282, 6283, + 6284, 12421, 6286, 6287, 6288, 6289, 6290, 6291, + 3787, 6293, 6294, 6295, 6296, 6297, 6938, 12477, + 6939, 6939, 6302, 6303, 6304, 6305, 6306, 1565, + 1565, 6309, 6310, 6311, 6312, 6313, 6314, 32767, + 32767, 32767, 32767, 1575, 6316, 6317, 6318, 6319, + 6320, 6321, 6322, 6323, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 6324, 6325, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 4679, 4711, 4681, 4682, 4683, 4684, 4685, 4686, - 4687, 4688, 4689, 0, 0, 32767, 0, 32767, - 32767, 32767, 0, 5257, 32767, 32767, 32767, 6885, - 6619, 9923, 6621, -1062, -1062, 6624, 6625, 644, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 1637, 32767, 1637, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 4690, 4691, 4692, 4693, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 4015, -5630, -5897, -5630, -8933, -5630, - 2054, 2055, -5630, -5630, 352, -1290, -1289, -1288, - -1287, -1286, 714, 715, 2889, 4605, 4055, 6335, - 720, 721, 32767, 2886, -1262, 724, 725, 726, - -1253, 728, 729, 730, 731, 2051, 5416, -1241, - 367, 367, 367, 367, 367, 740, 9926, 742, - 369, 6639, 6640, 746, 747, 748, 749, -114, + 32767, 32767, 32767, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 4796, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 3924, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, -8006, -8006, -2024, -3666, -3665, - -3664, -3663, -3662, 1670, -3660, 1673, 1674, 1675, - -2024, -2024, -3651, -2024, -2024, 32767, 32767, 32767, - 32767, 32767, -2866, 32767, 32767, -2865, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -2866, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -2866, 32767, - 32767, 32767, -4485, -4485, 758, 759, 32767, 32767, - 32767, 760, -2863, -2862, -2861, -2860, -2859, -2858, - -2857, 32767, 32767, 32767, 32767, 32767, 32767, 3014, - 3015, 3016, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 4795, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 0, 32767, 32767, 32767, 32767, 3017, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 2180, 2180, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 0, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 768, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 769, 770, 771, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 32767, 32767, 32767, 32767, 32767, 772, 773, 32767, - 774, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3018, - 32767, 32767, 0, 32767, 4716, 4717, 32767, 32767, - 32767, 32767, 32767, 32767, 3019, 0, 3919, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 32767, 32767, 3643, - 32767, 32767, 3641, 32767, 32767, 0, 0, -3355, - 0, 0, 0, 0, 1998, 0, 0, 0, - 0, 0, 0, -3366, -3366, -3366, 0, 0, - -3368, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -388,44 +774,76 @@ Decomp_hash_func(const void *key) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 3365, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 676, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 13799, 0, 0, 0, 9279, 2994, 32767, - 8461, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 1755, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 3196, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -4700, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -3816, - 360, 361, 362, -572, 364, 365, -571, -570, - 689, -2787, -9524, -9524, -3816, -3816, -9524, -9524, - -9524, -3816, 699, 700, 701, 702, 703, 704, - 705, 706, 707, 8659, 5531, 5531, 5531, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, - 32767, 32767, 32767, 32767, 32767, 4746, 4747, 32767, - 4748, 32767, 32767, 32767, 4749, -507, -507, -507, - -507, -507, -507, -507, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 32767, 0, 32767, 0, 32767, 0, + 32767, 0, 32767, 0, 32767, 0, 32767, 32767, + 0, 32767, 0, 32767, 0, 32767, 32767, 32767, + 32767, 4803, 32767, 0, 0, 32767, 0, 0, + 32767, 0, 0, 32767, 0, 0, 32767, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -872, - 32767, 32767, 3030, 3031, 3032, 3033, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -3585, -3585, -3585, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, + 32767, 32767, 32767, 32767, 0, 0, 0, 0, + 32767, 0, 0, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, + 32767, 0, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2044, -2044, -873, -873, + -873, 0, -2043, -2043, -2043, -868, 3607, 3607, + 3607, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -10784,-5076, -561, - -560, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 2304, 2304, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -434,267 +852,23 @@ Decomp_hash_func(const void *key) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -3368, -3368, - 798, 32767, -372, -372, -372, -372, -372, -372, - 0, 1160, 0, 0, 0, 0, 0, 0, - 1161, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 5714, 5714, 9877, 9878, 9879, 9880, 0, - 0, -97, -97, -97, -97, -97, 5712, 0, - 0, 9880, 6739, 0, 9882, 9883, 9884, 9885, - 5710, 0, 3820, 679, 0, 0, 9886, 9887, - 8951, 0, 0, 6737, 0, 0, 5708, 5708, - 0, 0, 0, 5708, 10223, 10224, 10225, 10226, - 10227, 10228, 10229, 0, 0, 10234, 10235, 10236, - 10237, 10238, 10239, 10240, 10241, 0, 0, 5952, - 5953, 5954, 5955, 5711, 5957, 5958, 5959, 5960, - 5961, 5962, 5963, 5711, 5711, 5711, 5711, 6603, - 5711, 5711, 5711, 5711, 5711, 5711, 5975, 5711, - 5711, 5711, 5979, 5980, 32767, 32767, 32767, 32767, - 803, 32767, 32767, 32767, 804, 32767, 32767, 32767, - 32767, 32767, 32767, 805, 806, 32767, 807, 808, - 32767, 809, 0, 5999, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1389, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 840, 841, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 842, 32767, 843, 32767, 844, 32767, 32767, 32767, - 32767, 32767, 0, 0, 10606, 0, 845, 5378, - 0, 0, 0, 0, 1113, 1113, 1113, 1113, - 1113, 1113, 1113, 1113, 1113, -6838, -3709, -3708, - -3707, 0, -3493, 0, 0, 0, 0, -3497, - -1953, -164, -1953, -1953, 5407, 5408, -1955, -1955, - -1955, -1955, -1955, -1955, -3509, -1955, -1955, 812, - 813, 814, 815, -1955, -5123, -5122, -5121, -5120, - -5119, -5118, -14675,5430, -5115, -2332, -11383,-2332, - -11382,-136, -135, -1739, -11384,-11651,-11384,-131, - -130, -3700, -3699, -80, 5447, -126, -125, 0, - 0, 0, 4756, 4757, 0, 5456, -1715, 0, - -117, 0, 10917, 0, 0, 0, 5465, 5466, - 776, 0, 0, 777, 0, -5426, 5473, 5474, - 0, 778, -5431, 0, 0, 0, 0, 4123, - 4124, 4125, 0, 0, 0, 0, 4130, 4131, - 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, - 4140, 4141, 4142, 4143, 5402, 4145, 4146, 4147, - 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, - 4156, 4157, 4158, 5431, 5432, 4161, 4162, 4163, - 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, - 4172, 4173, 4174, 4175, 4176, 4177, 4178, 4179, - 32767, 32767, 4180, 4181, 4182, 4183, 4184, 4185, - 4186, 4187, 4188, 4189, 4190, 4191, 4192, 6044, - 6045, 6046, 6047, 6048, 4198, 4199, 4200, 4201, - 4202, 4203, 4204, 4205, 4206, 4207, 4208, 974, - 4210, 11136, 3906, 3907, 3908, 3909, 3910, 3911, - 3912, 3913, 3914, 3915, 3916, 3917, 5988, 5989, - 0, 0, 3919, 3920, 3921, 3922, -6053, -6053, - -6053, -6053, -6053, -6053, -6053, -244, -5956, 3924, - 3925, 784, 3927, 3928, 3929, 3930, 3931, -244, - -244, 3932, 3933, 3934, 3000, 3936, 3937, 3001, - 3002, 4261, 785, -5952, -5952, -244, -244, -5952, - -5952, -5952, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -5739, -408, - -408, -408, 32767, 32767, 32767, 32767, 3724, 823, - 824, 32767, 32767, 32767, 32767, 32767, 32767, -421, - -2028, -2027, -2026, -2025, -2024, 517, -4312, -4312, - 827, 828, 522, 523, 524, 525, 526, 527, - 528, 529, 530, 531, 532, 533, 534, 535, - 536, 537, 538, 539, 540, 541, 542, 543, - 544, 545, 546, 547, 548, -3617, 550, 551, - 552, 553, 554, 555, 556, 32767, 557, 32767, - 558, 559, 32767, 560, 561, 32767, 562, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -1191, -1191, - -1191, -2792, 0, 0, 0, 0, -2791, -2790, - -4408, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -2791, -2790, - -2789, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 837, 838, 0, 0, -3001, 3038, - -3001, -3001, -3001, -1087, 0, -2999, -2999, 0, - 3047, 0, 0, 0, -2995, -2995, 0, 0, - 0, 0, 4287, 0, 0, 4292, 4293, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -550, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 3918, 0, 0, 0, 0, 9976, 0, - 0, 0, 0, -6065, -6065, 0, 0, 0, - 0, 3142, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 935, 0, 0, 937, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 9645, 0, -267, 0, -3303, 0, - 7684, 7685, 0, 0, 5982, 4340, 4341, 4342, - 4343, 4344, 9676, 4346, 4347, 9679, 9680, 4350, - 4351, 4352, 4353, 4354, 4355, 4356, 4357, 4358, - 4359, 4360, 4361, 4362, 4363, 4364, 4365, 4366, - 5974, 5974, 5974, 5974, 5974, 5974, 15532, -4572, - 5974, 3192, 12244, 3194, 12245, 12246, 12247, 2603, - 12249, 12517, 12251, 999, 12252, 4569, 4569, 12255, - 12256, 6275, 7918, 7918, 7918, 7918, 7918, 2587, - 7918, 2586, 2586, 2586, 6286, 6287, 7915, 6289, - 6290, 7913, 7913, 6293, 96, 7911, 7911, 7911, - 7911, 6299, 7910, 1253, 7911, 6304, 6305, 6306, - 6307, 6308, 6309, -3248, 16857, 6312, 43, 43, - 43, 43, 43, 43, 9688, 43, -224, 43, - -3260, 43, 7727, 7728, 43, 43, 6025, 1253, - 6234, 1253, 6384, 6385, 10526, -3272, 1253, 6388, - 10531, 1253, 1253, 1253, 1253, 6392, 6393, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 6053, 6054, 6055, - 6056, 6057, 6058, 32767, 6059, 6060, 6061, 6062, - 32767, 6063, 32767, 6064, 32767, 32767, 32767, 4761, - 32767, 32767, 6065, 32767, 4762, 4763, 32767, 9404, - 32767, 6067, 4764, 6068, 32767, 6069, 6070, 6071, - 32767, 6072, 6073, 32767, 6074, 32767, 32767, 6075, - 32767, 6076, 32767, 6077, 32767, 6078, 32767, 6079, - 32767, 4374, 4374, 32767, 4373, 32767, 32767, 5324, - 5325, 5326, 5327, 32767, 6087, 6088, 6089, 6090, - 6091, 3057, 6093, 32767, 6094, 4376, -4013, 4376, - 32767, 4375, 4375, 4375, 4375, 32767, 4374, 32767, - 4373, 6104, 6105, 6106, 6107, 6108, 6109, 6110, - 6111, -3652, 881, -645, -645, -1903, 6116, 6117, - 6118, 4399, 4400, 4401, 6122, 6123, 3567, 3567, - 4406, 6127, 7410, 6129, 32767, 32767, 32767, 32767, - 32767, 7409, 7410, 4412, 32767, 6133, 4414, 4415, - 6136, 6137, 32767, 4420, 9402, 6140, 134, 134, - 13933, 134, 134, 134, 9413, 4429, 6149, 4432, - 6151, 4435, 6153, 6304, 6305, 6306, 6307, 6308, - 6309, 6310, 6311, 6312, 6313, 6314, 1068, 6316, - 5378, 6318, 5379, 6320, 803, 6322, -2232, 6324, - 6325, 6326, 1074, 6328, 6329, 6330, 6331, 6332, - 6333, 6334, 6335, 6336, 6337, 6338, 6339, 6340, - 6341, 6342, 6343, 6344, 6345, 6346, 6347, 6348, - 6349, 6350, 6351, 6352, 6353, 6354, 6355, 6356, - 6357, 6358, 6359, 6360, 5417, 6362, 6363, 6364, - 6365, 6366, 6367, 5423, 6369, 5424, 6371, 6372, - 5426, 5426, 6375, 5427, 5427, 6378, 6379, 6380, - 6381, 6382, 6383, 6384, 6385, 6386, 6387, 6388, - 6389, 6390, 6391, 6392, 6393, 6394, 6395, 6396, - 6397, 6398, 6399, 6400, 6401, 6402, 6403, 6404, - 6405, 6406, 6407, 6408, 6409, 6410, 6411, 6412, - 6413, 6414, 6415, 6416, 6417, 6418, 6419, 6420, - 6421, 6422, 7182, 5465, 6425, 6426, 6427, 6428, - 6429, 6430, 6431, 6432, 6433, 6434, 6435, 6436, - 6437, 6438, 6439, 6440, 6441, 6442, 6443, 6444, - 6445, 6446, 6447, 6448, 6449, 6450, 6451, 6452, - 6453, 6454, 6455, 6456, 6457, 6458, 6459, 6460, - 6461, 6462, 6463, 6464, 6465, 6466, 6467, 6468, - 6469, 6470, 6471, 6472, 6473, 6474, 6475, 6476, - 6477, 6478, 5519, 5519, 6481, 6482, 6483, 6484, - 6485, 6486, 6487, 6488, 6489, 6490, 6491, 6492, - 6493, 6494, 6495, 6496, 6497, 6498, 6499, 6500, - 6501, 6502, 6503, 6504, 6505, 6506, 6507, 6508, - 6509, 6510, 6511, 6512, 6513, 6514, 6515, 6516, - 6517, 6518, 6519, 6520, 6521, 6522, 6523, 6524, - 6525, 6526, 6527, 6528, 6529, 6530, 6531, 6532, - 6533, 1330, 6535, 6536, 6537, 6538, 5577, 6540, - 6541, 6542, 6543, 6544, 6545, 6546, 6547, 6548, - 6549, 6550, 5588, 5588, 6553, 6554, 6555, 6556, - 32767, 6557, 1728, 1728, 6867, 6868, 6562, 6563, - 6564, 6565, 6566, 6567, 6568, 6569, 6570, 6571, - 6572, 6573, 6574, 6575, 6576, 6577, 6578, 6579, - 6580, 6581, 6582, 6583, 6584, 6585, 6586, 6587, - 6588, 2423, 6590, 6591, 6592, 6593, 6594, 6595, - 6596, 6597, 6598, 6599, 6600, 6601, 6602, 6603, - 6604, 6605, 6606, 6607, 6608, 6609, 6610, 6611, - 6612, 6613, 6614, 6615, 6616, 6617, 6618, 6619, - 6620, 6621, 6622, 6623, 6624, 6625, 6626, 6627, - 6628, 6629, 6630, 6631, 6632, 6633, 6634, 6635, - 6636, 6637, 6638, 6639, 6640, 6641, 6642, 6643, - 6644, 6645, 6646, 6647, 6648, 6649, 6650, 6651, - 6652, 6653, 6654, 6655, 6656, 6657, 6658, 6659, - 6660, 6661, 6693, 6663, 6664, 6665, 6666, 6667, - 6668, 6669, 6670, 6671, 6672, 6673, 6674, 6675, - 6676, 6677, 6678, 6679, 7476, 7476, 7476, 7476, - 7476, 7476, 5625, 5625, 5625, 5625, 5625, 6691, - 6692, 6693, 6694, 6695, 6696, 6697, 7483, 7483, - 7483, 7483, 10718, 6703, 6704, 6705, 6706, 6707, - 6708, 6709, 6710, 6711, 6712, 6713, 6714, 6715, - 6716, 6717, 6718, 6719, 6720, 6721, 6722, 6723, - 6724, 6725, 6726, 6727, 6728, 6729, 6730, 6731, - 6732, 6733, 6734, 6735, 6736, 6737, 6738, 7836, - 7836, 7836, 12012, 12013, 7838, 7838, 7838, 8773, - 7838, 7838, 8775, 8775, 7517, 10994, 17732, 17733, - 12026, 12027, 17736, 17737, 6760, 6761, 6762, 6763, - 6764, 6765, 6766, 6767, 6768, 6769, 6770, 6771, - 6772, 6773, 6774, 6775, 2927, 6777, 6778, 6779, - 6780, 2927, 4471, 6260, 4471, 6785, 6786, 6787, - 6788, 6789, 6790, 6791, 6792, 6793, 6794, 6795, - 6796, 6797, 6798, 6799, 6800, 6801, 6802, 6803, - 6804, 6805, 6806, 6807, 6808, 6809, 6810, 6811, - 6812, 32767, 3411, 5540, 5541, 3411, 12221, 3411, - 3411, 3411, 3411, 5548, 5549, 5550, 5551, 5552, - 5553, 5554, 5555, 5556, 5557, 5558, 5559, 5560, - 5561, 5562, 5563, 5564, 5565, 5566, 5567, 5568, - 32767, 32767, 32767, 32767, 32767, 32767, 3915, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 6155, 6156, 6157, 6158, 6159, - 6160, 6161, 6162, 6163, 6164, 6165, 32767, 32767, - 32767, 32767, 32767, 6166, 6167, 6168, 6169, 6170, - 6171, 6172, 6173, 6174, 6175, 6176, 6177, 6178, - 6179, 6180, 4615, 4616, 4617, 4618, 4619, 6186, - 6187, 6188, 6189, 6190, 6191, 6192, 6193, 6194, - 6195, 6196, 32767, 6197, 6198, 6199, 6200, 6201, - 7637, 4495, 4495, 6205, 4052, 4052, 6208, 6209, - 6210, 6211, 6212, 6213, 6214, 6215, 6216, 6217, - 6218, 6219, 6220, 6221, 6222, 6223, 6224, 6225, - 6226, 6227, 6228, 32767, 32767, 0, 0, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 6229, 6230, 6231, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 6232, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 884, 885, 886, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 5994, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 6233, 6234, 6235, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 6236, 6237, 6238, 6239, - 6240, 6241, 6242, 6243, 6244, 6245, 6246, 6247, - 6248, 6249, 6250, 6251, 6252, 6253, 6254, 6255, - 6256, 6257, 6258, 6259, 6260, 6261, 6262, 6263, - 6264, 6265, 6266, 6267, 6268, 6269, 6270, 6271, - 6272, 6273, 6274, 6275, 6276, 6277, 6278, 6279, - 32767, 32767, 32767, 32767, 6280, 6281, 6282, 6283, - 6284, 6285, 6286, 6287, 6288, 4770, 32767, 4771, - 4772, 32767, 32767, 32767, 6289, 6290, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -5160, 2071, - 2071, 2071, 2071, 32767, 2070, 2070, 2070, 2070, - 2070, 2070, 2070, 0, 0, 6236, 6237, 2075, - 2075, 2075, 2075, 12051, 12052, 12053, 12054, 12055, - 12056, 12057, 6249, 11962, 2083, 2083, 5225, 2083, - 2083, 2083, 2083, 2083, 6259, 6260, 2085, 2085, - 2085, 3020, 2085, 2085, 3022, 3022, 32767, 5240, - 11978, 11979, 6272, 6273, 11982, 11983, 11984, 6277, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 4773, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -706,252 +880,666 @@ Decomp_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 896, 897, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 415, + 32767, 32767, 0, 0, 0, 0, 0, 0, + 0, 0, 9932, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -2468, -2468, - -2468, -2468, -2468, -2468, -2468, -2468, -2468, -2468, - -2468, -2468, -2468, -2468, -2468, -2468, -2468, -2468, - 2089, 2090, 2091, -2465, -2465, -2465, -2465, -2465, - -2465, 2098, 2099, -2463, -2463, -2463, -2463, -2463, - -2463, 2106, 2107, -2461, -2461, -2461, -2461, -2461, - -2461, 0, 0, -2459, -2459, -2459, 2119, 2120, - 2121, -2456, 2123, 2124, 2125, 2126, 2127, 2128, - 0, 0, 2131, -6678, 2133, 2134, 2135, 2136, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2158, 2159, 2160, - 2161, 2162, 2163, 2164, 2165, 2166, 2167, 2168, - 2169, 2170, 2171, 2172, 2173, 2174, 2175, 2176, - 2177, 2178, 2179, 2180, 2181, -2412, -2412, -2412, - -2412, -2412, 2187, 2188, 2025, 2190, 2191, 2192, - 2193, 2194, 2195, 2196, 2197, 2198, 2199, 2200, - 2201, 2202, 2203, 2204, 2205, 2206, 2207, 2208, - 2209, 2210, 2211, 2212, 2213, 2214, 2215, 2216, - 2217, 2218, 2219, 2220, 2221, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1566, 1566, - 1566, 1566, 1566, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 32767, 0, - 0, 0, 0, 0, -1435, 1708, 1709, 0, - 2154, 2155, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 898, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, -1145, + -1144, -1143, -1142, -1141, -1140, -1139, -1138, 32767, + 32767, -1137, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, -3659, -5441, -5441, -3658, -3657, -3656, + -5441, -3655, -5441, -3654, -5441, -4524, -5441, -3652, + 32767, -3651, -3650, -5442, -3649, -3648, -3647, -5442, + -3646, -3645, -3644, -3643, -4515, -3641, -3640, -5442, + -3639, -4512, -3637, -3636, -3635, -3634, -5442, -5112, + 3037, -5442, -3633, -3632, -3631, -3630, -3629, -5442, + 32767, 32767, -5444, -5444, -5444, -3630, -3629, -3628, + -3627, -3626, -3625, -3624, -3623, -3622, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, -1079, 32767, -1078, 32767, -1076, 32767, -1075, + 32767, -1074, 32767, 32767, 32767, -1073, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 6326, + 6327, 6328, 6329, 6330, 6331, 6332, 6333, -3598, + 6335, 32767, 32767, 32767, 32767, 32767, -5387, 32767, + -5388, 32767, 32767, -3599, -3598, 32767, -3598, -3597, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 0, 0, 32767, 32767, 4774, 4775, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, -3608, -3607, + -3606, -3605, -3835, -3603, -3602, -3601, -3600, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 1116, 988, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 1118, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 990, 991, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 0, 0, 0, 0, - 0, 0, 0, 0, -2552, 0, 0, 32767, - 0, 32767, 32767, 32767, 1551, 293, 3770, 32767, - 32767, 32767, 32767, 0, 32767, 0, 32767, 32767, + 0, 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 0, 0, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 32767, 32767, 0, - 0, 32767, 0, 0, 32767, 32767, 32767, 32767, - 0, 32767, 0, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, -4309, -4309, 0, 32767, - 0, -4310, -4577, -4310, -7613, -4310, 3374, 3375, - -4310, -4310, 1672, -3100, 1881, -3100, 2031, 2032, - 6173, -7625, -3100, 2035, 6178, -3100, -3100, -3100, - -3100, 2039, 2040, 32767, 0, -4353, -4353, 0, - -4352, 0, 0, -4350, 0, -4349, -4349, -4349, - -4349, 32767, 0, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 32767, 0, 0, 32767, 0, 0, - 32767, 0, 0, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, -1267, -1721, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 0, 0, 0, - 0, 951, 952, 953, 954, 955, 956, 957, - 1717, 0, 32767, 32767, 32767, -1317, 32767, 32767, - 32767, 0, -8389, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 887, 888, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 0, 32767, -5009, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3014, + 2461, 2462, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 3021, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 5520, 32767, 32767, 32767, 32767, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 3022, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 993, 994, -5735, 32767, + -5736, 997, 998, 999, -4808, -5153, -5188, -5188, + 2961, -5188, 1006, 1007, -1001, 32767, 1009, -12336, + -4656, 883, -4655, 885, 748, 32767, 32767, 32767, + 32767, 1016, 32767, 32767, 32767, 32767, 32767, 32767, + 1017, 32767, 32767, 32767, 1018, 1019, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 3015, 3016, 3017, 3018, 32767, 32767, 32767, 3019, + 3020, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 5990, 5991, 5992, 5993, 32767, 32767, 32767, + 32767, 1020, 1021, 1022, 32767, 1023, 1024, -1357, + -3714, -1357, 1028, 1029, -1355, -1355, -1355, -1355, + 32767, -1356, 1035, 1036, -1354, -1354, 1039, -1353, + -1353, -1353, -1353, -8968, 1045, 1046, 1047, 1048, + 1049, 1050, 1051, 32767, -5673, -5673, 1054, 1055, + -5674, -5674, -5674, 1059, 1060, 1061, -4746, -5091, + -5126, -5126, 3023, -5126, 1068, 1069, -939, -5126, + 1072, -12273,-4593, 946, -4592, 948, 811, 1079, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 1080, 3024, 3025, + 3026, 3027, 3028, 3029, 3030, 3031, 3032, 0, + 0, 3035, 3036, 3037, 3038, 3039, 3040, 3041, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 1081, 1082, -3758, 1084, 1085, 1086, 1087, -3757, + 1089, 1090, -3756, 2739, 2701, 1223, 1224, 1225, + 1226, 2710, 1228, 1229, 1230, 1231, 1232, 1233, + 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, + 1242, 857, 858, 1245, -3754, 1247, 419, 1249, + 851, -5627, -5272, -5232, -5627, -9005, -1325, 855, + -5627, -5627, -5627, -5627, -5627, -5627, -5627, -5627, + -5627, -5627, -5627, -5627, -5627, -2274, 868, 868, + -4675, -5627, -5267, -5627, -5627, -5266, -5627, -5265, + -5627, -5627, -5627, -5627, -5627, -5627, -5627, -5627, + -5627, -5627, -5627, -5627, -5627, -5627, -5627, -5627, + -5627, -5627, -5627, -5627, -5627, -5627, -5264, -5627, + -5263, -5627, -5627, -5627, 1924, 32767, 1182, 1183, + 1184, 1185, 1186, 1187, 1188, 1189, 1190, 1191, + 1192, 1193, 1194, 1195, 1196, 1197, 1198, 1199, + 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, + 1208, 1209, 1210, 1211, -3629, 1213, 1214, 1215, + 1216, -3628, 1218, 1219, -3627, 2868, 2830, 1352, + 1353, 1354, 1355, 2839, 1357, 1358, 1359, 1360, + 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368, + 1369, 1370, 1371, 986, 987, 1374, -3625, 1376, + 548, 1378, 980, -5498, -5143, -5103, -5498, -8876, + -1196, 984, -5498, -5498, -5498, -5498, -5498, -5498, + -5498, -5498, -5498, -5498, -5498, -5498, -5498, -2145, + 997, 997, -4546, -5498, -5138, -5498, -5498, -5137, + -5498, -5136, -5498, -5498, -5498, -5498, -5498, -5498, + -5498, -5498, -5498, -5498, -5498, -5498, -5498, -5498, + -5498, -5498, -5498, -5498, -5498, -5498, -5498, -5498, + -5135, -5498, -5134, -5498, -5498, -5498, 2053, -5498, + -5498, -5498, -5498, -5498, -5498, -9874, -5132, -5131, + -5130, -5129, -5498, -5498, -5128, -5127, -5498, -5498, + -5126, -5125, -5498, -5498, -5498, -5498, -5498, -5498, + -9923, -5123, 32767, 32767, 32767, 32767, -5502, -5502, + -3605, -3604, -5502, -5502, -5502, -5502, 1346, 2865, + 2827, 1349, 1350, 1351, 1352, 2836, 1354, 1355, + 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, + 1364, 1365, 1366, 1367, 1368, 983, 984, 1371, + -3628, 1373, 545, 1375, 977, 1377, 1378, 549, + 509, 549, 1382, 1383, 1384, 1385, 1386, 1387, + 1388, 1389, 1390, 1391, 1392, 1393, 1394, 1395, + 562, 1397, 1398, 1399, 1400, 1401, 1402, 1403, + 1404, 1405, 1406, 1407, 1408, 1409, 1410, 1411, + 1412, 1413, 1414, 578, 578, 1417, 1418, 1419, + 1420, 1421, 1422, 1423, 1424, 1425, 1426, 1427, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, -5379, + 2172, -5379, -5379, -5379, -5379, -5379, -5379, -9755, + -5013, -5012, -5011, -5010, -5379, -5379, -5009, -5008, + -5379, -5379, -5007, -5006, -5379, 32767, 32767, -5381, + -5381, -5381, -9806, -5006, -5381, 32767, 32767, -5006, + -5383, -5383, -3486, -3485, -5383, -5383, -5383, -5383, + 1465, 2984, 2946, 1468, 1469, 1470, 1471, 2955, + 1473, 1474, 1475, 1476, 1477, 1478, 1479, 1480, + 1481, 1482, 1483, 1484, 1485, 1486, 1487, 1102, + 1103, 1490, -3509, 1492, 664, 32767, 32767, 1494, + 1495, 666, 626, 666, 1499, 32767, 32767, 1500, + 1501, 1502, 1503, 1504, 1505, 1506, 1507, 32767, + 1508, 32767, 675, 32767, 1510, 32767, 1511, 1512, + 1513, 1514, 1515, 1516, 1517, 1518, 1519, 1520, + 1521, 1522, 1523, 1524, 1525, 689, 689, 1528, + 1529, 1530, 1531, 1532, 1533, 1534, 1535, 1536, + 1537, 1538, 1539, 1540, 1541, 32767, 32767, 1542, + 1543, 1544, 1545, 1546, 1547, 1548, 1549, 1550, + 1551, 1552, 1553, 1554, 1555, 1556, 1557, 1558, + 1559, 1560, 1561, 1562, 1563, 1564, 1565, 1566, + 1567, 1568, 1569, 1570, 1571, 1572, -5863, 1574, + 1575, 1576, 1577, 1578, 1579, 1580, 1581, 1582, + 1583, 1584, 745, 428, 1587, 1588, 1589, 1590, + 1591, 1592, 429, 430, 32767, 1595, 1596, 1597, + 1598, 1599, 1600, 1601, 1602, 1603, -2463, -2462, + -2461, -2460, -2459, 1609, 32767, 1610, 1611, 770, + 1613, 771, 1615, 772, 1617, 1618, 1619, 1620, + 1621, 1622, 1623, 32767, 32767, 1624, 1625, 1626, + 1627, 1628, 1629, 32767, 1630, 1631, 1632, 1633, + 1634, 1635, 1636, 1637, 791, 1639, 1640, 1641, + 1642, 795, 1644, 1645, 1646, 1647, 799, 32767, + 32767, 1649, 426, 427, 32767, 1652, 1653, 1654, + 1655, 1656, 1657, 808, 1659, 1660, 32767, 32767, + 1661, 1662, 1663, 1664, 1665, 1666, 1667, 1668, + 1669, 1670, 1671, 32767, 32767, 32767, 32767, 32767, + 32767, 1672, 32767, 32767, 32767, 32767, 32767, 1673, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 1674, 1675, 1676, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1677, + 32767, 32767, 32767, 1678, 515, 32767, 1680, 1681, + 32767, 32767, 32767, 32767, 1682, 32767, 1683, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1684, + 1685, 844, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1687, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1688, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 840, 1690, 32767, 32767, 467, 468, 1693, 1694, + 1695, 1696, 1697, 1698, 1699, 850, 1701, 1702, + 1703, 1788, 1705, 1706, 1707, 857, 857, 857, + 857, 857, 857, 857, 857, 857, 857, 32767, + 856, 856, 1720, 1721, 858, 858, 858, 1789, + 1444, 1727, 859, 859, 1730, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 1731, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 1732, 1733, 1734, 1735, 1736, 1737, 1738, 1739, + 1740, 1741, 1511, 1743, 1744, 32767, 32767, 32767, + 32767, 1745, 32767, 32767, 32767, 1746, 1747, -4286, + 1749, 1750, -4284, 1752, 1753, 1754, 1755, 1756, + 1757, 32767, 32767, 32767, 32767, 4806, 4807, 4808, + 4809, 4810, 4811, 5051, 4813, 32767, 32767, 32767, + 32767, 1758, 1759, 1844, 1761, 32767, 1762, 912, + 912, 32767, 911, 911, 911, 911, 911, 911, + 911, 911, 911, 911, 1775, 0, 912, 912, + 0, 0, 1496, 1779, 911, 911, 1782, 0, + 0, 1783, 1784, 1785, 0, 1786, 0, 1787, + 0, 917, 0, 1789, 1790, 1791, 1792, 0, + 1793, 1794, 1795, 0, 1796, 1797, 1798, 1799, + 927, 1801, 1802, 0, 1803, 930, 1805, 1806, + 1807, 1808, 0, 330, 8479, 0, 1809, 1810, + 1811, 1812, 1813, 0, 0, 0, 0, 0, + 0, 1814, 1815, 1816, 1817, 1818, 1819, 1820, + 1821, 1822, 1823, 1824, 1594, 1826, 1827, 1828, + 1829, 1830, 1831, 1832, 1833, 1834, 1835, 1836, + 1837, -4196, 1839, 1840, -4194, 1842, 1843, 1844, + 1845, 1846, 1847, 1848, -3632, -3632, 1851, 1852, + 1853, 1854, 1855, -3627, 1857, 1858, 1859, 1860, + 1861, 0, 0, 0, 0, 0, 0, 0, + 0, 0, -2936, 0, 0, 0, 0, 0, + 480, 0, 481, 0, 482, 6597, 6598, 6599, + 6471, 6601, 6602, 4221, 1864, 4221, 6606, 6607, + 4223, 4223, 4223, 4223, 4223, 4223, 6614, 6615, + 4225, 4225, 6618, 4226, 4226, 4226, 4226, -3389, + 6624, 6625, 6626, 6627, 6628, 6629, 6630, -93, + -93, -93, 6634, 6635, -94, -94, -94, 6639, + 6640, 6641, 834, 489, 454, 454, 8603, 454, + 6648, 6649, 4641, 454, 6652, -6693, 987, 6526, + 988, 6528, 6391, 6659, 6392, 6392, 6392, 6663, + 497, 455, 1218, 1219, 1220, 1221, 6670, 6542, + 6543, 6673, 6674, 6675, 499, 499, 499, 499, + 6680, 6681, 6682, 6683, 6684, 6685, 6686, 6687, + 6688, 6689, 6690, 6691, 6692, 6693, 6694, 6695, + 6696, 6697, 6698, 6699, 6700, 6701, 6702, 6703, + 6704, 6705, 6706, 6707, 6708, 6709, 1869, 6711, + 6712, 6713, 6714, 1870, 6716, 6717, 1871, 8366, + 8328, 6850, 6851, 6852, 6853, 8337, 6855, 6856, + 6857, 6858, 6859, 6860, 6861, 6862, 6863, 6864, + 6865, 6866, 6867, 6868, 6869, 6484, 6485, 6872, + 1873, 6874, 6046, 6876, 6478, 0, 355, 395, + 0, -3378, 4302, 6482, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 3353, 6495, 6495, 952, 0, 360, 0, + 0, 361, 0, 362, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 363, 0, 364, 0, 0, 0, + 7551, 0, 0, 0, 0, 0, 0, -4376, + 366, 367, 368, 369, 0, 0, 370, 371, + 0, 0, 372, 373, 0, 0, 0, 0, + 0, 0, -4425, 375, 0, 0, 376, 377, + 0, 0, 1897, 1898, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 1899, 1900, + 1901, 1902, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 1903, 1904, 1905, 1906, 3065, 3066, + 3067, 3068, 3069, 3070, 1907, 1908, 3073, 3074, + 3075, 3076, 3077, 3078, 3079, 3080, 3081, 3082, + -984, -983, -982, -981, -980, 3088, 3089, 3090, + 3091, 2250, 3093, 2251, 3095, 2252, 3097, 3098, + 3099, 3100, 3101, 3102, 3103, 3104, 3105, 3106, + 3107, 3108, 3109, 3110, 3111, 3112, 3113, 3114, + 3115, 3116, 3117, 3118, 3119, 3120, 2274, 3122, + 3123, 3124, 3125, 2278, 3127, 3128, 3129, 3130, + 2282, 3132, 3133, 3134, 1911, 1912, 3137, 3138, + 3139, 3140, 3141, 3142, 3143, 2294, 3145, 3146, + 3147, 3232, 3149, 3150, 3151, 2301, 2301, 2301, + 2301, 2301, 2301, 2301, 2301, 2301, 2301, 2301, + 2301, 2301, 3165, 3166, 2303, 2303, 2303, 3234, + 2889, 3172, 2304, 2304, 3175, 3176, 3177, 3178, + 3179, 3180, 3181, 3182, 3183, 3184, 3185, 2315, + 3187, 3188, 3189, 3190, 3191, 3192, 3193, 3194, + 3195, 2324, 3197, 3198, 3199, 3200, 2328, 3202, + 3203, 3204, 3205, 2332, 3207, 3208, 3209, 3210, + 2336, 3212, 3213, 3214, 3215, 3216, 3217, 3218, + 3219, 3220, 3221, 3222, 3223, 2348, 3225, 3226, + 3227, 3228, 3229, 3230, 3231, 3232, 3233, 3234, + 3235, 3236, 3006, 3238, 3239, 3240, 3241, 3242, + 3243, 3244, 3245, 3246, 3247, 3248, 3249, -2784, + 3251, 3252, -2782, 3254, 3255, 3256, 3257, 3258, + 3259, 3260, -2220, -2220, 3263, 3264, 3265, 3266, + 3267, -2215, 3269, 3270, 3271, 3272, 3273, 3274, + 3275, 3276, 3277, 3278, 3279, 3280, 3281, 3282, + -1515, 3284, 3285, 3286, 3287, 3288, 3289, 3290, + 3291, 3292, 3293, 3294, 3295, 3296, 3297, 3298, + 3299, 5658, 3301, 3302, 3303, 3304, 3305, 3306, + 3307, 3308, 7922, 3310, 3311, 3312, 3313, 3314, + 3315, 3316, 1798, 1838, 1839, 32767, 3320, 3321, + 3322, 3323, 3324, 2453, 3326, 3327, 3328, 3329, + 2457, 3331, 3332, 3333, 32767, 32767, 3334, 32767, + 3335, 32767, 32767, 3336, 3337, 3338, 3339, 3340, + 3341, 3342, 3343, 3344, 3345, 32767, 3346, 32767, + 3347, 32767, 32767, 3348, 3349, 32767, 32767, 32767, + 3350, 3351, 3352, 3353, 3123, 3355, 3356, 3357, + 3358, 3359, 3360, 3361, 3362, 3363, 3364, 3365, + 3366, -2667, 3368, 3369, -2665, 3371, 3372, 3373, + 3374, 3375, 3376, 3377, -2103, -2103, 3380, 3381, + 3382, 3383, 3384, -2098, 3386, 3387, 3388, 3389, + 3390, 3391, 3392, 3393, 3394, 3395, 3396, 3397, + 3398, 3399, -1398, 3401, 3402, 3403, 3404, 3405, + 3406, 3407, 3408, 3409, 3410, 3411, 3412, 3413, + 3414, 3415, 3416, 5775, 32767, 32767, 3418, 3419, + 3420, 3421, 3422, 3423, 8037, 3425, 3426, 3427, + 3428, 3429, 3430, 3431, 1913, 1953, 1954, -1819, + 5861, 1957, 1958, 1959, 1921, 1922, 1923, 1924, + 1925, 1926, 1927, 1928, 1929, 2571, 1931, 1932, + 1933, 8070, 1935, 1936, 1937, 1938, 1939, 1940, + -564, 1942, 1943, 1944, 1945, 1946, 2587, 8126, + 2588, 2588, 1951, 1952, 1953, 1954, 1955, -2786, + -2786, 1958, 1959, 1960, 1961, 1962, 1963, 1964, + 1965, 1966, 1967, -2772, 1969, 1970, 1971, 1972, + 1973, 1974, 1975, 1976, 1977, -2763, 1979, 1980, + 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, + 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, + -2802, 1998, 1999, 2000, 2001, 2002, 2003, 2004, + 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, + 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, + 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, + 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, + 2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, + 2045, -1728, 5952, 2048, 2049, 2050, 2051, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 2012, 8149, 2014, 2015, 2016, 32767, + 32767, 32767, 32767, 32767, 2017, 2018, 2019, 2020, + 2661, 8200, 2662, 2662, 2025, 2026, 2027, 2028, + 2029, -2712, -2712, 2032, 2033, 2034, 2035, 2036, + 2037, 2038, 2039, 2040, 2041, -2698, 32767, 2043, + 2044, 2045, 2046, 2047, 32767, 2048, 32767, -2692, + 2050, 32767, 2051, 2052, 32767, 2053, 2054, 2055, + 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, + 2064, 2065, -2733, 2067, 2068, 2069, 2070, 2071, + 2072, 2073, 3593, 3594, 3595, 3596, 3597, 3598, + 3599, 3600, 2161, 2162, 3603, 3604, 3605, 3606, + 3607, 3608, 3609, 3610, 3611, 3612, -1161, 3614, + 3615, 3616, 3617, 3618, 3619, 3620, 2739, 3622, + 3623, 3624, 3625, 3626, 3627, 3628, 3629, 3630, + 3631, 3632, 3633, 3634, 3635, 3636, 3637, 3638, + 3639, 3640, 3641, 3642, 3643, 3644, 3645, 3646, + 3647, 3648, 3649, 3650, 3651, 3652, 3653, 3654, + 3655, -2386, -2386, -2386, -2386, -2386, 3661, 3662, + 3663, 3664, 3665, 3666, 3667, 3668, -2366, -2366, + -2366, -2366, -2366, -2366, -2366, 3676, 3677, 3678, + 3679, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 3680, 3681, 3682, -502, -502, 3685, + 3686, 3687, -1049, 3689, 3690, 3691, 3692, 3693, + 3694, 3695, 3696, 3697, 3698, 2816, 3700, 3701, + 3702, 3703, 3704, 3705, 3706, 3707, 3708, 3709, + 3710, 3711, 3712, 3713, 3714, 3715, 3716, 3717, + 3718, 3719, 3720, 3721, 3722, 3723, 3724, 32767, + 3725, 3726, 3727, 3728, 3729, 3730, 3731, 3732, + 3733, 3734, 3735, 3736, 3737, 3738, 3739, 3740, + 3741, 3742, 3743, 3744, 3745, 3746, 3747, 3748, + 3749, 3750, 3751, -2290, -2290, -2290, -2290, -2290, + 3757, 3758, 3759, 3760, 3761, 3762, 3763, 3764, + -2270, -2270, -2270, -2270, -2270, -2270, -2270, 3772, + 3773, 3774, 3775, 3776, 3777, 3778, 3779, 3780, + 3786, 3782, 3783, 3784, 3785, 3786, 3787, 3788, + 4953, 4954, 3791, 3792, 3793, 3794, 3795, 3796, + 3797, 3798, 3799, 3800, 3801, 3802, 3803, 3804, + 3805, 3806, 3807, 3808, 3809, 3810, 3811, -373, + -373, 3814, 3815, 3816, -920, 3818, 3819, 3820, + 3821, 3822, 3823, 3824, 3825, 3826, 3827, 2945, + 3829, 3830, 3831, 3832, 3833, 3834, 3835, 3836, + 3837, 3838, 3839, 3840, 3841, 3842, 3843, 3844, + 3845, 3846, 3847, 3848, 3849, 3850, 3851, 3852, + 3853, 3854, 3855, 3856, 3857, 3858, 3859, 3860, + 3861, 3862, 3863, 3864, 3865, 3866, 3867, 3868, + 3869, 3870, 3871, 3872, 3873, 3874, 3875, 3876, + 3877, 3878, 3879, 3880, -858, 3882, 3883, 3884, + 3885, 3886, 3887, 3888, 3889, 3986, 3891, 3892, + 3893, 3894, 3895, 3896, 3897, 3898, 3899, 3900, + 3901, 3902, 3903, 3943, 3944, 171, 7851, 3947, + 3948, 3949, 3911, 3912, 3913, 3914, 3915, 3916, + 3917, 3918, 3919, 4561, 3921, 3922, 3923, 10060, + 3925, 3926, 3927, 3928, 3929, 3930, 1426, 3932, + 3933, 3934, 3935, 3936, 4577, 10116, 4578, 4578, + 3941, 3942, 3943, 3944, 3945, -796, -796, 3948, + 3949, 3950, 3951, 3952, 3953, 3954, 3955, 3956, + 3957, -782, 3959, 3960, 3961, 3962, 3963, 3964, + 3965, 3966, 3967, -773, 3969, 3970, 3971, 3972, + 3973, 3974, 3975, 3976, 3977, 3978, 3979, 3980, + 32767, 3981, 3982, 3983, 3984, 3985, 3986, 3987, + 3988, 3989, 3990, 3991, 3992, 3993, 3994, 3995, + 3996, 3997, 3998, 3999, 4000, 4001, 4002, 4003, + 4004, 4005, 4006, 4007, 4008, 4009, -729, 4011, + 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4115, + 4020, 4021, 4022, 4023, 4024, 4025, 4026, 4027, + 4028, 4029, 4030, 4031, 4032, 4072, 4073, 300, + 7980, 4076, 4077, 4078, 4040, 4041, 4042, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 4043, 4044, 4045, 4046, 4047, 4688, 10227, + 4689, 4689, 4052, 4053, 4054, 4055, 4056, -685, + -685, 4059, 4060, 4061, 4062, 4063, 4064, 4065, + 4066, 4067, 4068, -671, 4070, 4071, 4072, 4073, + 4074, 4075, 4076, 4077, 4078, -662, 4080, 4081, + 4082, 4083, 4084, 4085, 4086, 4087, 4088, 4089, + 4090, 4091, 4092, 4093, 4094, 4095, 4096, 4097, + -701, 4099, 4100, 4101, 4102, 4103, 4104, 4105, + 4106, 32767, 32767, 4107, 4108, 4109, 4110, 4111, + 4112, 4113, 4114, 4115, 4116, 4117, 4118, 4119, + 4120, 4121, 4122, 4123, 4124, 4125, 4126, 4127, + 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, + 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, + 4144, 371, 8051, 4147, 4148, 4149, 4150, 4151, + 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, + 4160, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 4161, 4162, 4163, 4164, 4165, 4166, 4167, + 4168, 4169, 4170, 4171, 4172, 4173, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 4174, 4175, 4176, 4177, 4178, 4179, + 4180, 4181, 4182, 4183, 32767, 32767, 32767, 32767, + 32767, 32767, 4184, 4185, 4186, 4187, 4188, 4189, + 4190, 4191, 4192, 4193, 4194, 4195, 4196, 4197, + 4198, 4199, 4200, 4201, 4202, 4203, 4204, 4205, + 4206, 4207, 4208, 435, 8115, 4211, 4212, 4213, + 4214, 4215, 4216, 4217, 4218, 4219, 4220, 32767, + 32767, 4221, 4222, 4223, 4224, 4225, 4226, 4227, + 4228, 4229, 4230, 4231, 4232, 32767, 4233, 4234, + 4235, 4236, 4237, 4238, 4239, 4240, 4241, 4242, + 4243, 4244, 4245, 4246, 4247, 4248, 3899, 3900, + 4251, 32767, 4252, 4253, 4254, 4255, 32767, 32767, + 32767, 32767, 4256, 4257, 4258, 32767, 4259, 32767, + 4260, 4261, 4262, 4263, 4264, 4265, 4266, 4267, + 4268, 4269, 4270, 4271, 4272, 4273, 4274, 4275, + 4276, 4277, 4278, 4279, 4280, 4281, 4282, 4283, + 4284, 4285, 4286, 4287, 4288, -455, -455, 14151, + 6472, 4293, 4294, 4295, 4296, 4297, 4298, 4299, + 4300, 4301, 4302, 4303, 4304, 4305, 4306, 7449, + 4308, 4309, 9853, 4311, 4312, 4313, 4314, 4315, + 4316, 4317, 4318, 6174, 6175, 6176, 4322, 4323, + 4324, 4325, 4326, 4327, 4328, 4329, 4330, 4331, + 4332, 4333, 4334, 4335, 4336, 4337, 4338, 4339, + 4340, 4341, 4342, 4343, 4344, 4345, 4346, 4347, + 4348, 4349, 4350, 4351, 4352, 4353, 4354, 4355, + 4356, 4357, 4358, 4359, 4360, 4361, 4362, 4363, + 4364, 4365, 4366, -3782, 4368, -1825, 4920, 183, + 4922, -1825, 11521, 3842, -1696, 193, 193, 4929, + 4930, 4931, 4932, 4933, 4934, 4935, 4936, 5254, + 4938, 4939, 4940, 4941, 4942, 4943, 4944, 4941, + 4942, 4943, 4944, 4945, 4395, 4396, 4397, 4398, + 4399, 4400, 4401, 4402, 4403, 4404, 4405, 4406, + 4407, 4408, 4409, 4410, 4411, 4412, 4413, -330, + -330, 14276, 6597, 4418, 4419, 4420, 4421, 4422, + 4423, 4424, 4425, 4426, 4427, 4428, 4429, 4430, + 4431, 7574, 4433, 4434, 9978, 4436, 4437, 4438, + 4439, 4440, 4441, 4442, 4443, 6299, 6300, 6301, + 4447, 4448, 4449, 4450, 4451, 4452, 4453, 4454, + 4455, 4456, 4457, 4458, 4459, 4460, 4461, 4462, + 4463, 4464, 4465, 4466, 4467, 4468, 4469, 4470, + 4471, 4472, 4473, 4474, 4475, 4476, 4477, 4478, + 4479, 4480, 4481, 4482, 4483, 4484, 4485, 4486, + 4487, 4488, 4489, 4490, 4491, -3657, 4493, -1700, + 5045, 308, 5047, -1700, 11646, 3967, -1571, 318, + 318, 5054, 5055, 5056, 5057, 5058, 5059, 5060, + 5061, 5379, 5063, 5064, 5065, 5066, 5067, 5068, + 5069, 4520, 4521, 5071, 5072, 4524, 4525, 5073, + 4527, 4528, 5074, 5075, 4531, 4532, 5076, 5077, + -265, 5079, -264, -264, 5081, 5082, 5083, 5084, + 5085, 5086, 5127, 5088, 5130, 5090, 4549, 4550, + 5092, 4552, 5093, 4249, 5095, 5096, 5097, 5098, + 5099, 4560, 5100, 4266, 4266, 4257, 5104, 5105, + 5106, 5107, 5108, 5109, 5110, 7468, 5112, 5113, + 5114, 5115, 5116, 5117, 5118, 5119, 5120, 5121, + 5122, 5123, 5124, 5125, 5126, 5127, 5128, 5129, + 5130, 5131, 5132, 5133, 5134, 5135, 5136, 5137, + 5138, 5139, 5140, 5141, 5142, 5143, 5144, 5145, + 5146, 5147, 5148, 5149, 5150, 5151, 5152, 5153, + 5154, 5155, 5156, 4226, 4572, 4608, 4609, -3539, + 4611, -1582, 5163, 426, 4614, -1583, 11763, 4084, + -1454, 4085, 32767, 5169, 5170, 5171, 5172, 5173, + 5174, 5175, 5176, 32767, 5177, 5178, 5179, 5180, + 5181, 5182, 5183, 32767, 5184, 5185, 5186, 5187, + 5188, 5189, 5190, 5191, 5192, 5193, 5194, 5195, + 5196, 5197, -145, 5199, 5200, -143, 5202, 5203, + 5204, 5205, 5206, 5207, 5248, 5209, 5251, 5211, + 32767, 5212, 5213, 5214, 5215, 32767, 5216, 5217, + 5218, 5219, 5220, 32767, 5221, 32767, 2052, 32767, + 5222, 5223, 5224, 5225, 5226, 5227, 5228, 32767, + 5229, 5230, 5231, 5232, 5233, 5234, 5235, 5236, + 5237, 5238, 5239, 5240, 5241, 5242, 5243, 5244, + 5245, 5246, 5247, 5248, 5249, 5250, 5251, 5252, + 5253, 5254, 5255, 5256, 5257, 5258, 5259, 5260, + 5261, 5262, 5263, 5264, 5265, 5266, 5267, 5268, + 5269, 5270, 5271, 5272, 5273, 4343, 4689, 4690, + 5277, 5278, 5279, 5280, 5281, 5282, 5283, 5284, + 5285, 5286, 5287, 5288, 5289, 5290, 5291, 5292, + 5293, 5294, 5295, 5296, 5297, 5298, 5299, 5300, + 5301, 5302, 5303, 5304, 5305, 5306, 5307, 5308, + 5309, 5310, 5311, 5312, 5313, 5314, 5315, 5316, + 5317, 5318, 5319, 5320, 5321, 5322, 5323, 5324, + 5325, 5326, 5327, 5328, 5329, 5330, 5331, 5332, + 5333, 5334, 5335, 5336, 4691, 4692, 4693, 4694, + 4695, 4696, 4697, 5344, 5345, 5346, 5347, 5348, + 5349, 5350, 5351, 5352, 5353, 5354, 5355, 5356, + 5357, 5358, 5359, 5360, 5361, 5362, 5363, 5364, + 5365, 5366, 5367, 5368, 5369, 5370, 5371, 5372, + 5373, 5374, 5375, 5376, 5377, 5378, 5379, 5380, + 5381, 5382, 5383, 5384, 5385, 5386, 5387, 5388, + 5389, 5390, 5391, 5392, 5393, 5394, 5395, 5396, + 5397, 5398, 5399, 5400, 5401, 4815, 4472, 4472, + 4818, 4819, 5406, 5407, 5408, 5409, 5410, 5411, + 5412, 5413, 5414, 5415, 5416, 5417, 5418, 5419, + 5420, 5421, 5422, 5423, 5424, 5425, 5426, 5427, + 5428, 5429, 5430, 5431, 5432, 5433, 5434, 5435, + 5436, 5437, 5438, 5439, 5440, 5441, 5442, 5443, + 5444, 5445, 5446, 5447, 5448, 5449, 5450, 5451, + 5452, 5453, 5454, 5455, 5456, 5457, 5458, 5459, + 5460, 5461, 5462, 5463, 5464, 5465, 4820, 4821, + 4822, 4823, 4824, 4825, 4826, 5473, 5474, 5475, + 5476, 5477, 5478, 5479, 5480, 5481, 5482, 5483, + 5484, 5485, 5486, 5487, 5488, 5489, 5490, 5491, + 5492, 5493, 5494, 5495, 5496, 5497, 5498, 5499, + 5500, 5501, 5502, 5503, 5504, 5505, 5506, 5507, + 5508, 5509, 5510, 5511, 5512, 5513, 5514, 5515, + 5516, 5517, 5518, 5519, 5520, 5521, 5522, 5523, + 5524, 5525, 5526, 5527, 5528, 5529, 5530, 4944, + 5532, 5533, 5534, 5535, 5536, 5537, 5538, 5539, + 5540, 5541, 5542, 5543, 5544, 5545, 5546, 5547, + 5548, 5549, 5550, 5551, 5552, 5553, 5554, 5555, + 5556, 5557, 5558, 5559, 5560, 5561, 5562, 5563, + 5564, 5565, 5566, 5567, 5568, 32767, 32767, 5569, + 5570, 5571, 5572, 5573, 5574, 5575, 5576, 5577, + 5578, 5579, 5580, 5581, 5582, 5583, 5584, 5585, + 5586, 5587, 5588, 5589, 5590, 5591, 5592, 5593, + 5594, 5595, 5596, 5597, 5598, 5599, 5600, 5601, + 5602, 5603, 5604, 5605, 5606, 5607, 5608, 5609, + 5610, 5611, 5612, 5613, 5614, 5615, 5616, 5617, + 5618, 5619, 5620, 5621, 5622, 5623, 5624, 5625, + 5626, 5627, 5628, 5629, 5304, 5631, 5632, 5633, + 5634, 5635, 5299, 5637, 5638, 5639, 5640, 5641, + 5642, 5643, 5644, 5645, 5306, 5647, 5648, 5649, + 5650, 5651, 5652, 5653, 5654, 5655, 5656, 32767, + 5657, 5071, 5659, 5660, 5661, 5662, 5663, 5664, + 5665, 5666, 5667, 5668, 5669, 5670, 5671, 5672, + 5673, 5674, 5675, 5676, 5677, 5678, 5679, 5680, + 5681, 5682, 5683, 5684, 5685, 5686, 5687, 5688, + 5689, 5690, 5691, 5692, 5693, 5694, 5695, 5696, + 5697, 5698, 5699, 5700, 5701, 5702, 5703, 5704, + 5705, 5706, 5707, 5708, 5709, 5710, 5711, 5712, + 5713, 5714, 5715, 5716, 5717, 5718, 5719, 5720, + 5721, 5722, 5723, 5724, 5725, 5726, 5727, 5728, + 5729, 5730, 5731, 5732, 5733, 5734, 5735, 5736, + 5737, 5738, 5739, 5740, 5741, 5742, 5743, 5744, + 5745, 5746, 5747, 5748, 5749, 5750, 5751, 5752, + 5753, 5754, 5755, 5756, 5757, 5758, 5433, 5760, + 5761, 5762, 5763, 5764, 5428, 5766, 5767, 5768, + 5769, 5770, 5771, 5772, 5773, 5774, 5435, 5776, + 5777, 5778, 5779, 5780, 5781, 5782, 5783, 5784, + 5785, 5786, 5787, 5788, 5789, 5790, 5791, 5792, + 5793, 5794, 5795, 5796, 5797, 5798, 5799, 5800, + 5801, 5802, 5466, 5466, 5805, 5806, 5807, 5808, + 5809, 5810, 5811, 5468, 5468, 5814, 5815, 5816, + 5817, 5818, 5819, 5471, 5471, 5822, 5823, 5824, + 5825, 5826, 5827, 5828, 5829, 5830, 5831, 5832, + 5833, 5834, 5835, 5836, 5837, 5838, 5839, 5840, + 2057, 2058, 5843, 5844, 5845, 5846, 5847, 5848, + 5849, 5850, 5851, 5852, 5853, 5854, 5855, 5856, + 5857, 5858, 5859, 5860, 32767, 32767, 5861, 5862, + 5863, 5864, 5865, 5866, 5867, 5868, 5869, 5870, + 5871, 5872, 5873, 5874, 5875, 5876, 5877, 5878, + 5879, 5880, 5881, 5882, 5883, 5884, 5885, 5886, + 5887, 5888, 5889, 5890, 5891, 5892, 5893, 5894, + 5895, 5896, 5897, 5898, 5899, 5900, 5901, 5902, + 5903, 5904, 5905, 5906, 5907, 5908, 5909, 5910, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 890, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 891, 32767, 32767, 32767, + 32767, 32767, 32767, 4620, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 2059, 2060, 2061, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 799, 32767, 32767, 32767, - 32767, 800, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 801, 802, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, - 0, 0, 0, 0, 6545, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 32767, 32767, 32767, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 32767, 0, 0, - 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, - -5711, 241, 242, 243, 244, 0, 246, 247, - 248, 249, 250, 251, 252, 0, 0, 0, - 0, 892, 0, 0, 0, 0, 0, 0, - 264, 0, 0, 0, 32767, 32767, 0, 0, - 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 32767, 32767, 32767, 32767, + 4827, 4828, 4829, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2062, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2063, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 893, -266, 895, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 284, 285, 286, 287, 288, 0, 290, 291, - 292, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 0, 0, 0, - 0, 0, 0, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 293, 294, 295, 296, 297, 298, 32767, 32767, - 299, -4082, -4081, -4080, 303, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 32767, 0, 0, 0, 0, - 0, 32767, 0, 32767, 0, 0, 32767, 0, - 0, 32767, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -4245, -4245, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 32767, 32767, 32767, 32767, - 32767, 32767, -689, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4621, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -706, - -706, -706, -706, -706, -706, -706, -706, 32767, - 32767, -7551, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 2064, 2065, 2066, + 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, + 2075, 2076, 2077, 2078, 2079, 2080, 2081, 2082, + 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, + 2091, 2092, 2093, 2094, 2095, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, -4377, -4377, -8024, -8023, -4377, -4377, - -3916, -4377, -4377, -4377, -4377, -4377, -4377, -4377, - 32767, -4378, -4378, -4378, -4378, -4378, -4378, -4378, - -4378, -4378, -4378, -4378, -4378, -4378, -4378, -4378, - -4378, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 150, 1867, 150, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 5247, 0, 939, 0, 940, 0, 5518, - 0, 8555, 0, 0, 0, 5253, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 944, - 0, 0, 0, 0, 0, 0, 945, 0, - 946, 0, 0, 947, 948, 0, 949, 950, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -759, 959, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 960, 961, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 5204, 0, 0, 0, - 0, 962, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 963, 964, 0, - 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 4622, 4623, + 4624, 4625, 4626, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 5186, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 2096, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -961,768 +1549,94 @@ Decomp_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 2097, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 2098, + 2099, 2100, 2101, 2102, 2103, 2104, 2105, 2106, + 2107, 2108, 2109, 2110, 2111, 2112, 2113, 2114, + 2115, 2116, 2117, 2118, 2119, 2120, 2121, 2122, + 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, + 2131, 2132, 2133, 2134, 2135, 2136, 2137, 2138, + 2139, 2140, 2141, 2142, 2143, 2144, 2145, 2146, + 2147, 2148, 2149, 2150, 2151, 2152, 2153, 2154, + 2155, 2156, 2157, 2158, 2159, 2160, 2161, 2162, + 2163, 2164, 2165, 2166, 2167, 2168, 2169, 2170, + 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, + 2179, 2180, 2181, 2182, 2183, 2184, 2185, 2186, + 2187, 2188, 2189, 2190, 2191, 2192, 2193, 2194, + 2195, 2196, 2197, 2198, 2199, 2200, 2201, 2202, + 2203, 2204, 2205, 2206, 2207, 2208, 2209, 2210, + 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2218, + 2219, 2220, 2221, 2222, 2223, 2224, 2225, 2226, + 2227, 2228, 2229, 2230, 2231, 2232, 2233, 2234, + 2235, 2236, 2237, 2238, 2239, 2240, 2241, 2242, + 2243, 2244, 2245, 2246, 2247, 2248, 2249, 2250, + 2251, 2252, 2253, 2254, 2255, 2256, 2257, 2258, + 2259, 2260, 2261, 2262, 2263, 2264, 2265, 2266, + 2267, 2268, 2269, 2270, 2271, 2272, 2273, 2274, + 2275, 2276, 2277, 2278, 2279, 2280, 2281, 2282, + 2283, 2284, 2285, 2286, 2287, 2288, 2289, 2290, + 2291, 2292, 2293, 2294, 2295, 2296, 2297, 2298, + 2299, 2300, 2301, 2302, 2303, 2304, 2305, 2306, + 2307, 2308, 2309, 2310, 2311, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 5256, -4973, -4973, 32767, - 5261, 5262, 5263, 5264, 5265, 5266, 5267, -4974, - -4974, 978, 979, 980, 981, 32767, 982, 983, - 984, 985, 986, 987, 988, 32767, 32767, 32767, - 32767, 1624, 32767, 32767, 32767, 32767, 32767, 32767, - 990, 32767, 32767, 0, 991, 992, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4759, - 4760, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 2312, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -723, -722, -2974, 32767, 996, -719, -168, - -717, -716, -715, 1002, 1003, 1004, -712, -711, - 32767, -710, 1008, 1009, 1010, 1011, -2060, 5892, - 2764, 2764, 2764, 1017, 1018, 1019, 1020, 1021, - 1022, 2567, 1024, 32767, 1025, 1026, 1027, 1028, - 1029, 1030, 1031, 1032, 1033, 1034, 2589, 1036, - 1037, -1729, -1729, -1729, -1729, 1042, 4211, 4211, - 4211, 4211, 4211, 4211, 1049, 1050, 4209, 1427, - 32767, 32767, 32767, 0, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -825, 32767, 32767, - 32767, 32767, 32767, 4718, 4719, 4720, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 4721, 4722, - 4723, 4724, 4725, 4726, 4727, 6291, 6292, 6293, - 4728, 4729, 4730, 4731, 4732, 6299, 6300, -4992, - 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, - 1078, 1079, 1080, 4733, 1082, 4734, 1084, 1085, - 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, - 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, - 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, - 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, - 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, - 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, - 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, - 1142, 1143, 1144, 1145, 1146, 4737, 1148, 4738, - 4739, 1151, 1152, 1153, 1154, 32767, 4740, 4741, - 4742, 1158, 1159, 1160, 1161, 1162, 1163, 1164, - 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, - 1173, 1174, 1175, 1176, 1177, 1178, 1179, 1180, - 1181, 1182, 1183, 1184, 1185, 1186, 1187, 1188, - 1189, 1190, 1191, 1192, 1193, 1194, 11979, 6272, - 1758, 1758, 1199, 1200, 1201, 1202, 1203, 1204, - 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, - 1213, 1214, 1215, 1216, 1217, 1218, 1219, 1220, - 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, - 1229, -1074, -1073, 1232, 1233, 1234, 1235, 1236, - 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, - 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, - 1253, 1254, 1255, 1256, 1257, 1258, 1259, 1260, - 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, - 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, - 1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284, - 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, - 1293, 1294, 1295, 1296, 1297, 1298, 1299, 1300, - 1301, 1302, 1303, 1304, 1305, 1306, 1307, 4676, - 4677, 512, 32767, 32767, 32767, 32767, 1683, 1684, - 1685, 1314, 155, 1316, 1317, 1318, 1319, 1320, - 1321, 161, 1323, 1324, 1325, 1326, 1327, 1328, - 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, - 1337, 1338, -4375, -4374, -8536, -8536, -8536, -8536, - 1345, 1346, 1444, 1445, 1446, 1447, 1448, -4360, - 1353, 1354, -8525, -5383, 1357, -8524, -8524, -8524, - -8524, -4348, 1363, -2456, 686, 1366, 1367, -8518, - -8518, -7581, 1371, 1372, -5364, 1374, 1375, -4332, - -4331, 1378, 1379, 1380, -4327, -8841, -8841, -8841, - -8841, -8841, -8841, -8841, 1389, 1390, -8843, -8843, - -8843, -8843, -8843, -8843, -8843, -8843, 1399, 1400, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 5957, - 2655, -5028, -5028, 2658, 2659, -3322, -1679, -1679, - -1679, -1679, -1679, -7010, -1679, -1679, -7010, -7010, - -1679, -1679, -1679, -1679, -1679, 32767, 32767, -1681, - -1681, -1681, -1681, -1681, -1681, 32767, 32767, -1683, - -1683, -3290, -3289, -3288, -3287, -3286, -3285, -12842, - 7263, -3282, -499, -9550, -499, -9549, -9549, -9549, - 96, -9549, -9816, -9549, 1704, -9548, -1864, -1863, - -9548, -9548, -3566, -5208, -5207, -5206, -5205, -5204, - 128, -5202, 131, 132, 133, 32767, 32767, -5195, - -3568, -3568, -5190, -5189, -3568, 32767, 32767, -5185, - -5184, -5183, -3570, -5180, 1478, -5179, -3571, 32767, - -3572, 32767, -3573, 32767, 5984, 32767, -3575, 2695, - 2696, 2697, 2698, 2699, 2700, -6944, 2702, 2970, - 2704, 6008, 2706, -4977, -4977, 2709, 2710, -3271, - 1502, -3478, 1504, -3626, -3626, -7766, 6033, 1509, - -3625, -7767, 1512, 1513, 1514, 32767, 32767, -3625, - -3625, -3625, -3625, -3625, -3625, -3625, 1522, -3624, - 1524, 1525, 1526, 1527, 1528, 1529, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3128, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1581, -673, 0, 784, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 3025, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -6843, - -6843, 0, 0, 0, 1619, 1620, 1621, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -2788, 32767, 32767, - 1634, 1635, 1636, 1637, 1638, 1639, 1640, 1641, - 4194, 1643, 1644, 32767, 32767, 32767, 32767, 32767, - 32767, 1352, 32767, 32767, 32767, 32767, 32767, 1646, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 1647, 1648, 1649, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1650, - 32767, 32767, 32767, 1651, 1652, 32767, 1653, 1654, - 32767, 32767, 32767, 32767, 1655, 32767, 1656, 32767, - 32767, 32767, 32767, 399, 399, 32767, 32767, 1657, - 1658, 1659, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 0, 0, 0, 32767, 32767, 32767, 1660, - 32767, 32767, 32767, 32767, 32767, 6619, 32767, 1661, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 5971, 5972, 5973, 5974, 1664, 5975, 6243, 5977, - 9281, 5979, -1704, -1704, 5982, 5983, 2, 4775, - -205, 4777, -353, -353, -4493, 9306, 4782, -352, - -4494, 4785, 4786, 4787, 4788, -350, -350, 6044, - 1691, 6045, 6046, 1694, 6047, 1696, 1697, 6048, - 1699, 6049, 6050, 6051, 6052, 0, 0, 0, - 0, 0, 0, 32767, 0, 0, 0, 0, - 1704, 0, 32767, 0, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 32767, 32767, 32767, -3338, -3338, - -3338, 0, 32767, 0, 32767, 0, 0, 0, - 32767, 0, 0, 32767, 0, 32767, 32767, 0, - 0, 0, 0, 0, 32767, 0, 32767, 0, - 1705, 1706, 1707, 1708, 1709, 759, 759, 759, - 759, 759, 759, 759, 0, 0, 0, 0, - 0, 3035, 0, 32767, 0, 1719, 10109, 1721, - 1722, 1723, 1724, 1725, 1726, 1727, 1728, 1729, - 1730, 0, 0, 0, 0, 0, 0, 0, - 0, 9764, 6757, 6758, 6759, 8018, 0, 0, - 0, 1720, 1720, 1720, 0, 0, 2557, 2558, - 1720, 0, -1282, 0, -1281, -1281, -1281, 633, - 1720, -1279, -1279, 1720, 4767, 0, 1720, 1720, - 0, 0, 1718, 1718, -3263, 0, 6007, 6008, - -7790, 6010, 6011, 6012, -3266, 1719, 0, 1718, - 0, 1717, 0, -150, 1717, 1717, 1717, 32767, - 1716, 1716, 1716, 3969, 1715, 0, 1716, 1166, - 1716, 1716, 1716, 0, 0, 0, 1717, 1717, - 1717, 1717, 0, 0, 0, 0, 3072, -4879, - -1750, -1749, -1748, 0, 0, 0, 0, 0, - 0, -1544, 0, 1789, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -1554, 0, - 0, 2767, 2768, 2769, 2770, 0, -3168, -3167, - -3166, -3165, -3164, -3163, 0, 0, -3158, -375, - -9426, -375, -9425, 1821, 1822, 218, -9427, 1825, - -9426, 1827, 1828, -1742, -1741, 1878, 1879, 1833, - 1834, 32767, 32767, 32767, 32767, 2642, -4765, -1739, - -1738, 32767, 1835, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 2718, -3491, 3174, 3174, 3174, - -3669, -3669, -3669, -3669, -3669, -5232, -5232, -5232, - -3666, -3666, -3666, -3666, -3666, -5232, -5232, 6061, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -3652, 0, -3651, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, -3590, 0, -3589, - -3589, 0, 0, 0, 0, 32767, 1842, 32767, - 32767, 32767, 32767, 1843, 32767, 32767, 1844, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 1845, 32767, - 1846, 32767, 32767, 32767, 32767, 32767, 1847, 1848, - 0, 1849, 1850, 0, 32767, 32767, 0, 0, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 1851, 32767, 32767, 1852, 32767, - 32767, 1853, 32767, 1854, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, -4903, 32767, -6162, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 137, - 1858, 3141, 1860, 3142, 32767, 32767, 1229, 143, - 32767, 32767, 144, -2902, 0, 0, 32767, 32767, - 32767, 32767, 148, 5130, 0, 0, -4140, 9659, - 32767, 0, -4142, 5137, 5138, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 32767, - 0, 5148, 5149, 5150, 5151, 5152, 5153, 4782, - 3623, 3623, 3623, 3623, 3623, 3623, 3623, 3623, - 3623, 3623, 3623, 3623, 3623, 3623, 6751, 3623, - 3623, 3623, 3623, 3623, 3623, 3623, 3623, 3623, - 3623, 3623, 3623, 3623, 32767, 3622, 3622, 3622, - 3622, 32767, 3621, 3621, 3621, 0, 3622, 32767, - 3621, 32767, 32767, 32767, 3618, 3618, 3618, 3618, - 3618, 3618, 3618, 32767, 2944, 0, 4402, 3618, - 3618, 3618, 3618, 3618, 3618, 3618, 3618, 3618, - 3618, 3618, 3618, 3618, 0, 0, 3620, 3620, - 3620, 3620, 3620, 3620, -762, 6646, 3621, 3621, - 3621, 3621, 3621, 3621, 3621, 3621, 3621, 3621, - -3222, -3222, 3621, 3621, 3621, 5240, 5241, 5242, - 0, 0, 3623, 3623, 3623, 0, 3624, 3624, - 3624, 3624, 3624, 3624, 3624, 3624, 836, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -3647, -3646, 0, 0, - 461, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 468, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 0, 32767, 0, 470, 471, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 472, 473, 474, 475, 476, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 775, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 4803, 4804, 7596, 7596, 7596, - 4808, 4809, 4810, 4811, 4812, 4813, 4814, 4815, - 4816, 4817, 4818, 4819, 4820, 4821, 4822, 4823, - 4824, 3988, 3988, 4827, 4828, 7830, 1792, 7832, - 7833, 7834, 5921, 4835, 7835, 7836, 4838, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 3036, 6038, 0, - 6040, 6041, 6042, 4129, 3043, 6043, 6044, 3046, - 0, 3048, 3049, 3050, 6046, 6047, 3053, 3054, - 3055, 3056, -1230, 3058, 3059, -1232, -1232, 3062, - 3063, 3064, 3065, 3066, 3067, 3068, 3069, 3070, - 3071, 3072, 3073, 3074, 3075, 3076, 3077, 3078, - 3079, 3080, 3081, 3632, 3083, 3084, 3085, 3086, - 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, - 3095, 3096, 3097, 3098, 3099, 3100, 3101, 3102, - 3103, -814, 3105, 3106, 3107, 3108, -6867, 3110, - 3111, 3112, 3113, 9179, 9180, 3116, 3117, 3118, - 3119, -22, 3121, 3122, 3123, 3124, 3125, 3126, - 3127, 3128, 3129, 3130, 2196, 3132, 3133, 2197, - 3135, 3136, 3137, 3138, 3139, 3140, 3141, 3142, - 3143, 3144, -6500, 3146, 3414, 3148, 6452, 3150, - -4533, -4533, 3153, 3154, -2827, -1184, -1184, -1184, - -1184, -1184, -6515, -1184, -1184, -6515, -6515, -1184, - -1184, -1184, -1184, -1184, -1184, -1184, -1184, -1184, - -1184, -1184, -1184, -1184, -1184, -1184, -1184, -1184, - -2791, -2790, -2789, -2788, -2787, -2786, -12343,7762, - -2783, 0, -9051, 0, -9050, -9050, -9050, 595, - -9050, -9317, -9050, 2203, -9049, -1365, -1364, -9049, - -9049, -3067, -4709, -4708, -4707, -4706, -4705, 627, - -4703, 630, 631, 632, -3067, -3067, -4694, -3067, - -3067, -4689, -4688, -3067, 3131, -4683, -4682, -4681, - -4680, -3067, -4677, 1981, -4676, -3068, -3068, -3068, - -3068, -3068, -3068, 6490, -13614,-3068, 3202, 3203, - 3204, 3205, 3206, 3207, -6437, 3209, 3477, 3211, - 6515, 3213, -4470, -4470, 3216, 3217, -2764, 2009, - -2971, 2011, -3119, -3119, -7259, 6540, 2016, -3118, - -7260, 2019, 2020, 2021, 2022, -3116, -3116, 3278, - 3279, 3280, 3281, 3282, 3283, 3284, 3285, 3286, - 3287, 3288, 3289, 3290, 3291, 32767, 3292, 3293, - -869, -869, -869, -869, 9107, 9108, 9109, 9110, - 9111, 9112, 9113, 3305, 32767, 32767, -863, 32767, - -864, 32767, 32767, -866, -866, 3310, 3311, -864, - -864, -864, 71, -864, -864, 32767, 72, 32767, - 2290, 32767, 32767, 3320, 3321, 32767, 32767, 32767, - 3322, -1192, -1192, -1192, -1192, -1192, -1192, -1192, - -1192, -1192, -1192, -1192, -1192, -1192, -1192, -1192, - -1192, -1192, -1192, -1192, -1192, -1192, -1192, -1192, - -1192, -1192, -1192, -1192, -1192, -1192, -1192, -1192, - -1192, -1192, -1192, -1192, -1192, -1192, -1192, -1192, - -1192, -1192, -1192, 3365, 3366, 3367, -1189, -1189, - -1189, -1189, -1189, -1189, 3374, 3375, -1187, -1187, - -1187, -1187, -1187, -1187, 3382, 3383, -1185, -1185, - -1185, -1185, -1185, -1185, 32767, 32767, -1185, -1185, - -1185, 3393, 3394, 3395, -1182, 3397, 3398, 3399, - 3400, 3401, 3402, 1274, 1274, 3405, -5404, 3407, - 3408, 3409, 3410, 1274, 1274, 1274, 1274, 1274, - 1274, 1274, 1274, 1274, 1274, 1274, 1274, 1274, - 1274, 1274, 1274, 1274, 1274, 1274, 1274, 1274, - 3432, 3433, 3434, 3435, 3436, 3437, 3438, 3439, - 3440, 3441, 3442, 3443, 3444, 3445, 3446, 3447, - 3448, 3449, 3450, 3451, 3452, 3453, 3454, 3455, - -1138, -1138, -1138, -1138, -1138, 3461, 3462, 3299, - 3464, 3465, 3466, 3467, 3468, 3469, 3470, 3471, - 3472, 3473, 3474, 3475, 3476, 3477, 3478, 3479, - 3480, 3481, 3482, 3483, 3484, 3485, 3486, 3487, - 3488, 3489, 3490, 3491, 3492, 3493, 3494, 3495, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3496, - 3497, 3498, 3499, 3500, 3501, 3502, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 3503, 3211, 3211, 3211, 3211, 32767, - 32767, 32767, 32767, 32767, 7590, 7590, 7590, 3208, - 3512, 3513, 3514, 3515, 3516, 3517, 3518, 3519, - 3520, 3521, 3522, 3523, 3524, 3525, 3526, 3527, - 3528, 3529, 3530, 3531, 3532, 3533, 32767, 3534, - 3535, 3536, 3537, 3538, 32767, 3539, 32767, 3540, - 3541, 32767, 3542, 3543, 32767, 3544, 3545, 3546, - 3547, 3548, 3549, 3550, 3551, 3552, 3553, 3554, - 3555, 3556, 3557, 3558, 3559, 3560, 3561, 3562, - 3563, 3564, 3565, 3566, 3567, 3568, 3569, 3570, - 3571, 3572, 3573, 3574, 3575, 3576, 3577, 3578, - 3579, 3580, 3581, 3582, 3583, 3584, 3585, 3586, - 3587, 3588, 3589, 3590, 3591, 3592, 3593, 3594, - 3595, 3596, 3597, 3598, 3599, 3600, 3601, 3602, - 3603, 3604, 3605, 3606, 3607, 3608, 3609, 3610, - 3611, 3612, 3613, 3614, 3615, 3616, 3617, 3618, - 3619, 3620, 3621, 3622, 3623, 3624, 3625, 3626, - 3627, 3628, 3629, 3630, 3631, 3632, 3633, 3634, - 3635, 3636, 3637, 3638, 3639, 3640, 3641, 3642, - 3643, 3644, 3645, 3646, 3647, 3648, 3649, 3650, - 3651, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 3652, 3653, 3654, 3655, 3656, 3657, - 3658, 3659, 3660, 3661, 3662, 3663, 3664, 3665, - 3666, 3667, 3668, 3669, 3670, 3671, 3672, 3673, - 3674, 3675, 3676, 3677, 3678, 3679, 3680, 3681, - 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, - 3690, 3691, 3692, 3693, 3694, 3695, 3696, 32767, - 3244, 3698, 3699, 3700, 3701, 3702, 4131, 3704, - 3251, 3251, 3707, 3252, 3709, 3710, 3711, 3255, - 3713, 3714, 3715, 3716, 3259, 3259, 3259, 3720, - 3721, 3722, 3723, 3724, 3725, 3726, 3727, 3728, - 3729, 3730, 3731, 3732, 0, 0, 3735, 3736, - 3737, 3738, 3739, 3740, 3741, 3742, 3743, 3744, - 3745, 3746, 3747, 3748, 3749, 3750, 3751, 3752, - 3753, 3754, 3755, 3756, 3757, 3758, 3759, 3760, - 3761, 3300, 3763, 3764, 3765, 3766, 3767, 3768, - 3769, 3770, 3771, 3772, 3773, 3774, 3775, 3776, - 3777, 3778, 3779, 3780, 3781, 3782, 3783, 3784, - 3322, 3322, 3787, 3323, 3789, 3790, 3791, 3326, - 3793, 3794, 3795, 3796, 3330, 3798, 3330, 3800, - 3801, 3802, 3803, 3804, 3805, 3806, 3807, 3808, - 3809, 3810, 3811, 3812, 3813, 3814, 3815, 3816, - 3817, 3818, 3819, 3820, 3821, 3822, 3823, 3824, - 3825, -1667, 3827, 3828, 3829, 3830, 3831, 3832, - 3833, 3834, 3835, 3836, 3837, 3838, 3839, 3840, - 3841, 3842, 3843, 3844, -1685, 3846, 3847, 3848, - 3849, 3850, 3851, 3852, 3853, 3854, 3855, 3856, - 3857, 3858, 3859, 3860, 3861, 3862, 3863, 3864, - 3865, 3866, 3867, 3868, 3869, 3870, 3871, 3872, - 3873, 3874, 3875, 3876, 3877, 3878, 3879, 3880, - 3881, 3882, 3883, 3884, 3885, 3886, 3887, 3888, - 3889, 3890, 3891, 3892, 3893, 3894, 3895, 3896, - 3897, 8237, 8238, 3900, 3901, 3902, 3903, 3904, - 3905, 3906, 3907, 3908, 3909, 3910, 3911, 3912, - 11266, 11267, 11268, 9355, 3917, 3918, 11272, 8274, - 3921, 3922, 3438, 3438, 3438, 3438, 3438, 3438, - 3929, 3930, 3440, 3440, 3440, 3440, 6476, 9478, - 3937, 3938, 9483, 9484, 7571, 6485, 9485, 9486, - 6488, 3442, 6490, 6491, 6492, 9488, 3951, 3952, - 32767, -866, -866, -866, -866, -866, -866, -29, - -28, -866, -866, -3867, 2172, -3867, -3867, -3867, - -1953, -866, -3865, -3865, -866, 3973, 3974, 3975, - 3976, 3977, 3978, 3979, 3980, 3981, 3982, 3983, - 3984, 3985, 3986, 951, -2050, 3989, -2050, -2050, - -2050, -136, 951, -2048, -2048, 951, 3998, 951, - 951, 951, -2044, -2044, 951, 951, 951, 951, - 5238, 951, 951, 5243, 5244, 951, 951, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 383, 933, 933, 933, 933, 933, 933, - 933, 933, 933, 933, 933, 933, 933, 933, - 933, 933, 933, 933, 933, 933, 933, 4851, - 933, 933, 933, 933, 10909, 933, 933, 933, - 933, -5132, -5132, 933, 933, 933, 933, 4075, - 933, 933, 933, 933, 933, 933, 933, 933, - 933, 933, 1868, 933, 933, 1870, 933, 933, - 933, 933, 933, 933, 933, 933, 933, 933, - 10578, 0, 32767, 931, -2372, 931, 8615, 8616, - 931, 931, 6913, 5271, 5272, 5273, 5274, 5275, - 10607, 5277, 5278, 10610, 10611, 5281, 5282, 5283, - 5284, 5285, 5286, 5287, 5288, 5289, 5290, 5291, - 5292, 5293, 5294, 5295, 5296, 5297, 6905, 6905, - 6905, 6905, 6905, 6905, 16463, -3641, 6905, 4123, - 13175, 4125, 13176, 13177, 13178, 3534, 13180, 13448, - 13182, 32767, 32767, 32767, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 0, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -2357, 17748, 7203, 934, 934, 934, 934, - 934, 934, 10579, 934, 667, 934, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 4146, 4147, 4148, 4149, 4150, 4151, - 4152, 4153, 4154, 4155, 32767, 32767, 32767, 32767, - 32767, 32767, 4156, 4157, 4158, 4159, 4160, 4161, - 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, - 4170, 4171, 4172, 4173, 4174, 4175, 4176, 4177, - 4178, 4179, 4180, 4181, 4182, 5374, 5375, 5376, - 6978, 4187, 4188, 4189, 4190, 6982, 6982, 32767, - 32767, 4193, 4194, 4195, 4196, 4197, 4198, 4199, - 4200, 4201, 4202, 4203, 4204, 32767, 6995, 6995, - 4207, 4208, 4209, 4210, 4211, 4212, 4213, 4214, - 4215, 4216, 4217, 4218, 4219, 4220, 4221, 4222, - 4223, 32767, 3386, 4225, 4226, 7228, 32767, 32767, - 32767, 32767, 5315, 4229, 7229, 32767, 4231, 32767, - 4232, 4233, 4234, 7230, 7231, 4237, 4238, 4239, - 4240, -46, 4242, 4243, -48, -48, 4246, 4247, - 4248, 4249, 4250, 4251, 4252, 4253, 4254, 4255, - 4256, 4257, 4258, 4259, 4260, 4261, 4262, 4263, - 4264, 4265, 4816, 4267, 4268, 4269, 4270, 4271, - 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279, - 4280, 4281, 4282, 4283, 4284, 4285, 4286, 4287, - 370, 4289, 4290, 4291, 4292, -5683, 4294, 4295, - 4296, 4297, 10363, 10364, 4300, 4301, 4302, 4303, - 1162, 4305, 4306, 4307, 4308, 4309, 4310, 4311, - 4312, 4313, 4314, 3380, 4316, 4317, 3381, 4319, - 4320, 4321, 4322, 4323, 4324, 4325, 4326, 4327, - 4328, -5316, 4330, 4598, 4332, 7636, 4334, -3349, - -3349, 4337, 4338, -1643, 0, 0, 0, 0, - 0, -5331, 0, 0, -5331, -5331, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 4126, - 4127, 4128, 4129, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, -1258, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - -1272, -1272, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -1851, -1851, -1851, -1851, - -1851, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 3235, 0, -6925, 306, - 306, 306, 306, 306, 306, 306, 306, 306, - 306, 306, 306, -1764, -1764, 4472, 4473, 311, - 311, 311, 311, 10287, 10288, 10289, 10290, 10291, - 10292, 10293, 4485, 10198, 319, 319, 3461, 319, - 319, 319, 319, 319, 4495, 4496, 321, 321, - 321, 1256, 321, 321, 1258, 1258, 0, 3477, - 10215, 10216, 4509, 4510, 10219, 10220, 10221, 4514, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 4579, 4580, 4581, 4582, - 4583, 4584, 32767, 2455, 4586, -4223, 4588, 4589, - 4590, 4591, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 4885, 4886, 4887, - 4888, 4889, 4890, 4891, 4892, 4893, 4894, 4895, - 4896, 4897, 4898, 4899, 4900, 4901, 4902, 4903, - 4904, 4616, 4906, 4907, 4908, 4909, 4910, 4911, - 4912, 4913, 4914, 4915, 4916, 4917, 4918, 4919, - 4920, 4921, 4922, 4923, 4924, 4925, 4926, 4927, - 4928, 4929, 4930, 4931, 4932, 4933, 4934, 4935, - 4936, 4937, 4938, 4939, 4940, 4941, 4942, 4943, - 4944, 4945, 4946, 4947, 4948, 567, 568, 569, - 4952, -2455, 571, 572, 4956, 4957, 4958, 4959, - 4960, 4961, 4962, 4963, 4964, 4965, 4966, 4967, - 4968, 4969, 32767, 4970, 4971, 4972, 4973, 4974, - 4975, 4976, 4977, 4978, 4979, 4980, 6623, 6623, - 6623, 6623, 6623, 1292, 6623, 1291, 1291, 1291, - 4991, 4992, 6620, 4994, 4995, 6618, 6618, 4998, - -1199, 6616, 6616, 6616, 6616, 5004, 6615, 6615, - 6615, 5008, 5009, 5010, 5011, 5012, 5013, -4544, - 15561, 5016, -1253, -1253, -1253, -1253, -1253, -1253, - 8392, -1253, -1520, -1253, -4556, -1253, 6431, 6432, - -1253, -1253, 4729, 3087, 3088, 3089, 3090, 3091, - 8423, 3093, 3094, 8426, 8427, 3097, 3098, 3099, - 3100, 3101, 3102, 3103, 3104, 3105, 3106, 3107, - 3108, 3109, 3110, 3111, 3112, 3113, 4721, 4721, - 4721, 4721, 4721, 4721, 14279, -5825, 4721, 1939, - 10991, 1941, 10992, 10993, 10994, 1350, 10996, 11264, - 10998, -254, 10999, 3316, 3316, 11002, 11003, 5022, - 6665, 6665, 6665, 6665, 6665, 1334, 6665, 1333, - 1333, 1333, 5033, 5034, 6662, 5036, 5037, 6660, - 6660, 5040, -1157, 6658, 6658, 6658, 6658, 5046, - 6657, 0, 6658, 5051, 5052, 5053, 5054, 5055, - 5056, -4501, 15604, 5059, -1210, -1210, -1210, -1210, - -1210, -1210, 8435, -1210, -1477, -1210, -4513, -1210, - 6474, 6475, -1210, -1210, 4772, 0, 4981, 0, - 5131, 5132, 9273, -4525, 0, 5135, 9278, 0, - 0, 0, 0, 5139, 5140, 5141, 5142, 5143, - 5144, 5145, 5146, 0, 5147, 0, 0, 0, - 0, 0, 0, 1530, 1531, 1532, 1533, 1534, - 1535, 1536, 1537, 1538, 1539, 1540, 1541, 1542, - 1543, 1544, -1583, 1546, 1547, 1548, 1549, 1550, - 1551, 1552, 1553, 1554, 1555, 1556, 1557, 1558, - 1559, 1560, 1561, 1562, 1563, 1564, 1565, 1566, - 1567, 5189, 1568, 1569, 1570, 1571, 1572, 1573, - 1574, 1575, 1576, 1577, 1578, 1579, 1580, 0, - 2255, 5200, 799, 1584, 1585, 1586, 1587, 1588, - 1589, 1590, 1591, 1592, 1593, 1594, 1595, 1596, - 5215, 5216, 1597, 1598, 1599, 1600, 1601, 1602, - 5985, -1422, 1604, 1605, 1606, 1607, 1608, 1609, - 1610, 1611, 1612, 1613, 8457, 8458, 1616, 1617, - 1618, 0, 0, 0, 5243, 5244, 1622, 1623, - 1624, 5248, 1625, 1626, 1627, 1628, 1629, 1630, - 1631, 1632, 4421, 5258, 5259, 5260, 5261, 5262, - 5263, 5264, 5265, 5266, 5267, 5268, 5269, 5270, - 5271, 5272, 5273, 5274, 5275, 5276, 5277, 5278, - 5279, 5280, 5281, 5282, 5283, 5284, 5285, 5286, - 8934, 8934, 5289, 5290, 4830, 5292, 5293, 5294, - 5295, 5296, 5297, 5298, 5299, 5300, 5301, 5302, - 5303, 5304, 5305, 5306, 5307, 5308, 5309, 5310, - 5311, 5312, 5313, 5314, 5315, 5316, 5317, 5318, - 5319, 5320, 5321, 5322, 5323, 5324, 5325, 5326, - 5327, 5328, 5329, 5330, 5331, 5332, 5333, 5334, - 5335, 5336, 5337, 5338, 5339, 5340, 5341, 5342, - 5343, 5344, 5345, 5346, 5347, 5348, 5349, 5350, - 5351, 5352, 5353, 5354, 5355, 5356, 5357, 5358, - 4891, 5360, 5361, 5362, 5363, 5364, 5365, 5366, - 5367, 5368, 5369, 5370, 5371, 5372, 32767, 5373, - 5374, -5231, 5376, 4532, 0, 5379, 5380, 5381, - 5382, 4270, 4271, 4272, 4273, 4274, 4275, 4276, - 4277, 4278, 12230, 9102, 9102, 9102, 5396, 8890, - 5398, 5399, 5400, 5401, 8899, 7356, 5568, 7358, - 7359, 0, 0, 7364, 7365, 7366, 7367, 7368, - 7369, 8924, 7371, 7372, 4606, 4606, 4606, 4606, - 7377, 10546, 10546, 10546, 10546, 10546, 10546, 20104, - 0, 10546, 7764, 16816, 7766, 16817, 5572, 5572, - 7177, 16823, 17091, 16825, 5573, 5573, 9144, 9144, - 5526, 0, 5574, 5574, 5450, 5451, 5452, 697, - 697, 5455, 0, 7172, 5458, 5576, 5460, -5456, - 5462, 5463, 5464, 0, 0, 4691, 5468, 5469, - 4693, 5471, 10898, 0, 0, 5475, 4698, 10908, - 5478, 5479, 5480, 5481, 1359, 1359, 1359, 5485, - 5486, 5487, 5488, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 101, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, 1359, 1359, 1359, 1359, - 87, 87, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, -492, -492, -492, -492, - -492, 1359, 1359, 1359, 1359, 1359, 1359, 1359, - 1359, 1359, 1359, 1359, 4594, 1359, -5566, 1665, - 1665, 1665, 1665, 1665, 1665, 1665, 1665, 1665, - 1665, 1665, 1665, -405, -405, 5585, 5586, 1668, - 1668, 1668, 1668, 11644, 11645, 11646, 11647, 11648, - 11649, 11650, 5842, 11555, 1676, 1676, 4818, 1676, - 1676, 1676, 1676, 1676, 5852, 5853, 1678, 1678, - 1678, 2613, 1678, 1678, 2615, 2615, 1357, 4834, - 11572, 11573, 5866, 5867, 11576, 11577, 11578, 5871, - 1357, 1357, 1357, 1357, 1357, 1357, 1357, 1357, - 1357, -6594, -3465, -3464, -3463, 244, -3249, 244, - 244, 244, 244, -3253, -1709, 80, -1709, -1709, - 5651, 5652, -1711, -1711, -1711, -1711, -1711, -1711, - -3265, -1711, -1711, 1056, 1057, 1058, 1059, -1711, - -4879, -4878, -4877, -4876, -4875, -4874, -14431,5674, - -4871, -2088, -11139,-2088, -11138,108, 109, -1495, - -11140,-11407,-11140,113, 114, -3456, -3455, 164, - 5691, 118, 119, 244, 244, 244, 5000, 5001, - 244, 5700, -1471, 244, 127, 244, 11161, 244, - 244, 244, 5709, 5710, 1020, 244, 244, 1021, - 244, -5182, 5717, 5718, 244, 1022, -5187, 244, - 244, 244, 244, 4367, 4368, 4369, 244, 244, - 244, 244, 4374, 4375, 4376, 4377, 4378, 4379, - 4380, 4381, 4382, 4383, 4384, 4385, 4386, 4387, - 5646, 4389, 4390, 4391, 4392, 4393, 4394, 4395, - 4396, 4397, 4398, 4399, 4400, 4401, 4402, 5675, - 5676, 4405, 4406, 4407, 4408, 4409, 4410, 4411, - 4412, 4413, 4414, 4415, 4416, 4417, 4418, 4419, - 4420, 4421, 4422, 4423, 4424, 4425, 4426, 4427, - 4428, 4429, 4430, 4431, 4432, 4433, 4434, 4435, - 4436, 4437, 4438, 6290, 6291, 6292, 6293, 6294, - 4444, 4445, 4446, 4447, 4448, 4449, 4450, 4451, - 4452, 4453, 4454, 1220, 4456, 11382, 4152, 4153, - 4154, 4155, 4156, 4157, 4158, 4159, 4160, 4161, - 4162, 4163, 6234, 6235, 0, 0, 4163, 4164, - 4165, 4166, -5809, -5809, -5809, -5809, -5809, -5809, - -5809, 0, -5712, 4168, 4169, 1028, 4171, 4172, - 4173, 4174, 4175, 0, 0, 4176, 4177, 4178, - 3244, 4180, 4181, 3245, 3246, 4505, 1029, -5708, - -5708, 0, 0, -5708, -5708, -5708, 0, 4515, - 4516, 4517, 4518, 4519, 4520, 4521, 4522, 4523, - 4524, 4525, 4526, 4527, 4528, 4529, 4530, 4531, - 4532, 4533, 4534, 4535, 4536, 4537, 4538, 4539, - 4540, 4541, 4542, 4543, 4544, 4545, 4546, 4547, - 4548, 4549, 4550, 4551, 4552, 4553, 4554, 4555, - 4556, 0, 0, 0, 4557, 4558, 4559, 4560, - 4561, 4562, 0, 0, 4563, 4564, 4565, 4566, - 4567, 4568, 0, 0, 4569, 4570, 4571, 4572, - 4573, 4574, 2114, 2115, 4575, 4576, 4577, 0, - 0, 0, 4578, 0, 0, 0, 0, 0, - 0, 2129, 2130, 0, 8810, 0, 0, 0, - 0, 2137, 2138, 2139, 2140, 2141, 2142, 2143, - 2144, 2145, 2146, 2147, 2148, 2149, 2150, 2151, - 2152, 2153, 2154, 2155, 2156, 2157, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 4594, 4595, - 4596, 4597, 4598, 0, 0, 164, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 2222, 2223, - 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, - 2232, 2233, 2234, 2235, 2236, 2237, 2238, 2239, - 2240, 2241, 2242, 2243, 2244, 2245, 2246, 2247, - 2248, 2249, 2250, 2251, 2252, 2253, 2254, 2255, - 2256, 2257, 2258, 2259, 2260, 2261, 2262, 2263, - 2264, 2265, 2266, 2267, 2268, 2269, 2270, 705, - 706, 707, 708, 709, 2276, 2277, 2278, 2279, - 2280, 2281, 2282, 2283, 2284, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 2285, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 2286, 2287, 2288, 2289, 2290, 2291, - 32767, 32767, 32767, 32767, 32767, 32767, 2292, 32767, - 2293, 2294, 2295, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 2296, 32767, 2297, 32767, - 2298, 32767, 2299, 32767, 2300, 32767, 2301, 32767, - 2302, 32767, 2303, 32767, 2304, 32767, 2305, 32767, - 2306, 32767, 2307, 32767, 32767, 2308, 32767, 2309, - 32767, 2310, 32767, 32767, 32767, 32767, 32767, 32767, - 2311, 2312, 32767, 2313, 2314, 32767, 2315, 2316, - 32767, 2317, 2318, 32767, 2319, 2320, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 2321, 32767, 32767, 32767, - 32767, 2322, 2323, 2324, 2325, 32767, 2326, 2327, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 2328, 32767, 2329, 32767, - 2330, 32767, 2331, 32767, 2332, 32767, 2333, 32767, - 2334, 32767, 2335, 32767, 2336, 32767, 2337, 32767, - 2338, 32767, 2339, 32767, 32767, 2340, 32767, 2341, - 32767, 2342, 32767, 32767, 32767, 32767, 32767, 32767, - 2343, 2344, 32767, 2345, 2346, 32767, 2347, 2348, - 32767, 2349, 2350, 32767, 2351, 2352, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 2353, 32767, 32767, 2354, - 2355, 2356, 2357, 32767, 32767, 32767, 2358, 2359, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 2360, 2361, 2362, 2363, 2364, 2365, - 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2373, - 2374, -753, 2376, 2377, 2378, 2379, 2380, 2381, - 2382, 2383, 2384, 2385, 2386, 2387, 2388, 2389, - 2390, 2391, 2392, 2393, 2394, 2395, 2396, 2397, - 2398, 2399, 2400, 2401, 2402, 2403, 2404, 2405, - 2406, 2407, 2408, 2409, 2410, 2411, 831, 3086, - 2414, 1631, 2416, 2417, 2418, 2419, 2420, 2421, - 2422, 2423, 2424, 2425, 2426, 2427, 2428, 2429, - 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2437, - -587, 2439, 2440, 2441, 2442, 2443, 2444, 2445, - 2446, 2447, 2448, 9292, 9293, 2451, 2452, 2453, - 32767, 32767, 32767, 2454, 2455, 2456, 2457, 2458, - 2459, 2460, 2461, 2462, 2463, 2464, 2465, 2466, - 2467, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 4599, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 4600, 32767, + 32767, 32767, 2313, 2314, 2315, 2316, 2317, 2318, + 32767, 32767, 32767, 32767, 32767, 32767, 2319, 32767, + 2320, 2321, 2322, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2323, 32767, 2324, 32767, + 2325, 32767, 2326, 32767, 2327, 32767, 2328, 32767, + 2329, 32767, 2330, 32767, 2331, 32767, 2332, 32767, + 2333, 32767, 2334, 32767, 32767, 2335, 32767, 2336, + 32767, 2337, 32767, 32767, 32767, 32767, 32767, 32767, + 2338, 2339, 32767, 2340, 2341, 32767, 2342, 2343, + 32767, 2344, 2345, 32767, 2346, 2347, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2348, 32767, 32767, 32767, + 32767, 2349, 2350, 2351, 2352, 32767, 2353, 2354, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 2468, -1450, 2470, 2471, 2472, 2473, - 2474, 2475, 2476, 2477, 2478, 2479, 2480, 2481, - 2482, 2483, 2484, 2485, 2486, 2487, 2488, 2489, - 2490, 2491, 2492, 2493, 2494, 2495, 2496, 2497, - 2498, 32767, 2499, 2500, 2501, 2502, 2503, 2504, - 2505, 2506, 2507, 2508, 2509, 2510, 2511, 2512, - 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, - 2521, 2522, 2523, 2524, 2525, 2526, 2527, 2528, - 2529, 2530, 2531, 2532, 2533, 2534, 2535, 2536, - 2537, 2538, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 2539, 2540, 5896, 2542, 2543, 2544, - 2545, 548, 2547, 2548, 2549, 2550, 2551, 2552, - 5919, 5920, 5921, 2556, 2557, 5926, 2559, 2560, - 2561, 2562, 2563, 2564, 2565, 2566, 2567, 2568, - 2569, 2570, 2571, 2572, 2573, 2574, 2575, 2576, - 2577, 2578, 2579, 2580, 2581, 2582, 2583, 2584, - 2585, 32767, 2586, 2587, 2588, 2589, 2590, 2591, - 2592, 2593, 2594, 2595, 2596, 2597, 2598, 2599, - 2600, 2601, 2602, 2603, 2604, 2605, 2606, 2607, - 2608, 2609, 2610, 2611, 2612, 2613, 2614, 2615, - 2616, 2617, 2618, 2619, 2620, 2621, 2622, 2623, - 2624, 2625, 2626, 2627, 2628, 2629, 2630, 2631, - 2632, 2633, 2634, 2635, 2636, 2637, 2638, 2639, - 2640, 2641, 2642, 2643, 2644, 2645, 2646, 2647, - 2648, 2649, 2650, 2651, 2652, 2653, 2654, 2655, - 2656, 2657, 2658, 2659, 2660, 2661, -703, 2663, - 2664, 2665, 2666, 2667, 2668, 2669, 2670, 2671, - 2672, 2673, 2674, 1999, 2676, 2677, 2678, 2679, - 2680, 2681, 2682, 2683, 2684, 2685, 2686, 2687, - 2688, 2689, 2690, 2014, 2692, 2693, 2694, 2695, - 2696, 2697, 2698, 2699, 2700, 2701, 2702, 2703, - 2704, 2705, 2706, 2707, 2708, 2709, -11089,2711, - 2712, 2713, 32767, 2714, 2715, 2716, 2717, 2718, - 8050, 2720, 2721, 8053, 8054, 2724, 2725, 2726, - 2727, 2728, 2729, 2730, 2731, 2732, 2733, 2734, - 2735, 2736, 2737, 2738, 2739, 2740, 2741, 2742, - 2743, 2744, 2745, 2746, 2747, 2748, -1377, -1377, - -1377, -1377, 2753, 2754, 2755, 2756, 2757, 2758, - 2759, 2760, 2761, 2762, 2763, 2764, 2765, 2766, - 4025, 2768, 2769, 2770, 2771, 2772, 2773, 2774, - 2775, 2776, 2777, 2778, 2779, 2780, 2781, 4054, - 4055, 2784, 2785, 2786, 2787, 2788, 2789, 2790, - 2791, 2792, 2793, 2794, 2795, 2796, 2797, 2798, - 2799, 2800, 2801, 2802, 2803, 2804, 2805, 2806, - 2807, 2808, 2809, 2810, 2811, 2812, 2813, 2814, - 2815, 2816, 2817, 4669, 4670, 4671, 4672, 4673, - 2823, 2824, 2825, 2826, 2827, 2828, 2829, 2830, - 2831, 2832, 2833, -401, 2835, 9761, 2531, 2532, - 2533, 2534, 2535, 2536, 2537, 2538, 2539, 2540, - 2541, 2542, 4613, 4614, -1621, -1621, 2542, 2543, - 2544, 2545, -7430, -7430, -7430, -7430, -7430, -7430, - -7430, -1621, -7333, 2547, 2548, -593, 2550, 2551, - 2552, 2553, 2554, -1621, -1621, 2555, 2556, 2557, - 1623, 2559, 2560, 1624, 1625, 2884, -592, -7329, - -7329, -1621, -1621, -7329, -7329, -7329, -1621, 2894, - 2895, 2896, 2897, 2898, 2899, 2900, 2901, 2902, - 2903, 2904, 2905, 2906, 2907, 2908, 2909, 2910, - 2911, 2912, 2913, 2914, 2915, 2916, 2917, 2918, - 2919, 2920, 2921, 2922, 2923, 2924, 2925, 2926, - 2927, 2928, 2929, 2930, 2931, 2932, 2933, 2934, - 2935, 2936, 2937, 2938, 2939, 2940, 2941, 2942, - 2943, 2944, 2945, 2946, 2947, 2948, 2949, 2950, - 2951, 2952, 2953, 2954, 2955, 2956, 2957, 2958, - 2959, 2960, 2961, 2962, 2963, 2964, 2965, 2966, - 2967, 2968, 2969, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 4627, 2355, 32767, 2356, 32767, + 2357, 32767, 2358, 32767, 2359, 32767, 2360, 32767, + 2361, 32767, 2362, 32767, 2363, 32767, 2364, 32767, + 2365, 32767, 2366, 32767, 32767, 2367, 4628, 2368, + 32767, 2369, 32767, 32767, 32767, 32767, 32767, 32767, + 2370, 2371, 32767, 2372, 2373, 32767, 2374, 2375, + 32767, 2376, 2377, 32767, 2378, 2379, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2380, 32767, 32767, 2381, + 2382, 2383, 2384, 32767, 32767, 32767, 2385, 2386, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 0, 0, 0, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 2387, 2388, 2389, 2390, 2391, 2392, + 2393, 2394, 2395, 2396, 2397, 2398, 2399, 2400, + 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, + 2409, 2410, 2411, 2412, 2413, 2414, 2415, 2416, + 2417, 2418, 2419, 2420, 2421, 2422, 2423, 2424, + 2425, 2426, 2427, 2428, 2429, 2430, 2431, 2432, + 2433, 2434, 2435, 2436, 2437, 2438, 2439, 2440, + 2441, 2442, 2443, 2444, 2445, 2446, 2447, 2448, + 2449, 2450, 2451, 2452, 2453, 2454, 2455, 2456, + 2457, 2458, 2459, 2460, 2461, 2462, 2463, 2464, + 2465, 2466, 2467, 2468, 2469, 2470, 2471, 2472, + 2473, 2474, 2475, 2476, 2477, 2478, 2479, 2480, + 32767, 32767, 32767, 2481, 2482, 2483, 2484, 2485, + 2486, 2487, 2488, 2489, 2490, 2491, 2492, 2493, + 2494, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -1734,25 +1648,120 @@ Decomp_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 2495, 2496, 2497, 2498, 2499, 2500, + 2501, 2502, 2503, 2504, 2505, 2506, 2507, 2508, + 2509, 2510, 2511, 2512, 2513, 2514, 2515, 2516, + 2517, 2518, 2519, 2520, 2521, 2522, 2523, 2524, + 2525, 32767, 2526, 2527, 2528, 2529, 2530, 2531, + 2532, 2533, 2534, 2535, 2536, 2537, 2538, 2539, + 2540, 2541, 2542, 2543, 2544, 2545, 2546, 2547, + 2548, 2549, 2550, 2551, 2552, 2553, 2554, 2555, + 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, + 2564, 2565, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 2566, 2567, 2568, 2569, 2570, 2571, + 2572, 2573, 2574, 2575, 2576, 2577, 2578, 2579, + 2580, 2581, 2582, 2583, 2584, 4629, 4630, 3460, + 3461, 3462, 2590, 4634, 4635, 4636, 3462, -1012, + -1011, -1010, 2598, 2599, 2600, 2601, 2602, 2603, + 2604, 2605, 2606, 2607, 2608, 2609, 2610, 2611, + 2612, 4656, 2613, 2614, 2615, 2616, 2617, 2618, + 2619, 2620, 2621, 2622, 2623, 2624, 2625, 2626, + 2627, 2628, 2629, 2630, 2631, 2632, 2633, 2634, + 2635, 2636, 2637, 2638, 2639, 2640, 2641, 2642, + 2643, 2644, 2645, 2646, 2647, 2648, 2649, 2650, + 2651, 2652, 2653, 2654, 2655, 2656, 2657, 2658, + 2659, 2660, 2661, 2662, 2663, 2664, 2665, 2666, + 2667, 2668, 2669, 2670, 2671, 2672, 2673, 2674, + 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, + 2683, 2684, 2685, 2686, 2687, 2688, 2689, 2690, + 2691, 2692, 2693, 2694, 2695, 2696, 2697, 2698, + 2699, 2700, 2701, 2702, 2703, 2704, 2705, 2706, + 2707, 2708, 2709, 2710, 2711, 2712, 2713, 2714, + 2715, 2716, 2717, 2718, 2719, 2720, 2721, 2722, + 2723, 2724, 2725, 2726, 2727, 2728, 2729, 2730, + 2731, 2732, 2733, 2734, 2735, 2736, 2737, 2738, + 2739, 2740, 32767, 2741, 2742, 2743, 2744, 2745, + 2746, 2747, 2748, 2749, 2750, 2751, 2752, 2753, + 2754, 2755, 2756, 2757, 2758, 2759, 2760, 2761, + 2762, 2763, 2764, 2765, 2766, 2767, 2768, 2769, + 2770, 2771, 2772, 2773, 2774, 2775, 2776, 2777, + 2778, 2779, 2780, 2781, 2782, 2783, 2784, 2785, + 2786, 2787, 2788, 2789, 2790, 2791, 2792, 2793, + 2794, 2795, 2796, 2797, 2798, 2799, 2800, 2801, + 2802, 2803, 2804, 2805, 2806, 2807, 2808, 2809, + 2810, 2811, 2812, 2813, 2814, 2815, 2816, 2817, + 2818, 2819, 2820, 2821, 2822, 2823, 2824, 2825, + 2826, 2827, 2828, 2829, 2830, 2831, 2832, 2833, + 2834, 2835, 2836, 2837, 2838, 2839, 2840, 2841, + 2842, 2843, 2844, 2845, 2846, 2847, 2848, 2849, + 2850, 2851, 2852, 2853, 2854, 2855, 2856, 2857, + 2858, 2859, 2860, 2861, 2862, 2863, 2864, 2865, + 2866, 2867, 2868, 2869, 2870, 2871, 2872, 2873, + 2874, 2875, 2876, 2877, 2878, 2879, 2880, 2881, + 2882, 2883, 2884, 2885, 2886, 2887, 2888, 2889, + 2890, 2891, 2892, 2893, 2894, 2895, 2896, 2897, + 2898, 2899, 2900, 2901, 2902, 2903, 2904, 2905, + 2906, 2907, 2908, 2909, 2910, 2911, 2912, 2913, + 2914, 2915, 2916, 2917, 2918, 2919, 2920, 2921, + 2922, 2923, 2924, 2925, 2926, 2927, 2928, 2929, + 2930, 2931, 2932, 2933, 2934, 2935, 2936, 2937, + 2938, 2939, 2940, 2941, 2942, 2943, 2944, 2945, + 2946, 2947, 2948, 2949, 2950, 2951, 2952, 2953, + 2954, 2955, 2956, 2957, 2958, 2959, 2960, 2961, + 2962, 2963, 2964, 2965, 2966, 2967, 2968, 2969, + 2970, 2971, 2972, 2973, 2974, 2975, 2976, 2977, + 2978, 2979, 2980, 2981, 2982, 2983, 2984, 2985, + 2986, 2987, 2988, 2989, 2990, 2991, 2992, 2993, + 2994, 2995, 2996, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 0, 0, 0, 0, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 32767, 0, 32767, 0, 32767, 0, - 32767, 0, 32767, 0, 32767, 0, 32767, 0, - 32767, 0, 32767, 0, 32767, 0, 32767, 0, - 32767, 32767, 0, 32767, 0, 32767, 0, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 0, 32767, - 0, 0, 32767, 0, 0, 32767, 0, 0, - 32767, 0, 0, 32767, 32767, 32767, 32767 + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 5911, 5912, 5913, 5914, 5915, 5916, 5917, + 32767, 5918, 5919, 5920, 5921, 5922, 5923, 5924, + 5925, 5926, 5927, 5928, 5929, 5930, 5931, 5932, + 5933, 5934, 32767, 32767, 5935, 5936, 5937, 5938, + 5939, 5940, 5941, 32767, 5942, 5943, 32767, 5944, + 5945, 5946, 5947, 5948, 4685, 32767, 4686, 32767, + 32767, 5949, 5950, 5951, 5952, 5953, 5954, 5955, + 5956, 5957, 5958, 5959, 5960, 5961, 5962, 5963, + 5964, 5965, 5966, 5967, 5968, 5969, 5970, 5971, + 5972, 5973, 5974, 5975, 5976, 5977, 5978, 5979, + 5980, 5981, 5982, 5983, 5984, 5985, 5986, 4816, + 4817, 4818, 5990, 5991, 5992, 5993, 4819, 345, + 346, 347, 3955, 3956, 3957, 3958, 3959, 3960, + 3961, 3962, 3963, 3964, 3965, 3966, 3967, 32767, + 32767, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 4642, 0, 0, 0, 0, 32767, 32767, 0, + 1, 0, 2, 0, 0, 0, 0, 3, + 6011, 0, 4, 5, 6, 7, 0, 32767, + 8, 9, 10, 0, 11, 12, 336, 0, + 0, 0, -305, 339, 340, 341, 0, 342, + 343, 0, 0, 346, 25, 347, 348, 349, + 0, 0, 0, 351, 352, 353, 34, 0, + 0, 35, 36, 37, 38, 39, 0, 0, + 152, -284, 154, 155, 156 }; const unsigned char *k = (const unsigned char *) key; @@ -1765,9 +1774,9 @@ Decomp_hash_func(const void *key) unsigned char c = *k++; a = a * 257 + c; - b = b * 8191 + c; + b = b * 127 + c; } - return h[a % 13687] + h[b % 13687]; + return h[a % 13757] + h[b % 13757]; } /* Hash lookup information for decomposition */ @@ -1775,15 +1784,15 @@ static const pg_unicode_decompinfo UnicodeDecompInfo = { UnicodeDecompMain, Decomp_hash_func, - 6843 + 6878 }; /* Inverse lookup array -- contains indexes into UnicodeDecompMain[] */ static const uint16 RecompInverseLookup[961] = { - /* U+003C+0338 -> U+226E */ 1858, - /* U+003D+0338 -> U+2260 */ 1855, - /* U+003E+0338 -> U+226F */ 1859, + /* U+003C+0338 -> U+226E */ 1885, + /* U+003D+0338 -> U+2260 */ 1882, + /* U+003E+0338 -> U+226F */ 1886, /* U+0041+0300 -> U+00C0 */ 14, /* U+0041+0301 -> U+00C1 */ 15, /* U+0041+0302 -> U+00C2 */ 16, @@ -1792,60 +1801,60 @@ static const uint16 RecompInverseLookup[961] = /* U+0041+0306 -> U+0102 */ 69, /* U+0041+0307 -> U+0226 */ 270, /* U+0041+0308 -> U+00C4 */ 18, - /* U+0041+0309 -> U+1EA2 */ 1313, + /* U+0041+0309 -> U+1EA2 */ 1340, /* U+0041+030A -> U+00C5 */ 19, /* U+0041+030C -> U+01CD */ 194, /* U+0041+030F -> U+0200 */ 240, /* U+0041+0311 -> U+0202 */ 242, - /* U+0041+0323 -> U+1EA0 */ 1311, - /* U+0041+0325 -> U+1E00 */ 1155, + /* U+0041+0323 -> U+1EA0 */ 1338, + /* U+0041+0325 -> U+1E00 */ 1182, /* U+0041+0328 -> U+0104 */ 71, - /* U+0042+0307 -> U+1E02 */ 1157, - /* U+0042+0323 -> U+1E04 */ 1159, - /* U+0042+0331 -> U+1E06 */ 1161, + /* U+0042+0307 -> U+1E02 */ 1184, + /* U+0042+0323 -> U+1E04 */ 1186, + /* U+0042+0331 -> U+1E06 */ 1188, /* U+0043+0301 -> U+0106 */ 73, /* U+0043+0302 -> U+0108 */ 75, /* U+0043+0307 -> U+010A */ 77, /* U+0043+030C -> U+010C */ 79, /* U+0043+0327 -> U+00C7 */ 20, - /* U+0044+0307 -> U+1E0A */ 1165, + /* U+0044+0307 -> U+1E0A */ 1192, /* U+0044+030C -> U+010E */ 81, - /* U+0044+0323 -> U+1E0C */ 1167, - /* U+0044+0327 -> U+1E10 */ 1171, - /* U+0044+032D -> U+1E12 */ 1173, - /* U+0044+0331 -> U+1E0E */ 1169, + /* U+0044+0323 -> U+1E0C */ 1194, + /* U+0044+0327 -> U+1E10 */ 1198, + /* U+0044+032D -> U+1E12 */ 1200, + /* U+0044+0331 -> U+1E0E */ 1196, /* U+0045+0300 -> U+00C8 */ 21, /* U+0045+0301 -> U+00C9 */ 22, /* U+0045+0302 -> U+00CA */ 23, - /* U+0045+0303 -> U+1EBC */ 1339, + /* U+0045+0303 -> U+1EBC */ 1366, /* U+0045+0304 -> U+0112 */ 83, /* U+0045+0306 -> U+0114 */ 85, /* U+0045+0307 -> U+0116 */ 87, /* U+0045+0308 -> U+00CB */ 24, - /* U+0045+0309 -> U+1EBA */ 1337, + /* U+0045+0309 -> U+1EBA */ 1364, /* U+0045+030C -> U+011A */ 91, /* U+0045+030F -> U+0204 */ 244, /* U+0045+0311 -> U+0206 */ 246, - /* U+0045+0323 -> U+1EB8 */ 1335, + /* U+0045+0323 -> U+1EB8 */ 1362, /* U+0045+0327 -> U+0228 */ 272, /* U+0045+0328 -> U+0118 */ 89, - /* U+0045+032D -> U+1E18 */ 1179, - /* U+0045+0330 -> U+1E1A */ 1181, - /* U+0046+0307 -> U+1E1E */ 1185, + /* U+0045+032D -> U+1E18 */ 1206, + /* U+0045+0330 -> U+1E1A */ 1208, + /* U+0046+0307 -> U+1E1E */ 1212, /* U+0047+0301 -> U+01F4 */ 230, /* U+0047+0302 -> U+011C */ 93, - /* U+0047+0304 -> U+1E20 */ 1187, + /* U+0047+0304 -> U+1E20 */ 1214, /* U+0047+0306 -> U+011E */ 95, /* U+0047+0307 -> U+0120 */ 97, /* U+0047+030C -> U+01E6 */ 216, /* U+0047+0327 -> U+0122 */ 99, /* U+0048+0302 -> U+0124 */ 101, - /* U+0048+0307 -> U+1E22 */ 1189, - /* U+0048+0308 -> U+1E26 */ 1193, + /* U+0048+0307 -> U+1E22 */ 1216, + /* U+0048+0308 -> U+1E26 */ 1220, /* U+0048+030C -> U+021E */ 268, - /* U+0048+0323 -> U+1E24 */ 1191, - /* U+0048+0327 -> U+1E28 */ 1195, - /* U+0048+032E -> U+1E2A */ 1197, + /* U+0048+0323 -> U+1E24 */ 1218, + /* U+0048+0327 -> U+1E28 */ 1222, + /* U+0048+032E -> U+1E2A */ 1224, /* U+0049+0300 -> U+00CC */ 25, /* U+0049+0301 -> U+00CD */ 26, /* U+0049+0302 -> U+00CE */ 27, @@ -1854,37 +1863,37 @@ static const uint16 RecompInverseLookup[961] = /* U+0049+0306 -> U+012C */ 107, /* U+0049+0307 -> U+0130 */ 111, /* U+0049+0308 -> U+00CF */ 28, - /* U+0049+0309 -> U+1EC8 */ 1351, + /* U+0049+0309 -> U+1EC8 */ 1378, /* U+0049+030C -> U+01CF */ 196, /* U+0049+030F -> U+0208 */ 248, /* U+0049+0311 -> U+020A */ 250, - /* U+0049+0323 -> U+1ECA */ 1353, + /* U+0049+0323 -> U+1ECA */ 1380, /* U+0049+0328 -> U+012E */ 109, - /* U+0049+0330 -> U+1E2C */ 1199, + /* U+0049+0330 -> U+1E2C */ 1226, /* U+004A+0302 -> U+0134 */ 114, - /* U+004B+0301 -> U+1E30 */ 1203, + /* U+004B+0301 -> U+1E30 */ 1230, /* U+004B+030C -> U+01E8 */ 218, - /* U+004B+0323 -> U+1E32 */ 1205, + /* U+004B+0323 -> U+1E32 */ 1232, /* U+004B+0327 -> U+0136 */ 116, - /* U+004B+0331 -> U+1E34 */ 1207, + /* U+004B+0331 -> U+1E34 */ 1234, /* U+004C+0301 -> U+0139 */ 118, /* U+004C+030C -> U+013D */ 122, - /* U+004C+0323 -> U+1E36 */ 1209, + /* U+004C+0323 -> U+1E36 */ 1236, /* U+004C+0327 -> U+013B */ 120, - /* U+004C+032D -> U+1E3C */ 1215, - /* U+004C+0331 -> U+1E3A */ 1213, - /* U+004D+0301 -> U+1E3E */ 1217, - /* U+004D+0307 -> U+1E40 */ 1219, - /* U+004D+0323 -> U+1E42 */ 1221, + /* U+004C+032D -> U+1E3C */ 1242, + /* U+004C+0331 -> U+1E3A */ 1240, + /* U+004D+0301 -> U+1E3E */ 1244, + /* U+004D+0307 -> U+1E40 */ 1246, + /* U+004D+0323 -> U+1E42 */ 1248, /* U+004E+0300 -> U+01F8 */ 232, /* U+004E+0301 -> U+0143 */ 126, /* U+004E+0303 -> U+00D1 */ 29, - /* U+004E+0307 -> U+1E44 */ 1223, + /* U+004E+0307 -> U+1E44 */ 1250, /* U+004E+030C -> U+0147 */ 130, - /* U+004E+0323 -> U+1E46 */ 1225, + /* U+004E+0323 -> U+1E46 */ 1252, /* U+004E+0327 -> U+0145 */ 128, - /* U+004E+032D -> U+1E4A */ 1229, - /* U+004E+0331 -> U+1E48 */ 1227, + /* U+004E+032D -> U+1E4A */ 1256, + /* U+004E+0331 -> U+1E48 */ 1254, /* U+004F+0300 -> U+00D2 */ 30, /* U+004F+0301 -> U+00D3 */ 31, /* U+004F+0302 -> U+00D4 */ 32, @@ -1893,38 +1902,38 @@ static const uint16 RecompInverseLookup[961] = /* U+004F+0306 -> U+014E */ 135, /* U+004F+0307 -> U+022E */ 278, /* U+004F+0308 -> U+00D6 */ 34, - /* U+004F+0309 -> U+1ECE */ 1357, + /* U+004F+0309 -> U+1ECE */ 1384, /* U+004F+030B -> U+0150 */ 137, /* U+004F+030C -> U+01D1 */ 198, /* U+004F+030F -> U+020C */ 252, /* U+004F+0311 -> U+020E */ 254, /* U+004F+031B -> U+01A0 */ 181, - /* U+004F+0323 -> U+1ECC */ 1355, + /* U+004F+0323 -> U+1ECC */ 1382, /* U+004F+0328 -> U+01EA */ 220, - /* U+0050+0301 -> U+1E54 */ 1239, - /* U+0050+0307 -> U+1E56 */ 1241, + /* U+0050+0301 -> U+1E54 */ 1266, + /* U+0050+0307 -> U+1E56 */ 1268, /* U+0052+0301 -> U+0154 */ 139, - /* U+0052+0307 -> U+1E58 */ 1243, + /* U+0052+0307 -> U+1E58 */ 1270, /* U+0052+030C -> U+0158 */ 143, /* U+0052+030F -> U+0210 */ 256, /* U+0052+0311 -> U+0212 */ 258, - /* U+0052+0323 -> U+1E5A */ 1245, + /* U+0052+0323 -> U+1E5A */ 1272, /* U+0052+0327 -> U+0156 */ 141, - /* U+0052+0331 -> U+1E5E */ 1249, + /* U+0052+0331 -> U+1E5E */ 1276, /* U+0053+0301 -> U+015A */ 145, /* U+0053+0302 -> U+015C */ 147, - /* U+0053+0307 -> U+1E60 */ 1251, + /* U+0053+0307 -> U+1E60 */ 1278, /* U+0053+030C -> U+0160 */ 151, - /* U+0053+0323 -> U+1E62 */ 1253, + /* U+0053+0323 -> U+1E62 */ 1280, /* U+0053+0326 -> U+0218 */ 264, /* U+0053+0327 -> U+015E */ 149, - /* U+0054+0307 -> U+1E6A */ 1261, + /* U+0054+0307 -> U+1E6A */ 1288, /* U+0054+030C -> U+0164 */ 155, - /* U+0054+0323 -> U+1E6C */ 1263, + /* U+0054+0323 -> U+1E6C */ 1290, /* U+0054+0326 -> U+021A */ 266, /* U+0054+0327 -> U+0162 */ 153, - /* U+0054+032D -> U+1E70 */ 1267, - /* U+0054+0331 -> U+1E6E */ 1265, + /* U+0054+032D -> U+1E70 */ 1294, + /* U+0054+0331 -> U+1E6E */ 1292, /* U+0055+0300 -> U+00D9 */ 35, /* U+0055+0301 -> U+00DA */ 36, /* U+0055+0302 -> U+00DB */ 37, @@ -1932,43 +1941,43 @@ static const uint16 RecompInverseLookup[961] = /* U+0055+0304 -> U+016A */ 159, /* U+0055+0306 -> U+016C */ 161, /* U+0055+0308 -> U+00DC */ 38, - /* U+0055+0309 -> U+1EE6 */ 1381, + /* U+0055+0309 -> U+1EE6 */ 1408, /* U+0055+030A -> U+016E */ 163, /* U+0055+030B -> U+0170 */ 165, /* U+0055+030C -> U+01D3 */ 200, /* U+0055+030F -> U+0214 */ 260, /* U+0055+0311 -> U+0216 */ 262, /* U+0055+031B -> U+01AF */ 183, - /* U+0055+0323 -> U+1EE4 */ 1379, - /* U+0055+0324 -> U+1E72 */ 1269, + /* U+0055+0323 -> U+1EE4 */ 1406, + /* U+0055+0324 -> U+1E72 */ 1296, /* U+0055+0328 -> U+0172 */ 167, - /* U+0055+032D -> U+1E76 */ 1273, - /* U+0055+0330 -> U+1E74 */ 1271, - /* U+0056+0303 -> U+1E7C */ 1279, - /* U+0056+0323 -> U+1E7E */ 1281, - /* U+0057+0300 -> U+1E80 */ 1283, - /* U+0057+0301 -> U+1E82 */ 1285, + /* U+0055+032D -> U+1E76 */ 1300, + /* U+0055+0330 -> U+1E74 */ 1298, + /* U+0056+0303 -> U+1E7C */ 1306, + /* U+0056+0323 -> U+1E7E */ 1308, + /* U+0057+0300 -> U+1E80 */ 1310, + /* U+0057+0301 -> U+1E82 */ 1312, /* U+0057+0302 -> U+0174 */ 169, - /* U+0057+0307 -> U+1E86 */ 1289, - /* U+0057+0308 -> U+1E84 */ 1287, - /* U+0057+0323 -> U+1E88 */ 1291, - /* U+0058+0307 -> U+1E8A */ 1293, - /* U+0058+0308 -> U+1E8C */ 1295, - /* U+0059+0300 -> U+1EF2 */ 1393, + /* U+0057+0307 -> U+1E86 */ 1316, + /* U+0057+0308 -> U+1E84 */ 1314, + /* U+0057+0323 -> U+1E88 */ 1318, + /* U+0058+0307 -> U+1E8A */ 1320, + /* U+0058+0308 -> U+1E8C */ 1322, + /* U+0059+0300 -> U+1EF2 */ 1420, /* U+0059+0301 -> U+00DD */ 39, /* U+0059+0302 -> U+0176 */ 171, - /* U+0059+0303 -> U+1EF8 */ 1399, + /* U+0059+0303 -> U+1EF8 */ 1426, /* U+0059+0304 -> U+0232 */ 282, - /* U+0059+0307 -> U+1E8E */ 1297, + /* U+0059+0307 -> U+1E8E */ 1324, /* U+0059+0308 -> U+0178 */ 173, - /* U+0059+0309 -> U+1EF6 */ 1397, - /* U+0059+0323 -> U+1EF4 */ 1395, + /* U+0059+0309 -> U+1EF6 */ 1424, + /* U+0059+0323 -> U+1EF4 */ 1422, /* U+005A+0301 -> U+0179 */ 174, - /* U+005A+0302 -> U+1E90 */ 1299, + /* U+005A+0302 -> U+1E90 */ 1326, /* U+005A+0307 -> U+017B */ 176, /* U+005A+030C -> U+017D */ 178, - /* U+005A+0323 -> U+1E92 */ 1301, - /* U+005A+0331 -> U+1E94 */ 1303, + /* U+005A+0323 -> U+1E92 */ 1328, + /* U+005A+0331 -> U+1E94 */ 1330, /* U+0061+0300 -> U+00E0 */ 40, /* U+0061+0301 -> U+00E1 */ 41, /* U+0061+0302 -> U+00E2 */ 42, @@ -1977,61 +1986,61 @@ static const uint16 RecompInverseLookup[961] = /* U+0061+0306 -> U+0103 */ 70, /* U+0061+0307 -> U+0227 */ 271, /* U+0061+0308 -> U+00E4 */ 44, - /* U+0061+0309 -> U+1EA3 */ 1314, + /* U+0061+0309 -> U+1EA3 */ 1341, /* U+0061+030A -> U+00E5 */ 45, /* U+0061+030C -> U+01CE */ 195, /* U+0061+030F -> U+0201 */ 241, /* U+0061+0311 -> U+0203 */ 243, - /* U+0061+0323 -> U+1EA1 */ 1312, - /* U+0061+0325 -> U+1E01 */ 1156, + /* U+0061+0323 -> U+1EA1 */ 1339, + /* U+0061+0325 -> U+1E01 */ 1183, /* U+0061+0328 -> U+0105 */ 72, - /* U+0062+0307 -> U+1E03 */ 1158, - /* U+0062+0323 -> U+1E05 */ 1160, - /* U+0062+0331 -> U+1E07 */ 1162, + /* U+0062+0307 -> U+1E03 */ 1185, + /* U+0062+0323 -> U+1E05 */ 1187, + /* U+0062+0331 -> U+1E07 */ 1189, /* U+0063+0301 -> U+0107 */ 74, /* U+0063+0302 -> U+0109 */ 76, /* U+0063+0307 -> U+010B */ 78, /* U+0063+030C -> U+010D */ 80, /* U+0063+0327 -> U+00E7 */ 46, - /* U+0064+0307 -> U+1E0B */ 1166, + /* U+0064+0307 -> U+1E0B */ 1193, /* U+0064+030C -> U+010F */ 82, - /* U+0064+0323 -> U+1E0D */ 1168, - /* U+0064+0327 -> U+1E11 */ 1172, - /* U+0064+032D -> U+1E13 */ 1174, - /* U+0064+0331 -> U+1E0F */ 1170, + /* U+0064+0323 -> U+1E0D */ 1195, + /* U+0064+0327 -> U+1E11 */ 1199, + /* U+0064+032D -> U+1E13 */ 1201, + /* U+0064+0331 -> U+1E0F */ 1197, /* U+0065+0300 -> U+00E8 */ 47, /* U+0065+0301 -> U+00E9 */ 48, /* U+0065+0302 -> U+00EA */ 49, - /* U+0065+0303 -> U+1EBD */ 1340, + /* U+0065+0303 -> U+1EBD */ 1367, /* U+0065+0304 -> U+0113 */ 84, /* U+0065+0306 -> U+0115 */ 86, /* U+0065+0307 -> U+0117 */ 88, /* U+0065+0308 -> U+00EB */ 50, - /* U+0065+0309 -> U+1EBB */ 1338, + /* U+0065+0309 -> U+1EBB */ 1365, /* U+0065+030C -> U+011B */ 92, /* U+0065+030F -> U+0205 */ 245, /* U+0065+0311 -> U+0207 */ 247, - /* U+0065+0323 -> U+1EB9 */ 1336, + /* U+0065+0323 -> U+1EB9 */ 1363, /* U+0065+0327 -> U+0229 */ 273, /* U+0065+0328 -> U+0119 */ 90, - /* U+0065+032D -> U+1E19 */ 1180, - /* U+0065+0330 -> U+1E1B */ 1182, - /* U+0066+0307 -> U+1E1F */ 1186, + /* U+0065+032D -> U+1E19 */ 1207, + /* U+0065+0330 -> U+1E1B */ 1209, + /* U+0066+0307 -> U+1E1F */ 1213, /* U+0067+0301 -> U+01F5 */ 231, /* U+0067+0302 -> U+011D */ 94, - /* U+0067+0304 -> U+1E21 */ 1188, + /* U+0067+0304 -> U+1E21 */ 1215, /* U+0067+0306 -> U+011F */ 96, /* U+0067+0307 -> U+0121 */ 98, /* U+0067+030C -> U+01E7 */ 217, /* U+0067+0327 -> U+0123 */ 100, /* U+0068+0302 -> U+0125 */ 102, - /* U+0068+0307 -> U+1E23 */ 1190, - /* U+0068+0308 -> U+1E27 */ 1194, + /* U+0068+0307 -> U+1E23 */ 1217, + /* U+0068+0308 -> U+1E27 */ 1221, /* U+0068+030C -> U+021F */ 269, - /* U+0068+0323 -> U+1E25 */ 1192, - /* U+0068+0327 -> U+1E29 */ 1196, - /* U+0068+032E -> U+1E2B */ 1198, - /* U+0068+0331 -> U+1E96 */ 1305, + /* U+0068+0323 -> U+1E25 */ 1219, + /* U+0068+0327 -> U+1E29 */ 1223, + /* U+0068+032E -> U+1E2B */ 1225, + /* U+0068+0331 -> U+1E96 */ 1332, /* U+0069+0300 -> U+00EC */ 51, /* U+0069+0301 -> U+00ED */ 52, /* U+0069+0302 -> U+00EE */ 53, @@ -2039,38 +2048,38 @@ static const uint16 RecompInverseLookup[961] = /* U+0069+0304 -> U+012B */ 106, /* U+0069+0306 -> U+012D */ 108, /* U+0069+0308 -> U+00EF */ 54, - /* U+0069+0309 -> U+1EC9 */ 1352, + /* U+0069+0309 -> U+1EC9 */ 1379, /* U+0069+030C -> U+01D0 */ 197, /* U+0069+030F -> U+0209 */ 249, /* U+0069+0311 -> U+020B */ 251, - /* U+0069+0323 -> U+1ECB */ 1354, + /* U+0069+0323 -> U+1ECB */ 1381, /* U+0069+0328 -> U+012F */ 110, - /* U+0069+0330 -> U+1E2D */ 1200, + /* U+0069+0330 -> U+1E2D */ 1227, /* U+006A+0302 -> U+0135 */ 115, /* U+006A+030C -> U+01F0 */ 226, - /* U+006B+0301 -> U+1E31 */ 1204, + /* U+006B+0301 -> U+1E31 */ 1231, /* U+006B+030C -> U+01E9 */ 219, - /* U+006B+0323 -> U+1E33 */ 1206, + /* U+006B+0323 -> U+1E33 */ 1233, /* U+006B+0327 -> U+0137 */ 117, - /* U+006B+0331 -> U+1E35 */ 1208, + /* U+006B+0331 -> U+1E35 */ 1235, /* U+006C+0301 -> U+013A */ 119, /* U+006C+030C -> U+013E */ 123, - /* U+006C+0323 -> U+1E37 */ 1210, + /* U+006C+0323 -> U+1E37 */ 1237, /* U+006C+0327 -> U+013C */ 121, - /* U+006C+032D -> U+1E3D */ 1216, - /* U+006C+0331 -> U+1E3B */ 1214, - /* U+006D+0301 -> U+1E3F */ 1218, - /* U+006D+0307 -> U+1E41 */ 1220, - /* U+006D+0323 -> U+1E43 */ 1222, + /* U+006C+032D -> U+1E3D */ 1243, + /* U+006C+0331 -> U+1E3B */ 1241, + /* U+006D+0301 -> U+1E3F */ 1245, + /* U+006D+0307 -> U+1E41 */ 1247, + /* U+006D+0323 -> U+1E43 */ 1249, /* U+006E+0300 -> U+01F9 */ 233, /* U+006E+0301 -> U+0144 */ 127, /* U+006E+0303 -> U+00F1 */ 55, - /* U+006E+0307 -> U+1E45 */ 1224, + /* U+006E+0307 -> U+1E45 */ 1251, /* U+006E+030C -> U+0148 */ 131, - /* U+006E+0323 -> U+1E47 */ 1226, + /* U+006E+0323 -> U+1E47 */ 1253, /* U+006E+0327 -> U+0146 */ 129, - /* U+006E+032D -> U+1E4B */ 1230, - /* U+006E+0331 -> U+1E49 */ 1228, + /* U+006E+032D -> U+1E4B */ 1257, + /* U+006E+0331 -> U+1E49 */ 1255, /* U+006F+0300 -> U+00F2 */ 56, /* U+006F+0301 -> U+00F3 */ 57, /* U+006F+0302 -> U+00F4 */ 58, @@ -2079,39 +2088,39 @@ static const uint16 RecompInverseLookup[961] = /* U+006F+0306 -> U+014F */ 136, /* U+006F+0307 -> U+022F */ 279, /* U+006F+0308 -> U+00F6 */ 60, - /* U+006F+0309 -> U+1ECF */ 1358, + /* U+006F+0309 -> U+1ECF */ 1385, /* U+006F+030B -> U+0151 */ 138, /* U+006F+030C -> U+01D2 */ 199, /* U+006F+030F -> U+020D */ 253, /* U+006F+0311 -> U+020F */ 255, /* U+006F+031B -> U+01A1 */ 182, - /* U+006F+0323 -> U+1ECD */ 1356, + /* U+006F+0323 -> U+1ECD */ 1383, /* U+006F+0328 -> U+01EB */ 221, - /* U+0070+0301 -> U+1E55 */ 1240, - /* U+0070+0307 -> U+1E57 */ 1242, + /* U+0070+0301 -> U+1E55 */ 1267, + /* U+0070+0307 -> U+1E57 */ 1269, /* U+0072+0301 -> U+0155 */ 140, - /* U+0072+0307 -> U+1E59 */ 1244, + /* U+0072+0307 -> U+1E59 */ 1271, /* U+0072+030C -> U+0159 */ 144, /* U+0072+030F -> U+0211 */ 257, /* U+0072+0311 -> U+0213 */ 259, - /* U+0072+0323 -> U+1E5B */ 1246, + /* U+0072+0323 -> U+1E5B */ 1273, /* U+0072+0327 -> U+0157 */ 142, - /* U+0072+0331 -> U+1E5F */ 1250, + /* U+0072+0331 -> U+1E5F */ 1277, /* U+0073+0301 -> U+015B */ 146, /* U+0073+0302 -> U+015D */ 148, - /* U+0073+0307 -> U+1E61 */ 1252, + /* U+0073+0307 -> U+1E61 */ 1279, /* U+0073+030C -> U+0161 */ 152, - /* U+0073+0323 -> U+1E63 */ 1254, + /* U+0073+0323 -> U+1E63 */ 1281, /* U+0073+0326 -> U+0219 */ 265, /* U+0073+0327 -> U+015F */ 150, - /* U+0074+0307 -> U+1E6B */ 1262, - /* U+0074+0308 -> U+1E97 */ 1306, + /* U+0074+0307 -> U+1E6B */ 1289, + /* U+0074+0308 -> U+1E97 */ 1333, /* U+0074+030C -> U+0165 */ 156, - /* U+0074+0323 -> U+1E6D */ 1264, + /* U+0074+0323 -> U+1E6D */ 1291, /* U+0074+0326 -> U+021B */ 267, /* U+0074+0327 -> U+0163 */ 154, - /* U+0074+032D -> U+1E71 */ 1268, - /* U+0074+0331 -> U+1E6F */ 1266, + /* U+0074+032D -> U+1E71 */ 1295, + /* U+0074+0331 -> U+1E6F */ 1293, /* U+0075+0300 -> U+00F9 */ 61, /* U+0075+0301 -> U+00FA */ 62, /* U+0075+0302 -> U+00FB */ 63, @@ -2119,251 +2128,251 @@ static const uint16 RecompInverseLookup[961] = /* U+0075+0304 -> U+016B */ 160, /* U+0075+0306 -> U+016D */ 162, /* U+0075+0308 -> U+00FC */ 64, - /* U+0075+0309 -> U+1EE7 */ 1382, + /* U+0075+0309 -> U+1EE7 */ 1409, /* U+0075+030A -> U+016F */ 164, /* U+0075+030B -> U+0171 */ 166, /* U+0075+030C -> U+01D4 */ 201, /* U+0075+030F -> U+0215 */ 261, /* U+0075+0311 -> U+0217 */ 263, /* U+0075+031B -> U+01B0 */ 184, - /* U+0075+0323 -> U+1EE5 */ 1380, - /* U+0075+0324 -> U+1E73 */ 1270, + /* U+0075+0323 -> U+1EE5 */ 1407, + /* U+0075+0324 -> U+1E73 */ 1297, /* U+0075+0328 -> U+0173 */ 168, - /* U+0075+032D -> U+1E77 */ 1274, - /* U+0075+0330 -> U+1E75 */ 1272, - /* U+0076+0303 -> U+1E7D */ 1280, - /* U+0076+0323 -> U+1E7F */ 1282, - /* U+0077+0300 -> U+1E81 */ 1284, - /* U+0077+0301 -> U+1E83 */ 1286, + /* U+0075+032D -> U+1E77 */ 1301, + /* U+0075+0330 -> U+1E75 */ 1299, + /* U+0076+0303 -> U+1E7D */ 1307, + /* U+0076+0323 -> U+1E7F */ 1309, + /* U+0077+0300 -> U+1E81 */ 1311, + /* U+0077+0301 -> U+1E83 */ 1313, /* U+0077+0302 -> U+0175 */ 170, - /* U+0077+0307 -> U+1E87 */ 1290, - /* U+0077+0308 -> U+1E85 */ 1288, - /* U+0077+030A -> U+1E98 */ 1307, - /* U+0077+0323 -> U+1E89 */ 1292, - /* U+0078+0307 -> U+1E8B */ 1294, - /* U+0078+0308 -> U+1E8D */ 1296, - /* U+0079+0300 -> U+1EF3 */ 1394, + /* U+0077+0307 -> U+1E87 */ 1317, + /* U+0077+0308 -> U+1E85 */ 1315, + /* U+0077+030A -> U+1E98 */ 1334, + /* U+0077+0323 -> U+1E89 */ 1319, + /* U+0078+0307 -> U+1E8B */ 1321, + /* U+0078+0308 -> U+1E8D */ 1323, + /* U+0079+0300 -> U+1EF3 */ 1421, /* U+0079+0301 -> U+00FD */ 65, /* U+0079+0302 -> U+0177 */ 172, - /* U+0079+0303 -> U+1EF9 */ 1400, + /* U+0079+0303 -> U+1EF9 */ 1427, /* U+0079+0304 -> U+0233 */ 283, - /* U+0079+0307 -> U+1E8F */ 1298, + /* U+0079+0307 -> U+1E8F */ 1325, /* U+0079+0308 -> U+00FF */ 66, - /* U+0079+0309 -> U+1EF7 */ 1398, - /* U+0079+030A -> U+1E99 */ 1308, - /* U+0079+0323 -> U+1EF5 */ 1396, + /* U+0079+0309 -> U+1EF7 */ 1425, + /* U+0079+030A -> U+1E99 */ 1335, + /* U+0079+0323 -> U+1EF5 */ 1423, /* U+007A+0301 -> U+017A */ 175, - /* U+007A+0302 -> U+1E91 */ 1300, + /* U+007A+0302 -> U+1E91 */ 1327, /* U+007A+0307 -> U+017C */ 177, /* U+007A+030C -> U+017E */ 179, - /* U+007A+0323 -> U+1E93 */ 1302, - /* U+007A+0331 -> U+1E95 */ 1304, - /* U+00A8+0300 -> U+1FED */ 1619, + /* U+007A+0323 -> U+1E93 */ 1329, + /* U+007A+0331 -> U+1E95 */ 1331, + /* U+00A8+0300 -> U+1FED */ 1646, /* U+00A8+0301 -> U+0385 */ 419, - /* U+00A8+0342 -> U+1FC1 */ 1579, - /* U+00C2+0300 -> U+1EA6 */ 1317, - /* U+00C2+0301 -> U+1EA4 */ 1315, - /* U+00C2+0303 -> U+1EAA */ 1321, - /* U+00C2+0309 -> U+1EA8 */ 1319, + /* U+00A8+0342 -> U+1FC1 */ 1606, + /* U+00C2+0300 -> U+1EA6 */ 1344, + /* U+00C2+0301 -> U+1EA4 */ 1342, + /* U+00C2+0303 -> U+1EAA */ 1348, + /* U+00C2+0309 -> U+1EA8 */ 1346, /* U+00C4+0304 -> U+01DE */ 210, /* U+00C5+0301 -> U+01FA */ 234, /* U+00C6+0301 -> U+01FC */ 236, /* U+00C6+0304 -> U+01E2 */ 214, - /* U+00C7+0301 -> U+1E08 */ 1163, - /* U+00CA+0300 -> U+1EC0 */ 1343, - /* U+00CA+0301 -> U+1EBE */ 1341, - /* U+00CA+0303 -> U+1EC4 */ 1347, - /* U+00CA+0309 -> U+1EC2 */ 1345, - /* U+00CF+0301 -> U+1E2E */ 1201, - /* U+00D4+0300 -> U+1ED2 */ 1361, - /* U+00D4+0301 -> U+1ED0 */ 1359, - /* U+00D4+0303 -> U+1ED6 */ 1365, - /* U+00D4+0309 -> U+1ED4 */ 1363, - /* U+00D5+0301 -> U+1E4C */ 1231, + /* U+00C7+0301 -> U+1E08 */ 1190, + /* U+00CA+0300 -> U+1EC0 */ 1370, + /* U+00CA+0301 -> U+1EBE */ 1368, + /* U+00CA+0303 -> U+1EC4 */ 1374, + /* U+00CA+0309 -> U+1EC2 */ 1372, + /* U+00CF+0301 -> U+1E2E */ 1228, + /* U+00D4+0300 -> U+1ED2 */ 1388, + /* U+00D4+0301 -> U+1ED0 */ 1386, + /* U+00D4+0303 -> U+1ED6 */ 1392, + /* U+00D4+0309 -> U+1ED4 */ 1390, + /* U+00D5+0301 -> U+1E4C */ 1258, /* U+00D5+0304 -> U+022C */ 276, - /* U+00D5+0308 -> U+1E4E */ 1233, + /* U+00D5+0308 -> U+1E4E */ 1260, /* U+00D6+0304 -> U+022A */ 274, /* U+00D8+0301 -> U+01FE */ 238, /* U+00DC+0300 -> U+01DB */ 208, /* U+00DC+0301 -> U+01D7 */ 204, /* U+00DC+0304 -> U+01D5 */ 202, /* U+00DC+030C -> U+01D9 */ 206, - /* U+00E2+0300 -> U+1EA7 */ 1318, - /* U+00E2+0301 -> U+1EA5 */ 1316, - /* U+00E2+0303 -> U+1EAB */ 1322, - /* U+00E2+0309 -> U+1EA9 */ 1320, + /* U+00E2+0300 -> U+1EA7 */ 1345, + /* U+00E2+0301 -> U+1EA5 */ 1343, + /* U+00E2+0303 -> U+1EAB */ 1349, + /* U+00E2+0309 -> U+1EA9 */ 1347, /* U+00E4+0304 -> U+01DF */ 211, /* U+00E5+0301 -> U+01FB */ 235, /* U+00E6+0301 -> U+01FD */ 237, /* U+00E6+0304 -> U+01E3 */ 215, - /* U+00E7+0301 -> U+1E09 */ 1164, - /* U+00EA+0300 -> U+1EC1 */ 1344, - /* U+00EA+0301 -> U+1EBF */ 1342, - /* U+00EA+0303 -> U+1EC5 */ 1348, - /* U+00EA+0309 -> U+1EC3 */ 1346, - /* U+00EF+0301 -> U+1E2F */ 1202, - /* U+00F4+0300 -> U+1ED3 */ 1362, - /* U+00F4+0301 -> U+1ED1 */ 1360, - /* U+00F4+0303 -> U+1ED7 */ 1366, - /* U+00F4+0309 -> U+1ED5 */ 1364, - /* U+00F5+0301 -> U+1E4D */ 1232, + /* U+00E7+0301 -> U+1E09 */ 1191, + /* U+00EA+0300 -> U+1EC1 */ 1371, + /* U+00EA+0301 -> U+1EBF */ 1369, + /* U+00EA+0303 -> U+1EC5 */ 1375, + /* U+00EA+0309 -> U+1EC3 */ 1373, + /* U+00EF+0301 -> U+1E2F */ 1229, + /* U+00F4+0300 -> U+1ED3 */ 1389, + /* U+00F4+0301 -> U+1ED1 */ 1387, + /* U+00F4+0303 -> U+1ED7 */ 1393, + /* U+00F4+0309 -> U+1ED5 */ 1391, + /* U+00F5+0301 -> U+1E4D */ 1259, /* U+00F5+0304 -> U+022D */ 277, - /* U+00F5+0308 -> U+1E4F */ 1234, + /* U+00F5+0308 -> U+1E4F */ 1261, /* U+00F6+0304 -> U+022B */ 275, /* U+00F8+0301 -> U+01FF */ 239, /* U+00FC+0300 -> U+01DC */ 209, /* U+00FC+0301 -> U+01D8 */ 205, /* U+00FC+0304 -> U+01D6 */ 203, /* U+00FC+030C -> U+01DA */ 207, - /* U+0102+0300 -> U+1EB0 */ 1327, - /* U+0102+0301 -> U+1EAE */ 1325, - /* U+0102+0303 -> U+1EB4 */ 1331, - /* U+0102+0309 -> U+1EB2 */ 1329, - /* U+0103+0300 -> U+1EB1 */ 1328, - /* U+0103+0301 -> U+1EAF */ 1326, - /* U+0103+0303 -> U+1EB5 */ 1332, - /* U+0103+0309 -> U+1EB3 */ 1330, - /* U+0112+0300 -> U+1E14 */ 1175, - /* U+0112+0301 -> U+1E16 */ 1177, - /* U+0113+0300 -> U+1E15 */ 1176, - /* U+0113+0301 -> U+1E17 */ 1178, - /* U+014C+0300 -> U+1E50 */ 1235, - /* U+014C+0301 -> U+1E52 */ 1237, - /* U+014D+0300 -> U+1E51 */ 1236, - /* U+014D+0301 -> U+1E53 */ 1238, - /* U+015A+0307 -> U+1E64 */ 1255, - /* U+015B+0307 -> U+1E65 */ 1256, - /* U+0160+0307 -> U+1E66 */ 1257, - /* U+0161+0307 -> U+1E67 */ 1258, - /* U+0168+0301 -> U+1E78 */ 1275, - /* U+0169+0301 -> U+1E79 */ 1276, - /* U+016A+0308 -> U+1E7A */ 1277, - /* U+016B+0308 -> U+1E7B */ 1278, - /* U+017F+0307 -> U+1E9B */ 1310, - /* U+01A0+0300 -> U+1EDC */ 1371, - /* U+01A0+0301 -> U+1EDA */ 1369, - /* U+01A0+0303 -> U+1EE0 */ 1375, - /* U+01A0+0309 -> U+1EDE */ 1373, - /* U+01A0+0323 -> U+1EE2 */ 1377, - /* U+01A1+0300 -> U+1EDD */ 1372, - /* U+01A1+0301 -> U+1EDB */ 1370, - /* U+01A1+0303 -> U+1EE1 */ 1376, - /* U+01A1+0309 -> U+1EDF */ 1374, - /* U+01A1+0323 -> U+1EE3 */ 1378, - /* U+01AF+0300 -> U+1EEA */ 1385, - /* U+01AF+0301 -> U+1EE8 */ 1383, - /* U+01AF+0303 -> U+1EEE */ 1389, - /* U+01AF+0309 -> U+1EEC */ 1387, - /* U+01AF+0323 -> U+1EF0 */ 1391, - /* U+01B0+0300 -> U+1EEB */ 1386, - /* U+01B0+0301 -> U+1EE9 */ 1384, - /* U+01B0+0303 -> U+1EEF */ 1390, - /* U+01B0+0309 -> U+1EED */ 1388, - /* U+01B0+0323 -> U+1EF1 */ 1392, + /* U+0102+0300 -> U+1EB0 */ 1354, + /* U+0102+0301 -> U+1EAE */ 1352, + /* U+0102+0303 -> U+1EB4 */ 1358, + /* U+0102+0309 -> U+1EB2 */ 1356, + /* U+0103+0300 -> U+1EB1 */ 1355, + /* U+0103+0301 -> U+1EAF */ 1353, + /* U+0103+0303 -> U+1EB5 */ 1359, + /* U+0103+0309 -> U+1EB3 */ 1357, + /* U+0112+0300 -> U+1E14 */ 1202, + /* U+0112+0301 -> U+1E16 */ 1204, + /* U+0113+0300 -> U+1E15 */ 1203, + /* U+0113+0301 -> U+1E17 */ 1205, + /* U+014C+0300 -> U+1E50 */ 1262, + /* U+014C+0301 -> U+1E52 */ 1264, + /* U+014D+0300 -> U+1E51 */ 1263, + /* U+014D+0301 -> U+1E53 */ 1265, + /* U+015A+0307 -> U+1E64 */ 1282, + /* U+015B+0307 -> U+1E65 */ 1283, + /* U+0160+0307 -> U+1E66 */ 1284, + /* U+0161+0307 -> U+1E67 */ 1285, + /* U+0168+0301 -> U+1E78 */ 1302, + /* U+0169+0301 -> U+1E79 */ 1303, + /* U+016A+0308 -> U+1E7A */ 1304, + /* U+016B+0308 -> U+1E7B */ 1305, + /* U+017F+0307 -> U+1E9B */ 1337, + /* U+01A0+0300 -> U+1EDC */ 1398, + /* U+01A0+0301 -> U+1EDA */ 1396, + /* U+01A0+0303 -> U+1EE0 */ 1402, + /* U+01A0+0309 -> U+1EDE */ 1400, + /* U+01A0+0323 -> U+1EE2 */ 1404, + /* U+01A1+0300 -> U+1EDD */ 1399, + /* U+01A1+0301 -> U+1EDB */ 1397, + /* U+01A1+0303 -> U+1EE1 */ 1403, + /* U+01A1+0309 -> U+1EDF */ 1401, + /* U+01A1+0323 -> U+1EE3 */ 1405, + /* U+01AF+0300 -> U+1EEA */ 1412, + /* U+01AF+0301 -> U+1EE8 */ 1410, + /* U+01AF+0303 -> U+1EEE */ 1416, + /* U+01AF+0309 -> U+1EEC */ 1414, + /* U+01AF+0323 -> U+1EF0 */ 1418, + /* U+01B0+0300 -> U+1EEB */ 1413, + /* U+01B0+0301 -> U+1EE9 */ 1411, + /* U+01B0+0303 -> U+1EEF */ 1417, + /* U+01B0+0309 -> U+1EED */ 1415, + /* U+01B0+0323 -> U+1EF1 */ 1419, /* U+01B7+030C -> U+01EE */ 224, /* U+01EA+0304 -> U+01EC */ 222, /* U+01EB+0304 -> U+01ED */ 223, /* U+0226+0304 -> U+01E0 */ 212, /* U+0227+0304 -> U+01E1 */ 213, - /* U+0228+0306 -> U+1E1C */ 1183, - /* U+0229+0306 -> U+1E1D */ 1184, + /* U+0228+0306 -> U+1E1C */ 1210, + /* U+0229+0306 -> U+1E1D */ 1211, /* U+022E+0304 -> U+0230 */ 280, /* U+022F+0304 -> U+0231 */ 281, /* U+0292+030C -> U+01EF */ 225, - /* U+0391+0300 -> U+1FBA */ 1572, + /* U+0391+0300 -> U+1FBA */ 1599, /* U+0391+0301 -> U+0386 */ 420, - /* U+0391+0304 -> U+1FB9 */ 1571, - /* U+0391+0306 -> U+1FB8 */ 1570, - /* U+0391+0313 -> U+1F08 */ 1409, - /* U+0391+0314 -> U+1F09 */ 1410, - /* U+0391+0345 -> U+1FBC */ 1574, - /* U+0395+0300 -> U+1FC8 */ 1585, + /* U+0391+0304 -> U+1FB9 */ 1598, + /* U+0391+0306 -> U+1FB8 */ 1597, + /* U+0391+0313 -> U+1F08 */ 1436, + /* U+0391+0314 -> U+1F09 */ 1437, + /* U+0391+0345 -> U+1FBC */ 1601, + /* U+0395+0300 -> U+1FC8 */ 1612, /* U+0395+0301 -> U+0388 */ 422, - /* U+0395+0313 -> U+1F18 */ 1423, - /* U+0395+0314 -> U+1F19 */ 1424, - /* U+0397+0300 -> U+1FCA */ 1587, + /* U+0395+0313 -> U+1F18 */ 1450, + /* U+0395+0314 -> U+1F19 */ 1451, + /* U+0397+0300 -> U+1FCA */ 1614, /* U+0397+0301 -> U+0389 */ 423, - /* U+0397+0313 -> U+1F28 */ 1437, - /* U+0397+0314 -> U+1F29 */ 1438, - /* U+0397+0345 -> U+1FCC */ 1589, - /* U+0399+0300 -> U+1FDA */ 1601, + /* U+0397+0313 -> U+1F28 */ 1464, + /* U+0397+0314 -> U+1F29 */ 1465, + /* U+0397+0345 -> U+1FCC */ 1616, + /* U+0399+0300 -> U+1FDA */ 1628, /* U+0399+0301 -> U+038A */ 424, - /* U+0399+0304 -> U+1FD9 */ 1600, - /* U+0399+0306 -> U+1FD8 */ 1599, + /* U+0399+0304 -> U+1FD9 */ 1627, + /* U+0399+0306 -> U+1FD8 */ 1626, /* U+0399+0308 -> U+03AA */ 429, - /* U+0399+0313 -> U+1F38 */ 1453, - /* U+0399+0314 -> U+1F39 */ 1454, - /* U+039F+0300 -> U+1FF8 */ 1627, + /* U+0399+0313 -> U+1F38 */ 1480, + /* U+0399+0314 -> U+1F39 */ 1481, + /* U+039F+0300 -> U+1FF8 */ 1654, /* U+039F+0301 -> U+038C */ 425, - /* U+039F+0313 -> U+1F48 */ 1467, - /* U+039F+0314 -> U+1F49 */ 1468, - /* U+03A1+0314 -> U+1FEC */ 1618, - /* U+03A5+0300 -> U+1FEA */ 1616, + /* U+039F+0313 -> U+1F48 */ 1494, + /* U+039F+0314 -> U+1F49 */ 1495, + /* U+03A1+0314 -> U+1FEC */ 1645, + /* U+03A5+0300 -> U+1FEA */ 1643, /* U+03A5+0301 -> U+038E */ 426, - /* U+03A5+0304 -> U+1FE9 */ 1615, - /* U+03A5+0306 -> U+1FE8 */ 1614, + /* U+03A5+0304 -> U+1FE9 */ 1642, + /* U+03A5+0306 -> U+1FE8 */ 1641, /* U+03A5+0308 -> U+03AB */ 430, - /* U+03A5+0314 -> U+1F59 */ 1481, - /* U+03A9+0300 -> U+1FFA */ 1629, + /* U+03A5+0314 -> U+1F59 */ 1508, + /* U+03A9+0300 -> U+1FFA */ 1656, /* U+03A9+0301 -> U+038F */ 427, - /* U+03A9+0313 -> U+1F68 */ 1493, - /* U+03A9+0314 -> U+1F69 */ 1494, - /* U+03A9+0345 -> U+1FFC */ 1631, - /* U+03AC+0345 -> U+1FB4 */ 1567, - /* U+03AE+0345 -> U+1FC4 */ 1582, - /* U+03B1+0300 -> U+1F70 */ 1501, + /* U+03A9+0313 -> U+1F68 */ 1520, + /* U+03A9+0314 -> U+1F69 */ 1521, + /* U+03A9+0345 -> U+1FFC */ 1658, + /* U+03AC+0345 -> U+1FB4 */ 1594, + /* U+03AE+0345 -> U+1FC4 */ 1609, + /* U+03B1+0300 -> U+1F70 */ 1528, /* U+03B1+0301 -> U+03AC */ 431, - /* U+03B1+0304 -> U+1FB1 */ 1564, - /* U+03B1+0306 -> U+1FB0 */ 1563, - /* U+03B1+0313 -> U+1F00 */ 1401, - /* U+03B1+0314 -> U+1F01 */ 1402, - /* U+03B1+0342 -> U+1FB6 */ 1568, - /* U+03B1+0345 -> U+1FB3 */ 1566, - /* U+03B5+0300 -> U+1F72 */ 1503, + /* U+03B1+0304 -> U+1FB1 */ 1591, + /* U+03B1+0306 -> U+1FB0 */ 1590, + /* U+03B1+0313 -> U+1F00 */ 1428, + /* U+03B1+0314 -> U+1F01 */ 1429, + /* U+03B1+0342 -> U+1FB6 */ 1595, + /* U+03B1+0345 -> U+1FB3 */ 1593, + /* U+03B5+0300 -> U+1F72 */ 1530, /* U+03B5+0301 -> U+03AD */ 432, - /* U+03B5+0313 -> U+1F10 */ 1417, - /* U+03B5+0314 -> U+1F11 */ 1418, - /* U+03B7+0300 -> U+1F74 */ 1505, + /* U+03B5+0313 -> U+1F10 */ 1444, + /* U+03B5+0314 -> U+1F11 */ 1445, + /* U+03B7+0300 -> U+1F74 */ 1532, /* U+03B7+0301 -> U+03AE */ 433, - /* U+03B7+0313 -> U+1F20 */ 1429, - /* U+03B7+0314 -> U+1F21 */ 1430, - /* U+03B7+0342 -> U+1FC6 */ 1583, - /* U+03B7+0345 -> U+1FC3 */ 1581, - /* U+03B9+0300 -> U+1F76 */ 1507, + /* U+03B7+0313 -> U+1F20 */ 1456, + /* U+03B7+0314 -> U+1F21 */ 1457, + /* U+03B7+0342 -> U+1FC6 */ 1610, + /* U+03B7+0345 -> U+1FC3 */ 1608, + /* U+03B9+0300 -> U+1F76 */ 1534, /* U+03B9+0301 -> U+03AF */ 434, - /* U+03B9+0304 -> U+1FD1 */ 1594, - /* U+03B9+0306 -> U+1FD0 */ 1593, + /* U+03B9+0304 -> U+1FD1 */ 1621, + /* U+03B9+0306 -> U+1FD0 */ 1620, /* U+03B9+0308 -> U+03CA */ 436, - /* U+03B9+0313 -> U+1F30 */ 1445, - /* U+03B9+0314 -> U+1F31 */ 1446, - /* U+03B9+0342 -> U+1FD6 */ 1597, - /* U+03BF+0300 -> U+1F78 */ 1509, + /* U+03B9+0313 -> U+1F30 */ 1472, + /* U+03B9+0314 -> U+1F31 */ 1473, + /* U+03B9+0342 -> U+1FD6 */ 1624, + /* U+03BF+0300 -> U+1F78 */ 1536, /* U+03BF+0301 -> U+03CC */ 438, - /* U+03BF+0313 -> U+1F40 */ 1461, - /* U+03BF+0314 -> U+1F41 */ 1462, - /* U+03C1+0313 -> U+1FE4 */ 1610, - /* U+03C1+0314 -> U+1FE5 */ 1611, - /* U+03C5+0300 -> U+1F7A */ 1511, + /* U+03BF+0313 -> U+1F40 */ 1488, + /* U+03BF+0314 -> U+1F41 */ 1489, + /* U+03C1+0313 -> U+1FE4 */ 1637, + /* U+03C1+0314 -> U+1FE5 */ 1638, + /* U+03C5+0300 -> U+1F7A */ 1538, /* U+03C5+0301 -> U+03CD */ 439, - /* U+03C5+0304 -> U+1FE1 */ 1607, - /* U+03C5+0306 -> U+1FE0 */ 1606, + /* U+03C5+0304 -> U+1FE1 */ 1634, + /* U+03C5+0306 -> U+1FE0 */ 1633, /* U+03C5+0308 -> U+03CB */ 437, - /* U+03C5+0313 -> U+1F50 */ 1473, - /* U+03C5+0314 -> U+1F51 */ 1474, - /* U+03C5+0342 -> U+1FE6 */ 1612, - /* U+03C9+0300 -> U+1F7C */ 1513, + /* U+03C5+0313 -> U+1F50 */ 1500, + /* U+03C5+0314 -> U+1F51 */ 1501, + /* U+03C5+0342 -> U+1FE6 */ 1639, + /* U+03C9+0300 -> U+1F7C */ 1540, /* U+03C9+0301 -> U+03CE */ 440, - /* U+03C9+0313 -> U+1F60 */ 1485, - /* U+03C9+0314 -> U+1F61 */ 1486, - /* U+03C9+0342 -> U+1FF6 */ 1625, - /* U+03C9+0345 -> U+1FF3 */ 1623, - /* U+03CA+0300 -> U+1FD2 */ 1595, + /* U+03C9+0313 -> U+1F60 */ 1512, + /* U+03C9+0314 -> U+1F61 */ 1513, + /* U+03C9+0342 -> U+1FF6 */ 1652, + /* U+03C9+0345 -> U+1FF3 */ 1650, + /* U+03CA+0300 -> U+1FD2 */ 1622, /* U+03CA+0301 -> U+0390 */ 428, - /* U+03CA+0342 -> U+1FD7 */ 1598, - /* U+03CB+0300 -> U+1FE2 */ 1608, + /* U+03CA+0342 -> U+1FD7 */ 1625, + /* U+03CB+0300 -> U+1FE2 */ 1635, /* U+03CB+0301 -> U+03B0 */ 435, - /* U+03CB+0342 -> U+1FE7 */ 1613, - /* U+03CE+0345 -> U+1FF4 */ 1624, + /* U+03CB+0342 -> U+1FE7 */ 1640, + /* U+03CE+0345 -> U+1FF4 */ 1651, /* U+03D2+0301 -> U+03D3 */ 444, /* U+03D2+0308 -> U+03D4 */ 445, /* U+0406+0308 -> U+0407 */ 457, @@ -2452,296 +2461,296 @@ static const uint16 RecompInverseLookup[961] = /* U+0DD9+0DDF -> U+0DDE */ 820, /* U+0DDC+0DCA -> U+0DDD */ 819, /* U+1025+102E -> U+1026 */ 878, - /* U+1B05+1B35 -> U+1B06 */ 938, - /* U+1B07+1B35 -> U+1B08 */ 939, - /* U+1B09+1B35 -> U+1B0A */ 940, - /* U+1B0B+1B35 -> U+1B0C */ 941, - /* U+1B0D+1B35 -> U+1B0E */ 942, - /* U+1B11+1B35 -> U+1B12 */ 943, - /* U+1B3A+1B35 -> U+1B3B */ 945, - /* U+1B3C+1B35 -> U+1B3D */ 946, - /* U+1B3E+1B35 -> U+1B40 */ 947, - /* U+1B3F+1B35 -> U+1B41 */ 948, - /* U+1B42+1B35 -> U+1B43 */ 949, - /* U+1E36+0304 -> U+1E38 */ 1211, - /* U+1E37+0304 -> U+1E39 */ 1212, - /* U+1E5A+0304 -> U+1E5C */ 1247, - /* U+1E5B+0304 -> U+1E5D */ 1248, - /* U+1E62+0307 -> U+1E68 */ 1259, - /* U+1E63+0307 -> U+1E69 */ 1260, - /* U+1EA0+0302 -> U+1EAC */ 1323, - /* U+1EA0+0306 -> U+1EB6 */ 1333, - /* U+1EA1+0302 -> U+1EAD */ 1324, - /* U+1EA1+0306 -> U+1EB7 */ 1334, - /* U+1EB8+0302 -> U+1EC6 */ 1349, - /* U+1EB9+0302 -> U+1EC7 */ 1350, - /* U+1ECC+0302 -> U+1ED8 */ 1367, - /* U+1ECD+0302 -> U+1ED9 */ 1368, - /* U+1F00+0300 -> U+1F02 */ 1403, - /* U+1F00+0301 -> U+1F04 */ 1405, - /* U+1F00+0342 -> U+1F06 */ 1407, - /* U+1F00+0345 -> U+1F80 */ 1515, - /* U+1F01+0300 -> U+1F03 */ 1404, - /* U+1F01+0301 -> U+1F05 */ 1406, - /* U+1F01+0342 -> U+1F07 */ 1408, - /* U+1F01+0345 -> U+1F81 */ 1516, - /* U+1F02+0345 -> U+1F82 */ 1517, - /* U+1F03+0345 -> U+1F83 */ 1518, - /* U+1F04+0345 -> U+1F84 */ 1519, - /* U+1F05+0345 -> U+1F85 */ 1520, - /* U+1F06+0345 -> U+1F86 */ 1521, - /* U+1F07+0345 -> U+1F87 */ 1522, - /* U+1F08+0300 -> U+1F0A */ 1411, - /* U+1F08+0301 -> U+1F0C */ 1413, - /* U+1F08+0342 -> U+1F0E */ 1415, - /* U+1F08+0345 -> U+1F88 */ 1523, - /* U+1F09+0300 -> U+1F0B */ 1412, - /* U+1F09+0301 -> U+1F0D */ 1414, - /* U+1F09+0342 -> U+1F0F */ 1416, - /* U+1F09+0345 -> U+1F89 */ 1524, - /* U+1F0A+0345 -> U+1F8A */ 1525, - /* U+1F0B+0345 -> U+1F8B */ 1526, - /* U+1F0C+0345 -> U+1F8C */ 1527, - /* U+1F0D+0345 -> U+1F8D */ 1528, - /* U+1F0E+0345 -> U+1F8E */ 1529, - /* U+1F0F+0345 -> U+1F8F */ 1530, - /* U+1F10+0300 -> U+1F12 */ 1419, - /* U+1F10+0301 -> U+1F14 */ 1421, - /* U+1F11+0300 -> U+1F13 */ 1420, - /* U+1F11+0301 -> U+1F15 */ 1422, - /* U+1F18+0300 -> U+1F1A */ 1425, - /* U+1F18+0301 -> U+1F1C */ 1427, - /* U+1F19+0300 -> U+1F1B */ 1426, - /* U+1F19+0301 -> U+1F1D */ 1428, - /* U+1F20+0300 -> U+1F22 */ 1431, - /* U+1F20+0301 -> U+1F24 */ 1433, - /* U+1F20+0342 -> U+1F26 */ 1435, - /* U+1F20+0345 -> U+1F90 */ 1531, - /* U+1F21+0300 -> U+1F23 */ 1432, - /* U+1F21+0301 -> U+1F25 */ 1434, - /* U+1F21+0342 -> U+1F27 */ 1436, - /* U+1F21+0345 -> U+1F91 */ 1532, - /* U+1F22+0345 -> U+1F92 */ 1533, - /* U+1F23+0345 -> U+1F93 */ 1534, - /* U+1F24+0345 -> U+1F94 */ 1535, - /* U+1F25+0345 -> U+1F95 */ 1536, - /* U+1F26+0345 -> U+1F96 */ 1537, - /* U+1F27+0345 -> U+1F97 */ 1538, - /* U+1F28+0300 -> U+1F2A */ 1439, - /* U+1F28+0301 -> U+1F2C */ 1441, - /* U+1F28+0342 -> U+1F2E */ 1443, - /* U+1F28+0345 -> U+1F98 */ 1539, - /* U+1F29+0300 -> U+1F2B */ 1440, - /* U+1F29+0301 -> U+1F2D */ 1442, - /* U+1F29+0342 -> U+1F2F */ 1444, - /* U+1F29+0345 -> U+1F99 */ 1540, - /* U+1F2A+0345 -> U+1F9A */ 1541, - /* U+1F2B+0345 -> U+1F9B */ 1542, - /* U+1F2C+0345 -> U+1F9C */ 1543, - /* U+1F2D+0345 -> U+1F9D */ 1544, - /* U+1F2E+0345 -> U+1F9E */ 1545, - /* U+1F2F+0345 -> U+1F9F */ 1546, - /* U+1F30+0300 -> U+1F32 */ 1447, - /* U+1F30+0301 -> U+1F34 */ 1449, - /* U+1F30+0342 -> U+1F36 */ 1451, - /* U+1F31+0300 -> U+1F33 */ 1448, - /* U+1F31+0301 -> U+1F35 */ 1450, - /* U+1F31+0342 -> U+1F37 */ 1452, - /* U+1F38+0300 -> U+1F3A */ 1455, - /* U+1F38+0301 -> U+1F3C */ 1457, - /* U+1F38+0342 -> U+1F3E */ 1459, - /* U+1F39+0300 -> U+1F3B */ 1456, - /* U+1F39+0301 -> U+1F3D */ 1458, - /* U+1F39+0342 -> U+1F3F */ 1460, - /* U+1F40+0300 -> U+1F42 */ 1463, - /* U+1F40+0301 -> U+1F44 */ 1465, - /* U+1F41+0300 -> U+1F43 */ 1464, - /* U+1F41+0301 -> U+1F45 */ 1466, - /* U+1F48+0300 -> U+1F4A */ 1469, - /* U+1F48+0301 -> U+1F4C */ 1471, - /* U+1F49+0300 -> U+1F4B */ 1470, - /* U+1F49+0301 -> U+1F4D */ 1472, - /* U+1F50+0300 -> U+1F52 */ 1475, - /* U+1F50+0301 -> U+1F54 */ 1477, - /* U+1F50+0342 -> U+1F56 */ 1479, - /* U+1F51+0300 -> U+1F53 */ 1476, - /* U+1F51+0301 -> U+1F55 */ 1478, - /* U+1F51+0342 -> U+1F57 */ 1480, - /* U+1F59+0300 -> U+1F5B */ 1482, - /* U+1F59+0301 -> U+1F5D */ 1483, - /* U+1F59+0342 -> U+1F5F */ 1484, - /* U+1F60+0300 -> U+1F62 */ 1487, - /* U+1F60+0301 -> U+1F64 */ 1489, - /* U+1F60+0342 -> U+1F66 */ 1491, - /* U+1F60+0345 -> U+1FA0 */ 1547, - /* U+1F61+0300 -> U+1F63 */ 1488, - /* U+1F61+0301 -> U+1F65 */ 1490, - /* U+1F61+0342 -> U+1F67 */ 1492, - /* U+1F61+0345 -> U+1FA1 */ 1548, - /* U+1F62+0345 -> U+1FA2 */ 1549, - /* U+1F63+0345 -> U+1FA3 */ 1550, - /* U+1F64+0345 -> U+1FA4 */ 1551, - /* U+1F65+0345 -> U+1FA5 */ 1552, - /* U+1F66+0345 -> U+1FA6 */ 1553, - /* U+1F67+0345 -> U+1FA7 */ 1554, - /* U+1F68+0300 -> U+1F6A */ 1495, - /* U+1F68+0301 -> U+1F6C */ 1497, - /* U+1F68+0342 -> U+1F6E */ 1499, - /* U+1F68+0345 -> U+1FA8 */ 1555, - /* U+1F69+0300 -> U+1F6B */ 1496, - /* U+1F69+0301 -> U+1F6D */ 1498, - /* U+1F69+0342 -> U+1F6F */ 1500, - /* U+1F69+0345 -> U+1FA9 */ 1556, - /* U+1F6A+0345 -> U+1FAA */ 1557, - /* U+1F6B+0345 -> U+1FAB */ 1558, - /* U+1F6C+0345 -> U+1FAC */ 1559, - /* U+1F6D+0345 -> U+1FAD */ 1560, - /* U+1F6E+0345 -> U+1FAE */ 1561, - /* U+1F6F+0345 -> U+1FAF */ 1562, - /* U+1F70+0345 -> U+1FB2 */ 1565, - /* U+1F74+0345 -> U+1FC2 */ 1580, - /* U+1F7C+0345 -> U+1FF2 */ 1622, - /* U+1FB6+0345 -> U+1FB7 */ 1569, - /* U+1FBF+0300 -> U+1FCD */ 1590, - /* U+1FBF+0301 -> U+1FCE */ 1591, - /* U+1FBF+0342 -> U+1FCF */ 1592, - /* U+1FC6+0345 -> U+1FC7 */ 1584, - /* U+1FF6+0345 -> U+1FF7 */ 1626, - /* U+1FFE+0300 -> U+1FDD */ 1603, - /* U+1FFE+0301 -> U+1FDE */ 1604, - /* U+1FFE+0342 -> U+1FDF */ 1605, - /* U+2190+0338 -> U+219A */ 1836, - /* U+2192+0338 -> U+219B */ 1837, - /* U+2194+0338 -> U+21AE */ 1838, - /* U+21D0+0338 -> U+21CD */ 1839, - /* U+21D2+0338 -> U+21CF */ 1841, - /* U+21D4+0338 -> U+21CE */ 1840, - /* U+2203+0338 -> U+2204 */ 1842, - /* U+2208+0338 -> U+2209 */ 1843, - /* U+220B+0338 -> U+220C */ 1844, - /* U+2223+0338 -> U+2224 */ 1845, - /* U+2225+0338 -> U+2226 */ 1846, - /* U+223C+0338 -> U+2241 */ 1851, - /* U+2243+0338 -> U+2244 */ 1852, - /* U+2245+0338 -> U+2247 */ 1853, - /* U+2248+0338 -> U+2249 */ 1854, - /* U+224D+0338 -> U+226D */ 1857, - /* U+2261+0338 -> U+2262 */ 1856, - /* U+2264+0338 -> U+2270 */ 1860, - /* U+2265+0338 -> U+2271 */ 1861, - /* U+2272+0338 -> U+2274 */ 1862, - /* U+2273+0338 -> U+2275 */ 1863, - /* U+2276+0338 -> U+2278 */ 1864, - /* U+2277+0338 -> U+2279 */ 1865, - /* U+227A+0338 -> U+2280 */ 1866, - /* U+227B+0338 -> U+2281 */ 1867, - /* U+227C+0338 -> U+22E0 */ 1876, - /* U+227D+0338 -> U+22E1 */ 1877, - /* U+2282+0338 -> U+2284 */ 1868, - /* U+2283+0338 -> U+2285 */ 1869, - /* U+2286+0338 -> U+2288 */ 1870, - /* U+2287+0338 -> U+2289 */ 1871, - /* U+2291+0338 -> U+22E2 */ 1878, - /* U+2292+0338 -> U+22E3 */ 1879, - /* U+22A2+0338 -> U+22AC */ 1872, - /* U+22A8+0338 -> U+22AD */ 1873, - /* U+22A9+0338 -> U+22AE */ 1874, - /* U+22AB+0338 -> U+22AF */ 1875, - /* U+22B2+0338 -> U+22EA */ 1880, - /* U+22B3+0338 -> U+22EB */ 1881, - /* U+22B4+0338 -> U+22EC */ 1882, - /* U+22B5+0338 -> U+22ED */ 1883, - /* U+3046+3099 -> U+3094 */ 2321, - /* U+304B+3099 -> U+304C */ 2296, - /* U+304D+3099 -> U+304E */ 2297, - /* U+304F+3099 -> U+3050 */ 2298, - /* U+3051+3099 -> U+3052 */ 2299, - /* U+3053+3099 -> U+3054 */ 2300, - /* U+3055+3099 -> U+3056 */ 2301, - /* U+3057+3099 -> U+3058 */ 2302, - /* U+3059+3099 -> U+305A */ 2303, - /* U+305B+3099 -> U+305C */ 2304, - /* U+305D+3099 -> U+305E */ 2305, - /* U+305F+3099 -> U+3060 */ 2306, - /* U+3061+3099 -> U+3062 */ 2307, - /* U+3064+3099 -> U+3065 */ 2308, - /* U+3066+3099 -> U+3067 */ 2309, - /* U+3068+3099 -> U+3069 */ 2310, - /* U+306F+3099 -> U+3070 */ 2311, - /* U+306F+309A -> U+3071 */ 2312, - /* U+3072+3099 -> U+3073 */ 2313, - /* U+3072+309A -> U+3074 */ 2314, - /* U+3075+3099 -> U+3076 */ 2315, - /* U+3075+309A -> U+3077 */ 2316, - /* U+3078+3099 -> U+3079 */ 2317, - /* U+3078+309A -> U+307A */ 2318, - /* U+307B+3099 -> U+307C */ 2319, - /* U+307B+309A -> U+307D */ 2320, - /* U+309D+3099 -> U+309E */ 2326, - /* U+30A6+3099 -> U+30F4 */ 2353, - /* U+30AB+3099 -> U+30AC */ 2328, - /* U+30AD+3099 -> U+30AE */ 2329, - /* U+30AF+3099 -> U+30B0 */ 2330, - /* U+30B1+3099 -> U+30B2 */ 2331, - /* U+30B3+3099 -> U+30B4 */ 2332, - /* U+30B5+3099 -> U+30B6 */ 2333, - /* U+30B7+3099 -> U+30B8 */ 2334, - /* U+30B9+3099 -> U+30BA */ 2335, - /* U+30BB+3099 -> U+30BC */ 2336, - /* U+30BD+3099 -> U+30BE */ 2337, - /* U+30BF+3099 -> U+30C0 */ 2338, - /* U+30C1+3099 -> U+30C2 */ 2339, - /* U+30C4+3099 -> U+30C5 */ 2340, - /* U+30C6+3099 -> U+30C7 */ 2341, - /* U+30C8+3099 -> U+30C9 */ 2342, - /* U+30CF+3099 -> U+30D0 */ 2343, - /* U+30CF+309A -> U+30D1 */ 2344, - /* U+30D2+3099 -> U+30D3 */ 2345, - /* U+30D2+309A -> U+30D4 */ 2346, - /* U+30D5+3099 -> U+30D6 */ 2347, - /* U+30D5+309A -> U+30D7 */ 2348, - /* U+30D8+3099 -> U+30D9 */ 2349, - /* U+30D8+309A -> U+30DA */ 2350, - /* U+30DB+3099 -> U+30DC */ 2351, - /* U+30DB+309A -> U+30DD */ 2352, - /* U+30EF+3099 -> U+30F7 */ 2354, - /* U+30F0+3099 -> U+30F8 */ 2355, - /* U+30F1+3099 -> U+30F9 */ 2356, - /* U+30F2+3099 -> U+30FA */ 2357, - /* U+30FD+3099 -> U+30FE */ 2358, - /* U+105D2+0307 -> U+105C9 */ 4599, - /* U+105DA+0307 -> U+105E4 */ 4600, - /* U+11099+110BA -> U+1109A */ 4697, - /* U+1109B+110BA -> U+1109C */ 4698, - /* U+110A5+110BA -> U+110AB */ 4699, - /* U+11131+11127 -> U+1112E */ 4705, - /* U+11132+11127 -> U+1112F */ 4706, - /* U+11347+1133E -> U+1134B */ 4718, - /* U+11347+11357 -> U+1134C */ 4719, - /* U+11382+113C9 -> U+11383 */ 4733, - /* U+11384+113BB -> U+11385 */ 4734, - /* U+1138B+113C2 -> U+1138E */ 4735, - /* U+11390+113C9 -> U+11391 */ 4736, - /* U+113C2+113B8 -> U+113C7 */ 4738, - /* U+113C2+113C2 -> U+113C5 */ 4737, - /* U+113C2+113C9 -> U+113C8 */ 4739, - /* U+114B9+114B0 -> U+114BC */ 4747, - /* U+114B9+114BA -> U+114BB */ 4746, - /* U+114B9+114BD -> U+114BE */ 4748, - /* U+115B8+115AF -> U+115BA */ 4751, - /* U+115B9+115AF -> U+115BB */ 4752, - /* U+11935+11930 -> U+11938 */ 4761, - /* U+1611E+1611E -> U+16121 */ 4776, - /* U+1611E+1611F -> U+16123 */ 4778, - /* U+1611E+16120 -> U+16125 */ 4780, - /* U+1611E+16129 -> U+16122 */ 4777, - /* U+16121+1611F -> U+16126 */ 4781, - /* U+16121+16120 -> U+16128 */ 4783, - /* U+16122+1611F -> U+16127 */ 4782, - /* U+16129+1611F -> U+16124 */ 4779, - /* U+16D63+16D67 -> U+16D69 */ 4798, - /* U+16D67+16D67 -> U+16D68 */ 4797, - /* U+16D69+16D67 -> U+16D6A */ 4799 + /* U+1B05+1B35 -> U+1B06 */ 965, + /* U+1B07+1B35 -> U+1B08 */ 966, + /* U+1B09+1B35 -> U+1B0A */ 967, + /* U+1B0B+1B35 -> U+1B0C */ 968, + /* U+1B0D+1B35 -> U+1B0E */ 969, + /* U+1B11+1B35 -> U+1B12 */ 970, + /* U+1B3A+1B35 -> U+1B3B */ 972, + /* U+1B3C+1B35 -> U+1B3D */ 973, + /* U+1B3E+1B35 -> U+1B40 */ 974, + /* U+1B3F+1B35 -> U+1B41 */ 975, + /* U+1B42+1B35 -> U+1B43 */ 976, + /* U+1E36+0304 -> U+1E38 */ 1238, + /* U+1E37+0304 -> U+1E39 */ 1239, + /* U+1E5A+0304 -> U+1E5C */ 1274, + /* U+1E5B+0304 -> U+1E5D */ 1275, + /* U+1E62+0307 -> U+1E68 */ 1286, + /* U+1E63+0307 -> U+1E69 */ 1287, + /* U+1EA0+0302 -> U+1EAC */ 1350, + /* U+1EA0+0306 -> U+1EB6 */ 1360, + /* U+1EA1+0302 -> U+1EAD */ 1351, + /* U+1EA1+0306 -> U+1EB7 */ 1361, + /* U+1EB8+0302 -> U+1EC6 */ 1376, + /* U+1EB9+0302 -> U+1EC7 */ 1377, + /* U+1ECC+0302 -> U+1ED8 */ 1394, + /* U+1ECD+0302 -> U+1ED9 */ 1395, + /* U+1F00+0300 -> U+1F02 */ 1430, + /* U+1F00+0301 -> U+1F04 */ 1432, + /* U+1F00+0342 -> U+1F06 */ 1434, + /* U+1F00+0345 -> U+1F80 */ 1542, + /* U+1F01+0300 -> U+1F03 */ 1431, + /* U+1F01+0301 -> U+1F05 */ 1433, + /* U+1F01+0342 -> U+1F07 */ 1435, + /* U+1F01+0345 -> U+1F81 */ 1543, + /* U+1F02+0345 -> U+1F82 */ 1544, + /* U+1F03+0345 -> U+1F83 */ 1545, + /* U+1F04+0345 -> U+1F84 */ 1546, + /* U+1F05+0345 -> U+1F85 */ 1547, + /* U+1F06+0345 -> U+1F86 */ 1548, + /* U+1F07+0345 -> U+1F87 */ 1549, + /* U+1F08+0300 -> U+1F0A */ 1438, + /* U+1F08+0301 -> U+1F0C */ 1440, + /* U+1F08+0342 -> U+1F0E */ 1442, + /* U+1F08+0345 -> U+1F88 */ 1550, + /* U+1F09+0300 -> U+1F0B */ 1439, + /* U+1F09+0301 -> U+1F0D */ 1441, + /* U+1F09+0342 -> U+1F0F */ 1443, + /* U+1F09+0345 -> U+1F89 */ 1551, + /* U+1F0A+0345 -> U+1F8A */ 1552, + /* U+1F0B+0345 -> U+1F8B */ 1553, + /* U+1F0C+0345 -> U+1F8C */ 1554, + /* U+1F0D+0345 -> U+1F8D */ 1555, + /* U+1F0E+0345 -> U+1F8E */ 1556, + /* U+1F0F+0345 -> U+1F8F */ 1557, + /* U+1F10+0300 -> U+1F12 */ 1446, + /* U+1F10+0301 -> U+1F14 */ 1448, + /* U+1F11+0300 -> U+1F13 */ 1447, + /* U+1F11+0301 -> U+1F15 */ 1449, + /* U+1F18+0300 -> U+1F1A */ 1452, + /* U+1F18+0301 -> U+1F1C */ 1454, + /* U+1F19+0300 -> U+1F1B */ 1453, + /* U+1F19+0301 -> U+1F1D */ 1455, + /* U+1F20+0300 -> U+1F22 */ 1458, + /* U+1F20+0301 -> U+1F24 */ 1460, + /* U+1F20+0342 -> U+1F26 */ 1462, + /* U+1F20+0345 -> U+1F90 */ 1558, + /* U+1F21+0300 -> U+1F23 */ 1459, + /* U+1F21+0301 -> U+1F25 */ 1461, + /* U+1F21+0342 -> U+1F27 */ 1463, + /* U+1F21+0345 -> U+1F91 */ 1559, + /* U+1F22+0345 -> U+1F92 */ 1560, + /* U+1F23+0345 -> U+1F93 */ 1561, + /* U+1F24+0345 -> U+1F94 */ 1562, + /* U+1F25+0345 -> U+1F95 */ 1563, + /* U+1F26+0345 -> U+1F96 */ 1564, + /* U+1F27+0345 -> U+1F97 */ 1565, + /* U+1F28+0300 -> U+1F2A */ 1466, + /* U+1F28+0301 -> U+1F2C */ 1468, + /* U+1F28+0342 -> U+1F2E */ 1470, + /* U+1F28+0345 -> U+1F98 */ 1566, + /* U+1F29+0300 -> U+1F2B */ 1467, + /* U+1F29+0301 -> U+1F2D */ 1469, + /* U+1F29+0342 -> U+1F2F */ 1471, + /* U+1F29+0345 -> U+1F99 */ 1567, + /* U+1F2A+0345 -> U+1F9A */ 1568, + /* U+1F2B+0345 -> U+1F9B */ 1569, + /* U+1F2C+0345 -> U+1F9C */ 1570, + /* U+1F2D+0345 -> U+1F9D */ 1571, + /* U+1F2E+0345 -> U+1F9E */ 1572, + /* U+1F2F+0345 -> U+1F9F */ 1573, + /* U+1F30+0300 -> U+1F32 */ 1474, + /* U+1F30+0301 -> U+1F34 */ 1476, + /* U+1F30+0342 -> U+1F36 */ 1478, + /* U+1F31+0300 -> U+1F33 */ 1475, + /* U+1F31+0301 -> U+1F35 */ 1477, + /* U+1F31+0342 -> U+1F37 */ 1479, + /* U+1F38+0300 -> U+1F3A */ 1482, + /* U+1F38+0301 -> U+1F3C */ 1484, + /* U+1F38+0342 -> U+1F3E */ 1486, + /* U+1F39+0300 -> U+1F3B */ 1483, + /* U+1F39+0301 -> U+1F3D */ 1485, + /* U+1F39+0342 -> U+1F3F */ 1487, + /* U+1F40+0300 -> U+1F42 */ 1490, + /* U+1F40+0301 -> U+1F44 */ 1492, + /* U+1F41+0300 -> U+1F43 */ 1491, + /* U+1F41+0301 -> U+1F45 */ 1493, + /* U+1F48+0300 -> U+1F4A */ 1496, + /* U+1F48+0301 -> U+1F4C */ 1498, + /* U+1F49+0300 -> U+1F4B */ 1497, + /* U+1F49+0301 -> U+1F4D */ 1499, + /* U+1F50+0300 -> U+1F52 */ 1502, + /* U+1F50+0301 -> U+1F54 */ 1504, + /* U+1F50+0342 -> U+1F56 */ 1506, + /* U+1F51+0300 -> U+1F53 */ 1503, + /* U+1F51+0301 -> U+1F55 */ 1505, + /* U+1F51+0342 -> U+1F57 */ 1507, + /* U+1F59+0300 -> U+1F5B */ 1509, + /* U+1F59+0301 -> U+1F5D */ 1510, + /* U+1F59+0342 -> U+1F5F */ 1511, + /* U+1F60+0300 -> U+1F62 */ 1514, + /* U+1F60+0301 -> U+1F64 */ 1516, + /* U+1F60+0342 -> U+1F66 */ 1518, + /* U+1F60+0345 -> U+1FA0 */ 1574, + /* U+1F61+0300 -> U+1F63 */ 1515, + /* U+1F61+0301 -> U+1F65 */ 1517, + /* U+1F61+0342 -> U+1F67 */ 1519, + /* U+1F61+0345 -> U+1FA1 */ 1575, + /* U+1F62+0345 -> U+1FA2 */ 1576, + /* U+1F63+0345 -> U+1FA3 */ 1577, + /* U+1F64+0345 -> U+1FA4 */ 1578, + /* U+1F65+0345 -> U+1FA5 */ 1579, + /* U+1F66+0345 -> U+1FA6 */ 1580, + /* U+1F67+0345 -> U+1FA7 */ 1581, + /* U+1F68+0300 -> U+1F6A */ 1522, + /* U+1F68+0301 -> U+1F6C */ 1524, + /* U+1F68+0342 -> U+1F6E */ 1526, + /* U+1F68+0345 -> U+1FA8 */ 1582, + /* U+1F69+0300 -> U+1F6B */ 1523, + /* U+1F69+0301 -> U+1F6D */ 1525, + /* U+1F69+0342 -> U+1F6F */ 1527, + /* U+1F69+0345 -> U+1FA9 */ 1583, + /* U+1F6A+0345 -> U+1FAA */ 1584, + /* U+1F6B+0345 -> U+1FAB */ 1585, + /* U+1F6C+0345 -> U+1FAC */ 1586, + /* U+1F6D+0345 -> U+1FAD */ 1587, + /* U+1F6E+0345 -> U+1FAE */ 1588, + /* U+1F6F+0345 -> U+1FAF */ 1589, + /* U+1F70+0345 -> U+1FB2 */ 1592, + /* U+1F74+0345 -> U+1FC2 */ 1607, + /* U+1F7C+0345 -> U+1FF2 */ 1649, + /* U+1FB6+0345 -> U+1FB7 */ 1596, + /* U+1FBF+0300 -> U+1FCD */ 1617, + /* U+1FBF+0301 -> U+1FCE */ 1618, + /* U+1FBF+0342 -> U+1FCF */ 1619, + /* U+1FC6+0345 -> U+1FC7 */ 1611, + /* U+1FF6+0345 -> U+1FF7 */ 1653, + /* U+1FFE+0300 -> U+1FDD */ 1630, + /* U+1FFE+0301 -> U+1FDE */ 1631, + /* U+1FFE+0342 -> U+1FDF */ 1632, + /* U+2190+0338 -> U+219A */ 1863, + /* U+2192+0338 -> U+219B */ 1864, + /* U+2194+0338 -> U+21AE */ 1865, + /* U+21D0+0338 -> U+21CD */ 1866, + /* U+21D2+0338 -> U+21CF */ 1868, + /* U+21D4+0338 -> U+21CE */ 1867, + /* U+2203+0338 -> U+2204 */ 1869, + /* U+2208+0338 -> U+2209 */ 1870, + /* U+220B+0338 -> U+220C */ 1871, + /* U+2223+0338 -> U+2224 */ 1872, + /* U+2225+0338 -> U+2226 */ 1873, + /* U+223C+0338 -> U+2241 */ 1878, + /* U+2243+0338 -> U+2244 */ 1879, + /* U+2245+0338 -> U+2247 */ 1880, + /* U+2248+0338 -> U+2249 */ 1881, + /* U+224D+0338 -> U+226D */ 1884, + /* U+2261+0338 -> U+2262 */ 1883, + /* U+2264+0338 -> U+2270 */ 1887, + /* U+2265+0338 -> U+2271 */ 1888, + /* U+2272+0338 -> U+2274 */ 1889, + /* U+2273+0338 -> U+2275 */ 1890, + /* U+2276+0338 -> U+2278 */ 1891, + /* U+2277+0338 -> U+2279 */ 1892, + /* U+227A+0338 -> U+2280 */ 1893, + /* U+227B+0338 -> U+2281 */ 1894, + /* U+227C+0338 -> U+22E0 */ 1903, + /* U+227D+0338 -> U+22E1 */ 1904, + /* U+2282+0338 -> U+2284 */ 1895, + /* U+2283+0338 -> U+2285 */ 1896, + /* U+2286+0338 -> U+2288 */ 1897, + /* U+2287+0338 -> U+2289 */ 1898, + /* U+2291+0338 -> U+22E2 */ 1905, + /* U+2292+0338 -> U+22E3 */ 1906, + /* U+22A2+0338 -> U+22AC */ 1899, + /* U+22A8+0338 -> U+22AD */ 1900, + /* U+22A9+0338 -> U+22AE */ 1901, + /* U+22AB+0338 -> U+22AF */ 1902, + /* U+22B2+0338 -> U+22EA */ 1907, + /* U+22B3+0338 -> U+22EB */ 1908, + /* U+22B4+0338 -> U+22EC */ 1909, + /* U+22B5+0338 -> U+22ED */ 1910, + /* U+3046+3099 -> U+3094 */ 2348, + /* U+304B+3099 -> U+304C */ 2323, + /* U+304D+3099 -> U+304E */ 2324, + /* U+304F+3099 -> U+3050 */ 2325, + /* U+3051+3099 -> U+3052 */ 2326, + /* U+3053+3099 -> U+3054 */ 2327, + /* U+3055+3099 -> U+3056 */ 2328, + /* U+3057+3099 -> U+3058 */ 2329, + /* U+3059+3099 -> U+305A */ 2330, + /* U+305B+3099 -> U+305C */ 2331, + /* U+305D+3099 -> U+305E */ 2332, + /* U+305F+3099 -> U+3060 */ 2333, + /* U+3061+3099 -> U+3062 */ 2334, + /* U+3064+3099 -> U+3065 */ 2335, + /* U+3066+3099 -> U+3067 */ 2336, + /* U+3068+3099 -> U+3069 */ 2337, + /* U+306F+3099 -> U+3070 */ 2338, + /* U+306F+309A -> U+3071 */ 2339, + /* U+3072+3099 -> U+3073 */ 2340, + /* U+3072+309A -> U+3074 */ 2341, + /* U+3075+3099 -> U+3076 */ 2342, + /* U+3075+309A -> U+3077 */ 2343, + /* U+3078+3099 -> U+3079 */ 2344, + /* U+3078+309A -> U+307A */ 2345, + /* U+307B+3099 -> U+307C */ 2346, + /* U+307B+309A -> U+307D */ 2347, + /* U+309D+3099 -> U+309E */ 2353, + /* U+30A6+3099 -> U+30F4 */ 2380, + /* U+30AB+3099 -> U+30AC */ 2355, + /* U+30AD+3099 -> U+30AE */ 2356, + /* U+30AF+3099 -> U+30B0 */ 2357, + /* U+30B1+3099 -> U+30B2 */ 2358, + /* U+30B3+3099 -> U+30B4 */ 2359, + /* U+30B5+3099 -> U+30B6 */ 2360, + /* U+30B7+3099 -> U+30B8 */ 2361, + /* U+30B9+3099 -> U+30BA */ 2362, + /* U+30BB+3099 -> U+30BC */ 2363, + /* U+30BD+3099 -> U+30BE */ 2364, + /* U+30BF+3099 -> U+30C0 */ 2365, + /* U+30C1+3099 -> U+30C2 */ 2366, + /* U+30C4+3099 -> U+30C5 */ 2367, + /* U+30C6+3099 -> U+30C7 */ 2368, + /* U+30C8+3099 -> U+30C9 */ 2369, + /* U+30CF+3099 -> U+30D0 */ 2370, + /* U+30CF+309A -> U+30D1 */ 2371, + /* U+30D2+3099 -> U+30D3 */ 2372, + /* U+30D2+309A -> U+30D4 */ 2373, + /* U+30D5+3099 -> U+30D6 */ 2374, + /* U+30D5+309A -> U+30D7 */ 2375, + /* U+30D8+3099 -> U+30D9 */ 2376, + /* U+30D8+309A -> U+30DA */ 2377, + /* U+30DB+3099 -> U+30DC */ 2378, + /* U+30DB+309A -> U+30DD */ 2379, + /* U+30EF+3099 -> U+30F7 */ 2381, + /* U+30F0+3099 -> U+30F8 */ 2382, + /* U+30F1+3099 -> U+30F9 */ 2383, + /* U+30F2+3099 -> U+30FA */ 2384, + /* U+30FD+3099 -> U+30FE */ 2385, + /* U+105D2+0307 -> U+105C9 */ 4627, + /* U+105DA+0307 -> U+105E4 */ 4628, + /* U+11099+110BA -> U+1109A */ 4727, + /* U+1109B+110BA -> U+1109C */ 4728, + /* U+110A5+110BA -> U+110AB */ 4729, + /* U+11131+11127 -> U+1112E */ 4735, + /* U+11132+11127 -> U+1112F */ 4736, + /* U+11347+1133E -> U+1134B */ 4748, + /* U+11347+11357 -> U+1134C */ 4749, + /* U+11382+113C9 -> U+11383 */ 4763, + /* U+11384+113BB -> U+11385 */ 4764, + /* U+1138B+113C2 -> U+1138E */ 4765, + /* U+11390+113C9 -> U+11391 */ 4766, + /* U+113C2+113B8 -> U+113C7 */ 4768, + /* U+113C2+113C2 -> U+113C5 */ 4767, + /* U+113C2+113C9 -> U+113C8 */ 4769, + /* U+114B9+114B0 -> U+114BC */ 4777, + /* U+114B9+114BA -> U+114BB */ 4776, + /* U+114B9+114BD -> U+114BE */ 4778, + /* U+115B8+115AF -> U+115BA */ 4781, + /* U+115B9+115AF -> U+115BB */ 4782, + /* U+11935+11930 -> U+11938 */ 4791, + /* U+1611E+1611E -> U+16121 */ 4806, + /* U+1611E+1611F -> U+16123 */ 4808, + /* U+1611E+16120 -> U+16125 */ 4810, + /* U+1611E+16129 -> U+16122 */ 4807, + /* U+16121+1611F -> U+16126 */ 4811, + /* U+16121+16120 -> U+16128 */ 4813, + /* U+16122+1611F -> U+16127 */ 4812, + /* U+16129+1611F -> U+16124 */ 4809, + /* U+16D63+16D67 -> U+16D69 */ 4828, + /* U+16D67+16D67 -> U+16D68 */ 4827, + /* U+16D69+16D67 -> U+16D6A */ 4829 }; /* Perfect hash function for recomposition */ diff --git a/src/include/common/unicode_norm_table.h b/src/include/common/unicode_norm_table.h index 6c98313599641..724131046debc 100644 --- a/src/include/common/unicode_norm_table.h +++ b/src/include/common/unicode_norm_table.h @@ -3,7 +3,7 @@ * unicode_norm_table.h * Composition table used for Unicode normalization * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_norm_table.h @@ -36,7 +36,7 @@ typedef struct #define DECOMPOSITION_IS_COMPAT(x) (((x)->dec_size_flags & DECOMP_COMPAT) != 0) /* Table of Unicode codepoints and their decompositions */ -static const pg_unicode_decomposition UnicodeDecompMain[6843] = +static const pg_unicode_decomposition UnicodeDecompMain[6878] = { {0x00A0, 0, 1 | DECOMP_COMPAT | DECOMP_INLINE, 0x0020}, {0x00A8, 0, 2 | DECOMP_COMPAT, 0}, @@ -976,6 +976,33 @@ static const pg_unicode_decomposition UnicodeDecompMain[6843] = {0x1ACC, 230, 0, 0}, {0x1ACD, 230, 0, 0}, {0x1ACE, 230, 0, 0}, + {0x1ACF, 230, 0, 0}, + {0x1AD0, 230, 0, 0}, + {0x1AD1, 230, 0, 0}, + {0x1AD2, 230, 0, 0}, + {0x1AD3, 230, 0, 0}, + {0x1AD4, 230, 0, 0}, + {0x1AD5, 230, 0, 0}, + {0x1AD6, 230, 0, 0}, + {0x1AD7, 230, 0, 0}, + {0x1AD8, 230, 0, 0}, + {0x1AD9, 230, 0, 0}, + {0x1ADA, 230, 0, 0}, + {0x1ADB, 230, 0, 0}, + {0x1ADC, 230, 0, 0}, + {0x1ADD, 220, 0, 0}, + {0x1AE0, 230, 0, 0}, + {0x1AE1, 230, 0, 0}, + {0x1AE2, 230, 0, 0}, + {0x1AE3, 230, 0, 0}, + {0x1AE4, 230, 0, 0}, + {0x1AE5, 230, 0, 0}, + {0x1AE6, 220, 0, 0}, + {0x1AE7, 230, 0, 0}, + {0x1AE8, 230, 0, 0}, + {0x1AE9, 230, 0, 0}, + {0x1AEA, 230, 0, 0}, + {0x1AEB, 234, 0, 0}, {0x1B06, 0, 2, 885}, {0x1B08, 0, 2, 887}, {0x1B0A, 0, 2, 889}, @@ -3026,6 +3053,7 @@ static const pg_unicode_decomposition UnicodeDecompMain[6843] = {0xA6F0, 230, 0, 0}, {0xA6F1, 230, 0, 0}, {0xA770, 0, 1 | DECOMP_COMPAT | DECOMP_INLINE, 0xA76F}, + {0xA7F1, 0, 1 | DECOMP_COMPAT | DECOMP_INLINE, 0x0053}, {0xA7F2, 0, 1 | DECOMP_COMPAT | DECOMP_INLINE, 0x0043}, {0xA7F3, 0, 1 | DECOMP_COMPAT | DECOMP_INLINE, 0x0046}, {0xA7F4, 0, 1 | DECOMP_COMPAT | DECOMP_INLINE, 0x0051}, @@ -4714,6 +4742,8 @@ static const pg_unicode_decomposition UnicodeDecompMain[6843] = {0x10D6D, 230, 0, 0}, {0x10EAB, 230, 0, 0}, {0x10EAC, 230, 0, 0}, + {0x10EFA, 220, 0, 0}, + {0x10EFB, 220, 0, 0}, {0x10EFD, 220, 0, 0}, {0x10EFE, 220, 0, 0}, {0x10EFF, 220, 0, 0}, @@ -6038,6 +6068,11 @@ static const pg_unicode_decomposition UnicodeDecompMain[6843] = {0x1E4EF, 230, 0, 0}, {0x1E5EE, 230, 0, 0}, {0x1E5EF, 220, 0, 0}, + {0x1E6E3, 230, 0, 0}, + {0x1E6E6, 230, 0, 0}, + {0x1E6EE, 230, 0, 0}, + {0x1E6EF, 230, 0, 0}, + {0x1E6F5, 230, 0, 0}, {0x1E8D0, 220, 0, 0}, {0x1E8D1, 220, 0, 0}, {0x1E8D2, 220, 0, 0}, diff --git a/src/include/common/unicode_normprops_table.h b/src/include/common/unicode_normprops_table.h index f1072c9f09966..c389aad290765 100644 --- a/src/include/common/unicode_normprops_table.h +++ b/src/include/common/unicode_normprops_table.h @@ -3147,6 +3147,7 @@ static const pg_unicode_normprops UnicodeNormProps_NFKC_QC[] = { {0xA69C, UNICODE_NORM_QC_NO}, {0xA69D, UNICODE_NORM_QC_NO}, {0xA770, UNICODE_NORM_QC_NO}, + {0xA7F1, UNICODE_NORM_QC_NO}, {0xA7F2, UNICODE_NORM_QC_NO}, {0xA7F3, UNICODE_NORM_QC_NO}, {0xA7F4, UNICODE_NORM_QC_NO}, @@ -6724,70 +6725,144 @@ static const pg_unicode_normprops UnicodeNormProps_NFKC_QC[] = { static int NFKC_QC_hash_func(const void *key) { - static const int16 h[10193] = { - 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, - 1886, 1887, 1888, 32767, 32767, 1889, 3141, -7211, - 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, - -1320, 1901, 1902, -1322, 1904, 1905, 1921, 1921, - 1921, 1909, 1910, 1911, 1924, 1924, 1914, 1915, - 1916, 1917, 1918, 1919, 1920, 1921, 1922, 1923, - 1924, 1925, -1976, 4681, 1323, 1929, 1930, 4682, - 1932, 1933, 1934, 1935, 1936, 1937, 0, 0, + static const int16 h[10195] = { + 3142, -807, -807, -807, -807, -807, -807, 32767, + 3460, 3461, 3143, 3463, 32767, 3144, 1109, 3465, + 3466, 3467, 3468, 3469, -2468, -2468, 32767, -2469, + -2469, -2469, -2469, -347, -2469, -2469, 32767, -2470, + -2470, -2470, 3153, -2470, -2470, -617, -617, -1404, + -1404, -471, -470, -469, -468, -467, -466, -465, + -464, -463, -462, -461, -460, -459, -458, -457, + -456, -455, -454, 358, -453, 463, -451, -450, + 358, -449, -448, -447, -446, 0, 358, -444, + 358, 358, 358, -443, -1622, -2437, -2437, -2437, + -2437, -2437, 358, -2438, -435, -434, -433, -432, + -431, -430, -429, -428, -427, -426, -425, 358, + -2426, -2426, -2426, -2426, -2426, -4539, -3246, -416, + -415, -414, -2421, -4543, -2420, -2420, -2420, -2420, + -2420, -2420, 3555, 3556, 3557, -2423, -2423, -2423, + -2423, -2423, -2423, -2423, -2423, -2423, -2423, -2423, + -2423, -2423, -2423, -5599, 1496, 1496, 976, 977, + -5597, -5597, -5597, -5597, -5597, 2776, 2776, -2886, + -5597, -5597, -5597, -5597, -1411, -1411, -1411, -2466, + -1411, -1411, -1411, 3506, 3507, 3508, 4742, -205, + 0, -205, 3513, -205, -205, -205, 6575, -1411, + 1, -1411, 2, -1411, -1411, -1411, -1411, 3, + -1411, -1411, 4, 5, 6, 7, -484, -1411, + 8, 9, 10, -1411, 11, 12, 13, -1411, + -1411, -1411, -1284, 359, 359, -1283, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 1974, + 359, 359, 359, 359, 359, 2842, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 359, 359, 359, 359, + 359, 359, 359, 359, 0, 0, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 422, 510, 422, 422, - 0, 0, 0, 0, 0, 0, 0, 1822, - 0, 431, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 14, 15, 0, 0, 0, + 0, 0, 32767, 2707, 0, 0, 0, 32767, + 16, 17, 32767, 2711, 2712, 2713, 32767, 2714, + 32767, 2715, 18, 2717, 2718, 2719, 2720, 2721, + 4741, 2723, 2724, 2725, 2726, 2727, 2728, 2729, + 2730, 2731, 2732, 2733, 2734, 2735, 2736, 2737, + 2738, 2739, 2740, 2741, 2742, 2743, 2744, 2745, + 2746, 2747, 2748, 2749, 2750, 2751, 2752, 2753, + 2754, 2755, 2756, 2757, 2758, 2759, 2760, 2761, + 2762, -2601, 2764, 2765, 2766, 2767, 2768, 2769, + 19, 2771, 2772, 2773, 512, 512, 512, 512, + 512, 2779, 512, 512, 512, 512, 512, 512, + 512, 2787, 2788, 512, 2790, 512, 2792, 512, + 512, 2795, 2796, 2797, 3997, 3997, 3997, 3997, + 3997, 3997, 3997, 3997, 3997, 3997, 3997, 3997, + 3666, 1682, 1683, 1222, 1684, 1685, 1686, 1222, + 1687, 1688, 1689, 1690, 1691, 1692, 1693, 1222, + 1694, 1695, 1696, 1697, 1698, 2831, 1223, 1223, + 2834, 2835, 1703, 2837, 0, 1708, 1709, 22, + 23, 24, 25, 26, 27, 28, 1714, 1715, + 1716, 1717, 1718, 1719, 3954, 1721, 1722, 1723, + 1724, 1725, 0, 4056, 0, 4057, 4057, 4057, + 4057, 4057, 4057, 4057, 0, 4058, 4058, 4058, + 4058, 4058, 4058, 4058, 4058, 1748, 1749, 1750, + 11099, 11100, 11101, 11102, 11103, 1756, 1757, 5022, + 1759, 1760, 1761, 1762, 5023, 6395, 4421, 6052, + 1233, 5143, 1233, 2281, 2282, 2283, 6667, 6668, + 6669, 5024, 6671, 6672, 6673, 6674, 1233, 2909, + 2909, 1233, 1233, 1233, 1233, 4676, 3599, 1233, + 1233, 1233, 1233, 1233, 1233, 4684, 4685, 4686, + 4687, 4688, 4689, 2288, 2289, 2290, 4693, 2291, + 2292, 4696, 4697, 4698, 2293, 4700, 4701, 4702, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1840, 1840, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 3814, 3814, 4799, 3814, 4724, 4725, 3816, + 2798, 4728, 4729, 4730, 4731, 2794, 4733, 4734, + 4735, 4736, 2790, 4738, 4739, 4740, 4741, 4742, + 4743, 4744, 4745, 4746, 4747, 4748, 4749, 6909, + 4751, 4752, 4753, 677, 4755, 4756, 4757, 4758, + 4759, 4790, 4761, 4791, 4792, 4793, 4794, 4795, + 4767, 4768, 4769, 4770, 4771, 4772, 4773, 7972, + 4775, 4776, 4777, 4778, 4779, 4780, 4781, 4782, + 4783, 4784, 0, 0, 4787, 4788, 4789, 4790, + 4791, 3787, 4793, 4794, 4795, 4796, 4797, 4798, + 5380, 4800, 4801, 2582, 4803, 4804, 4805, 4806, + 2583, 4808, 4809, 4810, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 2010, 2011, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32, 33, 34, 35, 36, 37, + 38, 39, 40, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 678, 679, 680, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 41, 42, 43, 44, 45, 46, + 32767, 32767, 47, 48, 49, 50, 51, 4137, + -6157, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 52, 53, 54, 55, 56, + 32767, 57, 58, 59, 60, 61, 62, 63, + 32767, 32767, 64, 32767, 65, 32767, 66, 67, + 32767, 32767, 32767, 32767, 32767, 32767, -3417, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, -3416, -3084, + -1099, -1099, -637, -1098, 32767, 32767, 32767, 32767, + -1101, -1101, 32767, -1102, -1102, 32767, 32767, 32767, + 32767, 32767, 32767, -1107, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 592, -1115, -1115, 573, 573, + 573, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 0, 0, 0, 0, + 32767, 0, 0, 0, 0, 0, 0, 593, + 0, 0, 0, 0, 0, -1259, 0, 0, + 0, -1807, 0, 0, 0, 0, 0, -1806, + -1805, 0, -1804, 0, 32767, 0, 0, 32767, + 0, 32767, 32767, 0, 32767, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 32767, + 0, 0, 0, 0, 32767, 0, 32767, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, + 32767, 32767, 32767, 0, 32767, 0, 32767, 0, + 32767, 0, 0, 0, 32767, 0, 0, 32767, + 0, 32767, 32767, 0, 32767, 0, 32767, 0, + 32767, 0, 32767, 0, 32767, 0, 0, 32767, + 0, 32767, 32767, 0, 0, 0, -4383, -4383, + -4383, -2737, 0, 0, 0, 0, 0, 32767, + 0, 0, 0, 0, 32767, 0, 0, 0, + 0, 32767, 0, 32767, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 0, 0, 0, + 32767, 0, 0, 0, 0, 0, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2762, 2762, 2762, - 2762, 2762, 2762, 2762, 2762, 2762, 2762, 2762, - 2762, 2762, 2762, 2762, 2762, 2762, 2762, 2762, - 2762, 2762, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 2812, 2812, 2812, 2812, - 2812, 2812, 2812, 2812, 2812, 2812, 2812, 2812, - 2812, 2812, -1449, 5319, 2812, 2812, 2812, 2812, - 2812, 2812, 2812, 2812, 2812, 2812, -534, 0, - 0, 0, 0, 348, 32767, 349, 0, 0, - 0, 0, 0, 0, 32767, 32767, 0, 0, - 0, -1517, 0, -1517, 32767, -1516, 4370, 4371, - 4372, 4373, 4374, 4375, 4376, 4377, 4378, 4379, - 0, 32767, 32767, -23190,4110, 0, 0, 0, - 0, 0, 0, 32767, 0, 0, 0, 0, - -622, -622, 0, -3928, -1253, -1252, -5473, -3927, - -3927, -3927, -3927, -3927, -3927, -3927, -3927, -3927, - -3927, -3927, -3927, -3927, -3927, -3580, -3580, -3925, - -3580, -3924, -3580, -3580, -3922, -3922, -3922, -3922, - -3922, -3922, -3922, -3922, 0, 0, -3484, -3919, - -3482, -3918, -3480, -3578, -3916, -3916, -3578, -3578, - -3578, -3913, -3913, -3913, -3913, -3913, -3913, -3913, - -3913, -3913, -3913, -3913, -3913, -3913, -3913, -3913, - -3913, -3913, -3913, -3913, -3913, -3913, -3913, -3913, - -3913, -3913, -3913, -3913, -3913, -3913, -3913, -3913, - -3913, -3913, -3913, -3913, -3913, -3578, 625, -5288, - 627, 628, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 2496, 2497, 2498, 2499, 2500, - 2501, 2502, 2503, 2504, 2505, 2506, 2507, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, -1769, -1768, -1767, -3528, 32767, 32767, + 0, 0, 0, 0, 4405, 4406, 4407, 2139, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, -4117, -4459, -4459, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -6795,595 +6870,549 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 3146, 32767, 32767, + 32767, 32767, 32767, 3465, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -2719, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, -4059, -4059, -4059, -4059, 895, + 895, 895, -4059, -4059, 897, -4059, -4059, -4059, + -4059, -4059, -4059, -4059, -4059, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 32767, 1051, 1052, 1053, 1054, 1055, + 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, + 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, + 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, + 1080, 1081, 1082, 1083, 1084, 1085, 1086, 0, + 0, 0, 0, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 9750, 9751, 0, 1381, 1382, 1383, + 3694, 3694, 3694, -5654, -5654, -5654, -5654, -5654, + 3694, 3694, 430, 3694, 3694, 3694, 3694, 434, + -937, 1038, -592, 4228, 319, 4230, 3183, 3183, + 3183, -1200, -1200, -1200, 446, -1200, -1200, -1200, + -1200, 4242, 2567, 2568, 4245, 4246, 4247, 4248, + 806, 1884, 4251, 4252, 4253, 4254, 4255, 4256, + 806, 806, 806, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -2828, -975, -975, -1762, -1762, + -829, -828, -827, -826, -825, -824, -823, -822, + -821, -820, -819, -818, -817, -816, -815, -814, + -813, -812, 0, -811, 105, -809, -808, 0, + -807, -806, -805, -804, -358, 0, -802, 0, + 0, 0, -801, -1980, -2795, -2795, -2795, -2795, + -2795, 0, -2796, -793, -792, -791, -790, -789, + -788, -787, -786, -785, -784, -783, 0, -2784, + -2784, -2784, -2784, -2784, -4897, -3604, -774, -773, + -772, -2779, -4901, -2778, -2778, -2778, -2778, -2778, + -2778, 3197, 3198, 3199, -2781, -2781, -2781, -2781, + -2781, -2781, -2781, 32767, 32767, 1525, -698, 1527, + 1528, 32767, 32767, 32767, -2757, -2756, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, + 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, + 32767, 0, 32767, 32767, 0, 32767, 0, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32, 33, 34, 35, 36, 37, - 38, 39, 40, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -6479, -6478, -6477, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 41, 42, 43, 44, 45, 46, - 32767, 32767, 47, 48, 49, 50, 51, 32767, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 52, 1131, 8054, 3425, -19247, - 8047, -19248,8049, -19249,8051, -14620,8053, -19240, - 8056, 3330, 8058, 8059, 3430, -19242,3330, 3330, - 3330, 3330, 3330, -11231,8049, 3330, 3330, 3330, - 3330, -3326, 33, -3326, 3330, -3325, -3325, -3325, - 3330, -3326, -3326, 1075, 4681, 4682, 8076, -3327, - 8079, 19509, -7787, 4688, 4689, 1331, 4691, -1964, - 4692, 4693, 4694, 4695, 4696, 4697, 297, -3308, - -3308, -6701, 4703, -6702, -18131,9166, -3308, 4711, - -2199, 4724, 95, -22577,4717, -22578,4719, -22579, - 4721, -17950,4723, -22570,4726, 0, 4728, 4729, - 100, -22572,0, 0, 0, 0, 0, -14561, - 4719, 0, 0, 0, 0, -6656, -3297, -6656, - 0, -6655, -6655, -6655, 0, -6656, -6656, -2255, - 1351, 1352, 4746, -6657, 4749, 16179, -11117,1358, - 1359, -1999, 0, 0, 0, 938, 0, 0, - 0, 0, 0, 0, 1255, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 4081, 4081, 4081, - 4081, 4081, 2312, 2313, 2314, 553, 0, 0, - 0, 0, 0, 0, 0, 0, 3580, 2958, - 2958, 3580, -348, 2327, 2328, -1893, -347, -347, - -347, -347, -347, -347, -347, -347, -347, -347, - -347, -347, -347, -347, 0, 0, -345, 0, - -344, 0, 0, -342, -342, -342, -342, -342, - -342, -342, -342, -342, -342, 94, -341, 96, - -340, 98, 0, -338, -338, 0, 0, 0, - -335, -335, -335, -335, -335, -335, -335, -335, - -335, -335, -335, -335, -335, -335, -335, -335, - -335, -335, -335, -335, -335, -335, -335, -335, - -335, -335, -335, -335, -335, -335, -335, -335, - -335, -335, -335, -335, 0, 4203, -1710, 32767, + 32767, 106, 107, 108, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 109, 110, 111, 112, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 1, 32767, 2, 32767, 32767, 32767, 32767, -2759, - 32767, 32767, -2758, -2757, -2756, -2755, 32767, 32767, - -2754, -2753, -2752, 32767, -2751, -2750, -2749, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, -1362, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, -3508, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 682, - 683, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 2456, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 687, 688, 689, 690, + 691, 692, 693, 694, 695, 696, 697, 698, + 699, 700, 701, 702, 703, 704, 705, 706, + 707, 708, 709, 710, 711, 712, 713, 714, + 715, 716, 717, 718, 719, 720, 721, 722, + 723, 724, -921, 726, 727, 728, 729, 730, + 731, -1051, 733, 734, 735, 736, 737, 738, + 739, 740, 741, 742, 743, 744, 745, 746, + 747, 748, 749, 750, 751, 752, 753, 754, + 755, 756, 757, 758, 759, 760, 761, 762, + 763, 764, 765, 766, 767, 768, 769, 770, + 771, 772, 773, 774, 775, 776, 777, 778, + 779, 780, 781, 782, 783, 784, 785, 786, + 787, 788, 789, 790, 791, 792, 793, 794, + 795, 796, 797, 798, 799, 800, 801, 802, + 803, 804, 805, 806, 807, 529, 530, -6249, + 1738, 327, 1740, 328, 1742, 1743, 1744, 1745, + 332, 1747, 1748, 334, 334, 334, 334, 826, + 1754, 336, 336, 336, 1758, 337, 337, 337, + 1762, 1763, 1764, 3240, 3241, 3242, -519, -519, + 3245, 3246, 1412, 3248, 3249, 3250, 3251, 3252, + 3253, 3254, 3255, 1134, 3257, 3258, 1135, 3260, + 3261, 3262, -2360, 3264, 3265, 1413, 3267, 3268, + 3269, 868, 869, 870, 871, 872, 873, 874, + 875, 876, 877, 878, 879, 880, 881, 882, + 883, 884, 885, 886, 887, 888, 889, 890, + 891, 892, 893, 894, 895, 896, 897, 898, + 899, 900, 2875, 2875, 2875, 2875, 374, 2876, + 2876, 2876, 374, 2877, 2877, 2877, 2877, 2877, + 2877, 2877, 2877, 2877, 2877, 2877, 374, 2878, + 549, 550, 551, 552, 553, 554, 555, 556, + 557, 558, 559, 560, 561, 562, 563, 564, + 565, 566, 567, 568, 569, 901, 2886, 2886, + 3348, 2887, 2887, 2887, 3352, 2888, 2888, 2888, + 2888, 2888, 2888, 2888, 3360, 2889, 2889, 2889, + 2889, 2889, 1757, 3366, 3367, 1757, 1757, 2890, + 1757, 4595, 2888, 2888, 4576, 4576, 4576, 4576, + 4576, 4576, 4576, 2891, 2891, 2891, 2891, 2891, + 2891, 657, 2891, 2891, 2891, 2891, 2891, 4617, + 562, 4619, 563, 564, 565, 566, 567, 568, + 569, 4627, 570, 571, 572, 573, 574, 575, + 576, 577, 2888, 2888, 2888, -6460, -6460, -6460, + -6460, -6460, 2888, 2888, -376, 2888, 2888, 2888, + 2888, -372, -1743, 232, -1398, 3422, -487, 3424, + 2377, 2377, 2377, -2006, -2006, -2006, -360, -2006, + -2006, -2006, -2006, 3436, 1761, 1762, 3439, 3440, + 3441, 3442, 0, 1078, 3445, 3446, 3447, 3448, + 3449, 3450, 0, 0, 0, 0, 0, 0, + 2402, 2402, 2402, 0, 2403, 2403, 0, 0, + 0, 2406, 0, 0, 0, 4703, 4704, 4705, + 4706, 4707, 4708, 4709, 4710, 4711, 4712, 4713, + 4714, 4715, 4716, 4717, 4718, 4719, 906, 907, + -77, 909, 0, 0, 910, 1929, 0, 0, + 0, 0, 1938, 0, 0, 0, 0, 1947, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -2159, 0, 0, 0, + 4077, 0, 0, 0, 0, 0, -30, 0, + -29, -29, -29, -29, -29, 0, 0, 0, + 0, 0, 0, 0, -3198, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 4785, + 4786, 0, 0, 0, 0, 0, 1005, 0, + 0, 0, 0, 0, 0, -581, 0, 0, + 2220, 0, 0, 0, 0, 2224, 0, 0, + 0, 0, 1022, 4286, 4286, 4286, 4286, 1026, + -345, 1630, 0, 4820, 911, 4822, 3775, 3775, + 3775, -608, -608, -608, 1038, -608, -608, -608, + -608, 4834, 3159, 3160, 4837, 4838, 4839, 4840, + 1398, 2476, 4843, 4844, 4845, 4846, 4847, 4848, + 1398, 1398, 1398, 592, 592, 592, 592, 592, + 592, 592, 592, 4860, 4861, 4543, 4863, 4864, + 4865, 2511, 4867, 4868, 4869, 4870, 4871, -1066, + -1066, -1066, -1066, -1066, -1066, -1066, 1056, -1066, + -1066, 1058, -1066, -1066, -1066, 4557, -1066, -1066, + 787, 787, 0, 0, 933, 934, 935, 936, + 937, 938, 939, 940, 941, 942, 943, 944, + 945, 946, 947, 948, 949, 950, 4911, 952, + 1868, 954, 955, 4916, 957, 958, 959, 960, + 1406, 4922, 963, 4924, 4925, 4926, 967, -212, + -1027, -1027, -1027, -1027, -1027, -1027, -1027, 976, + 977, 978, 979, 980, 981, 982, 983, 984, + 985, 986, 1769, -1015, -1015, -1015, -1015, -1015, + -3128, -1835, 995, 996, 997, -1010, -3132, -1009, + -1009, -1009, -1009, -1009, -1009, 4966, 4967, 4968, + -1012, -1012, -1012, -1012, -1012, -1012, -1012, -1012, + -1012, -1012, -1012, -1012, -1012, -1012, -4188, 2907, + 2907, 2387, 2388, -4186, -4186, -4186, -4186, -4186, + 4187, 4187, -1475, -4186, -4186, -4186, -4186, 0, + 0, 0, -1055, 0, 0, 0, 4917, 4918, + 4919, 6153, 1206, 1411, 1206, 4924, 1206, 1206, + 1206, 7986, 0, 1412, 0, 1413, 0, 0, + 0, 0, 1414, 0, 0, 1415, 1416, 1417, + 1418, 927, 0, 1419, 1420, 1421, 0, 1422, + 1423, 1424, 0, 0, 0, 127, 1770, 1770, + 128, 1770, 1770, 1770, 1770, 1770, 1770, 1770, + 1770, 1770, 1770, 1770, 1770, 1770, 1770, 1770, + 1770, 1770, 32767, 5067, 5068, 5069, 5070, 5071, + 5072, 5073, 5074, 0, 5076, 5077, 5078, 5079, + 5080, 5081, 5082, 129, 130, 131, 5086, 5087, + 132, 5089, 5090, 5091, 5092, 5093, 5094, 5095, + 5096, 1038, 1039, 1040, 1041, 1042, 1043, 1044, + 1045, 1046, 1047, 1048, 1049, 1050, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1087, 1088, 1089, 1090, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 1091, + 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, + 1100, 1101, 1102, 1103, 1104, 1105, -8644, -8644, + 1108, -272, -272, -272, -2582, -2581, -2580, 6769, + 6770, 6771, 6772, 6773, -2574, -2573, 692, -2571, + -2570, -2569, -2568, 693, 2065, 91, 1722, -3097, + 813, -3097, -2049, -2048, -2047, 2337, 32767, 2338, + 693, 2340, 2341, 2342, 2343, -3098, -1422, -1422, + -3098, -3098, -3098, -3098, 345, -732, -3098, -3098, + -3098, -3098, -3098, -3098, 353, 354, 355, 1162, + 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, + 1171, 1172, 1173, 1174, 1175, 1176, 1177, 4006, + 2154, 2155, 2943, 2944, 2012, 2012, 2012, 2012, + 2012, 2012, 2012, 2012, 2012, 2012, 2012, 2012, + 2012, 2012, 2012, 2012, 2012, 2012, 1201, 2013, + 1098, 2013, 2013, 1206, 2014, 2014, 2014, 2014, + 1569, 1212, 2015, 1214, 1215, 1216, 2018, 3198, + 4014, 4015, 4016, 4017, 4018, 1224, 4021, 2019, + 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, + 2019, 2019, 1237, 4022, 4023, 4024, 4025, 4026, + 6140, 4848, 2019, 2019, 2019, 4027, 6150, 4028, + 4029, 4030, 4031, 4032, 4033, -1941, -1941, -1941, + 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, + 4048, 4049, 4050, 4051, 4052, 4053, 7230, 136, + 137, 658, 658, 7233, 7234, 7235, 7236, 7237, + -1135, -1134, 4529, 7241, 7242, 7243, 7244, 3059, + 3060, 3061, 4117, 3063, 3064, 3065, -1851, -1851, + -1851, -3084, 1864, 1660, 1866, -1851, 1868, 1869, + 1870, -4909, 3078, 1667, 3080, 1668, 3082, 3083, + 3084, 3085, 1672, 3087, 3088, 1674, 1674, 1674, + 1674, 2166, 3094, 1676, 1676, 1676, 3098, 1677, + 1677, 1677, 3102, 3103, 3104, 2978, 1336, 1337, + 2980, 1339, 1340, 1341, 1342, 1343, 1344, 1345, + 1346, 1347, 1348, 1349, 1350, 1351, 1352, 1353, + 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, + 1362, 1363, 1364, 1365, 1366, 1367, 1368, 1369, + 1370, 1371, 1372, 1373, 1374, 1375, 1376, 1377, + 1378, 1379, 1380, 1381, 1382, 1383, 1384, 1385, + 1386, 1387, 1388, 1389, 1390, 1391, 1392, 1393, + 1394, 1395, -219, 1397, 1398, 1399, 1400, 1401, + -1081, 1403, 1404, 1405, 1406, 1407, 1408, 1409, + 1410, 1411, 1412, 1413, 1414, 1415, 1416, 1417, + 1418, 1419, 1420, 1421, 1422, 1423, 1424, 1425, + 1426, 1427, 1428, 1429, 1430, 1431, 1432, 1433, + 1434, 1435, 1436, 1437, 1438, 1439, 1440, 1441, + 1442, 1443, 1444, 1445, 1446, 1447, 1448, 1449, + 1450, 1451, 1452, 1453, 1454, 1455, 1456, 1457, + 1458, 1459, 1460, 1461, 1462, 1463, 1464, 1465, + 1466, 1467, 1468, 1469, 1470, 1471, 1472, 1473, + 1474, 1475, 1476, 1477, 1478, 1479, 1480, 1481, + 1482, 1483, 1484, 1485, 1486, 1487, 1488, 1489, + 1490, 1491, 1492, 1493, 1494, 1495, 1496, 1497, + 1498, 1499, 1500, 1501, 1502, 1503, 1504, 1505, + 1506, 1507, 1508, 1509, 1510, 1511, 1512, 1513, + 1514, 1515, 1516, 1517, 1518, 1519, 1520, 1521, + -5554, -5554, -5554, -5554, -5554, -5554, -5554, -5554, + -5554, -5554, -5554, -5554, 4741, 4742, -5554, -5554, + -5554, -5554, -5554, -5554, -5554, 4598, 32767, 32767, + -5556, 4602, 4603, 4604, 4605, 4606, 4607, -1955, + -1955, -1955, 1435, 1436, 1437, 1438, 1439, 1440, + 4617, -2477, -2476, 4620, 4621, 4622, 4623, 4624, + 4625, 4626, -3746, -3745, 1918, 4630, 4631, 4632, + 4633, 448, 449, 450, 1506, 452, 453, 454, + -4462, -4462, -4462, -5695, -747, -951, -745, -4462, + -743, -742, -741, -7520, 467, -944, 469, -943, + 471, 472, 473, 474, -939, 476, 477, -937, + -937, -937, -937, -445, 483, -935, -935, -935, + 487, -934, -934, -934, 491, 492, 493, 1969, + 1970, 1971, -1790, -1790, 1974, 1975, 141, 1977, + 1978, 1979, 1980, 1981, 1982, 1983, 1984, -137, + 1986, 1987, -136, 1989, 1990, 1991, -3631, 1993, + 1994, 142, 143, 931, 932, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -3960, + 0, -915, 0, 0, -3960, 0, 0, 0, + 0, -445, -3960, 0, -3960, -3960, -3960, 0, + 1180, 1996, 1997, 1998, 1999, 2000, 2001, 2002, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -782, 2003, 2004, 2005, 2006, + 2007, 4121, 2829, 0, 0, 0, 2008, 4131, + 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, + 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, + 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, + 32767, 2033, 2034, 2035, 2036, 2037, 32767, 2038, + 32767, 2039, 2040, 144, 2041, 2042, 32767, 2043, + 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, + 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, + 2060, 2061, 2062, 2063, 2064, 2065, 1118, 2067, + 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, + 2076, 2077, 2078, 2079, 2080, 2081, 2082, 2083, + 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, + 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, + 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107, + 2108, 2109, 2110, 2111, 2112, 2113, 2114, 2115, + 2116, 2117, 2118, 2119, 2120, 2121, 2122, 2123, + 2124, 2125, 2126, -4915, -4915, -4915, -4915, -4915, + -4915, -4915, -4915, -4915, -4915, -4915, -4915, -4915, + -4915, -4915, 2142, 2143, -4913, -4913, -4913, -4913, + -4913, -4913, -4913, 1976, 1049, 2468, 2469, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + -435, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 146, + 32767, 32767, 32767, 32767, -8001, -8001, -5292, 2154, + -8003, -8003, -8003, -8003, -8003, -8003, 2161, 2162, + 2163, 2164, 2165, 2166, 2167, 2168, 2169, 2170, + 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, + 2179, 2180, 2181, 2182, 2183, 2184, 2185, 2186, + 2187, 2188, 2189, 2190, 2191, 2192, 2193, 2194, + 2195, 32767, 2196, 2197, 2198, 2199, 2200, 2201, + 2202, 2203, -494, 2205, 2206, 2207, 2208, 2209, + 4229, 2211, 2212, 2213, 2214, 2215, 2216, 2217, + 2218, 2219, 2220, 2221, 2222, 2223, 2224, 2225, + 2226, 2227, 2228, 2229, 2230, 2231, 2232, 2233, + 2234, 2235, 2236, 2237, 2238, 2239, 2240, 2241, + 2242, 2243, 2244, 2245, 2246, 2247, 2248, 2249, + 2250, -3113, 2252, 2253, 2254, 2255, 2256, 2257, + -493, 2259, 2260, 2261, 0, 0, 0, 0, + 0, 2267, 0, 0, 0, 0, 0, 0, + 0, 2275, 2276, 0, 2278, 0, 2280, 0, + 0, 2283, 2284, 2285, 3485, 3485, 3485, 3485, + 3485, 3485, 3485, 3485, 3485, 3485, 3485, 3485, + 3154, 1170, 1171, 710, 1172, 1173, 1174, 710, + 1175, 1176, 1177, 1178, 1179, 1180, 1181, 710, + 1182, 1183, 1184, 1185, 1186, 2319, 711, 711, + 2322, 2323, 1191, 2325, -512, 1196, 1197, -490, + -489, -488, -487, -486, -485, -484, 1202, 1203, + 2338, 2339, 2340, 2167, 2342, 2384, 2385, 2386, + 1895, 968, 2387, 2388, 2389, 968, 2390, 2391, + 2392, 968, 968, 968, -507, -507, -507, 3255, + 3256, -507, -507, 1328, -507, -507, -507, -507, + -507, -507, -507, -507, 1615, -507, -507, 1617, + -507, -507, -507, 5116, -507, -507, 1346, -507, + -507, -507, 1895, 1895, 1895, 1895, 1895, 1895, + 1895, 1895, 1895, 1895, 1895, 1895, 1895, 1895, + 1895, 1895, 1895, 1895, 1895, 1895, 1895, 1895, + 1895, 1895, 1895, -507, 1894, 1894, 1894, 1894, + 1894, 1894, 1894, -80, -79, -78, -77, 2425, + -76, -75, -74, 2429, -73, -72, -71, -70, + -69, -68, -67, -66, -65, -64, -63, 2441, + -62, 2268, 2268, 2268, 2268, 2268, 2268, 2268, + 2268, 2268, 32767, 8912, 8913, 8914, -433, -432, + 2833, -430, -429, -428, -427, 2834, 4206, 2232, + 3863, -956, 2954, -956, 92, 93, 94, 4478, + 4479, 4480, 2835, 4482, 4483, 4484, 4485, -956, + 720, 720, -956, -956, -956, -956, 2487, 1410, + -956, -956, -956, -956, -956, -956, 2495, 2496, + 2497, 2498, 2499, 2500, 99, 100, 101, 2504, + 102, 103, 2507, 2508, 2509, 104, 2511, 2512, + 2513, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 154, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 1607, 2592, 1607, 2517, 2518, + 1609, 591, 2521, 2522, 2523, 2524, 587, 2526, + 2527, 2528, 2529, 583, 2531, 2532, 2533, 2534, + 2535, 2536, 2537, 2538, 2539, 2540, 2541, 2542, + 4702, 2544, 2545, 2546, -1530, 2548, 2549, 2550, + 2551, 2552, 2583, 2554, 2584, 2585, 2586, 2587, + 2588, 2560, 2561, 2562, 2563, 2564, 2565, 2566, + 5765, 2568, 2569, 2570, 2571, 2572, 2573, 2574, + 2575, 2576, 2577, 32767, 32767, 2578, 2579, 2580, + 2581, 2582, 1578, 2584, 2585, 2586, 2587, 2588, + 2589, 3171, 2591, 2592, 373, 2594, 2595, 2596, + 2597, 374, 2599, 2600, 2601, 2602, 1581, -1682, + -1681, -1680, -1679, 1582, 2954, 980, 2611, -2208, + 1702, -2208, -1160, -1159, -1158, 3226, 3227, 3228, + 1583, 3230, 3231, 3232, 3233, -2208, -532, -532, + -2208, -2208, -2208, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 4205, 4206, 4207, - 4208, 4209, 4210, 4211, 4212, 4213, 4214, 4215, - 4216, -4880, 4218, 5470, -4882, 4221, 4222, 4223, - 4224, 4225, 4226, 4227, 4228, 1009, 4230, 4231, - 1007, 4233, 4234, 4250, 4250, 4250, 4238, 4239, - 4240, 4253, 4253, 4243, 4244, 4245, 4246, 4247, - 4248, 4249, 4250, 4251, 4252, 4253, 4254, 353, - 7010, 3652, 4258, 4259, 7011, 4261, 4262, 4263, - 4264, 4265, 4266, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 105, 32767, 32767, 32767, + 32767, 32767, 32767, 3698, 3699, 1576, 3701, 3702, + 3703, -1919, 3705, 3706, 1854, 1855, 2643, 2644, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2286, 2287, 2288, 2289, + 2290, 2291, 2292, 2293, 2294, 2295, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 2296, 2297, -185, 2299, + 2300, 2301, 2302, 2303, 2304, 2305, 2306, 2307, + 2308, 2309, 2310, 2311, 2312, 2313, 2314, 2315, + 2316, 32767, 32767, 2317, 2318, 2319, 2320, 2321, + 2322, 2323, 2324, 2325, 2326, 2686, 2687, 32767, + 2688, 2689, 2690, 2691, 2692, 2693, 2694, 2695, + 2696, 2697, 2698, 2699, 2686, 2686, 2702, 2703, + 2704, 2705, 2706, 32767, 0, 2708, 2709, 2710, + 32767, 8148, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 2698, 0, 0, 0, 0, + 0, -2019, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5364, 0, 0, 0, 0, 0, + 0, 2751, 0, 0, 0, 2262, 2263, 2264, + 2265, 2266, 0, 2268, 2269, 2270, 2271, 2272, + 2273, 2274, 0, 0, 2277, 0, 2279, 0, + 2281, 2282, 0, 0, 0, -1199, -1198, -1197, + -1196, -1195, -1194, -1193, -1192, -1191, -1190, -1189, + -1188, -856, 1129, 1129, 1591, 1130, 1130, 1130, + 1595, 1131, 1131, 1131, 1131, 1131, 1131, 1131, + 1603, 1132, 1132, 1132, 1132, 1132, 0, 1609, + 1610, 0, 0, 1133, 0, 2838, 1131, 1131, + 2819, 2819, 2819, 2819, 2819, 2819, 2819, 1134, + 1134, 0, 0, 0, 174, 0, -41, -41, + -41, 451, 1379, -39, -39, -39, 1383, -38, + -38, -38, 1387, 1388, 1389, 2865, 2866, 2867, + -894, -894, 2870, 2871, 1037, 2873, 2874, 2875, + 2876, 2877, 2878, 2879, 2880, 759, 2882, 2883, + 760, 2885, 2886, 2887, -2735, 2889, 2890, 1038, + 2892, 2893, 2894, 493, 494, 495, 496, 497, + 498, 499, 500, 501, 502, 503, 504, 505, + 506, 507, 508, 509, 510, 511, 512, 513, + 514, 515, 516, 517, 2920, 520, 521, 522, + 523, 524, 525, 526, 2501, 2501, 2501, 2501, + 0, 2502, 2502, 2502, 0, 2503, 2503, 2503, + 2503, 2503, 2503, 2503, 2503, 2503, 2503, 2503, + 0, 2504, 175, 176, 177, 178, 179, 180, + 181, 182, 183, 184, 185, 186, 187, 188, + 189, 190, 191, 192, 193, 194, 195, 527, + 2512, 2512, 2974, 2513, 2513, 2513, 2978, 2514, + 2514, 2514, 2514, 2514, 2514, 2514, 2986, 2515, + 2515, 2515, 2515, 2515, 2515, 2993, 2994, 2995, + 2996, 2519, 2519, 2519, 2519, 2519, 3002, 3003, + 4209, 4209, 4209, 4209, 2525, 2525, 2525, 2525, + 2525, 2525, 2525, 291, 2525, 2525, 2525, 2525, + 2525, 196, 197, 198, 199, 200, 201, 202, + 203, 204, 205, 206, 207, 208, 209, 210, + 211, 212, 213, 214, 215, -6826, -6826, -6826, + -6826, -6826, -6826, -6826, 2522, 32767, 32767, 2520, + 2520, 2520, 2520, -740, -2111, 32767, 32767, 3052, + -857, 3054, 2007, 2007, 2007, 32767, 32767, -2378, + -732, -2378, 32767, 32767, 32767, 3061, 1386, 1387, + 3064, 3065, 3066, 3067, 32767, 702, 3069, 3070, + 3071, 3072, 3073, 3074, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 2834, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -2302, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - -2356, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 32767, 32767, 32767, 32767, 0, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, - 32767, -4130, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -48, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, + 0, 0, 32767, 0, 0, 0, 32767, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 32767, 0, -2329, 32767, 32767, -2328, + -2327, -2326, -2325, -2324, 32767, 32767, -2323, -2322, + -2321, 32767, -2320, 32767, -2319, 32767, -2318, 32767, + -2317, -1985, 0, 0, 32767, 0, 0, 0, + 32767, 0, 0, 0, 0, 0, 0, 0, + 32767, 0, 0, 0, 0, 0, 0, 32767, + 32767, 32767, 32767, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, + 0, 0, 0, 0, 0, -2234, 0, 0, + 0, 0, 0, -2329, -2328, -2327, -2326, -2325, + -2324, -2323, -2322, -2321, -2320, -2319, -2318, -2317, + -2316, -2315, -2314, -2313, -2312, -2311, 0, 0, + 0, -9348, -9348, -9348, -9348, -9348, 0, 0, + -3264, 0, 0, 0, 0, -3260, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, -4903, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 4268, 4269, 4270, 4271, 32767, + 4272, 4273, 4274, 4275, 4276, 4277, 3685, 4279, + 4280, 4281, 4282, 4283, 5543, 4285, 4286, 4287, + 6095, 4289, 4290, 4291, 4292, 4293, 6100, 6100, + 4296, 6101, 4298, 32767, 4299, 4300, 32767, 4301, + 32767, 32767, 4302, 32767, 4303, 4304, 4305, 4306, + 4307, 4308, 4309, 4310, 4311, 4312, 32767, 4313, + 4314, 4315, 4316, 32767, 4317, 32767, 4318, 32767, + 32767, 32767, 32767, 32767, 32767, 4319, 32767, 32767, + 32767, 32767, 4320, 32767, 4321, 32767, 4322, 32767, + 4323, 4324, 4325, 32767, 4326, 4327, 32767, 4328, + 32767, 32767, 4329, 32767, 4330, 32767, 4331, 32767, + 4332, 32767, 4333, 32767, 4334, 4335, 32767, 4336, + 32767, 32767, 4337, 4338, 4339, 8723, 32767, 8724, + 7079, 4343, 4344, 4345, 4346, 4347, 32767, 4348, + 4349, 4350, 4351, 32767, 4352, 4353, 4354, 4355, + 32767, 4356, 32767, 4357, 4358, 4359, 4360, 4361, + 4362, 4363, 4364, 4365, 4366, 32767, 4367, 4368, + 4369, 4370, 4371, 4372, 4373, 4374, 4375, 4376, + 4377, 4378, 4379, 4380, 4381, 4382, 4383, 32767, + 32767, 32767, 32767, 32767, 4384, 4385, 4386, 32767, + 4387, 4388, 4389, 4390, 4391, 32767, 4392, 4393, + 4394, 4395, 4396, 4397, 4398, 4399, 4400, 4401, + 4402, 4403, 4404, 0, 0, 0, 2269, 2269, + 2269, 2269, 2269, 2269, 2269, 2269, 32767, 32767, + 32767, 32767, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 32767, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 299, 32767, 300, 301, -2381, -2381, -2381, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -2388, 32767, - 32767, -4398, -4398, -4398, 32767, 32767, 32767, 32767, + 32767, 32767, 0, 0, 10120, 0, 0, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 6563, + 6564, 6565, 3176, 3176, 3176, 3176, 3176, 3176, + 0, 7095, 7095, 0, 0, 0, 0, 0, + 0, 0, 32767, 8372, 0, 0, 0, 0, + 0, 4186, 4186, 4186, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - -17740,32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 4825, 32767, 32767, 32767, 32767, -4293, - 32767, 32767, 32767, 109, -5804, 111, 112, 32767, - 32767, -4301, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -2438, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 685, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - -20069,32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 3994, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, -6790, -6790, - 157, -3219, -17570,-1719, -1719, -3027, -3027, -19414, - -3027, -9665, -3028, -3028, -3028, -3028, -3028, 4977, - 577, -3028, -3028, -6421, 4983, -6422, -17851,9446, - -3028, -3028, 331, -3028, 3628, -3027, -3027, -3027, - -3027, -3027, -3027, 1374, 4980, 4981, 8375, -3028, - 8378, 19808, -7488, 4987, -3031, 3880, -3042, 1588, - 24261, -3032, 24264, -3032, 24267, -3032, 19640, -3032, - 24262, -3033, 1694, -3033, -3033, 1597, 24270, 1699, - 1700, 1701, 1702, 1703, 16265, -3014, 1706, 1707, - 1708, 1709, 8366, 5008, 8368, 1713, 8369, 8370, - 8371, 1717, 8374, 8375, 3975, 370, 370, -3023, - 8381, -3024, -14453,12844, 370, 8389, 1479, 8402, - 3773, -18899,8395, -18900,8397, -18901,8399, -14272, - 8401, -18892,8404, 3678, 8406, 8407, 3778, -18894, - 3678, 3678, 3678, 3678, 3678, -10883,8397, 3678, - 3678, 3678, 3678, -2978, 381, -2978, 3678, -2977, - -2977, -2977, 3678, -2978, -2978, 1423, 5029, 5030, - 8424, -2979, 8427, 19857, -7439, 5036, 5037, 1679, - 5039, -1616, 5040, 5041, 5042, 5043, 5044, 5045, - 645, -2960, -2960, -6353, 5051, -6354, -17783,9514, - -2960, 5059, -1851, 5072, 443, -22229,5065, -22230, - 5067, -22231,5069, -17602,5071, -22222,5074, 348, - 5076, 5077, 448, -22224,348, 348, 348, 348, - 348, -14213,5067, 348, 348, 348, 348, -6308, - -2949, -6308, 348, -6307, -6307, -6307, 348, -6308, - -6308, -1907, 1699, 1700, 5094, -6309, 5097, 16527, - -10769,1706, 1707, -1651, 348, 348, 348, 1286, - 348, 348, 348, 348, 348, 348, 1603, 348, - 348, 348, 348, 348, 348, 348, 348, 348, - 348, 348, 348, 348, 348, 348, 348, 348, - 348, 348, 348, 348, 348, 348, 348, 4429, - 4429, 4429, 4429, 4429, 2660, 2661, 2662, 901, - 348, 348, 348, 348, 348, 348, 348, 348, - 3928, 3306, 3306, 3928, 0, 2675, 2676, 2677, - 2678, 0, 2680, 0, 0, 2683, 2684, 2685, - 2686, 4556, 2687, 2688, 2689, 8575, 2691, 2692, - -9266, -9266, -4636, 18037, -9256, 18040, -9256, 18043, - 18044, 13415, -9257, 18037, -9258, 18039, -9259, -9259, - -4629, 18044, 4581, 4582, 4583, -2184, -4527, 10035, - -9244, -2185, -4524, 4590, -4523, 2134, -1224, 2717, - 4595, 2719, 2720, 4598, 6107, 6107, 6107, 2725, - 2726, 2727, 4605, 2729, 2730, 2731, 4609, 2736, - 4611, 4612, 4613, 6118, 6118, 4616, 6119, 2743, - 4619, 4620, 2744, 2745, 6123, 2747, 6124, 4626, - 4627, 4628, 4629, 2753, 4631, 2755, 4633, 8896, - 8897, 8898, 4637, 4638, 4639, 4640, 0, 0, - 0, 0, 0, -1812, 0, 0, 0, 8925, - 0, 0, 1330, -3389, 0, 0, 0, 8940, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 10649, 8068, 0, 0, 16017, 8807, 8808, -1676, - 906, 8811, -7039, -7038, 173, 173, 10658, 8077, - 173, 16024, 16024, 8814, 8815, -1669, 913, 8818, - -7032, -7031, 180, 180, 10665, 8084, 180, 16031, - 16031, 8821, 8822, -1662, 920, 8825, 9014, -7023, - 188, 188, 10673, 8092, 188, 0, 16038, 8828, - 8829, -1655, 927, 8832, 4727, 4728, 4729, 4730, - 4305, 4732, 4733, 4734, 4735, 4736, 4737, 4738, - 4739, 4740, 4741, 4742, 4743, 4744, 4745, 4746, - 4747, 4748, 1540, 1540, 1540, 1540, 1540, 1540, - 1540, 1540, 1540, 1540, 1540, -1893, 1540, 1540, - 1540, 1540, 1540, 1540, 1540, 1540, 1540, 1540, - 1540, 1540, 1540, 1540, 1540, 1540, 1540, 1540, - 1540, 1540, 1540, 1540, 1540, 1540, 911, 1540, - 4614, 4615, 4616, 4617, 4618, 1540, 4619, 4620, - 4621, 4622, 4623, 4624, 4625, 4626, 4627, -7592, - 11688, 4630, 4631, 4632, 4633, 4634, 4635, 32767, - 4810, 4811, 4812, 4813, 3850, 3851, 4816, 4817, - 4818, 4819, 4820, 4821, 4822, 4823, 4824, 4825, - 4573, 4573, 4573, 4573, 4830, 4574, 4574, 4574, - 4834, 4575, 4575, 4575, 4575, 4575, 917, 918, - 4575, 4575, 4575, 4575, 923, 4576, 4576, 926, - 927, 928, 7709, 930, 931, 932, 933, 934, - 7717, 936, 7719, 938, 7722, 940, 941, 942, - 943, 944, 945, 946, 7733, 948, 949, 950, - 951, 952, 953, 954, 955, 956, 957, 958, - 959, 960, 961, 962, 963, 964, 965, 4765, - 967, 968, 969, 970, 971, 972, 973, 974, - 975, 976, 977, 978, 4902, 4903, -2043, 1334, - 15686, -164, -163, 1146, 1147, 17535, 1149, 7788, - 1152, 1153, 1154, 1155, 1156, -6848, -2447, 1159, - 1160, 4554, -6849, 4557, 15987, -11309,1166, 7797, - 7798, 7799, -5489, 1167, 1168, 1169, 1170, 1171, - 1172, -3228, -6833, -6833, -10226,1178, -10227,-21656, - 7812, 7813, 7814, 7815, 7816, 4951, 7817, 7818, - 4954, 4955, 4956, 4957, 4958, 4959, 4960, 4961, - 4962, 7837, 4964, 4965, 4966, 4967, 4968, 4969, - 4970, 4971, 4972, 4973, 4974, 4975, 4976, 4977, - 4978, 4979, 4980, 4981, 4982, 4983, 4984, 4985, - 4986, 4987, 4988, 4989, 4990, 4991, 4992, 4993, - 4994, 4995, 4996, 4997, 4998, 4999, 5000, 5001, - 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, - 5010, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, -224, -224, -224, -6880, -3521, -6880, -224, - -6879, -6879, -6879, -6879, -6879, -6879, -2478, 1128, - 1129, 4523, -6880, 4526, 15956, -11340,1135, -6883, - 28, -6894, -2264, 20409, -6884, 20412, -6884, 20415, - 20416, 15787, -6885, 20409, -6886, -2159, -6886, -6886, - -2256, 20417, -2154, -2153, -2152, -2151, -2150, 12412, - -6867, -2147, -2146, -2145, -2144, 4513, 1155, 4515, - -2140, 4516, 4517, 4518, -2136, 4521, 4522, 122, - -3483, -3483, -6876, 4528, -6877, -18306,8991, -3483, - -3483, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -3490, 911, 4517, 4518, 7912, -3491, 7915, - 19345, -7951, 4524, -3494, 3417, -3505, 1125, 23798, - -3495, 23801, -3495, 23804, -3495, 19177, -3495, 23799, - -3496, 1231, -3496, -3496, 1134, 23807, 1236, 1237, - 1238, 1239, 1240, 15802, -3477, 1243, 1244, 1245, - 1246, 7903, 4545, 7905, 1250, 7906, 7907, 7908, - 7909, 7910, 7911, 3511, -94, -94, -3487, 7917, - -3488, -14917,12380, -94, 7925, 1015, 7938, 3309, - -19363,7931, -19364,7933, -19365,7935, -14736,7937, - -19356,7940, 3214, 7942, 7943, 3314, -19358,3214, - 3214, 3214, 3214, 3214, -11347,7933, 3214, 3214, - 3214, 3214, -3442, -83, -3442, 3214, -3441, -3441, - -3441, 3214, -3442, -3442, 959, 4565, 4566, 7960, - -3443, 7963, 19393, -7903, 4572, 4573, 1215, 4575, - -2080, 4576, 4577, 4578, 4579, 4580, 4581, 181, - -3424, -3424, -6817, 4587, -6818, -18247,9050, -3424, - 4595, -2315, 4608, -21, -22693,4601, -22694,4603, - -22695,4605, -18066,4607, -22686,4610, -116, 4612, - 4613, -16, -22688,-116, -116, -116, -116, -116, - -14677,4603, -116, -116, -116, -116, -6772, -3413, - -6772, -116, -6771, -6771, -6771, -116, -6772, -6772, - -2371, 1235, 1236, 4630, -6773, 4633, 16063, -11233, - 1242, -6776, 135, -6787, -2157, 20516, -6777, 20519, - -6777, 20522, -6777, 15895, -6777, 20517, -6778, -2051, - -6778, -6778, -2148, 20525, -2046, -2045, -2044, -2043, - -2042, 12520, -6759, -2039, -2038, -2037, -2036, 4621, - 1263, 4623, -2032, 4624, 4625, 4626, -2028, 4629, - 4630, 230, -3375, -3375, -6768, 4636, -6769, -18198, - 9099, -3375, -3375, -16, -3375, 3281, -3374, -3374, - -3374, -3374, -3374, -3374, 1027, 4633, 4634, 8028, - -3375, 8031, 19461, -7835, 4640, -3378, 3533, -3389, - 1241, 23914, -3379, 23917, -3379, 23920, -3379, 19293, - -3379, 23915, -3380, 1347, -3380, -3380, 1250, 23923, - 1352, 1353, 1354, 1355, 1356, 15918, -3361, 1359, - 1360, 1361, 1362, 8019, 4661, 8021, 1366, 8022, - 8023, 8024, 1370, 8027, 8028, 3628, 23, 23, - -3370, 8034, -3371, -14800,12497, 23, 23, 3382, - 1384, 1385, 1386, 449, 1388, 1389, 1390, 1391, - 1392, 1393, 139, 1395, 1396, 1397, 1398, 1399, - 1400, 1401, 1402, 1403, 1404, 1405, 1406, 1407, - 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415, - 1416, 1417, 1418, -2662, -2661, -2660, -2659, -2658, - -888, -888, -888, 874, 1428, 1429, 1430, 1431, - 1432, 1433, 1434, 1435, -2144, -1521, -1520, -2141, - 1788, -886, -886, 3336, 1791, 1792, 1793, 1794, - 1795, 1796, 1797, 1798, 1799, 1800, 1801, 1802, - 1803, 1804, 1458, 1459, 1805, 1461, 1806, 1463, - 1464, 1807, 1808, 1809, 1810, 1811, 1812, 1813, - 1814, 1815, 1816, 1381, 1817, 1381, 1818, 1381, - 1480, 1819, 1820, 1483, 1484, 1485, 1821, 1822, - 1823, 1824, 1825, 1826, 1827, 1828, 1829, 1830, - 1831, 1832, 1833, 1834, 1835, 1836, 1837, 1838, - 1839, 1840, 1841, 1842, 1843, 1844, 1845, 1846, - 1847, 1848, 1849, 1850, 1851, 1852, 1853, 1854, - 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, - 1863, 1864, 1865, 1866, 1867, 1868, 1869, 1870, - 1871, 1872, 1873, 1874, 1875, 1876, 1877, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 9097, 0, -1251, 9102, 0, - 0, 0, 0, 0, 0, 0, 0, 3220, - 0, 0, 3225, 0, 0, -15, -14, -13, - 0, 0, 0, -12, -11, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 3902, -2754, 605, 0, 0, -2751, 0, - 0, 0, 0, 0, 0, 1938, 1939, 1940, - 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, - 1949, 1950, 1951, 1530, 1443, 1532, 1533, 1956, - 1957, 1958, 1959, 1960, 1961, 1962, 141, 1964, - 1534, 1966, 1967, 1968, 1969, 1970, 1971, 1972, - 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, - 1981, 142, 143, 1984, 1985, 1986, 1987, 1988, - 1989, 1990, 1991, 1992, 1993, 1994, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 1665, 1996, 1997, - 1998, 1999, 2000, 2001, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 2002, 2003, 2004, 2005, 2006, 32767, 32767, 32767, - 32767, 32767, 2007, 32767, 2008, 2009, 2010, 2011, - 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, - 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, - -4501, -4501, -4501, -4501, -1422, -4500, -4500, -4500, - 2035, 2036, 32767, 1706, 32767, 1706, 1706, 32767, - 1705, 1705, 32767, 2042, 2043, 2044, 2045, 2046, - 2047, 1711, 2049, 1712, 2051, 4525, 4525, 4525, - 2055, 2056, 2057, 4528, 2059, 2060, 2061, 2062, - 2063, 2064, 2065, 7042, 2067, 4536, 4536, 4536, - 2071, 2072, 2073, 4539, 2075, 2076, 2077, 2078, - 2079, 2080, 2081, 2082, 2083, 2084, 1735, 1735, - 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, - 226, 227, 2097, 228, 229, 230, 6116, 232, - 233, -11725,-11725,-7095, 15578, -11715,15581, -11715, - 15584, 15585, 10956, -11716,15578, -11717,15580, -11718, - -11718,-7088, 15585, 2122, 2123, 2124, -4643, -6986, - 7576, -11703,-4644, -6983, 2131, -6982, -325, -3683, - 258, 2136, 260, 261, 2139, 3648, 3648, 3648, - 266, 267, 268, 2146, 270, 271, 272, -1029, - 32767, 32767, 32767, 32767, 6972, 2572, 32767, -1034, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -4286, - 32767, 32767, -2089, 32767, 3715, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - -2492, -2492, -2492, -2492, -4304, -2492, -2492, -2492, - 6433, -2492, -2492, -1162, -5881, -2492, -2492, -2492, - 6448, -2492, -2492, -2492, -2492, -2492, -2492, -2492, - -2492, -2492, -2492, -2492, -2492, -2492, -2492, -2492, - -2492, 8157, 5576, -2492, -2492, 13525, 6315, 6316, - -4168, -1586, 6319, -9531, -9530, 32767, 2195, 2196, - 2197, 2198, 2199, 2200, 2201, 2202, 2203, 2204, - 2205, 2206, 2207, 2208, 2209, 2210, 2211, 2212, - 2213, 2214, 2215, 2216, 2217, 2218, 2219, 2220, - 2221, 2222, 2223, 2224, 2225, 2226, 2227, 2228, - 2229, 2230, 2231, 2232, 2233, 2234, 2235, 2236, - 2237, 2238, 2239, 2240, 2241, 2242, 2243, 2244, - 2245, 2246, 2247, 2248, 2249, 2250, 2251, 2252, - 2253, 2254, 2255, 2256, 2257, 2258, 2259, 2260, - 2261, 2262, -5075, 2264, 2265, 2266, 2267, 2268, - 2269, 2270, 2271, 2272, 2273, 2274, 2275, 2276, - 2277, 2278, 2279, 2280, 2281, 2282, 2283, 2284, - 2285, 2286, 2287, 2288, 2289, 2290, 2291, 2292, - 2293, 2294, 2295, 2296, 2297, 2298, 2299, 2300, - 2301, 2302, 2303, 1667, 1667, 2306, 2307, 2308, - 2309, 2310, 2311, 2312, 2313, 2314, 2315, 2316, - 2317, 2318, 2319, 2320, 2321, 1358, 1359, 2324, - 2325, 2326, 2327, 2328, 2329, 2330, 2331, 2332, - 2333, 2081, 2081, 2081, 2081, 2338, 2082, 2082, - 2082, 2342, 2083, 2083, 2083, 2083, 2083, -1575, - -1574, 2083, 2083, 2083, 2083, -1569, 2084, 2084, - -1566, -1565, -1564, 5217, -1562, -1561, -1560, -1559, - -1558, 5225, -1556, 5227, -1554, 5230, -1552, -1551, - -1550, -1549, -1548, -1547, -1546, 5241, -1544, -1543, - -1542, -1541, -1540, -1539, -1538, -1537, -1536, -1535, - -1534, -1533, -1532, -1531, -1530, -1529, -1528, -1527, - 2273, -1525, -1524, -1523, -1522, -1521, -1520, -1519, - -1518, -1517, -1516, -1515, -1514, 2410, 2411, -4535, - -1158, 13194, -2656, -2655, -1346, -1345, 15043, -1343, - 5296, -1340, -1339, -1338, -1337, -1336, -9340, -4939, - -1333, -1332, 2062, -9341, 2065, 13495, -13801,-1326, - 5305, 5306, 5307, -7981, -1325, -1324, -1323, -1322, - -1321, -1320, -5720, -9325, -9325, -12718,32767, 2451, - 2452, 2453, 2454, 2455, 2456, 2457, 2458, 2459, - 2460, 2461, 2462, 2463, 2464, 2465, -6169, 4316, - 1735, -6169, 9682, 9682, 2472, 2473, -8011, -5429, - 2476, -13374,-13373,2479, 2480, 2481, 2482, 2483, - 2484, 2485, 2486, 2487, 2488, 2489, 2490, 2491, - 2492, 2493, 2494, 2495, 2496, 2497, 2498, 203, - 1141, 203, 203, 203, 203, 203, 203, 1458, - 203, 2509, 2510, 2511, 2512, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 155, 32767, 32767, 32767, - 32767, 156, 32767, 32767, 32767, 32767, 157, 207, - 4288, 4288, 4288, 4288, 4288, 2519, 2520, 2521, - 760, 207, 207, 207, 207, 207, 207, 207, - 207, 3787, 3165, 3165, 2534, 2535, 2536, 2537, - 2538, 2539, -139, 2541, -139, -139, 2544, 2545, - 2546, 2547, 2548, 2549, 2550, 2551, 8437, 2553, - 2554, -9404, -9404, -4774, 17899, -9394, 17902, -9394, - 17905, 17906, 13277, -9395, 17899, -9396, -4669, -9396, - -9396, -4766, 17907, -4664, -4663, -4662, -4661, 0, - 32767, -9379, -4659, -4658, -4657, -4656, 2001, -1357, - 2584, 4462, 2586, 2587, 4465, 5974, 5974, 5974, - 2592, 2593, 2594, 4472, 2596, 2597, 2598, 4476, - 2603, 4478, 4479, 4480, 5985, 5985, 4483, 5986, - 2610, 4486, 4487, 2611, 2612, 5990, 2614, 5991, - 4493, 4494, 4495, 4496, 2620, 4498, 2622, 4500, - 8763, 8764, 8765, 4504, 4505, 4506, 4507, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 8635, - -1849, 733, 8638, -7212, -7211, 0, 0, 10485, - 7904, 0, 15851, 15851, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 2296, 1359, 2298, 2299, 2300, 2301, 2302, 2303, - 1049, 2305, 0, 0, 0, 0, 0, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 173, 32767, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, - 2306, -1774, -1773, -1772, -1771, -1770, 0, 0, - 0, 1762, 2316, 2317, 2318, 2319, 2320, 2321, - 2322, 2323, -1256, -633, -632, 0, 0, 0, - 0, 0, 0, 2679, 0, 2681, 2682, 0, - 0, 0, 0, 0, 0, 0, 0, -5885, - 0, 0, 11959, 11960, 7331, -15341,11953, -15342, - 11955, -15343,-15343,-10713,11960, -15333,11963, 7237, - 11965, 11966, 7337, -15335,7237, 7237, 7237, 7237, - 7237, -7324, 11956, 7237, 7237, 7237, 7237, 581, - 3940, 0, -1877, 0, 0, -1877, -3385, -3384, - -3383, 0, 0, 0, -1877, 0, 0, 0, - -1877, -3, -1877, -1877, -1877, -3381, -3380, -1877, - -3379, -2, -1877, -1877, 0, 0, -3377, 0, - -3376, -1877, -1877, -1877, -1877, 0, -1877, 0, - -1877, -6139, -6139, -6139, -1877, -1877, -1877, -1877, - 2764, 2765, 2766, 2767, 2768, 4581, 2770, 2771, - 2772, -6152, 2774, 2775, 1446, 6166, 2778, 2779, - 2780, -6159, 2782, 2783, 2784, 2785, 2786, 2787, - 2788, 2789, 2790, 2791, 2792, 2793, 2794, 2795, - 2796, 2797, -7851, -5269, 2800, 2801, -13215,-6004, - -6004, 4481, 1900, -6004, 9847, 9847, 2637, 2638, - -7846, -5264, 2641, -13209,-13208,-5997, -5997, 4488, - 1907, -5997, 9854, 9854, 2644, 2645, -7839, -5257, - 2648, -13202,-13201,-5990, -5990, 4495, 1914, -5990, - -6178, 9860, 2650, 2651, -7833, -5251, 2654, 2843, - -13194,-5983, -5983, 4502, 1921, 32767, 32767, 32767, - 32767, 32767, 2849, 2850, 1029, 2852, 2422, 2854, - 2855, 2856, 2857, 2858, 2859, 2860, 2861, 2862, - 2863, 2864, 2865, 2866, 2867, 2868, 2869, 1030, - 1031, 2872, 2873, 2874, 2875, 2876, 2877, 2878, - 2879, 2880, 2881, 2882, 2883, 2884, 2885, 2886, - 2887, 2888, 2889, 2890, 2891, 2892, 2893, 2894, - 2895, 2896, 2897, 2898, 2899, 2900, 2901, 2902, - 2903, 2904, 2905, 2906, 2907, 2908, 2909, 2910, - 2911, 2912, 2913, 2914, 2915, 2916, 2917, 2918, - 2919, 2920, 2921, 2922, 2923, 2924, 2925, 2926, - 2927, 2928, 2929, 2930, 2931, 2932, 2933, 2934, - 2935, 2936, 175, 176, 177, 178, 179, 180, - 181, 182, 183, 184, 185, 186, 187, 188, - 189, 190, 191, 192, 193, 194, 195, 2958, - 2959, 2960, 2961, 2962, 2963, 2964, 2965, 2966, - 2967, 2968, 2969, 2970, 2971, 2972, 2973, 2974, - 2975, 2976, 2977, 2978, 2979, 2980, 2981, 2982, - 2983, 2984, 2985, 2986, 2987, 2988, 2989, 2990, - 2991, 2992, 2993, 2994, 2995, 2996, 2997, 2998, - 2999, 3000, 3001, 3002, 3003, 3004, 3005, 3006, - 3007, 196, 197, 198, 199, 200, 201, 202, - 203, 204, 205, 206, 207, 208, 209, 4471, - -2296, 212, 213, 214, 215, 216, 217, 218, - 219, 220, 221, 3568, 3035, 3036, 3037, 3038, - 32767, 32767, 32767, 3039, 3040, 3041, 3042, 3043, - 3044, 32767, 32767, 3045, 3046, 3047, 4565, 3049, - 4567, 32767, 32767, -1319, -1319, -1319, -1319, -1319, - -1319, 32767, 32767, -1321, -1321, 3059, 32767, 32767, - 32767, -1050, 3061, 3062, 3063, 3064, 3065, 3066, - 32767, 3067, 3068, 3069, 3070, 3693, 3694, 3073, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 338, 32767, 337, 337, 337, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 0, 0, 985, 0, + 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 0, 0, 0, - 0, 32767, 0, 0, 0, 0, 32767, 32767, - 0, 0, 0, 0, 0, 0, 0, 0, - 32767, 0, 0, 0, 0, 0, 0, 0, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, -3663, -3662, -3661, -3660, -3659, 0, 0, - -3656, -3655, -3654, -3653, 0, -3652, -3651, 0, - 0, 0, -6780, 0, 0, 0, 0, 0, - -6782, 0, -6782, 0, -6783, 0, 0, 0, - 0, 0, 0, 0, -6786, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -3799, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -3923, -3923, 3024, -352, - -14703,1148, 1148, -160, -160, -16547,-160, -6798, - -161, -161, -161, -161, -161, 7844, 3444, -161, - -161, -3554, 7850, -3555, -14984,12313, -161, -161, - 3198, -161, 6495, -160, -160, -160, -160, -160, - -160, 4241, 7847, 7848, 11242, -161, 11245, 22675, - -4621, 7854, -164, 6747, -175, 4455, 27128, -165, - 27131, -165, 27134, -165, 22507, -165, 27129, -166, - 4561, -166, -166, 4464, 27137, 4566, 4567, 4568, - 4569, 4570, 19132, -147, 4573, 4574, 4575, 4576, - 11233, 7875, 11235, 4580, 11236, 11237, 11238, 4584, - 11241, 11242, 6842, 3237, 3237, -156, 11248, -157, - -11586,15711, 3237, 11256, 4346, 11269, 6640, -16032, - 11262, -16033,11264, -16034,11266, -11405,11268, -16025, - 11271, 6545, 11273, 11274, 6645, -16027,6545, 6545, - 6545, 6545, 6545, -8016, 11264, 6545, 6545, 6545, - 6545, -111, 3248, -111, 6545, -110, -110, -110, - 6545, -111, -111, 4290, 7896, 7897, 11291, -112, - 11294, 22724, -4572, 4364, 4365, 32767, 4366, 4367, - 4368, 4369, 4370, 4371, 4372, 4373, 4374, 4375, - 4376, 4377, 4378, 4379, 4380, 4381, 4382, 32767, - 32767, 32767, 32767, 32767, 4383, 4384, 4385, 32767, - 4386, 4387, 4388, 4389, 4390, 32767, 4391, 4392, - 4393, 4394, 4395, 4396, 4397, 4398, 4399, 4400, - 4401, 4402, 4403, 4404, 4405, 4406, 4407, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, -1078, -8000, -3370, 19303, - 32767, 19305, -7991, 19308, -7991, 14681, -7991, 19303, - 32767, 32767, -7994, 32767, -3365, 32767, -3264, -3263, - 32767, 32767, 32767, 32767, 32767, 32767, -3262, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 3394, 3395, - -3259, 3398, 3399, -1001, 32767, 32767, 32767, 32767, - -8004, -19433,32767, -4611, -4611, 32767, 32767, 32767, - 32767, 32767, 32767, -4616, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 6782, 18212, -9084, 3391, -4627, - 2284, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, -1272, - 32767, 32767, 32767, 32767, 32767, -851, 32767, 32767, - 0, 88, 0, 0, 32767, 32767, 32767, 89, - 90, 32767, 91, 32767, 32767, 0, 32767, 32767, + 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7392,12 +7421,8 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 434, 435, 0, 436, 0, - 437, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 434, 435, 436, 32767, 437, - 438, 32767, 32767, 32767, 439, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7406,24 +7431,51 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 4409, 4410, + 4411, 2143, 2144, 2145, 2146, 2147, 2148, 2149, + 2150, 2151, 2152, 32767, 32767, 32767, 4420, 4421, + 4422, 4423, 4424, 4425, 4426, 4427, 4428, 4429, + 4430, 4431, 4432, 4433, 4434, 4435, 4436, 4437, + 4438, 4439, 4440, 4441, 4442, 4443, 4444, 4445, + 4446, 4447, 4448, 4449, 4450, 32767, 4451, 4452, + 4453, 4454, 4455, 4456, 4457, 4458, 4459, 4460, + 4461, 4462, 4463, 4464, 4465, 4466, 4467, 4468, + 4469, 4470, 4471, 4472, 4473, 4474, 4475, 4476, + 4477, 4478, 4479, 4480, 4481, 4482, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 4483, 4484, 4485, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, -3886, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4645, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 0, 0, 32767, 32767, + 0, 0, 0, 0, 0, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3078, + 3078, 3078, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3065, + 3065, 3065, 3065, 3065, 3065, 3065, 3065, 3065, + 3065, 3065, 3065, 3065, 3065, 3065, 3065, 3065, + 3065, 3065, 3065, 3065, 3065, 3065, 3065, 3065, + 3065, 3065, 3065, 3065, 3065, 3065, 3065, 3065, + 3065, 3065, 3065, 3065, 3065, 3065, 3065, 3065, + 3065, 3065, 3065, 32767, 32767, 32767, 32767, 3061, + 3061, 3061, 3061, 3061, 3061, 3061, 3061, 3061, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3054, + 3054, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - -1906, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7433,60 +7485,27 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 4408, 4409, - 4410, 4411, 4412, 4413, 4414, 4415, 4416, 4417, - 4418, 32767, 32767, 32767, 32767, 32767, 4419, 4420, - 4421, 4422, 4423, 4424, 4425, 4426, 4427, 4428, - 20816, 4430, 4431, 4432, 4433, 4434, 4435, 4436, - -3568, 833, 4439, 4440, 7834, -3569, 7837, 19267, - -8029, 4446, 4447, 1089, 4449, -2206, 4450, 4451, - 4452, 4453, 4454, 4455, 55, -3550, 4458, -6944, - 4460, 4461, -18375,4463, 4464, 4465, 4466, 4477, - -152, -22824,4470, -22825,4472, -22826,4474, -18197, - 4476, 4477, 4478, -248, 4480, 4481, 32767, -22821, - 32767, -250, 32767, 32767, 32767, 32767, 32767, 32767, - -256, 32767, 32767, 32767, 32767, -6916, 32767, -6916, - 32767, -6917, 32767, -6919, -6919, -2518, 32767, 1088, - 4482, 4483, 4484, 32767, 32767, 1091, 32767, -17, - 32767, -2310, 32767, -6931, 32767, -6932, 32767, -6933, - 15739, 32767, 20360, 32767, 32767, -6937, -6937, -2307, - 20366, 32767, -2205, -2204, -2203, -2202, 12360, -6919, - -2199, 32767, -2198, -2197, 4460, 1102, 4485, -2194, - 4462, 4463, 4464, 32767, 4466, 32767, 66, -3539, - -3539, -6932, 4472, -6933, -18362,8935, 0, 0, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 0, - 0, 0, 32767, 0, 0, 0, 0, 0, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 4486, - 4487, 4488, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4489, - 4490, 4491, 4492, 4493, 4494, 4495, 4496, 4497, - 4498, 4499, 4500, 4501, 4502, 4503, 4504, 4505, - 4506, 4507, 4508, 4509, 4510, 4511, 4512, 4513, - 4514, 4515, 4516, 4517, 4518, 4519, 4520, 4521, - 4522, 4523, 4524, 4525, 4526, 4527, 4528, 4529, - 4530, 4531, 4532, 32767, 32767, 32767, 32767, 4533, - 4534, 4535, 4536, 4537, 4538, 4539, 4540, 4541, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 4542, - 4543, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -1593, 32767, 32767, 0, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 32767, 32767, 0, 32767, - 0, 0, 0, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 5694, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 839, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -7494,46 +7513,29 @@ NFKC_QC_hash_func(const void *key) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 7338, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 948, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 637, - 638, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 964, 964, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 253, 254, 255, - 256, 0, 257, 258, 259, 0, 260, 261, - 262, 263, 264, 3923, 3923, 267, 268, 269, - 270, 3923, 271, 272, 3923, 3923, 3923, -2857, - 3923, 3923, 3923, 3923, 3923, -2859, 3923, -2859, - 3923, -2860, 3923, 3923, 3923, 3923, 3923, 3923, - 3923, -2863, 3923, 3923, 3923, 3923, 3923, 3923, - 3923, 3923, 3923, 3923, 3923, 3923, 3923, 3923, - 3923, 3923, 3923, 3923, 124, 3923, 3923, 3923, - 3923, 3923, 3923, 3923, 3923, 3923, 3923, 3923, - 3923, 0, 0, 6947, 3571, -10780,5071, 5071, - 3763, 3763, -12624,3763, -2875, 3762, 3762, 3762, - 3762, 3762, 11767, 7367, 3762, 3762, 369, 11773, - 368, -11061,16236, 3762, -2868, -2868, -2868, 10421, - 3766, 3766, 3766, 3766, 3766, 3766, 8167, 11773, - 11774, 15168, 3765, 15171, 26601, -2866, -2866, -2866, - -2866, -2866, 0, -2865, -2865, 0, 0, 0, - 0, 0, 0, 0, 0, 0, -2874, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 7042, 7043, 7044, 7045, 7046, 7047, 7048, + 7049, 7050, 7051, 7052, 7053, 7054, 7055, 7056, + 0, 0, 7057, 7058, 7059, 7060, 7061, 7062, + 7063, 7064, 7065, 7066, 7067, 7068, 7069, 7070, + 7071, 7072, 7073, 7074, 7075, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 5011, 5012, - 5013, 5014, 5015, 5016, 5017, 5018, 5019, 5020, - 5021, 5022, 5023, 5024, 5025, 5026, 5027, 5028, - 5029, 5030, 5031, 5032, 5033, 5034, 5035, 5036, - 5037, 5038, 5039, 5040, 5041, 5042, 5267, 5268, - 5269, 11926, 8568, 11928, 5273, 11929, 11930, 11931, - 11932, 11933, 11934, 7534, 3929, 3929, 536, 11940, - 535, -10894,16403, 3929, 11948, 32767, 32767, 32767, + 0, 10295, 10296, 0, 0, 0, 0, 0, + 0, 0, 10152, 10153, 7445, 0, 10158, 10159, + 10160, 10161, 10162, 10163, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7542,15 +7544,20 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 3161, 3162, 3163, 3164, 3165, 3166, + 3167, 3168, 3169, 3170, 3171, 3172, 3173, 3174, + 3175, 3176, 3177, 3178, 3179, 3180, 3181, 3182, + 3183, 3184, 7900, 3186, 6480, 3188, 7906, 3190, + 7909, 3192, 6142, 3194, 7787, 3196, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 0, + 0, 0, 0, 0, 0, 0, 0, 5075, + 0, 0, 0, 0, 0, 0, 0, 4954, + 4954, 4954, 0, 0, 4956, 0, 0, 0, + 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 3160, 3161, 3162, 3163, 3164, 3165, 3166, 3167, - 3168, 3169, 3170, 3171, 3172, 3173, 3174, 3175, - 3176, 3177, 3178, 3179, 3180, 3181, 3182, 3183, - 3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, - 3416, 3417, 3418, 10075, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7558,8 +7565,28 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 356, 357, 358, 1, 2, 3, + 4, 5, 6, 7, 8, 32767, 32767, 32767, + 32767, 32767, 32767, 367, 32767, 32767, 32767, 32767, + 32767, -3034, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 369, 19, + -3042, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 26, 32767, 32767, 32767, 27, 28, 32767, + 29, 30, 32767, 32767, 32767, 32767, 31, 32767, + 32, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 33, 34, 35, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 36, 32767, 32767, 32767, 32767, 32767, 3075, + 3076, -7043, 3078, 3079, 32767, 3080, 3081, 3082, + 3083, 3084, 3085, 3086, 3087, 3088, 3089, 3090, + 3091, 3092, 3093, 3094, 3095, 3096, 3097, 3098, + 3099, 3100, 3101, 3102, -3460, -3460, -3460, -70, + -69, -68, -67, -66, -65, 3112, -3982, -3981, + 3115, 3116, 3117, 3118, 3119, 3120, 3121, -5251, + -5250, 3123, 3124, 3125, 3126, 3127, -1058, -1057, + -1056, 0, -1054, -1053, -1052, -5968, -5968, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, -2461, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7572,40 +7599,8 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 32767, 32767, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - -3433, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, -629, 0, 3074, 3075, 3076, 3077, 3078, - 0, 3079, 3080, 3081, 3082, 3083, 3084, 3085, - 3086, 3087, -9132, 10148, 3090, 3091, 3092, 3093, - 3094, 3095, 3096, 3097, 3098, 3099, 3100, 3101, - 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, - 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, - 3118, 3119, 3120, 6777, 3121, 3122, 3123, 3124, - 3125, 3126, 3127, 3128, 3129, 6780, 0, 6780, - 6780, 6780, 0, 0, 0, 6782, 0, 0, - 0, 0, 6784, 0, 6785, 0, 6786, 6786, - 0, 6786, 0, 6787, 6787, 6787, 0, 6788, - 6788, 6788, 6788, 6788, 6788, 6788, 0, 6789, - 6789, 6789, 6789, 2990, 6789, 0, 0, 0, - 0, 6793, 6793, 6793, 6793, 6793, 0, 0, - 0, 0, 0, 0, -7904, 7947, 7947, 6639, - 6639, -9748, 32767, 0, 6637, 32767, 32767, 6635, - 32767, 32767, 10238, 6633, 32767, 32767, 14642, 3237, - -8192, 19105, 32767, 0, 0, 0, 13289, 6634, - 6634, 6634, 6634, 6634, 6634, 11035, 14641, 32767, - 18035, 32767, 18037, 29467, 0, 0, 0, 0, - 0, 32767, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 223, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7615,6 +7610,7 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7627,26 +7623,65 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1646, 0, 0, + 0, 0, 0, 0, 1783, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 279, 279, 7059, -927, 485, -927, 486, -927, + -927, -927, -927, 487, -927, -927, 488, 489, + 490, 491, 0, -927, 492, 493, 494, -927, + 495, 496, 497, -927, -927, -927, -2402, -2402, + -2402, 1360, 1361, -2402, -2402, -567, -2402, -2402, + -2402, -2402, -2402, -2402, -2402, -2402, -280, -2402, + -2402, -278, -2402, -2402, -2402, 3221, -2402, -2402, + -549, -2402, -2402, -2402, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 32767, 32767, + 32767, 224, 225, 226, 32767, 227, 228, 229, + -4687, -4687, -4687, -5920, -972, -1176, -970, -4687, + 32767, -968, -967, -7746, 241, -1170, 243, -1169, + 245, 246, 247, 248, -1165, 250, 251, -1163, + -1163, -1163, -1163, 32767, 256, -1162, -1162, -1162, + 260, -1161, -1161, -1161, 264, 265, 266, 140, + -1502, -1501, 142, -1499, -1498, -1497, -1496, -1495, + -1494, -1493, -1492, -1491, -1490, -1489, -1488, -1487, + 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 284, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 0, 0, - 0, 0, 0, 32767, 32767, 0, 0, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 285, 286, 287, 288, 289, 290, 291, 292, + 293, 294, 295, 296, 297, 298, 299, 300, + 301, 302, 303, 304, 305, 306, 307, 308, + 309, 310, 311, 312, 313, 314, 315, 316, + 317, 318, 319, 320, 321, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7654,331 +7689,308 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 3197, + 3198, 3199, 3200, 3201, 3202, 3203, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 2786, 32767, 0, 0, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 6251, 3205, 3206, 3207, + 4441, -506, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 224, 225, 226, 32767, 227, 228, 229, - 230, 231, 232, 233, 234, 235, 236, 237, - 32767, 2087, -494, -8398, 7453, 7453, 243, 244, - -10240,-7658, 247, -15603,-15602,250, 251, 252, - 253, 254, 255, 32767, 256, 257, 258, 259, - 260, 261, 262, 263, 264, 265, 266, 267, - 268, -2027, -1089, -2027, -2027, -2027, -2027, -2027, - -2027, -772, -2027, 279, 280, 281, 282, 283, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 284, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, -1717, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 285, 286, 287, 288, 289, 290, 291, 292, - 6178, 294, 295, -11663,-11663,-7033, 15640, -11653, - 15643, -11653,15646, 15647, 11018, -11654,15640, -11655, - -6928, -11655,-11655,-7025, 15648, -6923, -6922, -6921, - -6920, -6919, 7643, -11636,-6916, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + -1688, -1688, 0, 0, 0, 0, 0, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, -11072,-11072,-11072, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, -17220,-12590,10083, - -17210,10086, 5360, 10088, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 0, 0, 0, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -4715, 0, -3293, 0, -4717, 0, -4718, 0, + -2949, 0, -4592, 0, -2951, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + -3915, -3915, 32767, 32767, 32767, 32767, 0, 0, + 0, 0, 0, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, -4703, -4703, -4703, -4703, -4703, -4703, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 0, 0, 0, 0, 0, + 32767, 32767, -3370, 32767, -3371, -3371, -3371, -3371, + -3371, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 337, 32767, 338, 32767, 339, 340, 341, 32767, + 32767, 32767, -3383, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, -3032, 32767, -3033, -3033, -3033, 32767, + 0, 0, -3036, 0, 0, 0, 0, 0, + 0, 0, -3045, 0, -3047, 0, 0, 0, + -1233, 3715, 3511, 3717, 0, 3719, 3720, 3721, + -3058, 0, -3060, 0, -3062, 0, 0, 0, + 0, 0, 0, 358, 358, 358, 358, 358, + 358, 358, 358, 358, 358, 358, 358, 358, + 358, 0, 0, 356, 0, 355, 3402, 3402, + 353, 353, 353, 353, 353, 353, 353, 353, + 353, 353, 0, 352, 0, 351, 3413, 0, + 349, 349, 0, 0, 0, 346, 346, 346, + 346, 346, 346, 346, 346, 346, 346, 346, + 346, 346, 346, 346, 346, 346, 346, 346, + 346, 346, 346, 346, 346, 346, 346, 346, + 346, 346, 346, 346, 346, 346, 346, 346, + 346, 346, 346, 346, 346, 346, 346, 346, + 346, 346, 346, 7422, 7423, 7424, 7425, 7426, + 7427, 7428, 7429, 7430, 7431, 7432, 7433, -2861, + -2861, 7436, 7437, 7438, 7439, 7440, 7441, 7442, + -2709, -2709, 0, 7446, -2711, -2711, -2711, -2711, + -2711, -2711, 3852, 3853, 3854, 465, 465, 465, + 465, 465, 465, -2711, 4384, 4384, -2711, -2711, + -2711, -2711, -2711, -2711, -2711, 5662, 5662, 0, + -2711, -2711, -2711, -2711, 1475, 1475, 1475, 420, + 1475, 1475, 1475, 6392, 6393, 6394, 7628, 2681, + 2886, 2681, 6399, 2681, 2681, 2681, 9461, 1475, + 2887, 1475, 2888, 1475, 1475, 1475, 1475, 2889, + 1475, 1475, 2890, 2891, 2892, 2893, 2402, 1475, + 2894, 2895, 2896, 1475, 2897, 2898, 2899, 1475, + 1475, 1475, 0, 0, 0, 3762, 3763, 0, + 0, 1835, 0, 0, 0, 0, 0, 0, + 0, 0, 2122, 0, 0, 2124, 0, 0, + 0, 5623, 0, 0, 1853, 0, 0, 0, + 2402, 2402, 2402, 2402, 2402, 2402, 2402, 2402, + 2402, 2402, 2402, 2402, 2402, 2402, 2402, 2402, + 2402, 2402, 2402, 2402, 2402, 2402, 2402, 2402, + 2402, 0, 2401, 2401, 2401, 2401, 2401, 2401, + 2401, 427, 428, 429, 430, 2932, 431, 432, + 433, 2936, 434, 435, 436, 437, 438, 439, + 440, 441, 442, 443, 444, 2948, 445, 2775, + 2775, 2775, 2775, 2775, 2775, 2775, 2775, 2775, + 2775, 2775, 2775, 2775, 2775, 2775, 2775, 2775, + 2775, 2775, 2775, 2775, 2444, 460, 461, 0, + 462, 463, 464, 0, 465, 466, 467, 468, + 469, 470, 471, 0, 472, 473, 474, 475, + 476, 477, 0, 0, 0, 0, 478, 479, + 480, 481, 482, 0, 0, -1205, -1204, -1203, + -1202, 483, 484, 485, 486, 487, 488, 489, + 2724, 491, 492, 493, 494, 495, 2825, 2825, + 2825, 2825, 2825, 2825, 2825, 2825, 2825, 2825, + 2825, 2825, 2825, 2825, 2825, 2825, 2825, 2825, + 2825, 515, 516, 517, 9866, 9867, 9868, 9869, + 9870, 523, 524, 3789, 526, 527, 528, 529, + 3790, 5162, 3188, 4819, 0, 3910, 0, 1048, + 1049, 1050, 5434, 5435, 5436, 3791, 5438, 5439, + 5440, 5441, 0, 1676, 1676, 0, 0, 0, + 0, 3443, 2366, 0, 0, 0, 0, 0, + 0, 3451, 3452, 3453, 4260, 4261, 4262, 4263, + 4264, 4265, 4266, 4267, 0, 0, 319, 0, + 0, 0, 2355, 0, 0, 0, 0, 0, + 5938, 5939, 5940, 5941, 5942, 5943, 5944, 3823, + 5946, 5947, 3824, 5949, 5950, 5951, 329, 5953, + 5954, 4102, 4103, 4891, 4892, 3960, 3960, 3960, + 3960, 3960, 3960, 3960, 3960, 3960, 3960, 3960, + 3960, 3960, 3960, 3960, 3960, 3960, 3960, 0, + 3960, 3045, 3960, 3960, 0, 3960, 3960, 3960, + 3960, 3515, 0, 3960, 0, 0, 0, 3960, + 5140, 5956, 5957, 5958, 5959, 5960, 5961, 5962, + 3960, 3960, 3960, 3960, 3960, 3960, 3960, 3960, + 3960, 3960, 3960, 3178, 5963, 5964, 5965, 5966, + 5967, 8081, 6789, 3960, 3960, 3960, 5968, 8091, + 5969, 5970, 5971, 5972, 5973, 5974, 0, 0, + 0, 5981, 5982, 5983, 5984, 5985, 5986, 5987, + 5988, 5989, 5990, 5991, 5992, 5993, 5994, 9171, + 2077, 2078, 2599, 2599, 9174, 9175, 9176, 9177, + 9178, 806, 807, 6470, 9182, 9183, 9184, 9185, + 5000, 5001, 5002, 6058, 5004, 5005, 5006, 90, + 90, 90, -1143, 3805, 3601, 3807, 90, 3809, + 3810, 3811, -2968, 5019, 3608, 5021, 3609, 5023, + 5024, 5025, 5026, 3613, 5028, 5029, 3615, 3615, + 3615, 3615, 4107, 5035, 3617, 3617, 3617, 5039, + 3618, 3618, 3618, 5043, 5044, 5045, 4919, 3277, + 3278, 4921, 3280, 3281, 3282, 3283, 3284, 3285, + 3286, 3287, 3288, 3289, 3290, 3291, 3292, 3293, + 3294, 3295, 3296, 3297, 3298, 3299, 3300, 3301, + 3302, 3303, 3304, 3305, 3306, 3307, 3308, 3309, + 3310, 3311, 3312, 3313, 3314, 3315, 3316, 3317, + 3318, 3319, 3320, 3321, 3322, 3323, 3324, 3325, + 3326, 3327, 3328, 3329, 3330, 3331, 3332, 3333, + 3334, 3335, 3336, 1722, 3338, 32767, 3698, 3699, + 6736, 3701, 3702, 3703, 3704, 3705, 3706, 3707, + 6753, 3709, 6757, 3711, 3712, 3713, 4947, 0, + 205, 0, 3718, 0, 0, 0, 6780, 3723, + 6784, 3725, 6788, 3727, 3728, 3729, 3730, 3731, + 3732, 3375, 3376, 3377, 3378, 3379, 3380, 3381, + 3382, 3383, 3384, 3385, 3386, 3387, 3388, 3747, + 3748, 3393, 3750, 3396, 350, 351, 3401, 3402, + 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, + 3764, 3413, 3766, 3416, 355, 3769, 3421, 3422, + 3772, 3773, 3774, 3429, 3430, 3431, 3432, 3433, + 3434, 3435, 3436, 3437, 3438, 3439, 3440, 3441, + 3442, 3443, 3444, 3445, 3446, 3447, 3448, 3449, + 3450, 3451, 3452, 3453, 3454, 3455, 3456, 3457, + 3458, 3459, 3460, 3461, 3462, 3463, 3464, 3465, + 3466, 3467, 3468, 3469, 3470, 3471, 3472, 3473, + 3474, -3601, -3601, -3601, -3601, -3601, -3601, -3601, + -3601, -3601, -3601, -3601, -3601, 6694, 6695, -3601, + -3601, -3601, -3601, -3601, -3601, -3601, 6551, 6552, + 3844, -3601, 6557, 6558, 6559, 6560, 6561, 6562, + 0, 0, 0, 3390, 3391, 3392, 3393, 3394, + 3395, 6572, -522, -521, 0, 0, 6575, 6576, + 6577, 6578, 6579, -1793, -1792, 3871, 6583, 6584, + 6585, 6586, 2401, 2402, 2403, 3459, 2405, 2406, + 2407, -2509, -2509, -2509, -3742, 1206, 1002, 1208, + -2509, 1210, 1211, 1212, -5567, 2420, 1009, 2422, + 1010, 2424, 2425, 2426, 2427, 1014, 2429, 2430, + 1016, 1016, 1016, 1016, 1508, 2436, 1018, 1018, + 1018, 2440, 1019, 1019, 1019, 2444, 2445, 2446, + 3922, 3923, 3924, 163, 163, 3927, 3928, 2094, + 3930, 3931, 3932, 3933, 3934, 3935, 3936, 3937, + 1816, 3939, 3940, 1817, 3942, 3943, 3944, -1678, + 3946, 3947, 2095, 3949, 3950, 3951, 32767, 3952, + 3953, 3954, 3955, 3956, 3957, 3958, 3959, 3960, + 3961, 3962, 3963, 3964, 3965, 3966, 3967, 3968, + 3969, 3131, 3971, 3972, 3973, 3974, 3975, 3976, + 3977, 3978, 3979, 3980, 3981, 3982, 3983, 3984, + 3985, 3986, 3987, 3988, 3989, 3990, 3991, 3992, + 3993, 3994, 3995, 3996, 3997, 3998, 3999, 4000, + 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, + 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, + 4017, 4018, 4019, 4020, 4021, 4022, 4023, 4024, + 4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, + 4033, 4034, 4035, 4036, 4037, 4038, 4039, 4040, + 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4048, + 4049, 4050, 4051, 4052, 4053, 4054, 4055, 4056, + 4057, 4058, 4059, 4060, 4061, 4062, 4063, 4064, + 4065, 4066, 4067, 4068, 4069, 4070, 4071, 4072, + 4073, 4074, 4075, 4076, 4077, 4078, 4079, 3132, + 4081, 4082, 4083, 4084, 4085, 4086, 4087, 4088, + 4089, 4090, 4091, 4092, 4093, 4094, 4095, 4096, + 4097, 4098, 4099, 4100, 4101, 4102, 4103, 4104, + 4105, 4106, 4107, 4108, 4109, 4110, 4111, 4112, + 4113, 4114, 4115, 4116, 4117, 4118, 4119, 4120, + 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, + 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, + 4137, 4138, 4139, 4140, -2901, -2901, -2901, -2901, + -2901, -2901, -2901, -2901, -2901, -2901, -2901, -2901, + -2901, -2901, -2901, -2901, -2901, -2901, -2901, -2901, + -2901, -2901, -2901, -2901, -2901, -2901, -2901, -2901, + -2901, -2901, -2901, -2901, -2901, -2901, -2901, -2901, + 4175, 4176, 4177, 4178, 4179, 4180, 4181, 4182, + 4183, 4184, 4185, 4186, -6108, -6108, 4189, 4190, + 4191, 4192, 4193, 4194, 4195, -5956, -5956, -3247, + 4199, -5958, -5958, -5958, -5958, -5958, -5958, 605, + 606, 607, -2782, -2782, -2782, -2782, -2782, -2782, + -5958, 1137, 1137, 617, 618, -5956, -5956, -5956, + -5956, -5956, 2417, 2417, -3245, -5956, -5956, -5956, + -5956, -1770, -1770, -1770, -2825, -1770, -1770, -1770, + 3147, 3148, 3149, 4383, -564, -359, -564, 3154, + -564, -564, -564, 6216, -1770, -358, -1770, -357, + -1770, -1770, -1770, -1770, -356, -1770, -1770, -355, + -354, -353, -352, -843, -1770, -351, -350, -349, + -1770, -348, -347, -346, -1770, -1770, -1770, -1643, + 0, 0, -1642, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - -4261, 2507, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -3346, 32767, 32767, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 32767, 32767, 32767, 32767, 0, + 0, 0, 0, 0, 1615, 0, 0, 0, + 0, 0, 2483, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 324, 32767, 325, 32767, 326, 32767, 327, 32767, - 328, 32767, 329, 32767, 330, 0, 0, 0, - 0, 0, 0, 32767, 32767, 223, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, - 0, 0, 0, 0, 32767, 32767, 32767, 32767, - 32767, 0, 32767, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 6529, - 6530, 6531, 6532, 32767, 6532, 6533, 6534, 0, - 0, 32767, 331, 32767, 332, 333, 334, 335, - 336, 32767, 0, 0, 0, 0, 0, 0, - 337, 0, 338, 0, -2473, -2472, -2471, 0, - 0, 0, -2470, 0, 0, 0, 0, 0, - 0, 0, -4976, 0, -2468, -2467, -2466, 0, - 0, 0, -2465, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 350, 351, 0, - 0, 0, 0, 0, 0, 0, 0, 1869, - 1869, 0, 1870, 1870, 1870, -4015, 1870, 1870, - 13829, 13830, 9201, -13471,13823, -13472,13825, -13473, - -13473,-8843, 13830, -13463,13833, -13463,13836, 13837, - 9208, -13464,0, 0, 0, 6768, 9112, -5449, - 13831, 6773, 9113, 0, 9114, 2458, 5817, 1877, - 0, 1877, 1877, 0, -1508, -1507, -1506, 1877, - 1877, 1877, 0, 1877, 1877, 1877, 0, 1874, - 0, 0, 0, -1504, -1503, 0, -1502, 1875, - 0, 0, 1877, 1877, -1500, 1877, -1499, 0, - 0, 0, 0, 1877, 0, 1877, 0, -4262, - -4262, -4262, 0, 0, 0, 0, 4641, 4642, - 4643, 4644, 4645, 6458, 4647, 4648, 4649, -4275, - 4651, 4652, 3323, 8043, 4655, 4656, 4657, -4282, - 4659, 4660, 4661, 4662, 4663, 4664, 4665, 4666, - 4667, 4668, 4669, 4670, 4671, 4672, 4673, 4674, - -5974, -3392, 4677, 4678, -11338,-4127, -4127, 6358, - 3777, -4127, 11724, 11724, 4514, 4515, -5969, -3387, - 4518, -11332,-11331,-4120, -4120, 6365, 3784, -4120, - 11731, 11731, 4521, 4522, -5962, -3380, 4525, -11325, - -11324,-4113, -4113, 6372, 3791, -4113, -4301, 11737, - 4527, 4528, -5956, -3374, 4531, 4720, -11317,-4106, - -4106, 6379, 3798, -4106, 0, 0, 0, 0, - 426, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 3209, 3210, 3211, 3212, 3213, 3214, - 3215, 3216, 3217, 3218, 3219, 6653, 3221, 3222, - 3223, 3224, 3225, 3226, 3227, 3228, 3229, 3230, - 3231, 3232, 3233, 3234, 3235, 3236, 3237, 3238, - 3239, 3240, 3241, 3242, 3243, 3244, 3874, 3246, - 173, 173, 173, 173, 173, 3252, 174, 174, - 174, 174, 174, 174, 174, 174, 174, 12394, - -6885, 174, 174, 174, 174, 174, 174, 174, - 174, 174, 174, 174, 174, 174, 174, 174, - 174, 174, 174, 174, 174, 174, 174, 174, - 174, 174, 174, 174, 174, 174, 174, -3484, - -3483, 174, 174, 174, 174, 174, 174, 174, - 174, 174, -3476, 3305, -3474, -3473, -3472, 3309, - 3310, 3311, -3470, 3313, 3314, 3315, 3316, -3467, - 3318, -3466, 3320, -3465, -3464, 3323, -3462, 3325, - -3461, -3460, -3459, 3329, -3458, -3457, -3456, -3455, - -3454, -3453, -3452, 3337, -3451, -3450, -3449, -3448, - 352, -3446, 3344, 3345, 3346, 3347, -3445, -3444, - -3443, -3442, -3441, 3353, 3354, 3355, 3356, 3357, - 3358, 11263, -4587, -4586, -3277, -3276, 13112, -3274, - 3365, -3271, -3270, -3269, -3268, -3267, -11271,-6870, - -3264, -3263, 131, -11272,134, 11564, -15732,-3257, - 3374, 3375, 3376, -9912, -3256, -3255, -3254, -3253, - -3252, -3251, -7651, -11256,-11256,-14649,-3245, -14650, - -26079,3389, 3390, 3391, 3392, 3393, 528, 3394, - 3395, 3396, 3397, 3398, 3399, 3400, 3401, 3402, - 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, - 3411, 3412, 3413, 3414, 3415, 3416, 3417, 3418, - 3419, 3420, 3421, 3422, 3423, 3424, 3425, 3426, - 3427, 3428, 3429, 3430, 3431, 3432, 3433, 3434, - 3435, 3436, 3437, 3438, 3439, 3440, 3441, 3442, - 3443, 3444, 3445, 3446, 3447, 3448, 3449, 3450, - 3451, 3452, 32767, 3453, 3454, 3455, 3456, 3457, - 3458, 32767, 3459, 3460, 3461, 3462, 32767, 32767, - 3463, 3464, 3465, 3466, 3467, 3468, 3469, 3470, - 32767, 3471, 3472, 3473, 3474, 3475, 3476, 3477, - 32767, 3478, 3479, 3480, 3481, 3482, 3483, 3484, - 3485, 3486, 3487, 3488, 3489, 3490, 3491, 3492, - 3493, 7157, 7157, 7157, 7157, 7157, 3499, 3500, - 7157, 7157, 7157, 7157, 3505, 32767, 7157, 3507, - 3508, 3509, 32767, 3510, 3511, 3512, 3513, 3514, - 32767, 3515, 32767, 32767, 32767, 3516, 3517, 3518, - 3519, 3520, 3521, 3522, 32767, 3523, 3524, 3525, - 3526, 3527, 3528, 3529, 3530, 3531, 3532, 3533, - 3534, 3535, 3536, 3537, 3538, 3539, 3540, 7340, - 3542, 3543, 3544, 3545, 3546, 3547, 3548, 3549, - 3550, 3551, 3552, 3553, 7477, 7478, 532, 3909, - 18261, 2411, 2412, 3721, 3722, 20110, 3724, 10363, - 3727, 3728, 3729, 3730, 3731, -4273, 128, 3734, - 3735, 7129, -4274, 7132, 18562, -8734, 3741, 3742, - 384, 3744, -2911, 3745, 3746, 3747, 3748, 3749, - 3750, -650, -4255, -4255, -7648, 3756, -7649, -19078, - 8219, -4255, 3764, -3146, 3777, -852, -23524,3770, - -23525,3772, -23526,3774, -18897,3776, -23517,3779, - -947, 3781, 3782, -847, -23519,-947, -947, -947, - -947, -947, -15508,3772, -947, -947, -947, -947, - -7603, -4244, -7603, -947, -7602, -7602, -7602, -947, - -7603, -7603, -3202, 404, 405, 3799, -7604, 3802, - 15232, -12064,411, -7607, -696, -7618, -2988, 19685, - -7608, 19688, -7608, 19691, -7608, 15064, -7608, 19686, - -7609, -2882, -7609, -7609, -2979, 19694, -2877, -2876, - -2875, -2874, -2873, 11689, -7590, -2870, -2869, -2868, - -2867, 3790, 432, 3792, -2863, 3793, 3794, 3795, - -2859, 3798, 3799, -601, -4206, -4206, -7599, 3805, - -7600, -19029,8268, -4206, -4206, -847, -4206, 2450, - -4205, -4205, -4205, -4205, -4205, -4205, 196, 3802, - 3803, 7197, -4206, 7200, 18630, -8666, 3809, -4209, - 2702, -4220, 410, 23083, -4210, 23086, -4210, 23089, - -4210, 18462, -4210, 23084, -4211, 516, -4211, -4211, - 419, 23092, 521, 522, 523, 524, 525, 15087, - -4192, 528, 529, 530, 531, 7188, 3830, 7190, - 535, 7191, 7192, 7193, 539, 7196, 7197, 2797, - -808, -808, -4201, 7203, -4202, -15631,11666, -808, - -808, 2551, -808, 5848, -807, -807, -807, -807, - -807, -807, 3594, 7200, 7201, 10595, -808, 10598, - 22028, -5268, 7207, -811, 6100, -822, 3808, 26481, - -812, 26484, -812, 26487, -812, 21860, -812, 26482, - -813, 3914, -813, -813, 3817, 26490, 3919, 3920, - 3921, 3922, 3923, 18485, -794, 3926, 3927, 3928, - 3929, 10586, 7228, 10588, 3933, 10589, 10590, 10591, - 3937, 10594, 10595, 6195, 2590, 2590, -803, 10601, - -804, -12233,15064, 2590, 10609, 3699, 10622, 5993, - -16679,10615, -16680,10617, -16681,10619, -12052,10621, - -16672,10624, 5898, 10626, 10627, 5998, -16674,5898, - 5898, 5898, 5898, 5898, -8663, 10617, 5898, 5898, - 5898, 5898, 32767, 32767, -760, 5896, -759, -759, - -759, 5896, -760, -760, 3641, 7247, 7248, 10642, - -761, 10645, 22075, -5221, 7254, 7255, 3897, 7257, - 602, 7258, 7259, 7260, 7261, 7262, 7263, 2863, - -742, -742, -4135, 7269, -4136, -15565,11732, -742, - 7277, 367, 7290, 2661, -20011,7283, -20012,7285, - -20013,7287, -15384,7289, -20004,7292, 2566, 7294, - 7295, 2666, -20006,2566, 2566, 2566, 2566, 2566, - -11995,7285, 2566, 2566, 2566, 2566, -4090, -731, - -4090, 2566, -4089, -4089, -4089, 2566, -4090, -4090, - 311, 3917, 3918, 7312, -4091, 7315, 18745, -8551, - 3924, 3925, 567, 2566, 32767, 23198, -4095, 23201, - -4095, 23204, -4095, 18577, -4095, 23199, -4096, 631, - -4096, -4096, 534, 23207, 636, 637, 638, 639, - 640, 15202, -4077, 643, 644, 645, 646, 7303, - 3945, 7305, 650, 7306, 7307, 7308, 654, 7311, - 7312, 2912, -693, -693, -4086, 7318, -4087, -15516, - 11781, -693, -693, 2666, -693, 5963, -692, -692, - -692, -692, -692, -692, 3709, 7315, 7316, 10710, - -693, 10713, 22143, -5153, 7322, -696, 6215, -707, - 3923, 26596, -697, 26599, -697, 26602, -697, 21975, - -697, 26597, -698, 4029, -698, -698, 3932, 26605, - 4034, 4035, 4036, 4037, 4038, 18600, -679, 4041, - 4042, 4043, 4044, 10701, 7343, 10703, 4048, 10704, - 10705, 10706, 4052, 10709, 10710, 6310, 2705, 2705, - -688, 10716, -689, -12118,15179, 2705, 2705, 6064, - 4066, 4067, 4068, 3131, 4070, 4071, 4072, 4073, - 4074, 4075, 2821, 4077, 4078, 4079, 4080, 4081, - 4082, 4083, 4084, 4085, 4086, 4087, 4088, 4089, - 4090, 4091, 4092, 4093, 4094, 4095, 4096, 4097, - 4098, 4099, 4100, 20, 21, 22, 23, 24, - 1794, 1794, 1794, 3556, 4110, 4111, 4112, 4113, - 4114, 4115, 4116, 4117, 538, 1161, 1162, 541, - 4470, 1796, 1796, 6018, 4473, 4474, 4475, 4476, - 4477, 4478, 4479, 4480, 4481, 4482, 4483, 4484, - 4485, 4486, 4140, 4141, 4487, 4143, 4488, 4145, - 4146, 4489, 4490, 4491, 4492, 4493, 4494, 4495, - 4496, 575, 576, 4061, 4497, 4061, 4498, 4061, - 4160, 4499, 4500, 4163, 4164, 4165, 4501, 4502, - 4503, 4504, 4505, 4506, 4507, 4508, 4509, 4510, - 4511, 4512, 4513, 4514, 4515, 4516, 4517, 4518, - 4519, 4520, 4521, 4522, 4523, 4524, 4525, 4526, - 4527, 4528, 4529, 4530, 4531, 4532, 4533, 4534, - 4535, 4536, 4202, 0, 5914, 0, 0, 629, - 630, 631, 632, 633, 634, 635, 636, 637, - 638, 639, 640, 641, 642, 643, 644, 645, - 646, 647, 648, 649, 650, 651, 652, 653, - -1842, -1842, -1842, -1842, -1842, -1842, -1842, -1842, - -1842, -1842, -1842, -1842, 666, 667, 668, 669, - 670, 671, 672, 673, 674, 675, 676, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, -4470, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 6910, 0, 32767, 32767, 32767, - 32767, -20383,6914, 32767, 32767, 32767, 32767, 32767, - 0, 0, 0, 0, 0, 32767, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 12220, - -7059, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 3133, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 0, 32767, 0, 32767, 32767, 32767, 32767, 0, + 32767, 3134, 0, 0, 0, 0, 32767, 32767, + 0, 0, 0, 32767, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, -7886, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 3135, 32767, 32767, 3136, 32767, 32767, + 32767, 32767, 32767, 32767, 3137, 32767, 32767, 3138, + 32767, 3139, 3140, 3141, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 3132, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 3133, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 0, 32767, 32767, 32767, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 32767, 32767, 32767, 32767, - 32767, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, -16387,0, 0, 0, 0, - 0, 0, 0, 8005, 3605, 0, 0, -3393, - 8011, -3394, -14823,12474, 0, 0, 3359, 0, - 32767, 0, 0, 0, 0, 0, 0, 4401, - 8007, 0, 11403, 0, 0, 22837, 0, 0, - 0, 0, -10, 4620, 27293, 0, 27296, 0, - 27299, 0, 22672, 0, 0, 0, 4727, 0, - 0, 3135, 32767, 32767, 32767, 32767, 32767, 32767, - 3136, 32767, 32767, 3137, 32767, 3138, 3139, 3140, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 0, 0, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 0, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 0, + 0, 0, 32767, 32767, 0, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, @@ -7986,26 +7998,15 @@ NFKC_QC_hash_func(const void *key) 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, - 32767, 32767, 32767, 32767, 32767, 32767, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 32767, 32767, 0, 32767, - 0, 32767, 32767, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 32767, 0, 32767, - 0, 32767, 32767, 0, 0, 32767, 32767, 32767, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0 + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 32767, + 32767, 32767, 32767 }; const unsigned char *k = (const unsigned char *) key; size_t keylen = 4; uint32 a = 0; - uint32 b = 3; + uint32 b = 1; while (keylen--) { @@ -8014,12 +8015,12 @@ NFKC_QC_hash_func(const void *key) a = a * 257 + c; b = b * 8191 + c; } - return h[a % 10193] + h[b % 10193]; + return h[a % 10195] + h[b % 10195]; } /* Hash lookup information for NFKC_QC */ static const pg_unicode_norminfo UnicodeNormInfo_NFKC_QC = { UnicodeNormProps_NFKC_QC, NFKC_QC_hash_func, - 5096 + 5097 }; diff --git a/src/include/common/unicode_version.h b/src/include/common/unicode_version.h index 5f6a00ba1eaea..ffcaff64cb13b 100644 --- a/src/include/common/unicode_version.h +++ b/src/include/common/unicode_version.h @@ -3,7 +3,7 @@ * unicode_version.h * Unicode version used by Postgres. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/common/unicode_version.h @@ -11,4 +11,4 @@ *------------------------------------------------------------------------- */ -#define PG_UNICODE_VERSION "16.0" +#define PG_UNICODE_VERSION "17.0" diff --git a/src/include/common/username.h b/src/include/common/username.h index e133bb7c5ebf5..a24cfe4d511f1 100644 --- a/src/include/common/username.h +++ b/src/include/common/username.h @@ -2,7 +2,7 @@ * username.h * lookup effective username * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/common/username.h */ diff --git a/src/include/datatype/timestamp.h b/src/include/datatype/timestamp.h index a924d58aabe1a..45683678a4793 100644 --- a/src/include/datatype/timestamp.h +++ b/src/include/datatype/timestamp.h @@ -5,7 +5,7 @@ * * Note: this file must be includable in both frontend and backend contexts. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/datatype/timestamp.h diff --git a/src/include/executor/execAsync.h b/src/include/executor/execAsync.h index d8e8ea7955932..402ffe4999437 100644 --- a/src/include/executor/execAsync.h +++ b/src/include/executor/execAsync.h @@ -2,7 +2,7 @@ * execAsync.h * Support functions for asynchronous execution * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 75366203706cf..aa9b361fa318d 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -4,7 +4,7 @@ * Low level infrastructure related to expression evaluation * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/execExpr.h diff --git a/src/include/executor/execParallel.h b/src/include/executor/execParallel.h index 5e7106c397a26..5a2034811d563 100644 --- a/src/include/executor/execParallel.h +++ b/src/include/executor/execParallel.h @@ -2,7 +2,7 @@ * execParallel.h * POSTGRES parallel execution interface * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/executor/execPartition.h b/src/include/executor/execPartition.h index 3b3f46aced05f..82063ec2a16df 100644 --- a/src/include/executor/execPartition.h +++ b/src/include/executor/execPartition.h @@ -2,7 +2,7 @@ * execPartition.h * POSTGRES partitioning executor interface * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/executor/execScan.h b/src/include/executor/execScan.h index 837ea7785bb4c..18b03235c3c4a 100644 --- a/src/include/executor/execScan.h +++ b/src/include/executor/execScan.h @@ -2,7 +2,7 @@ * execScan.h * Inline-able support functions for Scan nodes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -15,6 +15,7 @@ #include "miscadmin.h" #include "executor/executor.h" +#include "executor/instrument.h" #include "nodes/execnodes.h" /* @@ -49,16 +50,24 @@ ExecScanFetch(ScanState *node, { /* * This is a ForeignScan or CustomScan which has pushed down a - * join to the remote side. The recheck method is responsible not - * only for rechecking the scan/join quals but also for storing - * the correct tuple in the slot. + * join to the remote side. If it is a descendant node in the EPQ + * recheck plan tree, run the recheck method function. Otherwise, + * run the access method function below. */ + if (bms_is_member(epqstate->epqParam, node->ps.plan->extParam)) + { + /* + * The recheck method is responsible not only for rechecking + * the scan/join quals but also for storing the correct tuple + * in the slot. + */ - TupleTableSlot *slot = node->ss_ScanTupleSlot; + TupleTableSlot *slot = node->ss_ScanTupleSlot; - if (!(*recheckMtd) (node, slot)) - ExecClearTuple(slot); /* would not be returned by scan */ - return slot; + if (!(*recheckMtd) (node, slot)) + ExecClearTuple(slot); /* would not be returned by scan */ + return slot; + } } else if (epqstate->relsubs_done[scanrelid - 1]) { diff --git a/src/include/executor/execdebug.h b/src/include/executor/execdebug.h index e5d27fb6c9ab4..3e11055191433 100644 --- a/src/include/executor/execdebug.h +++ b/src/include/executor/execdebug.h @@ -7,7 +7,7 @@ * for debug printouts, because that's more flexible than printf(). * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/execdebug.h @@ -34,22 +34,22 @@ * EXEC_NESTLOOPDEBUG is a flag which turns on debugging of the * nest loop node by NL_printf() and ENL_printf() in nodeNestloop.c * ---------------- -#undef EXEC_NESTLOOPDEBUG */ +/* #define EXEC_NESTLOOPDEBUG */ /* ---------------- * EXEC_SORTDEBUG is a flag which turns on debugging of * the ExecSort() stuff by SO_printf() in nodeSort.c * ---------------- -#undef EXEC_SORTDEBUG */ +/* #define EXEC_SORTDEBUG */ /* ---------------- * EXEC_MERGEJOINDEBUG is a flag which turns on debugging of * the ExecMergeJoin() stuff by MJ_printf() in nodeMergejoin.c * ---------------- -#undef EXEC_MERGEJOINDEBUG */ +/* #define EXEC_MERGEJOINDEBUG */ /* ---------------------------------------------------------------- * #defines controlled by above definitions diff --git a/src/include/executor/execdesc.h b/src/include/executor/execdesc.h index 86db3dc8d0de2..37c2576e4bc9b 100644 --- a/src/include/executor/execdesc.h +++ b/src/include/executor/execdesc.h @@ -5,7 +5,7 @@ * and related modules. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/execdesc.h @@ -42,6 +42,8 @@ typedef struct QueryDesc ParamListInfo params; /* param values being passed in */ QueryEnvironment *queryEnv; /* query environment passed in */ int instrument_options; /* OR of InstrumentOption flags */ + int query_instr_options; /* OR of InstrumentOption flags for + * query_instr */ /* These fields are set by ExecutorStart */ TupleDesc tupDesc; /* descriptor for result tuples */ @@ -51,8 +53,8 @@ typedef struct QueryDesc /* This field is set by ExecutePlan */ bool already_executed; /* true if previously executed */ - /* This is always set NULL by the core system, but plugins can change it */ - struct Instrumentation *totaltime; /* total time spent in ExecutorRun */ + /* This field is allocated by ExecutorStart if needed */ + struct Instrumentation *query_instr; /* query level instrumentation */ } QueryDesc; /* in pquery.c */ diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 104b059544dd3..33bbdbfeffb50 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -4,7 +4,7 @@ * support for the POSTGRES executor module * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/executor.h @@ -14,6 +14,8 @@ #ifndef EXECUTOR_H #define EXECUTOR_H +#include "access/xlogdefs.h" +#include "datatype/timestamp.h" #include "executor/execdesc.h" #include "fmgr.h" #include "nodes/lockoptions.h" @@ -99,12 +101,12 @@ extern PGDLLIMPORT ExecutorCheckPerms_hook_type ExecutorCheckPerms_hook; /* * prototypes from functions in execAmi.c */ -struct Path; /* avoid including pathnodes.h here */ +typedef struct Path Path; /* avoid including pathnodes.h here */ extern void ExecReScan(PlanState *node); extern void ExecMarkPos(PlanState *node); extern void ExecRestrPos(PlanState *node); -extern bool ExecSupportsMarkRestore(struct Path *pathnode); +extern bool ExecSupportsMarkRestore(Path *pathnode); extern bool ExecSupportsBackwardScan(Plan *node); extern bool ExecMaterializesOutput(NodeTag plantype); @@ -137,10 +139,10 @@ extern TupleHashTable BuildTupleHashTable(PlanState *parent, const Oid *eqfuncoids, FmgrInfo *hashfunctions, Oid *collations, - long nbuckets, + double nelements, Size additionalsize, MemoryContext metacxt, - MemoryContext tablecxt, + MemoryContext tuplescxt, MemoryContext tempcxt, bool use_variable_hash_iv); extern TupleHashEntry LookupTupleHashEntry(TupleHashTable hashtable, @@ -156,6 +158,9 @@ extern TupleHashEntry FindTupleHashEntry(TupleHashTable hashtable, ExprState *eqcomp, ExprState *hashexpr); extern void ResetTupleHashTable(TupleHashTable hashtable); +extern Size EstimateTupleHashTableSpace(double nentries, + Size tupleWidth, + Size additionalsize); #ifndef FRONTEND /* @@ -241,7 +246,9 @@ extern void standard_ExecutorEnd(QueryDesc *queryDesc); extern void ExecutorRewind(QueryDesc *queryDesc); extern bool ExecCheckPermissions(List *rangeTable, List *rteperminfos, bool ereport_on_violation); +extern bool ExecCheckOneRelPerms(RTEPermissionInfo *perminfo); extern void CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, + OnConflictAction onConflictAction, List *mergeActions); extern void InitResultRelInfo(ResultRelInfo *resultRelInfo, Relation resultRelationDesc, @@ -296,6 +303,13 @@ extern void ExecEndNode(PlanState *node); extern void ExecShutdownNode(PlanState *node); extern void ExecSetTupleBound(int64 tuples_needed, PlanState *child_node); +/* + * ExecProcNodeInstr() is implemented in instrument.c, as that allows for + * inlining of the instrumentation functions, but thematically it ought to be + * in execProcnode.c. + */ +extern TupleTableSlot *ExecProcNodeInstr(PlanState *node); + /* ---------------------------------------------------------------- * ExecProcNode @@ -318,6 +332,7 @@ ExecProcNode(PlanState *node) * prototypes from functions in execExpr.c */ extern ExprState *ExecInitExpr(Expr *node, PlanState *parent); +extern ExprState *ExecInitExprWithContext(Expr *node, PlanState *parent, Node *escontext); extern ExprState *ExecInitExprWithParams(Expr *node, ParamListInfo ext_params); extern ExprState *ExecInitQual(List *qual, PlanState *parent); extern ExprState *ExecInitCheck(List *qual, PlanState *parent); @@ -338,7 +353,7 @@ extern ExprState *ExecBuildHash32Expr(TupleDesc desc, const List *collations, const List *hash_exprs, const bool *opstrict, PlanState *parent, - uint32 init_value, bool keep_nulls); + uint32 init_value); extern ExprState *ExecBuildGroupingEqual(TupleDesc ldesc, TupleDesc rdesc, const TupleTableSlotOps *lops, const TupleTableSlotOps *rops, int numCols, @@ -366,6 +381,7 @@ extern ProjectionInfo *ExecBuildUpdateProjection(List *targetList, TupleTableSlot *slot, PlanState *parent); extern ExprState *ExecPrepareExpr(Expr *node, EState *estate); +extern ExprState *ExecPrepareExprWithContext(Expr *node, EState *estate, Node *escontext); extern ExprState *ExecPrepareQual(List *qual, EState *estate); extern ExprState *ExecPrepareCheck(List *qual, EState *estate); extern List *ExecPrepareExprList(List *nodes, EState *estate); @@ -398,7 +414,7 @@ ExecEvalExpr(ExprState *state, * Like ExecEvalExpr(), but for cases where no return value is expected, * because the side-effects of expression evaluation are what's desired. This * is e.g. used for projection and aggregate transition computation. - + * * Evaluate expression identified by "state" in the execution context * given by "econtext". * @@ -589,7 +605,8 @@ extern void ExecInitResultTupleSlotTL(PlanState *planstate, const TupleTableSlotOps *tts_ops); extern void ExecInitScanTupleSlot(EState *estate, ScanState *scanstate, TupleDesc tupledesc, - const TupleTableSlotOps *tts_ops); + const TupleTableSlotOps *tts_ops, + uint16 flags); extern TupleTableSlot *ExecInitExtraTupleSlot(EState *estate, TupleDesc tupledesc, const TupleTableSlotOps *tts_ops); @@ -680,6 +697,8 @@ extern void ExecCreateScanSlotFromOuterPlan(EState *estate, extern bool ExecRelationIsTargetRelation(EState *estate, Index scanrelid); +extern bool ScanRelIsReadOnly(ScanState *ss); + extern Relation ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags); extern void ExecInitRangeTable(EState *estate, List *rangeTable, List *permInfos, @@ -733,20 +752,23 @@ extern Bitmapset *ExecGetAllUpdatedCols(ResultRelInfo *relinfo, EState *estate); */ extern void ExecOpenIndices(ResultRelInfo *resultRelInfo, bool speculative); extern void ExecCloseIndices(ResultRelInfo *resultRelInfo); -extern List *ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, - TupleTableSlot *slot, EState *estate, - bool update, - bool noDupErr, - bool *specConflict, List *arbiterIndexes, - bool onlySummarizing); + +/* flags for ExecInsertIndexTuples */ +#define EIIT_IS_UPDATE (1<<0) +#define EIIT_NO_DUPE_ERROR (1<<1) +#define EIIT_ONLY_SUMMARIZING (1<<2) +extern List *ExecInsertIndexTuples(ResultRelInfo *resultRelInfo, EState *estate, + uint32 flags, TupleTableSlot *slot, + List *arbiterIndexes, + bool *specConflict); extern bool ExecCheckIndexConstraints(ResultRelInfo *resultRelInfo, TupleTableSlot *slot, EState *estate, ItemPointer conflictTid, - ItemPointer tupleid, + const ItemPointerData *tupleid, List *arbiterIndexes); extern void check_exclusion_constraint(Relation heap, Relation index, IndexInfo *indexInfo, - ItemPointer tupleid, + const ItemPointerData *tupleid, const Datum *values, const bool *isnull, EState *estate, bool newIndex); @@ -759,7 +781,18 @@ extern bool RelationFindReplTupleByIndex(Relation rel, Oid idxoid, TupleTableSlot *outslot); extern bool RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode, TupleTableSlot *searchslot, TupleTableSlot *outslot); - +extern bool RelationFindDeletedTupleInfoSeq(Relation rel, + TupleTableSlot *searchslot, + TransactionId oldestxmin, + TransactionId *delete_xid, + ReplOriginId *delete_origin, + TimestampTz *delete_time); +extern bool RelationFindDeletedTupleInfoByIndex(Relation rel, Oid idxoid, + TupleTableSlot *searchslot, + TransactionId oldestxmin, + TransactionId *delete_xid, + ReplOriginId *delete_origin, + TimestampTz *delete_time); extern void ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo, EState *estate, TupleTableSlot *slot); extern void ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo, @@ -770,8 +803,8 @@ extern void ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo, TupleTableSlot *searchslot); extern void CheckCmdReplicaIdentity(Relation rel, CmdType cmd); -extern void CheckSubscriptionRelkind(char relkind, const char *nspname, - const char *relname); +extern void CheckSubscriptionRelkind(char localrelkind, char remoterelkind, + const char *nspname, const char *relname); /* * prototypes from functions in nodeModifyTable.c diff --git a/src/include/executor/functions.h b/src/include/executor/functions.h index 58bdff9b0392d..75bd000ba4926 100644 --- a/src/include/executor/functions.h +++ b/src/include/executor/functions.h @@ -4,7 +4,7 @@ * Declarations for execution of SQL-language functions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/functions.h diff --git a/src/include/executor/hashjoin.h b/src/include/executor/hashjoin.h index ecff4842fd38d..4d342174b9a51 100644 --- a/src/include/executor/hashjoin.h +++ b/src/include/executor/hashjoin.h @@ -4,7 +4,7 @@ * internal structures for hash joins * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/hashjoin.h @@ -19,6 +19,8 @@ #include "storage/barrier.h" #include "storage/buffile.h" #include "storage/lwlock.h" +#include "utils/dsa.h" +#include "utils/sharedtuplestore.h" /* ---------------------------------------------------------------- * hash-join hash table structures @@ -68,6 +70,15 @@ * inner batch file. Subsequently, while reading either inner or outer batch * files, we might find tuples that no longer belong to the current batch; * if so, we just dump them out to the correct batch file. + * + * If an input tuple has a null join key, then it cannot match anything from + * the other side of the join. Normally we can just discard such a tuple + * immediately, but if it comes from the outer side of an outer join then we + * must emit it with null-extension of the other side. For various reasons + * it's not convenient to do that immediately on seeing the tuple, so we dump + * the tuple into a tuplestore and emit it later. (In the unlikely but + * supported case of a non-strict join operator, we treat null keys as normal + * data.) * ---------------------------------------------------------------- */ @@ -327,9 +338,16 @@ typedef struct HashJoinTableData bool growEnabled; /* flag to shut off nbatch increases */ - double totalTuples; /* # tuples obtained from inner plan */ - double partialTuples; /* # tuples obtained from inner plan by me */ - double skewTuples; /* # tuples inserted into skew tuples */ + /* + * totalTuples is the running total of tuples inserted into either the + * main or skew hash tables. reportTuples is the number of tuples that we + * want EXPLAIN to show as output from the Hash node (this includes saved + * null-keyed tuples as well as those inserted into the hash tables). + * skewTuples is the number of tuples present in the skew hash table. + */ + double totalTuples; + double reportTuples; + double skewTuples; /* * These arrays are allocated for the life of the hash join, but only if diff --git a/src/include/executor/instrument.h b/src/include/executor/instrument.h index 03653ab6c6cde..f093a52aae013 100644 --- a/src/include/executor/instrument.h +++ b/src/include/executor/instrument.h @@ -4,7 +4,7 @@ * definitions for run-time statistics collection * * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/executor/instrument.h * @@ -53,6 +53,7 @@ typedef struct WalUsage int64 wal_records; /* # of WAL records produced */ int64 wal_fpi; /* # of WAL full page images produced */ uint64 wal_bytes; /* size of WAL records produced */ + uint64 wal_fpi_bytes; /* size of WAL full page images produced */ int64 wal_buffers_full; /* # of times the WAL buffers became full */ } WalUsage; @@ -63,53 +64,89 @@ typedef enum InstrumentOption INSTRUMENT_BUFFERS = 1 << 1, /* needs buffer usage */ INSTRUMENT_ROWS = 1 << 2, /* needs row count */ INSTRUMENT_WAL = 1 << 3, /* needs WAL usage */ + INSTRUMENT_IO = 1 << 4, /* needs IO usage */ INSTRUMENT_ALL = PG_INT32_MAX } InstrumentOption; +/* + * General purpose instrumentation that can capture time and WAL/buffer usage + * + * Initialized through InstrAlloc, followed by one or more calls to a pair of + * InstrStart/InstrStop (activity is measured in between). + */ typedef struct Instrumentation { - /* Parameters set at node creation: */ + /* Parameters set at creation: */ bool need_timer; /* true if we need timer data */ bool need_bufusage; /* true if we need buffer usage data */ bool need_walusage; /* true if we need WAL usage data */ + /* Internal state keeping: */ + instr_time starttime; /* start time of last InstrStart */ + BufferUsage bufusage_start; /* buffer usage at start */ + WalUsage walusage_start; /* WAL usage at start */ + /* Accumulated statistics: */ + instr_time total; /* total runtime */ + BufferUsage bufusage; /* total buffer usage */ + WalUsage walusage; /* total WAL usage */ +} Instrumentation; + +/* + * Specialized instrumentation for per-node execution statistics + */ +typedef struct NodeInstrumentation +{ + Instrumentation instr; + /* Parameters set at node creation: */ bool async_mode; /* true if node is in async mode */ /* Info about current plan cycle: */ bool running; /* true if we've completed first tuple */ - instr_time starttime; /* start time of current iteration of node */ instr_time counter; /* accumulated runtime for this node */ - double firsttuple; /* time for first tuple of this cycle */ + instr_time firsttuple; /* time for first tuple of this cycle */ double tuplecount; /* # of tuples emitted so far this cycle */ - BufferUsage bufusage_start; /* buffer usage at start */ - WalUsage walusage_start; /* WAL usage at start */ /* Accumulated statistics across all completed cycles: */ - double startup; /* total startup time (in seconds) */ - double total; /* total time (in seconds) */ + instr_time startup; /* total startup time */ double ntuples; /* total tuples produced */ double ntuples2; /* secondary node-specific tuple counter */ double nloops; /* # of run cycles for this node */ double nfiltered1; /* # of tuples removed by scanqual or joinqual */ double nfiltered2; /* # of tuples removed by "other" quals */ - BufferUsage bufusage; /* total buffer usage */ - WalUsage walusage; /* total WAL usage */ -} Instrumentation; +} NodeInstrumentation; -typedef struct WorkerInstrumentation +typedef struct WorkerNodeInstrumentation { int num_workers; /* # of structures that follow */ - Instrumentation instrument[FLEXIBLE_ARRAY_MEMBER]; -} WorkerInstrumentation; + NodeInstrumentation instrument[FLEXIBLE_ARRAY_MEMBER]; +} WorkerNodeInstrumentation; + +typedef struct TriggerInstrumentation +{ + Instrumentation instr; + int64 firings; /* number of times the instrumented trigger + * was fired */ +} TriggerInstrumentation; extern PGDLLIMPORT BufferUsage pgBufferUsage; extern PGDLLIMPORT WalUsage pgWalUsage; -extern Instrumentation *InstrAlloc(int n, int instrument_options, - bool async_mode); -extern void InstrInit(Instrumentation *instr, int instrument_options); -extern void InstrStartNode(Instrumentation *instr); -extern void InstrStopNode(Instrumentation *instr, double nTuples); -extern void InstrUpdateTupleCount(Instrumentation *instr, double nTuples); -extern void InstrEndLoop(Instrumentation *instr); -extern void InstrAggNode(Instrumentation *dst, Instrumentation *add); +extern Instrumentation *InstrAlloc(int instrument_options); +extern void InstrInitOptions(Instrumentation *instr, int instrument_options); +extern void InstrStart(Instrumentation *instr); +extern void InstrStop(Instrumentation *instr); + +extern NodeInstrumentation *InstrAllocNode(int instrument_options, + bool async_mode); +extern void InstrInitNode(NodeInstrumentation *instr, int instrument_options, + bool async_mode); +extern void InstrStartNode(NodeInstrumentation *instr); +extern void InstrStopNode(NodeInstrumentation *instr, double nTuples); +extern void InstrUpdateTupleCount(NodeInstrumentation *instr, double nTuples); +extern void InstrEndLoop(NodeInstrumentation *instr); +extern void InstrAggNode(NodeInstrumentation *dst, NodeInstrumentation *add); + +extern TriggerInstrumentation *InstrAllocTrigger(int n, int instrument_options); +extern void InstrStartTrigger(TriggerInstrumentation *tginstr); +extern void InstrStopTrigger(TriggerInstrumentation *tginstr, int64 firings); + extern void InstrStartParallelQuery(void); extern void InstrEndParallelQuery(BufferUsage *bufusage, WalUsage *walusage); extern void InstrAccumParallelQuery(BufferUsage *bufusage, WalUsage *walusage); diff --git a/src/include/executor/instrument_node.h b/src/include/executor/instrument_node.h new file mode 100644 index 0000000000000..4076990408ef7 --- /dev/null +++ b/src/include/executor/instrument_node.h @@ -0,0 +1,306 @@ +/*------------------------------------------------------------------------- + * + * instrument_node.h + * Definitions for node-specific support for parallel query instrumentation + * + * These structs purposely contain no pointers because they are copied + * across processes during parallel query execution. Each worker copies its + * individual information into the container struct at executor shutdown time, + * to allow the leader to display the information in EXPLAIN ANALYZE. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/executor/instrument_node.h + * + *------------------------------------------------------------------------- + */ +#ifndef INSTRUMENT_NODE_H +#define INSTRUMENT_NODE_H + +/* + * Offset added to plan_node_id to create a second TOC key for per-worker scan + * instrumentation. Instrumentation and parallel-awareness are independent, so + * separate DSM chunks let each be allocated and initialized only when needed. + * In the future, if nodes need more DSM allocations, we would need a more + * robust system. + */ +#define PARALLEL_KEY_SCAN_INSTRUMENT_OFFSET UINT64CONST(0xD000000000000000) + +/* --------------------- + * Instrumentation information for aggregate function execution + * --------------------- + */ +typedef struct AggregateInstrumentation +{ + Size hash_mem_peak; /* peak hash table memory usage */ + uint64 hash_disk_used; /* kB of disk space used */ + int hash_batches_used; /* batches used during entire execution */ +} AggregateInstrumentation; + +/* + * Shared memory container for per-worker aggregate information + */ +typedef struct SharedAggInfo +{ + int num_workers; + AggregateInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedAggInfo; + + +/* --------------------- + * Instrumentation information about read streams and I/O + * --------------------- + */ +typedef struct IOStats +{ + /* number of buffers returned to consumer (for averaging distance) */ + uint64 prefetch_count; + + /* sum of pinned_buffers sampled at each buffer return */ + uint64 distance_sum; + + /* maximum actual pinned_buffers observed during the scan */ + int16 distance_max; + + /* maximum possible look-ahead distance (max_pinned_buffers) */ + int16 distance_capacity; + + /* number of waits for a read (for the I/O) */ + uint64 wait_count; + + /* I/O stats */ + uint64 io_count; /* number of I/Os */ + uint64 io_nblocks; /* sum of blocks for all I/Os */ + uint64 io_in_progress; /* sum of in-progress I/Os */ +} IOStats; + +typedef struct TableScanInstrumentation +{ + IOStats io; +} TableScanInstrumentation; + +/* merge IO statistics from 'src' into 'dst' */ +static inline void +AccumulateIOStats(IOStats *dst, IOStats *src) +{ + dst->prefetch_count += src->prefetch_count; + dst->distance_sum += src->distance_sum; + if (src->distance_max > dst->distance_max) + dst->distance_max = src->distance_max; + if (src->distance_capacity > dst->distance_capacity) + dst->distance_capacity = src->distance_capacity; + dst->wait_count += src->wait_count; + dst->io_count += src->io_count; + dst->io_nblocks += src->io_nblocks; + dst->io_in_progress += src->io_in_progress; +} + + +/* --------------------- + * Instrumentation information for indexscans (amgettuple and amgetbitmap) + * --------------------- + */ +typedef struct IndexScanInstrumentation +{ + /* Index search count (incremented with pgstat_count_index_scan call) */ + uint64 nsearches; +} IndexScanInstrumentation; + +/* + * Shared memory container for per-worker information + */ +typedef struct SharedIndexScanInstrumentation +{ + int num_workers; + IndexScanInstrumentation winstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedIndexScanInstrumentation; + + +/* --------------------- + * Instrumentation information for bitmap heap scans + * + * exact_pages total number of exact pages retrieved + * lossy_pages total number of lossy pages retrieved + * --------------------- + */ +typedef struct BitmapHeapScanInstrumentation +{ + uint64 exact_pages; + uint64 lossy_pages; + TableScanInstrumentation stats; +} BitmapHeapScanInstrumentation; + +/* + * Shared memory container for per-worker information + */ +typedef struct SharedBitmapHeapInstrumentation +{ + int num_workers; + BitmapHeapScanInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedBitmapHeapInstrumentation; + + +/* --------------------- + * Instrumentation information for Memoize + * --------------------- + */ +typedef struct MemoizeInstrumentation +{ + uint64 cache_hits; /* number of rescans where we've found the + * scan parameters values to be cached */ + uint64 cache_misses; /* number of rescans where we've not found the + * scan parameters values to be cached */ + uint64 cache_evictions; /* number of cache entries removed due to + * the need to free memory */ + uint64 cache_overflows; /* number of times we've had to bypass the + * cache when filling it due to not being + * able to free enough space to store the + * current scan's tuples */ + uint64 mem_peak; /* peak memory usage in bytes */ +} MemoizeInstrumentation; + +/* + * Shared memory container for per-worker memoize information + */ +typedef struct SharedMemoizeInfo +{ + int num_workers; + MemoizeInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedMemoizeInfo; + + +/* --------------------- + * Instrumentation information for Sorts. + * --------------------- + */ + +typedef enum +{ + SORT_SPACE_TYPE_DISK, + SORT_SPACE_TYPE_MEMORY, +} TuplesortSpaceType; + +/* + * The parallel-sort infrastructure relies on having a zero TuplesortMethod + * to indicate that a worker never did anything, so we assign zero to + * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be + * OR'ed together to represent a situation where different workers used + * different methods, so we need a separate bit for each one. Keep the + * NUM_TUPLESORTMETHODS constant in sync with the number of bits! + */ +typedef enum +{ + SORT_TYPE_STILL_IN_PROGRESS = 0, + SORT_TYPE_TOP_N_HEAPSORT = 1 << 0, + SORT_TYPE_QUICKSORT = 1 << 1, + SORT_TYPE_EXTERNAL_SORT = 1 << 2, + SORT_TYPE_EXTERNAL_MERGE = 1 << 3, +} TuplesortMethod; +#define NUM_TUPLESORTMETHODS 4 + +typedef struct TuplesortInstrumentation +{ + TuplesortMethod sortMethod; /* sort algorithm used */ + TuplesortSpaceType spaceType; /* type of space spaceUsed represents */ + int64 spaceUsed; /* space consumption, in kB */ +} TuplesortInstrumentation; + +/* + * Shared memory container for per-worker sort information + */ +typedef struct SharedSortInfo +{ + int num_workers; + TuplesortInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedSortInfo; + + +/* --------------------- + * Instrumentation information for nodeHash.c + * --------------------- + */ +typedef struct HashInstrumentation +{ + int nbuckets; /* number of buckets at end of execution */ + int nbuckets_original; /* planned number of buckets */ + int nbatch; /* number of batches at end of execution */ + int nbatch_original; /* planned number of batches */ + Size space_peak; /* peak memory usage in bytes */ +} HashInstrumentation; + +/* + * Shared memory container for per-worker information + */ +typedef struct SharedHashInfo +{ + int num_workers; + HashInstrumentation hinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedHashInfo; + + +/* --------------------- + * Instrumentation information for IncrementalSort + * --------------------- + */ +typedef struct IncrementalSortGroupInfo +{ + int64 groupCount; + int64 maxDiskSpaceUsed; + int64 totalDiskSpaceUsed; + int64 maxMemorySpaceUsed; + int64 totalMemorySpaceUsed; + uint32 sortMethods; /* bitmask of TuplesortMethod */ +} IncrementalSortGroupInfo; + +typedef struct IncrementalSortInfo +{ + IncrementalSortGroupInfo fullsortGroupInfo; + IncrementalSortGroupInfo prefixsortGroupInfo; +} IncrementalSortInfo; + +/* Shared memory container for per-worker incremental sort information */ +typedef struct SharedIncrementalSortInfo +{ + int num_workers; + IncrementalSortInfo sinfo[FLEXIBLE_ARRAY_MEMBER]; +} SharedIncrementalSortInfo; + + +/* --------------------- + * Instrumentation information for sequential scans + * --------------------- + */ +typedef struct SeqScanInstrumentation +{ + TableScanInstrumentation stats; +} SeqScanInstrumentation; + +/* + * Shared memory container for per-worker information + */ +typedef struct SharedSeqScanInstrumentation +{ + int num_workers; + SeqScanInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedSeqScanInstrumentation; + + +/* + * Instrumentation information for TID range scans + */ +typedef struct TidRangeScanInstrumentation +{ + TableScanInstrumentation stats; +} TidRangeScanInstrumentation; + +/* + * Shared memory container for per-worker information + */ +typedef struct SharedTidRangeScanInstrumentation +{ + int num_workers; + TidRangeScanInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; +} SharedTidRangeScanInstrumentation; + +#endif /* INSTRUMENT_NODE_H */ diff --git a/src/include/executor/nodeAgg.h b/src/include/executor/nodeAgg.h index 34b82d0f5d17d..1e1be9666ae50 100644 --- a/src/include/executor/nodeAgg.h +++ b/src/include/executor/nodeAgg.h @@ -4,7 +4,7 @@ * prototypes for nodeAgg.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeAgg.h @@ -173,7 +173,7 @@ typedef struct AggStatePerTransData FunctionCallInfo serialfn_fcinfo; FunctionCallInfo deserialfn_fcinfo; -} AggStatePerTransData; +} AggStatePerTransData; /* * AggStatePerAggData - per-aggregate information @@ -229,7 +229,7 @@ typedef struct AggStatePerAggData * aggregates because the final function is read-write. */ bool shareable; -} AggStatePerAggData; +} AggStatePerAggData; /* * AggStatePerGroupData - per-aggregate-per-group working state @@ -297,7 +297,7 @@ typedef struct AggStatePerPhaseData *---------- */ ExprState *evaltrans_cache[2][2]; -} AggStatePerPhaseData; +} AggStatePerPhaseData; /* * AggStatePerHashData - per-hashtable state @@ -319,7 +319,7 @@ typedef struct AggStatePerHashData AttrNumber *hashGrpColIdxInput; /* hash col indices in input slot */ AttrNumber *hashGrpColIdxHash; /* indices in hash table tuples */ Agg *aggnode; /* original Agg node, for numGroups etc. */ -} AggStatePerHashData; +} AggStatePerHashData; extern AggState *ExecInitAgg(Agg *node, EState *estate, int eflags); diff --git a/src/include/executor/nodeAppend.h b/src/include/executor/nodeAppend.h index 3c90847a32c87..7a52a92d53b66 100644 --- a/src/include/executor/nodeAppend.h +++ b/src/include/executor/nodeAppend.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeAppend.h diff --git a/src/include/executor/nodeBitmapAnd.h b/src/include/executor/nodeBitmapAnd.h index 99518736ec6eb..061d0e67c2a0a 100644 --- a/src/include/executor/nodeBitmapAnd.h +++ b/src/include/executor/nodeBitmapAnd.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeBitmapAnd.h diff --git a/src/include/executor/nodeBitmapHeapscan.h b/src/include/executor/nodeBitmapHeapscan.h index e241ac06f9d69..5c0b6592a1f77 100644 --- a/src/include/executor/nodeBitmapHeapscan.h +++ b/src/include/executor/nodeBitmapHeapscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeBitmapHeapscan.h @@ -28,6 +28,12 @@ extern void ExecBitmapHeapReInitializeDSM(BitmapHeapScanState *node, ParallelContext *pcxt); extern void ExecBitmapHeapInitializeWorker(BitmapHeapScanState *node, ParallelWorkerContext *pwcxt); +extern void ExecBitmapHeapInstrumentEstimate(BitmapHeapScanState *node, + ParallelContext *pcxt); +extern void ExecBitmapHeapInstrumentInitDSM(BitmapHeapScanState *node, + ParallelContext *pcxt); +extern void ExecBitmapHeapInstrumentInitWorker(BitmapHeapScanState *node, + ParallelWorkerContext *pwcxt); extern void ExecBitmapHeapRetrieveInstrumentation(BitmapHeapScanState *node); #endif /* NODEBITMAPHEAPSCAN_H */ diff --git a/src/include/executor/nodeBitmapIndexscan.h b/src/include/executor/nodeBitmapIndexscan.h index b6a5ae25ed19c..5a43bda943d1f 100644 --- a/src/include/executor/nodeBitmapIndexscan.h +++ b/src/include/executor/nodeBitmapIndexscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeBitmapIndexscan.h diff --git a/src/include/executor/nodeBitmapOr.h b/src/include/executor/nodeBitmapOr.h index 3ee94d334d19f..8c0977ffc300b 100644 --- a/src/include/executor/nodeBitmapOr.h +++ b/src/include/executor/nodeBitmapOr.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeBitmapOr.h diff --git a/src/include/executor/nodeCtescan.h b/src/include/executor/nodeCtescan.h index 007fede3d9dd1..6b16256b54d4d 100644 --- a/src/include/executor/nodeCtescan.h +++ b/src/include/executor/nodeCtescan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeCtescan.h diff --git a/src/include/executor/nodeCustom.h b/src/include/executor/nodeCustom.h index bc0ca512e090a..fb0acc6e414e9 100644 --- a/src/include/executor/nodeCustom.h +++ b/src/include/executor/nodeCustom.h @@ -4,7 +4,7 @@ * * prototypes for CustomScan nodes * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * ------------------------------------------------------------------------ diff --git a/src/include/executor/nodeForeignscan.h b/src/include/executor/nodeForeignscan.h index 3e55e0fa5974b..4a14e67ed08ed 100644 --- a/src/include/executor/nodeForeignscan.h +++ b/src/include/executor/nodeForeignscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeForeignscan.h diff --git a/src/include/executor/nodeFunctionscan.h b/src/include/executor/nodeFunctionscan.h index 10b06ad96365d..e6a1a42d3f913 100644 --- a/src/include/executor/nodeFunctionscan.h +++ b/src/include/executor/nodeFunctionscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeFunctionscan.h diff --git a/src/include/executor/nodeGather.h b/src/include/executor/nodeGather.h index 2961a9bc57fc2..38325fa6b959d 100644 --- a/src/include/executor/nodeGather.h +++ b/src/include/executor/nodeGather.h @@ -4,7 +4,7 @@ * prototypes for nodeGather.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeGather.h diff --git a/src/include/executor/nodeGatherMerge.h b/src/include/executor/nodeGatherMerge.h index 56076f49676ff..54bc779ca0834 100644 --- a/src/include/executor/nodeGatherMerge.h +++ b/src/include/executor/nodeGatherMerge.h @@ -4,7 +4,7 @@ * prototypes for nodeGatherMerge.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeGatherMerge.h diff --git a/src/include/executor/nodeGroup.h b/src/include/executor/nodeGroup.h index 0ead772fd499c..220221db7bde5 100644 --- a/src/include/executor/nodeGroup.h +++ b/src/include/executor/nodeGroup.h @@ -4,7 +4,7 @@ * prototypes for nodeGroup.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeGroup.h diff --git a/src/include/executor/nodeHash.h b/src/include/executor/nodeHash.h index 3c1a09415aa1e..9ff493b627a76 100644 --- a/src/include/executor/nodeHash.h +++ b/src/include/executor/nodeHash.h @@ -4,7 +4,7 @@ * prototypes for nodeHash.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeHash.h @@ -64,6 +64,7 @@ extern void ExecChooseHashTableSize(double ntuples, int tupwidth, bool useskew, int *numbatches, int *num_skew_mcvs); extern int ExecHashGetSkewBucket(HashJoinTable hashtable, uint32 hashvalue); +extern Tuplestorestate *ExecHashBuildNullTupleStore(HashJoinTable hashtable); extern void ExecHashEstimate(HashState *node, ParallelContext *pcxt); extern void ExecHashInitializeDSM(HashState *node, ParallelContext *pcxt); extern void ExecHashInitializeWorker(HashState *node, ParallelWorkerContext *pwcxt); diff --git a/src/include/executor/nodeHashjoin.h b/src/include/executor/nodeHashjoin.h index 646095b4dc65e..aebd39be8b522 100644 --- a/src/include/executor/nodeHashjoin.h +++ b/src/include/executor/nodeHashjoin.h @@ -4,7 +4,7 @@ * prototypes for nodeHashjoin.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeHashjoin.h diff --git a/src/include/executor/nodeIncrementalSort.h b/src/include/executor/nodeIncrementalSort.h index 12c16bca8a6f9..bcedb23cbdd2b 100644 --- a/src/include/executor/nodeIncrementalSort.h +++ b/src/include/executor/nodeIncrementalSort.h @@ -2,7 +2,7 @@ * * nodeIncrementalSort.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeIncrementalSort.h diff --git a/src/include/executor/nodeIndexonlyscan.h b/src/include/executor/nodeIndexonlyscan.h index ae85dee6d8f3a..b686244eb91c4 100644 --- a/src/include/executor/nodeIndexonlyscan.h +++ b/src/include/executor/nodeIndexonlyscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeIndexonlyscan.h @@ -32,6 +32,12 @@ extern void ExecIndexOnlyScanReInitializeDSM(IndexOnlyScanState *node, ParallelContext *pcxt); extern void ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *node, ParallelWorkerContext *pwcxt); +extern void ExecIndexOnlyScanInstrumentEstimate(IndexOnlyScanState *node, + ParallelContext *pcxt); +extern void ExecIndexOnlyScanInstrumentInitDSM(IndexOnlyScanState *node, + ParallelContext *pcxt); +extern void ExecIndexOnlyScanInstrumentInitWorker(IndexOnlyScanState *node, + ParallelWorkerContext *pwcxt); extern void ExecIndexOnlyScanRetrieveInstrumentation(IndexOnlyScanState *node); #endif /* NODEINDEXONLYSCAN_H */ diff --git a/src/include/executor/nodeIndexscan.h b/src/include/executor/nodeIndexscan.h index 08f0a148db6c6..c60cbc9a94b72 100644 --- a/src/include/executor/nodeIndexscan.h +++ b/src/include/executor/nodeIndexscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeIndexscan.h @@ -28,6 +28,12 @@ extern void ExecIndexScanInitializeDSM(IndexScanState *node, ParallelContext *pc extern void ExecIndexScanReInitializeDSM(IndexScanState *node, ParallelContext *pcxt); extern void ExecIndexScanInitializeWorker(IndexScanState *node, ParallelWorkerContext *pwcxt); +extern void ExecIndexScanInstrumentEstimate(IndexScanState *node, + ParallelContext *pcxt); +extern void ExecIndexScanInstrumentInitDSM(IndexScanState *node, + ParallelContext *pcxt); +extern void ExecIndexScanInstrumentInitWorker(IndexScanState *node, + ParallelWorkerContext *pwcxt); extern void ExecIndexScanRetrieveInstrumentation(IndexScanState *node); /* diff --git a/src/include/executor/nodeLimit.h b/src/include/executor/nodeLimit.h index 97a472c8f36a3..689cc7a490e6b 100644 --- a/src/include/executor/nodeLimit.h +++ b/src/include/executor/nodeLimit.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeLimit.h diff --git a/src/include/executor/nodeLockRows.h b/src/include/executor/nodeLockRows.h index ad7d63d32dc9d..4db017023f65b 100644 --- a/src/include/executor/nodeLockRows.h +++ b/src/include/executor/nodeLockRows.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeLockRows.h diff --git a/src/include/executor/nodeMaterial.h b/src/include/executor/nodeMaterial.h index 30c7a25dc0483..19b254db4c89a 100644 --- a/src/include/executor/nodeMaterial.h +++ b/src/include/executor/nodeMaterial.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeMaterial.h diff --git a/src/include/executor/nodeMemoize.h b/src/include/executor/nodeMemoize.h index 77a08f97d5008..aaf2928762c8c 100644 --- a/src/include/executor/nodeMemoize.h +++ b/src/include/executor/nodeMemoize.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2021-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeMemoize.h diff --git a/src/include/executor/nodeMergeAppend.h b/src/include/executor/nodeMergeAppend.h index 4eb05dc30d614..dfcf45099bad5 100644 --- a/src/include/executor/nodeMergeAppend.h +++ b/src/include/executor/nodeMergeAppend.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeMergeAppend.h diff --git a/src/include/executor/nodeMergejoin.h b/src/include/executor/nodeMergejoin.h index 5e79acadae848..382f770908e18 100644 --- a/src/include/executor/nodeMergejoin.h +++ b/src/include/executor/nodeMergejoin.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeMergejoin.h diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h index bf3b592e28fd1..f6070e1cdf3bc 100644 --- a/src/include/executor/nodeModifyTable.h +++ b/src/include/executor/nodeModifyTable.h @@ -3,7 +3,7 @@ * nodeModifyTable.h * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeModifyTable.h diff --git a/src/include/executor/nodeNamedtuplestorescan.h b/src/include/executor/nodeNamedtuplestorescan.h index abfe4a2789677..0fc608396de79 100644 --- a/src/include/executor/nodeNamedtuplestorescan.h +++ b/src/include/executor/nodeNamedtuplestorescan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeNamedtuplestorescan.h diff --git a/src/include/executor/nodeNestloop.h b/src/include/executor/nodeNestloop.h index 5a24cc9e8498d..7b64ff846db74 100644 --- a/src/include/executor/nodeNestloop.h +++ b/src/include/executor/nodeNestloop.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeNestloop.h diff --git a/src/include/executor/nodeProjectSet.h b/src/include/executor/nodeProjectSet.h index b0c7df0fb8068..388371a5062c2 100644 --- a/src/include/executor/nodeProjectSet.h +++ b/src/include/executor/nodeProjectSet.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeProjectSet.h diff --git a/src/include/executor/nodeRecursiveunion.h b/src/include/executor/nodeRecursiveunion.h index 7a5f20311b694..314ffb452a4f8 100644 --- a/src/include/executor/nodeRecursiveunion.h +++ b/src/include/executor/nodeRecursiveunion.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeRecursiveunion.h diff --git a/src/include/executor/nodeResult.h b/src/include/executor/nodeResult.h index 575e3639a9bc5..8f5c5beaa7ed2 100644 --- a/src/include/executor/nodeResult.h +++ b/src/include/executor/nodeResult.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeResult.h diff --git a/src/include/executor/nodeSamplescan.h b/src/include/executor/nodeSamplescan.h index 48fa135bb8010..d08d2be44e2cd 100644 --- a/src/include/executor/nodeSamplescan.h +++ b/src/include/executor/nodeSamplescan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeSamplescan.h diff --git a/src/include/executor/nodeSeqscan.h b/src/include/executor/nodeSeqscan.h index 3adad8b585b22..9c0ad4879d7cb 100644 --- a/src/include/executor/nodeSeqscan.h +++ b/src/include/executor/nodeSeqscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeSeqscan.h @@ -28,4 +28,13 @@ extern void ExecSeqScanReInitializeDSM(SeqScanState *node, ParallelContext *pcxt extern void ExecSeqScanInitializeWorker(SeqScanState *node, ParallelWorkerContext *pwcxt); +/* instrument support */ +extern void ExecSeqScanInstrumentEstimate(SeqScanState *node, + ParallelContext *pcxt); +extern void ExecSeqScanInstrumentInitDSM(SeqScanState *node, + ParallelContext *pcxt); +extern void ExecSeqScanInstrumentInitWorker(SeqScanState *node, + ParallelWorkerContext *pwcxt); +extern void ExecSeqScanRetrieveInstrumentation(SeqScanState *node); + #endif /* NODESEQSCAN_H */ diff --git a/src/include/executor/nodeSetOp.h b/src/include/executor/nodeSetOp.h index 024c6ba1fceb8..5f88bc78eb411 100644 --- a/src/include/executor/nodeSetOp.h +++ b/src/include/executor/nodeSetOp.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeSetOp.h @@ -20,4 +20,6 @@ extern SetOpState *ExecInitSetOp(SetOp *node, EState *estate, int eflags); extern void ExecEndSetOp(SetOpState *node); extern void ExecReScanSetOp(SetOpState *node); +extern Size EstimateSetOpHashTableSpace(double nentries, Size tupleWidth); + #endif /* NODESETOP_H */ diff --git a/src/include/executor/nodeSort.h b/src/include/executor/nodeSort.h index f3b7f1895b58e..ef4173114e61b 100644 --- a/src/include/executor/nodeSort.h +++ b/src/include/executor/nodeSort.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeSort.h diff --git a/src/include/executor/nodeSubplan.h b/src/include/executor/nodeSubplan.h index a1cafbcc694d1..56f82569244b7 100644 --- a/src/include/executor/nodeSubplan.h +++ b/src/include/executor/nodeSubplan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeSubplan.h @@ -20,6 +20,10 @@ extern SubPlanState *ExecInitSubPlan(SubPlan *subplan, PlanState *parent); extern Datum ExecSubPlan(SubPlanState *node, ExprContext *econtext, bool *isNull); +extern Size EstimateSubplanHashTableSpace(double nentries, + Size tupleWidth, + bool unknownEqFalse); + extern void ExecReScanSetParamPlan(SubPlanState *node, PlanState *parent); extern void ExecSetParamPlan(SubPlanState *node, ExprContext *econtext); diff --git a/src/include/executor/nodeSubqueryscan.h b/src/include/executor/nodeSubqueryscan.h index 8276a728e294a..914edb9af12fa 100644 --- a/src/include/executor/nodeSubqueryscan.h +++ b/src/include/executor/nodeSubqueryscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeSubqueryscan.h diff --git a/src/include/executor/nodeTableFuncscan.h b/src/include/executor/nodeTableFuncscan.h index eb71a18a42be3..b2298885a1f13 100644 --- a/src/include/executor/nodeTableFuncscan.h +++ b/src/include/executor/nodeTableFuncscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeTableFuncscan.h diff --git a/src/include/executor/nodeTidrangescan.h b/src/include/executor/nodeTidrangescan.h index a831f1202cae3..9e7d0a357bbd0 100644 --- a/src/include/executor/nodeTidrangescan.h +++ b/src/include/executor/nodeTidrangescan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeTidrangescan.h @@ -14,6 +14,7 @@ #ifndef NODETIDRANGESCAN_H #define NODETIDRANGESCAN_H +#include "access/parallel.h" #include "nodes/execnodes.h" extern TidRangeScanState *ExecInitTidRangeScan(TidRangeScan *node, @@ -21,4 +22,19 @@ extern TidRangeScanState *ExecInitTidRangeScan(TidRangeScan *node, extern void ExecEndTidRangeScan(TidRangeScanState *node); extern void ExecReScanTidRangeScan(TidRangeScanState *node); +/* parallel scan support */ +extern void ExecTidRangeScanEstimate(TidRangeScanState *node, ParallelContext *pcxt); +extern void ExecTidRangeScanInitializeDSM(TidRangeScanState *node, ParallelContext *pcxt); +extern void ExecTidRangeScanReInitializeDSM(TidRangeScanState *node, ParallelContext *pcxt); +extern void ExecTidRangeScanInitializeWorker(TidRangeScanState *node, ParallelWorkerContext *pwcxt); + +/* instrument support */ +extern void ExecTidRangeScanInstrumentEstimate(TidRangeScanState *node, + ParallelContext *pcxt); +extern void ExecTidRangeScanInstrumentInitDSM(TidRangeScanState *node, + ParallelContext *pcxt); +extern void ExecTidRangeScanInstrumentInitWorker(TidRangeScanState *node, + ParallelWorkerContext *pwcxt); +extern void ExecTidRangeScanRetrieveInstrumentation(TidRangeScanState *node); + #endif /* NODETIDRANGESCAN_H */ diff --git a/src/include/executor/nodeTidscan.h b/src/include/executor/nodeTidscan.h index 5ef5781bd7115..65f8587986fd7 100644 --- a/src/include/executor/nodeTidscan.h +++ b/src/include/executor/nodeTidscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeTidscan.h diff --git a/src/include/executor/nodeUnique.h b/src/include/executor/nodeUnique.h index 2964b50a9bb30..3a8fe211512fd 100644 --- a/src/include/executor/nodeUnique.h +++ b/src/include/executor/nodeUnique.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeUnique.h diff --git a/src/include/executor/nodeValuesscan.h b/src/include/executor/nodeValuesscan.h index 4a09fcce63dd2..b5e72cf682772 100644 --- a/src/include/executor/nodeValuesscan.h +++ b/src/include/executor/nodeValuesscan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeValuesscan.h diff --git a/src/include/executor/nodeWindowAgg.h b/src/include/executor/nodeWindowAgg.h index 2e96683307613..ada4a1c458c21 100644 --- a/src/include/executor/nodeWindowAgg.h +++ b/src/include/executor/nodeWindowAgg.h @@ -4,7 +4,7 @@ * prototypes for nodeWindowAgg.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeWindowAgg.h diff --git a/src/include/executor/nodeWorktablescan.h b/src/include/executor/nodeWorktablescan.h index f21e6524abdf0..a9fc8a67c1544 100644 --- a/src/include/executor/nodeWorktablescan.h +++ b/src/include/executor/nodeWorktablescan.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/nodeWorktablescan.h diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h index d064d1a9b76f9..f4985cb715d9f 100644 --- a/src/include/executor/spi.h +++ b/src/include/executor/spi.h @@ -3,7 +3,7 @@ * spi.h * Server Programming Interface public declarations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/spi.h @@ -111,7 +111,7 @@ extern int SPI_finish(void); extern int SPI_execute(const char *src, bool read_only, long tcount); extern int SPI_execute_extended(const char *src, const SPIExecuteOptions *options); -extern int SPI_execute_plan(SPIPlanPtr plan, Datum *Values, const char *Nulls, +extern int SPI_execute_plan(SPIPlanPtr plan, const Datum *Values, const char *Nulls, bool read_only, long tcount); extern int SPI_execute_plan_extended(SPIPlanPtr plan, const SPIExecuteOptions *options); @@ -122,13 +122,13 @@ extern int SPI_exec(const char *src, long tcount); extern int SPI_execp(SPIPlanPtr plan, Datum *Values, const char *Nulls, long tcount); extern int SPI_execute_snapshot(SPIPlanPtr plan, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, Snapshot snapshot, Snapshot crosscheck_snapshot, bool read_only, bool fire_triggers, long tcount); extern int SPI_execute_with_args(const char *src, int nargs, Oid *argtypes, - Datum *Values, const char *Nulls, + const Datum *Values, const char *Nulls, bool read_only, long tcount); extern SPIPlanPtr SPI_prepare(const char *src, int nargs, Oid *argtypes); extern SPIPlanPtr SPI_prepare_cursor(const char *src, int nargs, Oid *argtypes, @@ -172,7 +172,7 @@ extern void SPI_freetuple(HeapTuple tuple); extern void SPI_freetuptable(SPITupleTable *tuptable); extern Portal SPI_cursor_open(const char *name, SPIPlanPtr plan, - Datum *Values, const char *Nulls, bool read_only); + const Datum *Values, const char *Nulls, bool read_only); extern Portal SPI_cursor_open_with_args(const char *name, const char *src, int nargs, Oid *argtypes, diff --git a/src/include/executor/spi_priv.h b/src/include/executor/spi_priv.h index cc6688d5940e9..240383b97b9db 100644 --- a/src/include/executor/spi_priv.h +++ b/src/include/executor/spi_priv.h @@ -3,7 +3,7 @@ * spi_priv.h * Server Programming Interface private declarations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/spi_priv.h diff --git a/src/include/executor/tablefunc.h b/src/include/executor/tablefunc.h index 2c4498c5955e7..531609771a593 100644 --- a/src/include/executor/tablefunc.h +++ b/src/include/executor/tablefunc.h @@ -3,7 +3,7 @@ * tablefunc.h * interface for TableFunc executor node * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/tablefunc.h @@ -14,7 +14,7 @@ #define _TABLEFUNC_H /* Forward-declare this to avoid including execnodes.h here */ -struct TableFuncScanState; +typedef struct TableFuncScanState TableFuncScanState; /* * TableFuncRoutine holds function pointers used for generating content of @@ -51,17 +51,17 @@ struct TableFuncScanState; */ typedef struct TableFuncRoutine { - void (*InitOpaque) (struct TableFuncScanState *state, int natts); - void (*SetDocument) (struct TableFuncScanState *state, Datum value); - void (*SetNamespace) (struct TableFuncScanState *state, const char *name, + void (*InitOpaque) (TableFuncScanState *state, int natts); + void (*SetDocument) (TableFuncScanState *state, Datum value); + void (*SetNamespace) (TableFuncScanState *state, const char *name, const char *uri); - void (*SetRowFilter) (struct TableFuncScanState *state, const char *path); - void (*SetColumnFilter) (struct TableFuncScanState *state, + void (*SetRowFilter) (TableFuncScanState *state, const char *path); + void (*SetColumnFilter) (TableFuncScanState *state, const char *path, int colnum); - bool (*FetchRow) (struct TableFuncScanState *state); - Datum (*GetValue) (struct TableFuncScanState *state, int colnum, + bool (*FetchRow) (TableFuncScanState *state); + Datum (*GetValue) (TableFuncScanState *state, int colnum, Oid typid, int32 typmod, bool *isnull); - void (*DestroyOpaque) (struct TableFuncScanState *state); + void (*DestroyOpaque) (TableFuncScanState *state); } TableFuncRoutine; #endif /* _TABLEFUNC_H */ diff --git a/src/include/executor/tqueue.h b/src/include/executor/tqueue.h index 57d68a49c5807..db2ef4f999388 100644 --- a/src/include/executor/tqueue.h +++ b/src/include/executor/tqueue.h @@ -3,7 +3,7 @@ * tqueue.h * Use shm_mq to send & receive tuples between parallel backends * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/tqueue.h diff --git a/src/include/executor/tstoreReceiver.h b/src/include/executor/tstoreReceiver.h index 11feca70b0e88..5ed69159e9fc0 100644 --- a/src/include/executor/tstoreReceiver.h +++ b/src/include/executor/tstoreReceiver.h @@ -4,7 +4,7 @@ * prototypes for tstoreReceiver.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/tstoreReceiver.h diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h index 095e4cc82e3d8..d7e8e72aeae67 100644 --- a/src/include/executor/tuptable.h +++ b/src/include/executor/tuptable.h @@ -4,7 +4,7 @@ * tuple table support stuff * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/executor/tuptable.h @@ -15,7 +15,6 @@ #define TUPTABLE_H #include "access/htup.h" -#include "access/htup_details.h" #include "access/sysattr.h" #include "access/tupdesc.h" #include "storage/buf.h" @@ -85,9 +84,6 @@ * tts_values/tts_isnull are allocated either when the slot is created (when * the descriptor is provided), or when a descriptor is assigned to the slot; * they are of length equal to the descriptor's natts. - * - * The TTS_FLAG_SLOW flag is saved state for - * slot_deform_heap_tuple, and should not be touched by any other code. *---------- */ @@ -99,14 +95,24 @@ #define TTS_FLAG_SHOULDFREE (1 << 2) #define TTS_SHOULDFREE(slot) (((slot)->tts_flags & TTS_FLAG_SHOULDFREE) != 0) -/* saved state for slot_deform_heap_tuple */ -#define TTS_FLAG_SLOW (1 << 3) -#define TTS_SLOW(slot) (((slot)->tts_flags & TTS_FLAG_SLOW) != 0) +/* + * true = slot's formed tuple guaranteed to not have NULLs in NOT NULLable + * columns. + */ +#define TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS (1 << 3) +#define TTS_OBEYS_NOT_NULL_CONSTRAINTS(slot) \ + (((slot)->tts_flags & TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS) != 0) /* fixed tuple descriptor */ #define TTS_FLAG_FIXED (1 << 4) #define TTS_FIXED(slot) (((slot)->tts_flags & TTS_FLAG_FIXED) != 0) +/* + * Defines which of the above flags should never be set in tts_flags when the + * TupleTableSlot is created. + */ +#define TTS_FLAGS_TRANSIENT (TTS_FLAG_EMPTY|TTS_FLAG_SHOULDFREE) + struct TupleTableSlotOps; typedef struct TupleTableSlotOps TupleTableSlotOps; @@ -124,7 +130,14 @@ typedef struct TupleTableSlot #define FIELDNO_TUPLETABLESLOT_VALUES 5 Datum *tts_values; /* current per-attribute values */ #define FIELDNO_TUPLETABLESLOT_ISNULL 6 - bool *tts_isnull; /* current per-attribute isnull flags */ + bool *tts_isnull; /* current per-attribute isnull flags. Array + * size must always be rounded up to the next + * multiple of 8 elements. */ + int tts_first_nonguaranteed; /* The value from the TupleDesc's + * firstNonGuaranteedAttr, or 0 + * when tts_flags does not contain + * TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS */ + MemoryContext tts_mcxt; /* slot itself is in this context */ ItemPointerData tts_tid; /* stored tuple's tid */ Oid tts_tableOid; /* table oid of tuple */ @@ -152,10 +165,12 @@ struct TupleTableSlotOps /* * Fill up first natts entries of tts_values and tts_isnull arrays with - * values from the tuple contained in the slot. The function may be called - * with natts more than the number of attributes available in the tuple, - * in which case it should set tts_nvalid to the number of returned - * columns. + * values from the tuple contained in the slot and set the slot's + * tts_nvalid to natts. The function may be called with an natts value + * more than the number of attributes available in the tuple, in which + * case the function must call slot_getmissingattrs() to populate the + * remaining attributes. The function must raise an ERROR if 'natts' is + * higher than the number of attributes in the slot's TupleDesc. */ void (*getsomeattrs) (TupleTableSlot *slot, int natts); @@ -312,9 +327,11 @@ typedef struct MinimalTupleTableSlot /* in executor/execTuples.c */ extern TupleTableSlot *MakeTupleTableSlot(TupleDesc tupleDesc, - const TupleTableSlotOps *tts_ops); + const TupleTableSlotOps *tts_ops, + uint16 flags); extern TupleTableSlot *ExecAllocTableSlot(List **tupleTable, TupleDesc desc, - const TupleTableSlotOps *tts_ops); + const TupleTableSlotOps *tts_ops, + uint16 flags); extern void ExecResetTupleTable(List *tupleTable, bool shouldFree); extern TupleTableSlot *MakeSingleTupleTableSlot(TupleDesc tupdesc, const TupleTableSlotOps *tts_ops); @@ -358,8 +375,9 @@ extern void slot_getsomeattrs_int(TupleTableSlot *slot, int attnum); static inline void slot_getsomeattrs(TupleTableSlot *slot, int attnum) { + /* Populate slot with attributes up to 'attnum', if it's not already */ if (slot->tts_nvalid < attnum) - slot_getsomeattrs_int(slot, attnum); + slot->tts_ops->getsomeattrs(slot, attnum); } /* diff --git a/src/include/fe_utils/archive.h b/src/include/fe_utils/archive.h index bd744c320d89d..4cf884831bfdc 100644 --- a/src/include/fe_utils/archive.h +++ b/src/include/fe_utils/archive.h @@ -3,7 +3,7 @@ * archive.h * Routines to access WAL archives from frontend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/archive.h diff --git a/src/include/fe_utils/astreamer.h b/src/include/fe_utils/astreamer.h index 0e0031741fa51..8329e4efbc522 100644 --- a/src/include/fe_utils/astreamer.h +++ b/src/include/fe_utils/astreamer.h @@ -21,7 +21,7 @@ * make further decisions about how to process the data; for example, * it might choose to modify the archive contents. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/fe_utils/astreamer.h @@ -83,8 +83,10 @@ typedef struct mode_t mode; uid_t uid; gid_t gid; + /* note: special filetypes will set none of these flags */ + bool is_regular; bool is_directory; - bool is_link; + bool is_symlink; char linktarget[MAXPGPATH]; } astreamer_member; diff --git a/src/include/fe_utils/cancel.h b/src/include/fe_utils/cancel.h index 977115fad4df8..e174fb83b921c 100644 --- a/src/include/fe_utils/cancel.h +++ b/src/include/fe_utils/cancel.h @@ -3,7 +3,7 @@ * Query cancellation support for frontend code * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/cancel.h diff --git a/src/include/fe_utils/conditional.h b/src/include/fe_utils/conditional.h index 435e97557555d..17b0ac837fb56 100644 --- a/src/include/fe_utils/conditional.h +++ b/src/include/fe_utils/conditional.h @@ -14,7 +14,7 @@ * a true branch?) so that the interpreter knows whether to execute * code and whether to evaluate conditions. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/include/fe_utils/conditional.h * diff --git a/src/include/fe_utils/connect_utils.h b/src/include/fe_utils/connect_utils.h index 26bd5064843db..eadc42f754001 100644 --- a/src/include/fe_utils/connect_utils.h +++ b/src/include/fe_utils/connect_utils.h @@ -2,7 +2,7 @@ * * Facilities for frontend code to connect to and disconnect from databases. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/connect_utils.h diff --git a/src/include/fe_utils/mbprint.h b/src/include/fe_utils/mbprint.h index c8e37969e7d31..c5e9c8ed7e2a0 100644 --- a/src/include/fe_utils/mbprint.h +++ b/src/include/fe_utils/mbprint.h @@ -3,7 +3,7 @@ * Multibyte character printing support for frontend code * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/mbprint.h diff --git a/src/include/fe_utils/option_utils.h b/src/include/fe_utils/option_utils.h index 4504bbb36a83c..d975db77af222 100644 --- a/src/include/fe_utils/option_utils.h +++ b/src/include/fe_utils/option_utils.h @@ -2,7 +2,7 @@ * * Command line option processing facilities for frontend code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/option_utils.h @@ -24,5 +24,11 @@ extern bool option_parse_int(const char *optarg, const char *optname, int *result); extern bool parse_sync_method(const char *optarg, DataDirSyncMethod *sync_method); +extern void check_mut_excl_opts_internal(int n,...); + +/* see comment for check_mut_excl_opts_internal() in option_utils.c for info */ +#define check_mut_excl_opts(set, opt, ...) \ + check_mut_excl_opts_internal(VA_ARGS_NARGS(__VA_ARGS__) + 2, \ + set, opt, __VA_ARGS__) #endif /* OPTION_UTILS_H */ diff --git a/src/include/fe_utils/parallel_slot.h b/src/include/fe_utils/parallel_slot.h index 7770a20de3456..a6ebe273ce033 100644 --- a/src/include/fe_utils/parallel_slot.h +++ b/src/include/fe_utils/parallel_slot.h @@ -3,7 +3,7 @@ * parallel_slot.h * Parallel support for bin/scripts/ * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/fe_utils/parallel_slot.h * @@ -58,6 +58,13 @@ ParallelSlotClearHandler(ParallelSlot *slot) slot->handler_context = NULL; } +static inline void +ParallelSlotSetIdle(ParallelSlot *slot) +{ + slot->inUse = false; + ParallelSlotClearHandler(slot); +} + extern ParallelSlot *ParallelSlotsGetIdle(ParallelSlotArray *sa, const char *dbname); diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h index c99c2ee1a31a2..94f6a59361999 100644 --- a/src/include/fe_utils/print.h +++ b/src/include/fe_utils/print.h @@ -3,7 +3,7 @@ * Query-result printing support for frontend code * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/print.h @@ -184,6 +184,8 @@ typedef struct printQueryOpt { printTableOpt topt; /* the options above */ char *nullPrint; /* how to print null entities */ + char *truePrint; /* how to print boolean true values */ + char *falsePrint; /* how to print boolean false values */ char *title; /* override title */ char **footers; /* override footer (default is "(xx rows)") */ bool translate_header; /* do gettext on column headers */ diff --git a/src/include/fe_utils/psqlscan.h b/src/include/fe_utils/psqlscan.h index 39d2065fe987b..abd44fa140bc4 100644 --- a/src/include/fe_utils/psqlscan.h +++ b/src/include/fe_utils/psqlscan.h @@ -10,7 +10,7 @@ * backslash commands. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/psqlscan.h diff --git a/src/include/fe_utils/psqlscan_int.h b/src/include/fe_utils/psqlscan_int.h index 2a3a9d7c82aaa..488f416f0e551 100644 --- a/src/include/fe_utils/psqlscan_int.h +++ b/src/include/fe_utils/psqlscan_int.h @@ -34,7 +34,7 @@ * same flex version, or if they don't use the same flex options. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/psqlscan_int.h @@ -51,14 +51,8 @@ * validity checking; in actual use, this file should always be included * from the body of a flex file, where these symbols are already defined. */ -#ifndef YY_TYPEDEF_YY_BUFFER_STATE -#define YY_TYPEDEF_YY_BUFFER_STATE typedef struct yy_buffer_state *YY_BUFFER_STATE; -#endif -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif /* * We use a stack of flex buffers to handle substitution of psql variables. diff --git a/src/include/fe_utils/query_utils.h b/src/include/fe_utils/query_utils.h index 4d7f7d6057330..c8c6324edd803 100644 --- a/src/include/fe_utils/query_utils.h +++ b/src/include/fe_utils/query_utils.h @@ -2,7 +2,7 @@ * * Facilities for frontend code to query a databases. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/query_utils.h diff --git a/src/include/fe_utils/recovery_gen.h b/src/include/fe_utils/recovery_gen.h index c13f2263bcd33..575608b30b1b0 100644 --- a/src/include/fe_utils/recovery_gen.h +++ b/src/include/fe_utils/recovery_gen.h @@ -2,7 +2,7 @@ * * Generator for recovery configuration * - * Portions Copyright (c) 2011-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2011-2026, PostgreSQL Global Development Group * * src/include/fe_utils/recovery_gen.h * diff --git a/src/include/fe_utils/simple_list.h b/src/include/fe_utils/simple_list.h index 3b8e38414ecde..477912ca78e0b 100644 --- a/src/include/fe_utils/simple_list.h +++ b/src/include/fe_utils/simple_list.h @@ -7,7 +7,7 @@ * facilities, but it's all we need in, eg, pg_dump. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/simple_list.h diff --git a/src/include/fe_utils/string_utils.h b/src/include/fe_utils/string_utils.h index dc991a932194a..0680bb3c19ea3 100644 --- a/src/include/fe_utils/string_utils.h +++ b/src/include/fe_utils/string_utils.h @@ -6,7 +6,7 @@ * assorted contexts. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fe_utils/string_utils.h diff --git a/src/include/fe_utils/version.h b/src/include/fe_utils/version.h new file mode 100644 index 0000000000000..cbd5a8d3a4312 --- /dev/null +++ b/src/include/fe_utils/version.h @@ -0,0 +1,23 @@ +/*------------------------------------------------------------------------- + * + * Routines to retrieve information of PG_VERSION + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/fe_utils/version.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_VERSION_H +#define PG_VERSION_H + +/* + * Retrieve the version major number, useful for major version checks + * based on PG_MAJORVERSION_NUM. + */ +#define GET_PG_MAJORVERSION_NUM(v) ((v) / 10000) + +extern uint32 get_pg_version(const char *datadir, char **version_str); + +#endif /* PG_VERSION_H */ diff --git a/src/include/fmgr.h b/src/include/fmgr.h index 0fe7b4ebc7719..10d02bdb79fa4 100644 --- a/src/include/fmgr.h +++ b/src/include/fmgr.h @@ -8,7 +8,7 @@ * or call fmgr-callable functions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/fmgr.h @@ -19,14 +19,14 @@ #define FMGR_H /* We don't want to include primnodes.h here, so make some stub references */ -typedef struct Node *fmNodePtr; -typedef struct Aggref *fmAggrefPtr; +typedef struct Node Node; +typedef struct Aggref Aggref; /* Likewise, avoid including execnodes.h here */ -typedef void (*fmExprContextCallbackFunction) (Datum arg); +typedef void (*ExprContextCallbackFunction) (Datum arg); /* Likewise, avoid including stringinfo.h here */ -typedef struct StringInfoData *fmStringInfo; +typedef struct StringInfoData *StringInfo; /* @@ -63,7 +63,7 @@ typedef struct FmgrInfo unsigned char fn_stats; /* collect stats if track_functions > this */ void *fn_extra; /* extra space for use by handler */ MemoryContext fn_mcxt; /* memory context to store fn_extra in */ - fmNodePtr fn_expr; /* expression parse tree for call, or NULL */ + Node *fn_expr; /* expression parse tree for call, or NULL */ } FmgrInfo; /* @@ -85,8 +85,8 @@ typedef struct FmgrInfo typedef struct FunctionCallInfoBaseData { FmgrInfo *flinfo; /* ptr to lookup info used for this call */ - fmNodePtr context; /* pass info about context of call */ - fmNodePtr resultinfo; /* pass or return extra info about result */ + Node *context; /* pass info about context of call */ + Node *resultinfo; /* pass or return extra info about result */ Oid fncollation; /* collation for function to use */ #define FIELDNO_FUNCTIONCALLINFODATA_ISNULL 4 bool isnull; /* function must set true if result is NULL */ @@ -231,22 +231,22 @@ extern void fmgr_symbol(Oid functionId, char **mod, char **fn); * Note: it'd be nice if these could be macros, but I see no way to do that * without evaluating the arguments multiple times, which is NOT acceptable. */ -extern struct varlena *pg_detoast_datum(struct varlena *datum); -extern struct varlena *pg_detoast_datum_copy(struct varlena *datum); -extern struct varlena *pg_detoast_datum_slice(struct varlena *datum, - int32 first, int32 count); -extern struct varlena *pg_detoast_datum_packed(struct varlena *datum); +extern varlena *pg_detoast_datum(varlena *datum); +extern varlena *pg_detoast_datum_copy(varlena *datum); +extern varlena *pg_detoast_datum_slice(varlena *datum, + int32 first, int32 count); +extern varlena *pg_detoast_datum_packed(varlena *datum); #define PG_DETOAST_DATUM(datum) \ - pg_detoast_datum((struct varlena *) DatumGetPointer(datum)) + pg_detoast_datum((varlena *) DatumGetPointer(datum)) #define PG_DETOAST_DATUM_COPY(datum) \ - pg_detoast_datum_copy((struct varlena *) DatumGetPointer(datum)) + pg_detoast_datum_copy((varlena *) DatumGetPointer(datum)) #define PG_DETOAST_DATUM_SLICE(datum,f,c) \ - pg_detoast_datum_slice((struct varlena *) DatumGetPointer(datum), \ + pg_detoast_datum_slice((varlena *) DatumGetPointer(datum), \ (int32) (f), (int32) (c)) /* WARNING -- unaligned pointer */ #define PG_DETOAST_DATUM_PACKED(datum) \ - pg_detoast_datum_packed((struct varlena *) DatumGetPointer(datum)) + pg_detoast_datum_packed((varlena *) DatumGetPointer(datum)) /* * Support for cleaning up detoasted copies of inputs. This must only @@ -259,7 +259,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum); */ #define PG_FREE_IF_COPY(ptr,n) \ do { \ - if ((Pointer) (ptr) != PG_GETARG_POINTER(n)) \ + if ((ptr) != PG_GETARG_POINTER(n)) \ pfree(ptr); \ } while (0) @@ -273,6 +273,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum); #define PG_GETARG_CHAR(n) DatumGetChar(PG_GETARG_DATUM(n)) #define PG_GETARG_BOOL(n) DatumGetBool(PG_GETARG_DATUM(n)) #define PG_GETARG_OID(n) DatumGetObjectId(PG_GETARG_DATUM(n)) +#define PG_GETARG_OID8(n) DatumGetObjectId8(PG_GETARG_DATUM(n)) #define PG_GETARG_POINTER(n) DatumGetPointer(PG_GETARG_DATUM(n)) #define PG_GETARG_CSTRING(n) DatumGetCString(PG_GETARG_DATUM(n)) #define PG_GETARG_NAME(n) DatumGetName(PG_GETARG_DATUM(n)) @@ -282,7 +283,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum); #define PG_GETARG_FLOAT8(n) DatumGetFloat8(PG_GETARG_DATUM(n)) #define PG_GETARG_INT64(n) DatumGetInt64(PG_GETARG_DATUM(n)) /* use this if you want the raw, possibly-toasted input datum: */ -#define PG_GETARG_RAW_VARLENA_P(n) ((struct varlena *) PG_GETARG_POINTER(n)) +#define PG_GETARG_RAW_VARLENA_P(n) ((varlena *) PG_GETARG_POINTER(n)) /* use this if you want the input datum de-toasted: */ #define PG_GETARG_VARLENA_P(n) PG_DETOAST_DATUM(PG_GETARG_DATUM(n)) /* and this if you can handle 1-byte-header datums: */ @@ -358,6 +359,7 @@ extern struct varlena *pg_detoast_datum_packed(struct varlena *datum); #define PG_RETURN_CHAR(x) return CharGetDatum(x) #define PG_RETURN_BOOL(x) return BoolGetDatum(x) #define PG_RETURN_OID(x) return ObjectIdGetDatum(x) +#define PG_RETURN_OID8(x) return ObjectId8GetDatum(x) #define PG_RETURN_POINTER(x) return PointerGetDatum(x) #define PG_RETURN_CSTRING(x) return CStringGetDatum(x) #define PG_RETURN_NAME(x) return NameGetDatum(x) @@ -469,7 +471,7 @@ typedef struct int funcmaxargs; /* FUNC_MAX_ARGS */ int indexmaxkeys; /* INDEX_MAX_KEYS */ int namedatalen; /* NAMEDATALEN */ - int float8byval; /* FLOAT8PASSBYVAL */ + int float8byval; /* FLOAT8PASSBYVAL (now vestigial) */ char abi_extra[32]; /* see pg_config_manual.h */ } Pg_abi_values; @@ -742,19 +744,19 @@ extern Datum InputFunctionCall(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod); extern bool InputFunctionCallSafe(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod, - fmNodePtr escontext, + Node *escontext, Datum *result); extern bool DirectInputFunctionCallSafe(PGFunction func, char *str, Oid typioparam, int32 typmod, - fmNodePtr escontext, + Node *escontext, Datum *result); extern Datum OidInputFunctionCall(Oid functionId, char *str, Oid typioparam, int32 typmod); extern char *OutputFunctionCall(FmgrInfo *flinfo, Datum val); extern char *OidOutputFunctionCall(Oid functionId, Datum val); -extern Datum ReceiveFunctionCall(FmgrInfo *flinfo, fmStringInfo buf, +extern Datum ReceiveFunctionCall(FmgrInfo *flinfo, StringInfo buf, Oid typioparam, int32 typmod); -extern Datum OidReceiveFunctionCall(Oid functionId, fmStringInfo buf, +extern Datum OidReceiveFunctionCall(Oid functionId, StringInfo buf, Oid typioparam, int32 typmod); extern bytea *SendFunctionCall(FmgrInfo *flinfo, Datum val); extern bytea *OidSendFunctionCall(Oid functionId, Datum val); @@ -767,9 +769,9 @@ extern const Pg_finfo_record *fetch_finfo_record(void *filehandle, const char *f extern Oid fmgr_internal_function(const char *proname); extern Oid get_fn_expr_rettype(FmgrInfo *flinfo); extern Oid get_fn_expr_argtype(FmgrInfo *flinfo, int argnum); -extern Oid get_call_expr_argtype(fmNodePtr expr, int argnum); +extern Oid get_call_expr_argtype(Node *expr, int argnum); extern bool get_fn_expr_arg_stable(FmgrInfo *flinfo, int argnum); -extern bool get_call_expr_arg_stable(fmNodePtr expr, int argnum); +extern bool get_call_expr_arg_stable(Node *expr, int argnum); extern bool get_fn_expr_variadic(FmgrInfo *flinfo); extern bytea *get_fn_opclass_options(FmgrInfo *flinfo); extern bool has_fn_opclass_options(FmgrInfo *flinfo); @@ -814,11 +816,11 @@ extern void RestoreLibraryState(char *start_address); extern int AggCheckCallContext(FunctionCallInfo fcinfo, MemoryContext *aggcontext); -extern fmAggrefPtr AggGetAggref(FunctionCallInfo fcinfo); +extern Aggref *AggGetAggref(FunctionCallInfo fcinfo); extern MemoryContext AggGetTempMemoryContext(FunctionCallInfo fcinfo); extern bool AggStateIsShared(FunctionCallInfo fcinfo); extern void AggRegisterCallback(FunctionCallInfo fcinfo, - fmExprContextCallbackFunction func, + ExprContextCallbackFunction func, Datum arg); /* diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h index b4da4e6a16aac..abf59a0d8ad3b 100644 --- a/src/include/foreign/fdwapi.h +++ b/src/include/foreign/fdwapi.h @@ -3,7 +3,7 @@ * fdwapi.h * API for foreign-data wrappers * - * Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/foreign/fdwapi.h * @@ -16,8 +16,8 @@ #include "nodes/execnodes.h" #include "nodes/pathnodes.h" -/* To avoid including explain.h here, reference ExplainState thus: */ -struct ExplainState; +/* avoid including explain_state.h here */ +typedef struct ExplainState ExplainState; /* @@ -137,16 +137,16 @@ typedef void (*RefetchForeignRow_function) (EState *estate, bool *updated); typedef void (*ExplainForeignScan_function) (ForeignScanState *node, - struct ExplainState *es); + ExplainState *es); typedef void (*ExplainForeignModify_function) (ModifyTableState *mtstate, ResultRelInfo *rinfo, List *fdw_private, int subplan_index, - struct ExplainState *es); + ExplainState *es); typedef void (*ExplainDirectModify_function) (ForeignScanState *node, - struct ExplainState *es); + ExplainState *es); typedef int (*AcquireSampleRowsFunc) (Relation relation, int elevel, HeapTuple *rows, int targrows, @@ -157,6 +157,10 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation, AcquireSampleRowsFunc *func, BlockNumber *totalpages); +typedef bool (*ImportForeignStatistics_function) (Relation relation, + List *va_cols, + int elevel); + typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt, Oid serverOid); @@ -255,6 +259,7 @@ typedef struct FdwRoutine /* Support functions for ANALYZE */ AnalyzeForeignTable_function AnalyzeForeignTable; + ImportForeignStatistics_function ImportForeignStatistics; /* Support functions for IMPORT FOREIGN SCHEMA */ ImportForeignSchema_function ImportForeignSchema; diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index 7e9decd253789..92a55214feeb8 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -4,7 +4,7 @@ * support for foreign-data wrappers, servers and user mappings. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/foreign/foreign.h * @@ -28,6 +28,7 @@ typedef struct ForeignDataWrapper char *fdwname; /* Name of the FDW */ Oid fdwhandler; /* Oid of handler function, or 0 */ Oid fdwvalidator; /* Oid of validator function, or 0 */ + Oid fdwconnection; /* Oid of connection string function, or 0 */ List *options; /* fdwoptions as DefElem list */ } ForeignDataWrapper; @@ -66,13 +67,15 @@ typedef struct ForeignTable extern ForeignServer *GetForeignServer(Oid serverid); extern ForeignServer *GetForeignServerExtended(Oid serverid, - bits16 flags); + uint16 flags); extern ForeignServer *GetForeignServerByName(const char *srvname, bool missing_ok); +extern char *ForeignServerConnectionString(Oid userid, + ForeignServer *server); extern UserMapping *GetUserMapping(Oid userid, Oid serverid); extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, - bits16 flags); + uint16 flags); extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *fdwname, bool missing_ok); extern ForeignTable *GetForeignTable(Oid relid); diff --git a/src/include/funcapi.h b/src/include/funcapi.h index 94b89b36192b8..a31d6857a3555 100644 --- a/src/include/funcapi.h +++ b/src/include/funcapi.h @@ -8,7 +8,7 @@ * or call FUNCAPI-callable functions or macros. * * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * * src/include/funcapi.h * @@ -296,7 +296,7 @@ HeapTupleGetDatum(const HeapTupleData *tuple) #define MAT_SRF_USE_EXPECTED_DESC 0x01 /* use expectedDesc as tupdesc. */ #define MAT_SRF_BLESS 0x02 /* "Bless" a tuple descriptor with * BlessTupleDesc(). */ -extern void InitMaterializedSRF(FunctionCallInfo fcinfo, bits32 flags); +extern void InitMaterializedSRF(FunctionCallInfo fcinfo, uint32 flags); extern FuncCallContext *init_MultiFuncCall(PG_FUNCTION_ARGS); extern FuncCallContext *per_MultiFuncCall(PG_FUNCTION_ARGS); diff --git a/src/include/getopt_long.h b/src/include/getopt_long.h index e7bdf909f7bed..2276aecc6aa2e 100644 --- a/src/include/getopt_long.h +++ b/src/include/getopt_long.h @@ -2,7 +2,7 @@ * Portions Copyright (c) 1987, 1993, 1994 * The Regents of the University of California. All rights reserved. * - * Portions Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/getopt_long.h */ diff --git a/src/include/jit/SectionMemoryManager.h b/src/include/jit/SectionMemoryManager.h index 924a99b0d333a..4c9bd1c7f016f 100644 --- a/src/include/jit/SectionMemoryManager.h +++ b/src/include/jit/SectionMemoryManager.h @@ -1,5 +1,5 @@ /* - * This is a copy LLVM source code modified by the PostgreSQL project. + * This is a copy of LLVM source code modified by the PostgreSQL project. * See SectionMemoryManager.cpp for notes on provenance and license. */ diff --git a/src/include/jit/jit.h b/src/include/jit/jit.h index 33cb36c5d2ef7..e2baa4c2ed0a9 100644 --- a/src/include/jit/jit.h +++ b/src/include/jit/jit.h @@ -2,7 +2,7 @@ * jit.h * Provider independent JIT infrastructure. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/jit/jit.h * diff --git a/src/include/jit/llvmjit.h b/src/include/jit/llvmjit.h index 5038cf33e3fe9..a647dd65ba2e3 100644 --- a/src/include/jit/llvmjit.h +++ b/src/include/jit/llvmjit.h @@ -2,7 +2,7 @@ * llvmjit.h * LLVM JIT provider. * - * Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/jit/llvmjit.h * @@ -74,6 +74,7 @@ typedef struct LLVMJitContext extern PGDLLIMPORT LLVMTypeRef TypeParamBool; extern PGDLLIMPORT LLVMTypeRef TypePGFunction; extern PGDLLIMPORT LLVMTypeRef TypeSizeT; +extern PGDLLIMPORT LLVMTypeRef TypeDatum; extern PGDLLIMPORT LLVMTypeRef TypeStorageBool; extern PGDLLIMPORT LLVMTypeRef StructNullableDatum; @@ -113,7 +114,7 @@ extern void llvm_split_symbol_name(const char *name, char **modname, char **func extern LLVMTypeRef llvm_pg_var_type(const char *varname); extern LLVMTypeRef llvm_pg_var_func_type(const char *varname); extern LLVMValueRef llvm_pg_func(LLVMModuleRef mod, const char *funcname); -extern void llvm_copy_attributes(LLVMValueRef from, LLVMValueRef to); +extern void llvm_copy_attributes(LLVMValueRef v_from, LLVMValueRef v_to); extern LLVMValueRef llvm_function_reference(LLVMJitContext *context, LLVMBuilderRef builder, LLVMModuleRef mod, diff --git a/src/include/jit/llvmjit_backport.h b/src/include/jit/llvmjit_backport.h index cba8eafc4f3c8..71cfdfc832f78 100644 --- a/src/include/jit/llvmjit_backport.h +++ b/src/include/jit/llvmjit_backport.h @@ -8,14 +8,14 @@ #include /* - * LLVM's RuntimeDyld can produce code that crashes on larger memory ARM + * Pre-LLVM 22 RuntimeDyld can produce code that crashes on large memory ARM * systems, because llvm::SectionMemoryManager allocates multiple pieces of * memory that can be placed too far apart for the generated code. See * src/backend/jit/llvm/SectionMemoryManager.cpp for the patched replacement * class llvm::backport::SectionMemoryManager that we use as a workaround. * This header controls whether we use it. */ -#if defined(__aarch64__) +#if defined(__aarch64__) && LLVM_VERSION_MAJOR < 22 #define USE_LLVM_BACKPORT_SECTION_MEMORY_MANAGER #endif diff --git a/src/include/jit/llvmjit_emit.h b/src/include/jit/llvmjit_emit.h index df5a9fc85007a..089945391ee42 100644 --- a/src/include/jit/llvmjit_emit.h +++ b/src/include/jit/llvmjit_emit.h @@ -2,7 +2,7 @@ * llvmjit_emit.h * Helpers to make emitting LLVM IR a bit more concise and pgindent proof. * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * src/include/jit/llvmjit_emit.h */ @@ -86,6 +86,15 @@ l_sizet_const(size_t i) return LLVMConstInt(TypeSizeT, i, false); } +/* + * Emit constant integer. + */ +static inline LLVMValueRef +l_datum_const(Datum i) +{ + return LLVMConstInt(TypeDatum, i, false); +} + /* * Emit constant boolean, as used for storage (e.g. global vars, structs). */ @@ -313,7 +322,7 @@ l_funcnull(LLVMBuilderRef b, LLVMValueRef v_fcinfo, size_t argno) static inline LLVMValueRef l_funcvalue(LLVMBuilderRef b, LLVMValueRef v_fcinfo, size_t argno) { - return l_load(b, TypeSizeT, l_funcvaluep(b, v_fcinfo, argno), ""); + return l_load(b, TypeDatum, l_funcvaluep(b, v_fcinfo, argno), ""); } #endif /* USE_LLVM */ diff --git a/src/include/lib/binaryheap.h b/src/include/lib/binaryheap.h index e8a575ed867f7..e45b9e49c811a 100644 --- a/src/include/lib/binaryheap.h +++ b/src/include/lib/binaryheap.h @@ -3,7 +3,7 @@ * * A simple binary heap implementation * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * src/include/lib/binaryheap.h */ diff --git a/src/include/lib/bipartite_match.h b/src/include/lib/bipartite_match.h index 3ad8494f4d75c..bd85d3a7d5682 100644 --- a/src/include/lib/bipartite_match.h +++ b/src/include/lib/bipartite_match.h @@ -1,7 +1,7 @@ /* * bipartite_match.h * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * src/include/lib/bipartite_match.h */ diff --git a/src/include/lib/bloomfilter.h b/src/include/lib/bloomfilter.h index dd9fddd8422c8..860ee9bdc72a9 100644 --- a/src/include/lib/bloomfilter.h +++ b/src/include/lib/bloomfilter.h @@ -3,7 +3,7 @@ * bloomfilter.h * Space-efficient set membership testing * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/lib/bloomfilter.h diff --git a/src/include/lib/dshash.h b/src/include/lib/dshash.h index cf5c291cbbb44..64b758b381b34 100644 --- a/src/include/lib/dshash.h +++ b/src/include/lib/dshash.h @@ -3,7 +3,7 @@ * dshash.h * Concurrent hash tables backed by dynamic shared memory areas. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -92,15 +92,23 @@ extern void dshash_detach(dshash_table *hash_table); extern dshash_table_handle dshash_get_hash_table_handle(dshash_table *hash_table); extern void dshash_destroy(dshash_table *hash_table); +/* Flags for dshash_find_or_insert_extended. */ +#define DSHASH_INSERT_NO_OOM 0x01 /* no failure if out-of-memory */ + /* Finding, creating, deleting entries. */ extern void *dshash_find(dshash_table *hash_table, const void *key, bool exclusive); -extern void *dshash_find_or_insert(dshash_table *hash_table, - const void *key, bool *found); +extern void *dshash_find_or_insert_extended(dshash_table *hash_table, + const void *key, bool *found, + int flags); extern bool dshash_delete_key(dshash_table *hash_table, const void *key); extern void dshash_delete_entry(dshash_table *hash_table, void *entry); extern void dshash_release_lock(dshash_table *hash_table, void *entry); +/* Find or insert with error on out-of-memory. */ +#define dshash_find_or_insert(hash_table, key, found) \ + dshash_find_or_insert_extended(hash_table, key, found, 0) + /* seq scan support */ extern void dshash_seq_init(dshash_seq_status *status, dshash_table *hash_table, bool exclusive); diff --git a/src/include/lib/hyperloglog.h b/src/include/lib/hyperloglog.h index 1e9c50feeb82a..62083e77272eb 100644 --- a/src/include/lib/hyperloglog.h +++ b/src/include/lib/hyperloglog.h @@ -3,7 +3,7 @@ * * A simple HyperLogLog cardinality estimator implementation * - * Portions Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2014-2026, PostgreSQL Global Development Group * * Based on Hideaki Ohno's C++ implementation. The copyright terms of Ohno's * original version (the MIT license) follow. diff --git a/src/include/lib/ilist.h b/src/include/lib/ilist.h index 85a641eb41ddf..fc298a6c1d791 100644 --- a/src/include/lib/ilist.h +++ b/src/include/lib/ilist.h @@ -112,7 +112,7 @@ * } * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -517,7 +517,7 @@ dlist_has_prev(const dlist_head *head, const dlist_node *node) /* * Check if node is detached. A node is only detached if it either has been - * initialized with dlist_init_node(), or deleted with + * initialized with dlist_node_init(), or deleted with * dlist_delete_thoroughly() / dlist_delete_from_thoroughly() / * dclist_delete_from_thoroughly(). */ @@ -591,8 +591,8 @@ dlist_tail_node(dlist_head *head) * This is used to convert a dlist_node * back to its containing struct. */ #define dlist_container(type, membername, ptr) \ - (AssertVariableIsOfTypeMacro(ptr, dlist_node *), \ - AssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ + (StaticAssertVariableIsOfTypeMacro(ptr, dlist_node *), \ + StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ ((type *) ((char *) (ptr) - offsetof(type, membername)))) /* @@ -601,7 +601,7 @@ dlist_tail_node(dlist_head *head) * The list must not be empty. */ #define dlist_head_element(type, membername, lhead) \ - (AssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ + (StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ (type *) dlist_head_element_off(lhead, offsetof(type, membername))) /* @@ -610,7 +610,7 @@ dlist_tail_node(dlist_head *head) * The list must not be empty. */ #define dlist_tail_element(type, membername, lhead) \ - (AssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ + (StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ ((type *) dlist_tail_element_off(lhead, offsetof(type, membername)))) /* @@ -621,8 +621,8 @@ dlist_tail_node(dlist_head *head) * It is *not* allowed to manipulate the list during iteration. */ #define dlist_foreach(iter, lhead) \ - for (AssertVariableIsOfTypeMacro(iter, dlist_iter), \ - AssertVariableIsOfTypeMacro(lhead, dlist_head *), \ + for (StaticAssertVariableIsOfTypeMacro(iter, dlist_iter), \ + StaticAssertVariableIsOfTypeMacro(lhead, dlist_head *), \ (iter).end = &(lhead)->head, \ (iter).cur = (iter).end->next ? (iter).end->next : (iter).end; \ (iter).cur != (iter).end; \ @@ -638,8 +638,8 @@ dlist_tail_node(dlist_head *head) * fine to insert or delete adjacent nodes. */ #define dlist_foreach_modify(iter, lhead) \ - for (AssertVariableIsOfTypeMacro(iter, dlist_mutable_iter), \ - AssertVariableIsOfTypeMacro(lhead, dlist_head *), \ + for (StaticAssertVariableIsOfTypeMacro(iter, dlist_mutable_iter), \ + StaticAssertVariableIsOfTypeMacro(lhead, dlist_head *), \ (iter).end = &(lhead)->head, \ (iter).cur = (iter).end->next ? (iter).end->next : (iter).end, \ (iter).next = (iter).cur->next; \ @@ -652,8 +652,8 @@ dlist_tail_node(dlist_head *head) * It is *not* allowed to manipulate the list during iteration. */ #define dlist_reverse_foreach(iter, lhead) \ - for (AssertVariableIsOfTypeMacro(iter, dlist_iter), \ - AssertVariableIsOfTypeMacro(lhead, dlist_head *), \ + for (StaticAssertVariableIsOfTypeMacro(iter, dlist_iter), \ + StaticAssertVariableIsOfTypeMacro(lhead, dlist_head *), \ (iter).end = &(lhead)->head, \ (iter).cur = (iter).end->prev ? (iter).end->prev : (iter).end; \ (iter).cur != (iter).end; \ @@ -953,7 +953,7 @@ dclist_count(const dclist_head *head) * The list must not be empty. */ #define dclist_head_element(type, membername, lhead) \ - (AssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ + (StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ (type *) dclist_head_element_off(lhead, offsetof(type, membername))) /* @@ -962,7 +962,7 @@ dclist_count(const dclist_head *head) * The list must not be empty. */ #define dclist_tail_element(type, membername, lhead) \ - (AssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ + (StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, dlist_node), \ ((type *) dclist_tail_element_off(lhead, offsetof(type, membername)))) @@ -1104,8 +1104,8 @@ slist_delete_current(slist_mutable_iter *iter) * This is used to convert a slist_node * back to its containing struct. */ #define slist_container(type, membername, ptr) \ - (AssertVariableIsOfTypeMacro(ptr, slist_node *), \ - AssertVariableIsOfTypeMacro(((type *) NULL)->membername, slist_node), \ + (StaticAssertVariableIsOfTypeMacro(ptr, slist_node *), \ + StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, slist_node), \ ((type *) ((char *) (ptr) - offsetof(type, membername)))) /* @@ -1114,7 +1114,7 @@ slist_delete_current(slist_mutable_iter *iter) * The list must not be empty. */ #define slist_head_element(type, membername, lhead) \ - (AssertVariableIsOfTypeMacro(((type *) NULL)->membername, slist_node), \ + (StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, slist_node), \ (type *) slist_head_element_off(lhead, offsetof(type, membername))) /* @@ -1130,8 +1130,8 @@ slist_delete_current(slist_mutable_iter *iter) * not safe.) */ #define slist_foreach(iter, lhead) \ - for (AssertVariableIsOfTypeMacro(iter, slist_iter), \ - AssertVariableIsOfTypeMacro(lhead, slist_head *), \ + for (StaticAssertVariableIsOfTypeMacro(iter, slist_iter), \ + StaticAssertVariableIsOfTypeMacro(lhead, slist_head *), \ (iter).cur = (lhead)->head.next; \ (iter).cur != NULL; \ (iter).cur = (iter).cur->next) @@ -1146,8 +1146,8 @@ slist_delete_current(slist_mutable_iter *iter) * deletion of nodes adjacent to the current node would misbehave. */ #define slist_foreach_modify(iter, lhead) \ - for (AssertVariableIsOfTypeMacro(iter, slist_mutable_iter), \ - AssertVariableIsOfTypeMacro(lhead, slist_head *), \ + for (StaticAssertVariableIsOfTypeMacro(iter, slist_mutable_iter), \ + StaticAssertVariableIsOfTypeMacro(lhead, slist_head *), \ (iter).prev = &(lhead)->head, \ (iter).cur = (iter).prev->next, \ (iter).next = (iter).cur ? (iter).cur->next : NULL; \ diff --git a/src/include/lib/integerset.h b/src/include/lib/integerset.h index 00626a6353725..99b530ca6dc05 100644 --- a/src/include/lib/integerset.h +++ b/src/include/lib/integerset.h @@ -2,7 +2,7 @@ * integerset.h * In-memory data structure to hold a large set of integers efficiently * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * src/include/lib/integerset.h */ diff --git a/src/include/lib/knapsack.h b/src/include/lib/knapsack.h index b870122f1c8fa..c114bab478bf2 100644 --- a/src/include/lib/knapsack.h +++ b/src/include/lib/knapsack.h @@ -1,7 +1,7 @@ /* * knapsack.h * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group + * Copyright (c) 2017-2026, PostgreSQL Global Development Group * * src/include/lib/knapsack.h */ diff --git a/src/include/lib/pairingheap.h b/src/include/lib/pairingheap.h index 3c57d3fda1bbe..f1582c98626a7 100644 --- a/src/include/lib/pairingheap.h +++ b/src/include/lib/pairingheap.h @@ -3,7 +3,7 @@ * * A Pairing Heap implementation * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * src/include/lib/pairingheap.h */ @@ -41,16 +41,16 @@ typedef struct pairingheap_node * This is used to convert a pairingheap_node * back to its containing struct. */ #define pairingheap_container(type, membername, ptr) \ - (AssertVariableIsOfTypeMacro(ptr, pairingheap_node *), \ - AssertVariableIsOfTypeMacro(((type *) NULL)->membername, pairingheap_node), \ + (StaticAssertVariableIsOfTypeMacro(ptr, pairingheap_node *), \ + StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, pairingheap_node), \ ((type *) ((char *) (ptr) - offsetof(type, membername)))) /* * Like pairingheap_container, but used when the pointer is 'const ptr' */ #define pairingheap_const_container(type, membername, ptr) \ - (AssertVariableIsOfTypeMacro(ptr, const pairingheap_node *), \ - AssertVariableIsOfTypeMacro(((type *) NULL)->membername, pairingheap_node), \ + (StaticAssertVariableIsOfTypeMacro(ptr, const pairingheap_node *), \ + StaticAssertVariableIsOfTypeMacro(((type *) NULL)->membername, pairingheap_node), \ ((const type *) ((const char *) (ptr) - offsetof(type, membername)))) /* @@ -77,6 +77,9 @@ typedef struct pairingheap extern pairingheap *pairingheap_allocate(pairingheap_comparator compare, void *arg); +extern void pairingheap_initialize(pairingheap *heap, + pairingheap_comparator compare, + void *arg); extern void pairingheap_free(pairingheap *heap); extern void pairingheap_add(pairingheap *heap, pairingheap_node *node); extern pairingheap_node *pairingheap_first(pairingheap *heap); diff --git a/src/include/lib/qunique.h b/src/include/lib/qunique.h index c699053a9bcab..75dae1b89f336 100644 --- a/src/include/lib/qunique.h +++ b/src/include/lib/qunique.h @@ -2,7 +2,7 @@ * * qunique.h * inline array unique functions - * Portions Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/lib/qunique.h diff --git a/src/include/lib/radixtree.h b/src/include/lib/radixtree.h index a75b77270c4e6..694c1d1f835d7 100644 --- a/src/include/lib/radixtree.h +++ b/src/include/lib/radixtree.h @@ -143,7 +143,7 @@ * RT_DELETE - Delete a key-value pair. Declared/defined if RT_USE_DELETE is defined * * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/lib/radixtree.h @@ -403,7 +403,7 @@ typedef struct RT_NODE #else #define RT_PTR_ALLOC RT_NODE * #define RT_INVALID_PTR_ALLOC NULL -#define RT_PTR_ALLOC_IS_VALID(ptr) PointerIsValid(ptr) +#define RT_PTR_ALLOC_IS_VALID(ptr) ((ptr) != NULL) #endif /* @@ -1825,7 +1825,7 @@ RT_CREATE(MemoryContext ctx) dsa_pointer dp; #endif - tree = (RT_RADIX_TREE *) palloc0(sizeof(RT_RADIX_TREE)); + tree = palloc0_object(RT_RADIX_TREE); #ifdef RT_SHMEM tree->dsa = dsa; @@ -1835,7 +1835,7 @@ RT_CREATE(MemoryContext ctx) tree->ctl->magic = RT_RADIX_TREE_MAGIC; LWLockInitialize(&tree->ctl->lock, tranche_id); #else - tree->ctl = (RT_RADIX_TREE_CONTROL *) palloc0(sizeof(RT_RADIX_TREE_CONTROL)); + tree->ctl = palloc0_object(RT_RADIX_TREE_CONTROL); /* Create a slab context for each size class */ for (int i = 0; i < RT_NUM_SIZE_CLASSES; i++) @@ -1868,7 +1868,7 @@ RT_ATTACH(dsa_area *dsa, RT_HANDLE handle) RT_RADIX_TREE *tree; dsa_pointer control; - tree = (RT_RADIX_TREE *) palloc0(sizeof(RT_RADIX_TREE)); + tree = palloc0_object(RT_RADIX_TREE); /* Find the control object in shared memory */ control = handle; @@ -2057,7 +2057,7 @@ RT_BEGIN_ITERATE(RT_RADIX_TREE * tree) RT_ITER *iter; RT_CHILD_PTR root; - iter = (RT_ITER *) palloc0(sizeof(RT_ITER)); + iter = palloc0_object(RT_ITER); iter->tree = tree; Assert(RT_PTR_ALLOC_IS_VALID(tree->ctl->root)); @@ -2721,12 +2721,12 @@ RT_VERIFY_NODE(RT_NODE * node) case RT_NODE_KIND_256: { RT_NODE_256 *n256 = (RT_NODE_256 *) node; - int cnt = 0; + int cnt; /* RT_DUMP_NODE(node); */ - for (int i = 0; i < RT_BM_IDX(RT_NODE_MAX_SLOTS); i++) - cnt += bmw_popcount(n256->isset[i]); + cnt = pg_popcount((const char *) n256->isset, + RT_NODE_MAX_SLOTS / BITS_PER_BYTE); /* * Check if the number of used chunk matches, accounting for @@ -2777,8 +2777,8 @@ RT_STATS(RT_RADIX_TREE * tree) /* * Print out debugging information about the given node. */ -static void pg_attribute_unused() +static void RT_DUMP_NODE(RT_NODE * node) { #ifdef RT_SHMEM diff --git a/src/include/lib/rbtree.h b/src/include/lib/rbtree.h index 37d6d8ed03719..887e4983d7ac3 100644 --- a/src/include/lib/rbtree.h +++ b/src/include/lib/rbtree.h @@ -3,7 +3,7 @@ * rbtree.h * interface for PostgreSQL generic Red-Black binary tree package * - * Copyright (c) 2009-2025, PostgreSQL Global Development Group + * Copyright (c) 2009-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/lib/rbtree.h diff --git a/src/include/lib/simplehash.h b/src/include/lib/simplehash.h index 327274c2340bf..15af488abfb2c 100644 --- a/src/include/lib/simplehash.h +++ b/src/include/lib/simplehash.h @@ -87,13 +87,12 @@ * looking or is done - buckets following a deleted element are shifted * backwards, unless they're empty or already at their optimal position. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/lib/simplehash.h */ -#include "port/pg_bitutils.h" /* helpers */ #define SH_MAKE_PREFIX(a) CppConcat(a,_) @@ -125,6 +124,7 @@ #define SH_ITERATE SH_MAKE_NAME(iterate) #define SH_ALLOCATE SH_MAKE_NAME(allocate) #define SH_FREE SH_MAKE_NAME(free) +#define SH_ESTIMATE_SPACE SH_MAKE_NAME(estimate_space) #define SH_STAT SH_MAKE_NAME(stat) /* internal helper functions (no externally visible prototypes) */ @@ -242,7 +242,10 @@ SH_SCOPE void SH_START_ITERATE_AT(SH_TYPE * tb, SH_ITERATOR * iter, uint32 at); /* *_iterate(_hash *tb, _iterator *iter) */ SH_SCOPE SH_ELEMENT_TYPE *SH_ITERATE(SH_TYPE * tb, SH_ITERATOR * iter); -/* void _stat(_hash *tb */ +/* size_t _estimate_space(double nentries) */ +SH_SCOPE size_t SH_ESTIMATE_SPACE(double nentries); + +/* void _stat(_hash *tb) */ SH_SCOPE void SH_STAT(SH_TYPE * tb); #endif /* SH_DECLARE */ @@ -251,6 +254,8 @@ SH_SCOPE void SH_STAT(SH_TYPE * tb); /* generate implementation of the hash table */ #ifdef SH_DEFINE +#include "port/pg_bitutils.h" + #ifndef SH_RAW_ALLOCATOR #include "utils/memutils.h" #endif @@ -305,7 +310,7 @@ SH_SCOPE void SH_STAT(SH_TYPE * tb); /* * Compute allocation size for hashtable. Result can be passed to - * SH_UPDATE_PARAMETERS. + * SH_UPDATE_PARAMETERS. (Keep SH_ESTIMATE_SPACE in sync with this!) */ static inline uint64 SH_COMPUTE_SIZE(uint64 newsize) @@ -1044,6 +1049,10 @@ SH_START_ITERATE_AT(SH_TYPE * tb, SH_ITERATOR * iter, uint32 at) SH_SCOPE SH_ELEMENT_TYPE * SH_ITERATE(SH_TYPE * tb, SH_ITERATOR * iter) { + /* validate sanity of the given iterator */ + Assert(iter->cur < tb->size); + Assert(iter->end < tb->size); + while (!iter->done) { SH_ELEMENT_TYPE *elem; @@ -1064,6 +1073,47 @@ SH_ITERATE(SH_TYPE * tb, SH_ITERATOR * iter) return NULL; } +/* + * Estimate the amount of space needed for a hashtable with nentries entries. + * Return SIZE_MAX if that's too many entries. + * + * nentries is "double" because this is meant for use by the planner, + * which typically works with double rowcount estimates. So we'd need to + * clamp to integer somewhere and that might as well be here. We do expect + * the value not to be NaN or negative, else the result will be garbage. + */ +SH_SCOPE size_t +SH_ESTIMATE_SPACE(double nentries) +{ + uint64 size; + uint64 space; + + /* scale request by SH_FILLFACTOR, as SH_CREATE does */ + nentries = nentries / SH_FILLFACTOR; + + /* fail if we'd overrun SH_MAX_SIZE entries */ + if (nentries >= SH_MAX_SIZE) + return SIZE_MAX; + + /* should be safe to convert to uint64 */ + size = (uint64) nentries; + + /* supporting zero sized hashes would complicate matters */ + size = Max(size, 2); + + /* round up size to the next power of 2, that's how bucketing works */ + size = pg_nextpower2_64(size); + + /* calculate space needed for ->data */ + space = ((uint64) sizeof(SH_ELEMENT_TYPE)) * size; + + /* verify that allocation of ->data is possible on this platform */ + if (space >= SIZE_MAX / 2) + return SIZE_MAX; + + return (size_t) space + sizeof(SH_TYPE); +} + /* * Report some statistics about the state of the hashtable. For * debugging/profiling purposes only. @@ -1191,6 +1241,7 @@ SH_STAT(SH_TYPE * tb) #undef SH_ITERATE #undef SH_ALLOCATE #undef SH_FREE +#undef SH_ESTIMATE_SPACE #undef SH_STAT /* internal function names */ diff --git a/src/include/lib/sort_template.h b/src/include/lib/sort_template.h index 11e136ca53d06..22b2092d03b7b 100644 --- a/src/include/lib/sort_template.h +++ b/src/include/lib/sort_template.h @@ -5,7 +5,7 @@ * A template for a sort algorithm that supports varying degrees of * specialization. * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1992-1994, Regents of the University of California * * Usage notes: @@ -311,6 +311,14 @@ ST_SORT(ST_ELEMENT_TYPE * data, size_t n DO_CHECK_FOR_INTERRUPTS(); if (n < 7) { + /* + * Not strictly necessary, but a caller may pass a NULL pointer input + * and zero length, and this silences warnings about applying offsets + * to NULL pointers. + */ + if (n < 2) + return; + for (pm = a + ST_POINTER_STEP; pm < a + n * ST_POINTER_STEP; pm += ST_POINTER_STEP) for (pl = pm; pl > a && DO_COMPARE(pl - ST_POINTER_STEP, pl) > 0; @@ -387,29 +395,23 @@ ST_SORT(ST_ELEMENT_TYPE * data, size_t n if (d1 <= d2) { /* Recurse on left partition, then iterate on right partition */ - if (d1 > ST_POINTER_STEP) - DO_SORT(a, d1 / ST_POINTER_STEP); - if (d2 > ST_POINTER_STEP) - { - /* Iterate rather than recurse to save stack space */ - /* DO_SORT(pn - d2, d2 / ST_POINTER_STEP) */ - a = pn - d2; - n = d2 / ST_POINTER_STEP; - goto loop; - } + DO_SORT(a, d1 / ST_POINTER_STEP); + + /* Iterate rather than recurse to save stack space */ + /* DO_SORT(pn - d2, d2 / ST_POINTER_STEP) */ + a = pn - d2; + n = d2 / ST_POINTER_STEP; + goto loop; } else { /* Recurse on right partition, then iterate on left partition */ - if (d2 > ST_POINTER_STEP) - DO_SORT(pn - d2, d2 / ST_POINTER_STEP); - if (d1 > ST_POINTER_STEP) - { - /* Iterate rather than recurse to save stack space */ - /* DO_SORT(a, d1 / ST_POINTER_STEP) */ - n = d1 / ST_POINTER_STEP; - goto loop; - } + DO_SORT(pn - d2, d2 / ST_POINTER_STEP); + + /* Iterate rather than recurse to save stack space */ + /* DO_SORT(a, d1 / ST_POINTER_STEP) */ + n = d1 / ST_POINTER_STEP; + goto loop; } } #endif diff --git a/src/include/lib/stringinfo.h b/src/include/lib/stringinfo.h index c96df989bb0b7..079652c8ce4a9 100644 --- a/src/include/lib/stringinfo.h +++ b/src/include/lib/stringinfo.h @@ -8,7 +8,7 @@ * (null-terminated text) or arbitrary binary data. All storage is allocated * with palloc() (falling back to malloc in frontend code). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/lib/stringinfo.h diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index cc9643cce2fd8..be23c4ca3e4b8 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -4,7 +4,7 @@ * Definitions for network authentication routines * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/auth.h diff --git a/src/include/libpq/be-fsstubs.h b/src/include/libpq/be-fsstubs.h index 1b006438e906f..8775939f41082 100644 --- a/src/include/libpq/be-fsstubs.h +++ b/src/include/libpq/be-fsstubs.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/be-fsstubs.h diff --git a/src/include/libpq/be-gssapi-common.h b/src/include/libpq/be-gssapi-common.h index bfe8d7656edeb..21286df284366 100644 --- a/src/include/libpq/be-gssapi-common.h +++ b/src/include/libpq/be-gssapi-common.h @@ -3,7 +3,7 @@ * be-gssapi-common.h * Definitions for GSSAPI authentication and encryption handling * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/be-gssapi-common.h diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h index a1b4b36314336..ebef0d0f78c03 100644 --- a/src/include/libpq/crypt.h +++ b/src/include/libpq/crypt.h @@ -3,7 +3,7 @@ * crypt.h * Interface to libpq/crypt.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/crypt.h @@ -25,6 +25,9 @@ */ #define MAX_ENCRYPTED_PASSWORD_LEN (512) +/* Threshold for password expiration warnings. */ +extern PGDLLIMPORT int password_expiration_warning_threshold; + /* Enables deprecation warnings for MD5 passwords. */ extern PGDLLIMPORT bool md5_password_warnings; diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 3657f182db3e3..29e2a6c5b3db1 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -37,7 +37,6 @@ typedef enum UserAuth uaBSD, uaLDAP, uaCert, - uaRADIUS, uaPeer, uaOAuth, #define USER_AUTH_LAST uaOAuth /* Must be last value of this enum */ @@ -128,18 +127,12 @@ typedef struct HbaLine bool include_realm; bool compat_realm; bool upn_username; - List *radiusservers; - char *radiusservers_s; - List *radiussecrets; - char *radiussecrets_s; - List *radiusidentifiers; - char *radiusidentifiers_s; - List *radiusports; - char *radiusports_s; char *oauth_issuer; char *oauth_scope; char *oauth_validator; bool oauth_skip_usermap; + List *oauth_opt_keys; + List *oauth_opt_vals; } HbaLine; typedef struct IdentLine @@ -151,6 +144,36 @@ typedef struct IdentLine AuthToken *pg_user; } IdentLine; +typedef struct HostsLine +{ + int linenumber; + + char *sourcefile; + char *rawline; + + /* Required fields */ + List *hostnames; + char *ssl_key; + char *ssl_cert; + + /* Optional fields */ + char *ssl_ca; + char *ssl_passphrase_cmd; + bool ssl_passphrase_reload; + + /* Internal bookkeeping */ + void *ssl_ctx; /* associated SSL_CTX* for the above settings */ +} HostsLine; + +typedef enum HostsFileLoadResult +{ + HOSTSFILE_LOAD_OK = 0, + HOSTSFILE_LOAD_FAILED, + HOSTSFILE_EMPTY, + HOSTSFILE_MISSING, + HOSTSFILE_DISABLED, +} HostsFileLoadResult; + /* * TokenizedAuthLine represents one line lexed from an authentication * configuration file. Each item in the "fields" list is a sub-list of @@ -169,19 +192,18 @@ typedef struct TokenizedAuthLine char *err_msg; /* Error message if any */ } TokenizedAuthLine; -/* kluge to avoid including libpq/libpq-be.h here */ -typedef struct Port hbaPort; +/* avoid including libpq/libpq-be.h here */ +typedef struct Port Port; extern bool load_hba(void); extern bool load_ident(void); extern const char *hba_authname(UserAuth auth_method); -extern void hba_getauthmethod(hbaPort *port); +extern void hba_getauthmethod(Port *port); extern int check_usermap(const char *usermap_name, const char *pg_user, const char *system_user, bool case_insensitive); extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel); extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel); -extern bool pg_isblank(const char c); extern FILE *open_auth_file(const char *filename, int elevel, int depth, char **err_msg); extern void free_auth_file(FILE *file, int depth); diff --git a/src/include/libpq/ifaddr.h b/src/include/libpq/ifaddr.h index 846999df1911a..811b3f95bec36 100644 --- a/src/include/libpq/ifaddr.h +++ b/src/include/libpq/ifaddr.h @@ -3,7 +3,7 @@ * ifaddr.h * IP netmask calculations, and enumerating network interfaces. * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/libpq/ifaddr.h * diff --git a/src/include/libpq/libpq-be-fe-helpers.h b/src/include/libpq/libpq-be-fe-helpers.h index 16205b824fa55..85d8b63f01985 100644 --- a/src/include/libpq/libpq-be-fe-helpers.h +++ b/src/include/libpq/libpq-be-fe-helpers.h @@ -20,7 +20,7 @@ * into non-blocking mode. That can lead to blocking even when only the async * libpq functions are used. This should be fixed. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/libpq-be-fe-helpers.h @@ -30,17 +30,7 @@ #ifndef LIBPQ_BE_FE_HELPERS_H #define LIBPQ_BE_FE_HELPERS_H -/* - * Despite the name, BUILDING_DLL is set only when building code directly part - * of the backend. Which also is where libpq isn't allowed to be - * used. Obviously this doesn't protect against libpq-fe.h getting included - * otherwise, but perhaps still protects against a few mistakes... - */ -#ifdef BUILDING_DLL -#error "libpq may not be used code directly built into the backend" -#endif - -#include "libpq-fe.h" +#include "libpq/libpq-be-fe.h" #include "miscadmin.h" #include "storage/fd.h" #include "storage/latch.h" @@ -79,7 +69,7 @@ libpqsrv_connect(const char *conninfo, uint32 wait_event_info) /* * Like libpqsrv_connect(), except that this is a wrapper for * PQconnectdbParams(). - */ + */ static inline PGconn * libpqsrv_connect_params(const char *const *keywords, const char *const *values, @@ -289,41 +279,30 @@ libpqsrv_exec_params(PGconn *conn, static inline PGresult * libpqsrv_get_result_last(PGconn *conn, uint32 wait_event_info) { - PGresult *volatile lastResult = NULL; + PGresult *lastResult = NULL; - /* In what follows, do not leak any PGresults on an error. */ - PG_TRY(); + for (;;) { - for (;;) - { - /* Wait for, and collect, the next PGresult. */ - PGresult *result; + /* Wait for, and collect, the next PGresult. */ + PGresult *result; - result = libpqsrv_get_result(conn, wait_event_info); - if (result == NULL) - break; /* query is complete, or failure */ + result = libpqsrv_get_result(conn, wait_event_info); + if (result == NULL) + break; /* query is complete, or failure */ - /* - * Emulate PQexec()'s behavior of returning the last result when - * there are many. - */ - PQclear(lastResult); - lastResult = result; - - if (PQresultStatus(lastResult) == PGRES_COPY_IN || - PQresultStatus(lastResult) == PGRES_COPY_OUT || - PQresultStatus(lastResult) == PGRES_COPY_BOTH || - PQstatus(conn) == CONNECTION_BAD) - break; - } - } - PG_CATCH(); - { + /* + * Emulate PQexec()'s behavior of returning the last result when there + * are many. + */ PQclear(lastResult); - PG_RE_THROW(); - } - PG_END_TRY(); + lastResult = result; + if (PQresultStatus(lastResult) == PGRES_COPY_IN || + PQresultStatus(lastResult) == PGRES_COPY_OUT || + PQresultStatus(lastResult) == PGRES_COPY_BOTH || + PQstatus(conn) == CONNECTION_BAD) + break; + } return lastResult; } @@ -454,4 +433,45 @@ exit: ; return error; } +/* + * libpqsrv_notice_receiver + * + * Custom notice receiver for libpq connections. + * + * This function is intended to be set via PQsetNoticeReceiver() so that + * NOTICE, WARNING, and similar messages from the connection are reported via + * ereport(), instead of being printed to stderr. + * + * Because this will be called from libpq with a "real" (not wrapped) + * PGresult, we need to temporarily ignore libpq-be-fe.h's wrapper macros + * for PGresult and also PQresultErrorMessage, and put back the wrappers + * afterwards. That's not pretty, but there seems no better alternative. + */ +#undef PGresult +#undef PQresultErrorMessage + +static inline void +libpqsrv_notice_receiver(void *arg, const PGresult *res) +{ + const char *message; + int len; + const char *prefix = (const char *) arg; + + /* + * Trim the trailing newline from the message text returned from + * PQresultErrorMessage(), as it always includes one, to produce cleaner + * log output. + */ + message = PQresultErrorMessage(res); + len = strlen(message); + if (len > 0 && message[len - 1] == '\n') + len--; + + ereport(LOG, + errmsg_internal("%s: %.*s", prefix, len, message)); +} + +#define PGresult libpqsrv_PGresult +#define PQresultErrorMessage libpqsrv_PQresultErrorMessage + #endif /* LIBPQ_BE_FE_HELPERS_H */ diff --git a/src/include/libpq/libpq-be-fe.h b/src/include/libpq/libpq-be-fe.h new file mode 100644 index 0000000000000..78242f72c3f0c --- /dev/null +++ b/src/include/libpq/libpq-be-fe.h @@ -0,0 +1,259 @@ +/*------------------------------------------------------------------------- + * + * libpq-be-fe.h + * Wrapper functions for using libpq in extensions + * + * Code built directly into the backend is not allowed to link to libpq + * directly. Extension code is allowed to use libpq however. One of the + * main risks in doing so is leaking the malloc-allocated structures + * returned by libpq, causing a process-lifespan memory leak. + * + * This file provides wrapper objects to help in building memory-safe code. + * A PGresult object wrapped this way acts much as if it were palloc'd: + * it will go away when the specified context is reset or deleted. + * We might later extend the concept to other objects such as PGconns. + * + * See also the libpq-be-fe-helpers.h file, which provides additional + * facilities built on top of this one. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/libpq/libpq-be-fe.h + * + *------------------------------------------------------------------------- + */ +#ifndef LIBPQ_BE_FE_H +#define LIBPQ_BE_FE_H + +/* + * Despite the name, BUILDING_DLL is set only when building code directly part + * of the backend. Which also is where libpq isn't allowed to be + * used. Obviously this doesn't protect against libpq-fe.h getting included + * otherwise, but perhaps still protects against a few mistakes... + */ +#ifdef BUILDING_DLL +#error "libpq may not be used in code directly built into the backend" +#endif + +#include "libpq-fe.h" + +/* + * Memory-context-safe wrapper object for a PGresult. + */ +typedef struct libpqsrv_PGresult +{ + PGresult *res; /* the wrapped PGresult */ + MemoryContext ctx; /* the MemoryContext it's attached to */ + MemoryContextCallback cb; /* the callback that implements freeing */ +} libpqsrv_PGresult; + + +/* + * Wrap the given PGresult in a libpqsrv_PGresult object, so that it will + * go away automatically if the current memory context is reset or deleted. + * + * To avoid potential memory leaks, backend code must always apply this + * immediately to the output of any PGresult-yielding libpq function. + */ +static inline libpqsrv_PGresult * +libpqsrv_PQwrap(PGresult *res) +{ + libpqsrv_PGresult *bres; + MemoryContext ctx = CurrentMemoryContext; + + /* We pass through a NULL result as-is, since there's nothing to free */ + if (res == NULL) + return NULL; + /* Attempt to allocate the wrapper ... this had better not throw error */ + bres = (libpqsrv_PGresult *) + MemoryContextAllocExtended(ctx, + sizeof(libpqsrv_PGresult), + MCXT_ALLOC_NO_OOM); + /* If we failed to allocate a wrapper, free the PGresult before failing */ + if (bres == NULL) + { + PQclear(res); + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + } + /* Okay, set up the wrapper */ + bres->res = res; + bres->ctx = ctx; + bres->cb.func = (MemoryContextCallbackFunction) PQclear; + bres->cb.arg = res; + MemoryContextRegisterResetCallback(ctx, &bres->cb); + return bres; +} + +/* + * Free a wrapped PGresult, after detaching it from the memory context. + * Like PQclear(), allow the argument to be NULL. + */ +static inline void +libpqsrv_PQclear(libpqsrv_PGresult *bres) +{ + if (bres) + { + MemoryContextUnregisterResetCallback(bres->ctx, &bres->cb); + PQclear(bres->res); + pfree(bres); + } +} + +/* + * Move a wrapped PGresult to have a different parent context. + */ +static inline libpqsrv_PGresult * +libpqsrv_PGresultSetParent(libpqsrv_PGresult *bres, MemoryContext ctx) +{ + libpqsrv_PGresult *newres; + + /* We pass through a NULL result as-is */ + if (bres == NULL) + return NULL; + /* Make a new wrapper in the target context, raising error on OOM */ + newres = (libpqsrv_PGresult *) + MemoryContextAlloc(ctx, sizeof(libpqsrv_PGresult)); + /* Okay, set up the new wrapper */ + newres->res = bres->res; + newres->ctx = ctx; + newres->cb.func = (MemoryContextCallbackFunction) PQclear; + newres->cb.arg = bres->res; + MemoryContextRegisterResetCallback(ctx, &newres->cb); + /* Disarm and delete the old wrapper */ + MemoryContextUnregisterResetCallback(bres->ctx, &bres->cb); + pfree(bres); + return newres; +} + +/* + * Convenience wrapper for PQgetResult. + * + * We could supply wrappers for other PGresult-returning functions too, + * but at present there's no need. + */ +static inline libpqsrv_PGresult * +libpqsrv_PQgetResult(PGconn *conn) +{ + return libpqsrv_PQwrap(PQgetResult(conn)); +} + +/* + * Accessor functions for libpqsrv_PGresult. While it's not necessary to use + * these, they emulate the behavior of the underlying libpq functions when + * passed a NULL pointer. This is particularly important for PQresultStatus, + * which is often the first check on a result. + */ + +static inline ExecStatusType +libpqsrv_PQresultStatus(const libpqsrv_PGresult *res) +{ + if (!res) + return PGRES_FATAL_ERROR; + return PQresultStatus(res->res); +} + +static inline const char * +libpqsrv_PQresultErrorMessage(const libpqsrv_PGresult *res) +{ + if (!res) + return ""; + return PQresultErrorMessage(res->res); +} + +static inline char * +libpqsrv_PQresultErrorField(const libpqsrv_PGresult *res, int fieldcode) +{ + if (!res) + return NULL; + return PQresultErrorField(res->res, fieldcode); +} + +static inline char * +libpqsrv_PQcmdStatus(const libpqsrv_PGresult *res) +{ + if (!res) + return NULL; + return PQcmdStatus(res->res); +} + +static inline int +libpqsrv_PQntuples(const libpqsrv_PGresult *res) +{ + if (!res) + return 0; + return PQntuples(res->res); +} + +static inline int +libpqsrv_PQnfields(const libpqsrv_PGresult *res) +{ + if (!res) + return 0; + return PQnfields(res->res); +} + +static inline char * +libpqsrv_PQgetvalue(const libpqsrv_PGresult *res, int tup_num, int field_num) +{ + if (!res) + return NULL; + return PQgetvalue(res->res, tup_num, field_num); +} + +static inline int +libpqsrv_PQgetlength(const libpqsrv_PGresult *res, int tup_num, int field_num) +{ + if (!res) + return 0; + return PQgetlength(res->res, tup_num, field_num); +} + +static inline int +libpqsrv_PQgetisnull(const libpqsrv_PGresult *res, int tup_num, int field_num) +{ + if (!res) + return 1; /* pretend it is null */ + return PQgetisnull(res->res, tup_num, field_num); +} + +static inline char * +libpqsrv_PQfname(const libpqsrv_PGresult *res, int field_num) +{ + if (!res) + return NULL; + return PQfname(res->res, field_num); +} + +static inline const char * +libpqsrv_PQcmdTuples(const libpqsrv_PGresult *res) +{ + if (!res) + return ""; + return PQcmdTuples(res->res); +} + +/* + * Redefine these libpq entry point names concerned with PGresults so that + * they will operate on libpqsrv_PGresults instead. This avoids needing to + * convert a lot of pre-existing code, and reduces the notational differences + * between frontend and backend libpq-using code. + */ +#define PGresult libpqsrv_PGresult +#define PQclear libpqsrv_PQclear +#define PQgetResult libpqsrv_PQgetResult +#define PQresultStatus libpqsrv_PQresultStatus +#define PQresultErrorMessage libpqsrv_PQresultErrorMessage +#define PQresultErrorField libpqsrv_PQresultErrorField +#define PQcmdStatus libpqsrv_PQcmdStatus +#define PQntuples libpqsrv_PQntuples +#define PQnfields libpqsrv_PQnfields +#define PQgetvalue libpqsrv_PQgetvalue +#define PQgetlength libpqsrv_PQgetlength +#define PQgetisnull libpqsrv_PQgetisnull +#define PQfname libpqsrv_PQfname +#define PQcmdTuples libpqsrv_PQcmdTuples + +#endif /* LIBPQ_BE_FE_H */ diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index d6e671a638257..921b2daa4ff92 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -8,7 +8,7 @@ * Structs that need to be client-visible are in pqcomm.h. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/libpq-be.h diff --git a/src/include/libpq/libpq-fs.h b/src/include/libpq/libpq-fs.h index 26fa50c06b586..4c72f6377afa6 100644 --- a/src/include/libpq/libpq-fs.h +++ b/src/include/libpq/libpq-fs.h @@ -4,7 +4,7 @@ * definitions for using Inversion file system routines (ie, large objects) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/libpq-fs.h diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index aeb66ca40cf38..c9b934d232155 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -4,7 +4,7 @@ * POSTGRES LIBPQ buffer structure definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/libpq.h @@ -18,7 +18,10 @@ #include "lib/stringinfo.h" #include "libpq/libpq-be.h" -#include "storage/latch.h" + + +/* avoid including waiteventset.h */ +typedef struct WaitEventSet WaitEventSet; /* @@ -110,6 +113,7 @@ extern PGDLLIMPORT int ssl_max_protocol_version; extern PGDLLIMPORT char *ssl_passphrase_command; extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload; extern PGDLLIMPORT char *ssl_dh_params_file; +extern PGDLLIMPORT bool ssl_sni; extern PGDLLIMPORT char *SSLCipherSuites; extern PGDLLIMPORT char *SSLCipherList; extern PGDLLIMPORT char *SSLECDHCurve; @@ -118,6 +122,24 @@ extern PGDLLIMPORT bool SSLPreferServerCiphers; extern PGDLLIMPORT bool ssl_loaded_verify_locations; #endif +#ifdef USE_SSL +#define SSL_LIBRARY "OpenSSL" +#else +#define SSL_LIBRARY "" +#endif + +#ifdef USE_OPENSSL +#define DEFAULT_SSL_CIPHERS "HIGH:MEDIUM:+3DES:!aNULL" +#else +#define DEFAULT_SSL_CIPHERS "none" +#endif + +#ifdef USE_SSL +#define DEFAULT_SSL_GROUPS "X25519:prime256v1" +#else +#define DEFAULT_SSL_GROUPS "none" +#endif + /* * prototypes for functions in be-secure-gssapi.c */ @@ -137,9 +159,11 @@ enum ssl_protocol_versions /* * prototypes for functions in be-secure-common.c */ -extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start, +extern int run_ssl_passphrase_command(const char *cmd, const char *prompt, + bool is_server_start, char *buf, int size); extern bool check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart); +extern int load_hosts(List **hosts, char **err_msg); #endif /* LIBPQ_H */ diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h index f75d2e31a68a8..86f463a284ec4 100644 --- a/src/include/libpq/oauth.h +++ b/src/include/libpq/oauth.h @@ -3,7 +3,7 @@ * oauth.h * Interface to libpq/auth-oauth.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/oauth.h @@ -49,6 +49,20 @@ typedef struct ValidatorModuleResult * delegation. See the validator module documentation for details. */ char *authn_id; + + /* + * When validation fails, this may optionally be set to a string + * containing an explanation for the failure. It will be sent to the + * server log only; it is not provided to the client, and it's ignored if + * validation succeeds. + * + * This description will be attached to the final authentication failure + * message in the logs, as a DETAIL, which may be preferable to separate + * ereport() calls that have to be correlated by the reader. + * + * This string may be either of static duration or palloc'd. + */ + char *error_detail; } ValidatorModuleResult; /* @@ -82,6 +96,17 @@ typedef struct OAuthValidatorCallbacks ValidatorValidateCB validate_cb; } OAuthValidatorCallbacks; +/* + * A validator can register a list of custom option names during its startup_cb, + * then later retrieve the user settings for each during validation. This + * enables per-HBA-line configuration. For more information, refer to the OAuth + * validator modules documentation. + */ +extern void RegisterOAuthHBAOptions(ValidatorModuleState *state, int num, + const char *opts[]); +extern const char *GetOAuthHBAOption(const ValidatorModuleState *state, + const char *optname); + /* * Type of the shared library symbol _PG_oauth_validator_module_init which is * required for all validator modules. This function will be invoked during @@ -93,9 +118,7 @@ extern PGDLLEXPORT const OAuthValidatorCallbacks *_PG_oauth_validator_module_ini /* Implementation */ extern PGDLLIMPORT const pg_be_sasl_mech pg_be_oauth_mech; -/* - * Ensure a validator named in the HBA is permitted by the configuration. - */ extern bool check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg); +extern bool valid_oauth_hba_option_name(const char *name); #endif /* PG_OAUTH_H */ diff --git a/src/include/libpq/pg-gssapi.h b/src/include/libpq/pg-gssapi.h index f49fad14fcc4b..094ae924e8268 100644 --- a/src/include/libpq/pg-gssapi.h +++ b/src/include/libpq/pg-gssapi.h @@ -3,7 +3,7 @@ * pg-gssapi.h * Definitions for including GSSAPI headers * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/pg-gssapi.h diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index f04ca13565398..a29c9c94d796e 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -6,7 +6,7 @@ * NOTE: for historical reasons, this does not correspond to pqcomm.c. * pqcomm.c's routines are declared in libpq.h. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/pqcomm.h @@ -40,7 +40,6 @@ typedef struct } AddrInfo; /* Configure the UNIX socket location for the well known port. */ - #define UNIXSOCK_PATH(path, port, sockdir) \ (AssertMacro(sockdir), \ AssertMacro(*(sockdir) != '\0'), \ @@ -69,6 +68,7 @@ is_unixsock_path(const char *path) return is_absolute_path(path) || path[0] == '@'; } + /* * These manipulate the frontend/backend protocol version number. * @@ -83,7 +83,6 @@ is_unixsock_path(const char *path) * A frontend isn't required to support anything other than the current * version. */ - #define PG_PROTOCOL_MAJOR(v) ((v) >> 16) #define PG_PROTOCOL_MINOR(v) ((v) & 0x0000ffff) #define PG_PROTOCOL_FULL(v) (PG_PROTOCOL_MAJOR(v) * 10000 + PG_PROTOCOL_MINOR(v)) @@ -92,13 +91,45 @@ is_unixsock_path(const char *path) /* * The earliest and latest frontend/backend protocol version supported. */ - #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) #define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) -typedef uint32 ProtocolVersion; /* FE/BE protocol version number */ +/* + * Reserved protocol numbers, which have special semantics: + */ + +/* + * 3.1 would have collided with old pgbouncer deployments, and was skipped. We + * neither emit it nor accept it on the wire. + */ +#define PG_PROTOCOL_RESERVED_31 PG_PROTOCOL(3,1) + +/* + * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used + * for "greasing" (the practice of sending valid, but extraneous or otherwise + * unusual, messages to keep peer implementations honest). This helps ensure + * that servers properly implement protocol version negotiation. Version 3.9999 + * was chosen since it is safely within the valid range, it is representable + * via PQfullProtocolVersion, and it is unlikely to ever be needed in practice. + */ +#define PG_PROTOCOL_GREASE PG_PROTOCOL(3,9999) + +/* + * A client can send a cancel-current-operation request to the postmaster. + * This is uglier than sending it directly to the client's backend, but it + * avoids depending on out-of-band communication facilities. + */ +#define CANCEL_REQUEST_CODE PG_PROTOCOL(1234,5678) + +/* + * A client can also start by sending a SSL or GSSAPI negotiation request to + * get a secure channel. + */ +#define NEGOTIATE_SSL_CODE PG_PROTOCOL(1234,5679) +#define NEGOTIATE_GSS_CODE PG_PROTOCOL(1234,5680) + -typedef ProtocolVersion MsgType; +typedef uint32 ProtocolVersion; /* FE/BE protocol version number */ /* @@ -106,7 +137,6 @@ typedef ProtocolVersion MsgType; * * The initial length is omitted from the packet layouts appearing below. */ - typedef uint32 PacketLen; /* @@ -118,34 +148,28 @@ typedef uint32 PacketLen; #define MAX_STARTUP_PACKET_LENGTH 10000 -typedef uint32 AuthRequest; +typedef uint32 AuthRequest; /* an AUTH_REQ_* code */ /* - * A client can also send a cancel-current-operation request to the postmaster. - * This is uglier than sending it directly to the client's backend, but it - * avoids depending on out-of-band communication facilities. - * - * The cancel request code must not match any protocol version number - * we're ever likely to use. This random choice should do. + * The packet used with a CANCEL_REQUEST_CODE. * * Before PostgreSQL v18 and the protocol version bump from 3.0 to 3.2, the * cancel key was always 4 bytes. With protocol version 3.2, it's variable * length. */ - -#define CANCEL_REQUEST_CODE PG_PROTOCOL(1234,5678) - typedef struct CancelRequestPacket { /* Note that each field is stored in network byte order! */ - MsgType cancelRequestCode; /* code to identify a cancel request */ + ProtocolVersion cancelRequestCode; /* code to identify a cancel request */ uint32 backendPID; /* PID of client's backend */ uint8 cancelAuthCode[FLEXIBLE_ARRAY_MEMBER]; /* secret key to * authorize cancel */ } CancelRequestPacket; -/* Application-Layer Protocol Negotiation is required for direct connections + +/* + * Application-Layer Protocol Negotiation is required for direct connections * to avoid protocol confusion attacks (e.g https://alpaca-attack.com/). * * ALPN is specified in RFC 7301 @@ -165,11 +189,4 @@ typedef struct CancelRequestPacket #define PG_ALPN_PROTOCOL "postgresql" #define PG_ALPN_PROTOCOL_VECTOR { 10, 'p','o','s','t','g','r','e','s','q','l' } -/* - * A client can also start by sending a SSL or GSSAPI negotiation request to - * get a secure channel. - */ -#define NEGOTIATE_SSL_CODE PG_PROTOCOL(1234,5679) -#define NEGOTIATE_GSS_CODE PG_PROTOCOL(1234,5680) - #endif /* PQCOMM_H */ diff --git a/src/include/libpq/pqformat.h b/src/include/libpq/pqformat.h index 9a1534be521c6..bc4ab1381a917 100644 --- a/src/include/libpq/pqformat.h +++ b/src/include/libpq/pqformat.h @@ -3,7 +3,7 @@ * pqformat.h * Definitions for formatting and parsing frontend/backend messages * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/pqformat.h @@ -48,7 +48,7 @@ pq_writeint8(StringInfoData *pg_restrict buf, uint8 i) uint8 ni = i; Assert(buf->len + (int) sizeof(uint8) <= buf->maxlen); - memcpy((char *pg_restrict) (buf->data + buf->len), &ni, sizeof(uint8)); + memcpy(buf->data + buf->len, &ni, sizeof(uint8)); buf->len += sizeof(uint8); } @@ -62,7 +62,7 @@ pq_writeint16(StringInfoData *pg_restrict buf, uint16 i) uint16 ni = pg_hton16(i); Assert(buf->len + (int) sizeof(uint16) <= buf->maxlen); - memcpy((char *pg_restrict) (buf->data + buf->len), &ni, sizeof(uint16)); + memcpy(buf->data + buf->len, &ni, sizeof(uint16)); buf->len += sizeof(uint16); } @@ -76,7 +76,7 @@ pq_writeint32(StringInfoData *pg_restrict buf, uint32 i) uint32 ni = pg_hton32(i); Assert(buf->len + (int) sizeof(uint32) <= buf->maxlen); - memcpy((char *pg_restrict) (buf->data + buf->len), &ni, sizeof(uint32)); + memcpy(buf->data + buf->len, &ni, sizeof(uint32)); buf->len += sizeof(uint32); } @@ -90,7 +90,7 @@ pq_writeint64(StringInfoData *pg_restrict buf, uint64 i) uint64 ni = pg_hton64(i); Assert(buf->len + (int) sizeof(uint64) <= buf->maxlen); - memcpy((char *pg_restrict) (buf->data + buf->len), &ni, sizeof(uint64)); + memcpy(buf->data + buf->len, &ni, sizeof(uint64)); buf->len += sizeof(uint64); } @@ -116,7 +116,7 @@ pq_writestring(StringInfoData *pg_restrict buf, const char *pg_restrict str) Assert(buf->len + slen + 1 <= buf->maxlen); - memcpy(((char *pg_restrict) buf->data + buf->len), p, slen + 1); + memcpy(buf->data + buf->len, p, slen + 1); buf->len += slen + 1; if (p != str) diff --git a/src/include/libpq/pqmq.h b/src/include/libpq/pqmq.h index 348b4494333e1..36780c0816e67 100644 --- a/src/include/libpq/pqmq.h +++ b/src/include/libpq/pqmq.h @@ -3,7 +3,7 @@ * pqmq.h * Use the frontend/backend protocol for communication over a shm_mq * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/pqmq.h @@ -14,6 +14,7 @@ #define PQMQ_H #include "lib/stringinfo.h" +#include "storage/procnumber.h" #include "storage/shm_mq.h" extern void pq_redirect_to_shm_mq(dsm_segment *seg, shm_mq_handle *mqh); diff --git a/src/include/libpq/pqsignal.h b/src/include/libpq/pqsignal.h index 5be7870156ee0..9f494a1fdf922 100644 --- a/src/include/libpq/pqsignal.h +++ b/src/include/libpq/pqsignal.h @@ -3,7 +3,7 @@ * pqsignal.h * Backend signal(2) support (see also src/port/pqsignal.c) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/pqsignal.h diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index b0bcb3cdc26eb..eae8f0e72385b 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -4,7 +4,7 @@ * Definitions of the request/response codes for the wire protocol. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/protocol.h @@ -66,9 +66,31 @@ /* These are the codes sent by parallel workers to leader processes. */ + #define PqMsg_Progress 'P' +/* Replication codes sent by the primary (wrapped in CopyData messages). */ + +#define PqReplMsg_Keepalive 'k' +#define PqReplMsg_PrimaryStatusUpdate 's' +#define PqReplMsg_WALData 'w' + + +/* Replication codes sent by the standby (wrapped in CopyData messages). */ + +#define PqReplMsg_HotStandbyFeedback 'h' +#define PqReplMsg_PrimaryStatusRequest 'p' +#define PqReplMsg_StandbyStatusUpdate 'r' + + +/* Codes used for backups via COPY OUT (wrapped in CopyData messages). */ + +#define PqBackupMsg_Manifest 'm' +#define PqBackupMsg_NewArchive 'n' +#define PqBackupMsg_ProgressReport 'p' + + /* These are the authentication request codes sent by the backend. */ #define AUTH_REQ_OK 0 /* User is authenticated */ diff --git a/src/include/libpq/sasl.h b/src/include/libpq/sasl.h index 581e2986473de..bb2af7a7aff28 100644 --- a/src/include/libpq/sasl.h +++ b/src/include/libpq/sasl.h @@ -7,7 +7,7 @@ * * See src/interfaces/libpq/fe-auth-sasl.h for the frontend counterpart. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/sasl.h @@ -25,6 +25,7 @@ #define PG_SASL_EXCHANGE_CONTINUE 0 #define PG_SASL_EXCHANGE_SUCCESS 1 #define PG_SASL_EXCHANGE_FAILURE 2 +#define PG_SASL_EXCHANGE_ABANDONED 3 /* * Maximum accepted size of SASL messages. @@ -92,8 +93,8 @@ typedef struct pg_be_sasl_mech * * Produces a server challenge to be sent to the client. The callback * must return one of the PG_SASL_EXCHANGE_* values, depending on - * whether the exchange continues, has finished successfully, or has - * failed. + * whether the exchange continues, has finished successfully, has + * failed, or was abandoned by the client. * * Input parameters: * @@ -118,8 +119,9 @@ typedef struct pg_be_sasl_mech * returned and the mechanism requires data to be sent during * a successful outcome). The callback should set this to * NULL if the exchange is over and no output should be sent, - * which should correspond to either PG_SASL_EXCHANGE_FAILURE - * or a PG_SASL_EXCHANGE_SUCCESS with no outcome data. + * which should correspond to either PG_SASL_EXCHANGE_FAILURE, + * PG_SASL_EXCHANGE_ABANDONED, or a PG_SASL_EXCHANGE_SUCCESS + * with no outcome data. * * outputlen: The length of the challenge data. Ignored if *output is * NULL. @@ -128,7 +130,7 @@ typedef struct pg_be_sasl_mech * server log, to disambiguate failure modes. (The client * will only ever see the same generic authentication * failure message.) Ignored if the exchange is completed - * with PG_SASL_EXCHANGE_SUCCESS. + * with PG_SASL_EXCHANGE_SUCCESS or PG_SASL_EXCHANGE_ABANDONED. *--------- */ int (*exchange) (void *state, @@ -142,6 +144,7 @@ typedef struct pg_be_sasl_mech /* Common implementation for auth.c */ extern int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, - char *shadow_pass, const char **logdetail); + char *shadow_pass, const char **logdetail, + bool *abandoned); #endif /* PG_SASL_H */ diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index c1237d3bcd62d..7a6fc836682dd 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -3,7 +3,7 @@ * scram.h * Interface to libpq/scram.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/libpq/scram.h diff --git a/src/include/mb/pg_wchar.h b/src/include/mb/pg_wchar.h index 4b4a9974b75b3..deee2a832c348 100644 --- a/src/include/mb/pg_wchar.h +++ b/src/include/mb/pg_wchar.h @@ -3,7 +3,7 @@ * pg_wchar.h * multibyte-character support * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/mb/pg_wchar.h @@ -39,187 +39,21 @@ typedef unsigned int pg_wchar; #define SS3 0x8f /* single shift 3 (JIS0212) */ /* - * SJIS validation macros - */ -#define ISSJISHEAD(c) (((c) >= 0x81 && (c) <= 0x9f) || ((c) >= 0xe0 && (c) <= 0xfc)) -#define ISSJISTAIL(c) (((c) >= 0x40 && (c) <= 0x7e) || ((c) >= 0x80 && (c) <= 0xfc)) - -/*---------------------------------------------------- - * MULE Internal Encoding (MIC) - * - * This encoding follows the design used within XEmacs; it is meant to - * subsume many externally-defined character sets. Each character includes - * identification of the character set it belongs to, so the encoding is - * general but somewhat bulky. - * - * Currently PostgreSQL supports 5 types of MULE character sets: - * - * 1) 1-byte ASCII characters. Each byte is below 0x80. - * - * 2) "Official" single byte charsets such as ISO-8859-1 (Latin1). - * Each MULE character consists of 2 bytes: LC1 + C1, where LC1 is - * an identifier for the charset (in the range 0x81 to 0x8d) and C1 - * is the character code (in the range 0xa0 to 0xff). - * - * 3) "Private" single byte charsets such as SISHENG. Each MULE - * character consists of 3 bytes: LCPRV1 + LC12 + C1, where LCPRV1 - * is a private-charset flag, LC12 is an identifier for the charset, - * and C1 is the character code (in the range 0xa0 to 0xff). - * LCPRV1 is either 0x9a (if LC12 is in the range 0xa0 to 0xdf) - * or 0x9b (if LC12 is in the range 0xe0 to 0xef). - * - * 4) "Official" multibyte charsets such as JIS X0208. Each MULE - * character consists of 3 bytes: LC2 + C1 + C2, where LC2 is - * an identifier for the charset (in the range 0x90 to 0x99) and C1 - * and C2 form the character code (each in the range 0xa0 to 0xff). - * - * 5) "Private" multibyte charsets such as CNS 11643-1992 Plane 3. - * Each MULE character consists of 4 bytes: LCPRV2 + LC22 + C1 + C2, - * where LCPRV2 is a private-charset flag, LC22 is an identifier for - * the charset, and C1 and C2 form the character code (each in the range - * 0xa0 to 0xff). LCPRV2 is either 0x9c (if LC22 is in the range 0xf0 - * to 0xf4) or 0x9d (if LC22 is in the range 0xf5 to 0xfe). - * - * "Official" encodings are those that have been assigned code numbers by - * the XEmacs project; "private" encodings have Postgres-specific charset - * identifiers. - * - * See the "XEmacs Internals Manual", available at http://www.xemacs.org, - * for more details. Note that for historical reasons, Postgres' - * private-charset flag values do not match what XEmacs says they should be, - * so this isn't really exactly MULE (not that private charsets would be - * interoperable anyway). - * - * Note that XEmacs's implementation is different from what emacs does. - * We follow emacs's implementation, rather than XEmacs's. - *---------------------------------------------------- - */ - -/* - * Charset identifiers (also called "leading bytes" in the MULE documentation) + * EUC_TW planes */ - -/* - * Charset IDs for official single byte encodings (0x81-0x8e) - */ -#define LC_ISO8859_1 0x81 /* ISO8859 Latin 1 */ -#define LC_ISO8859_2 0x82 /* ISO8859 Latin 2 */ -#define LC_ISO8859_3 0x83 /* ISO8859 Latin 3 */ -#define LC_ISO8859_4 0x84 /* ISO8859 Latin 4 */ -#define LC_TIS620 0x85 /* Thai (not supported yet) */ -#define LC_ISO8859_7 0x86 /* Greek (not supported yet) */ -#define LC_ISO8859_6 0x87 /* Arabic (not supported yet) */ -#define LC_ISO8859_8 0x88 /* Hebrew (not supported yet) */ -#define LC_JISX0201K 0x89 /* Japanese 1 byte kana */ -#define LC_JISX0201R 0x8a /* Japanese 1 byte Roman */ -/* Note that 0x8b seems to be unused as of Emacs 20.7. - * However, there might be a chance that 0x8b could be used - * in later versions of Emacs. - */ -#define LC_KOI8_R 0x8b /* Cyrillic KOI8-R */ -#define LC_ISO8859_5 0x8c /* ISO8859 Cyrillic */ -#define LC_ISO8859_9 0x8d /* ISO8859 Latin 5 (not supported yet) */ -#define LC_ISO8859_15 0x8e /* ISO8859 Latin 15 (not supported yet) */ -/* #define CONTROL_1 0x8f control characters (unused) */ - -/* Is a leading byte for "official" single byte encodings? */ -#define IS_LC1(c) ((unsigned char)(c) >= 0x81 && (unsigned char)(c) <= 0x8d) - -/* - * Charset IDs for official multibyte encodings (0x90-0x99) - * 0x9a-0x9d are free. 0x9e and 0x9f are reserved. - */ -#define LC_JISX0208_1978 0x90 /* Japanese Kanji, old JIS (not supported) */ -#define LC_GB2312_80 0x91 /* Chinese */ -#define LC_JISX0208 0x92 /* Japanese Kanji (JIS X 0208) */ -#define LC_KS5601 0x93 /* Korean */ -#define LC_JISX0212 0x94 /* Japanese Kanji (JIS X 0212) */ #define LC_CNS11643_1 0x95 /* CNS 11643-1992 Plane 1 */ #define LC_CNS11643_2 0x96 /* CNS 11643-1992 Plane 2 */ -#define LC_JISX0213_1 0x97 /* Japanese Kanji (JIS X 0213 Plane 1) - * (not supported) */ -#define LC_BIG5_1 0x98 /* Plane 1 Chinese traditional (not - * supported) */ -#define LC_BIG5_2 0x99 /* Plane 1 Chinese traditional (not - * supported) */ - -/* Is a leading byte for "official" multibyte encodings? */ -#define IS_LC2(c) ((unsigned char)(c) >= 0x90 && (unsigned char)(c) <= 0x99) - -/* - * Postgres-specific prefix bytes for "private" single byte encodings - * (According to the MULE docs, we should be using 0x9e for this) - */ -#define LCPRV1_A 0x9a -#define LCPRV1_B 0x9b -#define IS_LCPRV1(c) ((unsigned char)(c) == LCPRV1_A || (unsigned char)(c) == LCPRV1_B) -#define IS_LCPRV1_A_RANGE(c) \ - ((unsigned char)(c) >= 0xa0 && (unsigned char)(c) <= 0xdf) -#define IS_LCPRV1_B_RANGE(c) \ - ((unsigned char)(c) >= 0xe0 && (unsigned char)(c) <= 0xef) - -/* - * Postgres-specific prefix bytes for "private" multibyte encodings - * (According to the MULE docs, we should be using 0x9f for this) - */ -#define LCPRV2_A 0x9c -#define LCPRV2_B 0x9d -#define IS_LCPRV2(c) ((unsigned char)(c) == LCPRV2_A || (unsigned char)(c) == LCPRV2_B) -#define IS_LCPRV2_A_RANGE(c) \ - ((unsigned char)(c) >= 0xf0 && (unsigned char)(c) <= 0xf4) -#define IS_LCPRV2_B_RANGE(c) \ - ((unsigned char)(c) >= 0xf5 && (unsigned char)(c) <= 0xfe) - -/* - * Charset IDs for private single byte encodings (0xa0-0xef) - */ -#define LC_SISHENG 0xa0 /* Chinese SiSheng characters for - * PinYin/ZhuYin (not supported) */ -#define LC_IPA 0xa1 /* IPA (International Phonetic - * Association) (not supported) */ -#define LC_VISCII_LOWER 0xa2 /* Vietnamese VISCII1.1 lower-case (not - * supported) */ -#define LC_VISCII_UPPER 0xa3 /* Vietnamese VISCII1.1 upper-case (not - * supported) */ -#define LC_ARABIC_DIGIT 0xa4 /* Arabic digit (not supported) */ -#define LC_ARABIC_1_COLUMN 0xa5 /* Arabic 1-column (not supported) */ -#define LC_ASCII_RIGHT_TO_LEFT 0xa6 /* ASCII (left half of ISO8859-1) with - * right-to-left direction (not - * supported) */ -#define LC_LAO 0xa7 /* Lao characters (ISO10646 0E80..0EDF) - * (not supported) */ -#define LC_ARABIC_2_COLUMN 0xa8 /* Arabic 1-column (not supported) */ - -/* - * Charset IDs for private multibyte encodings (0xf0-0xff) - */ -#define LC_INDIAN_1_COLUMN 0xf0 /* Indian charset for 1-column width - * glyphs (not supported) */ -#define LC_TIBETAN_1_COLUMN 0xf1 /* Tibetan 1-column width glyphs (not - * supported) */ -#define LC_UNICODE_SUBSET_2 0xf2 /* Unicode characters of the range - * U+2500..U+33FF. (not supported) */ -#define LC_UNICODE_SUBSET_3 0xf3 /* Unicode characters of the range - * U+E000..U+FFFF. (not supported) */ -#define LC_UNICODE_SUBSET 0xf4 /* Unicode characters of the range - * U+0100..U+24FF. (not supported) */ -#define LC_ETHIOPIC 0xf5 /* Ethiopic characters (not supported) */ #define LC_CNS11643_3 0xf6 /* CNS 11643-1992 Plane 3 */ #define LC_CNS11643_4 0xf7 /* CNS 11643-1992 Plane 4 */ #define LC_CNS11643_5 0xf8 /* CNS 11643-1992 Plane 5 */ #define LC_CNS11643_6 0xf9 /* CNS 11643-1992 Plane 6 */ #define LC_CNS11643_7 0xfa /* CNS 11643-1992 Plane 7 */ -#define LC_INDIAN_2_COLUMN 0xfb /* Indian charset for 2-column width - * glyphs (not supported) */ -#define LC_TIBETAN 0xfc /* Tibetan (not supported) */ -/* #define FREE 0xfd free (unused) */ -/* #define FREE 0xfe free (unused) */ -/* #define FREE 0xff free (unused) */ - -/*---------------------------------------------------- - * end of MULE stuff - *---------------------------------------------------- + +/* + * SJIS validation macros */ +#define ISSJISHEAD(c) (((c) >= 0x81 && (c) <= 0x9f) || ((c) >= 0xe0 && (c) <= 0xfc)) +#define ISSJISTAIL(c) (((c) >= 0x40 && (c) <= 0x7e) || ((c) >= 0x80 && (c) <= 0xfc)) /* * PostgreSQL encoding identifiers @@ -246,7 +80,7 @@ typedef enum pg_enc PG_EUC_TW, /* EUC for Taiwan */ PG_EUC_JIS_2004, /* EUC-JIS-2004 */ PG_UTF8, /* Unicode UTF8 */ - PG_MULE_INTERNAL, /* Mule internal code */ + PG_UNUSED_1, /* (Was Mule internal code) */ PG_LATIN1, /* ISO-8859-1 Latin 1 */ PG_LATIN2, /* ISO-8859-2 Latin 2 */ PG_LATIN3, /* ISO-8859-3 Latin 3 */ @@ -290,18 +124,21 @@ typedef enum pg_enc #define PG_ENCODING_BE_LAST PG_KOI8U +#define PG_UNUSED_ENCODING(_enc) \ + ((_enc) == PG_UNUSED_1) + /* * Please use these tests before access to pg_enc2name_tbl[] * or to other places... */ #define PG_VALID_BE_ENCODING(_enc) \ - ((_enc) >= 0 && (_enc) <= PG_ENCODING_BE_LAST) + ((_enc) >= 0 && (_enc) <= PG_ENCODING_BE_LAST && !PG_UNUSED_ENCODING(_enc)) #define PG_ENCODING_IS_CLIENT_ONLY(_enc) \ ((_enc) > PG_ENCODING_BE_LAST && (_enc) < _PG_LAST_ENCODING_) #define PG_VALID_ENCODING(_enc) \ - ((_enc) >= 0 && (_enc) < _PG_LAST_ENCODING_) + ((_enc) >= 0 && (_enc) < _PG_LAST_ENCODING_ && !PG_UNUSED_ENCODING(_enc)) /* On FE are possible all encodings */ #define PG_VALID_FE_ENCODING(_enc) PG_VALID_ENCODING(_enc) @@ -532,25 +369,25 @@ typedef uint32 (*utf_local_conversion_func) (uint32 code); * Some handy functions for Unicode-specific tests. */ static inline bool -is_valid_unicode_codepoint(pg_wchar c) +is_valid_unicode_codepoint(char32_t c) { return (c > 0 && c <= 0x10FFFF); } static inline bool -is_utf16_surrogate_first(pg_wchar c) +is_utf16_surrogate_first(char32_t c) { return (c >= 0xD800 && c <= 0xDBFF); } static inline bool -is_utf16_surrogate_second(pg_wchar c) +is_utf16_surrogate_second(char32_t c) { return (c >= 0xDC00 && c <= 0xDFFF); } -static inline pg_wchar -surrogate_pair_to_codepoint(pg_wchar first, pg_wchar second) +static inline char32_t +surrogate_pair_to_codepoint(char16_t first, char16_t second) { return ((first & 0x3FF) << 10) + 0x10000 + (second & 0x3FF); } @@ -561,20 +398,20 @@ surrogate_pair_to_codepoint(pg_wchar first, pg_wchar second) * * No error checks here, c must point to a long-enough string. */ -static inline pg_wchar +static inline char32_t utf8_to_unicode(const unsigned char *c) { if ((*c & 0x80) == 0) - return (pg_wchar) c[0]; + return (char32_t) c[0]; else if ((*c & 0xe0) == 0xc0) - return (pg_wchar) (((c[0] & 0x1f) << 6) | + return (char32_t) (((c[0] & 0x1f) << 6) | (c[1] & 0x3f)); else if ((*c & 0xf0) == 0xe0) - return (pg_wchar) (((c[0] & 0x0f) << 12) | + return (char32_t) (((c[0] & 0x0f) << 12) | ((c[1] & 0x3f) << 6) | (c[2] & 0x3f)); else if ((*c & 0xf8) == 0xf0) - return (pg_wchar) (((c[0] & 0x07) << 18) | + return (char32_t) (((c[0] & 0x07) << 18) | ((c[1] & 0x3f) << 12) | ((c[2] & 0x3f) << 6) | (c[3] & 0x3f)); @@ -588,7 +425,7 @@ utf8_to_unicode(const unsigned char *c) * unicode_utf8len(c) bytes available. */ static inline unsigned char * -unicode_to_utf8(pg_wchar c, unsigned char *utf8string) +unicode_to_utf8(char32_t c, unsigned char *utf8string) { if (c <= 0x7F) { @@ -620,7 +457,7 @@ unicode_to_utf8(pg_wchar c, unsigned char *utf8string) * Number of bytes needed to represent the given char in UTF8. */ static inline int -unicode_utf8len(pg_wchar c) +unicode_utf8len(char32_t c) { if (c <= 0x7F) return 1; @@ -676,11 +513,8 @@ extern int pg_valid_server_encoding(const char *name); extern bool is_encoding_supported_by_icu(int encoding); extern const char *get_encoding_name_for_icu(int encoding); -extern unsigned char *unicode_to_utf8(pg_wchar c, unsigned char *utf8string); -extern pg_wchar utf8_to_unicode(const unsigned char *c); extern bool pg_utf8_islegal(const unsigned char *source, int length); extern int pg_utf_mblen(const unsigned char *s); -extern int pg_mule_mblen(const unsigned char *s); /* * The remaining functions are backend-only. @@ -697,7 +531,14 @@ extern int pg_char_and_wchar_strcmp(const char *s1, const pg_wchar *s2); extern int pg_wchar_strncmp(const pg_wchar *s1, const pg_wchar *s2, size_t n); extern int pg_char_and_wchar_strncmp(const char *s1, const pg_wchar *s2, size_t n); extern size_t pg_wchar_strlen(const pg_wchar *str); +extern int pg_mblen_cstr(const char *mbstr); +extern int pg_mblen_range(const char *mbstr, const char *end); +extern int pg_mblen_with_len(const char *mbstr, int limit); +extern int pg_mblen_unbounded(const char *mbstr); + +/* deprecated */ extern int pg_mblen(const char *mbstr); + extern int pg_dsplen(const char *mbstr); extern int pg_mbstrlen(const char *mbstr); extern int pg_mbstrlen_with_len(const char *mbstr, int limit); @@ -739,8 +580,8 @@ extern char *pg_server_to_client(const char *s, int len); extern char *pg_any_to_server(const char *s, int len, int encoding); extern char *pg_server_to_any(const char *s, int len, int encoding); -extern void pg_unicode_to_server(pg_wchar c, unsigned char *s); -extern bool pg_unicode_to_server_noerror(pg_wchar c, unsigned char *s); +extern void pg_unicode_to_server(char32_t c, unsigned char *s); +extern bool pg_unicode_to_server_noerror(char32_t c, unsigned char *s); extern unsigned short BIG5toCNS(unsigned short big5, unsigned char *lc); extern unsigned short CNStoBIG5(unsigned short cns, unsigned char lc); @@ -777,16 +618,6 @@ pg_noreturn extern void report_untranslatable_char(int src_encoding, int dest_en extern int local2local(const unsigned char *l, unsigned char *p, int len, int src_encoding, int dest_encoding, const unsigned char *tab, bool noError); -extern int latin2mic(const unsigned char *l, unsigned char *p, int len, - int lc, int encoding, bool noError); -extern int mic2latin(const unsigned char *mic, unsigned char *p, int len, - int lc, int encoding, bool noError); -extern int latin2mic_with_table(const unsigned char *l, unsigned char *p, - int len, int lc, int encoding, - const unsigned char *tab, bool noError); -extern int mic2latin_with_table(const unsigned char *mic, unsigned char *p, - int len, int lc, int encoding, - const unsigned char *tab, bool noError); #ifdef WIN32 extern WCHAR *pgwin32_message_to_UTF16(const char *str, int len, int *utf16len); diff --git a/src/include/mb/stringinfo_mb.h b/src/include/mb/stringinfo_mb.h index f682e7b539ca1..919ee5e56843c 100644 --- a/src/include/mb/stringinfo_mb.h +++ b/src/include/mb/stringinfo_mb.h @@ -3,7 +3,7 @@ * stringinfo_mb.h * multibyte support for StringInfo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/mb/stringinfo_mb.h diff --git a/src/include/meson.build b/src/include/meson.build index 2e4b7aa529e26..7d734d92dabc4 100644 --- a/src/include/meson.build +++ b/src/include/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_config_os = configure_file( output: 'pg_config_os.h', @@ -36,9 +36,15 @@ config_paths_data.set_quoted('MANDIR', dir_prefix / dir_man) var_cc = ' '.join(cc.cmd_array()) var_cpp = ' '.join(cc.cmd_array() + ['-E']) var_cflags = ' '.join(cflags + cflags_builtin + cflags_warn + get_option('c_args')) -if llvm.found() +if have_cxx + var_cxx = ' '.join(cxx.cmd_array()) var_cxxflags = ' '.join(cxxflags + cxxflags_builtin + cxxflags_warn + get_option('cpp_args')) else + # Default to 'g++' so that PGXS users get a clear "g++ not found" + # error when building C++ extensions. Otherwise, they'd get a + # confusing error because no binary is specified in the build + # commands and the first flag would be interpreted as the program. + var_cxx = 'g++' var_cxxflags = '' endif var_cppflags = ' '.join(cppflags) @@ -167,6 +173,7 @@ install_subdir('catalog', exclude_files: [ '.gitignore', 'Makefile', + 'README', 'duplicate_oids', 'meson.build', 'reformat_dat_file.pl', @@ -177,3 +184,24 @@ install_subdir('catalog', # autoconf generates the file there, ensure we get a conflict generated_sources_ac += {'src/include': ['stamp-h']} + + +# Instead of having targets depending directly on a list of all generated +# headers, have them depend on a stamp files for all of them. Dependencies on +# headers are implemented as order-only dependencies in meson (and then using +# compiler generated dependencies during incremental rebuilds ). The benefit +# of using a stamp file is that it makes ninja.build considerably smaller and +# meson setup faster, as otherwise the list of headers is repeated for every C +# file, bloating build.ninja by ~2x. +generated_headers_stamp = custom_target('generated-headers-stamp.h', + output: 'generated-headers-stamp.h', + input: generated_headers, + command: stamp_cmd, +) + +generated_backend_headers_stamp = custom_target('generated-backend-headers-stamp.h', + output: 'generated-backend-headers-stamp.h', + input: generated_backend_headers, + depends: generated_headers_stamp, + command: stamp_cmd, +) diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c363..8ccdf61246b15 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -10,7 +10,7 @@ * Over time, this has also become the preferred place for widely known * resource-limitation stuff, such as work_mem and check_stack_depth(). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/miscadmin.h @@ -90,6 +90,8 @@ extern PGDLLIMPORT volatile sig_atomic_t InterruptPending; extern PGDLLIMPORT volatile sig_atomic_t QueryCancelPending; extern PGDLLIMPORT volatile sig_atomic_t ProcDiePending; +extern PGDLLIMPORT volatile int ProcDieSenderPid; +extern PGDLLIMPORT volatile int ProcDieSenderUid; extern PGDLLIMPORT volatile sig_atomic_t IdleInTransactionSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; @@ -114,7 +116,8 @@ extern void ProcessInterrupts(void); (unlikely(InterruptPending)) #else #define INTERRUPTS_PENDING_CONDITION() \ - (unlikely(UNBLOCKED_SIGNAL_QUEUE()) ? pgwin32_dispatch_queued_signals() : 0, \ + (unlikely(UNBLOCKED_SIGNAL_QUEUE()) ? \ + pgwin32_dispatch_queued_signals() : (void) 0, \ unlikely(InterruptPending)) #endif @@ -177,6 +180,7 @@ extern PGDLLIMPORT int MaxBackends; extern PGDLLIMPORT int MaxConnections; extern PGDLLIMPORT int max_worker_processes; extern PGDLLIMPORT int max_parallel_workers; +extern PGDLLIMPORT int autovacuum_max_parallel_workers; extern PGDLLIMPORT int commit_timestamp_buffers; extern PGDLLIMPORT int multixact_member_buffers; @@ -308,6 +312,15 @@ extern void PreventCommandIfReadOnly(const char *cmdname); extern void PreventCommandIfParallelMode(const char *cmdname); extern void PreventCommandDuringRecovery(const char *cmdname); +/* in replication/snapbuild.c */ + +/* + * Keep track of whether logical decoding in this backend promised not to + * access shared catalogs, as a safety check. This is checked by genam.c when + * a catalog scan takes place to verify that no shared catalogs are accessed. + */ +extern PGDLLIMPORT bool accessSharedCatalogsInDecoding; + /***************************************************************************** * pdir.h -- * * POSTGRES directory path definitions. * @@ -366,6 +379,9 @@ typedef enum BackendType B_WAL_SUMMARIZER, B_WAL_WRITER, + B_DATACHECKSUMSWORKER_LAUNCHER, + B_DATACHECKSUMSWORKER_WORKER, + /* * Logger is not connected to shared memory and does not have a PGPROC * entry. @@ -391,6 +407,9 @@ extern PGDLLIMPORT BackendType MyBackendType; #define AmWalSummarizerProcess() (MyBackendType == B_WAL_SUMMARIZER) #define AmWalWriterProcess() (MyBackendType == B_WAL_WRITER) #define AmIoWorkerProcess() (MyBackendType == B_IO_WORKER) +#define AmDataChecksumsWorkerProcess() \ + (MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \ + MyBackendType == B_DATACHECKSUMSWORKER_WORKER) #define AmSpecialWorkerProcess() \ (AmAutoVacuumLauncherProcess() || \ @@ -503,9 +522,10 @@ extern void InitializeMaxBackends(void); extern void InitializeFastPathLocks(void); extern void InitPostgres(const char *in_dbname, Oid dboid, const char *username, Oid useroid, - bits32 flags, + uint32 flags, char *out_dbname); extern void BaseInit(void); +extern void StoreConnectionWarning(char *msg, char *detail); /* in utils/init/miscinit.c */ extern PGDLLIMPORT bool IgnoreSystemIndexes; diff --git a/src/include/nodes/bitmapset.h b/src/include/nodes/bitmapset.h index 03faca93084c6..067ec72e99bce 100644 --- a/src/include/nodes/bitmapset.h +++ b/src/include/nodes/bitmapset.h @@ -9,7 +9,7 @@ * empty set by a NULL pointer. * * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/nodes/bitmapset.h * diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2492282213ff3..13359180d256a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -19,7 +19,7 @@ * not provided. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/execnodes.h @@ -29,35 +29,47 @@ #ifndef EXECNODES_H #define EXECNODES_H -#include "access/tupconvert.h" -#include "executor/instrument.h" +#include "access/htup.h" +#include "executor/instrument_node.h" #include "fmgr.h" #include "lib/ilist.h" -#include "lib/pairingheap.h" #include "nodes/miscnodes.h" #include "nodes/params.h" #include "nodes/plannodes.h" -#include "nodes/tidbitmap.h" #include "partitioning/partdefs.h" -#include "storage/condition_variable.h" -#include "utils/hsearch.h" -#include "utils/queryenvironment.h" +#include "storage/buf.h" #include "utils/reltrigger.h" -#include "utils/sharedtuplestore.h" -#include "utils/snapshot.h" -#include "utils/sortsupport.h" -#include "utils/tuplesort.h" -#include "utils/tuplestore.h" - -struct PlanState; /* forward references in this file */ -struct ParallelHashJoinState; -struct ExecRowMark; -struct ExprState; -struct ExprContext; -struct RangeTblEntry; /* avoid including parsenodes.h here */ -struct ExprEvalStep; /* avoid including execExpr.h everywhere */ -struct CopyMultiInsertBuffer; -struct LogicalTapeSet; +#include "utils/typcache.h" + + +/* + * forward references in this file + */ +typedef struct BufferUsage BufferUsage; +typedef struct ExecRowMark ExecRowMark; +typedef struct ExprState ExprState; +typedef struct ExprContext ExprContext; +typedef struct HTAB HTAB; +typedef struct Instrumentation Instrumentation; +typedef struct pairingheap pairingheap; +typedef struct PlanState PlanState; +typedef struct QueryEnvironment QueryEnvironment; +typedef struct RelationData *Relation; +typedef Relation *RelationPtr; +typedef struct ScanKeyData ScanKeyData; +typedef struct SnapshotData *Snapshot; +typedef struct SortSupportData *SortSupport; +typedef struct TIDBitmap TIDBitmap; +typedef struct NodeInstrumentation NodeInstrumentation; +typedef struct TriggerInstrumentation TriggerInstrumentation; +typedef struct TupleConversionMap TupleConversionMap; +typedef struct TupleDescData *TupleDesc; +typedef struct Tuplesortstate Tuplesortstate; +typedef struct Tuplestorestate Tuplestorestate; +typedef struct TupleTableSlot TupleTableSlot; +typedef struct TupleTableSlotOps TupleTableSlotOps; +typedef struct WalUsage WalUsage; +typedef struct WorkerNodeInstrumentation WorkerNodeInstrumentation; /* ---------------- @@ -67,8 +79,8 @@ struct LogicalTapeSet; * It contains instructions (in ->steps) to evaluate the expression. * ---------------- */ -typedef Datum (*ExprStateEvalFunc) (struct ExprState *expression, - struct ExprContext *econtext, +typedef Datum (*ExprStateEvalFunc) (ExprState *expression, + ExprContext *econtext, bool *isNull); /* Bits in ExprState->flags (see also execExpr.h for private flag bits): */ @@ -131,7 +143,7 @@ typedef struct ExprState int steps_alloc; /* allocated length of steps array */ #define FIELDNO_EXPRSTATE_PARENT 11 - struct PlanState *parent; /* parent PlanState node, if any */ + PlanState *parent; /* parent PlanState node, if any */ ParamListInfo ext_params; /* for compiling PARAM_EXTERN nodes */ Datum *innermost_caseval; @@ -157,34 +169,6 @@ typedef struct ExprState * entries for a particular index. Used for both index_build and * retail creation of index entries. * - * NumIndexAttrs total number of columns in this index - * NumIndexKeyAttrs number of key columns in index - * IndexAttrNumbers underlying-rel attribute numbers used as keys - * (zeroes indicate expressions). It also contains - * info about included columns. - * Expressions expr trees for expression entries, or NIL if none - * ExpressionsState exec state for expressions, or NIL if none - * Predicate partial-index predicate, or NIL if none - * PredicateState exec state for predicate, or NIL if none - * ExclusionOps Per-column exclusion operators, or NULL if none - * ExclusionProcs Underlying function OIDs for ExclusionOps - * ExclusionStrats Opclass strategy numbers for ExclusionOps - * UniqueOps These are like Exclusion*, but for unique indexes - * UniqueProcs - * UniqueStrats - * Unique is it a unique index? - * OpclassOptions opclass-specific options, or NULL if none - * ReadyForInserts is it valid for inserts? - * CheckedUnchanged IndexUnchanged status determined yet? - * IndexUnchanged aminsert hint, cached for retail inserts - * Concurrent are we doing a concurrent index build? - * BrokenHotChain did we detect any broken HOT chains? - * Summarizing is it a summarizing index? - * ParallelWorkers # of workers requested (excludes leader) - * Am Oid of index AM - * AmCache private cache area for index AM - * Context memory context holding this IndexInfo - * * ii_Concurrent, ii_BrokenHotChain, and ii_ParallelWorkers are used only * during index build; they're conventionally zeroed otherwise. * ---------------- @@ -192,31 +176,67 @@ typedef struct ExprState typedef struct IndexInfo { NodeTag type; - int ii_NumIndexAttrs; /* total number of columns in index */ - int ii_NumIndexKeyAttrs; /* number of key columns in index */ + + /* total number of columns in index */ + int ii_NumIndexAttrs; + /* number of key columns in index */ + int ii_NumIndexKeyAttrs; + + /* + * Underlying-rel attribute numbers used as keys (zeroes indicate + * expressions). It also contains info about included columns. + */ AttrNumber ii_IndexAttrNumbers[INDEX_MAX_KEYS]; + + /* expr trees for expression entries, or NIL if none */ List *ii_Expressions; /* list of Expr */ + /* exec state for expressions, or NIL if none */ List *ii_ExpressionsState; /* list of ExprState */ + + /* partial-index predicate, or NIL if none */ List *ii_Predicate; /* list of Expr */ + /* exec state for expressions, or NIL if none */ ExprState *ii_PredicateState; + + /* Per-column exclusion operators, or NULL if none */ Oid *ii_ExclusionOps; /* array with one entry per column */ + /* Underlying function OIDs for ExclusionOps */ Oid *ii_ExclusionProcs; /* array with one entry per column */ + /* Opclass strategy numbers for ExclusionOps */ uint16 *ii_ExclusionStrats; /* array with one entry per column */ + + /* These are like Exclusion*, but for unique indexes */ Oid *ii_UniqueOps; /* array with one entry per column */ Oid *ii_UniqueProcs; /* array with one entry per column */ uint16 *ii_UniqueStrats; /* array with one entry per column */ + + /* is it a unique index? */ bool ii_Unique; + /* is NULLS NOT DISTINCT? */ bool ii_NullsNotDistinct; + /* is it valid for inserts? */ bool ii_ReadyForInserts; + /* IndexUnchanged status determined yet? */ bool ii_CheckedUnchanged; + /* aminsert hint, cached for retail inserts */ bool ii_IndexUnchanged; + /* are we doing a concurrent index build? */ bool ii_Concurrent; + /* did we detect any broken HOT chains? */ bool ii_BrokenHotChain; + /* is it a summarizing index? */ bool ii_Summarizing; + /* is it a WITHOUT OVERLAPS index? */ bool ii_WithoutOverlaps; + /* # of workers requested (excludes leader) */ int ii_ParallelWorkers; + + /* Oid of index AM */ Oid ii_Am; + /* private cache area for index AM */ void *ii_AmCache; + + /* memory context holding this IndexInfo */ MemoryContext ii_Context; } IndexInfo; @@ -416,19 +436,20 @@ typedef struct JunkFilter } JunkFilter; /* - * OnConflictSetState + * OnConflictActionState * - * Executor state of an ON CONFLICT DO UPDATE operation. + * Executor state of an ON CONFLICT DO SELECT/UPDATE operation. */ -typedef struct OnConflictSetState +typedef struct OnConflictActionState { NodeTag type; TupleTableSlot *oc_Existing; /* slot to store existing target tuple in */ TupleTableSlot *oc_ProjSlot; /* CONFLICT ... SET ... projection target */ ProjectionInfo *oc_ProjInfo; /* for ON CONFLICT DO UPDATE SET */ + LockClauseStrength oc_LockStrength; /* lock strength for DO SELECT */ ExprState *oc_WhereClause; /* state for the WHERE clause */ -} OnConflictSetState; +} OnConflictActionState; /* ---------------- * MergeActionState information @@ -446,6 +467,24 @@ typedef struct MergeActionState ExprState *mas_whenqual; /* WHEN [NOT] MATCHED AND conditions */ } MergeActionState; +/* + * ForPortionOfState + * + * Executor state of a FOR PORTION OF operation. + */ +typedef struct ForPortionOfState +{ + NodeTag type; + + char *fp_rangeName; /* the column named in FOR PORTION OF */ + Oid fp_rangeType; /* the type of the FOR PORTION OF expression */ + int fp_rangeAttno; /* the attno of the range column */ + Datum fp_targetRange; /* the range/multirange from FOR PORTION OF */ + TypeCacheEntry *fp_leftoverstypcache; /* type cache entry of the range */ + TupleTableSlot *fp_Existing; /* slot to store old tuple */ + TupleTableSlot *fp_Leftover; /* slot to store leftover */ +} ForPortionOfState; + /* * ResultRelInfo * @@ -515,7 +554,7 @@ typedef struct ResultRelInfo ExprState **ri_TrigWhenExprs; /* optional runtime measurements for triggers */ - Instrumentation *ri_TrigInstrument; + TriggerInstrumentation *ri_TrigInstrument; /* On-demand created slots for triggers / returning processing */ TupleTableSlot *ri_ReturningSlot; /* for trigger output tuples */ @@ -573,8 +612,8 @@ typedef struct ResultRelInfo /* list of arbiter indexes to use to check conflicts */ List *ri_onConflictArbiterIndexes; - /* ON CONFLICT evaluation state */ - OnConflictSetState *ri_onConflict; + /* ON CONFLICT evaluation state for DO SELECT/UPDATE */ + OnConflictActionState *ri_onConflict; /* for MERGE, lists of MergeActionState (one per MergeMatchKind) */ List *ri_MergeActions[NUM_MERGE_MATCH_KINDS]; @@ -582,6 +621,9 @@ typedef struct ResultRelInfo /* for MERGE, expr state for checking the join condition */ ExprState *ri_MergeJoinCondition; + /* FOR PORTION OF evaluation state */ + ForPortionOfState *ri_forPortionOf; + /* partition check expression state (NULL if not set up yet) */ ExprState *ri_PartitionCheckExpr; @@ -601,15 +643,13 @@ typedef struct ResultRelInfo bool ri_RootToChildMapValid; /* - * Information needed by tuple routing target relations + * Other information needed by child result relations * - * RootResultRelInfo gives the target relation mentioned in the query, if - * it's a partitioned table. It is not set if the target relation - * mentioned in the query is an inherited table, nor when tuple routing is - * not needed. + * ri_RootResultRelInfo gives the target relation mentioned in the query. + * Used as the root for tuple routing and/or transition capture. * - * PartitionTupleSlot is non-NULL if RootToChild conversion is needed and - * the relation is a partition. + * ri_PartitionTupleSlot is non-NULL if the relation is a partition to + * route tuples into and ri_RootToChildMap conversion is needed. */ struct ResultRelInfo *ri_RootResultRelInfo; TupleTableSlot *ri_PartitionTupleSlot; @@ -632,8 +672,8 @@ typedef struct ResultRelInfo */ typedef struct AsyncRequest { - struct PlanState *requestor; /* Node that wants a tuple */ - struct PlanState *requestee; /* Node from which a tuple is wanted */ + PlanState *requestor; /* Node that wants a tuple */ + PlanState *requestee; /* Node from which a tuple is wanted */ int request_index; /* Scratch space for requestor */ bool callback_pending; /* Callback is needed */ bool request_complete; /* Request complete, result valid */ @@ -659,8 +699,8 @@ typedef struct EState Index es_range_table_size; /* size of the range table arrays */ Relation *es_relations; /* Array of per-range-table-entry Relation * pointers, or NULL if not yet opened */ - struct ExecRowMark **es_rowmarks; /* Array of per-range-table-entry - * ExecRowMarks, or NULL if none */ + ExecRowMark **es_rowmarks; /* Array of per-range-table-entry + * ExecRowMarks, or NULL if none */ List *es_rteperminfos; /* List of RTEPermissionInfo */ PlannedStmt *es_plannedstmt; /* link to top of plan tree */ List *es_part_prune_infos; /* List of PartitionPruneInfo */ @@ -840,9 +880,15 @@ typedef struct ExecAuxRowMark typedef struct TupleHashEntryData *TupleHashEntry; typedef struct TupleHashTableData *TupleHashTable; +/* + * TupleHashEntryData is a slot in the tuplehash_hash table. If it's + * populated, it contains a pointer to a MinimalTuple that can also have + * associated "additional data". That's stored in the TupleHashTable's + * tuplescxt. + */ typedef struct TupleHashEntryData { - MinimalTuple firstTuple; /* copy of first tuple in this group */ + MinimalTuple firstTuple; /* -> copy of first tuple in this group */ uint32 status; /* hash status */ uint32 hash; /* hash value (cached) */ } TupleHashEntryData; @@ -857,13 +903,13 @@ typedef struct TupleHashEntryData typedef struct TupleHashTableData { - tuplehash_hash *hashtab; /* underlying hash table */ + tuplehash_hash *hashtab; /* underlying simplehash hash table */ int numCols; /* number of columns in lookup key */ AttrNumber *keyColIdx; /* attr numbers of key columns */ ExprState *tab_hash_expr; /* ExprState for hashing table datatype(s) */ ExprState *tab_eq_func; /* comparator for table datatype(s) */ Oid *tab_collations; /* collations for hash and comparison */ - MemoryContext tablecxt; /* memory context containing table */ + MemoryContext tuplescxt; /* memory context storing hashed tuples */ MemoryContext tempcxt; /* context for function evaluations */ Size additionalsize; /* size of additional data */ TupleTableSlot *tableslot; /* slot for referencing table entries */ @@ -872,7 +918,7 @@ typedef struct TupleHashTableData ExprState *in_hash_expr; /* ExprState for hashing input datatype(s) */ ExprState *cur_eq_func; /* comparator for input vs. table */ ExprContext *exprcontext; /* expression context */ -} TupleHashTableData; +} TupleHashTableData; typedef tuplehash_iterator TupleHashIterator; @@ -1000,8 +1046,8 @@ typedef struct SubPlanState { NodeTag type; SubPlan *subplan; /* expression plan node */ - struct PlanState *planstate; /* subselect plan's state tree */ - struct PlanState *parent; /* parent plan node's state tree */ + PlanState *planstate; /* subselect plan's state tree */ + PlanState *parent; /* parent plan node's state tree */ ExprState *testexpr; /* state of combining expression */ HeapTuple curTuple; /* copy of most recent tuple from subplan */ Datum curArray; /* most recent array from ARRAY() subplan */ @@ -1013,8 +1059,7 @@ typedef struct SubPlanState TupleHashTable hashnulls; /* hash table for rows with null(s) */ bool havehashrows; /* true if hashtable is not empty */ bool havenullrows; /* true if hashnulls is not empty */ - MemoryContext hashtablecxt; /* memory context containing hash tables */ - MemoryContext hashtempcxt; /* temp memory context for hash tables */ + MemoryContext tuplesContext; /* context containing hash tables' tuples */ ExprContext *innerecontext; /* econtext for computing inner tuples */ int numCols; /* number of columns being hashed */ /* each of the remaining fields is an array of length numCols: */ @@ -1138,7 +1183,7 @@ typedef struct JsonExprState * if no more tuples are available. * ---------------- */ -typedef TupleTableSlot *(*ExecProcNodeMtd) (struct PlanState *pstate); +typedef TupleTableSlot *(*ExecProcNodeMtd) (PlanState *pstate); /* ---------------- * PlanState node @@ -1163,8 +1208,10 @@ typedef struct PlanState ExecProcNodeMtd ExecProcNodeReal; /* actual function, if above is a * wrapper */ - Instrumentation *instrument; /* Optional runtime stats for this node */ - WorkerInstrumentation *worker_instrument; /* per-worker instrumentation */ + NodeInstrumentation *instrument; /* Optional runtime stats for this + * node */ + WorkerNodeInstrumentation *worker_instrument; /* per-worker + * instrumentation */ /* Per-worker JIT instrumentation */ struct SharedJitInstrumentation *worker_jit_instrument; @@ -1175,8 +1222,8 @@ typedef struct PlanState * subPlan list, which does not exist in the plan tree). */ ExprState *qual; /* boolean qual condition */ - struct PlanState *lefttree; /* input plan tree(s) */ - struct PlanState *righttree; + PlanState *lefttree; /* input plan tree(s) */ + PlanState *righttree; List *initPlan; /* Init SubPlanState nodes (un-correlated expr * subselects) */ @@ -1563,7 +1610,7 @@ typedef struct RecursiveUnionState FmgrInfo *hashfunctions; /* per-grouping-field hash fns */ MemoryContext tempContext; /* short-term context for comparisons */ TupleHashTable hashtable; /* hash table for tuples already seen */ - MemoryContext tableContext; /* memory context containing hash table */ + MemoryContext tuplesContext; /* context containing hash table's tuples */ } RecursiveUnionState; /* ---------------- @@ -1623,6 +1670,7 @@ typedef struct SeqScanState { ScanState ss; /* its first field is NodeTag */ Size pscan_len; /* size of parallel heap scan descriptor */ + struct SharedSeqScanInstrumentation *sinstrument; } SeqScanState; /* ---------------- @@ -1653,14 +1701,14 @@ typedef struct SampleScanState */ typedef struct { - struct ScanKeyData *scan_key; /* scankey to put value into */ + ScanKeyData *scan_key; /* scankey to put value into */ ExprState *key_expr; /* expr to evaluate to get value */ bool key_toastable; /* is expr's result a toastable datatype? */ } IndexRuntimeKeyInfo; typedef struct { - struct ScanKeyData *scan_key; /* scankey to put value into */ + ScanKeyData *scan_key; /* scankey to put value into */ ExprState *array_expr; /* expr to evaluate to get array value */ int next_elem; /* next array element to use */ int num_elems; /* number of elems in current array value */ @@ -1701,9 +1749,9 @@ typedef struct IndexScanState ScanState ss; /* its first field is NodeTag */ ExprState *indexqualorig; List *indexorderbyorig; - struct ScanKeyData *iss_ScanKeys; + ScanKeyData *iss_ScanKeys; int iss_NumScanKeys; - struct ScanKeyData *iss_OrderByKeys; + ScanKeyData *iss_OrderByKeys; int iss_NumOrderByKeys; IndexRuntimeKeyInfo *iss_RuntimeKeys; int iss_NumRuntimeKeys; @@ -1711,7 +1759,7 @@ typedef struct IndexScanState ExprContext *iss_RuntimeContext; Relation iss_RelationDesc; struct IndexScanDescData *iss_ScanDesc; - IndexScanInstrumentation iss_Instrument; + IndexScanInstrumentation *iss_Instrument; SharedIndexScanInstrumentation *iss_SharedInfo; /* These are needed for re-checking ORDER BY expr ordering */ @@ -1752,9 +1800,9 @@ typedef struct IndexOnlyScanState { ScanState ss; /* its first field is NodeTag */ ExprState *recheckqual; - struct ScanKeyData *ioss_ScanKeys; + ScanKeyData *ioss_ScanKeys; int ioss_NumScanKeys; - struct ScanKeyData *ioss_OrderByKeys; + ScanKeyData *ioss_OrderByKeys; int ioss_NumOrderByKeys; IndexRuntimeKeyInfo *ioss_RuntimeKeys; int ioss_NumRuntimeKeys; @@ -1762,7 +1810,7 @@ typedef struct IndexOnlyScanState ExprContext *ioss_RuntimeContext; Relation ioss_RelationDesc; struct IndexScanDescData *ioss_ScanDesc; - IndexScanInstrumentation ioss_Instrument; + IndexScanInstrumentation *ioss_Instrument; SharedIndexScanInstrumentation *ioss_SharedInfo; TupleTableSlot *ioss_TableSlot; Buffer ioss_VMBuffer; @@ -1793,7 +1841,7 @@ typedef struct BitmapIndexScanState { ScanState ss; /* its first field is NodeTag */ TIDBitmap *biss_result; - struct ScanKeyData *biss_ScanKeys; + ScanKeyData *biss_ScanKeys; int biss_NumScanKeys; IndexRuntimeKeyInfo *biss_RuntimeKeys; int biss_NumRuntimeKeys; @@ -1803,72 +1851,10 @@ typedef struct BitmapIndexScanState ExprContext *biss_RuntimeContext; Relation biss_RelationDesc; struct IndexScanDescData *biss_ScanDesc; - IndexScanInstrumentation biss_Instrument; + IndexScanInstrumentation *biss_Instrument; SharedIndexScanInstrumentation *biss_SharedInfo; } BitmapIndexScanState; -/* ---------------- - * BitmapHeapScanInstrumentation information - * - * exact_pages total number of exact pages retrieved - * lossy_pages total number of lossy pages retrieved - * ---------------- - */ -typedef struct BitmapHeapScanInstrumentation -{ - uint64 exact_pages; - uint64 lossy_pages; -} BitmapHeapScanInstrumentation; - -/* ---------------- - * SharedBitmapState information - * - * BM_INITIAL TIDBitmap creation is not yet started, so first worker - * to see this state will set the state to BM_INPROGRESS - * and that process will be responsible for creating - * TIDBitmap. - * BM_INPROGRESS TIDBitmap creation is in progress; workers need to - * sleep until it's finished. - * BM_FINISHED TIDBitmap creation is done, so now all workers can - * proceed to iterate over TIDBitmap. - * ---------------- - */ -typedef enum -{ - BM_INITIAL, - BM_INPROGRESS, - BM_FINISHED, -} SharedBitmapState; - -/* ---------------- - * ParallelBitmapHeapState information - * tbmiterator iterator for scanning current pages - * mutex mutual exclusion for state - * state current state of the TIDBitmap - * cv conditional wait variable - * ---------------- - */ -typedef struct ParallelBitmapHeapState -{ - dsa_pointer tbmiterator; - slock_t mutex; - SharedBitmapState state; - ConditionVariable cv; -} ParallelBitmapHeapState; - -/* ---------------- - * Instrumentation data for a parallel bitmap heap scan. - * - * A shared memory struct that each parallel worker copies its - * BitmapHeapScanInstrumentation information into at executor shutdown to - * allow the leader to display the information in EXPLAIN ANALYZE. - * ---------------- - */ -typedef struct SharedBitmapHeapInstrumentation -{ - int num_workers; - BitmapHeapScanInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; -} SharedBitmapHeapInstrumentation; /* ---------------- * BitmapHeapScanState information @@ -1882,6 +1868,10 @@ typedef struct SharedBitmapHeapInstrumentation * recheck do current page's tuples need recheck * ---------------- */ + +/* this struct is defined in nodeBitmapHeapscan.c */ +typedef struct ParallelBitmapHeapState ParallelBitmapHeapState; + typedef struct BitmapHeapScanState { ScanState ss; /* its first field is NodeTag */ @@ -1921,6 +1911,7 @@ typedef struct TidScanState * trss_mintid the lowest TID in the scan range * trss_maxtid the highest TID in the scan range * trss_inScan is a scan currently in progress? + * trss_pscanlen size of parallel heap scan descriptor * ---------------- */ typedef struct TidRangeScanState @@ -1930,6 +1921,8 @@ typedef struct TidRangeScanState ItemPointerData trss_mintid; ItemPointerData trss_maxtid; bool trss_inScan; + Size trss_pscanlen; + struct SharedTidRangeScanInstrumentation *trss_sinstrument; } TidRangeScanState; /* ---------------- @@ -2236,8 +2229,11 @@ typedef struct MergeJoinState * hj_NullOuterTupleSlot prepared null tuple for right/right-anti/full * outer joins * hj_NullInnerTupleSlot prepared null tuple for left/full outer joins + * hj_NullOuterTupleStore tuplestore holding outer tuples that have + * null join keys (but must be emitted anyway) * hj_FirstOuterTupleSlot first tuple retrieved from outer plan * hj_JoinState current state of ExecHashJoin state machine + * hj_KeepNullTuples true to keep outer tuples with null join keys * hj_MatchedOuter true if found a join match for current outer * hj_OuterNotEmpty true if outer relation known not empty * ---------------- @@ -2261,8 +2257,10 @@ typedef struct HashJoinState TupleTableSlot *hj_HashTupleSlot; TupleTableSlot *hj_NullOuterTupleSlot; TupleTableSlot *hj_NullInnerTupleSlot; + Tuplestorestate *hj_NullOuterTupleStore; TupleTableSlot *hj_FirstOuterTupleSlot; int hj_JoinState; + bool hj_KeepNullTuples; bool hj_MatchedOuter; bool hj_OuterNotEmpty; } HashJoinState; @@ -2294,31 +2292,6 @@ struct MemoizeEntry; struct MemoizeTuple; struct MemoizeKey; -typedef struct MemoizeInstrumentation -{ - uint64 cache_hits; /* number of rescans where we've found the - * scan parameter values to be cached */ - uint64 cache_misses; /* number of rescans where we've not found the - * scan parameter values to be cached. */ - uint64 cache_evictions; /* number of cache entries removed due to - * the need to free memory */ - uint64 cache_overflows; /* number of times we've had to bypass the - * cache when filling it due to not being - * able to free enough space to store the - * current scan's tuples. */ - uint64 mem_peak; /* peak memory usage in bytes */ -} MemoizeInstrumentation; - -/* ---------------- - * Shared memory container for per-worker memoize information - * ---------------- - */ -typedef struct SharedMemoizeInfo -{ - int num_workers; - MemoizeInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; -} SharedMemoizeInfo; - /* ---------------- * MemoizeState information * @@ -2374,16 +2347,6 @@ typedef struct PresortedKeyData OffsetNumber attno; /* attribute number in tuple */ } PresortedKeyData; -/* ---------------- - * Shared memory container for per-worker sort information - * ---------------- - */ -typedef struct SharedSortInfo -{ - int num_workers; - TuplesortInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; -} SharedSortInfo; - /* ---------------- * SortState information * ---------------- @@ -2403,40 +2366,6 @@ typedef struct SortState SharedSortInfo *shared_info; /* one entry per worker */ } SortState; -/* ---------------- - * Instrumentation information for IncrementalSort - * ---------------- - */ -typedef struct IncrementalSortGroupInfo -{ - int64 groupCount; - int64 maxDiskSpaceUsed; - int64 totalDiskSpaceUsed; - int64 maxMemorySpaceUsed; - int64 totalMemorySpaceUsed; - bits32 sortMethods; /* bitmask of TuplesortMethod */ -} IncrementalSortGroupInfo; - -typedef struct IncrementalSortInfo -{ - IncrementalSortGroupInfo fullsortGroupInfo; - IncrementalSortGroupInfo prefixsortGroupInfo; -} IncrementalSortInfo; - -/* ---------------- - * Shared memory container for per-worker incremental sort information - * ---------------- - */ -typedef struct SharedIncrementalSortInfo -{ - int num_workers; - IncrementalSortInfo sinfo[FLEXIBLE_ARRAY_MEMBER]; -} SharedIncrementalSortInfo; - -/* ---------------- - * IncrementalSortState information - * ---------------- - */ typedef enum { INCSORT_LOADFULLSORT, @@ -2479,27 +2408,6 @@ typedef struct GroupState bool grp_done; /* indicates completion of Group scan */ } GroupState; -/* --------------------- - * per-worker aggregate information - * --------------------- - */ -typedef struct AggregateInstrumentation -{ - Size hash_mem_peak; /* peak hash table memory usage */ - uint64 hash_disk_used; /* kB of disk space used */ - int hash_batches_used; /* batches used during entire execution */ -} AggregateInstrumentation; - -/* ---------------- - * Shared memory container for per-worker aggregate information - * ---------------- - */ -typedef struct SharedAggInfo -{ - int num_workers; - AggregateInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER]; -} SharedAggInfo; - /* --------------------- * AggState information * @@ -2564,7 +2472,7 @@ typedef struct AggState bool table_filled; /* hash table filled yet? */ int num_hashes; MemoryContext hash_metacxt; /* memory for hash table bucket array */ - MemoryContext hash_tablecxt; /* memory for hash table entries */ + MemoryContext hash_tuplescxt; /* memory for hash table tuples */ struct LogicalTapeSet *hash_tapeset; /* tape set for hash spill tapes */ struct HashAggSpill *hash_spills; /* HashAggSpill for each grouping set, * exists only during first pass */ @@ -2776,29 +2684,6 @@ typedef struct GatherMergeState struct binaryheap *gm_heap; /* binary heap of slot indices */ } GatherMergeState; -/* ---------------- - * Values displayed by EXPLAIN ANALYZE - * ---------------- - */ -typedef struct HashInstrumentation -{ - int nbuckets; /* number of buckets at end of execution */ - int nbuckets_original; /* planned number of buckets */ - int nbatch; /* number of batches at end of execution */ - int nbatch_original; /* planned number of batches */ - Size space_peak; /* peak memory usage in bytes */ -} HashInstrumentation; - -/* ---------------- - * Shared memory container for per-worker hash information - * ---------------- - */ -typedef struct SharedHashInfo -{ - int num_workers; - HashInstrumentation hinstrument[FLEXIBLE_ARRAY_MEMBER]; -} SharedHashInfo; - /* ---------------- * HashState information * ---------------- @@ -2812,6 +2697,9 @@ typedef struct HashState FmgrInfo *skew_hashfunction; /* lookup data for skew hash function */ Oid skew_collation; /* collation to call skew_hashfunction with */ + Tuplestorestate *null_tuple_store; /* where to put null-keyed tuples */ + bool keep_null_tuples; /* do we need to save such tuples? */ + /* * In a parallelized hash join, the leader retains a pointer to the * shared-memory stats area in its shared_info field, and then copies the @@ -2863,7 +2751,7 @@ typedef struct SetOpState Oid *eqfuncoids; /* per-grouping-field equality fns */ FmgrInfo *hashfunctions; /* per-grouping-field hash fns */ TupleHashTable hashtable; /* hash table with one entry per group */ - MemoryContext tableContext; /* memory context containing hash table */ + MemoryContext tuplesContext; /* context containing hash table's tuples */ bool table_filled; /* hash table filled yet? */ TupleHashIterator hashiter; /* for iterating through hash table */ } SetOpState; diff --git a/src/include/nodes/extensible.h b/src/include/nodes/extensible.h index 1129c4ba4b1b4..517db95c4a360 100644 --- a/src/include/nodes/extensible.h +++ b/src/include/nodes/extensible.h @@ -4,7 +4,7 @@ * Definitions for extensible nodes and custom scans * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/extensible.h diff --git a/src/include/nodes/lockoptions.h b/src/include/nodes/lockoptions.h index 0b534e3060363..7961444eed1c3 100644 --- a/src/include/nodes/lockoptions.h +++ b/src/include/nodes/lockoptions.h @@ -4,7 +4,7 @@ * Common header for some locking-related declarations. * * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * src/include/nodes/lockoptions.h * @@ -20,7 +20,8 @@ */ typedef enum LockClauseStrength { - LCS_NONE, /* no such clause - only used in PlanRowMark */ + LCS_NONE, /* no such clause - only used in PlanRowMark + * and ON CONFLICT DO SELECT */ LCS_FORKEYSHARE, /* FOR KEY SHARE */ LCS_FORSHARE, /* FOR SHARE */ LCS_FORNOKEYUPDATE, /* FOR NO KEY UPDATE */ diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index 5473ce9a288a3..bf54d39feb05a 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -4,7 +4,7 @@ * prototypes for the creator functions of various nodes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/makefuncs.h @@ -117,7 +117,7 @@ extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr, extern Node *makeJsonKeyValue(Node *key, Node *value); extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type, bool unique_keys, - int location); + Oid exprBaseType, int location); extern JsonBehavior *makeJsonBehavior(JsonBehaviorType btype, Node *expr, int location); extern JsonTablePath *makeJsonTablePath(Const *pathvalue, char *pathname); diff --git a/src/include/nodes/memnodes.h b/src/include/nodes/memnodes.h index 5807ef805bd86..4b24778fe1e5d 100644 --- a/src/include/nodes/memnodes.h +++ b/src/include/nodes/memnodes.h @@ -4,7 +4,7 @@ * POSTGRES memory context node definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/memnodes.h @@ -120,7 +120,7 @@ typedef struct MemoryContextData NodeTag type; /* identifies exact kind of context */ /* these two fields are placed here to minimize alignment wastage: */ - bool isReset; /* T = no space alloced since last reset */ + bool isReset; /* T = no space allocated since last reset */ bool allowInCritSection; /* allow palloc in critical section */ Size mem_allocated; /* track memory allocated for this context */ const MemoryContextMethods *methods; /* virtual function table */ diff --git a/src/include/nodes/meson.build b/src/include/nodes/meson.build index d1ca24dd32f0b..96800215df1be 100644 --- a/src/include/nodes/meson.build +++ b/src/include/nodes/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group node_support_input_i = [ 'nodes/nodes.h', @@ -28,7 +28,7 @@ node_support_input_i = [ node_support_input = [] foreach i : node_support_input_i - node_support_input += meson.source_root() / 'src' / 'include' / i + node_support_input += meson.project_source_root() / 'src' / 'include' / i endforeach node_support_output = [ diff --git a/src/include/nodes/miscnodes.h b/src/include/nodes/miscnodes.h index bbd1a43c3fb03..ec833001ab0c5 100644 --- a/src/include/nodes/miscnodes.h +++ b/src/include/nodes/miscnodes.h @@ -10,7 +10,7 @@ * "context" pointers. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/miscnodes.h diff --git a/src/include/nodes/multibitmapset.h b/src/include/nodes/multibitmapset.h index a3b2c2a703134..f17b76f8393a8 100644 --- a/src/include/nodes/multibitmapset.h +++ b/src/include/nodes/multibitmapset.h @@ -18,7 +18,7 @@ * a small fraction of that has been built out; we'll add more as needed. * * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * src/include/nodes/multibitmapset.h * diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h index 5653fec8cbe5e..696ffdebd4be4 100644 --- a/src/include/nodes/nodeFuncs.h +++ b/src/include/nodes/nodeFuncs.h @@ -3,7 +3,7 @@ * nodeFuncs.h * Various general-purpose manipulations of Node trees * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/nodeFuncs.h @@ -15,7 +15,7 @@ #include "nodes/parsenodes.h" -struct PlanState; /* avoid including execnodes.h too */ +typedef struct PlanState PlanState; /* avoid including execnodes.h too */ /* flags bits for query_tree_walker and query_tree_mutator */ @@ -38,7 +38,7 @@ typedef bool (*check_function_callback) (Oid func_id, void *context); /* callback functions for tree walkers */ typedef bool (*tree_walker_callback) (Node *node, void *context); -typedef bool (*planstate_tree_walker_callback) (struct PlanState *planstate, +typedef bool (*planstate_tree_walker_callback) (PlanState *planstate, void *context); /* callback functions for tree mutators */ @@ -217,7 +217,7 @@ extern bool raw_expression_tree_walker_impl(Node *node, tree_walker_callback walker, void *context); -extern bool planstate_tree_walker_impl(struct PlanState *planstate, +extern bool planstate_tree_walker_impl(PlanState *planstate, planstate_tree_walker_callback walker, void *context); diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index fbe333d88fac9..a2925ae494606 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -4,7 +4,7 @@ * Definitions for tagged nodes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/nodes.h @@ -188,6 +188,8 @@ castNodeImpl(NodeTag type, void *ptr) * ---------------------------------------------------------------- */ +#ifndef FRONTEND + /* * nodes/{outfuncs.c,print.c} */ @@ -198,7 +200,7 @@ extern void outNode(struct StringInfoData *str, const void *obj); extern void outToken(struct StringInfoData *str, const char *s); extern void outBitmapset(struct StringInfoData *str, const struct Bitmapset *bms); -extern void outDatum(struct StringInfoData *str, uintptr_t value, +extern void outDatum(struct StringInfoData *str, Datum value, int typlen, bool typbyval); extern char *nodeToString(const void *obj); extern char *nodeToStringWithLocations(const void *obj); @@ -212,7 +214,7 @@ extern void *stringToNode(const char *str); extern void *stringToNodeWithLocations(const char *str); #endif extern struct Bitmapset *readBitmapset(void); -extern uintptr_t readDatum(bool typbyval); +extern Datum readDatum(bool typbyval); extern bool *readBoolCols(int numCols); extern int *readIntCols(int numCols); extern Oid *readOidCols(int numCols); @@ -224,8 +226,8 @@ extern int16 *readAttrNumberCols(int numCols); extern void *copyObjectImpl(const void *from); /* cast result back to argument type, if supported by compiler */ -#ifdef HAVE_TYPEOF -#define copyObject(obj) ((typeof(obj)) copyObjectImpl(obj)) +#ifdef HAVE_TYPEOF_UNQUAL +#define copyObject(obj) ((typeof_unqual(*(obj)) *) copyObjectImpl(obj)) #else #define copyObject(obj) copyObjectImpl(obj) #endif @@ -235,6 +237,8 @@ extern void *copyObjectImpl(const void *from); */ extern bool equal(const void *a, const void *b); +#endif /* !FRONTEND */ + /* * Typedef for parse location. This is just an int, but this way @@ -319,8 +323,8 @@ typedef enum JoinType * These codes are used internally in the planner, but are not supported * by the executor (nor, indeed, by most of the planner). */ - JOIN_UNIQUE_OUTER, /* LHS path must be made unique */ - JOIN_UNIQUE_INNER, /* RHS path must be made unique */ + JOIN_UNIQUE_OUTER, /* LHS has be made unique */ + JOIN_UNIQUE_INNER, /* RHS has be made unique */ /* * We might need additional join types someday. @@ -424,6 +428,7 @@ typedef enum OnConflictAction ONCONFLICT_NONE, /* No "ON CONFLICT" clause */ ONCONFLICT_NOTHING, /* ON CONFLICT ... DO NOTHING */ ONCONFLICT_UPDATE, /* ON CONFLICT ... DO UPDATE */ + ONCONFLICT_SELECT, /* ON CONFLICT ... DO SELECT */ } OnConflictAction; /* diff --git a/src/include/nodes/params.h b/src/include/nodes/params.h index 4321ca6329bf4..2a2f00c76de8f 100644 --- a/src/include/nodes/params.h +++ b/src/include/nodes/params.h @@ -4,7 +4,7 @@ * Support for finding the values associated with Param nodes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/params.h @@ -14,11 +14,10 @@ #ifndef PARAMS_H #define PARAMS_H -/* Forward declarations, to avoid including other headers */ -struct Bitmapset; -struct ExprState; -struct Param; -struct ParseState; +/* to avoid including other headers */ +typedef struct ExprState ExprState; +typedef struct Param Param; +typedef struct ParseState ParseState; /* @@ -101,11 +100,11 @@ typedef ParamExternData *(*ParamFetchHook) (ParamListInfo params, int paramid, bool speculative, ParamExternData *workspace); -typedef void (*ParamCompileHook) (ParamListInfo params, struct Param *param, - struct ExprState *state, +typedef void (*ParamCompileHook) (ParamListInfo params, Param *param, + ExprState *state, Datum *resv, bool *resnull); -typedef void (*ParserSetupHook) (struct ParseState *pstate, void *arg); +typedef void (*ParserSetupHook) (ParseState *pstate, void *arg); typedef struct ParamListInfoData { diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 4610fc61293b0..91377a6cde368 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -12,7 +12,7 @@ * identifying statement boundaries in multi-statement source strings. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/parsenodes.h @@ -127,8 +127,13 @@ typedef struct Query * query identifier (can be set by plugins); ignored for equal, as it * might not be set; also not stored. This is the result of the query * jumble, hence ignored. + * + * We store this as a signed value as this is the form it's displayed to + * users in places such as EXPLAIN and pg_stat_statements. Primarily this + * is done due to lack of an SQL type to represent the full range of + * uint64. */ - uint64 queryId pg_node_attr(equal_ignore, query_jumble_ignore, read_write_ignore, read_as(0)); + int64 queryId pg_node_attr(equal_ignore, query_jumble_ignore, read_write_ignore, read_as(0)); /* do I set the command result tag? */ bool canSetTag pg_node_attr(query_jumble_ignore); @@ -142,6 +147,9 @@ typedef struct Query */ int resultRelation pg_node_attr(query_jumble_ignore); + /* FOR PORTION OF clause for UPDATE/DELETE */ + ForPortionOfExpr *forPortionOf; + /* has aggregates in tlist or havingQual */ bool hasAggs pg_node_attr(query_jumble_ignore); /* has window functions in tlist */ @@ -195,7 +203,7 @@ typedef struct Query /* OVERRIDING clause */ OverridingKind override pg_node_attr(query_jumble_ignore); - OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */ + OnConflictExpr *onConflict; /* ON CONFLICT DO NOTHING/SELECT/UPDATE */ /* * The following three fields describe the contents of the RETURNING list @@ -209,7 +217,8 @@ typedef struct Query List *returningList; /* return-values list (of TargetEntry) */ List *groupClause; /* a list of SortGroupClause's */ - bool groupDistinct; /* is the group by clause distinct? */ + bool groupDistinct; /* was GROUP BY DISTINCT used? */ + bool groupByAll; /* was GROUP BY ALL used? */ List *groupingSets; /* a list of GroupingSet's if present */ @@ -346,6 +355,14 @@ typedef struct A_Expr List *name; /* possibly-qualified name of operator */ Node *lexpr; /* left argument, or NULL if none */ Node *rexpr; /* right argument, or NULL if none */ + + /* + * If rexpr is a list of some kind, we separately track its starting and + * ending location; it's not the same as the starting and ending location + * of the token itself. + */ + ParseLoc rexpr_list_start; + ParseLoc rexpr_list_end; ParseLoc location; /* token location, or -1 if unknown */ } A_Expr; @@ -439,6 +456,7 @@ typedef struct FuncCall List *agg_order; /* ORDER BY (list of SortBy) */ Node *agg_filter; /* FILTER clause, if any */ struct WindowDef *over; /* OVER clause, if any */ + int ignore_nulls; /* ignore nulls for window function */ bool agg_within_group; /* ORDER BY appeared in WITHIN GROUP */ bool agg_star; /* argument was really '*' */ bool agg_distinct; /* arguments were labeled DISTINCT */ @@ -501,6 +519,8 @@ typedef struct A_ArrayExpr { NodeTag type; List *elements; /* array element expressions */ + ParseLoc list_start; /* start of the element list */ + ParseLoc list_end; /* end of the elements list */ ParseLoc location; /* token location, or -1 if unknown */ } A_ArrayExpr; @@ -693,6 +713,19 @@ typedef struct RangeTableFuncCol ParseLoc location; /* token location, or -1 if unknown */ } RangeTableFuncCol; +/* + * RangeGraphTable - raw form of GRAPH_TABLE clause + */ +typedef struct RangeGraphTable +{ + NodeTag type; + RangeVar *graph_name; + struct GraphPattern *graph_pattern; + List *columns; + Alias *alias; /* table alias & optional column aliases */ + ParseLoc location; /* token location, or -1 if unknown */ +} RangeGraphTable; + /* * RangeTableSample - TABLESAMPLE appearing in a raw FROM clause * @@ -763,7 +796,7 @@ typedef struct TableLikeClause { NodeTag type; RangeVar *relation; - bits32 options; /* OR of TableLikeOption flags */ + uint32 options; /* OR of TableLikeOption flags */ Oid relationOid; /* If table has been looked up, its OID */ } TableLikeClause; @@ -799,6 +832,7 @@ typedef struct IndexElem List *opclassopts; /* opclass-specific options, or NIL */ SortByDir ordering; /* ASC/DESC/default */ SortByNulls nulls_ordering; /* FIRST/LAST/default */ + ParseLoc location; /* token location, or -1 if unknown */ } IndexElem; /* @@ -949,16 +983,78 @@ typedef struct PartitionRangeDatum } PartitionRangeDatum; /* - * PartitionCmd - info for ALTER TABLE/INDEX ATTACH/DETACH PARTITION commands + * PartitionDesc - info about a single partition for the ALTER TABLE SPLIT + * PARTITION command */ -typedef struct PartitionCmd +typedef struct SinglePartitionSpec { NodeTag type; - RangeVar *name; /* name of partition to attach/detach */ + + RangeVar *name; /* name of partition */ PartitionBoundSpec *bound; /* FOR VALUES, if attaching */ +} SinglePartitionSpec; + +/* + * PartitionCmd - info for ALTER TABLE/INDEX ATTACH/DETACH PARTITION and for + * ALTER TABLE SPLIT/MERGE PARTITION(S) commands + */ +typedef struct PartitionCmd +{ + NodeTag type; + + /* name of partition to attach/detach/merge/split */ + RangeVar *name; + + /* FOR VALUES, if attaching */ + PartitionBoundSpec *bound; + + /* + * list of partitions to be split/merged, used in ALTER TABLE MERGE + * PARTITIONS and ALTER TABLE SPLIT PARTITIONS. For merge partitions, + * partlist is a list of RangeVar; For split partition, it is a list of + * SinglePartitionSpec. + */ + List *partlist; + bool concurrent; } PartitionCmd; +/* + * Nodes for graph pattern + */ + +typedef struct GraphPattern +{ + NodeTag type; + List *path_pattern_list; + Node *whereClause; +} GraphPattern; + +typedef enum GraphElementPatternKind +{ + VERTEX_PATTERN, + EDGE_PATTERN_LEFT, + EDGE_PATTERN_RIGHT, + EDGE_PATTERN_ANY, + PAREN_EXPR, +} GraphElementPatternKind; + +#define IS_EDGE_PATTERN(kind) ((kind) == EDGE_PATTERN_ANY || \ + (kind) == EDGE_PATTERN_RIGHT || \ + (kind) == EDGE_PATTERN_LEFT) + +typedef struct GraphElementPattern +{ + NodeTag type; + GraphElementPatternKind kind; + const char *variable; + Node *labelexpr; + List *subexpr; + Node *whereClause; + List *quantifier; + ParseLoc location; +} GraphElementPattern; + /**************************************************************************** * Nodes for a Query tree ****************************************************************************/ @@ -1031,6 +1127,7 @@ typedef enum RTEKind RTE_VALUES, /* VALUES (), (), ... */ RTE_CTE, /* common table expr (WITH list element) */ RTE_NAMEDTUPLESTORE, /* tuplestore, e.g. for AFTER triggers */ + RTE_GRAPH_TABLE, /* GRAPH_TABLE clause */ RTE_RESULT, /* RTE represents an empty FROM clause; such * RTEs are added by the planner, they're not * present during parsing or rewriting */ @@ -1174,7 +1271,7 @@ typedef struct RangeTblEntry /* * join_using_alias is an alias clause attached directly to JOIN/USING. It - * is different from the alias field (below) in that it does not hide the + * is different from the alias field (above) in that it does not hide the * range variables of the tables being joined. */ Alias *join_using_alias pg_node_attr(query_jumble_ignore); @@ -1197,6 +1294,12 @@ typedef struct RangeTblEntry */ TableFunc *tablefunc; + /* + * Fields valid for a graph table RTE (else NULL): + */ + GraphPattern *graph_pattern; + List *graph_table_columns; + /* * Fields valid for a values RTE (else NIL): */ @@ -1250,7 +1353,7 @@ typedef struct RangeTblEntry * Fields valid for a GROUP RTE (else NIL): */ /* list of grouping expressions */ - List *groupexprs pg_node_attr(query_jumble_ignore); + List *groupexprs; /* * Fields valid in all RTEs: @@ -1373,7 +1476,8 @@ typedef enum WCOKind WCO_VIEW_CHECK, /* WCO on an auto-updatable view */ WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */ WCO_RLS_UPDATE_CHECK, /* RLS UPDATE WITH CHECK policy */ - WCO_RLS_CONFLICT_CHECK, /* RLS ON CONFLICT DO UPDATE USING policy */ + WCO_RLS_CONFLICT_CHECK, /* RLS ON CONFLICT DO SELECT/UPDATE USING + * policy */ WCO_RLS_MERGE_UPDATE_CHECK, /* RLS MERGE UPDATE USING policy */ WCO_RLS_MERGE_DELETE_CHECK, /* RLS MERGE DELETE USING policy */ } WCOKind; @@ -1596,6 +1700,22 @@ typedef struct RowMarkClause bool pushedDown; /* pushed down from higher query level? */ } RowMarkClause; +/* + * ForPortionOfClause + * representation of FOR PORTION OF FROM TO + * or FOR PORTION OF () + */ +typedef struct ForPortionOfClause +{ + NodeTag type; + char *range_name; /* column name of the range/multirange */ + ParseLoc location; /* token location, or -1 if unknown */ + ParseLoc target_location; /* token location, or -1 if unknown */ + Node *target; /* Expr from FOR PORTION OF col (...) syntax */ + Node *target_start; /* Expr from FROM ... TO ... syntax */ + Node *target_end; /* Expr from FROM ... TO ... syntax */ +} ForPortionOfClause; + /* * WithClause - * representation of WITH clause @@ -1635,9 +1755,10 @@ typedef struct InferClause typedef struct OnConflictClause { NodeTag type; - OnConflictAction action; /* DO NOTHING or UPDATE? */ + OnConflictAction action; /* DO NOTHING, SELECT, or UPDATE */ InferClause *infer; /* Optional index inference clause */ - List *targetList; /* the target list (of ResTarget) */ + LockClauseStrength lockStrength; /* lock strength for DO SELECT */ + List *targetList; /* target list (of ResTarget) for DO UPDATE */ Node *whereClause; /* qualifications */ ParseLoc location; /* token location, or -1 if unknown */ } OnConflictClause; @@ -2095,8 +2216,6 @@ typedef struct InsertStmt ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ OverridingKind override; /* OVERRIDING clause */ - ParseLoc stmt_location; /* start location, or -1 if unknown */ - ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } InsertStmt; /* ---------------------- @@ -2111,8 +2230,7 @@ typedef struct DeleteStmt Node *whereClause; /* qualifications */ ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ - ParseLoc stmt_location; /* start location, or -1 if unknown */ - ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ + ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */ } DeleteStmt; /* ---------------------- @@ -2128,8 +2246,7 @@ typedef struct UpdateStmt List *fromClause; /* optional from clause for more tables */ ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ - ParseLoc stmt_location; /* start location, or -1 if unknown */ - ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ + ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */ } UpdateStmt; /* ---------------------- @@ -2145,8 +2262,6 @@ typedef struct MergeStmt List *mergeWhenClauses; /* list of MergeWhenClause(es) */ ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ - ParseLoc stmt_location; /* start location, or -1 if unknown */ - ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ } MergeStmt; /* ---------------------- @@ -2185,6 +2300,7 @@ typedef struct SelectStmt Node *whereClause; /* WHERE qualification */ List *groupClause; /* GROUP BY clauses */ bool groupDistinct; /* Is this GROUP BY DISTINCT? */ + bool groupByAll; /* Is this GROUP BY ALL? */ Node *havingClause; /* HAVING conditional-expression */ List *windowClause; /* WINDOW window_name AS (...), ... */ @@ -2216,8 +2332,6 @@ typedef struct SelectStmt bool all; /* ALL specified? */ struct SelectStmt *larg; /* left child */ struct SelectStmt *rarg; /* right child */ - ParseLoc stmt_location; /* start location, or -1 if unknown */ - ParseLoc stmt_len; /* length in bytes; 0 means "rest of string" */ /* Eventually add fields for CORRESPONDING spec here */ } SelectStmt; @@ -2344,6 +2458,7 @@ typedef enum ObjectType OBJECT_PARAMETER_ACL, OBJECT_POLICY, OBJECT_PROCEDURE, + OBJECT_PROPGRAPH, OBJECT_PUBLICATION, OBJECT_PUBLICATION_NAMESPACE, OBJECT_PUBLICATION_REL, @@ -2468,6 +2583,8 @@ typedef enum AlterTableType AT_AttachPartition, /* ATTACH PARTITION */ AT_DetachPartition, /* DETACH PARTITION */ AT_DetachPartitionFinalize, /* DETACH PARTITION FINALIZE */ + AT_SplitPartition, /* SPLIT PARTITION */ + AT_MergePartitions, /* MERGE PARTITIONS */ AT_AddIdentity, /* ADD IDENTITY */ AT_SetIdentity, /* SET identity column options */ AT_DropIdentity, /* DROP IDENTITY */ @@ -2531,17 +2648,20 @@ typedef struct AlterCollationStmt * this command. * ---------------------- */ +typedef enum AlterDomainType +{ + AD_AlterDefault = 'T', /* SET|DROP DEFAULT */ + AD_DropNotNull = 'N', /* DROP NOT NULL */ + AD_SetNotNull = 'O', /* SET NOT NULL */ + AD_AddConstraint = 'C', /* ADD CONSTRAINT */ + AD_DropConstraint = 'X', /* DROP CONSTRAINT */ + AD_ValidateConstraint = 'V', /* VALIDATE CONSTRAINT */ +} AlterDomainType; + typedef struct AlterDomainStmt { NodeTag type; - char subtype; /*------------ - * T = alter column default - * N = alter column drop not null - * O = alter column set not null - * C = add constraint - * X = drop constraint - *------------ - */ + AlterDomainType subtype; /* subtype of command */ List *typeName; /* domain to work on */ char *name; /* column or constraint name to act on */ Node *def; /* definition of default or constraint */ @@ -2573,7 +2693,7 @@ typedef struct GrantStmt /* privileges == NIL denotes ALL PRIVILEGES */ List *grantees; /* list of RoleSpec nodes */ bool grant_option; /* grant or revoke grant option */ - RoleSpec *grantor; + RoleSpec *grantor; /* GRANTED BY clause, or NULL if none */ DropBehavior behavior; /* drop behavior (for REVOKE) */ } GrantStmt; @@ -2623,7 +2743,7 @@ typedef struct AccessPriv * Note: because of the parsing ambiguity with the GRANT * statement, granted_roles is a list of AccessPriv; the execution code * should complain if any column lists appear. grantee_roles is a list - * of role names, as String values. + * of role names, as RoleSpec values. * ---------------------- */ typedef struct GrantRoleStmt @@ -3417,15 +3537,44 @@ typedef enum FetchDirection FETCH_RELATIVE, } FetchDirection; +typedef enum FetchDirectionKeywords +{ + FETCH_KEYWORD_NONE = 0, + FETCH_KEYWORD_NEXT, + FETCH_KEYWORD_PRIOR, + FETCH_KEYWORD_FIRST, + FETCH_KEYWORD_LAST, + FETCH_KEYWORD_ABSOLUTE, + FETCH_KEYWORD_RELATIVE, + FETCH_KEYWORD_ALL, + FETCH_KEYWORD_FORWARD, + FETCH_KEYWORD_FORWARD_ALL, + FETCH_KEYWORD_BACKWARD, + FETCH_KEYWORD_BACKWARD_ALL, +} FetchDirectionKeywords; + #define FETCH_ALL LONG_MAX typedef struct FetchStmt { NodeTag type; FetchDirection direction; /* see above */ - long howMany; /* number of rows, or position argument */ - char *portalname; /* name of portal (cursor) */ - bool ismove; /* true if MOVE */ + /* number of rows, or position argument */ + long howMany pg_node_attr(query_jumble_ignore); + /* name of portal (cursor) */ + char *portalname; + /* true if MOVE */ + bool ismove; + + /* + * Set when a direction_keyword (e.g., FETCH FORWARD) is used, to + * distinguish it from a numeric variant (e.g., FETCH 1) for the purpose + * of query jumbling. + */ + FetchDirectionKeywords direction_keyword; + + /* token location, or -1 if unknown */ + ParseLoc location pg_node_attr(query_jumble_location); } FetchStmt; /* ---------------------- @@ -3911,18 +4060,6 @@ typedef struct AlterSystemStmt VariableSetStmt *setstmt; /* SET subcommand */ } AlterSystemStmt; -/* ---------------------- - * Cluster Statement (support pbrown's cluster index implementation) - * ---------------------- - */ -typedef struct ClusterStmt -{ - NodeTag type; - RangeVar *relation; /* relation being indexed, or NULL if all */ - char *indexname; /* original index defined */ - List *params; /* list of DefElem nodes */ -} ClusterStmt; - /* ---------------------- * Vacuum and Analyze Statements * @@ -3935,7 +4072,7 @@ typedef struct VacuumStmt NodeTag type; List *options; /* list of DefElem nodes */ List *rels; /* list of VacuumRelation, or NIL for all */ - bool is_vacuumcmd; /* true for VACUUM, false for ANALYZE */ + bool is_vacuumcmd; /* true for VACUUM, false otherwise */ } VacuumStmt; /* @@ -3953,6 +4090,27 @@ typedef struct VacuumRelation List *va_cols; /* list of column names, or NIL for all */ } VacuumRelation; +/* ---------------------- + * Repack Statement + * ---------------------- + */ +typedef enum RepackCommand +{ + REPACK_COMMAND_CLUSTER = 1, + REPACK_COMMAND_REPACK, + REPACK_COMMAND_VACUUMFULL, +} RepackCommand; + +typedef struct RepackStmt +{ + NodeTag type; + RepackCommand command; /* type of command being run */ + VacuumRelation *relation; /* relation being repacked */ + char *indexname; /* order tuples by this index */ + bool usingindex; /* whether USING INDEX is specified */ + List *params; /* list of DefElem nodes */ +} RepackStmt; + /* ---------------------- * Explain Statement * @@ -4010,6 +4168,7 @@ typedef struct RefreshMatViewStmt typedef struct CheckPointStmt { NodeTag type; + List *options; /* list of DefElem nodes */ } CheckPointStmt; /* ---------------------- @@ -4105,6 +4264,88 @@ typedef struct CreateCastStmt bool inout; } CreateCastStmt; +/* ---------------------- + * CREATE PROPERTY GRAPH Statement + * ---------------------- + */ +typedef struct CreatePropGraphStmt +{ + NodeTag type; + RangeVar *pgname; + List *vertex_tables; + List *edge_tables; +} CreatePropGraphStmt; + +typedef struct PropGraphVertex +{ + NodeTag type; + RangeVar *vtable; + List *vkey; + List *labels; + ParseLoc location; +} PropGraphVertex; + +typedef struct PropGraphEdge +{ + NodeTag type; + RangeVar *etable; + List *ekey; + List *esrckey; + char *esrcvertex; + List *esrcvertexcols; + List *edestkey; + char *edestvertex; + List *edestvertexcols; + List *labels; + ParseLoc location; +} PropGraphEdge; + +typedef struct PropGraphLabelAndProperties +{ + NodeTag type; + const char *label; + struct PropGraphProperties *properties; + ParseLoc location; +} PropGraphLabelAndProperties; + +typedef struct PropGraphProperties +{ + NodeTag type; + List *properties; + bool all; + ParseLoc location; +} PropGraphProperties; + +/* ---------------------- + * ALTER PROPERTY GRAPH Statement + * ---------------------- + */ + +typedef enum AlterPropGraphElementKind +{ + PROPGRAPH_ELEMENT_KIND_VERTEX = 1, + PROPGRAPH_ELEMENT_KIND_EDGE = 2, +} AlterPropGraphElementKind; + +typedef struct AlterPropGraphStmt +{ + NodeTag type; + RangeVar *pgname; + bool missing_ok; + List *add_vertex_tables; + List *add_edge_tables; + List *drop_vertex_tables; + List *drop_edge_tables; + DropBehavior drop_behavior; + AlterPropGraphElementKind element_kind; + const char *element_alias; + List *add_labels; + const char *drop_label; + const char *alter_label; + PropGraphProperties *add_properties; + List *drop_properties; +} AlterPropGraphStmt; + /* ---------------------- * CREATE TRANSFORM Statement * ---------------------- @@ -4227,9 +4468,10 @@ typedef struct AlterTSConfigurationStmt typedef struct PublicationTable { NodeTag type; - RangeVar *relation; /* relation to be published */ + RangeVar *relation; /* publication relation */ Node *whereClause; /* qualifications */ List *columns; /* List of columns in a publication table */ + bool except; /* True if listed in the EXCEPT clause */ } PublicationTable; /* @@ -4238,6 +4480,7 @@ typedef struct PublicationTable typedef enum PublicationObjSpecType { PUBLICATIONOBJ_TABLE, /* A table */ + PUBLICATIONOBJ_EXCEPT_TABLE, /* A table in the EXCEPT clause */ PUBLICATIONOBJ_TABLES_IN_SCHEMA, /* All tables in schema */ PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA, /* All tables in first element of * search_path */ @@ -4253,6 +4496,23 @@ typedef struct PublicationObjSpec ParseLoc location; /* token location, or -1 if unknown */ } PublicationObjSpec; +/* + * Types of objects supported by FOR ALL publications + */ +typedef enum PublicationAllObjType +{ + PUBLICATION_ALL_TABLES, + PUBLICATION_ALL_SEQUENCES, +} PublicationAllObjType; + +typedef struct PublicationAllObjSpec +{ + NodeTag type; + PublicationAllObjType pubobjtype; /* type of this publication object */ + List *except_tables; /* tables specified in the EXCEPT clause */ + ParseLoc location; /* token location, or -1 if unknown */ +} PublicationAllObjSpec; + typedef struct CreatePublicationStmt { NodeTag type; @@ -4260,6 +4520,8 @@ typedef struct CreatePublicationStmt List *options; /* List of DefElem nodes */ List *pubobjects; /* Optional list of publication objects */ bool for_all_tables; /* Special publication for all tables in db */ + bool for_all_sequences; /* Special publication for all sequences + * in db */ } CreatePublicationStmt; typedef enum AlterPublicationAction @@ -4282,15 +4544,17 @@ typedef struct AlterPublicationStmt * objects. */ List *pubobjects; /* Optional list of publication objects */ - bool for_all_tables; /* Special publication for all tables in db */ AlterPublicationAction action; /* What action to perform with the given * objects */ + bool for_all_tables; /* True if ALL TABLES is specified */ + bool for_all_sequences; /* True if ALL SEQUENCES is specified */ } AlterPublicationStmt; typedef struct CreateSubscriptionStmt { NodeTag type; char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ @@ -4299,11 +4563,13 @@ typedef struct CreateSubscriptionStmt typedef enum AlterSubscriptionType { ALTER_SUBSCRIPTION_OPTIONS, + ALTER_SUBSCRIPTION_SERVER, ALTER_SUBSCRIPTION_CONNECTION, ALTER_SUBSCRIPTION_SET_PUBLICATION, ALTER_SUBSCRIPTION_ADD_PUBLICATION, ALTER_SUBSCRIPTION_DROP_PUBLICATION, - ALTER_SUBSCRIPTION_REFRESH, + ALTER_SUBSCRIPTION_REFRESH_PUBLICATION, + ALTER_SUBSCRIPTION_REFRESH_SEQUENCES, ALTER_SUBSCRIPTION_ENABLED, ALTER_SUBSCRIPTION_SKIP, } AlterSubscriptionType; @@ -4313,6 +4579,7 @@ typedef struct AlterSubscriptionStmt NodeTag type; AlterSubscriptionType kind; /* ALTER_SUBSCRIPTION_OPTIONS, etc */ char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ @@ -4326,4 +4593,12 @@ typedef struct DropSubscriptionStmt DropBehavior behavior; /* RESTRICT or CASCADE behavior */ } DropSubscriptionStmt; +typedef struct WaitStmt +{ + NodeTag type; + char *lsn_literal; /* LSN string from grammar */ + List *options; /* List of DefElem nodes */ +} WaitStmt; + + #endif /* PARSENODES_H */ diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 6567759595daa..693b879f76d1a 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -6,7 +6,7 @@ * We don't support copying RelOptInfo, IndexOptInfo, or Path nodes. * There are some subsidiary structs that are useful to copy, though. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/pathnodes.h @@ -22,6 +22,79 @@ #include "nodes/parsenodes.h" #include "storage/block.h" +/* + * Path generation strategies. + * + * These constants are used to specify the set of strategies that the planner + * should use, either for the query as a whole or for a specific baserel or + * joinrel. The various planner-related enable_* GUCs are used to set the + * PlannerGlobal's default_pgs_mask, and that in turn is used to set each + * RelOptInfo's pgs_mask. In both cases, extensions can use hooks to modify the + * default value. Not every strategy listed here has a corresponding enable_* + * GUC; those that don't are always allowed unless disabled by an extension. + * Not all strategies are relevant for every RelOptInfo; e.g. PGS_SEQSCAN + * doesn't affect joinrels one way or the other. + * + * In most cases, disabling a path generation strategy merely means that any + * paths generated using that strategy are marked as disabled, but in some + * cases, path generation is skipped altogether. The latter strategy is only + * permissible when it can't result in planner failure -- for instance, we + * couldn't do this for sequential scans on a plain rel, because there might + * not be any other possible path. Nevertheless, the behaviors in each + * individual case are to some extent the result of historical accident, + * chosen to match the preexisting behaviors of the enable_* GUCs. + * + * In a few cases, we have more than one bit for the same strategy, controlling + * different aspects of the planner behavior. When PGS_CONSIDER_INDEXONLY is + * unset, we don't even consider index-only scans, and any such scans that + * would have been generated become index scans instead. On the other hand, + * unsetting PGS_INDEXSCAN or PGS_INDEXONLYSCAN causes generated paths of the + * corresponding types to be marked as disabled. Similarly, unsetting + * PGS_CONSIDER_PARTITIONWISE prevents any sort of thinking about partitionwise + * joins for the current rel, which incidentally will preclude higher-level + * joinrels from building partitionwise paths using paths taken from the + * current rel's children. On the other hand, unsetting PGS_APPEND or + * PGS_MERGE_APPEND will only arrange to disable paths of the corresponding + * types if they are generated at the level of the current rel. + * + * Finally, unsetting PGS_CONSIDER_NONPARTIAL disables all non-partial paths + * except those that use Gather or Gather Merge. In most other cases, a + * plugin can nudge the planner toward a particular strategy by disabling + * all of the others, but that doesn't work here: unsetting PGS_SEQSCAN, + * for instance, would disable both partial and non-partial sequential scans. + */ +#define PGS_SEQSCAN 0x00000001 +#define PGS_INDEXSCAN 0x00000002 +#define PGS_INDEXONLYSCAN 0x00000004 +#define PGS_BITMAPSCAN 0x00000008 +#define PGS_TIDSCAN 0x00000010 +#define PGS_FOREIGNJOIN 0x00000020 +#define PGS_MERGEJOIN_PLAIN 0x00000040 +#define PGS_MERGEJOIN_MATERIALIZE 0x00000080 +#define PGS_NESTLOOP_PLAIN 0x00000100 +#define PGS_NESTLOOP_MATERIALIZE 0x00000200 +#define PGS_NESTLOOP_MEMOIZE 0x00000400 +#define PGS_HASHJOIN 0x00000800 +#define PGS_APPEND 0x00001000 +#define PGS_MERGE_APPEND 0x00002000 +#define PGS_GATHER 0x00004000 +#define PGS_GATHER_MERGE 0x00008000 +#define PGS_CONSIDER_INDEXONLY 0x00010000 +#define PGS_CONSIDER_PARTITIONWISE 0x00020000 +#define PGS_CONSIDER_NONPARTIAL 0x00040000 + +/* + * Convenience macros for useful combination of the bits defined above. + */ +#define PGS_SCAN_ANY \ + (PGS_SEQSCAN | PGS_INDEXSCAN | PGS_INDEXONLYSCAN | PGS_BITMAPSCAN | \ + PGS_TIDSCAN) +#define PGS_MERGEJOIN_ANY \ + (PGS_MERGEJOIN_PLAIN | PGS_MERGEJOIN_MATERIALIZE) +#define PGS_NESTLOOP_ANY \ + (PGS_NESTLOOP_PLAIN | PGS_NESTLOOP_MATERIALIZE | PGS_NESTLOOP_MEMOIZE) +#define PGS_JOIN_ANY \ + (PGS_FOREIGNJOIN | PGS_MERGEJOIN_ANY | PGS_NESTLOOP_ANY | PGS_HASHJOIN) /* * Relids @@ -110,6 +183,9 @@ typedef struct PlannerGlobal /* PlannerInfos for SubPlan nodes */ List *subroots pg_node_attr(read_write_ignore); + /* names already used for subplans (list of C strings) */ + List *subplanNames pg_node_attr(read_write_ignore); + /* indices of subplans that require REWIND */ Bitmapset *rewindPlanIDs; @@ -132,6 +208,9 @@ typedef struct PlannerGlobal /* "flat" list of RTEPermissionInfos */ List *finalrteperminfos; + /* list of SubPlanRTInfo nodes */ + List *subrtinfos; + /* "flat" list of PlanRowMarks */ List *finalrowmarks; @@ -153,6 +232,9 @@ typedef struct PlannerGlobal /* type OIDs for PARAM_EXEC Params */ List *paramExecTypes; + /* info about nodes elided from the plan during setrefs processing */ + List *elidedNodes; + /* highest PlaceHolderVar ID assigned */ Index lastPHId; @@ -177,8 +259,18 @@ typedef struct PlannerGlobal /* worst PROPARALLEL hazard level */ char maxParallelHazard; + /* mask of allowed path generation strategies */ + uint64 default_pgs_mask; + /* partition descriptors */ PartitionDirectory partition_directory pg_node_attr(read_write_ignore); + + /* hash table for NOT NULL attnums of relations */ + struct HTAB *rel_notnullatts_hash pg_node_attr(read_write_ignore); + + /* extension state */ + void **extension_state pg_node_attr(read_write_ignore); + int extension_state_allocated; } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ @@ -195,9 +287,6 @@ typedef struct PlannerGlobal * original Query. Note that at present the planner extensively modifies * the passed-in Query data structure; someday that should stop. * - * For reasons explained in optimizer/optimizer.h, we define the typedef - * either here or in that header, whichever is read first. - * * Not all fields are printed. (In some cases, there is no print support for * the field type; in others, doing so would lead to infinite recursion or * bloat dump output more than seems useful.) @@ -208,10 +297,7 @@ typedef struct PlannerGlobal * correctly replaced with the keeping one. *---------- */ -#ifndef HAVE_PLANNERINFO_TYPEDEF typedef struct PlannerInfo PlannerInfo; -#define HAVE_PLANNERINFO_TYPEDEF 1 -#endif struct PlannerInfo { @@ -231,6 +317,21 @@ struct PlannerInfo /* NULL at outermost Query */ PlannerInfo *parent_root pg_node_attr(read_write_ignore); + /* Subplan name for EXPLAIN and debugging purposes (NULL at top level) */ + char *plan_name; + + /* + * If this PlannerInfo exists to consider an alternative implementation + * strategy for a portion of the query that could also be implemented by + * some other PlannerInfo, this is the plan_name for that other + * PlannerInfo. When we are considering the first or only alternative, it + * is the same as plan_name. + * + * Currently, we set this to a value other than plan_name only when + * considering a MinMaxAggPath or a hashed SubPlan. + */ + char *alternative_plan_name; + /* * plan_params contains the expressions that this query level needs to * make available to a lower query level that is currently being planned. @@ -394,6 +495,15 @@ struct PlannerInfo /* list of PlaceHolderInfos */ List *placeholder_list; + /* list of AggClauseInfos */ + List *agg_clause_list; + + /* list of GroupingExprInfos */ + List *group_expr_list; + + /* list of plain Vars contained in targetlist and havingQual */ + List *tlist_vars; + /* array of PlaceHolderInfos indexed by phid */ struct PlaceHolderInfo **placeholder_array pg_node_attr(read_write_ignore, array_size(placeholder_array_size)); /* allocated size of array */ @@ -529,6 +639,8 @@ struct PlannerInfo bool placeholdersFrozen; /* true if planning a recursive WITH item */ bool hasRecursion; + /* true if a planner extension may replan this subquery */ + bool assumeReplanning; /* * The rangetable index for the RTE_GROUP RTE, or 0 if there is no @@ -575,14 +687,12 @@ struct PlannerInfo bool *isAltSubplan pg_node_attr(read_write_ignore); bool *isUsedSubplan pg_node_attr(read_write_ignore); - /* optional private data for join_search_hook, e.g., GEQO */ - void *join_search_private pg_node_attr(read_write_ignore); - - /* Does this query modify any partition key columns? */ - bool partColsUpdated; - /* PartitionPruneInfos added in this query's plan. */ List *partPruneInfos; + + /* extension state */ + void **extension_state pg_node_attr(read_write_ignore); + int extension_state_allocated; }; @@ -623,7 +733,7 @@ typedef struct PartitionSchemeData /* Cached information about partition comparison functions. */ struct FmgrInfo *partsupfunc; -} PartitionSchemeData; +} PartitionSchemeData; typedef struct PartitionSchemeData *PartitionScheme; @@ -700,8 +810,6 @@ typedef struct PartitionSchemeData *PartitionScheme; * (regardless of ordering) among the unparameterized paths; * or if there is no unparameterized path, the path with lowest * total cost among the paths with minimum parameterization - * cheapest_unique_path - for caching cheapest path to produce unique - * (no duplicates) output from relation; NULL if not yet requested * cheapest_parameterized_paths - best paths for their parameterizations; * always includes cheapest_total_path, even if that's unparameterized * direct_lateral_relids - rels this rel has direct LATERAL references to @@ -719,6 +827,9 @@ typedef struct PartitionSchemeData *PartitionScheme; * the attribute is needed as part of final targetlist * attr_widths - cache space for per-attribute width estimates; * zero means not computed yet + * notnullattnums - zero-based set containing attnums of NOT NULL + * columns (not populated for rels corresponding to + * non-partitioned inh==true RTEs) * nulling_relids - relids of outer joins that can null this rel * lateral_vars - lateral cross-references of rel, if any (list of * Vars and PlaceHolderVars) @@ -764,6 +875,21 @@ typedef struct PartitionSchemeData *PartitionScheme; * other rels for which we have tried and failed to prove * this one unique * + * Three fields are used to cache information about unique-ification of this + * relation. This is used to support semijoins where the relation appears on + * the RHS: the relation is first unique-ified, and then a regular join is + * performed: + * + * unique_rel - the unique-ified version of the relation, containing paths + * that produce unique (no duplicates) output from relation; + * NULL if not yet requested + * unique_pathkeys - pathkeys that represent the ordering requirements for + * the relation's output in sort-based unique-ification + * implementations + * unique_groupclause - a list of SortGroupClause nodes that represent the + * columns to be grouped on in hash-based unique-ification + * implementations + * * The presence of the following fields depends on the restrictions * and joins that the relation participates in: * @@ -901,7 +1027,7 @@ typedef struct RelOptInfo Cardinality rows; /* - * per-relation planner control flags + * per-relation planner control */ /* keep cheap-startup-cost paths? */ bool consider_startup; @@ -909,6 +1035,8 @@ typedef struct RelOptInfo bool consider_param_startup; /* consider parallel paths? */ bool consider_parallel; + /* path generation strategy mask */ + uint64 pgs_mask; /* * default result targetlist for Paths scanning this relation; list of @@ -924,7 +1052,6 @@ typedef struct RelOptInfo List *partial_pathlist; /* partial Paths */ struct Path *cheapest_startup_path; struct Path *cheapest_total_path; - struct Path *cheapest_unique_path; List *cheapest_parameterized_paths; /* @@ -952,11 +1079,7 @@ typedef struct RelOptInfo Relids *attr_needed pg_node_attr(read_write_ignore); /* array indexed [min_attr .. max_attr] */ int32 *attr_widths pg_node_attr(read_write_ignore); - - /* - * Zero-based set containing attnums of NOT NULL columns. Not populated - * for rels corresponding to non-partitioned inh==true RTEs. - */ + /* zero-based set containing attnums of NOT NULL columns */ Bitmapset *notnullattnums; /* relids of outer joins that can null this baserel */ Relids nulling_relids; @@ -1002,6 +1125,16 @@ typedef struct RelOptInfo /* known not unique for these set(s) */ List *non_unique_for_rels; + /* + * information about unique-ification of this relation + */ + /* the unique-ified version of the relation */ + struct RelOptInfo *unique_rel; + /* pathkeys for sort-based unique-ification implementations */ + List *unique_pathkeys; + /* SortGroupClause nodes for hash-based unique-ification implementations */ + List *unique_groupclause; + /* * used by various scans and joins: */ @@ -1022,6 +1155,14 @@ typedef struct RelOptInfo /* consider partitionwise join paths? (if partitioned rel) */ bool consider_partitionwise_join; + /* + * used by eager aggregation: + */ + /* information needed to create grouped paths */ + struct RelAggInfo *agg_info; + /* the partially-aggregated version of the relation */ + struct RelOptInfo *grouped_rel; + /* * inheritance links, if this is an otherrel (otherwise NULL): */ @@ -1073,6 +1214,10 @@ typedef struct RelOptInfo List **partexprs pg_node_attr(read_write_ignore); /* Nullable partition key expressions */ List **nullable_partexprs pg_node_attr(read_write_ignore); + + /* extension state */ + void **extension_state pg_node_attr(read_write_ignore); + int extension_state_allocated; } RelOptInfo; /* @@ -1095,13 +1240,82 @@ typedef struct RelOptInfo ((rel)->part_scheme && (rel)->boundinfo && (rel)->nparts > 0 && \ (rel)->part_rels && (rel)->partexprs && (rel)->nullable_partexprs) +/* + * Is given relation unique-ified? + * + * When the nominal jointype is JOIN_INNER, sjinfo->jointype is JOIN_SEMI, and + * the given rel is exactly the RHS of the semijoin, it indicates that the rel + * has been unique-ified. + */ +#define RELATION_WAS_MADE_UNIQUE(rel, sjinfo, nominal_jointype) \ + ((nominal_jointype) == JOIN_INNER && (sjinfo)->jointype == JOIN_SEMI && \ + bms_equal((sjinfo)->syn_righthand, (rel)->relids)) + +/* + * Is the given relation a grouped relation? + */ +#define IS_GROUPED_REL(rel) \ + ((rel)->agg_info != NULL) + +/* + * RelAggInfo + * Information needed to create paths for a grouped relation. + * + * "target" is the default result targetlist for Paths scanning this grouped + * relation; list of Vars/Exprs, cost, width. + * + * "agg_input" is the output tlist for the paths that provide input to the + * grouped paths. One difference from the reltarget of the non-grouped + * relation is that agg_input has its sortgrouprefs[] initialized. + * + * "group_clauses" and "group_exprs" are lists of SortGroupClauses and the + * corresponding grouping expressions. + * + * "apply_agg_at" tracks the set of relids at which partial aggregation is + * applied in the paths of this grouped relation. + * + * "grouped_rows" is the estimated number of result tuples of the grouped + * relation. + * + * "agg_useful" is a flag to indicate whether the grouped paths are considered + * useful. It is set true if the average partial group size is no less than + * min_eager_agg_group_size, suggesting a significant row count reduction. + */ +typedef struct RelAggInfo +{ + pg_node_attr(no_copy_equal, no_read, no_query_jumble) + + NodeTag type; + + /* the output tlist for the grouped paths */ + struct PathTarget *target; + + /* the output tlist for the input paths */ + struct PathTarget *agg_input; + + /* a list of SortGroupClauses */ + List *group_clauses; + /* a list of grouping expressions */ + List *group_exprs; + + /* the set of relids partial aggregation is applied at */ + Relids apply_agg_at; + + /* estimated number of result tuples */ + Cardinality grouped_rows; + + /* the grouped paths are considered useful? */ + bool agg_useful; +} RelAggInfo; + /* * IndexOptInfo * Per-index information for planning/optimization * - * indexkeys[], indexcollations[] each have ncolumns entries. - * opfamily[], and opcintype[] each have nkeycolumns entries. They do - * not contain any information about included attributes. + * indexkeys[] and canreturn[] each have ncolumns entries. + * + * indexcollations[], opfamily[], and opcintype[] each have nkeycolumns + * entries. These don't contain any information about INCLUDE columns. * * sortopfamily[], reverse_sort[], and nulls_first[] have * nkeycolumns entries, if the index is ordered; but if it is unordered, @@ -1126,14 +1340,10 @@ typedef struct RelOptInfo * (by plancat.c), indrestrictinfo and predOK are set later, in * check_index_predicates(). */ -#ifndef HAVE_INDEXOPTINFO_TYPEDEF -typedef struct IndexOptInfo IndexOptInfo; -#define HAVE_INDEXOPTINFO_TYPEDEF 1 -#endif struct IndexPath; /* forward declaration */ -struct IndexOptInfo +typedef struct IndexOptInfo { pg_node_attr(no_copy_equal, no_read, no_query_jumble) @@ -1214,6 +1424,8 @@ struct IndexOptInfo bool nullsnotdistinct; /* is uniqueness enforced immediately? */ bool immediate; + /* true if paths using this index should be marked disabled */ + bool disabled; /* true if index doesn't really exist */ bool hypothetical; @@ -1235,7 +1447,7 @@ struct IndexOptInfo /* AM's cost estimator */ /* Rather than include amapi.h here, we declare amcostestimate like this */ void (*amcostestimate) (struct PlannerInfo *, struct IndexPath *, double, Cost *, Cost *, Selectivity *, double *, double *) pg_node_attr(read_write_ignore); -}; +} IndexOptInfo; /* * ForeignKeyOptInfo @@ -1739,8 +1951,8 @@ typedef struct ParamPathInfo * and the specified outer rel(s). * * "rows" is the same as parent->rows in simple paths, but in parameterized - * paths and UniquePaths it can be less than parent->rows, reflecting the - * fact that we've filtered by extra join conditions or removed duplicates. + * paths it can be less than parent->rows, reflecting the fact that we've + * filtered by extra join conditions. * * "pathkeys" is a List of PathKey nodes (see above), describing the sort * ordering of the path's output rows. @@ -2052,6 +2264,12 @@ typedef struct CustomPath * For partial Append, 'subpaths' contains non-partial subpaths followed by * partial subpaths. * + * Whenever accumulate_append_subpath() allows us to consolidate multiple + * levels of Append paths down to one, we store the RTI sets for the omitted + * paths in child_append_relid_sets. This is not necessary for planning or + * execution; we do it for the benefit of code that wants to inspect the + * final plan and understand how it came to be. + * * Note: it is possible for "subpaths" to contain only one, or even no, * elements. These cases are optimized during create_append_plan. * In particular, an AppendPath with no subpaths is a "dummy" path that @@ -2067,6 +2285,7 @@ typedef struct AppendPath /* Index of first partial path in subpaths; list_length(subpaths) if none */ int first_partial_path; Cardinality limit_tuples; /* hard limit on output tuples, or -1 */ + List *child_append_relid_sets; } AppendPath; #define IS_DUMMY_APPEND(p) \ @@ -2083,12 +2302,15 @@ extern bool is_dummy_rel(RelOptInfo *rel); /* * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted * results from several member plans to produce similarly-sorted output. + * + * child_append_relid_sets has the same meaning here as for AppendPath. */ typedef struct MergeAppendPath { Path path; List *subpaths; /* list of component Paths */ Cardinality limit_tuples; /* hard limit on output tuples, or -1 */ + List *child_append_relid_sets; } MergeAppendPath; /* @@ -2131,40 +2353,14 @@ typedef struct MemoizePath * complete after caching the first record. */ bool binary_mode; /* true when cache key should be compared bit * by bit, false when using hash equality ops */ - Cardinality calls; /* expected number of rescans */ uint32 est_entries; /* The maximum number of entries that the * planner expects will fit in the cache, or 0 * if unknown */ + Cardinality est_calls; /* expected number of rescans */ + Cardinality est_unique_keys; /* estimated unique keys, for EXPLAIN */ + double est_hit_ratio; /* estimated cache hit ratio, for EXPLAIN */ } MemoizePath; -/* - * UniquePath represents elimination of distinct rows from the output of - * its subpath. - * - * This can represent significantly different plans: either hash-based or - * sort-based implementation, or a no-op if the input path can be proven - * distinct already. The decision is sufficiently localized that it's not - * worth having separate Path node types. (Note: in the no-op case, we could - * eliminate the UniquePath node entirely and just return the subpath; but - * it's convenient to have a UniquePath in the path tree to signal upper-level - * routines that the input is known distinct.) - */ -typedef enum UniquePathMethod -{ - UNIQUE_PATH_NOOP, /* input is known unique already */ - UNIQUE_PATH_HASH, /* use hashing */ - UNIQUE_PATH_SORT, /* use sorting */ -} UniquePathMethod; - -typedef struct UniquePath -{ - Path path; - Path *subpath; - UniquePathMethod umethod; - List *in_operators; /* equality operators of the IN clause */ - List *uniq_exprs; /* expressions to be made unique */ -} UniquePath; - /* * GatherPath runs several copies of a plan in parallel and collects the * results. The parallel leader may also execute the plan, unless the @@ -2371,17 +2567,17 @@ typedef struct GroupPath } GroupPath; /* - * UpperUniquePath represents adjacent-duplicate removal (in presorted input) + * UniquePath represents adjacent-duplicate removal (in presorted input) * * The columns to be compared are the first numkeys columns of the path's * pathkeys. The input is presumed already sorted that way. */ -typedef struct UpperUniquePath +typedef struct UniquePath { Path path; Path *subpath; /* path representing input source */ int numkeys; /* number of pathkey columns to compare */ -} UpperUniquePath; +} UniquePath; /* * AggPath represents generic computation of aggregate functions @@ -2519,13 +2715,13 @@ typedef struct ModifyTablePath bool canSetTag; /* do we set the command tag/es_processed? */ Index nominalRelation; /* Parent RT index for use of EXPLAIN */ Index rootRelation; /* Root RT index, if partitioned/inherited */ - bool partColsUpdated; /* some part key in hierarchy updated? */ List *resultRelations; /* integer list of RT indexes */ List *updateColnosLists; /* per-target-table update_colnos lists */ List *withCheckOptionLists; /* per-target-table WCO lists */ List *returningLists; /* per-target-table RETURNING tlists */ List *rowMarks; /* PlanRowMarks (non-locking only) */ OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */ + ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */ int epqParam; /* ID of Param for EvalPlanQual re-eval */ List *mergeActionLists; /* per-target-table lists of actions for * MERGE */ @@ -3022,12 +3218,7 @@ typedef struct PlaceHolderVar * We also create transient SpecialJoinInfos for child joins during * partitionwise join planning, which are also not present in join_info_list. */ -#ifndef HAVE_SPECIALJOININFO_TYPEDEF -typedef struct SpecialJoinInfo SpecialJoinInfo; -#define HAVE_SPECIALJOININFO_TYPEDEF 1 -#endif - -struct SpecialJoinInfo +typedef struct SpecialJoinInfo { pg_node_attr(no_read, no_query_jumble) @@ -3048,7 +3239,7 @@ struct SpecialJoinInfo bool semi_can_hash; /* true if semi_operators are all hash */ List *semi_operators; /* OIDs of equality join operators */ List *semi_rhs_exprs; /* righthand-side expressions of these ops */ -}; +} SpecialJoinInfo; /* * Transient outer-join clause info. @@ -3274,6 +3465,49 @@ typedef struct MinMaxAggInfo Param *param; } MinMaxAggInfo; +/* + * For each distinct Aggref node that appears in the targetlist and HAVING + * clauses, we store an AggClauseInfo node in the PlannerInfo node's + * agg_clause_list. Each AggClauseInfo records the set of relations referenced + * by the aggregate expression. This information is used to determine how far + * the aggregate can be safely pushed down in the join tree. + */ +typedef struct AggClauseInfo +{ + pg_node_attr(no_read, no_query_jumble) + + NodeTag type; + + /* the Aggref expr */ + Aggref *aggref; + + /* lowest level we can evaluate this aggregate at */ + Relids agg_eval_at; +} AggClauseInfo; + +/* + * For each grouping expression that appears in grouping clauses, we store a + * GroupingExprInfo node in the PlannerInfo node's group_expr_list. Each + * GroupingExprInfo records the expression being grouped on, its sortgroupref, + * and the EquivalenceClass it belongs to. This information is necessary to + * reproduce correct grouping semantics at different levels of the join tree. + */ +typedef struct GroupingExprInfo +{ + pg_node_attr(no_read, no_query_jumble) + + NodeTag type; + + /* the represented expression */ + Expr *expr; + + /* the tleSortGroupRef of the corresponding SortGroupClause */ + Index sortgroupref; + + /* the equivalence class the expression belongs to */ + EquivalenceClass *ec pg_node_attr(copy_as_scalar, equal_as_scalar); +} GroupingExprInfo; + /* * At runtime, PARAM_EXEC slots are used to pass values around from one plan * node to another. They can be used to pass values down into subqueries (for @@ -3365,6 +3599,7 @@ typedef struct SemiAntiJoinFactors * sjinfo is extra info about special joins for selectivity estimation * semifactors is as shown above (only valid for SEMI/ANTI/inner_unique joins) * param_source_rels are OK targets for parameterization of result paths + * pgs_mask is a bitmask of PGS_* constants to limit the join strategy */ typedef struct JoinPathExtraData { @@ -3374,6 +3609,7 @@ typedef struct JoinPathExtraData SpecialJoinInfo *sjinfo; SemiAntiJoinFactors semifactors; Relids param_source_rels; + uint64 pgs_mask; } JoinPathExtraData; /* diff --git a/src/include/nodes/pg_list.h b/src/include/nodes/pg_list.h index 4d1cdbbcfdd5e..e93bcbf2698d6 100644 --- a/src/include/nodes/pg_list.h +++ b/src/include/nodes/pg_list.h @@ -29,7 +29,7 @@ * always be so; be careful to use the appropriate list type for your data.) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/pg_list.h @@ -204,10 +204,42 @@ list_length(const List *l) /* * Convenience macros for building fixed-length lists */ -#define list_make_ptr_cell(v) ((ListCell) {.ptr_value = (v)}) -#define list_make_int_cell(v) ((ListCell) {.int_value = (v)}) -#define list_make_oid_cell(v) ((ListCell) {.oid_value = (v)}) -#define list_make_xid_cell(v) ((ListCell) {.xid_value = (v)}) + +static inline ListCell +list_make_ptr_cell(void *v) +{ + ListCell c; + + c.ptr_value = v; + return c; +} + +static inline ListCell +list_make_int_cell(int v) +{ + ListCell c; + + c.int_value = v; + return c; +} + +static inline ListCell +list_make_oid_cell(Oid v) +{ + ListCell c; + + c.oid_value = v; + return c; +} + +static inline ListCell +list_make_xid_cell(TransactionId v) +{ + ListCell c; + + c.xid_value = v; + return c; +} #define list_make1(x1) \ list_make1_impl(T_List, list_make_ptr_cell(x1)) diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index f0d514e6e1526..14a1dfed2b997 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -4,7 +4,7 @@ * definitions for query plan nodes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/plannodes.h @@ -16,8 +16,6 @@ #include "access/sdir.h" #include "access/stratnum.h" -#include "common/relpath.h" -#include "lib/stringinfo.h" #include "nodes/bitmapset.h" #include "nodes/lockoptions.h" #include "nodes/primnodes.h" @@ -28,6 +26,21 @@ * ---------------------------------------------------------------- */ +/* ---------------- + * PlannedStmtOrigin + * + * PlannedStmtOrigin identifies from where a PlannedStmt comes from. + * ---------------- + */ +typedef enum PlannedStmtOrigin +{ + PLAN_STMT_UNKNOWN = 0, /* plan origin is not yet known */ + PLAN_STMT_INTERNAL, /* generated internally by a query */ + PLAN_STMT_STANDARD, /* standard planned statement */ + PLAN_STMT_CACHE_GENERIC, /* Generic cached plan */ + PLAN_STMT_CACHE_CUSTOM, /* Custom cached plan */ +} PlannedStmtOrigin; + /* ---------------- * PlannedStmt node * @@ -53,10 +66,13 @@ typedef struct PlannedStmt CmdType commandType; /* query identifier (copied from Query) */ - uint64 queryId; + int64 queryId; /* plan identifier (can be set by plugins) */ - uint64 planId; + int64 planId; + + /* origin of plan */ + PlannedStmtOrigin planOrigin; /* is it insert|update|delete|merge RETURNING? */ bool hasReturning; @@ -101,9 +117,8 @@ typedef struct PlannedStmt */ List *permInfos; - /* rtable indexes of target relations for INSERT/UPDATE/DELETE/MERGE */ - /* integer list of RT indexes, or NIL */ - List *resultRelations; + /* RT indexes of relations targeted by INSERT/UPDATE/DELETE/MERGE */ + Bitmapset *resultRelationRelids; /* list of AppendRelInfo nodes */ List *appendRelations; @@ -113,12 +128,18 @@ typedef struct PlannedStmt */ List *subplans; + /* a list of SubPlanRTInfo objects */ + List *subrtinfos; + /* indices of subplans that require REWIND */ Bitmapset *rewindPlanIDs; /* a list of PlanRowMark's */ List *rowMarks; + /* RT indexes of relations with row marks */ + Bitmapset *rowMarkRelids; + /* OIDs of relations the plan depends on */ List *relationOids; @@ -131,6 +152,18 @@ typedef struct PlannedStmt /* non-null if this is utility stmt */ Node *utilityStmt; + /* info about nodes elided from the plan during setrefs processing */ + List *elidedNodes; + + /* + * DefElem objects added by extensions, e.g. using planner_shutdown_hook + * + * Set each DefElem's defname to the name of the plugin or extension, and + * the argument to a tree of nodes that all have copy and read/write + * support. + */ + List *extension_state; + /* statement location in source string (copied from Query) */ /* start location, or -1 if unknown */ ParseLoc stmt_location; @@ -234,6 +267,20 @@ typedef struct Plan #define outerPlan(node) (((Plan *)(node))->lefttree) +/* ---------------- + * ResultType - + * Classification of Result nodes + * ---------------- + */ +typedef enum ResultType +{ + RESULT_TYPE_GATING, /* project or one-time-filter outer plan */ + RESULT_TYPE_SCAN, /* replace empty scan */ + RESULT_TYPE_JOIN, /* replace empty join */ + RESULT_TYPE_UPPER, /* replace degenerate upper rel */ + RESULT_TYPE_MINMAX /* implement minmax aggregate */ +} ResultType; + /* ---------------- * Result node - * If no outer plan, evaluate a variable-free targetlist. @@ -243,12 +290,20 @@ typedef struct Plan * If resconstantqual isn't NULL, it represents a one-time qualification * test (i.e., one that doesn't depend on any variables from the outer plan, * so needs to be evaluated only once). + * + * relids identifies the relation for which this Result node is generating the + * tuples. When subplan is not NULL, it should be empty: this node is not + * generating anything in that case, just acting on tuples generated by the + * subplan. Otherwise, it contains the relids of the planner relation that + * the Result represents. * ---------------- */ typedef struct Result { Plan plan; + ResultType result_type; Node *resconstantqual; + Bitmapset *relids; } Result; /* ---------------- @@ -289,8 +344,6 @@ typedef struct ModifyTable Index nominalRelation; /* Root RT index, if partitioned/inherited */ Index rootRelation; - /* some part key in hierarchy updated? */ - bool partColsUpdated; /* integer list of RT indexes */ List *resultRelations; /* per-target-table update_colnos lists */ @@ -315,12 +368,16 @@ typedef struct ModifyTable OnConflictAction onConflictAction; /* List of ON CONFLICT arbiter index OIDs */ List *arbiterIndexes; + /* lock strength for ON CONFLICT DO SELECT */ + LockClauseStrength onConflictLockStrength; /* INSERT ON CONFLICT DO UPDATE targetlist */ List *onConflictSet; /* target column numbers for onConflictSet */ List *onConflictCols; - /* WHERE for ON CONFLICT UPDATE */ + /* WHERE for ON CONFLICT DO SELECT/UPDATE */ Node *onConflictWhere; + /* FOR PORTION OF clause for UPDATE/DELETE */ + Node *forPortionOf; /* RTI of the EXCLUDED pseudo relation */ Index exclRelRTI; /* tlist of the EXCLUDED pseudo relation */ @@ -341,9 +398,16 @@ struct PartitionPruneInfo; /* forward reference to struct below */ typedef struct Append { Plan plan; + /* RTIs of appendrel(s) formed by this node */ Bitmapset *apprelids; + + /* sets of RTIs of appendrels consolidated into this node */ + List *child_append_relid_sets; + + /* plans to run */ List *appendplans; + /* # of asynchronous plans */ int nasyncplans; @@ -373,6 +437,10 @@ typedef struct MergeAppend /* RTIs of appendrel(s) formed by this node */ Bitmapset *apprelids; + /* sets of RTIs of appendrels consolidated into this node */ + List *child_append_relid_sets; + + /* plans to run */ List *mergeplans; /* these fields are just like the sort-key info in struct Sort: */ @@ -428,7 +496,7 @@ typedef struct RecursiveUnion Oid *dupCollations pg_node_attr(array_size(numCols)); /* estimated number of groups in input */ - long numGroups; + Cardinality numGroups; } RecursiveUnion; /* ---------------- @@ -1056,6 +1124,16 @@ typedef struct Memoize /* paramids from param_exprs */ Bitmapset *keyparamids; + + /* Estimated number of rescans, for EXPLAIN */ + Cardinality est_calls; + + /* Estimated number of distinct lookup keys, for EXPLAIN */ + Cardinality est_unique_keys; + + /* Estimated cache hit ratio, for EXPLAIN */ + double est_hit_ratio; + } Memoize; /* ---------------- @@ -1149,7 +1227,7 @@ typedef struct Agg Oid *grpCollations pg_node_attr(array_size(numCols)); /* estimated number of groups in input */ - long numGroups; + Cardinality numGroups; /* for pass-by-ref transition data */ uint64 transitionSpace; @@ -1389,7 +1467,7 @@ typedef struct SetOp bool *cmpNullsFirst pg_node_attr(array_size(numCols)); /* estimated number of groups in left input */ - long numGroups; + Cardinality numGroups; } SetOp; /* ---------------- @@ -1764,4 +1842,35 @@ typedef enum MonotonicFunction MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING, } MonotonicFunction; +/* + * SubPlanRTInfo + * + * Information about which range table entries came from which subquery + * planning cycles. + */ +typedef struct SubPlanRTInfo +{ + NodeTag type; + char *plan_name; + Index rtoffset; + bool dummy; +} SubPlanRTInfo; + +/* + * ElidedNode + * + * Information about nodes elided from the final plan tree: trivial subquery + * scans, and single-child Append and MergeAppend nodes. + * + * plan_node_id is that of the surviving plan node, the sole child of the + * one which was elided. + */ +typedef struct ElidedNode +{ + NodeTag type; + int plan_node_id; + NodeTag elided_type; + Bitmapset *relids; +} ElidedNode; + #endif /* PLANNODES_H */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 7d3b4198f2661..6dfc946c20bd0 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -7,7 +7,7 @@ * and join trees. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/primnodes.h @@ -20,6 +20,7 @@ #include "access/attnum.h" #include "access/cmptype.h" #include "nodes/bitmapset.h" +#include "nodes/lockoptions.h" #include "nodes/pg_list.h" @@ -389,14 +390,16 @@ typedef enum ParamKind typedef struct Param { + pg_node_attr(custom_query_jumble) + Expr xpr; ParamKind paramkind; /* kind of parameter. See above */ int paramid; /* numeric ID for parameter */ Oid paramtype; /* pg_type OID of parameter's datatype */ /* typmod value, if known */ - int32 paramtypmod pg_node_attr(query_jumble_ignore); + int32 paramtypmod; /* OID of collation, or InvalidOid if none */ - Oid paramcollid pg_node_attr(query_jumble_ignore); + Oid paramcollid; /* token location, or -1 if unknown */ ParseLoc location; } Param; @@ -577,6 +580,17 @@ typedef struct GroupingFunc * Collation information is irrelevant for the query jumbling, as is the * internal state information of the node like "winstar" and "winagg". */ + +/* + * Null Treatment options. If specified, initially set to PARSER_IGNORE_NULLS + * which is then converted to IGNORE_NULLS if the window function allows the + * null treatment clause. + */ +#define NO_NULLTREATMENT 0 +#define PARSER_IGNORE_NULLS 1 +#define PARSER_RESPECT_NULLS 2 +#define IGNORE_NULLS 3 + typedef struct WindowFunc { Expr xpr; @@ -600,6 +614,8 @@ typedef struct WindowFunc bool winstar pg_node_attr(query_jumble_ignore); /* is function a simple aggregate? */ bool winagg pg_node_attr(query_jumble_ignore); + /* ignore nulls. One of the Null Treatment options */ + int ignore_nulls; /* token location, or -1 if unknown */ ParseLoc location; } WindowFunc; @@ -1093,6 +1109,7 @@ typedef struct SubPlan Oid firstColCollation; /* Collation of first column of subplan * result */ /* Information about execution strategy: */ + bool isInitPlan; /* true if it's an InitPlan */ bool useHashTable; /* true to store subselect output in a hash * table (implies we are doing "IN") */ bool unknownEqFalse; /* true if it's okay to return FALSE when the @@ -1107,6 +1124,7 @@ typedef struct SubPlan List *parParam; /* indices of input Params from parent plan */ List *args; /* exprs to pass as parParam values */ /* Estimated execution costs: */ + int disabled_nodes; /* count of disabled nodes in the plan */ Cost startup_cost; /* one-time setup cost */ Cost per_call_cost; /* cost for each subplan evaluation */ } SubPlan; @@ -1397,6 +1415,10 @@ typedef struct ArrayExpr List *elements pg_node_attr(query_jumble_squash); /* true if elements are sub-arrays */ bool multidims pg_node_attr(query_jumble_ignore); + /* location of the start of the elements list */ + ParseLoc list_start; + /* location of the end of the elements list */ + ParseLoc list_end; /* token location, or -1 if unknown */ ParseLoc location; } ArrayExpr; @@ -1741,6 +1763,7 @@ typedef struct JsonIsPredicate JsonFormat *format; /* FORMAT clause, if specified */ JsonValueType item_type; /* JSON item type */ bool unique_keys; /* check key uniqueness? */ + Oid exprBaseType; /* base type of the subject expression */ ParseLoc location; /* token location, or -1 if unknown */ } JsonIsPredicate; @@ -2157,6 +2180,30 @@ typedef struct ReturningExpr Expr *retexpr; /* expression to be returned */ } ReturningExpr; +/* + * GraphLabelRef - label reference in label expression inside GRAPH_TABLE clause + */ +typedef struct GraphLabelRef +{ + NodeTag type; + Oid labelid; + ParseLoc location; +} GraphLabelRef; + +/* + * GraphPropertyRef - property reference inside GRAPH_TABLE clause + */ +typedef struct GraphPropertyRef +{ + Expr xpr; + const char *elvarname; + Oid propid; + Oid typeId; + int32 typmod; + Oid collation; + ParseLoc location; +} GraphPropertyRef; + /*-------------------- * TargetEntry - * a target entry (used in query target lists) @@ -2350,7 +2397,7 @@ typedef struct FromExpr typedef struct OnConflictExpr { NodeTag type; - OnConflictAction action; /* DO NOTHING or UPDATE? */ + OnConflictAction action; /* DO NOTHING, SELECT, or UPDATE */ /* Arbiter */ List *arbiterElems; /* unique index arbiter list (of @@ -2358,11 +2405,51 @@ typedef struct OnConflictExpr Node *arbiterWhere; /* unique index arbiter WHERE clause */ Oid constraint; /* pg_constraint OID for arbiter */ - /* ON CONFLICT UPDATE */ + /* ON CONFLICT DO SELECT */ + LockClauseStrength lockStrength; /* strength of lock for DO SELECT */ + + /* ON CONFLICT DO UPDATE */ List *onConflictSet; /* List of ON CONFLICT SET TargetEntrys */ - Node *onConflictWhere; /* qualifiers to restrict UPDATE to */ + + /* both ON CONFLICT DO SELECT and UPDATE */ + Node *onConflictWhere; /* qualifiers to restrict SELECT/UPDATE */ int exclRelIndex; /* RT index of 'excluded' relation */ List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */ } OnConflictExpr; +/*---------- + * ForPortionOfExpr - represents a FOR PORTION OF ... expression + * + * We set up an expression to make a range from the FROM/TO bounds, + * so that we can use range operators with it. + * + * Then we set up an overlaps expression between that and the range column, + * so that we can find the rows we need to update/delete. + * + * If the user used the FROM ... TO ... syntax, we save the individual + * expressions so that we can deparse them. + * + * In the executor we'll also build an intersect expression between the + * targeted range and the range column, so that we can update the start/end + * bounds of the UPDATE'd record. + *---------- + */ +typedef struct ForPortionOfExpr +{ + NodeTag type; + Var *rangeVar; /* Range column */ + char *range_name; /* Range name */ + Node *targetFrom; /* FOR PORTION OF FROM bound, if given */ + Node *targetTo; /* FOR PORTION OF TO bound, if given */ + Node *targetRange; /* FOR PORTION OF bounds as a range/multirange */ + Oid rangeType; /* (base)type of targetRange */ + bool isDomain; /* Is rangeVar a domain? */ + Node *overlapsExpr; /* range && targetRange */ + List *rangeTargetList; /* List of TargetEntrys to set the time + * column(s) */ + Oid withoutPortionProc; /* SRF proc for old_range - target_range */ + ParseLoc location; /* token location, or -1 if unknown */ + ParseLoc targetLocation; /* token location, or -1 if unknown */ +} ForPortionOfExpr; + #endif /* PRIMNODES_H */ diff --git a/src/include/nodes/print.h b/src/include/nodes/print.h index cc304dae82c1c..4b28e2cda912d 100644 --- a/src/include/nodes/print.h +++ b/src/include/nodes/print.h @@ -4,7 +4,7 @@ * definitions for nodes/print.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/print.h diff --git a/src/include/nodes/queryjumble.h b/src/include/nodes/queryjumble.h index da7c7abed2e6a..f331449ba78f6 100644 --- a/src/include/nodes/queryjumble.h +++ b/src/include/nodes/queryjumble.h @@ -3,7 +3,7 @@ * queryjumble.h * Query normalization and fingerprinting. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -24,11 +24,11 @@ typedef struct LocationLen int location; /* start offset in query text */ int length; /* length in bytes, or -1 to ignore */ - /* - * Indicates that this location represents the beginning or end of a run - * of squashed constants. - */ + /* Does this location represent a squashed list? */ bool squashed; + + /* Is this location a PARAM_EXTERN parameter? */ + bool extern_param; } LocationLen; /* @@ -52,9 +52,18 @@ typedef struct JumbleState /* Current number of valid entries in clocations array */ int clocations_count; - /* highest Param id we've seen, in order to start normalization correctly */ + /* + * ID of the highest PARAM_EXTERN parameter we've seen in the query; used + * to start normalization correctly. However, if there are any squashed + * lists in the query, we disregard query-supplied parameter numbers and + * renumber everything. This is to avoid possible gaps caused by + * squashing in case any params are in squashed lists. + */ int highest_extern_param_id; + /* Whether squashable lists are present */ + bool has_squashed_lists; + /* * Count of the number of NULL nodes seen since last appending a value. * These are flushed out to the jumble buffer before subsequent appends @@ -82,6 +91,9 @@ extern PGDLLIMPORT int compute_query_id; extern const char *CleanQuerytext(const char *query, int *location, int *len); +extern LocationLen *ComputeConstantLengths(const JumbleState *jstate, + const char *query, + int query_loc); extern JumbleState *JumbleQuery(Query *query); extern void EnableQueryId(void); diff --git a/src/include/nodes/readfuncs.h b/src/include/nodes/readfuncs.h index dd260c446042c..0a5abdd9691f1 100644 --- a/src/include/nodes/readfuncs.h +++ b/src/include/nodes/readfuncs.h @@ -4,7 +4,7 @@ * header file for read.c and readfuncs.c. These functions are internal * to the stringToNode interface and should not be used by anyone else. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/readfuncs.h diff --git a/src/include/nodes/replnodes.h b/src/include/nodes/replnodes.h index 87e938933ebf7..74c6e129fda0d 100644 --- a/src/include/nodes/replnodes.h +++ b/src/include/nodes/replnodes.h @@ -4,7 +4,7 @@ * definitions for replication grammar parse nodes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/replnodes.h diff --git a/src/include/nodes/subscripting.h b/src/include/nodes/subscripting.h index 234e8ad80120c..301f21dac2f13 100644 --- a/src/include/nodes/subscripting.h +++ b/src/include/nodes/subscripting.h @@ -3,7 +3,7 @@ * subscripting.h * API for generic type subscripting * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/subscripting.h @@ -15,10 +15,10 @@ #include "nodes/primnodes.h" -/* Forward declarations, to avoid including other headers */ -struct ParseState; -struct SubscriptingRefState; -struct SubscriptExecSteps; +/* to avoid including other headers */ +typedef struct ParseState ParseState; +typedef struct SubscriptingRefState SubscriptingRefState; +typedef struct SubscriptExecSteps SubscriptExecSteps; /* * The SQL-visible function that defines a subscripting method is declared @@ -35,14 +35,14 @@ struct SubscriptExecSteps; * several bool flags that specify properties of the subscripting actions * this data type can perform: * - * fetch_strict indicates that a fetch SubscriptRef is strict, i.e., returns + * fetch_strict indicates that a fetch SubscriptingRef is strict, i.e., returns * NULL if any input (either the container or any subscript) is NULL. * - * fetch_leakproof indicates that a fetch SubscriptRef is leakproof, i.e., + * fetch_leakproof indicates that a fetch SubscriptingRef is leakproof, i.e., * will not throw any data-value-dependent errors. Typically this requires * silently returning NULL for invalid subscripts. * - * store_leakproof similarly indicates whether an assignment SubscriptRef is + * store_leakproof similarly indicates whether an assignment SubscriptingRef is * leakproof. (It is common to prefer throwing errors for invalid subscripts * in assignments; that's fine, but it makes the operation not leakproof. * In current usage there is no advantage in making assignments leakproof.) @@ -51,7 +51,7 @@ struct SubscriptExecSteps; * undesirable, since for example a null subscript in an assignment would * cause the entire container to become NULL. * - * Regardless of these flags, all SubscriptRefs are expected to be immutable, + * Regardless of these flags, all SubscriptingRefs are expected to be immutable, * that is they must always give the same results for the same inputs. * They are expected to always be parallel-safe, as well. */ @@ -94,7 +94,7 @@ struct SubscriptExecSteps; */ typedef void (*SubscriptTransform) (SubscriptingRef *sbsref, List *indirection, - struct ParseState *pstate, + ParseState *pstate, bool isSlice, bool isAssignment); @@ -151,17 +151,18 @@ typedef void (*SubscriptTransform) (SubscriptingRef *sbsref, * Set the relevant pointers to NULL for any omitted methods. */ typedef void (*SubscriptExecSetup) (const SubscriptingRef *sbsref, - struct SubscriptingRefState *sbsrefstate, - struct SubscriptExecSteps *methods); + SubscriptingRefState *sbsrefstate, + SubscriptExecSteps *methods); /* Struct returned by the SQL-visible subscript handler function */ typedef struct SubscriptRoutines { SubscriptTransform transform; /* parse analysis function */ SubscriptExecSetup exec_setup; /* expression compilation function */ - bool fetch_strict; /* is fetch SubscriptRef strict? */ - bool fetch_leakproof; /* is fetch SubscriptRef leakproof? */ - bool store_leakproof; /* is assignment SubscriptRef leakproof? */ + bool fetch_strict; /* is fetch SubscriptingRef strict? */ + bool fetch_leakproof; /* is fetch SubscriptingRef leakproof? */ + bool store_leakproof; /* is assignment SubscriptingRef + * leakproof? */ } SubscriptRoutines; #endif /* SUBSCRIPTING_H */ diff --git a/src/include/nodes/supportnodes.h b/src/include/nodes/supportnodes.h index 9c047cc401bec..132a2bcd8af41 100644 --- a/src/include/nodes/supportnodes.h +++ b/src/include/nodes/supportnodes.h @@ -23,7 +23,7 @@ * allows for future extensions of the set of request cases. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/nodes/supportnodes.h @@ -35,10 +35,12 @@ #include "nodes/plannodes.h" -struct PlannerInfo; /* avoid including pathnodes.h here */ -struct IndexOptInfo; -struct SpecialJoinInfo; -struct WindowClause; +typedef struct PlannerInfo PlannerInfo; /* avoid including pathnodes.h here */ +typedef struct IndexOptInfo IndexOptInfo; +typedef struct SpecialJoinInfo SpecialJoinInfo; +typedef struct WindowClause WindowClause; +typedef struct RangeTblFunction RangeTblFunction; /* ditto for parsenodes.h */ +typedef struct HeapTupleData *HeapTuple; /* and htup.h too */ /* * The Simplify request allows the support function to perform plan-time @@ -65,10 +67,63 @@ typedef struct SupportRequestSimplify { NodeTag type; - struct PlannerInfo *root; /* Planner's infrastructure */ + PlannerInfo *root; /* Planner's infrastructure */ FuncExpr *fcall; /* Function call to be simplified */ } SupportRequestSimplify; +/* + * Similar to SupportRequestSimplify but for Aggref node types. + * + * This supports conversions such as swapping COUNT(1) or COUNT(notnullcol) + * for COUNT(*). + * + * Supporting functions can consult 'root' and the input 'aggref'. When the + * implementing support function deems the simplification is possible, it must + * create a new Node (probably another Aggref) and not modify the original. + * The newly created Node should then be returned to indicate that the + * conversion is to take place. When no conversion is possible, a NULL + * pointer should be returned. + * + * It is important to consider that implementing support functions can receive + * Aggrefs with agglevelsup > 0. Careful consideration should be given to + * whether the simplification is still possible at levels above 0. + */ +typedef struct SupportRequestSimplifyAggref +{ + NodeTag type; + + PlannerInfo *root; /* Planner's infrastructure */ + Aggref *aggref; /* Aggref to be simplified */ +} SupportRequestSimplifyAggref; + +/* + * The InlineInFrom request allows the support function to perform plan-time + * simplification of a call to its target function that appears in FROM. + * The rules for this are sufficiently different from ordinary expressions + * that it's best to make this a separate request from Simplify. + * + * The planner's PlannerInfo "root" is typically not needed, but can be + * consulted if it's necessary to obtain info about Vars present in + * the given node tree. Beware that root could be NULL in some usages. + * + * "rtfunc" will be a RangeTblFunction node for the support function's target + * function. The call appeared alone (and without ORDINALITY) in FROM. + * + * "proc" will be the HeapTuple for the pg_proc row of the target function. + * + * The result should be a semantically-equivalent SELECT Query tree, + * or NULL if no simplification could be performed. The tree must have + * been passed through parse analysis and rewrite. + */ +typedef struct SupportRequestInlineInFrom +{ + NodeTag type; + + PlannerInfo *root; /* Planner's infrastructure */ + RangeTblFunction *rtfunc; /* Function call to be simplified */ + HeapTuple proc; /* Function definition from pg_proc */ +} SupportRequestInlineInFrom; + /* * The Selectivity request allows the support function to provide a * selectivity estimate for a function appearing at top level of a WHERE @@ -93,14 +148,14 @@ typedef struct SupportRequestSelectivity NodeTag type; /* Input fields: */ - struct PlannerInfo *root; /* Planner's infrastructure */ + PlannerInfo *root; /* Planner's infrastructure */ Oid funcid; /* function we are inquiring about */ List *args; /* pre-simplified arguments to function */ Oid inputcollid; /* function's input collation */ bool is_join; /* is this a join or restriction case? */ int varRelid; /* if restriction, RTI of target relation */ JoinType jointype; /* if join, outer join type */ - struct SpecialJoinInfo *sjinfo; /* if outer join, info about join */ + SpecialJoinInfo *sjinfo; /* if outer join, info about join */ /* Output fields: */ Selectivity selectivity; /* returned selectivity estimate */ @@ -133,7 +188,7 @@ typedef struct SupportRequestCost NodeTag type; /* Input fields: */ - struct PlannerInfo *root; /* Planner's infrastructure (could be NULL) */ + PlannerInfo *root; /* Planner's infrastructure (could be NULL) */ Oid funcid; /* function we are inquiring about */ Node *node; /* parse node invoking function, or NULL */ @@ -160,7 +215,7 @@ typedef struct SupportRequestRows NodeTag type; /* Input fields: */ - struct PlannerInfo *root; /* Planner's infrastructure (could be NULL) */ + PlannerInfo *root; /* Planner's infrastructure (could be NULL) */ Oid funcid; /* function we are inquiring about */ Node *node; /* parse node invoking function */ @@ -225,11 +280,11 @@ typedef struct SupportRequestIndexCondition NodeTag type; /* Input fields: */ - struct PlannerInfo *root; /* Planner's infrastructure */ + PlannerInfo *root; /* Planner's infrastructure */ Oid funcid; /* function we are inquiring about */ Node *node; /* parse node invoking function */ int indexarg; /* index of function arg matching indexcol */ - struct IndexOptInfo *index; /* planner's info about target index */ + IndexOptInfo *index; /* planner's info about target index */ int indexcol; /* index of target index column (0-based) */ Oid opfamily; /* index column's operator family */ Oid indexcollation; /* index column's collation */ @@ -293,7 +348,7 @@ typedef struct SupportRequestWFuncMonotonic /* Input fields: */ WindowFunc *window_func; /* Pointer to the window function data */ - struct WindowClause *window_clause; /* Pointer to the window clause data */ + WindowClause *window_clause; /* Pointer to the window clause data */ /* Output fields: */ MonotonicFunction monotonic; @@ -336,7 +391,7 @@ typedef struct SupportRequestOptimizeWindowClause /* Input fields: */ WindowFunc *window_func; /* Pointer to the window function data */ - struct WindowClause *window_clause; /* Pointer to the window clause data */ + WindowClause *window_clause; /* Pointer to the window clause data */ /* Input/Output fields: */ int frameOptions; /* New frameOptions, or left untouched if no diff --git a/src/include/nodes/tidbitmap.h b/src/include/nodes/tidbitmap.h index 99f795ceab526..4a09ab41cdf02 100644 --- a/src/include/nodes/tidbitmap.h +++ b/src/include/nodes/tidbitmap.h @@ -13,7 +13,7 @@ * fact that a particular page needs to be visited. * * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/nodes/tidbitmap.h * @@ -22,7 +22,6 @@ #ifndef TIDBITMAP_H #define TIDBITMAP_H -#include "access/htup_details.h" #include "storage/itemptr.h" #include "utils/dsa.h" @@ -86,7 +85,7 @@ extern void tbm_free(TIDBitmap *tbm); extern void tbm_free_shared_area(dsa_area *dsa, dsa_pointer dp); extern void tbm_add_tuples(TIDBitmap *tbm, - const ItemPointer tids, int ntids, + const ItemPointerData *tids, int ntids, bool recheck); extern void tbm_add_page(TIDBitmap *tbm, BlockNumber pageno); diff --git a/src/include/nodes/value.h b/src/include/nodes/value.h index 3ee3b976b8f9b..9f038410ed337 100644 --- a/src/include/nodes/value.h +++ b/src/include/nodes/value.h @@ -4,7 +4,7 @@ * interface for value nodes * * - * Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/nodes/value.h * diff --git a/src/include/optimizer/appendinfo.h b/src/include/optimizer/appendinfo.h index d06f93b72667b..b59a6218853de 100644 --- a/src/include/optimizer/appendinfo.h +++ b/src/include/optimizer/appendinfo.h @@ -4,7 +4,7 @@ * Routines for mapping expressions between append rel parent(s) and * children * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/appendinfo.h diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h index 0dffec00ede93..853a28c0007a2 100644 --- a/src/include/optimizer/clauses.h +++ b/src/include/optimizer/clauses.h @@ -4,7 +4,7 @@ * prototypes for clauses.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/clauses.h @@ -42,6 +42,7 @@ extern Relids find_nonnullable_rels(Node *clause); extern List *find_nonnullable_vars(Node *clause); extern List *find_forced_null_vars(Node *node); extern Var *find_forced_null_var(Node *node); +extern bool query_outputs_are_not_nullable(Query *query); extern bool is_pseudo_constant_clause(Node *clause); extern bool is_pseudo_constant_clause_relids(Node *clause, Relids relids); @@ -50,8 +51,8 @@ extern int NumRelids(PlannerInfo *root, Node *clause); extern void CommuteOpExpr(OpExpr *clause); -extern Query *inline_set_returning_function(PlannerInfo *root, - RangeTblEntry *rte); +extern Query *inline_function_in_from(PlannerInfo *root, + RangeTblEntry *rte); extern Bitmapset *pull_paramids(Expr *expr); diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index d397fe27dc1e1..f2fd5d315078d 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -4,7 +4,7 @@ * prototypes for costsize.c and clausesel.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/cost.h @@ -118,14 +118,14 @@ extern void cost_incremental_sort(Path *path, Cost input_startup_cost, Cost input_total_cost, double input_tuples, int width, Cost comparison_cost, int sort_mem, double limit_tuples); -extern void cost_append(AppendPath *apath); +extern void cost_append(AppendPath *apath, PlannerInfo *root); extern void cost_merge_append(Path *path, PlannerInfo *root, List *pathkeys, int n_streams, int input_disabled_nodes, Cost input_startup_cost, Cost input_total_cost, double tuples); extern void cost_material(Path *path, - int input_disabled_nodes, + bool enabled, int input_disabled_nodes, Cost input_startup_cost, Cost input_total_cost, double tuples, int width); extern void cost_agg(Path *path, PlannerInfo *root, @@ -148,7 +148,7 @@ extern void cost_group(Path *path, PlannerInfo *root, double input_tuples); extern void initial_cost_nestloop(PlannerInfo *root, JoinCostWorkspace *workspace, - JoinType jointype, + JoinType jointype, uint64 enable_mask, Path *outer_path, Path *inner_path, JoinPathExtraData *extra); extern void final_cost_nestloop(PlannerInfo *root, NestPath *path, diff --git a/src/include/optimizer/extendplan.h b/src/include/optimizer/extendplan.h new file mode 100644 index 0000000000000..12e6ca90c8f47 --- /dev/null +++ b/src/include/optimizer/extendplan.h @@ -0,0 +1,72 @@ +/*------------------------------------------------------------------------- + * + * extendplan.h + * Extend core planner objects with additional private state + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/optimizer/extendplan.h + * + *------------------------------------------------------------------------- + */ +#ifndef EXTENDPLAN_H +#define EXTENDPLAN_H + +#include "nodes/pathnodes.h" + +extern int GetPlannerExtensionId(const char *extension_name); + +/* + * Get extension-specific state from a PlannerGlobal. + */ +static inline void * +GetPlannerGlobalExtensionState(PlannerGlobal *glob, int extension_id) +{ + Assert(extension_id >= 0); + + if (extension_id >= glob->extension_state_allocated) + return NULL; + + return glob->extension_state[extension_id]; +} + +/* + * Get extension-specific state from a PlannerInfo. + */ +static inline void * +GetPlannerInfoExtensionState(PlannerInfo *root, int extension_id) +{ + Assert(extension_id >= 0); + + if (extension_id >= root->extension_state_allocated) + return NULL; + + return root->extension_state[extension_id]; +} + +/* + * Get extension-specific state from a PlannerInfo. + */ +static inline void * +GetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id) +{ + Assert(extension_id >= 0); + + if (extension_id >= rel->extension_state_allocated) + return NULL; + + return rel->extension_state[extension_id]; +} + +/* Functions to store private state into various planner objects */ +extern void SetPlannerGlobalExtensionState(PlannerGlobal *glob, + int extension_id, + void *opaque); +extern void SetPlannerInfoExtensionState(PlannerInfo *root, int extension_id, + void *opaque); +extern void SetRelOptInfoExtensionState(RelOptInfo *rel, int extension_id, + void *opaque); + +#endif diff --git a/src/include/optimizer/geqo.h b/src/include/optimizer/geqo.h index 9f8e0f337aad4..890b47d281b0f 100644 --- a/src/include/optimizer/geqo.h +++ b/src/include/optimizer/geqo.h @@ -3,7 +3,7 @@ * geqo.h * prototypes for various files in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo.h @@ -24,6 +24,7 @@ #include "common/pg_prng.h" #include "nodes/pathnodes.h" +#include "optimizer/extendplan.h" #include "optimizer/geqo_gene.h" @@ -62,6 +63,8 @@ extern PGDLLIMPORT int Geqo_generations; /* 1 .. inf, or 0 to use default */ extern PGDLLIMPORT double Geqo_selection_bias; +extern PGDLLIMPORT int Geqo_planner_extension_id; + #define DEFAULT_GEQO_SELECTION_BIAS 2.0 #define MIN_GEQO_SELECTION_BIAS 1.5 #define MAX_GEQO_SELECTION_BIAS 2.0 @@ -70,7 +73,7 @@ extern PGDLLIMPORT double Geqo_seed; /* 0 .. 1 */ /* - * Private state for a GEQO run --- accessible via root->join_search_private + * Private state for a GEQO run --- accessible via GetGeqoPrivateData */ typedef struct { @@ -78,6 +81,13 @@ typedef struct pg_prng_state random_state; /* PRNG state */ } GeqoPrivateData; +static inline GeqoPrivateData * +GetGeqoPrivateData(PlannerInfo *root) +{ + /* headers must be C++-compliant, so the cast is required here */ + return (GeqoPrivateData *) + GetPlannerInfoExtensionState(root, Geqo_planner_extension_id); +} /* routines in geqo_main.c */ extern RelOptInfo *geqo(PlannerInfo *root, diff --git a/src/include/optimizer/geqo_copy.h b/src/include/optimizer/geqo_copy.h index 10e639f3ba85c..55e0d535812ca 100644 --- a/src/include/optimizer/geqo_copy.h +++ b/src/include/optimizer/geqo_copy.h @@ -3,7 +3,7 @@ * geqo_copy.h * prototypes for copy functions in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_copy.h diff --git a/src/include/optimizer/geqo_gene.h b/src/include/optimizer/geqo_gene.h index 9b9bcaadb7eb2..408462db666af 100644 --- a/src/include/optimizer/geqo_gene.h +++ b/src/include/optimizer/geqo_gene.h @@ -3,7 +3,7 @@ * geqo_gene.h * genome representation in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_gene.h diff --git a/src/include/optimizer/geqo_misc.h b/src/include/optimizer/geqo_misc.h index 233f7c104616f..0525bea554742 100644 --- a/src/include/optimizer/geqo_misc.h +++ b/src/include/optimizer/geqo_misc.h @@ -3,7 +3,7 @@ * geqo_misc.h * prototypes for printout routines in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_misc.h diff --git a/src/include/optimizer/geqo_mutation.h b/src/include/optimizer/geqo_mutation.h index 57c8c91bbcea5..a433da0ae0f9e 100644 --- a/src/include/optimizer/geqo_mutation.h +++ b/src/include/optimizer/geqo_mutation.h @@ -3,7 +3,7 @@ * geqo_mutation.h * prototypes for mutation functions in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_mutation.h diff --git a/src/include/optimizer/geqo_pool.h b/src/include/optimizer/geqo_pool.h index a28324af467b5..bd3de6248a73a 100644 --- a/src/include/optimizer/geqo_pool.h +++ b/src/include/optimizer/geqo_pool.h @@ -3,7 +3,7 @@ * geqo_pool.h * pool representation in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_pool.h diff --git a/src/include/optimizer/geqo_random.h b/src/include/optimizer/geqo_random.h index 8fe28e46843f2..a6f34d2ba5f7c 100644 --- a/src/include/optimizer/geqo_random.h +++ b/src/include/optimizer/geqo_random.h @@ -3,7 +3,7 @@ * geqo_random.h * random number generator * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_random.h diff --git a/src/include/optimizer/geqo_recombination.h b/src/include/optimizer/geqo_recombination.h index af394a2246048..1c07c92349006 100644 --- a/src/include/optimizer/geqo_recombination.h +++ b/src/include/optimizer/geqo_recombination.h @@ -3,7 +3,7 @@ * geqo_recombination.h * prototypes for recombination in the genetic query optimizer * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_recombination.h @@ -79,11 +79,11 @@ extern void px(PlannerInfo *root, Gene *tour1, Gene *tour2, Gene *offspring, int num_gene, City * city_table); /* order crossover [OX1] according to Davis */ -extern void ox1(PlannerInfo *root, Gene *mom, Gene *dad, Gene *offspring, +extern void ox1(PlannerInfo *root, Gene *tour1, Gene *tour2, Gene *offspring, int num_gene, City * city_table); /* order crossover [OX2] according to Syswerda */ -extern void ox2(PlannerInfo *root, Gene *mom, Gene *dad, Gene *offspring, +extern void ox2(PlannerInfo *root, Gene *tour1, Gene *tour2, Gene *offspring, int num_gene, City * city_table); #endif /* GEQO_RECOMBINATION_H */ diff --git a/src/include/optimizer/geqo_selection.h b/src/include/optimizer/geqo_selection.h index bfe3c2280d063..a1e82d88f60e5 100644 --- a/src/include/optimizer/geqo_selection.h +++ b/src/include/optimizer/geqo_selection.h @@ -3,7 +3,7 @@ * geqo_selection.h * prototypes for selection routines in optimizer/geqo * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/geqo_selection.h diff --git a/src/include/optimizer/inherit.h b/src/include/optimizer/inherit.h index 5f8df60b9dd9d..ec9d2686ba3ea 100644 --- a/src/include/optimizer/inherit.h +++ b/src/include/optimizer/inherit.h @@ -4,7 +4,7 @@ * prototypes for inherit.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/inherit.h diff --git a/src/include/optimizer/joininfo.h b/src/include/optimizer/joininfo.h index 2aa52c741be70..117195fbcfaf1 100644 --- a/src/include/optimizer/joininfo.h +++ b/src/include/optimizer/joininfo.h @@ -4,7 +4,7 @@ * prototypes for joininfo.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/joininfo.h diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h index 546828b54bd27..cb6241e2bdd08 100644 --- a/src/include/optimizer/optimizer.h +++ b/src/include/optimizer/optimizer.h @@ -12,7 +12,7 @@ * example. For the most part, however, code outside the core planner * should not need to include any optimizer/ header except this one. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/optimizer.h @@ -24,31 +24,22 @@ #include "nodes/parsenodes.h" +typedef struct ExplainState ExplainState; /* defined in explain_state.h */ + /* * We don't want to include nodes/pathnodes.h here, because non-planner * code should generally treat PlannerInfo as an opaque typedef. * But we'd like such code to use that typedef name, so define the - * typedef either here or in pathnodes.h, whichever is read first. + * typedef both here and in pathnodes.h. */ -#ifndef HAVE_PLANNERINFO_TYPEDEF typedef struct PlannerInfo PlannerInfo; -#define HAVE_PLANNERINFO_TYPEDEF 1 -#endif - -/* Likewise for IndexOptInfo and SpecialJoinInfo. */ -#ifndef HAVE_INDEXOPTINFO_TYPEDEF typedef struct IndexOptInfo IndexOptInfo; -#define HAVE_INDEXOPTINFO_TYPEDEF 1 -#endif -#ifndef HAVE_SPECIALJOININFO_TYPEDEF typedef struct SpecialJoinInfo SpecialJoinInfo; -#define HAVE_SPECIALJOININFO_TYPEDEF 1 -#endif /* It also seems best not to include plannodes.h, params.h, or htup.h here */ -struct PlannedStmt; -struct ParamListInfoData; -struct HeapTupleData; +typedef struct PlannedStmt PlannedStmt; +typedef struct ParamListInfoData *ParamListInfo; +typedef struct HeapTupleData *HeapTuple; /* in path/clausesel.c: */ @@ -91,7 +82,6 @@ extern PGDLLIMPORT int effective_cache_size; extern double clamp_row_est(double nrows); extern int32 clamp_width_est(int64 tuple_width); -extern long clamp_cardinality_to_long(Cardinality x); /* in path/indxpath.c: */ @@ -113,9 +103,10 @@ extern PGDLLIMPORT int debug_parallel_query; extern PGDLLIMPORT bool parallel_leader_participation; extern PGDLLIMPORT bool enable_distinct_reordering; -extern struct PlannedStmt *planner(Query *parse, const char *query_string, - int cursorOptions, - struct ParamListInfoData *boundParams); +extern PlannedStmt *planner(Query *parse, const char *query_string, + int cursorOptions, + ParamListInfo boundParams, + ExplainState *es); extern Expr *expression_planner(Expr *expr); extern Expr *expression_planner_with_deps(Expr *expr, @@ -139,6 +130,14 @@ extern Expr *canonicalize_qual(Expr *qual, bool is_check); /* in util/clauses.c: */ +/* Enum to specify where var_is_nonnullable should look for NOT NULL proofs */ +typedef enum +{ + NOTNULL_SOURCE_RELOPT, /* Use RelOptInfo */ + NOTNULL_SOURCE_HASHTABLE, /* Use Global Hash Table */ + NOTNULL_SOURCE_CATALOG, /* Use System Catalog */ +} NotNullSource; + extern bool contain_mutable_functions(Node *clause); extern bool contain_mutable_functions_after_planning(Expr *expr); extern bool contain_volatile_functions(Node *clause); @@ -154,9 +153,15 @@ extern Node *estimate_expression_value(PlannerInfo *root, Node *node); extern Expr *evaluate_expr(Expr *expr, Oid result_type, int32 result_typmod, Oid result_collation); +extern bool var_is_nonnullable(PlannerInfo *root, Var *var, + NotNullSource source); + +extern bool expr_is_nonnullable(PlannerInfo *root, Expr *expr, + NotNullSource source); + extern List *expand_function_arguments(List *args, bool include_out_arguments, Oid result_type, - struct HeapTupleData *func_tuple); + HeapTuple func_tuple); extern ScalarArrayOpExpr *make_SAOP_expr(Oid oper, Node *leftexpr, Oid coltype, Oid arraycollid, @@ -197,8 +202,6 @@ extern SortGroupClause *get_sortgroupref_clause_noerr(Index sortref, * output list */ #define PVC_RECURSE_PLACEHOLDERS 0x0020 /* recurse into PlaceHolderVar * arguments */ -#define PVC_INCLUDE_CONVERTROWTYPES 0x0040 /* include ConvertRowtypeExprs in - * output list */ extern Bitmapset *pull_varnos(PlannerInfo *root, Node *node); extern Bitmapset *pull_varnos_of_level(PlannerInfo *root, Node *node, int levelsup); @@ -210,6 +213,8 @@ extern bool contain_vars_returning_old_or_new(Node *node); extern int locate_var_of_level(Node *node, int levelsup); extern List *pull_var_clause(Node *node, int flags); extern Node *flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node); +extern Node *flatten_join_alias_for_parser(Query *query, Node *node, + int sublevels_up); extern Node *flatten_group_exprs(PlannerInfo *root, Query *query, Node *node); #endif /* OPTIMIZER_H */ diff --git a/src/include/optimizer/orclauses.h b/src/include/optimizer/orclauses.h index 046b08aaedb7a..a2554ac3b513a 100644 --- a/src/include/optimizer/orclauses.h +++ b/src/include/optimizer/orclauses.h @@ -4,7 +4,7 @@ * prototypes for orclauses.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/orclauses.h diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h index 59dcb1ff05399..5294aa6ff75ef 100644 --- a/src/include/optimizer/paramassign.h +++ b/src/include/optimizer/paramassign.h @@ -3,7 +3,7 @@ * paramassign.h * Functions for assigning PARAM_EXEC slots during planning. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/paramassign.h @@ -30,7 +30,8 @@ extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root, extern void process_subquery_nestloop_params(PlannerInfo *root, List *subplan_params); extern List *identify_current_nestloop_params(PlannerInfo *root, - Relids leftrelids); + Relids leftrelids, + Relids outerrelids); extern Param *generate_new_exec_param(PlannerInfo *root, Oid paramtype, int32 paramtypmod, Oid paramcollation); extern int assign_special_exec_param(PlannerInfo *root); diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index 60dcdb77e41be..e8db321f92be2 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -4,7 +4,7 @@ * prototypes for pathnode.c, relnode.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/pathnode.h @@ -17,6 +17,35 @@ #include "nodes/bitmapset.h" #include "nodes/pathnodes.h" +/* Hook for plugins to get control in build_simple_rel() */ +typedef void (*build_simple_rel_hook_type) (PlannerInfo *root, + RelOptInfo *rel, + RangeTblEntry *rte); +extern PGDLLIMPORT build_simple_rel_hook_type build_simple_rel_hook; + +/* + * Everything in subpaths or partial_subpaths will become part of the + * Append node's subpaths list. Partial and non-partial subpaths can be + * mixed in the same Append node only if it is parallel-aware. + * + * See the comments for AppendPath for the meaning and purpose of the + * child_append_relid_sets field. + */ +typedef struct AppendPathInput +{ + List *subpaths; + List *partial_subpaths; + List *child_append_relid_sets; +} AppendPathInput; + +/* Hook for plugins to get control during joinrel setup */ +typedef void (*joinrel_setup_hook_type) (PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outer_rel, + RelOptInfo *inner_rel, + SpecialJoinInfo *sjinfo, + List *restrictlist); +extern PGDLLIMPORT joinrel_setup_hook_type joinrel_setup_hook; /* * prototypes for pathnode.c @@ -32,7 +61,7 @@ extern bool add_path_precheck(RelOptInfo *parent_rel, int disabled_nodes, List *pathkeys, Relids required_outer); extern void add_partial_path(RelOptInfo *parent_rel, Path *new_path); extern bool add_partial_path_precheck(RelOptInfo *parent_rel, - int disabled_nodes, + int disabled_nodes, Cost startup_cost, Cost total_cost, List *pathkeys); extern Path *create_seqscan_path(PlannerInfo *root, RelOptInfo *rel, @@ -67,22 +96,26 @@ extern TidPath *create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, extern TidRangePath *create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel, List *tidrangequals, - Relids required_outer); + Relids required_outer, + int parallel_workers); + extern AppendPath *create_append_path(PlannerInfo *root, RelOptInfo *rel, - List *subpaths, List *partial_subpaths, + AppendPathInput input, List *pathkeys, Relids required_outer, int parallel_workers, bool parallel_aware, double rows); extern MergeAppendPath *create_merge_append_path(PlannerInfo *root, RelOptInfo *rel, List *subpaths, + List *child_append_relid_sets, List *pathkeys, Relids required_outer); extern GroupResultPath *create_group_result_path(PlannerInfo *root, RelOptInfo *rel, PathTarget *target, List *havingqual); -extern MaterialPath *create_material_path(RelOptInfo *rel, Path *subpath); +extern MaterialPath *create_material_path(RelOptInfo *rel, Path *subpath, + bool enabled); extern MemoizePath *create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, @@ -90,9 +123,7 @@ extern MemoizePath *create_memoize_path(PlannerInfo *root, List *hash_operators, bool singlerow, bool binary_mode, - double calls); -extern UniquePath *create_unique_path(PlannerInfo *root, RelOptInfo *rel, - Path *subpath, SpecialJoinInfo *sjinfo); + Cardinality est_calls); extern GatherPath *create_gather_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, PathTarget *target, Relids required_outer, double *rows); @@ -223,11 +254,11 @@ extern GroupPath *create_group_path(PlannerInfo *root, List *groupClause, List *qual, double numGroups); -extern UpperUniquePath *create_upper_unique_path(PlannerInfo *root, - RelOptInfo *rel, - Path *subpath, - int numCols, - double numGroups); +extern UniquePath *create_unique_path(PlannerInfo *root, + RelOptInfo *rel, + Path *subpath, + int numCols, + double numGroups); extern AggPath *create_agg_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, @@ -283,13 +314,12 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root, Path *subpath, CmdType operation, bool canSetTag, Index nominalRelation, Index rootRelation, - bool partColsUpdated, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, List *mergeActionLists, List *mergeJoinConditions, - int epqParam); + ForPortionOfExpr *forPortionOf, int epqParam); extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, Node *limitOffset, Node *limitCount, @@ -314,6 +344,8 @@ extern void setup_simple_rel_arrays(PlannerInfo *root); extern void expand_planner_arrays(PlannerInfo *root, int add_size); extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent); +extern RelOptInfo *build_simple_grouped_rel(PlannerInfo *root, RelOptInfo *rel); +extern RelOptInfo *build_grouped_rel(PlannerInfo *root, RelOptInfo *rel); extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid); extern RelOptInfo *find_base_rel_noerr(PlannerInfo *root, int relid); extern RelOptInfo *find_base_rel_ignore_join(PlannerInfo *root, int relid); @@ -353,4 +385,6 @@ extern RelOptInfo *build_child_join_rel(PlannerInfo *root, SpecialJoinInfo *sjinfo, int nappinfos, AppendRelInfo **appinfos); +extern RelAggInfo *create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel, + bool calculate_grouped_rows); #endif /* PATHNODE_H */ diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h index a48c972179712..17f2099ec3be4 100644 --- a/src/include/optimizer/paths.h +++ b/src/include/optimizer/paths.h @@ -4,7 +4,7 @@ * prototypes for various files in optimizer/path * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/paths.h @@ -21,12 +21,21 @@ * allpaths.c */ extern PGDLLIMPORT bool enable_geqo; +extern PGDLLIMPORT bool enable_eager_aggregate; extern PGDLLIMPORT int geqo_threshold; +extern PGDLLIMPORT double min_eager_agg_group_size; extern PGDLLIMPORT int min_parallel_table_scan_size; extern PGDLLIMPORT int min_parallel_index_scan_size; extern PGDLLIMPORT bool enable_group_by_reordering; -/* Hook for plugins to get control in set_rel_pathlist() */ +/* Hooks for plugins to get control in set_rel_pathlist() */ +typedef void (*join_path_setup_hook_type) (PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + JoinType jointype, + JoinPathExtraData *extra); +extern PGDLLIMPORT join_path_setup_hook_type join_path_setup_hook; typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root, RelOptInfo *rel, Index rti, @@ -57,6 +66,8 @@ extern void generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows); extern void generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows); +extern void generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel, + RelOptInfo *rel); extern int compute_parallel_worker(RelOptInfo *rel, double heap_pages, double index_pages, int max_workers); extern void create_partial_bitmap_paths(PlannerInfo *root, RelOptInfo *rel, @@ -71,10 +82,7 @@ extern void generate_partitionwise_join_paths(PlannerInfo *root, extern void create_index_paths(PlannerInfo *root, RelOptInfo *rel); extern bool relation_has_unique_index_for(PlannerInfo *root, RelOptInfo *rel, List *restrictlist, - List *exprlist, List *oprlist); -extern bool relation_has_unique_index_ext(PlannerInfo *root, RelOptInfo *rel, - List *restrictlist, List *exprlist, - List *oprlist, List **extra_clauses); + List **extra_clauses); extern bool indexcol_is_bool_constant_for_query(PlannerInfo *root, IndexOptInfo *index, int indexcol); @@ -109,8 +117,6 @@ extern Relids add_outer_joins_to_relids(PlannerInfo *root, Relids input_relids, List **pushed_down_joins); extern bool have_join_order_restriction(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2); -extern bool have_dangerous_phv(PlannerInfo *root, - Relids outer_relids, Relids inner_params); extern void mark_dummy_rel(RelOptInfo *rel); extern void init_dummy_sjinfo(SpecialJoinInfo *sjinfo, Relids left_relids, Relids right_relids); diff --git a/src/include/optimizer/placeholder.h b/src/include/optimizer/placeholder.h index d351045e2e056..60798281090a6 100644 --- a/src/include/optimizer/placeholder.h +++ b/src/include/optimizer/placeholder.h @@ -4,7 +4,7 @@ * prototypes for optimizer/util/placeholder.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/placeholder.h @@ -30,5 +30,8 @@ extern void add_placeholders_to_joinrel(PlannerInfo *root, RelOptInfo *joinrel, SpecialJoinInfo *sjinfo); extern bool contain_placeholder_references_to(PlannerInfo *root, Node *clause, int relid); +extern Relids get_placeholder_nulling_relids(PlannerInfo *root, + PlaceHolderInfo *phinfo); +extern Node *strip_noop_phvs(Node *node); #endif /* PLACEHOLDER_H */ diff --git a/src/include/optimizer/plancat.h b/src/include/optimizer/plancat.h index cd74e4b1e8b36..09baf1a691643 100644 --- a/src/include/optimizer/plancat.h +++ b/src/include/optimizer/plancat.h @@ -4,7 +4,7 @@ * prototypes for plancat.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/plancat.h @@ -17,17 +17,13 @@ #include "nodes/pathnodes.h" #include "utils/relcache.h" -/* Hook for plugins to get control in get_relation_info() */ -typedef void (*get_relation_info_hook_type) (PlannerInfo *root, - Oid relationObjectId, - bool inhparent, - RelOptInfo *rel); -extern PGDLLIMPORT get_relation_info_hook_type get_relation_info_hook; - - extern void get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, RelOptInfo *rel); +extern void get_relation_notnullatts(PlannerInfo *root, Relation relation); + +extern Bitmapset *find_relation_notnullatts(PlannerInfo *root, Oid relid); + extern List *infer_arbiter_indexes(PlannerInfo *root); extern void estimate_rel_size(Relation rel, int32 *attr_widths, @@ -72,6 +68,8 @@ extern double get_function_rows(PlannerInfo *root, Oid funcid, Node *node); extern bool has_row_triggers(PlannerInfo *root, Index rti, CmdType event); +extern bool has_transition_tables(PlannerInfo *root, Index rti, CmdType event); + extern bool has_stored_generated_columns(PlannerInfo *root, Index rti); extern Bitmapset *get_dependent_generated_columns(PlannerInfo *root, Index rti, diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h index 9d3debcab2801..d0dc3761b13fc 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -4,7 +4,7 @@ * prototypes for various files in optimizer/plan * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/planmain.h @@ -55,7 +55,7 @@ extern Sort *make_sort_from_sortclauses(List *sortcls, Plan *lefttree); extern Agg *make_agg(List *tlist, List *qual, AggStrategy aggstrategy, AggSplit aggsplit, int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations, - List *groupingSets, List *chain, double dNumGroups, + List *groupingSets, List *chain, Cardinality numGroups, Size transitionSpace, Plan *lefttree); extern Limit *make_limit(Plan *lefttree, Node *limitOffset, Node *limitCount, LimitOption limitOption, int uniqNumCols, @@ -76,6 +76,7 @@ extern void add_vars_to_targetlist(PlannerInfo *root, List *vars, extern void add_vars_to_attr_needed(PlannerInfo *root, List *vars, Relids where_needed); extern void remove_useless_groupby_columns(PlannerInfo *root); +extern void setup_eager_aggregation(PlannerInfo *root); extern void find_lateral_references(PlannerInfo *root); extern void rebuild_lateral_attr_needed(PlannerInfo *root); extern void create_lateral_join_info(PlannerInfo *root); diff --git a/src/include/optimizer/planner.h b/src/include/optimizer/planner.h index 347c582a78927..9c4950b340ffb 100644 --- a/src/include/optimizer/planner.h +++ b/src/include/optimizer/planner.h @@ -8,7 +8,7 @@ * non-planner code. Declarations here are meant for use by other * planner modules. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/planner.h @@ -22,13 +22,30 @@ #include "nodes/plannodes.h" +typedef struct ExplainState ExplainState; /* defined in explain_state.h */ + /* Hook for plugins to get control in planner() */ typedef PlannedStmt *(*planner_hook_type) (Query *parse, const char *query_string, int cursorOptions, - ParamListInfo boundParams); + ParamListInfo boundParams, + ExplainState *es); extern PGDLLIMPORT planner_hook_type planner_hook; +/* Hook for plugins to get control after PlannerGlobal is initialized */ +typedef void (*planner_setup_hook_type) (PlannerGlobal *glob, Query *parse, + const char *query_string, + int cursorOptions, + double *tuple_fraction, + ExplainState *es); +extern PGDLLIMPORT planner_setup_hook_type planner_setup_hook; + +/* Hook for plugins to get control before PlannerGlobal is discarded */ +typedef void (*planner_shutdown_hook_type) (PlannerGlobal *glob, Query *parse, + const char *query_string, + PlannedStmt *pstmt); +extern PGDLLIMPORT planner_shutdown_hook_type planner_shutdown_hook; + /* Hook for plugins to get control when grouping_planner() plans upper rels */ typedef void (*create_upper_paths_hook_type) (PlannerInfo *root, UpperRelationKind stage, @@ -40,10 +57,13 @@ extern PGDLLIMPORT create_upper_paths_hook_type create_upper_paths_hook; extern PlannedStmt *standard_planner(Query *parse, const char *query_string, int cursorOptions, - ParamListInfo boundParams); + ParamListInfo boundParams, + ExplainState *es); extern PlannerInfo *subquery_planner(PlannerGlobal *glob, Query *parse, + char *plan_name, PlannerInfo *parent_root, + PlannerInfo *alternative_root, bool hasRecursion, double tuple_fraction, SetOperationStmt *setops); @@ -59,4 +79,10 @@ extern Path *get_cheapest_fractional_path(RelOptInfo *rel, extern Expr *preprocess_phv_expression(PlannerInfo *root, Expr *expr); +extern RelOptInfo *create_unique_paths(PlannerInfo *root, RelOptInfo *rel, + SpecialJoinInfo *sjinfo); + +extern char *choose_plan_name(PlannerGlobal *glob, const char *name, + bool always_number); + #endif /* PLANNER_H */ diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h index df56202777c7f..00bc567da760b 100644 --- a/src/include/optimizer/prep.h +++ b/src/include/optimizer/prep.h @@ -4,7 +4,7 @@ * prototypes for files in optimizer/prep/ * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/prep.h @@ -22,10 +22,10 @@ * prototypes for prepjointree.c */ extern void transform_MERGE_to_join(Query *parse); +extern Query *preprocess_relation_rtes(PlannerInfo *root); extern void replace_empty_jointree(Query *parse); extern void pull_up_sublinks(PlannerInfo *root); extern void preprocess_function_rtes(PlannerInfo *root); -extern Query *expand_virtual_generated_columns(PlannerInfo *root); extern void pull_up_subqueries(PlannerInfo *root); extern void flatten_simple_union_all(PlannerInfo *root); extern void reduce_outer_joins(PlannerInfo *root); diff --git a/src/include/optimizer/restrictinfo.h b/src/include/optimizer/restrictinfo.h index ec91fc9c58308..72e9410d58085 100644 --- a/src/include/optimizer/restrictinfo.h +++ b/src/include/optimizer/restrictinfo.h @@ -4,7 +4,7 @@ * prototypes for restrictinfo.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/restrictinfo.h diff --git a/src/include/optimizer/subselect.h b/src/include/optimizer/subselect.h index 48507eb4bcad3..4ecccf46bd372 100644 --- a/src/include/optimizer/subselect.h +++ b/src/include/optimizer/subselect.h @@ -3,7 +3,7 @@ * subselect.h * Planning routines for subselects. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/subselect.h @@ -22,6 +22,7 @@ extern ScalarArrayOpExpr *convert_VALUES_to_ANY(PlannerInfo *root, Query *values); extern JoinExpr *convert_ANY_sublink_to_join(PlannerInfo *root, SubLink *sublink, + bool under_not, Relids available_rels); extern JoinExpr *convert_EXISTS_sublink_to_join(PlannerInfo *root, SubLink *sublink, diff --git a/src/include/optimizer/tlist.h b/src/include/optimizer/tlist.h index 9c355bf11794c..60142e166662e 100644 --- a/src/include/optimizer/tlist.h +++ b/src/include/optimizer/tlist.h @@ -4,7 +4,7 @@ * prototypes for tlist.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/optimizer/tlist.h @@ -48,6 +48,11 @@ extern void apply_pathtarget_labeling_to_tlist(List *tlist, PathTarget *target); extern void split_pathtarget_at_srfs(PlannerInfo *root, PathTarget *target, PathTarget *input_target, List **targets, List **targets_contain_srfs); +extern void split_pathtarget_at_srfs_grouping(PlannerInfo *root, + PathTarget *target, + PathTarget *input_target, + List **targets, + List **targets_contain_srfs); /* Convenience macro to get a PathTarget with valid cost/width fields */ #define create_pathtarget(root, tlist) \ diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h index f29ed03b476ec..9da833e40e5ae 100644 --- a/src/include/parser/analyze.h +++ b/src/include/parser/analyze.h @@ -4,7 +4,7 @@ * parse analysis for optimizable statements * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/analyze.h @@ -21,7 +21,7 @@ /* Hook for plugins to get control at end of parse analysis */ typedef void (*post_parse_analyze_hook_type) (ParseState *pstate, Query *query, - JumbleState *jstate); + const JumbleState *jstate); extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook; @@ -43,7 +43,8 @@ extern List *transformInsertRow(ParseState *pstate, List *exprlist, List *stmtcols, List *icolumns, List *attrnos, bool strip_indirection); extern List *transformUpdateTargetList(ParseState *pstate, - List *origTlist); + List *origTlist, + ForPortionOfExpr *forPortionOf); extern void transformReturningClause(ParseState *pstate, Query *qry, ReturningClause *returningClause, ParseExprKind exprKind); @@ -64,5 +65,8 @@ extern List *BuildOnConflictExcludedTargetlist(Relation targetrel, Index exclRelIndex); extern SortGroupClause *makeSortGroupClauseForSetOp(Oid rescoltype, bool require_hash); +extern void constructSetOpTargetlist(ParseState *pstate, SetOperationStmt *op, + const List *ltargetlist, const List *rtargetlist, + List **targetlist, const char *context, bool recursive); #endif /* ANALYZE_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a4af3f717a111..51ead54f01534 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -7,7 +7,7 @@ * by the PG_KEYWORD macro, which is not defined in this file; it can * be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -136,6 +136,7 @@ PG_KEYWORD("delimiters", DELIMITERS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("depends", DEPENDS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("depth", DEPTH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("desc", DESC, RESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("destination", DESTINATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("detach", DETACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("dictionary", DICTIONARY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("disable", DISABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) @@ -147,6 +148,7 @@ PG_KEYWORD("domain", DOMAIN_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("edge", EDGE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("empty", EMPTY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) @@ -191,6 +193,8 @@ PG_KEYWORD("generated", GENERATED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("global", GLOBAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("grant", GRANT, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("granted", GRANTED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("graph", GRAPH, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("graph_table", GRAPH_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("greatest", GREATEST, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("group", GROUP_P, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("grouping", GROUPING, COL_NAME_KEYWORD, BARE_LABEL) @@ -202,6 +206,7 @@ PG_KEYWORD("hold", HOLD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("hour", HOUR_P, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("identity", IDENTITY_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("if", IF_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("ignore", IGNORE_P, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("ilike", ILIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("immediate", IMMEDIATE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("immutable", IMMUTABLE, UNRESERVED_KEYWORD, BARE_LABEL) @@ -269,6 +274,7 @@ PG_KEYWORD("location", LOCATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("lsn", LSN_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL) @@ -295,6 +301,7 @@ PG_KEYWORD("nfd", NFD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfkc", NFKC, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfkd", NFKD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("no", NO, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("node", NODE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("none", NONE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("normalize", NORMALIZE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("normalized", NORMALIZED, UNRESERVED_KEYWORD, BARE_LABEL) @@ -337,6 +344,7 @@ PG_KEYWORD("parameter", PARAMETER, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) @@ -345,6 +353,7 @@ PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL) @@ -358,6 +367,8 @@ PG_KEYWORD("procedural", PROCEDURAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("properties", PROPERTIES, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("property", PROPERTY, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("quotes", QUOTES, UNRESERVED_KEYWORD, BARE_LABEL) @@ -371,13 +382,16 @@ PG_KEYWORD("references", REFERENCES, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("referencing", REFERENCING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("refresh", REFRESH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("relationship", RELATIONSHIP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("rename", RENAME, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("repack", REPACK, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("repeatable", REPEATABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("replace", REPLACE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("replica", REPLICA, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("reset", RESET, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("respect", RESPECT_P, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("restart", RESTART, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("restrict", RESTRICT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("return", RETURN, UNRESERVED_KEYWORD, BARE_LABEL) @@ -420,6 +434,7 @@ PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("snapshot", SNAPSHOT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("some", SOME, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("source", SOURCE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("split", SPLIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("sql", SQL_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("stable", STABLE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("standalone", STANDALONE_P, UNRESERVED_KEYWORD, BARE_LABEL) @@ -490,10 +505,12 @@ PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("vertex", VERTEX, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("virtual", VIRTUAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("wait", WAIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("when", WHEN, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("where", WHERE, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("whitespace", WHITESPACE_P, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_agg.h b/src/include/parser/parse_agg.h index 277d45baf83c6..8a5653798dbcd 100644 --- a/src/include/parser/parse_agg.h +++ b/src/include/parser/parse_agg.h @@ -3,7 +3,7 @@ * parse_agg.h * handle aggregates and window functions in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_agg.h diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 3e9894926de13..fe23461100755 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -4,7 +4,7 @@ * handle clauses in parser * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_clause.h @@ -26,6 +26,7 @@ extern Node *transformLimitClause(ParseState *pstate, Node *clause, ParseExprKind exprKind, const char *constructName, LimitOption limitOption); extern List *transformGroupClause(ParseState *pstate, List *grouplist, + bool groupByAll, List **groupingSets, List **targetlist, List *sortClause, ParseExprKind exprKind, bool useSQL99); diff --git a/src/include/parser/parse_coerce.h b/src/include/parser/parse_coerce.h index 8d775c72c5993..aabacd49b6543 100644 --- a/src/include/parser/parse_coerce.h +++ b/src/include/parser/parse_coerce.h @@ -4,7 +4,7 @@ * Routines for type coercion. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_coerce.h diff --git a/src/include/parser/parse_collate.h b/src/include/parser/parse_collate.h index 1be6750b685ae..90fab90160e6a 100644 --- a/src/include/parser/parse_collate.h +++ b/src/include/parser/parse_collate.h @@ -4,7 +4,7 @@ * Routines for assigning collation information. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_collate.h diff --git a/src/include/parser/parse_cte.h b/src/include/parser/parse_cte.h index 28ed837584973..3deb83b36cd63 100644 --- a/src/include/parser/parse_cte.h +++ b/src/include/parser/parse_cte.h @@ -4,7 +4,7 @@ * handle CTEs (common table expressions) in parser * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_cte.h diff --git a/src/include/parser/parse_enr.h b/src/include/parser/parse_enr.h index 3fe2bd0a026f8..5cb6959306c34 100644 --- a/src/include/parser/parse_enr.h +++ b/src/include/parser/parse_enr.h @@ -4,7 +4,7 @@ * Internal definitions for parser * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_enr.h diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h index efbaff8e71045..e3de50c0d261b 100644 --- a/src/include/parser/parse_expr.h +++ b/src/include/parser/parse_expr.h @@ -3,7 +3,7 @@ * parse_expr.h * handle expressions in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_expr.h diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h index a6f24b83d8436..cbff17abee0f4 100644 --- a/src/include/parser/parse_func.h +++ b/src/include/parser/parse_func.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_func.h @@ -40,6 +40,7 @@ extern FuncDetailCode func_get_detail(List *funcname, int nargs, Oid *argtypes, bool expand_variadic, bool expand_defaults, bool include_out_arguments, + int *fgc_flags, Oid *funcid, Oid *rettype, bool *retset, int *nvargs, Oid *vatype, Oid **true_typeids, List **argdefaults); diff --git a/src/include/parser/parse_graphtable.h b/src/include/parser/parse_graphtable.h new file mode 100644 index 0000000000000..e52e21512aa77 --- /dev/null +++ b/src/include/parser/parse_graphtable.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * parse_graphtable.h + * parsing of GRAPH_TABLE + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/parser/parse_graphtable.h + * + *------------------------------------------------------------------------- + */ +#ifndef PARSE_GRAPHTABLE_H +#define PARSE_GRAPHTABLE_H + +#include "nodes/pg_list.h" +#include "parser/parse_node.h" + +extern Node *transformGraphTablePropertyRef(ParseState *pstate, ColumnRef *cref); + +extern Node *transformGraphPattern(ParseState *pstate, GraphPattern *graph_pattern); + +#endif /* PARSE_GRAPHTABLE_H */ diff --git a/src/include/parser/parse_merge.h b/src/include/parser/parse_merge.h index 2e0cf0c9ae07e..364e5da97daa5 100644 --- a/src/include/parser/parse_merge.h +++ b/src/include/parser/parse_merge.h @@ -4,7 +4,7 @@ * handle MERGE statement in parser * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_merge.h diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index 994284019fbb9..f7f4ba6c2a8b8 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -4,7 +4,7 @@ * Internal definitions for parser * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_node.h @@ -56,6 +56,7 @@ typedef enum ParseExprKind EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */ EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */ EXPR_KIND_MERGE_WHEN, /* MERGE WHEN [NOT] MATCHED condition */ + EXPR_KIND_FOR_PORTION, /* UPDATE/DELETE FOR PORTION OF item */ EXPR_KIND_GROUP_BY, /* GROUP BY */ EXPR_KIND_ORDER_BY, /* ORDER BY */ EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */ @@ -82,6 +83,7 @@ typedef enum ParseExprKind EXPR_KIND_COPY_WHERE, /* WHERE condition in COPY FROM */ EXPR_KIND_GENERATED_COLUMN, /* generation expression for a column */ EXPR_KIND_CYCLE_MARK, /* cycle mark value */ + EXPR_KIND_PROPGRAPH_PROPERTY, /* derived property expression */ } ParseExprKind; @@ -95,6 +97,24 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, Oid targetTypeId, int32 targetTypeMod, int location); +/* + * Namespace for the GRAPH_TABLE reference being transformed. + * + * Labels, properties and variables used in the GRAPH_TABLE form the namespace. + * The names of the labels and properties used in GRAPH_TABLE are looked up using + * the OID of the property graph. Variables are collected in a list as graph + * patterns are transformed. This namespace is used to resolve label and property + * references in the GRAPH_TABLE. + */ +typedef struct GraphTableParseState +{ + Oid graphid; /* OID of the graph being referenced */ + List *variables; /* list of element pattern variables in + * GRAPH_TABLE */ + GraphElementPattern *cur_gep; /* The element pattern being transformed. + * NULL if no element pattern is being + * transformed. */ +} GraphTableParseState; /* * State information used during parse analysis @@ -108,20 +128,6 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, * byte-wise locations in parse structures to character-wise cursor * positions.) * - * p_stmt_location: location of the top level RawStmt's start. During - * transformation, the Query's location will be set to the statement's - * location if available. Otherwise, the RawStmt's start location will - * be used. Propagating the location through ParseState is needed for - * the Query length calculation (see p_stmt_len below). - * - * p_stmt_len: length of the top level RawStmt. Most of the time, the - * statement's length is not provided by the parser, with the exception - * of SelectStmt within parentheses and PreparableStmt in COPY. If the - * statement's location is provided by the parser, the top-level location - * and length are needed to accurately compute the Query's length. If the - * statement's location is not provided, the RawStmt's length can be used - * directly. - * * p_rtable: list of RTEs that will become the rangetable of the query. * Note that neither relname nor refname of these entries are necessarily * unique; searching the rtable by name is a bad idea. @@ -167,10 +173,6 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, * * p_grouping_nsitem: the ParseNamespaceItem that represents the grouping step. * - * p_is_insert: true to process assignment expressions like INSERT, false - * to process them like UPDATE. (Note this can change intra-statement, for - * cases like INSERT ON CONFLICT UPDATE.) - * * p_windowdefs: list of WindowDefs representing WINDOW and OVER clauses. * We collect these while transforming expressions and then transform them * afterwards (so that any resjunk tlist items needed for the sort/group @@ -192,6 +194,9 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, * p_resolve_unknowns: resolve unknown-type SELECT output columns as type TEXT * (this is true by default). * + * p_graph_table_pstate: Namespace for the GRAPH_TABLE reference being + * transformed, if any. + * * p_hasAggs, p_hasWindowFuncs, etc: true if we've found any of the indicated * constructs in the query. * @@ -207,8 +212,6 @@ struct ParseState { ParseState *parentParseState; /* stack link */ const char *p_sourcetext; /* source text, or NULL if not available */ - ParseLoc p_stmt_location; /* start location, or -1 if unknown */ - ParseLoc p_stmt_len; /* length in bytes; 0 means "rest of string" */ List *p_rtable; /* range table so far */ List *p_rteperminfos; /* list of RTEPermissionInfo nodes for each * RTE_RELATION entry in rtable */ @@ -225,7 +228,6 @@ struct ParseState Relation p_target_relation; /* INSERT/UPDATE/DELETE/MERGE target rel */ ParseNamespaceItem *p_target_nsitem; /* target rel's NSItem, or NULL */ ParseNamespaceItem *p_grouping_nsitem; /* NSItem for grouping, or NULL */ - bool p_is_insert; /* process assignment like INSERT not UPDATE */ List *p_windowdefs; /* raw representations of window clauses */ ParseExprKind p_expr_kind; /* what kind of expression we're parsing */ int p_next_resno; /* next targetlist resno to assign */ @@ -237,6 +239,8 @@ struct ParseState * type text */ QueryEnvironment *p_queryEnv; /* curr env, incl refs to enclosing env */ + GraphTableParseState *p_graph_table_pstate; /* Current graph table + * namespace, if any */ /* Flags telling about things found in the query: */ bool p_hasAggs; diff --git a/src/include/parser/parse_oper.h b/src/include/parser/parse_oper.h index 0f27609f8ffaf..97f6aa139cd2f 100644 --- a/src/include/parser/parse_oper.h +++ b/src/include/parser/parse_oper.h @@ -4,7 +4,7 @@ * handle operator things for parser * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_oper.h diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h index 821f89606c35f..3eba06d7b11eb 100644 --- a/src/include/parser/parse_param.h +++ b/src/include/parser/parse_param.h @@ -3,7 +3,7 @@ * parse_param.h * handle parameters in parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_param.h diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h index d59599cf2424e..59721a70efbdd 100644 --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -4,7 +4,7 @@ * prototypes for parse_relation.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_relation.h @@ -15,6 +15,7 @@ #define PARSE_RELATION_H #include "parser/parse_node.h" +#include "storage/lockdefs.h" extern ParseNamespaceItem *refnameNamespaceItem(ParseState *pstate, @@ -44,7 +45,7 @@ extern Node *colNameToVar(ParseState *pstate, const char *colname, bool localonl extern void markNullableIfNeeded(ParseState *pstate, Var *var); extern void markVarForSelectPriv(ParseState *pstate, Var *var); extern Relation parserOpenTable(ParseState *pstate, const RangeVar *relation, - int lockmode); + LOCKMODE lockmode); extern ParseNamespaceItem *addRangeTableEntry(ParseState *pstate, RangeVar *relation, Alias *alias, @@ -52,7 +53,7 @@ extern ParseNamespaceItem *addRangeTableEntry(ParseState *pstate, bool inFromCl); extern ParseNamespaceItem *addRangeTableEntryForRelation(ParseState *pstate, Relation rel, - int lockmode, + LOCKMODE lockmode, Alias *alias, bool inh, bool inFromCl); @@ -81,6 +82,14 @@ extern ParseNamespaceItem *addRangeTableEntryForTableFunc(ParseState *pstate, Alias *alias, bool lateral, bool inFromCl); +extern ParseNamespaceItem *addRangeTableEntryForGraphTable(ParseState *pstate, + Oid graphid, + GraphPattern *graph_pattern, + List *columns, + List *colnames, + Alias *alias, + bool lateral, + bool inFromCl); extern ParseNamespaceItem *addRangeTableEntryForJoin(ParseState *pstate, List *colnames, ParseNamespaceColumn *nscolumns, @@ -127,6 +136,5 @@ extern int attnameAttNum(Relation rd, const char *attname, bool sysColOK); extern const NameData *attnumAttName(Relation rd, int attid); extern Oid attnumTypeId(Relation rd, int attid); extern Oid attnumCollationId(Relation rd, int attid); -extern bool isQueryUsingTempRelation(Query *query); #endif /* PARSE_RELATION_H */ diff --git a/src/include/parser/parse_target.h b/src/include/parser/parse_target.h index 89357f31ad623..3f18c7cd112c9 100644 --- a/src/include/parser/parse_target.h +++ b/src/include/parser/parse_target.h @@ -4,7 +4,7 @@ * handle target lists * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_target.h diff --git a/src/include/parser/parse_type.h b/src/include/parser/parse_type.h index 0d919d8bfa22c..a335807b0b040 100644 --- a/src/include/parser/parse_type.h +++ b/src/include/parser/parse_type.h @@ -3,7 +3,7 @@ * parse_type.h * handle type operations for parser * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_type.h diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h index 9f2b58de79741..34c98e5122f4b 100644 --- a/src/include/parser/parse_utilcmd.h +++ b/src/include/parser/parse_utilcmd.h @@ -4,7 +4,7 @@ * parse analysis for utility commands * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parse_utilcmd.h @@ -16,7 +16,7 @@ #include "parser/parse_node.h" -struct AttrMap; /* avoid including attmap.h here */ +typedef struct AttrMap AttrMap; /* avoid including attmap.h here */ extern List *transformCreateStmt(CreateStmt *stmt, const char *queryString); @@ -30,7 +30,8 @@ extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt, const char *queryString); extern void transformRuleStmt(RuleStmt *stmt, const char *queryString, List **actions, Node **whereClause); -extern List *transformCreateSchemaStmtElements(List *schemaElts, +extern List *transformCreateSchemaStmtElements(ParseState *pstate, + List *schemaElts, const char *schemaName); extern PartitionBoundSpec *transformPartitionBound(ParseState *pstate, Relation parent, PartitionBoundSpec *spec); @@ -38,7 +39,7 @@ extern List *expandTableLikeClause(RangeVar *heapRel, TableLikeClause *table_like_clause); extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, - const struct AttrMap *attmap, + const AttrMap *attmap, Oid *constraintOid); #endif /* PARSE_UTILCMD_H */ diff --git a/src/include/parser/parser.h b/src/include/parser/parser.h index 350196cd641e7..93bd7c439f864 100644 --- a/src/include/parser/parser.h +++ b/src/include/parser/parser.h @@ -5,7 +5,7 @@ * * This is the external API for the raw lexing/parsing functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parser.h @@ -52,10 +52,8 @@ typedef enum BACKSLASH_QUOTE_SAFE_ENCODING, } BackslashQuoteType; -/* GUC variables in scan.l (every one of these is a bad idea :-() */ +/* GUC variable in scan.l */ extern PGDLLIMPORT int backslash_quote; -extern PGDLLIMPORT bool escape_string_warning; -extern PGDLLIMPORT bool standard_conforming_strings; /* Primary entry point for the raw parsing functions */ diff --git a/src/include/parser/parsetree.h b/src/include/parser/parsetree.h index 17f5c9ce11468..f6ce9746a213f 100644 --- a/src/include/parser/parsetree.h +++ b/src/include/parser/parsetree.h @@ -5,7 +5,7 @@ * parse trees. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/parsetree.h diff --git a/src/include/parser/scanner.h b/src/include/parser/scanner.h index 8d202d5b2848a..8c2de6556883c 100644 --- a/src/include/parser/scanner.h +++ b/src/include/parser/scanner.h @@ -8,7 +8,7 @@ * higher-level API provided by parser.h. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/scanner.h @@ -85,8 +85,6 @@ typedef struct core_yy_extra_type * prevailing GUC settings. */ int backslash_quote; - bool escape_string_warning; - bool standard_conforming_strings; /* * literalbuf is used to accumulate literal values when multiple rules are @@ -110,8 +108,7 @@ typedef struct core_yy_extra_type /* first part of UTF16 surrogate pair for Unicode escapes */ int32 utf16_first_part; - /* state variables for literal-lexing warnings */ - bool warn_on_first_escape; + /* true if we need to verify valid encoding of current literal string */ bool saw_non_ascii; } core_yy_extra_type; diff --git a/src/include/parser/scansup.h b/src/include/parser/scansup.h index 8f3a9f4c527bd..413c56bc729e5 100644 --- a/src/include/parser/scansup.h +++ b/src/include/parser/scansup.h @@ -3,7 +3,7 @@ * scansup.h * scanner support routines used by the core lexer * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/parser/scansup.h diff --git a/src/include/partitioning/partbounds.h b/src/include/partitioning/partbounds.h index 65f161f7188c4..25c926f4c8b7e 100644 --- a/src/include/partitioning/partbounds.h +++ b/src/include/partitioning/partbounds.h @@ -2,7 +2,7 @@ * * partbounds.h * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/include/partitioning/partbounds.h * @@ -15,7 +15,7 @@ #include "parser/parse_node.h" #include "partitioning/partdefs.h" -struct RelOptInfo; /* avoid including pathnodes.h here */ +typedef struct RelOptInfo RelOptInfo; /* avoid including pathnodes.h here */ /* @@ -114,8 +114,8 @@ extern PartitionBoundInfo partition_bounds_copy(PartitionBoundInfo src, extern PartitionBoundInfo partition_bounds_merge(int partnatts, FmgrInfo *partsupfunc, Oid *partcollation, - struct RelOptInfo *outer_rel, - struct RelOptInfo *inner_rel, + RelOptInfo *outer_rel, + RelOptInfo *inner_rel, JoinType jointype, List **outer_parts, List **inner_parts); @@ -130,8 +130,8 @@ extern void check_default_partition_contents(Relation parent, extern int32 partition_rbound_datum_cmp(FmgrInfo *partsupfunc, Oid *partcollation, - Datum *rb_datums, PartitionRangeDatumKind *rb_kind, - Datum *tuple_datums, int n_tuple_datums); + const Datum *rb_datums, PartitionRangeDatumKind *rb_kind, + const Datum *tuple_datums, int n_tuple_datums); extern int partition_list_bsearch(FmgrInfo *partsupfunc, Oid *partcollation, PartitionBoundInfo boundinfo, @@ -139,8 +139,18 @@ extern int partition_list_bsearch(FmgrInfo *partsupfunc, extern int partition_range_datum_bsearch(FmgrInfo *partsupfunc, Oid *partcollation, PartitionBoundInfo boundinfo, - int nvalues, Datum *values, bool *is_equal); + int nvalues, const Datum *values, bool *is_equal); extern int partition_hash_bsearch(PartitionBoundInfo boundinfo, int modulus, int remainder); +extern void check_partitions_for_split(Relation parent, + Oid splitPartOid, + List *partlist, + ParseState *pstate); +extern void calculate_partition_bound_for_merge(Relation parent, + List *partNames, + List *partOids, + PartitionBoundSpec *spec, + ParseState *pstate); + #endif /* PARTBOUNDS_H */ diff --git a/src/include/partitioning/partdefs.h b/src/include/partitioning/partdefs.h index 6deb24f1c0700..5987af6d92d48 100644 --- a/src/include/partitioning/partdefs.h +++ b/src/include/partitioning/partdefs.h @@ -3,7 +3,7 @@ * partdefs.h * Base definitions for partitioned table handling * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/include/partitioning/partdefs.h * diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h index 34533f7004c11..dd9c68692fe00 100644 --- a/src/include/partitioning/partdesc.h +++ b/src/include/partitioning/partdesc.h @@ -2,7 +2,7 @@ * * partdesc.h * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/partitioning/partdesc.h * diff --git a/src/include/partitioning/partprune.h b/src/include/partitioning/partprune.h index c413734789a97..49fcd355578db 100644 --- a/src/include/partitioning/partprune.h +++ b/src/include/partitioning/partprune.h @@ -4,7 +4,7 @@ * prototypes for partprune.c * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/partitioning/partprune.h @@ -17,8 +17,8 @@ #include "nodes/execnodes.h" #include "partitioning/partdefs.h" -struct PlannerInfo; /* avoid including pathnodes.h here */ -struct RelOptInfo; +typedef struct PlannerInfo PlannerInfo; /* avoid including pathnodes.h here */ +typedef struct RelOptInfo RelOptInfo; /* @@ -70,11 +70,11 @@ typedef struct PartitionPruneContext #define PruneCxtStateIdx(partnatts, step_id, keyno) \ ((partnatts) * (step_id) + (keyno)) -extern int make_partition_pruneinfo(struct PlannerInfo *root, - struct RelOptInfo *parentrel, +extern int make_partition_pruneinfo(PlannerInfo *root, + RelOptInfo *parentrel, List *subpaths, List *prunequal); -extern Bitmapset *prune_append_rel_partitions(struct RelOptInfo *rel); +extern Bitmapset *prune_append_rel_partitions(RelOptInfo *rel); extern Bitmapset *get_matching_partitions(PartitionPruneContext *context, List *pruning_steps); diff --git a/src/include/pch/meson.build b/src/include/pch/meson.build index f6babee6f6d88..f3811f09de5a7 100644 --- a/src/include/pch/meson.build +++ b/src/include/pch/meson.build @@ -1,6 +1,6 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # See https://github.com/mesonbuild/meson/issues/10338 -pch_c_h = meson.source_root() / meson.current_source_dir() / 'c_pch.h' -pch_postgres_h = meson.source_root() / meson.current_source_dir() / 'postgres_pch.h' -pch_postgres_fe_h = meson.source_root() / meson.current_source_dir() / 'postgres_fe_pch.h' +pch_c_h = meson.project_source_root() / meson.current_source_dir() / 'c_pch.h' +pch_postgres_h = meson.project_source_root() / meson.current_source_dir() / 'postgres_pch.h' +pch_postgres_fe_h = meson.project_source_root() / meson.current_source_dir() / 'postgres_fe_pch.h' diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 726a7c1be1f4d..4f8113c144b0c 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -12,9 +12,6 @@ /* The normal alignment of `int64_t', in bytes. */ #undef ALIGNOF_INT64_T -/* The normal alignment of `long', in bytes. */ -#undef ALIGNOF_LONG - /* The normal alignment of `PG_INT128_TYPE', in bytes. */ #undef ALIGNOF_PG_INT128_TYPE @@ -54,9 +51,6 @@ /* Define to 1 if you have the `append_history' function. */ #undef HAVE_APPEND_HISTORY -/* Define to 1 if you have the header file. */ -#undef HAVE_ATOMIC_H - /* Define to 1 if you have the `backtrace_symbols' function. */ #undef HAVE_BACKTRACE_SYMBOLS @@ -75,6 +69,14 @@ /* Define to 1 if you have the header file. */ #undef HAVE_CRTDEFS_H +/* Define to 1 if your C++ compiler understands `typeof' or something similar. + */ +#undef HAVE_CXX_TYPEOF + +/* Define to 1 if your C++ compiler understands `typeof_unqual' or something + similar. */ +#undef HAVE_CXX_TYPEOF_UNQUAL + /* Define to 1 if you have the declaration of `fdatasync', and to 0 if you don't. */ #undef HAVE_DECL_FDATASYNC @@ -83,14 +85,6 @@ don't. */ #undef HAVE_DECL_F_FULLFSYNC -/* Define to 1 if you have the declaration of - `LLVMCreateGDBRegistrationListener', and to 0 if you don't. */ -#undef HAVE_DECL_LLVMCREATEGDBREGISTRATIONLISTENER - -/* Define to 1 if you have the declaration of - `LLVMCreatePerfJITEventListener', and to 0 if you don't. */ -#undef HAVE_DECL_LLVMCREATEPERFJITEVENTLISTENER - /* Define to 1 if you have the declaration of `memset_s', and to 0 if you don't. */ #undef HAVE_DECL_MEMSET_S @@ -119,10 +113,6 @@ don't. */ #undef HAVE_DECL_STRLCPY -/* Define to 1 if you have the declaration of `strnlen', and to 0 if you - don't. */ -#undef HAVE_DECL_STRNLEN - /* Define to 1 if you have the declaration of `strsep', and to 0 if you don't. */ #undef HAVE_DECL_STRSEP @@ -146,6 +136,9 @@ /* Define to 1 if you have the `explicit_bzero' function. */ #undef HAVE_EXPLICIT_BZERO +/* Define to 1 if you have the `explicit_memset' function. */ +#undef HAVE_EXPLICIT_MEMSET + /* Define to 1 if fseeko (and presumably ftello) exists and is declared. */ #undef HAVE_FSEEKO @@ -229,6 +222,9 @@ /* Define to 1 if you have the global variable 'int timezone'. */ #undef HAVE_INT_TIMEZONE +/* Define to 1 if you have the `io_uring_queue_init_mem' function. */ +#undef HAVE_IO_URING_QUEUE_INIT_MEM + /* Define to 1 if __builtin_constant_p(x) implies "i"(x) acceptance. */ #undef HAVE_I_CONSTRAINT__BUILTIN_CONSTANT_P @@ -286,15 +282,15 @@ /* Define to 1 if you have the `localeconv_l' function. */ #undef HAVE_LOCALECONV_L -/* Define to 1 if you have the header file. */ -#undef HAVE_MBARRIER_H - /* Define to 1 if you have the `mbstowcs_l' function. */ #undef HAVE_MBSTOWCS_L /* Define to 1 if you have the header file. */ #undef HAVE_MEMORY_H +/* Define to 1 if you have the `memset_explicit' function. */ +#undef HAVE_MEMSET_EXPLICIT + /* Define to 1 if you have the `mkdtemp' function. */ #undef HAVE_MKDTEMP @@ -358,6 +354,9 @@ /* Define to 1 if you have the `rl_variable_bind' function. */ #undef HAVE_RL_VARIABLE_BIND +/* Define to 1 if you have SA_SIGINFO available. */ +#undef HAVE_SA_SIGINFO + /* Define to 1 if you have the header file. */ #undef HAVE_SECURITY_PAM_APPL_H @@ -376,6 +375,9 @@ /* Define to 1 if you have the `SSL_CTX_set_ciphersuites' function. */ #undef HAVE_SSL_CTX_SET_CIPHERSUITES +/* Define to 1 if you have the `SSL_CTX_set_client_hello_cb' function. */ +#undef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB + /* Define to 1 if you have the `SSL_CTX_set_keylog_callback' function. */ #undef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK @@ -403,9 +405,6 @@ /* Define to 1 if you have the `strlcpy' function. */ #undef HAVE_STRLCPY -/* Define to 1 if you have the `strnlen' function. */ -#undef HAVE_STRNLEN - /* Define to 1 if you have the `strsep' function. */ #undef HAVE_STRSEP @@ -469,6 +468,13 @@ /* Define to 1 if your compiler understands `typeof' or something similar. */ #undef HAVE_TYPEOF +/* Define to 1 if your compiler understands `typeof_unqual' or something + similar. */ +#undef HAVE_TYPEOF_UNQUAL + +/* Define to 1 if you have the header file. */ +#undef HAVE_UCHAR_H + /* Define to 1 if you have the header file. */ #undef HAVE_UCRED_H @@ -538,9 +544,6 @@ /* Define to 1 if your compiler understands __builtin_$op_overflow. */ #undef HAVE__BUILTIN_OP_OVERFLOW -/* Define to 1 if your compiler understands __builtin_popcount. */ -#undef HAVE__BUILTIN_POPCOUNT - /* Define to 1 if your compiler understands __builtin_types_compatible_p. */ #undef HAVE__BUILTIN_TYPES_COMPATIBLE_P @@ -559,9 +562,6 @@ /* Define to 1 if you have __get_cpuid_count. */ #undef HAVE__GET_CPUID_COUNT -/* Define to 1 if your compiler understands _Static_assert. */ -#undef HAVE__STATIC_ASSERT - /* Define as the maximum alignment requirement of any C data type. */ #undef MAXIMUM_ALIGNOF @@ -590,6 +590,14 @@ /* Define to the version of this package. */ #undef PACKAGE_VERSION +/* Define to best C++ printf format archetype, usually gnu_printf if + available. */ +#undef PG_CXX_PRINTF_ATTRIBUTE + +/* Define to best C printf format archetype, usually gnu_printf if available. + */ +#undef PG_C_PRINTF_ATTRIBUTE + /* Define to the name of a signed 128-bit integer type. */ #undef PG_INT128_TYPE @@ -606,9 +614,6 @@ /* PostgreSQL minor version number */ #undef PG_MINORVERSION_NUM -/* Define to best printf format archetype, usually gnu_printf if available. */ -#undef PG_PRINTF_ATTRIBUTE - /* PostgreSQL version as a string */ #undef PG_VERSION @@ -639,6 +644,9 @@ RELSEG_SIZE requires an initdb. */ #undef RELSEG_SIZE +/* The size of `intmax_t', as computed by sizeof. */ +#undef SIZEOF_INTMAX_T + /* The size of `long', as computed by sizeof. */ #undef SIZEOF_LONG @@ -669,6 +677,9 @@ /* Define to 1 to build with assertion checks. (--enable-cassert) */ #undef USE_ASSERT_CHECKING +/* Define to 1 to use AVX2 instructions with a runtime check. */ +#undef USE_AVX2_WITH_RUNTIME_CHECK + /* Define to 1 to use AVX-512 CRC algorithms with a runtime check. */ #undef USE_AVX512_CRC32C_WITH_RUNTIME_CHECK @@ -724,6 +735,9 @@ /* Define to 1 to build with PAM support. (--with-pam) */ #undef USE_PAM +/* Define to 1 to use Arm PMULL CRC algorithms with a runtime check. */ +#undef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK + /* Define to 1 to use software CRC-32C implementation (slicing-by-8). */ #undef USE_SLICING_BY_8_CRC32C @@ -787,11 +801,11 @@ /* Define for large files, on AIX-style hosts. */ #undef _LARGE_FILES -/* Define to `__inline__' or `__inline' if that's what the C compiler - calls it, or to nothing if 'inline' is not supported under any name. */ -#ifndef __cplusplus -#undef inline -#endif +/* Define to how the C++ compiler spells `typeof'. */ +#undef pg_cxx_typeof + +/* Define to how the C++ compiler spells `typeof_unqual'. */ +#undef pg_cxx_typeof_unqual /* Define to keyword to use for C99 restrict support, or to nothing if not supported */ @@ -813,3 +827,6 @@ /* Define to how the compiler spells `typeof'. */ #undef typeof + +/* Define to how the compiler spells `typeof_unqual'. */ +#undef typeof_unqual diff --git a/src/include/pg_config_manual.h b/src/include/pg_config_manual.h index 125d3eb5fff5e..521b49b88885c 100644 --- a/src/include/pg_config_manual.h +++ b/src/include/pg_config_manual.h @@ -6,7 +6,7 @@ * for developers. If you edit any of these, be sure to do a *full* * rebuild (and an initdb if noted). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/pg_config_manual.h @@ -19,6 +19,16 @@ */ #define DEFAULT_XLOG_SEG_SIZE (16*1024*1024) +/* + * SLRU segment size. A page is the same BLCKSZ as is used everywhere else in + * Postgres. The segment size can be chosen somewhat arbitrarily; we make it + * 32 pages by default, or 256Kb, i.e. 1M transactions for CLOG or 64K + * transactions for SUBTRANS. + * + * Changing this requires an initdb. + */ +#define SLRU_PAGES_PER_SEGMENT 32 + /* * Maximum length for identifiers (e.g. table names, column names, * function names). Names actually are limited to one fewer byte than this, @@ -74,17 +84,12 @@ #define PARTITION_MAX_KEYS 32 /* - * Decide whether built-in 8-byte types, including float8, int8, and - * timestamp, are passed by value. This is on by default if sizeof(Datum) >= - * 8 (that is, on 64-bit platforms). If sizeof(Datum) < 8 (32-bit platforms), - * this must be off. We keep this here as an option so that it is easy to - * test the pass-by-reference code paths on 64-bit platforms. - * - * Changing this requires an initdb. + * This symbol is now vestigial: built-in 8-byte types, including float8, + * int8, and timestamp, are always passed by value since we require Datum + * to be wide enough to permit that. We continue to define the symbol here + * so as not to unnecessarily break extension code. */ -#if SIZEOF_VOID_P >= 8 #define USE_FLOAT8_BYVAL 1 -#endif /* diff --git a/src/include/pg_getopt.h b/src/include/pg_getopt.h index aad0886c054d2..23bc3d8d04253 100644 --- a/src/include/pg_getopt.h +++ b/src/include/pg_getopt.h @@ -11,7 +11,7 @@ * Portions Copyright (c) 1987, 1993, 1994 * The Regents of the University of California. All rights reserved. * - * Portions Copyright (c) 2003-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2003-2026, PostgreSQL Global Development Group * * src/include/pg_getopt.h */ diff --git a/src/include/pg_trace.h b/src/include/pg_trace.h index c82f34726f74e..03c9503140072 100644 --- a/src/include/pg_trace.h +++ b/src/include/pg_trace.h @@ -3,7 +3,7 @@ * * Definitions for the PostgreSQL tracing framework * - * Copyright (c) 2006-2025, PostgreSQL Global Development Group + * Copyright (c) 2006-2026, PostgreSQL Global Development Group * * src/include/pg_trace.h * ---------- diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 378f2f2c2ba24..dfa2e8376382a 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -3,7 +3,7 @@ * * Definitions for the PostgreSQL cumulative statistics system. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/pgstat.h * ---------- @@ -15,11 +15,17 @@ #include "portability/instr_time.h" #include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */ #include "replication/conflict.h" +#include "storage/locktag.h" #include "utils/backend_progress.h" /* for backward compatibility */ /* IWYU pragma: export */ #include "utils/backend_status.h" /* for backward compatibility */ /* IWYU pragma: export */ #include "utils/pgstat_kind.h" -#include "utils/relcache.h" -#include "utils/wait_event.h" /* for backward compatibility */ /* IWYU pragma: export */ + + +/* avoid including access/transam.h */ +typedef struct FullTransactionId FullTransactionId; + +/* avoid including utils/relcache.h */ +typedef struct RelationData *Relation; /* ---------- @@ -107,7 +113,8 @@ typedef struct PgStat_FunctionCallUsage typedef struct PgStat_BackendSubEntry { PgStat_Counter apply_error_count; - PgStat_Counter sync_error_count; + PgStat_Counter sync_seq_error_count; + PgStat_Counter sync_table_error_count; PgStat_Counter conflict_count[CONFLICT_NUM_TYPES]; } PgStat_BackendSubEntry; @@ -211,7 +218,7 @@ typedef struct PgStat_TableXactStatus * ------------------------------------------------------------ */ -#define PGSTAT_FILE_FORMAT_ID 0x01A5BCB7 +#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBC typedef struct PgStat_ArchiverStats { @@ -339,6 +346,24 @@ typedef struct PgStat_IO PgStat_BktypeIO stats[BACKEND_NUM_TYPES]; } PgStat_IO; +typedef struct PgStat_LockEntry +{ + PgStat_Counter waits; + PgStat_Counter wait_time; /* time in milliseconds */ + PgStat_Counter fastpath_exceeded; +} PgStat_LockEntry; + +typedef struct PgStat_PendingLock +{ + PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1]; +} PgStat_PendingLock; + +typedef struct PgStat_Lock +{ + TimestampTz stat_reset_timestamp; + PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1]; +} PgStat_Lock; + typedef struct PgStat_StatDBEntry { PgStat_Counter xact_commit; @@ -383,6 +408,7 @@ typedef struct PgStat_StatFuncEntry PgStat_Counter total_time; /* times in microseconds */ PgStat_Counter self_time; + TimestampTz stat_reset_timestamp; } PgStat_StatFuncEntry; typedef struct PgStat_StatReplSlotEntry @@ -393,8 +419,11 @@ typedef struct PgStat_StatReplSlotEntry PgStat_Counter stream_txns; PgStat_Counter stream_count; PgStat_Counter stream_bytes; + PgStat_Counter mem_exceeded_count; PgStat_Counter total_txns; PgStat_Counter total_bytes; + PgStat_Counter slotsync_skip_count; + TimestampTz slotsync_last_skip; TimestampTz stat_reset_timestamp; } PgStat_StatReplSlotEntry; @@ -413,7 +442,8 @@ typedef struct PgStat_SLRUStats typedef struct PgStat_StatSubEntry { PgStat_Counter apply_error_count; - PgStat_Counter sync_error_count; + PgStat_Counter sync_seq_error_count; + PgStat_Counter sync_table_error_count; PgStat_Counter conflict_count[CONFLICT_NUM_TYPES]; TimestampTz stat_reset_timestamp; } PgStat_StatSubEntry; @@ -453,6 +483,8 @@ typedef struct PgStat_StatTabEntry PgStat_Counter total_autovacuum_time; PgStat_Counter total_analyze_time; PgStat_Counter total_autoanalyze_time; + + TimestampTz stat_reset_time; } PgStat_StatTabEntry; /* ------ @@ -468,6 +500,7 @@ typedef struct PgStat_WalCounters PgStat_Counter wal_records; PgStat_Counter wal_fpi; uint64 wal_bytes; + uint64 wal_fpi_bytes; PgStat_Counter wal_buffers_full; } PgStat_WalCounters; @@ -508,10 +541,6 @@ typedef struct PgStat_BackendPending * Functions in pgstat.c */ -/* functions called from postmaster */ -extern Size StatsShmemSize(void); -extern void StatsShmemInit(void); - /* Functions called during server startup / shutdown */ extern void pgstat_restore_stats(void); extern void pgstat_discard_stats(void); @@ -603,6 +632,15 @@ extern bool pgstat_tracks_io_op(BackendType bktype, IOObject io_object, IOContext io_context, IOOp io_op); +/* + * Functions in pgstat_lock.c + */ + +extern void pgstat_lock_flush(bool nowait); +extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type); +extern void pgstat_count_lock_waits(uint8 locktag_type, long msecs); +extern PgStat_Lock *pgstat_fetch_stat_lock(void); + /* * Functions in pgstat_database.c */ @@ -658,8 +696,8 @@ extern void pgstat_init_relation(Relation rel); extern void pgstat_assoc_relation(Relation rel); extern void pgstat_unlink_relation(Relation rel); -extern void pgstat_report_vacuum(Oid tableoid, bool shared, - PgStat_Counter livetuples, PgStat_Counter deadtuples, +extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples, + PgStat_Counter deadtuples, TimestampTz starttime); extern void pgstat_report_analyze(Relation rel, PgStat_Counter livetuples, PgStat_Counter deadtuples, @@ -718,14 +756,15 @@ extern void pgstat_count_heap_delete(Relation rel); extern void pgstat_count_truncate(Relation rel); extern void pgstat_update_heap_dead_tuples(Relation rel, int delta); -extern void pgstat_twophase_postcommit(TransactionId xid, uint16 info, +extern void pgstat_twophase_postcommit(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); -extern void pgstat_twophase_postabort(TransactionId xid, uint16 info, +extern void pgstat_twophase_postabort(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry(Oid relid); extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry_ext(bool shared, - Oid reloid); + Oid reloid, + bool *may_free); extern PgStat_TableStatus *find_tabstat_entry(Oid rel_id); @@ -736,6 +775,7 @@ extern PgStat_TableStatus *find_tabstat_entry(Oid rel_id); extern void pgstat_reset_replslot(const char *name); struct ReplicationSlot; extern void pgstat_report_replslot(struct ReplicationSlot *slot, const PgStat_StatReplSlotEntry *repSlotStat); +extern void pgstat_report_replslotsync(struct ReplicationSlot *slot); extern void pgstat_create_replslot(struct ReplicationSlot *slot); extern void pgstat_acquire_replslot(struct ReplicationSlot *slot); extern void pgstat_drop_replslot(struct ReplicationSlot *slot); @@ -747,11 +787,11 @@ extern PgStat_StatReplSlotEntry *pgstat_fetch_replslot(NameData slotname); */ extern void pgstat_reset_slru(const char *); -extern void pgstat_count_slru_page_zeroed(int slru_idx); -extern void pgstat_count_slru_page_hit(int slru_idx); -extern void pgstat_count_slru_page_read(int slru_idx); -extern void pgstat_count_slru_page_written(int slru_idx); -extern void pgstat_count_slru_page_exists(int slru_idx); +extern void pgstat_count_slru_blocks_zeroed(int slru_idx); +extern void pgstat_count_slru_blocks_hit(int slru_idx); +extern void pgstat_count_slru_blocks_read(int slru_idx); +extern void pgstat_count_slru_blocks_written(int slru_idx); +extern void pgstat_count_slru_blocks_exists(int slru_idx); extern void pgstat_count_slru_flush(int slru_idx); extern void pgstat_count_slru_truncate(int slru_idx); extern const char *pgstat_get_slru_name(int slru_idx); @@ -763,7 +803,7 @@ extern PgStat_SLRUStats *pgstat_fetch_slru(void); * Functions in pgstat_subscription.c */ -extern void pgstat_report_subscription_error(Oid subid, bool is_apply_error); +extern void pgstat_report_subscription_error(Oid subid); extern void pgstat_report_subscription_conflict(Oid subid, ConflictType type); extern void pgstat_create_subscription(Oid subid); extern void pgstat_drop_subscription(Oid subid); diff --git a/src/include/pgtar.h b/src/include/pgtar.h index b2677578a3d1d..323ba051f8b20 100644 --- a/src/include/pgtar.h +++ b/src/include/pgtar.h @@ -4,7 +4,7 @@ * Functions for manipulating tarfile datastructures (src/port/tar.c) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/pgtar.h @@ -55,11 +55,15 @@ enum tarHeaderOffset /* last 12 bytes of the 512-byte block are unassigned */ }; +/* See POSIX (not all the standard file type codes are listed here) */ enum tarFileType { TAR_FILETYPE_PLAIN = '0', + TAR_FILETYPE_PLAIN_OLD = '\0', /* backwards compatibility, per POSIX */ TAR_FILETYPE_SYMLINK = '2', TAR_FILETYPE_DIRECTORY = '5', + TAR_FILETYPE_PAX_EXTENDED = 'x', + TAR_FILETYPE_PAX_EXTENDED_GLOBAL = 'g', }; extern enum tarError tarCreateHeader(char *h, const char *filename, @@ -68,7 +72,8 @@ extern enum tarError tarCreateHeader(char *h, const char *filename, time_t mtime); extern uint64 read_tar_number(const char *s, int len); extern void print_tar_number(char *s, int len, uint64 val); -extern int tarChecksum(char *header); +extern int tarChecksum(const char *header); +extern bool isValidTarHeader(const char *header); /* * Compute the number of padding bytes required for an entry in a tar diff --git a/src/include/pgtime.h b/src/include/pgtime.h index 5fc9f223de30a..ba6705f71bd83 100644 --- a/src/include/pgtime.h +++ b/src/include/pgtime.h @@ -3,7 +3,7 @@ * pgtime.h * PostgreSQL internal timezone library * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/pgtime.h diff --git a/src/include/port.h b/src/include/port.h index 3964d3b12936e..c029878c6beeb 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -3,7 +3,7 @@ * port.h * Header for src/port/ compatibility functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port.h @@ -169,8 +169,29 @@ extern int pg_strcasecmp(const char *s1, const char *s2); extern int pg_strncasecmp(const char *s1, const char *s2, size_t n); extern unsigned char pg_toupper(unsigned char ch); extern unsigned char pg_tolower(unsigned char ch); -extern unsigned char pg_ascii_toupper(unsigned char ch); -extern unsigned char pg_ascii_tolower(unsigned char ch); + +/* + * Fold a character to upper case, following C/POSIX locale rules. + */ +static inline unsigned char +pg_ascii_toupper(unsigned char ch) +{ + if (ch >= 'a' && ch <= 'z') + ch += 'A' - 'a'; + return ch; +} + +/* + * Fold a character to lower case, following C/POSIX locale rules. + */ +static inline unsigned char +pg_ascii_tolower(unsigned char ch) +{ + if (ch >= 'A' && ch <= 'Z') + ch += 'a' - 'A'; + return ch; +} + /* * Beginning in v12, we always replace snprintf() and friends with our own @@ -338,7 +359,6 @@ extern bool rmtree(const char *path, bool rmtopdir); * open() and fopen() replacements to allow deletion of open files and * passing of other special options. */ -#define O_DIRECT 0x80000000 extern HANDLE pgwin32_open_handle(const char *, int, bool); extern int pgwin32_open(const char *, int,...); extern FILE *pgwin32_fopen(const char *, const char *); @@ -398,7 +418,7 @@ extern FILE *pgwin32_popen(const char *command, const char *type); /* Type to use with fseeko/ftello */ #ifndef WIN32 /* WIN32 is handled in port/win32_port.h */ -#define pgoff_t off_t +typedef off_t pgoff_t; #endif #ifndef HAVE_GETPEEREID @@ -456,16 +476,12 @@ extern size_t strlcat(char *dst, const char *src, size_t siz); extern size_t strlcpy(char *dst, const char *src, size_t siz); #endif -#if !HAVE_DECL_STRNLEN -extern size_t strnlen(const char *str, size_t maxlen); -#endif - #if !HAVE_DECL_STRSEP extern char *strsep(char **stringp, const char *delim); #endif #if !HAVE_DECL_TIMINGSAFE_BCMP -extern int timingsafe_bcmp(const void *b1, const void *b2, size_t len); +extern int timingsafe_bcmp(const void *b1, const void *b2, size_t n); #endif /* @@ -530,6 +546,9 @@ extern int pg_mkdir_p(char *path, int omode); #else #define pqsignal pqsignal_be #endif + +#define PG_SIG_DFL (pqsigfunc) (pg_funcptr_t) SIG_DFL +#define PG_SIG_IGN (pqsigfunc) (pg_funcptr_t) SIG_IGN typedef void (*pqsigfunc) (SIGNAL_ARGS); extern void pqsignal(int signo, pqsigfunc func); diff --git a/src/include/port/aix.h b/src/include/port/aix.h new file mode 100644 index 0000000000000..211b048cfbf2b --- /dev/null +++ b/src/include/port/aix.h @@ -0,0 +1,22 @@ +/* + * src/include/port/aix.h + */ +#include /* for size_t */ +#include /* for uid_t and gid_t */ +#include /* for wchar_t and locale_t */ + +/* AIX has getpeereid(), but fails to declare it as of 7.3 */ +extern int getpeereid(int socket, uid_t *euid, gid_t *egid); + +/* AIX has wcstombs_l(), but fails to declare it as of 7.3 */ +extern size_t wcstombs_l(char *dest, const wchar_t *src, size_t n, + locale_t loc); + +/* + * AIX doesn't seem to have ever modernized pam_appl.h to include + * "const" in the declaration of PAM conversation procs. We can avoid + * a compile error by setting _PAM_LEGACY_NONCONST; that doesn't do + * anything in AIX's system headers, but it makes us omit the "const" + * in our own code. (Compare solaris.h.) + */ +#define _PAM_LEGACY_NONCONST 1 diff --git a/src/include/port/atomics.h b/src/include/port/atomics.h index 074136fe4c4a8..d8b1d20fe60fa 100644 --- a/src/include/port/atomics.h +++ b/src/include/port/atomics.h @@ -28,7 +28,7 @@ * For an introduction to using memory barriers within the PostgreSQL backend, * see src/backend/storage/lmgr/README.barrier * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/atomics.h @@ -88,8 +88,6 @@ #include "port/atomics/generic-gcc.h" #elif defined(_MSC_VER) #include "port/atomics/generic-msvc.h" -#elif defined(__SUNPRO_C) && !defined(__GNUC__) -#include "port/atomics/generic-sunpro.h" #else /* Unknown compiler. */ #endif @@ -283,11 +281,13 @@ pg_atomic_write_u32(volatile pg_atomic_uint32 *ptr, uint32 val) /* * pg_atomic_unlocked_write_u32 - unlocked write to atomic variable. * - * The write is guaranteed to succeed as a whole, i.e. it's not possible to - * observe a partial write for any reader. But note that writing this way is - * not guaranteed to correctly interact with read-modify-write operations like - * pg_atomic_compare_exchange_u32. This should only be used in cases where - * minor performance regressions due to atomics emulation are unacceptable. + * Write to an atomic variable, without atomicity guarantees. I.e. it is not + * guaranteed that a concurrent reader will not see a torn value, nor is this + * guaranteed to correctly interact with concurrent read-modify-write + * operations like pg_atomic_compare_exchange_u32. This should only be used + * in cases where minor performance regressions due to atomic operations are + * unacceptable and where exclusive access is guaranteed via some external + * means. * * No barrier semantics. */ @@ -490,6 +490,16 @@ pg_atomic_write_u64(volatile pg_atomic_uint64 *ptr, uint64 val) pg_atomic_write_u64_impl(ptr, val); } +static inline void +pg_atomic_unlocked_write_u64(volatile pg_atomic_uint64 *ptr, uint64 val) +{ +#ifndef PG_HAVE_ATOMIC_U64_SIMULATION + AssertPointerAlignment(ptr, 8); +#endif + + pg_atomic_unlocked_write_u64_impl(ptr, val); +} + static inline void pg_atomic_write_membarrier_u64(volatile pg_atomic_uint64 *ptr, uint64 val) { diff --git a/src/include/port/atomics/arch-arm.h b/src/include/port/atomics/arch-arm.h index eb7e0ac93bacf..90280c7b751a9 100644 --- a/src/include/port/atomics/arch-arm.h +++ b/src/include/port/atomics/arch-arm.h @@ -3,7 +3,7 @@ * arch-arm.h * Atomic operations considerations specific to ARM * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * * NOTES: * diff --git a/src/include/port/atomics/arch-ppc.h b/src/include/port/atomics/arch-ppc.h index b93f6766d299f..65ced2c6d3a23 100644 --- a/src/include/port/atomics/arch-ppc.h +++ b/src/include/port/atomics/arch-ppc.h @@ -3,7 +3,7 @@ * arch-ppc.h * Atomic operations considerations specific to PowerPC * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES: @@ -36,7 +36,7 @@ typedef struct pg_atomic_uint32 #define PG_HAVE_ATOMIC_U64_SUPPORT typedef struct pg_atomic_uint64 { - volatile uint64 value pg_attribute_aligned(8); + alignas(8) volatile uint64 value; } pg_atomic_uint64; #endif diff --git a/src/include/port/atomics/arch-x86.h b/src/include/port/atomics/arch-x86.h index 8983dd89d53d7..bd6f4f56ca2cb 100644 --- a/src/include/port/atomics/arch-x86.h +++ b/src/include/port/atomics/arch-x86.h @@ -7,7 +7,7 @@ * support for xadd and cmpxchg. Given that the 386 isn't supported anywhere * anymore that's not much of a restriction luckily. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES: @@ -104,7 +104,7 @@ typedef struct pg_atomic_uint64 */ #if defined(__GNUC__) || defined(__INTEL_COMPILER) #define PG_HAVE_SPIN_DELAY -static __inline__ void +static inline void pg_spin_delay_impl(void) { __asm__ __volatile__(" rep; nop \n"); @@ -241,6 +241,6 @@ pg_atomic_fetch_add_u64_impl(volatile pg_atomic_uint64 *ptr, int64 add_) */ #if defined(__i568__) || defined(__i668__) || /* gcc i586+ */ \ (defined(_M_IX86) && _M_IX86 >= 500) || /* msvc i586+ */ \ - defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) /* gcc, sunpro, msvc */ + defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) /* gcc, msvc */ #define PG_HAVE_8BYTE_SINGLE_COPY_ATOMICITY #endif /* 8 byte single-copy atomicity */ diff --git a/src/include/port/atomics/fallback.h b/src/include/port/atomics/fallback.h index d2f1b851668e9..f37225189fc3b 100644 --- a/src/include/port/atomics/fallback.h +++ b/src/include/port/atomics/fallback.h @@ -4,7 +4,7 @@ * Fallback for platforms without 64 bit atomics support. Slower * than native atomics support, but not unusably slow. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/atomics/fallback.h diff --git a/src/include/port/atomics/generic-gcc.h b/src/include/port/atomics/generic-gcc.h index d8f04c89ccac2..5bfce82f687e9 100644 --- a/src/include/port/atomics/generic-gcc.h +++ b/src/include/port/atomics/generic-gcc.h @@ -3,7 +3,7 @@ * generic-gcc.h * Atomic operations, implemented using gcc (or compatible) intrinsics. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES: @@ -30,26 +30,34 @@ #define pg_compiler_barrier_impl() __asm__ __volatile__("" ::: "memory") /* - * If we're on GCC 4.1.0 or higher, we should be able to get a memory barrier + * If we're on GCC, we should be able to get a memory barrier * out of this compiler built-in. But we prefer to rely on platform specific * definitions where possible, and use this only as a fallback. */ #if !defined(pg_memory_barrier_impl) # if defined(HAVE_GCC__ATOMIC_INT32_CAS) # define pg_memory_barrier_impl() __atomic_thread_fence(__ATOMIC_SEQ_CST) -# elif (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)) +# elif defined(__GNUC__) # define pg_memory_barrier_impl() __sync_synchronize() # endif #endif /* !defined(pg_memory_barrier_impl) */ #if !defined(pg_read_barrier_impl) && defined(HAVE_GCC__ATOMIC_INT32_CAS) /* acquire semantics include read barrier semantics */ -# define pg_read_barrier_impl() __atomic_thread_fence(__ATOMIC_ACQUIRE) +# define pg_read_barrier_impl() do \ +{ \ + pg_compiler_barrier_impl(); \ + __atomic_thread_fence(__ATOMIC_ACQUIRE); \ +} while (0) #endif #if !defined(pg_write_barrier_impl) && defined(HAVE_GCC__ATOMIC_INT32_CAS) /* release semantics include write barrier semantics */ -# define pg_write_barrier_impl() __atomic_thread_fence(__ATOMIC_RELEASE) +# define pg_write_barrier_impl() do \ +{ \ + pg_compiler_barrier_impl(); \ + __atomic_thread_fence(__ATOMIC_RELEASE); \ +} while (0) #endif @@ -92,10 +100,9 @@ typedef struct pg_atomic_uint32 && (defined(HAVE_GCC__ATOMIC_INT64_CAS) || defined(HAVE_GCC__SYNC_INT64_CAS)) #define PG_HAVE_ATOMIC_U64_SUPPORT - typedef struct pg_atomic_uint64 { - volatile uint64 value pg_attribute_aligned(8); + alignas(8) volatile uint64 value; } pg_atomic_uint64; #endif /* defined(HAVE_GCC__ATOMIC_INT64_CAS) || defined(HAVE_GCC__SYNC_INT64_CAS) */ diff --git a/src/include/port/atomics/generic-msvc.h b/src/include/port/atomics/generic-msvc.h index a6ea5f1c2e767..6d09cd0e0438e 100644 --- a/src/include/port/atomics/generic-msvc.h +++ b/src/include/port/atomics/generic-msvc.h @@ -3,7 +3,7 @@ * generic-msvc.h * Atomic operations support when using MSVC * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES: @@ -37,9 +37,9 @@ typedef struct pg_atomic_uint32 } pg_atomic_uint32; #define PG_HAVE_ATOMIC_U64_SUPPORT -typedef struct pg_attribute_aligned(8) pg_atomic_uint64 +typedef struct pg_atomic_uint64 { - volatile uint64 value; + alignas(8) volatile uint64 value; } pg_atomic_uint64; diff --git a/src/include/port/atomics/generic-sunpro.h b/src/include/port/atomics/generic-sunpro.h deleted file mode 100644 index 09bba0be2037d..0000000000000 --- a/src/include/port/atomics/generic-sunpro.h +++ /dev/null @@ -1,113 +0,0 @@ -/*------------------------------------------------------------------------- - * - * generic-sunpro.h - * Atomic operations for solaris' CC - * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group - * - * NOTES: - * - * Documentation: - * * manpage for atomic_cas(3C) - * http://www.unix.com/man-page/opensolaris/3c/atomic_cas/ - * http://docs.oracle.com/cd/E23824_01/html/821-1465/atomic-cas-3c.html - * - * src/include/port/atomics/generic-sunpro.h - * - * ------------------------------------------------------------------------- - */ - -#ifdef HAVE_MBARRIER_H -#include - -#define pg_compiler_barrier_impl() __compiler_barrier() - -#ifndef pg_memory_barrier_impl -/* - * Despite the name this is actually a full barrier. Expanding to mfence/ - * membar #StoreStore | #LoadStore | #StoreLoad | #LoadLoad on x86/sparc - * respectively. - */ -# define pg_memory_barrier_impl() __machine_rw_barrier() -#endif -#ifndef pg_read_barrier_impl -# define pg_read_barrier_impl() __machine_r_barrier() -#endif -#ifndef pg_write_barrier_impl -# define pg_write_barrier_impl() __machine_w_barrier() -#endif - -#endif /* HAVE_MBARRIER_H */ - -/* Older versions of the compiler don't have atomic.h... */ -#ifdef HAVE_ATOMIC_H - -#include - -#define PG_HAVE_ATOMIC_U32_SUPPORT -typedef struct pg_atomic_uint32 -{ - volatile uint32 value; -} pg_atomic_uint32; - -#define PG_HAVE_ATOMIC_U64_SUPPORT -typedef struct pg_atomic_uint64 -{ - /* - * Syntax to enforce variable alignment should be supported by versions - * supporting atomic.h, but it's hard to find accurate documentation. If - * it proves to be a problem, we'll have to add more version checks for 64 - * bit support. - */ - volatile uint64 value pg_attribute_aligned(8); -} pg_atomic_uint64; - -#endif /* HAVE_ATOMIC_H */ - - -#ifdef HAVE_ATOMIC_H - -#define PG_HAVE_ATOMIC_COMPARE_EXCHANGE_U32 -static inline bool -pg_atomic_compare_exchange_u32_impl(volatile pg_atomic_uint32 *ptr, - uint32 *expected, uint32 newval) -{ - bool ret; - uint32 current; - - current = atomic_cas_32(&ptr->value, *expected, newval); - ret = current == *expected; - *expected = current; - return ret; -} - -#define PG_HAVE_ATOMIC_EXCHANGE_U32 -static inline uint32 -pg_atomic_exchange_u32_impl(volatile pg_atomic_uint32 *ptr, uint32 newval) -{ - return atomic_swap_32(&ptr->value, newval); -} - -#define PG_HAVE_ATOMIC_COMPARE_EXCHANGE_U64 -static inline bool -pg_atomic_compare_exchange_u64_impl(volatile pg_atomic_uint64 *ptr, - uint64 *expected, uint64 newval) -{ - bool ret; - uint64 current; - - AssertPointerAlignment(expected, 8); - current = atomic_cas_64(&ptr->value, *expected, newval); - ret = current == *expected; - *expected = current; - return ret; -} - -#define PG_HAVE_ATOMIC_EXCHANGE_U64 -static inline uint64 -pg_atomic_exchange_u64_impl(volatile pg_atomic_uint64 *ptr, uint64 newval) -{ - return atomic_swap_64(&ptr->value, newval); -} - -#endif /* HAVE_ATOMIC_H */ diff --git a/src/include/port/atomics/generic.h b/src/include/port/atomics/generic.h index 6b61a7b541665..fd64b6cbd8681 100644 --- a/src/include/port/atomics/generic.h +++ b/src/include/port/atomics/generic.h @@ -4,7 +4,7 @@ * Implement higher level operations based on some lower level atomic * operations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/atomics/generic.h @@ -297,6 +297,15 @@ pg_atomic_write_u64_impl(volatile pg_atomic_uint64 *ptr, uint64 val) #endif /* PG_HAVE_8BYTE_SINGLE_COPY_ATOMICITY && !PG_HAVE_ATOMIC_U64_SIMULATION */ #endif /* PG_HAVE_ATOMIC_WRITE_U64 */ +#ifndef PG_HAVE_ATOMIC_UNLOCKED_WRITE_U64 +#define PG_HAVE_ATOMIC_UNLOCKED_WRITE_U64 +static inline void +pg_atomic_unlocked_write_u64_impl(volatile pg_atomic_uint64 *ptr, uint64 val) +{ + ptr->value = val; +} +#endif + #ifndef PG_HAVE_ATOMIC_READ_U64 #define PG_HAVE_ATOMIC_READ_U64 diff --git a/src/include/port/pg_bitutils.h b/src/include/port/pg_bitutils.h index c7901bf8ddc0a..7a00d197013ba 100644 --- a/src/include/port/pg_bitutils.h +++ b/src/include/port/pg_bitutils.h @@ -4,7 +4,7 @@ * Miscellaneous functions for bit-wise operations. * * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * src/include/port/pg_bitutils.h * @@ -276,79 +276,63 @@ pg_ceil_log2_64(uint64 num) return pg_leftmost_one_pos64(num - 1) + 1; } -/* - * With MSVC on x86_64 builds, try using native popcnt instructions via the - * __popcnt and __popcnt64 intrinsics. These don't work the same as GCC's - * __builtin_popcount* intrinsic functions as they always emit popcnt - * instructions. - */ -#if defined(_MSC_VER) && defined(_M_AMD64) -#define HAVE_X86_64_POPCNTQ -#endif +extern uint64 pg_popcount_portable(const char *buf, int bytes); +extern uint64 pg_popcount_masked_portable(const char *buf, int bytes, uint8 mask); +#if defined(HAVE_X86_64_POPCNTQ) || defined(USE_SVE_POPCNT_WITH_RUNTIME_CHECK) /* - * On x86_64, we can use the hardware popcount instruction, but only if - * we can verify that the CPU supports it via the cpuid instruction. - * - * Otherwise, we fall back to a hand-rolled implementation. + * Attempt to use specialized CPU instructions, but perform a runtime check + * first. */ -#ifdef HAVE_X86_64_POPCNTQ -#if defined(HAVE__GET_CPUID) || defined(HAVE__CPUID) -#define TRY_POPCNT_X86_64 1 -#endif -#endif +extern PGDLLIMPORT uint64 (*pg_popcount_optimized) (const char *buf, int bytes); +extern PGDLLIMPORT uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, uint8 mask); -/* - * On AArch64, we can use Neon instructions if the compiler provides access to - * them (as indicated by __ARM_NEON). As in simd.h, we assume that all - * available 64-bit hardware has Neon support. - */ -#if defined(__aarch64__) && defined(__ARM_NEON) -#define POPCNT_AARCH64 1 -#endif +#else +/* Use a portable implementation -- no need for a function pointer. */ +extern uint64 pg_popcount_optimized(const char *buf, int bytes); +extern uint64 pg_popcount_masked_optimized(const char *buf, int bytes, uint8 mask); -#ifdef TRY_POPCNT_X86_64 -/* Attempt to use the POPCNT instruction, but perform a runtime check first */ -extern PGDLLIMPORT int (*pg_popcount32) (uint32 word); -extern PGDLLIMPORT int (*pg_popcount64) (uint64 word); -extern PGDLLIMPORT uint64 (*pg_popcount_optimized) (const char *buf, int bytes); -extern PGDLLIMPORT uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, bits8 mask); +#endif /* - * We can also try to use the AVX-512 popcount instruction on some systems. - * The implementation of that is located in its own file. + * pg_popcount32 + * Return the number of 1 bits set in word + * + * Adapted from + * https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel. + * + * Note that newer versions of popular compilers will automatically replace + * this with a special popcount instruction if possible, so we don't bother + * using builtin functions or intrinsics. */ -#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK -extern bool pg_popcount_avx512_available(void); -extern uint64 pg_popcount_avx512(const char *buf, int bytes); -extern uint64 pg_popcount_masked_avx512(const char *buf, int bytes, bits8 mask); -#endif - -#elif POPCNT_AARCH64 -/* Use the Neon version of pg_popcount{32,64} without function pointer. */ -extern int pg_popcount32(uint32 word); -extern int pg_popcount64(uint64 word); +static inline int +pg_popcount32(uint32 word) +{ + word -= (word >> 1) & 0x55555555; + word = (word & 0x33333333) + ((word >> 2) & 0x33333333); + return (((word + (word >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +} /* - * We can try to use an SVE-optimized pg_popcount() on some systems For that, - * we do use a function pointer. + * pg_popcount64 + * Return the number of 1 bits set in word + * + * Adapted from + * https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel. + * + * Note that newer versions of popular compilers will automatically replace + * this with a special popcount instruction if possible, so we don't bother + * using builtin functions or intrinsics. */ -#ifdef USE_SVE_POPCNT_WITH_RUNTIME_CHECK -extern PGDLLIMPORT uint64 (*pg_popcount_optimized) (const char *buf, int bytes); -extern PGDLLIMPORT uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, bits8 mask); -#else -extern uint64 pg_popcount_optimized(const char *buf, int bytes); -extern uint64 pg_popcount_masked_optimized(const char *buf, int bytes, bits8 mask); -#endif - -#else -/* Use a portable implementation -- no need for a function pointer. */ -extern int pg_popcount32(uint32 word); -extern int pg_popcount64(uint64 word); -extern uint64 pg_popcount_optimized(const char *buf, int bytes); -extern uint64 pg_popcount_masked_optimized(const char *buf, int bytes, bits8 mask); - -#endif /* TRY_POPCNT_X86_64 */ +static inline int +pg_popcount64(uint64 word) +{ + word -= (word >> 1) & UINT64CONST(0x5555555555555555); + word = (word & UINT64CONST(0x3333333333333333)) + + ((word >> 2) & UINT64CONST(0x3333333333333333)); + word = (word + (word >> 4)) & UINT64CONST(0xf0f0f0f0f0f0f0f); + return (word * UINT64CONST(0x101010101010101)) >> 56; +} /* * Returns the number of 1-bits in buf. @@ -366,13 +350,7 @@ pg_popcount(const char *buf, int bytes) * We set the threshold to the point at which we'll first use special * instructions in the optimized version. */ -#if SIZEOF_VOID_P >= 8 - int threshold = 8; -#else - int threshold = 4; -#endif - - if (bytes < threshold) + if (bytes < 8) { uint64 popcnt = 0; @@ -391,19 +369,13 @@ pg_popcount(const char *buf, int bytes) * it's likely to be faster. */ static inline uint64 -pg_popcount_masked(const char *buf, int bytes, bits8 mask) +pg_popcount_masked(const char *buf, int bytes, uint8 mask) { /* * We set the threshold to the point at which we'll first use special * instructions in the optimized version. */ -#if SIZEOF_VOID_P >= 8 - int threshold = 8; -#else - int threshold = 4; -#endif - - if (bytes < threshold) + if (bytes < 8) { uint64 popcnt = 0; diff --git a/src/include/port/pg_bswap.h b/src/include/port/pg_bswap.h index 33648433c6377..5b10066728601 100644 --- a/src/include/port/pg_bswap.h +++ b/src/include/port/pg_bswap.h @@ -11,7 +11,7 @@ * return the same. Use caution when using these wrapper macros with signed * integers. * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * src/include/port/pg_bswap.h * @@ -130,8 +130,7 @@ pg_bswap64(uint64 x) /* * Rearrange the bytes of a Datum from big-endian order into the native byte - * order. On big-endian machines, this does nothing at all. Note that the C - * type Datum is an unsigned integer type on all platforms. + * order. On big-endian machines, this does nothing at all. * * One possible application of the DatumBigEndianToNative() macro is to make * bitwise comparisons cheaper. A simple 3-way comparison of Datums @@ -139,23 +138,11 @@ pg_bswap64(uint64 x) * the same result as a memcmp() of the corresponding original Datums, but can * be much cheaper. It's generally safe to do this on big-endian systems * without any special transformation occurring first. - * - * If SIZEOF_DATUM is not defined, then postgres.h wasn't included and these - * macros probably shouldn't be used, so we define nothing. Note that - * SIZEOF_DATUM == 8 would evaluate as 0 == 8 in that case, potentially - * leading to the wrong implementation being selected and confusing errors, so - * defining nothing is safest. */ -#ifdef SIZEOF_DATUM #ifdef WORDS_BIGENDIAN #define DatumBigEndianToNative(x) (x) #else /* !WORDS_BIGENDIAN */ -#if SIZEOF_DATUM == 8 -#define DatumBigEndianToNative(x) pg_bswap64(x) -#else /* SIZEOF_DATUM != 8 */ -#define DatumBigEndianToNative(x) pg_bswap32(x) -#endif /* SIZEOF_DATUM == 8 */ +#define DatumBigEndianToNative(x) UInt64GetDatum(pg_bswap64(DatumGetUInt64(x))) #endif /* WORDS_BIGENDIAN */ -#endif /* SIZEOF_DATUM */ #endif /* PG_BSWAP_H */ diff --git a/src/include/port/pg_cpu.h b/src/include/port/pg_cpu.h new file mode 100644 index 0000000000000..a5d42f1b68d10 --- /dev/null +++ b/src/include/port/pg_cpu.h @@ -0,0 +1,63 @@ +/*------------------------------------------------------------------------- + * + * pg_cpu.h + * Runtime CPU feature detection + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/port/pg_cpu.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_CPU_H +#define PG_CPU_H + +#if defined(USE_SSE2) || defined(__i386__) + +typedef enum X86FeatureId +{ + /* Have we run feature detection? */ + INIT_PG_X86, + + /* scalar registers and 128-bit XMM registers */ + PG_SSE4_2, + PG_POPCNT, + + /* 256-bit YMM registers */ + PG_AVX2, + + /* 512-bit ZMM registers */ + PG_AVX512_BW, + PG_AVX512_VL, + PG_AVX512_VPCLMULQDQ, + PG_AVX512_VPOPCNTDQ, + + /* identification */ + PG_HYPERVISOR, + + /* Time-Stamp Counter (TSC) flags */ + PG_RDTSCP, + PG_TSC_INVARIANT, + PG_TSC_ADJUST, +} X86FeatureId; +#define X86FeaturesSize (PG_TSC_ADJUST + 1) + +extern PGDLLIMPORT bool X86Features[]; + +extern void set_x86_features(void); + +static inline bool +x86_feature_available(X86FeatureId feature) +{ + if (X86Features[INIT_PG_X86] == false) + set_x86_features(); + + return X86Features[feature]; +} + +extern uint32 x86_tsc_frequency_khz(void); + +#endif /* defined(USE_SSE2) || defined(__i386__) */ + +#endif /* PG_CPU_H */ diff --git a/src/include/port/pg_crc32c.h b/src/include/port/pg_crc32c.h index 82313bb7fcfee..e6085ca50d15d 100644 --- a/src/include/port/pg_crc32c.h +++ b/src/include/port/pg_crc32c.h @@ -23,7 +23,7 @@ * EQ_CRC32C(c1, c2) * Check for equality of two CRCs. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/pg_crc32c.h @@ -72,7 +72,7 @@ pg_comp_crc32c_dispatch(pg_crc32c crc, const void *data, size_t len) { if (__builtin_constant_p(len) && len < 32) { - const unsigned char *p = data; + const unsigned char *p = (const unsigned char *) data; /* * For small constant inputs, inline the computation to avoid a @@ -104,20 +104,37 @@ pg_comp_crc32c_dispatch(pg_crc32c crc, const void *data, size_t len) #define FIN_CRC32C(crc) ((crc) ^= 0xFFFFFFFF) extern pg_crc32c pg_comp_crc32c_sb8(pg_crc32c crc, const void *data, size_t len); -extern PGDLLIMPORT pg_crc32c (*pg_comp_crc32c) (pg_crc32c crc, const void *data, size_t len); +extern pg_crc32c (*pg_comp_crc32c) (pg_crc32c crc, const void *data, size_t len); extern pg_crc32c pg_comp_crc32c_sse42(pg_crc32c crc, const void *data, size_t len); #ifdef USE_AVX512_CRC32C_WITH_RUNTIME_CHECK extern pg_crc32c pg_comp_crc32c_avx512(pg_crc32c crc, const void *data, size_t len); #endif #elif defined(USE_ARMV8_CRC32C) -/* Use ARMv8 CRC Extension instructions. */ - +/* + * Use either ARMv8 CRC Extension or CRYPTO Extension (PMULL) instructions. + */ +#ifdef HAVE_PG_INTEGER_CONSTANT_P +/* + * We don't need a runtime check for CRC, so for constant inputs, where + * we assume the input is small, we can avoid an indirect function call. + */ #define COMP_CRC32C(crc, data, len) \ - ((crc) = pg_comp_crc32c_armv8((crc), (data), (len))) + ((crc) = pg_integer_constant_p(len) ? \ + pg_comp_crc32c_armv8((crc), (data), (len)) : \ + pg_comp_crc32c((crc), (data), (len))) +#else +#define COMP_CRC32C(crc, data, len) \ + ((crc) = pg_comp_crc32c((crc), (data), (len))) +#endif /* HAVE_PG_INTEGER_CONSTANT_P */ + #define FIN_CRC32C(crc) ((crc) ^= 0xFFFFFFFF) +extern pg_crc32c (*pg_comp_crc32c) (pg_crc32c crc, const void *data, size_t len); extern pg_crc32c pg_comp_crc32c_armv8(pg_crc32c crc, const void *data, size_t len); +#ifdef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK +extern pg_crc32c pg_comp_crc32c_pmull(pg_crc32c crc, const void *data, size_t len); +#endif #elif defined(USE_LOONGARCH_CRC32C) /* Use LoongArch CRCC instructions. */ @@ -131,8 +148,8 @@ extern pg_crc32c pg_comp_crc32c_loongarch(pg_crc32c crc, const void *data, size_ #elif defined(USE_ARMV8_CRC32C_WITH_RUNTIME_CHECK) /* - * Use ARMv8 instructions, but perform a runtime check first - * to check that they are available. + * Use either ARMv8 CRC Extension or CRYPTO Extension (PMULL) instructions, + * but perform a runtime check first to check that they are available. */ #define COMP_CRC32C(crc, data, len) \ ((crc) = pg_comp_crc32c((crc), (data), (len))) @@ -141,6 +158,9 @@ extern pg_crc32c pg_comp_crc32c_loongarch(pg_crc32c crc, const void *data, size_ extern pg_crc32c pg_comp_crc32c_sb8(pg_crc32c crc, const void *data, size_t len); extern pg_crc32c (*pg_comp_crc32c) (pg_crc32c crc, const void *data, size_t len); extern pg_crc32c pg_comp_crc32c_armv8(pg_crc32c crc, const void *data, size_t len); +#ifdef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK +extern pg_crc32c pg_comp_crc32c_pmull(pg_crc32c crc, const void *data, size_t len); +#endif #else /* diff --git a/src/include/port/pg_getopt_ctx.h b/src/include/port/pg_getopt_ctx.h new file mode 100644 index 0000000000000..39c3f113715cb --- /dev/null +++ b/src/include/port/pg_getopt_ctx.h @@ -0,0 +1,39 @@ +/* + * Re-entrant version of the standard getopt(3) function. + * + * Portions Copyright (c) 2026, PostgreSQL Global Development Group + * + * src/include/port/pg_getopt_ctx.h + */ +#ifndef PG_GETOPT_CTX_H +#define PG_GETOPT_CTX_H + +typedef struct +{ + int nargc; + char *const *nargv; + const char *ostr; + + /* + * Caller can modify 'opterr' between pg_getopt_start() and the first call + * to pg_getopt_next(). Equivalent to the global variable of the same + * name in standard getopt(3). + */ + int opterr; + + /* + * Output variables set by pg_getopt_next(). These are equivalent to the + * global variables with same names in standard getopt(3). + */ + char *optarg; + int optind; + int optopt; + + /* internal state */ + char *place; +} pg_getopt_ctx; + +extern void pg_getopt_start(pg_getopt_ctx *ctx, int nargc, char *const *nargv, const char *ostr); +extern int pg_getopt_next(pg_getopt_ctx *ctx); + +#endif /* PG_GETOPT_CTX_H */ diff --git a/src/include/port/pg_iovec.h b/src/include/port/pg_iovec.h index df40c7208be48..2fec456fb5816 100644 --- a/src/include/port/pg_iovec.h +++ b/src/include/port/pg_iovec.h @@ -3,7 +3,7 @@ * pg_iovec.h * Header for vectored I/O functions, to use in place of . * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/pg_iovec.h @@ -21,9 +21,6 @@ #else -/* POSIX requires at least 16 as a maximum iovcnt. */ -#define IOV_MAX 16 - /* Define our own POSIX-compatible iovec struct. */ struct iovec { @@ -33,6 +30,15 @@ struct iovec #endif +/* + * If didn't define IOV_MAX, define our own. X/Open requires at + * least 16. (GNU Hurd apparently feel that they're not bound by X/Open, + * because they don't define this symbol at all.) + */ +#ifndef IOV_MAX +#define IOV_MAX 16 +#endif + /* * Define a reasonable maximum that is safe to use on the stack in arrays of * struct iovec and other small types. The operating system could limit us to @@ -45,7 +51,7 @@ struct iovec * this changes the current file position. */ static inline ssize_t -pg_preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset) +pg_preadv(int fd, const struct iovec *iov, int iovcnt, pgoff_t offset) { #if HAVE_DECL_PREADV /* @@ -84,7 +90,7 @@ pg_preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset) * this changes the current file position. */ static inline ssize_t -pg_pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset) +pg_pwritev(int fd, const struct iovec *iov, int iovcnt, pgoff_t offset) { #if HAVE_DECL_PWRITEV /* diff --git a/src/include/port/pg_lfind.h b/src/include/port/pg_lfind.h index 20f7497dcb767..18856c7a8c919 100644 --- a/src/include/port/pg_lfind.h +++ b/src/include/port/pg_lfind.h @@ -4,7 +4,7 @@ * Optimized linear search routines using SIMD intrinsics where * available. * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/port/pg_lfind.h @@ -23,7 +23,7 @@ * return false. */ static inline bool -pg_lfind8(uint8 key, uint8 *base, uint32 nelem) +pg_lfind8(uint8 key, const uint8 *base, uint32 nelem) { uint32 i; @@ -55,7 +55,7 @@ pg_lfind8(uint8 key, uint8 *base, uint32 nelem) * 'key', otherwise return false. */ static inline bool -pg_lfind8_le(uint8 key, uint8 *base, uint32 nelem) +pg_lfind8_le(uint8 key, const uint8 *base, uint32 nelem) { uint32 i; diff --git a/src/include/port/pg_numa.h b/src/include/port/pg_numa.h index 40f1d324dcfe2..1b668fe1d9112 100644 --- a/src/include/port/pg_numa.h +++ b/src/include/port/pg_numa.h @@ -4,7 +4,7 @@ * Basic NUMA portability routines * * - * Copyright (c) 2025, PostgreSQL Global Development Group + * Copyright (c) 2025-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/port/pg_numa.h @@ -24,12 +24,17 @@ extern PGDLLIMPORT int pg_numa_get_max_node(void); * This is required on Linux, before pg_numa_query_pages() as we * need to page-fault before move_pages(2) syscall returns valid results. */ -#define pg_numa_touch_mem_if_required(ro_volatile_var, ptr) \ - ro_volatile_var = *(volatile uint64 *) ptr +static inline void +pg_numa_touch_mem_if_required(void *ptr) +{ + volatile uint64 touch pg_attribute_unused(); + + touch = *(volatile uint64 *) ptr; +} #else -#define pg_numa_touch_mem_if_required(ro_volatile_var, ptr) \ +#define pg_numa_touch_mem_if_required(ptr) \ do {} while(0) #endif diff --git a/src/include/port/simd.h b/src/include/port/simd.h index 97c5f3530221a..50615aec7f423 100644 --- a/src/include/port/simd.h +++ b/src/include/port/simd.h @@ -3,7 +3,7 @@ * simd.h * Support for platform-specific vector operations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/simd.h @@ -18,32 +18,19 @@ #ifndef SIMD_H #define SIMD_H -#if (defined(__x86_64__) || defined(_M_AMD64)) +#if defined(USE_SSE2) /* - * SSE2 instructions are part of the spec for the 64-bit x86 ISA. We assume - * that compilers targeting this architecture understand SSE2 intrinsics. - * * We use emmintrin.h rather than the comprehensive header immintrin.h in * order to exclude extensions beyond SSE2. This is because MSVC, at least, * will allow the use of intrinsics that haven't been enabled at compile * time. */ #include -#define USE_SSE2 typedef __m128i Vector8; typedef __m128i Vector32; -#elif defined(__aarch64__) && defined(__ARM_NEON) -/* - * We use the Neon instructions if the compiler provides access to them (as - * indicated by __ARM_NEON) and we are on aarch64. While Neon support is - * technically optional for aarch64, it appears that all available 64-bit - * hardware does have it. Neon exists in some 32-bit hardware too, but we - * could not realistically use it there without a run-time check, which seems - * not worth the trouble for now. - */ +#elif defined(USE_NEON) #include -#define USE_NEON typedef uint8x16_t Vector8; typedef uint32x4_t Vector32; @@ -86,7 +73,6 @@ static inline uint32 vector8_highbit_mask(const Vector8 v); static inline Vector8 vector8_or(const Vector8 v1, const Vector8 v2); #ifndef USE_NO_SIMD static inline Vector32 vector32_or(const Vector32 v1, const Vector32 v2); -static inline Vector8 vector8_ssub(const Vector8 v1, const Vector8 v2); #endif /* @@ -128,6 +114,21 @@ vector32_load(Vector32 *v, const uint32 *s) } #endif /* ! USE_NO_SIMD */ +/* + * Store a vector into the given memory address. + */ +#ifndef USE_NO_SIMD +static inline void +vector8_store(uint8 *s, Vector8 v) +{ +#ifdef USE_SSE2 + _mm_storeu_si128((Vector8 *) s, v); +#elif defined(USE_NEON) + vst1q_u8(s, v); +#endif +} +#endif /* ! USE_NO_SIMD */ + /* * Create a vector with all elements set to the same value. */ @@ -213,6 +214,10 @@ static inline bool vector8_has_le(const Vector8 v, const uint8 c) { bool result = false; +#ifdef USE_SSE2 + Vector8 umin; + Vector8 cmpe; +#endif /* pre-compute the result for assert checking */ #ifdef USE_ASSERT_CHECKING @@ -250,20 +255,37 @@ vector8_has_le(const Vector8 v, const uint8 c) } } } -#else - - /* - * Use saturating subtraction to find bytes <= c, which will present as - * NUL bytes. This approach is a workaround for the lack of unsigned - * comparison instructions on some architectures. - */ - result = vector8_has_zero(vector8_ssub(v, vector8_broadcast(c))); +#elif defined(USE_SSE2) + umin = vector8_min(v, vector8_broadcast(c)); + cmpe = vector8_eq(umin, v); + result = vector8_is_highbit_set(cmpe); +#elif defined(USE_NEON) + result = vminvq_u8(v) <= c; #endif Assert(assert_result == result); return result; } +/* + * Returns true if any elements in the vector are greater than or equal to the + * given scalar. + */ +#ifndef USE_NO_SIMD +static inline bool +vector8_has_ge(const Vector8 v, const uint8 c) +{ +#ifdef USE_SSE2 + Vector8 umax = _mm_max_epu8(v, vector8_broadcast(c)); + Vector8 cmpe = vector8_eq(umax, v); + + return vector8_is_highbit_set(cmpe); +#elif defined(USE_NEON) + return vmaxvq_u8(v) >= c; +#endif +} +#endif /* ! USE_NO_SIMD */ + /* * Return true if the high bit of any element is set */ @@ -358,20 +380,51 @@ vector32_or(const Vector32 v1, const Vector32 v2) } #endif /* ! USE_NO_SIMD */ +/* + * Return the bitwise AND of the inputs. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_and(const Vector8 v1, const Vector8 v2) +{ +#ifdef USE_SSE2 + return _mm_and_si128(v1, v2); +#elif defined(USE_NEON) + return vandq_u8(v1, v2); +#endif +} +#endif /* ! USE_NO_SIMD */ + +/* + * Return the result of adding the respective elements of the input vectors. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_add(const Vector8 v1, const Vector8 v2) +{ +#ifdef USE_SSE2 + return _mm_add_epi8(v1, v2); +#elif defined(USE_NEON) + return vaddq_u8(v1, v2); +#endif +} +#endif /* ! USE_NO_SIMD */ + /* * Return the result of subtracting the respective elements of the input - * vectors using saturation (i.e., if the operation would yield a value less - * than zero, zero is returned instead). For more information on saturation - * arithmetic, see https://en.wikipedia.org/wiki/Saturation_arithmetic + * vectors using signed saturation (i.e., if the operation would yield a value + * less than -128, -128 is returned instead). For more information on + * saturation arithmetic, see + * https://en.wikipedia.org/wiki/Saturation_arithmetic */ #ifndef USE_NO_SIMD static inline Vector8 -vector8_ssub(const Vector8 v1, const Vector8 v2) +vector8_issub(const Vector8 v1, const Vector8 v2) { #ifdef USE_SSE2 - return _mm_subs_epu8(v1, v2); + return _mm_subs_epi8(v1, v2); #elif defined(USE_NEON) - return vqsubq_u8(v1, v2); + return (Vector8) vqsubq_s8((int8x16_t) v1, (int8x16_t) v2); #endif } #endif /* ! USE_NO_SIMD */ @@ -404,6 +457,23 @@ vector32_eq(const Vector32 v1, const Vector32 v2) } #endif /* ! USE_NO_SIMD */ +/* + * Return a vector with all bits set for each lane of v1 that is greater than + * the corresponding lane of v2. NB: The comparison treats the elements as + * signed. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_gt(const Vector8 v1, const Vector8 v2) +{ +#ifdef USE_SSE2 + return _mm_cmpgt_epi8(v1, v2); +#elif defined(USE_NEON) + return vcgtq_s8((int8x16_t) v1, (int8x16_t) v2); +#endif +} +#endif /* ! USE_NO_SIMD */ + /* * Given two vectors, return a vector with the minimum element of each. */ @@ -419,4 +489,115 @@ vector8_min(const Vector8 v1, const Vector8 v2) } #endif /* ! USE_NO_SIMD */ +/* + * Interleave elements of low halves (e.g., for SSE2, bits 0-63) of given + * vectors. Bytes 0, 2, 4, etc. use v1, and bytes 1, 3, 5, etc. use v2. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_interleave_low(const Vector8 v1, const Vector8 v2) +{ +#ifdef USE_SSE2 + return _mm_unpacklo_epi8(v1, v2); +#elif defined(USE_NEON) + return vzip1q_u8(v1, v2); +#endif +} +#endif /* ! USE_NO_SIMD */ + +/* + * Interleave elements of high halves (e.g., for SSE2, bits 64-127) of given + * vectors. Bytes 0, 2, 4, etc. use v1, and bytes 1, 3, 5, etc. use v2. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_interleave_high(const Vector8 v1, const Vector8 v2) +{ +#ifdef USE_SSE2 + return _mm_unpackhi_epi8(v1, v2); +#elif defined(USE_NEON) + return vzip2q_u8(v1, v2); +#endif +} +#endif /* ! USE_NO_SIMD */ + +/* + * Pack 16-bit elements in the given vectors into a single vector of 8-bit + * elements. The first half of the return vector (e.g., for SSE2, bits 0-63) + * uses v1, and the second half (e.g., for SSE2, bits 64-127) uses v2. + * + * NB: The upper 8-bits of each 16-bit element must be zeros, else this will + * produce different results on different architectures. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_pack_16(const Vector8 v1, const Vector8 v2) +{ + Vector8 mask PG_USED_FOR_ASSERTS_ONLY; + + mask = vector8_interleave_low(vector8_broadcast(0), vector8_broadcast(0xff)); + Assert(!vector8_has_ge(vector8_and(v1, mask), 1)); + Assert(!vector8_has_ge(vector8_and(v2, mask), 1)); +#ifdef USE_SSE2 + return _mm_packus_epi16(v1, v2); +#elif defined(USE_NEON) + return vuzp1q_u8(v1, v2); +#endif +} +#endif /* ! USE_NO_SIMD */ + +/* + * Unsigned shift left of each 32-bit element in the vector by "i" bits. + * + * XXX AArch64 requires an integer literal, so we have to list all expected + * values of "i" from all callers in a switch statement. If you add a new + * caller, be sure your expected values of "i" are handled. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_shift_left(const Vector8 v1, int i) +{ +#ifdef USE_SSE2 + return _mm_slli_epi32(v1, i); +#elif defined(USE_NEON) + switch (i) + { + case 4: + return (Vector8) vshlq_n_u32((Vector32) v1, 4); + default: + Assert(false); + return vector8_broadcast(0); + } +#endif +} +#endif /* ! USE_NO_SIMD */ + +/* + * Unsigned shift right of each 32-bit element in the vector by "i" bits. + * + * XXX AArch64 requires an integer literal, so we have to list all expected + * values of "i" from all callers in a switch statement. If you add a new + * caller, be sure your expected values of "i" are handled. + */ +#ifndef USE_NO_SIMD +static inline Vector8 +vector8_shift_right(const Vector8 v1, int i) +{ +#ifdef USE_SSE2 + return _mm_srli_epi32(v1, i); +#elif defined(USE_NEON) + switch (i) + { + case 4: + return (Vector8) vshrq_n_u32((Vector32) v1, 4); + case 8: + return (Vector8) vshrq_n_u32((Vector32) v1, 8); + default: + Assert(false); + return vector8_broadcast(0); + } +#endif +} +#endif /* ! USE_NO_SIMD */ + #endif /* SIMD_H */ diff --git a/src/include/port/solaris.h b/src/include/port/solaris.h index e63a3bd824d6d..c352361c81d83 100644 --- a/src/include/port/solaris.h +++ b/src/include/port/solaris.h @@ -1,26 +1,14 @@ /* src/include/port/solaris.h */ -/* - * Sort this out for all operating systems some time. The __xxx - * symbols are defined on both GCC and Solaris CC, although GCC - * doesn't document them. The __xxx__ symbols are only on GCC. - */ -#if defined(__i386) && !defined(__i386__) -#define __i386__ -#endif - -#if defined(__amd64) && !defined(__amd64__) -#define __amd64__ -#endif - -#if defined(__x86_64) && !defined(__x86_64__) -#define __x86_64__ -#endif - -#if defined(__sparc) && !defined(__sparc__) -#define __sparc__ -#endif - #if defined(__i386__) #include #endif + +/* + * On original Solaris, PAM conversation procs lack a "const" in their + * declaration; but recent OpenIndiana versions put it there by default. + * The least messy way to deal with this is to define _PAM_LEGACY_NONCONST, + * which causes OpenIndiana to declare pam_conv per the Solaris tradition, + * and also use that symbol to control omitting the "const" in our own code. + */ +#define _PAM_LEGACY_NONCONST 1 diff --git a/src/include/port/win32/sys/socket.h b/src/include/port/win32/sys/socket.h index f2b475df5e5e0..4fe7693ad14e9 100644 --- a/src/include/port/win32/sys/socket.h +++ b/src/include/port/win32/sys/socket.h @@ -29,6 +29,6 @@ */ #undef gai_strerror -extern const char *gai_strerror(int ecode); +extern const char *gai_strerror(int errcode); #endif /* WIN32_SYS_SOCKET_H */ diff --git a/src/include/port/win32_port.h b/src/include/port/win32_port.h index ff7028bdc81f9..956c0b4b4c346 100644 --- a/src/include/port/win32_port.h +++ b/src/include/port/win32_port.h @@ -6,7 +6,7 @@ * Note this is read in MinGW as well as native Windows builds, * but not in Cygwin builds. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/win32_port.h @@ -194,7 +194,7 @@ extern DWORD pgwin32_get_file_type(HANDLE hFile); * with 64-bit offsets. Also, fseek() might not give an error for unseekable * streams, so harden that function with our version. */ -#define pgoff_t __int64 +typedef __int64 pgoff_t; #ifdef _MSC_VER extern int _pgfseeko64(FILE *stream, pgoff_t offset, int origin); @@ -335,18 +335,15 @@ extern int _pglstat64(const char *name, struct stat *buf); /* * Supplement to . - * This is the same value as _O_NOINHERIT in the MS header file. This is - * to ensure that we don't collide with a future definition. It means - * we cannot use _O_NOINHERIT ourselves. - */ -#define O_DSYNC 0x0080 - -/* - * Our open() replacement does not create inheritable handles, so it is safe to - * ignore O_CLOEXEC. (If we were using Windows' own open(), it might be - * necessary to convert this to _O_NOINHERIT.) + * + * We borrow bits from the high end when we have to, to avoid colliding with + * the system-defined values. Our open() replacement in src/port/open.c + * converts these to the equivalent CreateFile() flags, along with the ones + * from fcntl.h. */ -#define O_CLOEXEC 0 +#define O_CLOEXEC _O_NOINHERIT +#define O_DIRECT 0x80000000 +#define O_DSYNC 0x04000000 /* * Supplement to . @@ -508,7 +505,7 @@ extern SOCKET pgwin32_socket(int af, int type, int protocol); extern int pgwin32_bind(SOCKET s, struct sockaddr *addr, int addrlen); extern int pgwin32_listen(SOCKET s, int backlog); extern SOCKET pgwin32_accept(SOCKET s, struct sockaddr *addr, int *addrlen); -extern int pgwin32_connect(SOCKET s, const struct sockaddr *name, int namelen); +extern int pgwin32_connect(SOCKET s, const struct sockaddr *addr, int addrlen); extern int pgwin32_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout); extern int pgwin32_recv(SOCKET s, char *buf, int len, int flags); extern int pgwin32_send(SOCKET s, const void *buf, int len, int flags); @@ -584,9 +581,9 @@ typedef unsigned short mode_t; #endif /* in port/win32pread.c */ -extern ssize_t pg_pread(int fd, void *buf, size_t nbyte, off_t offset); +extern ssize_t pg_pread(int fd, void *buf, size_t size, pgoff_t offset); /* in port/win32pwrite.c */ -extern ssize_t pg_pwrite(int fd, const void *buf, size_t nbyte, off_t offset); +extern ssize_t pg_pwrite(int fd, const void *buf, size_t size, pgoff_t offset); #endif /* PG_WIN32_PORT_H */ diff --git a/src/include/port/win32ntdll.h b/src/include/port/win32ntdll.h index c03fd52816950..3c4a088b196e9 100644 --- a/src/include/port/win32ntdll.h +++ b/src/include/port/win32ntdll.h @@ -3,7 +3,7 @@ * win32ntdll.h * Dynamically loaded Windows NT functions. * - * Portions Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2021-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/port/win32ntdll.h diff --git a/src/include/portability/instr_time.h b/src/include/portability/instr_time.h index f71a851b18d87..92558e234ac1f 100644 --- a/src/include/portability/instr_time.h +++ b/src/include/portability/instr_time.h @@ -4,9 +4,10 @@ * portable high-precision interval timing * * This file provides an abstraction layer to hide portability issues in - * interval timing. On Unix we use clock_gettime(), and on Windows we use - * QueryPerformanceCounter(). These macros also give some breathing room to - * use other high-precision-timing APIs. + * interval timing. On x86 we use the RDTSC/RDTSCP instruction directly in + * certain cases, or alternatively clock_gettime() on Unix-like systems and + * QueryPerformanceCounter() on Windows. These macros also give some breathing + * room to use other high-precision-timing APIs. * * The basic data type is instr_time, which all callers should treat as an * opaque typedef. instr_time can store either an absolute time (of @@ -17,17 +18,23 @@ * * INSTR_TIME_SET_ZERO(t) set t to zero (memset is acceptable too) * - * INSTR_TIME_SET_CURRENT(t) set t to current time + * INSTR_TIME_SET_CURRENT_FAST(t) set t to current time without waiting + * for instructions in out-of-order window + * + * INSTR_TIME_SET_CURRENT(t) set t to current time while waiting for + * instructions in OOO to retire * - * INSTR_TIME_SET_CURRENT_LAZY(t) set t to current time if t is zero, - * evaluates to whether t changed * * INSTR_TIME_ADD(x, y) x += y * + * INSTR_TIME_ADD_NANOSEC(t, n) t += n in nanoseconds (converts to ticks) + * * INSTR_TIME_SUBTRACT(x, y) x -= y * * INSTR_TIME_ACCUM_DIFF(x, y, z) x += (y - z) * + * INSTR_TIME_GT(x, y) x > y + * * INSTR_TIME_GET_DOUBLE(t) convert t to double (in seconds) * * INSTR_TIME_GET_MILLISEC(t) convert t to double (in milliseconds) @@ -47,7 +54,7 @@ * Beware of multiple evaluations of the macro arguments. * * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/portability/instr_time.h * @@ -78,11 +85,108 @@ typedef struct instr_time #define NS_PER_MS INT64CONST(1000000) #define NS_PER_US INT64CONST(1000) +/* Shift amount for fixed-point ticks-to-nanoseconds conversion. */ +#define TICKS_TO_NS_SHIFT 14 -#ifndef WIN32 +/* + * PG_INSTR_TICKS_TO_NS controls whether pg_ticks_to_ns/pg_ns_to_ticks needs to + * check ticks_per_ns_scaled and potentially convert ticks <=> nanoseconds. + * + * PG_INSTR_TSC_CLOCK controls whether the TSC clock source is compiled in, and + * potentially used based on timing_tsc_enabled. + */ +#if defined(__x86_64__) || defined(_M_X64) +#define PG_INSTR_TICKS_TO_NS 1 +#define PG_INSTR_TSC_CLOCK 1 +#elif defined(WIN32) +#define PG_INSTR_TICKS_TO_NS 1 +#define PG_INSTR_TSC_CLOCK 0 +#else +#define PG_INSTR_TICKS_TO_NS 0 +#define PG_INSTR_TSC_CLOCK 0 +#endif + +/* + * Variables used to translate ticks to nanoseconds, initialized by + * pg_initialize_timing and adjusted by pg_set_timing_clock_source calls or + * changes of the "timing_clock_source" GUC. + * + * Note that changing these values after setting an instr_time and before + * reading/converting it will lead to incorrect results. This is technically + * possible because the GUC can be changed at runtime, but unlikely, and we + * allow changing this at runtime to simplify testing of different sources. + */ +extern PGDLLIMPORT uint64 ticks_per_ns_scaled; +extern PGDLLIMPORT uint64 max_ticks_no_overflow; +extern PGDLLIMPORT bool timing_initialized; + +typedef enum +{ + TIMING_CLOCK_SOURCE_AUTO, + TIMING_CLOCK_SOURCE_SYSTEM, +#if PG_INSTR_TSC_CLOCK + TIMING_CLOCK_SOURCE_TSC +#endif +} TimingClockSourceType; + +extern PGDLLIMPORT int timing_clock_source; + +/* + * Initialize timing infrastructure + * + * This must be called at least once before using INSTR_TIME_SET_CURRENT* + * macros. + * + * If you want to use the TSC clock source in a client program, + * pg_set_timing_clock_source() needs to also be called. + */ +extern void pg_initialize_timing(void); + +/* + * Sets the time source to be used. Mainly intended for frontend programs, + * the backend should set it via the timing_clock_source GUC instead. + * + * Returns false if the clock source could not be set, for example when TSC + * is not available despite being explicitly set. + */ +extern bool pg_set_timing_clock_source(TimingClockSourceType source); +/* Whether to actually use TSC based on availability and GUC settings. */ +extern PGDLLIMPORT bool timing_tsc_enabled; -/* Use clock_gettime() */ +/* + * TSC frequency in kHz, set during initialization. + * + * -1 = not yet initialized, 0 = TSC not usable, >0 = frequency in kHz. + */ +extern PGDLLIMPORT int32 timing_tsc_frequency_khz; + +#if PG_INSTR_TSC_CLOCK + +extern void pg_initialize_timing_tsc(void); + +extern uint32 pg_tsc_calibrate_frequency(void); + +#endif /* PG_INSTR_TSC_CLOCK */ + +/* + * Returns the current timing clock source effectively in use, resolving + * TIMING_CLOCK_SOURCE_AUTO to either TIMING_CLOCK_SOURCE_SYSTEM or + * TIMING_CLOCK_SOURCE_TSC. + */ +static inline TimingClockSourceType +pg_current_timing_clock_source(void) +{ +#if PG_INSTR_TSC_CLOCK + if (timing_tsc_enabled) + return TIMING_CLOCK_SOURCE_TSC; +#endif + return TIMING_CLOCK_SOURCE_SYSTEM; +} + +#ifndef WIN32 + +/* On POSIX, use clock_gettime() for system clock source */ #include @@ -97,70 +201,216 @@ typedef struct instr_time * than CLOCK_MONOTONIC. In particular, as of macOS 10.12, Apple provides * CLOCK_MONOTONIC_RAW which is both faster to read and higher resolution than * their version of CLOCK_MONOTONIC. + * + * Note this does not get used in case the TSC clock source logic is used, + * which directly calls architecture specific timing instructions (e.g. RDTSC). */ #if defined(__darwin__) && defined(CLOCK_MONOTONIC_RAW) -#define PG_INSTR_CLOCK CLOCK_MONOTONIC_RAW +#define PG_INSTR_SYSTEM_CLOCK CLOCK_MONOTONIC_RAW +#define PG_INSTR_SYSTEM_CLOCK_NAME "clock_gettime (CLOCK_MONOTONIC_RAW)" #elif defined(CLOCK_MONOTONIC) -#define PG_INSTR_CLOCK CLOCK_MONOTONIC +#define PG_INSTR_SYSTEM_CLOCK CLOCK_MONOTONIC +#define PG_INSTR_SYSTEM_CLOCK_NAME "clock_gettime (CLOCK_MONOTONIC)" #else -#define PG_INSTR_CLOCK CLOCK_REALTIME +#define PG_INSTR_SYSTEM_CLOCK CLOCK_REALTIME +#define PG_INSTR_SYSTEM_CLOCK_NAME "clock_gettime (CLOCK_REALTIME)" #endif -/* helper for INSTR_TIME_SET_CURRENT */ static inline instr_time -pg_clock_gettime_ns(void) +pg_get_ticks_system(void) { instr_time now; struct timespec tmp; - clock_gettime(PG_INSTR_CLOCK, &tmp); + Assert(timing_initialized); + + clock_gettime(PG_INSTR_SYSTEM_CLOCK, &tmp); now.ticks = tmp.tv_sec * NS_PER_S + tmp.tv_nsec; return now; } -#define INSTR_TIME_SET_CURRENT(t) \ - ((t) = pg_clock_gettime_ns()) - -#define INSTR_TIME_GET_NANOSEC(t) \ - ((int64) (t).ticks) - - #else /* WIN32 */ +/* On Windows, use QueryPerformanceCounter() for system clock source */ -/* Use QueryPerformanceCounter() */ - -/* helper for INSTR_TIME_SET_CURRENT */ +#define PG_INSTR_SYSTEM_CLOCK_NAME "QueryPerformanceCounter" static inline instr_time -pg_query_performance_counter(void) +pg_get_ticks_system(void) { instr_time now; LARGE_INTEGER tmp; + Assert(timing_initialized); + QueryPerformanceCounter(&tmp); now.ticks = tmp.QuadPart; return now; } -static inline double -GetTimerFrequency(void) +#endif /* WIN32 */ + +static inline int64 +pg_ticks_to_ns(int64 ticks) { - LARGE_INTEGER f; +#if PG_INSTR_TICKS_TO_NS + int64 ns = 0; + + Assert(timing_initialized); + + /* + * Avoid doing work if we don't use scaled ticks, e.g. system clock on + * Unix (in that case ticks is counted in nanoseconds) + */ + if (ticks_per_ns_scaled == 0) + return ticks; + + /* + * Would multiplication overflow? If so perform computation in two parts. + */ + if (unlikely(ticks > (int64) max_ticks_no_overflow)) + { + /* + * To avoid overflow, first scale total ticks down by the fixed + * factor, and *afterwards* multiply them by the frequency-based scale + * factor. + * + * The remaining ticks can follow the regular formula, since they + * won't overflow. + */ + int64 count = ticks >> TICKS_TO_NS_SHIFT; + + ns = count * ticks_per_ns_scaled; + ticks -= (count << TICKS_TO_NS_SHIFT); + } + + ns += (ticks * ticks_per_ns_scaled) >> TICKS_TO_NS_SHIFT; + + return ns; +#else + Assert(timing_initialized); - QueryPerformanceFrequency(&f); - return (double) f.QuadPart; + return ticks; +#endif /* PG_INSTR_TICKS_TO_NS */ } -#define INSTR_TIME_SET_CURRENT(t) \ - ((t) = pg_query_performance_counter()) +static inline int64 +pg_ns_to_ticks(int64 ns) +{ +#if PG_INSTR_TICKS_TO_NS + int64 ticks = 0; -#define INSTR_TIME_GET_NANOSEC(t) \ - ((int64) ((t).ticks * ((double) NS_PER_S / GetTimerFrequency()))) + Assert(timing_initialized); -#endif /* WIN32 */ + /* + * If ticks_per_ns_scaled is zero, ticks are already in nanoseconds (e.g. + * system clock on Unix). + */ + if (ticks_per_ns_scaled == 0) + return ns; + + /* + * The reverse of pg_ticks_to_ns to avoid a similar overflow problem. + */ + if (unlikely(ns > (INT64_MAX >> TICKS_TO_NS_SHIFT))) + { + int64 count = ns / ticks_per_ns_scaled; + + ticks = count << TICKS_TO_NS_SHIFT; + ns -= count * ticks_per_ns_scaled; + } + + ticks += (ns << TICKS_TO_NS_SHIFT) / ticks_per_ns_scaled; + + return ticks; +#else + Assert(timing_initialized); + + return ns; +#endif /* PG_INSTR_TICKS_TO_NS */ +} +#if PG_INSTR_TSC_CLOCK + +#define PG_INSTR_TSC_CLOCK_NAME_FAST "RDTSC" +#define PG_INSTR_TSC_CLOCK_NAME "RDTSCP" + +#ifdef _MSC_VER +#include +#endif /* defined(_MSC_VER) */ + +/* Helpers to abstract compiler differences for reading the x86 TSC. */ +static inline int64 +pg_rdtsc(void) +{ +#ifdef _MSC_VER + return __rdtsc(); +#else + return __builtin_ia32_rdtsc(); +#endif /* defined(_MSC_VER) */ +} + +static inline int64 +pg_rdtscp(void) +{ + uint32 unused; + +#ifdef _MSC_VER + return __rdtscp(&unused); +#else + return __builtin_ia32_rdtscp(&unused); +#endif /* defined(_MSC_VER) */ +} + +/* + * Marked always_inline due to a shortcoming in gcc's heuristics leading to + * only inlining the function partially. + * See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=124795 + */ +static pg_attribute_always_inline instr_time +pg_get_ticks(void) +{ + if (likely(timing_tsc_enabled)) + { + instr_time now; + + now.ticks = pg_rdtscp(); + return now; + } + + return pg_get_ticks_system(); +} + +static pg_attribute_always_inline instr_time +pg_get_ticks_fast(void) +{ + if (likely(timing_tsc_enabled)) + { + instr_time now; + + now.ticks = pg_rdtsc(); + return now; + } + + return pg_get_ticks_system(); +} + +#else + +static pg_attribute_always_inline instr_time +pg_get_ticks(void) +{ + return pg_get_ticks_system(); +} + +static pg_attribute_always_inline instr_time +pg_get_ticks_fast(void) +{ + return pg_get_ticks_system(); +} + +#endif /* PG_INSTR_TSC_CLOCK */ /* * Common macros @@ -168,22 +418,32 @@ GetTimerFrequency(void) #define INSTR_TIME_IS_ZERO(t) ((t).ticks == 0) - #define INSTR_TIME_SET_ZERO(t) ((t).ticks = 0) -#define INSTR_TIME_SET_CURRENT_LAZY(t) \ - (INSTR_TIME_IS_ZERO(t) ? INSTR_TIME_SET_CURRENT(t), true : false) +#define INSTR_TIME_SET_CURRENT_FAST(t) \ + ((t) = pg_get_ticks_fast()) + +#define INSTR_TIME_SET_CURRENT(t) \ + ((t) = pg_get_ticks()) #define INSTR_TIME_ADD(x,y) \ ((x).ticks += (y).ticks) +#define INSTR_TIME_ADD_NANOSEC(t, n) \ + ((t).ticks += pg_ns_to_ticks(n)) + #define INSTR_TIME_SUBTRACT(x,y) \ ((x).ticks -= (y).ticks) #define INSTR_TIME_ACCUM_DIFF(x,y,z) \ ((x).ticks += (y).ticks - (z).ticks) +#define INSTR_TIME_GT(x,y) \ + ((x).ticks > (y).ticks) + +#define INSTR_TIME_GET_NANOSEC(t) \ + (pg_ticks_to_ns((t).ticks)) #define INSTR_TIME_GET_DOUBLE(t) \ ((double) INSTR_TIME_GET_NANOSEC(t) / NS_PER_S) diff --git a/src/include/portability/mem.h b/src/include/portability/mem.h index ef9800732d901..c048e8836c514 100644 --- a/src/include/portability/mem.h +++ b/src/include/portability/mem.h @@ -3,7 +3,7 @@ * mem.h * portability definitions for various memory operations * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/portability/mem.h * @@ -38,8 +38,6 @@ #define MAP_NOSYNC 0 #endif -#define PG_MMAP_FLAGS (MAP_SHARED|MAP_ANONYMOUS|MAP_HASSEMAPHORE) - /* Some really old systems don't define MAP_FAILED. */ #ifndef MAP_FAILED #define MAP_FAILED ((void *) -1) diff --git a/src/include/postgres.h b/src/include/postgres.h index 8a41a6686877f..87f6bd4949930 100644 --- a/src/include/postgres.h +++ b/src/include/postgres.h @@ -7,7 +7,7 @@ * Client-side code should include postgres_fe.h instead. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1995, Regents of the University of California * * src/include/postgres.h @@ -58,15 +58,22 @@ /* * A Datum contains either a value of a pass-by-value type or a pointer to a - * value of a pass-by-reference type. Therefore, we require: - * - * sizeof(Datum) == sizeof(void *) == 4 or 8 + * value of a pass-by-reference type. Therefore, we must have + * sizeof(Datum) >= sizeof(void *). No current or foreseeable Postgres + * platform has pointers wider than 8 bytes, and standardizing on Datum being + * exactly 8 bytes has advantages in reducing cross-platform differences. * * The functions below and the analogous functions for other types should be used to * convert between a Datum and the appropriate C type. */ -typedef uintptr_t Datum; +typedef uint64_t Datum; + +/* + * This symbol is now vestigial, but we continue to define it so as not to + * unnecessarily break extension code. + */ +#define SIZEOF_DATUM 8 /* * A NullableDatum is used in places where both a Datum and its nullness needs @@ -83,8 +90,6 @@ typedef struct NullableDatum /* due to alignment padding this could be used for flags for free */ } NullableDatum; -#define SIZEOF_DATUM SIZEOF_VOID_P - /* * DatumGetBool * Returns boolean value of a datum. @@ -129,16 +134,6 @@ CharGetDatum(char X) return (Datum) X; } -/* - * Int8GetDatum - * Returns datum representation for an 8-bit integer. - */ -static inline Datum -Int8GetDatum(int8 X) -{ - return (Datum) X; -} - /* * DatumGetUInt8 * Returns 8-bit unsigned integer value of a datum. @@ -259,6 +254,26 @@ ObjectIdGetDatum(Oid X) return (Datum) X; } +/* + * DatumGetObjectId8 + * Returns 8-byte object identifier value of a datum. + */ +static inline Oid8 +DatumGetObjectId8(Datum X) +{ + return (Oid8) X; +} + +/* + * ObjectId8GetDatum + * Returns datum representation for an 8-byte object identifier + */ +static inline Datum +ObjectId8GetDatum(Oid8 X) +{ + return (Datum) X; +} + /* * DatumGetTransactionId * Returns transaction identifier value of a datum. @@ -316,18 +331,28 @@ CommandIdGetDatum(CommandId X) static inline Pointer DatumGetPointer(Datum X) { - return (Pointer) X; + return (Pointer) (uintptr_t) X; } /* * PointerGetDatum * Returns datum representation for a pointer. + * + * This used to be defined as "static inline Datum PointerGetDatum(const void + * *X) ", but it had the problem that the compiler would see the const + * attribute, and could rightly assume that the function won't modify *X. + * While PointerGetDatum() itself doesn't modify *X, the resulting Datum could + * later be passed to a function that converts it back to a non-const pointer + * and modifies it. Most functions don't modify their arguments passed by + * reference - that would be very bogus for any operators or functions exposed + * in SQL - but some functions like GIN support functions do have output + * arguments that are pointer Datums. + * + * The odd-looking "true ? (X) : NULL" conditional expression has the effect + * of producing a compiler error if X is not a pointer. */ -static inline Datum -PointerGetDatum(const void *X) -{ - return (Datum) X; -} +#define PointerGetDatum(X) \ + ((Datum) (uintptr_t) (true ? (X) : NULL)) /* * DatumGetCString @@ -346,6 +371,9 @@ DatumGetCString(Datum X) * CStringGetDatum * Returns datum representation for a C string (null-terminated string). * + * We assume that the resulting Datum is not used to modify the string, hence + * the argument can be marked as const. + * * Note: C string is not a full-fledged Postgres type at present, * but type output functions use this conversion for their outputs. * Note: CString is pass-by-reference; caller must ensure the pointed-to @@ -383,68 +411,41 @@ NameGetDatum(const NameData *X) /* * DatumGetInt64 * Returns 64-bit integer value of a datum. - * - * Note: this function hides whether int64 is pass by value or by reference. */ static inline int64 DatumGetInt64(Datum X) { -#ifdef USE_FLOAT8_BYVAL return (int64) X; -#else - return *((int64 *) DatumGetPointer(X)); -#endif } /* * Int64GetDatum * Returns datum representation for a 64-bit integer. - * - * Note: if int64 is pass by reference, this function returns a reference - * to palloc'd space. */ -#ifdef USE_FLOAT8_BYVAL static inline Datum Int64GetDatum(int64 X) { return (Datum) X; } -#else -extern Datum Int64GetDatum(int64 X); -#endif - /* * DatumGetUInt64 * Returns 64-bit unsigned integer value of a datum. - * - * Note: this function hides whether int64 is pass by value or by reference. */ static inline uint64 DatumGetUInt64(Datum X) { -#ifdef USE_FLOAT8_BYVAL return (uint64) X; -#else - return *((uint64 *) DatumGetPointer(X)); -#endif } /* * UInt64GetDatum * Returns datum representation for a 64-bit unsigned integer. - * - * Note: if int64 is pass by reference, this function returns a reference - * to palloc'd space. */ static inline Datum UInt64GetDatum(uint64 X) { -#ifdef USE_FLOAT8_BYVAL return (Datum) X; -#else - return Int64GetDatum((int64) X); -#endif } /* @@ -492,13 +493,10 @@ Float4GetDatum(float4 X) /* * DatumGetFloat8 * Returns 8-byte floating point value of a datum. - * - * Note: this function hides whether float8 is pass by value or by reference. */ static inline float8 DatumGetFloat8(Datum X) { -#ifdef USE_FLOAT8_BYVAL union { int64 value; @@ -507,19 +505,12 @@ DatumGetFloat8(Datum X) myunion.value = DatumGetInt64(X); return myunion.retval; -#else - return *((float8 *) DatumGetPointer(X)); -#endif } /* * Float8GetDatum * Returns datum representation for an 8-byte floating point number. - * - * Note: if float8 is pass by reference, this function returns a reference - * to palloc'd space. */ -#ifdef USE_FLOAT8_BYVAL static inline Datum Float8GetDatum(float8 X) { @@ -532,35 +523,22 @@ Float8GetDatum(float8 X) myunion.value = X; return Int64GetDatum(myunion.retval); } -#else -extern Datum Float8GetDatum(float8 X); -#endif - /* * Int64GetDatumFast * Float8GetDatumFast * - * These macros are intended to allow writing code that does not depend on + * These macros were intended to allow writing code that does not depend on * whether int64 and float8 are pass-by-reference types, while not - * sacrificing performance when they are. The argument must be a variable - * that will exist and have the same value for as long as the Datum is needed. - * In the pass-by-ref case, the address of the variable is taken to use as - * the Datum. In the pass-by-val case, these are the same as the non-Fast - * functions, except for asserting that the variable is of the correct type. + * sacrificing performance when they are. They are no longer different + * from the regular functions, though we keep the assertions to protect + * code that might get back-patched into older branches. */ -#ifdef USE_FLOAT8_BYVAL #define Int64GetDatumFast(X) \ - (AssertVariableIsOfTypeMacro(X, int64), Int64GetDatum(X)) + (StaticAssertVariableIsOfTypeMacro(X, int64), Int64GetDatum(X)) #define Float8GetDatumFast(X) \ - (AssertVariableIsOfTypeMacro(X, double), Float8GetDatum(X)) -#else -#define Int64GetDatumFast(X) \ - (AssertVariableIsOfTypeMacro(X, int64), PointerGetDatum(&(X))) -#define Float8GetDatumFast(X) \ - (AssertVariableIsOfTypeMacro(X, double), PointerGetDatum(&(X))) -#endif + (StaticAssertVariableIsOfTypeMacro(X, double), Float8GetDatum(X)) /* ---------------------------------------------------------------- @@ -568,6 +546,20 @@ extern Datum Float8GetDatum(float8 X); * ---------------------------------------------------------------- */ +/* + * pg_ternary + * Boolean value with an extra "unset" value + * + * This enum can be used for values that want to distinguish between true, + * false, and unset. +*/ +typedef enum pg_ternary +{ + PG_TERNARY_FALSE = 0, + PG_TERNARY_TRUE = 1, + PG_TERNARY_UNSET = -1 +} pg_ternary; + /* * NON_EXEC_STATIC: It's sometimes useful to define a variable or function * that is normally static but extern when using EXEC_BACKEND (see diff --git a/src/include/postgres_ext.h b/src/include/postgres_ext.h index bf45c50dcf318..6a8f25a9907e4 100644 --- a/src/include/postgres_ext.h +++ b/src/include/postgres_ext.h @@ -24,6 +24,8 @@ #ifndef POSTGRES_EXT_H #define POSTGRES_EXT_H +#include + /* * Object ID is a fundamental type in Postgres. */ @@ -42,6 +44,9 @@ typedef unsigned int Oid; /* the above needs */ +/* deprecated name for int64_t, formerly used in client API declarations */ +typedef int64_t pg_int64; + /* * Identifiers of error message fields. Kept here to keep common * between frontend and backend, and also to export them to libpq diff --git a/src/include/postgres_fe.h b/src/include/postgres_fe.h index a3f65b1dca45f..3393cf2988913 100644 --- a/src/include/postgres_fe.h +++ b/src/include/postgres_fe.h @@ -8,7 +8,7 @@ * postgres.h. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1995, Regents of the University of California * * src/include/postgres_fe.h diff --git a/src/include/postmaster/autovacuum.h b/src/include/postmaster/autovacuum.h index e8135f41a1c24..8954f6b28eed4 100644 --- a/src/include/postmaster/autovacuum.h +++ b/src/include/postmaster/autovacuum.h @@ -4,7 +4,7 @@ * header file for integrated autovacuum daemon * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/postmaster/autovacuum.h @@ -43,11 +43,13 @@ extern PGDLLIMPORT int autovacuum_freeze_max_age; extern PGDLLIMPORT int autovacuum_multixact_freeze_max_age; extern PGDLLIMPORT double autovacuum_vac_cost_delay; extern PGDLLIMPORT int autovacuum_vac_cost_limit; - -/* autovacuum launcher PID, only valid when worker is shutting down */ -extern PGDLLIMPORT int AutovacuumLauncherPid; - +extern PGDLLIMPORT double autovacuum_freeze_score_weight; +extern PGDLLIMPORT double autovacuum_multixact_freeze_score_weight; +extern PGDLLIMPORT double autovacuum_vacuum_score_weight; +extern PGDLLIMPORT double autovacuum_vacuum_insert_score_weight; +extern PGDLLIMPORT double autovacuum_analyze_score_weight; extern PGDLLIMPORT int Log_autovacuum_min_duration; +extern PGDLLIMPORT int Log_autoanalyze_min_duration; /* Status inquiry functions */ extern bool AutoVacuumingActive(void); @@ -64,8 +66,4 @@ pg_noreturn extern void AutoVacWorkerMain(const void *startup_data, size_t start extern bool AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId, BlockNumber blkno); -/* shared memory stuff */ -extern Size AutoVacuumShmemSize(void); -extern void AutoVacuumShmemInit(void); - #endif /* AUTOVACUUM_H */ diff --git a/src/include/postmaster/auxprocess.h b/src/include/postmaster/auxprocess.h index 23d4b2ad2ec39..dabb07867b4c8 100644 --- a/src/include/postmaster/auxprocess.h +++ b/src/include/postmaster/auxprocess.h @@ -3,7 +3,7 @@ * include file for functions related to auxiliary processes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/postmaster/bgworker.h b/src/include/postmaster/bgworker.h index 058667a47a0a1..7418e64e19b43 100644 --- a/src/include/postmaster/bgworker.h +++ b/src/include/postmaster/bgworker.h @@ -31,7 +31,7 @@ * different) code. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -59,6 +59,13 @@ */ #define BGWORKER_BACKEND_DATABASE_CONNECTION 0x0002 +/* + * Exit the bgworker if its database is involved in a CREATE, ALTER or DROP + * database command. It requires BGWORKER_SHMEM_ACCESS and + * BGWORKER_BACKEND_DATABASE_CONNECTION. + */ +#define BGWORKER_INTERRUPTIBLE 0x0004 + /* * This class is used internally for parallel queries, to keep track of the * number of active parallel workers and make sure we never launch more than @@ -129,6 +136,9 @@ extern const char *GetBackgroundWorkerTypeByPid(pid_t pid); /* Terminate a bgworker */ extern void TerminateBackgroundWorker(BackgroundWorkerHandle *handle); +/* Terminate background workers connected to database */ +extern void TerminateBackgroundWorkersForDatabase(Oid databaseId); + /* This is valid in a running worker */ extern PGDLLIMPORT BackgroundWorker *MyBgworkerEntry; diff --git a/src/include/postmaster/bgworker_internals.h b/src/include/postmaster/bgworker_internals.h index 26cbc821edfbd..b6261bc01df7f 100644 --- a/src/include/postmaster/bgworker_internals.h +++ b/src/include/postmaster/bgworker_internals.h @@ -2,7 +2,7 @@ * bgworker_internals.h * POSTGRES pluggable background workers internals * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -41,8 +41,6 @@ typedef struct RegisteredBgWorker extern PGDLLIMPORT dlist_head BackgroundWorkerList; -extern Size BackgroundWorkerShmemSize(void); -extern void BackgroundWorkerShmemInit(void); extern void BackgroundWorkerStateChange(bool allow_new_workers); extern void ForgetBackgroundWorker(RegisteredBgWorker *rw); extern void ReportBackgroundWorkerPID(RegisteredBgWorker *rw); diff --git a/src/include/postmaster/bgwriter.h b/src/include/postmaster/bgwriter.h index 800ecbfd13b31..36eea0b1ab0c6 100644 --- a/src/include/postmaster/bgwriter.h +++ b/src/include/postmaster/bgwriter.h @@ -6,7 +6,7 @@ * The bgwriter process used to handle checkpointing duties too. Now * there is a separate process, but we did not bother to split this header. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/postmaster/bgwriter.h * @@ -15,6 +15,7 @@ #ifndef _BGWRITER_H #define _BGWRITER_H +#include "parser/parse_node.h" #include "storage/block.h" #include "storage/relfilelocator.h" #include "storage/smgr.h" @@ -30,6 +31,7 @@ extern PGDLLIMPORT double CheckPointCompletionTarget; pg_noreturn extern void BackgroundWriterMain(const void *startup_data, size_t startup_data_len); pg_noreturn extern void CheckpointerMain(const void *startup_data, size_t startup_data_len); +extern void ExecCheckpoint(ParseState *pstate, CheckPointStmt *stmt); extern void RequestCheckpoint(int flags); extern void CheckpointWriteDelay(int flags, double progress); @@ -37,9 +39,6 @@ extern bool ForwardSyncRequest(const FileTag *ftag, SyncRequestType type); extern void AbsorbSyncRequests(void); -extern Size CheckpointerShmemSize(void); -extern void CheckpointerShmemInit(void); - extern bool FirstCallSinceLastCheckpoint(void); #endif /* _BGWRITER_H */ diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h new file mode 100644 index 0000000000000..2a1ae10d55d20 --- /dev/null +++ b/src/include/postmaster/datachecksum_state.h @@ -0,0 +1,54 @@ +/*------------------------------------------------------------------------- + * + * datachecksum_state.h + * header file for data checksum helper background worker and data + * checksum state manipulation + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/postmaster/datachecksum_state.h + * + *------------------------------------------------------------------------- + */ +#ifndef DATACHECKSUM_STATE_H +#define DATACHECKSUM_STATE_H + +#include "storage/procsignal.h" + +/* Possible operations the DataChecksumsWorker can perform */ +typedef enum DataChecksumsWorkerOperation +{ + ENABLE_DATACHECKSUMS, + DISABLE_DATACHECKSUMS, +} DataChecksumsWorkerOperation; + +/* + * Possible states for a database entry which has been processed. Exported + * here since we want to be able to reference this from injection point tests. + */ +typedef enum +{ + DATACHECKSUMSWORKER_SUCCESSFUL = 0, + DATACHECKSUMSWORKER_ABORTED, + DATACHECKSUMSWORKER_FAILED, + DATACHECKSUMSWORKER_DROPDB, +} DataChecksumsWorkerResult; + +/* Prototypes for data checksum state manipulation */ +bool AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier); +void EmitAndWaitDataChecksumsBarrier(uint32 state); + +/* Prototypes for data checksum background worker */ + +/* Start the background processes for enabling or disabling checksums */ +void StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op, + int cost_delay, + int cost_limit); + +/* Background worker entrypoints */ +void DataChecksumsWorkerLauncherMain(Datum arg); +void DataChecksumsWorkerMain(Datum arg); + +#endif /* DATACHECKSUM_STATE_H */ diff --git a/src/include/postmaster/fork_process.h b/src/include/postmaster/fork_process.h index a1b20316ecd82..b6466d486f259 100644 --- a/src/include/postmaster/fork_process.h +++ b/src/include/postmaster/fork_process.h @@ -3,7 +3,7 @@ * fork_process.h * Exports from postmaster/fork_process.c. * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/postmaster/fork_process.h * diff --git a/src/include/postmaster/interrupt.h b/src/include/postmaster/interrupt.h index 3bf4932084683..e1482626dfd12 100644 --- a/src/include/postmaster/interrupt.h +++ b/src/include/postmaster/interrupt.h @@ -7,7 +7,7 @@ * have their own implementations, but we provide a few generic things * here to facilitate code reuse. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/postmaster/pgarch.h b/src/include/postmaster/pgarch.h index ef7b12520861f..9772bb573a1fc 100644 --- a/src/include/postmaster/pgarch.h +++ b/src/include/postmaster/pgarch.h @@ -3,7 +3,7 @@ * pgarch.h * Exports from postmaster/pgarch.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/postmaster/pgarch.h @@ -26,8 +26,6 @@ #define MAX_XFN_CHARS 40 #define VALID_XFN_CHARS "0123456789ABCDEF.history.backup.partial" -extern Size PgArchShmemSize(void); -extern void PgArchShmemInit(void); extern bool PgArchCanRestart(void); pg_noreturn extern void PgArchiverMain(const void *startup_data, size_t startup_data_len); extern void PgArchWakeup(void); diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h index 92497cd6a0fb5..716b4c912b33c 100644 --- a/src/include/postmaster/postmaster.h +++ b/src/include/postmaster/postmaster.h @@ -3,7 +3,7 @@ * postmaster.h * Exports from postmaster/postmaster.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/postmaster/postmaster.h @@ -108,9 +108,9 @@ extern PGDLLIMPORT struct ClientSocket *MyClientSocket; /* prototypes for functions in launch_backend.c */ extern pid_t postmaster_child_launch(BackendType child_type, int child_slot, - const void *startup_data, + void *startup_data, size_t startup_data_len, - struct ClientSocket *client_sock); + const struct ClientSocket *client_sock); const char *PostmasterChildName(BackendType child_type); #ifdef EXEC_BACKEND pg_noreturn extern void SubPostmasterMain(int argc, char *argv[]); diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h new file mode 100644 index 0000000000000..b3477e6f17a4b --- /dev/null +++ b/src/include/postmaster/proctypelist.h @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------- + * + * proctypelist.h + * + * The list of process types is kept on its own source file for use by + * automatic tools. The exact representation of a process type is + * determined by the PG_PROCTYPE macro, which is not defined in this + * file; it can be defined by the caller for special purposes. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/include/postmaster/proctypelist.h + * + *------------------------------------------------------------------------- + */ + +/* there is deliberately not an #ifndef PROCTYPELIST_H here */ + +/* + * WAL senders start their life as regular backend processes, and change their + * type after authenticating the client for replication. We list it here for + * PostmasterChildName() but cannot launch them directly. + */ + +/* + * List of process types (symbol, category, description, Main function, + * shmem_attach) entries. + */ + + +/* bktype, bkcategory, description, main_func, shmem_attach */ +PG_PROCTYPE(B_ARCHIVER, "archiver", gettext_noop("archiver"), PgArchiverMain, true) +PG_PROCTYPE(B_AUTOVAC_LAUNCHER, "autovacuum", gettext_noop("autovacuum launcher"), AutoVacLauncherMain, true) +PG_PROCTYPE(B_AUTOVAC_WORKER, "autovacuum", gettext_noop("autovacuum worker"), AutoVacWorkerMain, true) +PG_PROCTYPE(B_BACKEND, "backend", gettext_noop("client backend"), BackendMain, true) +PG_PROCTYPE(B_BG_WORKER, "bgworker", gettext_noop("background worker"), BackgroundWorkerMain, true) +PG_PROCTYPE(B_BG_WRITER, "bgwriter", gettext_noop("background writer"), BackgroundWriterMain, true) +PG_PROCTYPE(B_CHECKPOINTER, "checkpointer", gettext_noop("checkpointer"), CheckpointerMain, true) +PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, "checksums", gettext_noop("datachecksum launcher"), NULL, false) +PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, "checksums", gettext_noop("datachecksum worker"), NULL, false) +PG_PROCTYPE(B_DEAD_END_BACKEND, "backend", gettext_noop("dead-end client backend"), BackendMain, true) +PG_PROCTYPE(B_INVALID, "postmaster", gettext_noop("unrecognized"), NULL, false) +PG_PROCTYPE(B_IO_WORKER, "ioworker", gettext_noop("io worker"), IoWorkerMain, true) +PG_PROCTYPE(B_LOGGER, "syslogger", gettext_noop("syslogger"), SysLoggerMain, false) +PG_PROCTYPE(B_SLOTSYNC_WORKER, "slotsyncworker", gettext_noop("slotsync worker"), ReplSlotSyncWorkerMain, true) +PG_PROCTYPE(B_STANDALONE_BACKEND, "backend", gettext_noop("standalone backend"), NULL, false) +PG_PROCTYPE(B_STARTUP, "startup", gettext_noop("startup"), StartupProcessMain, true) +PG_PROCTYPE(B_WAL_RECEIVER, "walreceiver", gettext_noop("walreceiver"), WalReceiverMain, true) +PG_PROCTYPE(B_WAL_SENDER, "walsender", gettext_noop("walsender"), NULL, true) +PG_PROCTYPE(B_WAL_SUMMARIZER, "walsummarizer", gettext_noop("walsummarizer"), WalSummarizerMain, true) +PG_PROCTYPE(B_WAL_WRITER, "walwriter", gettext_noop("walwriter"), WalWriterMain, true) diff --git a/src/include/postmaster/startup.h b/src/include/postmaster/startup.h index ec2c8d3bff56b..4d1e12131d304 100644 --- a/src/include/postmaster/startup.h +++ b/src/include/postmaster/startup.h @@ -3,7 +3,7 @@ * startup.h * Exports from postmaster/startup.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/postmaster/startup.h * diff --git a/src/include/postmaster/syslogger.h b/src/include/postmaster/syslogger.h index 15fca58ff5dd2..30c4b2daf3f66 100644 --- a/src/include/postmaster/syslogger.h +++ b/src/include/postmaster/syslogger.h @@ -3,7 +3,7 @@ * syslogger.h * Exports from postmaster/syslogger.c. * - * Copyright (c) 2004-2025, PostgreSQL Global Development Group + * Copyright (c) 2004-2026, PostgreSQL Global Development Group * * src/include/postmaster/syslogger.h * @@ -46,7 +46,7 @@ typedef struct char nuls[2]; /* always \0\0 */ uint16 len; /* size of this chunk (counts data only) */ int32 pid; /* writer's pid */ - bits8 flags; /* bitmask of PIPE_PROTO_* */ + uint8 flags; /* bitmask of PIPE_PROTO_* */ char data[FLEXIBLE_ARRAY_MEMBER]; /* data payload starts here */ } PipeProtoHeader; diff --git a/src/include/postmaster/walsummarizer.h b/src/include/postmaster/walsummarizer.h index 34dda607c6931..b9a755fadbc0e 100644 --- a/src/include/postmaster/walsummarizer.h +++ b/src/include/postmaster/walsummarizer.h @@ -4,7 +4,7 @@ * * Header file for background WAL summarization process. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/postmaster/walsummarizer.h @@ -19,8 +19,6 @@ extern PGDLLIMPORT bool summarize_wal; extern PGDLLIMPORT int wal_summary_keep_time; -extern Size WalSummarizerShmemSize(void); -extern void WalSummarizerShmemInit(void); pg_noreturn extern void WalSummarizerMain(const void *startup_data, size_t startup_data_len); extern void GetWalSummarizerState(TimeLineID *summarized_tli, diff --git a/src/include/postmaster/walwriter.h b/src/include/postmaster/walwriter.h index 58e5c913d5ab6..b80214a613f43 100644 --- a/src/include/postmaster/walwriter.h +++ b/src/include/postmaster/walwriter.h @@ -3,7 +3,7 @@ * walwriter.h * Exports from postmaster/walwriter.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/postmaster/walwriter.h * diff --git a/src/include/regex/regcustom.h b/src/include/regex/regcustom.h index af0fe97c796d2..4557e7a62c066 100644 --- a/src/include/regex/regcustom.h +++ b/src/include/regex/regcustom.h @@ -53,6 +53,7 @@ #define FREE(p) pfree(VS(p)) #define REALLOC(p,n) repalloc_extended(VS(p),(n), MCXT_ALLOC_NO_OOM) #define INTERRUPT(re) CHECK_FOR_INTERRUPTS() +#undef assert #define assert(x) Assert(x) /* internal character type and related */ @@ -88,10 +89,10 @@ typedef unsigned uchr; /* unsigned type that will hold a chr */ #define MAX_SIMPLE_CHR 0x7FF /* suitable value for Unicode */ /* functions operating on chr */ -#define iscalnum(x) pg_wc_isalnum(x) -#define iscalpha(x) pg_wc_isalpha(x) -#define iscdigit(x) pg_wc_isdigit(x) -#define iscspace(x) pg_wc_isspace(x) +#define iscalnum(x) regc_wc_isalnum(x) +#define iscalpha(x) regc_wc_isalpha(x) +#define iscdigit(x) regc_wc_isdigit(x) +#define iscspace(x) regc_wc_isspace(x) /* and pick up the standard header */ #include "regex.h" diff --git a/src/include/regex/regexport.h b/src/include/regex/regexport.h index fe086d110f9bf..421eaaa446277 100644 --- a/src/include/regex/regexport.h +++ b/src/include/regex/regexport.h @@ -17,7 +17,7 @@ * line and start/end of string. Colors are numbered 0..C-1, but note that * color 0 is "white" (all unused characters) and can generally be ignored. * - * Portions Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2013-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1998, 1999 Henry Spencer * * IDENTIFICATION diff --git a/src/include/replication/conflict.h b/src/include/replication/conflict.h index 6c59125f25657..2d9dbcf4d0de1 100644 --- a/src/include/replication/conflict.h +++ b/src/include/replication/conflict.h @@ -2,15 +2,22 @@ * conflict.h * Exports for conflicts logging. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ #ifndef CONFLICT_H #define CONFLICT_H -#include "nodes/execnodes.h" -#include "utils/timestamp.h" +#include "access/xlogdefs.h" +#include "datatype/timestamp.h" +#include "nodes/pg_list.h" + +/* Avoid including execnodes.h here */ +typedef struct EState EState; +typedef struct ResultRelInfo ResultRelInfo; +typedef struct TupleTableSlot TupleTableSlot; + /* * Conflict types that could occur while applying remote changes. @@ -32,6 +39,9 @@ typedef enum /* The updated row value violates unique constraint */ CT_UPDATE_EXISTS, + /* The row to be updated was concurrently deleted by a different origin */ + CT_UPDATE_DELETED, + /* The row to be updated is missing */ CT_UPDATE_MISSING, @@ -54,7 +64,7 @@ typedef enum #define CONFLICT_NUM_TYPES (CT_MULTIPLE_UNIQUE_CONFLICTS + 1) /* - * Information for the existing local tuple that caused the conflict. + * Information for the local row that caused the conflict. */ typedef struct ConflictTupleInfo { @@ -64,14 +74,14 @@ typedef struct ConflictTupleInfo * occurred */ TransactionId xmin; /* transaction ID of the modification causing * the conflict */ - RepOriginId origin; /* origin identifier of the modification */ + ReplOriginId origin; /* origin identifier of the modification */ TimestampTz ts; /* timestamp of when the modification on the - * conflicting local tuple occurred */ + * conflicting local row occurred */ } ConflictTupleInfo; extern bool GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, - RepOriginId *localorigin, + ReplOriginId *localorigin, TimestampTz *localts); extern void ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, ConflictType type, diff --git a/src/include/replication/decode.h b/src/include/replication/decode.h index d6c3fc7aa9c6a..2ef177902058d 100644 --- a/src/include/replication/decode.h +++ b/src/include/replication/decode.h @@ -2,7 +2,7 @@ * decode.h * PostgreSQL WAL to logical transformation * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -22,6 +22,7 @@ typedef struct XLogRecordBuffer } XLogRecordBuffer; extern void xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf); +extern void xlog2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf); extern void heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf); extern void heap2_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf); extern void xact_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf); @@ -31,4 +32,8 @@ extern void logicalmsg_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf extern void LogicalDecodingProcessRecord(LogicalDecodingContext *ctx, XLogReaderState *record); +/* in commands/repack_worker.c */ +extern bool change_useless_for_repack(XLogRecordBuffer *buf); + + #endif diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index 2e562bee5a9c1..6e0b7628001c8 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -2,7 +2,7 @@ * logical.h * PostgreSQL logical decoding coordination * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -115,11 +115,12 @@ typedef struct LogicalDecodingContext } LogicalDecodingContext; -extern void CheckLogicalDecodingRequirements(void); +extern void CheckLogicalDecodingRequirements(bool repack); extern LogicalDecodingContext *CreateInitDecodingContext(const char *plugin, List *output_plugin_options, bool need_full_snapshot, + bool for_repack, XLogRecPtr restart_lsn, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, @@ -144,12 +145,34 @@ extern void LogicalConfirmReceivedLocation(XLogRecPtr lsn); extern bool filter_prepare_cb_wrapper(LogicalDecodingContext *ctx, TransactionId xid, const char *gid); -extern bool filter_by_origin_cb_wrapper(LogicalDecodingContext *ctx, RepOriginId origin_id); +extern bool filter_by_origin_cb_wrapper(LogicalDecodingContext *ctx, ReplOriginId origin_id); extern void ResetLogicalStreamingState(void); extern void UpdateDecodingStats(LogicalDecodingContext *ctx); -extern bool LogicalReplicationSlotHasPendingWal(XLogRecPtr end_of_wal); +extern XLogRecPtr LogicalReplicationSlotCheckPendingWal(XLogRecPtr end_of_wal, + XLogRecPtr scan_cutoff_lsn); extern XLogRecPtr LogicalSlotAdvanceAndCheckSnapState(XLogRecPtr moveto, bool *found_consistent_snapshot); + +/* + * This macro determines the log level for messages about starting logical + * decoding and finding a consistent point. + * + * When logical decoding is triggered by a foreground SQL function (e.g., + * pg_logical_slot_peek_binary_changes()), these messages are logged at DEBUG1 + * to avoid excessive log noise. This is acceptable since such issues are + * typically less critical, and the messages can still be enabled by lowering + * client_min_messages or log_min_messages for the session. + * + * When the messages originate from background activity (e.g., walsender or + * slotsync worker), they are logged at LOG, as these events are less frequent + * and more likely to indicate issues that DBAs should notice by default. + * + * Note that SQL functions executed by the logical walsender are treated as + * background activity. + */ +#define LogicalDecodingLogLevel() \ + (AmRegularBackendProcess() ? DEBUG1 : LOG) + #endif diff --git a/src/include/replication/logicalctl.h b/src/include/replication/logicalctl.h new file mode 100644 index 0000000000000..0bc1302f13083 --- /dev/null +++ b/src/include/replication/logicalctl.h @@ -0,0 +1,30 @@ +/*------------------------------------------------------------------------- + * + * logicalctl.h + * Definitions for logical decoding status control facility. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/include/replication/logicalctl.h + * + *------------------------------------------------------------------------- + */ +#ifndef LOGICALCTL_H +#define LOGICALCTL_H + +extern void StartupLogicalDecodingStatus(bool last_status); +extern void InitializeProcessXLogLogicalInfo(void); +extern bool ProcessBarrierUpdateXLogLogicalInfo(void); +extern bool IsLogicalDecodingEnabled(void); +extern bool IsXLogLogicalInfoEnabled(void); +extern void AtEOXact_LogicalCtl(void); +extern void EnsureLogicalDecodingEnabled(void); +extern void EnableLogicalDecoding(void); +extern void RequestDisableLogicalDecoding(void); +extern void DisableLogicalDecodingIfNecessary(void); +extern void DisableLogicalDecoding(void); +extern void UpdateLogicalDecodingStatusEndOfRecovery(void); + +#endif diff --git a/src/include/replication/logicallauncher.h b/src/include/replication/logicallauncher.h index 82b202f330515..5f0c1b9c68274 100644 --- a/src/include/replication/logicallauncher.h +++ b/src/include/replication/logicallauncher.h @@ -3,7 +3,7 @@ * logicallauncher.h * Exports for logical replication launcher. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/replication/logicallauncher.h * @@ -19,14 +19,14 @@ extern PGDLLIMPORT int max_parallel_apply_workers_per_subscription; extern void ApplyLauncherRegister(void); extern void ApplyLauncherMain(Datum main_arg); -extern Size ApplyLauncherShmemSize(void); -extern void ApplyLauncherShmemInit(void); - extern void ApplyLauncherForgetWorkerStartTime(Oid subid); extern void ApplyLauncherWakeupAtCommit(void); +extern void ApplyLauncherWakeup(void); extern void AtEOXact_ApplyLauncher(bool isCommit); +extern void CreateConflictDetectionSlot(void); + extern bool IsLogicalLauncher(void); extern pid_t GetLeaderApplyWorkerPid(pid_t pid); diff --git a/src/include/replication/logicalproto.h b/src/include/replication/logicalproto.h index b261c60d3fa14..058a955e20caf 100644 --- a/src/include/replication/logicalproto.h +++ b/src/include/replication/logicalproto.h @@ -3,7 +3,7 @@ * logicalproto.h * logical replication protocol * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/replication/logicalproto.h diff --git a/src/include/replication/logicalrelation.h b/src/include/replication/logicalrelation.h index 7a561a8e8d829..efe0f9d603171 100644 --- a/src/include/replication/logicalrelation.h +++ b/src/include/replication/logicalrelation.h @@ -3,7 +3,7 @@ * logicalrelation.h * Relation definitions for logical replication relation mapping. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/replication/logicalrelation.h * diff --git a/src/include/replication/logicalworker.h b/src/include/replication/logicalworker.h index 88912606e4d58..7d748a28da82b 100644 --- a/src/include/replication/logicalworker.h +++ b/src/include/replication/logicalworker.h @@ -3,7 +3,7 @@ * logicalworker.h * Exports for logical replication workers. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/replication/logicalworker.h * @@ -18,7 +18,8 @@ extern PGDLLIMPORT volatile sig_atomic_t ParallelApplyMessagePending; extern void ApplyWorkerMain(Datum main_arg); extern void ParallelApplyWorkerMain(Datum main_arg); -extern void TablesyncWorkerMain(Datum main_arg); +extern void TableSyncWorkerMain(Datum main_arg); +extern void SequenceSyncWorkerMain(Datum main_arg); extern bool IsLogicalWorker(void); extern bool IsLogicalParallelApplyWorker(void); diff --git a/src/include/replication/message.h b/src/include/replication/message.h index 8a0893ea53961..d1c7275508460 100644 --- a/src/include/replication/message.h +++ b/src/include/replication/message.h @@ -2,7 +2,7 @@ * message.h * Exports from replication/logical/message.c * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * src/include/replication/message.h *------------------------------------------------------------------------- diff --git a/src/include/replication/origin.h b/src/include/replication/origin.h index 2a73f6aa49296..a69faf6eaaf68 100644 --- a/src/include/replication/origin.h +++ b/src/include/replication/origin.h @@ -2,7 +2,7 @@ * origin.h * Exports from replication/logical/origin.c * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * src/include/replication/origin.h *------------------------------------------------------------------------- @@ -18,19 +18,19 @@ typedef struct xl_replorigin_set { XLogRecPtr remote_lsn; - RepOriginId node_id; + ReplOriginId node_id; bool force; } xl_replorigin_set; typedef struct xl_replorigin_drop { - RepOriginId node_id; + ReplOriginId node_id; } xl_replorigin_drop; #define XLOG_REPLORIGIN_SET 0x00 #define XLOG_REPLORIGIN_DROP 0x10 -#define InvalidRepOriginId 0 +#define InvalidReplOriginId 0 #define DoNotReplicateId PG_UINT16_MAX /* @@ -40,33 +40,41 @@ typedef struct xl_replorigin_drop */ #define MAX_RONAME_LEN 512 -extern PGDLLIMPORT RepOriginId replorigin_session_origin; -extern PGDLLIMPORT XLogRecPtr replorigin_session_origin_lsn; -extern PGDLLIMPORT TimestampTz replorigin_session_origin_timestamp; +typedef struct ReplOriginXactState +{ + ReplOriginId origin; + XLogRecPtr origin_lsn; + TimestampTz origin_timestamp; +} ReplOriginXactState; + +extern PGDLLIMPORT ReplOriginXactState replorigin_xact_state; /* GUCs */ extern PGDLLIMPORT int max_active_replication_origins; /* API for querying & manipulating replication origins */ -extern RepOriginId replorigin_by_name(const char *roname, bool missing_ok); -extern RepOriginId replorigin_create(const char *roname); +extern ReplOriginId replorigin_by_name(const char *roname, bool missing_ok); +extern ReplOriginId replorigin_create(const char *roname); extern void replorigin_drop_by_name(const char *name, bool missing_ok, bool nowait); -extern bool replorigin_by_oid(RepOriginId roident, bool missing_ok, +extern bool replorigin_by_oid(ReplOriginId roident, bool missing_ok, char **roname); /* API for querying & manipulating replication progress tracking */ -extern void replorigin_advance(RepOriginId node, +extern void replorigin_advance(ReplOriginId node, XLogRecPtr remote_commit, XLogRecPtr local_commit, bool go_backward, bool wal_log); -extern XLogRecPtr replorigin_get_progress(RepOriginId node, bool flush); +extern XLogRecPtr replorigin_get_progress(ReplOriginId node, bool flush); extern void replorigin_session_advance(XLogRecPtr remote_commit, XLogRecPtr local_commit); -extern void replorigin_session_setup(RepOriginId node, int acquired_by); +extern void replorigin_session_setup(ReplOriginId node, int acquired_by); extern void replorigin_session_reset(void); extern XLogRecPtr replorigin_session_get_progress(bool flush); +/* Per-transaction replication origin state manipulation */ +extern void replorigin_xact_clear(bool clear_origin); + /* Checkpoint/Startup integration */ extern void CheckPointReplicationOrigin(void); extern void StartupReplicationOrigin(void); @@ -76,8 +84,4 @@ extern void replorigin_redo(XLogReaderState *record); extern void replorigin_desc(StringInfo buf, XLogReaderState *record); extern const char *replorigin_identify(uint8 info); -/* shared memory allocation */ -extern Size ReplicationOriginShmemSize(void); -extern void ReplicationOriginShmemInit(void); - #endif /* PG_ORIGIN_H */ diff --git a/src/include/replication/output_plugin.h b/src/include/replication/output_plugin.h index 8d4d5b71887d2..917f3cff2320a 100644 --- a/src/include/replication/output_plugin.h +++ b/src/include/replication/output_plugin.h @@ -2,7 +2,7 @@ * output_plugin.h * PostgreSQL Logical Decode Plugin Interface * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -27,6 +27,7 @@ typedef struct OutputPluginOptions { OutputPluginOutputType output_type; bool receive_rewrites; + bool need_shared_catalogs; } OutputPluginOptions; /* @@ -94,7 +95,7 @@ typedef void (*LogicalDecodeMessageCB) (struct LogicalDecodingContext *ctx, * Filter changes by origin. */ typedef bool (*LogicalDecodeFilterByOriginCB) (struct LogicalDecodingContext *ctx, - RepOriginId origin_id); + ReplOriginId origin_id); /* * Called to shutdown an output plugin. diff --git a/src/include/replication/pgoutput.h b/src/include/replication/pgoutput.h index e53456c26073d..774ebe53fa4c3 100644 --- a/src/include/replication/pgoutput.h +++ b/src/include/replication/pgoutput.h @@ -3,7 +3,7 @@ * pgoutput.h * Logical Replication output plugin * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/replication/pgoutput.h diff --git a/src/include/replication/reorderbuffer.h b/src/include/replication/reorderbuffer.h index 24e88c409ba7e..ff825e4b7b238 100644 --- a/src/include/replication/reorderbuffer.h +++ b/src/include/replication/reorderbuffer.h @@ -2,7 +2,7 @@ * reorderbuffer.h * PostgreSQL logical replay/reorder buffer management. * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * src/include/replication/reorderbuffer.h */ @@ -83,7 +83,7 @@ typedef struct ReorderBufferChange /* Transaction this change belongs to. */ struct ReorderBufferTXN *txn; - RepOriginId origin_id; + ReplOriginId origin_id; /* * Context data for the change. Which part of the union is valid depends @@ -176,6 +176,7 @@ typedef struct ReorderBufferChange #define RBTXN_SENT_PREPARE 0x0200 #define RBTXN_IS_COMMITTED 0x0400 #define RBTXN_IS_ABORTED 0x0800 +#define RBTXN_DISTR_INVAL_OVERFLOWED 0x1000 #define RBTXN_PREPARE_STATUS_MASK (RBTXN_IS_PREPARED | RBTXN_SKIPPED_PREPARE | RBTXN_SENT_PREPARE) @@ -265,6 +266,12 @@ typedef struct ReorderBufferChange ((txn)->txn_flags & RBTXN_SKIPPED_PREPARE) != 0 \ ) +/* Is the array of distributed inval messages overflowed? */ +#define rbtxn_distr_inval_overflowed(txn) \ +( \ + ((txn)->txn_flags & RBTXN_DISTR_INVAL_OVERFLOWED) != 0 \ +) + /* Is this a top-level transaction? */ #define rbtxn_is_toptxn(txn) \ ( \ @@ -286,7 +293,7 @@ typedef struct ReorderBufferChange typedef struct ReorderBufferTXN { /* See above */ - bits32 txn_flags; + uint32 txn_flags; /* The transaction's transaction id, can be a toplevel or sub xid. */ TransactionId xid; @@ -340,7 +347,7 @@ typedef struct ReorderBufferTXN XLogRecPtr restart_decoding_lsn; /* origin of the change that caused this transaction */ - RepOriginId origin_id; + ReplOriginId origin_id; XLogRecPtr origin_lsn; /* @@ -352,7 +359,7 @@ typedef struct ReorderBufferTXN TimestampTz commit_time; TimestampTz prepare_time; TimestampTz abort_time; - } xact_time; + }; /* * The base snapshot is used to decode all changes until either this @@ -422,6 +429,12 @@ typedef struct ReorderBufferTXN uint32 ninvalidations; SharedInvalidationMessage *invalidations; + /* + * Stores cache invalidation messages distributed by other transactions. + */ + uint32 ninvalidations_distributed; + SharedInvalidationMessage *invalidations_distributed; + /* --- * Position in one of two lists: * * list of subtransactions if we are *known* to be subxact @@ -677,6 +690,9 @@ struct ReorderBuffer int64 streamCount; /* streaming invocation counter */ int64 streamBytes; /* amount of data decoded */ + /* Number of times the logical_decoding_work_mem limit has been reached */ + int64 memExceededCount; + /* * Statistics about all the transactions sent to the decoding output * plugin @@ -708,12 +724,12 @@ extern void ReorderBufferQueueMessage(ReorderBuffer *rb, TransactionId xid, Size message_size, const char *message); extern void ReorderBufferCommit(ReorderBuffer *rb, TransactionId xid, XLogRecPtr commit_lsn, XLogRecPtr end_lsn, - TimestampTz commit_time, RepOriginId origin_id, XLogRecPtr origin_lsn); + TimestampTz commit_time, ReplOriginId origin_id, XLogRecPtr origin_lsn); extern void ReorderBufferFinishPrepared(ReorderBuffer *rb, TransactionId xid, XLogRecPtr commit_lsn, XLogRecPtr end_lsn, XLogRecPtr two_phase_at, TimestampTz commit_time, - RepOriginId origin_id, XLogRecPtr origin_lsn, + ReplOriginId origin_id, XLogRecPtr origin_lsn, char *gid, bool is_commit); extern void ReorderBufferAssignChild(ReorderBuffer *rb, TransactionId xid, TransactionId subxid, XLogRecPtr lsn); @@ -738,6 +754,9 @@ extern void ReorderBufferAddNewTupleCids(ReorderBuffer *rb, TransactionId xid, CommandId cmin, CommandId cmax, CommandId combocid); extern void ReorderBufferAddInvalidations(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn, Size nmsgs, SharedInvalidationMessage *msgs); +extern void ReorderBufferAddDistributedInvalidations(ReorderBuffer *rb, TransactionId xid, + XLogRecPtr lsn, Size nmsgs, + SharedInvalidationMessage *msgs); extern void ReorderBufferImmediateInvalidation(ReorderBuffer *rb, uint32 ninvalidations, SharedInvalidationMessage *invalidations); extern void ReorderBufferProcessXid(ReorderBuffer *rb, TransactionId xid, XLogRecPtr lsn); @@ -749,7 +768,7 @@ extern bool ReorderBufferXidHasBaseSnapshot(ReorderBuffer *rb, TransactionId xid extern bool ReorderBufferRememberPrepareInfo(ReorderBuffer *rb, TransactionId xid, XLogRecPtr prepare_lsn, XLogRecPtr end_lsn, TimestampTz prepare_time, - RepOriginId origin_id, XLogRecPtr origin_lsn); + ReplOriginId origin_id, XLogRecPtr origin_lsn); extern void ReorderBufferSkipPrepare(ReorderBuffer *rb, TransactionId xid); extern void ReorderBufferPrepare(ReorderBuffer *rb, TransactionId xid, char *gid); extern ReorderBufferTXN *ReorderBufferGetOldestTXN(ReorderBuffer *rb); diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h index eb0b93b11141d..77c8d0975b682 100644 --- a/src/include/replication/slot.h +++ b/src/include/replication/slot.h @@ -2,7 +2,7 @@ * slot.h * Replication slot management. * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * *------------------------------------------------------------------------- */ @@ -20,6 +20,13 @@ /* directory to store replication slot data in */ #define PG_REPLSLOT_DIR "pg_replslot" +/* + * The reserved name for a replication slot used to retain dead tuples for + * conflict detection in logical replication. See + * maybe_advance_nonremovable_xid() for detail. + */ +#define CONFLICT_DETECTION_SLOT "pg_conflict_detection" + /* * Behaviour of replication slots, upon release or crash. * @@ -64,6 +71,24 @@ typedef enum ReplicationSlotInvalidationCause /* Maximum number of invalidation causes */ #define RS_INVAL_MAX_CAUSES 4 +/* + * When the slot synchronization worker is running, or when + * pg_sync_replication_slots is executed, slot synchronization may be + * skipped. This enum defines the possible reasons for skipping slot + * synchronization. + */ +typedef enum SlotSyncSkipReason +{ + SS_SKIP_NONE, /* No skip */ + SS_SKIP_WAL_NOT_FLUSHED, /* Standby did not flush the wal corresponding + * to confirmed flush of remote slot */ + SS_SKIP_WAL_OR_ROWS_REMOVED, /* Remote slot is behind; required WAL or + * rows may be removed or at risk */ + SS_SKIP_NO_CONSISTENT_SNAPSHOT, /* Standby could not build a consistent + * snapshot */ + SS_SKIP_INVALID /* Local slot is invalid */ +} SlotSyncSkipReason; + /* * On-Disk data of a replication slot, preserved across restarts. */ @@ -127,7 +152,7 @@ typedef struct ReplicationSlotPersistentData /* * Was this slot synchronized from the primary server? */ - char synced; + bool synced; /* * Is this a failover slot (sync candidate for standbys)? Only relevant @@ -160,8 +185,11 @@ typedef struct ReplicationSlot /* is this slot defined */ bool in_use; - /* Who is streaming out changes for this slot? 0 in unused slots. */ - pid_t active_pid; + /* + * Who is streaming out changes for this slot? INVALID_PROC_NUMBER in + * unused slots. + */ + ProcNumber active_proc; /* any outstanding modifications? */ bool just_dirtied; @@ -187,7 +215,7 @@ typedef struct ReplicationSlot /* is somebody performing io on this slot? */ LWLock io_in_progress_lock; - /* Condition variable signaled when active_pid changes */ + /* Condition variable signaled when active_proc changes */ ConditionVariable active_cv; /* all the remaining data is only used for logical slots */ @@ -215,6 +243,45 @@ typedef struct ReplicationSlot * recently stopped. */ TimestampTz inactive_since; + + /* + * Latest restart_lsn that has been flushed to disk. For persistent slots + * the flushed LSN should be taken into account when calculating the + * oldest LSN for WAL segments removal. + * + * Do not assume that restart_lsn will always move forward, i.e., that the + * previously flushed restart_lsn is always behind data.restart_lsn. In + * streaming replication using a physical slot, the restart_lsn is updated + * based on the flushed WAL position reported by the walreceiver. + * + * This replication mode allows duplicate WAL records to be received and + * overwritten. If the walreceiver receives older WAL records and then + * reports them as flushed to the walsender, the restart_lsn may appear to + * move backward. + * + * This typically occurs at the beginning of replication. One reason is + * that streaming replication starts at the beginning of a segment, so, if + * restart_lsn is in the middle of a segment, it will be updated to an + * earlier LSN, see RequestXLogStreaming. Another reason is that the + * walreceiver chooses its startpoint based on the replayed LSN, so, if + * some records have been received but not yet applied, they will be + * received again and leads to updating the restart_lsn to an earlier + * position. + */ + XLogRecPtr last_saved_restart_lsn; + + /* + * Reason for the most recent slot synchronization skip. + * + * Slot sync skips can occur for both temporary and persistent replication + * slots. They are more common for temporary slots, but persistent slots + * may also skip synchronization in rare cases (e.g., + * SS_SKIP_WAL_NOT_FLUSHED or SS_SKIP_WAL_OR_ROWS_REMOVED). + * + * Since, temporary slots are dropped after server restart, persisting + * slotsync_skip_reason provides no practical benefit. + */ + SlotSyncSkipReason slotsync_skip_reason; } ReplicationSlot; #define SlotIsPhysical(slot) ((slot)->data.database == InvalidOid) @@ -257,17 +324,14 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot; /* GUCs */ extern PGDLLIMPORT int max_replication_slots; +extern PGDLLIMPORT int max_repack_replication_slots; extern PGDLLIMPORT char *synchronized_standby_slots; -extern PGDLLIMPORT int idle_replication_slot_timeout_mins; - -/* shmem initialization functions */ -extern Size ReplicationSlotsShmemSize(void); -extern void ReplicationSlotsShmemInit(void); +extern PGDLLIMPORT int idle_replication_slot_timeout_secs; /* management of individual slots */ extern void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, + bool two_phase, bool repack, bool failover, bool synced); extern void ReplicationSlotPersist(void); extern void ReplicationSlotDrop(const char *name, bool nowait); @@ -284,12 +348,18 @@ extern void ReplicationSlotMarkDirty(void); /* misc stuff */ extern void ReplicationSlotInitialize(void); -extern bool ReplicationSlotValidateName(const char *name, int elevel); +extern bool ReplicationSlotValidateName(const char *name, + bool allow_reserved_name, + int elevel); +extern bool ReplicationSlotValidateNameInternal(const char *name, + bool allow_reserved_name, + int *err_code, char **err_msg, char **err_hint); extern void ReplicationSlotReserveWal(void); extern void ReplicationSlotsComputeRequiredXmin(bool already_locked); extern void ReplicationSlotsComputeRequiredLSN(void); extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void); extern bool ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive); +extern bool CheckLogicalSlotExists(void); extern void ReplicationSlotsDropDBSlots(Oid dboid); extern bool InvalidateObsoleteReplicationSlots(uint32 possible_causes, XLogSegNo oldestSegno, @@ -304,7 +374,7 @@ extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname extern void StartupReplicationSlots(void); extern void CheckPointReplicationSlots(bool is_shutdown); -extern void CheckSlotRequirements(void); +extern void CheckSlotRequirements(bool repack); extern void CheckSlotPermissions(void); extern ReplicationSlotInvalidationCause GetSlotInvalidationCause(const char *cause_name); diff --git a/src/include/replication/slotsync.h b/src/include/replication/slotsync.h index 16b721463dde6..a55d1f0dccc89 100644 --- a/src/include/replication/slotsync.h +++ b/src/include/replication/slotsync.h @@ -3,7 +3,7 @@ * slotsync.h * Exports for slot synchronization. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/replication/slotsync.h * @@ -12,10 +12,15 @@ #ifndef SLOTSYNC_H #define SLOTSYNC_H +#include + #include "replication/walreceiver.h" extern PGDLLIMPORT bool sync_replication_slots; +/* Interrupt flag set by HandleSlotSyncMessageInterrupt() */ +extern PGDLLIMPORT volatile sig_atomic_t SlotSyncShutdownPending; + /* * GUCs needed by slot sync worker to connect to the primary * server and carry on with slots synchronization. @@ -31,8 +36,8 @@ pg_noreturn extern void ReplSlotSyncWorkerMain(const void *startup_data, size_t extern void ShutDownSlotSync(void); extern bool SlotSyncWorkerCanRestart(void); extern bool IsSyncingReplicationSlots(void); -extern Size SlotSyncShmemSize(void); -extern void SlotSyncShmemInit(void); extern void SyncReplicationSlots(WalReceiverConn *wrconn); +extern void HandleSlotSyncMessageInterrupt(void); +extern void ProcessSlotSyncMessage(void); #endif /* SLOTSYNC_H */ diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h index 44031dcf6e368..d02530a912a0c 100644 --- a/src/include/replication/snapbuild.h +++ b/src/include/replication/snapbuild.h @@ -3,7 +3,7 @@ * snapbuild.h * Exports from replication/logical/snapbuild.c. * - * Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Copyright (c) 2012-2026, PostgreSQL Global Development Group * * src/include/replication/snapbuild.h * @@ -15,6 +15,14 @@ #include "access/xlogdefs.h" #include "utils/snapmgr.h" +/* + * forward declarations in this file + */ +typedef struct ReorderBuffer ReorderBuffer; +typedef struct SnapBuild SnapBuild; +typedef struct xl_heap_new_cid xl_heap_new_cid; +typedef struct xl_running_xacts xl_running_xacts; + /* * Please keep get_snapbuild_state_desc() (located in the pg_logicalinspect * module) updated if a change needs to be made to SnapBuildState. @@ -50,20 +58,11 @@ typedef enum SNAPBUILD_CONSISTENT = 2, } SnapBuildState; -/* forward declare so we don't have to include snapbuild_internal.h */ -struct SnapBuild; -typedef struct SnapBuild SnapBuild; - -/* forward declare so we don't have to include reorderbuffer.h */ -struct ReorderBuffer; -/* forward declare so we don't have to include heapam_xlog.h */ -struct xl_heap_new_cid; -struct xl_running_xacts; extern void CheckPointSnapBuild(void); -extern SnapBuild *AllocateSnapshotBuilder(struct ReorderBuffer *reorder, +extern SnapBuild *AllocateSnapshotBuilder(ReorderBuffer *reorder, TransactionId xmin_horizon, XLogRecPtr start_lsn, bool need_full_snapshot, bool in_slot_creation, @@ -91,9 +90,10 @@ extern bool SnapBuildProcessChange(SnapBuild *builder, TransactionId xid, XLogRecPtr lsn); extern void SnapBuildProcessNewCid(SnapBuild *builder, TransactionId xid, XLogRecPtr lsn, - struct xl_heap_new_cid *xlrec); + xl_heap_new_cid *xlrec); extern void SnapBuildProcessRunningXacts(SnapBuild *builder, XLogRecPtr lsn, - struct xl_running_xacts *running); + xl_running_xacts *running, + bool db_specific); extern void SnapBuildSerializationPoint(SnapBuild *builder, XLogRecPtr lsn); extern bool SnapBuildSnapshotExists(XLogRecPtr lsn); diff --git a/src/include/replication/snapbuild_internal.h b/src/include/replication/snapbuild_internal.h index 3b915dc8793b8..363f7f5977cd6 100644 --- a/src/include/replication/snapbuild_internal.h +++ b/src/include/replication/snapbuild_internal.h @@ -4,7 +4,7 @@ * This file contains declarations for logical decoding utility * functions for internal use. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * src/include/replication/snapbuild_internal.h * diff --git a/src/include/replication/syncrep.h b/src/include/replication/syncrep.h index 675669a79f7d3..b42b5862a70a3 100644 --- a/src/include/replication/syncrep.h +++ b/src/include/replication/syncrep.h @@ -3,7 +3,7 @@ * syncrep.h * Exports from replication/syncrep.c. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/replication/syncrep.h @@ -97,13 +97,10 @@ extern void SyncRepUpdateSyncStandbysDefined(void); * in syncrep_gram.y and syncrep_scanner.l */ union YYSTYPE; -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif extern int syncrep_yyparse(SyncRepConfigData **syncrep_parse_result_p, char **syncrep_parse_error_msg_p, yyscan_t yyscanner); extern int syncrep_yylex(union YYSTYPE *yylval_param, char **syncrep_parse_error_msg_p, yyscan_t yyscanner); -extern void syncrep_yyerror(SyncRepConfigData **syncrep_parse_result_p, char **syncrep_parse_error_msg_p, yyscan_t yyscanner, const char *str); +extern void syncrep_yyerror(SyncRepConfigData **syncrep_parse_result_p, char **syncrep_parse_error_msg_p, yyscan_t yyscanner, const char *message); extern void syncrep_scanner_init(const char *str, yyscan_t *yyscannerp); extern void syncrep_scanner_finish(yyscan_t yyscanner); diff --git a/src/include/replication/walreceiver.h b/src/include/replication/walreceiver.h index 89f63f908f8a8..47c07574d4d82 100644 --- a/src/include/replication/walreceiver.h +++ b/src/include/replication/walreceiver.h @@ -3,7 +3,7 @@ * walreceiver.h * Exports from replication/walreceiverfuncs.c. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/replication/walreceiver.h * @@ -47,6 +47,7 @@ typedef enum WALRCV_STOPPED, /* stopped and mustn't start up again */ WALRCV_STARTING, /* launched, but the process hasn't * initialized yet */ + WALRCV_CONNECTING, /* connecting to upstream server */ WALRCV_STREAMING, /* walreceiver is streaming */ WALRCV_WAITING, /* stopped streaming, waiting for orders */ WALRCV_RESTARTING, /* asked to restart streaming */ @@ -155,11 +156,11 @@ typedef struct pg_atomic_uint64 writtenUpto; /* - * force walreceiver reply? This doesn't need to be locked; memory + * request walreceiver reply? This doesn't need to be locked; memory * barriers for ordering are sufficient. But we do need atomic fetch and * store semantics, so use sig_atomic_t. */ - sig_atomic_t force_reply; /* used as a bool */ + sig_atomic_t apply_reply_requested; /* used as a bool */ } WalRcvData; extern PGDLLIMPORT WalRcvData *WalRcv; @@ -487,14 +488,13 @@ walrcv_clear_result(WalRcvExecResult *walres) /* prototypes for functions in walreceiver.c */ pg_noreturn extern void WalReceiverMain(const void *startup_data, size_t startup_data_len); -extern void WalRcvForceReply(void); +extern void WalRcvRequestApplyReply(void); /* prototypes for functions in walreceiverfuncs.c */ -extern Size WalRcvShmemSize(void); -extern void WalRcvShmemInit(void); extern void ShutdownWalRcv(void); extern bool WalRcvStreaming(void); extern bool WalRcvRunning(void); +extern WalRcvState WalRcvGetState(void); extern void RequestXLogStreaming(TimeLineID tli, XLogRecPtr recptr, const char *conninfo, const char *slotname, bool create_temp_slot); diff --git a/src/include/replication/walsender.h b/src/include/replication/walsender.h index c3e8e19133951..386cedfc7aa10 100644 --- a/src/include/replication/walsender.h +++ b/src/include/replication/walsender.h @@ -3,7 +3,7 @@ * walsender.h * Exports from replication/walsender.c. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/replication/walsender.h * @@ -33,6 +33,7 @@ extern PGDLLIMPORT bool wake_wal_senders; /* user-settable parameters */ extern PGDLLIMPORT int max_wal_senders; extern PGDLLIMPORT int wal_sender_timeout; +extern PGDLLIMPORT int wal_sender_shutdown_timeout; extern PGDLLIMPORT bool log_replication_commands; extern void InitWalSender(void); @@ -41,8 +42,6 @@ extern void WalSndErrorCleanup(void); extern void PhysicalWakeupLogicalWalSnd(void); extern XLogRecPtr GetStandbyFlushRecPtr(TimeLineID *tli); extern void WalSndSignals(void); -extern Size WalSndShmemSize(void); -extern void WalSndShmemInit(void); extern void WalSndWakeup(bool physical, bool logical); extern void WalSndInitStopping(void); extern void WalSndWaitStopping(void); diff --git a/src/include/replication/walsender_private.h b/src/include/replication/walsender_private.h index e98701038f506..b0c80deeb24de 100644 --- a/src/include/replication/walsender_private.h +++ b/src/include/replication/walsender_private.h @@ -3,7 +3,7 @@ * walsender_private.h * Private definitions from replication/walsender.c. * - * Portions Copyright (c) 2010-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2010-2026, PostgreSQL Global Development Group * * src/include/replication/walsender_private.h * @@ -100,7 +100,7 @@ typedef struct * can't reload the config file safely, so checkpointer updates this value * as needed. Protected by SyncRepLock. */ - bits8 sync_standbys_status; + uint8 sync_standbys_status; /* used as a registry of physical / logical walsenders to wake */ ConditionVariable wal_flush_cv; @@ -141,10 +141,7 @@ extern void WalSndSetState(WalSndState state); * repl_scanner.l */ union YYSTYPE; -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif extern int replication_yyparse(Node **replication_parse_result_p, yyscan_t yyscanner); extern int replication_yylex(union YYSTYPE *yylval_param, yyscan_t yyscanner); pg_noreturn extern void replication_yyerror(Node **replication_parse_result_p, yyscan_t yyscanner, const char *message); diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h index 30b2775952c38..745b7d9e96966 100644 --- a/src/include/replication/worker_internal.h +++ b/src/include/replication/worker_internal.h @@ -3,7 +3,7 @@ * worker_internal.h * Internal headers shared by logical replication workers. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * src/include/replication/worker_internal.h * @@ -20,7 +20,6 @@ #include "replication/walreceiver.h" #include "storage/buffile.h" #include "storage/fileset.h" -#include "storage/lock.h" #include "storage/shm_mq.h" #include "storage/shm_toc.h" #include "storage/spin.h" @@ -30,6 +29,7 @@ typedef enum LogicalRepWorkerType { WORKERTYPE_UNKNOWN = 0, WORKERTYPE_TABLESYNC, + WORKERTYPE_SEQUENCESYNC, WORKERTYPE_APPLY, WORKERTYPE_PARALLEL_APPLY, } LogicalRepWorkerType; @@ -86,12 +86,28 @@ typedef struct LogicalRepWorker /* Indicates whether apply can be performed in parallel. */ bool parallel_apply; + /* + * Changes made by this transaction and subsequent ones must be preserved. + * This ensures that update_deleted conflicts can be accurately detected + * during the apply phase of logical replication by this worker. + * + * The logical replication launcher manages an internal replication slot + * named "pg_conflict_detection". It asynchronously collects this ID to + * decide when to advance the xmin value of the slot. + * + * This ID is set to InvalidTransactionId when the apply worker stops + * retaining information needed for conflict detection. + */ + TransactionId oldest_nonremovable_xid; + /* Stats. */ XLogRecPtr last_lsn; TimestampTz last_send_time; TimestampTz last_recv_time; XLogRecPtr reply_lsn; TimestampTz reply_time; + + TimestampTz last_seqsync_start_time; } LogicalRepWorker; /* @@ -237,31 +253,48 @@ extern PGDLLIMPORT bool in_remote_transaction; extern PGDLLIMPORT bool InitializingApplyWorker; +extern PGDLLIMPORT List *table_states_not_ready; + extern void logicalrep_worker_attach(int slot); -extern LogicalRepWorker *logicalrep_worker_find(Oid subid, Oid relid, +extern LogicalRepWorker *logicalrep_worker_find(LogicalRepWorkerType wtype, + Oid subid, Oid relid, bool only_running); extern List *logicalrep_workers_find(Oid subid, bool only_running, bool acquire_lock); extern bool logicalrep_worker_launch(LogicalRepWorkerType wtype, Oid dbid, Oid subid, const char *subname, Oid userid, Oid relid, - dsm_handle subworker_dsm); -extern void logicalrep_worker_stop(Oid subid, Oid relid); + dsm_handle subworker_dsm, + bool retain_dead_tuples); +extern void logicalrep_worker_stop(LogicalRepWorkerType wtype, Oid subid, + Oid relid); extern void logicalrep_pa_worker_stop(ParallelApplyWorkerInfo *winfo); -extern void logicalrep_worker_wakeup(Oid subid, Oid relid); +extern void logicalrep_worker_wakeup(LogicalRepWorkerType wtype, Oid subid, + Oid relid); extern void logicalrep_worker_wakeup_ptr(LogicalRepWorker *worker); +extern void logicalrep_reset_seqsync_start_time(void); extern int logicalrep_sync_worker_count(Oid subid); extern void ReplicationOriginNameForLogicalRep(Oid suboid, Oid relid, char *originname, Size szoriginname); extern bool AllTablesyncsReady(void); +extern bool HasSubscriptionTablesCached(void); extern void UpdateTwoPhaseState(Oid suboid, char new_state); -extern void process_syncing_tables(XLogRecPtr current_lsn); -extern void invalidate_syncing_table_states(Datum arg, int cacheid, - uint32 hashvalue); +extern void ProcessSyncingTablesForSync(XLogRecPtr current_lsn); +extern void ProcessSyncingTablesForApply(XLogRecPtr current_lsn); +extern void ProcessSequencesForSync(void); + +pg_noreturn extern void FinishSyncWorker(void); +extern void InvalidateSyncingRelStates(Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); +extern void launch_sync_worker(LogicalRepWorkerType wtype, int nsyncworkers, + Oid relid, TimestampTz *last_start_time); +extern void ProcessSyncingRelations(XLogRecPtr current_lsn); +extern void FetchRelationStates(bool *has_pending_subtables, + bool *has_pending_subsequences, bool *started_tx); extern void stream_start_internal(TransactionId xid, bool first_segment); extern void stream_stop_internal(TransactionId xid); @@ -328,13 +361,21 @@ extern void pa_xact_finish(ParallelApplyWorkerInfo *winfo, #define isParallelApplyWorker(worker) ((worker)->in_use && \ (worker)->type == WORKERTYPE_PARALLEL_APPLY) -#define isTablesyncWorker(worker) ((worker)->in_use && \ +#define isTableSyncWorker(worker) ((worker)->in_use && \ (worker)->type == WORKERTYPE_TABLESYNC) +#define isSequenceSyncWorker(worker) ((worker)->in_use && \ + (worker)->type == WORKERTYPE_SEQUENCESYNC) static inline bool am_tablesync_worker(void) { - return isTablesyncWorker(MyLogicalRepWorker); + return isTableSyncWorker(MyLogicalRepWorker); +} + +static inline bool +am_sequencesync_worker(void) +{ + return isSequenceSyncWorker(MyLogicalRepWorker); } static inline bool @@ -351,4 +392,11 @@ am_parallel_apply_worker(void) return isParallelApplyWorker(MyLogicalRepWorker); } +static inline LogicalRepWorkerType +get_logical_worker_type(void) +{ + Assert(MyLogicalRepWorker->in_use); + return MyLogicalRepWorker->type; +} + #endif /* WORKER_INTERNAL_H */ diff --git a/src/include/rewrite/prs2lock.h b/src/include/rewrite/prs2lock.h index 17f468b351245..17ee88881f5ee 100644 --- a/src/include/rewrite/prs2lock.h +++ b/src/include/rewrite/prs2lock.h @@ -3,7 +3,7 @@ * prs2lock.h * data structures for POSTGRES Rule System II (rewrite rules only) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/prs2lock.h diff --git a/src/include/rewrite/rewriteDefine.h b/src/include/rewrite/rewriteDefine.h index 63842e08649e0..684f501293c43 100644 --- a/src/include/rewrite/rewriteDefine.h +++ b/src/include/rewrite/rewriteDefine.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/rewriteDefine.h diff --git a/src/include/rewrite/rewriteGraphTable.h b/src/include/rewrite/rewriteGraphTable.h new file mode 100644 index 0000000000000..2b3be1528e3ce --- /dev/null +++ b/src/include/rewrite/rewriteGraphTable.h @@ -0,0 +1,21 @@ +/*------------------------------------------------------------------------- + * + * rewriteGraphTable.h + * Support for rewriting GRAPH_TABLE clauses. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/rewrite/rewriteGraphTable.h + * + *------------------------------------------------------------------------- + */ +#ifndef REWRITEGRAPHTABLE_H +#define REWRITEGRAPHTABLE_H + +#include "nodes/parsenodes.h" + +extern Query *rewriteGraphTable(Query *parsetree, int rt_index); + +#endif /* REWRITEGRAPHTABLE_H */ diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h index 99cab1a3bfad6..e31373b00ed2d 100644 --- a/src/include/rewrite/rewriteHandler.h +++ b/src/include/rewrite/rewriteHandler.h @@ -4,7 +4,7 @@ * External interface to query rewriter. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/rewriteHandler.h diff --git a/src/include/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h index 7c018f2a4e358..a6d4e888e0600 100644 --- a/src/include/rewrite/rewriteManip.h +++ b/src/include/rewrite/rewriteManip.h @@ -4,7 +4,7 @@ * Querytree manipulation subroutines for query rewriter. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/rewriteManip.h @@ -17,12 +17,12 @@ #include "nodes/parsenodes.h" #include "nodes/pathnodes.h" -struct AttrMap; /* avoid including attmap.h here */ +typedef struct AttrMap AttrMap; /* avoid including attmap.h here */ typedef struct replace_rte_variables_context replace_rte_variables_context; -typedef Node *(*replace_rte_variables_callback) (Var *var, +typedef Node *(*replace_rte_variables_callback) (const Var *var, replace_rte_variables_context *context); struct replace_rte_variables_context @@ -101,10 +101,10 @@ extern Node *replace_rte_variables_mutator(Node *node, extern Node *map_variable_attnos(Node *node, int target_varno, int sublevels_up, - const struct AttrMap *attno_map, + const AttrMap *attno_map, Oid to_rowtype, bool *found_whole_row); -extern Node *ReplaceVarFromTargetList(Var *var, +extern Node *ReplaceVarFromTargetList(const Var *var, RangeTblEntry *target_rte, List *targetlist, int result_relation, diff --git a/src/include/rewrite/rewriteRemove.h b/src/include/rewrite/rewriteRemove.h index 3caa1c4296367..2e1feac61e9ef 100644 --- a/src/include/rewrite/rewriteRemove.h +++ b/src/include/rewrite/rewriteRemove.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/rewriteRemove.h diff --git a/src/include/rewrite/rewriteSearchCycle.h b/src/include/rewrite/rewriteSearchCycle.h index 9ed456dcd0527..1d190d7db6e47 100644 --- a/src/include/rewrite/rewriteSearchCycle.h +++ b/src/include/rewrite/rewriteSearchCycle.h @@ -4,7 +4,7 @@ * Support for rewriting SEARCH and CYCLE clauses. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/rewriteSearchCycle.h diff --git a/src/include/rewrite/rewriteSupport.h b/src/include/rewrite/rewriteSupport.h index 50f5af211e029..ee2257ce148b5 100644 --- a/src/include/rewrite/rewriteSupport.h +++ b/src/include/rewrite/rewriteSupport.h @@ -4,7 +4,7 @@ * * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/rewrite/rewriteSupport.h diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h index 8f81c40a2898b..29031e9079eb0 100644 --- a/src/include/rewrite/rowsecurity.h +++ b/src/include/rewrite/rowsecurity.h @@ -5,7 +5,7 @@ * prototypes for rewrite/rowsecurity.c and the structures for managing * the row security policies for relations in relcache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * ------------------------------------------------------------------------- diff --git a/src/include/snowball/libstemmer/api.h b/src/include/snowball/libstemmer/api.h index ba9d1c14bcae1..83e29142ffc05 100644 --- a/src/include/snowball/libstemmer/api.h +++ b/src/include/snowball/libstemmer/api.h @@ -1,3 +1,5 @@ +#ifndef SNOWBALL_API_H_INCLUDED +#define SNOWBALL_API_H_INCLUDED typedef unsigned char symbol; @@ -5,28 +7,28 @@ typedef unsigned char symbol; More precisely, replace 'char' with whatever type guarantees the character width you need. Note however that sizeof(symbol) should divide - HEAD, defined in header.h as 2*sizeof(int), without remainder, otherwise - there is an alignment problem. In the unlikely event of a problem here, - consult Martin Porter. - + HEAD, defined in snowball_runtime.h as 2*sizeof(int), without remainder, + otherwise there is an alignment problem. In the unlikely event of a problem + here, consult Martin Porter. */ struct SN_env { symbol * p; int c; int l; int lb; int bra; int ket; - symbol * * S; - int * I; + int af; }; #ifdef __cplusplus extern "C" { #endif -extern struct SN_env * SN_create_env(int S_size, int I_size); -extern void SN_close_env(struct SN_env * z, int S_size); +extern struct SN_env * SN_new_env(int alloc_size); +extern void SN_delete_env(struct SN_env * z); extern int SN_set_current(struct SN_env * z, int size, const symbol * s); #ifdef __cplusplus } #endif + +#endif diff --git a/src/include/snowball/libstemmer/header.h b/src/include/snowball/libstemmer/header.h deleted file mode 100644 index bf172d5b9bdf9..0000000000000 --- a/src/include/snowball/libstemmer/header.h +++ /dev/null @@ -1,61 +0,0 @@ - -#include - -#include "api.h" - -#define MAXINT INT_MAX -#define MININT INT_MIN - -#define HEAD 2*sizeof(int) - -#define SIZE(p) ((int *)(p))[-1] -#define SET_SIZE(p, n) ((int *)(p))[-1] = n -#define CAPACITY(p) ((int *)(p))[-2] - -struct among -{ int s_size; /* number of chars in string */ - const symbol * s; /* search string */ - int substring_i;/* index to longest matching substring */ - int result; /* result of the lookup */ - int (* function)(struct SN_env *); -}; - -extern symbol * create_s(void); -extern void lose_s(symbol * p); - -extern int skip_utf8(const symbol * p, int c, int limit, int n); - -extern int skip_b_utf8(const symbol * p, int c, int limit, int n); - -extern int in_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); -extern int in_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); -extern int out_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); -extern int out_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); - -extern int in_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); -extern int in_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); -extern int out_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); -extern int out_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); - -extern int eq_s(struct SN_env * z, int s_size, const symbol * s); -extern int eq_s_b(struct SN_env * z, int s_size, const symbol * s); -extern int eq_v(struct SN_env * z, const symbol * p); -extern int eq_v_b(struct SN_env * z, const symbol * p); - -extern int find_among(struct SN_env * z, const struct among * v, int v_size); -extern int find_among_b(struct SN_env * z, const struct among * v, int v_size); - -extern int replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const symbol * s, int * adjustment); -extern int slice_from_s(struct SN_env * z, int s_size, const symbol * s); -extern int slice_from_v(struct SN_env * z, const symbol * p); -extern int slice_del(struct SN_env * z); - -extern int insert_s(struct SN_env * z, int bra, int ket, int s_size, const symbol * s); -extern int insert_v(struct SN_env * z, int bra, int ket, const symbol * p); - -extern symbol * slice_to(struct SN_env * z, symbol * p); -extern symbol * assign_to(struct SN_env * z, symbol * p); - -extern int len_utf8(const symbol * p); - -extern void debug(struct SN_env * z, int number, int line_count); diff --git a/src/include/snowball/libstemmer/snowball_runtime.h b/src/include/snowball/libstemmer/snowball_runtime.h new file mode 100644 index 0000000000000..cc135daa8cd6f --- /dev/null +++ b/src/include/snowball/libstemmer/snowball_runtime.h @@ -0,0 +1,109 @@ +#ifndef SNOWBALL_INCLUDED_SNOWBALL_RUNTIME_H +#define SNOWBALL_INCLUDED_SNOWBALL_RUNTIME_H + +#include "api.h" + +#define HEAD 2*sizeof(int) + +#ifdef __cplusplus +/* Use reinterpret_cast<> to avoid -Wcast-align warnings from clang++. */ +# define SIZE(p) (reinterpret_cast(p))[-1] +# define SET_SIZE(p, n) (reinterpret_cast(p))[-1] = n +# define CAPACITY(p) (reinterpret_cast(p))[-2] +#else +# define SIZE(p) ((const int *)(p))[-1] +# define SET_SIZE(p, n) ((int *)(p))[-1] = n +# define CAPACITY(p) ((int *)(p))[-2] +#endif + +#ifdef SNOWBALL_RUNTIME_THROW_EXCEPTIONS +# define SNOWBALL_ERR void +#else +# define SNOWBALL_ERR int +#endif + +#ifdef SNOWBALL_DEBUG_COMMAND_USED +# include +static void debug(struct SN_env * z, int number, int line_count) { + int i; + int limit = SIZE(z->p); + if (number >= 0) printf("%3d (line %4d): [%d]'", number, line_count, limit); + for (i = 0; i <= limit; i++) { + if (z->lb == i) printf("{"); + if (z->bra == i) printf("["); + if (z->c == i) printf("|"); + if (z->ket == i) printf("]"); + if (z->l == i) printf("}"); + if (i < limit) { + int ch = z->p[i]; + if (ch == 0) ch = '#'; + printf("%c", ch); + } + } + printf("'\n"); +} +#endif + +struct among +{ + /* Number of symbols in s. */ + int s_size; + /* Search string. */ + const symbol * s; + /* Delta of index to longest matching substring, or 0 if none. */ + int substring_i; + /* Result of the lookup. */ + int result; + /* Optional condition routine index, or 0 if none. */ + int function; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +extern symbol * create_s(void); +extern void lose_s(symbol * p); + +extern int skip_utf8(const symbol * p, int c, int limit, int n); + +extern int skip_b_utf8(const symbol * p, int c, int limit, int n); + +extern int in_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int in_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping_b_U(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); + +extern int in_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int in_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); +extern int out_grouping_b(struct SN_env * z, const unsigned char * s, int min, int max, int repeat); + +extern int eq_s(struct SN_env * z, int s_size, const symbol * s); +extern int eq_s_b(struct SN_env * z, int s_size, const symbol * s); +extern int eq_v(struct SN_env * z, const symbol * p); +extern int eq_v_b(struct SN_env * z, const symbol * p); + +extern int find_among(struct SN_env * z, const struct among * v, int v_size, + int (*)(struct SN_env *)); +extern int find_among_b(struct SN_env * z, const struct among * v, int v_size, + int (*)(struct SN_env *)); + +extern SNOWBALL_ERR replace_s(struct SN_env * z, int c_bra, int c_ket, int s_size, const symbol * s); +extern SNOWBALL_ERR slice_from_s(struct SN_env * z, int s_size, const symbol * s); +extern SNOWBALL_ERR slice_from_v(struct SN_env * z, const symbol * p); +extern SNOWBALL_ERR slice_del(struct SN_env * z); + +extern SNOWBALL_ERR insert_s(struct SN_env * z, int bra, int ket, int s_size, const symbol * s); +extern SNOWBALL_ERR insert_v(struct SN_env * z, int bra, int ket, const symbol * p); + +extern SNOWBALL_ERR slice_to(struct SN_env * z, symbol ** p); +extern SNOWBALL_ERR assign_to(struct SN_env * z, symbol ** p); + +extern int len_utf8(const symbol * p); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_basque.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_basque.h index bffb6e9043e19..69f56be5e453c 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_basque.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_basque.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from basque.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int basque_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_catalan.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_catalan.h index 96e97ddfa2a7b..814d468812164 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_catalan.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_catalan.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from catalan.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int catalan_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_danish.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_danish.h index 965436d9a1e25..e6cc56d8a6c02 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_danish.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_danish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from danish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int danish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch.h index 64f1c6d916329..68fec2aae675a 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from dutch.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int dutch_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch_porter.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch_porter.h new file mode 100644 index 0000000000000..77b1374e98e18 --- /dev/null +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_dutch_porter.h @@ -0,0 +1,14 @@ +/* Generated from dutch_porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct SN_env * dutch_porter_ISO_8859_1_create_env(void); +extern void dutch_porter_ISO_8859_1_close_env(struct SN_env * z); + +extern int dutch_porter_ISO_8859_1_stem(struct SN_env * z); + +#ifdef __cplusplus +} +#endif diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_english.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_english.h index ea90984b00246..b941db11c73b3 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_english.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_english.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from english.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int english_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_finnish.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_finnish.h index 2c80e4cdead31..4149cc2883d94 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_finnish.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_finnish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from finnish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int finnish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_french.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_french.h index 1febb49d20448..512a38e29e924 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_french.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_french.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from french.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int french_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_german.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_german.h index 98696bb336dc9..6918eb3bf6d26 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_german.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_german.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from german.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int german_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_indonesian.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_indonesian.h index d998b41b2cfb8..2992337eb2967 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_indonesian.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_indonesian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from indonesian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int indonesian_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_irish.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_irish.h index d91d231790614..510b789a15847 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_irish.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_irish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from irish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int irish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_italian.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_italian.h index 22950bd2347eb..a9ca0fe03d17d 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_italian.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_italian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from italian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int italian_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_norwegian.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_norwegian.h index 5393096056925..b0d18c120e42b 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_norwegian.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_norwegian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from norwegian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int norwegian_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_porter.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_porter.h index f504be101a60f..b13b72bbb7b60 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_porter.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_porter.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int porter_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_portuguese.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_portuguese.h index c7b517c091274..f52c3fe84cc53 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_portuguese.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_portuguese.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from portuguese.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int portuguese_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_spanish.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_spanish.h index b066b4fc26fa2..a922ab69091f5 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_spanish.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_spanish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from spanish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int spanish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_1_swedish.h b/src/include/snowball/libstemmer/stem_ISO_8859_1_swedish.h index 7b5ec75523c74..f99edb42def53 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_1_swedish.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_1_swedish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from swedish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int swedish_ISO_8859_1_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_2_hungarian.h b/src/include/snowball/libstemmer/stem_ISO_8859_2_hungarian.h index be6ebf685ab2b..3415405822eff 100644 --- a/src/include/snowball/libstemmer/stem_ISO_8859_2_hungarian.h +++ b/src/include/snowball/libstemmer/stem_ISO_8859_2_hungarian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from hungarian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int hungarian_ISO_8859_2_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_ISO_8859_2_polish.h b/src/include/snowball/libstemmer/stem_ISO_8859_2_polish.h new file mode 100644 index 0000000000000..9386248c1d6b8 --- /dev/null +++ b/src/include/snowball/libstemmer/stem_ISO_8859_2_polish.h @@ -0,0 +1,14 @@ +/* Generated from polish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct SN_env * polish_ISO_8859_2_create_env(void); +extern void polish_ISO_8859_2_close_env(struct SN_env * z); + +extern int polish_ISO_8859_2_stem(struct SN_env * z); + +#ifdef __cplusplus +} +#endif diff --git a/src/include/snowball/libstemmer/stem_KOI8_R_russian.h b/src/include/snowball/libstemmer/stem_KOI8_R_russian.h index 42a8518b953fb..3ddc608b0cab3 100644 --- a/src/include/snowball/libstemmer/stem_KOI8_R_russian.h +++ b/src/include/snowball/libstemmer/stem_KOI8_R_russian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from russian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int russian_KOI8_R_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_arabic.h b/src/include/snowball/libstemmer/stem_UTF_8_arabic.h index cad02f3453a92..ad239f8fb035d 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_arabic.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_arabic.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from arabic.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int arabic_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_armenian.h b/src/include/snowball/libstemmer/stem_UTF_8_armenian.h index 793fd09e6c07a..c49bc6878062a 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_armenian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_armenian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from armenian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int armenian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_basque.h b/src/include/snowball/libstemmer/stem_UTF_8_basque.h index 79ddc983af478..f7d8afceac9ff 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_basque.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_basque.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from basque.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int basque_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_catalan.h b/src/include/snowball/libstemmer/stem_UTF_8_catalan.h index 58c9995d71c61..b1475478fb6a0 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_catalan.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_catalan.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from catalan.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int catalan_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_danish.h b/src/include/snowball/libstemmer/stem_UTF_8_danish.h index a5084dc60f228..12bc0b9b5fd55 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_danish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_danish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from danish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int danish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_dutch.h b/src/include/snowball/libstemmer/stem_UTF_8_dutch.h index 16cb995413f1f..d1d1b490cbd8a 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_dutch.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_dutch.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from dutch.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int dutch_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_dutch_porter.h b/src/include/snowball/libstemmer/stem_UTF_8_dutch_porter.h new file mode 100644 index 0000000000000..274efcfb4dfec --- /dev/null +++ b/src/include/snowball/libstemmer/stem_UTF_8_dutch_porter.h @@ -0,0 +1,14 @@ +/* Generated from dutch_porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct SN_env * dutch_porter_UTF_8_create_env(void); +extern void dutch_porter_UTF_8_close_env(struct SN_env * z); + +extern int dutch_porter_UTF_8_stem(struct SN_env * z); + +#ifdef __cplusplus +} +#endif diff --git a/src/include/snowball/libstemmer/stem_UTF_8_english.h b/src/include/snowball/libstemmer/stem_UTF_8_english.h index 11fa090e7dbe4..00370d6534e04 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_english.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_english.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from english.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int english_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_esperanto.h b/src/include/snowball/libstemmer/stem_UTF_8_esperanto.h new file mode 100644 index 0000000000000..55b89f911906c --- /dev/null +++ b/src/include/snowball/libstemmer/stem_UTF_8_esperanto.h @@ -0,0 +1,14 @@ +/* Generated from esperanto.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct SN_env * esperanto_UTF_8_create_env(void); +extern void esperanto_UTF_8_close_env(struct SN_env * z); + +extern int esperanto_UTF_8_stem(struct SN_env * z); + +#ifdef __cplusplus +} +#endif diff --git a/src/include/snowball/libstemmer/stem_UTF_8_estonian.h b/src/include/snowball/libstemmer/stem_UTF_8_estonian.h index 9b9deb18a0ebe..2b332b4abfa5e 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_estonian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_estonian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from estonian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int estonian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_finnish.h b/src/include/snowball/libstemmer/stem_UTF_8_finnish.h index eebaa2de9e939..0085a0486fd8c 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_finnish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_finnish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from finnish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int finnish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_french.h b/src/include/snowball/libstemmer/stem_UTF_8_french.h index 22158b07c78d3..a0d3e720aa271 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_french.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_french.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from french.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int french_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_german.h b/src/include/snowball/libstemmer/stem_UTF_8_german.h index f276c53b265c6..91973781f9fc5 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_german.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_german.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from german.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int german_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_greek.h b/src/include/snowball/libstemmer/stem_UTF_8_greek.h index 77667a31f009a..4ef70ded05875 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_greek.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_greek.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from greek.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int greek_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_hindi.h b/src/include/snowball/libstemmer/stem_UTF_8_hindi.h index bbc2e9b72313a..b782f01fbbb71 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_hindi.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_hindi.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from hindi.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int hindi_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_hungarian.h b/src/include/snowball/libstemmer/stem_UTF_8_hungarian.h index cc29d77b9975f..e78b827ebdc6c 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_hungarian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_hungarian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from hungarian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int hungarian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_indonesian.h b/src/include/snowball/libstemmer/stem_UTF_8_indonesian.h index 9f51324ca9270..aefdbc879c416 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_indonesian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_indonesian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from indonesian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int indonesian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_irish.h b/src/include/snowball/libstemmer/stem_UTF_8_irish.h index f06da96d4c7a0..75977919cae10 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_irish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_irish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from irish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int irish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_italian.h b/src/include/snowball/libstemmer/stem_UTF_8_italian.h index f00dcaa5dc107..4e1b4d2722e53 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_italian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_italian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from italian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int italian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_lithuanian.h b/src/include/snowball/libstemmer/stem_UTF_8_lithuanian.h index e62ff1c9d5815..1212880569fe1 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_lithuanian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_lithuanian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from lithuanian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int lithuanian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_nepali.h b/src/include/snowball/libstemmer/stem_UTF_8_nepali.h index f8f50af8a0c21..9fe033a56eac9 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_nepali.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_nepali.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from nepali.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int nepali_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_norwegian.h b/src/include/snowball/libstemmer/stem_UTF_8_norwegian.h index 72aab40230d07..38079671067bb 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_norwegian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_norwegian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from norwegian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int norwegian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_polish.h b/src/include/snowball/libstemmer/stem_UTF_8_polish.h new file mode 100644 index 0000000000000..4e27da42c33f8 --- /dev/null +++ b/src/include/snowball/libstemmer/stem_UTF_8_polish.h @@ -0,0 +1,14 @@ +/* Generated from polish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct SN_env * polish_UTF_8_create_env(void); +extern void polish_UTF_8_close_env(struct SN_env * z); + +extern int polish_UTF_8_stem(struct SN_env * z); + +#ifdef __cplusplus +} +#endif diff --git a/src/include/snowball/libstemmer/stem_UTF_8_porter.h b/src/include/snowball/libstemmer/stem_UTF_8_porter.h index 00685b2c96ad1..feaf9ef32d457 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_porter.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_porter.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from porter.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int porter_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_portuguese.h b/src/include/snowball/libstemmer/stem_UTF_8_portuguese.h index 7be43352c14e3..d717ad7565fcb 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_portuguese.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_portuguese.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from portuguese.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int portuguese_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_romanian.h b/src/include/snowball/libstemmer/stem_UTF_8_romanian.h index c93cd335b90bc..4e765d931465b 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_romanian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_romanian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from romanian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int romanian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_russian.h b/src/include/snowball/libstemmer/stem_UTF_8_russian.h index ca1d88216ca42..64815b733674d 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_russian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_russian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from russian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int russian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_serbian.h b/src/include/snowball/libstemmer/stem_UTF_8_serbian.h index 1df04f64674e3..941b87cf625c3 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_serbian.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_serbian.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from serbian.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int serbian_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_spanish.h b/src/include/snowball/libstemmer/stem_UTF_8_spanish.h index dfd8dc3649d7f..8b7b65f18ac37 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_spanish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_spanish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from spanish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int spanish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_swedish.h b/src/include/snowball/libstemmer/stem_UTF_8_swedish.h index ca08a64750ecf..c89bae80c3a37 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_swedish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_swedish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from swedish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int swedish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_tamil.h b/src/include/snowball/libstemmer/stem_UTF_8_tamil.h index 5f5ae352baa1c..08d2958a4424f 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_tamil.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_tamil.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from tamil.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int tamil_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_turkish.h b/src/include/snowball/libstemmer/stem_UTF_8_turkish.h index 68405929c3bae..7b3f0defb4cf6 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_turkish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_turkish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from turkish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int turkish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/libstemmer/stem_UTF_8_yiddish.h b/src/include/snowball/libstemmer/stem_UTF_8_yiddish.h index 55b66256a27ac..8633385922800 100644 --- a/src/include/snowball/libstemmer/stem_UTF_8_yiddish.h +++ b/src/include/snowball/libstemmer/stem_UTF_8_yiddish.h @@ -1,4 +1,4 @@ -/* Generated by Snowball 2.2.0 - https://snowballstem.org/ */ +/* Generated from yiddish.sbl by Snowball 3.0.0 - https://snowballstem.org/ */ #ifdef __cplusplus extern "C" { @@ -12,4 +12,3 @@ extern int yiddish_UTF_8_stem(struct SN_env * z); #ifdef __cplusplus } #endif - diff --git a/src/include/snowball/header.h b/src/include/snowball/snowball_runtime.h similarity index 67% rename from src/include/snowball/header.h rename to src/include/snowball/snowball_runtime.h index 84dc8280851ea..a8f8947c35677 100644 --- a/src/include/snowball/header.h +++ b/src/include/snowball/snowball_runtime.h @@ -1,26 +1,26 @@ /*------------------------------------------------------------------------- * - * header.h + * snowball_runtime.h * Replacement header file for Snowball stemmer modules * - * The Snowball stemmer modules do #include "header.h", and think they - * are including snowball/libstemmer/header.h. We adjust the CPPFLAGS - * so that this file is found instead, and thereby we can modify the - * headers they see. The main point here is to ensure that pg_config.h + * The Snowball stemmer modules do #include "snowball_runtime.h", and think + * they are including snowball/libstemmer/snowball_runtime.h. We adjust + * the CPPFLAGS so that this file is found instead, and thereby we can modify + * the headers they see. The main point here is to ensure that pg_config.h * is included before any system headers such as ; without that, * we have portability issues on some platforms due to variation in * largefile options across different modules in the backend. * * NOTE: this file should not be included into any non-snowball sources! * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * - * src/include/snowball/header.h + * src/include/snowball/snowball_runtime.h * *------------------------------------------------------------------------- */ -#ifndef SNOWBALL_HEADR_H -#define SNOWBALL_HEADR_H +#ifndef PG_SNOWBALL_RUNTIME_H +#define PG_SNOWBALL_RUNTIME_H /* * It's against Postgres coding conventions to include postgres.h in a @@ -37,8 +37,8 @@ #undef MININT #endif -/* Now we can include the original Snowball header.h */ -#include "snowball/libstemmer/header.h" +/* Now we can include the original Snowball snowball_runtime.h */ +#include "snowball/libstemmer/snowball_runtime.h" /* * Redefine standard memory allocation interface to pgsql's one. @@ -64,4 +64,4 @@ #endif #define free(a) pfree(a) -#endif /* SNOWBALL_HEADR_H */ +#endif /* PG_SNOWBALL_RUNTIME_H */ diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h index efcb7dc35461e..c775442f2ee44 100644 --- a/src/include/statistics/extended_stats_internal.h +++ b/src/include/statistics/extended_stats_internal.h @@ -3,7 +3,7 @@ * extended_stats_internal.h * POSTGRES extended statistics internal declarations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -72,15 +72,29 @@ typedef struct StatsBuildData extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *data); extern bytea *statext_ndistinct_serialize(MVNDistinct *ndistinct); extern MVNDistinct *statext_ndistinct_deserialize(bytea *data); +extern bool statext_ndistinct_validate(const MVNDistinct *ndistinct, + const int2vector *stxkeys, + int numexprs, int elevel); +extern void statext_ndistinct_free(MVNDistinct *ndistinct); extern MVDependencies *statext_dependencies_build(StatsBuildData *data); extern bytea *statext_dependencies_serialize(MVDependencies *dependencies); extern MVDependencies *statext_dependencies_deserialize(bytea *data); +extern bool statext_dependencies_validate(const MVDependencies *dependencies, + const int2vector *stxkeys, + int numexprs, int elevel); +extern void statext_dependencies_free(MVDependencies *dependencies); extern MCVList *statext_mcv_build(StatsBuildData *data, double totalrows, int stattarget); extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats); extern MCVList *statext_mcv_deserialize(bytea *data); +extern void statext_mcv_free(MCVList *mcvlist); +extern Datum statext_mcv_import(int elevel, int numattrs, Oid *atttypids, + int32 *atttypmods, Oid *atttypcolls, + int nitems, Datum *mcv_elems, + bool *mcv_nulls, float8 *freqs, + float8 *base_freqs); extern MultiSortSupport multi_sort_init(int ndims); extern void multi_sort_add_dimension(MultiSortSupport mss, int sortdim, diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h index 512eb776e0e04..74da779057969 100644 --- a/src/include/statistics/stat_utils.h +++ b/src/include/statistics/stat_utils.h @@ -3,7 +3,7 @@ * stat_utils.h * Extended statistics and selectivity estimation functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/statistics/stat_utils.h @@ -13,8 +13,12 @@ #ifndef STATS_UTILS_H #define STATS_UTILS_H +#include "access/attnum.h" #include "fmgr.h" +/* avoid including primnodes.h here */ +typedef struct RangeVar RangeVar; + struct StatsArgInfo { const char *argname; @@ -30,12 +34,28 @@ extern bool stats_check_arg_pair(FunctionCallInfo fcinfo, struct StatsArgInfo *arginfo, int argnum1, int argnum2); -extern void stats_lock_check_privileges(Oid reloid); - -extern Oid stats_lookup_relid(const char *nspname, const char *relname); +extern void RangeVarCallbackForStats(const RangeVar *relation, + Oid relId, Oid oldRelId, void *arg); extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo, FunctionCallInfo positional_fcinfo, struct StatsArgInfo *arginfo); +extern void statatt_get_type(Oid reloid, AttrNumber attnum, + Oid *atttypid, int32 *atttypmod, + char *atttyptype, Oid *atttypcoll, + Oid *eq_opr, Oid *lt_opr); +extern void statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited, + Datum *values, bool *nulls, bool *replaces); + +extern void statatt_set_slot(Datum *values, bool *nulls, bool *replaces, + int16 stakind, Oid staop, Oid stacoll, + Datum stanumbers, bool stanumbers_isnull, + Datum stavalues, bool stavalues_isnull); + +extern Datum statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Datum d, + Oid typid, int32 typmod, bool *ok); +extern bool statatt_get_elem_type(Oid atttypid, char atttyptype, + Oid *elemtypid, Oid *elem_eq_opr); + #endif /* STATS_UTILS_H */ diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h index 7dd0f9755454a..8f9b9d237fd5d 100644 --- a/src/include/statistics/statistics.h +++ b/src/include/statistics/statistics.h @@ -3,7 +3,7 @@ * statistics.h * Extended statistics and selectivity estimation functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/statistics/statistics.h @@ -101,6 +101,7 @@ extern MCVList *statext_mcv_load(Oid mvoid, bool inh); extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows, int numrows, HeapTuple *rows, int natts, VacAttrStats **vacattrstats); +extern bool HasRelationExtStatistics(Relation onerel); extern int ComputeExtStatisticsRows(Relation onerel, int natts, VacAttrStats **vacattrstats); extern bool statext_is_kind_built(HeapTuple htup, char type); diff --git a/src/include/statistics/statistics_format.h b/src/include/statistics/statistics_format.h new file mode 100644 index 0000000000000..19720772fd4e6 --- /dev/null +++ b/src/include/statistics/statistics_format.h @@ -0,0 +1,47 @@ +/*------------------------------------------------------------------------- + * + * statistics_format.h + * Data related to the format of extended statistics, usable by both + * frontend and backend code. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/include/statistics/statistics_format.h + * + *------------------------------------------------------------------------- + */ +#ifndef STATISTICS_FORMAT_H +#define STATISTICS_FORMAT_H + +/* ---------- + * pg_ndistinct in human-readable format is a JSON array made of elements with + * a predefined set of keys, like: + * + * [{"ndistinct": 11, "attributes": [3,4]}, + * {"ndistinct": 11, "attributes": [3,6]}, + * {"ndistinct": 11, "attributes": [4,6]}, + * {"ndistinct": 11, "attributes": [3,4,6]}, + * ... ] + * ---------- + */ +#define PG_NDISTINCT_KEY_ATTRIBUTES "attributes" +#define PG_NDISTINCT_KEY_NDISTINCT "ndistinct" + + +/* ---------- + * pg_dependencies in human-readable format is a JSON array made of elements + * with a predefined set of keys, like: + * + * [{"degree": 1.000000, "attributes": [3], "dependency": 4}, + * {"degree": 1.000000, "attributes": [3], "dependency": 6}, + * ... ] + * ---------- + */ + +#define PG_DEPENDENCIES_KEY_ATTRIBUTES "attributes" +#define PG_DEPENDENCIES_KEY_DEPENDENCY "dependency" +#define PG_DEPENDENCIES_KEY_DEGREE "degree" + +#endif /* STATISTICS_FORMAT_H */ diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h index f3726bc3dc511..ec543b7840943 100644 --- a/src/include/storage/aio.h +++ b/src/include/storage/aio.h @@ -8,7 +8,7 @@ * include aio_types.h. Initialization related functions are in the dedicated * aio_init.h. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/aio.h @@ -36,7 +36,7 @@ typedef enum IoMethod #ifdef IOMETHOD_IO_URING_ENABLED IOMETHOD_IO_URING, #endif -} IoMethod; +} IoMethod; /* We'll default to worker based execution. */ #define DEFAULT_IO_METHOD IOMETHOD_WORKER @@ -201,7 +201,7 @@ typedef enum PgAioHandleCallbackID } PgAioHandleCallbackID; #define PGAIO_HCB_MAX PGAIO_HCB_LOCAL_BUFFER_READV -StaticAssertDecl(PGAIO_HCB_MAX <= (1 << PGAIO_RESULT_ID_BITS), +StaticAssertDecl(PGAIO_HCB_MAX < (1 << PGAIO_RESULT_ID_BITS), "PGAIO_HCB_MAX is too big for PGAIO_RESULT_ID_BITS"); diff --git a/src/include/storage/aio_internal.h b/src/include/storage/aio_internal.h index 2d37a243abe52..9ca4087aa7fef 100644 --- a/src/include/storage/aio_internal.h +++ b/src/include/storage/aio_internal.h @@ -5,7 +5,7 @@ * internally. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/aio_internal.h @@ -20,6 +20,8 @@ #include "port/pg_iovec.h" #include "storage/aio.h" #include "storage/condition_variable.h" +#include "storage/ipc.h" +#include "storage/shmem.h" /* @@ -92,17 +94,23 @@ typedef enum PgAioHandleState struct ResourceOwnerData; -/* typedef is in aio_types.h */ +/* + * Typedef is in aio_types.h + * + * We don't use the underlying enums for state, target and op to avoid wasting + * space. We tried using bitfields, but several compilers generate rather + * horrid code for that. + */ struct PgAioHandle { /* all state updates should go through pgaio_io_update_state() */ - PgAioHandleState state:8; + uint8 state; /* what are we operating on */ - PgAioTargetID target:8; + uint8 target; /* which IO operation */ - PgAioOp op:8; + uint8 op; /* bitfield of PgAioHandleFlags */ uint8 flags; @@ -261,20 +269,8 @@ typedef struct IoMethodOps */ bool wait_on_fd_before_close; - /* global initialization */ - - /* - * Amount of additional shared memory to reserve for the io_method. Called - * just like a normal ipci.c style *Size() function. Optional. - */ - size_t (*shmem_size) (void); - - /* - * Initialize shared memory. First time is true if AIO's shared memory was - * just initialized, false otherwise. Optional. - */ - void (*shmem_init) (bool first_time); + ShmemCallbacks shmem_callbacks; /* * Per-backend initialization. Optional. @@ -322,6 +318,21 @@ typedef struct IoMethodOps */ void (*wait_one) (PgAioHandle *ioh, uint64 ref_generation); + + /* --- + * Check if IO has already completed. Optional. + * + * Some IO methods need to poll a kernel object to see if IO has already + * completed in the background. This callback allows to do so. + * + * This callback may not wait for IO to complete, however it is allowed, + * although not desirable, to wait for short-lived locks. It is ok from a + * correctness perspective to not process any/all available completions, + * it just can lead to inferior performance. + * --- + */ + void (*check_one) (PgAioHandle *ioh, + uint64 ref_generation); } IoMethodOps; diff --git a/src/include/storage/aio_subsys.h b/src/include/storage/aio_subsys.h index 0cf36bb35da70..b0e5c3372282f 100644 --- a/src/include/storage/aio_subsys.h +++ b/src/include/storage/aio_subsys.h @@ -8,7 +8,7 @@ * form. E.g. postmaster.c and shared memory initialization need to initialize * AIO but don't perform AIO. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/aio_subsys.h @@ -20,9 +20,6 @@ /* aio_init.c */ -extern Size AioShmemSize(void); -extern void AioShmemInit(void); - extern void pgaio_init_backend(void); diff --git a/src/include/storage/aio_types.h b/src/include/storage/aio_types.h index 181833660778e..17b59aeed7ccf 100644 --- a/src/include/storage/aio_types.h +++ b/src/include/storage/aio_types.h @@ -5,7 +5,7 @@ * "include burden". * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/aio_types.h @@ -107,7 +107,7 @@ typedef struct PgAioResult /* of type PgAioResultStatus, see above */ uint32 status:PGAIO_RESULT_STATUS_BITS; - /* meaning defined by callback->error */ + /* meaning defined by callback->report */ uint32 error_data:PGAIO_RESULT_ERROR_BITS; int32 result; diff --git a/src/include/storage/barrier.h b/src/include/storage/barrier.h index 4d8a95f4a4aec..81b29d5cd5235 100644 --- a/src/include/storage/barrier.h +++ b/src/include/storage/barrier.h @@ -3,7 +3,7 @@ * barrier.h * Barriers for synchronizing cooperating processes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/barrier.h diff --git a/src/include/storage/block.h b/src/include/storage/block.h index cee5fcf3c5ec4..54656b2ff3517 100644 --- a/src/include/storage/block.h +++ b/src/include/storage/block.h @@ -4,7 +4,7 @@ * POSTGRES disk block definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/block.h diff --git a/src/include/storage/buf.h b/src/include/storage/buf.h index 2a209cc3934fb..b21445522b180 100644 --- a/src/include/storage/buf.h +++ b/src/include/storage/buf.h @@ -4,7 +4,7 @@ * Basic buffer manager data types. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/buf.h diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 0dec7d93b3b27..89615a254a3ed 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -5,7 +5,7 @@ * strategy. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/buf_internals.h @@ -23,6 +23,7 @@ #include "storage/condition_variable.h" #include "storage/lwlock.h" #include "storage/procnumber.h" +#include "storage/proclist_types.h" #include "storage/shmem.h" #include "storage/smgr.h" #include "storage/spin.h" @@ -30,11 +31,15 @@ #include "utils/resowner.h" /* - * Buffer state is a single 32-bit variable where following data is combined. + * Buffer state is a single 64-bit variable where following data is combined. * + * State of the buffer itself (in order): * - 18 bits refcount * - 4 bits usage count - * - 10 bits of flags + * - 12 bits of flags + * - 18 bits share-lock count + * - 1 bit share-exclusive locked + * - 1 bit exclusive locked * * Combining these values allows to perform some operations without locking * the buffer header, by modifying them together with a CAS loop. @@ -43,21 +48,49 @@ */ #define BUF_REFCOUNT_BITS 18 #define BUF_USAGECOUNT_BITS 4 -#define BUF_FLAG_BITS 10 +#define BUF_FLAG_BITS 12 +#define BUF_LOCK_BITS (18+2) -StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS == 32, - "parts of buffer state space need to equal 32"); +StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS + BUF_LOCK_BITS <= 64, + "parts of buffer state space need to be <= 64"); +/* refcount related definitions */ #define BUF_REFCOUNT_ONE 1 -#define BUF_REFCOUNT_MASK ((1U << BUF_REFCOUNT_BITS) - 1) -#define BUF_USAGECOUNT_MASK (((1U << BUF_USAGECOUNT_BITS) - 1) << (BUF_REFCOUNT_BITS)) -#define BUF_USAGECOUNT_ONE (1U << BUF_REFCOUNT_BITS) -#define BUF_USAGECOUNT_SHIFT BUF_REFCOUNT_BITS -#define BUF_FLAG_MASK (((1U << BUF_FLAG_BITS) - 1) << (BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS)) +#define BUF_REFCOUNT_MASK \ + ((UINT64CONST(1) << BUF_REFCOUNT_BITS) - 1) + +/* usage count related definitions */ +#define BUF_USAGECOUNT_SHIFT \ + BUF_REFCOUNT_BITS +#define BUF_USAGECOUNT_MASK \ + (((UINT64CONST(1) << BUF_USAGECOUNT_BITS) - 1) << (BUF_USAGECOUNT_SHIFT)) +#define BUF_USAGECOUNT_ONE \ + (UINT64CONST(1) << BUF_REFCOUNT_BITS) + +/* flags related definitions */ +#define BUF_FLAG_SHIFT \ + (BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS) +#define BUF_FLAG_MASK \ + (((UINT64CONST(1) << BUF_FLAG_BITS) - 1) << BUF_FLAG_SHIFT) + +/* lock state related definitions */ +#define BM_LOCK_SHIFT \ + (BUF_FLAG_SHIFT + BUF_FLAG_BITS) +#define BM_LOCK_VAL_SHARED \ + (UINT64CONST(1) << (BM_LOCK_SHIFT)) +#define BM_LOCK_VAL_SHARE_EXCLUSIVE \ + (UINT64CONST(1) << (BM_LOCK_SHIFT + MAX_BACKENDS_BITS)) +#define BM_LOCK_VAL_EXCLUSIVE \ + (UINT64CONST(1) << (BM_LOCK_SHIFT + MAX_BACKENDS_BITS + 1)) +#define BM_LOCK_MASK \ + ((((uint64) MAX_BACKENDS) << BM_LOCK_SHIFT) | BM_LOCK_VAL_SHARE_EXCLUSIVE | BM_LOCK_VAL_EXCLUSIVE) + /* Get refcount and usagecount from buffer state */ -#define BUF_STATE_GET_REFCOUNT(state) ((state) & BUF_REFCOUNT_MASK) -#define BUF_STATE_GET_USAGECOUNT(state) (((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT) +#define BUF_STATE_GET_REFCOUNT(state) \ + ((uint32)((state) & BUF_REFCOUNT_MASK)) +#define BUF_STATE_GET_USAGECOUNT(state) \ + ((uint32)(((state) & BUF_USAGECOUNT_MASK) >> BUF_USAGECOUNT_SHIFT)) /* * Flags for buffer descriptors @@ -65,31 +98,53 @@ StaticAssertDecl(BUF_REFCOUNT_BITS + BUF_USAGECOUNT_BITS + BUF_FLAG_BITS == 32, * Note: BM_TAG_VALID essentially means that there is a buffer hashtable * entry associated with the buffer's tag. */ -#define BM_LOCKED (1U << 22) /* buffer header is locked */ -#define BM_DIRTY (1U << 23) /* data needs writing */ -#define BM_VALID (1U << 24) /* data is valid */ -#define BM_TAG_VALID (1U << 25) /* tag is assigned */ -#define BM_IO_IN_PROGRESS (1U << 26) /* read or write in progress */ -#define BM_IO_ERROR (1U << 27) /* previous I/O failed */ -#define BM_JUST_DIRTIED (1U << 28) /* dirtied since write started */ -#define BM_PIN_COUNT_WAITER (1U << 29) /* have waiter for sole pin */ -#define BM_CHECKPOINT_NEEDED (1U << 30) /* must write for checkpoint */ -#define BM_PERMANENT (1U << 31) /* permanent buffer (not unlogged, - * or init fork) */ + +#define BUF_DEFINE_FLAG(flagno) \ + (UINT64CONST(1) << (BUF_FLAG_SHIFT + (flagno))) + +/* buffer header is locked */ +#define BM_LOCKED BUF_DEFINE_FLAG( 0) +/* data needs writing */ +#define BM_DIRTY BUF_DEFINE_FLAG( 1) +/* data is valid */ +#define BM_VALID BUF_DEFINE_FLAG( 2) +/* tag is assigned */ +#define BM_TAG_VALID BUF_DEFINE_FLAG( 3) +/* read or write in progress */ +#define BM_IO_IN_PROGRESS BUF_DEFINE_FLAG( 4) +/* previous I/O failed */ +#define BM_IO_ERROR BUF_DEFINE_FLAG( 5) +/* flag bit 6 is not used anymore */ +/* have waiter for sole pin */ +#define BM_PIN_COUNT_WAITER BUF_DEFINE_FLAG( 7) +/* must write for checkpoint */ +#define BM_CHECKPOINT_NEEDED BUF_DEFINE_FLAG( 8) +/* permanent buffer (not unlogged, or init fork) */ +#define BM_PERMANENT BUF_DEFINE_FLAG( 9) +/* content lock has waiters */ +#define BM_LOCK_HAS_WAITERS BUF_DEFINE_FLAG(10) +/* waiter for content lock has been signalled but not yet run */ +#define BM_LOCK_WAKE_IN_PROGRESS BUF_DEFINE_FLAG(11) + + +StaticAssertDecl(MAX_BACKENDS_BITS <= BUF_REFCOUNT_BITS, + "MAX_BACKENDS_BITS needs to be <= BUF_REFCOUNT_BITS"); +StaticAssertDecl(MAX_BACKENDS_BITS <= (BUF_LOCK_BITS - 2), + "MAX_BACKENDS_BITS needs to be <= BUF_LOCK_BITS - 2"); + + /* * The maximum allowed value of usage_count represents a tradeoff between * accuracy and speed of the clock-sweep buffer management algorithm. A * large value (comparable to NBuffers) would approximate LRU semantics. - * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of - * clock sweeps to find a free buffer, so in practice we don't want the + * But it can take as many as BM_MAX_USAGE_COUNT+1 complete cycles of the + * clock-sweep hand to find a free buffer, so in practice we don't want the * value to be very large. */ #define BM_MAX_USAGE_COUNT 5 -StaticAssertDecl(BM_MAX_USAGE_COUNT < (1 << BUF_USAGECOUNT_BITS), +StaticAssertDecl(BM_MAX_USAGE_COUNT < (UINT64CONST(1) << BUF_USAGECOUNT_BITS), "BM_MAX_USAGE_COUNT doesn't fit in BUF_USAGECOUNT_BITS bits"); -StaticAssertDecl(MAX_BACKENDS_BITS <= BUF_REFCOUNT_BITS, - "MAX_BACKENDS_BITS needs to be <= BUF_REFCOUNT_BITS"); /* * Buffer tag identifies which disk block the buffer contains. @@ -211,29 +266,33 @@ BufMappingPartitionLockByIndex(uint32 index) /* * BufferDesc -- shared descriptor/state data for a single shared buffer. * - * Note: Buffer header lock (BM_LOCKED flag) must be held to examine or change - * tag, state or wait_backend_pgprocno fields. In general, buffer header lock - * is a spinlock which is combined with flags, refcount and usagecount into - * single atomic variable. This layout allow us to do some operations in a - * single atomic operation, without actually acquiring and releasing spinlock; - * for instance, increase or decrease refcount. buf_id field never changes - * after initialization, so does not need locking. freeNext is protected by - * the buffer_strategy_lock not buffer header lock. The LWLock can take care - * of itself. The buffer header lock is *not* used to control access to the - * data in the buffer! + * The state of the buffer is controlled by the, drumroll, state variable. It + * only may be modified using atomic operations. The state variable combines + * various flags, the buffer's refcount and usage count. See comment above + * BUF_REFCOUNT_BITS for details about the division. This layout allow us to + * do some operations in a single atomic operation, without actually acquiring + * and releasing the spinlock; for instance, increasing or decreasing the + * refcount. + * + * One of the aforementioned flags is BM_LOCKED, used to implement the buffer + * header lock. See the following paragraphs, as well as the documentation for + * individual fields, for more details. + * + * The identity of the buffer (BufferDesc.tag) can only be changed by the + * backend holding the buffer header lock. * - * It's assumed that nobody changes the state field while buffer header lock - * is held. Thus buffer header lock holder can do complex updates of the - * state variable in single write, simultaneously with lock release (cleaning - * BM_LOCKED flag). On the other hand, updating of state without holding - * buffer header lock is restricted to CAS, which ensures that BM_LOCKED flag - * is not set. Atomic increment/decrement, OR/AND etc. are not allowed. + * If the lock is held by another backend, neither additional buffer pins may + * be established (we would like to relax this eventually), nor can flags be + * set/cleared. These operations either need to acquire the buffer header + * spinlock, or need to use a CAS loop, waiting for the lock to be released if + * it is held. However, existing buffer pins may be released while the buffer + * header spinlock is held, using an atomic subtraction. * - * An exception is that if we have the buffer pinned, its tag can't change - * underneath us, so we can examine the tag without locking the buffer header. - * Also, in places we do one-time reads of the flags without bothering to - * lock the buffer header; this is generally for situations where we don't - * expect the flag bit being tested to be changing. + * If we have the buffer pinned, its tag can't change underneath us, so we can + * examine the tag without locking the buffer header. Also, in places we do + * one-time reads of the flags without bothering to lock the buffer header; + * this is generally for situations where we don't expect the flag bit being + * tested to be changing. * * We can't physically remove items from a disk page if another backend has * the buffer pinned. Hence, a backend may need to wait for all other pins @@ -241,11 +300,20 @@ BufMappingPartitionLockByIndex(uint32 index) * wait_backend_pgprocno and setting flag bit BM_PIN_COUNT_WAITER. At present, * there can be only one such waiter per buffer. * + * The content of buffers is protected via the buffer content lock, + * implemented as part of the buffer state. Note that the buffer header lock + * is *not* used to control access to the data in the buffer! We used to use + * an LWLock to implement the content lock, but having a dedicated + * implementation of content locks allows us to implement some otherwise hard + * things (e.g. race-freely checking if AIO is in progress before locking a + * buffer exclusively) and enables otherwise impossible optimizations + * (e.g. unlocking and unpinning a buffer in one atomic operation). + * * We use this same struct for local buffer headers, but the locks are not * used and not all of the flag bits are useful either. To avoid unnecessary * overhead, manipulations of the state field should be done without actual - * atomic operations (i.e. only pg_atomic_read_u32() and - * pg_atomic_unlocked_write_u32()). + * atomic operations (i.e. only pg_atomic_read_u64() and + * pg_atomic_unlocked_write_u64()). * * Be careful to avoid increasing the size of the struct when adding or * reordering members. Keeping it below 64 bytes (the most common CPU @@ -257,17 +325,37 @@ BufMappingPartitionLockByIndex(uint32 index) */ typedef struct BufferDesc { - BufferTag tag; /* ID of page contained in buffer */ - int buf_id; /* buffer's index number (from 0) */ + /* + * ID of page contained in buffer. The buffer header spinlock needs to be + * held to modify this field. + */ + BufferTag tag; + + /* + * Buffer's index number (from 0). The field never changes after + * initialization, so does not need locking. + */ + int buf_id; - /* state of the tag, containing flags, refcount and usagecount */ - pg_atomic_uint32 state; + /* + * State of the buffer, containing flags, refcount and usagecount. See + * BUF_* and BM_* defines at the top of this file. + */ + pg_atomic_uint64 state; - int wait_backend_pgprocno; /* backend of pin-count waiter */ - int freeNext; /* link in freelist chain */ + /* + * Backend of pin-count waiter. The buffer header spinlock needs to be + * held to modify this field. + */ + int wait_backend_pgprocno; PgAioWaitRef io_wref; /* set iff AIO is in progress */ - LWLock content_lock; /* to lock access to buffer contents */ + + /* + * List of PGPROCs waiting for the buffer content lock. Protected by the + * buffer header spinlock. + */ + proclist_head lock_waiters; } BufferDesc; /* @@ -354,32 +442,62 @@ BufferDescriptorGetIOCV(const BufferDesc *bdesc) return &(BufferIOCVArray[bdesc->buf_id]).cv; } -static inline LWLock * -BufferDescriptorGetContentLock(const BufferDesc *bdesc) -{ - return (LWLock *) (&bdesc->content_lock); -} - -/* - * The freeNext field is either the index of the next freelist entry, - * or one of these special values: - */ -#define FREENEXT_END_OF_LIST (-1) -#define FREENEXT_NOT_IN_LIST (-2) - /* * Functions for acquiring/releasing a shared buffer header's spinlock. Do * not apply these to local buffers! */ -extern uint32 LockBufHdr(BufferDesc *desc); +extern uint64 LockBufHdr(BufferDesc *desc); +/* + * Unlock the buffer header. + * + * This can only be used if the caller did not modify BufferDesc.state. To + * set/unset flag bits or change the refcount use UnlockBufHdrExt(). + */ static inline void -UnlockBufHdr(BufferDesc *desc, uint32 buf_state) +UnlockBufHdr(BufferDesc *desc) +{ + Assert(pg_atomic_read_u64(&desc->state) & BM_LOCKED); + + pg_atomic_fetch_sub_u64(&desc->state, BM_LOCKED); +} + +/* + * Unlock the buffer header, while atomically adding the flags in set_bits, + * unsetting the ones in unset_bits and changing the refcount by + * refcount_change. + * + * Note that this approach would not work for usagecount, since we need to cap + * the usagecount at BM_MAX_USAGE_COUNT. + */ +static inline uint64 +UnlockBufHdrExt(BufferDesc *desc, uint64 old_buf_state, + uint64 set_bits, uint64 unset_bits, + int refcount_change) { - pg_write_barrier(); - pg_atomic_write_u32(&desc->state, buf_state & (~BM_LOCKED)); + for (;;) + { + uint64 buf_state = old_buf_state; + + Assert(buf_state & BM_LOCKED); + + buf_state |= set_bits; + buf_state &= ~unset_bits; + buf_state &= ~BM_LOCKED; + + if (refcount_change != 0) + buf_state += BUF_REFCOUNT_ONE * refcount_change; + + if (pg_atomic_compare_exchange_u64(&desc->state, &old_buf_state, + buf_state)) + { + return old_buf_state; + } + } } +extern uint64 WaitBufHdrUnlocked(BufferDesc *buf); + /* in bufmgr.c */ /* @@ -401,18 +519,18 @@ extern PGDLLIMPORT CkptSortItem *CkptBufferIds; /* ResourceOwner callbacks to hold buffer I/Os and pins */ extern PGDLLIMPORT const ResourceOwnerDesc buffer_io_resowner_desc; -extern PGDLLIMPORT const ResourceOwnerDesc buffer_pin_resowner_desc; +extern PGDLLIMPORT const ResourceOwnerDesc buffer_resowner_desc; /* Convenience wrappers over ResourceOwnerRemember/Forget */ static inline void ResourceOwnerRememberBuffer(ResourceOwner owner, Buffer buffer) { - ResourceOwnerRemember(owner, Int32GetDatum(buffer), &buffer_pin_resowner_desc); + ResourceOwnerRemember(owner, Int32GetDatum(buffer), &buffer_resowner_desc); } static inline void ResourceOwnerForgetBuffer(ResourceOwner owner, Buffer buffer) { - ResourceOwnerForget(owner, Int32GetDatum(buffer), &buffer_pin_resowner_desc); + ResourceOwnerForget(owner, Int32GetDatum(buffer), &buffer_resowner_desc); } static inline void ResourceOwnerRememberBufferIO(ResourceOwner owner, Buffer buffer) @@ -434,30 +552,42 @@ extern void IssuePendingWritebacks(WritebackContext *wb_context, IOContext io_co extern void ScheduleBufferTagForWriteback(WritebackContext *wb_context, IOContext io_context, BufferTag *tag); -/* solely to make it easier to write tests */ -extern bool StartBufferIO(BufferDesc *buf, bool forInput, bool nowait); -extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint32 set_flag_bits, +extern void TrackNewBufferPin(Buffer buf); + +/* + * Return value for StartBufferIO / StartSharedBufferIO / StartLocalBufferIO. + * + * When preparing a buffer for I/O and setting BM_IO_IN_PROGRESS, the buffer + * may already have I/O in progress or the I/O may have been done by another + * backend. See the documentation of StartSharedBufferIO for more details. + */ +typedef enum StartBufferIOResult +{ + BUFFER_IO_ALREADY_DONE, + BUFFER_IO_IN_PROGRESS, + BUFFER_IO_READY_FOR_IO, +} StartBufferIOResult; + +/* the following are exposed to make it easier to write tests */ +extern StartBufferIOResult StartBufferIO(Buffer buffer, bool forInput, bool wait, + PgAioWaitRef *io_wref); +extern StartBufferIOResult StartSharedBufferIO(BufferDesc *buf, bool forInput, bool wait, + PgAioWaitRef *io_wref); +extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag_bits, bool forget_owner, bool release_aio); /* freelist.c */ extern IOContext IOContextForStrategy(BufferAccessStrategy strategy); extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy, - uint32 *buf_state, bool *from_ring); -extern void StrategyFreeBuffer(BufferDesc *buf); + uint64 *buf_state, bool *from_ring); extern bool StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring); extern int StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc); extern void StrategyNotifyBgWriter(int bgwprocno); -extern Size StrategyShmemSize(void); -extern void StrategyInitialize(bool init); -extern bool have_free_buffer(void); - /* buf_table.c */ -extern Size BufTableShmemSize(int size); -extern void InitBufTable(int size); extern uint32 BufTableHashCode(BufferTag *tagPtr); extern int BufTableLookup(BufferTag *tagPtr, uint32 hashcode); extern int BufTableInsert(BufferTag *tagPtr, uint32 hashcode, int buf_id); @@ -481,13 +611,14 @@ extern BlockNumber ExtendBufferedRelLocal(BufferManagerRelation bmr, uint32 *extended_by); extern void MarkLocalBufferDirty(Buffer buffer); extern void TerminateLocalBufferIO(BufferDesc *bufHdr, bool clear_dirty, - uint32 set_flag_bits, bool release_aio); -extern bool StartLocalBufferIO(BufferDesc *bufHdr, bool forInput, bool nowait); + uint64 set_flag_bits, bool release_aio); +extern StartBufferIOResult StartLocalBufferIO(BufferDesc *bufHdr, bool forInput, + bool wait, PgAioWaitRef *io_wref); extern void FlushLocalBuffer(BufferDesc *bufHdr, SMgrRelation reln); extern void InvalidateLocalBuffer(BufferDesc *bufHdr, bool check_unreferenced); extern void DropRelationLocalBuffers(RelFileLocator rlocator, - ForkNumber forkNum, - BlockNumber firstDelBlock); + ForkNumber *forkNum, int nforks, + BlockNumber *firstDelBlock); extern void DropRelationAllLocalBuffers(RelFileLocator rlocator); extern void AtEOXact_LocalBuffers(bool isCommit); diff --git a/src/include/storage/buffile.h b/src/include/storage/buffile.h index a2f4821f240b9..6754f37836c59 100644 --- a/src/include/storage/buffile.h +++ b/src/include/storage/buffile.h @@ -15,7 +15,7 @@ * but currently we have no need for oversize temp files without buffered * access. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/buffile.h @@ -42,8 +42,8 @@ pg_nodiscard extern size_t BufFileRead(BufFile *file, void *ptr, size_t size); extern void BufFileReadExact(BufFile *file, void *ptr, size_t size); extern size_t BufFileReadMaybeEOF(BufFile *file, void *ptr, size_t size, bool eofOK); extern void BufFileWrite(BufFile *file, const void *ptr, size_t size); -extern int BufFileSeek(BufFile *file, int fileno, off_t offset, int whence); -extern void BufFileTell(BufFile *file, int *fileno, off_t *offset); +extern int BufFileSeek(BufFile *file, int fileno, pgoff_t offset, int whence); +extern void BufFileTell(BufFile *file, int *fileno, pgoff_t *offset); extern int BufFileSeekBlock(BufFile *file, int64 blknum); extern int64 BufFileSize(BufFile *file); extern int64 BufFileAppend(BufFile *target, BufFile *source); @@ -54,6 +54,6 @@ extern BufFile *BufFileOpenFileSet(FileSet *fileset, const char *name, int mode, bool missing_ok); extern void BufFileDeleteFileSet(FileSet *fileset, const char *name, bool missing_ok); -extern void BufFileTruncateFileSet(BufFile *file, int fileno, off_t offset); +extern void BufFileTruncateFileSet(BufFile *file, int fileno, pgoff_t offset); #endif /* BUFFILE_H */ diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h index 41fdc1e76938e..6837b35fc6d0b 100644 --- a/src/include/storage/bufmgr.h +++ b/src/include/storage/bufmgr.h @@ -4,7 +4,7 @@ * POSTGRES buffer manager definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/bufmgr.h @@ -93,20 +93,30 @@ typedef enum ExtendBufferedFlags EB_LOCK_TARGET = (1 << 5), } ExtendBufferedFlags; +/* forward declared, to avoid including smgr.h here */ +typedef struct SMgrRelationData *SMgrRelation; + /* * Some functions identify relations either by relation or smgr + - * relpersistence. Used via the BMR_REL()/BMR_SMGR() macros below. This - * allows us to use the same function for both recovery and normal operation. + * relpersistence, initialized via the BMR_REL()/BMR_SMGR() macros below. + * This allows us to use the same function for both recovery and normal + * operation. When BMR_REL is used, it's not valid to cache its rd_smgr here, + * because our pointer would be obsolete in case of relcache invalidation. + * For simplicity, use BMR_GET_SMGR to read the smgr. */ typedef struct BufferManagerRelation { Relation rel; - struct SMgrRelationData *smgr; + SMgrRelation smgr; char relpersistence; } BufferManagerRelation; -#define BMR_REL(p_rel) ((BufferManagerRelation){.rel = p_rel}) -#define BMR_SMGR(p_smgr, p_relpersistence) ((BufferManagerRelation){.smgr = p_smgr, .relpersistence = p_relpersistence}) +#define BMR_REL(p_rel) \ + ((BufferManagerRelation){.rel = p_rel}) +#define BMR_SMGR(p_smgr, p_relpersistence) \ + ((BufferManagerRelation){.smgr = p_smgr, .relpersistence = p_relpersistence}) +#define BMR_GET_SMGR(bmr) \ + (RelationIsValid((bmr).rel) ? RelationGetSmgr((bmr).rel) : (bmr).smgr) /* Zero out page if reading fails. */ #define READ_BUFFERS_ZERO_ON_ERROR (1 << 0) @@ -122,7 +132,7 @@ struct ReadBuffersOperation { /* The following members should be set by the caller. */ Relation rel; /* optional */ - struct SMgrRelationData *smgr; + SMgrRelation smgr; char persistence; ForkNumber forknum; BufferAccessStrategy strategy; @@ -134,20 +144,19 @@ struct ReadBuffersOperation */ Buffer *buffers; BlockNumber blocknum; - int flags; + uint16 flags; int16 nblocks; int16 nblocks_done; + /* true if waiting on another backend's IO */ + bool foreign_io; PgAioWaitRef io_wref; PgAioReturn io_return; }; typedef struct ReadBuffersOperation ReadBuffersOperation; -/* forward declared, to avoid having to expose buf_internals.h here */ -struct WritebackContext; - -/* forward declared, to avoid including smgr.h here */ -struct SMgrRelationData; +/* to avoid having to expose buf_internals.h here */ +typedef struct WritebackContext WritebackContext; /* in globals.c ... this duplicates miscadmin.h */ extern PGDLLIMPORT int NBuffers; @@ -193,15 +202,31 @@ extern PGDLLIMPORT int32 *LocalRefCount; /* * Buffer content lock modes (mode argument for LockBuffer()) */ -#define BUFFER_LOCK_UNLOCK 0 -#define BUFFER_LOCK_SHARE 1 -#define BUFFER_LOCK_EXCLUSIVE 2 +typedef enum BufferLockMode +{ + BUFFER_LOCK_UNLOCK, + + /* + * A share lock conflicts with exclusive locks. + */ + BUFFER_LOCK_SHARE, + + /* + * A share-exclusive lock conflicts with itself and exclusive locks. + */ + BUFFER_LOCK_SHARE_EXCLUSIVE, + + /* + * An exclusive lock conflicts with every other lock type. + */ + BUFFER_LOCK_EXCLUSIVE, +} BufferLockMode; /* * prototypes for functions in bufmgr.c */ -extern PrefetchBufferResult PrefetchSharedBuffer(struct SMgrRelationData *smgr_reln, +extern PrefetchBufferResult PrefetchSharedBuffer(SMgrRelation smgr_reln, ForkNumber forkNum, BlockNumber blockNum); extern PrefetchBufferResult PrefetchBuffer(Relation reln, ForkNumber forkNum, @@ -226,11 +251,12 @@ extern bool StartReadBuffers(ReadBuffersOperation *operation, BlockNumber blockNum, int *nblocks, int flags); -extern void WaitReadBuffers(ReadBuffersOperation *operation); +extern bool WaitReadBuffers(ReadBuffersOperation *operation); extern void ReleaseBuffer(Buffer buffer); extern void UnlockReleaseBuffer(Buffer buffer); -extern bool BufferIsExclusiveLocked(Buffer buffer); +extern bool BufferIsLockedByMe(Buffer buffer); +extern bool BufferIsLockedByMeInMode(Buffer buffer, BufferLockMode mode); extern bool BufferIsDirty(Buffer buffer); extern void MarkBufferDirty(Buffer buffer); extern void IncrBufferRefCount(Buffer buffer); @@ -268,15 +294,15 @@ extern BlockNumber RelationGetNumberOfBlocksInFork(Relation relation, ForkNumber forkNum); extern void FlushOneBuffer(Buffer buffer); extern void FlushRelationBuffers(Relation rel); -extern void FlushRelationsAllBuffers(struct SMgrRelationData **smgrs, int nrels); +extern void FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels); extern void CreateAndCopyRelationData(RelFileLocator src_rlocator, RelFileLocator dst_rlocator, bool permanent); extern void FlushDatabaseBuffers(Oid dbid); -extern void DropRelationBuffers(struct SMgrRelationData *smgr_reln, +extern void DropRelationBuffers(SMgrRelation smgr_reln, ForkNumber *forkNum, int nforks, BlockNumber *firstDelBlock); -extern void DropRelationsAllBuffers(struct SMgrRelationData **smgr_reln, +extern void DropRelationsAllBuffers(SMgrRelation *smgr_reln, int nlocators); extern void DropDatabaseBuffers(Oid dbid); @@ -290,15 +316,36 @@ extern void BufferGetTag(Buffer buffer, RelFileLocator *rlocator, extern void MarkBufferDirtyHint(Buffer buffer, bool buffer_std); +extern bool BufferSetHintBits16(uint16 *ptr, uint16 val, Buffer buffer); +extern bool BufferBeginSetHintBits(Buffer buffer); +extern void BufferFinishSetHintBits(Buffer buffer, bool mark_dirty, bool buffer_std); + extern void UnlockBuffers(void); -extern void LockBuffer(Buffer buffer, int mode); +extern void UnlockBuffer(Buffer buffer); +extern void LockBufferInternal(Buffer buffer, BufferLockMode mode); + +/* + * Handling BUFFER_LOCK_UNLOCK in bufmgr.c leads to sufficiently worse branch + * prediction to impact performance. Therefore handle that switch here, where + * most of the time `mode` will be a constant and thus can be optimized out by + * the compiler. + */ +static inline void +LockBuffer(Buffer buffer, BufferLockMode mode) +{ + if (mode == BUFFER_LOCK_UNLOCK) + UnlockBuffer(buffer); + else + LockBufferInternal(buffer, mode); +} + extern bool ConditionalLockBuffer(Buffer buffer); extern void LockBufferForCleanup(Buffer buffer); extern bool ConditionalLockBufferForCleanup(Buffer buffer); extern bool IsBufferCleanupOK(Buffer buffer); extern bool HoldingBufferPinThatDelaysRecovery(void); -extern bool BgBufferSync(struct WritebackContext *wb_context); +extern bool BgBufferSync(WritebackContext *wb_context); extern uint32 GetPinLimit(void); extern uint32 GetLocalPinLimit(void); @@ -315,10 +362,14 @@ extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed, int32 *buffers_skipped); - -/* in buf_init.c */ -extern void BufferManagerShmemInit(void); -extern Size BufferManagerShmemSize(void); +extern bool MarkDirtyUnpinnedBuffer(Buffer buf, bool *buffer_already_dirty); +extern void MarkDirtyRelUnpinnedBuffers(Relation rel, + int32 *buffers_dirtied, + int32 *buffers_already_dirty, + int32 *buffers_skipped); +extern void MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied, + int32 *buffers_already_dirty, + int32 *buffers_skipped); /* in localbuf.c */ extern void AtProcExit_LocalBuffers(void); diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h index aeb67c498c59f..634e1e49ee52a 100644 --- a/src/include/storage/bufpage.h +++ b/src/include/storage/bufpage.h @@ -4,7 +4,7 @@ * Standard POSTGRES buffer page definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/bufpage.h @@ -16,7 +16,6 @@ #include "access/xlogdefs.h" #include "storage/block.h" -#include "storage/item.h" #include "storage/off.h" /* GUC variable */ @@ -92,23 +91,49 @@ typedef uint16 LocationIndex; /* - * For historical reasons, the 64-bit LSN value is stored as two 32-bit - * values. + * Store the LSN as a single 64-bit value, to allow atomic loads/stores. + * + * For historical reasons, the storage of 64-bit LSN values depends on CPU + * endianness; PageXLogRecPtr used to be a struct consisting of two 32-bit + * values. When reading (and writing) the pd_lsn field from page headers, the + * caller must convert from (and convert to) the platform's native endianness. */ typedef struct { - uint32 xlogid; /* high bits */ - uint32 xrecoff; /* low bits */ + uint64 lsn; } PageXLogRecPtr; +#ifdef WORDS_BIGENDIAN + +static inline XLogRecPtr +PageXLogRecPtrGet(const volatile PageXLogRecPtr *val) +{ + return val->lsn; +} + +static inline void +PageXLogRecPtrSet(volatile PageXLogRecPtr *ptr, XLogRecPtr lsn) +{ + ptr->lsn = lsn; +} + +#else + static inline XLogRecPtr -PageXLogRecPtrGet(PageXLogRecPtr val) +PageXLogRecPtrGet(const volatile PageXLogRecPtr *val) +{ + PageXLogRecPtr tmp = {val->lsn}; + + return (tmp.lsn << 32) | (tmp.lsn >> 32); +} + +static inline void +PageXLogRecPtrSet(volatile PageXLogRecPtr *ptr, XLogRecPtr lsn) { - return (uint64) val.xlogid << 32 | val.xrecoff; + ptr->lsn = (lsn << 32) | (lsn >> 32); } -#define PageXLogRecPtrSet(ptr, lsn) \ - ((ptr).xlogid = (uint32) ((lsn) >> 32), (ptr).xrecoff = (uint32) (lsn)) +#endif /* * disk page organization @@ -205,7 +230,6 @@ typedef PageHeaderData *PageHeader; * handling pages. */ #define PG_PAGE_LAYOUT_VERSION 4 -#define PG_DATA_CHECKSUM_VERSION 1 /* ---------------------------------------------------------------- * page support functions @@ -351,13 +375,13 @@ PageValidateSpecialPointer(const PageData *page) * This does not change the status of any of the resources passed. * The semantics may change in the future. */ -static inline Item -PageGetItem(const PageData *page, const ItemIdData *itemId) +static inline void * +PageGetItem(PageData *page, const ItemIdData *itemId) { Assert(page); Assert(ItemIdHasStorage(itemId)); - return (Item) (((const char *) page) + ItemIdGetOffset(itemId)); + return (char *) page + ItemIdGetOffset(itemId); } /* @@ -386,12 +410,13 @@ PageGetMaxOffsetNumber(const PageData *page) static inline XLogRecPtr PageGetLSN(const PageData *page) { - return PageXLogRecPtrGet(((const PageHeaderData *) page)->pd_lsn); + return PageXLogRecPtrGet(&((const PageHeaderData *) page)->pd_lsn); } + static inline void PageSetLSN(Page page, XLogRecPtr lsn) { - PageXLogRecPtrSet(((PageHeader) page)->pd_lsn, lsn); + PageXLogRecPtrSet(&((PageHeader) page)->pd_lsn, lsn); } static inline bool @@ -442,6 +467,12 @@ PageClearAllVisible(Page page) ((PageHeader) page)->pd_flags &= ~PD_ALL_VISIBLE; } +static inline TransactionId +PageGetPruneXid(const PageData *page) +{ + return ((const PageHeaderData *) page)->pd_prune_xid; +} + /* * These two require "access/transam.h", so left as macros. */ @@ -469,6 +500,7 @@ do { \ #define PIV_LOG_WARNING (1 << 0) #define PIV_LOG_LOG (1 << 1) #define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2) +#define PIV_ZERO_BUFFERS_ON_ERROR (1 << 3) #define PageAddItem(page, item, size, offsetNumber, overwrite, is_heap) \ PageAddItemExtended(page, item, size, offsetNumber, \ @@ -488,7 +520,7 @@ StaticAssertDecl(BLCKSZ == ((BLCKSZ / sizeof(size_t)) * sizeof(size_t)), extern void PageInit(Page page, Size pageSize, Size specialSize); extern bool PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_failure_p); -extern OffsetNumber PageAddItemExtended(Page page, Item item, Size size, +extern OffsetNumber PageAddItemExtended(Page page, const void *item, Size size, OffsetNumber offsetNumber, int flags); extern Page PageGetTempPage(const PageData *page); extern Page PageGetTempPageCopy(const PageData *page); @@ -504,8 +536,7 @@ extern void PageIndexTupleDelete(Page page, OffsetNumber offnum); extern void PageIndexMultiDelete(Page page, OffsetNumber *itemnos, int nitems); extern void PageIndexTupleDeleteNoCompact(Page page, OffsetNumber offnum); extern bool PageIndexTupleOverwrite(Page page, OffsetNumber offnum, - Item newtup, Size newsize); -extern char *PageSetChecksumCopy(Page page, BlockNumber blkno); -extern void PageSetChecksumInplace(Page page, BlockNumber blkno); + const void *newtup, Size newsize); +extern void PageSetChecksum(Page page, BlockNumber blkno); #endif /* BUFPAGE_H */ diff --git a/src/include/storage/bulk_write.h b/src/include/storage/bulk_write.h index 7885415f6cb8f..d430f64eaacc7 100644 --- a/src/include/storage/bulk_write.h +++ b/src/include/storage/bulk_write.h @@ -4,7 +4,7 @@ * Efficiently and reliably populate a new relation * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/bulk_write.h @@ -28,10 +28,10 @@ typedef struct BulkWriteState BulkWriteState; typedef PGIOAlignedBlock *BulkWriteBuffer; /* forward declared from smgr.h */ -struct SMgrRelationData; +typedef struct SMgrRelationData *SMgrRelation; extern BulkWriteState *smgr_bulk_start_rel(Relation rel, ForkNumber forknum); -extern BulkWriteState *smgr_bulk_start_smgr(struct SMgrRelationData *smgr, ForkNumber forknum, bool use_wal); +extern BulkWriteState *smgr_bulk_start_smgr(SMgrRelation smgr, ForkNumber forknum, bool use_wal); extern BulkWriteBuffer smgr_bulk_get_buf(BulkWriteState *bulkstate); extern void smgr_bulk_write(BulkWriteState *bulkstate, BlockNumber blocknum, BulkWriteBuffer buf, bool page_std); diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h index 25d13a798d102..3b1440c0c95b0 100644 --- a/src/include/storage/checksum.h +++ b/src/include/storage/checksum.h @@ -3,7 +3,7 @@ * checksum.h * Checksum implementation for data pages. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/checksum.h @@ -15,6 +15,22 @@ #include "storage/block.h" +/* + * Checksum state 0 is used for when data checksums are disabled (OFF). + * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either + * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines + * that data checksums are enabled. The ChecksumStateType is stored in + * pg_control so changing requires a catversion bump, and the values cannot + * be reordered. New states must be added at the end. + */ +typedef enum ChecksumStateType +{ + PG_DATA_CHECKSUM_OFF = 0, + PG_DATA_CHECKSUM_VERSION = 1, + PG_DATA_CHECKSUM_INPROGRESS_OFF = 2, + PG_DATA_CHECKSUM_INPROGRESS_ON = 3, +} ChecksumStateType; + /* * Compute the checksum for a Postgres page. The page must be aligned on a * 4-byte boundary. diff --git a/src/include/storage/checksum_block_internal.h b/src/include/storage/checksum_block_internal.h new file mode 100644 index 0000000000000..b8338fed08dc0 --- /dev/null +++ b/src/include/storage/checksum_block_internal.h @@ -0,0 +1,42 @@ +/*------------------------------------------------------------------------- + * + * checksum_block_internal.h + * Core algorithm for page checksums, semi-private to checksum_impl.h + * and checksum.c. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/checksum_block_internal.h + * + *------------------------------------------------------------------------- + */ + +/* there is deliberately not an #ifndef CHECKSUM_BLOCK_INTERNAL_H here */ + +uint32 sums[N_SUMS]; +uint32 result = 0; +uint32 i, + j; + +/* ensure that the size is compatible with the algorithm */ +Assert(sizeof(PGChecksummablePage) == BLCKSZ); + +/* initialize partial checksums to their corresponding offsets */ +memcpy(sums, checksumBaseOffsets, sizeof(checksumBaseOffsets)); + +/* main checksum calculation */ +for (i = 0; i < (uint32) (BLCKSZ / (sizeof(uint32) * N_SUMS)); i++) + for (j = 0; j < N_SUMS; j++) + CHECKSUM_COMP(sums[j], page->data[i][j]); + +/* finally add in two rounds of zeroes for additional mixing */ +for (i = 0; i < 2; i++) + for (j = 0; j < N_SUMS; j++) + CHECKSUM_COMP(sums[j], 0); + +/* xor fold partial checksums together */ +for (i = 0; i < N_SUMS; i++) + result ^= sums[i]; + +return result; diff --git a/src/include/storage/checksum_impl.h b/src/include/storage/checksum_impl.h index da87d61ba5242..576673b7b8505 100644 --- a/src/include/storage/checksum_impl.h +++ b/src/include/storage/checksum_impl.h @@ -8,7 +8,7 @@ * referenced by storage/checksum.h. (Note: you may need to redefine * Assert() as empty to compile this successfully externally.) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/checksum_impl.h @@ -72,12 +72,13 @@ * random segments of page with 0x00, 0xFF and random data all show optimal * 2e-16 false positive rate within margin of error. * - * Vectorization of the algorithm requires 32bit x 32bit -> 32bit integer - * multiplication instruction. As of 2013 the corresponding instruction is - * available on x86 SSE4.1 extensions (pmulld) and ARM NEON (vmul.i32). - * Vectorization requires a compiler to do the vectorization for us. For recent - * GCC versions the flags -msse4.1 -funroll-loops -ftree-vectorize are enough - * to achieve vectorization. + * Vectorization of the algorithm works best with a 32bit x 32bit -> 32bit + * vector integer multiplication instruction, Examples include x86 AVX2 + * extensions (vpmulld) and ARM NEON (vmul.i32). Without that, vectorization + * is still possible if the compiler can turn multiplication by FNV_PRIME + * into a sequence of vectorized shifts and adds. For simplicity we rely + * on the compiler to do the vectorization for us. For GCC and clang the + * flags -funroll-loops -ftree-vectorize are enough to achieve vectorization. * * The optimal amount of parallelism to use depends on CPU specific instruction * latency, SIMD instruction width, throughput and the amount of registers @@ -89,8 +90,9 @@ * * The parallelism number 32 was chosen based on the fact that it is the * largest state that fits into architecturally visible x86 SSE registers while - * leaving some free registers for intermediate values. For future processors - * with 256bit vector registers this will leave some performance on the table. + * leaving some free registers for intermediate values. For processors + * with 256-bit vector registers this leaves some performance on the table. + * * When vectorization is not available it might be beneficial to restructure * the computation to calculate a subset of the columns at a time and perform * multiple passes to avoid register spilling. This optimization opportunity @@ -142,37 +144,20 @@ do { \ * Block checksum algorithm. The page must be adequately aligned * (at least on 4-byte boundary). */ +#ifdef PG_CHECKSUM_INTERNAL +/* definitions in src/backend/storage/page/checksum.c */ +static uint32 (*pg_checksum_block) (const PGChecksummablePage *page); + +#else +/* static definition for external programs */ static uint32 pg_checksum_block(const PGChecksummablePage *page) { - uint32 sums[N_SUMS]; - uint32 result = 0; - uint32 i, - j; - - /* ensure that the size is compatible with the algorithm */ - Assert(sizeof(PGChecksummablePage) == BLCKSZ); - - /* initialize partial checksums to their corresponding offsets */ - memcpy(sums, checksumBaseOffsets, sizeof(checksumBaseOffsets)); - - /* main checksum calculation */ - for (i = 0; i < (uint32) (BLCKSZ / (sizeof(uint32) * N_SUMS)); i++) - for (j = 0; j < N_SUMS; j++) - CHECKSUM_COMP(sums[j], page->data[i][j]); - - /* finally add in two rounds of zeroes for additional mixing */ - for (i = 0; i < 2; i++) - for (j = 0; j < N_SUMS; j++) - CHECKSUM_COMP(sums[j], 0); - - /* xor fold partial checksums together */ - for (i = 0; i < N_SUMS; i++) - result ^= sums[i]; - - return result; +#include "storage/checksum_block_internal.h" } +#endif + /* * Compute the checksum for a Postgres page. * diff --git a/src/include/storage/condition_variable.h b/src/include/storage/condition_variable.h index 526c4ee105137..14bd6dd55c033 100644 --- a/src/include/storage/condition_variable.h +++ b/src/include/storage/condition_variable.h @@ -12,7 +12,7 @@ * can be canceled prior to the fulfillment of the condition) and do not * use pointers internally (so that they are safe to use within DSMs). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/condition_variable.h diff --git a/src/include/storage/copydir.h b/src/include/storage/copydir.h index 940d74462d129..40849c2ef9947 100644 --- a/src/include/storage/copydir.h +++ b/src/include/storage/copydir.h @@ -3,7 +3,7 @@ * copydir.h * Copy a directory. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/copydir.h @@ -17,7 +17,7 @@ typedef enum FileCopyMethod { FILE_COPY_METHOD_COPY, FILE_COPY_METHOD_CLONE, -} FileCopyMethod; +} FileCopyMethod; /* GUC parameters */ extern PGDLLIMPORT int file_copy_method; diff --git a/src/include/storage/dsm.h b/src/include/storage/dsm.h index 2302cc7f40b4b..1bde71b4406d1 100644 --- a/src/include/storage/dsm.h +++ b/src/include/storage/dsm.h @@ -3,7 +3,7 @@ * dsm.h * manage dynamic shared memory segments * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/dsm.h @@ -20,15 +20,12 @@ typedef struct dsm_segment dsm_segment; #define DSM_CREATE_NULL_IF_MAXSEGMENTS 0x0001 /* Startup and shutdown functions. */ -struct PGShmemHeader; /* avoid including pg_shmem.h */ +typedef struct PGShmemHeader PGShmemHeader; /* avoid including pg_shmem.h */ extern void dsm_cleanup_using_control_segment(dsm_handle old_control_handle); -extern void dsm_postmaster_startup(struct PGShmemHeader *); +extern void dsm_postmaster_startup(PGShmemHeader *); extern void dsm_backend_shutdown(void); extern void dsm_detach_all(void); -extern size_t dsm_estimate_size(void); -extern void dsm_shmem_init(void); - #ifdef EXEC_BACKEND extern void dsm_set_control_handle(dsm_handle h); #endif diff --git a/src/include/storage/dsm_impl.h b/src/include/storage/dsm_impl.h index 7248cb1f9acc6..299a198998752 100644 --- a/src/include/storage/dsm_impl.h +++ b/src/include/storage/dsm_impl.h @@ -3,7 +3,7 @@ * dsm_impl.h * low-level dynamic shared memory primitives * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/dsm_impl.h diff --git a/src/include/storage/dsm_registry.h b/src/include/storage/dsm_registry.h index b381e44bc9d87..a2269c89f0154 100644 --- a/src/include/storage/dsm_registry.h +++ b/src/include/storage/dsm_registry.h @@ -3,7 +3,7 @@ * dsm_registry.h * Functions for interfacing with the dynamic shared memory registry. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/dsm_registry.h @@ -13,11 +13,14 @@ #ifndef DSM_REGISTRY_H #define DSM_REGISTRY_H -extern void *GetNamedDSMSegment(const char *name, size_t size, - void (*init_callback) (void *ptr), - bool *found); +#include "lib/dshash.h" -extern Size DSMRegistryShmemSize(void); -extern void DSMRegistryShmemInit(void); +extern void *GetNamedDSMSegment(const char *name, size_t size, + void (*init_callback) (void *ptr, void *arg), + bool *found, void *arg); +extern dsa_area *GetNamedDSA(const char *name, bool *found); +extern dshash_table *GetNamedDSHash(const char *name, + const dshash_parameters *params, + bool *found); #endif /* DSM_REGISTRY_H */ diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h index b77d8e5e30e3a..8ac466fd3469a 100644 --- a/src/include/storage/fd.h +++ b/src/include/storage/fd.h @@ -4,7 +4,7 @@ * Virtual file descriptor definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/fd.h @@ -55,12 +55,23 @@ typedef int File; #define IO_DIRECT_WAL 0x02 #define IO_DIRECT_WAL_INIT 0x04 +enum FileExtendMethod +{ +#ifdef HAVE_POSIX_FALLOCATE + FILE_EXTEND_METHOD_POSIX_FALLOCATE, +#endif + FILE_EXTEND_METHOD_WRITE_ZEROS, +}; + +/* Default to the first available file_extend_method. */ +#define DEFAULT_FILE_EXTEND_METHOD 0 /* GUC parameter */ extern PGDLLIMPORT int max_files_per_process; extern PGDLLIMPORT bool data_sync_retry; extern PGDLLIMPORT int recovery_init_sync_method; extern PGDLLIMPORT int io_direct_flags; +extern PGDLLIMPORT int file_extend_method; /* * This is private to fd.c, but exported for save/restore_backend_variables() @@ -85,14 +96,29 @@ extern PGDLLIMPORT int max_safe_fds; * to the appropriate Windows flag in src/port/open.c. We simulate it with * fcntl(F_NOCACHE) on macOS inside fd.c's open() wrapper. We use the name * PG_O_DIRECT rather than defining O_DIRECT in that case (probably not a good - * idea on a Unix). We can only use it if the compiler will correctly align - * PGIOAlignedBlock for us, though. + * idea on a Unix). */ -#if defined(O_DIRECT) && defined(pg_attribute_aligned) +#if defined(O_DIRECT) #define PG_O_DIRECT O_DIRECT #elif defined(F_NOCACHE) #define PG_O_DIRECT 0x80000000 #define PG_O_DIRECT_USE_F_NOCACHE +/* + * The value we defined to stand in for O_DIRECT when simulating it with + * F_NOCACHE had better not collide with any of the standard flags. + */ +StaticAssertDecl((PG_O_DIRECT & + (O_APPEND | + O_CLOEXEC | + O_CREAT | + O_DSYNC | + O_EXCL | + O_RDWR | + O_RDONLY | + O_SYNC | + O_TRUNC | + O_WRONLY)) == 0, + "PG_O_DIRECT value collides with standard flag"); #else #define PG_O_DIRECT 0 #endif @@ -108,17 +134,17 @@ extern File PathNameOpenFile(const char *fileName, int fileFlags); extern File PathNameOpenFilePerm(const char *fileName, int fileFlags, mode_t fileMode); extern File OpenTemporaryFile(bool interXact); extern void FileClose(File file); -extern int FilePrefetch(File file, off_t offset, off_t amount, uint32 wait_event_info); -extern ssize_t FileReadV(File file, const struct iovec *iov, int iovcnt, off_t offset, uint32 wait_event_info); -extern ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, off_t offset, uint32 wait_event_info); -extern int FileStartReadV(struct PgAioHandle *ioh, File file, int iovcnt, off_t offset, uint32 wait_event_info); +extern int FilePrefetch(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info); +extern ssize_t FileReadV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info); +extern ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info); +extern int FileStartReadV(struct PgAioHandle *ioh, File file, int iovcnt, pgoff_t offset, uint32 wait_event_info); extern int FileSync(File file, uint32 wait_event_info); -extern int FileZero(File file, off_t offset, off_t amount, uint32 wait_event_info); -extern int FileFallocate(File file, off_t offset, off_t amount, uint32 wait_event_info); +extern int FileZero(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info); +extern int FileFallocate(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info); -extern off_t FileSize(File file); -extern int FileTruncate(File file, off_t offset, uint32 wait_event_info); -extern void FileWriteback(File file, off_t offset, off_t nbytes, uint32 wait_event_info); +extern pgoff_t FileSize(File file); +extern int FileTruncate(File file, pgoff_t offset, uint32 wait_event_info); +extern void FileWriteback(File file, pgoff_t offset, pgoff_t nbytes, uint32 wait_event_info); extern char *FilePathName(File file); extern int FileGetRawDesc(File file); extern int FileGetRawFlags(File file); @@ -186,8 +212,8 @@ extern int pg_fsync_no_writethrough(int fd); extern int pg_fsync_writethrough(int fd); extern int pg_fdatasync(int fd); extern bool pg_file_exists(const char *name); -extern void pg_flush_data(int fd, off_t offset, off_t nbytes); -extern int pg_truncate(const char *path, off_t length); +extern void pg_flush_data(int fd, pgoff_t offset, pgoff_t nbytes); +extern int pg_truncate(const char *path, pgoff_t length); extern void fsync_fname(const char *fname, bool isdir); extern int fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel); extern int durable_rename(const char *oldfile, const char *newfile, int elevel); @@ -196,7 +222,7 @@ extern void SyncDataDirectory(void); extern int data_sync_elevel(int elevel); static inline ssize_t -FileRead(File file, void *buffer, size_t amount, off_t offset, +FileRead(File file, void *buffer, size_t amount, pgoff_t offset, uint32 wait_event_info) { struct iovec iov = { @@ -208,7 +234,7 @@ FileRead(File file, void *buffer, size_t amount, off_t offset, } static inline ssize_t -FileWrite(File file, const void *buffer, size_t amount, off_t offset, +FileWrite(File file, const void *buffer, size_t amount, pgoff_t offset, uint32 wait_event_info) { struct iovec iov = { diff --git a/src/include/storage/fileset.h b/src/include/storage/fileset.h index 80250015f5ec1..c2e96773a3651 100644 --- a/src/include/storage/fileset.h +++ b/src/include/storage/fileset.h @@ -3,7 +3,7 @@ * fileset.h * Management of named temporary files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/fileset.h diff --git a/src/include/storage/freespace.h b/src/include/storage/freespace.h index 427cb6a0df947..1d5945b754da8 100644 --- a/src/include/storage/freespace.h +++ b/src/include/storage/freespace.h @@ -4,7 +4,7 @@ * POSTGRES free space map for quickly finding free space in relations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/freespace.h diff --git a/src/include/storage/fsm_internals.h b/src/include/storage/fsm_internals.h index 77b48d1ebc77a..9416ec7d4845c 100644 --- a/src/include/storage/fsm_internals.h +++ b/src/include/storage/fsm_internals.h @@ -4,7 +4,7 @@ * internal functions for free space map * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/fsm_internals.h diff --git a/src/include/storage/indexfsm.h b/src/include/storage/indexfsm.h index 39ebb42ac8b7f..7174bcdff9960 100644 --- a/src/include/storage/indexfsm.h +++ b/src/include/storage/indexfsm.h @@ -4,7 +4,7 @@ * POSTGRES free space map for quickly finding an unused page in index * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/indexfsm.h diff --git a/src/include/storage/io_worker.h b/src/include/storage/io_worker.h index 7bde7e89c8a8d..c852c9f374143 100644 --- a/src/include/storage/io_worker.h +++ b/src/include/storage/io_worker.h @@ -4,10 +4,10 @@ * IO worker for implementing AIO "ourselves" * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * src/include/storage/io.h + * src/include/storage/io_worker.h * *------------------------------------------------------------------------- */ @@ -17,6 +17,15 @@ pg_noreturn extern void IoWorkerMain(const void *startup_data, size_t startup_data_len); -extern PGDLLIMPORT int io_workers; +/* Public GUCs. */ +extern PGDLLIMPORT int io_min_workers; +extern PGDLLIMPORT int io_max_workers; +extern PGDLLIMPORT int io_worker_idle_timeout; +extern PGDLLIMPORT int io_worker_launch_interval; + +/* Interfaces visible to the postmaster. */ +extern bool pgaio_worker_pm_test_grow_signal_sent(void); +extern void pgaio_worker_pm_clear_grow_signal_sent(void); +extern bool pgaio_worker_pm_test_grow(void); #endif /* IO_WORKER_H */ diff --git a/src/include/storage/ipc.h b/src/include/storage/ipc.h index 3baf418b3d1ed..b205b00e7a1ab 100644 --- a/src/include/storage/ipc.h +++ b/src/include/storage/ipc.h @@ -8,7 +8,7 @@ * exit-time cleanup for either a postmaster or a backend. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/ipc.h @@ -77,7 +77,8 @@ extern void check_on_shmem_exit_lists_are_empty(void); /* ipci.c */ extern PGDLLIMPORT shmem_startup_hook_type shmem_startup_hook; -extern Size CalculateShmemSize(int *num_semaphores); +extern void RegisterBuiltinShmemCallbacks(void); +extern Size CalculateShmemSize(void); extern void CreateSharedMemoryAndSemaphores(void); #ifdef EXEC_BACKEND extern void AttachSharedMemoryStructs(void); diff --git a/src/include/storage/item.h b/src/include/storage/item.h deleted file mode 100644 index 5a4539cc381b0..0000000000000 --- a/src/include/storage/item.h +++ /dev/null @@ -1,19 +0,0 @@ -/*------------------------------------------------------------------------- - * - * item.h - * POSTGRES disk item definitions. - * - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * src/include/storage/item.h - * - *------------------------------------------------------------------------- - */ -#ifndef ITEM_H -#define ITEM_H - -typedef Pointer Item; - -#endif /* ITEM_H */ diff --git a/src/include/storage/itemid.h b/src/include/storage/itemid.h index bfefacaab5c5e..f902e66bae52f 100644 --- a/src/include/storage/itemid.h +++ b/src/include/storage/itemid.h @@ -4,7 +4,7 @@ * Standard POSTGRES buffer page item identifier/line pointer definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/itemid.h @@ -83,7 +83,7 @@ typedef uint16 ItemLength; * True iff item identifier is valid. * This is a pretty weak test, probably useful only in Asserts. */ -#define ItemIdIsValid(itemId) PointerIsValid(itemId) +#define ItemIdIsValid(itemId) ((itemId) != NULL) /* * ItemIdIsUsed diff --git a/src/include/storage/itemptr.h b/src/include/storage/itemptr.h index 74b87a9114a57..8b3392dbefef1 100644 --- a/src/include/storage/itemptr.h +++ b/src/include/storage/itemptr.h @@ -4,7 +4,7 @@ * POSTGRES disk item pointer definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/itemptr.h @@ -82,7 +82,7 @@ typedef ItemPointerData *ItemPointer; static inline bool ItemPointerIsValid(const ItemPointerData *pointer) { - return PointerIsValid(pointer) && pointer->ip_posid != 0; + return pointer && pointer->ip_posid != 0; } /* @@ -134,7 +134,7 @@ ItemPointerGetOffsetNumber(const ItemPointerData *pointer) static inline void ItemPointerSet(ItemPointerData *pointer, BlockNumber blockNumber, OffsetNumber offNum) { - Assert(PointerIsValid(pointer)); + Assert(pointer); BlockIdSet(&pointer->ip_blkid, blockNumber); pointer->ip_posid = offNum; } @@ -146,7 +146,7 @@ ItemPointerSet(ItemPointerData *pointer, BlockNumber blockNumber, OffsetNumber o static inline void ItemPointerSetBlockNumber(ItemPointerData *pointer, BlockNumber blockNumber) { - Assert(PointerIsValid(pointer)); + Assert(pointer); BlockIdSet(&pointer->ip_blkid, blockNumber); } @@ -157,7 +157,7 @@ ItemPointerSetBlockNumber(ItemPointerData *pointer, BlockNumber blockNumber) static inline void ItemPointerSetOffsetNumber(ItemPointerData *pointer, OffsetNumber offsetNumber) { - Assert(PointerIsValid(pointer)); + Assert(pointer); pointer->ip_posid = offsetNumber; } @@ -171,8 +171,8 @@ ItemPointerSetOffsetNumber(ItemPointerData *pointer, OffsetNumber offsetNumber) static inline void ItemPointerCopy(const ItemPointerData *fromPointer, ItemPointerData *toPointer) { - Assert(PointerIsValid(toPointer)); - Assert(PointerIsValid(fromPointer)); + Assert(toPointer); + Assert(fromPointer); *toPointer = *fromPointer; } @@ -183,7 +183,7 @@ ItemPointerCopy(const ItemPointerData *fromPointer, ItemPointerData *toPointer) static inline void ItemPointerSetInvalid(ItemPointerData *pointer) { - Assert(PointerIsValid(pointer)); + Assert(pointer); BlockIdSet(&pointer->ip_blkid, InvalidBlockNumber); pointer->ip_posid = InvalidOffsetNumber; } @@ -217,8 +217,8 @@ ItemPointerSetMovedPartitions(ItemPointerData *pointer) * ---------------- */ -extern bool ItemPointerEquals(ItemPointer pointer1, ItemPointer pointer2); -extern int32 ItemPointerCompare(ItemPointer arg1, ItemPointer arg2); +extern bool ItemPointerEquals(const ItemPointerData *pointer1, const ItemPointerData *pointer2); +extern int32 ItemPointerCompare(const ItemPointerData *arg1, const ItemPointerData *arg2); extern void ItemPointerInc(ItemPointer pointer); extern void ItemPointerDec(ItemPointer pointer); diff --git a/src/include/storage/large_object.h b/src/include/storage/large_object.h index 6fecf44244636..0291e4e249817 100644 --- a/src/include/storage/large_object.h +++ b/src/include/storage/large_object.h @@ -5,7 +5,7 @@ * zillions of large objects (internal, external, jaquith, inversion). * Now we only support inversion. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/large_object.h diff --git a/src/include/storage/latch.h b/src/include/storage/latch.h index e41dc70785afe..053a32ecf9d3e 100644 --- a/src/include/storage/latch.h +++ b/src/include/storage/latch.h @@ -91,7 +91,7 @@ * WaitLatchOrSocket. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/latch.h @@ -104,6 +104,8 @@ #include #include "storage/waiteventset.h" /* for WL_* arguments to WaitLatch */ +#include "utils/wait_classes.h" /* for backward compatibility */ /* IWYU pragma: keep */ + /* * Latch structure should be treated as opaque and only accessed through diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 58eee4e0d54c2..2a985ce5e154d 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -4,7 +4,7 @@ * POSTGRES lock manager definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/lmgr.h @@ -16,7 +16,7 @@ #include "lib/stringinfo.h" #include "storage/itemptr.h" -#include "storage/lock.h" +#include "storage/locktag.h" #include "utils/rel.h" @@ -71,16 +71,16 @@ extern bool ConditionalLockPage(Relation relation, BlockNumber blkno, LOCKMODE l extern void UnlockPage(Relation relation, BlockNumber blkno, LOCKMODE lockmode); /* Lock a tuple (see heap_lock_tuple before assuming you understand this) */ -extern void LockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode); -extern bool ConditionalLockTuple(Relation relation, ItemPointer tid, +extern void LockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE lockmode); +extern bool ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE lockmode, bool logLockFailure); -extern void UnlockTuple(Relation relation, ItemPointer tid, LOCKMODE lockmode); +extern void UnlockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE lockmode); /* Lock an XID (used to wait for a transaction to finish) */ extern void XactLockTableInsert(TransactionId xid); extern void XactLockTableDelete(TransactionId xid); extern void XactLockTableWait(TransactionId xid, Relation rel, - ItemPointer ctid, XLTW_Oper oper); + const ItemPointerData *ctid, XLTW_Oper oper); extern bool ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index 6f2108a44e8fb..ee3cb1dc203ca 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -4,7 +4,7 @@ * POSTGRES low-level lock mechanism * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/lock.h @@ -18,8 +18,10 @@ #error "lock.h may not be included from frontend code" #endif +#include "access/transam.h" #include "lib/ilist.h" #include "storage/lockdefs.h" +#include "storage/locktag.h" #include "storage/lwlock.h" #include "storage/procnumber.h" #include "storage/shmem.h" @@ -30,7 +32,7 @@ typedef struct PGPROC PGPROC; /* GUC variables */ extern PGDLLIMPORT int max_locks_per_xact; -extern PGDLLIMPORT bool log_lock_failure; +extern PGDLLIMPORT bool log_lock_failures; #ifdef LOCK_DEBUG extern PGDLLIMPORT int Trace_lock_oidmin; @@ -57,7 +59,7 @@ extern PGDLLIMPORT bool Debug_deadlocks; * coding errors from trying to use struct assignment with it; instead use * GET_VXID_FROM_PGPROC(). */ -typedef struct +typedef struct VirtualTransactionId { ProcNumber procNumber; /* proc number of the PGPROC */ LocalTransactionId localTransactionId; /* lxid from PGPROC */ @@ -116,178 +118,6 @@ typedef struct LockMethodData typedef const LockMethodData *LockMethod; -/* - * Lock methods are identified by LOCKMETHODID. (Despite the declaration as - * uint16, we are constrained to 256 lockmethods by the layout of LOCKTAG.) - */ -typedef uint16 LOCKMETHODID; - -/* These identify the known lock methods */ -#define DEFAULT_LOCKMETHOD 1 -#define USER_LOCKMETHOD 2 - -/* - * LOCKTAG is the key information needed to look up a LOCK item in the - * lock hashtable. A LOCKTAG value uniquely identifies a lockable object. - * - * The LockTagType enum defines the different kinds of objects we can lock. - * We can handle up to 256 different LockTagTypes. - */ -typedef enum LockTagType -{ - LOCKTAG_RELATION, /* whole relation */ - LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */ - LOCKTAG_DATABASE_FROZEN_IDS, /* pg_database.datfrozenxid */ - LOCKTAG_PAGE, /* one page of a relation */ - LOCKTAG_TUPLE, /* one physical tuple */ - LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */ - LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */ - LOCKTAG_SPECULATIVE_TOKEN, /* speculative insertion Xid and token */ - LOCKTAG_OBJECT, /* non-relation database object */ - LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */ - LOCKTAG_ADVISORY, /* advisory user locks */ - LOCKTAG_APPLY_TRANSACTION, /* transaction being applied on a logical - * replication subscriber */ -} LockTagType; - -#define LOCKTAG_LAST_TYPE LOCKTAG_APPLY_TRANSACTION - -extern PGDLLIMPORT const char *const LockTagTypeNames[]; - -/* - * The LOCKTAG struct is defined with malice aforethought to fit into 16 - * bytes with no padding. Note that this would need adjustment if we were - * to widen Oid, BlockNumber, or TransactionId to more than 32 bits. - * - * We include lockmethodid in the locktag so that a single hash table in - * shared memory can store locks of different lockmethods. - */ -typedef struct LOCKTAG -{ - uint32 locktag_field1; /* a 32-bit ID field */ - uint32 locktag_field2; /* a 32-bit ID field */ - uint32 locktag_field3; /* a 32-bit ID field */ - uint16 locktag_field4; /* a 16-bit ID field */ - uint8 locktag_type; /* see enum LockTagType */ - uint8 locktag_lockmethodid; /* lockmethod indicator */ -} LOCKTAG; - -/* - * These macros define how we map logical IDs of lockable objects into - * the physical fields of LOCKTAG. Use these to set up LOCKTAG values, - * rather than accessing the fields directly. Note multiple eval of target! - */ - -/* ID info for a relation is DB OID + REL OID; DB OID = 0 if shared */ -#define SET_LOCKTAG_RELATION(locktag,dboid,reloid) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = (reloid), \ - (locktag).locktag_field3 = 0, \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_RELATION, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* same ID info as RELATION */ -#define SET_LOCKTAG_RELATION_EXTEND(locktag,dboid,reloid) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = (reloid), \ - (locktag).locktag_field3 = 0, \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_RELATION_EXTEND, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* ID info for frozen IDs is DB OID */ -#define SET_LOCKTAG_DATABASE_FROZEN_IDS(locktag,dboid) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = 0, \ - (locktag).locktag_field3 = 0, \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_DATABASE_FROZEN_IDS, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* ID info for a page is RELATION info + BlockNumber */ -#define SET_LOCKTAG_PAGE(locktag,dboid,reloid,blocknum) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = (reloid), \ - (locktag).locktag_field3 = (blocknum), \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_PAGE, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* ID info for a tuple is PAGE info + OffsetNumber */ -#define SET_LOCKTAG_TUPLE(locktag,dboid,reloid,blocknum,offnum) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = (reloid), \ - (locktag).locktag_field3 = (blocknum), \ - (locktag).locktag_field4 = (offnum), \ - (locktag).locktag_type = LOCKTAG_TUPLE, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* ID info for a transaction is its TransactionId */ -#define SET_LOCKTAG_TRANSACTION(locktag,xid) \ - ((locktag).locktag_field1 = (xid), \ - (locktag).locktag_field2 = 0, \ - (locktag).locktag_field3 = 0, \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_TRANSACTION, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* ID info for a virtual transaction is its VirtualTransactionId */ -#define SET_LOCKTAG_VIRTUALTRANSACTION(locktag,vxid) \ - ((locktag).locktag_field1 = (vxid).procNumber, \ - (locktag).locktag_field2 = (vxid).localTransactionId, \ - (locktag).locktag_field3 = 0, \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_VIRTUALTRANSACTION, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* - * ID info for a speculative insert is TRANSACTION info + - * its speculative insert counter. - */ -#define SET_LOCKTAG_SPECULATIVE_INSERTION(locktag,xid,token) \ - ((locktag).locktag_field1 = (xid), \ - (locktag).locktag_field2 = (token), \ - (locktag).locktag_field3 = 0, \ - (locktag).locktag_field4 = 0, \ - (locktag).locktag_type = LOCKTAG_SPECULATIVE_TOKEN, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -/* - * ID info for an object is DB OID + CLASS OID + OBJECT OID + SUBID - * - * Note: object ID has same representation as in pg_depend and - * pg_description, but notice that we are constraining SUBID to 16 bits. - * Also, we use DB OID = 0 for shared objects such as tablespaces. - */ -#define SET_LOCKTAG_OBJECT(locktag,dboid,classoid,objoid,objsubid) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = (classoid), \ - (locktag).locktag_field3 = (objoid), \ - (locktag).locktag_field4 = (objsubid), \ - (locktag).locktag_type = LOCKTAG_OBJECT, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - -#define SET_LOCKTAG_ADVISORY(locktag,id1,id2,id3,id4) \ - ((locktag).locktag_field1 = (id1), \ - (locktag).locktag_field2 = (id2), \ - (locktag).locktag_field3 = (id3), \ - (locktag).locktag_field4 = (id4), \ - (locktag).locktag_type = LOCKTAG_ADVISORY, \ - (locktag).locktag_lockmethodid = USER_LOCKMETHOD) - -/* - * ID info for a remote transaction on a logical replication subscriber is: DB - * OID + SUBSCRIPTION OID + TRANSACTION ID + OBJID - */ -#define SET_LOCKTAG_APPLY_TRANSACTION(locktag,dboid,suboid,xid,objid) \ - ((locktag).locktag_field1 = (dboid), \ - (locktag).locktag_field2 = (suboid), \ - (locktag).locktag_field3 = (xid), \ - (locktag).locktag_field4 = (objid), \ - (locktag).locktag_type = LOCKTAG_APPLY_TRANSACTION, \ - (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) - /* * Per-locked-object lock information: * @@ -545,8 +375,6 @@ typedef enum /* * function prototypes */ -extern void LockManagerShmemInit(void); -extern Size LockManagerShmemSize(void); extern void InitLockManagerAccess(void); extern LockMethod GetLocksMethodTable(const LOCK *lock); extern LockMethod GetLockTagsMethodTable(const LOCKTAG *locktag); @@ -581,7 +409,7 @@ extern bool LockHasWaiters(const LOCKTAG *locktag, extern VirtualTransactionId *GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp); extern void AtPrepare_Locks(void); -extern void PostPrepare_Locks(TransactionId xid); +extern void PostPrepare_Locks(FullTransactionId fxid); extern bool LockCheckConflicts(LockMethod lockMethodTable, LOCKMODE lockmode, LOCK *lock, PROCLOCK *proclock); @@ -597,13 +425,13 @@ extern BlockedProcsData *GetBlockerStatusData(int blocked_pid); extern xl_standby_lock *GetRunningTransactionLocks(int *nlocks); extern const char *GetLockmodeName(LOCKMETHODID lockmethodid, LOCKMODE mode); -extern void lock_twophase_recover(TransactionId xid, uint16 info, +extern void lock_twophase_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); -extern void lock_twophase_postcommit(TransactionId xid, uint16 info, +extern void lock_twophase_postcommit(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); -extern void lock_twophase_postabort(TransactionId xid, uint16 info, +extern void lock_twophase_postabort(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); -extern void lock_twophase_standby_recover(TransactionId xid, uint16 info, +extern void lock_twophase_standby_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); extern DeadLockState DeadLockCheck(PGPROC *proc); diff --git a/src/include/storage/lockdefs.h b/src/include/storage/lockdefs.h index 7f3ba0352f655..3785b009808d8 100644 --- a/src/include/storage/lockdefs.h +++ b/src/include/storage/lockdefs.h @@ -7,7 +7,7 @@ * contains definition that have to (indirectly) be available when included by * FRONTEND code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/lockdefs.h @@ -36,8 +36,8 @@ typedef int LOCKMODE; #define AccessShareLock 1 /* SELECT */ #define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */ #define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */ -#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL), ANALYZE, CREATE - * INDEX CONCURRENTLY */ +#define ShareUpdateExclusiveLock 4 /* VACUUM (non-exclusive), ANALYZE, CREATE + * INDEX CONCURRENTLY, REPACK CONCURRENTLY */ #define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */ #define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW * SHARE */ diff --git a/src/include/storage/locktag.h b/src/include/storage/locktag.h new file mode 100644 index 0000000000000..6b122497ec6de --- /dev/null +++ b/src/include/storage/locktag.h @@ -0,0 +1,190 @@ +/*------------------------------------------------------------------------- + * + * locktag.h + * LOCKTAG declarations, for lookups in the Postgres lock hashtable. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/locktag.h + * + *------------------------------------------------------------------------- + */ + +#ifndef _PG_LOCKTAG_H_ +#define _PG_LOCKTAG_H_ + +/* + * Lock methods are identified by LOCKMETHODID. (Despite the declaration as + * uint16, we are constrained to 256 lockmethods by the layout of LOCKTAG.) + */ +typedef uint16 LOCKMETHODID; + +/* These identify the known lock methods */ +#define DEFAULT_LOCKMETHOD 1 +#define USER_LOCKMETHOD 2 + +/* + * LOCKTAG is the key information needed to look up a LOCK item in the + * lock hashtable. A LOCKTAG value uniquely identifies a lockable object. + * + * The LockTagType enum defines the different kinds of objects we can lock. + * We can handle up to 256 different LockTagTypes. + */ +typedef enum LockTagType +{ + LOCKTAG_RELATION, /* whole relation */ + LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */ + LOCKTAG_DATABASE_FROZEN_IDS, /* pg_database.datfrozenxid */ + LOCKTAG_PAGE, /* one page of a relation */ + LOCKTAG_TUPLE, /* one physical tuple */ + LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */ + LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */ + LOCKTAG_SPECULATIVE_TOKEN, /* speculative insertion Xid and token */ + LOCKTAG_OBJECT, /* non-relation database object */ + LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */ + LOCKTAG_ADVISORY, /* advisory user locks */ + LOCKTAG_APPLY_TRANSACTION, /* transaction being applied on a logical + * replication subscriber */ +} LockTagType; + +#define LOCKTAG_LAST_TYPE LOCKTAG_APPLY_TRANSACTION + +extern PGDLLIMPORT const char *const LockTagTypeNames[]; + +/* + * The LOCKTAG struct is defined with malice aforethought to fit into 16 + * bytes with no padding. Note that this would need adjustment if we were + * to widen Oid, BlockNumber, or TransactionId to more than 32 bits. + * + * We include lockmethodid in the locktag so that a single hash table in + * shared memory can store locks of different lockmethods. + */ +typedef struct LOCKTAG +{ + uint32 locktag_field1; /* a 32-bit ID field */ + uint32 locktag_field2; /* a 32-bit ID field */ + uint32 locktag_field3; /* a 32-bit ID field */ + uint16 locktag_field4; /* a 16-bit ID field */ + uint8 locktag_type; /* see enum LockTagType */ + uint8 locktag_lockmethodid; /* lockmethod indicator */ +} LOCKTAG; + +/* + * These macros define how we map logical IDs of lockable objects into + * the physical fields of LOCKTAG. Use these to set up LOCKTAG values, + * rather than accessing the fields directly. Note multiple eval of target! + */ + +/* ID info for a relation is DB OID + REL OID; DB OID = 0 if shared */ +#define SET_LOCKTAG_RELATION(locktag,dboid,reloid) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = (reloid), \ + (locktag).locktag_field3 = 0, \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_RELATION, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* same ID info as RELATION */ +#define SET_LOCKTAG_RELATION_EXTEND(locktag,dboid,reloid) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = (reloid), \ + (locktag).locktag_field3 = 0, \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_RELATION_EXTEND, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* ID info for frozen IDs is DB OID */ +#define SET_LOCKTAG_DATABASE_FROZEN_IDS(locktag,dboid) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = 0, \ + (locktag).locktag_field3 = 0, \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_DATABASE_FROZEN_IDS, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* ID info for a page is RELATION info + BlockNumber */ +#define SET_LOCKTAG_PAGE(locktag,dboid,reloid,blocknum) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = (reloid), \ + (locktag).locktag_field3 = (blocknum), \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_PAGE, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* ID info for a tuple is PAGE info + OffsetNumber */ +#define SET_LOCKTAG_TUPLE(locktag,dboid,reloid,blocknum,offnum) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = (reloid), \ + (locktag).locktag_field3 = (blocknum), \ + (locktag).locktag_field4 = (offnum), \ + (locktag).locktag_type = LOCKTAG_TUPLE, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* ID info for a transaction is its TransactionId */ +#define SET_LOCKTAG_TRANSACTION(locktag,xid) \ + ((locktag).locktag_field1 = (xid), \ + (locktag).locktag_field2 = 0, \ + (locktag).locktag_field3 = 0, \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_TRANSACTION, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* ID info for a virtual transaction is its VirtualTransactionId */ +#define SET_LOCKTAG_VIRTUALTRANSACTION(locktag,vxid) \ + ((locktag).locktag_field1 = (vxid).procNumber, \ + (locktag).locktag_field2 = (vxid).localTransactionId, \ + (locktag).locktag_field3 = 0, \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_VIRTUALTRANSACTION, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* + * ID info for a speculative insert is TRANSACTION info + + * its speculative insert counter. + */ +#define SET_LOCKTAG_SPECULATIVE_INSERTION(locktag,xid,token) \ + ((locktag).locktag_field1 = (xid), \ + (locktag).locktag_field2 = (token), \ + (locktag).locktag_field3 = 0, \ + (locktag).locktag_field4 = 0, \ + (locktag).locktag_type = LOCKTAG_SPECULATIVE_TOKEN, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +/* + * ID info for an object is DB OID + CLASS OID + OBJECT OID + SUBID + * + * Note: object ID has same representation as in pg_depend and + * pg_description, but notice that we are constraining SUBID to 16 bits. + * Also, we use DB OID = 0 for shared objects such as tablespaces. + */ +#define SET_LOCKTAG_OBJECT(locktag,dboid,classoid,objoid,objsubid) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = (classoid), \ + (locktag).locktag_field3 = (objoid), \ + (locktag).locktag_field4 = (objsubid), \ + (locktag).locktag_type = LOCKTAG_OBJECT, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +#define SET_LOCKTAG_ADVISORY(locktag,id1,id2,id3,id4) \ + ((locktag).locktag_field1 = (id1), \ + (locktag).locktag_field2 = (id2), \ + (locktag).locktag_field3 = (id3), \ + (locktag).locktag_field4 = (id4), \ + (locktag).locktag_type = LOCKTAG_ADVISORY, \ + (locktag).locktag_lockmethodid = USER_LOCKMETHOD) + +/* + * ID info for a remote transaction on a logical replication subscriber is: DB + * OID + SUBSCRIPTION OID + TRANSACTION ID + OBJID + */ +#define SET_LOCKTAG_APPLY_TRANSACTION(locktag,dboid,suboid,xid,objid) \ + ((locktag).locktag_field1 = (dboid), \ + (locktag).locktag_field2 = (suboid), \ + (locktag).locktag_field3 = (xid), \ + (locktag).locktag_field4 = (objid), \ + (locktag).locktag_type = LOCKTAG_APPLY_TRANSACTION, \ + (locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD) + +#endif /* _PG_LOCKTAG_H_ */ diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h index 08a72569ae5fd..efa5b427e9f22 100644 --- a/src/include/storage/lwlock.h +++ b/src/include/storage/lwlock.h @@ -4,7 +4,7 @@ * Lightweight lock manager * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/lwlock.h @@ -73,16 +73,6 @@ typedef union LWLockPadded extern PGDLLIMPORT LWLockPadded *MainLWLockArray; -/* struct for storing named tranche information */ -typedef struct NamedLWLockTranche -{ - int trancheId; - char *trancheName; -} NamedLWLockTranche; - -extern PGDLLIMPORT NamedLWLockTranche *NamedLWLockTrancheArray; -extern PGDLLIMPORT int NamedLWLockTrancheRequests; - /* * It's a bit odd to declare NUM_BUFFER_PARTITIONS and NUM_LOCK_PARTITIONS * here, but we need them to figure out offsets within MainLWLockArray, and @@ -129,10 +119,6 @@ extern bool LWLockAcquireOrWait(LWLock *lock, LWLockMode mode); extern void LWLockRelease(LWLock *lock); extern void LWLockReleaseClearVar(LWLock *lock, pg_atomic_uint64 *valptr, uint64 val); extern void LWLockReleaseAll(void); -extern void LWLockDisown(LWLock *lock); -extern void LWLockReleaseDisowned(LWLock *lock, LWLockMode mode); -extern void ForEachLWLockHeldByMe(void (*callback) (LWLock *, LWLockMode, void *), - void *context); extern bool LWLockHeldByMe(LWLock *lock); extern bool LWLockAnyHeldByMe(LWLock *lock, int nlocks, size_t stride); extern bool LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode); @@ -140,8 +126,6 @@ extern bool LWLockHeldByMeInMode(LWLock *lock, LWLockMode mode); extern bool LWLockWaitForVar(LWLock *lock, pg_atomic_uint64 *valptr, uint64 oldval, uint64 *newval); extern void LWLockUpdateVar(LWLock *lock, pg_atomic_uint64 *valptr, uint64 val); -extern Size LWLockShmemSize(void); -extern void CreateLWLocks(void); extern void InitLWLockAccess(void); extern const char *GetLWLockIdentifier(uint32 classId, uint16 eventId); @@ -157,70 +141,34 @@ extern LWLockPadded *GetNamedLWLockTranche(const char *tranche_name); /* * There is another, more flexible method of obtaining lwlocks. First, call - * LWLockNewTrancheId just once to obtain a tranche ID; this allocates from - * a shared counter. Next, each individual process using the tranche should - * call LWLockRegisterTranche() to associate that tranche ID with a name. - * Finally, LWLockInitialize should be called just once per lwlock, passing - * the tranche ID as an argument. - * - * It may seem strange that each process using the tranche must register it - * separately, but dynamic shared memory segments aren't guaranteed to be - * mapped at the same address in all coordinating backends, so storing the - * registration in the main shared memory segment wouldn't work for that case. + * LWLockNewTrancheId to obtain a tranche ID; this allocates from a shared + * counter. Second, LWLockInitialize should be called just once per lwlock, + * passing the tranche ID as an argument. */ -extern int LWLockNewTrancheId(void); -extern void LWLockRegisterTranche(int tranche_id, const char *tranche_name); +extern int LWLockNewTrancheId(const char *name); extern void LWLockInitialize(LWLock *lock, int tranche_id); /* * Every tranche ID less than NUM_INDIVIDUAL_LWLOCKS is reserved; also, * we reserve additional tranche IDs for builtin tranches not included in * the set of individual LWLocks. A call to LWLockNewTrancheId will never - * return a value less than LWTRANCHE_FIRST_USER_DEFINED. + * return a value less than LWTRANCHE_FIRST_USER_DEFINED. The actual list of + * built-in tranches is kept in lwlocklist.h. */ typedef enum BuiltinTrancheIds { - LWTRANCHE_XACT_BUFFER = NUM_INDIVIDUAL_LWLOCKS, - LWTRANCHE_COMMITTS_BUFFER, - LWTRANCHE_SUBTRANS_BUFFER, - LWTRANCHE_MULTIXACTOFFSET_BUFFER, - LWTRANCHE_MULTIXACTMEMBER_BUFFER, - LWTRANCHE_NOTIFY_BUFFER, - LWTRANCHE_SERIAL_BUFFER, - LWTRANCHE_WAL_INSERT, - LWTRANCHE_BUFFER_CONTENT, - LWTRANCHE_REPLICATION_ORIGIN_STATE, - LWTRANCHE_REPLICATION_SLOT_IO, - LWTRANCHE_LOCK_FASTPATH, - LWTRANCHE_BUFFER_MAPPING, - LWTRANCHE_LOCK_MANAGER, - LWTRANCHE_PREDICATE_LOCK_MANAGER, - LWTRANCHE_PARALLEL_HASH_JOIN, - LWTRANCHE_PARALLEL_BTREE_SCAN, - LWTRANCHE_PARALLEL_QUERY_DSA, - LWTRANCHE_PER_SESSION_DSA, - LWTRANCHE_PER_SESSION_RECORD_TYPE, - LWTRANCHE_PER_SESSION_RECORD_TYPMOD, - LWTRANCHE_SHARED_TUPLESTORE, - LWTRANCHE_SHARED_TIDBITMAP, - LWTRANCHE_PARALLEL_APPEND, - LWTRANCHE_PER_XACT_PREDICATE_LIST, - LWTRANCHE_PGSTATS_DSA, - LWTRANCHE_PGSTATS_HASH, - LWTRANCHE_PGSTATS_DATA, - LWTRANCHE_LAUNCHER_DSA, - LWTRANCHE_LAUNCHER_HASH, - LWTRANCHE_DSM_REGISTRY_DSA, - LWTRANCHE_DSM_REGISTRY_HASH, - LWTRANCHE_COMMITTS_SLRU, - LWTRANCHE_MULTIXACTMEMBER_SLRU, - LWTRANCHE_MULTIXACTOFFSET_SLRU, - LWTRANCHE_NOTIFY_SLRU, - LWTRANCHE_SERIAL_SLRU, - LWTRANCHE_SUBTRANS_SLRU, - LWTRANCHE_XACT_SLRU, - LWTRANCHE_PARALLEL_VACUUM_DSA, - LWTRANCHE_AIO_URING_COMPLETION, + /* + * LWTRANCHE_INVALID is an unused value that only exists to initialize the + * rest of the tranches to appropriate values. + */ + LWTRANCHE_INVALID = NUM_INDIVIDUAL_LWLOCKS - 1, + +#define PG_LWLOCK(id, name) +#define PG_LWLOCKTRANCHE(id, name) LWTRANCHE_##id, +#include "storage/lwlocklist.h" +#undef PG_LWLOCK +#undef PG_LWLOCKTRANCHE + LWTRANCHE_FIRST_USER_DEFINED, } BuiltinTrancheIds; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index a9681738146e1..d7eb648bd2758 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -2,14 +2,15 @@ * * lwlocklist.h * - * The predefined LWLock list is kept in its own source file for use by - * automatic tools. The exact representation of a keyword is determined by - * the PG_LWLOCK macro, which is not defined in this file; it can be + * The list of predefined LWLocks and built-in LWLock tranches is kept in + * its own source file for use by automatic tools. The exact + * representation of a keyword is determined by the PG_LWLOCK and + * PG_LWLOCKTRANCHE macros, which are not defined in this file; they can be * defined by the caller for special purposes. * * Also, generate-lwlocknames.pl processes this file to create lwlocknames.h. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -31,13 +32,13 @@ */ /* 0 is available; was formerly BufFreelistLock */ -PG_LWLOCK(1, ShmemIndex) +/* 1 was ShmemIndex */ PG_LWLOCK(2, OidGen) PG_LWLOCK(3, XidGen) PG_LWLOCK(4, ProcArray) PG_LWLOCK(5, SInvalRead) PG_LWLOCK(6, SInvalWrite) -/* 7 was WALBufMapping */ +PG_LWLOCK(7, WALBufMapping) PG_LWLOCK(8, WALWrite) PG_LWLOCK(9, ControlFile) /* 10 was CheckpointLock */ @@ -84,3 +85,58 @@ PG_LWLOCK(50, DSMRegistry) PG_LWLOCK(51, InjectionPoint) PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) +PG_LWLOCK(54, WaitLSN) +PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, DataChecksumsWorker) +PG_LWLOCK(57, AioWorkerControl) + +/* + * There also exist several built-in LWLock tranches. As with the predefined + * LWLocks, be sure to update the WaitEventLWLock section of + * src/backend/utils/activity/wait_event_names.txt when modifying this list. + * + * Note that the IDs here (the first value) don't include the LWTRANCHE_ + * prefix. It's added elsewhere. + */ +PG_LWLOCKTRANCHE(XACT_BUFFER, XactBuffer) +PG_LWLOCKTRANCHE(COMMITTS_BUFFER, CommitTsBuffer) +PG_LWLOCKTRANCHE(SUBTRANS_BUFFER, SubtransBuffer) +PG_LWLOCKTRANCHE(MULTIXACTOFFSET_BUFFER, MultiXactOffsetBuffer) +PG_LWLOCKTRANCHE(MULTIXACTMEMBER_BUFFER, MultiXactMemberBuffer) +PG_LWLOCKTRANCHE(NOTIFY_BUFFER, NotifyBuffer) +PG_LWLOCKTRANCHE(NOTIFY_CHANNEL_HASH, NotifyChannelHash) +PG_LWLOCKTRANCHE(SERIAL_BUFFER, SerialBuffer) +PG_LWLOCKTRANCHE(WAL_INSERT, WALInsert) +PG_LWLOCKTRANCHE(REPLICATION_ORIGIN_STATE, ReplicationOriginState) +PG_LWLOCKTRANCHE(REPLICATION_SLOT_IO, ReplicationSlotIO) +PG_LWLOCKTRANCHE(LOCK_FASTPATH, LockFastPath) +PG_LWLOCKTRANCHE(BUFFER_MAPPING, BufferMapping) +PG_LWLOCKTRANCHE(LOCK_MANAGER, LockManager) +PG_LWLOCKTRANCHE(PREDICATE_LOCK_MANAGER, PredicateLockManager) +PG_LWLOCKTRANCHE(PARALLEL_HASH_JOIN, ParallelHashJoin) +PG_LWLOCKTRANCHE(PARALLEL_BTREE_SCAN, ParallelBtreeScan) +PG_LWLOCKTRANCHE(PARALLEL_QUERY_DSA, ParallelQueryDSA) +PG_LWLOCKTRANCHE(PER_SESSION_DSA, PerSessionDSA) +PG_LWLOCKTRANCHE(PER_SESSION_RECORD_TYPE, PerSessionRecordType) +PG_LWLOCKTRANCHE(PER_SESSION_RECORD_TYPMOD, PerSessionRecordTypmod) +PG_LWLOCKTRANCHE(SHARED_TUPLESTORE, SharedTupleStore) +PG_LWLOCKTRANCHE(SHARED_TIDBITMAP, SharedTidBitmap) +PG_LWLOCKTRANCHE(PARALLEL_APPEND, ParallelAppend) +PG_LWLOCKTRANCHE(PER_XACT_PREDICATE_LIST, PerXactPredicateList) +PG_LWLOCKTRANCHE(PGSTATS_DSA, PgStatsDSA) +PG_LWLOCKTRANCHE(PGSTATS_HASH, PgStatsHash) +PG_LWLOCKTRANCHE(PGSTATS_DATA, PgStatsData) +PG_LWLOCKTRANCHE(LAUNCHER_DSA, LogicalRepLauncherDSA) +PG_LWLOCKTRANCHE(LAUNCHER_HASH, LogicalRepLauncherHash) +PG_LWLOCKTRANCHE(DSM_REGISTRY_DSA, DSMRegistryDSA) +PG_LWLOCKTRANCHE(DSM_REGISTRY_HASH, DSMRegistryHash) +PG_LWLOCKTRANCHE(COMMITTS_SLRU, CommitTsSLRU) +PG_LWLOCKTRANCHE(MULTIXACTOFFSET_SLRU, MultiXactOffsetSLRU) +PG_LWLOCKTRANCHE(MULTIXACTMEMBER_SLRU, MultiXactMemberSLRU) +PG_LWLOCKTRANCHE(NOTIFY_SLRU, NotifySLRU) +PG_LWLOCKTRANCHE(SERIAL_SLRU, SerialSLRU) +PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) +PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) +PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) +PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(SHMEM_INDEX, ShmemIndex) diff --git a/src/include/storage/md.h b/src/include/storage/md.h index b563c27abf092..b8d10329eb874 100644 --- a/src/include/storage/md.h +++ b/src/include/storage/md.h @@ -4,7 +4,7 @@ * magnetic disk storage manager public interface declarations. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/md.h diff --git a/src/include/storage/meson.build b/src/include/storage/meson.build index 1e0f5080727ed..efd2458f69975 100644 --- a/src/include/storage/meson.build +++ b/src/include/storage/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group lwlocknames_h = custom_target('lwlocknames_h', input: files( diff --git a/src/include/storage/off.h b/src/include/storage/off.h index b0bcd371b0464..79528c08d8fcf 100644 --- a/src/include/storage/off.h +++ b/src/include/storage/off.h @@ -4,7 +4,7 @@ * POSTGRES disk "offset" definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/off.h diff --git a/src/include/storage/pg_sema.h b/src/include/storage/pg_sema.h index fa6ca35a51f5c..fe50ee505ba41 100644 --- a/src/include/storage/pg_sema.h +++ b/src/include/storage/pg_sema.h @@ -10,7 +10,7 @@ * be provided by each port. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/pg_sema.h @@ -37,11 +37,11 @@ typedef HANDLE PGSemaphore; #endif -/* Report amount of shared memory needed */ -extern Size PGSemaphoreShmemSize(int maxSemas); +/* Request shared memory needed for semaphores */ +extern void PGSemaphoreShmemRequest(int maxSemas); /* Module initialization (called during postmaster start or shmem reinit) */ -extern void PGReserveSemaphores(int maxSemas); +extern void PGSemaphoreInit(int maxSemas); /* Allocate a PGSemaphore structure with initial count 1 */ extern PGSemaphore PGSemaphoreCreate(void); diff --git a/src/include/storage/pg_shmem.h b/src/include/storage/pg_shmem.h index 5f7d4b83a60eb..10c7b06586120 100644 --- a/src/include/storage/pg_shmem.h +++ b/src/include/storage/pg_shmem.h @@ -14,7 +14,7 @@ * only one ID number. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/pg_shmem.h @@ -32,9 +32,9 @@ typedef struct PGShmemHeader /* standard header for all Postgres shmem */ #define PGShmemMagic 679834894 pid_t creatorPID; /* PID of creating process (set but unread) */ Size totalsize; /* total size of segment */ - Size freeoffset; /* offset to first free space */ + Size content_offset; /* offset to the data, i.e. size of this + * header */ dsm_handle dsm_control; /* ID of dynamic shared memory control seg */ - void *index; /* pointer to ShmemIndex table */ #ifndef WIN32 /* Windows doesn't have useful inode#s */ dev_t device; /* device data directory is on */ ino_t inode; /* inode number of data directory */ diff --git a/src/include/storage/pmsignal.h b/src/include/storage/pmsignal.h index 428aa3fd68a0a..bcce401179071 100644 --- a/src/include/storage/pmsignal.h +++ b/src/include/storage/pmsignal.h @@ -4,7 +4,7 @@ * routines for signaling between the postmaster and its child processes * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/pmsignal.h @@ -38,6 +38,7 @@ typedef enum PMSIGNAL_ROTATE_LOGFILE, /* send SIGUSR1 to syslogger to rotate logfile */ PMSIGNAL_START_AUTOVAC_LAUNCHER, /* start an autovacuum launcher */ PMSIGNAL_START_AUTOVAC_WORKER, /* start an autovacuum worker */ + PMSIGNAL_IO_WORKER_GROW, /* I/O worker pool wants to grow */ PMSIGNAL_BACKGROUND_WORKER_CHANGE, /* background worker state change */ PMSIGNAL_START_WALRECEIVER, /* start a walreceiver */ PMSIGNAL_ADVANCE_STATE_MACHINE, /* advance postmaster's state machine */ @@ -66,8 +67,6 @@ extern PGDLLIMPORT volatile PMSignalData *PMSignalState; /* * prototypes for functions in pmsignal.c */ -extern Size PMSignalShmemSize(void); -extern void PMSignalShmemInit(void); extern void SendPostmasterSignal(PMSignalReason reason); extern bool CheckPostmasterSignal(PMSignalReason reason); extern void SetQuitSignalReason(QuitSignalReason reason); diff --git a/src/include/storage/predicate.h b/src/include/storage/predicate.h index 267d5d90e9486..443bffb58fdb9 100644 --- a/src/include/storage/predicate.h +++ b/src/include/storage/predicate.h @@ -4,7 +4,7 @@ * POSTGRES public predicate locking definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/predicate.h @@ -14,11 +14,16 @@ #ifndef PREDICATE_H #define PREDICATE_H +#include "access/transam.h" #include "storage/itemptr.h" -#include "storage/lock.h" #include "utils/relcache.h" #include "utils/snapshot.h" +/* + * forward references in this file + */ +typedef struct VirtualTransactionId VirtualTransactionId; + /* * GUC variables @@ -36,11 +41,6 @@ typedef void *SerializableXactHandle; /* * function prototypes */ - -/* housekeeping for shared memory predicate lock structures */ -extern void PredicateLockShmemInit(void); -extern Size PredicateLockShmemSize(void); - extern void CheckPointPredicate(void); /* predicate lock reporting */ @@ -54,7 +54,7 @@ extern void SetSerializableTransactionSnapshot(Snapshot snapshot, extern void RegisterPredicateLockingXid(TransactionId xid); extern void PredicateLockRelation(Relation relation, Snapshot snapshot); extern void PredicateLockPage(Relation relation, BlockNumber blkno, Snapshot snapshot); -extern void PredicateLockTID(Relation relation, ItemPointer tid, Snapshot snapshot, +extern void PredicateLockTID(Relation relation, const ItemPointerData *tid, Snapshot snapshot, TransactionId tuple_xid); extern void PredicateLockPageSplit(Relation relation, BlockNumber oldblkno, BlockNumber newblkno); extern void PredicateLockPageCombine(Relation relation, BlockNumber oldblkno, BlockNumber newblkno); @@ -64,7 +64,7 @@ extern void ReleasePredicateLocks(bool isCommit, bool isReadOnlySafe); /* conflict detection (may also trigger rollback) */ extern bool CheckForSerializableConflictOutNeeded(Relation relation, Snapshot snapshot); extern void CheckForSerializableConflictOut(Relation relation, TransactionId xid, Snapshot snapshot); -extern void CheckForSerializableConflictIn(Relation relation, ItemPointer tid, BlockNumber blkno); +extern void CheckForSerializableConflictIn(Relation relation, const ItemPointerData *tid, BlockNumber blkno); extern void CheckTableForSerializableConflictIn(Relation relation); /* final rollback checking */ @@ -72,9 +72,9 @@ extern void PreCommit_CheckForSerializationFailure(void); /* two-phase commit support */ extern void AtPrepare_PredicateLocks(void); -extern void PostPrepare_PredicateLocks(TransactionId xid); -extern void PredicateLockTwoPhaseFinish(TransactionId xid, bool isCommit); -extern void predicatelock_twophase_recover(TransactionId xid, uint16 info, +extern void PostPrepare_PredicateLocks(FullTransactionId fxid); +extern void PredicateLockTwoPhaseFinish(FullTransactionId fxid, bool isCommit); +extern void predicatelock_twophase_recover(FullTransactionId fxid, uint16 info, void *recdata, uint32 len); /* parallel query support */ diff --git a/src/include/storage/predicate_internals.h b/src/include/storage/predicate_internals.h index 5c3ea37f25097..2026273d14970 100644 --- a/src/include/storage/predicate_internals.h +++ b/src/include/storage/predicate_internals.h @@ -4,7 +4,7 @@ * POSTGRES internal predicate locking definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/predicate_internals.h diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 9f9b3fcfbf1d7..3e1d1fad5f9a4 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -4,7 +4,7 @@ * per-process shared memory data structures * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/proc.h @@ -14,14 +14,18 @@ #ifndef _PROC_H_ #define _PROC_H_ -#include "access/clog.h" #include "access/xlogdefs.h" #include "lib/ilist.h" +#include "miscadmin.h" #include "storage/latch.h" #include "storage/lock.h" #include "storage/pg_sema.h" #include "storage/proclist_types.h" #include "storage/procnumber.h" +#include "storage/spin.h" + +/* Avoid including clog.h here */ +typedef int XidStatus; /* * Each backend advertises up to PGPROC_MAX_CACHED_SUBXIDS TransactionIds @@ -130,9 +134,17 @@ extern PGDLLIMPORT int FastPathLockGroupsPerBackend; * the checkpoint are actually destroyed on disk. Replay can cope with a file * or block that doesn't exist, but not with a block that has the wrong * contents. + * + * Setting DELAY_CHKPT_IN_COMMIT is similar to setting DELAY_CHKPT_START, but + * it explicitly indicates that the reason for delaying the checkpoint is due + * to a transaction being within a critical commit section. We need this new + * flag to ensure all the transactions that have acquired commit timestamp are + * finished before we allow the logical replication client to advance its xid + * which is used to hold back dead rows for conflict detection. */ #define DELAY_CHKPT_START (1<<0) #define DELAY_CHKPT_COMPLETE (1<<1) +#define DELAY_CHKPT_IN_COMMIT (DELAY_CHKPT_START | 1<<2) typedef enum { @@ -145,10 +157,6 @@ typedef enum * Each backend has a PGPROC struct in shared memory. There is also a list of * currently-unused PGPROC structs that will be reallocated to new backends. * - * links: list link for any list the PGPROC is in. When waiting for a lock, - * the PGPROC is linked into that lock's waitProcs queue. A recycled PGPROC - * is linked into ProcGlobal's freeProcs list. - * * Note: twophase.c also sets up a dummy PGPROC struct for each currently * prepared transaction. These PGPROCs appear in the ProcArray data structure * so that the prepared transactions appear to be still running and are @@ -158,7 +166,7 @@ typedef enum * but its myProcLocks[] lists are valid. * * We allow many fields of this struct to be accessed without locks, such as - * delayChkptFlags and isRegularBackend. However, keep in mind that writing + * delayChkptFlags and backendType. However, keep in mind that writing * mirrored ones (see below) requires holding ProcArrayLock or XidGenLock in * at least shared mode, so that pgxactoff does not change concurrently. * @@ -173,32 +181,46 @@ typedef enum * * See PROC_HDR for details. */ -struct PGPROC +typedef struct PGPROC { - dlist_node links; /* list link if process is in a list */ + /* + * Align the struct at cache line boundaries. This is just for + * performance, to avoid false sharing. + */ + alignas(PG_CACHE_LINE_SIZE) dlist_head *procgloballist; /* procglobal list that owns this PGPROC */ + dlist_node freeProcsLink; /* link in procgloballist, when in recycled + * state */ - PGSemaphore sem; /* ONE semaphore to sleep on */ - ProcWaitStatus waitStatus; - - Latch procLatch; /* generic latch for process */ - + /************************************************************************ + * Backend identity + ************************************************************************/ - TransactionId xid; /* id of top-level transaction currently being - * executed by this proc, if running and XID - * is assigned; else InvalidTransactionId. - * mirrored in ProcGlobal->xids[pgxactoff] */ + /* + * These fields that don't change after backend startup, or only very + * rarely + */ + int pid; /* Backend's process ID; 0 if prepared xact */ + BackendType backendType; /* what kind of process is this? */ - TransactionId xmin; /* minimal running XID as it was when we were - * starting our xact, excluding LAZY VACUUM: - * vacuum must not remove tuples deleted by - * xid >= xmin ! */ + /* These fields are zero while a backend is still starting up: */ + Oid databaseId; /* OID of database this backend is using */ + Oid roleId; /* OID of role using this backend */ - int pid; /* Backend's process ID; 0 if prepared xact */ + Oid tempNamespaceId; /* OID of temp schema this backend is + * using */ int pgxactoff; /* offset into various ProcGlobal->arrays with * data mirrored from this PGPROC */ + uint8 statusFlags; /* this backend's status flags, see PROC_* + * above. mirrored in + * ProcGlobal->statusFlags[pgxactoff] */ + + /************************************************************************ + * Transactions and snapshots + ************************************************************************/ + /* * Currently running top-level transaction's virtual xid. Together these * form a VirtualTransactionId, but we don't use that struct because this @@ -218,23 +240,52 @@ struct PGPROC * InvalidLocalTransactionId */ } vxid; - /* These fields are zero while a backend is still starting up: */ - Oid databaseId; /* OID of database this backend is using */ - Oid roleId; /* OID of role using this backend */ + TransactionId xid; /* id of top-level transaction currently being + * executed by this proc, if running and XID + * is assigned; else InvalidTransactionId. + * mirrored in ProcGlobal->xids[pgxactoff] */ + + TransactionId xmin; /* minimal running XID as it was when we were + * starting our xact, excluding LAZY VACUUM: + * vacuum must not remove tuples deleted by + * xid >= xmin ! */ + + XidCacheStatus subxidStatus; /* mirrored with + * ProcGlobal->subxidStates[i] */ + struct XidCache subxids; /* cache for subtransaction XIDs */ - Oid tempNamespaceId; /* OID of temp schema this backend is - * using */ - bool isRegularBackend; /* true if it's a regular backend. */ + /************************************************************************ + * Inter-process signaling + ************************************************************************/ + + Latch procLatch; /* generic latch for process */ + + PGSemaphore sem; /* ONE semaphore to sleep on */ + + int delayChkptFlags; /* for DELAY_CHKPT_* flags */ /* * While in hot standby mode, shows that a conflict signal has been sent * for the current transaction. Set/cleared while holding ProcArrayLock, * though not required. Accessed without lock, if needed. + * + * This is a bitmask; each bit corresponds to a RecoveryConflictReason + * enum value. */ - bool recoveryConflictPending; + pg_atomic_uint32 pendingRecoveryConflicts; - /* Info about LWLock the process is currently waiting for, if any. */ + /************************************************************************ + * LWLock waiting + ************************************************************************/ + + /* + * Info about LWLock the process is currently waiting for, if any. + * + * This is currently used both for lwlocks and buffer content locks, which + * is acceptable, although not pretty, because a backend can't wait for + * both types of locks at the same time. + */ uint8 lwWaiting; /* see LWLockWaitState */ uint8 lwWaitMode; /* lwlock mode being waited for */ proclist_node lwWaitLink; /* position in LW lock wait list */ @@ -242,21 +293,50 @@ struct PGPROC /* Support for condition variables. */ proclist_node cvWaitLink; /* position in CV wait list */ + /************************************************************************ + * Lock manager data + ************************************************************************/ + + /* + * Support for lock groups. Use LockHashPartitionLockByProc on the group + * leader to get the LWLock protecting these fields. + */ + PGPROC *lockGroupLeader; /* lock group leader, if I'm a member */ + dlist_head lockGroupMembers; /* list of members, if I'm a leader */ + dlist_node lockGroupLink; /* my member link, if I'm a member */ + /* Info about lock the process is currently waiting for, if any. */ /* waitLock and waitProcLock are NULL if not currently waiting. */ LOCK *waitLock; /* Lock object we're sleeping on ... */ + dlist_node waitLink; /* position in waitLock->waitProcs queue */ PROCLOCK *waitProcLock; /* Per-holder info for awaited lock */ LOCKMODE waitLockMode; /* type of lock we're waiting for */ LOCKMASK heldLocks; /* bitmask for lock types already held on this * lock object by this backend */ + pg_atomic_uint64 waitStart; /* time at which wait for lock acquisition * started */ - int delayChkptFlags; /* for DELAY_CHKPT_* flags */ + ProcWaitStatus waitStatus; - uint8 statusFlags; /* this backend's status flags, see PROC_* - * above. mirrored in - * ProcGlobal->statusFlags[pgxactoff] */ + /* + * All PROCLOCK objects for locks held or awaited by this backend are + * linked into one of these lists, according to the partition number of + * their lock. + */ + dlist_head myProcLocks[NUM_LOCK_PARTITIONS]; + + /*-- recording fast-path locks taken by this backend. --*/ + LWLock fpInfoLock; /* protects per-backend fast-path state */ + uint64 *fpLockBits; /* lock modes held for each fast-path slot */ + Oid *fpRelId; /* slots for rel oids */ + bool fpVXIDLock; /* are we holding a fast-path VXID lock? */ + LocalTransactionId fpLocalTransactionId; /* lxid for fast-path VXID + * lock */ + + /************************************************************************ + * Synchronous replication waiting + ************************************************************************/ /* * Info to allow us to wait for synchronous replication, if needed. @@ -268,18 +348,10 @@ struct PGPROC int syncRepState; /* wait state for sync rep */ dlist_node syncRepLinks; /* list link if process is in syncrep queue */ - /* - * All PROCLOCK objects for locks held or awaited by this backend are - * linked into one of these lists, according to the partition number of - * their lock. - */ - dlist_head myProcLocks[NUM_LOCK_PARTITIONS]; - - XidCacheStatus subxidStatus; /* mirrored with - * ProcGlobal->subxidStates[i] */ - struct XidCache subxids; /* cache for subtransaction XIDs */ + /************************************************************************ + * Support for group XID clearing + ************************************************************************/ - /* Support for group XID clearing. */ /* true, if member of ProcArray group waiting for XID clear */ bool procArrayGroupMember; /* next ProcArray group member waiting for XID clear */ @@ -291,9 +363,10 @@ struct PGPROC */ TransactionId procArrayGroupMemberXid; - uint32 wait_event_info; /* proc's wait information */ + /************************************************************************ + * Support for group transaction status update + ************************************************************************/ - /* Support for group transaction status update. */ bool clogGroupMember; /* true, if member of clog group */ pg_atomic_uint32 clogGroupNext; /* next clog group member */ TransactionId clogGroupMemberXid; /* transaction id of clog group member */ @@ -304,25 +377,13 @@ struct PGPROC XLogRecPtr clogGroupMemberLsn; /* WAL location of commit record for clog * group member */ - /* Lock manager data, recording fast-path locks taken by this backend. */ - LWLock fpInfoLock; /* protects per-backend fast-path state */ - uint64 *fpLockBits; /* lock modes held for each fast-path slot */ - Oid *fpRelId; /* slots for rel oids */ - bool fpVXIDLock; /* are we holding a fast-path VXID lock? */ - LocalTransactionId fpLocalTransactionId; /* lxid for fast-path VXID - * lock */ - - /* - * Support for lock groups. Use LockHashPartitionLockByProc on the group - * leader to get the LWLock protecting these fields. - */ - PGPROC *lockGroupLeader; /* lock group leader, if I'm a member */ - dlist_head lockGroupMembers; /* list of members, if I'm a leader */ - dlist_node lockGroupLink; /* my member link, if I'm a member */ -}; - -/* NOTE: "typedef struct PGPROC PGPROC" appears in storage/lock.h. */ + /************************************************************************ + * Status reporting + ************************************************************************/ + uint32 wait_event_info; /* proc's wait information */ +} +PGPROC; extern PGDLLIMPORT PGPROC *MyProc; @@ -402,6 +463,16 @@ typedef struct PROC_HDR /* Length of allProcs array */ uint32 allProcCount; + + /* + * This spinlock protects the below freelists of PGPROC structures. We + * cannot use an LWLock because the LWLock manager depends on already + * having a PGPROC and a wait semaphore! But these structures are touched + * relatively infrequently (only at backend startup or shutdown) and not + * for very long, so a spinlock is okay. + */ + slock_t freeProcsLock; + /* Head of list of free PGPROC structures */ dlist_head freeProcs; /* Head of list of autovacuum & special worker free PGPROC structures */ @@ -410,6 +481,7 @@ typedef struct PROC_HDR dlist_head bgworkerFreeProcs; /* Head of list of walsender free PGPROC structures */ dlist_head walsenderFreeProcs; + /* First pgproc waiting for group XID clear */ pg_atomic_uint32 procArrayGroupFirst; /* First pgproc waiting for group transaction status update */ @@ -460,6 +532,7 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs; #define MAX_IO_WORKERS 32 #define NUM_AUXILIARY_PROCS (6 + MAX_IO_WORKERS) +#define FIRST_PREPARED_XACT_PROC_NUMBER (MaxBackends + NUM_AUXILIARY_PROCS) /* configurable options */ extern PGDLLIMPORT int DeadlockTimeout; @@ -471,7 +544,6 @@ extern PGDLLIMPORT int IdleSessionTimeout; extern PGDLLIMPORT bool log_lock_waits; #ifdef EXEC_BACKEND -extern PGDLLIMPORT slock_t *ProcStructLock; extern PGDLLIMPORT PGPROC *AuxiliaryProcs; #endif @@ -480,8 +552,6 @@ extern PGDLLIMPORT PGPROC *AuxiliaryProcs; * Function Prototypes */ extern int ProcGlobalSemas(void); -extern Size ProcGlobalShmemSize(void); -extern void InitProcGlobal(void); extern void InitProcess(void); extern void InitProcessPhase2(void); extern void InitAuxiliaryProcess(void); diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h index ef0b733ebe8be..ec89c4482204d 100644 --- a/src/include/storage/procarray.h +++ b/src/include/storage/procarray.h @@ -4,7 +4,7 @@ * POSTGRES process array definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/procarray.h @@ -14,14 +14,11 @@ #ifndef PROCARRAY_H #define PROCARRAY_H -#include "storage/lock.h" #include "storage/standby.h" #include "utils/relcache.h" #include "utils/snapshot.h" -extern Size ProcArrayShmemSize(void); -extern void ProcArrayShmemInit(void); extern void ProcArrayAdd(PGPROC *proc); extern void ProcArrayRemove(PGPROC *proc, TransactionId latestXid); @@ -50,13 +47,13 @@ extern bool ProcArrayInstallImportedXmin(TransactionId xmin, VirtualTransactionId *sourcevxid); extern bool ProcArrayInstallRestoredXmin(TransactionId xmin, PGPROC *proc); -extern RunningTransactions GetRunningTransactionData(void); +extern RunningTransactions GetRunningTransactionData(Oid dbid); extern bool TransactionIdIsInProgress(TransactionId xid); -extern bool TransactionIdIsActive(TransactionId xid); extern TransactionId GetOldestNonRemovableTransactionId(Relation rel); extern TransactionId GetOldestTransactionIdConsideredRunning(void); -extern TransactionId GetOldestActiveTransactionId(void); +extern TransactionId GetOldestActiveTransactionId(bool inCommitOnly, + bool allDbs); extern TransactionId GetOldestSafeDecodingTransactionId(bool catalogOnly); extern void GetReplicationHorizons(TransactionId *xmin, TransactionId *catalog_xmin); @@ -77,14 +74,15 @@ extern VirtualTransactionId *GetCurrentVirtualXIDs(TransactionId limitXmin, bool excludeXmin0, bool allDbs, int excludeVacuum, int *nvxids); extern VirtualTransactionId *GetConflictingVirtualXIDs(TransactionId limitXmin, Oid dbOid); -extern pid_t CancelVirtualTransaction(VirtualTransactionId vxid, ProcSignalReason sigmode); -extern pid_t SignalVirtualTransaction(VirtualTransactionId vxid, ProcSignalReason sigmode, - bool conflictPending); + +extern bool SignalRecoveryConflict(PGPROC *proc, pid_t pid, RecoveryConflictReason reason); +extern bool SignalRecoveryConflictWithVirtualXID(VirtualTransactionId vxid, RecoveryConflictReason reason); +extern void SignalRecoveryConflictWithDatabase(Oid databaseid, RecoveryConflictReason reason); + extern bool MinimumActiveBackends(int min); extern int CountDBBackends(Oid databaseid); extern int CountDBConnections(Oid databaseid); -extern void CancelDBBackends(Oid databaseid, ProcSignalReason sigmode, bool conflictPending); extern int CountUserBackends(Oid roleid); extern bool CountOtherDBBackends(Oid databaseId, int *nbackends, int *nprepared); diff --git a/src/include/storage/proclist.h b/src/include/storage/proclist.h index a157f8df67f35..9caf109a8450c 100644 --- a/src/include/storage/proclist.h +++ b/src/include/storage/proclist.h @@ -10,7 +10,7 @@ * See proclist_types.h for the structs that these functions operate on. They * are separated to break a header dependency cycle with proc.h. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/storage/proclist.h @@ -204,8 +204,8 @@ proclist_pop_head_node_offset(proclist_head *list, size_t node_offset) * node with proclist_delete(list, iter.cur, node_offset). */ #define proclist_foreach_modify(iter, lhead, link_member) \ - for (AssertVariableIsOfTypeMacro(iter, proclist_mutable_iter), \ - AssertVariableIsOfTypeMacro(lhead, proclist_head *), \ + for (StaticAssertVariableIsOfTypeMacro(iter, proclist_mutable_iter), \ + StaticAssertVariableIsOfTypeMacro(lhead, proclist_head *), \ (iter).cur = (lhead)->head, \ (iter).next = (iter).cur == INVALID_PROC_NUMBER ? INVALID_PROC_NUMBER : \ proclist_node_get((iter).cur, \ diff --git a/src/include/storage/proclist_types.h b/src/include/storage/proclist_types.h index f314a6c3cfa0a..4b7cade6d16fe 100644 --- a/src/include/storage/proclist_types.h +++ b/src/include/storage/proclist_types.h @@ -5,7 +5,7 @@ * * See proclist.h for functions that operate on these types. * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/storage/proclist_types.h diff --git a/src/include/storage/procnumber.h b/src/include/storage/procnumber.h index 2ddaaf0c64600..bd9cb3891ccac 100644 --- a/src/include/storage/procnumber.h +++ b/src/include/storage/procnumber.h @@ -4,7 +4,7 @@ * definition of process number * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/procnumber.h @@ -27,13 +27,13 @@ typedef int ProcNumber; /* * Note: MAX_BACKENDS_BITS is 18 as that is the space available for buffer - * refcounts in buf_internals.h. This limitation could be lifted by using a - * 64bit state; but it's unlikely to be worthwhile as 2^18-1 backends exceed - * currently realistic configurations. Even if that limitation were removed, - * we still could not a) exceed 2^23-1 because inval.c stores the ProcNumber - * as a 3-byte signed integer, b) INT_MAX/4 because some places compute - * 4*MaxBackends without any overflow check. We check that the configured - * number of backends does not exceed MAX_BACKENDS in InitializeMaxBackends(). + * refcounts in buf_internals.h. This limitation could be lifted, but it's + * unlikely to be worthwhile as 2^18-1 backends exceed currently realistic + * configurations. Even if that limitation were removed, we still could not a) + * exceed 2^23-1 because inval.c stores the ProcNumber as a 3-byte signed + * integer, b) INT_MAX/4 because some places compute 4*MaxBackends without any + * overflow check. We check that the configured number of backends does not + * exceed MAX_BACKENDS in InitializeMaxBackends(). */ #define MAX_BACKENDS_BITS 18 #define MAX_BACKENDS ((1U << MAX_BACKENDS_BITS)-1) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019f8..aaa158bfd6618 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -4,7 +4,7 @@ * Routines for interprocess signaling * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/procsignal.h @@ -36,24 +36,24 @@ typedef enum PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ - - /* Recovery conflict reasons */ - PROCSIG_RECOVERY_CONFLICT_FIRST, - PROCSIG_RECOVERY_CONFLICT_DATABASE = PROCSIG_RECOVERY_CONFLICT_FIRST, - PROCSIG_RECOVERY_CONFLICT_TABLESPACE, - PROCSIG_RECOVERY_CONFLICT_LOCK, - PROCSIG_RECOVERY_CONFLICT_SNAPSHOT, - PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT, - PROCSIG_RECOVERY_CONFLICT_BUFFERPIN, - PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK, - PROCSIG_RECOVERY_CONFLICT_LAST = PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK, + PROCSIG_SLOTSYNC_MESSAGE, /* ask slot synchronization to stop */ + PROCSIG_REPACK_MESSAGE, /* Message from repack worker */ + PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check + * PGPROC->pendingRecoveryConflicts for the + * reason */ } ProcSignalReason; -#define NUM_PROCSIGNALS (PROCSIG_RECOVERY_CONFLICT_LAST + 1) +#define NUM_PROCSIGNALS (PROCSIG_RECOVERY_CONFLICT + 1) typedef enum { PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */ + PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO, /* ask to update + * XLogLogicalInfo */ + PROCSIGNAL_BARRIER_CHECKSUM_OFF, + PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON, + PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF, + PROCSIGNAL_BARRIER_CHECKSUM_ON, } ProcSignalBarrierType; /* @@ -69,9 +69,6 @@ typedef enum /* * prototypes for functions in procsignal.c */ -extern Size ProcSignalShmemSize(void); -extern void ProcSignalShmemInit(void); - extern void ProcSignalInit(const uint8 *cancel_key, int cancel_key_len); extern int SendProcSignal(pid_t pid, ProcSignalReason reason, ProcNumber procNumber); diff --git a/src/include/storage/read_stream.h b/src/include/storage/read_stream.h index 9b0d65161d02c..48995c6d534bd 100644 --- a/src/include/storage/read_stream.h +++ b/src/include/storage/read_stream.h @@ -4,7 +4,7 @@ * Mechanism for accessing buffered relation data with look-ahead * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/read_stream.h @@ -65,6 +65,7 @@ struct ReadStream; typedef struct ReadStream ReadStream; +struct IOStats; /* for block_range_read_stream_cb */ typedef struct BlockRangeReadStreamPrivate @@ -99,7 +100,10 @@ extern ReadStream *read_stream_begin_smgr_relation(int flags, ReadStreamBlockNumberCB callback, void *callback_private_data, size_t per_buffer_data_size); +extern BlockNumber read_stream_pause(ReadStream *stream); +extern void read_stream_resume(ReadStream *stream); extern void read_stream_reset(ReadStream *stream); extern void read_stream_end(ReadStream *stream); +extern void read_stream_enable_stats(ReadStream *stream, struct IOStats *stats); #endif /* READ_STREAM_H */ diff --git a/src/include/storage/reinit.h b/src/include/storage/reinit.h index 37fcbb1345f66..a053742f69587 100644 --- a/src/include/storage/reinit.h +++ b/src/include/storage/reinit.h @@ -4,7 +4,7 @@ * Reinitialization of unlogged relations * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/reinit.h diff --git a/src/include/storage/relfilelocator.h b/src/include/storage/relfilelocator.h index 0f841e3804d4e..317fca069238d 100644 --- a/src/include/storage/relfilelocator.h +++ b/src/include/storage/relfilelocator.h @@ -4,7 +4,7 @@ * Physical access information for relations. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/relfilelocator.h diff --git a/src/include/storage/s_lock.h b/src/include/storage/s_lock.h index 2f73f9fcf57a2..c9e5251199005 100644 --- a/src/include/storage/s_lock.h +++ b/src/include/storage/s_lock.h @@ -21,10 +21,6 @@ * void S_UNLOCK(slock_t *lock) * Unlock a previously acquired lock. * - * bool S_LOCK_FREE(slock_t *lock) - * Tests if the lock is free. Returns true if free, false if locked. - * This does *not* change the state of the lock. - * * void SPIN_DELAY(void) * Delay operation to occur inside spinlock wait loop. * @@ -79,7 +75,7 @@ * instruction. Equivalent OS-supplied mutex routines could be used too. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/s_lock.h @@ -119,6 +115,10 @@ * gcc from thinking it can cache the values of shared-memory fields * across the asm code. Add "cc" if your asm code changes the condition * code register, and also list any temp registers the code uses. + * + * If you need branch target labels within the asm block, include "%=" + * in the label names to make them distinct across multiple asm blocks + * within a source file. *---------- */ @@ -130,7 +130,7 @@ typedef unsigned char slock_t; #define TAS(lock) tas(lock) -static __inline__ int +static inline int tas(volatile slock_t *lock) { slock_t _res = 1; @@ -147,11 +147,11 @@ tas(volatile slock_t *lock) * leave it alone. */ __asm__ __volatile__( - " cmpb $0,%1 \n" - " jne 1f \n" - " lock \n" - " xchgb %0,%1 \n" - "1: \n" + " cmpb $0,%1 \n" + " jne TAS%=_out \n" + " lock \n" + " xchgb %0,%1 \n" + "TAS%=_out: \n" : "+q"(_res), "+m"(*lock) : /* no inputs */ : "memory", "cc"); @@ -160,7 +160,7 @@ tas(volatile slock_t *lock) #define SPIN_DELAY() spin_delay() -static __inline__ void +static inline void spin_delay(void) { /* @@ -211,7 +211,7 @@ typedef unsigned char slock_t; */ #define TAS_SPIN(lock) (*(lock) ? 1 : TAS(lock)) -static __inline__ int +static inline int tas(volatile slock_t *lock) { slock_t _res = 1; @@ -227,7 +227,7 @@ tas(volatile slock_t *lock) #define SPIN_DELAY() spin_delay() -static __inline__ void +static inline void spin_delay(void) { /* @@ -255,7 +255,7 @@ spin_delay(void) typedef int slock_t; -static __inline__ int +static inline int tas(volatile slock_t *lock) { return __sync_lock_test_and_set(lock, 1); @@ -273,7 +273,7 @@ tas(volatile slock_t *lock) #define SPIN_DELAY() spin_delay() -static __inline__ void +static inline void spin_delay(void) { /* @@ -298,7 +298,7 @@ typedef unsigned int slock_t; #define TAS(lock) tas(lock) -static __inline__ int +static inline int tas(volatile slock_t *lock) { int _res = 0; @@ -327,15 +327,15 @@ typedef unsigned char slock_t; #define TAS(lock) tas(lock) -static __inline__ int +static inline int tas(volatile slock_t *lock) { slock_t _res; /* - * See comment in src/backend/port/tas/sunstudio_sparc.s for why this - * uses "ldstub", and that file uses "cas". gcc currently generates - * sparcv7-targeted binaries, so "cas" use isn't possible. + * "cas" would be better than "ldstub", but it is only present on + * sparcv8plus and later, while some platforms still support sparcv7 or + * sparcv8. Also, "cas" requires that the system be running in TSO mode. */ __asm__ __volatile__( " ldstub [%2], %0 \n" @@ -412,7 +412,7 @@ typedef unsigned int slock_t; * But if the spinlock is in ordinary memory, we can use lwsync instead for * better performance. */ -static __inline__ int +static inline int tas(volatile slock_t *lock) { slock_t _t; @@ -421,17 +421,17 @@ tas(volatile slock_t *lock) __asm__ __volatile__( " lwarx %0,0,%3,1 \n" " cmpwi %0,0 \n" -" bne 1f \n" +" bne TAS%=_fail \n" " addi %0,%0,1 \n" " stwcx. %0,0,%3 \n" -" beq 2f \n" -"1: \n" +" beq TAS%=_ok \n" +"TAS%=_fail: \n" " li %1,1 \n" -" b 3f \n" -"2: \n" +" b TAS%=_out \n" +"TAS%=_ok: \n" " lwsync \n" " li %1,0 \n" -"3: \n" +"TAS%=_out: \n" : "=&b"(_t), "=r"(_res), "+m"(*lock) : "r"(lock) : "memory", "cc"); @@ -478,7 +478,7 @@ typedef unsigned int slock_t; #define MIPS_SET_MIPS2 #endif -static __inline__ int +static inline int tas(volatile slock_t *lock) { volatile slock_t *_l = lock; @@ -540,7 +540,7 @@ do \ typedef int slock_t; -static __inline__ int +static inline int tas(volatile slock_t *lock) { return __sync_lock_test_and_set(lock, 1); @@ -555,7 +555,7 @@ tas(volatile slock_t *lock) typedef char slock_t; -static __inline__ int +static inline int tas(volatile slock_t *lock) { return __sync_lock_test_and_set(lock, 1); @@ -594,24 +594,6 @@ tas(volatile slock_t *lock) #if !defined(HAS_TEST_AND_SET) /* We didn't trigger above, let's try here */ -/* These are in sunstudio_(sparc|x86).s */ - -#if defined(__SUNPRO_C) && (defined(__i386) || defined(__x86_64__) || defined(__sparc__) || defined(__sparc)) -#define HAS_TEST_AND_SET - -#if defined(__i386) || defined(__x86_64__) || defined(__sparcv9) || defined(__sparcv8plus) -typedef unsigned int slock_t; -#else -typedef unsigned char slock_t; -#endif - -extern slock_t pg_atomic_cas(volatile slock_t *lock, slock_t with, - slock_t cmp); - -#define TAS(a) (pg_atomic_cas((a), 1, 0) != 0) -#endif - - #ifdef _MSC_VER typedef LONG slock_t; @@ -620,13 +602,24 @@ typedef LONG slock_t; #define SPIN_DELAY() spin_delay() -/* If using Visual C++ on Win64, inline assembly is unavailable. - * Use a _mm_pause intrinsic instead of rep nop. - */ -#if defined(_WIN64) +#ifdef _M_ARM64 static __forceinline void spin_delay(void) { + /* + * Research indicates ISB is better than __yield() on AArch64. See + * https://postgr.es/m/1c2a29b8-5b1e-44f7-a871-71ec5fefc120%40app.fastmail.com. + */ + __isb(_ARM64_BARRIER_SY); +} +#elif defined(_WIN64) +static __forceinline void +spin_delay(void) +{ + /* + * If using Visual C++ on Win64, inline assembly is unavailable. + * Use a _mm_pause intrinsic instead of rep nop. + */ _mm_pause(); } #else @@ -639,12 +632,21 @@ spin_delay(void) #endif #include -#pragma intrinsic(_ReadWriteBarrier) +#ifdef _M_ARM64 + +/* _ReadWriteBarrier() is insufficient on non-TSO architectures. */ +#pragma intrinsic(_InterlockedExchange) +#define S_UNLOCK(lock) _InterlockedExchange(lock, 0) + +#else + +#pragma intrinsic(_ReadWriteBarrier) #define S_UNLOCK(lock) \ do { _ReadWriteBarrier(); (*(lock)) = 0; } while (0) #endif +#endif #endif /* !defined(HAS_TEST_AND_SET) */ @@ -665,10 +667,6 @@ spin_delay(void) (TAS(lock) ? s_lock((lock), __FILE__, __LINE__, __func__) : 0) #endif /* S_LOCK */ -#if !defined(S_LOCK_FREE) -#define S_LOCK_FREE(lock) (*(lock) == 0) -#endif /* S_LOCK_FREE */ - #if !defined(S_UNLOCK) /* * Our default implementation of S_UNLOCK is essentially *(lock) = 0. This diff --git a/src/include/storage/sharedfileset.h b/src/include/storage/sharedfileset.h index 71a2167158a55..904396e717382 100644 --- a/src/include/storage/sharedfileset.h +++ b/src/include/storage/sharedfileset.h @@ -4,7 +4,7 @@ * Shared temporary file management. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/sharedfileset.h diff --git a/src/include/storage/shm_mq.h b/src/include/storage/shm_mq.h index 4e58e6675d4d7..6f78f4a3acb06 100644 --- a/src/include/storage/shm_mq.h +++ b/src/include/storage/shm_mq.h @@ -3,7 +3,7 @@ * shm_mq.h * single-reader, single-writer shared memory message queue * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/shm_mq.h @@ -15,7 +15,9 @@ #include "postmaster/bgworker.h" #include "storage/dsm.h" -#include "storage/proc.h" + +/* avoid including storage/proc.h */ +typedef struct PGPROC PGPROC; /* The queue itself, in shared memory. */ struct shm_mq; diff --git a/src/include/storage/shm_toc.h b/src/include/storage/shm_toc.h index 800363470905c..65e4f16eee9a0 100644 --- a/src/include/storage/shm_toc.h +++ b/src/include/storage/shm_toc.h @@ -12,7 +12,7 @@ * other data structure within the segment and only put the pointer to * the data structure itself in the table of contents. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/shm_toc.h diff --git a/src/include/storage/shmem.h b/src/include/storage/shmem.h index c1f668ded9523..af7fe893bc4b3 100644 --- a/src/include/storage/shmem.h +++ b/src/include/storage/shmem.h @@ -3,6 +3,11 @@ * shmem.h * shared memory management structures * + * This file contains public functions for other core subsystems and + * extensions to allocate shared memory. Internal functions for the shmem + * allocator itself and hooking it to the rest of the system are in + * shmem_internal.h + * * Historical note: * A long time ago, Postgres' shared memory region was allowed to be mapped * at a different address in each process, and shared memory "pointers" were @@ -11,7 +16,7 @@ * at the same address. This means shared memory pointers can be passed * around directly between different processes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/shmem.h @@ -21,23 +26,169 @@ #ifndef SHMEM_H #define SHMEM_H -#include "storage/spin.h" #include "utils/hsearch.h" +/* + * Options for ShmemRequestStruct() + * + * 'name' and 'size' are required. Initialize any optional fields that you + * don't use to zeros. + * + * After registration, the shmem machinery reserves memory for the area, sets + * '*ptr' to point to the allocation, and calls the callbacks at the right + * moments. + */ +typedef struct ShmemStructOpts +{ + const char *name; -/* shmem.c */ -extern PGDLLIMPORT slock_t *ShmemLock; -struct PGShmemHeader; /* avoid including storage/pg_shmem.h here */ -extern void InitShmemAccess(struct PGShmemHeader *seghdr); -extern void InitShmemAllocation(void); -extern void *ShmemAlloc(Size size); -extern void *ShmemAllocNoError(Size size); -extern void *ShmemAllocUnlocked(Size size); + /* + * Requested size of the shmem allocation. + * + * When attaching to an existing allocation, the size must match the size + * given when the shmem region was allocated. This cross-check can be + * disabled specifying SHMEM_ATTACH_UNKNOWN_SIZE. + */ + ssize_t size; + + /* + * Alignment of the starting address. If not set, defaults to cacheline + * boundary. Must be a power of two. + */ + size_t alignment; + + /* + * When the shmem area is initialized or attached to, pointer to it is + * stored in *ptr. It usually points to a global variable, used to access + * the shared memory area later. *ptr is set before the init_fn or + * attach_fn callback is called. + */ + void **ptr; +} ShmemStructOpts; + +#define SHMEM_ATTACH_UNKNOWN_SIZE (-1) + +/* + * Options for ShmemRequestHash() + * + * Each hash table is backed by a contiguous shmem area. + */ +typedef struct ShmemHashOpts +{ + /* Options for allocating the underlying shmem area; do not touch directly */ + ShmemStructOpts base; + + /* + * Name of the shared memory area. Required. Must be unique across the + * system. + */ + const char *name; + + /* + * 'nelems' is the max number of elements for the hash table. + */ + int64 nelems; + + /* + * Hash table options passed to hash_create() + * + * hash_info and hash_flags must specify at least the entry sizes and key + * comparison semantics (see hash_create()). Flag bits and values + * specific to shared-memory hash tables are added implicitly in + * ShmemRequestHash(), except that callers may choose to specify + * HASH_PARTITION and/or HASH_FIXED_SIZE. + */ + HASHCTL hash_info; + int hash_flags; + + /* + * When the hash table is initialized or attached to, pointer to its + * backend-private handle is stored in *ptr. It usually points to a + * global variable, used to access the hash table later. + */ + HTAB **ptr; +} ShmemHashOpts; + +typedef void (*ShmemRequestCallback) (void *opaque_arg); +typedef void (*ShmemInitCallback) (void *opaque_arg); +typedef void (*ShmemAttachCallback) (void *opaque_arg); + +/* + * Shared memory is reserved and allocated in stages at postmaster startup, + * and in EXEC_BACKEND mode, there's some extra work done to "attach" to them + * at backend startup. ShmemCallbacks holds callback functions that are + * called at different stages. + */ +typedef struct ShmemCallbacks +{ + /* SHMEM_CALLBACKS_* flags */ + int flags; + + /* + * 'request_fn' is called during postmaster startup, before the shared + * memory has been allocated. The function should call + * ShmemRequestStruct() and ShmemRequestHash() to register the subsystem's + * shared memory needs. + */ + ShmemRequestCallback request_fn; + + /* + * Initialization callback function. This is called after the shared + * memory area has been allocated, usually at postmaster startup. + */ + ShmemInitCallback init_fn; + + /* + * Attachment callback function. In EXEC_BACKEND mode, this is called at + * startup of each backend. In !EXEC_BACKEND mode, this is only called if + * the shared memory area is registered after postmaster startup (see + * SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP). + */ + ShmemAttachCallback attach_fn; + + /* + * Argument passed to the callbacks. This is opaque to the shmem system, + * callbacks can use it for their own purposes. + */ + void *opaque_arg; +} ShmemCallbacks; + +/* + * Flags to control the behavior of RegisterShmemCallbacks(). + * + * SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP: Normally, calling + * RegisterShmemCallbacks() after postmaster startup, e.g. in an add-in + * library loaded on-demand in a backend, results in an error, because shared + * memory should generally be requested at postmaster startup time. But if + * this flag is set, it is allowed and the callbacks are called immediately to + * initialize or attach to the requested shared memory areas. This is not + * used by any built-in subsystems, but extensions may find it useful. + */ +#define SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP 0x00000001 + +extern void RegisterShmemCallbacks(const ShmemCallbacks *callbacks); extern bool ShmemAddrIsValid(const void *addr); -extern void InitShmemIndex(void); -extern HTAB *ShmemInitHash(const char *name, long init_size, long max_size, - HASHCTL *infoP, int hash_flags); + +/* + * These macros provide syntactic sugar for calling the underlying functions + * with named arguments -like syntax. + */ +#define ShmemRequestStruct(...) \ + ShmemRequestStructWithOpts(&(ShmemStructOpts){__VA_ARGS__}) + +#define ShmemRequestHash(...) \ + ShmemRequestHashWithOpts(&(ShmemHashOpts){__VA_ARGS__}) + +extern void ShmemRequestStructWithOpts(const ShmemStructOpts *options); +extern void ShmemRequestHashWithOpts(const ShmemHashOpts *options); + +/* legacy shmem allocation functions */ extern void *ShmemInitStruct(const char *name, Size size, bool *foundPtr); +extern HTAB *ShmemInitHash(const char *name, int64 nelems, + HASHCTL *infoP, int hash_flags); +extern void *ShmemAlloc(Size size); +extern void *ShmemAllocNoError(Size size); + extern Size add_size(Size s1, Size s2); extern Size mul_size(Size s1, Size s2); @@ -46,19 +197,4 @@ extern PGDLLIMPORT Size pg_get_shmem_pagesize(void); /* ipci.c */ extern void RequestAddinShmemSpace(Size size); -/* size constants for the shmem index table */ - /* max size of data structure string name */ -#define SHMEM_INDEX_KEYSIZE (48) - /* estimated size of the shmem index table (not a hard limit) */ -#define SHMEM_INDEX_SIZE (64) - -/* this is a hash bucket in the shmem index table */ -typedef struct -{ - char key[SHMEM_INDEX_KEYSIZE]; /* string name */ - void *location; /* location in shared mem */ - Size size; /* # bytes requested for the structure */ - Size allocated_size; /* # bytes actually allocated */ -} ShmemIndexEnt; - #endif /* SHMEM_H */ diff --git a/src/include/storage/shmem_internal.h b/src/include/storage/shmem_internal.h new file mode 100644 index 0000000000000..8746b614fa3a3 --- /dev/null +++ b/src/include/storage/shmem_internal.h @@ -0,0 +1,53 @@ +/*------------------------------------------------------------------------- + * + * shmem_internal.h + * Internal functions related to shmem allocation + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/shmem_internal.h + * + *------------------------------------------------------------------------- + */ +#ifndef SHMEM_INTERNAL_H +#define SHMEM_INTERNAL_H + +#include "storage/shmem.h" +#include "utils/hsearch.h" + +/* Different kinds of shmem areas. */ +typedef enum +{ + SHMEM_KIND_STRUCT = 0, /* plain, contiguous area of memory */ + SHMEM_KIND_HASH, /* a hash table */ + SHMEM_KIND_SLRU, /* SLRU buffers and control structures */ +} ShmemRequestKind; + +/* shmem.c */ +typedef struct PGShmemHeader PGShmemHeader; /* avoid including + * storage/pg_shmem.h here */ +extern void ShmemCallRequestCallbacks(void); +extern void InitShmemAllocator(PGShmemHeader *seghdr); +#ifdef EXEC_BACKEND +extern void AttachShmemAllocator(PGShmemHeader *seghdr); +#endif +extern void ResetShmemAllocator(void); + +extern void ShmemRequestInternal(ShmemStructOpts *options, ShmemRequestKind kind); + +extern size_t ShmemGetRequestedSize(void); +extern void ShmemInitRequested(void); +#ifdef EXEC_BACKEND +extern void ShmemAttachRequested(void); +#endif + +extern PGDLLIMPORT Size pg_get_shmem_pagesize(void); + +/* shmem_hash.c */ +extern HTAB *shmem_hash_create(void *location, size_t size, bool found, + const char *name, int64 nelems, HASHCTL *infoP, int hash_flags); +extern void shmem_hash_init(void *location, ShmemStructOpts *base_options); +extern void shmem_hash_attach(void *location, ShmemStructOpts *base_options); + +#endif /* SHMEM_INTERNAL_H */ diff --git a/src/include/storage/sinval.h b/src/include/storage/sinval.h index 5dc5aafe5c9ff..4d33fdcabe1f3 100644 --- a/src/include/storage/sinval.h +++ b/src/include/storage/sinval.h @@ -4,7 +4,7 @@ * POSTGRES shared cache invalidation communication definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/sinval.h @@ -119,7 +119,7 @@ typedef struct Oid dbId; /* database ID */ Oid relid; /* relation ID, or 0 if whole * RelationSyncCache */ -} SharedInvalRelSyncMsg; +} SharedInvalRelSyncMsg; typedef union { diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.h index ea71cea8cacde..208ea9d051e25 100644 --- a/src/include/storage/sinvaladt.h +++ b/src/include/storage/sinvaladt.h @@ -12,7 +12,7 @@ * The struct type SharedInvalidationMessage, defining the contents of * a single message, is defined in sinval.h. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/sinvaladt.h @@ -22,14 +22,11 @@ #ifndef SINVALADT_H #define SINVALADT_H -#include "storage/lock.h" #include "storage/sinval.h" /* * prototypes for functions in sinvaladt.c */ -extern Size SharedInvalShmemSize(void); -extern void SharedInvalShmemInit(void); extern void SharedInvalBackendInit(bool sendOnly); extern void SIInsertDataEntries(const SharedInvalidationMessage *data, int n); diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h index 3964d9334b3a7..09bd42fcf4ba6 100644 --- a/src/include/storage/smgr.h +++ b/src/include/storage/smgr.h @@ -4,7 +4,7 @@ * storage manager switch public interface declarations. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/smgr.h diff --git a/src/include/storage/spin.h b/src/include/storage/spin.h index 33e2ab8757233..9cf6adb671a86 100644 --- a/src/include/storage/spin.h +++ b/src/include/storage/spin.h @@ -5,7 +5,7 @@ * * * The interface to spinlocks is defined by the typedef "slock_t" and - * these macros: + * these functions: * * void SpinLockInit(volatile slock_t *lock) * Initialize a spinlock (to the unlocked state). @@ -18,13 +18,6 @@ * void SpinLockRelease(volatile slock_t *lock) * Unlock a previously acquired lock. * - * bool SpinLockFree(slock_t *lock) - * Tests if the lock is free. Returns true if free, false if locked. - * This does *not* change the state of the lock. - * - * Callers must beware that the macro argument may be evaluated multiple - * times! - * * Load and store operations in calling code are guaranteed not to be * reordered with respect to these operations, because they include a * compiler barrier. (Before PostgreSQL 9.5, callers needed to use a @@ -33,15 +26,15 @@ * Keep in mind the coding rule that spinlocks must not be held for more * than a few instructions. In particular, we assume it is not possible * for a CHECK_FOR_INTERRUPTS() to occur while holding a spinlock, and so - * it is not necessary to do HOLD/RESUME_INTERRUPTS() in these macros. + * it is not necessary to do HOLD/RESUME_INTERRUPTS() in these functions. * - * These macros are implemented in terms of hardware-dependent macros + * These functions are implemented in terms of hardware-dependent macros * supplied by s_lock.h. There is not currently any extra functionality * added by this header, but there has been in the past and may someday * be again. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/spin.h @@ -53,13 +46,22 @@ #include "storage/s_lock.h" +static inline void +SpinLockInit(volatile slock_t *lock) +{ + S_INIT_LOCK(lock); +} -#define SpinLockInit(lock) S_INIT_LOCK(lock) - -#define SpinLockAcquire(lock) S_LOCK(lock) - -#define SpinLockRelease(lock) S_UNLOCK(lock) +static inline void +SpinLockAcquire(volatile slock_t *lock) +{ + S_LOCK(lock); +} -#define SpinLockFree(lock) S_LOCK_FREE(lock) +static inline void +SpinLockRelease(volatile slock_t *lock) +{ + S_UNLOCK(lock); +} #endif /* SPIN_H */ diff --git a/src/include/storage/standby.h b/src/include/storage/standby.h index 24e2f5082bc80..8715c08e94f20 100644 --- a/src/include/storage/standby.h +++ b/src/include/storage/standby.h @@ -4,7 +4,7 @@ * Definitions for hot standby mode. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/standby.h @@ -15,16 +15,57 @@ #define STANDBY_H #include "datatype/timestamp.h" -#include "storage/lock.h" -#include "storage/procsignal.h" +#include "storage/locktag.h" #include "storage/relfilelocator.h" #include "storage/standbydefs.h" +typedef struct PGPROC PGPROC; +typedef struct VirtualTransactionId VirtualTransactionId; + /* User-settable GUC parameters */ extern PGDLLIMPORT int max_standby_archive_delay; extern PGDLLIMPORT int max_standby_streaming_delay; extern PGDLLIMPORT bool log_recovery_conflict_waits; +/* Recovery conflict reasons */ +typedef enum +{ + /* Backend is connected to a database that is being dropped */ + RECOVERY_CONFLICT_DATABASE, + + /* Backend is using a tablespace that is being dropped */ + RECOVERY_CONFLICT_TABLESPACE, + + /* Backend is holding a lock that is blocking recovery */ + RECOVERY_CONFLICT_LOCK, + + /* Backend is holding a snapshot that is blocking recovery */ + RECOVERY_CONFLICT_SNAPSHOT, + + /* Backend is using a logical replication slot that must be invalidated */ + RECOVERY_CONFLICT_LOGICALSLOT, + + /* Backend is holding a pin on a buffer that is blocking recovery */ + RECOVERY_CONFLICT_BUFFERPIN, + + /* + * The backend is requested to check for deadlocks. The startup process + * doesn't check for deadlock directly, because we want to kill one of the + * other backends instead of the startup process. + */ + RECOVERY_CONFLICT_STARTUP_DEADLOCK, + + /* + * Like RECOVERY_CONFLICT_STARTUP_DEADLOCK is, but the suspected deadlock + * involves a buffer pin that some other backend is holding. That needs + * special checking because the normal deadlock detector doesn't track the + * buffer pins. + */ + RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK, +} RecoveryConflictReason; + +#define NUM_RECOVERY_CONFLICT_REASONS (RECOVERY_CONFLICT_BUFFERPIN_DEADLOCK + 1) + extern void InitRecoveryTransactionEnvironment(void); extern void ShutdownRecoveryTransactionEnvironment(void); @@ -43,7 +84,7 @@ extern void CheckRecoveryConflictDeadlock(void); extern void StandbyDeadLockHandler(void); extern void StandbyTimeoutHandler(void); extern void StandbyLockTimeoutHandler(void); -extern void LogRecoveryConflict(ProcSignalReason reason, TimestampTz wait_start, +extern void LogRecoveryConflict(RecoveryConflictReason reason, TimestampTz wait_start, TimestampTz now, VirtualTransactionId *wait_list, bool still_waiting); @@ -85,6 +126,7 @@ typedef enum typedef struct RunningTransactionsData { + Oid dbid; /* only track xacts in this database */ int xcnt; /* # of xact ids in xids[] */ int subxcnt; /* # of subxact ids in xids[] */ subxids_array_status subxid_status; @@ -102,7 +144,7 @@ typedef RunningTransactionsData *RunningTransactions; extern void LogAccessExclusiveLock(Oid dbOid, Oid relOid); extern void LogAccessExclusiveLockPrepare(void); -extern XLogRecPtr LogStandbySnapshot(void); +extern XLogRecPtr LogStandbySnapshot(Oid dbid); extern void LogStandbyInvalidations(int nmsgs, SharedInvalidationMessage *msgs, bool relcacheInitFileInval); diff --git a/src/include/storage/standbydefs.h b/src/include/storage/standbydefs.h index 71e5ae878b579..e75b70787665e 100644 --- a/src/include/storage/standbydefs.h +++ b/src/include/storage/standbydefs.h @@ -4,7 +4,7 @@ * Frontend exposed definitions for hot standby mode. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/standbydefs.h @@ -46,6 +46,7 @@ typedef struct xl_standby_locks */ typedef struct xl_running_xacts { + Oid dbid; /* only track xacts in this database */ int xcnt; /* # of xact ids in xids[] */ int subxcnt; /* # of subxact ids in xids[] */ bool subxid_overflow; /* snapshot overflowed, subxids missing */ diff --git a/src/include/storage/subsystemlist.h b/src/include/storage/subsystemlist.h new file mode 100644 index 0000000000000..9ad619080be22 --- /dev/null +++ b/src/include/storage/subsystemlist.h @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------- + * subsystemlist.h + * + * List of initialization callbacks of built-in subsystems. This is kept in + * its own source file for possible use by automatic tools. + * PG_SHMEM_SUBSYSTEM is defined in the callers depending on how the list is + * used. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/subsystemlist.h + *--------------------------------------------------------------------------- + */ + +/* there is deliberately not an #ifndef SUBSYSTEMLIST_H here */ + +/* + * Note: there are some inter-dependencies between these, so the order of some + * of these matter. + */ + +/* + * LWLocks first, in case any of the other shmem init functions use LWLocks. + * (Nothing else can be running during startup, so they don't need to do any + * locking yet, but we nevertheless allow it.) + */ +PG_SHMEM_SUBSYSTEM(LWLockCallbacks) + +PG_SHMEM_SUBSYSTEM(dsm_shmem_callbacks) +PG_SHMEM_SUBSYSTEM(DSMRegistryShmemCallbacks) + +/* xlog, clog, and buffers */ +PG_SHMEM_SUBSYSTEM(VarsupShmemCallbacks) +PG_SHMEM_SUBSYSTEM(XLOGShmemCallbacks) +PG_SHMEM_SUBSYSTEM(XLogPrefetchShmemCallbacks) +PG_SHMEM_SUBSYSTEM(XLogRecoveryShmemCallbacks) +PG_SHMEM_SUBSYSTEM(CLOGShmemCallbacks) +PG_SHMEM_SUBSYSTEM(CommitTsShmemCallbacks) +PG_SHMEM_SUBSYSTEM(SUBTRANSShmemCallbacks) +PG_SHMEM_SUBSYSTEM(MultiXactShmemCallbacks) +PG_SHMEM_SUBSYSTEM(BufferManagerShmemCallbacks) +PG_SHMEM_SUBSYSTEM(StrategyCtlShmemCallbacks) +PG_SHMEM_SUBSYSTEM(BufTableShmemCallbacks) + +/* lock manager */ +PG_SHMEM_SUBSYSTEM(LockManagerShmemCallbacks) + +/* predicate lock manager */ +PG_SHMEM_SUBSYSTEM(PredicateLockShmemCallbacks) + +/* process table */ +PG_SHMEM_SUBSYSTEM(ProcGlobalShmemCallbacks) +PG_SHMEM_SUBSYSTEM(ProcArrayShmemCallbacks) +PG_SHMEM_SUBSYSTEM(BackendStatusShmemCallbacks) +PG_SHMEM_SUBSYSTEM(TwoPhaseShmemCallbacks) +PG_SHMEM_SUBSYSTEM(BackgroundWorkerShmemCallbacks) + +/* shared-inval messaging */ +PG_SHMEM_SUBSYSTEM(SharedInvalShmemCallbacks) + +/* interprocess signaling mechanisms */ +PG_SHMEM_SUBSYSTEM(PMSignalShmemCallbacks) +PG_SHMEM_SUBSYSTEM(ProcSignalShmemCallbacks) +PG_SHMEM_SUBSYSTEM(CheckpointerShmemCallbacks) +PG_SHMEM_SUBSYSTEM(AutoVacuumShmemCallbacks) +PG_SHMEM_SUBSYSTEM(ReplicationSlotsShmemCallbacks) +PG_SHMEM_SUBSYSTEM(ReplicationOriginShmemCallbacks) +PG_SHMEM_SUBSYSTEM(WalSndShmemCallbacks) +PG_SHMEM_SUBSYSTEM(WalRcvShmemCallbacks) +PG_SHMEM_SUBSYSTEM(WalSummarizerShmemCallbacks) +PG_SHMEM_SUBSYSTEM(PgArchShmemCallbacks) +PG_SHMEM_SUBSYSTEM(ApplyLauncherShmemCallbacks) +PG_SHMEM_SUBSYSTEM(SlotSyncShmemCallbacks) + +/* other modules that need some shared memory space */ +PG_SHMEM_SUBSYSTEM(BTreeShmemCallbacks) +PG_SHMEM_SUBSYSTEM(SyncScanShmemCallbacks) +PG_SHMEM_SUBSYSTEM(AsyncShmemCallbacks) +PG_SHMEM_SUBSYSTEM(StatsShmemCallbacks) +PG_SHMEM_SUBSYSTEM(WaitEventCustomShmemCallbacks) +#ifdef USE_INJECTION_POINTS +PG_SHMEM_SUBSYSTEM(InjectionPointShmemCallbacks) +#endif +PG_SHMEM_SUBSYSTEM(WaitLSNShmemCallbacks) +PG_SHMEM_SUBSYSTEM(LogicalDecodingCtlShmemCallbacks) +PG_SHMEM_SUBSYSTEM(DataChecksumsShmemCallbacks) + +/* AIO subsystem. This delegates to the method-specific callbacks */ +PG_SHMEM_SUBSYSTEM(AioShmemCallbacks) diff --git a/src/include/storage/subsystems.h b/src/include/storage/subsystems.h new file mode 100644 index 0000000000000..38b735bec67ad --- /dev/null +++ b/src/include/storage/subsystems.h @@ -0,0 +1,30 @@ +/*------------------------------------------------------------------------- + * + * subsystems.h + * Provide extern declarations for all the built-in subsystem callbacks + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/storage/subsystems.h + * + *------------------------------------------------------------------------- + */ +#ifndef SUBSYSTEMS_H +#define SUBSYSTEMS_H + +#include "storage/shmem.h" + +/* + * Extern declarations of all the built-in subsystem callbacks + * + * The actual list is in subsystemlist.h, so that the same list can be used + * for other purposes. + */ +#define PG_SHMEM_SUBSYSTEM(callbacks) \ + extern const ShmemCallbacks callbacks; +#include "storage/subsystemlist.h" +#undef PG_SHMEM_SUBSYSTEM + +#endif /* SUBSYSTEMS_H */ diff --git a/src/include/storage/sync.h b/src/include/storage/sync.h index c2272d1417585..88290500bc912 100644 --- a/src/include/storage/sync.h +++ b/src/include/storage/sync.h @@ -3,7 +3,7 @@ * sync.h * File synchronization management code. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/sync.h diff --git a/src/include/storage/waiteventset.h b/src/include/storage/waiteventset.h index dd514d5299104..5341267f0a0fe 100644 --- a/src/include/storage/waiteventset.h +++ b/src/include/storage/waiteventset.h @@ -15,7 +15,7 @@ * functions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/storage/waiteventset.h diff --git a/src/include/tcop/backend_startup.h b/src/include/tcop/backend_startup.h index dcb9d056643f2..d486f9263195a 100644 --- a/src/include/tcop/backend_startup.h +++ b/src/include/tcop/backend_startup.h @@ -4,7 +4,7 @@ * prototypes for backend_startup.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/backend_startup.h @@ -86,7 +86,7 @@ typedef enum LogConnectionOption LOG_CONNECTION_AUTHENTICATION | LOG_CONNECTION_AUTHORIZATION | LOG_CONNECTION_SETUP_DURATIONS, -} LogConnectionOption; +} LogConnectionOption; /* * A collection of timings of various stages of connection establishment and diff --git a/src/include/tcop/cmdtag.h b/src/include/tcop/cmdtag.h index 8d027fcc457cd..cf2e87b98f314 100644 --- a/src/include/tcop/cmdtag.h +++ b/src/include/tcop/cmdtag.h @@ -3,7 +3,7 @@ * cmdtag.h * Declarations for commandtag names and enumeration. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/cmdtag.h diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h index d250a714d597d..befae5f6b4fce 100644 --- a/src/include/tcop/cmdtaglist.h +++ b/src/include/tcop/cmdtaglist.h @@ -8,7 +8,7 @@ * determined by the PG_CMDTAG macro, which is not defined in this file; * it can be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/cmdtaglist.h @@ -48,6 +48,7 @@ PG_CMDTAG(CMDTAG_ALTER_OPERATOR_CLASS, "ALTER OPERATOR CLASS", true, false, fals PG_CMDTAG(CMDTAG_ALTER_OPERATOR_FAMILY, "ALTER OPERATOR FAMILY", true, false, false) PG_CMDTAG(CMDTAG_ALTER_POLICY, "ALTER POLICY", true, false, false) PG_CMDTAG(CMDTAG_ALTER_PROCEDURE, "ALTER PROCEDURE", true, false, false) +PG_CMDTAG(CMDTAG_ALTER_PROPERTY_GRAPH, "ALTER PROPERTY GRAPH", true, false, false) PG_CMDTAG(CMDTAG_ALTER_PUBLICATION, "ALTER PUBLICATION", true, false, false) PG_CMDTAG(CMDTAG_ALTER_ROLE, "ALTER ROLE", false, false, false) PG_CMDTAG(CMDTAG_ALTER_ROUTINE, "ALTER ROUTINE", true, false, false) @@ -103,6 +104,7 @@ PG_CMDTAG(CMDTAG_CREATE_OPERATOR_CLASS, "CREATE OPERATOR CLASS", true, false, fa PG_CMDTAG(CMDTAG_CREATE_OPERATOR_FAMILY, "CREATE OPERATOR FAMILY", true, false, false) PG_CMDTAG(CMDTAG_CREATE_POLICY, "CREATE POLICY", true, false, false) PG_CMDTAG(CMDTAG_CREATE_PROCEDURE, "CREATE PROCEDURE", true, false, false) +PG_CMDTAG(CMDTAG_CREATE_PROPERTY_GRAPH, "CREATE PROPERTY GRAPH", true, false, false) PG_CMDTAG(CMDTAG_CREATE_PUBLICATION, "CREATE PUBLICATION", true, false, false) PG_CMDTAG(CMDTAG_CREATE_ROLE, "CREATE ROLE", false, false, false) PG_CMDTAG(CMDTAG_CREATE_ROUTINE, "CREATE ROUTINE", true, false, false) @@ -156,6 +158,7 @@ PG_CMDTAG(CMDTAG_DROP_OPERATOR_FAMILY, "DROP OPERATOR FAMILY", true, false, fals PG_CMDTAG(CMDTAG_DROP_OWNED, "DROP OWNED", true, false, false) PG_CMDTAG(CMDTAG_DROP_POLICY, "DROP POLICY", true, false, false) PG_CMDTAG(CMDTAG_DROP_PROCEDURE, "DROP PROCEDURE", true, false, false) +PG_CMDTAG(CMDTAG_DROP_PROPERTY_GRAPH, "DROP PROPERTY GRAPH", true, false, false) PG_CMDTAG(CMDTAG_DROP_PUBLICATION, "DROP PUBLICATION", true, false, false) PG_CMDTAG(CMDTAG_DROP_ROLE, "DROP ROLE", false, false, false) PG_CMDTAG(CMDTAG_DROP_ROUTINE, "DROP ROUTINE", true, false, false) @@ -196,6 +199,7 @@ PG_CMDTAG(CMDTAG_REASSIGN_OWNED, "REASSIGN OWNED", false, false, false) PG_CMDTAG(CMDTAG_REFRESH_MATERIALIZED_VIEW, "REFRESH MATERIALIZED VIEW", true, false, false) PG_CMDTAG(CMDTAG_REINDEX, "REINDEX", true, false, false) PG_CMDTAG(CMDTAG_RELEASE, "RELEASE", false, false, false) +PG_CMDTAG(CMDTAG_REPACK, "REPACK", false, false, false) PG_CMDTAG(CMDTAG_RESET, "RESET", false, false, false) PG_CMDTAG(CMDTAG_REVOKE, "REVOKE", true, false, false) PG_CMDTAG(CMDTAG_REVOKE_ROLE, "REVOKE ROLE", false, false, false) @@ -217,3 +221,4 @@ PG_CMDTAG(CMDTAG_TRUNCATE_TABLE, "TRUNCATE TABLE", false, false, false) PG_CMDTAG(CMDTAG_UNLISTEN, "UNLISTEN", false, false, false) PG_CMDTAG(CMDTAG_UPDATE, "UPDATE", false, false, true) PG_CMDTAG(CMDTAG_VACUUM, "VACUUM", false, false, false) +PG_CMDTAG(CMDTAG_WAIT, "WAIT", false, false, false) diff --git a/src/include/tcop/deparse_utility.h b/src/include/tcop/deparse_utility.h index 36510451d833f..30868bd6c7713 100644 --- a/src/include/tcop/deparse_utility.h +++ b/src/include/tcop/deparse_utility.h @@ -2,7 +2,7 @@ * * deparse_utility.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/deparse_utility.h diff --git a/src/include/tcop/dest.h b/src/include/tcop/dest.h index 00c092e3d7c06..4e4f532d8cc2f 100644 --- a/src/include/tcop/dest.h +++ b/src/include/tcop/dest.h @@ -57,7 +57,7 @@ * calls in portal and cursor manipulations. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/dest.h diff --git a/src/include/tcop/fastpath.h b/src/include/tcop/fastpath.h index 3a94ec4f8a6ae..23c8fb8a91d09 100644 --- a/src/include/tcop/fastpath.h +++ b/src/include/tcop/fastpath.h @@ -3,7 +3,7 @@ * fastpath.h * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/fastpath.h diff --git a/src/include/tcop/pquery.h b/src/include/tcop/pquery.h index fa3cc5f2dfcaf..aabb81ec71eb6 100644 --- a/src/include/tcop/pquery.h +++ b/src/include/tcop/pquery.h @@ -4,7 +4,7 @@ * prototypes for pquery.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/pquery.h @@ -17,7 +17,7 @@ #include "nodes/parsenodes.h" #include "utils/portal.h" -struct PlannedStmt; /* avoid including plannodes.h here */ +typedef struct PlannedStmt PlannedStmt; /* avoid including plannodes.h here */ extern PGDLLIMPORT Portal ActivePortal; @@ -44,7 +44,7 @@ extern uint64 PortalRunFetch(Portal portal, long count, DestReceiver *dest); -extern bool PlannedStmtRequiresSnapshot(struct PlannedStmt *pstmt); +extern bool PlannedStmtRequiresSnapshot(PlannedStmt *pstmt); extern void EnsurePortalSnapshotExists(void); diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h index a83cc4f485016..5bc5bcfb20d12 100644 --- a/src/include/tcop/tcopprot.h +++ b/src/include/tcop/tcopprot.h @@ -4,7 +4,7 @@ * prototypes for postgres.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/tcopprot.h @@ -20,6 +20,7 @@ #include "utils/guc.h" #include "utils/queryenvironment.h" +typedef struct ExplainState ExplainState; /* defined in explain_state.h */ extern PGDLLIMPORT CommandDest whereToSendOutput; extern PGDLLIMPORT const char *debug_query_string; @@ -63,7 +64,8 @@ extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree, QueryEnvironment *queryEnv); extern PlannedStmt *pg_plan_query(Query *querytree, const char *query_string, int cursorOptions, - ParamListInfo boundParams); + ParamListInfo boundParams, + ExplainState *es); extern List *pg_plan_queries(List *querytrees, const char *query_string, int cursorOptions, ParamListInfo boundParams); @@ -72,7 +74,7 @@ extern void die(SIGNAL_ARGS); pg_noreturn extern void quickdie(SIGNAL_ARGS); extern void StatementCancelHandler(SIGNAL_ARGS); pg_noreturn extern void FloatExceptionHandler(SIGNAL_ARGS); -extern void HandleRecoveryConflictInterrupt(ProcSignalReason reason); +extern void HandleRecoveryConflictInterrupt(void); extern void ProcessClientReadInterrupt(bool blocked); extern void ProcessClientWriteInterrupt(bool blocked); diff --git a/src/include/tcop/utility.h b/src/include/tcop/utility.h index 2f05dd50e2584..abbdb401bf6d2 100644 --- a/src/include/tcop/utility.h +++ b/src/include/tcop/utility.h @@ -4,7 +4,7 @@ * prototypes for utility.c. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tcop/utility.h diff --git a/src/include/tsearch/dicts/regis.h b/src/include/tsearch/dicts/regis.h index 3f40cdc92e452..5d1a8e21c8d67 100644 --- a/src/include/tsearch/dicts/regis.h +++ b/src/include/tsearch/dicts/regis.h @@ -4,7 +4,7 @@ * * Declarations for fast regex subset, used by ISpell * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/tsearch/dicts/regis.h * diff --git a/src/include/tsearch/dicts/spell.h b/src/include/tsearch/dicts/spell.h index 919b7df63705b..038b4384fb44a 100644 --- a/src/include/tsearch/dicts/spell.h +++ b/src/include/tsearch/dicts/spell.h @@ -4,7 +4,7 @@ * * Declarations for ISpell dictionary * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/tsearch/dicts/spell.h * diff --git a/src/include/tsearch/ts_cache.h b/src/include/tsearch/ts_cache.h index ed7527bb954db..a433096b07d11 100644 --- a/src/include/tsearch/ts_cache.h +++ b/src/include/tsearch/ts_cache.h @@ -3,7 +3,7 @@ * ts_cache.h * Tsearch related object caches. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/tsearch/ts_cache.h diff --git a/src/include/tsearch/ts_locale.h b/src/include/tsearch/ts_locale.h index c29588d8532c6..f4edf300c2b84 100644 --- a/src/include/tsearch/ts_locale.h +++ b/src/include/tsearch/ts_locale.h @@ -3,7 +3,7 @@ * ts_locale.h * locale compatibility layer for tsearch * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * src/include/tsearch/ts_locale.h * @@ -37,10 +37,34 @@ typedef struct /* The second argument of t_iseq() must be a plain ASCII character */ #define t_iseq(x,c) (TOUCHAR(x) == (unsigned char) (c)) -#define COPYCHAR(d,s) memcpy(d, s, pg_mblen(s)) +/* Copy multibyte character of known byte length, return byte length. */ +static inline int +ts_copychar_with_len(void *dest, const void *src, int length) +{ + memcpy(dest, src, length); + return length; +} + +/* Copy multibyte character from null-terminated string, return byte length. */ +static inline int +ts_copychar_cstr(void *dest, const void *src) +{ + return ts_copychar_with_len(dest, src, pg_mblen_cstr((const char *) src)); +} + +/* Historical macro for the above. */ +#define COPYCHAR ts_copychar_cstr + +#define GENERATE_T_ISCLASS_DECL(character_class) \ +extern int t_is##character_class##_with_len(const char *ptr, int mblen); \ +extern int t_is##character_class##_cstr(const char *ptr); \ +extern int t_is##character_class##_unbounded(const char *ptr); \ +\ +/* deprecated */ \ +extern int t_is##character_class(const char *ptr); -extern int t_isalpha(const char *ptr); -extern int t_isalnum(const char *ptr); +GENERATE_T_ISCLASS_DECL(alnum); +GENERATE_T_ISCLASS_DECL(alpha); extern bool tsearch_readline_begin(tsearch_readline_state *stp, const char *filename); diff --git a/src/include/tsearch/ts_public.h b/src/include/tsearch/ts_public.h index 52e8bde683e95..dcc19cdce7bc7 100644 --- a/src/include/tsearch/ts_public.h +++ b/src/include/tsearch/ts_public.h @@ -4,7 +4,7 @@ * Public interface to various tsearch modules, such as * parsers and dictionaries. * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * src/include/tsearch/ts_public.h * diff --git a/src/include/tsearch/ts_type.h b/src/include/tsearch/ts_type.h index d9d5334f28ba1..847f6d3497ecd 100644 --- a/src/include/tsearch/ts_type.h +++ b/src/include/tsearch/ts_type.h @@ -3,7 +3,7 @@ * ts_type.h * Definitions for the tsvector and tsquery types * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * src/include/tsearch/ts_type.h * diff --git a/src/include/tsearch/ts_utils.h b/src/include/tsearch/ts_utils.h index 7debc85ed80fa..3eb0770f9c20a 100644 --- a/src/include/tsearch/ts_utils.h +++ b/src/include/tsearch/ts_utils.h @@ -3,7 +3,7 @@ * ts_utils.h * helper utilities for tsearch * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * src/include/tsearch/ts_utils.h * @@ -40,21 +40,19 @@ extern bool gettoken_tsvector(TSVectorParseState state, extern void close_tsvector_parser(TSVectorParseState state); /* phrase operator begins with '<' */ -#define ISOPERATOR(x) \ - ( pg_mblen(x) == 1 && ( *(x) == '!' || \ - *(x) == '&' || \ - *(x) == '|' || \ - *(x) == '(' || \ - *(x) == ')' || \ - *(x) == '<' \ - ) ) +#define ISOPERATOR(x) (*(x) == '!' || \ + *(x) == '&' || \ + *(x) == '|' || \ + *(x) == '(' || \ + *(x) == ')' || \ + *(x) == '<') /* parse_tsquery */ struct TSQueryParserStateData; /* private in backend/utils/adt/tsquery.c */ typedef struct TSQueryParserStateData *TSQueryParserState; -typedef void (*PushFunction) (Datum opaque, TSQueryParserState state, +typedef void (*PushFunction) (void *opaque, TSQueryParserState state, char *token, int tokenlen, int16 tokenweights, /* bitmap as described in * QueryOperand struct */ @@ -66,7 +64,7 @@ typedef void (*PushFunction) (Datum opaque, TSQueryParserState state, extern TSQuery parse_tsquery(char *buf, PushFunction pushval, - Datum opaque, + void *opaque, int flags, Node *escontext); diff --git a/src/include/utils/.gitignore b/src/include/utils/.gitignore index c1b4c662139b1..ff6f61cd7ee7b 100644 --- a/src/include/utils/.gitignore +++ b/src/include/utils/.gitignore @@ -1,6 +1,9 @@ /fmgroids.h /fmgrprotos.h +/guc_tables.inc.c /probes.h /errcodes.h /header-stamp +/pgstat_wait_event.c +/wait_event_funcs_data.c /wait_event_types.h diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 01ae5b719fd78..0b9b04e78eeaf 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -4,7 +4,7 @@ * Definition of (and support for) access control list data structures. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/acl.h @@ -166,6 +166,7 @@ typedef struct ArrayType Acl; #define ACL_ALL_RIGHTS_LANGUAGE (ACL_USAGE) #define ACL_ALL_RIGHTS_LARGEOBJECT (ACL_SELECT|ACL_UPDATE) #define ACL_ALL_RIGHTS_PARAMETER_ACL (ACL_SET|ACL_ALTER_SYSTEM) +#define ACL_ALL_RIGHTS_PROPGRAPH (ACL_SELECT) #define ACL_ALL_RIGHTS_SCHEMA (ACL_USAGE|ACL_CREATE) #define ACL_ALL_RIGHTS_TABLESPACE (ACL_CREATE) #define ACL_ALL_RIGHTS_TYPE (ACL_USAGE) @@ -223,7 +224,7 @@ extern void check_rolespec_name(const RoleSpec *role, const char *detail_msg); extern HeapTuple get_rolespec_tuple(const RoleSpec *role); extern char *get_rolespec_name(const RoleSpec *role); -extern void select_best_grantor(Oid roleId, AclMode privileges, +extern void select_best_grantor(const RoleSpec *grantedBy, AclMode privileges, const Acl *acl, Oid ownerId, Oid *grantorId, AclMode *grantOptions); diff --git a/src/include/utils/aclchk_internal.h b/src/include/utils/aclchk_internal.h index 62af290779a8f..fa0b65fbba7d8 100644 --- a/src/include/utils/aclchk_internal.h +++ b/src/include/utils/aclchk_internal.h @@ -2,7 +2,7 @@ * * aclchk_internal.h * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/aclchk_internal.h @@ -38,6 +38,7 @@ typedef struct List *col_privs; List *grantees; bool grant_option; + RoleSpec *grantor; DropBehavior behavior; } InternalGrant; diff --git a/src/include/utils/array.h b/src/include/utils/array.h index 52f1fbf8d43f6..88e4f4d70d8e4 100644 --- a/src/include/utils/array.h +++ b/src/include/utils/array.h @@ -51,7 +51,7 @@ * arrays holding the elements. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/array.h @@ -65,8 +65,8 @@ #include "utils/expandeddatum.h" /* avoid including execnodes.h here */ -struct ExprState; -struct ExprContext; +typedef struct ExprState ExprState; +typedef struct ExprContext ExprContext; /* @@ -206,7 +206,7 @@ typedef struct ArrayBuildStateArr { MemoryContext mcontext; /* where all the temp stuff is kept */ char *data; /* accumulated data */ - bits8 *nullbitmap; /* bitmap of is-null flags, or NULL if none */ + uint8 *nullbitmap; /* bitmap of is-null flags, or NULL if none */ int abytes; /* allocated length of "data" */ int nbytes; /* number of bytes used so far */ int aitems; /* allocated length of bitmap (in elements) */ @@ -299,9 +299,9 @@ typedef struct ArrayIteratorData *ArrayIterator; #define ARR_NULLBITMAP(a) \ (ARR_HASNULL(a) ? \ - (bits8 *) (((char *) (a)) + sizeof(ArrayType) + \ + (uint8 *) (((char *) (a)) + sizeof(ArrayType) + \ 2 * sizeof(int) * ARR_NDIM(a)) \ - : (bits8 *) NULL) + : (uint8 *) NULL) /* * The total array header size (in bytes) for an array with the specified @@ -352,8 +352,8 @@ extern PGDLLIMPORT bool Array_nulls; * prototypes for functions defined in arrayfuncs.c */ extern void CopyArrayEls(ArrayType *array, - Datum *values, - bool *nulls, + const Datum *values, + const bool *nulls, int nitems, int typlen, bool typbyval, @@ -384,11 +384,11 @@ extern ArrayType *array_set(ArrayType *array, int nSubscripts, int *indx, int arraytyplen, int elmlen, bool elmbyval, char elmalign); extern Datum array_map(Datum arrayd, - struct ExprState *exprstate, struct ExprContext *econtext, + ExprState *exprstate, ExprContext *econtext, Oid retType, ArrayMapState *amstate); -extern void array_bitmap_copy(bits8 *destbitmap, int destoffset, - const bits8 *srcbitmap, int srcoffset, +extern void array_bitmap_copy(uint8 *destbitmap, int destoffset, + const uint8 *srcbitmap, int srcoffset, int nitems); extern ArrayType *construct_array(Datum *elems, int nelems, @@ -405,14 +405,14 @@ extern ArrayType *construct_empty_array(Oid elmtype); extern ExpandedArrayHeader *construct_empty_expanded_array(Oid element_type, MemoryContext parentcontext, ArrayMetaState *metacache); -extern void deconstruct_array(ArrayType *array, +extern void deconstruct_array(const ArrayType *array, Oid elmtype, int elmlen, bool elmbyval, char elmalign, Datum **elemsp, bool **nullsp, int *nelemsp); -extern void deconstruct_array_builtin(ArrayType *array, +extern void deconstruct_array_builtin(const ArrayType *array, Oid elmtype, Datum **elemsp, bool **nullsp, int *nelemsp); -extern bool array_contains_nulls(ArrayType *array); +extern bool array_contains_nulls(const ArrayType *array); extern ArrayBuildState *initArrayResult(Oid element_type, MemoryContext rcontext, bool subcontext); diff --git a/src/include/utils/arrayaccess.h b/src/include/utils/arrayaccess.h index 0e48ee07f57fd..b983014ab0f07 100644 --- a/src/include/utils/arrayaccess.h +++ b/src/include/utils/arrayaccess.h @@ -4,7 +4,7 @@ * Declarations for element-by-element access to Postgres arrays. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/arrayaccess.h @@ -22,8 +22,8 @@ * Functions for iterating through elements of a flat or expanded array. * These require a state struct "array_iter iter". * - * Use "array_iter_setup(&iter, arrayptr);" to prepare to iterate, and - * "datumvar = array_iter_next(&iter, &isnullvar, index, ...);" to fetch + * Use "array_iter_setup(&iter, arrayptr, ...);" to prepare to iterate, + * and "datumvar = array_iter_next(&iter, &isnullvar, index);" to fetch * the next element into datumvar/isnullvar. * "index" must be the zero-origin element number; we make caller provide * this since caller is generally counting the elements anyway. Despite @@ -40,13 +40,19 @@ typedef struct array_iter /* Fields used when we have a flat array */ char *dataptr; /* Current spot in the data area */ - bits8 *bitmapptr; /* Current byte of the nulls bitmap, or NULL */ + uint8 *bitmapptr; /* Current byte of the nulls bitmap, or NULL */ int bitmask; /* mask for current bit in nulls bitmap */ + + /* Fields used in both cases: data about array's element type */ + int elmlen; + bool elmbyval; + uint8 elmalignby; } array_iter; static inline void -array_iter_setup(array_iter *it, AnyArrayType *a) +array_iter_setup(array_iter *it, AnyArrayType *a, + int elmlen, bool elmbyval, char elmalign) { if (VARATT_IS_EXPANDED_HEADER(a)) { @@ -75,11 +81,13 @@ array_iter_setup(array_iter *it, AnyArrayType *a) it->bitmapptr = ARR_NULLBITMAP((ArrayType *) a); } it->bitmask = 1; + it->elmlen = elmlen; + it->elmbyval = elmbyval; + it->elmalignby = typalign_to_alignby(elmalign); } static inline Datum -array_iter_next(array_iter *it, bool *isnull, int i, - int elmlen, bool elmbyval, char elmalign) +array_iter_next(array_iter *it, bool *isnull, int i) { Datum ret; @@ -98,10 +106,11 @@ array_iter_next(array_iter *it, bool *isnull, int i, else { *isnull = false; - ret = fetch_att(it->dataptr, elmbyval, elmlen); - it->dataptr = att_addlength_pointer(it->dataptr, elmlen, + ret = fetch_att(it->dataptr, it->elmbyval, it->elmlen); + it->dataptr = att_addlength_pointer(it->dataptr, it->elmlen, it->dataptr); - it->dataptr = (char *) att_align_nominal(it->dataptr, elmalign); + it->dataptr = (char *) att_nominal_alignby(it->dataptr, + it->elmalignby); } it->bitmask <<= 1; if (it->bitmask == 0x100) diff --git a/src/include/utils/ascii.h b/src/include/utils/ascii.h index 02d45d5494533..183928a6305af 100644 --- a/src/include/utils/ascii.h +++ b/src/include/utils/ascii.h @@ -1,7 +1,7 @@ /*----------------------------------------------------------------------- * ascii.h * - * Portions Copyright (c) 1999-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1999-2026, PostgreSQL Global Development Group * * src/include/utils/ascii.h * diff --git a/src/include/utils/attoptcache.h b/src/include/utils/attoptcache.h index f684a772af52a..172055fcd102a 100644 --- a/src/include/utils/attoptcache.h +++ b/src/include/utils/attoptcache.h @@ -3,7 +3,7 @@ * attoptcache.h * Attribute options cache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/attoptcache.h diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h index dda813ab4076b..61e13c40e28e2 100644 --- a/src/include/utils/backend_progress.h +++ b/src/include/utils/backend_progress.h @@ -6,7 +6,7 @@ * backend's command progress counters, without ascribing meaning to the * individual fields. See commands/progress.h and system_views.sql for that. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/backend_progress.h * ---------- @@ -24,10 +24,11 @@ typedef enum ProgressCommandType PROGRESS_COMMAND_INVALID, PROGRESS_COMMAND_VACUUM, PROGRESS_COMMAND_ANALYZE, - PROGRESS_COMMAND_CLUSTER, PROGRESS_COMMAND_CREATE_INDEX, PROGRESS_COMMAND_BASEBACKUP, PROGRESS_COMMAND_COPY, + PROGRESS_COMMAND_REPACK, + PROGRESS_COMMAND_DATACHECKSUMS, } ProgressCommandType; #define PGSTAT_NUM_PROGRESS_PARAM 20 diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h index 430ccd7d78e41..a334e096e4a6b 100644 --- a/src/include/utils/backend_status.h +++ b/src/include/utils/backend_status.h @@ -2,7 +2,7 @@ * backend_status.h * Definitions related to backend status reporting * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/backend_status.h * ---------- @@ -170,10 +170,10 @@ typedef struct PgBackendStatus int64 st_progress_param[PGSTAT_NUM_PROGRESS_PARAM]; /* query identifier, optionally computed using post_parse_analyze_hook */ - uint64 st_query_id; + int64 st_query_id; /* plan identifier, optionally computed using planner_hook */ - uint64 st_plan_id; + int64 st_plan_id; } PgBackendStatus; @@ -298,14 +298,6 @@ extern PGDLLIMPORT int pgstat_track_activity_query_size; extern PGDLLIMPORT PgBackendStatus *MyBEEntry; -/* ---------- - * Functions called from postmaster - * ---------- - */ -extern Size BackendStatusShmemSize(void); -extern void BackendStatusShmemInit(void); - - /* ---------- * Functions called from backends * ---------- @@ -321,17 +313,16 @@ extern void pgstat_clear_backend_activity_snapshot(void); /* Activity reporting functions */ extern void pgstat_report_activity(BackendState state, const char *cmd_str); -extern void pgstat_report_query_id(uint64 query_id, bool force); -extern void pgstat_report_plan_id(uint64 plan_id, bool force); +extern void pgstat_report_query_id(int64 query_id, bool force); +extern void pgstat_report_plan_id(int64 plan_id, bool force); extern void pgstat_report_tempfile(size_t filesize); extern void pgstat_report_appname(const char *appname); extern void pgstat_report_xact_timestamp(TimestampTz tstamp); extern const char *pgstat_get_backend_current_activity(int pid, bool checkUser); extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer, int buflen); -extern uint64 pgstat_get_my_query_id(void); -extern uint64 pgstat_get_my_plan_id(void); -extern BackendType pgstat_get_backend_type_by_proc_number(ProcNumber procNumber); +extern int64 pgstat_get_my_query_id(void); +extern int64 pgstat_get_my_plan_id(void); /* ---------- diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index 1c98c7d2255ce..b6a11bfa28832 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -4,7 +4,7 @@ * Declarations for operations on built-in types. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/builtins.h @@ -68,6 +68,7 @@ extern char *pg_ultostr(char *str, uint32 value); /* oid.c */ extern oidvector *buildoidvector(const Oid *oids, int n); +extern void check_valid_oidvector(const oidvector *oidArray); extern Oid oidparse(Node *node); extern int oid_cmp(const void *p1, const void *p2); @@ -80,7 +81,7 @@ extern PGDLLIMPORT bool quote_all_identifiers; extern const char *quote_identifier(const char *ident); extern char *quote_qualified_identifier(const char *qualifier, const char *ident); -extern void generate_operator_clause(fmStringInfo buf, +extern void generate_operator_clause(StringInfo buf, const char *leftop, Oid leftoptype, Oid opoid, const char *rightop, Oid rightoptype); @@ -125,7 +126,7 @@ extern Datum numeric_float8_no_overflow(PG_FUNCTION_ARGS); #define FORMAT_TYPE_ALLOW_INVALID 0x02 /* allow invalid types */ #define FORMAT_TYPE_FORCE_QUALIFY 0x04 /* force qualification of type */ #define FORMAT_TYPE_INVALID_AS_NULL 0x08 /* NULL if undefined */ -extern char *format_type_extended(Oid type_oid, int32 typemod, bits16 flags); +extern char *format_type_extended(Oid type_oid, int32 typemod, uint16 flags); extern char *format_type_be(Oid type_oid); extern char *format_type_be_qualified(Oid type_oid); diff --git a/src/include/utils/bytea.h b/src/include/utils/bytea.h index 7b31fc84ba46b..dc25da39e898e 100644 --- a/src/include/utils/bytea.h +++ b/src/include/utils/bytea.h @@ -4,7 +4,7 @@ * Declarations for BYTEA data type support. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/bytea.h diff --git a/src/include/utils/catcache.h b/src/include/utils/catcache.h index 277ec33c00bac..a28a1e483eb9a 100644 --- a/src/include/utils/catcache.h +++ b/src/include/utils/catcache.h @@ -10,7 +10,7 @@ * guarantee that there can only be one matching row for a key combination. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/catcache.h @@ -69,24 +69,32 @@ typedef struct catcache * doesn't break ABI for other modules */ #ifdef CATCACHE_STATS - long cc_searches; /* total # searches against this cache */ - long cc_hits; /* # of matches against existing entry */ - long cc_neg_hits; /* # of matches against negative entry */ - long cc_newloads; /* # of successful loads of new entry */ + uint64 cc_searches; /* total # searches against this cache */ + uint64 cc_hits; /* # of matches against existing entry */ + uint64 cc_neg_hits; /* # of matches against negative entry */ + uint64 cc_newloads; /* # of successful loads of new entry */ /* * cc_searches - (cc_hits + cc_neg_hits + cc_newloads) is number of failed * searches, each of which will result in loading a negative entry */ - long cc_invals; /* # of entries invalidated from cache */ - long cc_lsearches; /* total # list-searches */ - long cc_lhits; /* # of matches against existing lists */ + uint64 cc_invals; /* # of entries invalidated from cache */ + uint64 cc_lsearches; /* total # list-searches */ + uint64 cc_lhits; /* # of matches against existing lists */ #endif } CatCache; typedef struct catctup { + /* + * Each tuple in a cache is a member of a dlist that stores the elements + * of its hash bucket. We keep each dlist in LRU order to speed repeated + * lookups. Keep the dlist_node field first so that Valgrind understands + * the struct is reachable. + */ + dlist_node cache_elem; /* member for CatCache.cc_bucket[] dlist */ + int ct_magic; /* for identifying CatCTup entries */ #define CT_MAGIC 0x57261502 @@ -98,13 +106,6 @@ typedef struct catctup */ Datum keys[CATCACHE_MAXKEYS]; - /* - * Each tuple in a cache is a member of a dlist that stores the elements - * of its hash bucket. We keep each dlist in LRU order to speed repeated - * lookups. - */ - dlist_node cache_elem; /* list member of per-bucket list */ - /* * A tuple marked "dead" must not be returned by subsequent searches. * However, it won't be physically deleted from the cache until its @@ -143,9 +144,6 @@ typedef struct catctup * object contains links to cache entries for all the table rows satisfying * the partial key. (Note: none of these will be negative cache entries.) * - * A CatCList is only a member of a per-cache list; we do not currently - * divide them into hash buckets. - * * A list marked "dead" must not be returned by subsequent searches. * However, it won't be physically deleted from the cache until its * refcount goes to zero. (A list should be marked dead if any of its @@ -158,13 +156,17 @@ typedef struct catctup */ typedef struct catclist { + /* + * Keep the dlist_node field first so that Valgrind understands the struct + * is reachable. + */ + dlist_node cache_elem; /* member for CatCache.cc_lbucket[] dlist */ + int cl_magic; /* for identifying CatCList entries */ #define CL_MAGIC 0x52765103 uint32 hash_value; /* hash value for lookup keys */ - dlist_node cache_elem; /* list member of per-catcache list */ - /* * Lookup keys for the entry, with the first nkeys elements being valid. * All by-reference are separately allocated. diff --git a/src/include/utils/combocid.h b/src/include/utils/combocid.h index 4a2069b2869b7..6cd3cdd40355a 100644 --- a/src/include/utils/combocid.h +++ b/src/include/utils/combocid.h @@ -4,7 +4,7 @@ * Combo command ID support routines * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/combocid.h diff --git a/src/include/utils/conffiles.h b/src/include/utils/conffiles.h index 799abb41bd066..1548cd70bfa1f 100644 --- a/src/include/utils/conffiles.h +++ b/src/include/utils/conffiles.h @@ -3,7 +3,7 @@ * * Utilities related to configuration files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/conffiles.h diff --git a/src/include/utils/date.h b/src/include/utils/date.h index bb5c1e57b073e..6063810891e99 100644 --- a/src/include/utils/date.h +++ b/src/include/utils/date.h @@ -4,7 +4,7 @@ * Definitions for the SQL "date" and "time" types. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/date.h @@ -14,8 +14,6 @@ #ifndef DATE_H #define DATE_H -#include - #include "datatype/timestamp.h" #include "fmgr.h" #include "pgtime.h" @@ -30,6 +28,14 @@ typedef struct int32 zone; /* numeric time zone, in seconds */ } TimeTzADT; +/* + * sizeof(TimeTzADT) will be 16 on most platforms due to alignment padding. + * However, timetz's typlen is 12 according to pg_type. In most places + * we can get away with using sizeof(TimeTzADT), but where it's important + * to match the declared typlen, use TIMETZ_TYPLEN. + */ +#define TIMETZ_TYPLEN 12 + /* * Infinity and minus infinity must be the max and min values of DateADT. */ @@ -98,8 +104,10 @@ TimeTzADTPGetDatum(const TimeTzADT *X) /* date.c */ extern int32 anytime_typmod_check(bool istz, int32 typmod); extern double date2timestamp_no_overflow(DateADT dateVal); -extern Timestamp date2timestamp_opt_overflow(DateADT dateVal, int *overflow); -extern TimestampTz date2timestamptz_opt_overflow(DateADT dateVal, int *overflow); +extern Timestamp date2timestamp_safe(DateADT dateVal, Node *escontext); +extern TimestampTz date2timestamptz_safe(DateADT dateVal, Node *escontext); +extern DateADT timestamp2date_safe(Timestamp timestamp, Node *escontext); +extern DateADT timestamptz2date_safe(TimestampTz timestamp, Node *escontext); extern int32 date_cmp_timestamp_internal(DateADT dateVal, Timestamp dt2); extern int32 date_cmp_timestamptz_internal(DateADT dateVal, TimestampTz dt2); diff --git a/src/include/utils/datetime.h b/src/include/utils/datetime.h index 53a1c69eda52d..f77c6acd8b61c 100644 --- a/src/include/utils/datetime.h +++ b/src/include/utils/datetime.h @@ -6,7 +6,7 @@ * including date, and time. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/datetime.h diff --git a/src/include/utils/datum.h b/src/include/utils/datum.h index fce048ec033f3..b42d9826882cd 100644 --- a/src/include/utils/datum.h +++ b/src/include/utils/datum.h @@ -8,7 +8,7 @@ * of the Datum. (We do it this way because in most situations the caller * can look up the info just once and use it for many per-datum operations.) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/datum.h diff --git a/src/include/utils/dsa.h b/src/include/utils/dsa.h index 9eca87889087c..38b6cdb1465e4 100644 --- a/src/include/utils/dsa.h +++ b/src/include/utils/dsa.h @@ -3,7 +3,7 @@ * dsa.h * Dynamic shared memory areas. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -114,13 +114,13 @@ typedef pg_atomic_uint64 dsa_pointer_atomic; dsa_allocate_extended(area, size, DSA_ALLOC_ZERO) /* Create dsa_area with default segment sizes */ -#define dsa_create(tranch_id) \ - dsa_create_ext(tranch_id, DSA_DEFAULT_INIT_SEGMENT_SIZE, \ +#define dsa_create(tranche_id) \ + dsa_create_ext(tranche_id, DSA_DEFAULT_INIT_SEGMENT_SIZE, \ DSA_MAX_SEGMENT_SIZE) /* Create dsa_area with default segment sizes in an existing share memory space */ -#define dsa_create_in_place(place, size, tranch_id, segment) \ - dsa_create_in_place_ext(place, size, tranch_id, segment, \ +#define dsa_create_in_place(place, size, tranche_id, segment) \ + dsa_create_in_place_ext(place, size, tranche_id, segment, \ DSA_DEFAULT_INIT_SEGMENT_SIZE, \ DSA_MAX_SEGMENT_SIZE) @@ -145,6 +145,7 @@ extern dsa_area *dsa_create_in_place_ext(void *place, size_t size, size_t init_segment_size, size_t max_segment_size); extern dsa_area *dsa_attach(dsa_handle handle); +extern bool dsa_is_attached(dsa_handle handle); extern dsa_area *dsa_attach_in_place(void *place, dsm_segment *segment); extern void dsa_release_in_place(void *place); extern void dsa_on_dsm_detach_release_in_place(dsm_segment *, Datum); @@ -160,6 +161,7 @@ extern dsa_pointer dsa_allocate_extended(dsa_area *area, size_t size, int flags) extern void dsa_free(dsa_area *area, dsa_pointer dp); extern void *dsa_get_address(dsa_area *area, dsa_pointer dp); extern size_t dsa_get_total_size(dsa_area *area); +extern size_t dsa_get_total_size_from_handle(dsa_handle handle); extern void dsa_trim(dsa_area *area); extern void dsa_dump(dsa_area *area); diff --git a/src/include/utils/dynahash.h b/src/include/utils/dynahash.h deleted file mode 100644 index 8a31d9524e2a4..0000000000000 --- a/src/include/utils/dynahash.h +++ /dev/null @@ -1,20 +0,0 @@ -/*------------------------------------------------------------------------- - * - * dynahash.h - * POSTGRES dynahash.h file definitions - * - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/include/utils/dynahash.h - * - *------------------------------------------------------------------------- - */ -#ifndef DYNAHASH_H -#define DYNAHASH_H - -extern int my_log2(long num); - -#endif /* DYNAHASH_H */ diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h index 5eac0e16970c3..9c1e75c59ac60 100644 --- a/src/include/utils/elog.h +++ b/src/include/utils/elog.h @@ -4,7 +4,7 @@ * POSTGRES error reporting/logging definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/elog.h @@ -23,6 +23,7 @@ struct Node; /* Error level codes */ +#define LOG_NEVER 0 /* Never emit this message */ #define DEBUG5 10 /* Debugging messages, in categories of * decreasing detail. */ #define DEBUG4 11 @@ -53,7 +54,8 @@ struct Node; * known state */ #define PGERROR 21 /* Must equal ERROR; see NOTE below. */ #define FATAL 22 /* fatal error - abort process */ -#define PANIC 23 /* take down the other backends with me */ +#define FATAL_CLIENT_ONLY 23 /* fatal version of WARNING_CLIENT_ONLY */ +#define PANIC 24 /* take down the other backends with me */ /* * NOTE: the alternate names PGWARNING and PGERROR are useful for dealing @@ -119,36 +121,37 @@ struct Node; * ereport_domain() directly, or preferably they can override the TEXTDOMAIN * macro. * - * When __builtin_constant_p is available and elevel >= ERROR we make a call - * to errstart_cold() instead of errstart(). This version of the function is - * marked with pg_attribute_cold which will coax supporting compilers into - * generating code which is more optimized towards non-ERROR cases. Because - * we use __builtin_constant_p() in the condition, when elevel is not a - * compile-time constant, or if it is, but it's < ERROR, the compiler has no - * need to generate any code for this branch. It can simply call errstart() - * unconditionally. + * When pg_integer_constant_p is available and elevel >= ERROR we make + * a call to errstart_cold() instead of errstart(). This version of the + * function is marked with pg_attribute_cold which will coax supporting + * compilers into generating code which is more optimized towards non-ERROR + * cases. Because we use pg_integer_constant_p() in the condition, + * when elevel is not a compile-time constant, or if it is, but it's < ERROR, + * the compiler has no need to generate any code for this branch. It can + * simply call errstart() unconditionally. * * If elevel >= ERROR, the call will not return; we try to inform the compiler * of that via pg_unreachable(). However, no useful optimization effect is * obtained unless the compiler sees elevel as a compile-time constant, else - * we're just adding code bloat. So, if __builtin_constant_p is available, - * use that to cause the second if() to vanish completely for non-constant - * cases. We avoid using a local variable because it's not necessary and - * prevents gcc from making the unreachability deduction at optlevel -O0. + * we're just adding code bloat. So, if pg_integer_constant_p is + * available, use that to cause the second if() to vanish completely for + * non-constant cases. We avoid using a local variable because it's not + * necessary and prevents gcc from making the unreachability deduction at + * optlevel -O0. *---------- */ -#ifdef HAVE__BUILTIN_CONSTANT_P +#ifdef HAVE_PG_INTEGER_CONSTANT_P #define ereport_domain(elevel, domain, ...) \ do { \ pg_prevent_errno_in_scope(); \ - if (__builtin_constant_p(elevel) && (elevel) >= ERROR ? \ + if (pg_integer_constant_p(elevel) && (elevel) >= ERROR ? \ errstart_cold(elevel, domain) : \ errstart(elevel, domain)) \ __VA_ARGS__, errfinish(__FILE__, __LINE__, __func__); \ - if (__builtin_constant_p(elevel) && (elevel) >= ERROR) \ + if (pg_integer_constant_p(elevel) && (elevel) >= ERROR) \ pg_unreachable(); \ } while(0) -#else /* !HAVE__BUILTIN_CONSTANT_P */ +#else /* !HAVE_PG_INTEGER_CONSTANT_P */ #define ereport_domain(elevel, domain, ...) \ do { \ const int elevel_ = (elevel); \ @@ -158,7 +161,7 @@ struct Node; if (elevel_ >= ERROR) \ pg_unreachable(); \ } while(0) -#endif /* HAVE__BUILTIN_CONSTANT_P */ +#endif /* HAVE_PG_INTEGER_CONSTANT_P */ #define ereport(elevel, ...) \ ereport_domain(elevel, TEXTDOMAIN, __VA_ARGS__) @@ -485,7 +488,7 @@ typedef enum PGERROR_TERSE, /* single-line error messages */ PGERROR_DEFAULT, /* recommended style */ PGERROR_VERBOSE, /* all the facts, ma'am */ -} PGErrorVerbosity; +} PGErrorVerbosity; extern PGDLLIMPORT int Log_error_verbosity; extern PGDLLIMPORT char *Log_line_prefix; @@ -527,5 +530,6 @@ extern void write_jsonlog(ErrorData *edata); * safely (memory context, GUC load etc) */ extern void write_stderr(const char *fmt,...) pg_attribute_printf(1, 2); +extern void vwrite_stderr(const char *fmt, va_list ap) pg_attribute_printf(1, 0); #endif /* ELOG_H */ diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h index 9d9fcb8657b97..2373a8bd45460 100644 --- a/src/include/utils/evtcache.h +++ b/src/include/utils/evtcache.h @@ -3,7 +3,7 @@ * evtcache.h * Special-purpose cache for event trigger data. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/include/utils/expandeddatum.h b/src/include/utils/expandeddatum.h index cde9a0c073b8c..7c2d5bbc46c5b 100644 --- a/src/include/utils/expandeddatum.h +++ b/src/include/utils/expandeddatum.h @@ -34,7 +34,7 @@ * value if they fail partway through. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/expandeddatum.h @@ -95,7 +95,7 @@ typedef struct ExpandedObjectMethods * But note that these pointers are just a convenience; a pointer object * appearing somewhere else would still be legal. * - * The typedef declaration for this appears in postgres.h. + * The typedef declaration for this appears in varatt.h. */ struct ExpandedObjectHeader { diff --git a/src/include/utils/expandedrecord.h b/src/include/utils/expandedrecord.h index 4e5cbc44d635d..0c017a3e0c2cb 100644 --- a/src/include/utils/expandedrecord.h +++ b/src/include/utils/expandedrecord.h @@ -3,7 +3,7 @@ * expandedrecord.h * Declarations for composite expanded objects. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/expandedrecord.h diff --git a/src/include/utils/float.h b/src/include/utils/float.h index 0e2e9ec534783..ffa743d627310 100644 --- a/src/include/utils/float.h +++ b/src/include/utils/float.h @@ -3,7 +3,7 @@ * float.h * Definitions for the built-in floating-point types * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -25,13 +25,6 @@ /* Radians per degree, a.k.a. PI / 180 */ #define RADIANS_PER_DEGREE 0.0174532925199432957692 -/* Visual C++ etc lacks NAN, and won't accept 0.0/0.0. */ -#if defined(WIN32) && !defined(NAN) -static const uint32 nan[2] = {0xffffffff, 0x7fffffff}; - -#define NAN (*(const float8 *) nan) -#endif - extern PGDLLIMPORT int extra_float_digits; /* @@ -40,6 +33,9 @@ extern PGDLLIMPORT int extra_float_digits; pg_noreturn extern void float_overflow_error(void); pg_noreturn extern void float_underflow_error(void); pg_noreturn extern void float_zero_divide_error(void); +extern float8 float_overflow_error_ext(struct Node *escontext); +extern float8 float_underflow_error_ext(struct Node *escontext); +extern float8 float_zero_divide_error_ext(struct Node *escontext); extern int is_infinite(float8 val); extern float8 float8in_internal(char *num, char **endptr_p, const char *type_name, const char *orig_string, @@ -52,84 +48,46 @@ extern int float4_cmp_internal(float4 a, float4 b); extern int float8_cmp_internal(float8 a, float8 b); /* - * Routines to provide reasonably platform-independent handling of - * infinity and NaN + * Postgres requires IEEE-standard float arithmetic, including infinities + * and NaNs. We used to support pre-C99 compilers on which might + * not supply the standard macros INFINITY and NAN. We no longer do so, + * but these wrapper functions are still preferred over using those macros + * directly. * - * We assume that isinf() and isnan() are available and work per spec. - * (On some platforms, we have to supply our own; see src/port.) However, - * generating an Infinity or NaN in the first place is less well standardized; - * pre-C99 systems tend not to have C99's INFINITY and NaN macros. We - * centralize our workarounds for this here. + * If you change these functions, see copies in interfaces/ecpg/ecpglib/data.c. */ -/* - * The funny placements of the two #pragmas is necessary because of a - * long lived bug in the Microsoft compilers. - * See http://support.microsoft.com/kb/120968/en-us for details - */ -#ifdef _MSC_VER -#pragma warning(disable:4756) -#endif static inline float4 get_float4_infinity(void) { -#ifdef INFINITY /* C99 standard way */ return (float4) INFINITY; -#else -#ifdef _MSC_VER -#pragma warning(default:4756) -#endif - - /* - * On some platforms, HUGE_VAL is an infinity, elsewhere it's just the - * largest normal float8. We assume forcing an overflow will get us a - * true infinity. - */ - return (float4) (HUGE_VAL * HUGE_VAL); -#endif } static inline float8 get_float8_infinity(void) { -#ifdef INFINITY /* C99 standard way */ return (float8) INFINITY; -#else - - /* - * On some platforms, HUGE_VAL is an infinity, elsewhere it's just the - * largest normal float8. We assume forcing an overflow will get us a - * true infinity. - */ - return (float8) (HUGE_VAL * HUGE_VAL); -#endif } +/* The C standard allows implementations to omit NAN, but we don't */ +#ifndef NAN +#error "Postgres requires support for IEEE quiet NaNs" +#endif + static inline float4 get_float4_nan(void) { -#ifdef NAN /* C99 standard way */ return (float4) NAN; -#else - /* Assume we can get a NAN via zero divide */ - return (float4) (0.0 / 0.0); -#endif } static inline float8 get_float8_nan(void) { - /* (float8) NAN doesn't work on some NetBSD/MIPS releases */ -#if defined(NAN) && !(defined(__NetBSD__) && defined(__mips__)) /* C99 standard way */ return (float8) NAN; -#else - /* Assume we can get a NaN via zero divide */ - return (float8) (0.0 / 0.0); -#endif } /* @@ -155,17 +113,23 @@ float4_pl(const float4 val1, const float4 val2) } static inline float8 -float8_pl(const float8 val1, const float8 val2) +float8_pl_safe(const float8 val1, const float8 val2, struct Node *escontext) { float8 result; result = val1 + val2; if (unlikely(isinf(result)) && !isinf(val1) && !isinf(val2)) - float_overflow_error(); + return float_overflow_error_ext(escontext); return result; } +static inline float8 +float8_pl(const float8 val1, const float8 val2) +{ + return float8_pl_safe(val1, val2, NULL); +} + static inline float4 float4_mi(const float4 val1, const float4 val2) { @@ -179,17 +143,23 @@ float4_mi(const float4 val1, const float4 val2) } static inline float8 -float8_mi(const float8 val1, const float8 val2) +float8_mi_safe(const float8 val1, const float8 val2, struct Node *escontext) { float8 result; result = val1 - val2; if (unlikely(isinf(result)) && !isinf(val1) && !isinf(val2)) - float_overflow_error(); + return float_overflow_error_ext(escontext); return result; } +static inline float8 +float8_mi(const float8 val1, const float8 val2) +{ + return float8_mi_safe(val1, val2, NULL); +} + static inline float4 float4_mul(const float4 val1, const float4 val2) { @@ -205,19 +175,25 @@ float4_mul(const float4 val1, const float4 val2) } static inline float8 -float8_mul(const float8 val1, const float8 val2) +float8_mul_safe(const float8 val1, const float8 val2, struct Node *escontext) { float8 result; result = val1 * val2; if (unlikely(isinf(result)) && !isinf(val1) && !isinf(val2)) - float_overflow_error(); + return float_overflow_error_ext(escontext); if (unlikely(result == 0.0) && val1 != 0.0 && val2 != 0.0) - float_underflow_error(); + return float_underflow_error_ext(escontext); return result; } +static inline float8 +float8_mul(const float8 val1, const float8 val2) +{ + return float8_mul_safe(val1, val2, NULL); +} + static inline float4 float4_div(const float4 val1, const float4 val2) { @@ -235,21 +211,27 @@ float4_div(const float4 val1, const float4 val2) } static inline float8 -float8_div(const float8 val1, const float8 val2) +float8_div_safe(const float8 val1, const float8 val2, struct Node *escontext) { float8 result; if (unlikely(val2 == 0.0) && !isnan(val1)) - float_zero_divide_error(); + return float_zero_divide_error_ext(escontext); result = val1 / val2; if (unlikely(isinf(result)) && !isinf(val1)) - float_overflow_error(); + return float_overflow_error_ext(escontext); if (unlikely(result == 0.0) && val1 != 0.0 && !isinf(val2)) - float_underflow_error(); + return float_underflow_error_ext(escontext); return result; } +static inline float8 +float8_div(const float8 val1, const float8 val2) +{ + return float8_div_safe(val1, val2, NULL); +} + /* * Routines for NaN-aware comparisons * diff --git a/src/include/utils/fmgrtab.h b/src/include/utils/fmgrtab.h index 8d43583c6880f..52bfd59c7d4ef 100644 --- a/src/include/utils/fmgrtab.h +++ b/src/include/utils/fmgrtab.h @@ -3,7 +3,7 @@ * fmgrtab.h * The function manager's table of internal functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/fmgrtab.h diff --git a/src/include/utils/formatting.h b/src/include/utils/formatting.h index 835307dac0935..6f2d7864d6df5 100644 --- a/src/include/utils/formatting.h +++ b/src/include/utils/formatting.h @@ -4,7 +4,7 @@ * src/include/utils/formatting.h * * - * Portions Copyright (c) 1999-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1999-2026, PostgreSQL Global Development Group * * The PostgreSQL routines for a DateTime/int/float/numeric formatting, * inspired by the Oracle TO_CHAR() / TO_DATE() / TO_NUMBER() routines. diff --git a/src/include/utils/freepage.h b/src/include/utils/freepage.h index 18643a8c35d31..2681fd6d5ea96 100644 --- a/src/include/utils/freepage.h +++ b/src/include/utils/freepage.h @@ -3,7 +3,7 @@ * freepage.h * Management of page-organized free memory. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/freepage.h @@ -65,7 +65,7 @@ struct FreePageManager /* Macros to convert between page numbers (expressed as Size) and pointers. */ #define fpm_page_to_pointer(base, page) \ - (AssertVariableIsOfTypeMacro(page, Size), \ + (StaticAssertVariableIsOfTypeMacro(page, Size), \ (base) + FPM_PAGE_SIZE * (page)) #define fpm_pointer_to_page(base, ptr) \ (((Size) (((char *) (ptr)) - (base))) / FPM_PAGE_SIZE) diff --git a/src/include/utils/funccache.h b/src/include/utils/funccache.h index e0112ebfa11de..ee8f03ee99776 100644 --- a/src/include/utils/funccache.h +++ b/src/include/utils/funccache.h @@ -5,7 +5,7 @@ * * See funccache.c for comments. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/funccache.h diff --git a/src/include/utils/geo_decls.h b/src/include/utils/geo_decls.h index 8a9df75c93c9d..a7014cded5190 100644 --- a/src/include/utils/geo_decls.h +++ b/src/include/utils/geo_decls.h @@ -3,7 +3,7 @@ * geo_decls.h - Declarations for various 2D constructs. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/geo_decls.h @@ -88,7 +88,6 @@ FPge(double A, double B) #define FPge(A,B) ((A) >= (B)) #endif -#define HYPOT(A, B) pg_hypot(A, B) /*--------------------------------------------------------------------- * Point - (x,y) @@ -275,11 +274,4 @@ CirclePGetDatum(const CIRCLE *X) #define PG_GETARG_CIRCLE_P(n) DatumGetCircleP(PG_GETARG_DATUM(n)) #define PG_RETURN_CIRCLE_P(x) return CirclePGetDatum(x) - -/* - * in geo_ops.c - */ - -extern float8 pg_hypot(float8 x, float8 y); - #endif /* GEO_DECLS_H */ diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index f619100467df2..dc406d6651aa2 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -3,7 +3,7 @@ * * External declarations pertaining to Grand Unified Configuration. * - * Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Copyright (c) 2000-2026, PostgreSQL Global Development Group * Written by Peter Eisentraut . * * src/include/utils/guc.h @@ -106,7 +106,7 @@ typedef enum * will show as "default" in pg_settings. If there is a specific reason not * to want that, use source == PGC_S_OVERRIDE. * - * NB: see GucSource_Names in guc.c if you change this. + * NB: see GucSource_Names in guc_tables.c if you change this. */ typedef enum { @@ -247,6 +247,7 @@ typedef enum /* GUC vars that are actually defined in guc_tables.c, rather than elsewhere */ extern PGDLLIMPORT bool Debug_print_plan; extern PGDLLIMPORT bool Debug_print_parse; +extern PGDLLIMPORT bool Debug_print_raw_parse; extern PGDLLIMPORT bool Debug_print_rewritten; extern PGDLLIMPORT bool Debug_pretty_print; @@ -254,8 +255,31 @@ extern PGDLLIMPORT bool Debug_pretty_print; extern PGDLLIMPORT bool Debug_copy_parse_plan_trees; extern PGDLLIMPORT bool Debug_write_read_parse_plan_trees; extern PGDLLIMPORT bool Debug_raw_expression_coverage_test; + +/* + * support for legacy compile-time settings + */ + +#ifdef COPY_PARSE_PLAN_TREES +#define DEFAULT_DEBUG_COPY_PARSE_PLAN_TREES true +#else +#define DEFAULT_DEBUG_COPY_PARSE_PLAN_TREES false +#endif + +#ifdef WRITE_READ_PARSE_PLAN_TREES +#define DEFAULT_DEBUG_WRITE_READ_PARSE_PLAN_TREES true +#else +#define DEFAULT_DEBUG_WRITE_READ_PARSE_PLAN_TREES false +#endif + +#ifdef RAW_EXPRESSION_COVERAGE_TEST +#define DEFAULT_DEBUG_RAW_EXPRESSION_COVERAGE_TEST true +#else +#define DEFAULT_DEBUG_RAW_EXPRESSION_COVERAGE_TEST false #endif +#endif /* DEBUG_NODE_TESTS_ENABLED */ + extern PGDLLIMPORT bool log_parser_stats; extern PGDLLIMPORT bool log_planner_stats; extern PGDLLIMPORT bool log_executor_stats; @@ -271,7 +295,7 @@ extern PGDLLIMPORT bool log_duration; extern PGDLLIMPORT int log_parameter_max_length; extern PGDLLIMPORT int log_parameter_max_length_on_error; extern PGDLLIMPORT int log_min_error_statement; -extern PGDLLIMPORT int log_min_messages; +extern PGDLLIMPORT int log_min_messages[]; extern PGDLLIMPORT int client_min_messages; extern PGDLLIMPORT int log_min_duration_sample; extern PGDLLIMPORT int log_min_duration_statement; @@ -288,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name; extern PGDLLIMPORT char *ConfigFileName; extern PGDLLIMPORT char *HbaFileName; extern PGDLLIMPORT char *IdentFileName; +extern PGDLLIMPORT char *HostsFileName; extern PGDLLIMPORT char *external_pid_file; extern PGDLLIMPORT char *application_name; @@ -320,6 +345,7 @@ extern PGDLLIMPORT const struct config_enum_entry archive_mode_options[]; extern PGDLLIMPORT const struct config_enum_entry dynamic_shared_memory_options[]; extern PGDLLIMPORT const struct config_enum_entry io_method_options[]; extern PGDLLIMPORT const struct config_enum_entry recovery_target_action_options[]; +extern PGDLLIMPORT const struct config_enum_entry server_message_level_options[]; extern PGDLLIMPORT const struct config_enum_entry wal_level_options[]; extern PGDLLIMPORT const struct config_enum_entry wal_sync_method_options[]; diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h index 799fa7ace6847..307f4fbaefe08 100644 --- a/src/include/utils/guc_hooks.h +++ b/src/include/utils/guc_hooks.h @@ -7,7 +7,7 @@ * declare them all here to avoid having to propagate guc.h into * a lot of unrelated header files. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/utils/guc_hooks.h * @@ -61,6 +61,7 @@ extern bool check_default_text_search_config(char **newval, void **extra, GucSou extern void assign_default_text_search_config(const char *newval, void *extra); extern bool check_default_with_oids(bool *newval, void **extra, GucSource source); +extern const char *show_effective_wal_level(void); extern bool check_huge_page_size(int *newval, void **extra, GucSource source); extern void assign_io_method(int newval, void *extra); extern bool check_io_max_concurrency(int *newval, void **extra, GucSource source); @@ -84,8 +85,6 @@ extern const char *show_log_timezone(void); extern void assign_maintenance_io_concurrency(int newval, void *extra); extern void assign_io_max_combine_limit(int newval, void *extra); extern void assign_io_combine_limit(int newval, void *extra); -extern bool check_max_slot_wal_keep_size(int *newval, void **extra, - GucSource source); extern void assign_max_wal_size(int newval, void *extra); extern bool check_max_stack_depth(int *newval, void **extra, GucSource source); extern void assign_max_stack_depth(int newval, void *extra); @@ -134,7 +133,10 @@ extern void assign_session_authorization(const char *newval, void *extra); extern void assign_session_replication_role(int newval, void *extra); extern void assign_stats_fetch_consistency(int newval, void *extra); extern bool check_ssl(bool *newval, void **extra, GucSource source); +extern bool check_ssl_sni(bool *newval, void **extra, GucSource source); extern bool check_stage_log_stats(bool *newval, void **extra, GucSource source); +extern bool check_standard_conforming_strings(bool *newval, void **extra, + GucSource source); extern bool check_subtrans_buffers(int *newval, void **extra, GucSource source); extern bool check_synchronous_standby_names(char **newval, void **extra, @@ -161,6 +163,9 @@ extern const char *show_timezone(void); extern bool check_timezone_abbreviations(char **newval, void **extra, GucSource source); extern void assign_timezone_abbreviations(const char *newval, void *extra); +extern void assign_timing_clock_source(int newval, void *extra); +extern bool check_timing_clock_source(int *newval, void **extra, GucSource source); +extern const char *show_timing_clock_source(void); extern bool check_transaction_buffers(int *newval, void **extra, GucSource source); extern bool check_transaction_deferrable(bool *newval, void **extra, GucSource source); extern bool check_transaction_isolation(int *newval, void **extra, GucSource source); @@ -176,7 +181,7 @@ extern void assign_wal_sync_method(int new_wal_sync_method, void *extra); extern bool check_synchronized_standby_slots(char **newval, void **extra, GucSource source); extern void assign_synchronized_standby_slots(const char *newval, void *extra); -extern bool check_idle_replication_slot_timeout(int *newval, void **extra, - GucSource source); +extern bool check_log_min_messages(char **newval, void **extra, GucSource source); +extern void assign_log_min_messages(const char *newval, void *extra); #endif /* GUC_HOOKS_H */ diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h index f72ce944d7f9f..63440b8e36c83 100644 --- a/src/include/utils/guc_tables.h +++ b/src/include/utils/guc_tables.h @@ -5,7 +5,7 @@ * * See src/backend/utils/misc/README for design notes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/utils/guc_tables.h * @@ -60,6 +60,7 @@ enum config_group CONN_AUTH_TCP, CONN_AUTH_AUTH, CONN_AUTH_SSL, + RESOURCES_TIME, RESOURCES_MEM, RESOURCES_DISK, RESOURCES_KERNEL, @@ -132,90 +133,11 @@ typedef struct guc_stack config_var_value masked; /* SET value in a GUC_SET_LOCAL entry */ } GucStack; -/* - * Generic fields applicable to all types of variables - * - * The short description should be less than 80 chars in length. Some - * applications may use the long description as well, and will append - * it to the short description. (separated by a newline or '. ') - * - * If the GUC accepts a special value like -1 to disable the feature, use a - * system default, etc., it should be mentioned in the long description with - * the following style: - * - * - Special values should be listed at the end of the long description. - * - Descriptions should use numerals (e.g., "0") instead of words (e.g., - * "zero"). - * - Special value mentions should be concise and direct (e.g., "0 disables - * the timeout.", "An empty string means use the operating system - * setting."). - * - Multiple special values should be listed in ascending order. - * - * As an exception, special values should _not_ be mentioned if the description - * would be too complex or if the meaning is sufficiently obvious. - * - * srole is the role that set the current value, or BOOTSTRAP_SUPERUSERID - * if the value came from an internal source or the config file. Similarly - * for reset_srole (which is usually BOOTSTRAP_SUPERUSERID, but not always). - * - * Variables that are currently of active interest for maintenance - * operations are linked into various lists using the xxx_link fields. - * The link fields are unused/garbage in variables not currently having - * the specified properties. - * - * Note that sourcefile/sourceline are kept here, and not pushed into stacked - * values, although in principle they belong with some stacked value if the - * active value is session- or transaction-local. This is to avoid bloating - * stack entries. We know they are only relevant when source == PGC_S_FILE. - */ -struct config_generic -{ - /* constant fields, must be set correctly in initial value: */ - const char *name; /* name of variable - MUST BE FIRST */ - GucContext context; /* context required to set the variable */ - enum config_group group; /* to help organize variables by function */ - const char *short_desc; /* short desc. of this variable's purpose */ - const char *long_desc; /* long desc. of this variable's purpose */ - int flags; /* flag bits, see guc.h */ - /* variable fields, initialized at runtime: */ - enum config_type vartype; /* type of variable (set only at startup) */ - int status; /* status bits, see below */ - GucSource source; /* source of the current actual value */ - GucSource reset_source; /* source of the reset_value */ - GucContext scontext; /* context that set the current value */ - GucContext reset_scontext; /* context that set the reset value */ - Oid srole; /* role that set the current value */ - Oid reset_srole; /* role that set the reset value */ - GucStack *stack; /* stacked prior values */ - void *extra; /* "extra" pointer for current actual value */ - dlist_node nondef_link; /* list link for variables that have source - * different from PGC_S_DEFAULT */ - slist_node stack_link; /* list link for variables that have non-NULL - * stack */ - slist_node report_link; /* list link for variables that have the - * GUC_NEEDS_REPORT bit set in status */ - char *last_reported; /* if variable is GUC_REPORT, value last sent - * to client (NULL if not yet sent) */ - char *sourcefile; /* file current setting is from (NULL if not - * set in config file) */ - int sourceline; /* line in source file */ -}; - -/* bit values in status field */ -#define GUC_IS_IN_FILE 0x0001 /* found it in config file */ -/* - * Caution: the GUC_IS_IN_FILE bit is transient state for ProcessConfigFile. - * Do not assume that its value represents useful information elsewhere. - */ -#define GUC_PENDING_RESTART 0x0002 /* changed value cannot be applied yet */ -#define GUC_NEEDS_REPORT 0x0004 /* new value must be reported to client */ - /* GUC records for specific variable types */ struct config_bool { - struct config_generic gen; /* constant fields, must be set correctly in initial value: */ bool *variable; bool boot_val; @@ -224,12 +146,10 @@ struct config_bool GucShowHook show_hook; /* variable fields, initialized at runtime: */ bool reset_val; - void *reset_extra; }; struct config_int { - struct config_generic gen; /* constant fields, must be set correctly in initial value: */ int *variable; int boot_val; @@ -240,12 +160,10 @@ struct config_int GucShowHook show_hook; /* variable fields, initialized at runtime: */ int reset_val; - void *reset_extra; }; struct config_real { - struct config_generic gen; /* constant fields, must be set correctly in initial value: */ double *variable; double boot_val; @@ -256,7 +174,6 @@ struct config_real GucShowHook show_hook; /* variable fields, initialized at runtime: */ double reset_val; - void *reset_extra; }; /* @@ -271,7 +188,6 @@ struct config_real */ struct config_string { - struct config_generic gen; /* constant fields, must be set correctly in initial value: */ char **variable; const char *boot_val; @@ -280,12 +196,10 @@ struct config_string GucShowHook show_hook; /* variable fields, initialized at runtime: */ char *reset_val; - void *reset_extra; }; struct config_enum { - struct config_generic gen; /* constant fields, must be set correctly in initial value: */ int *variable; int boot_val; @@ -295,21 +209,106 @@ struct config_enum GucShowHook show_hook; /* variable fields, initialized at runtime: */ int reset_val; +}; + +/* + * Generic fields applicable to all types of variables + * + * The short description should be less than 80 chars in length. Some + * applications may use the long description as well, and will append + * it to the short description. (separated by a newline or '. ') + * + * If the GUC accepts a special value like -1 to disable the feature, use a + * system default, etc., it should be mentioned in the long description with + * the following style: + * + * - Special values should be listed at the end of the long description. + * - Descriptions should use numerals (e.g., "0") instead of words (e.g., + * "zero"). + * - Special value mentions should be concise and direct (e.g., "0 disables + * the timeout.", "An empty string means use the operating system + * setting."). + * - Multiple special values should be listed in ascending order. + * + * As an exception, special values should _not_ be mentioned if the description + * would be too complex or if the meaning is sufficiently obvious. + * + * srole is the role that set the current value, or BOOTSTRAP_SUPERUSERID + * if the value came from an internal source or the config file. Similarly + * for reset_srole (which is usually BOOTSTRAP_SUPERUSERID, but not always). + * + * Variables that are currently of active interest for maintenance + * operations are linked into various lists using the xxx_link fields. + * The link fields are unused/garbage in variables not currently having + * the specified properties. + * + * Note that sourcefile/sourceline are kept here, and not pushed into stacked + * values, although in principle they belong with some stacked value if the + * active value is session- or transaction-local. This is to avoid bloating + * stack entries. We know they are only relevant when source == PGC_S_FILE. + */ +struct config_generic +{ + /* constant fields, must be set correctly in initial value: */ + const char *name; /* name of variable */ + GucContext context; /* context required to set the variable */ + enum config_group group; /* to help organize variables by function */ + const char *short_desc; /* short desc. of this variable's purpose */ + const char *long_desc; /* long desc. of this variable's purpose */ + int flags; /* flag bits, see guc.h */ + enum config_type vartype; /* type of variable */ + /* variable fields, initialized at runtime: */ + int status; /* status bits, see below */ + GucSource source; /* source of the current actual value */ + GucSource reset_source; /* source of the reset_value */ + GucContext scontext; /* context that set the current value */ + GucContext reset_scontext; /* context that set the reset value */ + Oid srole; /* role that set the current value */ + Oid reset_srole; /* role that set the reset value */ + GucStack *stack; /* stacked prior values */ + void *extra; /* "extra" pointer for current actual value */ void *reset_extra; + dlist_node nondef_link; /* list link for variables that have source + * different from PGC_S_DEFAULT */ + slist_node stack_link; /* list link for variables that have non-NULL + * stack */ + slist_node report_link; /* list link for variables that have the + * GUC_NEEDS_REPORT bit set in status */ + char *last_reported; /* if variable is GUC_REPORT, value last sent + * to client (NULL if not yet sent) */ + char *sourcefile; /* file current setting is from (NULL if not + * set in config file) */ + int sourceline; /* line in source file */ + + /* fields for specific variable types */ + union + { + struct config_bool _bool; + struct config_int _int; + struct config_real _real; + struct config_string _string; + struct config_enum _enum; + }; }; +/* bit values in status field */ +#define GUC_IS_IN_FILE 0x0001 /* found it in config file */ +/* + * Caution: the GUC_IS_IN_FILE bit is transient state for ProcessConfigFile. + * Do not assume that its value represents useful information elsewhere. + */ +#define GUC_PENDING_RESTART 0x0002 /* changed value cannot be applied yet */ +#define GUC_NEEDS_REPORT 0x0004 /* new value must be reported to client */ + + /* constant tables corresponding to enums above and in guc.h */ extern PGDLLIMPORT const char *const config_group_names[]; extern PGDLLIMPORT const char *const config_type_names[]; extern PGDLLIMPORT const char *const GucContext_Names[]; extern PGDLLIMPORT const char *const GucSource_Names[]; -/* data arrays defining all the built-in GUC variables */ -extern PGDLLIMPORT struct config_bool ConfigureNamesBool[]; -extern PGDLLIMPORT struct config_int ConfigureNamesInt[]; -extern PGDLLIMPORT struct config_real ConfigureNamesReal[]; -extern PGDLLIMPORT struct config_string ConfigureNamesString[]; -extern PGDLLIMPORT struct config_enum ConfigureNamesEnum[]; +/* data array defining all the built-in GUC variables */ +extern PGDLLIMPORT struct config_generic ConfigureNames[]; /* lookup GUC variables, returning config_generic pointers */ extern struct config_generic *find_option(const char *name, @@ -319,10 +318,10 @@ extern struct config_generic *find_option(const char *name, extern struct config_generic **get_explain_guc_options(int *num); /* get string value of variable */ -extern char *ShowGUCOption(struct config_generic *record, bool use_units); +extern char *ShowGUCOption(const struct config_generic *record, bool use_units); /* get whether or not the GUC variable is visible to current user */ -extern bool ConfigOptionIsVisible(struct config_generic *conf); +extern bool ConfigOptionIsVisible(const struct config_generic *conf); /* get the current set of variables */ extern struct config_generic **get_guc_variables(int *num_vars); @@ -330,10 +329,10 @@ extern struct config_generic **get_guc_variables(int *num_vars); extern void build_guc_variables(void); /* search in enum options */ -extern const char *config_enum_lookup_by_value(struct config_enum *record, int val); -extern bool config_enum_lookup_by_name(struct config_enum *record, +extern const char *config_enum_lookup_by_value(const struct config_generic *record, int val); +extern bool config_enum_lookup_by_name(const struct config_enum *record, const char *value, int *retval); -extern char *config_enum_get_options(struct config_enum *record, +extern char *config_enum_get_options(const struct config_enum *record, const char *prefix, const char *suffix, const char *separator); diff --git a/src/include/utils/help_config.h b/src/include/utils/help_config.h index 4e58f130054e0..38a70da83d135 100644 --- a/src/include/utils/help_config.h +++ b/src/include/utils/help_config.h @@ -3,7 +3,7 @@ * help_config.h * Interface to the --help-config option of main.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/utils/help_config.h * diff --git a/src/include/utils/hsearch.h b/src/include/utils/hsearch.h index 932cc4f34d90d..896a22a2142ee 100644 --- a/src/include/utils/hsearch.h +++ b/src/include/utils/hsearch.h @@ -4,7 +4,7 @@ * exported definitions for utils/hash/dynahash.c; see notes therein * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/hsearch.h @@ -37,11 +37,10 @@ typedef int (*HashCompareFunc) (const void *key1, const void *key2, typedef void *(*HashCopyFunc) (void *dest, const void *src, Size keysize); /* - * Space allocation function for a hashtable --- designed to match malloc(). - * Note: there is no free function API; can't destroy a hashtable unless you - * use the default allocator. + * Space allocation function for a hashtable. Note: there is no free function + * API; can't destroy a hashtable unless you use the default allocator. */ -typedef void *(*HashAllocFunc) (Size request); +typedef void *(*HashAllocFunc) (Size request, void *alloc_arg); /* * HASHELEMENT is the private part of a hashtable entry. The caller's data @@ -65,12 +64,7 @@ typedef struct HTAB HTAB; typedef struct HASHCTL { /* Used if HASH_PARTITION flag is set: */ - long num_partitions; /* # partitions (must be power of 2) */ - /* Used if HASH_SEGMENT flag is set: */ - long ssize; /* segment size */ - /* Used if HASH_DIRSIZE flag is set: */ - long dsize; /* (initial) directory size */ - long max_dsize; /* limit to dsize if dir size is limited */ + int64 num_partitions; /* # partitions (must be power of 2) */ /* Used if HASH_ELEM flag is set (which is now required): */ Size keysize; /* hash key length in bytes */ Size entrysize; /* total user element size in bytes */ @@ -82,16 +76,17 @@ typedef struct HASHCTL HashCopyFunc keycopy; /* key copying function */ /* Used if HASH_ALLOC flag is set: */ HashAllocFunc alloc; /* memory allocator */ + void *alloc_arg; /* opaque argument passed to allocator */ /* Used if HASH_CONTEXT flag is set: */ MemoryContext hcxt; /* memory context to use for allocations */ - /* Used if HASH_SHARED_MEM flag is set: */ + /* Used if HASH_ATTACH flag is set: */ HASHHDR *hctl; /* location of header in shared mem */ } HASHCTL; /* Flag bits for hash_create; most indicate which parameters are supplied */ #define HASH_PARTITION 0x0001 /* Hashtable is used w/partitioned locking */ -#define HASH_SEGMENT 0x0002 /* Set segment size */ -#define HASH_DIRSIZE 0x0004 /* Set directory size (initial and max) */ +/* 0x0002 is unused */ +/* 0x0004 is unused */ #define HASH_ELEM 0x0008 /* Set keysize and entrysize (now required!) */ #define HASH_STRINGS 0x0010 /* Select support functions for string keys */ #define HASH_BLOBS 0x0020 /* Select support functions for binary keys */ @@ -129,10 +124,10 @@ typedef struct /* * prototypes for functions in dynahash.c */ -extern HTAB *hash_create(const char *tabname, long nelem, +extern HTAB *hash_create(const char *tabname, int64 nelem, const HASHCTL *info, int flags); extern void hash_destroy(HTAB *hashp); -extern void hash_stats(const char *where, HTAB *hashp); +extern void hash_stats(const char *caller, HTAB *hashp); extern void *hash_search(HTAB *hashp, const void *keyPtr, HASHACTION action, bool *foundPtr); extern uint32 get_hash_value(HTAB *hashp, const void *keyPtr); @@ -141,7 +136,7 @@ extern void *hash_search_with_hash_value(HTAB *hashp, const void *keyPtr, bool *foundPtr); extern bool hash_update_hash_key(HTAB *hashp, void *existingEntry, const void *newKeyPtr); -extern long hash_get_num_entries(HTAB *hashp); +extern int64 hash_get_num_entries(HTAB *hashp); extern void hash_seq_init(HASH_SEQ_STATUS *status, HTAB *hashp); extern void hash_seq_init_with_hash_value(HASH_SEQ_STATUS *status, HTAB *hashp, @@ -149,9 +144,7 @@ extern void hash_seq_init_with_hash_value(HASH_SEQ_STATUS *status, extern void *hash_seq_search(HASH_SEQ_STATUS *status); extern void hash_seq_term(HASH_SEQ_STATUS *status); extern void hash_freeze(HTAB *hashp); -extern Size hash_estimate_size(long num_entries, Size entrysize); -extern long hash_select_dirsize(long num_entries); -extern Size hash_get_shared_size(HASHCTL *info, int flags); +extern Size hash_estimate_size(int64 num_entries, Size entrysize); extern void AtEOXact_HashTables(bool isCommit); extern void AtEOSubXact_HashTables(bool isCommit, int nestDepth); diff --git a/src/include/utils/index_selfuncs.h b/src/include/utils/index_selfuncs.h index 6c64db6d456cf..74793a1a19d8a 100644 --- a/src/include/utils/index_selfuncs.h +++ b/src/include/utils/index_selfuncs.h @@ -9,7 +9,7 @@ * If you make it depend on anything besides access/amapi.h, that's likely * a mistake. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/index_selfuncs.h diff --git a/src/include/utils/inet.h b/src/include/utils/inet.h index ace8957f22082..e4ae01ab3b6b5 100644 --- a/src/include/utils/inet.h +++ b/src/include/utils/inet.h @@ -4,7 +4,7 @@ * Declarations for operations on INET datatypes. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/inet.h diff --git a/src/include/utils/injection_point.h b/src/include/utils/injection_point.h index a37958e1835fd..fabd1455c3ccf 100644 --- a/src/include/utils/injection_point.h +++ b/src/include/utils/injection_point.h @@ -2,7 +2,7 @@ * injection_point.h * Definitions related to injection points. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/injection_point.h *------------------------------------------------------------------------- @@ -11,6 +11,19 @@ #ifndef INJECTION_POINT_H #define INJECTION_POINT_H +#include "nodes/pg_list.h" + +/* + * Injection point data, used when retrieving a list of all the attached + * injection points. + */ +typedef struct InjectionPointData +{ + const char *name; + const char *library; + const char *function; +} InjectionPointData; + /* * Injection points require --enable-injection-points. */ @@ -33,9 +46,6 @@ typedef void (*InjectionPointCallback) (const char *name, const void *private_data, void *arg); -extern Size InjectionPointShmemSize(void); -extern void InjectionPointShmemInit(void); - extern void InjectionPointAttach(const char *name, const char *library, const char *function, @@ -47,6 +57,9 @@ extern void InjectionPointCached(const char *name, void *arg); extern bool IsInjectionPointAttached(const char *name); extern bool InjectionPointDetach(const char *name); +/* Get the current set of injection points attached */ +extern List *InjectionPointList(void); + #ifdef EXEC_BACKEND extern PGDLLIMPORT struct InjectionPointsCtl *ActiveInjectionPoints; #endif diff --git a/src/include/utils/inval.h b/src/include/utils/inval.h index 9b871caef622f..735e42f731088 100644 --- a/src/include/utils/inval.h +++ b/src/include/utils/inval.h @@ -4,7 +4,7 @@ * POSTGRES cache invalidation dispatcher definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/inval.h @@ -15,12 +15,32 @@ #define INVAL_H #include "access/htup.h" +#include "catalog/syscache_ids.h" #include "storage/relfilelocator.h" #include "utils/relcache.h" extern PGDLLIMPORT int debug_discard_caches; -typedef void (*SyscacheCallbackFunction) (Datum arg, int cacheid, uint32 hashvalue); +#define MIN_DEBUG_DISCARD_CACHES 0 + +#ifdef DISCARD_CACHES_ENABLED + /* Set default based on older compile-time-only cache clobber macros */ +#if defined(CLOBBER_CACHE_RECURSIVELY) +#define DEFAULT_DEBUG_DISCARD_CACHES 3 +#elif defined(CLOBBER_CACHE_ALWAYS) +#define DEFAULT_DEBUG_DISCARD_CACHES 1 +#else +#define DEFAULT_DEBUG_DISCARD_CACHES 0 +#endif +#define MAX_DEBUG_DISCARD_CACHES 5 +#else /* not DISCARD_CACHES_ENABLED */ +#define DEFAULT_DEBUG_DISCARD_CACHES 0 +#define MAX_DEBUG_DISCARD_CACHES 0 +#endif /* not DISCARD_CACHES_ENABLED */ + + +typedef void (*SyscacheCallbackFunction) (Datum arg, SysCacheIdentifier cacheid, + uint32 hashvalue); typedef void (*RelcacheCallbackFunction) (Datum arg, Oid relid); typedef void (*RelSyncCallbackFunction) (Datum arg, Oid relid); @@ -43,8 +63,7 @@ extern void CacheInvalidateHeapTuple(Relation relation, HeapTuple tuple, HeapTuple newtuple); extern void CacheInvalidateHeapTupleInplace(Relation relation, - HeapTuple tuple, - HeapTuple newtuple); + HeapTuple key_equivalent_tuple); extern void CacheInvalidateCatalog(Oid catalogId); @@ -64,7 +83,7 @@ extern void CacheInvalidateSmgr(RelFileLocatorBackend rlocator); extern void CacheInvalidateRelmap(Oid databaseId); -extern void CacheRegisterSyscacheCallback(int cacheid, +extern void CacheRegisterSyscacheCallback(SysCacheIdentifier cacheid, SyscacheCallbackFunction func, Datum arg); @@ -74,7 +93,7 @@ extern void CacheRegisterRelcacheCallback(RelcacheCallbackFunction func, extern void CacheRegisterRelSyncCallback(RelSyncCallbackFunction func, Datum arg); -extern void CallSyscacheCallbacks(int cacheid, uint32 hashvalue); +extern void CallSyscacheCallbacks(SysCacheIdentifier cacheid, uint32 hashvalue); extern void CallRelSyncCallbacks(Oid relid); diff --git a/src/include/utils/json.h b/src/include/utils/json.h index 49bbda7ac06a5..2f4be40518d6b 100644 --- a/src/include/utils/json.h +++ b/src/include/utils/json.h @@ -3,7 +3,7 @@ * json.h * Declarations for JSON data type support. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/json.h @@ -17,6 +17,8 @@ #include "lib/stringinfo.h" /* functions in json.c */ +extern void composite_to_json(Datum composite, StringInfo result, + bool use_line_feeds); extern void escape_json(StringInfo buf, const char *str); extern void escape_json_with_len(StringInfo buf, const char *str, int len); extern void escape_json_text(StringInfo buf, const text *txt); diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index fecb33b9c671e..ca13efba0fb14 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -3,7 +3,7 @@ * jsonb.h * Declarations for jsonb data type support. * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/utils/jsonb.h * @@ -67,8 +67,10 @@ typedef enum #define JGINFLAG_HASHED 0x10 /* OR'd into flag if value was hashed */ #define JGIN_MAXLENGTH 125 /* max length of text part before hashing */ +/* Forward struct references */ typedef struct JsonbPair JsonbPair; typedef struct JsonbValue JsonbValue; +typedef struct JsonbParseState JsonbParseState; /* * Jsonbs are varlena objects, so must meet the varlena convention that the @@ -315,15 +317,40 @@ struct JsonbPair uint32 order; /* Pair's index in original sequence */ }; -/* Conversion state used when parsing Jsonb from text, or for type coercion */ -typedef struct JsonbParseState +/* + * State used while constructing or manipulating a JsonbValue. + * For example, when parsing Jsonb from text, we construct a JsonbValue + * data structure and then flatten that into the Jsonb on-disk format. + * JsonbValues are also useful in aggregation and type coercion. + * + * Callers providing a JsonbInState must initialize it to zeroes/nulls, + * except for optionally setting outcontext (if that's left NULL, + * CurrentMemoryContext is used) and escontext (if that's left NULL, + * parsing errors are thrown via ereport). + */ +typedef struct JsonbInState { - JsonbValue contVal; - Size size; - struct JsonbParseState *next; + JsonbValue *result; /* The completed value; NULL until complete */ + MemoryContext outcontext; /* The context to build it in, or NULL */ + struct Node *escontext; /* Optional soft-error-reporting context */ + /* Remaining fields should be treated as private to jsonb.c/jsonb_util.c */ + JsonbParseState *parseState; /* Stack of parsing contexts */ + bool unique_keys; /* Check object key uniqueness */ +} JsonbInState; + +/* + * Parsing context for one level of Jsonb array or object nesting. + * The contVal will be part of the constructed JsonbValue tree, + * but the other fields are just transient state. + */ +struct JsonbParseState +{ + JsonbValue contVal; /* An array or object JsonbValue */ + Size size; /* Allocated length of array or object */ + JsonbParseState *next; /* Link to next outer level, if any */ bool unique_keys; /* Check object key uniqueness */ bool skip_nulls; /* Skip null object fields */ -} JsonbParseState; +}; /* * JsonbIterator holds details of the type for each iteration. It also stores a @@ -404,8 +431,8 @@ extern JsonbValue *getKeyJsonValueFromContainer(JsonbContainer *container, JsonbValue *res); extern JsonbValue *getIthJsonbValueFromContainer(JsonbContainer *container, uint32 i); -extern JsonbValue *pushJsonbValue(JsonbParseState **pstate, - JsonbIteratorToken seq, JsonbValue *jbval); +extern void pushJsonbValue(JsonbInState *pstate, + JsonbIteratorToken seq, JsonbValue *jbval); extern JsonbIterator *JsonbIteratorInit(JsonbContainer *container); extern JsonbIteratorToken JsonbIteratorNext(JsonbIterator **it, JsonbValue *val, bool skipNested); @@ -426,9 +453,9 @@ extern char *JsonbUnquote(Jsonb *jb); extern bool JsonbExtractScalar(JsonbContainer *jbc, JsonbValue *res); extern const char *JsonbTypeName(JsonbValue *val); -extern Datum jsonb_set_element(Jsonb *jb, Datum *path, int path_len, +extern Datum jsonb_set_element(Jsonb *jb, const Datum *path, int path_len, JsonbValue *newval); -extern Datum jsonb_get_element(Jsonb *jb, Datum *path, int npath, +extern Datum jsonb_get_element(Jsonb *jb, const Datum *path, int npath, bool *isnull, bool as_text); extern bool to_jsonb_is_immutable(Oid typoid); extern Datum jsonb_build_object_worker(int nargs, const Datum *args, const bool *nulls, diff --git a/src/include/utils/jsonfuncs.h b/src/include/utils/jsonfuncs.h index ebbab6f461615..27713be3aeb33 100644 --- a/src/include/utils/jsonfuncs.h +++ b/src/include/utils/jsonfuncs.h @@ -3,7 +3,7 @@ * jsonfuncs.h * Functions to process JSON data types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/jsonfuncs.h @@ -83,6 +83,8 @@ typedef enum extern void json_categorize_type(Oid typoid, bool is_jsonb, JsonTypeCategory *tcategory, Oid *outfuncoid); +extern void json_check_mutability(Oid typoid, bool is_jsonb, + bool *has_mutable); extern Datum datum_to_json(Datum val, JsonTypeCategory tcategory, Oid outfuncoid); extern Datum datum_to_jsonb(Datum val, JsonTypeCategory tcategory, diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 23a76d233e932..8d27206e242d7 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -3,7 +3,7 @@ * jsonpath.h * Definitions for jsonpath datatype * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/include/utils/jsonpath.h @@ -115,6 +115,14 @@ typedef enum JsonPathItemType jpiTimeTz, /* .time_tz() item method */ jpiTimestamp, /* .timestamp() item method */ jpiTimestampTz, /* .timestamp_tz() item method */ + jpiStrReplace, /* .replace() item method */ + jpiStrLower, /* .lower() item method */ + jpiStrUpper, /* .upper() item method */ + jpiStrLtrim, /* .ltrim() item method */ + jpiStrRtrim, /* .rtrim() item method */ + jpiStrBtrim, /* .btrim() item method */ + jpiStrInitcap, /* .initcap() item method */ + jpiStrSplitPart, /* .split_part() item method */ } JsonPathItemType; /* XQuery regex mode flags for LIKE_REGEX predicate */ diff --git a/src/include/utils/logtape.h b/src/include/utils/logtape.h index 0eedece4741cd..29c3f215f1c22 100644 --- a/src/include/utils/logtape.h +++ b/src/include/utils/logtape.h @@ -5,7 +5,7 @@ * * See logtape.c for explanations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/logtape.h diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index fa7c7e0323b10..2e0258d877e3c 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -3,7 +3,7 @@ * lsyscache.h * Convenience routines for common queries in the system catalog cache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/lsyscache.h @@ -19,7 +19,7 @@ #include "nodes/pg_list.h" /* avoid including subscripting.h here */ -struct SubscriptRoutines; +typedef struct SubscriptRoutines SubscriptRoutines; /* Result list element for get_op_index_interpretation */ typedef struct OpIndexInterpretation @@ -89,6 +89,7 @@ extern bool get_op_hash_functions(Oid opno, extern List *get_op_index_interpretation(Oid opno); extern bool equality_ops_are_compatible(Oid opno1, Oid opno2); extern bool comparison_ops_are_compatible(Oid opno1, Oid opno2); +extern bool op_is_safe_index_member(Oid opno); extern Oid get_opfamily_proc(Oid opfamily, Oid lefttype, Oid righttype, int16 procnum); extern char *get_attname(Oid relid, AttrNumber attnum, bool missing_ok); @@ -104,7 +105,7 @@ extern bool get_collation_isdeterministic(Oid colloid); extern char *get_constraint_name(Oid conoid); extern Oid get_constraint_index(Oid conoid); extern char get_constraint_type(Oid conoid); - +extern char *get_database_name(Oid dbid); extern char *get_language_name(Oid langoid, bool missing_ok); extern Oid get_opclass_family(Oid opclass); extern Oid get_opclass_input_type(Oid opclass); @@ -187,8 +188,8 @@ extern Oid get_typmodin(Oid typid); extern Oid get_typcollation(Oid typid); extern bool type_is_collatable(Oid typid); extern RegProcedure get_typsubscript(Oid typid, Oid *typelemp); -extern const struct SubscriptRoutines *getSubscriptingRoutines(Oid typid, - Oid *typelemp); +extern const SubscriptRoutines *getSubscriptingRoutines(Oid typid, + Oid *typelemp); extern Oid getBaseType(Oid typid); extern Oid getBaseTypeAndTypmod(Oid typid, int32 *typmod); extern int32 get_typavgwidth(Oid typid, int32 typmod); @@ -200,6 +201,7 @@ extern char *get_namespace_name(Oid nspid); extern char *get_namespace_name_or_temp(Oid nspid); extern Oid get_range_subtype(Oid rangeOid); extern Oid get_range_collation(Oid rangeOid); +extern Oid get_range_constructor2(Oid rangeOid); extern Oid get_range_multirange(Oid rangeOid); extern Oid get_multirange_range(Oid multirangeOid); extern Oid get_index_column_opclass(Oid index_oid, int attno); @@ -211,6 +213,9 @@ extern char *get_publication_name(Oid pubid, bool missing_ok); extern Oid get_subscription_oid(const char *subname, bool missing_ok); extern char *get_subscription_name(Oid subid, bool missing_ok); +extern char *get_propgraph_label_name(Oid labeloid); +extern char *get_propgraph_property_name(Oid propoid); + #define type_is_array(typid) (get_element_type(typid) != InvalidOid) /* type_is_array_domain accepts both plain arrays and domains over arrays */ #define type_is_array_domain(typid) (get_base_element_type(typid) != InvalidOid) diff --git a/src/include/utils/memdebug.h b/src/include/utils/memdebug.h index 7309271834b9f..ceb6864d43531 100644 --- a/src/include/utils/memdebug.h +++ b/src/include/utils/memdebug.h @@ -7,7 +7,7 @@ * empty definitions for Valgrind client request macros we use. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/memdebug.h @@ -29,6 +29,7 @@ #define VALGRIND_MEMPOOL_ALLOC(context, addr, size) do {} while (0) #define VALGRIND_MEMPOOL_FREE(context, addr) do {} while (0) #define VALGRIND_MEMPOOL_CHANGE(context, optr, nptr, size) do {} while (0) +#define VALGRIND_MEMPOOL_TRIM(context, addr, size) do {} while (0) #endif diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce272..11ab1717a1626 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -7,7 +7,7 @@ * of the API of the memory management subsystem. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/memutils.h @@ -30,7 +30,7 @@ * * palloc() enforces MaxAllocSize, chosen to correspond to the limiting size * of varlena objects under TOAST. See VARSIZE_4B() and related macros in - * postgres.h. Many datatypes assume that any allocatable size can be + * varatt.h. Many datatypes assume that any allocatable size can be * represented in a varlena header. This limit also permits a caller to use * an "int" variable for an index into or length of an allocation. Callers * careful to avoid these hazards can access the higher limit with @@ -253,7 +253,7 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) */ for (; p < aligned_end; p += sizeof(size_t)) { - if (*(size_t *) p != 0) + if (*(const size_t *) p != 0) return false; } @@ -290,10 +290,10 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) */ for (; p < aligned_end - (sizeof(size_t) * 7); p += sizeof(size_t) * 8) { - if ((((size_t *) p)[0] != 0) | (((size_t *) p)[1] != 0) | - (((size_t *) p)[2] != 0) | (((size_t *) p)[3] != 0) | - (((size_t *) p)[4] != 0) | (((size_t *) p)[5] != 0) | - (((size_t *) p)[6] != 0) | (((size_t *) p)[7] != 0)) + if ((((const size_t *) p)[0] != 0) | (((const size_t *) p)[1] != 0) | + (((const size_t *) p)[2] != 0) | (((const size_t *) p)[3] != 0) | + (((const size_t *) p)[4] != 0) | (((const size_t *) p)[5] != 0) | + (((const size_t *) p)[6] != 0) | (((const size_t *) p)[7] != 0)) return false; } @@ -305,7 +305,7 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) */ for (; p < aligned_end; p += sizeof(size_t)) { - if (*(size_t *) p != 0) + if (*(const size_t *) p != 0) return false; } diff --git a/src/include/utils/memutils_internal.h b/src/include/utils/memutils_internal.h index a6caa6335e34a..475e91b336b7f 100644 --- a/src/include/utils/memutils_internal.h +++ b/src/include/utils/memutils_internal.h @@ -5,7 +5,7 @@ * functions for internal use. * * - * Portions Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2022-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/memutils_internal.h diff --git a/src/include/utils/memutils_memorychunk.h b/src/include/utils/memutils_memorychunk.h index 36f9e4df1cbdb..bda9912182d20 100644 --- a/src/include/utils/memutils_memorychunk.h +++ b/src/include/utils/memutils_memorychunk.h @@ -76,7 +76,7 @@ * PointerGetMemoryChunk * MemoryChunkGetPointer * - * Portions Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2022-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/memutils_memorychunk.h diff --git a/src/include/utils/meson.build b/src/include/utils/meson.build index 78c6b9b0a232a..fd3a2352df5d4 100644 --- a/src/include/utils/meson.build +++ b/src/include/utils/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group wait_event_output = ['wait_event_types.h', 'pgstat_wait_event.c', 'wait_event_funcs_data.c'] wait_event_target = custom_target('wait_event_names', @@ -30,6 +30,13 @@ errcodes = custom_target('errcodes', ) generated_headers += errcodes +guc_tables = custom_target('guc_tables', + input: files('../../backend/utils/misc/guc_parameters.dat'), + output: ['guc_tables.inc.c'], + depend_files: catalog_pm, + command: [perl, files('../../backend/utils/misc/gen_guc_tables.pl'), '@INPUT@', '@OUTPUT@']) +generated_headers += guc_tables + if dtrace.found() probes_tmp = custom_target('probes.h.tmp', input: files('../../backend/utils/probes.d'), @@ -72,8 +79,6 @@ generated_backend_headers += fmgrtab_target[1] # autoconf generates the file there, ensure we get a conflict generated_sources_ac += { - 'src/backend/utils': fmgrtab_output + ['errcodes.h', 'probes.h', 'fmgr-stamp'], + 'src/backend/utils': fmgrtab_output + ['errcodes.h', 'wait_event_types.h', 'probes.h', 'fmgr-stamp'], 'src/include/utils': ['header-stamp'], } - -generated_sources_ac += {'src/backend/utils/activity': ['wait_event_types.h']} diff --git a/src/include/utils/multirangetypes.h b/src/include/utils/multirangetypes.h index 6e336170f53ab..b4dc281a51f63 100644 --- a/src/include/utils/multirangetypes.h +++ b/src/include/utils/multirangetypes.h @@ -4,7 +4,7 @@ * Declarations for Postgres multirange types. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/multirangetypes.h diff --git a/src/include/utils/numeric.h b/src/include/utils/numeric.h index 9e79fc376cbea..b1cf40ed9fdd3 100644 --- a/src/include/utils/numeric.h +++ b/src/include/utils/numeric.h @@ -5,7 +5,7 @@ * * Original coding 1998, Jan Wieck. Heavily revised 2003, Tom Lane. * - * Copyright (c) 1998-2025, PostgreSQL Global Development Group + * Copyright (c) 1998-2026, PostgreSQL Global Development Group * * src/include/utils/numeric.h * @@ -17,6 +17,9 @@ #include "common/pg_prng.h" #include "fmgr.h" +/* forward declaration to avoid node.h include */ +typedef struct Node Node; + /* * Limits on the precision and scale specifiable in a NUMERIC typmod. The * precision is strictly positive, but the scale may be positive or negative. @@ -91,18 +94,13 @@ extern char *numeric_normalize(Numeric num); extern Numeric int64_to_numeric(int64 val); extern Numeric int64_div_fast_to_numeric(int64 val1, int log10val2); -extern Numeric numeric_add_opt_error(Numeric num1, Numeric num2, - bool *have_error); -extern Numeric numeric_sub_opt_error(Numeric num1, Numeric num2, - bool *have_error); -extern Numeric numeric_mul_opt_error(Numeric num1, Numeric num2, - bool *have_error); -extern Numeric numeric_div_opt_error(Numeric num1, Numeric num2, - bool *have_error); -extern Numeric numeric_mod_opt_error(Numeric num1, Numeric num2, - bool *have_error); -extern int32 numeric_int4_opt_error(Numeric num, bool *have_error); -extern int64 numeric_int8_opt_error(Numeric num, bool *have_error); +extern Numeric numeric_add_safe(Numeric num1, Numeric num2, Node *escontext); +extern Numeric numeric_sub_safe(Numeric num1, Numeric num2, Node *escontext); +extern Numeric numeric_mul_safe(Numeric num1, Numeric num2, Node *escontext); +extern Numeric numeric_div_safe(Numeric num1, Numeric num2, Node *escontext); +extern Numeric numeric_mod_safe(Numeric num1, Numeric num2, Node *escontext); +extern int32 numeric_int4_safe(Numeric num, Node *escontext); +extern int64 numeric_int8_safe(Numeric num, Node *escontext); extern Numeric random_numeric(pg_prng_state *state, Numeric rmin, Numeric rmax); diff --git a/src/include/utils/palloc.h b/src/include/utils/palloc.h index e1b42267b22aa..ac53f189a4e4c 100644 --- a/src/include/utils/palloc.h +++ b/src/include/utils/palloc.h @@ -18,7 +18,7 @@ * everything that should be freed. See utils/mmgr/README for more info. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/palloc.h @@ -133,6 +133,8 @@ MemoryContextSwitchTo(MemoryContext context) /* Registration of memory context reset/delete callbacks */ extern void MemoryContextRegisterResetCallback(MemoryContext context, MemoryContextCallback *cb); +extern void MemoryContextUnregisterResetCallback(MemoryContext context, + MemoryContextCallback *cb); /* * These are like standard strdup() except the copied string is diff --git a/src/include/utils/partcache.h b/src/include/utils/partcache.h index 0fb6fc1c808ef..b1608c985ee2d 100644 --- a/src/include/utils/partcache.h +++ b/src/include/utils/partcache.h @@ -2,7 +2,7 @@ * * partcache.h * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/include/utils/partcache.h * diff --git a/src/include/utils/pg_crc.h b/src/include/utils/pg_crc.h index 7900147ea82ff..ccbce997f59c1 100644 --- a/src/include/utils/pg_crc.h +++ b/src/include/utils/pg_crc.h @@ -26,7 +26,7 @@ * * The CRC-32C variant is in port/pg_crc32c.h. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/pg_crc.h diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h index 7b8cbf58d2c43..444350bb803a9 100644 --- a/src/include/utils/pg_locale.h +++ b/src/include/utils/pg_locale.h @@ -4,7 +4,7 @@ * * src/include/utils/pg_locale.h * - * Copyright (c) 2002-2025, PostgreSQL Global Development Group + * Copyright (c) 2002-2026, PostgreSQL Global Development Group * *----------------------------------------------------------------------- */ @@ -12,13 +12,25 @@ #ifndef _PG_LOCALE_ #define _PG_LOCALE_ -#ifdef USE_ICU -#include -#endif +#include "mb/pg_wchar.h" /* use for libc locale names */ #define LOCALE_NAME_BUFLEN 128 +/* + * Maximum number of bytes needed to map a single codepoint. Useful for + * mapping and processing a single input codepoint at a time with a + * statically-allocated buffer. + * + * With full case mapping, an input codepoint may be mapped to as many as + * three output codepoints. See Unicode 16.0.0, section 5.18.2, "Change in + * Length": + * + * https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G29675 + */ +#define UNICODE_CASEMAP_LEN 3 +#define UNICODE_CASEMAP_BUFSZ (UNICODE_CASEMAP_LEN * MAX_MULTIBYTE_CHAR_LEN) + /* GUC settings */ extern PGDLLIMPORT char *locale_messages; extern PGDLLIMPORT char *locale_monetary; @@ -32,9 +44,6 @@ extern PGDLLIMPORT char *localized_full_days[]; extern PGDLLIMPORT char *localized_abbrev_months[]; extern PGDLLIMPORT char *localized_full_months[]; -/* is the databases's LC_CTYPE the C locale? */ -extern PGDLLIMPORT bool database_ctype_is_c; - extern bool check_locale(int category, const char *locale, char **canonname); extern char *pg_perm_setlocale(int category, const char *locale); @@ -77,6 +86,41 @@ struct collate_methods bool strxfrm_is_safe; }; +struct ctype_methods +{ + /* case mapping: LOWER()/INITCAP()/UPPER() */ + size_t (*strlower) (char *dest, size_t destsize, + const char *src, ssize_t srclen, + pg_locale_t locale); + size_t (*strtitle) (char *dest, size_t destsize, + const char *src, ssize_t srclen, + pg_locale_t locale); + size_t (*strupper) (char *dest, size_t destsize, + const char *src, ssize_t srclen, + pg_locale_t locale); + size_t (*strfold) (char *dest, size_t destsize, + const char *src, ssize_t srclen, + pg_locale_t locale); + size_t (*downcase_ident) (char *dest, size_t destsize, + const char *src, ssize_t srclen, + pg_locale_t locale); + + /* required */ + bool (*wc_isdigit) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isalpha) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isalnum) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isupper) (pg_wchar wc, pg_locale_t locale); + bool (*wc_islower) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isgraph) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isprint) (pg_wchar wc, pg_locale_t locale); + bool (*wc_ispunct) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isspace) (pg_wchar wc, pg_locale_t locale); + bool (*wc_isxdigit) (pg_wchar wc, pg_locale_t locale); + bool (*wc_iscased) (pg_wchar wc, pg_locale_t locale); + pg_wchar (*wc_toupper) (pg_wchar wc, pg_locale_t locale); + pg_wchar (*wc_tolower) (pg_wchar wc, pg_locale_t locale); +}; + /* * We use a discriminated union to hold either a locale_t or an ICU collator. * pg_locale_t is occasionally checked for truth, so make it a pointer. @@ -88,20 +132,18 @@ struct collate_methods * "default" collation, there are separate static cache variables, since * consulting the pg_collation catalog doesn't tell us what we need. * - * Note that some code relies on the flags not reporting false negatives - * (that is, saying it's not C when it is). For example, char2wchar() - * could fail if the locale is C, so str_tolower() shouldn't call it - * in that case. + * Note that some code, such as wchar2char(), relies on the flags not + * reporting false negatives (that is, saying it's not C when it is). */ struct pg_locale_struct { - char provider; bool deterministic; bool collate_is_c; bool ctype_is_c; bool is_default; const struct collate_methods *collate; /* NULL if collate_is_c */ + const struct ctype_methods *ctype; /* NULL if ctype_is_c */ union { @@ -115,16 +157,20 @@ struct pg_locale_struct struct { const char *locale; - UCollator *ucol; + struct UCollator *ucol; + struct UCaseMap *ucasemap; + locale_t lt; } icu; #endif - } info; + }; }; extern void init_database_collation(void); +extern pg_locale_t pg_database_locale(void); extern pg_locale_t pg_newlocale_from_collation(Oid collid); extern char *get_collation_actual_version(char collprovider, const char *collcollate); + extern size_t pg_strlower(char *dst, size_t dstsize, const char *src, ssize_t srclen, pg_locale_t locale); @@ -137,6 +183,8 @@ extern size_t pg_strupper(char *dst, size_t dstsize, extern size_t pg_strfold(char *dst, size_t dstsize, const char *src, ssize_t srclen, pg_locale_t locale); +extern size_t pg_downcase_ident(char *dst, size_t dstsize, + const char *src, ssize_t srclen); extern int pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale); extern int pg_strncoll(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2, pg_locale_t locale); @@ -151,16 +199,30 @@ extern size_t pg_strxfrm_prefix(char *dest, const char *src, size_t destsize, extern size_t pg_strnxfrm_prefix(char *dest, size_t destsize, const char *src, ssize_t srclen, pg_locale_t locale); +extern bool pg_iswdigit(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswalpha(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswalnum(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswupper(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswlower(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswgraph(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswprint(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswpunct(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswspace(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswxdigit(pg_wchar wc, pg_locale_t locale); +extern bool pg_iswcased(pg_wchar wc, pg_locale_t locale); +extern pg_wchar pg_towupper(pg_wchar wc, pg_locale_t locale); +extern pg_wchar pg_towlower(pg_wchar wc, pg_locale_t locale); + +extern const char *pg_icu_unicode_version(void); + extern int builtin_locale_encoding(const char *locale); extern const char *builtin_validate_locale(int encoding, const char *locale); extern void icu_validate_locale(const char *loc_str); extern char *icu_language_tag(const char *loc_str, int elevel); extern void report_newlocale_failure(const char *localename); -/* These functions convert from/to libc's wchar_t, *not* pg_wchar_t */ +/* This function converts from libc's wchar_t, *not* pg_wchar */ extern size_t wchar2char(char *to, const wchar_t *from, size_t tolen, - pg_locale_t locale); -extern size_t char2wchar(wchar_t *to, size_t tolen, - const char *from, size_t fromlen, pg_locale_t locale); + locale_t loc); #endif /* _PG_LOCALE_ */ diff --git a/src/include/utils/pg_locale_c.h b/src/include/utils/pg_locale_c.h new file mode 100644 index 0000000000000..cb4921f39fa3b --- /dev/null +++ b/src/include/utils/pg_locale_c.h @@ -0,0 +1,160 @@ +/*----------------------------------------------------------------------- + * + * PostgreSQL locale utilities + * + * src/include/utils/pg_locale_c.h + * + * Copyright (c) 2002-2026, PostgreSQL Global Development Group + * + *----------------------------------------------------------------------- + */ + +#ifndef _PG_LOCALE_C_ +#define _PG_LOCALE_C_ + +/* + * Hard-wired character properties for C locale + */ + +#define PG_ISDIGIT 0x01 +#define PG_ISALPHA 0x02 +#define PG_ISALNUM (PG_ISDIGIT | PG_ISALPHA) +#define PG_ISUPPER 0x04 +#define PG_ISLOWER 0x08 +#define PG_ISGRAPH 0x10 +#define PG_ISPRINT 0x20 +#define PG_ISPUNCT 0x40 +#define PG_ISSPACE 0x80 + +static const unsigned char pg_char_properties[128] = { + /* NUL */ 0, + /* ^A */ 0, + /* ^B */ 0, + /* ^C */ 0, + /* ^D */ 0, + /* ^E */ 0, + /* ^F */ 0, + /* ^G */ 0, + /* ^H */ 0, + /* ^I */ PG_ISSPACE, + /* ^J */ PG_ISSPACE, + /* ^K */ PG_ISSPACE, + /* ^L */ PG_ISSPACE, + /* ^M */ PG_ISSPACE, + /* ^N */ 0, + /* ^O */ 0, + /* ^P */ 0, + /* ^Q */ 0, + /* ^R */ 0, + /* ^S */ 0, + /* ^T */ 0, + /* ^U */ 0, + /* ^V */ 0, + /* ^W */ 0, + /* ^X */ 0, + /* ^Y */ 0, + /* ^Z */ 0, + /* ^[ */ 0, + /* ^\ */ 0, + /* ^] */ 0, + /* ^^ */ 0, + /* ^_ */ 0, + /* */ PG_ISPRINT | PG_ISSPACE, + /* ! */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* " */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* # */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* $ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* % */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* & */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ' */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ( */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ) */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* * */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* + */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* , */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* - */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* . */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* / */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* 0 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 1 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 2 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 3 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 4 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 5 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 6 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 7 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 8 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* 9 */ PG_ISDIGIT | PG_ISGRAPH | PG_ISPRINT, + /* : */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ; */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* < */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* = */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* > */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ? */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* @ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* A */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* B */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* C */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* D */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* E */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* F */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* G */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* H */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* I */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* J */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* K */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* L */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* M */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* N */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* O */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* P */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* Q */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* R */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* S */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* T */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* U */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* V */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* W */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* X */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* Y */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* Z */ PG_ISALPHA | PG_ISUPPER | PG_ISGRAPH | PG_ISPRINT, + /* [ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* \ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ] */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ^ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* _ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ` */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* a */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* b */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* c */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* d */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* e */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* f */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* g */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* h */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* i */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* j */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* k */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* l */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* m */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* n */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* o */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* p */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* q */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* r */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* s */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* t */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* u */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* v */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* w */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* x */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* y */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* z */ PG_ISALPHA | PG_ISLOWER | PG_ISGRAPH | PG_ISPRINT, + /* { */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* | */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* } */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* ~ */ PG_ISGRAPH | PG_ISPRINT | PG_ISPUNCT, + /* DEL */ 0 +}; + +#endif /* _PG_LOCALE_C_ */ diff --git a/src/include/utils/pg_lsn.h b/src/include/utils/pg_lsn.h index ae198af745029..a61a35bab45a7 100644 --- a/src/include/utils/pg_lsn.h +++ b/src/include/utils/pg_lsn.h @@ -5,7 +5,7 @@ * PostgreSQL. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/pg_lsn.h @@ -18,6 +18,9 @@ #include "access/xlogdefs.h" #include "fmgr.h" +/* forward declaration to avoid node.h include */ +typedef struct Node Node; + static inline XLogRecPtr DatumGetLSN(Datum X) { @@ -33,6 +36,6 @@ LSNGetDatum(XLogRecPtr X) #define PG_GETARG_LSN(n) DatumGetLSN(PG_GETARG_DATUM(n)) #define PG_RETURN_LSN(x) return LSNGetDatum(x) -extern XLogRecPtr pg_lsn_in_internal(const char *str, bool *have_error); +extern XLogRecPtr pg_lsn_in_safe(const char *str, Node *escontext); #endif /* PG_LSN_H */ diff --git a/src/include/utils/pg_rusage.h b/src/include/utils/pg_rusage.h index 31fb532f5aea3..8e3ed858ccfb5 100644 --- a/src/include/utils/pg_rusage.h +++ b/src/include/utils/pg_rusage.h @@ -4,7 +4,7 @@ * header file for resource usage measurement support routines * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/pg_rusage.h diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index d5557e6e998cd..fe463faaf6393 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -5,7 +5,7 @@ * only be needed by files implementing statistics support (rather than ones * reporting / querying stats). * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/pgstat_internal.h * ---------- @@ -48,7 +48,13 @@ * PgStatShared_Common as the first element. */ -/* struct for shared statistics hash entry key. */ +/* + * Struct for shared statistics hash entry key. + * + * NB: We assume that this struct contains no padding. Also, 8 bytes + * allocated for the object ID are good enough to ensure the uniqueness + * of the hash key, hence the addition of new fields is not recommended. + */ typedef struct PgStat_HashKey { PgStat_Kind kind; /* statistics entry kind */ @@ -57,6 +63,29 @@ typedef struct PgStat_HashKey * identifier. */ } PgStat_HashKey; +/* + * Tracks if the stats file is being read, written or discarded, used in + * combination with the finish callback. + * + * These states allow plugins that create auxiliary data files to determine + * the current operation and perform any necessary file cleanup. + */ +typedef enum PgStat_StatsFileOp +{ + STATS_WRITE, + STATS_READ, + STATS_DISCARD, +} PgStat_StatsFileOp; + +/* + * PgStat_HashKey should not have any padding. Checking that the structure + * size matches with the sum of each field is a check simple enough to + * enforce this policy. + */ +StaticAssertDecl((sizeof(PgStat_Kind) + sizeof(uint64) + sizeof(Oid)) == + sizeof(PgStat_HashKey), + "PgStat_HashKey should have no padding"); + /* * Shared statistics hash entry. Doesn't itself contain any stats, but points * to them (with ->body). That allows the stats entries themselves to be of @@ -216,6 +245,12 @@ typedef struct PgStat_KindInfo /* Should stats be written to the on-disk stats file? */ bool write_to_file:1; + /* + * Should the number of entries be tracked? For variable-numbered stats, + * to update its PgStat_ShmemControl.entry_counts. + */ + bool track_entry_count:1; + /* * The size of an entry in the shared stats hash table (pointed to by * PgStatShared_HashEntry->body). For fixed-numbered statistics, this is @@ -282,6 +317,39 @@ typedef struct PgStat_KindInfo const PgStatShared_Common *header, NameData *name); bool (*from_serialized_name) (const NameData *name, PgStat_HashKey *key); + /* + * For variable-numbered stats: read or write additional data related to + * an entry, in the stats file or optionally in a different file. + * Optional. + * + * to_serialized_data: write auxiliary data for an entry. + * + * from_serialized_data: read auxiliary data for an entry. Returns true + * on success, false on read error. + * + * "statfile" is a pointer to the on-disk stats file, named + * PGSTAT_STAT_PERMANENT_FILENAME. "key" is the hash key of the entry + * just written or read. "header" is a pointer to the stats data; it may + * be modified only in from_serialized_data to reconstruct an entry. + */ + void (*to_serialized_data) (const PgStat_HashKey *key, + const PgStatShared_Common *header, + FILE *statfile); + bool (*from_serialized_data) (const PgStat_HashKey *key, + PgStatShared_Common *header, + FILE *statfile); + + /* + * For fixed-numbered or variable-numbered statistics. + * + * Perform custom actions when done processing the on-disk stats file + * after all the stats entries have been processed. Optional. + * + * "status" tracks the operation done for the on-disk stats file (read, + * write, discard). + */ + void (*finish) (PgStat_StatsFileOp status); + /* * For fixed-numbered statistics: Initialize shared memory state. * @@ -295,18 +363,11 @@ typedef struct PgStat_KindInfo * * Returns true if some of the stats could not be flushed, due to lock * contention for example. Optional. - */ - bool (*flush_static_cb) (bool nowait); - - /* - * For fixed-numbered or variable-numbered statistics: Check for pending - * stats in need of flush with flush_static_cb, when these do not use - * PgStat_EntryRef->pending. * - * Returns true if there are any stats pending for flush, triggering - * flush_static_cb. Optional. + * "pgstat_report_fixed" needs to be set to trigger the flush of pending + * stats. */ - bool (*have_static_pending_cb) (void); + bool (*flush_static_cb) (bool nowait); /* * For fixed-numbered statistics: Reset All. @@ -403,6 +464,13 @@ typedef struct PgStatShared_IO PgStat_IO stats; } PgStatShared_IO; +typedef struct PgStatShared_Lock +{ + /* lock protects ->stats */ + LWLock lock; + PgStat_Lock stats; +} PgStatShared_Lock; + typedef struct PgStatShared_SLRU { /* lock protects ->stats */ @@ -492,6 +560,16 @@ typedef struct PgStat_ShmemControl */ pg_atomic_uint64 gc_request_count; + /* + * Counters for the number of entries associated to a single stats kind + * that uses variable-numbered objects stored in the shared hash table. + * These counters can be enabled on a per-kind basis, when + * track_entry_count is set. This counter is incremented each time a new + * entry is created (not reused) in the shared hash table, and is + * decremented each time an entry is freed from the shared hash table. + */ + pg_atomic_uint64 entry_counts[PGSTAT_KIND_MAX]; + /* * Stats data for fixed-numbered objects. */ @@ -499,6 +577,7 @@ typedef struct PgStat_ShmemControl PgStatShared_BgWriter bgwriter; PgStatShared_Checkpointer checkpointer; PgStatShared_IO io; + PgStatShared_Lock lock; PgStatShared_SLRU slru; PgStatShared_Wal wal; @@ -531,6 +610,8 @@ typedef struct PgStat_Snapshot PgStat_IO io; + PgStat_Lock lock; + PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS]; PgStat_WalStats wal; @@ -604,7 +685,8 @@ extern PgStat_EntryRef *pgstat_prep_pending_entry(PgStat_Kind kind, Oid dboid, extern PgStat_EntryRef *pgstat_fetch_pending_entry(PgStat_Kind kind, Oid dboid, uint64 objid); -extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid); +extern void *pgstat_fetch_entry(PgStat_Kind kind, Oid dboid, uint64 objid, + bool *may_free); extern void pgstat_snapshot_fixed(PgStat_Kind kind); @@ -625,9 +707,8 @@ extern void pgstat_archiver_snapshot_cb(void); #define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */ #define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL) -extern bool pgstat_flush_backend(bool nowait, bits32 flags); +extern bool pgstat_flush_backend(bool nowait, uint32 flags); extern bool pgstat_backend_flush_cb(bool nowait); -extern bool pgstat_backend_have_pending_cb(void); extern void pgstat_backend_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts); @@ -668,6 +749,7 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time */ extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); +extern void pgstat_function_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts); /* @@ -676,12 +758,19 @@ extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); extern void pgstat_flush_io(bool nowait); -extern bool pgstat_io_have_pending_cb(void); extern bool pgstat_io_flush_cb(bool nowait); extern void pgstat_io_init_shmem_cb(void *stats); extern void pgstat_io_reset_all_cb(TimestampTz ts); extern void pgstat_io_snapshot_cb(void); +/* + * Functions in pgstat_lock.c + */ + +extern bool pgstat_lock_flush_cb(bool nowait); +extern void pgstat_lock_init_shmem_cb(void *stats); +extern void pgstat_lock_reset_all_cb(TimestampTz ts); +extern void pgstat_lock_snapshot_cb(void); /* * Functions in pgstat_relation.c @@ -694,6 +783,7 @@ extern void PostPrepare_PgStat_Relations(PgStat_SubXactStatus *xact_state); extern bool pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); extern void pgstat_relation_delete_pending_cb(PgStat_EntryRef *entry_ref); +extern void pgstat_relation_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts); /* @@ -738,7 +828,6 @@ extern PgStatShared_Common *pgstat_init_entry(PgStat_Kind kind, * Functions in pgstat_slru.c */ -extern bool pgstat_slru_have_pending_cb(void); extern bool pgstat_slru_flush_cb(bool nowait); extern void pgstat_slru_init_shmem_cb(void *stats); extern void pgstat_slru_reset_all_cb(TimestampTz ts); @@ -750,7 +839,6 @@ extern void pgstat_slru_snapshot_cb(void); */ extern void pgstat_wal_init_backend_cb(void); -extern bool pgstat_wal_have_pending_cb(void); extern bool pgstat_wal_flush_cb(bool nowait); extern void pgstat_wal_init_shmem_cb(void *stats); extern void pgstat_wal_reset_all_cb(TimestampTz ts); @@ -778,8 +866,29 @@ extern void pgstat_create_transactional(PgStat_Kind kind, Oid dboid, uint64 obji * Variables in pgstat.c */ +/* + * Track if *any* pending fixed-numbered statistics should be flushed to + * shared memory. + * + * This flag can be switched to true by fixed-numbered statistics to let + * pgstat_report_stat() know if it needs to go through one round of + * reports, calling flush_static_cb for each fixed-numbered statistics + * kind. When this flag is not set, pgstat_report_stat() is able to do + * a fast exit, knowing that there are no pending fixed-numbered statistics. + * + * Statistics callbacks should never reset this flag; pgstat_report_stat() + * is in charge of doing that. + */ +extern PGDLLIMPORT bool pgstat_report_fixed; + +/* Backend-local stats state */ extern PGDLLIMPORT PgStat_LocalState pgStatLocal; +/* Helper functions for reading and writing of on-disk stats file */ +extern void pgstat_write_chunk(FILE *fpout, void *ptr, size_t len); +extern bool pgstat_read_chunk(FILE *fpin, void *ptr, size_t len); +#define pgstat_read_chunk_s(fpin, ptr) pgstat_read_chunk(fpin, ptr, sizeof(*ptr)) +#define pgstat_write_chunk_s(fpout, ptr) pgstat_write_chunk(fpout, ptr, sizeof(*ptr)) /* * Implementation of inline functions declared above. @@ -906,6 +1015,17 @@ pgstat_get_entry_data(PgStat_Kind kind, PgStatShared_Common *entry) return ((char *) (entry)) + off; } +/* + * Returns the number of entries counted for a stats kind. + */ +static inline uint64 +pgstat_get_entry_count(PgStat_Kind kind) +{ + Assert(pgstat_get_kind_info(kind)->track_entry_count); + + return pg_atomic_read_u64(&pgStatLocal.shmem->entry_counts[kind - 1]); +} + /* * Returns a pointer to the shared memory area of custom stats for * fixed-numbered statistics. diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h index f44169fd5a3c7..2d78a02968347 100644 --- a/src/include/utils/pgstat_kind.h +++ b/src/include/utils/pgstat_kind.h @@ -5,7 +5,7 @@ * cumulative statistics system. Can be included in backend or * frontend code. * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/pgstat_kind.h * ---------- @@ -18,7 +18,7 @@ /* Range of IDs allowed, for built-in and custom kinds */ #define PGSTAT_KIND_MIN 1 /* Minimum ID allowed */ -#define PGSTAT_KIND_MAX 256 /* Maximum ID allowed */ +#define PGSTAT_KIND_MAX 32 /* Maximum ID allowed */ /* use 0 for INVALID, to catch zero-initialized data */ #define PGSTAT_KIND_INVALID 0 @@ -36,8 +36,9 @@ #define PGSTAT_KIND_BGWRITER 8 #define PGSTAT_KIND_CHECKPOINTER 9 #define PGSTAT_KIND_IO 10 -#define PGSTAT_KIND_SLRU 11 -#define PGSTAT_KIND_WAL 12 +#define PGSTAT_KIND_LOCK 11 +#define PGSTAT_KIND_SLRU 12 +#define PGSTAT_KIND_WAL 13 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE #define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL @@ -46,7 +47,7 @@ /* Custom stats kinds */ /* Range of IDs allowed for custom stats kinds */ -#define PGSTAT_KIND_CUSTOM_MIN 128 +#define PGSTAT_KIND_CUSTOM_MIN 24 #define PGSTAT_KIND_CUSTOM_MAX PGSTAT_KIND_MAX #define PGSTAT_KIND_CUSTOM_SIZE (PGSTAT_KIND_CUSTOM_MAX - PGSTAT_KIND_CUSTOM_MIN + 1) @@ -55,7 +56,7 @@ * development and have not reserved their own unique kind ID yet. See: * https://wiki.postgresql.org/wiki/CustomCumulativeStats */ -#define PGSTAT_KIND_EXPERIMENTAL 128 +#define PGSTAT_KIND_EXPERIMENTAL 24 static inline bool pgstat_is_kind_builtin(PgStat_Kind kind) diff --git a/src/include/utils/pidfile.h b/src/include/utils/pidfile.h index c8f248fd92493..82b4c1990c965 100644 --- a/src/include/utils/pidfile.h +++ b/src/include/utils/pidfile.h @@ -3,7 +3,7 @@ * pidfile.h * Declarations describing the data directory lock file (postmaster.pid) * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/pidfile.h diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index 1baa6d50bfd7a..7a4a85c803835 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -5,7 +5,7 @@ * * See plancache.c for comments. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/plancache.h @@ -24,8 +24,8 @@ /* Forward declarations, to avoid including parsenodes.h here */ -struct Query; -struct RawStmt; +typedef struct Query Query; +typedef struct RawStmt RawStmt; /* possible values for plan_cache_mode */ typedef enum @@ -105,8 +105,8 @@ typedef void (*PostRewriteHook) (List *querytree_list, void *arg); typedef struct CachedPlanSource { int magic; /* should equal CACHEDPLANSOURCE_MAGIC */ - struct RawStmt *raw_parse_tree; /* output of raw_parser(), or NULL */ - struct Query *analyzed_parse_tree; /* analyzed parse tree, or NULL */ + RawStmt *raw_parse_tree; /* output of raw_parser(), or NULL */ + Query *analyzed_parse_tree; /* analyzed parse tree, or NULL */ const char *query_string; /* source text of query */ CommandTag commandTag; /* command tag for query */ Oid *param_types; /* array of parameter type OIDs, or NULL */ @@ -202,13 +202,13 @@ extern void ResetPlanCache(void); extern void ReleaseAllPlanCacheRefsInOwner(ResourceOwner owner); -extern CachedPlanSource *CreateCachedPlan(struct RawStmt *raw_parse_tree, +extern CachedPlanSource *CreateCachedPlan(const RawStmt *raw_parse_tree, const char *query_string, CommandTag commandTag); -extern CachedPlanSource *CreateCachedPlanForQuery(struct Query *analyzed_parse_tree, +extern CachedPlanSource *CreateCachedPlanForQuery(Query *analyzed_parse_tree, const char *query_string, CommandTag commandTag); -extern CachedPlanSource *CreateOneShotCachedPlan(struct RawStmt *raw_parse_tree, +extern CachedPlanSource *CreateOneShotCachedPlan(RawStmt *raw_parse_tree, const char *query_string, CommandTag commandTag); extern void CompleteCachedPlan(CachedPlanSource *plansource, diff --git a/src/include/utils/portal.h b/src/include/utils/portal.h index 0b62143af8bcf..a7bedb12c1817 100644 --- a/src/include/utils/portal.h +++ b/src/include/utils/portal.h @@ -36,7 +36,7 @@ * to look like NO SCROLL cursors. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/portal.h @@ -208,7 +208,7 @@ typedef struct PortalData * PortalIsValid * True iff portal is valid. */ -#define PortalIsValid(p) PointerIsValid(p) +#define PortalIsValid(p) ((p) != NULL) /* Prototypes for functions in utils/mmgr/portalmem.c */ diff --git a/src/include/utils/queryenvironment.h b/src/include/utils/queryenvironment.h index d44b20a778cea..b605a987fbd8a 100644 --- a/src/include/utils/queryenvironment.h +++ b/src/include/utils/queryenvironment.h @@ -4,7 +4,7 @@ * Access to functions to mutate the query environment and retrieve the * actual data related to entries (if any). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/queryenvironment.h diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h index 50adb3c8c1391..7b2e1efc3c4e3 100644 --- a/src/include/utils/rangetypes.h +++ b/src/include/utils/rangetypes.h @@ -4,7 +4,7 @@ * Declarations for Postgres range types. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/rangetypes.h @@ -164,5 +164,7 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache); extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2, RangeType **output1, RangeType **output2); +extern void range_minus_multi_internal(TypeCacheEntry *typcache, RangeType *r1, + RangeType *r2, RangeType **outputs, int *outputn); #endif /* RANGETYPES_H */ diff --git a/src/include/utils/regproc.h b/src/include/utils/regproc.h index 81bf59fba54b7..fd7e391519e94 100644 --- a/src/include/utils/regproc.h +++ b/src/include/utils/regproc.h @@ -3,7 +3,7 @@ * regproc.h * Functions for the built-in types regproc, regclass, regtype, etc. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/regproc.h @@ -18,12 +18,12 @@ /* Control flags for format_procedure_extended */ #define FORMAT_PROC_INVALID_AS_NULL 0x01 /* NULL if undefined */ #define FORMAT_PROC_FORCE_QUALIFY 0x02 /* force qualification */ -extern char *format_procedure_extended(Oid procedure_oid, bits16 flags); +extern char *format_procedure_extended(Oid procedure_oid, uint16 flags); /* Control flags for format_operator_extended */ #define FORMAT_OPERATOR_INVALID_AS_NULL 0x01 /* NULL if undefined */ #define FORMAT_OPERATOR_FORCE_QUALIFY 0x02 /* force qualification */ -extern char *format_operator_extended(Oid operator_oid, bits16 flags); +extern char *format_operator_extended(Oid operator_oid, uint16 flags); extern List *stringToQualifiedNameList(const char *string, Node *escontext); extern char *format_procedure(Oid procedure_oid); diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index b552359915f19..cd1e92f230258 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -4,7 +4,7 @@ * POSTGRES relation descriptor (a/k/a relcache entry) definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/rel.h @@ -203,7 +203,7 @@ typedef struct RelationData */ MemoryContext rd_indexcxt; /* private memory cxt for this stuff */ /* use "struct" here to avoid needing to include amapi.h: */ - struct IndexAmRoutine *rd_indam; /* index AM's API struct */ + const struct IndexAmRoutine *rd_indam; /* index AM's API struct */ Oid *rd_opfamily; /* OIDs of op families for each index col */ Oid *rd_opcintype; /* OIDs of opclass declared input data types */ RegProcedure *rd_support; /* OIDs of support procedures */ @@ -311,6 +311,8 @@ typedef struct ForeignKeyCacheInfo typedef struct AutoVacOpts { bool enabled; + + int autovacuum_parallel_workers; int vacuum_threshold; int vacuum_max_threshold; int vacuum_ins_threshold; @@ -322,7 +324,8 @@ typedef struct AutoVacOpts int multixact_freeze_min_age; int multixact_freeze_max_age; int multixact_freeze_table_age; - int log_min_duration; + int log_vacuum_min_duration; + int log_analyze_min_duration; float8 vacuum_cost_delay; float8 vacuum_scale_factor; float8 vacuum_ins_scale_factor; @@ -346,8 +349,7 @@ typedef struct StdRdOptions bool user_catalog_table; /* use as an additional catalog relation */ int parallel_workers; /* max number of parallel workers */ StdRdOptIndexCleanup vacuum_index_cleanup; /* controls index vacuuming */ - bool vacuum_truncate; /* enables vacuum to truncate a relation */ - bool vacuum_truncate_set; /* whether vacuum_truncate is set */ + pg_ternary vacuum_truncate; /* enables vacuum to truncate a relation */ /* * Fraction of pages in a relation that vacuum can eagerly scan and fail @@ -486,9 +488,7 @@ typedef struct ViewOptions * RelationIsValid * True iff relation descriptor is valid. */ -#define RelationIsValid(relation) PointerIsValid(relation) - -#define InvalidRelation ((Relation) NULL) +#define RelationIsValid(relation) ((relation) != NULL) /* * RelationHasReferenceCountZero diff --git a/src/include/utils/relcache.h b/src/include/utils/relcache.h index 3561c6bef0bfc..2700224939a72 100644 --- a/src/include/utils/relcache.h +++ b/src/include/utils/relcache.h @@ -4,7 +4,7 @@ * Relation descriptor cache definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/relcache.h diff --git a/src/include/utils/relfilenumbermap.h b/src/include/utils/relfilenumbermap.h index 86fadefe499a7..3eddbaf7cd347 100644 --- a/src/include/utils/relfilenumbermap.h +++ b/src/include/utils/relfilenumbermap.h @@ -3,7 +3,7 @@ * relfilenumbermap.h * relfilenumber to oid mapping cache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/relfilenumbermap.h diff --git a/src/include/utils/relmapper.h b/src/include/utils/relmapper.h index ae1a7567d948f..aa9226edac366 100644 --- a/src/include/utils/relmapper.h +++ b/src/include/utils/relmapper.h @@ -4,7 +4,7 @@ * Catalog-to-filenumber mapping * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/relmapper.h diff --git a/src/include/utils/relptr.h b/src/include/utils/relptr.h index ea340fee6576a..94975f2f237ba 100644 --- a/src/include/utils/relptr.h +++ b/src/include/utils/relptr.h @@ -3,7 +3,7 @@ * relptr.h * This file contains basic declarations for relative pointers. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/relptr.h @@ -38,18 +38,14 @@ #define relptr_declare(type, relptrtype) \ typedef relptr(type) relptrtype -#ifdef HAVE__BUILTIN_TYPES_COMPATIBLE_P +#ifdef HAVE_TYPEOF #define relptr_access(base, rp) \ - (AssertVariableIsOfTypeMacro(base, char *), \ - (__typeof__((rp).relptr_type)) ((rp).relptr_off == 0 ? NULL : \ + (StaticAssertVariableIsOfTypeMacro(base, char *), \ + (typeof((rp).relptr_type)) ((rp).relptr_off == 0 ? NULL : \ (base) + (rp).relptr_off - 1)) #else -/* - * If we don't have __builtin_types_compatible_p, assume we might not have - * __typeof__ either. - */ #define relptr_access(base, rp) \ - (AssertVariableIsOfTypeMacro(base, char *), \ + (StaticAssertVariableIsOfTypeMacro(base, char *), \ (void *) ((rp).relptr_off == 0 ? NULL : (base) + (rp).relptr_off - 1)) #endif @@ -72,18 +68,14 @@ relptr_store_eval(char *base, char *val) } } -#ifdef HAVE__BUILTIN_TYPES_COMPATIBLE_P +#ifdef HAVE_TYPEOF #define relptr_store(base, rp, val) \ - (AssertVariableIsOfTypeMacro(base, char *), \ - AssertVariableIsOfTypeMacro(val, __typeof__((rp).relptr_type)), \ + (StaticAssertVariableIsOfTypeMacro(base, char *), \ + StaticAssertVariableIsOfTypeMacro(val, typeof((rp).relptr_type)), \ (rp).relptr_off = relptr_store_eval((base), (char *) (val))) #else -/* - * If we don't have __builtin_types_compatible_p, assume we might not have - * __typeof__ either. - */ #define relptr_store(base, rp, val) \ - (AssertVariableIsOfTypeMacro(base, char *), \ + (StaticAssertVariableIsOfTypeMacro(base, char *), \ (rp).relptr_off = relptr_store_eval((base), (char *) (val))) #endif diff --git a/src/include/utils/reltrigger.h b/src/include/utils/reltrigger.h index 5fda290eb40ef..7ec9c88bc03db 100644 --- a/src/include/utils/reltrigger.h +++ b/src/include/utils/reltrigger.h @@ -4,7 +4,7 @@ * POSTGRES relation trigger definitions. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/reltrigger.h diff --git a/src/include/utils/resowner.h b/src/include/utils/resowner.h index aede4bfc820a3..eb6033b4fdb65 100644 --- a/src/include/utils/resowner.h +++ b/src/include/utils/resowner.h @@ -9,7 +9,7 @@ * See utils/resowner/README for more info. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/resowner.h diff --git a/src/include/utils/rls.h b/src/include/utils/rls.h index d18ac368b4d8f..3520b13daa75a 100644 --- a/src/include/utils/rls.h +++ b/src/include/utils/rls.h @@ -4,7 +4,7 @@ * Header file for Row Level Security (RLS) utility commands to be used * with the rowsecurity feature. * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/include/utils/rls.h * diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h index 5f2ea2e4d0ebf..25c05e2f649f7 100644 --- a/src/include/utils/ruleutils.h +++ b/src/include/utils/ruleutils.h @@ -3,7 +3,7 @@ * ruleutils.h * Declarations for ruleutils.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/ruleutils.h @@ -17,8 +17,8 @@ #include "nodes/parsenodes.h" #include "nodes/pg_list.h" -struct Plan; /* avoid including plannodes.h here */ -struct PlannedStmt; +typedef struct Plan Plan; /* avoid including plannodes.h here */ +typedef struct PlannedStmt PlannedStmt; /* Flags for pg_get_indexdef_columns_extended() */ #define RULE_INDEXDEF_PRETTY 0x01 @@ -27,7 +27,7 @@ struct PlannedStmt; extern char *pg_get_indexdef_string(Oid indexrelid); extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty); extern char *pg_get_indexdef_columns_extended(Oid indexrelid, - bits16 flags); + uint16 flags); extern char *pg_get_querydef(Query *query, bool pretty); extern char *pg_get_partkeydef_columns(Oid relid, bool pretty); @@ -37,10 +37,10 @@ extern char *pg_get_constraintdef_command(Oid constraintId); extern char *deparse_expression(Node *expr, List *dpcontext, bool forceprefix, bool showimplicit); extern List *deparse_context_for(const char *aliasname, Oid relid); -extern List *deparse_context_for_plan_tree(struct PlannedStmt *pstmt, +extern List *deparse_context_for_plan_tree(PlannedStmt *pstmt, List *rtable_names); extern List *set_deparse_context_plan(List *dpcontext, - struct Plan *plan, List *ancestors); + Plan *plan, List *ancestors); extern List *select_rtable_names_for_explain(List *rtable, Bitmapset *rels_used); extern char *get_window_frame_options_for_explain(int frameOptions, @@ -51,6 +51,7 @@ extern char *get_window_frame_options_for_explain(int frameOptions, extern char *generate_collation_name(Oid collid); extern char *generate_opclass_name(Oid opclass); extern char *get_range_partbound_string(List *bound_datums); +extern void get_reloptions(StringInfo buf, Datum reloptions); extern char *pg_get_statisticsobjdef_string(Oid statextid); diff --git a/src/include/utils/sampling.h b/src/include/utils/sampling.h index 02c550aabd568..41ed4c0c90652 100644 --- a/src/include/utils/sampling.h +++ b/src/include/utils/sampling.h @@ -3,7 +3,7 @@ * sampling.h * definitions for sampling functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/sampling.h diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h index 013049b3098ac..8d9fff95a1915 100644 --- a/src/include/utils/selfuncs.h +++ b/src/include/utils/selfuncs.h @@ -5,7 +5,7 @@ * infrastructure for selectivity and cost estimation. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/selfuncs.h @@ -96,7 +96,8 @@ typedef struct VariableStatData int32 atttypmod; /* actual typmod (after stripping relabel) */ bool isunique; /* matches unique index, DISTINCT or GROUP-BY * clause */ - bool acl_ok; /* result of ACL check on table or column */ + bool acl_ok; /* true if user has SELECT privilege on all + * rows from the table or column */ } VariableStatData; #define ReleaseVariableStats(vardata) \ @@ -121,6 +122,12 @@ typedef struct VariableStatData * Similarly, they can set num_sa_scans to some value >= 1 for an index AM * that doesn't necessarily perform exactly one primitive index scan per * distinct combination of ScalarArrayOp array elements. + * Similarly, they can set numNonLeafPages to some value >= 1 if they know + * how many index pages are not leaf pages. (It's always good to count + * totally non-data-bearing pages such as metapages here, since accounting + * for the metapage can move cost estimates for a small index significantly. + * But upper pages in large indexes may be few enough relative to leaf pages + * that it's not worth trying to count them.) */ typedef struct { @@ -135,6 +142,7 @@ typedef struct double numIndexTuples; /* number of leaf tuples visited */ double spc_random_page_cost; /* relevant random_page_cost value */ double num_sa_scans; /* # indexscans from ScalarArrayOpExprs */ + BlockNumber numNonLeafPages; /* # of index pages that are not leaves */ } GenericCosts; /* Hooks for plugins to get control when we ask for stats */ @@ -153,6 +161,7 @@ extern PGDLLIMPORT get_index_stats_hook_type get_index_stats_hook; extern void examine_variable(PlannerInfo *root, Node *node, int varRelid, VariableStatData *vardata); +extern bool all_rows_selectable(PlannerInfo *root, Index varno, Bitmapset *varattnos); extern bool statistic_proc_security_check(VariableStatData *vardata, Oid func_oid); extern bool get_restriction_variable(PlannerInfo *root, List *args, int varRelid, diff --git a/src/include/utils/sharedtuplestore.h b/src/include/utils/sharedtuplestore.h index 9cc6514214773..48bfea4d583de 100644 --- a/src/include/utils/sharedtuplestore.h +++ b/src/include/utils/sharedtuplestore.h @@ -3,7 +3,7 @@ * sharedtuplestore.h * Simple mechanism for sharing tuples between backends. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/sharedtuplestore.h diff --git a/src/include/utils/skipsupport.h b/src/include/utils/skipsupport.h index bc51847cf617a..818bbc4c06165 100644 --- a/src/include/utils/skipsupport.h +++ b/src/include/utils/skipsupport.h @@ -35,7 +35,7 @@ * value is the next possible indexable value never (or hardly ever) works out. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/skipsupport.h diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h index d346be716423e..1c55009639373 100644 --- a/src/include/utils/snapmgr.h +++ b/src/include/utils/snapmgr.h @@ -3,7 +3,7 @@ * snapmgr.h * POSTGRES snapshot manager * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/snapmgr.h @@ -51,10 +51,28 @@ extern PGDLLIMPORT SnapshotData SnapshotToastData; ((snapshotdata).snapshot_type = SNAPSHOT_NON_VACUUMABLE, \ (snapshotdata).vistest = (vistestp)) -/* This macro encodes the knowledge of which snapshots are MVCC-safe */ +/* + * Is the snapshot implemented as an MVCC snapshot (i.e. it uses + * SNAPSHOT_MVCC)? If so, there will be at most one visible tuple in a chain + * of updated tuples, and each visible tuple will be seen exactly once. + */ #define IsMVCCSnapshot(snapshot) \ - ((snapshot)->snapshot_type == SNAPSHOT_MVCC || \ - (snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC) + ((snapshot)->snapshot_type == SNAPSHOT_MVCC) + +/* + * Special kind of MVCC snapshot, used during logical decoding. The visibility + * is checked from the perspective of an already committed transaction that is + * being decoded. + */ +#define IsHistoricMVCCSnapshot(snapshot) \ + ((snapshot)->snapshot_type == SNAPSHOT_HISTORIC_MVCC) + +/* + * Is the snapshot either an MVCC snapshot or has equivalent visibility + * semantics (see IsMVCCSnapshot())? + */ +#define IsMVCCLikeSnapshot(snapshot) \ + (IsMVCCSnapshot(snapshot) || IsHistoricMVCCSnapshot(snapshot)) extern Snapshot GetTransactionSnapshot(void); extern Snapshot GetLatestSnapshot(void); @@ -97,10 +115,17 @@ extern char *ExportSnapshot(Snapshot snapshot); */ typedef struct GlobalVisState GlobalVisState; extern GlobalVisState *GlobalVisTestFor(Relation rel); -extern bool GlobalVisTestIsRemovableXid(GlobalVisState *state, TransactionId xid); -extern bool GlobalVisTestIsRemovableFullXid(GlobalVisState *state, FullTransactionId fxid); +extern bool GlobalVisTestIsRemovableXid(GlobalVisState *state, + TransactionId xid, + bool allow_update); +extern bool GlobalVisTestIsRemovableFullXid(GlobalVisState *state, + FullTransactionId fxid, + bool allow_update); extern bool GlobalVisCheckRemovableXid(Relation rel, TransactionId xid); extern bool GlobalVisCheckRemovableFullXid(Relation rel, FullTransactionId fxid); +extern bool GlobalVisTestXidConsideredRunning(GlobalVisState *state, + TransactionId xid, + bool allow_update); /* * Utility functions for implementing visibility routines in table AMs. @@ -117,6 +142,7 @@ extern bool HistoricSnapshotActive(void); extern Size EstimateSnapshotSpace(Snapshot snapshot); extern void SerializeSnapshot(Snapshot snapshot, char *start_address); extern Snapshot RestoreSnapshot(char *start_address); -extern void RestoreTransactionSnapshot(Snapshot snapshot, void *source_pgproc); +struct PGPROC; +extern void RestoreTransactionSnapshot(Snapshot snapshot, struct PGPROC *source_pgproc); #endif /* SNAPMGR_H */ diff --git a/src/include/utils/snapshot.h b/src/include/utils/snapshot.h index 0e546ec14974f..9766aabcad4bf 100644 --- a/src/include/utils/snapshot.h +++ b/src/include/utils/snapshot.h @@ -3,7 +3,7 @@ * snapshot.h * POSTGRES snapshot definition * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/snapshot.h diff --git a/src/include/utils/sortsupport.h b/src/include/utils/sortsupport.h index b7abaf7802de0..a8f8f9f026a59 100644 --- a/src/include/utils/sortsupport.h +++ b/src/include/utils/sortsupport.h @@ -42,7 +42,7 @@ * function for such cases, but probably not any other acceleration method. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/sortsupport.h @@ -229,109 +229,6 @@ ApplySortComparator(Datum datum1, bool isNull1, return compare; } -static inline int -ApplyUnsignedSortComparator(Datum datum1, bool isNull1, - Datum datum2, bool isNull2, - SortSupport ssup) -{ - int compare; - - if (isNull1) - { - if (isNull2) - compare = 0; /* NULL "=" NULL */ - else if (ssup->ssup_nulls_first) - compare = -1; /* NULL "<" NOT_NULL */ - else - compare = 1; /* NULL ">" NOT_NULL */ - } - else if (isNull2) - { - if (ssup->ssup_nulls_first) - compare = 1; /* NOT_NULL ">" NULL */ - else - compare = -1; /* NOT_NULL "<" NULL */ - } - else - { - compare = datum1 < datum2 ? -1 : datum1 > datum2 ? 1 : 0; - if (ssup->ssup_reverse) - INVERT_COMPARE_RESULT(compare); - } - - return compare; -} - -#if SIZEOF_DATUM >= 8 -static inline int -ApplySignedSortComparator(Datum datum1, bool isNull1, - Datum datum2, bool isNull2, - SortSupport ssup) -{ - int compare; - - if (isNull1) - { - if (isNull2) - compare = 0; /* NULL "=" NULL */ - else if (ssup->ssup_nulls_first) - compare = -1; /* NULL "<" NOT_NULL */ - else - compare = 1; /* NULL ">" NOT_NULL */ - } - else if (isNull2) - { - if (ssup->ssup_nulls_first) - compare = 1; /* NOT_NULL ">" NULL */ - else - compare = -1; /* NOT_NULL "<" NULL */ - } - else - { - compare = DatumGetInt64(datum1) < DatumGetInt64(datum2) ? -1 : - DatumGetInt64(datum1) > DatumGetInt64(datum2) ? 1 : 0; - if (ssup->ssup_reverse) - INVERT_COMPARE_RESULT(compare); - } - - return compare; -} -#endif - -static inline int -ApplyInt32SortComparator(Datum datum1, bool isNull1, - Datum datum2, bool isNull2, - SortSupport ssup) -{ - int compare; - - if (isNull1) - { - if (isNull2) - compare = 0; /* NULL "=" NULL */ - else if (ssup->ssup_nulls_first) - compare = -1; /* NULL "<" NOT_NULL */ - else - compare = 1; /* NULL ">" NOT_NULL */ - } - else if (isNull2) - { - if (ssup->ssup_nulls_first) - compare = 1; /* NOT_NULL ">" NULL */ - else - compare = -1; /* NOT_NULL "<" NULL */ - } - else - { - compare = DatumGetInt32(datum1) < DatumGetInt32(datum2) ? -1 : - DatumGetInt32(datum1) > DatumGetInt32(datum2) ? 1 : 0; - if (ssup->ssup_reverse) - INVERT_COMPARE_RESULT(compare); - } - - return compare; -} - /* * Apply a sort comparator function and return a 3-way comparison using full, * authoritative comparator. This takes care of handling reverse-sort and @@ -376,9 +273,7 @@ ApplySortAbbrevFullComparator(Datum datum1, bool isNull1, * are eligible for faster sorting. */ extern int ssup_datum_unsigned_cmp(Datum x, Datum y, SortSupport ssup); -#if SIZEOF_DATUM >= 8 extern int ssup_datum_signed_cmp(Datum x, Datum y, SortSupport ssup); -#endif extern int ssup_datum_int32_cmp(Datum x, Datum y, SortSupport ssup); /* Other functions in utils/sort/sortsupport.c */ diff --git a/src/include/utils/spccache.h b/src/include/utils/spccache.h index d7edd79b18d10..1c4cfcb32517d 100644 --- a/src/include/utils/spccache.h +++ b/src/include/utils/spccache.h @@ -3,7 +3,7 @@ * spccache.h * Tablespace cache. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/spccache.h diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index 3039713259ecd..81e5933708e1f 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -6,7 +6,7 @@ * See also lsyscache.h, which provides convenience routines for * common cache-lookup operations. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/syscache.h @@ -25,35 +25,35 @@ extern void InitCatalogCache(void); extern void InitCatalogCachePhase2(void); -extern HeapTuple SearchSysCache(int cacheId, +extern HeapTuple SearchSysCache(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4); /* * The use of argument specific numbers is encouraged. They're faster, and * insulates the caller from changes in the maximum number of keys. */ -extern HeapTuple SearchSysCache1(int cacheId, +extern HeapTuple SearchSysCache1(SysCacheIdentifier cacheId, Datum key1); -extern HeapTuple SearchSysCache2(int cacheId, +extern HeapTuple SearchSysCache2(SysCacheIdentifier cacheId, Datum key1, Datum key2); -extern HeapTuple SearchSysCache3(int cacheId, +extern HeapTuple SearchSysCache3(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3); -extern HeapTuple SearchSysCache4(int cacheId, +extern HeapTuple SearchSysCache4(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4); extern void ReleaseSysCache(HeapTuple tuple); -extern HeapTuple SearchSysCacheLocked1(int cacheId, +extern HeapTuple SearchSysCacheLocked1(SysCacheIdentifier cacheId, Datum key1); /* convenience routines */ -extern HeapTuple SearchSysCacheCopy(int cacheId, +extern HeapTuple SearchSysCacheCopy(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4); -extern HeapTuple SearchSysCacheLockedCopy1(int cacheId, +extern HeapTuple SearchSysCacheLockedCopy1(SysCacheIdentifier cacheId, Datum key1); -extern bool SearchSysCacheExists(int cacheId, +extern bool SearchSysCacheExists(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4); -extern Oid GetSysCacheOid(int cacheId, AttrNumber oidcol, +extern Oid GetSysCacheOid(SysCacheIdentifier cacheId, AttrNumber oidcol, Datum key1, Datum key2, Datum key3, Datum key4); extern HeapTuple SearchSysCacheAttName(Oid relid, const char *attname); @@ -63,21 +63,21 @@ extern bool SearchSysCacheExistsAttName(Oid relid, const char *attname); extern HeapTuple SearchSysCacheAttNum(Oid relid, int16 attnum); extern HeapTuple SearchSysCacheCopyAttNum(Oid relid, int16 attnum); -extern Datum SysCacheGetAttr(int cacheId, HeapTuple tup, +extern Datum SysCacheGetAttr(SysCacheIdentifier cacheId, HeapTuple tup, AttrNumber attributeNumber, bool *isNull); -extern Datum SysCacheGetAttrNotNull(int cacheId, HeapTuple tup, +extern Datum SysCacheGetAttrNotNull(SysCacheIdentifier cacheId, HeapTuple tup, AttrNumber attributeNumber); -extern uint32 GetSysCacheHashValue(int cacheId, +extern uint32 GetSysCacheHashValue(SysCacheIdentifier cacheId, Datum key1, Datum key2, Datum key3, Datum key4); /* list-search interface. Users of this must import catcache.h too */ struct catclist; -extern struct catclist *SearchSysCacheList(int cacheId, int nkeys, +extern struct catclist *SearchSysCacheList(SysCacheIdentifier cacheId, int nkeys, Datum key1, Datum key2, Datum key3); -extern void SysCacheInvalidate(int cacheId, uint32 hashValue); +extern void SysCacheInvalidate(SysCacheIdentifier cacheId, uint32 hashValue); extern bool RelationInvalidatesSnapshotsOnly(Oid relid); extern bool RelationHasSysCache(Oid relid); diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h index 7b19beafdc950..0965b590b3401 100644 --- a/src/include/utils/timeout.h +++ b/src/include/utils/timeout.h @@ -4,7 +4,7 @@ * Routines to multiplex SIGALRM interrupts for multiple timeout reasons. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/timeout.h diff --git a/src/include/utils/timestamp.h b/src/include/utils/timestamp.h index 8c205859c3be5..a355709936f0a 100644 --- a/src/include/utils/timestamp.h +++ b/src/include/utils/timestamp.h @@ -3,7 +3,7 @@ * timestamp.h * Definitions for the SQL "timestamp" and "interval" types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/timestamp.h @@ -142,8 +142,11 @@ extern int timestamp_cmp_internal(Timestamp dt1, Timestamp dt2); /* timestamp comparison works for timestamptz also */ #define timestamptz_cmp_internal(dt1,dt2) timestamp_cmp_internal(dt1, dt2) -extern TimestampTz timestamp2timestamptz_opt_overflow(Timestamp timestamp, - int *overflow); +extern TimestampTz timestamp2timestamptz_safe(Timestamp timestamp, + Node *escontext); +extern Timestamp timestamptz2timestamp_safe(TimestampTz timestamp, + Node *escontext); + extern int32 timestamp_cmp_timestamptz_internal(Timestamp timestampVal, TimestampTz dt2); diff --git a/src/include/utils/tuplesort.h b/src/include/utils/tuplesort.h index ef79f259f935f..da68f45acf299 100644 --- a/src/include/utils/tuplesort.h +++ b/src/include/utils/tuplesort.h @@ -11,7 +11,7 @@ * algorithm. Parallel sorts use a variant of this external sort * algorithm, and are typically only used for large amounts of data. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/tuplesort.h @@ -21,15 +21,17 @@ #ifndef TUPLESORT_H #define TUPLESORT_H -#include "access/brin_tuple.h" -#include "access/gin_tuple.h" #include "access/itup.h" +#include "executor/instrument_node.h" #include "executor/tuptable.h" #include "storage/dsm.h" #include "utils/logtape.h" #include "utils/relcache.h" #include "utils/sortsupport.h" +/* We don't want this file to depend on AM-specific header files */ +typedef struct BrinTuple BrinTuple; +typedef struct GinTuple GinTuple; /* * Tuplesortstate and Sharedsort are opaque types whose details are not @@ -57,39 +59,10 @@ typedef struct SortCoordinateData /* Private opaque state (points to shared memory) */ Sharedsort *sharedsort; -} SortCoordinateData; +} SortCoordinateData; typedef struct SortCoordinateData *SortCoordinate; -/* - * Data structures for reporting sort statistics. Note that - * TuplesortInstrumentation can't contain any pointers because we - * sometimes put it in shared memory. - * - * The parallel-sort infrastructure relies on having a zero TuplesortMethod - * to indicate that a worker never did anything, so we assign zero to - * SORT_TYPE_STILL_IN_PROGRESS. The other values of this enum can be - * OR'ed together to represent a situation where different workers used - * different methods, so we need a separate bit for each one. Keep the - * NUM_TUPLESORTMETHODS constant in sync with the number of bits! - */ -typedef enum -{ - SORT_TYPE_STILL_IN_PROGRESS = 0, - SORT_TYPE_TOP_N_HEAPSORT = 1 << 0, - SORT_TYPE_QUICKSORT = 1 << 1, - SORT_TYPE_EXTERNAL_SORT = 1 << 2, - SORT_TYPE_EXTERNAL_MERGE = 1 << 3, -} TuplesortMethod; - -#define NUM_TUPLESORTMETHODS 4 - -typedef enum -{ - SORT_SPACE_TYPE_DISK, - SORT_SPACE_TYPE_MEMORY, -} TuplesortSpaceType; - /* Bitwise option flags for tuple sorts */ #define TUPLESORT_NONE 0 @@ -108,13 +81,6 @@ typedef enum */ #define TupleSortUseBumpTupleCxt(opt) (((opt) & TUPLESORT_ALLOWBOUNDED) == 0) -typedef struct TuplesortInstrumentation -{ - TuplesortMethod sortMethod; /* sort algorithm used */ - TuplesortSpaceType spaceType; /* type of space spaceUsed represents */ - int64 spaceUsed; /* space consumption, in kB */ -} TuplesortInstrumentation; - /* * The objects we actually sort are SortTuple structs. These contain * a pointer to the tuple proper (might be a MinimalTuple or IndexTuple), @@ -150,6 +116,7 @@ typedef struct void *tuple; /* the tuple itself */ Datum datum1; /* value of first key column */ bool isnull1; /* is first key column NULL? */ + uint8 curbyte; /* chunk of datum1 for current radix sort pass */ int srctape; /* source tape number */ } SortTuple; @@ -458,7 +425,7 @@ extern void tuplesort_puttupleslot(Tuplesortstate *state, TupleTableSlot *slot); extern void tuplesort_putheaptuple(Tuplesortstate *state, HeapTuple tup); extern void tuplesort_putindextuplevalues(Tuplesortstate *state, - Relation rel, ItemPointer self, + Relation rel, const ItemPointerData *self, const Datum *values, const bool *isnull); extern void tuplesort_putbrintuple(Tuplesortstate *state, BrinTuple *tuple, Size size); extern void tuplesort_putgintuple(Tuplesortstate *state, GinTuple *tuple, Size size); diff --git a/src/include/utils/tuplestore.h b/src/include/utils/tuplestore.h index 865ba7b826551..f638b96e15607 100644 --- a/src/include/utils/tuplestore.h +++ b/src/include/utils/tuplestore.h @@ -21,7 +21,7 @@ * Also, we have changed the API to return tuples in TupleTableSlots, * so that there is a check to prevent attempted access to system columns. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/tuplestore.h @@ -73,6 +73,9 @@ extern bool tuplestore_in_memory(Tuplestorestate *state); extern bool tuplestore_gettupleslot(Tuplestorestate *state, bool forward, bool copy, TupleTableSlot *slot); +extern bool tuplestore_gettupleslot_force(Tuplestorestate *state, bool forward, + bool copy, TupleTableSlot *slot); + extern bool tuplestore_advance(Tuplestorestate *state, bool forward); extern bool tuplestore_skiptuples(Tuplestorestate *state, diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h index 1cb30f1818c2b..5a4aa9ec840eb 100644 --- a/src/include/utils/typcache.h +++ b/src/include/utils/typcache.h @@ -6,7 +6,7 @@ * The type cache exists to speed lookup of certain information about data * types that is not directly available from a type's pg_type row. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/typcache.h @@ -183,7 +183,7 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref, extern void UpdateDomainConstraintRef(DomainConstraintRef *ref); -extern bool DomainHasConstraints(Oid type_id); +extern bool DomainHasConstraints(Oid type_id, bool *has_volatile); extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod); diff --git a/src/include/utils/tzparser.h b/src/include/utils/tzparser.h index 6b93fc6c6d56d..75212e65f1331 100644 --- a/src/include/utils/tzparser.h +++ b/src/include/utils/tzparser.h @@ -3,7 +3,7 @@ * tzparser.h * Timezone offset file parsing definitions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/tzparser.h diff --git a/src/include/utils/uuid.h b/src/include/utils/uuid.h index 239f365a54728..572d8cf4c36cf 100644 --- a/src/include/utils/uuid.h +++ b/src/include/utils/uuid.h @@ -5,7 +5,7 @@ * to avoid conflicts with any uuid_t type that might be defined by * the system headers. * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * src/include/utils/uuid.h * diff --git a/src/include/utils/varbit.h b/src/include/utils/varbit.h index 2f525aaa2af9a..8e2b981e1f62d 100644 --- a/src/include/utils/varbit.h +++ b/src/include/utils/varbit.h @@ -5,7 +5,7 @@ * * Code originally contributed by Adriaan Joubert. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/varbit.h @@ -20,7 +20,7 @@ #include "fmgr.h" /* - * Modeled on struct varlena from postgres.h, but data type is bits8. + * Modeled on varlena from c.h, but data type is uint8. * * Caution: if bit_len is not a multiple of BITS_PER_BYTE, the low-order * bits of the last byte of bit_dat[] are unused and MUST be zeroes. @@ -31,7 +31,7 @@ typedef struct { int32 vl_len_; /* varlena header (do not touch directly!) */ int32 bit_len; /* number of valid bits */ - bits8 bit_dat[FLEXIBLE_ARRAY_MEMBER]; /* bit string, most sig. byte + uint8 bit_dat[FLEXIBLE_ARRAY_MEMBER]; /* bit string, most sig. byte * first */ } VarBit; @@ -82,7 +82,7 @@ VarBitPGetDatum(const VarBit *X) */ #define VARBITMAXLEN (INT_MAX - BITS_PER_BYTE + 1) /* pointer beyond the end of the bit string (like end() in STL containers) */ -#define VARBITEND(PTR) (((bits8 *) (PTR)) + VARSIZE(PTR)) +#define VARBITEND(PTR) (((uint8 *) (PTR)) + VARSIZE(PTR)) /* Mask that will cover exactly one byte, i.e. BITS_PER_BYTE bits */ #define BITMASK 0xFF diff --git a/src/include/utils/varlena.h b/src/include/utils/varlena.h index db9fdf72941dd..fe8d8a5895291 100644 --- a/src/include/utils/varlena.h +++ b/src/include/utils/varlena.h @@ -3,7 +3,7 @@ * varlena.h * Functions for the variable-length built-in types. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/varlena.h @@ -27,6 +27,9 @@ extern int varstr_levenshtein_less_equal(const char *source, int slen, int ins_c, int del_c, int sub_c, int max_d, bool trusted); extern List *textToQualifiedNameList(text *textval); +extern char *scan_quoted_identifier(char **endp, char **nextp); +extern char *scan_identifier(char **endp, char **nextp, char separator, + bool downcase_unquoted); extern bool SplitIdentifierString(char *rawstring, char separator, List **namelist); extern bool SplitDirectoriesString(char *rawstring, char separator, diff --git a/src/include/utils/wait_classes.h b/src/include/utils/wait_classes.h index 51ee68397d595..b91690a22c63b 100644 --- a/src/include/utils/wait_classes.h +++ b/src/include/utils/wait_classes.h @@ -2,7 +2,7 @@ * wait_classes.h * Definitions related to wait event classes * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/wait_classes.h * ---------- @@ -17,7 +17,7 @@ */ #define PG_WAIT_LWLOCK 0x01000000U #define PG_WAIT_LOCK 0x03000000U -#define PG_WAIT_BUFFERPIN 0x04000000U +#define PG_WAIT_BUFFER 0x04000000U #define PG_WAIT_ACTIVITY 0x05000000U #define PG_WAIT_CLIENT 0x06000000U #define PG_WAIT_EXTENSION 0x07000000U diff --git a/src/include/utils/wait_event.h b/src/include/utils/wait_event.h index f5815b4994ab4..c28effcc6c08b 100644 --- a/src/include/utils/wait_event.h +++ b/src/include/utils/wait_event.h @@ -2,7 +2,7 @@ * wait_event.h * Definitions related to wait event reporting * - * Copyright (c) 2001-2025, PostgreSQL Global Development Group + * Copyright (c) 2001-2026, PostgreSQL Global Development Group * * src/include/utils/wait_event.h * ---------- @@ -12,6 +12,7 @@ /* enums for wait events */ #include "utils/wait_event_types.h" +#include "utils/probes.h" extern const char *pgstat_get_wait_event(uint32 wait_event_info); extern const char *pgstat_get_wait_event_type(uint32 wait_event_info); @@ -42,8 +43,6 @@ extern PGDLLIMPORT uint32 *my_wait_event_info; extern uint32 WaitEventExtensionNew(const char *wait_event_name); extern uint32 WaitEventInjectionPointNew(const char *wait_event_name); -extern void WaitEventCustomShmemInit(void); -extern Size WaitEventCustomShmemSize(void); extern char **GetWaitEventCustomNames(uint32 classId, int *nwaitevents); /* ---------- @@ -73,6 +72,8 @@ pgstat_report_wait_start(uint32 wait_event_info) * four-bytes, updates are atomic. */ *(volatile uint32 *) my_wait_event_info = wait_event_info; + + TRACE_POSTGRESQL_WAIT_EVENT_START(wait_event_info); } /* ---------- @@ -86,6 +87,8 @@ pgstat_report_wait_end(void) { /* see pgstat_report_wait_start() */ *(volatile uint32 *) my_wait_event_info = 0; + + TRACE_POSTGRESQL_WAIT_EVENT_END(); } diff --git a/src/include/utils/xid8.h b/src/include/utils/xid8.h index 35399b7f6fbb8..d1cefb3e05356 100644 --- a/src/include/utils/xid8.h +++ b/src/include/utils/xid8.h @@ -3,7 +3,7 @@ * xid8.h * Header file for the "xid8" ADT. * - * Copyright (c) 2020-2025, PostgreSQL Global Development Group + * Copyright (c) 2020-2026, PostgreSQL Global Development Group * * src/include/utils/xid8.h * diff --git a/src/include/utils/xml.h b/src/include/utils/xml.h index 0d7a816b9f933..ca266f448d689 100644 --- a/src/include/utils/xml.h +++ b/src/include/utils/xml.h @@ -4,7 +4,7 @@ * Declarations for XML data type support. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/include/utils/xml.h @@ -20,7 +20,7 @@ #include "nodes/execnodes.h" #include "nodes/primnodes.h" -typedef struct varlena xmltype; +typedef varlena xmltype; typedef enum { @@ -71,9 +71,9 @@ extern void xml_ereport(PgXmlErrorContext *errcxt, int level, int sqlcode, extern xmltype *xmlconcat(List *args); extern xmltype *xmlelement(XmlExpr *xexpr, - Datum *named_argvalue, bool *named_argnull, - Datum *argvalue, bool *argnull); -extern xmltype *xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace); + const Datum *named_argvalue, const bool *named_argnull, + const Datum *argvalue, const bool *argnull); +extern xmltype *xmlparse(text *data, XmlOptionType xmloption_arg, bool preserve_whitespace, Node *escontext); extern xmltype *xmlpi(const char *target, text *arg, bool arg_is_null, bool *result_is_null); extern xmltype *xmlroot(xmltype *data, text *version, int standalone); extern bool xml_is_document(xmltype *arg); diff --git a/src/include/varatt.h b/src/include/varatt.h index 2e8564d49980b..000bdf33b9236 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -4,7 +4,7 @@ * variable-length datatypes (TOAST support) * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1995, Regents of the University of California * * src/include/varatt.h @@ -16,7 +16,7 @@ #define VARATT_H /* - * struct varatt_external is a traditional "TOAST pointer", that is, the + * varatt_external is a traditional "TOAST pointer", that is, the * information needed to fetch a Datum stored out-of-line in a TOAST table. * The data is compressed if and only if the external size stored in * va_extinfo is less than va_rawsize - VARHDRSZ. @@ -36,7 +36,7 @@ typedef struct varatt_external * compression method */ Oid va_valueid; /* Unique ID of value within TOAST table */ Oid va_toastrelid; /* RelID of TOAST table containing it */ -} varatt_external; +} varatt_external; /* * These macros define the "saved size" portion of va_extinfo. Its remaining @@ -46,27 +46,27 @@ typedef struct varatt_external #define VARLENA_EXTSIZE_MASK ((1U << VARLENA_EXTSIZE_BITS) - 1) /* - * struct varatt_indirect is a "TOAST pointer" representing an out-of-line + * varatt_indirect is a "TOAST pointer" representing an out-of-line * Datum that's stored in memory, not in an external toast relation. * The creator of such a Datum is entirely responsible that the referenced * storage survives for as long as referencing pointer Datums can exist. * - * Note that just as for struct varatt_external, this struct is stored + * Note that just as for varatt_external, this struct is stored * unaligned within any containing tuple. */ typedef struct varatt_indirect { - struct varlena *pointer; /* Pointer to in-memory varlena */ -} varatt_indirect; + varlena *pointer; /* Pointer to in-memory varlena */ +} varatt_indirect; /* - * struct varatt_expanded is a "TOAST pointer" representing an out-of-line + * varatt_expanded is a "TOAST pointer" representing an out-of-line * Datum that is stored in memory, in some type-specific, not necessarily * physically contiguous format that is convenient for computation not * storage. APIs for this, in particular the definition of struct * ExpandedObjectHeader, are in src/include/utils/expandeddatum.h. * - * Note that just as for struct varatt_external, this struct is stored + * Note that just as for varatt_external, this struct is stored * unaligned within any containing tuple. */ typedef struct ExpandedObjectHeader ExpandedObjectHeader; @@ -89,20 +89,35 @@ typedef enum vartag_external VARTAG_ONDISK = 18 } vartag_external; +/* Is a TOAST pointer either type of expanded-object pointer? */ /* this test relies on the specific tag values above */ -#define VARTAG_IS_EXPANDED(tag) \ - (((tag) & ~1) == VARTAG_EXPANDED_RO) +static inline bool +VARTAG_IS_EXPANDED(vartag_external tag) +{ + return ((tag & ~1) == VARTAG_EXPANDED_RO); +} -#define VARTAG_SIZE(tag) \ - ((tag) == VARTAG_INDIRECT ? sizeof(varatt_indirect) : \ - VARTAG_IS_EXPANDED(tag) ? sizeof(varatt_expanded) : \ - (tag) == VARTAG_ONDISK ? sizeof(varatt_external) : \ - (AssertMacro(false), 0)) +/* Size of the data part of a "TOAST pointer" datum */ +static inline Size +VARTAG_SIZE(vartag_external tag) +{ + if (tag == VARTAG_INDIRECT) + return sizeof(varatt_indirect); + else if (VARTAG_IS_EXPANDED(tag)) + return sizeof(varatt_expanded); + else if (tag == VARTAG_ONDISK) + return sizeof(varatt_external); + else + { + Assert(false); + return 0; + } +} /* * These structs describe the header of a varlena object that may have been * TOASTed. Generally, don't reference these structs directly, but use the - * macros below. + * functions and macros below. * * We use separate structs for the aligned and unaligned cases because the * compiler might otherwise think it could generate code that assumes @@ -166,7 +181,9 @@ typedef struct /* * Endian-dependent macros. These are considered internal --- use the - * external macros below instead of using these directly. + * external functions below instead of using these directly. All of these + * expect an argument that is a pointer, not a Datum. Some of them have + * multiple-evaluation hazards, too. * * Note: IS_1B is true for external toast records but VARSIZE_1B will return 0 * for such records. Hence you should usually check for IS_EXTERNAL before @@ -176,25 +193,25 @@ typedef struct #ifdef WORDS_BIGENDIAN #define VARATT_IS_4B(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0x80) == 0x00) + ((((const varattrib_1b *) (PTR))->va_header & 0x80) == 0x00) #define VARATT_IS_4B_U(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0xC0) == 0x00) + ((((const varattrib_1b *) (PTR))->va_header & 0xC0) == 0x00) #define VARATT_IS_4B_C(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0xC0) == 0x40) + ((((const varattrib_1b *) (PTR))->va_header & 0xC0) == 0x40) #define VARATT_IS_1B(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0x80) == 0x80) + ((((const varattrib_1b *) (PTR))->va_header & 0x80) == 0x80) #define VARATT_IS_1B_E(PTR) \ - ((((varattrib_1b *) (PTR))->va_header) == 0x80) + ((((const varattrib_1b *) (PTR))->va_header) == 0x80) #define VARATT_NOT_PAD_BYTE(PTR) \ - (*((uint8 *) (PTR)) != 0) + (*((const uint8 *) (PTR)) != 0) /* VARSIZE_4B() should only be used on known-aligned data */ #define VARSIZE_4B(PTR) \ - (((varattrib_4b *) (PTR))->va_4byte.va_header & 0x3FFFFFFF) + (((const varattrib_4b *) (PTR))->va_4byte.va_header & 0x3FFFFFFF) #define VARSIZE_1B(PTR) \ - (((varattrib_1b *) (PTR))->va_header & 0x7F) + (((const varattrib_1b *) (PTR))->va_header & 0x7F) #define VARTAG_1B_E(PTR) \ - (((varattrib_1b_e *) (PTR))->va_tag) + ((vartag_external) ((const varattrib_1b_e *) (PTR))->va_tag) #define SET_VARSIZE_4B(PTR,len) \ (((varattrib_4b *) (PTR))->va_4byte.va_header = (len) & 0x3FFFFFFF) @@ -209,25 +226,25 @@ typedef struct #else /* !WORDS_BIGENDIAN */ #define VARATT_IS_4B(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0x01) == 0x00) + ((((const varattrib_1b *) (PTR))->va_header & 0x01) == 0x00) #define VARATT_IS_4B_U(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0x03) == 0x00) + ((((const varattrib_1b *) (PTR))->va_header & 0x03) == 0x00) #define VARATT_IS_4B_C(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0x03) == 0x02) + ((((const varattrib_1b *) (PTR))->va_header & 0x03) == 0x02) #define VARATT_IS_1B(PTR) \ - ((((varattrib_1b *) (PTR))->va_header & 0x01) == 0x01) + ((((const varattrib_1b *) (PTR))->va_header & 0x01) == 0x01) #define VARATT_IS_1B_E(PTR) \ - ((((varattrib_1b *) (PTR))->va_header) == 0x01) + ((((const varattrib_1b *) (PTR))->va_header) == 0x01) #define VARATT_NOT_PAD_BYTE(PTR) \ - (*((uint8 *) (PTR)) != 0) + (*((const uint8 *) (PTR)) != 0) /* VARSIZE_4B() should only be used on known-aligned data */ #define VARSIZE_4B(PTR) \ - ((((varattrib_4b *) (PTR))->va_4byte.va_header >> 2) & 0x3FFFFFFF) + ((((const varattrib_4b *) (PTR))->va_4byte.va_header >> 2) & 0x3FFFFFFF) #define VARSIZE_1B(PTR) \ - ((((varattrib_1b *) (PTR))->va_header >> 1) & 0x7F) + ((((const varattrib_1b *) (PTR))->va_header >> 1) & 0x7F) #define VARTAG_1B_E(PTR) \ - (((varattrib_1b_e *) (PTR))->va_tag) + ((vartag_external) ((const varattrib_1b_e *) (PTR))->va_tag) #define SET_VARSIZE_4B(PTR,len) \ (((varattrib_4b *) (PTR))->va_4byte.va_header = (((uint32) (len)) << 2)) @@ -247,19 +264,19 @@ typedef struct #define VARDATA_1B_E(PTR) (((varattrib_1b_e *) (PTR))->va_data) /* - * Externally visible TOAST macros begin here. + * Externally visible TOAST functions and macros begin here. All of these + * were originally macros, accounting for the upper-case naming. + * + * Most of these functions accept a pointer to a value of a toastable data + * type. The caller's variable might be declared "text *" or the like, + * so we use "void *" here. Callers that are working with a Datum variable + * must apply DatumGetPointer before calling these functions. */ #define VARHDRSZ_EXTERNAL offsetof(varattrib_1b_e, va_data) #define VARHDRSZ_COMPRESSED offsetof(varattrib_4b, va_compressed.va_data) #define VARHDRSZ_SHORT offsetof(varattrib_1b, va_data) - #define VARATT_SHORT_MAX 0x7F -#define VARATT_CAN_MAKE_SHORT(PTR) \ - (VARATT_IS_4B_U(PTR) && \ - (VARSIZE(PTR) - VARHDRSZ + VARHDRSZ_SHORT) <= VARATT_SHORT_MAX) -#define VARATT_CONVERTED_SHORT_SIZE(PTR) \ - (VARSIZE(PTR) - VARHDRSZ + VARHDRSZ_SHORT) /* * In consumers oblivious to data alignment, call PG_DETOAST_DATUM_PACKED(), @@ -272,70 +289,234 @@ typedef struct * Code assembling a new datum should call VARDATA() and SET_VARSIZE(). * (Datums begin life untoasted.) * - * Other macros here should usually be used only by tuple assembly/disassembly + * Other functions here should usually be used only by tuple assembly/disassembly * code and code that specifically wants to work with still-toasted Datums. */ -#define VARDATA(PTR) VARDATA_4B(PTR) -#define VARSIZE(PTR) VARSIZE_4B(PTR) - -#define VARSIZE_SHORT(PTR) VARSIZE_1B(PTR) -#define VARDATA_SHORT(PTR) VARDATA_1B(PTR) - -#define VARTAG_EXTERNAL(PTR) VARTAG_1B_E(PTR) -#define VARSIZE_EXTERNAL(PTR) (VARHDRSZ_EXTERNAL + VARTAG_SIZE(VARTAG_EXTERNAL(PTR))) -#define VARDATA_EXTERNAL(PTR) VARDATA_1B_E(PTR) - -#define VARATT_IS_COMPRESSED(PTR) VARATT_IS_4B_C(PTR) -#define VARATT_IS_EXTERNAL(PTR) VARATT_IS_1B_E(PTR) -#define VARATT_IS_EXTERNAL_ONDISK(PTR) \ - (VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK) -#define VARATT_IS_EXTERNAL_INDIRECT(PTR) \ - (VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT) -#define VARATT_IS_EXTERNAL_EXPANDED_RO(PTR) \ - (VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_EXPANDED_RO) -#define VARATT_IS_EXTERNAL_EXPANDED_RW(PTR) \ - (VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_EXPANDED_RW) -#define VARATT_IS_EXTERNAL_EXPANDED(PTR) \ - (VARATT_IS_EXTERNAL(PTR) && VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR))) -#define VARATT_IS_EXTERNAL_NON_EXPANDED(PTR) \ - (VARATT_IS_EXTERNAL(PTR) && !VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR))) -#define VARATT_IS_SHORT(PTR) VARATT_IS_1B(PTR) -#define VARATT_IS_EXTENDED(PTR) (!VARATT_IS_4B_U(PTR)) - -#define SET_VARSIZE(PTR, len) SET_VARSIZE_4B(PTR, len) -#define SET_VARSIZE_SHORT(PTR, len) SET_VARSIZE_1B(PTR, len) -#define SET_VARSIZE_COMPRESSED(PTR, len) SET_VARSIZE_4B_C(PTR, len) - -#define SET_VARTAG_EXTERNAL(PTR, tag) SET_VARTAG_1B_E(PTR, tag) - -#define VARSIZE_ANY(PTR) \ - (VARATT_IS_1B_E(PTR) ? VARSIZE_EXTERNAL(PTR) : \ - (VARATT_IS_1B(PTR) ? VARSIZE_1B(PTR) : \ - VARSIZE_4B(PTR))) - -/* Size of a varlena data, excluding header */ -#define VARSIZE_ANY_EXHDR(PTR) \ - (VARATT_IS_1B_E(PTR) ? VARSIZE_EXTERNAL(PTR)-VARHDRSZ_EXTERNAL : \ - (VARATT_IS_1B(PTR) ? VARSIZE_1B(PTR)-VARHDRSZ_SHORT : \ - VARSIZE_4B(PTR)-VARHDRSZ)) +/* Size of a known-not-toasted varlena datum, including header */ +static inline Size +VARSIZE(const void *PTR) +{ + return VARSIZE_4B(PTR); +} + +/* Start of data area of a known-not-toasted varlena datum */ +static inline char * +VARDATA(const void *PTR) +{ + return VARDATA_4B(PTR); +} + +/* Size of a known-short-header varlena datum, including header */ +static inline Size +VARSIZE_SHORT(const void *PTR) +{ + return VARSIZE_1B(PTR); +} + +/* Start of data area of a known-short-header varlena datum */ +static inline char * +VARDATA_SHORT(const void *PTR) +{ + return VARDATA_1B(PTR); +} + +/* Type tag of a "TOAST pointer" datum */ +static inline vartag_external +VARTAG_EXTERNAL(const void *PTR) +{ + return VARTAG_1B_E(PTR); +} + +/* Size of a "TOAST pointer" datum, including header */ +static inline Size +VARSIZE_EXTERNAL(const void *PTR) +{ + return VARHDRSZ_EXTERNAL + VARTAG_SIZE(VARTAG_EXTERNAL(PTR)); +} + +/* Start of data area of a "TOAST pointer" datum */ +static inline char * +VARDATA_EXTERNAL(const void *PTR) +{ + return VARDATA_1B_E(PTR); +} + +/* Is varlena datum in inline-compressed format? */ +static inline bool +VARATT_IS_COMPRESSED(const void *PTR) +{ + return VARATT_IS_4B_C(PTR); +} + +/* Is varlena datum a "TOAST pointer" datum? */ +static inline bool +VARATT_IS_EXTERNAL(const void *PTR) +{ + return VARATT_IS_1B_E(PTR); +} + +/* Is varlena datum a pointer to on-disk toasted data? */ +static inline bool +VARATT_IS_EXTERNAL_ONDISK(const void *PTR) +{ + return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_ONDISK; +} + +/* Is varlena datum an indirect pointer? */ +static inline bool +VARATT_IS_EXTERNAL_INDIRECT(const void *PTR) +{ + return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_INDIRECT; +} + +/* Is varlena datum a read-only pointer to an expanded object? */ +static inline bool +VARATT_IS_EXTERNAL_EXPANDED_RO(const void *PTR) +{ + return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_EXPANDED_RO; +} + +/* Is varlena datum a read-write pointer to an expanded object? */ +static inline bool +VARATT_IS_EXTERNAL_EXPANDED_RW(const void *PTR) +{ + return VARATT_IS_EXTERNAL(PTR) && VARTAG_EXTERNAL(PTR) == VARTAG_EXPANDED_RW; +} + +/* Is varlena datum either type of pointer to an expanded object? */ +static inline bool +VARATT_IS_EXTERNAL_EXPANDED(const void *PTR) +{ + return VARATT_IS_EXTERNAL(PTR) && VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)); +} + +/* Is varlena datum a "TOAST pointer", but not for an expanded object? */ +static inline bool +VARATT_IS_EXTERNAL_NON_EXPANDED(const void *PTR) +{ + return VARATT_IS_EXTERNAL(PTR) && !VARTAG_IS_EXPANDED(VARTAG_EXTERNAL(PTR)); +} + +/* Is varlena datum a short-header datum? */ +static inline bool +VARATT_IS_SHORT(const void *PTR) +{ + return VARATT_IS_1B(PTR); +} + +/* Is varlena datum not in traditional (4-byte-header, uncompressed) format? */ +static inline bool +VARATT_IS_EXTENDED(const void *PTR) +{ + return !VARATT_IS_4B_U(PTR); +} + +/* Is varlena datum short enough to convert to short-header format? */ +static inline bool +VARATT_CAN_MAKE_SHORT(const void *PTR) +{ + return VARATT_IS_4B_U(PTR) && + (VARSIZE(PTR) - VARHDRSZ + VARHDRSZ_SHORT) <= VARATT_SHORT_MAX; +} + +/* Size that datum will have in short-header format, including header */ +static inline Size +VARATT_CONVERTED_SHORT_SIZE(const void *PTR) +{ + return VARSIZE(PTR) - VARHDRSZ + VARHDRSZ_SHORT; +} + +/* Set the size (including header) of a 4-byte-header varlena datum */ +static inline void +SET_VARSIZE(void *PTR, Size len) +{ + SET_VARSIZE_4B(PTR, len); +} + +/* Set the size (including header) of a short-header varlena datum */ +static inline void +SET_VARSIZE_SHORT(void *PTR, Size len) +{ + SET_VARSIZE_1B(PTR, len); +} + +/* Set the size (including header) of an inline-compressed varlena datum */ +static inline void +SET_VARSIZE_COMPRESSED(void *PTR, Size len) +{ + SET_VARSIZE_4B_C(PTR, len); +} + +/* Set the type tag of a "TOAST pointer" datum */ +static inline void +SET_VARTAG_EXTERNAL(void *PTR, vartag_external tag) +{ + SET_VARTAG_1B_E(PTR, tag); +} + +/* Size of a varlena datum of any format, including header */ +static inline Size +VARSIZE_ANY(const void *PTR) +{ + if (VARATT_IS_1B_E(PTR)) + return VARSIZE_EXTERNAL(PTR); + else if (VARATT_IS_1B(PTR)) + return VARSIZE_1B(PTR); + else + return VARSIZE_4B(PTR); +} + +/* Size of a varlena datum of any format, excluding header */ +static inline Size +VARSIZE_ANY_EXHDR(const void *PTR) +{ + if (VARATT_IS_1B_E(PTR)) + return VARSIZE_EXTERNAL(PTR) - VARHDRSZ_EXTERNAL; + else if (VARATT_IS_1B(PTR)) + return VARSIZE_1B(PTR) - VARHDRSZ_SHORT; + else + return VARSIZE_4B(PTR) - VARHDRSZ; +} + +/* Start of data area of a plain or short-header varlena datum */ /* caution: this will not work on an external or compressed-in-line Datum */ /* caution: this will return a possibly unaligned pointer */ -#define VARDATA_ANY(PTR) \ - (VARATT_IS_1B(PTR) ? VARDATA_1B(PTR) : VARDATA_4B(PTR)) +static inline char * +VARDATA_ANY(const void *PTR) +{ + return VARATT_IS_1B(PTR) ? VARDATA_1B(PTR) : VARDATA_4B(PTR); +} -/* Decompressed size and compression method of a compressed-in-line Datum */ -#define VARDATA_COMPRESSED_GET_EXTSIZE(PTR) \ - (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK) -#define VARDATA_COMPRESSED_GET_COMPRESS_METHOD(PTR) \ - (((varattrib_4b *) (PTR))->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS) +/* Decompressed size of a compressed-in-line varlena datum */ +static inline Size +VARDATA_COMPRESSED_GET_EXTSIZE(const void *PTR) +{ + return ((const varattrib_4b *) PTR)->va_compressed.va_tcinfo & VARLENA_EXTSIZE_MASK; +} + +/* Compression method of a compressed-in-line varlena datum */ +static inline uint32 +VARDATA_COMPRESSED_GET_COMPRESS_METHOD(const void *PTR) +{ + return ((const varattrib_4b *) PTR)->va_compressed.va_tcinfo >> VARLENA_EXTSIZE_BITS; +} -/* Same for external Datums; but note argument is a struct varatt_external */ -#define VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) \ - ((toast_pointer).va_extinfo & VARLENA_EXTSIZE_MASK) -#define VARATT_EXTERNAL_GET_COMPRESS_METHOD(toast_pointer) \ - ((toast_pointer).va_extinfo >> VARLENA_EXTSIZE_BITS) +/* Same for external Datums; but note argument is a varatt_external */ +static inline Size +VARATT_EXTERNAL_GET_EXTSIZE(varatt_external toast_pointer) +{ + return toast_pointer.va_extinfo & VARLENA_EXTSIZE_MASK; +} +static inline uint32 +VARATT_EXTERNAL_GET_COMPRESS_METHOD(varatt_external toast_pointer) +{ + return toast_pointer.va_extinfo >> VARLENA_EXTSIZE_BITS; +} + +/* Set size and compress method of an externally-stored varlena datum */ +/* This has to remain a macro; beware multiple evaluations! */ #define VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, len, cm) \ do { \ Assert((cm) == TOAST_PGLZ_COMPRESSION_ID || \ @@ -351,8 +532,11 @@ typedef struct * VARHDRSZ overhead, the former doesn't. We never use compression unless it * actually saves space, so we expect either equality or less-than. */ -#define VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) \ - (VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < \ - (toast_pointer).va_rawsize - VARHDRSZ) +static inline bool +VARATT_EXTERNAL_IS_COMPRESSED(varatt_external toast_pointer) +{ + return VARATT_EXTERNAL_GET_EXTSIZE(toast_pointer) < + (Size) (toast_pointer.va_rawsize - VARHDRSZ); +} #endif diff --git a/src/include/windowapi.h b/src/include/windowapi.h index cb2ece166b6e9..2ec18209f8e6f 100644 --- a/src/include/windowapi.h +++ b/src/include/windowapi.h @@ -19,7 +19,7 @@ * function in nodeWindowAgg.c for details. * * - * Portions Copyright (c) 2000-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2000-2026, PostgreSQL Global Development Group * * src/include/windowapi.h * @@ -28,6 +28,8 @@ #ifndef WINDOWAPI_H #define WINDOWAPI_H +#include "fmgr.h" + /* values of "seektype" */ #define WINDOW_SEEK_CURRENT 0 #define WINDOW_SEEK_HEAD 1 @@ -41,6 +43,10 @@ typedef struct WindowObjectData *WindowObject; #define WindowObjectIsValid(winobj) \ ((winobj) != NULL && IsA(winobj, WindowObjectData)) +extern void WinCheckAndInitializeNullTreatment(WindowObject winobj, + bool allowNullTreatment, + FunctionCallInfo fcinfo); + extern void *WinGetPartitionLocalMemory(WindowObject winobj, Size sz); extern int64 WinGetCurrentPosition(WindowObject winobj); diff --git a/src/interfaces/ecpg/Makefile b/src/interfaces/ecpg/Makefile index 3002bc3c1bb66..1977981aa9558 100644 --- a/src/interfaces/ecpg/Makefile +++ b/src/interfaces/ecpg/Makefile @@ -2,7 +2,12 @@ subdir = src/interfaces/ecpg top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = include pgtypeslib ecpglib compatlib preproc +SUBDIRS = \ + compatlib \ + ecpglib \ + include \ + pgtypeslib \ + preproc # Suppress parallel build of subdirectories to avoid a bug in GNU make 3.82, cf # https://savannah.gnu.org/bugs/?30653 diff --git a/src/interfaces/ecpg/compatlib/Makefile b/src/interfaces/ecpg/compatlib/Makefile index 457f1a81212b3..f72b24fbe7117 100644 --- a/src/interfaces/ecpg/compatlib/Makefile +++ b/src/interfaces/ecpg/compatlib/Makefile @@ -2,7 +2,7 @@ # # Makefile for ecpg compatibility library # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/interfaces/ecpg/compatlib/Makefile diff --git a/src/interfaces/ecpg/compatlib/informix.c b/src/interfaces/ecpg/compatlib/informix.c index e829d722f2228..ca11a81f1bcfe 100644 --- a/src/interfaces/ecpg/compatlib/informix.c +++ b/src/interfaces/ecpg/compatlib/informix.c @@ -807,8 +807,10 @@ rfmtlong(long lng_val, const char *fmt, char *outbuf) if (strchr(fmt, (int) '(') && strchr(fmt, (int) ')')) brackets_ok = 1; - /* get position of the right-most dot in the format-string */ - /* and fill the temp-string wit '0's up to there. */ + /* + * get position of the right-most dot in the format-string and fill the + * temp-string with '0's up to there. + */ dotpos = getRightMostDot(fmt); /* start to parse the format-string */ diff --git a/src/interfaces/ecpg/compatlib/meson.build b/src/interfaces/ecpg/compatlib/meson.build index 56e0a21651b7a..0efe8a382302c 100644 --- a/src/interfaces/ecpg/compatlib/meson.build +++ b/src/interfaces/ecpg/compatlib/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ecpg_compat_sources = files( 'informix.c', @@ -16,6 +16,7 @@ if host_system == 'windows' endif # see src/interfaces/libpq/meson.build +if build_static_lib ecpg_compat_st = static_library('libecpg_compat', ecpg_compat_sources, include_directories: ecpg_compat_inc, @@ -25,14 +26,16 @@ ecpg_compat_st = static_library('libecpg_compat', kwargs: default_lib_args, ) ecpg_targets += ecpg_compat_st +endif +if build_shared_lib ecpg_compat_so = shared_library('libecpg_compat', ecpg_compat_sources + ecpg_compat_so_sources, include_directories: ecpg_compat_inc, c_args: ecpg_compat_c_args, dependencies: [frontend_shlib_code, thread_dep], link_with: [ecpglib_so, ecpg_pgtypes_so], - soversion: host_system != 'windows' ? '3' : '', + soversion: host_system not in ['aix', 'windows'] ? '3' : '', darwin_versions: ['3', '3.' + pg_version_major.to_string()], version: '3.' + pg_version_major.to_string(), link_args: export_fmt.format(export_file.full_path()), @@ -40,6 +43,7 @@ ecpg_compat_so = shared_library('libecpg_compat', kwargs: default_lib_args, ) ecpg_targets += ecpg_compat_so +endif pkgconfig.generate( name: 'libecpg_compat', diff --git a/src/interfaces/ecpg/ecpglib/Makefile b/src/interfaces/ecpg/ecpglib/Makefile index 3c99715273bd3..aee8d31211962 100644 --- a/src/interfaces/ecpg/ecpglib/Makefile +++ b/src/interfaces/ecpg/ecpglib/Makefile @@ -2,7 +2,7 @@ # # Makefile for ecpg library # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/interfaces/ecpg/ecpglib/Makefile diff --git a/src/interfaces/ecpg/ecpglib/connect.c b/src/interfaces/ecpg/ecpglib/connect.c index 2bbb70333dcb4..78de9f298ba62 100644 --- a/src/interfaces/ecpg/ecpglib/connect.c +++ b/src/interfaces/ecpg/ecpglib/connect.c @@ -58,7 +58,12 @@ ecpg_get_connection_nr(const char *connection_name) for (con = all_connections; con != NULL; con = con->next) { - if (strcmp(connection_name, con->name) == 0) + /* + * Check for the case of a NULL connection name, stored as such in + * the connection information by ECPGconnect() when the database + * name is not specified by its caller. + */ + if (con->name != NULL && strcmp(connection_name, con->name) == 0) break; } ret = con; @@ -259,7 +264,8 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p struct connection *this; int i, connect_params = 0; - char *dbname = name ? ecpg_strdup(name, lineno) : NULL, + bool alloc_failed = (sqlca == NULL); + char *dbname = name ? ecpg_strdup(name, lineno, &alloc_failed) : NULL, *host = NULL, *tmp, *port = NULL, @@ -268,11 +274,12 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p const char **conn_keywords; const char **conn_values; - if (sqlca == NULL) + if (alloc_failed) { ecpg_raise(lineno, ECPG_OUT_OF_MEMORY, ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL); - ecpg_free(dbname); + if (dbname) + ecpg_free(dbname); return false; } @@ -297,7 +304,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p if (envname) { ecpg_free(dbname); - dbname = ecpg_strdup(envname, lineno); + dbname = ecpg_strdup(envname, lineno, &alloc_failed); } } @@ -349,7 +356,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p tmp = strrchr(dbname + offset, '?'); if (tmp != NULL) /* options given */ { - options = ecpg_strdup(tmp + 1, lineno); + options = ecpg_strdup(tmp + 1, lineno, &alloc_failed); *tmp = '\0'; } @@ -358,7 +365,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p { if (tmp[1] != '\0') /* non-empty database name */ { - realname = ecpg_strdup(tmp + 1, lineno); + realname = ecpg_strdup(tmp + 1, lineno, &alloc_failed); connect_params++; } *tmp = '\0'; @@ -368,7 +375,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p if (tmp != NULL) /* port number given */ { *tmp = '\0'; - port = ecpg_strdup(tmp + 1, lineno); + port = ecpg_strdup(tmp + 1, lineno, &alloc_failed); connect_params++; } @@ -402,7 +409,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p { if (*(dbname + offset) != '\0') { - host = ecpg_strdup(dbname + offset, lineno); + host = ecpg_strdup(dbname + offset, lineno, &alloc_failed); connect_params++; } } @@ -414,7 +421,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p tmp = strrchr(dbname, ':'); if (tmp != NULL) /* port number given */ { - port = ecpg_strdup(tmp + 1, lineno); + port = ecpg_strdup(tmp + 1, lineno, &alloc_failed); connect_params++; *tmp = '\0'; } @@ -422,14 +429,14 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p tmp = strrchr(dbname, '@'); if (tmp != NULL) /* host name given */ { - host = ecpg_strdup(tmp + 1, lineno); + host = ecpg_strdup(tmp + 1, lineno, &alloc_failed); connect_params++; *tmp = '\0'; } if (strlen(dbname) > 0) { - realname = ecpg_strdup(dbname, lineno); + realname = ecpg_strdup(dbname, lineno, &alloc_failed); connect_params++; } else @@ -460,7 +467,18 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p */ conn_keywords = (const char **) ecpg_alloc((connect_params + 1) * sizeof(char *), lineno); conn_values = (const char **) ecpg_alloc(connect_params * sizeof(char *), lineno); - if (conn_keywords == NULL || conn_values == NULL) + + /* Decide on a connection name */ + if (connection_name != NULL || realname != NULL) + { + this->name = ecpg_strdup(connection_name ? connection_name : realname, + lineno, &alloc_failed); + } + else + this->name = NULL; + + /* Deal with any failed allocations above */ + if (conn_keywords == NULL || conn_values == NULL || alloc_failed) { if (host) ecpg_free(host); @@ -476,6 +494,8 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p ecpg_free(conn_keywords); if (conn_values) ecpg_free(conn_values); + if (this->name) + ecpg_free(this->name); free(this); return false; } @@ -510,17 +530,14 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p ecpg_free(conn_keywords); if (conn_values) ecpg_free(conn_values); + if (this->name) + ecpg_free(this->name); free(this); return false; } } #endif - if (connection_name != NULL) - this->name = ecpg_strdup(connection_name, lineno); - else - this->name = ecpg_strdup(realname, lineno); - this->cache_head = NULL; this->prep_stmts = NULL; diff --git a/src/interfaces/ecpg/ecpglib/data.c b/src/interfaces/ecpg/ecpglib/data.c index fa56276758533..d5d40f7b65419 100644 --- a/src/interfaces/ecpg/ecpglib/data.c +++ b/src/interfaces/ecpg/ecpglib/data.c @@ -69,34 +69,23 @@ garbage_left(enum ARRAY_TYPE isarray, char **scan_length, enum COMPAT_MODE compa return false; } -/* stolen code from src/backend/utils/adt/float.c */ -#if defined(WIN32) && !defined(NAN) -static const uint32 nan[2] = {0xffffffff, 0x7fffffff}; - -#define NAN (*(const double *) nan) -#endif +/* + * Portability wrappers borrowed from src/include/utils/float.h + */ static double get_float8_infinity(void) { -#ifdef INFINITY return (double) INFINITY; -#else - return (double) (HUGE_VAL * HUGE_VAL); -#endif } static double get_float8_nan(void) { - /* (double) NAN doesn't work on some NetBSD/MIPS releases */ -#if defined(NAN) && !(defined(__NetBSD__) && defined(__mips__)) return (double) NAN; -#else - return (double) (0.0 / 0.0); -#endif } + static bool check_special_value(char *ptr, double *retval, char **endptr) { diff --git a/src/interfaces/ecpg/ecpglib/descriptor.c b/src/interfaces/ecpg/ecpglib/descriptor.c index 651d5c8b2ed3c..39cd5130ec97a 100644 --- a/src/interfaces/ecpg/ecpglib/descriptor.c +++ b/src/interfaces/ecpg/ecpglib/descriptor.c @@ -113,7 +113,7 @@ get_int_item(int lineno, void *var, enum ECPGttype vartype, int value) *(short *) var = (short) value; break; case ECPGt_int: - *(int *) var = (int) value; + *(int *) var = value; break; case ECPGt_long: *(long *) var = (long) value; @@ -240,8 +240,9 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...) act_tuple; struct variable data_var; struct sqlca_t *sqlca = ECPGget_sqlca(); + bool alloc_failed = (sqlca == NULL); - if (sqlca == NULL) + if (alloc_failed) { ecpg_raise(lineno, ECPG_OUT_OF_MEMORY, ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL); @@ -493,7 +494,14 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...) #ifdef WIN32 stmt.oldthreadlocale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE); #endif - stmt.oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno); + stmt.oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), + lineno, &alloc_failed); + if (alloc_failed) + { + va_end(args); + return false; + } + setlocale(LC_NUMERIC, "C"); #endif diff --git a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h index 75cc68275bdac..c92f0aa1081e6 100644 --- a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h +++ b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h @@ -52,7 +52,7 @@ struct ECPGgeneric_bytea struct ECPGtype_information_cache { struct ECPGtype_information_cache *next; - int oid; + Oid oid; enum ARRAY_TYPE isarray; }; @@ -175,7 +175,7 @@ void ecpg_free(void *ptr); bool ecpg_init(const struct connection *con, const char *connection_name, const int lineno); -char *ecpg_strdup(const char *string, int lineno); +char *ecpg_strdup(const char *string, int lineno, bool *alloc_failed); const char *ecpg_type_name(enum ECPGttype typ); int ecpg_dynamic_type(Oid type); int sqlda_dynamic_type(Oid type, enum COMPAT_MODE compat); @@ -218,7 +218,7 @@ void ecpg_log(const char *format,...) pg_attribute_printf(1, 2); bool ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, char **name, const char *query); bool ecpg_register_prepared_stmt(struct statement *stmt); -void ecpg_init_sqlca(struct sqlca_t *sqlca); +void ecpg_init_sqlca(struct sqlca_t *sqlca_p); struct sqlda_compat *ecpg_build_compat_sqlda(int line, PGresult *res, int row, enum COMPAT_MODE compat); diff --git a/src/interfaces/ecpg/ecpglib/execute.c b/src/interfaces/ecpg/ecpglib/execute.c index f52da06de9a1d..ba41732dec6f4 100644 --- a/src/interfaces/ecpg/ecpglib/execute.c +++ b/src/interfaces/ecpg/ecpglib/execute.c @@ -54,7 +54,7 @@ quote_postgres(char *arg, bool quote, int lineno) { length = strlen(arg); buffer_len = 2 * length + 1; - res = (char *) ecpg_alloc(buffer_len + 3, lineno); + res = ecpg_alloc(buffer_len + 3, lineno); if (!res) return res; escaped_len = PQescapeString(res + 1, arg, buffer_len); @@ -145,7 +145,7 @@ next_insert(char *text, int pos, bool questionmarks, bool std_strings) } static bool -ecpg_type_infocache_push(struct ECPGtype_information_cache **cache, int oid, enum ARRAY_TYPE isarray, int lineno) +ecpg_type_infocache_push(struct ECPGtype_information_cache **cache, Oid oid, enum ARRAY_TYPE isarray, int lineno) { struct ECPGtype_information_cache *new_entry = (struct ECPGtype_information_cache *) ecpg_alloc(sizeof(struct ECPGtype_information_cache), lineno); @@ -161,7 +161,7 @@ ecpg_type_infocache_push(struct ECPGtype_information_cache **cache, int oid, enu } static enum ARRAY_TYPE -ecpg_is_type_an_array(int type, const struct statement *stmt, const struct variable *var) +ecpg_is_type_an_array(Oid type, const struct statement *stmt, const struct variable *var) { char *array_query; enum ARRAY_TYPE isarray = ECPG_ARRAY_NOT_SET; @@ -263,11 +263,11 @@ ecpg_is_type_an_array(int type, const struct statement *stmt, const struct varia return cache_entry->isarray; } - array_query = (char *) ecpg_alloc(strlen("select typlen from pg_type where oid= and typelem<>0") + 11, stmt->lineno); + array_query = ecpg_alloc(strlen("select typlen from pg_type where oid= and typelem<>0") + 11, stmt->lineno); if (array_query == NULL) return ECPG_ARRAY_ERROR; - sprintf(array_query, "select typlen from pg_type where oid=%d and typelem<>0", type); + sprintf(array_query, "select typlen from pg_type where oid=%u and typelem<>0", type); query = PQexec(stmt->connection->connection, array_query); ecpg_free(array_query); if (!ecpg_check_PQresult(query, stmt->lineno, stmt->connection->connection, stmt->compat)) @@ -294,7 +294,8 @@ ecpg_is_type_an_array(int type, const struct statement *stmt, const struct varia return ECPG_ARRAY_ERROR; ecpg_type_infocache_push(&(stmt->connection->cache_head), type, isarray, stmt->lineno); - ecpg_log("ecpg_is_type_an_array on line %d: type (%d); C (%d); array (%s)\n", stmt->lineno, type, var->type, ECPG_IS_ARRAY(isarray) ? "yes" : "no"); + ecpg_log("ecpg_is_type_an_array on line %d: type (%u); C (%d); array (%s)\n", + stmt->lineno, type, var->type, ECPG_IS_ARRAY(isarray) ? "yes" : "no"); return isarray; } @@ -391,7 +392,7 @@ ecpg_store_result(const PGresult *results, int act_field, } ecpg_log("ecpg_store_result on line %d: allocating memory for %d tuples\n", stmt->lineno, ntuples); - var->value = (char *) ecpg_auto_alloc(len, stmt->lineno); + var->value = ecpg_auto_alloc(len, stmt->lineno); if (!var->value) return false; *((char **) var->pointer) = var->value; @@ -402,7 +403,7 @@ ecpg_store_result(const PGresult *results, int act_field, { int len = var->ind_offset * ntuples; - var->ind_value = (char *) ecpg_auto_alloc(len, stmt->lineno); + var->ind_value = ecpg_auto_alloc(len, stmt->lineno); if (!var->ind_value) return false; *((char **) var->ind_pointer) = var->ind_value; @@ -822,7 +823,7 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari struct ECPGgeneric_bytea *variable = (struct ECPGgeneric_bytea *) (var->value); - if (!(mallocedval = (char *) ecpg_alloc(variable->len, lineno))) + if (!(mallocedval = ecpg_alloc(variable->len, lineno))) return false; memcpy(mallocedval, variable->arr, variable->len); @@ -835,7 +836,7 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari struct ECPGgeneric_varchar *variable = (struct ECPGgeneric_varchar *) (var->value); - if (!(newcopy = (char *) ecpg_alloc(variable->len + 1, lineno))) + if (!(newcopy = ecpg_alloc(variable->len + 1, lineno))) return false; strncpy(newcopy, variable->arr, variable->len); @@ -860,9 +861,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari numeric *nval; if (var->arrsize > 1) - mallocedval = ecpg_strdup("{", lineno); + mallocedval = ecpg_strdup("{", lineno, NULL); else - mallocedval = ecpg_strdup("", lineno); + mallocedval = ecpg_strdup("", lineno, NULL); if (!mallocedval) return false; @@ -923,9 +924,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari int slen; if (var->arrsize > 1) - mallocedval = ecpg_strdup("{", lineno); + mallocedval = ecpg_strdup("{", lineno, NULL); else - mallocedval = ecpg_strdup("", lineno); + mallocedval = ecpg_strdup("", lineno, NULL); if (!mallocedval) return false; @@ -970,9 +971,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari int slen; if (var->arrsize > 1) - mallocedval = ecpg_strdup("{", lineno); + mallocedval = ecpg_strdup("{", lineno, NULL); else - mallocedval = ecpg_strdup("", lineno); + mallocedval = ecpg_strdup("", lineno, NULL); if (!mallocedval) return false; @@ -1017,9 +1018,9 @@ ecpg_store_input(const int lineno, const bool force_indicator, const struct vari int slen; if (var->arrsize > 1) - mallocedval = ecpg_strdup("{", lineno); + mallocedval = ecpg_strdup("{", lineno, NULL); else - mallocedval = ecpg_strdup("", lineno); + mallocedval = ecpg_strdup("", lineno, NULL); if (!mallocedval) return false; @@ -1128,9 +1129,7 @@ insert_tobeinserted(int position, int ph_len, struct statement *stmt, char *tobe { char *newcopy; - if (!(newcopy = (char *) ecpg_alloc(strlen(stmt->command) - + strlen(tobeinserted) - + 1, stmt->lineno))) + if (!(newcopy = ecpg_alloc(strlen(stmt->command) + strlen(tobeinserted) + 1, stmt->lineno))) { ecpg_free(tobeinserted); return false; @@ -1536,7 +1535,7 @@ ecpg_build_params(struct statement *stmt) int buffersize = sizeof(int) * CHAR_BIT * 10 / 3; /* a rough guess of the * size we need */ - if (!(tobeinserted = (char *) ecpg_alloc(buffersize, stmt->lineno))) + if (!(tobeinserted = ecpg_alloc(buffersize, stmt->lineno))) { ecpg_free_params(stmt, false); return false; @@ -2001,7 +2000,8 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator, return false; } #endif - stmt->oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno); + stmt->oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno, + NULL); if (stmt->oldlocale == NULL) { ecpg_do_epilogue(stmt); @@ -2030,7 +2030,14 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator, statement_type = ECPGst_execute; } else - stmt->command = ecpg_strdup(query, lineno); + { + stmt->command = ecpg_strdup(query, lineno, NULL); + if (!stmt->command) + { + ecpg_do_epilogue(stmt); + return false; + } + } stmt->name = NULL; @@ -2042,7 +2049,12 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator, if (command) { stmt->name = stmt->command; - stmt->command = ecpg_strdup(command, lineno); + stmt->command = ecpg_strdup(command, lineno, NULL); + if (!stmt->command) + { + ecpg_do_epilogue(stmt); + return false; + } } else { @@ -2175,7 +2187,12 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator, if (!is_prepared_name_set && stmt->statement_type == ECPGst_prepare) { - stmt->name = ecpg_strdup(var->value, lineno); + stmt->name = ecpg_strdup(var->value, lineno, NULL); + if (!stmt->name) + { + ecpg_do_epilogue(stmt); + return false; + } is_prepared_name_set = true; } } diff --git a/src/interfaces/ecpg/ecpglib/memory.c b/src/interfaces/ecpg/ecpglib/memory.c index 6979be2c988ac..2112e55b6e42c 100644 --- a/src/interfaces/ecpg/ecpglib/memory.c +++ b/src/interfaces/ecpg/ecpglib/memory.c @@ -43,8 +43,15 @@ ecpg_realloc(void *ptr, long size, int lineno) return new; } +/* + * Wrapper for strdup(), with NULL in input treated as a correct case. + * + * "alloc_failed" can be optionally specified by the caller to check for + * allocation failures. The caller is responsible for its initialization, + * as ecpg_strdup() may be called repeatedly across multiple allocations. + */ char * -ecpg_strdup(const char *string, int lineno) +ecpg_strdup(const char *string, int lineno, bool *alloc_failed) { char *new; @@ -54,6 +61,8 @@ ecpg_strdup(const char *string, int lineno) new = strdup(string); if (!new) { + if (alloc_failed) + *alloc_failed = true; ecpg_raise(lineno, ECPG_OUT_OF_MEMORY, ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL); return NULL; } diff --git a/src/interfaces/ecpg/ecpglib/meson.build b/src/interfaces/ecpg/ecpglib/meson.build index 8f478c6a73ee4..cb7288f2ee6ef 100644 --- a/src/interfaces/ecpg/ecpglib/meson.build +++ b/src/interfaces/ecpg/ecpglib/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ecpglib_sources = files( 'connect.c', @@ -25,6 +25,7 @@ if host_system == 'windows' endif # see src/interfaces/libpq/meson.build +if build_static_lib ecpglib_st = static_library('libecpg', ecpglib_sources, include_directories: ecpglib_inc, @@ -35,7 +36,9 @@ ecpglib_st = static_library('libecpg', kwargs: default_lib_args, ) ecpg_targets += ecpglib_st +endif +if build_shared_lib ecpglib_so = shared_library('libecpg', ecpglib_sources + ecpglib_so_sources, include_directories: ecpglib_inc, @@ -43,7 +46,7 @@ ecpglib_so = shared_library('libecpg', c_pch: pch_postgres_fe_h, dependencies: [frontend_shlib_code, libpq, thread_dep], link_with: ecpg_pgtypes_so, - soversion: host_system != 'windows' ? '6' : '', + soversion: host_system not in ['aix', 'windows'] ? '6' : '', darwin_versions: ['6', '6.' + pg_version_major.to_string()], version: '6.' + pg_version_major.to_string(), link_args: export_fmt.format(export_file.full_path()), @@ -51,6 +54,7 @@ ecpglib_so = shared_library('libecpg', kwargs: default_lib_args, ) ecpg_targets += ecpglib_so +endif pkgconfig.generate( name: 'libecpg', diff --git a/src/interfaces/ecpg/ecpglib/misc.c b/src/interfaces/ecpg/ecpglib/misc.c index 1885732a65203..40ea174ae9fde 100644 --- a/src/interfaces/ecpg/ecpglib/misc.c +++ b/src/interfaces/ecpg/ecpglib/misc.c @@ -64,9 +64,9 @@ static volatile int simple_debug = 0; static FILE *debugstream = NULL; void -ecpg_init_sqlca(struct sqlca_t *sqlca) +ecpg_init_sqlca(struct sqlca_t *sqlca_p) { - memcpy(sqlca, &sqlca_init, sizeof(struct sqlca_t)); + memcpy(sqlca_p, &sqlca_init, sizeof(struct sqlca_t)); } bool diff --git a/src/interfaces/ecpg/ecpglib/po/meson.build b/src/interfaces/ecpg/ecpglib/po/meson.build index 420363f108a52..a6c1b016298ac 100644 --- a/src/interfaces/ecpg/ecpglib/po/meson.build +++ b/src/interfaces/ecpg/ecpglib/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('ecpglib' + '6' + '-' + pg_version_major.to_string())] diff --git a/src/interfaces/ecpg/ecpglib/prepare.c b/src/interfaces/ecpg/ecpglib/prepare.c index ea1146f520f34..5c7c5397535d8 100644 --- a/src/interfaces/ecpg/ecpglib/prepare.c +++ b/src/interfaces/ecpg/ecpglib/prepare.c @@ -85,9 +85,22 @@ ecpg_register_prepared_stmt(struct statement *stmt) /* create statement */ prep_stmt->lineno = lineno; prep_stmt->connection = con; - prep_stmt->command = ecpg_strdup(stmt->command, lineno); + prep_stmt->command = ecpg_strdup(stmt->command, lineno, NULL); + if (!prep_stmt->command) + { + ecpg_free(prep_stmt); + ecpg_free(this); + return false; + } prep_stmt->inlist = prep_stmt->outlist = NULL; - this->name = ecpg_strdup(stmt->name, lineno); + this->name = ecpg_strdup(stmt->name, lineno, NULL); + if (!this->name) + { + ecpg_free(prep_stmt->command); + ecpg_free(prep_stmt); + ecpg_free(this); + return false; + } this->stmt = prep_stmt; this->prepared = true; @@ -125,14 +138,14 @@ replace_variables(char **text, int lineno) char *buffer, *newcopy; - if (!(buffer = (char *) ecpg_alloc(buffersize, lineno))) + if (!(buffer = ecpg_alloc(buffersize, lineno))) return false; snprintf(buffer, buffersize, "$%d", counter++); for (len = 1; (*text)[ptr + len] && isvarchar((*text)[ptr + len]); len++) /* skip */ ; - if (!(newcopy = (char *) ecpg_alloc(strlen(*text) - len + strlen(buffer) + 1, lineno))) + if (!(newcopy = ecpg_alloc(strlen(*text) - len + strlen(buffer) + 1, lineno))) { ecpg_free(buffer); return false; @@ -177,14 +190,33 @@ prepare_common(int lineno, struct connection *con, const char *name, const char /* create statement */ stmt->lineno = lineno; stmt->connection = con; - stmt->command = ecpg_strdup(variable, lineno); + stmt->command = ecpg_strdup(variable, lineno, NULL); + if (!stmt->command) + { + ecpg_free(stmt); + ecpg_free(this); + return false; + } stmt->inlist = stmt->outlist = NULL; /* if we have C variables in our statement replace them with '?' */ - replace_variables(&(stmt->command), lineno); + if (!replace_variables(&(stmt->command), lineno)) + { + ecpg_free(stmt->command); + ecpg_free(stmt); + ecpg_free(this); + return false; + } /* add prepared statement to our list */ - this->name = ecpg_strdup(name, lineno); + this->name = ecpg_strdup(name, lineno, NULL); + if (!this->name) + { + ecpg_free(stmt->command); + ecpg_free(stmt); + ecpg_free(this); + return false; + } this->stmt = stmt; /* and finally really prepare the statement */ @@ -270,7 +302,7 @@ deallocate_one(int lineno, enum COMPAT_MODE c, struct connection *con, char *text; PGresult *query; - text = (char *) ecpg_alloc(strlen("deallocate \"\" ") + strlen(this->name), this->stmt->lineno); + text = ecpg_alloc(strlen("deallocate \"\" ") + strlen(this->name), this->stmt->lineno); if (text) { @@ -477,7 +509,7 @@ ecpg_freeStmtCacheEntry(int lineno, int compat, if (entry->ecpgQuery) { ecpg_free(entry->ecpgQuery); - entry->ecpgQuery = 0; + entry->ecpgQuery = NULL; } return entNo; @@ -540,7 +572,9 @@ AddStmtToCache(int lineno, /* line # of statement */ /* add the query to the entry */ entry = &stmtCacheEntries[entNo]; entry->lineno = lineno; - entry->ecpgQuery = ecpg_strdup(ecpgQuery, lineno); + entry->ecpgQuery = ecpg_strdup(ecpgQuery, lineno, NULL); + if (!entry->ecpgQuery) + return -1; entry->connection = connection; entry->execs = 0; memcpy(entry->stmtID, stmtID, sizeof(entry->stmtID)); @@ -567,14 +601,19 @@ ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, cha ecpg_log("ecpg_auto_prepare on line %d: statement found in cache; entry %d\n", lineno, entNo); stmtID = stmtCacheEntries[entNo].stmtID; + *name = ecpg_strdup(stmtID, lineno, NULL); + if (*name == NULL) + return false; con = ecpg_get_connection(connection_name); prep = ecpg_find_prepared_statement(stmtID, con, NULL); /* This prepared name doesn't exist on this connection. */ if (!prep && !prepare_common(lineno, con, stmtID, query)) + { + ecpg_free(*name); return false; + } - *name = ecpg_strdup(stmtID, lineno); } else { @@ -584,15 +623,22 @@ ecpg_auto_prepare(int lineno, const char *connection_name, const int compat, cha /* generate a statement ID */ sprintf(stmtID, "ecpg%d", nextStmtID++); + *name = ecpg_strdup(stmtID, lineno, NULL); + if (*name == NULL) + return false; if (!ECPGprepare(lineno, connection_name, 0, stmtID, query)) + { + ecpg_free(*name); return false; + } entNo = AddStmtToCache(lineno, stmtID, connection_name, compat, query); if (entNo < 0) + { + ecpg_free(*name); return false; - - *name = ecpg_strdup(stmtID, lineno); + } } /* increase usage counter */ diff --git a/src/interfaces/ecpg/include/meson.build b/src/interfaces/ecpg/include/meson.build index a6541e1a68657..a34a4e30f38ef 100644 --- a/src/interfaces/ecpg/include/meson.build +++ b/src/interfaces/ecpg/include/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ecpg_inc = include_directories('.') diff --git a/src/interfaces/ecpg/meson.build b/src/interfaces/ecpg/meson.build index 2d173ce376f8c..b75d1ef4401d6 100644 --- a/src/interfaces/ecpg/meson.build +++ b/src/interfaces/ecpg/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ecpg_targets = [] diff --git a/src/interfaces/ecpg/pgtypeslib/Makefile b/src/interfaces/ecpg/pgtypeslib/Makefile index c993888c61df8..10223628a41c4 100644 --- a/src/interfaces/ecpg/pgtypeslib/Makefile +++ b/src/interfaces/ecpg/pgtypeslib/Makefile @@ -2,7 +2,7 @@ # # Makefile for ecpg pgtypes library # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/interfaces/ecpg/pgtypeslib/Makefile diff --git a/src/interfaces/ecpg/pgtypeslib/datetime.c b/src/interfaces/ecpg/pgtypeslib/datetime.c index 1b253747fc403..f43343b459499 100644 --- a/src/interfaces/ecpg/pgtypeslib/datetime.c +++ b/src/interfaces/ecpg/pgtypeslib/datetime.c @@ -335,8 +335,8 @@ PGTYPESdate_defmt_asc(date * d, const char *fmt, const char *str) */ int token[3][2]; int token_values[3] = {-1, -1, -1}; - char *fmt_token_order; - char *fmt_ystart, + const char *fmt_token_order; + const char *fmt_ystart, *fmt_mstart, *fmt_dstart; unsigned int i; diff --git a/src/interfaces/ecpg/pgtypeslib/interval.c b/src/interfaces/ecpg/pgtypeslib/interval.c index 936a688381688..463455398f1c4 100644 --- a/src/interfaces/ecpg/pgtypeslib/interval.c +++ b/src/interfaces/ecpg/pgtypeslib/interval.c @@ -6,10 +6,6 @@ #include #include -#ifdef __FAST_MATH__ -#error -ffast-math is known to break this code -#endif - #include "common/string.h" #include "dt.h" #include "pgtypes_error.h" @@ -184,7 +180,7 @@ DecodeISO8601Interval(char *str, continue; } /* Else fall through to extended alternative format */ - /* FALLTHROUGH */ + pg_fallthrough; case '-': /* ISO 8601 4.4.3.3 Alternative Format, * Extended */ if (havefield) @@ -263,7 +259,7 @@ DecodeISO8601Interval(char *str, return 0; } /* Else fall through to extended alternative format */ - /* FALLTHROUGH */ + pg_fallthrough; case ':': /* ISO 8601 4.4.3.3 Alternative Format, * Extended */ if (havefield) @@ -391,7 +387,7 @@ DecodeInterval(char **field, int *ftype, int nf, /* int range, */ tmask = DTK_M(TZ); break; } - /* FALL THROUGH */ + pg_fallthrough; case DTK_DATE: case DTK_NUMBER: diff --git a/src/interfaces/ecpg/pgtypeslib/meson.build b/src/interfaces/ecpg/pgtypeslib/meson.build index 02301ec9acb90..f62f3b92e4967 100644 --- a/src/interfaces/ecpg/pgtypeslib/meson.build +++ b/src/interfaces/ecpg/pgtypeslib/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ecpg_pgtypes_sources = files( 'common.c', @@ -21,6 +21,7 @@ if host_system == 'windows' endif # see src/interfaces/libpq/meson.build +if build_static_lib ecpg_pgtypes_st = static_library('libpgtypes', ecpg_pgtypes_sources, include_directories: ecpg_pgtypes_inc, @@ -30,7 +31,9 @@ ecpg_pgtypes_st = static_library('libpgtypes', kwargs: default_lib_args, ) ecpg_targets += ecpg_pgtypes_st +endif +if build_shared_lib ecpg_pgtypes_so = shared_library('libpgtypes', ecpg_pgtypes_sources + ecpg_pgtypes_so_sources, include_directories: ecpg_pgtypes_inc, @@ -38,13 +41,14 @@ ecpg_pgtypes_so = shared_library('libpgtypes', c_pch: pch_postgres_fe_h, dependencies: frontend_shlib_code, version: '3.' + pg_version_major.to_string(), - soversion: host_system != 'windows' ? '3' : '', + soversion: host_system not in ['aix', 'windows'] ? '3' : '', darwin_versions: ['3', '3.' + pg_version_major.to_string()], link_args: export_fmt.format(export_file.full_path()), link_depends: export_file, kwargs: default_lib_args, ) ecpg_targets += ecpg_pgtypes_so +endif pkgconfig.generate( name: 'libpgtypes', diff --git a/src/interfaces/ecpg/pgtypeslib/timestamp.c b/src/interfaces/ecpg/pgtypeslib/timestamp.c index 7cf433266f458..9bf1b91455333 100644 --- a/src/interfaces/ecpg/pgtypeslib/timestamp.c +++ b/src/interfaces/ecpg/pgtypeslib/timestamp.c @@ -7,10 +7,6 @@ #include #include -#ifdef __FAST_MATH__ -#error -ffast-math is known to break this code -#endif - #include "common/int.h" #include "dt.h" #include "pgtypes_date.h" diff --git a/src/interfaces/ecpg/preproc/Makefile b/src/interfaces/ecpg/preproc/Makefile index f2ac885047969..6968fca3ceb38 100644 --- a/src/interfaces/ecpg/preproc/Makefile +++ b/src/interfaces/ecpg/preproc/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/interfaces/ecpg/preproc # -# Copyright (c) 1998-2025, PostgreSQL Global Development Group +# Copyright (c) 1998-2026, PostgreSQL Global Development Group # # src/interfaces/ecpg/preproc/Makefile # diff --git a/src/interfaces/ecpg/preproc/c_kwlist.h b/src/interfaces/ecpg/preproc/c_kwlist.h index 82a7c0a592d92..df3acbae41c51 100644 --- a/src/interfaces/ecpg/preproc/c_kwlist.h +++ b/src/interfaces/ecpg/preproc/c_kwlist.h @@ -7,7 +7,7 @@ * by the PG_KEYWORD macro, which is not defined in this file; it can * be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/ecpg/preproc/c_kwlist.h diff --git a/src/interfaces/ecpg/preproc/ecpg.c b/src/interfaces/ecpg/preproc/ecpg.c index 3f0f10e654afc..1db55be473f1f 100644 --- a/src/interfaces/ecpg/preproc/ecpg.c +++ b/src/interfaces/ecpg/preproc/ecpg.c @@ -1,7 +1,7 @@ /* src/interfaces/ecpg/preproc/ecpg.c */ /* Main for ecpg, the PostgreSQL embedded SQL precompiler. */ -/* Copyright (c) 1996-2025, PostgreSQL Global Development Group */ +/* Copyright (c) 1996-2026, PostgreSQL Global Development Group */ #include "postgres_fe.h" diff --git a/src/interfaces/ecpg/preproc/ecpg.trailer b/src/interfaces/ecpg/preproc/ecpg.trailer index 6f94b832a037f..e4c13fdd2b607 100644 --- a/src/interfaces/ecpg/preproc/ecpg.trailer +++ b/src/interfaces/ecpg/preproc/ecpg.trailer @@ -1975,12 +1975,14 @@ civarind: cvariable indicator char_civar: char_variable { - char *ptr = strstr(@1, ".arr"); + char *var = loc_strdup(@1); + char *ptr = strstr(var, ".arr"); if (ptr) /* varchar, we need the struct name here, not * the struct element */ *ptr = '\0'; - add_variable_to_head(&argsinsert, find_variable(@1), &no_indicator); + add_variable_to_head(&argsinsert, find_variable(var), &no_indicator); + @$ = var; } ; diff --git a/src/interfaces/ecpg/preproc/ecpg_kwlist.h b/src/interfaces/ecpg/preproc/ecpg_kwlist.h index 3ab4655386e5b..1cd8afe1e975b 100644 --- a/src/interfaces/ecpg/preproc/ecpg_kwlist.h +++ b/src/interfaces/ecpg/preproc/ecpg_kwlist.h @@ -7,7 +7,7 @@ * by the PG_KEYWORD macro, which is not defined in this file; it can * be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/ecpg/preproc/ecpg_kwlist.h diff --git a/src/interfaces/ecpg/preproc/keywords.c b/src/interfaces/ecpg/preproc/keywords.c index cc861897d2a41..277f8cc4aa20e 100644 --- a/src/interfaces/ecpg/preproc/keywords.c +++ b/src/interfaces/ecpg/preproc/keywords.c @@ -4,7 +4,7 @@ * lexical token lookup for key words in PostgreSQL * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/interfaces/ecpg/preproc/meson.build b/src/interfaces/ecpg/preproc/meson.build index aa948efc0dce8..3a56e2bb4ef1e 100644 --- a/src/interfaces/ecpg/preproc/meson.build +++ b/src/interfaces/ecpg/preproc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ecpg_sources = files( '../ecpglib/typename.c', diff --git a/src/interfaces/ecpg/preproc/parse.pl b/src/interfaces/ecpg/preproc/parse.pl index f22ca213c21a7..a4312ef2804c1 100644 --- a/src/interfaces/ecpg/preproc/parse.pl +++ b/src/interfaces/ecpg/preproc/parse.pl @@ -9,7 +9,7 @@ # --parser: the backend gram.y file to read (required, no default) # --output: where to write preproc.y (required, no default) # -# Copyright (c) 2007-2025, PostgreSQL Global Development Group +# Copyright (c) 2007-2026, PostgreSQL Global Development Group # # Written by Mike Aubury # Michael Meskes @@ -256,9 +256,9 @@ sub main $has_if_command = 1 if /^\s*if/; } - # Make sure any braces are split into separate fields - s/{/ { /g; - s/}/ } /g; + # Make sure any (unquoted) braces are split into separate fields + s/(?= 'A' && *ptr <= 'Z') - *ptr += 'a' - 'A'; + char *tmp; + + tmp = loc_strdup(base_yytext); + /* Apply an ASCII-only downcasing */ + for (unsigned char *ptr = (unsigned char *) tmp; *ptr; ptr++) + { + if (*ptr >= 'A' && *ptr <= 'Z') + *ptr += 'a' - 'A'; + } + base_yylloc = tmp; + break; } - break; } return token; } diff --git a/src/interfaces/ecpg/preproc/pgc.l b/src/interfaces/ecpg/preproc/pgc.l index 63283a4a1e5a4..c6f36e0275b4a 100644 --- a/src/interfaces/ecpg/preproc/pgc.l +++ b/src/interfaces/ecpg/preproc/pgc.l @@ -10,7 +10,7 @@ * only here to simplify syncing this file with scan.l. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -335,6 +335,8 @@ less_equals "<=" greater_equals ">=" less_greater "<>" not_equals "!=" +/* Note there is no need for left_arrow, since "<-" is not a single operator. */ +right_arrow "->" /* * "self" is the set of chars that should be returned as single-character @@ -346,7 +348,7 @@ not_equals "!=" * If you change either set, adjust the character lists appearing in the * rule for "operator"! */ -self [,()\[\].;\:\+\-\*\/\%\^\<\>\=] +self [,()\[\].;\:\|\+\-\*\/\%\^\<\>\=] op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=] operator {op_chars}+ @@ -854,6 +856,10 @@ cppline {space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})((\/\*[^*/]*\*+ return NOT_EQUALS; } +{right_arrow} { + return RIGHT_ARROW; + } + {informix_special} { /* are we simulating Informix? */ if (INFORMIX_MODE) @@ -947,7 +953,7 @@ cppline {space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})((\/\*[^*/]*\*+ * that the "self" rule would have. */ if (nchars == 1 && - strchr(",()[].;:+-*/%^<>=", yytext[0])) + strchr(",()[].;:|+-*/%^<>=", yytext[0])) return yytext[0]; /* @@ -968,6 +974,8 @@ cppline {space}*#([^i][A-Za-z]*|{if}|{ifdef}|{ifndef}|{import})((\/\*[^*/]*\*+ return NOT_EQUALS; if (yytext[0] == '!' && yytext[1] == '=') return NOT_EQUALS; + if (yytext[0] == '-' && yytext[1] == '>') + return RIGHT_ARROW; } } diff --git a/src/interfaces/ecpg/preproc/po/meson.build b/src/interfaces/ecpg/preproc/po/meson.build index 208861ade7234..af693c114da1e 100644 --- a/src/interfaces/ecpg/preproc/po/meson.build +++ b/src/interfaces/ecpg/preproc/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('ecpg-' + pg_version_major.to_string())] diff --git a/src/interfaces/ecpg/preproc/t/001_ecpg_err_warn_msg.pl b/src/interfaces/ecpg/preproc/t/001_ecpg_err_warn_msg.pl index a18e09e6ee859..4ff6b6ac93ea9 100644 --- a/src/interfaces/ecpg/preproc/t/001_ecpg_err_warn_msg.pl +++ b/src/interfaces/ecpg/preproc/t/001_ecpg_err_warn_msg.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/interfaces/ecpg/preproc/t/002_ecpg_err_warn_msg_informix.pl b/src/interfaces/ecpg/preproc/t/002_ecpg_err_warn_msg_informix.pl index cb0502dfc2b06..f2db6f9e77c2b 100644 --- a/src/interfaces/ecpg/preproc/t/002_ecpg_err_warn_msg_informix.pl +++ b/src/interfaces/ecpg/preproc/t/002_ecpg_err_warn_msg_informix.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/interfaces/ecpg/preproc/variable.c b/src/interfaces/ecpg/preproc/variable.c index 2c67e33e92e6a..ad5201a222fc5 100644 --- a/src/interfaces/ecpg/preproc/variable.c +++ b/src/interfaces/ecpg/preproc/variable.c @@ -6,6 +6,19 @@ static struct variable *allvariables = NULL; +/* this probably belongs in util.c, but for now it's only needed here */ +static char * +loc_nstrdup(const char *in, size_t len) +{ + char *out; + + out = loc_alloc(len + 1); + memcpy(out, in, len); + out[len] = '\0'; + + return out; +} + struct variable * new_variable(const char *name, struct ECPGtype *type, int brace_level) { @@ -21,22 +34,34 @@ new_variable(const char *name, struct ECPGtype *type, int brace_level) return p; } +/* + * Perform lookup of a field within a struct + * + * 'name' is the entire C variable name + * 'str' points just before the next field name to parse + * 'members' and 'brace_level' describe the struct we are looking into + * + * Returns NULL if field is not found. + * + * This recurses if needed to handle sub-fields. + */ static struct variable * -find_struct_member(const char *name, char *str, struct ECPGstruct_member *members, int brace_level) +find_struct_member(const char *name, const char *str, + struct ECPGstruct_member *members, int brace_level) { - char *next = strpbrk(++str, ".-["), + /* ++ here skips over the '.', or the '>' of '->' */ + const char *next = strpbrk(++str, ".-["), *end, - c = '\0'; + *field; if (next != NULL) - { - c = *next; - *next = '\0'; - } + field = loc_nstrdup(str, next - str); + else + field = str; for (; members; members = members->next) { - if (strcmp(members->name, str) == 0) + if (strcmp(members->name, field) == 0) { if (next == NULL) { @@ -54,14 +79,13 @@ find_struct_member(const char *name, char *str, struct ECPGstruct_member *member } else { - *next = c; - if (c == '[') + if (*next == '[') { int count; /* * We don't care about what's inside the array brackets so - * just eat up the character + * just scan to find the matching right bracket. */ for (count = 1, end = next + 1; count; end++) { @@ -122,26 +146,35 @@ find_struct_member(const char *name, char *str, struct ECPGstruct_member *member return NULL; } +/* + * Do struct lookup when we have found var.field, var->field, or var[n].field + * + * 'name' is the entire C variable name + * 'next' points at the character after the base name + * 'end' points at the character after the subscript, if there was a + * subscript, else it's the same as 'next'. + * + * This is used only at the first level of field reference; sub-fields will + * be handled by internal recursion in find_struct_member. + */ static struct variable * -find_struct(const char *name, char *next, char *end) +find_struct(const char *name, const char *next, const char *end) { + char *prefix; struct variable *p; - char c = *next; /* first get the mother structure entry */ - *next = '\0'; - p = find_variable(name); + prefix = loc_nstrdup(name, next - name); + p = find_variable(prefix); - if (c == '-') + if (*next == '-') { + /* We have var->field */ if (p->type->type != ECPGt_array) - mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer", name); + mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer", prefix); if (p->type->u.element->type != ECPGt_struct && p->type->u.element->type != ECPGt_union) - mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer to a structure or a union", name); - - /* restore the name, we will need it later */ - *next = c; + mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer to a structure or a union", prefix); return find_struct_member(name, ++end, p->type->u.element->u.members, p->brace_level); } @@ -149,30 +182,27 @@ find_struct(const char *name, char *next, char *end) { if (next == end) { + /* We have var.field */ if (p->type->type != ECPGt_struct && p->type->type != ECPGt_union) - mmfatal(PARSE_ERROR, "variable \"%s\" is neither a structure nor a union", name); - - /* restore the name, we will need it later */ - *next = c; + mmfatal(PARSE_ERROR, "variable \"%s\" is neither a structure nor a union", prefix); return find_struct_member(name, end, p->type->u.members, p->brace_level); } else { + /* We have var[n].field */ if (p->type->type != ECPGt_array) - mmfatal(PARSE_ERROR, "variable \"%s\" is not an array", name); + mmfatal(PARSE_ERROR, "variable \"%s\" is not an array", prefix); if (p->type->u.element->type != ECPGt_struct && p->type->u.element->type != ECPGt_union) - mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer to a structure or a union", name); - - /* restore the name, we will need it later */ - *next = c; + mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer to a structure or a union", prefix); return find_struct_member(name, end, p->type->u.element->u.members, p->brace_level); } } } +/* Look up a variable given its base name */ static struct variable * find_simple(const char *name) { @@ -187,12 +217,24 @@ find_simple(const char *name) return NULL; } -/* Note that this function will end the program in case of an unknown */ -/* variable */ +/* + * Build a "struct variable" for a C variable reference. + * + * The given "name" string is a CVARIABLE per pgc.l, so it can include not + * only a base variable name but also ".field", "->field", or "[subscript]" + * decoration. We don't need to understand that fully, because we always + * duplicate the whole string into the name field of the result variable + * and emit it literally to the output file; the C compiler will make sense + * of it later. What we do need to do here is identify the type of the + * target field or array element so that we can attach a correct ECPGtype + * struct to the result. + * + * Note that this function will end the program in case of an unknown variable. + */ struct variable * find_variable(const char *name) { - char *next, + const char *next, *end; struct variable *p; int count; @@ -200,11 +242,12 @@ find_variable(const char *name) next = strpbrk(name, ".[-"); if (next) { + /* Deal with field/subscript decoration */ if (*next == '[') { /* * We don't care about what's inside the array brackets so just - * eat up the characters + * scan to find the matching right bracket. */ for (count = 1, end = next + 1; count; end++) { @@ -224,18 +267,25 @@ find_variable(const char *name) } } if (*end == '.') + { + /* We have var[n].field */ p = find_struct(name, next, end); + } else { - char c = *next; + /* + * Note: this part assumes we must just have var[n] without + * any further decoration, which fails to handle cases such as + * var[n]->field. For now, that's okay because + * pointer-to-pointer variables are rejected elsewhere. + */ + char *prefix = loc_nstrdup(name, next - name); - *next = '\0'; - p = find_simple(name); + p = find_simple(prefix); if (p == NULL) - mmfatal(PARSE_ERROR, "variable \"%s\" is not declared", name); + mmfatal(PARSE_ERROR, "variable \"%s\" is not declared", prefix); if (p->type->type != ECPGt_array) - mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer", name); - *next = c; + mmfatal(PARSE_ERROR, "variable \"%s\" is not a pointer", prefix); switch (p->type->u.element->type) { case ECPGt_array: @@ -249,7 +299,10 @@ find_variable(const char *name) } } else + { + /* Must be var.field or var->field */ p = find_struct(name, next, next); + } } else p = find_simple(name); diff --git a/src/interfaces/ecpg/test/compat_informix/intoasc.pgc b/src/interfaces/ecpg/test/compat_informix/intoasc.pgc index d13c83bb7a7a2..cf8b342ab4c01 100644 --- a/src/interfaces/ecpg/test/compat_informix/intoasc.pgc +++ b/src/interfaces/ecpg/test/compat_informix/intoasc.pgc @@ -8,7 +8,7 @@ EXEC SQL BEGIN DECLARE SECTION; interval *interval_ptr; EXEC SQL END DECLARE SECTION; -int main() +int main(void) { interval_ptr = (interval *) malloc(sizeof(interval)); interval_ptr->time = 100000000; diff --git a/src/interfaces/ecpg/test/compat_informix/meson.build b/src/interfaces/ecpg/test/compat_informix/meson.build index fdf810115cdeb..64b7f3e7ac4c5 100644 --- a/src/interfaces/ecpg/test/compat_informix/meson.build +++ b/src/interfaces/ecpg/test/compat_informix/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'charfuncs', diff --git a/src/interfaces/ecpg/test/compat_oracle/char_array.pgc b/src/interfaces/ecpg/test/compat_oracle/char_array.pgc index de18cbb57ffd4..303fe02aa2733 100644 --- a/src/interfaces/ecpg/test/compat_oracle/char_array.pgc +++ b/src/interfaces/ecpg/test/compat_oracle/char_array.pgc @@ -17,7 +17,7 @@ static void warn(void) should be fixed-length, blank-padded, then null-terminated. Conforms to the ANSI Fixed Character type. */ -int main() { +int main(void) { EXEC SQL WHENEVER SQLWARNING do warn(); EXEC SQL WHENEVER SQLERROR STOP; diff --git a/src/interfaces/ecpg/test/compat_oracle/meson.build b/src/interfaces/ecpg/test/compat_oracle/meson.build index 43084e5b8ef86..4016b5f0646f1 100644 --- a/src/interfaces/ecpg/test/compat_oracle/meson.build +++ b/src/interfaces/ecpg/test/compat_oracle/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'char_array', diff --git a/src/interfaces/ecpg/test/connect/meson.build b/src/interfaces/ecpg/test/connect/meson.build index 5154167663d0d..591e04bc422ae 100644 --- a/src/interfaces/ecpg/test/connect/meson.build +++ b/src/interfaces/ecpg/test/connect/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'test1', diff --git a/src/interfaces/ecpg/test/ecpg_schedule b/src/interfaces/ecpg/test/ecpg_schedule index 254a0bacc755e..b75e16fde1e72 100644 --- a/src/interfaces/ecpg/test/ecpg_schedule +++ b/src/interfaces/ecpg/test/ecpg_schedule @@ -53,6 +53,7 @@ test: sql/quote test: sql/show test: sql/sqljson test: sql/sqljson_jsontable +test: sql/sqlpgq test: sql/insupd test: sql/parser test: sql/prepareas diff --git a/src/interfaces/ecpg/test/expected/compat_informix-intoasc.c b/src/interfaces/ecpg/test/expected/compat_informix-intoasc.c index 30988809e92e9..56b927c6aaefd 100644 --- a/src/interfaces/ecpg/test/expected/compat_informix-intoasc.c +++ b/src/interfaces/ecpg/test/expected/compat_informix-intoasc.c @@ -27,7 +27,7 @@ #line 9 "intoasc.pgc" -int main() +int main(void) { interval_ptr = (interval *) malloc(sizeof(interval)); interval_ptr->time = 100000000; diff --git a/src/interfaces/ecpg/test/expected/compat_oracle-char_array.c b/src/interfaces/ecpg/test/expected/compat_oracle-char_array.c index 16db663dcc5dd..2daedb546c26d 100644 --- a/src/interfaces/ecpg/test/expected/compat_oracle-char_array.c +++ b/src/interfaces/ecpg/test/expected/compat_oracle-char_array.c @@ -57,7 +57,7 @@ static void warn(void) should be fixed-length, blank-padded, then null-terminated. Conforms to the ANSI Fixed Character type. */ -int main() { +int main(void) { /* exec sql whenever sql_warning do warn ( ) ; */ #line 22 "char_array.pgc" diff --git a/src/interfaces/ecpg/test/expected/preproc-array_of_struct.c b/src/interfaces/ecpg/test/expected/preproc-array_of_struct.c index 1cf371092fe10..31349aa550956 100644 --- a/src/interfaces/ecpg/test/expected/preproc-array_of_struct.c +++ b/src/interfaces/ecpg/test/expected/preproc-array_of_struct.c @@ -50,7 +50,14 @@ typedef struct ind { #line 21 "array_of_struct.pgc" -int main() +typedef struct { +#line 26 "array_of_struct.pgc" + customer customers [ 10 ] ; + } company ; +#line 27 "array_of_struct.pgc" + + +int main(void) { /* exec sql begin declare section */ @@ -60,14 +67,14 @@ int main() typedef struct { -#line 30 "array_of_struct.pgc" +#line 36 "array_of_struct.pgc" struct varchar_2 { int len; char arr[ 50 ]; } name ; -#line 31 "array_of_struct.pgc" +#line 37 "array_of_struct.pgc" int phone ; } customer2 ; -#line 32 "array_of_struct.pgc" +#line 38 "array_of_struct.pgc" @@ -80,106 +87,134 @@ int main() + + + -#line 26 "array_of_struct.pgc" +#line 32 "array_of_struct.pgc" customer custs1 [ 10 ] ; -#line 27 "array_of_struct.pgc" +#line 33 "array_of_struct.pgc" cust_ind inds [ 10 ] ; -#line 33 "array_of_struct.pgc" +#line 39 "array_of_struct.pgc" customer2 custs2 [ 10 ] ; -#line 38 "array_of_struct.pgc" +#line 44 "array_of_struct.pgc" struct customer3 { -#line 36 "array_of_struct.pgc" +#line 42 "array_of_struct.pgc" struct varchar_3 { int len; char arr[ 50 ]; } name ; -#line 37 "array_of_struct.pgc" +#line 43 "array_of_struct.pgc" int phone ; } custs3 [ 10 ] ; -#line 43 "array_of_struct.pgc" +#line 49 "array_of_struct.pgc" struct customer4 { -#line 41 "array_of_struct.pgc" +#line 47 "array_of_struct.pgc" struct varchar_4 { int len; char arr[ 50 ]; } name ; -#line 42 "array_of_struct.pgc" +#line 48 "array_of_struct.pgc" int phone ; } custs4 ; -#line 44 "array_of_struct.pgc" +#line 51 "array_of_struct.pgc" + company acme ; + +#line 53 "array_of_struct.pgc" int r ; -#line 45 "array_of_struct.pgc" +#line 54 "array_of_struct.pgc" struct varchar_5 { int len; char arr[ 50 ]; } onlyname [ 2 ] ; /* exec sql end declare section */ -#line 46 "array_of_struct.pgc" +#line 55 "array_of_struct.pgc" ECPGdebug(1, stderr); { ECPGconnect(__LINE__, 0, "ecpg1_regression" , NULL, NULL , NULL, 0); -#line 50 "array_of_struct.pgc" +#line 59 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 50 "array_of_struct.pgc" +#line 59 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 50 "array_of_struct.pgc" +#line 59 "array_of_struct.pgc" { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create table customers ( c varchar ( 50 ) , p int )", ECPGt_EOIT, ECPGt_EORT); -#line 52 "array_of_struct.pgc" +#line 61 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 52 "array_of_struct.pgc" +#line 61 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 52 "array_of_struct.pgc" +#line 61 "array_of_struct.pgc" - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( 'John Doe' , '12345' )", ECPGt_EOIT, ECPGt_EORT); -#line 53 "array_of_struct.pgc" + + /* First we'll insert some data using C variable references */ + strcpy(custs1[0].name.arr, "John Doe"); + custs1[0].name.len = strlen(custs1[0].name.arr); + custs1[0].phone = 12345; + + strcpy(acme.customers[1].name.arr, "Jane Doe"); + acme.customers[1].name.len = strlen(acme.customers[1].name.arr); + acme.customers[1].phone = 67890; + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( $1 , $2 )", + ECPGt_varchar,&(custs1->name),(long)50,(long)1,sizeof(struct varchar_1), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(custs1[0].phone),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, ECPGt_EORT); +#line 73 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 53 "array_of_struct.pgc" +#line 73 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 53 "array_of_struct.pgc" +#line 73 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 53 "array_of_struct.pgc" +#line 73 "array_of_struct.pgc" - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( 'Jane Doe' , '67890' )", ECPGt_EOIT, ECPGt_EORT); -#line 54 "array_of_struct.pgc" + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( $1 , $2 )", + ECPGt_varchar,&(acme.customers[1].name),(long)50,(long)1,sizeof(struct varchar_1), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(acme.customers[1].phone),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, ECPGt_EORT); +#line 75 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 54 "array_of_struct.pgc" +#line 75 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 54 "array_of_struct.pgc" +#line 75 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 54 "array_of_struct.pgc" +#line 75 "array_of_struct.pgc" + + /* Clear the array, to be sure reading back into it actually gets data */ + memset(custs1, 0, sizeof(customer) * 10); + /* Now read back the data */ { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select * from customers limit 2", ECPGt_EOIT, ECPGt_varchar,&(custs1->name),(long)50,(long)10,sizeof( customer ), ECPGt_short,&(inds->name_ind),(long)1,(long)10,sizeof( struct ind ), ECPGt_int,&(custs1->phone),(long)1,(long)10,sizeof( customer ), ECPGt_short,&(inds->phone_ind),(long)1,(long)10,sizeof( struct ind ), ECPGt_EORT); -#line 56 "array_of_struct.pgc" +#line 81 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 56 "array_of_struct.pgc" +#line 81 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 56 "array_of_struct.pgc" +#line 81 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 56 "array_of_struct.pgc" +#line 81 "array_of_struct.pgc" printf("custs1:\n"); for (r = 0; r < 2; r++) @@ -193,16 +228,16 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_short,&(inds->name_ind),(long)1,(long)10,sizeof( struct ind ), ECPGt_int,&(custs2->phone),(long)1,(long)10,sizeof( customer2 ), ECPGt_short,&(inds->phone_ind),(long)1,(long)10,sizeof( struct ind ), ECPGt_EORT); -#line 64 "array_of_struct.pgc" +#line 89 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 64 "array_of_struct.pgc" +#line 89 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 64 "array_of_struct.pgc" +#line 89 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 64 "array_of_struct.pgc" +#line 89 "array_of_struct.pgc" printf("\ncusts2:\n"); for (r = 0; r < 2; r++) @@ -216,16 +251,16 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_short,&(inds->name_ind),(long)1,(long)10,sizeof( struct ind ), ECPGt_int,&(custs3->phone),(long)1,(long)10,sizeof( struct customer3 ), ECPGt_short,&(inds->phone_ind),(long)1,(long)10,sizeof( struct ind ), ECPGt_EORT); -#line 72 "array_of_struct.pgc" +#line 97 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 72 "array_of_struct.pgc" +#line 97 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 72 "array_of_struct.pgc" +#line 97 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 72 "array_of_struct.pgc" +#line 97 "array_of_struct.pgc" printf("\ncusts3:\n"); for (r = 0; r < 2; r++) @@ -239,16 +274,16 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_short,&(inds[0].name_ind),(long)1,(long)1,sizeof( struct ind ), ECPGt_int,&(custs4.phone),(long)1,(long)1,sizeof( struct customer4 ), ECPGt_short,&(inds[0].phone_ind),(long)1,(long)1,sizeof( struct ind ), ECPGt_EORT); -#line 80 "array_of_struct.pgc" +#line 105 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 80 "array_of_struct.pgc" +#line 105 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 80 "array_of_struct.pgc" +#line 105 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 80 "array_of_struct.pgc" +#line 105 "array_of_struct.pgc" printf("\ncusts4:\n"); printf( "name - %s\n", custs4.name.arr ); @@ -257,16 +292,16 @@ if (sqlca.sqlcode < 0) sqlprint();} { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select c from customers limit 2", ECPGt_EOIT, ECPGt_varchar,(onlyname),(long)50,(long)2,sizeof(struct varchar_5), ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); -#line 85 "array_of_struct.pgc" +#line 110 "array_of_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 85 "array_of_struct.pgc" +#line 110 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 85 "array_of_struct.pgc" +#line 110 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 85 "array_of_struct.pgc" +#line 110 "array_of_struct.pgc" printf("\nname:\n"); for (r = 0; r < 2; r++) @@ -275,13 +310,13 @@ if (sqlca.sqlcode < 0) sqlprint();} } { ECPGdisconnect(__LINE__, "ALL"); -#line 92 "array_of_struct.pgc" +#line 117 "array_of_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 92 "array_of_struct.pgc" +#line 117 "array_of_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 92 "array_of_struct.pgc" +#line 117 "array_of_struct.pgc" return 0; diff --git a/src/interfaces/ecpg/test/expected/preproc-array_of_struct.stderr b/src/interfaces/ecpg/test/expected/preproc-array_of_struct.stderr index 64aa4627cc410..fe4b5adf35ccd 100644 --- a/src/interfaces/ecpg/test/expected/preproc-array_of_struct.stderr +++ b/src/interfaces/ecpg/test/expected/preproc-array_of_struct.stderr @@ -2,85 +2,93 @@ [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ECPGconnect: opening database ecpg1_regression on port [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 52: query: create table customers ( c varchar ( 50 ) , p int ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 61: query: create table customers ( c varchar ( 50 ) , p int ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 52: using PQexec +[NO_PID]: ecpg_execute on line 61: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 52: OK: CREATE TABLE +[NO_PID]: ecpg_process_output on line 61: OK: CREATE TABLE [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 53: query: insert into customers values ( 'John Doe' , '12345' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 72: query: insert into customers values ( $1 , $2 ); with 2 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 53: using PQexec +[NO_PID]: ecpg_execute on line 72: using PQexecParams [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 53: OK: INSERT 0 1 +[NO_PID]: ecpg_free_params on line 72: parameter 1 = John Doe [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 54: query: insert into customers values ( 'Jane Doe' , '67890' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_free_params on line 72: parameter 2 = 12345 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 54: using PQexec +[NO_PID]: ecpg_process_output on line 72: OK: INSERT 0 1 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 54: OK: INSERT 0 1 +[NO_PID]: ecpg_execute on line 74: query: insert into customers values ( $1 , $2 ); with 2 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 56: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 74: using PQexecParams [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 56: using PQexec +[NO_PID]: ecpg_free_params on line 74: parameter 1 = Jane Doe [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 56: correctly got 2 tuples with 2 fields +[NO_PID]: ecpg_free_params on line 74: parameter 2 = 67890 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 56: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_process_output on line 74: OK: INSERT 0 1 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 56: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 81: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 56: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 81: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 56: RESULT: 67890 offset: -1; array: no +[NO_PID]: ecpg_process_output on line 81: correctly got 2 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 64: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_get_data on line 81: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 64: using PQexec +[NO_PID]: ecpg_get_data on line 81: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 64: correctly got 2 tuples with 2 fields +[NO_PID]: ecpg_get_data on line 81: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 64: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 81: RESULT: 67890 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 64: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 89: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 64: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 89: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 64: RESULT: 67890 offset: -1; array: no +[NO_PID]: ecpg_process_output on line 89: correctly got 2 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 72: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_get_data on line 89: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 72: using PQexec +[NO_PID]: ecpg_get_data on line 89: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 72: correctly got 2 tuples with 2 fields +[NO_PID]: ecpg_get_data on line 89: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 72: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 89: RESULT: 67890 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 72: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 97: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 72: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 97: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 72: RESULT: 67890 offset: -1; array: no +[NO_PID]: ecpg_process_output on line 97: correctly got 2 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 80: query: select * from customers limit 1; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_get_data on line 97: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 80: using PQexec +[NO_PID]: ecpg_get_data on line 97: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 80: correctly got 1 tuples with 2 fields +[NO_PID]: ecpg_get_data on line 97: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 80: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 97: RESULT: 67890 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 80: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 105: query: select * from customers limit 1; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 85: query: select c from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 105: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 85: using PQexec +[NO_PID]: ecpg_process_output on line 105: correctly got 1 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 85: correctly got 2 tuples with 1 fields +[NO_PID]: ecpg_get_data on line 105: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 85: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 105: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 85: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 110: query: select c from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 110: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 110: correctly got 2 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 110: RESULT: John Doe offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 110: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_finish: connection ecpg1_regression closed [NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/preproc-autoprep.c b/src/interfaces/ecpg/test/expected/preproc-autoprep.c index 8ed5a2ca2c148..9f811c200a921 100644 --- a/src/interfaces/ecpg/test/expected/preproc-autoprep.c +++ b/src/interfaces/ecpg/test/expected/preproc-autoprep.c @@ -248,7 +248,7 @@ if (sqlca.sqlcode < 0) sqlprint();} } -int main() { +int main(void) { test(); test(); /* retry */ diff --git a/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.c b/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.c index 7b1f58e835ff4..aa72604051fef 100644 --- a/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.c +++ b/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.c @@ -51,7 +51,14 @@ typedef struct ind { #line 22 "pointer_to_struct.pgc" -int main() +typedef struct { +#line 27 "pointer_to_struct.pgc" + customer * customers ; + } company ; +#line 28 "pointer_to_struct.pgc" + + +int main(void) { /* exec sql begin declare section */ @@ -61,14 +68,14 @@ int main() typedef struct { -#line 31 "pointer_to_struct.pgc" +#line 37 "pointer_to_struct.pgc" struct varchar_2 { int len; char arr[ 50 ]; } name ; -#line 32 "pointer_to_struct.pgc" +#line 38 "pointer_to_struct.pgc" int phone ; } customer2 ; -#line 33 "pointer_to_struct.pgc" +#line 39 "pointer_to_struct.pgc" @@ -85,105 +92,134 @@ int main() + + -#line 27 "pointer_to_struct.pgc" +#line 33 "pointer_to_struct.pgc" customer * custs1 = ( customer * ) malloc ( sizeof ( customer ) * 10 ) ; -#line 28 "pointer_to_struct.pgc" +#line 34 "pointer_to_struct.pgc" cust_ind * inds = ( cust_ind * ) malloc ( sizeof ( cust_ind ) * 10 ) ; -#line 34 "pointer_to_struct.pgc" +#line 40 "pointer_to_struct.pgc" customer2 * custs2 = ( customer2 * ) malloc ( sizeof ( customer2 ) * 10 ) ; -#line 40 "pointer_to_struct.pgc" +#line 46 "pointer_to_struct.pgc" struct customer3 { -#line 38 "pointer_to_struct.pgc" +#line 44 "pointer_to_struct.pgc" char name [ 50 ] ; -#line 39 "pointer_to_struct.pgc" +#line 45 "pointer_to_struct.pgc" int phone ; } * custs3 = ( struct customer3 * ) malloc ( sizeof ( struct customer3 ) * 10 ) ; -#line 46 "pointer_to_struct.pgc" +#line 52 "pointer_to_struct.pgc" struct customer4 { -#line 44 "pointer_to_struct.pgc" +#line 50 "pointer_to_struct.pgc" struct varchar_3 { int len; char arr[ 50 ]; } name ; -#line 45 "pointer_to_struct.pgc" +#line 51 "pointer_to_struct.pgc" int phone ; } * custs4 = ( struct customer4 * ) malloc ( sizeof ( struct customer4 ) ) ; -#line 48 "pointer_to_struct.pgc" +#line 54 "pointer_to_struct.pgc" + company acme ; + +#line 56 "pointer_to_struct.pgc" int r ; -#line 49 "pointer_to_struct.pgc" +#line 57 "pointer_to_struct.pgc" struct varchar_4 { int len; char arr[ 50 ]; } onlyname [ 2 ] ; /* exec sql end declare section */ -#line 50 "pointer_to_struct.pgc" +#line 58 "pointer_to_struct.pgc" ECPGdebug(1, stderr); + acme.customers = (customer *) malloc(sizeof(customer) * 10); + { ECPGconnect(__LINE__, 0, "ecpg1_regression" , NULL, NULL , NULL, 0); -#line 54 "pointer_to_struct.pgc" +#line 64 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 54 "pointer_to_struct.pgc" +#line 64 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 54 "pointer_to_struct.pgc" +#line 64 "pointer_to_struct.pgc" { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create table customers ( c varchar ( 50 ) , p int )", ECPGt_EOIT, ECPGt_EORT); -#line 56 "pointer_to_struct.pgc" +#line 66 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 56 "pointer_to_struct.pgc" +#line 66 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 56 "pointer_to_struct.pgc" +#line 66 "pointer_to_struct.pgc" - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( 'John Doe' , '12345' )", ECPGt_EOIT, ECPGt_EORT); -#line 57 "pointer_to_struct.pgc" + + /* First we'll insert some data using C variable references */ + strcpy(custs1[0].name.arr, "John Doe"); + custs1[0].name.len = strlen(custs1[0].name.arr); + custs1[0].phone = 12345; + + strcpy(acme.customers[1].name.arr, "Jane Doe"); + acme.customers[1].name.len = strlen(acme.customers[1].name.arr); + acme.customers[1].phone = 67890; + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( $1 , $2 )", + ECPGt_varchar,&(custs1->name),(long)50,(long)1,sizeof(struct varchar_1), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(custs1[0].phone),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, ECPGt_EORT); +#line 78 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 57 "pointer_to_struct.pgc" +#line 78 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 57 "pointer_to_struct.pgc" +#line 78 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 57 "pointer_to_struct.pgc" +#line 78 "pointer_to_struct.pgc" - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( 'Jane Doe' , '67890' )", ECPGt_EOIT, ECPGt_EORT); -#line 58 "pointer_to_struct.pgc" + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( $1 , $2 )", + ECPGt_varchar,&(acme.customers[1].name),(long)50,(long)1,sizeof(struct varchar_1), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(acme.customers[1].phone),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, ECPGt_EORT); +#line 80 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 58 "pointer_to_struct.pgc" +#line 80 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 58 "pointer_to_struct.pgc" +#line 80 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 58 "pointer_to_struct.pgc" +#line 80 "pointer_to_struct.pgc" + + /* Clear the array, to be sure reading back into it actually gets data */ + memset(custs1, 0, sizeof(customer) * 10); + /* Now read back the data */ { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select * from customers limit 2", ECPGt_EOIT, ECPGt_varchar,&(custs1->name),(long)50,(long)-1,sizeof( customer ), ECPGt_short,&(inds->name_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_int,&(custs1->phone),(long)1,(long)-1,sizeof( customer ), ECPGt_short,&(inds->phone_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_EORT); -#line 60 "pointer_to_struct.pgc" +#line 86 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 60 "pointer_to_struct.pgc" +#line 86 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 60 "pointer_to_struct.pgc" +#line 86 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 60 "pointer_to_struct.pgc" +#line 86 "pointer_to_struct.pgc" printf("custs1:\n"); for (r = 0; r < 2; r++) @@ -197,16 +233,16 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_short,&(inds->name_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_int,&(custs2->phone),(long)1,(long)-1,sizeof( customer2 ), ECPGt_short,&(inds->phone_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_EORT); -#line 68 "pointer_to_struct.pgc" +#line 94 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 68 "pointer_to_struct.pgc" +#line 94 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 68 "pointer_to_struct.pgc" +#line 94 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 68 "pointer_to_struct.pgc" +#line 94 "pointer_to_struct.pgc" printf("\ncusts2:\n"); for (r = 0; r < 2; r++) @@ -220,16 +256,16 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_short,&(inds->name_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_int,&(custs3->phone),(long)1,(long)-1,sizeof( struct customer3 ), ECPGt_short,&(inds->phone_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_EORT); -#line 76 "pointer_to_struct.pgc" +#line 102 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 76 "pointer_to_struct.pgc" +#line 102 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 76 "pointer_to_struct.pgc" +#line 102 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 76 "pointer_to_struct.pgc" +#line 102 "pointer_to_struct.pgc" printf("\ncusts3:\n"); for (r = 0; r < 2; r++) @@ -243,16 +279,16 @@ if (sqlca.sqlcode < 0) sqlprint();} ECPGt_short,&(inds->name_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_int,&(custs4->phone),(long)1,(long)-1,sizeof( struct customer4 ), ECPGt_short,&(inds->phone_ind),(long)1,(long)-1,sizeof( struct ind ), ECPGt_EORT); -#line 84 "pointer_to_struct.pgc" +#line 110 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 84 "pointer_to_struct.pgc" +#line 110 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 84 "pointer_to_struct.pgc" +#line 110 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 84 "pointer_to_struct.pgc" +#line 110 "pointer_to_struct.pgc" printf("\ncusts4:\n"); printf( "name - %s\n", custs4->name.arr ); @@ -261,16 +297,16 @@ if (sqlca.sqlcode < 0) sqlprint();} { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select c from customers limit 2", ECPGt_EOIT, ECPGt_varchar,(onlyname),(long)50,(long)2,sizeof(struct varchar_4), ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); -#line 89 "pointer_to_struct.pgc" +#line 115 "pointer_to_struct.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) sqlprint(); -#line 89 "pointer_to_struct.pgc" +#line 115 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 89 "pointer_to_struct.pgc" +#line 115 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 89 "pointer_to_struct.pgc" +#line 115 "pointer_to_struct.pgc" printf("\nname:\n"); for (r = 0; r < 2; r++) @@ -279,13 +315,13 @@ if (sqlca.sqlcode < 0) sqlprint();} } { ECPGdisconnect(__LINE__, "ALL"); -#line 96 "pointer_to_struct.pgc" +#line 122 "pointer_to_struct.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 96 "pointer_to_struct.pgc" +#line 122 "pointer_to_struct.pgc" if (sqlca.sqlcode < 0) sqlprint();} -#line 96 "pointer_to_struct.pgc" +#line 122 "pointer_to_struct.pgc" /* All the memory will anyway be freed at the end */ diff --git a/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.stderr b/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.stderr index 707640860b838..a561583936be1 100644 --- a/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.stderr +++ b/src/interfaces/ecpg/test/expected/preproc-pointer_to_struct.stderr @@ -2,85 +2,93 @@ [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ECPGconnect: opening database ecpg1_regression on port [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 56: query: create table customers ( c varchar ( 50 ) , p int ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 66: query: create table customers ( c varchar ( 50 ) , p int ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 56: using PQexec +[NO_PID]: ecpg_execute on line 66: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 56: OK: CREATE TABLE +[NO_PID]: ecpg_process_output on line 66: OK: CREATE TABLE [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 57: query: insert into customers values ( 'John Doe' , '12345' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 77: query: insert into customers values ( $1 , $2 ); with 2 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 57: using PQexec +[NO_PID]: ecpg_execute on line 77: using PQexecParams [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 57: OK: INSERT 0 1 +[NO_PID]: ecpg_free_params on line 77: parameter 1 = John Doe [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 58: query: insert into customers values ( 'Jane Doe' , '67890' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_free_params on line 77: parameter 2 = 12345 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 58: using PQexec +[NO_PID]: ecpg_process_output on line 77: OK: INSERT 0 1 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 58: OK: INSERT 0 1 +[NO_PID]: ecpg_execute on line 79: query: insert into customers values ( $1 , $2 ); with 2 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 60: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 79: using PQexecParams [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 60: using PQexec +[NO_PID]: ecpg_free_params on line 79: parameter 1 = Jane Doe [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 60: correctly got 2 tuples with 2 fields +[NO_PID]: ecpg_free_params on line 79: parameter 2 = 67890 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 60: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_process_output on line 79: OK: INSERT 0 1 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 60: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 86: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 60: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 86: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 60: RESULT: 67890 offset: -1; array: no +[NO_PID]: ecpg_process_output on line 86: correctly got 2 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 68: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_get_data on line 86: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 68: using PQexec +[NO_PID]: ecpg_get_data on line 86: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 68: correctly got 2 tuples with 2 fields +[NO_PID]: ecpg_get_data on line 86: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 68: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 86: RESULT: 67890 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 68: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 94: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 68: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 94: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 68: RESULT: 67890 offset: -1; array: no +[NO_PID]: ecpg_process_output on line 94: correctly got 2 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 76: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_get_data on line 94: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 76: using PQexec +[NO_PID]: ecpg_get_data on line 94: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 76: correctly got 2 tuples with 2 fields +[NO_PID]: ecpg_get_data on line 94: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 76: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 94: RESULT: 67890 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 76: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 102: query: select * from customers limit 2; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 76: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 102: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 76: RESULT: 67890 offset: -1; array: no +[NO_PID]: ecpg_process_output on line 102: correctly got 2 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 84: query: select * from customers limit 1; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_get_data on line 102: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 84: using PQexec +[NO_PID]: ecpg_get_data on line 102: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 84: correctly got 1 tuples with 2 fields +[NO_PID]: ecpg_get_data on line 102: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 84: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 102: RESULT: 67890 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 84: RESULT: 12345 offset: -1; array: no +[NO_PID]: ecpg_execute on line 110: query: select * from customers limit 1; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 89: query: select c from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 110: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 89: using PQexec +[NO_PID]: ecpg_process_output on line 110: correctly got 1 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 89: correctly got 2 tuples with 1 fields +[NO_PID]: ecpg_get_data on line 110: RESULT: John Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 89: RESULT: John Doe offset: -1; array: no +[NO_PID]: ecpg_get_data on line 110: RESULT: 12345 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 89: RESULT: Jane Doe offset: -1; array: no +[NO_PID]: ecpg_execute on line 115: query: select c from customers limit 2; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 115: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 115: correctly got 2 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 115: RESULT: John Doe offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 115: RESULT: Jane Doe offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_finish: connection ecpg1_regression closed [NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/preproc-strings.c b/src/interfaces/ecpg/test/expected/preproc-strings.c index a26817968de79..06a86a2bfae4b 100644 --- a/src/interfaces/ecpg/test/expected/preproc-strings.c +++ b/src/interfaces/ecpg/test/expected/preproc-strings.c @@ -18,6 +18,16 @@ #line 3 "strings.pgc" /* exec sql begin declare section */ #line 1 "strings.h" +/* redundant declaration to silence -Wmissing-variable-declarations */ + + + + + + + + + @@ -29,7 +39,10 @@ #line 5 "strings.pgc" -#line 1 "strings.h" +#line 2 "strings.h" + extern char * s1 , * s2 , * s3 , * s4 , * s5 , * s6 , * s7 , * s8 ; + +#line 11 "strings.h" char * s1 , * s2 , * s3 , * s4 , * s5 , * s6 , * s7 , * s8 ; /* exec sql end declare section */ #line 5 "strings.pgc" diff --git a/src/interfaces/ecpg/test/expected/sql-code100.c b/src/interfaces/ecpg/test/expected/sql-code100.c index 4c85530a17afa..f05a9540b0e8c 100644 --- a/src/interfaces/ecpg/test/expected/sql-code100.c +++ b/src/interfaces/ecpg/test/expected/sql-code100.c @@ -92,7 +92,7 @@ struct sqlca_t *ECPGget_sqlca(void); -int main() +int main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-copystdout.c b/src/interfaces/ecpg/test/expected/sql-copystdout.c index d2599fb0e9398..f875036f478ed 100644 --- a/src/interfaces/ecpg/test/expected/sql-copystdout.c +++ b/src/interfaces/ecpg/test/expected/sql-copystdout.c @@ -96,7 +96,7 @@ struct sqlca_t *ECPGget_sqlca(void); int -main () +main(void) { ECPGdebug (1, stderr); diff --git a/src/interfaces/ecpg/test/expected/sql-declare.c b/src/interfaces/ecpg/test/expected/sql-declare.c index 6248d992175b1..2a59313bf469b 100644 --- a/src/interfaces/ecpg/test/expected/sql-declare.c +++ b/src/interfaces/ecpg/test/expected/sql-declare.c @@ -578,7 +578,7 @@ if (sqlca.sqlcode < 0) sqlprint();} printf("count: %d, length: %d, data: %s\n", count, length, f3[0]); } -void commitTable() +void commitTable(void) { { ECPGtrans(__LINE__, "con1", "commit"); #line 187 "declare.pgc" @@ -597,7 +597,7 @@ if (sqlca.sqlcode < 0) sqlprint();} /* * reset all the output variables */ -void reset() +void reset(void) { memset(f1, 0, sizeof(f1)); memset(f2, 0, sizeof(f2)); diff --git a/src/interfaces/ecpg/test/expected/sql-dyntest.c b/src/interfaces/ecpg/test/expected/sql-dyntest.c index 513d44c63045c..8eccfbfc1b525 100644 --- a/src/interfaces/ecpg/test/expected/sql-dyntest.c +++ b/src/interfaces/ecpg/test/expected/sql-dyntest.c @@ -151,7 +151,7 @@ error (void) } int -main () +main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-fetch.c b/src/interfaces/ecpg/test/expected/sql-fetch.c index ca7d14e97c4c3..0aea4a9192af5 100644 --- a/src/interfaces/ecpg/test/expected/sql-fetch.c +++ b/src/interfaces/ecpg/test/expected/sql-fetch.c @@ -22,7 +22,7 @@ #line 5 "fetch.pgc" -int main() { +int main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-func.c b/src/interfaces/ecpg/test/expected/sql-func.c index 17c5d26ea4198..fa590bd16cab1 100644 --- a/src/interfaces/ecpg/test/expected/sql-func.c +++ b/src/interfaces/ecpg/test/expected/sql-func.c @@ -22,7 +22,7 @@ #line 5 "func.pgc" -int main() { +int main(void) { #line 8 "func.pgc" char text [ 25 ] ; diff --git a/src/interfaces/ecpg/test/expected/sql-indicators.c b/src/interfaces/ecpg/test/expected/sql-indicators.c index 7cf43ad6228cf..796bd906af668 100644 --- a/src/interfaces/ecpg/test/expected/sql-indicators.c +++ b/src/interfaces/ecpg/test/expected/sql-indicators.c @@ -91,7 +91,7 @@ struct sqlca_t *ECPGget_sqlca(void); #line 4 "indicators.pgc" -int main() +int main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-insupd.c b/src/interfaces/ecpg/test/expected/sql-insupd.c index 5f73bf566ff4f..dde2bdc9688b3 100644 --- a/src/interfaces/ecpg/test/expected/sql-insupd.c +++ b/src/interfaces/ecpg/test/expected/sql-insupd.c @@ -22,7 +22,7 @@ #line 5 "insupd.pgc" -int main() { +int main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-parser.c b/src/interfaces/ecpg/test/expected/sql-parser.c index 32bb2c212608f..ba53ab03309f8 100644 --- a/src/interfaces/ecpg/test/expected/sql-parser.c +++ b/src/interfaces/ecpg/test/expected/sql-parser.c @@ -23,7 +23,7 @@ #line 6 "parser.pgc" -int main() { +int main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-quote.c b/src/interfaces/ecpg/test/expected/sql-quote.c index 05841bd6999d5..3001df4069f17 100644 --- a/src/interfaces/ecpg/test/expected/sql-quote.c +++ b/src/interfaces/ecpg/test/expected/sql-quote.c @@ -22,7 +22,7 @@ #line 5 "quote.pgc" -int main() { +int main(void) { /* exec sql begin declare section */ @@ -61,7 +61,7 @@ if (sqlca.sqlcode < 0) exit (1);} #line 20 "quote.pgc" - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "set standard_conforming_strings to off", ECPGt_EOIT, ECPGt_EORT); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "set standard_conforming_strings to on", ECPGt_EOIT, ECPGt_EORT); #line 22 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); @@ -84,8 +84,8 @@ if (sqlca.sqlcode < 0) exit (1);} printf("Standard conforming strings: %s\n", var); - /* this is a\\b actually */ - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into \"My_Table\" values ( 1 , 'a\\\\\\\\b' )", ECPGt_EOIT, ECPGt_EORT); + /* this is a\\\\b actually */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into \"My_Table\" values ( 2 , 'a\\\\\\\\b' )", ECPGt_EOIT, ECPGt_EORT); #line 28 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); @@ -95,7 +95,7 @@ if (sqlca.sqlcode < 0) exit (1);} #line 28 "quote.pgc" /* this is a\\b */ - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into \"My_Table\" values ( 1 , E'a\\\\\\\\b' )", ECPGt_EOIT, ECPGt_EORT); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into \"My_Table\" values ( 2 , E'a\\\\\\\\b' )", ECPGt_EOIT, ECPGt_EORT); #line 30 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); @@ -105,7 +105,7 @@ if (sqlca.sqlcode < 0) exit (1);} #line 30 "quote.pgc" - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "set standard_conforming_strings to on", ECPGt_EOIT, ECPGt_EORT); + { ECPGtrans(__LINE__, NULL, "begin"); #line 32 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); @@ -114,66 +114,22 @@ if (sqlca.sqlwarn[0] == 'W') sqlprint(); if (sqlca.sqlcode < 0) exit (1);} #line 32 "quote.pgc" - - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "show standard_conforming_strings", ECPGt_EOIT, - ECPGt_char,(var),(long)25,(long)1,(25)*sizeof(char), - ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); -#line 34 "quote.pgc" - -if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 34 "quote.pgc" - -if (sqlca.sqlcode < 0) exit (1);} -#line 34 "quote.pgc" - - printf("Standard conforming strings: %s\n", var); - - /* this is a\\\\b actually */ - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into \"My_Table\" values ( 2 , 'a\\\\\\\\b' )", ECPGt_EOIT, ECPGt_EORT); -#line 38 "quote.pgc" - -if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 38 "quote.pgc" - -if (sqlca.sqlcode < 0) exit (1);} -#line 38 "quote.pgc" - - /* this is a\\b */ - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into \"My_Table\" values ( 2 , E'a\\\\\\\\b' )", ECPGt_EOIT, ECPGt_EORT); -#line 40 "quote.pgc" - -if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 40 "quote.pgc" - -if (sqlca.sqlcode < 0) exit (1);} -#line 40 "quote.pgc" - - - { ECPGtrans(__LINE__, NULL, "begin"); -#line 42 "quote.pgc" - -if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 42 "quote.pgc" - -if (sqlca.sqlcode < 0) exit (1);} -#line 42 "quote.pgc" - /* declare C cursor for select * from \"My_Table\" */ -#line 43 "quote.pgc" +#line 33 "quote.pgc" { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "declare C cursor for select * from \"My_Table\"", ECPGt_EOIT, ECPGt_EORT); -#line 45 "quote.pgc" +#line 35 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 45 "quote.pgc" +#line 35 "quote.pgc" if (sqlca.sqlcode < 0) exit (1);} -#line 45 "quote.pgc" +#line 35 "quote.pgc" /* exec sql whenever not found break ; */ -#line 47 "quote.pgc" +#line 37 "quote.pgc" for (loopcount = 0; loopcount < 100; loopcount++) @@ -183,47 +139,47 @@ if (sqlca.sqlcode < 0) exit (1);} ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_char,(var),(long)25,(long)1,(25)*sizeof(char), ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); -#line 51 "quote.pgc" +#line 41 "quote.pgc" if (sqlca.sqlcode == ECPG_NOT_FOUND) break; -#line 51 "quote.pgc" +#line 41 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 51 "quote.pgc" +#line 41 "quote.pgc" if (sqlca.sqlcode < 0) exit (1);} -#line 51 "quote.pgc" +#line 41 "quote.pgc" printf("value: %d %s\n", i, var); } { ECPGtrans(__LINE__, NULL, "rollback"); -#line 55 "quote.pgc" +#line 45 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 55 "quote.pgc" +#line 45 "quote.pgc" if (sqlca.sqlcode < 0) exit (1);} -#line 55 "quote.pgc" +#line 45 "quote.pgc" { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "drop table \"My_Table\"", ECPGt_EOIT, ECPGt_EORT); -#line 56 "quote.pgc" +#line 46 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 56 "quote.pgc" +#line 46 "quote.pgc" if (sqlca.sqlcode < 0) exit (1);} -#line 56 "quote.pgc" +#line 46 "quote.pgc" { ECPGdisconnect(__LINE__, "ALL"); -#line 58 "quote.pgc" +#line 48 "quote.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); -#line 58 "quote.pgc" +#line 48 "quote.pgc" if (sqlca.sqlcode < 0) exit (1);} -#line 58 "quote.pgc" +#line 48 "quote.pgc" return 0; diff --git a/src/interfaces/ecpg/test/expected/sql-quote.stderr b/src/interfaces/ecpg/test/expected/sql-quote.stderr index 3df8702a8a6b7..5f5bfb8133a2a 100644 --- a/src/interfaces/ecpg/test/expected/sql-quote.stderr +++ b/src/interfaces/ecpg/test/expected/sql-quote.stderr @@ -10,7 +10,7 @@ [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_process_output on line 20: OK: CREATE TABLE [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 22: query: set standard_conforming_strings to off; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 22: query: set standard_conforming_strings to on; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_execute on line 22: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 @@ -22,114 +22,63 @@ [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_process_output on line 24: correctly got 1 tuples with 1 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 24: RESULT: off offset: -1; array: no +[NO_PID]: ecpg_get_data on line 24: RESULT: on offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 28: query: insert into "My_Table" values ( 1 , 'a\\\\b' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 28: query: insert into "My_Table" values ( 2 , 'a\\\\b' ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ECPGnoticeReceiver: nonstandard use of \\ in a string literal -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: raising sqlcode 0 -[NO_PID]: sqlca: code: 0, state: 22P06 [NO_PID]: ecpg_execute on line 28: using PQexec -[NO_PID]: sqlca: code: 0, state: 22P06 +[NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_process_output on line 28: OK: INSERT 0 1 -[NO_PID]: sqlca: code: 0, state: 22P06 -SQL error: nonstandard use of \\ in a string literal -[NO_PID]: ecpg_execute on line 30: query: insert into "My_Table" values ( 1 , E'a\\\\b' ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 30: query: insert into "My_Table" values ( 2 , E'a\\\\b' ); with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_execute on line 30: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_process_output on line 30: OK: INSERT 0 1 [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 32: query: set standard_conforming_strings to on; with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 32: using PQexec -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 32: OK: SET -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 34: query: show standard_conforming_strings; with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 34: using PQexec -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 34: correctly got 1 tuples with 1 fields -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 34: RESULT: on offset: -1; array: no -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 38: query: insert into "My_Table" values ( 2 , 'a\\\\b' ); with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 38: using PQexec -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 38: OK: INSERT 0 1 -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 40: query: insert into "My_Table" values ( 2 , E'a\\\\b' ); with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 40: using PQexec -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 40: OK: INSERT 0 1 -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ECPGtrans on line 42: action "begin"; connection "ecpg1_regression" -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 45: query: declare C cursor for select * from "My_Table"; with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 45: using PQexec -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 45: OK: DECLARE CURSOR -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: query: fetch C; with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: using PQexec -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 51: correctly got 1 tuples with 2 fields -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: 1 offset: -1; array: no -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: a\\b offset: -1; array: no -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: query: fetch C; with 0 parameter(s) on connection ecpg1_regression -[NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: using PQexec +[NO_PID]: ECPGtrans on line 32: action "begin"; connection "ecpg1_regression" [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 51: correctly got 1 tuples with 2 fields +[NO_PID]: ecpg_execute on line 35: query: declare C cursor for select * from "My_Table"; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: 1 offset: -1; array: no +[NO_PID]: ecpg_execute on line 35: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: a\\b offset: -1; array: no +[NO_PID]: ecpg_process_output on line 35: OK: DECLARE CURSOR [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: query: fetch C; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 41: query: fetch C; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: using PQexec +[NO_PID]: ecpg_execute on line 41: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 51: correctly got 1 tuples with 2 fields +[NO_PID]: ecpg_process_output on line 41: correctly got 1 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: 2 offset: -1; array: no +[NO_PID]: ecpg_get_data on line 41: RESULT: 2 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: a\\\\b offset: -1; array: no +[NO_PID]: ecpg_get_data on line 41: RESULT: a\\\\b offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: query: fetch C; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 41: query: fetch C; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: using PQexec +[NO_PID]: ecpg_execute on line 41: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 51: correctly got 1 tuples with 2 fields +[NO_PID]: ecpg_process_output on line 41: correctly got 1 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: 2 offset: -1; array: no +[NO_PID]: ecpg_get_data on line 41: RESULT: 2 offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 51: RESULT: a\\b offset: -1; array: no +[NO_PID]: ecpg_get_data on line 41: RESULT: a\\b offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: query: fetch C; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 41: query: fetch C; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 51: using PQexec +[NO_PID]: ecpg_execute on line 41: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 51: correctly got 0 tuples with 2 fields +[NO_PID]: ecpg_process_output on line 41: correctly got 0 tuples with 2 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: raising sqlcode 100 on line 51: no data found on line 51 +[NO_PID]: raising sqlcode 100 on line 41: no data found on line 41 [NO_PID]: sqlca: code: 100, state: 02000 -[NO_PID]: ECPGtrans on line 55: action "rollback"; connection "ecpg1_regression" +[NO_PID]: ECPGtrans on line 45: action "rollback"; connection "ecpg1_regression" [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 56: query: drop table "My_Table"; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 46: query: drop table "My_Table"; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 56: using PQexec +[NO_PID]: ecpg_execute on line 46: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_process_output on line 56: OK: DROP TABLE +[NO_PID]: ecpg_process_output on line 46: OK: DROP TABLE [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_finish: connection ecpg1_regression closed [NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/sql-quote.stdout b/src/interfaces/ecpg/test/expected/sql-quote.stdout index 2f92f994831de..31892d5261e19 100644 --- a/src/interfaces/ecpg/test/expected/sql-quote.stdout +++ b/src/interfaces/ecpg/test/expected/sql-quote.stdout @@ -1,6 +1,3 @@ -Standard conforming strings: off Standard conforming strings: on -value: 1 a\\b -value: 1 a\\b value: 2 a\\\\b value: 2 a\\b diff --git a/src/interfaces/ecpg/test/expected/sql-show.c b/src/interfaces/ecpg/test/expected/sql-show.c index 1b52d5eaf4e57..f4add40743913 100644 --- a/src/interfaces/ecpg/test/expected/sql-show.c +++ b/src/interfaces/ecpg/test/expected/sql-show.c @@ -22,7 +22,7 @@ #line 5 "show.pgc" -int main() { +int main(void) { /* exec sql begin declare section */ @@ -90,7 +90,7 @@ if (sqlca.sqlcode < 0) sqlprint();} printf("Var: Search path: %s\n", var); - { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "set standard_conforming_strings to off", ECPGt_EOIT, ECPGt_EORT); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "set standard_conforming_strings to on", ECPGt_EOIT, ECPGt_EORT); #line 26 "show.pgc" if (sqlca.sqlwarn[0] == 'W') sqlprint(); diff --git a/src/interfaces/ecpg/test/expected/sql-show.stderr b/src/interfaces/ecpg/test/expected/sql-show.stderr index c303a845b25d6..e906abad91ea0 100644 --- a/src/interfaces/ecpg/test/expected/sql-show.stderr +++ b/src/interfaces/ecpg/test/expected/sql-show.stderr @@ -30,7 +30,7 @@ [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_get_data on line 23: RESULT: public offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_execute on line 26: query: set standard_conforming_strings to off; with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: ecpg_execute on line 26: query: set standard_conforming_strings to on; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_execute on line 26: using PQexec [NO_PID]: sqlca: code: 0, state: 00000 @@ -42,7 +42,7 @@ [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_process_output on line 27: correctly got 1 tuples with 1 fields [NO_PID]: sqlca: code: 0, state: 00000 -[NO_PID]: ecpg_get_data on line 27: RESULT: off offset: -1; array: no +[NO_PID]: ecpg_get_data on line 27: RESULT: on offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_execute on line 30: query: set time zone PST8PDT; with 0 parameter(s) on connection ecpg1_regression [NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/sql-show.stdout b/src/interfaces/ecpg/test/expected/sql-show.stdout index 9319c5d113580..d5628025a0e4e 100644 --- a/src/interfaces/ecpg/test/expected/sql-show.stdout +++ b/src/interfaces/ecpg/test/expected/sql-show.stdout @@ -1,5 +1,5 @@ Var: Search path: public Var: Search path: public -Var: Standard conforming strings: off +Var: Standard conforming strings: on Time Zone: PST8PDT Transaction isolation level: read committed diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.c b/src/interfaces/ecpg/test/expected/sql-sqljson.c index 39221f9ea5dbe..be7fbe19e1344 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.c +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.c @@ -96,7 +96,7 @@ struct sqlca_t *ECPGget_sqlca(void); int -main () +main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson_jsontable.c b/src/interfaces/ecpg/test/expected/sql-sqljson_jsontable.c index b2a0f11eb6e42..cbbc99539cb7a 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson_jsontable.c +++ b/src/interfaces/ecpg/test/expected/sql-sqljson_jsontable.c @@ -96,7 +96,7 @@ struct sqlca_t *ECPGget_sqlca(void); int -main () +main(void) { /* exec sql begin declare section */ diff --git a/src/interfaces/ecpg/test/expected/sql-sqlpgq.c b/src/interfaces/ecpg/test/expected/sql-sqlpgq.c new file mode 100644 index 0000000000000..66dedc20a3e87 --- /dev/null +++ b/src/interfaces/ecpg/test/expected/sql-sqlpgq.c @@ -0,0 +1,285 @@ +/* Processed by ecpg (regression mode) */ +/* These include files are added by the preprocessor */ +#include +#include +#include +/* End of automatic include section */ +#define ECPGdebug(X,Y) ECPGdebug((X)+100,(Y)) + +#line 1 "sqlpgq.pgc" +#include +#include +#include + + +#line 1 "regression.h" + + + + + + +#line 5 "sqlpgq.pgc" + + +/* exec sql whenever sqlerror sqlprint ; */ +#line 7 "sqlpgq.pgc" + + +int +main(void) +{ +/* exec sql begin declare section */ + + + + + +#line 13 "sqlpgq.pgc" + char command [ 512 ] ; + +#line 14 "sqlpgq.pgc" + char search_address [ 10 ] ; + +#line 15 "sqlpgq.pgc" + char cname [ 100 ] ; + +#line 16 "sqlpgq.pgc" + int reg ; +/* exec sql end declare section */ +#line 17 "sqlpgq.pgc" + + + ECPGdebug(1, stderr); + + { ECPGconnect(__LINE__, 0, "ecpg1_regression" , NULL, NULL , "main", 0); +#line 21 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 21 "sqlpgq.pgc" + + + /* Create schema and tables for property graph testing */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create schema graph_ecpg_tests", ECPGt_EOIT, ECPGt_EORT); +#line 24 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 24 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "set search_path = graph_ecpg_tests", ECPGt_EOIT, ECPGt_EORT); +#line 25 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 25 "sqlpgq.pgc" + + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create table customers ( customer_id integer primary key , name varchar , address varchar )", ECPGt_EOIT, ECPGt_EORT); +#line 31 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 31 "sqlpgq.pgc" + + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create table orders ( order_id integer primary key , register integer )", ECPGt_EOIT, ECPGt_EORT); +#line 36 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 36 "sqlpgq.pgc" + + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create table customer_orders ( customer_orders_id integer primary key , customer_id integer references customers ( customer_id ) , order_id integer references orders ( order_id ) )", ECPGt_EOIT, ECPGt_EORT); +#line 42 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 42 "sqlpgq.pgc" + + + /* Insert test data */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customers values ( 1 , 'customer1' , 'US' ) , ( 2 , 'customer2' , 'CA' ) , ( 3 , 'customer3' , 'GL' )", ECPGt_EOIT, ECPGt_EORT); +#line 45 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 45 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into orders values ( 1 , 100 ) , ( 2 , 200 ) , ( 3 , 500 )", ECPGt_EOIT, ECPGt_EORT); +#line 46 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 46 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "insert into customer_orders ( customer_orders_id , customer_id , order_id ) values ( 1 , 1 , 1 ) , ( 2 , 2 , 2 ) , ( 3 , 3 , 3 )", ECPGt_EOIT, ECPGt_EORT); +#line 47 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 47 "sqlpgq.pgc" + + + /* Create property graph */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "create property graph shopgraph vertex tables ( customers , orders ) edge tables ( customer_orders key ( customer_orders_id ) source key ( customer_id ) references customers ( customer_id ) destination key ( order_id ) references orders ( order_id ) )", ECPGt_EOIT, ECPGt_EORT); +#line 59 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 59 "sqlpgq.pgc" + + + { ECPGtrans(__LINE__, NULL, "commit"); +#line 61 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 61 "sqlpgq.pgc" + + + /* direct sql - US customers */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select * from graph_table ( shopgraph match ( c is customers where c . address = 'US' ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) )", ECPGt_EOIT, + ECPGt_char,(cname),(long)100,(long)1,(100)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(reg),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 64 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 64 "sqlpgq.pgc" + + printf("found %ld results (%s, %d)\n", sqlca.sqlerrd[2], cname, reg); + + /* direct sql with C variable - GL customers */ + strcpy(search_address, "GL"); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select * from graph_table ( shopgraph match ( c is customers where c . address = $1 ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) )", + ECPGt_char,(search_address),(long)10,(long)1,(10)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, + ECPGt_char,(cname),(long)100,(long)1,(100)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(reg),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 69 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 69 "sqlpgq.pgc" + + printf("found %ld results (%s, %d)\n", sqlca.sqlerrd[2], cname, reg); + + /* prepared statement - CA customers */ + sprintf(command, "select * from graph_table (shopgraph match (c is customers where c.address = $1)-[is customer_orders]->(o is orders) columns (c.name, o.register))"); + { ECPGprepare(__LINE__, NULL, 0, "graph_stmt", command); +#line 74 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 74 "sqlpgq.pgc" + + strcpy(search_address, "CA"); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_execute, "graph_stmt", + ECPGt_char,(search_address),(long)10,(long)1,(10)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EOIT, + ECPGt_char,(cname),(long)100,(long)1,(100)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(reg),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 76 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 76 "sqlpgq.pgc" + + printf("found %ld results (%s, %d)\n", sqlca.sqlerrd[2], cname, reg); + { ECPGdeallocate(__LINE__, 0, NULL, "graph_stmt"); +#line 78 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 78 "sqlpgq.pgc" + + + /* cursor test - all customers with orders */ + /* declare graph_cursor cursor for select * from graph_table ( shopgraph match ( c is customers ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) ) order by name */ +#line 81 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "declare graph_cursor cursor for select * from graph_table ( shopgraph match ( c is customers ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) ) order by name", ECPGt_EOIT, ECPGt_EORT); +#line 82 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 82 "sqlpgq.pgc" + + /* exec sql whenever not found break ; */ +#line 83 "sqlpgq.pgc" + + while (1) { + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "fetch graph_cursor", ECPGt_EOIT, + ECPGt_char,(cname),(long)100,(long)1,(100)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, + ECPGt_int,&(reg),(long)1,(long)1,sizeof(int), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 85 "sqlpgq.pgc" + +if (sqlca.sqlcode == ECPG_NOT_FOUND) break; +#line 85 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 85 "sqlpgq.pgc" + + printf("cursor result: %s, %d\n", cname, reg); + } + /* exec sql whenever not found continue ; */ +#line 88 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "close graph_cursor", ECPGt_EOIT, ECPGt_EORT); +#line 89 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 89 "sqlpgq.pgc" + + + /* label disjunction syntax test */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select * from graph_table ( shopgraph match ( c is customers | customers where c . address = 'US' ) columns ( c . name ) )", ECPGt_EOIT, + ECPGt_char,(cname),(long)100,(long)1,(100)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 92 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 92 "sqlpgq.pgc" + + printf("found %ld results (%s)\n", sqlca.sqlerrd[2], cname); + + /* Clean up */ + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "drop property graph shopgraph", ECPGt_EOIT, ECPGt_EORT); +#line 96 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 96 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "drop table customer_orders", ECPGt_EOIT, ECPGt_EORT); +#line 97 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 97 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "drop table orders", ECPGt_EOIT, ECPGt_EORT); +#line 98 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 98 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "drop table customers", ECPGt_EOIT, ECPGt_EORT); +#line 99 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 99 "sqlpgq.pgc" + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "drop schema graph_ecpg_tests", ECPGt_EOIT, ECPGt_EORT); +#line 100 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 100 "sqlpgq.pgc" + + { ECPGtrans(__LINE__, NULL, "commit"); +#line 101 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 101 "sqlpgq.pgc" + + { ECPGdisconnect(__LINE__, "CURRENT"); +#line 102 "sqlpgq.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 102 "sqlpgq.pgc" + + + return 0; +} diff --git a/src/interfaces/ecpg/test/expected/sql-sqlpgq.stderr b/src/interfaces/ecpg/test/expected/sql-sqlpgq.stderr new file mode 100644 index 0000000000000..503a344a262a8 --- /dev/null +++ b/src/interfaces/ecpg/test/expected/sql-sqlpgq.stderr @@ -0,0 +1,190 @@ +[NO_PID]: ECPGdebug: set to 1 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ECPGconnect: opening database ecpg1_regression on port +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 24: query: create schema graph_ecpg_tests; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 24: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 24: OK: CREATE SCHEMA +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 25: query: set search_path = graph_ecpg_tests; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 25: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 25: OK: SET +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 27: query: create table customers ( customer_id integer primary key , name varchar , address varchar ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 27: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 27: OK: CREATE TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 33: query: create table orders ( order_id integer primary key , register integer ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 33: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 33: OK: CREATE TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 38: query: create table customer_orders ( customer_orders_id integer primary key , customer_id integer references customers ( customer_id ) , order_id integer references orders ( order_id ) ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 38: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 38: OK: CREATE TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 45: query: insert into customers values ( 1 , 'customer1' , 'US' ) , ( 2 , 'customer2' , 'CA' ) , ( 3 , 'customer3' , 'GL' ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 45: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 45: OK: INSERT 0 3 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 46: query: insert into orders values ( 1 , 100 ) , ( 2 , 200 ) , ( 3 , 500 ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 46: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 46: OK: INSERT 0 3 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 47: query: insert into customer_orders ( customer_orders_id , customer_id , order_id ) values ( 1 , 1 , 1 ) , ( 2 , 2 , 2 ) , ( 3 , 3 , 3 ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 47: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 47: OK: INSERT 0 3 +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 50: query: create property graph shopgraph vertex tables ( customers , orders ) edge tables ( customer_orders key ( customer_orders_id ) source key ( customer_id ) references customers ( customer_id ) destination key ( order_id ) references orders ( order_id ) ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 50: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 50: OK: CREATE PROPERTY GRAPH +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ECPGtrans on line 61: action "commit"; connection "main" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 64: query: select * from graph_table ( shopgraph match ( c is customers where c . address = 'US' ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 64: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 64: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 64: RESULT: customer1 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 64: RESULT: 100 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 69: query: select * from graph_table ( shopgraph match ( c is customers where c . address = $1 ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) ); with 1 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 69: using PQexecParams +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_free_params on line 69: parameter 1 = GL +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 69: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 69: RESULT: customer3 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 69: RESULT: 500 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: prepare_common on line 74: name graph_stmt; query: "select * from graph_table (shopgraph match (c is customers where c.address = $1)-[is customer_orders]->(o is orders) columns (c.name, o.register))" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 76: query: select * from graph_table (shopgraph match (c is customers where c.address = $1)-[is customer_orders]->(o is orders) columns (c.name, o.register)); with 1 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 76: using PQexecPrepared for "select * from graph_table (shopgraph match (c is customers where c.address = $1)-[is customer_orders]->(o is orders) columns (c.name, o.register))" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_free_params on line 76: parameter 1 = CA +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 76: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 76: RESULT: customer2 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 76: RESULT: 200 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: deallocate_one on line 78: name graph_stmt +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 82: query: declare graph_cursor cursor for select * from graph_table ( shopgraph match ( c is customers ) - [ is customer_orders ] -> ( o is orders ) columns ( c . name , o . register ) ) order by name; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 82: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 82: OK: DECLARE CURSOR +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: query: fetch graph_cursor; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 85: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 85: RESULT: customer1 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 85: RESULT: 100 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: query: fetch graph_cursor; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 85: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 85: RESULT: customer2 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 85: RESULT: 200 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: query: fetch graph_cursor; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 85: correctly got 1 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 85: RESULT: customer3 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 85: RESULT: 500 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: query: fetch graph_cursor; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 85: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 85: correctly got 0 tuples with 2 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlcode 100 on line 85: no data found on line 85 +[NO_PID]: sqlca: code: 100, state: 02000 +[NO_PID]: ecpg_execute on line 89: query: close graph_cursor; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 89: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 89: OK: CLOSE CURSOR +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 92: query: select * from graph_table ( shopgraph match ( c is customers | customers where c . address = 'US' ) columns ( c . name ) ); with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 92: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 92: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 92: RESULT: customer1 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 96: query: drop property graph shopgraph; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 96: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 96: OK: DROP PROPERTY GRAPH +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 97: query: drop table customer_orders; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 97: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 97: OK: DROP TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 98: query: drop table orders; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 98: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 98: OK: DROP TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 99: query: drop table customers; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 99: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 99: OK: DROP TABLE +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 100: query: drop schema graph_ecpg_tests; with 0 parameter(s) on connection main +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 100: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 100: OK: DROP SCHEMA +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ECPGtrans on line 101: action "commit"; connection "main" +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_finish: connection main closed +[NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/sql-sqlpgq.stdout b/src/interfaces/ecpg/test/expected/sql-sqlpgq.stdout new file mode 100644 index 0000000000000..430a0fad481bd --- /dev/null +++ b/src/interfaces/ecpg/test/expected/sql-sqlpgq.stdout @@ -0,0 +1,7 @@ +found 1 results (customer1, 100) +found 1 results (customer3, 500) +found 1 results (customer2, 200) +cursor result: customer1, 100 +cursor result: customer2, 200 +cursor result: customer3, 500 +found 1 results (customer1) diff --git a/src/interfaces/ecpg/test/expected/thread-alloc.c b/src/interfaces/ecpg/test/expected/thread-alloc.c index 3b31d27fd3476..912d94c134c2c 100644 --- a/src/interfaces/ecpg/test/expected/thread-alloc.c +++ b/src/interfaces/ecpg/test/expected/thread-alloc.c @@ -179,7 +179,7 @@ if (sqlca.sqlcode < 0) sqlprint();} return 0; } -int main () +int main(void) { intptr_t i; #ifdef WIN32 diff --git a/src/interfaces/ecpg/test/expected/thread-descriptor.c b/src/interfaces/ecpg/test/expected/thread-descriptor.c index e34f4708d1379..f9322b3e9bb49 100644 --- a/src/interfaces/ecpg/test/expected/thread-descriptor.c +++ b/src/interfaces/ecpg/test/expected/thread-descriptor.c @@ -125,7 +125,7 @@ if (sqlca.sqlcode < 0) sqlprint(); return 0; } -int main () +int main(void) { int i; #ifdef WIN32 diff --git a/src/interfaces/ecpg/test/expected/thread-prep.c b/src/interfaces/ecpg/test/expected/thread-prep.c index 052e27b634f98..4fb19e3fd1cb6 100644 --- a/src/interfaces/ecpg/test/expected/thread-prep.c +++ b/src/interfaces/ecpg/test/expected/thread-prep.c @@ -189,7 +189,7 @@ if (sqlca.sqlcode < 0) sqlprint();} return 0; } -int main () +int main(void) { intptr_t i; #ifdef WIN32 diff --git a/src/interfaces/ecpg/test/expected/thread-thread.c b/src/interfaces/ecpg/test/expected/thread-thread.c index 95faa223c2002..71da6f2e07d06 100644 --- a/src/interfaces/ecpg/test/expected/thread-thread.c +++ b/src/interfaces/ecpg/test/expected/thread-thread.c @@ -38,7 +38,7 @@ void *test_thread(void *arg); int nthreads = 10; int iterations = 20; -int main() +int main(void) { #ifndef WIN32 pthread_t *threads; diff --git a/src/interfaces/ecpg/test/expected/thread-thread_implicit.c b/src/interfaces/ecpg/test/expected/thread-thread_implicit.c index 7ac0297a234a3..259fc09dad61e 100644 --- a/src/interfaces/ecpg/test/expected/thread-thread_implicit.c +++ b/src/interfaces/ecpg/test/expected/thread-thread_implicit.c @@ -38,7 +38,7 @@ void *test_thread(void *arg); int nthreads = 10; int iterations = 20; -int main() +int main(void) { #ifndef WIN32 pthread_t *threads; diff --git a/src/interfaces/ecpg/test/meson.build b/src/interfaces/ecpg/test/meson.build index 4ccdf9d295a70..7f5b950c186fc 100644 --- a/src/interfaces/ecpg/test/meson.build +++ b/src/interfaces/ecpg/test/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # can't run ecpg to build test dependencies, at least not without an emulator if meson.is_cross_build() diff --git a/src/interfaces/ecpg/test/pg_regress_ecpg.c b/src/interfaces/ecpg/test/pg_regress_ecpg.c index ba3477f9dd8b7..ad66e4d65c03a 100644 --- a/src/interfaces/ecpg/test/pg_regress_ecpg.c +++ b/src/interfaces/ecpg/test/pg_regress_ecpg.c @@ -8,7 +8,7 @@ * * This code is released under the terms of the PostgreSQL License. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/ecpg/test/pg_regress_ecpg.c diff --git a/src/interfaces/ecpg/test/pgtypeslib/meson.build b/src/interfaces/ecpg/test/pgtypeslib/meson.build index 499ad68aab610..1b3c864db3ebd 100644 --- a/src/interfaces/ecpg/test/pgtypeslib/meson.build +++ b/src/interfaces/ecpg/test/pgtypeslib/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'dt_test', diff --git a/src/interfaces/ecpg/test/preproc/array_of_struct.pgc b/src/interfaces/ecpg/test/preproc/array_of_struct.pgc index 69f575847428e..7cc76075536be 100644 --- a/src/interfaces/ecpg/test/preproc/array_of_struct.pgc +++ b/src/interfaces/ecpg/test/preproc/array_of_struct.pgc @@ -20,7 +20,13 @@ EXEC SQL TYPE cust_ind IS short phone_ind; }; -int main() +EXEC SQL TYPE company IS + struct + { + customer customers[10]; + }; + +int main(void) { EXEC SQL begin declare section; customer custs1[10]; @@ -41,6 +47,9 @@ int main() varchar name[50]; int phone; } custs4; + + company acme; + int r; varchar onlyname[2][50]; EXEC SQL end declare section; @@ -50,9 +59,25 @@ int main() EXEC SQL connect to REGRESSDB1; EXEC SQL create table customers (c varchar(50), p int); - EXEC SQL insert into customers values ('John Doe', '12345'); - EXEC SQL insert into customers values ('Jane Doe', '67890'); + /* First we'll insert some data using C variable references */ + strcpy(custs1[0].name.arr, "John Doe"); + custs1[0].name.len = strlen(custs1[0].name.arr); + custs1[0].phone = 12345; + + strcpy(acme.customers[1].name.arr, "Jane Doe"); + acme.customers[1].name.len = strlen(acme.customers[1].name.arr); + acme.customers[1].phone = 67890; + + EXEC SQL insert into customers values (:custs1->name, + :custs1[0].phone); + EXEC SQL insert into customers values (:acme.customers[1].name, + :acme.customers[1].phone); + + /* Clear the array, to be sure reading back into it actually gets data */ + memset(custs1, 0, sizeof(customer) * 10); + + /* Now read back the data */ EXEC SQL select * INTO :custs1:inds from customers limit 2; printf("custs1:\n"); for (r = 0; r < 2; r++) diff --git a/src/interfaces/ecpg/test/preproc/autoprep.pgc b/src/interfaces/ecpg/test/preproc/autoprep.pgc index d3d9305da5b92..6da232d59ab6c 100644 --- a/src/interfaces/ecpg/test/preproc/autoprep.pgc +++ b/src/interfaces/ecpg/test/preproc/autoprep.pgc @@ -64,7 +64,7 @@ static void test(void) { EXEC SQL DISCONNECT ALL; } -int main() { +int main(void) { test(); test(); /* retry */ diff --git a/src/interfaces/ecpg/test/preproc/meson.build b/src/interfaces/ecpg/test/preproc/meson.build index 775502e010266..2a645080429d0 100644 --- a/src/interfaces/ecpg/test/preproc/meson.build +++ b/src/interfaces/ecpg/test/preproc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'array_of_struct', diff --git a/src/interfaces/ecpg/test/preproc/pointer_to_struct.pgc b/src/interfaces/ecpg/test/preproc/pointer_to_struct.pgc index 1ec651e3fc31a..46404373ce6bd 100644 --- a/src/interfaces/ecpg/test/preproc/pointer_to_struct.pgc +++ b/src/interfaces/ecpg/test/preproc/pointer_to_struct.pgc @@ -21,7 +21,13 @@ EXEC SQL TYPE cust_ind IS short phone_ind; }; -int main() +EXEC SQL TYPE company IS + struct + { + customer *customers; + }; + +int main(void) { EXEC SQL begin declare section; customer *custs1 = (customer *) malloc(sizeof(customer) * 10); @@ -45,18 +51,38 @@ int main() int phone; } *custs4 = (struct customer4 *) malloc(sizeof(struct customer4)); + company acme; + int r; varchar onlyname[2][50]; EXEC SQL end declare section; ECPGdebug(1, stderr); + acme.customers = (customer *) malloc(sizeof(customer) * 10); + EXEC SQL connect to REGRESSDB1; EXEC SQL create table customers (c varchar(50), p int); - EXEC SQL insert into customers values ('John Doe', '12345'); - EXEC SQL insert into customers values ('Jane Doe', '67890'); + /* First we'll insert some data using C variable references */ + strcpy(custs1[0].name.arr, "John Doe"); + custs1[0].name.len = strlen(custs1[0].name.arr); + custs1[0].phone = 12345; + + strcpy(acme.customers[1].name.arr, "Jane Doe"); + acme.customers[1].name.len = strlen(acme.customers[1].name.arr); + acme.customers[1].phone = 67890; + + EXEC SQL insert into customers values (:custs1->name, + :custs1[0].phone); + EXEC SQL insert into customers values (:acme.customers[1].name, + :acme.customers[1].phone); + + /* Clear the array, to be sure reading back into it actually gets data */ + memset(custs1, 0, sizeof(customer) * 10); + + /* Now read back the data */ EXEC SQL select * INTO :custs1:inds from customers limit 2; printf("custs1:\n"); for (r = 0; r < 2; r++) diff --git a/src/interfaces/ecpg/test/preproc/strings.h b/src/interfaces/ecpg/test/preproc/strings.h index edb5be5339e6d..8932b4ea9ed11 100644 --- a/src/interfaces/ecpg/test/preproc/strings.h +++ b/src/interfaces/ecpg/test/preproc/strings.h @@ -1,3 +1,13 @@ +/* redundant declaration to silence -Wmissing-variable-declarations */ +extern char *s1, + *s2, + *s3, + *s4, + *s5, + *s6, + *s7, + *s8; + char *s1, *s2, *s3, diff --git a/src/interfaces/ecpg/test/sql/.gitignore b/src/interfaces/ecpg/test/sql/.gitignore index 46adc571b4829..b0be3d0c73d90 100644 --- a/src/interfaces/ecpg/test/sql/.gitignore +++ b/src/interfaces/ecpg/test/sql/.gitignore @@ -50,5 +50,7 @@ /sqljson.c /sqljson_jsontable /sqljson_jsontable.c +/sqlpgq +/sqlpgq.c /twophase /twophase.c diff --git a/src/interfaces/ecpg/test/sql/Makefile b/src/interfaces/ecpg/test/sql/Makefile index 3ff190a523365..5296848cda97f 100644 --- a/src/interfaces/ecpg/test/sql/Makefile +++ b/src/interfaces/ecpg/test/sql/Makefile @@ -25,6 +25,7 @@ TESTS = array array.c \ show show.c \ sqljson sqljson.c \ sqljson_jsontable sqljson_jsontable.c \ + sqlpgq sqlpgq.c \ insupd insupd.c \ twophase twophase.c \ insupd insupd.c \ diff --git a/src/interfaces/ecpg/test/sql/code100.pgc b/src/interfaces/ecpg/test/sql/code100.pgc index d9a5e52444a79..8b29b0a9d2cdd 100644 --- a/src/interfaces/ecpg/test/sql/code100.pgc +++ b/src/interfaces/ecpg/test/sql/code100.pgc @@ -4,7 +4,7 @@ exec sql include sqlca; exec sql include ../regression; -int main() +int main(void) { exec sql begin declare section; int index; exec sql end declare section; diff --git a/src/interfaces/ecpg/test/sql/copystdout.pgc b/src/interfaces/ecpg/test/sql/copystdout.pgc index 9ecce7d42c254..f385a86f2adfc 100644 --- a/src/interfaces/ecpg/test/sql/copystdout.pgc +++ b/src/interfaces/ecpg/test/sql/copystdout.pgc @@ -6,7 +6,7 @@ exec sql include ../regression; EXEC SQL WHENEVER SQLERROR sqlprint; int -main () +main(void) { ECPGdebug (1, stderr); diff --git a/src/interfaces/ecpg/test/sql/declare.pgc b/src/interfaces/ecpg/test/sql/declare.pgc index e7ee4aa5348c8..4dfe1dd8bd349 100644 --- a/src/interfaces/ecpg/test/sql/declare.pgc +++ b/src/interfaces/ecpg/test/sql/declare.pgc @@ -182,7 +182,7 @@ void execute_test(void) printf("count: %d, length: %d, data: %s\n", count, length, f3[0]); } -void commitTable() +void commitTable(void) { EXEC SQL AT con1 COMMIT; EXEC SQL AT con2 COMMIT; @@ -191,7 +191,7 @@ void commitTable() /* * reset all the output variables */ -void reset() +void reset(void) { memset(f1, 0, sizeof(f1)); memset(f2, 0, sizeof(f2)); diff --git a/src/interfaces/ecpg/test/sql/dyntest.pgc b/src/interfaces/ecpg/test/sql/dyntest.pgc index 0222c89851547..f7cc72933230d 100644 --- a/src/interfaces/ecpg/test/sql/dyntest.pgc +++ b/src/interfaces/ecpg/test/sql/dyntest.pgc @@ -16,7 +16,7 @@ error (void) } int -main () +main(void) { exec sql begin declare section; int COUNT; diff --git a/src/interfaces/ecpg/test/sql/fetch.pgc b/src/interfaces/ecpg/test/sql/fetch.pgc index 31e525e42d4eb..6367cdb8727a5 100644 --- a/src/interfaces/ecpg/test/sql/fetch.pgc +++ b/src/interfaces/ecpg/test/sql/fetch.pgc @@ -4,7 +4,7 @@ EXEC SQL INCLUDE ../regression; -int main() { +int main(void) { EXEC SQL BEGIN DECLARE SECTION; char str[25]; int i, count=1, loopcount; diff --git a/src/interfaces/ecpg/test/sql/func.pgc b/src/interfaces/ecpg/test/sql/func.pgc index 5ebcafa11dac6..fcc2632c3de36 100644 --- a/src/interfaces/ecpg/test/sql/func.pgc +++ b/src/interfaces/ecpg/test/sql/func.pgc @@ -4,7 +4,7 @@ EXEC SQL INCLUDE ../regression; -int main() { +int main(void) { EXEC SQL char text[25]; ECPGdebug(1, stderr); diff --git a/src/interfaces/ecpg/test/sql/indicators.pgc b/src/interfaces/ecpg/test/sql/indicators.pgc index c1f26e33adcfe..d925faf35ce18 100644 --- a/src/interfaces/ecpg/test/sql/indicators.pgc +++ b/src/interfaces/ecpg/test/sql/indicators.pgc @@ -3,7 +3,7 @@ exec sql include sqlca; exec sql include ../regression; -int main() +int main(void) { exec sql begin declare section; int intvar = 5; diff --git a/src/interfaces/ecpg/test/sql/insupd.pgc b/src/interfaces/ecpg/test/sql/insupd.pgc index b12f66f791a88..50dac5e5f1f6e 100644 --- a/src/interfaces/ecpg/test/sql/insupd.pgc +++ b/src/interfaces/ecpg/test/sql/insupd.pgc @@ -4,7 +4,7 @@ EXEC SQL INCLUDE ../regression; -int main() { +int main(void) { EXEC SQL BEGIN DECLARE SECTION; int i1[3], i2[3], i3[3], i4; EXEC SQL END DECLARE SECTION; diff --git a/src/interfaces/ecpg/test/sql/meson.build b/src/interfaces/ecpg/test/sql/meson.build index c0608173e4ba7..00d7debe80f53 100644 --- a/src/interfaces/ecpg/test/sql/meson.build +++ b/src/interfaces/ecpg/test/sql/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'array', @@ -27,6 +27,7 @@ pgc_files = [ 'sqlda', 'sqljson', 'sqljson_jsontable', + 'sqlpgq', 'twophase', ] diff --git a/src/interfaces/ecpg/test/sql/parser.pgc b/src/interfaces/ecpg/test/sql/parser.pgc index 6e15f1364d523..ab2655b755cd0 100644 --- a/src/interfaces/ecpg/test/sql/parser.pgc +++ b/src/interfaces/ecpg/test/sql/parser.pgc @@ -5,7 +5,7 @@ /* test parser addition that merges two tokens into one */ EXEC SQL INCLUDE ../regression; -int main() { +int main(void) { EXEC SQL BEGIN DECLARE SECTION; int item[3], ind[3], i; EXEC SQL END DECLARE SECTION; diff --git a/src/interfaces/ecpg/test/sql/quote.pgc b/src/interfaces/ecpg/test/sql/quote.pgc index 9b62b7da90d99..da42aab056d9d 100644 --- a/src/interfaces/ecpg/test/sql/quote.pgc +++ b/src/interfaces/ecpg/test/sql/quote.pgc @@ -4,7 +4,7 @@ EXEC SQL INCLUDE ../regression; -int main() { +int main(void) { EXEC SQL BEGIN DECLARE SECTION; char var[25]; int i, loopcount; @@ -19,16 +19,6 @@ int main() { EXEC SQL CREATE TABLE "My_Table" ( Item1 int, Item2 text ); - EXEC SQL SET standard_conforming_strings TO off; - - EXEC SQL SHOW standard_conforming_strings INTO :var; - printf("Standard conforming strings: %s\n", var); - - /* this is a\\b actually */ - EXEC SQL INSERT INTO "My_Table" VALUES ( 1, 'a\\\\b' ); - /* this is a\\b */ - EXEC SQL INSERT INTO "My_Table" VALUES ( 1, E'a\\\\b' ); - EXEC SQL SET standard_conforming_strings TO on; EXEC SQL SHOW standard_conforming_strings INTO :var; diff --git a/src/interfaces/ecpg/test/sql/show.pgc b/src/interfaces/ecpg/test/sql/show.pgc index 339678ae994ca..b1318453058ff 100644 --- a/src/interfaces/ecpg/test/sql/show.pgc +++ b/src/interfaces/ecpg/test/sql/show.pgc @@ -4,7 +4,7 @@ EXEC SQL INCLUDE ../regression; -int main() { +int main(void) { EXEC SQL BEGIN DECLARE SECTION; char var[25] = "public"; EXEC SQL END DECLARE SECTION; @@ -23,7 +23,7 @@ int main() { EXEC SQL SHOW search_path INTO :var; printf("Var: Search path: %s\n", var); - EXEC SQL SET standard_conforming_strings TO off; + EXEC SQL SET standard_conforming_strings TO on; EXEC SQL SHOW standard_conforming_strings INTO :var; printf("Var: Standard conforming strings: %s\n", var); diff --git a/src/interfaces/ecpg/test/sql/sqljson.pgc b/src/interfaces/ecpg/test/sql/sqljson.pgc index ddcbcc3b3cb53..6cc8a375dd5de 100644 --- a/src/interfaces/ecpg/test/sql/sqljson.pgc +++ b/src/interfaces/ecpg/test/sql/sqljson.pgc @@ -6,7 +6,7 @@ exec sql include ../regression; EXEC SQL WHENEVER SQLERROR sqlprint; int -main () +main(void) { EXEC SQL BEGIN DECLARE SECTION; char json[1024]; diff --git a/src/interfaces/ecpg/test/sql/sqljson_jsontable.pgc b/src/interfaces/ecpg/test/sql/sqljson_jsontable.pgc index aa2b4494bb6eb..3e9a110944d65 100644 --- a/src/interfaces/ecpg/test/sql/sqljson_jsontable.pgc +++ b/src/interfaces/ecpg/test/sql/sqljson_jsontable.pgc @@ -6,7 +6,7 @@ exec sql include ../regression; EXEC SQL WHENEVER SQLERROR sqlprint; int -main () +main(void) { EXEC SQL BEGIN DECLARE SECTION; int foo; diff --git a/src/interfaces/ecpg/test/sql/sqlpgq.pgc b/src/interfaces/ecpg/test/sql/sqlpgq.pgc new file mode 100644 index 0000000000000..b3c21c6238a68 --- /dev/null +++ b/src/interfaces/ecpg/test/sql/sqlpgq.pgc @@ -0,0 +1,105 @@ +#include +#include +#include + +exec sql include ../regression; + +exec sql whenever sqlerror sqlprint; + +int +main(void) +{ +exec sql begin declare section; + char command[512]; + char search_address[10]; + char cname[100]; + int reg; +exec sql end declare section; + + ECPGdebug(1, stderr); + + exec sql connect to REGRESSDB1 as main; + + /* Create schema and tables for property graph testing */ + exec sql create schema graph_ecpg_tests; + exec sql set search_path = graph_ecpg_tests; + + exec sql create table customers ( + customer_id integer primary key, + name varchar, + address varchar + ); + + exec sql create table orders ( + order_id integer primary key, + register integer + ); + + exec sql create table customer_orders ( + customer_orders_id integer primary key, + customer_id integer references customers (customer_id), + order_id integer references orders (order_id) + ); + + /* Insert test data */ + exec sql insert into customers values (1, 'customer1', 'US'), (2, 'customer2', 'CA'), (3, 'customer3', 'GL'); + exec sql insert into orders values (1, 100), (2, 200), (3, 500); + exec sql insert into customer_orders (customer_orders_id, customer_id, order_id) values (1, 1, 1), (2, 2, 2), (3, 3, 3); + + /* Create property graph */ + exec sql create property graph shopgraph + vertex tables ( + customers, + orders + ) + edge tables ( + customer_orders key (customer_orders_id) + source key (customer_id) references customers (customer_id) + destination key (order_id) references orders (order_id) + ); + + exec sql commit; + + /* direct sql - US customers */ + exec sql select * into :cname, :reg from graph_table (shopgraph match (c is customers where c.address = 'US')-[is customer_orders]->(o is orders) columns (c.name, o.register)); + printf("found %ld results (%s, %d)\n", sqlca.sqlerrd[2], cname, reg); + + /* direct sql with C variable - GL customers */ + strcpy(search_address, "GL"); + exec sql select * into :cname, :reg from graph_table (shopgraph match (c is customers where c.address = :search_address)-[is customer_orders]->(o is orders) columns (c.name, o.register)); + printf("found %ld results (%s, %d)\n", sqlca.sqlerrd[2], cname, reg); + + /* prepared statement - CA customers */ + sprintf(command, "select * from graph_table (shopgraph match (c is customers where c.address = $1)-[is customer_orders]->(o is orders) columns (c.name, o.register))"); + exec sql prepare graph_stmt from :command; + strcpy(search_address, "CA"); + exec sql execute graph_stmt into :cname, :reg using :search_address; + printf("found %ld results (%s, %d)\n", sqlca.sqlerrd[2], cname, reg); + exec sql deallocate graph_stmt; + + /* cursor test - all customers with orders */ + exec sql declare graph_cursor cursor for select * from graph_table (shopgraph match (c is customers)-[is customer_orders]->(o is orders) columns (c.name, o.register)) order by name; + exec sql open graph_cursor; + exec sql whenever not found do break; + while (1) { + exec sql fetch graph_cursor into :cname, :reg; + printf("cursor result: %s, %d\n", cname, reg); + } + exec sql whenever not found continue; + exec sql close graph_cursor; + + /* label disjunction syntax test */ + exec sql select * into :cname from graph_table (shopgraph match (c is customers | customers where c.address = 'US') columns (c.name)); + printf("found %ld results (%s)\n", sqlca.sqlerrd[2], cname); + + /* Clean up */ + exec sql drop property graph shopgraph; + exec sql drop table customer_orders; + exec sql drop table orders; + exec sql drop table customers; + exec sql drop schema graph_ecpg_tests; + exec sql commit; + exec sql disconnect; + + return 0; +} diff --git a/src/interfaces/ecpg/test/thread/alloc.pgc b/src/interfaces/ecpg/test/thread/alloc.pgc index d3d35493bf99e..39faa6dc2eff1 100644 --- a/src/interfaces/ecpg/test/thread/alloc.pgc +++ b/src/interfaces/ecpg/test/thread/alloc.pgc @@ -51,7 +51,7 @@ static void* fn(void* arg) return 0; } -int main () +int main(void) { intptr_t i; #ifdef WIN32 diff --git a/src/interfaces/ecpg/test/thread/descriptor.pgc b/src/interfaces/ecpg/test/thread/descriptor.pgc index 30bce7c87bcc8..b1485ac2a5c02 100644 --- a/src/interfaces/ecpg/test/thread/descriptor.pgc +++ b/src/interfaces/ecpg/test/thread/descriptor.pgc @@ -32,7 +32,7 @@ static void* fn(void* arg) return 0; } -int main () +int main(void) { int i; #ifdef WIN32 diff --git a/src/interfaces/ecpg/test/thread/meson.build b/src/interfaces/ecpg/test/thread/meson.build index 6a6745ce0fc4e..b23289730b752 100644 --- a/src/interfaces/ecpg/test/thread/meson.build +++ b/src/interfaces/ecpg/test/thread/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgc_files = [ 'thread_implicit', diff --git a/src/interfaces/ecpg/test/thread/prep.pgc b/src/interfaces/ecpg/test/thread/prep.pgc index f61b31ce101ce..a389a6ae39ecd 100644 --- a/src/interfaces/ecpg/test/thread/prep.pgc +++ b/src/interfaces/ecpg/test/thread/prep.pgc @@ -51,7 +51,7 @@ static void* fn(void* arg) return 0; } -int main () +int main(void) { intptr_t i; #ifdef WIN32 diff --git a/src/interfaces/ecpg/test/thread/thread.pgc b/src/interfaces/ecpg/test/thread/thread.pgc index b9b9ebb441de8..36ff91fe816df 100644 --- a/src/interfaces/ecpg/test/thread/thread.pgc +++ b/src/interfaces/ecpg/test/thread/thread.pgc @@ -20,7 +20,7 @@ void *test_thread(void *arg); int nthreads = 10; int iterations = 20; -int main() +int main(void) { #ifndef WIN32 pthread_t *threads; diff --git a/src/interfaces/ecpg/test/thread/thread_implicit.pgc b/src/interfaces/ecpg/test/thread/thread_implicit.pgc index ff9b12a943707..711a8e26cc60a 100644 --- a/src/interfaces/ecpg/test/thread/thread_implicit.pgc +++ b/src/interfaces/ecpg/test/thread/thread_implicit.pgc @@ -20,7 +20,7 @@ void *test_thread(void *arg); int nthreads = 10; int iterations = 20; -int main() +int main(void) { #ifndef WIN32 pthread_t *threads; diff --git a/src/interfaces/libpq-oauth/.gitignore b/src/interfaces/libpq-oauth/.gitignore new file mode 100644 index 0000000000000..eb5b98aea544c --- /dev/null +++ b/src/interfaces/libpq-oauth/.gitignore @@ -0,0 +1,4 @@ +/exports.list +/oauth_tests + +/tmp_check/ diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile index 270fc0cf2d9d9..11e1a3cf5280d 100644 --- a/src/interfaces/libpq-oauth/Makefile +++ b/src/interfaces/libpq-oauth/Makefile @@ -2,7 +2,7 @@ # # Makefile for libpq-oauth # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/interfaces/libpq-oauth/Makefile @@ -16,15 +16,22 @@ include $(top_builddir)/src/Makefile.global PGFILEDESC = "libpq-oauth - device authorization OAuth support" # This is an internal module; we don't want an SONAME and therefore do not set -# SO_MAJOR_VERSION. -NAME = pq-oauth-$(MAJORVERSION) - -# Force the name "libpq-oauth" for both the static and shared libraries. The -# staticlib doesn't need version information in its name. +# SO_MAJOR_VERSION. This requires an explicit override for the shared library +# name. +NAME = pq-oauth override shlib := lib$(NAME)$(DLSUFFIX) override stlib := libpq-oauth.a -override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(CPPFLAGS) $(LIBCURL_CPPFLAGS) +override CFLAGS += $(PTHREAD_CFLAGS) + +override CPPFLAGS_SHLIB := -DUSE_DYNAMIC_OAUTH + +# A bit of forward-looking paranoia: don't allow libpq-oauth.so to accidentally +# depend on the encoding IDs coming from libpq. They're not guaranteed to match +# the IDs in use by our version of pgcommon, now that we allow the major version +# of libpq to differ from the major version of libpq-oauth. +override CPPFLAGS_SHLIB += -DUSE_PRIVATE_ENCODING_FUNCS OBJS = \ $(WIN32RES) @@ -36,8 +43,7 @@ OBJS_SHLIB = \ oauth-curl_shlib.o \ oauth-utils.o \ -oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH -oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH +oauth-utils.o: override CPPFLAGS += $(CPPFLAGS_SHLIB) # Add shlib-/stlib-specific objects. $(shlib): override OBJS += $(OBJS_SHLIB) @@ -47,17 +53,13 @@ $(stlib): override OBJS += $(OBJS_STATIC) $(stlib): $(OBJS_STATIC) SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) -SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) $(filter -lintl, $(LIBS)) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) $(filter -lintl -lm $(PTHREAD_LIBS), $(LIBS)) SHLIB_PREREQS = submake-libpq SHLIB_EXPORTS = exports.txt # Disable -bundle_loader on macOS. BE_DLLLIBS = -# By default, a library without an SONAME doesn't get a static library, so we -# add it to the build explicitly. -all: all-lib all-static-lib - # Shared library stuff include $(top_srcdir)/src/Makefile.shlib @@ -66,6 +68,28 @@ include $(top_srcdir)/src/Makefile.shlib %_shlib.o: %.c %.o $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ +.PHONY: all-tests +all-tests: oauth_tests$(X) + +oauth_tests$(X): test-oauth-curl.o oauth-utils.o $(WIN32RES) | submake-libpgport submake-libpq + $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(SHLIB_LINK) -o $@ + +# +# Top-Level Targets +# +# The existence of a t/ folder induces the buildfarm to run Make directly on +# this subdirectory, bypassing the recursion skip in src/interfaces/Makefile. +# Wrap the standard build targets in a with_libcurl conditional to avoid +# building OAuth code on platforms that haven't requested it. (The "clean"-style +# targets remain available.) +# + +ifeq ($(with_libcurl), yes) + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + # Ignore the standard rules for SONAME-less installation; we want both the # static and shared libraries to go into libdir. install: all installdirs $(stlib) $(shlib) @@ -75,9 +99,19 @@ install: all installdirs $(stlib) $(shlib) installdirs: $(MKDIR_P) '$(DESTDIR)$(libdir)' +check: all-tests + $(prove_check) + +installcheck: all-tests + $(prove_installcheck) + +endif # with_libcurl + uninstall: rm -f '$(DESTDIR)$(libdir)/$(stlib)' rm -f '$(DESTDIR)$(libdir)/$(shlib)' clean distclean: clean-lib rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) + rm -f test-oauth-curl.o oauth_tests$(X) + rm -rf tmp_check diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README index 553962d644e94..a27a8238d4bbf 100644 --- a/src/interfaces/libpq-oauth/README +++ b/src/interfaces/libpq-oauth/README @@ -10,48 +10,33 @@ results in a failed connection. = Load-Time ABI = -This module ABI is an internal implementation detail, so it's subject to change -across major releases; the name of the module (libpq-oauth-MAJOR) reflects this. -The module exports the following symbols: - -- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -- void pg_fe_cleanup_oauth_flow(PGconn *conn); - -pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of -conn->async_auth and conn->cleanup_async_auth, respectively. - -At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and -libpq_gettext(), which must be injected by libpq using this initialization -function before the flow is run: - -- void libpq_oauth_init(pgthreadlock_t threadlock, - libpq_gettext_func gettext_impl, - conn_errorMessage_func errmsg_impl, - conn_oauth_client_id_func clientid_impl, - conn_oauth_client_secret_func clientsecret_impl, - conn_oauth_discovery_uri_func discoveryuri_impl, - conn_oauth_issuer_id_func issuerid_impl, - conn_oauth_scope_func scope_impl, - conn_sasl_state_func saslstate_impl, - set_conn_altsock_func setaltsock_impl, - set_conn_oauth_token_func settoken_impl); - -It also relies on access to several members of the PGconn struct. Not only can -these change positions across minor versions, but the offsets aren't necessarily -stable within a single minor release (conn->errorMessage, for instance, can -change offsets depending on configure-time options). Therefore the necessary -accessors (named conn_*) and mutators (set_conn_*) are injected here. With this -approach, we can safely search the standard dlopen() paths (e.g. RPATH, -LD_LIBRARY_PATH, the SO cache) for an implementation module to use, even if that -module wasn't compiled at the same time as libpq -- which becomes especially -important during "live upgrade" situations where a running libpq application has -the libpq-oauth module updated out from under it before it's first loaded from -disk. +As of v19, this module ABI is public and cannot change incompatibly without also +changing the entry points. Both libpq and libpq-oauth must gracefully handle +situations where the other library is of a different release version, past or +future, since upgrades to the libraries may happen in either order. + +(Don't assume that package version dependencies from libpq-oauth to libpq will +simplify the situation! Since libpq delay-loads libpq-oauth, we still have to +handle cases where a long-running client application has a libpq that's older +than a newly upgraded plugin.) + +The module exports the following symbol: + +- int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request); + +The module then behaves as if it had received a PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 +request via the PQauthDataHook API, and it either fills in an existing token or +populates the necessary callbacks for a token to be obtained asynchronously. +(See the documentation for PGoauthBearerRequest.) The function returns zero on +success and nonzero on failure. + +Additionally, libpq-oauth relies on libpq's libpq_gettext(), which must be +injected by libpq using this initialization function before the flow is run: + +- void libpq_oauth_init(libpq_gettext_func gettext_impl); = Static Build = The static library libpq.a does not perform any dynamic loading. If the builtin -flow is enabled, the application is expected to link against libpq-oauth.a -directly to provide the necessary symbols. (libpq.a and libpq-oauth.a must be -part of the same build. Unlike the dynamic module, there are no translation -shims provided.) +flow is enabled, the application is expected to link against libpq-oauth.a to +provide the necessary symbol, or else implement pg_start_oauthbearer() itself. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt index 6891a83dbf998..7bc12b860d79a 100644 --- a/src/interfaces/libpq-oauth/exports.txt +++ b/src/interfaces/libpq-oauth/exports.txt @@ -1,4 +1,3 @@ # src/interfaces/libpq-oauth/exports.txt libpq_oauth_init 1 -pg_fe_run_oauth_flow 2 -pg_fe_cleanup_oauth_flow 3 +pg_start_oauthbearer 2 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build index df064c59a4070..ea3a900f4f18a 100644 --- a/src/interfaces/libpq-oauth/meson.build +++ b/src/interfaces/libpq-oauth/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not oauth_flow_supported subdir_done() @@ -12,7 +12,15 @@ libpq_oauth_sources = files( libpq_oauth_so_sources = files( 'oauth-utils.c', ) -libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] +libpq_oauth_so_c_args = [ + '-DUSE_DYNAMIC_OAUTH', + + # A bit of forward-looking paranoia: don't allow anyone to accidentally depend + # on the encoding IDs coming from libpq. They're not guaranteed to match the + # IDs in use by our version of pgcommon, now that we allow the major version + # of libpq to differ from the major version of libpq-oauth. + '-DUSE_PRIVATE_ENCODING_FUNCS', +] export_file = custom_target('libpq-oauth.exports', kwargs: gen_export_kwargs, @@ -21,6 +29,7 @@ export_file = custom_target('libpq-oauth.exports', # port needs to be in include path due to pthread-win32.h libpq_oauth_inc = include_directories('.', '../libpq', '../../port') +if build_static_lib libpq_oauth_st = static_library('libpq-oauth', libpq_oauth_sources, include_directories: [libpq_oauth_inc, postgres_inc], @@ -32,18 +41,56 @@ libpq_oauth_st = static_library('libpq-oauth', ], kwargs: default_lib_args, ) +libpq_targets += libpq_oauth_st +endif # This is an internal module; we don't want an SONAME and therefore do not set # SO_MAJOR_VERSION. -libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) - -libpq_oauth_so = shared_module(libpq_oauth_name, +if build_shared_lib +libpq_oauth_so = shared_module('libpq-oauth', libpq_oauth_sources + libpq_oauth_so_sources, include_directories: [libpq_oauth_inc, postgres_inc], - c_args: libpq_so_c_args, + c_args: libpq_oauth_so_c_args, c_pch: pch_postgres_fe_h, dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], link_depends: export_file, link_args: export_fmt.format(export_file.full_path()), kwargs: default_lib_args, ) +libpq_targets += libpq_oauth_so +endif + +libpq_oauth_test_deps = [] + +oauth_test_sources = files('test-oauth-curl.c') + libpq_oauth_so_sources + +if host_system == 'windows' + oauth_test_sources += rc_bin_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'oauth_tests', + '--FILEDESC', 'OAuth unit test program',]) +endif + +libpq_oauth_test_deps += executable('oauth_tests', + oauth_test_sources, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + kwargs: default_bin_args + { + 'c_args': default_bin_args.get('c_args', []) + libpq_oauth_so_c_args, + 'c_pch': pch_postgres_fe_h, + 'include_directories': [libpq_inc, postgres_inc], + 'install': false, + } +) + +testprep_targets += libpq_oauth_test_deps + +tests += { + 'name': 'libpq-oauth', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_oauth.pl', + ], + 'deps': libpq_oauth_test_deps, + }, +} diff --git a/src/interfaces/libpq-oauth/oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c index dba9a684fa8a5..abbef93f95f67 100644 --- a/src/interfaces/libpq-oauth/oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -4,7 +4,7 @@ * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -29,7 +29,6 @@ #endif #include "common/jsonapi.h" -#include "fe-auth-oauth.h" #include "mb/pg_wchar.h" #include "oauth-curl.h" @@ -44,25 +43,18 @@ #else /* !USE_DYNAMIC_OAUTH */ -/* - * Static builds may rely on PGconn offsets directly. Keep these aligned with - * the bank of callbacks in oauth-utils.h. - */ +/* Static builds may make use of libpq internals directly. */ +#include "fe-auth-oauth.h" #include "libpq-int.h" -#define conn_errorMessage(CONN) (&CONN->errorMessage) -#define conn_oauth_client_id(CONN) (CONN->oauth_client_id) -#define conn_oauth_client_secret(CONN) (CONN->oauth_client_secret) -#define conn_oauth_discovery_uri(CONN) (CONN->oauth_discovery_uri) -#define conn_oauth_issuer_id(CONN) (CONN->oauth_issuer_id) -#define conn_oauth_scope(CONN) (CONN->oauth_scope) -#define conn_sasl_state(CONN) (CONN->sasl_state) - -#define set_conn_altsock(CONN, VAL) do { CONN->altsock = VAL; } while (0) -#define set_conn_oauth_token(CONN, VAL) do { CONN->oauth_token = VAL; } while (0) - #endif /* USE_DYNAMIC_OAUTH */ +/* + * oauth-debug.h needs the declaration of libpq_gettext(), from one of the above + * sources. + */ +#include "oauth-debug.h" + /* One final guardrail against accidental inclusion... */ #if defined(USE_DYNAMIC_OAUTH) && defined(LIBPQ_INT_H) #error do not rely on libpq-int.h in dynamic builds of libpq-oauth @@ -227,6 +219,16 @@ enum OAuthStep */ struct async_ctx { + /* relevant connection options cached from the PGconn */ + char *client_id; /* oauth_client_id */ + char *client_secret; /* oauth_client_secret (may be NULL) */ + char *ca_file; /* oauth_ca_file */ + + /* options cached from the PGoauthBearerRequest (we don't own these) */ + const char *discovery_uri; + const char *issuer_id; + const char *scope; + enum OAuthStep step; /* where are we in the flow? */ int timerfd; /* descriptor for signaling async timeouts */ @@ -247,7 +249,8 @@ struct async_ctx * our entry point, errors have three parts: * * - errctx: an optional static string, describing the global operation - * currently in progress. It'll be translated for you. + * currently in progress. Should be translated with + * libpq_gettext(). * * - errbuf: contains the actual error message. Generally speaking, use * actx_error[_str] to manipulate this. This must be filled @@ -277,24 +280,23 @@ struct async_ctx int running; /* is asynchronous work in progress? */ bool user_prompted; /* have we already sent the authz prompt? */ bool used_basic_auth; /* did we send a client secret? */ - bool debugging; /* can we give unsafe developer assistance? */ + uint32 debug_flags; /* can we give developer assistance? */ + int dbg_num_calls; /* (debug mode) how many times were we called? */ }; /* * Tears down the Curl handles and frees the async_ctx. */ static void -free_async_ctx(PGconn *conn, struct async_ctx *actx) +free_async_ctx(struct async_ctx *actx) { /* * In general, none of the error cases below should ever happen if we have * no bugs above. But if we do hit them, surfacing those errors somehow * might be the only way to have a chance to debug them. * - * TODO: At some point it'd be nice to have a standard way to warn about - * teardown failures. Appending to the connection's error message only - * helps if the bug caused a connection failure; otherwise it'll be - * buried... + * Print them as warnings to stderr, following the example of similar + * situations in fe-secure-openssl.c and fe-connect.c. */ if (actx->curlm && actx->curl) @@ -302,9 +304,9 @@ free_async_ctx(PGconn *conn, struct async_ctx *actx) CURLMcode err = curl_multi_remove_handle(actx->curlm, actx->curl); if (err) - libpq_append_conn_error(conn, - "libcurl easy handle removal failed: %s", - curl_multi_strerror(err)); + fprintf(stderr, + libpq_gettext("WARNING: libcurl easy handle removal failed: %s\n"), + curl_multi_strerror(err)); } if (actx->curl) @@ -322,9 +324,9 @@ free_async_ctx(PGconn *conn, struct async_ctx *actx) CURLMcode err = curl_multi_cleanup(actx->curlm); if (err) - libpq_append_conn_error(conn, - "libcurl multi handle cleanup failed: %s", - curl_multi_strerror(err)); + fprintf(stderr, + libpq_gettext("WARNING: libcurl multi handle cleanup failed: %s\n"), + curl_multi_strerror(err)); } free_provider(&actx->provider); @@ -339,40 +341,87 @@ free_async_ctx(PGconn *conn, struct async_ctx *actx) if (actx->timerfd >= 0) close(actx->timerfd); + free(actx->client_id); + free(actx->client_secret); + free(actx->ca_file); + free(actx); } /* - * Release resources used for the asynchronous exchange and disconnect the - * altsock. - * - * This is called either at the end of a successful authentication, or during - * pqDropConnection(), so we won't leak resources even if PQconnectPoll() never - * calls us back. + * Release resources used for the asynchronous exchange. */ -void -pg_fe_cleanup_oauth_flow(PGconn *conn) +static void +pg_fe_cleanup_oauth_flow(PGconn *conn, PGoauthBearerRequest *request) { - fe_oauth_state *state = conn_sasl_state(conn); + struct async_ctx *actx = request->user; + + /* request->cleanup is only set after actx has been allocated. */ + Assert(actx); + + free_async_ctx(actx); + request->user = NULL; - if (state->async_ctx) + /* libpq has made its own copy of the token; clear ours now. */ + if (request->token) { - free_async_ctx(conn, state->async_ctx); - state->async_ctx = NULL; + explicit_bzero(request->token, strlen(request->token)); + free(request->token); + request->token = NULL; } +} + +/* + * Builds an error message from actx and stores it in req->error. The allocation + * is backed by actx->work_data (which will be reset first). + */ +static void +append_actx_error(PGoauthBearerRequestV2 *req, struct async_ctx *actx) +{ + PQExpBuffer errbuf = &actx->work_data; - set_conn_altsock(conn, PGINVALID_SOCKET); + resetPQExpBuffer(errbuf); + + /* + * Assemble the three parts of our error: context, body, and detail. See + * also the documentation for struct async_ctx. + */ + if (actx->errctx) + appendPQExpBuffer(errbuf, "%s: ", actx->errctx); + + if (PQExpBufferDataBroken(actx->errbuf)) + appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); + else + appendPQExpBufferStr(errbuf, actx->errbuf.data); + + if (actx->curl_err[0]) + { + appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); + + /* Sometimes libcurl adds a newline to the error buffer. :( */ + if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n') + { + errbuf->data[errbuf->len - 2] = ')'; + errbuf->data[errbuf->len - 1] = '\0'; + errbuf->len--; + } + } + + req->error = errbuf->data; } /* * Macros for manipulating actx->errbuf. actx_error() translates and formats a - * string for you; actx_error_str() appends a string directly without - * translation. + * string for you, actx_error_internal() is the untranslated equivalent, and + * actx_error_str() appends a string directly (also without translation). */ #define actx_error(ACTX, FMT, ...) \ appendPQExpBuffer(&(ACTX)->errbuf, libpq_gettext(FMT), ##__VA_ARGS__) +#define actx_error_internal(ACTX, FMT, ...) \ + appendPQExpBuffer(&(ACTX)->errbuf, FMT, ##__VA_ARGS__) + #define actx_error_str(ACTX, S) \ appendPQExpBufferStr(&(ACTX)->errbuf, S) @@ -438,7 +487,7 @@ struct json_field { char **scalar; /* for all scalar types */ struct curl_slist **array; /* for type == JSON_TOKEN_ARRAY_START */ - } target; + }; bool required; /* REQUIRED field, or just OPTIONAL? */ }; @@ -460,6 +509,9 @@ struct oauth_parse #define oauth_parse_set_error(ctx, fmt, ...) \ appendPQExpBuffer((ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__) +#define oauth_parse_set_error_internal(ctx, fmt, ...) \ + appendPQExpBuffer((ctx)->errbuf, fmt, ##__VA_ARGS__) + static void report_type_mismatch(struct oauth_parse *ctx) { @@ -474,20 +526,20 @@ report_type_mismatch(struct oauth_parse *ctx) switch (ctx->active->type) { case JSON_TOKEN_STRING: - msgfmt = "field \"%s\" must be a string"; + msgfmt = gettext_noop("field \"%s\" must be a string"); break; case JSON_TOKEN_NUMBER: - msgfmt = "field \"%s\" must be a number"; + msgfmt = gettext_noop("field \"%s\" must be a number"); break; case JSON_TOKEN_ARRAY_START: - msgfmt = "field \"%s\" must be an array of strings"; + msgfmt = gettext_noop("field \"%s\" must be an array of strings"); break; default: Assert(false); - msgfmt = "field \"%s\" has unexpected type"; + msgfmt = gettext_noop("field \"%s\" has unexpected type"); } oauth_parse_set_error(ctx, msgfmt, ctx->active->name); @@ -535,9 +587,9 @@ oauth_json_object_field_start(void *state, char *name, bool isnull) if (ctx->active) { Assert(false); - oauth_parse_set_error(ctx, - "internal error: started field '%s' before field '%s' was finished", - name, ctx->active->name); + oauth_parse_set_error_internal(ctx, + "internal error: started field \"%s\" before field \"%s\" was finished", + name, ctx->active->name); return JSON_SEM_ACTION_FAILED; } @@ -560,8 +612,8 @@ oauth_json_object_field_start(void *state, char *name, bool isnull) { field = ctx->active; - if ((field->type == JSON_TOKEN_ARRAY_START && *field->target.array) - || (field->type != JSON_TOKEN_ARRAY_START && *field->target.scalar)) + if ((field->type == JSON_TOKEN_ARRAY_START && *field->array) + || (field->type != JSON_TOKEN_ARRAY_START && *field->scalar)) { oauth_parse_set_error(ctx, "field \"%s\" is duplicated", field->name); @@ -587,9 +639,9 @@ oauth_json_object_end(void *state) if (!ctx->nested && ctx->active) { Assert(false); - oauth_parse_set_error(ctx, - "internal error: field '%s' still active at end of object", - ctx->active->name); + oauth_parse_set_error_internal(ctx, + "internal error: field \"%s\" still active at end of object", + ctx->active->name); return JSON_SEM_ACTION_FAILED; } @@ -643,9 +695,9 @@ oauth_json_array_end(void *state) if (ctx->nested != 2 || ctx->active->type != JSON_TOKEN_ARRAY_START) { Assert(false); - oauth_parse_set_error(ctx, - "internal error: found unexpected array end while parsing field '%s'", - ctx->active->name); + oauth_parse_set_error_internal(ctx, + "internal error: found unexpected array end while parsing field \"%s\"", + ctx->active->name); return JSON_SEM_ACTION_FAILED; } @@ -698,24 +750,24 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type) if (ctx->nested != 1) { Assert(false); - oauth_parse_set_error(ctx, - "internal error: scalar target found at nesting level %d", - ctx->nested); + oauth_parse_set_error_internal(ctx, + "internal error: scalar target found at nesting level %d", + ctx->nested); return JSON_SEM_ACTION_FAILED; } /* ...and that a result has not already been set. */ - if (*field->target.scalar) + if (*field->scalar) { Assert(false); - oauth_parse_set_error(ctx, - "internal error: scalar field '%s' would be assigned twice", - ctx->active->name); + oauth_parse_set_error_internal(ctx, + "internal error: scalar field \"%s\" would be assigned twice", + ctx->active->name); return JSON_SEM_ACTION_FAILED; } - *field->target.scalar = strdup(token); - if (!*field->target.scalar) + *field->scalar = strdup(token); + if (!*field->scalar) return JSON_OUT_OF_MEMORY; ctx->active = NULL; @@ -730,18 +782,18 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type) if (ctx->nested != 2) { Assert(false); - oauth_parse_set_error(ctx, - "internal error: array member found at nesting level %d", - ctx->nested); + oauth_parse_set_error_internal(ctx, + "internal error: array member found at nesting level %d", + ctx->nested); return JSON_SEM_ACTION_FAILED; } /* Note that curl_slist_append() makes a copy of the token. */ - temp = curl_slist_append(*field->target.array, token); + temp = curl_slist_append(*field->array, token); if (!temp) return JSON_OUT_OF_MEMORY; - *field->target.array = temp; + *field->array = temp; } } else @@ -877,8 +929,8 @@ parse_oauth_json(struct async_ctx *actx, const struct json_field *fields) while (fields->name) { if (fields->required - && !*fields->target.scalar - && !*fields->target.array) + && !*fields->scalar + && !*fields->array) { actx_error(actx, "field \"%s\" is missing", fields->name); goto cleanup; @@ -977,7 +1029,7 @@ parse_interval(struct async_ctx *actx, const char *interval_str) parsed = ceil(parsed); if (parsed < 1) - return actx->debugging ? 0 : 1; + return (actx->debug_flags & OAUTHDEBUG_UNSAFE_DOS_ENDPOINT) ? 0 : 1; else if (parsed >= INT_MAX) return INT_MAX; @@ -1086,7 +1138,7 @@ parse_token_error(struct async_ctx *actx, struct token_error *err) * override the errctx if parsing explicitly fails. */ if (!result) - actx->errctx = "failed to parse token error response"; + actx->errctx = libpq_gettext("failed to parse token error response"); return result; } @@ -1114,8 +1166,8 @@ record_token_error(struct async_ctx *actx, const struct token_error *err) if (response_code == 401) { actx_error(actx, actx->used_basic_auth - ? "provider rejected the oauth_client_secret" - : "provider requires client authentication, and no oauth_client_secret is set"); + ? gettext_noop("provider rejected the oauth_client_secret") + : gettext_noop("provider requires client authentication, and no oauth_client_secret is set")); actx_error_str(actx, " "); } } @@ -1178,20 +1230,20 @@ setup_multiplexer(struct async_ctx *actx) actx->mux = epoll_create1(EPOLL_CLOEXEC); if (actx->mux < 0) { - actx_error(actx, "failed to create epoll set: %m"); + actx_error_internal(actx, "failed to create epoll set: %m"); return false; } actx->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); if (actx->timerfd < 0) { - actx_error(actx, "failed to create timerfd: %m"); + actx_error_internal(actx, "failed to create timerfd: %m"); return false; } if (epoll_ctl(actx->mux, EPOLL_CTL_ADD, actx->timerfd, &ev) < 0) { - actx_error(actx, "failed to add timerfd to epoll set: %m"); + actx_error_internal(actx, "failed to add timerfd to epoll set: %m"); return false; } @@ -1200,8 +1252,7 @@ setup_multiplexer(struct async_ctx *actx) actx->mux = kqueue(); if (actx->mux < 0) { - /*- translator: the term "kqueue" (kernel queue) should not be translated */ - actx_error(actx, "failed to create kqueue: %m"); + actx_error_internal(actx, "failed to create kqueue: %m"); return false; } @@ -1214,7 +1265,7 @@ setup_multiplexer(struct async_ctx *actx) actx->timerfd = kqueue(); if (actx->timerfd < 0) { - actx_error(actx, "failed to create timer kqueue: %m"); + actx_error_internal(actx, "failed to create timer kqueue: %m"); return false; } @@ -1258,7 +1309,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, break; default: - actx_error(actx, "unknown libcurl socket operation: %d", what); + actx_error_internal(actx, "unknown libcurl socket operation: %d", what); return -1; } @@ -1275,15 +1326,15 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, switch (op) { case EPOLL_CTL_ADD: - actx_error(actx, "could not add to epoll set: %m"); + actx_error_internal(actx, "could not add to epoll set: %m"); break; case EPOLL_CTL_DEL: - actx_error(actx, "could not delete from epoll set: %m"); + actx_error_internal(actx, "could not delete from epoll set: %m"); break; default: - actx_error(actx, "could not update epoll set: %m"); + actx_error_internal(actx, "could not update epoll set: %m"); } return -1; @@ -1291,22 +1342,31 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, return 0; #elif defined(HAVE_SYS_EVENT_H) - struct kevent ev[2] = {0}; + struct kevent ev[2]; struct kevent ev_out[2]; struct timespec timeout = {0}; int nev = 0; int res; + /* + * We don't know which of the events is currently registered, perhaps + * both, so we always try to remove unneeded events. This means we need to + * tolerate ENOENT below. + */ switch (what) { case CURL_POLL_IN: EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD | EV_RECEIPT, 0, 0, 0); nev++; + EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_DELETE | EV_RECEIPT, 0, 0, 0); + nev++; break; case CURL_POLL_OUT: EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD | EV_RECEIPT, 0, 0, 0); nev++; + EV_SET(&ev[nev], socket, EVFILT_READ, EV_DELETE | EV_RECEIPT, 0, 0, 0); + nev++; break; case CURL_POLL_INOUT: @@ -1317,12 +1377,6 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, break; case CURL_POLL_REMOVE: - - /* - * We don't know which of these is currently registered, perhaps - * both, so we try to remove both. This means we need to tolerate - * ENOENT below. - */ EV_SET(&ev[nev], socket, EVFILT_READ, EV_DELETE | EV_RECEIPT, 0, 0, 0); nev++; EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_DELETE | EV_RECEIPT, 0, 0, 0); @@ -1330,14 +1384,17 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, break; default: - actx_error(actx, "unknown libcurl socket operation: %d", what); + actx_error_internal(actx, "unknown libcurl socket operation: %d", what); return -1; } - res = kevent(actx->mux, ev, nev, ev_out, lengthof(ev_out), &timeout); + Assert(nev <= lengthof(ev)); + Assert(nev <= lengthof(ev_out)); + + res = kevent(actx->mux, ev, nev, ev_out, nev, &timeout); if (res < 0) { - actx_error(actx, "could not modify kqueue: %m"); + actx_error_internal(actx, "could not modify kqueue: %m"); return -1; } @@ -1361,10 +1418,10 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, switch (what) { case CURL_POLL_REMOVE: - actx_error(actx, "could not delete from kqueue: %m"); + actx_error_internal(actx, "could not delete from kqueue: %m"); break; default: - actx_error(actx, "could not add to kqueue: %m"); + actx_error_internal(actx, "could not add to kqueue: %m"); } return -1; } @@ -1376,6 +1433,53 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, #endif } +/* + * If there is no work to do on any of the descriptors in the multiplexer, then + * this function must ensure that the multiplexer is not readable. + * + * Unlike epoll descriptors, kqueue descriptors only transition from readable to + * unreadable when kevent() is called and finds nothing, after removing + * level-triggered conditions that have gone away. We therefore need a dummy + * kevent() call after operations might have been performed on the monitored + * sockets or timer_fd. Any event returned is ignored here, but it also remains + * queued (being level-triggered) and leaves the descriptor readable. This is a + * no-op for epoll descriptors. + */ +static bool +comb_multiplexer(struct async_ctx *actx) +{ +#if defined(HAVE_SYS_EPOLL_H) + /* The epoll implementation doesn't hold onto stale events. */ + return true; +#elif defined(HAVE_SYS_EVENT_H) + struct timespec timeout = {0}; + struct kevent ev; + + /* + * Try to read a single pending event. We can actually ignore the result: + * either we found an event to process, in which case the multiplexer is + * correctly readable for that event at minimum, and it doesn't matter if + * there are any stale events; or we didn't find any, in which case the + * kernel will have discarded any stale events as it traveled to the end + * of the queue. + * + * Note that this depends on our registrations being level-triggered -- + * even the timer, so we use a chained kqueue for that instead of an + * EVFILT_TIMER on the top-level mux. If we used edge-triggered events, + * this call would improperly discard them. + */ + if (kevent(actx->mux, NULL, 0, &ev, 1, &timeout) < 0) + { + actx_error_internal(actx, "could not comb kqueue: %m"); + return false; + } + + return true; +#else +#error comb_multiplexer is not implemented on this platform +#endif +} + /* * Enables or disables the timer in the multiplexer set. The timeout value is * in milliseconds (negative values disable the timer). @@ -1416,7 +1520,7 @@ set_timer(struct async_ctx *actx, long timeout) if (timerfd_settime(actx->timerfd, 0 /* no flags */ , &spec, NULL) < 0) { - actx_error(actx, "setting timerfd to %ld: %m", timeout); + actx_error_internal(actx, "setting timerfd to %ld: %m", timeout); return false; } @@ -1446,14 +1550,14 @@ set_timer(struct async_ctx *actx, long timeout) EV_SET(&ev, 1, EVFILT_TIMER, EV_DELETE, 0, 0, 0); if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT) { - actx_error(actx, "deleting kqueue timer: %m"); + actx_error_internal(actx, "deleting kqueue timer: %m"); return false; } EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_DELETE, 0, 0, 0); if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT) { - actx_error(actx, "removing kqueue timer from multiplexer: %m"); + actx_error_internal(actx, "removing kqueue timer from multiplexer: %m"); return false; } @@ -1464,14 +1568,14 @@ set_timer(struct async_ctx *actx, long timeout) EV_SET(&ev, 1, EVFILT_TIMER, (EV_ADD | EV_ONESHOT), 0, timeout, 0); if (kevent(actx->timerfd, &ev, 1, NULL, 0, NULL) < 0) { - actx_error(actx, "setting kqueue timer to %ld: %m", timeout); + actx_error_internal(actx, "setting kqueue timer to %ld: %m", timeout); return false; } EV_SET(&ev, actx->timerfd, EVFILT_READ, EV_ADD, 0, 0, 0); if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0) { - actx_error(actx, "adding kqueue timer to multiplexer: %m"); + actx_error_internal(actx, "adding kqueue timer to multiplexer: %m"); return false; } @@ -1483,40 +1587,20 @@ set_timer(struct async_ctx *actx, long timeout) /* * Returns 1 if the timeout in the multiplexer set has expired since the last - * call to set_timer(), 0 if the timer is still running, or -1 (with an - * actx_error() report) if the timer cannot be queried. + * call to set_timer(), 0 if the timer is either still running or disarmed, or + * -1 (with an actx_error() report) if the timer cannot be queried. */ static int timer_expired(struct async_ctx *actx) { -#if defined(HAVE_SYS_EPOLL_H) - struct itimerspec spec = {0}; - - if (timerfd_gettime(actx->timerfd, &spec) < 0) - { - actx_error(actx, "getting timerfd value: %m"); - return -1; - } - - /* - * This implementation assumes we're using single-shot timers. If you - * change to using intervals, you'll need to reimplement this function - * too, possibly with the read() or select() interfaces for timerfd. - */ - Assert(spec.it_interval.tv_sec == 0 - && spec.it_interval.tv_nsec == 0); - - /* If the remaining time to expiration is zero, we're done. */ - return (spec.it_value.tv_sec == 0 - && spec.it_value.tv_nsec == 0); -#elif defined(HAVE_SYS_EVENT_H) +#if defined(HAVE_SYS_EPOLL_H) || defined(HAVE_SYS_EVENT_H) int res; - /* Is the timer queue ready? */ + /* Is the timer ready? */ res = PQsocketPoll(actx->timerfd, 1 /* forRead */ , 0, 0); if (res < 0) { - actx_error(actx, "checking kqueue for timeout: %m"); + actx_error(actx, "checking timer expiration: %m"); return -1; } @@ -1548,6 +1632,36 @@ register_timer(CURLM *curlm, long timeout, void *ctx) return 0; } +/* + * Removes any expired-timer event from the multiplexer. If was_expired is not + * NULL, it will contain whether or not the timer was expired at time of call. + */ +static bool +drain_timer_events(struct async_ctx *actx, bool *was_expired) +{ + int res; + + res = timer_expired(actx); + if (res < 0) + return false; + + if (res > 0) + { + /* + * Timer is expired. We could drain the event manually from the + * timerfd, but it's easier to simply disable it; that keeps the + * platform-specific code in set_timer(). + */ + if (!set_timer(actx, -1)) + return false; + } + + if (was_expired) + *was_expired = (res > 0); + + return true; +} + /* * Prints Curl request debugging information to stderr. * @@ -1689,7 +1803,7 @@ setup_curl_handles(struct async_ctx *actx) */ CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false); - if (actx->debugging) + if (actx->debug_flags & OAUTHDEBUG_UNSAFE_TRACE) { /* * Set a callback for retrieving error information from libcurl, the @@ -1721,27 +1835,15 @@ setup_curl_handles(struct async_ctx *actx) const long unsafe = CURLPROTO_HTTPS | CURLPROTO_HTTP; #endif - if (actx->debugging) + if (actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) protos = unsafe; CHECK_SETOPT(actx, popt, protos, return false); } - /* - * If we're in debug mode, allow the developer to change the trusted CA - * list. For now, this is not something we expose outside of the UNSAFE - * mode, because it's not clear that it's useful in production: both libpq - * and the user's browser must trust the same authorization servers for - * the flow to work at all, so any changes to the roots are likely to be - * done system-wide. - */ - if (actx->debugging) - { - const char *env; - - if ((env = getenv("PGOAUTHCAFILE")) != NULL) - CHECK_SETOPT(actx, CURLOPT_CAINFO, env, return false); - } + /* Allow the user to change the trusted CA list. */ + if (actx->ca_file != NULL) + CHECK_SETOPT(actx, CURLOPT_CAINFO, actx->ca_file, return false); /* * Suppress the Accept header to make our request as minimal as possible. @@ -1855,6 +1957,12 @@ start_request(struct async_ctx *actx) #define CURL_IGNORE_DEPRECATION(x) x #endif +/* + * Add another macro layer that inserts the needed semicolon, to avoid having + * to write a literal semicolon in the call below, which would break pgindent. + */ +#define PG_CURL_IGNORE_DEPRECATION(x) CURL_IGNORE_DEPRECATION(x;) + /* * Drives the multi handle towards completion. The caller should have already * set up an asynchronous request via start_request(). @@ -1884,11 +1992,10 @@ drive_request(struct async_ctx *actx) * to remove or break this API, so ignore the deprecation. See * * https://curl.se/mail/lib-2024-11/0028.html - * */ - CURL_IGNORE_DEPRECATION( - err = curl_multi_socket_all(actx->curlm, &actx->running); - ) + PG_CURL_IGNORE_DEPRECATION(err = + curl_multi_socket_all(actx->curlm, + &actx->running)); if (err) { @@ -2084,7 +2191,7 @@ finish_discovery(struct async_ctx *actx) /* * Pull the fields we care about from the document. */ - actx->errctx = "failed to parse OpenID discovery document"; + actx->errctx = libpq_gettext("failed to parse OpenID discovery document"); if (!parse_provider(actx, &actx->provider)) return false; /* error message already set */ @@ -2124,7 +2231,7 @@ static bool check_issuer(struct async_ctx *actx, PGconn *conn) { const struct provider *provider = &actx->provider; - const char *oauth_issuer_id = conn_oauth_issuer_id(conn); + const char *oauth_issuer_id = actx->issuer_id; Assert(oauth_issuer_id); /* ensured by setup_oauth_parameters() */ Assert(provider->issuer); /* ensured by parse_provider() */ @@ -2196,7 +2303,7 @@ check_for_device_flow(struct async_ctx *actx) * decent time to bail out if we're not using HTTPS for the endpoints * we'll use for the flow. */ - if (!actx->debugging) + if ((actx->debug_flags & OAUTHDEBUG_UNSAFE_HTTP) == 0) { if (pg_strncasecmp(provider->device_authorization_endpoint, HTTPS_SCHEME, strlen(HTTPS_SCHEME)) != 0) @@ -2227,8 +2334,8 @@ check_for_device_flow(struct async_ctx *actx) static bool add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *conn) { - const char *oauth_client_id = conn_oauth_client_id(conn); - const char *oauth_client_secret = conn_oauth_client_secret(conn); + const char *oauth_client_id = actx->client_id; + const char *oauth_client_secret = actx->client_secret; bool success = false; char *username = NULL; @@ -2311,11 +2418,10 @@ add_client_identification(struct async_ctx *actx, PQExpBuffer reqbody, PGconn *c static bool start_device_authz(struct async_ctx *actx, PGconn *conn) { - const char *oauth_scope = conn_oauth_scope(conn); + const char *oauth_scope = actx->scope; const char *device_authz_uri = actx->provider.device_authorization_endpoint; PQExpBuffer work_buffer = &actx->work_data; - Assert(conn_oauth_client_id(conn)); /* ensured by setup_oauth_parameters() */ Assert(device_authz_uri); /* ensured by check_for_device_flow() */ /* Construct our request body. */ @@ -2352,7 +2458,7 @@ finish_device_authz(struct async_ctx *actx) */ if (response_code == 200) { - actx->errctx = "failed to parse device authorization"; + actx->errctx = libpq_gettext("failed to parse device authorization"); if (!parse_device_authz(actx, &actx->authz)) return false; /* error message already set */ @@ -2403,7 +2509,6 @@ start_token_request(struct async_ctx *actx, PGconn *conn) const char *device_code = actx->authz.device_code; PQExpBuffer work_buffer = &actx->work_data; - Assert(conn_oauth_client_id(conn)); /* ensured by setup_oauth_parameters() */ Assert(token_uri); /* ensured by parse_provider() */ Assert(device_code); /* ensured by parse_device_authz() */ @@ -2440,7 +2545,7 @@ finish_token_request(struct async_ctx *actx, struct token *tok) */ if (response_code == 200) { - actx->errctx = "failed to parse access token response"; + actx->errctx = libpq_gettext("failed to parse access token response"); if (!parse_access_token(actx, tok)) return false; /* error message already set */ @@ -2582,7 +2687,7 @@ prompt_user(struct async_ctx *actx, PGconn *conn) * function will not try to reinitialize Curl on successive calls. */ static bool -initialize_curl(PGconn *conn) +initialize_curl(PGoauthBearerRequestV2 *req) { /* * Don't let the compiler play tricks with this variable. In the @@ -2616,8 +2721,7 @@ initialize_curl(PGconn *conn) goto done; else if (init_successful == PG_BOOL_NO) { - libpq_append_conn_error(conn, - "curl_global_init previously failed during OAuth setup"); + req->error = libpq_gettext("curl_global_init previously failed during OAuth setup"); goto done; } @@ -2635,8 +2739,7 @@ initialize_curl(PGconn *conn) */ if (curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32) != CURLE_OK) { - libpq_append_conn_error(conn, - "curl_global_init failed during OAuth setup"); + req->error = libpq_gettext("curl_global_init failed during OAuth setup"); init_successful = PG_BOOL_NO; goto done; } @@ -2657,11 +2760,11 @@ initialize_curl(PGconn *conn) * In a downgrade situation, the damage is already done. Curl global * state may be corrupted. Be noisy. */ - libpq_append_conn_error(conn, "libcurl is no longer thread-safe\n" - "\tCurl initialization was reported thread-safe when libpq\n" - "\twas compiled, but the currently installed version of\n" - "\tlibcurl reports that it is not. Recompile libpq against\n" - "\tthe installed version of libcurl."); + req->error = libpq_gettext("libcurl is no longer thread-safe\n" + "\tCurl initialization was reported thread-safe when libpq\n" + "\twas compiled, but the currently installed version of\n" + "\tlibcurl reports that it is not. Recompile libpq against\n" + "\tthe installed version of libcurl."); init_successful = PG_BOOL_NO; goto done; } @@ -2691,54 +2794,16 @@ initialize_curl(PGconn *conn) * provider. */ static PostgresPollingStatusType -pg_fe_run_oauth_flow_impl(PGconn *conn) +pg_fe_run_oauth_flow_impl(PGconn *conn, PGoauthBearerRequestV2 *request, + int *altsock) { - fe_oauth_state *state = conn_sasl_state(conn); - struct async_ctx *actx; + struct async_ctx *actx = request->v1.user; char *oauth_token = NULL; - PQExpBuffer errbuf; - - if (!initialize_curl(conn)) - return PGRES_POLLING_FAILED; - - if (!state->async_ctx) - { - /* - * Create our asynchronous state, and hook it into the upper-level - * OAuth state immediately, so any failures below won't leak the - * context allocation. - */ - actx = calloc(1, sizeof(*actx)); - if (!actx) - { - libpq_append_conn_error(conn, "out of memory"); - return PGRES_POLLING_FAILED; - } - - actx->mux = PGINVALID_SOCKET; - actx->timerfd = -1; - - /* Should we enable unsafe features? */ - actx->debugging = oauth_unsafe_debugging_enabled(); - - state->async_ctx = actx; - - initPQExpBuffer(&actx->work_data); - initPQExpBuffer(&actx->errbuf); - - if (!setup_multiplexer(actx)) - goto error_return; - - if (!setup_curl_handles(actx)) - goto error_return; - } - - actx = state->async_ctx; do { /* By default, the multiplexer is the altsock. Reassign as desired. */ - set_conn_altsock(conn, actx->mux); + *altsock = actx->mux; switch (actx->step) { @@ -2751,38 +2816,64 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) { PostgresPollingStatusType status; + /* + * Clear any expired timeout before calling back into + * Curl. Curl is not guaranteed to do this for us, because + * its API expects us to use single-shot (i.e. + * edge-triggered) timeouts, and ours are level-triggered + * via the mux. + * + * This can't be combined with the comb_multiplexer() call + * below: we might accidentally clear a short timeout that + * was both set and expired during the call to + * drive_request(). + */ + if (!drain_timer_events(actx, NULL)) + goto error_return; + + /* Move the request forward. */ status = drive_request(actx); if (status == PGRES_POLLING_FAILED) goto error_return; - else if (status != PGRES_POLLING_OK) - { - /* not done yet */ - return status; - } + else if (status == PGRES_POLLING_OK) + break; /* done! */ - break; + /* + * This request is still running. + * + * Make sure that stale events don't cause us to come back + * early. (Currently, this can occur only with kqueue.) If + * this is forgotten, the multiplexer can get stuck in a + * signaled state and we'll burn CPU cycles pointlessly. + */ + if (!comb_multiplexer(actx)) + goto error_return; + + return status; } case OAUTH_STEP_WAIT_INTERVAL: - - /* - * The client application is supposed to wait until our timer - * expires before calling PQconnectPoll() again, but that - * might not happen. To avoid sending a token request early, - * check the timer before continuing. - */ - if (!timer_expired(actx)) { - set_conn_altsock(conn, actx->timerfd); - return PGRES_POLLING_READING; - } + bool expired; - /* Disable the expired timer. */ - if (!set_timer(actx, -1)) - goto error_return; + /* + * The client application is supposed to wait until our + * timer expires before calling PQconnectPoll() again, but + * that might not happen. To avoid sending a token request + * early, check the timer before continuing. + */ + if (!drain_timer_events(actx, &expired)) + goto error_return; - break; + if (!expired) + { + *altsock = actx->timerfd; + return PGRES_POLLING_READING; + } + + break; + } } /* @@ -2793,8 +2884,8 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) switch (actx->step) { case OAUTH_STEP_INIT: - actx->errctx = "failed to fetch OpenID discovery document"; - if (!start_discovery(actx, conn_oauth_discovery_uri(conn))) + actx->errctx = libpq_gettext("failed to fetch OpenID discovery document"); + if (!start_discovery(actx, actx->discovery_uri)) goto error_return; actx->step = OAUTH_STEP_DISCOVERY; @@ -2807,11 +2898,11 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) if (!check_issuer(actx, conn)) goto error_return; - actx->errctx = "cannot run OAuth device authorization"; + actx->errctx = libpq_gettext("cannot run OAuth device authorization"); if (!check_for_device_flow(actx)) goto error_return; - actx->errctx = "failed to obtain device authorization"; + actx->errctx = libpq_gettext("failed to obtain device authorization"); if (!start_device_authz(actx, conn)) goto error_return; @@ -2822,7 +2913,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) if (!finish_device_authz(actx)) goto error_return; - actx->errctx = "failed to obtain access token"; + actx->errctx = libpq_gettext("failed to obtain access token"); if (!start_token_request(actx, conn)) goto error_return; @@ -2834,10 +2925,10 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) goto error_return; /* - * Hook any oauth_token into the PGconn immediately so that - * the allocation isn't lost in case of an error. + * Hook any oauth_token into the request struct immediately so + * that the allocation isn't lost in case of an error. */ - set_conn_oauth_token(conn, oauth_token); + request->v1.token = oauth_token; if (!actx->user_prompted) { @@ -2866,14 +2957,14 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) * the client wait directly on the timerfd rather than the * multiplexer. */ - set_conn_altsock(conn, actx->timerfd); + *altsock = actx->timerfd; actx->step = OAUTH_STEP_WAIT_INTERVAL; actx->running = 1; break; case OAUTH_STEP_WAIT_INTERVAL: - actx->errctx = "failed to obtain access token"; + actx->errctx = libpq_gettext("failed to obtain access token"); if (!start_token_request(actx, conn)) goto error_return; @@ -2892,46 +2983,21 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) return oauth_token ? PGRES_POLLING_OK : PGRES_POLLING_READING; error_return: - errbuf = conn_errorMessage(conn); - - /* - * Assemble the three parts of our error: context, body, and detail. See - * also the documentation for struct async_ctx. - */ - if (actx->errctx) - appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx)); - - if (PQExpBufferDataBroken(actx->errbuf)) - appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); - else - appendPQExpBufferStr(errbuf, actx->errbuf.data); - - if (actx->curl_err[0]) - { - appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); - - /* Sometimes libcurl adds a newline to the error buffer. :( */ - if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n') - { - errbuf->data[errbuf->len - 2] = ')'; - errbuf->data[errbuf->len - 1] = '\0'; - errbuf->len--; - } - } - - appendPQExpBufferChar(errbuf, '\n'); + append_actx_error(request, actx); return PGRES_POLLING_FAILED; } /* - * The top-level entry point. This is a convenient place to put necessary - * wrapper logic before handing off to the true implementation, above. + * The top-level entry point for the flow. This is a convenient place to put + * necessary wrapper logic before handing off to the true implementation, above. */ -PostgresPollingStatusType -pg_fe_run_oauth_flow(PGconn *conn) +static PostgresPollingStatusType +pg_fe_run_oauth_flow(PGconn *conn, struct PGoauthBearerRequest *request, + int *altsock) { PostgresPollingStatusType result; + struct async_ctx *actx = request->user; #ifndef WIN32 sigset_t osigset; bool sigpipe_pending; @@ -2958,7 +3024,22 @@ pg_fe_run_oauth_flow(PGconn *conn) masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0); #endif - result = pg_fe_run_oauth_flow_impl(conn); + result = pg_fe_run_oauth_flow_impl(conn, + (PGoauthBearerRequestV2 *) request, + altsock); + + /* + * To assist with finding bugs in comb_multiplexer() and + * drain_timer_events(), when we're in debug mode, track the total number + * of calls to this function and print that at the end of the flow. + */ + if (actx->debug_flags & OAUTHDEBUG_CALL_COUNT) + { + actx->dbg_num_calls++; + if (result == PGRES_POLLING_OK || result == PGRES_POLLING_FAILED) + fprintf(stderr, "[libpq] total number of polls: %d\n", + actx->dbg_num_calls); + } #ifndef WIN32 if (masked) @@ -2973,3 +3054,110 @@ pg_fe_run_oauth_flow(PGconn *conn) return result; } + +/* + * Callback registration for OAUTHBEARER. libpq calls this once per OAuth + * connection. + */ +int +pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request) +{ + struct async_ctx *actx; + PQconninfoOption *conninfo = NULL; + + if (!initialize_curl(request)) + return -1; + + /* + * Create our asynchronous state, and hook it into the upper-level OAuth + * state immediately, so any failures below won't leak the context + * allocation. + */ + actx = calloc(1, sizeof(*actx)); + if (!actx) + goto oom; + + actx->mux = PGINVALID_SOCKET; + actx->timerfd = -1; + + /* + * Now we have a valid (but still useless) actx, so we can fill in the + * request object. From this point onward, failures will result in a call + * to pg_fe_cleanup_oauth_flow(). Further cleanup logic belongs there. + */ + request->v1.async = pg_fe_run_oauth_flow; + request->v1.cleanup = pg_fe_cleanup_oauth_flow; + request->v1.user = actx; + + /* + * Now finish filling in the actx. + */ + + /* Parse debug flags from the environment. */ + actx->debug_flags = oauth_parse_debug_flags(); + + initPQExpBuffer(&actx->work_data); + initPQExpBuffer(&actx->errbuf); + + /* Pull relevant connection options. */ + conninfo = PQconninfo(conn); + if (!conninfo) + goto oom; + + for (PQconninfoOption *opt = conninfo; opt->keyword; opt++) + { + if (!opt->val) + continue; /* simplifies the strdup logic below */ + + if (strcmp(opt->keyword, "oauth_client_id") == 0) + { + actx->client_id = strdup(opt->val); + if (!actx->client_id) + goto oom; + } + else if (strcmp(opt->keyword, "oauth_client_secret") == 0) + { + actx->client_secret = strdup(opt->val); + if (!actx->client_secret) + goto oom; + } + else if (strcmp(opt->keyword, "oauth_ca_file") == 0) + { + actx->ca_file = strdup(opt->val); + if (!actx->ca_file) + goto oom; + } + } + + PQconninfoFree(conninfo); + conninfo = NULL; /* keeps `goto oom` safe */ + + actx->discovery_uri = request->v1.openid_configuration; + actx->issuer_id = request->issuer; + actx->scope = request->v1.scope; + + Assert(actx->client_id); /* ensured by setup_oauth_parameters() */ + Assert(actx->issuer_id); /* ensured by setup_oauth_parameters() */ + Assert(actx->discovery_uri); /* ensured by oauth_exchange() */ + + if (!setup_multiplexer(actx)) + { + append_actx_error(request, actx); + return -1; + } + + if (!setup_curl_handles(actx)) + { + append_actx_error(request, actx); + return -1; + } + + return 0; + +oom: + if (conninfo) + PQconninfoFree(conninfo); + + request->error = libpq_gettext("out of memory"); + return -1; +} diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h index 248d0424ad0d6..1d4dd766217cb 100644 --- a/src/interfaces/libpq-oauth/oauth-curl.h +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -4,7 +4,7 @@ * * Definitions for OAuth Device Authorization module * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq-oauth/oauth-curl.h @@ -17,8 +17,8 @@ #include "libpq-fe.h" -/* Exported async-auth callbacks. */ -extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); +/* Exported flow callback. */ +extern PGDLLEXPORT int pg_start_oauthbearer(PGconn *conn, + PGoauthBearerRequestV2 *request); #endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c index 45fdc7579f285..004d41f02aaa1 100644 --- a/src/interfaces/libpq-oauth/oauth-utils.c +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -5,7 +5,7 @@ * "Glue" helpers providing a copy of some internal APIs from libpq. At * some point in the future, we might be able to deduplicate. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -35,85 +35,18 @@ pgthreadlock_t pg_g_threadlock; static libpq_gettext_func libpq_gettext_impl; -conn_errorMessage_func conn_errorMessage; -conn_oauth_client_id_func conn_oauth_client_id; -conn_oauth_client_secret_func conn_oauth_client_secret; -conn_oauth_discovery_uri_func conn_oauth_discovery_uri; -conn_oauth_issuer_id_func conn_oauth_issuer_id; -conn_oauth_scope_func conn_oauth_scope; -conn_sasl_state_func conn_sasl_state; - -set_conn_altsock_func set_conn_altsock; -set_conn_oauth_token_func set_conn_oauth_token; - /*- * Initializes libpq-oauth by setting necessary callbacks. * - * The current implementation relies on the following private implementation - * details of libpq: - * - * - pg_g_threadlock: protects libcurl initialization if the underlying Curl - * installation is not threadsafe - * - * - libpq_gettext: translates error messages using libpq's message domain - * - * The implementation also needs access to several members of the PGconn struct, - * which are not guaranteed to stay in place across minor versions. Accessors - * (named conn_*) and mutators (named set_conn_*) are injected here. + * The current implementation relies on libpq_gettext to translate error + * messages using libpq's message domain, so libpq injects it here. We also use + * this chance to initialize our threadlock. */ void -libpq_oauth_init(pgthreadlock_t threadlock_impl, - libpq_gettext_func gettext_impl, - conn_errorMessage_func errmsg_impl, - conn_oauth_client_id_func clientid_impl, - conn_oauth_client_secret_func clientsecret_impl, - conn_oauth_discovery_uri_func discoveryuri_impl, - conn_oauth_issuer_id_func issuerid_impl, - conn_oauth_scope_func scope_impl, - conn_sasl_state_func saslstate_impl, - set_conn_altsock_func setaltsock_impl, - set_conn_oauth_token_func settoken_impl) +libpq_oauth_init(libpq_gettext_func gettext_impl) { - pg_g_threadlock = threadlock_impl; + pg_g_threadlock = PQgetThreadLock(); libpq_gettext_impl = gettext_impl; - conn_errorMessage = errmsg_impl; - conn_oauth_client_id = clientid_impl; - conn_oauth_client_secret = clientsecret_impl; - conn_oauth_discovery_uri = discoveryuri_impl; - conn_oauth_issuer_id = issuerid_impl; - conn_oauth_scope = scope_impl; - conn_sasl_state = saslstate_impl; - set_conn_altsock = setaltsock_impl; - set_conn_oauth_token = settoken_impl; -} - -/* - * Append a formatted string to the error message buffer of the given - * connection, after translating it. This is a copy of libpq's internal API. - */ -void -libpq_append_conn_error(PGconn *conn, const char *fmt,...) -{ - int save_errno = errno; - bool done; - va_list args; - PQExpBuffer errorMessage = conn_errorMessage(conn); - - Assert(fmt[strlen(fmt) - 1] != '\n'); - - if (PQExpBufferBroken(errorMessage)) - return; /* already failed */ - - /* Loop in case we have to retry after enlarging the buffer. */ - do - { - errno = save_errno; - va_start(args, fmt); - done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); - va_end(args); - } while (!done); - - appendPQExpBufferChar(errorMessage, '\n'); } #ifdef ENABLE_NLS @@ -142,17 +75,6 @@ libpq_gettext(const char *msgid) #endif /* ENABLE_NLS */ -/* - * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. - */ -bool -oauth_unsafe_debugging_enabled(void) -{ - const char *env = getenv("PGOAUTHDEBUG"); - - return (env && strcmp(env, "UNSAFE") == 0); -} - /* * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by * pq_block/reset_sigpipe(). diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h index f4ffefef208f5..dacd2dbacfeec 100644 --- a/src/interfaces/libpq-oauth/oauth-utils.h +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -4,7 +4,7 @@ * * Definitions providing missing libpq internal APIs * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq-oauth/oauth-utils.h @@ -15,53 +15,13 @@ #ifndef OAUTH_UTILS_H #define OAUTH_UTILS_H -#include "fe-auth-oauth.h" #include "libpq-fe.h" #include "pqexpbuffer.h" -/* - * A bank of callbacks to safely access members of PGconn, which are all passed - * to libpq_oauth_init() by libpq. - * - * Keep these aligned with the definitions in fe-auth-oauth.c as well as the - * static declarations in oauth-curl.c. - */ -#define DECLARE_GETTER(TYPE, MEMBER) \ - typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \ - extern conn_ ## MEMBER ## _func conn_ ## MEMBER; - -#define DECLARE_SETTER(TYPE, MEMBER) \ - typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \ - extern set_conn_ ## MEMBER ## _func set_conn_ ## MEMBER; - -DECLARE_GETTER(PQExpBuffer, errorMessage); -DECLARE_GETTER(char *, oauth_client_id); -DECLARE_GETTER(char *, oauth_client_secret); -DECLARE_GETTER(char *, oauth_discovery_uri); -DECLARE_GETTER(char *, oauth_issuer_id); -DECLARE_GETTER(char *, oauth_scope); -DECLARE_GETTER(fe_oauth_state *, sasl_state); - -DECLARE_SETTER(pgsocket, altsock); -DECLARE_SETTER(char *, oauth_token); - -#undef DECLARE_GETTER -#undef DECLARE_SETTER - typedef char *(*libpq_gettext_func) (const char *msgid); /* Initializes libpq-oauth. */ -extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, - libpq_gettext_func gettext_impl, - conn_errorMessage_func errmsg_impl, - conn_oauth_client_id_func clientid_impl, - conn_oauth_client_secret_func clientsecret_impl, - conn_oauth_discovery_uri_func discoveryuri_impl, - conn_oauth_issuer_id_func issuerid_impl, - conn_oauth_scope_func scope_impl, - conn_sasl_state_func saslstate_impl, - set_conn_altsock_func setaltsock_impl, - set_conn_oauth_token_func settoken_impl); +extern PGDLLEXPORT void libpq_oauth_init(libpq_gettext_func gettext_impl); /* * Duplicated APIs, copied from libpq (primarily libpq-int.h, which we cannot @@ -75,8 +35,6 @@ typedef enum PG_BOOL_NO /* No (false) */ } PGTernaryBool; -extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); -extern bool oauth_unsafe_debugging_enabled(void); extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); diff --git a/src/interfaces/libpq-oauth/t/001_oauth.pl b/src/interfaces/libpq-oauth/t/001_oauth.pl new file mode 100644 index 0000000000000..aa9504689c002 --- /dev/null +++ b/src/interfaces/libpq-oauth/t/001_oauth.pl @@ -0,0 +1,24 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Utils; +use Test::More; + +# Defer entirely to the oauth_tests executable. stdout/err is routed through +# Test::More so that our logging infrastructure can handle it correctly. Using +# IPC::Run::new_chunker seems to help interleave the two streams a little better +# than without. +# +# TODO: prove can also deal with native executables itself, which we could +# probably make use of via PROVE_TESTS on the Makefile side. But the Meson setup +# calls Perl directly, which would require more code to work around... and +# there's still the matter of logging. +my $builder = Test::More->builder; +my $out = $builder->output; +my $err = $builder->failure_output; + +IPC::Run::run ['oauth_tests'], + '>' => (IPC::Run::new_chunker, sub { $out->print($_[0]) }), + '2>' => (IPC::Run::new_chunker, sub { $err->print($_[0]) }) + or die "oauth_tests returned $?"; diff --git a/src/interfaces/libpq-oauth/test-oauth-curl.c b/src/interfaces/libpq-oauth/test-oauth-curl.c new file mode 100644 index 0000000000000..9db39053dd340 --- /dev/null +++ b/src/interfaces/libpq-oauth/test-oauth-curl.c @@ -0,0 +1,527 @@ +/* + * test-oauth-curl.c + * + * A unit test driver for libpq-oauth. This #includes oauth-curl.c, which lets + * the tests reference static functions and other internals. + * + * USE_ASSERT_CHECKING is required, to make it easy for tests to wrap + * must-succeed code as part of test setup. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + */ + +#include "oauth-curl.c" + +#include + +#ifdef USE_ASSERT_CHECKING + +/* + * TAP Helpers + */ + +static int num_tests = 0; + +/* + * Reports ok/not ok to the TAP stream on stdout. + */ +#define ok(OK, TEST) \ + ok_impl(OK, TEST, #OK, __FILE__, __LINE__) + +static bool +ok_impl(bool ok, const char *test, const char *teststr, const char *file, int line) +{ + printf("%sok %d - %s\n", ok ? "" : "not ", ++num_tests, test); + + if (!ok) + { + printf("# at %s:%d:\n", file, line); + printf("# expression is false: %s\n", teststr); + } + + return ok; +} + +/* + * Like ok(this == that), but with more diagnostics on failure. + * + * Only works on ints, but luckily that's all we need here. Note that the much + * simpler-looking macro implementation + * + * is_diag(ok(THIS == THAT, TEST), THIS, #THIS, THAT, #THAT) + * + * suffers from multiple evaluation of the macro arguments... + */ +#define is(THIS, THAT, TEST) \ + do { \ + int this_ = (THIS), \ + that_ = (THAT); \ + is_diag( \ + ok_impl(this_ == that_, TEST, #THIS " == " #THAT, __FILE__, __LINE__), \ + this_, #THIS, that_, #THAT \ + ); \ + } while (0) + +static bool +is_diag(bool ok, int this, const char *thisstr, int that, const char *thatstr) +{ + if (!ok) + printf("# %s = %d; %s = %d\n", thisstr, this, thatstr, that); + + return ok; +} + +/* + * Utilities + */ + +/* + * Creates a partially-initialized async_ctx for the purposes of testing. Free + * with free_test_actx(). + */ +static struct async_ctx * +init_test_actx(void) +{ + struct async_ctx *actx; + + actx = calloc(1, sizeof(*actx)); + Assert(actx); + + actx->mux = PGINVALID_SOCKET; + actx->timerfd = -1; + actx->debug_flags = OAUTHDEBUG_LEGACY_UNSAFE; + + initPQExpBuffer(&actx->errbuf); + + Assert(setup_multiplexer(actx)); + + return actx; +} + +static void +free_test_actx(struct async_ctx *actx) +{ + termPQExpBuffer(&actx->errbuf); + + if (actx->mux != PGINVALID_SOCKET) + close(actx->mux); + if (actx->timerfd >= 0) + close(actx->timerfd); + + free(actx); +} + +static char dummy_buf[4 * 1024]; /* for fill_pipe/drain_pipe */ + +/* + * Writes to the write side of a pipe until it won't take any more data. Returns + * the amount written. + */ +static ssize_t +fill_pipe(int fd) +{ + int mode; + ssize_t written = 0; + + /* Don't block. */ + Assert((mode = fcntl(fd, F_GETFL)) != -1); + Assert(fcntl(fd, F_SETFL, mode | O_NONBLOCK) == 0); + + while (true) + { + ssize_t w; + + w = write(fd, dummy_buf, sizeof(dummy_buf)); + if (w < 0) + { + if (errno != EAGAIN && errno != EWOULDBLOCK) + { + perror("write to pipe"); + written = -1; + } + break; + } + + written += w; + } + + /* Reset the descriptor flags. */ + Assert(fcntl(fd, F_SETFD, mode) == 0); + + return written; +} + +/* + * Drains the requested amount of data from the read side of a pipe. + */ +static bool +drain_pipe(int fd, ssize_t n) +{ + Assert(n > 0); + + while (n) + { + size_t to_read = (n <= sizeof(dummy_buf)) ? n : sizeof(dummy_buf); + ssize_t drained; + + drained = read(fd, dummy_buf, to_read); + if (drained < 0) + { + perror("read from pipe"); + return false; + } + + n -= drained; + } + + return true; +} + +/* + * Tests whether the multiplexer is marked ready by the deadline. This is a + * macro so that file/line information makes sense during failures. + * + * NB: our current multiplexer implementations (epoll/kqueue) are *readable* + * when the underlying libcurl sockets are *writable*. This behavior is pinned + * here to record that expectation; PGRES_POLLING_READING is hardcoded + * throughout the flow and would need to be changed if a new multiplexer does + * something different. + */ +#define mux_is_ready(MUX, DEADLINE, TEST) \ + do { \ + int res_ = PQsocketPoll(MUX, 1, 0, DEADLINE); \ + Assert(res_ != -1); \ + ok(res_ > 0, "multiplexer is ready " TEST); \ + } while (0) + +/* + * The opposite of mux_is_ready(). + */ +#define mux_is_not_ready(MUX, TEST) \ + do { \ + int res_ = PQsocketPoll(MUX, 1, 0, 0); \ + Assert(res_ != -1); \ + is(res_, 0, "multiplexer is not ready " TEST); \ + } while (0) + +/* + * Test Suites + */ + +/* Per-suite timeout. Set via the PG_TEST_TIMEOUT_DEFAULT envvar. */ +static pg_usec_time_t timeout_us = 180 * 1000 * 1000; + +static void +test_set_timer(void) +{ + struct async_ctx *actx = init_test_actx(); + const pg_usec_time_t deadline = PQgetCurrentTimeUSec() + timeout_us; + + printf("# test_set_timer\n"); + + /* A zero-duration timer should result in a near-immediate ready signal. */ + Assert(set_timer(actx, 0)); + mux_is_ready(actx->mux, deadline, "when timer expires"); + is(timer_expired(actx), 1, "timer_expired() returns 1 when timer expires"); + + /* Resetting the timer far in the future should unset the ready signal. */ + Assert(set_timer(actx, INT_MAX)); + mux_is_not_ready(actx->mux, "when timer is reset to the future"); + is(timer_expired(actx), 0, "timer_expired() returns 0 with unexpired timer"); + + /* Setting another zero-duration timer should override the previous one. */ + Assert(set_timer(actx, 0)); + mux_is_ready(actx->mux, deadline, "when timer is re-expired"); + is(timer_expired(actx), 1, "timer_expired() returns 1 when timer is re-expired"); + + /* And disabling that timer should once again unset the ready signal. */ + Assert(set_timer(actx, -1)); + mux_is_not_ready(actx->mux, "when timer is unset"); + is(timer_expired(actx), 0, "timer_expired() returns 0 when timer is unset"); + + { + bool expired; + + /* Make sure drain_timer_events() functions correctly as well. */ + Assert(set_timer(actx, 0)); + mux_is_ready(actx->mux, deadline, "when timer is re-expired (drain_timer_events)"); + + Assert(drain_timer_events(actx, &expired)); + mux_is_not_ready(actx->mux, "when timer is drained after expiring"); + is(expired, 1, "drain_timer_events() reports expiration"); + is(timer_expired(actx), 0, "timer_expired() returns 0 after timer is drained"); + + /* A second drain should do nothing. */ + Assert(drain_timer_events(actx, &expired)); + mux_is_not_ready(actx->mux, "when timer is drained a second time"); + is(expired, 0, "drain_timer_events() reports no expiration"); + is(timer_expired(actx), 0, "timer_expired() still returns 0"); + } + + free_test_actx(actx); +} + +static void +test_register_socket(void) +{ + struct async_ctx *actx = init_test_actx(); + int pipefd[2]; + int rfd, + wfd; + bool bidirectional; + + /* Create a local pipe for communication. */ + Assert(pipe(pipefd) == 0); + rfd = pipefd[0]; + wfd = pipefd[1]; + + /* + * Some platforms (FreeBSD) implement bidirectional pipes, affecting the + * behavior of some of these tests. Store that knowledge for later. + */ + bidirectional = PQsocketPoll(rfd /* read */ , 0, 1 /* write */ , 0) > 0; + + /* + * This suite runs twice -- once using CURL_POLL_IN/CURL_POLL_OUT for + * read/write operations, respectively, and once using CURL_POLL_INOUT for + * both sides. + */ + for (int inout = 0; inout < 2; inout++) + { + const int in_event = inout ? CURL_POLL_INOUT : CURL_POLL_IN; + const int out_event = inout ? CURL_POLL_INOUT : CURL_POLL_OUT; + const pg_usec_time_t deadline = PQgetCurrentTimeUSec() + timeout_us; + size_t bidi_pipe_size = 0; /* silence compiler warnings */ + + printf("# test_register_socket %s\n", inout ? "(INOUT)" : ""); + + /* + * At the start of the test, the read side should be blocked and the + * write side should be open. (There's a mistake at the end of this + * loop otherwise.) + */ + Assert(PQsocketPoll(rfd, 1, 0, 0) == 0); + Assert(PQsocketPoll(wfd, 0, 1, 0) > 0); + + /* + * For bidirectional systems, emulate unidirectional behavior here by + * filling up the "read side" of the pipe. + */ + if (bidirectional) + Assert((bidi_pipe_size = fill_pipe(rfd)) > 0); + + /* Listen on the read side. The multiplexer shouldn't be ready yet. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when fd is not readable"); + + /* Writing to the pipe should result in a read-ready multiplexer. */ + Assert(write(wfd, "x", 1) == 1); + mux_is_ready(actx->mux, deadline, "when fd is readable"); + + /* + * Update the registration to wait on write events instead. The + * multiplexer should be unset. + */ + Assert(register_socket(NULL, rfd, CURL_POLL_OUT, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when waiting for writes on readable fd"); + + /* Re-register for read events. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when waiting for reads again"); + + /* Stop listening. The multiplexer should be unset. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when readable fd is removed"); + + /* Listen again. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when readable fd is re-added"); + + /* + * Draining the pipe should unset the multiplexer again, once the old + * event is cleared. + */ + Assert(drain_pipe(rfd, 1)); + Assert(comb_multiplexer(actx)); + mux_is_not_ready(actx->mux, "when fd is drained"); + + /* Undo any unidirectional emulation. */ + if (bidirectional) + Assert(drain_pipe(wfd, bidi_pipe_size)); + + /* Listen on the write side. An empty buffer should be writable. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + Assert(register_socket(NULL, wfd, out_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when fd is writable"); + + /* As above, wait on read events instead. */ + Assert(register_socket(NULL, wfd, CURL_POLL_IN, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when waiting for reads on writable fd"); + + /* Re-register for write events. */ + Assert(register_socket(NULL, wfd, out_event, actx, NULL) == 0); + mux_is_ready(actx->mux, deadline, "when waiting for writes again"); + + { + ssize_t written; + + /* + * Fill the pipe. Once the old writable event is cleared, the mux + * should not be ready. + */ + Assert((written = fill_pipe(wfd)) > 0); + printf("# pipe buffer is full at %zd bytes\n", written); + + Assert(comb_multiplexer(actx)); + mux_is_not_ready(actx->mux, "when fd buffer is full"); + + /* Drain the pipe again. */ + Assert(drain_pipe(rfd, written)); + mux_is_ready(actx->mux, deadline, "when fd buffer is drained"); + } + + /* Stop listening. */ + Assert(register_socket(NULL, wfd, CURL_POLL_REMOVE, actx, NULL) == 0); + mux_is_not_ready(actx->mux, "when fd is removed"); + + /* Make sure an expired timer doesn't interfere with event draining. */ + { + bool expired; + + /* Make the rfd appear unidirectional if necessary. */ + if (bidirectional) + Assert((bidi_pipe_size = fill_pipe(rfd)) > 0); + + /* Set the timer and wait for it to expire. */ + Assert(set_timer(actx, 0)); + Assert(PQsocketPoll(actx->timerfd, 1, 0, deadline) > 0); + is(timer_expired(actx), 1, "timer is expired"); + + /* Register for read events and make the fd readable. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + Assert(write(wfd, "x", 1) == 1); + mux_is_ready(actx->mux, deadline, "when fd is readable and timer expired"); + + /* + * Draining the pipe should unset the multiplexer again, once the + * old event is drained and the timer is reset. + * + * Order matters, since comb_multiplexer() doesn't have to remove + * stale events when active events exist. Follow the call sequence + * used in the code: drain the timer expiration, drain the pipe, + * then clear the stale events. + */ + Assert(drain_timer_events(actx, &expired)); + Assert(drain_pipe(rfd, 1)); + Assert(comb_multiplexer(actx)); + + is(expired, 1, "drain_timer_events() reports expiration"); + is(timer_expired(actx), 0, "timer is no longer expired"); + mux_is_not_ready(actx->mux, "when fd is drained and timer reset"); + + /* Stop listening. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + + /* Undo any unidirectional emulation. */ + if (bidirectional) + Assert(drain_pipe(wfd, bidi_pipe_size)); + } + + /* Ensure comb_multiplexer() can handle multiple stale events. */ + { + int rfd2, + wfd2; + + /* Create a second local pipe. */ + Assert(pipe(pipefd) == 0); + rfd2 = pipefd[0]; + wfd2 = pipefd[1]; + + /* Make both rfds appear unidirectional if necessary. */ + if (bidirectional) + { + Assert((bidi_pipe_size = fill_pipe(rfd)) > 0); + Assert(fill_pipe(rfd2) == bidi_pipe_size); + } + + /* Register for read events on both fds, and make them readable. */ + Assert(register_socket(NULL, rfd, in_event, actx, NULL) == 0); + Assert(register_socket(NULL, rfd2, in_event, actx, NULL) == 0); + + Assert(write(wfd, "x", 1) == 1); + Assert(write(wfd2, "x", 1) == 1); + + mux_is_ready(actx->mux, deadline, "when two fds are readable"); + + /* + * Drain both fds. comb_multiplexer() should then ensure that the + * mux is no longer readable. + */ + Assert(drain_pipe(rfd, 1)); + Assert(drain_pipe(rfd2, 1)); + Assert(comb_multiplexer(actx)); + mux_is_not_ready(actx->mux, "when two fds are drained"); + + /* Stop listening. */ + Assert(register_socket(NULL, rfd, CURL_POLL_REMOVE, actx, NULL) == 0); + Assert(register_socket(NULL, rfd2, CURL_POLL_REMOVE, actx, NULL) == 0); + + /* Undo any unidirectional emulation. */ + if (bidirectional) + { + Assert(drain_pipe(wfd, bidi_pipe_size)); + Assert(drain_pipe(wfd2, bidi_pipe_size)); + } + + close(rfd2); + close(wfd2); + } + } + + close(rfd); + close(wfd); + free_test_actx(actx); +} + +int +main(int argc, char *argv[]) +{ + const char *timeout; + + /* Grab the default timeout. */ + timeout = getenv("PG_TEST_TIMEOUT_DEFAULT"); + if (timeout) + { + int timeout_s = atoi(timeout); + + if (timeout_s > 0) + timeout_us = timeout_s * 1000 * 1000; + } + + /* + * Set up line buffering for our output, to let stderr interleave in the + * log files. + */ + setvbuf(stdout, NULL, PG_IOLBF, 0); + + test_set_timer(); + test_register_socket(); + + printf("1..%d\n", num_tests); + return 0; +} + +#else /* !USE_ASSERT_CHECKING */ + +/* + * Skip the test suite when we don't have assertions. + */ +int +main(int argc, char *argv[]) +{ + printf("1..0 # skip: cassert is not enabled\n"); + + return 0; +} + +#endif /* USE_ASSERT_CHECKING */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 853aab4b1b886..0963995eed422 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/interfaces/libpq library # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/interfaces/libpq/Makefile @@ -24,7 +24,7 @@ NAME= pq SO_MAJOR_VERSION= 5 SO_MINOR_VERSION= $(MAJORVERSION) -override CPPFLAGS := -I$(srcdir) $(CPPFLAGS) -I$(top_builddir)/src/port -I$(top_srcdir)/src/port +override CPPFLAGS := -I$(srcdir) -I$(top_builddir)/src/port -I$(top_srcdir)/src/port $(CPPFLAGS) ifneq ($(PORTNAME), win32) override CFLAGS += $(PTHREAD_CFLAGS) endif @@ -87,7 +87,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -ldl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif @@ -129,24 +129,15 @@ $(shlib): $(OBJS_SHLIB) $(stlib): override OBJS += $(OBJS_STATIC) $(stlib): $(OBJS_STATIC) -# Check for functions that libpq must not call, currently just exit(). -# (Ideally we'd reject abort() too, but there are various scenarios where -# build toolchains insert abort() calls, e.g. to implement assert().) -# If nm doesn't exist or doesn't work on shlibs, this test will do nothing, -# which is fine. The exclusion of __cxa_atexit is necessary on OpenBSD, -# which seems to insert references to that even in pure C code. Excluding -# __tsan_func_exit is necessary when using ThreadSanitizer data race detector -# which use this function for instrumentation of function exit. -# Skip the test when profiling, as gcc may insert exit() calls for that. -# Also skip the test on platforms where libpq infrastructure may be provided -# by statically-linked libraries, as we can't expect them to honor this -# coding rule. +# Check for functions that libpq must not call. See libpq_check.pl for the +# full set of platform rules. Skip the test when profiling, as gcc may +# insert exit() calls for that. Also skip the test on platforms where libpq +# infrastructure may be provided by statically-linked libraries, as we can't +# expect them to honor this coding rule. libpq-refs-stamp: $(shlib) ifneq ($(enable_coverage), yes) -ifeq (,$(filter solaris,$(PORTNAME))) - @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ - echo 'libpq must not be calling any function which invokes exit'; exit 1; \ - fi +ifeq (,$(filter aix,$(PORTNAME))) + $(PERL) $(srcdir)/libpq_check.pl --input_file $< --nm='$(NM)' endif endif touch $@ diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 0625cf39e9af3..1e3d5bd5867f5 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -205,9 +205,9 @@ PQcancelFinish 202 PQsocketPoll 203 PQsetChunkedRowsMode 204 PQgetCurrentTimeUSec 205 -PQservice 206 -PQsetAuthDataHook 207 -PQgetAuthDataHook 208 -PQdefaultAuthDataHook 209 -PQfullProtocolVersion 210 -appendPQExpBufferVA 211 +PQsetAuthDataHook 206 +PQgetAuthDataHook 207 +PQdefaultAuthDataHook 208 +PQfullProtocolVersion 209 +appendPQExpBufferVA 210 +PQgetThreadLock 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index d146c5f567c29..826f7461cb3b4 100644 --- a/src/interfaces/libpq/fe-auth-oauth.c +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -4,7 +4,7 @@ * The front-end (client) implementation of OAuth/OIDC authentication * using the SASL OAUTHBEARER mechanism. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -26,7 +26,14 @@ #include "fe-auth.h" #include "fe-auth-oauth.h" #include "mb/pg_wchar.h" +#include "oauth-debug.h" #include "pg_config_paths.h" +#include "utils/memdebug.h" + +static PostgresPollingStatusType do_async(fe_oauth_state *state, + PGoauthBearerRequestV2 *request); +static void do_cleanup(fe_oauth_state *state, PGoauthBearerRequestV2 *request); +static void poison_req_v2(PGoauthBearerRequestV2 *request, bool poison); /* The exported OAuth callback mechanism. */ static void *oauth_init(PGconn *conn, const char *password, @@ -78,7 +85,7 @@ oauth_init(PGconn *conn, const char *password, * This handles only mechanism state tied to the connection lifetime; state * stored in state->async_ctx is freed up either immediately after the * authentication handshake succeeds, or before the mechanism is cleaned up on - * failure. See pg_fe_cleanup_oauth_flow() and cleanup_user_oauth_flow(). + * failure. See pg_fe_cleanup_oauth_flow() and cleanup_oauth_flow(). */ static void oauth_free(void *opaq) @@ -183,7 +190,14 @@ struct json_ctx #define oauth_json_has_error(ctx) \ (PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg) -#define oauth_json_set_error(ctx, ...) \ +#define oauth_json_set_error(ctx, fmt, ...) \ + do { \ + appendPQExpBuffer(&(ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__); \ + (ctx)->errmsg = (ctx)->errbuf.data; \ + } while (0) + +/* An untranslated version of oauth_json_set_error(). */ +#define oauth_json_set_error_internal(ctx, ...) \ do { \ appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \ (ctx)->errmsg = (ctx)->errbuf.data; \ @@ -199,13 +213,13 @@ oauth_json_object_start(void *state) Assert(ctx->nested == 1); oauth_json_set_error(ctx, - libpq_gettext("field \"%s\" must be a string"), + "field \"%s\" must be a string", ctx->target_field_name); } ++ctx->nested; if (ctx->nested > MAX_SASL_NESTING_LEVEL) - oauth_json_set_error(ctx, libpq_gettext("JSON is too deeply nested")); + oauth_json_set_error(ctx, "JSON is too deeply nested"); return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS; } @@ -254,20 +268,20 @@ oauth_json_array_start(void *state) if (!ctx->nested) { - ctx->errmsg = libpq_gettext("top-level element must be an object"); + oauth_json_set_error(ctx, "top-level element must be an object"); } else if (ctx->target_field) { Assert(ctx->nested == 1); oauth_json_set_error(ctx, - libpq_gettext("field \"%s\" must be a string"), + "field \"%s\" must be a string", ctx->target_field_name); } ++ctx->nested; if (ctx->nested > MAX_SASL_NESTING_LEVEL) - oauth_json_set_error(ctx, libpq_gettext("JSON is too deeply nested")); + oauth_json_set_error(ctx, "JSON is too deeply nested"); return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS; } @@ -288,7 +302,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type) if (!ctx->nested) { - ctx->errmsg = libpq_gettext("top-level element must be an object"); + oauth_json_set_error(ctx, "top-level element must be an object"); return JSON_SEM_ACTION_FAILED; } @@ -301,9 +315,9 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type) * Assert and don't continue any further for production builds. */ Assert(false); - oauth_json_set_error(ctx, - "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing", - ctx->nested); + oauth_json_set_error_internal(ctx, + "internal error: target scalar found at nesting level %d during OAUTHBEARER parsing", + ctx->nested); return JSON_SEM_ACTION_FAILED; } @@ -314,7 +328,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type) if (*ctx->target_field) { oauth_json_set_error(ctx, - libpq_gettext("field \"%s\" is duplicated"), + "field \"%s\" is duplicated", ctx->target_field_name); return JSON_SEM_ACTION_FAILED; } @@ -323,7 +337,7 @@ oauth_json_scalar(void *state, char *token, JsonTokenType type) if (type != JSON_TOKEN_STRING) { oauth_json_set_error(ctx, - libpq_gettext("field \"%s\" must be a string"), + "field \"%s\" must be a string", ctx->target_field_name); return JSON_SEM_ACTION_FAILED; } @@ -376,7 +390,7 @@ issuer_from_well_known_uri(PGconn *conn, const char *wkuri) authority_start = wkuri + strlen(HTTPS_SCHEME); if (!authority_start - && oauth_unsafe_debugging_enabled() + && (oauth_parse_debug_flags() & OAUTHDEBUG_UNSAFE_HTTP) && pg_strncasecmp(wkuri, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0) { /* Allow http:// for testing only. */ @@ -669,31 +683,76 @@ handle_oauth_sasl_error(PGconn *conn, const char *msg, int msglen) } /* - * Callback implementation of conn->async_auth() for a user-defined OAuth flow. - * Delegates the retrieval of the token to the application's async callback. + * Helper for handling flow failures. If anything was put into request->error, + * it's added to conn->errorMessage here. + */ +static void +report_flow_error(PGconn *conn, const PGoauthBearerRequestV2 *request) +{ + fe_oauth_state *state = conn->sasl_state; + const char *errmsg = request->error; + + /* + * User-defined flows are called out explicitly so that the user knows who + * to blame. Builtin flows don't need that extra message length; we expect + * them to always fill in request->error on failure anyway. + */ + if (state->builtin) + { + if (!errmsg) + { + /* + * Don't turn a bug here into a crash in production, but don't + * bother translating either. + */ + Assert(false); + errmsg = "builtin flow failed but did not provide an error message"; + } + + appendPQExpBufferStr(&conn->errorMessage, errmsg); + } + else + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("user-defined OAuth flow failed")); + if (errmsg) + { + appendPQExpBufferStr(&conn->errorMessage, ": "); + appendPQExpBufferStr(&conn->errorMessage, errmsg); + } + } + + appendPQExpBufferChar(&conn->errorMessage, '\n'); +} + +/* + * Callback implementation of conn->async_auth() for OAuth flows. Delegates the + * retrieval of the token to the PGoauthBearerRequestV2.async() callback. * - * This will be called multiple times as needed; the application is responsible - * for setting an altsock to signal and returning the correct PGRES_POLLING_* + * This will be called multiple times as needed; the callback is responsible for + * setting an altsock to signal and returning the correct PGRES_POLLING_* * statuses for use by PQconnectPoll(). */ static PostgresPollingStatusType -run_user_oauth_flow(PGconn *conn) +run_oauth_flow(PGconn *conn) { fe_oauth_state *state = conn->sasl_state; - PGoauthBearerRequest *request = state->async_ctx; + PGoauthBearerRequestV2 *request = state->async_ctx; PostgresPollingStatusType status; - if (!request->async) + if (!request->v1.async) { + Assert(!state->builtin); /* be very noisy if our code does this */ libpq_append_conn_error(conn, "user-defined OAuth flow provided neither a token nor an async callback"); return PGRES_POLLING_FAILED; } - status = request->async(conn, request, &conn->altsock); + status = do_async(state, request); + if (status == PGRES_POLLING_FAILED) { - libpq_append_conn_error(conn, "user-defined OAuth flow failed"); + report_flow_error(conn, request); return status; } else if (status == PGRES_POLLING_OK) @@ -703,14 +762,15 @@ run_user_oauth_flow(PGconn *conn) * onto the original string, since it may not be safe for us to free() * it.) */ - if (!request->token) + if (!request->v1.token) { + Assert(!state->builtin); libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a token"); return PGRES_POLLING_FAILED; } - conn->oauth_token = strdup(request->token); + conn->oauth_token = strdup(request->v1.token); if (!conn->oauth_token) { libpq_append_conn_error(conn, "out of memory"); @@ -723,6 +783,7 @@ run_user_oauth_flow(PGconn *conn) /* The hook wants the client to poll the altsock. Make sure it set one. */ if (conn->altsock == PGINVALID_SOCKET) { + Assert(!state->builtin); libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a socket for polling"); return PGRES_POLLING_FAILED; @@ -732,19 +793,23 @@ run_user_oauth_flow(PGconn *conn) } /* - * Cleanup callback for the async user flow. Delegates most of its job to the - * user-provided cleanup implementation, then disconnects the altsock. + * Cleanup callback for the async flow. Delegates most of its job to + * PGoauthBearerRequest.cleanup(), then disconnects the altsock and frees the + * request itself. + * + * This is called either at the end of a successful authentication, or during + * pqDropConnection(), so we won't leak resources even if PQconnectPoll() never + * calls us back. */ static void -cleanup_user_oauth_flow(PGconn *conn) +cleanup_oauth_flow(PGconn *conn) { fe_oauth_state *state = conn->sasl_state; - PGoauthBearerRequest *request = state->async_ctx; + PGoauthBearerRequestV2 *request = state->async_ctx; Assert(request); - if (request->cleanup) - request->cleanup(conn, request); + do_cleanup(state, request); conn->altsock = PGINVALID_SOCKET; free(request); @@ -756,12 +821,16 @@ cleanup_user_oauth_flow(PGconn *conn) * * There are three potential implementations of use_builtin_flow: * - * 1) If the OAuth client is disabled at configuration time, return false. + * 1) If the OAuth client is disabled at configuration time, return zero. * Dependent clients must provide their own flow. * 2) If the OAuth client is enabled and USE_DYNAMIC_OAUTH is defined, dlopen() * the libpq-oauth plugin and use its implementation. * 3) Otherwise, use flow callbacks that are statically linked into the * executable. + * + * For caller convenience, the return value follows the convention of + * PQauthDataHook: zero means no implementation is provided, negative indicates + * failure, and positive indicates success. */ #if !defined(USE_LIBCURL) @@ -770,10 +839,10 @@ cleanup_user_oauth_flow(PGconn *conn) * This configuration doesn't support the builtin flow. */ -bool -use_builtin_flow(PGconn *conn, fe_oauth_state *state) +static int +use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request) { - return false; + return 0; } #elif defined(USE_DYNAMIC_OAUTH) @@ -784,36 +853,6 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state) typedef char *(*libpq_gettext_func) (const char *msgid); -/* - * Define accessor/mutator shims to inject into libpq-oauth, so that it doesn't - * depend on the offsets within PGconn. (These have changed during minor version - * updates in the past.) - */ - -#define DEFINE_GETTER(TYPE, MEMBER) \ - typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \ - static TYPE conn_ ## MEMBER(PGconn *conn) { return conn->MEMBER; } - -/* Like DEFINE_GETTER, but returns a pointer to the member. */ -#define DEFINE_GETTER_P(TYPE, MEMBER) \ - typedef TYPE (*conn_ ## MEMBER ## _func) (PGconn *conn); \ - static TYPE conn_ ## MEMBER(PGconn *conn) { return &conn->MEMBER; } - -#define DEFINE_SETTER(TYPE, MEMBER) \ - typedef void (*set_conn_ ## MEMBER ## _func) (PGconn *conn, TYPE val); \ - static void set_conn_ ## MEMBER(PGconn *conn, TYPE val) { conn->MEMBER = val; } - -DEFINE_GETTER_P(PQExpBuffer, errorMessage); -DEFINE_GETTER(char *, oauth_client_id); -DEFINE_GETTER(char *, oauth_client_secret); -DEFINE_GETTER(char *, oauth_discovery_uri); -DEFINE_GETTER(char *, oauth_issuer_id); -DEFINE_GETTER(char *, oauth_scope); -DEFINE_GETTER(fe_oauth_state *, sasl_state); - -DEFINE_SETTER(pgsocket, altsock); -DEFINE_SETTER(char *, oauth_token); - /* * Loads the libpq-oauth plugin via dlopen(), initializes it, and plugs its * callbacks into the connection's async auth handlers. @@ -822,27 +861,19 @@ DEFINE_SETTER(char *, oauth_token); * handle the use case where the build supports loading a flow but a user does * not want to install it. Troubleshooting of linker/loader failures can be done * via PGOAUTHDEBUG. + * + * The lifetime of *request ends shortly after this call, so it must be copied + * to longer-lived storage. */ -bool -use_builtin_flow(PGconn *conn, fe_oauth_state *state) +static int +use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request) { static bool initialized = false; static pthread_mutex_t init_mutex = PTHREAD_MUTEX_INITIALIZER; int lockerr; - void (*init) (pgthreadlock_t threadlock, - libpq_gettext_func gettext_impl, - conn_errorMessage_func errmsg_impl, - conn_oauth_client_id_func clientid_impl, - conn_oauth_client_secret_func clientsecret_impl, - conn_oauth_discovery_uri_func discoveryuri_impl, - conn_oauth_issuer_id_func issuerid_impl, - conn_oauth_scope_func scope_impl, - conn_sasl_state_func saslstate_impl, - set_conn_altsock_func setaltsock_impl, - set_conn_oauth_token_func settoken_impl); - PostgresPollingStatusType (*flow) (PGconn *conn); - void (*cleanup) (PGconn *conn); + void (*init) (libpq_gettext_func gettext_impl); + int (*start_flow) (PGconn *conn, PGoauthBearerRequestV2 *request); /* * On macOS only, load the module using its absolute install path; the @@ -855,13 +886,13 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state) */ const char *const module_name = #if defined(__darwin__) - LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX; + LIBDIR "/libpq-oauth" DLSUFFIX; #else - "libpq-oauth-" PG_MAJORVERSION DLSUFFIX; + "libpq-oauth" DLSUFFIX; #endif - state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL); - if (!state->builtin_flow) + state->flow_module = dlopen(module_name, RTLD_NOW | RTLD_LOCAL); + if (!state->flow_module) { /* * For end users, this probably isn't an error condition, it just @@ -870,25 +901,36 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state) * * Note that POSIX dlerror() isn't guaranteed to be threadsafe. */ - if (oauth_unsafe_debugging_enabled()) + if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS) fprintf(stderr, "failed dlopen for libpq-oauth: %s\n", dlerror()); - return false; + return 0; } - if ((init = dlsym(state->builtin_flow, "libpq_oauth_init")) == NULL - || (flow = dlsym(state->builtin_flow, "pg_fe_run_oauth_flow")) == NULL - || (cleanup = dlsym(state->builtin_flow, "pg_fe_cleanup_oauth_flow")) == NULL) + /* + * Our libpq-oauth.so provides a special initialization function for libpq + * integration. If we don't find this, assume that a custom module is in + * use instead. + */ + init = dlsym(state->flow_module, "libpq_oauth_init"); + if (!init) + state->builtin = false; /* adjust our error messages */ + + if ((start_flow = dlsym(state->flow_module, "pg_start_oauthbearer")) == NULL) { /* - * This is more of an error condition than the one above, but due to - * the dlerror() threadsafety issue, lock it behind PGOAUTHDEBUG too. + * This is more of an error condition than the one above, but the + * cause is still locked behind PGOAUTHDEBUG due to the dlerror() + * threadsafety issue. */ - if (oauth_unsafe_debugging_enabled()) + if (oauth_parse_debug_flags() & OAUTHDEBUG_PLUGIN_ERRORS) fprintf(stderr, "failed dlsym for libpq-oauth: %s\n", dlerror()); - dlclose(state->builtin_flow); - return false; + dlclose(state->flow_module); + state->flow_module = NULL; + + request->error = libpq_gettext("could not find entry point for libpq-oauth"); + return -1; } /* @@ -896,68 +938,59 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state) * permanently. */ - /* - * We need to inject necessary function pointers into the module. This - * only needs to be done once -- even if the pointers are constant, - * assigning them while another thread is executing the flows feels like - * tempting fate. - */ - if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0) + if (init) { - /* Should not happen... but don't continue if it does. */ - Assert(false); + /* + * We need to inject necessary function pointers into the module. This + * only needs to be done once -- even if the pointers are constant, + * assigning them while another thread is executing the flows feels + * like tempting fate. + */ + if ((lockerr = pthread_mutex_lock(&init_mutex)) != 0) + { + /* Should not happen... but don't continue if it does. */ + Assert(false); - libpq_append_conn_error(conn, "failed to lock mutex (%d)", lockerr); - return false; - } + appendPQExpBuffer(&conn->errorMessage, + "use_builtin_flow: failed to lock mutex (%d)\n", + lockerr); - if (!initialized) - { - init(pg_g_threadlock, + request->error = ""; /* satisfy report_flow_error() */ + return -1; + } + + if (!initialized) + { + init( #ifdef ENABLE_NLS - libpq_gettext, + libpq_gettext #else - NULL, + NULL #endif - conn_errorMessage, - conn_oauth_client_id, - conn_oauth_client_secret, - conn_oauth_discovery_uri, - conn_oauth_issuer_id, - conn_oauth_scope, - conn_sasl_state, - set_conn_altsock, - set_conn_oauth_token); - - initialized = true; - } + ); - pthread_mutex_unlock(&init_mutex); + initialized = true; + } - /* Set our asynchronous callbacks. */ - conn->async_auth = flow; - conn->cleanup_async_auth = cleanup; + pthread_mutex_unlock(&init_mutex); + } - return true; + return (start_flow(conn, request) == 0) ? 1 : -1; } #else /* - * Use the builtin flow in libpq-oauth.a (see libpq-oauth/oauth-curl.h). + * For static builds, we can just call pg_start_oauthbearer() directly. It's + * provided by libpq-oauth.a. */ -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); +extern int pg_start_oauthbearer(PGconn *conn, PGoauthBearerRequestV2 *request); -bool -use_builtin_flow(PGconn *conn, fe_oauth_state *state) +static int +use_builtin_flow(PGconn *conn, fe_oauth_state *state, PGoauthBearerRequestV2 *request) { - /* Set our asynchronous callbacks. */ - conn->async_auth = pg_fe_run_oauth_flow; - conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow; - - return true; + return (pg_start_oauthbearer(conn, request) == 0) ? 1 : -1; } #endif /* USE_LIBCURL */ @@ -968,13 +1001,13 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state) * token for presentation to the server. * * If the application has registered a custom flow handler using - * PQAUTHDATA_OAUTH_BEARER_TOKEN, it may either return a token immediately (e.g. - * if it has one cached for immediate use), or set up for a series of - * asynchronous callbacks which will be managed by run_user_oauth_flow(). + * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2], it may either return a token immediately + * (e.g. if it has one cached for immediate use), or set up for a series of + * asynchronous callbacks which will be managed by run_oauth_flow(). * * If the default handler is used instead, a Device Authorization flow is used - * for the connection if support has been compiled in. (See - * fe-auth-oauth-curl.c for implementation details.) + * for the connection if support has been compiled in. (See oauth-curl.c for + * implementation details.) * * If neither a custom handler nor the builtin flow is available, the connection * fails here. @@ -983,27 +1016,50 @@ static bool setup_token_request(PGconn *conn, fe_oauth_state *state) { int res; - PGoauthBearerRequest request = { - .openid_configuration = conn->oauth_discovery_uri, - .scope = conn->oauth_scope, + PGoauthBearerRequestV2 request = { + .v1 = { + .openid_configuration = conn->oauth_discovery_uri, + .scope = conn->oauth_scope, + }, + .issuer = conn->oauth_issuer_id, }; - Assert(request.openid_configuration); + Assert(request.v1.openid_configuration); + Assert(request.issuer); + + /* + * The client may have overridden the OAuth flow. Try the v2 hook first, + * then fall back to the v1 implementation. If neither is available, try + * the builtin flow. + */ + res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, conn, &request); + if (res == 0) + { + poison_req_v2(&request, true); + + res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request); + state->v1 = (res != 0); + + poison_req_v2(&request, false); + } + if (res == 0) + { + state->builtin = true; + res = use_builtin_flow(conn, state, &request); + } - /* The client may have overridden the OAuth flow. */ - res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request); if (res > 0) { - PGoauthBearerRequest *request_copy; + PGoauthBearerRequestV2 *request_copy; - if (request.token) + if (request.v1.token) { /* * We already have a token, so copy it into the conn. (We can't * hold onto the original string, since it may not be safe for us * to free() it.) */ - conn->oauth_token = strdup(request.token); + conn->oauth_token = strdup(request.v1.token); if (!conn->oauth_token) { libpq_append_conn_error(conn, "out of memory"); @@ -1011,8 +1067,7 @@ setup_token_request(PGconn *conn, fe_oauth_state *state) } /* short-circuit */ - if (request.cleanup) - request.cleanup(conn, &request); + do_cleanup(state, &request); return true; } @@ -1025,26 +1080,24 @@ setup_token_request(PGconn *conn, fe_oauth_state *state) *request_copy = request; - conn->async_auth = run_user_oauth_flow; - conn->cleanup_async_auth = cleanup_user_oauth_flow; + conn->async_auth = run_oauth_flow; + conn->cleanup_async_auth = cleanup_oauth_flow; state->async_ctx = request_copy; - } - else if (res < 0) - { - libpq_append_conn_error(conn, "user-defined OAuth flow failed"); - goto fail; - } - else if (!use_builtin_flow(conn, state)) - { - libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); - goto fail; + + return true; } - return true; + /* + * Failure cases: either we tried to set up a flow and failed, or there + * was no flow to try. + */ + if (res < 0) + report_flow_error(conn, &request); + else + libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); fail: - if (request.cleanup) - request.cleanup(conn, &request); + do_cleanup(state, &request); return false; } @@ -1386,12 +1439,117 @@ pqClearOAuthToken(PGconn *conn) } /* - * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + * Hook v1 Poisoning + * + * Try to catch misuses of the v1 PQAUTHDATA_OAUTH_BEARER_TOKEN hook and its + * callbacks, which are not allowed to downcast their request argument to + * PGoauthBearerRequestV2. (Such clients may crash or worse when speaking to + * libpq 18.) + * + * This attempts to use Valgrind hooks, if present, to mark the extra members as + * inaccessible. For uninstrumented builds, it also munges request->issuer to + * try to crash clients that perform string operations, and it aborts if + * request->error is set. + */ + +#define MASK_BITS ((uintptr_t) 0x55aa55aa55aa55aa) +#define POISON_MASK(ptr) ((void *) (((uintptr_t) ptr) ^ MASK_BITS)) + +/* + * Workhorse for v2 request poisoning. This must be called exactly twice: once + * to poison, once to unpoison. + * + * NB: Unpoisoning must restore the request to its original state, because we + * might still switch back to a v2 implementation internally. Don't do anything + * destructive during the poison operation. + */ +static void +poison_req_v2(PGoauthBearerRequestV2 *request, bool poison) +{ +#ifdef USE_VALGRIND + void *const base = (char *) request + sizeof(request->v1); + const size_t len = sizeof(*request) - sizeof(request->v1); +#endif + + if (poison) + { + /* Poison request->issuer with a mask to help uninstrumented builds. */ + request->issuer = POISON_MASK(request->issuer); + + /* + * We'll check to make sure request->error wasn't assigned when + * unpoisoning, so it had better not be assigned now. + */ + Assert(!request->error); + + VALGRIND_MAKE_MEM_NOACCESS(base, len); + } + else + { + /* + * XXX Using DEFINED here is technically too lax; we might catch + * struct padding in the blast radius. But since this API has to + * poison stack addresses, and Valgrind can't track/manage undefined + * stack regions, we can't be any stricter without tracking the + * original state of the memory. + */ + VALGRIND_MAKE_MEM_DEFINED(base, len); + + /* Undo our mask. */ + request->issuer = POISON_MASK(request->issuer); + + /* + * For uninstrumented builds, make sure request->error wasn't touched. + */ + if (request->error) + { + fprintf(stderr, + "abort! out-of-bounds write to PGoauthBearerRequest by PQAUTHDATA_OAUTH_BEARER_TOKEN hook\n"); + abort(); + } + } +} + +/* + * Wrapper around PGoauthBearerRequest.async() which applies poison during the + * callback when necessary. */ -bool -oauth_unsafe_debugging_enabled(void) +static PostgresPollingStatusType +do_async(fe_oauth_state *state, PGoauthBearerRequestV2 *request) { - const char *env = getenv("PGOAUTHDEBUG"); + PostgresPollingStatusType ret; + PGconn *conn = state->conn; + + Assert(request->v1.async); + + if (state->v1) + poison_req_v2(request, true); + + ret = request->v1.async(conn, + (PGoauthBearerRequest *) request, + &conn->altsock); + + if (state->v1) + poison_req_v2(request, false); + + return ret; +} + +/* + * Similar wrapper for the optional PGoauthBearerRequest.cleanup() callback. + * Does nothing if one is not defined. + */ +static void +do_cleanup(fe_oauth_state *state, PGoauthBearerRequestV2 *request) +{ + if (!request->v1.cleanup) + return; + + if (state->v1) + poison_req_v2(request, true); + + request->v1.cleanup(state->conn, (PGoauthBearerRequest *) request); - return (env && strcmp(env, "UNSAFE") == 0); + if (state->v1) + poison_req_v2(request, false); } diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 0d59e91605bbe..a50d7b0340836 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -4,7 +4,7 @@ * * Definitions for OAuth authentication implementations * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/fe-auth-oauth.h @@ -27,11 +27,6 @@ enum fe_oauth_step FE_OAUTH_SERVER_ERROR, }; -/* - * This struct is exported to the libpq-oauth module. If changes are needed - * during backports to stable branches, please keep ABI compatibility (no - * changes to existing members, add new members at the end, etc.). - */ typedef struct { enum fe_oauth_step step; @@ -39,12 +34,12 @@ typedef struct PGconn *conn; void *async_ctx; - void *builtin_flow; + bool v1; + bool builtin; + void *flow_module; } fe_oauth_state; extern void pqClearOAuthToken(PGconn *conn); -extern bool oauth_unsafe_debugging_enabled(void); -extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state); /* Mechanisms in fe-auth-oauth.c */ extern const pg_fe_sasl_mech pg_oauth_mech; diff --git a/src/interfaces/libpq/fe-auth-sasl.h b/src/interfaces/libpq/fe-auth-sasl.h index f06f547c07dab..b620d44b09d0f 100644 --- a/src/interfaces/libpq/fe-auth-sasl.h +++ b/src/interfaces/libpq/fe-auth-sasl.h @@ -8,7 +8,7 @@ * * See src/include/libpq/sasl.h for the backend counterpart. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/fe-auth-sasl.h diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index f6d6a5aa977b8..99103b7e2b667 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -3,7 +3,7 @@ * fe-auth-scram.c * The front-end (client) implementation of SCRAM authentication. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -819,7 +819,7 @@ calculate_client_proof(fe_scram_state *state, strlen(state->server_first_message)) < 0 || pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || pg_hmac_update(ctx, - (uint8 *) client_final_message_without_proof, + (const uint8 *) client_final_message_without_proof, strlen(client_final_message_without_proof)) < 0 || pg_hmac_final(ctx, ClientSignature, state->key_length) < 0) { diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 84a042269dec9..f05aaea96510a 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -3,7 +3,7 @@ * fe-auth.c * The front-end (client) authorization routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -1369,7 +1369,7 @@ PQencryptPassword(const char *passwd, const char *user) if (!crypt_pwd) return NULL; - if (!pg_md5_encrypt(passwd, (uint8 *) user, strlen(user), crypt_pwd, &errstr)) + if (!pg_md5_encrypt(passwd, (const uint8 *) user, strlen(user), crypt_pwd, &errstr)) { free(crypt_pwd); return NULL; @@ -1482,7 +1482,7 @@ PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, { const char *errstr = NULL; - if (!pg_md5_encrypt(passwd, (uint8 *) user, strlen(user), crypt_pwd, &errstr)) + if (!pg_md5_encrypt(passwd, (const uint8 *) user, strlen(user), crypt_pwd, &errstr)) { libpq_append_conn_error(conn, "could not encrypt password: %s", errstr); free(crypt_pwd); diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index de98e0d20c4b4..b27c4ede3400b 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -4,7 +4,7 @@ * * Definitions for network authentication routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/fe-auth.h diff --git a/src/interfaces/libpq/fe-cancel.c b/src/interfaces/libpq/fe-cancel.c index 8c7c198a53071..4b5945979c44f 100644 --- a/src/interfaces/libpq/fe-cancel.c +++ b/src/interfaces/libpq/fe-cancel.c @@ -3,7 +3,7 @@ * fe-cancel.c * functions related to query cancellation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -114,7 +114,7 @@ PQcancelCreate(PGconn *conn) if (conn->be_cancel_key != NULL) { cancelConn->be_cancel_key = malloc(conn->be_cancel_key_len); - if (!conn->be_cancel_key) + if (cancelConn->be_cancel_key == NULL) goto oom_error; memcpy(cancelConn->be_cancel_key, conn->be_cancel_key, conn->be_cancel_key_len); } @@ -137,6 +137,7 @@ PQcancelCreate(PGconn *conn) goto oom_error; originalHost = conn->connhost[conn->whichhost]; + cancelConn->connhost[0].type = originalHost.type; if (originalHost.host) { cancelConn->connhost[0].host = strdup(originalHost.host); @@ -378,7 +379,24 @@ PQgetCancel(PGconn *conn) /* Check that we have received a cancellation key */ if (conn->be_cancel_key_len == 0) - return NULL; + { + /* + * In case there is no cancel key, return an all-zero PGcancel object. + * Actually calling PQcancel on this will fail, but we allow creating + * the PGcancel object anyway. Arguably it would be better return NULL + * to indicate that cancellation is not possible, but there'd be no + * way for the caller to distinguish "out of memory" from "server did + * not send a cancel key". Also, this is how PGgetCancel() has always + * behaved, and if we changed it, some clients would stop working + * altogether with servers that don't support cancellation. (The + * modern PQcancelCreate() function returns a failed connection object + * instead.) + * + * The returned dummy object has cancel_pkt_len == 0; we check for + * that in PQcancel() to identify it as a dummy. + */ + return calloc(1, sizeof(PGcancel)); + } cancel_req_len = offsetof(CancelRequestPacket, cancelAuthCode) + conn->be_cancel_key_len; cancel = malloc(offsetof(PGcancel, cancel_req) + cancel_req_len); @@ -430,7 +448,7 @@ PQgetCancel(PGconn *conn) } req = (CancelRequestPacket *) &cancel->cancel_req; - req->cancelRequestCode = (MsgType) pg_hton32(CANCEL_REQUEST_CODE); + req->cancelRequestCode = pg_hton32(CANCEL_REQUEST_CODE); req->backendPID = pg_hton32(conn->be_pid); memcpy(req->cancelAuthCode, conn->be_cancel_key, conn->be_cancel_key_len); /* include the length field itself in the length */ @@ -461,7 +479,7 @@ PQsendCancelRequest(PGconn *cancelConn) /* Send the message body. */ memset(&req, 0, offsetof(CancelRequestPacket, cancelAuthCode)); - req.cancelRequestCode = (MsgType) pg_hton32(CANCEL_REQUEST_CODE); + req.cancelRequestCode = pg_hton32(CANCEL_REQUEST_CODE); req.backendPID = pg_hton32(cancelConn->be_pid); if (pqPutnchar(&req, offsetof(CancelRequestPacket, cancelAuthCode), cancelConn)) return STATUS_ERROR; @@ -543,6 +561,15 @@ PQcancel(PGcancel *cancel, char *errbuf, int errbufsize) return false; } + if (cancel->cancel_pkt_len == 0) + { + /* This is a dummy PGcancel object, see PQgetCancel */ + strlcpy(errbuf, "PQcancel() -- no cancellation key received", errbufsize); + /* strlcpy probably doesn't change errno, but be paranoid */ + SOCK_ERRNO_SET(save_errno); + return false; + } + /* * We need to open a temporary connection to the postmaster. Do this with * only kernel calls. diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index ccb01aad36109..4272d386e64a6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -3,7 +3,7 @@ * fe-connect.c * functions related to setting up a connection to the backend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -90,8 +91,9 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options, /* This is part of the protocol so just define it */ #define ERRCODE_INVALID_PASSWORD "28P01" -/* This too */ +/* These too */ #define ERRCODE_CANNOT_CONNECT_NOW "57P03" +#define ERRCODE_PROTOCOL_VIOLATION "08P01" /* * Cope with the various platform-specific ways to spell TCP keepalive socket @@ -201,6 +203,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Database-Service", "", 20, offsetof(struct pg_conn, pgservice)}, + {"servicefile", "PGSERVICEFILE", NULL, NULL, + "Database-Service-File", "", 64, + offsetof(struct pg_conn, pgservicefile)}, + {"user", "PGUSER", NULL, NULL, "Database-User", "", 20, offsetof(struct pg_conn, pguser)}, @@ -407,6 +413,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "OAuth-Scope", "", 15, offsetof(struct pg_conn, oauth_scope)}, + {"oauth_ca_file", "PGOAUTHCAFILE", NULL, NULL, + "OAuth-CA-File", "", 64, + offsetof(struct pg_conn, oauth_ca_file)}, + {"sslkeylogfile", NULL, NULL, NULL, "SSL-Key-Log-File", "D", 64, offsetof(struct pg_conn, sslkeylogfile)}, @@ -497,8 +507,9 @@ static int parseServiceFile(const char *serviceFile, PQExpBuffer errorMessage, bool *group_found); static char *pwdfMatchesString(char *buf, const char *token); -static char *passwordFromFile(const char *hostname, const char *port, const char *dbname, - const char *username, const char *pgpassfile); +static char *passwordFromFile(const char *hostname, const char *port, + const char *dbname, const char *username, + const char *pgpassfile, const char **errmsg); static void pgpassfileWarning(PGconn *conn); static void default_threadlock(int acquire); static bool sslVerifyProtocolVersion(const char *version); @@ -1135,7 +1146,7 @@ parse_comma_separated_list(char **startptr, bool *more) char *p; char *s = *startptr; char *e; - int len; + size_t len; /* * Search for the end of the current element; a comma or end-of-string @@ -1450,6 +1461,7 @@ pqConnectOptions2(PGconn *conn) * least one of them is guaranteed nonempty by now). */ const char *pwhost = conn->connhost[i].host; + const char *password_errmsg = NULL; if (pwhost == NULL || pwhost[0] == '\0') pwhost = conn->connhost[i].hostaddr; @@ -1459,7 +1471,15 @@ pqConnectOptions2(PGconn *conn) conn->connhost[i].port, conn->dbName, conn->pguser, - conn->pgpassfile); + conn->pgpassfile, + &password_errmsg); + + if (password_errmsg != NULL) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "%s", password_errmsg); + return false; + } } } } @@ -2127,21 +2147,19 @@ pqConnectOptions2(PGconn *conn) else { /* - * To not break connecting to older servers/poolers that do not yet - * support NegotiateProtocolVersion, default to the 3.0 protocol at - * least for a while longer. Except when min_protocol_version is set - * to something larger, then we might as well default to the latest. + * Default to PG_PROTOCOL_GREASE, which is larger than all real + * versions, to test negotiation. The server should automatically + * downgrade to a supported version. + * + * This behavior is for 19beta only. It will be reverted before RC1. */ - if (conn->min_pversion > PG_PROTOCOL(3, 0)) - conn->max_pversion = PG_PROTOCOL_LATEST; - else - conn->max_pversion = PG_PROTOCOL(3, 0); + conn->max_pversion = PG_PROTOCOL_GREASE; } if (conn->min_pversion > conn->max_pversion) { conn->status = CONNECTION_BAD; - libpq_append_conn_error(conn, "min_protocol_version is greater than max_protocol_version"); + libpq_append_conn_error(conn, "\"%s\" is greater than \"%s\"", "min_protocol_version", "max_protocol_version"); return false; } @@ -3079,9 +3097,9 @@ PQconnectPoll(PGconn *conn) UNIXSOCK_PATH(portstr, thisport, ch->host); if (strlen(portstr) >= UNIXSOCK_PATH_BUFLEN) { - libpq_append_conn_error(conn, "Unix-domain socket path \"%s\" is too long (maximum %d bytes)", + libpq_append_conn_error(conn, "Unix-domain socket path \"%s\" is too long (maximum %zu bytes)", portstr, - (int) (UNIXSOCK_PATH_BUFLEN - 1)); + (UNIXSOCK_PATH_BUFLEN - 1)); goto keep_going; } @@ -4141,6 +4159,32 @@ PQconnectPoll(PGconn *conn) /* Check to see if we should mention pgpassfile */ pgpassfileWarning(conn); + /* + * ...and whether we should mention grease. If the error + * message contains the PG_PROTOCOL_GREASE number (in + * major.minor, decimal, or hex format) or a complaint + * about a protocol violation before we've even started an + * authentication exchange, it's probably caused by a + * grease interaction. + */ + if (conn->max_pversion == PG_PROTOCOL_GREASE && + !conn->auth_req_received) + { + const char *sqlstate = PQresultErrorField(conn->result, + PG_DIAG_SQLSTATE); + + if ((sqlstate && + strcmp(sqlstate, ERRCODE_PROTOCOL_VIOLATION) == 0) || + (conn->errorMessage.len > 0 && + (strstr(conn->errorMessage.data, "3.9999") || + strstr(conn->errorMessage.data, "206607") || + strstr(conn->errorMessage.data, "3270F") || + strstr(conn->errorMessage.data, "3270f")))) + { + libpq_append_grease_info(conn); + } + } + CONNECTION_FAILED(); } /* Handle NegotiateProtocolVersion */ @@ -4371,6 +4415,14 @@ PQconnectPoll(PGconn *conn) goto error_return; } + if (conn->max_pversion == PG_PROTOCOL_GREASE && + conn->pversion == PG_PROTOCOL_GREASE) + { + libpq_append_conn_error(conn, "server incorrectly accepted \"grease\" protocol version 3.9999 without negotiation"); + libpq_append_grease_info(conn); + goto error_return; + } + /* Almost there now ... */ conn->status = CONNECTION_CHECK_TARGET; goto keep_going; @@ -5062,6 +5114,7 @@ freePGconn(PGconn *conn) free(conn->dbName); free(conn->replication); free(conn->pgservice); + free(conn->pgservicefile); free(conn->pguser); if (conn->pgpass) { @@ -5109,6 +5162,7 @@ freePGconn(PGconn *conn) free(conn->oauth_discovery_uri); free(conn->oauth_client_id); free(conn->oauth_client_secret); + free(conn->oauth_ca_file); free(conn->oauth_scope); /* Note that conn->Pfdebug is not ours to close or free */ free(conn->events); @@ -5489,6 +5543,7 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options, *entry; struct berval **values; LDAP_TIMEVAL time = {PGLDAP_TIMEOUT, 0}; + int ldapversion = LDAP_VERSION3; if ((url = strdup(purl)) == NULL) { @@ -5620,6 +5675,15 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options, return 3; } + if ((rc = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldapversion)) != LDAP_SUCCESS) + { + libpq_append_error(errorMessage, "could not set LDAP protocol version: %s", + ldap_err2string(rc)); + free(url); + ldap_unbind(ld); + return 3; + } + /* * Perform an explicit anonymous bind. * @@ -5744,7 +5808,21 @@ ldapServiceLookup(const char *purl, PQconninfoOption *options, /* concatenate values into a single string with newline terminators */ size = 1; /* for the trailing null */ for (i = 0; values[i] != NULL; i++) + { + if (values[i]->bv_len >= INT_MAX || + size > (INT_MAX - (values[i]->bv_len + 1))) + { + libpq_append_error(errorMessage, + "connection info string size exceeds the maximum allowed (%d)", + INT_MAX); + ldap_value_free_len(values); + ldap_unbind(ld); + return 3; + } + size += values[i]->bv_len + 1; + } + if ((result = malloc(size)) == NULL) { libpq_append_error(errorMessage, "out of memory"); @@ -5914,6 +5992,7 @@ static int parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage) { const char *service = conninfo_getval(options, "service"); + const char *service_fname = conninfo_getval(options, "servicefile"); char serviceFile[MAXPGPATH]; char *env; bool group_found = false; @@ -5933,10 +6012,13 @@ parseServiceInfo(PQconninfoOption *options, PQExpBuffer errorMessage) return 0; /* - * Try PGSERVICEFILE if specified, else try ~/.pg_service.conf (if that - * exists). + * First, try the "servicefile" option in connection string. Then, try + * the PGSERVICEFILE environment variable. Finally, check + * ~/.pg_service.conf (if that exists). */ - if ((env = getenv("PGSERVICEFILE")) != NULL) + if (service_fname != NULL) + strlcpy(serviceFile, service_fname, sizeof(serviceFile)); + else if ((env = getenv("PGSERVICEFILE")) != NULL) strlcpy(serviceFile, env, sizeof(serviceFile)); else { @@ -6092,7 +6174,17 @@ parseServiceFile(const char *serviceFile, if (strcmp(key, "service") == 0) { libpq_append_error(errorMessage, - "nested service specifications not supported in service file \"%s\", line %d", + "nested \"service\" specifications not supported in service file \"%s\", line %d", + serviceFile, + linenr); + result = 3; + goto exit; + } + + if (strcmp(key, "servicefile") == 0) + { + libpq_append_error(errorMessage, + "nested \"servicefile\" specifications not supported in service file \"%s\", line %d", serviceFile, linenr); result = 3; @@ -6135,6 +6227,33 @@ parseServiceFile(const char *serviceFile, } exit: + + /* + * If a service has been successfully found, set the "servicefile" option + * if not already set. This matters when we use a default service file or + * PGSERVICEFILE, where we want to be able track the value. + */ + if (*group_found && result == 0) + { + for (i = 0; options[i].keyword; i++) + { + if (strcmp(options[i].keyword, "servicefile") != 0) + continue; + + /* If value is already set, nothing to do */ + if (options[i].val != NULL) + break; + + options[i].val = strdup(serviceFile); + if (options[i].val == NULL) + { + libpq_append_error(errorMessage, "out of memory"); + result = 3; + } + break; + } + } + fclose(f); return result; @@ -7461,14 +7580,6 @@ PQdb(const PGconn *conn) return conn->dbName; } -char * -PQservice(const PGconn *conn) -{ - if (!conn) - return NULL; - return conn->pgservice; -} - char * PQuser(const PGconn *conn) { @@ -7536,10 +7647,12 @@ PQport(const PGconn *conn) if (!conn) return NULL; - if (conn->connhost != NULL) + if (conn->connhost != NULL && + conn->connhost[conn->whichhost].port != NULL && + conn->connhost[conn->whichhost].port[0] != '\0') return conn->connhost[conn->whichhost].port; - return ""; + return DEF_PGPORT_STR; } /* @@ -7892,10 +8005,16 @@ pwdfMatchesString(char *buf, const char *token) return NULL; } -/* Get a password from the password file. Return value is malloc'd. */ +/* + * Get a password from the password file. Return value is malloc'd. + * + * On failure, *errmsg is set to an error to be returned. It is + * left NULL on success, or if no password could be found. + */ static char * -passwordFromFile(const char *hostname, const char *port, const char *dbname, - const char *username, const char *pgpassfile) +passwordFromFile(const char *hostname, const char *port, + const char *dbname, const char *username, + const char *pgpassfile, const char **errmsg) { FILE *fp; #ifndef WIN32 @@ -7903,6 +8022,8 @@ passwordFromFile(const char *hostname, const char *port, const char *dbname, #endif PQExpBufferData buf; + *errmsg = NULL; + if (dbname == NULL || dbname[0] == '\0') return NULL; @@ -7969,7 +8090,10 @@ passwordFromFile(const char *hostname, const char *port, const char *dbname, { /* Make sure there's a reasonable amount of room in the buffer */ if (!enlargePQExpBuffer(&buf, 128)) + { + *errmsg = libpq_gettext("out of memory"); break; + } /* Read some data, appending it to what we already have */ if (fgets(buf.data + buf.len, buf.maxlen - buf.len, fp) == NULL) @@ -8008,7 +8132,7 @@ passwordFromFile(const char *hostname, const char *port, const char *dbname, if (!ret) { - /* Out of memory. XXX: an error message would be nice. */ + *errmsg = libpq_gettext("out of memory"); return NULL; } @@ -8295,3 +8419,10 @@ PQregisterThreadLock(pgthreadlock_t newhandler) return prev; } + +pgthreadlock_t +PQgetThreadLock(void) +{ + Assert(pg_g_threadlock); + return pg_g_threadlock; +} diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 4256ae5c0cc5f..203d388bdbf2a 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -3,7 +3,7 @@ * fe-exec.c * functions related to sending a query down to the backend * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -24,6 +24,7 @@ #include #endif +#include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" #include "mb/pg_wchar.h" @@ -511,7 +512,7 @@ PQsetvalue(PGresult *res, int tup_num, int field_num, char *value, int len) } else { - attval->value = (char *) pqResultAlloc(res, len + 1, true); + attval->value = (char *) pqResultAlloc(res, (size_t) len + 1, true); if (!attval->value) goto fail; attval->len = len; @@ -603,8 +604,13 @@ pqResultAlloc(PGresult *res, size_t nBytes, bool isBinary) */ if (nBytes >= PGRESULT_SEP_ALLOC_THRESHOLD) { - size_t alloc_size = nBytes + PGRESULT_BLOCK_OVERHEAD; + size_t alloc_size; + /* Don't wrap around with overly large requests. */ + if (nBytes > SIZE_MAX - PGRESULT_BLOCK_OVERHEAD) + return NULL; + + alloc_size = nBytes + PGRESULT_BLOCK_OVERHEAD; block = (PGresult_data *) malloc(alloc_size); if (!block) return NULL; @@ -1076,8 +1082,12 @@ pqSaveMessageField(PGresult *res, char code, const char *value) /* * pqSaveParameterStatus - remember parameter status sent by backend + * + * Returns 1 on success, 0 on out-of-memory. (Note that on out-of-memory, we + * have already released the old value of the parameter, if any. The only + * really safe way to recover is to terminate the connection.) */ -void +int pqSaveParameterStatus(PGconn *conn, const char *name, const char *value) { pgParameterStatus *pstatus; @@ -1119,6 +1129,11 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value) pstatus->next = conn->pstatus; conn->pstatus = pstatus; } + else + { + /* out of memory */ + return 0; + } /* * Save values of settings that are of interest to libpq in fields of the @@ -1190,6 +1205,8 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value) { conn->scram_sha_256_iterations = atoi(value); } + + return 1; } @@ -1263,7 +1280,7 @@ pqRowProcessor(PGconn *conn, const char **errmsgp) bool isbinary = (res->attDescs[i].format != 0); char *val; - val = (char *) pqResultAlloc(res, clen + 1, isbinary); + val = (char *) pqResultAlloc(res, (size_t) clen + 1, isbinary); if (val == NULL) return 0; @@ -4216,8 +4233,8 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) const char *s; char *result; char *rp; - int num_quotes = 0; /* single or double, depending on as_ident */ - int num_backslashes = 0; + size_t num_quotes = 0; /* single or double, depending on as_ident */ + size_t num_backslashes = 0; size_t input_len = strnlen(str, len); size_t result_size; char quote_char = as_ident ? '"' : '\''; @@ -4283,10 +4300,21 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) } } - /* Allocate output buffer. */ - result_size = input_len + num_quotes + 3; /* two quotes, plus a NUL */ + /* + * Allocate output buffer. Protect against overflow, in case the caller + * has allocated a large fraction of the available size_t. + */ + if (pg_add_size_overflow(input_len, num_quotes, &result_size) || + pg_add_size_overflow(result_size, 3, &result_size)) /* two quotes plus a NUL */ + goto overflow; + if (!as_ident && num_backslashes > 0) - result_size += num_backslashes + 2; + { + if (pg_add_size_overflow(result_size, num_backslashes, &result_size) || + pg_add_size_overflow(result_size, 2, &result_size)) /* for " E" prefix */ + goto overflow; + } + result = rp = (char *) malloc(result_size); if (rp == NULL) { @@ -4359,6 +4387,12 @@ PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident) *rp = '\0'; return result; + +overflow: + libpq_append_conn_error(conn, + "escaped string size exceeds the maximum allowed (%zu)", + SIZE_MAX); + return NULL; } char * @@ -4424,16 +4458,25 @@ PQescapeByteaInternal(PGconn *conn, unsigned char *result; size_t i; size_t len; - size_t bslash_len = (std_strings ? 1 : 2); + const size_t bslash_len = (std_strings ? 1 : 2); /* - * empty string has 1 char ('\0') + * Calculate the escaped length, watching for overflow as we do with + * PQescapeInternal(). The following code relies on a small constant + * bslash_len so that small additions and multiplications don't need their + * own overflow checks. + * + * Start with the empty string, which has 1 char ('\0'). */ len = 1; if (use_hex) { - len += bslash_len + 1 + 2 * from_length; + /* We prepend "\x" and double each input character. */ + if (pg_add_size_overflow(len, bslash_len + 1, &len) || + pg_add_size_overflow(len, from_length, &len) || + pg_add_size_overflow(len, from_length, &len)) + goto overflow; } else { @@ -4441,13 +4484,25 @@ PQescapeByteaInternal(PGconn *conn, for (i = from_length; i > 0; i--, vp++) { if (*vp < 0x20 || *vp > 0x7e) - len += bslash_len + 3; + { + if (pg_add_size_overflow(len, bslash_len + 3, &len)) /* octal "\ooo" */ + goto overflow; + } else if (*vp == '\'') - len += 2; + { + if (pg_add_size_overflow(len, 2, &len)) /* double each quote */ + goto overflow; + } else if (*vp == '\\') - len += bslash_len + bslash_len; + { + if (pg_add_size_overflow(len, bslash_len * 2, &len)) /* double each backslash */ + goto overflow; + } else - len++; + { + if (pg_add_size_overflow(len, 1, &len)) + goto overflow; + } } } @@ -4508,6 +4563,13 @@ PQescapeByteaInternal(PGconn *conn, *rp = '\0'; return result; + +overflow: + if (conn) + libpq_append_conn_error(conn, + "escaped bytea size exceeds the maximum allowed (%zu)", + SIZE_MAX); + return NULL; } unsigned char * diff --git a/src/interfaces/libpq/fe-gssapi-common.c b/src/interfaces/libpq/fe-gssapi-common.c index 1ae2bd24c1dc3..e7776fe76e72d 100644 --- a/src/interfaces/libpq/fe-gssapi-common.c +++ b/src/interfaces/libpq/fe-gssapi-common.c @@ -3,7 +3,7 @@ * fe-gssapi-common.c * The front-end (client) GSSAPI common code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/interfaces/libpq/fe-gssapi-common.h b/src/interfaces/libpq/fe-gssapi-common.h index fe96b491bc26b..f89d9e071e877 100644 --- a/src/interfaces/libpq/fe-gssapi-common.h +++ b/src/interfaces/libpq/fe-gssapi-common.h @@ -4,7 +4,7 @@ * * Definitions for GSSAPI common routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/fe-gssapi-common.h diff --git a/src/interfaces/libpq/fe-lobj.c b/src/interfaces/libpq/fe-lobj.c index 05e17bed508ff..0c57e0d2de5f0 100644 --- a/src/interfaces/libpq/fe-lobj.c +++ b/src/interfaces/libpq/fe-lobj.c @@ -3,7 +3,7 @@ * fe-lobj.c * Front-end large object interface * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index c14e3c952502d..13775cfb8b90c 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -19,7 +19,7 @@ * routines. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -236,8 +236,8 @@ pqGetInt(int *result, size_t bytes, PGconn *conn) break; default: pqInternalNotice(&conn->noticeHooks, - "integer of size %lu not supported by pqGetInt", - (unsigned long) bytes); + "integer of size %zu not supported by pqGetInt", + bytes); return EOF; } @@ -269,8 +269,8 @@ pqPutInt(int value, size_t bytes, PGconn *conn) break; default: pqInternalNotice(&conn->noticeHooks, - "integer of size %lu not supported by pqPutInt", - (unsigned long) bytes); + "integer of size %zu not supported by pqPutInt", + bytes); return EOF; } @@ -553,9 +553,35 @@ pqPutMsgEnd(PGconn *conn) /* Make message eligible to send */ conn->outCount = conn->outMsgEnd; + /* If appropriate, try to push out some data */ if (conn->outCount >= 8192) { - int toSend = conn->outCount - (conn->outCount % 8192); + int toSend = conn->outCount; + + /* + * On Unix-pipe connections, it seems profitable to prefer sending + * pipe-buffer-sized packets not randomly-sized ones, so retain the + * last partial-8K chunk in our buffer for now. On TCP connections, + * the advantage of that is far less clear. Moreover, it flat out + * isn't safe when using SSL or GSSAPI, because those code paths have + * API stipulations that if they fail to send all the data that was + * offered in the previous write attempt, we mustn't offer less data + * in this write attempt. The previous write attempt might've been + * pqFlush attempting to send everything in the buffer, so we mustn't + * offer less now. (Presently, we won't try to use SSL or GSSAPI on + * Unix connections, so those checks are just Asserts. They'll have + * to become part of the regular if-test if we ever change that.) + */ + if (conn->raddr.addr.ss_family == AF_UNIX) + { +#ifdef USE_SSL + Assert(!conn->ssl_in_use); +#endif +#ifdef ENABLE_GSS + Assert(!conn->gssenc); +#endif + toSend -= toSend % 8192; + } if (pqSendSome(conn, toSend) < 0) return EOF; @@ -1397,3 +1423,21 @@ libpq_append_conn_error(PGconn *conn, const char *fmt,...) appendPQExpBufferChar(&conn->errorMessage, '\n'); } + +/* + * For 19beta only, some protocol errors will have additional information + * appended to help with the "grease" campaign. + */ +void +libpq_append_grease_info(PGconn *conn) +{ + /* translator: %s is a URL */ + libpq_append_conn_error(conn, + "\tThis indicates a bug in either the server being contacted\n" + "\tor a proxy handling the connection. Please consider\n" + "\treporting this to the maintainers of that software.\n" + "\tFor more information, including instructions on how to\n" + "\twork around this issue for now, visit\n" + "\t\t%s", + "https://www.postgresql.org/docs/devel/libpq-connect.html#LIBPQ-CONNECT-MAX-PROTOCOL-VERSION"); +} diff --git a/src/interfaces/libpq/fe-print.c b/src/interfaces/libpq/fe-print.c index 6a4de16fe4e9e..c4c709bc3e46b 100644 --- a/src/interfaces/libpq/fe-print.c +++ b/src/interfaces/libpq/fe-print.c @@ -3,7 +3,7 @@ * fe-print.c * functions for pretty-printing query results * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * These functions were formerly part of fe-exec.c, but they @@ -33,6 +33,7 @@ #endif #endif +#include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" @@ -104,6 +105,16 @@ PQprint(FILE *fout, const PGresult *res, const PQprintOpt *po) } screen_size; #endif + /* + * Quick sanity check on po->fieldSep, since we make heavy use of int + * math throughout. + */ + if (fs_len < strlen(po->fieldSep)) + { + fprintf(stderr, libpq_gettext("overlong field separator\n")); + goto exit; + } + nTups = PQntuples(res); fieldNames = (const char **) calloc(nFields, sizeof(char *)); fieldNotNum = (unsigned char *) calloc(nFields, 1); @@ -391,7 +402,7 @@ do_field(const PQprintOpt *po, const PGresult *res, { if (plen > fieldMax[j]) fieldMax[j] = plen; - if (!(fields[i * nFields + j] = (char *) malloc(plen + 1))) + if (!(fields[i * nFields + j] = (char *) malloc((size_t) plen + 1))) { fprintf(stderr, libpq_gettext("out of memory\n")); return false; @@ -453,15 +464,31 @@ do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, fputs("", fout); else { - int tot = 0; + size_t tot = 0; int n = 0; char *p = NULL; + /* Calculate the border size, checking for overflow. */ for (; n < nFields; n++) - tot += fieldMax[n] + fs_len + (po->standard ? 2 : 0); + { + /* Field plus separator, plus 2 extra '-' in standard format. */ + if (pg_add_size_overflow(tot, fieldMax[n], &tot) || + pg_add_size_overflow(tot, fs_len, &tot) || + (po->standard && pg_add_size_overflow(tot, 2, &tot))) + goto overflow; + } if (po->standard) - tot += fs_len * 2 + 2; - border = malloc(tot + 1); + { + /* An extra separator at the front and back. */ + if (pg_add_size_overflow(tot, fs_len, &tot) || + pg_add_size_overflow(tot, fs_len, &tot) || + pg_add_size_overflow(tot, 2, &tot)) + goto overflow; + } + if (pg_add_size_overflow(tot, 1, &tot)) /* terminator */ + goto overflow; + + border = malloc(tot); if (!border) { fprintf(stderr, libpq_gettext("out of memory\n")); @@ -524,6 +551,10 @@ do_header(FILE *fout, const PQprintOpt *po, const int nFields, int *fieldMax, else fprintf(fout, "\n%s\n", border); return border; + +overflow: + fprintf(stderr, libpq_gettext("header size exceeds the maximum allowed\n")); + return NULL; } diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index beb1c889aad73..b0638bd325484 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -3,7 +3,7 @@ * fe-protocol3.c * functions that are specific to frontend/backend protocol version 3 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,6 +16,7 @@ #include #include +#include #ifdef WIN32 #include "win32.h" @@ -24,6 +25,7 @@ #include #endif +#include "common/int.h" #include "libpq-fe.h" #include "libpq-int.h" #include "mb/pg_wchar.h" @@ -43,6 +45,7 @@ (id) == PqMsg_RowDescription) +static void handleFatalError(PGconn *conn); static void handleSyncLoss(PGconn *conn, char id, int msgLength); static int getRowDescriptions(PGconn *conn, int msgLength); static int getParamDescriptions(PGconn *conn, int msgLength); @@ -54,8 +57,8 @@ static int getCopyStart(PGconn *conn, ExecStatusType copytype); static int getReadyForQuery(PGconn *conn); static void reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding); -static int build_startup_packet(const PGconn *conn, char *packet, - const PQEnvironmentOption *options); +static size_t build_startup_packet(const PGconn *conn, char *packet, + const PQEnvironmentOption *options); /* @@ -120,12 +123,12 @@ pqParseInput3(PGconn *conn) conn)) { /* - * XXX add some better recovery code... plan is to skip over - * the message using its length, then report an error. For the - * moment, just treat this like loss of sync (which indeed it - * might be!) + * Abandon the connection. There's not much else we can + * safely do; we can't just ignore the message or we could + * miss important changes to the connection state. + * pqCheckInBufferSpace() already reported the error. */ - handleSyncLoss(conn, id, msgLength); + handleFatalError(conn); } return; } @@ -456,6 +459,11 @@ pqParseInput3(PGconn *conn) /* Normal case: parsing agrees with specified length */ pqParseDone(conn, conn->inCursor); } + else if (conn->error_result && conn->status == CONNECTION_BAD) + { + /* The connection was abandoned and we already reported it */ + return; + } else { /* Trouble --- report it */ @@ -470,15 +478,14 @@ pqParseInput3(PGconn *conn) } /* - * handleSyncLoss: clean up after loss of message-boundary sync + * handleFatalError: clean up after a nonrecoverable error * - * There isn't really a lot we can do here except abandon the connection. + * This is for errors where we need to abandon the connection. The caller has + * already saved the error message in conn->errorMessage. */ static void -handleSyncLoss(PGconn *conn, char id, int msgLength) +handleFatalError(PGconn *conn) { - libpq_append_conn_error(conn, "lost synchronization with server: got message type \"%c\", length %d", - id, msgLength); /* build an error result holding the error message */ pqSaveErrorResult(conn); conn->asyncStatus = PGASYNC_READY; /* drop out of PQgetResult wait loop */ @@ -487,6 +494,19 @@ handleSyncLoss(PGconn *conn, char id, int msgLength) conn->status = CONNECTION_BAD; /* No more connection to backend */ } +/* + * handleSyncLoss: clean up after loss of message-boundary sync + * + * There isn't really a lot we can do here except abandon the connection. + */ +static void +handleSyncLoss(PGconn *conn, char id, int msgLength) +{ + libpq_append_conn_error(conn, "lost synchronization with server: got message type \"%c\", length %d", + id, msgLength); + handleFatalError(conn); +} + /* * parseInput subroutine to read a 'T' (row descriptions) message. * We'll build a new PGresult structure (unless called for a Describe @@ -1216,8 +1236,21 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding) * scridx[] respectively. */ - /* we need a safe allocation size... */ + /* + * We need a safe allocation size. + * + * The only caller of reportErrorPosition() is pqBuildErrorMessage3(); it + * gets its query from either a PQresultErrorField() or a PGcmdQueueEntry, + * both of which must have fit into conn->inBuffer/outBuffer. So slen fits + * inside an int, but we can't assume that (slen * sizeof(int)) fits + * inside a size_t. + */ slen = strlen(wquery) + 1; + if (slen > SIZE_MAX / sizeof(int)) + { + free(wquery); + return; + } qidx = (int *) malloc(slen * sizeof(int)); if (qidx == NULL) @@ -1411,6 +1444,15 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) { int their_version; int num; + bool found_test_protocol_negotiation; + bool expect_test_protocol_negotiation; + + /* + * During 19beta only, if protocol grease is in use, assume that it's the + * cause of any invalid messages encountered below. We'll print extra + * information for the end user in that case. + */ + bool need_grease_info = (conn->max_pversion == PG_PROTOCOL_GREASE); if (pqGetInt(&their_version, 4, conn) != 0) goto eof; @@ -1418,7 +1460,19 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) if (pqGetInt(&num, 4, conn) != 0) goto eof; - /* Check the protocol version */ + /* + * Check the protocol version. + * + * PG_PROTOCOL_GREASE is intentionally unsupported and reserved. It's + * higher than any real version, so check for that first, to get the most + * specific error message. Then check the upper and lower bounds. + */ + if (their_version == PG_PROTOCOL_GREASE) + { + libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested \"grease\" protocol version 3.9999"); + goto failure; + } + if (their_version > conn->pversion) { libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested downgrade to a higher-numbered version"); @@ -1432,9 +1486,9 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) } /* 3.1 never existed, we went straight from 3.0 to 3.2 */ - if (their_version == PG_PROTOCOL(3, 1)) + if (their_version == PG_PROTOCOL_RESERVED_31) { - libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requests downgrade to non-existent 3.1 protocol version"); + libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested downgrade to non-existent 3.1 protocol version"); goto failure; } @@ -1452,12 +1506,14 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) if (their_version < conn->min_pversion) { - libpq_append_conn_error(conn, "server only supports protocol version %d.%d, but min_protocol_version was set to %d.%d", + libpq_append_conn_error(conn, "server only supports protocol version %d.%d, but \"%s\" was set to %d.%d", PG_PROTOCOL_MAJOR(their_version), PG_PROTOCOL_MINOR(their_version), + "min_protocol_version", PG_PROTOCOL_MAJOR(conn->min_pversion), PG_PROTOCOL_MINOR(conn->min_pversion)); + need_grease_info = false; /* this is valid server behavior */ goto failure; } @@ -1465,9 +1521,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) conn->pversion = their_version; /* - * We don't currently request any protocol extensions, so we don't expect - * the server to reply with any either. + * Check that all expected unsupported parameters are reported by the + * server. */ + found_test_protocol_negotiation = false; + expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE); + for (int i = 0; i < num; i++) { if (pqGets(&conn->workBuffer, conn)) @@ -1476,10 +1535,32 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) } if (strncmp(conn->workBuffer.data, "_pq_.", 5) != 0) { - libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a _pq_. prefix (\"%s\")", conn->workBuffer.data); + libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data); goto failure; } - libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data); + + /* Check if this is the expected test parameter */ + if (expect_test_protocol_negotiation && + strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0) + { + found_test_protocol_negotiation = true; + } + else + { + libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", + conn->workBuffer.data); + goto failure; + } + } + + /* + * If we requested protocol grease, the server must report + * _pq_.test_protocol_negotiation as unsupported. This ensures + * comprehensive NegotiateProtocolVersion implementation. + */ + if (expect_test_protocol_negotiation && !found_test_protocol_negotiation) + { + libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message"); goto failure; } @@ -1488,6 +1569,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn) eof: libpq_append_conn_error(conn, "received invalid protocol negotiation message: message too short"); failure: + if (need_grease_info) + libpq_append_grease_info(conn); conn->asyncStatus = PGASYNC_READY; pqSaveErrorResult(conn); return 1; @@ -1518,14 +1601,18 @@ getParameterStatus(PGconn *conn) return EOF; } /* And save it */ - pqSaveParameterStatus(conn, conn->workBuffer.data, valueBuf.data); + if (!pqSaveParameterStatus(conn, conn->workBuffer.data, valueBuf.data)) + { + libpq_append_conn_error(conn, "out of memory"); + handleFatalError(conn); + } termPQExpBuffer(&valueBuf); return 0; } /* * parseInput subroutine to read a BackendKeyData message. - * Entry: 'v' message type and length have already been consumed. + * Entry: 'K' message type and length have already been consumed. * Exit: returns 0 if successfully consumed message. * returns EOF if not enough data. */ @@ -1546,12 +1633,33 @@ getBackendKeyData(PGconn *conn, int msgLength) cancel_key_len = 5 + msgLength - (conn->inCursor - conn->inStart); + if (cancel_key_len != 4 && conn->pversion == PG_PROTOCOL(3, 0)) + { + libpq_append_conn_error(conn, "received invalid BackendKeyData message: cancel key with length %d not allowed in protocol version 3.0 (must be 4 bytes)", cancel_key_len); + handleFatalError(conn); + return 0; + } + + if (cancel_key_len < 4) + { + libpq_append_conn_error(conn, "received invalid BackendKeyData message: cancel key with length %d is too short (minimum 4 bytes)", cancel_key_len); + handleFatalError(conn); + return 0; + } + + if (cancel_key_len > 256) + { + libpq_append_conn_error(conn, "received invalid BackendKeyData message: cancel key with length %d is too long (maximum 256 bytes)", cancel_key_len); + handleFatalError(conn); + return 0; + } + conn->be_cancel_key = malloc(cancel_key_len); if (conn->be_cancel_key == NULL) { libpq_append_conn_error(conn, "out of memory"); - /* discard the message */ - return EOF; + handleFatalError(conn); + return 0; } if (pqGetnchar(conn->be_cancel_key, cancel_key_len, conn)) { @@ -1588,7 +1696,17 @@ getNotify(PGconn *conn) /* must save name while getting extra string */ svname = strdup(conn->workBuffer.data); if (!svname) - return EOF; + { + /* + * Notify messages can arrive at any state, so we cannot associate the + * error with any particular query. There's no way to return back an + * "async error", so the best we can do is drop the connection. That + * seems better than silently ignoring the notification. + */ + libpq_append_conn_error(conn, "out of memory"); + handleFatalError(conn); + return 0; + } if (pqGets(&conn->workBuffer, conn)) { free(svname); @@ -1603,21 +1721,26 @@ getNotify(PGconn *conn) nmlen = strlen(svname); extralen = strlen(conn->workBuffer.data); newNotify = (PGnotify *) malloc(sizeof(PGnotify) + nmlen + extralen + 2); - if (newNotify) - { - newNotify->relname = (char *) newNotify + sizeof(PGnotify); - strcpy(newNotify->relname, svname); - newNotify->extra = newNotify->relname + nmlen + 1; - strcpy(newNotify->extra, conn->workBuffer.data); - newNotify->be_pid = be_pid; - newNotify->next = NULL; - if (conn->notifyTail) - conn->notifyTail->next = newNotify; - else - conn->notifyHead = newNotify; - conn->notifyTail = newNotify; + if (!newNotify) + { + free(svname); + libpq_append_conn_error(conn, "out of memory"); + handleFatalError(conn); + return 0; } + newNotify->relname = (char *) newNotify + sizeof(PGnotify); + strcpy(newNotify->relname, svname); + newNotify->extra = newNotify->relname + nmlen + 1; + strcpy(newNotify->extra, conn->workBuffer.data); + newNotify->be_pid = be_pid; + newNotify->next = NULL; + if (conn->notifyTail) + conn->notifyTail->next = newNotify; + else + conn->notifyHead = newNotify; + conn->notifyTail = newNotify; + free(svname); return 0; } @@ -1751,12 +1874,12 @@ getCopyDataMessage(PGconn *conn) conn)) { /* - * XXX add some better recovery code... plan is to skip over - * the message using its length, then report an error. For the - * moment, just treat this like loss of sync (which indeed it - * might be!) + * Abandon the connection. There's not much else we can + * safely do; we can't just ignore the message or we could + * miss important changes to the connection state. + * pqCheckInBufferSpace() already reported the error. */ - handleSyncLoss(conn, id, msgLength); + handleFatalError(conn); return -2; } return 0; @@ -2185,12 +2308,12 @@ pqFunctionCall3(PGconn *conn, Oid fnid, conn)) { /* - * XXX add some better recovery code... plan is to skip over - * the message using its length, then report an error. For the - * moment, just treat this like loss of sync (which indeed it - * might be!) + * Abandon the connection. There's not much else we can + * safely do; we can't just ignore the message or we could + * miss important changes to the connection state. + * pqCheckInBufferSpace() already reported the error. */ - handleSyncLoss(conn, id, msgLength); + handleFatalError(conn); break; } continue; @@ -2203,7 +2326,7 @@ pqFunctionCall3(PGconn *conn, Oid fnid, */ switch (id) { - case 'V': /* function result */ + case PqMsg_FunctionCallResponse: if (pqGetInt(actual_result_len, 4, conn)) continue; if (*actual_result_len != -1) @@ -2224,22 +2347,22 @@ pqFunctionCall3(PGconn *conn, Oid fnid, /* correctly finished function result message */ status = PGRES_COMMAND_OK; break; - case 'E': /* error return */ + case PqMsg_ErrorResponse: if (pqGetErrorNotice3(conn, true)) continue; status = PGRES_FATAL_ERROR; break; - case 'A': /* notify message */ + case PqMsg_NotificationResponse: /* handle notify and go back to processing return values */ if (getNotify(conn)) continue; break; - case 'N': /* notice */ + case PqMsg_NoticeResponse: /* handle notice and go back to processing return values */ if (pqGetErrorNotice3(conn, false)) continue; break; - case 'Z': /* backend is ready for new query */ + case PqMsg_ReadyForQuery: if (getReadyForQuery(conn)) continue; @@ -2271,7 +2394,7 @@ pqFunctionCall3(PGconn *conn, Oid fnid, } /* and we're out */ return pqPrepareAsyncResult(conn); - case 'S': /* parameter status */ + case PqMsg_ParameterStatus: if (getParameterStatus(conn)) continue; break; @@ -2314,12 +2437,20 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen, const PQEnvironmentOption *options) { char *startpacket; + size_t len; + + len = build_startup_packet(conn, NULL, options); + if (len == 0 || len > INT_MAX) + return NULL; - *packetlen = build_startup_packet(conn, NULL, options); + *packetlen = len; startpacket = (char *) malloc(*packetlen); if (!startpacket) return NULL; - *packetlen = build_startup_packet(conn, startpacket, options); + + len = build_startup_packet(conn, startpacket, options); + Assert(*packetlen == len); + return startpacket; } @@ -2330,13 +2461,13 @@ pqBuildStartupPacket3(PGconn *conn, int *packetlen, * To avoid duplicate logic, this routine is called twice: the first time * (with packet == NULL) just counts the space needed, the second time * (with packet == allocated space) fills it in. Return value is the number - * of bytes used. + * of bytes used, or zero in the unlikely event of size_t overflow. */ -static int +static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options) { - int packet_len = 0; + size_t packet_len = 0; const PQEnvironmentOption *next_eo; const char *val; @@ -2355,10 +2486,12 @@ build_startup_packet(const PGconn *conn, char *packet, do { \ if (packet) \ strcpy(packet + packet_len, optname); \ - packet_len += strlen(optname) + 1; \ + if (pg_add_size_overflow(packet_len, strlen(optname) + 1, &packet_len)) \ + return 0; \ if (packet) \ strcpy(packet + packet_len, optval); \ - packet_len += strlen(optval) + 1; \ + if (pg_add_size_overflow(packet_len, strlen(optval) + 1, &packet_len)) \ + return 0; \ } while(0) if (conn->pguser && conn->pguser[0]) @@ -2380,6 +2513,14 @@ build_startup_packet(const PGconn *conn, char *packet, if (conn->client_encoding_initial && conn->client_encoding_initial[0]) ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial); + /* + * Add the test_protocol_negotiation option when greasing, to test that + * servers properly report unsupported protocol options in addition to + * unsupported minor versions. + */ + if (conn->pversion == PG_PROTOCOL_GREASE) + ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", ""); + /* Add any environment-driven GUC settings needed */ for (next_eo = options; next_eo->envName; next_eo++) { @@ -2393,7 +2534,8 @@ build_startup_packet(const PGconn *conn, char *packet, /* Add trailing terminator */ if (packet) packet[packet_len] = '\0'; - packet_len++; + if (pg_add_size_overflow(packet_len, 1, &packet_len)) + return 0; return packet_len; } diff --git a/src/interfaces/libpq/fe-secure-common.c b/src/interfaces/libpq/fe-secure-common.c index 56d0d612eed4e..db5f26f20520b 100644 --- a/src/interfaces/libpq/fe-secure-common.c +++ b/src/interfaces/libpq/fe-secure-common.c @@ -8,7 +8,7 @@ * file contains support routines that are used by the library-specific * implementations such as fe-secure-openssl.c. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/interfaces/libpq/fe-secure-common.h b/src/interfaces/libpq/fe-secure-common.h index f65edb8e1da92..90010bb86a4b8 100644 --- a/src/interfaces/libpq/fe-secure-common.h +++ b/src/interfaces/libpq/fe-secure-common.h @@ -4,7 +4,7 @@ * * common implementation-independent SSL support code * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/interfaces/libpq/fe-secure-gssapi.c b/src/interfaces/libpq/fe-secure-gssapi.c index ce183bc04b4c8..72f438dfa9cc0 100644 --- a/src/interfaces/libpq/fe-secure-gssapi.c +++ b/src/interfaces/libpq/fe-secure-gssapi.c @@ -3,7 +3,7 @@ * fe-secure-gssapi.c * The front-end (client) encryption support for GSSAPI * - * Portions Copyright (c) 2016-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2016-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/interfaces/libpq/fe-secure-gssapi.c @@ -47,11 +47,18 @@ * don't want the other side to send arbitrarily huge packets as we * would have to allocate memory for them to then pass them to GSSAPI. * - * Therefore, these two #define's are effectively part of the protocol + * Therefore, this #define is effectively part of the protocol * spec and can't ever be changed. */ -#define PQ_GSS_SEND_BUFFER_SIZE 16384 -#define PQ_GSS_RECV_BUFFER_SIZE 16384 +#define PQ_GSS_MAX_PACKET_SIZE 16384 /* includes uint32 header word */ + +/* + * However, during the authentication exchange we must cope with whatever + * message size the GSSAPI library wants to send (because our protocol + * doesn't support splitting those messages). Depending on configuration + * those messages might be as much as 64kB. + */ +#define PQ_GSS_AUTH_BUFFER_SIZE 65536 /* includes uint32 header word */ /* * We need these state variables per-connection. To allow the functions @@ -105,16 +112,16 @@ pg_GSS_write(PGconn *conn, const void *ptr, size_t len) * again, so if it offers a len less than that, something is wrong. * * Note: it may seem attractive to report partial write completion once - * we've successfully sent any encrypted packets. However, that can cause - * problems for callers; notably, pqPutMsgEnd's heuristic to send only - * full 8K blocks interacts badly with such a hack. We won't save much, + * we've successfully sent any encrypted packets. However, doing that + * expands the state space of this processing and has been responsible for + * bugs in the past (cf. commit d053a879b). We won't save much, * typically, by letting callers discard data early, so don't risk it. */ if (len < PqGSSSendConsumed) { appendPQExpBufferStr(&conn->errorMessage, "GSSAPI caller failed to retransmit all data needing to be retried\n"); - errno = EINVAL; + SOCK_ERRNO_SET(EINVAL); return -1; } @@ -192,23 +199,23 @@ pg_GSS_write(PGconn *conn, const void *ptr, size_t len) if (major != GSS_S_COMPLETE) { pg_GSS_error(libpq_gettext("GSSAPI wrap error"), conn, major, minor); - errno = EIO; /* for lack of a better idea */ + SOCK_ERRNO_SET(EIO); /* for lack of a better idea */ goto cleanup; } if (conf_state == 0) { libpq_append_conn_error(conn, "outgoing GSSAPI message would not use confidentiality"); - errno = EIO; /* for lack of a better idea */ + SOCK_ERRNO_SET(EIO); /* for lack of a better idea */ goto cleanup; } - if (output.length > PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)) + if (output.length > PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)) { libpq_append_conn_error(conn, "client tried to send oversize GSSAPI packet (%zu > %zu)", (size_t) output.length, - PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)); - errno = EIO; /* for lack of a better idea */ + PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)); + SOCK_ERRNO_SET(EIO); /* for lack of a better idea */ goto cleanup; } @@ -334,7 +341,7 @@ pg_GSS_read(PGconn *conn, void *ptr, size_t len) /* If we still haven't got the length, return to the caller */ if (PqGSSRecvLength < sizeof(uint32)) { - errno = EWOULDBLOCK; + SOCK_ERRNO_SET(EWOULDBLOCK); return -1; } } @@ -342,12 +349,12 @@ pg_GSS_read(PGconn *conn, void *ptr, size_t len) /* Decode the packet length and check for overlength packet */ input.length = pg_ntoh32(*(uint32 *) PqGSSRecvBuffer); - if (input.length > PQ_GSS_RECV_BUFFER_SIZE - sizeof(uint32)) + if (input.length > PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)) { libpq_append_conn_error(conn, "oversize GSSAPI packet sent by the server (%zu > %zu)", (size_t) input.length, - PQ_GSS_RECV_BUFFER_SIZE - sizeof(uint32)); - errno = EIO; /* for lack of a better idea */ + PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32)); + SOCK_ERRNO_SET(EIO); /* for lack of a better idea */ return -1; } @@ -366,7 +373,7 @@ pg_GSS_read(PGconn *conn, void *ptr, size_t len) /* If we don't yet have the whole packet, return to the caller */ if (PqGSSRecvLength - sizeof(uint32) < input.length) { - errno = EWOULDBLOCK; + SOCK_ERRNO_SET(EWOULDBLOCK); return -1; } @@ -386,7 +393,7 @@ pg_GSS_read(PGconn *conn, void *ptr, size_t len) pg_GSS_error(libpq_gettext("GSSAPI unwrap error"), conn, major, minor); ret = -1; - errno = EIO; /* for lack of a better idea */ + SOCK_ERRNO_SET(EIO); /* for lack of a better idea */ goto cleanup; } @@ -394,7 +401,7 @@ pg_GSS_read(PGconn *conn, void *ptr, size_t len) { libpq_append_conn_error(conn, "incoming GSSAPI message did not use confidentiality"); ret = -1; - errno = EIO; /* for lack of a better idea */ + SOCK_ERRNO_SET(EIO); /* for lack of a better idea */ goto cleanup; } @@ -430,7 +437,8 @@ gss_read(PGconn *conn, void *recv_buffer, size_t length, ssize_t *ret) *ret = pqsecure_raw_read(conn, recv_buffer, length); if (*ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + if (SOCK_ERRNO == EAGAIN || SOCK_ERRNO == EWOULDBLOCK || + SOCK_ERRNO == EINTR) return PGRES_POLLING_READING; else return PGRES_POLLING_FAILED; @@ -450,7 +458,8 @@ gss_read(PGconn *conn, void *recv_buffer, size_t length, ssize_t *ret) *ret = pqsecure_raw_read(conn, recv_buffer, length); if (*ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + if (SOCK_ERRNO == EAGAIN || SOCK_ERRNO == EWOULDBLOCK || + SOCK_ERRNO == EINTR) return PGRES_POLLING_READING; else return PGRES_POLLING_FAILED; @@ -485,12 +494,15 @@ pqsecure_open_gss(PGconn *conn) * initialize state variables. By malloc'ing the buffers separately, we * ensure that they are sufficiently aligned for the length-word accesses * that we do in some places in this file. + * + * We'll use PQ_GSS_AUTH_BUFFER_SIZE-sized buffers until transport + * negotiation is complete, then switch to PQ_GSS_MAX_PACKET_SIZE. */ if (PqGSSSendBuffer == NULL) { - PqGSSSendBuffer = malloc(PQ_GSS_SEND_BUFFER_SIZE); - PqGSSRecvBuffer = malloc(PQ_GSS_RECV_BUFFER_SIZE); - PqGSSResultBuffer = malloc(PQ_GSS_RECV_BUFFER_SIZE); + PqGSSSendBuffer = malloc(PQ_GSS_AUTH_BUFFER_SIZE); + PqGSSRecvBuffer = malloc(PQ_GSS_AUTH_BUFFER_SIZE); + PqGSSResultBuffer = malloc(PQ_GSS_AUTH_BUFFER_SIZE); if (!PqGSSSendBuffer || !PqGSSRecvBuffer || !PqGSSResultBuffer) { libpq_append_conn_error(conn, "out of memory"); @@ -510,7 +522,8 @@ pqsecure_open_gss(PGconn *conn) ret = pqsecure_raw_write(conn, PqGSSSendBuffer + PqGSSSendNext, amount); if (ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + if (SOCK_ERRNO == EAGAIN || SOCK_ERRNO == EWOULDBLOCK || + SOCK_ERRNO == EINTR) return PGRES_POLLING_WRITING; else return PGRES_POLLING_FAILED; @@ -564,13 +577,13 @@ pqsecure_open_gss(PGconn *conn) * so leave a spot at the end for a NULL byte too) and report that * back to the caller. */ - result = gss_read(conn, PqGSSRecvBuffer + PqGSSRecvLength, PQ_GSS_RECV_BUFFER_SIZE - PqGSSRecvLength - 1, &ret); + result = gss_read(conn, PqGSSRecvBuffer + PqGSSRecvLength, PQ_GSS_AUTH_BUFFER_SIZE - PqGSSRecvLength - 1, &ret); if (result != PGRES_POLLING_OK) return result; PqGSSRecvLength += ret; - Assert(PqGSSRecvLength < PQ_GSS_RECV_BUFFER_SIZE); + Assert(PqGSSRecvLength < PQ_GSS_AUTH_BUFFER_SIZE); PqGSSRecvBuffer[PqGSSRecvLength] = '\0'; appendPQExpBuffer(&conn->errorMessage, "%s\n", PqGSSRecvBuffer + 1); @@ -584,11 +597,11 @@ pqsecure_open_gss(PGconn *conn) /* Get the length and check for over-length packet */ input.length = pg_ntoh32(*(uint32 *) PqGSSRecvBuffer); - if (input.length > PQ_GSS_RECV_BUFFER_SIZE - sizeof(uint32)) + if (input.length > PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)) { libpq_append_conn_error(conn, "oversize GSSAPI packet sent by the server (%zu > %zu)", (size_t) input.length, - PQ_GSS_RECV_BUFFER_SIZE - sizeof(uint32)); + PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)); return PGRES_POLLING_FAILED; } @@ -668,12 +681,33 @@ pqsecure_open_gss(PGconn *conn) conn->gcred = GSS_C_NO_CREDENTIAL; gss_release_buffer(&minor, &output); + /* + * Release the large authentication buffers and allocate the ones we + * want for normal operation. (This maneuver is safe only because + * pqDropConnection will drop the buffers; otherwise, during a + * reconnection we'd be at risk of using undersized buffers during + * negotiation.) + */ + free(PqGSSSendBuffer); + free(PqGSSRecvBuffer); + free(PqGSSResultBuffer); + PqGSSSendBuffer = malloc(PQ_GSS_MAX_PACKET_SIZE); + PqGSSRecvBuffer = malloc(PQ_GSS_MAX_PACKET_SIZE); + PqGSSResultBuffer = malloc(PQ_GSS_MAX_PACKET_SIZE); + if (!PqGSSSendBuffer || !PqGSSRecvBuffer || !PqGSSResultBuffer) + { + libpq_append_conn_error(conn, "out of memory"); + return PGRES_POLLING_FAILED; + } + PqGSSSendLength = PqGSSSendNext = PqGSSSendConsumed = 0; + PqGSSRecvLength = PqGSSResultLength = PqGSSResultNext = 0; + /* * Determine the max packet size which will fit in our buffer, after * accounting for the length. pg_GSS_write will need this. */ major = gss_wrap_size_limit(&minor, conn->gctx, 1, GSS_C_QOP_DEFAULT, - PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32), + PQ_GSS_MAX_PACKET_SIZE - sizeof(uint32), &PqGSSMaxPktSize); if (GSS_ERROR(major)) @@ -687,10 +721,11 @@ pqsecure_open_gss(PGconn *conn) } /* Must have output.length > 0 */ - if (output.length > PQ_GSS_SEND_BUFFER_SIZE - sizeof(uint32)) + if (output.length > PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)) { - pg_GSS_error(libpq_gettext("GSSAPI context establishment error"), - conn, major, minor); + libpq_append_conn_error(conn, "client tried to send oversize GSSAPI packet (%zu > %zu)", + (size_t) output.length, + PQ_GSS_AUTH_BUFFER_SIZE - sizeof(uint32)); gss_release_buffer(&minor, &output); return PGRES_POLLING_FAILED; } diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 78f9e84eb353b..fbd3c63fb5d9f 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -4,7 +4,7 @@ * OpenSSL support * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -693,34 +693,35 @@ static unsigned char alpn_protos[] = PG_ALPN_PROTOCOL_VECTOR; * purposes. The file will be written using the NSS keylog format. LibreSSL * 3.5 introduced stub function to set the callback for OpenSSL compatibility * but the callback is never invoked. + * + * Error messages added to the connection object won't be printed anywhere if + * the connection is successful. Errors in processing keylogging are printed + * to stderr to overcome this. */ static void SSL_CTX_keylog_cb(const SSL *ssl, const char *line) { int fd; - mode_t old_umask; ssize_t rc; PGconn *conn = SSL_get_app_data(ssl); if (conn == NULL) return; - old_umask = umask(077); fd = open(conn->sslkeylogfile, O_WRONLY | O_APPEND | O_CREAT, 0600); - umask(old_umask); if (fd == -1) { - libpq_append_conn_error(conn, "could not open ssl keylog file \"%s\": %s", - conn->sslkeylogfile, pg_strerror(errno)); + fprintf(stderr, libpq_gettext("WARNING: could not open SSL key logging file \"%s\": %m\n"), + conn->sslkeylogfile); return; } /* line is guaranteed by OpenSSL to be NUL terminated */ rc = write(fd, line, strlen(line)); if (rc < 0) - libpq_append_conn_error(conn, "could not write to ssl keylog file \"%s\": %s", - conn->sslkeylogfile, pg_strerror(errno)); + fprintf(stderr, libpq_gettext("WARNING: could not write to SSL key logging file \"%s\": %m\n"), + conn->sslkeylogfile); else rc = write(fd, "\n", 1); (void) rc; /* silence compiler warnings */ @@ -1044,6 +1045,10 @@ initialize_SSL(PGconn *conn) } conn->ssl_in_use = true; + /* + * If SSL key logging is requested, set up the callback if a compatible + * version of OpenSSL is used and libpq was compiled to support it. + */ if (conn->sslkeylogfile && strlen(conn->sslkeylogfile) > 0) { #ifdef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK @@ -1057,7 +1062,6 @@ initialize_SSL(PGconn *conn) #endif } - /* * SSL contexts are reference counted by OpenSSL. We can free it as soon * as we have created the SSL object, and it will stick around for as long diff --git a/src/interfaces/libpq/fe-secure.c b/src/interfaces/libpq/fe-secure.c index e686681ba155b..31d5b48d3f92a 100644 --- a/src/interfaces/libpq/fe-secure.c +++ b/src/interfaces/libpq/fe-secure.c @@ -6,7 +6,7 @@ * message integrity and endpoint authentication. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -379,7 +379,7 @@ pqsecure_raw_write(PGconn *conn, const void *ptr, size_t len) /* Set flag for EPIPE */ REMEMBER_EPIPE(spinfo, true); - /* FALL THRU */ + pg_fallthrough; case ECONNRESET: conn->write_failed = true; diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index a45f0d855871b..c348b08c39b63 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -3,7 +3,7 @@ * fe-trace.c * functions for libpq protocol tracing * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -113,7 +113,7 @@ pqTraceOutputByte1(FILE *pfdebug, const char *data, int *cursor) * that completes ErrorResponse and NoticeResponse messages. */ if (!isprint((unsigned char) *v)) - fprintf(pfdebug, " \\x%02x", *v); + fprintf(pfdebug, " \\x%02x", (unsigned char) *v); else fprintf(pfdebug, " %c", *v); *cursor += 1; @@ -212,7 +212,7 @@ pqTraceOutputNchar(FILE *pfdebug, int len, const char *data, int *cursor, bool s else { fwrite(v + next, 1, i - next, pfdebug); - fprintf(pfdebug, "\\x%02x", v[i]); + fprintf(pfdebug, "\\x%02x", (unsigned char) v[i]); next = i + 1; } } diff --git a/src/interfaces/libpq/legacy-pqsignal.c b/src/interfaces/libpq/legacy-pqsignal.c index ebd1695bf074f..0735e4ee0d5a6 100644 --- a/src/interfaces/libpq/legacy-pqsignal.c +++ b/src/interfaces/libpq/legacy-pqsignal.c @@ -4,7 +4,7 @@ * reliable BSD-style signal(2) routine stolen from RWW who stole it * from Stevens... * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -36,10 +36,12 @@ * is to ensure that no in-tree code accidentally calls this version.) */ #undef pqsignal -extern pqsigfunc pqsignal(int signo, pqsigfunc func); -pqsigfunc -pqsignal(int signo, pqsigfunc func) +typedef void (*pqsigfunc_legacy) (int postgres_signal_arg); +extern pqsigfunc_legacy pqsignal(int signo, pqsigfunc_legacy func); + +pqsigfunc_legacy +pqsignal(int signo, pqsigfunc_legacy func) { #ifndef WIN32 struct sigaction act, diff --git a/src/interfaces/libpq/libpq-events.c b/src/interfaces/libpq/libpq-events.c index a96e3694f1fa5..072679f966273 100644 --- a/src/interfaces/libpq/libpq-events.c +++ b/src/interfaces/libpq/libpq-events.c @@ -3,7 +3,7 @@ * libpq-events.c * functions for supporting the libpq "events" API * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/interfaces/libpq/libpq-events.h b/src/interfaces/libpq/libpq-events.h index 38dba297ae8bd..8528ab85aa6db 100644 --- a/src/interfaces/libpq/libpq-events.h +++ b/src/interfaces/libpq/libpq-events.h @@ -5,7 +5,7 @@ * that invoke the libpq "events" API, but are not interesting to * ordinary users of libpq. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/libpq-events.h diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 7d3a9df6fd559..f06e7a972c3c5 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -4,7 +4,7 @@ * This file contains definitions for structures and * externs for functions used by frontend postgres applications. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/libpq-fe.h @@ -63,6 +63,12 @@ extern "C" /* Indicates presence of the PQAUTHDATA_PROMPT_OAUTH_DEVICE authdata hook */ #define LIBPQ_HAS_PROMPT_OAUTH_DEVICE 1 +/* Features added in PostgreSQL v19: */ +/* Indicates presence of PQgetThreadLock */ +#define LIBPQ_HAS_GET_THREAD_LOCK 1 +/* Indicates presence of the PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 authdata hook */ +#define LIBPQ_HAS_OAUTH_BEARER_TOKEN_V2 1 + /* * Option flags for PQcopyResult */ @@ -193,7 +199,9 @@ typedef enum { PQAUTHDATA_PROMPT_OAUTH_DEVICE, /* user must visit a device-authorization * URL */ - PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token */ + PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token + * (v2 is preferred; see below) */ + PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, /* newest API for OAuth Bearer tokens */ } PGauthData; /* PGconn encapsulates a connection to the backend. @@ -234,9 +242,6 @@ typedef struct pgNotify struct pgNotify *next; /* list link */ } PGnotify; -/* deprecated name for int64_t */ -typedef int64_t pg_int64; - /* pg_usec_time_t is like time_t, but with microsecond resolution */ typedef int64_t pg_usec_time_t; @@ -400,7 +405,6 @@ extern int PQrequestCancel(PGconn *conn); /* Accessor functions for PGconn objects */ extern char *PQdb(const PGconn *conn); -extern char *PQservice(const PGconn *conn); extern char *PQuser(const PGconn *conn); extern char *PQpass(const PGconn *conn); extern char *PQhost(const PGconn *conn); @@ -466,12 +470,14 @@ extern PQnoticeProcessor PQsetNoticeProcessor(PGconn *conn, * Used to set callback that prevents concurrent access to * non-thread safe functions that libpq needs. * The default implementation uses a libpq internal mutex. - * Only required for multithreaded apps that use kerberos - * both within their app and for postgresql connections. + * Only required for multithreaded apps that use Kerberos or + * older (non-threadsafe) versions of Curl both within their + * app and for postgresql connections. */ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); +extern pgthreadlock_t PQgetThreadLock(void); /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); @@ -733,6 +739,7 @@ extern int PQenv2encoding(void); /* === in fe-auth.c === */ +/* Authdata for PQAUTHDATA_PROMPT_OAUTH_DEVICE */ typedef struct _PGpromptOAuthDevice { const char *verification_uri; /* verification URI to visit */ @@ -742,13 +749,18 @@ typedef struct _PGpromptOAuthDevice int expires_in; /* seconds until user code expires */ } PGpromptOAuthDevice; -/* for PGoauthBearerRequest.async() */ +/* + * For PGoauthBearerRequest.async(). This macro just allows clients to avoid + * depending on libpq-int.h or Winsock for the "socket" type; it's undefined + * immediately below. + */ #ifdef _WIN32 -#define SOCKTYPE uintptr_t /* avoids depending on winsock2.h for SOCKET */ +#define PQ_SOCKTYPE uintptr_t /* avoids depending on winsock2.h for SOCKET */ #else -#define SOCKTYPE int +#define PQ_SOCKTYPE int #endif +/* Authdata for PQAUTHDATA_OAUTH_BEARER_TOKEN */ typedef struct PGoauthBearerRequest { /* Hook inputs (constant across all calls) */ @@ -772,14 +784,18 @@ typedef struct PGoauthBearerRequest * blocking during the original call to the PQAUTHDATA_OAUTH_BEARER_TOKEN * hook, it may be returned directly, but one of request->async or * request->token must be set by the hook. + * + * The (PQ_SOCKTYPE *) in the signature is a placeholder for the platform's + * native socket type: (SOCKET *) on Windows, and (int *) everywhere else. */ PostgresPollingStatusType (*async) (PGconn *conn, struct PGoauthBearerRequest *request, - SOCKTYPE * altsock); + PQ_SOCKTYPE * altsock); /* * Callback to clean up custom allocations. A hook implementation may use - * this to free request->token and any resources in request->user. + * this to free request->token and any resources in request->user. V2 + * implementations should additionally free request->error, if set. * * This is technically optional, but highly recommended, because there is * no other indication as to when it is safe to free the token. @@ -802,7 +818,27 @@ typedef struct PGoauthBearerRequest void *user; } PGoauthBearerRequest; -#undef SOCKTYPE +#undef PQ_SOCKTYPE + +/* Authdata for PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 */ +typedef struct +{ + PGoauthBearerRequest v1; /* see the PGoauthBearerRequest struct, above */ + + /* Hook inputs (constant across all calls) */ + const char *issuer; /* the issuer identifier (RFC 9207) in use, as + * derived from the connection's oauth_issuer */ + + /* Hook outputs */ + + /* + * Hook-defined error message which will be included in the connection's + * PQerrorMessage() output when the flow fails. libpq does not take + * ownership of this pointer; any allocations should be freed during the + * cleanup callback. + */ + const char *error; +} PGoauthBearerRequestV2; extern char *PQencryptPassword(const char *passwd, const char *user); extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index a6cfd7f5c9d83..23de98290c9e2 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -9,7 +9,7 @@ * more likely to break across PostgreSQL releases than code that uses * only the official API. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/libpq-int.h @@ -357,7 +357,8 @@ typedef struct pg_conn_host pg_conn_host_type type; /* type of host address */ char *host; /* host name or socket path */ char *hostaddr; /* host numeric IP address */ - char *port; /* port number (always provided) */ + char *port; /* port number (if NULL or empty, use + * DEF_PGPORT[_STR]) */ char *password; /* password for this host, read from the * password file; NULL if not sought or not * found in password file. */ @@ -389,6 +390,8 @@ struct pg_conn char *dbName; /* database name */ char *replication; /* connect as the replication standby? */ char *pgservice; /* Postgres service, if any */ + char *pgservicefile; /* path to a service file containing + * service(s) */ char *pguser; /* Postgres username and password, if any */ char *pgpass; char *pgpassfile; /* path to a file containing password(s) */ @@ -441,6 +444,7 @@ struct pg_conn char *oauth_client_secret; /* client secret */ char *oauth_scope; /* access token scope */ char *oauth_token; /* access token */ + char *oauth_ca_file; /* CA file path */ bool oauth_want_retry; /* should we retry on failure? */ /* Optional file to write trace info to */ @@ -560,7 +564,16 @@ struct pg_conn pg_prng_state prng_state; /* prng state for load balancing connections */ - /* Buffer for data received from backend and not yet processed */ + /* + * Buffer for data received from backend and not yet processed. + * + * NB: We rely on a maximum inBufSize/outBufSize of INT_MAX (and therefore + * an INT_MAX upper bound on the size of any and all packet contents) to + * avoid overflow; for example in reportErrorPosition(). Changing the type + * would require not only an adjustment to the overflow protection in + * pqCheck{In,Out}BufferSpace(), but also a careful audit of all libpq + * code that uses ints during size calculations. + */ char *inBuffer; /* currently allocated buffer */ int inBufSize; /* allocated size of buffer */ int inStart; /* offset to first unconsumed data in buffer */ @@ -743,7 +756,7 @@ extern PGresult *pqPrepareAsyncResult(PGconn *conn); extern void pqInternalNotice(const PGNoticeHooks *hooks, const char *fmt,...) pg_attribute_printf(2, 3); extern void pqSaveMessageField(PGresult *res, char code, const char *value); -extern void pqSaveParameterStatus(PGconn *conn, const char *name, +extern int pqSaveParameterStatus(PGconn *conn, const char *name, const char *value); extern int pqRowProcessor(PGconn *conn, const char **errmsgp); extern void pqCommandQueueAdvance(PGconn *conn, bool isReadyForQuery, @@ -946,6 +959,7 @@ extern char *libpq_ngettext(const char *msgid, const char *msgid_plural, unsigne extern void libpq_append_error(PQExpBuffer errorMessage, const char *fmt,...) pg_attribute_printf(2, 3); extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern void libpq_append_grease_info(PGconn *conn); /* * These macros are needed to let error-handling code be portable between diff --git a/src/interfaces/libpq/libpq_check.pl b/src/interfaces/libpq/libpq_check.pl new file mode 100755 index 0000000000000..833f5315c3c45 --- /dev/null +++ b/src/interfaces/libpq/libpq_check.pl @@ -0,0 +1,92 @@ +#!/usr/bin/perl +# +# src/interfaces/libpq/libpq_check.pl +# +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# Check the state of a libpq library. Currently, this script checks that +# exit() is not called, because client libraries must not terminate the +# host application. +# +# This script is called by both Makefile and Meson. + +use strict; +use warnings FATAL => 'all'; + +use Getopt::Long; +use Config; + +my $nm_path; +my $input_file; +my $stamp_file; +my @problematic_lines; + +Getopt::Long::GetOptions( + 'nm:s' => \$nm_path, + 'input_file:s' => \$input_file, + 'stamp_file:s' => \$stamp_file) or die "$0: wrong arguments\n"; + +die "$0: --input_file must be specified\n" unless defined $input_file; +die "$0: --nm must be specified\n" unless defined $nm_path and -x $nm_path; + +sub create_stamp_file +{ + open my $fh, '>', $stamp_file + or die "can't open $stamp_file: $!"; + close $fh; +} + +# Skip on Windows and Solaris +if ( $Config{osname} =~ /MSWin32|cygwin|msys/i + || $Config{osname} =~ /solaris/i) +{ + exit 0; +} + +# Run nm to scan for symbols. If nm fails at runtime, skip the check. +open my $fh, '-|', "$nm_path -A -u $input_file 2>/dev/null" + or exit 0; + +while (<$fh>) +{ + # Set of symbols allowed. + + # The exclusion of __cxa_atexit is necessary on OpenBSD, which seems + # to insert references to that even in pure C code. + next if /__cxa_atexit/; + + # Excluding __tsan_func_exit is necessary when using ThreadSanitizer data + # race detector which uses this function for instrumentation of function + # exit. + next if /__tsan_func_exit/; + + # Excluding pthread_exit allows legitimate thread terminations in some + # builds. + next if /pthread_exit/; + + # Anything containing "exit" is suspicious. + # (Ideally we should reject abort() too, but there are various scenarios + # where build toolchains insert abort() calls, e.g. to implement + # assert().) + if (/exit/) + { + push @problematic_lines, $_; + } +} +close $fh; + +if (@problematic_lines) +{ + print "libpq must not be calling any function which invokes exit\n"; + print "Problematic symbol references:\n"; + print @problematic_lines; + + exit 1; +} +# Create stamp file, if required +if (defined($stamp_file)) +{ + create_stamp_file(); +} + +exit 0; diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index a74e885b169d7..b0ae72167a1ca 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group libpq_sources = files( 'fe-auth-oauth.c', @@ -55,8 +55,10 @@ libpq_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] # libpq_st, and {pgport,common}_shlib for libpq_sh # # We could try to avoid building the source files twice, but it probably adds -# more complexity than its worth (reusing object files requires also linking -# to the library on windows or breaks precompiled headers). +# more complexity than its worth (AIX doesn't support link_whole yet, reusing +# object files requires also linking to the library on windows or breaks +# precompiled headers). +if build_static_lib libpq_st = static_library('libpq', libpq_sources, include_directories: [libpq_inc], @@ -65,26 +67,49 @@ libpq_st = static_library('libpq', dependencies: [frontend_stlib_code, libpq_deps], kwargs: default_lib_args, ) +libpq_targets += libpq_st +endif +if build_shared_lib libpq_so = shared_library('libpq', libpq_sources + libpq_so_sources, include_directories: [libpq_inc, postgres_inc], c_args: libpq_c_args + libpq_so_c_args, c_pch: pch_postgres_fe_h, version: '5.' + pg_version_major.to_string(), - soversion: host_system != 'windows' ? '5' : '', + soversion: host_system not in ['aix', 'windows'] ? '5' : '', darwin_versions: ['5', '5.' + pg_version_major.to_string()], dependencies: [frontend_shlib_code, libpq_deps], link_depends: export_file, link_args: export_fmt.format(export_file.full_path()), kwargs: default_lib_args, ) +libpq_targets += libpq_so +endif libpq = declare_dependency( link_with: [libpq_so], include_directories: [include_directories('.')] ) +# Check for functions that libpq must not call. See libpq_check.pl for the +# full set of platform rules. Skip the test when profiling, as gcc may +# insert exit() calls for that. +if nm.found() and not get_option('b_coverage') + custom_target( + 'libpq_check', + input: libpq_so, + output: 'libpq-refs-stamp', + command: [ + perl, files('libpq_check.pl'), + '--input_file', '@INPUT@', + '--stamp_file', '@OUTPUT@', + '--nm', nm.full_path(), + ], + build_by_default: true, + ) +endif + private_deps = [ frontend_stlib_code, libpq_deps, diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index b87df277d930d..3fa87a0aaace1 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -1,6 +1,7 @@ # src/interfaces/libpq/nls.mk CATALOG_NAME = libpq GETTEXT_FILES = fe-auth.c \ + fe-auth-oauth.c \ fe-auth-scram.c \ fe-cancel.c \ fe-connect.c \ @@ -21,6 +22,7 @@ GETTEXT_TRIGGERS = actx_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_json_set_error:2 \ oauth_parse_set_error:2 \ pqInternalNotice:2 GETTEXT_FLAGS = actx_error:2:c-format \ @@ -29,5 +31,6 @@ GETTEXT_FLAGS = actx_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_json_set_error:2:c-format \ oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/interfaces/libpq/oauth-debug.h b/src/interfaces/libpq/oauth-debug.h new file mode 100644 index 0000000000000..4f0c87117e154 --- /dev/null +++ b/src/interfaces/libpq/oauth-debug.h @@ -0,0 +1,142 @@ +/*------------------------------------------------------------------------- + * + * oauth-debug.h + * Parsing logic for PGOAUTHDEBUG environment variable + * + * Both libpq and libpq-oauth need this logic, so it's packaged in a small + * header for convenience. This is not quite a standalone header, due to the + * complication introduced by libpq_gettext(); see note below. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/oauth-debug.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_DEBUG_H +#define OAUTH_DEBUG_H + +#include "postgres_fe.h" + +/* + * XXX libpq-oauth can't compile against libpq-int.h, so clients of this header + * need to provide the declaration of libpq_gettext() before #including it. + * Fortunately, there are only two such clients. + */ +/* #include "libpq-int.h" */ + +/* + * Debug flags for the PGOAUTHDEBUG environment variable. Each flag controls a + * specific debug feature. OAUTHDEBUG_UNSAFE_* flags require the envvar to have + * a literal "UNSAFE:" prefix. + */ + +/* allow HTTP (unencrypted) connections */ +#define OAUTHDEBUG_UNSAFE_HTTP (1<<0) +/* log HTTP traffic (exposes secrets) */ +#define OAUTHDEBUG_UNSAFE_TRACE (1<<1) +/* allow zero-second retry intervals */ +#define OAUTHDEBUG_UNSAFE_DOS_ENDPOINT (1<<2) + +/* mind the gap in values; see OAUTHDEBUG_UNSAFE_MASK below */ + +/* print PQconnectPoll statistics */ +#define OAUTHDEBUG_CALL_COUNT (1<<16) +/* print plugin loading errors */ +#define OAUTHDEBUG_PLUGIN_ERRORS (1<<17) + +/* all safe and unsafe flags, for the legacy UNSAFE behavior */ +#define OAUTHDEBUG_LEGACY_UNSAFE ((uint32) ~0) + +/* Flags are divided into "safe" and "unsafe" based on bit position. */ +#define OAUTHDEBUG_UNSAFE_MASK ((uint32) 0x0000FFFF) + +static_assert(OAUTHDEBUG_CALL_COUNT == OAUTHDEBUG_UNSAFE_MASK + 1, + "the first safe OAUTHDEBUG flag should be above OAUTHDEBUG_UNSAFE_MASK"); + +/* + * Parses the PGOAUTHDEBUG environment variable and returns debug flags. + * + * Supported formats: + * PGOAUTHDEBUG=UNSAFE - legacy format, enables all features + * PGOAUTHDEBUG=option1,option2 - enable safe features only + * PGOAUTHDEBUG=UNSAFE:opt1,opt2 - enable unsafe and/or safe features + * + * Prints a warning and skips the invalid option if: + * - An unrecognized option is specified + * - An unsafe option is specified without the UNSAFE: prefix + * + * XXX The parsing, and any warnings, will happen each time the function is + * called, so consider caching the result in cases where that might get + * annoying. But don't try to cache inside this function, unless you also have a + * plan for getting libpq and libpq-oauth to share that cache safely... probably + * not worth the effort for a debugging aid? + */ +static uint32 +oauth_parse_debug_flags(void) +{ + uint32 flags = 0; + const char *env = getenv("PGOAUTHDEBUG"); + char *options_str; + char *option; + char *saveptr = NULL; + bool unsafe_allowed = false; + + if (!env || env[0] == '\0') + return flags; + + if (strcmp(env, "UNSAFE") == 0) + return OAUTHDEBUG_LEGACY_UNSAFE; + + if (strncmp(env, "UNSAFE:", 7) == 0) + { + unsafe_allowed = true; + env += 7; + } + + options_str = strdup(env); + if (!options_str) + return flags; + + option = strtok_r(options_str, ",", &saveptr); + while (option != NULL) + { + uint32 flag = 0; + + if (strcmp(option, "http") == 0) + flag = OAUTHDEBUG_UNSAFE_HTTP; + else if (strcmp(option, "trace") == 0) + flag = OAUTHDEBUG_UNSAFE_TRACE; + else if (strcmp(option, "dos-endpoint") == 0) + flag = OAUTHDEBUG_UNSAFE_DOS_ENDPOINT; + else if (strcmp(option, "call-count") == 0) + flag = OAUTHDEBUG_CALL_COUNT; + else if (strcmp(option, "plugin-errors") == 0) + flag = OAUTHDEBUG_PLUGIN_ERRORS; + else + fprintf(stderr, + libpq_gettext("WARNING: unrecognized PGOAUTHDEBUG option \"%s\" (ignored)\n"), + option); + + if (!unsafe_allowed && ((flag & OAUTHDEBUG_UNSAFE_MASK) != 0)) + { + flag = 0; + + fprintf(stderr, + libpq_gettext("WARNING: PGOAUTHDEBUG option \"%s\" is unsafe (ignored)\n"), + option); + } + + flags |= flag; + option = strtok_r(NULL, ",", &saveptr); + } + + free(options_str); + + return flags; +} + +#endif /* OAUTH_DEBUG_H */ diff --git a/src/interfaces/libpq/po/meson.build b/src/interfaces/libpq/po/meson.build index 527379c5ce627..2f6daa863d410 100644 --- a/src/interfaces/libpq/po/meson.build +++ b/src/interfaces/libpq/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('libpq' + '5' + '-' + pg_version_major.to_string())] diff --git a/src/interfaces/libpq/pqexpbuffer.c b/src/interfaces/libpq/pqexpbuffer.c index 51ef4d591dad1..153ae6af6cb2c 100644 --- a/src/interfaces/libpq/pqexpbuffer.c +++ b/src/interfaces/libpq/pqexpbuffer.c @@ -15,7 +15,7 @@ * a usable vsnprintf(), then a copy of our own implementation of it will * be linked into libpq. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/pqexpbuffer.c diff --git a/src/interfaces/libpq/pqexpbuffer.h b/src/interfaces/libpq/pqexpbuffer.h index a0af7ad6adff1..dc83f4ffe86c7 100644 --- a/src/interfaces/libpq/pqexpbuffer.h +++ b/src/interfaces/libpq/pqexpbuffer.h @@ -15,7 +15,7 @@ * a usable vsnprintf(), then a copy of our own implementation of it will * be linked into libpq. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/interfaces/libpq/pqexpbuffer.h diff --git a/src/interfaces/libpq/pthread-win32.c b/src/interfaces/libpq/pthread-win32.c index db75d491b90d4..cf66284f007e7 100644 --- a/src/interfaces/libpq/pthread-win32.c +++ b/src/interfaces/libpq/pthread-win32.c @@ -3,7 +3,7 @@ * pthread-win32.c * partial pthread implementation for win32 * -* Copyright (c) 2004-2025, PostgreSQL Global Development Group +* Copyright (c) 2004-2026, PostgreSQL Global Development Group * IDENTIFICATION * src/interfaces/libpq/pthread-win32.c * diff --git a/src/interfaces/libpq/t/001_uri.pl b/src/interfaces/libpq/t/001_uri.pl index b0edcb3be8846..64f257ae0461e 100644 --- a/src/interfaces/libpq/t/001_uri.pl +++ b/src/interfaces/libpq/t/001_uri.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/interfaces/libpq/t/002_api.pl b/src/interfaces/libpq/t/002_api.pl index 3202c87ebe79d..409345836db90 100644 --- a/src/interfaces/libpq/t/002_api.pl +++ b/src/interfaces/libpq/t/002_api.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/interfaces/libpq/t/003_load_balance_host_list.pl b/src/interfaces/libpq/t/003_load_balance_host_list.pl index 6e859c49351ff..1f970ff994b51 100644 --- a/src/interfaces/libpq/t/003_load_balance_host_list.pl +++ b/src/interfaces/libpq/t/003_load_balance_host_list.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; use Config; @@ -61,10 +61,13 @@ my $total_occurrences = $node1_occurrences + $node2_occurrences + $node3_occurrences; -ok($node1_occurrences > 1, "received at least one connection on node1"); -ok($node2_occurrences > 1, "received at least one connection on node2"); -ok($node3_occurrences > 1, "received at least one connection on node3"); -ok($total_occurrences == 50, "received 50 connections across all nodes"); +cmp_ok($node1_occurrences, '>', 1, + "received at least one connection on node1"); +cmp_ok($node2_occurrences, '>', 1, + "received at least one connection on node2"); +cmp_ok($node3_occurrences, '>', 1, + "received at least one connection on node3"); +is($total_occurrences, 50, "received 50 connections across all nodes"); $node1->stop(); $node2->stop(); diff --git a/src/interfaces/libpq/t/004_load_balance_dns.pl b/src/interfaces/libpq/t/004_load_balance_dns.pl index 19a4f80fd7f14..e1ff9a0602480 100644 --- a/src/interfaces/libpq/t/004_load_balance_dns.pl +++ b/src/interfaces/libpq/t/004_load_balance_dns.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; use Config; @@ -111,10 +111,13 @@ my $total_occurrences = $node1_occurrences + $node2_occurrences + $node3_occurrences; -ok($node1_occurrences > 1, "received at least one connection on node1"); -ok($node2_occurrences > 1, "received at least one connection on node2"); -ok($node3_occurrences > 1, "received at least one connection on node3"); -ok($total_occurrences == 50, "received 50 connections across all nodes"); +cmp_ok($node1_occurrences, '>', 1, + "received at least one connection on node1"); +cmp_ok($node2_occurrences, '>', 1, + "received at least one connection on node2"); +cmp_ok($node3_occurrences, '>', 1, + "received at least one connection on node3"); +is($total_occurrences, 50, "received 50 connections across all nodes"); $node1->stop(); $node2->stop(); @@ -123,7 +126,7 @@ # working one. $node3->connect_ok( "host=pg-loadbalancetest port=$port load_balance_hosts=disable", - "load_balance_hosts=disable continues until it connects to the a working node", + "load_balance_hosts=disable continues until it connects to a working node", sql => "SELECT 'connect3'", log_like => [qr/statement: SELECT 'connect3'/]); @@ -133,7 +136,7 @@ { $node3->connect_ok( "host=pg-loadbalancetest port=$port load_balance_hosts=random", - "load_balance_hosts=random continues until it connects to the a working node", + "load_balance_hosts=random continues until it connects to a working node", sql => "SELECT 'connect4'", log_like => [qr/statement: SELECT 'connect4'/]); } diff --git a/src/interfaces/libpq/t/005_negotiate_encryption.pl b/src/interfaces/libpq/t/005_negotiate_encryption.pl index ac6d8bcb4a648..18c100fb117f1 100644 --- a/src/interfaces/libpq/t/005_negotiate_encryption.pl +++ b/src/interfaces/libpq/t/005_negotiate_encryption.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # OVERVIEW # -------- diff --git a/src/interfaces/libpq/t/006_service.pl b/src/interfaces/libpq/t/006_service.pl index 4fe5adc5c2acd..9e92e7b0203ac 100644 --- a/src/interfaces/libpq/t/006_service.pl +++ b/src/interfaces/libpq/t/006_service.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; use File::Copy; @@ -22,18 +22,14 @@ my $td = PostgreSQL::Test::Utils::tempdir; -# Windows vs non-Windows: CRLF vs LF for the file's newline, relying on -# the fact that libpq uses fgets() when reading the lines of a service file. -my $newline = $windows_os ? "\r\n" : "\n"; - # Create the set of service files used in the tests. # File that includes a valid service name, and uses a decomposed connection # string for its contents, split on spaces. my $srvfile_valid = "$td/pg_service_valid.conf"; -append_to_file($srvfile_valid, "[my_srv]" . $newline); +append_to_file($srvfile_valid, "[my_srv]\n"); foreach my $param (split(/\s+/, $node->connstr)) { - append_to_file($srvfile_valid, $param . $newline); + append_to_file($srvfile_valid, $param . "\n"); } # File defined with no contents, used as default value for PGSERVICEFILE, @@ -47,6 +43,19 @@ # Missing service file. my $srvfile_missing = "$td/pg_service_missing.conf"; +# Service file with nested "service" defined. +my $srvfile_nested = "$td/pg_service_nested.conf"; +copy($srvfile_valid, $srvfile_nested) + or die "Could not copy $srvfile_valid to $srvfile_nested: $!"; +append_to_file($srvfile_nested, "service=invalid_srv\n"); + +# Service file with nested "servicefile" defined. +my $srvfile_nested_2 = "$td/pg_service_nested_2.conf"; +copy($srvfile_valid, $srvfile_nested_2) + or die "Could not copy $srvfile_valid to $srvfile_nested_2: $!"; +append_to_file($srvfile_nested_2, + 'servicefile=' . $srvfile_default . "\n"); + # Set the fallback directory lookup of the service file to the temporary # directory of this test. PGSYSCONFDIR is used if the service file # defined in PGSERVICEFILE cannot be found, or when a service file is @@ -146,6 +155,85 @@ unlink($srvfile_default); } +# Checks nested service file contents. +{ + local $ENV{PGSERVICEFILE} = $srvfile_nested; + + $dummy_node->connect_fails( + 'service=my_srv', + 'connection with "service" in nested service file', + expected_stderr => + qr/nested "service" specifications not supported in service file/); + + local $ENV{PGSERVICEFILE} = $srvfile_nested_2; + + $dummy_node->connect_fails( + 'service=my_srv', + 'connection with "servicefile" in nested service file', + expected_stderr => + qr/nested "servicefile" specifications not supported in service file/ + ); +} + +# Properly escape backslashes in the path, to ensure the generation of +# correct connection strings. +my $srvfile_win_cared = $srvfile_valid; +$srvfile_win_cared =~ s/\\/\\\\/g; + +# Checks that the "servicefile" option works as expected +{ + $dummy_node->connect_ok( + q{service=my_srv servicefile='} . $srvfile_win_cared . q{'}, + 'connection with valid servicefile in connection string', + sql => "SELECT 'connect3_1'", + expected_stdout => qr/connect3_1/); + + # Encode slashes and backslash + my $encoded_srvfile = $srvfile_valid =~ s{([\\/])}{ + $1 eq '/' ? '%2F' : '%5C' + }ger; + + # Additionally encode a colon in servicefile path of Windows + $encoded_srvfile =~ s/:/%3A/g; + + $dummy_node->connect_ok( + 'postgresql:///?service=my_srv&servicefile=' . $encoded_srvfile, + 'connection with valid servicefile in URI', + sql => "SELECT 'connect3_2'", + expected_stdout => qr/connect3_2/); + + local $ENV{PGSERVICE} = 'my_srv'; + $dummy_node->connect_ok( + q{servicefile='} . $srvfile_win_cared . q{'}, + 'connection with PGSERVICE and servicefile in connection string', + sql => "SELECT 'connect3_3'", + expected_stdout => qr/connect3_3/); + + $dummy_node->connect_ok( + 'postgresql://?servicefile=' . $encoded_srvfile, + 'connection with PGSERVICE and servicefile in URI', + sql => "SELECT 'connect3_4'", + expected_stdout => qr/connect3_4/); +} + +# Check that the "servicefile" option takes priority over the PGSERVICEFILE +# environment variable. +{ + local $ENV{PGSERVICEFILE} = 'non-existent-file.conf'; + + $dummy_node->connect_fails( + 'service=my_srv', + 'connection with invalid PGSERVICEFILE', + expected_stderr => + qr/service file "non-existent-file\.conf" not found/); + + $dummy_node->connect_ok( + q{service=my_srv servicefile='} . $srvfile_win_cared . q{'}, + 'connection with both servicefile and PGSERVICEFILE', + sql => "SELECT 'connect4_1'", + expected_stdout => qr/connect4_1/); +} + $node->teardown_node; done_testing(); diff --git a/src/interfaces/libpq/test/libpq_testclient.c b/src/interfaces/libpq/test/libpq_testclient.c index 44aff6c0f911b..20730709ee769 100644 --- a/src/interfaces/libpq/test/libpq_testclient.c +++ b/src/interfaces/libpq/test/libpq_testclient.c @@ -2,7 +2,7 @@ * libpq_testclient.c * A test program for the libpq public API * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/interfaces/libpq/test/libpq_testclient.c @@ -13,7 +13,7 @@ #include "libpq-fe.h" static void -print_ssl_library() +print_ssl_library(void) { const char *lib = PQsslAttribute(NULL, "library"); diff --git a/src/interfaces/libpq/test/libpq_uri_regress.c b/src/interfaces/libpq/test/libpq_uri_regress.c index bb12cf65537d4..c26e0f79e5e71 100644 --- a/src/interfaces/libpq/test/libpq_uri_regress.c +++ b/src/interfaces/libpq/test/libpq_uri_regress.c @@ -7,7 +7,7 @@ * prints out the values from the parsed PQconninfoOption struct that differ * from the defaults (obtained from PQconndefaults). * - * Portions Copyright (c) 2012-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2012-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/interfaces/libpq/test/libpq_uri_regress.c diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build index 07a5facc321c7..e203486615c40 100644 --- a/src/interfaces/libpq/test/meson.build +++ b/src/interfaces/libpq/test/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group libpq_test_deps = [] diff --git a/src/interfaces/libpq/win32.c b/src/interfaces/libpq/win32.c index 0adae19c1f567..b0c558b55a5a8 100644 --- a/src/interfaces/libpq/win32.c +++ b/src/interfaces/libpq/win32.c @@ -15,7 +15,7 @@ * The error constants are taken from the Frambak Bakfram LGSOCKET * library guys who in turn took them from the Winsock FAQ. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * */ diff --git a/src/interfaces/meson.build b/src/interfaces/meson.build index 50623b31b6907..5adfc5cffe40c 100644 --- a/src/interfaces/meson.build +++ b/src/interfaces/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # NB: libpq is entered directly from the toplevel meson file subdir('ecpg') diff --git a/src/makefiles/Makefile.aix b/src/makefiles/Makefile.aix new file mode 100644 index 0000000000000..ecba693b6a979 --- /dev/null +++ b/src/makefiles/Makefile.aix @@ -0,0 +1,42 @@ +# MAKE_EXPORTS is required for svr4 loaders that want a file of +# symbol names to tell them what to export/import. +MAKE_EXPORTS= true + +# -blibpath must contain ALL directories where we should look for libraries +libpath := $(shell echo $(subst -L,:,$(filter -L/%,$(LDFLAGS))) | sed -e's/ //g'):/opt/freeware/lib:/usr/lib:/lib + +# when building with gcc, need to make sure that libgcc can be found +ifeq ($(GCC), yes) +libpath := $(libpath):$(dir $(shell gcc -print-libgcc-file-name)) +endif + +rpath = -Wl,-blibpath:'$(rpathdir)$(libpath)' + +LDFLAGS_SL += -Wl,-bnoentry -Wl,-H512 -Wl,-bM:SRE + +# gcc needs to know it's building a shared lib, otherwise it'll not emit +# correct code / link to the right support libraries +ifeq ($(GCC), yes) +LDFLAGS_SL += -shared +endif + +# instruct ar to process 64-bit objects +AROPT := -X64 $(AROPT) + +# env var name to use in place of LD_LIBRARY_PATH +ld_library_path_var = LIBPATH + + +POSTGRES_IMP= postgres.imp + +ifdef PGXS +BE_DLLLIBS= -Wl,-bI:$(pkglibdir)/$(POSTGRES_IMP) +else +BE_DLLLIBS= -Wl,-bI:$(top_builddir)/src/backend/$(POSTGRES_IMP) +endif + +MKLDEXPORT_DIR=src/backend/port/aix +MKLDEXPORT=$(top_srcdir)/$(MKLDEXPORT_DIR)/mkldexport.sh + +%$(DLSUFFIX): %.o + $(CC) $(CFLAGS) $*.o $(LDFLAGS) $(LDFLAGS_SL) -o $@ $(BE_DLLLIBS) diff --git a/src/makefiles/Makefile.cygwin b/src/makefiles/Makefile.cygwin index 7759397263821..5ca6f872ae4a8 100644 --- a/src/makefiles/Makefile.cygwin +++ b/src/makefiles/Makefile.cygwin @@ -16,6 +16,7 @@ ifneq (,$(findstring backend,$(subdir))) ifeq (,$(findstring conversion_procs,$(subdir))) ifeq (,$(findstring libpqwalreceiver,$(subdir))) ifeq (,$(findstring replication/pgoutput,$(subdir))) +ifeq (,$(findstring replication/pgrepack,$(subdir))) ifeq (,$(findstring snowball,$(subdir))) override CPPFLAGS+= -DBUILDING_DLL endif @@ -23,6 +24,7 @@ endif endif endif endif +endif ifneq (,$(findstring src/common,$(subdir))) override CPPFLAGS+= -DBUILDING_DLL diff --git a/src/makefiles/Makefile.win32 b/src/makefiles/Makefile.win32 index dc1aafa115ae0..fc12b845788a2 100644 --- a/src/makefiles/Makefile.win32 +++ b/src/makefiles/Makefile.win32 @@ -14,6 +14,7 @@ ifneq (,$(findstring backend,$(subdir))) ifeq (,$(findstring conversion_procs,$(subdir))) ifeq (,$(findstring libpqwalreceiver,$(subdir))) ifeq (,$(findstring replication/pgoutput,$(subdir))) +ifeq (,$(findstring replication/pgrepack,$(subdir))) ifeq (,$(findstring snowball,$(subdir))) override CPPFLAGS+= -DBUILDING_DLL endif @@ -21,6 +22,7 @@ endif endif endif endif +endif ifneq (,$(findstring src/common,$(subdir))) override CPPFLAGS+= -DBUILDING_DLL diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 91a8de1ee9b9d..2401025d1cd6f 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group ### Compute pgxs_data, used in src/meson.build to generate Makefile.global ### etc, that's complete enough for PGXS to work. @@ -6,7 +6,7 @@ # Emulation of PGAC_CHECK_STRIP strip_bin = find_program(get_option('STRIP'), required: false, native: true) -strip_cmd = strip_bin.found() ? [strip_bin.path()] : [':'] +strip_cmd = strip_bin.found() ? [strip_bin.full_path()] : [':'] working_strip = false if strip_bin.found() @@ -49,8 +49,8 @@ pgxs_kv = { 'PORTNAME': portname, 'PG_SYSROOT': pg_sysroot, - 'abs_top_builddir': meson.build_root(), - 'abs_top_srcdir': meson.source_root(), + 'abs_top_builddir': meson.project_build_root(), + 'abs_top_srcdir': meson.project_source_root(), 'enable_rpath': get_option('rpath') ? 'yes' : 'no', 'enable_nls': libintl.found() ? 'yes' : 'no', @@ -63,8 +63,6 @@ pgxs_kv = { 'DLSUFFIX': dlsuffix, 'EXEEXT': exesuffix, - 'SUN_STUDIO_CC': 'no', # not supported so far - # want the chosen option, rather than the library 'with_ssl' : ssl_library, 'with_uuid': uuidopt, @@ -79,6 +77,9 @@ pgxs_kv = { 'STRIP_STATIC_LIB': ' '.join(strip_static_cmd), 'STRIP_SHARED_LIB': ' '.join(strip_shared_cmd), + 'perl_includespec': perl_includespec, + 'python_includespec': python_includespec, + # these seem to be standard these days 'MKDIR_P': 'mkdir -p', 'LN_S': 'ln -s', @@ -88,6 +89,7 @@ pgxs_kv = { 'CC': var_cc, 'CPP': var_cpp, + 'CXX': var_cxx, 'GCC': cc.get_argument_syntax() == 'gcc' ? 'yes' : 'no', 'CPPFLAGS': var_cppflags, @@ -119,18 +121,18 @@ pgxs_kv = { 'FLEXFLAGS': ' '.join(flex_flags), 'LIBS': var_libs, + + 'have_cxx': have_cxx ? 'yes' : 'no', } if llvm.found() pgxs_kv += { - 'CLANG': clang.path(), - 'CXX': ' '.join(cpp.cmd_array()), + 'CLANG': clang.full_path(), 'LLVM_BINPATH': llvm_binpath, } else pgxs_kv += { 'CLANG': '', - 'CXX': '', 'LLVM_BINPATH': '', } endif @@ -156,9 +158,10 @@ pgxs_bins = { pgxs_empty = [ 'ICU_CFLAGS', # needs to be added, included by public server headers - # hard to see why we'd need either? + # hard to see why we'd need these ones? 'ZIC', 'TCLSH', + 'NM', # docs don't seem to be supported by pgxs 'XMLLINT', @@ -179,12 +182,12 @@ pgxs_empty = [ 'WANTED_LANGUAGES', # Not needed because we don't build the server / PLs with the generated makefile - 'LIBOBJS', 'PG_CRC32C_OBJS', 'TAS', + 'LIBOBJS', 'PG_CRC32C_OBJS', 'PG_TEST_EXTRA', 'DTRACEFLAGS', # only server has dtrace probes - 'perl_archlibexp', 'perl_embed_ccflags', 'perl_embed_ldflags', 'perl_includespec', 'perl_privlibexp', - 'python_additional_libs', 'python_includespec', 'python_libdir', 'python_libspec', 'python_majorversion', 'python_version', + 'perl_archlibexp', 'perl_embed_ccflags', 'perl_embed_ldflags', 'perl_privlibexp', + 'python_additional_libs', 'python_libdir', 'python_libspec', 'python_majorversion', 'python_version', # possible that some of these are referenced explicitly in pgxs makefiles? # For now not worth it. @@ -258,7 +261,7 @@ pgxs_deps = { pgxs_cdata = configuration_data(pgxs_kv) foreach b, p : pgxs_bins - pgxs_cdata.set(b, p.found() ? p.path() : '') + pgxs_cdata.set(b, p.found() ? p.full_path() : '') endforeach foreach pe : pgxs_empty diff --git a/src/makefiles/pgxs.mk b/src/makefiles/pgxs.mk index 0de3737e789b4..039cee3dfe5d9 100644 --- a/src/makefiles/pgxs.mk +++ b/src/makefiles/pgxs.mk @@ -376,10 +376,7 @@ endif ifdef REGRESS # things created by various check targets rm -rf $(pg_regress_clean_files) -ifeq ($(PORTNAME), win) - rm -f regress.def endif -endif # REGRESS ifdef TAP_TESTS rm -rf tmp_check/ endif diff --git a/src/meson.build b/src/meson.build index f098f9f164e5b..b57d4a5c25912 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # libraries that other subsystems might depend upon first, in their respective # dependency order diff --git a/src/nls-global.mk b/src/nls-global.mk index dfff472cb3f48..73a6db10a1da2 100644 --- a/src/nls-global.mk +++ b/src/nls-global.mk @@ -142,8 +142,13 @@ init-po: po/$(CATALOG_NAME).pot # For performance reasons, only calculate these when the user actually # requested update-po or a specific file. ifneq (,$(filter update-po %.po.new,$(MAKECMDGOALS))) +ifdef PGXS +ALL_LANGUAGES := $(shell find . -name '*.po' -print | sed 's,^.*/\([^/]*\).po$$,\1,' | LC_ALL=C sort -u) +all_compendia := $(shell find . -name '*.po' -print | LC_ALL=C sort) +else ALL_LANGUAGES := $(shell find $(top_srcdir) -name '*.po' -print | sed 's,^.*/\([^/]*\).po$$,\1,' | LC_ALL=C sort -u) all_compendia := $(shell find $(top_srcdir) -name '*.po' -print | LC_ALL=C sort) +endif else ALL_LANGUAGES = $(AVAIL_LANGUAGES) all_compendia = FORCE diff --git a/src/pl/meson.build b/src/pl/meson.build index f8ff0671b4381..866a706e8491c 100644 --- a/src/pl/meson.build +++ b/src/pl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('plpgsql') diff --git a/src/pl/plperl/GNUmakefile b/src/pl/plperl/GNUmakefile index 558c764aadb2a..d7c8917f822e6 100644 --- a/src/pl/plperl/GNUmakefile +++ b/src/pl/plperl/GNUmakefile @@ -62,7 +62,7 @@ endif REGRESS_OPTS = --dbname=$(PL_TESTDB) --dlpath=$(top_builddir)/src/test/regress REGRESS = plperl_setup plperl plperl_lc plperl_trigger plperl_shared \ - plperl_elog plperl_util plperl_init plperlu plperl_array \ + plperl_elog plperl_unicode plperl_util plperl_init plperlu plperl_array \ plperl_call plperl_transaction plperl_env # if Perl can support two interpreters in one backend, # test plperl-and-plperlu cases diff --git a/src/pl/plperl/expected/plperl_elog.out b/src/pl/plperl/expected/plperl_elog.out index a6d35cb79c4f9..042719d3a6ddb 100644 --- a/src/pl/plperl/expected/plperl_elog.out +++ b/src/pl/plperl/expected/plperl_elog.out @@ -41,7 +41,7 @@ select uses_global(); ERROR: function uses_global() does not exist LINE 1: select uses_global(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no function of that name. SET plperl.use_strict = false; create or replace function uses_global() returns text language plperl as $$ @@ -97,16 +97,3 @@ NOTICE: caught die 2 (1 row) --- Test non-ASCII error messages --- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. -SET client_encoding TO UTF8; -create or replace function error_with_nbsp() returns void language plperl as $$ - elog(ERROR, "this message contains a no-break space"); -$$; -select error_with_nbsp(); -ERROR: this message contains a no-break space at line 2. -CONTEXT: PL/Perl function "error_with_nbsp" diff --git a/src/pl/plperl/expected/plperl_elog_1.out b/src/pl/plperl/expected/plperl_elog_1.out index 85aa460ec4c2c..42d411146b2d4 100644 --- a/src/pl/plperl/expected/plperl_elog_1.out +++ b/src/pl/plperl/expected/plperl_elog_1.out @@ -41,7 +41,7 @@ select uses_global(); ERROR: function uses_global() does not exist LINE 1: select uses_global(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no function of that name. SET plperl.use_strict = false; create or replace function uses_global() returns text language plperl as $$ @@ -97,16 +97,3 @@ NOTICE: caught die 2 (1 row) --- Test non-ASCII error messages --- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. -SET client_encoding TO UTF8; -create or replace function error_with_nbsp() returns void language plperl as $$ - elog(ERROR, "this message contains a no-break space"); -$$; -select error_with_nbsp(); -ERROR: this message contains a no-break space at line 2. -CONTEXT: PL/Perl function "error_with_nbsp" diff --git a/src/pl/plperl/expected/plperl_unicode.out b/src/pl/plperl/expected/plperl_unicode.out new file mode 100644 index 0000000000000..3c48f2e961139 --- /dev/null +++ b/src/pl/plperl/expected/plperl_unicode.out @@ -0,0 +1,18 @@ +-- Test non-ASCII error messages +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif +SET client_encoding TO UTF8; +create or replace function error_with_nbsp() returns void language plperl as $$ + elog(ERROR, "this message contains a no-break space"); +$$; +select error_with_nbsp(); +ERROR: this message contains a no-break space at line 2. +CONTEXT: PL/Perl function "error_with_nbsp" diff --git a/src/pl/plperl/expected/plperl_unicode_1.out b/src/pl/plperl/expected/plperl_unicode_1.out new file mode 100644 index 0000000000000..761de04b1ee57 --- /dev/null +++ b/src/pl/plperl/expected/plperl_unicode_1.out @@ -0,0 +1,10 @@ +-- Test non-ASCII error messages +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/pl/plperl/meson.build b/src/pl/plperl/meson.build index b463d4d56c5b3..ff41812ca46f0 100644 --- a/src/pl/plperl/meson.build +++ b/src/pl/plperl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not perl_dep.found() subdir_done() @@ -88,6 +88,7 @@ tests += { 'plperl_trigger', 'plperl_shared', 'plperl_elog', + 'plperl_unicode', 'plperl_util', 'plperl_init', 'plperlu', @@ -96,7 +97,7 @@ tests += { 'plperl_transaction', 'plperl_env', ], - 'regress_args': ['--dlpath', meson.build_root() / 'src/test/regress'], + 'regress_args': ['--dlpath', meson.project_build_root() / 'src/test/regress'], }, } diff --git a/src/pl/plperl/plc_perlboot.pl b/src/pl/plperl/plc_perlboot.pl index 28a1a4cd6f2b9..8f79de67a73aa 100644 --- a/src/pl/plperl/plc_perlboot.pl +++ b/src/pl/plperl/plc_perlboot.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # src/pl/plperl/plc_perlboot.pl diff --git a/src/pl/plperl/plc_trusted.pl b/src/pl/plperl/plc_trusted.pl index 1fc94259a0b11..a6a9939391d27 100644 --- a/src/pl/plperl/plc_trusted.pl +++ b/src/pl/plperl/plc_trusted.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # src/pl/plperl/plc_trusted.pl diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c index 29cb4d7e47f80..c1f9b8932a361 100644 --- a/src/pl/plperl/plperl.c +++ b/src/pl/plperl/plperl.c @@ -35,6 +35,7 @@ #include "utils/memutils.h" #include "utils/rel.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/typcache.h" /* define our text domain for translations */ @@ -247,7 +248,7 @@ static plperl_call_data *current_call_data = NULL; **********************************************************************/ static PerlInterpreter *plperl_init_interp(void); -static void plperl_destroy_interp(PerlInterpreter **); +static void plperl_destroy_interp(PerlInterpreter **interp); static void plperl_fini(int code, Datum arg); static void set_interp_require(bool trusted); @@ -283,12 +284,14 @@ static Datum plperl_hash_to_datum(SV *src, TupleDesc td); static void plperl_init_shared_libs(pTHX); static void plperl_trusted_init(void); static void plperl_untrusted_init(void); -static HV *plperl_spi_execute_fetch_result(SPITupleTable *, uint64, int); +static HV *plperl_spi_execute_fetch_result(SPITupleTable *tuptable, + uint64 processed, int status); static void plperl_return_next_internal(SV *sv); static char *hek2cstr(HE *he); static SV **hv_store_string(HV *hv, const char *key, SV *val); static SV **hv_fetch_string(HV *hv, const char *key); -static void plperl_create_sub(plperl_proc_desc *desc, const char *s, Oid fn_oid); +static void plperl_create_sub(plperl_proc_desc *prodesc, const char *s, + Oid fn_oid); static SV *plperl_call_perl_func(plperl_proc_desc *desc, FunctionCallInfo fcinfo); static void plperl_compile_callback(void *arg); @@ -1082,8 +1085,8 @@ plperl_build_tuple_result(HV *perlhash, TupleDesc td) HE *he; HeapTuple tup; - values = palloc0(sizeof(Datum) * td->natts); - nulls = palloc(sizeof(bool) * td->natts); + values = palloc0_array(Datum, td->natts); + nulls = palloc_array(bool, td->natts); memset(nulls, true, sizeof(bool) * td->natts); hv_iterinit(perlhash); @@ -1092,7 +1095,7 @@ plperl_build_tuple_result(HV *perlhash, TupleDesc td) SV *val = HeVAL(he); char *key = hek2cstr(he); int attn = SPI_fnumber(td, key); - Form_pg_attribute attr = TupleDescAttr(td, attn - 1); + Form_pg_attribute attr; if (attn == SPI_ERROR_NOATTRIBUTE) ereport(ERROR, @@ -1105,6 +1108,7 @@ plperl_build_tuple_result(HV *perlhash, TupleDesc td) errmsg("cannot set system attribute \"%s\"", key))); + attr = TupleDescAttr(td, attn - 1); values[attn - 1] = plperl_sv_to_datum(val, attr->atttypid, attr->atttypmod, @@ -1453,7 +1457,7 @@ plperl_sv_to_literal(SV *sv, char *fqtypename) check_spi_usage_allowed(); - typid = DirectFunctionCall1(regtypein, CStringGetDatum(fqtypename)); + typid = DatumGetObjectId(DirectFunctionCall1(regtypein, CStringGetDatum(fqtypename))); if (!OidIsValid(typid)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), @@ -1502,7 +1506,7 @@ plperl_ref_from_pg_array(Datum arg, Oid typid) * Currently we make no effort to cache any of the stuff we look up here, * which is bad. */ - info = palloc0(sizeof(plperl_array_info)); + info = palloc0_object(plperl_array_info); /* get element type information, including output conversion function */ get_type_io_data(elementtype, IOFunc_output, @@ -1538,7 +1542,7 @@ plperl_ref_from_pg_array(Datum arg, Oid typid) &nitems); /* Get total number of elements in each dimension */ - info->nelems = palloc(sizeof(int) * info->ndims); + info->nelems = palloc_array(int, info->ndims); info->nelems[0] = nitems; for (i = 1; i < info->ndims; i++) info->nelems[i] = info->nelems[i - 1] / dims[i - 1]; @@ -1798,7 +1802,7 @@ plperl_modify_tuple(HV *hvTD, TriggerData *tdata, HeapTuple otup) char *key = hek2cstr(he); SV *val = HeVAL(he); int attn = SPI_fnumber(tupdesc, key); - Form_pg_attribute attr = TupleDescAttr(tupdesc, attn - 1); + Form_pg_attribute attr; if (attn == SPI_ERROR_NOATTRIBUTE) ereport(ERROR, @@ -1810,6 +1814,8 @@ plperl_modify_tuple(HV *hvTD, TriggerData *tdata, HeapTuple otup) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot set system attribute \"%s\"", key))); + + attr = TupleDescAttr(tupdesc, attn - 1); if (attr->attgenerated) ereport(ERROR, (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), @@ -2569,13 +2575,13 @@ plperl_trigger_handler(PG_FUNCTION_ARGS) TriggerData *trigdata = ((TriggerData *) fcinfo->context); if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event)) - retval = (Datum) trigdata->tg_trigtuple; + retval = PointerGetDatum(trigdata->tg_trigtuple); else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event)) - retval = (Datum) trigdata->tg_newtuple; + retval = PointerGetDatum(trigdata->tg_newtuple); else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) - retval = (Datum) trigdata->tg_trigtuple; + retval = PointerGetDatum(trigdata->tg_trigtuple); else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event)) - retval = (Datum) trigdata->tg_trigtuple; + retval = PointerGetDatum(trigdata->tg_trigtuple); else retval = (Datum) 0; /* can this happen? */ } @@ -2797,7 +2803,7 @@ compile_plperl_function(Oid fn_oid, bool is_trigger, bool is_event_trigger) * struct prodesc and subsidiary data must all live in proc_cxt. ************************************************************/ oldcontext = MemoryContextSwitchTo(proc_cxt); - prodesc = (plperl_proc_desc *) palloc0(sizeof(plperl_proc_desc)); + prodesc = palloc0_object(plperl_proc_desc); prodesc->proname = pstrdup(NameStr(procStruct->proname)); MemoryContextSetIdentifier(proc_cxt, prodesc->proname); prodesc->fn_cxt = proc_cxt; @@ -3596,7 +3602,7 @@ plperl_spi_prepare(char *query, int argc, SV **argv) "PL/Perl spi_prepare query", ALLOCSET_SMALL_SIZES); MemoryContextSwitchTo(plan_cxt); - qdesc = (plperl_query_desc *) palloc0(sizeof(plperl_query_desc)); + qdesc = palloc0_object(plperl_query_desc); snprintf(qdesc->qname, sizeof(qdesc->qname), "%p", qdesc); qdesc->plan_cxt = plan_cxt; qdesc->nargs = argc; diff --git a/src/pl/plperl/plperl.h b/src/pl/plperl/plperl.h index 5c4c3ac577074..4c03f9e0df6a6 100644 --- a/src/pl/plperl/plperl.h +++ b/src/pl/plperl/plperl.h @@ -6,7 +6,7 @@ * This should be included _AFTER_ postgres.h and system include files, as * well as headers that could in turn include system headers. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1995, Regents of the University of California * * src/pl/plperl/plperl.h diff --git a/src/pl/plperl/plperl_opmask.pl b/src/pl/plperl/plperl_opmask.pl index c64c7041a5a55..3d0ddfc0f7b30 100644 --- a/src/pl/plperl/plperl_opmask.pl +++ b/src/pl/plperl/plperl_opmask.pl @@ -1,6 +1,6 @@ #!perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/pl/plperl/plperl_system.h b/src/pl/plperl/plperl_system.h index 4c09a5cc1592b..62b3b47dfdd00 100644 --- a/src/pl/plperl/plperl_system.h +++ b/src/pl/plperl/plperl_system.h @@ -8,7 +8,7 @@ * declarations should be put here. However, we do include some stuff * that is meant to prevent conflicts between our code and Perl. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1995, Regents of the University of California * * src/pl/plperl/plperl_system.h diff --git a/src/pl/plperl/po/meson.build b/src/pl/plperl/po/meson.build index d0c3f62b780a5..424a8fccf0cb5 100644 --- a/src/pl/plperl/po/meson.build +++ b/src/pl/plperl/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('plperl-' + pg_version_major.to_string())] diff --git a/src/pl/plperl/sql/plperl_elog.sql b/src/pl/plperl/sql/plperl_elog.sql index 9ea1350069b86..032fd8b8ba74a 100644 --- a/src/pl/plperl/sql/plperl_elog.sql +++ b/src/pl/plperl/sql/plperl_elog.sql @@ -76,18 +76,3 @@ return $a + $b; $$; select indirect_die_caller(); - --- Test non-ASCII error messages --- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. - -SET client_encoding TO UTF8; - -create or replace function error_with_nbsp() returns void language plperl as $$ - elog(ERROR, "this message contains a no-break space"); -$$; - -select error_with_nbsp(); diff --git a/src/pl/plperl/sql/plperl_unicode.sql b/src/pl/plperl/sql/plperl_unicode.sql new file mode 100644 index 0000000000000..7e1ad745cdd51 --- /dev/null +++ b/src/pl/plperl/sql/plperl_unicode.sql @@ -0,0 +1,19 @@ +-- Test non-ASCII error messages +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif + +SET client_encoding TO UTF8; + +create or replace function error_with_nbsp() returns void language plperl as $$ + elog(ERROR, "this message contains a no-break space"); +$$; + +select error_with_nbsp(); diff --git a/src/pl/plperl/text2macro.pl b/src/pl/plperl/text2macro.pl index 3c9c70ae223c8..0a91f1ae02890 100644 --- a/src/pl/plperl/text2macro.pl +++ b/src/pl/plperl/text2macro.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # src/pl/plperl/text2macro.pl diff --git a/src/pl/plpgsql/meson.build b/src/pl/plpgsql/meson.build index db9a73fd77175..088297b0cd502 100644 --- a/src/pl/plpgsql/meson.build +++ b/src/pl/plpgsql/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('src') diff --git a/src/pl/plpgsql/src/expected/plpgsql_call.out b/src/pl/plpgsql/src/expected/plpgsql_call.out index ea7107dca0d90..3d0b117f236b8 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_call.out +++ b/src/pl/plpgsql/src/expected/plpgsql_call.out @@ -440,7 +440,8 @@ $$; ERROR: procedure test_proc12(integer, integer, text[]) does not exist LINE 1: CALL test_proc12(_a, _b, _c) ^ -HINT: No procedure matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No procedure of that name accepts the given argument types. +HINT: You might need to add explicit type casts. QUERY: CALL test_proc12(_a, _b, _c) CONTEXT: PL/pgSQL function inline_code_block line 5 at CALL -- transition variable assignment diff --git a/src/pl/plpgsql/src/expected/plpgsql_domain.out b/src/pl/plpgsql/src/expected/plpgsql_domain.out index 516c2b9e08ee1..11c012ea0242f 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_domain.out +++ b/src/pl/plpgsql/src/expected/plpgsql_domain.out @@ -395,3 +395,16 @@ SELECT * FROM test_assign_ordered_named_pairs(1,2,0); -- should fail someday {"(1,2)"} (1 row) +CREATE FUNCTION test_null_ordered_named_pair() + RETURNS ordered_named_pair AS $$ +declare v ordered_named_pair; +begin +return v; +end +$$ LANGUAGE plpgsql; +SELECT * FROM test_null_ordered_named_pair(); + i | j +---+--- + | +(1 row) + diff --git a/src/pl/plpgsql/src/expected/plpgsql_misc.out b/src/pl/plpgsql/src/expected/plpgsql_misc.out index a6511df08ec9f..ffb377f5f54ff 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_misc.out +++ b/src/pl/plpgsql/src/expected/plpgsql_misc.out @@ -65,3 +65,39 @@ do $$ declare x public.foo%rowtype; begin end $$; ERROR: relation "public.foo" does not exist CONTEXT: compilation of PL/pgSQL function "inline_code_block" near line 1 do $$ declare x public.misc_table%rowtype; begin end $$; +-- Test handling of an unreserved keyword as a variable name +-- and record field name. +do $$ +declare + execute int; + r record; +begin + execute := 10; + raise notice 'execute = %', execute; + select 1 as strict into r; + raise notice 'r.strict = %', r.strict; +end $$; +NOTICE: execute = 10 +NOTICE: r.strict = 1 +-- Test handling of a reserved keyword as a record field name. +do $$ declare r record; +begin + select 1 as x, 2 as foreach into r; + raise notice 'r.x = %', r.x; + raise notice 'r.foreach = %', r.foreach; -- fails +end $$; +NOTICE: r.x = 1 +ERROR: field name "foreach" is a reserved key word +LINE 1: r.foreach + ^ +HINT: Use double quotes to quote it. +QUERY: r.foreach +CONTEXT: PL/pgSQL function inline_code_block line 5 at RAISE +do $$ declare r record; +begin + select 1 as x, 2 as foreach into r; + raise notice 'r.x = %', r.x; + raise notice 'r."foreach" = %', r."foreach"; -- ok +end $$; +NOTICE: r.x = 1 +NOTICE: r."foreach" = 2 diff --git a/src/pl/plpgsql/src/expected/plpgsql_record.out b/src/pl/plpgsql/src/expected/plpgsql_record.out index e5de7143606ab..511f9e03c85d5 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_record.out +++ b/src/pl/plpgsql/src/expected/plpgsql_record.out @@ -466,7 +466,8 @@ select getf1(1); ERROR: function getf1(integer) does not exist LINE 1: select getf1(1); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select getf1(row(1,2)); getf1 ------- diff --git a/src/pl/plpgsql/src/expected/plpgsql_simple.out b/src/pl/plpgsql/src/expected/plpgsql_simple.out index da351873e742e..ccf15ea22001e 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_simple.out +++ b/src/pl/plpgsql/src/expected/plpgsql_simple.out @@ -129,3 +129,22 @@ begin raise notice 'val = %', val; end; $$; NOTICE: val = 42 +-- We now optimize "SELECT simple-expr INTO var" using the simple-expression +-- logic. Verify that error reporting works the same as it did before. +do $$ +declare x bigint := 2^30; y int; +begin + -- overflow during assignment step does not get an extra context line + select x*x into y; +end $$; +ERROR: integer out of range +CONTEXT: PL/pgSQL function inline_code_block line 5 at SQL statement +do $$ +declare x bigint := 2^30; y int; +begin + -- overflow during expression evaluation step does get an extra context line + select x*x*x into y; +end $$; +ERROR: bigint out of range +CONTEXT: SQL statement "select x*x*x" +PL/pgSQL function inline_code_block line 5 at SQL statement diff --git a/src/pl/plpgsql/src/expected/plpgsql_trap.out b/src/pl/plpgsql/src/expected/plpgsql_trap.out index 62d1679c28f03..c37ac3b5c6983 100644 --- a/src/pl/plpgsql/src/expected/plpgsql_trap.out +++ b/src/pl/plpgsql/src/expected/plpgsql_trap.out @@ -138,13 +138,11 @@ select * from foo; drop table foo; create function trap_timeout() returns void as $$ begin - declare x int; begin - -- we assume this will take longer than 1 second: - select count(*) into x from generate_series(1, 1_000_000_000_000); + perform pg_sleep(10); exception when others then - raise notice 'caught others?'; + raise notice 'caught others: %', sqlerrm; when query_canceled then raise notice 'nyeah nyeah, can''t stop me'; end; @@ -157,7 +155,7 @@ set statement_timeout to 1000; select trap_timeout(); NOTICE: nyeah nyeah, can't stop me ERROR: end of function -CONTEXT: PL/pgSQL function trap_timeout() line 15 at RAISE +CONTEXT: PL/pgSQL function trap_timeout() line 13 at RAISE rollback; -- Test for pass-by-ref values being stored in proper context create function test_variable_storage() returns text as $$ diff --git a/src/pl/plpgsql/src/generate-plerrcodes.pl b/src/pl/plpgsql/src/generate-plerrcodes.pl index 7719432325375..2f312336456b6 100644 --- a/src/pl/plpgsql/src/generate-plerrcodes.pl +++ b/src/pl/plpgsql/src/generate-plerrcodes.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate the plerrcodes.h header from errcodes.txt -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/pl/plpgsql/src/meson.build b/src/pl/plpgsql/src/meson.build index 33c49ac25d946..6ff27006cfc5a 100644 --- a/src/pl/plpgsql/src/meson.build +++ b/src/pl/plpgsql/src/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group plpgsql_sources = files( 'pl_comp.c', diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c index 519f7695d7c14..b72c963b3be51 100644 --- a/src/pl/plpgsql/src/pl_comp.c +++ b/src/pl/plpgsql/src/pl_comp.c @@ -3,7 +3,7 @@ * pl_comp.c - Compiler part of the PL/pgSQL * procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -177,6 +177,7 @@ plpgsql_compile_callback(FunctionCallInfo fcinfo, yyscan_t scanner; Datum prosrcdatum; char *proc_source; + char *proc_signature; HeapTuple typeTup; Form_pg_type typeStruct; PLpgSQL_variable *var; @@ -223,16 +224,24 @@ plpgsql_compile_callback(FunctionCallInfo fcinfo, plpgsql_check_syntax = forValidator; plpgsql_curr_compile = function; + /* format_procedure leaks memory, so run it in temp context */ + proc_signature = format_procedure(fcinfo->flinfo->fn_oid); + /* * All the permanent output of compilation (e.g. parse tree) is kept in a * per-function memory context, so it can be reclaimed easily. + * + * While the func_cxt needs to be long-lived, we initially make it a child + * of the assumed-short-lived caller's context, and reparent it under + * CacheMemoryContext only upon success. This arrangement avoids memory + * leakage during compilation of a faulty function. */ - func_cxt = AllocSetContextCreate(TopMemoryContext, + func_cxt = AllocSetContextCreate(CurrentMemoryContext, "PL/pgSQL function", ALLOCSET_DEFAULT_SIZES); plpgsql_compile_tmp_cxt = MemoryContextSwitchTo(func_cxt); - function->fn_signature = format_procedure(fcinfo->flinfo->fn_oid); + function->fn_signature = pstrdup(proc_signature); MemoryContextSetIdentifier(func_cxt, function->fn_signature); function->fn_oid = fcinfo->flinfo->fn_oid; function->fn_input_collation = fcinfo->fncollation; @@ -289,8 +298,8 @@ plpgsql_compile_callback(FunctionCallInfo fcinfo, forValidator, plpgsql_error_funcname); - in_arg_varnos = (int *) palloc(numargs * sizeof(int)); - out_arg_variables = (PLpgSQL_variable **) palloc(numargs * sizeof(PLpgSQL_variable *)); + in_arg_varnos = palloc_array(int, numargs); + out_arg_variables = palloc_array(PLpgSQL_variable *, numargs); MemoryContextSwitchTo(func_cxt); @@ -703,6 +712,11 @@ plpgsql_compile_callback(FunctionCallInfo fcinfo, if (plpgsql_DumpExecTree) plpgsql_dumptree(function); + /* + * All is well, so make the func_cxt long-lived + */ + MemoryContextSetParent(func_cxt, CacheMemoryContext); + /* * Pop the error context stack */ @@ -758,7 +772,7 @@ plpgsql_compile_inline(char *proc_source) plpgsql_check_syntax = check_function_bodies; /* Function struct does not live past current statement */ - function = (PLpgSQL_function *) palloc0(sizeof(PLpgSQL_function)); + function = palloc0_object(PLpgSQL_function); plpgsql_curr_compile = function; @@ -940,7 +954,7 @@ add_dummy_return(PLpgSQL_function *function) { PLpgSQL_stmt_block *new; - new = palloc0(sizeof(PLpgSQL_stmt_block)); + new = palloc0_object(PLpgSQL_stmt_block); new->cmd_type = PLPGSQL_STMT_BLOCK; new->stmtid = ++function->nstatements; new->body = list_make1(function->action); @@ -952,7 +966,7 @@ add_dummy_return(PLpgSQL_function *function) { PLpgSQL_stmt_return *new; - new = palloc0(sizeof(PLpgSQL_stmt_return)); + new = palloc0_object(PLpgSQL_stmt_return); new->cmd_type = PLPGSQL_STMT_RETURN; new->stmtid = ++function->nstatements; new->expr = NULL; @@ -1201,17 +1215,22 @@ resolve_column_ref(ParseState *pstate, PLpgSQL_expr *expr, } /* - * We should not get here, because a RECFIELD datum should - * have been built at parse time for every possible qualified - * reference to fields of this record. But if we do, handle - * it like field-not-found: throw error or return NULL. + * Ideally we'd never get here, because a RECFIELD datum + * should have been built at parse time for every qualified + * reference to a field of this record that appears in the + * source text. However, plpgsql_yylex will not build such a + * datum unless the field name lexes as token type IDENT. + * Hence, if the would-be field name is a PL/pgSQL reserved + * word, we lose. Assume that that's what happened and tell + * the user to quote it, unless the caller prefers we just + * return NULL. */ if (error_if_no_field) ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("record \"%s\" has no field \"%s\"", - (nnames_field == 1) ? name1 : name2, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("field name \"%s\" is a reserved key word", colname), + errhint("Use double quotes to quote it."), parser_errposition(pstate, cref->location))); } break; @@ -1658,6 +1677,11 @@ plpgsql_parse_wordrowtype(char *ident) { Oid classOid; Oid typOid; + TypeName *typName; + MemoryContext oldCxt; + + /* Avoid memory leaks in long-term function context */ + oldCxt = MemoryContextSwitchTo(plpgsql_compile_tmp_cxt); /* * Look up the relation. Note that because relation rowtypes have the @@ -1680,9 +1704,12 @@ plpgsql_parse_wordrowtype(char *ident) errmsg("relation \"%s\" does not have a composite type", ident))); + typName = makeTypeName(ident); + + MemoryContextSwitchTo(oldCxt); + /* Build and return the row type struct */ - return plpgsql_build_datatype(typOid, -1, InvalidOid, - makeTypeName(ident)); + return plpgsql_build_datatype(typOid, -1, InvalidOid, typName); } /* ---------- @@ -1696,6 +1723,7 @@ plpgsql_parse_cwordrowtype(List *idents) Oid classOid; Oid typOid; RangeVar *relvar; + TypeName *typName; MemoryContext oldCxt; /* @@ -1718,11 +1746,12 @@ plpgsql_parse_cwordrowtype(List *idents) errmsg("relation \"%s\" does not have a composite type", relvar->relname))); + typName = makeTypeNameFromNameList(idents); + MemoryContextSwitchTo(oldCxt); /* Build and return the row type struct */ - return plpgsql_build_datatype(typOid, -1, InvalidOid, - makeTypeNameFromNameList(idents)); + return plpgsql_build_datatype(typOid, -1, InvalidOid, typName); } /* @@ -1747,7 +1776,7 @@ plpgsql_build_variable(const char *refname, int lineno, PLpgSQL_type *dtype, /* Ordinary scalar datatype */ PLpgSQL_var *var; - var = palloc0(sizeof(PLpgSQL_var)); + var = palloc0_object(PLpgSQL_var); var->dtype = PLPGSQL_DTYPE_VAR; var->refname = pstrdup(refname); var->lineno = lineno; @@ -1804,7 +1833,7 @@ plpgsql_build_record(const char *refname, int lineno, { PLpgSQL_rec *rec; - rec = palloc0(sizeof(PLpgSQL_rec)); + rec = palloc0_object(PLpgSQL_rec); rec->dtype = PLPGSQL_DTYPE_REC; rec->refname = pstrdup(refname); rec->lineno = lineno; @@ -1830,14 +1859,14 @@ build_row_from_vars(PLpgSQL_variable **vars, int numvars) PLpgSQL_row *row; int i; - row = palloc0(sizeof(PLpgSQL_row)); + row = palloc0_object(PLpgSQL_row); row->dtype = PLPGSQL_DTYPE_ROW; row->refname = "(unnamed row)"; row->lineno = -1; row->rowtupdesc = CreateTemplateTupleDesc(numvars); row->nfields = numvars; - row->fieldnames = palloc(numvars * sizeof(char *)); - row->varnos = palloc(numvars * sizeof(int)); + row->fieldnames = palloc_array(char *, numvars); + row->varnos = palloc_array(int, numvars); for (i = 0; i < numvars; i++) { @@ -1883,6 +1912,8 @@ build_row_from_vars(PLpgSQL_variable **vars, int numvars) TupleDescInitEntryCollation(row->rowtupdesc, i + 1, typcoll); } + TupleDescFinalize(row->rowtupdesc); + return row; } @@ -1911,7 +1942,7 @@ plpgsql_build_recfield(PLpgSQL_rec *rec, const char *fldname) } /* nope, so make a new one */ - recfield = palloc0(sizeof(PLpgSQL_recfield)); + recfield = palloc0_object(PLpgSQL_recfield); recfield->dtype = PLPGSQL_DTYPE_RECFIELD; recfield->fieldname = pstrdup(fldname); recfield->recparentno = rec->dno; @@ -1937,6 +1968,8 @@ plpgsql_build_recfield(PLpgSQL_rec *rec, const char *fldname) * origtypname is the parsed form of what the user wrote as the type name. * It can be NULL if the type could not be a composite type, or if it was * identified by OID to begin with (e.g., it's a function argument type). + * origtypname is in short-lived storage and must be copied if we choose + * to incorporate it into the function's parse tree. */ PLpgSQL_type * plpgsql_build_datatype(Oid typeOid, int32 typmod, @@ -1973,7 +2006,7 @@ build_datatype(HeapTuple typeTup, int32 typmod, errmsg("type \"%s\" is only a shell", NameStr(typeStruct->typname)))); - typ = (PLpgSQL_type *) palloc(sizeof(PLpgSQL_type)); + typ = palloc_object(PLpgSQL_type); typ->typname = pstrdup(NameStr(typeStruct->typname)); typ->typoid = typeStruct->oid; @@ -2055,7 +2088,7 @@ build_datatype(HeapTuple typeTup, int32 typmod, errmsg("type %s is not composite", format_type_be(typ->typoid)))); - typ->origtypname = origtypname; + typ->origtypname = copyObject(origtypname); typ->tcache = typentry; typ->tupdesc_id = typentry->tupDesc_identifier; } @@ -2153,7 +2186,7 @@ plpgsql_parse_err_condition(char *condname) if (strcmp(condname, "others") == 0) { - new = palloc(sizeof(PLpgSQL_condition)); + new = palloc_object(PLpgSQL_condition); new->sqlerrstate = PLPGSQL_OTHERS; new->condname = condname; new->next = NULL; @@ -2165,7 +2198,7 @@ plpgsql_parse_err_condition(char *condname) { if (strcmp(condname, exception_label_map[i].label) == 0) { - new = palloc(sizeof(PLpgSQL_condition)); + new = palloc_object(PLpgSQL_condition); new->sqlerrstate = exception_label_map[i].sqlerrstate; new->condname = condname; new->next = prev; @@ -2227,7 +2260,7 @@ plpgsql_finish_datums(PLpgSQL_function *function) int i; function->ndatums = plpgsql_nDatums; - function->datums = palloc(sizeof(PLpgSQL_datum *) * plpgsql_nDatums); + function->datums = palloc_array(PLpgSQL_datum *, plpgsql_nDatums); for (i = 0; i < plpgsql_nDatums; i++) { function->datums[i] = plpgsql_Datums[i]; @@ -2292,7 +2325,7 @@ plpgsql_add_initdatums(int **varnos) { if (n > 0) { - *varnos = (int *) palloc(sizeof(int) * n); + *varnos = palloc_array(int, n); n = 0; for (i = datums_last; i < plpgsql_nDatums; i++) @@ -2302,6 +2335,7 @@ plpgsql_add_initdatums(int **varnos) case PLPGSQL_DTYPE_VAR: case PLPGSQL_DTYPE_REC: (*varnos)[n++] = plpgsql_Datums[i]->dno; + break; default: break; diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index bb99781c56e39..65b0fd0790f2e 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -3,7 +3,7 @@ * pl_exec.c - Executor for the PL/pgSQL * procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -267,6 +267,7 @@ typedef struct count_param_references_context static void coerce_function_result_tuple(PLpgSQL_execstate *estate, TupleDesc tupdesc); static void plpgsql_exec_error_callback(void *arg); +static void plpgsql_execsql_error_callback(void *arg); static void copy_plpgsql_datums(PLpgSQL_execstate *estate, PLpgSQL_function *func); static void plpgsql_fulfill_promise(PLpgSQL_execstate *estate, @@ -1301,6 +1302,37 @@ plpgsql_exec_error_callback(void *arg) estate->func->fn_signature); } +/* + * error context callback used for "SELECT simple-expr INTO var" + * + * This should match the behavior of spi.c's _SPI_error_callback(), + * so that the construct still reports errors the same as it did + * before we optimized it with the simple-expression code path. + */ +static void +plpgsql_execsql_error_callback(void *arg) +{ + PLpgSQL_expr *expr = (PLpgSQL_expr *) arg; + const char *query = expr->query; + int syntaxerrposition; + + /* + * If there is a syntax error position, convert to internal syntax error; + * otherwise treat the query as an item of context stack + */ + syntaxerrposition = geterrposition(); + if (syntaxerrposition > 0) + { + errposition(0); + internalerrposition(syntaxerrposition); + internalerrquery(query); + } + else + { + errcontext("SQL statement \"%s\"", query); + } +} + /* ---------- * Support function for initializing local execution variables @@ -1318,8 +1350,7 @@ copy_plpgsql_datums(PLpgSQL_execstate *estate, int i; /* Allocate local datum-pointer array */ - estate->datums = (PLpgSQL_datum **) - palloc(sizeof(PLpgSQL_datum *) * ndatums); + estate->datums = palloc_array(PLpgSQL_datum *, ndatums); /* * To reduce palloc overhead, we make a single palloc request for all the @@ -1497,7 +1528,7 @@ plpgsql_fulfill_promise(PLpgSQL_execstate *estate, int lbs[1]; int i; - elems = palloc(sizeof(Datum) * nelems); + elems = palloc_array(Datum, nelems); for (i = 0; i < nelems; i++) elems[i] = CStringGetTextDatum(estate->trigdata->tg_trigger->tgargs[i]); dims[0] = nelems; @@ -2340,11 +2371,11 @@ make_callstmt_target(PLpgSQL_execstate *estate, PLpgSQL_expr *expr) */ MemoryContextSwitchTo(estate->func->fn_cxt); - row = (PLpgSQL_row *) palloc0(sizeof(PLpgSQL_row)); + row = palloc0_object(PLpgSQL_row); row->dtype = PLPGSQL_DTYPE_ROW; row->refname = "(unnamed row)"; row->lineno = -1; - row->varnos = (int *) palloc(numargs * sizeof(int)); + row->varnos = palloc_array(int, numargs); MemoryContextSwitchTo(get_eval_mcontext(estate)); @@ -3231,7 +3262,7 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt) /* fulfill promise if needed, then handle like regular var */ plpgsql_fulfill_promise(estate, (PLpgSQL_var *) retvar); - /* FALL THRU */ + pg_fallthrough; case PLPGSQL_DTYPE_VAR: { @@ -3256,28 +3287,14 @@ exec_stmt_return(PLpgSQL_execstate *estate, PLpgSQL_stmt_return *stmt) } break; - case PLPGSQL_DTYPE_REC: - { - PLpgSQL_rec *rec = (PLpgSQL_rec *) retvar; - - /* If record is empty, we return NULL not a row of nulls */ - if (rec->erh && !ExpandedRecordIsEmpty(rec->erh)) - { - estate->retval = ExpandedRecordGetDatum(rec->erh); - estate->retisnull = false; - estate->rettype = rec->rectypeid; - } - } - break; - case PLPGSQL_DTYPE_ROW: + case PLPGSQL_DTYPE_REC: { - PLpgSQL_row *row = (PLpgSQL_row *) retvar; + /* exec_eval_datum can handle these cases */ int32 rettypmod; - /* We get here if there are multiple OUT parameters */ exec_eval_datum(estate, - (PLpgSQL_datum *) row, + retvar, &estate->rettype, &rettypmod, &estate->retval, @@ -3377,14 +3394,14 @@ exec_stmt_return_next(PLpgSQL_execstate *estate, /* fulfill promise if needed, then handle like regular var */ plpgsql_fulfill_promise(estate, (PLpgSQL_var *) retvar); - /* FALL THRU */ + pg_fallthrough; case PLPGSQL_DTYPE_VAR: { PLpgSQL_var *var = (PLpgSQL_var *) retvar; Datum retval = var->value; bool isNull = var->isnull; - Form_pg_attribute attr = TupleDescAttr(tupdesc, 0); + Form_pg_attribute attr; if (natts != 1) ereport(ERROR, @@ -3397,6 +3414,7 @@ exec_stmt_return_next(PLpgSQL_execstate *estate, var->datatype->typlen); /* coerce type if needed */ + attr = TupleDescAttr(tupdesc, 0); retval = exec_cast_value(estate, retval, &isNull, @@ -3515,7 +3533,7 @@ exec_stmt_return_next(PLpgSQL_execstate *estate, } else { - Form_pg_attribute attr = TupleDescAttr(tupdesc, 0); + Form_pg_attribute attr; /* Simple scalar result */ if (natts != 1) @@ -3524,6 +3542,7 @@ exec_stmt_return_next(PLpgSQL_execstate *estate, errmsg("wrong result type supplied in RETURN NEXT"))); /* coerce type if needed */ + attr = TupleDescAttr(tupdesc, 0); retval = exec_cast_value(estate, retval, &isNull, @@ -4268,6 +4287,74 @@ exec_stmt_execsql(PLpgSQL_execstate *estate, stmt->mod_stmt_set = true; } + /* + * Some users write "SELECT expr INTO var" instead of "var := expr". If + * the expression is simple and the INTO target is a single variable, we + * can bypass SPI and call ExecEvalExpr() directly. (exec_eval_expr would + * actually work for non-simple expressions too, but such an expression + * might return more or less than one row, complicating matters greatly. + * The potential performance win is small if it's non-simple, and any + * errors we might issue would likely look different, so avoid using this + * code path for non-simple cases.) + */ + if (expr->expr_simple_expr && stmt->into) + { + PLpgSQL_datum *target = estate->datums[stmt->target->dno]; + + if (target->dtype == PLPGSQL_DTYPE_ROW) + { + PLpgSQL_row *row = (PLpgSQL_row *) target; + + if (row->nfields == 1) + { + ErrorContextCallback plerrcontext; + Datum value; + bool isnull; + Oid valtype; + int32 valtypmod; + + /* + * Setup error traceback support for ereport(). This is so + * that error reports for the expression will look similar + * whether or not we take this code path. + */ + plerrcontext.callback = plpgsql_execsql_error_callback; + plerrcontext.arg = expr; + plerrcontext.previous = error_context_stack; + error_context_stack = &plerrcontext; + + /* If first time through, create a plan for this expression */ + if (expr->plan == NULL) + exec_prepare_plan(estate, expr, 0); + + /* And evaluate the expression */ + value = exec_eval_expr(estate, expr, + &isnull, &valtype, &valtypmod); + + /* + * Pop the error context stack: the code below would not use + * SPI's error handling during the assignment step. + */ + error_context_stack = plerrcontext.previous; + + /* Assign the result to the INTO target */ + exec_assign_value(estate, estate->datums[row->varnos[0]], + value, isnull, valtype, valtypmod); + exec_eval_cleanup(estate); + + /* + * We must duplicate the other effects of the code below, as + * well. We know that exactly one row was returned, so it + * doesn't matter whether the INTO was STRICT or not. + */ + exec_set_found(estate, true); + estate->eval_processed = 1; + + return PLPGSQL_RC_OK; + } + } + } + /* * Set up ParamListInfo to pass to executor */ @@ -5314,7 +5401,7 @@ exec_eval_datum(PLpgSQL_execstate *estate, /* fulfill promise if needed, then handle like regular var */ plpgsql_fulfill_promise(estate, (PLpgSQL_var *) datum); - /* FALL THRU */ + pg_fallthrough; case PLPGSQL_DTYPE_VAR: { @@ -5703,7 +5790,7 @@ exec_eval_expr(PLpgSQL_execstate *estate, /* * Else do it the hard way via exec_run_select */ - rc = exec_run_select(estate, expr, 2, NULL); + rc = exec_run_select(estate, expr, 0, NULL); if (rc != SPI_OK_SELECT) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -5757,6 +5844,10 @@ exec_eval_expr(PLpgSQL_execstate *estate, /* ---------- * exec_run_select Execute a select query + * + * Note: passing maxtuples different from 0 ("return all tuples") is + * deprecated because it will prevent parallel execution of the query. + * However, we retain the parameter in case we need it someday. * ---------- */ static int @@ -8606,6 +8697,15 @@ exec_set_found(PLpgSQL_execstate *estate, bool state) PLpgSQL_var *var; var = (PLpgSQL_var *) (estate->datums[estate->found_varno]); + + /* + * Use pg_assume() to avoid a spurious warning with some compilers, by + * telling the compiler that the VARATT_IS_EXTERNAL_NON_EXPANDED() branch + * in assign_simple_var() will never be reached when called from here, due + * to "found" being a boolean (i.e. a byvalue type), not a varlena. + */ + pg_assume(var->datatype->typlen != -1); + assign_simple_var(estate, var, BoolGetDatum(state), false, false); } @@ -8806,7 +8906,7 @@ assign_simple_var(PLpgSQL_execstate *estate, PLpgSQL_var *var, * pain, but there's little choice. */ oldcxt = MemoryContextSwitchTo(get_eval_mcontext(estate)); - detoasted = PointerGetDatum(detoast_external_attr((struct varlena *) DatumGetPointer(newvalue))); + detoasted = PointerGetDatum(detoast_external_attr((varlena *) DatumGetPointer(newvalue))); MemoryContextSwitchTo(oldcxt); /* Now's a good time to not leak the input value if it's freeable */ if (freeable) diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c index bc7a61feb4d23..92cd9116c0e51 100644 --- a/src/pl/plpgsql/src/pl_funcs.c +++ b/src/pl/plpgsql/src/pl_funcs.c @@ -3,7 +3,7 @@ * pl_funcs.c - Misc functions for the PL/pgSQL * procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index 5612e66d0239d..5009e59a78f12 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -3,7 +3,7 @@ * * pl_gram.y - Parser for the PL/pgSQL procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -426,7 +426,7 @@ pl_block : decl_sect K_BEGIN proc_sect exception_sect K_END opt_label { PLpgSQL_stmt_block *new; - new = palloc0(sizeof(PLpgSQL_stmt_block)); + new = palloc0_object(PLpgSQL_stmt_block); new->cmd_type = PLPGSQL_STMT_BLOCK; new->lineno = plpgsql_location_to_lineno(@2, yyscanner); @@ -606,14 +606,14 @@ decl_cursor_args : int i; ListCell *l; - new = palloc0(sizeof(PLpgSQL_row)); + new = palloc0_object(PLpgSQL_row); new->dtype = PLPGSQL_DTYPE_ROW; new->refname = "(unnamed row)"; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->rowtupdesc = NULL; new->nfields = list_length($2); - new->fieldnames = palloc(new->nfields * sizeof(char *)); - new->varnos = palloc(new->nfields * sizeof(int)); + new->fieldnames = palloc_array(char *, new->nfields); + new->varnos = palloc_array(int, new->nfields); i = 0; foreach (l, $2) @@ -898,7 +898,7 @@ stmt_perform : K_PERFORM PLpgSQL_stmt_perform *new; int startloc; - new = palloc0(sizeof(PLpgSQL_stmt_perform)); + new = palloc0_object(PLpgSQL_stmt_perform); new->cmd_type = PLPGSQL_STMT_PERFORM; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -934,7 +934,7 @@ stmt_call : K_CALL { PLpgSQL_stmt_call *new; - new = palloc0(sizeof(PLpgSQL_stmt_call)); + new = palloc0_object(PLpgSQL_stmt_call); new->cmd_type = PLPGSQL_STMT_CALL; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -953,7 +953,7 @@ stmt_call : K_CALL /* use the same structures as for CALL, for simplicity */ PLpgSQL_stmt_call *new; - new = palloc0(sizeof(PLpgSQL_stmt_call)); + new = palloc0_object(PLpgSQL_stmt_call); new->cmd_type = PLPGSQL_STMT_CALL; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -992,7 +992,7 @@ stmt_assign : T_DATUM } check_assignable($1.datum, @1, yyscanner); - new = palloc0(sizeof(PLpgSQL_stmt_assign)); + new = palloc0_object(PLpgSQL_stmt_assign); new->cmd_type = PLPGSQL_STMT_ASSIGN; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -1015,7 +1015,7 @@ stmt_getdiag : K_GET getdiag_area_opt K_DIAGNOSTICS getdiag_list ';' PLpgSQL_stmt_getdiag *new; ListCell *lc; - new = palloc0(sizeof(PLpgSQL_stmt_getdiag)); + new = palloc0_object(PLpgSQL_stmt_getdiag); new->cmd_type = PLPGSQL_STMT_GETDIAG; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -1101,7 +1101,7 @@ getdiag_list_item : getdiag_target assign_operator getdiag_item { PLpgSQL_diag_item *new; - new = palloc(sizeof(PLpgSQL_diag_item)); + new = palloc_object(PLpgSQL_diag_item); new->target = $1->dno; new->kind = $3; @@ -1191,7 +1191,7 @@ stmt_if : K_IF expr_until_then proc_sect stmt_elsifs stmt_else K_END K_IF ';' { PLpgSQL_stmt_if *new; - new = palloc0(sizeof(PLpgSQL_stmt_if)); + new = palloc0_object(PLpgSQL_stmt_if); new->cmd_type = PLPGSQL_STMT_IF; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -1212,7 +1212,7 @@ stmt_elsifs : { PLpgSQL_if_elsif *new; - new = palloc0(sizeof(PLpgSQL_if_elsif)); + new = palloc0_object(PLpgSQL_if_elsif); new->lineno = plpgsql_location_to_lineno(@2, yyscanner); new->cond = $3; new->stmts = $4; @@ -1264,7 +1264,7 @@ case_when_list : case_when_list case_when case_when : K_WHEN expr_until_then proc_sect { - PLpgSQL_case_when *new = palloc(sizeof(PLpgSQL_case_when)); + PLpgSQL_case_when *new = palloc_object(PLpgSQL_case_when); new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->expr = $2; @@ -1296,7 +1296,7 @@ stmt_loop : opt_loop_label K_LOOP loop_body { PLpgSQL_stmt_loop *new; - new = palloc0(sizeof(PLpgSQL_stmt_loop)); + new = palloc0_object(PLpgSQL_stmt_loop); new->cmd_type = PLPGSQL_STMT_LOOP; new->lineno = plpgsql_location_to_lineno(@2, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -1314,7 +1314,7 @@ stmt_while : opt_loop_label K_WHILE expr_until_loop loop_body { PLpgSQL_stmt_while *new; - new = palloc0(sizeof(PLpgSQL_stmt_while)); + new = palloc0_object(PLpgSQL_stmt_while); new->cmd_type = PLPGSQL_STMT_WHILE; new->lineno = plpgsql_location_to_lineno(@2, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -1368,7 +1368,8 @@ for_control : for_variable K_IN int tok = yylex(&yylval, &yylloc, yyscanner); int tokloc = yylloc; - if (tok == K_EXECUTE) + if (tok_is_keyword(tok, &yylval, + K_EXECUTE, "execute")) { /* EXECUTE means it's a dynamic FOR loop */ PLpgSQL_stmt_dynfors *new; @@ -1379,7 +1380,7 @@ for_control : for_variable K_IN "LOOP or USING", &term, &yylval, &yylloc, yyscanner); - new = palloc0(sizeof(PLpgSQL_stmt_dynfors)); + new = palloc0_object(PLpgSQL_stmt_dynfors); new->cmd_type = PLPGSQL_STMT_DYNFORS; new->stmtid = ++plpgsql_curr_compile->nstatements; if ($1.row) @@ -1425,7 +1426,7 @@ for_control : for_variable K_IN PLpgSQL_stmt_forc *new; PLpgSQL_var *cursor = (PLpgSQL_var *) yylval.wdatum.datum; - new = (PLpgSQL_stmt_forc *) palloc0(sizeof(PLpgSQL_stmt_forc)); + new = palloc0_object(PLpgSQL_stmt_forc); new->cmd_type = PLPGSQL_STMT_FORC; new->stmtid = ++plpgsql_curr_compile->nstatements; new->curvar = cursor->dno; @@ -1544,7 +1545,7 @@ for_control : for_variable K_IN NULL), true); - new = palloc0(sizeof(PLpgSQL_stmt_fori)); + new = palloc0_object(PLpgSQL_stmt_fori); new->cmd_type = PLPGSQL_STMT_FORI; new->stmtid = ++plpgsql_curr_compile->nstatements; new->var = fvar; @@ -1572,7 +1573,7 @@ for_control : for_variable K_IN check_sql_expr(expr1->query, expr1->parseMode, expr1loc, yyscanner); - new = palloc0(sizeof(PLpgSQL_stmt_fors)); + new = palloc0_object(PLpgSQL_stmt_fors); new->cmd_type = PLPGSQL_STMT_FORS; new->stmtid = ++plpgsql_curr_compile->nstatements; if ($1.row) @@ -1674,7 +1675,7 @@ stmt_foreach_a : opt_loop_label K_FOREACH for_variable foreach_slice K_IN K_ARRA { PLpgSQL_stmt_foreach_a *new; - new = palloc0(sizeof(PLpgSQL_stmt_foreach_a)); + new = palloc0_object(PLpgSQL_stmt_foreach_a); new->cmd_type = PLPGSQL_STMT_FOREACH_A; new->lineno = plpgsql_location_to_lineno(@2, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -1722,7 +1723,7 @@ stmt_exit : exit_type opt_label opt_exitcond { PLpgSQL_stmt_exit *new; - new = palloc0(sizeof(PLpgSQL_stmt_exit)); + new = palloc0_object(PLpgSQL_stmt_exit); new->cmd_type = PLPGSQL_STMT_EXIT; new->stmtid = ++plpgsql_curr_compile->nstatements; new->is_exit = $1; @@ -1812,7 +1813,7 @@ stmt_raise : K_RAISE PLpgSQL_stmt_raise *new; int tok; - new = palloc(sizeof(PLpgSQL_stmt_raise)); + new = palloc_object(PLpgSQL_stmt_raise); new->cmd_type = PLPGSQL_STMT_RAISE; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); @@ -1958,7 +1959,7 @@ stmt_assert : K_ASSERT PLpgSQL_stmt_assert *new; int tok; - new = palloc(sizeof(PLpgSQL_stmt_assert)); + new = palloc_object(PLpgSQL_stmt_assert); new->cmd_type = PLPGSQL_STMT_ASSERT; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); @@ -2044,7 +2045,7 @@ stmt_dynexecute : K_EXECUTE NULL, &endtoken, &yylval, &yylloc, yyscanner); - new = palloc(sizeof(PLpgSQL_stmt_dynexecute)); + new = palloc_object(PLpgSQL_stmt_dynexecute); new->cmd_type = PLPGSQL_STMT_DYNEXECUTE; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -2102,7 +2103,7 @@ stmt_open : K_OPEN cursor_variable PLpgSQL_stmt_open *new; int tok; - new = palloc0(sizeof(PLpgSQL_stmt_open)); + new = palloc0_object(PLpgSQL_stmt_open); new->cmd_type = PLPGSQL_STMT_OPEN; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -2135,7 +2136,8 @@ stmt_open : K_OPEN cursor_variable yyerror(&yylloc, NULL, yyscanner, "syntax error, expected \"FOR\""); tok = yylex(&yylval, &yylloc, yyscanner); - if (tok == K_EXECUTE) + if (tok_is_keyword(tok, &yylval, + K_EXECUTE, "execute")) { int endtoken; @@ -2227,7 +2229,7 @@ stmt_close : K_CLOSE cursor_variable ';' { PLpgSQL_stmt_close *new; - new = palloc(sizeof(PLpgSQL_stmt_close)); + new = palloc_object(PLpgSQL_stmt_close); new->cmd_type = PLPGSQL_STMT_CLOSE; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -2248,7 +2250,7 @@ stmt_commit : K_COMMIT opt_transaction_chain ';' { PLpgSQL_stmt_commit *new; - new = palloc(sizeof(PLpgSQL_stmt_commit)); + new = palloc_object(PLpgSQL_stmt_commit); new->cmd_type = PLPGSQL_STMT_COMMIT; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -2262,7 +2264,7 @@ stmt_rollback : K_ROLLBACK opt_transaction_chain ';' { PLpgSQL_stmt_rollback *new; - new = palloc(sizeof(PLpgSQL_stmt_rollback)); + new = palloc_object(PLpgSQL_stmt_rollback); new->cmd_type = PLPGSQL_STMT_ROLLBACK; new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -2325,7 +2327,7 @@ exception_sect : * current block. */ int lineno = plpgsql_location_to_lineno(@1, yyscanner); - PLpgSQL_exception_block *new = palloc(sizeof(PLpgSQL_exception_block)); + PLpgSQL_exception_block *new = palloc_object(PLpgSQL_exception_block); PLpgSQL_variable *var; plpgsql_curr_compile->has_exception_block = true; @@ -2373,7 +2375,7 @@ proc_exception : K_WHEN proc_conditions K_THEN proc_sect { PLpgSQL_exception *new; - new = palloc0(sizeof(PLpgSQL_exception)); + new = palloc0_object(PLpgSQL_exception); new->lineno = plpgsql_location_to_lineno(@1, yyscanner); new->conditions = $2; new->action = $4; @@ -2418,7 +2420,7 @@ proc_condition : any_identifier if (strspn(sqlstatestr, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != 5) yyerror(&yylloc, NULL, yyscanner, "invalid SQLSTATE code"); - new = palloc(sizeof(PLpgSQL_condition)); + new = palloc_object(PLpgSQL_condition); new->sqlerrstate = MAKE_SQLSTATE(sqlstatestr[0], sqlstatestr[1], @@ -2536,6 +2538,7 @@ unreserved_keyword : | K_ERRCODE | K_ERROR | K_EXCEPTION + | K_EXECUTE | K_EXIT | K_FETCH | K_FIRST @@ -2581,6 +2584,7 @@ unreserved_keyword : | K_SLICE | K_SQLSTATE | K_STACKED + | K_STRICT | K_TABLE | K_TABLE_NAME | K_TYPE @@ -2667,7 +2671,7 @@ static PLpgSQL_expr * make_plpgsql_expr(const char *query, RawParseMode parsemode) { - PLpgSQL_expr *expr = palloc0(sizeof(PLpgSQL_expr)); + PLpgSQL_expr *expr = palloc0_object(PLpgSQL_expr); expr->query = pstrdup(query); expr->parseMode = parsemode; @@ -3177,7 +3181,7 @@ make_execsql_stmt(int firsttoken, int location, PLword *word, YYSTYPE *yylvalp, check_sql_expr(expr->query, expr->parseMode, location, yyscanner); - execsql = palloc0(sizeof(PLpgSQL_stmt_execsql)); + execsql = palloc0_object(PLpgSQL_stmt_execsql); execsql->cmd_type = PLPGSQL_STMT_EXECSQL; execsql->lineno = plpgsql_location_to_lineno(location, yyscanner); execsql->stmtid = ++plpgsql_curr_compile->nstatements; @@ -3204,7 +3208,7 @@ read_fetch_direction(YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t yyscanner) * We create the PLpgSQL_stmt_fetch struct here, but only fill in the * fields arising from the optional direction clause */ - fetch = (PLpgSQL_stmt_fetch *) palloc0(sizeof(PLpgSQL_stmt_fetch)); + fetch = (PLpgSQL_stmt_fetch *) palloc0_object(PLpgSQL_stmt_fetch); fetch->cmd_type = PLPGSQL_STMT_FETCH; fetch->stmtid = ++plpgsql_curr_compile->nstatements; /* set direction defaults: */ @@ -3356,7 +3360,7 @@ make_return_stmt(int location, YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t yysc { PLpgSQL_stmt_return *new; - new = palloc0(sizeof(PLpgSQL_stmt_return)); + new = palloc0_object(PLpgSQL_stmt_return); new->cmd_type = PLPGSQL_STMT_RETURN; new->lineno = plpgsql_location_to_lineno(location, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -3444,7 +3448,7 @@ make_return_next_stmt(int location, YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t errmsg("cannot use RETURN NEXT in a non-SETOF function"), parser_errposition(location))); - new = palloc0(sizeof(PLpgSQL_stmt_return_next)); + new = palloc0_object(PLpgSQL_stmt_return_next); new->cmd_type = PLPGSQL_STMT_RETURN_NEXT; new->lineno = plpgsql_location_to_lineno(location, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; @@ -3508,13 +3512,14 @@ make_return_query_stmt(int location, YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_ errmsg("cannot use RETURN QUERY in a non-SETOF function"), parser_errposition(location))); - new = palloc0(sizeof(PLpgSQL_stmt_return_query)); + new = palloc0_object(PLpgSQL_stmt_return_query); new->cmd_type = PLPGSQL_STMT_RETURN_QUERY; new->lineno = plpgsql_location_to_lineno(location, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; /* check for RETURN QUERY EXECUTE */ - if ((tok = yylex(yylvalp, yyllocp, yyscanner)) != K_EXECUTE) + tok = yylex(yylvalp, yyllocp, yyscanner); + if (!tok_is_keyword(tok, yylvalp, K_EXECUTE, "execute")) { /* ordinary static query */ plpgsql_push_back_token(tok, yylvalp, yyllocp, yyscanner); @@ -3597,7 +3602,7 @@ read_into_target(PLpgSQL_variable **target, bool *strict, YYSTYPE *yylvalp, YYLT *strict = false; tok = yylex(yylvalp, yyllocp, yyscanner); - if (strict && tok == K_STRICT) + if (strict && tok_is_keyword(tok, yylvalp, K_STRICT, "strict")) { *strict = true; tok = yylex(yylvalp, yyllocp, yyscanner); @@ -3701,14 +3706,14 @@ read_into_scalar_list(char *initial_name, */ plpgsql_push_back_token(tok, yylvalp, yyllocp, yyscanner); - row = palloc0(sizeof(PLpgSQL_row)); + row = palloc0_object(PLpgSQL_row); row->dtype = PLPGSQL_DTYPE_ROW; row->refname = "(unnamed row)"; row->lineno = plpgsql_location_to_lineno(initial_location, yyscanner); row->rowtupdesc = NULL; row->nfields = nfields; - row->fieldnames = palloc(sizeof(char *) * nfields); - row->varnos = palloc(sizeof(int) * nfields); + row->fieldnames = palloc_array(char *, nfields); + row->varnos = palloc_array(int, nfields); while (--nfields >= 0) { row->fieldnames[nfields] = fieldnames[nfields]; @@ -3736,14 +3741,14 @@ make_scalar_list1(char *initial_name, check_assignable(initial_datum, location, yyscanner); - row = palloc0(sizeof(PLpgSQL_row)); + row = palloc0_object(PLpgSQL_row); row->dtype = PLPGSQL_DTYPE_ROW; row->refname = "(unnamed row)"; row->lineno = lineno; row->rowtupdesc = NULL; row->nfields = 1; - row->fieldnames = palloc(sizeof(char *)); - row->varnos = palloc(sizeof(int)); + row->fieldnames = palloc_object(char *); + row->varnos = palloc_object(int); row->fieldnames[0] = initial_name; row->varnos[0] = initial_datum->dno; @@ -3848,6 +3853,7 @@ parse_datatype(const char *string, int location, yyscan_t yyscanner) int32 typmod; sql_error_callback_arg cbarg; ErrorContextCallback syntax_errcontext; + MemoryContext oldCxt; cbarg.location = location; cbarg.yyscanner = yyscanner; @@ -3857,9 +3863,14 @@ parse_datatype(const char *string, int location, yyscan_t yyscanner) syntax_errcontext.previous = error_context_stack; error_context_stack = &syntax_errcontext; - /* Let the main parser try to parse it under standard SQL rules */ + /* + * Let the main parser try to parse it under standard SQL rules. The + * parser leaks memory, so run it in temp context. + */ + oldCxt = MemoryContextSwitchTo(plpgsql_compile_tmp_cxt); typeName = typeStringToTypeName(string, NULL); typenameTypeIdAndMod(NULL, typeName, &type_id, &typmod); + MemoryContextSwitchTo(oldCxt); /* Restore former ereport callback */ error_context_stack = syntax_errcontext.previous; @@ -3944,7 +3955,7 @@ read_cursor_args(PLpgSQL_var *cursor, int until, YYSTYPE *yylvalp, YYLTYPE *yyll * Read the arguments, one by one. */ row = (PLpgSQL_row *) plpgsql_Datums[cursor->cursor_explicit_argrow]; - argv = (char **) palloc0(row->nfields * sizeof(char *)); + argv = (char **) palloc0_array(char *, row->nfields); for (argc = 0; argc < row->nfields; argc++) { @@ -4080,7 +4091,7 @@ read_raise_options(YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t yyscanner) if ((tok = yylex(yylvalp, yyllocp, yyscanner)) == 0) yyerror(yyllocp, NULL, yyscanner, "unexpected end of function definition"); - opt = (PLpgSQL_raise_option *) palloc(sizeof(PLpgSQL_raise_option)); + opt = palloc_object(PLpgSQL_raise_option); if (tok_is_keyword(tok, yylvalp, K_ERRCODE, "errcode")) @@ -4171,7 +4182,7 @@ make_case(int location, PLpgSQL_expr *t_expr, { PLpgSQL_stmt_case *new; - new = palloc(sizeof(PLpgSQL_stmt_case)); + new = palloc_object(PLpgSQL_stmt_case); new->cmd_type = PLPGSQL_STMT_CASE; new->lineno = plpgsql_location_to_lineno(location, yyscanner); new->stmtid = ++plpgsql_curr_compile->nstatements; diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c index e9a729299470e..3055c3db5d21f 100644 --- a/src/pl/plpgsql/src/pl_handler.c +++ b/src/pl/plpgsql/src/pl_handler.c @@ -3,7 +3,7 @@ * pl_handler.c - Handler for the PL/pgSQL * procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/pl/plpgsql/src/pl_reserved_kwlist.h b/src/pl/plpgsql/src/pl_reserved_kwlist.h index ce7b0c9d33121..a74fc73ce53f9 100644 --- a/src/pl/plpgsql/src/pl_reserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_reserved_kwlist.h @@ -7,7 +7,7 @@ * by the PG_KEYWORD macro, which is not defined in this file; it can * be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/pl/plpgsql/src/pl_reserved_kwlist.h @@ -33,7 +33,6 @@ PG_KEYWORD("case", K_CASE) PG_KEYWORD("declare", K_DECLARE) PG_KEYWORD("else", K_ELSE) PG_KEYWORD("end", K_END) -PG_KEYWORD("execute", K_EXECUTE) PG_KEYWORD("for", K_FOR) PG_KEYWORD("foreach", K_FOREACH) PG_KEYWORD("from", K_FROM) @@ -44,7 +43,6 @@ PG_KEYWORD("loop", K_LOOP) PG_KEYWORD("not", K_NOT) PG_KEYWORD("null", K_NULL) PG_KEYWORD("or", K_OR) -PG_KEYWORD("strict", K_STRICT) PG_KEYWORD("then", K_THEN) PG_KEYWORD("to", K_TO) PG_KEYWORD("using", K_USING) diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c index d08187dafcb4c..042dcb4c5cf53 100644 --- a/src/pl/plpgsql/src/pl_scanner.c +++ b/src/pl/plpgsql/src/pl_scanner.c @@ -4,7 +4,7 @@ * lexical scanning for PL/pgSQL * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -53,7 +53,7 @@ IdentifierLookup plpgsql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL; * We try to avoid reserving more keywords than we have to; but there's * little point in not reserving a word if it's reserved in the core grammar. * Currently, the following words are reserved here but not in the core: - * BEGIN BY DECLARE EXECUTE FOREACH IF LOOP STRICT WHILE + * BEGIN BY DECLARE FOREACH IF LOOP WHILE */ /* ScanKeywordList lookup data for PL/pgSQL keywords */ diff --git a/src/pl/plpgsql/src/pl_unreserved_kwlist.h b/src/pl/plpgsql/src/pl_unreserved_kwlist.h index 98f99ec470cf4..6379e86c8cbea 100644 --- a/src/pl/plpgsql/src/pl_unreserved_kwlist.h +++ b/src/pl/plpgsql/src/pl_unreserved_kwlist.h @@ -7,7 +7,7 @@ * by the PG_KEYWORD macro, which is not defined in this file; it can * be defined by the caller for special purposes. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/pl/plpgsql/src/pl_unreserved_kwlist.h @@ -58,6 +58,7 @@ PG_KEYWORD("elsif", K_ELSIF) PG_KEYWORD("errcode", K_ERRCODE) PG_KEYWORD("error", K_ERROR) PG_KEYWORD("exception", K_EXCEPTION) +PG_KEYWORD("execute", K_EXECUTE) PG_KEYWORD("exit", K_EXIT) PG_KEYWORD("fetch", K_FETCH) PG_KEYWORD("first", K_FIRST) @@ -103,6 +104,7 @@ PG_KEYWORD("scroll", K_SCROLL) PG_KEYWORD("slice", K_SLICE) PG_KEYWORD("sqlstate", K_SQLSTATE) PG_KEYWORD("stacked", K_STACKED) +PG_KEYWORD("strict", K_STRICT) PG_KEYWORD("table", K_TABLE) PG_KEYWORD("table_name", K_TABLE_NAME) PG_KEYWORD("type", K_TYPE) diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index 41e52b8ce7183..addb14a9959c7 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -3,7 +3,7 @@ * plpgsql.h - Definitions for the PL/pgSQL * procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -1307,10 +1307,7 @@ extern void plpgsql_dumptree(PLpgSQL_function *func); */ union YYSTYPE; #define YYLTYPE int -#ifndef YY_TYPEDEF_YY_SCANNER_T -#define YY_TYPEDEF_YY_SCANNER_T typedef void *yyscan_t; -#endif extern int plpgsql_yylex(union YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t yyscanner); extern int plpgsql_token_length(yyscan_t yyscanner); extern void plpgsql_push_back_token(int token, union YYSTYPE *yylvalp, YYLTYPE *yyllocp, yyscan_t yyscanner); diff --git a/src/pl/plpgsql/src/po/meson.build b/src/pl/plpgsql/src/po/meson.build index 7808a20ba8436..dd8f34f925a0b 100644 --- a/src/pl/plpgsql/src/po/meson.build +++ b/src/pl/plpgsql/src/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('plpgsql-' + pg_version_major.to_string())] diff --git a/src/pl/plpgsql/src/sql/plpgsql_domain.sql b/src/pl/plpgsql/src/sql/plpgsql_domain.sql index 8f99aae5a9ffc..4c5dd7dc7072d 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_domain.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_domain.sql @@ -277,3 +277,13 @@ $$ LANGUAGE plpgsql; SELECT * FROM test_assign_ordered_named_pairs(1,2,3); SELECT * FROM test_assign_ordered_named_pairs(2,1,3); SELECT * FROM test_assign_ordered_named_pairs(1,2,0); -- should fail someday + +CREATE FUNCTION test_null_ordered_named_pair() + RETURNS ordered_named_pair AS $$ +declare v ordered_named_pair; +begin +return v; +end +$$ LANGUAGE plpgsql; + +SELECT * FROM test_null_ordered_named_pair(); diff --git a/src/pl/plpgsql/src/sql/plpgsql_misc.sql b/src/pl/plpgsql/src/sql/plpgsql_misc.sql index d3a7f703a758d..0bc39fcf3257c 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_misc.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_misc.sql @@ -37,3 +37,32 @@ do $$ declare x foo.bar%rowtype; begin end $$; do $$ declare x foo.bar.baz%rowtype; begin end $$; do $$ declare x public.foo%rowtype; begin end $$; do $$ declare x public.misc_table%rowtype; begin end $$; + +-- Test handling of an unreserved keyword as a variable name +-- and record field name. +do $$ +declare + execute int; + r record; +begin + execute := 10; + raise notice 'execute = %', execute; + select 1 as strict into r; + raise notice 'r.strict = %', r.strict; +end $$; + +-- Test handling of a reserved keyword as a record field name. + +do $$ declare r record; +begin + select 1 as x, 2 as foreach into r; + raise notice 'r.x = %', r.x; + raise notice 'r.foreach = %', r.foreach; -- fails +end $$; + +do $$ declare r record; +begin + select 1 as x, 2 as foreach into r; + raise notice 'r.x = %', r.x; + raise notice 'r."foreach" = %', r."foreach"; -- ok +end $$; diff --git a/src/pl/plpgsql/src/sql/plpgsql_simple.sql b/src/pl/plpgsql/src/sql/plpgsql_simple.sql index 72d8afe4500d1..d64e791800bac 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_simple.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_simple.sql @@ -114,3 +114,20 @@ begin fetch p_CurData into val; raise notice 'val = %', val; end; $$; + +-- We now optimize "SELECT simple-expr INTO var" using the simple-expression +-- logic. Verify that error reporting works the same as it did before. + +do $$ +declare x bigint := 2^30; y int; +begin + -- overflow during assignment step does not get an extra context line + select x*x into y; +end $$; + +do $$ +declare x bigint := 2^30; y int; +begin + -- overflow during expression evaluation step does get an extra context line + select x*x*x into y; +end $$; diff --git a/src/pl/plpgsql/src/sql/plpgsql_trap.sql b/src/pl/plpgsql/src/sql/plpgsql_trap.sql index 5459b347e7f1f..731d592040ed2 100644 --- a/src/pl/plpgsql/src/sql/plpgsql_trap.sql +++ b/src/pl/plpgsql/src/sql/plpgsql_trap.sql @@ -85,13 +85,11 @@ drop table foo; create function trap_timeout() returns void as $$ begin - declare x int; begin - -- we assume this will take longer than 1 second: - select count(*) into x from generate_series(1, 1_000_000_000_000); + perform pg_sleep(10); exception when others then - raise notice 'caught others?'; + raise notice 'caught others: %', sqlerrm; when query_canceled then raise notice 'nyeah nyeah, can''t stop me'; end; diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile index f959083a0bdec..25f295c3709e2 100644 --- a/src/pl/plpython/Makefile +++ b/src/pl/plpython/Makefile @@ -11,7 +11,7 @@ ifeq ($(PORTNAME), win32) override python_libspec = endif -override CPPFLAGS := -I. -I$(srcdir) $(python_includespec) $(CPPFLAGS) +override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS) $(python_includespec) rpathdir = $(python_libdir) diff --git a/src/pl/plpython/expected/README b/src/pl/plpython/expected/README deleted file mode 100644 index 388c553a5890a..0000000000000 --- a/src/pl/plpython/expected/README +++ /dev/null @@ -1,3 +0,0 @@ -Guide to alternative expected files: - -plpython_error_5.out Python 3.5 and newer diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out index 68722b00097ed..96bbdfb95862c 100644 --- a/src/pl/plpython/expected/plpython_error.out +++ b/src/pl/plpython/expected/plpython_error.out @@ -63,7 +63,7 @@ SELECT exception_index_invalid_nested(); ERROR: spiexceptions.UndefinedFunction: function test5(unknown) does not exist LINE 1: SELECT test5('foo') ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no function of that name. QUERY: SELECT test5('foo') CONTEXT: Traceback (most recent call last): PL/Python function "exception_index_invalid_nested", line 1, in @@ -243,7 +243,7 @@ $$ plpy.nonexistent $$ LANGUAGE plpython3u; SELECT toplevel_attribute_error(); -ERROR: AttributeError: 'module' object has no attribute 'nonexistent' +ERROR: AttributeError: module 'plpy' has no attribute 'nonexistent' CONTEXT: Traceback (most recent call last): PL/Python function "toplevel_attribute_error", line 2, in plpy.nonexistent diff --git a/src/pl/plpython/expected/plpython_error_5.out b/src/pl/plpython/expected/plpython_error_5.out deleted file mode 100644 index fd9cd73be743a..0000000000000 --- a/src/pl/plpython/expected/plpython_error_5.out +++ /dev/null @@ -1,460 +0,0 @@ --- test error handling, i forgot to restore Warn_restart in --- the trigger handler once. the errors and subsequent core dump were --- interesting. -/* Flat out Python syntax error - */ -CREATE FUNCTION python_syntax_error() RETURNS text - AS -'.syntaxerror' - LANGUAGE plpython3u; -ERROR: could not compile PL/Python function "python_syntax_error" -DETAIL: SyntaxError: invalid syntax (, line 2) -/* With check_function_bodies = false the function should get defined - * and the error reported when called - */ -SET check_function_bodies = false; -CREATE FUNCTION python_syntax_error() RETURNS text - AS -'.syntaxerror' - LANGUAGE plpython3u; -SELECT python_syntax_error(); -ERROR: could not compile PL/Python function "python_syntax_error" -DETAIL: SyntaxError: invalid syntax (, line 2) -/* Run the function twice to check if the hashtable entry gets cleaned up */ -SELECT python_syntax_error(); -ERROR: could not compile PL/Python function "python_syntax_error" -DETAIL: SyntaxError: invalid syntax (, line 2) -RESET check_function_bodies; -/* Flat out syntax error - */ -CREATE FUNCTION sql_syntax_error() RETURNS text - AS -'plpy.execute("syntax error")' - LANGUAGE plpython3u; -SELECT sql_syntax_error(); -ERROR: spiexceptions.SyntaxError: syntax error at or near "syntax" -LINE 1: syntax error - ^ -QUERY: syntax error -CONTEXT: Traceback (most recent call last): - PL/Python function "sql_syntax_error", line 1, in - plpy.execute("syntax error") -PL/Python function "sql_syntax_error" -/* check the handling of uncaught python exceptions - */ -CREATE FUNCTION exception_index_invalid(text) RETURNS text - AS -'return args[1]' - LANGUAGE plpython3u; -SELECT exception_index_invalid('test'); -ERROR: IndexError: list index out of range -CONTEXT: Traceback (most recent call last): - PL/Python function "exception_index_invalid", line 1, in - return args[1] -PL/Python function "exception_index_invalid" -/* check handling of nested exceptions - */ -CREATE FUNCTION exception_index_invalid_nested() RETURNS text - AS -'rv = plpy.execute("SELECT test5(''foo'')") -return rv[0]' - LANGUAGE plpython3u; -SELECT exception_index_invalid_nested(); -ERROR: spiexceptions.UndefinedFunction: function test5(unknown) does not exist -LINE 1: SELECT test5('foo') - ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. -QUERY: SELECT test5('foo') -CONTEXT: Traceback (most recent call last): - PL/Python function "exception_index_invalid_nested", line 1, in - rv = plpy.execute("SELECT test5('foo')") -PL/Python function "exception_index_invalid_nested" -/* a typo - */ -CREATE FUNCTION invalid_type_uncaught(a text) RETURNS text - AS -'if "plan" not in SD: - q = "SELECT fname FROM users WHERE lname = $1" - SD["plan"] = plpy.prepare(q, [ "test" ]) -rv = plpy.execute(SD["plan"], [ a ]) -if len(rv): - return rv[0]["fname"] -return None -' - LANGUAGE plpython3u; -SELECT invalid_type_uncaught('rick'); -ERROR: spiexceptions.UndefinedObject: type "test" does not exist -CONTEXT: Traceback (most recent call last): - PL/Python function "invalid_type_uncaught", line 3, in - SD["plan"] = plpy.prepare(q, [ "test" ]) -PL/Python function "invalid_type_uncaught" -/* for what it's worth catch the exception generated by - * the typo, and return None - */ -CREATE FUNCTION invalid_type_caught(a text) RETURNS text - AS -'if "plan" not in SD: - q = "SELECT fname FROM users WHERE lname = $1" - try: - SD["plan"] = plpy.prepare(q, [ "test" ]) - except plpy.SPIError as ex: - plpy.notice(str(ex)) - return None -rv = plpy.execute(SD["plan"], [ a ]) -if len(rv): - return rv[0]["fname"] -return None -' - LANGUAGE plpython3u; -SELECT invalid_type_caught('rick'); -NOTICE: type "test" does not exist - invalid_type_caught ---------------------- - -(1 row) - -/* for what it's worth catch the exception generated by - * the typo, and reraise it as a plain error - */ -CREATE FUNCTION invalid_type_reraised(a text) RETURNS text - AS -'if "plan" not in SD: - q = "SELECT fname FROM users WHERE lname = $1" - try: - SD["plan"] = plpy.prepare(q, [ "test" ]) - except plpy.SPIError as ex: - plpy.error(str(ex)) -rv = plpy.execute(SD["plan"], [ a ]) -if len(rv): - return rv[0]["fname"] -return None -' - LANGUAGE plpython3u; -SELECT invalid_type_reraised('rick'); -ERROR: plpy.Error: type "test" does not exist -CONTEXT: Traceback (most recent call last): - PL/Python function "invalid_type_reraised", line 6, in - plpy.error(str(ex)) -PL/Python function "invalid_type_reraised" -/* no typo no messing about - */ -CREATE FUNCTION valid_type(a text) RETURNS text - AS -'if "plan" not in SD: - SD["plan"] = plpy.prepare("SELECT fname FROM users WHERE lname = $1", [ "text" ]) -rv = plpy.execute(SD["plan"], [ a ]) -if len(rv): - return rv[0]["fname"] -return None -' - LANGUAGE plpython3u; -SELECT valid_type('rick'); - valid_type ------------- - -(1 row) - -/* error in nested functions to get a traceback -*/ -CREATE FUNCTION nested_error() RETURNS text - AS -'def fun1(): - plpy.error("boom") - -def fun2(): - fun1() - -def fun3(): - fun2() - -fun3() -return "not reached" -' - LANGUAGE plpython3u; -SELECT nested_error(); -ERROR: plpy.Error: boom -CONTEXT: Traceback (most recent call last): - PL/Python function "nested_error", line 10, in - fun3() - PL/Python function "nested_error", line 8, in fun3 - fun2() - PL/Python function "nested_error", line 5, in fun2 - fun1() - PL/Python function "nested_error", line 2, in fun1 - plpy.error("boom") -PL/Python function "nested_error" -/* raising plpy.Error is just like calling plpy.error -*/ -CREATE FUNCTION nested_error_raise() RETURNS text - AS -'def fun1(): - raise plpy.Error("boom") - -def fun2(): - fun1() - -def fun3(): - fun2() - -fun3() -return "not reached" -' - LANGUAGE plpython3u; -SELECT nested_error_raise(); -ERROR: plpy.Error: boom -CONTEXT: Traceback (most recent call last): - PL/Python function "nested_error_raise", line 10, in - fun3() - PL/Python function "nested_error_raise", line 8, in fun3 - fun2() - PL/Python function "nested_error_raise", line 5, in fun2 - fun1() - PL/Python function "nested_error_raise", line 2, in fun1 - raise plpy.Error("boom") -PL/Python function "nested_error_raise" -/* using plpy.warning should not produce a traceback -*/ -CREATE FUNCTION nested_warning() RETURNS text - AS -'def fun1(): - plpy.warning("boom") - -def fun2(): - fun1() - -def fun3(): - fun2() - -fun3() -return "you''ve been warned" -' - LANGUAGE plpython3u; -SELECT nested_warning(); -WARNING: boom - nested_warning --------------------- - you've been warned -(1 row) - -/* AttributeError at toplevel used to give segfaults with the traceback -*/ -CREATE FUNCTION toplevel_attribute_error() RETURNS void AS -$$ -plpy.nonexistent -$$ LANGUAGE plpython3u; -SELECT toplevel_attribute_error(); -ERROR: AttributeError: module 'plpy' has no attribute 'nonexistent' -CONTEXT: Traceback (most recent call last): - PL/Python function "toplevel_attribute_error", line 2, in - plpy.nonexistent -PL/Python function "toplevel_attribute_error" -/* Calling PL/Python functions from SQL and vice versa should not lose context. - */ -CREATE OR REPLACE FUNCTION python_traceback() RETURNS void AS $$ -def first(): - second() - -def second(): - third() - -def third(): - plpy.execute("select sql_error()") - -first() -$$ LANGUAGE plpython3u; -CREATE OR REPLACE FUNCTION sql_error() RETURNS void AS $$ -begin - select 1/0; -end -$$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION python_from_sql_error() RETURNS void AS $$ -begin - select python_traceback(); -end -$$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION sql_from_python_error() RETURNS void AS $$ -plpy.execute("select sql_error()") -$$ LANGUAGE plpython3u; -SELECT python_traceback(); -ERROR: spiexceptions.DivisionByZero: division by zero -CONTEXT: Traceback (most recent call last): - PL/Python function "python_traceback", line 11, in - first() - PL/Python function "python_traceback", line 3, in first - second() - PL/Python function "python_traceback", line 6, in second - third() - PL/Python function "python_traceback", line 9, in third - plpy.execute("select sql_error()") -PL/Python function "python_traceback" -SELECT sql_error(); -ERROR: division by zero -CONTEXT: SQL statement "select 1/0" -PL/pgSQL function sql_error() line 3 at SQL statement -SELECT python_from_sql_error(); -ERROR: spiexceptions.DivisionByZero: division by zero -CONTEXT: Traceback (most recent call last): - PL/Python function "python_traceback", line 11, in - first() - PL/Python function "python_traceback", line 3, in first - second() - PL/Python function "python_traceback", line 6, in second - third() - PL/Python function "python_traceback", line 9, in third - plpy.execute("select sql_error()") -PL/Python function "python_traceback" -SQL statement "select python_traceback()" -PL/pgSQL function python_from_sql_error() line 3 at SQL statement -SELECT sql_from_python_error(); -ERROR: spiexceptions.DivisionByZero: division by zero -CONTEXT: Traceback (most recent call last): - PL/Python function "sql_from_python_error", line 2, in - plpy.execute("select sql_error()") -PL/Python function "sql_from_python_error" -/* check catching specific types of exceptions - */ -CREATE TABLE specific ( - i integer PRIMARY KEY -); -CREATE FUNCTION specific_exception(i integer) RETURNS void AS -$$ -from plpy import spiexceptions -try: - plpy.execute("insert into specific values (%s)" % (i or "NULL")); -except spiexceptions.NotNullViolation as e: - plpy.notice("Violated the NOT NULL constraint, sqlstate %s" % e.sqlstate) -except spiexceptions.UniqueViolation as e: - plpy.notice("Violated the UNIQUE constraint, sqlstate %s" % e.sqlstate) -$$ LANGUAGE plpython3u; -SELECT specific_exception(2); - specific_exception --------------------- - -(1 row) - -SELECT specific_exception(NULL); -NOTICE: Violated the NOT NULL constraint, sqlstate 23502 - specific_exception --------------------- - -(1 row) - -SELECT specific_exception(2); -NOTICE: Violated the UNIQUE constraint, sqlstate 23505 - specific_exception --------------------- - -(1 row) - -/* SPI errors in PL/Python functions should preserve the SQLSTATE value - */ -CREATE FUNCTION python_unique_violation() RETURNS void AS $$ -plpy.execute("insert into specific values (1)") -plpy.execute("insert into specific values (1)") -$$ LANGUAGE plpython3u; -CREATE FUNCTION catch_python_unique_violation() RETURNS text AS $$ -begin - begin - perform python_unique_violation(); - exception when unique_violation then - return 'ok'; - end; - return 'not reached'; -end; -$$ language plpgsql; -SELECT catch_python_unique_violation(); - catch_python_unique_violation -------------------------------- - ok -(1 row) - -/* manually starting subtransactions - a bad idea - */ -CREATE FUNCTION manual_subxact() RETURNS void AS $$ -plpy.execute("savepoint save") -plpy.execute("create table foo(x integer)") -plpy.execute("rollback to save") -$$ LANGUAGE plpython3u; -SELECT manual_subxact(); -ERROR: plpy.SPIError: SPI_execute failed: SPI_ERROR_TRANSACTION -CONTEXT: Traceback (most recent call last): - PL/Python function "manual_subxact", line 2, in - plpy.execute("savepoint save") -PL/Python function "manual_subxact" -/* same for prepared plans - */ -CREATE FUNCTION manual_subxact_prepared() RETURNS void AS $$ -save = plpy.prepare("savepoint save") -rollback = plpy.prepare("rollback to save") -plpy.execute(save) -plpy.execute("create table foo(x integer)") -plpy.execute(rollback) -$$ LANGUAGE plpython3u; -SELECT manual_subxact_prepared(); -ERROR: plpy.SPIError: SPI_execute_plan failed: SPI_ERROR_TRANSACTION -CONTEXT: Traceback (most recent call last): - PL/Python function "manual_subxact_prepared", line 4, in - plpy.execute(save) -PL/Python function "manual_subxact_prepared" -/* raising plpy.spiexception.* from python code should preserve sqlstate - */ -CREATE FUNCTION plpy_raise_spiexception() RETURNS void AS $$ -raise plpy.spiexceptions.DivisionByZero() -$$ LANGUAGE plpython3u; -DO $$ -BEGIN - SELECT plpy_raise_spiexception(); -EXCEPTION WHEN division_by_zero THEN - -- NOOP -END -$$ LANGUAGE plpgsql; -/* setting a custom sqlstate should be handled - */ -CREATE FUNCTION plpy_raise_spiexception_override() RETURNS void AS $$ -exc = plpy.spiexceptions.DivisionByZero() -exc.sqlstate = 'SILLY' -raise exc -$$ LANGUAGE plpython3u; -DO $$ -BEGIN - SELECT plpy_raise_spiexception_override(); -EXCEPTION WHEN SQLSTATE 'SILLY' THEN - -- NOOP -END -$$ LANGUAGE plpgsql; -/* test the context stack trace for nested execution levels - */ -CREATE FUNCTION notice_innerfunc() RETURNS int AS $$ -plpy.execute("DO LANGUAGE plpython3u $x$ plpy.notice('inside DO') $x$") -return 1 -$$ LANGUAGE plpython3u; -CREATE FUNCTION notice_outerfunc() RETURNS int AS $$ -plpy.execute("SELECT notice_innerfunc()") -return 1 -$$ LANGUAGE plpython3u; -\set SHOW_CONTEXT always -SELECT notice_outerfunc(); -NOTICE: inside DO -CONTEXT: PL/Python anonymous code block -SQL statement "DO LANGUAGE plpython3u $x$ plpy.notice('inside DO') $x$" -PL/Python function "notice_innerfunc" -SQL statement "SELECT notice_innerfunc()" -PL/Python function "notice_outerfunc" - notice_outerfunc ------------------- - 1 -(1 row) - -/* test error logged with an underlying exception that includes a detail - * string (bug #18070). - */ -CREATE FUNCTION python_error_detail() RETURNS SETOF text AS $$ - plan = plpy.prepare("SELECT to_date('xy', 'DD') d") - for row in plpy.cursor(plan): - yield row['d'] -$$ LANGUAGE plpython3u; -SELECT python_error_detail(); -ERROR: error fetching next item from iterator -DETAIL: spiexceptions.InvalidDatetimeFormat: invalid value "xy" for "DD" -CONTEXT: Traceback (most recent call last): -PL/Python function "python_error_detail" diff --git a/src/pl/plpython/expected/plpython_trigger.out b/src/pl/plpython/expected/plpython_trigger.out index 64eab2fa3f4b5..bd35b220c5eda 100644 --- a/src/pl/plpython/expected/plpython_trigger.out +++ b/src/pl/plpython/expected/plpython_trigger.out @@ -646,3 +646,30 @@ SELECT * FROM recursive_trigger_test; 1 | 2 (2 rows) +-- event triggers +CREATE OR REPLACE FUNCTION pysnitch() RETURNS event_trigger +LANGUAGE plpython3u +AS $$ + plpy.notice("TD[event] => " + TD["event"] + " ; TD[tag] => " + TD["tag"]); +$$; +CREATE EVENT TRIGGER python_a_snitch ON ddl_command_start + EXECUTE PROCEDURE pysnitch(); +CREATE EVENT TRIGGER python_b_snitch ON ddl_command_end + EXECUTE PROCEDURE pysnitch(); +CREATE OR REPLACE FUNCTION foobar() RETURNS int LANGUAGE sql AS $$SELECT 1;$$; +NOTICE: TD[event] => ddl_command_start ; TD[tag] => CREATE FUNCTION +NOTICE: TD[event] => ddl_command_end ; TD[tag] => CREATE FUNCTION +ALTER FUNCTION foobar() COST 77; +NOTICE: TD[event] => ddl_command_start ; TD[tag] => ALTER FUNCTION +NOTICE: TD[event] => ddl_command_end ; TD[tag] => ALTER FUNCTION +DROP FUNCTION foobar(); +NOTICE: TD[event] => ddl_command_start ; TD[tag] => DROP FUNCTION +NOTICE: TD[event] => ddl_command_end ; TD[tag] => DROP FUNCTION +CREATE TABLE foo(); +NOTICE: TD[event] => ddl_command_start ; TD[tag] => CREATE TABLE +NOTICE: TD[event] => ddl_command_end ; TD[tag] => CREATE TABLE +DROP TABLE foo; +NOTICE: TD[event] => ddl_command_start ; TD[tag] => DROP TABLE +NOTICE: TD[event] => ddl_command_end ; TD[tag] => DROP TABLE +DROP EVENT TRIGGER python_a_snitch; +DROP EVENT TRIGGER python_b_snitch; diff --git a/src/pl/plpython/expected/plpython_unicode.out b/src/pl/plpython/expected/plpython_unicode.out index fd54b0b88e8f8..bd8d9c561c956 100644 --- a/src/pl/plpython/expected/plpython_unicode.out +++ b/src/pl/plpython/expected/plpython_unicode.out @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; CREATE TABLE unicode_test ( testvalue text NOT NULL diff --git a/src/pl/plpython/expected/plpython_unicode_1.out b/src/pl/plpython/expected/plpython_unicode_1.out new file mode 100644 index 0000000000000..f8b21fd7eb7ab --- /dev/null +++ b/src/pl/plpython/expected/plpython_unicode_1.out @@ -0,0 +1,12 @@ +-- +-- Unicode handling +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +-- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/pl/plpython/generate-spiexceptions.pl b/src/pl/plpython/generate-spiexceptions.pl index a0df785c725ed..35899344920c5 100644 --- a/src/pl/plpython/generate-spiexceptions.pl +++ b/src/pl/plpython/generate-spiexceptions.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate the spiexceptions.h header from errcodes.txt -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/pl/plpython/meson.build b/src/pl/plpython/meson.build index 709e5932a93a8..ef8aba5653935 100644 --- a/src/pl/plpython/meson.build +++ b/src/pl/plpython/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not python3_dep.found() subdir_done() diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c index 37d7efca77ce5..cc74c4df6ba67 100644 --- a/src/pl/plpython/plpy_cursorobject.c +++ b/src/pl/plpython/plpy_cursorobject.c @@ -58,9 +58,9 @@ static PyType_Slot PLyCursor_slots[] = static PyType_Spec PLyCursor_spec = { .name = "PLyCursor", - .basicsize = sizeof(PLyCursorObject), - .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .slots = PLyCursor_slots, + .basicsize = sizeof(PLyCursorObject), + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .slots = PLyCursor_slots, }; static PyTypeObject *PLy_CursorType; diff --git a/src/pl/plpython/plpy_elog.c b/src/pl/plpython/plpy_elog.c index ddf3573f0e70b..e109888cceda5 100644 --- a/src/pl/plpython/plpy_elog.c +++ b/src/pl/plpython/plpy_elog.c @@ -18,7 +18,8 @@ PyObject *PLy_exc_spi_error = NULL; static void PLy_traceback(PyObject *e, PyObject *v, PyObject *tb, - char **xmsg, char **tbmsg, int *tb_depth); + char *volatile *xmsg, char *volatile *tbmsg, + int *tb_depth); static void PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hint, char **query, int *position, char **schema_name, char **table_name, char **column_name, @@ -43,78 +44,82 @@ void PLy_elog_impl(int elevel, const char *fmt,...) { int save_errno = errno; - char *xmsg; - char *tbmsg; + char *volatile xmsg = NULL; + char *volatile tbmsg = NULL; int tb_depth; StringInfoData emsg; PyObject *exc, *val, *tb; - const char *primary = NULL; - int sqlerrcode = 0; - char *detail = NULL; - char *hint = NULL; - char *query = NULL; - int position = 0; - char *schema_name = NULL; - char *table_name = NULL; - char *column_name = NULL; - char *datatype_name = NULL; - char *constraint_name = NULL; + + /* If we'll need emsg, must initialize it before entering PG_TRY */ + if (fmt) + initStringInfo(&emsg); PyErr_Fetch(&exc, &val, &tb); - if (exc != NULL) + /* Use a PG_TRY block to ensure we release the PyObjects just acquired */ + PG_TRY(); { - PyErr_NormalizeException(&exc, &val, &tb); - - if (PyErr_GivenExceptionMatches(val, PLy_exc_spi_error)) - PLy_get_spi_error_data(val, &sqlerrcode, - &detail, &hint, &query, &position, + const char *primary = NULL; + int sqlerrcode = 0; + char *detail = NULL; + char *hint = NULL; + char *query = NULL; + int position = 0; + char *schema_name = NULL; + char *table_name = NULL; + char *column_name = NULL; + char *datatype_name = NULL; + char *constraint_name = NULL; + + if (exc != NULL) + { + PyErr_NormalizeException(&exc, &val, &tb); + + if (PyErr_GivenExceptionMatches(val, PLy_exc_spi_error)) + PLy_get_spi_error_data(val, &sqlerrcode, + &detail, &hint, &query, &position, + &schema_name, &table_name, &column_name, + &datatype_name, &constraint_name); + else if (PyErr_GivenExceptionMatches(val, PLy_exc_error)) + PLy_get_error_data(val, &sqlerrcode, &detail, &hint, &schema_name, &table_name, &column_name, &datatype_name, &constraint_name); - else if (PyErr_GivenExceptionMatches(val, PLy_exc_error)) - PLy_get_error_data(val, &sqlerrcode, &detail, &hint, - &schema_name, &table_name, &column_name, - &datatype_name, &constraint_name); - else if (PyErr_GivenExceptionMatches(val, PLy_exc_fatal)) - elevel = FATAL; - } + else if (PyErr_GivenExceptionMatches(val, PLy_exc_fatal)) + elevel = FATAL; + } - /* this releases our refcount on tb! */ - PLy_traceback(exc, val, tb, - &xmsg, &tbmsg, &tb_depth); + PLy_traceback(exc, val, tb, + &xmsg, &tbmsg, &tb_depth); - if (fmt) - { - initStringInfo(&emsg); - for (;;) + if (fmt) { - va_list ap; - int needed; - - errno = save_errno; - va_start(ap, fmt); - needed = appendStringInfoVA(&emsg, dgettext(TEXTDOMAIN, fmt), ap); - va_end(ap); - if (needed == 0) - break; - enlargeStringInfo(&emsg, needed); - } - primary = emsg.data; + for (;;) + { + va_list ap; + int needed; + + errno = save_errno; + va_start(ap, fmt); + needed = appendStringInfoVA(&emsg, dgettext(TEXTDOMAIN, fmt), ap); + va_end(ap); + if (needed == 0) + break; + enlargeStringInfo(&emsg, needed); + } + primary = emsg.data; - /* If there's an exception message, it goes in the detail. */ - if (xmsg) - detail = xmsg; - } - else - { - if (xmsg) - primary = xmsg; - } + /* If there's an exception message, it goes in the detail. */ + if (xmsg) + detail = xmsg; + } + else + { + if (xmsg) + primary = xmsg; + } - PG_TRY(); - { ereport(elevel, (errcode(sqlerrcode ? sqlerrcode : ERRCODE_EXTERNAL_ROUTINE_EXCEPTION), errmsg_internal("%s", primary ? primary : "no exception data"), @@ -136,14 +141,16 @@ PLy_elog_impl(int elevel, const char *fmt,...) } PG_FINALLY(); { + Py_XDECREF(exc); + Py_XDECREF(val); + Py_XDECREF(tb); + /* For neatness' sake, also release our string buffers */ if (fmt) pfree(emsg.data); if (xmsg) pfree(xmsg); if (tbmsg) pfree(tbmsg); - Py_XDECREF(exc); - Py_XDECREF(val); } PG_END_TRY(); } @@ -154,21 +161,14 @@ PLy_elog_impl(int elevel, const char *fmt,...) * The exception error message is returned in xmsg, the traceback in * tbmsg (both as palloc'd strings) and the traceback depth in * tb_depth. - * - * We release refcounts on all the Python objects in the traceback stack, - * but not on e or v. */ static void PLy_traceback(PyObject *e, PyObject *v, PyObject *tb, - char **xmsg, char **tbmsg, int *tb_depth) + char *volatile *xmsg, char *volatile *tbmsg, int *tb_depth) { - PyObject *e_type_o; - PyObject *e_module_o; - char *e_type_s = NULL; - char *e_module_s = NULL; - PyObject *vob = NULL; - char *vstr; - StringInfoData xstr; + PyObject *volatile e_type_o = NULL; + PyObject *volatile e_module_o = NULL; + PyObject *volatile vob = NULL; StringInfoData tbstr; /* @@ -186,47 +186,59 @@ PLy_traceback(PyObject *e, PyObject *v, PyObject *tb, /* * Format the exception and its value and put it in xmsg. */ - - e_type_o = PyObject_GetAttrString(e, "__name__"); - e_module_o = PyObject_GetAttrString(e, "__module__"); - if (e_type_o) - e_type_s = PLyUnicode_AsString(e_type_o); - if (e_type_s) - e_module_s = PLyUnicode_AsString(e_module_o); - - if (v && ((vob = PyObject_Str(v)) != NULL)) - vstr = PLyUnicode_AsString(vob); - else - vstr = "unknown"; - - initStringInfo(&xstr); - if (!e_type_s || !e_module_s) + PG_TRY(); { - /* shouldn't happen */ - appendStringInfoString(&xstr, "unrecognized exception"); + char *e_type_s = NULL; + char *e_module_s = NULL; + const char *vstr; + StringInfoData xstr; + + e_type_o = PyObject_GetAttrString(e, "__name__"); + e_module_o = PyObject_GetAttrString(e, "__module__"); + if (e_type_o) + e_type_s = PLyUnicode_AsString(e_type_o); + if (e_module_o) + e_module_s = PLyUnicode_AsString(e_module_o); + + if (v && ((vob = PyObject_Str(v)) != NULL)) + vstr = PLyUnicode_AsString(vob); + else + vstr = "unknown"; + + initStringInfo(&xstr); + if (!e_type_s || !e_module_s) + { + /* shouldn't happen */ + appendStringInfoString(&xstr, "unrecognized exception"); + } + /* mimics behavior of traceback.format_exception_only */ + else if (strcmp(e_module_s, "builtins") == 0 + || strcmp(e_module_s, "__main__") == 0 + || strcmp(e_module_s, "exceptions") == 0) + appendStringInfoString(&xstr, e_type_s); + else + appendStringInfo(&xstr, "%s.%s", e_module_s, e_type_s); + appendStringInfo(&xstr, ": %s", vstr); + + *xmsg = xstr.data; } - /* mimics behavior of traceback.format_exception_only */ - else if (strcmp(e_module_s, "builtins") == 0 - || strcmp(e_module_s, "__main__") == 0 - || strcmp(e_module_s, "exceptions") == 0) - appendStringInfoString(&xstr, e_type_s); - else - appendStringInfo(&xstr, "%s.%s", e_module_s, e_type_s); - appendStringInfo(&xstr, ": %s", vstr); - - *xmsg = xstr.data; + PG_FINALLY(); + { + Py_XDECREF(e_type_o); + Py_XDECREF(e_module_o); + Py_XDECREF(vob); + } + PG_END_TRY(); /* * Now format the traceback and put it in tbmsg. */ - *tb_depth = 0; initStringInfo(&tbstr); /* Mimic Python traceback reporting as close as possible. */ appendStringInfoString(&tbstr, "Traceback (most recent call last):"); while (tb != NULL && tb != Py_None) { - PyObject *volatile tb_prev = NULL; PyObject *volatile frame = NULL; PyObject *volatile code = NULL; PyObject *volatile name = NULL; @@ -254,95 +266,92 @@ PLy_traceback(PyObject *e, PyObject *v, PyObject *tb, filename = PyObject_GetAttrString(code, "co_filename"); if (filename == NULL) elog(ERROR, "could not get file name from Python code object"); + + /* The first frame always points at , skip it. */ + if (*tb_depth > 0) + { + PLyExecutionContext *exec_ctx = PLy_current_execution_context(); + char *proname; + char *fname; + char *line; + char *plain_filename; + long plain_lineno; + + /* + * The second frame points at the internal function, but to + * mimic Python error reporting we want to say . + */ + if (*tb_depth == 1) + fname = ""; + else + fname = PLyUnicode_AsString(name); + + proname = PLy_procedure_name(exec_ctx->curr_proc); + plain_filename = PLyUnicode_AsString(filename); + plain_lineno = PyLong_AsLong(lineno); + + if (proname == NULL) + appendStringInfo(&tbstr, "\n PL/Python anonymous code block, line %ld, in %s", + plain_lineno - 1, fname); + else + appendStringInfo(&tbstr, "\n PL/Python function \"%s\", line %ld, in %s", + proname, plain_lineno - 1, fname); + + /* + * function code object was compiled with "" as the + * filename + */ + if (exec_ctx->curr_proc && plain_filename != NULL && + strcmp(plain_filename, "") == 0) + { + /* + * If we know the current procedure, append the exact line + * from the source, again mimicking Python's traceback.py + * module behavior. We could store the already line-split + * source to avoid splitting it every time, but producing + * a traceback is not the most important scenario to + * optimize for. But we do not go as far as traceback.py + * in reading the source of imported modules. + */ + line = get_source_line(exec_ctx->curr_proc->src, plain_lineno); + if (line) + { + appendStringInfo(&tbstr, "\n %s", line); + pfree(line); + } + } + } } - PG_CATCH(); + PG_FINALLY(); { Py_XDECREF(frame); Py_XDECREF(code); Py_XDECREF(name); Py_XDECREF(lineno); Py_XDECREF(filename); - PG_RE_THROW(); } PG_END_TRY(); - /* The first frame always points at , skip it. */ - if (*tb_depth > 0) - { - PLyExecutionContext *exec_ctx = PLy_current_execution_context(); - char *proname; - char *fname; - char *line; - char *plain_filename; - long plain_lineno; - - /* - * The second frame points at the internal function, but to mimic - * Python error reporting we want to say . - */ - if (*tb_depth == 1) - fname = ""; - else - fname = PLyUnicode_AsString(name); - - proname = PLy_procedure_name(exec_ctx->curr_proc); - plain_filename = PLyUnicode_AsString(filename); - plain_lineno = PyLong_AsLong(lineno); - - if (proname == NULL) - appendStringInfo(&tbstr, "\n PL/Python anonymous code block, line %ld, in %s", - plain_lineno - 1, fname); - else - appendStringInfo(&tbstr, "\n PL/Python function \"%s\", line %ld, in %s", - proname, plain_lineno - 1, fname); - - /* - * function code object was compiled with "" as the - * filename - */ - if (exec_ctx->curr_proc && plain_filename != NULL && - strcmp(plain_filename, "") == 0) - { - /* - * If we know the current procedure, append the exact line - * from the source, again mimicking Python's traceback.py - * module behavior. We could store the already line-split - * source to avoid splitting it every time, but producing a - * traceback is not the most important scenario to optimize - * for. But we do not go as far as traceback.py in reading - * the source of imported modules. - */ - line = get_source_line(exec_ctx->curr_proc->src, plain_lineno); - if (line) - { - appendStringInfo(&tbstr, "\n %s", line); - pfree(line); - } - } - } - - Py_DECREF(frame); - Py_DECREF(code); - Py_DECREF(name); - Py_DECREF(lineno); - Py_DECREF(filename); - - /* Release the current frame and go to the next one. */ - tb_prev = tb; + /* Advance to the next frame. */ tb = PyObject_GetAttrString(tb, "tb_next"); - Assert(tb_prev != Py_None); - Py_DECREF(tb_prev); if (tb == NULL) elog(ERROR, "could not traverse Python traceback"); + + /* + * Release the refcount that PyObject_GetAttrString acquired on the + * next frame object. We don't need it, because our caller has a + * refcount on the first frame object and the frame objects each have + * a refcount on the next one. If we tried to hold this refcount + * longer, it would greatly complicate cleanup in the event of a + * failure in the above PG_TRY block. + */ + Py_DECREF(tb); + (*tb_depth)++; } /* Return the traceback. */ *tbmsg = tbstr.data; - - Py_XDECREF(e_type_o); - Py_XDECREF(e_module_o); - Py_XDECREF(vob); } /* @@ -371,6 +380,10 @@ PLy_get_sqlerrcode(PyObject *exc, int *sqlerrcode) /* * Extract the error data from a SPIError + * + * Note: the returned string values are pointers into the given PyObject. + * They must not be free()'d, and are not guaranteed to be valid once + * we stop holding a reference on the PyObject. */ static void PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, @@ -407,6 +420,11 @@ PLy_get_spi_error_data(PyObject *exc, int *sqlerrcode, char **detail, * * Note: position and query attributes are never set for Error so, unlike * PLy_get_spi_error_data, this function doesn't return them. + * + * Note: the returned string values are palloc'd in the current context. + * While our caller could pfree them later, there's no real need to do so, + * and it would be complicated to handle both this convention and that of + * PLy_get_spi_error_data. */ static void PLy_get_error_data(PyObject *exc, int *sqlerrcode, char **detail, char **hint, @@ -566,7 +584,7 @@ get_string_attr(PyObject *obj, char *attrname, char **str) val = PyObject_GetAttrString(obj, attrname); if (val != NULL && val != Py_None) { - *str = pstrdup(PLyUnicode_AsString(val)); + *str = PLyUnicode_AsString(val); } Py_XDECREF(val); } diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c index 28fbd443b98c9..0117f1e77efa6 100644 --- a/src/pl/plpython/plpy_exec.c +++ b/src/pl/plpython/plpy_exec.c @@ -9,6 +9,7 @@ #include "access/htup_details.h" #include "access/xact.h" #include "catalog/pg_type.h" +#include "commands/event_trigger.h" #include "commands/trigger.h" #include "executor/spi.h" #include "funcapi.h" @@ -427,6 +428,47 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc) return rv; } +/* + * event trigger subhandler + */ +void +PLy_exec_event_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc) +{ + EventTriggerData *tdata; + PyObject *volatile pltdata = NULL; + + Assert(CALLED_AS_EVENT_TRIGGER(fcinfo)); + tdata = (EventTriggerData *) fcinfo->context; + + PG_TRY(); + { + PyObject *pltevent, + *plttag; + + pltdata = PyDict_New(); + if (!pltdata) + PLy_elog(ERROR, NULL); + + pltevent = PLyUnicode_FromString(tdata->event); + PyDict_SetItemString(pltdata, "event", pltevent); + Py_DECREF(pltevent); + + plttag = PLyUnicode_FromString(GetCommandTagName(tdata->tag)); + PyDict_SetItemString(pltdata, "tag", plttag); + Py_DECREF(plttag); + + PLy_procedure_call(proc, "TD", pltdata); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish() failed"); + } + PG_FINALLY(); + { + Py_XDECREF(pltdata); + } + PG_END_TRY(); +} + /* helper functions for Python code execution */ static PyObject * @@ -509,7 +551,7 @@ PLy_function_save_args(PLyProcedure *proc) Py_XINCREF(result->args); /* If it's a trigger, also save "TD" */ - if (proc->is_trigger) + if (proc->is_trigger == PLPY_TRIGGER) { result->td = PyDict_GetItemString(proc->globals, "TD"); Py_XINCREF(result->td); @@ -1004,7 +1046,7 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata, Py_INCREF(plval); /* We assume proc->result is set up to convert tuples properly */ - att = &proc->result.u.tuple.atts[attn - 1]; + att = &proc->result.tuple.atts[attn - 1]; modvalues[attn - 1] = PLy_output_convert(att, plval, diff --git a/src/pl/plpython/plpy_exec.h b/src/pl/plpython/plpy_exec.h index 68da1ffcb2ef1..f35eabbd8ee8e 100644 --- a/src/pl/plpython/plpy_exec.h +++ b/src/pl/plpython/plpy_exec.h @@ -9,5 +9,6 @@ extern Datum PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc); extern HeapTuple PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc); +extern void PLy_exec_event_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc); #endif /* PLPY_EXEC_H */ diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c index f36eadbadc66d..9f07c115f8000 100644 --- a/src/pl/plpython/plpy_main.c +++ b/src/pl/plpython/plpy_main.c @@ -9,6 +9,7 @@ #include "access/htup_details.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" +#include "commands/event_trigger.h" #include "commands/trigger.h" #include "executor/spi.h" #include "miscadmin.h" @@ -38,18 +39,13 @@ PG_FUNCTION_INFO_V1(plpython3_call_handler); PG_FUNCTION_INFO_V1(plpython3_inline_handler); -static bool PLy_procedure_is_trigger(Form_pg_proc procStruct); +static PLyTrigType PLy_procedure_is_trigger(Form_pg_proc procStruct); static void plpython_error_callback(void *arg); static void plpython_inline_error_callback(void *arg); -static void PLy_init_interp(void); static PLyExecutionContext *PLy_push_execution_context(bool atomic_context); static void PLy_pop_execution_context(void); -/* static state for Python library conflict detection */ -static int *plpython_version_bitmask_ptr = NULL; -static int plpython_version_bitmask = 0; - /* initialize global variables */ PyObject *PLy_interp_globals = NULL; @@ -60,101 +56,58 @@ static PLyExecutionContext *PLy_execution_contexts = NULL; void _PG_init(void) { - int **bitmask_ptr; - - /* - * Set up a shared bitmask variable telling which Python version(s) are - * loaded into this process's address space. If there's more than one, we - * cannot call into libpython for fear of causing crashes. But postpone - * the actual failure for later, so that operations like pg_restore can - * load more than one plpython library so long as they don't try to do - * anything much with the language. - * - * While we only support Python 3 these days, somebody might create an - * out-of-tree version adding back support for Python 2. Conflicts with - * such an extension should be detected. - */ - bitmask_ptr = (int **) find_rendezvous_variable("plpython_version_bitmask"); - if (!(*bitmask_ptr)) /* am I the first? */ - *bitmask_ptr = &plpython_version_bitmask; - /* Retain pointer to the agreed-on shared variable ... */ - plpython_version_bitmask_ptr = *bitmask_ptr; - /* ... and announce my presence */ - *plpython_version_bitmask_ptr |= (1 << PY_MAJOR_VERSION); + PyObject *main_mod; + PyObject *main_dict; + PyObject *GD; + PyObject *plpy_mod; - /* - * This should be safe even in the presence of conflicting plpythons, and - * it's necessary to do it before possibly throwing a conflict error, or - * the error message won't get localized. - */ pg_bindtextdomain(TEXTDOMAIN); -} -/* - * Perform one-time setup of PL/Python, after checking for a conflict - * with other versions of Python. - */ -static void -PLy_initialize(void) -{ - static bool inited = false; + /* Add plpy to table of built-in modules. */ + PyImport_AppendInittab("plpy", PyInit_plpy); + + /* Initialize Python interpreter. */ + Py_Initialize(); + + main_mod = PyImport_AddModule("__main__"); + if (main_mod == NULL || PyErr_Occurred()) + PLy_elog(ERROR, "could not import \"%s\" module", "__main__"); + Py_INCREF(main_mod); + + main_dict = PyModule_GetDict(main_mod); + if (main_dict == NULL) + PLy_elog(ERROR, NULL); /* - * Check for multiple Python libraries before actively doing anything with - * libpython. This must be repeated on each entry to PL/Python, in case a - * conflicting library got loaded since we last looked. - * - * It is attractive to weaken this error from FATAL to ERROR, but there - * would be corner cases, so it seems best to be conservative. + * Set up GD. */ - if (*plpython_version_bitmask_ptr != (1 << PY_MAJOR_VERSION)) - ereport(FATAL, - (errmsg("multiple Python libraries are present in session"), - errdetail("Only one Python major version can be used in one session."))); + GD = PyDict_New(); + if (GD == NULL) + PLy_elog(ERROR, NULL); + PyDict_SetItemString(main_dict, "GD", GD); - /* The rest should only be done once per session */ - if (inited) - return; + /* + * Import plpy. + */ + plpy_mod = PyImport_ImportModule("plpy"); + if (plpy_mod == NULL) + PLy_elog(ERROR, "could not import \"%s\" module", "plpy"); + if (PyDict_SetItemString(main_dict, "plpy", plpy_mod) == -1) + PLy_elog(ERROR, NULL); - PyImport_AppendInittab("plpy", PyInit_plpy); - Py_Initialize(); - PyImport_ImportModule("plpy"); - PLy_init_interp(); - PLy_init_plpy(); if (PyErr_Occurred()) PLy_elog(FATAL, "untrapped error in initialization"); + Py_INCREF(main_dict); + PLy_interp_globals = main_dict; + + Py_DECREF(main_mod); + init_procedure_caches(); explicit_subtransactions = NIL; PLy_execution_contexts = NULL; - - inited = true; -} - -/* - * This should be called only once, from PLy_initialize. Initialize the Python - * interpreter and global data. - */ -static void -PLy_init_interp(void) -{ - static PyObject *PLy_interp_safe_globals = NULL; - PyObject *mainmod; - - mainmod = PyImport_AddModule("__main__"); - if (mainmod == NULL || PyErr_Occurred()) - PLy_elog(ERROR, "could not import \"__main__\" module"); - Py_INCREF(mainmod); - PLy_interp_globals = PyModule_GetDict(mainmod); - PLy_interp_safe_globals = PyDict_New(); - if (PLy_interp_safe_globals == NULL) - PLy_elog(ERROR, NULL); - PyDict_SetItemString(PLy_interp_globals, "GD", PLy_interp_safe_globals); - Py_DECREF(mainmod); - if (PLy_interp_globals == NULL || PyErr_Occurred()) - PLy_elog(ERROR, "could not initialize globals"); } Datum @@ -163,7 +116,7 @@ plpython3_validator(PG_FUNCTION_ARGS) Oid funcoid = PG_GETARG_OID(0); HeapTuple tuple; Form_pg_proc procStruct; - bool is_trigger; + PLyTrigType is_trigger; if (!CheckFunctionValidatorAccess(fcinfo->flinfo->fn_oid, funcoid)) PG_RETURN_VOID(); @@ -171,9 +124,6 @@ plpython3_validator(PG_FUNCTION_ARGS) if (!check_function_bodies) PG_RETURN_VOID(); - /* Do this only after making sure we need to do something */ - PLy_initialize(); - /* Get the new function's pg_proc entry */ tuple = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcoid)); if (!HeapTupleIsValid(tuple)) @@ -185,7 +135,7 @@ plpython3_validator(PG_FUNCTION_ARGS) ReleaseSysCache(tuple); /* We can't validate triggers against any particular table ... */ - PLy_procedure_get(funcoid, InvalidOid, is_trigger); + (void) PLy_procedure_get(funcoid, InvalidOid, is_trigger); PG_RETURN_VOID(); } @@ -198,8 +148,6 @@ plpython3_call_handler(PG_FUNCTION_ARGS) PLyExecutionContext *exec_ctx; ErrorContextCallback plerrcontext; - PLy_initialize(); - nonatomic = fcinfo->context && IsA(fcinfo->context, CallContext) && !castNode(CallContext, fcinfo->context)->atomic; @@ -235,14 +183,21 @@ plpython3_call_handler(PG_FUNCTION_ARGS) Relation tgrel = ((TriggerData *) fcinfo->context)->tg_relation; HeapTuple trv; - proc = PLy_procedure_get(funcoid, RelationGetRelid(tgrel), true); + proc = PLy_procedure_get(funcoid, RelationGetRelid(tgrel), PLPY_TRIGGER); exec_ctx->curr_proc = proc; trv = PLy_exec_trigger(fcinfo, proc); retval = PointerGetDatum(trv); } + else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) + { + proc = PLy_procedure_get(funcoid, InvalidOid, PLPY_EVENT_TRIGGER); + exec_ctx->curr_proc = proc; + PLy_exec_event_trigger(fcinfo, proc); + retval = (Datum) 0; + } else { - proc = PLy_procedure_get(funcoid, InvalidOid, false); + proc = PLy_procedure_get(funcoid, InvalidOid, PLPY_NOT_TRIGGER); exec_ctx->curr_proc = proc; retval = PLy_exec_function(fcinfo, proc); } @@ -271,8 +226,6 @@ plpython3_inline_handler(PG_FUNCTION_ARGS) PLyExecutionContext *exec_ctx; ErrorContextCallback plerrcontext; - PLy_initialize(); - /* Note: SPI_finish() happens in plpy_exec.c, which is dubious design */ SPI_connect_ext(codeblock->atomic ? 0 : SPI_OPT_NONATOMIC); @@ -336,10 +289,25 @@ plpython3_inline_handler(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } -static bool +static PLyTrigType PLy_procedure_is_trigger(Form_pg_proc procStruct) { - return (procStruct->prorettype == TRIGGEROID); + PLyTrigType ret; + + switch (procStruct->prorettype) + { + case TRIGGEROID: + ret = PLPY_TRIGGER; + break; + case EVENT_TRIGGEROID: + ret = PLPY_EVENT_TRIGGER; + break; + default: + ret = PLPY_NOT_TRIGGER; + break; + } + + return ret; } static void diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c index 6044893afdd13..edfb76c877020 100644 --- a/src/pl/plpython/plpy_planobject.c +++ b/src/pl/plpython/plpy_planobject.c @@ -45,9 +45,9 @@ static PyType_Slot PLyPlan_slots[] = static PyType_Spec PLyPlan_spec = { .name = "PLyPlan", - .basicsize = sizeof(PLyPlanObject), - .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .slots = PLyPlan_slots, + .basicsize = sizeof(PLyPlanObject), + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .slots = PLyPlan_slots, }; static PyTypeObject *PLy_PlanType; diff --git a/src/pl/plpython/plpy_plpymodule.c b/src/pl/plpython/plpy_plpymodule.c index 1f980b44b2a31..72806c17e17a4 100644 --- a/src/pl/plpython/plpy_plpymodule.c +++ b/src/pl/plpython/plpy_plpymodule.c @@ -133,39 +133,12 @@ PyInit_plpy(void) PLy_add_exceptions(m); - return m; -} - -void -PLy_init_plpy(void) -{ - PyObject *main_mod, - *main_dict, - *plpy_mod; - - /* - * initialize plpy module - */ PLy_plan_init_type(); PLy_result_init_type(); PLy_subtransaction_init_type(); PLy_cursor_init_type(); - PyModule_Create(&PLy_module); - - /* PyDict_SetItemString(plpy, "PlanType", (PyObject *) &PLy_PlanType); */ - - /* - * initialize main module, and add plpy - */ - main_mod = PyImport_AddModule("__main__"); - main_dict = PyModule_GetDict(main_mod); - plpy_mod = PyImport_AddModule("plpy"); - if (plpy_mod == NULL) - PLy_elog(ERROR, "could not import \"plpy\" module"); - PyDict_SetItemString(main_dict, "plpy", plpy_mod); - if (PyErr_Occurred()) - PLy_elog(ERROR, "could not import \"plpy\" module"); + return m; } static void @@ -174,18 +147,6 @@ PLy_add_exceptions(PyObject *plpy) PyObject *excmod; HASHCTL hash_ctl; - excmod = PyModule_Create(&PLy_exc_module); - if (excmod == NULL) - PLy_elog(ERROR, "could not create the spiexceptions module"); - - /* - * PyModule_AddObject does not add a refcount to the object, for some odd - * reason; we must do that. - */ - Py_INCREF(excmod); - if (PyModule_AddObject(plpy, "spiexceptions", excmod) < 0) - PLy_elog(ERROR, "could not add the spiexceptions module"); - PLy_exc_error = PLy_create_exception("plpy.Error", NULL, NULL, "Error", plpy); PLy_exc_fatal = PLy_create_exception("plpy.Fatal", NULL, NULL, @@ -193,16 +154,28 @@ PLy_add_exceptions(PyObject *plpy) PLy_exc_spi_error = PLy_create_exception("plpy.SPIError", NULL, NULL, "SPIError", plpy); + excmod = PyModule_Create(&PLy_exc_module); + if (excmod == NULL) + PLy_elog(ERROR, "could not create the spiexceptions module"); + hash_ctl.keysize = sizeof(int); hash_ctl.entrysize = sizeof(PLyExceptionEntry); PLy_spi_exceptions = hash_create("PL/Python SPI exceptions", 256, &hash_ctl, HASH_ELEM | HASH_BLOBS); PLy_generate_spi_exceptions(excmod, PLy_exc_spi_error); + + if (PyModule_AddObject(plpy, "spiexceptions", excmod) < 0) + { + Py_XDECREF(excmod); + PLy_elog(ERROR, "could not add the spiexceptions module"); + } } /* * Create an exception object and add it to the module + * + * The created exception object is also returned. */ static PyObject * PLy_create_exception(char *name, PyObject *base, PyObject *dict, @@ -215,18 +188,19 @@ PLy_create_exception(char *name, PyObject *base, PyObject *dict, PLy_elog(ERROR, NULL); /* - * PyModule_AddObject does not add a refcount to the object, for some odd - * reason; we must do that. + * PyModule_AddObject() (below) steals the reference to exc, but we also + * want to return the value from this function, so add another ref to + * account for that. (The caller will store a pointer to the exception + * object in some permanent variable.) */ Py_INCREF(exc); - PyModule_AddObject(mod, modname, exc); - /* - * The caller will also store a pointer to the exception object in some - * permanent variable, so add another ref to account for that. This is - * probably excessively paranoid, but let's be sure. - */ - Py_INCREF(exc); + if (PyModule_AddObject(mod, modname, exc) < 0) + { + Py_XDECREF(exc); + PLy_elog(ERROR, "could not add exception %s", name); + } + return exc; } @@ -369,7 +343,7 @@ PLy_quote_ident(PyObject *self, PyObject *args) return ret; } -/* enforce cast of object to string */ +/* enforce cast of object to string (returns a palloc'd string or NULL) */ static char * object_to_string(PyObject *obj) { @@ -381,7 +355,7 @@ object_to_string(PyObject *obj) { char *str; - str = pstrdup(PLyUnicode_AsString(so)); + str = PLyUnicode_AsString(so); Py_DECREF(so); return str; diff --git a/src/pl/plpython/plpy_plpymodule.h b/src/pl/plpython/plpy_plpymodule.h index 1ca3823daf283..88f902346a6ec 100644 --- a/src/pl/plpython/plpy_plpymodule.h +++ b/src/pl/plpython/plpy_plpymodule.h @@ -13,6 +13,5 @@ extern HTAB *PLy_spi_exceptions; PyMODINIT_FUNC PyInit_plpy(void); -extern void PLy_init_plpy(void); #endif /* PLPY_PLPYMODULE_H */ diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c index c176d24e80118..750ba586e0c85 100644 --- a/src/pl/plpython/plpy_procedure.c +++ b/src/pl/plpython/plpy_procedure.c @@ -21,7 +21,7 @@ static HTAB *PLy_procedure_cache = NULL; -static PLyProcedure *PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger); +static PLyProcedure *PLy_procedure_create(HeapTuple procTup, Oid fn_oid, PLyTrigType is_trigger); static bool PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup); static char *PLy_procedure_munge_source(const char *name, const char *src); @@ -60,18 +60,28 @@ PLy_procedure_name(PLyProcedure *proc) * * The reason that both fn_rel and is_trigger need to be passed is that when * trigger functions get validated we don't know which relation(s) they'll - * be used with, so no sensible fn_rel can be passed. + * be used with, so no sensible fn_rel can be passed. Also, in that case + * we can't make a cache entry because we can't construct the right cache key. + * To forestall leakage of the PLyProcedure in such cases, delete it after + * construction and return NULL. That's okay because the only caller that + * would pass that set of values is plpython3_validator, which ignores our + * result anyway. */ PLyProcedure * -PLy_procedure_get(Oid fn_oid, Oid fn_rel, bool is_trigger) +PLy_procedure_get(Oid fn_oid, Oid fn_rel, PLyTrigType is_trigger) { - bool use_cache = !(is_trigger && fn_rel == InvalidOid); + bool use_cache; HeapTuple procTup; PLyProcedureKey key; PLyProcedureEntry *volatile entry = NULL; PLyProcedure *volatile proc = NULL; bool found = false; + if (is_trigger == PLPY_TRIGGER && fn_rel == InvalidOid) + use_cache = false; + else + use_cache = true; + procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(fn_oid)); if (!HeapTupleIsValid(procTup)) elog(ERROR, "cache lookup failed for function %u", fn_oid); @@ -97,6 +107,12 @@ PLy_procedure_get(Oid fn_oid, Oid fn_rel, bool is_trigger) proc = PLy_procedure_create(procTup, fn_oid, is_trigger); if (use_cache) entry->proc = proc; + else + { + /* Delete the proc, otherwise it's a memory leak */ + PLy_procedure_delete(proc); + proc = NULL; + } } else if (!PLy_procedure_valid(proc, procTup)) { @@ -127,7 +143,7 @@ PLy_procedure_get(Oid fn_oid, Oid fn_rel, bool is_trigger) * Create a new PLyProcedure structure */ static PLyProcedure * -PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) +PLy_procedure_create(HeapTuple procTup, Oid fn_oid, PLyTrigType is_trigger) { char procName[NAMEDATALEN + 256]; Form_pg_proc procStruct; @@ -161,7 +177,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) oldcxt = MemoryContextSwitchTo(cxt); - proc = (PLyProcedure *) palloc0(sizeof(PLyProcedure)); + proc = palloc0_object(PLyProcedure); proc->mcxt = cxt; PG_TRY(); @@ -200,7 +216,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) * get information required for output conversion of the return value, * but only if this isn't a trigger. */ - if (!is_trigger) + if (is_trigger == PLPY_NOT_TRIGGER) { Oid rettype = procStruct->prorettype; HeapTuple rvTypeTup; @@ -277,8 +293,8 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger) } /* Allocate arrays for per-input-argument data */ - proc->argnames = (char **) palloc0(sizeof(char *) * proc->nargs); - proc->args = (PLyDatumToOb *) palloc0(sizeof(PLyDatumToOb) * proc->nargs); + proc->argnames = (char **) palloc0_array(char *, proc->nargs); + proc->args = (PLyDatumToOb *) palloc0_array(PLyDatumToOb, proc->nargs); for (i = pos = 0; i < total; i++) { diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h index 5db854fc8bd2d..3ef22844a9b71 100644 --- a/src/pl/plpython/plpy_procedure.h +++ b/src/pl/plpython/plpy_procedure.h @@ -11,6 +11,16 @@ extern void init_procedure_caches(void); +/* + * Trigger type + */ +typedef enum PLyTrigType +{ + PLPY_TRIGGER, + PLPY_EVENT_TRIGGER, + PLPY_NOT_TRIGGER, +} PLyTrigType; + /* saved arguments for outer recursion level or set-returning function */ typedef struct PLySavedArgs { @@ -33,7 +43,7 @@ typedef struct PLyProcedure bool fn_readonly; bool is_setof; /* true, if function returns result set */ bool is_procedure; - bool is_trigger; /* called as trigger? */ + PLyTrigType is_trigger; /* called as trigger? */ PLyObToDatum result; /* Function result output conversion info */ PLyDatumToOb result_in; /* For converting input tuples in a trigger */ char *src; /* textual procedure code, after mangling */ @@ -65,7 +75,7 @@ typedef struct PLyProcedureEntry /* PLyProcedure manipulation */ extern char *PLy_procedure_name(PLyProcedure *proc); -extern PLyProcedure *PLy_procedure_get(Oid fn_oid, Oid fn_rel, bool is_trigger); +extern PLyProcedure *PLy_procedure_get(Oid fn_oid, Oid fn_rel, PLyTrigType is_trigger); extern void PLy_procedure_compile(PLyProcedure *proc, const char *src); extern void PLy_procedure_delete(PLyProcedure *proc); diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c index 0d9997cbaa32c..d433929b36039 100644 --- a/src/pl/plpython/plpy_resultobject.c +++ b/src/pl/plpython/plpy_resultobject.c @@ -70,9 +70,9 @@ static PyType_Slot PLyResult_slots[] = static PyType_Spec PLyResult_spec = { .name = "PLyResult", - .basicsize = sizeof(PLyResultObject), - .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .slots = PLyResult_slots, + .basicsize = sizeof(PLyResultObject), + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .slots = PLyResult_slots, }; static PyTypeObject *PLy_ResultType; diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c index 1e386aadcca2b..46f2ca0f792a8 100644 --- a/src/pl/plpython/plpy_spi.c +++ b/src/pl/plpython/plpy_spi.c @@ -65,8 +65,8 @@ PLy_spi_prepare(PyObject *self, PyObject *args) nargs = list ? PySequence_Length(list) : 0; plan->nargs = nargs; - plan->types = nargs ? palloc0(sizeof(Oid) * nargs) : NULL; - plan->args = nargs ? palloc0(sizeof(PLyObToDatum) * nargs) : NULL; + plan->types = nargs ? palloc0_array(Oid, nargs) : NULL; + plan->args = nargs ? palloc0_array(PLyObToDatum, nargs) : NULL; MemoryContextSwitchTo(oldcontext); diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c index c2484a99b4ae3..c225b652ab4a5 100644 --- a/src/pl/plpython/plpy_subxactobject.c +++ b/src/pl/plpython/plpy_subxactobject.c @@ -46,9 +46,9 @@ static PyType_Slot PLySubtransaction_slots[] = static PyType_Spec PLySubtransaction_spec = { .name = "PLySubtransaction", - .basicsize = sizeof(PLySubtransactionObject), - .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - .slots = PLySubtransaction_slots, + .basicsize = sizeof(PLySubtransactionObject), + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .slots = PLySubtransaction_slots, }; static PyTypeObject *PLy_SubtransactionType; diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c index f6509a4190282..92d55bf9f423e 100644 --- a/src/pl/plpython/plpy_typeio.c +++ b/src/pl/plpython/plpy_typeio.c @@ -35,7 +35,7 @@ static PyObject *PLyUnicode_FromScalar(PLyDatumToOb *arg, Datum d); static PyObject *PLyObject_FromTransform(PLyDatumToOb *arg, Datum d); static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d); static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, - char **dataptr_p, bits8 **bitmap_p, int *bitmask_p); + char **dataptr_p, uint8 **bitmap_p, int *bitmask_p); static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d); static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool include_generated); @@ -171,15 +171,15 @@ PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, PLyProcedure *proc) /* Save pointer to tupdesc, but only if this is an anonymous record type */ if (arg->typoid == RECORDOID && arg->typmod < 0) - arg->u.tuple.recdesc = desc; + arg->tuple.recdesc = desc; /* (Re)allocate atts array as needed */ - if (arg->u.tuple.natts != desc->natts) + if (arg->tuple.natts != desc->natts) { - if (arg->u.tuple.atts) - pfree(arg->u.tuple.atts); - arg->u.tuple.natts = desc->natts; - arg->u.tuple.atts = (PLyDatumToOb *) + if (arg->tuple.atts) + pfree(arg->tuple.atts); + arg->tuple.natts = desc->natts; + arg->tuple.atts = (PLyDatumToOb *) MemoryContextAllocZero(arg->mcxt, desc->natts * sizeof(PLyDatumToOb)); } @@ -188,7 +188,7 @@ PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, PLyProcedure *proc) for (i = 0; i < desc->natts; i++) { Form_pg_attribute attr = TupleDescAttr(desc, i); - PLyDatumToOb *att = &arg->u.tuple.atts[i]; + PLyDatumToOb *att = &arg->tuple.atts[i]; if (attr->attisdropped) continue; @@ -221,15 +221,15 @@ PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) /* Save pointer to tupdesc, but only if this is an anonymous record type */ if (arg->typoid == RECORDOID && arg->typmod < 0) - arg->u.tuple.recdesc = desc; + arg->tuple.recdesc = desc; /* (Re)allocate atts array as needed */ - if (arg->u.tuple.natts != desc->natts) + if (arg->tuple.natts != desc->natts) { - if (arg->u.tuple.atts) - pfree(arg->u.tuple.atts); - arg->u.tuple.natts = desc->natts; - arg->u.tuple.atts = (PLyObToDatum *) + if (arg->tuple.atts) + pfree(arg->tuple.atts); + arg->tuple.natts = desc->natts; + arg->tuple.atts = (PLyObToDatum *) MemoryContextAllocZero(arg->mcxt, desc->natts * sizeof(PLyObToDatum)); } @@ -238,7 +238,7 @@ PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) for (i = 0; i < desc->natts; i++) { Form_pg_attribute attr = TupleDescAttr(desc, i); - PLyObToDatum *att = &arg->u.tuple.atts[i]; + PLyObToDatum *att = &arg->tuple.atts[i]; if (attr->attisdropped) continue; @@ -277,9 +277,9 @@ PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc) * for the record type. */ arg->typmod = desc->tdtypmod; - if (arg->u.tuple.recdesc && - arg->u.tuple.recdesc->tdtypmod != arg->typmod) - arg->u.tuple.recdesc = NULL; + if (arg->tuple.recdesc && + arg->tuple.recdesc->tdtypmod != arg->typmod) + arg->tuple.recdesc = NULL; /* Update derived data if necessary */ PLy_output_setup_tuple(arg, desc, proc); @@ -343,11 +343,11 @@ PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, { /* Domain */ arg->func = PLyObject_ToDomain; - arg->u.domain.domain_info = NULL; + arg->domain.domain_info = NULL; /* Recursively set up conversion info for the element type */ - arg->u.domain.base = (PLyObToDatum *) + arg->domain.base = (PLyObToDatum *) MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum)); - PLy_output_setup_func(arg->u.domain.base, arg_mcxt, + PLy_output_setup_func(arg->domain.base, arg_mcxt, typentry->domainBaseType, typentry->domainBaseTypmod, proc); @@ -359,11 +359,11 @@ PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, arg->func = PLySequence_ToArray; /* Get base type OID to insert into constructed array */ /* (note this might not be the same as the immediate child type) */ - arg->u.array.elmbasetype = getBaseType(typentry->typelem); + arg->array.elmbasetype = getBaseType(typentry->typelem); /* Recursively set up conversion info for the element type */ - arg->u.array.elm = (PLyObToDatum *) + arg->array.elm = (PLyObToDatum *) MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum)); - PLy_output_setup_func(arg->u.array.elm, arg_mcxt, + PLy_output_setup_func(arg->array.elm, arg_mcxt, typentry->typelem, typmod, proc); } @@ -372,20 +372,20 @@ PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, proc->trftypes))) { arg->func = PLyObject_ToTransform; - fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt); + fmgr_info_cxt(trfuncid, &arg->transform.typtransform, arg_mcxt); } else if (typtype == TYPTYPE_COMPOSITE) { /* Named composite type, or RECORD */ arg->func = PLyObject_ToComposite; /* We'll set up the per-field data later */ - arg->u.tuple.recdesc = NULL; - arg->u.tuple.typentry = typentry; - arg->u.tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER; - arg->u.tuple.atts = NULL; - arg->u.tuple.natts = 0; + arg->tuple.recdesc = NULL; + arg->tuple.typentry = typentry; + arg->tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER; + arg->tuple.atts = NULL; + arg->tuple.natts = 0; /* Mark this invalid till needed, too */ - arg->u.tuple.recinfunc.fn_oid = InvalidOid; + arg->tuple.recinfunc.fn_oid = InvalidOid; } else { @@ -400,8 +400,8 @@ PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt, break; default: arg->func = PLyObject_ToScalar; - getTypeInputInfo(typeOid, &typinput, &arg->u.scalar.typioparam); - fmgr_info_cxt(typinput, &arg->u.scalar.typfunc, arg_mcxt); + getTypeInputInfo(typeOid, &typinput, &arg->scalar.typioparam); + fmgr_info_cxt(typinput, &arg->scalar.typfunc, arg_mcxt); break; } } @@ -476,9 +476,9 @@ PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt, /* Standard array */ arg->func = PLyList_FromArray; /* Recursively set up conversion info for the element type */ - arg->u.array.elm = (PLyDatumToOb *) + arg->array.elm = (PLyDatumToOb *) MemoryContextAllocZero(arg_mcxt, sizeof(PLyDatumToOb)); - PLy_input_setup_func(arg->u.array.elm, arg_mcxt, + PLy_input_setup_func(arg->array.elm, arg_mcxt, typentry->typelem, typmod, proc); } @@ -487,18 +487,18 @@ PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt, proc->trftypes))) { arg->func = PLyObject_FromTransform; - fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt); + fmgr_info_cxt(trfuncid, &arg->transform.typtransform, arg_mcxt); } else if (typtype == TYPTYPE_COMPOSITE) { /* Named composite type, or RECORD */ arg->func = PLyDict_FromComposite; /* We'll set up the per-field data later */ - arg->u.tuple.recdesc = NULL; - arg->u.tuple.typentry = typentry; - arg->u.tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER; - arg->u.tuple.atts = NULL; - arg->u.tuple.natts = 0; + arg->tuple.recdesc = NULL; + arg->tuple.typentry = typentry; + arg->tuple.tupdescid = INVALID_TUPLEDESC_IDENTIFIER; + arg->tuple.atts = NULL; + arg->tuple.natts = 0; } else { @@ -535,7 +535,7 @@ PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt, default: arg->func = PLyUnicode_FromScalar; getTypeOutputInfo(typeOid, &typoutput, &typisvarlena); - fmgr_info_cxt(typoutput, &arg->u.scalar.typfunc, arg_mcxt); + fmgr_info_cxt(typoutput, &arg->scalar.typfunc, arg_mcxt); break; } } @@ -641,7 +641,7 @@ PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d) static PyObject * PLyUnicode_FromScalar(PLyDatumToOb *arg, Datum d) { - char *x = OutputFunctionCall(&arg->u.scalar.typfunc, d); + char *x = OutputFunctionCall(&arg->scalar.typfunc, d); PyObject *r = PLyUnicode_FromString(x); pfree(x); @@ -656,7 +656,7 @@ PLyObject_FromTransform(PLyDatumToOb *arg, Datum d) { Datum t; - t = FunctionCall1(&arg->u.transform.typtransform, d); + t = FunctionCall1(&arg->transform.typtransform, d); return (PyObject *) DatumGetPointer(t); } @@ -667,11 +667,11 @@ static PyObject * PLyList_FromArray(PLyDatumToOb *arg, Datum d) { ArrayType *array = DatumGetArrayTypeP(d); - PLyDatumToOb *elm = arg->u.array.elm; + PLyDatumToOb *elm = arg->array.elm; int ndim; int *dims; char *dataptr; - bits8 *bitmap; + uint8 *bitmap; int bitmask; if (ARR_NDIM(array) == 0) @@ -705,7 +705,7 @@ PLyList_FromArray(PLyDatumToOb *arg, Datum d) static PyObject * PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, - char **dataptr_p, bits8 **bitmap_p, int *bitmask_p) + char **dataptr_p, uint8 **bitmap_p, int *bitmask_p) { int i; PyObject *list; @@ -733,8 +733,9 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, * for this slice. */ char *dataptr = *dataptr_p; - bits8 *bitmap = *bitmap_p; + uint8 *bitmap = *bitmap_p; int bitmask = *bitmask_p; + uint8 typalignby = typalign_to_alignby(elm->typalign); for (i = 0; i < dims[dim]; i++) { @@ -751,7 +752,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim, itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen); PyList_SetItem(list, i, elm->func(elm, itemvalue)); dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr); - dataptr = (char *) att_align_nominal(dataptr, elm->typalign); + dataptr = (char *) att_nominal_alignby(dataptr, typalignby); } /* advance bitmap pointer if any */ @@ -817,7 +818,7 @@ PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool inclu PyObject *volatile dict; /* Simple sanity check that desc matches */ - Assert(desc->natts == arg->u.tuple.natts); + Assert(desc->natts == arg->tuple.natts); dict = PyDict_New(); if (dict == NULL) @@ -827,9 +828,9 @@ PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc, bool inclu { int i; - for (i = 0; i < arg->u.tuple.natts; i++) + for (i = 0; i < arg->tuple.natts; i++) { - PLyDatumToOb *att = &arg->u.tuple.atts[i]; + PLyDatumToOb *att = &arg->tuple.atts[i]; Form_pg_attribute attr = TupleDescAttr(desc, i); char *key; Datum vattr; @@ -971,22 +972,22 @@ PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv, { desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod); /* We should have the descriptor of the type's typcache entry */ - Assert(desc == arg->u.tuple.typentry->tupDesc); + Assert(desc == arg->tuple.typentry->tupDesc); /* Detect change of descriptor, update cache if needed */ - if (arg->u.tuple.tupdescid != arg->u.tuple.typentry->tupDesc_identifier) + if (arg->tuple.tupdescid != arg->tuple.typentry->tupDesc_identifier) { PLy_output_setup_tuple(arg, desc, PLy_current_execution_context()->curr_proc); - arg->u.tuple.tupdescid = arg->u.tuple.typentry->tupDesc_identifier; + arg->tuple.tupdescid = arg->tuple.typentry->tupDesc_identifier; } } else { - desc = arg->u.tuple.recdesc; + desc = arg->tuple.recdesc; if (desc == NULL) { desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod); - arg->u.tuple.recdesc = desc; + arg->tuple.recdesc = desc; } else { @@ -996,7 +997,7 @@ PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv, } /* Simple sanity check on our caching */ - Assert(desc->natts == arg->u.tuple.natts); + Assert(desc->natts == arg->tuple.natts); /* * Convert, using the appropriate method depending on the type of the @@ -1088,9 +1089,9 @@ PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv, str = PLyObject_AsString(plrv); - return InputFunctionCall(&arg->u.scalar.typfunc, + return InputFunctionCall(&arg->scalar.typfunc, str, - arg->u.scalar.typioparam, + arg->scalar.typioparam, arg->typmod); } @@ -1103,11 +1104,11 @@ PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv, bool *isnull, bool inarray) { Datum result; - PLyObToDatum *base = arg->u.domain.base; + PLyObToDatum *base = arg->domain.base; result = base->func(base, plrv, isnull, inarray); domain_check(result, *isnull, arg->typoid, - &arg->u.domain.domain_info, arg->mcxt); + &arg->domain.domain_info, arg->mcxt); return result; } @@ -1125,7 +1126,7 @@ PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv, return (Datum) 0; } *isnull = false; - return FunctionCall1(&arg->u.transform.typtransform, PointerGetDatum(plrv)); + return FunctionCall1(&arg->transform.typtransform, PointerGetDatum(plrv)); } @@ -1169,12 +1170,12 @@ PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv, */ PLySequence_ToArray_recurse(plrv, &astate, &ndims, dims, 1, - arg->u.array.elm, - arg->u.array.elmbasetype); + arg->array.elm, + arg->array.elmbasetype); /* ensure we get zero-D array for no inputs, as per PG convention */ if (astate == NULL) - return PointerGetDatum(construct_empty_array(arg->u.array.elmbasetype)); + return PointerGetDatum(construct_empty_array(arg->array.elmbasetype)); for (int i = 0; i < ndims; i++) lbs[i] = 1; @@ -1289,8 +1290,8 @@ PLyUnicode_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray) * Set up call data for record_in, if we didn't already. (We can't just * use DirectFunctionCall, because record_in needs a fn_extra field.) */ - if (!OidIsValid(arg->u.tuple.recinfunc.fn_oid)) - fmgr_info_cxt(F_RECORD_IN, &arg->u.tuple.recinfunc, arg->mcxt); + if (!OidIsValid(arg->tuple.recinfunc.fn_oid)) + fmgr_info_cxt(F_RECORD_IN, &arg->tuple.recinfunc, arg->mcxt); str = PLyObject_AsString(string); @@ -1334,7 +1335,7 @@ PLyUnicode_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray) errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\"."))); } - return InputFunctionCall(&arg->u.tuple.recinfunc, + return InputFunctionCall(&arg->tuple.recinfunc, str, arg->typoid, arg->typmod); @@ -1353,8 +1354,8 @@ PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping) Assert(PyMapping_Check(mapping)); /* Build tuple */ - values = palloc(sizeof(Datum) * desc->natts); - nulls = palloc(sizeof(bool) * desc->natts); + values = palloc_array(Datum, desc->natts); + nulls = palloc_array(bool, desc->natts); for (i = 0; i < desc->natts; ++i) { char *key; @@ -1371,7 +1372,7 @@ PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping) key = NameStr(attr->attname); value = NULL; - att = &arg->u.tuple.atts[i]; + att = &arg->tuple.atts[i]; PG_TRY(); { value = PyMapping_GetItemString(mapping, key); @@ -1426,7 +1427,7 @@ PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence) idx = 0; for (i = 0; i < desc->natts; i++) { - if (!TupleDescAttr(desc, i)->attisdropped) + if (!TupleDescCompactAttr(desc, i)->attisdropped) idx++; } if (PySequence_Length(sequence) != idx) @@ -1435,15 +1436,15 @@ PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence) errmsg("length of returned sequence did not match number of columns in row"))); /* Build tuple */ - values = palloc(sizeof(Datum) * desc->natts); - nulls = palloc(sizeof(bool) * desc->natts); + values = palloc_array(Datum, desc->natts); + nulls = palloc_array(bool, desc->natts); idx = 0; for (i = 0; i < desc->natts; ++i) { PyObject *volatile value; PLyObToDatum *att; - if (TupleDescAttr(desc, i)->attisdropped) + if (TupleDescCompactAttr(desc, i)->attisdropped) { values[i] = (Datum) 0; nulls[i] = true; @@ -1451,7 +1452,7 @@ PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence) } value = NULL; - att = &arg->u.tuple.atts[i]; + att = &arg->tuple.atts[i]; PG_TRY(); { value = PySequence_GetItem(sequence, idx); @@ -1493,8 +1494,8 @@ PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object volatile int i; /* Build tuple */ - values = palloc(sizeof(Datum) * desc->natts); - nulls = palloc(sizeof(bool) * desc->natts); + values = palloc_array(Datum, desc->natts); + nulls = palloc_array(bool, desc->natts); for (i = 0; i < desc->natts; ++i) { char *key; @@ -1511,7 +1512,7 @@ PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object key = NameStr(attr->attname); value = NULL; - att = &arg->u.tuple.atts[i]; + att = &arg->tuple.atts[i]; PG_TRY(); { value = PyObject_GetAttrString(object, key); diff --git a/src/pl/plpython/plpy_typeio.h b/src/pl/plpython/plpy_typeio.h index 5417f0945d260..29258509b5c73 100644 --- a/src/pl/plpython/plpy_typeio.h +++ b/src/pl/plpython/plpy_typeio.h @@ -69,7 +69,7 @@ struct PLyDatumToOb PLyArrayToOb array; PLyTupleToOb tuple; PLyTransformToOb transform; - } u; + }; }; /* @@ -143,7 +143,7 @@ struct PLyObToDatum PLyObToTuple tuple; PLyObToDomain domain; PLyObToTransform transform; - } u; + }; }; diff --git a/src/pl/plpython/plpy_util.c b/src/pl/plpython/plpy_util.c index ef710aa371eac..c2338decba205 100644 --- a/src/pl/plpython/plpy_util.c +++ b/src/pl/plpython/plpy_util.c @@ -40,8 +40,8 @@ PLyUnicode_Bytes(PyObject *unicode) * * PyUnicode_AsEncodedString could be used to encode the object directly * in the server encoding, but Python doesn't support all the encodings - * that PostgreSQL does (EUC_TW and MULE_INTERNAL). UTF-8 is used as an - * intermediary in PLyUnicode_FromString as well. + * that PostgreSQL does (EUC_TW). UTF-8 is used as an intermediary in + * PLyUnicode_FromString as well. */ if (GetDatabaseEncoding() != PG_UTF8) { diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h index 118b3100840f6..595e4af749126 100644 --- a/src/pl/plpython/plpython.h +++ b/src/pl/plpython/plpython.h @@ -6,7 +6,7 @@ * (plpy_elog.h, etc). It's therefore unnecessary for any plpython *.c * files to include it directly. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/pl/plpython/plpython.h @@ -25,12 +25,8 @@ /* * Enable Python Limited API - * - * XXX currently not enabled on MSVC because of build failures */ -#if !defined(_MSC_VER) #define Py_LIMITED_API 0x03020000 -#endif /* * Pull in Python headers via a wrapper header, to control the scope of diff --git a/src/pl/plpython/plpython_system.h b/src/pl/plpython/plpython_system.h index ace9fc631b224..d581518ef0be8 100644 --- a/src/pl/plpython/plpython_system.h +++ b/src/pl/plpython/plpython_system.h @@ -7,7 +7,7 @@ * declarations should be put here. However, we do include some stuff * that is meant to prevent conflicts between our code and Python. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/pl/plpython/plpython_system.h diff --git a/src/pl/plpython/po/meson.build b/src/pl/plpython/po/meson.build index 1843641e1990e..fff195a7a5551 100644 --- a/src/pl/plpython/po/meson.build +++ b/src/pl/plpython/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('plpython-' + pg_version_major.to_string())] diff --git a/src/pl/plpython/sql/plpython_trigger.sql b/src/pl/plpython/sql/plpython_trigger.sql index 440549c0785da..e1a552e079fe8 100644 --- a/src/pl/plpython/sql/plpython_trigger.sql +++ b/src/pl/plpython/sql/plpython_trigger.sql @@ -492,3 +492,27 @@ CREATE TRIGGER recursive_trigger_trig INSERT INTO recursive_trigger_test VALUES (0, 0); UPDATE recursive_trigger_test SET a = 11 WHERE b = 0; SELECT * FROM recursive_trigger_test; + + +-- event triggers + +CREATE OR REPLACE FUNCTION pysnitch() RETURNS event_trigger +LANGUAGE plpython3u +AS $$ + plpy.notice("TD[event] => " + TD["event"] + " ; TD[tag] => " + TD["tag"]); +$$; + +CREATE EVENT TRIGGER python_a_snitch ON ddl_command_start + EXECUTE PROCEDURE pysnitch(); +CREATE EVENT TRIGGER python_b_snitch ON ddl_command_end + EXECUTE PROCEDURE pysnitch(); + +CREATE OR REPLACE FUNCTION foobar() RETURNS int LANGUAGE sql AS $$SELECT 1;$$; +ALTER FUNCTION foobar() COST 77; +DROP FUNCTION foobar(); + +CREATE TABLE foo(); +DROP TABLE foo; + +DROP EVENT TRIGGER python_a_snitch; +DROP EVENT TRIGGER python_b_snitch; diff --git a/src/pl/plpython/sql/plpython_unicode.sql b/src/pl/plpython/sql/plpython_unicode.sql index 14f7b4e0053db..f45844b906e47 100644 --- a/src/pl/plpython/sql/plpython_unicode.sql +++ b/src/pl/plpython/sql/plpython_unicode.sql @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; diff --git a/src/pl/tcl/Makefile b/src/pl/tcl/Makefile index ea52a2efc229d..7dc7cf3497ac8 100644 --- a/src/pl/tcl/Makefile +++ b/src/pl/tcl/Makefile @@ -11,7 +11,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -override CPPFLAGS := -I. -I$(srcdir) $(TCL_INCLUDE_SPEC) $(CPPFLAGS) +override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS) $(TCL_INCLUDE_SPEC) # On Windows, we don't link directly with the Tcl library; see below ifneq ($(PORTNAME), win32) @@ -30,7 +30,17 @@ DATA = pltcl.control pltcl--1.0.sql \ pltclu.control pltclu--1.0.sql REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=pltcl -REGRESS = pltcl_setup pltcl_queries pltcl_trigger pltcl_call pltcl_start_proc pltcl_subxact pltcl_unicode pltcl_transaction +# "pltcl_setup" is first because the other tests depend on the objects it +# creates. +REGRESS = \ + pltcl_setup \ + pltcl_call \ + pltcl_queries \ + pltcl_start_proc \ + pltcl_subxact \ + pltcl_transaction \ + pltcl_trigger \ + pltcl_unicode # Tcl on win32 ships with import libraries only for Microsoft Visual C++, # which are not compatible with mingw gcc. Therefore we need to build a diff --git a/src/pl/tcl/expected/pltcl_unicode.out b/src/pl/tcl/expected/pltcl_unicode.out index eea7d70664f47..d33afd7548f0f 100644 --- a/src/pl/tcl/expected/pltcl_unicode.out +++ b/src/pl/tcl/expected/pltcl_unicode.out @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; CREATE TABLE unicode_test ( testvalue text NOT NULL diff --git a/src/pl/tcl/expected/pltcl_unicode_1.out b/src/pl/tcl/expected/pltcl_unicode_1.out new file mode 100644 index 0000000000000..f8b21fd7eb7ab --- /dev/null +++ b/src/pl/tcl/expected/pltcl_unicode_1.out @@ -0,0 +1,12 @@ +-- +-- Unicode handling +-- +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. +-- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit diff --git a/src/pl/tcl/generate-pltclerrcodes.pl b/src/pl/tcl/generate-pltclerrcodes.pl index 25da160c97f7d..00ceef0ef991c 100644 --- a/src/pl/tcl/generate-pltclerrcodes.pl +++ b/src/pl/tcl/generate-pltclerrcodes.pl @@ -1,7 +1,7 @@ #!/usr/bin/perl # # Generate the pltclerrcodes.h header from errcodes.txt -# Copyright (c) 2000-2025, PostgreSQL Global Development Group +# Copyright (c) 2000-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/pl/tcl/meson.build b/src/pl/tcl/meson.build index 5ab40f7f1d8d9..baea998608efb 100644 --- a/src/pl/tcl/meson.build +++ b/src/pl/tcl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not tcl_dep.found() subdir_done() diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c index 73d660e88a693..85e83bbf1e34d 100644 --- a/src/pl/tcl/pltcl.c +++ b/src/pl/tcl/pltcl.c @@ -31,11 +31,13 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/guc.h" +#include "utils/hsearch.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/regproc.h" #include "utils/rel.h" #include "utils/syscache.h" +#include "utils/tuplestore.h" #include "utils/typcache.h" @@ -1586,7 +1588,7 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid, * struct prodesc and subsidiary data must all live in proc_cxt. ************************************************************/ oldcontext = MemoryContextSwitchTo(proc_cxt); - prodesc = (pltcl_proc_desc *) palloc0(sizeof(pltcl_proc_desc)); + prodesc = palloc0_object(pltcl_proc_desc); prodesc->user_proname = pstrdup(user_proname); MemoryContextSetIdentifier(proc_cxt, prodesc->user_proname); prodesc->internal_proname = pstrdup(internal_proname); @@ -2545,7 +2547,7 @@ pltcl_process_SPI_result(Tcl_Interp *interp, break; } /* fall through for utility returning tuples */ - /* FALLTHROUGH */ + pg_fallthrough; case SPI_OK_SELECT: case SPI_OK_INSERT_RETURNING: @@ -2668,7 +2670,7 @@ pltcl_SPI_prepare(ClientData cdata, Tcl_Interp *interp, "PL/Tcl spi_prepare query", ALLOCSET_SMALL_SIZES); MemoryContextSwitchTo(plan_cxt); - qdesc = (pltcl_query_desc *) palloc0(sizeof(pltcl_query_desc)); + qdesc = palloc0_object(pltcl_query_desc); snprintf(qdesc->qname, sizeof(qdesc->qname), "%p", qdesc); qdesc->nargs = nargs; qdesc->argtypes = (Oid *) palloc(nargs * sizeof(Oid)); diff --git a/src/pl/tcl/po/meson.build b/src/pl/tcl/po/meson.build index 1f8fac8810e0a..65490e4ceac65 100644 --- a/src/pl/tcl/po/meson.build +++ b/src/pl/tcl/po/meson.build @@ -1,3 +1,3 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group nls_targets += [i18n.gettext('pltcl-' + pg_version_major.to_string())] diff --git a/src/pl/tcl/sql/pltcl_unicode.sql b/src/pl/tcl/sql/pltcl_unicode.sql index f0006046127d4..a09e4998b2bf6 100644 --- a/src/pl/tcl/sql/pltcl_unicode.sql +++ b/src/pl/tcl/sql/pltcl_unicode.sql @@ -1,11 +1,16 @@ -- -- Unicode handling -- --- Note: this test case is known to fail if the database encoding is --- EUC_CN, EUC_JP, EUC_KR, or EUC_TW, for lack of any equivalent to --- U+00A0 (no-break space) in those encodings. However, testing with --- plain ASCII data would be rather useless, so we must live with that. +-- This test case would fail if the database encoding is EUC_CN, EUC_JP, +-- EUC_KR, or EUC_TW, for lack of any equivalent to U+00A0 (no-break space) in +-- those encodings. However, testing with plain ASCII data would be rather +-- useless, so we must live with that. -- +SELECT getdatabaseencoding() IN ('EUC_CN', 'EUC_JP', 'EUC_KR', 'EUC_TW') + AS skip_test \gset +\if :skip_test +\quit +\endif SET client_encoding TO UTF8; diff --git a/src/port/Makefile b/src/port/Makefile index 4274949dfa4c1..7e9b58776529a 100644 --- a/src/port/Makefile +++ b/src/port/Makefile @@ -44,10 +44,12 @@ OBJS = \ noblock.o \ path.o \ pg_bitutils.o \ + pg_cpu_x86.o \ + pg_getopt_ctx.o \ pg_localeconv_r.o \ pg_numa.o \ pg_popcount_aarch64.o \ - pg_popcount_avx512.o \ + pg_popcount_x86.o \ pg_strong_random.o \ pgcheckdir.o \ pgmkdirp.o \ diff --git a/src/port/README b/src/port/README index ed5c54a72fac7..97f18a623382a 100644 --- a/src/port/README +++ b/src/port/README @@ -28,5 +28,5 @@ applications. from libpgport are linked first. This avoids having applications dependent on symbols that are _used_ by libpq, but not intended to be exported by libpq. libpq's libpgport usage changes over time, so such a -dependency is a problem. Windows, Linux, and macOS use an export +dependency is a problem. Windows, Linux, AIX, and macOS use an export list to control the symbols exported by libpq. diff --git a/src/port/bsearch_arg.c b/src/port/bsearch_arg.c index e9798aa8ee66a..bc2db8239cdc9 100644 --- a/src/port/bsearch_arg.c +++ b/src/port/bsearch_arg.c @@ -1,7 +1,7 @@ /* * bsearch_arg.c: bsearch variant with a user-supplied pointer * - * Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Copyright (c) 2021-2026, PostgreSQL Global Development Group * Copyright (c) 1990 Regents of the University of California. * All rights reserved. * diff --git a/src/port/chklocale.c b/src/port/chklocale.c index 034939f7fd2c0..664eeab9050c4 100644 --- a/src/port/chklocale.c +++ b/src/port/chklocale.c @@ -4,7 +4,7 @@ * Functions for handling locale-related info * * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -202,7 +202,7 @@ static char * win32_get_codeset(const char *ctype) { char *r = NULL; - char *codepage; + const char *codepage; uint32 cp; WCHAR wctype[LOCALE_NAME_MAX_LENGTH]; diff --git a/src/port/dirent.c b/src/port/dirent.c index a10484ea32cc2..0505bdf9dc9b0 100644 --- a/src/port/dirent.c +++ b/src/port/dirent.c @@ -3,7 +3,7 @@ * dirent.c * opendir/readdir/closedir for win32/msvc * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/dirmod.c b/src/port/dirmod.c index c1187d249deca..467b50d6f09e0 100644 --- a/src/port/dirmod.c +++ b/src/port/dirmod.c @@ -3,7 +3,7 @@ * dirmod.c * directory handling functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * This includes replacement versions of functions that work on diff --git a/src/port/explicit_bzero.c b/src/port/explicit_bzero.c index 53766e86e940d..6be7660d28f9a 100644 --- a/src/port/explicit_bzero.c +++ b/src/port/explicit_bzero.c @@ -2,7 +2,7 @@ * * explicit_bzero.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -16,7 +16,23 @@ #include "c.h" -#if HAVE_DECL_MEMSET_S +#if defined(HAVE_MEMSET_EXPLICIT) + +void +explicit_bzero(void *buf, size_t len) +{ + (void) memset_explicit(buf, 0, len); +} + +#elif defined(HAVE_EXPLICIT_MEMSET) + +void +explicit_bzero(void *buf, size_t len) +{ + (void) explicit_memset(buf, 0, len); +} + +#elif HAVE_DECL_MEMSET_S void explicit_bzero(void *buf, size_t len) diff --git a/src/port/getopt.c b/src/port/getopt.c index 655fef3b0c771..2b9f957abc044 100644 --- a/src/port/getopt.c +++ b/src/port/getopt.c @@ -32,11 +32,7 @@ #include "c.h" #include "pg_getopt.h" - -#if defined(LIBC_SCCS) && !defined(lint) -static char sccsid[] = "@(#)getopt.c 8.3 (Berkeley) 4/27/95"; -#endif /* LIBC_SCCS and not lint */ - +#include "port/pg_getopt_ctx.h" /* * On OpenBSD and some versions of Solaris, opterr and friends are defined in @@ -54,84 +50,39 @@ char *optarg; /* argument associated with option */ #endif -#define BADCH (int)'?' -#define BADARG (int)':' -#define EMSG "" - /* * getopt * Parse argc/argv argument vector. * + * We use the re-entrant pg_getopt_start/next() functions under the hood, but + * expose the standard non re-entrant API. + * * This implementation does not use optreset. Instead, we guarantee that * it can be restarted on a new argv array after a previous call returned -1, * if the caller resets optind to 1 before the first call of the new series. - * (Internally, this means we must be sure to reset "place" to EMSG before + * (Internally, this means we must be sure to reset "active" before * returning -1.) */ int getopt(int nargc, char *const *nargv, const char *ostr) { - static char *place = EMSG; /* option letter processing */ - char *oli; /* option letter list index */ + static bool active = false; + static pg_getopt_ctx ctx; + int result; - if (!*place) - { /* update scanning pointer */ - if (optind >= nargc || *(place = nargv[optind]) != '-') - { - place = EMSG; - return -1; - } - if (place[1] && *++place == '-' && place[1] == '\0') - { /* found "--" */ - ++optind; - place = EMSG; - return -1; - } - } /* option letter okay? */ - if ((optopt = (int) *place++) == (int) ':' || - !(oli = strchr(ostr, optopt))) + if (!active) { - /* - * if the user didn't specify '-' as an option, assume it means -1. - */ - if (optopt == (int) '-') - { - place = EMSG; - return -1; - } - if (!*place) - ++optind; - if (opterr && *ostr != ':') - (void) fprintf(stderr, - "illegal option -- %c\n", optopt); - return BADCH; + pg_getopt_start(&ctx, nargc, nargv, ostr); + ctx.opterr = opterr; + active = true; } - if (*++oli != ':') - { /* don't need argument */ - optarg = NULL; - if (!*place) - ++optind; - } - else - { /* need an argument */ - if (*place) /* no white space */ - optarg = place; - else if (nargc <= ++optind) - { /* no arg */ - place = EMSG; - if (*ostr == ':') - return BADARG; - if (opterr) - (void) fprintf(stderr, - "option requires an argument -- %c\n", - optopt); - return BADCH; - } - else - /* white space */ - optarg = nargv[optind]; - place = EMSG; - ++optind; - } - return optopt; /* dump back option letter */ + + result = pg_getopt_next(&ctx); + opterr = ctx.opterr; + optind = ctx.optind; + optopt = ctx.optopt; + optarg = ctx.optarg; + if (result == -1) + active = false; + return result; } diff --git a/src/port/getopt_long.c b/src/port/getopt_long.c index f83de0dff9789..20953db9db1e6 100644 --- a/src/port/getopt_long.c +++ b/src/port/getopt_long.c @@ -62,7 +62,7 @@ getopt_long(int argc, char *const argv[], const struct option *longopts, int *longindex) { static char *place = EMSG; /* option letter processing */ - char *oli; /* option letter list index */ + const char *oli; /* option letter list index */ static int nonopt_start = -1; static bool force_nonopt = false; diff --git a/src/port/getpeereid.c b/src/port/getpeereid.c index 345478599a014..e1371171a038e 100644 --- a/src/port/getpeereid.c +++ b/src/port/getpeereid.c @@ -3,7 +3,7 @@ * getpeereid.c * get peer userid for UNIX-domain socket connection * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/port/kill.c b/src/port/kill.c index 72fac20d50715..343c88628d7dc 100644 --- a/src/port/kill.c +++ b/src/port/kill.c @@ -3,7 +3,7 @@ * kill.c * kill() * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * This is a replacement version of kill for Win32 which sends * signals that the backend can recognize. diff --git a/src/port/meson.build b/src/port/meson.build index fc7b059fee50f..922b3f646768d 100644 --- a/src/port/meson.build +++ b/src/port/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pgport_sources = [ 'bsearch_arg.c', @@ -7,10 +7,12 @@ pgport_sources = [ 'noblock.c', 'path.c', 'pg_bitutils.c', + 'pg_cpu_x86.c', + 'pg_getopt_ctx.c', 'pg_localeconv_r.c', 'pg_numa.c', 'pg_popcount_aarch64.c', - 'pg_popcount_avx512.c', + 'pg_popcount_x86.c', 'pg_strong_random.c', 'pgcheckdir.c', 'pgmkdirp.c', @@ -72,7 +74,6 @@ replace_funcs_neg = [ ['mkdtemp'], ['strlcat'], ['strlcpy'], - ['strnlen'], ['strsep'], ['timingsafe_bcmp'], ] @@ -87,13 +88,12 @@ replace_funcs_pos = [ # x86/x64 ['pg_crc32c_sse42', 'USE_SSE42_CRC32C'], ['pg_crc32c_sse42', 'USE_SSE42_CRC32C_WITH_RUNTIME_CHECK'], - ['pg_crc32c_sse42_choose', 'USE_SSE42_CRC32C'], - ['pg_crc32c_sse42_choose', 'USE_SSE42_CRC32C_WITH_RUNTIME_CHECK'], ['pg_crc32c_sb8', 'USE_SSE42_CRC32C_WITH_RUNTIME_CHECK'], # arm / aarch64 ['pg_crc32c_armv8', 'USE_ARMV8_CRC32C'], ['pg_crc32c_armv8', 'USE_ARMV8_CRC32C_WITH_RUNTIME_CHECK', 'crc'], + ['pg_crc32c_armv8_choose', 'USE_ARMV8_CRC32C'], ['pg_crc32c_armv8_choose', 'USE_ARMV8_CRC32C_WITH_RUNTIME_CHECK'], ['pg_crc32c_sb8', 'USE_ARMV8_CRC32C_WITH_RUNTIME_CHECK'], @@ -193,6 +193,7 @@ foreach name, opts : pgport_variants c_pch: pch_c_h, kwargs: opts + { 'dependencies': opts['dependencies'] + [ssl], + 'install': install_internal_static_lib and name != '_srv', } ) pgport += {name: lib} diff --git a/src/port/mkdtemp.c b/src/port/mkdtemp.c index 027b95723fee7..7f8edc8a02d1c 100644 --- a/src/port/mkdtemp.c +++ b/src/port/mkdtemp.c @@ -3,7 +3,7 @@ * mkdtemp.c * create a mode-0700 temporary directory * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/port/noblock.c b/src/port/noblock.c index eee4d42d861d7..c4a6080990a78 100644 --- a/src/port/noblock.c +++ b/src/port/noblock.c @@ -3,7 +3,7 @@ * noblock.c * set a file descriptor as blocking or non-blocking * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/port/open.c b/src/port/open.c index 4a31c5d7b778b..2cd26060aa37c 100644 --- a/src/port/open.c +++ b/src/port/open.c @@ -4,7 +4,7 @@ * Win32 open() replacement * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/port/open.c * @@ -74,13 +74,23 @@ pgwin32_open_handle(const char *fileName, int fileFlags, bool backup_semantics) /* Check that we can handle the request */ assert((fileFlags & ((O_RDONLY | O_WRONLY | O_RDWR) | O_APPEND | (O_RANDOM | O_SEQUENTIAL | O_TEMPORARY) | - _O_SHORT_LIVED | O_DSYNC | O_DIRECT | + _O_SHORT_LIVED | O_DSYNC | O_DIRECT | O_CLOEXEC | (O_CREAT | O_TRUNC | O_EXCL) | (O_TEXT | O_BINARY))) == fileFlags); sa.nLength = sizeof(sa); - sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; + /* + * If O_CLOEXEC is specified, create a non-inheritable handle. Otherwise, + * create an inheritable handle (the default Windows behavior). + * + * Note: We could instead use SetHandleInformation() after CreateFile() to + * clear HANDLE_FLAG_INHERIT, but this way avoids rare leaks in + * multi-threaded programs that create processes, just like POSIX + * O_CLOEXEC. + */ + sa.bInheritHandle = !(fileFlags & O_CLOEXEC); + while ((h = CreateFile(fileName, /* cannot use O_RDONLY, as it == 0 */ (fileFlags & O_RDWR) ? (GENERIC_WRITE | GENERIC_READ) : diff --git a/src/port/path.c b/src/port/path.c index 63503409d5a8c..2395b96fd3b98 100644 --- a/src/port/path.c +++ b/src/port/path.c @@ -3,7 +3,7 @@ * path.c * portable path handling routines * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/pg_bitutils.c b/src/port/pg_bitutils.c index 61c7388f47477..7b11c38c41790 100644 --- a/src/port/pg_bitutils.c +++ b/src/port/pg_bitutils.c @@ -3,7 +3,7 @@ * pg_bitutils.c * Miscellaneous functions for bit-wise operations. * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/pg_bitutils.c @@ -12,13 +12,6 @@ */ #include "c.h" -#ifdef HAVE__GET_CPUID -#include -#endif -#ifdef HAVE__CPUID -#include -#endif - #include "port/pg_bitutils.h" @@ -104,307 +97,11 @@ const uint8 pg_number_of_ones[256] = { }; /* - * If we are building the Neon versions, we don't need the "slow" fallbacks. - */ -#ifndef POPCNT_AARCH64 -static inline int pg_popcount32_slow(uint32 word); -static inline int pg_popcount64_slow(uint64 word); -static uint64 pg_popcount_slow(const char *buf, int bytes); -static uint64 pg_popcount_masked_slow(const char *buf, int bytes, bits8 mask); -#endif - -#ifdef TRY_POPCNT_X86_64 -static bool pg_popcount_available(void); -static int pg_popcount32_choose(uint32 word); -static int pg_popcount64_choose(uint64 word); -static uint64 pg_popcount_choose(const char *buf, int bytes); -static uint64 pg_popcount_masked_choose(const char *buf, int bytes, bits8 mask); -static inline int pg_popcount32_fast(uint32 word); -static inline int pg_popcount64_fast(uint64 word); -static uint64 pg_popcount_fast(const char *buf, int bytes); -static uint64 pg_popcount_masked_fast(const char *buf, int bytes, bits8 mask); - -int (*pg_popcount32) (uint32 word) = pg_popcount32_choose; -int (*pg_popcount64) (uint64 word) = pg_popcount64_choose; -uint64 (*pg_popcount_optimized) (const char *buf, int bytes) = pg_popcount_choose; -uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, bits8 mask) = pg_popcount_masked_choose; -#endif /* TRY_POPCNT_X86_64 */ - -#ifdef TRY_POPCNT_X86_64 - -/* - * Return true if CPUID indicates that the POPCNT instruction is available. - */ -static bool -pg_popcount_available(void) -{ - unsigned int exx[4] = {0, 0, 0, 0}; - -#if defined(HAVE__GET_CPUID) - __get_cpuid(1, &exx[0], &exx[1], &exx[2], &exx[3]); -#elif defined(HAVE__CPUID) - __cpuid(exx, 1); -#else -#error cpuid instruction not available -#endif - - return (exx[2] & (1 << 23)) != 0; /* POPCNT */ -} - -/* - * These functions get called on the first call to pg_popcount32 etc. - * They detect whether we can use the asm implementations, and replace - * the function pointers so that subsequent calls are routed directly to - * the chosen implementation. - */ -static inline void -choose_popcount_functions(void) -{ - if (pg_popcount_available()) - { - pg_popcount32 = pg_popcount32_fast; - pg_popcount64 = pg_popcount64_fast; - pg_popcount_optimized = pg_popcount_fast; - pg_popcount_masked_optimized = pg_popcount_masked_fast; - } - else - { - pg_popcount32 = pg_popcount32_slow; - pg_popcount64 = pg_popcount64_slow; - pg_popcount_optimized = pg_popcount_slow; - pg_popcount_masked_optimized = pg_popcount_masked_slow; - } - -#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK - if (pg_popcount_avx512_available()) - { - pg_popcount_optimized = pg_popcount_avx512; - pg_popcount_masked_optimized = pg_popcount_masked_avx512; - } -#endif -} - -static int -pg_popcount32_choose(uint32 word) -{ - choose_popcount_functions(); - return pg_popcount32(word); -} - -static int -pg_popcount64_choose(uint64 word) -{ - choose_popcount_functions(); - return pg_popcount64(word); -} - -static uint64 -pg_popcount_choose(const char *buf, int bytes) -{ - choose_popcount_functions(); - return pg_popcount_optimized(buf, bytes); -} - -static uint64 -pg_popcount_masked_choose(const char *buf, int bytes, bits8 mask) -{ - choose_popcount_functions(); - return pg_popcount_masked(buf, bytes, mask); -} - -/* - * pg_popcount32_fast - * Return the number of 1 bits set in word - */ -static inline int -pg_popcount32_fast(uint32 word) -{ -#ifdef _MSC_VER - return __popcnt(word); -#else - uint32 res; - -__asm__ __volatile__(" popcntl %1,%0\n":"=q"(res):"rm"(word):"cc"); - return (int) res; -#endif -} - -/* - * pg_popcount64_fast - * Return the number of 1 bits set in word - */ -static inline int -pg_popcount64_fast(uint64 word) -{ -#ifdef _MSC_VER - return __popcnt64(word); -#else - uint64 res; - -__asm__ __volatile__(" popcntq %1,%0\n":"=q"(res):"rm"(word):"cc"); - return (int) res; -#endif -} - -/* - * pg_popcount_fast - * Returns the number of 1-bits in buf - */ -static uint64 -pg_popcount_fast(const char *buf, int bytes) -{ - uint64 popcnt = 0; - -#if SIZEOF_VOID_P >= 8 - /* Process in 64-bit chunks if the buffer is aligned. */ - if (buf == (const char *) TYPEALIGN(8, buf)) - { - const uint64 *words = (const uint64 *) buf; - - while (bytes >= 8) - { - popcnt += pg_popcount64_fast(*words++); - bytes -= 8; - } - - buf = (const char *) words; - } -#else - /* Process in 32-bit chunks if the buffer is aligned. */ - if (buf == (const char *) TYPEALIGN(4, buf)) - { - const uint32 *words = (const uint32 *) buf; - - while (bytes >= 4) - { - popcnt += pg_popcount32_fast(*words++); - bytes -= 4; - } - - buf = (const char *) words; - } -#endif - - /* Process any remaining bytes */ - while (bytes--) - popcnt += pg_number_of_ones[(unsigned char) *buf++]; - - return popcnt; -} - -/* - * pg_popcount_masked_fast - * Returns the number of 1-bits in buf after applying the mask to each byte - */ -static uint64 -pg_popcount_masked_fast(const char *buf, int bytes, bits8 mask) -{ - uint64 popcnt = 0; - -#if SIZEOF_VOID_P >= 8 - /* Process in 64-bit chunks if the buffer is aligned */ - uint64 maskv = ~UINT64CONST(0) / 0xFF * mask; - - if (buf == (const char *) TYPEALIGN(8, buf)) - { - const uint64 *words = (const uint64 *) buf; - - while (bytes >= 8) - { - popcnt += pg_popcount64_fast(*words++ & maskv); - bytes -= 8; - } - - buf = (const char *) words; - } -#else - /* Process in 32-bit chunks if the buffer is aligned. */ - uint32 maskv = ~((uint32) 0) / 0xFF * mask; - - if (buf == (const char *) TYPEALIGN(4, buf)) - { - const uint32 *words = (const uint32 *) buf; - - while (bytes >= 4) - { - popcnt += pg_popcount32_fast(*words++ & maskv); - bytes -= 4; - } - - buf = (const char *) words; - } -#endif - - /* Process any remaining bytes */ - while (bytes--) - popcnt += pg_number_of_ones[(unsigned char) *buf++ & mask]; - - return popcnt; -} - -#endif /* TRY_POPCNT_X86_64 */ - -/* - * If we are building the Neon versions, we don't need the "slow" fallbacks. - */ -#ifndef POPCNT_AARCH64 - -/* - * pg_popcount32_slow - * Return the number of 1 bits set in word - */ -static inline int -pg_popcount32_slow(uint32 word) -{ -#ifdef HAVE__BUILTIN_POPCOUNT - return __builtin_popcount(word); -#else /* !HAVE__BUILTIN_POPCOUNT */ - int result = 0; - - while (word != 0) - { - result += pg_number_of_ones[word & 255]; - word >>= 8; - } - - return result; -#endif /* HAVE__BUILTIN_POPCOUNT */ -} - -/* - * pg_popcount64_slow - * Return the number of 1 bits set in word - */ -static inline int -pg_popcount64_slow(uint64 word) -{ -#ifdef HAVE__BUILTIN_POPCOUNT -#if SIZEOF_LONG == 8 - return __builtin_popcountl(word); -#elif SIZEOF_LONG_LONG == 8 - return __builtin_popcountll(word); -#else -#error "cannot find integer of the same size as uint64_t" -#endif -#else /* !HAVE__BUILTIN_POPCOUNT */ - int result = 0; - - while (word != 0) - { - result += pg_number_of_ones[word & 255]; - word >>= 8; - } - - return result; -#endif /* HAVE__BUILTIN_POPCOUNT */ -} - -/* - * pg_popcount_slow + * pg_popcount_portable * Returns the number of 1-bits in buf */ -static uint64 -pg_popcount_slow(const char *buf, int bytes) +uint64 +pg_popcount_portable(const char *buf, int bytes) { uint64 popcnt = 0; @@ -416,24 +113,10 @@ pg_popcount_slow(const char *buf, int bytes) while (bytes >= 8) { - popcnt += pg_popcount64_slow(*words++); + popcnt += pg_popcount64(*words++); bytes -= 8; } - buf = (const char *) words; - } -#else - /* Process in 32-bit chunks if the buffer is aligned. */ - if (buf == (const char *) TYPEALIGN(4, buf)) - { - const uint32 *words = (const uint32 *) buf; - - while (bytes >= 4) - { - popcnt += pg_popcount32_slow(*words++); - bytes -= 4; - } - buf = (const char *) words; } #endif @@ -446,11 +129,11 @@ pg_popcount_slow(const char *buf, int bytes) } /* - * pg_popcount_masked_slow + * pg_popcount_masked_portable * Returns the number of 1-bits in buf after applying the mask to each byte */ -static uint64 -pg_popcount_masked_slow(const char *buf, int bytes, bits8 mask) +uint64 +pg_popcount_masked_portable(const char *buf, int bytes, uint8 mask) { uint64 popcnt = 0; @@ -464,26 +147,10 @@ pg_popcount_masked_slow(const char *buf, int bytes, bits8 mask) while (bytes >= 8) { - popcnt += pg_popcount64_slow(*words++ & maskv); + popcnt += pg_popcount64(*words++ & maskv); bytes -= 8; } - buf = (const char *) words; - } -#else - /* Process in 32-bit chunks if the buffer is aligned. */ - uint32 maskv = ~((uint32) 0) / 0xFF * mask; - - if (buf == (const char *) TYPEALIGN(4, buf)) - { - const uint32 *words = (const uint32 *) buf; - - while (bytes >= 4) - { - popcnt += pg_popcount32_slow(*words++ & maskv); - bytes -= 4; - } - buf = (const char *) words; } #endif @@ -495,27 +162,14 @@ pg_popcount_masked_slow(const char *buf, int bytes, bits8 mask) return popcnt; } -#endif /* ! POPCNT_AARCH64 */ - -#if !defined(TRY_POPCNT_X86_64) && !defined(POPCNT_AARCH64) +#if !defined(HAVE_X86_64_POPCNTQ) && !defined(USE_NEON) /* * When special CPU instructions are not available, there's no point in using - * function pointers to vary the implementation between the fast and slow - * method. We instead just make these actual external functions. The compiler - * should be able to inline the slow versions here. + * function pointers to vary the implementation. We instead just make these + * actual external functions. The compiler should be able to inline the + * portable versions here. */ -int -pg_popcount32(uint32 word) -{ - return pg_popcount32_slow(word); -} - -int -pg_popcount64(uint64 word) -{ - return pg_popcount64_slow(word); -} /* * pg_popcount_optimized @@ -524,7 +178,7 @@ pg_popcount64(uint64 word) uint64 pg_popcount_optimized(const char *buf, int bytes) { - return pg_popcount_slow(buf, bytes); + return pg_popcount_portable(buf, bytes); } /* @@ -532,9 +186,9 @@ pg_popcount_optimized(const char *buf, int bytes) * Returns the number of 1-bits in buf after applying the mask to each byte */ uint64 -pg_popcount_masked_optimized(const char *buf, int bytes, bits8 mask) +pg_popcount_masked_optimized(const char *buf, int bytes, uint8 mask) { - return pg_popcount_masked_slow(buf, bytes, mask); + return pg_popcount_masked_portable(buf, bytes, mask); } -#endif /* ! TRY_POPCNT_X86_64 && ! POPCNT_AARCH64 */ +#endif /* ! HAVE_X86_64_POPCNTQ && ! USE_NEON */ diff --git a/src/port/pg_cpu_x86.c b/src/port/pg_cpu_x86.c new file mode 100644 index 0000000000000..150b4a1d574d2 --- /dev/null +++ b/src/port/pg_cpu_x86.c @@ -0,0 +1,267 @@ +/*------------------------------------------------------------------------- + * + * pg_cpu_x86.c + * Runtime CPU feature detection for x86 + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/port/pg_cpu_x86.c + * + *------------------------------------------------------------------------- + */ + +#include "c.h" + +#if defined(USE_SSE2) || defined(__i386__) + +#ifdef _MSC_VER +#include +#else +#include +#endif + +#ifdef HAVE_XSAVE_INTRINSICS +#include +#endif + +#include "port/pg_cpu.h" + +/* + * XSAVE state component bits that we need + * + * https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-1-manual.pdf + * Chapter "MANAGING STATE USING THE XSAVE FEATURE SET" + */ +#define XMM (1<<1) +#define YMM (1<<2) +#define OPMASK (1<<5) +#define ZMM0_15 (1<<6) +#define ZMM16_31 (1<<7) + + +/* array indexed by enum X86FeatureId */ +bool X86Features[X86FeaturesSize] = {0}; + +static bool +mask_available(uint32 value, uint32 mask) +{ + return (value & mask) == mask; +} + +/* Named indexes for CPUID register array */ +#define EAX 0 +#define EBX 1 +#define ECX 2 +#define EDX 3 + +/* + * Request CPUID information for the specified leaf. + */ +static inline void +pg_cpuid(int leaf, unsigned int *reg) +{ +#if defined(HAVE__GET_CPUID) + __get_cpuid(leaf, ®[EAX], ®[EBX], ®[ECX], ®[EDX]); +#elif defined(HAVE__CPUID) + __cpuid((int *) reg, leaf); +#else +#error cpuid instruction not available +#endif +} + +/* + * Request CPUID information for the specified leaf and subleaf. + * + * Returns true if the CPUID leaf/subleaf is supported, false otherwise. + */ +static inline bool +pg_cpuid_subleaf(int leaf, int subleaf, unsigned int *reg) +{ + memset(reg, 0, 4 * sizeof(unsigned int)); +#if defined(HAVE__GET_CPUID_COUNT) + return __get_cpuid_count(leaf, subleaf, ®[EAX], ®[EBX], ®[ECX], ®[EDX]) == 1; +#elif defined(HAVE__CPUIDEX) + __cpuidex((int *) reg, leaf, subleaf); + return true; +#else + return false; +#endif +} + +/* + * Parse the CPU ID info for runtime checks. + */ +#ifdef HAVE_XSAVE_INTRINSICS +pg_attribute_target("xsave") +#endif +void +set_x86_features(void) +{ + unsigned int reg[4] = {0}; + bool have_osxsave; + + pg_cpuid(0x01, reg); + + X86Features[PG_SSE4_2] = reg[ECX] >> 20 & 1; + X86Features[PG_POPCNT] = reg[ECX] >> 23 & 1; + X86Features[PG_HYPERVISOR] = reg[ECX] >> 31 & 1; + have_osxsave = reg[ECX] >> 27 & 1; + + pg_cpuid_subleaf(0x07, 0, reg); + + X86Features[PG_TSC_ADJUST] = reg[EBX] >> 1 & 1; + + /* leaf 7 features that depend on OSXSAVE */ + if (have_osxsave) + { + uint32 xcr0_val = 0; + +#ifdef HAVE_XSAVE_INTRINSICS + /* get value of Extended Control Register */ + xcr0_val = _xgetbv(0); +#endif + + /* Are YMM registers enabled? */ + if (mask_available(xcr0_val, XMM | YMM)) + X86Features[PG_AVX2] = reg[EBX] >> 5 & 1; + + /* Are ZMM registers enabled? */ + if (mask_available(xcr0_val, XMM | YMM | + OPMASK | ZMM0_15 | ZMM16_31)) + { + X86Features[PG_AVX512_BW] = reg[EBX] >> 30 & 1; + X86Features[PG_AVX512_VL] = reg[EBX] >> 31 & 1; + + X86Features[PG_AVX512_VPCLMULQDQ] = reg[ECX] >> 10 & 1; + X86Features[PG_AVX512_VPOPCNTDQ] = reg[ECX] >> 14 & 1; + } + } + + /* Check for other TSC related flags */ + pg_cpuid(0x80000001, reg); + X86Features[PG_RDTSCP] = reg[EDX] >> 27 & 1; + + pg_cpuid(0x80000007, reg); + X86Features[PG_TSC_INVARIANT] = reg[EDX] >> 8 & 1; + + X86Features[INIT_PG_X86] = true; +} + +/* TSC (Time-stamp Counter) handling code */ + +static uint32 x86_hypervisor_tsc_frequency_khz(void); + +/* + * Determine the TSC frequency of the CPU through CPUID, where supported. + * + * Needed to interpret the tick value returned by RDTSC/RDTSCP. Return value of + * 0 indicates the frequency information was not accessible via CPUID. + */ +uint32 +x86_tsc_frequency_khz(void) +{ + unsigned int reg[4] = {0}; + + /* + * If we're inside a virtual machine, try to fetch the TSC frequency from + * the hypervisor, using a hypervisor specific method. + * + * Note it is not safe to utilize the regular 0x15/0x16 CPUID registers + * (i.e. the logic below) in virtual machines, as they have been observed + * to be wildly incorrect when virtualized. + */ + if (x86_feature_available(PG_HYPERVISOR)) + return x86_hypervisor_tsc_frequency_khz(); + + /* + * On modern Intel CPUs, the TSC is implemented by invariant timekeeping + * hardware, also called "Always Running Timer", or ART. The ART stays + * consistent even if the CPU changes frequency due to changing power + * levels. + * + * As documented in "Determining the Processor Base Frequency" in the + * "Intel® 64 and IA-32 Architectures Software Developer's Manual", + * February 2026 Edition, we can get the TSC frequency as follows: + * + * Nominal TSC frequency = ( CPUID.15H:ECX[31:0] * CPUID.15H:EBX[31:0] ) / + * CPUID.15H:EAX[31:0] + * + * With CPUID.15H:ECX representing the nominal core crystal clock + * frequency, and EAX/EBX representing values used to translate the TSC + * value to that frequency, see "Chapter 20.17 "Time-Stamp Counter" of + * that manual. + * + * Older Intel CPUs, and other vendors do not set CPUID.15H:ECX, and as + * such we fall back to alternate approaches. + */ + pg_cpuid(0x15, reg); + if (reg[ECX] > 0) + { + /* + * EBX not being set indicates invariant TSC is not available. Require + * EAX being non-zero too, to avoid a theoretical divide by zero. + */ + if (reg[EAX] == 0 || reg[EBX] == 0) + return 0; + + return reg[ECX] / 1000 * reg[EBX] / reg[EAX]; + } + + /* + * When CPUID.15H is not available/incomplete, we can instead try to get + * the processor base frequency in MHz from CPUID.16H:EAX, the "Processor + * Frequency Information Leaf". + */ + pg_cpuid(0x16, reg); + if (reg[EAX] > 0) + return reg[EAX] * 1000; + + return 0; +} + +/* + * Support for reading TSC frequency for hypervisors passing it to a guest VM. + * + * Two Hypervisors (VMware and KVM) are known to make TSC frequency in KHz + * available at the vendor-specific 0x40000010 leaf in the EAX register. + * + * For some other Hypervisors that have an invariant TSC, e.g. HyperV, we would + * need to access a model-specific register (MSR) to get the frequency. MSRs are + * separate from CPUID and typically not available for unprivileged processes, + * so we can't get the frequency this way. + */ +#define CPUID_HYPERVISOR_VMWARE(r) (r[EBX] == 0x61774d56 && r[ECX] == 0x4d566572 && r[EDX] == 0x65726177) /* VMwareVMware */ +#define CPUID_HYPERVISOR_KVM(r) (r[EBX] == 0x4b4d564b && r[ECX] == 0x564b4d56 && r[EDX] == 0x0000004d) /* KVMKVMKVM */ +static uint32 +x86_hypervisor_tsc_frequency_khz(void) +{ +#if defined(HAVE__CPUIDEX) + unsigned int reg[4] = {0}; + + /* + * The hypervisor is determined using the 0x40000000 Hypervisor + * information leaf, which requires use of __cpuidex to set ECX to 0 to + * access it. + * + * The similar __get_cpuid_count function does not work as expected since + * it contains a check for __get_cpuid_max, which has been observed to be + * lower than the special Hypervisor leaf, despite it being available. + */ + __cpuidex((int *) reg, 0x40000000, 0); + + if (reg[EAX] >= 0x40000010 && (CPUID_HYPERVISOR_VMWARE(reg) || CPUID_HYPERVISOR_KVM(reg))) + { + __cpuidex((int *) reg, 0x40000010, 0); + if (reg[EAX] > 0) + return reg[EAX]; + } +#endif /* HAVE__CPUIDEX */ + + return 0; +} + + +#endif /* defined(USE_SSE2) || defined(__i386__) */ diff --git a/src/port/pg_crc32c_armv8.c b/src/port/pg_crc32c_armv8.c index 5ba070bb99d8b..8cf838a510a21 100644 --- a/src/port/pg_crc32c_armv8.c +++ b/src/port/pg_crc32c_armv8.c @@ -3,7 +3,7 @@ * pg_crc32c_armv8.c * Compute CRC-32C checksum using ARMv8 CRC Extension instructions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -14,7 +14,15 @@ */ #include "c.h" +#ifdef _MSC_VER +#include +#else #include +#endif + +#ifdef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK +#include +#endif #include "port/pg_crc32c.h" @@ -38,32 +46,32 @@ pg_comp_crc32c_armv8(pg_crc32c crc, const void *data, size_t len) if (!PointerIsAligned(p, uint32) && p + 2 <= pend) { - crc = __crc32ch(crc, *(uint16 *) p); + crc = __crc32ch(crc, *(const uint16 *) p); p += 2; } if (!PointerIsAligned(p, uint64) && p + 4 <= pend) { - crc = __crc32cw(crc, *(uint32 *) p); + crc = __crc32cw(crc, *(const uint32 *) p); p += 4; } /* Process eight bytes at a time, as far as we can. */ while (p + 8 <= pend) { - crc = __crc32cd(crc, *(uint64 *) p); + crc = __crc32cd(crc, *(const uint64 *) p); p += 8; } /* Process remaining 0-7 bytes. */ if (p + 4 <= pend) { - crc = __crc32cw(crc, *(uint32 *) p); + crc = __crc32cw(crc, *(const uint32 *) p); p += 4; } if (p + 2 <= pend) { - crc = __crc32ch(crc, *(uint16 *) p); + crc = __crc32ch(crc, *(const uint16 *) p); p += 2; } if (p < pend) @@ -73,3 +81,137 @@ pg_comp_crc32c_armv8(pg_crc32c crc, const void *data, size_t len) return crc; } + +#ifdef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK + +/* + * Note: There is no copyright notice in the following generated code. + * + * We have modified the output to + * - match our function declaration + * - match whitespace to our project style + * - be more friendly for pgindent + * - exit early for small inputs + */ + +/* Generated by https://github.com/corsix/fast-crc32/ using: */ +/* ./generate -i neon -p crc32c -a v4e */ +/* MIT licensed */ + +pg_attribute_target("+crypto") +static inline +uint64x2_t +clmul_lo_e(uint64x2_t a, uint64x2_t b, uint64x2_t c) +{ + uint64x2_t r; + +__asm("pmull %0.1q, %2.1d, %3.1d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r), "+w"(c):"w"(a), "w"(b)); + return r; +} + +pg_attribute_target("+crypto") +static inline +uint64x2_t +clmul_hi_e(uint64x2_t a, uint64x2_t b, uint64x2_t c) +{ + uint64x2_t r; + +__asm("pmull2 %0.1q, %2.2d, %3.2d\neor %0.16b, %0.16b, %1.16b\n":"=w"(r), "+w"(c):"w"(a), "w"(b)); + return r; +} + +pg_attribute_target("+crypto") +pg_crc32c +pg_comp_crc32c_pmull(pg_crc32c crc, const void *data, size_t len) +{ + /* adjust names to match generated code */ + pg_crc32c crc0 = crc; + const char *buf = data; + + /* + * Immediately fall back to the scalar path if the vector path is not + * guaranteed to perform at least one iteration after the alignment + * preamble. + */ + if (len < 5 * sizeof(uint64x2_t)) + return pg_comp_crc32c_armv8(crc, data, len); + + /* align to 16 bytes */ + for (; (uintptr_t) buf & 7; --len) + { + crc0 = __crc32cb(crc0, *buf++); + } + if ((uintptr_t) buf & 8) + { + crc0 = __crc32cd(crc0, *(const uint64_t *) buf); + buf += 8; + len -= 8; + } + + Assert(len >= 64); + + { + const char *end = buf + len; + const char *limit = buf + len - 64; + + /* First vector chunk. */ + uint64x2_t x0 = vld1q_u64((const uint64_t *) buf), + y0; + uint64x2_t x1 = vld1q_u64((const uint64_t *) (buf + 16)), + y1; + uint64x2_t x2 = vld1q_u64((const uint64_t *) (buf + 32)), + y2; + uint64x2_t x3 = vld1q_u64((const uint64_t *) (buf + 48)), + y3; + uint64x2_t k; + + { + static const uint64_t pg_attribute_aligned(16) k_[] = {0x740eef02, 0x9e4addf8}; + + k = vld1q_u64(k_); + } + + /* + * pgindent complained of unmatched parens, so the following has been + * re-written with intrinsics: + * + * x0 = veorq_u64((uint64x2_t) {crc0, 0}, x0); + */ + x0 = veorq_u64((uint64x2_t) vsetq_lane_u64(crc0, vdupq_n_u64(0), 0), x0); + buf += 64; + + /* Main loop. */ + while (buf <= limit) + { + y0 = clmul_lo_e(x0, k, vld1q_u64((const uint64_t *) buf)), x0 = clmul_hi_e(x0, k, y0); + y1 = clmul_lo_e(x1, k, vld1q_u64((const uint64_t *) (buf + 16))), x1 = clmul_hi_e(x1, k, y1); + y2 = clmul_lo_e(x2, k, vld1q_u64((const uint64_t *) (buf + 32))), x2 = clmul_hi_e(x2, k, y2); + y3 = clmul_lo_e(x3, k, vld1q_u64((const uint64_t *) (buf + 48))), x3 = clmul_hi_e(x3, k, y3); + buf += 64; + } + + /* Reduce x0 ... x3 to just x0. */ + { + static const uint64_t pg_attribute_aligned(16) k_[] = {0xf20c0dfe, 0x493c7d27}; + + k = vld1q_u64(k_); + } + y0 = clmul_lo_e(x0, k, x1), x0 = clmul_hi_e(x0, k, y0); + y2 = clmul_lo_e(x2, k, x3), x2 = clmul_hi_e(x2, k, y2); + { + static const uint64_t pg_attribute_aligned(16) k_[] = {0x3da6d0cb, 0xba4fc28e}; + + k = vld1q_u64(k_); + } + y0 = clmul_lo_e(x0, k, x2), x0 = clmul_hi_e(x0, k, y0); + + /* Reduce 128 bits to 32 bits, and multiply by x^32. */ + crc0 = __crc32cd(0, vgetq_lane_u64(x0, 0)); + crc0 = __crc32cd(crc0, vgetq_lane_u64(x0, 1)); + len = end - buf; + } + + return pg_comp_crc32c_armv8(crc0, buf, len); +} + +#endif diff --git a/src/port/pg_crc32c_armv8_choose.c b/src/port/pg_crc32c_armv8_choose.c index ec12be1bbc393..591e23df44b45 100644 --- a/src/port/pg_crc32c_armv8_choose.c +++ b/src/port/pg_crc32c_armv8_choose.c @@ -8,7 +8,7 @@ * computation. Otherwise, fall back to the pure software implementation * (slicing-by-8). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -26,7 +26,8 @@ #if defined(HAVE_ELF_AUX_INFO) || defined(HAVE_GETAUXVAL) #include -#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32) +/* Ancient glibc releases don't include the HWCAPxxx macros in sys/auxv.h */ +#if defined(__linux__) && (defined(__aarch64__) ? !defined(HWCAP_CRC32) : !defined(HWCAP2_CRC32)) #include #endif #endif @@ -107,6 +108,29 @@ pg_crc32c_armv8_available(void) #endif } +#ifdef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK +static bool +pg_pmull_available(void) +{ +#if defined(__aarch64__) && defined(HWCAP_PMULL) + +#ifdef HAVE_ELF_AUX_INFO + unsigned long value; + + return elf_aux_info(AT_HWCAP, &value, sizeof(value)) == 0 && + (value & HWCAP_PMULL) != 0; +#elif defined(HAVE_GETAUXVAL) + return (getauxval(AT_HWCAP) & HWCAP_PMULL) != 0; +#else + return false; +#endif + +#else + return false; +#endif +} +#endif /* USE_PMULL_CRC32C_WITH_RUNTIME_CHECK */ + /* * This gets called on the first call. It replaces the function pointer * so that subsequent calls are routed directly to the chosen implementation. @@ -114,10 +138,23 @@ pg_crc32c_armv8_available(void) static pg_crc32c pg_comp_crc32c_choose(pg_crc32c crc, const void *data, size_t len) { + /* set fallbacks */ +#ifdef USE_ARMV8_CRC32C + /* On e.g. MacOS, our runtime feature detection doesn't work */ + pg_comp_crc32c = pg_comp_crc32c_armv8; +#else + pg_comp_crc32c = pg_comp_crc32c_sb8; +#endif + if (pg_crc32c_armv8_available()) + { pg_comp_crc32c = pg_comp_crc32c_armv8; - else - pg_comp_crc32c = pg_comp_crc32c_sb8; + +#ifdef USE_PMULL_CRC32C_WITH_RUNTIME_CHECK + if (pg_pmull_available()) + pg_comp_crc32c = pg_comp_crc32c_pmull; +#endif + } return pg_comp_crc32c(crc, data, len); } diff --git a/src/port/pg_crc32c_loongarch.c b/src/port/pg_crc32c_loongarch.c index 90679ce0d0cf3..f0afc7119ddfd 100644 --- a/src/port/pg_crc32c_loongarch.c +++ b/src/port/pg_crc32c_loongarch.c @@ -3,7 +3,7 @@ * pg_crc32c_loongarch.c * Compute CRC-32C checksum using LoongArch CRCC instructions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/pg_crc32c_sb8.c b/src/port/pg_crc32c_sb8.c index 19659d186a00b..b91667ad29cdc 100644 --- a/src/port/pg_crc32c_sb8.c +++ b/src/port/pg_crc32c_sb8.c @@ -8,7 +8,7 @@ * Generation", IEEE Transactions on Computers, vol.57, no. 11, * pp. 1550-1560, November 2008, doi:10.1109/TC.2008.85 * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/pg_crc32c_sse42.c b/src/port/pg_crc32c_sse42.c index 9af3474a6ca95..b8e77faf4d9d1 100644 --- a/src/port/pg_crc32c_sse42.c +++ b/src/port/pg_crc32c_sse42.c @@ -3,7 +3,7 @@ * pg_crc32c_sse42.c * Compute CRC-32C checksum using Intel SSE 4.2 or AVX-512 instructions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -19,8 +19,11 @@ #include #endif +#include "port/pg_cpu.h" #include "port/pg_crc32c.h" +static pg_crc32c pg_comp_crc32c_choose(pg_crc32c crc, const void *data, size_t len); + pg_attribute_no_sanitize_alignment() pg_attribute_target("sse4.2") pg_crc32c @@ -123,7 +126,7 @@ pg_comp_crc32c_avx512(pg_crc32c crc, const void *data, size_t len) __m512i k; k = _mm512_broadcast_i32x4(_mm_setr_epi32(0x740eef02, 0, 0x9e4addf8, 0)); - x0 = _mm512_xor_si512(_mm512_castsi128_si512(_mm_cvtsi32_si128(crc0)), x0); + x0 = _mm512_xor_si512(_mm512_zextsi128_si512(_mm_cvtsi32_si128(crc0)), x0); buf += 64; /* Main loop. */ @@ -158,4 +161,33 @@ pg_comp_crc32c_avx512(pg_crc32c crc, const void *data, size_t len) return pg_comp_crc32c_sse42(crc0, buf, len); } +#endif /* USE_AVX512_CRC32C_WITH_RUNTIME_CHECK */ + +/* + * This gets called on the first call. It replaces the function pointer + * so that subsequent calls are routed directly to the chosen implementation. + */ +static pg_crc32c +pg_comp_crc32c_choose(pg_crc32c crc, const void *data, size_t len) +{ + /* + * Set fallback. We must guard since slicing-by-8 is not visible + * everywhere. + */ +#ifdef USE_SSE42_CRC32C_WITH_RUNTIME_CHECK + pg_comp_crc32c = pg_comp_crc32c_sb8; +#endif + + if (x86_feature_available(PG_SSE4_2)) + pg_comp_crc32c = pg_comp_crc32c_sse42; + +#ifdef USE_AVX512_CRC32C_WITH_RUNTIME_CHECK + if (x86_feature_available(PG_AVX512_VL) && + x86_feature_available(PG_AVX512_VPCLMULQDQ)) + pg_comp_crc32c = pg_comp_crc32c_avx512; #endif + + return pg_comp_crc32c(crc, data, len); +} + +pg_crc32c (*pg_comp_crc32c) (pg_crc32c crc, const void *data, size_t len) = pg_comp_crc32c_choose; diff --git a/src/port/pg_crc32c_sse42_choose.c b/src/port/pg_crc32c_sse42_choose.c deleted file mode 100644 index 74d2421ba2be9..0000000000000 --- a/src/port/pg_crc32c_sse42_choose.c +++ /dev/null @@ -1,109 +0,0 @@ -/*------------------------------------------------------------------------- - * - * pg_crc32c_sse42_choose.c - * Choose between Intel SSE 4.2 and software CRC-32C implementation. - * - * On first call, checks if the CPU we're running on supports Intel SSE - * 4.2. If it does, use the special SSE instructions for CRC-32C - * computation. Otherwise, fall back to the pure software implementation - * (slicing-by-8). - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * - * IDENTIFICATION - * src/port/pg_crc32c_sse42_choose.c - * - *------------------------------------------------------------------------- - */ - -#include "c.h" - -#if defined(HAVE__GET_CPUID) || defined(HAVE__GET_CPUID_COUNT) -#include -#endif - -#if defined(HAVE__CPUID) || defined(HAVE__CPUIDEX) -#include -#endif - -#ifdef HAVE_XSAVE_INTRINSICS -#include -#endif - -#include "port/pg_crc32c.h" - -/* - * Does XGETBV say the ZMM registers are enabled? - * - * NB: Caller is responsible for verifying that osxsave is available - * before calling this. - */ -#ifdef HAVE_XSAVE_INTRINSICS -pg_attribute_target("xsave") -#endif -static bool -zmm_regs_available(void) -{ -#ifdef HAVE_XSAVE_INTRINSICS - return (_xgetbv(0) & 0xe6) == 0xe6; -#else - return false; -#endif -} - -/* - * This gets called on the first call. It replaces the function pointer - * so that subsequent calls are routed directly to the chosen implementation. - */ -static pg_crc32c -pg_comp_crc32c_choose(pg_crc32c crc, const void *data, size_t len) -{ - unsigned int exx[4] = {0, 0, 0, 0}; - - /* - * Set fallback. We must guard since slicing-by-8 is not visible - * everywhere. - */ -#ifdef USE_SSE42_CRC32C_WITH_RUNTIME_CHECK - pg_comp_crc32c = pg_comp_crc32c_sb8; -#endif - -#if defined(HAVE__GET_CPUID) - __get_cpuid(1, &exx[0], &exx[1], &exx[2], &exx[3]); -#elif defined(HAVE__CPUID) - __cpuid(exx, 1); -#else -#error cpuid instruction not available -#endif - - if ((exx[2] & (1 << 20)) != 0) /* SSE 4.2 */ - { - pg_comp_crc32c = pg_comp_crc32c_sse42; - - if (exx[2] & (1 << 27) && /* OSXSAVE */ - zmm_regs_available()) - { - /* second cpuid call on leaf 7 to check extended AVX-512 support */ - - memset(exx, 0, 4 * sizeof(exx[0])); - -#if defined(HAVE__GET_CPUID_COUNT) - __get_cpuid_count(7, 0, &exx[0], &exx[1], &exx[2], &exx[3]); -#elif defined(HAVE__CPUIDEX) - __cpuidex(exx, 7, 0); -#endif - -#ifdef USE_AVX512_CRC32C_WITH_RUNTIME_CHECK - if (exx[2] & (1 << 10) && /* VPCLMULQDQ */ - exx[1] & (1 << 31)) /* AVX512-VL */ - pg_comp_crc32c = pg_comp_crc32c_avx512; -#endif - } - } - - return pg_comp_crc32c(crc, data, len); -} - -pg_crc32c (*pg_comp_crc32c) (pg_crc32c crc, const void *data, size_t len) = pg_comp_crc32c_choose; diff --git a/src/port/pg_getopt_ctx.c b/src/port/pg_getopt_ctx.c new file mode 100644 index 0000000000000..7a6d2fd6e922a --- /dev/null +++ b/src/port/pg_getopt_ctx.c @@ -0,0 +1,136 @@ +/*------------------------------------------------------------------------- + * + * pg_getopt_ctx.c + * Thread-safe implementation of getopt() + * + * Copyright (c) 1987, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * + * IDENTIFICATION + * src/port/pg_getopt_ctx.c + * + *------------------------------------------------------------------------- + */ + +#include "c.h" + +#include "port/pg_getopt_ctx.h" + +#define BADCH (int)'?' +#define BADARG (int)':' +#define EMSG "" + +/* + * Start parsing argc/argv argument vector. + * + * This is a re-entrant version of the standard library getopt(3) function. + * To use, first call pg_getopt_start() to initialize the state, and then call + * pg_getopt_next() until it returns -1. + */ +void +pg_getopt_start(pg_getopt_ctx *ctx, int nargc, char *const *nargv, const char *ostr) +{ + ctx->nargc = nargc; + ctx->nargv = nargv; + ctx->ostr = ostr; + + ctx->optind = 1; + ctx->optarg = NULL; + ctx->opterr = 1; /* Caller may clear this after the call */ + ctx->optopt = 0; + + ctx->place = EMSG; /* option letter processing */ +} + +/* + * Parse next option in argc/argv argument vector + */ +int +pg_getopt_next(pg_getopt_ctx *ctx) +{ + const char *oli; /* option letter list index */ + + if (!*ctx->place) + { /* update scanning pointer */ + if (ctx->optind >= ctx->nargc || *(ctx->place = ctx->nargv[ctx->optind]) != '-') + { + ctx->place = EMSG; + return -1; + } + if (ctx->place[1] && *++ctx->place == '-' && ctx->place[1] == '\0') + { /* found "--" */ + ++ctx->optind; + ctx->place = EMSG; + return -1; + } + } /* option letter okay? */ + if ((ctx->optopt = (int) *ctx->place++) == (int) ':' || + !(oli = strchr(ctx->ostr, ctx->optopt))) + { + /* + * if the user didn't specify '-' as an option, assume it means -1. + */ + if (ctx->optopt == (int) '-') + { + ctx->place = EMSG; + return -1; + } + if (!*ctx->place) + ++ctx->optind; + if (ctx->opterr && *ctx->ostr != ':') + (void) fprintf(stderr, + "illegal option -- %c\n", ctx->optopt); + return BADCH; + } + if (*++oli != ':') + { /* don't need argument */ + ctx->optarg = NULL; + if (!*ctx->place) + ++ctx->optind; + } + else + { /* need an argument */ + if (*ctx->place) /* no white space */ + ctx->optarg = ctx->place; + else if (ctx->nargc <= ++ctx->optind) + { /* no arg */ + ctx->place = EMSG; + if (*ctx->ostr == ':') + return BADARG; + if (ctx->opterr) + (void) fprintf(stderr, + "option requires an argument -- %c\n", + ctx->optopt); + return BADCH; + } + else + /* white space */ + ctx->optarg = ctx->nargv[ctx->optind]; + ctx->place = EMSG; + ++ctx->optind; + } + return ctx->optopt; /* dump back option letter */ +} diff --git a/src/port/pg_localeconv_r.c b/src/port/pg_localeconv_r.c index 61510b2e0ea37..640cb97a62ef2 100644 --- a/src/port/pg_localeconv_r.c +++ b/src/port/pg_localeconv_r.c @@ -3,7 +3,7 @@ * pg_localeconv_r.c * Thread-safe implementations of localeconv() * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/pg_numa.c b/src/port/pg_numa.c index 4b487a2a4e814..8954669273ae3 100644 --- a/src/port/pg_numa.c +++ b/src/port/pg_numa.c @@ -4,7 +4,7 @@ * Basic NUMA portability routines * * - * Copyright (c) 2025, PostgreSQL Global Development Group + * Copyright (c) 2025-2026, PostgreSQL Global Development Group * * * IDENTIFICATION @@ -16,6 +16,7 @@ #include "c.h" #include +#include "miscadmin.h" #include "port/pg_numa.h" /* @@ -29,11 +30,34 @@ #include #include +/* + * numa_move_pages() chunk size, has to be <= 16 to work around a kernel bug + * in do_pages_stat() (chunked by DO_PAGES_STAT_CHUNK_NR). By using the same + * chunk size, we make it work even on unfixed kernels. + * + * 64-bit system are not affected by the bug, and so use much larger chunks. + */ +#if SIZEOF_SIZE_T == 4 +#define NUMA_QUERY_CHUNK_SIZE 16 +#else +#define NUMA_QUERY_CHUNK_SIZE 1024 +#endif + /* libnuma requires initialization as per numa(3) on Linux */ int pg_numa_init(void) { - int r = numa_available(); + int r; + + /* + * XXX libnuma versions before 2.0.19 don't handle EPERM by disabling + * NUMA, which then leads to unexpected failures later. This affects + * containers that disable get_mempolicy by a seccomp profile. + */ + if (get_mempolicy(NULL, NULL, 0, 0, 0) < 0 && (errno == EPERM)) + r = -1; + else + r = numa_available(); return r; } @@ -42,11 +66,50 @@ pg_numa_init(void) * We use move_pages(2) syscall here - instead of get_mempolicy(2) - as the * first one allows us to batch and query about many memory pages in one single * giant system call that is way faster. + * + * We call numa_move_pages() for smaller chunks of the whole array. The first + * reason is to work around a kernel bug, but also to allow interrupting the + * query between the calls (for many pointers processing the whole array can + * take a lot of time). */ int pg_numa_query_pages(int pid, unsigned long count, void **pages, int *status) { - return numa_move_pages(pid, count, pages, NULL, status, 0); + unsigned long next = 0; + int ret = 0; + + /* + * Chunk pointers passed to numa_move_pages to NUMA_QUERY_CHUNK_SIZE + * items, to work around a kernel bug in do_pages_stat(). + */ + while (next < count) + { + unsigned long count_chunk = Min(count - next, + NUMA_QUERY_CHUNK_SIZE); + +#ifndef FRONTEND + CHECK_FOR_INTERRUPTS(); +#endif + + /* + * Bail out if any of the chunks errors out (ret<0). We ignore (ret>0) + * which is used to return number of nonmigrated pages, but we're not + * migrating any pages here. + */ + ret = numa_move_pages(pid, count_chunk, &pages[next], NULL, &status[next], 0); + if (ret < 0) + { + /* plain error, return as is */ + return ret; + } + + next += count_chunk; + } + + /* should have consumed the input array exactly */ + Assert(next == count); + + return 0; } int diff --git a/src/port/pg_popcount_aarch64.c b/src/port/pg_popcount_aarch64.c index e515e4d45b8c4..3969a42523ca0 100644 --- a/src/port/pg_popcount_aarch64.c +++ b/src/port/pg_popcount_aarch64.c @@ -3,7 +3,7 @@ * pg_popcount_aarch64.c * Holds the AArch64 popcount implementations. * - * Copyright (c) 2025, PostgreSQL Global Development Group + * Copyright (c) 2025-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/pg_popcount_aarch64.c @@ -12,9 +12,7 @@ */ #include "c.h" -#include "port/pg_bitutils.h" - -#ifdef POPCNT_AARCH64 +#ifdef USE_NEON #include @@ -23,15 +21,21 @@ #if defined(HAVE_ELF_AUX_INFO) || defined(HAVE_GETAUXVAL) #include +/* Ancient glibc releases don't include the HWCAPxxx macros in sys/auxv.h */ +#if defined(__linux__) && !defined(HWCAP_SVE) +#include +#endif #endif #endif +#include "port/pg_bitutils.h" + /* * The Neon versions are built regardless of whether we are building the SVE * versions. */ static uint64 pg_popcount_neon(const char *buf, int bytes); -static uint64 pg_popcount_masked_neon(const char *buf, int bytes, bits8 mask); +static uint64 pg_popcount_masked_neon(const char *buf, int bytes, uint8 mask); #ifdef USE_SVE_POPCNT_WITH_RUNTIME_CHECK @@ -39,7 +43,7 @@ static uint64 pg_popcount_masked_neon(const char *buf, int bytes, bits8 mask); * These are the SVE implementations of the popcount functions. */ static uint64 pg_popcount_sve(const char *buf, int bytes); -static uint64 pg_popcount_masked_sve(const char *buf, int bytes, bits8 mask); +static uint64 pg_popcount_masked_sve(const char *buf, int bytes, uint8 mask); /* * The function pointers are initially set to "choose" functions. These @@ -48,9 +52,9 @@ static uint64 pg_popcount_masked_sve(const char *buf, int bytes, bits8 mask); * caller's request. */ static uint64 pg_popcount_choose(const char *buf, int bytes); -static uint64 pg_popcount_masked_choose(const char *buf, int bytes, bits8 mask); +static uint64 pg_popcount_masked_choose(const char *buf, int bytes, uint8 mask); uint64 (*pg_popcount_optimized) (const char *buf, int bytes) = pg_popcount_choose; -uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, bits8 mask) = pg_popcount_masked_choose; +uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, uint8 mask) = pg_popcount_masked_choose; static inline bool pg_popcount_sve_available(void) @@ -90,7 +94,7 @@ pg_popcount_choose(const char *buf, int bytes) } static uint64 -pg_popcount_masked_choose(const char *buf, int bytes, bits8 mask) +pg_popcount_masked_choose(const char *buf, int bytes, uint8 mask) { choose_popcount_functions(); return pg_popcount_masked_optimized(buf, bytes, mask); @@ -186,7 +190,7 @@ pg_popcount_sve(const char *buf, int bytes) */ pg_attribute_target("arch=armv8-a+sve") static uint64 -pg_popcount_masked_sve(const char *buf, int bytes, bits8 mask) +pg_popcount_masked_sve(const char *buf, int bytes, uint8 mask) { svbool_t pred = svptrue_b64(); svuint64_t accum1 = svdup_u64(0), @@ -280,7 +284,7 @@ pg_popcount_optimized(const char *buf, int bytes) } uint64 -pg_popcount_masked_optimized(const char *buf, int bytes, bits8 mask) +pg_popcount_masked_optimized(const char *buf, int bytes, uint8 mask) { return pg_popcount_masked_neon(buf, bytes, mask); } @@ -288,27 +292,12 @@ pg_popcount_masked_optimized(const char *buf, int bytes, bits8 mask) #endif /* ! USE_SVE_POPCNT_WITH_RUNTIME_CHECK */ /* - * pg_popcount32 + * pg_popcount64_neon * Return number of 1 bits in word */ -int -pg_popcount32(uint32 word) +static inline int +pg_popcount64_neon(uint64 word) { - return pg_popcount64((uint64) word); -} - -/* - * pg_popcount64 - * Return number of 1 bits in word - */ -int -pg_popcount64(uint64 word) -{ - /* - * For some compilers, __builtin_popcountl() already emits Neon - * instructions. The line below should compile to the same code on those - * systems. - */ return vaddv_u8(vcnt_u8(vld1_u8((const uint8 *) &word))); } @@ -379,7 +368,7 @@ pg_popcount_neon(const char *buf, int bytes) */ for (; bytes >= sizeof(uint64); bytes -= sizeof(uint64)) { - popcnt += pg_popcount64(*((uint64 *) buf)); + popcnt += pg_popcount64_neon(*((const uint64 *) buf)); buf += sizeof(uint64); } @@ -397,7 +386,7 @@ pg_popcount_neon(const char *buf, int bytes) * Returns number of 1 bits in buf after applying the mask to each byte */ static uint64 -pg_popcount_masked_neon(const char *buf, int bytes, bits8 mask) +pg_popcount_masked_neon(const char *buf, int bytes, uint8 mask) { uint8x16_t vec, maskv = vdupq_n_u8(mask); @@ -461,7 +450,7 @@ pg_popcount_masked_neon(const char *buf, int bytes, bits8 mask) */ for (; bytes >= sizeof(uint64); bytes -= sizeof(uint64)) { - popcnt += pg_popcount64(*((uint64 *) buf) & mask64); + popcnt += pg_popcount64_neon(*((const uint64 *) buf) & mask64); buf += sizeof(uint64); } @@ -474,4 +463,4 @@ pg_popcount_masked_neon(const char *buf, int bytes, bits8 mask) return popcnt; } -#endif /* POPCNT_AARCH64 */ +#endif /* USE_NEON */ diff --git a/src/port/pg_popcount_avx512.c b/src/port/pg_popcount_avx512.c deleted file mode 100644 index 80c0aee3e73fa..0000000000000 --- a/src/port/pg_popcount_avx512.c +++ /dev/null @@ -1,223 +0,0 @@ -/*------------------------------------------------------------------------- - * - * pg_popcount_avx512.c - * Holds the AVX-512 pg_popcount() implementation. - * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group - * - * IDENTIFICATION - * src/port/pg_popcount_avx512.c - * - *------------------------------------------------------------------------- - */ -#include "c.h" - -#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK - -#if defined(HAVE__GET_CPUID) || defined(HAVE__GET_CPUID_COUNT) -#include -#endif - -#include - -#if defined(HAVE__CPUID) || defined(HAVE__CPUIDEX) -#include -#endif - -#include "port/pg_bitutils.h" - -/* - * It's probably unlikely that TRY_POPCNT_X86_64 won't be set if we are able to - * use AVX-512 intrinsics, but we check it anyway to be sure. We piggy-back on - * the function pointers that are only used when TRY_POPCNT_X86_64 is set. - */ -#ifdef TRY_POPCNT_X86_64 - -/* - * Does CPUID say there's support for XSAVE instructions? - */ -static inline bool -xsave_available(void) -{ - unsigned int exx[4] = {0, 0, 0, 0}; - -#if defined(HAVE__GET_CPUID) - __get_cpuid(1, &exx[0], &exx[1], &exx[2], &exx[3]); -#elif defined(HAVE__CPUID) - __cpuid(exx, 1); -#else -#error cpuid instruction not available -#endif - return (exx[2] & (1 << 27)) != 0; /* osxsave */ -} - -/* - * Does XGETBV say the ZMM registers are enabled? - * - * NB: Caller is responsible for verifying that xsave_available() returns true - * before calling this. - */ -#ifdef HAVE_XSAVE_INTRINSICS -pg_attribute_target("xsave") -#endif -static inline bool -zmm_regs_available(void) -{ -#ifdef HAVE_XSAVE_INTRINSICS - return (_xgetbv(0) & 0xe6) == 0xe6; -#else - return false; -#endif -} - -/* - * Does CPUID say there's support for AVX-512 popcount and byte-and-word - * instructions? - */ -static inline bool -avx512_popcnt_available(void) -{ - unsigned int exx[4] = {0, 0, 0, 0}; - -#if defined(HAVE__GET_CPUID_COUNT) - __get_cpuid_count(7, 0, &exx[0], &exx[1], &exx[2], &exx[3]); -#elif defined(HAVE__CPUIDEX) - __cpuidex(exx, 7, 0); -#else -#error cpuid instruction not available -#endif - return (exx[2] & (1 << 14)) != 0 && /* avx512-vpopcntdq */ - (exx[1] & (1 << 30)) != 0; /* avx512-bw */ -} - -/* - * Returns true if the CPU supports the instructions required for the AVX-512 - * pg_popcount() implementation. - */ -bool -pg_popcount_avx512_available(void) -{ - return xsave_available() && - zmm_regs_available() && - avx512_popcnt_available(); -} - -/* - * pg_popcount_avx512 - * Returns the number of 1-bits in buf - */ -pg_attribute_target("avx512vpopcntdq,avx512bw") -uint64 -pg_popcount_avx512(const char *buf, int bytes) -{ - __m512i val, - cnt; - __m512i accum = _mm512_setzero_si512(); - const char *final; - int tail_idx; - __mmask64 mask = ~UINT64CONST(0); - - /* - * Align buffer down to avoid double load overhead from unaligned access. - * Calculate a mask to ignore preceding bytes. Find start offset of final - * iteration and ensure it is not empty. - */ - mask <<= ((uintptr_t) buf) % sizeof(__m512i); - tail_idx = (((uintptr_t) buf + bytes - 1) % sizeof(__m512i)) + 1; - final = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf + bytes - 1); - buf = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf); - - /* - * Iterate through all but the final iteration. Starting from the second - * iteration, the mask is ignored. - */ - if (buf < final) - { - val = _mm512_maskz_loadu_epi8(mask, (const __m512i *) buf); - cnt = _mm512_popcnt_epi64(val); - accum = _mm512_add_epi64(accum, cnt); - - buf += sizeof(__m512i); - mask = ~UINT64CONST(0); - - for (; buf < final; buf += sizeof(__m512i)) - { - val = _mm512_load_si512((const __m512i *) buf); - cnt = _mm512_popcnt_epi64(val); - accum = _mm512_add_epi64(accum, cnt); - } - } - - /* Final iteration needs to ignore bytes that are not within the length */ - mask &= (~UINT64CONST(0) >> (sizeof(__m512i) - tail_idx)); - - val = _mm512_maskz_loadu_epi8(mask, (const __m512i *) buf); - cnt = _mm512_popcnt_epi64(val); - accum = _mm512_add_epi64(accum, cnt); - - return _mm512_reduce_add_epi64(accum); -} - -/* - * pg_popcount_masked_avx512 - * Returns the number of 1-bits in buf after applying the mask to each byte - */ -pg_attribute_target("avx512vpopcntdq,avx512bw") -uint64 -pg_popcount_masked_avx512(const char *buf, int bytes, bits8 mask) -{ - __m512i val, - vmasked, - cnt; - __m512i accum = _mm512_setzero_si512(); - const char *final; - int tail_idx; - __mmask64 bmask = ~UINT64CONST(0); - const __m512i maskv = _mm512_set1_epi8(mask); - - /* - * Align buffer down to avoid double load overhead from unaligned access. - * Calculate a mask to ignore preceding bytes. Find start offset of final - * iteration and ensure it is not empty. - */ - bmask <<= ((uintptr_t) buf) % sizeof(__m512i); - tail_idx = (((uintptr_t) buf + bytes - 1) % sizeof(__m512i)) + 1; - final = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf + bytes - 1); - buf = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf); - - /* - * Iterate through all but the final iteration. Starting from the second - * iteration, the mask is ignored. - */ - if (buf < final) - { - val = _mm512_maskz_loadu_epi8(bmask, (const __m512i *) buf); - vmasked = _mm512_and_si512(val, maskv); - cnt = _mm512_popcnt_epi64(vmasked); - accum = _mm512_add_epi64(accum, cnt); - - buf += sizeof(__m512i); - bmask = ~UINT64CONST(0); - - for (; buf < final; buf += sizeof(__m512i)) - { - val = _mm512_load_si512((const __m512i *) buf); - vmasked = _mm512_and_si512(val, maskv); - cnt = _mm512_popcnt_epi64(vmasked); - accum = _mm512_add_epi64(accum, cnt); - } - } - - /* Final iteration needs to ignore bytes that are not within the length */ - bmask &= (~UINT64CONST(0) >> (sizeof(__m512i) - tail_idx)); - - val = _mm512_maskz_loadu_epi8(bmask, (const __m512i *) buf); - vmasked = _mm512_and_si512(val, maskv); - cnt = _mm512_popcnt_epi64(vmasked); - accum = _mm512_add_epi64(accum, cnt); - - return _mm512_reduce_add_epi64(accum); -} - -#endif /* TRY_POPCNT_X86_64 */ -#endif /* USE_AVX512_POPCNT_WITH_RUNTIME_CHECK */ diff --git a/src/port/pg_popcount_x86.c b/src/port/pg_popcount_x86.c new file mode 100644 index 0000000000000..91579e6b56967 --- /dev/null +++ b/src/port/pg_popcount_x86.c @@ -0,0 +1,304 @@ +/*------------------------------------------------------------------------- + * + * pg_popcount_x86.c + * Holds the x86-64 pg_popcount() implementations. + * + * Copyright (c) 2024-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/port/pg_popcount_x86.c + * + *------------------------------------------------------------------------- + */ +#include "c.h" + +#ifdef HAVE_X86_64_POPCNTQ + +#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK +#include +#endif + +#include "port/pg_bitutils.h" +#include "port/pg_cpu.h" + +/* + * The SSE4.2 versions are built regardless of whether we are building the + * AVX-512 versions. + * + * Technically, POPCNT is not part of SSE4.2, and isn't even a vector + * operation, but in practice this is close enough, and "sse42" seems easier to + * follow than "popcnt" for these names. + */ +static uint64 pg_popcount_sse42(const char *buf, int bytes); +static uint64 pg_popcount_masked_sse42(const char *buf, int bytes, uint8 mask); + +/* + * These are the AVX-512 implementations of the popcount functions. + */ +#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK +static uint64 pg_popcount_avx512(const char *buf, int bytes); +static uint64 pg_popcount_masked_avx512(const char *buf, int bytes, uint8 mask); +#endif /* USE_AVX512_POPCNT_WITH_RUNTIME_CHECK */ + +/* + * The function pointers are initially set to "choose" functions. These + * functions will first set the pointers to the right implementations (base on + * what the current CPU supports) and then will call the pointer to fulfill the + * caller's request. + */ +static uint64 pg_popcount_choose(const char *buf, int bytes); +static uint64 pg_popcount_masked_choose(const char *buf, int bytes, uint8 mask); +uint64 (*pg_popcount_optimized) (const char *buf, int bytes) = pg_popcount_choose; +uint64 (*pg_popcount_masked_optimized) (const char *buf, int bytes, uint8 mask) = pg_popcount_masked_choose; + + +#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK + +/* + * Returns true if the CPU supports the instructions required for the AVX-512 + * pg_popcount() implementation. + */ +static bool +pg_popcount_avx512_available(void) +{ + return x86_feature_available(PG_AVX512_BW) && + x86_feature_available(PG_AVX512_VPOPCNTDQ); +} + +#endif /* USE_AVX512_POPCNT_WITH_RUNTIME_CHECK */ + +/* + * These functions get called on the first call to pg_popcount(), etc. + * They detect whether we can use the asm implementations, and replace + * the function pointers so that subsequent calls are routed directly to + * the chosen implementation. + */ +static inline void +choose_popcount_functions(void) +{ + if (x86_feature_available(PG_POPCNT)) + { + pg_popcount_optimized = pg_popcount_sse42; + pg_popcount_masked_optimized = pg_popcount_masked_sse42; + } + else + { + pg_popcount_optimized = pg_popcount_portable; + pg_popcount_masked_optimized = pg_popcount_masked_portable; + } + +#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK + if (pg_popcount_avx512_available()) + { + pg_popcount_optimized = pg_popcount_avx512; + pg_popcount_masked_optimized = pg_popcount_masked_avx512; + } +#endif +} + +static uint64 +pg_popcount_choose(const char *buf, int bytes) +{ + choose_popcount_functions(); + return pg_popcount_optimized(buf, bytes); +} + +static uint64 +pg_popcount_masked_choose(const char *buf, int bytes, uint8 mask) +{ + choose_popcount_functions(); + return pg_popcount_masked(buf, bytes, mask); +} + +#ifdef USE_AVX512_POPCNT_WITH_RUNTIME_CHECK + +/* + * pg_popcount_avx512 + * Returns the number of 1-bits in buf + */ +pg_attribute_target("avx512vpopcntdq,avx512bw") +static uint64 +pg_popcount_avx512(const char *buf, int bytes) +{ + __m512i val, + cnt; + __m512i accum = _mm512_setzero_si512(); + const char *final; + int tail_idx; + __mmask64 mask = ~UINT64CONST(0); + + /* + * Align buffer down to avoid double load overhead from unaligned access. + * Calculate a mask to ignore preceding bytes. Find start offset of final + * iteration and ensure it is not empty. + */ + mask <<= ((uintptr_t) buf) % sizeof(__m512i); + tail_idx = (((uintptr_t) buf + bytes - 1) % sizeof(__m512i)) + 1; + final = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf + bytes - 1); + buf = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf); + + /* + * Iterate through all but the final iteration. Starting from the second + * iteration, the mask is ignored. + */ + if (buf < final) + { + val = _mm512_maskz_loadu_epi8(mask, (const __m512i *) buf); + cnt = _mm512_popcnt_epi64(val); + accum = _mm512_add_epi64(accum, cnt); + + buf += sizeof(__m512i); + mask = ~UINT64CONST(0); + + for (; buf < final; buf += sizeof(__m512i)) + { + val = _mm512_load_si512((const __m512i *) buf); + cnt = _mm512_popcnt_epi64(val); + accum = _mm512_add_epi64(accum, cnt); + } + } + + /* Final iteration needs to ignore bytes that are not within the length */ + mask &= (~UINT64CONST(0) >> (sizeof(__m512i) - tail_idx)); + + val = _mm512_maskz_loadu_epi8(mask, (const __m512i *) buf); + cnt = _mm512_popcnt_epi64(val); + accum = _mm512_add_epi64(accum, cnt); + + return _mm512_reduce_add_epi64(accum); +} + +/* + * pg_popcount_masked_avx512 + * Returns the number of 1-bits in buf after applying the mask to each byte + */ +pg_attribute_target("avx512vpopcntdq,avx512bw") +static uint64 +pg_popcount_masked_avx512(const char *buf, int bytes, uint8 mask) +{ + __m512i val, + vmasked, + cnt; + __m512i accum = _mm512_setzero_si512(); + const char *final; + int tail_idx; + __mmask64 bmask = ~UINT64CONST(0); + const __m512i maskv = _mm512_set1_epi8(mask); + + /* + * Align buffer down to avoid double load overhead from unaligned access. + * Calculate a mask to ignore preceding bytes. Find start offset of final + * iteration and ensure it is not empty. + */ + bmask <<= ((uintptr_t) buf) % sizeof(__m512i); + tail_idx = (((uintptr_t) buf + bytes - 1) % sizeof(__m512i)) + 1; + final = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf + bytes - 1); + buf = (const char *) TYPEALIGN_DOWN(sizeof(__m512i), buf); + + /* + * Iterate through all but the final iteration. Starting from the second + * iteration, the mask is ignored. + */ + if (buf < final) + { + val = _mm512_maskz_loadu_epi8(bmask, (const __m512i *) buf); + vmasked = _mm512_and_si512(val, maskv); + cnt = _mm512_popcnt_epi64(vmasked); + accum = _mm512_add_epi64(accum, cnt); + + buf += sizeof(__m512i); + bmask = ~UINT64CONST(0); + + for (; buf < final; buf += sizeof(__m512i)) + { + val = _mm512_load_si512((const __m512i *) buf); + vmasked = _mm512_and_si512(val, maskv); + cnt = _mm512_popcnt_epi64(vmasked); + accum = _mm512_add_epi64(accum, cnt); + } + } + + /* Final iteration needs to ignore bytes that are not within the length */ + bmask &= (~UINT64CONST(0) >> (sizeof(__m512i) - tail_idx)); + + val = _mm512_maskz_loadu_epi8(bmask, (const __m512i *) buf); + vmasked = _mm512_and_si512(val, maskv); + cnt = _mm512_popcnt_epi64(vmasked); + accum = _mm512_add_epi64(accum, cnt); + + return _mm512_reduce_add_epi64(accum); +} + +#endif /* USE_AVX512_POPCNT_WITH_RUNTIME_CHECK */ + +/* + * pg_popcount64_sse42 + * Return the number of 1 bits set in word + */ +static inline int +pg_popcount64_sse42(uint64 word) +{ +#ifdef _MSC_VER + return __popcnt64(word); +#else + uint64 res; + +__asm__ __volatile__(" popcntq %1,%0\n":"=q"(res):"rm"(word):"cc"); + return (int) res; +#endif +} + +/* + * pg_popcount_sse42 + * Returns the number of 1-bits in buf + */ +pg_attribute_no_sanitize_alignment() +static uint64 +pg_popcount_sse42(const char *buf, int bytes) +{ + uint64 popcnt = 0; + const uint64 *words = (const uint64 *) buf; + + while (bytes >= 8) + { + popcnt += pg_popcount64_sse42(*words++); + bytes -= 8; + } + + buf = (const char *) words; + + /* Process any remaining bytes */ + while (bytes--) + popcnt += pg_number_of_ones[(unsigned char) *buf++]; + + return popcnt; +} + +/* + * pg_popcount_masked_sse42 + * Returns the number of 1-bits in buf after applying the mask to each byte + */ +pg_attribute_no_sanitize_alignment() +static uint64 +pg_popcount_masked_sse42(const char *buf, int bytes, uint8 mask) +{ + uint64 popcnt = 0; + uint64 maskv = ~UINT64CONST(0) / 0xFF * mask; + const uint64 *words = (const uint64 *) buf; + + while (bytes >= 8) + { + popcnt += pg_popcount64_sse42(*words++ & maskv); + bytes -= 8; + } + + buf = (const char *) words; + + /* Process any remaining bytes */ + while (bytes--) + popcnt += pg_number_of_ones[(unsigned char) *buf++ & mask]; + + return popcnt; +} + +#endif /* HAVE_X86_64_POPCNTQ */ diff --git a/src/port/pg_strong_random.c b/src/port/pg_strong_random.c index ea6780dcc9f1c..c8d8a70d8962b 100644 --- a/src/port/pg_strong_random.c +++ b/src/port/pg_strong_random.c @@ -10,7 +10,7 @@ * therefore, even when built for backend, it cannot rely on backend * infrastructure such as elog() or palloc(). * - * Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/pg_strong_random.c diff --git a/src/port/pgcheckdir.c b/src/port/pgcheckdir.c index 8333051610744..807c21131d841 100644 --- a/src/port/pgcheckdir.c +++ b/src/port/pgcheckdir.c @@ -5,7 +5,7 @@ * A simple subroutine to check whether a directory exists and is empty or not. * Useful in both initdb and the backend. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/port/pgmkdirp.c b/src/port/pgmkdirp.c index d943559760d89..7d7cea4dd0ea1 100644 --- a/src/port/pgmkdirp.c +++ b/src/port/pgmkdirp.c @@ -73,7 +73,7 @@ pg_mkdir_p(char *path, int omode) if (p[0] == '/' && p[1] == '/') { /* network drive */ - p = strstr(p + 2, "/"); + p = strchr(p + 2, '/'); if (p == NULL) { errno = EINVAL; diff --git a/src/port/pgsleep.c b/src/port/pgsleep.c index dfe824fe46266..4c31e503eb12c 100644 --- a/src/port/pgsleep.c +++ b/src/port/pgsleep.c @@ -4,7 +4,7 @@ * Portable delay handling. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/port/pgsleep.c * diff --git a/src/port/pgstrcasecmp.c b/src/port/pgstrcasecmp.c index ec2b3a75c3dcd..080b3caf8330f 100644 --- a/src/port/pgstrcasecmp.c +++ b/src/port/pgstrcasecmp.c @@ -13,12 +13,8 @@ * * NB: this code should match downcase_truncate_identifier() in scansup.c. * - * We also provide strict ASCII-only case conversion functions, which can - * be used to implement C/POSIX case folding semantics no matter what the - * C library thinks the locale is. * - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/port/pgstrcasecmp.c * @@ -127,25 +123,3 @@ pg_tolower(unsigned char ch) ch = tolower(ch); return ch; } - -/* - * Fold a character to upper case, following C/POSIX locale rules. - */ -unsigned char -pg_ascii_toupper(unsigned char ch) -{ - if (ch >= 'a' && ch <= 'z') - ch += 'A' - 'a'; - return ch; -} - -/* - * Fold a character to lower case, following C/POSIX locale rules. - */ -unsigned char -pg_ascii_tolower(unsigned char ch) -{ - if (ch >= 'A' && ch <= 'Z') - ch += 'a' - 'A'; - return ch; -} diff --git a/src/port/pgstrsignal.c b/src/port/pgstrsignal.c index 4e6bb878dbc08..5058140745928 100644 --- a/src/port/pgstrsignal.c +++ b/src/port/pgstrsignal.c @@ -6,7 +6,7 @@ * On platforms compliant with modern POSIX, this just wraps strsignal(3). * Elsewhere, we do the best we can. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/port/pqsignal.c b/src/port/pqsignal.c index 26943845e2179..dd43f9eb26268 100644 --- a/src/port/pqsignal.c +++ b/src/port/pqsignal.c @@ -4,7 +4,7 @@ * reliable BSD-style signal(2) routine stolen from RWW who stole it * from Stevens... * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -63,6 +63,14 @@ #define PG_NSIG (64) /* XXX: wild guess */ #endif +#if !(defined(WIN32) && defined(FRONTEND)) +#define USE_SIGACTION +#endif + +#if defined(USE_SIGACTION) && defined(HAVE_SA_SIGINFO) +#define USE_SIGINFO +#endif + /* Check a couple of common signals to make sure PG_NSIG is accurate. */ StaticAssertDecl(SIGUSR2 < PG_NSIG, "SIGUSR2 >= PG_NSIG"); StaticAssertDecl(SIGHUP < PG_NSIG, "SIGHUP >= PG_NSIG"); @@ -82,10 +90,16 @@ static volatile pqsigfunc pqsignal_handlers[PG_NSIG]; * * This wrapper also handles restoring the value of errno. */ +#if defined(USE_SIGACTION) && defined(USE_SIGINFO) +static void +wrapper_handler(int postgres_signal_arg, siginfo_t * info, void *context) +#else /* no USE_SIGINFO */ static void -wrapper_handler(SIGNAL_ARGS) +wrapper_handler(int postgres_signal_arg) +#endif { int save_errno = errno; + pg_signal_info pg_info; Assert(postgres_signal_arg > 0); Assert(postgres_signal_arg < PG_NSIG); @@ -101,13 +115,32 @@ wrapper_handler(SIGNAL_ARGS) if (unlikely(MyProcPid != (int) getpid())) { - pqsignal(postgres_signal_arg, SIG_DFL); + pqsignal(postgres_signal_arg, PG_SIG_DFL); raise(postgres_signal_arg); return; } #endif - (*pqsignal_handlers[postgres_signal_arg]) (postgres_signal_arg); +#ifdef HAVE_SA_SIGINFO + + /* + * If supported by the system, forward interesting information from the + * system's extended signal information to our platform independent + * format. + */ + pg_info.pid = info->si_pid; + pg_info.uid = info->si_uid; +#else + + /* + * Otherwise forward values indicating that we do not have the + * information. + */ + pg_info.pid = 0; + pg_info.uid = 0; +#endif + + (*pqsignal_handlers[postgres_signal_arg]) (postgres_signal_arg, &pg_info); errno = save_errno; } @@ -122,32 +155,66 @@ wrapper_handler(SIGNAL_ARGS) void pqsignal(int signo, pqsigfunc func) { -#if !(defined(WIN32) && defined(FRONTEND)) +#ifdef USE_SIGACTION struct sigaction act; +#else + void (*wrapper_func_ptr) (int); #endif + bool is_ign = func == PG_SIG_IGN; + bool is_dfl = func == PG_SIG_DFL; Assert(signo > 0); Assert(signo < PG_NSIG); - if (func != SIG_IGN && func != SIG_DFL) + /* set up indirection handler */ + if (!(is_ign || is_dfl)) { pqsignal_handlers[signo] = func; /* assumed atomic */ - func = wrapper_handler; } -#if !(defined(WIN32) && defined(FRONTEND)) - act.sa_handler = func; + /* + * Configure system to either ignore/reset the signal handler, or to + * forward it to wrapper_handler. + */ +#ifdef USE_SIGACTION sigemptyset(&act.sa_mask); act.sa_flags = SA_RESTART; + + if (is_ign) + act.sa_handler = SIG_IGN; + else if (is_dfl) + act.sa_handler = SIG_DFL; +#ifdef USE_SIGINFO + else + { + act.sa_sigaction = wrapper_handler; + act.sa_flags |= SA_SIGINFO; + } +#else + else + act.sa_handler = wrapper_handler; +#endif + #ifdef SA_NOCLDSTOP if (signo == SIGCHLD) act.sa_flags |= SA_NOCLDSTOP; #endif if (sigaction(signo, &act, NULL) < 0) Assert(false); /* probably indicates coding error */ -#else - /* Forward to Windows native signal system. */ - if (signal(signo, func) == SIG_ERR) +#else /* no USE_SIGACTION */ + + /* + * Forward to Windows native signal system, we need to send this though + * wrapper handler as it it needs to take single argument only. + */ + if (is_ign) + wrapper_func_ptr = SIG_IGN; + else if (is_dfl) + wrapper_func_ptr = SIG_DFL; + else + wrapper_func_ptr = wrapper_handler; + + if (signal(signo, wrapper_func_ptr) == SIG_ERR) Assert(false); /* probably indicates coding error */ #endif } diff --git a/src/port/pthread_barrier_wait.c b/src/port/pthread_barrier_wait.c index 2387a2acb24bf..caa1b12aaf3f3 100644 --- a/src/port/pthread_barrier_wait.c +++ b/src/port/pthread_barrier_wait.c @@ -3,7 +3,7 @@ * pthread_barrier_wait.c * Implementation of pthread_barrier_t support for platforms lacking it. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/pthread_barrier_wait.c diff --git a/src/port/quotes.c b/src/port/quotes.c index 3d3884036b82c..99ed63d4fa14d 100644 --- a/src/port/quotes.c +++ b/src/port/quotes.c @@ -3,7 +3,7 @@ * quotes.c * string quoting and escaping functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/snprintf.c b/src/port/snprintf.c index d7f18b42d19ac..5deee44d3a200 100644 --- a/src/port/snprintf.c +++ b/src/port/snprintf.c @@ -2,7 +2,7 @@ * Copyright (c) 1983, 1995, 1996 Eric P. Allman * Copyright (c) 1988, 1993 * The Regents of the University of California. All rights reserved. - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -462,7 +462,7 @@ dopr(PrintfTarget *target, const char *format, va_list args) /* set zero padding if no nonzero digits yet */ if (accum == 0 && !pointflag) zpad = '0'; - /* FALL THRU */ + pg_fallthrough; case '1': case '2': case '3': @@ -557,34 +557,21 @@ dopr(PrintfTarget *target, const char *format, va_list args) fmtpos = accum; accum = 0; goto nextch2; -#ifdef WIN32 - case 'I': - /* Windows PRI*{32,64,PTR} size */ - if (format[0] == '3' && format[1] == '2') - format += 2; - else if (format[0] == '6' && format[1] == '4') - { - format += 2; - longlongflag = 1; - } - else - { -#if SIZEOF_VOID_P == SIZEOF_LONG - longflag = 1; -#elif SIZEOF_VOID_P == SIZEOF_LONG_LONG - longlongflag = 1; -#else -#error "cannot find integer type of the same size as intptr_t" -#endif - } - goto nextch2; -#endif case 'l': if (longflag) longlongflag = 1; else longflag = 1; goto nextch2; + case 'j': +#if SIZEOF_INTMAX_T == SIZEOF_LONG + longflag = 1; +#elif SIZEOF_INTMAX_T == SIZEOF_LONG_LONG + longlongflag = 1; +#else +#error "cannot find integer type of the same size as intmax_t" +#endif + goto nextch2; case 'z': #if SIZEOF_SIZE_T == SIZEOF_LONG longflag = 1; @@ -842,34 +829,21 @@ find_arguments(const char *format, va_list args, fmtpos = accum; accum = 0; goto nextch1; -#ifdef WIN32 - case 'I': - /* Windows PRI*{32,64,PTR} size */ - if (format[0] == '3' && format[1] == '2') - format += 2; - else if (format[0] == '6' && format[1] == '4') - { - format += 2; - longlongflag = 1; - } - else - { -#if SIZEOF_VOID_P == SIZEOF_LONG - longflag = 1; -#elif SIZEOF_VOID_P == SIZEOF_LONG_LONG - longlongflag = 1; -#else -#error "cannot find integer type of the same size as intptr_t" -#endif - } - goto nextch1; -#endif case 'l': if (longflag) longlongflag = 1; else longflag = 1; goto nextch1; + case 'j': +#if SIZEOF_INTMAX_T == SIZEOF_LONG + longflag = 1; +#elif SIZEOF_INTMAX_T == SIZEOF_LONG_LONG + longlongflag = 1; +#else +#error "cannot find integer type of the same size as intmax_t" +#endif + goto nextch1; case 'z': #if SIZEOF_SIZE_T == SIZEOF_LONG longflag = 1; @@ -1249,22 +1223,6 @@ fmtfloat(double value, char type, int forcesign, int leftjust, } if (vallen < 0) goto fail; - - /* - * Windows, alone among our supported platforms, likes to emit - * three-digit exponent fields even when two digits would do. Hack - * such results to look like the way everyone else does it. - */ -#ifdef WIN32 - if (vallen >= 6 && - convert[vallen - 5] == 'e' && - convert[vallen - 3] == '0') - { - convert[vallen - 3] = convert[vallen - 2]; - convert[vallen - 2] = convert[vallen - 1]; - vallen--; - } -#endif } padlen = compute_padlen(minlen, vallen + zeropadlen, leftjust); @@ -1380,17 +1338,6 @@ pg_strfromd(char *str, size_t count, int precision, double value) target.failed = true; goto fail; } - -#ifdef WIN32 - if (vallen >= 6 && - convert[vallen - 5] == 'e' && - convert[vallen - 3] == '0') - { - convert[vallen - 3] = convert[vallen - 2]; - convert[vallen - 2] = convert[vallen - 1]; - vallen--; - } -#endif } } diff --git a/src/port/strerror.c b/src/port/strerror.c index f07465177702c..d70734196b2ab 100644 --- a/src/port/strerror.c +++ b/src/port/strerror.c @@ -3,7 +3,7 @@ * strerror.c * Replacements for standard strerror() and strerror_r() functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -214,8 +214,10 @@ get_errno_symbol(int errnum) return "ENOTCONN"; case ENOTDIR: return "ENOTDIR"; +#if defined(ENOTEMPTY) && (ENOTEMPTY != EEXIST) /* same code on AIX */ case ENOTEMPTY: return "ENOTEMPTY"; +#endif case ENOTSOCK: return "ENOTSOCK"; #ifdef ENOTSUP diff --git a/src/port/strlcpy.c b/src/port/strlcpy.c index d07b6536adbfa..a80e5d7280f5c 100644 --- a/src/port/strlcpy.c +++ b/src/port/strlcpy.c @@ -3,7 +3,7 @@ * strlcpy.c * strncpy done right * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/port/strnlen.c b/src/port/strnlen.c deleted file mode 100644 index 60c3b458c6bf8..0000000000000 --- a/src/port/strnlen.c +++ /dev/null @@ -1,33 +0,0 @@ -/*------------------------------------------------------------------------- - * - * strnlen.c - * Fallback implementation of strnlen(). - * - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/port/strnlen.c - * - *------------------------------------------------------------------------- - */ - -#include "c.h" - -/* - * Implementation of posix' strnlen for systems where it's not available. - * - * Returns the number of characters before a null-byte in the string pointed - * to by str, unless there's no null-byte before maxlen. In the latter case - * maxlen is returned. - */ -size_t -strnlen(const char *str, size_t maxlen) -{ - const char *p = str; - - while (maxlen-- > 0 && *p) - p++; - return p - str; -} diff --git a/src/port/strtof.c b/src/port/strtof.c index e7258b3d6dbe6..e0029bebf199c 100644 --- a/src/port/strtof.c +++ b/src/port/strtof.c @@ -2,7 +2,7 @@ * * strtof.c * - * Portions Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2019-2026, PostgreSQL Global Development Group * * * IDENTIFICATION diff --git a/src/port/system.c b/src/port/system.c index a55946e056380..d72677a4843b4 100644 --- a/src/port/system.c +++ b/src/port/system.c @@ -29,7 +29,7 @@ * quote character on the command line, preserving any text after the last * quote character. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/port/system.c * diff --git a/src/port/tar.c b/src/port/tar.c index 592b4fb7b0f4e..fee9dbbf5be2d 100644 --- a/src/port/tar.c +++ b/src/port/tar.c @@ -87,7 +87,7 @@ read_tar_number(const char *s, int len) * be 512 bytes, per the tar standard. */ int -tarChecksum(char *header) +tarChecksum(const char *header) { int i, sum; @@ -95,15 +95,44 @@ tarChecksum(char *header) /* * Per POSIX, the checksum is the simple sum of all bytes in the header, * treating the bytes as unsigned, and treating the checksum field (at - * offset 148) as though it contained 8 spaces. + * offset TAR_OFFSET_CHECKSUM) as though it contained 8 spaces. */ sum = 8 * ' '; /* presumed value for checksum field */ - for (i = 0; i < 512; i++) - if (i < 148 || i >= 156) + for (i = 0; i < TAR_BLOCK_SIZE; i++) + if (i < TAR_OFFSET_CHECKSUM || i >= TAR_OFFSET_CHECKSUM + 8) sum += 0xFF & header[i]; return sum; } +/* + * Check validity of a tar header (assumed to be 512 bytes long). + * We verify the checksum and the magic number / version. + */ +bool +isValidTarHeader(const char *header) +{ + int sum; + int chk = tarChecksum(header); + + sum = read_tar_number(&header[TAR_OFFSET_CHECKSUM], 8); + + if (sum != chk) + return false; + + /* POSIX tar format */ + if (memcmp(&header[TAR_OFFSET_MAGIC], "ustar\0", 6) == 0 && + memcmp(&header[TAR_OFFSET_VERSION], "00", 2) == 0) + return true; + /* GNU tar format */ + if (memcmp(&header[TAR_OFFSET_MAGIC], "ustar \0", 8) == 0) + return true; + /* not-quite-POSIX format written by pre-9.3 pg_dump */ + if (memcmp(&header[TAR_OFFSET_MAGIC], "ustar00\0", 8) == 0) + return true; + + return false; +} + /* * Fill in the buffer pointed to by h with a tar format header. This buffer diff --git a/src/port/win32common.c b/src/port/win32common.c index d43d5d5324f66..0e1792d253137 100644 --- a/src/port/win32common.c +++ b/src/port/win32common.c @@ -3,7 +3,7 @@ * win32common.c * Common routines shared among the win32*.c ports. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/win32dlopen.c b/src/port/win32dlopen.c index cc34846596915..d9def5e61a221 100644 --- a/src/port/win32dlopen.c +++ b/src/port/win32dlopen.c @@ -3,7 +3,7 @@ * win32dlopen.c * dynamic loader for Windows * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/win32env.c b/src/port/win32env.c index b22fbafde4012..1ef58debc9f6b 100644 --- a/src/port/win32env.c +++ b/src/port/win32env.c @@ -6,7 +6,7 @@ * These functions update both the process environment and caches in * (potentially multiple) C run-time library (CRT) versions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -152,6 +152,13 @@ pgwin32_unsetenv(const char *name) int res; char *envbuf; + /* Error conditions, per POSIX */ + if (name == NULL || name[0] == '\0' || strchr(name, '=') != NULL) + { + errno = EINVAL; + return -1; + } + envbuf = (char *) malloc(strlen(name) + 2); if (!envbuf) return -1; diff --git a/src/port/win32error.c b/src/port/win32error.c index 7ea99738bdbc3..62233b2935671 100644 --- a/src/port/win32error.c +++ b/src/port/win32error.c @@ -3,7 +3,7 @@ * win32error.c * Map win32 error codes to errno values * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32error.c diff --git a/src/port/win32fdatasync.c b/src/port/win32fdatasync.c index 66d759770138c..dd46df1329ae5 100644 --- a/src/port/win32fdatasync.c +++ b/src/port/win32fdatasync.c @@ -4,7 +4,7 @@ * Win32 fdatasync() replacement * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * src/port/win32fdatasync.c * diff --git a/src/port/win32fseek.c b/src/port/win32fseek.c index a75e488ce21d6..1209810b53d76 100644 --- a/src/port/win32fseek.c +++ b/src/port/win32fseek.c @@ -3,7 +3,7 @@ * win32fseek.c * Replacements for fseeko() and ftello(). * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32fseek.c diff --git a/src/port/win32gai_strerror.c b/src/port/win32gai_strerror.c index d52e9a1ef37d3..fb774c3cea588 100644 --- a/src/port/win32gai_strerror.c +++ b/src/port/win32gai_strerror.c @@ -3,7 +3,7 @@ * win32gai_strerror.c * Thread-safe gai_strerror() for Windows. * - * Portions Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32gai_strerror.c diff --git a/src/port/win32getrusage.c b/src/port/win32getrusage.c index 6a197c94376f9..fa2b79cd5ed83 100644 --- a/src/port/win32getrusage.c +++ b/src/port/win32getrusage.c @@ -3,7 +3,7 @@ * win32getrusage.c * get information about resource utilisation * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/win32link.c b/src/port/win32link.c index ee4d6c96c99d5..e7c8623810fe4 100644 --- a/src/port/win32link.c +++ b/src/port/win32link.c @@ -2,7 +2,7 @@ * * win32link.c * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/win32ntdll.c b/src/port/win32ntdll.c index ab6820fb8e5b8..86e4719f3333a 100644 --- a/src/port/win32ntdll.c +++ b/src/port/win32ntdll.c @@ -3,7 +3,7 @@ * win32ntdll.c * Dynamically loaded Windows NT functions. * - * Portions Copyright (c) 2021-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 2021-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -62,7 +62,7 @@ initialize_ntdll(void) return -1; } - *(pg_funcptr_t *) routines[i].address = address; + *routines[i].address = address; } initialized = true; diff --git a/src/port/win32pread.c b/src/port/win32pread.c index 32d56c462ecb9..ddb40ef0198a9 100644 --- a/src/port/win32pread.c +++ b/src/port/win32pread.c @@ -3,7 +3,7 @@ * win32pread.c * Implementation of pread(2) for Windows. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32pread.c @@ -17,7 +17,7 @@ #include ssize_t -pg_pread(int fd, void *buf, size_t size, off_t offset) +pg_pread(int fd, void *buf, size_t size, pgoff_t offset) { OVERLAPPED overlapped = {0}; HANDLE handle; @@ -34,7 +34,9 @@ pg_pread(int fd, void *buf, size_t size, off_t offset) size = Min(size, 1024 * 1024 * 1024); /* Note that this changes the file position, despite not using it. */ - overlapped.Offset = offset; + overlapped.Offset = (DWORD) offset; + overlapped.OffsetHigh = (DWORD) (offset >> 32); + if (!ReadFile(handle, buf, size, &result, &overlapped)) { if (GetLastError() == ERROR_HANDLE_EOF) diff --git a/src/port/win32pwrite.c b/src/port/win32pwrite.c index 249aa6c468537..5eacb8419fb96 100644 --- a/src/port/win32pwrite.c +++ b/src/port/win32pwrite.c @@ -3,7 +3,7 @@ * win32pwrite.c * Implementation of pwrite(2) for Windows. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32pwrite.c @@ -17,7 +17,7 @@ #include ssize_t -pg_pwrite(int fd, const void *buf, size_t size, off_t offset) +pg_pwrite(int fd, const void *buf, size_t size, pgoff_t offset) { OVERLAPPED overlapped = {0}; HANDLE handle; @@ -34,7 +34,9 @@ pg_pwrite(int fd, const void *buf, size_t size, off_t offset) size = Min(size, 1024 * 1024 * 1024); /* Note that this changes the file position, despite not using it. */ - overlapped.Offset = offset; + overlapped.Offset = (DWORD) offset; + overlapped.OffsetHigh = (DWORD) (offset >> 32); + if (!WriteFile(handle, buf, size, &result, &overlapped)) { _dosmaperr(GetLastError()); diff --git a/src/port/win32security.c b/src/port/win32security.c index a46b82dd0482b..46d2fc774b4c5 100644 --- a/src/port/win32security.c +++ b/src/port/win32security.c @@ -3,7 +3,7 @@ * win32security.c * Microsoft Windows Win32 Security Support Functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32security.c @@ -31,9 +31,9 @@ log_error(const char *fmt,...) va_start(ap, fmt); #ifndef FRONTEND - write_stderr(fmt, ap); + vwrite_stderr(fmt, ap); #else - fprintf(stderr, fmt, ap); + vfprintf(stderr, fmt, ap); #endif va_end(ap); } diff --git a/src/port/win32setlocale.c b/src/port/win32setlocale.c index 7c0982439dbcd..0622ecd833c6f 100644 --- a/src/port/win32setlocale.c +++ b/src/port/win32setlocale.c @@ -3,7 +3,7 @@ * win32setlocale.c * Wrapper to work around bugs in Windows setlocale() implementation * - * Copyright (c) 2011-2025, PostgreSQL Global Development Group + * Copyright (c) 2011-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/port/win32setlocale.c @@ -119,9 +119,9 @@ map_locale(const struct locale_map *map, const char *locale) const char *needle_start = map[i].locale_name_start; const char *needle_end = map[i].locale_name_end; const char *replacement = map[i].replacement; - char *match; - char *match_start = NULL; - char *match_end = NULL; + const char *match; + const char *match_start = NULL; + const char *match_end = NULL; match = strstr(locale, needle_start); if (match) @@ -148,7 +148,7 @@ map_locale(const struct locale_map *map, const char *locale) /* Found a match. Replace the matched string. */ int matchpos = match_start - locale; int replacementlen = strlen(replacement); - char *rest = match_end; + const char *rest = match_end; int restlen = strlen(rest); /* check that the result fits in the static buffer */ diff --git a/src/port/win32stat.c b/src/port/win32stat.c index d9bad97b113b8..563696a5e1d90 100644 --- a/src/port/win32stat.c +++ b/src/port/win32stat.c @@ -3,7 +3,7 @@ * win32stat.c * Replacements for functions using GetFileInformationByHandle * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/port/win32ver.rc b/src/port/win32ver.rc index e62020e72a163..68c0772146f05 100644 --- a/src/port/win32ver.rc +++ b/src/port/win32ver.rc @@ -20,7 +20,7 @@ BEGIN VALUE "FileDescription", FILEDESC VALUE "FileVersion", PG_VERSION VALUE "InternalName", _INTERNAL_NAME_ - VALUE "LegalCopyright", "Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group. Portions Copyright (c) 1994, Regents of the University of California." + VALUE "LegalCopyright", "Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group. Portions Copyright (c) 1994, Regents of the University of California." VALUE "OriginalFileName", _ORIGINAL_NAME_ VALUE "ProductName", "PostgreSQL" VALUE "ProductVersion", PG_VERSION diff --git a/src/template/aix b/src/template/aix new file mode 100644 index 0000000000000..9abe1efacd488 --- /dev/null +++ b/src/template/aix @@ -0,0 +1,19 @@ +# src/template/aix + +# Prefer unnamed POSIX semaphores if available, unless user overrides choice +if test x"$PREFERRED_SEMAPHORES" = x"" ; then + PREFERRED_SEMAPHORES=UNNAMED_POSIX +fi + +# Extra CFLAGS for code that will go into a shared library +CFLAGS_SL="" + +if test "$GCC" = yes; then + # We force 64-bit builds, because AIX doesn't play very nice with dynamic + # library loading in 32-bit mode: there's not enough address space. + CPPFLAGS="$CPPFLAGS -maix64" + + # For large binaries/libraries, there will be TOC overflows in AIX. To + # avoid this, pass -bbigtoc linker option to enlarge TOC access range. + LDFLAGS="$LDFLAGS -maix64 -Wl,-bbigtoc" +fi diff --git a/src/template/linux b/src/template/linux index ec3302c4a223f..faefe64254a90 100644 --- a/src/template/linux +++ b/src/template/linux @@ -14,26 +14,3 @@ CFLAGS_SL="-fPIC" # If --enable-profiling is specified, we need -DLINUX_PROFILE PLATFORM_PROFILE_FLAGS="-DLINUX_PROFILE" - -if test "$SUN_STUDIO_CC" = "yes" ; then - CC="$CC -Xa" # relaxed ISO C mode - CFLAGS="-v" # -v is like gcc -Wall - if test "$enable_debug" != yes; then - CFLAGS="$CFLAGS -O" # any optimization breaks debug - fi - - # Pick the right test-and-set (TAS) code for the Sun compiler. - # We would like to use in-line assembler, but the compiler - # requires *.il files to be on every compile line, making - # the build system too fragile. - case $host_cpu in - sparc) - need_tas=yes - tas_file=sunstudio_sparc.s - ;; - i?86|x86_64) - need_tas=yes - tas_file=sunstudio_x86.s - ;; - esac -fi diff --git a/src/template/solaris b/src/template/solaris index f88b1cdad37f8..ea524fdb2bd10 100644 --- a/src/template/solaris +++ b/src/template/solaris @@ -1,31 +1,9 @@ # src/template/solaris -# Extra CFLAGS for code that will go into a shared library -if test "$GCC" = yes ; then - CFLAGS_SL="-fPIC" -else - CFLAGS_SL="-KPIC" +# Prefer unnamed POSIX semaphores if available, unless user overrides choice +if test x"$PREFERRED_SEMAPHORES" = x"" ; then + PREFERRED_SEMAPHORES=UNNAMED_POSIX fi -if test "$SUN_STUDIO_CC" = yes ; then - CC="$CC -Xa" # relaxed ISO C mode - CFLAGS="-v" # -v is like gcc -Wall - if test "$enable_debug" != yes; then - CFLAGS="$CFLAGS -O" # any optimization breaks debug - fi - - # Pick the right test-and-set (TAS) code for the Sun compiler. - # We would like to use in-line assembler, but the compiler - # requires *.il files to be on every compile line, making - # the build system too fragile. - case $host_cpu in - sparc) - need_tas=yes - tas_file=sunstudio_sparc.s - ;; - i?86|x86_64) - need_tas=yes - tas_file=sunstudio_x86.s - ;; - esac -fi +# Extra CFLAGS for code that will go into a shared library +CFLAGS_SL="-fPIC" diff --git a/src/test/Makefile b/src/test/Makefile index 511a72e6238a5..3eb0a06abb46e 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -12,7 +12,15 @@ subdir = src/test top_builddir = ../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = perl postmaster regress isolation modules authentication recovery subscription +SUBDIRS = \ + authentication \ + isolation \ + modules \ + perl \ + postmaster \ + recovery \ + regress \ + subscription ifeq ($(with_icu),yes) SUBDIRS += icu diff --git a/src/test/authentication/Makefile b/src/test/authentication/Makefile index 8b5beced08061..1c12ff823fe56 100644 --- a/src/test/authentication/Makefile +++ b/src/test/authentication/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/authentication # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/authentication/Makefile diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build index 800b3a5ff40fb..282a5054e2ceb 100644 --- a/src/test/authentication/meson.build +++ b/src/test/authentication/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'authentication', diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index 37d96d95a1aeb..a4b11673c26e4 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Set of tests for authentication and pg_hba.conf. The following password # methods are checked through this test: @@ -68,8 +68,24 @@ sub test_conn $node->append_conf('postgresql.conf', "log_connections = on\n"); # Needed to allow connect_fails to inspect postmaster log: $node->append_conf('postgresql.conf', "log_min_messages = debug2"); +$node->append_conf('postgresql.conf', "password_expiration_warning_threshold = '1100d'"); $node->start; +# Set up roles for password_expiration_warning_threshold test +my $current_year = 1900 + ${ [ localtime(time) ] }[5]; +my $expire_year = $current_year - 1; +$node->safe_psql( + 'postgres', + "CREATE ROLE expired LOGIN VALID UNTIL '$expire_year-01-01' PASSWORD 'pass'"); +$expire_year = $current_year + 2; +$node->safe_psql( + 'postgres', + "CREATE ROLE expiration_warnings LOGIN VALID UNTIL '$expire_year-01-01' PASSWORD 'pass'"); +$expire_year = $current_year + 5; +$node->safe_psql( + 'postgres', + "CREATE ROLE no_warnings LOGIN VALID UNTIL '$expire_year-01-01' PASSWORD 'pass'"); + # Test behavior of log_connections GUC # # There wasn't another test file where these tests obviously fit, and we don't @@ -79,39 +95,40 @@ sub test_conn # other tests are added to this file in the future $node->safe_psql('postgres', "CREATE DATABASE test_log_connections"); -my $log_connections = $node->safe_psql('test_log_connections', q(SHOW log_connections;)); +my $log_connections = + $node->safe_psql('test_log_connections', q(SHOW log_connections;)); is($log_connections, 'on', qq(check log connections has expected value 'on')); -$node->connect_ok('test_log_connections', +$node->connect_ok( + 'test_log_connections', qq(log_connections 'on' works as expected for backwards compatibility), log_like => [ qr/connection received/, qr/connection authenticated/, qr/connection authorized: user=\S+ database=test_log_connections/, ], - log_unlike => [ - qr/connection ready/, - ],); + log_unlike => [ qr/connection ready/, ],); -$node->safe_psql('test_log_connections', +$node->safe_psql( + 'test_log_connections', q[ALTER SYSTEM SET log_connections = receipt,authorization,setup_durations; SELECT pg_reload_conf();]); -$node->connect_ok('test_log_connections', +$node->connect_ok( + 'test_log_connections', q(log_connections with subset of specified options logs only those aspects), log_like => [ qr/connection received/, qr/connection authorized: user=\S+ database=test_log_connections/, qr/connection ready/, ], - log_unlike => [ - qr/connection authenticated/, - ],); + log_unlike => [ qr/connection authenticated/, ],); $node->safe_psql('test_log_connections', qq(ALTER SYSTEM SET log_connections = 'all'; SELECT pg_reload_conf();)); -$node->connect_ok('test_log_connections', +$node->connect_ok( + 'test_log_connections', qq(log_connections 'all' logs all available connection aspects), log_like => [ qr/connection received/, @@ -482,6 +499,8 @@ sub test_conn { skip "MD5 not supported" unless $md5_works; test_conn($node, 'user=md5_role', 'md5', 0, + expected_stderr => + qr/authenticated with an MD5-encrypted password/, log_like => [qr/connection authenticated: identity="md5_role" method=md5/]); } @@ -530,6 +549,24 @@ sub test_conn qr/authentication method requirement "!password,!md5,!scram-sha-256" failed: server requested SCRAM-SHA-256 authentication/ ); +# Test password_expiration_warning_threshold +$node->connect_fails( + "user=expired dbname=postgres", + "connection fails due to expired password", + expected_stderr => + qr/password authentication failed for user "expired"/ +); +$node->connect_ok( + "user=expiration_warnings dbname=postgres", + "connection succeeds with password expiration warning", + expected_stderr => + qr/role password will expire soon/ +); +$node->connect_ok( + "user=no_warnings dbname=postgres", + "connection succeeds with no password expiration warning" +); + # Test SYSTEM_USER <> NULL with parallel workers. $node->safe_psql( 'postgres', diff --git a/src/test/authentication/t/002_saslprep.pl b/src/test/authentication/t/002_saslprep.pl index cdf0f96525231..c41c62873bbbb 100644 --- a/src/test/authentication/t/002_saslprep.pl +++ b/src/test/authentication/t/002_saslprep.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test password normalization in SCRAM. # diff --git a/src/test/authentication/t/003_peer.pl b/src/test/authentication/t/003_peer.pl index f2320b62c8721..5c774babd3233 100644 --- a/src/test/authentication/t/003_peer.pl +++ b/src/test/authentication/t/003_peer.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for peer authentication and user name map. # The test is skipped if the platform does not support peer authentication, @@ -171,7 +171,8 @@ sub test_role # Test with regular expression in user name map. # Extract the last 3 characters from the system_user -# or the entire system_user (if its length is <= -3). +# or the entire system_user name (if its length is <= 3). +# We trust this will not include any regex metacharacters. my $regex_test_string = substr($system_user, -3); # Success as the system user regular expression matches. @@ -210,12 +211,17 @@ sub test_role log_like => [qr/connection authenticated: identity="$system_user" method=peer/]); +# Create target role for \1 tests. +my $mapped_name = "test${regex_test_string}map${regex_test_string}user"; +$node->safe_psql('postgres', "CREATE ROLE $mapped_name LOGIN"); + # Success as the regular expression matches and \1 is replaced in the given # subexpression. -reset_pg_ident($node, 'mypeermap', qq{/^$system_user(.*)\$}, 'test\1mapuser'); +reset_pg_ident($node, 'mypeermap', qq{/^.*($regex_test_string)\$}, + 'test\1map\1user'); test_role( $node, - qq{testmapuser}, + $mapped_name, 'peer', 0, 'with regular expression in user name map with \1 replaced', @@ -224,11 +230,11 @@ sub test_role # Success as the regular expression matches and \1 is replaced in the given # subexpression, even if quoted. -reset_pg_ident($node, 'mypeermap', qq{/^$system_user(.*)\$}, - '"test\1mapuser"'); +reset_pg_ident($node, 'mypeermap', qq{/^.*($regex_test_string)\$}, + '"test\1map\1user"'); test_role( $node, - qq{testmapuser}, + $mapped_name, 'peer', 0, 'with regular expression in user name map with quoted \1 replaced', diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl index b9d3663542daf..616183a7af8ac 100644 --- a/src/test/authentication/t/004_file_inclusion.pl +++ b/src/test/authentication/t/004_file_inclusion.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for include directives in HBA and ident files. This test can # only run with Unix-domain sockets. diff --git a/src/test/authentication/t/005_sspi.pl b/src/test/authentication/t/005_sspi.pl index cb3e169002f36..bb3ec4fee3969 100644 --- a/src/test/authentication/t/005_sspi.pl +++ b/src/test/authentication/t/005_sspi.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests targeting SSPI on Windows. diff --git a/src/test/authentication/t/006_login_trigger.pl b/src/test/authentication/t/006_login_trigger.pl index b06de1706ae14..20ea1cef10069 100644 --- a/src/test/authentication/t/006_login_trigger.pl +++ b/src/test/authentication/t/006_login_trigger.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests of authentication via login trigger. Mostly for rejection via # exception, because this scenario cannot be covered with *.sql/*.out regress @@ -144,7 +144,7 @@ BEGIN err_like => [qr/You are welcome/]); # Try to login as allowed Alice. We don't check the Mallory login, because -# FATAL error could cause a timing-dependant panic of IPC::Run. +# FATAL error could cause a timing-dependent panic of IPC::Run. psql_command( $node, 'SELECT 1;', 0, 'try regress_alice', connstr => 'user=regress_alice', diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl index 7b3765e6d253b..04063f4721d9d 100644 --- a/src/test/authentication/t/007_pre_auth.pl +++ b/src/test/authentication/t/007_pre_auth.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for connection behavior prior to authentication. diff --git a/src/test/examples/Makefile b/src/test/examples/Makefile index e72d058e0c4b8..3a4e36465baf1 100644 --- a/src/test/examples/Makefile +++ b/src/test/examples/Makefile @@ -14,7 +14,13 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) LDFLAGS_INTERNAL += $(libpq_pgport) -PROGS = testlibpq testlibpq2 testlibpq3 testlibpq4 testlo testlo64 +PROGS = \ + testlibpq \ + testlibpq2 \ + testlibpq3 \ + testlibpq4 \ + testlo \ + testlo64 all: $(PROGS) diff --git a/src/test/examples/testlibpq3.c b/src/test/examples/testlibpq3.c index 4f7b791388975..cf74865ccb96e 100644 --- a/src/test/examples/testlibpq3.c +++ b/src/test/examples/testlibpq3.c @@ -10,7 +10,6 @@ * * CREATE SCHEMA testlibpq3; * SET search_path = testlibpq3; - * SET standard_conforming_strings = ON; * CREATE TABLE test1 (i int4, t text, b bytea); * INSERT INTO test1 values (1, 'joe''s place', '\000\001\002\003\004'); * INSERT INTO test1 values (2, 'ho there', '\004\003\002\001\000'); diff --git a/src/test/examples/testlibpq3.sql b/src/test/examples/testlibpq3.sql index 35a95ca347b52..a113849b14bd8 100644 --- a/src/test/examples/testlibpq3.sql +++ b/src/test/examples/testlibpq3.sql @@ -1,6 +1,5 @@ CREATE SCHEMA testlibpq3; SET search_path = testlibpq3; -SET standard_conforming_strings = ON; CREATE TABLE test1 (i int4, t text, b bytea); INSERT INTO test1 values (1, 'joe''s place', '\000\001\002\003\004'); INSERT INTO test1 values (2, 'ho there', '\004\003\002\001\000'); diff --git a/src/test/examples/testlo.c b/src/test/examples/testlo.c index 2277000eddb58..fefef1395b8ff 100644 --- a/src/test/examples/testlo.c +++ b/src/test/examples/testlo.c @@ -3,7 +3,7 @@ * testlo.c * test using large objects with libpq * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/test/examples/testlo64.c b/src/test/examples/testlo64.c index c303db92e5bae..32404e59f5de6 100644 --- a/src/test/examples/testlo64.c +++ b/src/test/examples/testlo64.c @@ -3,7 +3,7 @@ * testlo64.c * test using large objects with libpq using 64-bit APIs * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * diff --git a/src/test/icu/Makefile b/src/test/icu/Makefile index 5383c0b30017a..c464bda53509f 100644 --- a/src/test/icu/Makefile +++ b/src/test/icu/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/icu # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/icu/Makefile diff --git a/src/test/icu/meson.build b/src/test/icu/meson.build index bc6c318fcbe91..d2cff55220a53 100644 --- a/src/test/icu/meson.build +++ b/src/test/icu/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'icu', diff --git a/src/test/icu/t/010_database.pl b/src/test/icu/t/010_database.pl index 6ab8b15431987..c6f34fea84bb1 100644 --- a/src/test/icu/t/010_database.pl +++ b/src/test/icu/t/010_database.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/isolation/expected/async-notify.out b/src/test/isolation/expected/async-notify.out index 556e1805893a4..5d6bcce2b025b 100644 --- a/src/test/isolation/expected/async-notify.out +++ b/src/test/isolation/expected/async-notify.out @@ -1,4 +1,4 @@ -Parsed test spec with 3 sessions +Parsed test spec with 7 sessions starting permutation: listenc notify1 notify2 notify3 notifyf step listenc: LISTEN c1; LISTEN c2; @@ -47,6 +47,115 @@ notifier: NOTIFY "c2" with payload "payload" from notifier notifier: NOTIFY "c1" with payload "payloads" from notifier notifier: NOTIFY "c2" with payload "payloads" from notifier +starting permutation: listenc notifys_simple +step listenc: LISTEN c1; LISTEN c2; +step notifys_simple: + BEGIN; + SAVEPOINT s1; + NOTIFY c1, 'simple1'; + NOTIFY c2, 'simple2'; + RELEASE SAVEPOINT s1; + COMMIT; + +notifier: NOTIFY "c1" with payload "simple1" from notifier +notifier: NOTIFY "c2" with payload "simple2" from notifier + +starting permutation: lsbegin lssavepoint lslisten lsrelease lscommit lsnotify +step lsbegin: BEGIN; +step lssavepoint: SAVEPOINT s1; +step lslisten: LISTEN c1; LISTEN c2; +step lsrelease: RELEASE SAVEPOINT s1; +step lscommit: COMMIT; +step lsnotify: NOTIFY c1, 'subxact_test'; +listen_subxact: NOTIFY "c1" with payload "subxact_test" from listen_subxact + +starting permutation: lsbegin lslisten_outer lssavepoint lslisten lsrelease lscommit lsnotify +step lsbegin: BEGIN; +step lslisten_outer: LISTEN c3; +step lssavepoint: SAVEPOINT s1; +step lslisten: LISTEN c1; LISTEN c2; +step lsrelease: RELEASE SAVEPOINT s1; +step lscommit: COMMIT; +step lsnotify: NOTIFY c1, 'subxact_test'; +listen_subxact: NOTIFY "c1" with payload "subxact_test" from listen_subxact + +starting permutation: lsbegin lssavepoint lslisten lsrollback lscommit lsnotify_check +step lsbegin: BEGIN; +step lssavepoint: SAVEPOINT s1; +step lslisten: LISTEN c1; LISTEN c2; +step lsrollback: ROLLBACK TO SAVEPOINT s1; +step lscommit: COMMIT; +step lsnotify_check: NOTIFY c1, 'should_not_receive'; + +starting permutation: lunlisten_all notify1 lcheck +step lunlisten_all: BEGIN; LISTEN c1; UNLISTEN *; COMMIT; +step notify1: NOTIFY c1; +step lcheck: SELECT 1 AS x; +x +- +1 +(1 row) + + +starting permutation: listenc notify_many_with_dup +step listenc: LISTEN c1; LISTEN c2; +step notify_many_with_dup: + BEGIN; + SELECT pg_notify('c1', 'msg' || s::text) FROM generate_series(1, 17) s; + SELECT pg_notify('c1', 'msg1'); + COMMIT; + +pg_notify +--------- + + + + + + + + + + + + + + + + + +(17 rows) + +pg_notify +--------- + +(1 row) + +notifier: NOTIFY "c1" with payload "msg1" from notifier +notifier: NOTIFY "c1" with payload "msg2" from notifier +notifier: NOTIFY "c1" with payload "msg3" from notifier +notifier: NOTIFY "c1" with payload "msg4" from notifier +notifier: NOTIFY "c1" with payload "msg5" from notifier +notifier: NOTIFY "c1" with payload "msg6" from notifier +notifier: NOTIFY "c1" with payload "msg7" from notifier +notifier: NOTIFY "c1" with payload "msg8" from notifier +notifier: NOTIFY "c1" with payload "msg9" from notifier +notifier: NOTIFY "c1" with payload "msg10" from notifier +notifier: NOTIFY "c1" with payload "msg11" from notifier +notifier: NOTIFY "c1" with payload "msg12" from notifier +notifier: NOTIFY "c1" with payload "msg13" from notifier +notifier: NOTIFY "c1" with payload "msg14" from notifier +notifier: NOTIFY "c1" with payload "msg15" from notifier +notifier: NOTIFY "c1" with payload "msg16" from notifier +notifier: NOTIFY "c1" with payload "msg17" from notifier + +starting permutation: listenc llisten l2listen l3listen lslisten +step listenc: LISTEN c1; LISTEN c2; +step llisten: LISTEN c1; LISTEN c2; +step l2listen: LISTEN c1; +step l3listen: LISTEN c1; +step lslisten: LISTEN c1; LISTEN c2; + starting permutation: llisten notify1 notify2 notify3 notifyf lcheck step llisten: LISTEN c1; LISTEN c2; step notify1: NOTIFY c1; @@ -95,6 +204,8 @@ listener: NOTIFY "c2" with payload "" from notifier starting permutation: l2listen l2begin notify1 lbegins llisten lcommit l2commit l2stop step l2listen: LISTEN c1; +listener2: NOTIFY "c1" with payload "" from notifier +listener2: NOTIFY "c1" with payload "" from notifier step l2begin: BEGIN; step notify1: NOTIFY c1; step lbegins: BEGIN ISOLATION LEVEL SERIALIZABLE; @@ -104,6 +215,17 @@ step l2commit: COMMIT; listener2: NOTIFY "c1" with payload "" from notifier step l2stop: UNLISTEN *; +starting permutation: lch_listen nch_notify lch_check +step lch_listen: LISTEN ch; +step nch_notify: NOTIFY ch, 'aa'; +step lch_check: SELECT 1 AS x; +x +- +1 +(1 row) + +listener_ch: NOTIFY "ch" with payload "aa" from notifier_ch + starting permutation: llisten lbegin usage bignotify usage step llisten: LISTEN c1; LISTEN c2; step lbegin: BEGIN; diff --git a/src/test/isolation/expected/cluster-toast-value-reuse.out b/src/test/isolation/expected/cluster-toast-value-reuse.out new file mode 100644 index 0000000000000..cb14ddcee34b8 --- /dev/null +++ b/src/test/isolation/expected/cluster-toast-value-reuse.out @@ -0,0 +1,29 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_begin s1_update s2_store_chunk_ids s2_cluster s1_commit s2_verify_chunk_ids +step s1_begin: BEGIN; +step s1_update: UPDATE cluster_toast_value SET flag = 1 WHERE TRUE; +step s2_store_chunk_ids: + CREATE TABLE cluster_chunk_id AS + SELECT c.id, pg_column_toast_chunk_id(c.value) AS chunk_id + FROM cluster_toast_value c; + SELECT count(*) FROM cluster_chunk_id; + +count +----- + 1 +(1 row) + +step s2_cluster: CLUSTER cluster_toast_value; +step s1_commit: COMMIT; +step s2_cluster: <... completed> +step s2_verify_chunk_ids: + SELECT o.id AS chunk_ids_preserved + FROM cluster_chunk_id o + JOIN cluster_toast_value c ON o.id = c.id + WHERE o.chunk_id != pg_column_toast_chunk_id(c.value); + +chunk_ids_preserved +------------------- +(0 rows) + diff --git a/src/test/isolation/expected/eval-plan-qual.out b/src/test/isolation/expected/eval-plan-qual.out index 032d4208d51f3..a122047fd2aef 100644 --- a/src/test/isolation/expected/eval-plan-qual.out +++ b/src/test/isolation/expected/eval-plan-qual.out @@ -1218,6 +1218,100 @@ subid|id (1 row) +starting permutation: tid1 tid2 c1 c2 read +step tid1: UPDATE accounts SET balance = balance + 100 WHERE ctid = '(0,1)' RETURNING accountid, balance; +accountid|balance +---------+------- +checking | 700 +(1 row) + +step tid2: UPDATE accounts SET balance = balance + 200 WHERE ctid = '(0,1)' RETURNING accountid, balance; +step c1: COMMIT; +step tid2: <... completed> +accountid|balance +---------+------- +(0 rows) + +step c2: COMMIT; +step read: SELECT * FROM accounts ORDER BY accountid; +accountid|balance|balance2 +---------+-------+-------- +checking | 700| 1400 +savings | 600| 1200 +(2 rows) + + +starting permutation: tid1 tidsucceed2 c1 c2 read +step tid1: UPDATE accounts SET balance = balance + 100 WHERE ctid = '(0,1)' RETURNING accountid, balance; +accountid|balance +---------+------- +checking | 700 +(1 row) + +step tidsucceed2: UPDATE accounts SET balance = balance + 200 WHERE ctid = '(0,1)' OR ctid = '(0,3)' RETURNING accountid, balance; +step c1: COMMIT; +step tidsucceed2: <... completed> +accountid|balance +---------+------- +checking | 900 +(1 row) + +step c2: COMMIT; +step read: SELECT * FROM accounts ORDER BY accountid; +accountid|balance|balance2 +---------+-------+-------- +checking | 900| 1800 +savings | 600| 1200 +(2 rows) + + +starting permutation: tidrange1 tidrange2 c1 c2 read +step tidrange1: UPDATE accounts SET balance = balance + 100 WHERE ctid BETWEEN '(0,1)' AND '(0,1)' RETURNING accountid, balance; +accountid|balance +---------+------- +checking | 700 +(1 row) + +step tidrange2: UPDATE accounts SET balance = balance + 200 WHERE ctid BETWEEN '(0,1)' AND '(0,1)' RETURNING accountid, balance; +step c1: COMMIT; +step tidrange2: <... completed> +accountid|balance +---------+------- +(0 rows) + +step c2: COMMIT; +step read: SELECT * FROM accounts ORDER BY accountid; +accountid|balance|balance2 +---------+-------+-------- +checking | 700| 1400 +savings | 600| 1200 +(2 rows) + + +starting permutation: tid1 tid2 r1 c2 read +step tid1: UPDATE accounts SET balance = balance + 100 WHERE ctid = '(0,1)' RETURNING accountid, balance; +accountid|balance +---------+------- +checking | 700 +(1 row) + +step tid2: UPDATE accounts SET balance = balance + 200 WHERE ctid = '(0,1)' RETURNING accountid, balance; +step r1: ROLLBACK; +step tid2: <... completed> +accountid|balance +---------+------- +checking | 800 +(1 row) + +step c2: COMMIT; +step read: SELECT * FROM accounts ORDER BY accountid; +accountid|balance|balance2 +---------+-------+-------- +checking | 800| 1600 +savings | 600| 1200 +(2 rows) + + starting permutation: simplepartupdate conditionalpartupdate c1 c2 read_part step simplepartupdate: update parttbl set b = b + 10; @@ -1363,3 +1457,39 @@ step sysmerge2: step c1: COMMIT; step sysmerge2: <... completed> step c2: COMMIT; + +starting permutation: s1pp1 s2pp1 s2pp2 s2pp3 c1 c2 +step s1pp1: UPDATE another_parttbl SET b = b + 1 WHERE a = 1; +step s2pp1: SET plan_cache_mode TO force_generic_plan; +step s2pp2: PREPARE epd AS DELETE FROM another_parttbl WHERE a = $1; +step s2pp3: EXECUTE epd(1); +step c1: COMMIT; +step s2pp3: <... completed> +step c2: COMMIT; + +starting permutation: s1pp1 s2pp4 c1 c2 +step s1pp1: UPDATE another_parttbl SET b = b + 1 WHERE a = 1; +step s2pp4: DELETE FROM another_parttbl WHERE a = (SELECT 1); +step c1: COMMIT; +step s2pp4: <... completed> +step c2: COMMIT; + +starting permutation: updateformergevalues mergevalues c1 c2 read +step updateformergevalues: UPDATE accounts SET balance = balance + 100; +step mergevalues: + MERGE INTO accounts + USING (VALUES ('checking', 610), ('savings', 620)) v(accountid, balance) + ON v.accountid = accounts.accountid + WHEN MATCHED THEN UPDATE SET balance = v.balance + WHEN NOT MATCHED THEN INSERT VALUES ('unmatched', -1); + +step c1: COMMIT; +step mergevalues: <... completed> +step c2: COMMIT; +step read: SELECT * FROM accounts ORDER BY accountid; +accountid|balance|balance2 +---------+-------+-------- +checking | 610| 1220 +savings | 620| 1240 +(2 rows) + diff --git a/src/test/isolation/expected/fk-concurrent-pk-upd.out b/src/test/isolation/expected/fk-concurrent-pk-upd.out new file mode 100644 index 0000000000000..4dd9535d3c0ba --- /dev/null +++ b/src/test/isolation/expected/fk-concurrent-pk-upd.out @@ -0,0 +1,105 @@ +Parsed test spec with 3 sessions + +starting permutation: s2b s2ukey s1b s1i s2c s1c s2s s1s +step s2b: BEGIN; +step s2ukey: UPDATE parent SET parent_key = 2 WHERE parent_key = 1; +step s1b: BEGIN; +step s1i: INSERT INTO child VALUES (1, 1); +step s2c: COMMIT; +step s1i: <... completed> +ERROR: insert or update on table "child" violates foreign key constraint "child_parent_key_fkey" +step s1c: COMMIT; +step s2s: SELECT * FROM parent; +parent_key|aux +----------+--- + 2|foo +(1 row) + +step s1s: SELECT * FROM child; +child_key|parent_key +---------+---------- +(0 rows) + + +starting permutation: s2b s2uaux s1b s1i s2c s1c s2s s1s +step s2b: BEGIN; +step s2uaux: UPDATE parent SET aux = 'bar' WHERE parent_key = 1; +step s1b: BEGIN; +step s1i: INSERT INTO child VALUES (1, 1); +step s2c: COMMIT; +step s1c: COMMIT; +step s2s: SELECT * FROM parent; +parent_key|aux +----------+--- + 1|bar +(1 row) + +step s1s: SELECT * FROM child; +child_key|parent_key +---------+---------- + 1| 1 +(1 row) + + +starting permutation: s2b s2ukey s1b s1i s2ukey2 s2c s1c s2s s1s +step s2b: BEGIN; +step s2ukey: UPDATE parent SET parent_key = 2 WHERE parent_key = 1; +step s1b: BEGIN; +step s1i: INSERT INTO child VALUES (1, 1); +step s2ukey2: UPDATE parent SET parent_key = 1 WHERE parent_key = 2; +step s2c: COMMIT; +step s1i: <... completed> +step s1c: COMMIT; +step s2s: SELECT * FROM parent; +parent_key|aux +----------+--- + 1|foo +(1 row) + +step s1s: SELECT * FROM child; +child_key|parent_key +---------+---------- + 1| 1 +(1 row) + + +starting permutation: s2b s2ukey s3b s3i s2c s3c s2s s3s +step s2b: BEGIN; +step s2ukey: UPDATE parent SET parent_key = 2 WHERE parent_key = 1; +step s3b: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s3i: INSERT INTO child VALUES (2, 1); +step s2c: COMMIT; +step s3i: <... completed> +ERROR: could not serialize access due to concurrent update +step s3c: COMMIT; +step s2s: SELECT * FROM parent; +parent_key|aux +----------+--- + 2|foo +(1 row) + +step s3s: SELECT * FROM child; +child_key|parent_key +---------+---------- +(0 rows) + + +starting permutation: s2b s2uaux s3b s3i s2c s3c s2s s3s +step s2b: BEGIN; +step s2uaux: UPDATE parent SET aux = 'bar' WHERE parent_key = 1; +step s3b: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s3i: INSERT INTO child VALUES (2, 1); +step s2c: COMMIT; +step s3c: COMMIT; +step s2s: SELECT * FROM parent; +parent_key|aux +----------+--- + 1|bar +(1 row) + +step s3s: SELECT * FROM child; +child_key|parent_key +---------+---------- + 2| 1 +(1 row) + diff --git a/src/test/isolation/expected/fk-snapshot-2.out b/src/test/isolation/expected/fk-snapshot-2.out new file mode 100644 index 0000000000000..0a4c9646fca4e --- /dev/null +++ b/src/test/isolation/expected/fk-snapshot-2.out @@ -0,0 +1,61 @@ +Parsed test spec with 2 sessions + +starting permutation: s1rr s2rr s2ins s1del s2c s1c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2ins: INSERT INTO child VALUES (1, 1); +step s1del: DELETE FROM parent WHERE parent_id = 1; +step s2c: COMMIT; +step s1del: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1rr s2rr s1del s2ins s1c s2c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del: DELETE FROM parent WHERE parent_id = 1; +step s2ins: INSERT INTO child VALUES (1, 1); +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; + +starting permutation: s1rc s2rc s2ins s1del s2c s1c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2ins: INSERT INTO child VALUES (1, 1); +step s1del: DELETE FROM parent WHERE parent_id = 1; +step s2c: COMMIT; +step s1del: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1rc s2rc s1del s2ins s1c s2c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1del: DELETE FROM parent WHERE parent_id = 1; +step s2ins: INSERT INTO child VALUES (1, 1); +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: insert or update on table "child" violates foreign key constraint "child_parent_id_fkey" +step s2c: COMMIT; + +starting permutation: s1ser s2ser s2ins s1del s2c s1c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ins: INSERT INTO child VALUES (1, 1); +step s1del: DELETE FROM parent WHERE parent_id = 1; +step s2c: COMMIT; +step s1del: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1ser s2ser s1del s2ins s1c s2c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1del: DELETE FROM parent WHERE parent_id = 1; +step s2ins: INSERT INTO child VALUES (1, 1); +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; diff --git a/src/test/isolation/expected/fk-snapshot-3.out b/src/test/isolation/expected/fk-snapshot-3.out new file mode 100644 index 0000000000000..f98cb72fdac30 --- /dev/null +++ b/src/test/isolation/expected/fk-snapshot-3.out @@ -0,0 +1,213 @@ +Parsed test spec with 2 sessions + +starting permutation: s1rr s2rr s2ins s1del s2c s1c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1del: DELETE FROM parent WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1del: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_valid_at_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1rr s2rr s1del s2ins s1c s2c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del: DELETE FROM parent WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; + +starting permutation: s1rc s2rc s2ins s1del s2c s1c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1del: DELETE FROM parent WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1del: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_valid_at_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1rc s2rc s1del s2ins s1c s2c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1del: DELETE FROM parent WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: insert or update on table "child" violates foreign key constraint "child_parent_id_valid_at_fkey" +step s2c: COMMIT; + +starting permutation: s1ser s2ser s2ins s1del s2c s1c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1del: DELETE FROM parent WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1del: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_valid_at_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1ser s2ser s1del s2ins s1c s2c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1del: DELETE FROM parent WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; + +starting permutation: s1rc s2rc s2ins s1upok s2c s1c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1upok: UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1upok: <... completed> +step s1c: COMMIT; + +starting permutation: s1rc s2rc s1upok s2ins s1c s2c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1upok: UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +step s2c: COMMIT; + +starting permutation: s1rr s2rr s2ins s1upok s2c s1c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1upok: UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1upok: <... completed> +step s1c: COMMIT; + +starting permutation: s1rr s2rr s1upok s2ins s1c s2c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upok: UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; + +starting permutation: s1ser s2ser s2ins s1upok s2c s1c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1upok: UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1upok: <... completed> +step s1c: COMMIT; + +starting permutation: s1ser s2ser s1upok s2ins s1c s2c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1upok: UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; + +starting permutation: s1rc s2rc s2ins s1upbad s2c s1c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1upbad: UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1upbad: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_valid_at_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1rc s2rc s1upbad s2ins s1c s2c +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1upbad: UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: insert or update on table "child" violates foreign key constraint "child_parent_id_valid_at_fkey" +step s2c: COMMIT; + +starting permutation: s1rr s2rr s2ins s1upbad s2c s1c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1upbad: UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1upbad: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_valid_at_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1rr s2rr s1upbad s2ins s1c s2c +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upbad: UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; + +starting permutation: s1ser s2ser s2ins s1upbad s2c s1c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1upbad: UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; +step s2c: COMMIT; +step s1upbad: <... completed> +ERROR: update or delete on table "parent" violates foreign key constraint "child_parent_id_valid_at_fkey" on table "child" +step s1c: COMMIT; + +starting permutation: s1ser s2ser s1upbad s2ins s1c s2c +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1upbad: UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; +step s2ins: + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); + +step s1c: COMMIT; +step s2ins: <... completed> +ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; diff --git a/src/test/isolation/expected/for-portion-of.out b/src/test/isolation/expected/for-portion-of.out new file mode 100644 index 0000000000000..2438c46959960 --- /dev/null +++ b/src/test/isolation/expected/for-portion-of.out @@ -0,0 +1,4089 @@ +Parsed test spec with 2 sessions + +starting permutation: s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-03-01,2025-04-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +(2 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2upd2027 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2upd202503 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-01-01,2026-01-01)| 8.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2upd20252026 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)|10.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s2upd2027 s1upd2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s2upd202503 s1upd2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)| 8.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-03-01,2025-04-01)|10.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-03-01,2025-04-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s2upd20252026 s1upd2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-06-01,2026-06-01)|10.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2del2027 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2del202503 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-01-01,2026-01-01)| 8.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2del20252026 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2del2027 s1upd2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2del202503 s1upd2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2del20252026 s1upd2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1upd2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +(2 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2upd2027 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2upd202503 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id|valid_at|price +--+--------+----- +(0 rows) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2upd20252026 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2upd2027 s1del2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rc s2rc s2upd202503 s1del2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-03-01,2025-04-01)|10.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2upd20252026 s1del2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2025-06-01,2026-06-01)|10.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2del2027 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2del202503 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id|valid_at|price +--+--------+----- +(0 rows) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2del20252026 s2c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2del2027 s1del2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock2027: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rc s2rc s2del202503 s1del2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock202503: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2del20252026 s1del2025 s2c s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1del2025 s1c s1q +step s1rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2rc: BEGIN ISOLATION LEVEL READ COMMITTED; +step s2lock20252026: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; + +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1lock2025: + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; + +step s2c: COMMIT; +step s1lock2025: <... completed> +id|valid_at|price +--+--------+----- +(0 rows) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2upd2027 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s2upd202503 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-03-01,2025-04-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s2upd20252026 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s1upd2025 s1c s2upd2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s1upd2025 s1c s2upd202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s1upd2025 s1c s2upd20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)|10.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s2upd2027 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2upd202503 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2upd20252026 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd2027 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd202503 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd20252026 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s1upd2025 s1c s2upd2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s1q s1upd2025 s1c s2upd202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s1q s1upd2025 s1c s2upd20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)|10.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1rr s2rr s1q s2upd2027 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd202503 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd20252026 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2del2027 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s2del202503 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s2del20252026 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1upd2025 s1c s2del2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s1upd2025 s1c s2del202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s1upd2025 s1c s2del20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2del2027 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2del202503 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2del20252026 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del2027 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del202503 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del20252026 s2c s1upd2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s1upd2025 s1c s2del2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s1q s1upd2025 s1c s2del202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-03-01)| 8.00 +[1,2)|[2025-04-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s1q s1upd2025 s1c s2del20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2del2027 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del202503 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del20252026 s1upd2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2upd2027 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s2upd202503 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2upd20252026 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1del2025 s1c s2upd2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s1del2025 s1c s2upd202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1del2025 s1c s2upd20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2upd2027 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2upd202503 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2upd20252026 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd2027 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd202503 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd20252026 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s1del2025 s1c s2upd2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(4 rows) + + +starting permutation: s1rr s2rr s1q s1del2025 s1c s2upd202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s1del2025 s1c s2upd20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd2027 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd202503 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd202503: + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-03-01,2025-04-01)|10.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s2upd20252026 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent update +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2025-06-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2del2027 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s2del202503 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2del20252026 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1del2025 s1c s2del2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1del2025 s1c s2del202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1del2025 s1c s2del20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2del2027 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2del202503 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s2del20252026 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del2027 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del202503 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del20252026 s2c s1del2025 s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s1del2025 s1c s2del2027 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(3 rows) + + +starting permutation: s1rr s2rr s1q s1del2025 s1c s2del202503 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s1del2025 s1c s2del20252026 s2c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del2027 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del2027: + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2027-01-01)| 5.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del202503 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del202503: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-03-01)| 5.00 +[1,2)|[2025-04-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1rr s2rr s1q s2del20252026 s1del2025 s2c s1c s1q +step s1rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2rr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2030-01-01)| 5.00 +(1 row) + +step s2del20252026: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; + +step s1del2025: + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1del2025: <... completed> +ERROR: could not serialize access due to concurrent delete +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-06-01)| 5.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(2 rows) + + +starting permutation: s1ser s2ser s2upd2027 s2c s1upd2025 s1c s1q +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2upd2027: + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2027-01-01)| 5.00 +[1,2)|[2027-01-01,2028-01-01)|10.00 +[1,2)|[2028-01-01,2030-01-01)| 5.00 +(5 rows) + + +starting permutation: s1ser s2ser s2upd20252026 s2c s1upd2025 s1c s1q +step s1ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2ser: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2upd20252026: + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; + +step s2c: COMMIT; +step s1upd2025: + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; + +step s1c: COMMIT; +step s1q: SELECT * FROM products ORDER BY id, valid_at; +id |valid_at |price +-----+-----------------------+----- +[1,2)|[2020-01-01,2025-01-01)| 5.00 +[1,2)|[2025-01-01,2025-06-01)| 8.00 +[1,2)|[2025-06-01,2026-01-01)| 8.00 +[1,2)|[2026-01-01,2026-06-01)|10.00 +[1,2)|[2026-06-01,2030-01-01)| 5.00 +(5 rows) + diff --git a/src/test/isolation/expected/insert-conflict-do-select.out b/src/test/isolation/expected/insert-conflict-do-select.out new file mode 100644 index 0000000000000..bccfd47dcfbee --- /dev/null +++ b/src/test/isolation/expected/insert-conflict-do-select.out @@ -0,0 +1,138 @@ +Parsed test spec with 2 sessions + +starting permutation: insert1 insert2 c1 select2 c2 +step insert1: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step insert2: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step c1: COMMIT; +step select2: SELECT * FROM doselect; +key|val +---+-------- + 1|original +(1 row) + +step c2: COMMIT; + +starting permutation: insert1_update insert2_update c1 select2 c2 +step insert1_update: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +step c1: COMMIT; +step insert2_update: <... completed> +key|val +---+-------- + 1|original +(1 row) + +step select2: SELECT * FROM doselect; +key|val +---+-------- + 1|original +(1 row) + +step c2: COMMIT; + +starting permutation: insert1_update insert2_update a1 select2 c2 +step insert1_update: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +step a1: ABORT; +step insert2_update: <... completed> +key|val +---+-------- + 1|original +(1 row) + +step select2: SELECT * FROM doselect; +key|val +---+-------- + 1|original +(1 row) + +step c2: COMMIT; + +starting permutation: insert1_keyshare insert2_update c1 select2 c2 +step insert1_keyshare: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR KEY SHARE RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +step c1: COMMIT; +step insert2_update: <... completed> +key|val +---+-------- + 1|original +(1 row) + +step select2: SELECT * FROM doselect; +key|val +---+-------- + 1|original +(1 row) + +step c2: COMMIT; + +starting permutation: insert1_share insert2_update c1 select2 c2 +step insert1_share: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR SHARE RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +step c1: COMMIT; +step insert2_update: <... completed> +key|val +---+-------- + 1|original +(1 row) + +step select2: SELECT * FROM doselect; +key|val +---+-------- + 1|original +(1 row) + +step c2: COMMIT; + +starting permutation: insert1_nokeyupd insert2_update c1 select2 c2 +step insert1_nokeyupd: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR NO KEY UPDATE RETURNING *; +key|val +---+-------- + 1|original +(1 row) + +step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; +step c1: COMMIT; +step insert2_update: <... completed> +key|val +---+-------- + 1|original +(1 row) + +step select2: SELECT * FROM doselect; +key|val +---+-------- + 1|original +(1 row) + +step c2: COMMIT; diff --git a/src/test/isolation/expected/insert-conflict-do-update-4.out b/src/test/isolation/expected/insert-conflict-do-update-4.out new file mode 100644 index 0000000000000..80af2798a9409 --- /dev/null +++ b/src/test/isolation/expected/insert-conflict-do-update-4.out @@ -0,0 +1,63 @@ +Parsed test spec with 2 sessions + +starting permutation: lock2 insert1 update2a c2 select1 c1 +step lock2: SELECT * FROM upsert WHERE i = 1 FOR UPDATE; +i| j| k +-+--+--- +1|10|100 +(1 row) + +step insert1: INSERT INTO upsert VALUES (1, 11, 111) + ON CONFLICT (i) DO UPDATE SET k = EXCLUDED.k; +step update2a: UPDATE upsert SET i = i + 10 WHERE i = 1; +step c2: COMMIT; +step insert1: <... completed> +step select1: SELECT * FROM upsert ORDER BY i; + i| j| k +--+--+--- + 1|11|111 +11|10|100 +(2 rows) + +step c1: COMMIT; + +starting permutation: lock2 insert1 update2b c2 select1 c1 +step lock2: SELECT * FROM upsert WHERE i = 1 FOR UPDATE; +i| j| k +-+--+--- +1|10|100 +(1 row) + +step insert1: INSERT INTO upsert VALUES (1, 11, 111) + ON CONFLICT (i) DO UPDATE SET k = EXCLUDED.k; +step update2b: UPDATE upsert SET i = i + 150 WHERE i = 1; +step c2: COMMIT; +step insert1: <... completed> +step select1: SELECT * FROM upsert ORDER BY i; + i| j| k +---+--+--- + 1|11|111 +151|10|100 +(2 rows) + +step c1: COMMIT; + +starting permutation: lock2 insert1 delete2 c2 select1 c1 +step lock2: SELECT * FROM upsert WHERE i = 1 FOR UPDATE; +i| j| k +-+--+--- +1|10|100 +(1 row) + +step insert1: INSERT INTO upsert VALUES (1, 11, 111) + ON CONFLICT (i) DO UPDATE SET k = EXCLUDED.k; +step delete2: DELETE FROM upsert WHERE i = 1; +step c2: COMMIT; +step insert1: <... completed> +step select1: SELECT * FROM upsert ORDER BY i; +i| j| k +-+--+--- +1|11|111 +(1 row) + +step c1: COMMIT; diff --git a/src/test/isolation/expected/intra-grant-inplace.out b/src/test/isolation/expected/intra-grant-inplace.out index 1aa9da622da05..23c34d0ca0935 100644 --- a/src/test/isolation/expected/intra-grant-inplace.out +++ b/src/test/isolation/expected/intra-grant-inplace.out @@ -226,7 +226,7 @@ step revoke4: <... completed> starting permutation: b1 drop1 b3 sfu3 revoke4 c1 r3 step b1: BEGIN; step drop1: - DROP TABLE intra_grant_inplace; + DELETE FROM pg_class WHERE relname = 'intra_grant_inplace'; step b3: BEGIN ISOLATION LEVEL READ COMMITTED; step sfu3: @@ -248,6 +248,6 @@ relhasindex ----------- (0 rows) -s4: WARNING: got: relation "intra_grant_inplace" does not exist +s4: WARNING: got: cache lookup failed for relation REDACTED step revoke4: <... completed> step r3: ROLLBACK; diff --git a/src/test/isolation/expected/merge-match-recheck.out b/src/test/isolation/expected/merge-match-recheck.out index 9a44a5959270b..4250b85af2d3c 100644 --- a/src/test/isolation/expected/merge-match-recheck.out +++ b/src/test/isolation/expected/merge-match-recheck.out @@ -241,7 +241,41 @@ starting permutation: update_bal1_tg merge_bal_tg c2 select1_tg c1 s2: NOTICE: Update: (1,160,s1,setup) -> (1,50,s1,"setup updated by update_bal1_tg") step update_bal1_tg: UPDATE target_tg t SET balance = 50, val = t.val || ' updated by update_bal1_tg' WHERE t.key = 1; step merge_bal_tg: - MERGE INTO target_tg t + WITH t AS ( + MERGE INTO target_tg t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.* + ) + SELECT * FROM t; + +step c2: COMMIT; +s1: NOTICE: Update: (1,50,s1,"setup updated by update_bal1_tg") -> (1,100,s1,"setup updated by update_bal1_tg when1") +step merge_bal_tg: <... completed> +key|balance|status|val +---+-------+------+------------------------------------- + 1| 100|s1 |setup updated by update_bal1_tg when1 +(1 row) + +step select1_tg: SELECT * FROM target_tg; +key|balance|status|val +---+-------+------+------------------------------------- + 1| 100|s1 |setup updated by update_bal1_tg when1 +(1 row) + +step c1: COMMIT; + +starting permutation: update1 update6 merge_bal c2 select1 c1 +step update1: UPDATE target t SET balance = balance + 10, val = t.val || ' updated by update1' WHERE t.key = 1; +step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; +step merge_bal: + MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED AND balance < 100 THEN @@ -252,16 +286,136 @@ step merge_bal_tg: UPDATE SET balance = balance * 8, val = t.val || ' when3'; step c2: COMMIT; -s1: NOTICE: Update: (1,50,s1,"setup updated by update_bal1_tg") -> (1,100,s1,"setup updated by update_bal1_tg when1") +step merge_bal: <... completed> +step select1: SELECT * FROM target; +key|balance|status|val +---+-------+------+------------------------------------------------- + 1| 140|s1 |setup updated by update1 updated by update6 when1 +(1 row) + +step c1: COMMIT; + +starting permutation: update1_pa update6_pa merge_bal_pa c2 select1_pa c1 +step update1_pa: UPDATE target_pa t SET balance = balance + 10, val = t.val || ' updated by update1_pa' WHERE t.key = 1; +step update6_pa: UPDATE target_pa t SET balance = balance - 100, val = t.val || ' updated by update6_pa' WHERE t.key = 1; +step merge_bal_pa: + MERGE INTO target_pa t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3'; + +step c2: COMMIT; +step merge_bal_pa: <... completed> +step select1_pa: SELECT * FROM target_pa; +key|balance|status|val +---+-------+------+------------------------------------------------------- + 1| 140|s1 |setup updated by update1_pa updated by update6_pa when1 +(1 row) + +step c1: COMMIT; + +starting permutation: update1_tg update6_tg merge_bal_tg c2 select1_tg c1 +s2: NOTICE: Update: (1,160,s1,setup) -> (1,170,s1,"setup updated by update1_tg") +step update1_tg: UPDATE target_tg t SET balance = balance + 10, val = t.val || ' updated by update1_tg' WHERE t.key = 1; +s2: NOTICE: Update: (1,170,s1,"setup updated by update1_tg") -> (1,70,s1,"setup updated by update1_tg updated by update6_tg") +step update6_tg: UPDATE target_tg t SET balance = balance - 100, val = t.val || ' updated by update6_tg' WHERE t.key = 1; +step merge_bal_tg: + WITH t AS ( + MERGE INTO target_tg t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.* + ) + SELECT * FROM t; + +step c2: COMMIT; +s1: NOTICE: Update: (1,70,s1,"setup updated by update1_tg updated by update6_tg") -> (1,140,s1,"setup updated by update1_tg updated by update6_tg when1") step merge_bal_tg: <... completed> +key|balance|status|val +---+-------+------+------------------------------------------------------- + 1| 140|s1 |setup updated by update1_tg updated by update6_tg when1 +(1 row) + step select1_tg: SELECT * FROM target_tg; -key|balance|status|val ----+-------+------+------------------------------------- - 1| 100|s1 |setup updated by update_bal1_tg when1 +key|balance|status|val +---+-------+------+------------------------------------------------------- + 1| 140|s1 |setup updated by update1_tg updated by update6_tg when1 +(1 row) + +step c1: COMMIT; + +starting permutation: update7 update6 merge_bal c2 select1 c1 +step update7: UPDATE target t SET balance = 350, val = t.val || ' updated by update7' WHERE t.key = 1; +step update6: UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; +step merge_bal: + MERGE INTO target t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3'; + +step c2: COMMIT; +step merge_bal: <... completed> +step select1: SELECT * FROM target; +key|balance|status|val +---+-------+------+------------------------------------------------- + 1| 2000|s1 |setup updated by update7 updated by update6 when3 (1 row) step c1: COMMIT; +starting permutation: update1_pa_move merge_bal_pa c2 c1 +step update1_pa_move: UPDATE target_pa t SET balance = 210, val = t.val || ' updated by update1_pa_move' WHERE t.key = 1; +step merge_bal_pa: + MERGE INTO target_pa t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3'; + +step c2: COMMIT; +step merge_bal_pa: <... completed> +ERROR: tuple to be locked was already moved to another partition due to concurrent update +step c1: COMMIT; + +starting permutation: update1_pa update1_pa_move merge_bal_pa c2 c1 +step update1_pa: UPDATE target_pa t SET balance = balance + 10, val = t.val || ' updated by update1_pa' WHERE t.key = 1; +step update1_pa_move: UPDATE target_pa t SET balance = 210, val = t.val || ' updated by update1_pa_move' WHERE t.key = 1; +step merge_bal_pa: + MERGE INTO target_pa t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3'; + +step c2: COMMIT; +step merge_bal_pa: <... completed> +ERROR: tuple to be locked was already moved to another partition due to concurrent update +step c1: COMMIT; + starting permutation: update1 merge_delete c2 select1 c1 step update1: UPDATE target t SET balance = balance + 10, val = t.val || ' updated by update1' WHERE t.key = 1; step merge_delete: diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out index 677263d1ec1b8..821565b43035f 100644 --- a/src/test/isolation/expected/merge-update.out +++ b/src/test/isolation/expected/merge-update.out @@ -57,6 +57,52 @@ key|val step c2: COMMIT; +starting permutation: merge1 c1 explain_merge2a select2 c2 +step merge1: + MERGE INTO target t + USING (SELECT 1 as key, 'merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c1: COMMIT; +step explain_merge2a: + SELECT explain_filter($$ + EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + MERGE INTO target t + USING (SELECT 1 as key, 'merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN + UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a' + RETURNING merge_action(), old, new, t.* + $$); + +explain_filter +----------------------------------------------------------- +Merge on target t (actual rows=2.00 loops=1) + Tuples: inserted=1 updated=1 + -> Hash Full Join (actual rows=2.00 loops=1) + Hash Cond: (t.key = (1)) + -> Seq Scan on target t (actual rows=1.00 loops=1) + -> Hash (actual rows=1.00 loops=1) + -> Result (actual rows=1.00 loops=1) +(7 rows) + +step select2: SELECT * FROM target; +key|val +---+------------------------------------------------------ + 3|setup1 updated by merge1 source not matched by merge2a + 1|merge2a +(2 rows) + +step c2: COMMIT; + starting permutation: pa_merge1 c1 pa_merge2c_dup a2 step pa_merge1: MERGE INTO pa_target t @@ -117,6 +163,53 @@ key|val step c2: COMMIT; +starting permutation: merge1 explain_merge2a c1 select2 c2 +step merge1: + MERGE INTO target t + USING (SELECT 1 as key, 'merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step explain_merge2a: + SELECT explain_filter($$ + EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + MERGE INTO target t + USING (SELECT 1 as key, 'merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN + UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a' + RETURNING merge_action(), old, new, t.* + $$); + +step c1: COMMIT; +step explain_merge2a: <... completed> +explain_filter +----------------------------------------------------------- +Merge on target t (actual rows=2.00 loops=1) + Tuples: inserted=1 updated=1 + -> Hash Full Join (actual rows=2.00 loops=1) + Hash Cond: (t.key = (1)) + -> Seq Scan on target t (actual rows=1.00 loops=1) + -> Hash (actual rows=1.00 loops=1) + -> Result (actual rows=1.00 loops=1) +(7 rows) + +step select2: SELECT * FROM target; +key|val +---+------------------------------------------------------ + 3|setup1 updated by merge1 source not matched by merge2a + 1|merge2a +(2 rows) + +step c2: COMMIT; + starting permutation: merge1 merge2a a1 select2 c2 step merge1: MERGE INTO target t @@ -253,6 +346,59 @@ key|val step c2: COMMIT; +starting permutation: pa_merge1 explain_pa_merge2a c1 pa_select2 c2 +step pa_merge1: + MERGE INTO pa_target t + USING (SELECT 1 as key, 'pa_merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set val = t.val || ' updated by ' || s.val; + +step explain_pa_merge2a: + SELECT explain_filter($$ + EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + MERGE INTO pa_target t + USING (SELECT 1 as key, 'pa_merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN + UPDATE set key = t.key + 1, val = t.val || ' source not matched by pa_merge2a' + RETURNING merge_action(), old, new, t.* + $$); + +step c1: COMMIT; +step explain_pa_merge2a: <... completed> +explain_filter +------------------------------------------------------------------ +Merge on pa_target t (actual rows=2.00 loops=1) + Merge on part1 t_1 + Merge on part2 t_2 + Merge on part3 t_3 + Tuples: updated=2 + -> Hash Full Join (actual rows=2.00 loops=1) + Hash Cond: (t.key = (1)) + -> Append (actual rows=2.00 loops=1) + -> Seq Scan on part1 t_1 (actual rows=1.00 loops=1) + -> Seq Scan on part2 t_2 (actual rows=1.00 loops=1) + -> Seq Scan on part3 t_3 (actual rows=0.00 loops=1) + -> Hash (actual rows=1.00 loops=1) + -> Result (actual rows=1.00 loops=1) +(13 rows) + +step pa_select2: SELECT * FROM pa_target; +key|val +---+-------------------------------------------------- + 2|initial updated by pa_merge1 updated by pa_merge2a + 3|initial source not matched by pa_merge2a +(2 rows) + +step c2: COMMIT; + starting permutation: pa_merge2 pa_merge2a c1 pa_select2 c2 step pa_merge2: MERGE INTO pa_target t @@ -403,3 +549,36 @@ step c1: COMMIT; step pa_merge2c_dup: <... completed> ERROR: MERGE command cannot affect row a second time step a2: ABORT; + +starting permutation: merge2a c1 s1beginrr merge1 c2 +step merge2a: + MERGE INTO target t + USING (SELECT 1 as key, 'merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN + UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a' + RETURNING merge_action(), old, new, t.*; + +merge_action|old |new |key|val +------------+----------+-------------------------------+---+------------------------- +UPDATE |(1,setup1)|(2,"setup1 updated by merge2a")| 2|setup1 updated by merge2a +(1 row) + +step c1: COMMIT; +step s1beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step merge1: + MERGE INTO target t + USING (SELECT 1 as key, 'merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c2: COMMIT; +step merge1: <... completed> +ERROR: could not serialize access due to concurrent update diff --git a/src/test/isolation/expected/multixact-stats.out b/src/test/isolation/expected/multixact-stats.out new file mode 100644 index 0000000000000..27a6510c4ad57 --- /dev/null +++ b/src/test/isolation/expected/multixact-stats.out @@ -0,0 +1,89 @@ +Parsed test spec with 2 sessions + +starting permutation: snap0 s1_begin s1_lock snap1 s2_begin s2_lock snap2 check_while_pinned s1_commit s2_commit +step snap0: + CREATE TEMP TABLE snap0 AS + SELECT num_mxids, num_members, oldest_multixact + FROM pg_get_multixact_stats(); + +step s1_begin: BEGIN; +step s1_lock: SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE; +?column? +-------- + 1 +(1 row) + +step snap1: + CREATE TEMP TABLE snap1 AS + SELECT num_mxids, num_members, oldest_multixact + FROM pg_get_multixact_stats(); + +step s2_begin: BEGIN; +step s2_lock: SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE; +?column? +-------- + 1 +(1 row) + +step snap2: + CREATE TEMP TABLE snap2 AS + SELECT num_mxids, num_members, oldest_multixact + FROM pg_get_multixact_stats(); + +step check_while_pinned: + SELECT r.assertion, r.ok + FROM snap0 s0 + JOIN snap1 s1 ON TRUE + JOIN snap2 s2 ON TRUE, + LATERAL unnest( + ARRAY[ + 'is_init_mxids', + 'is_init_members', + 'is_init_oldest_mxid', + 'is_init_oldest_off', + 'is_oldest_mxid_nondec_01', + 'is_oldest_mxid_nondec_12', + 'is_oldest_off_nondec_01', + 'is_oldest_off_nondec_12', + 'is_members_increased_ge1', + 'is_mxids_nondec_01', + 'is_mxids_nondec_12', + 'is_members_nondec_01', + 'is_members_nondec_12' + ], + ARRAY[ + (s2.num_mxids IS NOT NULL), + (s2.num_members IS NOT NULL), + (s2.oldest_multixact IS NOT NULL), + + (s1.oldest_multixact::text::bigint >= COALESCE(s0.oldest_multixact::text::bigint, 0)), + (s2.oldest_multixact::text::bigint >= COALESCE(s1.oldest_multixact::text::bigint, 0)), + + (s2.num_members >= COALESCE(s1.num_members, 0) + 1), + + (s1.num_mxids >= COALESCE(s0.num_mxids, 0)), + (s2.num_mxids >= COALESCE(s1.num_mxids, 0)), + (s1.num_members >= COALESCE(s0.num_members, 0)), + (s2.num_members >= COALESCE(s1.num_members, 0)) + ] + ) AS r(assertion, ok); + +assertion |ok +------------------------+-- +is_init_mxids |t +is_init_members |t +is_init_oldest_mxid |t +is_init_oldest_off |t +is_oldest_mxid_nondec_01|t +is_oldest_mxid_nondec_12|t +is_oldest_off_nondec_01 |t +is_oldest_off_nondec_12 |t +is_members_increased_ge1|t +is_mxids_nondec_01 |t +is_mxids_nondec_12 | +is_members_nondec_01 | +is_members_nondec_12 | +(13 rows) + +step s1_commit: COMMIT; +step s2_commit: COMMIT; diff --git a/src/test/isolation/expected/partition-merge.out b/src/test/isolation/expected/partition-merge.out new file mode 100644 index 0000000000000..5f6472671b9af --- /dev/null +++ b/src/test/isolation/expected/partition-merge.out @@ -0,0 +1,243 @@ +Parsed test spec with 2 sessions + +starting permutation: s2b s2i s2c s1b s1merg s2b s2u s1c s2c s2s +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1b: BEGIN; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2b s2i s2c s1brr s1merg s2b s2u s1c s2c s2s +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2b s2i s2c s1bs s1merg s2b s2u s1c s2c s2s +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2brr s2i s2c s1b s1merg s2b s2u s1c s2c s2s +step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1b: BEGIN; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2brr s2i s2c s1brr s1merg s2b s2u s1c s2c s2s +step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2brr s2i s2c s1bs s1merg s2b s2u s1c s2c s2s +step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2bs s2i s2c s1b s1merg s2b s2u s1c s2c s2s +step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1b: BEGIN; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2bs s2i s2c s1brr s1merg s2b s2u s1c s2c s2s +step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2bs s2i s2c s1bs s1merg s2b s2u s1c s2c s2s +step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u: UPDATE tpart SET t = 'text01modif' where i = 1; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+----------- +tpart_00_20 | 1|text01modif +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2b s2i s2c s1b s1merg s2b s2u2 s1c s2c s2s +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1b: BEGIN; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u2: UPDATE tpart SET i = 21 where i = 1; +step s1c: COMMIT; +step s2u2: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_20 | 5|text05 +tpart_00_20 |15|text15 +tpart_20_30 |21|text01 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s2b s2i s2c s1b s1merg s2b s2u3 s1c s2c s2s +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s2c: COMMIT; +step s1b: BEGIN; +step s1merg: ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; +step s2b: BEGIN; +step s2u3: UPDATE tpart SET i = 11 where i = 1; +step s1c: COMMIT; +step s2u3: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_20 | 5|text05 +tpart_00_20 |11|text01 +tpart_00_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + diff --git a/src/test/isolation/expected/partition-split.out b/src/test/isolation/expected/partition-split.out new file mode 100644 index 0000000000000..02a5bb4f1f5b2 --- /dev/null +++ b/src/test/isolation/expected/partition-split.out @@ -0,0 +1,230 @@ +Parsed test spec with 2 sessions + +starting permutation: s1b s1splt s2b s2i s1c s2c s2s +step s1b: BEGIN; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1b s1splt s2brr s2i s1c s2c s2s +step s1b: BEGIN; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1b s1splt s2bs s2i s1c s2c s2s +step s1b: BEGIN; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1brr s1splt s2b s2i s1c s2c s2s +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1brr s1splt s2brr s2i s1c s2c s2s +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1brr s1splt s2bs s2i s1c s2c s2s +step s1brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1bs s1splt s2b s2i s1c s2c s2s +step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2b: BEGIN; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1bs s1splt s2brr s2i s1c s2c s2s +step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2brr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1bs s1splt s2bs s2i s1c s2c s2s +step s1bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2bs: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s2i: INSERT INTO tpart VALUES (1, 'text01'); +step s1c: COMMIT; +step s2i: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 1|text01 +tpart_00_10 | 5|text05 +tpart_15_20 |15|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(5 rows) + + +starting permutation: s1b s1splt s2b s2u s1c s2c s2s +step s1b: BEGIN; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2b: BEGIN; +step s2u: UPDATE tpart SET i = 16 where i = 5; +step s1c: COMMIT; +step s2u: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_15_20 |15|text15 +tpart_15_20 |16|text05 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(4 rows) + + +starting permutation: s1b s1splt s2b s2u2 s1c s2c s2s +step s1b: BEGIN; +step s1splt: ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); +step s2b: BEGIN; +step s2u2: UPDATE tpart SET i = 11 where i = 15; +step s1c: COMMIT; +step s2u2: <... completed> +step s2c: COMMIT; +step s2s: SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; +tableoid | i|t +-------------+--+------ +tpart_00_10 | 5|text05 +tpart_10_15 |11|text15 +tpart_20_30 |25|text25 +tpart_default|35|text35 +(4 rows) + diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out index 8c7fe60217e98..cfad309ccf34a 100644 --- a/src/test/isolation/expected/stats.out +++ b/src/test/isolation/expected/stats.out @@ -1025,7 +1025,7 @@ test_stat_func| | | (1 row) -starting permutation: s1_track_funcs_all s2_track_funcs_all s1_func_call s2_func_call s2_func_call2 s1_ff s2_ff s1_func_stats s2_func_call s2_func_call2 s2_ff s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset s1_func_stats s1_func_stats2 s1_func_stats +starting permutation: s1_track_funcs_all s2_track_funcs_all s1_func_call s2_func_call s2_func_call2 s1_ff s2_ff s1_func_stats s2_func_call s2_func_call2 s2_ff s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset_check s1_func_stats_reset s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset_check pg_stat_force_next_flush ------------------------ @@ -1137,6 +1137,15 @@ name |pg_stat_get_function_calls|total_above_zero|self_above_zero test_stat_func| 3|t |t (1 row) +step s1_func_stats_reset_check: + SELECT pg_stat_get_function_stat_reset_time('test_stat_func'::regproc) + IS NOT NULL AS has_stats_reset; + +has_stats_reset +--------------- +f +(1 row) + step s1_func_stats_reset: SELECT pg_stat_reset_single_function_counters('test_stat_func'::regproc); pg_stat_reset_single_function_counters -------------------------------------- @@ -1185,6 +1194,15 @@ name |pg_stat_get_function_calls|total_above_zero|self_above_zero test_stat_func| 0|f |f (1 row) +step s1_func_stats_reset_check: + SELECT pg_stat_get_function_stat_reset_time('test_stat_func'::regproc) + IS NOT NULL AS has_stats_reset; + +has_stats_reset +--------------- +t +(1 row) + starting permutation: s1_func_stats_nonexistent s1_func_stats_reset_nonexistent s1_func_stats_nonexistent pg_stat_force_next_flush diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out index 6b965bb95534a..e1d937784cb1e 100644 --- a/src/test/isolation/expected/stats_1.out +++ b/src/test/isolation/expected/stats_1.out @@ -1025,7 +1025,7 @@ test_stat_func| | | (1 row) -starting permutation: s1_track_funcs_all s2_track_funcs_all s1_func_call s2_func_call s2_func_call2 s1_ff s2_ff s1_func_stats s2_func_call s2_func_call2 s2_ff s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset s1_func_stats s1_func_stats2 s1_func_stats +starting permutation: s1_track_funcs_all s2_track_funcs_all s1_func_call s2_func_call s2_func_call2 s1_ff s2_ff s1_func_stats s2_func_call s2_func_call2 s2_ff s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset_check s1_func_stats_reset s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset_check pg_stat_force_next_flush ------------------------ @@ -1137,6 +1137,15 @@ name |pg_stat_get_function_calls|total_above_zero|self_above_zero test_stat_func| 3|t |t (1 row) +step s1_func_stats_reset_check: + SELECT pg_stat_get_function_stat_reset_time('test_stat_func'::regproc) + IS NOT NULL AS has_stats_reset; + +has_stats_reset +--------------- +f +(1 row) + step s1_func_stats_reset: SELECT pg_stat_reset_single_function_counters('test_stat_func'::regproc); pg_stat_reset_single_function_counters -------------------------------------- @@ -1185,6 +1194,15 @@ name |pg_stat_get_function_calls|total_above_zero|self_above_zero test_stat_func| 0|f |f (1 row) +step s1_func_stats_reset_check: + SELECT pg_stat_get_function_stat_reset_time('test_stat_func'::regproc) + IS NOT NULL AS has_stats_reset; + +has_stats_reset +--------------- +t +(1 row) + starting permutation: s1_func_stats_nonexistent s1_func_stats_reset_nonexistent s1_func_stats_nonexistent pg_stat_force_next_flush diff --git a/src/test/isolation/expected/temp-schema-cleanup.out b/src/test/isolation/expected/temp-schema-cleanup.out index d10aee53a8041..a16d30ffefaf7 100644 --- a/src/test/isolation/expected/temp-schema-cleanup.out +++ b/src/test/isolation/expected/temp-schema-cleanup.out @@ -25,6 +25,8 @@ exec (1 row) +s1: NOTICE: function "uses_a_temp_type" will be effectively temporary +DETAIL: It depends on temporary type just_give_me_a_type. step s1_discard_temp: DISCARD TEMP; @@ -82,6 +84,8 @@ exec (1 row) +s1: NOTICE: function "uses_a_temp_type" will be effectively temporary +DETAIL: It depends on temporary type just_give_me_a_type. step s1_exit: SELECT pg_terminate_backend(pg_backend_pid()); diff --git a/src/test/isolation/isolation_main.c b/src/test/isolation/isolation_main.c index a13f9cdcaf773..423b018afbe52 100644 --- a/src/test/isolation/isolation_main.c +++ b/src/test/isolation/isolation_main.c @@ -2,7 +2,7 @@ * * isolation_main --- pg_regress test launcher for isolation tests * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/isolation/isolation_main.c diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index e3c669a29c7aa..1578ba191c801 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -35,6 +35,9 @@ test: fk-deadlock2 test: fk-partitioned-1 test: fk-partitioned-2 test: fk-snapshot +test: fk-snapshot-2 +test: fk-snapshot-3 +test: fk-concurrent-pk-upd test: subxid-overflow test: eval-plan-qual test: eval-plan-qual-trigger @@ -50,7 +53,9 @@ test: insert-conflict-do-nothing-2 test: insert-conflict-do-update test: insert-conflict-do-update-2 test: insert-conflict-do-update-3 +test: insert-conflict-do-update-4 test: insert-conflict-specconflict +test: insert-conflict-do-select test: merge-insert-update test: merge-delete test: merge-update @@ -61,6 +66,7 @@ test: delete-abort-savept-2 test: aborted-keyrevoke test: multixact-no-deadlock test: multixact-no-forget +test: multixact-stats test: lock-committed-update test: lock-committed-keyupdate test: update-locked-tuple @@ -107,12 +113,16 @@ test: partition-key-update-1 test: partition-key-update-2 test: partition-key-update-3 test: partition-key-update-4 +test: partition-merge +test: partition-split test: plpgsql-toast test: cluster-conflict test: cluster-conflict-partition +test: cluster-toast-value-reuse test: truncate-conflict test: serializable-parallel test: serializable-parallel-2 test: serializable-parallel-3 test: matview-write-skew test: lock-nowait +test: for-portion-of diff --git a/src/test/isolation/isolationtester.c b/src/test/isolation/isolationtester.c index e01c0c9de936e..440c875b8ac6b 100644 --- a/src/test/isolation/isolationtester.c +++ b/src/test/isolation/isolationtester.c @@ -147,7 +147,7 @@ main(int argc, char **argv) * extra for lock wait detection and global work. */ nconns = 1 + testspec->nsessions; - conns = (IsoConnInfo *) pg_malloc0(nconns * sizeof(IsoConnInfo)); + conns = pg_malloc0_array(IsoConnInfo, nconns); atexit(disconnect_atexit); for (i = 0; i < nconns; i++) @@ -262,7 +262,7 @@ check_testspec(TestSpec *testspec) for (i = 0; i < testspec->nsessions; i++) nallsteps += testspec->sessions[i]->nsteps; - allsteps = pg_malloc(nallsteps * sizeof(Step *)); + allsteps = pg_malloc_array(Step *, nallsteps); k = 0; for (i = 0; i < testspec->nsessions; i++) @@ -417,8 +417,8 @@ run_all_permutations(TestSpec *testspec) nsteps += testspec->sessions[i]->nsteps; /* Create PermutationStep workspace array */ - steps = (PermutationStep *) pg_malloc0(sizeof(PermutationStep) * nsteps); - stepptrs = (PermutationStep **) pg_malloc(sizeof(PermutationStep *) * nsteps); + steps = pg_malloc0_array(PermutationStep, nsteps); + stepptrs = pg_malloc_array(PermutationStep *, nsteps); for (i = 0; i < nsteps; i++) stepptrs[i] = steps + i; @@ -431,7 +431,7 @@ run_all_permutations(TestSpec *testspec) * A pile is actually just an integer which tells how many steps we've * already picked from this pile. */ - piles = pg_malloc(sizeof(int) * testspec->nsessions); + piles = pg_malloc_array(int, testspec->nsessions); for (i = 0; i < testspec->nsessions; i++) piles[i] = 0; @@ -498,8 +498,8 @@ run_named_permutations(TestSpec *testspec) static int step_qsort_cmp(const void *a, const void *b) { - Step *stepa = *((Step **) a); - Step *stepb = *((Step **) b); + Step *stepa = *((Step *const *) a); + Step *stepb = *((Step *const *) b); return strcmp(stepa->name, stepb->name); } @@ -507,8 +507,8 @@ step_qsort_cmp(const void *a, const void *b) static int step_bsearch_cmp(const void *a, const void *b) { - char *stepname = (char *) a; - Step *step = *((Step **) b); + const char *stepname = (const char *) a; + Step *step = *((Step *const *) b); return strcmp(stepname, step->name); } @@ -524,7 +524,7 @@ run_permutation(TestSpec *testspec, int nsteps, PermutationStep **steps) int nwaiting = 0; PermutationStep **waiting; - waiting = pg_malloc(sizeof(PermutationStep *) * testspec->nsessions); + waiting = pg_malloc_array(PermutationStep *, testspec->nsessions); printf("\nstarting permutation:"); for (i = 0; i < nsteps; i++) diff --git a/src/test/isolation/isolationtester.h b/src/test/isolation/isolationtester.h index 1ef14dc990970..a9328a94516ef 100644 --- a/src/test/isolation/isolationtester.h +++ b/src/test/isolation/isolationtester.h @@ -3,7 +3,7 @@ * isolationtester.h * include file for isolation tests * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/test/isolation/meson.build b/src/test/isolation/meson.build index a180e4e2741ad..c55b8d71848c2 100644 --- a/src/test/isolation/meson.build +++ b/src/test/isolation/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # pg_regress_c helpfully provided by regress/meson.build diff --git a/src/test/isolation/specparse.y b/src/test/isolation/specparse.y index 98949a446e5c6..f6b9058e55714 100644 --- a/src/test/isolation/specparse.y +++ b/src/test/isolation/specparse.y @@ -4,7 +4,7 @@ * specparse.y * bison grammar for the isolation test file format * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- @@ -83,8 +83,8 @@ setup_list: } | setup_list setup { - $$.elements = pg_realloc($1.elements, - ($1.nelements + 1) * sizeof(void *)); + $$.elements = pg_realloc_array($1.elements, void *, + $1.nelements + 1); $$.elements[$1.nelements] = $2; $$.nelements = $1.nelements + 1; } @@ -107,15 +107,15 @@ opt_teardown: session_list: session_list session { - $$.elements = pg_realloc($1.elements, - ($1.nelements + 1) * sizeof(void *)); + $$.elements = pg_realloc_array($1.elements, void *, + $1.nelements + 1); $$.elements[$1.nelements] = $2; $$.nelements = $1.nelements + 1; } | session { $$.nelements = 1; - $$.elements = pg_malloc(sizeof(void *)); + $$.elements = pg_malloc_object(void *); $$.elements[0] = $1; } ; @@ -123,7 +123,7 @@ session_list: session: SESSION identifier opt_setup step_list opt_teardown { - $$ = pg_malloc(sizeof(Session)); + $$ = pg_malloc_object(Session); $$->name = $2; $$->setupsql = $3; $$->steps = (Step **) $4.elements; @@ -135,15 +135,15 @@ session: step_list: step_list step { - $$.elements = pg_realloc($1.elements, - ($1.nelements + 1) * sizeof(void *)); + $$.elements = pg_realloc_array($1.elements, void *, + $1.nelements + 1); $$.elements[$1.nelements] = $2; $$.nelements = $1.nelements + 1; } | step { $$.nelements = 1; - $$.elements = pg_malloc(sizeof(void *)); + $$.elements = pg_malloc_object(void *); $$.elements[0] = $1; } ; @@ -152,7 +152,7 @@ step_list: step: STEP identifier sqlblock { - $$ = pg_malloc(sizeof(Step)); + $$ = pg_malloc_object(Step); $$->name = $2; $$->sql = $3; $$->session = -1; /* until filled */ @@ -175,15 +175,15 @@ opt_permutation_list: permutation_list: permutation_list permutation { - $$.elements = pg_realloc($1.elements, - ($1.nelements + 1) * sizeof(void *)); + $$.elements = pg_realloc_array($1.elements, void *, + $1.nelements + 1); $$.elements[$1.nelements] = $2; $$.nelements = $1.nelements + 1; } | permutation { $$.nelements = 1; - $$.elements = pg_malloc(sizeof(void *)); + $$.elements = pg_malloc_object(void *); $$.elements[0] = $1; } ; @@ -192,7 +192,7 @@ permutation_list: permutation: PERMUTATION permutation_step_list { - $$ = pg_malloc(sizeof(Permutation)); + $$ = pg_malloc_object(Permutation); $$->nsteps = $2.nelements; $$->steps = (PermutationStep **) $2.elements; } @@ -201,15 +201,15 @@ permutation: permutation_step_list: permutation_step_list permutation_step { - $$.elements = pg_realloc($1.elements, - ($1.nelements + 1) * sizeof(void *)); + $$.elements = pg_realloc_array($1.elements, void *, + $1.nelements + 1); $$.elements[$1.nelements] = $2; $$.nelements = $1.nelements + 1; } | permutation_step { $$.nelements = 1; - $$.elements = pg_malloc(sizeof(void *)); + $$.elements = pg_malloc_object(void *); $$.elements[0] = $1; } ; @@ -217,7 +217,7 @@ permutation_step_list: permutation_step: identifier { - $$ = pg_malloc(sizeof(PermutationStep)); + $$ = pg_malloc_object(PermutationStep); $$->name = $1; $$->blockers = NULL; $$->nblockers = 0; @@ -225,7 +225,7 @@ permutation_step: } | identifier '(' blocker_list ')' { - $$ = pg_malloc(sizeof(PermutationStep)); + $$ = pg_malloc_object(PermutationStep); $$->name = $1; $$->blockers = (PermutationStepBlocker **) $3.elements; $$->nblockers = $3.nelements; @@ -236,15 +236,15 @@ permutation_step: blocker_list: blocker_list ',' blocker { - $$.elements = pg_realloc($1.elements, - ($1.nelements + 1) * sizeof(void *)); + $$.elements = pg_realloc_array($1.elements, void *, + $1.nelements + 1); $$.elements[$1.nelements] = $3; $$.nelements = $1.nelements + 1; } | blocker { $$.nelements = 1; - $$.elements = pg_malloc(sizeof(void *)); + $$.elements = pg_malloc_object(void *); $$.elements[0] = $1; } ; @@ -252,7 +252,7 @@ blocker_list: blocker: identifier { - $$ = pg_malloc(sizeof(PermutationStepBlocker)); + $$ = pg_malloc_object(PermutationStepBlocker); $$->stepname = $1; $$->blocktype = PSB_OTHER_STEP; $$->num_notices = -1; @@ -261,7 +261,7 @@ blocker: } | identifier NOTICES INTEGER { - $$ = pg_malloc(sizeof(PermutationStepBlocker)); + $$ = pg_malloc_object(PermutationStepBlocker); $$->stepname = $1; $$->blocktype = PSB_NUM_NOTICES; $$->num_notices = $3; @@ -270,7 +270,7 @@ blocker: } | '*' { - $$ = pg_malloc(sizeof(PermutationStepBlocker)); + $$ = pg_malloc_object(PermutationStepBlocker); $$->stepname = NULL; $$->blocktype = PSB_ONCE; $$->num_notices = -1; diff --git a/src/test/isolation/specs/async-notify.spec b/src/test/isolation/specs/async-notify.spec index 0b8cfd9108374..d09c2297f0953 100644 --- a/src/test/isolation/specs/async-notify.spec +++ b/src/test/isolation/specs/async-notify.spec @@ -31,6 +31,20 @@ step notifys1 { ROLLBACK TO SAVEPOINT s2; COMMIT; } +step notifys_simple { + BEGIN; + SAVEPOINT s1; + NOTIFY c1, 'simple1'; + NOTIFY c2, 'simple2'; + RELEASE SAVEPOINT s1; + COMMIT; +} +step notify_many_with_dup { + BEGIN; + SELECT pg_notify('c1', 'msg' || s::text) FROM generate_series(1, 17) s; + SELECT pg_notify('c1', 'msg1'); + COMMIT; +} step usage { SELECT pg_notification_queue_usage() > 0 AS nonzero; } step bignotify { SELECT count(pg_notify('c1', s::text)) FROM generate_series(1, 1000) s; } teardown { UNLISTEN *; } @@ -43,6 +57,7 @@ step lcheck { SELECT 1 AS x; } step lbegin { BEGIN; } step lbegins { BEGIN ISOLATION LEVEL SERIALIZABLE; } step lcommit { COMMIT; } +step lunlisten_all { BEGIN; LISTEN c1; UNLISTEN *; COMMIT; } teardown { UNLISTEN *; } # In some tests we need a second listener, just to block the queue. @@ -53,6 +68,38 @@ step l2begin { BEGIN; } step l2commit { COMMIT; } step l2stop { UNLISTEN *; } +# Third listener session for testing array growth. + +session listener3 +step l3listen { LISTEN c1; } +teardown { UNLISTEN *; } + +# Listener session for cross-session notification test with channel 'ch'. + +session listener_ch +step lch_listen { LISTEN ch; } +step lch_check { SELECT 1 AS x; } +teardown { UNLISTEN *; } + +# Notifier session for cross-session notification test with channel 'ch'. + +session notifier_ch +step nch_notify { NOTIFY ch, 'aa'; } + +# Session for testing LISTEN in subtransaction with separate steps. + +session listen_subxact +step lsbegin { BEGIN; } +step lslisten_outer { LISTEN c3; } +step lssavepoint { SAVEPOINT s1; } +step lslisten { LISTEN c1; LISTEN c2; } +step lsrelease { RELEASE SAVEPOINT s1; } +step lsrollback { ROLLBACK TO SAVEPOINT s1; } +step lscommit { COMMIT; } +step lsnotify { NOTIFY c1, 'subxact_test'; } +step lsnotify_check { NOTIFY c1, 'should_not_receive'; } +teardown { UNLISTEN *; } + # Trivial cases. permutation listenc notify1 notify2 notify3 notifyf @@ -60,6 +107,27 @@ permutation listenc notify1 notify2 notify3 notifyf # Check simple and less-simple deduplication. permutation listenc notifyd1 notifyd2 notifys1 +# Check simple NOTIFY reparenting when parent has no action. +permutation listenc notifys_simple + +# Check LISTEN reparenting in subtransaction. +permutation lsbegin lssavepoint lslisten lsrelease lscommit lsnotify + +# Check LISTEN merge path when both outer and inner transactions have actions. +permutation lsbegin lslisten_outer lssavepoint lslisten lsrelease lscommit lsnotify + +# Check LISTEN abort path (ROLLBACK TO SAVEPOINT discards pending actions). +permutation lsbegin lssavepoint lslisten lsrollback lscommit lsnotify_check + +# Check UNLISTEN * cancels a LISTEN in the same transaction. +permutation lunlisten_all notify1 lcheck + +# Check notification_match function (triggered by hash table duplicate detection). +permutation listenc notify_many_with_dup + +# Check ChannelHashAddListener array growth. +permutation listenc llisten l2listen l3listen lslisten + # Cross-backend notification delivery. We use a "select 1" to force the # listener session to check for notifies. In principle we could just wait # for delivery, but that would require extra support in isolationtester @@ -73,6 +141,10 @@ permutation listenc llisten notify1 notify2 notify3 notifyf lcheck # and notify queue is not empty permutation l2listen l2begin notify1 lbegins llisten lcommit l2commit l2stop +# Check that notifications sent from a backend that has not done LISTEN +# are properly delivered to a listener in another backend. +permutation lch_listen nch_notify lch_check + # Verify that pg_notification_queue_usage correctly reports a non-zero result, # after submitting notifications while another connection is listening for # those notifications and waiting inside an active transaction. We have to diff --git a/src/test/isolation/specs/cluster-toast-value-reuse.spec b/src/test/isolation/specs/cluster-toast-value-reuse.spec new file mode 100644 index 0000000000000..9a2d10600b39f --- /dev/null +++ b/src/test/isolation/specs/cluster-toast-value-reuse.spec @@ -0,0 +1,69 @@ +# Tests with CLUSTER for toast values + +# This test does a relation rewrite, with toast values reused to make the +# rewrite cheaper (see data_todo = 0 case in toast_save_datum()). +# +# A first session updates the table with an attribute not toasted. CLUSTER +# is then executed in a second session. The comparison of the values +# allocated for the toasted values are done using a CTAS. The allocated +# chunk_ids are saved before the rewrite, and compared after the rewrite. + +# ---------- global setup ---------- +setup +{ + DROP TABLE IF EXISTS cluster_toast_value CASCADE; + DROP TABLE IF EXISTS cluster_chunk_id CASCADE; + + CREATE TABLE cluster_toast_value ( + id serial PRIMARY KEY, + flag integer, + value text); + + -- Make sure 'value' is large enough to be toasted. + ALTER TABLE cluster_toast_value ALTER COLUMN value SET STORAGE EXTERNAL; + + -- Clustering index. + CLUSTER cluster_toast_value_pkey ON cluster_toast_value; + + -- Seed data: one row with big string to force TOAST tuple and trigger the todo=0 code path. + INSERT INTO cluster_toast_value(flag, value) + VALUES (0, repeat(encode(sha256('1'), 'hex'), 120) || repeat('x', 8000)); + + CLUSTER cluster_toast_value; +} + +teardown +{ + DROP TABLE IF EXISTS cluster_toast_value; + DROP TABLE IF EXISTS cluster_chunk_id; +} + +session s1 +step s1_begin { BEGIN; } +step s1_update { UPDATE cluster_toast_value SET flag = 1 WHERE TRUE; } +step s1_commit { COMMIT; } + +session s2 +# Store the primary key values and their associated chunk IDs. This makes +# sure that some data is captured. +step s2_store_chunk_ids { + CREATE TABLE cluster_chunk_id AS + SELECT c.id, pg_column_toast_chunk_id(c.value) AS chunk_id + FROM cluster_toast_value c; + SELECT count(*) FROM cluster_chunk_id; +} +step s2_cluster { CLUSTER cluster_toast_value; } + +# Verify that toast values allocated are the same, indicating reuse. +# This query reports the tuples with toast values that do not match. +step s2_verify_chunk_ids { + SELECT o.id AS chunk_ids_preserved + FROM cluster_chunk_id o + JOIN cluster_toast_value c ON o.id = c.id + WHERE o.chunk_id != pg_column_toast_chunk_id(c.value); +} + +# Run UPDATE with its transaction still open, then store the chunk IDs. +# CLUSTER will wait until the first transaction commit. Finally, the chunk +# IDs are compared. +permutation s1_begin s1_update s2_store_chunk_ids s2_cluster s1_commit s2_verify_chunk_ids diff --git a/src/test/isolation/specs/eval-plan-qual-trigger.spec b/src/test/isolation/specs/eval-plan-qual-trigger.spec index b512edd28798a..232b3e27652a1 100644 --- a/src/test/isolation/specs/eval-plan-qual-trigger.spec +++ b/src/test/isolation/specs/eval-plan-qual-trigger.spec @@ -336,7 +336,7 @@ permutation s1_trig_rep_b_u s1_trig_rep_a_u s1_ins_a s1_ins_c s1_b_rc s2_b_rc s1_upd_a_tob s2_upd_all_data s1_c s2_c s0_rep -# s1 deletes, s2 updates, s1 committs, EPQ failure should lead to no update +# s1 deletes, s2 updates, s1 commits, EPQ failure should lead to no update permutation s1_trig_rep_b_d s1_trig_rep_b_u s1_trig_rep_a_d s1_trig_rep_a_u s1_ins_a s1_ins_c s1_b_rc s2_b_rc s1_del_a s2_upd_a_data s1_c s2_c @@ -346,7 +346,7 @@ permutation s1_trig_rep_b_d s1_trig_rep_b_u s1_trig_rep_a_d s1_trig_rep_a_u s1_ins_a s1_ins_c s1_b_rc s2_b_rc s1_del_a s2_upd_a_data s1_r s2_c s0_rep -# s1 deletes, s2 deletes, s1 committs, EPQ failure should lead to no delete +# s1 deletes, s2 deletes, s1 commits, EPQ failure should lead to no delete permutation s1_trig_rep_b_d s1_trig_rep_a_d s1_ins_a s1_ins_c s1_b_rc s2_b_rc s1_del_a s2_del_a s1_c s2_c diff --git a/src/test/isolation/specs/eval-plan-qual.spec b/src/test/isolation/specs/eval-plan-qual.spec index 07307e623e473..fb57fb237ddfd 100644 --- a/src/test/isolation/specs/eval-plan-qual.spec +++ b/src/test/isolation/specs/eval-plan-qual.spec @@ -99,6 +99,10 @@ step upsert1 { WHERE NOT EXISTS (SELECT 1 FROM upsert); } +# Tests for Tid / Tid Range Scan +step tid1 { UPDATE accounts SET balance = balance + 100 WHERE ctid = '(0,1)' RETURNING accountid, balance; } +step tidrange1 { UPDATE accounts SET balance = balance + 100 WHERE ctid BETWEEN '(0,1)' AND '(0,1)' RETURNING accountid, balance; } + # tests with table p check inheritance cases: # readp1/writep1/readp2 tests a bug where nodeLockRows did the wrong thing # when the first updated tuple was in a non-first child table. @@ -200,6 +204,9 @@ step sys1 { UPDATE pg_class SET reltuples = 123 WHERE oid = 'accounts'::regclass; } +step s1pp1 { UPDATE another_parttbl SET b = b + 1 WHERE a = 1; } + +step updateformergevalues { UPDATE accounts SET balance = balance + 100; } session s2 setup { BEGIN ISOLATION LEVEL READ COMMITTED; } @@ -241,6 +248,11 @@ step updateforcip3 { step wrtwcte { UPDATE table_a SET value = 'tableAValue2' WHERE id = 1; } step wrjt { UPDATE jointest SET data = 42 WHERE id = 7; } +step tid2 { UPDATE accounts SET balance = balance + 200 WHERE ctid = '(0,1)' RETURNING accountid, balance; } +step tidrange2 { UPDATE accounts SET balance = balance + 200 WHERE ctid BETWEEN '(0,1)' AND '(0,1)' RETURNING accountid, balance; } +# here, recheck succeeds; (0,3) is the id that step tid1 will assign +step tidsucceed2 { UPDATE accounts SET balance = balance + 200 WHERE ctid = '(0,1)' OR ctid = '(0,3)' RETURNING accountid, balance; } + step conditionalpartupdate { update parttbl set c = -c where b < 10; } @@ -303,6 +315,19 @@ step sysmerge2 { step c2 { COMMIT; } step r2 { ROLLBACK; } +step s2pp1 { SET plan_cache_mode TO force_generic_plan; } +step s2pp2 { PREPARE epd AS DELETE FROM another_parttbl WHERE a = $1; } +step s2pp3 { EXECUTE epd(1); } +step s2pp4 { DELETE FROM another_parttbl WHERE a = (SELECT 1); } + +step mergevalues { + MERGE INTO accounts + USING (VALUES ('checking', 610), ('savings', 620)) v(accountid, balance) + ON v.accountid = accounts.accountid + WHEN MATCHED THEN UPDATE SET balance = v.balance + WHEN NOT MATCHED THEN INSERT VALUES ('unmatched', -1); +} + session s3 setup { BEGIN ISOLATION LEVEL READ COMMITTED; } step read { SELECT * FROM accounts ORDER BY accountid; } @@ -392,6 +417,11 @@ permutation wrtwcte readwcte c1 c2 permutation wrjt selectjoinforupdate c2 c1 permutation wrjt selectresultforupdate c2 c1 permutation wrtwcte multireadwcte c1 c2 +permutation tid1 tid2 c1 c2 read +permutation tid1 tidsucceed2 c1 c2 read +permutation tidrange1 tidrange2 c1 c2 read +# test that a rollback on s1 has s2 perform the update on the original row +permutation tid1 tid2 r1 c2 read permutation simplepartupdate conditionalpartupdate c1 c2 read_part permutation simplepartupdate complexpartupdate c1 c2 read_part @@ -401,3 +431,10 @@ permutation simplepartupdate_noroute complexpartupdate_doesnt_route c1 c2 read_p permutation sys1 sysupd2 c1 c2 permutation sys1 sysmerge2 c1 c2 + +# Exercise run-time partition pruning code in an EPQ recheck +permutation s1pp1 s2pp1 s2pp2 s2pp3 c1 c2 +permutation s1pp1 s2pp4 c1 c2 + +# test EPQ recheck in MERGE from VALUES_RTE, cf bug #19355 +permutation updateformergevalues mergevalues c1 c2 read diff --git a/src/test/isolation/specs/fk-concurrent-pk-upd.spec b/src/test/isolation/specs/fk-concurrent-pk-upd.spec new file mode 100644 index 0000000000000..03dc7f260cd69 --- /dev/null +++ b/src/test/isolation/specs/fk-concurrent-pk-upd.spec @@ -0,0 +1,53 @@ +# Tests that an INSERT on referencing table correctly fails when +# the referenced value disappears due to a concurrent update +setup +{ + CREATE TABLE parent ( + parent_key int PRIMARY KEY, + aux text NOT NULL + ); + + CREATE TABLE child ( + child_key int PRIMARY KEY, + parent_key int8 NOT NULL REFERENCES parent + ); + + INSERT INTO parent VALUES (1, 'foo'); +} + +teardown +{ + DROP TABLE parent, child; +} + +session s1 +step s1b { BEGIN; } +step s1i { INSERT INTO child VALUES (1, 1); } +step s1c { COMMIT; } +step s1s { SELECT * FROM child; } + +session s2 +step s2b { BEGIN; } +step s2ukey { UPDATE parent SET parent_key = 2 WHERE parent_key = 1; } +step s2uaux { UPDATE parent SET aux = 'bar' WHERE parent_key = 1; } +step s2ukey2 { UPDATE parent SET parent_key = 1 WHERE parent_key = 2; } +step s2c { COMMIT; } +step s2s { SELECT * FROM parent; } + +session s3 +step s3b { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s3i { INSERT INTO child VALUES (2, 1); } +step s3c { COMMIT; } +step s3s { SELECT * FROM child; } + +# fail +permutation s2b s2ukey s1b s1i s2c s1c s2s s1s +# ok +permutation s2b s2uaux s1b s1i s2c s1c s2s s1s +# ok +permutation s2b s2ukey s1b s1i s2ukey2 s2c s1c s2s s1s + +# RR: key update -> serialization failure +permutation s2b s2ukey s3b s3i s2c s3c s2s s3s +# RR: non-key update -> old version visible via transaction snapshot +permutation s2b s2uaux s3b s3i s2c s3c s2s s3s diff --git a/src/test/isolation/specs/fk-snapshot-2.spec b/src/test/isolation/specs/fk-snapshot-2.spec new file mode 100644 index 0000000000000..94cd151aab9d3 --- /dev/null +++ b/src/test/isolation/specs/fk-snapshot-2.spec @@ -0,0 +1,50 @@ +# RI Trigger test +# +# Test C-based referential integrity enforcement. +# Under REPEATABLE READ we need some snapshot trickery in C, +# or we would permit things that violate referential integrity. + +setup +{ + CREATE TABLE parent (parent_id SERIAL NOT NULL PRIMARY KEY); + CREATE TABLE child ( + child_id SERIAL NOT NULL PRIMARY KEY, + parent_id INTEGER REFERENCES parent); + INSERT INTO parent VALUES(1); +} + +teardown { DROP TABLE parent, child; } + +session s1 +step s1rc { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s1rr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s1ser { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s1del { DELETE FROM parent WHERE parent_id = 1; } +step s1c { COMMIT; } + +session s2 +step s2rc { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s2rr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s2ser { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s2ins { INSERT INTO child VALUES (1, 1); } +step s2c { COMMIT; } + +# Violates referential integrity unless we use a crosscheck snapshot, +# which is up-to-date compared with the transaction's snapshot. +permutation s1rr s2rr s2ins s1del s2c s1c + +# Raises a can't-serialize exception +# when the INSERT trigger does SELECT FOR KEY SHARE: +permutation s1rr s2rr s1del s2ins s1c s2c + +# Test the same scenarios in READ COMMITTED: +# A crosscheck snapshot is not required here. +permutation s1rc s2rc s2ins s1del s2c s1c +permutation s1rc s2rc s1del s2ins s1c s2c + +# Test the same scenarios in SERIALIZABLE: +# We should report the FK violation: +permutation s1ser s2ser s2ins s1del s2c s1c +# We raise a concurrent update error +# which is good enough: +permutation s1ser s2ser s1del s2ins s1c s2c diff --git a/src/test/isolation/specs/fk-snapshot-3.spec b/src/test/isolation/specs/fk-snapshot-3.spec new file mode 100644 index 0000000000000..d8237ca2e7afa --- /dev/null +++ b/src/test/isolation/specs/fk-snapshot-3.spec @@ -0,0 +1,82 @@ +# RI Trigger test +# +# Test C-based temporal referential integrity enforcement. +# Under REPEATABLE READ we need some snapshot trickery in C, +# or we would permit things that violate referential integrity. + +setup +{ + CREATE TABLE parent ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)); + CREATE TABLE child ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + parent_id int4range, + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES parent); + INSERT INTO parent VALUES ('[1,2)', '[2020-01-01,2030-01-01)'); +} + +teardown { DROP TABLE parent, child; } + +session s1 +step s1rc { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s1rr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s1ser { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s1del { DELETE FROM parent WHERE id = '[1,2)'; } +step s1upok { UPDATE parent SET valid_at = '[2020-01-01,2026-01-01)' WHERE id = '[1,2)'; } +step s1upbad { UPDATE parent SET valid_at = '[2020-01-01,2024-01-01)' WHERE id = '[1,2)'; } +step s1c { COMMIT; } + +session s2 +step s2rc { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s2rr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s2ser { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s2ins { + INSERT INTO child VALUES ('[1,2)', '[2020-01-01,2025-01-01)', '[1,2)'); +} +step s2c { COMMIT; } + +# Violates referential integrity unless we use an up-to-date crosscheck snapshot: +permutation s1rr s2rr s2ins s1del s2c s1c + +# Raises a can't-serialize exception +# when the INSERT trigger does SELECT FOR KEY SHARE: +permutation s1rr s2rr s1del s2ins s1c s2c + +# Test the same scenarios in READ COMMITTED: +# A crosscheck snapshot is not required here. +permutation s1rc s2rc s2ins s1del s2c s1c +permutation s1rc s2rc s1del s2ins s1c s2c + +# Test the same scenarios in SERIALIZABLE: +# We should report the FK violation: +permutation s1ser s2ser s2ins s1del s2c s1c +# We raise a concurrent update error +# which is good enough: +permutation s1ser s2ser s1del s2ins s1c s2c + +# Also check updating the valid time (without violating RI): + +# ...with READ COMMITTED: +permutation s1rc s2rc s2ins s1upok s2c s1c +permutation s1rc s2rc s1upok s2ins s1c s2c +# ...with REPEATABLE READ: +permutation s1rr s2rr s2ins s1upok s2c s1c +permutation s1rr s2rr s1upok s2ins s1c s2c +# ...with SERIALIZABLE: +permutation s1ser s2ser s2ins s1upok s2c s1c +permutation s1ser s2ser s1upok s2ins s1c s2c + +# Also check updating the valid time (while violating RI): + +# ...with READ COMMITTED: +permutation s1rc s2rc s2ins s1upbad s2c s1c +permutation s1rc s2rc s1upbad s2ins s1c s2c +# ...with REPEATABLE READ: +permutation s1rr s2rr s2ins s1upbad s2c s1c +permutation s1rr s2rr s1upbad s2ins s1c s2c +# ...with SERIALIZABLE: +permutation s1ser s2ser s2ins s1upbad s2c s1c +permutation s1ser s2ser s1upbad s2ins s1c s2c diff --git a/src/test/isolation/specs/for-portion-of.spec b/src/test/isolation/specs/for-portion-of.spec new file mode 100644 index 0000000000000..1ab9b257551e9 --- /dev/null +++ b/src/test/isolation/specs/for-portion-of.spec @@ -0,0 +1,597 @@ +# UPDATE/DELETE FOR PORTION OF test +# +# Test inserting temporal leftovers from a FOR PORTION OF update/delete. +# +# In READ COMMITTED mode, concurrent updates/deletes to the same records cause +# weird results. Portions of history that should have been updated/deleted don't +# get changed. That's because the leftovers from one operation are added too +# late to be seen by the other. EvalPlanQual will reload the changed-in-common +# row, but it won't re-scan to find new leftovers. +# +# MariaDB similarly gives undesirable results in READ COMMITTED mode (although +# not the same results). DB2 doesn't have READ COMMITTED, but it gives correct +# results at all levels, in particular READ STABILITY (which seems closest). +# +# A workaround is to lock the part of history you want before changing it (using +# SELECT FOR UPDATE). That way the search for rows is late enough to see +# leftovers from the other session(s). This shouldn't impose any new deadlock +# risks, since the locks are the same as before. Adding a third/fourth/etc. +# connection also doesn't change the semantics. The READ COMMITTED tests here +# demonstrate the problem and also show that solving it with manual locks is +# viable and not vitiated by any bugs. Incidentally, this approach also works in +# MariaDB. +# +# We run the same tests under REPEATABLE READ to show the problem goes away. +# In general they do what you'd want with no explicit locking required, but some +# orderings raise a concurrent update/delete failure (as expected). If there is +# a prior read by s1, concurrent update/delete failures are more common. +# +# To save on test time, we only run a couple SERIALIZABLE tests (for the more +# problematic permutations). +# +# We test updates where s2 updates history that is: +# +# - non-overlapping with s1, +# - contained entirely in s1, +# - partly contained in s1. +# +# We don't need to test where s2 entirely contains s1 because of symmetry: +# we test both when s1 precedes s2 and when s2 precedes s1, so that scenario is +# covered. +# +# We test various orderings of the update/delete/commit from s1 and s2. +# Note that `s1lock s2lock s1change` is boring because it's the same as +# `s1lock s1change s2lock`. In other words it doesn't matter if something +# interposes between the lock and its change (as long as everyone is following +# the same policy). + +setup +{ + CREATE TABLE products ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + price decimal NOT NULL, + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)); + INSERT INTO products VALUES + ('[1,2)', '[2020-01-01,2030-01-01)', 5.00); +} + +teardown { DROP TABLE products; } + +session s1 +setup { SET datestyle TO ISO, YMD; } +step s1rc { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s1rr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s1ser { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s1lock2025 { + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-01-01,2026-01-01)' + ORDER BY valid_at FOR UPDATE; +} +step s1upd2025 { + UPDATE products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + SET price = 8.00 + WHERE id = '[1,2)'; +} +step s1del2025 { + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-01-01' TO '2026-01-01' + WHERE id = '[1,2)'; +} +step s1q { SELECT * FROM products ORDER BY id, valid_at; } +step s1c { COMMIT; } + +session s2 +setup { SET datestyle TO ISO, YMD; } +step s2rc { BEGIN ISOLATION LEVEL READ COMMITTED; } +step s2rr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s2ser { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s2lock202503 { + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-03-01,2025-04-01)' + ORDER BY valid_at FOR UPDATE; +} +step s2lock20252026 { + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2025-06-01,2026-06-01)' + ORDER BY valid_at FOR UPDATE; +} +step s2lock2027 { + SELECT * FROM products + WHERE id = '[1,2)' AND valid_at && '[2027-01-01,2028-01-01)' + ORDER BY valid_at FOR UPDATE; +} +step s2upd202503 { + UPDATE products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + SET price = 10.00 + WHERE id = '[1,2)'; +} +step s2upd20252026 { + UPDATE products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + SET price = 10.00 + WHERE id = '[1,2)'; +} +step s2upd2027 { + UPDATE products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + SET price = 10.00 + WHERE id = '[1,2)'; +} +step s2del202503 { + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-04-01' + WHERE id = '[1,2)'; +} +step s2del20252026 { + DELETE FROM products + FOR PORTION OF valid_at FROM '2025-06-01' TO '2026-06-01' + WHERE id = '[1,2)'; +} +step s2del2027 { + DELETE FROM products + FOR PORTION OF valid_at FROM '2027-01-01' TO '2028-01-01' + WHERE id = '[1,2)'; +} +step s2c { COMMIT; } + +# ######################################## +# READ COMMITTED tests, UPDATE+UPDATE: +# ######################################## + +# s1 sees the leftovers +permutation s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1upd2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1upd2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1upd2025 s1c s1q + +# s2 sees the leftovers +permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2upd2027 s2c s1q + +# s2 loads the updated row +permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2upd202503 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2upd20252026 s2c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be updated either. +permutation s1rc s2rc s2upd2027 s1upd2025 s2c s1c s1q + +# Workaround: +# s1 updates the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1upd2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# but EvalPlanQual still matches the row to be updated. +permutation s1rc s2rc s2upd202503 s1upd2025 s2c s1c s1q + +# Workaround: +# s1 overwrites the row from s2 and sees its leftovers +permutation s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1upd2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# but EvalPlanQual still matches the row to be updated, +# and s1's leftovers don't conflict with s2's. +permutation s1rc s2rc s2upd20252026 s1upd2025 s2c s1c s1q + +# Workaround: +# s1 overwrites the row from s2 and sees its leftovers +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1upd2025 s1c s1q + +# ######################################## +# READ COMMITTED tests, UPDATE+DELETE: +# ######################################## + +# s1 sees the leftovers +permutation s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1upd2025 s1c s1q + +# s1 ignores the deleted row and sees its leftovers +permutation s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1upd2025 s1c s1q + +# s1 ignores the deleted row and sees its leftovers +permutation s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1upd2025 s1c s1q + +# s2 sees the leftovers +permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock2027 s2del2027 s2c s1q + +# s2 loads the updated row +permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock202503 s2del202503 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rc s2rc s1lock2025 s1upd2025 s1c s2lock20252026 s2del20252026 s2c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be updated either. +permutation s1rc s2rc s2del2027 s1upd2025 s2c s1c s1q + +# Workaround: +# s1 updates the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1upd2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be updated either. +permutation s1rc s2rc s2del202503 s1upd2025 s2c s1c s1q + +# Workaround: +# s1 sees the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1upd2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be updated either. +permutation s1rc s2rc s2del20252026 s1upd2025 s2c s1c s1q + +# Workaround: +# s1 sees the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1upd2025 s1c s1q + +# ######################################## +# READ COMMITTED tests, DELETE+UPDATE: +# ######################################## + +# s1 sees the leftovers +permutation s1rc s2rc s2lock2027 s2upd2027 s2c s1lock2025 s1del2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rc s2rc s2lock202503 s2upd202503 s2c s1lock2025 s1del2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rc s2rc s2lock20252026 s2upd20252026 s2c s1lock2025 s1del2025 s1c s1q + +# s2 sees the leftovers +permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2upd2027 s2c s1q + +# s2 ignores the deleted row +permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2upd202503 s2c s1q + +# s2 ignores the deleted row and sees its leftovers +permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2upd20252026 s2c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be deleted either. +permutation s1rc s2rc s2upd2027 s1del2025 s2c s1c s1q + +# Workaround: +# s1 deletes the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock2027 s2upd2027 s1lock2025 s2c s1del2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# but EvalPlanQual still matches the row to be deleted. +permutation s1rc s2rc s2upd202503 s1del2025 s2c s1c s1q + +# Workaround: +# s1 deletes the new row from s2 and its leftovers +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock202503 s2upd202503 s1lock2025 s2c s1del2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# but EvalPlanQual still matches the row to be deleted, +# and s1 leaves leftovers from the row created by s2. +permutation s1rc s2rc s2upd20252026 s1del2025 s2c s1c s1q + +# Workaround: +# s1 deletes the new row from s2 and its leftovers +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock20252026 s2upd20252026 s1lock2025 s2c s1del2025 s1c s1q + +# ######################################## +# READ COMMITTED tests, DELETE+DELETE: +# ######################################## + +# s1 sees the leftovers +permutation s1rc s2rc s2lock2027 s2del2027 s2c s1lock2025 s1del2025 s1c s1q + +# s1 ignores the deleted row and sees its leftovers +permutation s1rc s2rc s2lock202503 s2del202503 s2c s1lock2025 s1del2025 s1c s1q + +# s1 ignores the deleted row and sees its leftovers +permutation s1rc s2rc s2lock20252026 s2del20252026 s2c s1lock2025 s1del2025 s1c s1q + +# s2 sees the leftovers +permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock2027 s2del2027 s2c s1q + +# s2 ignores the deleted row +permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock202503 s2del202503 s2c s1q + +# s2 ignores the deleted row and sees its leftovers +permutation s1rc s2rc s1lock2025 s1del2025 s1c s2lock20252026 s2del20252026 s2c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be deleted either. +permutation s1rc s2rc s2del2027 s1del2025 s2c s1c s1q + +# Workaround: +# s1 deletes the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock2027 s2del2027 s1lock2025 s2c s1del2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be deleted either. +permutation s1rc s2rc s2del202503 s1del2025 s2c s1c s1q + +# Workaround: +# s1 deletes the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock202503 s2del202503 s1lock2025 s2c s1del2025 s1c s1q + +# Problem: +# s1 (without locking) overlooks the leftovers from s2 +# and EvalPlanQual no longer matches the row to be deleted either. +permutation s1rc s2rc s2del20252026 s1del2025 s2c s1c s1q + +# Workaround: +# s1 deletes the leftovers from s2 +# Locking is required or s1 won't see the leftovers. +permutation s1rc s2rc s2lock20252026 s2del20252026 s1lock2025 s2c s1del2025 s1c s1q + +# ######################################## +# REPEATABLE READ tests, UPDATE+UPDATE: +# ######################################## + +# s1 sees the leftovers +permutation s1rr s2rr s2upd2027 s2c s1upd2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rr s2rr s2upd202503 s2c s1upd2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rr s2rr s2upd20252026 s2c s1upd2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1upd2025 s1c s2upd2027 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rr s2rr s1upd2025 s1c s2upd202503 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rr s2rr s1upd2025 s1c s2upd20252026 s2c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s2upd2027 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s2upd202503 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s2upd20252026 s1upd2025 s2c s1c s1q + +## with prior read by s1: + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd2027 s2c s1upd2025 s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd202503 s2c s1upd2025 s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd20252026 s2c s1upd2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1q s1upd2025 s1c s2upd2027 s2c s1q + +# s2 loads the updated row +permutation s1rr s2rr s1q s1upd2025 s1c s2upd202503 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rr s2rr s1q s1upd2025 s1c s2upd20252026 s2c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd2027 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd202503 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd20252026 s1upd2025 s2c s1c s1q + +# ######################################## +# REPEATABLE READ tests, UPDATE+DELETE: +# ######################################## + +# s1 sees the leftovers +permutation s1rr s2rr s2del2027 s2c s1upd2025 s1c s1q + +# s1 ignores the deleted row and sees its leftovers +permutation s1rr s2rr s2del202503 s2c s1upd2025 s1c s1q + +# s1 ignores the deleted row and sees its leftovers +permutation s1rr s2rr s2del20252026 s2c s1upd2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1upd2025 s1c s2del2027 s2c s1q + +# s2 loads the updated row +permutation s1rr s2rr s1upd2025 s1c s2del202503 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rr s2rr s1upd2025 s1c s2del20252026 s2c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s2del2027 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s2del202503 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s2del20252026 s1upd2025 s2c s1c s1q + +## with prior read by s1: + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del2027 s2c s1upd2025 s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del202503 s2c s1upd2025 s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del20252026 s2c s1upd2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1q s1upd2025 s1c s2del2027 s2c s1q + +# s2 loads the updated row +permutation s1rr s2rr s1q s1upd2025 s1c s2del202503 s2c s1q + +# s2 loads the updated row and sees its leftovers +permutation s1rr s2rr s1q s1upd2025 s1c s2del20252026 s2c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del2027 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del202503 s1upd2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del20252026 s1upd2025 s2c s1c s1q + +# ######################################## +# REPEATABLE READ tests, DELETE+UPDATE: +# ######################################## + +# s1 sees the leftovers +permutation s1rr s2rr s2upd2027 s2c s1del2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rr s2rr s2upd202503 s2c s1del2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rr s2rr s2upd20252026 s2c s1del2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1del2025 s1c s2upd2027 s2c s1q + +# s2 ignores the deleted row +permutation s1rr s2rr s1del2025 s1c s2upd202503 s2c s1q + +# s2 ignores the deleted row and sees its leftovers +permutation s1rr s2rr s1del2025 s1c s2upd20252026 s2c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s2upd2027 s1del2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s2upd202503 s1del2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s2upd20252026 s1del2025 s2c s1c s1q + +## with prior read by s1: + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd2027 s2c s1del2025 s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd202503 s2c s1del2025 s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd20252026 s2c s1del2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1q s1del2025 s1c s2upd2027 s2c s1q + +# s2 ignores the deleted row +permutation s1rr s2rr s1q s1del2025 s1c s2upd202503 s2c s1q + +# s2 ignores the deleted row and sees its leftovers +permutation s1rr s2rr s1q s1del2025 s1c s2upd20252026 s2c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd2027 s1del2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd202503 s1del2025 s2c s1c s1q + +# s1 fails from concurrent update +permutation s1rr s2rr s1q s2upd20252026 s1del2025 s2c s1c s1q + +# ######################################## +# REPEATABLE READ tests, DELETE+DELETE: +# ######################################## + +# s1 sees the leftovers +permutation s1rr s2rr s2del2027 s2c s1del2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rr s2rr s2del202503 s2c s1del2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1rr s2rr s2del20252026 s2c s1del2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1del2025 s1c s2del2027 s2c s1q + +# s2 ignores the deleted row +permutation s1rr s2rr s1del2025 s1c s2del202503 s2c s1q + +# s2 ignores the deleted row and sees its leftovers +permutation s1rr s2rr s1del2025 s1c s2del20252026 s2c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s2del2027 s1del2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s2del202503 s1del2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s2del20252026 s1del2025 s2c s1c s1q + +## with prior read by s1: + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del2027 s2c s1del2025 s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del202503 s2c s1del2025 s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del20252026 s2c s1del2025 s1c s1q + +# s2 sees the leftovers +permutation s1rr s2rr s1q s1del2025 s1c s2del2027 s2c s1q + +# s2 ignores the deleted row +permutation s1rr s2rr s1q s1del2025 s1c s2del202503 s2c s1q + +# s2 ignores the deleted row and sees its leftovers +permutation s1rr s2rr s1q s1del2025 s1c s2del20252026 s2c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del2027 s1del2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del202503 s1del2025 s2c s1c s1q + +# s1 fails from concurrent delete +permutation s1rr s2rr s1q s2del20252026 s1del2025 s2c s1c s1q + +# ######################################## +# SERIALIZABLE tests, UPDATE+UPDATE: +# ######################################## + +# s1 sees the leftovers +permutation s1ser s2ser s2upd2027 s2c s1upd2025 s1c s1q + +# s1 reloads the updated row and sees its leftovers +permutation s1ser s2ser s2upd20252026 s2c s1upd2025 s1c s1q diff --git a/src/test/isolation/specs/insert-conflict-do-select.spec b/src/test/isolation/specs/insert-conflict-do-select.spec new file mode 100644 index 0000000000000..dcfd9f8cb5363 --- /dev/null +++ b/src/test/isolation/specs/insert-conflict-do-select.spec @@ -0,0 +1,53 @@ +# INSERT...ON CONFLICT DO SELECT test +# +# This test verifies locking behavior of ON CONFLICT DO SELECT with different +# lock strengths: no lock, FOR KEY SHARE, FOR SHARE, FOR NO KEY UPDATE, and +# FOR UPDATE. + +setup +{ + CREATE TABLE doselect (key int primary key, val text); + INSERT INTO doselect VALUES (1, 'original'); +} + +teardown +{ + DROP TABLE doselect; +} + +session s1 +setup +{ + BEGIN ISOLATION LEVEL READ COMMITTED; +} +step insert1 { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT RETURNING *; } +step insert1_keyshare { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR KEY SHARE RETURNING *; } +step insert1_share { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR SHARE RETURNING *; } +step insert1_nokeyupd { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR NO KEY UPDATE RETURNING *; } +step insert1_update { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; } +step c1 { COMMIT; } +step a1 { ABORT; } + +session s2 +setup +{ + BEGIN ISOLATION LEVEL READ COMMITTED; +} +step insert2 { INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT RETURNING *; } +step insert2_update { INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; } +step select2 { SELECT * FROM doselect; } +step c2 { COMMIT; } + +# Test 1: DO SELECT without locking - should not block +permutation insert1 insert2 c1 select2 c2 + +# Test 2: DO SELECT FOR UPDATE - should block until first transaction commits +permutation insert1_update insert2_update c1 select2 c2 + +# Test 3: DO SELECT FOR UPDATE - should unblock when first transaction aborts +permutation insert1_update insert2_update a1 select2 c2 + +# Test 4: Different lock strengths all properly acquire locks +permutation insert1_keyshare insert2_update c1 select2 c2 +permutation insert1_share insert2_update c1 select2 c2 +permutation insert1_nokeyupd insert2_update c1 select2 c2 diff --git a/src/test/isolation/specs/insert-conflict-do-update-4.spec b/src/test/isolation/specs/insert-conflict-do-update-4.spec new file mode 100644 index 0000000000000..a62531660d3aa --- /dev/null +++ b/src/test/isolation/specs/insert-conflict-do-update-4.spec @@ -0,0 +1,42 @@ +# INSERT...ON CONFLICT DO UPDATE test with partitioned table +# +# We use SELECT FOR UPDATE to block the INSERT at the point where +# it has found an existing tuple and is attempting to update it. +# Then we can execute a conflicting update and verify the results. +# Of course, this only works in READ COMMITTED mode, else we'd get an error. + +setup +{ + CREATE TABLE upsert (i int PRIMARY KEY, j int, k int) PARTITION BY RANGE (i); + CREATE TABLE upsert_1 PARTITION OF upsert FOR VALUES FROM (1) TO (100); + CREATE TABLE upsert_2 PARTITION OF upsert FOR VALUES FROM (100) TO (200); + + INSERT INTO upsert VALUES (1, 10, 100); +} + +teardown +{ + DROP TABLE upsert; +} + +session s1 +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step insert1 { INSERT INTO upsert VALUES (1, 11, 111) + ON CONFLICT (i) DO UPDATE SET k = EXCLUDED.k; } +step select1 { SELECT * FROM upsert ORDER BY i; } +step c1 { COMMIT; } + +session s2 +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step lock2 { SELECT * FROM upsert WHERE i = 1 FOR UPDATE; } +step update2a { UPDATE upsert SET i = i + 10 WHERE i = 1; } +step update2b { UPDATE upsert SET i = i + 150 WHERE i = 1; } +step delete2 { DELETE FROM upsert WHERE i = 1; } +step c2 { COMMIT; } + +# Test case where concurrent update moves the target row within the partition +permutation lock2 insert1 update2a c2 select1 c1 +# Test case where concurrent update moves the target row to another partition +permutation lock2 insert1 update2b c2 select1 c1 +# Test case where target row is concurrently deleted +permutation lock2 insert1 delete2 c2 select1 c1 diff --git a/src/test/isolation/specs/intra-grant-inplace.spec b/src/test/isolation/specs/intra-grant-inplace.spec index 9936d389359e5..e9c7848624cd7 100644 --- a/src/test/isolation/specs/intra-grant-inplace.spec +++ b/src/test/isolation/specs/intra-grant-inplace.spec @@ -20,7 +20,7 @@ step grant1 { GRANT SELECT ON intra_grant_inplace TO PUBLIC; } step drop1 { - DROP TABLE intra_grant_inplace; + DELETE FROM pg_class WHERE relname = 'intra_grant_inplace'; } step c1 { COMMIT; } diff --git a/src/test/isolation/specs/merge-match-recheck.spec b/src/test/isolation/specs/merge-match-recheck.spec index 26266b8c2978e..6e7a776d17e5a 100644 --- a/src/test/isolation/specs/merge-match-recheck.spec +++ b/src/test/isolation/specs/merge-match-recheck.spec @@ -99,15 +99,19 @@ step "merge_bal_pa" } step "merge_bal_tg" { - MERGE INTO target_tg t - USING (SELECT 1 as key) s - ON s.key = t.key - WHEN MATCHED AND balance < 100 THEN - UPDATE SET balance = balance * 2, val = t.val || ' when1' - WHEN MATCHED AND balance < 200 THEN - UPDATE SET balance = balance * 4, val = t.val || ' when2' - WHEN MATCHED AND balance < 300 THEN - UPDATE SET balance = balance * 8, val = t.val || ' when3'; + WITH t AS ( + MERGE INTO target_tg t + USING (SELECT 1 as key) s + ON s.key = t.key + WHEN MATCHED AND balance < 100 THEN + UPDATE SET balance = balance * 2, val = t.val || ' when1' + WHEN MATCHED AND balance < 200 THEN + UPDATE SET balance = balance * 4, val = t.val || ' when2' + WHEN MATCHED AND balance < 300 THEN + UPDATE SET balance = balance * 8, val = t.val || ' when3' + RETURNING t.* + ) + SELECT * FROM t; } step "merge_delete" @@ -142,6 +146,8 @@ setup BEGIN ISOLATION LEVEL READ COMMITTED; } step "update1" { UPDATE target t SET balance = balance + 10, val = t.val || ' updated by update1' WHERE t.key = 1; } +step "update1_pa" { UPDATE target_pa t SET balance = balance + 10, val = t.val || ' updated by update1_pa' WHERE t.key = 1; } +step "update1_pa_move" { UPDATE target_pa t SET balance = 210, val = t.val || ' updated by update1_pa_move' WHERE t.key = 1; } step "update1_tg" { UPDATE target_tg t SET balance = balance + 10, val = t.val || ' updated by update1_tg' WHERE t.key = 1; } step "update2" { UPDATE target t SET status = 's2', val = t.val || ' updated by update2' WHERE t.key = 1; } step "update2_tg" { UPDATE target_tg t SET status = 's2', val = t.val || ' updated by update2_tg' WHERE t.key = 1; } @@ -149,6 +155,10 @@ step "update3" { UPDATE target t SET status = 's3', val = t.val || ' updated by step "update3_tg" { UPDATE target_tg t SET status = 's3', val = t.val || ' updated by update3_tg' WHERE t.key = 1; } step "update5" { UPDATE target t SET status = 's5', val = t.val || ' updated by update5' WHERE t.key = 1; } step "update5_tg" { UPDATE target_tg t SET status = 's5', val = t.val || ' updated by update5_tg' WHERE t.key = 1; } +step "update6" { UPDATE target t SET balance = balance - 100, val = t.val || ' updated by update6' WHERE t.key = 1; } +step "update6_pa" { UPDATE target_pa t SET balance = balance - 100, val = t.val || ' updated by update6_pa' WHERE t.key = 1; } +step "update6_tg" { UPDATE target_tg t SET balance = balance - 100, val = t.val || ' updated by update6_tg' WHERE t.key = 1; } +step "update7" { UPDATE target t SET balance = 350, val = t.val || ' updated by update7' WHERE t.key = 1; } step "update_bal1" { UPDATE target t SET balance = 50, val = t.val || ' updated by update_bal1' WHERE t.key = 1; } step "update_bal1_pa" { UPDATE target_pa t SET balance = 50, val = t.val || ' updated by update_bal1_pa' WHERE t.key = 1; } step "update_bal1_tg" { UPDATE target_tg t SET balance = 50, val = t.val || ' updated by update_bal1_tg' WHERE t.key = 1; } @@ -175,6 +185,18 @@ permutation "update_bal1" "merge_bal" "c2" "select1" "c1" permutation "update_bal1_pa" "merge_bal_pa" "c2" "select1_pa" "c1" permutation "update_bal1_tg" "merge_bal_tg" "c2" "select1_tg" "c1" +# merge_bal sees row concurrently updated twice and rechecks WHEN conditions, different check passes, so final balance = 140 +permutation "update1" "update6" "merge_bal" "c2" "select1" "c1" +permutation "update1_pa" "update6_pa" "merge_bal_pa" "c2" "select1_pa" "c1" +permutation "update1_tg" "update6_tg" "merge_bal_tg" "c2" "select1_tg" "c1" + +# merge_bal sees row concurrently updated twice, first update would cause all checks to fail, second update causes different check to pass, so final balance = 2000 +permutation "update7" "update6" "merge_bal" "c2" "select1" "c1" + +# merge_bal sees concurrently updated row moved to new partition, so fails +permutation "update1_pa_move" "merge_bal_pa" "c2" "c1" +permutation "update1_pa" "update1_pa_move" "merge_bal_pa" "c2" "c1" + # merge_delete sees concurrently updated row and rechecks WHEN conditions, but recheck passes and row is deleted permutation "update1" "merge_delete" "c2" "select1" "c1" permutation "update1_tg" "merge_delete_tg" "c2" "select1_tg" "c1" diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec index c718ff646b2d1..b902779edd671 100644 --- a/src/test/isolation/specs/merge-update.spec +++ b/src/test/isolation/specs/merge-update.spec @@ -23,12 +23,27 @@ setup INSERT INTO pa_target VALUES (1, 'initial'); INSERT INTO pa_target VALUES (2, 'initial'); + + CREATE FUNCTION explain_filter(text) RETURNS SETOF text + LANGUAGE plpgsql AS + $$ + DECLARE + ln text; + BEGIN + FOR ln IN EXECUTE $1 LOOP + -- Ignore hash memory usage because it varies depending on the system + CONTINUE WHEN (ln ~ 'Memory Usage'); + RETURN NEXT ln; + END LOOP; + END; + $$; } teardown { DROP TABLE target; DROP TABLE pa_target CASCADE; + DROP FUNCTION explain_filter; } session "s1" @@ -78,6 +93,7 @@ step "pa_merge3" } step "c1" { COMMIT; } step "a1" { ABORT; } +step "s1beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; } session "s2" setup @@ -97,6 +113,22 @@ step "merge2a" UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a' RETURNING merge_action(), old, new, t.*; } +step "explain_merge2a" +{ + SELECT explain_filter($$ + EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + MERGE INTO target t + USING (SELECT 1 as key, 'merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN + UPDATE set key = t.key + 1, val = t.val || ' source not matched by merge2a' + RETURNING merge_action(), old, new, t.* + $$); +} step "merge2b" { MERGE INTO target t @@ -130,6 +162,22 @@ step "pa_merge2a" UPDATE set key = t.key + 1, val = t.val || ' source not matched by pa_merge2a' RETURNING merge_action(), old, new, t.*; } +step "explain_pa_merge2a" +{ + SELECT explain_filter($$ + EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) + MERGE INTO pa_target t + USING (SELECT 1 as key, 'pa_merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN + UPDATE set key = t.key + 1, val = t.val || ' source not matched by pa_merge2a' + RETURNING merge_action(), old, new, t.* + $$); +} # MERGE proceeds only if 'val' unchanged step "pa_merge2b_when" { @@ -160,16 +208,20 @@ permutation "merge1" "c1" "select2" "c2" # One after the other, no concurrency permutation "merge1" "c1" "merge2a" "select2" "c2" +permutation "merge1" "c1" "explain_merge2a" "select2" "c2" permutation "pa_merge1" "c1" "pa_merge2c_dup" "a2" # Now with concurrency permutation "merge1" "merge2a" "c1" "select2" "c2" +permutation "merge1" "explain_merge2a" "c1" "select2" "c2" permutation "merge1" "merge2a" "a1" "select2" "c2" permutation "merge1" "merge2b" "c1" "select2" "c2" permutation "merge1" "merge2c" "c1" "select2" "c2" permutation "pa_merge1" "pa_merge2a" "c1" "pa_select2" "c2" +permutation "pa_merge1" "explain_pa_merge2a" "c1" "pa_select2" "c2" permutation "pa_merge2" "pa_merge2a" "c1" "pa_select2" "c2" # fails permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds permutation "pa_merge3" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN not satisfied by updated tuple permutation "pa_merge1" "pa_merge2b_when" "c1" "pa_select2" "c2" # WHEN satisfied by updated tuple permutation "pa_merge1" "pa_merge2c_dup" "c1" "a2" +permutation "merge2a" "c1" "s1beginrr" "merge1" "c2" diff --git a/src/test/isolation/specs/multixact-stats.spec b/src/test/isolation/specs/multixact-stats.spec new file mode 100644 index 0000000000000..07d4b11be6dcc --- /dev/null +++ b/src/test/isolation/specs/multixact-stats.spec @@ -0,0 +1,111 @@ +# Test for pg_get_multixact_stats() +# +# This test creates one multixact on a brand-new table. While the multixact +# is pinned by two open transactions, we check some patterns that VACUUM and +# FREEZE cannot violate: +# 1) "members" increased by at least 1 when the second session locked the row. +# 2) (num_mxids / num_members) not decreased compared to earlier snapshots. +# 3) "oldest_*" fields never decreased. +# +# This test does not run checks after releasing locks, as freezing and/or +# truncation may shrink the multixact ranges calculated. + +setup +{ + CREATE TABLE mxq(id int PRIMARY KEY, v int); + INSERT INTO mxq VALUES (1, 42); +} + +teardown +{ + DROP TABLE mxq; +} + +# Two sessions that lock the same tuple, leading to one multixact with +# at least 2 members. +session "s1" +setup { SET client_min_messages = warning; SET lock_timeout = '5s'; } +step s1_begin { BEGIN; } +step s1_lock { SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE; } +step s1_commit { COMMIT; } + +session "s2" +setup { SET client_min_messages = warning; SET lock_timeout = '5s'; } +step s2_begin { BEGIN; } +step s2_lock { SELECT 1 FROM mxq WHERE id=1 FOR KEY SHARE; } +step s2_commit { COMMIT; } + +# Save multixact state *BEFORE* any locking; some of these may be NULLs if +# multixacts have not initialized yet. +step snap0 { + CREATE TEMP TABLE snap0 AS + SELECT num_mxids, num_members, oldest_multixact + FROM pg_get_multixact_stats(); +} + +# Save multixact state after s1 has locked the row. +step snap1 { + CREATE TEMP TABLE snap1 AS + SELECT num_mxids, num_members, oldest_multixact + FROM pg_get_multixact_stats(); +} + +# Save multixact state after s2 joins to lock the same row, leading to +# a multixact with at least 2 members. +step snap2 { + CREATE TEMP TABLE snap2 AS + SELECT num_mxids, num_members, oldest_multixact + FROM pg_get_multixact_stats(); +} + +# Pretty, deterministic key/value outputs based of boolean checks: +# is_init_mxids : num_mxids not NULL +# is_init_members : num_members not NULL +# is_init_oldest_mxid : oldest_multixact not NULL +# is_oldest_mxid_nondec_01 : oldest_multixact not decreased (snap0->snap1) +# is_oldest_mxid_nondec_12 : oldest_multixact did not decreased (snap1->snap2) +# is_members_increased_ge1 : members increased by at least 1 when s2 joined +# is_mxids_nondec_01 : num_mxids not decreased (snap0->snap1) +# is_mxids_nondec_12 : num_mxids not decreased (snap1->snap2) +# is_members_nondec_01 : num_members not decreased (snap0->snap1) +# is_members_nondec_12 : num_members not decreased (snap1->snap2) +step check_while_pinned { + SELECT r.assertion, r.ok + FROM snap0 s0 + JOIN snap1 s1 ON TRUE + JOIN snap2 s2 ON TRUE, + LATERAL unnest( + ARRAY[ + 'is_init_mxids', + 'is_init_members', + 'is_init_oldest_mxid', + 'is_init_oldest_off', + 'is_oldest_mxid_nondec_01', + 'is_oldest_mxid_nondec_12', + 'is_oldest_off_nondec_01', + 'is_oldest_off_nondec_12', + 'is_members_increased_ge1', + 'is_mxids_nondec_01', + 'is_mxids_nondec_12', + 'is_members_nondec_01', + 'is_members_nondec_12' + ], + ARRAY[ + (s2.num_mxids IS NOT NULL), + (s2.num_members IS NOT NULL), + (s2.oldest_multixact IS NOT NULL), + + (s1.oldest_multixact::text::bigint >= COALESCE(s0.oldest_multixact::text::bigint, 0)), + (s2.oldest_multixact::text::bigint >= COALESCE(s1.oldest_multixact::text::bigint, 0)), + + (s2.num_members >= COALESCE(s1.num_members, 0) + 1), + + (s1.num_mxids >= COALESCE(s0.num_mxids, 0)), + (s2.num_mxids >= COALESCE(s1.num_mxids, 0)), + (s1.num_members >= COALESCE(s0.num_members, 0)), + (s2.num_members >= COALESCE(s1.num_members, 0)) + ] + ) AS r(assertion, ok); +} + +permutation snap0 s1_begin s1_lock snap1 s2_begin s2_lock snap2 check_while_pinned s1_commit s2_commit diff --git a/src/test/isolation/specs/partition-merge.spec b/src/test/isolation/specs/partition-merge.spec new file mode 100644 index 0000000000000..f3c5ce2fbf1d6 --- /dev/null +++ b/src/test/isolation/specs/partition-merge.spec @@ -0,0 +1,62 @@ +# Verify that MERGE operation locks DML operations with partitioned table + +setup +{ + DROP TABLE IF EXISTS tpart; + CREATE TABLE tpart(i int, t text) partition by range(i); + CREATE TABLE tpart_00_10 PARTITION OF tpart FOR VALUES FROM (0) TO (10); + CREATE TABLE tpart_10_20 PARTITION OF tpart FOR VALUES FROM (10) TO (20); + CREATE TABLE tpart_20_30 PARTITION OF tpart FOR VALUES FROM (20) TO (30); + CREATE TABLE tpart_default PARTITION OF tpart DEFAULT; + INSERT INTO tpart VALUES (5, 'text05'); + INSERT INTO tpart VALUES (15, 'text15'); + INSERT INTO tpart VALUES (25, 'text25'); + INSERT INTO tpart VALUES (35, 'text35'); +} + +teardown +{ + DROP TABLE tpart; +} + +session s1 +step s1b { BEGIN; } +step s1brr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s1bs { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s1merg { ALTER TABLE tpart MERGE PARTITIONS (tpart_00_10, tpart_10_20) INTO tpart_00_20; } +step s1c { COMMIT; } + + +session s2 +step s2b { BEGIN; } +step s2brr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s2bs { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s2i { INSERT INTO tpart VALUES (1, 'text01'); } +step s2u { UPDATE tpart SET t = 'text01modif' where i = 1; } +step s2u2 { UPDATE tpart SET i = 21 where i = 1; } +step s2u3 { UPDATE tpart SET i = 11 where i = 1; } +step s2c { COMMIT; } +step s2s { SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; } + + +# s2 inserts row into table. s1 starts MERGE PARTITIONS then +# s2 is trying to update inserted row and waits until s1 finishes +# MERGE operation. + +permutation s2b s2i s2c s1b s1merg s2b s2u s1c s2c s2s +permutation s2b s2i s2c s1brr s1merg s2b s2u s1c s2c s2s +permutation s2b s2i s2c s1bs s1merg s2b s2u s1c s2c s2s + +permutation s2brr s2i s2c s1b s1merg s2b s2u s1c s2c s2s +permutation s2brr s2i s2c s1brr s1merg s2b s2u s1c s2c s2s +permutation s2brr s2i s2c s1bs s1merg s2b s2u s1c s2c s2s + +permutation s2bs s2i s2c s1b s1merg s2b s2u s1c s2c s2s +permutation s2bs s2i s2c s1brr s1merg s2b s2u s1c s2c s2s +permutation s2bs s2i s2c s1bs s1merg s2b s2u s1c s2c s2s + +# Tuple routing between partitions. +permutation s2b s2i s2c s1b s1merg s2b s2u2 s1c s2c s2s + +# Tuple routing between merging partitions. +permutation s2b s2i s2c s1b s1merg s2b s2u3 s1c s2c s2s diff --git a/src/test/isolation/specs/partition-split.spec b/src/test/isolation/specs/partition-split.spec new file mode 100644 index 0000000000000..af954be5dc010 --- /dev/null +++ b/src/test/isolation/specs/partition-split.spec @@ -0,0 +1,62 @@ +# Verify that SPLIT operation locks DML operations with partitioned table + +setup +{ + DROP TABLE IF EXISTS tpart; + CREATE TABLE tpart(i int, t text) partition by range(i); + CREATE TABLE tpart_00_10 PARTITION OF tpart FOR VALUES FROM (0) TO (10); + CREATE TABLE tpart_10_20 PARTITION OF tpart FOR VALUES FROM (10) TO (20); + CREATE TABLE tpart_20_30 PARTITION OF tpart FOR VALUES FROM (20) TO (30); + CREATE TABLE tpart_default PARTITION OF tpart DEFAULT; + INSERT INTO tpart VALUES (5, 'text05'); + INSERT INTO tpart VALUES (15, 'text15'); + INSERT INTO tpart VALUES (25, 'text25'); + INSERT INTO tpart VALUES (35, 'text35'); +} + +teardown +{ + DROP TABLE tpart; +} + +session s1 +step s1b { BEGIN; } +step s1brr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s1bs { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s1splt { ALTER TABLE tpart SPLIT PARTITION tpart_10_20 INTO + (PARTITION tpart_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tpart_15_20 FOR VALUES FROM (15) TO (20)); } +step s1c { COMMIT; } + + +session s2 +step s2b { BEGIN; } +step s2brr { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s2bs { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step s2i { INSERT INTO tpart VALUES (1, 'text01'); } +step s2c { COMMIT; } +step s2s { SELECT tableoid::regclass, * FROM tpart ORDER BY tableoid::regclass::text COLLATE "C", i; } +step s2u { UPDATE tpart SET i = 16 where i = 5; } +step s2u2 { UPDATE tpart SET i = 11 where i = 15; } + + +# s1 starts SPLIT PARTITION then s2 trying to insert row and +# waits until s1 finished SPLIT operation. + +permutation s1b s1splt s2b s2i s1c s2c s2s +permutation s1b s1splt s2brr s2i s1c s2c s2s +permutation s1b s1splt s2bs s2i s1c s2c s2s + +permutation s1brr s1splt s2b s2i s1c s2c s2s +permutation s1brr s1splt s2brr s2i s1c s2c s2s +permutation s1brr s1splt s2bs s2i s1c s2c s2s + +permutation s1bs s1splt s2b s2i s1c s2c s2s +permutation s1bs s1splt s2brr s2i s1c s2c s2s +permutation s1bs s1splt s2bs s2i s1c s2c s2s + +# Tuple routing between partitions. +permutation s1b s1splt s2b s2u s1c s2c s2s + +# Tuple routing inside splitting partition. +permutation s1b s1splt s2b s2u2 s1c s2c s2s diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec index 1d98ac785b816..da16710da0fb2 100644 --- a/src/test/isolation/specs/stats.spec +++ b/src/test/isolation/specs/stats.spec @@ -6,10 +6,13 @@ setup INSERT INTO test_stat_tab(key, value) VALUES('k0', 1); INSERT INTO test_stat_oid(name, oid) VALUES('test_stat_tab', 'test_stat_tab'::regclass); - CREATE FUNCTION test_stat_func() RETURNS VOID LANGUAGE plpgsql AS $$BEGIN END;$$; + -- include 10us sleep to ensure that runtime measures as more than zero + CREATE FUNCTION test_stat_func() RETURNS VOID LANGUAGE plpgsql AS + $$BEGIN PERFORM pg_sleep(10e-6); END;$$; INSERT INTO test_stat_oid(name, oid) VALUES('test_stat_func', 'test_stat_func'::regproc); - CREATE FUNCTION test_stat_func2() RETURNS VOID LANGUAGE plpgsql AS $$BEGIN END;$$; + CREATE FUNCTION test_stat_func2() RETURNS VOID LANGUAGE plpgsql AS + $$BEGIN PERFORM pg_sleep(10e-6); END;$$; INSERT INTO test_stat_oid(name, oid) VALUES('test_stat_func2', 'test_stat_func2'::regproc); CREATE TABLE test_slru_stats(slru TEXT, stat TEXT, value INT); @@ -55,6 +58,10 @@ step s1_track_funcs_none { SET track_functions = 'none'; } step s1_func_call { SELECT test_stat_func(); } step s1_func_drop { DROP FUNCTION test_stat_func(); } step s1_func_stats_reset { SELECT pg_stat_reset_single_function_counters('test_stat_func'::regproc); } +step s1_func_stats_reset_check { + SELECT pg_stat_get_function_stat_reset_time('test_stat_func'::regproc) + IS NOT NULL AS has_stats_reset; +} step s1_func_stats_reset_nonexistent { SELECT pg_stat_reset_single_function_counters(12000); } step s1_reset { SELECT pg_stat_reset(); } step s1_func_stats { @@ -232,9 +239,9 @@ permutation s1_ff s2_ff s1_func_stats s2_func_call s2_func_call2 s2_ff - s1_func_stats s1_func_stats2 s1_func_stats + s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset_check s1_func_stats_reset - s1_func_stats s1_func_stats2 s1_func_stats + s1_func_stats s1_func_stats2 s1_func_stats s1_func_stats_reset_check # test pg_stat_reset_single_function_counters of non-existing function permutation diff --git a/src/test/isolation/specscanner.l b/src/test/isolation/specscanner.l index 068a77836c962..11921cec50ac8 100644 --- a/src/test/isolation/specscanner.l +++ b/src/test/isolation/specscanner.l @@ -4,7 +4,7 @@ * specscanner.l * a lexical scanner for an isolation test specification * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * *------------------------------------------------------------------------- diff --git a/src/test/kerberos/Makefile b/src/test/kerberos/Makefile index ddaa6deaa6793..8338a0e483fe8 100644 --- a/src/test/kerberos/Makefile +++ b/src/test/kerberos/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/kerberos # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/kerberos/Makefile diff --git a/src/test/kerberos/meson.build b/src/test/kerberos/meson.build index 509e993558500..11aa732e69bf9 100644 --- a/src/test/kerberos/meson.build +++ b/src/test/kerberos/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'kerberos', diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl index b0be96f2beba6..d4018acde88d7 100644 --- a/src/test/kerberos/t/001_auth.pl +++ b/src/test/kerberos/t/001_auth.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Sets up a KDC and then runs a variety of tests to make sure that the # GSSAPI/Kerberos authentication and encryption are working properly, diff --git a/src/test/ldap/LdapServer.pm b/src/test/ldap/LdapServer.pm index 58619a3db0a4d..488aa915249aa 100644 --- a/src/test/ldap/LdapServer.pm +++ b/src/test/ldap/LdapServer.pm @@ -5,7 +5,7 @@ # # Module to set up an LDAP server for testing pg_hba.conf ldap authentication # -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # ############################################################################ diff --git a/src/test/ldap/Makefile b/src/test/ldap/Makefile index e852069d7fbbc..2dfcc403f1fb7 100644 --- a/src/test/ldap/Makefile +++ b/src/test/ldap/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/ldap # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/ldap/Makefile diff --git a/src/test/ldap/meson.build b/src/test/ldap/meson.build index 7eaa393212ad0..d8961e6c8d705 100644 --- a/src/test/ldap/meson.build +++ b/src/test/ldap/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'ldap', @@ -8,6 +8,7 @@ tests += { 'tests': [ 't/001_auth.pl', 't/002_bindpasswd.pl', + 't/003_ldap_connection_param_lookup.pl', ], 'env': { 'with_ldap': ldap.found() ? 'yes' : 'no', diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl index 440c30b7ddd1d..8afb88ed8cc77 100644 --- a/src/test/ldap/t/001_auth.pl +++ b/src/test/ldap/t/001_auth.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/ldap/t/002_bindpasswd.pl b/src/test/ldap/t/002_bindpasswd.pl index 642bb2d9a7759..64cb2e412bbd7 100644 --- a/src/test/ldap/t/002_bindpasswd.pl +++ b/src/test/ldap/t/002_bindpasswd.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/ldap/t/003_ldap_connection_param_lookup.pl b/src/test/ldap/t/003_ldap_connection_param_lookup.pl new file mode 100644 index 0000000000000..359fc7a998a03 --- /dev/null +++ b/src/test/ldap/t/003_ldap_connection_param_lookup.pl @@ -0,0 +1,205 @@ + +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use FindBin; +use lib "$FindBin::RealBin/.."; + +use File::Copy; +use LdapServer; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +if ($ENV{with_ldap} ne 'yes') +{ + plan skip_all => 'LDAP not supported by this build'; +} +elsif (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bldap\b/) +{ + plan skip_all => + 'Potentially unsafe test LDAP not enabled in PG_TEST_EXTRA'; +} +elsif (!$LdapServer::setup) +{ + plan skip_all => $LdapServer::setup_error; +} + +# This tests scenarios related to the service name and the service file, +# for the connection options and their environment variables. +my $dummy_node = PostgreSQL::Test::Cluster->new('dummy_node'); +$dummy_node->init; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +note "setting up LDAP server"; + +my $ldap_rootpw = 'secret'; +my $ldap = LdapServer->new($ldap_rootpw, 'anonymous'); # use anonymous auth +$ldap->ldapadd_file('authdata.ldif'); +$ldap->ldapsetpw('uid=test1,dc=example,dc=net', 'secret1'); +$ldap->ldapsetpw('uid=test2,dc=example,dc=net', 'secret2'); + +my $td = PostgreSQL::Test::Utils::tempdir; + +# create ldap file based on postgres connection info +my $ldif_valid = "$td/connection_params.ldif"; +append_to_file( + $ldif_valid, qq{ +version:1 +dn:cn=mydatabase,dc=example,dc=net +changetype:add +objectclass:top +objectclass:device +cn:mydatabase +description:host=} . $node->host . qq{ +description:port=} . $node->port . qq{ +}); + +$ldap->ldapadd_file($ldif_valid); + +my ($ldap_server, $ldap_port, $ldaps_port, $ldap_url, + $ldaps_url, $ldap_basedn, $ldap_rootdn +) = $ldap->prop(qw(server port s_port url s_url basedn rootdn)); + +# don't bother to check the server's cert (though perhaps we should) +$ENV{'LDAPTLS_REQCERT'} = "never"; + +note "setting up PostgreSQL instance"; + +# Create the set of service files used in the tests. + +# File that includes a valid service name, that uses a decomposed +# connection string for its contents, split on spaces. +my $srvfile_valid = "$td/pg_service_valid.conf"; +append_to_file( + $srvfile_valid, qq{ +[my_srv] +ldap://localhost:$ldap_port/dc=example,dc=net?description?one?(cn=mydatabase) +}); + +# File defined with no contents, used as default value for +# PGSERVICEFILE, so that no lookup is attempted in the user's home +# directory. +my $srvfile_empty = "$td/pg_service_empty.conf"; +append_to_file($srvfile_empty, ''); + +# Default service file in PGSYSCONFDIR. +my $srvfile_default = "$td/pg_service.conf"; + +# Missing service file. +my $srvfile_missing = "$td/pg_service_missing.conf"; + +# Set the fallback directory lookup of the service file to the +# temporary directory of this test. PGSYSCONFDIR is used if the +# service file defined in PGSERVICEFILE cannot be found, or when a +# service file is found but not the service name. +local $ENV{PGSYSCONFDIR} = $td; + +# Force PGSERVICEFILE to a default location, so as this test never +# tries to look at a home directory. This value needs to remain at +# the top of this script before running any tests, and should never be +# changed. +local $ENV{PGSERVICEFILE} = "$srvfile_empty"; + +# Checks combinations of service name and a valid service file. +{ + local $ENV{PGSERVICEFILE} = $srvfile_valid; + + $dummy_node->connect_ok( + 'service=my_srv', + 'connection with correct "service" string and PGSERVICEFILE', + sql => "SELECT 'connect1_1'", + expected_stdout => qr/connect1_1/); + + $dummy_node->connect_ok( + 'postgres://?service=my_srv', + 'connection with correct "service" URI and PGSERVICEFILE', + sql => "SELECT 'connect1_2'", + expected_stdout => qr/connect1_2/); + + $dummy_node->connect_fails( + 'service=undefined-service', + 'connection with incorrect "service" string and PGSERVICEFILE', + expected_stderr => + qr/definition of service "undefined-service" not found/); + + local $ENV{PGSERVICE} = 'my_srv'; + + $dummy_node->connect_ok( + '', + 'connection with correct PGSERVICE and PGSERVICEFILE', + sql => "SELECT 'connect1_3'", + expected_stdout => qr/connect1_3/); + + local $ENV{PGSERVICE} = 'undefined-service'; + + $dummy_node->connect_fails( + '', + 'connection with incorrect PGSERVICE and PGSERVICEFILE', + expected_stdout => + qr/definition of service "undefined-service" not found/); +} + +# Checks case of incorrect service file. +{ + local $ENV{PGSERVICEFILE} = $srvfile_missing; + + $dummy_node->connect_fails( + 'service=my_srv', + 'connection with correct "service" string and incorrect PGSERVICEFILE', + expected_stderr => + qr/service file ".*pg_service_missing.conf" not found/); +} + +# Checks case of service file named "pg_service.conf" in PGSYSCONFDIR. +{ + # Create copy of valid file + my $srvfile_default = "$td/pg_service.conf"; + copy($srvfile_valid, $srvfile_default); + + $dummy_node->connect_ok( + 'service=my_srv', + 'connection with correct "service" string and pg_service.conf', + sql => "SELECT 'connect2_1'", + expected_stdout => qr/connect2_1/); + + $dummy_node->connect_ok( + 'postgres://?service=my_srv', + 'connection with correct "service" URI and default pg_service.conf', + sql => "SELECT 'connect2_2'", + expected_stdout => qr/connect2_2/); + + $dummy_node->connect_fails( + 'service=undefined-service', + 'connection with incorrect "service" string and default pg_service.conf', + expected_stderr => + qr/definition of service "undefined-service" not found/); + + local $ENV{PGSERVICE} = 'my_srv'; + + $dummy_node->connect_ok( + '', + 'connection with correct PGSERVICE and default pg_service.conf', + sql => "SELECT 'connect2_3'", + expected_stdout => qr/connect2_3/); + + local $ENV{PGSERVICE} = 'undefined-service'; + + $dummy_node->connect_fails( + '', + 'connection with incorrect PGSERVICE and default pg_service.conf', + expected_stdout => + qr/definition of service "undefined-service" not found/); + + # Remove default pg_service.conf. + unlink($srvfile_default); +} + +$node->teardown_node; + +done_testing(); diff --git a/src/test/locale/sort-test.pl b/src/test/locale/sort-test.pl index 7f0b6edeb771d..4e75d44b0002b 100755 --- a/src/test/locale/sort-test.pl +++ b/src/test/locale/sort-test.pl @@ -1,6 +1,6 @@ #! /usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/mb/expected/mule_internal.out b/src/test/mb/expected/mule_internal.out deleted file mode 100644 index ac8b57dc4216f..0000000000000 --- a/src/test/mb/expected/mule_internal.out +++ /dev/null @@ -1,333 +0,0 @@ -drop table ’·×’»»’µ¡’ÍÑ’¸ì; -ERROR: table "’·×’»»’µ¡’ÍÑ’¸ì" does not exist -create table ’·×’»»’µ¡’ÍÑ’¸ì (’ÍÑ’¸ì text, ’ʬ’Îà’¥³’¡¼’¥É varchar, ’È÷’¹Í1A’¤À’¤è char(16)); -create index ’·×’»»’µ¡’ÍÑ’¸ìindex1 on ’·×’»»’µ¡’ÍÑ’¸ì using btree (’ÍÑ’¸ì); -create index ’·×’»»’µ¡’ÍÑ’¸ìindex2 on ’·×’»»’µ¡’ÍÑ’¸ì using hash (’ʬ’Îà’¥³’¡¼’¥É); -insert into ’·×’»»’µ¡’ÍÑ’¸ì values('’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤','’µ¡A01’¾å'); -insert into ’·×’»»’µ¡’ÍÑ’¸ì values('’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹','’ʬB10’Ãæ'); -insert into ’·×’»»’µ¡’ÍÑ’¸ì values('’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼','’¿ÍZ01’²¼'); -vacuum ’·×’»»’µ¡’ÍÑ’¸ì; -select * from ’·×’»»’µ¡’ÍÑ’¸ì; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è -----------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | -(3 rows) - -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É = '’¿ÍZ01’²¼'; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è ---------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | -(1 row) - -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É ~* '’¿Íz01’²¼'; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è ---------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | -(1 row) - -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É like '_Z01_'; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è ---------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | -(1 row) - -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É like '_Z%'; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è ---------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | -(1 row) - -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ÍÑ’¸ì ~ '’¥³’¥ó’¥Ô’¥å’¡¼’¥¿[’¥Ç’¥°]'; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è -----------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | -(2 rows) - -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ÍÑ’¸ì ~* '’¥³’¥ó’¥Ô’¥å’¡¼’¥¿[’¥Ç’¥°]'; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è -----------------------------+------------+------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | -(2 rows) - -select *,character_length(’ÍÑ’¸ì) from ’·×’»»’µ¡’ÍÑ’¸ì; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è | character_length -----------------------------+------------+------------+------------------ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | | 12 - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | | 13 - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | | 12 -(3 rows) - -select *,octet_length(’ÍÑ’¸ì) from ’·×’»»’µ¡’ÍÑ’¸ì; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è | octet_length -----------------------------+------------+------------+-------------- - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | | 36 - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | | 39 - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | | 36 -(3 rows) - -select *,position('’¥Ç' in ’ÍÑ’¸ì) from ’·×’»»’µ¡’ÍÑ’¸ì; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è | position -----------------------------+------------+------------+---------- - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | | 7 - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | | 0 - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | | 0 -(3 rows) - -select *,substring(’ÍÑ’¸ì from 10 for 4) from ’·×’»»’µ¡’ÍÑ’¸ì; - ’ÍÑ’¸ì | ’ʬ’Îà’¥³’¡¼’¥É | ’È÷’¹Í1a’¤À’¤è | substring -----------------------------+------------+------------+----------- - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤ | ’µ¡A01’¾å | | ’¥×’¥ì’¥¤ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹ | ’ʬB10’Ãæ | | ’¥£’¥Ã’¥¯’¥¹ - ’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼ | ’¿ÍZ01’²¼ | | ’¥é’¥Þ’¡¼ -(3 rows) - -drop table ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -ERROR: table "‘¼Æ‘Ëã‘»ú‘Êõ‘Óï" does not exist -create table ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï(‘Êõ‘Óï text, ‘·Ö‘Àà‘ºÅ varchar, ‘±¸‘×¢1A char(16)); -create index ‘¼Æ‘Ëã‘»ú‘Êõ‘Óïindex1 on ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï using btree(‘Êõ‘Óï); -create index ‘¼Æ‘Ëã‘»ú‘Êõ‘Óïindex2 on ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï using btree(‘·Ö‘Àà‘ºÅ); -insert into ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï values('‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ','‘»úA01‘ÉÏ'); -insert into ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï values('‘µç‘Äԑͼ‘ÐÎ','‘·ÖB01‘ÖÐ'); -insert into ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï values('‘µç‘ÄÔ‘³Ì‘Ðò‘Ô±','‘ÈËZ01‘ÏÂ'); -vacuum ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | -(3 rows) - -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ = '‘ÈËZ01‘ÏÂ'; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | -(1 row) - -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ ~* '‘ÈËz01‘ÏÂ'; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | -(1 row) - -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ like '_Z01_'; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | -(1 row) - -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ like '_Z%'; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | -(1 row) - -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘Êõ‘Óï ~ '‘µç‘ÄÔ[‘Ïԑͼ]'; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | -(2 rows) - -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘Êõ‘Óï ~* '‘µç‘ÄÔ[‘Ïԑͼ]'; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a -------------+---------+-------- - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | -(2 rows) - -select *,character_length(‘Êõ‘Óï) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a | character_length -------------+---------+--------+------------------ - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | | 5 - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | | 4 - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | | 5 -(3 rows) - -select *,octet_length(‘Êõ‘Óï) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a | octet_length -------------+---------+--------+-------------- - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | | 15 - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | | 12 - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | | 15 -(3 rows) - -select *,position('‘ÏÔ' in ‘Êõ‘Óï) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a | position -------------+---------+--------+---------- - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | | 3 - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | | 0 - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | | 0 -(3 rows) - -select *,substring(‘Êõ‘Óï from 3 for 4) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; - ‘Êõ‘Óï | ‘·Ö‘Àà‘ºÅ | ‘±¸‘×¢1a | substring -------------+---------+--------+----------- - ‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ | ‘»úA01‘ÉÏ | | ‘Ïԑʾ‘ÆÁ - ‘µç‘Äԑͼ‘ÐÎ | ‘·ÖB01‘ÖÐ | | ‘ͼ‘ÐÎ - ‘µç‘ÄÔ‘³Ì‘Ðò‘Ô± | ‘ÈËZ01‘Ï | | ‘³Ì‘Ðò‘Ô± -(3 rows) - -drop table “ͪ“ß©“Ѧ“¿ë“¾î; -ERROR: table "“ͪ“ß©“Ѧ“¿ë“¾î" does not exist -create table “ͪ“ß©“Ѧ“¿ë“¾î (“¿ë“¾î text, “Ý“׾“ÄÚ“µå varchar, “ºñ“°í1A“¶ó“±¸ char(16)); -create index “ͪ“ß©“Ѧ“¿ë“¾îindex1 on “ͪ“ß©“Ѧ“¿ë“¾î using btree (“¿ë“¾î); -create index “ͪ“ß©“Ѧ“¿ë“¾îindex2 on “ͪ“ß©“Ѧ“¿ë“¾î using hash (“Ý“׾“ÄÚ“µå); -insert into “ͪ“ß©“Ѧ“¿ë“¾î values('“Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ', '“ѦA01“ß¾'); -insert into “ͪ“ß©“Ѧ“¿ë“¾î values('“Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º', '“ÝÂB10“ñé'); -insert into “ͪ“ß©“Ѧ“¿ë“¾î values('“Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó', '“ìÑZ01“ù»'); -vacuum “ͪ“ß©“Ѧ“¿ë“¾î; -select * from “ͪ“ß©“Ѧ“¿ë“¾î; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | -(3 rows) - -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå = '“ìÑZ01“ù»'; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | -(1 row) - -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå ~* '“ìÑz01“ù»'; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | -(1 row) - -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå like '_Z01_'; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | -(1 row) - -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå like '_Z%'; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | -(1 row) - -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “¿ë“¾î ~ '“Äēǻ“ÅÍ[“µð“±×]'; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | -(2 rows) - -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “¿ë“¾î ~* '“Äēǻ“ÅÍ[“µð“±×]'; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ -------------------+----------+------------ - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | -(2 rows) - -select *,character_length(“¿ë“¾î) from “ͪ“ß©“Ѧ“¿ë“¾î; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ | character_length -------------------+----------+------------+------------------ - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | | 8 - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | | 7 - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | | 8 -(3 rows) - -select *,octet_length(“¿ë“¾î) from “ͪ“ß©“Ѧ“¿ë“¾î; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ | octet_length -------------------+----------+------------+-------------- - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | | 24 - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | | 21 - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | | 24 -(3 rows) - -select *,position('“µð' in “¿ë“¾î) from “ͪ“ß©“Ѧ“¿ë“¾î; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ | position -------------------+----------+------------+---------- - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | | 4 - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | | 0 - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | | 0 -(3 rows) - -select *,substring(“¿ë“¾î from 3 for 4) from “ͪ“ß©“Ѧ“¿ë“¾î; - “¿ë“¾î | “Ý“׾“ÄÚ“µå | “ºñ“°í1a“¶ó“±¸ | substring -------------------+----------+------------+----------- - “Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ | “ѦA01“ß¾ | | “ÅÍ“µð“½º“Çà - “Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º | “ÝÂB10“ñé | | “ÅÍ“±×“·¡“ÇÈ - “Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó | “ìÑZ01“ù» | | “ÅÍ“ÇÁ“·Î“±× -(3 rows) - -drop table test; -ERROR: table "test" does not exist -create table test (t text); -insert into test values('ENGLISH'); -insert into test values('FRANÇAIS'); -insert into test values('ESPAÑOL'); -insert into test values('ÍSLENSKA'); -insert into test values('ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA'); -vacuum test; -select * from test; - t ------------------------------------ - ENGLISH - FRANÇAIS - ESPAÑOL - ÍSLENSKA - ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA -(5 rows) - -select * from test where t = 'ESPAÑOL'; - t ---------- - ESPAÑOL -(1 row) - -select * from test where t ~* 'espaÑol'; - t ------------------------------------ - ESPAÑOL - ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA -(2 rows) - -select *,character_length(t) from test; - t | character_length ------------------------------------+------------------ - ENGLISH | 7 - FRANÇAIS | 8 - ESPAÑOL | 7 - ÍSLENSKA | 8 - ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA | 33 -(5 rows) - -select *,octet_length(t) from test; - t | octet_length ------------------------------------+-------------- - ENGLISH | 7 - FRANÇAIS | 9 - ESPAÑOL | 8 - ÍSLENSKA | 9 - ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA | 36 -(5 rows) - -select *,position('L' in t) from test; - t | position ------------------------------------+---------- - ENGLISH | 4 - FRANÇAIS | 0 - ESPAÑOL | 7 - ÍSLENSKA | 3 - ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA | 4 -(5 rows) - -select *,substring(t from 3 for 4) from test; - t | substring ------------------------------------+----------- - ENGLISH | GLIS - FRANÇAIS | ANÇA - ESPAÑOL | PAÑO - ÍSLENSKA | LENS - ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA | GLIS -(5 rows) - diff --git a/src/test/mb/mbregress.sh b/src/test/mb/mbregress.sh index c313565fb4b73..b650b31074f98 100755 --- a/src/test/mb/mbregress.sh +++ b/src/test/mb/mbregress.sh @@ -21,7 +21,7 @@ PSQL="psql -X -n -e -q" # in the test list, client-only encodings must follow the server encoding # they're to be tested with; see hard-coded cases below -tests="euc_jp sjis euc_kr euc_cn euc_tw big5 utf8 gb18030 mule_internal" +tests="euc_jp sjis euc_kr euc_cn euc_tw big5 utf8 gb18030" EXITCODE=0 diff --git a/src/test/mb/sql/mule_internal.sql b/src/test/mb/sql/mule_internal.sql deleted file mode 100644 index 2e381f0f7ed12..0000000000000 --- a/src/test/mb/sql/mule_internal.sql +++ /dev/null @@ -1,72 +0,0 @@ -drop table ’·×’»»’µ¡’ÍÑ’¸ì; -create table ’·×’»»’µ¡’ÍÑ’¸ì (’ÍÑ’¸ì text, ’ʬ’Îà’¥³’¡¼’¥É varchar, ’È÷’¹Í1A’¤À’¤è char(16)); -create index ’·×’»»’µ¡’ÍÑ’¸ìindex1 on ’·×’»»’µ¡’ÍÑ’¸ì using btree (’ÍÑ’¸ì); -create index ’·×’»»’µ¡’ÍÑ’¸ìindex2 on ’·×’»»’µ¡’ÍÑ’¸ì using hash (’ʬ’Îà’¥³’¡¼’¥É); -insert into ’·×’»»’µ¡’ÍÑ’¸ì values('’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥Ç’¥£’¥¹’¥×’¥ì’¥¤','’µ¡A01’¾å'); -insert into ’·×’»»’µ¡’ÍÑ’¸ì values('’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥°’¥é’¥Õ’¥£’¥Ã’¥¯’¥¹','’ʬB10’Ãæ'); -insert into ’·×’»»’µ¡’ÍÑ’¸ì values('’¥³’¥ó’¥Ô’¥å’¡¼’¥¿’¥×’¥í’¥°’¥é’¥Þ’¡¼','’¿ÍZ01’²¼'); -vacuum ’·×’»»’µ¡’ÍÑ’¸ì; -select * from ’·×’»»’µ¡’ÍÑ’¸ì; -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É = '’¿ÍZ01’²¼'; -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É ~* '’¿Íz01’²¼'; -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É like '_Z01_'; -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ʬ’Îà’¥³’¡¼’¥É like '_Z%'; -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ÍÑ’¸ì ~ '’¥³’¥ó’¥Ô’¥å’¡¼’¥¿[’¥Ç’¥°]'; -select * from ’·×’»»’µ¡’ÍÑ’¸ì where ’ÍÑ’¸ì ~* '’¥³’¥ó’¥Ô’¥å’¡¼’¥¿[’¥Ç’¥°]'; -select *,character_length(’ÍÑ’¸ì) from ’·×’»»’µ¡’ÍÑ’¸ì; -select *,octet_length(’ÍÑ’¸ì) from ’·×’»»’µ¡’ÍÑ’¸ì; -select *,position('’¥Ç' in ’ÍÑ’¸ì) from ’·×’»»’µ¡’ÍÑ’¸ì; -select *,substring(’ÍÑ’¸ì from 10 for 4) from ’·×’»»’µ¡’ÍÑ’¸ì; -drop table ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -create table ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï(‘Êõ‘Óï text, ‘·Ö‘Àà‘ºÅ varchar, ‘±¸‘×¢1A char(16)); -create index ‘¼Æ‘Ëã‘»ú‘Êõ‘Óïindex1 on ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï using btree(‘Êõ‘Óï); -create index ‘¼Æ‘Ëã‘»ú‘Êõ‘Óïindex2 on ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï using btree(‘·Ö‘Àà‘ºÅ); -insert into ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï values('‘µç‘ÄÔ‘Ïԑʾ‘ÆÁ','‘»úA01‘ÉÏ'); -insert into ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï values('‘µç‘Äԑͼ‘ÐÎ','‘·ÖB01‘ÖÐ'); -insert into ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï values('‘µç‘ÄÔ‘³Ì‘Ðò‘Ô±','‘ÈËZ01‘ÏÂ'); -vacuum ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ = '‘ÈËZ01‘ÏÂ'; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ ~* '‘ÈËz01‘ÏÂ'; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ like '_Z01_'; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘·Ö‘Àà‘ºÅ like '_Z%'; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘Êõ‘Óï ~ '‘µç‘ÄÔ[‘Ïԑͼ]'; -select * from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï where ‘Êõ‘Óï ~* '‘µç‘ÄÔ[‘Ïԑͼ]'; -select *,character_length(‘Êõ‘Óï) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -select *,octet_length(‘Êõ‘Óï) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -select *,position('‘ÏÔ' in ‘Êõ‘Óï) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -select *,substring(‘Êõ‘Óï from 3 for 4) from ‘¼Æ‘Ëã‘»ú‘Êõ‘Óï; -drop table “ͪ“ß©“Ѧ“¿ë“¾î; -create table “ͪ“ß©“Ѧ“¿ë“¾î (“¿ë“¾î text, “Ý“׾“ÄÚ“µå varchar, “ºñ“°í1A“¶ó“±¸ char(16)); -create index “ͪ“ß©“Ѧ“¿ë“¾îindex1 on “ͪ“ß©“Ѧ“¿ë“¾î using btree (“¿ë“¾î); -create index “ͪ“ß©“Ѧ“¿ë“¾îindex2 on “ͪ“ß©“Ѧ“¿ë“¾î using hash (“Ý“׾“ÄÚ“µå); -insert into “ͪ“ß©“Ѧ“¿ë“¾î values('“Äēǻ“ÅÍ“µð“½º“ÇÓ·¹“ÀÌ', '“ѦA01“ß¾'); -insert into “ͪ“ß©“Ѧ“¿ë“¾î values('“Äēǻ“ÅÍ“±×“·¡“ÇÈ“½º', '“ÝÂB10“ñé'); -insert into “ͪ“ß©“Ѧ“¿ë“¾î values('“Äēǻ“ÅÍ“ÇÁ“·Î“±×“·¡“¸Ó', '“ìÑZ01“ù»'); -vacuum “ͪ“ß©“Ѧ“¿ë“¾î; -select * from “ͪ“ß©“Ѧ“¿ë“¾î; -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå = '“ìÑZ01“ù»'; -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå ~* '“ìÑz01“ù»'; -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå like '_Z01_'; -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “Ý“׾“ÄÚ“µå like '_Z%'; -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “¿ë“¾î ~ '“Äēǻ“ÅÍ[“µð“±×]'; -select * from “ͪ“ß©“Ѧ“¿ë“¾î where “¿ë“¾î ~* '“Äēǻ“ÅÍ[“µð“±×]'; -select *,character_length(“¿ë“¾î) from “ͪ“ß©“Ѧ“¿ë“¾î; -select *,octet_length(“¿ë“¾î) from “ͪ“ß©“Ѧ“¿ë“¾î; -select *,position('“µð' in “¿ë“¾î) from “ͪ“ß©“Ѧ“¿ë“¾î; -select *,substring(“¿ë“¾î from 3 for 4) from “ͪ“ß©“Ѧ“¿ë“¾î; -drop table test; -create table test (t text); -insert into test values('ENGLISH'); -insert into test values('FRANÇAIS'); -insert into test values('ESPAÑOL'); -insert into test values('ÍSLENSKA'); -insert into test values('ENGLISH FRANÇAIS ESPAÑOL ÍSLENSKA'); -vacuum test; -select * from test; -select * from test where t = 'ESPAÑOL'; -select * from test where t ~* 'espaÑol'; -select *,character_length(t) from test; -select *,octet_length(t) from test; -select *,position('L' in t) from test; -select *,substring(t from 3 for 4) from test; diff --git a/src/test/meson.build b/src/test/meson.build index ccc31d6a86a1b..cd45cbf57fb0f 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('regress') subdir('isolation') diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index aa1d27bbed310..0a74ab5c86f51 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -10,33 +10,46 @@ SUBDIRS = \ delay_execution \ dummy_index_am \ dummy_seclabel \ + index \ libpq_pipeline \ oauth_validator \ plsample \ spgist_name_ops \ test_aio \ + test_autovacuum \ + test_binaryheap \ + test_bitmapset \ test_bloomfilter \ + test_cloexec \ + test_checksums \ test_copy_callbacks \ test_custom_rmgrs \ + test_custom_stats \ + test_custom_types \ test_ddl_deparse \ test_dsa \ test_dsm_registry \ test_escape \ test_extensions \ test_ginpostinglist \ + test_int128 \ test_integerset \ test_json_parser \ test_lfind \ + test_lwlock_tranches \ test_misc \ test_oat_hooks \ test_parser \ test_pg_dump \ + test_plan_advice \ test_predtest \ test_radixtree \ test_rbtree \ test_regex \ test_resowner \ test_rls_hooks \ + test_saslprep \ + test_shmem \ test_shm_mq \ test_slru \ test_tidstore \ @@ -68,5 +81,11 @@ else ALWAYS_SUBDIRS += ldap_password_func endif +ifeq ($(have_cxx),yes) +SUBDIRS += test_cplusplusext +else +ALWAYS_SUBDIRS += test_cplusplusext +endif + $(recurse) $(recurse_always) diff --git a/src/test/modules/brin/meson.build b/src/test/modules/brin/meson.build index e88a7c39c90b2..39a8b2fc925c0 100644 --- a/src/test/modules/brin/meson.build +++ b/src/test/modules/brin/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'brin', diff --git a/src/test/modules/brin/t/01_workitems.pl b/src/test/modules/brin/t/01_workitems.pl index c3b1fb517062d..ffcca3d71323b 100644 --- a/src/test/modules/brin/t/01_workitems.pl +++ b/src/test/modules/brin/t/01_workitems.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify that work items work correctly @@ -24,23 +24,61 @@ create index brin_wi_idx on brin_wi using brin (a) with (pages_per_range=1, autosummarize=on); ' ); +# Another table with an index that requires a snapshot to run +$node->safe_psql( + 'postgres', + 'create table journal (d timestamp) with (fillfactor = 10); + create function packdate(d timestamp) returns text language plpgsql + as $$ begin return to_char(d, \'yyyymm\'); end; $$ + returns null on null input immutable; + create index brin_packdate_idx on journal using brin (packdate(d)) + with (autosummarize = on, pages_per_range = 1); + ' +); + my $count = $node->safe_psql('postgres', "select count(*) from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass)" ); -is($count, '1', "initial index state is correct"); +is($count, '1', "initial brin_wi_idx index state is correct"); +$count = $node->safe_psql('postgres', + "select count(*) from brin_page_items(get_raw_page('brin_packdate_idx', 2), 'brin_packdate_idx'::regclass)" +); +is($count, '1', "initial brin_packdate_idx index state is correct"); $node->safe_psql('postgres', 'insert into brin_wi select * from generate_series(1, 100)'); +$node->safe_psql('postgres', + "insert into journal select * from generate_series(timestamp '1976-08-01', '1976-10-28', '1 day')" +); + +# Give a little time for autovacuum to react. This matches the naptime +# configured above. +sleep(1); $node->poll_query_until( 'postgres', "select count(*) > 1 from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass)", 't'); -$count = $node->safe_psql('postgres', - "select count(*) > 1 from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass)" +$count = $node->safe_psql( + 'postgres', + "select count(*) from brin_page_items(get_raw_page('brin_wi_idx', 2), 'brin_wi_idx'::regclass) + where not placeholder;" +); +cmp_ok($count, '>', '1', "$count brin_wi_idx ranges got summarized"); + +$node->poll_query_until( + 'postgres', + "select count(*) > 1 from brin_page_items(get_raw_page('brin_packdate_idx', 2), 'brin_packdate_idx'::regclass)", + 't'); + +$count = $node->safe_psql( + 'postgres', + "select count(*) from brin_page_items(get_raw_page('brin_packdate_idx', 2), 'brin_packdate_idx'::regclass) + where not placeholder;" ); -is($count, 't', "index got summarized"); +cmp_ok($count, '>', '1', "$count brin_packdate_idx ranges got summarized"); + $node->stop; done_testing(); diff --git a/src/test/modules/brin/t/02_wal_consistency.pl b/src/test/modules/brin/t/02_wal_consistency.pl index 66919ab391cdc..e2dfd252bf121 100644 --- a/src/test/modules/brin/t/02_wal_consistency.pl +++ b/src/test/modules/brin/t/02_wal_consistency.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify WAL consistency diff --git a/src/test/modules/commit_ts/meson.build b/src/test/modules/commit_ts/meson.build index 27598253e020a..d8ee6ec426d1d 100644 --- a/src/test/modules/commit_ts/meson.build +++ b/src/test/modules/commit_ts/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'commit_ts', diff --git a/src/test/modules/commit_ts/t/001_base.pl b/src/test/modules/commit_ts/t/001_base.pl index 1953b18f6b3c3..0b664b6f2d491 100644 --- a/src/test/modules/commit_ts/t/001_base.pl +++ b/src/test/modules/commit_ts/t/001_base.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Single-node test: value can be set, and is still present after recovery @@ -11,8 +11,7 @@ use PostgreSQL::Test::Cluster; my $node = PostgreSQL::Test::Cluster->new('foxtrot'); -$node->init; -$node->append_conf('postgresql.conf', 'track_commit_timestamp = on'); +$node->init(extra => [ '-c', "track_commit_timestamp=on" ]); $node->start; # Create a table, compare "now()" to the commit TS of its xmin diff --git a/src/test/modules/commit_ts/t/002_standby.pl b/src/test/modules/commit_ts/t/002_standby.pl index f76d474270bd0..3b64628e67796 100644 --- a/src/test/modules/commit_ts/t/002_standby.pl +++ b/src/test/modules/commit_ts/t/002_standby.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test simple scenario involving a standby diff --git a/src/test/modules/commit_ts/t/003_standby_2.pl b/src/test/modules/commit_ts/t/003_standby_2.pl index 3b27da6b23752..faedade7724a9 100644 --- a/src/test/modules/commit_ts/t/003_standby_2.pl +++ b/src/test/modules/commit_ts/t/003_standby_2.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test primary/standby scenario where the track_commit_timestamp GUC is # repeatedly toggled on and off. diff --git a/src/test/modules/commit_ts/t/004_restart.pl b/src/test/modules/commit_ts/t/004_restart.pl index b31bbbe24ed0c..c4e8438c61619 100644 --- a/src/test/modules/commit_ts/t/004_restart.pl +++ b/src/test/modules/commit_ts/t/004_restart.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Testing of commit timestamps preservation across restarts use strict; diff --git a/src/test/modules/delay_execution/delay_execution.c b/src/test/modules/delay_execution/delay_execution.c index 7bc97f84a1c3c..e6008ed2079a8 100644 --- a/src/test/modules/delay_execution/delay_execution.c +++ b/src/test/modules/delay_execution/delay_execution.c @@ -10,7 +10,7 @@ * test behaviors where some specified action happens in another backend * between parsing and execution of any desired query. * - * Copyright (c) 2020-2025, PostgreSQL Global Development Group + * Copyright (c) 2020-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/delay_execution/delay_execution.c @@ -40,17 +40,18 @@ static planner_hook_type prev_planner_hook = NULL; /* planner_hook function to provide the desired delay */ static PlannedStmt * delay_execution_planner(Query *parse, const char *query_string, - int cursorOptions, ParamListInfo boundParams) + int cursorOptions, ParamListInfo boundParams, + ExplainState *es) { PlannedStmt *result; /* Invoke the planner, possibly via a previous hook user */ if (prev_planner_hook) result = prev_planner_hook(parse, query_string, cursorOptions, - boundParams); + boundParams, es); else result = standard_planner(parse, query_string, cursorOptions, - boundParams); + boundParams, es); /* If enabled, delay by taking and releasing the specified lock */ if (post_planning_lock_id != 0) diff --git a/src/test/modules/delay_execution/meson.build b/src/test/modules/delay_execution/meson.build index b53488f76d2ad..ffdbe9b7215cc 100644 --- a/src/test/modules/delay_execution/meson.build +++ b/src/test/modules/delay_execution/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group delay_execution_sources = files( 'delay_execution.c', diff --git a/src/test/modules/dummy_index_am/README b/src/test/modules/dummy_index_am/README index 61510f02faea3..604d823c2e442 100644 --- a/src/test/modules/dummy_index_am/README +++ b/src/test/modules/dummy_index_am/README @@ -5,7 +5,7 @@ Dummy index AM is a module for testing any facility usable by an index access method, whose code is kept a maximum simple. This includes tests for all relation option types: -- boolean +- boolean & ternary - enum - integer - real diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c index 94ef639b6fcdb..31f8d2b816155 100644 --- a/src/test/modules/dummy_index_am/dummy_index_am.c +++ b/src/test/modules/dummy_index_am/dummy_index_am.c @@ -3,7 +3,7 @@ * dummy_index_am.c * Index AM template main file. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -22,7 +22,7 @@ PG_MODULE_MAGIC; /* parse table for fillRelOptions */ -static relopt_parse_elt di_relopt_tab[6]; +static relopt_parse_elt di_relopt_tab[8]; /* Kind of relation options for dummy index */ static relopt_kind di_relopt_kind; @@ -40,6 +40,7 @@ typedef struct DummyIndexOptions int option_int; double option_real; bool option_bool; + pg_ternary option_ternary_1; DummyAmEnum option_enum; int option_string_val_offset; int option_string_null_offset; @@ -73,28 +74,41 @@ validate_string_option(const char *value) static void create_reloptions_table(void) { + int i = 0; + di_relopt_kind = add_reloption_kind(); add_int_reloption(di_relopt_kind, "option_int", "Integer option for dummy_index_am", 10, -10, 100, AccessExclusiveLock); - di_relopt_tab[0].optname = "option_int"; - di_relopt_tab[0].opttype = RELOPT_TYPE_INT; - di_relopt_tab[0].offset = offsetof(DummyIndexOptions, option_int); + di_relopt_tab[i].optname = "option_int"; + di_relopt_tab[i].opttype = RELOPT_TYPE_INT; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_int); + i++; add_real_reloption(di_relopt_kind, "option_real", "Real option for dummy_index_am", 3.1415, -10, 100, AccessExclusiveLock); - di_relopt_tab[1].optname = "option_real"; - di_relopt_tab[1].opttype = RELOPT_TYPE_REAL; - di_relopt_tab[1].offset = offsetof(DummyIndexOptions, option_real); + di_relopt_tab[i].optname = "option_real"; + di_relopt_tab[i].opttype = RELOPT_TYPE_REAL; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_real); + i++; add_bool_reloption(di_relopt_kind, "option_bool", "Boolean option for dummy_index_am", true, AccessExclusiveLock); - di_relopt_tab[2].optname = "option_bool"; - di_relopt_tab[2].opttype = RELOPT_TYPE_BOOL; - di_relopt_tab[2].offset = offsetof(DummyIndexOptions, option_bool); + di_relopt_tab[i].optname = "option_bool"; + di_relopt_tab[i].opttype = RELOPT_TYPE_BOOL; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_bool); + i++; + + add_ternary_reloption(di_relopt_kind, "option_ternary_1", + "One ternary option for dummy_index_am", + AccessExclusiveLock); + di_relopt_tab[i].optname = "option_ternary_1"; + di_relopt_tab[i].opttype = RELOPT_TYPE_TERNARY; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_ternary_1); + i++; add_enum_reloption(di_relopt_kind, "option_enum", "Enum option for dummy_index_am", @@ -102,18 +116,20 @@ create_reloptions_table(void) DUMMY_AM_ENUM_ONE, "Valid values are \"one\" and \"two\".", AccessExclusiveLock); - di_relopt_tab[3].optname = "option_enum"; - di_relopt_tab[3].opttype = RELOPT_TYPE_ENUM; - di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_enum); + di_relopt_tab[i].optname = "option_enum"; + di_relopt_tab[i].opttype = RELOPT_TYPE_ENUM; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_enum); + i++; add_string_reloption(di_relopt_kind, "option_string_val", "String option for dummy_index_am with non-NULL default", "DefaultValue", &validate_string_option, AccessExclusiveLock); - di_relopt_tab[4].optname = "option_string_val"; - di_relopt_tab[4].opttype = RELOPT_TYPE_STRING; - di_relopt_tab[4].offset = offsetof(DummyIndexOptions, + di_relopt_tab[i].optname = "option_string_val"; + di_relopt_tab[i].opttype = RELOPT_TYPE_STRING; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_string_val_offset); + i++; /* * String option for dummy_index_am with NULL default, and without @@ -123,10 +139,11 @@ create_reloptions_table(void) NULL, /* description */ NULL, &validate_string_option, AccessExclusiveLock); - di_relopt_tab[5].optname = "option_string_null"; - di_relopt_tab[5].opttype = RELOPT_TYPE_STRING; - di_relopt_tab[5].offset = offsetof(DummyIndexOptions, + di_relopt_tab[i].optname = "option_string_null"; + di_relopt_tab[i].opttype = RELOPT_TYPE_STRING; + di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_string_null_offset); + i++; } @@ -138,7 +155,7 @@ dibuild(Relation heap, Relation index, IndexInfo *indexInfo) { IndexBuildResult *result; - result = (IndexBuildResult *) palloc(sizeof(IndexBuildResult)); + result = palloc_object(IndexBuildResult); /* let's pretend that no tuples were scanned */ result->heap_tuples = 0; @@ -276,56 +293,57 @@ diendscan(IndexScanDesc scan) Datum dihandler(PG_FUNCTION_ARGS) { - IndexAmRoutine *amroutine = makeNode(IndexAmRoutine); - - amroutine->amstrategies = 0; - amroutine->amsupport = 1; - amroutine->amcanorder = false; - amroutine->amcanorderbyop = false; - amroutine->amcanhash = false; - amroutine->amconsistentequality = false; - amroutine->amconsistentordering = false; - amroutine->amcanbackward = false; - amroutine->amcanunique = false; - amroutine->amcanmulticol = false; - amroutine->amoptionalkey = false; - amroutine->amsearcharray = false; - amroutine->amsearchnulls = false; - amroutine->amstorage = false; - amroutine->amclusterable = false; - amroutine->ampredlocks = false; - amroutine->amcanparallel = false; - amroutine->amcanbuildparallel = false; - amroutine->amcaninclude = false; - amroutine->amusemaintenanceworkmem = false; - amroutine->amsummarizing = false; - amroutine->amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL; - amroutine->amkeytype = InvalidOid; - - amroutine->ambuild = dibuild; - amroutine->ambuildempty = dibuildempty; - amroutine->aminsert = diinsert; - amroutine->ambulkdelete = dibulkdelete; - amroutine->amvacuumcleanup = divacuumcleanup; - amroutine->amcanreturn = NULL; - amroutine->amcostestimate = dicostestimate; - amroutine->amgettreeheight = NULL; - amroutine->amoptions = dioptions; - amroutine->amproperty = NULL; - amroutine->ambuildphasename = NULL; - amroutine->amvalidate = divalidate; - amroutine->ambeginscan = dibeginscan; - amroutine->amrescan = direscan; - amroutine->amgettuple = NULL; - amroutine->amgetbitmap = NULL; - amroutine->amendscan = diendscan; - amroutine->ammarkpos = NULL; - amroutine->amrestrpos = NULL; - amroutine->amestimateparallelscan = NULL; - amroutine->aminitparallelscan = NULL; - amroutine->amparallelrescan = NULL; - - PG_RETURN_POINTER(amroutine); + static const IndexAmRoutine amroutine = { + .type = T_IndexAmRoutine, + .amstrategies = 0, + .amsupport = 1, + .amcanorder = false, + .amcanorderbyop = false, + .amcanhash = false, + .amconsistentequality = false, + .amconsistentordering = false, + .amcanbackward = false, + .amcanunique = false, + .amcanmulticol = false, + .amoptionalkey = false, + .amsearcharray = false, + .amsearchnulls = false, + .amstorage = false, + .amclusterable = false, + .ampredlocks = false, + .amcanparallel = false, + .amcanbuildparallel = false, + .amcaninclude = false, + .amusemaintenanceworkmem = false, + .amsummarizing = false, + .amparallelvacuumoptions = VACUUM_OPTION_NO_PARALLEL, + .amkeytype = InvalidOid, + + .ambuild = dibuild, + .ambuildempty = dibuildempty, + .aminsert = diinsert, + .ambulkdelete = dibulkdelete, + .amvacuumcleanup = divacuumcleanup, + .amcanreturn = NULL, + .amcostestimate = dicostestimate, + .amgettreeheight = NULL, + .amoptions = dioptions, + .amproperty = NULL, + .ambuildphasename = NULL, + .amvalidate = divalidate, + .ambeginscan = dibeginscan, + .amrescan = direscan, + .amgettuple = NULL, + .amgetbitmap = NULL, + .amendscan = diendscan, + .ammarkpos = NULL, + .amrestrpos = NULL, + .amestimateparallelscan = NULL, + .aminitparallelscan = NULL, + .amparallelrescan = NULL, + }; + + PG_RETURN_POINTER(&amroutine); } void diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out index c873a80bb75d7..3b06d51499516 100644 --- a/src/test/modules/dummy_index_am/expected/reloptions.out +++ b/src/test/modules/dummy_index_am/expected/reloptions.out @@ -18,6 +18,7 @@ SET client_min_messages TO 'notice'; CREATE INDEX dummy_test_idx ON dummy_test_tab USING dummy_index_am (i) WITH ( option_bool = false, + option_ternary_1, option_int = 5, option_real = 3.1, option_enum = 'two', @@ -31,16 +32,18 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; unnest ------------------------ option_bool=false + option_ternary_1=true option_int=5 option_real=3.1 option_enum=two option_string_val=null option_string_null=val -(6 rows) +(7 rows) -- ALTER INDEX .. SET ALTER INDEX dummy_test_idx SET (option_int = 10); ALTER INDEX dummy_test_idx SET (option_bool = true); +ALTER INDEX dummy_test_idx SET (option_ternary_1 = false); ALTER INDEX dummy_test_idx SET (option_real = 3.2); ALTER INDEX dummy_test_idx SET (option_string_val = 'val2'); ALTER INDEX dummy_test_idx SET (option_string_null = NULL); @@ -53,15 +56,17 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; ------------------------- option_int=10 option_bool=true + option_ternary_1=false option_real=3.2 option_string_val=val2 option_string_null=null option_enum=one -(6 rows) +(7 rows) -- ALTER INDEX .. RESET ALTER INDEX dummy_test_idx RESET (option_int); ALTER INDEX dummy_test_idx RESET (option_bool); +ALTER INDEX dummy_test_idx RESET (option_ternary_1); ALTER INDEX dummy_test_idx RESET (option_real); ALTER INDEX dummy_test_idx RESET (option_enum); ALTER INDEX dummy_test_idx RESET (option_string_val); @@ -100,6 +105,21 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; (1 row) ALTER INDEX dummy_test_idx RESET (option_bool); +-- Ternary +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 4); -- error +ERROR: invalid value for boolean option "option_ternary_1": 4 +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 1); -- ok, as true +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 3.4); -- error +ERROR: invalid value for boolean option "option_ternary_1": 3.4 +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 'val4'); -- error +ERROR: invalid value for boolean option "option_ternary_1": val4 +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; + unnest +-------------------- + option_ternary_1=1 +(1 row) + +ALTER INDEX dummy_test_idx RESET (option_ternary_1); -- Float ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok ALTER INDEX dummy_test_idx SET (option_real = true); -- error diff --git a/src/test/modules/dummy_index_am/meson.build b/src/test/modules/dummy_index_am/meson.build index 6430eefc81e47..9cedfd2cf8bfd 100644 --- a/src/test/modules/dummy_index_am/meson.build +++ b/src/test/modules/dummy_index_am/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group dummy_index_am_sources = files( 'dummy_index_am.c', diff --git a/src/test/modules/dummy_index_am/sql/reloptions.sql b/src/test/modules/dummy_index_am/sql/reloptions.sql index 6749d763e6aa4..2cdff0820f65f 100644 --- a/src/test/modules/dummy_index_am/sql/reloptions.sql +++ b/src/test/modules/dummy_index_am/sql/reloptions.sql @@ -18,6 +18,7 @@ SET client_min_messages TO 'notice'; CREATE INDEX dummy_test_idx ON dummy_test_tab USING dummy_index_am (i) WITH ( option_bool = false, + option_ternary_1, option_int = 5, option_real = 3.1, option_enum = 'two', @@ -30,6 +31,7 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; -- ALTER INDEX .. SET ALTER INDEX dummy_test_idx SET (option_int = 10); ALTER INDEX dummy_test_idx SET (option_bool = true); +ALTER INDEX dummy_test_idx SET (option_ternary_1 = false); ALTER INDEX dummy_test_idx SET (option_real = 3.2); ALTER INDEX dummy_test_idx SET (option_string_val = 'val2'); ALTER INDEX dummy_test_idx SET (option_string_null = NULL); @@ -40,6 +42,7 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; -- ALTER INDEX .. RESET ALTER INDEX dummy_test_idx RESET (option_int); ALTER INDEX dummy_test_idx RESET (option_bool); +ALTER INDEX dummy_test_idx RESET (option_ternary_1); ALTER INDEX dummy_test_idx RESET (option_real); ALTER INDEX dummy_test_idx RESET (option_enum); ALTER INDEX dummy_test_idx RESET (option_string_val); @@ -60,6 +63,13 @@ ALTER INDEX dummy_test_idx SET (option_bool = 3.4); -- error ALTER INDEX dummy_test_idx SET (option_bool = 'val4'); -- error SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; ALTER INDEX dummy_test_idx RESET (option_bool); +-- Ternary +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 4); -- error +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 1); -- ok, as true +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 3.4); -- error +ALTER INDEX dummy_test_idx SET (option_ternary_1 = 'val4'); -- error +SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx'; +ALTER INDEX dummy_test_idx RESET (option_ternary_1); -- Float ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok ALTER INDEX dummy_test_idx SET (option_real = true); -- error diff --git a/src/test/modules/dummy_seclabel/dummy_seclabel.c b/src/test/modules/dummy_seclabel/dummy_seclabel.c index 0b09f8a9c243d..7277f61902d5d 100644 --- a/src/test/modules/dummy_seclabel/dummy_seclabel.c +++ b/src/test/modules/dummy_seclabel/dummy_seclabel.c @@ -7,7 +7,7 @@ * perspective, but allows regression testing independent of platform-specific * features like SELinux. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California */ #include "postgres.h" diff --git a/src/test/modules/dummy_seclabel/meson.build b/src/test/modules/dummy_seclabel/meson.build index 9269a02d8aa0f..0bc5a53edf5f7 100644 --- a/src/test/modules/dummy_seclabel/meson.build +++ b/src/test/modules/dummy_seclabel/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group dummy_seclabel_sources = files( 'dummy_seclabel.c', diff --git a/src/test/modules/gin/expected/gin_incomplete_splits.out b/src/test/modules/gin/expected/gin_incomplete_splits.out index 15574e547ac9a..0f3ac9a04660b 100644 --- a/src/test/modules/gin/expected/gin_incomplete_splits.out +++ b/src/test/modules/gin/expected/gin_incomplete_splits.out @@ -192,3 +192,4 @@ SELECT injection_points_detach('gin-finish-incomplete-split'); (1 row) +drop extension injection_points; diff --git a/src/test/modules/gin/meson.build b/src/test/modules/gin/meson.build index 12465ce187bec..ae2c1abe3687f 100644 --- a/src/test/modules/gin/meson.build +++ b/src/test/modules/gin/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not get_option('injection_points') subdir_done() diff --git a/src/test/modules/gin/sql/gin_incomplete_splits.sql b/src/test/modules/gin/sql/gin_incomplete_splits.sql index ebf0f620f0c1a..d451257c2753f 100644 --- a/src/test/modules/gin/sql/gin_incomplete_splits.sql +++ b/src/test/modules/gin/sql/gin_incomplete_splits.sql @@ -148,3 +148,5 @@ select insert_n(:next_i, 10) as next_i select verify(:next_i); SELECT injection_points_detach('gin-finish-incomplete-split'); + +drop extension injection_points; diff --git a/src/test/modules/index/.gitignore b/src/test/modules/index/.gitignore new file mode 100644 index 0000000000000..b4903eba657fa --- /dev/null +++ b/src/test/modules/index/.gitignore @@ -0,0 +1,6 @@ +# Generated subdirectories +/log/ +/results/ +/output_iso/ +/tmp_check/ +/tmp_check_iso/ diff --git a/src/test/modules/index/Makefile b/src/test/modules/index/Makefile new file mode 100644 index 0000000000000..29047044ede3d --- /dev/null +++ b/src/test/modules/index/Makefile @@ -0,0 +1,16 @@ +# src/test/modules/index/Makefile + +EXTRA_INSTALL = contrib/btree_gin contrib/btree_gist + +ISOLATION = killtuples + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/index +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/index/expected/killtuples.out b/src/test/modules/index/expected/killtuples.out new file mode 100644 index 0000000000000..a3db2c4093689 --- /dev/null +++ b/src/test/modules/index/expected/killtuples.out @@ -0,0 +1,430 @@ +Parsed test spec with 1 sessions + +starting permutation: create_table fill_500 create_btree flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table +step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); +step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i); +step create_btree: CREATE INDEX kill_prior_tuple_btree ON kill_prior_tuple USING btree (key); +step flush: SELECT FROM pg_stat_force_next_flush(); +step disable_seq: SET enable_seqscan = false; +step disable_bitmap: SET enable_bitmapscan = false; +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +-------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +-------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step delete: DELETE FROM kill_prior_tuple; +step flush: SELECT FROM pg_stat_force_next_flush(); +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +-------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +-------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_btree on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +f +(1 row) + +step drop_table: DROP TABLE IF EXISTS kill_prior_tuple; + +starting permutation: create_table fill_500 create_ext_btree_gist create_gist flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table drop_ext_btree_gist +step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); +step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i); +step create_ext_btree_gist: CREATE EXTENSION btree_gist; +step create_gist: CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key); +step flush: SELECT FROM pg_stat_force_next_flush(); +step disable_seq: SET enable_seqscan = false; +step disable_bitmap: SET enable_bitmapscan = false; +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step delete: DELETE FROM kill_prior_tuple; +step flush: SELECT FROM pg_stat_force_next_flush(); +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +f +(1 row) + +step drop_table: DROP TABLE IF EXISTS kill_prior_tuple; +step drop_ext_btree_gist: DROP EXTENSION btree_gist; + +starting permutation: create_table fill_10 create_ext_btree_gist create_gist flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table drop_ext_btree_gist +step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); +step fill_10: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 10) g(i); +step create_ext_btree_gist: CREATE EXTENSION btree_gist; +step create_gist: CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key); +step flush: SELECT FROM pg_stat_force_next_flush(); +step disable_seq: SET enable_seqscan = false; +step disable_bitmap: SET enable_bitmapscan = false; +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step delete: DELETE FROM kill_prior_tuple; +step flush: SELECT FROM pg_stat_force_next_flush(); +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_gist on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step drop_table: DROP TABLE IF EXISTS kill_prior_tuple; +step drop_ext_btree_gist: DROP EXTENSION btree_gist; + +starting permutation: create_table fill_500 create_hash flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table +step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); +step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i); +step create_hash: CREATE INDEX kill_prior_tuple_hash ON kill_prior_tuple USING hash (key); +step flush: SELECT FROM pg_stat_force_next_flush(); +step disable_seq: SET enable_seqscan = false; +step disable_bitmap: SET enable_bitmapscan = false; +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step delete: DELETE FROM kill_prior_tuple; +step flush: SELECT FROM pg_stat_force_next_flush(); +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +f +(1 row) + +step drop_table: DROP TABLE IF EXISTS kill_prior_tuple; + +starting permutation: create_table fill_same create_hash flush disable_seq disable_bitmap measure access flush result measure access flush result delete flush measure access flush result measure access flush result drop_table +step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); +step fill_same: INSERT INTO kill_prior_tuple(key, cat) SELECT 1, 'a' FROM generate_series(1, 408) g(i); +step create_hash: CREATE INDEX kill_prior_tuple_hash ON kill_prior_tuple USING hash (key); +step flush: SELECT FROM pg_stat_force_next_flush(); +step disable_seq: SET enable_seqscan = false; +step disable_bitmap: SET enable_bitmapscan = false; +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +--------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=408.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +--------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=408.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step delete: DELETE FROM kill_prior_tuple; +step flush: SELECT FROM pg_stat_force_next_flush(); +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +------------------------------------------------------------------------------------- +Index Scan using kill_prior_tuple_hash on kill_prior_tuple (actual rows=0.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(3 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +f +(1 row) + +step drop_table: DROP TABLE IF EXISTS kill_prior_tuple; + +starting permutation: create_table fill_500 create_ext_btree_gin create_gin flush disable_seq delete flush measure access flush result measure access flush result drop_table drop_ext_btree_gin +step create_table: CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); +step fill_500: INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i); +step create_ext_btree_gin: CREATE EXTENSION btree_gin; +step create_gin: CREATE INDEX kill_prior_tuple_gin ON kill_prior_tuple USING gin (key); +step flush: SELECT FROM pg_stat_force_next_flush(); +step disable_seq: SET enable_seqscan = false; +step delete: DELETE FROM kill_prior_tuple; +step flush: SELECT FROM pg_stat_force_next_flush(); +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +-------------------------------------------------------------------------- +Bitmap Heap Scan on kill_prior_tuple (actual rows=0.00 loops=1) + Recheck Cond: (key = 1) + Heap Blocks: exact=1 + -> Bitmap Index Scan on kill_prior_tuple_gin (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(6 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step measure: UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); +step access: EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; +QUERY PLAN +-------------------------------------------------------------------------- +Bitmap Heap Scan on kill_prior_tuple (actual rows=0.00 loops=1) + Recheck Cond: (key = 1) + Heap Blocks: exact=1 + -> Bitmap Index Scan on kill_prior_tuple_gin (actual rows=1.00 loops=1) + Index Cond: (key = 1) + Index Searches: 1 +(6 rows) + +step flush: SELECT FROM pg_stat_force_next_flush(); +step result: SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; +has_new_heap_accesses +--------------------- +t +(1 row) + +step drop_table: DROP TABLE IF EXISTS kill_prior_tuple; +step drop_ext_btree_gin: DROP EXTENSION btree_gin; diff --git a/src/test/modules/index/meson.build b/src/test/modules/index/meson.build new file mode 100644 index 0000000000000..834ce081f099f --- /dev/null +++ b/src/test/modules/index/meson.build @@ -0,0 +1,12 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +tests += { + 'name': 'index', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'isolation': { + 'specs': [ + 'killtuples', + ], + }, +} diff --git a/src/test/modules/index/specs/killtuples.spec b/src/test/modules/index/specs/killtuples.spec new file mode 100644 index 0000000000000..3b98ff9f76d8d --- /dev/null +++ b/src/test/modules/index/specs/killtuples.spec @@ -0,0 +1,140 @@ +# Basic testing of killtuples / kill_prior_tuples / all_dead testing +# for various index AMs +# +# This tests just enough to ensure that the kill* routines are actually +# executed and does something approximately reasonable. It's *not* sufficient +# testing for adding killitems support to a new AM! +# +# This doesn't really need to be an isolation test, it could be written as a +# regular regression test. However, writing it as an isolation test ends up a +# *lot* less verbose. + +setup +{ + CREATE TABLE counter(heap_accesses int); + INSERT INTO counter(heap_accesses) VALUES (0); +} + +teardown +{ + DROP TABLE counter; +} + +session s1 +# to ensure GUCs are reset +setup { RESET ALL; } + +step disable_seq { SET enable_seqscan = false; } + +step disable_bitmap { SET enable_bitmapscan = false; } + +# use a temporary table to make sure no other session can interfere with +# visibility determinations +step create_table { CREATE TEMPORARY TABLE kill_prior_tuple(key int not null, cat text not null); } + +step fill_10 { INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 10) g(i); } + +step fill_500 { INSERT INTO kill_prior_tuple(key, cat) SELECT g.i, 'a' FROM generate_series(1, 500) g(i); } + +step fill_same { INSERT INTO kill_prior_tuple(key, cat) SELECT 1, 'a' FROM generate_series(1, 408) g(i); } + +# column-less select to make output easier to read +step flush { SELECT FROM pg_stat_force_next_flush(); } + +step measure { UPDATE counter SET heap_accesses = (SELECT heap_blks_read + heap_blks_hit FROM pg_statio_all_tables WHERE relname = 'kill_prior_tuple'); } + +step result { SELECT ((heap_blks_read + heap_blks_hit - counter.heap_accesses) > 0) AS has_new_heap_accesses FROM counter, pg_statio_all_tables WHERE relname = 'kill_prior_tuple'; } + +step access { EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) SELECT * FROM kill_prior_tuple WHERE key = 1; } + +step delete { DELETE FROM kill_prior_tuple; } + +step drop_table { DROP TABLE IF EXISTS kill_prior_tuple; } + +### steps for testing btree indexes ### +step create_btree { CREATE INDEX kill_prior_tuple_btree ON kill_prior_tuple USING btree (key); } + +### steps for testing gist indexes ### +# Creating the extensions takes time, so we don't want to do so when testing +# other AMs +step create_ext_btree_gist { CREATE EXTENSION btree_gist; } +step drop_ext_btree_gist { DROP EXTENSION btree_gist; } +step create_gist { CREATE INDEX kill_prior_tuple_gist ON kill_prior_tuple USING gist (key); } + +### steps for testing gin indexes ### +# See create_ext_btree_gist +step create_ext_btree_gin { CREATE EXTENSION btree_gin; } +step drop_ext_btree_gin { DROP EXTENSION btree_gin; } +step create_gin { CREATE INDEX kill_prior_tuple_gin ON kill_prior_tuple USING gin (key); } + +### steps for testing hash indexes ### +step create_hash { CREATE INDEX kill_prior_tuple_hash ON kill_prior_tuple USING hash (key); } + + +# test killtuples with btree index +permutation + create_table fill_500 create_btree flush + disable_seq disable_bitmap + # show each access to non-deleted tuple increments heap_blks_* + measure access flush result + measure access flush result + delete flush + # first access after accessing deleted tuple still needs to access heap + measure access flush result + # but after kill_prior_tuple did its thing, we shouldn't access heap anymore + measure access flush result + drop_table + +# Same as first permutation, except testing gist +permutation + create_table fill_500 create_ext_btree_gist create_gist flush + disable_seq disable_bitmap + measure access flush result + measure access flush result + delete flush + measure access flush result + measure access flush result + drop_table drop_ext_btree_gist + +# Test gist, but with fewer rows - shows that killitems doesn't work anymore! +permutation + create_table fill_10 create_ext_btree_gist create_gist flush + disable_seq disable_bitmap + measure access flush result + measure access flush result + delete flush + measure access flush result + measure access flush result + drop_table drop_ext_btree_gist + +# Same as first permutation, except testing hash +permutation + create_table fill_500 create_hash flush + disable_seq disable_bitmap + measure access flush result + measure access flush result + delete flush + measure access flush result + measure access flush result + drop_table + +# Test the hash special case of use of the overflow page due to lots of duplicates +permutation + create_table fill_same create_hash flush + disable_seq disable_bitmap + measure access flush result + measure access flush result + delete flush + measure access flush result + measure access flush result + drop_table + +# # Similar to first permutation, except that gin does not have killtuples support +permutation + create_table fill_500 create_ext_btree_gin create_gin flush + disable_seq + delete flush + measure access flush result + # will still fetch from heap + measure access flush result + drop_table drop_ext_btree_gin diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile index e680991f8d4f0..f057d143d1abe 100644 --- a/src/test/modules/injection_points/Makefile +++ b/src/test/modules/injection_points/Makefile @@ -4,19 +4,23 @@ MODULE_big = injection_points OBJS = \ $(WIN32RES) \ injection_points.o \ - injection_stats.o \ - injection_stats_fixed.o \ regress_injection.o EXTENSION = injection_points DATA = injection_points--1.0.sql PGFILEDESC = "injection_points - facility for injection points" -REGRESS = injection_points hashagg reindex_conc +REGRESS = injection_points hashagg reindex_conc vacuum REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress -ISOLATION = basic inplace syscache-update-pruned +ISOLATION = basic \ + inplace \ + repack \ + repack_toast \ + syscache-update-pruned \ + heap_lock_update -TAP_TESTS = 1 +# some isolation tests require wal_level=replica +ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf # The injection points are cluster-wide, so disable installcheck NO_INSTALLCHECK = 1 diff --git a/src/test/modules/injection_points/expected/heap_lock_update.out b/src/test/modules/injection_points/expected/heap_lock_update.out new file mode 100644 index 0000000000000..1ec8d876414c0 --- /dev/null +++ b/src/test/modules/injection_points/expected/heap_lock_update.out @@ -0,0 +1,83 @@ +Parsed test spec with 2 sessions + +starting permutation: s1begin s1update s2lock s1abort vacuum reinsert wake +step s1begin: BEGIN; +step s1update: UPDATE t SET id = 10000 WHERE id = 1 RETURNING ctid; +ctid +----- +(1,2) +(1 row) + +step s2lock: select * from t where id = 1 for update; +step s1abort: ABORT; +step vacuum: VACUUM t; +step reinsert: + INSERT INTO t VALUES (10001) RETURNING ctid; + UPDATE t SET id = 10002 WHERE id = 10001 RETURNING ctid; + +ctid +----- +(1,2) +(1 row) + +ctid +----- +(1,3) +(1 row) + +step wake: + SELECT FROM injection_points_detach('heap_lock_updated_tuple'); + SELECT FROM injection_points_wakeup('heap_lock_updated_tuple'); + +step s2lock: <... completed> +id +-- + 1 +(1 row) + +step wake: <... completed> + +starting permutation: s1begin s1update s2lock s1abort vacuum reinsert_and_lock wake +step s1begin: BEGIN; +step s1update: UPDATE t SET id = 10000 WHERE id = 1 RETURNING ctid; +ctid +----- +(1,2) +(1 row) + +step s2lock: select * from t where id = 1 for update; +step s1abort: ABORT; +step vacuum: VACUUM t; +step reinsert_and_lock: + BEGIN; + INSERT INTO t VALUES (10001) RETURNING ctid; + SELECT ctid, * FROM t WHERE id = 1 FOR UPDATE; + COMMIT; + UPDATE t SET id = 10002 WHERE id = 10001 returning ctid; + +ctid +----- +(1,2) +(1 row) + +ctid |id +-----+-- +(0,1)| 1 +(1 row) + +ctid +----- +(1,3) +(1 row) + +step wake: + SELECT FROM injection_points_detach('heap_lock_updated_tuple'); + SELECT FROM injection_points_wakeup('heap_lock_updated_tuple'); + +step s2lock: <... completed> +id +-- + 1 +(1 row) + +step wake: <... completed> diff --git a/src/test/modules/injection_points/expected/injection_points.out b/src/test/modules/injection_points/expected/injection_points.out index 43bcdd01582f7..a3ccaee54727a 100644 --- a/src/test/modules/injection_points/expected/injection_points.out +++ b/src/test/modules/injection_points/expected/injection_points.out @@ -39,6 +39,15 @@ SELECT injection_points_attach('TestInjectionLog2', 'notice'); (1 row) +SELECT point_name, library, function FROM injection_points_list() + ORDER BY point_name COLLATE "C"; + point_name | library | function +--------------------+------------------+------------------ + TestInjectionError | injection_points | injection_error + TestInjectionLog | injection_points | injection_notice + TestInjectionLog2 | injection_points | injection_notice +(3 rows) + SELECT injection_points_run('TestInjectionBooh'); -- nothing injection_points_run ---------------------- @@ -298,5 +307,58 @@ SELECT injection_points_detach('TestConditionLocal1'); (1 row) +-- Function variant for attach. +SELECT injection_points_attach(repeat('a', 64), 'injection_points', + 'injection_notice', NULL); +ERROR: injection point name aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa too long (maximum of 63 characters) +SELECT injection_points_attach('TestInjectionNoticeFunc', repeat('a', 128), + 'injection_notice', NULL); +ERROR: injection point library aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa too long (maximum of 127 characters) +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + repeat('a', 128), NULL); +ERROR: injection point function aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa too long (maximum of 127 characters) +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + 'injection_notice', repeat('a', 1025)::bytea); +ERROR: injection point data too long (maximum of 1024 bytes) +SELECT injection_points_attach(NULL, NULL, NULL, NULL); +ERROR: injection point name cannot be NULL +SELECT injection_points_attach('TestInjectionNoticeFunc', NULL, NULL, NULL); +ERROR: injection point library cannot be NULL +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + NULL, NULL); +ERROR: injection point function cannot be NULL +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + 'injection_notice', NULL); + injection_points_attach +------------------------- + +(1 row) + +SELECT point_name, library, function FROM injection_points_list() + ORDER BY point_name COLLATE "C"; + point_name | library | function +-------------------------+------------------+------------------ + TestInjectionNoticeFunc | injection_points | injection_notice +(1 row) + +SELECT injection_points_run('TestInjectionNoticeFunc', NULL); -- notice +NOTICE: notice triggered for injection point TestInjectionNoticeFunc + injection_points_run +---------------------- + +(1 row) + +SELECT injection_points_detach('TestInjectionNoticeFunc'); + injection_points_detach +------------------------- + +(1 row) + +SELECT point_name, library, function FROM injection_points_list() + ORDER BY point_name COLLATE "C"; + point_name | library | function +------------+---------+---------- +(0 rows) + DROP EXTENSION injection_points; DROP FUNCTION wait_pid; diff --git a/src/test/modules/injection_points/expected/repack.out b/src/test/modules/injection_points/expected/repack.out new file mode 100644 index 0000000000000..b575e9052ee8c --- /dev/null +++ b/src/test/modules/injection_points/expected/repack.out @@ -0,0 +1,113 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock change_existing change_new change_subxact1 change_subxact2 check2 wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey; + +step change_existing: + UPDATE repack_test SET i=10 where i=1; + UPDATE repack_test SET j=20 where i=2; + UPDATE repack_test SET i=30 where i=3; + UPDATE repack_test SET i=40 where i=30; + DELETE FROM repack_test WHERE i=4; + +step change_new: + INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8); + UPDATE repack_test SET i=50 where i=5; + UPDATE repack_test SET j=60 where i=6; + DELETE FROM repack_test WHERE i=7; + +step change_subxact1: + BEGIN; + INSERT INTO repack_test(i, j) VALUES (100, 100); + SAVEPOINT s1; + UPDATE repack_test SET i=101 where i=100; + SAVEPOINT s2; + UPDATE repack_test SET i=102 where i=101; + COMMIT; + +step change_subxact2: + BEGIN; + SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 110); + ROLLBACK TO SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 111); + COMMIT; + +step check2: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; + + i| j +---+--- + 2| 20 + 6| 60 + 8| 8 + 10| 1 + 40| 3 + 50| 5 +102|100 +110|111 +(8 rows) + +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 2 +(1 row) + + i| j +---+--- + 2| 20 + 6| 60 + 8| 8 + 10| 1 + 40| 3 + 50| 5 +102|100 +110|111 +(8 rows) + +count +----- + 0 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/expected/repack_toast.out b/src/test/modules/injection_points/expected/repack_toast.out new file mode 100644 index 0000000000000..b56dde134f853 --- /dev/null +++ b/src/test/modules/injection_points/expected/repack_toast.out @@ -0,0 +1,65 @@ +Parsed test spec with 2 sessions + +starting permutation: wait_before_lock change check2 wakeup_before_lock check1 +injection_points_attach +----------------------- + +(1 row) + +step wait_before_lock: + REPACK (CONCURRENTLY) repack_test; + +step change: + UPDATE repack_test SET j=get_long_string() where i=2; + DELETE FROM repack_test WHERE i=3; + INSERT INTO repack_test(i, j) VALUES (4, get_long_string()); + UPDATE repack_test SET i=3 where i=1; + +step check2: + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; + +step wakeup_before_lock: + SELECT injection_points_wakeup('repack-concurrently-before-lock'); + +injection_points_wakeup +----------------------- + +(1 row) + +step wait_before_lock: <... completed> +step check1: + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; + +count +----- + 4 +(1 row) + +count +----- + 0 +(1 row) + +injection_points_detach +----------------------- + +(1 row) + diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out new file mode 100644 index 0000000000000..58df59fa927e3 --- /dev/null +++ b/src/test/modules/injection_points/expected/vacuum.out @@ -0,0 +1,122 @@ +-- Tests for VACUUM +CREATE EXTENSION injection_points; +SELECT injection_points_set_local(); + injection_points_set_local +---------------------------- + +(1 row) + +SELECT injection_points_attach('vacuum-index-cleanup-auto', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +SELECT injection_points_attach('vacuum-index-cleanup-disabled', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +SELECT injection_points_attach('vacuum-index-cleanup-enabled', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +SELECT injection_points_attach('vacuum-truncate-auto', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +SELECT injection_points_attach('vacuum-truncate-disabled', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +SELECT injection_points_attach('vacuum-truncate-enabled', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +-- Check state of index_cleanup and truncate in VACUUM. +CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH + (autovacuum_enabled=false, + vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false, + vacuum_truncate=true, toast.vacuum_truncate=false); +CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH + (autovacuum_enabled=false, + vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true, + vacuum_truncate=false, toast.vacuum_truncate=true); +-- Multiple relations should use their options in isolation. +VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on; +NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled +NOTICE: notice triggered for injection point vacuum-truncate-enabled +NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled +NOTICE: notice triggered for injection point vacuum-truncate-disabled +NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled +NOTICE: notice triggered for injection point vacuum-truncate-disabled +NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled +NOTICE: notice triggered for injection point vacuum-truncate-enabled +-- Check "auto" case of index_cleanup and "truncate" controlled by +-- its GUC. +CREATE TABLE vac_tab_auto(i int, j text) WITH + (autovacuum_enabled=false, + vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto); +SET vacuum_truncate = false; +VACUUM vac_tab_auto; +NOTICE: notice triggered for injection point vacuum-index-cleanup-auto +NOTICE: notice triggered for injection point vacuum-truncate-disabled +NOTICE: notice triggered for injection point vacuum-index-cleanup-auto +NOTICE: notice triggered for injection point vacuum-truncate-disabled +SET vacuum_truncate = true; +VACUUM vac_tab_auto; +NOTICE: notice triggered for injection point vacuum-index-cleanup-auto +NOTICE: notice triggered for injection point vacuum-truncate-enabled +NOTICE: notice triggered for injection point vacuum-index-cleanup-auto +NOTICE: notice triggered for injection point vacuum-truncate-enabled +RESET vacuum_truncate; +DROP TABLE vac_tab_auto; +DROP TABLE vac_tab_on_toast_off; +DROP TABLE vac_tab_off_toast_on; +-- Cleanup +SELECT injection_points_detach('vacuum-index-cleanup-auto'); + injection_points_detach +------------------------- + +(1 row) + +SELECT injection_points_detach('vacuum-index-cleanup-disabled'); + injection_points_detach +------------------------- + +(1 row) + +SELECT injection_points_detach('vacuum-index-cleanup-enabled'); + injection_points_detach +------------------------- + +(1 row) + +SELECT injection_points_detach('vacuum-truncate-auto'); + injection_points_detach +------------------------- + +(1 row) + +SELECT injection_points_detach('vacuum-truncate-disabled'); + injection_points_detach +------------------------- + +(1 row) + +SELECT injection_points_detach('vacuum-truncate-enabled'); + injection_points_detach +------------------------- + +(1 row) + +DROP EXTENSION injection_points; diff --git a/src/test/modules/injection_points/extra.conf b/src/test/modules/injection_points/extra.conf new file mode 100644 index 0000000000000..010abb193a8bd --- /dev/null +++ b/src/test/modules/injection_points/extra.conf @@ -0,0 +1 @@ +wal_level=replica diff --git a/src/test/modules/injection_points/injection_points--1.0.sql b/src/test/modules/injection_points/injection_points--1.0.sql index cc76b1bf99ae6..861c7355d4e36 100644 --- a/src/test/modules/injection_points/injection_points--1.0.sql +++ b/src/test/modules/injection_points/injection_points--1.0.sql @@ -14,6 +14,18 @@ RETURNS void AS 'MODULE_PATHNAME', 'injection_points_attach' LANGUAGE C STRICT PARALLEL UNSAFE; +-- +-- injection_points_attach() +-- +-- Attaches a function to the given injection point, with library name, +-- function name and private data. +-- +CREATE FUNCTION injection_points_attach(IN point_name TEXT, + IN library_name TEXT, IN function_name TEXT, IN private_data BYTEA) +RETURNS void +AS 'MODULE_PATHNAME', 'injection_points_attach_func' +LANGUAGE C PARALLEL UNSAFE; + -- -- injection_points_load() -- @@ -78,37 +90,16 @@ AS 'MODULE_PATHNAME', 'injection_points_detach' LANGUAGE C STRICT PARALLEL UNSAFE; -- --- injection_points_stats_numcalls() +-- injection_points_list() -- --- Reports statistics, if any, related to the given injection point. --- -CREATE FUNCTION injection_points_stats_numcalls(IN point_name TEXT) -RETURNS bigint -AS 'MODULE_PATHNAME', 'injection_points_stats_numcalls' -LANGUAGE C STRICT; - --- --- injection_points_stats_drop() --- --- Drop all statistics of injection points. --- -CREATE FUNCTION injection_points_stats_drop() -RETURNS void -AS 'MODULE_PATHNAME', 'injection_points_stats_drop' -LANGUAGE C STRICT; - +-- List of all the injection points currently attached. -- --- injection_points_stats_fixed() --- --- Reports fixed-numbered statistics for injection points. -CREATE FUNCTION injection_points_stats_fixed(OUT numattach int8, - OUT numdetach int8, - OUT numrun int8, - OUT numcached int8, - OUT numloaded int8) -RETURNS record -AS 'MODULE_PATHNAME', 'injection_points_stats_fixed' -LANGUAGE C STRICT; +CREATE FUNCTION injection_points_list(OUT point_name text, + OUT library text, + OUT function text) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'injection_points_list' +LANGUAGE C STRICT VOLATILE PARALLEL RESTRICTED; -- -- regress_injection.c functions diff --git a/src/test/modules/injection_points/injection_points.c b/src/test/modules/injection_points/injection_points.c index 3da0cbc10e08f..0f1af51367357 100644 --- a/src/test/modules/injection_points/injection_points.c +++ b/src/test/modules/injection_points/injection_points.c @@ -6,7 +6,7 @@ * Injection points are able to trigger user-defined callbacks in pre-defined * code paths. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -18,7 +18,7 @@ #include "postgres.h" #include "fmgr.h" -#include "injection_stats.h" +#include "funcapi.h" #include "miscadmin.h" #include "nodes/pg_list.h" #include "nodes/value.h" @@ -31,6 +31,7 @@ #include "utils/guc.h" #include "utils/injection_point.h" #include "utils/memutils.h" +#include "utils/tuplestore.h" #include "utils/wait_event.h" PG_MODULE_MAGIC; @@ -106,25 +107,20 @@ extern PGDLLEXPORT void injection_wait(const char *name, /* track if injection points attached in this process are linked to it */ static bool injection_point_local = false; -/* - * GUC variable - * - * This GUC is useful to control if statistics should be enabled or not - * during a test with injection points, like for example if a test relies - * on a callback run in a critical section where no allocation should happen. - */ -bool inj_stats_enabled = false; +static void injection_shmem_request(void *arg); +static void injection_shmem_init(void *arg); -/* Shared memory init callbacks */ -static shmem_request_hook_type prev_shmem_request_hook = NULL; -static shmem_startup_hook_type prev_shmem_startup_hook = NULL; +static const ShmemCallbacks injection_shmem_callbacks = { + .request_fn = injection_shmem_request, + .init_fn = injection_shmem_init, +}; /* * Routine for shared memory area initialization, used as a callback * when initializing dynamically with a DSM or when loading the module. */ static void -injection_point_init_state(void *ptr) +injection_point_init_state(void *ptr, void *arg) { InjectionPointSharedState *state = (InjectionPointSharedState *) ptr; @@ -134,44 +130,23 @@ injection_point_init_state(void *ptr) ConditionVariableInit(&state->wait_point); } -/* Shared memory initialization when loading module */ static void -injection_shmem_request(void) +injection_shmem_request(void *arg) { - Size size; - - if (prev_shmem_request_hook) - prev_shmem_request_hook(); - - size = MAXALIGN(sizeof(InjectionPointSharedState)); - RequestAddinShmemSpace(size); + ShmemRequestStruct(.name = "injection_points", + .size = sizeof(InjectionPointSharedState), + .ptr = (void **) &inj_state, + ); } static void -injection_shmem_startup(void) +injection_shmem_init(void *arg) { - bool found; - - if (prev_shmem_startup_hook) - prev_shmem_startup_hook(); - - /* Create or attach to the shared memory state */ - LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); - - inj_state = ShmemInitStruct("injection_points", - sizeof(InjectionPointSharedState), - &found); - - if (!found) - { - /* - * First time through, so initialize. This is shared with the dynamic - * initialization using a DSM. - */ - injection_point_init_state(inj_state); - } - - LWLockRelease(AddinShmemInitLock); + /* + * First time through, so initialize. This is shared with the dynamic + * initialization using a DSM. + */ + injection_point_init_state(inj_state, NULL); } /* @@ -188,7 +163,7 @@ injection_init_shmem(void) inj_state = GetNamedDSMSegment("injection_points", sizeof(InjectionPointSharedState), injection_point_init_state, - &found); + &found, NULL); } /* @@ -198,7 +173,7 @@ injection_init_shmem(void) * otherwise. */ static bool -injection_point_allowed(InjectionPointCondition *condition) +injection_point_allowed(const InjectionPointCondition *condition) { bool result = true; @@ -234,9 +209,6 @@ injection_points_cleanup(int code, Datum arg) char *name = strVal(lfirst(lc)); (void) InjectionPointDetach(name); - - /* Remove stats entry */ - pgstat_drop_inj(name); } } @@ -244,14 +216,12 @@ injection_points_cleanup(int code, Datum arg) void injection_error(const char *name, const void *private_data, void *arg) { - InjectionPointCondition *condition = (InjectionPointCondition *) private_data; - char *argstr = (char *) arg; + const InjectionPointCondition *condition = private_data; + char *argstr = arg; if (!injection_point_allowed(condition)) return; - pgstat_report_inj(name); - if (argstr) elog(ERROR, "error triggered for injection point %s (%s)", name, argstr); @@ -262,14 +232,12 @@ injection_error(const char *name, const void *private_data, void *arg) void injection_notice(const char *name, const void *private_data, void *arg) { - InjectionPointCondition *condition = (InjectionPointCondition *) private_data; - char *argstr = (char *) arg; + const InjectionPointCondition *condition = private_data; + char *argstr = arg; if (!injection_point_allowed(condition)) return; - pgstat_report_inj(name); - if (argstr) elog(NOTICE, "notice triggered for injection point %s (%s)", name, argstr); @@ -284,7 +252,7 @@ injection_wait(const char *name, const void *private_data, void *arg) uint32 old_wait_counts = 0; int index = -1; uint32 injection_wait_event = 0; - InjectionPointCondition *condition = (InjectionPointCondition *) private_data; + const InjectionPointCondition *condition = private_data; if (inj_state == NULL) injection_init_shmem(); @@ -292,8 +260,6 @@ injection_wait(const char *name, const void *private_data, void *arg) if (!injection_point_allowed(condition)) return; - pgstat_report_inj(name); - /* * Use the injection point name for this custom wait event. Note that * this custom wait event name is not released, but we don't care much for @@ -370,7 +336,6 @@ injection_points_attach(PG_FUNCTION_ARGS) condition.pid = MyProcPid; } - pgstat_report_inj_fixed(1, 0, 0, 0, 0); InjectionPointAttach(name, "injection_points", function, &condition, sizeof(InjectionPointCondition)); @@ -384,9 +349,46 @@ injection_points_attach(PG_FUNCTION_ARGS) MemoryContextSwitchTo(oldctx); } - /* Add entry for stats */ - pgstat_create_inj(name); + PG_RETURN_VOID(); +} + +/* + * SQL function for creating an injection point with library name, function + * name and private data. + */ +PG_FUNCTION_INFO_V1(injection_points_attach_func); +Datum +injection_points_attach_func(PG_FUNCTION_ARGS) +{ + char *name; + char *lib_name; + char *function; + bytea *private_data = NULL; + int private_data_size = 0; + + if (PG_ARGISNULL(0)) + elog(ERROR, "injection point name cannot be NULL"); + if (PG_ARGISNULL(1)) + elog(ERROR, "injection point library cannot be NULL"); + if (PG_ARGISNULL(2)) + elog(ERROR, "injection point function cannot be NULL"); + name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + lib_name = text_to_cstring(PG_GETARG_TEXT_PP(1)); + function = text_to_cstring(PG_GETARG_TEXT_PP(2)); + + if (!PG_ARGISNULL(3)) + { + private_data = PG_GETARG_BYTEA_PP(3); + private_data_size = VARSIZE_ANY_EXHDR(private_data); + } + + if (private_data != NULL) + InjectionPointAttach(name, lib_name, function, VARDATA_ANY(private_data), + private_data_size); + else + InjectionPointAttach(name, lib_name, function, NULL, + 0); PG_RETURN_VOID(); } @@ -402,7 +404,6 @@ injection_points_load(PG_FUNCTION_ARGS) if (inj_state == NULL) injection_init_shmem(); - pgstat_report_inj_fixed(0, 0, 0, 0, 1); INJECTION_POINT_LOAD(name); PG_RETURN_VOID(); @@ -425,7 +426,6 @@ injection_points_run(PG_FUNCTION_ARGS) if (!PG_ARGISNULL(1)) arg = text_to_cstring(PG_GETARG_TEXT_PP(1)); - pgstat_report_inj_fixed(0, 0, 1, 0, 0); INJECTION_POINT(name, arg); PG_RETURN_VOID(); @@ -448,7 +448,6 @@ injection_points_cached(PG_FUNCTION_ARGS) if (!PG_ARGISNULL(1)) arg = text_to_cstring(PG_GETARG_TEXT_PP(1)); - pgstat_report_inj_fixed(0, 0, 0, 1, 0); INJECTION_POINT_CACHED(name, arg); PG_RETURN_VOID(); @@ -525,7 +524,6 @@ injection_points_detach(PG_FUNCTION_ARGS) { char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); - pgstat_report_inj_fixed(0, 1, 0, 0, 0); if (!InjectionPointDetach(name)) elog(ERROR, "could not detach injection point \"%s\"", name); @@ -539,12 +537,46 @@ injection_points_detach(PG_FUNCTION_ARGS) MemoryContextSwitchTo(oldctx); } - /* Remove stats entry */ - pgstat_drop_inj(name); - PG_RETURN_VOID(); } +/* + * SQL function for listing all the injection points attached. + */ +PG_FUNCTION_INFO_V1(injection_points_list); +Datum +injection_points_list(PG_FUNCTION_ARGS) +{ +#define NUM_INJECTION_POINTS_LIST 3 + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + List *inj_points; + ListCell *lc; + + /* Build a tuplestore to return our results in */ + InitMaterializedSRF(fcinfo, 0); + + inj_points = InjectionPointList(); + + foreach(lc, inj_points) + { + Datum values[NUM_INJECTION_POINTS_LIST]; + bool nulls[NUM_INJECTION_POINTS_LIST]; + InjectionPointData *inj_point = lfirst(lc); + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + values[0] = PointerGetDatum(cstring_to_text(inj_point->name)); + values[1] = PointerGetDatum(cstring_to_text(inj_point->library)); + values[2] = PointerGetDatum(cstring_to_text(inj_point->function)); + + /* shove row into tuplestore */ + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + } + + return (Datum) 0; +#undef NUM_INJECTION_POINTS_LIST +} void _PG_init(void) @@ -552,25 +584,5 @@ _PG_init(void) if (!process_shared_preload_libraries_in_progress) return; - DefineCustomBoolVariable("injection_points.stats", - "Enables statistics for injection points.", - NULL, - &inj_stats_enabled, - false, - PGC_POSTMASTER, - 0, - NULL, - NULL, - NULL); - - MarkGUCPrefixReserved("injection_points"); - - /* Shared memory initialization */ - prev_shmem_request_hook = shmem_request_hook; - shmem_request_hook = injection_shmem_request; - prev_shmem_startup_hook = shmem_startup_hook; - shmem_startup_hook = injection_shmem_startup; - - pgstat_register_inj(); - pgstat_register_inj_fixed(); + RegisterShmemCallbacks(&injection_shmem_callbacks); } diff --git a/src/test/modules/injection_points/injection_stats.c b/src/test/modules/injection_points/injection_stats.c deleted file mode 100644 index 14903c629e0d1..0000000000000 --- a/src/test/modules/injection_points/injection_stats.c +++ /dev/null @@ -1,218 +0,0 @@ -/*-------------------------------------------------------------------------- - * - * injection_stats.c - * Code for statistics of injection points. - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/test/modules/injection_points/injection_stats.c - * - * ------------------------------------------------------------------------- - */ - -#include "postgres.h" - -#include "fmgr.h" - -#include "common/hashfn.h" -#include "injection_stats.h" -#include "pgstat.h" -#include "utils/builtins.h" -#include "utils/pgstat_internal.h" - -/* Structures for statistics of injection points */ -typedef struct PgStat_StatInjEntry -{ - PgStat_Counter numcalls; /* number of times point has been run */ -} PgStat_StatInjEntry; - -typedef struct PgStatShared_InjectionPoint -{ - PgStatShared_Common header; - PgStat_StatInjEntry stats; -} PgStatShared_InjectionPoint; - -static bool injection_stats_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); - -static const PgStat_KindInfo injection_stats = { - .name = "injection_points", - .fixed_amount = false, /* Bounded by the number of points */ - .write_to_file = true, - - /* Injection points are system-wide */ - .accessed_across_databases = true, - - .shared_size = sizeof(PgStatShared_InjectionPoint), - .shared_data_off = offsetof(PgStatShared_InjectionPoint, stats), - .shared_data_len = sizeof(((PgStatShared_InjectionPoint *) 0)->stats), - .pending_size = sizeof(PgStat_StatInjEntry), - .flush_pending_cb = injection_stats_flush_cb, -}; - -/* - * Compute stats entry idx from point name with an 8-byte hash. - */ -#define PGSTAT_INJ_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0) - -/* - * Kind ID reserved for statistics of injection points. - */ -#define PGSTAT_KIND_INJECTION 129 - -/* Track if stats are loaded */ -static bool inj_stats_loaded = false; - -/* - * Callback for stats handling - */ -static bool -injection_stats_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) -{ - PgStat_StatInjEntry *localent; - PgStatShared_InjectionPoint *shfuncent; - - localent = (PgStat_StatInjEntry *) entry_ref->pending; - shfuncent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats; - - if (!pgstat_lock_entry(entry_ref, nowait)) - return false; - - shfuncent->stats.numcalls += localent->numcalls; - - pgstat_unlock_entry(entry_ref); - - return true; -} - -/* - * Support function for the SQL-callable pgstat* functions. Returns - * a pointer to the injection point statistics struct. - */ -static PgStat_StatInjEntry * -pgstat_fetch_stat_injentry(const char *name) -{ - PgStat_StatInjEntry *entry = NULL; - - if (!inj_stats_loaded || !inj_stats_enabled) - return NULL; - - /* Compile the lookup key as a hash of the point name */ - entry = (PgStat_StatInjEntry *) pgstat_fetch_entry(PGSTAT_KIND_INJECTION, - InvalidOid, - PGSTAT_INJ_IDX(name)); - return entry; -} - -/* - * Workhorse to do the registration work, called in _PG_init(). - */ -void -pgstat_register_inj(void) -{ - pgstat_register_kind(PGSTAT_KIND_INJECTION, &injection_stats); - - /* mark stats as loaded */ - inj_stats_loaded = true; -} - -/* - * Report injection point creation. - */ -void -pgstat_create_inj(const char *name) -{ - PgStat_EntryRef *entry_ref; - PgStatShared_InjectionPoint *shstatent; - - /* leave if disabled */ - if (!inj_stats_loaded || !inj_stats_enabled) - return; - - entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name), NULL); - - shstatent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats; - - /* initialize shared memory data */ - memset(&shstatent->stats, 0, sizeof(shstatent->stats)); -} - -/* - * Report injection point drop. - */ -void -pgstat_drop_inj(const char *name) -{ - /* leave if disabled */ - if (!inj_stats_loaded || !inj_stats_enabled) - return; - - if (!pgstat_drop_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name))) - pgstat_request_entry_refs_gc(); -} - -/* - * Report statistics for injection point. - * - * This is simple because the set of stats to report currently is simple: - * track the number of times a point has been run. - */ -void -pgstat_report_inj(const char *name) -{ - PgStat_EntryRef *entry_ref; - PgStatShared_InjectionPoint *shstatent; - PgStat_StatInjEntry *statent; - - /* leave if disabled */ - if (!inj_stats_loaded || !inj_stats_enabled) - return; - - entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_INJECTION, InvalidOid, - PGSTAT_INJ_IDX(name), NULL); - - shstatent = (PgStatShared_InjectionPoint *) entry_ref->shared_stats; - statent = &shstatent->stats; - - /* Update the injection point statistics */ - statent->numcalls++; -} - -/* - * SQL function returning the number of times an injection point - * has been called. - */ -PG_FUNCTION_INFO_V1(injection_points_stats_numcalls); -Datum -injection_points_stats_numcalls(PG_FUNCTION_ARGS) -{ - char *name = text_to_cstring(PG_GETARG_TEXT_PP(0)); - PgStat_StatInjEntry *entry = pgstat_fetch_stat_injentry(name); - - if (entry == NULL) - PG_RETURN_NULL(); - - PG_RETURN_INT64(entry->numcalls); -} - -/* Only used by injection_points_stats_drop() */ -static bool -match_inj_entries(PgStatShared_HashEntry *entry, Datum match_data) -{ - return entry->key.kind == PGSTAT_KIND_INJECTION; -} - -/* - * SQL function that drops all injection point statistics. - */ -PG_FUNCTION_INFO_V1(injection_points_stats_drop); -Datum -injection_points_stats_drop(PG_FUNCTION_ARGS) -{ - pgstat_drop_matching_entries(match_inj_entries, 0); - - PG_RETURN_VOID(); -} diff --git a/src/test/modules/injection_points/injection_stats.h b/src/test/modules/injection_points/injection_stats.h deleted file mode 100644 index ba310c52c7f0c..0000000000000 --- a/src/test/modules/injection_points/injection_stats.h +++ /dev/null @@ -1,35 +0,0 @@ -/*-------------------------------------------------------------------------- - * - * injection_stats.h - * Definitions for statistics of injection points. - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/test/modules/injection_points/injection_stats.h - * - * ------------------------------------------------------------------------- - */ - -#ifndef INJECTION_STATS -#define INJECTION_STATS - -/* GUC variable */ -extern bool inj_stats_enabled; - -/* injection_stats.c */ -extern void pgstat_register_inj(void); -extern void pgstat_create_inj(const char *name); -extern void pgstat_drop_inj(const char *name); -extern void pgstat_report_inj(const char *name); - -/* injection_stats_fixed.c */ -extern void pgstat_register_inj_fixed(void); -extern void pgstat_report_inj_fixed(uint32 numattach, - uint32 numdetach, - uint32 numrun, - uint32 numcached, - uint32 numloaded); - -#endif diff --git a/src/test/modules/injection_points/injection_stats_fixed.c b/src/test/modules/injection_points/injection_stats_fixed.c deleted file mode 100644 index 3d0c01bdd05ab..0000000000000 --- a/src/test/modules/injection_points/injection_stats_fixed.c +++ /dev/null @@ -1,209 +0,0 @@ -/*-------------------------------------------------------------------------- - * - * injection_stats_fixed.c - * Code for fixed-numbered statistics of injection points. - * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group - * Portions Copyright (c) 1994, Regents of the University of California - * - * IDENTIFICATION - * src/test/modules/injection_points/injection_stats_fixed.c - * - * ------------------------------------------------------------------------- - */ - -#include "postgres.h" - -#include "fmgr.h" - -#include "common/hashfn.h" -#include "funcapi.h" -#include "injection_stats.h" -#include "pgstat.h" -#include "utils/builtins.h" -#include "utils/pgstat_internal.h" - -/* Structures for statistics of injection points, fixed-size */ -typedef struct PgStat_StatInjFixedEntry -{ - PgStat_Counter numattach; /* number of points attached */ - PgStat_Counter numdetach; /* number of points detached */ - PgStat_Counter numrun; /* number of points run */ - PgStat_Counter numcached; /* number of points cached */ - PgStat_Counter numloaded; /* number of points loaded */ - TimestampTz stat_reset_timestamp; -} PgStat_StatInjFixedEntry; - -typedef struct PgStatShared_InjectionPointFixed -{ - LWLock lock; /* protects all the counters */ - uint32 changecount; - PgStat_StatInjFixedEntry stats; - PgStat_StatInjFixedEntry reset_offset; -} PgStatShared_InjectionPointFixed; - -/* Callbacks for fixed-numbered stats */ -static void injection_stats_fixed_init_shmem_cb(void *stats); -static void injection_stats_fixed_reset_all_cb(TimestampTz ts); -static void injection_stats_fixed_snapshot_cb(void); - -static const PgStat_KindInfo injection_stats_fixed = { - .name = "injection_points_fixed", - .fixed_amount = true, - .write_to_file = true, - - .shared_size = sizeof(PgStat_StatInjFixedEntry), - .shared_data_off = offsetof(PgStatShared_InjectionPointFixed, stats), - .shared_data_len = sizeof(((PgStatShared_InjectionPointFixed *) 0)->stats), - - .init_shmem_cb = injection_stats_fixed_init_shmem_cb, - .reset_all_cb = injection_stats_fixed_reset_all_cb, - .snapshot_cb = injection_stats_fixed_snapshot_cb, -}; - -/* - * Kind ID reserved for statistics of injection points. - */ -#define PGSTAT_KIND_INJECTION_FIXED 130 - -/* Track if fixed-numbered stats are loaded */ -static bool inj_fixed_loaded = false; - -static void -injection_stats_fixed_init_shmem_cb(void *stats) -{ - PgStatShared_InjectionPointFixed *stats_shmem = - (PgStatShared_InjectionPointFixed *) stats; - - LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); -} - -static void -injection_stats_fixed_reset_all_cb(TimestampTz ts) -{ - PgStatShared_InjectionPointFixed *stats_shmem = - pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED); - - LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); - pgstat_copy_changecounted_stats(&stats_shmem->reset_offset, - &stats_shmem->stats, - sizeof(stats_shmem->stats), - &stats_shmem->changecount); - stats_shmem->stats.stat_reset_timestamp = ts; - LWLockRelease(&stats_shmem->lock); -} - -static void -injection_stats_fixed_snapshot_cb(void) -{ - PgStatShared_InjectionPointFixed *stats_shmem = - pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED); - PgStat_StatInjFixedEntry *stat_snap = - pgstat_get_custom_snapshot_data(PGSTAT_KIND_INJECTION_FIXED); - PgStat_StatInjFixedEntry *reset_offset = &stats_shmem->reset_offset; - PgStat_StatInjFixedEntry reset; - - pgstat_copy_changecounted_stats(stat_snap, - &stats_shmem->stats, - sizeof(stats_shmem->stats), - &stats_shmem->changecount); - - LWLockAcquire(&stats_shmem->lock, LW_SHARED); - memcpy(&reset, reset_offset, sizeof(stats_shmem->stats)); - LWLockRelease(&stats_shmem->lock); - - /* compensate by reset offsets */ -#define FIXED_COMP(fld) stat_snap->fld -= reset.fld; - FIXED_COMP(numattach); - FIXED_COMP(numdetach); - FIXED_COMP(numrun); - FIXED_COMP(numcached); - FIXED_COMP(numloaded); -#undef FIXED_COMP -} - -/* - * Workhorse to do the registration work, called in _PG_init(). - */ -void -pgstat_register_inj_fixed(void) -{ - pgstat_register_kind(PGSTAT_KIND_INJECTION_FIXED, &injection_stats_fixed); - - /* mark stats as loaded */ - inj_fixed_loaded = true; -} - -/* - * Report fixed number of statistics for an injection point. - */ -void -pgstat_report_inj_fixed(uint32 numattach, - uint32 numdetach, - uint32 numrun, - uint32 numcached, - uint32 numloaded) -{ - PgStatShared_InjectionPointFixed *stats_shmem; - - /* leave if disabled */ - if (!inj_fixed_loaded || !inj_stats_enabled) - return; - - stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_INJECTION_FIXED); - - pgstat_begin_changecount_write(&stats_shmem->changecount); - stats_shmem->stats.numattach += numattach; - stats_shmem->stats.numdetach += numdetach; - stats_shmem->stats.numrun += numrun; - stats_shmem->stats.numcached += numcached; - stats_shmem->stats.numloaded += numloaded; - pgstat_end_changecount_write(&stats_shmem->changecount); -} - -/* - * SQL function returning fixed-numbered statistics for injection points. - */ -PG_FUNCTION_INFO_V1(injection_points_stats_fixed); -Datum -injection_points_stats_fixed(PG_FUNCTION_ARGS) -{ - TupleDesc tupdesc; - Datum values[5] = {0}; - bool nulls[5] = {0}; - PgStat_StatInjFixedEntry *stats; - - if (!inj_fixed_loaded || !inj_stats_enabled) - PG_RETURN_NULL(); - - pgstat_snapshot_fixed(PGSTAT_KIND_INJECTION_FIXED); - stats = pgstat_get_custom_snapshot_data(PGSTAT_KIND_INJECTION_FIXED); - - /* Initialise attributes information in the tuple descriptor */ - tupdesc = CreateTemplateTupleDesc(5); - TupleDescInitEntry(tupdesc, (AttrNumber) 1, "numattach", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 2, "numdetach", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 3, "numrun", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 4, "numcached", - INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 5, "numloaded", - INT8OID, -1, 0); - BlessTupleDesc(tupdesc); - - values[0] = Int64GetDatum(stats->numattach); - values[1] = Int64GetDatum(stats->numdetach); - values[2] = Int64GetDatum(stats->numrun); - values[3] = Int64GetDatum(stats->numcached); - values[4] = Int64GetDatum(stats->numloaded); - nulls[0] = false; - nulls[1] = false; - nulls[2] = false; - nulls[3] = false; - nulls[4] = false; - - /* Returns the record as Datum */ - PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); -} diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build index d61149712fd7d..fb1418e2caa7d 100644 --- a/src/test/modules/injection_points/meson.build +++ b/src/test/modules/injection_points/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not get_option('injection_points') subdir_done() @@ -6,8 +6,6 @@ endif injection_points_sources = files( 'injection_points.c', - 'injection_stats.c', - 'injection_stats_fixed.c', 'regress_injection.c', ) @@ -37,8 +35,9 @@ tests += { 'injection_points', 'hashagg', 'reindex_conc', + 'vacuum', ], - 'regress_args': ['--dlpath', meson.build_root() / 'src/test/regress'], + 'regress_args': ['--dlpath', meson.project_build_root() / 'src/test/regress'], # The injection points are cluster-wide, so disable installcheck 'runningcheck': false, }, @@ -46,16 +45,17 @@ tests += { 'specs': [ 'basic', 'inplace', + 'repack', + 'repack_toast', 'syscache-update-pruned', + 'heap_lock_update', ], 'runningcheck': false, # see syscache-update-pruned - }, - 'tap': { - 'env': { - 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', - }, - 'tests': [ - 't/001_stats.pl', + # Some tests wait for all snapshots, so avoid parallel execution + 'runningcheck-parallel': false, + # some tests require wal_level=replica + 'regress_args': [ + '--temp-config', files('extra.conf'), ], }, } diff --git a/src/test/modules/injection_points/regress_injection.c b/src/test/modules/injection_points/regress_injection.c index 7bba1c97d0f26..0c3113eac2f6a 100644 --- a/src/test/modules/injection_points/regress_injection.c +++ b/src/test/modules/injection_points/regress_injection.c @@ -3,7 +3,7 @@ * regress_injection.c * Functions supporting test-specific subject matter. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -50,7 +50,7 @@ removable_cutoff(PG_FUNCTION_ARGS) if (!PG_ARGISNULL(0)) rel = table_open(PG_GETARG_OID(0), AccessShareLock); - if (!rel->rd_rel->relisshared && autovacuum_start_daemon) + if (rel != NULL && !rel->rd_rel->relisshared && autovacuum_start_daemon) elog(WARNING, "removable_cutoff(non-shared-rel) can move backward under autovacuum=on"); diff --git a/src/test/modules/injection_points/specs/heap_lock_update.spec b/src/test/modules/injection_points/specs/heap_lock_update.spec new file mode 100644 index 0000000000000..b3992a1eb7a51 --- /dev/null +++ b/src/test/modules/injection_points/specs/heap_lock_update.spec @@ -0,0 +1,117 @@ +# Test race condition in tuple locking +# +# This is a reproducer for the bug reported at: +# https://www.postgresql.org/message-id/CAOG%2BRQ74x0q%3DkgBBQ%3DmezuvOeZBfSxM1qu_o0V28bwDz3dHxLw%40mail.gmail.com +# +# The bug was that when following an update chain when locking tuples, +# we sometimes failed to check that the xmin on the next tuple matched +# the prior's xmax. If the updated tuple version was vacuumed away and +# the slot was reused for an unrelated tuple, we'd incorrectly follow +# and lock the unrelated tuple. + + +# Set up a test table with enough rows to fill a page. We need the +# UPDATE used in the test to put the new tuple on a different page, +# because otherwise the VACUUM cannot remove the aborted tuple because +# we hold a pin on the first page. +# +# The exact number of rows inserted doesn't otherwise matter, but we +# arrange things in a deterministic fashion so that the last inserted +# tuple goes to (1,1), and the updated and aborted tuple goes to +# (1,2). That way we can just memorize those ctids in the expected +# output, to verify that the test exercises the scenario we want. +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE t (id int PRIMARY KEY); + do $$ + DECLARE + i int; + tid tid; + BEGIN + FOR i IN 1..5000 LOOP + INSERT INTO t VALUES (i) RETURNING ctid INTO tid; + IF tid = '(1,1)' THEN + RETURN; + END IF; + END LOOP; + RAISE 'expected to insert tuple to (1,1)'; + END; + $$; +} +teardown +{ + DROP TABLE t; + DROP EXTENSION injection_points; +} + +session s1 +step s1begin { BEGIN; } +step s1update { UPDATE t SET id = 10000 WHERE id = 1 RETURNING ctid; } +step s1abort { ABORT; } +step vacuum { VACUUM t; } + +# Insert a new tuple, and update it. +step reinsert { + INSERT INTO t VALUES (10001) RETURNING ctid; + UPDATE t SET id = 10002 WHERE id = 10001 RETURNING ctid; +} + +# Same as the 'reinsert' step, but for extra confusion, we also stamp +# the original tuple with the same 'xmax' as the re-inserted one. +step reinsert_and_lock { + BEGIN; + INSERT INTO t VALUES (10001) RETURNING ctid; + SELECT ctid, * FROM t WHERE id = 1 FOR UPDATE; + COMMIT; + UPDATE t SET id = 10002 WHERE id = 10001 returning ctid; +} + +step wake { + SELECT FROM injection_points_detach('heap_lock_updated_tuple'); + SELECT FROM injection_points_wakeup('heap_lock_updated_tuple'); +} + +session s2 +setup { + SELECT FROM injection_points_set_local(); + SELECT FROM injection_points_attach('heap_lock_updated_tuple', 'wait'); +} +step s2lock { select * from t where id = 1 for update; } + +permutation + # Begin transaction, update a row. Because of how we set up the + # test table, the updated tuple lands at (1,2) + s1begin + s1update + + # While the updating transaction is open, start a new session that + # tries to lock the row. This blocks on the open transaction. + s2lock + + # Abort the updating transaction. This unblocks session 2, but it + # will immediately hit the injection point and block on that. + s1abort + # Vacuum away the updated, aborted tuple. + vacuum + + # Insert a new tuple. It lands at the same location where the + # updated tuple was. + reinsert + + # Let the locking transaction continue. It should lock the + # original tuple, ignoring the re-inserted tuple. + wake(s2lock) + +# Variant where the re-inserted tuple is also locked by the inserting +# transaction. This failed an earlier version of the fix during +# development. +permutation + s1begin + s1update + s2lock + s1abort + vacuum + reinsert_and_lock + wake(s2lock) diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec new file mode 100644 index 0000000000000..d727a9b056bbb --- /dev/null +++ b/src/test/modules/injection_points/specs/repack.spec @@ -0,0 +1,142 @@ +# REPACK (CONCURRENTLY) ... USING INDEX ...; +setup +{ + CREATE EXTENSION injection_points; + + CREATE TABLE repack_test(i int PRIMARY KEY, j int); + INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j int); + CREATE TABLE data_s2(i int, j int); +} + +teardown +{ + DROP TABLE repack_test; + DROP EXTENSION injection_points; + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +# Change the existing data. UPDATE changes both key and non-key columns. Also +# update one row twice to test whether tuple version generated by this session +# can be found. +step change_existing +{ + UPDATE repack_test SET i=10 where i=1; + UPDATE repack_test SET j=20 where i=2; + UPDATE repack_test SET i=30 where i=3; + UPDATE repack_test SET i=40 where i=30; + DELETE FROM repack_test WHERE i=4; +} +# Insert new rows and UPDATE / DELETE some of them. Again, update both key and +# non-key column. +step change_new +{ + INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8); + UPDATE repack_test SET i=50 where i=5; + UPDATE repack_test SET j=60 where i=6; + DELETE FROM repack_test WHERE i=7; +} + +# When applying concurrent data changes, we should see the effects of an +# in-progress subtransaction. +# +# XXX Not sure this test is useful now - it was designed for the patch that +# preserves tuple visibility and which therefore modifies +# TransactionIdIsCurrentTransactionId(). +step change_subxact1 +{ + BEGIN; + INSERT INTO repack_test(i, j) VALUES (100, 100); + SAVEPOINT s1; + UPDATE repack_test SET i=101 where i=100; + SAVEPOINT s2; + UPDATE repack_test SET i=102 where i=101; + COMMIT; +} + +# When applying concurrent data changes, we should not see the effects of a +# rolled back subtransaction. +# +# XXX Is this test useful? See above. +step change_subxact2 +{ + BEGIN; + SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 110); + ROLLBACK TO SAVEPOINT s1; + INSERT INTO repack_test(i, j) VALUES (110, 111); + COMMIT; +} + +# Check the table from the perspective of s2. +step check2 +{ + INSERT INTO relfilenodes(node) + SELECT relfilenode FROM pg_class WHERE relname='repack_test'; + + SELECT i, j FROM repack_test ORDER BY i, j; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; +} +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + change_existing + change_new + change_subxact1 + change_subxact2 + check2 + wakeup_before_lock + check1 diff --git a/src/test/modules/injection_points/specs/repack_toast.spec b/src/test/modules/injection_points/specs/repack_toast.spec new file mode 100644 index 0000000000000..b878b198971fe --- /dev/null +++ b/src/test/modules/injection_points/specs/repack_toast.spec @@ -0,0 +1,112 @@ +# REPACK (CONCURRENTLY); +# +# Test handling of TOAST. At the same time, no tuplesort. +setup +{ + CREATE EXTENSION injection_points; + + -- Return a string that needs to be TOASTed. + CREATE FUNCTION get_long_string() + RETURNS text + LANGUAGE sql as $$ + SELECT string_agg(chr(65 + trunc(25 * random())::int), '') + FROM generate_series(1, 2048) s(x); + $$; + + CREATE TABLE repack_test(i int PRIMARY KEY, j text); + INSERT INTO repack_test(i, j) VALUES (1, get_long_string()), + (2, get_long_string()), (3, get_long_string()); + + CREATE TABLE relfilenodes(node oid); + + CREATE TABLE data_s1(i int, j text); + CREATE TABLE data_s2(i int, j text); +} + +teardown +{ + DROP TABLE repack_test; + DROP EXTENSION injection_points; + DROP FUNCTION get_long_string(); + + DROP TABLE relfilenodes; + DROP TABLE data_s1; + DROP TABLE data_s2; +} + +session s1 +setup +{ + SELECT injection_points_set_local(); + SELECT injection_points_attach('repack-concurrently-before-lock', 'wait'); +} +# Perform the initial load and wait for s2 to do some data changes. +step wait_before_lock +{ + REPACK (CONCURRENTLY) repack_test; +} +# Check the table from the perspective of s1. +# +# Besides the contents, we also check that relfilenode has changed. + +# Have each session write the contents into a table and use FULL JOIN to check +# if the outputs are identical. +step check1 +{ + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + SELECT count(DISTINCT node) FROM relfilenodes; + + INSERT INTO data_s1(i, j) + SELECT i, j FROM repack_test; + + SELECT count(*) + FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j) + WHERE d1.i ISNULL OR d2.i ISNULL; +} +teardown +{ + SELECT injection_points_detach('repack-concurrently-before-lock'); +} + +session s2 +step change +# Separately test UPDATE of both plain ("i") and TOASTed ("j") attribute. In +# the first case, the new tuple we get from reorderbuffer.c contains "j" as a +# TOAST pointer, which we need to update so it points to the new heap. In the +# latter case, we receive "j" as "external indirect" value - here we test that +# the decoding worker writes the tuple to a file correctly and that the +# backend executing REPACK manages to restore it. +{ + UPDATE repack_test SET j=get_long_string() where i=2; + DELETE FROM repack_test WHERE i=3; + INSERT INTO repack_test(i, j) VALUES (4, get_long_string()); + UPDATE repack_test SET i=3 where i=1; +} +# Check the table from the perspective of s2. +step check2 +{ + INSERT INTO relfilenodes(node) + SELECT c2.relfilenode + FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid + WHERE c1.relname='repack_test'; + + INSERT INTO data_s2(i, j) + SELECT i, j FROM repack_test; +} +step wakeup_before_lock +{ + SELECT injection_points_wakeup('repack-concurrently-before-lock'); +} + +# Test if data changes introduced while one session is performing REPACK +# CONCURRENTLY find their way into the table. +permutation + wait_before_lock + change + check2 + wakeup_before_lock + check1 diff --git a/src/test/modules/injection_points/sql/injection_points.sql b/src/test/modules/injection_points/sql/injection_points.sql index d9748331c7715..ba14df706ef3f 100644 --- a/src/test/modules/injection_points/sql/injection_points.sql +++ b/src/test/modules/injection_points/sql/injection_points.sql @@ -18,6 +18,9 @@ SELECT injection_points_attach('TestInjectionError', 'error'); SELECT injection_points_attach('TestInjectionLog', 'notice'); SELECT injection_points_attach('TestInjectionLog2', 'notice'); +SELECT point_name, library, function FROM injection_points_list() + ORDER BY point_name COLLATE "C"; + SELECT injection_points_run('TestInjectionBooh'); -- nothing SELECT injection_points_run('TestInjectionLog2'); -- notice SELECT injection_points_run('TestInjectionLog2', NULL); -- notice @@ -85,5 +88,27 @@ SELECT injection_points_detach('TestConditionError'); SELECT injection_points_attach('TestConditionLocal1', 'error'); SELECT injection_points_detach('TestConditionLocal1'); +-- Function variant for attach. +SELECT injection_points_attach(repeat('a', 64), 'injection_points', + 'injection_notice', NULL); +SELECT injection_points_attach('TestInjectionNoticeFunc', repeat('a', 128), + 'injection_notice', NULL); +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + repeat('a', 128), NULL); +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + 'injection_notice', repeat('a', 1025)::bytea); +SELECT injection_points_attach(NULL, NULL, NULL, NULL); +SELECT injection_points_attach('TestInjectionNoticeFunc', NULL, NULL, NULL); +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + NULL, NULL); +SELECT injection_points_attach('TestInjectionNoticeFunc', 'injection_points', + 'injection_notice', NULL); +SELECT point_name, library, function FROM injection_points_list() + ORDER BY point_name COLLATE "C"; +SELECT injection_points_run('TestInjectionNoticeFunc', NULL); -- notice +SELECT injection_points_detach('TestInjectionNoticeFunc'); +SELECT point_name, library, function FROM injection_points_list() + ORDER BY point_name COLLATE "C"; + DROP EXTENSION injection_points; DROP FUNCTION wait_pid; diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql new file mode 100644 index 0000000000000..23760dd0f380a --- /dev/null +++ b/src/test/modules/injection_points/sql/vacuum.sql @@ -0,0 +1,47 @@ +-- Tests for VACUUM + +CREATE EXTENSION injection_points; + +SELECT injection_points_set_local(); +SELECT injection_points_attach('vacuum-index-cleanup-auto', 'notice'); +SELECT injection_points_attach('vacuum-index-cleanup-disabled', 'notice'); +SELECT injection_points_attach('vacuum-index-cleanup-enabled', 'notice'); +SELECT injection_points_attach('vacuum-truncate-auto', 'notice'); +SELECT injection_points_attach('vacuum-truncate-disabled', 'notice'); +SELECT injection_points_attach('vacuum-truncate-enabled', 'notice'); + +-- Check state of index_cleanup and truncate in VACUUM. +CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH + (autovacuum_enabled=false, + vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false, + vacuum_truncate=true, toast.vacuum_truncate=false); +CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH + (autovacuum_enabled=false, + vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true, + vacuum_truncate=false, toast.vacuum_truncate=true); +-- Multiple relations should use their options in isolation. +VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on; + +-- Check "auto" case of index_cleanup and "truncate" controlled by +-- its GUC. +CREATE TABLE vac_tab_auto(i int, j text) WITH + (autovacuum_enabled=false, + vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto); +SET vacuum_truncate = false; +VACUUM vac_tab_auto; +SET vacuum_truncate = true; +VACUUM vac_tab_auto; +RESET vacuum_truncate; + +DROP TABLE vac_tab_auto; +DROP TABLE vac_tab_on_toast_off; +DROP TABLE vac_tab_off_toast_on; + +-- Cleanup +SELECT injection_points_detach('vacuum-index-cleanup-auto'); +SELECT injection_points_detach('vacuum-index-cleanup-disabled'); +SELECT injection_points_detach('vacuum-index-cleanup-enabled'); +SELECT injection_points_detach('vacuum-truncate-auto'); +SELECT injection_points_detach('vacuum-truncate-disabled'); +SELECT injection_points_detach('vacuum-truncate-enabled'); +DROP EXTENSION injection_points; diff --git a/src/test/modules/injection_points/t/001_stats.pl b/src/test/modules/injection_points/t/001_stats.pl deleted file mode 100644 index 25de5fc46fe4a..0000000000000 --- a/src/test/modules/injection_points/t/001_stats.pl +++ /dev/null @@ -1,91 +0,0 @@ - -# Copyright (c) 2024-2025, PostgreSQL Global Development Group - -# Tests for Custom Cumulative Statistics. - -use strict; -use warnings FATAL => 'all'; -use locale; - -use PostgreSQL::Test::Cluster; -use PostgreSQL::Test::Utils; -use Test::More; - -# Test persistency of statistics generated for injection points. -if ($ENV{enable_injection_points} ne 'yes') -{ - plan skip_all => 'Injection points not supported by this build'; -} - -# Node initialization -my $node = PostgreSQL::Test::Cluster->new('master'); -$node->init; -$node->append_conf( - 'postgresql.conf', qq( -shared_preload_libraries = 'injection_points' -injection_points.stats = true -)); -$node->start; -$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); - -# This should count for two calls. -$node->safe_psql('postgres', - "SELECT injection_points_attach('stats-notice', 'notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -my $numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '2', 'number of stats calls'); -my $fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '1|0|2|0|0', 'fixed stats after some calls'); - -# Loading and caching. -$node->safe_psql( - 'postgres', " -SELECT injection_points_load('stats-notice'); -SELECT injection_points_cached('stats-notice'); -"); -$fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '1|0|2|1|1', 'fixed stats after loading and caching'); - -# Restart the node cleanly, stats should still be around. -$node->restart; -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '3', 'number of stats after clean restart'); -$fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '1|0|2|1|1', 'fixed stats after clean restart'); - -# On crash the stats are gone. -$node->stop('immediate'); -$node->start; -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '', 'number of stats after crash'); -$fixedstats = $node->safe_psql('postgres', - "SELECT * FROM injection_points_stats_fixed();"); -is($fixedstats, '0|0|0|0|0', 'fixed stats after crash'); - -# On drop all stats are gone -$node->safe_psql('postgres', - "SELECT injection_points_attach('stats-notice', 'notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -$node->safe_psql('postgres', "SELECT injection_points_run('stats-notice');"); -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '2', 'number of stats calls'); -$node->safe_psql('postgres', "SELECT injection_points_stats_drop();"); -$numcalls = $node->safe_psql('postgres', - "SELECT injection_points_stats_numcalls('stats-notice');"); -is($numcalls, '', 'no stats after drop via SQL function'); - -# Stop the server, disable the module, then restart. The server -# should be able to come up. -$node->stop; -$node->adjust_conf('postgresql.conf', 'shared_preload_libraries', "''"); -$node->start; - -done_testing(); diff --git a/src/test/modules/ldap_password_func/Makefile b/src/test/modules/ldap_password_func/Makefile index bb080135454dc..558505812aee0 100644 --- a/src/test/modules/ldap_password_func/Makefile +++ b/src/test/modules/ldap_password_func/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # ldap_password_func Makefile diff --git a/src/test/modules/ldap_password_func/ldap_password_func.c b/src/test/modules/ldap_password_func/ldap_password_func.c index 79c023bfc68c9..f57bdf087c162 100644 --- a/src/test/modules/ldap_password_func/ldap_password_func.c +++ b/src/test/modules/ldap_password_func/ldap_password_func.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * ldap_password_func.c * diff --git a/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl b/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl index 5dc1e442d299f..0f254939df5d5 100644 --- a/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl +++ b/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index 9a3c0236325c6..aa0a6bbe762ff 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -3,7 +3,7 @@ * libpq_pipeline.c * Verify libpq pipeline execution functionality * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -88,20 +88,67 @@ pg_fatal_impl(int line, const char *fmt,...) } /* - * Check that the query on the given connection got canceled. + * Check that libpq next returns a PGresult with the specified status, + * returning the PGresult so that caller can perform additional checks. */ -#define confirm_query_canceled(conn) confirm_query_canceled_impl(__LINE__, conn) -static void -confirm_query_canceled_impl(int line, PGconn *conn) +#define confirm_result_status(conn, status) confirm_result_status_impl(__LINE__, conn, status) +static PGresult * +confirm_result_status_impl(int line, PGconn *conn, ExecStatusType status) { - PGresult *res = NULL; + PGresult *res; res = PQgetResult(conn); if (res == NULL) - pg_fatal_impl(line, "PQgetResult returned null: %s", + pg_fatal_impl(line, "PQgetResult returned null unexpectedly: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_FATAL_ERROR) - pg_fatal_impl(line, "query did not fail when it was expected"); + if (PQresultStatus(res) != status) + pg_fatal_impl(line, "PQgetResult returned status %s, expected %s: %s", + PQresStatus(PQresultStatus(res)), + PQresStatus(status), + PQerrorMessage(conn)); + return res; +} + +/* + * Check that libpq next returns a PGresult with the specified status, + * then free the PGresult. + */ +#define consume_result_status(conn, status) consume_result_status_impl(__LINE__, conn, status) +static void +consume_result_status_impl(int line, PGconn *conn, ExecStatusType status) +{ + PGresult *res; + + res = confirm_result_status_impl(line, conn, status); + PQclear(res); +} + +/* + * Check that libpq next returns a null PGresult. + */ +#define consume_null_result(conn) consume_null_result_impl(__LINE__, conn) +static void +consume_null_result_impl(int line, PGconn *conn) +{ + PGresult *res; + + res = PQgetResult(conn); + if (res != NULL) + pg_fatal_impl(line, "expected NULL PGresult, got %s: %s", + PQresStatus(PQresultStatus(res)), + PQerrorMessage(conn)); +} + +/* + * Check that the query on the given connection got canceled. + */ +#define consume_query_cancel(conn) consume_query_cancel_impl(__LINE__, conn) +static void +consume_query_cancel_impl(int line, PGconn *conn) +{ + PGresult *res; + + res = confirm_result_status_impl(line, conn, PGRES_FATAL_ERROR); if (strcmp(PQresultErrorField(res, PG_DIAG_SQLSTATE), "57014") != 0) pg_fatal_impl(line, "query failed with a different error than cancellation: %s", PQerrorMessage(conn)); @@ -213,8 +260,8 @@ copy_connection(PGconn *conn) nopts++; nopts++; /* for the NULL terminator */ - keywords = pg_malloc(sizeof(char *) * nopts); - vals = pg_malloc(sizeof(char *) * nopts); + keywords = pg_malloc_array(const char *, nopts); + vals = pg_malloc_array(const char *, nopts); i = 0; for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt) @@ -234,6 +281,10 @@ copy_connection(PGconn *conn) pg_fatal("Connection to database failed: %s", PQerrorMessage(copyConn)); + pfree(keywords); + pfree(vals); + PQconninfoFree(opts); + return copyConn; } @@ -265,13 +316,13 @@ test_cancel(PGconn *conn) cancel = PQgetCancel(conn); if (!PQcancel(cancel, errorbuf, sizeof(errorbuf))) pg_fatal("failed to run PQcancel: %s", errorbuf); - confirm_query_canceled(conn); + consume_query_cancel(conn); /* PGcancel object can be reused for the next query */ send_cancellable_query(conn, monitorConn); if (!PQcancel(cancel, errorbuf, sizeof(errorbuf))) pg_fatal("failed to run PQcancel: %s", errorbuf); - confirm_query_canceled(conn); + consume_query_cancel(conn); PQfreeCancel(cancel); @@ -279,14 +330,14 @@ test_cancel(PGconn *conn) send_cancellable_query(conn, monitorConn); if (!PQrequestCancel(conn)) pg_fatal("failed to run PQrequestCancel: %s", PQerrorMessage(conn)); - confirm_query_canceled(conn); + consume_query_cancel(conn); /* test PQcancelBlocking */ send_cancellable_query(conn, monitorConn); cancelConn = PQcancelCreate(conn); if (!PQcancelBlocking(cancelConn)) pg_fatal("failed to run PQcancelBlocking: %s", PQcancelErrorMessage(cancelConn)); - confirm_query_canceled(conn); + consume_query_cancel(conn); PQcancelFinish(cancelConn); /* test PQcancelCreate and then polling with PQcancelPoll */ @@ -340,7 +391,7 @@ test_cancel(PGconn *conn) } if (PQcancelStatus(cancelConn) != CONNECTION_OK) pg_fatal("unexpected cancel connection status: %s", PQcancelErrorMessage(cancelConn)); - confirm_query_canceled(conn); + consume_query_cancel(conn); /* * test PQcancelReset works on the cancel connection and it can be reused @@ -397,9 +448,10 @@ test_cancel(PGconn *conn) } if (PQcancelStatus(cancelConn) != CONNECTION_OK) pg_fatal("unexpected cancel connection status: %s", PQcancelErrorMessage(cancelConn)); - confirm_query_canceled(conn); + consume_query_cancel(conn); PQcancelFinish(cancelConn); + PQfinish(monitorConn); fprintf(stderr, "ok\n"); } @@ -428,6 +480,7 @@ test_disallowed_in_pipeline(PGconn *conn) "synchronous command execution functions are not allowed in pipeline mode\n") != 0) pg_fatal("did not get expected error message; got: \"%s\"", PQerrorMessage(conn)); + PQclear(res); /* PQsendQuery should fail in pipeline mode */ if (PQsendQuery(conn, "SELECT 1") != 0) @@ -460,6 +513,7 @@ test_disallowed_in_pipeline(PGconn *conn) if (PQresultStatus(res) != PGRES_TUPLES_OK) pg_fatal("PQexec should succeed after exiting pipeline mode but failed with: %s", PQerrorMessage(conn)); + PQclear(res); fprintf(stderr, "ok\n"); } @@ -467,7 +521,6 @@ test_disallowed_in_pipeline(PGconn *conn) static void test_multi_pipelines(PGconn *conn) { - PGresult *res = NULL; const char *dummy_params[1] = {"1"}; Oid dummy_param_oids[1] = {INT4OID}; @@ -508,87 +561,31 @@ test_multi_pipelines(PGconn *conn) /* OK, start processing the results */ /* first pipeline */ + consume_result_status(conn, PGRES_TUPLES_OK); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when there's a pipeline item: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Unexpected result code %s from first pipeline item", - PQresStatus(PQresultStatus(res))); - PQclear(res); - res = NULL; - - if (PQgetResult(conn) != NULL) - pg_fatal("PQgetResult returned something extra after first result"); + consume_null_result(conn); if (PQexitPipelineMode(conn) != 0) pg_fatal("exiting pipeline mode after query but before sync succeeded incorrectly"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when sync result expected: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s instead of sync result, error: %s", - PQresStatus(PQresultStatus(res)), PQerrorMessage(conn)); - PQclear(res); + consume_result_status(conn, PGRES_PIPELINE_SYNC); /* second pipeline */ + consume_result_status(conn, PGRES_TUPLES_OK); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when there's a pipeline item: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Unexpected result code %s from second pipeline item", - PQresStatus(PQresultStatus(res))); - PQclear(res); - res = NULL; - - if (PQgetResult(conn) != NULL) - pg_fatal("PQgetResult returned something extra after first result"); + consume_null_result(conn); if (PQexitPipelineMode(conn) != 0) pg_fatal("exiting pipeline mode after query but before sync succeeded incorrectly"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when sync result expected: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s instead of sync result, error: %s", - PQresStatus(PQresultStatus(res)), PQerrorMessage(conn)); - PQclear(res); + consume_result_status(conn, PGRES_PIPELINE_SYNC); /* third pipeline */ + consume_result_status(conn, PGRES_TUPLES_OK); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when there's a pipeline item: %s", - PQerrorMessage(conn)); + consume_null_result(conn); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Unexpected result code %s from third pipeline item", - PQresStatus(PQresultStatus(res))); - - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("Expected null result, got %s", - PQresStatus(PQresultStatus(res))); - - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when there's a pipeline item: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s from second pipeline sync", - PQresStatus(PQresultStatus(res))); + consume_result_status(conn, PGRES_PIPELINE_SYNC); /* We're still in pipeline mode ... */ if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) @@ -657,36 +654,17 @@ test_nosync(PGconn *conn) /* Now read all results */ for (;;) { - PGresult *res; - - res = PQgetResult(conn); - - /* NULL results are only expected after TUPLES_OK */ - if (res == NULL) - pg_fatal("got unexpected NULL result after %d results", results); - /* We expect exactly one TUPLES_OK result for each query we sent */ - if (PQresultStatus(res) == PGRES_TUPLES_OK) - { - PGresult *res2; + consume_result_status(conn, PGRES_TUPLES_OK); - /* and one NULL result should follow each */ - res2 = PQgetResult(conn); - if (res2 != NULL) - pg_fatal("expected NULL, got %s", - PQresStatus(PQresultStatus(res2))); - PQclear(res); - results++; + /* and one NULL result should follow each */ + consume_null_result(conn); - /* if we're done, we're done */ - if (results == numqueries) - break; + results++; - continue; - } - - /* anything else is unexpected */ - pg_fatal("got unexpected %s\n", PQresStatus(PQresultStatus(res))); + /* if we're done, we're done */ + if (results == numqueries) + break; } fprintf(stderr, "ok\n"); @@ -716,10 +694,12 @@ test_pipeline_abort(PGconn *conn) res = PQexec(conn, drop_table_sql); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("dispatching DROP TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); res = PQexec(conn, create_table_sql); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("dispatching CREATE TABLE failed: %s", PQerrorMessage(conn)); + PQclear(res); /* * Queue up a couple of small pipelines and process each without returning @@ -763,33 +743,16 @@ test_pipeline_abort(PGconn *conn) * a pipeline aborted message for the second insert, a pipeline-end, then * a command-ok and a pipeline-ok for the second pipeline operation. */ - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("Unexpected result status %s: %s", - PQresStatus(PQresultStatus(res)), - PQresultErrorMessage(res)); - PQclear(res); + consume_result_status(conn, PGRES_COMMAND_OK); /* NULL result to signal end-of-results for this command */ - if ((res = PQgetResult(conn)) != NULL) - pg_fatal("Expected null result, got %s", - PQresStatus(PQresultStatus(res))); + consume_null_result(conn); /* Second query caused error, so we expect an error next */ - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_FATAL_ERROR) - pg_fatal("Unexpected result code -- expected PGRES_FATAL_ERROR, got %s", - PQresStatus(PQresultStatus(res))); - PQclear(res); + consume_result_status(conn, PGRES_FATAL_ERROR); /* NULL result to signal end-of-results for this command */ - if ((res = PQgetResult(conn)) != NULL) - pg_fatal("Expected null result, got %s", - PQresStatus(PQresultStatus(res))); + consume_null_result(conn); /* * pipeline should now be aborted. @@ -802,17 +765,10 @@ test_pipeline_abort(PGconn *conn) pg_fatal("pipeline should be flagged as aborted but isn't"); /* third query in pipeline, the second insert */ - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_PIPELINE_ABORTED) - pg_fatal("Unexpected result code -- expected PGRES_PIPELINE_ABORTED, got %s", - PQresStatus(PQresultStatus(res))); - PQclear(res); + consume_result_status(conn, PGRES_PIPELINE_ABORTED); /* NULL result to signal end-of-results for this command */ - if ((res = PQgetResult(conn)) != NULL) - pg_fatal("Expected null result, got %s", PQresStatus(PQresultStatus(res))); + consume_null_result(conn); if (PQpipelineStatus(conn) != PQ_PIPELINE_ABORTED) pg_fatal("pipeline should be flagged as aborted but isn't"); @@ -827,14 +783,7 @@ test_pipeline_abort(PGconn *conn) * (This is so clients know to start processing results normally again and * can tell the difference between skipped commands and the sync.) */ - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code from first pipeline sync\n" - "Expected PGRES_PIPELINE_SYNC, got %s", - PQresStatus(PQresultStatus(res))); - PQclear(res); + consume_result_status(conn, PGRES_PIPELINE_SYNC); if (PQpipelineStatus(conn) == PQ_PIPELINE_ABORTED) pg_fatal("sync should've cleared the aborted flag but didn't"); @@ -844,30 +793,16 @@ test_pipeline_abort(PGconn *conn) pg_fatal("Fell out of pipeline mode somehow"); /* the insert from the second pipeline */ - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("Unexpected result code %s from first item in second pipeline", - PQresStatus(PQresultStatus(res))); - PQclear(res); + consume_result_status(conn, PGRES_COMMAND_OK); /* Read the NULL result at the end of the command */ - if ((res = PQgetResult(conn)) != NULL) - pg_fatal("Expected null result, got %s", PQresStatus(PQresultStatus(res))); + consume_null_result(conn); /* the second pipeline sync */ - if ((res = PQgetResult(conn)) == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s from second pipeline sync", - PQresStatus(PQresultStatus(res))); - PQclear(res); + consume_result_status(conn, PGRES_PIPELINE_SYNC); - if ((res = PQgetResult(conn)) != NULL) - pg_fatal("Expected null result, got %s: %s", - PQresStatus(PQresultStatus(res)), - PQerrorMessage(conn)); + /* Read the NULL result at the end of the command */ + consume_null_result(conn); /* Try to send two queries in one command */ if (PQsendQueryParams(conn, "SELECT 1; SELECT 2", 0, NULL, NULL, NULL, NULL, 0) != 1) @@ -890,15 +825,14 @@ test_pipeline_abort(PGconn *conn) pg_fatal("got unexpected status %s", PQresStatus(PQresultStatus(res))); break; } + PQclear(res); } if (!goterror) pg_fatal("did not get cannot-insert-multiple-commands error"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("got NULL result"); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s from pipeline sync", - PQresStatus(PQresultStatus(res))); + + /* the second pipeline sync */ + consume_result_status(conn, PGRES_PIPELINE_SYNC); + fprintf(stderr, "ok\n"); /* Test single-row mode with an error partways */ @@ -935,13 +869,9 @@ test_pipeline_abort(PGconn *conn) pg_fatal("did not get division-by-zero error"); if (gotrows != 3) pg_fatal("did not get three rows"); + /* the third pipeline sync */ - if ((res = PQgetResult(conn)) == NULL) - pg_fatal("Unexpected NULL result: %s", PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s from third pipeline sync", - PQresStatus(PQresultStatus(res))); - PQclear(res); + consume_result_status(conn, PGRES_PIPELINE_SYNC); /* We're still in pipeline mode... */ if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) @@ -1274,21 +1204,11 @@ test_prepared(PGconn *conn) if (PQpipelineSync(conn) != 1) pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null"); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); - PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("expected NULL result"); + consume_result_status(conn, PGRES_COMMAND_OK); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned NULL"); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + consume_null_result(conn); + + res = confirm_result_status(conn, PGRES_COMMAND_OK); if (PQnfields(res) != lengthof(expected_oids)) pg_fatal("expected %zu columns, got %d", lengthof(expected_oids), PQnfields(res)); @@ -1300,13 +1220,10 @@ test_prepared(PGconn *conn) i, expected_oids[i], typ); } PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("expected NULL result"); - res = PQgetResult(conn); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("expected PGRES_PIPELINE_SYNC, got %s", PQresStatus(PQresultStatus(res))); + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); fprintf(stderr, "closing statement.."); if (PQsendClosePrepared(conn, "select_one") != 1) @@ -1314,18 +1231,11 @@ test_prepared(PGconn *conn) if (PQpipelineSync(conn) != 1) pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("expected non-NULL result"); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); - PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("expected NULL result"); - res = PQgetResult(conn); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("expected PGRES_PIPELINE_SYNC, got %s", PQresStatus(PQresultStatus(res))); + consume_result_status(conn, PGRES_COMMAND_OK); + + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); if (PQexitPipelineMode(conn) != 1) pg_fatal("could not exit pipeline mode: %s", PQerrorMessage(conn)); @@ -1334,6 +1244,7 @@ test_prepared(PGconn *conn) res = PQdescribePrepared(conn, "select_one"); if (PQresultStatus(res) != PGRES_FATAL_ERROR) pg_fatal("expected FATAL_ERROR, got %s", PQresStatus(PQresultStatus(res))); + PQclear(res); /* * Also test the blocking close, this should not fail since closing a @@ -1342,32 +1253,36 @@ test_prepared(PGconn *conn) res = PQclosePrepared(conn, "select_one"); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + PQclear(res); fprintf(stderr, "creating portal... "); - PQexec(conn, "BEGIN"); - PQexec(conn, "DECLARE cursor_one CURSOR FOR SELECT 1"); + + res = PQexec(conn, "BEGIN"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("BEGIN failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "DECLARE cursor_one CURSOR FOR SELECT 1"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("DECLARE CURSOR failed: %s", PQerrorMessage(conn)); + PQclear(res); + PQenterPipelineMode(conn); if (PQsendDescribePortal(conn, "cursor_one") != 1) pg_fatal("PQsendDescribePortal failed: %s", PQerrorMessage(conn)); if (PQpipelineSync(conn) != 1) pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null"); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + res = confirm_result_status(conn, PGRES_COMMAND_OK); typ = PQftype(res, 0); if (typ != INT4OID) pg_fatal("portal: expected type %u, got %u", INT4OID, typ); PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("expected NULL result"); - res = PQgetResult(conn); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("expected PGRES_PIPELINE_SYNC, got %s", PQresStatus(PQresultStatus(res))); + + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); fprintf(stderr, "closing portal... "); if (PQsendClosePortal(conn, "cursor_one") != 1) @@ -1375,18 +1290,11 @@ test_prepared(PGconn *conn) if (PQpipelineSync(conn) != 1) pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("expected non-NULL result"); - if (PQresultStatus(res) != PGRES_COMMAND_OK) - pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); - PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("expected NULL result"); - res = PQgetResult(conn); - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("expected PGRES_PIPELINE_SYNC, got %s", PQresStatus(PQresultStatus(res))); + consume_result_status(conn, PGRES_COMMAND_OK); + + consume_null_result(conn); + + consume_result_status(conn, PGRES_PIPELINE_SYNC); if (PQexitPipelineMode(conn) != 1) pg_fatal("could not exit pipeline mode: %s", PQerrorMessage(conn)); @@ -1395,6 +1303,7 @@ test_prepared(PGconn *conn) res = PQdescribePortal(conn, "cursor_one"); if (PQresultStatus(res) != PGRES_FATAL_ERROR) pg_fatal("expected FATAL_ERROR, got %s", PQresStatus(PQresultStatus(res))); + PQclear(res); /* * Also test the blocking close, this should not fail since closing a @@ -1403,6 +1312,7 @@ test_prepared(PGconn *conn) res = PQclosePortal(conn, "cursor_one"); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("expected COMMAND_OK, got %s", PQresStatus(PQresultStatus(res))); + PQclear(res); fprintf(stderr, "ok\n"); } @@ -1418,37 +1328,55 @@ test_protocol_version(PGconn *conn) int nopts; PQconninfoOption *opts = PQconninfo(conn); int protocol_version; - int max_protocol_version_index; + int max_protocol_version_index = -1; int i; - /* - * Prepare keywords/vals arrays, copied from the existing connection, with - * an extra slot for 'max_protocol_version'. - */ + /* Prepare keywords/vals arrays, copied from the existing connection. */ nopts = 0; for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt) nopts++; - nopts++; /* max_protocol_version */ nopts++; /* NULL terminator */ - keywords = pg_malloc0(sizeof(char *) * nopts); - vals = pg_malloc0(sizeof(char *) * nopts); + keywords = pg_malloc0_array(const char *, nopts); + vals = pg_malloc0_array(const char *, nopts); i = 0; for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt) { - if (opt->val) - { - keywords[i] = opt->keyword; - vals[i] = opt->val; - i++; - } + /* + * If the test already specified max_protocol_version, we want to + * replace it rather than attempting to override it. This matters when + * testing defaults, because empty option values at the end of the + * connection string won't replace earlier settings. + */ + if (strcmp(opt->keyword, "max_protocol_version") == 0) + max_protocol_version_index = i; + else if (!opt->val) + continue; + + keywords[i] = opt->keyword; + vals[i] = opt->val; + + i++; } - max_protocol_version_index = i; - keywords[i] = "max_protocol_version"; /* value is filled in below */ - i++; - keywords[i] = vals[i] = NULL; + Assert(max_protocol_version_index >= 0); + + /* + * Test default protocol_version (GREASE - should negotiate down to 3.2) + */ + vals[max_protocol_version_index] = ""; + conn = PQconnectdbParams(keywords, vals, false); + + if (PQstatus(conn) != CONNECTION_OK) + pg_fatal("Connection to database failed: %s", + PQerrorMessage(conn)); + + protocol_version = PQfullProtocolVersion(conn); + if (protocol_version != 30002) + pg_fatal("expected 30002, got %d", protocol_version); + + PQfinish(conn); /* * Test max_protocol_version=3.0 @@ -1509,6 +1437,10 @@ test_protocol_version(PGconn *conn) pg_fatal("expected 30002, got %d", protocol_version); PQfinish(conn); + + pfree(keywords); + pfree(vals); + PQconninfoFree(opts); } /* Notice processor: print notices, and count how many we got */ @@ -1525,7 +1457,6 @@ notice_processor(void *arg, const char *message) static void test_pipeline_idle(PGconn *conn) { - PGresult *res; int n_notices = 0; fprintf(stderr, "\npipeline idle...\n"); @@ -1538,17 +1469,11 @@ test_pipeline_idle(PGconn *conn) if (PQsendQueryParams(conn, "SELECT 1", 0, NULL, NULL, NULL, NULL, 0) != 1) pg_fatal("failed to send query: %s", PQerrorMessage(conn)); PQsendFlushRequest(conn); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when there's a pipeline item: %s", - PQerrorMessage(conn)); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("unexpected result code %s from first pipeline item", - PQresStatus(PQresultStatus(res))); - PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("did not receive terminating NULL"); + + consume_result_status(conn, PGRES_TUPLES_OK); + + consume_null_result(conn); + if (PQsendQueryParams(conn, "SELECT 2", 0, NULL, NULL, NULL, NULL, 0) != 1) pg_fatal("failed to send query: %s", PQerrorMessage(conn)); if (PQexitPipelineMode(conn) == 1) @@ -1558,14 +1483,11 @@ test_pipeline_idle(PGconn *conn) pg_fatal("did not get expected error; got: %s", PQerrorMessage(conn)); PQsendFlushRequest(conn); - res = PQgetResult(conn); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("unexpected result code %s from second pipeline item", - PQresStatus(PQresultStatus(res))); - PQclear(res); - res = PQgetResult(conn); - if (res != NULL) - pg_fatal("did not receive terminating NULL"); + + consume_result_status(conn, PGRES_TUPLES_OK); + + consume_null_result(conn); + if (PQexitPipelineMode(conn) != 1) pg_fatal("exiting pipeline failed: %s", PQerrorMessage(conn)); @@ -1579,11 +1501,9 @@ test_pipeline_idle(PGconn *conn) if (PQsendQueryParams(conn, "SELECT pg_catalog.pg_advisory_unlock(1,1)", 0, NULL, NULL, NULL, NULL, 0) != 1) pg_fatal("failed to send query: %s", PQerrorMessage(conn)); PQsendFlushRequest(conn); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL result received"); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("unexpected result code %s", PQresStatus(PQresultStatus(res))); + + consume_result_status(conn, PGRES_TUPLES_OK); + if (PQexitPipelineMode(conn) != 1) pg_fatal("failed to exit pipeline mode: %s", PQerrorMessage(conn)); fprintf(stderr, "ok - 2\n"); @@ -1592,7 +1512,6 @@ test_pipeline_idle(PGconn *conn) static void test_simple_pipeline(PGconn *conn) { - PGresult *res = NULL; const char *dummy_params[1] = {"1"}; Oid dummy_param_oids[1] = {INT4OID}; @@ -1623,20 +1542,9 @@ test_simple_pipeline(PGconn *conn) if (PQpipelineSync(conn) != 1) pg_fatal("pipeline sync failed: %s", PQerrorMessage(conn)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when there's a pipeline item: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Unexpected result code %s from first pipeline item", - PQresStatus(PQresultStatus(res))); + consume_result_status(conn, PGRES_TUPLES_OK); - PQclear(res); - res = NULL; - - if (PQgetResult(conn) != NULL) - pg_fatal("PQgetResult returned something extra after first query result."); + consume_null_result(conn); /* * Even though we've processed the result there's still a sync to come and @@ -1645,21 +1553,9 @@ test_simple_pipeline(PGconn *conn) if (PQexitPipelineMode(conn) != 0) pg_fatal("exiting pipeline mode after query but before sync succeeded incorrectly"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("PQgetResult returned null when sync result PGRES_PIPELINE_SYNC expected: %s", - PQerrorMessage(conn)); - - if (PQresultStatus(res) != PGRES_PIPELINE_SYNC) - pg_fatal("Unexpected result code %s instead of PGRES_PIPELINE_SYNC, error: %s", - PQresStatus(PQresultStatus(res)), PQerrorMessage(conn)); + consume_result_status(conn, PGRES_PIPELINE_SYNC); - PQclear(res); - res = NULL; - - if (PQgetResult(conn) != NULL) - pg_fatal("PQgetResult returned something extra after pipeline end: %s", - PQresStatus(PQresultStatus(res))); + consume_null_result(conn); /* We're still in pipeline mode... */ if (PQpipelineStatus(conn) == PQ_PIPELINE_OFF) @@ -1698,7 +1594,7 @@ test_singlerowmode(PGconn *conn) "SELECT generate_series(42, $1)", 1, NULL, - (const char **) param, + (const char *const *) param, NULL, NULL, 0) != 1) @@ -1792,20 +1688,12 @@ test_singlerowmode(PGconn *conn) pg_fatal("failed to send flush request"); if (PQsetSingleRowMode(conn) != 1) pg_fatal("PQsetSingleRowMode() failed"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL"); - if (PQresultStatus(res) != PGRES_SINGLE_TUPLE) - pg_fatal("Expected PGRES_SINGLE_TUPLE, got %s", - PQresStatus(PQresultStatus(res))); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL"); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Expected PGRES_TUPLES_OK, got %s", - PQresStatus(PQresultStatus(res))); - if (PQgetResult(conn) != NULL) - pg_fatal("expected NULL result"); + + consume_result_status(conn, PGRES_SINGLE_TUPLE); + + consume_result_status(conn, PGRES_TUPLES_OK); + + consume_null_result(conn); if (PQsendQueryParams(conn, "SELECT 1", 0, NULL, NULL, NULL, NULL, 0) != 1) @@ -1813,14 +1701,10 @@ test_singlerowmode(PGconn *conn) PQerrorMessage(conn)); if (PQsendFlushRequest(conn) != 1) pg_fatal("failed to send flush request"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL"); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Expected PGRES_TUPLES_OK, got %s", - PQresStatus(PQresultStatus(res))); - if (PQgetResult(conn) != NULL) - pg_fatal("expected NULL result"); + + consume_result_status(conn, PGRES_TUPLES_OK); + + consume_null_result(conn); /* * Try chunked mode as well; make sure that it correctly delivers a @@ -1834,33 +1718,23 @@ test_singlerowmode(PGconn *conn) pg_fatal("failed to send flush request"); if (PQsetChunkedRowsMode(conn, 3) != 1) pg_fatal("PQsetChunkedRowsMode() failed"); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL"); - if (PQresultStatus(res) != PGRES_TUPLES_CHUNK) - pg_fatal("Expected PGRES_TUPLES_CHUNK, got %s: %s", - PQresStatus(PQresultStatus(res)), - PQerrorMessage(conn)); + + res = confirm_result_status(conn, PGRES_TUPLES_CHUNK); if (PQntuples(res) != 3) pg_fatal("Expected 3 rows, got %d", PQntuples(res)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL"); - if (PQresultStatus(res) != PGRES_TUPLES_CHUNK) - pg_fatal("Expected PGRES_TUPLES_CHUNK, got %s", - PQresStatus(PQresultStatus(res))); + PQclear(res); + + res = confirm_result_status(conn, PGRES_TUPLES_CHUNK); if (PQntuples(res) != 2) pg_fatal("Expected 2 rows, got %d", PQntuples(res)); - res = PQgetResult(conn); - if (res == NULL) - pg_fatal("unexpected NULL"); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - pg_fatal("Expected PGRES_TUPLES_OK, got %s", - PQresStatus(PQresultStatus(res))); + PQclear(res); + + res = confirm_result_status(conn, PGRES_TUPLES_OK); if (PQntuples(res) != 0) pg_fatal("Expected 0 rows, got %d", PQntuples(res)); - if (PQgetResult(conn) != NULL) - pg_fatal("expected NULL result"); + PQclear(res); + + consume_null_result(conn); if (PQexitPipelineMode(conn) != 1) pg_fatal("failed to end pipeline mode: %s", PQerrorMessage(conn)); @@ -1995,9 +1869,8 @@ test_transaction(PGconn *conn) if (num_syncs <= 0) break; } - if (PQgetResult(conn) != NULL) - pg_fatal("returned something extra after all the syncs: %s", - PQresStatus(PQresultStatus(res))); + + consume_null_result(conn); if (PQexitPipelineMode(conn) != 1) pg_fatal("failed to end pipeline mode: %s", PQerrorMessage(conn)); @@ -2053,16 +1926,19 @@ test_uniqviol(PGconn *conn) "create table ppln_uniqviol(id bigint primary key, idata bigint)"); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("failed to create table: %s", PQerrorMessage(conn)); + PQclear(res); res = PQexec(conn, "begin"); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("failed to begin transaction: %s", PQerrorMessage(conn)); + PQclear(res); res = PQprepare(conn, "insertion", "insert into ppln_uniqviol values ($1, $2) returning id", 2, paramTypes); - if (res == NULL || PQresultStatus(res) != PGRES_COMMAND_OK) + if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("failed to prepare query: %s", PQerrorMessage(conn)); + PQclear(res); if (PQenterPipelineMode(conn) != 1) pg_fatal("failed to enter pipeline mode"); @@ -2191,7 +2067,6 @@ test_uniqviol(PGconn *conn) static bool process_result(PGconn *conn, PGresult *res, int results, int numsent) { - PGresult *res2; bool got_error = false; if (res == NULL) @@ -2203,29 +2078,19 @@ process_result(PGconn *conn, PGresult *res, int results, int numsent) got_error = true; fprintf(stderr, "result %d/%d (error): %s\n", results, numsent, PQerrorMessage(conn)); PQclear(res); - - res2 = PQgetResult(conn); - if (res2 != NULL) - pg_fatal("expected NULL, got %s", - PQresStatus(PQresultStatus(res2))); + consume_null_result(conn); break; case PGRES_TUPLES_OK: fprintf(stderr, "result %d/%d: %s\n", results, numsent, PQgetvalue(res, 0, 0)); PQclear(res); - - res2 = PQgetResult(conn); - if (res2 != NULL) - pg_fatal("expected NULL, got %s", - PQresStatus(PQresultStatus(res2))); + consume_null_result(conn); break; case PGRES_PIPELINE_ABORTED: fprintf(stderr, "result %d/%d: pipeline aborted\n", results, numsent); - res2 = PQgetResult(conn); - if (res2 != NULL) - pg_fatal("expected NULL, got %s", - PQresStatus(PQresultStatus(res2))); + PQclear(res); + consume_null_result(conn); break; default: @@ -2271,7 +2136,7 @@ main(int argc, char **argv) { const char *conninfo = ""; PGconn *conn; - FILE *trace; + FILE *trace = NULL; char *testname; int numrows = 10000; PGresult *res; @@ -2332,9 +2197,11 @@ main(int argc, char **argv) res = PQexec(conn, "SET lc_messages TO \"C\""); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("failed to set \"lc_messages\": %s", PQerrorMessage(conn)); + PQclear(res); res = PQexec(conn, "SET debug_parallel_query = off"); if (PQresultStatus(res) != PGRES_COMMAND_OK) pg_fatal("failed to set \"debug_parallel_query\": %s", PQerrorMessage(conn)); + PQclear(res); /* Set the trace file, if requested */ if (tracefile != NULL) @@ -2388,5 +2255,9 @@ main(int argc, char **argv) /* close the connection to the database and cleanup */ PQfinish(conn); + + if (trace && trace != stdout) + fclose(trace); + return 0; } diff --git a/src/test/modules/libpq_pipeline/meson.build b/src/test/modules/libpq_pipeline/meson.build index 3fd70a04a38d9..5bb895d8548fa 100644 --- a/src/test/modules/libpq_pipeline/meson.build +++ b/src/test/modules/libpq_pipeline/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group libpq_pipeline_sources = files( 'libpq_pipeline.c', diff --git a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl index 61524bdbd8f28..f2c72581f9478 100644 --- a/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl +++ b/src/test/modules/libpq_pipeline/t/001_libpq_pipeline.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -53,7 +53,8 @@ BEGIN $node->command_ok( [ 'libpq_pipeline', @extraargs, - $testname, $node->connstr('postgres') . " max_protocol_version=latest" + $testname, + $node->connstr('postgres') . " max_protocol_version=latest" ], "libpq_pipeline $testname"); @@ -76,7 +77,8 @@ BEGIN # test separately that it still works the old protocol version too. $node->command_ok( [ - 'libpq_pipeline', 'cancel', $node->connstr('postgres') . " max_protocol_version=3.0" + 'libpq_pipeline', 'cancel', + $node->connstr('postgres') . " max_protocol_version=3.0" ], "libpq_pipeline cancel with protocol 3.0"); diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace index cf6ccec6b9d19..3e5007d13b238 100644 --- a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace +++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace @@ -27,7 +27,7 @@ B 4 ParseComplete B 4 BindComplete B 4 NoData B 15 CommandComplete "INSERT 0 1" -B NN ErrorResponse S "ERROR" V "ERROR" C "42883" M "function no_such_function(integer) does not exist" H "No function matches the given name and argument types. You might need to add explicit type casts." P "8" F "SSSS" L "SSSS" R "SSSS" \x00 +B NN ErrorResponse S "ERROR" V "ERROR" C "42883" M "function no_such_function(integer) does not exist" D "There is no function of that name." P "8" F "SSSS" L "SSSS" R "SSSS" \x00 B 5 ReadyForQuery I B 4 ParseComplete B 4 BindComplete diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 9de0057bd1d43..4bca42bb3706a 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group subdir('brin') subdir('commit_ts') @@ -6,36 +6,51 @@ subdir('delay_execution') subdir('dummy_index_am') subdir('dummy_seclabel') subdir('gin') +subdir('index') subdir('injection_points') subdir('ldap_password_func') subdir('libpq_pipeline') +subdir('nbtree') subdir('oauth_validator') subdir('plsample') subdir('spgist_name_ops') subdir('ssl_passphrase_callback') subdir('test_aio') +subdir('test_autovacuum') +subdir('test_binaryheap') +subdir('test_bitmapset') subdir('test_bloomfilter') +subdir('test_cloexec') +subdir('test_checksums') subdir('test_copy_callbacks') +subdir('test_cplusplusext') subdir('test_custom_rmgrs') +subdir('test_custom_stats') +subdir('test_custom_types') subdir('test_ddl_deparse') subdir('test_dsa') subdir('test_dsm_registry') subdir('test_escape') subdir('test_extensions') subdir('test_ginpostinglist') +subdir('test_int128') subdir('test_integerset') subdir('test_json_parser') subdir('test_lfind') +subdir('test_lwlock_tranches') subdir('test_misc') subdir('test_oat_hooks') subdir('test_parser') subdir('test_pg_dump') +subdir('test_plan_advice') subdir('test_predtest') subdir('test_radixtree') subdir('test_rbtree') subdir('test_regex') subdir('test_resowner') subdir('test_rls_hooks') +subdir('test_saslprep') +subdir('test_shmem') subdir('test_shm_mq') subdir('test_slru') subdir('test_tidstore') diff --git a/src/test/modules/nbtree/.gitignore b/src/test/modules/nbtree/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/nbtree/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/nbtree/Makefile b/src/test/modules/nbtree/Makefile new file mode 100644 index 0000000000000..eec264b16a4ce --- /dev/null +++ b/src/test/modules/nbtree/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/nbtree/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points contrib/amcheck + +REGRESS = nbtree_half_dead_pages \ + nbtree_incomplete_splits + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/nbtree +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global + +# XXX: This test is conditional on enable_injection_points in the +# parent Makefile, so we should never get here in the first place if +# injection points are not enabled. But the buildfarm 'misc-check' +# step doesn't pay attention to the if-condition in the parent +# Makefile. To work around that, disable running the test here too. +ifeq ($(enable_injection_points),yes) +include $(top_srcdir)/contrib/contrib-global.mk +else +check: + @echo "injection points are disabled in this build" +endif + +endif diff --git a/src/test/modules/nbtree/expected/nbtree_half_dead_pages.out b/src/test/modules/nbtree/expected/nbtree_half_dead_pages.out new file mode 100644 index 0000000000000..1fdd7d7e5284b --- /dev/null +++ b/src/test/modules/nbtree/expected/nbtree_half_dead_pages.out @@ -0,0 +1,110 @@ +-- +-- Test half-dead pages in B-tree indexes. +-- +-- Half-dead pages is an intermediate state while vacuum is deleting a +-- page. You can encounter them if you query concurrently with vacuum, +-- or if vacuum is interrupted while it's deleting a page. A B-tree +-- with half-dead pages is a valid state, but they rarely observed by +-- other backends in practice because, so it's good to have some +-- targeted tests to exercise them. +-- +-- This uses injection points to interrupt some page deletions +set client_min_messages TO 'warning'; +create extension if not exists injection_points; +create extension if not exists amcheck; +reset client_min_messages; +-- Wait until all recently-dead tuples on a table become fully dead +-- and removable by vacuum. (We don't run any concurrent transactions +-- in the test itself, but auto-analyze can kick in at any time and +-- hold a transaction open, holding back the vacuum horizon.) +CREATE PROCEDURE wait_prunable() LANGUAGE plpgsql AS $$ + DECLARE + barrier xid8; + cutoff xid8; + BEGIN + barrier := pg_current_xact_id(); + -- Pass a shared catalog rather than the table we'll + -- prune, to prevent the cutoff from moving + -- backwards. See comments at removable_cutoff() + LOOP + ROLLBACK; -- release MyProc->xmin, which could be the oldest + cutoff := removable_cutoff('pg_database'); + EXIT WHEN cutoff >= barrier; + RAISE LOG 'removable cutoff %; waiting for %', cutoff, barrier; + PERFORM pg_sleep(.1); + END LOOP; + END +$$; +-- Make all injection points local to this process, for concurrency. +SELECT injection_points_set_local(); + injection_points_set_local +---------------------------- + +(1 row) + +-- Use the index for all the queries +set enable_seqscan=off; +-- Print a NOTICE whenever a half-dead page is deleted +SELECT injection_points_attach('nbtree-finish-half-dead-page-vacuum', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +create table nbtree_half_dead_pages(id bigint) with (autovacuum_enabled = off); +insert into nbtree_half_dead_pages SELECT g from generate_series(1, 150000) g; +create index nbtree_half_dead_pages_id_idx on nbtree_half_dead_pages using btree (id); +delete from nbtree_half_dead_pages where id > 100000 and id < 120000; +-- Run VACUUM and interrupt it so that it leaves behind a half-dead page +call wait_prunable(); +SELECT injection_points_attach('nbtree-leave-page-half-dead', 'error'); + injection_points_attach +------------------------- + +(1 row) + +vacuum nbtree_half_dead_pages; +ERROR: error triggered for injection point nbtree-leave-page-half-dead +CONTEXT: while vacuuming index "nbtree_half_dead_pages_id_idx" of relation "public.nbtree_half_dead_pages" +SELECT injection_points_detach('nbtree-leave-page-half-dead'); + injection_points_detach +------------------------- + +(1 row) + +select * from nbtree_half_dead_pages where id > 99998 and id < 120002; + id +-------- + 99999 + 100000 + 120000 + 120001 +(4 rows) + +-- Also check the index with amcheck +select bt_index_parent_check('nbtree_half_dead_pages_id_idx'::regclass, true, true); + bt_index_parent_check +----------------------- + +(1 row) + +-- Finish the deletion and re-check +vacuum nbtree_half_dead_pages; +NOTICE: notice triggered for injection point nbtree-finish-half-dead-page-vacuum +select * from nbtree_half_dead_pages where id > 99998 and id < 120002; + id +-------- + 99999 + 100000 + 120000 + 120001 +(4 rows) + +select bt_index_parent_check('nbtree_half_dead_pages_id_idx'::regclass, true, true); + bt_index_parent_check +----------------------- + +(1 row) + +drop extension amcheck; +drop extension injection_points; diff --git a/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out b/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out new file mode 100644 index 0000000000000..00168c6ebb88a --- /dev/null +++ b/src/test/modules/nbtree/expected/nbtree_incomplete_splits.out @@ -0,0 +1,188 @@ +-- +-- Test incomplete splits in B-tree indexes. +-- +-- We use a test table with integers from 1 to :next_i. Each integer +-- occurs exactly once, no gaps or duplicates, although the index does +-- contain some duplicates because some of the inserting transactions +-- are rolled back during the test. The exact contents of the table +-- depend on the physical layout of the index, which in turn depends +-- at least on the block size, so instead of checking the exact +-- contents, we check those invariants. :next_i psql variable is +-- maintained at all times to hold the last inserted integer + 1. +-- +-- This uses injection points to cause errors that leave some page +-- splits in "incomplete" state +set client_min_messages TO 'warning'; +create extension if not exists injection_points; +create extension if not exists amcheck; +reset client_min_messages; +-- Make all injection points local to this process, for concurrency. +SELECT injection_points_set_local(); + injection_points_set_local +---------------------------- + +(1 row) + +-- Use the index for all the queries +set enable_seqscan=off; +-- Print a NOTICE whenever an incomplete split gets fixed +SELECT injection_points_attach('nbtree-finish-incomplete-split', 'notice'); + injection_points_attach +------------------------- + +(1 row) + +-- +-- First create the test table and some helper functions +-- +create table nbtree_incomplete_splits(i int4) with (autovacuum_enabled = off); +create index nbtree_incomplete_splits_i_idx on nbtree_incomplete_splits using btree (i); +-- Inserts 'n' rows to the test table. Pass :next_i as the first +-- argument, returns the new value for :next_i. +create function insert_n(first_i int, n int) returns int language plpgsql as $$ +begin + insert into nbtree_incomplete_splits select g from generate_series(first_i, first_i + n - 1) as g; + return first_i + n; +end; +$$; +-- Inserts to the table until an insert fails. Like insert_n(), returns the +-- new value for :next_i. +create function insert_until_fail(next_i int, step int default 1) returns int language plpgsql as $$ +declare + i integer; +begin + -- Insert rows in batches of 'step' rows each, until an error occurs. + i := 0; + loop + begin + select insert_n(next_i, step) into next_i; + exception when others then + raise notice 'failed with: %', sqlerrm; + exit; + end; + + -- The caller is expected to set an injection point that eventually + -- causes an error. But bail out if still no error after 10000 + -- attempts, so that we don't get stuck in an infinite loop. + i := i + 1; + if i = 10000 then + raise 'no error on inserts after % iterations', i; + end if; + end loop; + + return next_i; +end; +$$; +-- Check the invariants. +create function verify(next_i int) returns bool language plpgsql as $$ +declare + c integer; +begin + -- Perform a scan over the trailing part of the index, where the + -- possible incomplete splits are. (We don't check the whole table, + -- because that'd be pretty slow.) + -- + -- Find all rows that overlap with the last 200 inserted integers. Or + -- the next 100, which shouldn't exist. + select count(*) into c from nbtree_incomplete_splits where i between next_i - 200 and next_i + 100; + if c <> 200 then + raise 'unexpected count % ', c; + end if; + + -- Also check the index with amcheck. Both to test that the index is + -- valid, but also to test that amcheck doesn't wrongly complain + -- about incomplete splits. + perform bt_index_parent_check('nbtree_incomplete_splits_i_idx'::regclass, true, true); + + return true; +end; +$$; +-- Insert one array to get started. +select insert_n(1, 1000) as next_i +\gset +select verify(:next_i); + verify +-------- + t +(1 row) + +-- +-- Test incomplete leaf split +-- +SELECT injection_points_attach('nbtree-leave-leaf-split-incomplete', 'error'); + injection_points_attach +------------------------- + +(1 row) + +select insert_until_fail(:next_i) as next_i +\gset +NOTICE: failed with: error triggered for injection point nbtree-leave-leaf-split-incomplete +SELECT injection_points_detach('nbtree-leave-leaf-split-incomplete'); + injection_points_detach +------------------------- + +(1 row) + +-- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + verify +-------- + t +(1 row) + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +NOTICE: notice triggered for injection point nbtree-finish-incomplete-split +-- Verify that a scan still works +select verify(:next_i); + verify +-------- + t +(1 row) + +-- +-- Test incomplete internal page split +-- +SELECT injection_points_attach('nbtree-leave-internal-split-incomplete', 'error'); + injection_points_attach +------------------------- + +(1 row) + +select insert_until_fail(:next_i, 100) as next_i +\gset +NOTICE: failed with: error triggered for injection point nbtree-leave-internal-split-incomplete +SELECT injection_points_detach('nbtree-leave-internal-split-incomplete'); + injection_points_detach +------------------------- + +(1 row) + + -- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + verify +-------- + t +(1 row) + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +NOTICE: notice triggered for injection point nbtree-finish-incomplete-split +-- Verify that a scan still works +select verify(:next_i); + verify +-------- + t +(1 row) + +SELECT injection_points_detach('nbtree-finish-incomplete-split'); + injection_points_detach +------------------------- + +(1 row) + +drop extension amcheck; +drop extension injection_points; diff --git a/src/test/modules/nbtree/meson.build b/src/test/modules/nbtree/meson.build new file mode 100644 index 0000000000000..209c3323b71f0 --- /dev/null +++ b/src/test/modules/nbtree/meson.build @@ -0,0 +1,17 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +if not get_option('injection_points') + subdir_done() +endif + +tests += { + 'name': 'nbtree', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'nbtree_half_dead_pages', + 'nbtree_incomplete_splits', + ], + }, +} diff --git a/src/test/modules/nbtree/sql/nbtree_half_dead_pages.sql b/src/test/modules/nbtree/sql/nbtree_half_dead_pages.sql new file mode 100644 index 0000000000000..c9eb0e50d0efc --- /dev/null +++ b/src/test/modules/nbtree/sql/nbtree_half_dead_pages.sql @@ -0,0 +1,75 @@ +-- +-- Test half-dead pages in B-tree indexes. +-- +-- Half-dead pages is an intermediate state while vacuum is deleting a +-- page. You can encounter them if you query concurrently with vacuum, +-- or if vacuum is interrupted while it's deleting a page. A B-tree +-- with half-dead pages is a valid state, but they rarely observed by +-- other backends in practice because, so it's good to have some +-- targeted tests to exercise them. +-- + +-- This uses injection points to interrupt some page deletions +set client_min_messages TO 'warning'; +create extension if not exists injection_points; +create extension if not exists amcheck; +reset client_min_messages; + +-- Wait until all recently-dead tuples on a table become fully dead +-- and removable by vacuum. (We don't run any concurrent transactions +-- in the test itself, but auto-analyze can kick in at any time and +-- hold a transaction open, holding back the vacuum horizon.) +CREATE PROCEDURE wait_prunable() LANGUAGE plpgsql AS $$ + DECLARE + barrier xid8; + cutoff xid8; + BEGIN + barrier := pg_current_xact_id(); + -- Pass a shared catalog rather than the table we'll + -- prune, to prevent the cutoff from moving + -- backwards. See comments at removable_cutoff() + LOOP + ROLLBACK; -- release MyProc->xmin, which could be the oldest + cutoff := removable_cutoff('pg_database'); + EXIT WHEN cutoff >= barrier; + RAISE LOG 'removable cutoff %; waiting for %', cutoff, barrier; + PERFORM pg_sleep(.1); + END LOOP; + END +$$; + +-- Make all injection points local to this process, for concurrency. +SELECT injection_points_set_local(); + +-- Use the index for all the queries +set enable_seqscan=off; + +-- Print a NOTICE whenever a half-dead page is deleted +SELECT injection_points_attach('nbtree-finish-half-dead-page-vacuum', 'notice'); + +create table nbtree_half_dead_pages(id bigint) with (autovacuum_enabled = off); + +insert into nbtree_half_dead_pages SELECT g from generate_series(1, 150000) g; + +create index nbtree_half_dead_pages_id_idx on nbtree_half_dead_pages using btree (id); + +delete from nbtree_half_dead_pages where id > 100000 and id < 120000; + +-- Run VACUUM and interrupt it so that it leaves behind a half-dead page +call wait_prunable(); +SELECT injection_points_attach('nbtree-leave-page-half-dead', 'error'); +vacuum nbtree_half_dead_pages; +SELECT injection_points_detach('nbtree-leave-page-half-dead'); + +select * from nbtree_half_dead_pages where id > 99998 and id < 120002; + +-- Also check the index with amcheck +select bt_index_parent_check('nbtree_half_dead_pages_id_idx'::regclass, true, true); + +-- Finish the deletion and re-check +vacuum nbtree_half_dead_pages; +select * from nbtree_half_dead_pages where id > 99998 and id < 120002; +select bt_index_parent_check('nbtree_half_dead_pages_id_idx'::regclass, true, true); + +drop extension amcheck; +drop extension injection_points; diff --git a/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql b/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql new file mode 100644 index 0000000000000..23a112a074bee --- /dev/null +++ b/src/test/modules/nbtree/sql/nbtree_incomplete_splits.sql @@ -0,0 +1,144 @@ +-- +-- Test incomplete splits in B-tree indexes. +-- +-- We use a test table with integers from 1 to :next_i. Each integer +-- occurs exactly once, no gaps or duplicates, although the index does +-- contain some duplicates because some of the inserting transactions +-- are rolled back during the test. The exact contents of the table +-- depend on the physical layout of the index, which in turn depends +-- at least on the block size, so instead of checking the exact +-- contents, we check those invariants. :next_i psql variable is +-- maintained at all times to hold the last inserted integer + 1. +-- + +-- This uses injection points to cause errors that leave some page +-- splits in "incomplete" state +set client_min_messages TO 'warning'; +create extension if not exists injection_points; +create extension if not exists amcheck; +reset client_min_messages; + +-- Make all injection points local to this process, for concurrency. +SELECT injection_points_set_local(); + +-- Use the index for all the queries +set enable_seqscan=off; + +-- Print a NOTICE whenever an incomplete split gets fixed +SELECT injection_points_attach('nbtree-finish-incomplete-split', 'notice'); + +-- +-- First create the test table and some helper functions +-- +create table nbtree_incomplete_splits(i int4) with (autovacuum_enabled = off); + +create index nbtree_incomplete_splits_i_idx on nbtree_incomplete_splits using btree (i); + +-- Inserts 'n' rows to the test table. Pass :next_i as the first +-- argument, returns the new value for :next_i. +create function insert_n(first_i int, n int) returns int language plpgsql as $$ +begin + insert into nbtree_incomplete_splits select g from generate_series(first_i, first_i + n - 1) as g; + return first_i + n; +end; +$$; + +-- Inserts to the table until an insert fails. Like insert_n(), returns the +-- new value for :next_i. +create function insert_until_fail(next_i int, step int default 1) returns int language plpgsql as $$ +declare + i integer; +begin + -- Insert rows in batches of 'step' rows each, until an error occurs. + i := 0; + loop + begin + select insert_n(next_i, step) into next_i; + exception when others then + raise notice 'failed with: %', sqlerrm; + exit; + end; + + -- The caller is expected to set an injection point that eventually + -- causes an error. But bail out if still no error after 10000 + -- attempts, so that we don't get stuck in an infinite loop. + i := i + 1; + if i = 10000 then + raise 'no error on inserts after % iterations', i; + end if; + end loop; + + return next_i; +end; +$$; + +-- Check the invariants. +create function verify(next_i int) returns bool language plpgsql as $$ +declare + c integer; +begin + -- Perform a scan over the trailing part of the index, where the + -- possible incomplete splits are. (We don't check the whole table, + -- because that'd be pretty slow.) + -- + -- Find all rows that overlap with the last 200 inserted integers. Or + -- the next 100, which shouldn't exist. + select count(*) into c from nbtree_incomplete_splits where i between next_i - 200 and next_i + 100; + if c <> 200 then + raise 'unexpected count % ', c; + end if; + + -- Also check the index with amcheck. Both to test that the index is + -- valid, but also to test that amcheck doesn't wrongly complain + -- about incomplete splits. + perform bt_index_parent_check('nbtree_incomplete_splits_i_idx'::regclass, true, true); + + return true; +end; +$$; + +-- Insert one array to get started. +select insert_n(1, 1000) as next_i +\gset +select verify(:next_i); + + +-- +-- Test incomplete leaf split +-- +SELECT injection_points_attach('nbtree-leave-leaf-split-incomplete', 'error'); +select insert_until_fail(:next_i) as next_i +\gset +SELECT injection_points_detach('nbtree-leave-leaf-split-incomplete'); + +-- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +-- Verify that a scan still works +select verify(:next_i); + + +-- +-- Test incomplete internal page split +-- +SELECT injection_points_attach('nbtree-leave-internal-split-incomplete', 'error'); +select insert_until_fail(:next_i, 100) as next_i +\gset +SELECT injection_points_detach('nbtree-leave-internal-split-incomplete'); + + -- Verify that a scan works even though there's an incomplete split +select verify(:next_i); + +-- Insert some more rows, finishing the split +select insert_n(:next_i, 10) as next_i +\gset +-- Verify that a scan still works +select verify(:next_i); + +SELECT injection_points_detach('nbtree-finish-incomplete-split'); + +drop extension amcheck; +drop extension injection_points; diff --git a/src/test/modules/oauth_validator/Makefile b/src/test/modules/oauth_validator/Makefile index 05b9f06ed7387..0b39a88fd9fec 100644 --- a/src/test/modules/oauth_validator/Makefile +++ b/src/test/modules/oauth_validator/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/modules/oauth_validator # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/modules/oauth_validator/Makefile @@ -36,5 +36,6 @@ include $(top_srcdir)/contrib/contrib-global.mk export PYTHON export with_libcurl export with_python +export cert_dir=$(top_srcdir)/src/test/ssl/ssl endif diff --git a/src/test/modules/oauth_validator/fail_validator.c b/src/test/modules/oauth_validator/fail_validator.c index bf04182a48612..3de0470a5418c 100644 --- a/src/test/modules/oauth_validator/fail_validator.c +++ b/src/test/modules/oauth_validator/fail_validator.c @@ -4,7 +4,7 @@ * Test module for serverside OAuth token validation callbacks, which is * guaranteed to always fail in the validation callback * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/modules/oauth_validator/fail_validator.c diff --git a/src/test/modules/oauth_validator/magic_validator.c b/src/test/modules/oauth_validator/magic_validator.c index e0547caf22f3c..550da41d11bc3 100644 --- a/src/test/modules/oauth_validator/magic_validator.c +++ b/src/test/modules/oauth_validator/magic_validator.c @@ -5,7 +5,7 @@ * should fail due to using the wrong PG_OAUTH_VALIDATOR_MAGIC marker * and thus the wrong ABI version * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/modules/oauth_validator/magic_validator.c diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build index e190f9cf15a4a..506a9894b8d3c 100644 --- a/src/test/modules/oauth_validator/meson.build +++ b/src/test/modules/oauth_validator/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group validator_sources = files( 'validator.c', @@ -77,9 +77,11 @@ tests += { 't/002_client.pl', ], 'env': { - 'PYTHON': python.path(), + 'PYTHON': python.full_path(), 'with_libcurl': oauth_flow_supported ? 'yes' : 'no', 'with_python': 'yes', + 'cert_dir': meson.project_source_root() / 'src/test/ssl/ssl', }, + 'deps': [oauth_hook_client], }, } diff --git a/src/test/modules/oauth_validator/oauth_hook_client.c b/src/test/modules/oauth_validator/oauth_hook_client.c index 15d0cf938a824..4695d73e8f7cc 100644 --- a/src/test/modules/oauth_validator/oauth_hook_client.c +++ b/src/test/modules/oauth_validator/oauth_hook_client.c @@ -4,7 +4,7 @@ * Test driver for t/002_client.pl, which verifies OAuth hook * functionality in libpq. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -36,13 +36,16 @@ usage(char *argv[]) printf("recognized flags:\n"); printf(" -h, --help show this message\n"); + printf(" -v VERSION select the hook API version (default 2)\n"); printf(" --expected-scope SCOPE fail if received scopes do not match SCOPE\n"); printf(" --expected-uri URI fail if received configuration link does not match URI\n"); + printf(" --expected-issuer ISS fail if received issuer does not match ISS (v2 only)\n"); printf(" --misbehave=MODE have the hook fail required postconditions\n" " (MODEs: no-hook, fail-async, no-token, no-socket)\n"); printf(" --no-hook don't install OAuth hooks\n"); printf(" --hang-forever don't ever return a token (combine with connect_timeout)\n"); printf(" --token TOKEN use the provided TOKEN value\n"); + printf(" --error ERRMSG fail instead, with the given ERRMSG (v2 only)\n"); printf(" --stress-async busy-loop on PQconnectPoll rather than polling\n"); } @@ -51,9 +54,12 @@ static bool no_hook = false; static bool hang_forever = false; static bool stress_async = false; static const char *expected_uri = NULL; +static const char *expected_issuer = NULL; static const char *expected_scope = NULL; static const char *misbehave_mode = NULL; static char *token = NULL; +static char *errmsg = NULL; +static int hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2; int main(int argc, char *argv[]) @@ -68,6 +74,8 @@ main(int argc, char *argv[]) {"hang-forever", no_argument, NULL, 1004}, {"misbehave", required_argument, NULL, 1005}, {"stress-async", no_argument, NULL, 1006}, + {"expected-issuer", required_argument, NULL, 1007}, + {"error", required_argument, NULL, 1008}, {0} }; @@ -75,7 +83,7 @@ main(int argc, char *argv[]) PGconn *conn; int c; - while ((c = getopt_long(argc, argv, "h", long_options, NULL)) != -1) + while ((c = getopt_long(argc, argv, "hv:", long_options, NULL)) != -1) { switch (c) { @@ -83,6 +91,18 @@ main(int argc, char *argv[]) usage(argv); return 0; + case 'v': + if (strcmp(optarg, "1") == 0) + hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN; + else if (strcmp(optarg, "2") == 0) + hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2; + else + { + usage(argv); + return 1; + } + break; + case 1000: /* --expected-scope */ expected_scope = optarg; break; @@ -111,6 +131,14 @@ main(int argc, char *argv[]) stress_async = true; break; + case 1007: /* --expected-issuer */ + expected_issuer = optarg; + break; + + case 1008: /* --error */ + errmsg = optarg; + break; + default: usage(argv); return 1; @@ -167,16 +195,24 @@ main(int argc, char *argv[]) /* * PQauthDataHook implementation. Replaces the default client flow by handling - * PQAUTHDATA_OAUTH_BEARER_TOKEN. + * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2]. */ static int handle_auth_data(PGauthData type, PGconn *conn, void *data) { - PGoauthBearerRequest *req = data; + PGoauthBearerRequest *req; + PGoauthBearerRequestV2 *req2 = NULL; + + Assert(hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN || + hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2); - if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN)) + if (no_hook || type != hook_version) return 0; + req = data; + if (type == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2) + req2 = data; + if (hang_forever) { /* Start asynchronous processing. */ @@ -221,6 +257,44 @@ handle_auth_data(PGauthData type, PGconn *conn, void *data) } } + if (expected_issuer) + { + if (!req2) + { + fprintf(stderr, "--expected-issuer cannot be combined with -v1\n"); + return -1; + } + + if (!req2->issuer) + { + fprintf(stderr, "expected issuer \"%s\", got NULL\n", expected_issuer); + return -1; + } + + if (strcmp(expected_issuer, req2->issuer) != 0) + { + fprintf(stderr, "expected issuer \"%s\", got \"%s\"\n", expected_issuer, req2->issuer); + return -1; + } + } + + if (errmsg) + { + if (token) + { + fprintf(stderr, "--error cannot be combined with --token\n"); + return -1; + } + else if (!req2) + { + fprintf(stderr, "--error cannot be combined with -v1\n"); + return -1; + } + + req2->error = errmsg; + return -1; + } + req->token = token; return 1; } @@ -273,6 +347,20 @@ misbehave_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock) if (strcmp(misbehave_mode, "fail-async") == 0) { /* Just fail "normally". */ + if (errmsg) + { + PGoauthBearerRequestV2 *req2; + + if (hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN) + { + fprintf(stderr, "--error cannot be combined with -v1\n"); + exit(1); + } + + req2 = (PGoauthBearerRequestV2 *) req; + req2->error = errmsg; + } + return PGRES_POLLING_FAILED; } else if (strcmp(misbehave_mode, "no-token") == 0) diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl index 41672ebd5c6dc..eb2566c3775e4 100644 --- a/src/test/modules/oauth_validator/t/001_server.pl +++ b/src/test/modules/oauth_validator/t/001_server.pl @@ -3,7 +3,7 @@ # Tests the libpq builtin OAuth flow, as well as server-side HBA and validator # setup. # -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # use strict; @@ -71,9 +71,46 @@ END $? = $exit_code; } +# To test against HTTPS with our custom CA, we'll set PGOAUTHCAFILE. But first, +# check to make sure the client refuses HTTP and untrusted HTTPS connections by +# default. my $port = $webserver->port(); my $issuer = "http://127.0.0.1:$port"; +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all test oauth issuer="$issuer" scope="openid postgres" +}); +$node->reload; + +my $log_start = $node->wait_for_log(qr/reloading configuration files/); + +$node->connect_fails( + "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", + "HTTPS is required without debug mode", + expected_stderr => + qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@ +); + +{ + # PGOAUTHDEBUG=http should have no effect (it needs an UNSAFE: marker). + local $ENV{PGOAUTHDEBUG} = "http"; + + $node->connect_fails( + "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", + "HTTPS is required without debug mode (bad PGOAUTHDEBUG value)", + expected_stderr => qr[ + ^WARNING: .* \Qoption "http" is unsafe\E + .* + \QOAuth discovery URI "$issuer/.well-known/openid-configuration" must use HTTPS\E + ]msx + ); +} + +# Switch to HTTPS. +$issuer = "https://127.0.0.1:$port"; + unlink($node->data_dir . '/pg_hba.conf'); $node->append_conf( 'pg_hba.conf', qq{ @@ -83,7 +120,8 @@ END }); $node->reload; -my $log_start = $node->wait_for_log(qr/reloading configuration files/); +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); # Check pg_hba_file_rules() support. my $contents = $bgconn->query_safe( @@ -96,18 +134,46 @@ END 3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}}, "pg_hba_file_rules recreates OAuth HBA settings"); -# To test against HTTP rather than HTTPS, we need to enable PGOAUTHDEBUG. But -# first, check to make sure the client refuses such connections by default. -$node->connect_fails( - "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", - "HTTPS is required without debug mode", +{ + # Make sure PGOAUTHDEBUG=UNSAFE doesn't disable certificate verification. + local $ENV{PGOAUTHDEBUG} = "UNSAFE"; + + $node->connect_fails( + "user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", + "HTTPS trusts only system CA roots by default", + # Note that the latter half of this error message comes from Curl, which + # has had a few variants since 7.61: + # + # - SSL peer certificate or SSH remote key was not OK + # - Peer certificate cannot be authenticated with given CA certificates + # - Issuer check against peer certificate failed + # + # Key off of the "peer certificate" portion, since that seems to have + # remained constant over a long period of time. + expected_stderr => + qr/failed to fetch OpenID discovery document:.*peer certificate/i); +} + +my $alternative_ca = "$ENV{cert_dir}/root+server_ca.crt"; +my $user = "test"; + +# Make sure we can use oauth_ca_file option to specify the alternative CA path +$node->connect_ok( + "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635 oauth_ca_file=$alternative_ca", + "connect as test (oauth_ca_file)", expected_stderr => - qr@OAuth discovery URI "\Q$issuer\E/.well-known/openid-configuration" must use HTTPS@ -); + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/oauth_validator: token="9243959234", role="$user"/, + qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/, + qr/connection authenticated: identity="test" method=oauth/, + qr/connection authorized/, + ]); -$ENV{PGOAUTHDEBUG} = "UNSAFE"; +# Make sure that we can use the environment variable without PGOAUTHDEBUG, and +# then use it for the rest of the tests +$ENV{PGOAUTHCAFILE} = $alternative_ca; -my $user = "test"; $node->connect_ok( "user=$user dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635", "connect as test", @@ -118,7 +184,14 @@ END qr/oauth_validator: issuer="\Q$issuer\E", scope="openid postgres"/, qr/connection authenticated: identity="test" method=oauth/, qr/connection authorized/, - ]); + ], + log_unlike => [qr/FATAL.*OAuth bearer authentication failed/]); + +# Enable some debugging features for all remaining tests: +# - trace, for detailed Curl logs on failure +# - dos-endpoint, to speed up the three-way handshake +# - call-count, for our later sanity check +$ENV{PGOAUTHDEBUG} = "UNSAFE:trace,dos-endpoint,call-count"; # The /alternate issuer uses slightly different parameters, along with an # OAuth-style discovery document. @@ -133,7 +206,8 @@ END qr|oauth_validator: issuer="\Q$issuer/.well-known/oauth-authorization-server/alternate\E", scope="openid postgres alt"|, qr/connection authenticated: identity="testalt" method=oauth/, qr/connection authorized/, - ]); + ], + log_unlike => [qr/FATAL.*OAuth bearer authentication failed/]); # The issuer linked by the server must match the client's oauth_issuer setting. $node->connect_fails( @@ -418,6 +492,35 @@ sub connstr qr/failed to obtain access token: mutual TLS required for client \(invalid_client\)/ ); +# Count the number of calls to the internal flow when multiple retries are +# triggered. The exact number depends on many things -- the TCP stack, the +# version of Curl in use, random chance -- but a ridiculously high number +# suggests something is wrong with our ability to clear multiplexer events after +# they're no longer applicable. +my ($ret, $stdout, $stderr) = $node->psql( + 'postgres', + "SELECT 'connected for call count'", + extra_params => ['-w'], + connstr => connstr(stage => 'token', retries => 2), + on_error_stop => 0); + +is($ret, 0, "call count connection succeeds"); +like( + $stderr, + qr@Visit https://example\.com/ and enter the code: postgresuser@, + "call count: stderr matches"); + +my $count_pattern = qr/\[libpq\] total number of polls: (\d+)/; +if (like($stderr, $count_pattern, "call count: count is printed")) +{ + # For reference, a typical flow with two retries might take between 5-15 + # calls to the client implementation. And while this will probably continue + # to change across OSes and Curl updates, we're likely in trouble if we see + # hundreds or thousands of calls. + $stderr =~ $count_pattern; + cmp_ok($1, '<', 100, "call count is reasonably small"); +} + # Stress test: make sure our builtin flow operates correctly even if the client # application isn't respecting PGRES_POLLING_READING/WRITING signals returned # from PQconnectPoll(). @@ -428,7 +531,7 @@ sub connstr connstr(stage => 'all', retries => 1, interval => 1)); note "running '" . join("' '", @cmd) . "'"; -my ($stdout, $stderr) = run_command(\@cmd); +($stdout, $stderr) = run_command(\@cmd); like($stdout, qr/connection succeeded/, "stress-async: stdout matches"); unlike( @@ -459,8 +562,8 @@ sub connstr expected_stderr => qr/OAuth bearer authentication failed/, log_like => [ qr/connection authenticated: identity=""/, - qr/DETAIL:\s+Validator provided no identity/, qr/FATAL:\s+OAuth bearer authentication failed/, + qr/DETAIL:\s+Validator provided no identity/, ]); # Even if a validator authenticates the user, if the token isn't considered @@ -479,10 +582,67 @@ sub connstr expected_stderr => qr/OAuth bearer authentication failed/, log_like => [ qr/connection authenticated: identity="test\@example\.org"/, + qr/FATAL:\s+OAuth bearer authentication failed/, qr/DETAIL:\s+Validator failed to authorize the provided token/, + ]); + +# Validators can provide their own explanations. +$bgconn->query_safe( + "ALTER SYSTEM SET oauth_validator.error_detail TO 'something failed'"); +$node->reload; +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +$node->connect_fails( + "$common_connstr user=test", + "validator must authorize token explicitly (custom logdetail)", + expected_stderr => qr/OAuth bearer authentication failed/, + log_like => [ + qr/connection authenticated: identity="test\@example\.org"/, qr/FATAL:\s+OAuth bearer authentication failed/, + qr/DETAIL:\s+something failed/, + ]); + +$bgconn->query_safe( + "ALTER SYSTEM SET oauth_validator.internal_error TO true"); +$node->reload; +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +$node->connect_fails( + "$common_connstr user=test", + "validator internal error (custom logdetail)", + expected_stderr => qr/OAuth bearer authentication failed/, + log_like => [ + qr/WARNING:\s+internal error in OAuth validator module/, + qr/DETAIL:\s+something failed/, ]); +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.error_detail"); +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.internal_error"); + +# We complain when bad option names are registered, but connections may proceed +# (since users can't set those options in the HBA anyway). +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id"); +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authorize_tokens"); +$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.invalid_hba TO true"); + +$node->reload; +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +$node->connect_ok( + "$common_connstr user=test", + "bad registered HBA option", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/WARNING:\s+HBA option name "bad option name" is invalid and will be ignored/, + qr/CONTEXT:\s+validator module "validator", in call to RegisterOAuthHBAOptions/, + ]); + +$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.invalid_hba"); + # # Test user mapping. # @@ -551,6 +711,84 @@ sub connstr $log_start = $node->wait_for_log(qr/reloading configuration files/, $log_start); +$bgconn->quit; # the tests below restart the server + +# +# Test validator-specific HBA options. +# + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all test oauth issuer="$issuer" scope="openid postgres" delegate_ident_mapping=1 \\ + validator.authn_id="ignored" validator.authn_id="other-identity" +local all testalt oauth issuer="$issuer" scope="openid postgres" validator.log="testalt message" +}); + +$node->reload; +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); + +$node->connect_ok( + "$common_connstr user=test", + "custom HBA setting (test)", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [qr/connection authenticated: identity="other-identity"/]); +$node->connect_ok( + "$common_connstr user=testalt", + "custom HBA setting (testalt)", + expected_stderr => + qr@Visit https://example\.com/ and enter the code: postgresuser@, + log_like => [ + qr/LOG:\s+testalt message/, + qr/connection authenticated: identity="testalt"/, + ]); + +# bad syntax +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all testalt oauth issuer="$issuer" scope="openid postgres" validator.=1 +}); + +$log_start = -s $node->logfile; +$node->restart(fail_ok => 1); +$node->log_check("empty HBA option name", + $log_start, + log_like => [qr/invalid OAuth validator option name: "validator\."/]); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all testalt oauth issuer="$issuer" scope="openid postgres" validator.@@=1 +}); + +$log_start = -s $node->logfile; +$node->restart(fail_ok => 1); +$node->log_check("invalid HBA option name", + $log_start, + log_like => [qr/invalid OAuth validator option name: "validator\.@@"/]); + +# unknown settings (validation is deferred to connect time) +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', qq{ +local all testalt oauth issuer="$issuer" scope="openid postgres" \\ + validator.log=ignored validator.bad=1 +}); +$node->restart; + +$node->connect_fails( + "$common_connstr user=testalt", + "bad HBA setting", + expected_stderr => qr/OAuth bearer authentication failed/, + log_like => [ + qr/WARNING:\s+unrecognized authentication option name: "validator\.bad"/, + qr/FATAL:\s+OAuth bearer authentication failed/, + qr/DETAIL:\s+unrecognized authentication option name: "validator\.bad"/, + ]); + # # Test multiple validators. # diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl index aac0220d2152d..dac684d7852ab 100644 --- a/src/test/modules/oauth_validator/t/002_client.pl +++ b/src/test/modules/oauth_validator/t/002_client.pl @@ -2,7 +2,7 @@ # Exercises the API for custom OAuth client flows, using the oauth_hook_client # test driver. # -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # use strict; @@ -29,6 +29,8 @@ $node->append_conf('postgresql.conf', "log_connections = all\n"); $node->append_conf('postgresql.conf', "oauth_validator_libraries = 'validator'\n"); +# Needed to inspect postmaster log after connection failure: +$node->append_conf('postgresql.conf', "log_min_messages = debug2"); $node->start; $node->safe_psql('postgres', 'CREATE USER test;'); @@ -47,7 +49,7 @@ }); $node->reload; -my $log_start = $node->wait_for_log(qr/reloading configuration files/); +$node->wait_for_log(qr/reloading configuration files/); $ENV{PGOAUTHDEBUG} = "UNSAFE"; @@ -73,11 +75,12 @@ sub test my @cmd = ("oauth_hook_client", @{$flags}, $common_connstr); note "running '" . join("' '", @cmd) . "'"; + my $log_start = -s $node->logfile; my ($stdout, $stderr) = run_command(\@cmd); - if (defined($params{expected_stdout})) + if ($params{expect_success}) { - like($stdout, $params{expected_stdout}, "$test_name: stdout matches"); + like($stdout, qr/connection succeeded/, "$test_name: stdout matches"); } if (defined($params{expected_stderr})) @@ -88,6 +91,18 @@ sub test { is($stderr, "", "$test_name: no stderr"); } + + if (defined($params{log_like})) + { + # See Cluster::connect_fails(). To avoid races, we have to wait for the + # postmaster to flush the log for the finished connection. + $node->wait_for_log( + qr/DEBUG: (?:00000: )?forked new client backend, pid=(\d+) socket.*DEBUG: (?:00000: )?client backend \(PID \1\) exited with exit code \d/s, + $log_start); + + $node->log_check("$test_name: log matches", + $log_start, log_like => $params{log_like}); + } } test( @@ -95,13 +110,40 @@ sub test flags => [ "--token", "my-token", "--expected-uri", "$issuer/.well-known/openid-configuration", + "--expected-issuer", "$issuer", "--expected-scope", $scope, ], - expected_stdout => qr/connection succeeded/); + expect_success => 1, + log_like => [qr/oauth_validator: token="my-token", role="$user"/]); -$node->log_check("validator receives correct token", - $log_start, - log_like => [ qr/oauth_validator: token="my-token", role="$user"/, ]); +# The issuer ID provided to the hook is based on, but not equal to, +# oauth_issuer. Make sure the correct string is passed. +$common_connstr = + "$base_connstr oauth_issuer=$issuer/.well-known/openid-configuration oauth_client_id=myID oauth_scope='$scope'"; +test( + "derived issuer ID is correctly provided", + flags => [ + "--token", "my-token", + "--expected-uri", "$issuer/.well-known/openid-configuration", + "--expected-issuer", "$issuer", + "--expected-scope", $scope, + ], + expect_success => 1, + log_like => [qr/oauth_validator: token="my-token", role="$user"/]); + +$common_connstr = "$base_connstr oauth_issuer=$issuer oauth_client_id=myID"; + +# Make sure the v1 hook continues to work. +test( + "v1 synchronous hook can provide a token", + flags => [ + "-v1", + "--token" => "my-token-v1", + "--expected-uri" => "$issuer/.well-known/openid-configuration", + "--expected-scope" => $scope, + ], + expect_success => 1, + log_like => [qr/oauth_validator: token="my-token-v1", role="$user"/]); if ($ENV{with_libcurl} ne 'yes') { @@ -114,6 +156,15 @@ sub test ); } +# v2 synchronous flows should be able to set custom error messages. +test( + "basic synchronous hook can set error messages", + flags => [ + "--error" => "a custom error message", + ], + expected_stderr => + qr/user-defined OAuth flow failed: a custom error message/); + # connect_timeout should work if the flow doesn't respond. $common_connstr = "$common_connstr connect_timeout=1"; test( @@ -151,6 +202,21 @@ sub test "hook misbehavior: $c->{'flag'}", flags => [ $c->{'flag'} ], expected_stderr => $c->{'expected_error'}); + + test( + "hook misbehavior: $c->{'flag'} (v1)", + flags => [ '-v1', $c->{'flag'} ], + expected_stderr => $c->{'expected_error'}); } +# v2 async flows should be able to set error messages, too. +test( + "asynchronous hook can set error messages", + flags => [ + "--misbehave" => "fail-async", + "--error" => "async error message", + ], + expected_stderr => + qr/user-defined OAuth flow failed: async error message/); + done_testing(); diff --git a/src/test/modules/oauth_validator/t/OAuth/Server.pm b/src/test/modules/oauth_validator/t/OAuth/Server.pm index 52ae7afa991c3..62a29c283dfda 100644 --- a/src/test/modules/oauth_validator/t/OAuth/Server.pm +++ b/src/test/modules/oauth_validator/t/OAuth/Server.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group =pod @@ -15,7 +15,7 @@ OAuth::Server - runs a mock OAuth authorization server for testing $server->run; my $port = $server->port; - my $issuer = "http://127.0.0.1:$port"; + my $issuer = "https://127.0.0.1:$port"; # test against $issuer... @@ -27,9 +27,8 @@ This is glue API between the Perl tests and the Python authorization server daemon implemented in t/oauth_server.py. (Python has a fairly usable HTTP server in its standard library, so the implementation was ported from Perl.) -This authorization server does not use TLS (it implements a nonstandard, unsafe -issuer at "http://127.0.0.1:"), so libpq in particular will need to set -PGOAUTHDEBUG=UNSAFE to be able to talk to it. +This authorization server serves HTTPS on 127.0.0.1 (IPv4 only). libpq will need +to set PGOAUTHCAFILE with the right CA. =cut diff --git a/src/test/modules/oauth_validator/t/oauth_server.py b/src/test/modules/oauth_validator/t/oauth_server.py index 0f8836aadf372..6df8c2ca5df3b 100755 --- a/src/test/modules/oauth_validator/t/oauth_server.py +++ b/src/test/modules/oauth_validator/t/oauth_server.py @@ -11,12 +11,17 @@ import http.server import json import os +import ssl import sys import time import urllib.parse from collections import defaultdict from typing import Dict +ssl_dir = os.getenv("cert_dir") +ssl_cert = ssl_dir + "/server-localhost-alt-names.crt" +ssl_key = ssl_dir + "/server-localhost-alt-names.key" + class OAuthHandler(http.server.BaseHTTPRequestHandler): """ @@ -257,13 +262,33 @@ def _access_token(self): return token + def _log_response(self, js: JsonObject) -> None: + """ + Trims the response JSON, if necessary, and logs it for later debugging. + """ + # At the moment the biggest problem for tests is the _pad_ member, which + # is a megabyte in size, so truncate that to something more reasonable. + if "_pad_" in js: + pad = js["_pad_"] + + # Don't modify the original dict. + js = dict(js) + js["_pad_"] = pad[:64] + f"[...truncated from {len(pad)} bytes]" + + resp = json.dumps(js).encode("ascii") + self.log_message("sending JSON response: %s", resp) + + # If you've tripped this assertion, please truncate the new addition as + # above, or else come up with a new strategy. + assert len(resp) < 1024, "_log_response must be adjusted for new JSON" + def _send_json(self, js: JsonObject) -> None: """ Sends the provided JSON dict as an application/json response. self._response_code can be modified to send JSON error responses. """ resp = json.dumps(js).encode("ascii") - self.log_message("sending JSON response: %s", resp) + self._log_response(js) self.send_response(self._response_code) self.send_header("Content-Type", self._content_type) @@ -275,7 +300,11 @@ def _send_json(self, js: JsonObject) -> None: def config(self) -> JsonObject: port = self.server.socket.getsockname()[1] - issuer = f"http://127.0.0.1:{port}" + # XXX This IPv4-only Issuer can't be changed to "localhost" unless our + # server also listens on the corresponding IPv6 port when available. + # Otherwise, other processes with ephemeral sockets could accidentally + # interfere with our Curl client, causing intermittent failures. + issuer = f"https://127.0.0.1:{port}" if self._alt_issuer: issuer += "/alternate" elif self._parameterized: @@ -388,9 +417,18 @@ def main(): Starts the authorization server on localhost. The ephemeral port in use will be printed to stdout. """ - + # XXX Listen exclusively on IPv4. Listening on a dual-stack socket would be + # more true-to-life, but every OS/Python combination in the buildfarm and CI + # would need to provide the functionality first. s = http.server.HTTPServer(("127.0.0.1", 0), OAuthHandler) + # Speak HTTPS. + # TODO: switch to HTTPSServer with Python 3.14 + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(ssl_cert, ssl_key) + + s.socket = ssl_context.wrap_socket(s.socket, server_side=True) + # Attach a "cache" dictionary to the server to allow the OAuthHandlers to # track state across token requests. The use of defaultdict ensures that new # entries will be created automatically. diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c index 42b69646fbb9c..85fb4c08bf201 100644 --- a/src/test/modules/oauth_validator/validator.c +++ b/src/test/modules/oauth_validator/validator.c @@ -3,7 +3,7 @@ * validator.c * Test module for serverside OAuth token validation callbacks * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/modules/oauth_validator/validator.c @@ -40,17 +40,34 @@ static const OAuthValidatorCallbacks validator_callbacks = { /* GUCs */ static char *authn_id = NULL; static bool authorize_tokens = true; +static char *error_detail = NULL; +static bool internal_error = false; +static bool invalid_hba = false; + +/* HBA options */ +static const char *hba_opts[] = { + "authn_id", /* overrides the default authn_id */ + "log", /* logs an arbitrary string */ +}; /*--- * Extension entry point. Sets up GUCs for use by tests: * * - oauth_validator.authn_id Sets the user identifier to return during token * validation. Defaults to the username in the - * startup packet. + * startup packet, or the validator.authn_id HBA + * option if it is set. * * - oauth_validator.authorize_tokens * Sets whether to successfully validate incoming * tokens. Defaults to true. + * + * - oauth_validator.error_detail + * Sets an error message to be included as a + * DETAIL on failure. + * + * - oauth_validator.internal_error + * Reports an internal error to the server. */ void _PG_init(void) @@ -71,6 +88,30 @@ _PG_init(void) PGC_SIGHUP, 0, NULL, NULL, NULL); + DefineCustomStringVariable("oauth_validator.error_detail", + "Error message to print during failures", + NULL, + &error_detail, + NULL, + PGC_SIGHUP, + 0, + NULL, NULL, NULL); + DefineCustomBoolVariable("oauth_validator.internal_error", + "Should the validator report an internal error?", + NULL, + &internal_error, + false, + PGC_SIGHUP, + 0, + NULL, NULL, NULL); + DefineCustomBoolVariable("oauth_validator.invalid_hba", + "Should the validator register an invalid option?", + NULL, + &invalid_hba, + false, + PGC_SIGHUP, + 0, + NULL, NULL, NULL); MarkGUCPrefixReserved("oauth_validator"); } @@ -99,6 +140,29 @@ validator_startup(ValidatorModuleState *state) if (state->sversion != PG_VERSION_NUM) elog(ERROR, "oauth_validator: sversion set to %d", state->sversion); + /* + * Test the behavior of custom HBA options. Registered options should not + * be retrievable during startup (we want to discourage modules from + * relying on the relative order of client connections and the + * startup_cb). + */ + RegisterOAuthHBAOptions(state, lengthof(hba_opts), hba_opts); + for (int i = 0; i < lengthof(hba_opts); i++) + { + if (GetOAuthHBAOption(state, hba_opts[i])) + elog(ERROR, + "oauth_validator: GetOAuthValidatorOption(\"%s\") was non-NULL during startup_cb", + hba_opts[i]); + } + + if (invalid_hba) + { + /* Register a bad option, which should print a WARNING to the logs. */ + const char *invalid = "bad option name"; + + RegisterOAuthHBAOptions(state, 1, &invalid); + } + state->private_data = PRIVATE_COOKIE; } @@ -116,7 +180,7 @@ validator_shutdown(ValidatorModuleState *state) /* * Validator implementation. Logs the incoming data and authorizes the token by - * default; the behavior can be modified via the module's GUC settings. + * default; the behavior can be modified via the module's GUC and HBA settings. */ static bool validate_token(const ValidatorModuleState *state, @@ -128,14 +192,23 @@ validate_token(const ValidatorModuleState *state, elog(ERROR, "oauth_validator: private state cookie changed to %p in validate", state->private_data); + if (GetOAuthHBAOption(state, "log")) + elog(LOG, "%s", GetOAuthHBAOption(state, "log")); + elog(LOG, "oauth_validator: token=\"%s\", role=\"%s\"", token, role); elog(LOG, "oauth_validator: issuer=\"%s\", scope=\"%s\"", MyProcPort->hba->oauth_issuer, MyProcPort->hba->oauth_scope); + res->error_detail = error_detail; /* only relevant for failures */ + if (internal_error) + return false; + res->authorized = authorize_tokens; if (authn_id) res->authn_id = pstrdup(authn_id); + else if (GetOAuthHBAOption(state, "authn_id")) + res->authn_id = pstrdup(GetOAuthHBAOption(state, "authn_id")); else res->authn_id = pstrdup(role); diff --git a/src/test/modules/plsample/meson.build b/src/test/modules/plsample/meson.build index bb3e1bcb96a4b..b5da63bf37150 100644 --- a/src/test/modules/plsample/meson.build +++ b/src/test/modules/plsample/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group plsample_sources = files( 'plsample.c', diff --git a/src/test/modules/plsample/plsample.c b/src/test/modules/plsample/plsample.c index 78802a94f6284..f294f5ca4ad0f 100644 --- a/src/test/modules/plsample/plsample.c +++ b/src/test/modules/plsample/plsample.c @@ -3,7 +3,7 @@ * plsample.c * Handler for the PL/Sample procedural language * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * @@ -21,6 +21,7 @@ #include "commands/trigger.h" #include "executor/spi.h" #include "funcapi.h" +#include "utils/builtins.h" #include "utils/fmgrprotos.h" #include "utils/lsyscache.h" #include "utils/syscache.h" @@ -128,7 +129,7 @@ plsample_func_handler(PG_FUNCTION_ARGS) if (isnull) elog(ERROR, "could not find source text of function \"%s\"", proname); - source = DatumGetCString(DirectFunctionCall1(textout, ret)); + source = TextDatumGetCString(ret); ereport(NOTICE, (errmsg("source text of function \"%s\": %s", proname, source))); @@ -244,7 +245,7 @@ plsample_trigger_handler(PG_FUNCTION_ARGS) if (isnull) elog(ERROR, "could not find source text of function \"%s\"", proname); - source = DatumGetCString(DirectFunctionCall1(textout, ret)); + source = TextDatumGetCString(ret); ereport(NOTICE, (errmsg("source text of function \"%s\": %s", proname, source))); diff --git a/src/test/modules/spgist_name_ops/meson.build b/src/test/modules/spgist_name_ops/meson.build index a9596a8a12df6..1a3127ac5a6c9 100644 --- a/src/test/modules/spgist_name_ops/meson.build +++ b/src/test/modules/spgist_name_ops/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group spgist_name_ops_sources = files( 'spgist_name_ops.c', diff --git a/src/test/modules/spgist_name_ops/spgist_name_ops.c b/src/test/modules/spgist_name_ops/spgist_name_ops.c index bcc16ce366ec5..0c269740839d4 100644 --- a/src/test/modules/spgist_name_ops/spgist_name_ops.c +++ b/src/test/modules/spgist_name_ops/spgist_name_ops.c @@ -11,7 +11,7 @@ * Unlike spgtextproc.c, we don't bother with collation-aware logic. * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -33,7 +33,9 @@ PG_FUNCTION_INFO_V1(spgist_name_config); Datum spgist_name_config(PG_FUNCTION_ARGS) { - /* spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); */ +#ifdef NOT_USED + spgConfigIn *cfgin = (spgConfigIn *) PG_GETARG_POINTER(0); +#endif spgConfigOut *cfg = (spgConfigOut *) PG_GETARG_POINTER(1); cfg->prefixType = TEXTOID; @@ -94,7 +96,7 @@ commonPrefix(const char *a, const char *b, int lena, int lenb) * On success, *i gets the match location; on failure, it gets where to insert */ static bool -searchChar(Datum *nodeLabels, int nNodes, int16 c, int *i) +searchChar(const Datum *nodeLabels, int nNodes, int16 c, int *i) { int StopLow = 0, StopHigh = nNodes; @@ -171,7 +173,7 @@ spgist_name_choose(PG_FUNCTION_ARGS) } out->result.splitTuple.prefixNNodes = 1; out->result.splitTuple.prefixNodeLabels = - (Datum *) palloc(sizeof(Datum)); + palloc_object(Datum); out->result.splitTuple.prefixNodeLabels[0] = Int16GetDatum(*(unsigned char *) (prefixStr + commonLen)); @@ -243,7 +245,7 @@ spgist_name_choose(PG_FUNCTION_ARGS) out->result.splitTuple.prefixHasPrefix = in->hasPrefix; out->result.splitTuple.prefixPrefixDatum = in->prefixDatum; out->result.splitTuple.prefixNNodes = 1; - out->result.splitTuple.prefixNodeLabels = (Datum *) palloc(sizeof(Datum)); + out->result.splitTuple.prefixNodeLabels = palloc_object(Datum); out->result.splitTuple.prefixNodeLabels[0] = Int16GetDatum(-2); out->result.splitTuple.childNodeN = 0; out->result.splitTuple.postfixHasPrefix = false; @@ -318,9 +320,9 @@ spgist_name_inner_consistent(PG_FUNCTION_ARGS) * and see if it's consistent with the query. If so, emit an entry into * the output arrays. */ - out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes); - out->levelAdds = (int *) palloc(sizeof(int) * in->nNodes); - out->reconstructedValues = (Datum *) palloc(sizeof(Datum) * in->nNodes); + out->nodeNumbers = palloc_array(int, in->nNodes); + out->levelAdds = palloc_array(int, in->nNodes); + out->reconstructedValues = palloc_array(Datum, in->nNodes); out->nNodes = 0; for (i = 0; i < in->nNodes; i++) diff --git a/src/test/modules/ssl_passphrase_callback/meson.build b/src/test/modules/ssl_passphrase_callback/meson.build index 9cc893dc1e411..1b4078c037e55 100644 --- a/src/test/modules/ssl_passphrase_callback/meson.build +++ b/src/test/modules/ssl_passphrase_callback/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not ssl.found() subdir_done() diff --git a/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl b/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl index c16ff25718095..09ff5364f0a36 100644 --- a/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl +++ b/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -15,6 +15,8 @@ plan skip_all => 'OpenSSL not supported by this build'; } +my $libressl = not check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1"); + my $rot13pass = "SbbOnE1"; # see the Makefile for how the certificate and key have been generated @@ -76,4 +78,36 @@ # just in case $node->stop('fast'); +# Make sure the hook is bypassed when SNI is enabled. +SKIP: +{ + skip 'SNI not supported with LibreSSL', 2 if ($libressl); + + $node->append_conf( + 'postgresql.conf', qq{ +ssl_passphrase_command = 'echo FooBaR1' +ssl_sni = on +}); + $node->append_conf( + 'pg_hosts.conf', qq{ +example.org $ddir/server.crt $ddir/server.key "" "echo FooBaR1" on +example.com $ddir/server.crt $ddir/server.key "" "echo FooBaR1" on +}); + + # If the servers starts and runs, the bad ssl_passphrase.passphrase was + # correctly ignored. + $node->start; + ok(-e "$ddir/postmaster.pid", "postgres started after SNI"); + + $node->stop('fast'); + $log_contents = slurp_file($log); + like( + $log_contents, + qr/WARNING.*SNI is enabled; installed TLS init hook will be ignored/, + "server warns that init hook and SNI are incompatible"); + # Ensure that the warning was printed once and not once per host line + my $count =()= $log_contents =~ m/installed TLS init hook will be ignored/; + is($count, 1, 'Only one WARNING'); +} + done_testing(); diff --git a/src/test/modules/test_aio/meson.build b/src/test/modules/test_aio/meson.build index 73d2fd68eaa19..909f81d96c14d 100644 --- a/src/test/modules/test_aio/meson.build +++ b/src/test/modules/test_aio/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group test_aio_sources = files( 'test_aio.c', @@ -32,6 +32,8 @@ tests += { 'tests': [ 't/001_aio.pl', 't/002_io_workers.pl', + 't/003_initdb.pl', + 't/004_read_stream.pl', ], }, } diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl index 4527c70785d34..63cadd64c15b7 100644 --- a/src/test/modules/test_aio/t/001_aio.pl +++ b/src/test/modules/test_aio/t/001_aio.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -7,126 +7,56 @@ use PostgreSQL::Test::Utils; use Test::More; +use FindBin; +use lib $FindBin::RealBin; -### -# Test io_method=worker -### -my $node_worker = create_node('worker'); -$node_worker->start(); - -test_generic('worker', $node_worker); -SKIP: -{ - skip 'Injection points not supported by this build', 1 - unless $ENV{enable_injection_points} eq 'yes'; - test_inject_worker('worker', $node_worker); -} +use TestAio; -$node_worker->stop(); +my @methods = TestAio::supported_io_methods(); +my %nodes; ### -# Test io_method=io_uring +# Create and configure one instance for each io_method ### -if (have_io_uring()) +foreach my $method (@methods) { - my $node_uring = create_node('io_uring'); - $node_uring->start(); - test_generic('io_uring', $node_uring); - $node_uring->stop(); -} - - -### -# Test io_method=sync -### + my $node = PostgreSQL::Test::Cluster->new($method); -my $node_sync = create_node('sync'); - -# just to have one test not use the default auto-tuning + $nodes{$method} = $node; + $node->init(); + $node->append_conf('postgresql.conf', "io_method=$method"); + TestAio::configure($node); +} -$node_sync->append_conf( +# Just to have one test not use the default auto-tuning +$nodes{'sync'}->append_conf( 'postgresql.conf', qq( -io_max_concurrency=4 + io_max_concurrency=4 )); -$node_sync->start(); -test_generic('sync', $node_sync); -$node_sync->stop(); - -done_testing(); - ### -# Test Helpers +# Execute the tests for each io_method ### -sub create_node +foreach my $method (@methods) { - local $Test::Builder::Level = $Test::Builder::Level + 1; - - my $io_method = shift; - - my $node = PostgreSQL::Test::Cluster->new($io_method); - - # Want to test initdb for each IO method, otherwise we could just reuse - # the cluster. - # - # Unfortunately Cluster::init() puts PG_TEST_INITDB_EXTRA_OPTS after the - # options specified by ->extra, if somebody puts -c io_method=xyz in - # PG_TEST_INITDB_EXTRA_OPTS it would break this test. Fix that up if we - # detect it. - local $ENV{PG_TEST_INITDB_EXTRA_OPTS} = $ENV{PG_TEST_INITDB_EXTRA_OPTS}; - if (defined $ENV{PG_TEST_INITDB_EXTRA_OPTS} - && $ENV{PG_TEST_INITDB_EXTRA_OPTS} =~ m/io_method=/) - { - $ENV{PG_TEST_INITDB_EXTRA_OPTS} .= " -c io_method=$io_method"; - } + my $node = $nodes{$method}; - $node->init(extra => [ '-c', "io_method=$io_method" ]); - - $node->append_conf( - 'postgresql.conf', qq( -shared_preload_libraries=test_aio -log_min_messages = 'DEBUG3' -log_statement=all -log_error_verbosity=default -restart_after_crash=false -temp_buffers=100 -)); + $node->start(); + test_io_method($method, $node); + $node->stop(); +} - # Even though we used -c io_method=... above, if TEMP_CONFIG sets - # io_method, it'd override the setting persisted at initdb time. While - # using (and later verifying) the setting from initdb provides some - # verification of having used the io_method during initdb, it's probably - # not worth the complication of only appending if the variable is set in - # in TEMP_CONFIG. - $node->append_conf( - 'postgresql.conf', qq( -io_method=$io_method -)); +done_testing(); - ok(1, "$io_method: initdb"); - return $node; -} +### +# Test Helpers +### -sub have_io_uring -{ - # To detect if io_uring is supported, we look at the error message for - # assigning an invalid value to an enum GUC, which lists all the valid - # options. We need to use -C to deal with running as administrator on - # windows, the superuser check is omitted if -C is used. - my ($stdout, $stderr) = - run_command [qw(postgres -C invalid -c io_method=invalid)]; - die "can't determine supported io_method values" - unless $stderr =~ m/Available values: ([^\.]+)\./; - my $methods = $1; - note "supported io_method values are: $methods"; - - return ($methods =~ m/io_uring/) ? 1 : 0; -} sub psql_like { @@ -149,6 +79,9 @@ sub psql_like return $output; } +# Issue query, wait for the specified wait event to be reached. If +# wait_current_session is true, we will wait for the event in the current +# session, otherwise we'll wait for any session. sub query_wait_block { local $Test::Builder::Level = $Test::Builder::Level + 1; @@ -158,16 +91,29 @@ sub query_wait_block my $name = shift; my $sql = shift; my $waitfor = shift; + my $wait_current_session = shift; my $pid = $psql->query_safe('SELECT pg_backend_pid()'); $psql->{stdin} .= qq($sql;\n); $psql->{run}->pump_nb(); + note "issued sql: $sql;\n"; ok(1, "$io_method: $name: issued sql"); - $node->poll_query_until('postgres', - qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid), - $waitfor); + my $waitquery; + if ($wait_current_session) + { + $waitquery = + qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid); + } + else + { + $waitquery = + qq(SELECT wait_event FROM pg_stat_activity WHERE wait_event = '$waitfor'); + } + + note "polling for completion with $waitquery"; + $node->poll_query_until('postgres', $waitquery, $waitfor); ok(1, "$io_method: $name: observed $waitfor wait event"); } @@ -396,8 +342,8 @@ sub test_io_error { my $invalid_page_re = $tblname eq 'tbl_corr' - ? qr/invalid page in block 1 of relation base\/\d+\/\d+/ - : qr/invalid page in block 1 of relation base\/\d+\/t\d+_\d+/; + ? qr/invalid page in block 1 of relation "base\/\d+\/\d+/ + : qr/invalid page in block 1 of relation "base\/\d+\/t\d+_\d+/; # verify the error is reported in custom C code psql_like( @@ -453,7 +399,7 @@ sub test_startwait_io $io_method, $psql_a, "first StartBufferIO", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);), qr/^t$/, qr/^$/); @@ -462,14 +408,14 @@ sub test_startwait_io $io_method, $psql_a, "second StartBufferIO fails, same session", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);), qr/^f$/, qr/^$/); psql_like( $io_method, $psql_b, "second StartBufferIO fails, other session", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);), qr/^f$/, qr/^$/); @@ -479,8 +425,9 @@ sub test_startwait_io $node, $psql_b, "blocking start buffer io", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);), - "BufferIo"); + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);), + "BufferIo", + 1); # Terminate the IO, without marking it as success, this should trigger the # waiting session to be able to start the io @@ -508,7 +455,7 @@ sub test_startwait_io $io_method, $psql_a, "blocking buffer io w/ success: first start buffer io", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);), qr/^t$/, qr/^$/); @@ -518,8 +465,9 @@ sub test_startwait_io $node, $psql_b, "blocking start buffer io", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);), - "BufferIo"); + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);), + "BufferIo", + 1); # Terminate the IO, marking it as success psql_like( @@ -556,7 +504,7 @@ sub test_startwait_io $io_method, $psql_a, "first StartLocalBufferIO", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);), qr/^t$/, qr/^$/); @@ -567,7 +515,7 @@ sub test_startwait_io $io_method, $psql_a, "second StartLocalBufferIO succeeds, same session", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);), qr/^t$/, qr/^$/); @@ -579,7 +527,7 @@ sub test_startwait_io $io_method, $psql_a, "StartLocalBufferIO after not marking valid succeeds, same session", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>true);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>false);), qr/^t$/, qr/^$/); @@ -594,7 +542,7 @@ sub test_startwait_io $io_method, $psql_a, "StartLocalBufferIO after marking valid fails", - qq(SELECT buffer_call_start_io($buf_id, for_input=>true, nowait=>false);), + qq(SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true);), qr/^f$/, qr/^$/); @@ -798,7 +746,7 @@ sub test_inject "shortened multi-block read detects invalid page", qq(SELECT count(*) FROM tbl_corr WHERE ctid < '(2, 1)'), qr/^$/, - qr/ERROR:.*invalid page in block 1 of relation base\/.*/); + qr/ERROR:.*invalid page in block 1 of relation "base\/.*/); # trigger a hard error, should error out $psql->query_safe( @@ -985,7 +933,7 @@ sub test_zero qq( SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)), qr/^$/, - qr/^psql::\d+: ERROR: invalid page in block 0 of relation base\/.*\/.*$/ + qr/^psql::\d+: ERROR: invalid page in block 0 of relation "base\/.*\/.*$/ ); # Check that page validity errors are zeroed @@ -996,7 +944,7 @@ sub test_zero qq( SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>true)), qr/^$/, - qr/^psql::\d+: WARNING: invalid page in block 0 of relation base\/.*\/.*; zeroing out page$/ + qr/^psql::\d+: WARNING: invalid page in block 0 of relation "base\/.*\/.*"; zeroing out page$/ ); # And that once the corruption is fixed, we can read again @@ -1027,7 +975,7 @@ sub test_zero "$persistency: test zeroing of invalid block 3", qq(SELECT read_rel_block_ll('tbl_zero', 3, zero_on_error=>true);), qr/^$/, - qr/^psql::\d+: WARNING: invalid page in block 3 of relation base\/.*\/.*; zeroing out page$/ + qr/^psql::\d+: WARNING: invalid page in block 3 of relation "base\/.*\/.*"; zeroing out page$/ ); @@ -1044,7 +992,7 @@ sub test_zero "$persistency: test reading of invalid block 2,3 in larger read", qq(SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)), qr/^$/, - qr/^psql::\d+: ERROR: 2 invalid pages among blocks 1..4 of relation base\/.*\/.*\nDETAIL: Block 2 held first invalid page\.\nHINT:[^\n]+$/ + qr/^psql::\d+: ERROR: 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first invalid page\.\nHINT:[^\n]+$/ ); # Then test zeroing via ZERO_ON_ERROR flag @@ -1054,7 +1002,7 @@ sub test_zero "$persistency: test zeroing of invalid block 2,3 in larger read, ZERO_ON_ERROR", qq(SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>true)), qr/^$/, - qr/^psql::\d+: WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation base\/.*\/.*\nDETAIL: Block 2 held first zeroed page\.\nHINT:[^\n]+$/ + qr/^psql::\d+: WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first zeroed page\.\nHINT:[^\n]+$/ ); # Then test zeroing via zero_damaged_pages @@ -1069,7 +1017,7 @@ sub test_zero COMMIT; ), qr/^$/, - qr/^psql::\d+: WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation base\/.*\/.*\nDETAIL: Block 2 held first zeroed page\.\nHINT:[^\n]+$/ + qr/^psql::\d+: WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first zeroed page\.\nHINT:[^\n]+$/ ); $psql_a->query_safe(qq(COMMIT)); @@ -1091,7 +1039,7 @@ sub test_zero qq( SELECT count(*) FROM tbl_zero), qr/^$/, - qr/^psql::\d+: ERROR: invalid page in block 2 of relation base\/.*\/.*$/ + qr/^psql::\d+: ERROR: invalid page in block 2 of relation "base\/.*\/.*$/ ); # Verify that bufmgr.c IO zeroes out pages with page validity errors @@ -1106,7 +1054,7 @@ sub test_zero COMMIT; ), qr/^\d+$/, - qr/^psql::\d+: WARNING: invalid page in block 2 of relation base\/.*\/.*$/ + qr/^psql::\d+: WARNING: invalid page in block 2 of relation "base\/.*\/.*$/ ); # Check that warnings/errors about page validity in an IO started by @@ -1123,7 +1071,8 @@ sub test_zero { # Create a corruption and then read the block without waiting for # completion. - $psql_a->query(qq( + $psql_a->query( + qq( SELECT modify_rel_block('tbl_zero', 1, corrupt_header=>true); SELECT read_rel_block_ll('tbl_zero', 1, wait_complete=>false, zero_on_error=>true) )); @@ -1133,7 +1082,8 @@ sub test_zero $psql_b, "$persistency: test completing read by other session doesn't generate warning", qq(SELECT count(*) > 0 FROM tbl_zero;), - qr/^t$/, qr/^$/); + qr/^t$/, + qr/^$/); } # Clean up @@ -1190,7 +1140,7 @@ sub test_checksum qq( SELECT read_rel_block_ll('tbl_normal', 3, nblocks=>1, zero_on_error=>false);), qr/^$/, - qr/^psql::\d+: ERROR: invalid page in block 3 of relation base\/\d+\/\d+$/ + qr/^psql::\d+: ERROR: invalid page in block 3 of relation "base\/\d+\/\d+"$/ ); my ($cs_count_after, $cs_ts_after) = @@ -1212,7 +1162,7 @@ sub test_checksum qq( SELECT read_rel_block_ll('tbl_temp', 4, nblocks=>2, zero_on_error=>false);), qr/^$/, - qr/^psql::\d+: ERROR: invalid page in block 4 of relation base\/\d+\/t\d+_\d+$/ + qr/^psql::\d+: ERROR: invalid page in block 4 of relation "base\/\d+\/t\d+_\d+"$/ ); ($cs_count_after, $cs_ts_after) = checksum_failures($psql_a, 'postgres'); @@ -1233,7 +1183,7 @@ sub test_checksum qq( SELECT read_rel_block_ll('pg_shseclabel', 2, nblocks=>2, zero_on_error=>false);), qr/^$/, - qr/^psql::\d+: ERROR: 2 invalid pages among blocks 2..3 of relation global\/\d+\nDETAIL: Block 2 held first invalid page\.\nHINT:[^\n]+$/ + qr/^psql::\d+: ERROR: 2 invalid pages among blocks 2..3 of relation "global\/\d+"\nDETAIL: Block 2 held the first invalid page\.\nHINT:[^\n]+$/ ); ($cs_count_after, $cs_ts_after) = checksum_failures($psql_a); @@ -1298,7 +1248,7 @@ sub test_checksum_createdb "create database w/ wal strategy, invalid source", $createdb_sql, qr/^$/, - qr/psql::\d+: ERROR: invalid page in block 1 of relation base\/\d+\/\d+$/ + qr/psql::\d+: ERROR: invalid page in block 1 of relation "base\/\d+\/\d+"$/ ); my ($cs_count_after, $cs_ts_after) = checksum_failures($psql, 'regression_createdb_source'); @@ -1355,18 +1305,24 @@ sub test_ignore_checksum )); $psql->query_safe($invalidate_sql); - psql_like($io_method, $psql, + psql_like( + $io_method, + $psql, "reading block w/ wrong checksum with ignore_checksum_failure=off fails", - $count_sql, qr/^$/, qr/ERROR: invalid page in block/); + $count_sql, + qr/^$/, + qr/ERROR: invalid page in block/); $psql->query_safe("SET ignore_checksum_failure=on"); $psql->query_safe($invalidate_sql); - psql_like($io_method, $psql, - "reading block w/ wrong checksum with ignore_checksum_failure=off succeeds", - $count_sql, - qr/^$expect$/, - qr/WARNING: ignoring (checksum failure|\d checksum failures)/); + psql_like( + $io_method, + $psql, + "reading block w/ wrong checksum with ignore_checksum_failure=off succeeds", + $count_sql, + qr/^$expect$/, + qr/WARNING: ignoring (checksum failure|\d checksum failures)/); # Verify that ignore_checksum_failure=off works in multi-block reads @@ -1401,7 +1357,7 @@ sub test_ignore_checksum qq( SELECT read_rel_block_ll('tbl_cs_fail', 2, nblocks=>3, zero_on_error=>false);), qr/^$/, - qr/^psql::\d+: ERROR: invalid page in block 4 of relation base\/\d+\/\d+$/ + qr/^psql::\d+: ERROR: invalid page in block 4 of relation "base\/\d+\/\d+"$/ ); # Test multi-block read with different problems in different blocks @@ -1423,7 +1379,7 @@ sub test_ignore_checksum qq( SELECT read_rel_block_ll('tbl_cs_fail', 1, nblocks=>5, zero_on_error=>true);), qr/^$/, - qr/^psql::\d+: WARNING: zeroing 3 page\(s\) and ignoring 2 checksum failure\(s\) among blocks 1..5 of relation/ + qr/^psql::\d+: WARNING: zeroing 3 page\(s\) and ignoring 2 checksum failure\(s\) among blocks 1..5 of relation "/ ); @@ -1432,19 +1388,22 @@ sub test_ignore_checksum # file. $node->wait_for_log(qr/LOG: ignoring checksum failure in block 2/, - $log_location); + $log_location); ok(1, "$io_method: found information about checksum failure in block 2"); - $node->wait_for_log(qr/LOG: invalid page in block 3 of relation base.*; zeroing out page/, - $log_location); + $node->wait_for_log( + qr/LOG: invalid page in block 3 of relation "base.*"; zeroing out page/, + $log_location); ok(1, "$io_method: found information about invalid page in block 3"); - $node->wait_for_log(qr/LOG: invalid page in block 4 of relation base.*; zeroing out page/, - $log_location); + $node->wait_for_log( + qr/LOG: invalid page in block 4 of relation "base.*"; zeroing out page/, + $log_location); ok(1, "$io_method: found information about checksum failure in block 4"); - $node->wait_for_log(qr/LOG: invalid page in block 5 of relation base.*; zeroing out page/, - $log_location); + $node->wait_for_log( + qr/LOG: invalid page in block 5 of relation "base.*"; zeroing out page/, + $log_location); ok(1, "$io_method: found information about checksum failure in block 5"); @@ -1462,8 +1421,7 @@ sub test_ignore_checksum qq( SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);), qr/^$/, - qr/^psql::\d+: ERROR: invalid page in block 3 of relation/ - ); + qr/^psql::\d+: ERROR: invalid page in block 3 of relation "/); psql_like( $io_method, @@ -1472,7 +1430,7 @@ sub test_ignore_checksum qq( SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>true);), qr/^$/, - qr/^psql::\d+: WARNING: invalid page in block 3 of relation base\/.*; zeroing out page/ + qr/^psql::\d+: WARNING: invalid page in block 3 of relation "base\/.*"; zeroing out page/ ); @@ -1480,8 +1438,412 @@ sub test_ignore_checksum } -# Run all tests that are supported for all io_methods -sub test_generic +# Tests for StartReadBuffers() +sub test_read_buffers +{ + my $io_method = shift; + my $node = shift; + my ($ret, $output); + my $table; + + my $psql_a = $node->background_psql('postgres', on_error_stop => 0); + my $psql_b = $node->background_psql('postgres', on_error_stop => 0); + + $psql_a->query_safe( + qq( +CREATE TEMPORARY TABLE tmp_ok(data int not null); +INSERT INTO tmp_ok SELECT generate_series(1, 5000); +)); + + foreach my $persistency (qw(normal temporary)) + { + $table = $persistency eq 'normal' ? 'tbl_ok' : 'tmp_ok'; + + # check that consecutive misses are combined into one read + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, combine, block 0-1", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|, + qr/^0\|0\|t\|2$/, + qr/^$/); + + # but if we do it again, i.e. it's in the buffer pool, there will be + # two operations + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, doesn't combine hits, block 0-1", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|, + qr/^0\|0\|f\|1\n1\|1\|f\|1$/, + qr/^$/); + + # Check that a larger read interrupted by a hit works + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, prep, block 3", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 3, 1)|, + qr/^0\|3\|t\|1$/, + qr/^$/); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, interrupted by hit on 3, block 2-5", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 2, 4)|, + qr/^0\|2\|t\|1\n1\|3\|f\|1\n2\|4\|t\|2$/, + qr/^$/); + + + # Verify that a read with an initial buffer hit works + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, miss, block 0", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 1)|, + qr/^0\|0\|t\|1$/, + qr/^$/); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, hit, block 0", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 1)|, + qr/^0\|0\|f\|1$/, + qr/^$/); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, miss, block 1", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 1)|, + qr/^0\|1\|t\|1$/, + qr/^$/); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, hit, block 1", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 1)|, + qr/^0\|1\|f\|1$/, + qr/^$/); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, hit, block 0-1", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 2)|, + qr/^0\|0\|f\|1\n1\|1\|f\|1$/, + qr/^$/); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, hit 0-1, miss 2", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 3)|, + qr/^0\|0\|f\|1\n1\|1\|f\|1\n2\|2\|t\|1$/, + qr/^$/); + + # Verify that a read with an initial miss and trailing buffer hit(s) works + $psql_a->query_safe(qq|SELECT invalidate_rel_block('$table', 0)|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, miss 0, hit 1-2", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 0, 3)|, + qr/^0\|0\|t\|1\n1\|1\|f\|1\n2\|2\|f\|1$/, + qr/^$/); + $psql_a->query_safe(qq|SELECT invalidate_rel_block('$table', 1)|); + $psql_a->query_safe(qq|SELECT invalidate_rel_block('$table', 2)|); + $psql_a->query_safe(qq|SELECT * FROM read_buffers('$table', 3, 2)|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, miss 1-2, hit 3-4", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 4)|, + qr/^0\|1\|t\|2\n2\|3\|f\|1\n3\|4\|f\|1$/, + qr/^$/); + + # Verify that we aren't doing reads larger than + # io_combine_limit. That's just enforced in read_buffers() function, + # but kinda still worth testing. + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + $psql_a->query_safe(qq|SET io_combine_limit=3|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, io_combine_limit has effect", + qq|SELECT blockoff, blocknum, io_reqd, nblocks FROM read_buffers('$table', 1, 5)|, + qr/^0\|1\|t\|3\n3\|4\|t\|2$/, + qr/^$/); + $psql_a->query_safe(qq|RESET io_combine_limit|); + + + # Test encountering buffer IO we started in the first block of the + # range. + # + # Depending on how quick the IO we start completes, the IO might be + # completed or we "join" the foreign IO. To hide that variability, the + # query below treats a foreign IO as not having needed to do IO. + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + $psql_a->query_safe( + qq|SELECT read_rel_block_ll('$table', 1, wait_complete=>false)|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, in-progress 1, read 1-3", + qq|SELECT blockoff, blocknum, io_reqd and not foreign_io, nblocks FROM read_buffers('$table', 1, 3)|, + qr/^0\|1\|f\|1\n1\|2\|t\|2$/, + qr/^$/); + + # Test in-progress IO in the middle block of the range + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + $psql_a->query_safe( + qq|SELECT read_rel_block_ll('$table', 2, wait_complete=>false)|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, in-progress 2, read 1-3", + qq|SELECT blockoff, blocknum, io_reqd and not foreign_io, nblocks FROM read_buffers('$table', 1, 3)|, + qr/^0\|1\|t\|1\n1\|2\|f\|1\n2\|3\|t\|1$/, + qr/^$/); + + # Test in-progress IO on the last block of the range + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + $psql_a->query_safe( + qq|SELECT read_rel_block_ll('$table', 3, wait_complete=>false)|); + psql_like( + $io_method, + $psql_a, + "$persistency: read buffers, in-progress 3, read 1-3", + qq|SELECT blockoff, blocknum, io_reqd and not foreign_io, nblocks FROM read_buffers('$table', 1, 3)|, + qr/^0\|1\|t\|2\n2\|3\|f\|1$/, + qr/^$/); + } + + # The remaining tests don't make sense for temp tables, as they are + # concerned with multiple sessions interacting with each other. + $table = 'tbl_ok'; + my $persistency = 'normal'; + + # Test start buffer IO will split IO if there's IO in progress. We can't + # observe this with sync, as that does not start the IO operation in + # StartReadBuffers(). + if ($io_method ne 'sync') + { + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + + my $buf_id = + $psql_b->query_safe(qq|SELECT buffer_create_toy('$table', 3)|); + $psql_b->query_safe( + qq|SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true)| + ); + + query_wait_block( + $io_method, + $node, + $psql_a, + "$persistency: read buffers blocks waiting for concurrent IO", + qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM read_buffers('$table', 1, 5);\n|, + "BufferIo", + 1); + $psql_b->query_safe( + qq|SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, release_aio=>false)| + ); + # Because no IO wref was assigned, block 3 should not report foreign IO + pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout}, + qr/0\|1\|t\|f\|2\n2\|3\|t\|f\|3/); + ok(1, + "$io_method: $persistency: IO was split due to concurrent failed IO" + ); + + # Same as before, except the concurrent IO succeeds this time + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + $buf_id = + $psql_b->query_safe(qq|SELECT buffer_create_toy('$table', 3)|); + $psql_b->query_safe( + qq|SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true)| + ); + + query_wait_block( + $io_method, + $node, + $psql_a, + "$persistency: read buffers blocks waiting for concurrent IO", + qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM read_buffers('$table', 1, 5);\n|, + "BufferIo", + 1); + $psql_b->query_safe( + qq|SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>true, io_error=>false, release_aio=>false)| + ); + # Because no IO wref was assigned, block 3 should not report foreign IO + pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout}, + qr/0\|1\|t\|f\|2\n2\|3\|f\|f\|1\n3\|4\|t\|f\|2/); + ok(1, + "$io_method: $persistency: IO was split due to concurrent successful IO" + ); + } + + $psql_a->quit(); + $psql_b->quit(); +} + + +# Tests for StartReadBuffers() that depend on injection point support +sub test_read_buffers_inject +{ + my $io_method = shift; + my $node = shift; + + my $psql_a = $node->background_psql('postgres', on_error_stop => 0); + my $psql_b = $node->background_psql('postgres', on_error_stop => 0); + my $psql_c = $node->background_psql('postgres', on_error_stop => 0); + + my $expected; + + # We can't easily test waiting for foreign IOs on temporary tables, as the + # waiting in the completion hook will just stall the backend. For worker + # that is because temporary table IO is executed synchronously, for + # io_uring the completion will be executed in the same process, but due to + # temporary tables not being shared, we can't do the wait in another + # backend. + my $table = 'tbl_ok'; + my $persistency = 'normal'; + + + ### + # Test if a read buffers encounters AIO in progress by another backend, it + # recognizes that other IO as a foreign IO. + ### + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + + # B: Trigger wait in the next AIO read for block 1. + $psql_b->query_safe( + qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(), + relfilenode=>pg_relation_filenode('$table'), + blockno=>1);/); + ok(1, + "$io_method: $persistency: configure wait in completion of block 1"); + + # B: Read block 1 and wait for the completion hook to be reached (which could + # be in B itself or in an IO worker) + query_wait_block( + $io_method, + $node, + $psql_b, + "$persistency: wait in completion of block 1", + qq|SELECT read_rel_block_ll('$table', blockno=>1, nblocks=>1)|, + 'completion_wait', + 0); + + # A: Start read, wait until we're waiting for IO completion + query_wait_block( + $io_method, + $node, + $psql_a, + "$persistency: read 1-4, blocked on in-progress 1", + qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM read_buffers('$table', 1, 4)|, + "AioIoCompletion", + 1); + + # C: Release B from completion hook + $psql_c->query_safe(qq|SELECT inj_io_completion_continue()|); + ok(1, "$io_method: $persistency: continued completion of block 1"); + + # A: Check that we recognized the foreign IO wait, if possible + # + # Due to sync mode not actually issuing IO below StartReadBuffers(), we + # can't observe encountering foreign IO. It still seems worth exercising these + # paths however. + if ($io_method ne 'sync') + { + # A foreign IO covering block 1, and one IO covering blocks 2-4. + $expected = qr/0\|1\|t\|t\|1\n1\|2\|t\|f\|3/; + } + else + { + # One IO covering everything, as that's what StartReadBuffers() will + # return for something with misses in sync mode. + $expected = qr/0\|1\|t\|f\|4/; + } + pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout}, + $expected); + ok(1, + "$io_method: $persistency: read 1-3, blocked on in-progress 1, see expected result" + ); + $psql_a->{stdout} = ''; + + + ### + # Test if a read buffers encounters AIO in progress by another backend, it + # recognizes that other IO as a foreign IO. This time we encounter the + # foreign IO multiple times. + ### + $psql_a->query_safe(qq|SELECT evict_rel('$table')|); + + # B: Trigger wait in the next AIO read for block 3. + $psql_b->query_safe( + qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(), + relfilenode=>pg_relation_filenode('$table'), + blockno=>3);/); + ok(1, + "$io_method: $persistency: configure wait in completion of block 3"); + + # B: Read block 2-3 and wait for the completion hook to be reached (which + # could be in B itself or in an IO worker) + query_wait_block( + $io_method, + $node, + $psql_b, + "$persistency: wait in completion of block 2+3", + qq|SELECT read_rel_block_ll('$table', blockno=>2, nblocks=>2)|, + 'completion_wait', + 0); + + # A: Start read, wait until we're waiting for IO completion + # + # Note that we need to defer waiting for IO until the end of + # read_buffers(), to be able to see that the IO on 3 is still in progress. + query_wait_block( + $io_method, + $node, + $psql_a, + "$persistency: read 0-3, blocked on in-progress 2+3", + qq|SELECT blockoff, blocknum, io_reqd, foreign_io, nblocks FROM +read_buffers('$table', 0, 4)|, + "AioIoCompletion", 1); + + # C: Release B from completion hook + $psql_c->query_safe(qq|SELECT inj_io_completion_continue()|); + ok(1, "$io_method: $persistency: continued completion of block 2+3"); + + # A: Check that we recognized the foreign IO wait, if possible + # + # See comment further up about sync mode. + if ($io_method ne 'sync') + { + # One IO covering blocks 0-1, A foreign IO covering block 2, and a + # foreign IO covering block 3 (same wref as for block 2). + $expected = qr/0\|0\|t\|f\|2\n2\|2\|t\|t\|1\n3\|3\|t\|t\|1/; + } + else + { + # One IO covering everything, as that's what StartReadBuffers() will + # return for something with misses in sync mode. + $expected = qr/0\|0\|t\|f\|4/; + } + pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout}, + $expected); + ok(1, + "$io_method: $persistency: read 0-3, blocked on in-progress 2+3, see expected result" + ); + $psql_a->{stdout} = ''; + + + $psql_a->quit(); + $psql_b->quit(); + $psql_c->quit(); +} + +# Run all tests that for the specified node / io_method +sub test_io_method { my $io_method = shift; my $node = shift; @@ -1515,11 +1877,26 @@ sub test_generic test_checksum($io_method, $node); test_ignore_checksum($io_method, $node); test_checksum_createdb($io_method, $node); + test_read_buffers($io_method, $node); + # generic injection tests SKIP: { skip 'Injection points not supported by this build', 1 unless $ENV{enable_injection_points} eq 'yes'; test_inject($io_method, $node); + test_read_buffers_inject($io_method, $node); + } + + # worker specific injection tests + if ($io_method eq 'worker') + { + SKIP: + { + skip 'Injection points not supported by this build', 1 + unless $ENV{enable_injection_points} eq 'yes'; + + test_inject_worker($io_method, $node); + } } } diff --git a/src/test/modules/test_aio/t/002_io_workers.pl b/src/test/modules/test_aio/t/002_io_workers.pl index af5fae15ea78e..b9775811d4ddb 100644 --- a/src/test/modules/test_aio/t/002_io_workers.pl +++ b/src/test/modules/test_aio/t/002_io_workers.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -14,6 +14,9 @@ $node->append_conf( 'postgresql.conf', qq( io_method=worker +io_worker_idle_timeout=0ms +io_worker_launch_interval=0ms +io_max_workers=32 )); $node->start(); @@ -31,7 +34,7 @@ sub test_number_of_io_workers_dynamic { my $node = shift; - my $prev_worker_count = $node->safe_psql('postgres', 'SHOW io_workers'); + my $prev_worker_count = $node->safe_psql('postgres', 'SHOW io_min_workers'); # Verify that worker count can't be set to 0 change_number_of_io_workers($node, 0, $prev_worker_count, 1); @@ -62,23 +65,24 @@ sub change_number_of_io_workers my ($result, $stdout, $stderr); ($result, $stdout, $stderr) = - $node->psql('postgres', "ALTER SYSTEM SET io_workers = $worker_count"); + $node->psql('postgres', "ALTER SYSTEM SET io_min_workers = $worker_count"); $node->safe_psql('postgres', 'SELECT pg_reload_conf()'); if ($expect_failure) { - ok( $stderr =~ - /$worker_count is outside the valid range for parameter "io_workers"/, - "updating number of io_workers to $worker_count failed, as expected" + like( + $stderr, + qr/$worker_count is outside the valid range for parameter "io_min_workers"/, + "updating io_min_workers to $worker_count failed, as expected" ); return $prev_worker_count; } else { - is( $node->safe_psql('postgres', 'SHOW io_workers'), + is( $node->safe_psql('postgres', 'SHOW io_min_workers'), $worker_count, - "updating number of io_workers from $prev_worker_count to $worker_count" + "updating number of io_min_workers from $prev_worker_count to $worker_count" ); check_io_worker_count($node, $worker_count); diff --git a/src/test/modules/test_aio/t/003_initdb.pl b/src/test/modules/test_aio/t/003_initdb.pl new file mode 100644 index 0000000000000..c03ae58d00a2c --- /dev/null +++ b/src/test/modules/test_aio/t/003_initdb.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# +# Test initdb for each IO method. This is done separately from 001_aio.pl, as +# it isn't fast. This way the more commonly failing / hacked-on 001_aio.pl can +# be iterated on more quickly. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use TestAio; + + +foreach my $method (TestAio::supported_io_methods()) +{ + test_create_node($method); +} + +done_testing(); + + +sub test_create_node +{ + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my $io_method = shift; + + my $node = PostgreSQL::Test::Cluster->new($io_method); + + # Want to test initdb for each IO method, otherwise we could just reuse + # the cluster. + # + # Unfortunately Cluster::init() puts PG_TEST_INITDB_EXTRA_OPTS after the + # options specified by ->extra, if somebody puts -c io_method=xyz in + # PG_TEST_INITDB_EXTRA_OPTS it would break this test. Fix that up if we + # detect it. + local $ENV{PG_TEST_INITDB_EXTRA_OPTS} = $ENV{PG_TEST_INITDB_EXTRA_OPTS}; + if (defined $ENV{PG_TEST_INITDB_EXTRA_OPTS} + && $ENV{PG_TEST_INITDB_EXTRA_OPTS} =~ m/io_method=/) + { + $ENV{PG_TEST_INITDB_EXTRA_OPTS} .= " -c io_method=$io_method"; + } + + $node->init(extra => [ '-c', "io_method=$io_method" ]); + + TestAio::configure($node); + + # Even though we used -c io_method=... above, if TEMP_CONFIG sets + # io_method, it'd override the setting persisted at initdb time. While + # using (and later verifying) the setting from initdb provides some + # verification of having used the io_method during initdb, it's probably + # not worth the complication of only appending if the variable is set in + # in TEMP_CONFIG. + $node->append_conf( + 'postgresql.conf', qq( +io_method=$io_method +)); + + ok(1, "$io_method: initdb"); + + $node->start(); + $node->stop(); + ok(1, "$io_method: start & stop"); + + return $node; +} diff --git a/src/test/modules/test_aio/t/004_read_stream.pl b/src/test/modules/test_aio/t/004_read_stream.pl new file mode 100644 index 0000000000000..32311c07ac02b --- /dev/null +++ b/src/test/modules/test_aio/t/004_read_stream.pl @@ -0,0 +1,278 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use TestAio; + + +my $node = PostgreSQL::Test::Cluster->new('test'); +$node->init(); + +TestAio::configure($node); + +$node->append_conf( + 'postgresql.conf', qq( +max_connections=8 +io_method=worker +)); + +$node->start(); +test_setup($node); +$node->stop(); + + +foreach my $method (TestAio::supported_io_methods()) +{ + $node->adjust_conf('postgresql.conf', 'io_method', $method); + $node->start(); + test_io_method($method, $node); + $node->stop(); +} + +done_testing(); + + +sub test_setup +{ + my $node = shift; + + $node->safe_psql( + 'postgres', qq( +CREATE EXTENSION test_aio; + +CREATE TABLE largeish(k int not null) WITH (FILLFACTOR=10); +INSERT INTO largeish(k) SELECT generate_series(1, 10000); +)); + ok(1, "setup"); +} + + +sub test_repeated_blocks +{ + my $io_method = shift; + my $node = shift; + + my $psql = $node->background_psql('postgres', on_error_stop => 0); + + # Preventing larger reads makes testing easier + $psql->query_safe(qq/SET io_combine_limit = 1/); + + # test miss of the same block twice in a row + $psql->query_safe(qq/SELECT evict_rel('largeish');/); + + # block 0 grows the distance enough that the stream will look ahead and try + # to start a pending read for block 2 (and later block 4) twice before + # returning any buffers. + $psql->query_safe( + qq/SELECT * FROM read_stream_for_blocks('largeish', + ARRAY[0, 2, 2, 4, 4]);/); + + ok(1, "$io_method: stream missing the same block repeatedly"); + + $psql->query_safe( + qq/SELECT * FROM read_stream_for_blocks('largeish', + ARRAY[0, 2, 2, 4, 4]);/); + ok(1, "$io_method: stream hitting the same block repeatedly"); + + # test hit of the same block twice in a row + $psql->query_safe(qq/SELECT evict_rel('largeish');/); + $psql->query_safe( + qq/SELECT * FROM read_stream_for_blocks('largeish', + ARRAY[0, 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0]);/); + ok(1, "$io_method: stream accessing same block"); + + # Test repeated blocks with a temp table, using invalidate_rel_block() + # to evict individual local buffers. + $psql->query_safe( + qq/CREATE TEMP TABLE largeish_temp(k int not null) WITH (FILLFACTOR=10); + INSERT INTO largeish_temp(k) SELECT generate_series(1, 200);/); + + # Evict the specific blocks we'll request to force misses + $psql->query_safe(qq/SELECT invalidate_rel_block('largeish_temp', 0);/); + $psql->query_safe(qq/SELECT invalidate_rel_block('largeish_temp', 2);/); + $psql->query_safe(qq/SELECT invalidate_rel_block('largeish_temp', 4);/); + + $psql->query_safe( + qq/SELECT * FROM read_stream_for_blocks('largeish_temp', + ARRAY[0, 2, 2, 4, 4]);/); + ok(1, "$io_method: temp stream missing the same block repeatedly"); + + # Now the blocks are cached, so repeated access should be hits + $psql->query_safe( + qq/SELECT * FROM read_stream_for_blocks('largeish_temp', + ARRAY[0, 2, 2, 4, 4]);/); + ok(1, "$io_method: temp stream hitting the same block repeatedly"); + + $psql->quit(); +} + + +sub test_inject_foreign +{ + my $io_method = shift; + my $node = shift; + + my $psql_a = $node->background_psql('postgres', on_error_stop => 0); + my $psql_b = $node->background_psql('postgres', on_error_stop => 0); + + my $pid_a = $psql_a->query_safe(qq/SELECT pg_backend_pid();/); + + + ### + # Test read stream encountering buffers undergoing IO in another backend, + # with the other backend's reads succeeding. + ### + $psql_a->query_safe(qq/SELECT evict_rel('largeish');/); + + $psql_b->query_safe( + qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(), + relfilenode=>pg_relation_filenode('largeish'));/); + + $psql_b->{stdin} .= qq/SELECT read_rel_block_ll('largeish', + blockno=>5, nblocks=>1);\n/; + $psql_b->{run}->pump_nb(); + + $node->poll_query_until( + 'postgres', qq/SELECT wait_event FROM pg_stat_activity + WHERE wait_event = 'completion_wait';/, + 'completion_wait'); + + # Block 5 is undergoing IO in session b, so session a will move on to start + # a new IO for block 7. + $psql_a->{stdin} .= qq/SELECT array_agg(blocknum) FROM + read_stream_for_blocks('largeish', ARRAY[0, 2, 5, 7]);\n/; + $psql_a->{run}->pump_nb(); + + $node->poll_query_until('postgres', + qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid_a), + 'AioIoCompletion'); + + $node->safe_psql('postgres', qq/SELECT inj_io_completion_continue()/); + + pump_until( + $psql_a->{run}, $psql_a->{timeout}, + \$psql_a->{stdout}, qr/\{0,2,5,7\}/); + $psql_a->{stdout} = ''; + + ok(1, + qq/$io_method: read stream encounters succeeding IO by another backend/ + ); + + ### + # Test read stream encountering buffers undergoing IO in another backend, + # with the other backend's reads failing. + ### + $psql_a->query_safe(qq/SELECT evict_rel('largeish');/); + + $psql_b->query_safe( + qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(), + relfilenode=>pg_relation_filenode('largeish'));/); + + $psql_b->query_safe( + qq/SELECT inj_io_short_read_attach(-errno_from_string('EIO'), + pid=>pg_backend_pid(), + relfilenode=>pg_relation_filenode('largeish'));/); + + $psql_b->{stdin} .= qq/SELECT read_rel_block_ll('largeish', + blockno=>5, nblocks=>1);\n/; + $psql_b->{run}->pump_nb(); + + $node->poll_query_until( + 'postgres', + qq/SELECT wait_event FROM pg_stat_activity + WHERE wait_event = 'completion_wait';/, + 'completion_wait'); + + $psql_a->{stdin} .= qq/SELECT array_agg(blocknum) FROM + read_stream_for_blocks('largeish', ARRAY[0, 2, 5, 7]);\n/; + $psql_a->{run}->pump_nb(); + + $node->poll_query_until('postgres', + qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid_a), + 'AioIoCompletion'); + + $node->safe_psql('postgres', qq/SELECT inj_io_completion_continue()/); + + pump_until( + $psql_a->{run}, $psql_a->{timeout}, + \$psql_a->{stdout}, qr/\{0,2,5,7\}/); + $psql_a->{stdout} = ''; + + pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stderr}, + qr/ERROR.*could not read blocks 5\.\.5/); + ok(1, "$io_method: injected error occurred"); + $psql_b->{stderr} = ''; + $psql_b->query_safe(qq/SELECT inj_io_short_read_detach();/); + + ok(1, + qq/$io_method: read stream encounters failing IO by another backend/); + + + ### + # Test read stream encountering two buffers that are undergoing the same + # IO, started by another backend. + ### + $psql_a->query_safe(qq/SELECT evict_rel('largeish');/); + + $psql_b->query_safe( + qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(), + relfilenode=>pg_relation_filenode('largeish'));/); + + $psql_b->{stdin} .= qq/SELECT read_rel_block_ll('largeish', + blockno=>2, nblocks=>3);\n/; + $psql_b->{run}->pump_nb(); + + $node->poll_query_until( + 'postgres', + qq/SELECT wait_event FROM pg_stat_activity + WHERE wait_event = 'completion_wait';/, + 'completion_wait'); + + # Blocks 2 and 4 are undergoing IO initiated by session b + $psql_a->{stdin} .= qq/SELECT array_agg(blocknum) FROM + read_stream_for_blocks('largeish', ARRAY[0, 2, 4]);\n/; + $psql_a->{run}->pump_nb(); + + $node->poll_query_until('postgres', + qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid_a), + 'AioIoCompletion'); + + $node->safe_psql('postgres', qq/SELECT inj_io_completion_continue()/); + + pump_until( + $psql_a->{run}, $psql_a->{timeout}, + \$psql_a->{stdout}, qr/\{0,2,4\}/); + $psql_a->{stdout} = ''; + + ok(1, qq/$io_method: read stream encounters two buffer read in one IO/); + + $psql_a->quit(); + $psql_b->quit(); +} + + +sub test_io_method +{ + my $io_method = shift; + my $node = shift; + + is($node->safe_psql('postgres', 'SHOW io_method'), + $io_method, "$io_method: io_method set correctly"); + + test_repeated_blocks($io_method, $node); + + SKIP: + { + skip 'Injection points not supported by this build', 1 + unless $ENV{enable_injection_points} eq 'yes'; + test_inject_foreign($io_method, $node); + } +} diff --git a/src/test/modules/test_aio/t/TestAio.pm b/src/test/modules/test_aio/t/TestAio.pm new file mode 100644 index 0000000000000..c150118d033e6 --- /dev/null +++ b/src/test/modules/test_aio/t/TestAio.pm @@ -0,0 +1,93 @@ +# Copyright (c) 2024-2025, PostgreSQL Global Development Group + +=pod + +=head1 NAME + +TestAio - helpers for writing AIO related tests + +=cut + +package TestAio; + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + + +=pod + +=head1 METHODS + +=over + +=item TestAio::supported_io_methods() + +Return an array of all the supported values for the io_method GUC + +=cut + +sub supported_io_methods() +{ + my @io_methods = ('worker'); + + push(@io_methods, "io_uring") if have_io_uring(); + + # Return sync last, as it will least commonly fail + push(@io_methods, "sync"); + + return @io_methods; +} + + +=item TestAio::configure() + +Prepare a cluster for AIO test + +=cut + +sub configure +{ + my $node = shift; + + $node->append_conf( + 'postgresql.conf', qq( +shared_preload_libraries=test_aio +log_min_messages = 'DEBUG3' +log_statement=all +log_error_verbosity=default +restart_after_crash=false +temp_buffers=100 +)); + +} + + +=pod + +=item TestAio::have_io_uring() + +Return if io_uring is supported + +=cut + +sub have_io_uring +{ + # To detect if io_uring is supported, we look at the error message for + # assigning an invalid value to an enum GUC, which lists all the valid + # options. We need to use -C to deal with running as administrator on + # windows, the superuser check is omitted if -C is used. + my ($stdout, $stderr) = + run_command [qw(postgres -C invalid -c io_method=invalid)]; + die "can't determine supported io_method values" + unless $stderr =~ m/Available values: ([^\.]+)\./; + my $methods = $1; + note "supported io_method values are: $methods"; + + return ($methods =~ m/io_uring/) ? 1 : 0; +} + +1; diff --git a/src/test/modules/test_aio/test_aio--1.0.sql b/src/test/modules/test_aio/test_aio--1.0.sql index e495481c41e43..762ac29512f7f 100644 --- a/src/test/modules/test_aio/test_aio--1.0.sql +++ b/src/test/modules/test_aio/test_aio--1.0.sql @@ -33,6 +33,10 @@ CREATE FUNCTION read_rel_block_ll( RETURNS pg_catalog.void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION evict_rel(rel regclass) +RETURNS pg_catalog.void STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION invalidate_rel_block(rel regclass, blockno int) RETURNS pg_catalog.void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; @@ -41,7 +45,7 @@ CREATE FUNCTION buffer_create_toy(rel regclass, blockno int4) RETURNS pg_catalog.int4 STRICT AS 'MODULE_PATHNAME' LANGUAGE C; -CREATE FUNCTION buffer_call_start_io(buffer int, for_input bool, nowait bool) +CREATE FUNCTION buffer_call_start_io(buffer int, for_input bool, wait bool) RETURNS pg_catalog.bool STRICT AS 'MODULE_PATHNAME' LANGUAGE C; @@ -49,6 +53,16 @@ CREATE FUNCTION buffer_call_terminate_io(buffer int, for_input bool, succeed boo RETURNS pg_catalog.void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION read_buffers(rel regclass, startblock int4, nblocks int4, OUT blockoff int4, OUT blocknum int4, OUT io_reqd bool, OUT foreign_io bool, OUT nblocks int4, OUT buf int4[]) +RETURNS SETOF record STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +/* + * Read stream related functions + */ +CREATE FUNCTION read_stream_for_blocks(rel regclass, blocks int4[], OUT blockoff int4, OUT blocknum int4, OUT buf int4) +RETURNS SETOF record STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; /* @@ -91,8 +105,16 @@ AS 'MODULE_PATHNAME' LANGUAGE C; /* * Injection point related functions */ -CREATE FUNCTION inj_io_short_read_attach(result int) -RETURNS pg_catalog.void STRICT +CREATE FUNCTION inj_io_completion_wait(pid int DEFAULT NULL, relfilenode oid DEFAULT NULL, blockno int4 DEFAULT NULL) +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION inj_io_completion_continue() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION inj_io_short_read_attach(result int, pid int DEFAULT NULL, relfilenode oid DEFAULT NULL) +RETURNS pg_catalog.void AS 'MODULE_PATHNAME' LANGUAGE C; CREATE FUNCTION inj_io_short_read_detach() diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c index 5cdfb89210b28..35efba1a5e3c9 100644 --- a/src/test/modules/test_aio/test_aio.c +++ b/src/test/modules/test_aio/test_aio.c @@ -8,7 +8,7 @@ * It'd not generally be safe to export these functions to SQL, but for a test * that's fine. * - * Copyright (c) 2020-2025, PostgreSQL Global Development Group + * Copyright (c) 2020-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_aio/test_aio.c @@ -19,36 +19,70 @@ #include "postgres.h" #include "access/relation.h" +#include "catalog/pg_type.h" #include "fmgr.h" +#include "funcapi.h" #include "storage/aio.h" #include "storage/aio_internal.h" #include "storage/buf_internals.h" #include "storage/bufmgr.h" #include "storage/checksum.h" -#include "storage/ipc.h" +#include "storage/condition_variable.h" #include "storage/lwlock.h" +#include "storage/proc.h" +#include "storage/procnumber.h" +#include "storage/read_stream.h" +#include "utils/array.h" #include "utils/builtins.h" #include "utils/injection_point.h" #include "utils/rel.h" +#include "utils/tuplestore.h" +#include "utils/wait_event.h" PG_MODULE_MAGIC; +/* In shared memory */ typedef struct InjIoErrorState { + ConditionVariable cv; + bool enabled_short_read; bool enabled_reopen; + bool enabled_completion_wait; + Oid completion_wait_relfilenode; + BlockNumber completion_wait_blockno; + pid_t completion_wait_pid; + uint32 completion_wait_event; + bool short_read_result_set; + Oid short_read_relfilenode; + pid_t short_read_pid; int short_read_result; -} InjIoErrorState; +} InjIoErrorState; + +typedef struct BlocksReadStreamData +{ + int nblocks; + int curblock; + uint32 *blocks; +} BlocksReadStreamData; -static InjIoErrorState * inj_io_error_state; + +static InjIoErrorState *inj_io_error_state; /* Shared memory init callbacks */ -static shmem_request_hook_type prev_shmem_request_hook = NULL; -static shmem_startup_hook_type prev_shmem_startup_hook = NULL; +static void test_aio_shmem_request(void *arg); +static void test_aio_shmem_init(void *arg); +static void test_aio_shmem_attach(void *arg); + +static const ShmemCallbacks inj_io_shmem_callbacks = { + .request_fn = test_aio_shmem_request, + .init_fn = test_aio_shmem_init, + .attach_fn = test_aio_shmem_attach, +}; static PgAioHandle *last_handle; @@ -56,66 +90,55 @@ static PgAioHandle *last_handle; static void -test_aio_shmem_request(void) +test_aio_shmem_request(void *arg) { - if (prev_shmem_request_hook) - prev_shmem_request_hook(); - - RequestAddinShmemSpace(sizeof(InjIoErrorState)); + ShmemRequestStruct(.name = "test_aio injection points", + .size = sizeof(InjIoErrorState), + .ptr = (void **) &inj_io_error_state, + ); } static void -test_aio_shmem_startup(void) +test_aio_shmem_init(void *arg) { - bool found; - - if (prev_shmem_startup_hook) - prev_shmem_startup_hook(); - - /* Create or attach to the shared memory state */ - LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + /* First time through, initialize */ + inj_io_error_state->enabled_short_read = false; + inj_io_error_state->enabled_reopen = false; + inj_io_error_state->enabled_completion_wait = false; - inj_io_error_state = ShmemInitStruct("injection_points", - sizeof(InjIoErrorState), - &found); - - if (!found) - { - /* First time through, initialize */ - inj_io_error_state->enabled_short_read = false; - inj_io_error_state->enabled_reopen = false; + ConditionVariableInit(&inj_io_error_state->cv); + inj_io_error_state->completion_wait_event = WaitEventInjectionPointNew("completion_wait"); #ifdef USE_INJECTION_POINTS - InjectionPointAttach("aio-process-completion-before-shared", - "test_aio", - "inj_io_short_read", - NULL, - 0); - InjectionPointLoad("aio-process-completion-before-shared"); - - InjectionPointAttach("aio-worker-after-reopen", - "test_aio", - "inj_io_reopen", - NULL, - 0); - InjectionPointLoad("aio-worker-after-reopen"); + InjectionPointAttach("aio-process-completion-before-shared", + "test_aio", + "inj_io_completion_hook", + NULL, + 0); + InjectionPointLoad("aio-process-completion-before-shared"); + + InjectionPointAttach("aio-worker-after-reopen", + "test_aio", + "inj_io_reopen", + NULL, + 0); + InjectionPointLoad("aio-worker-after-reopen"); #endif - } - else - { - /* - * Pre-load the injection points now, so we can call them in a - * critical section. - */ +} + +static void +test_aio_shmem_attach(void *arg) +{ + /* + * Pre-load the injection points now, so we can call them in a critical + * section. + */ #ifdef USE_INJECTION_POINTS - InjectionPointLoad("aio-process-completion-before-shared"); - InjectionPointLoad("aio-worker-after-reopen"); - elog(LOG, "injection point loaded"); + InjectionPointLoad("aio-process-completion-before-shared"); + InjectionPointLoad("aio-worker-after-reopen"); + elog(LOG, "injection point loaded"); #endif - } - - LWLockRelease(AddinShmemInitLock); } void @@ -124,10 +147,7 @@ _PG_init(void) if (!process_shared_preload_libraries_in_progress) return; - prev_shmem_request_hook = shmem_request_hook; - shmem_request_hook = test_aio_shmem_request; - prev_shmem_startup_hook = shmem_startup_hook; - shmem_startup_hook = test_aio_shmem_startup; + RegisterShmemCallbacks(&inj_io_shmem_callbacks); } @@ -221,9 +241,7 @@ modify_rel_block(PG_FUNCTION_ARGS) */ memcpy(page, BufferGetPage(buf), BLCKSZ); - LockBuffer(buf, BUFFER_LOCK_UNLOCK); - - ReleaseBuffer(buf); + UnlockReleaseBuffer(buf); /* * Don't want to have a buffer in-memory that's marked valid where the @@ -288,7 +306,7 @@ modify_rel_block(PG_FUNCTION_ARGS) } else { - PageSetChecksumInplace(page, blkno); + PageSetChecksum(page, blkno); } smgrwrite(RelationGetSmgr(rel), @@ -308,8 +326,9 @@ create_toy_buffer(Relation rel, BlockNumber blkno) { Buffer buf; BufferDesc *buf_hdr; - uint32 buf_state; + uint64 buf_state; bool was_pinned = false; + uint64 unset_bits = 0; /* place buffer in shared buffers without erroring out */ buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_ZERO_AND_LOCK, NULL); @@ -318,7 +337,7 @@ create_toy_buffer(Relation rel, BlockNumber blkno) if (RelationUsesLocalBuffers(rel)) { buf_hdr = GetLocalBufferDescriptor(-buf - 1); - buf_state = pg_atomic_read_u32(&buf_hdr->state); + buf_state = pg_atomic_read_u64(&buf_hdr->state); } else { @@ -334,12 +353,17 @@ create_toy_buffer(Relation rel, BlockNumber blkno) if (BUF_STATE_GET_REFCOUNT(buf_state) > 1) was_pinned = true; else - buf_state &= ~(BM_VALID | BM_DIRTY); + unset_bits |= BM_VALID | BM_DIRTY; if (RelationUsesLocalBuffers(rel)) - pg_atomic_unlocked_write_u32(&buf_hdr->state, buf_state); + { + buf_state &= ~unset_bits; + pg_atomic_unlocked_write_u64(&buf_hdr->state, buf_state); + } else - UnlockBufHdr(buf_hdr, buf_state); + { + UnlockBufHdrExt(buf_hdr, buf_state, 0, unset_bits, 0); + } if (was_pinned) elog(ERROR, "toy buffer %d was already pinned", @@ -378,7 +402,7 @@ read_rel_block_ll(PG_FUNCTION_ARGS) if (nblocks <= 0 || nblocks > PG_IOV_MAX) elog(ERROR, "nblocks is out of range"); - rel = relation_open(relid, AccessExclusiveLock); + rel = relation_open(relid, AccessShareLock); for (int i = 0; i < nblocks; i++) { @@ -399,13 +423,13 @@ read_rel_block_ll(PG_FUNCTION_ARGS) if (RelationUsesLocalBuffers(rel)) { for (int i = 0; i < nblocks; i++) - StartLocalBufferIO(buf_hdrs[i], true, false); + StartLocalBufferIO(buf_hdrs[i], true, true, NULL); pgaio_io_set_flag(ioh, PGAIO_HF_REFERENCES_LOCAL); } else { for (int i = 0; i < nblocks; i++) - StartBufferIO(buf_hdrs[i], true, false); + StartSharedBufferIO(buf_hdrs[i], true, true, NULL); } pgaio_io_set_handle_data_32(ioh, (uint32 *) bufs, nblocks); @@ -452,29 +476,24 @@ read_rel_block_ll(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } -PG_FUNCTION_INFO_V1(invalidate_rel_block); -Datum -invalidate_rel_block(PG_FUNCTION_ARGS) +/* helper for invalidate_rel_block() and evict_rel() */ +static void +invalidate_one_block(Relation rel, ForkNumber forknum, BlockNumber blkno) { - Oid relid = PG_GETARG_OID(0); - BlockNumber blkno = PG_GETARG_UINT32(1); - Relation rel; PrefetchBufferResult pr; Buffer buf; - rel = relation_open(relid, AccessExclusiveLock); - /* * This is a gross hack, but there's no other API exposed that allows to * get a buffer ID without actually reading the block in. */ - pr = PrefetchBuffer(rel, MAIN_FORKNUM, blkno); + pr = PrefetchBuffer(rel, forknum, blkno); buf = pr.recent_buffer; if (BufferIsValid(buf)) { /* if the buffer contents aren't valid, this'll return false */ - if (ReadRecentBuffer(rel->rd_locator, MAIN_FORKNUM, blkno, buf)) + if (ReadRecentBuffer(rel->rd_locator, forknum, blkno, buf)) { BufferDesc *buf_hdr = BufferIsLocal(buf) ? GetLocalBufferDescriptor(-buf - 1) @@ -483,15 +502,14 @@ invalidate_rel_block(PG_FUNCTION_ARGS) LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE); - if (pg_atomic_read_u32(&buf_hdr->state) & BM_DIRTY) + if (pg_atomic_read_u64(&buf_hdr->state) & BM_DIRTY) { if (BufferIsLocal(buf)) FlushLocalBuffer(buf_hdr, NULL); else FlushOneBuffer(buf); } - LockBuffer(buf, BUFFER_LOCK_UNLOCK); - ReleaseBuffer(buf); + UnlockReleaseBuffer(buf); if (BufferIsLocal(buf)) InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true); @@ -500,11 +518,74 @@ invalidate_rel_block(PG_FUNCTION_ARGS) } } +} + +PG_FUNCTION_INFO_V1(invalidate_rel_block); +Datum +invalidate_rel_block(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + BlockNumber blkno = PG_GETARG_UINT32(1); + Relation rel; + + rel = relation_open(relid, AccessExclusiveLock); + + invalidate_one_block(rel, MAIN_FORKNUM, blkno); + relation_close(rel, AccessExclusiveLock); PG_RETURN_VOID(); } +PG_FUNCTION_INFO_V1(evict_rel); +Datum +evict_rel(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + Relation rel; + + rel = relation_open(relid, AccessExclusiveLock); + + /* + * EvictRelUnpinnedBuffers() doesn't support temp tables, so for temp + * tables we have to do it the expensive way and evict every possible + * buffer. + */ + if (RelationUsesLocalBuffers(rel)) + { + SMgrRelation smgr = RelationGetSmgr(rel); + + for (int forknum = MAIN_FORKNUM; forknum <= MAX_FORKNUM; forknum++) + { + BlockNumber nblocks; + + if (!smgrexists(smgr, forknum)) + continue; + + nblocks = smgrnblocks(smgr, forknum); + + for (int blkno = 0; blkno < nblocks; blkno++) + { + invalidate_one_block(rel, forknum, blkno); + } + } + } + else + { + int32 buffers_evicted, + buffers_flushed, + buffers_skipped; + + EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed, + &buffers_skipped); + } + + relation_close(rel, AccessExclusiveLock); + + + PG_RETURN_VOID(); +} + PG_FUNCTION_INFO_V1(buffer_create_toy); Datum buffer_create_toy(PG_FUNCTION_ARGS) @@ -530,15 +611,18 @@ buffer_call_start_io(PG_FUNCTION_ARGS) { Buffer buf = PG_GETARG_INT32(0); bool for_input = PG_GETARG_BOOL(1); - bool nowait = PG_GETARG_BOOL(2); + bool wait = PG_GETARG_BOOL(2); + StartBufferIOResult result; bool can_start; if (BufferIsLocal(buf)) - can_start = StartLocalBufferIO(GetLocalBufferDescriptor(-buf - 1), - for_input, nowait); + result = StartLocalBufferIO(GetLocalBufferDescriptor(-buf - 1), + for_input, wait, NULL); else - can_start = StartBufferIO(GetBufferDescriptor(buf - 1), - for_input, nowait); + result = StartSharedBufferIO(GetBufferDescriptor(buf - 1), + for_input, wait, NULL); + + can_start = result == BUFFER_IO_READY_FOR_IO; /* * For tests we don't want the resowner release preventing us from @@ -566,7 +650,7 @@ buffer_call_terminate_io(PG_FUNCTION_ARGS) bool io_error = PG_GETARG_BOOL(3); bool release_aio = PG_GETARG_BOOL(4); bool clear_dirty = false; - uint32 set_flag_bits = 0; + uint64 set_flag_bits = 0; if (io_error) set_flag_bits |= BM_IO_ERROR; @@ -604,6 +688,234 @@ buffer_call_terminate_io(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +PG_FUNCTION_INFO_V1(read_buffers); +/* + * Infrastructure to test StartReadBuffers() + */ +Datum +read_buffers(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + BlockNumber startblock = PG_GETARG_UINT32(1); + int32 nblocks = PG_GETARG_INT32(2); + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + Relation rel; + SMgrRelation smgr; + int nblocks_done = 0; + int nblocks_disp = 0; + int nios = 0; + ReadBuffersOperation *operations; + Buffer *buffers; + Datum *buffers_datum; + bool *io_reqds; + int *nblocks_per_io; + + Assert(nblocks > 0); + + InitMaterializedSRF(fcinfo, 0); + + /* at worst each block gets its own IO */ + operations = palloc0(sizeof(ReadBuffersOperation) * nblocks); + buffers = palloc0(sizeof(Buffer) * nblocks); + buffers_datum = palloc0(sizeof(Datum) * nblocks); + io_reqds = palloc0(sizeof(bool) * nblocks); + nblocks_per_io = palloc0(sizeof(int) * nblocks); + + rel = relation_open(relid, AccessShareLock); + smgr = RelationGetSmgr(rel); + + /* + * Do StartReadBuffers() until IO for all the required blocks has been + * started (if required). + */ + while (nblocks_done < nblocks) + { + ReadBuffersOperation *operation = &operations[nios]; + int nblocks_this_io = + Min(nblocks - nblocks_done, io_combine_limit); + + operation->rel = rel; + operation->smgr = smgr; + operation->persistence = rel->rd_rel->relpersistence; + operation->strategy = NULL; + operation->forknum = MAIN_FORKNUM; + + io_reqds[nios] = StartReadBuffers(operation, + &buffers[nblocks_done], + startblock + nblocks_done, + &nblocks_this_io, + 0); + nblocks_per_io[nios] = nblocks_this_io; + nios++; + nblocks_done += nblocks_this_io; + } + + /* + * Now wait for all operations that required IO. This is done at the end, + * as otherwise waiting for IO in progress in other backends could + * influence the result for subsequent buffers / blocks. + */ + for (int nio = 0; nio < nios; nio++) + { + ReadBuffersOperation *operation = &operations[nio]; + + if (io_reqds[nio]) + WaitReadBuffers(operation); + } + + /* + * Convert what has been done into SQL SRF return value. + */ + for (int nio = 0; nio < nios; nio++) + { + ReadBuffersOperation *operation = &operations[nio]; + int nblocks_this_io = nblocks_per_io[nio]; + Datum values[6] = {0}; + bool nulls[6] = {0}; + ArrayType *buffers_arr; + + /* convert buffer array to datum array */ + for (int i = 0; i < nblocks_this_io; i++) + { + Buffer buf = buffers[nblocks_disp + i]; + + Assert(BufferGetBlockNumber(buf) == startblock + nblocks_disp + i); + + buffers_datum[nblocks_disp + i] = Int32GetDatum(buf); + } + + buffers_arr = construct_array_builtin(&buffers_datum[nblocks_disp], + nblocks_this_io, + INT4OID); + + /* blockoff */ + values[0] = Int32GetDatum(nblocks_disp); + nulls[0] = false; + + /* blocknum */ + values[1] = UInt32GetDatum(startblock + nblocks_disp); + nulls[1] = false; + + /* io_reqd */ + values[2] = BoolGetDatum(io_reqds[nio]); + nulls[2] = false; + + /* foreign IO - only valid when IO was required */ + values[3] = BoolGetDatum(io_reqds[nio] ? operation->foreign_io : false); + nulls[3] = false; + + /* nblocks */ + values[4] = Int32GetDatum(nblocks_this_io); + nulls[4] = false; + + /* array of buffers */ + values[5] = PointerGetDatum(buffers_arr); + nulls[5] = false; + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + + nblocks_disp += nblocks_this_io; + } + + /* release pins on all the buffers */ + for (int i = 0; i < nblocks_done; i++) + ReleaseBuffer(buffers[i]); + + /* + * Free explicitly, to have a chance to detect potential issues with too + * long lived references to the operation. + */ + pfree(operations); + pfree(buffers); + pfree(buffers_datum); + pfree(io_reqds); + pfree(nblocks_per_io); + + relation_close(rel, NoLock); + + return (Datum) 0; +} + + +static BlockNumber +read_stream_for_blocks_cb(ReadStream *stream, + void *callback_private_data, + void *per_buffer_data) +{ + BlocksReadStreamData *stream_data = callback_private_data; + + if (stream_data->curblock >= stream_data->nblocks) + return InvalidBlockNumber; + return stream_data->blocks[stream_data->curblock++]; +} + +PG_FUNCTION_INFO_V1(read_stream_for_blocks); +Datum +read_stream_for_blocks(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + ArrayType *blocksarray = PG_GETARG_ARRAYTYPE_P(1); + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + Relation rel; + BlocksReadStreamData stream_data; + ReadStream *stream; + + InitMaterializedSRF(fcinfo, 0); + + /* + * We expect the input to be an N-element int4 array; verify that. We + * don't need to use deconstruct_array() since the array data is just + * going to look like a C array of N int4 values. + */ + if (ARR_NDIM(blocksarray) != 1 || + ARR_HASNULL(blocksarray) || + ARR_ELEMTYPE(blocksarray) != INT4OID) + elog(ERROR, "expected 1 dimensional int4 array"); + + stream_data.curblock = 0; + stream_data.nblocks = ARR_DIMS(blocksarray)[0]; + stream_data.blocks = (uint32 *) ARR_DATA_PTR(blocksarray); + + rel = relation_open(relid, AccessShareLock); + + stream = read_stream_begin_relation(READ_STREAM_FULL, + NULL, + rel, + MAIN_FORKNUM, + read_stream_for_blocks_cb, + &stream_data, + 0); + + for (int i = 0; i < stream_data.nblocks; i++) + { + Buffer buf = read_stream_next_buffer(stream, NULL); + Datum values[3] = {0}; + bool nulls[3] = {0}; + + if (!BufferIsValid(buf)) + elog(ERROR, "read_stream_next_buffer() call %d is unexpectedly invalid", i); + + values[0] = Int32GetDatum(i); + values[1] = UInt32GetDatum(stream_data.blocks[i]); + values[2] = UInt32GetDatum(buf); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); + + ReleaseBuffer(buf); + } + + if (read_stream_next_buffer(stream, NULL) != InvalidBuffer) + elog(ERROR, "read_stream_next_buffer() call %d is unexpectedly valid", + stream_data.nblocks); + + read_stream_end(stream); + + relation_close(rel, NoLock); + + return (Datum) 0; +} + + PG_FUNCTION_INFO_V1(handle_get); Datum handle_get(PG_FUNCTION_ARGS) @@ -674,75 +986,169 @@ batch_end(PG_FUNCTION_ARGS) } #ifdef USE_INJECTION_POINTS -extern PGDLLEXPORT void inj_io_short_read(const char *name, - const void *private_data, - void *arg); +extern PGDLLEXPORT void inj_io_completion_hook(const char *name, + const void *private_data, + void *arg); extern PGDLLEXPORT void inj_io_reopen(const char *name, const void *private_data, void *arg); -void -inj_io_short_read(const char *name, const void *private_data, void *arg) +static bool +inj_io_short_read_matches(PgAioHandle *ioh) +{ + PGPROC *io_proc; + int32 io_pid; + int32 inj_pid; + PgAioTargetData *td; + + if (!inj_io_error_state->enabled_short_read) + return false; + + if (!inj_io_error_state->short_read_result_set) + return false; + + io_proc = GetPGProcByNumber(pgaio_io_get_owner(ioh)); + io_pid = io_proc->pid; + inj_pid = inj_io_error_state->short_read_pid; + + if (inj_pid != InvalidPid && inj_pid != io_pid) + return false; + + td = pgaio_io_get_target_data(ioh); + + if (inj_io_error_state->short_read_relfilenode != InvalidOid && + td->smgr.rlocator.relNumber != inj_io_error_state->short_read_relfilenode) + return false; + + /* + * Only shorten reads that are actually longer than the target size, + * otherwise we can trigger over-reads. + */ + if (inj_io_error_state->short_read_result >= ioh->result) + return false; + + return true; +} + +static bool +inj_io_completion_wait_matches(PgAioHandle *ioh) +{ + PGPROC *io_proc; + int32 io_pid; + PgAioTargetData *td; + int32 inj_pid; + BlockNumber io_blockno; + BlockNumber inj_blockno; + Oid inj_relfilenode; + + if (!inj_io_error_state->enabled_completion_wait) + return false; + + io_proc = GetPGProcByNumber(pgaio_io_get_owner(ioh)); + io_pid = io_proc->pid; + inj_pid = inj_io_error_state->completion_wait_pid; + + if (inj_pid != InvalidPid && inj_pid != io_pid) + return false; + + td = pgaio_io_get_target_data(ioh); + + inj_relfilenode = inj_io_error_state->completion_wait_relfilenode; + if (inj_relfilenode != InvalidOid && + td->smgr.rlocator.relNumber != inj_relfilenode) + return false; + + inj_blockno = inj_io_error_state->completion_wait_blockno; + io_blockno = td->smgr.blockNum; + if (inj_blockno != InvalidBlockNumber && + !(inj_blockno >= io_blockno && inj_blockno < (io_blockno + td->smgr.nblocks))) + return false; + + return true; +} + +static void +inj_io_completion_wait_hook(const char *name, const void *private_data, void *arg) +{ + PgAioHandle *ioh = (PgAioHandle *) arg; + + if (!inj_io_completion_wait_matches(ioh)) + return; + + ConditionVariablePrepareToSleep(&inj_io_error_state->cv); + + while (true) + { + if (!inj_io_completion_wait_matches(ioh)) + break; + + ConditionVariableSleep(&inj_io_error_state->cv, + inj_io_error_state->completion_wait_event); + } + + ConditionVariableCancelSleep(); +} + +static void +inj_io_short_read_hook(const char *name, const void *private_data, void *arg) { PgAioHandle *ioh = (PgAioHandle *) arg; ereport(LOG, errmsg("short read injection point called, is enabled: %d", - inj_io_error_state->enabled_reopen), + inj_io_error_state->enabled_short_read), errhidestmt(true), errhidecontext(true)); - if (inj_io_error_state->enabled_short_read) + if (inj_io_short_read_matches(ioh)) { + struct iovec *iov = &pgaio_ctl->iovecs[ioh->iovec_off]; + int32 old_result = ioh->result; + int32 new_result = inj_io_error_state->short_read_result; + int32 processed = 0; + + ereport(LOG, + errmsg("short read inject point, changing result from %d to %d", + old_result, new_result), + errhidestmt(true), errhidecontext(true)); + /* - * Only shorten reads that are actually longer than the target size, - * otherwise we can trigger over-reads. + * The underlying IO actually completed OK, and thus the "invalid" + * portion of the IOV actually contains valid data. That can hide a + * lot of problems, e.g. if we were to wrongly mark a buffer, that + * wasn't read according to the shortened-read, IO as valid, the + * contents would look valid and we might miss a bug. + * + * To avoid that, iterate through the IOV and zero out the "failed" + * portion of the IO. */ - if (inj_io_error_state->short_read_result_set - && ioh->op == PGAIO_OP_READV - && inj_io_error_state->short_read_result <= ioh->result) + for (int i = 0; i < ioh->op_data.read.iov_length; i++) { - struct iovec *iov = &pgaio_ctl->iovecs[ioh->iovec_off]; - int32 old_result = ioh->result; - int32 new_result = inj_io_error_state->short_read_result; - int32 processed = 0; - - ereport(LOG, - errmsg("short read inject point, changing result from %d to %d", - old_result, new_result), - errhidestmt(true), errhidecontext(true)); - - /* - * The underlying IO actually completed OK, and thus the "invalid" - * portion of the IOV actually contains valid data. That can hide - * a lot of problems, e.g. if we were to wrongly mark a buffer, - * that wasn't read according to the shortened-read, IO as valid, - * the contents would look valid and we might miss a bug. - * - * To avoid that, iterate through the IOV and zero out the - * "failed" portion of the IO. - */ - for (int i = 0; i < ioh->op_data.read.iov_length; i++) + if (processed + iov[i].iov_len <= new_result) + processed += iov[i].iov_len; + else if (processed <= new_result) { - if (processed + iov[i].iov_len <= new_result) - processed += iov[i].iov_len; - else if (processed <= new_result) - { - uint32 ok_part = new_result - processed; - - memset((char *) iov[i].iov_base + ok_part, 0, iov[i].iov_len - ok_part); - processed += iov[i].iov_len; - } - else - { - memset((char *) iov[i].iov_base, 0, iov[i].iov_len); - } - } + uint32 ok_part = new_result - processed; - ioh->result = new_result; + memset((char *) iov[i].iov_base + ok_part, 0, iov[i].iov_len - ok_part); + processed += iov[i].iov_len; + } + else + { + memset((char *) iov[i].iov_base, 0, iov[i].iov_len); + } } + + ioh->result = new_result; } } +void +inj_io_completion_hook(const char *name, const void *private_data, void *arg) +{ + inj_io_completion_wait_hook(name, private_data, arg); + inj_io_short_read_hook(name, private_data, arg); +} + void inj_io_reopen(const char *name, const void *private_data, void *arg) { @@ -756,6 +1162,42 @@ inj_io_reopen(const char *name, const void *private_data, void *arg) } #endif +PG_FUNCTION_INFO_V1(inj_io_completion_wait); +Datum +inj_io_completion_wait(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + inj_io_error_state->enabled_completion_wait = true; + inj_io_error_state->completion_wait_pid = + PG_ARGISNULL(0) ? InvalidPid : PG_GETARG_INT32(0); + inj_io_error_state->completion_wait_relfilenode = + PG_ARGISNULL(1) ? InvalidOid : PG_GETARG_OID(1); + inj_io_error_state->completion_wait_blockno = + PG_ARGISNULL(2) ? InvalidBlockNumber : PG_GETARG_UINT32(2); +#else + elog(ERROR, "injection points not supported"); +#endif + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(inj_io_completion_continue); +Datum +inj_io_completion_continue(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + inj_io_error_state->enabled_completion_wait = false; + inj_io_error_state->completion_wait_pid = InvalidPid; + inj_io_error_state->completion_wait_relfilenode = InvalidOid; + inj_io_error_state->completion_wait_blockno = InvalidBlockNumber; + ConditionVariableBroadcast(&inj_io_error_state->cv); +#else + elog(ERROR, "injection points not supported"); +#endif + + PG_RETURN_VOID(); +} + PG_FUNCTION_INFO_V1(inj_io_short_read_attach); Datum inj_io_short_read_attach(PG_FUNCTION_ARGS) @@ -765,6 +1207,10 @@ inj_io_short_read_attach(PG_FUNCTION_ARGS) inj_io_error_state->short_read_result_set = !PG_ARGISNULL(0); if (inj_io_error_state->short_read_result_set) inj_io_error_state->short_read_result = PG_GETARG_INT32(0); + inj_io_error_state->short_read_pid = + PG_ARGISNULL(1) ? InvalidPid : PG_GETARG_INT32(1); + inj_io_error_state->short_read_relfilenode = + PG_ARGISNULL(2) ? InvalidOid : PG_GETARG_OID(2); #else elog(ERROR, "injection points not supported"); #endif diff --git a/src/test/modules/test_autovacuum/.gitignore b/src/test/modules/test_autovacuum/.gitignore new file mode 100644 index 0000000000000..716e17f5a2ad4 --- /dev/null +++ b/src/test/modules/test_autovacuum/.gitignore @@ -0,0 +1,2 @@ +# Generated subdirectories +/tmp_check/ diff --git a/src/test/modules/test_autovacuum/Makefile b/src/test/modules/test_autovacuum/Makefile new file mode 100644 index 0000000000000..15e83010c1cad --- /dev/null +++ b/src/test/modules/test_autovacuum/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/test_autovacuum/Makefile + +PGFILEDESC = "test_autovacuum - test code for autovacuum" + +TAP_TESTS = 1 + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_autovacuum +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build new file mode 100644 index 0000000000000..86e392bc0de5b --- /dev/null +++ b/src/test/modules/test_autovacuum/meson.build @@ -0,0 +1,15 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +tests += { + 'name': 'test_autovacuum', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'env': { + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, + 'tests': [ + 't/001_parallel_autovacuum.pl', + ], + }, +} diff --git a/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl new file mode 100644 index 0000000000000..22f40cb1d50d7 --- /dev/null +++ b/src/test/modules/test_autovacuum/t/001_parallel_autovacuum.pl @@ -0,0 +1,171 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test parallel autovacuum behavior + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# Before each test we should disable autovacuum for 'test_autovac' table and +# generate some dead tuples in it. +sub prepare_for_next_test +{ + my ($node, $test_number) = @_; + + $node->safe_psql( + 'postgres', qq{ + ALTER TABLE test_autovac SET (autovacuum_enabled = false); + UPDATE test_autovac SET col_1 = $test_number; + }); +} + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; + +# Limit to one autovacuum worker and disable autovacuum logging globally +# (enabled only on the test table) so that log checks below match only +# activity on the expected table. +$node->append_conf( + 'postgresql.conf', qq{ +autovacuum_max_workers = 1 +autovacuum_worker_slots = 1 +autovacuum_max_parallel_workers = 2 +max_worker_processes = 10 +max_parallel_workers = 10 +log_min_messages = debug2 +autovacuum_naptime = '1s' +min_parallel_index_scan_size = 0 +log_autovacuum_min_duration = -1 +}); +$node->start; + +# Check if the extension injection_points is available, as it may be +# possible that this script is run with installcheck, where the module +# would not be installed by default. +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +# Create all functions needed for testing +$node->safe_psql( + 'postgres', qq{ + CREATE EXTENSION injection_points; +}); + +my $indexes_num = 3; +my $initial_rows_num = 10_000; +my $autovacuum_parallel_workers = 2; + +# Create table and fill it with some data +$node->safe_psql( + 'postgres', qq{ + CREATE TABLE test_autovac ( + id SERIAL PRIMARY KEY, + col_1 INTEGER, col_2 INTEGER, col_3 INTEGER, col_4 INTEGER + ) WITH (autovacuum_parallel_workers = $autovacuum_parallel_workers, + log_autovacuum_min_duration = 0); + + INSERT INTO test_autovac + SELECT + g AS col1, + g + 1 AS col2, + g + 2 AS col3, + g + 3 AS col4 + FROM generate_series(1, $initial_rows_num) AS g; +}); + +# Create specified number of b-tree indexes on the table +$node->safe_psql( + 'postgres', qq{ + DO \$\$ + DECLARE + i INTEGER; + BEGIN + FOR i IN 1..$indexes_num LOOP + EXECUTE format('CREATE INDEX idx_col_\%s ON test_autovac (col_\%s);', i, i); + END LOOP; + END \$\$; +}); + +# Test 1 : +# Our table has enough indexes and appropriate reloptions, so autovacuum must +# be able to process it in parallel mode. Just check if it can do it. + +prepare_for_next_test($node, 1); +my $log_offset = -s $node->logfile; + +$node->safe_psql( + 'postgres', qq{ + ALTER TABLE test_autovac SET (autovacuum_enabled = true); +}); + +# Wait for parallel autovacuum to complete; check worker count matches reloptions. +$node->wait_for_log( + qr/parallel workers: index vacuum: 2 planned, 2 launched in total/, + $log_offset); +ok(1, "parallel autovacuum on test_autovac table"); + +# Test 2: +# Check whether parallel autovacuum leader can propagate cost-based parameters +# to the parallel workers. + +prepare_for_next_test($node, 2); +$log_offset = -s $node->logfile; + +$node->safe_psql( + 'postgres', qq{ + SELECT injection_points_attach('autovacuum-start-parallel-vacuum', 'wait'); + + ALTER TABLE test_autovac SET (autovacuum_parallel_workers = 1, autovacuum_enabled = true); +}); + +# Wait until parallel autovacuum is inited +$node->wait_for_event('autovacuum worker', + 'autovacuum-start-parallel-vacuum'); + +# Update the shared cost-based delay parameters. +$node->safe_psql( + 'postgres', qq{ + ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 500; + ALTER SYSTEM SET autovacuum_vacuum_cost_delay = 5; + ALTER SYSTEM SET vacuum_cost_page_miss = 10; + ALTER SYSTEM SET vacuum_cost_page_dirty = 10; + ALTER SYSTEM SET vacuum_cost_page_hit = 10; + SELECT pg_reload_conf(); +}); + +# Resume the leader process to update the shared parameters during heap scan (i.e. +# vacuum_delay_point() is called) and launch a parallel vacuum worker, but it stops +# before vacuuming indexes due to the injection point. +$node->safe_psql( + 'postgres', qq{ + SELECT injection_points_wakeup('autovacuum-start-parallel-vacuum'); +}); + +# Check whether parallel worker successfully updated all parameters during +# index processing. +$node->wait_for_log( + qr/parallel autovacuum worker updated cost params: cost_limit=500, cost_delay=5, cost_page_miss=10, cost_page_dirty=10, cost_page_hit=10/, + $log_offset); + +# Cleanup +$node->safe_psql( + 'postgres', qq{ + SELECT injection_points_detach('autovacuum-start-parallel-vacuum'); +}); + +ok(1, + "vacuum delay parameter changes are propagated to parallel vacuum workers" +); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_binaryheap/.gitignore b/src/test/modules/test_binaryheap/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_binaryheap/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_binaryheap/Makefile b/src/test/modules/test_binaryheap/Makefile new file mode 100644 index 0000000000000..d310fbc9e88fb --- /dev/null +++ b/src/test/modules/test_binaryheap/Makefile @@ -0,0 +1,24 @@ +# src/test/modules/test_binaryheap/Makefile + +MODULE_big = test_binaryheap +OBJS = \ + $(WIN32RES) \ + test_binaryheap.o + +PGFILEDESC = "test_binaryheap - test code for binaryheap" + +EXTENSION = test_binaryheap +DATA = test_binaryheap--1.0.sql + +REGRESS = test_binaryheap + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_binaryheap +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_binaryheap/expected/test_binaryheap.out b/src/test/modules/test_binaryheap/expected/test_binaryheap.out new file mode 100644 index 0000000000000..16ce07875e3f7 --- /dev/null +++ b/src/test/modules/test_binaryheap/expected/test_binaryheap.out @@ -0,0 +1,12 @@ +CREATE EXTENSION test_binaryheap; +-- +-- These tests don't produce any interesting output. We're checking that +-- the operations complete without crashing or hanging and that none of their +-- internal sanity tests fail. +-- +SELECT test_binaryheap(); + test_binaryheap +----------------- + +(1 row) + diff --git a/src/test/modules/test_binaryheap/meson.build b/src/test/modules/test_binaryheap/meson.build new file mode 100644 index 0000000000000..b942347b17fa2 --- /dev/null +++ b/src/test/modules/test_binaryheap/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +test_binaryheap_sources = files( + 'test_binaryheap.c', +) + +if host_system == 'windows' + test_binaryheap_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_binaryheap', + '--FILEDESC', 'test_binaryheap - test code for binaryheap',]) +endif + +test_binaryheap = shared_module('test_binaryheap', + test_binaryheap_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_binaryheap + +test_install_data += files( + 'test_binaryheap.control', + 'test_binaryheap--1.0.sql', +) + +tests += { + 'name': 'test_binaryheap', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_binaryheap', + ], + }, +} diff --git a/src/test/modules/test_binaryheap/sql/test_binaryheap.sql b/src/test/modules/test_binaryheap/sql/test_binaryheap.sql new file mode 100644 index 0000000000000..8439545815b37 --- /dev/null +++ b/src/test/modules/test_binaryheap/sql/test_binaryheap.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION test_binaryheap; + +-- +-- These tests don't produce any interesting output. We're checking that +-- the operations complete without crashing or hanging and that none of their +-- internal sanity tests fail. +-- +SELECT test_binaryheap(); diff --git a/src/test/modules/test_binaryheap/test_binaryheap--1.0.sql b/src/test/modules/test_binaryheap/test_binaryheap--1.0.sql new file mode 100644 index 0000000000000..cddceeee60337 --- /dev/null +++ b/src/test/modules/test_binaryheap/test_binaryheap--1.0.sql @@ -0,0 +1,7 @@ +/* src/test/modules/test_binaryheap/test_binaryheap--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_binaryheap" to load this file. \quit + +CREATE FUNCTION test_binaryheap() RETURNS VOID + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_binaryheap/test_binaryheap.c b/src/test/modules/test_binaryheap/test_binaryheap.c new file mode 100644 index 0000000000000..66d4c09cd8545 --- /dev/null +++ b/src/test/modules/test_binaryheap/test_binaryheap.c @@ -0,0 +1,275 @@ +/*-------------------------------------------------------------------------- + * + * test_binaryheap.c + * Test correctness of binary heap implementation. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_binaryheap/test_binaryheap.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "common/int.h" +#include "common/pg_prng.h" +#include "fmgr.h" +#include "lib/binaryheap.h" + +PG_MODULE_MAGIC; + +/* + * Test binaryheap_comparator for max-heap of integers. + */ +static int +int_cmp(Datum a, Datum b, void *arg) +{ + return pg_cmp_s32(DatumGetInt32(a), DatumGetInt32(b)); +} + +/* + * Loops through all nodes and returns the maximum value. + */ +static int +get_max_from_heap(binaryheap *heap) +{ + int max = -1; + + for (int i = 0; i < binaryheap_size(heap); i++) + max = Max(max, DatumGetInt32(binaryheap_get_node(heap, i))); + + return max; +} + +/* + * Generate a random permutation of the integers 0..size-1. + */ +static int * +get_permutation(int size) +{ + int *permutation = (int *) palloc(size * sizeof(int)); + + permutation[0] = 0; + + /* + * This is the "inside-out" variant of the Fisher-Yates shuffle algorithm. + * Notionally, we append each new value to the array and then swap it with + * a randomly-chosen array element (possibly including itself, else we + * fail to generate permutations with the last integer last). The swap + * step can be optimized by combining it with the insertion. + */ + for (int i = 1; i < size; i++) + { + int j = pg_prng_uint64_range(&pg_global_prng_state, 0, i); + + if (j < i) /* avoid fetching undefined data if j=i */ + permutation[i] = permutation[j]; + permutation[j] = i; + } + + return permutation; +} + +/* + * Ensure that the heap property holds for the given heap, i.e., each parent is + * greater than or equal to its children. + */ +static void +verify_heap_property(binaryheap *heap) +{ + for (int i = 0; i < binaryheap_size(heap); i++) + { + int left = 2 * i + 1; + int right = 2 * i + 2; + int parent_val = DatumGetInt32(binaryheap_get_node(heap, i)); + + if (left < binaryheap_size(heap) && + parent_val < DatumGetInt32(binaryheap_get_node(heap, left))) + elog(ERROR, "parent node less than left child"); + + if (right < binaryheap_size(heap) && + parent_val < DatumGetInt32(binaryheap_get_node(heap, right))) + elog(ERROR, "parent node less than right child"); + } +} + +/* + * Check correctness of basic operations. + */ +static void +test_basic(int size) +{ + binaryheap *heap = binaryheap_allocate(size, int_cmp, NULL); + int *permutation = get_permutation(size); + + if (!binaryheap_empty(heap)) + elog(ERROR, "new heap not empty"); + if (binaryheap_size(heap) != 0) + elog(ERROR, "wrong size for new heap"); + + for (int i = 0; i < size; i++) + { + binaryheap_add(heap, Int32GetDatum(permutation[i])); + verify_heap_property(heap); + } + + if (binaryheap_empty(heap)) + elog(ERROR, "heap empty after adding values"); + if (binaryheap_size(heap) != size) + elog(ERROR, "wrong size for heap after adding values"); + + if (DatumGetInt32(binaryheap_first(heap)) != get_max_from_heap(heap)) + elog(ERROR, "incorrect root node after adding values"); + + for (int i = 0; i < size; i++) + { + int expected = get_max_from_heap(heap); + int actual = DatumGetInt32(binaryheap_remove_first(heap)); + + if (actual != expected) + elog(ERROR, "incorrect root node after removing root"); + verify_heap_property(heap); + } + + if (!binaryheap_empty(heap)) + elog(ERROR, "heap not empty after removing all nodes"); +} + +/* + * Test building heap after unordered additions. + */ +static void +test_build(int size) +{ + binaryheap *heap = binaryheap_allocate(size, int_cmp, NULL); + int *permutation = get_permutation(size); + + for (int i = 0; i < size; i++) + binaryheap_add_unordered(heap, Int32GetDatum(permutation[i])); + + if (binaryheap_size(heap) != size) + elog(ERROR, "wrong size for heap after unordered additions"); + + binaryheap_build(heap); + verify_heap_property(heap); +} + +/* + * Test removing nodes. + */ +static void +test_remove_node(int size) +{ + binaryheap *heap = binaryheap_allocate(size, int_cmp, NULL); + int *permutation = get_permutation(size); + int remove_count = pg_prng_uint64_range(&pg_global_prng_state, + 0, size - 1); + + for (int i = 0; i < size; i++) + binaryheap_add(heap, Int32GetDatum(permutation[i])); + + for (int i = 0; i < remove_count; i++) + { + int idx = pg_prng_uint64_range(&pg_global_prng_state, + 0, binaryheap_size(heap) - 1); + + binaryheap_remove_node(heap, idx); + verify_heap_property(heap); + } + + if (binaryheap_size(heap) != size - remove_count) + elog(ERROR, "wrong size after removing nodes"); +} + +/* + * Test replacing the root node. + */ +static void +test_replace_first(int size) +{ + binaryheap *heap = binaryheap_allocate(size, int_cmp, NULL); + + for (int i = 0; i < size; i++) + binaryheap_add(heap, Int32GetDatum(i)); + + /* + * Replace root with a value smaller than everything in the heap. + */ + binaryheap_replace_first(heap, Int32GetDatum(-1)); + verify_heap_property(heap); + + /* + * Replace root with a value in the middle of the heap. + */ + binaryheap_replace_first(heap, Int32GetDatum(size / 2)); + verify_heap_property(heap); + + /* + * Replace root with a larger value than everything in the heap. + */ + binaryheap_replace_first(heap, Int32GetDatum(size + 1)); + verify_heap_property(heap); +} + +/* + * Test duplicate values. + */ +static void +test_duplicates(int size) +{ + binaryheap *heap = binaryheap_allocate(size, int_cmp, NULL); + int dup = pg_prng_uint64_range(&pg_global_prng_state, 0, size - 1); + + for (int i = 0; i < size; i++) + binaryheap_add(heap, Int32GetDatum(dup)); + + for (int i = 0; i < size; i++) + { + if (DatumGetInt32(binaryheap_remove_first(heap)) != dup) + elog(ERROR, "unexpected value in heap with duplicates"); + } +} + +/* + * Test resetting. + */ +static void +test_reset(int size) +{ + binaryheap *heap = binaryheap_allocate(size, int_cmp, NULL); + + for (int i = 0; i < size; i++) + binaryheap_add(heap, Int32GetDatum(i)); + + binaryheap_reset(heap); + + if (!binaryheap_empty(heap)) + elog(ERROR, "heap not empty after resetting"); +} + +/* + * SQL-callable entry point to perform all tests. + */ +PG_FUNCTION_INFO_V1(test_binaryheap); + +Datum +test_binaryheap(PG_FUNCTION_ARGS) +{ + static const int test_sizes[] = {1, 2, 3, 10, 100, 1000}; + + for (int i = 0; i < sizeof(test_sizes) / sizeof(int); i++) + { + int size = test_sizes[i]; + + test_basic(size); + test_build(size); + test_remove_node(size); + test_replace_first(size); + test_duplicates(size); + test_reset(size); + } + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_binaryheap/test_binaryheap.control b/src/test/modules/test_binaryheap/test_binaryheap.control new file mode 100644 index 0000000000000..dd0785e05bdaf --- /dev/null +++ b/src/test/modules/test_binaryheap/test_binaryheap.control @@ -0,0 +1,5 @@ +# test_binaryheap extension +comment = 'Test code for binaryheap' +default_version = '1.0' +module_pathname = '$libdir/test_binaryheap' +relocatable = true diff --git a/src/test/modules/test_bitmapset/.gitignore b/src/test/modules/test_bitmapset/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_bitmapset/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_bitmapset/Makefile b/src/test/modules/test_bitmapset/Makefile new file mode 100644 index 0000000000000..99fb22ae8071c --- /dev/null +++ b/src/test/modules/test_bitmapset/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_bitmapset/Makefile + +MODULE_big = test_bitmapset +OBJS = \ + $(WIN32RES) \ + test_bitmapset.o +PGFILEDESC = "test_bitmapset - test code for bitmapset" + +EXTENSION = test_bitmapset +DATA = test_bitmapset--1.0.sql + +REGRESS = test_bitmapset + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_bitmapset +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_bitmapset/expected/test_bitmapset.out b/src/test/modules/test_bitmapset/expected/test_bitmapset.out new file mode 100644 index 0000000000000..0b72b91cd1f84 --- /dev/null +++ b/src/test/modules/test_bitmapset/expected/test_bitmapset.out @@ -0,0 +1,1578 @@ +-- Tests for Bitmapsets +CREATE EXTENSION test_bitmapset; +-- bms_make_singleton() +SELECT test_bms_make_singleton(-1); +ERROR: negative bitmapset member not allowed +SELECT test_bms_make_singleton(42) AS result; + result +-------- + (b 42) +(1 row) + +SELECT test_bms_make_singleton(0) AS result; + result +-------- + (b 0) +(1 row) + +SELECT test_bms_make_singleton(1000) AS result; + result +---------- + (b 1000) +(1 row) + +-- Test module check +SELECT test_bms_make_singleton(NULL) AS result; + result +-------- + +(1 row) + +-- bms_add_member() +SELECT test_bms_add_member('(b 1)', -1); -- error +ERROR: negative bitmapset member not allowed +SELECT test_bms_add_member('(b)', -10); -- error +ERROR: negative bitmapset member not allowed +SELECT test_bms_add_member('(b)', 10) AS result; + result +-------- + (b 10) +(1 row) + +SELECT test_bms_add_member('(b 5)', 10) AS result; + result +---------- + (b 5 10) +(1 row) + +-- sort check +SELECT test_bms_add_member('(b 10)', 5) AS result; + result +---------- + (b 5 10) +(1 row) + +-- idempotent change +SELECT test_bms_add_member('(b 10)', 10) AS result; + result +-------- + (b 10) +(1 row) + +-- Test module check +SELECT test_bms_add_member('(b)', NULL) AS result; + result +-------- + +(1 row) + +-- bms_replace_members() +SELECT test_bms_replace_members(NULL, '(b 1 2 3)') AS result; + result +----------- + (b 1 2 3) +(1 row) + +SELECT test_bms_replace_members('(b 1 2 3)', NULL) AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_replace_members('(b 1 2 3)', '(b 3 5 6)') AS result; + result +----------- + (b 3 5 6) +(1 row) + +SELECT test_bms_replace_members('(b 1 2 3)', '(b 3 5)') AS result; + result +--------- + (b 3 5) +(1 row) + +SELECT test_bms_replace_members('(b 1 2)', '(b 3 5 7)') AS result; + result +----------- + (b 3 5 7) +(1 row) + +-- Force repalloc() with larger set +SELECT test_bms_replace_members('(b 1 2 3 4 5)', '(b 500 600)') AS result; + result +------------- + (b 500 600) +(1 row) + +-- bms_del_member() +SELECT test_bms_del_member('(b)', -20); -- error +ERROR: negative bitmapset member not allowed +SELECT test_bms_del_member('(b)', 10) AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_del_member('(b 10)', 10) AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_del_member('(b 10)', 5) AS result; + result +-------- + (b 10) +(1 row) + +SELECT test_bms_del_member('(b 1 2 3)', 2) AS result; + result +--------- + (b 1 3) +(1 row) + +-- Reallocation check +SELECT test_bms_del_member(test_bms_del_member('(b 0 31 32 63 64)', 32), 63) AS result; + result +------------- + (b 0 31 64) +(1 row) + +-- Word boundary +SELECT test_bms_del_member(test_bms_add_range('(b)', 30, 34), 32) AS result; + result +----------------- + (b 30 31 33 34) +(1 row) + +-- Force word count changes +SELECT test_bms_del_member('(b 1 200)', 200) AS result; + result +-------- + (b 1) +(1 row) + +SELECT test_bms_del_member('(b 1 50 100 200)', 200) AS result; + result +-------------- + (b 1 50 100) +(1 row) + +SELECT test_bms_del_member('(b 1 50 100 200)', 100) AS result; + result +-------------- + (b 1 50 200) +(1 row) + +-- Test module checks +SELECT test_bms_del_member('(b 5)', NULL) AS result; + result +-------- + +(1 row) + +-- bms_del_members() +SELECT test_bms_del_members('(b)', '(b 10)') AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_del_members('(b 10)', '(b 10)') AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_del_members('(b 10)', '(b 5)') AS result; + result +-------- + (b 10) +(1 row) + +SELECT test_bms_del_members('(b 1 2 3)', '(b 2)') AS result; + result +--------- + (b 1 3) +(1 row) + +SELECT test_bms_del_members('(b 5 100)', '(b 100)') AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_del_members('(b 5 100 200)', '(b 200)') AS result; + result +----------- + (b 5 100) +(1 row) + +-- Force word count changes +SELECT test_bms_del_members('(b 1 2 100 200 300)', '(b 1 2)') AS result; + result +----------------- + (b 100 200 300) +(1 row) + +SELECT test_bms_del_members('(b 1 2 100 200 300)', '(b 200 300)') AS result; + result +------------- + (b 1 2 100) +(1 row) + +-- NULL inputs +SELECT test_bms_del_members('(b 5)', NULL) AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_del_members(NULL, '(b 5)') AS result; + result +-------- + <> +(1 row) + +-- bms_join() +SELECT test_bms_join('(b 1 3 5)', NULL) AS result; + result +----------- + (b 1 3 5) +(1 row) + +SELECT test_bms_join(NULL, '(b 2 4 6)') AS result; + result +----------- + (b 2 4 6) +(1 row) + +SELECT test_bms_join('(b 1 3 5)', '(b 2 4 6)') AS result; + result +----------------- + (b 1 2 3 4 5 6) +(1 row) + +SELECT test_bms_join('(b 1 3 5)', '(b 1 4 5)') AS result; + result +------------- + (b 1 3 4 5) +(1 row) + +-- Force word count changes +SELECT test_bms_join('(b 5)', '(b 100)') AS result; + result +----------- + (b 5 100) +(1 row) + +SELECT test_bms_join('(b 1 2)', '(b 100 200 300)') AS result; + result +--------------------- + (b 1 2 100 200 300) +(1 row) + +-- NULL inputs +SELECT test_bms_join('(b 5)', NULL) AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_join(NULL, '(b 5)') AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_join(NULL, NULL) AS result; + result +-------- + <> +(1 row) + +-- bms_union() +-- Overlapping sets +SELECT test_bms_union('(b 1 3 5)', '(b 3 5 7)') AS result; + result +------------- + (b 1 3 5 7) +(1 row) + +-- Union with NULL +SELECT test_bms_union('(b 1 3 5)', '(b)') AS result; + result +----------- + (b 1 3 5) +(1 row) + +-- Union of empty with empty +SELECT test_bms_union('(b)', '(b)') AS result; + result +-------- + <> +(1 row) + +-- Overlapping ranges +SELECT test_bms_union( + test_bms_add_range('(b)', 0, 15), + test_bms_add_range('(b)', 10, 20) + ) AS result; + result +---------------------------------------------------------- + (b 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20) +(1 row) + +-- Union with varying word counts +SELECT test_bms_union('(b 1 2)', '(b 100 300)') AS result; + result +----------------- + (b 1 2 100 300) +(1 row) + +SELECT test_bms_union('(b 100 300)', '(b 1 2)') AS result; + result +----------------- + (b 1 2 100 300) +(1 row) + +-- NULL inputs +SELECT test_bms_union('(b 5)', NULL) AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_union(NULL, '(b 5)') AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_union(NULL, NULL) AS result; + result +-------- + <> +(1 row) + +-- bms_intersect() +-- Overlapping sets +SELECT test_bms_intersect('(b 1 3 5)', '(b 3 5 7)') AS result; + result +--------- + (b 3 5) +(1 row) + +-- Disjoint sets +SELECT test_bms_intersect('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + <> +(1 row) + +-- Intersect with empty +SELECT test_bms_intersect('(b 1 3 5)', '(b)') AS result; + result +-------- + <> +(1 row) + +-- Intersect with varying word counts +SELECT test_bms_intersect('(b 1 300)', '(b 1 2 3 4 5)') AS result; + result +-------- + (b 1) +(1 row) + +SELECT test_bms_intersect('(b 1 2 3 4 5)', '(b 1 300)') AS result; + result +-------- + (b 1) +(1 row) + +-- NULL inputs +SELECT test_bms_intersect('(b 5)', NULL) AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_intersect(NULL, '(b 5)') AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_intersect(NULL, NULL) AS result; + result +-------- + <> +(1 row) + +-- bms_int_members() +-- Overlapping sets +SELECT test_bms_int_members('(b 1 3 5)', '(b 3 5 7)') AS result; + result +--------- + (b 3 5) +(1 row) + +-- Disjoint sets +SELECT test_bms_int_members('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + <> +(1 row) + +-- Intersect with empty +SELECT test_bms_int_members('(b 1 3 5)', '(b)') AS result; + result +-------- + <> +(1 row) + +-- Multiple members +SELECT test_bms_int_members('(b 0 31 32 63 64)', '(b 31 32 64 65)') AS result; + result +-------------- + (b 31 32 64) +(1 row) + +-- NULL inputs +SELECT test_bms_int_members('(b 5)', NULL) AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_int_members(NULL, '(b 5)') AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_int_members(NULL, NULL) AS result; + result +-------- + <> +(1 row) + +-- bms_difference() +-- Overlapping sets +SELECT test_bms_difference('(b 1 3 5)', '(b 3 5 7)') AS result; + result +-------- + (b 1) +(1 row) + +-- Disjoint sets +SELECT test_bms_difference('(b 1 3 5)', '(b 2 4 6)') AS result; + result +----------- + (b 1 3 5) +(1 row) + +-- Identical sets +SELECT test_bms_difference('(b 1 3 5)', '(b 1 3 5)') AS result; + result +-------- + <> +(1 row) + +-- Subtraction to empty +SELECT test_bms_difference('(b 42)', '(b 42)') AS result; + result +-------- + <> +(1 row) + +-- Subtraction edge case +SELECT test_bms_difference( + test_bms_add_range('(b)', 0, 100), + test_bms_add_range('(b)', 50, 150) + ) AS result; + result +------------------------------------------------------------------------------------------------------------------------------------------------- + (b 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49) +(1 row) + +-- Difference with different word counts +SELECT test_bms_difference('(b 5 100)', '(b 5)') AS result; + result +--------- + (b 100) +(1 row) + +SELECT test_bms_difference('(b 1 2 100 200)', '(b 1 2)') AS result; + result +------------- + (b 100 200) +(1 row) + +-- NULL inputs +SELECT test_bms_difference('(b 5)', NULL) AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_difference(NULL, '(b 5)') AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_difference(NULL, NULL) AS result; + result +-------- + <> +(1 row) + +-- bms_is_member() +SELECT test_bms_is_member('(b)', -5); -- error +ERROR: negative bitmapset member not allowed +SELECT test_bms_is_member('(b 1 3 5)', 1) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_member('(b 1 3 5)', 2) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_is_member('(b 1 3 5)', 3) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_member('(b)', 1) AS result; + result +-------- + f +(1 row) + +-- Test module check +SELECT test_bms_is_member('(b 5)', NULL) AS result; + result +-------- + +(1 row) + +-- bms_member_index() +SELECT test_bms_member_index(NULL, 1) AS result; + result +-------- + -1 +(1 row) + +SELECT test_bms_member_index('(b 1 3 5)', 2) AS result; + result +-------- + -1 +(1 row) + +SELECT test_bms_member_index('(b 1 3 5)', 1) AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_member_index('(b 1 3 5)', 3) AS result; + result +-------- + 1 +(1 row) + +-- Member index with various word positions +SELECT test_bms_member_index('(b 100 200)', 100) AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_member_index('(b 100 200)', 200) AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_member_index('(b 1 50 100 200)', 200) AS result; + result +-------- + 3 +(1 row) + +-- Test module check +SELECT test_bms_member_index('(b 1 3 5)', NULL) AS result; + result +-------- + +(1 row) + +-- bms_num_members() +SELECT test_bms_num_members('(b)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_num_members('(b 1 3 5)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_num_members('(b 2 4 6 8 10)') AS result; + result +-------- + 5 +(1 row) + +-- test_bms_equal() +SELECT test_bms_equal('(b)', '(b)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_equal('(b)', '(b 1 3 5)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_equal('(b 1 3 5)', '(b)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_equal('(b 1 3 5)', '(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_equal('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + f +(1 row) + +-- Equal with different word counts +SELECT test_bms_equal('(b 5)', '(b 100)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_equal('(b 5 10)', '(b 100 200 300)') AS result; + result +-------- + f +(1 row) + +-- NULL inputs +SELECT test_bms_equal('(b 5)', NULL) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_equal(NULL, '(b 5)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_equal(NULL, NULL) AS result; + result +-------- + t +(1 row) + +-- bms_compare() +SELECT test_bms_compare('(b)', '(b)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_compare('(b)', '(b 1 3)') AS result; + result +-------- + -1 +(1 row) + +SELECT test_bms_compare('(b 1 3)', '(b)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_compare('(b 1 3)', '(b 1 3)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_compare('(b 1 3)', '(b 1 3 5)') AS result; + result +-------- + -1 +(1 row) + +SELECT test_bms_compare('(b 1 3 5)', '(b 1 3)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_compare( + test_bms_add_range('(b)', 0, 63), + test_bms_add_range('(b)', 0, 64) + ) AS result; + result +-------- + -1 +(1 row) + +-- NULL inputs +SELECT test_bms_compare('(b 5)', NULL) AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_compare(NULL, '(b 5)') AS result; + result +-------- + -1 +(1 row) + +SELECT test_bms_compare(NULL, NULL) AS result; + result +-------- + 0 +(1 row) + +-- bms_add_range() +SELECT test_bms_add_range('(b)', -5, 10); -- error +ERROR: negative bitmapset member not allowed +SELECT test_bms_add_range('(b)', 5, 7) AS result; + result +----------- + (b 5 6 7) +(1 row) + +SELECT test_bms_add_range('(b)', 5, 5) AS result; + result +-------- + (b 5) +(1 row) + +SELECT test_bms_add_range('(b 1 10)', 5, 7) AS result; + result +---------------- + (b 1 5 6 7 10) +(1 row) + +-- Word boundary of 31 +SELECT test_bms_add_range('(b)', 30, 34) AS result; + result +-------------------- + (b 30 31 32 33 34) +(1 row) + +-- Word boundary of 63 +SELECT test_bms_add_range('(b)', 62, 66) AS result; + result +-------------------- + (b 62 63 64 65 66) +(1 row) + +-- Large range +SELECT length(test_bms_add_range('(b)', 0, 1000)) AS result; + result +-------- + 3898 +(1 row) + +-- Force reallocations +SELECT length(test_bms_add_range('(b)', 0, 200)) AS result; + result +-------- + 697 +(1 row) + +SELECT length(test_bms_add_range('(b)', 1000, 1100)) AS result; + result +-------- + 508 +(1 row) + +-- Force word count expansion +SELECT test_bms_add_range('(b 5)', 100, 105) AS result; + result +------------------------------- + (b 5 100 101 102 103 104 105) +(1 row) + +SELECT length(test_bms_add_range('(b 1 2)', 200, 250)) AS result; + result +-------- + 211 +(1 row) + +-- Test module checks +SELECT test_bms_add_range('(b 5)', 5, NULL) AS result; + result +-------- + +(1 row) + +SELECT test_bms_add_range('(b 5)', NULL, 10) AS result; + result +-------- + +(1 row) + +SELECT test_bms_add_range('(b 5)', NULL, NULL) AS result; + result +-------- + +(1 row) + +-- NULL inputs +SELECT test_bms_add_range(NULL, 5, 10) AS result; + result +------------------ + (b 5 6 7 8 9 10) +(1 row) + +SELECT test_bms_add_range(NULL, 10, 5) AS result; + result +-------- + <> +(1 row) + +-- bms_membership() +SELECT test_bms_membership('(b)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_membership('(b 42)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_membership('(b 1 2)') AS result; + result +-------- + 2 +(1 row) + +-- NULL input +SELECT test_bms_membership(NULL) AS result; + result +-------- + 0 +(1 row) + +-- bms_is_empty() +SELECT test_bms_is_empty(NULL) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_empty('(b)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_empty('(b 1)') AS result; + result +-------- + f +(1 row) + +-- bms_singleton_member() +SELECT test_bms_singleton_member('(b)'); -- error +ERROR: bitmapset is empty +SELECT test_bms_singleton_member('(b 1 2)'); -- error +ERROR: bitmapset has multiple members +SELECT test_bms_singleton_member('(b 42)') AS result; + result +-------- + 42 +(1 row) + +-- NULL input +SELECT test_bms_singleton_member(NULL) AS result; + result +-------- + +(1 row) + +-- bms_get_singleton_member() +SELECT test_bms_get_singleton_member('(b)'); + test_bms_get_singleton_member +------------------------------- + -1 +(1 row) + +-- Try with an empty set +SELECT test_bms_get_singleton_member(NULL) AS result; + result +-------- + -1 +(1 row) + +-- Not a singleton, returns input default +SELECT test_bms_get_singleton_member('(b 3 6)') AS result; + result +-------- + -1 +(1 row) + +-- Singletone, returns sole member +SELECT test_bms_get_singleton_member('(b 400)') AS result; + result +-------- + 400 +(1 row) + +-- bms_next_member() and bms_prev_member() +-- First member +SELECT test_bms_next_member('(b 5 10 15 20)', -1) AS result; + result +-------- + 5 +(1 row) + +-- Second member +SELECT test_bms_next_member('(b 5 10 15 20)', 5) AS result; + result +-------- + 10 +(1 row) + +-- Member past the end +SELECT test_bms_next_member('(b 5 10 15 20)', 20) AS result; + result +-------- + -2 +(1 row) + +-- Empty set +SELECT test_bms_next_member('(b)', -1) AS result; + result +-------- + -2 +(1 row) + +-- Last member +SELECT test_bms_prev_member('(b 5 10 15 20)', 21) AS result; + result +-------- + 20 +(1 row) + +-- Penultimate member +SELECT test_bms_prev_member('(b 5 10 15 20)', 20) AS result; + result +-------- + 15 +(1 row) + +-- Past beginning member +SELECT test_bms_prev_member('(b 5 10 15 20)', 5) AS result; + result +-------- + -2 +(1 row) + +-- Empty set +SELECT test_bms_prev_member('(b)', 100) AS result; + result +-------- + -2 +(1 row) + +-- Negative prevbit should result in highest possible bit in set +SELECT test_bms_prev_member('(b 0 63 64 127)', -1) AS result; + result +-------- + 127 +(1 row) + +-- NULL inputs +SELECT test_bms_next_member(NULL, 5) AS result; + result +-------- + -2 +(1 row) + +SELECT test_bms_prev_member(NULL, 5) AS result; + result +-------- + -2 +(1 row) + +-- Test module check +SELECT test_bms_next_member('(b 5 10)', NULL) AS result; + result +-------- + +(1 row) + +SELECT test_bms_prev_member('(b 5 10)', NULL) AS result; + result +-------- + +(1 row) + +-- bms_hash_value() +SELECT test_bms_hash_value('(b)') = 0 AS result; + result +-------- + t +(1 row) + +SELECT test_bms_hash_value('(b 1 3 5)') = test_bms_hash_value('(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_hash_value('(b 1 3 5)') != test_bms_hash_value('(b 2 4 6)') AS result; + result +-------- + t +(1 row) + +-- NULL input +SELECT test_bms_hash_value(NULL) AS result; + result +-------- + 0 +(1 row) + +-- bms_overlap() +SELECT test_bms_overlap('(b 1 3 5)', '(b 3 5 7)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_overlap('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_overlap('(b)', '(b 1 3 5)') AS result; + result +-------- + f +(1 row) + +-- NULL inputs +SELECT test_bms_overlap('(b 5)', NULL) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_overlap(NULL, '(b 5)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_overlap(NULL, NULL) AS result; + result +-------- + f +(1 row) + +-- bms_is_subset() +SELECT test_bms_is_subset('(b)', '(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_subset('(b 1 3)', '(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_subset('(b 1 3 5)', '(b 1 3)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_is_subset('(b 1 3)', '(b 2 4)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_is_subset(test_bms_add_range(NULL, 0, 31), + test_bms_add_range(NULL, 0, 63)) AS result; + result +-------- + t +(1 row) + +-- Is subset with shorter word counts? +SELECT test_bms_is_subset('(b 5 100)', '(b 5)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_is_subset('(b 1 2 50 100)', '(b 1 2)') AS result; + result +-------- + f +(1 row) + +-- NULL inputs +SELECT test_bms_is_subset('(b 5)', NULL) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_is_subset(NULL, '(b 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_is_subset(NULL, NULL) AS result; + result +-------- + t +(1 row) + +-- bms_subset_compare() +SELECT test_bms_subset_compare(NULL, NULL) AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_subset_compare(NULL, '(b 1 3)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_subset_compare('(b)', '(b)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_subset_compare('(b)', '(b 1)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_subset_compare('(b 1)', '(b)') AS result; + result +-------- + 2 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3)', NULL) AS result; + result +-------- + 2 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3 5)', '(b 1 3 5)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3)', '(b 1 3 5)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3 5)', '(b 1 3)') AS result; + result +-------- + 2 +(1 row) + +SELECT test_bms_subset_compare('(b 1 2)', '(b 1 3)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 1 2)', '(b 1 4)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3)', '(b 1 3 64)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3 64)', '(b 1 3)') AS result; + result +-------- + 2 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3 64)', '(b 1 3 65)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 1 3)', '(b 2 4)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 1)', '(b 64)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 0)', '(b 32)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 0)', '(b 64)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 64)', '(b 1)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 1 2)', '(b 1 2 64)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bms_subset_compare('(b 64 200)', '(b 1 201)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 1 64 65)', '(b 1 2 64)') AS result; + result +-------- + 3 +(1 row) + +SELECT test_bms_subset_compare('(b 2 64 128)', '(b 1 65)') AS result; + result +-------- + 3 +(1 row) + +-- bms_copy() +SELECT test_bms_copy(NULL) AS result; + result +-------- + <> +(1 row) + +SELECT test_bms_copy('(b 1 3 5 7)') AS result; + result +------------- + (b 1 3 5 7) +(1 row) + +-- bms_add_members() +SELECT test_bms_add_members('(b 1 3)', '(b 5 7)') AS result; + result +------------- + (b 1 3 5 7) +(1 row) + +SELECT test_bms_add_members('(b 1 3 5)', '(b 2 5 7)') AS result; + result +--------------- + (b 1 2 3 5 7) +(1 row) + +SELECT test_bms_add_members('(b 1 3 5)', '(b 100 200 300)') AS result; + result +----------------------- + (b 1 3 5 100 200 300) +(1 row) + +-- bitmap_hash() +SELECT test_bitmap_hash('(b)') = 0 AS result; + result +-------- + t +(1 row) + +SELECT test_bitmap_hash('(b 1 3 5)') = test_bitmap_hash('(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bitmap_hash('(b 1 3 5)') = test_bms_hash_value('(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bitmap_hash('(b 1 3 5)') != test_bitmap_hash('(b 2 4 6)') AS result; + result +-------- + t +(1 row) + +-- Test module check +SELECT test_bitmap_hash(NULL) AS result; + result +-------- + 0 +(1 row) + +-- bitmap_match() +SELECT test_bitmap_match('(b)', '(b)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bitmap_match('(b)', '(b 1 3 5)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bitmap_match('(b 1 3 5)', '(b)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bitmap_match('(b 1 3 5)', '(b 1 3 5)') AS result; + result +-------- + 0 +(1 row) + +SELECT test_bitmap_match('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bitmap_match('(b 1 3)', '(b 1 3 5)') AS result; + result +-------- + 1 +(1 row) + +-- Check relationship of bitmap_match() with bms_equal() +SELECT (test_bitmap_match('(b 1 3 5)', '(b 1 3 5)') = 0) = + test_bms_equal('(b 1 3 5)', '(b 1 3 5)') AS result; + result +-------- + t +(1 row) + +SELECT (test_bitmap_match('(b 1 3 5)', '(b 2 4 6)') = 0) = + test_bms_equal('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + t +(1 row) + +SELECT (test_bitmap_match('(b)', '(b)') = 0) = + test_bms_equal('(b)', '(b)') AS result; + result +-------- + t +(1 row) + +-- NULL inputs +SELECT test_bitmap_match('(b 5)', NULL) AS result; + result +-------- + 1 +(1 row) + +SELECT test_bitmap_match(NULL, '(b 5)') AS result; + result +-------- + 1 +(1 row) + +SELECT test_bitmap_match(NULL, NULL) AS result; + result +-------- + 0 +(1 row) + +-- bms_overlap_list() +SELECT test_bms_overlap_list('(b 0)', ARRAY[0]) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_overlap_list('(b 2 3)', ARRAY[1,2]) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_overlap_list('(b 3 4)', ARRAY[3,4,5]) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_overlap_list('(b 7 10)', ARRAY[6,7,8,9]) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_overlap_list('(b 1 5)', ARRAY[6,7,8,9]) AS result; + result +-------- + f +(1 row) + +-- Empty list +SELECT test_bms_overlap_list('(b 1)', ARRAY[]::integer[]) AS result; + result +-------- + f +(1 row) + +-- Overlap list with negative numbers +SELECT test_bms_overlap_list('(b 5 10)', ARRAY[-1,5]) AS result; -- error +ERROR: negative bitmapset member not allowed +SELECT test_bms_overlap_list('(b 1 2 3)', ARRAY[-5,-1,0]) AS result; -- error +ERROR: negative bitmapset member not allowed +-- NULL inputs +SELECT test_bms_overlap_list('(b 5)', NULL) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_overlap_list(NULL, ARRAY[1,2,3]) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_overlap_list(NULL, NULL) AS result; + result +-------- + f +(1 row) + +-- bms_nonempty_difference() +SELECT test_bms_nonempty_difference(NULL, '(b 1 3 5)') AS result; + result +-------- + f +(1 row) + +SELECT test_bms_nonempty_difference('(b 1 3 5)', NULL) AS result; + result +-------- + t +(1 row) + +SELECT test_bms_nonempty_difference(NULL, NULL) AS result; + result +-------- + f +(1 row) + +SELECT test_bms_nonempty_difference('(b 1 3 5)', '(b 2 4 6)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_nonempty_difference('(b 1 3 5)', '(b 1 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_nonempty_difference('(b 1 3 5)', '(b 1 3 5)') AS result; + result +-------- + f +(1 row) + +-- Difference with different word counts +SELECT test_bms_nonempty_difference('(b 5)', '(b 100)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_nonempty_difference('(b 100)', '(b 5)') AS result; + result +-------- + t +(1 row) + +SELECT test_bms_nonempty_difference('(b 1 2)', '(b 50 100)') AS result; + result +-------- + t +(1 row) + +-- random operations +SELECT test_random_operations(NULL, 10000, 81920, 0) > 0 AS result; + result +-------- + t +(1 row) + +DROP EXTENSION test_bitmapset; diff --git a/src/test/modules/test_bitmapset/meson.build b/src/test/modules/test_bitmapset/meson.build new file mode 100644 index 0000000000000..ad9b1d9e2776d --- /dev/null +++ b/src/test/modules/test_bitmapset/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +test_bitmapset_sources = files( + 'test_bitmapset.c', +) + +if host_system == 'windows' + test_bitmapset_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_bitmapset', + '--FILEDESC', 'test_bitmapset - test code for src/include/nodes/bitmapset.h',]) +endif + +test_bitmapset = shared_module('test_bitmapset', + test_bitmapset_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_bitmapset + +test_install_data += files( + 'test_bitmapset.control', + 'test_bitmapset--1.0.sql', +) + +tests += { + 'name': 'test_bitmapset', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_bitmapset', + ], + }, +} diff --git a/src/test/modules/test_bitmapset/sql/test_bitmapset.sql b/src/test/modules/test_bitmapset/sql/test_bitmapset.sql new file mode 100644 index 0000000000000..c53232e0ada57 --- /dev/null +++ b/src/test/modules/test_bitmapset/sql/test_bitmapset.sql @@ -0,0 +1,406 @@ +-- Tests for Bitmapsets +CREATE EXTENSION test_bitmapset; + +-- bms_make_singleton() +SELECT test_bms_make_singleton(-1); +SELECT test_bms_make_singleton(42) AS result; +SELECT test_bms_make_singleton(0) AS result; +SELECT test_bms_make_singleton(1000) AS result; +-- Test module check +SELECT test_bms_make_singleton(NULL) AS result; + +-- bms_add_member() +SELECT test_bms_add_member('(b 1)', -1); -- error +SELECT test_bms_add_member('(b)', -10); -- error +SELECT test_bms_add_member('(b)', 10) AS result; +SELECT test_bms_add_member('(b 5)', 10) AS result; +-- sort check +SELECT test_bms_add_member('(b 10)', 5) AS result; +-- idempotent change +SELECT test_bms_add_member('(b 10)', 10) AS result; +-- Test module check +SELECT test_bms_add_member('(b)', NULL) AS result; + +-- bms_replace_members() +SELECT test_bms_replace_members(NULL, '(b 1 2 3)') AS result; +SELECT test_bms_replace_members('(b 1 2 3)', NULL) AS result; +SELECT test_bms_replace_members('(b 1 2 3)', '(b 3 5 6)') AS result; +SELECT test_bms_replace_members('(b 1 2 3)', '(b 3 5)') AS result; +SELECT test_bms_replace_members('(b 1 2)', '(b 3 5 7)') AS result; +-- Force repalloc() with larger set +SELECT test_bms_replace_members('(b 1 2 3 4 5)', '(b 500 600)') AS result; + +-- bms_del_member() +SELECT test_bms_del_member('(b)', -20); -- error +SELECT test_bms_del_member('(b)', 10) AS result; +SELECT test_bms_del_member('(b 10)', 10) AS result; +SELECT test_bms_del_member('(b 10)', 5) AS result; +SELECT test_bms_del_member('(b 1 2 3)', 2) AS result; +-- Reallocation check +SELECT test_bms_del_member(test_bms_del_member('(b 0 31 32 63 64)', 32), 63) AS result; +-- Word boundary +SELECT test_bms_del_member(test_bms_add_range('(b)', 30, 34), 32) AS result; +-- Force word count changes +SELECT test_bms_del_member('(b 1 200)', 200) AS result; +SELECT test_bms_del_member('(b 1 50 100 200)', 200) AS result; +SELECT test_bms_del_member('(b 1 50 100 200)', 100) AS result; +-- Test module checks +SELECT test_bms_del_member('(b 5)', NULL) AS result; + +-- bms_del_members() +SELECT test_bms_del_members('(b)', '(b 10)') AS result; +SELECT test_bms_del_members('(b 10)', '(b 10)') AS result; +SELECT test_bms_del_members('(b 10)', '(b 5)') AS result; +SELECT test_bms_del_members('(b 1 2 3)', '(b 2)') AS result; +SELECT test_bms_del_members('(b 5 100)', '(b 100)') AS result; +SELECT test_bms_del_members('(b 5 100 200)', '(b 200)') AS result; +-- Force word count changes +SELECT test_bms_del_members('(b 1 2 100 200 300)', '(b 1 2)') AS result; +SELECT test_bms_del_members('(b 1 2 100 200 300)', '(b 200 300)') AS result; +-- NULL inputs +SELECT test_bms_del_members('(b 5)', NULL) AS result; +SELECT test_bms_del_members(NULL, '(b 5)') AS result; + +-- bms_join() +SELECT test_bms_join('(b 1 3 5)', NULL) AS result; +SELECT test_bms_join(NULL, '(b 2 4 6)') AS result; +SELECT test_bms_join('(b 1 3 5)', '(b 2 4 6)') AS result; +SELECT test_bms_join('(b 1 3 5)', '(b 1 4 5)') AS result; +-- Force word count changes +SELECT test_bms_join('(b 5)', '(b 100)') AS result; +SELECT test_bms_join('(b 1 2)', '(b 100 200 300)') AS result; +-- NULL inputs +SELECT test_bms_join('(b 5)', NULL) AS result; +SELECT test_bms_join(NULL, '(b 5)') AS result; +SELECT test_bms_join(NULL, NULL) AS result; + +-- bms_union() +-- Overlapping sets +SELECT test_bms_union('(b 1 3 5)', '(b 3 5 7)') AS result; +-- Union with NULL +SELECT test_bms_union('(b 1 3 5)', '(b)') AS result; +-- Union of empty with empty +SELECT test_bms_union('(b)', '(b)') AS result; +-- Overlapping ranges +SELECT test_bms_union( + test_bms_add_range('(b)', 0, 15), + test_bms_add_range('(b)', 10, 20) + ) AS result; +-- Union with varying word counts +SELECT test_bms_union('(b 1 2)', '(b 100 300)') AS result; +SELECT test_bms_union('(b 100 300)', '(b 1 2)') AS result; +-- NULL inputs +SELECT test_bms_union('(b 5)', NULL) AS result; +SELECT test_bms_union(NULL, '(b 5)') AS result; +SELECT test_bms_union(NULL, NULL) AS result; + +-- bms_intersect() +-- Overlapping sets +SELECT test_bms_intersect('(b 1 3 5)', '(b 3 5 7)') AS result; +-- Disjoint sets +SELECT test_bms_intersect('(b 1 3 5)', '(b 2 4 6)') AS result; +-- Intersect with empty +SELECT test_bms_intersect('(b 1 3 5)', '(b)') AS result; +-- Intersect with varying word counts +SELECT test_bms_intersect('(b 1 300)', '(b 1 2 3 4 5)') AS result; +SELECT test_bms_intersect('(b 1 2 3 4 5)', '(b 1 300)') AS result; +-- NULL inputs +SELECT test_bms_intersect('(b 5)', NULL) AS result; +SELECT test_bms_intersect(NULL, '(b 5)') AS result; +SELECT test_bms_intersect(NULL, NULL) AS result; + +-- bms_int_members() +-- Overlapping sets +SELECT test_bms_int_members('(b 1 3 5)', '(b 3 5 7)') AS result; +-- Disjoint sets +SELECT test_bms_int_members('(b 1 3 5)', '(b 2 4 6)') AS result; +-- Intersect with empty +SELECT test_bms_int_members('(b 1 3 5)', '(b)') AS result; +-- Multiple members +SELECT test_bms_int_members('(b 0 31 32 63 64)', '(b 31 32 64 65)') AS result; +-- NULL inputs +SELECT test_bms_int_members('(b 5)', NULL) AS result; +SELECT test_bms_int_members(NULL, '(b 5)') AS result; +SELECT test_bms_int_members(NULL, NULL) AS result; + +-- bms_difference() +-- Overlapping sets +SELECT test_bms_difference('(b 1 3 5)', '(b 3 5 7)') AS result; +-- Disjoint sets +SELECT test_bms_difference('(b 1 3 5)', '(b 2 4 6)') AS result; +-- Identical sets +SELECT test_bms_difference('(b 1 3 5)', '(b 1 3 5)') AS result; +-- Subtraction to empty +SELECT test_bms_difference('(b 42)', '(b 42)') AS result; +-- Subtraction edge case +SELECT test_bms_difference( + test_bms_add_range('(b)', 0, 100), + test_bms_add_range('(b)', 50, 150) + ) AS result; +-- Difference with different word counts +SELECT test_bms_difference('(b 5 100)', '(b 5)') AS result; +SELECT test_bms_difference('(b 1 2 100 200)', '(b 1 2)') AS result; +-- NULL inputs +SELECT test_bms_difference('(b 5)', NULL) AS result; +SELECT test_bms_difference(NULL, '(b 5)') AS result; +SELECT test_bms_difference(NULL, NULL) AS result; + +-- bms_is_member() +SELECT test_bms_is_member('(b)', -5); -- error +SELECT test_bms_is_member('(b 1 3 5)', 1) AS result; +SELECT test_bms_is_member('(b 1 3 5)', 2) AS result; +SELECT test_bms_is_member('(b 1 3 5)', 3) AS result; +SELECT test_bms_is_member('(b)', 1) AS result; +-- Test module check +SELECT test_bms_is_member('(b 5)', NULL) AS result; + +-- bms_member_index() +SELECT test_bms_member_index(NULL, 1) AS result; +SELECT test_bms_member_index('(b 1 3 5)', 2) AS result; +SELECT test_bms_member_index('(b 1 3 5)', 1) AS result; +SELECT test_bms_member_index('(b 1 3 5)', 3) AS result; +-- Member index with various word positions +SELECT test_bms_member_index('(b 100 200)', 100) AS result; +SELECT test_bms_member_index('(b 100 200)', 200) AS result; +SELECT test_bms_member_index('(b 1 50 100 200)', 200) AS result; +-- Test module check +SELECT test_bms_member_index('(b 1 3 5)', NULL) AS result; + +-- bms_num_members() +SELECT test_bms_num_members('(b)') AS result; +SELECT test_bms_num_members('(b 1 3 5)') AS result; +SELECT test_bms_num_members('(b 2 4 6 8 10)') AS result; + +-- test_bms_equal() +SELECT test_bms_equal('(b)', '(b)') AS result; +SELECT test_bms_equal('(b)', '(b 1 3 5)') AS result; +SELECT test_bms_equal('(b 1 3 5)', '(b)') AS result; +SELECT test_bms_equal('(b 1 3 5)', '(b 1 3 5)') AS result; +SELECT test_bms_equal('(b 1 3 5)', '(b 2 4 6)') AS result; +-- Equal with different word counts +SELECT test_bms_equal('(b 5)', '(b 100)') AS result; +SELECT test_bms_equal('(b 5 10)', '(b 100 200 300)') AS result; +-- NULL inputs +SELECT test_bms_equal('(b 5)', NULL) AS result; +SELECT test_bms_equal(NULL, '(b 5)') AS result; +SELECT test_bms_equal(NULL, NULL) AS result; + +-- bms_compare() +SELECT test_bms_compare('(b)', '(b)') AS result; +SELECT test_bms_compare('(b)', '(b 1 3)') AS result; +SELECT test_bms_compare('(b 1 3)', '(b)') AS result; +SELECT test_bms_compare('(b 1 3)', '(b 1 3)') AS result; +SELECT test_bms_compare('(b 1 3)', '(b 1 3 5)') AS result; +SELECT test_bms_compare('(b 1 3 5)', '(b 1 3)') AS result; +SELECT test_bms_compare( + test_bms_add_range('(b)', 0, 63), + test_bms_add_range('(b)', 0, 64) + ) AS result; +-- NULL inputs +SELECT test_bms_compare('(b 5)', NULL) AS result; +SELECT test_bms_compare(NULL, '(b 5)') AS result; +SELECT test_bms_compare(NULL, NULL) AS result; + +-- bms_add_range() +SELECT test_bms_add_range('(b)', -5, 10); -- error +SELECT test_bms_add_range('(b)', 5, 7) AS result; +SELECT test_bms_add_range('(b)', 5, 5) AS result; +SELECT test_bms_add_range('(b 1 10)', 5, 7) AS result; +-- Word boundary of 31 +SELECT test_bms_add_range('(b)', 30, 34) AS result; +-- Word boundary of 63 +SELECT test_bms_add_range('(b)', 62, 66) AS result; +-- Large range +SELECT length(test_bms_add_range('(b)', 0, 1000)) AS result; +-- Force reallocations +SELECT length(test_bms_add_range('(b)', 0, 200)) AS result; +SELECT length(test_bms_add_range('(b)', 1000, 1100)) AS result; +-- Force word count expansion +SELECT test_bms_add_range('(b 5)', 100, 105) AS result; +SELECT length(test_bms_add_range('(b 1 2)', 200, 250)) AS result; +-- Test module checks +SELECT test_bms_add_range('(b 5)', 5, NULL) AS result; +SELECT test_bms_add_range('(b 5)', NULL, 10) AS result; +SELECT test_bms_add_range('(b 5)', NULL, NULL) AS result; +-- NULL inputs +SELECT test_bms_add_range(NULL, 5, 10) AS result; +SELECT test_bms_add_range(NULL, 10, 5) AS result; + +-- bms_membership() +SELECT test_bms_membership('(b)') AS result; +SELECT test_bms_membership('(b 42)') AS result; +SELECT test_bms_membership('(b 1 2)') AS result; +-- NULL input +SELECT test_bms_membership(NULL) AS result; + +-- bms_is_empty() +SELECT test_bms_is_empty(NULL) AS result; +SELECT test_bms_is_empty('(b)') AS result; +SELECT test_bms_is_empty('(b 1)') AS result; + +-- bms_singleton_member() +SELECT test_bms_singleton_member('(b)'); -- error +SELECT test_bms_singleton_member('(b 1 2)'); -- error +SELECT test_bms_singleton_member('(b 42)') AS result; +-- NULL input +SELECT test_bms_singleton_member(NULL) AS result; + +-- bms_get_singleton_member() +SELECT test_bms_get_singleton_member('(b)'); +-- Try with an empty set +SELECT test_bms_get_singleton_member(NULL) AS result; +-- Not a singleton, returns input default +SELECT test_bms_get_singleton_member('(b 3 6)') AS result; +-- Singletone, returns sole member +SELECT test_bms_get_singleton_member('(b 400)') AS result; + +-- bms_next_member() and bms_prev_member() +-- First member +SELECT test_bms_next_member('(b 5 10 15 20)', -1) AS result; +-- Second member +SELECT test_bms_next_member('(b 5 10 15 20)', 5) AS result; +-- Member past the end +SELECT test_bms_next_member('(b 5 10 15 20)', 20) AS result; +-- Empty set +SELECT test_bms_next_member('(b)', -1) AS result; +-- Last member +SELECT test_bms_prev_member('(b 5 10 15 20)', 21) AS result; +-- Penultimate member +SELECT test_bms_prev_member('(b 5 10 15 20)', 20) AS result; +-- Past beginning member +SELECT test_bms_prev_member('(b 5 10 15 20)', 5) AS result; +-- Empty set +SELECT test_bms_prev_member('(b)', 100) AS result; +-- Negative prevbit should result in highest possible bit in set +SELECT test_bms_prev_member('(b 0 63 64 127)', -1) AS result; +-- NULL inputs +SELECT test_bms_next_member(NULL, 5) AS result; +SELECT test_bms_prev_member(NULL, 5) AS result; +-- Test module check +SELECT test_bms_next_member('(b 5 10)', NULL) AS result; +SELECT test_bms_prev_member('(b 5 10)', NULL) AS result; + +-- bms_hash_value() +SELECT test_bms_hash_value('(b)') = 0 AS result; +SELECT test_bms_hash_value('(b 1 3 5)') = test_bms_hash_value('(b 1 3 5)') AS result; +SELECT test_bms_hash_value('(b 1 3 5)') != test_bms_hash_value('(b 2 4 6)') AS result; +-- NULL input +SELECT test_bms_hash_value(NULL) AS result; + +-- bms_overlap() +SELECT test_bms_overlap('(b 1 3 5)', '(b 3 5 7)') AS result; +SELECT test_bms_overlap('(b 1 3 5)', '(b 2 4 6)') AS result; +SELECT test_bms_overlap('(b)', '(b 1 3 5)') AS result; +-- NULL inputs +SELECT test_bms_overlap('(b 5)', NULL) AS result; +SELECT test_bms_overlap(NULL, '(b 5)') AS result; +SELECT test_bms_overlap(NULL, NULL) AS result; + +-- bms_is_subset() +SELECT test_bms_is_subset('(b)', '(b 1 3 5)') AS result; +SELECT test_bms_is_subset('(b 1 3)', '(b 1 3 5)') AS result; +SELECT test_bms_is_subset('(b 1 3 5)', '(b 1 3)') AS result; +SELECT test_bms_is_subset('(b 1 3)', '(b 2 4)') AS result; +SELECT test_bms_is_subset(test_bms_add_range(NULL, 0, 31), + test_bms_add_range(NULL, 0, 63)) AS result; +-- Is subset with shorter word counts? +SELECT test_bms_is_subset('(b 5 100)', '(b 5)') AS result; +SELECT test_bms_is_subset('(b 1 2 50 100)', '(b 1 2)') AS result; +-- NULL inputs +SELECT test_bms_is_subset('(b 5)', NULL) AS result; +SELECT test_bms_is_subset(NULL, '(b 5)') AS result; +SELECT test_bms_is_subset(NULL, NULL) AS result; + +-- bms_subset_compare() +SELECT test_bms_subset_compare(NULL, NULL) AS result; +SELECT test_bms_subset_compare(NULL, '(b 1 3)') AS result; +SELECT test_bms_subset_compare('(b)', '(b)') AS result; +SELECT test_bms_subset_compare('(b)', '(b 1)') AS result; +SELECT test_bms_subset_compare('(b 1)', '(b)') AS result; +SELECT test_bms_subset_compare('(b 1 3)', NULL) AS result; +SELECT test_bms_subset_compare('(b 1 3 5)', '(b 1 3 5)') AS result; +SELECT test_bms_subset_compare('(b 1 3)', '(b 1 3 5)') AS result; +SELECT test_bms_subset_compare('(b 1 3 5)', '(b 1 3)') AS result; +SELECT test_bms_subset_compare('(b 1 2)', '(b 1 3)') AS result; +SELECT test_bms_subset_compare('(b 1 2)', '(b 1 4)') AS result; +SELECT test_bms_subset_compare('(b 1 3)', '(b 1 3 64)') AS result; +SELECT test_bms_subset_compare('(b 1 3 64)', '(b 1 3)') AS result; +SELECT test_bms_subset_compare('(b 1 3 64)', '(b 1 3 65)') AS result; +SELECT test_bms_subset_compare('(b 1 3)', '(b 2 4)') AS result; +SELECT test_bms_subset_compare('(b 1)', '(b 64)') AS result; +SELECT test_bms_subset_compare('(b 0)', '(b 32)') AS result; +SELECT test_bms_subset_compare('(b 0)', '(b 64)') AS result; +SELECT test_bms_subset_compare('(b 64)', '(b 1)') AS result; +SELECT test_bms_subset_compare('(b 1 2)', '(b 1 2 64)') AS result; +SELECT test_bms_subset_compare('(b 64 200)', '(b 1 201)') AS result; +SELECT test_bms_subset_compare('(b 1 64 65)', '(b 1 2 64)') AS result; +SELECT test_bms_subset_compare('(b 2 64 128)', '(b 1 65)') AS result; + +-- bms_copy() +SELECT test_bms_copy(NULL) AS result; +SELECT test_bms_copy('(b 1 3 5 7)') AS result; + +-- bms_add_members() +SELECT test_bms_add_members('(b 1 3)', '(b 5 7)') AS result; +SELECT test_bms_add_members('(b 1 3 5)', '(b 2 5 7)') AS result; +SELECT test_bms_add_members('(b 1 3 5)', '(b 100 200 300)') AS result; + +-- bitmap_hash() +SELECT test_bitmap_hash('(b)') = 0 AS result; +SELECT test_bitmap_hash('(b 1 3 5)') = test_bitmap_hash('(b 1 3 5)') AS result; +SELECT test_bitmap_hash('(b 1 3 5)') = test_bms_hash_value('(b 1 3 5)') AS result; +SELECT test_bitmap_hash('(b 1 3 5)') != test_bitmap_hash('(b 2 4 6)') AS result; +-- Test module check +SELECT test_bitmap_hash(NULL) AS result; + +-- bitmap_match() +SELECT test_bitmap_match('(b)', '(b)') AS result; +SELECT test_bitmap_match('(b)', '(b 1 3 5)') AS result; +SELECT test_bitmap_match('(b 1 3 5)', '(b)') AS result; +SELECT test_bitmap_match('(b 1 3 5)', '(b 1 3 5)') AS result; +SELECT test_bitmap_match('(b 1 3 5)', '(b 2 4 6)') AS result; +SELECT test_bitmap_match('(b 1 3)', '(b 1 3 5)') AS result; +-- Check relationship of bitmap_match() with bms_equal() +SELECT (test_bitmap_match('(b 1 3 5)', '(b 1 3 5)') = 0) = + test_bms_equal('(b 1 3 5)', '(b 1 3 5)') AS result; +SELECT (test_bitmap_match('(b 1 3 5)', '(b 2 4 6)') = 0) = + test_bms_equal('(b 1 3 5)', '(b 2 4 6)') AS result; +SELECT (test_bitmap_match('(b)', '(b)') = 0) = + test_bms_equal('(b)', '(b)') AS result; +-- NULL inputs +SELECT test_bitmap_match('(b 5)', NULL) AS result; +SELECT test_bitmap_match(NULL, '(b 5)') AS result; +SELECT test_bitmap_match(NULL, NULL) AS result; + +-- bms_overlap_list() +SELECT test_bms_overlap_list('(b 0)', ARRAY[0]) AS result; +SELECT test_bms_overlap_list('(b 2 3)', ARRAY[1,2]) AS result; +SELECT test_bms_overlap_list('(b 3 4)', ARRAY[3,4,5]) AS result; +SELECT test_bms_overlap_list('(b 7 10)', ARRAY[6,7,8,9]) AS result; +SELECT test_bms_overlap_list('(b 1 5)', ARRAY[6,7,8,9]) AS result; +-- Empty list +SELECT test_bms_overlap_list('(b 1)', ARRAY[]::integer[]) AS result; +-- Overlap list with negative numbers +SELECT test_bms_overlap_list('(b 5 10)', ARRAY[-1,5]) AS result; -- error +SELECT test_bms_overlap_list('(b 1 2 3)', ARRAY[-5,-1,0]) AS result; -- error +-- NULL inputs +SELECT test_bms_overlap_list('(b 5)', NULL) AS result; +SELECT test_bms_overlap_list(NULL, ARRAY[1,2,3]) AS result; +SELECT test_bms_overlap_list(NULL, NULL) AS result; + +-- bms_nonempty_difference() +SELECT test_bms_nonempty_difference(NULL, '(b 1 3 5)') AS result; +SELECT test_bms_nonempty_difference('(b 1 3 5)', NULL) AS result; +SELECT test_bms_nonempty_difference(NULL, NULL) AS result; +SELECT test_bms_nonempty_difference('(b 1 3 5)', '(b 2 4 6)') AS result; +SELECT test_bms_nonempty_difference('(b 1 3 5)', '(b 1 5)') AS result; +SELECT test_bms_nonempty_difference('(b 1 3 5)', '(b 1 3 5)') AS result; +-- Difference with different word counts +SELECT test_bms_nonempty_difference('(b 5)', '(b 100)') AS result; +SELECT test_bms_nonempty_difference('(b 100)', '(b 5)') AS result; +SELECT test_bms_nonempty_difference('(b 1 2)', '(b 50 100)') AS result; + +-- random operations +SELECT test_random_operations(NULL, 10000, 81920, 0) > 0 AS result; + +DROP EXTENSION test_bitmapset; diff --git a/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql b/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql new file mode 100644 index 0000000000000..e7b263e51f543 --- /dev/null +++ b/src/test/modules/test_bitmapset/test_bitmapset--1.0.sql @@ -0,0 +1,140 @@ +/* src/test/modules/test_bitmapset/test_bitmapset--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_bitmapset" to load this file. \quit + +-- Bitmapset API functions +CREATE FUNCTION test_bms_make_singleton(integer) +RETURNS text STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_add_member(text, integer) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_del_member(text, integer) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_is_member(text, integer) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_num_members(text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_copy(text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_equal(text, text) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_compare(text, text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_is_subset(text, text) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_subset_compare(text, text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_union(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_intersect(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_difference(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_is_empty(text) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_membership(text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_singleton_member(text) +RETURNS integer STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_get_singleton_member(text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_next_member(text, integer) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_prev_member(text, integer) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_hash_value(text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_overlap(text, text) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_overlap_list(text, int4[]) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_nonempty_difference(text, text) +RETURNS boolean +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_member_index(text, integer) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_add_range(text, integer, integer) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_add_members(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_int_members(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_del_members(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_replace_members(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bms_join(text, text) +RETURNS text +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bitmap_hash(text) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_bitmap_match(text, text) +RETURNS int +AS 'MODULE_PATHNAME' LANGUAGE C; + +-- Test utility functions +CREATE FUNCTION test_random_operations(bigint, integer, integer, integer) +RETURNS integer +AS 'MODULE_PATHNAME' LANGUAGE C; + +COMMENT ON EXTENSION test_bitmapset IS 'Test code for Bitmapset'; diff --git a/src/test/modules/test_bitmapset/test_bitmapset.c b/src/test/modules/test_bitmapset/test_bitmapset.c new file mode 100644 index 0000000000000..3a185369651b9 --- /dev/null +++ b/src/test/modules/test_bitmapset/test_bitmapset.c @@ -0,0 +1,768 @@ +/*------------------------------------------------------------------------- + * + * test_bitmapset.c + * Test the Bitmapset data structure. + * + * This module tests the Bitmapset implementation in PostgreSQL, covering + * all public API functions. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_bitmapset/test_bitmapset.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include +#include "catalog/pg_type.h" +#include "common/pg_prng.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "nodes/bitmapset.h" +#include "nodes/nodes.h" +#include "nodes/pg_list.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/timestamp.h" + +PG_MODULE_MAGIC; + +/* Bitmapset API functions in order of appearance in bitmapset.c */ +PG_FUNCTION_INFO_V1(test_bms_make_singleton); +PG_FUNCTION_INFO_V1(test_bms_add_member); +PG_FUNCTION_INFO_V1(test_bms_del_member); +PG_FUNCTION_INFO_V1(test_bms_is_member); +PG_FUNCTION_INFO_V1(test_bms_num_members); +PG_FUNCTION_INFO_V1(test_bms_copy); +PG_FUNCTION_INFO_V1(test_bms_equal); +PG_FUNCTION_INFO_V1(test_bms_compare); +PG_FUNCTION_INFO_V1(test_bms_is_subset); +PG_FUNCTION_INFO_V1(test_bms_subset_compare); +PG_FUNCTION_INFO_V1(test_bms_union); +PG_FUNCTION_INFO_V1(test_bms_intersect); +PG_FUNCTION_INFO_V1(test_bms_difference); +PG_FUNCTION_INFO_V1(test_bms_is_empty); +PG_FUNCTION_INFO_V1(test_bms_membership); +PG_FUNCTION_INFO_V1(test_bms_singleton_member); +PG_FUNCTION_INFO_V1(test_bms_get_singleton_member); +PG_FUNCTION_INFO_V1(test_bms_next_member); +PG_FUNCTION_INFO_V1(test_bms_prev_member); +PG_FUNCTION_INFO_V1(test_bms_hash_value); +PG_FUNCTION_INFO_V1(test_bms_overlap); +PG_FUNCTION_INFO_V1(test_bms_overlap_list); +PG_FUNCTION_INFO_V1(test_bms_nonempty_difference); +PG_FUNCTION_INFO_V1(test_bms_member_index); +PG_FUNCTION_INFO_V1(test_bms_add_range); +PG_FUNCTION_INFO_V1(test_bms_add_members); +PG_FUNCTION_INFO_V1(test_bms_int_members); +PG_FUNCTION_INFO_V1(test_bms_del_members); +PG_FUNCTION_INFO_V1(test_bms_replace_members); +PG_FUNCTION_INFO_V1(test_bms_join); +PG_FUNCTION_INFO_V1(test_bitmap_hash); +PG_FUNCTION_INFO_V1(test_bitmap_match); + +/* Test utility functions */ +PG_FUNCTION_INFO_V1(test_random_operations); + +/* Convenient macros to test results */ +#define EXPECT_TRUE(expr) \ + do { \ + if (!(expr)) \ + elog(ERROR, \ + "%s was unexpectedly false in file \"%s\" line %u", \ + #expr, __FILE__, __LINE__); \ + } while (0) + +#define EXPECT_NOT_NULL(expr) \ + do { \ + if ((expr) == NULL) \ + elog(ERROR, \ + "%s was unexpectedly true in file \"%s\" line %u", \ + #expr, __FILE__, __LINE__); \ + } while (0) + +/* Encode/Decode to/from TEXT and Bitmapset */ +#define BITMAPSET_TO_TEXT(bms) cstring_to_text(nodeToString(bms)) +#define TEXT_TO_BITMAPSET(str) ((Bitmapset *) stringToNode(text_to_cstring(str))) + +/* + * Helper macro to fetch text parameters as Bitmapsets. SQL-NULL means empty + * set. + */ +#define PG_ARG_GETBITMAPSET(n) \ + (PG_ARGISNULL(n) ? NULL : TEXT_TO_BITMAPSET(PG_GETARG_TEXT_PP(n))) + +/* + * Helper macro to handle converting sets back to text, returning the + * resulting text representation of the set. + */ +#define PG_RETURN_BITMAPSET_AS_TEXT(bms) \ + PG_RETURN_TEXT_P(BITMAPSET_TO_TEXT(bms)) + +/* + * Individual test functions for each bitmapset API function + * + * Primarily, we aim to keep these as close to simple wrapper functions as + * possible in order to publish the functions of bitmapset.c to the SQL layer + * with as little interference as possible. We opt to return SQL NULL in + * cases where the input given to the SQL function isn't valid to pass to the + * underlying bitmapset.c function. For example we cannot do much useful + * testing if someone calls test_bms_make_singleton(NULL) since + * bms_make_singleton() expects an integer argument. + * + * For function arguments which are to be converted to Bitmapsets, we accept + * SQL NULL as a valid argument to mean an empty set. Optionally callers may + * pass '(b)'. + * + * For the test functions which return a Bitmapset, these are converted back + * to text with result generated by nodeToString(). + */ + +Datum +test_bms_add_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int member; + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + member = PG_GETARG_INT32(1); + + bms = bms_add_member(bms, member); + + PG_RETURN_BITMAPSET_AS_TEXT(bms); +} + +Datum +test_bms_add_members(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + + /* left input is recycled */ + bms1 = bms_add_members(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(bms1); +} + +Datum +test_bms_del_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 member; + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + member = PG_GETARG_INT32(1); + + bms = bms_del_member(bms, member); + + PG_RETURN_BITMAPSET_AS_TEXT(bms); +} + +Datum +test_bms_is_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 member; + bool result; + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + member = PG_GETARG_INT32(1); + + result = bms_is_member(member, bms); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_num_members(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + int result; + + result = bms_num_members(bms); + + PG_RETURN_INT32(result); +} + +Datum +test_bms_make_singleton(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 member; + + member = PG_GETARG_INT32(0); + bms = bms_make_singleton(member); + + PG_RETURN_BITMAPSET_AS_TEXT(bms); +} + +Datum +test_bms_copy(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + Bitmapset *copy_bms; + + copy_bms = bms_copy(bms); + + PG_RETURN_BITMAPSET_AS_TEXT(copy_bms); +} + +Datum +test_bms_equal(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + bool result; + + result = bms_equal(bms1, bms2); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_union(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + Bitmapset *result_bms; + + result_bms = bms_union(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(result_bms); +} + +Datum +test_bms_membership(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + BMS_Membership result; + + result = bms_membership(bms); + + PG_RETURN_INT32((int32) result); +} + +Datum +test_bms_next_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 prevmember; + int result; + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + prevmember = PG_GETARG_INT32(1); + + result = bms_next_member(bms, prevmember); + + PG_RETURN_INT32(result); +} + +Datum +test_bms_intersect(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + Bitmapset *result_bms; + + result_bms = bms_intersect(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(result_bms); +} + +Datum +test_bms_difference(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + Bitmapset *result_bms; + + result_bms = bms_difference(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(result_bms); +} + +Datum +test_bms_compare(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + int result; + + result = bms_compare(bms1, bms2); + + PG_RETURN_INT32(result); +} + +Datum +test_bms_is_empty(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + bool result; + + result = bms_is_empty(bms); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_is_subset(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + bool result; + + result = bms_is_subset(bms1, bms2); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_subset_compare(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + BMS_Comparison result; + + result = bms_subset_compare(bms1, bms2); + + PG_RETURN_INT32((int32) result); +} + +Datum +test_bms_singleton_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + int result; + + result = bms_singleton_member(bms); + + PG_RETURN_INT32(result); +} + +Datum +test_bms_get_singleton_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + int member; + + /* + * Keep this simple. Return -1 when we detect the set is not a singleton + * set, otherwise return the singleton member. + */ + if (!bms_get_singleton_member(bms, &member)) + member = -1; + + PG_RETURN_INT32(member); +} + +Datum +test_bms_prev_member(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 prevmember; + int result; + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + prevmember = PG_GETARG_INT32(1); + + result = bms_prev_member(bms, prevmember); + + PG_RETURN_INT32(result); +} + +Datum +test_bms_overlap(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + bool result; + + result = bms_overlap(bms1, bms2); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_overlap_list(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + ArrayType *array; + List *int_list = NIL; + bool result; + Datum *elem_datums = NULL; + bool *elem_nulls = NULL; + int elem_count; + int i; + + bms = PG_ARG_GETBITMAPSET(0); + + if (!PG_ARGISNULL(1)) + { + array = PG_GETARG_ARRAYTYPE_P(1); + + deconstruct_array(array, + INT4OID, sizeof(int32), true, 'i', + &elem_datums, &elem_nulls, &elem_count); + + for (i = 0; i < elem_count; i++) + { + if (!elem_nulls[i]) + { + int32 member = DatumGetInt32(elem_datums[i]); + + int_list = lappend_int(int_list, member); + } + } + } + + result = bms_overlap_list(bms, int_list); + + list_free(int_list); + + if (elem_datums) + pfree(elem_datums); + + if (elem_nulls) + pfree(elem_nulls); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_nonempty_difference(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + bool result; + + result = bms_nonempty_difference(bms1, bms2); + + PG_RETURN_BOOL(result); +} + +Datum +test_bms_member_index(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 member; + int result; + + if (PG_ARGISNULL(1)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + member = PG_GETARG_INT32(1); + + result = bms_member_index(bms, member); + + PG_RETURN_INT32(result); +} + +Datum +test_bms_add_range(PG_FUNCTION_ARGS) +{ + Bitmapset *bms; + int32 lower, + upper; + + if (PG_ARGISNULL(1) || PG_ARGISNULL(2)) + PG_RETURN_NULL(); /* invalid input */ + + bms = PG_ARG_GETBITMAPSET(0); + lower = PG_GETARG_INT32(1); + upper = PG_GETARG_INT32(2); + + bms = bms_add_range(bms, lower, upper); + + PG_RETURN_BITMAPSET_AS_TEXT(bms); +} + +Datum +test_bms_int_members(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + + /* left input gets recycled */ + bms1 = bms_int_members(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(bms1); +} + +Datum +test_bms_del_members(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + + /* left input gets recycled */ + bms1 = bms_del_members(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(bms1); +} + +Datum +test_bms_replace_members(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + + /* left input gets recycled */ + bms1 = bms_replace_members(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(bms1); +} + +Datum +test_bms_join(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + Bitmapset *result_bms; + + /* either input can be recycled */ + result_bms = bms_join(bms1, bms2); + + PG_RETURN_BITMAPSET_AS_TEXT(result_bms); +} + +Datum +test_bms_hash_value(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + uint32 hash_result; + + hash_result = bms_hash_value(bms); + + PG_RETURN_INT32(hash_result); +} + +Datum +test_bitmap_hash(PG_FUNCTION_ARGS) +{ + Bitmapset *bms = PG_ARG_GETBITMAPSET(0); + uint32 hash_result; + + /* Call bitmap_hash */ + hash_result = bitmap_hash(&bms, sizeof(Bitmapset *)); + + PG_RETURN_INT32(hash_result); +} + +Datum +test_bitmap_match(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = PG_ARG_GETBITMAPSET(0); + Bitmapset *bms2 = PG_ARG_GETBITMAPSET(1); + int match_result; + + /* Call bitmap_match with addresses of the Bitmapset pointers */ + match_result = bitmap_match(&bms1, &bms2, sizeof(Bitmapset *)); + + PG_RETURN_INT32(match_result); +} + +/* + * Contrary to all the other functions which are one-one mappings with the + * equivalent C functions, this stresses Bitmapsets in a random fashion for + * various operations. + * + * "min_value" is the minimal value used for the members, that will stand + * up to a range of "max_range". "num_ops" defines the number of time each + * operation is done. "seed" is a random seed used to calculate the member + * values. When "seed" is NULL, a random seed will be chosen automatically. + * + * The return value is the number of times all operations have been executed. + */ +Datum +test_random_operations(PG_FUNCTION_ARGS) +{ + Bitmapset *bms1 = NULL; + Bitmapset *bms2 = NULL; + Bitmapset *bms = NULL; + Bitmapset *result = NULL; + pg_prng_state state; + uint64 seed = GetCurrentTimestamp(); + int num_ops; + int max_range; + int min_value; + int member; + int *members; + int num_members = 0; + int total_ops = 0; + + if (!PG_ARGISNULL(0)) + seed = PG_GETARG_INT64(0); + + num_ops = PG_GETARG_INT32(1); + max_range = PG_GETARG_INT32(2); + min_value = PG_GETARG_INT32(3); + + if (PG_ARGISNULL(1) || num_ops <= 0) + elog(ERROR, "invalid number of operations"); + if (PG_ARGISNULL(2) || max_range <= 0) + elog(ERROR, "invalid maximum range"); + if (PG_ARGISNULL(3) || min_value < 0) + elog(ERROR, "invalid minimum value"); + + pg_prng_seed(&state, seed); + + /* + * There can be up to "num_ops" members added. This is very unlikely, + * still possible if all the operations hit the "0" case during phase 4 + * where multiple operation types are mixed together. + */ + members = palloc_array(int, num_ops); + + /* Phase 1: Random insertions in first set */ + for (int i = 0; i < num_ops / 2; i++) + { + CHECK_FOR_INTERRUPTS(); + + member = pg_prng_uint32(&state) % max_range + min_value; + + if (!bms_is_member(member, bms1)) + members[num_members++] = member; + bms1 = bms_add_member(bms1, member); + } + + /* Phase 2: Random insertions in second set */ + for (int i = 0; i < num_ops / 4; i++) + { + CHECK_FOR_INTERRUPTS(); + + member = pg_prng_uint32(&state) % max_range + min_value; + + if (!bms_is_member(member, bms2)) + members[num_members++] = member; + bms2 = bms_add_member(bms2, member); + } + + /* Test union */ + result = bms_union(bms1, bms2); + EXPECT_NOT_NULL(result); + + /* Verify union contains all members from first and second sets */ + for (int i = 0; i < num_members; i++) + { + CHECK_FOR_INTERRUPTS(); + + if (!bms_is_member(members[i], result)) + elog(ERROR, "union missing member %d, seed " INT64_FORMAT, + members[i], seed); + } + bms_free(result); + + /* + * Test intersection, checking that all the members in the result are from + * both the first and second sets. + */ + result = bms_intersect(bms1, bms2); + if (result != NULL) + { + member = -1; + + while ((member = bms_next_member(result, member)) >= 0) + { + CHECK_FOR_INTERRUPTS(); + + if (!bms_is_member(member, bms1) || !bms_is_member(member, bms2)) + elog(ERROR, "intersection contains invalid member %d, seed " INT64_FORMAT, + member, seed); + } + bms_free(result); + } + + /* Phase 3: Test range operations */ + result = NULL; + for (int i = 0; i < num_ops; i++) + { + int lower = pg_prng_uint32(&state) % 100; + int upper = lower + (pg_prng_uint32(&state) % 20); + + CHECK_FOR_INTERRUPTS(); + + result = bms_add_range(result, lower, upper); + } + if (result != NULL) + { + EXPECT_TRUE(bms_num_members(result) > 0); + bms_free(result); + } + + bms_free(bms1); + bms_free(bms2); + + /* + * Phase 4: mix of operations on a single set, cross-checking a bitmap + * with a secondary state, "members". + */ + num_members = 0; + + for (int op = 0; op < num_ops; op++) + { + CHECK_FOR_INTERRUPTS(); + + switch (pg_prng_uint32(&state) % 3) + { + case 0: /* add */ + member = pg_prng_uint32(&state) % max_range + min_value; + if (!bms_is_member(member, bms)) + members[num_members++] = member; + bms = bms_add_member(bms, member); + break; + case 1: /* delete */ + if (num_members > 0) + { + int pos = pg_prng_uint32(&state) % num_members; + + member = members[pos]; + if (!bms_is_member(member, bms)) + elog(ERROR, "expected %d to be a valid member, seed " INT64_FORMAT, + member, seed); + + bms = bms_del_member(bms, member); + + /* + * Move the final array member at the position of the + * member just deleted, reducing the array size by one. + */ + members[pos] = members[--num_members]; + } + break; + case 2: /* test membership */ + /* Verify that bitmap contains all members */ + for (int i = 0; i < num_members; i++) + { + if (!bms_is_member(members[i], bms)) + elog(ERROR, "missing member %d, seed " INT64_FORMAT, + members[i], seed); + } + break; + } + total_ops++; + } + + bms_free(bms); + pfree(members); + + PG_RETURN_INT32(total_ops); +} diff --git a/src/test/modules/test_bitmapset/test_bitmapset.control b/src/test/modules/test_bitmapset/test_bitmapset.control new file mode 100644 index 0000000000000..8d02ec8bf0a95 --- /dev/null +++ b/src/test/modules/test_bitmapset/test_bitmapset.control @@ -0,0 +1,4 @@ +comment = 'Test code for Bitmapset' +default_version = '1.0' +module_pathname = '$libdir/test_bitmapset' +relocatable = true diff --git a/src/test/modules/test_bloomfilter/meson.build b/src/test/modules/test_bloomfilter/meson.build index 3326e37c31d2e..947c01a329756 100644 --- a/src/test/modules/test_bloomfilter/meson.build +++ b/src/test/modules/test_bloomfilter/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_bloomfilter_sources = files( 'test_bloomfilter.c', diff --git a/src/test/modules/test_bloomfilter/test_bloomfilter.c b/src/test/modules/test_bloomfilter/test_bloomfilter.c index 499d437a9c4da..df41066138c8c 100644 --- a/src/test/modules/test_bloomfilter/test_bloomfilter.c +++ b/src/test/modules/test_bloomfilter/test_bloomfilter.c @@ -3,7 +3,7 @@ * test_bloomfilter.c * Test false positive rate of Bloom filter. * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_bloomfilter/test_bloomfilter.c @@ -125,7 +125,7 @@ test_bloomfilter(PG_FUNCTION_ARGS) elog(ERROR, "invalid number of tests: %d", tests); if (nelements < 0) - elog(ERROR, "invalid number of elements: %d", tests); + elog(ERROR, "invalid number of elements: " INT64_FORMAT, nelements); for (i = 0; i < tests; i++) { diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore new file mode 100644 index 0000000000000..871e943d50e1a --- /dev/null +++ b/src/test/modules/test_checksums/.gitignore @@ -0,0 +1,2 @@ +# Generated by test suite +/tmp_check/ diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile new file mode 100644 index 0000000000000..71455cd557721 --- /dev/null +++ b/src/test/modules/test_checksums/Makefile @@ -0,0 +1,36 @@ +#------------------------------------------------------------------------- +# +# Makefile for src/test/modules/test_checksums +# +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/test/modules/test_checksums/Makefile +# +#------------------------------------------------------------------------- + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points + +MODULE_big = test_checksums +OBJS = \ + $(WIN32RES) \ + test_checksums.o +PGFILEDESC = "test_checksums - test code for data checksums" + +EXTENSION = test_checksums +DATA = test_checksums--1.0.sql + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_checksums +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README new file mode 100644 index 0000000000000..6a23e4ff0ae36 --- /dev/null +++ b/src/test/modules/test_checksums/README @@ -0,0 +1,30 @@ +src/test/modules/test_checksums/README + +Regression tests for data checksums +=================================== +This directory contains a test suite for enabling, and disabling, data +checksums both offline as well as in a running cluster. + +Running the tests with autoconf +=============================== + + make check + +or + + make installcheck + +Running the tests with meson +============================ +From your build directory, issue the following command: + + meson test -q --print-errorlogs --suite setup --suite test_checksums + +NOTE: This creates a temporary installation (in the case of "make check" or +"--suite setup"), with multiple nodes, be they master or standby(s) for the +purpose of the tests. + +NOTE: This test suite requires TAP tests to be enabled, a subset of the tests +also require injection points to function. In order to run the extended test +then "checksum_extended" must be set in the PG_TEST_EXTRA environment +variable. diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build new file mode 100644 index 0000000000000..9b1421a9b9136 --- /dev/null +++ b/src/test/modules/test_checksums/meson.build @@ -0,0 +1,38 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_checksums_sources = files( + 'test_checksums.c', +) + +test_checksums = shared_module('test_checksums', + test_checksums_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_checksums + +test_install_data += files( + 'test_checksums.control', + 'test_checksums--1.0.sql', +) + +tests += { + 'name': 'test_checksums', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'env': { + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, + 'tests': [ + 't/001_basic.pl', + 't/002_restarts.pl', + 't/003_standby_restarts.pl', + 't/004_offline.pl', + 't/005_injection.pl', + 't/006_pgbench_single.pl', + 't/007_pgbench_standby.pl', + 't/008_pitr.pl', + 't/009_fpi.pl', + ], + }, +} diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl new file mode 100644 index 0000000000000..a78118320d551 --- /dev/null +++ b/src/test/modules/test_checksums/t/001_basic.pl @@ -0,0 +1,63 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# Initialize node with checksums disabled. +my $node = PostgreSQL::Test::Cluster->new('basic_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Create some content to have un-checksummed data in the cluster +$node->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;"); + +# Ensure that checksums are turned off +test_checksum_state($node, 'off'); + +# Enable data checksums and wait for the state transition to 'on' +enable_data_checksums($node, wait => 'on'); + +# Run a dummy query just to make sure we can read back data +my $result = + $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 "); +is($result, '9999', 'ensure checksummed pages can be read back'); + +# Enable data checksums again which should be a no-op so we explicitly don't +# wait for any state transition as none should happen here. +enable_data_checksums($node); +test_checksum_state($node, 'on'); +# ..and make sure we can still read/write data +$node->safe_psql('postgres', "UPDATE t SET a = a + 1;"); +$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '10000', 'ensure checksummed pages can be read back'); + +# Disable checksums again and wait for the state transition +disable_data_checksums($node, wait => 1); + +# Test reading data again +$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '10000', 'ensure previously checksummed pages can be read back'); + +# Re-enable checksums and make sure that the underlying data has changed to +# ensure that checksums will be different. +$node->safe_psql('postgres', "UPDATE t SET a = a + 1;"); +enable_data_checksums($node, wait => 'on'); + +# Run a dummy query just to make sure we can read back the data +$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '10000', 'ensure checksummed pages can be read back'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl new file mode 100644 index 0000000000000..bab59be82bd91 --- /dev/null +++ b/src/test/modules/test_checksums/t/002_restarts.pl @@ -0,0 +1,110 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with a +# restart which breaks processing. +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# Initialize node with checksums disabled. +my $node = PostgreSQL::Test::Cluster->new('restarts_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Initialize result storage for queries +my $result; + +# Create some content to have un-checksummed data in the cluster +$node->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;"); + +# Ensure that checksums are disabled +test_checksum_state($node, 'off'); + +SKIP: +{ + skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6 + if (!$ENV{PG_TEST_EXTRA} + || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/); + + # Create a barrier for checksum enablement to block on, in this case a pre- + # existing temporary table which is kept open while processing is started. + # We can accomplish this by setting up an interactive psql process which + # keeps the temporary table created as we enable checksums in another psql + # process. + # + # This is a similar test to the synthetic variant in 005_injection.pl + # which fakes this scenario. + my $bsession = $node->background_psql('postgres'); + $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);'); + + # In another session, make sure we can see the blocking temp table but + # start processing anyways and check that we are blocked with a proper + # wait event. + $result = $node->safe_psql('postgres', + "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';" + ); + is($result, 't', 'ensure we can see the temporary table'); + + # Enabling data checksums shouldn't work as the process is blocked on the + # temporary table held open by $bsession. Ensure that we reach inprogress- + # on before we do more tests. + enable_data_checksums($node, wait => 'inprogress-on'); + + # Wait for processing to finish and the worker waiting for leftover temp + # relations to be able to actually finish + $result = $node->poll_query_until( + 'postgres', + "SELECT wait_event FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum worker';", + 'ChecksumEnableTemptableWait'); + + # The datachecksumsworker waits for temporary tables to disappear for 3 + # seconds before retrying, so sleep for 4 seconds to be guaranteed to see + # a retry cycle + sleep(4); + + # Re-check the wait event to ensure we are blocked on the right thing. + $result = $node->safe_psql('postgres', + "SELECT wait_event FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum worker';"); + is($result, 'ChecksumEnableTemptableWait', + 'ensure the correct wait condition is set'); + test_checksum_state($node, 'inprogress-on'); + + # Stop the cluster while bsession is still attached. We can't close the + # session first since the brief period between closing and stopping might + # be enough for checksums to get enabled. + $node->stop; + $bsession->quit; + $node->start; + + # Ensure the checksums aren't enabled across the restart. This leaves the + # cluster in the same state as before we entered the SKIP block. + test_checksum_state($node, 'off'); +} + +enable_data_checksums($node, wait => 'on'); + +$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '9999', 'ensure checksummed pages can be read back'); + +$result = $node->poll_query_until( + 'postgres', + "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';", + '0'); +is($result, 1, 'await datachecksums worker/launcher termination'); + +disable_data_checksums($node, wait => 1); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl new file mode 100644 index 0000000000000..11e15c9d73471 --- /dev/null +++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl @@ -0,0 +1,120 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# streaming replication +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# Initialize primary node +my $node_primary = PostgreSQL::Test::Cluster->new('standby_restarts_primary'); +$node_primary->init(allows_streaming => 1, no_data_checksums => 1); +$node_primary->start; + +my $slotname = 'physical_slot'; +$node_primary->safe_psql('postgres', + "SELECT pg_create_physical_replication_slot('$slotname')"); + +# Take backup +my $backup_name = 'my_backup'; +$node_primary->backup($backup_name); + +# Create streaming standby linking to primary +my $node_standby = PostgreSQL::Test::Cluster->new('standby_restarts_standby'); +$node_standby->init_from_backup($node_primary, $backup_name, + has_streaming => 1); +$node_standby->append_conf( + 'postgresql.conf', qq[ +primary_slot_name = '$slotname' +]); +$node_standby->start; + +# Create some content on the primary to have un-checksummed data in the cluster +$node_primary->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;"); + +# Wait for standby to catch up +$node_primary->wait_for_catchup($node_standby, 'replay', + $node_primary->lsn('insert')); + +# Check that checksums are turned off on all nodes +test_checksum_state($node_primary, 'off'); +test_checksum_state($node_standby, 'off'); + +# --------------------------------------------------------------------------- +# Enable checksums for the cluster, and make sure that both the primary and +# standby change state. +# + +# Initiate enabling of checksums and ensure that the primary switches to +# either "inprogress-on" or "on" +enable_data_checksums($node_primary); +my $result = $node_primary->poll_query_until( + 'postgres', + "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';", + 'f'); +is($result, 1, 'ensure primary has transitioned from off'); +# Wait for checksum enable to be replayed +$node_primary->wait_for_catchup($node_standby, 'replay'); + +# Ensure that the standby has switched to "inprogress-on" or "on". Normally it +# would be "inprogress-on", but it is theoretically possible for the primary to +# complete the checksum enabling *and* have the standby replay that record +# before we reach the check below. +$result = $node_standby->poll_query_until( + 'postgres', + "SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';", + 'f'); +is($result, 1, 'ensure standby has absorbed the inprogress-on barrier'); +$result = $node_standby->safe_psql('postgres', + "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';" +); + +is(($result eq 'inprogress-on' || $result eq 'on'), + 1, 'ensure checksums are on, or in progress, on standby_1'); + +# Insert some more data which should be checksummed on INSERT +$node_primary->safe_psql('postgres', + "INSERT INTO t VALUES (generate_series(1, 10000));"); + +# Wait for checksums enabled on the primary and standby +wait_for_checksum_state($node_primary, 'on'); +wait_for_checksum_state($node_standby, 'on'); + +$result = + $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1"); +is($result, '19998', 'ensure we can safely read all data with checksums'); + +$result = $node_primary->poll_query_until( + 'postgres', + "SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';", + '0'); +is($result, 1, 'await datachecksums worker/launcher termination'); + +# +# Disable checksums and ensure it's propagated to standby and that we can +# still read all data +# + +# Disable checksums and wait for the operation to be replayed +disable_data_checksums($node_primary); +$node_primary->wait_for_catchup($node_standby, 'replay'); +# Ensure that the primary and standby has switched to off +wait_for_checksum_state($node_primary, 'off'); +wait_for_checksum_state($node_standby, 'off'); +# Double-check reading data without errors +$result = + $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1"); +is($result, "19998", 'ensure we can safely read all data without checksums'); + +$node_standby->stop; +$node_primary->stop; +done_testing(); diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl new file mode 100644 index 0000000000000..73c279e75e00f --- /dev/null +++ b/src/test/modules/test_checksums/t/004_offline.pl @@ -0,0 +1,86 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums offline from various states +# of checksum processing +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# Initialize node with checksums disabled. +my $node = PostgreSQL::Test::Cluster->new('offline_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Create some content to have un-checksummed data in the cluster +$node->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;"); + +# Ensure that checksums are disabled +test_checksum_state($node, 'off'); + +# Enable checksums offline using pg_checksums +$node->stop; +$node->checksum_enable_offline; +$node->start; + +# Ensure that checksums are enabled +test_checksum_state($node, 'on'); + +# Run a dummy query just to make sure we can read back some data +my $result = + $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '9999', 'ensure checksummed pages can be read back'); + +# Disable checksums offline again using pg_checksums +$node->stop; +$node->checksum_disable_offline; +$node->start; + +# Ensure that checksums are disabled +test_checksum_state($node, 'off'); + +# Create a barrier for checksum enablement to block on, in this case a pre- +# existing temporary table which is kept open while processing is started. We +# can accomplish this by setting up an interactive psql process which keeps the +# temporary table created as we enable checksums in another psql process. + +my $bsession = $node->background_psql('postgres'); +$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);'); + +# In another session, make sure we can see the blocking temp table but start +# processing anyways and check that we are blocked with a proper wait event. +$result = $node->safe_psql('postgres', + "SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"); +is($result, 't', 'ensure we can see the temporary table'); + +# Enable, but stop waiting at inprogress-on since it will sit there until the +# above temporary table is removed. +enable_data_checksums($node, wait => 'inprogress-on'); + +# Turn the cluster off and enable checksums offline, then start back up. +# Stop the cluster before exiting the background session since otherwise +# checksums might have time to get enabled before shutting down the cluster. +$node->stop('fast'); +$bsession->quit; +$node->checksum_enable_offline; +$node->start; + +# Ensure that checksums are now enabled even though processing wasn't +# restarted +test_checksum_state($node, 'on'); + +# Run a dummy query just to make sure we can read back some data +$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '9999', 'ensure checksummed pages can be read back'); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl new file mode 100644 index 0000000000000..7240b93bdd135 --- /dev/null +++ b/src/test/modules/test_checksums/t/005_injection.pl @@ -0,0 +1,80 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# injection point tests injecting failures into the processing + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# --------------------------------------------------------------------------- +# Test cluster setup +# + +# Initiate test cluster +my $node = PostgreSQL::Test::Cluster->new('injection_node'); +$node->init(no_data_checksums => 1); +$node->start; + +# Set up test environment +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# --------------------------------------------------------------------------- +# Inducing failures and crashes in processing + +# Force enabling checksums to fail by marking one of the databases as having +# failed in processing. +disable_data_checksums($node, wait => 1); +$node->safe_psql('postgres', + "SELECT injection_points_attach('datachecksumsworker-fail-db-result','notice');" +); +enable_data_checksums($node, wait => 'off'); +$node->safe_psql('postgres', + "SELECT injection_points_detach('datachecksumsworker-fail-db-result');"); + +# Make sure that disabling after a failure works +disable_data_checksums($node); +test_checksum_state($node, 'off'); + +# --------------------------------------------------------------------------- +# Timing and retry related tests +# + +SKIP: +{ + skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4 + if (!$ENV{PG_TEST_EXTRA} + || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/); + + # Inject a delay in the barrier for enabling checksums + disable_data_checksums($node, wait => 1); + $node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();'); + enable_data_checksums($node, wait => 'on'); + + # Fake the existence of a temporary table at the start of processing, which + # will force the processing to wait and retry in order to wait for it to + # disappear. + disable_data_checksums($node, wait => 1); + $node->safe_psql('postgres', + "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait', 'notice');" + ); + enable_data_checksums($node, wait => 'on'); +} + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl new file mode 100644 index 0000000000000..f5ccc1b5b082d --- /dev/null +++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl @@ -0,0 +1,285 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster with +# concurrent activity via pgbench runs + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} +else +{ + plan skip_all => 'Expensive data checksums test disabled'; +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node; +my $node_loglocation = 0; + +# The number of full test iterations which will be performed. The exact number +# of tests performed and the wall time taken is non-deterministic as the test +# performs a lot of randomized actions, but 10 iterations will be a long test +# run regardless. +my $TEST_ITERATIONS = 1; +$TEST_ITERATIONS = 10 if ($extended); + +# Variables which record the current state of the cluster +my $data_checksum_state = 'off'; +my $pgbench = undef; + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + my $clients = 1; + my $runtime = 2; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +# Invert the state of data checksums in the cluster, if data checksums are on +# then disable them and vice versa. Also performs proper validation of the +# before and after state. +sub flip_data_checksums +{ + my $temptablewait = 0; + + # First, make sure the cluster is in the state we expect it to be + test_checksum_state($node, $data_checksum_state); + + if ($data_checksum_state eq 'off') + { + # Coin-toss to see if we are injecting a retry due to a temptable + if (cointoss()) + { + $node->safe_psql('postgres', + "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait', 'notice');" + ); + $temptablewait = 1; + } + + # log LSN right before we start changing checksums + my $result = + $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN before enabling: " . $result . "\n"); + + # Ensure that the primary switches to "inprogress-on" + enable_data_checksums($node, wait => 'inprogress-on'); + + random_sleep() if ($extended); + + # Wait for checksums enabled on the primary + wait_for_checksum_state($node, 'on'); + + # log LSN right after the primary flips checksums to "on" + $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN after enabling: " . $result . "\n"); + + random_sleep() if ($extended); + + $node->safe_psql('postgres', + "SELECT injection_points_detach('datachecksumsworker-fake-temptable-wait');" + ) if ($temptablewait); + $data_checksum_state = 'on'; + } + elsif ($data_checksum_state eq 'on') + { + random_sleep() if ($extended); + + # log LSN right before we start changing checksums + my $result = + $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN before disabling: " . $result . "\n"); + + disable_data_checksums($node); + + # Wait for checksums disabled on the primary + wait_for_checksum_state($node, 'off'); + + # log LSN right after the primary flips checksums to "off" + $result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN after disabling: " . $result . "\n"); + + random_sleep() if ($extended); + + $data_checksum_state = 'off'; + } + else + { + # This should only happen due to programmer error when hacking on the + # test code, but since that might pass subtly we error out. + BAIL_OUT('data_checksum_state variable has invalid state:' + . $data_checksum_state); + } +} + +# Create and start a cluster with one node +$node = PostgreSQL::Test::Cluster->new('pgbench_single_main'); +$node->init(allows_streaming => 1, no_data_checksums => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Create some content to have un-checksummed data in the cluster +$node->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;"); +# Initialize pgbench +my $scalefactor = ($extended ? 10 : 1); +$node->command_ok( + [ + 'pgbench', '-p', $node->port, '-i', + '-s', $scalefactor, '-q', 'postgres' + ]); +# Start the test suite with pgbench running. +background_rw_pgbench($node->port); + +# Main test suite. This loop will start a pgbench run on the cluster and while +# that's running flip the state of data checksums concurrently. It will then +# randomly restart the cluster and then check for +# the desired state. The idea behind doing things randomly is to stress out +# any timing related issues by subjecting the cluster for varied workloads. +# A TODO is to generate a trace such that any test failure can be traced to +# its order of operations for debugging. +for (my $i = 0; $i < $TEST_ITERATIONS; $i++) +{ + note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS); + + if (!$node->is_alive) + { + # Start, to do recovery, and stop + $node->start; + $node->stop('fast'); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_loglocation = -s $node->logfile; + + # Randomize the WAL size, to trigger checkpoints less/more often + my $sb = 64 + int(rand(1024)); + $node->append_conf('postgresql.conf', qq[max_wal_size = $sb]); + note("changing max_wal_size to " . $sb); + + $node->start; + + # Start a pgbench in the background against the primary + background_rw_pgbench($node->port); + } + + $node->safe_psql('postgres', "UPDATE t SET a = a + 1;"); + + flip_data_checksums(); + random_sleep() if ($extended); + my $result = + $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); + is($result, '100000', 'ensure data pages can be read back on primary'); + + random_sleep() if ($extended); + + # Potentially powercycle the node + if (cointoss()) + { + $node->stop(stopmode()); + + PostgreSQL::Test::Utils::system_log("pg_controldata", + $node->data_dir); + + my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, + $node_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (outside WAL recovery)" + ); + $node_loglocation = -s $node->logfile; + } + + random_sleep() if ($extended); +} + +# Make sure the node is running +if (!$node->is_alive) +{ + $node->start; +} + +# Testrun is over, ensure that data reads back as expected and perform a final +# verification of the data checksum state. +my $result = + $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '100000', 'ensure data pages can be read back on primary'); +test_checksum_state($node, $data_checksum_state); + +# Perform one final pass over the logs and hunt for unexpected errors +my $log = + PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation); +unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log"); +$node_loglocation = -s $node->logfile; + +$node->teardown_node; + +done_testing(); diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl new file mode 100644 index 0000000000000..f3611e7ce256b --- /dev/null +++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl @@ -0,0 +1,407 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster, +# comprising of a primary and a replicated standby, with concurrent activity +# via pgbench runs + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. The full suite can run for hours +# on slow or constrained systems. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} +else +{ + plan skip_all => 'Expensive data checksums test disabled'; +} + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node_primary_slot = 'physical_slot'; +my $node_primary_backup = 'primary_backup'; +my $node_primary; +my $node_primary_loglocation = 0; +my $node_standby; +my $node_standby_loglocation = 0; + +# The number of full test iterations which will be performed. The exact number +# of tests performed and the wall time taken is non-deterministic as the test +# performs a lot of randomized actions, but 5 iterations will be a long test +# run regardless. +my $TEST_ITERATIONS = 5; +$TEST_ITERATIONS = 1 if ($extended); + +# Variables which record the current state of the cluster +my $data_checksum_state = 'off'; + +my $pgbench_primary = undef; +my $pgbench_standby = undef; + +# Start a pgbench run in the background against the server specified via the +# port passed as parameter +sub background_pgbench +{ + my ($port, $standby) = @_; + my $pgbench = ($standby ? \$pgbench_standby : \$pgbench_primary); + + # Terminate any currently running pgbench process before continuing + $$pgbench->finish if $$pgbench; + + my $clients = 1; + my $runtime = 5; + + if ($extended) + { + # Randomize the number of pgbench clients a bit (range 1-16) + $clients = 1 + int(rand(15)); + $runtime = 600; + } + + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss()); + # If we run on a standby it needs to be a read-only benchmark + push(@cmd, '-S') if ($standby); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $$pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +# Invert the state of data checksums in the cluster, if data checksums are on +# then disable them and vice versa. Also performs proper validation of the +# before and after state. +sub flip_data_checksums +{ + my $temptablewait = 0; + + # First, make sure the cluster is in the state we expect it to be + test_checksum_state($node_primary, $data_checksum_state); + test_checksum_state($node_standby, $data_checksum_state); + + if ($data_checksum_state eq 'off') + { + # Coin-toss to see if we are injecting a retry due to a temptable + if (cointoss()) + { + $node_primary->safe_psql('postgres', + "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait', 'notice');" + ); + $temptablewait = 1; + } + + # log LSN right before we start changing checksums + my $result = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN before enabling: " . $result . "\n"); + + # Ensure that the primary switches to "inprogress-on" + enable_data_checksums($node_primary, wait => 'inprogress-on'); + + random_sleep() if ($extended); + + # Wait for checksum enable to be replayed + $node_primary->wait_for_catchup($node_standby, 'replay'); + + # Ensure that the standby has switched to "inprogress-on" or "on". + # Normally it would be "inprogress-on", but it is theoretically + # possible for the primary to complete the checksum enabling *and* have + # the standby replay that record before we reach the check below. + $result = $node_standby->poll_query_until( + 'postgres', + "SELECT setting = 'off' " + . "FROM pg_catalog.pg_settings " + . "WHERE name = 'data_checksums';", + 'f'); + is($result, 1, + 'ensure standby has absorbed the inprogress-on barrier'); + $result = $node_standby->safe_psql('postgres', + "SELECT setting " + . "FROM pg_catalog.pg_settings " + . "WHERE name = 'data_checksums';"); + + is(($result eq 'inprogress-on' || $result eq 'on'), + 1, 'ensure checksums are on, or in progress, on standby_1'); + + # Wait for checksums enabled on the primary and standby + wait_for_checksum_state($node_primary, 'on'); + + # log LSN right after the primary flips checksums to "on" + $result = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN after enabling: " . $result . "\n"); + + random_sleep() if ($extended); + wait_for_checksum_state($node_standby, 'on'); + + $node_primary->safe_psql('postgres', + "SELECT injection_points_detach('datachecksumsworker-fake-temptable-wait');" + ) if ($temptablewait); + $data_checksum_state = 'on'; + } + elsif ($data_checksum_state eq 'on') + { + random_sleep() if ($extended); + + # log LSN right before we start changing checksums + my $result = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN before disabling: " . $result . "\n"); + + disable_data_checksums($node_primary); + $node_primary->wait_for_catchup($node_standby, 'replay'); + + # Wait for checksums disabled on the primary and standby + random_sleep() if ($extended); + wait_for_checksum_state($node_primary, 'off'); + wait_for_checksum_state($node_standby, 'off'); + + # log LSN right after the primary flips checksums to "off" + $result = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN after disabling: " . $result . "\n"); + + $data_checksum_state = 'off'; + } + else + { + # This should only happen due to programmer error when hacking on the + # test code, but since that might pass subtly we error out. + BAIL_OUT('data_checksum_state variable has invalid state:' + . $data_checksum_state); + } +} + +# Create and start a cluster with one primary and one standby node, and ensure +# they are caught up and in sync. +$node_primary = PostgreSQL::Test::Cluster->new('pgbench_standby_main'); +$node_primary->init(allows_streaming => 1, no_data_checksums => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node_primary->append_conf( + 'postgresql.conf', + qq[ +max_connections = 30 +log_statement = none +]); +$node_primary->start; +$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +$node_primary->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Create some content to have un-checksummed data in the cluster +$node_primary->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;"); +$node_primary->safe_psql('postgres', + "SELECT pg_create_physical_replication_slot('$node_primary_slot');"); +$node_primary->backup($node_primary_backup); + +$node_standby = PostgreSQL::Test::Cluster->new('pgbench_standby_standby'); +$node_standby->init_from_backup($node_primary, $node_primary_backup, + has_streaming => 1); +$node_standby->append_conf( + 'postgresql.conf', qq[ +primary_slot_name = '$node_primary_slot' +]); +$node_standby->start; + +# Initialize pgbench and wait for the objects to be created on the standby +my $scalefactor = ($extended ? 10 : 1); +$node_primary->command_ok( + [ + 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q', + 'postgres' + ]); +$node_primary->wait_for_catchup($node_standby, 'replay'); + +# Start the test suite with pgbench running on all nodes +background_pgbench($node_standby->port, 1); +background_pgbench($node_primary->port, 0); + +# Main test suite. This loop will start a pgbench run on the cluster and while +# that's running flip the state of data checksums concurrently. It will then +# randomly restart the cluster and then check for +# the desired state. The idea behind doing things randomly is to stress out +# any timing related issues by subjecting the cluster for varied workloads. +# A TODO is to generate a trace such that any test failure can be traced to +# its order of operations for debugging. +for (my $i = 0; $i < $TEST_ITERATIONS; $i++) +{ + note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS); + + if (!$node_primary->is_alive) + { + # start, to do recovery, and stop + $node_primary->start; + $node_primary->stop('fast'); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, + $node_primary_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (during WAL recovery)" + ); + $node_primary_loglocation = -s $node_primary->logfile; + + # randomize the WAL size, to trigger checkpoints less/more often + my $sb = 32 + int(rand(960)); + $node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]); + + note("changing primary max_wal_size to " . $sb); + + $node_primary->start; + + # Start a pgbench in the background against the primary + background_pgbench($node_primary->port, 0); + } + + if (!$node_standby->is_alive) + { + $node_standby->start; + $node_standby->stop('fast'); + + # Since the log isn't being written to now, parse the log and check + # for instances of checksum verification failures. + my $log = + PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, + $node_standby_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in standby_1 log (during WAL recovery)" + ); + $node_standby_loglocation = -s $node_standby->logfile; + + # randomize the WAL size, to trigger checkpoints less/more often + my $sb = 32 + int(rand(960)); + $node_standby->append_conf('postgresql.conf', qq[max_wal_size = $sb]); + + note("changing standby max_wal_size to " . $sb); + + $node_standby->start; + + # Start a read-only pgbench in the background on the standby + background_pgbench($node_standby->port, 1); + } + + $node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;"); + $node_primary->wait_for_catchup($node_standby, 'write'); + + flip_data_checksums(); + random_sleep() if ($extended); + my $result = $node_primary->safe_psql('postgres', + "SELECT count(*) FROM t WHERE a > 1"); + is($result, '100000', 'ensure data pages can be read back on primary'); + random_sleep(); + + # Potentially powercycle the cluster (the nodes independently). A TODO is + # to randomly stop the nodes in the opposite order too. + if ($extended && cointoss()) + { + $node_primary->stop(stopmode()); + + # print the contents of the control file on the primary + PostgreSQL::Test::Utils::system_log("pg_controldata", + $node_primary->data_dir); + + # slurp the file after shutdown, so that it doesn't interfere with the recovery + my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, + $node_primary_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log (outside WAL recovery)" + ); + $node_primary_loglocation = -s $node_primary->logfile; + } + + random_sleep() if ($extended); + + if ($extended && cointoss()) + { + $node_standby->stop(stopmode()); + + # print the contents of the control file on the standby + PostgreSQL::Test::Utils::system_log("pg_controldata", + $node_standby->data_dir); + + # slurp the file after shutdown, so that it doesn't interfere with the recovery + my $log = + PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, + $node_standby_loglocation); + unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in standby_1 log (outside WAL recovery)" + ); + $node_standby_loglocation = -s $node_standby->logfile; + } +} + +# make sure the nodes are running +if (!$node_primary->is_alive) +{ + $node_primary->start; +} + +if (!$node_standby->is_alive) +{ + $node_standby->start; +} + +# Testrun is over, ensure that data reads back as expected and perform a final +# verification of the data checksum state. +my $result = + $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '100000', 'ensure data pages can be read back on primary'); +test_checksum_state($node_primary, $data_checksum_state); +test_checksum_state($node_standby, $data_checksum_state); + +# Perform one final pass over the logs and hunt for unexpected errors +my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile, + $node_primary_loglocation); +unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in primary log"); +$node_primary_loglocation = -s $node_primary->logfile; +$log = PostgreSQL::Test::Utils::slurp_file($node_standby->logfile, + $node_standby_loglocation); +unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in standby_1 log"); +$node_standby_loglocation = -s $node_standby->logfile; + +$node_standby->teardown_node; +$node_primary->teardown_node; + +done_testing(); diff --git a/src/test/modules/test_checksums/t/008_pitr.pl b/src/test/modules/test_checksums/t/008_pitr.pl new file mode 100644 index 0000000000000..e8cb2b0ed96b6 --- /dev/null +++ b/src/test/modules/test_checksums/t/008_pitr.pl @@ -0,0 +1,189 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# This test suite is expensive, or very expensive, to execute. There are two +# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite +# an "checksum_extended" for the full suite. +my $extended = undef; +if ($ENV{PG_TEST_EXTRA}) +{ + $extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/); + plan skip_all => 'Expensive data checksums test disabled' + unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/); +} +else +{ + plan skip_all => 'Expensive data checksums test disabled'; +} + + +my $pgbench = undef; +my $data_checksum_state = 'off'; + +my $node_primary; + +# Invert the state of data checksums in the cluster, if data checksums are on +# then disable them and vice versa. Also performs proper validation of the +# before and after state. +sub flip_data_checksums +{ + my $lsn_pre = undef; + my $lsn_post = undef; + + # First, make sure the cluster is in the state we expect it to be + test_checksum_state($node_primary, $data_checksum_state); + + if ($data_checksum_state eq 'off') + { + # log LSN right before we start changing checksums + $lsn_pre = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN before enabling: " . $lsn_pre . "\n"); + + # Wait for checksums enabled on the primary + enable_data_checksums($node_primary, wait => 'on'); + + # log LSN right after the primary flips checksums to "on" + $lsn_post = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN after enabling: " . $lsn_post . "\n"); + + $data_checksum_state = 'on'; + } + elsif ($data_checksum_state eq 'on') + { + # log LSN right before we start changing checksums + $lsn_pre = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN before disabling: " . $lsn_pre . "\n"); + + # Disable checksums on the primary and wait for completion + disable_data_checksums($node_primary, wait => 1); + + # log LSN right after the primary flips checksums to "off" + $lsn_post = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()"); + note("LSN after disabling: " . $lsn_post . "\n"); + + $data_checksum_state = 'off'; + } + else + { + # This should only happen due to programmer error when hacking on the + # test code, but since that might pass subtly we error out. + BAIL_OUT('data_checksum_state variable has invalid state:' + . $data_checksum_state); + } + + return ($lsn_pre, $lsn_post); +} +# Start a pgbench run in the background against the server specified via the +# port passed as parameter. +sub background_rw_pgbench +{ + my $port = shift; + + # If a previous pgbench is still running, start by shutting it down. + $pgbench->finish if $pgbench; + + # Randomize the number of pgbench clients in extended mode, else 1 client + my $clients = ($extended ? 1 + int(rand(15)) : 1); + my $runtime = ($extended ? 600 : 5); + + my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients); + + # Randomize whether we spawn connections or not + push(@cmd, '-C') if ($extended && cointoss()); + # Finally add the database name to use + push(@cmd, 'postgres'); + + $pgbench = IPC::Run::start( + \@cmd, + '<' => '/dev/null', + '>' => '/dev/null', + '2>' => '/dev/null', + IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default)); +} + +# Start a primary node with WAL archiving enabled and with enough connections +# available to handle pgbench clients. +$node_primary = PostgreSQL::Test::Cluster->new('pitr_main'); +$node_primary->init( + has_archiving => 1, + allows_streaming => 1, + no_data_checksums => 1); +$node_primary->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node_primary->start; + +# Prime the cluster with a bit of known data which we can read back to check +# for data consistency as well as page verification faults in the logfile. +$node_primary->safe_psql('postgres', + 'CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;'); +# Initialize and start pgbench in read/write mode against the cluster +my $scalefactor = ($extended ? 10 : 1); +$node_primary->command_ok( + [ + 'pgbench', '-p', $node_primary->port, '-i', '-s', $scalefactor, '-q', + 'postgres' + ]); +background_rw_pgbench($node_primary->port); + +# Take a backup to use for PITR +my $backup_name = 'my_backup'; +$node_primary->backup($backup_name); + +my ($pre_lsn, $post_lsn) = flip_data_checksums(); + +$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;"); +$node_primary->safe_psql('postgres', "SELECT pg_create_restore_point('a');"); +$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;"); +$node_primary->stop('immediate'); + +my $node_pitr = PostgreSQL::Test::Cluster->new('pitr_backup'); +$node_pitr->init_from_backup( + $node_primary, $backup_name, + standby => 0, + has_restoring => 1); +$node_pitr->append_conf( + 'postgresql.conf', qq{ +recovery_target_lsn = '$post_lsn' +recovery_target_action = 'promote' +recovery_target_inclusive = on +}); + +$node_pitr->start; + +$node_pitr->poll_query_until('postgres', "SELECT pg_is_in_recovery() = 'f';") + or die "Timed out while waiting for PITR promotion"; + +test_checksum_state($node_pitr, $data_checksum_state); +my $result = + $node_pitr->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1"); +is($result, '99999', 'ensure data pages can be read back on primary'); + +$node_pitr->stop; + +my $log = PostgreSQL::Test::Utils::slurp_file($node_pitr->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in pitr log"); + +done_testing(); diff --git a/src/test/modules/test_checksums/t/009_fpi.pl b/src/test/modules/test_checksums/t/009_fpi.pl new file mode 100644 index 0000000000000..a1cea91f78795 --- /dev/null +++ b/src/test/modules/test_checksums/t/009_fpi.pl @@ -0,0 +1,64 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('fpi_node'); +$node->init(allows_streaming => 1, no_data_checksums => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;'); +# Create some content to have un-checksummed data in the cluster +$node->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1, 1000000) AS a;"); + +# Enable data checksums and wait for the state transition to 'on' +enable_data_checksums($node, wait => 'on'); + +$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;'); + +disable_data_checksums($node, wait => 1); + +$node->append_conf('postgresql.conf', 'full_page_writes = off'); +$node->restart; +test_checksum_state($node, 'off'); + +$node->safe_psql('postgres', 'UPDATE t SET a = a + 1;'); +$node->safe_psql('postgres', 'DELETE FROM t WHERE a < 10000;'); + +$node->adjust_conf('postgresql.conf', 'full_page_writes', 'on'); +$node->restart; +test_checksum_state($node, 'off'); + +enable_data_checksums($node, wait => 'on'); + +my $result = $node->safe_psql('postgres', 'SELECT count(*) FROM t;'); +is($result, '990003', 'Reading back all data from table t'); + +$node->stop; +my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile, 0); +unlike( + $log, + qr/page verification failed,.+\d$/, + "no checksum validation errors in server log"); + +done_testing(); diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm new file mode 100644 index 0000000000000..fb704623a6006 --- /dev/null +++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm @@ -0,0 +1,280 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +=pod + +=head1 NAME + +DataChecksums::Utils - Utility functions for testing data checksums in a running cluster + +=head1 SYNOPSIS + + use PostgreSQL::Test::Cluster; + use DataChecksums::Utils qw( .. ); + + # Create, and start, a new cluster + my $node = PostgreSQL::Test::Cluster->new('primary'); + $node->init; + $node->start; + + test_checksum_state($node, 'off'); + + enable_data_checksums($node); + + wait_for_checksum_state($node, 'on'); + + +=cut + +package DataChecksums::Utils; + +use strict; +use warnings FATAL => 'all'; +use Exporter 'import'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +our @EXPORT = qw( + cointoss + disable_data_checksums + enable_data_checksums + random_sleep + stopmode + test_checksum_state + wait_for_checksum_state + wait_for_cluster_crash +); + +=pod + +=head1 METHODS + +=over + +=item test_checksum_state(node, state) + +Test that the current value of the data checksum GUC in the server running +at B matches B. If the values differ, a test failure is logged. +Returns True if the values match, otherwise False. + +=cut + +sub test_checksum_state +{ + my ($postgresnode, $state) = @_; + + my $result = $postgresnode->safe_psql('postgres', + "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';" + ); + is($result, $state, 'ensure checksums are set to ' . $state); + return $result eq $state; +} + +=item wait_for_checksum_state(node, state) + +Test the value of the data checksum GUC in the server running at B +repeatedly until it matches B or times out. Processing will run for +$PostgreSQL::Test::Utils::timeout_default seconds before timing out. If the +values differ when the process times out, False is returned and a test failure +is logged, otherwise True. + +=cut + +sub wait_for_checksum_state +{ + my ($postgresnode, $state) = @_; + + my $res = $postgresnode->poll_query_until( + 'postgres', + "SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';", + $state); + is($res, 1, 'ensure data checksums are transitioned to ' . $state); + return $res == 1; +} + +=item wait_for_cluster_crash(node, params) + +Repeatedly test if the cluster running at B responds to connections +and return when it no longer does so, or when it times out. Processing will +run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout +value is specified as a parameter. Returns True if the cluster crashed, else +False if the process timed out. + +=over + +=item timeout + +Approximate number of seconds to wait for cluster to crash, default is +$PostgreSQL::Test::Utils::timeout_default. There are no real-time guarantees +that the total process time won't exceed the timeout. + +=back + +=cut + +sub wait_for_cluster_crash +{ + my $postgresnode = shift; + my %params = @_; + my $crash = 0; + + $params{timeout} = $PostgreSQL::Test::Utils::timeout_default + unless (defined($params{timeout})); + + for (my $naps = 0; $naps < $params{timeout}; $naps++) + { + if (!$postgresnode->is_alive) + { + $crash = 1; + last; + } + sleep(1); + } + + return $crash == 1; +} + +=item enable_data_checksums($node, %params) + +Function for enabling data checksums in the cluster running at B. + +=over + +=item cost_delay + +The B to use when enabling data checksums, default is 0. + +=item cost_limit + +The B to use when enabling data checksums, default is 100. + +=item wait + +If defined, the function will wait for the state defined in this parameter, +waiting timing out, before returning. The function will wait for +$PostgreSQL::Test::Utils::timeout_default seconds before timing out. + +=back + +=cut + +sub enable_data_checksums +{ + my $postgresnode = shift; + my %params = @_; + + # Set sane defaults for the parameters + $params{cost_delay} = 0 unless (defined($params{cost_delay})); + $params{cost_limit} = 100 unless (defined($params{cost_limit})); + + my $query = <<'EOQ'; +SELECT pg_enable_data_checksums(%s, %s); +EOQ + + $postgresnode->safe_psql('postgres', + sprintf($query, $params{cost_delay}, $params{cost_limit})); + + if (defined($params{wait})) + { + wait_for_checksum_state($postgresnode, $params{wait}); + # If we are tasked with waiting for an end state, also wait for the + # launcher to exit. + if ($params{wait} eq 'on' || $params{wait} eq 'off') + { + $postgresnode->poll_query_until('postgres', + "SELECT count(*) = 0 " + . "FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum launcher';"); + } + } +} + +=item disable_data_checksums($node, %params) + +Function for disabling data checksums in the cluster running at B. + +=over + +=item wait + +If defined, the function will wait for the state to turn to B, or +waiting timing out, before returning. The function will wait for +$PostgreSQL::Test::Utils::timeout_default seconds before timing out. +Unlike in C the value of the parameter is discarded. + +=back + +=cut + +sub disable_data_checksums +{ + my $postgresnode = shift; + my %params = @_; + + $postgresnode->safe_psql('postgres', + 'SELECT pg_disable_data_checksums();'); + + if (defined($params{wait})) + { + wait_for_checksum_state($postgresnode, 'off'); + $postgresnode->poll_query_until('postgres', + "SELECT count(*) = 0 " + . "FROM pg_catalog.pg_stat_activity " + . "WHERE backend_type = 'datachecksum launcher';"); + } +} + +=item cointoss + +Helper for retrieving a binary value with random distribution for deciding +whether to turn things off during testing. + +=back + +=cut + +sub cointoss +{ + return int(rand() < 0.5); +} + +=item random_sleep(max) + +Helper for injecting random sleeps here and there in the testrun. The sleep +duration will be in the range (0,B), but won't be predictable in order to +avoid sleep patterns that manage to avoid race conditions and timing bugs. +The default B is 3 seconds. + +=back + +=cut + +sub random_sleep +{ + my $max = shift; + return if (defined($max) && ($max == 0)); + sleep(int(rand(defined($max) ? $max : 3))) if cointoss; +} + +=item stopmode + +Small helper function for randomly selecting a valid stopmode. + +=back + +=cut + +sub stopmode +{ + return 'immediate' if (cointoss); + return 'fast'; +} + +=pod + +=back + +=cut + +1; diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql new file mode 100644 index 0000000000000..c674fee02bbb4 --- /dev/null +++ b/src/test/modules/test_checksums/test_checksums--1.0.sql @@ -0,0 +1,16 @@ +/* src/test/modules/test_checksums/test_checksums--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit + +CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true) + RETURNS pg_catalog.void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true) + RETURNS pg_catalog.void + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true) + RETURNS pg_catalog.void + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c new file mode 100644 index 0000000000000..621cf788dadbd --- /dev/null +++ b/src/test/modules/test_checksums/test_checksums.c @@ -0,0 +1,105 @@ +/*-------------------------------------------------------------------------- + * + * test_checksums.c + * Test data checksums + * + * Copyright (c) 2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_checksums/test_checksums.c + * + * ------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "funcapi.h" +#include "miscadmin.h" +#include "postmaster/datachecksum_state.h" +#include "storage/latch.h" +#include "utils/injection_point.h" +#include "utils/wait_event.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg); + +/* + * Test for delaying emission of procsignalbarriers. + */ +void +dc_delay_barrier(const char *name, const void *private_data, void *arg) +{ + (void) name; + (void) private_data; + + (void) WaitLatch(MyLatch, + WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + (3 * 1000), + WAIT_EVENT_PG_SLEEP); +} + +PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier); +Datum +dcw_inject_delay_barrier(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + bool attach = PG_GETARG_BOOL(0); + + if (attach) + InjectionPointAttach("datachecksums-enable-checksums-delay", + "test_checksums", + "dc_delay_barrier", + NULL, + 0); + else + InjectionPointDetach("datachecksums-enable-checksums-delay"); +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay); +Datum +dcw_inject_launcher_delay(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + bool attach = PG_GETARG_BOOL(0); + + if (attach) + InjectionPointAttach("datachecksumsworker-launcher-delay", + "test_checksums", + "dc_delay_barrier", + NULL, + 0); + else + InjectionPointDetach("datachecksumsworker-launcher-delay"); +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(dcw_inject_startup_delay); +Datum +dcw_inject_startup_delay(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + bool attach = PG_GETARG_BOOL(0); + + if (attach) + InjectionPointAttach("datachecksumsworker-startup-delay", + "test_checksums", + "dc_delay_barrier", + NULL, + 0); + else + InjectionPointDetach("datachecksumsworker-startup-delay"); +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control new file mode 100644 index 0000000000000..84b4cc035a788 --- /dev/null +++ b/src/test/modules/test_checksums/test_checksums.control @@ -0,0 +1,4 @@ +comment = 'Test code for data checksums' +default_version = '1.0' +module_pathname = '$libdir/test_checksums' +relocatable = true diff --git a/src/test/modules/test_cloexec/.gitignore b/src/test/modules/test_cloexec/.gitignore new file mode 100644 index 0000000000000..0234e9d8406f2 --- /dev/null +++ b/src/test/modules/test_cloexec/.gitignore @@ -0,0 +1,3 @@ +/test_cloexec +# Generated subdirectories +/tmp_check/ diff --git a/src/test/modules/test_cloexec/Makefile b/src/test/modules/test_cloexec/Makefile new file mode 100644 index 0000000000000..db3876f21e4a1 --- /dev/null +++ b/src/test/modules/test_cloexec/Makefile @@ -0,0 +1,21 @@ +# src/test/modules/test_cloexec/Makefile + +PGFILEDESC = "test_cloexec - test O_CLOEXEC flag handling" +PGAPPICON = win32 + +PROGRAM = test_cloexec +OBJS = $(WIN32RES) test_cloexec.o + +NO_INSTALLCHECK = 1 +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_cloexec +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_cloexec/meson.build b/src/test/modules/test_cloexec/meson.build new file mode 100644 index 0000000000000..63c8658b04e99 --- /dev/null +++ b/src/test/modules/test_cloexec/meson.build @@ -0,0 +1,26 @@ +# src/test/modules/test_cloexec/meson.build + +# This test is Windows-only +if host_system != 'windows' + subdir_done() +endif + +test_cloexec_sources = files('test_cloexec.c') + +test_cloexec = executable('test_cloexec', + test_cloexec_sources, + dependencies: [frontend_code], + link_with: [pgport[''], pgcommon['']], + kwargs: default_bin_args, +) + +tests += { + 'name': 'test_cloexec', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_cloexec.pl', + ], + }, +} diff --git a/src/test/modules/test_cloexec/t/001_cloexec.pl b/src/test/modules/test_cloexec/t/001_cloexec.pl new file mode 100644 index 0000000000000..5cea63d982b10 --- /dev/null +++ b/src/test/modules/test_cloexec/t/001_cloexec.pl @@ -0,0 +1,60 @@ +# Test O_CLOEXEC flag handling on Windows +# +# This test verifies that file handles opened with O_CLOEXEC are not +# inherited by child processes, while handles opened without O_CLOEXEC +# are inherited. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Utils; +use Test::More; +use IPC::Run qw(run); +use File::Spec; +use Cwd 'abs_path'; + +if (!$PostgreSQL::Test::Utils::windows_os) +{ + plan skip_all => 'test is Windows-specific'; +} + +plan tests => 1; + +my $test_prog; +foreach my $dir (split(/$Config::Config{path_sep}/, $ENV{PATH})) +{ + my $candidate = File::Spec->catfile($dir, 'test_cloexec.exe'); + if (-f $candidate && -x $candidate) + { + $test_prog = $candidate; + last; + } +} + +if (!$test_prog) +{ + $test_prog = './test_cloexec.exe'; +} + +if (!-f $test_prog) +{ + BAIL_OUT("test program not found: $test_prog"); +} + +note("Using test program: $test_prog"); + +my ($stdout, $stderr); +my $result = run [ $test_prog ], '>', \$stdout, '2>', \$stderr; + +note("Test program output:"); +note($stdout) if $stdout; + +if ($stderr) +{ + diag("Test program stderr:"); + diag($stderr); +} + +ok($result && $stdout =~ /SUCCESS.*O_CLOEXEC behavior verified/s, + "O_CLOEXEC prevents handle inheritance"); + +done_testing(); diff --git a/src/test/modules/test_cloexec/test_cloexec.c b/src/test/modules/test_cloexec/test_cloexec.c new file mode 100644 index 0000000000000..40b00e490d75b --- /dev/null +++ b/src/test/modules/test_cloexec/test_cloexec.c @@ -0,0 +1,240 @@ +/*------------------------------------------------------------------------- + * + * test_cloexec.c + * Test O_CLOEXEC flag handling on Windows + * + * This program tests that: + * 1. File handles opened with O_CLOEXEC are NOT inherited by child processes + * 2. File handles opened without O_CLOEXEC ARE inherited by child processes + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include +#include + +#ifdef WIN32 +#include +#endif + +#include "common/file_utils.h" +#include "port.h" + +#ifdef WIN32 +static void run_parent_tests(const char *testfile1, const char *testfile2); +static void run_child_tests(const char *handle1_str, const char *handle2_str); +static bool try_write_to_handle(HANDLE h, const char *label); +#endif + +int +main(int argc, char *argv[]) +{ + /* Windows-only test */ +#ifndef WIN32 + fprintf(stderr, "This test only runs on Windows\n"); + return 0; +#else + char testfile1[MAXPGPATH]; + char testfile2[MAXPGPATH]; + + if (argc == 3) + { + /* + * Child mode: receives two handle values as hex strings and attempts + * to write to them. + */ + run_child_tests(argv[1], argv[2]); + return 0; + } + else if (argc == 1) + { + /* Parent mode: opens files and spawns child */ + snprintf(testfile1, sizeof(testfile1), "test_cloexec_1_%d.tmp", (int) getpid()); + snprintf(testfile2, sizeof(testfile2), "test_cloexec_2_%d.tmp", (int) getpid()); + + run_parent_tests(testfile1, testfile2); + + /* Clean up test files */ + unlink(testfile1); + unlink(testfile2); + + return 0; + } + else + { + fprintf(stderr, "Usage: %s [handle1_hex handle2_hex]\n", argv[0]); + return 1; + } +#endif +} + +#ifdef WIN32 +static void +run_parent_tests(const char *testfile1, const char *testfile2) +{ + int fd1, + fd2; + HANDLE h1, + h2; + char exe_path[MAXPGPATH]; + char cmdline[MAXPGPATH + 100]; + STARTUPINFO si = {.cb = sizeof(si)}; + PROCESS_INFORMATION pi = {0}; + DWORD exit_code; + + printf("Parent: Opening test files...\n"); + + /* Open first file WITH O_CLOEXEC - should NOT be inherited */ + fd1 = open(testfile1, O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (fd1 < 0) + { + fprintf(stderr, "Failed to open %s: %s\n", testfile1, strerror(errno)); + exit(1); + } + + /* Open second file WITHOUT O_CLOEXEC - should be inherited */ + fd2 = open(testfile2, O_RDWR | O_CREAT | O_TRUNC, 0600); + if (fd2 < 0) + { + fprintf(stderr, "Failed to open %s: %s\n", testfile2, strerror(errno)); + close(fd1); + exit(1); + } + + /* Get Windows HANDLEs from file descriptors */ + h1 = (HANDLE) _get_osfhandle(fd1); + h2 = (HANDLE) _get_osfhandle(fd2); + + if (h1 == INVALID_HANDLE_VALUE || h2 == INVALID_HANDLE_VALUE) + { + fprintf(stderr, "Failed to get OS handles\n"); + close(fd1); + close(fd2); + exit(1); + } + + printf("Parent: fd1=%d (O_CLOEXEC) -> HANDLE=%p\n", fd1, h1); + printf("Parent: fd2=%d (no O_CLOEXEC) -> HANDLE=%p\n", fd2, h2); + + /* + * Find the actual executable path by removing any arguments from + * GetCommandLine(), and add our new arguments. + */ + GetModuleFileName(NULL, exe_path, sizeof(exe_path)); + snprintf(cmdline, sizeof(cmdline), "\"%s\" %p %p", exe_path, h1, h2); + + printf("Parent: Spawning child process...\n"); + printf("Parent: Command line: %s\n", cmdline); + + if (!CreateProcess(NULL, /* application name */ + cmdline, /* command line */ + NULL, /* process security attributes */ + NULL, /* thread security attributes */ + TRUE, /* bInheritHandles - CRITICAL! */ + 0, /* creation flags */ + NULL, /* environment */ + NULL, /* current directory */ + &si, /* startup info */ + &pi)) /* process information */ + { + fprintf(stderr, "CreateProcess failed: %lu\n", GetLastError()); + close(fd1); + close(fd2); + exit(1); + } + + printf("Parent: Waiting for child process...\n"); + + /* Wait for child to complete */ + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exit_code); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + close(fd1); + close(fd2); + + printf("Parent: Child exit code: %lu\n", exit_code); + + if (exit_code == 0) + { + printf("Parent: SUCCESS - O_CLOEXEC behavior verified\n"); + } + else + { + printf("Parent: FAILURE - O_CLOEXEC not working correctly\n"); + exit(1); + } +} + +static void +run_child_tests(const char *handle1_str, const char *handle2_str) +{ + HANDLE h1, + h2; + bool h1_worked, + h2_worked; + + /* Parse handle values from hex strings */ + if (sscanf(handle1_str, "%p", &h1) != 1 || + sscanf(handle2_str, "%p", &h2) != 1) + { + fprintf(stderr, "Child: Failed to parse handle values\n"); + exit(1); + } + + printf("Child: Received HANDLE1=%p (should fail - O_CLOEXEC)\n", h1); + printf("Child: Received HANDLE2=%p (should work - no O_CLOEXEC)\n", h2); + + /* Try to write to both handles */ + h1_worked = try_write_to_handle(h1, "HANDLE1"); + h2_worked = try_write_to_handle(h2, "HANDLE2"); + + printf("Child: HANDLE1 (O_CLOEXEC): %s\n", + h1_worked ? "ACCESSIBLE (BAD!)" : "NOT ACCESSIBLE (GOOD!)"); + printf("Child: HANDLE2 (no O_CLOEXEC): %s\n", + h2_worked ? "ACCESSIBLE (GOOD!)" : "NOT ACCESSIBLE (BAD!)"); + + /* + * For O_CLOEXEC to work correctly, h1 should NOT be accessible (h1_worked + * == false) and h2 SHOULD be accessible (h2_worked == true). + */ + if (!h1_worked && h2_worked) + { + printf("Child: Test PASSED - O_CLOEXEC working correctly\n"); + exit(0); + } + else + { + printf("Child: Test FAILED - O_CLOEXEC not working correctly\n"); + exit(1); + } +} + +static bool +try_write_to_handle(HANDLE h, const char *label) +{ + const char *test_data = "test\n"; + DWORD bytes_written; + BOOL result; + + result = WriteFile(h, test_data, strlen(test_data), &bytes_written, NULL); + + if (result && bytes_written == strlen(test_data)) + { + printf("Child: Successfully wrote to %s\n", label); + return true; + } + else + { + printf("Child: Failed to write to %s (error %lu)\n", + label, GetLastError()); + return false; + } +} +#endif diff --git a/src/test/modules/test_copy_callbacks/meson.build b/src/test/modules/test_copy_callbacks/meson.build index 2af31b19f254e..66bf6ad8bf386 100644 --- a/src/test/modules/test_copy_callbacks/meson.build +++ b/src/test/modules/test_copy_callbacks/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_copy_callbacks_sources = files( 'test_copy_callbacks.c', diff --git a/src/test/modules/test_copy_callbacks/test_copy_callbacks.c b/src/test/modules/test_copy_callbacks/test_copy_callbacks.c index 41113a8acdcff..f6b113e3e9826 100644 --- a/src/test/modules/test_copy_callbacks/test_copy_callbacks.c +++ b/src/test/modules/test_copy_callbacks/test_copy_callbacks.c @@ -3,7 +3,7 @@ * test_copy_callbacks.c * Code for testing COPY callbacks. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/test/modules/test_cplusplusext/.gitignore b/src/test/modules/test_cplusplusext/.gitignore new file mode 100644 index 0000000000000..913175ff6e63a --- /dev/null +++ b/src/test/modules/test_cplusplusext/.gitignore @@ -0,0 +1,3 @@ +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_cplusplusext/Makefile b/src/test/modules/test_cplusplusext/Makefile new file mode 100644 index 0000000000000..88cd4403823dc --- /dev/null +++ b/src/test/modules/test_cplusplusext/Makefile @@ -0,0 +1,26 @@ +# src/test/modules/test_cplusplusext/Makefile + +MODULE_big = test_cplusplusext +OBJS = \ + $(WIN32RES) \ + test_cplusplusext.o +PGFILEDESC = "test_cplusplusext - test C++ compatibility of PostgreSQL headers" + +EXTENSION = test_cplusplusext +DATA = test_cplusplusext--1.0.sql + +REGRESS = test_cplusplusext + +# Use C++ compiler for linking because this module includes C++ files +override COMPILER = $(CXX) $(CXXFLAGS) + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_cplusplusext +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_cplusplusext/README b/src/test/modules/test_cplusplusext/README new file mode 100644 index 0000000000000..8e4090edf0125 --- /dev/null +++ b/src/test/modules/test_cplusplusext/README @@ -0,0 +1,10 @@ +test_cplusplusext - Test C++ Extension Compatibility +==================================================== + +This test module verifies that PostgreSQL headers and macros work +correctly when compiled with a C++ compiler. + +While PostgreSQL already tests that headers are syntactically valid +C++ using headerscheck, the macros defined in those headers might +still expand to invalid C++ code. This module catches such issues by +actually compiling and running an extension that's written in C++. diff --git a/src/test/modules/test_cplusplusext/expected/test_cplusplusext.out b/src/test/modules/test_cplusplusext/expected/test_cplusplusext.out new file mode 100644 index 0000000000000..ab0b04b5c5e1a --- /dev/null +++ b/src/test/modules/test_cplusplusext/expected/test_cplusplusext.out @@ -0,0 +1,7 @@ +CREATE EXTENSION test_cplusplusext; +SELECT test_cplusplus_add(1, 2); + test_cplusplus_add +-------------------- + 3 +(1 row) + diff --git a/src/test/modules/test_cplusplusext/meson.build b/src/test/modules/test_cplusplusext/meson.build new file mode 100644 index 0000000000000..5860464a5032c --- /dev/null +++ b/src/test/modules/test_cplusplusext/meson.build @@ -0,0 +1,37 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +if not have_cxx or not have_cxx_desinit + subdir_done() +endif + +test_cplusplusext_sources = files( + 'test_cplusplusext.cpp', +) + +if host_system == 'windows' + test_cplusplusext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_cplusplusext', + '--FILEDESC', 'test_cplusplusext - test C++ compatibility of PostgreSQL headers',]) +endif + +test_cplusplusext = shared_module('test_cplusplusext', + test_cplusplusext_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_cplusplusext + +test_install_data += files( + 'test_cplusplusext.control', + 'test_cplusplusext--1.0.sql', +) + +tests += { + 'name': 'test_cplusplusext', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_cplusplusext', + ], + }, +} diff --git a/src/test/modules/test_cplusplusext/sql/test_cplusplusext.sql b/src/test/modules/test_cplusplusext/sql/test_cplusplusext.sql new file mode 100644 index 0000000000000..a41682417ae4c --- /dev/null +++ b/src/test/modules/test_cplusplusext/sql/test_cplusplusext.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION test_cplusplusext; + +SELECT test_cplusplus_add(1, 2); diff --git a/src/test/modules/test_cplusplusext/test_cplusplusext--1.0.sql b/src/test/modules/test_cplusplusext/test_cplusplusext--1.0.sql new file mode 100644 index 0000000000000..c54acb768236f --- /dev/null +++ b/src/test/modules/test_cplusplusext/test_cplusplusext--1.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/test_cplusplusext/test_cplusplusext--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_cplusplusext" to load this file. \quit + +CREATE FUNCTION test_cplusplus_add(int4, int4) RETURNS int4 +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; diff --git a/src/test/modules/test_cplusplusext/test_cplusplusext.control b/src/test/modules/test_cplusplusext/test_cplusplusext.control new file mode 100644 index 0000000000000..640a0a51f3584 --- /dev/null +++ b/src/test/modules/test_cplusplusext/test_cplusplusext.control @@ -0,0 +1,4 @@ +comment = 'Test module for C++ extension compatibility' +default_version = '1.0' +module_pathname = '$libdir/test_cplusplusext' +relocatable = true diff --git a/src/test/modules/test_cplusplusext/test_cplusplusext.cpp b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp new file mode 100644 index 0000000000000..93cd7dd07f702 --- /dev/null +++ b/src/test/modules/test_cplusplusext/test_cplusplusext.cpp @@ -0,0 +1,72 @@ +/*-------------------------------------------------------------------------- + * + * test_cplusplusext.cpp + * Test that PostgreSQL headers compile with a C++ compiler. + * + * This file is compiled with a C++ compiler to verify that PostgreSQL + * headers remain compatible with C++ extensions. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_cplusplusext/test_cplusplusext.cpp + * + * ------------------------------------------------------------------------- + */ + +extern "C" { +#include "postgres.h" +#include "fmgr.h" +#include "nodes/pg_list.h" +#include "nodes/primnodes.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_cplusplus_add); +} + +StaticAssertDecl(sizeof(int32) == 4, "int32 should be 4 bytes"); + +/* + * Simple function that returns the sum of two integers. This verifies that + * C++ extension modules can be loaded and called correctly at runtime. + */ +extern "C" Datum +test_cplusplus_add(PG_FUNCTION_ARGS) +{ + int32 a = PG_GETARG_INT32(0); + int32 b = PG_GETARG_INT32(1); + RangeTblRef *node = makeNode(RangeTblRef); + const RangeTblRef *nodec = node; + RangeTblRef *copy = copyObject(nodec); + List *list = list_make1(node); + + foreach_ptr(RangeTblRef, rtr, list) + { + (void) rtr; + } + + foreach_node(RangeTblRef, rtr, list) + { + (void) rtr; + } + + StaticAssertStmt(sizeof(int32) == 4, "int32 should be 4 bytes"); + (void) StaticAssertExpr(sizeof(int64) == 8, "int64 should be 8 bytes"); + + list_free(list); + pfree(node); + pfree(copy); + + switch (a) + { + case 1: + elog(DEBUG1, "1"); + pg_fallthrough; + case 2: + elog(DEBUG1, "2"); + break; + } + + PG_RETURN_INT32(a + b); +} diff --git a/src/test/modules/test_custom_rmgrs/meson.build b/src/test/modules/test_custom_rmgrs/meson.build index 6869c534c7421..ef26d24a1baee 100644 --- a/src/test/modules/test_custom_rmgrs/meson.build +++ b/src/test/modules/test_custom_rmgrs/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_custom_rmgrs_sources = files( 'test_custom_rmgrs.c', diff --git a/src/test/modules/test_custom_rmgrs/t/001_basic.pl b/src/test/modules/test_custom_rmgrs/t/001_basic.pl index a1e0e1ba678e9..c75d6a56bff6f 100644 --- a/src/test/modules/test_custom_rmgrs/t/001_basic.pl +++ b/src/test/modules/test_custom_rmgrs/t/001_basic.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/modules/test_custom_rmgrs/test_custom_rmgrs.c b/src/test/modules/test_custom_rmgrs/test_custom_rmgrs.c index 1a424ad55a8d8..6333661e4378f 100644 --- a/src/test/modules/test_custom_rmgrs/test_custom_rmgrs.c +++ b/src/test/modules/test_custom_rmgrs/test_custom_rmgrs.c @@ -3,7 +3,7 @@ * test_custom_rmgrs.c * Code for testing custom WAL resource managers. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/test/modules/test_custom_stats/.gitignore b/src/test/modules/test_custom_stats/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_custom_stats/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_custom_stats/Makefile b/src/test/modules/test_custom_stats/Makefile new file mode 100644 index 0000000000000..324020d061abc --- /dev/null +++ b/src/test/modules/test_custom_stats/Makefile @@ -0,0 +1,27 @@ +# src/test/modules/test_custom_stats/Makefile + +MODULES = test_custom_var_stats test_custom_fixed_stats + +EXTENSION = test_custom_var_stats test_custom_fixed_stats + +OBJS = \ + $(WIN32RES) \ + test_custom_var_stats.o \ + test_custom_fixed_stats.o +PGFILEDESC = "test_custom_stats - custom pgstats" + +DATA = test_custom_var_stats--1.0.sql \ + test_custom_fixed_stats--1.0.sql + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_custom_stats +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_custom_stats/meson.build b/src/test/modules/test_custom_stats/meson.build new file mode 100644 index 0000000000000..e458f6bc65ff3 --- /dev/null +++ b/src/test/modules/test_custom_stats/meson.build @@ -0,0 +1,55 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +test_custom_var_stats_sources = files( + 'test_custom_var_stats.c', +) + +if host_system == 'windows' + test_custom_var_stats_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_var_stats', + '--FILEDESC', 'test_custom_var_stats - variable-sized custom pgstats',]) +endif + +test_custom_var_stats = shared_module('test_custom_var_stats', + test_custom_var_stats_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_var_stats + +test_install_data += files( + 'test_custom_var_stats.control', + 'test_custom_var_stats--1.0.sql', +) + +test_custom_fixed_stats_sources = files( + 'test_custom_fixed_stats.c', +) + +if host_system == 'windows' + test_custom_fixed_stats_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_fixed_stats', + '--FILEDESC', 'test_custom_fixed_stats - fixed-sized custom pgstats',]) +endif + +test_custom_fixed_stats = shared_module('test_custom_fixed_stats', + test_custom_fixed_stats_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_fixed_stats + +test_install_data += files( + 'test_custom_fixed_stats.control', + 'test_custom_fixed_stats--1.0.sql', +) + +tests += { + 'name': 'test_custom_stats', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_custom_stats.pl', + ], + 'runningcheck': false, + }, +} diff --git a/src/test/modules/test_custom_stats/t/001_custom_stats.pl b/src/test/modules/test_custom_stats/t/001_custom_stats.pl new file mode 100644 index 0000000000000..9e6a7a3857752 --- /dev/null +++ b/src/test/modules/test_custom_stats/t/001_custom_stats.pl @@ -0,0 +1,160 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Test custom pgstats functionality +# +# This script includes tests for both variable and fixed-sized custom +# pgstats: +# - Creation, updates, and reporting. +# - Persistence across restarts. +# - Loss after crash recovery. +# - Resets for fixed-sized stats. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use File::Copy; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', + "shared_preload_libraries = 'test_custom_var_stats, test_custom_fixed_stats'" +); +$node->start; + +$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_var_stats)); +$node->safe_psql('postgres', q(CREATE EXTENSION test_custom_fixed_stats)); + +# Create entries for variable-sized stats. +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry1', 'Test entry 1'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry2', 'Test entry 2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry3', 'Test entry 3'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_create('entry4', 'Test entry 4'))); + +# Update counters: entry1=2, entry2=3, entry3=2, entry4=3, fixed=3 +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry1'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry1'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry2'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry3'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry3'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry4'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry4'))); +$node->safe_psql('postgres', + q(select test_custom_stats_var_update('entry4'))); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); + +# Test data reports. +my $result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry1'))); +is( $result, + "entry1|2|Test entry 1", + "report for variable-sized data of entry1"); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry2'))); +is( $result, + "entry2|3|Test entry 2", + "report for variable-sized data of entry2"); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry3'))); +is( $result, + "entry3|2|Test entry 3", + "report for variable-sized data of entry3"); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry4'))); +is( $result, + "entry4|3|Test entry 4", + "report for variable-sized data of entry4"); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_fixed_report())); +is($result, "3|", "report for fixed-sized stats"); + +# Test drop of variable-sized stats. +$node->safe_psql('postgres', + q(select * from test_custom_stats_var_drop('entry3'))); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry3'))); +is($result, "", "entry3 not found after drop"); +$node->safe_psql('postgres', + q(select * from test_custom_stats_var_drop('entry4'))); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry4'))); +is($result, "", "entry4 not found after drop"); + +# Test persistence across clean restart. +$node->stop(); +$node->start(); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry1'))); +is( $result, + "entry1|2|Test entry 1", + "variable-sized stats persist after clean restart"); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry2'))); +is( $result, + "entry2|3|Test entry 2", + "variable-sized stats persist after clean restart"); + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_fixed_report())); +is($result, "3|", "fixed-sized stats persist after clean restart"); + +# Test persistence after crash recovery. +$node->stop('immediate'); +$node->start; + +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry1'))); +is($result, "", "variable-sized stats of entry1 lost after crash recovery"); +$result = $node->safe_psql('postgres', + q(select * from test_custom_stats_var_report('entry2'))); +is($result, "", "variable-sized stats of entry2 lost after crash recovery"); + +# Crash recovery sets the reset timestamp. +$result = $node->safe_psql('postgres', + q(select numcalls from test_custom_stats_fixed_report() where stats_reset is not null) +); +is($result, "0", "fixed-sized stats are reset after crash recovery"); + +# Test reset of fixed-sized stats. +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); +$node->safe_psql('postgres', q(select test_custom_stats_fixed_update())); + +$result = $node->safe_psql('postgres', + q(select numcalls from test_custom_stats_fixed_report())); +is($result, "3", "report of fixed-sized before manual reset"); + +$node->safe_psql('postgres', q(select test_custom_stats_fixed_reset())); + +$result = $node->safe_psql('postgres', + q(select numcalls from test_custom_stats_fixed_report() where stats_reset is not null) +); +is($result, "0", "report of fixed-sized after manual reset"); + +# Test completed successfully +done_testing(); diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql new file mode 100644 index 0000000000000..69a93b5241f5a --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql @@ -0,0 +1,20 @@ +/* src/test/modules/test_custom_stats/test_custom_fixed_stats--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_fixed_stats" to load this file. \quit + +CREATE FUNCTION test_custom_stats_fixed_update() +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_update' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_fixed_report(OUT numcalls bigint, + OUT stats_reset timestamptz) +RETURNS record +AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_report' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_fixed_reset() +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_fixed_reset' +LANGUAGE C STRICT PARALLEL UNSAFE; diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.c b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c new file mode 100644 index 0000000000000..a066ce117a6d4 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.c @@ -0,0 +1,226 @@ +/*-------------------------------------------------------------------------- + * + * test_custom_fixed_stats.c + * Test module for fixed-sized custom pgstats + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_stats/test_custom_fixed_stats.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "funcapi.h" +#include "pgstat.h" +#include "utils/builtins.h" +#include "utils/pgstat_internal.h" +#include "utils/timestamp.h" + +PG_MODULE_MAGIC_EXT( + .name = "test_custom_fixed_stats", + .version = PG_VERSION +); + +/* Fixed-amount custom statistics entry */ +typedef struct PgStat_StatCustomFixedEntry +{ + PgStat_Counter numcalls; /* # of times update function called */ + TimestampTz stat_reset_timestamp; +} PgStat_StatCustomFixedEntry; + +typedef struct PgStatShared_CustomFixedEntry +{ + LWLock lock; /* protects counters */ + uint32 changecount; /* for atomic reads */ + PgStat_StatCustomFixedEntry stats; /* current counters */ + PgStat_StatCustomFixedEntry reset_offset; /* reset baseline */ +} PgStatShared_CustomFixedEntry; + +/* Callbacks for fixed-amount statistics */ +static void test_custom_stats_fixed_init_shmem_cb(void *stats); +static void test_custom_stats_fixed_reset_all_cb(TimestampTz ts); +static void test_custom_stats_fixed_snapshot_cb(void); + +static const PgStat_KindInfo custom_stats = { + .name = "test_custom_fixed_stats", + .fixed_amount = true, /* exactly one entry */ + .write_to_file = true, /* persist to stats file */ + + .shared_size = sizeof(PgStatShared_CustomFixedEntry), + .shared_data_off = offsetof(PgStatShared_CustomFixedEntry, stats), + .shared_data_len = sizeof(((PgStatShared_CustomFixedEntry *) 0)->stats), + + .init_shmem_cb = test_custom_stats_fixed_init_shmem_cb, + .reset_all_cb = test_custom_stats_fixed_reset_all_cb, + .snapshot_cb = test_custom_stats_fixed_snapshot_cb, +}; + +/* + * Kind ID for test_custom_fixed_stats. + */ +#define PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS 26 + +/*-------------------------------------------------------------------------- + * Module initialization + *-------------------------------------------------------------------------- + */ + +void +_PG_init(void) +{ + /* Must be loaded via shared_preload_libraries */ + if (!process_shared_preload_libraries_in_progress) + return; + + /* Register custom statistics kind */ + pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS, &custom_stats); +} + +/* + * test_custom_stats_fixed_init_shmem_cb + * Initialize shared memory structure + */ +static void +test_custom_stats_fixed_init_shmem_cb(void *stats) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + (PgStatShared_CustomFixedEntry *) stats; + + LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA); +} + +/* + * test_custom_stats_fixed_reset_all_cb + * Reset the fixed-sized stats + */ +static void +test_custom_stats_fixed_reset_all_cb(TimestampTz ts) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + /* see explanation above PgStatShared_Archiver for the reset protocol */ + LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); + pgstat_copy_changecounted_stats(&stats_shmem->reset_offset, + &stats_shmem->stats, + sizeof(stats_shmem->stats), + &stats_shmem->changecount); + stats_shmem->stats.stat_reset_timestamp = ts; + LWLockRelease(&stats_shmem->lock); +} + +/* + * test_custom_stats_fixed_snapshot_cb + * Copy current stats to snapshot area + */ +static void +test_custom_stats_fixed_snapshot_cb(void) +{ + PgStatShared_CustomFixedEntry *stats_shmem = + pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + PgStat_StatCustomFixedEntry *stat_snap = + pgstat_get_custom_snapshot_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + PgStat_StatCustomFixedEntry *reset_offset = &stats_shmem->reset_offset; + PgStat_StatCustomFixedEntry reset; + + pgstat_copy_changecounted_stats(stat_snap, + &stats_shmem->stats, + sizeof(stats_shmem->stats), + &stats_shmem->changecount); + + LWLockAcquire(&stats_shmem->lock, LW_SHARED); + memcpy(&reset, reset_offset, sizeof(stats_shmem->stats)); + LWLockRelease(&stats_shmem->lock); + + /* Apply reset offsets */ +#define FIXED_COMP(fld) stat_snap->fld -= reset.fld; + FIXED_COMP(numcalls); +#undef FIXED_COMP +} + +/*-------------------------------------------------------------------------- + * SQL-callable functions + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_fixed_update + * Increment call counter + */ +PG_FUNCTION_INFO_V1(test_custom_stats_fixed_update); +Datum +test_custom_stats_fixed_update(PG_FUNCTION_ARGS) +{ + PgStatShared_CustomFixedEntry *stats_shmem; + + stats_shmem = pgstat_get_custom_shmem_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE); + + pgstat_begin_changecount_write(&stats_shmem->changecount); + stats_shmem->stats.numcalls++; + pgstat_end_changecount_write(&stats_shmem->changecount); + + LWLockRelease(&stats_shmem->lock); + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_fixed_reset + * Reset statistics by calling pgstat system + */ +PG_FUNCTION_INFO_V1(test_custom_stats_fixed_reset); +Datum +test_custom_stats_fixed_reset(PG_FUNCTION_ARGS) +{ + pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_fixed_report + * Return current counter values + */ +PG_FUNCTION_INFO_V1(test_custom_stats_fixed_report); +Datum +test_custom_stats_fixed_report(PG_FUNCTION_ARGS) +{ + TupleDesc tupdesc; + Datum values[2] = {0}; + bool nulls[2] = {false}; + PgStat_StatCustomFixedEntry *stats; + + /* Take snapshot (applies reset offsets) */ + pgstat_snapshot_fixed(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + stats = pgstat_get_custom_snapshot_data(PGSTAT_KIND_TEST_CUSTOM_FIXED_STATS); + + /* Build return tuple */ + tupdesc = CreateTemplateTupleDesc(2); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "numcalls", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "stats_reset", + TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); + BlessTupleDesc(tupdesc); + + values[0] = Int64GetDatum(stats->numcalls); + + /* Handle uninitialized timestamp (no reset yet) */ + if (stats->stat_reset_timestamp == 0) + { + nulls[1] = true; + } + else + { + values[1] = TimestampTzGetDatum(stats->stat_reset_timestamp); + } + + /* Return as tuple */ + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} diff --git a/src/test/modules/test_custom_stats/test_custom_fixed_stats.control b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control new file mode 100644 index 0000000000000..3e80cc24e6bd0 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_fixed_stats.control @@ -0,0 +1,4 @@ +comment = 'fixed-sized custom pgstats' +default_version = '1.0' +module_pathname = '$libdir/test_custom_fixed_stats' +relocatable = true diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql new file mode 100644 index 0000000000000..5ed8cfc2dcf1d --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats--1.0.sql @@ -0,0 +1,26 @@ +/* src/test/modules/test_custom_var_stats/test_custom_var_stats--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_var_stats" to load this file. \quit + +CREATE FUNCTION test_custom_stats_var_create(IN name TEXT, in description TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_var_create' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_var_update(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_var_update' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_var_drop(IN name TEXT) +RETURNS void +AS 'MODULE_PATHNAME', 'test_custom_stats_var_drop' +LANGUAGE C STRICT PARALLEL UNSAFE; + +CREATE FUNCTION test_custom_stats_var_report(INOUT name TEXT, + OUT calls BIGINT, + OUT description TEXT) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'test_custom_stats_var_report' +LANGUAGE C STRICT PARALLEL UNSAFE; diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.c b/src/test/modules/test_custom_stats/test_custom_var_stats.c new file mode 100644 index 0000000000000..5c4871ed37cac --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.c @@ -0,0 +1,694 @@ +/*------------------------------------------------------------------------------------ + * + * test_custom_var_stats.c + * Test module for variable-sized custom pgstats + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_var_stats/test_custom_var_stats.c + * + * ------------------------------------------------------------------------------------ + */ +#include "postgres.h" + +#include "access/htup_details.h" +#include "common/hashfn.h" +#include "funcapi.h" +#include "storage/dsm_registry.h" +#include "storage/fd.h" +#include "utils/builtins.h" +#include "utils/pgstat_internal.h" + +PG_MODULE_MAGIC_EXT( + .name = "test_custom_var_stats", + .version = PG_VERSION +); + +#define TEST_CUSTOM_VAR_MAGIC_NUMBER (0xBEEFBEEF) + +/*-------------------------------------------------------------------------- + * Macros and constants + *-------------------------------------------------------------------------- + */ + +/* + * Kind ID for test_custom_var_stats statistics. + */ +#define PGSTAT_KIND_TEST_CUSTOM_VAR_STATS 25 + +/* File paths for auxiliary data serialization */ +#define TEST_CUSTOM_AUX_DATA_DESC "pg_stat/test_custom_var_stats_desc.stats" + +/* + * Hash statistic name to generate entry index for pgstat lookup. + */ +#define PGSTAT_CUSTOM_VAR_STATS_IDX(name) hash_bytes_extended((const unsigned char *) name, strlen(name), 0) + +/*-------------------------------------------------------------------------- + * Type definitions + *-------------------------------------------------------------------------- + */ + +/* Backend-local pending statistics before flush to shared memory */ +typedef struct PgStat_StatCustomVarEntry +{ + PgStat_Counter numcalls; /* times statistic was incremented */ +} PgStat_StatCustomVarEntry; + +/* Shared memory statistics entry visible to all backends */ +typedef struct PgStatShared_CustomVarEntry +{ + PgStatShared_Common header; /* standard pgstat entry header */ + PgStat_StatCustomVarEntry stats; /* custom statistics data */ + dsa_pointer description; /* pointer to description string in DSA */ +} PgStatShared_CustomVarEntry; + +/*-------------------------------------------------------------------------- + * Global Variables + *-------------------------------------------------------------------------- + */ + +/* File handle for auxiliary data serialization */ +static FILE *fd_description = NULL; + +/* Current write offset in fd_description file */ +static pgoff_t fd_description_offset = 0; + +/* DSA area for storing variable-length description strings */ +static dsa_area *custom_stats_description_dsa = NULL; + +/*-------------------------------------------------------------------------- + * Function prototypes + *-------------------------------------------------------------------------- + */ + +/* Flush callback: merge pending stats into shared memory */ +static bool test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, + bool nowait); + +/* Serialization callback: write auxiliary entry data */ +static void test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, + const PgStatShared_Common *header, + FILE *statfile); + +/* Deserialization callback: read auxiliary entry data */ +static bool test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, + PgStatShared_Common *header, + FILE *statfile); + +/* Finish callback: end of statistics file operations */ +static void test_custom_stats_var_finish(PgStat_StatsFileOp status); + +/*-------------------------------------------------------------------------- + * Custom kind configuration + *-------------------------------------------------------------------------- + */ + +static const PgStat_KindInfo custom_stats = { + .name = "test_custom_var_stats", + .fixed_amount = false, /* variable number of entries */ + .write_to_file = true, /* persist across restarts */ + .track_entry_count = true, /* count active entries */ + .accessed_across_databases = true, /* global statistics */ + .shared_size = sizeof(PgStatShared_CustomVarEntry), + .shared_data_off = offsetof(PgStatShared_CustomVarEntry, stats), + .shared_data_len = sizeof(((PgStatShared_CustomVarEntry *) 0)->stats), + .pending_size = sizeof(PgStat_StatCustomVarEntry), + .flush_pending_cb = test_custom_stats_var_flush_pending_cb, + .to_serialized_data = test_custom_stats_var_to_serialized_data, + .from_serialized_data = test_custom_stats_var_from_serialized_data, + .finish = test_custom_stats_var_finish, +}; + +/*-------------------------------------------------------------------------- + * Module initialization + *-------------------------------------------------------------------------- + */ + +void +_PG_init(void) +{ + /* Must be loaded via shared_preload_libraries */ + if (!process_shared_preload_libraries_in_progress) + return; + + /* Register custom statistics kind */ + pgstat_register_kind(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, &custom_stats); +} + +/*-------------------------------------------------------------------------- + * Statistics callback functions + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_var_flush_pending_cb + * Merge pending backend statistics into shared memory + * + * Called by pgstat collector to flush accumulated local statistics + * to shared memory where other backends can read them. + * + * Returns false only if nowait=true and lock acquisition fails. + */ +static bool +test_custom_stats_var_flush_pending_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + PgStat_StatCustomVarEntry *pending_entry; + PgStatShared_CustomVarEntry *shared_entry; + + pending_entry = (PgStat_StatCustomVarEntry *) entry_ref->pending; + shared_entry = (PgStatShared_CustomVarEntry *) entry_ref->shared_stats; + + if (!pgstat_lock_entry(entry_ref, nowait)) + return false; + + /* Add pending counts to shared totals */ + shared_entry->stats.numcalls += pending_entry->numcalls; + + pgstat_unlock_entry(entry_ref); + + return true; +} + +/* + * test_custom_stats_var_to_serialized_data() - + * + * Serialize auxiliary data (descriptions) for custom statistics entries + * to a secondary statistics file. This is called while writing the statistics + * to disk. + * + * This callback writes a mix of data within the main pgstats file and a + * secondary statistics file. The following data is written to the main file for + * each entry: + * - An arbitrary magic number. + * - An offset. This is used to know the location we need to look at + * to retrieve the information from the second file. + * + * The following data is written to the secondary statistics file: + * - The entry key, cross-checked with the data from the main file + * when reloaded. + * - The length of the description. + * - The description data itself. + */ +static void +test_custom_stats_var_to_serialized_data(const PgStat_HashKey *key, + const PgStatShared_Common *header, + FILE *statfile) +{ + char *description; + size_t len; + const PgStatShared_CustomVarEntry *entry = (const PgStatShared_CustomVarEntry *) header; + bool found; + uint32 magic_number = TEST_CUSTOM_VAR_MAGIC_NUMBER; + + /* + * First mark the main file with a magic number, keeping a trace that some + * auxiliary data will exist in the secondary statistics file. + */ + pgstat_write_chunk_s(statfile, &magic_number); + + /* Open statistics file for writing. */ + if (!fd_description) + { + fd_description = AllocateFile(TEST_CUSTOM_AUX_DATA_DESC, PG_BINARY_W); + if (fd_description == NULL) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open statistics file \"%s\" for writing: %m", + TEST_CUSTOM_AUX_DATA_DESC))); + return; + } + + /* Initialize offset for secondary statistics file. */ + fd_description_offset = 0; + } + + /* Write offset to the main data file */ + pgstat_write_chunk_s(statfile, &fd_description_offset); + + /* + * First write the entry key to the secondary statistics file. This will + * be cross-checked with the key read from main stats file at loading + * time. + */ + pgstat_write_chunk_s(fd_description, (PgStat_HashKey *) key); + fd_description_offset += sizeof(PgStat_HashKey); + + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("test_custom_stat_dsa", &found); + + /* Handle entries without descriptions */ + if (!DsaPointerIsValid(entry->description) || !custom_stats_description_dsa) + { + /* length to description file */ + len = 0; + pgstat_write_chunk_s(fd_description, &len); + fd_description_offset += sizeof(size_t); + return; + } + + /* + * Retrieve description from DSA, then write the length followed by the + * description. + */ + description = dsa_get_address(custom_stats_description_dsa, + entry->description); + len = strlen(description) + 1; + pgstat_write_chunk_s(fd_description, &len); + pgstat_write_chunk(fd_description, description, len); + + /* + * Update offset for next entry, counting for the length (size_t) of the + * description and the description contents. + */ + fd_description_offset += len + sizeof(size_t); +} + +/* + * test_custom_stats_var_from_serialized_data() - + * + * Read auxiliary data (descriptions) for custom statistics entries from + * the secondary statistics file. This is called while loading the statistics + * at startup. + * + * See the top of test_custom_stats_var_to_serialized_data() for a + * detailed description of the data layout read here. + */ +static bool +test_custom_stats_var_from_serialized_data(const PgStat_HashKey *key, + PgStatShared_Common *header, + FILE *statfile) +{ + PgStatShared_CustomVarEntry *entry; + dsa_pointer dp; + size_t len; + pgoff_t offset; + char *buffer; + bool found; + uint32 magic_number = 0; + PgStat_HashKey file_key; + + /* Check the magic number first, in the main file. */ + if (!pgstat_read_chunk_s(statfile, &magic_number)) + { + elog(WARNING, "failed to read magic number from statistics file"); + return false; + } + + if (magic_number != TEST_CUSTOM_VAR_MAGIC_NUMBER) + { + elog(WARNING, "found magic number %u from statistics file, should be %u", + magic_number, TEST_CUSTOM_VAR_MAGIC_NUMBER); + return false; + } + + /* + * Read the offset from the main stats file, to be able to read the + * auxiliary data from the secondary statistics file. + */ + if (!pgstat_read_chunk_s(statfile, &offset)) + { + elog(WARNING, "failed to read metadata offset from statistics file"); + return false; + } + + /* Open statistics file for reading if not already open */ + if (!fd_description) + { + fd_description = AllocateFile(TEST_CUSTOM_AUX_DATA_DESC, PG_BINARY_R); + if (fd_description == NULL) + { + if (errno != ENOENT) + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open statistics file \"%s\" for reading: %m", + TEST_CUSTOM_AUX_DATA_DESC))); + pgstat_reset_of_kind(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS); + return false; + } + } + + /* Read data from the secondary statistics file, at the specified offset */ + if (fseeko(fd_description, offset, SEEK_SET) != 0) + { + elog(WARNING, "could not seek in file \"%s\": %m", + TEST_CUSTOM_AUX_DATA_DESC); + return false; + } + + /* Read the hash key from the secondary statistics file */ + if (!pgstat_read_chunk_s(fd_description, &file_key)) + { + elog(WARNING, "failed to read hash key from file"); + return false; + } + + /* Check key consistency */ + if (file_key.kind != key->kind || + file_key.dboid != key->dboid || + file_key.objid != key->objid) + { + elog(WARNING, "found entry key %u/%u/%" PRIu64 " not matching with %u/%u/%" PRIu64, + file_key.kind, file_key.dboid, file_key.objid, + key->kind, key->dboid, key->objid); + return false; + } + + entry = (PgStatShared_CustomVarEntry *) header; + + /* Read the description length and its data */ + if (!pgstat_read_chunk_s(fd_description, &len)) + { + elog(WARNING, "failed to read metadata length from statistics file"); + return false; + } + + /* Handle empty descriptions */ + if (len == 0) + { + entry->description = InvalidDsaPointer; + return true; + } + + /* Initialize DSA if needed */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("test_custom_stat_dsa", &found); + + if (!custom_stats_description_dsa) + { + elog(WARNING, "could not access DSA for custom statistics descriptions"); + return false; + } + + buffer = palloc(len); + if (!pgstat_read_chunk(fd_description, buffer, len)) + { + pfree(buffer); + elog(WARNING, "failed to read description from file"); + return false; + } + + /* Allocate space in DSA and copy the description */ + dp = dsa_allocate(custom_stats_description_dsa, len); + memcpy(dsa_get_address(custom_stats_description_dsa, dp), buffer, len); + entry->description = dp; + pfree(buffer); + + return true; +} + +/* + * test_custom_stats_var_finish() - + * + * Cleanup function called at the end of statistics file operations. + * Handles closing files and cleanup based on the operation type. + */ +static void +test_custom_stats_var_finish(PgStat_StatsFileOp status) +{ + switch (status) + { + case STATS_WRITE: + if (!fd_description) + return; + + fd_description_offset = 0; + + /* Check for write errors and cleanup if necessary */ + if (ferror(fd_description)) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not write to file \"%s\": %m", + TEST_CUSTOM_AUX_DATA_DESC))); + FreeFile(fd_description); + unlink(TEST_CUSTOM_AUX_DATA_DESC); + } + else if (FreeFile(fd_description) < 0) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + TEST_CUSTOM_AUX_DATA_DESC))); + unlink(TEST_CUSTOM_AUX_DATA_DESC); + } + break; + + case STATS_READ: + if (fd_description) + FreeFile(fd_description); + + /* Remove the file after reading */ + elog(DEBUG2, "removing file \"%s\"", TEST_CUSTOM_AUX_DATA_DESC); + unlink(TEST_CUSTOM_AUX_DATA_DESC); + break; + + case STATS_DISCARD: + { + int ret; + + /* Attempt to remove the file */ + ret = unlink(TEST_CUSTOM_AUX_DATA_DESC); + if (ret != 0) + { + if (errno == ENOENT) + elog(LOG, + "didn't need to unlink file \"%s\" - didn't exist", + TEST_CUSTOM_AUX_DATA_DESC); + else + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not unlink file \"%s\": %m", + TEST_CUSTOM_AUX_DATA_DESC))); + } + else + { + ereport(LOG, + (errmsg_internal("unlinked file \"%s\"", + TEST_CUSTOM_AUX_DATA_DESC))); + } + } + break; + } + + fd_description = NULL; +} + +/*-------------------------------------------------------------------------- + * Helper functions + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_var_fetch_entry + * Look up custom statistic by name + * + * Returns statistics entry from shared memory, or NULL if not found. + */ +static PgStat_StatCustomVarEntry * +test_custom_stats_var_fetch_entry(const char *stat_name) +{ + /* Fetch entry by hashed name */ + return (PgStat_StatCustomVarEntry *) + pgstat_fetch_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, + InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), + NULL); +} + +/*-------------------------------------------------------------------------- + * SQL-callable functions + *-------------------------------------------------------------------------- + */ + +/* + * test_custom_stats_var_create + * Create new custom statistic entry + * + * Initializes a statistics entry with the given name and description. + */ +PG_FUNCTION_INFO_V1(test_custom_stats_var_create); +Datum +test_custom_stats_var_create(PG_FUNCTION_ARGS) +{ + PgStat_EntryRef *entry_ref; + PgStatShared_CustomVarEntry *shared_entry; + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + char *description = text_to_cstring(PG_GETARG_TEXT_PP(1)); + dsa_pointer dp = InvalidDsaPointer; + bool found; + + /* Validate name length first */ + if (strlen(stat_name) >= NAMEDATALEN) + ereport(ERROR, + (errcode(ERRCODE_NAME_TOO_LONG), + errmsg("custom statistic name \"%s\" is too long", stat_name), + errdetail("Name must be less than %d characters.", NAMEDATALEN))); + + /* Initialize DSA and description provided */ + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("test_custom_stat_dsa", &found); + + if (!custom_stats_description_dsa) + ereport(ERROR, + (errmsg("could not access DSA for custom statistics descriptions"))); + + /* Allocate space in DSA and copy description */ + dp = dsa_allocate(custom_stats_description_dsa, strlen(description) + 1); + memcpy(dsa_get_address(custom_stats_description_dsa, dp), + description, + strlen(description) + 1); + + /* Create or get existing entry */ + entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), true); + + if (!entry_ref) + PG_RETURN_VOID(); + + shared_entry = (PgStatShared_CustomVarEntry *) entry_ref->shared_stats; + + /* Zero-initialize statistics */ + memset(&shared_entry->stats, 0, sizeof(shared_entry->stats)); + + /* Store description pointer */ + shared_entry->description = dp; + + pgstat_unlock_entry(entry_ref); + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_var_update + * Increment custom statistic counter + * + * Increments call count in backend-local memory. Changes are flushed + * to shared memory by the statistics collector. + */ +PG_FUNCTION_INFO_V1(test_custom_stats_var_update); +Datum +test_custom_stats_var_update(PG_FUNCTION_ARGS) +{ + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + PgStat_EntryRef *entry_ref; + PgStat_StatCustomVarEntry *pending_entry; + + /* Get pending entry in local memory */ + entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), NULL); + + pending_entry = (PgStat_StatCustomVarEntry *) entry_ref->pending; + pending_entry->numcalls++; + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_var_drop + * Remove custom statistic entry + * + * Drops the named statistic from shared memory. + */ +PG_FUNCTION_INFO_V1(test_custom_stats_var_drop); +Datum +test_custom_stats_var_drop(PG_FUNCTION_ARGS) +{ + char *stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + /* Drop entry and request GC if the entry could not be freed */ + if (!pgstat_drop_entry(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name))) + pgstat_request_entry_refs_gc(); + + PG_RETURN_VOID(); +} + +/* + * test_custom_stats_var_report + * Retrieve custom statistic values + * + * Returns single row with statistic name, call count, and description if the + * statistic exists, otherwise returns no rows. + */ +PG_FUNCTION_INFO_V1(test_custom_stats_var_report); +Datum +test_custom_stats_var_report(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + char *stat_name; + PgStat_StatCustomVarEntry *stat_entry; + + if (SRF_IS_FIRSTCALL()) + { + TupleDesc tupdesc; + MemoryContext oldcontext; + + /* Initialize SRF context */ + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* Get composite return type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "test_custom_stats_var_report: return type is not composite"); + + funcctx->tuple_desc = BlessTupleDesc(tupdesc); + funcctx->max_calls = 1; /* single row result */ + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + + if (funcctx->call_cntr < funcctx->max_calls) + { + Datum values[3]; + bool nulls[3] = {false, false, false}; + HeapTuple tuple; + PgStat_EntryRef *entry_ref; + PgStatShared_CustomVarEntry *shared_entry; + char *description = NULL; + bool found; + + stat_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + stat_entry = test_custom_stats_var_fetch_entry(stat_name); + + /* Return row only if entry exists */ + if (stat_entry) + { + /* Get entry ref to access shared entry */ + entry_ref = pgstat_get_entry_ref(PGSTAT_KIND_TEST_CUSTOM_VAR_STATS, InvalidOid, + PGSTAT_CUSTOM_VAR_STATS_IDX(stat_name), false, NULL); + + if (entry_ref) + { + shared_entry = (PgStatShared_CustomVarEntry *) entry_ref->shared_stats; + + /* Get description from DSA if available */ + if (DsaPointerIsValid(shared_entry->description)) + { + if (!custom_stats_description_dsa) + custom_stats_description_dsa = GetNamedDSA("test_custom_stat_dsa", &found); + + if (custom_stats_description_dsa) + description = dsa_get_address(custom_stats_description_dsa, shared_entry->description); + } + } + + values[0] = PointerGetDatum(cstring_to_text(stat_name)); + values[1] = Int64GetDatum(stat_entry->numcalls); + + if (description) + values[2] = PointerGetDatum(cstring_to_text(description)); + else + nulls[2] = true; + + tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); + SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple)); + } + } + + SRF_RETURN_DONE(funcctx); +} diff --git a/src/test/modules/test_custom_stats/test_custom_var_stats.control b/src/test/modules/test_custom_stats/test_custom_var_stats.control new file mode 100644 index 0000000000000..bea2097a54551 --- /dev/null +++ b/src/test/modules/test_custom_stats/test_custom_var_stats.control @@ -0,0 +1,4 @@ +comment = 'variable-sized custom pgstats' +default_version = '1.0' +module_pathname = '$libdir/test_custom_var_stats' +relocatable = true diff --git a/src/test/modules/test_custom_types/.gitignore b/src/test/modules/test_custom_types/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_custom_types/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_custom_types/Makefile b/src/test/modules/test_custom_types/Makefile new file mode 100644 index 0000000000000..e1b582b26ea33 --- /dev/null +++ b/src/test/modules/test_custom_types/Makefile @@ -0,0 +1,20 @@ +# src/test/modules/test_custom_types/Makefile + +MODULES = test_custom_types + +EXTENSION = test_custom_types +DATA = test_custom_types--1.0.sql +PGFILEDESC = "test_custom_types - tests for dummy custom types" + +REGRESS = test_custom_types + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_custom_types +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_custom_types/README b/src/test/modules/test_custom_types/README new file mode 100644 index 0000000000000..a37d2db577eac --- /dev/null +++ b/src/test/modules/test_custom_types/README @@ -0,0 +1,9 @@ +test_custom_types +================= + +This module contains a set of custom data types, with some of the following +patterns: + +- typanalyze function registered to a custom type, returning false. +- typanalyze function registered to a custom type, registering invalid stats + data. diff --git a/src/test/modules/test_custom_types/expected/test_custom_types.out b/src/test/modules/test_custom_types/expected/test_custom_types.out new file mode 100644 index 0000000000000..59f0917c2d748 --- /dev/null +++ b/src/test/modules/test_custom_types/expected/test_custom_types.out @@ -0,0 +1,180 @@ +-- Tests with various custom types +CREATE EXTENSION test_custom_types; +-- Test comparison functions +SELECT '42'::int_custom = '42'::int_custom AS eq_test; + eq_test +--------- + t +(1 row) + +SELECT '42'::int_custom <> '42'::int_custom AS nt_test; + nt_test +--------- + f +(1 row) + +SELECT '42'::int_custom < '100'::int_custom AS lt_test; + lt_test +--------- + t +(1 row) + +SELECT '100'::int_custom > '42'::int_custom AS gt_test; + gt_test +--------- + t +(1 row) + +SELECT '42'::int_custom <= '100'::int_custom AS le_test; + le_test +--------- + t +(1 row) + +SELECT '100'::int_custom >= '42'::int_custom AS ge_test; + ge_test +--------- + t +(1 row) + +-- Create a table with the int_custom type +CREATE TABLE test_table ( + id int, + data int_custom +); +INSERT INTO test_table VALUES (1, '42'), (2, '100'), (3, '200'); +-- Verify data was inserted correctly +SELECT * FROM test_table ORDER BY id; + id | data +----+------ + 1 | 42 + 2 | 100 + 3 | 200 +(3 rows) + +-- Dummy function used for expression evaluations. +-- Note that this function does not use a SQL-standard function body on +-- purpose, so as external statistics can be loaded from it. +CREATE OR REPLACE FUNCTION func_int_custom (p_value int_custom) + RETURNS int_custom LANGUAGE plpgsql AS $$ + BEGIN + RETURN p_value; + END; $$; +-- Switch type to use typanalyze function that always returns false. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_false); +-- Extended statistics with an attribute that cannot be analyzed. +-- This includes all statistics kinds. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table" +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Extended statistics with an expression that cannot be analyzed. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +WARNING: statistics object "public.test_stats" could not be computed for relation "public.test_table" +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- There should be no data stored for the expression. +SELECT tablename, + statistics_name, + null_frac, + avg_width + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx +(0 rows) + +-- Switch type to use typanalyze function that generates invalid data. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_invalid); +-- Extended statistics with an attribute that generates invalid stats. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | t +(1 row) + +DROP STATISTICS test_stats; +-- Extended statistics with an expression that generates invalid data. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, some data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; + stxname | expr_stats_is_null +------------+-------------------- + test_stats | f +(1 row) + +-- There should be some data stored for the expression, stored as NULL. +SELECT tablename, + statistics_name, + null_frac, + avg_width, + n_distinct, + most_common_vals, + most_common_freqs, + histogram_bounds, + correlation, + most_common_elems, + most_common_elem_freqs, + elem_count_histogram, + range_length_histogram, + range_empty_frac, + range_bounds_histogram + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx +-[ RECORD 1 ]----------+----------- +tablename | test_table +statistics_name | test_stats +null_frac | +avg_width | +n_distinct | +most_common_vals | +most_common_freqs | +histogram_bounds | +correlation | +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | +range_length_histogram | +range_empty_frac | +range_bounds_histogram | + +-- Run a query able to load the extended stats, including the NULL data. +SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); + count +------- + 1 + 1 + 1 +(3 rows) + +DROP STATISTICS test_stats; +-- Cleanup +DROP FUNCTION func_int_custom; +DROP TABLE test_table; +DROP EXTENSION test_custom_types; diff --git a/src/test/modules/test_custom_types/meson.build b/src/test/modules/test_custom_types/meson.build new file mode 100644 index 0000000000000..3d4f433dd510b --- /dev/null +++ b/src/test/modules/test_custom_types/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_custom_types_sources = files( + 'test_custom_types.c', +) + +if host_system == 'windows' + test_custom_types_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_custom_types', + '--FILEDESC', 'test_custom_types - tests for dummy custom types',]) +endif + +test_custom_types = shared_module('test_custom_types', + test_custom_types_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_custom_types + +test_install_data += files( + 'test_custom_types.control', + 'test_custom_types--1.0.sql', +) + +tests += { + 'name': 'test_custom_types', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_custom_types', + ], + }, +} diff --git a/src/test/modules/test_custom_types/sql/test_custom_types.sql b/src/test/modules/test_custom_types/sql/test_custom_types.sql new file mode 100644 index 0000000000000..f1c75e4ba5b55 --- /dev/null +++ b/src/test/modules/test_custom_types/sql/test_custom_types.sql @@ -0,0 +1,107 @@ +-- Tests with various custom types + +CREATE EXTENSION test_custom_types; + +-- Test comparison functions +SELECT '42'::int_custom = '42'::int_custom AS eq_test; +SELECT '42'::int_custom <> '42'::int_custom AS nt_test; +SELECT '42'::int_custom < '100'::int_custom AS lt_test; +SELECT '100'::int_custom > '42'::int_custom AS gt_test; +SELECT '42'::int_custom <= '100'::int_custom AS le_test; +SELECT '100'::int_custom >= '42'::int_custom AS ge_test; + +-- Create a table with the int_custom type +CREATE TABLE test_table ( + id int, + data int_custom +); +INSERT INTO test_table VALUES (1, '42'), (2, '100'), (3, '200'); + +-- Verify data was inserted correctly +SELECT * FROM test_table ORDER BY id; + +-- Dummy function used for expression evaluations. +-- Note that this function does not use a SQL-standard function body on +-- purpose, so as external statistics can be loaded from it. +CREATE OR REPLACE FUNCTION func_int_custom (p_value int_custom) + RETURNS int_custom LANGUAGE plpgsql AS $$ + BEGIN + RETURN p_value; + END; $$; + +-- Switch type to use typanalyze function that always returns false. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_false); + +-- Extended statistics with an attribute that cannot be analyzed. +-- This includes all statistics kinds. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; + +-- Extended statistics with an expression that cannot be analyzed. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; +-- There should be no data stored for the expression. +SELECT tablename, + statistics_name, + null_frac, + avg_width + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx + +-- Switch type to use typanalyze function that generates invalid data. +ALTER TYPE int_custom SET (ANALYZE = int_custom_typanalyze_invalid); + +-- Extended statistics with an attribute that generates invalid stats. +CREATE STATISTICS test_stats ON data, id FROM test_table; +-- Computation of the stats fails, no data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +DROP STATISTICS test_stats; + +-- Extended statistics with an expression that generates invalid data. +CREATE STATISTICS test_stats ON func_int_custom(data), (id) FROM test_table; +-- Computation of the stats fails, some data generated. +ANALYZE test_table; +SELECT stxname, stxdexpr IS NULL as expr_stats_is_null + FROM pg_statistic_ext s + LEFT JOIN pg_statistic_ext_data d ON s.oid = d.stxoid + WHERE stxname = 'test_stats'; +-- There should be some data stored for the expression, stored as NULL. +SELECT tablename, + statistics_name, + null_frac, + avg_width, + n_distinct, + most_common_vals, + most_common_freqs, + histogram_bounds, + correlation, + most_common_elems, + most_common_elem_freqs, + elem_count_histogram, + range_length_histogram, + range_empty_frac, + range_bounds_histogram + FROM pg_stats_ext_exprs WHERE statistics_name = 'test_stats' \gx +-- Run a query able to load the extended stats, including the NULL data. +SELECT COUNT(*) FROM test_table GROUP BY (func_int_custom(data)); +DROP STATISTICS test_stats; + +-- Cleanup +DROP FUNCTION func_int_custom; +DROP TABLE test_table; +DROP EXTENSION test_custom_types; diff --git a/src/test/modules/test_custom_types/test_custom_types--1.0.sql b/src/test/modules/test_custom_types/test_custom_types--1.0.sql new file mode 100644 index 0000000000000..ce0e905d63648 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types--1.0.sql @@ -0,0 +1,164 @@ +/* src/test/modules/test_custom_types/test_custom_types--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_custom_types" to load this file. \quit + +-- +-- Input/output functions for int_custom type +-- +CREATE FUNCTION int_custom_in(cstring) +RETURNS int_custom +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_out(int_custom) +RETURNS cstring +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +-- +-- Typanalyze function that returns false +-- +CREATE FUNCTION int_custom_typanalyze_false(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- Typanalyze function that returns invalid stats +-- +CREATE FUNCTION int_custom_typanalyze_invalid(internal) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- The int_custom type definition +-- +-- This type is identical to int4 in storage, and is used in subsequent +-- tests to have different properties. +-- +CREATE TYPE int_custom ( + INPUT = int_custom_in, + OUTPUT = int_custom_out, + LIKE = int4 +); + +-- +-- Comparison functions for int_custom +-- +-- These are required to create a btree operator class, which is needed +-- for the type to be usable in extended statistics objects. +-- +CREATE FUNCTION int_custom_eq(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_ne(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_lt(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_le(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_gt(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_ge(int_custom, int_custom) +RETURNS boolean +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION int_custom_cmp(int_custom, int_custom) +RETURNS integer +AS 'MODULE_PATHNAME' +LANGUAGE C IMMUTABLE STRICT; + +-- Operators for int_custom, for btree operator class +CREATE OPERATOR = ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_eq, + COMMUTATOR = =, + NEGATOR = <>, + RESTRICT = eqsel, + JOIN = eqjoinsel, + HASHES, + MERGES +); + +CREATE OPERATOR <> ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_ne, + COMMUTATOR = <>, + NEGATOR = =, + RESTRICT = neqsel, + JOIN = neqjoinsel +); + +CREATE OPERATOR < ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_lt, + COMMUTATOR = >, + NEGATOR = >=, + RESTRICT = scalarltsel, + JOIN = scalarltjoinsel +); + +CREATE OPERATOR <= ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_le, + COMMUTATOR = >=, + NEGATOR = >, + RESTRICT = scalarlesel, + JOIN = scalarlejoinsel +); + +CREATE OPERATOR > ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_gt, + COMMUTATOR = <, + NEGATOR = <=, + RESTRICT = scalargtsel, + JOIN = scalargtjoinsel +); + +CREATE OPERATOR >= ( + LEFTARG = int_custom, + RIGHTARG = int_custom, + FUNCTION = int_custom_ge, + COMMUTATOR = <=, + NEGATOR = <, + RESTRICT = scalargesel, + JOIN = scalargejoinsel +); + +-- +-- Btree operator class for int_custom +-- +-- This is required for the type to be usable in extended statistics objects, +-- for attributes and expressions. +-- +CREATE OPERATOR CLASS int_custom_ops + DEFAULT FOR TYPE int_custom USING btree AS + OPERATOR 1 <, + OPERATOR 2 <=, + OPERATOR 3 =, + OPERATOR 4 >=, + OPERATOR 5 >, + FUNCTION 1 int_custom_cmp(int_custom, int_custom); diff --git a/src/test/modules/test_custom_types/test_custom_types.c b/src/test/modules/test_custom_types/test_custom_types.c new file mode 100644 index 0000000000000..41262044b4757 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types.c @@ -0,0 +1,182 @@ +/*-------------------------------------------------------------------------- + * + * test_custom_types.c + * Test module for a set of functions for custom types. + * + * The custom type used in this module is similar to int4 for simplicity, + * except that it is able to use various typanalyze functions to enforce + * check patterns with ANALYZE. + * + * Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_custom_types/test_custom_types.c + * + *-------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "commands/vacuum.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +/* Function declarations */ +PG_FUNCTION_INFO_V1(int_custom_in); +PG_FUNCTION_INFO_V1(int_custom_out); +PG_FUNCTION_INFO_V1(int_custom_typanalyze_false); +PG_FUNCTION_INFO_V1(int_custom_typanalyze_invalid); +PG_FUNCTION_INFO_V1(int_custom_eq); +PG_FUNCTION_INFO_V1(int_custom_ne); +PG_FUNCTION_INFO_V1(int_custom_lt); +PG_FUNCTION_INFO_V1(int_custom_le); +PG_FUNCTION_INFO_V1(int_custom_gt); +PG_FUNCTION_INFO_V1(int_custom_ge); +PG_FUNCTION_INFO_V1(int_custom_cmp); + +/* + * int_custom_in - input function for int_custom type + * + * Converts a string to a int_custom (which is just an int32 internally). + */ +Datum +int_custom_in(PG_FUNCTION_ARGS) +{ + char *num = PG_GETARG_CSTRING(0); + + PG_RETURN_INT32(pg_strtoint32_safe(num, fcinfo->context)); +} + +/* + * int_custom_out - output function for int_custom type + * + * Converts a int_custom to a string. + */ +Datum +int_custom_out(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + char *result = (char *) palloc(12); /* sign, 10 digits, '\0' */ + + pg_ltoa(arg1, result); + PG_RETURN_CSTRING(result); +} + +/* + * int_custom_typanalyze_false - typanalyze function that returns false + * + * This function returns false, to simulate a type that cannot be analyzed. + */ +Datum +int_custom_typanalyze_false(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(false); +} + +/* + * Callback used to compute invalid statistics. + */ +static void +int_custom_invalid_stats(VacAttrStats *stats, AnalyzeAttrFetchFunc fetchfunc, + int samplerows, double totalrows) +{ + /* We are not valid, and do not want to be. */ + stats->stats_valid = false; +} + +/* + * int_custom_typanalyze_invalid + * + * This function sets some invalid stats data, letting the caller know that + * we are safe for an analyze, returning true. + */ +Datum +int_custom_typanalyze_invalid(PG_FUNCTION_ARGS) +{ + VacAttrStats *stats = (VacAttrStats *) PG_GETARG_POINTER(0); + + /* If the attstattarget column is negative, use the default value */ + if (stats->attstattarget < 0) + stats->attstattarget = default_statistics_target; + + /* Buggy number, no need to care as long as it is positive */ + stats->minrows = 300; + + /* Set callback to compute some invalid stats */ + stats->compute_stats = int_custom_invalid_stats; + + PG_RETURN_BOOL(true); +} + +/* + * Comparison functions for int_custom type + */ +Datum +int_custom_eq(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 == arg2); +} + +Datum +int_custom_ne(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 != arg2); +} + +Datum +int_custom_lt(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 < arg2); +} + +Datum +int_custom_le(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 <= arg2); +} + +Datum +int_custom_gt(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 > arg2); +} + +Datum +int_custom_ge(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + PG_RETURN_BOOL(arg1 >= arg2); +} + +Datum +int_custom_cmp(PG_FUNCTION_ARGS) +{ + int32 arg1 = PG_GETARG_INT32(0); + int32 arg2 = PG_GETARG_INT32(1); + + if (arg1 < arg2) + PG_RETURN_INT32(-1); + else if (arg1 > arg2) + PG_RETURN_INT32(1); + else + PG_RETURN_INT32(0); +} diff --git a/src/test/modules/test_custom_types/test_custom_types.control b/src/test/modules/test_custom_types/test_custom_types.control new file mode 100644 index 0000000000000..d25a74ef5c468 --- /dev/null +++ b/src/test/modules/test_custom_types/test_custom_types.control @@ -0,0 +1,5 @@ +# test_custom_types extension +comment = 'Tests for dummy custom types' +default_version = '1.0' +module_pathname = '$libdir/test_custom_types' +relocatable = true diff --git a/src/test/modules/test_ddl_deparse/Makefile b/src/test/modules/test_ddl_deparse/Makefile index 3a57a95c84969..f91a78d8d92bd 100644 --- a/src/test/modules/test_ddl_deparse/Makefile +++ b/src/test/modules/test_ddl_deparse/Makefile @@ -13,7 +13,7 @@ REGRESS = test_ddl_deparse \ create_type \ create_conversion \ create_domain \ - create_sequence_1 \ + create_sequence \ create_table \ create_transform \ alter_table \ @@ -27,6 +27,7 @@ REGRESS = test_ddl_deparse \ alter_type_enum \ opfamily \ defprivs \ + textsearch \ matviews EXTRA_INSTALL = contrib/pg_stat_statements diff --git a/src/test/modules/test_ddl_deparse/expected/alter_table.out b/src/test/modules/test_ddl_deparse/expected/alter_table.out index 50d0354a3417b..3a2f576f3b6eb 100644 --- a/src/test/modules/test_ddl_deparse/expected/alter_table.out +++ b/src/test/modules/test_ddl_deparse/expected/alter_table.out @@ -41,25 +41,6 @@ ALTER TABLE parent ADD CONSTRAINT a_pos CHECK (a > 0); NOTICE: merging constraint "a_pos" with inherited definition NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint a_pos on table parent -CREATE TABLE part ( - a int -) PARTITION BY RANGE (a); -NOTICE: DDL test: type simple, tag CREATE TABLE -CREATE TABLE part1 PARTITION OF part FOR VALUES FROM (1) to (100); -NOTICE: DDL test: type simple, tag CREATE TABLE -CREATE TABLE part2 (a int); -NOTICE: DDL test: type simple, tag CREATE TABLE -ALTER TABLE part ATTACH PARTITION part2 FOR VALUES FROM (101) to (200); -NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type ATTACH PARTITION desc table part2 -ALTER TABLE part DETACH PARTITION part2; -NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type DETACH PARTITION desc table part2 -DROP TABLE part2; -ALTER TABLE part ADD PRIMARY KEY (a); -NOTICE: DDL test: type alter table, tag ALTER TABLE -NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part -NOTICE: subcommand: type ADD INDEX desc index part_pkey ALTER TABLE parent ALTER COLUMN a SET NOT NULL; NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent @@ -117,11 +98,43 @@ NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type ALTER COLUMN SET DEFAULT desc column c of table parent NOTICE: subcommand: type ALTER COLUMN SET DEFAULT desc column c of table child NOTICE: subcommand: type ALTER COLUMN SET DEFAULT desc column c of table grandchild +CREATE TABLE part ( + a int +) PARTITION BY RANGE (a); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE part1 PARTITION OF part FOR VALUES FROM (1) to (100); +NOTICE: DDL test: type simple, tag CREATE TABLE +CREATE TABLE part2 (a int); +NOTICE: DDL test: type simple, tag CREATE TABLE +ALTER TABLE part ATTACH PARTITION part2 FOR VALUES FROM (101) to (200); +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ATTACH PARTITION desc table part2 +ALTER TABLE part DETACH PARTITION part2; +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type DETACH PARTITION desc table part2 +DROP TABLE part2; +CREATE TABLE part2 PARTITION OF part FOR VALUES FROM (100) to (200); +NOTICE: DDL test: type simple, tag CREATE TABLE +ALTER TABLE part MERGE PARTITIONS (part1, part2) INTO part1; +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type MERGE PARTITIONS desc +ALTER TABLE part SPLIT PARTITION part1 INTO + (PARTITION part1 FOR VALUES FROM (1) to (100), + PARTITION part2 FOR VALUES FROM (100) to (200)); +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type SPLIT PARTITION desc +ALTER TABLE part ADD PRIMARY KEY (a); +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint part_a_not_null on table part +NOTICE: subcommand: type ADD INDEX desc index part_pkey CREATE TABLE tbl ( a int generated always as (b::int * 2) stored, b text ); NOTICE: DDL test: type simple, tag CREATE TABLE +ALTER TABLE tbl ALTER COLUMN a SET EXPRESSION AS (b::int * 3); +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type SET EXPRESSION desc column a of table tbl ALTER TABLE tbl ALTER COLUMN a DROP EXPRESSION; NOTICE: DDL test: type alter table, tag ALTER TABLE NOTICE: subcommand: type DROP EXPRESSION desc column a of table tbl diff --git a/src/test/modules/test_ddl_deparse/expected/create_schema.out b/src/test/modules/test_ddl_deparse/expected/create_schema.out index 8ab4eb0338523..a867786899bbb 100644 --- a/src/test/modules/test_ddl_deparse/expected/create_schema.out +++ b/src/test/modules/test_ddl_deparse/expected/create_schema.out @@ -13,7 +13,68 @@ CREATE SCHEMA IF NOT EXISTS baz; NOTICE: schema "baz" already exists, skipping CREATE SCHEMA element_test CREATE TABLE foo (id int) - CREATE VIEW bar AS SELECT * FROM foo; + CREATE VIEW bar AS SELECT * FROM foo + CREATE COLLATION coll (LOCALE="C") + CREATE DOMAIN d1 AS INT + CREATE FUNCTION et_add(int4, int4) RETURNS int4 LANGUAGE sql + AS 'SELECT $1 + $2' + CREATE PROCEDURE et_proc(int4, int4) + BEGIN ATOMIC SELECT et_add($1,$2); END + CREATE TYPE floatrange AS RANGE (subtype = float8, subtype_diff = float8mi) + CREATE TYPE ss AS (a int) + CREATE TYPE sss + CREATE TYPE rainbow AS ENUM ('red', 'orange') + CREATE TEXT SEARCH PARSER et_ts_prs + (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, + lextypes = prsd_lextype) +; NOTICE: DDL test: type simple, tag CREATE SCHEMA NOTICE: DDL test: type simple, tag CREATE TABLE NOTICE: DDL test: type simple, tag CREATE VIEW +NOTICE: DDL test: type simple, tag CREATE COLLATION +NOTICE: DDL test: type simple, tag CREATE DOMAIN +NOTICE: DDL test: type simple, tag CREATE FUNCTION +NOTICE: DDL test: type simple, tag CREATE PROCEDURE +NOTICE: DDL test: type simple, tag CREATE TYPE +NOTICE: DDL test: type simple, tag CREATE TYPE +NOTICE: DDL test: type simple, tag CREATE TYPE +NOTICE: DDL test: type simple, tag CREATE TYPE +NOTICE: DDL test: type simple, tag CREATE TEXT SEARCH PARSER +DROP SCHEMA element_test CASCADE; +NOTICE: drop cascades to 11 other objects +DETAIL: drop cascades to table element_test.foo +drop cascades to view element_test.bar +drop cascades to collation element_test.coll +drop cascades to type element_test.d1 +drop cascades to function element_test.et_add(integer,integer) +drop cascades to function element_test.et_proc(integer,integer) +drop cascades to type element_test.floatrange +drop cascades to type element_test.ss +drop cascades to type element_test.sss +drop cascades to type element_test.rainbow +drop cascades to text search parser element_test.et_ts_prs +CREATE SCHEMA regress_schema_1 +CREATE TABLE t4( + b INT, + a INT REFERENCES t5 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE) +CREATE TABLE t5 (a INT, b INT, PRIMARY KEY (a)) +CREATE TABLE t6 (a INT, b INT, PRIMARY KEY (a)); +NOTICE: DDL test: type simple, tag CREATE SCHEMA +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type simple, tag CREATE TABLE +NOTICE: DDL test: type simple, tag CREATE INDEX +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint t4_a_fkey on table regress_schema_1.t4 +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint t4_a_fkey1 on table regress_schema_1.t4 +NOTICE: DDL test: type alter table, tag ALTER TABLE +NOTICE: subcommand: type ADD CONSTRAINT (and recurse) desc constraint fk on table regress_schema_1.t4 +DROP SCHEMA regress_schema_1 CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table regress_schema_1.t4 +drop cascades to table regress_schema_1.t5 +drop cascades to table regress_schema_1.t6 diff --git a/src/test/modules/test_ddl_deparse/expected/create_sequence_1.out b/src/test/modules/test_ddl_deparse/expected/create_sequence.out similarity index 100% rename from src/test/modules/test_ddl_deparse/expected/create_sequence_1.out rename to src/test/modules/test_ddl_deparse/expected/create_sequence.out diff --git a/src/test/modules/test_ddl_deparse/expected/create_view.out b/src/test/modules/test_ddl_deparse/expected/create_view.out index 4ae0f4978ec1a..0a8cc4627a8b2 100644 --- a/src/test/modules/test_ddl_deparse/expected/create_view.out +++ b/src/test/modules/test_ddl_deparse/expected/create_view.out @@ -12,6 +12,12 @@ NOTICE: subcommand: type REPLACE RELOPTIONS desc CREATE VIEW datatype_view AS SELECT * FROM datatype_table; NOTICE: DDL test: type simple, tag CREATE VIEW +CREATE OR REPLACE VIEW datatype_view AS + SELECT * FROM datatype_table, static_view; +NOTICE: DDL test: type simple, tag CREATE VIEW +NOTICE: DDL test: type alter table, tag CREATE VIEW +NOTICE: subcommand: type ADD COLUMN TO VIEW desc column col of view datatype_view +NOTICE: subcommand: type REPLACE RELOPTIONS desc CREATE RECURSIVE VIEW nums_1_100 (n) AS VALUES (1) UNION ALL diff --git a/src/test/modules/test_ddl_deparse/expected/textsearch.out b/src/test/modules/test_ddl_deparse/expected/textsearch.out new file mode 100644 index 0000000000000..da0d89e9704dd --- /dev/null +++ b/src/test/modules/test_ddl_deparse/expected/textsearch.out @@ -0,0 +1,5 @@ +CREATE TEXT SEARCH CONFIGURATION evttrig_tscfg (COPY = pg_catalog.simple); +NOTICE: DDL test: type simple, tag CREATE TEXT SEARCH CONFIGURATION +ALTER TEXT SEARCH CONFIGURATION evttrig_tscfg + DROP MAPPING FOR word; +NOTICE: DDL test: type alter text search configuration, tag ALTER TEXT SEARCH CONFIGURATION diff --git a/src/test/modules/test_ddl_deparse/meson.build b/src/test/modules/test_ddl_deparse/meson.build index bff65ba6333d8..85decdf1f2ba3 100644 --- a/src/test/modules/test_ddl_deparse/meson.build +++ b/src/test/modules/test_ddl_deparse/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_ddl_deparse_sources = files( 'test_ddl_deparse.c', @@ -33,7 +33,7 @@ tests += { 'create_type', 'create_conversion', 'create_domain', - 'create_sequence_1', + 'create_sequence', 'create_table', 'create_transform', 'alter_table', @@ -47,6 +47,7 @@ tests += { 'alter_type_enum', 'opfamily', 'defprivs', + 'textsearch', 'matviews', ], }, diff --git a/src/test/modules/test_ddl_deparse/sql/alter_table.sql b/src/test/modules/test_ddl_deparse/sql/alter_table.sql index 9ad1cf908d428..0980097048ee0 100644 --- a/src/test/modules/test_ddl_deparse/sql/alter_table.sql +++ b/src/test/modules/test_ddl_deparse/sql/alter_table.sql @@ -24,19 +24,6 @@ ALTER TABLE ONLY grandchild ADD CONSTRAINT a_pos CHECK (a > 0); -- Constraint, with recursion ALTER TABLE parent ADD CONSTRAINT a_pos CHECK (a > 0); -CREATE TABLE part ( - a int -) PARTITION BY RANGE (a); - -CREATE TABLE part1 PARTITION OF part FOR VALUES FROM (1) to (100); - -CREATE TABLE part2 (a int); -ALTER TABLE part ATTACH PARTITION part2 FOR VALUES FROM (101) to (200); -ALTER TABLE part DETACH PARTITION part2; -DROP TABLE part2; - -ALTER TABLE part ADD PRIMARY KEY (a); - ALTER TABLE parent ALTER COLUMN a SET NOT NULL; ALTER TABLE parent ALTER COLUMN a DROP NOT NULL; ALTER TABLE parent ALTER COLUMN a SET NOT NULL; @@ -62,11 +49,32 @@ ALTER TABLE parent ALTER COLUMN c TYPE numeric; ALTER TABLE parent ALTER COLUMN c SET DEFAULT 0; +CREATE TABLE part ( + a int +) PARTITION BY RANGE (a); + +CREATE TABLE part1 PARTITION OF part FOR VALUES FROM (1) to (100); + +CREATE TABLE part2 (a int); +ALTER TABLE part ATTACH PARTITION part2 FOR VALUES FROM (101) to (200); +ALTER TABLE part DETACH PARTITION part2; +DROP TABLE part2; + +CREATE TABLE part2 PARTITION OF part FOR VALUES FROM (100) to (200); +ALTER TABLE part MERGE PARTITIONS (part1, part2) INTO part1; + +ALTER TABLE part SPLIT PARTITION part1 INTO + (PARTITION part1 FOR VALUES FROM (1) to (100), + PARTITION part2 FOR VALUES FROM (100) to (200)); + +ALTER TABLE part ADD PRIMARY KEY (a); + CREATE TABLE tbl ( a int generated always as (b::int * 2) stored, b text ); +ALTER TABLE tbl ALTER COLUMN a SET EXPRESSION AS (b::int * 3); ALTER TABLE tbl ALTER COLUMN a DROP EXPRESSION; ALTER TABLE tbl ALTER COLUMN b SET COMPRESSION pglz; diff --git a/src/test/modules/test_ddl_deparse/sql/create_schema.sql b/src/test/modules/test_ddl_deparse/sql/create_schema.sql index f314dc2b840b8..7ba641d06d67c 100644 --- a/src/test/modules/test_ddl_deparse/sql/create_schema.sql +++ b/src/test/modules/test_ddl_deparse/sql/create_schema.sql @@ -14,4 +14,31 @@ CREATE SCHEMA IF NOT EXISTS baz; CREATE SCHEMA element_test CREATE TABLE foo (id int) - CREATE VIEW bar AS SELECT * FROM foo; + CREATE VIEW bar AS SELECT * FROM foo + CREATE COLLATION coll (LOCALE="C") + CREATE DOMAIN d1 AS INT + CREATE FUNCTION et_add(int4, int4) RETURNS int4 LANGUAGE sql + AS 'SELECT $1 + $2' + CREATE PROCEDURE et_proc(int4, int4) + BEGIN ATOMIC SELECT et_add($1,$2); END + CREATE TYPE floatrange AS RANGE (subtype = float8, subtype_diff = float8mi) + CREATE TYPE ss AS (a int) + CREATE TYPE sss + CREATE TYPE rainbow AS ENUM ('red', 'orange') + CREATE TEXT SEARCH PARSER et_ts_prs + (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, + lextypes = prsd_lextype) +; + +DROP SCHEMA element_test CASCADE; + +CREATE SCHEMA regress_schema_1 +CREATE TABLE t4( + b INT, + a INT REFERENCES t5 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE) +CREATE TABLE t5 (a INT, b INT, PRIMARY KEY (a)) +CREATE TABLE t6 (a INT, b INT, PRIMARY KEY (a)); + +DROP SCHEMA regress_schema_1 CASCADE; diff --git a/src/test/modules/test_ddl_deparse/sql/create_sequence_1.sql b/src/test/modules/test_ddl_deparse/sql/create_sequence.sql similarity index 100% rename from src/test/modules/test_ddl_deparse/sql/create_sequence_1.sql rename to src/test/modules/test_ddl_deparse/sql/create_sequence.sql diff --git a/src/test/modules/test_ddl_deparse/sql/create_view.sql b/src/test/modules/test_ddl_deparse/sql/create_view.sql index 030b76f86fa64..f473dd74171d8 100644 --- a/src/test/modules/test_ddl_deparse/sql/create_view.sql +++ b/src/test/modules/test_ddl_deparse/sql/create_view.sql @@ -11,6 +11,9 @@ CREATE OR REPLACE VIEW static_view AS CREATE VIEW datatype_view AS SELECT * FROM datatype_table; +CREATE OR REPLACE VIEW datatype_view AS + SELECT * FROM datatype_table, static_view; + CREATE RECURSIVE VIEW nums_1_100 (n) AS VALUES (1) UNION ALL diff --git a/src/test/modules/test_ddl_deparse/sql/textsearch.sql b/src/test/modules/test_ddl_deparse/sql/textsearch.sql new file mode 100644 index 0000000000000..633899a31cb32 --- /dev/null +++ b/src/test/modules/test_ddl_deparse/sql/textsearch.sql @@ -0,0 +1,3 @@ +CREATE TEXT SEARCH CONFIGURATION evttrig_tscfg (COPY = pg_catalog.simple); +ALTER TEXT SEARCH CONFIGURATION evttrig_tscfg + DROP MAPPING FOR word; diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c index 193669f2bc1eb..64a1dfa9f793e 100644 --- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c +++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -2,7 +2,7 @@ * test_ddl_deparse.c * Support functions for the test_ddl_deparse module * - * Copyright (c) 2014-2025, PostgreSQL Global Development Group + * Copyright (c) 2014-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_ddl_deparse/test_ddl_deparse.c @@ -15,6 +15,7 @@ #include "tcop/deparse_utility.h" #include "tcop/utility.h" #include "utils/builtins.h" +#include "utils/tuplestore.h" PG_MODULE_MAGIC; @@ -296,6 +297,12 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS) case AT_DetachPartitionFinalize: strtype = "DETACH PARTITION ... FINALIZE"; break; + case AT_SplitPartition: + strtype = "SPLIT PARTITION"; + break; + case AT_MergePartitions: + strtype = "MERGE PARTITIONS"; + break; case AT_AddIdentity: strtype = "ADD IDENTITY"; break; diff --git a/src/test/modules/test_dsa/expected/test_dsa.out b/src/test/modules/test_dsa/expected/test_dsa.out index 266010e77fe9e..4b53a7de4a443 100644 --- a/src/test/modules/test_dsa/expected/test_dsa.out +++ b/src/test/modules/test_dsa/expected/test_dsa.out @@ -11,3 +11,19 @@ SELECT test_dsa_resowners(); (1 row) +-- Test allocations across a pre-defined range of pages. This covers enough +-- range to check for the case of odd-sized segments, without making the test +-- too slow. +SELECT test_dsa_allocate(1001, 2000, 100); + test_dsa_allocate +------------------- + +(1 row) + +-- Larger size with odd-sized segment. +SELECT test_dsa_allocate(6501, 6600, 100); + test_dsa_allocate +------------------- + +(1 row) + diff --git a/src/test/modules/test_dsa/meson.build b/src/test/modules/test_dsa/meson.build index d72913a4a88fb..b08b665c024aa 100644 --- a/src/test/modules/test_dsa/meson.build +++ b/src/test/modules/test_dsa/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_dsa_sources = files( 'test_dsa.c', diff --git a/src/test/modules/test_dsa/sql/test_dsa.sql b/src/test/modules/test_dsa/sql/test_dsa.sql index c3d8db9437206..99b4a60dd14ca 100644 --- a/src/test/modules/test_dsa/sql/test_dsa.sql +++ b/src/test/modules/test_dsa/sql/test_dsa.sql @@ -2,3 +2,10 @@ CREATE EXTENSION test_dsa; SELECT test_dsa_basic(); SELECT test_dsa_resowners(); + +-- Test allocations across a pre-defined range of pages. This covers enough +-- range to check for the case of odd-sized segments, without making the test +-- too slow. +SELECT test_dsa_allocate(1001, 2000, 100); +-- Larger size with odd-sized segment. +SELECT test_dsa_allocate(6501, 6600, 100); diff --git a/src/test/modules/test_dsa/test_dsa--1.0.sql b/src/test/modules/test_dsa/test_dsa--1.0.sql index 2904cb23525e3..3ee2e44cc0068 100644 --- a/src/test/modules/test_dsa/test_dsa--1.0.sql +++ b/src/test/modules/test_dsa/test_dsa--1.0.sql @@ -10,3 +10,7 @@ CREATE FUNCTION test_dsa_basic() CREATE FUNCTION test_dsa_resowners() RETURNS pg_catalog.void AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_dsa_allocate(int, int, int) + RETURNS pg_catalog.void + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c index cd24d0f48736d..edcab105de621 100644 --- a/src/test/modules/test_dsa/test_dsa.c +++ b/src/test/modules/test_dsa/test_dsa.c @@ -3,7 +3,7 @@ * test_dsa.c * Test dynamic shared memory areas (DSAs) * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_dsa/test_dsa.c @@ -13,26 +13,36 @@ #include "postgres.h" #include "fmgr.h" +#include "storage/dsm_registry.h" #include "storage/lwlock.h" #include "utils/dsa.h" +#include "utils/freepage.h" #include "utils/resowner.h" PG_MODULE_MAGIC; +static void +init_tranche(void *ptr, void *arg) +{ + int *tranche_id = (int *) ptr; + + *tranche_id = LWLockNewTrancheId("test_dsa"); +} + /* Test basic DSA functionality */ PG_FUNCTION_INFO_V1(test_dsa_basic); Datum test_dsa_basic(PG_FUNCTION_ARGS) { - int tranche_id; + int *tranche_id; + bool found; dsa_area *a; dsa_pointer p[100]; - /* XXX: this tranche is leaked */ - tranche_id = LWLockNewTrancheId(); - LWLockRegisterTranche(tranche_id, "test_dsa"); + tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int), + init_tranche, &found, NULL); - a = dsa_create(tranche_id); + a = dsa_create(*tranche_id); for (int i = 0; i < 100; i++) { p[i] = dsa_allocate(a, 1000); @@ -63,18 +73,18 @@ PG_FUNCTION_INFO_V1(test_dsa_resowners); Datum test_dsa_resowners(PG_FUNCTION_ARGS) { - int tranche_id; + int *tranche_id; + bool found; dsa_area *a; dsa_pointer p[10000]; ResourceOwner oldowner; ResourceOwner childowner; - /* XXX: this tranche is leaked */ - tranche_id = LWLockNewTrancheId(); - LWLockRegisterTranche(tranche_id, "test_dsa"); + tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int), + init_tranche, &found, NULL); /* Create DSA in parent resource owner */ - a = dsa_create(tranche_id); + a = dsa_create(*tranche_id); /* * Switch to child resource owner, and do a bunch of allocations in the @@ -111,3 +121,42 @@ test_dsa_resowners(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + +/* + * test_dsa_allocate + * + * Test DSA allocation across a range of sizes to exercise the pagemap + * sizing logic in make_new_segment(). A fresh DSA is created for each + * iteration so that each allocation triggers a new segment creation, + * including the odd-sized segment path. + */ +PG_FUNCTION_INFO_V1(test_dsa_allocate); +Datum +test_dsa_allocate(PG_FUNCTION_ARGS) +{ + int start_num_pages = PG_GETARG_INT32(0); + int end_num_pages = PG_GETARG_INT32(1); + int step = PG_GETARG_INT32(2); + size_t usable_pages; + int *tranche_id; + bool found; + dsa_area *a; + dsa_pointer dp; + + if (start_num_pages > end_num_pages) + elog(ERROR, "incorrect start and end parameters"); + + tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int), + init_tranche, &found, NULL); + + for (usable_pages = start_num_pages; usable_pages < end_num_pages; usable_pages += step) + { + a = dsa_create(*tranche_id); + dp = dsa_allocate(a, usable_pages * FPM_PAGE_SIZE); + + dsa_free(a, dp); + dsa_detach(a); + } + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_dsm_registry/expected/test_dsm_registry.out b/src/test/modules/test_dsm_registry/expected/test_dsm_registry.out index 8ffbd343a05af..9128e171b1b3e 100644 --- a/src/test/modules/test_dsm_registry/expected/test_dsm_registry.out +++ b/src/test/modules/test_dsm_registry/expected/test_dsm_registry.out @@ -1,3 +1,10 @@ +SELECT name, type, size > 0 AS size_ok +FROM pg_dsm_registry_allocations +WHERE name like 'test_dsm_registry%' ORDER BY name; + name | type | size_ok +------+------+--------- +(0 rows) + CREATE EXTENSION test_dsm_registry; SELECT set_val_in_shmem(1236); set_val_in_shmem @@ -5,6 +12,12 @@ SELECT set_val_in_shmem(1236); (1 row) +SELECT set_val_in_hash('test', '1414'); + set_val_in_hash +----------------- + +(1 row) + \c SELECT get_val_in_shmem(); get_val_in_shmem @@ -12,3 +25,20 @@ SELECT get_val_in_shmem(); 1236 (1 row) +SELECT get_val_in_hash('test'); + get_val_in_hash +----------------- + 1414 +(1 row) + +\c +SELECT name, type, size > 0 AS size_ok +FROM pg_dsm_registry_allocations +WHERE name like 'test_dsm_registry%' ORDER BY name; + name | type | size_ok +------------------------+---------+--------- + test_dsm_registry_dsa | area | t + test_dsm_registry_dsm | segment | t + test_dsm_registry_hash | hash | t +(3 rows) + diff --git a/src/test/modules/test_dsm_registry/meson.build b/src/test/modules/test_dsm_registry/meson.build index 7411b7f79d206..27cce44cdaa0f 100644 --- a/src/test/modules/test_dsm_registry/meson.build +++ b/src/test/modules/test_dsm_registry/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group test_dsm_registry_sources = files( 'test_dsm_registry.c', diff --git a/src/test/modules/test_dsm_registry/sql/test_dsm_registry.sql b/src/test/modules/test_dsm_registry/sql/test_dsm_registry.sql index b3351be0a16bc..a606e8872a14e 100644 --- a/src/test/modules/test_dsm_registry/sql/test_dsm_registry.sql +++ b/src/test/modules/test_dsm_registry/sql/test_dsm_registry.sql @@ -1,4 +1,13 @@ +SELECT name, type, size > 0 AS size_ok +FROM pg_dsm_registry_allocations +WHERE name like 'test_dsm_registry%' ORDER BY name; CREATE EXTENSION test_dsm_registry; SELECT set_val_in_shmem(1236); +SELECT set_val_in_hash('test', '1414'); \c SELECT get_val_in_shmem(); +SELECT get_val_in_hash('test'); +\c +SELECT name, type, size > 0 AS size_ok +FROM pg_dsm_registry_allocations +WHERE name like 'test_dsm_registry%' ORDER BY name; diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry--1.0.sql b/src/test/modules/test_dsm_registry/test_dsm_registry--1.0.sql index 8c55b0919b11f..5da45155be9f5 100644 --- a/src/test/modules/test_dsm_registry/test_dsm_registry--1.0.sql +++ b/src/test/modules/test_dsm_registry/test_dsm_registry--1.0.sql @@ -8,3 +8,9 @@ CREATE FUNCTION set_val_in_shmem(val INT) RETURNS VOID CREATE FUNCTION get_val_in_shmem() RETURNS INT AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION set_val_in_hash(key TEXT, val TEXT) RETURNS VOID + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION get_val_in_hash(key TEXT) RETURNS TEXT + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_dsm_registry/test_dsm_registry.c b/src/test/modules/test_dsm_registry/test_dsm_registry.c index 462a80f8790d9..2b1c44ffcc7ea 100644 --- a/src/test/modules/test_dsm_registry/test_dsm_registry.c +++ b/src/test/modules/test_dsm_registry/test_dsm_registry.c @@ -3,7 +3,7 @@ * test_dsm_registry.c * Test the dynamic shared memory registry. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_dsm_registry/test_dsm_registry.c @@ -15,6 +15,7 @@ #include "fmgr.h" #include "storage/dsm_registry.h" #include "storage/lwlock.h" +#include "utils/builtins.h" PG_MODULE_MAGIC; @@ -24,15 +25,34 @@ typedef struct TestDSMRegistryStruct LWLock lck; } TestDSMRegistryStruct; -static TestDSMRegistryStruct *tdr_state; +typedef struct TestDSMRegistryHashEntry +{ + char key[64]; + dsa_pointer val; +} TestDSMRegistryHashEntry; + +static TestDSMRegistryStruct *tdr_dsm; +static dsa_area *tdr_dsa; +static dshash_table *tdr_hash; + +static const dshash_parameters dsh_params = { + offsetof(TestDSMRegistryHashEntry, val), + sizeof(TestDSMRegistryHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; static void -tdr_init_shmem(void *ptr) +init_tdr_dsm(void *ptr, void *arg) { - TestDSMRegistryStruct *state = (TestDSMRegistryStruct *) ptr; + TestDSMRegistryStruct *dsm = (TestDSMRegistryStruct *) ptr; + + if ((int) (intptr_t) arg != 5432) + elog(ERROR, "unexpected arg value %d", (int) (intptr_t) arg); - LWLockInitialize(&state->lck, LWLockNewTrancheId()); - state->val = 0; + LWLockInitialize(&dsm->lck, LWLockNewTrancheId("test_dsm_registry")); + dsm->val = 0; } static void @@ -40,11 +60,16 @@ tdr_attach_shmem(void) { bool found; - tdr_state = GetNamedDSMSegment("test_dsm_registry", - sizeof(TestDSMRegistryStruct), - tdr_init_shmem, - &found); - LWLockRegisterTranche(tdr_state->lck.tranche, "test_dsm_registry"); + tdr_dsm = GetNamedDSMSegment("test_dsm_registry_dsm", + sizeof(TestDSMRegistryStruct), + init_tdr_dsm, + &found, (void *) (intptr_t) 5432); + + if (tdr_dsa == NULL) + tdr_dsa = GetNamedDSA("test_dsm_registry_dsa", &found); + + if (tdr_hash == NULL) + tdr_hash = GetNamedDSHash("test_dsm_registry_hash", &dsh_params, &found); } PG_FUNCTION_INFO_V1(set_val_in_shmem); @@ -53,9 +78,9 @@ set_val_in_shmem(PG_FUNCTION_ARGS) { tdr_attach_shmem(); - LWLockAcquire(&tdr_state->lck, LW_EXCLUSIVE); - tdr_state->val = PG_GETARG_UINT32(0); - LWLockRelease(&tdr_state->lck); + LWLockAcquire(&tdr_dsm->lck, LW_EXCLUSIVE); + tdr_dsm->val = PG_GETARG_INT32(0); + LWLockRelease(&tdr_dsm->lck); PG_RETURN_VOID(); } @@ -68,9 +93,57 @@ get_val_in_shmem(PG_FUNCTION_ARGS) tdr_attach_shmem(); - LWLockAcquire(&tdr_state->lck, LW_SHARED); - ret = tdr_state->val; - LWLockRelease(&tdr_state->lck); + LWLockAcquire(&tdr_dsm->lck, LW_SHARED); + ret = tdr_dsm->val; + LWLockRelease(&tdr_dsm->lck); + + PG_RETURN_INT32(ret); +} + +PG_FUNCTION_INFO_V1(set_val_in_hash); +Datum +set_val_in_hash(PG_FUNCTION_ARGS) +{ + TestDSMRegistryHashEntry *entry; + char *key = TextDatumGetCString(PG_GETARG_DATUM(0)); + char *val = TextDatumGetCString(PG_GETARG_DATUM(1)); + bool found; + + if (strlen(key) >= offsetof(TestDSMRegistryHashEntry, val)) + ereport(ERROR, + (errmsg("key too long"))); + + tdr_attach_shmem(); + + entry = dshash_find_or_insert(tdr_hash, key, &found); + if (found) + dsa_free(tdr_dsa, entry->val); + + entry->val = dsa_allocate(tdr_dsa, strlen(val) + 1); + strcpy(dsa_get_address(tdr_dsa, entry->val), val); + + dshash_release_lock(tdr_hash, entry); + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(get_val_in_hash); +Datum +get_val_in_hash(PG_FUNCTION_ARGS) +{ + TestDSMRegistryHashEntry *entry; + char *key = TextDatumGetCString(PG_GETARG_DATUM(0)); + text *val = NULL; + + tdr_attach_shmem(); + + entry = dshash_find(tdr_hash, key, false); + if (entry == NULL) + PG_RETURN_NULL(); + + val = cstring_to_text(dsa_get_address(tdr_dsa, entry->val)); + + dshash_release_lock(tdr_hash, entry); - PG_RETURN_UINT32(ret); + PG_RETURN_TEXT_P(val); } diff --git a/src/test/modules/test_escape/t/001_test_escape.pl b/src/test/modules/test_escape/t/001_test_escape.pl index 0d5aec3ed748f..3c6c968c07ba3 100644 --- a/src/test/modules/test_escape/t/001_test_escape.pl +++ b/src/test/modules/test_escape/t/001_test_escape.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; use Config; diff --git a/src/test/modules/test_escape/test_escape.c b/src/test/modules/test_escape/test_escape.c index 59430ed46c484..6234a9bd129cc 100644 --- a/src/test/modules/test_escape/test_escape.c +++ b/src/test/modules/test_escape/test_escape.c @@ -1,7 +1,7 @@ /* * test_escape.c Test escape functions * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_escape/test_escape.c @@ -192,7 +192,7 @@ test_gb18030_page_multiple(pe_test_config *tc) testname = createPQExpBuffer(); appendPQExpBuffer(testname, ">repeat(%c, %zu)", input[0], input_len - 1); escapify(testname, input + input_len - 1, 1); - appendPQExpBuffer(testname, "< - GB18030 - PQescapeLiteral"); + appendPQExpBufferStr(testname, "< - GB18030 - PQescapeLiteral"); /* test itself */ PQsetClientEncoding(tc->conn, "GB18030"); @@ -229,9 +229,9 @@ test_gb18030_json(pe_test_config *tc) /* name to describe the test */ testname = createPQExpBuffer(); - appendPQExpBuffer(testname, ">"); + appendPQExpBufferChar(testname, '>'); escapify(testname, input, input_len); - appendPQExpBuffer(testname, "< - GB18030 - pg_parse_json"); + appendPQExpBufferStr(testname, "< - GB18030 - pg_parse_json"); /* test itself */ lex = makeJsonLexContextCstringLen(NULL, raw_buf->data, input_len, @@ -526,8 +526,6 @@ static pe_test_vector pe_test_vectors[] = TV("gbk", "\x80\""), TV("gbk", "\x80\\"), - TV("mule_internal", "\\\x9c';\0;"), - TV("sql_ascii", "1\xC0'"), /* diff --git a/src/test/modules/test_extensions/Makefile b/src/test/modules/test_extensions/Makefile index a3591bf3d2f35..d1b0b81e5fd3d 100644 --- a/src/test/modules/test_extensions/Makefile +++ b/src/test/modules/test_extensions/Makefile @@ -1,6 +1,7 @@ # src/test/modules/test_extensions/Makefile MODULE = test_extensions +MODULE_big = test_ext PGFILEDESC = "test_extensions - regression testing for EXTENSION support" EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ @@ -11,6 +12,8 @@ EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \ test_ext_set_schema \ test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3 +OBJS = test_ext.o + DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \ test_ext4--1.0.sql test_ext5--1.0.sql test_ext6--1.0.sql \ test_ext7--1.0.sql test_ext7--1.0--2.0.sql \ diff --git a/src/test/modules/test_extensions/expected/test_extdepend.out b/src/test/modules/test_extensions/expected/test_extdepend.out index 0b62015d18c80..ede5dc64c04a7 100644 --- a/src/test/modules/test_extensions/expected/test_extdepend.out +++ b/src/test/modules/test_extensions/expected/test_extdepend.out @@ -186,3 +186,123 @@ DROP MATERIALIZED VIEW d; DROP INDEX e; DROP SCHEMA test_ext CASCADE; NOTICE: drop cascades to table a +-- Fifth test: extension dependencies on partition indexes survive MERGE and +-- SPLIT PARTITION operations, and mismatches between source partitions are +-- reported. +RESET search_path; +CREATE EXTENSION test_ext3; +CREATE EXTENSION test_ext5; +CREATE TABLE part_extdep (i int, x int) PARTITION BY RANGE (i); +CREATE TABLE part_extdep_1 PARTITION OF part_extdep FOR VALUES FROM (1) TO (2); +CREATE TABLE part_extdep_2 PARTITION OF part_extdep FOR VALUES FROM (2) TO (3); +CREATE TABLE part_extdep_3 PARTITION OF part_extdep FOR VALUES FROM (3) TO (4); +CREATE TABLE part_extdep_4 PARTITION OF part_extdep FOR VALUES FROM (4) TO (5); +CREATE TABLE part_extdep_5 PARTITION OF part_extdep FOR VALUES FROM (5) TO (6); +CREATE INDEX part_extdep_i_idx ON part_extdep(i); +CREATE INDEX part_extdep_x_idx ON part_extdep(x); +-- Partitions 1, 2, 3 depend on the same two extensions. +ALTER INDEX part_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5; +ALTER INDEX part_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5; +ALTER INDEX part_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5; +-- Partition 4 depends on a different extension on one index. +ALTER INDEX part_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5; +-- Partition 5 has no dependency at all. +-- Merge matching partitions: should succeed and preserve dependencies on the +-- new partition's indexes (DROP EXTENSION must fail, naming the new index). +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_1, part_extdep_2) + INTO part_extdep_merged; +DROP EXTENSION test_ext3; +ERROR: cannot drop index part_extdep_merged_i_idx because index part_extdep_i_idx requires it +HINT: You can drop index part_extdep_i_idx instead. +SELECT c.relname, e.extname +FROM pg_depend d +JOIN pg_class c ON d.objid = c.oid +JOIN pg_extension e ON d.refobjid = e.oid +WHERE c.relname IN ('part_extdep_merged_i_idx', 'part_extdep_merged_x_idx') + AND e.extname IN ('test_ext3', 'test_ext5') + AND d.deptype = 'x' +ORDER BY c.relname, e.extname; + relname | extname +--------------------------+----------- + part_extdep_merged_i_idx | test_ext3 + part_extdep_merged_x_idx | test_ext5 +(2 rows) + +-- An index created directly on a partition has no parent in the partitioned +-- index tree. Such an index is dropped with its old partition during merge, +-- and any extension dependency it carries goes away with it: the dep is not +-- promoted to the merged partition. Verify by attaching test_ext9 to such +-- an orphan index, merging, and observing that test_ext9 becomes droppable. +CREATE EXTENSION test_ext9; +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x); +ALTER INDEX part_extdep_3_extra_idx DEPENDS ON EXTENSION test_ext9; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3) + INTO part_extdep_merged2; +DROP EXTENSION test_ext9; +-- Mismatched dependencies: partition 4's index depends on a different +-- extension than partition_merged2's. Both orderings must fail, and the +-- error must cite both partition indexes. +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged2, part_extdep_4) + INTO part_extdep_bad; +ERROR: cannot merge partitions with conflicting extension dependencies +DETAIL: Partition indexes "part_extdep_4_i_idx" and "part_extdep_merged2_i_idx" depend on different extensions. +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_merged2) + INTO part_extdep_bad; +ERROR: cannot merge partitions with conflicting extension dependencies +DETAIL: Partition indexes "part_extdep_4_i_idx" and "part_extdep_merged2_i_idx" depend on different extensions. +-- Empty vs non-empty dependency set (the subset case the earlier linear +-- check missed in one direction). +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5) + INTO part_extdep_bad; +ERROR: cannot merge partitions with conflicting extension dependencies +DETAIL: Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions. +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4) + INTO part_extdep_bad; +ERROR: cannot merge partitions with conflicting extension dependencies +DETAIL: Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions. +-- Subset: partition 5's i_idx depends on a strict superset of partition 4's +-- i_idx dependencies. Partition 4 = {test_ext5}, partition 5 will be +-- {test_ext3, test_ext5}. Both orderings must fail; in particular the case +-- where the first partition we walk has fewer extensions than the second +-- must still be rejected. +ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext5; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5) + INTO part_extdep_bad; +ERROR: cannot merge partitions with conflicting extension dependencies +DETAIL: Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions. +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4) + INTO part_extdep_bad; +ERROR: cannot merge partitions with conflicting extension dependencies +DETAIL: Partition indexes "part_extdep_4_i_idx" and "part_extdep_5_i_idx" depend on different extensions. +-- Reset partition 5 so it doesn't interfere with the SPLIT test below. +ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext5; +-- Split: the single source partition's dependencies must appear on every +-- new partition's matching index, identified by extension name. +ALTER TABLE part_extdep SPLIT PARTITION part_extdep_merged2 INTO + (PARTITION part_extdep_s1 FOR VALUES FROM (1) TO (3), + PARTITION part_extdep_s2 FOR VALUES FROM (3) TO (4)); +SELECT c.relname, e.extname +FROM pg_depend d +JOIN pg_class c ON d.objid = c.oid +JOIN pg_extension e ON d.refobjid = e.oid +WHERE c.relname IN ('part_extdep_s1_i_idx', 'part_extdep_s1_x_idx', + 'part_extdep_s2_i_idx', 'part_extdep_s2_x_idx') + AND e.extname IN ('test_ext3', 'test_ext5') + AND d.deptype = 'x' +ORDER BY c.relname, e.extname; + relname | extname +----------------------+----------- + part_extdep_s1_i_idx | test_ext3 + part_extdep_s1_x_idx | test_ext5 + part_extdep_s2_i_idx | test_ext3 + part_extdep_s2_x_idx | test_ext5 +(4 rows) + +DROP TABLE part_extdep; +DROP EXTENSION test_ext3; +DROP EXTENSION test_ext5; diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out index 72bae1bf254b5..fdae52d6ab2bf 100644 --- a/src/test/modules/test_extensions/expected/test_extensions.out +++ b/src/test/modules/test_extensions/expected/test_extensions.out @@ -333,7 +333,7 @@ SELECT ext_cor_func(); ERROR: function ext_cor_func() does not exist LINE 1: SELECT ext_cor_func(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no function of that name. SELECT * FROM ext_cor_view; col ------------------------ @@ -649,7 +649,6 @@ SELECT dep_req3b(); -- fails ERROR: function public.dep_req2() does not exist LINE 1: SELECT public.dep_req2() || ' req3b' ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. QUERY: SELECT public.dep_req2() || ' req3b' CONTEXT: SQL function "dep_req3b" statement 1 DROP EXTENSION test_ext_req_schema3; diff --git a/src/test/modules/test_extensions/meson.build b/src/test/modules/test_extensions/meson.build index 3c7e378bf3595..2c7cea189e286 100644 --- a/src/test/modules/test_extensions/meson.build +++ b/src/test/modules/test_extensions/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_install_data += files( 'test_ext1--1.0.sql', @@ -46,6 +46,19 @@ test_install_data += files( 'test_ext_set_schema.control', ) +test_ext_sources = files('test_ext.c') + +if host_system == 'windows' + test_ext_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_ext', + '--FILEDESC', 'test_ext - test C extension for pg_upgrade',]) +endif + +test_ext = shared_module('test_ext', + test_ext_sources, + kwargs: pg_test_mod_args, +) + tests += { 'name': 'test_extensions', 'sd': meson.current_source_dir(), diff --git a/src/test/modules/test_extensions/sql/test_extdepend.sql b/src/test/modules/test_extensions/sql/test_extdepend.sql index 63240a1af5dfd..ad734af1e71ea 100644 --- a/src/test/modules/test_extensions/sql/test_extdepend.sql +++ b/src/test/modules/test_extensions/sql/test_extdepend.sql @@ -88,3 +88,107 @@ DROP FUNCTION b(); DROP MATERIALIZED VIEW d; DROP INDEX e; DROP SCHEMA test_ext CASCADE; + +-- Fifth test: extension dependencies on partition indexes survive MERGE and +-- SPLIT PARTITION operations, and mismatches between source partitions are +-- reported. +RESET search_path; +CREATE EXTENSION test_ext3; +CREATE EXTENSION test_ext5; + +CREATE TABLE part_extdep (i int, x int) PARTITION BY RANGE (i); +CREATE TABLE part_extdep_1 PARTITION OF part_extdep FOR VALUES FROM (1) TO (2); +CREATE TABLE part_extdep_2 PARTITION OF part_extdep FOR VALUES FROM (2) TO (3); +CREATE TABLE part_extdep_3 PARTITION OF part_extdep FOR VALUES FROM (3) TO (4); +CREATE TABLE part_extdep_4 PARTITION OF part_extdep FOR VALUES FROM (4) TO (5); +CREATE TABLE part_extdep_5 PARTITION OF part_extdep FOR VALUES FROM (5) TO (6); +CREATE INDEX part_extdep_i_idx ON part_extdep(i); +CREATE INDEX part_extdep_x_idx ON part_extdep(x); + +-- Partitions 1, 2, 3 depend on the same two extensions. +ALTER INDEX part_extdep_1_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_1_x_idx DEPENDS ON EXTENSION test_ext5; +ALTER INDEX part_extdep_2_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_2_x_idx DEPENDS ON EXTENSION test_ext5; +ALTER INDEX part_extdep_3_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_3_x_idx DEPENDS ON EXTENSION test_ext5; + +-- Partition 4 depends on a different extension on one index. +ALTER INDEX part_extdep_4_i_idx DEPENDS ON EXTENSION test_ext5; + +-- Partition 5 has no dependency at all. + +-- Merge matching partitions: should succeed and preserve dependencies on the +-- new partition's indexes (DROP EXTENSION must fail, naming the new index). +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_1, part_extdep_2) + INTO part_extdep_merged; +DROP EXTENSION test_ext3; +SELECT c.relname, e.extname +FROM pg_depend d +JOIN pg_class c ON d.objid = c.oid +JOIN pg_extension e ON d.refobjid = e.oid +WHERE c.relname IN ('part_extdep_merged_i_idx', 'part_extdep_merged_x_idx') + AND e.extname IN ('test_ext3', 'test_ext5') + AND d.deptype = 'x' +ORDER BY c.relname, e.extname; + +-- An index created directly on a partition has no parent in the partitioned +-- index tree. Such an index is dropped with its old partition during merge, +-- and any extension dependency it carries goes away with it: the dep is not +-- promoted to the merged partition. Verify by attaching test_ext9 to such +-- an orphan index, merging, and observing that test_ext9 becomes droppable. +CREATE EXTENSION test_ext9; +CREATE INDEX part_extdep_3_extra_idx ON part_extdep_3(x); +ALTER INDEX part_extdep_3_extra_idx DEPENDS ON EXTENSION test_ext9; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged, part_extdep_3) + INTO part_extdep_merged2; +DROP EXTENSION test_ext9; + +-- Mismatched dependencies: partition 4's index depends on a different +-- extension than partition_merged2's. Both orderings must fail, and the +-- error must cite both partition indexes. +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_merged2, part_extdep_4) + INTO part_extdep_bad; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_merged2) + INTO part_extdep_bad; + +-- Empty vs non-empty dependency set (the subset case the earlier linear +-- check missed in one direction). +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5) + INTO part_extdep_bad; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4) + INTO part_extdep_bad; + +-- Subset: partition 5's i_idx depends on a strict superset of partition 4's +-- i_idx dependencies. Partition 4 = {test_ext5}, partition 5 will be +-- {test_ext3, test_ext5}. Both orderings must fail; in particular the case +-- where the first partition we walk has fewer extensions than the second +-- must still be rejected. +ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_5_i_idx DEPENDS ON EXTENSION test_ext5; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_4, part_extdep_5) + INTO part_extdep_bad; +ALTER TABLE part_extdep MERGE PARTITIONS (part_extdep_5, part_extdep_4) + INTO part_extdep_bad; +-- Reset partition 5 so it doesn't interfere with the SPLIT test below. +ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext3; +ALTER INDEX part_extdep_5_i_idx NO DEPENDS ON EXTENSION test_ext5; + +-- Split: the single source partition's dependencies must appear on every +-- new partition's matching index, identified by extension name. +ALTER TABLE part_extdep SPLIT PARTITION part_extdep_merged2 INTO + (PARTITION part_extdep_s1 FOR VALUES FROM (1) TO (3), + PARTITION part_extdep_s2 FOR VALUES FROM (3) TO (4)); +SELECT c.relname, e.extname +FROM pg_depend d +JOIN pg_class c ON d.objid = c.oid +JOIN pg_extension e ON d.refobjid = e.oid +WHERE c.relname IN ('part_extdep_s1_i_idx', 'part_extdep_s1_x_idx', + 'part_extdep_s2_i_idx', 'part_extdep_s2_x_idx') + AND e.extname IN ('test_ext3', 'test_ext5') + AND d.deptype = 'x' +ORDER BY c.relname, e.extname; + +DROP TABLE part_extdep; +DROP EXTENSION test_ext3; +DROP EXTENSION test_ext5; diff --git a/src/test/modules/test_extensions/t/001_extension_control_path.pl b/src/test/modules/test_extensions/t/001_extension_control_path.pl index 1a9c97bbf4dcc..f8302503b4bfa 100644 --- a/src/test/modules/test_extensions/t/001_extension_control_path.pl +++ b/src/test/modules/test_extensions/t/001_extension_control_path.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -9,32 +9,44 @@ my $node = PostgreSQL::Test::Cluster->new('node'); -$node->init; +$node->init('auth_extra' => [ '--create-role', 'user01' ]); -# Create a temporary directory for the extension control file +# Create temporary directories for the extension control files my $ext_dir = PostgreSQL::Test::Utils::tempdir(); mkpath("$ext_dir/extension"); +my $ext_dir2 = PostgreSQL::Test::Utils::tempdir(); +mkpath("$ext_dir2/extension"); my $ext_name = "test_custom_ext_paths"; create_extension($ext_name, $ext_dir); +create_extension($ext_name, $ext_dir2); my $ext_name2 = "test_custom_ext_paths_using_directory"; mkpath("$ext_dir/$ext_name2"); create_extension($ext_name2, $ext_dir, $ext_name2); +# Make windows path use Unix slashes as canonicalize_path() is called when +# collecting extension control paths. See get_extension_control_directories(). +my $ext_dir_canonicalized = $ext_dir; +$ext_dir_canonicalized =~ s!\\!/!g if $windows_os; + # Use the correct separator and escape \ when running on Windows. my $sep = $windows_os ? ";" : ":"; $node->append_conf( 'postgresql.conf', qq{ -extension_control_path = '\$system$sep@{[ $windows_os ? ($ext_dir =~ s/\\/\\\\/gr) : $ext_dir ]}' +extension_control_path = '\$system$sep@{[ $windows_os ? ($ext_dir =~ s/\\/\\\\/gr) : $ext_dir ]}$sep@{[ $windows_os ? ($ext_dir2 =~ s/\\/\\\\/gr) : $ext_dir2 ]}' }); # Start node $node->start; +# Create a user to test permissions to read extension locations. +my $user = "user01"; +$node->safe_psql('postgres', "CREATE USER $user"); + my $ecp = $node->safe_psql('postgres', 'show extension_control_path;'); -is($ecp, "\$system$sep$ext_dir", +is($ecp, "\$system$sep$ext_dir$sep$ext_dir2", "custom extension control directory path configured"); $node->safe_psql('postgres', "CREATE EXTENSION $ext_name"); @@ -43,48 +55,65 @@ my $ret = $node->safe_psql('postgres', "select * from pg_available_extensions where name = '$ext_name'"); is( $ret, - "test_custom_ext_paths|1.0|1.0|Test extension_control_path", - "extension is installed correctly on pg_available_extensions"); + "test_custom_ext_paths|1.0|1.0|$ext_dir_canonicalized/extension|Test extension_control_path", + "extension is shown correctly in pg_available_extensions"); $ret = $node->safe_psql('postgres', "select * from pg_available_extension_versions where name = '$ext_name'"); is( $ret, - "test_custom_ext_paths|1.0|t|t|f|t|||Test extension_control_path", - "extension is installed correctly on pg_available_extension_versions"); + "test_custom_ext_paths|1.0|t|t|f|t|||$ext_dir_canonicalized/extension|Test extension_control_path", + "extension is shown correctly in pg_available_extension_versions"); $ret = $node->safe_psql('postgres', "select * from pg_available_extensions where name = '$ext_name2'"); is( $ret, - "test_custom_ext_paths_using_directory|1.0|1.0|Test extension_control_path", - "extension is installed correctly on pg_available_extensions"); + "test_custom_ext_paths_using_directory|1.0|1.0|$ext_dir_canonicalized/extension|Test extension_control_path", + "extension is shown correctly in pg_available_extensions"); $ret = $node->safe_psql('postgres', "select * from pg_available_extension_versions where name = '$ext_name2'" ); is( $ret, - "test_custom_ext_paths_using_directory|1.0|t|t|f|t|||Test extension_control_path", - "extension is installed correctly on pg_available_extension_versions"); + "test_custom_ext_paths_using_directory|1.0|t|t|f|t|||$ext_dir_canonicalized/extension|Test extension_control_path", + "extension is shown correctly in pg_available_extension_versions"); + +# Test that a non-superuser is not able to read the extension location in +# pg_available_extensions +$ret = $node->safe_psql('postgres', + "select location from pg_available_extensions where name = '$ext_name2'", + connstr => "user=$user"); +is( $ret, + "", + "extension location is hidden in pg_available_extensions for users with insufficient privilege"); -# Ensure that extensions installed on $system is still visible when using with +# Test that a non-superuser is not able to read the extension location in +# pg_available_extension_versions +$ret = $node->safe_psql('postgres', + "select location from pg_available_extension_versions where name = '$ext_name2'", + connstr => "user=$user"); +is( $ret, + "", + "extension location is hidden in pg_available_extension_versions for users with insufficient privilege"); + +# Ensure that extensions installed in $system are still visible when used with # custom extension control path. $ret = $node->safe_psql('postgres', "select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'" ); is($ret, "t", - "\$system extension is installed correctly on pg_available_extensions"); - + "\$system extension is shown correctly in pg_available_extensions"); $ret = $node->safe_psql('postgres', "set extension_control_path = ''; select count(*) > 0 as ok from pg_available_extensions where name = 'plpgsql'" ); is($ret, "t", - "\$system extension is installed correctly on pg_available_extensions with empty extension_control_path" + "\$system extension is shown correctly in pg_available_extensions with empty extension_control_path" ); # Test with an extension that does not exists my ($code, $stdout, $stderr) = $node->psql('postgres', "CREATE EXTENSION invalid"); -is($code, 3, 'error to create an extension that does not exists'); +is($code, 3, 'error creating an extension that does not exist'); like($stderr, qr/ERROR: extension "invalid" is not available/); sub create_extension diff --git a/src/test/modules/test_extensions/test_ext.c b/src/test/modules/test_extensions/test_ext.c new file mode 100644 index 0000000000000..a23165ba67abd --- /dev/null +++ b/src/test/modules/test_extensions/test_ext.c @@ -0,0 +1,22 @@ +/* + * test_ext.c + * + * Dummy C extension for testing extension_control_path with pg_upgrade + * + * Portions Copyright (c) 2026, PostgreSQL Global Development Group + */ +#include "postgres.h" + +#include "fmgr.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(test_ext); + +Datum +test_ext(PG_FUNCTION_ARGS) +{ + ereport(NOTICE, + (errmsg("running successful"))); + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_ginpostinglist/meson.build b/src/test/modules/test_ginpostinglist/meson.build index 08ddf009723e0..e8b257ef97ba6 100644 --- a/src/test/modules/test_ginpostinglist/meson.build +++ b/src/test/modules/test_ginpostinglist/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_ginpostinglist_sources = files( 'test_ginpostinglist.c', diff --git a/src/test/modules/test_ginpostinglist/test_ginpostinglist.c b/src/test/modules/test_ginpostinglist/test_ginpostinglist.c index e1161bdfbfacf..6d84364f5c5a7 100644 --- a/src/test/modules/test_ginpostinglist/test_ginpostinglist.c +++ b/src/test/modules/test_ginpostinglist/test_ginpostinglist.c @@ -3,7 +3,7 @@ * test_ginpostinglist.c * Test varbyte-encoding in ginpostinglist.c * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_ginpostinglist/test_ginpostinglist.c @@ -72,12 +72,12 @@ test_itemptr_pair(BlockNumber blk, OffsetNumber off, int maxsize) ItemPointerGetOffsetNumber(&decoded_itemptrs[0])); if (ndecoded == 2 && - !ItemPointerEquals(&orig_itemptrs[0], &decoded_itemptrs[0])) + !ItemPointerEquals(&orig_itemptrs[1], &decoded_itemptrs[1])) { elog(ERROR, "mismatch on second itemptr: (%u, %d) vs (%u, %d)", - 0, 1, - ItemPointerGetBlockNumber(&decoded_itemptrs[0]), - ItemPointerGetOffsetNumber(&decoded_itemptrs[0])); + blk, off, + ItemPointerGetBlockNumber(&decoded_itemptrs[1]), + ItemPointerGetOffsetNumber(&decoded_itemptrs[1])); } } diff --git a/src/test/modules/test_int128/.gitignore b/src/test/modules/test_int128/.gitignore new file mode 100644 index 0000000000000..277fec6ed2cd6 --- /dev/null +++ b/src/test/modules/test_int128/.gitignore @@ -0,0 +1,2 @@ +/tmp_check/ +/test_int128 diff --git a/src/test/modules/test_int128/Makefile b/src/test/modules/test_int128/Makefile new file mode 100644 index 0000000000000..2e86ee93a9d7c --- /dev/null +++ b/src/test/modules/test_int128/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_int128/Makefile + +PGFILEDESC = "test_int128 - test 128-bit integer arithmetic" + +PROGRAM = test_int128 +OBJS = $(WIN32RES) test_int128.o + +PG_CPPFLAGS = -I$(libpq_srcdir) +PG_LIBS_INTERNAL += $(libpq_pgport) + +NO_INSTALL = 1 +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_int128 +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_int128/meson.build b/src/test/modules/test_int128/meson.build new file mode 100644 index 0000000000000..74456112433c7 --- /dev/null +++ b/src/test/modules/test_int128/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +test_int128_sources = files( + 'test_int128.c', +) + +if host_system == 'windows' + test_int128_sources += rc_bin_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_int128', + '--FILEDESC', 'test int128 program',]) +endif + +test_int128 = executable('test_int128', + test_int128_sources, + dependencies: [frontend_code, libpq], + kwargs: default_bin_args + { + 'install': false, + }, +) +testprep_targets += test_int128 + + +tests += { + 'name': 'test_int128', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_test_int128.pl', + ], + 'deps': [test_int128], + }, +} diff --git a/src/test/modules/test_int128/t/001_test_int128.pl b/src/test/modules/test_int128/t/001_test_int128.pl new file mode 100644 index 0000000000000..71265c47d8c16 --- /dev/null +++ b/src/test/modules/test_int128/t/001_test_int128.pl @@ -0,0 +1,27 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Test 128-bit integer arithmetic code in int128.h + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Utils; +use Test::More; + +# Run the test program with 1M iterations +my $exe = "test_int128"; +my $size = 1_000_000; + +note "testing executable $exe"; + +my ($stdout, $stderr) = run_command([ $exe, $size ]); + +SKIP: +{ + skip "no native int128 type", 2 if $stdout =~ /skipping tests/; + + is($stdout, "", "test_int128: no stdout"); + is($stderr, "", "test_int128: no stderr"); +} + +done_testing(); diff --git a/src/test/modules/test_int128/test_int128.c b/src/test/modules/test_int128/test_int128.c new file mode 100644 index 0000000000000..8f230f3937be2 --- /dev/null +++ b/src/test/modules/test_int128/test_int128.c @@ -0,0 +1,285 @@ +/*------------------------------------------------------------------------- + * + * test_int128.c + * Testbed for roll-our-own 128-bit integer arithmetic. + * + * This is a standalone test program that compares the behavior of an + * implementation in int128.h to an (assumed correct) int128 native type. + * + * Copyright (c) 2017-2026, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/test/modules/test_int128/test_int128.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include + +/* Require a native int128 type */ +#ifdef HAVE_INT128 + +/* + * By default, we test the non-native implementation in int128.h; but + * by predefining USE_NATIVE_INT128 to 1, you can test the native + * implementation, just to be sure. + */ +#ifndef USE_NATIVE_INT128 +#define USE_NATIVE_INT128 0 +#endif + +#include "common/int128.h" +#include "common/pg_prng.h" + +/* + * We assume the parts of this union are laid out compatibly. + */ +typedef union +{ + int128 i128; + INT128 I128; + struct + { +#ifdef WORDS_BIGENDIAN + int64 hi; + uint64 lo; +#else + uint64 lo; + int64 hi; +#endif + } hl; +} test128; + +#define INT128_HEX_FORMAT "%016" PRIx64 "%016" PRIx64 + +/* + * Control version of comparator. + */ +static inline int +my_int128_compare(int128 x, int128 y) +{ + if (x < y) + return -1; + if (x > y) + return 1; + return 0; +} + +/* + * Main program. + * + * Generates a lot of random numbers and tests the implementation for each. + * The results should be reproducible, since we use a fixed PRNG seed. + * + * You can give a loop count if you don't like the default 1B iterations. + */ +int +main(int argc, char **argv) +{ + long count; + + pg_prng_seed(&pg_global_prng_state, (uint64) time(NULL)); + + if (argc >= 2) + count = strtol(argv[1], NULL, 0); + else + count = 1000000000; + + while (count-- > 0) + { + int64 x = pg_prng_int64(&pg_global_prng_state); + int64 y = pg_prng_int64(&pg_global_prng_state); + int64 z = pg_prng_int64(&pg_global_prng_state); + int64 w = pg_prng_int64(&pg_global_prng_state); + int32 z32 = pg_prng_int32(&pg_global_prng_state); + test128 t1; + test128 t2; + test128 t3; + int32 r1; + int32 r2; + + /* prevent division by zero in the 128/32-bit division test */ + while (z32 == 0) + z32 = pg_prng_int32(&pg_global_prng_state); + + /* check unsigned addition */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t1.i128 += (int128) (uint64) z; + int128_add_uint64(&t2.I128, (uint64) z); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " + unsigned %016" PRIx64 "\n", x, y, z); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check signed addition */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t1.i128 += (int128) z; + int128_add_int64(&t2.I128, z); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " + signed %016" PRIx64 "\n", x, y, z); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check 128-bit signed addition */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t3.hl.hi = z; + t3.hl.lo = w; + t1.i128 += t3.i128; + int128_add_int128(&t2.I128, t3.I128); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " + " INT128_HEX_FORMAT "\n", x, y, z, w); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check unsigned subtraction */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t1.i128 -= (int128) (uint64) z; + int128_sub_uint64(&t2.I128, (uint64) z); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " - unsigned %016" PRIx64 "\n", x, y, z); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check signed subtraction */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t1.i128 -= (int128) z; + int128_sub_int64(&t2.I128, z); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " - signed %016" PRIx64 "\n", x, y, z); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check 64x64-bit multiply-add */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t1.i128 += (int128) z * (int128) w; + int128_add_int64_mul_int64(&t2.I128, z, w); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " + %016" PRIx64 " * %016" PRIx64 "\n", x, y, z, w); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check 64x64-bit multiply-subtract */ + t1.hl.hi = x; + t1.hl.lo = y; + t2 = t1; + t1.i128 -= (int128) z * (int128) w; + int128_sub_int64_mul_int64(&t2.I128, z, w); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " - %016" PRIx64 " * %016" PRIx64 "\n", x, y, z, w); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check 128/32-bit division */ + t3.hl.hi = x; + t3.hl.lo = y; + t1.i128 = t3.i128 / z32; + r1 = (int32) (t3.i128 % z32); + t2 = t3; + int128_div_mod_int32(&t2.I128, z32, &r2); + + if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) + { + printf(INT128_HEX_FORMAT " / signed %08X\n", t3.hl.hi, t3.hl.lo, z32); + printf("native = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("result = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + if (r1 != r2) + { + printf(INT128_HEX_FORMAT " %% signed %08X\n", t3.hl.hi, t3.hl.lo, z32); + printf("native = %08X\n", r1); + printf("result = %08X\n", r2); + return 1; + } + + /* check comparison */ + t1.hl.hi = x; + t1.hl.lo = y; + t2.hl.hi = z; + t2.hl.lo = w; + + if (my_int128_compare(t1.i128, t2.i128) != + int128_compare(t1.I128, t2.I128)) + { + printf("comparison failure: %d vs %d\n", + my_int128_compare(t1.i128, t2.i128), + int128_compare(t1.I128, t2.I128)); + printf("arg1 = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("arg2 = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + + /* check case with identical hi parts; above will hardly ever hit it */ + t2.hl.hi = x; + + if (my_int128_compare(t1.i128, t2.i128) != + int128_compare(t1.I128, t2.I128)) + { + printf("comparison failure: %d vs %d\n", + my_int128_compare(t1.i128, t2.i128), + int128_compare(t1.I128, t2.I128)); + printf("arg1 = " INT128_HEX_FORMAT "\n", t1.hl.hi, t1.hl.lo); + printf("arg2 = " INT128_HEX_FORMAT "\n", t2.hl.hi, t2.hl.lo); + return 1; + } + } + + return 0; +} + +#else /* ! HAVE_INT128 */ + +/* + * For now, do nothing if we don't have a native int128 type. + */ +int +main(int argc, char **argv) +{ + printf("skipping tests: no native int128 type\n"); + return 0; +} + +#endif diff --git a/src/test/modules/test_integerset/meson.build b/src/test/modules/test_integerset/meson.build index 16e32e6e8916e..3d7772df3ef7b 100644 --- a/src/test/modules/test_integerset/meson.build +++ b/src/test/modules/test_integerset/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_integerset_sources = files( 'test_integerset.c', diff --git a/src/test/modules/test_integerset/test_integerset.c b/src/test/modules/test_integerset/test_integerset.c index cfdc6762785de..81334d4903d67 100644 --- a/src/test/modules/test_integerset/test_integerset.c +++ b/src/test/modules/test_integerset/test_integerset.c @@ -3,7 +3,7 @@ * test_integerset.c * Test integer set data structure. * - * Copyright (c) 2019-2025, PostgreSQL Global Development Group + * Copyright (c) 2019-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_integerset/test_integerset.c @@ -385,7 +385,7 @@ test_single_value_and_filler(uint64 value, uint64 filler_min, uint64 filler_max) intset = intset_create(); - iter_expected = palloc(sizeof(uint64) * (filler_max - filler_min + 1)); + iter_expected = palloc_array(uint64, filler_max - filler_min + 1); if (value < filler_min) { intset_add_member(intset, value); diff --git a/src/test/modules/test_json_parser/README b/src/test/modules/test_json_parser/README index ceccd499f4358..61e7c78d588a9 100644 --- a/src/test/modules/test_json_parser/README +++ b/src/test/modules/test_json_parser/README @@ -6,10 +6,12 @@ This module contains two programs for testing the json parsers. - `test_json_parser_incremental` is for testing the incremental parser, It reads in a file and passes it in very small chunks (default is 60 bytes at a time) to the incremental parser. It's not meant to be a speed test but to - test the accuracy of the incremental parser. There are two option arguments, - "-c nn" specifies an alternative chunk size, and "-s" specifies using - semantic routines. The semantic routines re-output the json, although not in - a very pretty form. The required non-option argument is the input file name. + test the accuracy of the incremental parser. The option "-c nn" specifies an + alternative chunk size, "-r nn" runs a range of chunk sizes down to one byte + on the same input (with output separated by null bytes), and "-s" specifies + using semantic routines. The semantic routines re-output the json, although + not in a very pretty form. The required non-option argument is the input file + name. - `test_json_parser_perf` is for speed testing both the standard recursive descent parser and the non-recursive incremental parser. If given the `-i` flag it uses the non-recursive parser, diff --git a/src/test/modules/test_json_parser/meson.build b/src/test/modules/test_json_parser/meson.build index 5672045f4964d..2688686e37b38 100644 --- a/src/test/modules/test_json_parser/meson.build +++ b/src/test/modules/test_json_parser/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group test_json_parser_incremental_sources = files( 'test_json_parser_incremental.c', diff --git a/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl b/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl index 80b01abd00e6e..e5621af05c6cc 100644 --- a/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl +++ b/src/test/modules/test_json_parser/t/001_test_json_parser_incremental.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Test the incremental (table-driven) json parser. diff --git a/src/test/modules/test_json_parser/t/002_inline.pl b/src/test/modules/test_json_parser/t/002_inline.pl index 7c8b64977ec7d..9813cf3f433e3 100644 --- a/src/test/modules/test_json_parser/t/002_inline.pl +++ b/src/test/modules/test_json_parser/t/002_inline.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test success or failure of the incremental (table-driven) JSON parser # for a variety of small inputs. @@ -33,23 +33,37 @@ sub test print $fh "$json"; close($fh); + # The -r mode runs the parser in a loop, with output separated by nulls. + # Unpack that as a list of null-terminated ASCII strings (Z*) and check that + # each run produces the same result. + my ($all_stdout, $all_stderr) = + run_command([ @exe, "-r", $chunk, $fname ]); + + my @stdout = unpack("(Z*)*", $all_stdout); + my @stderr = unpack("(Z*)*", $all_stderr); + + is(scalar @stdout, $chunk, "$name: stdout has correct number of entries"); + is(scalar @stderr, $chunk, "$name: stderr has correct number of entries"); + + my $i = 0; + foreach my $size (reverse(1 .. $chunk)) { - my ($stdout, $stderr) = run_command([ @exe, "-c", $size, $fname ]); - if (defined($params{error})) { - unlike($stdout, qr/SUCCESS/, + unlike($stdout[$i], qr/SUCCESS/, "$name, chunk size $size: test fails"); - like($stderr, $params{error}, + like($stderr[$i], $params{error}, "$name, chunk size $size: correct error output"); } else { - like($stdout, qr/SUCCESS/, + like($stdout[$i], qr/SUCCESS/, "$name, chunk size $size: test succeeds"); - is($stderr, "", "$name, chunk size $size: no error output"); + is($stderr[$i], "", "$name, chunk size $size: no error output"); } + + $i++; } } diff --git a/src/test/modules/test_json_parser/t/003_test_semantic.pl b/src/test/modules/test_json_parser/t/003_test_semantic.pl index 7ba8b63467c82..9bd89eccadc03 100644 --- a/src/test/modules/test_json_parser/t/003_test_semantic.pl +++ b/src/test/modules/test_json_parser/t/003_test_semantic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test the incremental JSON parser with semantic routines, and compare the # output with the expected output. diff --git a/src/test/modules/test_json_parser/t/004_test_parser_perf.pl b/src/test/modules/test_json_parser/t/004_test_parser_perf.pl index 9ea21ee89907a..1152e8292b8dd 100644 --- a/src/test/modules/test_json_parser/t/004_test_parser_perf.pl +++ b/src/test/modules/test_json_parser/t/004_test_parser_perf.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test the JSON parser performance tester. Here we are just checking that # the performance tester can run, both with the standard parser and the diff --git a/src/test/modules/test_json_parser/test_json_parser_incremental.c b/src/test/modules/test_json_parser/test_json_parser_incremental.c index d1e3e4ab4ea59..8fbd180c8616e 100644 --- a/src/test/modules/test_json_parser/test_json_parser_incremental.c +++ b/src/test/modules/test_json_parser/test_json_parser_incremental.c @@ -3,7 +3,7 @@ * test_json_parser_incremental.c * Test program for incremental JSON parser * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_json_parser/test_json_parser_incremental.c @@ -12,9 +12,14 @@ * the parser in very small chunks. In practice you would normally use * much larger chunks, but doing this makes it more likely that the * full range of increment handling, especially in the lexer, is exercised. + * * If the "-c SIZE" option is provided, that chunk size is used instead * of the default of 60. * + * If the "-r SIZE" option is provided, a range of chunk sizes from SIZE down to + * 1 are run sequentially. A null byte is printed to the streams after each + * iteration. + * * If the -s flag is given, the program does semantic processing. This should * just mirror back the json, albeit with white space changes. * @@ -88,8 +93,8 @@ main(int argc, char **argv) StringInfoData json; int n_read; size_t chunk_size = DEFAULT_CHUNK_SIZE; + bool run_chunk_ranges = false; struct stat statbuf; - off_t bytes_left; const JsonSemAction *testsem = &nullSemAction; char *testfile; int c; @@ -102,11 +107,14 @@ main(int argc, char **argv) if (!lex) pg_fatal("out of memory"); - while ((c = getopt(argc, argv, "c:os")) != -1) + while ((c = getopt(argc, argv, "r:c:os")) != -1) { switch (c) { - case 'c': /* chunksize */ + case 'r': /* chunk range */ + run_chunk_ranges = true; + pg_fallthrough; + case 'c': /* chunk size */ chunk_size = strtou64(optarg, NULL, 10); if (chunk_size > BUFSIZE) pg_fatal("chunk size cannot exceed %d", BUFSIZE); @@ -116,7 +124,7 @@ main(int argc, char **argv) break; case 's': /* do semantic processing */ testsem = &sem; - sem.semstate = palloc(sizeof(struct DoState)); + sem.semstate = palloc_object(struct DoState); ((struct DoState *) sem.semstate)->lex = lex; ((struct DoState *) sem.semstate)->buf = makeStringInfo(); need_strings = true; @@ -135,8 +143,6 @@ main(int argc, char **argv) exit(1); } - makeJsonLexContextIncremental(lex, PG_UTF8, need_strings); - setJsonLexContextOwnsTokens(lex, lex_owns_tokens); initStringInfo(&json); if ((json_file = fopen(testfile, PG_BINARY_R)) == NULL) @@ -145,61 +151,88 @@ main(int argc, char **argv) if (fstat(fileno(json_file), &statbuf) != 0) pg_fatal("error statting input: %m"); - bytes_left = statbuf.st_size; - - for (;;) + do { - /* We will break when there's nothing left to read */ - - if (bytes_left < chunk_size) - chunk_size = bytes_left; + /* + * This outer loop only repeats in -r mode. Reset the parse state and + * our position in the input file for the inner loop, which performs + * the incremental parsing. + */ + off_t bytes_left = statbuf.st_size; + size_t to_read = chunk_size; - n_read = fread(buff, 1, chunk_size, json_file); - if (n_read < chunk_size) - pg_fatal("error reading input file: %d", ferror(json_file)); + makeJsonLexContextIncremental(lex, PG_UTF8, need_strings); + setJsonLexContextOwnsTokens(lex, lex_owns_tokens); - appendBinaryStringInfo(&json, buff, n_read); + rewind(json_file); + resetStringInfo(&json); - /* - * Append some trailing junk to the buffer passed to the parser. This - * helps us ensure that the parser does the right thing even if the - * chunk isn't terminated with a '\0'. - */ - appendStringInfoString(&json, "1+23 trailing junk"); - bytes_left -= n_read; - if (bytes_left > 0) + for (;;) { - result = pg_parse_json_incremental(lex, testsem, - json.data, n_read, - false); - if (result != JSON_INCOMPLETE) + /* We will break when there's nothing left to read */ + + if (bytes_left < to_read) + to_read = bytes_left; + + n_read = fread(buff, 1, to_read, json_file); + if (n_read < to_read) + pg_fatal("error reading input file: %d", ferror(json_file)); + + appendBinaryStringInfo(&json, buff, n_read); + + /* + * Append some trailing junk to the buffer passed to the parser. + * This helps us ensure that the parser does the right thing even + * if the chunk isn't terminated with a '\0'. + */ + appendStringInfoString(&json, "1+23 trailing junk"); + bytes_left -= n_read; + if (bytes_left > 0) { - fprintf(stderr, "%s\n", json_errdetail(result, lex)); - ret = 1; - goto cleanup; + result = pg_parse_json_incremental(lex, testsem, + json.data, n_read, + false); + if (result != JSON_INCOMPLETE) + { + fprintf(stderr, "%s\n", json_errdetail(result, lex)); + ret = 1; + goto cleanup; + } + resetStringInfo(&json); } - resetStringInfo(&json); - } - else - { - result = pg_parse_json_incremental(lex, testsem, - json.data, n_read, - true); - if (result != JSON_SUCCESS) + else { - fprintf(stderr, "%s\n", json_errdetail(result, lex)); - ret = 1; - goto cleanup; + result = pg_parse_json_incremental(lex, testsem, + json.data, n_read, + true); + if (result != JSON_SUCCESS) + { + fprintf(stderr, "%s\n", json_errdetail(result, lex)); + ret = 1; + goto cleanup; + } + if (!need_strings) + printf("SUCCESS!\n"); + break; } - if (!need_strings) - printf("SUCCESS!\n"); - break; } - } cleanup: + freeJsonLexContext(lex); + + /* + * In -r mode, separate output with nulls so that the calling test can + * split it up, decrement the chunk size, and loop back to the top. + * All other modes immediately fall out of the loop and exit. + */ + if (run_chunk_ranges) + { + fputc('\0', stdout); + fputc('\0', stderr); + } + } while (run_chunk_ranges && (--chunk_size > 0)); + fclose(json_file); - freeJsonLexContext(lex); free(json.data); free(lex); diff --git a/src/test/modules/test_json_parser/test_json_parser_perf.c b/src/test/modules/test_json_parser/test_json_parser_perf.c index e48360b6daae3..9786263a19109 100644 --- a/src/test/modules/test_json_parser/test_json_parser_perf.c +++ b/src/test/modules/test_json_parser/test_json_parser_perf.c @@ -3,7 +3,7 @@ * test_json_parser_perf.c * Performance test program for both flavors of the JSON parser * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_json_parser/test_json_parser_perf.c diff --git a/src/test/modules/test_lfind/meson.build b/src/test/modules/test_lfind/meson.build index 6ae9b3c95eaf0..b9479cb9aa5d0 100644 --- a/src/test/modules/test_lfind/meson.build +++ b/src/test/modules/test_lfind/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_lfind_sources = files( 'test_lfind.c', diff --git a/src/test/modules/test_lfind/test_lfind.c b/src/test/modules/test_lfind/test_lfind.c index 8dcaa8f9fdaec..037f4923175c9 100644 --- a/src/test/modules/test_lfind/test_lfind.c +++ b/src/test/modules/test_lfind/test_lfind.c @@ -3,7 +3,7 @@ * test_lfind.c * Test correctness of optimized linear search functions. * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_lfind/test_lfind.c diff --git a/src/test/modules/test_lwlock_tranches/.gitignore b/src/test/modules/test_lwlock_tranches/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_lwlock_tranches/Makefile b/src/test/modules/test_lwlock_tranches/Makefile new file mode 100644 index 0000000000000..e357b7d6d6680 --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/test_lwlock_tranches/Makefile + +MODULE_big = test_lwlock_tranches +OBJS = \ + $(WIN32RES) \ + test_lwlock_tranches.o +PGFILEDESC = "test_lwlock_tranches - test code for LWLock tranches allocated by extensions" + +EXTENSION = test_lwlock_tranches +DATA = test_lwlock_tranches--1.0.sql + +REGRESS_OPTS = --temp-config $(top_srcdir)/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf +REGRESS = test_lwlock_tranches +NO_INSTALLCHECK = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_lwlock_tranches +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out new file mode 100644 index 0000000000000..fc25e42c4976c --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/expected/test_lwlock_tranches.out @@ -0,0 +1,60 @@ +CREATE EXTENSION test_lwlock_tranches; +-- Test lock tranches created with RequestNamedLWLockTranche() +SELECT test_startup_lwlocks(); + test_startup_lwlocks +---------------------- + +(1 row) + +-- Test creating new lock tranches with LWLockNewTrancheId() +CREATE TEMP TABLE test_tranches(tranche_name text, tranche_id int); +INSERT INTO test_tranches VALUES + ('test_tranche_a', test_lwlock_tranche_create('test_tranche_a')), + ('test_tranche_b', test_lwlock_tranche_create('test_tranche_b')); +-- You can create multiple tranches with the same name. It's confusing, so +-- not recommended, but test it. +INSERT INTO test_tranches VALUES + ('test_tranche_duplicate', test_lwlock_tranche_create('test_tranche_duplicate')), + ('test_tranche_duplicate', test_lwlock_tranche_create('test_tranche_duplicate')); +-- Check that all tranches were assigned different tranche IDs, including +-- the ones with duplicate names +select count(distinct tranche_id) from test_tranches; + count +------- + 4 +(1 row) + +-- Test GetLWLockIdentifier() on tranches created with LWLockNewTrancheId() +select tranche_name, test_lwlock_get_lwlock_identifier(tranche_id) as lookup_result +from test_tranches; + tranche_name | lookup_result +------------------------+------------------------ + test_tranche_a | test_tranche_a + test_tranche_b | test_tranche_b + test_tranche_duplicate | test_tranche_duplicate + test_tranche_duplicate | test_tranche_duplicate +(4 rows) + +-- negative tests +SELECT test_lwlock_tranche_create(NULL); +ERROR: tranche name cannot be NULL +SELECT test_lwlock_tranche_create(repeat('a', 64)); +ERROR: tranche name too long +DETAIL: LWLock tranche names must be no longer than 63 bytes. +SELECT test_lwlock_tranche_lookup('bogus'); +ERROR: requested tranche is not registered +SELECT test_lwlock_tranche_lookup('test_tranche_a'); +ERROR: requested tranche was not registered with RequestNamedLWLockTranche() +SELECT test_lwlock_initialize(65535); +ERROR: tranche 65535 is not registered +-- Test what happens when you use up all the slots. +-- +-- MAX_USER_DEFINED_TRANCHES is 256. Two locks were created with +-- RequestNamedLWLockTranche() when the library was loaded, and we created +-- four more. +insert into test_tranches + select 'test_tranche_consume_all', test_lwlock_tranche_create('test_tranche_consume_all') + from generate_series(1, 256 - 2 - 4); +select test_lwlock_tranche_create('out-of-tranches'); +ERROR: maximum number of tranches already registered +DETAIL: No more than 256 tranches may be registered. diff --git a/src/test/modules/test_lwlock_tranches/meson.build b/src/test/modules/test_lwlock_tranches/meson.build new file mode 100644 index 0000000000000..56212f032bf32 --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/meson.build @@ -0,0 +1,35 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +test_lwlock_tranches_sources = files( + 'test_lwlock_tranches.c', +) + +if host_system == 'windows' + test_lwlock_tranches_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_lwlock_tranches', + '--FILEDESC', 'test_lwlock_tranches - test code for LWLock tranches allocated by extensions',]) +endif + +test_lwlock_tranches = shared_module('test_lwlock_tranches', + test_lwlock_tranches_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_lwlock_tranches + +test_install_data += files( + 'test_lwlock_tranches.control', + 'test_lwlock_tranches--1.0.sql', +) + +tests += { + 'name': 'test_lwlock_tranches', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_lwlock_tranches', + ], + 'regress_args': ['--temp-config', files('test_lwlock_tranches.conf')], + 'runningcheck': false, + }, +} diff --git a/src/test/modules/test_lwlock_tranches/sql/test_lwlock_tranches.sql b/src/test/modules/test_lwlock_tranches/sql/test_lwlock_tranches.sql new file mode 100644 index 0000000000000..6f5fbcc3f237d --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/sql/test_lwlock_tranches.sql @@ -0,0 +1,42 @@ +CREATE EXTENSION test_lwlock_tranches; + +-- Test lock tranches created with RequestNamedLWLockTranche() +SELECT test_startup_lwlocks(); + +-- Test creating new lock tranches with LWLockNewTrancheId() +CREATE TEMP TABLE test_tranches(tranche_name text, tranche_id int); +INSERT INTO test_tranches VALUES + ('test_tranche_a', test_lwlock_tranche_create('test_tranche_a')), + ('test_tranche_b', test_lwlock_tranche_create('test_tranche_b')); + +-- You can create multiple tranches with the same name. It's confusing, so +-- not recommended, but test it. +INSERT INTO test_tranches VALUES + ('test_tranche_duplicate', test_lwlock_tranche_create('test_tranche_duplicate')), + ('test_tranche_duplicate', test_lwlock_tranche_create('test_tranche_duplicate')); + +-- Check that all tranches were assigned different tranche IDs, including +-- the ones with duplicate names +select count(distinct tranche_id) from test_tranches; + +-- Test GetLWLockIdentifier() on tranches created with LWLockNewTrancheId() +select tranche_name, test_lwlock_get_lwlock_identifier(tranche_id) as lookup_result +from test_tranches; + +-- negative tests +SELECT test_lwlock_tranche_create(NULL); +SELECT test_lwlock_tranche_create(repeat('a', 64)); +SELECT test_lwlock_tranche_lookup('bogus'); +SELECT test_lwlock_tranche_lookup('test_tranche_a'); +SELECT test_lwlock_initialize(65535); + +-- Test what happens when you use up all the slots. +-- +-- MAX_USER_DEFINED_TRANCHES is 256. Two locks were created with +-- RequestNamedLWLockTranche() when the library was loaded, and we created +-- four more. +insert into test_tranches + select 'test_tranche_consume_all', test_lwlock_tranche_create('test_tranche_consume_all') + from generate_series(1, 256 - 2 - 4); + +select test_lwlock_tranche_create('out-of-tranches'); diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql new file mode 100644 index 0000000000000..9111580db4822 --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql @@ -0,0 +1,19 @@ +/* src/test/modules/test_lwlock_tranches/test_lwlock_tranches--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_lwlock_tranches" to load this file. \quit + +CREATE FUNCTION test_startup_lwlocks() RETURNS VOID + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_lwlock_tranche_create(tranche_name TEXT) RETURNS INT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_lwlock_tranche_lookup(tranche_name TEXT) RETURNS VOID + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_lwlock_get_lwlock_identifier(event_id INT) RETURNS TEXT + AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION test_lwlock_initialize(tranche_id INT) RETURNS VOID + AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c new file mode 100644 index 0000000000000..9585578f18e69 --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c @@ -0,0 +1,141 @@ +/*-------------------------------------------------------------------------- + * + * test_lwlock_tranches.c + * Test code for LWLock tranches allocated by extensions. + * + * Copyright (c) 2025-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_lwlock_tranches/test_lwlock_tranches.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "miscadmin.h" +#include "storage/lwlock.h" +#include "utils/builtins.h" +#include "utils/wait_classes.h" + +PG_MODULE_MAGIC; + +static shmem_request_hook_type prev_shmem_request_hook; +static void test_lwlock_tranches_shmem_request(void); + +void +_PG_init(void) +{ + prev_shmem_request_hook = shmem_request_hook; + shmem_request_hook = test_lwlock_tranches_shmem_request; +} + +static void +test_lwlock_tranches_shmem_request(void) +{ + if (prev_shmem_request_hook) + prev_shmem_request_hook(); + + /* + * Request some tranches with RequestNamedLWLockTranche() for + * test_startup_lwlocks() + */ + RequestNamedLWLockTranche("test_lwlock_tranches_startup", 1); + RequestNamedLWLockTranche("test_lwlock_tranches_startup10", 10); +} + +/* + * Test locks requested with RequestNamedLWLockTranche + */ +PG_FUNCTION_INFO_V1(test_startup_lwlocks); +Datum +test_startup_lwlocks(PG_FUNCTION_ARGS) +{ + LWLockPadded *lwlock_startup; + LWLockPadded *lwlock_startup10; + + /* Check that the locks can be used */ + lwlock_startup = GetNamedLWLockTranche("test_lwlock_tranches_startup"); + lwlock_startup10 = GetNamedLWLockTranche("test_lwlock_tranches_startup10"); + + LWLockAcquire(&lwlock_startup->lock, LW_EXCLUSIVE); + for (int i = 0; i < 10; i++) + LWLockAcquire(&lwlock_startup10[i].lock, LW_EXCLUSIVE); + + LWLockRelease(&lwlock_startup->lock); + for (int i = 0; i < 10; i++) + LWLockRelease(&lwlock_startup10[i].lock); + + /* + * Check that GetLWLockIdentifier() returns the expected value for + * tranches + */ + if (strcmp("test_lwlock_tranches_startup", + GetLWLockIdentifier(PG_WAIT_LWLOCK, LWTRANCHE_FIRST_USER_DEFINED)) != 0) + elog(ERROR, "incorrect startup lock tranche name"); + if (strcmp("test_lwlock_tranches_startup10", + GetLWLockIdentifier(PG_WAIT_LWLOCK, LWTRANCHE_FIRST_USER_DEFINED + 1)) != 0) + elog(ERROR, "incorrect startup lock tranche name"); + + PG_RETURN_VOID(); +} + +/* + * Wrapper for LWLockNewTrancheId(). + */ +PG_FUNCTION_INFO_V1(test_lwlock_tranche_create); +Datum +test_lwlock_tranche_create(PG_FUNCTION_ARGS) +{ + char *tranche_name = PG_ARGISNULL(0) ? NULL : TextDatumGetCString(PG_GETARG_DATUM(0)); + int tranche_id; + + tranche_id = LWLockNewTrancheId(tranche_name); + + PG_RETURN_INT32(tranche_id); +} + +/* + * Wrapper for GetNamedLWLockTranche(). + */ +PG_FUNCTION_INFO_V1(test_lwlock_tranche_lookup); +Datum +test_lwlock_tranche_lookup(PG_FUNCTION_ARGS) +{ + char *tranche_name = TextDatumGetCString(PG_GETARG_DATUM(0)); + + (void) GetNamedLWLockTranche(tranche_name); + + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(test_lwlock_get_lwlock_identifier); +Datum +test_lwlock_get_lwlock_identifier(PG_FUNCTION_ARGS) +{ + int eventId = PG_GETARG_INT32(0); + const char *tranche_name; + + tranche_name = GetLWLockIdentifier(PG_WAIT_LWLOCK, eventId); + + if (tranche_name) + PG_RETURN_TEXT_P(cstring_to_text(tranche_name)); + else + PG_RETURN_NULL(); +} + +/* + * Wrapper for LWLockInitialize(). + */ +PG_FUNCTION_INFO_V1(test_lwlock_initialize); +Datum +test_lwlock_initialize(PG_FUNCTION_ARGS) +{ + int tranche_id = PG_GETARG_INT32(0); + LWLock lock; + + LWLockInitialize(&lock, tranche_id); + + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf new file mode 100644 index 0000000000000..acbe5bf51c3c4 --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.conf @@ -0,0 +1 @@ +shared_preload_libraries = 'test_lwlock_tranches' diff --git a/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control new file mode 100644 index 0000000000000..1cde04bdc0b9f --- /dev/null +++ b/src/test/modules/test_lwlock_tranches/test_lwlock_tranches.control @@ -0,0 +1,4 @@ +comment = 'Test code for LWLock tranches allocated by extensions' +default_version = '1.0' +module_pathname = '$libdir/test_lwlock_tranches' +relocatable = true diff --git a/src/test/modules/test_misc/Makefile b/src/test/modules/test_misc/Makefile index 919a25fc67fd3..fedbef071ef94 100644 --- a/src/test/modules/test_misc/Makefile +++ b/src/test/modules/test_misc/Makefile @@ -2,7 +2,11 @@ TAP_TESTS = 1 -EXTRA_INSTALL=src/test/modules/injection_points +EXTRA_INSTALL=src/test/modules/injection_points \ + contrib/test_decoding + +# The injection points are cluster-wide, so disable installcheck +NO_INSTALLCHECK = 1 export enable_injection_points diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 9c50de7efb0f7..1b25d98f7f33d 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'test_misc', @@ -16,6 +16,12 @@ tests += { 't/005_timeouts.pl', 't/006_signal_autovacuum.pl', 't/007_catcache_inval.pl', + 't/008_replslot_single_user.pl', + 't/009_log_temp_files.pl', + 't/010_index_concurrently_upsert.pl', + 't/011_lock_stats.pl', ], + # The injection points are cluster-wide, so disable installcheck + 'runningcheck': false, }, } diff --git a/src/test/modules/test_misc/t/001_constraint_validation.pl b/src/test/modules/test_misc/t/001_constraint_validation.pl index 1d86936ec6947..6121c5bcae517 100644 --- a/src/test/modules/test_misc/t/001_constraint_validation.pl +++ b/src/test/modules/test_misc/t/001_constraint_validation.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Verify that ALTER TABLE optimizes certain operations as expected @@ -58,8 +58,9 @@ sub is_table_verified # normal run will verify table data $output = run_sql_command('alter table atacc1 alter test_a set not null;'); ok(!is_table_verified($output), 'with constraint will not scan table'); -ok( $output =~ - m/existing constraints on column "atacc1.test_a" are sufficient to prove that it does not contain nulls/, +like( + $output, + qr/existing constraints on column "atacc1.test_a" are sufficient to prove that it does not contain nulls/, 'test_a proved by constraints'); run_sql_command('alter table atacc1 alter test_a drop not null;'); @@ -70,9 +71,9 @@ sub is_table_verified ); ok(is_table_verified($output), 'table was scanned'); # we may miss debug message for test_a constraint because we need verify table due test_b -ok( !( $output =~ - m/existing constraints on column "atacc1.test_b" are sufficient to prove that it does not contain nulls/ - ), +unlike( + $output, + qr/existing constraints on column "atacc1.test_b" are sufficient to prove that it does not contain nulls/, 'test_b not proved by wrong constraints'); run_sql_command( 'alter table atacc1 alter test_a drop not null, alter test_b drop not null;' @@ -86,11 +87,13 @@ sub is_table_verified 'alter table atacc1 alter test_b set not null, alter test_a set not null;' ); ok(!is_table_verified($output), 'table was not scanned for both columns'); -ok( $output =~ - m/existing constraints on column "atacc1.test_a" are sufficient to prove that it does not contain nulls/, +like( + $output, + qr/existing constraints on column "atacc1.test_a" are sufficient to prove that it does not contain nulls/, 'test_a proved by constraints'); -ok( $output =~ - m/existing constraints on column "atacc1.test_b" are sufficient to prove that it does not contain nulls/, +like( + $output, + qr/existing constraints on column "atacc1.test_b" are sufficient to prove that it does not contain nulls/, 'test_b proved by constraints'); run_sql_command('drop table atacc1;'); @@ -119,8 +122,9 @@ sub is_table_verified 'ALTER TABLE list_parted2 ATTACH PARTITION part_3_4 FOR VALUES IN (3, 4);' ); ok(!is_table_verified($output), 'table part_3_4 not scanned'); -ok( $output =~ - m/partition constraint for table "part_3_4" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "part_3_4" is implied by existing constraints/, 'part_3_4 verified by existing constraints'); # test attach default partition @@ -131,16 +135,18 @@ sub is_table_verified $output = run_sql_command( 'ALTER TABLE list_parted2 ATTACH PARTITION list_parted2_def default;'); ok(!is_table_verified($output), 'table list_parted2_def not scanned'); -ok( $output =~ - m/partition constraint for table "list_parted2_def" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "list_parted2_def" is implied by existing constraints/, 'list_parted2_def verified by existing constraints'); $output = run_sql_command( 'CREATE TABLE part_55_66 PARTITION OF list_parted2 FOR VALUES IN (55, 66);' ); ok(!is_table_verified($output), 'table list_parted2_def not scanned'); -ok( $output =~ - m/updated partition constraint for default partition "list_parted2_def" is implied by existing constraints/, +like( + $output, + qr/updated partition constraint for default partition "list_parted2_def" is implied by existing constraints/, 'updated partition constraint for default partition list_parted2_def'); # test attach another partitioned table @@ -153,11 +159,14 @@ sub is_table_verified ); $output = run_sql_command( 'ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5);'); -ok(!($output =~ m/verifying table "part_5"/), 'table part_5 not scanned'); -ok($output =~ m/verifying table "list_parted2_def"/, +unlike($output, qr/verifying table "part_5"/, 'table part_5 not scanned'); +like( + $output, + qr/verifying table "list_parted2_def"/, 'list_parted2_def scanned'); -ok( $output =~ - m/partition constraint for table "part_5" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "part_5" is implied by existing constraints/, 'part_5 verified by existing constraints'); run_sql_command( @@ -171,11 +180,14 @@ sub is_table_verified ); $output = run_sql_command( 'ALTER TABLE list_parted2 ATTACH PARTITION part_5 FOR VALUES IN (5);'); -ok(!($output =~ m/verifying table "part_5"/), 'table part_5 not scanned'); -ok($output =~ m/verifying table "list_parted2_def"/, +unlike($output, qr/verifying table "part_5"/, 'table part_5 not scanned'); +like( + $output, + qr/verifying table "list_parted2_def"/, 'list_parted2_def scanned'); -ok( $output =~ - m/partition constraint for table "part_5" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "part_5" is implied by existing constraints/, 'part_5 verified by existing constraints'); # Check the case where attnos of the partitioning columns in the table being @@ -190,11 +202,14 @@ sub is_table_verified ALTER TABLE part_6 DROP c;'); $output = run_sql_command( 'ALTER TABLE list_parted2 ATTACH PARTITION part_6 FOR VALUES IN (6);'); -ok(!($output =~ m/verifying table "part_6"/), 'table part_6 not scanned'); -ok($output =~ m/verifying table "list_parted2_def"/, +unlike($output, qr/verifying table "part_6"/, 'table part_6 not scanned'); +like( + $output, + qr/verifying table "list_parted2_def"/, 'list_parted2_def scanned'); -ok( $output =~ - m/partition constraint for table "part_6" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "part_6" is implied by existing constraints/, 'part_6 verified by existing constraints'); # Similar to above, but the table being attached is a partitioned table @@ -219,17 +234,20 @@ sub is_table_verified 'ALTER TABLE part_7 ATTACH PARTITION part_7_a_null FOR VALUES IN (\'a\', null);' ); ok(!is_table_verified($output), 'table not scanned'); -ok( $output =~ - m/partition constraint for table "part_7_a_null" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "part_7_a_null" is implied by existing constraints/, 'part_7_a_null verified by existing constraints'); $output = run_sql_command( 'ALTER TABLE list_parted2 ATTACH PARTITION part_7 FOR VALUES IN (7);'); ok(!is_table_verified($output), 'tables not scanned'); -ok( $output =~ - m/partition constraint for table "part_7" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "part_7" is implied by existing constraints/, 'part_7 verified by existing constraints'); -ok( $output =~ - m/updated partition constraint for default partition "list_parted2_def" is implied by existing constraints/, +like( + $output, + qr/updated partition constraint for default partition "list_parted2_def" is implied by existing constraints/, 'updated partition constraint for default partition list_parted2_def'); run_sql_command( @@ -245,9 +263,9 @@ sub is_table_verified 'ALTER TABLE range_parted ATTACH PARTITION range_part1 FOR VALUES FROM (1, 1) TO (1, 10);' ); ok(is_table_verified($output), 'table range_part1 scanned'); -ok( !( $output =~ - m/partition constraint for table "range_part1" is implied by existing constraints/ - ), +unlike( + $output, + qr/partition constraint for table "range_part1" is implied by existing constraints/, 'range_part1 not verified by existing constraints'); run_sql_command( @@ -259,8 +277,9 @@ sub is_table_verified 'ALTER TABLE range_parted ATTACH PARTITION range_part2 FOR VALUES FROM (1, 10) TO (1, 20);' ); ok(!is_table_verified($output), 'table range_part2 not scanned'); -ok( $output =~ - m/partition constraint for table "range_part2" is implied by existing constraints/, +like( + $output, + qr/partition constraint for table "range_part2" is implied by existing constraints/, 'range_part2 verified by existing constraints'); # If a partitioned table being created or an existing table being attached @@ -278,19 +297,22 @@ sub is_table_verified $output = run_sql_command( 'ALTER TABLE quuux ATTACH PARTITION quuux1 FOR VALUES IN (1);'); ok(is_table_verified($output), 'quuux1 table scanned'); -ok( !( $output =~ - m/partition constraint for table "quuux1" is implied by existing constraints/ - ), +unlike( + $output, + qr/partition constraint for table "quuux1" is implied by existing constraints/, 'quuux1 verified by existing constraints'); run_sql_command('CREATE TABLE quuux2 (a int, b text);'); $output = run_sql_command( 'ALTER TABLE quuux ATTACH PARTITION quuux2 FOR VALUES IN (2);'); -ok(!($output =~ m/verifying table "quuux_default1"/), +unlike( + $output, + qr/verifying table "quuux_default1"/, 'quuux_default1 not scanned'); -ok($output =~ m/verifying table "quuux2"/, 'quuux2 scanned'); -ok( $output =~ - m/updated partition constraint for default partition "quuux_default1" is implied by existing constraints/, +like($output, qr/verifying table "quuux2"/, 'quuux2 scanned'); +like( + $output, + qr/updated partition constraint for default partition "quuux_default1" is implied by existing constraints/, 'updated partition constraint for default partition quuux_default1'); run_sql_command('DROP TABLE quuux1, quuux2;'); @@ -298,15 +320,16 @@ sub is_table_verified $output = run_sql_command( 'CREATE TABLE quuux1 PARTITION OF quuux FOR VALUES IN (1);'); ok(!is_table_verified($output), 'tables not scanned'); -ok( !( $output =~ - m/partition constraint for table "quuux1" is implied by existing constraints/ - ), +unlike( + $output, + qr/partition constraint for table "quuux1" is implied by existing constraints/, 'quuux1 verified by existing constraints'); $output = run_sql_command( 'CREATE TABLE quuux2 PARTITION OF quuux FOR VALUES IN (2);'); ok(!is_table_verified($output), 'tables not scanned'); -ok( $output =~ - m/updated partition constraint for default partition "quuux_default1" is implied by existing constraints/, +like( + $output, + qr/updated partition constraint for default partition "quuux_default1" is implied by existing constraints/, 'updated partition constraint for default partition quuux_default1'); run_sql_command('DROP TABLE quuux;'); diff --git a/src/test/modules/test_misc/t/002_tablespace.pl b/src/test/modules/test_misc/t/002_tablespace.pl index b8a5617c78817..6957423109f36 100644 --- a/src/test/modules/test_misc/t/002_tablespace.pl +++ b/src/test/modules/test_misc/t/002_tablespace.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Simple tablespace tests that can't be replicated on the same host # due to the use of absolute paths, so we keep them out of the regular @@ -29,69 +29,69 @@ # Create a tablespace with an absolute path $result = $node->psql('postgres', "CREATE TABLESPACE regress_ts1 LOCATION '$TS1_LOCATION'"); -ok($result == 0, 'create tablespace with absolute path'); +is($result, 0, 'create tablespace with absolute path'); # Can't create a tablespace where there is one already $result = $node->psql('postgres', "CREATE TABLESPACE regress_ts1 LOCATION '$TS1_LOCATION'"); -ok($result != 0, 'clobber tablespace with absolute path'); +isnt($result, 0, 'clobber tablespace with absolute path'); # Create table in it $result = $node->psql('postgres', "CREATE TABLE t () TABLESPACE regress_ts1"); -ok($result == 0, 'create table in tablespace with absolute path'); +is($result, 0, 'create table in tablespace with absolute path'); # Can't drop a tablespace that still has a table in it $result = $node->psql('postgres', "DROP TABLESPACE regress_ts1"); -ok($result != 0, 'drop tablespace with absolute path'); +isnt($result, 0, 'drop tablespace with absolute path'); # Drop the table $result = $node->psql('postgres', "DROP TABLE t"); -ok($result == 0, 'drop table in tablespace with absolute path'); +is($result, 0, 'drop table in tablespace with absolute path'); # Drop the tablespace $result = $node->psql('postgres', "DROP TABLESPACE regress_ts1"); -ok($result == 0, 'drop tablespace with absolute path'); +is($result, 0, 'drop tablespace with absolute path'); # Create two absolute tablespaces and two in-place tablespaces, so we can # testing various kinds of tablespace moves. $result = $node->psql('postgres', "CREATE TABLESPACE regress_ts1 LOCATION '$TS1_LOCATION'"); -ok($result == 0, 'create tablespace 1 with absolute path'); +is($result, 0, 'create tablespace 1 with absolute path'); $result = $node->psql('postgres', "CREATE TABLESPACE regress_ts2 LOCATION '$TS2_LOCATION'"); -ok($result == 0, 'create tablespace 2 with absolute path'); +is($result, 0, 'create tablespace 2 with absolute path'); $result = $node->psql('postgres', "SET allow_in_place_tablespaces=on; CREATE TABLESPACE regress_ts3 LOCATION ''" ); -ok($result == 0, 'create tablespace 3 with in-place directory'); +is($result, 0, 'create tablespace 3 with in-place directory'); $result = $node->psql('postgres', "SET allow_in_place_tablespaces=on; CREATE TABLESPACE regress_ts4 LOCATION ''" ); -ok($result == 0, 'create tablespace 4 with in-place directory'); +is($result, 0, 'create tablespace 4 with in-place directory'); # Create a table and test moving between absolute and in-place tablespaces $result = $node->psql('postgres', "CREATE TABLE t () TABLESPACE regress_ts1"); -ok($result == 0, 'create table in tablespace 1'); +is($result, 0, 'create table in tablespace 1'); $result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts2"); -ok($result == 0, 'move table abs->abs'); +is($result, 0, 'move table abs->abs'); $result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts3"); -ok($result == 0, 'move table abs->in-place'); +is($result, 0, 'move table abs->in-place'); $result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts4"); -ok($result == 0, 'move table in-place->in-place'); +is($result, 0, 'move table in-place->in-place'); $result = $node->psql('postgres', "ALTER TABLE t SET tablespace regress_ts1"); -ok($result == 0, 'move table in-place->abs'); +is($result, 0, 'move table in-place->abs'); # Drop everything $result = $node->psql('postgres', "DROP TABLE t"); -ok($result == 0, 'create table in tablespace 1'); +is($result, 0, 'create table in tablespace 1'); $result = $node->psql('postgres', "DROP TABLESPACE regress_ts1"); -ok($result == 0, 'drop tablespace 1'); +is($result, 0, 'drop tablespace 1'); $result = $node->psql('postgres', "DROP TABLESPACE regress_ts2"); -ok($result == 0, 'drop tablespace 2'); +is($result, 0, 'drop tablespace 2'); $result = $node->psql('postgres', "DROP TABLESPACE regress_ts3"); -ok($result == 0, 'drop tablespace 3'); +is($result, 0, 'drop tablespace 3'); $result = $node->psql('postgres', "DROP TABLESPACE regress_ts4"); -ok($result == 0, 'drop tablespace 4'); +is($result, 0, 'drop tablespace 4'); $node->stop; diff --git a/src/test/modules/test_misc/t/003_check_guc.pl b/src/test/modules/test_misc/t/003_check_guc.pl index 5ae23192a47c9..be96b5174de8d 100644 --- a/src/test/modules/test_misc/t/003_check_guc.pl +++ b/src/test/modules/test_misc/t/003_check_guc.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Tests to cross-check the consistency of GUC parameters with # postgresql.conf.sample. @@ -44,19 +44,27 @@ # List of all the GUCs found in the sample file. my @gucs_in_file; +# List of all lines with tabs in the sample file. +my @lines_with_tabs; + # Read the sample file line-by-line, checking its contents to build a list # of everything known as a GUC. my $num_tests = 0; +my $line_num = 0; open(my $contents, '<', $sample_file) || die "Could not open $sample_file: $!"; while (my $line = <$contents>) { + $line_num++; + push @lines_with_tabs, $line_num + if $line =~ /\t/; + # Check if this line matches a GUC parameter: # - Each parameter is preceded by "#", but not "# " in the sample # file. # - Valid configuration options are followed immediately by " = ", # with one space before and after the equal sign. - if ($line =~ m/^#?([_[:alnum:]]+) = .*/) + if ($line =~ m/^#([_[:alnum:]]+) = .*/) { # Lower-case conversion matters for some of the GUCs. my $param_name = lc($1); @@ -69,7 +77,12 @@ # Update the list of GUCs found in the sample file, for the # follow-up tests. push @gucs_in_file, $param_name; + + next; } + # Make sure each line starts with either a # or whitespace + fail("$line missing initial # in postgresql.conf.sample") + if $line =~ /^\s*[^#\s]/; } close $contents; @@ -92,6 +105,8 @@ is(scalar(@sample_intersect), 0, "no parameters marked as NOT_IN_SAMPLE in postgresql.conf.sample"); +is(scalar(@lines_with_tabs), 0, "no lines with tabs in postgresql.conf.sample"); + # These would log some information only on errors. foreach my $param (@missing_from_file) { @@ -111,5 +126,9 @@ "found GUC $param in postgresql.conf.sample, marked as NOT_IN_SAMPLE\n" ); } +foreach my $param (@lines_with_tabs) +{ + print("found tab in line $param in postgresql.conf.sample\n"); +} done_testing(); diff --git a/src/test/modules/test_misc/t/004_io_direct.pl b/src/test/modules/test_misc/t/004_io_direct.pl index 44984e5fd42a5..705eb76a1d790 100644 --- a/src/test/modules/test_misc/t/004_io_direct.pl +++ b/src/test/modules/test_misc/t/004_io_direct.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Very simple exercise of direct I/O GUC. diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl index cdce2afd93569..c16b7dbf5e605 100644 --- a/src/test/modules/test_misc/t/005_timeouts.pl +++ b/src/test/modules/test_misc/t/005_timeouts.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/modules/test_misc/t/006_signal_autovacuum.pl b/src/test/modules/test_misc/t/006_signal_autovacuum.pl index e868510931787..2fafe85b9ae9f 100644 --- a/src/test/modules/test_misc/t/006_signal_autovacuum.pl +++ b/src/test/modules/test_misc/t/006_signal_autovacuum.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Test signaling autovacuum worker with pg_signal_autovacuum_worker. # diff --git a/src/test/modules/test_misc/t/007_catcache_inval.pl b/src/test/modules/test_misc/t/007_catcache_inval.pl index 422301b534222..424556261c9c9 100644 --- a/src/test/modules/test_misc/t/007_catcache_inval.pl +++ b/src/test/modules/test_misc/t/007_catcache_inval.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group # Test recursive catalog cache invalidation, i.e. invalidation while a # catalog cache entry is being built. diff --git a/src/test/modules/test_misc/t/008_replslot_single_user.pl b/src/test/modules/test_misc/t/008_replslot_single_user.pl new file mode 100644 index 0000000000000..024bb5cc72067 --- /dev/null +++ b/src/test/modules/test_misc/t/008_replslot_single_user.pl @@ -0,0 +1,95 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Test manipulations of replication slots with the single-user mode. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Skip the tests on Windows, as single-user mode would fail on permission +# failure with privileged accounts. +if ($windows_os) +{ + plan skip_all => 'this test is not supported by this platform'; +} + +# Run set of queries in single-user mode. +sub test_single_mode +{ + my ($node, $queries, $testname) = @_; + + my $result = run_log( + [ + 'postgres', '--single', '-F', + '-c' => 'exit_on_error=true', + '-D' => $node->data_dir, + 'postgres' + ], + '<' => \$queries); + + ok($result, $testname); +} + +my $slot_logical = 'slot_logical'; +my $slot_physical = 'slot_physical'; + +# Initialize a node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init(allows_streaming => "logical"); +$node->start; + +# Define initial table +$node->safe_psql('postgres', "CREATE TABLE foo (id int)"); + +$node->stop; + +test_single_mode( + $node, + "SELECT pg_create_logical_replication_slot('$slot_logical', 'test_decoding')", + "logical slot creation"); +test_single_mode( + $node, + "SELECT pg_create_physical_replication_slot('$slot_physical', true)", + "physical slot creation"); +test_single_mode( + $node, + "SELECT pg_create_physical_replication_slot('slot_tmp', true, true)", + "temporary physical slot creation"); + +test_single_mode( + $node, qq( +INSERT INTO foo VALUES (1); +SELECT pg_logical_slot_get_changes('$slot_logical', NULL, NULL); +), + "logical decoding"); + +test_single_mode( + $node, + "SELECT pg_replication_slot_advance('$slot_logical', pg_current_wal_lsn())", + "logical slot advance"); +test_single_mode( + $node, + "SELECT pg_replication_slot_advance('$slot_physical', pg_current_wal_lsn())", + "physical slot advance"); + +test_single_mode( + $node, + "SELECT pg_copy_logical_replication_slot('$slot_logical', 'slot_log_copy')", + "logical slot copy"); +test_single_mode( + $node, + "SELECT pg_copy_physical_replication_slot('$slot_physical', 'slot_phy_copy')", + "physical slot copy"); + +test_single_mode( + $node, + "SELECT pg_drop_replication_slot('$slot_logical')", + "logical slot drop"); +test_single_mode( + $node, + "SELECT pg_drop_replication_slot('$slot_physical')", + "physical slot drop"); + +done_testing(); diff --git a/src/test/modules/test_misc/t/009_log_temp_files.pl b/src/test/modules/test_misc/t/009_log_temp_files.pl new file mode 100644 index 0000000000000..6411acb4cab70 --- /dev/null +++ b/src/test/modules/test_misc/t/009_log_temp_files.pl @@ -0,0 +1,157 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# Check how temporary file removals and statement queries are associated +# in the server logs for various query sequences with the simple and +# extended query protocols. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize a new PostgreSQL test cluster +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init(); +$node->append_conf( + 'postgresql.conf', qq( +work_mem = 64kB +log_temp_files = 0 +debug_parallel_query = off +log_error_verbosity = default +)); +$node->start; + +# Setup table and populate with data +$node->safe_psql( + "postgres", qq{ +CREATE UNLOGGED TABLE foo(a int); +INSERT INTO foo(a) SELECT * FROM generate_series(1, 5000); +}); + +note "unnamed portal: temporary file dropped under second SELECT query"; +my $log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +BEGIN; +SELECT a FROM foo ORDER BY a OFFSET \$1 \\bind 4990 \\g +SELECT 'unnamed portal'; +END; +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT 'unnamed portal'/s, + $log_offset), + "unnamed portal"); + +note "bind and implicit transaction: temporary file dropped without query"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +SELECT a FROM foo ORDER BY a OFFSET \$1 \\bind 4991 \\g +}); +ok( $node->log_contains(qr/LOG:\s+temporary file:/s, $log_offset), + "bind and implicit transaction, temporary file removed"); +ok( !$node->log_contains(qr/STATEMENT:/s, $log_offset), + "bind and implicit transaction, no statement logged"); + +note "named portal: temporary file dropped under second SELECT query"; +$node->safe_psql( + "postgres", qq{ +BEGIN; +SELECT a FROM foo ORDER BY a OFFSET \$1 \\parse stmt +\\bind_named stmt 4999 \\g +SELECT 'named portal'; +END; +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT 'named portal'/s, + $log_offset), + "named portal"); + +note "pipelined query: temporary file dropped under second SELECT query"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +\\startpipeline +SELECT a FROM foo ORDER BY a OFFSET \$1 \\bind 4992 \\sendpipeline +SELECT 'pipelined query'; +\\endpipeline +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT 'pipelined query'/s, + $log_offset), + "pipelined query"); + +note "parse and bind: temporary file dropped without query"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +SELECT a, a, a FROM foo ORDER BY a OFFSET \$1 \\parse p1 +\\bind_named p1 4993 \\g +}); +ok($node->log_contains(qr/LOG:\s+temporary file:/s, $log_offset), + "parse and bind, temporary file removed"); +ok(!$node->log_contains(qr/STATEMENT:/s, $log_offset), + "bind and bind, no statement logged"); + +note "simple query: temporary file dropped under SELECT query"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +BEGIN; +SELECT a FROM foo ORDER BY a OFFSET 4994; +END; +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+SELECT a FROM foo ORDER BY a OFFSET 4994;/s, + $log_offset), + "simple query"); + +note "cursor: temporary file dropped under CLOSE"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +BEGIN; +DECLARE mycur CURSOR FOR SELECT a FROM foo ORDER BY a OFFSET 4995; +FETCH 10 FROM mycur; +SELECT 1; +CLOSE mycur; +END; +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+CLOSE mycur;/s, + $log_offset), + "cursor"); + +note "cursor WITH HOLD: temporary file dropped under COMMIT"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +BEGIN; +DECLARE holdcur CURSOR WITH HOLD FOR SELECT a FROM foo ORDER BY a OFFSET 4996; +FETCH 10 FROM holdcur; +COMMIT; +CLOSE holdcur; +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+COMMIT;/s, + $log_offset), + "cursor WITH HOLD"); + +note "prepare/execute: temporary file dropped under EXECUTE"; +$log_offset = -s $node->logfile; +$node->safe_psql( + "postgres", qq{ +BEGIN; +PREPARE p1 AS SELECT a FROM foo ORDER BY a OFFSET 4997; +EXECUTE p1; +DEALLOCATE p1; +END; +}); +ok( $node->log_contains( + qr/LOG:\s+temporary file: path.*\n.*\ STATEMENT:\s+EXECUTE p1;/s, + $log_offset), + "prepare/execute"); + +$node->stop('fast'); +done_testing(); diff --git a/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl b/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl new file mode 100644 index 0000000000000..3bdb632887e55 --- /dev/null +++ b/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl @@ -0,0 +1,958 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test INSERT ON CONFLICT DO UPDATE behavior concurrent with +# CREATE INDEX CONCURRENTLY and REINDEX CONCURRENTLY. +# +# These tests verify the fix for "duplicate key value violates unique +# constraint" errors that occurred when infer_arbiter_indexes() only considered +# indisvalid indexes, causing different transactions to use different arbiter +# indexes. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use Time::HiRes qw(usleep); + +plan skip_all => 'Injection points not supported by this build' + unless $ENV{enable_injection_points} eq 'yes'; + +# Node initialization +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init(); +$node->start(); + +# Check if the extension injection_points is available +plan skip_all => 'Extension injection_points not installed' + unless $node->check_extension('injection_points'); + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +$node->safe_psql( + 'postgres', q[ +CREATE SCHEMA test; +CREATE UNLOGGED TABLE test.tblpk (i int PRIMARY KEY, updated_at timestamp); +ALTER TABLE test.tblpk SET (parallel_workers=0); + +CREATE TABLE test.tblparted(i int primary key, updated_at timestamp) PARTITION BY RANGE (i); +CREATE TABLE test.tbl_partition PARTITION OF test.tblparted + FOR VALUES FROM (0) TO (10000) + WITH (parallel_workers = 0); + +CREATE UNLOGGED TABLE test.tblexpr(i int, updated_at timestamp); +CREATE UNIQUE INDEX tbl_pkey_special ON test.tblexpr(abs(i)) WHERE i < 1000; +ALTER TABLE test.tblexpr SET (parallel_workers=0); + +]); + +############################################################################ +note('Test: REINDEX CONCURRENTLY + UPSERT (wakeup at set-dead phase)'); + +# Create sessions with on_error_stop => 0 so psql doesn't exit on SQL errors. +# This allows us to collect stderr and detect errors after the test completes. +my $s1 = $node->background_psql('postgres', on_error_stop => 0); +my $s2 = $node->background_psql('postgres', on_error_stop => 0); +my $s3 = $node->background_psql('postgres', on_error_stop => 0); + +# Setup injection points for each session +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait'); +]); + +# s3 starts REINDEX (will block on reindex-relation-concurrently-before-set-dead) +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tblpk_pkey; +]); + +# Wait for s3 to hit injection point +ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead'); + +# s1 starts UPSERT (will block on check-exclusion-or-unique-constraint-no-conflict) +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +# Wait for s1 to hit injection point +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +# Wakeup s3 to continue (reindex-relation-concurrently-before-set-dead) +wakeup_injection_point($node, + 'reindex-relation-concurrently-before-set-dead'); + +# s2 starts UPSERT (will block on exec-insert-before-insert-speculative) +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +# Wait for s2 to hit injection point +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +# Wakeup s1 (check-exclusion-or-unique-constraint-no-conflict) +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); + +# Wakeup s2 (exec-insert-before-insert-speculative) +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +clean_safe_quit_ok($s1, $s2, $s3); + +# Cleanup test 1 +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk'); + +############################################################################ +note('Test: REINDEX CONCURRENTLY + UPSERT (wakeup at swap phase)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tblpk_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk'); + +############################################################################ +note('Test: REINDEX CONCURRENTLY + UPSERT (s1 wakes before reindex)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tblpk_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +# Start s2 BEFORE waking reindex (key difference from permutation 1) +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +# Wake s1 first, then reindex, then s2 +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); +wakeup_injection_point($node, + 'reindex-relation-concurrently-before-set-dead'); +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk'); + +############################################################################ +note('Test: REINDEX + UPSERT ON CONSTRAINT (set-dead phase)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tblpk_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +wakeup_injection_point($node, + 'reindex-relation-concurrently-before-set-dead'); + +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk'); + +############################################################################ +note('Test: REINDEX + UPSERT ON CONSTRAINT (swap phase)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tblpk_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk'); + +############################################################################ +note('Test: REINDEX + UPSERT ON CONSTRAINT (s1 wakes before reindex)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tblpk_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +# Start s2 BEFORE waking reindex +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +# Wake s1 first, then reindex, then s2 +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); +wakeup_injection_point($node, + 'reindex-relation-concurrently-before-set-dead'); +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk'); + +############################################################################ +note('Test: REINDEX on partitioned table (set-dead phase)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +wakeup_injection_point($node, + 'reindex-relation-concurrently-before-set-dead'); + +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted'); + +############################################################################ +note('Test: REINDEX on partitioned table (swap phase)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted'); + +############################################################################ +note('Test: REINDEX on partitioned table (s1 wakes before reindex)'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait'); +]); + +$s3->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +# Start s2 BEFORE waking reindex +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +# Wake s1 first, then reindex, then s2 +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); +wakeup_injection_point($node, + 'reindex-relation-concurrently-before-set-dead'); +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted'); + +############################################################################ +note('Test: REINDEX on partitioned table, cache inval between two get_partition_ancestors'); + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-init-partition-after-get-partition-ancestors', 'wait'); +]); + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait'); +]); + +$s2->query_until( + qr/starting_reindex/, q[ +\echo starting_reindex +REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey; +]); + +ok_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, + 'exec-init-partition-after-get-partition-ancestors'); + +wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap'); + +wakeup_injection_point($node, + 'exec-init-partition-after-get-partition-ancestors'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted'); + +############################################################################ +note('Test: CREATE INDEX CONCURRENTLY + UPSERT'); +# Uses invalidate-catalog-snapshot-end to test catalog invalidation +# during UPSERT + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +my $s1_pid = $s1->query_safe('SELECT pg_backend_pid()'); + +# s1 attaches BOTH injection points - the unique constraint check AND catalog snapshot +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s1->query_until( + qr/attaching_injection_point/, q[ +\echo attaching_injection_point +SELECT injection_points_attach('invalidate-catalog-snapshot-end', 'wait'); +]); + +# In cases of cache clobbering, s1 may hit the injection point during attach. +# Wait for that session to become idle (attach completed), or wake it up if +# it becomes stuck on injection point. +if (!wait_for_idle($node, $s1_pid)) +{ + ok_injection_point( + $node, + 'invalidate-catalog-snapshot-end', + 's1 hit injection point during attach (cache clobbering mode)'); + $node->safe_psql( + 'postgres', q[ + SELECT injection_points_wakeup('invalidate-catalog-snapshot-end'); + ]); +} + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('define-index-before-set-valid', 'wait'); +]); + +# s3: Start CREATE INDEX CONCURRENTLY (blocks on define-index-before-set-valid) +$s3->query_until( + qr/starting_create_index/, q[ +\echo starting_create_index +CREATE UNIQUE INDEX CONCURRENTLY tbl_pkey_duplicate ON test.tblpk(i); +]); + +ok_injection_point($node, 'define-index-before-set-valid'); + +# s1: Start UPSERT (blocks on invalidate-catalog-snapshot-end) +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'invalidate-catalog-snapshot-end'); + +# Wakeup s3 (CREATE INDEX continues, triggers catalog invalidation) +wakeup_injection_point($node, 'define-index-before-set-valid'); + +# s2: Start UPSERT (blocks on exec-insert-before-insert-speculative) +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, 'invalidate-catalog-snapshot-end'); + +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); + +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); + +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted'); + +############################################################################ +note('Test: CREATE INDEX CONCURRENTLY on partial index + UPSERT'); +# Uses invalidate-catalog-snapshot-end to test catalog invalidation during UPSERT + +$s1 = $node->background_psql('postgres', on_error_stop => 0); +$s2 = $node->background_psql('postgres', on_error_stop => 0); +$s3 = $node->background_psql('postgres', on_error_stop => 0); + +$s1_pid = $s1->query_safe('SELECT pg_backend_pid()'); + +# s1 attaches BOTH injection points - the unique constraint check AND catalog snapshot +$s1->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait'); +]); + +$s1->query_until( + qr/attaching_injection_point/, q[ +\echo attaching_injection_point +SELECT injection_points_attach('invalidate-catalog-snapshot-end', 'wait'); +]); + +# In cases of cache clobbering, s1 may hit the injection point during attach. +# Wait for that session to become idle (attach completed), or wake it up if +# it becomes stuck on injection point. +if (!wait_for_idle($node, $s1_pid)) +{ + ok_injection_point( + $node, + 'invalidate-catalog-snapshot-end', + 's1 hit injection point during attach (cache clobbering mode)'); + $node->safe_psql( + 'postgres', q[ + SELECT injection_points_wakeup('invalidate-catalog-snapshot-end'); + ]); +} + +$s2->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait'); +]); + +$s3->query_safe( + q[ +SELECT injection_points_set_local(); +SELECT injection_points_attach('define-index-before-set-valid', 'wait'); +]); + +# s3: Start CREATE INDEX CONCURRENTLY (blocks on define-index-before-set-valid) +$s3->query_until( + qr/starting_create_index/, q[ +\echo starting_create_index +CREATE UNIQUE INDEX CONCURRENTLY tbl_pkey_special_duplicate ON test.tblexpr(abs(i)) WHERE i < 10000; +]); + +ok_injection_point($node, 'define-index-before-set-valid'); + +# s1: Start UPSERT (blocks on invalidate-catalog-snapshot-end) +$s1->query_until( + qr/starting_upsert_s1/, q[ +\echo starting_upsert_s1 +INSERT INTO test.tblexpr VALUES(13,now()) ON CONFLICT (abs(i)) WHERE i < 100 DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'invalidate-catalog-snapshot-end'); + +# Wakeup s3 (CREATE INDEX continues, triggers catalog invalidation) +wakeup_injection_point($node, 'define-index-before-set-valid'); + +# s2: Start UPSERT (blocks on exec-insert-before-insert-speculative) +$s2->query_until( + qr/starting_upsert_s2/, q[ +\echo starting_upsert_s2 +INSERT INTO test.tblexpr VALUES(13,now()) ON CONFLICT (abs(i)) WHERE i < 100 DO UPDATE SET updated_at = now(); +]); + +ok_injection_point($node, 'exec-insert-before-insert-speculative'); +wakeup_injection_point($node, 'invalidate-catalog-snapshot-end'); +ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict'); +wakeup_injection_point($node, 'exec-insert-before-insert-speculative'); +wakeup_injection_point($node, + 'check-exclusion-or-unique-constraint-no-conflict'); + +clean_safe_quit_ok($s1, $s2, $s3); + +$node->safe_psql('postgres', 'TRUNCATE TABLE test.tblexpr'); + +done_testing(); + +############################################################################ +# Helper functions +# +############################################################################ + +# Helper: Wait for a session to hit an injection point. +# Optional second argument is timeout in seconds. +# Returns true if found, false if timeout. +# On timeout, logs diagnostic information about all active queries. +sub wait_for_injection_point +{ + my ($node, $point_name, $timeout) = @_; + $timeout //= $PostgreSQL::Test::Utils::timeout_default / 2; + + for (my $elapsed = 0; $elapsed < $timeout * 10; $elapsed++) + { + my $pid = $node->safe_psql( + 'postgres', qq[ + SELECT pid FROM pg_stat_activity + WHERE wait_event_type = 'InjectionPoint' + AND wait_event = '$point_name' + LIMIT 1; + ]); + return 1 if $pid ne ''; + usleep(100_000); + } + + # Timeout - report diagnostic information + my $activity = $node->safe_psql( + 'postgres', q[ + SELECT format('pid=%s, state=%s, wait_event_type=%s, wait_event=%s, backend_xmin=%s, backend_xid=%s, query=%s', + pid, state, wait_event_type, wait_event, backend_xmin, backend_xid, left(query, 100)) + FROM pg_stat_activity + ORDER BY pid; + ]); + diag( "wait_for_injection_point timeout waiting for: $point_name\n" + . "Current queries in pg_stat_activity:\n$activity"); + + return 0; +} + +# Test helper: ok() a wait for the given injection point +# Third argument is an optional test name. +sub ok_injection_point +{ + my ($node, $injection_point, $testname) = @_; + $testname //= "hit injection point $injection_point"; + + ok(wait_for_injection_point($node, $injection_point), $testname); +} + +# Helper: Wait for a specific backend to become idle. +# Returns true if idle, false if waiting for injection point or timeout. +sub wait_for_idle +{ + my ($node, $pid, $timeout) = @_; + $timeout //= $PostgreSQL::Test::Utils::timeout_default / 2; + + for (my $elapsed = 0; $elapsed < $timeout * 10; $elapsed++) + { + my $result = $node->safe_psql( + 'postgres', qq[ + SELECT state, wait_event_type FROM pg_stat_activity WHERE pid = $pid; + ]); + my ($state, $wait_event_type) = split(/\|/, $result, 2); + $state //= ''; + $wait_event_type //= ''; + return 1 if $state eq 'idle'; + return 0 if $wait_event_type eq 'InjectionPoint'; + + usleep(100_000); + } + return 0; +} + +# Helper: Detach and wakeup an injection point +sub wakeup_injection_point +{ + my ($node, $point_name) = @_; + $node->safe_psql( + 'postgres', qq[ +SELECT injection_points_detach('$point_name'); +SELECT injection_points_wakeup('$point_name'); +]); +} + +# Wait for any pending query to complete, capture stderr, and close the session. +# Returns the stderr output (excluding internal markers). +sub safe_quit +{ + my ($session) = @_; + + # Send a marker and wait for it to ensure any pending query completes + my $banner = "safe_quit_marker"; + my $banner_match = qr/(^|\n)$banner\r?\n/; + + $session->{stdin} .= "\\echo $banner\n\\warn $banner\n"; + + pump_until( + $session->{run}, $session->{timeout}, + \$session->{stdout}, $banner_match); + pump_until( + $session->{run}, $session->{timeout}, + \$session->{stderr}, $banner_match); + + # Capture stderr (excluding the banner) + my $stderr = $session->{stderr}; + $stderr =~ s/$banner_match//; + + # Close the session + $session->quit; + + return $stderr; +} + +# Helper function: verify that the given sessions exit cleanly. +sub clean_safe_quit_ok +{ + my $i = 1; + foreach my $session (@_) + { + is(safe_quit($session), '', "session " . $i++ . " quit cleanly"); + } +} diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl new file mode 100644 index 0000000000000..45d7d26f70ccb --- /dev/null +++ b/src/test/modules/test_misc/t/011_lock_stats.pl @@ -0,0 +1,319 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test for the lock statistics and log_lock_waits +# +# This test creates multiple locking situations when a session (s2) has to +# wait on a lock for longer than deadlock_timeout. The first tests each test a +# dedicated lock type. +# The last one checks that log_lock_waits has no impact on the statistics +# counters. +# +# This test also checks that log_lock_waits messages are emitted both when +# a wait occurs and when the lock is acquired, and that the "still waiting for" +# message is logged exactly once per wait, even if the backend wakes due +# to signals. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +plan skip_all => 'Injection points not supported by this build' + unless $ENV{enable_injection_points} eq 'yes'; + +my $deadlock_timeout = 10; +my $s1; +my $s2; +my $node; + +# Setup the 2 sessions +sub setup_sessions +{ + $s1 = $node->background_psql('postgres'); + $s2 = $node->background_psql('postgres'); + + # Setup injection points for the waiting session + $s2->query_until( + qr/attaching_injection_point/, q[ + \echo attaching_injection_point + SELECT injection_points_attach('deadlock-timeout-fired', 'wait'); + ]); +} + +# Fetch waits and wait_time from pg_stat_lock for a given lock type +# until they reached expected values: at least one wait and waiting longer +# than the deadlock_timeout. +sub wait_for_pg_stat_lock +{ + my ($node, $lock_type) = @_; + + $node->poll_query_until( + 'postgres', qq[ + SELECT waits > 0 AND wait_time >= $deadlock_timeout + FROM pg_stat_lock + WHERE locktype = '$lock_type'; + ]) or die "Timed out waiting for pg_stat_lock for $lock_type"; +} + +# Convenience wrapper to wait for a point, then detach it. +sub wait_and_detach +{ + my ($node, $point_name) = @_; + + $node->wait_for_event('client backend', $point_name); + $node->safe_psql( + 'postgres', qq[ +SELECT injection_points_detach('$point_name'); +SELECT injection_points_wakeup('$point_name'); +]); +} + +# Node initialization +$node = PostgreSQL::Test::Cluster->new('node'); +$node->init(); +$node->append_conf('postgresql.conf', + "deadlock_timeout = ${deadlock_timeout}ms"); +$node->start(); + +# Check if the extension injection_points is available +plan skip_all => 'Extension injection_points not installed' + unless $node->check_extension('injection_points'); + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +$node->safe_psql( + 'postgres', q[ +CREATE TABLE test_stat_tab(key text not null, value int); +INSERT INTO test_stat_tab(key, value) VALUES('k0', 1); +]); + +############################################################################ + +####### Relation lock + +setup_sessions(); + +my $log_offset = -s $node->logfile; + +$s1->query_safe( + q[ +SELECT pg_stat_reset_shared('lock'); +BEGIN; +LOCK TABLE test_stat_tab; +]); + +# s2 setup +$s2->query_safe( + q[ +BEGIN; +SELECT pg_stat_force_next_flush(); +]); +# s2 blocks on LOCK. +$s2->query_until( + qr/lock_s2/, q[ +\echo lock_s2 +LOCK TABLE test_stat_tab; +]); + +wait_and_detach($node, 'deadlock-timeout-fired'); + +# Check that log_lock_waits message is emitted during a lock wait. +$node->wait_for_log(qr/still waiting for AccessExclusiveLock on relation/, + $log_offset); + +# Wake the backend waiting on the lock and confirm it woke by calling +# pg_log_backend_memory_contexts() and checking for the logged memory +# contexts. This is necessary to test later that the "still waiting for" +# message is logged exactly once per wait, even if the backend wakes +# during the wait. +$node->safe_psql( + 'postgres', q[SELECT pg_log_backend_memory_contexts(pid) + FROM pg_locks WHERE locktype = 'relation' AND + relation = 'test_stat_tab'::regclass AND NOT granted;]); +$node->wait_for_log(qr/logging memory contexts/, $log_offset); + +# deadlock_timeout fired, now commit in s1 and s2 +$s1->query_safe(q(COMMIT)); +$s2->query_safe(q(COMMIT)); + +# check that pg_stat_lock has been updated +wait_for_pg_stat_lock($node, 'relation'); +ok(1, "Lock stats ok for relation"); + +# Check that log_lock_waits message is emitted when the lock is acquired +# after waiting. +$node->wait_for_log(qr/acquired AccessExclusiveLock on relation/, + $log_offset); + +# Check that the "still waiting for" message is logged exactly once per wait, +# even if the backend wakes during the wait. +my $log_contents = slurp_file($node->logfile, $log_offset); +my @still_waiting = ($log_contents =~ /still waiting for/g); +is( scalar @still_waiting, + 1, + "still waiting logged exactly once despite wakeups from pg_log_backend_memory_contexts()" +); + +# close sessions +$s1->quit; +$s2->quit; + +####### transaction lock + +setup_sessions(); + +$log_offset = -s $node->logfile; + +$s1->query_safe( + q[ +SELECT pg_stat_reset_shared('lock'); +INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1); +BEGIN; +UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1'; +]); + +# s2 setup +$s2->query_safe( + q[ +SET log_lock_waits = on; +BEGIN; +SELECT pg_stat_force_next_flush(); +]); +# s2 blocks here on UPDATE +$s2->query_until( + qr/lock_s2/, q[ +\echo lock_s2 +UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1'; +]); + +wait_and_detach($node, 'deadlock-timeout-fired'); + +# Check that log_lock_waits message is emitted during a lock wait. +$node->wait_for_log(qr/still waiting for ShareLock on transaction/, + $log_offset); + +# deadlock_timeout fired, now commit in s1 and s2 +$s1->query_safe(q(COMMIT)); +$s2->query_safe(q(COMMIT)); + +# check that pg_stat_lock has been updated +wait_for_pg_stat_lock($node, 'transactionid'); +ok(1, "Lock stats ok for transactionid"); + +# Check that log_lock_waits message is emitted when the lock is acquired +# after waiting. +$node->wait_for_log(qr/acquired ShareLock on transaction/, $log_offset); + +# Close sessions +$s1->quit; +$s2->quit; + +####### advisory lock + +setup_sessions(); + +$log_offset = -s $node->logfile; + +$s1->query_safe( + q[ +SELECT pg_stat_reset_shared('lock'); +SELECT pg_advisory_lock(1); +]); + +# s2 setup +$s2->query_safe( + q[ +SET log_lock_waits = on; +BEGIN; +SELECT pg_stat_force_next_flush(); +]); +# s2 blocks on the advisory lock. +$s2->query_until( + qr/lock_s2/, q[ +\echo lock_s2 +SELECT pg_advisory_lock(1); +]); + +wait_and_detach($node, 'deadlock-timeout-fired'); + +# Check that log_lock_waits message is emitted during a lock wait. +$node->wait_for_log(qr/still waiting for ExclusiveLock on advisory lock/, + $log_offset); + +# deadlock_timeout fired, now unlock and commit s2 +$s1->query_safe(q(SELECT pg_advisory_unlock(1))); +$s2->query_safe( + q[ +SELECT pg_advisory_unlock(1); +COMMIT; +]); + +# check that pg_stat_lock has been updated +wait_for_pg_stat_lock($node, 'advisory'); +ok(1, "Lock stats ok for advisory"); + +# Check that log_lock_waits message is emitted when the lock is acquired +# after waiting. +$node->wait_for_log(qr/acquired ExclusiveLock on advisory lock/, $log_offset); + +# Close sessions +$s1->quit; +$s2->quit; + +####### Ensure log_lock_waits has no impact + +setup_sessions(); + +$log_offset = -s $node->logfile; + +$s1->query_safe( + q[ +SELECT pg_stat_reset_shared('lock'); +BEGIN; +LOCK TABLE test_stat_tab; +]); + +# s2 setup +$s2->query_safe( + q[ +SET log_lock_waits = off; +BEGIN; +SELECT pg_stat_force_next_flush(); +]); +# s2 blocks on LOCK. +$s2->query_until( + qr/lock_s2/, q[ +\echo lock_s2 +LOCK TABLE test_stat_tab; +]); + +wait_and_detach($node, 'deadlock-timeout-fired'); + +# deadlock_timeout fired, now commit in s1 and s2 +$s1->query_safe(q(COMMIT)); +$s2->query_safe(q(COMMIT)); + +# check that pg_stat_lock has been updated +wait_for_pg_stat_lock($node, 'relation'); +ok(1, "log_lock_waits has no impact on Lock stats"); + +# Check that no log_lock_waits messages are emitted +ok( !$node->log_contains( + "still waiting for AccessExclusiveLock on relation", $log_offset), + 'check that no log_lock_waits message is emitted during a lock wait'); +ok( !$node->log_contains( + "acquired AccessExclusiveLock on relation", $log_offset), + 'check that no log_lock_waits message is emitted when the lock is acquired after waiting' +); + +# close sessions +$s1->quit; +$s2->quit; + +# cleanup +$node->safe_psql('postgres', q[DROP TABLE test_stat_tab;]); + +done_testing(); diff --git a/src/test/modules/test_oat_hooks/meson.build b/src/test/modules/test_oat_hooks/meson.build index 1e600b0f4c7d0..66e65bc0c524c 100644 --- a/src/test/modules/test_oat_hooks/meson.build +++ b/src/test/modules/test_oat_hooks/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_oat_hooks_sources = files( 'test_oat_hooks.c', diff --git a/src/test/modules/test_oat_hooks/test_oat_hooks.c b/src/test/modules/test_oat_hooks/test_oat_hooks.c index 447cbbd03dee5..57fd4b5cc9054 100644 --- a/src/test/modules/test_oat_hooks/test_oat_hooks.c +++ b/src/test/modules/test_oat_hooks/test_oat_hooks.c @@ -3,7 +3,7 @@ * test_oat_hooks.c * Code for testing mandatory access control (MAC) using object access hooks. * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_oat_hooks/test_oat_hooks.c diff --git a/src/test/modules/test_parser/meson.build b/src/test/modules/test_parser/meson.build index 0e13469fafc2a..4ee251b4f9557 100644 --- a/src/test/modules/test_parser/meson.build +++ b/src/test/modules/test_parser/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_parser_sources = files( 'test_parser.c', diff --git a/src/test/modules/test_parser/test_parser.c b/src/test/modules/test_parser/test_parser.c index 15ed3617cb50e..65d97e2f5714d 100644 --- a/src/test/modules/test_parser/test_parser.c +++ b/src/test/modules/test_parser/test_parser.c @@ -3,7 +3,7 @@ * test_parser.c * Simple example of a text search parser * - * Copyright (c) 2007-2025, PostgreSQL Global Development Group + * Copyright (c) 2007-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_parser/test_parser.c @@ -46,7 +46,7 @@ PG_FUNCTION_INFO_V1(testprs_lextype); Datum testprs_start(PG_FUNCTION_ARGS) { - ParserState *pst = (ParserState *) palloc0(sizeof(ParserState)); + ParserState *pst = palloc0_object(ParserState); pst->buffer = (char *) PG_GETARG_POINTER(0); pst->len = PG_GETARG_INT32(1); @@ -112,7 +112,7 @@ testprs_lextype(PG_FUNCTION_ARGS) * the same lexids like Teodor in the default word parser; in this way we * can reuse the headline function of the default word parser. */ - LexDescr *descr = (LexDescr *) palloc(sizeof(LexDescr) * (2 + 1)); + LexDescr *descr = palloc_array(LexDescr, 2 + 1); /* there are only two types in this parser */ descr[0].lexid = 3; diff --git a/src/test/modules/test_pg_dump/meson.build b/src/test/modules/test_pg_dump/meson.build index d8e1719586aee..1d2f57360923b 100644 --- a/src/test/modules/test_pg_dump/meson.build +++ b/src/test/modules/test_pg_dump/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_install_data += files( 'test_pg_dump.control', diff --git a/src/test/modules/test_pg_dump/t/001_base.pl b/src/test/modules/test_pg_dump/t/001_base.pl index adcaa419616cd..3d65ce4497a2d 100644 --- a/src/test/modules/test_pg_dump/t/001_base.pl +++ b/src/test/modules/test_pg_dump/t/001_base.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -981,7 +981,8 @@ if ($tests{$test}->{like}->{$test_key} && !defined($tests{$test}->{unlike}->{$test_key})) { - if (!ok($output_file =~ $tests{$test}->{regexp}, + if (!like( + $output_file, $tests{$test}->{regexp}, "$run: should dump $test")) { diag("Review $run results in $tempdir"); @@ -989,7 +990,8 @@ } else { - if (!ok($output_file !~ $tests{$test}->{regexp}, + if (!unlike( + $output_file, $tests{$test}->{regexp}, "$run: should not dump $test")) { diag("Review $run results in $tempdir"); diff --git a/src/test/modules/test_plan_advice/.gitignore b/src/test/modules/test_plan_advice/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_plan_advice/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_plan_advice/Makefile b/src/test/modules/test_plan_advice/Makefile new file mode 100644 index 0000000000000..d101fd6a6aa36 --- /dev/null +++ b/src/test/modules/test_plan_advice/Makefile @@ -0,0 +1,28 @@ +# src/test/modules/test_plan_advice/Makefile + +PGFILEDESC = "test_plan_advice - test whether generated plan advice works" + +MODULE_big = test_plan_advice +OBJS = \ + $(WIN32RES) \ + test_plan_advice.o + +EXTRA_INSTALL = contrib/pg_plan_advice + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +override CPPFLAGS += -I$(includedir_server)/contrib/pg_plan_advice +else +subdir = src/test/modules/test_plan_advice +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +override CPPFLAGS += -I$(top_srcdir)/contrib/pg_plan_advice +endif + +REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX) +export REGRESS_SHLIB diff --git a/src/test/modules/test_plan_advice/meson.build b/src/test/modules/test_plan_advice/meson.build new file mode 100644 index 0000000000000..3dfa950ac79f1 --- /dev/null +++ b/src/test/modules/test_plan_advice/meson.build @@ -0,0 +1,30 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +test_plan_advice_sources = files( + 'test_plan_advice.c', +) + +if host_system == 'windows' + test_plan_advice_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_plan_advice', + '--FILEDESC', 'test_plan_advice - test whether generated plan advice works',]) +endif + +test_plan_advice = shared_module('test_plan_advice', + test_plan_advice_sources, + include_directories: pg_plan_advice_inc, + kwargs: pg_test_mod_args, +) +test_install_libs += test_plan_advice + +tests += { + 'name': 'test_plan_advice', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_replan_regress.pl', + ], + 'test_kwargs': {'priority': 50} + }, +} diff --git a/src/test/modules/test_plan_advice/t/001_replan_regress.pl b/src/test/modules/test_plan_advice/t/001_replan_regress.pl new file mode 100644 index 0000000000000..452b179a665f7 --- /dev/null +++ b/src/test/modules/test_plan_advice/t/001_replan_regress.pl @@ -0,0 +1,66 @@ +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +# Run the core regression tests under pg_plan_advice to check for problems. +use strict; +use warnings FATAL => 'all'; + +use Cwd qw(abs_path); +use File::Basename qw(dirname); + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize the primary node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(); + +# Set up our desired configuration. +$node->append_conf('postgresql.conf', <start; + +my $srcdir = abs_path("../../../.."); + +# --dlpath is needed to be able to find the location of regress.so +# and any libraries the regression tests require. +my $dlpath = dirname($ENV{REGRESS_SHLIB}); + +# --outputdir points to the path where to place the output files. +my $outputdir = $PostgreSQL::Test::Utils::tmp_check; + +# --inputdir points to the path of the input files. +my $inputdir = "$srcdir/src/test/regress"; + +# Run the tests. +my $rc = + system($ENV{PG_REGRESS} . " " + . "--bindir= " + . "--dlpath=\"$dlpath\" " + . "--host=" . $node->host . " " + . "--port=" . $node->port . " " + . "--schedule=$srcdir/src/test/regress/parallel_schedule " + . "--max-concurrent-tests=20 " + . "--inputdir=\"$inputdir\" " + . "--outputdir=\"$outputdir\""); + +# Dump out the regression diffs file, if there is one +if ($rc != 0) +{ + my $diffs = "$outputdir/regression.diffs"; + if (-e $diffs) + { + print "=== dumping $diffs ===\n"; + print slurp_file($diffs); + print "=== EOF ===\n"; + } +} + +# Report results +is($rc, 0, 'regression tests pass'); + +done_testing(); diff --git a/src/test/modules/test_plan_advice/test_plan_advice.c b/src/test/modules/test_plan_advice/test_plan_advice.c new file mode 100644 index 0000000000000..cff5039b5c816 --- /dev/null +++ b/src/test/modules/test_plan_advice/test_plan_advice.c @@ -0,0 +1,143 @@ +/*------------------------------------------------------------------------- + * + * test_plan_advice.c + * Test pg_plan_advice by planning every query with generated advice. + * + * With this module loaded, every time a query is executed, we end up + * planning it twice. The first time we plan it, we generate plan advice, + * which we then feed back to pg_plan_advice as the supplied plan advice. + * It is then planned a second time using that advice. This hopefully + * allows us to detect cases where the advice is incorrect or causes + * failures or plan changes for some reason. + * + * Copyright (c) 2016-2026, PostgreSQL Global Development Group + * + * src/test/modules/test_plan_advice/test_plan_advice.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/xact.h" +#include "fmgr.h" +#include "optimizer/optimizer.h" +#include "pg_plan_advice.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +static bool in_recursion = false; + +static char *test_plan_advice_advisor(PlannerGlobal *glob, + Query *parse, + const char *query_string, + int cursorOptions, + ExplainState *es); +static DefElem *find_defelem_by_defname(List *deflist, char *defname); + +/* + * Initialize this module. + */ +void +_PG_init(void) +{ + void (*add_advisor_fn) (pg_plan_advice_advisor_hook hook); + + /* + * Ask pg_plan_advice to get advice strings from test_plan_advice_advisor + */ + add_advisor_fn = + load_external_function("pg_plan_advice", "pg_plan_advice_add_advisor", + true, NULL); + + (*add_advisor_fn) (test_plan_advice_advisor); +} + +/* + * Re-plan the given query and return the generated advice string as the + * supplied advice. + */ +static char * +test_plan_advice_advisor(PlannerGlobal *glob, Query *parse, + const char *query_string, int cursorOptions, + ExplainState *es) +{ + PlannedStmt *pstmt; + int save_nestlevel = 0; + DefElem *pgpa_item; + DefElem *advice_string_item; + + /* + * Since this function is called from the planner and triggers planning, + * we need a recursion guard. + */ + if (in_recursion) + return NULL; + + PG_TRY(); + { + in_recursion = true; + + /* + * Planning can trigger expression evaluation, which can result in + * sending NOTICE messages or other output to the client. To avoid + * that, we set client_min_messages = ERROR in the hopes of getting + * the same output with and without this module. + * + * We also need to set pg_plan_advice.always_store_advice_details so + * that pg_plan_advice will generate an advice string, since the whole + * point of this function is to get access to that. + */ + save_nestlevel = NewGUCNestLevel(); + set_config_option("client_min_messages", "error", + PGC_SUSET, PGC_S_SESSION, + GUC_ACTION_SAVE, true, 0, false); + set_config_option("pg_plan_advice.always_store_advice_details", "true", + PGC_SUSET, PGC_S_SESSION, + GUC_ACTION_SAVE, true, 0, false); + + /* + * Replan. We must copy the Query, because the planner modifies it. + * (As noted elsewhere, that's unfortunate; perhaps it will be fixed + * some day.) + */ + pstmt = planner(copyObject(parse), query_string, cursorOptions, + glob->boundParams, es); + } + PG_FINALLY(); + { + in_recursion = false; + } + PG_END_TRY(); + + /* Roll back any GUC changes */ + if (save_nestlevel > 0) + AtEOXact_GUC(false, save_nestlevel); + + /* Extract and return the advice string */ + pgpa_item = find_defelem_by_defname(pstmt->extension_state, + "pg_plan_advice"); + if (pgpa_item == NULL) + elog(ERROR, "extension state for pg_plan_advice not found"); + advice_string_item = find_defelem_by_defname((List *) pgpa_item->arg, + "advice_string"); + if (advice_string_item == NULL) + elog(ERROR, + "advice string for pg_plan_advice not found in extension state"); + return strVal(advice_string_item->arg); +} + +/* + * Search a list of DefElem objects for a given defname. + */ +static DefElem * +find_defelem_by_defname(List *deflist, char *defname) +{ + foreach_node(DefElem, item, deflist) + { + if (strcmp(item->defname, defname) == 0) + return item; + } + + return NULL; +} diff --git a/src/test/modules/test_predtest/expected/test_predtest.out b/src/test/modules/test_predtest/expected/test_predtest.out index 6d21bcd603ee6..ad82b4f8f91cd 100644 --- a/src/test/modules/test_predtest/expected/test_predtest.out +++ b/src/test/modules/test_predtest/expected/test_predtest.out @@ -1066,7 +1066,7 @@ w_r_holds | t -- as does nullness of the array select * from test_predtest($$ -select x = any(opaque_array(array[y])), array[y] is null +select x = any(opaque_array(array[y])), opaque_array(array[y]) is null from integers $$); -[ RECORD 1 ]-----+-- diff --git a/src/test/modules/test_predtest/meson.build b/src/test/modules/test_predtest/meson.build index 738b76b6ab842..10418c91472fe 100644 --- a/src/test/modules/test_predtest/meson.build +++ b/src/test/modules/test_predtest/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_predtest_sources = files( 'test_predtest.c', diff --git a/src/test/modules/test_predtest/sql/test_predtest.sql b/src/test/modules/test_predtest/sql/test_predtest.sql index 072eb5b0d5022..dc59f0c22f079 100644 --- a/src/test/modules/test_predtest/sql/test_predtest.sql +++ b/src/test/modules/test_predtest/sql/test_predtest.sql @@ -431,7 +431,7 @@ $$); -- as does nullness of the array select * from test_predtest($$ -select x = any(opaque_array(array[y])), array[y] is null +select x = any(opaque_array(array[y])), opaque_array(array[y]) is null from integers $$); diff --git a/src/test/modules/test_predtest/test_predtest.c b/src/test/modules/test_predtest/test_predtest.c index be5b8c409146b..48ca2a4ea700a 100644 --- a/src/test/modules/test_predtest/test_predtest.c +++ b/src/test/modules/test_predtest/test_predtest.c @@ -3,7 +3,7 @@ * test_predtest.c * Test correctness of optimizer's predicate proof logic. * - * Copyright (c) 2018-2025, PostgreSQL Global Development Group + * Copyright (c) 2018-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_predtest/test_predtest.c @@ -230,6 +230,7 @@ test_predtest(PG_FUNCTION_ARGS) "s_r_holds", BOOLOID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 8, "w_r_holds", BOOLOID, -1, 0); + TupleDescFinalize(tupdesc); tupdesc = BlessTupleDesc(tupdesc); values[0] = BoolGetDatum(strong_implied_by); diff --git a/src/test/modules/test_radixtree/meson.build b/src/test/modules/test_radixtree/meson.build index 02b77c37d497c..85432f3f9adeb 100644 --- a/src/test/modules/test_radixtree/meson.build +++ b/src/test/modules/test_radixtree/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group test_radixtree_sources = files( 'test_radixtree.c', diff --git a/src/test/modules/test_radixtree/test_radixtree.c b/src/test/modules/test_radixtree/test_radixtree.c index 32de6a3123e4d..4ad1abaf46056 100644 --- a/src/test/modules/test_radixtree/test_radixtree.c +++ b/src/test/modules/test_radixtree/test_radixtree.c @@ -3,7 +3,7 @@ * test_radixtree.c * Test module for adaptive radix tree. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_radixtree/test_radixtree.c @@ -44,7 +44,7 @@ uint64 _expected = (expected_expr); \ if (_result != _expected) \ elog(ERROR, \ - "%s yielded " UINT64_HEX_FORMAT ", expected " UINT64_HEX_FORMAT " (%s) in file \"%s\" line %u", \ + "%s yielded %" PRIx64 ", expected %" PRIx64 " (%s) in file \"%s\" line %u", \ #result_expr, _result, _expected, #expected_expr, __FILE__, __LINE__); \ } while (0) @@ -124,10 +124,9 @@ test_empty(void) rt_iter *iter; uint64 key; #ifdef TEST_SHARED_RT - int tranche_id = LWLockNewTrancheId(); + int tranche_id = LWLockNewTrancheId("test_radix_tree"); dsa_area *dsa; - LWLockRegisterTranche(tranche_id, "test_radix_tree"); dsa = dsa_create(tranche_id); radixtree = rt_create(dsa, tranche_id); #else @@ -167,10 +166,9 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc) uint64 *keys; int children = test_info->nkeys; #ifdef TEST_SHARED_RT - int tranche_id = LWLockNewTrancheId(); + int tranche_id = LWLockNewTrancheId("test_radix_tree"); dsa_area *dsa; - LWLockRegisterTranche(tranche_id, "test_radix_tree"); dsa = dsa_create(tranche_id); radixtree = rt_create(dsa, tranche_id); #else @@ -185,7 +183,7 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc) elog(NOTICE, "testing node %s with shift %d and %s keys", test_info->class_name, shift, asc ? "ascending" : "descending"); - keys = palloc(sizeof(uint64) * children); + keys = palloc_array(uint64, children); for (int i = 0; i < children; i++) { if (asc) @@ -221,7 +219,7 @@ test_basic(rt_node_class_test_elem *test_info, int shift, bool asc) TestValueType update = keys[i] + 1; /* rt_set should report the key found */ - EXPECT_TRUE(rt_set(radixtree, keys[i], (TestValueType *) &update)); + EXPECT_TRUE(rt_set(radixtree, keys[i], &update)); } /* delete and re-insert keys */ @@ -304,10 +302,9 @@ test_random(void) int num_keys = 100000; uint64 *keys; #ifdef TEST_SHARED_RT - int tranche_id = LWLockNewTrancheId(); + int tranche_id = LWLockNewTrancheId("test_radix_tree"); dsa_area *dsa; - LWLockRegisterTranche(tranche_id, "test_radix_tree"); dsa = dsa_create(tranche_id); radixtree = rt_create(dsa, tranche_id); #else diff --git a/src/test/modules/test_rbtree/meson.build b/src/test/modules/test_rbtree/meson.build index f98e4b2bc6ef8..0a14fe3c4074c 100644 --- a/src/test/modules/test_rbtree/meson.build +++ b/src/test/modules/test_rbtree/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_rbtree_sources = files( 'test_rbtree.c', diff --git a/src/test/modules/test_rbtree/test_rbtree.c b/src/test/modules/test_rbtree/test_rbtree.c index 9113f1c8d52a0..39c199c6760c5 100644 --- a/src/test/modules/test_rbtree/test_rbtree.c +++ b/src/test/modules/test_rbtree/test_rbtree.c @@ -3,7 +3,7 @@ * test_rbtree.c * Test correctness of red-black tree operations. * - * Copyright (c) 2009-2025, PostgreSQL Global Development Group + * Copyright (c) 2009-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_rbtree/test_rbtree.c diff --git a/src/test/modules/test_regex/expected/test_regex.out b/src/test/modules/test_regex/expected/test_regex.out index c44c717edf4cf..a3d35fc142cfc 100644 --- a/src/test/modules/test_regex/expected/test_regex.out +++ b/src/test/modules/test_regex/expected/test_regex.out @@ -5,7 +5,6 @@ -- Most commented lines below are copied from reg.test. Each -- test case is followed by an equivalent test using test_regex(). create extension test_regex; -set standard_conforming_strings = on; -- # support functions and preliminary misc. -- # This is sensitive to changes in message wording, but we really have to -- # test the code->message expansion at least once. diff --git a/src/test/modules/test_regex/expected/test_regex_utf8.out b/src/test/modules/test_regex/expected/test_regex_utf8.out index befd75e96f3e2..329780ef4001b 100644 --- a/src/test/modules/test_regex/expected/test_regex_utf8.out +++ b/src/test/modules/test_regex/expected/test_regex_utf8.out @@ -8,7 +8,6 @@ SELECT getdatabaseencoding() <> 'UTF8' \quit \endif set client_encoding = utf8; -set standard_conforming_strings = on; -- Run the Tcl test cases that require Unicode -- expectMatch 9.44 EMP* {a[\u00fe-\u0507][\u00ff-\u0300]b} \ -- "a\u0102\u02ffb" "a\u0102\u02ffb" diff --git a/src/test/modules/test_regex/meson.build b/src/test/modules/test_regex/meson.build index a3e20fa3c9464..9b342d6d23af1 100644 --- a/src/test/modules/test_regex/meson.build +++ b/src/test/modules/test_regex/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_regex_sources = files( 'test_regex.c', diff --git a/src/test/modules/test_regex/sql/test_regex.sql b/src/test/modules/test_regex/sql/test_regex.sql index b2a847577e805..8f80ce0fe1d29 100644 --- a/src/test/modules/test_regex/sql/test_regex.sql +++ b/src/test/modules/test_regex/sql/test_regex.sql @@ -7,8 +7,6 @@ create extension test_regex; -set standard_conforming_strings = on; - -- # support functions and preliminary misc. -- # This is sensitive to changes in message wording, but we really have to -- # test the code->message expansion at least once. diff --git a/src/test/modules/test_regex/sql/test_regex_utf8.sql b/src/test/modules/test_regex/sql/test_regex_utf8.sql index 2aa3e0f1022d3..1f69f105fd775 100644 --- a/src/test/modules/test_regex/sql/test_regex_utf8.sql +++ b/src/test/modules/test_regex/sql/test_regex_utf8.sql @@ -11,8 +11,6 @@ SELECT getdatabaseencoding() <> 'UTF8' set client_encoding = utf8; -set standard_conforming_strings = on; - -- Run the Tcl test cases that require Unicode diff --git a/src/test/modules/test_regex/test_regex.c b/src/test/modules/test_regex/test_regex.c index 2548a0ef7b159..cfe569aa0602c 100644 --- a/src/test/modules/test_regex/test_regex.c +++ b/src/test/modules/test_regex/test_regex.c @@ -3,7 +3,7 @@ * test_regex.c * Test harness for the regular expression package. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -14,6 +14,7 @@ #include "postgres.h" +#include "catalog/pg_type_d.h" #include "funcapi.h" #include "regex/regex.h" #include "utils/array.h" @@ -107,10 +108,8 @@ test_regex(PG_FUNCTION_ARGS) true); /* Pre-create workspace that build_test_match_result needs */ - matchctx->elems = (Datum *) palloc(sizeof(Datum) * - (matchctx->npatterns + 1)); - matchctx->nulls = (bool *) palloc(sizeof(bool) * - (matchctx->npatterns + 1)); + matchctx->elems = palloc_array(Datum, matchctx->npatterns + 1); + matchctx->nulls = palloc_array(bool, matchctx->npatterns + 1); MemoryContextSwitchTo(oldcontext); funcctx->user_fctx = matchctx; @@ -413,7 +412,8 @@ parse_test_flags(test_re_flags *flags, text *opts) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid regular expression test option: \"%.*s\"", - pg_mblen(opt_p + i), opt_p + i))); + pg_mblen_range(opt_p + i, opt_p + opt_len), + opt_p + i))); break; } } @@ -436,7 +436,7 @@ setup_test_matches(text *orig_str, Oid collation, bool use_subpatterns) { - test_regex_ctx *matchctx = palloc0(sizeof(test_regex_ctx)); + test_regex_ctx *matchctx = palloc0_object(test_regex_ctx); int eml = pg_database_encoding_max_length(); int orig_len; pg_wchar *wide_str; @@ -457,7 +457,7 @@ setup_test_matches(text *orig_str, /* convert string to pg_wchar form for matching */ orig_len = VARSIZE_ANY_EXHDR(orig_str); - wide_str = (pg_wchar *) palloc(sizeof(pg_wchar) * (orig_len + 1)); + wide_str = palloc_array(pg_wchar, orig_len + 1); wide_len = pg_mb2wchar_with_len(VARDATA_ANY(orig_str), wide_str, orig_len); /* do we want to remember subpatterns? */ @@ -474,7 +474,7 @@ setup_test_matches(text *orig_str, } /* temporary output space for RE package */ - pmatch = palloc(sizeof(regmatch_t) * pmatch_len); + pmatch = palloc_array(regmatch_t, pmatch_len); /* * the real output space (grown dynamically if needed) @@ -483,7 +483,7 @@ setup_test_matches(text *orig_str, * than at 2^27 */ array_len = re_flags->glob ? 255 : 31; - matchctx->match_locs = (int *) palloc(sizeof(int) * array_len); + matchctx->match_locs = palloc_array(int, array_len); array_idx = 0; /* search for the pattern, perhaps repeatedly */ diff --git a/src/test/modules/test_resowner/meson.build b/src/test/modules/test_resowner/meson.build index b1ae1fc76ea3c..a8b5721fe8d2f 100644 --- a/src/test/modules/test_resowner/meson.build +++ b/src/test/modules/test_resowner/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_resowner_sources = files( 'test_resowner_basic.c', diff --git a/src/test/modules/test_resowner/test_resowner_basic.c b/src/test/modules/test_resowner/test_resowner_basic.c index 8f7949963719f..c05f5ca11f394 100644 --- a/src/test/modules/test_resowner/test_resowner_basic.c +++ b/src/test/modules/test_resowner/test_resowner_basic.c @@ -3,7 +3,7 @@ * test_resowner_basic.c * Test basic ResourceOwner functionality * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_resowner/test_resowner_basic.c @@ -35,13 +35,13 @@ static const ResourceOwnerDesc string_desc = { static void ReleaseString(Datum res) { - elog(NOTICE, "releasing string: %s", DatumGetPointer(res)); + elog(NOTICE, "releasing string: %s", DatumGetCString(res)); } static char * PrintString(Datum res) { - return psprintf("test string \"%s\"", DatumGetPointer(res)); + return psprintf("test string \"%s\"", DatumGetCString(res)); } /* demonstrates phases and priorities between a parent and child context */ diff --git a/src/test/modules/test_resowner/test_resowner_many.c b/src/test/modules/test_resowner/test_resowner_many.c index 1f64939404f48..4962b371bdce9 100644 --- a/src/test/modules/test_resowner/test_resowner_many.c +++ b/src/test/modules/test_resowner/test_resowner_many.c @@ -3,7 +3,7 @@ * test_resowner_many.c * Test ResourceOwner functionality with lots of resources * - * Copyright (c) 2022-2025, PostgreSQL Global Development Group + * Copyright (c) 2022-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_resowner/test_resowner_many.c @@ -121,7 +121,7 @@ RememberManyTestResources(ResourceOwner owner, for (int i = 0; i < nresources; i++) { - ManyTestResource *mres = palloc(sizeof(ManyTestResource)); + ManyTestResource *mres = palloc_object(ManyTestResource); mres->kind = &kinds[kind_idx]; dlist_node_init(&mres->node); diff --git a/src/test/modules/test_rls_hooks/meson.build b/src/test/modules/test_rls_hooks/meson.build index 77c1f3ee586fd..153de2caaaa37 100644 --- a/src/test/modules/test_rls_hooks/meson.build +++ b/src/test/modules/test_rls_hooks/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_rls_hooks_sources = files( 'test_rls_hooks.c', diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.c b/src/test/modules/test_rls_hooks/test_rls_hooks.c index b1f161cf7bbf7..1a293d43bff04 100644 --- a/src/test/modules/test_rls_hooks/test_rls_hooks.c +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.c @@ -3,7 +3,7 @@ * test_rls_hooks.c * Code for testing RLS hooks. * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_rls_hooks/test_rls_hooks.c @@ -44,7 +44,7 @@ List * test_rls_hooks_permissive(CmdType cmdtype, Relation relation) { List *policies = NIL; - RowSecurityPolicy *policy = palloc0(sizeof(RowSecurityPolicy)); + RowSecurityPolicy *policy = palloc0_object(RowSecurityPolicy); Datum role; FuncCall *n; Node *e; @@ -112,7 +112,7 @@ List * test_rls_hooks_restrictive(CmdType cmdtype, Relation relation) { List *policies = NIL; - RowSecurityPolicy *policy = palloc0(sizeof(RowSecurityPolicy)); + RowSecurityPolicy *policy = palloc0_object(RowSecurityPolicy); Datum role; FuncCall *n; Node *e; diff --git a/src/test/modules/test_rls_hooks/test_rls_hooks.h b/src/test/modules/test_rls_hooks/test_rls_hooks.h index ff4141ea6b60d..586cfb3badb1b 100644 --- a/src/test/modules/test_rls_hooks/test_rls_hooks.h +++ b/src/test/modules/test_rls_hooks/test_rls_hooks.h @@ -3,7 +3,7 @@ * test_rls_hooks.h * Definitions for RLS hooks * - * Copyright (c) 2015-2025, PostgreSQL Global Development Group + * Copyright (c) 2015-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_rls_hooks/test_rls_hooks.h diff --git a/src/test/modules/test_saslprep/.gitignore b/src/test/modules/test_saslprep/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_saslprep/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_saslprep/Makefile b/src/test/modules/test_saslprep/Makefile new file mode 100644 index 0000000000000..f74375ee4ab45 --- /dev/null +++ b/src/test/modules/test_saslprep/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/test_saslprep/Makefile + +MODULE_big = test_saslprep +OBJS = \ + $(WIN32RES) \ + test_saslprep.o +PGFILEDESC = "test_saslprep - test SASLprep implementation" + +EXTENSION = test_saslprep +DATA = test_saslprep--1.0.sql + +REGRESS = test_saslprep + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_saslprep +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_saslprep/README b/src/test/modules/test_saslprep/README new file mode 100644 index 0000000000000..3064436df019b --- /dev/null +++ b/src/test/modules/test_saslprep/README @@ -0,0 +1,25 @@ +src/test/modules/test_saslprep + +Tests for SASLprep +================== + +This repository contains a test suite for stressing the SASLprep +implementation internal to PostgreSQL. + +It provides a set of functions able to check the validity of a SASLprep +operation for a single byte as well as a range of these, acting as +wrappers around pg_saslprep(). + +Running the tests +================= + +NOTE: A portion of the tests requires --enable-tap-tests, with +PG_TEST_EXTRA=saslprep set to run the TAP test suite. + +Run + make check PG_TEST_EXTRA=saslprep +or + make installcheck PG_TEST_EXTRA=saslprep + +The SQL test suite can run with or without PG_TEST_EXTRA=saslprep +set. diff --git a/src/test/modules/test_saslprep/expected/test_saslprep.out b/src/test/modules/test_saslprep/expected/test_saslprep.out new file mode 100644 index 0000000000000..92f93365343e3 --- /dev/null +++ b/src/test/modules/test_saslprep/expected/test_saslprep.out @@ -0,0 +1,152 @@ +-- Tests for SASLprep +CREATE EXTENSION test_saslprep; +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + test_saslprep +----------------- + (,INVALID_UTF8) +(1 row) + +-- Range of ASCII characters. +SELECT + CASE + WHEN a = 0 THEN '' + WHEN a < 32 THEN '' + WHEN a = 127 THEN '' + ELSE chr(a) END AS dat, + set_byte('\x00'::bytea, 0, a) AS byt, + test_saslprep(set_byte('\x00'::bytea, 0, a)) AS saslprep + FROM generate_series(0,127) AS a; + dat | byt | saslprep +----------+------+------------------- + | \x00 | (,PROHIBITED) + | \x01 | (,PROHIBITED) + | \x02 | (,PROHIBITED) + | \x03 | (,PROHIBITED) + | \x04 | (,PROHIBITED) + | \x05 | (,PROHIBITED) + | \x06 | (,PROHIBITED) + | \x07 | (,PROHIBITED) + | \x08 | (,PROHIBITED) + | \x09 | (,PROHIBITED) + | \x0a | (,PROHIBITED) + | \x0b | (,PROHIBITED) + | \x0c | (,PROHIBITED) + | \x0d | (,PROHIBITED) + | \x0e | (,PROHIBITED) + | \x0f | (,PROHIBITED) + | \x10 | (,PROHIBITED) + | \x11 | (,PROHIBITED) + | \x12 | (,PROHIBITED) + | \x13 | (,PROHIBITED) + | \x14 | (,PROHIBITED) + | \x15 | (,PROHIBITED) + | \x16 | (,PROHIBITED) + | \x17 | (,PROHIBITED) + | \x18 | (,PROHIBITED) + | \x19 | (,PROHIBITED) + | \x1a | (,PROHIBITED) + | \x1b | (,PROHIBITED) + | \x1c | (,PROHIBITED) + | \x1d | (,PROHIBITED) + | \x1e | (,PROHIBITED) + | \x1f | (,PROHIBITED) + | \x20 | ("\\x20",SUCCESS) + ! | \x21 | ("\\x21",SUCCESS) + " | \x22 | ("\\x22",SUCCESS) + # | \x23 | ("\\x23",SUCCESS) + $ | \x24 | ("\\x24",SUCCESS) + % | \x25 | ("\\x25",SUCCESS) + & | \x26 | ("\\x26",SUCCESS) + ' | \x27 | ("\\x27",SUCCESS) + ( | \x28 | ("\\x28",SUCCESS) + ) | \x29 | ("\\x29",SUCCESS) + * | \x2a | ("\\x2a",SUCCESS) + + | \x2b | ("\\x2b",SUCCESS) + , | \x2c | ("\\x2c",SUCCESS) + - | \x2d | ("\\x2d",SUCCESS) + . | \x2e | ("\\x2e",SUCCESS) + / | \x2f | ("\\x2f",SUCCESS) + 0 | \x30 | ("\\x30",SUCCESS) + 1 | \x31 | ("\\x31",SUCCESS) + 2 | \x32 | ("\\x32",SUCCESS) + 3 | \x33 | ("\\x33",SUCCESS) + 4 | \x34 | ("\\x34",SUCCESS) + 5 | \x35 | ("\\x35",SUCCESS) + 6 | \x36 | ("\\x36",SUCCESS) + 7 | \x37 | ("\\x37",SUCCESS) + 8 | \x38 | ("\\x38",SUCCESS) + 9 | \x39 | ("\\x39",SUCCESS) + : | \x3a | ("\\x3a",SUCCESS) + ; | \x3b | ("\\x3b",SUCCESS) + < | \x3c | ("\\x3c",SUCCESS) + = | \x3d | ("\\x3d",SUCCESS) + > | \x3e | ("\\x3e",SUCCESS) + ? | \x3f | ("\\x3f",SUCCESS) + @ | \x40 | ("\\x40",SUCCESS) + A | \x41 | ("\\x41",SUCCESS) + B | \x42 | ("\\x42",SUCCESS) + C | \x43 | ("\\x43",SUCCESS) + D | \x44 | ("\\x44",SUCCESS) + E | \x45 | ("\\x45",SUCCESS) + F | \x46 | ("\\x46",SUCCESS) + G | \x47 | ("\\x47",SUCCESS) + H | \x48 | ("\\x48",SUCCESS) + I | \x49 | ("\\x49",SUCCESS) + J | \x4a | ("\\x4a",SUCCESS) + K | \x4b | ("\\x4b",SUCCESS) + L | \x4c | ("\\x4c",SUCCESS) + M | \x4d | ("\\x4d",SUCCESS) + N | \x4e | ("\\x4e",SUCCESS) + O | \x4f | ("\\x4f",SUCCESS) + P | \x50 | ("\\x50",SUCCESS) + Q | \x51 | ("\\x51",SUCCESS) + R | \x52 | ("\\x52",SUCCESS) + S | \x53 | ("\\x53",SUCCESS) + T | \x54 | ("\\x54",SUCCESS) + U | \x55 | ("\\x55",SUCCESS) + V | \x56 | ("\\x56",SUCCESS) + W | \x57 | ("\\x57",SUCCESS) + X | \x58 | ("\\x58",SUCCESS) + Y | \x59 | ("\\x59",SUCCESS) + Z | \x5a | ("\\x5a",SUCCESS) + [ | \x5b | ("\\x5b",SUCCESS) + \ | \x5c | ("\\x5c",SUCCESS) + ] | \x5d | ("\\x5d",SUCCESS) + ^ | \x5e | ("\\x5e",SUCCESS) + _ | \x5f | ("\\x5f",SUCCESS) + ` | \x60 | ("\\x60",SUCCESS) + a | \x61 | ("\\x61",SUCCESS) + b | \x62 | ("\\x62",SUCCESS) + c | \x63 | ("\\x63",SUCCESS) + d | \x64 | ("\\x64",SUCCESS) + e | \x65 | ("\\x65",SUCCESS) + f | \x66 | ("\\x66",SUCCESS) + g | \x67 | ("\\x67",SUCCESS) + h | \x68 | ("\\x68",SUCCESS) + i | \x69 | ("\\x69",SUCCESS) + j | \x6a | ("\\x6a",SUCCESS) + k | \x6b | ("\\x6b",SUCCESS) + l | \x6c | ("\\x6c",SUCCESS) + m | \x6d | ("\\x6d",SUCCESS) + n | \x6e | ("\\x6e",SUCCESS) + o | \x6f | ("\\x6f",SUCCESS) + p | \x70 | ("\\x70",SUCCESS) + q | \x71 | ("\\x71",SUCCESS) + r | \x72 | ("\\x72",SUCCESS) + s | \x73 | ("\\x73",SUCCESS) + t | \x74 | ("\\x74",SUCCESS) + u | \x75 | ("\\x75",SUCCESS) + v | \x76 | ("\\x76",SUCCESS) + w | \x77 | ("\\x77",SUCCESS) + x | \x78 | ("\\x78",SUCCESS) + y | \x79 | ("\\x79",SUCCESS) + z | \x7a | ("\\x7a",SUCCESS) + { | \x7b | ("\\x7b",SUCCESS) + | | \x7c | ("\\x7c",SUCCESS) + } | \x7d | ("\\x7d",SUCCESS) + ~ | \x7e | ("\\x7e",SUCCESS) + | \x7f | (,PROHIBITED) +(128 rows) + +DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/meson.build b/src/test/modules/test_saslprep/meson.build new file mode 100644 index 0000000000000..2fcc403ca0728 --- /dev/null +++ b/src/test/modules/test_saslprep/meson.build @@ -0,0 +1,38 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +test_saslprep_sources = files( + 'test_saslprep.c', +) + +if host_system == 'windows' + test_saslprep_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_saslprep', + '--FILEDESC', 'test_saslprep - test SASLprep implementation',]) +endif + +test_saslprep = shared_module('test_saslprep', + test_saslprep_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_saslprep + +test_install_data += files( + 'test_saslprep.control', + 'test_saslprep--1.0.sql', +) + +tests += { + 'name': 'test_saslprep', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'regress': { + 'sql': [ + 'test_saslprep', + ], + }, + 'tap': { + 'tests': [ + 't/001_saslprep_ranges.pl', + ], + }, +} diff --git a/src/test/modules/test_saslprep/sql/test_saslprep.sql b/src/test/modules/test_saslprep/sql/test_saslprep.sql new file mode 100644 index 0000000000000..00bad48eca70b --- /dev/null +++ b/src/test/modules/test_saslprep/sql/test_saslprep.sql @@ -0,0 +1,19 @@ +-- Tests for SASLprep + +CREATE EXTENSION test_saslprep; + +-- Incomplete UTF-8 sequence. +SELECT test_saslprep('\xef'); + +-- Range of ASCII characters. +SELECT + CASE + WHEN a = 0 THEN '' + WHEN a < 32 THEN '' + WHEN a = 127 THEN '' + ELSE chr(a) END AS dat, + set_byte('\x00'::bytea, 0, a) AS byt, + test_saslprep(set_byte('\x00'::bytea, 0, a)) AS saslprep + FROM generate_series(0,127) AS a; + +DROP EXTENSION test_saslprep; diff --git a/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl b/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl new file mode 100644 index 0000000000000..4a7cb5aaa5886 --- /dev/null +++ b/src/test/modules/test_saslprep/t/001_saslprep_ranges.pl @@ -0,0 +1,36 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test all ranges of valid UTF-8 codepoints under SASLprep. +# +# This test is expensive and enabled with PG_TEST_EXTRA, which is +# why it exists as a TAP test. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use Test::More; + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bsaslprep\b/) +{ + plan skip_all => "test saslprep not enabled in PG_TEST_EXTRA"; +} + +# Initialize node +my $node = PostgreSQL::Test::Cluster->new('main'); + +$node->init; +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_saslprep;'); + +# Among all the valid UTF-8 codepoint ranges, our implementation of +# SASLprep should never return an empty password if the operation is +# considered a success. +my $result = $node->safe_psql( + 'postgres', qq[SELECT * FROM test_saslprep_ranges() + WHERE status = 'SUCCESS' AND res IN (NULL, '') +]); + +is($result, '', "valid codepoints returning an empty password"); + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_saslprep/test_saslprep--1.0.sql b/src/test/modules/test_saslprep/test_saslprep--1.0.sql new file mode 100644 index 0000000000000..01e5244809e74 --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep--1.0.sql @@ -0,0 +1,30 @@ +/* src/test/modules/test_saslprep/test_saslprep--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_saslprep" to load this file. \quit + +-- +-- test_saslprep(bytea) +-- +-- Tests single byte sequence in SASLprep. +-- +CREATE FUNCTION test_saslprep(IN src bytea, + OUT res bytea, + OUT status text) +RETURNS record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; + +-- +-- test_saslprep_ranges +-- +-- Tests all possible ranges of byte sequences in SASLprep. +-- +CREATE FUNCTION test_saslprep_ranges( + OUT codepoint text, + OUT status text, + OUT src bytea, + OUT res bytea) +RETURNS SETOF record +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT; diff --git a/src/test/modules/test_saslprep/test_saslprep.c b/src/test/modules/test_saslprep/test_saslprep.c new file mode 100644 index 0000000000000..121212d4fa21e --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep.c @@ -0,0 +1,278 @@ +/*-------------------------------------------------------------------------- + * + * test_saslprep.c + * Test harness for the SASLprep implementation. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_saslprep/test_saslprep.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "common/saslprep.h" +#include "fmgr.h" +#include "funcapi.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "utils/builtins.h" + +PG_MODULE_MAGIC; + +static const char * +saslprep_status_to_text(pg_saslprep_rc rc) +{ + const char *status = "???"; + + switch (rc) + { + case SASLPREP_OOM: + status = "OOM"; + break; + case SASLPREP_SUCCESS: + status = "SUCCESS"; + break; + case SASLPREP_INVALID_UTF8: + status = "INVALID_UTF8"; + break; + case SASLPREP_PROHIBITED: + status = "PROHIBITED"; + break; + } + + return status; +} + +/* + * Simple function to test SASLprep with arbitrary bytes as input. + * + * This takes a bytea in input, returning in output the generating data as + * bytea with the status returned by pg_saslprep(). + */ +PG_FUNCTION_INFO_V1(test_saslprep); +Datum +test_saslprep(PG_FUNCTION_ARGS) +{ + bytea *string = PG_GETARG_BYTEA_PP(0); + char *src; + Size src_len; + char *input_data; + char *result; + Size result_len; + bytea *result_bytea = NULL; + const char *status = NULL; + Datum *values; + bool *nulls; + TupleDesc tupdesc; + pg_saslprep_rc rc; + + /* determine result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + values = palloc0_array(Datum, tupdesc->natts); + nulls = palloc0_array(bool, tupdesc->natts); + + src_len = VARSIZE_ANY_EXHDR(string); + src = VARDATA_ANY(string); + + /* + * Copy the input given, to make SASLprep() act on a sanitized string. + */ + input_data = palloc0(src_len + 1); + memcpy(input_data, src, src_len); + input_data[src_len] = '\0'; + + rc = pg_saslprep(input_data, &result); + status = saslprep_status_to_text(rc); + + if (result) + { + result_len = strlen(result); + result_bytea = palloc(result_len + VARHDRSZ); + SET_VARSIZE(result_bytea, result_len + VARHDRSZ); + memcpy(VARDATA(result_bytea), result, result_len); + values[0] = PointerGetDatum(result_bytea); + } + else + nulls[0] = true; + + values[1] = CStringGetTextDatum(status); + + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + +/* Context structure for set-returning function with ranges */ +typedef struct +{ + int current_range; + char32_t current_codepoint; +} pg_saslprep_test_context; + +/* + * UTF-8 code point ranges. + */ +typedef struct +{ + char32_t start_codepoint; + char32_t end_codepoint; +} pg_utf8_codepoint_range; + +static const pg_utf8_codepoint_range pg_utf8_test_ranges[] = { + /* 1, 2, 3 bytes */ + {0x0000, 0xD7FF}, /* Basic Multilingual Plane, before surrogates */ + {0xE000, 0xFFFF}, /* Basic Multilingual Plane, after surrogates */ + /* 4 bytes */ + {0x10000, 0x1FFFF}, /* Supplementary Multilingual Plane */ + {0x20000, 0x2FFFF}, /* Supplementary Ideographic Plane */ + {0x30000, 0x3FFFF}, /* Tertiary Ideographic Plane */ + {0x40000, 0xDFFFF}, /* Unassigned planes */ + {0xE0000, 0xEFFFF}, /* Supplementary Special-purpose Plane */ + {0xF0000, 0xFFFFF}, /* Private Use Area A */ + {0x100000, 0x10FFFF}, /* Private Use Area B */ +}; + +#define PG_UTF8_TEST_RANGES_LEN \ + (sizeof(pg_utf8_test_ranges) / sizeof(pg_utf8_test_ranges[0])) + + +/* + * test_saslprep_ranges + * + * Test SASLprep across various UTF-8 ranges. + */ +PG_FUNCTION_INFO_V1(test_saslprep_ranges); +Datum +test_saslprep_ranges(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + pg_saslprep_test_context *ctx; + HeapTuple tuple; + Datum result; + + /* First call setup */ + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + TupleDesc tupdesc; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + funcctx->tuple_desc = tupdesc; + + /* Allocate context with range setup */ + ctx = (pg_saslprep_test_context *) palloc(sizeof(pg_saslprep_test_context)); + ctx->current_range = 0; + ctx->current_codepoint = pg_utf8_test_ranges[0].start_codepoint; + funcctx->user_fctx = ctx; + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + ctx = (pg_saslprep_test_context *) funcctx->user_fctx; + + while (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + { + char32_t codepoint = ctx->current_codepoint; + unsigned char utf8_buf[5]; + char input_str[6]; + char *output = NULL; + pg_saslprep_rc rc; + int utf8_len; + const char *status; + bytea *input_bytea; + bytea *output_bytea; + char codepoint_str[16]; + Datum values[4] = {0}; + bool nulls[4] = {0}; + const pg_utf8_codepoint_range *range = + &pg_utf8_test_ranges[ctx->current_range]; + + CHECK_FOR_INTERRUPTS(); + + /* Switch to next range if finished with the previous one */ + if (ctx->current_codepoint > range->end_codepoint) + { + ctx->current_range++; + if (ctx->current_range < PG_UTF8_TEST_RANGES_LEN) + ctx->current_codepoint = + pg_utf8_test_ranges[ctx->current_range].start_codepoint; + continue; + } + + codepoint = ctx->current_codepoint; + + /* Convert code point to UTF-8 */ + utf8_len = unicode_utf8len(codepoint); + if (utf8_len == 0) + { + ctx->current_codepoint++; + continue; + } + unicode_to_utf8(codepoint, utf8_buf); + + /* Create null-terminated string */ + memcpy(input_str, utf8_buf, utf8_len); + input_str[utf8_len] = '\0'; + + /* Test with pg_saslprep */ + rc = pg_saslprep(input_str, &output); + + /* Prepare output values */ + memset(nulls, false, sizeof(nulls)); + + /* codepoint as text U+XXXX format */ + if (codepoint <= 0xFFFF) + snprintf(codepoint_str, sizeof(codepoint_str), "U+%04X", codepoint); + else + snprintf(codepoint_str, sizeof(codepoint_str), "U+%06X", codepoint); + values[0] = CStringGetTextDatum(codepoint_str); + + /* status */ + status = saslprep_status_to_text(rc); + values[1] = CStringGetTextDatum(status); + + /* input_bytes */ + input_bytea = (bytea *) palloc(VARHDRSZ + utf8_len); + SET_VARSIZE(input_bytea, VARHDRSZ + utf8_len); + memcpy(VARDATA(input_bytea), utf8_buf, utf8_len); + values[2] = PointerGetDatum(input_bytea); + + /* output_bytes */ + if (output != NULL) + { + int output_len = strlen(output); + + output_bytea = (bytea *) palloc(VARHDRSZ + output_len); + SET_VARSIZE(output_bytea, VARHDRSZ + output_len); + memcpy(VARDATA(output_bytea), output, output_len); + values[3] = PointerGetDatum(output_bytea); + pfree(output); + } + else + { + nulls[3] = true; + values[3] = (Datum) 0; + } + + /* Build and return tuple */ + tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); + result = HeapTupleGetDatum(tuple); + + /* Move to next code point */ + ctx->current_codepoint++; + + SRF_RETURN_NEXT(funcctx, result); + } + + /* All done */ + SRF_RETURN_DONE(funcctx); +} diff --git a/src/test/modules/test_saslprep/test_saslprep.control b/src/test/modules/test_saslprep/test_saslprep.control new file mode 100644 index 0000000000000..13015c43880fb --- /dev/null +++ b/src/test/modules/test_saslprep/test_saslprep.control @@ -0,0 +1,5 @@ +# test_saslprep extension +comment = 'Test SASLprep implementation' +default_version = '1.0' +module_pathname = '$libdir/test_saslprep' +relocatable = true diff --git a/src/test/modules/test_shm_mq/meson.build b/src/test/modules/test_shm_mq/meson.build index eb2406897262f..80893b8d0b670 100644 --- a/src/test/modules/test_shm_mq/meson.build +++ b/src/test/modules/test_shm_mq/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_shm_mq_sources = files( 'setup.c', diff --git a/src/test/modules/test_shm_mq/setup.c b/src/test/modules/test_shm_mq/setup.c index 2a20ffb12736c..4f40a61e3d928 100644 --- a/src/test/modules/test_shm_mq/setup.c +++ b/src/test/modules/test_shm_mq/setup.c @@ -5,7 +5,7 @@ * number of background workers for shared memory message queue * testing. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_shm_mq/setup.c @@ -18,9 +18,11 @@ #include "miscadmin.h" #include "pgstat.h" #include "postmaster/bgworker.h" +#include "storage/proc.h" #include "storage/shm_toc.h" #include "test_shm_mq.h" #include "utils/memutils.h" +#include "utils/wait_event.h" typedef struct { @@ -228,6 +230,7 @@ setup_background_workers(int nworkers, dsm_segment *seg) /* Register the workers. */ for (i = 0; i < nworkers; ++i) { + snprintf(worker.bgw_name, BGW_MAXLEN, "test_shm_mq worker %d", i + 1); if (!RegisterDynamicBackgroundWorker(&worker, &wstate->handle[i])) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_RESOURCES), diff --git a/src/test/modules/test_shm_mq/test.c b/src/test/modules/test_shm_mq/test.c index 443281addd06b..0e55287e51015 100644 --- a/src/test/modules/test_shm_mq/test.c +++ b/src/test/modules/test_shm_mq/test.c @@ -3,7 +3,7 @@ * test.c * Test harness code for shared memory message queues. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_shm_mq/test.c @@ -16,6 +16,8 @@ #include "fmgr.h" #include "miscadmin.h" #include "pgstat.h" +#include "storage/proc.h" +#include "utils/wait_event.h" #include "varatt.h" #include "test_shm_mq.h" diff --git a/src/test/modules/test_shm_mq/test_shm_mq.h b/src/test/modules/test_shm_mq/test_shm_mq.h index 5346557d473c4..b6a0290289cef 100644 --- a/src/test/modules/test_shm_mq/test_shm_mq.h +++ b/src/test/modules/test_shm_mq/test_shm_mq.h @@ -3,7 +3,7 @@ * test_shm_mq.h * Definitions for shared memory message queues * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_shm_mq/test_shm_mq.h diff --git a/src/test/modules/test_shm_mq/worker.c b/src/test/modules/test_shm_mq/worker.c index 96cd304dbbc83..e13c05ae5c7cf 100644 --- a/src/test/modules/test_shm_mq/worker.c +++ b/src/test/modules/test_shm_mq/worker.c @@ -9,7 +9,7 @@ * but it should be possible to use much of the control logic just * as presented here. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_shm_mq/worker.c @@ -21,6 +21,8 @@ #include "miscadmin.h" #include "storage/ipc.h" +#include "storage/latch.h" +#include "storage/proc.h" #include "storage/procarray.h" #include "storage/shm_mq.h" #include "storage/shm_toc.h" @@ -54,13 +56,7 @@ test_shm_mq_main(Datum main_arg) int myworkernumber; PGPROC *registrant; - /* - * Establish signal handlers. - * - * We want CHECK_FOR_INTERRUPTS() to kill off this worker process just as - * it would a normal user backend. To make that happen, we use die(). - */ - pqsignal(SIGTERM, die); + /* Unblock signals. The standard signal handlers are OK for us. */ BackgroundWorkerUnblockSignals(); /* @@ -77,7 +73,7 @@ test_shm_mq_main(Datum main_arg) * exit, which is fine. If there were a ResourceOwner, it would acquire * ownership of the mapping, but we have no need for that. */ - seg = dsm_attach(DatumGetInt32(main_arg)); + seg = dsm_attach(DatumGetUInt32(main_arg)); if (seg == NULL) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), diff --git a/src/test/modules/test_shmem/.gitignore b/src/test/modules/test_shmem/.gitignore new file mode 100644 index 0000000000000..5dcb3ff972350 --- /dev/null +++ b/src/test/modules/test_shmem/.gitignore @@ -0,0 +1,4 @@ +# Generated subdirectories +/log/ +/results/ +/tmp_check/ diff --git a/src/test/modules/test_shmem/Makefile b/src/test/modules/test_shmem/Makefile new file mode 100644 index 0000000000000..2407f7462fee1 --- /dev/null +++ b/src/test/modules/test_shmem/Makefile @@ -0,0 +1,24 @@ +# src/test/modules/test_shmem/Makefile + +PGFILEDESC = "test_shmem - test code for shmem allocations" + +MODULE_big = test_shmem +OBJS = \ + $(WIN32RES) \ + test_shmem.o + +EXTENSION = test_shmem +DATA = test_shmem--1.0.sql + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_shmem +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_shmem/meson.build b/src/test/modules/test_shmem/meson.build new file mode 100644 index 0000000000000..fb4bf328b8f42 --- /dev/null +++ b/src/test/modules/test_shmem/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +test_shmem_sources = files( + 'test_shmem.c', +) + +if host_system == 'windows' + test_shmem_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'test_shmem', + '--FILEDESC', 'test_shmem - test code for shmem allocations',]) +endif + +test_shmem = shared_module('test_shmem', + test_shmem_sources, + kwargs: pg_test_mod_args, +) +test_install_libs += test_shmem + +test_install_data += files( + 'test_shmem.control', + 'test_shmem--1.0.sql', +) + +tests += { + 'name': 'test_shmem', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_late_shmem_alloc.pl', + ], + }, +} diff --git a/src/test/modules/test_shmem/t/001_late_shmem_alloc.pl b/src/test/modules/test_shmem/t/001_late_shmem_alloc.pl new file mode 100644 index 0000000000000..c154f57682ac4 --- /dev/null +++ b/src/test/modules/test_shmem/t/001_late_shmem_alloc.pl @@ -0,0 +1,49 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +### +# Test allocating memory after startup, i.e. when the library is not +# in shared_preload_libraries +### +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->start; + + +$node->safe_psql("postgres", "CREATE EXTENSION test_shmem;"); + +# Check that the attach counter is incremented on a new connection +my $attach_count1 = $node->safe_psql("postgres", "SELECT get_test_shmem_attach_count();"); +my $attach_count2 = $node->safe_psql("postgres", "SELECT get_test_shmem_attach_count();"); +cmp_ok($attach_count2, '>', $attach_count1, "attach callback is called in each backend"); +$node->stop; + +### +# Test that loading via shared_preload_libraries also works +### +$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_shmem'"); +$node->start; + +# When loaded via shared_preload_libraries, the attach callback is +# called or not, depending on whether this is an EXEC_BACKEND build. +my $exec_backend = $node->safe_psql("postgres", "SHOW debug_exec_backend;") eq 'on'; +$attach_count1 = $node->safe_psql("postgres", "SELECT get_test_shmem_attach_count();"); +$attach_count2 = $node->safe_psql("postgres", "SELECT get_test_shmem_attach_count();"); + +if ($exec_backend) +{ + cmp_ok($attach_count2, '>', $attach_count1, "attach callback is called in each backend when loaded via shared_preload_libraries"); +} +else +{ + ok($attach_count1 == 0 && $attach_count2 == 0, "attach callback is not called when loaded via shared_preload_libraries"); +} + +$node->stop; +done_testing(); diff --git a/src/test/modules/test_shmem/test_shmem--1.0.sql b/src/test/modules/test_shmem/test_shmem--1.0.sql new file mode 100644 index 0000000000000..2d01fd9256c1b --- /dev/null +++ b/src/test/modules/test_shmem/test_shmem--1.0.sql @@ -0,0 +1,9 @@ +/* src/test/modules/test_shmem/test_shmem--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION test_shmem" to load this file. \quit + + +CREATE FUNCTION get_test_shmem_attach_count() +RETURNS pg_catalog.int4 STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_shmem/test_shmem.c b/src/test/modules/test_shmem/test_shmem.c new file mode 100644 index 0000000000000..9bd4012b435d1 --- /dev/null +++ b/src/test/modules/test_shmem/test_shmem.c @@ -0,0 +1,101 @@ +/*------------------------------------------------------------------------- + * + * test_shmem.c + * Helpers to test shmem allocation routines + * + * Test basic memory allocation in an extension module. One notable feature + * that is not exercised by any other module in the repository is the + * allocating (non-DSM) shared memory after postmaster startup. + * + * Copyright (c) 2020-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_shmem/test_shmem.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "fmgr.h" +#include "miscadmin.h" +#include "storage/shmem.h" + + +PG_MODULE_MAGIC; + +typedef struct TestShmemData +{ + int value; + bool initialized; + int attach_count; +} TestShmemData; + +static TestShmemData *TestShmem; + +static bool attached_or_initialized = false; + +static void test_shmem_request(void *arg); +static void test_shmem_init(void *arg); +static void test_shmem_attach(void *arg); + +static const ShmemCallbacks TestShmemCallbacks = { + .flags = SHMEM_CALLBACKS_ALLOW_AFTER_STARTUP, + .request_fn = test_shmem_request, + .init_fn = test_shmem_init, + .attach_fn = test_shmem_attach, +}; + +static void +test_shmem_request(void *arg) +{ + elog(LOG, "test_shmem_request callback called"); + + ShmemRequestStruct(.name = "test_shmem area", + .size = sizeof(TestShmemData), + .ptr = (void **) &TestShmem); +} + +static void +test_shmem_init(void *arg) +{ + elog(LOG, "init callback called"); + if (TestShmem->initialized) + elog(ERROR, "shmem area already initialized"); + TestShmem->initialized = true; + + if (attached_or_initialized) + elog(ERROR, "attach or initialize already called in this process"); + attached_or_initialized = true; +} + +static void +test_shmem_attach(void *arg) +{ + elog(LOG, "test_shmem_attach callback called"); + if (!TestShmem->initialized) + elog(ERROR, "shmem area not yet initialized"); + TestShmem->attach_count++; + + if (attached_or_initialized) + elog(ERROR, "attach or initialize already called in this process"); + attached_or_initialized = true; +} + +void +_PG_init(void) +{ + elog(LOG, "test_shmem module's _PG_init called"); + RegisterShmemCallbacks(&TestShmemCallbacks); +} + +PG_FUNCTION_INFO_V1(get_test_shmem_attach_count); +Datum +get_test_shmem_attach_count(PG_FUNCTION_ARGS) +{ + if (!attached_or_initialized) + elog(ERROR, "shmem area not attached or initialized in this process"); + if (!TestShmem->initialized) + elog(ERROR, "shmem area not yet initialized"); + PG_RETURN_INT32(TestShmem->attach_count); +} diff --git a/src/test/modules/test_shmem/test_shmem.control b/src/test/modules/test_shmem/test_shmem.control new file mode 100644 index 0000000000000..f2f26f4537a4e --- /dev/null +++ b/src/test/modules/test_shmem/test_shmem.control @@ -0,0 +1,3 @@ +comment = 'Test code for shmem allocations' +default_version = '1.0' +module_pathname = '$libdir/test_shmem' diff --git a/src/test/modules/test_slru/expected/test_slru.out b/src/test/modules/test_slru/expected/test_slru.out index 185c56e5d628b..7ae1b0d4a9806 100644 --- a/src/test/modules/test_slru/expected/test_slru.out +++ b/src/test/modules/test_slru/expected/test_slru.out @@ -23,6 +23,10 @@ SELECT test_slru_page_exists(12345); t (1 row) +-- Test read failure +SELECT test_slru_page_read(54321, false, '123'::xid); +ERROR: could not open file "pg_test_slru/0000000000006A1": No such file or directory +DETAIL: Could not access test_slru entry 123. -- 48 extra pages SELECT count(test_slru_page_write(a, 'Test SLRU')) FROM generate_series(12346, 12393, 1) as a; diff --git a/src/test/modules/test_slru/meson.build b/src/test/modules/test_slru/meson.build index e58bbdf75ac7b..00f3ee3054d87 100644 --- a/src/test/modules/test_slru/meson.build +++ b/src/test/modules/test_slru/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_slru_sources = files( 'test_slru.c', @@ -38,7 +38,8 @@ tests += { 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', }, 'tests': [ - 't/001_multixact.pl' + 't/001_multixact.pl', + 't/002_multixact_wraparound.pl' ], }, } diff --git a/src/test/modules/test_slru/sql/test_slru.sql b/src/test/modules/test_slru/sql/test_slru.sql index b1b376581ab74..c6454c5bf82bf 100644 --- a/src/test/modules/test_slru/sql/test_slru.sql +++ b/src/test/modules/test_slru/sql/test_slru.sql @@ -5,6 +5,9 @@ SELECT test_slru_page_write(12345, 'Test SLRU'); SELECT test_slru_page_read(12345); SELECT test_slru_page_exists(12345); +-- Test read failure +SELECT test_slru_page_read(54321, false, '123'::xid); + -- 48 extra pages SELECT count(test_slru_page_write(a, 'Test SLRU')) FROM generate_series(12346, 12393, 1) as a; diff --git a/src/test/modules/test_slru/t/001_multixact.pl b/src/test/modules/test_slru/t/001_multixact.pl index e2b567a603d31..f6f45895ebda0 100644 --- a/src/test/modules/test_slru/t/001_multixact.pl +++ b/src/test/modules/test_slru/t/001_multixact.pl @@ -1,10 +1,6 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group -# This test verifies edge case of reading a multixact: -# when we have multixact that is followed by exactly one another multixact, -# and another multixact have no offset yet, we must wait until this offset -# becomes observable. Previously we used to wait for 1ms in a loop in this -# case, but now we use CV for this. This test is exercising such a sleep. +# Test multixid corner cases. use strict; use warnings FATAL => 'all'; @@ -19,9 +15,7 @@ plan skip_all => 'Injection points not supported by this build'; } -my ($node, $result); - -$node = PostgreSQL::Test::Cluster->new('mike'); +my $node = PostgreSQL::Test::Cluster->new('main'); $node->init; $node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_slru,injection_points'"); @@ -29,95 +23,47 @@ $node->safe_psql('postgres', q(CREATE EXTENSION injection_points)); $node->safe_psql('postgres', q(CREATE EXTENSION test_slru)); -# Test for Multixact generation edge case -$node->safe_psql('postgres', - q{select injection_points_attach('test-multixact-read','wait')}); -$node->safe_psql('postgres', - q{select injection_points_attach('multixact-get-members-cv-sleep','wait')} -); +# This test creates three multixacts. The middle one is never +# WAL-logged or recorded on the offsets page, because we pause the +# backend and crash the server before that. After restart, verify that +# the other multixacts are readable, despite the middle one being +# lost. -# This session must observe sleep on the condition variable while generating a -# multixact. To achieve this it first will create a multixact, then pause -# before reading it. -my $observer = $node->background_psql('postgres'); - -# This query will create a multixact, and hang just before reading it. -$observer->query_until( - qr/start/, - q{ - \echo start - SELECT test_read_multixact(test_create_multixact()); -}); -$node->wait_for_event('client backend', 'test-multixact-read'); - -# This session will create the next Multixact. This is necessary to avoid -# multixact.c's non-sleeping edge case 1. -my $creator = $node->background_psql('postgres'); +# Create the first multixact +my $bg_psql = $node->background_psql('postgres'); +my $multi1 = $bg_psql->query_safe(q(SELECT test_create_multixact();)); + +# Assign the middle multixact. Use an injection point to prevent it +# from being fully recorded. $node->safe_psql('postgres', q{SELECT injection_points_attach('multixact-create-from-members','wait');} ); -# We expect this query to hang in the critical section after generating new -# multixact, but before filling its offset into SLRU. -# Running an injection point inside a critical section requires it to be -# loaded beforehand. -$creator->query_until( - qr/start/, q{ - \echo start +$bg_psql->query_until( + qr/assigning lost multi/, q( +\echo assigning lost multi SELECT test_create_multixact(); -}); +)); $node->wait_for_event('client backend', 'multixact-create-from-members'); - -# Ensure we have the backends waiting that we expect -is( $node->safe_psql( - 'postgres', - q{SELECT string_agg(wait_event, ', ' ORDER BY wait_event) - FROM pg_stat_activity WHERE wait_event_type = 'InjectionPoint'} - ), - 'multixact-create-from-members, test-multixact-read', - "matching injection point waits"); - -# Now wake observer to get it to read the initial multixact. A subsequent -# multixact already exists, but that one doesn't have an offset assigned, so -# this will hit multixact.c's edge case 2. -$node->safe_psql('postgres', - q{SELECT injection_points_wakeup('test-multixact-read')}); -$node->wait_for_event('client backend', 'multixact-get-members-cv-sleep'); - -# Ensure we have the backends waiting that we expect -is( $node->safe_psql( - 'postgres', - q{SELECT string_agg(wait_event, ', ' ORDER BY wait_event) - FROM pg_stat_activity WHERE wait_event_type = 'InjectionPoint'} - ), - 'multixact-create-from-members, multixact-get-members-cv-sleep', - "matching injection point waits"); - -# Now we have two backends waiting in multixact-create-from-members and -# multixact-get-members-cv-sleep. Also we have 3 injections points set to wait. -# If we wakeup multixact-get-members-cv-sleep it will happen again, so we must -# detach it first. So let's detach all injection points, then wake up all -# backends. - -$node->safe_psql('postgres', - q{SELECT injection_points_detach('test-multixact-read')}); $node->safe_psql('postgres', q{SELECT injection_points_detach('multixact-create-from-members')}); -$node->safe_psql('postgres', - q{SELECT injection_points_detach('multixact-get-members-cv-sleep')}); -$node->safe_psql('postgres', - q{SELECT injection_points_wakeup('multixact-create-from-members')}); -$node->safe_psql('postgres', - q{SELECT injection_points_wakeup('multixact-get-members-cv-sleep')}); +# Create the third multixid +my $multi2 = $node->safe_psql('postgres', q{SELECT test_create_multixact();}); + +# All set and done, it's time for hard restart +$node->stop('immediate'); +$node->start; +$bg_psql->{run}->finish; -# Background psql will now be able to read the result and disconnect. -$observer->quit; -$creator->quit; +# Verify that the recorded multixids are readable +is( $node->safe_psql('postgres', qq{SELECT test_read_multixact('$multi1');}), + '', + 'first recorded multi is readable'); -$node->stop; +is( $node->safe_psql('postgres', qq{SELECT test_read_multixact('$multi2');}), + '', + 'second recorded multi is readable'); -# If we reached this point - everything is OK. -ok(1); done_testing(); diff --git a/src/test/modules/test_slru/t/002_multixact_wraparound.pl b/src/test/modules/test_slru/t/002_multixact_wraparound.pl new file mode 100644 index 0000000000000..3793ac1c45047 --- /dev/null +++ b/src/test/modules/test_slru/t/002_multixact_wraparound.pl @@ -0,0 +1,86 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group + +# Test multixact wraparound + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; + +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf('postgresql.conf', + "shared_preload_libraries = 'test_slru'"); + +# Set the cluster's next multitransaction close to wraparound +my $node_pgdata = $node->data_dir; +command_ok( + [ + 'pg_resetwal', + '--multixact-ids' => '0xFFFFFFF8,0xFFFFFFF8', + $node_pgdata + ], + "set the cluster's next multitransaction to 0xFFFFFFF8"); + +# Extract a few values from pg_resetwal --dry-run output that we need for +# the calculations below +my $out = (run_command([ 'pg_resetwal', '--dry-run', $node->data_dir ]))[0]; +$out =~ /^Database block size: *(\d+)$/m or die; +my $blcksz = $1; +$out =~ /^Pages per SLRU segment: *(\d+)$/m or die; +my $slru_pages_per_segment = $1; + +# Fixup the SLRU files to match the state we reset to. + +# initialize the 'offsets' SLRU file containing the new next multixid +# with zeros +my $multixact_offsets_per_page = $blcksz / 8; # sizeof(MultiXactOffset) == 8 +my $segno = + int(0xFFFFFFF8 / $multixact_offsets_per_page / $slru_pages_per_segment); +my $slru_file = sprintf('%s/pg_multixact/offsets/%04X', $node_pgdata, $segno); +open my $fh, ">", $slru_file + or die "could not open \"$slru_file\": $!"; +binmode $fh; +my $bytes_per_seg = $slru_pages_per_segment * $blcksz; +syswrite($fh, "\0" x $bytes_per_seg) == $bytes_per_seg + or die "could not write to \"$slru_file\": $!"; +close $fh; + +# remove old file +unlink("$node_pgdata/pg_multixact/offsets/0000") + or die "could not unlink \"$node_pgdata/pg_multixact/offsets/0000\": $!"; + +# Consume multixids to wrap around. We start at 0xFFFFFFF8, so after +# creating 16 multixacts we should definitely have wrapped around. +$node->start; +$node->safe_psql('postgres', q(CREATE EXTENSION test_slru)); + +my @multixact_ids; +foreach my $i (1 .. 16) +{ + my $multi = + $node->safe_psql('postgres', q{SELECT test_create_multixact();}); + push @multixact_ids, $multi; +} + +# Verify that wraparound occurred (last_multi should be numerically +# smaller than first_multi) +my $first_multi = $multixact_ids[0]; +my $last_multi = $multixact_ids[-1]; +ok( $last_multi < $first_multi, + "multixact wraparound occurred (first: $first_multi, last: $last_multi)"); + +# Verify that all the multixacts we created are readable +foreach my $i (0 .. $#multixact_ids) +{ + my $multi = $multixact_ids[$i]; + is( $node->safe_psql( + 'postgres', qq{SELECT test_read_multixact('$multi');}), + '', + "multixact $i (ID: $multi) is readable after wraparound"); +} + +done_testing(); diff --git a/src/test/modules/test_slru/test_multixact.c b/src/test/modules/test_slru/test_multixact.c index 6c9b0420717cc..e6de33c42461d 100644 --- a/src/test/modules/test_slru/test_multixact.c +++ b/src/test/modules/test_slru/test_multixact.c @@ -3,7 +3,7 @@ * test_multixact.c * Support code for multixact testing * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -17,7 +17,6 @@ #include "access/multixact.h" #include "access/xact.h" #include "fmgr.h" -#include "utils/injection_point.h" PG_FUNCTION_INFO_V1(test_create_multixact); PG_FUNCTION_INFO_V1(test_read_multixact); @@ -37,8 +36,7 @@ test_create_multixact(PG_FUNCTION_ARGS) } /* - * Reads given multixact after running an injection point. Discards local cache - * to make a real read. Tailored for multixact testing. + * Reads given multixact. Discards local cache to make a real read. */ Datum test_read_multixact(PG_FUNCTION_ARGS) @@ -46,7 +44,6 @@ test_read_multixact(PG_FUNCTION_ARGS) MultiXactId id = PG_GETARG_TRANSACTIONID(0); MultiXactMember *members; - INJECTION_POINT("test-multixact-read", NULL); /* discard caches */ AtEOXact_MultiXact(); diff --git a/src/test/modules/test_slru/test_slru--1.0.sql b/src/test/modules/test_slru/test_slru--1.0.sql index abecb5e21838f..2148e7204ec53 100644 --- a/src/test/modules/test_slru/test_slru--1.0.sql +++ b/src/test/modules/test_slru/test_slru--1.0.sql @@ -7,7 +7,7 @@ CREATE OR REPLACE FUNCTION test_slru_page_writeall() RETURNS VOID AS 'MODULE_PATHNAME', 'test_slru_page_writeall' LANGUAGE C; CREATE OR REPLACE FUNCTION test_slru_page_sync(bigint) RETURNS VOID AS 'MODULE_PATHNAME', 'test_slru_page_sync' LANGUAGE C; -CREATE OR REPLACE FUNCTION test_slru_page_read(bigint, bool DEFAULT true) RETURNS text +CREATE OR REPLACE FUNCTION test_slru_page_read(bigint, bool DEFAULT true, xid DEFAULT '0') RETURNS text AS 'MODULE_PATHNAME', 'test_slru_page_read' LANGUAGE C; CREATE OR REPLACE FUNCTION test_slru_page_readonly(bigint) RETURNS text AS 'MODULE_PATHNAME', 'test_slru_page_readonly' LANGUAGE C; diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c index 32750930e433d..40efffdbf621c 100644 --- a/src/test/modules/test_slru/test_slru.c +++ b/src/test/modules/test_slru/test_slru.c @@ -3,7 +3,7 @@ * test_slru.c * Test correctness of SLRU functions. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION @@ -40,14 +40,22 @@ PG_FUNCTION_INFO_V1(test_slru_delete_all); /* Number of SLRU page slots */ #define NUM_TEST_BUFFERS 16 -static SlruCtlData TestSlruCtlData; -#define TestSlruCtl (&TestSlruCtlData) +static void test_slru_shmem_request(void *arg); +static bool test_slru_page_precedes_logically(int64 page1, int64 page2); +static int test_slru_errdetail_for_io_error(const void *opaque_data); -static shmem_request_hook_type prev_shmem_request_hook = NULL; -static shmem_startup_hook_type prev_shmem_startup_hook = NULL; +static const char *TestSlruDir = "pg_test_slru"; + +static SlruDesc TestSlruDesc; + +static const ShmemCallbacks test_slru_shmem_callbacks = { + .request_fn = test_slru_shmem_request +}; + +#define TestSlruCtl (&TestSlruDesc) static bool -test_slru_scan_cb(SlruCtl ctl, char *filename, int64 segpage, void *data) +test_slru_scan_cb(SlruDesc *ctl, char *filename, int64 segpage, void *data) { elog(NOTICE, "Calling test_slru_scan_cb()"); return SlruScanDirCbDeleteAll(ctl, filename, segpage, data); @@ -93,14 +101,14 @@ test_slru_page_read(PG_FUNCTION_ARGS) { int64 pageno = PG_GETARG_INT64(0); bool write_ok = PG_GETARG_BOOL(1); + TransactionId xid = PG_GETARG_TRANSACTIONID(2); char *data = NULL; int slotno; LWLock *lock = SimpleLruGetBankLock(TestSlruCtl, pageno); /* find page in buffers, reading it if necessary */ LWLockAcquire(lock, LW_EXCLUSIVE); - slotno = SimpleLruReadPage(TestSlruCtl, pageno, - write_ok, InvalidTransactionId); + slotno = SimpleLruReadPage(TestSlruCtl, pageno, write_ok, &xid); data = (char *) TestSlruCtl->shared->page_buffer[slotno]; LWLockRelease(lock); @@ -118,7 +126,7 @@ test_slru_page_readonly(PG_FUNCTION_ARGS) /* find page in buffers, reading it if necessary */ slotno = SimpleLruReadPage_ReadOnly(TestSlruCtl, pageno, - InvalidTransactionId); + NULL); Assert(LWLockHeldByMe(lock)); data = (char *) TestSlruCtl->shared->page_buffer[slotno]; LWLockRelease(lock); @@ -190,59 +198,18 @@ test_slru_delete_all(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } -/* - * Module load callbacks and initialization. - */ - -static void -test_slru_shmem_request(void) -{ - if (prev_shmem_request_hook) - prev_shmem_request_hook(); - - /* reserve shared memory for the test SLRU */ - RequestAddinShmemSpace(SimpleLruShmemSize(NUM_TEST_BUFFERS, 0)); -} - static bool test_slru_page_precedes_logically(int64 page1, int64 page2) { return page1 < page2; } -static void -test_slru_shmem_startup(void) +static int +test_slru_errdetail_for_io_error(const void *opaque_data) { - /* - * Short segments names are well tested elsewhere so in this test we are - * focusing on long names. - */ - const bool long_segment_names = true; - const char slru_dir_name[] = "pg_test_slru"; - int test_tranche_id; - int test_buffer_tranche_id; - - if (prev_shmem_startup_hook) - prev_shmem_startup_hook(); - - /* - * Create the SLRU directory if it does not exist yet, from the root of - * the data directory. - */ - (void) MakePGDirectory(slru_dir_name); - - /* initialize the SLRU facility */ - test_tranche_id = LWLockNewTrancheId(); - LWLockRegisterTranche(test_tranche_id, "test_slru_tranche"); + TransactionId xid = *(const TransactionId *) opaque_data; - test_buffer_tranche_id = LWLockNewTrancheId(); - LWLockRegisterTranche(test_tranche_id, "test_buffer_tranche"); - - TestSlruCtl->PagePrecedes = test_slru_page_precedes_logically; - SimpleLruInit(TestSlruCtl, "TestSLRU", - NUM_TEST_BUFFERS, 0, slru_dir_name, - test_buffer_tranche_id, test_tranche_id, SYNC_HANDLER_NONE, - long_segment_names); + return errdetail("Could not access test_slru entry %u.", xid); } void @@ -254,9 +221,37 @@ _PG_init(void) errdetail("\"%s\" must be loaded with \"shared_preload_libraries\".", "test_slru"))); - prev_shmem_request_hook = shmem_request_hook; - shmem_request_hook = test_slru_shmem_request; + /* + * Create the SLRU directory if it does not exist yet, from the root of + * the data directory. + */ + (void) MakePGDirectory(TestSlruDir); + + RegisterShmemCallbacks(&test_slru_shmem_callbacks); +} + +static void +test_slru_shmem_request(void *arg) +{ + SimpleLruRequest(.desc = &TestSlruDesc, + .name = "TestSLRU", + .Dir = TestSlruDir, + + /* + * Short segments names are well tested elsewhere so in this test we are + * focusing on long names. + */ + .long_segment_names = true, + + .nslots = NUM_TEST_BUFFERS, + .nlsns = 0, + + .sync_handler = SYNC_HANDLER_NONE, + .PagePrecedes = test_slru_page_precedes_logically, + .errdetail_for_io_error = test_slru_errdetail_for_io_error, - prev_shmem_startup_hook = shmem_startup_hook; - shmem_startup_hook = test_slru_shmem_startup; + /* let slru.c assign these */ + .buffer_tranche_id = 0, + .bank_tranche_id = 0, + ); } diff --git a/src/test/modules/test_tidstore/meson.build b/src/test/modules/test_tidstore/meson.build index 8f259078b87ea..b1ebb9ca8cb56 100644 --- a/src/test/modules/test_tidstore/meson.build +++ b/src/test/modules/test_tidstore/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group test_tidstore_sources = files( 'test_tidstore.c', diff --git a/src/test/modules/test_tidstore/test_tidstore.c b/src/test/modules/test_tidstore/test_tidstore.c index eb16e0fbfa647..c9a035fa49416 100644 --- a/src/test/modules/test_tidstore/test_tidstore.c +++ b/src/test/modules/test_tidstore/test_tidstore.c @@ -7,7 +7,7 @@ * a single process to use the TidStore. It is meant to be an example of * usage. * - * Copyright (c) 2024-2025, PostgreSQL Global Development Group + * Copyright (c) 2024-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/test_tidstore/test_tidstore.c @@ -56,16 +56,16 @@ itemptr_cmp(const void *left, const void *right) OffsetNumber loff, roff; - lblk = ItemPointerGetBlockNumber((ItemPointer) left); - rblk = ItemPointerGetBlockNumber((ItemPointer) right); + lblk = ItemPointerGetBlockNumber((const ItemPointerData *) left); + rblk = ItemPointerGetBlockNumber((const ItemPointerData *) right); if (lblk < rblk) return -1; if (lblk > rblk) return 1; - loff = ItemPointerGetOffsetNumber((ItemPointer) left); - roff = ItemPointerGetOffsetNumber((ItemPointer) right); + loff = ItemPointerGetOffsetNumber((const ItemPointerData *) left); + roff = ItemPointerGetOffsetNumber((const ItemPointerData *) right); if (loff < roff) return -1; @@ -103,8 +103,7 @@ test_create(PG_FUNCTION_ARGS) { int tranche_id; - tranche_id = LWLockNewTrancheId(); - LWLockRegisterTranche(tranche_id, "test_tidstore"); + tranche_id = LWLockNewTrancheId("test_tidstore"); tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id); diff --git a/src/test/modules/typcache/expected/typcache_rel_type_cache.out b/src/test/modules/typcache/expected/typcache_rel_type_cache.out index a91bd72b9bd4d..54c61602d082c 100644 --- a/src/test/modules/typcache/expected/typcache_rel_type_cache.out +++ b/src/test/modules/typcache/expected/typcache_rel_type_cache.out @@ -39,3 +39,4 @@ SELECT '(1,2)'::t; (1,2) (1 row) +DROP EXTENSION injection_points; diff --git a/src/test/modules/typcache/meson.build b/src/test/modules/typcache/meson.build index 6f30cda11ea83..0dafb230dca15 100644 --- a/src/test/modules/typcache/meson.build +++ b/src/test/modules/typcache/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group if not get_option('injection_points') subdir_done() diff --git a/src/test/modules/typcache/sql/typcache_rel_type_cache.sql b/src/test/modules/typcache/sql/typcache_rel_type_cache.sql index c1a3af509c018..f004a31c32a85 100644 --- a/src/test/modules/typcache/sql/typcache_rel_type_cache.sql +++ b/src/test/modules/typcache/sql/typcache_rel_type_cache.sql @@ -19,3 +19,5 @@ SELECT '(1)'::t; SELECT injection_points_detach('typecache-before-rel-type-cache-insert'); ALTER TABLE t ADD COLUMN j int; SELECT '(1,2)'::t; + +DROP EXTENSION injection_points; diff --git a/src/test/modules/unsafe_tests/expected/alter_system_table.out b/src/test/modules/unsafe_tests/expected/alter_system_table.out index b73b9442b8d4d..9ea6061f4ae76 100644 --- a/src/test/modules/unsafe_tests/expected/alter_system_table.out +++ b/src/test/modules/unsafe_tests/expected/alter_system_table.out @@ -64,6 +64,11 @@ ERROR: permission denied: "pg_description" is a system catalog CREATE TABLESPACE pg_foo LOCATION '/no/such/location'; ERROR: unacceptable tablespace name "pg_foo" DETAIL: The prefix "pg_" is reserved for system tablespaces. +-- contains \n\r tablespace name +CREATE TABLESPACE "invalid +name" LOCATION '/no/such/location'; +ERROR: tablespace name "invalid +name" contains a newline or carriage return character -- triggers CREATE FUNCTION tf1() RETURNS trigger LANGUAGE plpgsql diff --git a/src/test/modules/unsafe_tests/expected/guc_privs.out b/src/test/modules/unsafe_tests/expected/guc_privs.out index 6c0ad898341ff..3cf2f2fdb8520 100644 --- a/src/test/modules/unsafe_tests/expected/guc_privs.out +++ b/src/test/modules/unsafe_tests/expected/guc_privs.out @@ -581,6 +581,21 @@ DROP ROLE regress_host_resource_newadmin; -- ok, nothing was transferred -- Use "drop owned by" so we can drop the role DROP OWNED BY regress_host_resource_admin; -- ok DROP ROLE regress_host_resource_admin; -- ok +-- Test for GUC synchronized standby slots +-- Cannot set synchronized_standby_slots to a reserved slot name +ALTER SYSTEM SET synchronized_standby_slots='pg_conflict_detection'; +ERROR: invalid value for parameter "synchronized_standby_slots": "pg_conflict_detection" +DETAIL: replication slot name "pg_conflict_detection" is reserved +HINT: The name "pg_conflict_detection" is reserved for the conflict detection slot. +-- Cannot set synchronized_standby_slots to an invalid slot name +ALTER SYSTEM SET synchronized_standby_slots='invalid*'; +ERROR: invalid value for parameter "synchronized_standby_slots": "invalid*" +DETAIL: replication slot name "invalid*" contains invalid character +HINT: Replication slot names may only contain lower case letters, numbers, and the underscore character. +-- Can set synchronized_standby_slots to a non-existent slot name +ALTER SYSTEM SET synchronized_standby_slots='missing'; +-- Reset the GUC +ALTER SYSTEM RESET synchronized_standby_slots; -- Clean up RESET SESSION AUTHORIZATION; DROP ROLE regress_admin; -- ok diff --git a/src/test/modules/unsafe_tests/expected/rolenames.out b/src/test/modules/unsafe_tests/expected/rolenames.out index 61396b2a8054a..9dfb8475e164b 100644 --- a/src/test/modules/unsafe_tests/expected/rolenames.out +++ b/src/test/modules/unsafe_tests/expected/rolenames.out @@ -102,6 +102,10 @@ DETAIL: Role names starting with "pg_" are reserved. CREATE ROLE "pg_abcdef"; -- error ERROR: role name "pg_abcdef" is reserved DETAIL: Role names starting with "pg_" are reserved. +CREATE ROLE "invalid +rolename"; -- error +ERROR: role name "invalid +rolename" contains a newline or carriage return character CREATE ROLE regress_testrol0 SUPERUSER LOGIN; CREATE ROLE regress_testrolx SUPERUSER LOGIN; CREATE ROLE regress_testrol2 SUPERUSER; diff --git a/src/test/modules/unsafe_tests/expected/setconfig.out b/src/test/modules/unsafe_tests/expected/setconfig.out index 5f42443e144b9..5318f075d1e8c 100644 --- a/src/test/modules/unsafe_tests/expected/setconfig.out +++ b/src/test/modules/unsafe_tests/expected/setconfig.out @@ -62,6 +62,19 @@ SELECT current_user, session_user; SET ROLE NONE; DO $$BEGIN EXECUTE format( 'ALTER DATABASE %I RESET role', current_catalog); END$$; +-- Test some error cases +-- We have to use terse mode so that the database name doesn't +-- appear in the error output. +\set VERBOSITY terse +DO $$BEGIN EXECUTE format( + 'ALTER DATABASE %I SET bogus = 0', current_catalog); END$$; +ERROR: unrecognized configuration parameter "bogus" +DO $$BEGIN EXECUTE format( + 'ALTER DATABASE %I RESET bogus', current_catalog); END$$; +ERROR: unrecognized configuration parameter "bogus" +ALTER USER regress_authenticated_user_db_ssa RESET bogus; +ERROR: unrecognized configuration parameter "bogus" +\set VERBOSITY default -- Test connection string options \c -reuse-previous=on "user=regress_authenticated_user_db_sr options=-crole=regress_current_user" SELECT current_user, session_user; diff --git a/src/test/modules/unsafe_tests/meson.build b/src/test/modules/unsafe_tests/meson.build index af9bbf5b9fdc0..8217bbeb1fd9d 100644 --- a/src/test/modules/unsafe_tests/meson.build +++ b/src/test/modules/unsafe_tests/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'unsafe_tests', diff --git a/src/test/modules/unsafe_tests/sql/alter_system_table.sql b/src/test/modules/unsafe_tests/sql/alter_system_table.sql index c1515100845cd..1b0eb727eaaab 100644 --- a/src/test/modules/unsafe_tests/sql/alter_system_table.sql +++ b/src/test/modules/unsafe_tests/sql/alter_system_table.sql @@ -64,6 +64,10 @@ ALTER TABLE pg_description SET SCHEMA public; -- reserved tablespace name CREATE TABLESPACE pg_foo LOCATION '/no/such/location'; +-- contains \n\r tablespace name +CREATE TABLESPACE "invalid +name" LOCATION '/no/such/location'; + -- triggers CREATE FUNCTION tf1() RETURNS trigger LANGUAGE plpgsql diff --git a/src/test/modules/unsafe_tests/sql/guc_privs.sql b/src/test/modules/unsafe_tests/sql/guc_privs.sql index 9bcbbfa9040cf..d0d16f3c29f4f 100644 --- a/src/test/modules/unsafe_tests/sql/guc_privs.sql +++ b/src/test/modules/unsafe_tests/sql/guc_privs.sql @@ -262,6 +262,16 @@ DROP ROLE regress_host_resource_newadmin; -- ok, nothing was transferred DROP OWNED BY regress_host_resource_admin; -- ok DROP ROLE regress_host_resource_admin; -- ok +-- Test for GUC synchronized standby slots +-- Cannot set synchronized_standby_slots to a reserved slot name +ALTER SYSTEM SET synchronized_standby_slots='pg_conflict_detection'; +-- Cannot set synchronized_standby_slots to an invalid slot name +ALTER SYSTEM SET synchronized_standby_slots='invalid*'; +-- Can set synchronized_standby_slots to a non-existent slot name +ALTER SYSTEM SET synchronized_standby_slots='missing'; +-- Reset the GUC +ALTER SYSTEM RESET synchronized_standby_slots; + -- Clean up RESET SESSION AUTHORIZATION; DROP ROLE regress_admin; -- ok diff --git a/src/test/modules/unsafe_tests/sql/rolenames.sql b/src/test/modules/unsafe_tests/sql/rolenames.sql index adac36536db4d..70b342abeb665 100644 --- a/src/test/modules/unsafe_tests/sql/rolenames.sql +++ b/src/test/modules/unsafe_tests/sql/rolenames.sql @@ -75,6 +75,8 @@ CREATE ROLE pg_abc; -- error CREATE ROLE "pg_abc"; -- error CREATE ROLE pg_abcdef; -- error CREATE ROLE "pg_abcdef"; -- error +CREATE ROLE "invalid +rolename"; -- error CREATE ROLE regress_testrol0 SUPERUSER LOGIN; CREATE ROLE regress_testrolx SUPERUSER LOGIN; diff --git a/src/test/modules/unsafe_tests/sql/setconfig.sql b/src/test/modules/unsafe_tests/sql/setconfig.sql index 81296d1091b47..4349490f94117 100644 --- a/src/test/modules/unsafe_tests/sql/setconfig.sql +++ b/src/test/modules/unsafe_tests/sql/setconfig.sql @@ -50,6 +50,19 @@ DO $$BEGIN EXECUTE format( 'ALTER DATABASE %I RESET role', current_catalog); END$$; +-- Test some error cases +-- We have to use terse mode so that the database name doesn't +-- appear in the error output. + +\set VERBOSITY terse +DO $$BEGIN EXECUTE format( + 'ALTER DATABASE %I SET bogus = 0', current_catalog); END$$; +DO $$BEGIN EXECUTE format( + 'ALTER DATABASE %I RESET bogus', current_catalog); END$$; +ALTER USER regress_authenticated_user_db_ssa RESET bogus; +\set VERBOSITY default + + -- Test connection string options \c -reuse-previous=on "user=regress_authenticated_user_db_sr options=-crole=regress_current_user" diff --git a/src/test/modules/worker_spi/Makefile b/src/test/modules/worker_spi/Makefile index 024b34cdbb356..e7c5c059e3219 100644 --- a/src/test/modules/worker_spi/Makefile +++ b/src/test/modules/worker_spi/Makefile @@ -6,6 +6,10 @@ EXTENSION = worker_spi DATA = worker_spi--1.0.sql PGFILEDESC = "worker_spi - background worker example" +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points + TAP_TESTS = 1 ifdef USE_PGXS diff --git a/src/test/modules/worker_spi/meson.build b/src/test/modules/worker_spi/meson.build index d673ece48a052..6475e23f60173 100644 --- a/src/test/modules/worker_spi/meson.build +++ b/src/test/modules/worker_spi/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group test_worker_spi_sources = files( 'worker_spi.c', @@ -26,8 +26,12 @@ tests += { 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { + 'env': { + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, 'tests': [ 't/001_worker_spi.pl', + 't/002_worker_terminate.pl' ], }, } diff --git a/src/test/modules/worker_spi/t/001_worker_spi.pl b/src/test/modules/worker_spi/t/001_worker_spi.pl index 76fe7e63ac38f..65a2971c61341 100644 --- a/src/test/modules/worker_spi/t/001_worker_spi.pl +++ b/src/test/modules/worker_spi/t/001_worker_spi.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Test worker_spi module. diff --git a/src/test/modules/worker_spi/t/002_worker_terminate.pl b/src/test/modules/worker_spi/t/002_worker_terminate.pl new file mode 100644 index 0000000000000..69972a838dc4b --- /dev/null +++ b/src/test/modules/worker_spi/t/002_worker_terminate.pl @@ -0,0 +1,164 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test background workers can be terminated by db commands + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# This test depends on injection points to detect whether background workers +# remain. +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# Ensure the worker_spi dynamic worker is launched on the specified database. +# Returns the PID of the worker launched. +sub launch_bgworker +{ + my ($node, $database, $testcase, $interruptible) = @_; + + # Launch a background worker on the given database. + my $pid = $node->safe_psql( + 'postgres', qq( + SELECT worker_spi_launch($testcase, '$database'::regdatabase, 0, '{}', $interruptible); + )); + + # Check that the bgworker is initialized and napping. + my $result = + $node->poll_query_until('postgres', + qq[SELECT wait_event FROM pg_stat_activity WHERE pid = $pid;], + qq[WorkerSpiMain]); + is($result, 1, "dynamic bgworker $testcase launched"); + + return $pid; +} + +# Run query and verify that the bgworker with the specified PID has been +# terminated. +sub run_bgworker_interruptible_test +{ + my ($node, $command, $testname, $pid) = @_; + my $offset = -s $node->logfile; + + $node->safe_psql('postgres', $command); + + $node->wait_for_log( + qr/terminating background worker \"worker_spi dynamic\" due to administrator command/, + $offset); + + # Postmaster entry reporting the worker as exiting. + $node->wait_for_log( + qr/LOG: .*background worker \"worker_spi dynamic\" \(PID $pid\) exited with exit code/, + $offset); + + my $result = $node->safe_psql('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE pid = $pid;"); + is($result, 't', "dynamic bgworker stopped for $testname"); +} + +my $node = PostgreSQL::Test::Cluster->new('mynode'); +$node->init; +# The naptime is large enough to give some room on slow machines, so as +# the spawned workers have the time to process the interrupt requests sent +# by the database commands. +$node->append_conf( + "postgresql.conf", qq( +autovacuum = off +debug_parallel_query = off +log_min_messages = debug1 +worker_spi.naptime = 600 +)); +$node->start; + +# Check if the extension injection_points is available, as it may be +# possible that this script is run with installcheck, where the module +# would not be installed by default. +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', 'CREATE EXTENSION worker_spi;'); + +# Launch a background worker without BGWORKER_INTERRUPTIBLE. +my $pid = launch_bgworker($node, 'postgres', 0, 'false'); + +# Ensure CREATE DATABASE WITH TEMPLATE fails because a non-interruptible +# bgworker exists. + +# The injection point 'procarray-reduce-count' reduces the number of backend +# retries, allowing for shorter test runs. See CountOtherDBBackends(). +$node->safe_psql('postgres', "CREATE EXTENSION injection_points;"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('procarray-reduce-count', 'error');"); + +my $stderr; + +$node->psql( + 'postgres', + "CREATE DATABASE testdb WITH TEMPLATE postgres", + stderr => \$stderr); +ok( $stderr =~ + "source database \"postgres\" is being accessed by other users", + "background worker blocked the database creation"); + +# Confirm that the non-interruptible bgworker is still running. +my $result = $node->safe_psql( + "postgres", qq( + SELECT count(1) FROM pg_stat_activity + WHERE backend_type = 'worker_spi dynamic';)); + +is($result, '1', + "background worker is still running after CREATE DATABASE WITH TEMPLATE"); + +# Terminate the non-interruptible worker for the next tests. +$node->safe_psql( + "postgres", qq( + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity WHERE backend_type = 'worker_spi dynamic';)); + +# The injection point is not used anymore, release it. +$node->safe_psql('postgres', + "SELECT injection_points_detach('procarray-reduce-count');"); + +# Check that BGWORKER_INTERRUPTIBLE allows background workers to be +# terminated with database-related commands. + +# Test case 1: CREATE DATABASE WITH TEMPLATE +$pid = launch_bgworker($node, 'postgres', 1, 'true'); +run_bgworker_interruptible_test( + $node, + "CREATE DATABASE testdb WITH TEMPLATE postgres", + "CREATE DATABASE WITH TEMPLATE", $pid); + +# Test case 2: ALTER DATABASE RENAME +$pid = launch_bgworker($node, 'testdb', 2, 'true'); +run_bgworker_interruptible_test( + $node, + "ALTER DATABASE testdb RENAME TO renameddb", + "ALTER DATABASE RENAME", $pid); + +# Preparation for the next test, create a tablespace. +my $tablespace = PostgreSQL::Test::Utils::tempdir; +$node->safe_psql('postgres', + "CREATE TABLESPACE test_tablespace LOCATION '$tablespace'"); + +# Test case 3: ALTER DATABASE SET TABLESPACE +$pid = launch_bgworker($node, 'renameddb', 3, 'true'); +run_bgworker_interruptible_test( + $node, + "ALTER DATABASE renameddb SET TABLESPACE test_tablespace", + "ALTER DATABASE SET TABLESPACE", $pid); + +# Test case 4: DROP DATABASE +$pid = launch_bgworker($node, 'renameddb', 4, 'true'); +run_bgworker_interruptible_test( + $node, + "DROP DATABASE renameddb", + "DROP DATABASE", $pid); + +done_testing(); diff --git a/src/test/modules/worker_spi/worker_spi--1.0.sql b/src/test/modules/worker_spi/worker_spi--1.0.sql index 84deb6199f635..f5e9621b0d1e8 100644 --- a/src/test/modules/worker_spi/worker_spi--1.0.sql +++ b/src/test/modules/worker_spi/worker_spi--1.0.sql @@ -7,7 +7,8 @@ CREATE FUNCTION worker_spi_launch(index int4, dboid oid DEFAULT 0, roleoid oid DEFAULT 0, - flags text[] DEFAULT '{}') + flags text[] DEFAULT '{}', + interruptible boolean DEFAULT false) RETURNS pg_catalog.int4 STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/worker_spi/worker_spi.c b/src/test/modules/worker_spi/worker_spi.c index 9c53d896b6ae5..b635a48634947 100644 --- a/src/test/modules/worker_spi/worker_spi.c +++ b/src/test/modules/worker_spi/worker_spi.c @@ -13,7 +13,7 @@ * "delta" type. Delta rows will be deleted by this worker and their values * aggregated into the total. * - * Copyright (c) 2013-2025, PostgreSQL Global Development Group + * Copyright (c) 2013-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/test/modules/worker_spi/worker_spi.c @@ -30,7 +30,7 @@ /* these headers are used by this particular worker's code */ #include "access/xact.h" -#include "commands/dbcommands.h" +#include "catalog/pg_database.h" #include "executor/spi.h" #include "fmgr.h" #include "lib/stringinfo.h" @@ -39,12 +39,13 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/snapmgr.h" +#include "utils/wait_event.h" PG_MODULE_MAGIC; PG_FUNCTION_INFO_V1(worker_spi_launch); -PGDLLEXPORT pg_noreturn void worker_spi_main(Datum main_arg); +pg_noreturn PGDLLEXPORT void worker_spi_main(Datum main_arg); /* GUC variables */ static int worker_spi_naptime = 10; @@ -140,9 +141,9 @@ worker_spi_main(Datum main_arg) Oid dboid; Oid roleoid; char *p; - bits32 flags = 0; + uint32 flags = 0; - table = palloc(sizeof(worktable)); + table = palloc_object(worktable); sprintf(name, "schema%d", index); table->schema = pstrdup(name); table->name = pstrdup("counted"); @@ -153,7 +154,7 @@ worker_spi_main(Datum main_arg) p += sizeof(Oid); memcpy(&roleoid, p, sizeof(Oid)); p += sizeof(Oid); - memcpy(&flags, p, sizeof(bits32)); + memcpy(&flags, p, sizeof(uint32)); /* Establish signal handlers before unblocking signals. */ pqsignal(SIGHUP, SignalHandlerForConfigReload); @@ -399,15 +400,20 @@ worker_spi_launch(PG_FUNCTION_ARGS) BgwHandleStatus status; pid_t pid; char *p; - bits32 flags = 0; + uint32 flags = 0; ArrayType *arr = PG_GETARG_ARRAYTYPE_P(3); Size ndim; int nelems; Datum *datum_flags; + bool interruptible = PG_GETARG_BOOL(4); memset(&worker, 0, sizeof(worker)); worker.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION; + + if (interruptible) + worker.bgw_flags |= BGWORKER_INTERRUPTIBLE; + worker.bgw_start_time = BgWorkerStart_RecoveryFinished; worker.bgw_restart_time = BGW_NEVER_RESTART; sprintf(worker.bgw_library_name, "worker_spi"); @@ -466,7 +472,7 @@ worker_spi_launch(PG_FUNCTION_ARGS) p += sizeof(Oid); memcpy(p, &roleoid, sizeof(Oid)); p += sizeof(Oid); - memcpy(p, &flags, sizeof(bits32)); + memcpy(p, &flags, sizeof(uint32)); if (!RegisterDynamicBackgroundWorker(&worker, &handle)) PG_RETURN_NULL(); diff --git a/src/test/modules/xid_wraparound/meson.build b/src/test/modules/xid_wraparound/meson.build index f7dada67f678a..97ce670f9ac21 100644 --- a/src/test/modules/xid_wraparound/meson.build +++ b/src/test/modules/xid_wraparound/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group xid_wraparound_sources = files( 'xid_wraparound.c', @@ -30,6 +30,7 @@ tests += { 't/001_emergency_vacuum.pl', 't/002_limits.pl', 't/003_wraparounds.pl', + 't/004_notify_freeze.pl', ], }, } diff --git a/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl b/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl index 73d1ec4af19a7..213f9052ed23b 100644 --- a/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl +++ b/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Test wraparound emergency autovacuum. use strict; diff --git a/src/test/modules/xid_wraparound/t/002_limits.pl b/src/test/modules/xid_wraparound/t/002_limits.pl index aa1d8765d3a0f..86632a8d51094 100644 --- a/src/test/modules/xid_wraparound/t/002_limits.pl +++ b/src/test/modules/xid_wraparound/t/002_limits.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # Test XID wraparound limits. # @@ -90,7 +90,7 @@ last; } } -ok($warn_limit == 1, "warn-limit reached"); +is($warn_limit, 1, "warn-limit reached"); # We can still INSERT, despite the warnings. $node->safe_psql('postgres', diff --git a/src/test/modules/xid_wraparound/t/003_wraparounds.pl b/src/test/modules/xid_wraparound/t/003_wraparounds.pl index 2aeaee8769c7d..f44c4a00a4cf8 100644 --- a/src/test/modules/xid_wraparound/t/003_wraparounds.pl +++ b/src/test/modules/xid_wraparound/t/003_wraparounds.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # Consume a lot of XIDs, wrapping around a few times. # diff --git a/src/test/modules/xid_wraparound/t/004_notify_freeze.pl b/src/test/modules/xid_wraparound/t/004_notify_freeze.pl new file mode 100644 index 0000000000000..d0a1f1fe2fc93 --- /dev/null +++ b/src/test/modules/xid_wraparound/t/004_notify_freeze.pl @@ -0,0 +1,71 @@ +# Copyright (c) 2024-2026, PostgreSQL Global Development Group +# +# Test freezing XIDs in the async notification queue. This isn't +# really wraparound-related, but the test depends on the +# consume_xids() helper function. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bxid_wraparound\b/) +{ + plan skip_all => "test xid_wraparound not enabled in PG_TEST_EXTRA"; +} + +# Setup +$node->safe_psql('postgres', 'CREATE EXTENSION xid_wraparound'); +$node->safe_psql('postgres', + 'ALTER DATABASE template0 WITH ALLOW_CONNECTIONS true'); + +# Start Session 1 and leave it idle in transaction +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_safe('listen s;'); +$psql_session1->query_safe('begin;'); + +# Send some notifys from other sessions +for my $i (1 .. 10) +{ + $node->safe_psql('postgres', "NOTIFY s, '$i'"); +} + +# Consume enough XIDs to trigger truncation, and one more with +# 'txid_current' to bump up the freeze horizon. +$node->safe_psql('postgres', 'select consume_xids(10000000);'); +$node->safe_psql('postgres', 'select txid_current()'); + +# Remember current datfrozenxid before vacuum freeze so that we can +# check that it is advanced. (Taking the min() this way assumes that +# XID wraparound doesn't happen.) +my $datafronzenxid = $node->safe_psql('postgres', + "select min(datfrozenxid::text::bigint) from pg_database"); + +# Execute vacuum freeze on all databases +$node->command_ok([ 'vacuumdb', '--all', '--freeze', '--port', $node->port ], + "vacuumdb --all --freeze"); + +# Check that vacuumdb advanced datfrozenxid +my $datafronzenxid_freeze = $node->safe_psql('postgres', + "select min(datfrozenxid::text::bigint) from pg_database"); +ok($datafronzenxid_freeze > $datafronzenxid, 'datfrozenxid advanced'); + +# On Session 1, commit and ensure that the all the notifications are +# received. This depends on correctly freezing the XIDs in the pending +# notification entries. +my $res = $psql_session1->query_safe('commit;'); +my $notifications_count = 0; +foreach my $i (split('\n', $res)) +{ + $notifications_count++; + like($i, + qr/Asynchronous notification "s" with payload "$notifications_count" received/ + ); +} +is($notifications_count, 10, 'received all committed notifications'); + +done_testing(); diff --git a/src/test/modules/xid_wraparound/xid_wraparound.c b/src/test/modules/xid_wraparound/xid_wraparound.c index e27a5fa576954..ca25d7e02069e 100644 --- a/src/test/modules/xid_wraparound/xid_wraparound.c +++ b/src/test/modules/xid_wraparound/xid_wraparound.c @@ -4,7 +4,7 @@ * Utilities for testing XID wraparound * * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION diff --git a/src/test/perl/Makefile b/src/test/perl/Makefile index 3c179005c30d8..fd4fdaf700be9 100644 --- a/src/test/perl/Makefile +++ b/src/test/perl/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/perl # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/perl/Makefile diff --git a/src/test/perl/PostgreSQL/Test/AdjustDump.pm b/src/test/perl/PostgreSQL/Test/AdjustDump.pm index 5b642396ac7ec..f745601e55ff1 100644 --- a/src/test/perl/PostgreSQL/Test/AdjustDump.pm +++ b/src/test/perl/PostgreSQL/Test/AdjustDump.pm @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group =pod diff --git a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm index 1725fe2f94871..5cc7a0b50ae79 100644 --- a/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm +++ b/src/test/perl/PostgreSQL/Test/AdjustUpgrade.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group =pod @@ -112,6 +112,29 @@ sub adjust_database_contents 'drop extension if exists test_ext7'); } + # btree_gist inet/cidr indexes cannot be upgraded to v19 + if ($old_version < 19) + { + if ($dbnames{"contrib_regression_btree_gist"}) + { + _add_st($result, 'contrib_regression_btree_gist', + "drop index if exists public.inettmp_a_a1_idx"); + _add_st($result, 'contrib_regression_btree_gist', + "drop index if exists public.inetidx"); + _add_st($result, 'contrib_regression_btree_gist', + "drop index public.cidridx"); + } + if ($dbnames{"regression_btree_gist"}) + { + _add_st($result, 'regression_btree_gist', + "drop index if exists public.inettmp_a_a1_idx"); + _add_st($result, 'regression_btree_gist', + "drop index if exists public.inetidx"); + _add_st($result, 'regression_btree_gist', + "drop index public.cidridx"); + } + } + # we removed these test-support functions in v18 if ($old_version < 18) { @@ -251,6 +274,32 @@ sub adjust_database_contents 'drop operator if exists public.=> (bigint, NONE)'); } + # Version 19 changed the output format of pg_lsn. To avoid output + # differences, set all pg_lsn columns to NULL if the old version is + # older than 19. + if ($old_version < 19) + { + if ($old_version >= '9.5') + { + _add_st($result, 'regression', + "update brintest set lsncol = NULL"); + } + + if ($old_version >= 12) + { + _add_st($result, 'regression', + "update tab_core_types set pg_lsn = NULL"); + } + + if ($old_version >= 14) + { + _add_st($result, 'regression', + "update brintest_multi set lsncol = NULL"); + _add_st($result, 'regression', + "update brintest_bloom set lsncol = NULL"); + } + } + return $result; } @@ -304,8 +353,8 @@ sub adjust_old_dumpfile # Version comments will certainly not match. $dump =~ s/^-- Dumped from database version.*\n//mg; - # Same with version argument to pg_restore_relation_stats() or - # pg_restore_attribute_stats(). + # Same with version argument to pg_restore_relation_stats(), + # pg_restore_attribute_stats() or pg_restore_extended_stats(). $dump =~ s {\n(\s+'version',) '\d+'::integer,$} {$1 '000000'::integer,}mg; @@ -654,8 +703,8 @@ sub adjust_new_dumpfile # Version comments will certainly not match. $dump =~ s/^-- Dumped from database version.*\n//mg; - # Same with version argument to pg_restore_relation_stats() or - # pg_restore_attribute_stats(). + # Same with version argument to pg_restore_relation_stats(), + # pg_restore_attribute_stats() or pg_restore_extended_stats(). $dump =~ s {\n(\s+'version',) '\d+'::integer,$} {$1 '000000'::integer,}mg; diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm index 60bbd5dd445b3..c6ff2dbde4cac 100644 --- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm +++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group =pod @@ -155,11 +155,11 @@ sub wait_connect # # See query() for details about why/how the banner is used. my $banner = "background_psql: ready"; - my $banner_match = qr/(^|\n)$banner\r?\n/; - $self->{stdin} .= "\\echo $banner\n\\warn $banner\n"; + my $banner_match = qr/$banner\r?\n/; + $self->{stdin} .= "\\echo '$banner'\n\\warn '$banner'\n"; $self->{run}->pump() until ($self->{stdout} =~ /$banner_match/ - && $self->{stderr} =~ /$banner\r?\n/) + && $self->{stderr} =~ /$banner_match/) || $self->{timeout}->is_expired; note "connect output:\n", @@ -230,18 +230,23 @@ Executes a query in the current session and returns the output in scalar context and (output, error) in list context where error is 1 in case there was output generated on stderr when executing the query. +By default, the query and its results are printed to the test output. This +can be disabled by passing the keyword parameter verbose => false. + =cut sub query { - my ($self, $query) = @_; + my ($self, $query, %params) = @_; my $ret; my $output; my $query_cnt = $self->{query_cnt}++; + $params{verbose} = 1 unless defined $params{verbose}; + local $Test::Builder::Level = $Test::Builder::Level + 1; - note "issuing query $query_cnt via background psql: $query"; + note "issuing query $query_cnt via background psql: $query" unless !$params{verbose}; $self->{timeout}->start() if (defined($self->{query_timer_restart})); @@ -259,32 +264,30 @@ sub query # stderr (or vice versa), even if psql printed them in the opposite # order. We therefore wait on both. # - # We need to match for the newline, because we try to remove it below, and - # it's possible to consume just the input *without* the newline. In - # interactive psql we emit \r\n, so we need to allow for that. Also need - # to be careful that we don't e.g. match the echoed \echo command, rather - # than its output. + # In interactive psql we emit \r\n, so we need to allow for that. + # Also, include quotes around the banner string in the \echo and \warn + # commands, not because the string needs quoting but so that $banner_match + # can't match readline's echoing of these commands. my $banner = "background_psql: QUERY_SEPARATOR $query_cnt:"; - my $banner_match = qr/(^|\n)$banner\r?\n/; - $self->{stdin} .= "$query\n;\n\\echo $banner\n\\warn $banner\n"; - pump_until( - $self->{run}, $self->{timeout}, - \$self->{stdout}, qr/$banner_match/); - pump_until( - $self->{run}, $self->{timeout}, - \$self->{stderr}, qr/$banner_match/); - - die "psql query timed out" if $self->{timeout}->is_expired; + my $banner_match = qr/$banner\r?\n/; + $self->{stdin} .= "$query\n;\n\\echo '$banner'\n\\warn '$banner'\n"; + $self->{run}->pump() + until ($self->{stdout} =~ /$banner_match/ + && $self->{stderr} =~ /$banner_match/) + || $self->{timeout}->is_expired; note "results query $query_cnt:\n", explain { stdout => $self->{stdout}, stderr => $self->{stderr}, - }; + } unless !$params{verbose}; + + die "psql query timed out" if $self->{timeout}->is_expired; - # Remove banner from stdout and stderr, our caller doesn't care. The - # first newline is optional, as there would not be one if consuming an - # empty query result. + # Remove banner from stdout and stderr, our caller doesn't want it. + # Also remove the query output's trailing newline, if present (there + # would not be one if consuming an empty query result). + $banner_match = qr/\r?\n?$banner\r?\n/; $output = $self->{stdout}; $output =~ s/$banner_match//; $self->{stderr} =~ s/$banner_match//; @@ -308,9 +311,9 @@ Query failure is determined by it producing output on stderr. sub query_safe { - my ($self, $query) = @_; + my ($self, $query, %params) = @_; - my $ret = $self->query($query); + my $ret = $self->query($query, %params); if ($self->{stderr} ne "") { diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm index 1c11750ac1d07..bdc051e6ead51 100644 --- a/src/test/perl/PostgreSQL/Test/Cluster.pm +++ b/src/test/perl/PostgreSQL/Test/Cluster.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group =pod @@ -57,7 +57,7 @@ PostgreSQL::Test::Cluster - class representing PostgreSQL server instance # run query every second until it returns 't' # or times out $node->poll_query_until('postgres', q|SELECT random() < 0.1;|') - or die "timed out"; + or croak "timed out"; # Do an online pg_basebackup my $ret = $node->backup('testbackup1'); @@ -290,6 +290,33 @@ sub connstr =pod +=item $node->is_alive() + +Check if the node is alive, using pg_isready. +Returns 1 if successful, 0 on failure. + +=cut + +sub is_alive +{ + my ($self) = @_; + local %ENV = $self->_get_env(); + + my $ret = PostgreSQL::Test::Utils::system_log( + 'pg_isready', + '--timeout' => $PostgreSQL::Test::Utils::timeout_default, + '--host' => $self->host, + '--port' => $self->port); + + if ($ret != 0) + { + return 0; + } + return 1; +} + +=pod + =item $node->raw_connect() Open a raw TCP or Unix domain socket connection to the server. This is @@ -312,7 +339,7 @@ sub raw_connect $socket = IO::Socket::UNIX->new( Type => SOCK_STREAM(), Peer => $path, - ) or die "Cannot create socket - $IO::Socket::errstr\n"; + ) or croak "Cannot create socket - $IO::Socket::errstr\n"; } else { @@ -320,7 +347,7 @@ sub raw_connect PeerHost => $pghost, PeerPort => $pgport, Proto => 'tcp' - ) or die "Cannot create socket - $IO::Socket::errstr\n"; + ) or croak "Cannot create socket - $IO::Socket::errstr\n"; } return $socket; } @@ -379,7 +406,7 @@ sub group_access my $dir_stat = stat($self->data_dir); defined($dir_stat) - or die('unable to stat ' . $self->data_dir); + or croak('unable to stat ' . $self->data_dir); return (S_IMODE($dir_stat->mode) == 0750); } @@ -481,7 +508,7 @@ sub config_data my $result = IPC::Run::run [ $self->installed_command('pg_config'), @options ], '>', \$stdout, '2>', \$stderr - or die "could not execute pg_config"; + or croak "could not execute pg_config"; # standardize line endings $stdout =~ s/\r(?=\n)//g; # no options, scalar context: just hand back the output @@ -515,7 +542,7 @@ sub info { my ($self) = @_; my $_info = ''; - open my $fh, '>', \$_info or die; + open my $fh, '>', \$_info or croak; print $fh "Name: " . $self->name . "\n"; print $fh "Version: " . $self->{_pg_version} . "\n" if $self->{_pg_version}; @@ -526,7 +553,7 @@ sub info print $fh "Log file: " . $self->logfile . "\n"; print $fh "Install Path: ", $self->{_install_path} . "\n" if $self->{_install_path}; - close $fh or die; + close $fh or croak; return $_info; } @@ -556,7 +583,7 @@ sub set_replication_conf $self->host eq $test_pghost or croak "set_replication_conf only works with the default host"; - open my $hba, '>>', "$pgdata/pg_hba.conf" or die $!; + open my $hba, '>>', "$pgdata/pg_hba.conf" or croak $!; print $hba "\n# Allow replication (set up by PostgreSQL::Test::Cluster.pm)\n"; if ($PostgreSQL::Test::Utils::windows_os @@ -680,11 +707,11 @@ sub init PostgreSQL::Test::Utils::system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata, @{ $params{auth_extra} }); - open my $conf, '>>', "$pgdata/postgresql.conf" or die $!; + open my $conf, '>>', "$pgdata/postgresql.conf" or croak $!; print $conf "\n# Added by PostgreSQL::Test::Cluster.pm\n"; print $conf "fsync = off\n"; print $conf "restart_after_crash = off\n"; - print $conf "log_line_prefix = '%m [%p] %q%a '\n"; + print $conf "log_line_prefix = '%m %b[%p] %q%a '\n"; print $conf "log_statement = all\n"; print $conf "log_replication_commands = on\n"; print $conf "wal_retrieve_retry_interval = '500ms'\n"; @@ -737,7 +764,7 @@ sub init close $conf; chmod($self->group_access ? 0640 : 0600, "$pgdata/postgresql.conf") - or die("unable to set permissions for $pgdata/postgresql.conf"); + or croak("unable to set permissions for $pgdata/postgresql.conf"); $self->set_replication_conf if $params{allows_streaming}; $self->enable_archiving if $params{has_archiving}; @@ -766,7 +793,7 @@ sub append_conf PostgreSQL::Test::Utils::append_to_file($conffile, $str . "\n"); chmod($self->group_access() ? 0640 : 0600, $conffile) - or die("unable to set permissions for $conffile"); + or croak("unable to set permissions for $conffile"); return; } @@ -812,7 +839,7 @@ sub adjust_conf close $fh; chmod($self->group_access() ? 0640 : 0600, $conffile) - or die("unable to set permissions for $conffile"); + or croak("unable to set permissions for $conffile"); } =pod @@ -968,7 +995,7 @@ sub init_from_backup } elsif (defined $params{tar_program}) { - mkdir($data_path) || die "mkdir $data_path: $!"; + mkdir($data_path) || croak "mkdir $data_path: $!"; PostgreSQL::Test::Utils::system_or_bail( $params{tar_program}, 'xf' => $backup_path . '/base.tar', @@ -980,7 +1007,7 @@ sub init_from_backup # We need to generate a tablespace_map file. open(my $tsmap, ">", "$data_path/tablespace_map") - || die "$data_path/tablespace_map: $!"; + || croak "$data_path/tablespace_map: $!"; # Extract tarfiles and add tablespace_map entries my @tstars = grep { /^\d+.tar/ } @@ -990,12 +1017,12 @@ sub init_from_backup my $tsoid = $tstar; $tsoid =~ s/\.tar$//; - die "no tablespace mapping for $tstar" + croak "no tablespace mapping for $tstar" if !exists $params{tablespace_map} || !exists $params{tablespace_map}{$tsoid}; my $newdir = $params{tablespace_map}{$tsoid}; - mkdir($newdir) || die "mkdir $newdir: $!"; + mkdir($newdir) || croak "mkdir $newdir: $!"; PostgreSQL::Test::Utils::system_or_bail( $params{tar_program}, 'xf' => $backup_path . '/' . $tstar, @@ -1034,12 +1061,12 @@ sub init_from_backup { # We need to generate a tablespace_map file. open(my $tsmap, ">", "$data_path/tablespace_map") - || die "$data_path/tablespace_map: $!"; + || croak "$data_path/tablespace_map: $!"; # Now use the list of tablespace links to copy each tablespace. for my $tsoid (@tsoids) { - die "no tablespace mapping for $tsoid" + croak "no tablespace mapping for $tsoid" if !exists $params{tablespace_map} || !exists $params{tablespace_map}{$tsoid}; @@ -1056,7 +1083,7 @@ sub init_from_backup close($tsmap); } } - chmod(0700, $data_path) or die $!; + chmod(0700, $data_path) or croak $!; # Base configuration for this node $self->append_conf( @@ -1275,6 +1302,27 @@ Wrapper for pg_ctl restart. With optional extra param fail_ok => 1, returns 0 for failure instead of bailing out. +=over + +=item fail_ok => 1 + +By default, failure terminates the entire F invocation. If given, +instead return 0 for failure instead of bailing out. + +=item log_unlike => B + +When defined, the logfile is inspected for the presence of the fragment by +matching the specified pattern. If the pattern matches against the logfile a +test failure will be logged. + +=item log_like => B + +When defined, the logfile is inspected for the presence of the fragment by +matching the pattern. If the pattern doesn't match a test failure will be +logged. + +=back + =cut sub restart @@ -1287,6 +1335,8 @@ sub restart print "### Restarting node \"$name\"\n"; + my $log_location = -s $self->logfile; + # -w is now the default but having it here does no harm and helps # compatibility with older versions. $ret = PostgreSQL::Test::Utils::system_log( @@ -1295,6 +1345,18 @@ sub restart '--log' => $self->logfile, 'restart'); + # Check for expected and/or unexpected log fragments if the caller + # specified such checks in the params + if (defined $params{log_unlike} || defined $params{log_like}) + { + my $log = + PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location); + unlike($log, $params{log_unlike}, "unexpected fragment found in log") + if defined $params{log_unlike}; + like($log, $params{log_like}, "expected fragment not found in log") + if defined $params{log_like}; + } + if ($ret != 0) { print "# pg_ctl restart failed; see logfile for details: " @@ -1620,10 +1682,10 @@ sub new or BAIL_OUT("could not create data directory \"$node->{_basedir}\": $!"); - $node->dump_info; - $node->_set_pg_version; + $node->dump_info; + my $ver = $node->{_pg_version}; # Use a subclass as defined below (or elsewhere) if this version @@ -1766,13 +1828,20 @@ sub _get_env return (%inst_env); } -# Private routine to get an installation path qualified command. -# -# IPC::Run maintains a cache, %cmd_cache, mapping commands to paths. Tests -# which use nodes spanning more than one postgres installation path need to -# avoid confusing which installation's binaries get run. Setting $ENV{PATH} is -# insufficient, as IPC::Run does not check to see if the path has changed since -# caching a command. +=pod + +=item $node->installed_command(cmd) + +Get an installation path qualified command. + +IPC::Run maintains a cache, %cmd_cache, mapping commands to paths. Tests +which use nodes spanning more than one postgres installation path need to +avoid confusing which installation's binaries get run. Setting $ENV{PATH} is +insufficient, as IPC::Run does not check to see if the path has changed since +caching a command. + +=cut + sub installed_command { my ($self, $cmd) = @_; @@ -1872,7 +1941,7 @@ sub can_bind my $paddr = sockaddr_in($port, $iaddr); socket(SOCK, PF_INET, SOCK_STREAM, 0) - or die "socket failed: $!"; + or croak "socket failed: $!"; # As in postmaster, don't use SO_REUSEADDR on Windows setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) @@ -1890,9 +1959,9 @@ sub _reserve_port # open in rw mode so we don't have to reopen it and lose the lock my $filename = "$portdir/$port.rsv"; sysopen(my $portfile, $filename, O_RDWR | O_CREAT) - || die "opening port file $filename: $!"; + || croak "opening port file $filename: $!"; # take an exclusive lock to avoid concurrent access - flock($portfile, LOCK_EX) || die "locking port file $filename: $!"; + flock($portfile, LOCK_EX) || croak "locking port file $filename: $!"; # see if someone else has or had a reservation of this port my $pid = <$portfile> || "0"; chomp $pid; @@ -1901,16 +1970,16 @@ sub _reserve_port if (kill 0, $pid) { # process exists and is owned by us, so we can't reserve this port - flock($portfile, LOCK_UN) || die $!; + flock($portfile, LOCK_UN) || croak $!; close($portfile); return 0; } } # All good, go ahead and reserve the port - seek($portfile, 0, SEEK_SET) || die $!; + seek($portfile, 0, SEEK_SET) || croak $!; # print the pid with a fixed width so we don't leave any trailing junk print $portfile sprintf("%10d\n", $$); - flock($portfile, LOCK_UN) || die $!; + flock($portfile, LOCK_UN) || croak $!; close($portfile); push(@port_reservation_files, $filename); return 1; @@ -2199,18 +2268,26 @@ sub psql $ret = $?; }; my $exc_save = $@; + + # we need a dummy $stderr from hereon, if we didn't collect it + if (! defined $stderr) + { + my $errtxt = ""; + $stderr = \$errtxt; + } + if ($exc_save) { # IPC::Run::run threw an exception. re-throw unless it's a # timeout, which we'll handle by testing is_expired - die $exc_save + croak $exc_save if (blessed($exc_save) || $exc_save !~ /^\Q$timeout_exception\E/); $ret = undef; - die "Got timeout exception '$exc_save' but timer not expired?!" + croak "Got timeout exception '$exc_save' but timer not expired?!" unless $timeout->is_expired; if (defined($params{timed_out})) @@ -2219,7 +2296,7 @@ sub psql } else { - die "psql timed out: stderr: '$$stderr'\n" + croak "psql timed out: stderr: '$$stderr'\n" . "while running '@psql_params'"; } } @@ -2242,7 +2319,7 @@ sub psql if (defined $ret) { my $core = $ret & 128 ? " (core dumped)" : ""; - die "psql exited with signal " + croak "psql exited with signal " . ($ret & 127) . "$core: '$$stderr' while running '@psql_params'" if $ret & 127; @@ -2251,14 +2328,14 @@ sub psql if ($ret && $params{on_error_die}) { - die "psql error: stderr: '$$stderr'\nwhile running '@psql_params'" + croak "psql error: stderr: '$$stderr'\nwhile running '@psql_params'" if $ret == 1; - die "connection error: '$$stderr'\nwhile running '@psql_params'" + croak "connection error: '$$stderr'\nwhile running '@psql_params'" if $ret == 2; - die + croak "error running SQL: '$$stderr'\nwhile running '@psql_params' with sql '$sql'" if $ret == 3; - die "psql returns $ret: '$$stderr'\nwhile running '@psql_params'"; + croak "psql returns $ret: '$$stderr'\nwhile running '@psql_params'"; } if (wantarray) @@ -2459,7 +2536,7 @@ sub _pgbench_make_files if (-e $filename) { ok(0, "$filename must not already exist"); - unlink $filename or die "cannot unlink $filename: $!"; + unlink $filename or croak "cannot unlink $filename: $!"; } PostgreSQL::Test::Utils::append_to_file($filename, $$files{$fn}); } @@ -3081,8 +3158,8 @@ sub write_wal my $path = sprintf("%s/pg_wal/%08X%08X%08X", $self->data_dir, $tli, 0, $segment); - open my $fh, "+<:raw", $path or die "could not open WAL segment $path"; - seek($fh, $offset, SEEK_SET) or die "could not seek WAL segment $path"; + open my $fh, "+<:raw", $path or croak "could not open WAL segment $path"; + seek($fh, $offset, SEEK_SET) or croak "could not seek WAL segment $path"; print $fh $data; close $fh; @@ -3246,7 +3323,7 @@ sub wait_for_event SELECT count(*) > 0 FROM pg_stat_activity WHERE backend_type = '$backend_type' AND wait_event = '$wait_event_name' ]) - or die + or croak qq(timed out when waiting for $backend_type to reach wait event '$wait_event_name'); return; @@ -3256,11 +3333,10 @@ sub wait_for_event =item $node->wait_for_catchup(standby_name, mode, target_lsn) -Wait for the replication connection with application_name standby_name until -its 'mode' replication column in pg_stat_replication equals or passes the -specified or default target_lsn. By default the replay_lsn is waited for, -but 'mode' may be specified to wait for any of sent|write|flush|replay. -The replication connection must be in a streaming state. +Wait until the standby identified by standby_name has reached the specified +or default target_lsn for the given 'mode'. By default the replay_lsn +is waited for, but 'mode' may be specified to wait for any of +sent|write|flush|replay. When doing physical replication, the standby is usually identified by passing its PostgreSQL::Test::Cluster instance. When doing logical @@ -3278,12 +3354,20 @@ If you pass an explicit value of target_lsn, it should almost always be the primary's write LSN; so this parameter is seldom needed except when querying some intermediate replication node rather than the primary. -If there is no active replication connection from this peer, waits until -poll_query_until timeout. +When the standby is passed as a PostgreSQL::Test::Cluster instance and the +mode is replay, write, or flush, the function uses WAIT FOR LSN on the +standby for latch-based wakeup instead of polling. If the standby has been +promoted, if the session is interrupted by a recovery conflict, or if the +standby is unreachable, it falls back to polling. + +For 'sent' mode, when the standby is passed as a string (e.g., a +subscription name), when the sparc64+ext4 bug is detected, or as a fallback +from the above, the function polls pg_stat_replication on the upstream. +The replication connection must be in a streaming state for this path. Requires that the 'postgres' db exists and is accessible. -This is not a test. It die()s on failure. +This is not a test. It croak()s on failure. =cut @@ -3297,10 +3381,13 @@ sub wait_for_catchup . join(', ', keys(%valid_modes)) unless exists($valid_modes{$mode}); - # Allow passing of a PostgreSQL::Test::Cluster instance as shorthand + # Keep a reference to the standby node if passed as an object, so we can + # use WAIT FOR LSN on it later. + my $standby_node; if (blessed($standby_name) && $standby_name->isa("PostgreSQL::Test::Cluster")) { + $standby_node = $standby_name; $standby_name = $standby_name->name; } if (!defined($target_lsn)) @@ -3325,6 +3412,88 @@ sub wait_for_catchup . $self->name . "\n"; # Before release 12 walreceiver just set the application name to # "walreceiver" + + # Use WAIT FOR LSN on the standby when: + # - The standby was passed as a Cluster object (so we can connect to it) + # - The mode is replay, write, or flush (not 'sent') + # - There is no sparc64+ext4 bug + # This is more efficient than polling pg_stat_replication on the upstream, + # as WAIT FOR LSN uses a latch-based wakeup mechanism. + # + # We skip the pg_is_in_recovery() pre-check and just attempt WAIT FOR + # LSN directly. If the standby was promoted, it returns 'not_in_recovery' + # and we fall back to polling. + if ( defined($standby_node) + && ($mode ne 'sent') + && (!PostgreSQL::Test::Utils::has_wal_read_bug)) + { + # Map mode names to WAIT FOR LSN mode names + my %mode_map = ( + 'replay' => 'standby_replay', + 'write' => 'standby_write', + 'flush' => 'standby_flush',); + my $wait_mode = $mode_map{$mode}; + my $timeout = $PostgreSQL::Test::Utils::timeout_default; + my $wait_query = + qq[WAIT FOR LSN '${target_lsn}' WITH (MODE '${wait_mode}', timeout '${timeout}s', no_throw);]; + + # Try WAIT FOR LSN. If it succeeds, we're done. If it returns + # 'not_in_recovery' (standby was promoted), fall back to polling. + # If the session is interrupted (e.g., killed by recovery conflict), + # fall back to polling on the upstream which is immune to standby- + # side conflicts. + my $output; + local $@; + my $wait_succeeded = eval { + $output = $standby_node->safe_psql('postgres', $wait_query); + chomp($output); + 1; + }; + + if ($wait_succeeded && $output eq 'success') + { + print "done\n"; + return; + } + + # 'not in recovery' means the standby was promoted. + if ($wait_succeeded && $output eq 'not in recovery') + { + diag + "WAIT FOR LSN returned 'not in recovery', falling back to polling"; + } + # 'timeout' is a hard failure - no point falling back to polling. + elsif ($wait_succeeded) + { + my $details = $self->safe_psql('postgres', + "SELECT * FROM pg_catalog.pg_stat_replication"); + diag qq(WAIT FOR LSN returned '$output' +pg_stat_replication on upstream: +${details}); + croak + "WAIT FOR LSN '$wait_mode' to '$target_lsn' returned '$output'"; + } + # WAIT FOR LSN was interrupted. Fall back to polling if this + # looks like a recovery conflict. We match the English error + # message "conflict with recovery" which is reliable because the + # test suite runs with LC_MESSAGES=C. Other errors should fail + # immediately rather than being masked by a silent fallback. + elsif ($@ =~ /conflict with recovery/i) + { + diag qq(WAIT FOR LSN interrupted, falling back to polling: +$@); + } + else + { + croak "WAIT FOR LSN failed: $@"; + } + } + + # Fall back to polling pg_stat_replication on the upstream for: + # - 'sent' mode (no corresponding WAIT FOR LSN mode) + # - When standby_name is a string (e.g., subscription name) + # - When the standby is no longer in recovery (was promoted) + # - When WAIT FOR LSN was interrupted (e.g., killed by a recovery conflict) my $query = qq[SELECT '$target_lsn' <= ${mode}_lsn AND state = 'streaming' FROM pg_catalog.pg_stat_replication WHERE application_name IN ('$standby_name', 'walreceiver')]; @@ -3367,7 +3536,7 @@ The replication connection must be in a streaming state. Requires that the 'postgres' db exists and is accessible. -This is not a test. It die()s on failure. +This is not a test. It croak()s on failure. =cut @@ -3387,7 +3556,7 @@ be 'restart' or 'confirmed_flush'. Requires that the 'postgres' db exists and is accessible. -This is not a test. It die()s on failure. +This is not a test. It croak()s on failure. If the slot is not active, will time out after poll_query_until's timeout. @@ -3442,7 +3611,7 @@ creating a new subscription. If there is no active replication connection from this peer, wait until poll_query_until timeout. -This is not a test. It die()s on failure. +This is not a test. It croak()s on failure. =cut @@ -3493,25 +3662,9 @@ If successful, returns the length of the entire log file, in bytes. sub wait_for_log { my ($self, $regexp, $offset) = @_; - $offset = 0 unless defined $offset; - - my $max_attempts = 10 * $PostgreSQL::Test::Utils::timeout_default; - my $attempts = 0; - - while ($attempts < $max_attempts) - { - my $log = - PostgreSQL::Test::Utils::slurp_file($self->logfile, $offset); - - return $offset + length($log) if ($log =~ m/$regexp/); - - # Wait 0.1 second before retrying. - usleep(100_000); - - $attempts++; - } - croak "timed out waiting for match: $regexp"; + return PostgreSQL::Test::Utils::wait_for_file($self->logfile, $regexp, + $offset); } =pod @@ -3598,7 +3751,7 @@ Disallows pg_recvlogical from internally retrying on error by passing --no-loop. Plugin options are passed as additional keyword arguments. -If called in scalar context, returns stdout, and die()s on timeout or nonzero return. +If called in scalar context, returns stdout, and croak()s on timeout or nonzero return. If called in array context, returns a tuple of (retval, stdout, stderr, timeout). timeout is the IPC::Run::Timeout object whose is_expired method can be tested @@ -3654,15 +3807,15 @@ sub pg_recvlogical_upto # IPC::Run::run threw an exception. re-throw unless it's a # timeout, which we'll handle by testing is_expired - die $exc_save + croak $exc_save if (blessed($exc_save) || $exc_save !~ qr/$timeout_exception/); $ret = undef; - die "Got timeout exception '$exc_save' but timer not expired?!" + croak "Got timeout exception '$exc_save' but timer not expired?!" unless $timeout->is_expired; - die + croak "$exc_save waiting for endpos $endpos with stdout '$stdout', stderr '$stderr'" unless wantarray; } @@ -3674,7 +3827,7 @@ sub pg_recvlogical_upto } else { - die + croak "pg_recvlogical exited with code '$ret', stdout '$stdout' and stderr '$stderr'" if $ret; return $stdout; @@ -3699,14 +3852,14 @@ sub corrupt_page_checksum my $pgdata = $self->data_dir; my $pageheader; - open my $fh, '+<', "$pgdata/$file" or die "open($file) failed: $!"; + open my $fh, '+<', "$pgdata/$file" or croak "open($file) failed: $!"; binmode $fh; - sysseek($fh, $page_offset, 0) or die "sysseek failed: $!"; - sysread($fh, $pageheader, 24) or die "sysread failed: $!"; + sysseek($fh, $page_offset, 0) or croak "sysseek failed: $!"; + sysread($fh, $pageheader, 24) or croak "sysread failed: $!"; # This inverts the pd_checksum field (only); see struct PageHeaderData $pageheader ^= "\0\0\0\0\0\0\0\0\xff\xff"; - sysseek($fh, $page_offset, 0) or die "sysseek failed: $!"; - syswrite($fh, $pageheader) or die "syswrite failed: $!"; + sysseek($fh, $page_offset, 0) or croak "sysseek failed: $!"; + syswrite($fh, $pageheader) or croak "syswrite failed: $!"; close $fh; return; @@ -3734,7 +3887,7 @@ sub log_standby_snapshot SELECT restart_lsn IS NOT NULL FROM pg_catalog.pg_replication_slots WHERE slot_name = '$slot_name' ]) - or die + or croak "timed out waiting for logical slot to calculate its restart_lsn"; # Then arrange for the xl_running_xacts record for which the standby is @@ -3776,7 +3929,7 @@ sub create_logical_slot_on_standby is($self->slot($slot_name)->{'slot_type'}, 'logical', $slot_name . ' on standby created') - or die "could not create slot" . $slot_name; + or croak "could not create slot" . $slot_name; } =pod @@ -3807,7 +3960,7 @@ sub validate_slot_inactive_since ), 't', "last inactive time for slot $slot_name is valid on node $name") - or die "could not validate captured inactive_since for slot $slot_name"; + or croak "could not validate captured inactive_since for slot $slot_name"; return $inactive_since; } @@ -3837,6 +3990,42 @@ sub advance_wal } } +=item $node->checksum_enable_offline() + +Enable data page checksums in an offline cluster with B. The +caller is responsible for ensuring that the cluster is in the right state for +this operation. + +=cut + +sub checksum_enable_offline +{ + my ($self) = @_; + + print "# Enabling checksums in \"$self->data_dir\"\n"; + PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D', + $self->data_dir, '-e'); + return; +} + +=item $node->checksum_disable_offline() + +Disable data page checksums in an offline cluster with B. The +caller is responsible for ensuring that the cluster is in the right state for +this operation. + +=cut + +sub checksum_disable_offline +{ + my ($self) = @_; + + print "# Disabling checksums in \"$self->data_dir\"\n"; + PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D', + $self->data_dir, '-d'); + return; +} + =pod =back diff --git a/src/test/perl/PostgreSQL/Test/Kerberos.pm b/src/test/perl/PostgreSQL/Test/Kerberos.pm index b72dd2fbaf414..e861d93533e6a 100644 --- a/src/test/perl/PostgreSQL/Test/Kerberos.pm +++ b/src/test/perl/PostgreSQL/Test/Kerberos.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Sets up a stand-alone KDC for testing PostgreSQL GSSAPI / Kerberos # functionality. @@ -9,6 +9,7 @@ package PostgreSQL::Test::Kerberos; use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Utils; +use Test::More; our ( $krb5_bin_dir, $krb5_sbin_dir, $krb5_config, $kinit, diff --git a/src/test/perl/PostgreSQL/Test/RecursiveCopy.pm b/src/test/perl/PostgreSQL/Test/RecursiveCopy.pm index 3363aeef87005..17b801da5b7ca 100644 --- a/src/test/perl/PostgreSQL/Test/RecursiveCopy.pm +++ b/src/test/perl/PostgreSQL/Test/RecursiveCopy.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group =pod diff --git a/src/test/perl/PostgreSQL/Test/SimpleTee.pm b/src/test/perl/PostgreSQL/Test/SimpleTee.pm index 52bdae4180679..3c1ca73375ad9 100644 --- a/src/test/perl/PostgreSQL/Test/SimpleTee.pm +++ b/src/test/perl/PostgreSQL/Test/SimpleTee.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # A simple 'tee' implementation, using perl tie. # diff --git a/src/test/perl/PostgreSQL/Test/Utils.pm b/src/test/perl/PostgreSQL/Test/Utils.pm index 7d7ca83495f49..81dbcab8257c9 100644 --- a/src/test/perl/PostgreSQL/Test/Utils.pm +++ b/src/test/perl/PostgreSQL/Test/Utils.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group =pod @@ -58,20 +58,27 @@ use File::Temp (); use IPC::Run; use POSIX qw(locale_h); use PostgreSQL::Test::SimpleTee; +use Time::HiRes qw(usleep); # We need a version of Test::More recent enough to support subtests use Test::More 0.98; +# When Utils functions are called via Cluster.pm wrappers, croak() should +# skip both packages and report the caller in the test script. +our @CARP_NOT = qw(PostgreSQL::Test::Cluster); + our @EXPORT = qw( generate_ascii_string slurp_dir slurp_file append_to_file string_replace_file + read_head_tail check_mode_recursive chmod_recursive check_pg_config compare_files + wait_for_file dir_symlink scan_server_header system_or_bail @@ -92,6 +99,8 @@ our @EXPORT = qw( command_ok_or_fails_like command_checks_all + tar_portability_options + $windows_os $is_msys2 $use_unix_sockets @@ -108,6 +117,7 @@ BEGIN delete $ENV{LANGUAGE}; delete $ENV{LC_ALL}; $ENV{LC_MESSAGES} = 'C'; + $ENV{LC_NUMERIC} = 'C'; setlocale(LC_ALL, ""); # This list should be kept in sync with pg_regress.c. @@ -240,6 +250,24 @@ INIT autoflush STDOUT 1; autoflush STDERR 1; autoflush $testlog 1; + + # Because of the above redirection the tap output wouldn't contain + # information about tests failing due to die etc. Fix that by also + # printing the failure to the original stderr. + $SIG{__DIE__} = sub { + # Ignore dies because of syntax errors, those will be displayed + # correctly anyway. + return if !defined $^S; + + # Ignore dies inside evals + return if $^S == 1; + + diag("die: $_[0]"); + # Also call done_testing() to avoid the confusing "no plan was declared" + # message in TAP output when a test dies. + eval { done_testing(); } + }; + } END @@ -589,6 +617,55 @@ sub string_replace_file =pod +=item read_head_tail(filename) + +Return lines from the head and the tail of a file. If the file is smaller +than the number of lines requested, all its contents are returned in @head, +leaving @tail empty. + +If the PG_TEST_FILE_READ_LINES environment variable is set, use it instead +of the default of 50 lines. + +=cut + +sub read_head_tail +{ + my $filename = shift; + my (@head, @tail); + my $line_count = 50; + + # Use PG_TEST_FILE_READ_LINES if set. + if (defined $ENV{PG_TEST_FILE_READ_LINES}) + { + $line_count = $ENV{PG_TEST_FILE_READ_LINES}; + } + + return ([], []) if $line_count <= 0; + + open my $fh, '<', $filename or croak "couldn't open file: $filename\n"; + my @lines = <$fh>; + close $fh; + + chomp @lines; + + my $total = scalar @lines; + + # If the file is small enough, return all lines in @head. + if (2 * $line_count >= $total) + { + @head = @lines; + @tail = (); + return (\@head, \@tail); + } + + @head = @lines[ 0 .. $line_count - 1 ]; + @tail = @lines[ $total - $line_count .. $total - 1 ]; + + return (\@head, \@tail); +} + +=pod + =item check_mode_recursive(dir, expected_dir_mode, expected_file_mode, ignore_list) Check that all file/dir modes in a directory match the expected values, @@ -630,7 +707,7 @@ sub check_mode_recursive } else { - die $msg; + croak $msg; } } @@ -669,7 +746,7 @@ sub check_mode_recursive # Else something we can't handle else { - die "unknown file type for $File::Find::name"; + croak "unknown file type for $File::Find::name"; } } }, @@ -701,7 +778,7 @@ sub chmod_recursive chmod( S_ISDIR($file_stat->mode) ? $dir_mode : $file_mode, $File::Find::name - ) or die "unable to chmod $File::Find::name"; + ) or croak "unable to chmod $File::Find::name"; } } }, @@ -727,11 +804,11 @@ sub scan_server_header my $result = IPC::Run::run [ 'pg_config', '--includedir-server' ], '>' => \$stdout, '2>' => \$stderr - or die "could not execute pg_config"; + or croak "could not execute pg_config"; chomp($stdout); $stdout =~ s/\r$//; - open my $header_h, '<', "$stdout/$header_path" or die "$!"; + open my $header_h, '<', "$stdout/$header_path" or croak "$!"; my @match = undef; while (<$header_h>) @@ -745,7 +822,7 @@ sub scan_server_header } close $header_h; - die "could not find match in header $header_path\n" + croak "could not find match in header $header_path\n" unless @match; return @match; } @@ -766,11 +843,11 @@ sub check_pg_config my $result = IPC::Run::run [ 'pg_config', '--includedir' ], '>' => \$stdout, '2>' => \$stderr - or die "could not execute pg_config"; + or croak "could not execute pg_config"; chomp($stdout); $stdout =~ s/\r$//; - open my $pg_config_h, '<', "$stdout/pg_config.h" or die "$!"; + open my $pg_config_h, '<', "$stdout/pg_config.h" or croak "$!"; my $match = (grep { /^$regexp/ } <$pg_config_h>); close $pg_config_h; return $match; @@ -817,6 +894,43 @@ sub compare_files =pod +=item wait_for_file(filename, regexp[, offset]) + +Waits for the contents of the specified file, starting at the given offset, to +match the supplied regular expression. Checks the entire file if no offset is +given. Times out after $timeout_default seconds. + +If successful, returns the length of the entire file, in bytes. + +=cut + +sub wait_for_file +{ + my ($filename, $regexp, $offset) = @_; + $offset = 0 unless defined $offset; + + my $max_attempts = 10 * $timeout_default; + my $attempts = 0; + + while ($attempts < $max_attempts) + { + if (-e $filename) + { + my $contents = slurp_file($filename, $offset); + return $offset + length($contents) if ($contents =~ m/$regexp/); + } + + # Wait 0.1 second before retrying. + usleep(100_000); + + $attempts++; + } + + croak "timed out waiting for file $filename contents to match: $regexp"; +} + +=pod + =item dir_symlink(oldname, newname) Portably create a symlink for a directory. On Windows this creates a junction @@ -838,13 +952,42 @@ sub dir_symlink # need some indirection on msys $cmd = qq{echo '$cmd' | \$COMSPEC /Q}; } - system($cmd) == 0 or die; + system($cmd) == 0 or croak; } else { - symlink $oldname, $newname or die $!; + symlink $oldname, $newname or croak $!; } - die "No $newname" unless -e $newname; + croak "No $newname" unless -e $newname; +} + +# Log command output. Truncates to first/last 30 lines if over 60 lines. +sub _diag_command_output +{ + my ($cmd, $stdout, $stderr) = @_; + + diag(join(" ", @$cmd)); + + for my $channel (['stdout', $stdout], ['stderr', $stderr]) + { + my ($name, $output) = @$channel; + next unless $output; + + diag("-------------- $name --------------"); + my @lines = split /\n/, $output; + if (@lines > 60) + { + diag(join("\n", @lines[0 .. 29])); + diag("... " . (@lines - 60) . " lines omitted ..."); + diag(join("\n", @lines[-30 .. -1])); + } + else + { + diag($output); + } + } + + diag("------------------------------------"); } =pod @@ -857,7 +1000,7 @@ sub dir_symlink =item command_ok(cmd, test_name) -Check that the command runs (via C) successfully. +Check that the command runs successfully. =cut @@ -865,8 +1008,14 @@ sub command_ok { local $Test::Builder::Level = $Test::Builder::Level + 1; my ($cmd, $test_name) = @_; - my $result = run_log($cmd); - ok($result, $test_name); + my ($stdout, $stderr); + print("# Running: " . join(" ", @{$cmd}) . "\n"); + my $result = IPC::Run::run $cmd, '>' => \$stdout, '2>' => \$stderr; + ok($result, $test_name) or do + { + diag("---------- command failed ----------"); + _diag_command_output($cmd, $stdout, $stderr); + }; return; } @@ -874,7 +1023,7 @@ sub command_ok =item command_fails(cmd, test_name) -Check that the command fails (when run via C). +Check that the command fails. =cut @@ -882,8 +1031,14 @@ sub command_fails { local $Test::Builder::Level = $Test::Builder::Level + 1; my ($cmd, $test_name) = @_; - my $result = run_log($cmd); - ok(!$result, $test_name); + my ($stdout, $stderr); + print("# Running: " . join(" ", @{$cmd}) . "\n"); + my $result = IPC::Run::run $cmd, '>' => \$stdout, '2>' => \$stderr; + ok(!$result, $test_name) or do + { + diag("-- command succeeded unexpectedly --"); + _diag_command_output($cmd, $stdout, $stderr); + }; return; } @@ -1126,7 +1281,7 @@ sub command_checks_all # See http://perldoc.perl.org/perlvar.html#%24CHILD_ERROR my $ret = $?; - die "command exited with signal " . ($ret & 127) + croak "command exited with signal " . ($ret & 127) if $ret & 127; $ret = $ret >> 8; @@ -1151,6 +1306,51 @@ sub command_checks_all =pod +=item tar_portability_options(tar) + +Check for non-default options we need to give to tar to create +a tarfile we can decode (i.e., no "pax" extensions). +Not needed in tests that only use tar to read tarfiles. + +Returns options as an array. + +=cut + +sub tar_portability_options +{ + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ($tar) = @_; + + my @tar_p_flags = (); + + return @tar_p_flags if (!defined $tar || $tar eq ''); + + # GNU tar typically produces gnu-format archives, which we can read fine. + # But some platforms configure it to default to posix/pax format, and + # apparently they enable --sparse too. Override that. + # + # ustar format supports UIDs only up to 2^21 - 1 (2097151). Override + # owner/group to avoid failures on systems where the running user's UID/GID + # exceeds that limit. + my $devnull = File::Spec->devnull(); + if (system( + "$tar --format=ustar --owner=0 --group=0 -cf $devnull $devnull 2>$devnull" + ) == 0) + { + # GNU tar (Linux), BSD tar (FreeBSD, NetBSD, macOS, Windows) + push(@tar_p_flags, "--format=ustar", "--owner=0", "--group=0"); + } + elsif (system("$tar -F ustar -cf $devnull $devnull 2>$devnull") == 0) + { + # OpenBSD tar + push(@tar_p_flags, "-F", "ustar"); + } + return @tar_p_flags; +} + +=pod + =back =cut diff --git a/src/test/perl/PostgreSQL/Version.pm b/src/test/perl/PostgreSQL/Version.pm index d8625bc9ef2d5..372a96020c64e 100644 --- a/src/test/perl/PostgreSQL/Version.pm +++ b/src/test/perl/PostgreSQL/Version.pm @@ -4,7 +4,7 @@ # # Module encapsulating Postgres Version numbers # -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # ############################################################################ diff --git a/src/test/perl/meson.build b/src/test/perl/meson.build index e9726d54283bf..0fd36c9e57002 100644 --- a/src/test/perl/meson.build +++ b/src/test/perl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # could use install_data's preserve_path option in >=0.64.0 diff --git a/src/test/postmaster/Makefile b/src/test/postmaster/Makefile index e06b81f8c47fc..d86fe222c46a1 100644 --- a/src/test/postmaster/Makefile +++ b/src/test/postmaster/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/postmaster # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/postmaster/Makefile diff --git a/src/test/postmaster/meson.build b/src/test/postmaster/meson.build index 0ab32b42f2e0e..d2709867da71c 100644 --- a/src/test/postmaster/meson.build +++ b/src/test/postmaster/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'postmaster', diff --git a/src/test/postmaster/t/001_basic.pl b/src/test/postmaster/t/001_basic.pl index fb3984bb287ea..8b263e0f17704 100644 --- a/src/test/postmaster/t/001_basic.pl +++ b/src/test/postmaster/t/001_basic.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/postmaster/t/002_connection_limits.pl b/src/test/postmaster/t/002_connection_limits.pl index 6442500fc379a..8c67c4a86c719 100644 --- a/src/test/postmaster/t/002_connection_limits.pl +++ b/src/test/postmaster/t/002_connection_limits.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test connection limits, i.e. max_connections, reserved_connections # and superuser_reserved_connections. @@ -68,11 +68,17 @@ sub connect_fails_wait my $log_location = -s $node->logfile; $node->connect_fails($connstr, $test_name, %params); - $node->wait_for_log(qr/DEBUG: (00000: )?client backend.*exited with exit code 1/, + $node->wait_for_log( + qr/DEBUG: (00000: )?client backend.*exited with exit code 1/, $log_location); ok(1, "$test_name: client backend process exited"); } +# Restart the server to ensure that any backends launched for the +# initialization steps are gone. Otherwise they could still be using +# up connection slots and mess with our expectations. +$node->restart; + my @sessions = (); my @raw_connections = (); diff --git a/src/test/postmaster/t/003_start_stop.pl b/src/test/postmaster/t/003_start_stop.pl index 58e7ba6cc425e..e384518c47467 100644 --- a/src/test/postmaster/t/003_start_stop.pl +++ b/src/test/postmaster/t/003_start_stop.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test postmaster start and stop state machine. @@ -46,6 +46,11 @@ plan skip_all => "this test requires working raw_connect()"; } +# Restart the server to ensure that the backend launched for +# raw_connect_works() is gone. Otherwise, it might free up the +# connection slot later, when we expect all the slots to be in use. +$node->restart; + my @raw_connections = (); # Open a lot of TCP (or Unix domain socket) connections to use up all @@ -81,7 +86,7 @@ # clients already" instead of "role does not exist" error. Test that # to ensure that we have used up all the slots. $node->connect_fails("dbname=postgres user=invalid_user", - "connect ", + "connection is rejected when all slots are in use", expected_stderr => qr/FATAL: sorry, too many clients already/); # Open one more connection, to really ensure that we have at least one diff --git a/src/test/recovery/Makefile b/src/test/recovery/Makefile index f6e4bc8d1a7cd..d41aaaf8ae13d 100644 --- a/src/test/recovery/Makefile +++ b/src/test/recovery/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/recovery # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/recovery/Makefile diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build index cb983766c6793..36d789720a3c8 100644 --- a/src/test/recovery/meson.build +++ b/src/test/recovery/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'recovery', @@ -54,6 +54,13 @@ tests += { 't/043_no_contrecord_switch.pl', 't/044_invalidate_inactive_slots.pl', 't/045_archive_restartpoint.pl', + 't/046_checkpoint_logical_slot.pl', + 't/047_checkpoint_physical_slot.pl', + 't/048_vacuum_horizon_floor.pl', + 't/049_wait_for_lsn.pl', + 't/050_redo_segment_missing.pl', + 't/051_effective_wal_level.pl', + 't/052_checkpoint_segment_missing.pl', ], }, } diff --git a/src/test/recovery/t/001_stream_rep.pl b/src/test/recovery/t/001_stream_rep.pl index 2cbcc509d7665..a4fa4b96c61f3 100644 --- a/src/test/recovery/t/001_stream_rep.pl +++ b/src/test/recovery/t/001_stream_rep.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Minimal test testing streaming replication use strict; @@ -82,6 +82,11 @@ BEGIN print "standby 2: $result\n"; is($result, qq(1002), 'check streamed content on standby 2'); +$result = $node_standby_1->safe_psql('postgres', + "SELECT count(*) FROM pg_stat_recovery WHERE promote_triggered IS NOT NULL" +); +is($result, qq(1), 'check recovery state on standby 1'); + # Likewise, but for a sequence $node_primary->safe_psql('postgres', "CREATE SEQUENCE seq1; SELECT nextval('seq1')"); @@ -265,26 +270,26 @@ sub test_target_session_attrs 'postgres', 'SHOW ALL;', on_error_die => 1, extra_params => [ '--dbname' => $connstr_rep ]); -ok($ret == 0, "SHOW ALL with replication role and physical replication"); +is($ret, 0, "SHOW ALL with replication role and physical replication"); ($ret, $stdout, $stderr) = $node_primary->psql( 'postgres', 'SHOW ALL;', on_error_die => 1, extra_params => [ '--dbname' => $connstr_db ]); -ok($ret == 0, "SHOW ALL with replication role and logical replication"); +is($ret, 0, "SHOW ALL with replication role and logical replication"); # Test SHOW with a user-settable parameter ($ret, $stdout, $stderr) = $node_primary->psql( 'postgres', 'SHOW work_mem;', on_error_die => 1, extra_params => [ '--dbname' => $connstr_rep ]); -ok( $ret == 0, +is($ret, 0, "SHOW with user-settable parameter, replication role and physical replication" ); ($ret, $stdout, $stderr) = $node_primary->psql( 'postgres', 'SHOW work_mem;', on_error_die => 1, extra_params => [ '--dbname' => $connstr_db ]); -ok( $ret == 0, +is($ret, 0, "SHOW with user-settable parameter, replication role and logical replication" ); @@ -293,14 +298,14 @@ sub test_target_session_attrs 'postgres', 'SHOW primary_conninfo;', on_error_die => 1, extra_params => [ '--dbname' => $connstr_rep ]); -ok( $ret == 0, +is($ret, 0, "SHOW with superuser-settable parameter, replication role and physical replication" ); ($ret, $stdout, $stderr) = $node_primary->psql( 'postgres', 'SHOW primary_conninfo;', on_error_die => 1, extra_params => [ '--dbname' => $connstr_db ]); -ok( $ret == 0, +is($ret, 0, "SHOW with superuser-settable parameter, replication role and logical replication" ); @@ -312,7 +317,7 @@ sub test_target_session_attrs 'postgres', 'READ_REPLICATION_SLOT non_existent_slot;', extra_params => [ '--dbname' => $connstr_rep ]); -ok($ret == 0, "READ_REPLICATION_SLOT exit code 0 on success"); +is($ret, 0, "READ_REPLICATION_SLOT exit code 0 on success"); like($stdout, qr/^\|\|$/, "READ_REPLICATION_SLOT returns NULL values if slot does not exist"); @@ -325,7 +330,7 @@ sub test_target_session_attrs 'postgres', "READ_REPLICATION_SLOT $slotname;", extra_params => [ '--dbname' => $connstr_rep ]); -ok($ret == 0, "READ_REPLICATION_SLOT success with existing slot"); +is($ret, 0, "READ_REPLICATION_SLOT success with existing slot"); like($stdout, qr/^physical\|[^|]*\|1$/, "READ_REPLICATION_SLOT returns tuple with slot information"); @@ -577,7 +582,7 @@ sub replay_check "SELECT restart_lsn from pg_replication_slots WHERE slot_name = '$phys_slot';" ); chomp($phys_restart_lsn_post); -ok( ($phys_restart_lsn_pre cmp $phys_restart_lsn_post) == 0, +is($phys_restart_lsn_pre, $phys_restart_lsn_post, "physical slot advance persists across restarts"); # Check if the previous segment gets correctly recycled after the diff --git a/src/test/recovery/t/002_archiving.pl b/src/test/recovery/t/002_archiving.pl index 3acdb9ff1ebe7..aa40f58e6d647 100644 --- a/src/test/recovery/t/002_archiving.pl +++ b/src/test/recovery/t/002_archiving.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # test for archiving with hot standby use strict; @@ -115,6 +115,17 @@ recovery_end_command = 'echo recovery_end_failed > missing_dir/xyz.file' )); +# Create recovery.signal and confirm that both signal files exist. +# This is necessary to test how recovery behaves when both files are present, +# i.e., standby.signal should take precedence and both files should be +# removed at the end of recovery. +$node_standby2->set_recovery_mode(); +my $node_standby2_data = $node_standby2->data_dir; +ok(-f "$node_standby2_data/recovery.signal", + "recovery.signal is present at the beginning of recovery"); +ok(-f "$node_standby2_data/standby.signal", + "standby.signal is present at the beginning of recovery"); + $node_standby2->start; # Save the log location, to see the failure of recovery_end_command. @@ -126,7 +137,6 @@ # Check the logs of the standby to see that the commands have failed. my $log_contents = slurp_file($node_standby2->logfile, $log_location); -my $node_standby2_data = $node_standby2->data_dir; like( $log_contents, @@ -141,4 +151,10 @@ qr/WARNING:.*recovery_end_command/s, "recovery_end_command failure detected in logs after promotion"); +# Check that no signal files are present after promotion. +ok( !-f "$node_standby2_data/recovery.signal", + "recovery.signal was left behind after promotion"); +ok( !-f "$node_standby2_data/standby.signal", + "standby.signal was left behind after promotion"); + done_testing(); diff --git a/src/test/recovery/t/003_recovery_targets.pl b/src/test/recovery/t/003_recovery_targets.pl index 0ae2e98272709..047eb13293ae3 100644 --- a/src/test/recovery/t/003_recovery_targets.pl +++ b/src/test/recovery/t/003_recovery_targets.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for recovery targets: name, timestamp, XID use strict; @@ -155,7 +155,9 @@ sub test_recovery_standby ok(!$res, 'invalid recovery startup fails'); my $logfile = slurp_file($node_standby->logfile()); -ok($logfile =~ qr/multiple recovery targets specified/, +like( + $logfile, + qr/multiple recovery targets specified/, 'multiple conflicting settings'); # Check behavior when recovery ends before target is reached @@ -183,8 +185,53 @@ sub test_recovery_standby usleep(100_000); } $logfile = slurp_file($node_standby->logfile()); -ok( $logfile =~ - qr/FATAL: .* recovery ended before configured recovery target was reached/, +like( + $logfile, + qr/FATAL: .* recovery ended before configured recovery target was reached/, 'recovery end before target reached is a fatal error'); +# Invalid recovery_target_timeline tests +my ($result, $stdout, $stderr) = $node_primary->psql('postgres', + "ALTER SYSTEM SET recovery_target_timeline TO 'bogus'"); +like( + $stderr, + qr/is not a valid number/, + "invalid recovery_target_timeline (bogus value)"); + +($result, $stdout, $stderr) = $node_primary->psql('postgres', + "ALTER SYSTEM SET recovery_target_timeline TO '0'"); +like( + $stderr, + qr/must be between 1 and 4294967295/, + "invalid recovery_target_timeline (lower bound check)"); + +($result, $stdout, $stderr) = $node_primary->psql('postgres', + "ALTER SYSTEM SET recovery_target_timeline TO '4294967296'"); +like( + $stderr, + qr/must be between 1 and 4294967295/, + "invalid recovery_target_timeline (upper bound check)"); + +# Invalid recovery_target_xid tests +($result, $stdout, $stderr) = $node_primary->psql('postgres', + "ALTER SYSTEM SET recovery_target_xid TO 'bogus'"); +like( + $stderr, + qr/is not a valid number/, + "invalid recovery_target_xid (bogus value)"); + +($result, $stdout, $stderr) = $node_primary->psql('postgres', + "ALTER SYSTEM SET recovery_target_xid TO '-1'"); +like( + $stderr, + qr/is not a valid number/, + "invalid recovery_target_xid (negative)"); + +($result, $stdout, $stderr) = $node_primary->psql('postgres', + "ALTER SYSTEM SET recovery_target_xid TO '0'"); +like( + $stderr, + qr/without epoch must be greater than or equal to 3/, + "invalid recovery_target_xid (lower bound check)"); + done_testing(); diff --git a/src/test/recovery/t/004_timeline_switch.pl b/src/test/recovery/t/004_timeline_switch.pl index 9c8334cf278f8..5afd2f4446684 100644 --- a/src/test/recovery/t/004_timeline_switch.pl +++ b/src/test/recovery/t/004_timeline_switch.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for timeline switch use strict; @@ -34,11 +34,10 @@ $node_primary->safe_psql('postgres', "CREATE TABLE tab_int AS SELECT generate_series(1,1000) AS a"); -# Wait until standby has replayed enough data on standby 1 -$node_primary->wait_for_catchup($node_standby_1); - -# Stop and remove primary -$node_primary->teardown_node; +# Cleanly stop and remove primary. A clean stop is required so as all +# the records generated on the primary are received and flushed by the two +# standbys. +$node_primary->stop; # promote standby 1 using "pg_promote", switching it to a new timeline my $psql_out = ''; @@ -54,8 +53,19 @@ 'postgresql.conf', qq( primary_conninfo='$connstr_1' )); + +# Rotate logfile before restarting, for the log checks done below. +$node_standby_2->rotate_logfile; $node_standby_2->restart; +# Wait for walreceiver to reconnect after the restart. We want to +# verify that after reconnection, the walreceiver stays alive during +# the timeline switch. +$node_standby_2->poll_query_until('postgres', + "SELECT EXISTS(SELECT 1 FROM pg_stat_wal_receiver)"); +my $wr_pid_before_switch = $node_standby_2->safe_psql('postgres', + "SELECT pid FROM pg_stat_wal_receiver"); + # Insert some data in standby 1 and check its presence in standby 2 # to ensure that the timeline switch has been done. $node_standby_1->safe_psql('postgres', @@ -66,6 +76,22 @@ $node_standby_2->safe_psql('postgres', "SELECT count(*) FROM tab_int"); is($result, qq(2000), 'check content of standby 2'); +# Check the logs, WAL receiver should not have been stopped while +# transitioning to its new timeline. There is no need to rely on an +# offset in this check of the server logs: a new log file is used on +# node restart when primary_conninfo is updated above. +ok( !$node_standby_2->log_contains( + "FATAL: .* terminating walreceiver process due to administrator command" + ), + 'WAL receiver should not be stopped across timeline jumps'); + +# Verify that the walreceiver process stayed alive across the timeline +# switch, check its PID. +my $wr_pid_after_switch = $node_standby_2->safe_psql('postgres', + "SELECT pid FROM pg_stat_wal_receiver"); + +is($wr_pid_before_switch, $wr_pid_after_switch, + 'WAL receiver PID matches across timeline jumps'); # Ensure that a standby is able to follow a primary on a newer timeline # when WAL archiving is enabled. diff --git a/src/test/recovery/t/005_replay_delay.pl b/src/test/recovery/t/005_replay_delay.pl index be05fcea8a792..caec921e549cb 100644 --- a/src/test/recovery/t/005_replay_delay.pl +++ b/src/test/recovery/t/005_replay_delay.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Checks for recovery_min_apply_delay and recovery pause use strict; @@ -53,10 +53,8 @@ # This test is successful if and only if the LSN has been applied with at least # the configured apply delay. -ok(time() - $primary_insert_time >= $delay, - "standby applies WAL only after replication delay"); - - +cmp_ok(time() - $primary_insert_time, + '>=', $delay, "standby applies WAL only after replication delay"); # Check that recovery can be paused or resumed expectedly. my $node_standby2 = PostgreSQL::Test::Cluster->new('standby2'); $node_standby2->init_from_backup($node_primary, $backup_name, diff --git a/src/test/recovery/t/006_logical_decoding.pl b/src/test/recovery/t/006_logical_decoding.pl index 2137c4e5e305f..97d11f98b59a4 100644 --- a/src/test/recovery/t/006_logical_decoding.pl +++ b/src/test/recovery/t/006_logical_decoding.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Testing of logical decoding using SQL interface and/or pg_recvlogical # @@ -21,7 +21,6 @@ wal_level = logical )); $node_primary->start; -my $backup_name = 'primary_backup'; $node_primary->safe_psql('postgres', qq[CREATE TABLE decoding_test(x integer, y text);]); @@ -35,8 +34,9 @@ 'template1', qq[START_REPLICATION SLOT test_slot LOGICAL 0/0], replication => 'database'); -ok( $stderr =~ - m/replication slot "test_slot" was not created in this database/, +like( + $stderr, + qr/replication slot "test_slot" was not created in this database/, "Logical decoding correctly fails to start"); ($result, $stdout, $stderr) = $node_primary->psql( @@ -54,7 +54,9 @@ 'template1', qq[START_REPLICATION SLOT s1 LOGICAL 0/1], replication => 'true'); -ok($stderr =~ /ERROR: logical decoding requires a database connection/, +like( + $stderr, + qr/ERROR: logical decoding requires a database connection/, "Logical decoding fails on non-database connection"); $node_primary->safe_psql('postgres', @@ -201,7 +203,7 @@ "SELECT restart_lsn from pg_replication_slots WHERE slot_name = '$logical_slot';" ); chomp($logical_restart_lsn_post); -ok(($logical_restart_lsn_pre cmp $logical_restart_lsn_post) == 0, +is($logical_restart_lsn_pre, $logical_restart_lsn_post, "logical slot advance persists across restarts"); my $stats_test_slot1 = 'test_slot'; diff --git a/src/test/recovery/t/007_sync_rep.pl b/src/test/recovery/t/007_sync_rep.pl index 602652a76386d..8eab64998cd2b 100644 --- a/src/test/recovery/t/007_sync_rep.pl +++ b/src/test/recovery/t/007_sync_rep.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Minimal test testing synchronous replication sync_state transition use strict; diff --git a/src/test/recovery/t/008_fsm_truncation.pl b/src/test/recovery/t/008_fsm_truncation.pl index 37e6c55f3a165..5c49989eabf20 100644 --- a/src/test/recovery/t/008_fsm_truncation.pl +++ b/src/test/recovery/t/008_fsm_truncation.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test FSM-driven INSERT just after truncation clears FSM slots indicating # free space in removed blocks. diff --git a/src/test/recovery/t/009_twophase.pl b/src/test/recovery/t/009_twophase.pl index 1a662ebe499dc..aa73d3e106c5e 100644 --- a/src/test/recovery/t/009_twophase.pl +++ b/src/test/recovery/t/009_twophase.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests dedicated to two-phase commit in recovery use strict; @@ -222,7 +222,7 @@ sub configure_and_reload SAVEPOINT s1; INSERT INTO t_009_tbl VALUES (22, 'issued to ${cur_primary_name}'); PREPARE TRANSACTION 'xact_009_10';"); -$cur_primary->teardown_node; +$cur_primary->stop; $cur_standby->promote; # change roles diff --git a/src/test/recovery/t/010_logical_decoding_timelines.pl b/src/test/recovery/t/010_logical_decoding_timelines.pl index 351a9ef7bddd4..2eb72e9dd9779 100644 --- a/src/test/recovery/t/010_logical_decoding_timelines.pl +++ b/src/test/recovery/t/010_logical_decoding_timelines.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Demonstrate that logical can follow timeline switches. # diff --git a/src/test/recovery/t/012_subtransactions.pl b/src/test/recovery/t/012_subtransactions.pl index 25df73463b795..d4680e638bfae 100644 --- a/src/test/recovery/t/012_subtransactions.pl +++ b/src/test/recovery/t/012_subtransactions.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests dedicated to subtransactions in recovery use strict; diff --git a/src/test/recovery/t/013_crash_restart.pl b/src/test/recovery/t/013_crash_restart.pl index debfa635c36fe..56afb1aa6ebb5 100644 --- a/src/test/recovery/t/013_crash_restart.pl +++ b/src/test/recovery/t/013_crash_restart.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Tests restarts of postgres due to crashes of a subprocess. @@ -21,14 +21,32 @@ my $node = PostgreSQL::Test::Cluster->new('primary'); $node->init(allows_streaming => 1); + +# Enable pg_stat_statements to test restart of shared_preload_libraries. +$node->append_conf( + 'postgresql.conf', + qq{shared_preload_libraries = 'pg_stat_statements' +pg_stat_statements.max = 50000 +compute_query_id = 'regress' +}); + $node->start(); # by default PostgreSQL::Test::Cluster doesn't restart after a crash $node->safe_psql( - 'postgres', - q[ALTER SYSTEM SET restart_after_crash = 1; - ALTER SYSTEM SET log_connections = receipt; - SELECT pg_reload_conf();]); + 'postgres', q[ + ALTER SYSTEM SET restart_after_crash = 1; + ALTER SYSTEM SET log_connections = receipt; + SELECT pg_reload_conf(); + ]); + +# Remember the time that pg_stat_statements was reset. We'll use it later to +# verify that it gets re-initialized after crash. +my $stats_reset = $node->safe_psql( + 'postgres', q[ + CREATE EXTENSION pg_stat_statements; + SELECT stats_reset FROM pg_stat_statements_info; + ]); # Run psql, keeping session alive, so we have an alive backend to kill. my ($killme_stdin, $killme_stdout, $killme_stderr) = ('', '', ''); @@ -141,6 +159,13 @@ ($monitor_stdin, $monitor_stdout, $monitor_stderr) = ('', '', ''); $monitor->run(); +# Verify that pg_stat_statements, loaded via shared_preload_libraries, +# was re-initialized at the crash. +my $stats_reset_after = $node->safe_psql('postgres', + q[SELECT stats_reset FROM pg_stat_statements_info]); +cmp_ok($stats_reset, 'ne', $stats_reset_after, + "pg_stat_statements was reset by restart"); + # Acquire pid of new backend $killme_stdin .= q[ @@ -228,6 +253,13 @@ 'before-orderly-restart', 'can still write after crash restart'); +# Confirm that the logical replication launcher, a background worker +# without the never-restart flag, has also restarted successfully. +is($node->poll_query_until('postgres', + "SELECT count(*) = 1 FROM pg_stat_activity WHERE backend_type = 'logical replication launcher'"), + '1', + 'logical replication launcher restarted after crash'); + # Just to be sure, check that an orderly restart now still works $node->restart(); diff --git a/src/test/recovery/t/014_unlogged_reinit.pl b/src/test/recovery/t/014_unlogged_reinit.pl index c19242146f229..1d50f21fc6f14 100644 --- a/src/test/recovery/t/014_unlogged_reinit.pl +++ b/src/test/recovery/t/014_unlogged_reinit.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests that unlogged tables are properly reinitialized after a crash. # diff --git a/src/test/recovery/t/015_promotion_pages.pl b/src/test/recovery/t/015_promotion_pages.pl index ae0849256f04e..00e97507a13c5 100644 --- a/src/test/recovery/t/015_promotion_pages.pl +++ b/src/test/recovery/t/015_promotion_pages.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for promotion handling with WAL records generated post-promotion # before the first checkpoint is generated. This test case checks for diff --git a/src/test/recovery/t/016_min_consistency.pl b/src/test/recovery/t/016_min_consistency.pl index 9a3b4866fce4a..7bfe0e793af07 100644 --- a/src/test/recovery/t/016_min_consistency.pl +++ b/src/test/recovery/t/016_min_consistency.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for checking consistency of on-disk pages for a cluster with # the minimum recovery LSN, ensuring that the updates happen across @@ -39,7 +39,7 @@ sub find_largest_lsn defined($len) or die "read error on $filename: $!"; close($fh); - return sprintf("%X/%X", $max_hi, $max_lo); + return sprintf("%X/%08X", $max_hi, $max_lo); } # Initialize primary node diff --git a/src/test/recovery/t/017_shm.pl b/src/test/recovery/t/017_shm.pl index c73aa3f0c2c4a..86eeb32611776 100644 --- a/src/test/recovery/t/017_shm.pl +++ b/src/test/recovery/t/017_shm.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Tests of pg_shmem.h functions diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl index 4b3667b98a38b..8f25b5dd16560 100644 --- a/src/test/recovery/t/018_wal_optimize.pl +++ b/src/test/recovery/t/018_wal_optimize.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test WAL replay when some operation has skipped WAL. # diff --git a/src/test/recovery/t/019_replslot_limit.pl b/src/test/recovery/t/019_replslot_limit.pl index 6468784b83d6f..7b253e64d9ccb 100644 --- a/src/test/recovery/t/019_replslot_limit.pl +++ b/src/test/recovery/t/019_replslot_limit.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for replication slot limit # Ensure that max_slot_wal_keep_size limits the number of WAL files to diff --git a/src/test/recovery/t/020_archive_status.pl b/src/test/recovery/t/020_archive_status.pl index 5d1fd19124324..5bb8aa9ec17ee 100644 --- a/src/test/recovery/t/020_archive_status.pl +++ b/src/test/recovery/t/020_archive_status.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # Tests related to WAL archiving and recovery. @@ -245,7 +245,9 @@ $standby2->stop; my $logfile = slurp_file($standby2->logfile, $log_location); -ok( $logfile =~ qr/archiver process shutting down/, +like( + $logfile, + qr/archiver process shutting down/, 'check shutdown callback of shell archive module'); # Test that we can enter and leave backup mode without crashes diff --git a/src/test/recovery/t/021_row_visibility.pl b/src/test/recovery/t/021_row_visibility.pl index 42740745bfd9f..0a4d22b369825 100644 --- a/src/test/recovery/t/021_row_visibility.pl +++ b/src/test/recovery/t/021_row_visibility.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Checks that snapshots on standbys behave in a minimally reasonable # way. diff --git a/src/test/recovery/t/022_crash_temp_files.pl b/src/test/recovery/t/022_crash_temp_files.pl index 0b68860bd3e94..5de9b0fb0eba6 100644 --- a/src/test/recovery/t/022_crash_temp_files.pl +++ b/src/test/recovery/t/022_crash_temp_files.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test remove of temporary files after a crash. use strict; diff --git a/src/test/recovery/t/023_pitr_prepared_xact.pl b/src/test/recovery/t/023_pitr_prepared_xact.pl index 592b7bc013b64..4bd73af3c0a68 100644 --- a/src/test/recovery/t/023_pitr_prepared_xact.pl +++ b/src/test/recovery/t/023_pitr_prepared_xact.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for point-in-time recovery (PITR) with prepared transactions use strict; diff --git a/src/test/recovery/t/024_archive_recovery.pl b/src/test/recovery/t/024_archive_recovery.pl index b4527ec084325..722bf7334b428 100644 --- a/src/test/recovery/t/024_archive_recovery.pl +++ b/src/test/recovery/t/024_archive_recovery.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test for archive recovery of WAL generated with wal_level=minimal use strict; @@ -91,8 +91,9 @@ sub test_recovery_wal_level_minimal # Confirm that the archive recovery fails with an expected error my $logfile = slurp_file($recovery_node->logfile()); - ok( $logfile =~ - qr/FATAL: .* WAL was generated with "wal_level=minimal", cannot continue recovering/, + like( + $logfile, + qr/FATAL: .* WAL was generated with "wal_level=minimal", cannot continue recovering/, "$node_text ends with an error because it finds WAL generated with \"wal_level=minimal\"" ); } diff --git a/src/test/recovery/t/025_stuck_on_old_timeline.pl b/src/test/recovery/t/025_stuck_on_old_timeline.pl index f52ac9678e0e2..9da5fbef2057e 100644 --- a/src/test/recovery/t/025_stuck_on_old_timeline.pl +++ b/src/test/recovery/t/025_stuck_on_old_timeline.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Testing streaming replication where standby is promoted and a new cascading # standby (without WAL) is connected to the promoted standby. Both archiving diff --git a/src/test/recovery/t/026_overwrite_contrecord.pl b/src/test/recovery/t/026_overwrite_contrecord.pl index f408d4f69b6ef..82567ca6bfcb0 100644 --- a/src/test/recovery/t/026_overwrite_contrecord.pl +++ b/src/test/recovery/t/026_overwrite_contrecord.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for already-propagated WAL segments ending in incomplete WAL records. @@ -58,7 +58,7 @@ END $node->safe_psql('postgres', qq{SELECT pg_logical_emit_message(true, 'test 026', repeat('xyzxz', 123456))} ); -#$node->safe_psql('postgres', qq{create table foo ()}); + my $endfile = $node->safe_psql('postgres', 'SELECT pg_walfile_name(pg_current_wal_insert_lsn())'); ok($initfile ne $endfile, "$initfile differs from $endfile"); diff --git a/src/test/recovery/t/027_stream_regress.pl b/src/test/recovery/t/027_stream_regress.pl index 83def062d11e1..ae97729784943 100644 --- a/src/test/recovery/t/027_stream_regress.pl +++ b/src/test/recovery/t/027_stream_regress.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Run the standard regression tests with streaming replication use strict; @@ -68,31 +68,25 @@ # Run the regression tests against the primary. my $extra_opts = $ENV{EXTRA_REGRESS_OPTS} || ""; -my $rc = - system($ENV{PG_REGRESS} - . " $extra_opts " - . "--dlpath=\"$dlpath\" " - . "--bindir= " - . "--host=" - . $node_primary->host . " " - . "--port=" - . $node_primary->port . " " - . "--schedule=../regress/parallel_schedule " - . "--max-concurrent-tests=20 " - . "--inputdir=../regress " - . "--outputdir=\"$outputdir\""); -if ($rc != 0) -{ - # Dump out the regression diffs file, if there is one - my $diffs = "$outputdir/regression.diffs"; - if (-e $diffs) - { - print "=== dumping $diffs ===\n"; - print slurp_file($diffs); - print "=== EOF ===\n"; - } -} -is($rc, 0, 'regression tests pass'); +command_ok( + [ + $ENV{PG_REGRESS}, + split(' ', $extra_opts), + "--dlpath=$dlpath", + '--bindir=', + '--host=' . $node_primary->host, + '--port=' . $node_primary->port, + '--schedule=../regress/parallel_schedule', + '--max-concurrent-tests=20', + '--inputdir=../regress', + "--outputdir=$outputdir" + ], + 'regression tests pass'); + +my $primary_alive = $node_primary->is_alive; +my $standby_alive = $node_standby_1->is_alive; +is($primary_alive, 1, 'primary alive after regression test run'); +is($standby_alive, 1, 'standby alive after regression test run'); # Clobber all sequences with their next value, so that we don't have # differences between nodes due to caching. @@ -108,6 +102,7 @@ 'pg_dumpall', '--file' => $outputdir . '/primary.dump', '--no-sync', '--no-statistics', + '--restrict-key' => 'test', '--port' => $node_primary->port, '--no-unlogged-table-data', # if unlogged, standby has schema only ], @@ -117,6 +112,7 @@ 'pg_dumpall', '--file' => $outputdir . '/standby.dump', '--no-sync', '--no-statistics', + '--restrict-key' => 'test', '--port' => $node_standby_1->port, ], 'dump standby server'); @@ -136,6 +132,7 @@ '--schema' => 'pg_catalog', '--file' => $outputdir . '/catalogs_primary.dump', '--no-sync', + '--restrict-key' => 'test', '--port', $node_primary->port, '--no-unlogged-table-data', 'regression', @@ -147,6 +144,7 @@ '--schema' => 'pg_catalog', '--file' => $outputdir . '/catalogs_standby.dump', '--no-sync', + '--restrict-key' => 'test', '--port' => $node_standby_1->port, 'regression', ], diff --git a/src/test/recovery/t/028_pitr_timelines.pl b/src/test/recovery/t/028_pitr_timelines.pl index 184ce15c0b6f1..ae617f6195a86 100644 --- a/src/test/recovery/t/028_pitr_timelines.pl +++ b/src/test/recovery/t/028_pitr_timelines.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Test recovering to a point-in-time using WAL archive, such that the # target point is physically in a WAL segment with a higher TLI than diff --git a/src/test/recovery/t/029_stats_restart.pl b/src/test/recovery/t/029_stats_restart.pl index 021e2bf361ff8..cdc427dbc7805 100644 --- a/src/test/recovery/t/029_stats_restart.pl +++ b/src/test/recovery/t/029_stats_restart.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests statistics handling around restarts, including handling of crashes and # invalid stats files, as well as restoring stats after "normal" restarts. diff --git a/src/test/recovery/t/030_stats_cleanup_replica.pl b/src/test/recovery/t/030_stats_cleanup_replica.pl index 20629e80c34e3..a7cbe99930a49 100644 --- a/src/test/recovery/t/030_stats_cleanup_replica.pl +++ b/src/test/recovery/t/030_stats_cleanup_replica.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests that standbys: # - drop stats for objects when the those records are replayed diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl index 028b0b5f0e15d..7a740f69806d9 100644 --- a/src/test/recovery/t/031_recovery_conflict.pl +++ b/src/test/recovery/t/031_recovery_conflict.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test that connections to a hot standby are correctly canceled when a # recovery conflict is detected Also, test that statistics in diff --git a/src/test/recovery/t/032_relfilenode_reuse.pl b/src/test/recovery/t/032_relfilenode_reuse.pl index 0c44883cc34b1..d9e22e9bcaafb 100644 --- a/src/test/recovery/t/032_relfilenode_reuse.pl +++ b/src/test/recovery/t/032_relfilenode_reuse.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/recovery/t/033_replay_tsp_drops.pl b/src/test/recovery/t/033_replay_tsp_drops.pl index 56c7d6b2137e4..93c89550511e1 100644 --- a/src/test/recovery/t/033_replay_tsp_drops.pl +++ b/src/test/recovery/t/033_replay_tsp_drops.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test replay of tablespace/database creation/drop diff --git a/src/test/recovery/t/034_create_database.pl b/src/test/recovery/t/034_create_database.pl index 351d085e8ddf8..194d1837e23df 100644 --- a/src/test/recovery/t/034_create_database.pl +++ b/src/test/recovery/t/034_create_database.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Test WAL replay for CREATE DATABASE .. STRATEGY WAL_LOG. diff --git a/src/test/recovery/t/035_standby_logical_decoding.pl b/src/test/recovery/t/035_standby_logical_decoding.pl index 921813483e37c..d264a698ff631 100644 --- a/src/test/recovery/t/035_standby_logical_decoding.pl +++ b/src/test/recovery/t/035_standby_logical_decoding.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # logical decoding on standby : test logical decoding, # recovery conflict and standby promotion. @@ -8,6 +8,7 @@ use PostgreSQL::Test::Cluster; use PostgreSQL::Test::Utils; +use Time::HiRes qw(usleep); use Test::More; if ($ENV{enable_injection_points} ne 'yes') @@ -393,8 +394,9 @@ sub wait_until_vacuum_can_remove # Confirm that the server startup fails with an expected error my $logfile = slurp_file($node_standby->logfile()); -ok( $logfile =~ - qr/FATAL: .* logical replication slot ".*" exists on the standby, but "hot_standby" = "off"/, +like( + $logfile, + qr/FATAL: .* logical replication slot ".*" exists on the standby, but "hot_standby" = "off"/, "the standby ends with an error during startup because hot_standby was disabled" ); $node_standby->adjust_conf('postgresql.conf', 'hot_standby', 'on'); @@ -486,8 +488,9 @@ sub wait_until_vacuum_can_remove ($result, $stdout, $stderr) = $node_standby->psql('otherdb', "SELECT lsn FROM pg_logical_slot_peek_changes('behaves_ok_activeslot', NULL, NULL) ORDER BY lsn DESC LIMIT 1;" ); -ok( $stderr =~ - m/replication slot "behaves_ok_activeslot" was not created in this database/, +like( + $stderr, + qr/replication slot "behaves_ok_activeslot" was not created in this database/, "replaying logical slot from another database fails"); ################################################## @@ -619,11 +622,12 @@ sub wait_until_vacuum_can_remove 'postgres', qq[select pg_copy_logical_replication_slot('vacuum_full_inactiveslot', 'vacuum_full_inactiveslot_copy');], replication => 'database'); -ok( $stderr =~ - /ERROR: cannot copy invalidated replication slot "vacuum_full_inactiveslot"/, +like( + $stderr, + qr/ERROR: cannot copy invalidated replication slot "vacuum_full_inactiveslot"/, "invalidated slot cannot be copied"); -# Turn hot_standby_feedback back on +# Set hot_standby_feedback to on change_hot_standby_feedback_and_wait_for_xmins(1, 1); ################################################## @@ -754,12 +758,12 @@ sub wait_until_vacuum_can_remove # message should not be issued ok( !$node_standby->log_contains( - "invalidating obsolete slot \"no_conflict_inactiveslot\"", $logstart), + "invalidating obsolete replication slot \"no_conflict_inactiveslot\"", $logstart), 'inactiveslot slot invalidation is not logged with vacuum on conflict_test' ); ok( !$node_standby->log_contains( - "invalidating obsolete slot \"no_conflict_activeslot\"", $logstart), + "invalidating obsolete replication slot \"no_conflict_activeslot\"", $logstart), 'activeslot slot invalidation is not logged with vacuum on conflict_test' ); @@ -874,9 +878,10 @@ sub wait_until_vacuum_can_remove $handle = make_slot_active($node_standby, 'wal_level_', 0, \$stdout, \$stderr); -# We are not able to read from the slot as it requires wal_level >= logical on the primary server +# We are not able to read from the slot as it requires effective_wal_level >= logical on +# the primary server check_pg_recvlogical_stderr($handle, - "logical decoding on standby requires \"wal_level\" >= \"logical\" on the primary" + "logical decoding on standby requires \"effective_wal_level\" >= \"logical\" on the primary" ); # Restore primary wal_level diff --git a/src/test/recovery/t/036_truncated_dropped.pl b/src/test/recovery/t/036_truncated_dropped.pl index db2303ae771ad..bf7bba3996055 100644 --- a/src/test/recovery/t/036_truncated_dropped.pl +++ b/src/test/recovery/t/036_truncated_dropped.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests recovery scenarios where the files are shorter than in the common # cases, e.g. due to replaying WAL records of a relation that was subsequently diff --git a/src/test/recovery/t/037_invalid_database.pl b/src/test/recovery/t/037_invalid_database.pl index dc52c55c7af8d..a0947108700a1 100644 --- a/src/test/recovery/t/037_invalid_database.pl +++ b/src/test/recovery/t/037_invalid_database.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # Test we handle interrupted DROP DATABASE correctly. diff --git a/src/test/recovery/t/038_save_logical_slots_shutdown.pl b/src/test/recovery/t/038_save_logical_slots_shutdown.pl index a47b2d9230303..c0392d50460b2 100644 --- a/src/test/recovery/t/038_save_logical_slots_shutdown.pl +++ b/src/test/recovery/t/038_save_logical_slots_shutdown.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # Test logical replication slots are always flushed to disk during a shutdown # checkpoint. diff --git a/src/test/recovery/t/039_end_of_wal.pl b/src/test/recovery/t/039_end_of_wal.pl index 47f9bb15e0335..f46d089a0fbc1 100644 --- a/src/test/recovery/t/039_end_of_wal.pl +++ b/src/test/recovery/t/039_end_of_wal.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2025, PostgreSQL Global Development Group +# Copyright (c) 2023-2026, PostgreSQL Global Development Group # # Test detecting end-of-WAL conditions. This test suite generates # fake defective page and record headers to trigger various failure diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl index 9c8b49e942d88..47d64d05ad1a8 100644 --- a/src/test/recovery/t/040_standby_failover_slots_sync.pl +++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -100,8 +100,9 @@ # Disable failover for enabled subscription my ($result, $stdout, $stderr) = $subscriber1->psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 SET (failover = false)"); -ok( $stderr =~ - /ERROR: cannot set option "failover" for enabled subscription/, +like( + $stderr, + qr/ERROR: cannot set option "failover" for enabled subscription/, "altering failover is not allowed for enabled subscription"); ################################################## @@ -110,8 +111,9 @@ ($result, $stdout, $stderr) = $publisher->psql('postgres', "SELECT pg_sync_replication_slots();"); -ok( $stderr =~ - /ERROR: replication slots can only be synchronized to a standby server/, +like( + $stderr, + qr/ERROR: replication slots can only be synchronized to a standby server/, "cannot sync slots on a non-standby server"); ################################################## @@ -313,8 +315,9 @@ # Attempting to perform logical decoding on a synced slot should result in an error ($result, $stdout, $stderr) = $standby1->psql('postgres', "select * from pg_logical_slot_get_changes('lsub1_slot', NULL, NULL);"); -ok( $stderr =~ - /ERROR: cannot use replication slot "lsub1_slot" for logical decoding/, +like( + $stderr, + qr/ERROR: cannot use replication slot "lsub1_slot" for logical decoding/, "logical decoding is not allowed on synced slot"); # Attempting to alter a synced slot should result in an error @@ -322,13 +325,17 @@ 'postgres', qq[ALTER_REPLICATION_SLOT lsub1_slot (failover);], replication => 'database'); -ok($stderr =~ /ERROR: cannot alter replication slot "lsub1_slot"/, +like( + $stderr, + qr/ERROR: cannot alter replication slot "lsub1_slot"/, "synced slot on standby cannot be altered"); # Attempting to drop a synced slot should result in an error ($result, $stdout, $stderr) = $standby1->psql('postgres', "SELECT pg_drop_replication_slot('lsub1_slot');"); -ok($stderr =~ /ERROR: cannot drop replication slot "lsub1_slot"/, +like( + $stderr, + qr/ERROR: cannot drop replication slot "lsub1_slot"/, "synced slot on standby cannot be dropped"); ################################################## @@ -337,12 +344,25 @@ ################################################## $standby1->append_conf('postgresql.conf', "primary_conninfo = '$connstr_1'"); + +# Capture the log position before reload to check for walreceiver +# termination. +$log_offset = -s $standby1->logfile; + $standby1->reload; +# Wait for the walreceiver to be stopped and restarted after a configuration +# reload. When primary_conninfo changes, the walreceiver should be +# terminated and a new one spawned. +$standby1->wait_for_log( + qr/FATAL: .* terminating walreceiver process due to administrator command/, + $log_offset); + ($result, $stdout, $stderr) = $standby1->psql('postgres', "SELECT pg_sync_replication_slots();"); -ok( $stderr =~ - /ERROR: replication slot synchronization requires "dbname" to be specified in "primary_conninfo"/, +like( + $stderr, + qr/ERROR: replication slot synchronization requires "dbname" to be specified in "primary_conninfo"/, "cannot sync slots if dbname is not specified in primary_conninfo"); # Add the dbname back to the primary_conninfo for further tests @@ -379,8 +399,9 @@ ($result, $stdout, $stderr) = $cascading_standby->psql('postgres', "SELECT pg_sync_replication_slots();"); -ok( $stderr =~ - /ERROR: cannot synchronize replication slots from a standby server/, +like( + $stderr, + qr/ERROR: cannot synchronize replication slots from a standby server/, "cannot sync slots to a cascading standby server"); $cascading_standby->stop; @@ -941,8 +962,7 @@ 'synced slot retained on the new primary'); # Commit the prepared transaction -$standby1->safe_psql('postgres', - "COMMIT PREPARED 'test_twophase_slotsync';"); +$standby1->safe_psql('postgres', "COMMIT PREPARED 'test_twophase_slotsync';"); $standby1->wait_for_catchup('regress_mysub1'); # Confirm that the prepared transaction is replicated to the subscriber @@ -967,4 +987,119 @@ is($result, '1', "data can be consumed using snap_test_slot"); +################################################## +# Remove any unnecessary replication slots and clear pending transactions on the +# primary server to ensure a clean environment. +################################################## + +$primary->psql( + 'postgres', qq( + SELECT pg_drop_replication_slot('sb1_slot'); + SELECT pg_drop_replication_slot('lsub1_slot'); + SELECT pg_drop_replication_slot('snap_test_slot'); +)); + +$subscriber2->safe_psql('postgres', 'DROP SUBSCRIPTION regress_mysub2;'); +$subscriber1->safe_psql('postgres', 'DROP SUBSCRIPTION regress_mysub1;'); +$subscriber1->safe_psql('postgres', 'TRUNCATE tab_int;'); + +# Remove the dropped sb1_slot from the synchronized_standby_slots list and reload the +# configuration. +$primary->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''"); +$primary->reload; + +# Verify that all slots have been removed except the one necessary for standby2, +# which is needed for further testing. +is( $primary->safe_psql( + 'postgres', + q{SELECT count(*) = 0 FROM pg_replication_slots WHERE slot_name != 'sb2_slot';} + ), + "t", + 'all replication slots have been dropped except the physical slot used by standby2' +); + +# Commit the pending prepared transaction +$primary->safe_psql('postgres', "COMMIT PREPARED 'test_twophase_slotsync';"); +$primary->wait_for_replay_catchup($standby2); + +################################################## +# Test that pg_sync_replication_slots() on the standby skips and retries +# until the slot becomes sync-ready (when the remote slot catches up with +# the locally reserved position). +# Also verify that slotsync skip statistics are correctly updated when the +# slotsync operation is skipped. +################################################## + +# Recreate the slot by creating a subscription on the subscriber, keep it disabled. +$subscriber1->safe_psql('postgres', qq[ + CREATE TABLE push_wal (a int); + CREATE SUBSCRIPTION regress_mysub1 CONNECTION '$publisher_connstr' PUBLICATION regress_mypub WITH (slot_name = lsub1_slot, failover = true, enabled = false);]); + +# Create some DDL on the primary so that the slot lags behind the standby +$primary->safe_psql('postgres', "CREATE TABLE push_wal (a int);"); + +# Make sure the DDL changes are synced to the standby +$primary->wait_for_replay_catchup($standby2); + +$log_offset = -s $standby2->logfile; + +# Enable standby for slot synchronization +$standby2->append_conf( + 'postgresql.conf', qq( +hot_standby_feedback = on +primary_conninfo = '$connstr_1 dbname=postgres' +log_min_messages = 'debug2' +)); + +$standby2->reload; + +# Attempt to synchronize slots using API. The API will continue retrying +# synchronization until the remote slot catches up. +# The API will not return until this happens, to be able to make +# further calls, call the API in a background process. +my $h = $standby2->background_psql('postgres', on_error_stop => 0); + +$h->query_until(qr/start/, q( + \echo start + SELECT pg_sync_replication_slots(); + )); + +# Confirm that the slot sync is skipped due to the remote slot lagging behind +$standby2->wait_for_log( + qr/could not synchronize replication slot \"lsub1_slot\"/, $log_offset); + +# Confirm that the slotsync skip reason is updated +$result = $standby2->safe_psql('postgres', + "SELECT slotsync_skip_reason FROM pg_replication_slots WHERE slot_name = 'lsub1_slot'" +); +is($result, 'wal_or_rows_removed', "check slot sync skip reason"); + +# Confirm that the slotsync skip statistics is updated +$result = $standby2->safe_psql('postgres', + "SELECT slotsync_skip_count > 0 FROM pg_stat_replication_slots WHERE slot_name = 'lsub1_slot'" +); +is($result, 't', "check slot sync skip count increments"); + +# Configure primary to disallow any logical slots that have enabled failover +# from getting ahead of the specified physical replication slot (sb2_slot). +$primary->append_conf( + 'postgresql.conf', qq( +synchronized_standby_slots = 'sb2_slot' +)); +$primary->reload; + +# Enable the Subscription, so that the remote slot catches up +$subscriber1->safe_psql('postgres', "ALTER SUBSCRIPTION regress_mysub1 ENABLE"); +$subscriber1->wait_for_subscription_sync; + +# Create xl_running_xacts on the primary to speed up restart_lsn advancement. +$primary->safe_psql('postgres', "SELECT pg_log_standby_snapshot();"); + +# Confirm from the log that the slot is sync-ready now. +$standby2->wait_for_log( + qr/newly created replication slot \"lsub1_slot\" is sync-ready now/, + $log_offset); + +$h->quit; + done_testing(); diff --git a/src/test/recovery/t/041_checkpoint_at_promote.pl b/src/test/recovery/t/041_checkpoint_at_promote.pl index cb63ac8d5c9b0..d0783fef9aec7 100644 --- a/src/test/recovery/t/041_checkpoint_at_promote.pl +++ b/src/test/recovery/t/041_checkpoint_at_promote.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -91,7 +91,7 @@ # Check the logs that the restart point has started on standby. This is # optional, but let's be sure. ok( $node_standby->log_contains( - "restartpoint starting: immediate wait", $logstart), + "restartpoint starting: fast wait", $logstart), "restartpoint has started"); # Trigger promotion during the restart point. diff --git a/src/test/recovery/t/042_low_level_backup.pl b/src/test/recovery/t/042_low_level_backup.pl index 5749a1df53303..df4ae029fe645 100644 --- a/src/test/recovery/t/042_low_level_backup.pl +++ b/src/test/recovery/t/042_low_level_backup.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Test low-level backup method by using pg_backup_start() and pg_backup_stop() # to create backups. @@ -105,8 +105,8 @@ $node_replica->start; -ok($node_replica->safe_psql('postgres', $canary_query) == 0, - 'canary is missing'); +is($node_replica->safe_psql('postgres', $canary_query), + '0', 'canary is missing'); # Check log to ensure that crash recovery was used as there is no # backup_label. @@ -134,8 +134,8 @@ has_restoring => 1); $node_replica->start; -ok($node_replica->safe_psql('postgres', $canary_query) == 1, - 'canary is present'); +is($node_replica->safe_psql('postgres', $canary_query), + '1', 'canary is present'); # Check log to ensure that backup_label was used for recovery. ok($node_replica->log_contains('starting backup recovery with redo LSN'), diff --git a/src/test/recovery/t/043_no_contrecord_switch.pl b/src/test/recovery/t/043_no_contrecord_switch.pl index ead41e6f40bed..6fab73a064a5d 100644 --- a/src/test/recovery/t/043_no_contrecord_switch.pl +++ b/src/test/recovery/t/043_no_contrecord_switch.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for already-propagated WAL segments ending in incomplete WAL records. diff --git a/src/test/recovery/t/044_invalidate_inactive_slots.pl b/src/test/recovery/t/044_invalidate_inactive_slots.pl index ccace14b4dd80..29da1729f31eb 100644 --- a/src/test/recovery/t/044_invalidate_inactive_slots.pl +++ b/src/test/recovery/t/044_invalidate_inactive_slots.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group # Test for replication slots invalidation due to idle_timeout use strict; @@ -94,7 +94,9 @@ sub wait_for_slot_invalidation 'postgres', qq[ SELECT pg_replication_slot_advance('logical_slot', '0/1'); ]); -ok( $stderr =~ /can no longer access replication slot "logical_slot"/, +like( + $stderr, + qr/can no longer access replication slot "logical_slot"/, "detected error upon trying to acquire invalidated slot on node") or die "could not detect error upon trying to acquire invalidated slot \"logical_slot\" on node"; diff --git a/src/test/recovery/t/045_archive_restartpoint.pl b/src/test/recovery/t/045_archive_restartpoint.pl index b143bc4e1d4e7..82946d2b56e29 100644 --- a/src/test/recovery/t/045_archive_restartpoint.pl +++ b/src/test/recovery/t/045_archive_restartpoint.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Test restartpoints during archive recovery. use strict; diff --git a/src/test/recovery/t/046_checkpoint_logical_slot.pl b/src/test/recovery/t/046_checkpoint_logical_slot.pl new file mode 100644 index 0000000000000..1ecb47a8b5817 --- /dev/null +++ b/src/test/recovery/t/046_checkpoint_logical_slot.pl @@ -0,0 +1,228 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# This test verifies the case when the logical slot is advanced during +# checkpoint. The test checks that the logical slot's restart_lsn still refers +# to an existed WAL segment after immediate restart. +# +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; + +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my ($node, $result); + +$node = PostgreSQL::Test::Cluster->new('mike'); +$node->init(allows_streaming => 'logical'); +$node->start; + +# Check if the extension injection_points is available, as it may be +# possible that this script is run with installcheck, where the module +# would not be installed by default. +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', q(CREATE EXTENSION injection_points)); + +# Create the two slots we'll need. +$node->safe_psql('postgres', + q{select pg_create_logical_replication_slot('slot_logical', 'test_decoding')} +); +$node->safe_psql('postgres', + q{select pg_create_physical_replication_slot('slot_physical', true)}); + +# Advance both slots to the current position just to have everything "valid". +$node->safe_psql('postgres', + q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null)} +); +$node->safe_psql('postgres', + q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())} +); + +# Run checkpoint to flush current state to disk and set a baseline. +$node->safe_psql('postgres', q{checkpoint}); + +# Generate some transactions to get RUNNING_XACTS. +my $xacts = $node->background_psql('postgres'); +$xacts->query_until( + qr/run_xacts/, + q(\echo run_xacts +SELECT 1 \watch 0.1 +\q +)); + +$node->advance_wal(20); + +# Run another checkpoint to set a new restore LSN. +$node->safe_psql('postgres', q{checkpoint}); + +$node->advance_wal(20); + +# Run another checkpoint, this time in the background, and make it wait +# on the injection point so that the checkpoint stops right before +# removing old WAL segments. +note('starting checkpoint'); + +my $checkpoint = $node->background_psql('postgres'); +$checkpoint->query_safe( + q(select injection_points_attach('checkpoint-before-old-wal-removal','wait')) +); +$checkpoint->query_until( + qr/starting_checkpoint/, + q(\echo starting_checkpoint +checkpoint; +\q +)); + +# Wait until the checkpoint stops right before removing WAL segments. +note('waiting for injection_point'); +$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal'); +note('injection_point is reached'); + +# Try to advance the logical slot, but make it stop when it moves to the next +# WAL segment (this has to happen in the background, too). +my $logical = $node->background_psql('postgres'); +$logical->query_safe( + q{select injection_points_attach('logical-replication-slot-advance-segment','wait');} +); +$logical->query_until( + qr/get_changes/, + q( +\echo get_changes +select count(*) from pg_logical_slot_get_changes('slot_logical', null, null) \watch 1 +\q +)); + +# Wait until the slot's restart_lsn points to the next WAL segment. +note('waiting for injection_point'); +$node->wait_for_event('client backend', + 'logical-replication-slot-advance-segment'); +note('injection_point is reached'); + +# OK, we're in the right situation: time to advance the physical slot, which +# recalculates the required LSN, and then unblock the checkpoint, which +# removes the WAL still needed by the logical slot. +$node->safe_psql('postgres', + q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())} +); + +# Generate a long WAL record, spawning at least two pages for the follow-up +# post-recovery check. +$node->safe_psql('postgres', + q{select pg_logical_emit_message(false, '', repeat('123456789', 1000))}); + +# Continue the checkpoint and wait for its completion. +my $log_offset = -s $node->logfile; +$node->safe_psql('postgres', + q{select injection_points_wakeup('checkpoint-before-old-wal-removal')}); +$node->wait_for_log(qr/checkpoint complete/, $log_offset); + +# Abruptly stop the server. +$node->stop('immediate'); + +$node->start; + +eval { + $node->safe_psql('postgres', + q{select count(*) from pg_logical_slot_get_changes('slot_logical', null, null);} + ); +}; +is($@, '', "Logical slot still valid"); + +# If we send \q with $->quit the command can be sent to the +# session already closed. So \q is in initial script, here we only finish +# IPC::Run +$xacts->{run}->finish; +$checkpoint->{run}->finish; +$logical->{run}->finish; + +# Verify that the synchronized slots won't be invalidated immediately after +# synchronization in the presence of a concurrent checkpoint. +my $primary = $node; + +$primary->append_conf('postgresql.conf', "autovacuum = off"); +$primary->reload; + +my $backup_name = 'backup'; + +$primary->backup($backup_name); + +# Create a standby +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup( + $primary, $backup_name, + has_streaming => 1); + +my $connstr_1 = $primary->connstr; +$standby->append_conf( + 'postgresql.conf', qq( +hot_standby_feedback = on +primary_slot_name = 'phys_slot' +primary_conninfo = '$connstr_1 dbname=postgres' +)); + +$primary->safe_psql('postgres', + q{SELECT pg_create_logical_replication_slot('failover_slot', 'test_decoding', false, false, true); + SELECT pg_create_physical_replication_slot('phys_slot');} +); + +$standby->start; + +# Generate some activity and switch WAL file on the primary +$primary->advance_wal(1); +$primary->safe_psql('postgres', "CHECKPOINT"); +$primary->wait_for_replay_catchup($standby); + +# checkpoint on the standby and make it wait on the injection point so that the +# checkpoint stops right before invalidating replication slots. +note('starting checkpoint'); + +$checkpoint = $standby->background_psql('postgres'); +$checkpoint->query_safe( + q(select injection_points_attach('restartpoint-before-slot-invalidation','wait')) +); +$checkpoint->query_until( + qr/starting_checkpoint/, + q(\echo starting_checkpoint +checkpoint; +)); + +# Wait until the checkpoint stops right before invalidating slots +note('waiting for injection_point'); +$standby->wait_for_event('checkpointer', 'restartpoint-before-slot-invalidation'); +note('injection_point is reached'); + +# Enable slot sync worker to synchronize the failover slot to the standby +$standby->append_conf('postgresql.conf', qq(sync_replication_slots = on)); +$standby->reload; + +# Wait for the slot to be synced +$standby->poll_query_until( + 'postgres', + "SELECT COUNT(*) > 0 FROM pg_replication_slots WHERE slot_name = 'failover_slot'"); + +# Release the checkpointer +$standby->safe_psql('postgres', + q{select injection_points_wakeup('restartpoint-before-slot-invalidation'); + select injection_points_detach('restartpoint-before-slot-invalidation')}); + +$checkpoint->quit; + +# Confirm that the slot is not invalidated +is( $standby->safe_psql( + 'postgres', + q{SELECT invalidation_reason IS NULL AND synced FROM pg_replication_slots WHERE slot_name = 'failover_slot';} + ), + "t", + 'logical slot is not invalidated'); + +done_testing(); diff --git a/src/test/recovery/t/047_checkpoint_physical_slot.pl b/src/test/recovery/t/047_checkpoint_physical_slot.pl new file mode 100644 index 0000000000000..4334145abe1bc --- /dev/null +++ b/src/test/recovery/t/047_checkpoint_physical_slot.pl @@ -0,0 +1,131 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# This test verifies the case when the physical slot is advanced during +# checkpoint. The test checks that the physical slot's restart_lsn still refers +# to an existed WAL segment after immediate restart. +# +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; + +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my ($node, $result); + +$node = PostgreSQL::Test::Cluster->new('mike'); +$node->init; +$node->append_conf('postgresql.conf', "wal_level = 'replica'"); +$node->start; + +# Check if the extension injection_points is available, as it may be +# possible that this script is run with installcheck, where the module +# would not be installed by default. +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} + +$node->safe_psql('postgres', q(CREATE EXTENSION injection_points)); + +# Create a physical replication slot. +$node->safe_psql('postgres', + q{select pg_create_physical_replication_slot('slot_physical', true)}); + +# Advance slot to the current position, just to have everything "valid". +$node->safe_psql('postgres', + q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())} +); + +# Run checkpoint to flush current state to disk and set a baseline. +$node->safe_psql('postgres', q{checkpoint}); + +$node->advance_wal(20); + +# Advance slot to the current position, just to have everything "valid". +$node->safe_psql('postgres', + q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())} +); + +# Run another checkpoint to set a new restore LSN. +$node->safe_psql('postgres', q{checkpoint}); + +$node->advance_wal(20); + +my $restart_lsn_init = $node->safe_psql('postgres', + q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'} +); +chomp($restart_lsn_init); +note("restart lsn before checkpoint: $restart_lsn_init"); + +# Run another checkpoint, this time in the background, and make it wait +# on the injection point so that the checkpoint stops right before +# removing old WAL segments. +note('starting checkpoint'); + +my $checkpoint = $node->background_psql('postgres'); +$checkpoint->query_safe( + q{select injection_points_attach('checkpoint-before-old-wal-removal','wait')} +); +$checkpoint->query_until( + qr/starting_checkpoint/, + q(\echo starting_checkpoint +checkpoint; +\q +)); + +# Wait until the checkpoint stops right before removing WAL segments. +note('waiting for injection_point'); +$node->wait_for_event('checkpointer', 'checkpoint-before-old-wal-removal'); +note('injection_point is reached'); + +# OK, we're in the right situation: time to advance the physical slot, which +# recalculates the required LSN and then unblock the checkpoint, which +# removes the WAL still needed by the physical slot. +$node->safe_psql('postgres', + q{select pg_replication_slot_advance('slot_physical', pg_current_wal_lsn())} +); + +# Continue the checkpoint and wait for its completion. +my $log_offset = -s $node->logfile; +$node->safe_psql('postgres', + q{select injection_points_wakeup('checkpoint-before-old-wal-removal')}); +$node->wait_for_log(qr/checkpoint complete/, $log_offset); + +my $restart_lsn_old = $node->safe_psql('postgres', + q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'} +); +chomp($restart_lsn_old); +note("restart lsn before stop: $restart_lsn_old"); + +# Abruptly stop the server. +$node->stop('immediate'); + +$node->start; + +# Get the restart_lsn of the slot right after restarting. +my $restart_lsn = $node->safe_psql('postgres', + q{select restart_lsn from pg_replication_slots where slot_name = 'slot_physical'} +); +chomp($restart_lsn); +note("restart lsn: $restart_lsn"); + +# Get the WAL segment name for the slot's restart_lsn. +my $restart_lsn_segment = $node->safe_psql('postgres', + "SELECT pg_walfile_name('$restart_lsn'::pg_lsn)"); +chomp($restart_lsn_segment); + +# Check if the required wal segment exists. +note("required by slot segment name: $restart_lsn_segment"); +my $datadir = $node->data_dir; +ok( -f "$datadir/pg_wal/$restart_lsn_segment", + "WAL segment $restart_lsn_segment for physical slot's restart_lsn $restart_lsn exists" +); + +done_testing(); diff --git a/src/test/recovery/t/048_vacuum_horizon_floor.pl b/src/test/recovery/t/048_vacuum_horizon_floor.pl new file mode 100644 index 0000000000000..52acb5561d6a5 --- /dev/null +++ b/src/test/recovery/t/048_vacuum_horizon_floor.pl @@ -0,0 +1,290 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# Test that vacuum prunes away all dead tuples killed before OldestXmin +# +# This test creates a table on a primary, updates the table to generate dead +# tuples for vacuum, and then, during the vacuum, uses the replica to force +# GlobalVisState->maybe_needed on the primary to move backwards and precede +# the value of OldestXmin set at the beginning of vacuuming the table. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use Test::More; + +# Set up nodes +my $node_primary = PostgreSQL::Test::Cluster->new('primary'); +$node_primary->init(allows_streaming => 'physical'); + +# io_combine_limit is set to 1 to avoid pinning more than one buffer at a time +# to ensure test determinism. +$node_primary->append_conf( + 'postgresql.conf', qq[ +hot_standby_feedback = on +autovacuum = off +log_min_messages = INFO +maintenance_work_mem = 64 +io_combine_limit = 1 +]); +$node_primary->start; + +my $node_replica = PostgreSQL::Test::Cluster->new('standby'); + +$node_primary->backup('my_backup'); +$node_replica->init_from_backup($node_primary, 'my_backup', + has_streaming => 1); + +$node_replica->start; + +my $test_db = "test_db"; +$node_primary->safe_psql('postgres', "CREATE DATABASE $test_db"); + +# Save the original connection info for later use +my $orig_conninfo = $node_primary->connstr(); + +my $table1 = "vac_horizon_floor_table"; + +# Long-running Primary Session A +my $psql_primaryA = + $node_primary->background_psql($test_db, on_error_stop => 1); + +# Long-running Primary Session B +my $psql_primaryB = + $node_primary->background_psql($test_db, on_error_stop => 1); + +# Our test relies on two rounds of index vacuuming for reasons elaborated +# later. To trigger two rounds of index vacuuming, we must fill up the +# TidStore with dead items partway through a vacuum of the table. The number +# of rows is just enough to ensure we exceed maintenance_work_mem on all +# supported platforms, while keeping test runtime as short as we can. +my $nrows = 2000; + +# Because vacuum's first pass, pruning, is where we use the GlobalVisState to +# check tuple visibility, GlobalVisState->maybe_needed must move backwards +# during pruning before checking the visibility for a tuple which would have +# been considered HEAPTUPLE_DEAD prior to maybe_needed moving backwards but +# HEAPTUPLE_RECENTLY_DEAD compared to the new, older value of maybe_needed. +# +# We must not only force the horizon on the primary to move backwards but also +# force the vacuuming backend's GlobalVisState to be updated. GlobalVisState +# is forced to update during index vacuuming. +# +# _bt_pendingfsm_finalize() calls GetOldestNonRemovableTransactionId() at the +# end of a round of index vacuuming, updating the backend's GlobalVisState +# and, in our case, moving maybe_needed backwards. +# +# Then vacuum's first (pruning) pass will continue and pruning will find our +# later inserted and updated tuple HEAPTUPLE_RECENTLY_DEAD when compared to +# maybe_needed but HEAPTUPLE_DEAD when compared to OldestXmin. +# +# Thus, we must force at least two rounds of index vacuuming to ensure that +# some tuple visibility checks will happen after a round of index vacuuming. +# To accomplish this, we set maintenance_work_mem to its minimum value and +# insert and delete enough rows that we force at least one round of index +# vacuuming before getting to a dead tuple which was killed after the standby +# is disconnected. +$node_primary->safe_psql( + $test_db, qq[ + CREATE TABLE ${table1}(col1 int) + WITH (autovacuum_enabled=false, fillfactor=10); + INSERT INTO $table1 VALUES(7); + INSERT INTO $table1 SELECT generate_series(1, $nrows) % 3; + CREATE INDEX on ${table1}(col1); + DELETE FROM $table1 WHERE col1 = 0; + INSERT INTO $table1 VALUES(7); +]); + +# We will later move the primary forward while the standby is disconnected. +# For now, however, there is no reason not to wait for the standby to catch +# up. +my $primary_lsn = $node_primary->lsn('flush'); +$node_primary->wait_for_catchup($node_replica, 'replay', $primary_lsn); + +# Test that the WAL receiver is up and running. +$node_replica->poll_query_until( + $test_db, qq[ + SELECT EXISTS (SELECT * FROM pg_stat_wal_receiver);], 't'); + +# Set primary_conninfo to something invalid on the replica and reload the +# config. Once the config is reloaded, the startup process will force the WAL +# receiver to restart and it will be unable to reconnect because of the +# invalid connection information. +$node_replica->safe_psql( + $test_db, qq[ + ALTER SYSTEM SET primary_conninfo = ''; + SELECT pg_reload_conf(); + ]); + +# Wait until the WAL receiver has shut down and been unable to start up again. +$node_replica->poll_query_until( + $test_db, qq[ + SELECT EXISTS (SELECT * FROM pg_stat_wal_receiver);], 'f'); + +# Now insert and update a tuple which will be visible to the vacuum on the +# primary but which will have xmax newer than the oldest xmin on the standby +# that was recently disconnected. +my $res = $psql_primaryA->query_safe( + qq[ + INSERT INTO $table1 VALUES (99); + UPDATE $table1 SET col1 = 100 WHERE col1 = 99; + SELECT 'after_update'; + ] +); + +# Make sure the UPDATE finished +like($res, qr/^after_update$/m, "UPDATE occurred on primary session A"); + +# Open a cursor on the primary whose pin will keep VACUUM from getting a +# cleanup lock on the first page of the relation. We want VACUUM to be able to +# start, calculate initial values for OldestXmin and GlobalVisState and then +# be unable to proceed with pruning our dead tuples. This will allow us to +# reconnect the standby and push the horizon back before we start actual +# pruning and vacuuming. +my $primary_cursor1 = "vac_horizon_floor_cursor1"; + +# The first value inserted into the table was a 7, so FETCH FORWARD should +# return a 7. That's how we know the cursor has a pin. +# Disable index scans so the cursor pins heap pages and not index pages. +$res = $psql_primaryB->query_safe( + qq[ + BEGIN; + SET enable_bitmapscan = off; + SET enable_indexscan = off; + SET enable_indexonlyscan = off; + DECLARE $primary_cursor1 CURSOR FOR SELECT * FROM $table1 WHERE col1 = 7; + FETCH $primary_cursor1; + ] +); + +is($res, 7, qq[Cursor query returned $res. Expected value 7.]); + +# Get the PID of the session which will run the VACUUM FREEZE so that we can +# use it to filter pg_stat_activity later. +my $vacuum_pid = $psql_primaryA->query_safe("SELECT pg_backend_pid();"); + +# Now start a VACUUM FREEZE on the primary. It will call vacuum_get_cutoffs() +# and establish values of OldestXmin and GlobalVisState which are newer than +# all of our dead tuples. Then it will be unable to get a cleanup lock to +# start pruning, so it will hang. +# +# We use VACUUM FREEZE because it will wait for a cleanup lock instead of +# skipping the page pinned by the cursor. Note that works because the target +# tuple's xmax precedes OldestXmin which ensures that lazy_scan_noprune() will +# return false and we will wait for the cleanup lock. +# +# Disable any prefetching, parallelism, or other concurrent I/O by vacuum. The +# pages of the heap must be processed in order by a single worker to ensure +# test stability (PARALLEL 0 shouldn't be necessary but guards against the +# possibility of parallel heap vacuuming). +$psql_primaryA->{stdin} .= qq[ + SET maintenance_io_concurrency = 0; + VACUUM (VERBOSE, FREEZE, PARALLEL 0) $table1; + \\echo VACUUM + ]; + +# Make sure the VACUUM command makes it to the server. +$psql_primaryA->{run}->pump_nb(); + +# Make sure that the VACUUM has already called vacuum_get_cutoffs() and is +# just waiting on the lock to start vacuuming. We don't want the standby to +# re-establish a connection to the primary and push the horizon back until +# we've saved initial values in GlobalVisState and calculated OldestXmin. +$node_primary->poll_query_until( + $test_db, + qq[ + SELECT count(*) >= 1 FROM pg_stat_activity + WHERE pid = $vacuum_pid + AND wait_event = 'BufferCleanup'; + ], + 't'); + +# Ensure the WAL receiver is still not active on the replica. +$node_replica->poll_query_until( + $test_db, qq[ + SELECT EXISTS (SELECT * FROM pg_stat_wal_receiver);], 'f'); + +# Allow the WAL receiver connection to re-establish. +$node_replica->safe_psql( + $test_db, qq[ + ALTER SYSTEM SET primary_conninfo = '$orig_conninfo'; + SELECT pg_reload_conf(); + ]); + +# Ensure the new WAL receiver has connected. +$node_replica->poll_query_until( + $test_db, qq[ + SELECT EXISTS (SELECT * FROM pg_stat_wal_receiver);], 't'); + +# Once the WAL sender is shown on the primary, the replica should have +# connected with the primary and pushed the horizon backward. Primary Session +# A won't see that until the VACUUM FREEZE proceeds and does its first round +# of index vacuuming. +$node_primary->poll_query_until( + $test_db, qq[ + SELECT EXISTS (SELECT * FROM pg_stat_replication);], 't'); + +# Move the cursor forward to the next 7. We inserted the 7 much later, so +# advancing the cursor should allow vacuum to proceed vacuuming most pages of +# the relation. Because we set maintenance_work_mem sufficiently low, we +# expect that a round of index vacuuming has happened and that the vacuum is +# now waiting for the cursor to release its pin on the last page of the +# relation. +$res = $psql_primaryB->query_safe("FETCH $primary_cursor1"); +is($res, 7, + qq[Cursor query returned $res from second fetch. Expected value 7.]); + +# Prevent the test from incorrectly passing by confirming that we did indeed +# do a pass of index vacuuming. +$node_primary->poll_query_until( + $test_db, qq[ + SELECT index_vacuum_count > 0 + FROM pg_stat_progress_vacuum + WHERE datname='$test_db' AND relid::regclass = '$table1'::regclass; + ], 't'); + +# Commit the transaction with the open cursor so that the VACUUM can finish. +$psql_primaryB->query_until( + qr/^commit$/m, + qq[ + COMMIT; + \\echo commit + ] +); + +# VACUUM proceeds with pruning and does a visibility check on each tuple. In +# older versions of Postgres, pruning found our final dead tuple +# non-removable (HEAPTUPLE_RECENTLY_DEAD) since its xmax is after the new +# value of maybe_needed. Then heap_prepare_freeze_tuple() would decide the +# tuple xmax should be frozen because it precedes OldestXmin. Vacuum would +# then error out in heap_pre_freeze_checks() with "cannot freeze committed +# xmax". This was fixed by changing pruning to find all +# HEAPTUPLE_RECENTLY_DEAD tuples with xmaxes preceding OldestXmin +# HEAPTUPLE_DEAD and removing them. + +# With the fix, VACUUM should finish successfully, incrementing the table +# vacuum_count. +$node_primary->poll_query_until( + $test_db, + qq[ + SELECT vacuum_count > 0 + FROM pg_stat_all_tables WHERE relname = '${table1}'; + ] + , 't'); + +$primary_lsn = $node_primary->lsn('flush'); + +# Make sure something causes us to flush +$node_primary->safe_psql($test_db, "INSERT INTO $table1 VALUES (1);"); + +# Nothing on the replica should cause a recovery conflict, so this should +# finish successfully. +$node_primary->wait_for_catchup($node_replica, 'replay', $primary_lsn); + +## Shut down psqls +$psql_primaryA->quit; +$psql_primaryB->quit; + +$node_replica->stop(); +$node_primary->stop(); + +done_testing(); diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl new file mode 100644 index 0000000000000..0e74175f9ebbc --- /dev/null +++ b/src/test/recovery/t/049_wait_for_lsn.pl @@ -0,0 +1,677 @@ +# Checks waiting for the LSN using the WAIT FOR command. +# Tests standby modes (standby_replay/standby_write/standby_flush) on standby +# and primary_flush mode on primary. +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Helper functions to control walreceiver for testing wait conditions. +# These allow us to stop WAL streaming so waiters block, then resume it. +my $saved_primary_conninfo; + +sub stop_walreceiver +{ + my ($node) = @_; + $saved_primary_conninfo = $node->safe_psql( + 'postgres', qq[ + SELECT pg_catalog.quote_literal(setting) + FROM pg_settings + WHERE name = 'primary_conninfo'; + ]); + $node->safe_psql( + 'postgres', qq[ + ALTER SYSTEM SET primary_conninfo = ''; + SELECT pg_reload_conf(); + ]); + + $node->poll_query_until('postgres', + "SELECT NOT EXISTS (SELECT * FROM pg_stat_wal_receiver);"); +} + +sub resume_walreceiver +{ + my ($node) = @_; + $node->safe_psql( + 'postgres', qq[ + ALTER SYSTEM SET primary_conninfo = $saved_primary_conninfo; + SELECT pg_reload_conf(); + ]); + + $node->poll_query_until('postgres', + "SELECT EXISTS (SELECT * FROM pg_stat_wal_receiver);"); +} + +# Initialize primary node +my $node_primary = PostgreSQL::Test::Cluster->new('primary'); +$node_primary->init(allows_streaming => 1); +$node_primary->start; + +# And some content and take a backup +$node_primary->safe_psql('postgres', + "CREATE TABLE wait_test AS SELECT generate_series(1,10) AS a"); +my $backup_name = 'my_backup'; +$node_primary->backup($backup_name); + +# Create a streaming standby with a 1 second delay from the backup +my $node_standby = PostgreSQL::Test::Cluster->new('standby'); +my $delay = 1; +$node_standby->init_from_backup($node_primary, $backup_name, + has_streaming => 1); +$node_standby->append_conf( + 'postgresql.conf', qq[ + recovery_min_apply_delay = '${delay}s' +]); +$node_standby->start; + +# 1. Make sure that WAIT FOR works: add new content to +# primary and memorize primary's insert LSN, then wait for that LSN to be +# replayed on standby. +$node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (generate_series(11, 20))"); +my $lsn1 = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); +my $output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn1}' WITH (timeout '1d'); + SELECT pg_lsn_cmp(pg_last_wal_replay_lsn(), '${lsn1}'::pg_lsn); +]); + +# Make sure the current LSN on standby is at least as big as the LSN we +# observed on primary's before. +ok((split("\n", $output))[-1] >= 0, + "standby reached the same LSN as primary after WAIT FOR"); + +# 2. Check that new data is visible after calling WAIT FOR +$node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (generate_series(21, 30))"); +my $lsn2 = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn2}'; + SELECT count(*) FROM wait_test; +]); + +# Make sure the count(*) on standby reflects the recent changes on primary +ok((split("\n", $output))[-1] eq 30, + "standby reached the same LSN as primary"); + +# 3. Check that WAIT FOR works with standby_write, standby_flush, and +# primary_flush modes. +$node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (generate_series(31, 40))"); +my $lsn_write = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn_write}' WITH (MODE 'standby_write', timeout '1d'); + SELECT pg_lsn_cmp((SELECT written_lsn FROM pg_stat_wal_receiver), '${lsn_write}'::pg_lsn); +]); + +ok( (split("\n", $output))[-1] >= 0, + "standby wrote WAL up to target LSN after WAIT FOR with MODE 'standby_write'" +); + +$node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (generate_series(41, 50))"); +my $lsn_flush = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn_flush}' WITH (MODE 'standby_flush', timeout '1d'); + SELECT pg_lsn_cmp(pg_last_wal_receive_lsn(), '${lsn_flush}'::pg_lsn); +]); + +ok( (split("\n", $output))[-1] >= 0, + "standby flushed WAL up to target LSN after WAIT FOR with MODE 'standby_flush'" +); + +# Check primary_flush mode on primary +$node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (generate_series(51, 60))"); +my $lsn_primary_flush = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); +$output = $node_primary->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn_primary_flush}' WITH (MODE 'primary_flush', timeout '1d'); + SELECT pg_lsn_cmp(pg_current_wal_flush_lsn(), '${lsn_primary_flush}'::pg_lsn); +]); + +ok( (split("\n", $output))[-1] >= 0, + "primary flushed WAL up to target LSN after WAIT FOR with MODE 'primary_flush'" +); + +# 4. Check that waiting for unreachable LSN triggers the timeout. The +# unreachable LSN must be well in advance. So WAL records issued by +# the concurrent autovacuum could not affect that. +my $lsn3 = + $node_primary->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn() + 10000000000"); +my $stderr; +$node_standby->safe_psql('postgres', + "WAIT FOR LSN '${lsn2}' WITH (timeout '10ms');"); +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${lsn3}' WITH (timeout '1000ms');", + stderr => \$stderr); +ok( $stderr =~ /timed out while waiting for target LSN/, + "get timeout on waiting for unreachable LSN"); + +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn2}' WITH (timeout '0.1s', no_throw);]); +ok($output eq "success", + "WAIT FOR returns correct status after successful waiting"); +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn3}' WITH (timeout '10ms', no_throw);]); +ok($output eq "timeout", "WAIT FOR returns correct status after timeout"); + +# 5. Check mode validation: standby modes error on primary, primary mode errors +# on standby, and primary_flush works on primary. Also check that WAIT FOR +# triggers an error if called within a function, procedure, anonymous DO block, +# or inside a transaction with an isolation level higher than READ COMMITTED. + +# Test standby_flush on primary - should error +$node_primary->psql( + 'postgres', + "WAIT FOR LSN '${lsn3}' WITH (MODE 'standby_flush');", + stderr => \$stderr); +ok($stderr =~ /recovery is not in progress/, + "get an error when running standby_flush on the primary"); + +# Test primary_flush on standby - should error +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${lsn3}' WITH (MODE 'primary_flush');", + stderr => \$stderr); +ok($stderr =~ /recovery is in progress/, + "get an error when running primary_flush on the standby"); + +$node_standby->psql( + 'postgres', + "BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT 1; WAIT FOR LSN '${lsn3}';", + stderr => \$stderr); +ok( $stderr =~ + /WAIT FOR must be called without an active or registered snapshot/, + "get an error when running in a transaction with an isolation level higher than REPEATABLE READ" +); + +# Test wrapping WAIT FOR into function, procedure, and anonymous DO block -- +# should error +$node_primary->safe_psql( + 'postgres', qq[ +CREATE FUNCTION pg_wal_replay_wait_wrap(target_lsn pg_lsn) RETURNS void AS \$\$ + BEGIN + EXECUTE format('WAIT FOR LSN %L;', target_lsn); + END +\$\$ +LANGUAGE plpgsql; + +CREATE PROCEDURE pg_wal_replay_wait_proc(target_lsn pg_lsn) AS \$\$ + BEGIN + EXECUTE format('WAIT FOR LSN %L;', target_lsn); + END +\$\$ +LANGUAGE plpgsql; +]); + +$node_primary->wait_for_catchup($node_standby); +$node_standby->psql( + 'postgres', + "SELECT pg_wal_replay_wait_wrap('${lsn3}');", + stderr => \$stderr); +ok($stderr =~ /WAIT FOR can only be executed as a top-level statement/, + "get an error when running within a function"); + +$node_standby->psql( + 'postgres', + "CALL pg_wal_replay_wait_proc('${lsn3}');", + stderr => \$stderr); +ok($stderr =~ /WAIT FOR can only be executed as a top-level statement/, + "get an error when running within a procedure"); + +$node_standby->psql( + 'postgres', + "DO \$\$ BEGIN EXECUTE format('WAIT FOR LSN %L;', '${lsn3}'); END \$\$;", + stderr => \$stderr); +ok($stderr =~ /WAIT FOR can only be executed as a top-level statement/, + "get an error when running within a DO block"); + +# 6. Check parameter validation error cases on standby before promotion +my $test_lsn = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); + +# Test negative timeout +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (timeout '-1000ms');", + stderr => \$stderr); +ok($stderr =~ /timeout cannot be negative/, "get error for negative timeout"); + +# Test unknown parameter with WITH clause +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (unknown_param 'value');", + stderr => \$stderr); +ok($stderr =~ /option "unknown_param" not recognized/, + "get error for unknown parameter"); + +# Test duplicate TIMEOUT parameter with WITH clause +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (timeout '1000', timeout '2000');", + stderr => \$stderr); +ok( $stderr =~ /conflicting or redundant options/, + "get error for duplicate TIMEOUT parameter"); + +# Test duplicate NO_THROW parameter with WITH clause +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (no_throw, no_throw);", + stderr => \$stderr); +ok( $stderr =~ /conflicting or redundant options/, + "get error for duplicate NO_THROW parameter"); + +# Test syntax error - options without WITH keyword +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' (timeout '100ms');", + stderr => \$stderr); +ok($stderr =~ /syntax error/, + "get syntax error when options specified without WITH keyword"); + +# Test syntax error - missing LSN +$node_standby->psql('postgres', "WAIT FOR TIMEOUT 1000;", stderr => \$stderr); +ok($stderr =~ /syntax error/, "get syntax error for missing LSN"); + +# Test invalid LSN format +$node_standby->psql( + 'postgres', + "WAIT FOR LSN 'invalid_lsn';", + stderr => \$stderr); +ok($stderr =~ /invalid input syntax for type pg_lsn/, + "get error for invalid LSN format"); + +# Test invalid timeout format +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (timeout 'invalid');", + stderr => \$stderr); +ok($stderr =~ /invalid timeout value/, + "get error for invalid timeout format"); + +# Test new WITH clause syntax +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn2}' WITH (timeout '0.1s', no_throw);]); +ok($output eq "success", "WAIT FOR WITH clause syntax works correctly"); + +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn3}' WITH (timeout 100, no_throw);]); +ok($output eq "timeout", + "WAIT FOR WITH clause returns correct timeout status"); + +# Test WITH clause error case - invalid option +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (invalid_option 'value');", + stderr => \$stderr); +ok( $stderr =~ /option "invalid_option" not recognized/, + "get error for invalid WITH clause option"); + +# Test invalid MODE value +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (MODE 'invalid');", + stderr => \$stderr); +ok($stderr =~ /unrecognized value for WAIT option "mode": "invalid"/, + "get error for invalid MODE value"); + +# Test duplicate MODE parameter +$node_standby->psql( + 'postgres', + "WAIT FOR LSN '${test_lsn}' WITH (MODE 'standby_replay', MODE 'standby_write');", + stderr => \$stderr); +ok( $stderr =~ /conflicting or redundant options/, + "get error for duplicate MODE parameter"); + +# 7a. Check the scenario of multiple standby_replay waiters. We make 5 +# background psql sessions each waiting for a corresponding insertion. When +# waiting is finished, stored procedures logs if there are visible as many +# rows as should be. +$node_primary->safe_psql( + 'postgres', qq[ +CREATE FUNCTION log_count(i int) RETURNS void AS \$\$ + DECLARE + count int; + BEGIN + SELECT count(*) FROM wait_test INTO count; + IF count >= 31 + i THEN + RAISE LOG 'count %', i; + END IF; + END +\$\$ +LANGUAGE plpgsql; + +CREATE FUNCTION log_wait_done(prefix text, i int) RETURNS void AS \$\$ + BEGIN + RAISE LOG '% %', prefix, i; + END +\$\$ +LANGUAGE plpgsql; +]); + +$node_standby->safe_psql('postgres', "SELECT pg_wal_replay_pause();"); + +my @psql_sessions; +for (my $i = 0; $i < 5; $i++) +{ + $node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (${i});"); + my $lsn = + $node_primary->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn()"); + $psql_sessions[$i] = $node_standby->background_psql('postgres'); + $psql_sessions[$i]->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${lsn}'; + SELECT log_count(${i}); + ]); +} + +my $log_offset = -s $node_standby->logfile; +$node_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume();"); +for (my $i = 0; $i < 5; $i++) +{ + $node_standby->wait_for_log("count ${i}", $log_offset); + $psql_sessions[$i]->quit; +} + +ok(1, 'multiple standby_replay waiters reported consistent data'); + +# 7b. Check the scenario of multiple standby_write waiters. +# Stop walreceiver to ensure waiters actually block. +stop_walreceiver($node_standby); + +# Generate WAL on primary (standby won't receive it yet) +my @write_lsns; +for (my $i = 0; $i < 5; $i++) +{ + $node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (100 + ${i});"); + $write_lsns[$i] = + $node_primary->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn()"); +} + +# Start standby_write waiters (they will block since walreceiver is stopped) +my @write_sessions; +for (my $i = 0; $i < 5; $i++) +{ + $write_sessions[$i] = $node_standby->background_psql('postgres'); + $write_sessions[$i]->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '$write_lsns[$i]' WITH (MODE 'standby_write', timeout '1d'); + SELECT log_wait_done('write_done', $i); + ]); +} + +# Verify waiters are blocked +$node_standby->poll_query_until('postgres', + "SELECT count(*) = 5 FROM pg_stat_activity WHERE wait_event = 'WaitForWalWrite'" +); + +# Restore walreceiver to unblock waiters +my $write_log_offset = -s $node_standby->logfile; +resume_walreceiver($node_standby); + +# Wait for all waiters to complete and close sessions +for (my $i = 0; $i < 5; $i++) +{ + $node_standby->wait_for_log("write_done $i", $write_log_offset); + $write_sessions[$i]->quit; +} + +# Verify on standby that WAL was written up to the target LSN +$output = $node_standby->safe_psql('postgres', + "SELECT pg_lsn_cmp((SELECT written_lsn FROM pg_stat_wal_receiver), '$write_lsns[4]'::pg_lsn);" +); + +ok($output >= 0, + "multiple standby_write waiters: standby wrote WAL up to target LSN"); + +# 7c. Check the scenario of multiple standby_flush waiters. +# Stop walreceiver to ensure waiters actually block. +stop_walreceiver($node_standby); + +# Generate WAL on primary (standby won't receive it yet) +my @flush_lsns; +for (my $i = 0; $i < 5; $i++) +{ + $node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (200 + ${i});"); + $flush_lsns[$i] = + $node_primary->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn()"); +} + +# Start standby_flush waiters (they will block since walreceiver is stopped) +my @flush_sessions; +for (my $i = 0; $i < 5; $i++) +{ + $flush_sessions[$i] = $node_standby->background_psql('postgres'); + $flush_sessions[$i]->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '$flush_lsns[$i]' WITH (MODE 'standby_flush', timeout '1d'); + SELECT log_wait_done('flush_done', $i); + ]); +} + +# Verify waiters are blocked +$node_standby->poll_query_until('postgres', + "SELECT count(*) = 5 FROM pg_stat_activity WHERE wait_event = 'WaitForWalFlush'" +); + +# Restore walreceiver to unblock waiters +my $flush_log_offset = -s $node_standby->logfile; +resume_walreceiver($node_standby); + +# Wait for all waiters to complete and close sessions +for (my $i = 0; $i < 5; $i++) +{ + $node_standby->wait_for_log("flush_done $i", $flush_log_offset); + $flush_sessions[$i]->quit; +} + +# Verify on standby that WAL was flushed up to the target LSN +$output = $node_standby->safe_psql('postgres', + "SELECT pg_lsn_cmp(pg_last_wal_receive_lsn(), '$flush_lsns[4]'::pg_lsn);" +); + +ok($output >= 0, + "multiple standby_flush waiters: standby flushed WAL up to target LSN"); + +# 7d. Check the scenario of mixed standby mode waiters (standby_replay, +# standby_write, standby_flush) running concurrently. We start 6 sessions: +# 2 for each mode, all waiting for the same target LSN. We stop the +# walreceiver and pause replay to ensure all waiters block. Then we resume +# replay and restart the walreceiver to verify they unblock and complete +# correctly. + +# Stop walreceiver first to ensure we can control the flow without hanging +# (stopping it after pausing replay can hang if the startup process is paused). +stop_walreceiver($node_standby); + +# Pause replay +$node_standby->safe_psql('postgres', "SELECT pg_wal_replay_pause();"); + +# Generate WAL on primary +$node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (generate_series(301, 310));"); +my $mixed_target_lsn = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); + +# Start 6 waiters: 2 for each mode +my @mixed_sessions; +my @mixed_modes = ('standby_replay', 'standby_write', 'standby_flush'); +for (my $i = 0; $i < 6; $i++) +{ + $mixed_sessions[$i] = $node_standby->background_psql('postgres'); + $mixed_sessions[$i]->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${mixed_target_lsn}' WITH (MODE '$mixed_modes[$i % 3]', timeout '1d'); + SELECT log_wait_done('mixed_done', $i); + ]); +} + +# Verify all waiters are blocked +$node_standby->poll_query_until('postgres', + "SELECT count(*) = 6 FROM pg_stat_activity WHERE wait_event LIKE 'WaitForWal%'" +); + +# Resume replay (waiters should still be blocked as no WAL has arrived) +my $mixed_log_offset = -s $node_standby->logfile; +$node_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume();"); +$node_standby->poll_query_until('postgres', + "SELECT NOT pg_is_wal_replay_paused();"); + +# Restore walreceiver to allow WAL to arrive +resume_walreceiver($node_standby); + +# Wait for all sessions to complete and close them +for (my $i = 0; $i < 6; $i++) +{ + $node_standby->wait_for_log("mixed_done $i", $mixed_log_offset); + $mixed_sessions[$i]->quit; +} + +# Verify all modes reached the target LSN +$output = $node_standby->safe_psql( + 'postgres', qq[ + SELECT pg_lsn_cmp((SELECT written_lsn FROM pg_stat_wal_receiver), '${mixed_target_lsn}'::pg_lsn) >= 0 AND + pg_lsn_cmp(pg_last_wal_receive_lsn(), '${mixed_target_lsn}'::pg_lsn) >= 0 AND + pg_lsn_cmp(pg_last_wal_replay_lsn(), '${mixed_target_lsn}'::pg_lsn) >= 0; +]); + +ok($output eq 't', + "mixed mode waiters: all modes completed and reached target LSN"); + +# 7e. Check the scenario of multiple primary_flush waiters on primary. +# We start 5 background sessions waiting for different LSNs with primary_flush +# mode. Each waiter logs when done. +my @primary_flush_lsns; +for (my $i = 0; $i < 5; $i++) +{ + $node_primary->safe_psql('postgres', + "INSERT INTO wait_test VALUES (400 + ${i});"); + $primary_flush_lsns[$i] = + $node_primary->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn()"); +} + +my $primary_flush_log_offset = -s $node_primary->logfile; + +# Start primary_flush waiters +my @primary_flush_sessions; +for (my $i = 0; $i < 5; $i++) +{ + $primary_flush_sessions[$i] = $node_primary->background_psql('postgres'); + $primary_flush_sessions[$i]->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '$primary_flush_lsns[$i]' WITH (MODE 'primary_flush', timeout '1d'); + SELECT log_wait_done('primary_flush_done', $i); + ]); +} + +# The WAL should already be flushed, so waiters should complete quickly +for (my $i = 0; $i < 5; $i++) +{ + $node_primary->wait_for_log("primary_flush_done $i", + $primary_flush_log_offset); + $primary_flush_sessions[$i]->quit; +} + +# Verify on primary that WAL was flushed up to the target LSN +$output = $node_primary->safe_psql('postgres', + "SELECT pg_lsn_cmp(pg_current_wal_flush_lsn(), '$primary_flush_lsns[4]'::pg_lsn);" +); + +ok($output >= 0, + "multiple primary_flush waiters: primary flushed WAL up to target LSN"); + +# 8. Check that the standby promotion terminates all standby wait modes. Start +# waiting for unreachable LSNs with standby_replay, standby_write, and +# standby_flush modes, then promote. Check the log for the relevant error +# messages. Also, check that waiting for already replayed LSN doesn't cause +# an error even after promotion. +my $lsn4 = + $node_primary->safe_psql('postgres', + "SELECT pg_current_wal_insert_lsn() + 10000000000"); + +my $lsn5 = + $node_primary->safe_psql('postgres', "SELECT pg_current_wal_insert_lsn()"); + +# Start background sessions waiting for unreachable LSN with all modes +my @wait_modes = ('standby_replay', 'standby_write', 'standby_flush'); +my @wait_sessions; +for (my $i = 0; $i < 3; $i++) +{ + $wait_sessions[$i] = $node_standby->background_psql('postgres'); + $wait_sessions[$i]->query_until( + qr/start/, qq[ + \\echo start + WAIT FOR LSN '${lsn4}' WITH (MODE '$wait_modes[$i]'); + ]); +} + +# Make sure standby will be promoted at least at the primary insert LSN we +# have just observed. Use pg_switch_wal() to force the insert LSN to be +# written then wait for standby to catchup. +$node_primary->safe_psql('postgres', 'SELECT pg_switch_wal();'); +$node_primary->wait_for_catchup($node_standby); + +$log_offset = -s $node_standby->logfile; +$node_standby->promote; + +# Wait for all three sessions to get the error (each mode has distinct message) +$node_standby->wait_for_log(qr/Recovery ended before target LSN.*was written/, + $log_offset); +$node_standby->wait_for_log(qr/Recovery ended before target LSN.*was flushed/, + $log_offset); +$node_standby->wait_for_log( + qr/Recovery ended before target LSN.*was replayed/, $log_offset); + +ok(1, 'promotion interrupted all wait modes'); + +$node_standby->safe_psql('postgres', "WAIT FOR LSN '${lsn5}';"); + +ok(1, 'wait for already replayed LSN exits immediately even after promotion'); + +$output = $node_standby->safe_psql( + 'postgres', qq[ + WAIT FOR LSN '${lsn4}' WITH (timeout '10ms', no_throw);]); +ok($output eq "not in recovery", + "WAIT FOR returns correct status after standby promotion"); + + +$node_standby->stop; +$node_primary->stop; + +# If we send \q with $session->quit the command can be sent to the session +# already closed. So \q is in initial script, here we only finish IPC::Run. +for (my $i = 0; $i < 3; $i++) +{ + $wait_sessions[$i]->{run}->finish; +} + +done_testing(); diff --git a/src/test/recovery/t/050_redo_segment_missing.pl b/src/test/recovery/t/050_redo_segment_missing.pl new file mode 100644 index 0000000000000..e07ff0c72fee7 --- /dev/null +++ b/src/test/recovery/t/050_redo_segment_missing.pl @@ -0,0 +1,117 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# Evaluates PostgreSQL's recovery behavior when a WAL segment containing the +# redo record is missing, with a checkpoint record located in a different +# segment. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('testnode'); +$node->init; +$node->append_conf('postgresql.conf', 'log_checkpoints = on'); +$node->start; + +# Check if the extension injection_points is available, as it may be +# possible that this script is run with installcheck, where the module +# would not be installed by default. +if (!$node->check_extension('injection_points')) +{ + plan skip_all => 'Extension injection_points not installed'; +} +$node->safe_psql('postgres', q(CREATE EXTENSION injection_points)); + +# Note that this uses two injection points based on waits, not one. This +# may look strange, but this works as a workaround to enforce all memory +# allocations to happen outside the critical section of the checkpoint +# required for this test. +# First, "create-checkpoint-initial" is run outside the critical section +# section, and is used as a way to initialize the shared memory required +# for the wait machinery with its DSM registry. +# Then, "create-checkpoint-run" is loaded outside the critical section of +# a checkpoint to allocate any memory required by the library load, and +# its callback is run inside the critical section. +$node->safe_psql('postgres', + q{select injection_points_attach('create-checkpoint-initial', 'wait')}); +$node->safe_psql('postgres', + q{select injection_points_attach('create-checkpoint-run', 'wait')}); + +# Start a psql session to run the checkpoint in the background and make +# the test wait on the injection point so the checkpoint stops just after +# it starts. +my $checkpoint = $node->background_psql('postgres'); +$checkpoint->query_until( + qr/starting_checkpoint/, + q(\echo starting_checkpoint +checkpoint; +)); + +# Wait for the initial point to finish, the checkpointer is still +# outside its critical section. Then release to reach the second +# point. +$node->wait_for_event('checkpointer', 'create-checkpoint-initial'); +$node->safe_psql('postgres', + q{select injection_points_wakeup('create-checkpoint-initial')}); + +# Wait until the checkpoint has reached the second injection point. +# We are now in the middle of a checkpoint running, after the redo +# record has been logged. +$node->wait_for_event('checkpointer', 'create-checkpoint-run'); + +# Switch the WAL segment, ensuring that the redo record will be included +# in a different segment than the checkpoint record. +$node->safe_psql('postgres', 'SELECT pg_switch_wal()'); + +# Continue the checkpoint and wait for its completion. +my $log_offset = -s $node->logfile; +$node->safe_psql('postgres', + q{select injection_points_wakeup('create-checkpoint-run')}); +$node->wait_for_log(qr/checkpoint complete/, $log_offset); + +$checkpoint->quit; + +# Retrieve the WAL file names for the redo record and checkpoint record. +my $redo_lsn = $node->safe_psql('postgres', + "SELECT redo_lsn FROM pg_control_checkpoint()"); +my $redo_walfile_name = + $node->safe_psql('postgres', "SELECT pg_walfile_name('$redo_lsn')"); +my $checkpoint_lsn = $node->safe_psql('postgres', + "SELECT checkpoint_lsn FROM pg_control_checkpoint()"); +my $checkpoint_walfile_name = + $node->safe_psql('postgres', "SELECT pg_walfile_name('$checkpoint_lsn')"); + +# Redo record and checkpoint record should be on different segments. +isnt($redo_walfile_name, $checkpoint_walfile_name, + 'redo and checkpoint records on different segments'); + +# Remove the WAL segment containing the redo record. +unlink $node->data_dir . "/pg_wal/$redo_walfile_name" + or die "could not remove WAL file: $!"; + +$node->stop('immediate'); + +# Use run_log instead of node->start because this test expects that +# the server ends with an error during recovery. +run_log( + [ + 'pg_ctl', + '--pgdata' => $node->data_dir, + '--log' => $node->logfile, + 'start', + ]); + +# Confirm that recovery has failed, as expected. +my $logfile = slurp_file($node->logfile()); +ok( $logfile =~ + qr/FATAL: .* could not find redo location .* referenced by checkpoint record at .*/, + "ends with FATAL because it could not find redo location"); + +done_testing(); diff --git a/src/test/recovery/t/051_effective_wal_level.pl b/src/test/recovery/t/051_effective_wal_level.pl new file mode 100644 index 0000000000000..991473726a94c --- /dev/null +++ b/src/test/recovery/t/051_effective_wal_level.pl @@ -0,0 +1,404 @@ +# Copyright (c) 2025-2026, PostgreSQL Global Development Group +# +# Test that effective_wal_level changes upon logical replication slot creation +# and deletion. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Check both wal_level and effective_wal_level values on the given node +# are expected. +sub test_wal_level +{ + my ($node, $expected, $msg) = @_; + + is( $node->safe_psql( + 'postgres', + qq[select current_setting('wal_level'), current_setting('effective_wal_level');] + ), + "$expected", + "$msg"); +} + +# Wait for the checkpointer to decrease effective_wal_level to 'replica'. +sub wait_for_logical_decoding_disabled +{ + my ($node) = @_; + + $node->poll_query_until('postgres', + qq[select current_setting('effective_wal_level') = 'replica';]); +} + +# Initialize the primary server with wal_level = 'replica'. +my $primary = PostgreSQL::Test::Cluster->new('primary'); +$primary->init(allows_streaming => 1); +$primary->append_conf('postgresql.conf', "log_min_messages = debug1"); +$primary->start(); + +# Check both initial wal_level and effective_wal_level values. +test_wal_level($primary, "replica|replica", + "wal_level and effective_wal_level start with the same value 'replica'"); + +# Create a physical slot and verify that it doesn't affect effective_wal_level. +$primary->safe_psql('postgres', + qq[select pg_create_physical_replication_slot('test_phy_slot', false, false)] +); +test_wal_level($primary, "replica|replica", + "effective_wal_level doesn't change with a new physical slot"); +$primary->safe_psql('postgres', + qq[select pg_drop_replication_slot('test_phy_slot')]); + +# Create a temporary logical slot but exit without releasing it explicitly. +# This enables logical decoding but skips disabling it and delegates to the +# checkpointer. +$primary->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('test_tmp_slot', 'test_decoding', true)] +); +ok( $primary->log_contains( + "logical decoding is enabled upon creating a new logical replication slot" + ), + "logical decoding has been enabled upon creating a temp slot"); + +# Wait for the checkpointer to disable logical decoding. +wait_for_logical_decoding_disabled($primary); + +# Create a new logical slot and check that effective_wal_level must be increased +# to 'logical'. +$primary->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('test_slot', 'pgoutput')]); +test_wal_level($primary, "replica|logical", + "effective_wal_level increased to 'logical' upon a logical slot creation" +); + +# Restart the server and check again. +$primary->restart(); +test_wal_level($primary, "replica|logical", + "effective_wal_level remains 'logical' even after a server restart"); + +# Create and drop another logical slot, then verify that effective_wal_level remains +# 'logical'. +$primary->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('test_slot2', 'pgoutput')]); +$primary->safe_psql('postgres', + qq[select pg_drop_replication_slot('test_slot2')]); +test_wal_level($primary, "replica|logical", + "effective_wal_level stays 'logical' as one slot remains"); + +# Verify that the server cannot start with wal_level='minimal' when there is +# at least one replication slot. +$primary->adjust_conf('postgresql.conf', 'wal_level', 'minimal'); +$primary->adjust_conf('postgresql.conf', 'max_wal_senders', '0'); +$primary->stop; + +command_fails( + [ + 'pg_ctl', + '--pgdata' => $primary->data_dir, + '--log' => $primary->logfile, + 'start', + ], + "cannot start server with wal_level='minimal' as there is in-use logical slot"); + +my $logfile = slurp_file($primary->logfile()); +like( + $logfile, + qr/logical replication slot "test_slot" exists, but "wal_level" < "replica"/, + 'logical slots requires logical decoding enabled at server startup'); + +# Revert the modified settings. +$primary->adjust_conf('postgresql.conf', 'wal_level', 'replica'); +$primary->adjust_conf('postgresql.conf', 'max_wal_senders', '10'); + +# Add other settings to test if we disable logical decoding when invalidating the last +# logical slot. +$primary->append_conf( + 'postgresql.conf', + qq[ +min_wal_size = 32MB +max_wal_size = 32MB +max_slot_wal_keep_size = 16MB +]); +$primary->start; + +# Advance WAL and verify that the slot gets invalidated. +$primary->advance_wal(2); +$primary->safe_psql('postgres', qq[CHECKPOINT]); +is( $primary->safe_psql( + 'postgres', + qq[ +select invalidation_reason = 'wal_removed' from pg_replication_slots where slot_name = 'test_slot'; + ]), + 't', + 'test_slot gets invalidated due to wal_removed'); + +# Verify that logical decoding is disabled after invalidating the last logical slot. +wait_for_logical_decoding_disabled($primary); +test_wal_level($primary, "replica|replica", + "effective_wal_level got decreased to 'replica' after invalidating the last logical slot" +); + +# Revert the modified settings, and restart the server. +$primary->adjust_conf('postgresql.conf', 'max_slot_wal_keep_size', undef); +$primary->adjust_conf('postgresql.conf', 'min_wal_size', undef); +$primary->adjust_conf('postgresql.conf', 'max_wal_size', undef); +$primary->restart; + +# Recreate the logical slot to enable logical decoding again. +$primary->safe_psql('postgres', + qq[select pg_drop_replication_slot('test_slot')]); +$primary->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('test_slot', 'pgoutput')]); + +# Take backup during the effective_wal_level being 'logical'. But note that +# replication slots are not included in the backup. +$primary->backup('my_backup'); + +# Initialize standby1 node. +my $standby1 = PostgreSQL::Test::Cluster->new('standby1'); +$standby1->init_from_backup($primary, 'my_backup', has_streaming => 1); +$standby1->start; + +# Creating a logical slot on standby should succeed as the primary enables +# it. +$primary->wait_for_replay_catchup($standby1); +$standby1->create_logical_slot_on_standby($primary, 'standby1_slot', + 'postgres'); + +# Promote the standby1 node that has one logical slot. So effective_wal_level +# remains 'logical' even after the promotion. +$standby1->promote; +test_wal_level($standby1, "replica|logical", + "effective_wal_level remains 'logical' even after the promotion"); + +# Confirm if we can create a logical slot after the promotion. +$standby1->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('standby1_slot2', 'pgoutput')] +); +$standby1->stop; + +# Initialize standby2 node and start it with wal_level = 'logical'. +my $standby2 = PostgreSQL::Test::Cluster->new('standby2'); +$standby2->init_from_backup($primary, 'my_backup', has_streaming => 1); +$standby2->append_conf('postgresql.conf', qq[wal_level = 'logical']); +$standby2->start(); +$standby2->backup('my_backup3'); + +# Initialize cascade standby and start with wal_level = 'replica'. +my $cascade = PostgreSQL::Test::Cluster->new('cascade'); +$cascade->init_from_backup($standby2, 'my_backup3', has_streaming => 1); +$cascade->adjust_conf('postgresql.conf', 'wal_level', 'replica'); +$cascade->start(); + +# Regardless of their wal_level values, effective_wal_level values on the +# standby and the cascaded standby depend on the primary's value, 'logical'. +test_wal_level($standby2, "logical|logical", + "check wal_level and effective_wal_level on standby"); +test_wal_level($cascade, "replica|logical", + "check wal_level and effective_wal_level on cascaded standby"); + +# Drop the primary's last logical slot, decreasing effective_wal_level to +# 'replica' on all nodes. +$primary->safe_psql('postgres', + qq[select pg_drop_replication_slot('test_slot')]); +wait_for_logical_decoding_disabled($primary); + +$primary->wait_for_replay_catchup($standby2); +$standby2->wait_for_replay_catchup($cascade, $primary); + +test_wal_level($primary, "replica|replica", + "effective_wal_level got decreased to 'replica' on primary"); +test_wal_level($standby2, "logical|replica", + "effective_wal_level got decreased to 'replica' on standby"); +test_wal_level($cascade, "replica|replica", + "effective_wal_level got decreased to 'replica' on cascaded standby"); + +# Promote standby2, increasing effective_wal_level to 'logical' as its wal_level +# is set to 'logical'. +$standby2->promote; + +# Verify that effective_wal_level is increased to 'logical' on the cascaded standby. +$standby2->wait_for_replay_catchup($cascade); +test_wal_level($cascade, "replica|logical", + "effective_wal_level got increased to 'logical' on standby as the new primary has wal_level='logical'" +); + +$standby2->stop; +$cascade->stop; + +# Initialize standby3 node and start it. +my $standby3 = PostgreSQL::Test::Cluster->new('standby3'); +$standby3->init_from_backup($primary, 'my_backup', has_streaming => 1); +$standby3->start; + +# Create logical slots on both nodes. +$primary->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('test_slot', 'pgoutput')]); +$primary->wait_for_replay_catchup($standby3); +$standby3->create_logical_slot_on_standby($primary, 'standby3_slot', + 'postgres'); + +# Drop the logical slot from the primary, decreasing effective_wal_level to +# 'replica' on the primary, which leads to invalidating the logical slot on the +# standby due to 'wal_level_insufficient'. +$primary->safe_psql('postgres', + qq[select pg_drop_replication_slot('test_slot')]); +wait_for_logical_decoding_disabled($primary); +test_wal_level($primary, "replica|replica", + "effective_wal_level got decreased to 'replica' on the primary to invalidate standby's slots" +); +$standby3->poll_query_until( + 'postgres', qq[ +select invalidation_reason = 'wal_level_insufficient' from pg_replication_slots where slot_name = 'standby3_slot' + ]); + +# Restart the server to verify that the slot is successfully restored during +# startup. +$standby3->restart; + +# Check that the logical decoding is not enabled on the standby3. Note that it still has +# the invalidated logical slot. +test_wal_level($standby3, "replica|replica", + "effective_wal_level got decreased to 'replica' on standby"); + +my ($result, $stdout, $stderr) = $standby3->psql('postgres', + qq[select pg_logical_slot_get_changes('standby3_slot', null, null)]); +like( + $stderr, + qr/ERROR: logical decoding on standby requires "effective_wal_level" >= "logical" on the primary/, + "cannot use logical decoding on standby as it is disabled on primary"); + +# Restart the primary with setting wal_level = 'logical' and create a new logical +# slot. +$primary->append_conf('postgresql.conf', qq[wal_level = 'logical']); +$primary->restart; +$primary->safe_psql('postgres', + qq[select pg_create_logical_replication_slot('test_slot', 'pgoutput')]); + +# effective_wal_level should be 'logical' on both nodes. +$primary->wait_for_replay_catchup($standby3); +test_wal_level($primary, "logical|logical", + "check WAL levels on the primary node"); +test_wal_level($standby3, "replica|logical", + "effective_wal_level got increased to 'logical' again on standby"); + +# Set wal_level to 'replica' and restart the primary. Since one logical slot +# is still present on the primary, effective_wal_level remains 'logical' even +# if wal_level got decreased to 'replica'. +$primary->adjust_conf('postgresql.conf', 'wal_level', 'replica'); +$primary->restart; +$primary->wait_for_replay_catchup($standby3); + +# Verify that the effective_wal_level remains 'logical' on both nodes +test_wal_level($primary, "replica|logical", + "effective_wal_level remains 'logical' on primary even after setting wal_level to 'replica'" +); +test_wal_level($standby3, "replica|logical", + "effective_wal_level remains 'logical' on standby even after setting wal_level to 'replica' on primary" +); + +# Promote the standby3 and verify that effective_wal_level got decreased to +# 'replica' after the promotion since there is no valid logical slot. +$standby3->promote; +test_wal_level($standby3, "replica|replica", + "effective_wal_level got decreased to 'replica' as there is no valid logical slot" +); + +# Cleanup the invalidated slot. +$standby3->safe_psql('postgres', + qq[select pg_drop_replication_slot('standby3_slot')]); + +$standby3->stop; + +# Test the race condition at end of the recovery between the startup and logical +# decoding status change. This test requires injection points enabled. +if ( $ENV{enable_injection_points} eq 'yes' + && $primary->check_extension('injection_points')) +{ + # Initialize standby4 and start it. + my $standby4 = PostgreSQL::Test::Cluster->new('standby4'); + $standby4->init_from_backup($primary, 'my_backup', has_streaming => 1); + $standby4->start; + + # Both servers have one logical slot. + $primary->wait_for_replay_catchup($standby4); + $standby4->create_logical_slot_on_standby($primary, 'standby4_slot', + 'postgres'); + + # Enable and attach the injection point on the standby4. + $primary->safe_psql('postgres', 'create extension injection_points'); + $primary->wait_for_replay_catchup($standby4); + $standby4->safe_psql('postgres', + qq[select injection_points_attach('startup-logical-decoding-status-change-end-of-recovery', 'wait');] + ); + + # Trigger promotion with no wait, and wait for the startup process to reach + # the injection point. + $standby4->safe_psql('postgres', qq[select pg_promote(false)]); + note('promote the standby and waiting for injection_point'); + $standby4->wait_for_event('startup', + 'startup-logical-decoding-status-change-end-of-recovery'); + note( + "injection_point 'startup-logical-decoding-status-change-end-of-recovery' is reached" + ); + + # Drop the logical slot, requesting to disable logical decoding to the checkpointer. + $standby4->safe_psql('postgres', + qq[select pg_drop_replication_slot('standby4_slot');]); + + # Resume the startup process to complete the recovery. + $standby4->safe_psql('postgres', + qq[select injection_points_wakeup('startup-logical-decoding-status-change-end-of-recovery')] + ); + + # Verify that logical decoding got disabled after the recovery. + wait_for_logical_decoding_disabled($standby4); + test_wal_level($standby4, "replica|replica", + "effective_wal_level properly got decreased to 'replica'"); + $standby4->stop; + + # Test the abort process of logical decoding activation. We drop the primary's + # slot to decrease its effective_wal_level to 'replica'. + $primary->safe_psql('postgres', + qq[select pg_drop_replication_slot('test_slot')]); + wait_for_logical_decoding_disabled($primary); + test_wal_level($primary, "replica|replica", + "effective_wal_level got decreased to 'replica' on primary"); + + # Start a psql session to test the case where the activation process is + # interrupted. + my $psql_create_slot = $primary->background_psql('postgres'); + + # Start the logical decoding activation process upon creating the logical + # slot, but it will wait due to the injection point. + $psql_create_slot->query_until( + qr/create_slot_canceled/, + q(\echo create_slot_canceled +select injection_points_set_local(); +select injection_points_attach('logical-decoding-activation', 'wait'); +select pg_create_logical_replication_slot('slot_canceled', 'pgoutput'); +\q +)); + + $primary->wait_for_event('client backend', 'logical-decoding-activation'); + note("injection_point 'logical-decoding-activation' is reached"); + + # Cancel the backend initiated by $psql_create_slot, aborting its activation + # process. + $primary->safe_psql( + 'postgres', + qq[ +select pg_cancel_backend(pid) from pg_stat_activity where query ~ 'slot_canceled' and pid <> pg_backend_pid() +]); + + # Verify that the backend aborted the activation process. + $primary->wait_for_log("aborting logical decoding activation process"); + test_wal_level($primary, "replica|replica", + "the activation process aborted"); +} + +$primary->stop; + +done_testing(); diff --git a/src/test/recovery/t/052_checkpoint_segment_missing.pl b/src/test/recovery/t/052_checkpoint_segment_missing.pl new file mode 100644 index 0000000000000..da54d141f0dea --- /dev/null +++ b/src/test/recovery/t/052_checkpoint_segment_missing.pl @@ -0,0 +1,59 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group +# +# Verify crash recovery behavior when the WAL segment containing the +# checkpoint record referenced by pg_controldata is missing. This +# checks the code path where there is no backup_label file, where the +# startup process should fail with FATAL and log a message about the +# missing checkpoint record. + +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('testnode'); +$node->init; +$node->append_conf('postgresql.conf', 'log_checkpoints = on'); +$node->start; + +# Force a checkpoint so as pg_controldata points to a checkpoint record we +# can target. +$node->safe_psql('postgres', 'CHECKPOINT;'); + +# Retrieve the checkpoint LSN and derive the WAL segment name. +my $checkpoint_walfile = $node->safe_psql('postgres', + "SELECT pg_walfile_name(checkpoint_lsn) FROM pg_control_checkpoint()"); + +ok($checkpoint_walfile ne '', + "derived checkpoint WAL file name: $checkpoint_walfile"); + +# Stop the node. +$node->stop('immediate'); + +# Remove the WAL segment containing the checkpoint record. +my $walpath = $node->data_dir . "/pg_wal/$checkpoint_walfile"; +ok(-f $walpath, "checkpoint WAL file exists before deletion: $walpath"); + +unlink $walpath + or die "could not remove WAL file $walpath: $!"; + +ok(!-e $walpath, "checkpoint WAL file removed: $walpath"); + +# Use run_log instead of node->start because this test expects that +# the server ends with an error during recovery. +run_log( + [ + 'pg_ctl', + '--pgdata' => $node->data_dir, + '--log' => $node->logfile, + 'start', + ]); + +# Confirm that recovery has failed as expected. +my $logfile = slurp_file($node->logfile()); +ok( $logfile =~ + qr/FATAL: .* could not locate a valid checkpoint record at .*/, + "FATAL logged for missing checkpoint record (no backup_label path)"); + +done_testing(); diff --git a/src/test/regress/GNUmakefile b/src/test/regress/GNUmakefile index ef2bddf42cabf..a8ba19e5971fd 100644 --- a/src/test/regress/GNUmakefile +++ b/src/test/regress/GNUmakefile @@ -3,7 +3,7 @@ # GNUmakefile-- # Makefile for src/test/regress (the regression tests) # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/regress/GNUmakefile diff --git a/src/test/regress/Makefile b/src/test/regress/Makefile index 6409a485e8441..7c665ff892d3c 100644 --- a/src/test/regress/Makefile +++ b/src/test/regress/Makefile @@ -7,6 +7,11 @@ # GNU make uses a make file named "GNUmakefile" in preference to "Makefile" # if it exists. Postgres is shipped with a "GNUmakefile". + +# AIX make defaults to building *every* target of the first rule. Start with +# a single-target, empty rule to make the other targets non-default. +all: + all install clean check installcheck: @echo "You must use GNU make to use Postgres. It may be installed" @echo "on your system with the name 'gmake'." diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out index 1f1ce2380af68..ff80869fb3390 100644 --- a/src/test/regress/expected/aggregates.out +++ b/src/test/regress/expected/aggregates.out @@ -515,6 +515,62 @@ SELECT covar_pop(1::float8,'nan'::float8), covar_samp(3::float8,'nan'::float8); NaN | (1 row) +-- check some cases that formerly had poor roundoff-error behavior +SELECT corr(0.09, g), regr_r2(0.09, g) + FROM generate_series(1, 30) g; + corr | regr_r2 +------+--------- + | 1 +(1 row) + +SELECT corr(g, 0.09), regr_r2(g, 0.09), regr_slope(g, 0.09), regr_intercept(g, 0.09) + FROM generate_series(1, 30) g; + corr | regr_r2 | regr_slope | regr_intercept +------+---------+------------+---------------- + | | | +(1 row) + +SELECT corr(1.3 + g * 1e-16, 1.3 + g * 1e-16) + FROM generate_series(1, 3) g; + corr +------ + +(1 row) + +SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) + FROM generate_series(1, 3) g; + corr +------ + 1 +(1 row) + +SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) + FROM generate_series(1, 30) g; + corr +------ + 1 +(1 row) + +-- these examples pose definitional questions for NaN inputs, +-- which we resolve by saying that an all-NaN input column is not all equal +SELECT corr(g, 'NaN') FROM generate_series(1, 30) g; + corr +------ + NaN +(1 row) + +SELECT corr(0.1, 'NaN') FROM generate_series(1, 30) g; + corr +------ + +(1 row) + +SELECT corr('NaN', 'NaN') FROM generate_series(1, 30) g; + corr +------ + NaN +(1 row) + -- test accum and combine functions directly CREATE TABLE regr_test (x float8, y float8); INSERT INTO regr_test VALUES (10,150),(20,250),(30,350),(80,540),(100,200); @@ -538,10 +594,10 @@ SELECT float8_accum('{4,140,2900}'::float8[], 100); {5,240,6280} (1 row) -SELECT float8_regr_accum('{4,140,2900,1290,83075,15050}'::float8[], 200, 100); - float8_regr_accum ------------------------------- - {5,240,6280,1490,95080,8680} +SELECT float8_regr_accum('{4,140,2900,1290,83075,15050,100,0}'::float8[], 200, 100); + float8_regr_accum +--------------------------------------- + {5,240,2900,1490,95080,15050,100,NaN} (1 row) SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) @@ -576,25 +632,25 @@ SELECT float8_combine('{3,60,200}'::float8[], '{2,180,200}'::float8[]); {5,240,6280} (1 row) -SELECT float8_regr_combine('{3,60,200,750,20000,2000}'::float8[], - '{0,0,0,0,0,0}'::float8[]); - float8_regr_combine ---------------------------- - {3,60,200,750,20000,2000} +SELECT float8_regr_combine('{3,60,200,750,20000,2000,1,NaN}'::float8[], + '{0,0,0,0,0,0,0,0}'::float8[]); + float8_regr_combine +--------------------------------- + {3,60,200,750,20000,2000,1,NaN} (1 row) -SELECT float8_regr_combine('{0,0,0,0,0,0}'::float8[], - '{2,180,200,740,57800,-3400}'::float8[]); - float8_regr_combine ------------------------------ - {2,180,200,740,57800,-3400} +SELECT float8_regr_combine('{0,0,0,0,0,0,0,0}'::float8[], + '{2,180,200,740,57800,-3400,NaN,1}'::float8[]); + float8_regr_combine +----------------------------------- + {2,180,200,740,57800,-3400,NaN,1} (1 row) -SELECT float8_regr_combine('{3,60,200,750,20000,2000}'::float8[], - '{2,180,200,740,57800,-3400}'::float8[]); - float8_regr_combine ------------------------------- - {5,240,6280,1490,95080,8680} +SELECT float8_regr_combine('{3,60,200,750,20000,2000,7,8}'::float8[], + '{2,180,200,740,57800,-3400,7,9}'::float8[]); + float8_regr_combine +------------------------------------ + {5,240,6280,1490,95080,8680,7,NaN} (1 row) DROP TABLE regr_test; @@ -680,6 +736,25 @@ SELECT sum2(q1,q2) FROM int8_tbl; 18271560493827981 (1 row) +-- sanity checks +SELECT sum(q1+q2), sum(q1)+sum(q2) FROM int8_tbl; + sum | ?column? +-------------------+------------------- + 18271560493827981 | 18271560493827981 +(1 row) + +SELECT sum(q1-q2), sum(q2-q1), sum(q1)-sum(q2) FROM int8_tbl; + sum | sum | ?column? +------------------+-------------------+------------------ + 9135780246913245 | -9135780246913245 | 9135780246913245 +(1 row) + +SELECT sum(q1*2000), sum(-q1*2000), 2000*sum(q1) FROM int8_tbl; + sum | sum | ?column? +----------------------+-----------------------+---------------------- + 27407340740741226000 | -27407340740741226000 | 27407340740741226000 +(1 row) + -- test for outer-level aggregates -- this should work select ten, sum(distinct four) from onek a @@ -763,9 +838,9 @@ select array(select sum(x+y) s QUERY PLAN ------------------------------------------------------------------- Function Scan on pg_catalog.generate_series x - Output: ARRAY(SubPlan 1) + Output: ARRAY(SubPlan array_1) Function Call: generate_series(1, 3) - SubPlan 1 + SubPlan array_1 -> Sort Output: (sum((x.x + y.y))), y.y Sort Key: (sum((x.x + y.y))) @@ -787,6 +862,26 @@ select array(select sum(x+y) s {4,5,6} (3 rows) +-- Test handling of grouping-expression references within sublinks +select two + four as g, (select f1 from int4_tbl where f1 = (two + four)) +from tenk1 t1 +group by two + four order by 1; + g | f1 +---+---- + 0 | 0 + 2 | + 4 | +(3 rows) + +select q1, (select q1) as ss -- q1 is actually a COALESCE expression here +from int8_tbl i81 full outer join int8_tbl i82 using (q1) +group by q1 order by q1; + q1 | ss +------------------+------------------ + 123 | 123 + 4567890123456789 | 4567890123456789 +(2 rows) + -- -- test for bitwise integer aggregates -- @@ -940,11 +1035,12 @@ explain (costs off) QUERY PLAN ------------------------------------------------------------ Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan using tenk1_unique1 on tenk1 Index Cond: (unique1 IS NOT NULL) -(5 rows) +(6 rows) select min(unique1) from tenk1; min @@ -957,11 +1053,12 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique1 on tenk1 Index Cond: (unique1 IS NOT NULL) -(5 rows) +(6 rows) select max(unique1) from tenk1; max @@ -974,11 +1071,12 @@ explain (costs off) QUERY PLAN ------------------------------------------------------------------------ Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique1 on tenk1 Index Cond: ((unique1 IS NOT NULL) AND (unique1 < 42)) -(5 rows) +(6 rows) select max(unique1) from tenk1 where unique1 < 42; max @@ -991,11 +1089,12 @@ explain (costs off) QUERY PLAN ------------------------------------------------------------------------ Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique1 on tenk1 Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42)) -(5 rows) +(6 rows) select max(unique1) from tenk1 where unique1 > 42; max @@ -1014,11 +1113,12 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique1 on tenk1 Index Cond: ((unique1 IS NOT NULL) AND (unique1 > 42000)) -(5 rows) +(6 rows) select max(unique1) from tenk1 where unique1 > 42000; max @@ -1033,11 +1133,12 @@ explain (costs off) QUERY PLAN ---------------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_thous_tenthous on tenk1 Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL)) -(5 rows) +(6 rows) select max(tenthous) from tenk1 where thousand = 33; max @@ -1050,11 +1151,12 @@ explain (costs off) QUERY PLAN -------------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan using tenk1_thous_tenthous on tenk1 Index Cond: ((thousand = 33) AND (tenthous IS NOT NULL)) -(5 rows) +(6 rows) select min(tenthous) from tenk1 where thousand = 33; min @@ -1069,13 +1171,14 @@ explain (costs off) QUERY PLAN ----------------------------------------------------------------------------------------- Seq Scan on int4_tbl - SubPlan 2 + SubPlan expr_1 -> Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan using tenk1_unique1 on tenk1 Index Cond: ((unique1 IS NOT NULL) AND (unique1 > int4_tbl.f1)) -(7 rows) +(8 rows) select f1, (select min(unique1) from tenk1 where unique1 > f1) AS gt from int4_tbl; @@ -1094,13 +1197,14 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------- HashAggregate - Group Key: (InitPlan 1).col1 - InitPlan 1 + Group Key: (InitPlan minmax_1).col1 + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique2 on tenk1 Index Cond: (unique2 IS NOT NULL) -> Result -(7 rows) + Replaces: MinMaxAggregate +(8 rows) select distinct max(unique2) from tenk1; max @@ -1113,13 +1217,14 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------- Sort - Sort Key: ((InitPlan 1).col1) - InitPlan 1 + Sort Key: ((InitPlan minmax_1).col1) + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique2 on tenk1 Index Cond: (unique2 IS NOT NULL) -> Result -(7 rows) + Replaces: MinMaxAggregate +(8 rows) select max(unique2) from tenk1 order by 1; max @@ -1132,13 +1237,14 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------- Sort - Sort Key: ((InitPlan 1).col1) - InitPlan 1 + Sort Key: ((InitPlan minmax_1).col1) + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique2 on tenk1 Index Cond: (unique2 IS NOT NULL) -> Result -(7 rows) + Replaces: MinMaxAggregate +(8 rows) select max(unique2) from tenk1 order by max(unique2); max @@ -1151,13 +1257,14 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------- Sort - Sort Key: (((InitPlan 1).col1 + 1)) - InitPlan 1 + Sort Key: (((InitPlan minmax_1).col1 + 1)) + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique2 on tenk1 Index Cond: (unique2 IS NOT NULL) -> Result -(7 rows) + Replaces: MinMaxAggregate +(8 rows) select max(unique2) from tenk1 order by max(unique2)+1; max @@ -1171,13 +1278,14 @@ explain (costs off) --------------------------------------------------------------------- Sort Sort Key: (generate_series(1, 3)) DESC - InitPlan 1 + InitPlan minmax_1 -> Limit -> Index Only Scan Backward using tenk1_unique2 on tenk1 Index Cond: (unique2 IS NOT NULL) -> ProjectSet -> Result -(8 rows) + Replaces: MinMaxAggregate +(9 rows) select max(unique2), generate_series(1,3) as g from tenk1 order by g desc; max | g @@ -1187,18 +1295,18 @@ select max(unique2), generate_series(1,3) as g from tenk1 order by g desc; 9999 | 1 (3 rows) --- interesting corner case: constant gets optimized into a seqscan +-- two interesting corner cases: both non-null and null constant gets +-- optimized into a seqscan explain (costs off) select max(100) from tenk1; - QUERY PLAN ----------------------------------------------------- + QUERY PLAN +--------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit - -> Result - One-Time Filter: (100 IS NOT NULL) - -> Seq Scan on tenk1 -(6 rows) + -> Seq Scan on tenk1 +(5 rows) select max(100) from tenk1; max @@ -1206,6 +1314,25 @@ select max(100) from tenk1; 100 (1 row) +explain (costs off) + select max(null) from tenk1; + QUERY PLAN +----------------------------------------------------------- + Result + Replaces: MinMaxAggregate + InitPlan minmax_1 + -> Limit + -> Result + One-Time Filter: (NULL::text IS NOT NULL) + -> Seq Scan on tenk1 +(7 rows) + +select max(null) from tenk1; + max +----- + +(1 row) + -- try it on an inheritance tree create table minmaxtest(f1 int); create table minmaxtest1() inherits (minmaxtest); @@ -1224,7 +1351,8 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Merge Append Sort Key: minmaxtest.f1 @@ -1235,7 +1363,7 @@ explain (costs off) -> Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest_3 Index Cond: (f1 IS NOT NULL) -> Index Only Scan using minmaxtest3i on minmaxtest3 minmaxtest_4 - InitPlan 2 + InitPlan minmax_2 -> Limit -> Merge Append Sort Key: minmaxtest_5.f1 DESC @@ -1246,7 +1374,7 @@ explain (costs off) -> Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8 Index Cond: (f1 IS NOT NULL) -> Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9 -(23 rows) +(24 rows) select min(f1), max(f1) from minmaxtest; min | max @@ -1260,7 +1388,7 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------------------------------- Unique - InitPlan 1 + InitPlan minmax_1 -> Limit -> Merge Append Sort Key: minmaxtest.f1 @@ -1271,7 +1399,7 @@ explain (costs off) -> Index Only Scan Backward using minmaxtest2i on minmaxtest2 minmaxtest_3 Index Cond: (f1 IS NOT NULL) -> Index Only Scan using minmaxtest3i on minmaxtest3 minmaxtest_4 - InitPlan 2 + InitPlan minmax_2 -> Limit -> Merge Append Sort Key: minmaxtest_5.f1 DESC @@ -1283,9 +1411,10 @@ explain (costs off) Index Cond: (f1 IS NOT NULL) -> Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9 -> Sort - Sort Key: ((InitPlan 1).col1), ((InitPlan 2).col1) + Sort Key: ((InitPlan minmax_1).col1), ((InitPlan minmax_2).col1) -> Result -(26 rows) + Replaces: MinMaxAggregate +(27 rows) select distinct min(f1), max(f1) from minmaxtest; min | max @@ -1307,15 +1436,16 @@ explain (costs off) QUERY PLAN --------------------------------------------------------------------- Seq Scan on int4_tbl t0 - SubPlan 2 + SubPlan expr_1 -> HashAggregate - Group Key: (InitPlan 1).col1 - InitPlan 1 + Group Key: (InitPlan minmax_1).col1 + InitPlan minmax_1 -> Limit -> Seq Scan on int4_tbl t1 Filter: ((f1 IS NOT NULL) AND (f1 = t0.f1)) -> Result -(9 rows) + Replaces: MinMaxAggregate +(10 rows) select f1, (select distinct min(t1.f1) from int4_tbl t1 where t1.f1 = t0.f1) from int4_tbl t0; @@ -1521,6 +1651,129 @@ drop table t2; drop table t3; drop table p_t1; -- +-- Test GROUP BY ALL +-- +-- We don't care about the data here, just the proper transformation of the +-- GROUP BY clause, so test some queries and verify the EXPLAIN plans. +-- +CREATE TEMP TABLE t1 ( + a int, + b int, + c int +); +-- basic example +EXPLAIN (COSTS OFF) SELECT b, COUNT(*) FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + HashAggregate + Group Key: b + -> Seq Scan on t1 +(3 rows) + +-- multiple columns, non-consecutive order +EXPLAIN (COSTS OFF) SELECT a, SUM(b), b FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + HashAggregate + Group Key: a, b + -> Seq Scan on t1 +(3 rows) + +-- multi columns, no aggregate +EXPLAIN (COSTS OFF) SELECT a + b FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + HashAggregate + Group Key: (a + b) + -> Seq Scan on t1 +(3 rows) + +-- check we detect a non-top-level aggregate +EXPLAIN (COSTS OFF) SELECT a, SUM(b) + 4 FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + HashAggregate + Group Key: a + -> Seq Scan on t1 +(3 rows) + +-- including grouped column is okay +EXPLAIN (COSTS OFF) SELECT a, SUM(b) + a FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + HashAggregate + Group Key: a + -> Seq Scan on t1 +(3 rows) + +-- including non-grouped column, not so much +EXPLAIN (COSTS OFF) SELECT a, SUM(b) + c FROM t1 GROUP BY ALL; +ERROR: column "t1.c" must appear in the GROUP BY clause or be used in an aggregate function +LINE 1: EXPLAIN (COSTS OFF) SELECT a, SUM(b) + c FROM t1 GROUP BY AL... + ^ +-- all aggregates, should reduce to GROUP BY () +EXPLAIN (COSTS OFF) SELECT COUNT(a), SUM(b) FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + Aggregate + Group Key: () + -> Seq Scan on t1 +(3 rows) + +-- likewise with empty target list +EXPLAIN (COSTS OFF) SELECT FROM t1 GROUP BY ALL; + QUERY PLAN +----------------------- + Result + Replaces: Aggregate +(2 rows) + +-- window functions are not to be included in GROUP BY, either +EXPLAIN (COSTS OFF) SELECT a, COUNT(a) OVER (PARTITION BY a) FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------------------- + WindowAgg + Window: w1 AS (PARTITION BY a) + -> Sort + Sort Key: a + -> HashAggregate + Group Key: a + -> Seq Scan on t1 +(7 rows) + +-- all cols +EXPLAIN (COSTS OFF) SELECT *, count(*) FROM t1 GROUP BY ALL; + QUERY PLAN +---------------------- + HashAggregate + Group Key: a, b, c + -> Seq Scan on t1 +(3 rows) + +-- group by all with grouping element(s) (equivalent to GROUP BY's +-- default behavior, explicit antithesis to GROUP BY DISTINCT) +EXPLAIN (COSTS OFF) SELECT a, count(*) FROM t1 GROUP BY ALL a; + QUERY PLAN +---------------------- + HashAggregate + Group Key: a + -> Seq Scan on t1 +(3 rows) + +-- verify deparsing of GROUP BY ALL +CREATE TEMP VIEW v1 AS SELECT b, COUNT(*) FROM t1 GROUP BY ALL; +SELECT pg_get_viewdef('v1'::regclass); + pg_get_viewdef +----------------------- + SELECT b, + + count(*) AS count+ + FROM t1 + + GROUP BY ALL; +(1 row) + +DROP VIEW v1; +DROP TABLE t1; +-- -- Test GROUP BY matching of join columns that are type-coerced due to USING -- create temp table t1(f1 int, f2 int); @@ -1561,6 +1814,19 @@ group by f2; ----+------- (0 rows) +-- check that we preserve join alias in GROUP BY expressions +create temp view v1 as +select f1::int from t1 left join t2 using (f1) group by f1; +select pg_get_viewdef('v1'::regclass); + pg_get_viewdef +------------------------------- + SELECT (f1)::integer AS f1 + + FROM (t1 + + LEFT JOIN t2 USING (f1))+ + GROUP BY f1; +(1 row) + +drop view v1; drop table t1, t2; -- -- Test planner's selection of pathkeys for ORDER BY aggregates @@ -2662,6 +2928,101 @@ select pg_typeof(cleast_agg(variadic array[4.5,f1])) from int4_tbl; numeric (1 row) +-- +-- Test SupportRequestSimplifyAggref code +-- +begin; +create table agg_simplify (a int, not_null_col int not null, nullable_col int); +-- Ensure count(not_null_col) uses count(*) +explain (costs off, verbose) +select count(not_null_col) from agg_simplify; + QUERY PLAN +----------------------------------------------- + Aggregate + Output: count(*) + -> Seq Scan on public.agg_simplify + Output: a, not_null_col, nullable_col +(4 rows) + +-- Ensure count() uses count(*) +explain (costs off, verbose) +select count('bananas') from agg_simplify; + QUERY PLAN +----------------------------------------------- + Aggregate + Output: count(*) + -> Seq Scan on public.agg_simplify + Output: a, not_null_col, nullable_col +(4 rows) + +-- Ensure count(null) isn't optimized +explain (costs off, verbose) +select count(null) from agg_simplify; + QUERY PLAN +----------------------------------------------- + Aggregate + Output: count(NULL::unknown) + -> Seq Scan on public.agg_simplify + Output: a, not_null_col, nullable_col +(4 rows) + +-- Ensure count(nullable_col) does not use count(*) +explain (costs off, verbose) +select count(nullable_col) from agg_simplify; + QUERY PLAN +----------------------------------------------- + Aggregate + Output: count(nullable_col) + -> Seq Scan on public.agg_simplify + Output: a, not_null_col, nullable_col +(4 rows) + +-- Ensure there's no optimization with DISTINCT aggs +explain (costs off, verbose) +select count(distinct not_null_col) from agg_simplify; + QUERY PLAN +--------------------------------------------- + Aggregate + Output: count(DISTINCT not_null_col) + -> Sort + Output: not_null_col + Sort Key: agg_simplify.not_null_col + -> Seq Scan on public.agg_simplify + Output: not_null_col +(7 rows) + +-- Ensure there's no optimization with ORDER BY aggs +explain (costs off, verbose) +select count(not_null_col order by not_null_col) from agg_simplify; + QUERY PLAN +----------------------------------------------------- + Aggregate + Output: count(not_null_col ORDER BY not_null_col) + -> Sort + Output: not_null_col + Sort Key: agg_simplify.not_null_col + -> Seq Scan on public.agg_simplify + Output: not_null_col +(7 rows) + +-- Ensure we don't optimize to count(*) with agglevelsup > 0 +explain (costs off, verbose) +select a from agg_simplify a group by a +having exists (select 1 from onek b where count(a.not_null_col) = b.four); + QUERY PLAN +----------------------------------------------------- + HashAggregate + Output: a.a + Group Key: a.a + Filter: EXISTS(SubPlan exists_1) + -> Seq Scan on public.agg_simplify a + Output: a.a, a.not_null_col, a.nullable_col + SubPlan exists_1 + -> Seq Scan on public.onek b + Filter: (count(a.not_null_col) = b.four) +(9 rows) + +rollback; -- test aggregates with common transition functions share the same states begin work; create type avg_state as (total bigint, count bigint); @@ -3379,26 +3740,6 @@ select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*) ba | 0 | 1 (2 rows) --- Make sure that generation of HashAggregate for uniqification purposes --- does not lead to array overflow due to unexpected duplicate hash keys --- see CAFeeJoKKu0u+A_A9R9316djW-YW3-+Gtgvy3ju655qRHR3jtdA@mail.gmail.com -set enable_memoize to off; -explain (costs off) - select 1 from tenk1 - where (hundred, thousand) in (select twothousand, twothousand from onek); - QUERY PLAN -------------------------------------------------------------- - Hash Join - Hash Cond: (tenk1.hundred = onek.twothousand) - -> Seq Scan on tenk1 - Filter: (hundred = thousand) - -> Hash - -> HashAggregate - Group Key: onek.twothousand, onek.twothousand - -> Seq Scan on onek -(8 rows) - -reset enable_memoize; -- -- Hash Aggregation Spill tests -- diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out index 23bf33f10a919..97d83f4e9b4f2 100644 --- a/src/test/regress/expected/alter_generic.out +++ b/src/test/regress/expected/alter_generic.out @@ -524,6 +524,49 @@ ERROR: left and right associated data types for operator class options parsing ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) test_opclass_options_func(internal); -- Ok ALTER OPERATOR FAMILY alt_opf19 USING btree DROP FUNCTION 5 (int4, int4); DROP OPERATOR FAMILY alt_opf19 USING btree; +-- +-- Property Graph +-- +SET SESSION AUTHORIZATION regress_alter_generic_user1; +CREATE PROPERTY GRAPH alt_graph1; +CREATE PROPERTY GRAPH alt_graph2; +CREATE PROPERTY GRAPH alt_graph3; +ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph2; -- failed (name conflict) +ERROR: relation "alt_graph2" already exists +ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph4; -- OK +ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user2; -- failed (no role membership) +ERROR: must be able to SET ROLE "regress_alter_generic_user2" +ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user3; -- OK +ALTER PROPERTY GRAPH alt_graph4 SET SCHEMA alt_nsp2; -- OK +ALTER PROPERTY GRAPH alt_nsp2.alt_graph4 RENAME TO alt_graph2; -- OK +ALTER PROPERTY GRAPH alt_graph2 SET SCHEMA alt_nsp2; -- failed (name conflict) +ERROR: relation "alt_graph2" already exists in schema "alt_nsp2" +SET SESSION AUTHORIZATION regress_alter_generic_user2; +CREATE PROPERTY GRAPH alt_graph5; +ALTER PROPERTY GRAPH alt_graph3 RENAME TO alt_graph5; -- failed (not owner) +ERROR: must be owner of property graph alt_graph3 +ALTER PROPERTY GRAPH alt_graph5 RENAME TO alt_graph6; -- OK +ALTER PROPERTY GRAPH alt_graph3 OWNER TO regress_alter_generic_user2; -- failed (not owner) +ERROR: must be owner of property graph alt_graph3 +ALTER PROPERTY GRAPH alt_graph6 OWNER TO regress_alter_generic_user3; -- failed (no role membership) +ERROR: must be able to SET ROLE "regress_alter_generic_user3" +ALTER PROPERTY GRAPH alt_graph3 SET SCHEMA alt_nsp2; -- failed (not owner) +ERROR: must be owner of property graph alt_graph3 +RESET SESSION AUTHORIZATION; +SELECT nspname, relname, rolname + FROM pg_class c, pg_namespace n, pg_authid a + WHERE c.relnamespace = n.oid AND c.relowner = a.oid + AND n.nspname in ('alt_nsp1', 'alt_nsp2') + AND c.relkind = 'g' + ORDER BY nspname, relname; + nspname | relname | rolname +----------+------------+----------------------------- + alt_nsp1 | alt_graph2 | regress_alter_generic_user3 + alt_nsp1 | alt_graph3 | regress_alter_generic_user1 + alt_nsp1 | alt_graph6 | regress_alter_generic_user2 + alt_nsp2 | alt_graph2 | regress_alter_generic_user1 +(4 rows) + -- -- Statistics -- @@ -714,7 +757,7 @@ NOTICE: drop cascades to server alt_fserv3 DROP LANGUAGE alt_lang2 CASCADE; DROP LANGUAGE alt_lang3 CASCADE; DROP SCHEMA alt_nsp1 CASCADE; -NOTICE: drop cascades to 28 other objects +NOTICE: drop cascades to 31 other objects DETAIL: drop cascades to function alt_func3(integer) drop cascades to function alt_agg3(integer) drop cascades to function alt_func4(integer) @@ -731,6 +774,9 @@ drop cascades to operator family alt_opc1 for access method hash drop cascades to operator family alt_opc2 for access method hash drop cascades to operator family alt_opf4 for access method hash drop cascades to operator family alt_opf2 for access method hash +drop cascades to property graph alt_graph2 +drop cascades to property graph alt_graph3 +drop cascades to property graph alt_graph6 drop cascades to table alt_regress_1 drop cascades to table alt_regress_2 drop cascades to text search dictionary alt_ts_dict3 @@ -744,12 +790,13 @@ drop cascades to text search template alt_ts_temp2 drop cascades to text search parser alt_ts_prs3 drop cascades to text search parser alt_ts_prs2 DROP SCHEMA alt_nsp2 CASCADE; -NOTICE: drop cascades to 9 other objects +NOTICE: drop cascades to 10 other objects DETAIL: drop cascades to function alt_nsp2.alt_func2(integer) drop cascades to function alt_nsp2.alt_agg2(integer) drop cascades to conversion alt_nsp2.alt_conv2 drop cascades to operator alt_nsp2.@-@(integer,integer) drop cascades to operator family alt_nsp2.alt_opf2 for access method hash +drop cascades to property graph alt_nsp2.alt_graph2 drop cascades to text search dictionary alt_nsp2.alt_ts_dict2 drop cascades to text search configuration alt_nsp2.alt_ts_conf2 drop cascades to text search template alt_nsp2.alt_ts_temp2 diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out index 476266e3f4b03..dad9d36937e91 100644 --- a/src/test/regress/expected/alter_table.out +++ b/src/test/regress/expected/alter_table.out @@ -350,7 +350,8 @@ NOTICE: merging constraint "con1" with inherited definition d | integer | | | Check constraints: "con1" CHECK (a > 0) -Inherits: constraint_rename_test +Inherits: + constraint_rename_test ALTER TABLE constraint_rename_test2 RENAME CONSTRAINT con1 TO con1foo; -- fail ERROR: cannot rename inherited constraint "con1" @@ -378,7 +379,8 @@ Number of child tables: 1 (Use \d+ to list them.) d | integer | | | Check constraints: "con1foo" CHECK (a > 0) -Inherits: constraint_rename_test +Inherits: + constraint_rename_test ALTER TABLE constraint_rename_test ADD CONSTRAINT con2 CHECK (b > 0) NO INHERIT; ALTER TABLE ONLY constraint_rename_test RENAME CONSTRAINT con2 TO con2foo; -- ok @@ -405,7 +407,8 @@ Number of child tables: 1 (Use \d+ to list them.) d | integer | | | Check constraints: "con1foo" CHECK (a > 0) -Inherits: constraint_rename_test +Inherits: + constraint_rename_test ALTER TABLE constraint_rename_test ADD CONSTRAINT con3 PRIMARY KEY (a); ALTER TABLE constraint_rename_test RENAME CONSTRAINT con3 TO con3foo; -- ok @@ -433,7 +436,8 @@ Number of child tables: 1 (Use \d+ to list them.) d | integer | | | Check constraints: "con1foo" CHECK (a > 0) -Inherits: constraint_rename_test +Inherits: + constraint_rename_test DROP TABLE constraint_rename_test2; DROP TABLE constraint_rename_test; @@ -650,7 +654,8 @@ alter table nv_parent add check (d between '2001-01-01'::date and '2099-12-31':: Check constraints: "nv_child_2009_d_check" CHECK (d >= '01-01-2009'::date AND d <= '12-31-2009'::date) "nv_parent_d_check" CHECK (d >= '01-01-2001'::date AND d <= '12-31-2099'::date) NOT VALID -Inherits: nv_parent +Inherits: + nv_parent -- we leave nv_parent and children around to help test pg_dump logic -- Foreign key adding test with mixed types @@ -1236,7 +1241,8 @@ primary key, btree, for table "public.atnnpart1" Partition key: LIST (id) Not-null constraints: "dummy_constr" NOT NULL "id" NOT VALID -Partitions: atnnpart1 FOR VALUES IN (1) +Partitions: + atnnpart1 FOR VALUES IN (1) BEGIN; ALTER TABLE atnnparted VALIDATE CONSTRAINT dummy_constr; @@ -1267,7 +1273,8 @@ primary key, btree, for table "public.atnnpart1" Partition key: LIST (id) Not-null constraints: "dummy_constr" NOT NULL "id" -Partitions: atnnpart1 FOR VALUES IN (1) +Partitions: + atnnpart1 FOR VALUES IN (1) ROLLBACK; -- leave a table in this state for the pg_upgrade test @@ -1585,8 +1592,12 @@ ERROR: column "........pg.dropped.1........" referenced in foreign key constrai drop table atacc2; create index "testing_idx" on atacc1(a); ERROR: column "a" does not exist +LINE 1: create index "testing_idx" on atacc1(a); + ^ create index "testing_idx" on atacc1("........pg.dropped.1........"); ERROR: column "........pg.dropped.1........" does not exist +LINE 1: create index "testing_idx" on atacc1("........pg.dropped.1..... + ^ -- test create as and select into insert into atacc1 values (21, 22, 23); create table attest1 as select * from atacc1; @@ -2041,7 +2052,8 @@ alter table anothertab alter column atcol1 drop default; alter table anothertab alter column atcol1 type boolean using case when atcol1 % 2 = 0 then true else false end; -- fails ERROR: operator does not exist: boolean <= integer -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. alter table anothertab drop constraint anothertab_chk; alter table anothertab drop constraint anothertab_chk; -- fails ERROR: constraint "anothertab_chk" of relation "anothertab" does not exist @@ -2401,7 +2413,8 @@ Number of child tables: 1 (Use \d+ to list them.) b | double precision | | | Check constraints: "test_inh_check_a_check" CHECK (a > 10.2::double precision) -Inherits: test_inh_check +Inherits: + test_inh_check select relname, conname, coninhcount, conislocal, connoinherit from pg_constraint c, pg_class r @@ -2432,7 +2445,8 @@ Number of child tables: 1 (Use \d+ to list them.) b | double precision | | | Check constraints: "test_inh_check_a_check" CHECK (a::double precision > 10.2::double precision) -Inherits: test_inh_check +Inherits: + test_inh_check select relname, conname, coninhcount, conislocal, connoinherit from pg_constraint c, pg_class r @@ -2472,7 +2486,8 @@ Check constraints: "blocal" CHECK (b < 1000::double precision) "bmerged" CHECK (b > 1::double precision) "test_inh_check_a_check" CHECK (a::double precision > 10.2::double precision) -Inherits: test_inh_check +Inherits: + test_inh_check select relname, conname, coninhcount, conislocal, connoinherit from pg_constraint c, pg_class r @@ -2512,7 +2527,8 @@ Check constraints: "blocal" CHECK (b::double precision < 1000::double precision) "bmerged" CHECK (b::double precision > 1::double precision) "test_inh_check_a_check" CHECK (a::double precision > 10.2::double precision) -Inherits: test_inh_check +Inherits: + test_inh_check select relname, conname, coninhcount, conislocal, connoinherit from pg_constraint c, pg_class r @@ -3301,7 +3317,8 @@ Typed table of type: test_type2 --------+---------+-----------+----------+--------- aa | integer | | | c | text | | | -Inherits: test_tbl2 +Inherits: + test_tbl2 DROP TABLE test_tbl2_subclass, test_tbl2; DROP TYPE test_type2; @@ -3567,12 +3584,15 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F -- filenode function call can return NULL for a relation dropped concurrently -- with the call's surrounding query, so ignore a NULL mapped_oid for -- relations that no longer exist after all calls finish. +-- Temporary relations are ignored, as not supported by pg_filenode_relation(). CREATE TEMP TABLE filenode_mapping AS SELECT oid, mapped_oid, reltablespace, relfilenode, relname FROM pg_class, pg_filenode_relation(reltablespace, pg_relation_filenode(oid)) AS mapped_oid -WHERE relkind IN ('r', 'i', 'S', 't', 'm') AND mapped_oid IS DISTINCT FROM oid; +WHERE relkind IN ('r', 'i', 'S', 't', 'm') + AND relpersistence != 't' + AND mapped_oid IS DISTINCT FROM oid; SELECT m.* FROM filenode_mapping m LEFT JOIN pg_class c ON c.oid = m.oid WHERE c.oid IS NOT NULL OR m.mapped_oid IS NOT NULL; oid | mapped_oid | reltablespace | relfilenode | relname @@ -3841,6 +3861,16 @@ Referenced by: ALTER TABLE test_add_column ADD COLUMN IF NOT EXISTS c5 SERIAL CHECK (c5 > 10); NOTICE: column "c5" of relation "test_add_column" already exists, skipping +ALTER TABLE test_add_column + ADD c6 integer; -- omit COLUMN +ALTER TABLE test_add_column + ADD IF NOT EXISTS c6 integer; +NOTICE: column "c6" of relation "test_add_column" already exists, skipping +ALTER TABLE test_add_column + DROP c6; -- omit COLUMN +ALTER TABLE test_add_column + DROP IF EXISTS c6; +NOTICE: column "c6" of relation "test_add_column" does not exist, skipping \d test_add_column* Table "public.test_add_column" Column | Type | Collation | Nullable | Default @@ -3989,10 +4019,19 @@ CREATE TABLE nonpartitioned ( a int, b int ); -ALTER TABLE partitioned INHERIT nonpartitioned; -ERROR: cannot change inheritance of partitioned table -ALTER TABLE nonpartitioned INHERIT partitioned; +ALTER TABLE partitioned INHERIT nonpartitioned; -- fail +ERROR: ALTER action INHERIT cannot be performed on relation "partitioned" +DETAIL: This operation is not supported for partitioned tables. +ALTER TABLE partitioned NO INHERIT nonpartitioned; -- fail +ERROR: ALTER action NO INHERIT cannot be performed on relation "partitioned" +DETAIL: This operation is not supported for partitioned tables. +ALTER TABLE nonpartitioned INHERIT partitioned; -- fail ERROR: cannot inherit from partitioned table "partitioned" +CREATE TABLE partitioned_p1 PARTITION OF partitioned FOR VALUES FROM (0, 0) TO (10, 100); +ALTER TABLE partitioned_p1 INHERIT nonpartitioned; -- fail +ERROR: cannot change inheritance of a partition +ALTER TABLE partitioned_p1 NO INHERIT nonpartitioned; -- fail +ERROR: cannot change inheritance of a partition -- cannot add NO INHERIT constraint to partitioned tables ALTER TABLE partitioned ADD CONSTRAINT chk_a CHECK (a > 0) NO INHERIT; ERROR: cannot add NO INHERIT constraint to partitioned table "partitioned" @@ -4466,27 +4505,15 @@ ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY; Partition key: RANGE (a) Number of partitions: 0 --- constraint should be created -\d part_rp - Table "public.part_rp" - Column | Type | Collation | Nullable | Default ---------+---------+-----------+----------+--------- - a | integer | | | -Check constraints: - "part_rp_a_check" CHECK (a IS NOT NULL AND a >= 0 AND a < 100) - -CREATE TABLE part_rp100 PARTITION OF range_parted2 (CHECK (a>=123 AND a<133 AND a IS NOT NULL)) FOR VALUES FROM (100) to (200); -ALTER TABLE range_parted2 DETACH PARTITION part_rp100 CONCURRENTLY; --- redundant constraint should not be created -\d part_rp100 - Table "public.part_rp100" - Column | Type | Collation | Nullable | Default ---------+---------+-----------+----------+--------- - a | integer | | | -Check constraints: - "part_rp100_a_check" CHECK (a >= 123 AND a < 133 AND a IS NOT NULL) - DROP TABLE range_parted2; +-- Test that hash partitions continue to work after they're concurrently +-- detached (bugs #18371, #19070) +CREATE TABLE hash_parted2 (a int) PARTITION BY HASH(a); +CREATE TABLE part_hp PARTITION OF hash_parted2 FOR VALUES WITH (MODULUS 2, REMAINDER 0); +ALTER TABLE hash_parted2 DETACH PARTITION part_hp CONCURRENTLY; +DROP TABLE hash_parted2; +INSERT INTO part_hp VALUES (1); +DROP TABLE part_hp; -- Check ALTER TABLE commands for partitioned tables and partitions -- cannot add/drop column to/from *only* the parent ALTER TABLE ONLY list_parted2 ADD COLUMN c int; @@ -4745,6 +4772,21 @@ alter table attbl alter column p1 set data type bigint; alter table atref alter column c1 set data type bigint; drop table attbl, atref; /* End test case for bug #17409 */ +/* Test case for bug #18970 */ +create table attbl(a int); +create table atref(b attbl check ((b).a is not null)); +alter table attbl alter column a type numeric; -- someday this should work +ERROR: cannot alter table "attbl" because column "atref.b" uses its row type +alter table atref drop constraint atref_b_check; +create statistics atref_stat on ((b).a is not null) from atref; +alter table attbl alter column a type numeric; -- someday this should work +ERROR: cannot alter table "attbl" because column "atref.b" uses its row type +drop statistics atref_stat; +create index atref_idx on atref (((b).a)); +alter table attbl alter column a type numeric; -- someday this should work +ERROR: cannot alter table "attbl" because column "atref.b" uses its row type +drop table attbl, atref; +/* End test case for bug #18970 */ -- Test that ALTER TABLE rewrite preserves a clustered index -- for normal indexes and indexes on constraints. create table alttype_cluster (a int); diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out index b815473f414b2..66439d427a317 100644 --- a/src/test/regress/expected/arrays.out +++ b/src/test/regress/expected/arrays.out @@ -1737,6 +1737,11 @@ select '[-2147483648:-2147483647]={1,2}'::int[]; (1 row) -- all of the above should be accepted +-- some day we might allow these cases, but for now they're errors: +select array[]::oidvector; +ERROR: array is not a valid oidvector +select array[]::int2vector; +ERROR: array is not a valid int2vector -- tests for array aggregates CREATE TEMP TABLE arraggtest ( f1 INT[], f2 TEXT[][], f3 FLOAT[]); INSERT INTO arraggtest (f1, f2, f3) VALUES @@ -1782,17 +1787,17 @@ SELECT max(f1), min(f1), max(f2), min(f2), max(f3), min(f3) FROM arraggtest; (1 row) -- A few simple tests for arrays of composite types -create type comptype as (f1 int, f2 text); +create type comptype as (f1 int, f2 text, f3 int[]); create table comptable (c1 comptype, c2 comptype[]); -- XXX would like to not have to specify row() construct types here ... insert into comptable - values (row(1,'foo'), array[row(2,'bar')::comptype, row(3,'baz')::comptype]); + values (row(1,'foo',array[10,20]), array[row(2,'bar',array[30,40])::comptype, row(3,'baz',array[50,60])::comptype]); -- check that implicitly named array type _comptype isn't a problem create type _comptype as enum('fooey'); select * from comptable; - c1 | c2 ----------+----------------------- - (1,foo) | {"(2,bar)","(3,baz)"} + c1 | c2 +-------------------+----------------------------------------------- + (1,foo,"{10,20}") | {"(2,bar,\"{30,40}\")","(3,baz,\"{50,60}\")"} (1 row) select c2[2].f2 from comptable; @@ -1801,6 +1806,22 @@ select c2[2].f2 from comptable; baz (1 row) +select c2[2].f3 from comptable; + f3 +--------- + {50,60} +(1 row) + +select c2[2].f3[1:2] from comptable; + f3 +--------- + {50,60} +(1 row) + +select c2[1:2].f3[1:2] from comptable; +ERROR: column notation .f3 applied to type comptype[], which is not a composite type +LINE 1: select c2[1:2].f3[1:2] from comptable; + ^ drop type _comptype; drop table comptable; drop type comptype; @@ -2747,7 +2768,8 @@ SELECT width_bucket('5'::text, ARRAY[3, 4]::integer[]); ERROR: function width_bucket(text, integer[]) does not exist LINE 1: SELECT width_bucket('5'::text, ARRAY[3, 4]::integer[]); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. SELECT width_bucket(5, ARRAY[3, 4, NULL]); ERROR: thresholds array must not contain NULLs SELECT width_bucket(5, ARRAY[ARRAY[1, 2], ARRAY[3, 4]]); diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out index bfb1a286ea4ad..21dc9b5783a7c 100644 --- a/src/test/regress/expected/btree_index.out +++ b/src/test/regress/expected/btree_index.out @@ -195,54 +195,123 @@ ORDER BY proname DESC, proargtypes DESC, pronamespace DESC LIMIT 1; (1 row) -- --- Add coverage for RowCompare quals whose rhs row has a NULL that ends scan +-- Forwards scan RowCompare qual whose row arg has a NULL that affects our +-- initial positioning strategy -- explain (costs off) SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) < ('abs', NULL) + WHERE (proname, proargtypes) >= ('abs', NULL) AND proname <= 'abs' ORDER BY proname, proargtypes, pronamespace; - QUERY PLAN -------------------------------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------------------------- Index Only Scan using pg_proc_proname_args_nsp_index on pg_proc - Index Cond: ((ROW(proname, proargtypes) < ROW('abs'::name, NULL::oidvector)) AND (proname = 'abs'::name)) + Index Cond: ((ROW(proname, proargtypes) >= ROW('abs'::name, NULL::oidvector)) AND (proname <= 'abs'::name)) (2 rows) SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) < ('abs', NULL) + WHERE (proname, proargtypes) >= ('abs', NULL) AND proname <= 'abs' ORDER BY proname, proargtypes, pronamespace; proname | proargtypes | pronamespace ---------+-------------+-------------- (0 rows) -- --- Add coverage for backwards scan RowCompare quals whose rhs row has a NULL --- that ends scan +-- Forwards scan RowCompare quals whose row arg has a NULL that ends scan -- explain (costs off) SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) > ('abs', NULL) + WHERE proname >= 'abs' AND (proname, proargtypes) < ('abs', NULL) +ORDER BY proname, proargtypes, pronamespace; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Index Only Scan using pg_proc_proname_args_nsp_index on pg_proc + Index Cond: ((proname >= 'abs'::name) AND (ROW(proname, proargtypes) < ROW('abs'::name, NULL::oidvector))) +(2 rows) + +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname >= 'abs' AND (proname, proargtypes) < ('abs', NULL) +ORDER BY proname, proargtypes, pronamespace; + proname | proargtypes | pronamespace +---------+-------------+-------------- +(0 rows) + +-- +-- Backwards scan RowCompare qual whose row arg has a NULL that affects our +-- initial positioning strategy +-- +explain (costs off) +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname >= 'abs' AND (proname, proargtypes) <= ('abs', NULL) +ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------- + Index Only Scan Backward using pg_proc_proname_args_nsp_index on pg_proc + Index Cond: ((proname >= 'abs'::name) AND (ROW(proname, proargtypes) <= ROW('abs'::name, NULL::oidvector))) +(2 rows) + +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname >= 'abs' AND (proname, proargtypes) <= ('abs', NULL) +ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; + proname | proargtypes | pronamespace +---------+-------------+-------------- +(0 rows) + +-- +-- Backwards scan RowCompare qual whose row arg has a NULL that ends scan +-- +explain (costs off) +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE (proname, proargtypes) > ('abs', NULL) AND proname <= 'abs' ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; - QUERY PLAN -------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- Index Only Scan Backward using pg_proc_proname_args_nsp_index on pg_proc - Index Cond: ((ROW(proname, proargtypes) > ROW('abs'::name, NULL::oidvector)) AND (proname = 'abs'::name)) + Index Cond: ((ROW(proname, proargtypes) > ROW('abs'::name, NULL::oidvector)) AND (proname <= 'abs'::name)) (2 rows) SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) > ('abs', NULL) + WHERE (proname, proargtypes) > ('abs', NULL) AND proname <= 'abs' ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; proname | proargtypes | pronamespace ---------+-------------+-------------- (0 rows) +-- Makes B-Tree preprocessing deal with unmarking redundant keys that were +-- initially marked required (test case relies on current row compare +-- preprocessing limitations) +explain (costs off) +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname = 'zzzzzz' AND (proname, proargtypes) > ('abs', NULL) + AND pronamespace IN (1, 2, 3) AND proargtypes IN ('26 23', '5077') +ORDER BY proname, proargtypes, pronamespace; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Index Only Scan using pg_proc_proname_args_nsp_index on pg_proc + Index Cond: ((ROW(proname, proargtypes) > ROW('abs'::name, NULL::oidvector)) AND (proname = 'zzzzzz'::name) AND (proargtypes = ANY ('{"26 23",5077}'::oidvector[])) AND (pronamespace = ANY ('{1,2,3}'::oid[]))) +(2 rows) + +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname = 'zzzzzz' AND (proname, proargtypes) > ('abs', NULL) + AND pronamespace IN (1, 2, 3) AND proargtypes IN ('26 23', '5077') +ORDER BY proname, proargtypes, pronamespace; + proname | proargtypes | pronamespace +---------+-------------+-------------- +(0 rows) + -- --- Add coverage for recheck of > key following array advancement on previous --- (left sibling) page that used a high key whose attribute value corresponding --- to the > key was -inf (due to being truncated when the high key was created). +-- Performs a recheck of > key following array advancement on previous (left +-- sibling) page that used a high key whose attribute value corresponding to +-- the > key was -inf (due to being truncated when the high key was created). -- -- XXX This relies on the assumption that tenk1_thous_tenthous has a truncated -- high key "(183, -inf)" on the first page that we'll scan. The test will only diff --git a/src/test/regress/expected/case.out b/src/test/regress/expected/case.out index efee7fc43173b..4c77f7e3961ad 100644 --- a/src/test/regress/expected/case.out +++ b/src/test/regress/expected/case.out @@ -266,27 +266,30 @@ SELECT * -- Tests for constant subexpression simplification explain (costs off) SELECT * FROM CASE_TBL WHERE NULLIF(1, 2) = 2; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Scan on case_tbl One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) SELECT * FROM CASE_TBL WHERE NULLIF(1, 1) IS NOT NULL; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Scan on case_tbl One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) SELECT * FROM CASE_TBL WHERE NULLIF(1, null) = 2; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Scan on case_tbl One-Time Filter: false -(2 rows) +(3 rows) -- -- Examples of updates involving tables diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index 4d40a6809ab46..712701349851c 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -492,12 +492,58 @@ Number of partitions: 3 (Use \d+ to list them.) CLUSTER clstrpart; ERROR: there is no previously clustered index for table "clstrpart" ALTER TABLE clstrpart SET WITHOUT CLUSTER; -ERROR: cannot mark index clustered in partitioned table +ERROR: ALTER action SET WITHOUT CLUSTER cannot be performed on relation "clstrpart" +DETAIL: This operation is not supported for partitioned tables. ALTER TABLE clstrpart CLUSTER ON clstrpart_idx; -ERROR: cannot mark index clustered in partitioned table +ERROR: ALTER action CLUSTER ON cannot be performed on relation "clstrpart" +DETAIL: This operation is not supported for partitioned tables. +-- and they cannot get an index-ordered REPACK without an explicit index name +REPACK clstrpart USING INDEX; +ERROR: cannot execute REPACK on partitioned table "clstrpart" USING INDEX with no index name +-- Check that REPACK sets new relfilenodes: it should process exactly the same +-- tables as CLUSTER did. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart USING INDEX clstrpart_idx; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + relname | level | relkind | ?column? +-------------+-------+---------+---------- + clstrpart | 0 | p | t + clstrpart1 | 1 | p | t + clstrpart11 | 2 | r | f + clstrpart12 | 2 | p | t + clstrpart2 | 1 | r | f + clstrpart3 | 1 | p | t + clstrpart33 | 2 | r | f +(7 rows) + +-- And finally the same for REPACK w/o index. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + relname | level | relkind | ?column? +-------------+-------+---------+---------- + clstrpart | 0 | p | t + clstrpart1 | 1 | p | t + clstrpart11 | 2 | r | f + clstrpart12 | 2 | p | t + clstrpart2 | 1 | r | f + clstrpart3 | 1 | p | t + clstrpart33 | 2 | r | f +(7 rows) + +-- CONCURRENTLY doesn't like partitioned tables +REPACK (CONCURRENTLY) clstrpart; +ERROR: REPACK (CONCURRENTLY) is not supported for partitioned tables +HINT: Consider running the command on individual partitions. DROP TABLE clstrpart; -- Ownership of partitions is checked -CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i); +CREATE TABLE ptnowner(i int unique not null) PARTITION BY LIST (i); CREATE INDEX ptnowner_i_idx ON ptnowner(i); CREATE TABLE ptnowner1 PARTITION OF ptnowner FOR VALUES IN (1); CREATE ROLE regress_ptnowner; @@ -506,6 +552,8 @@ ALTER TABLE ptnowner1 OWNER TO regress_ptnowner; SET SESSION AUTHORIZATION regress_ptnowner; CLUSTER ptnowner USING ptnowner_i_idx; ERROR: permission denied for table ptnowner +ALTER TABLE ptnowner1 REPLICA IDENTITY USING INDEX ptnowner1_i_key; +REPACK (CONCURRENTLY) ptnowner1; RESET SESSION AUTHORIZATION; ALTER TABLE ptnowner OWNER TO regress_ptnowner; CREATE TEMP TABLE ptnowner_oldnodes AS @@ -513,7 +561,12 @@ CREATE TEMP TABLE ptnowner_oldnodes AS JOIN pg_class AS c ON c.oid=tree.relid; SET SESSION AUTHORIZATION regress_ptnowner; CLUSTER ptnowner USING ptnowner_i_idx; -WARNING: permission denied to cluster "ptnowner2", skipping it +WARNING: permission denied to execute CLUSTER on "ptnowner2", skipping it +-- still can't repack without a replica identity +ALTER TABLE ptnowner1 REPLICA IDENTITY DEFAULT; +REPACK (CONCURRENTLY) ptnowner1; +ERROR: cannot process relation "ptnowner1" +HINT: Relation "ptnowner1" has no identity index. RESET SESSION AUTHORIZATION; SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C"; @@ -524,6 +577,15 @@ SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a ptnowner2 | t (3 rows) +SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a + JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C"; + relname | ?column? +-----------+---------- + ptnowner | t + ptnowner1 | f + ptnowner2 | t +(3 rows) + DROP TABLE ptnowner; DROP ROLE regress_ptnowner; -- Test CLUSTER with external tuplesorting @@ -665,6 +727,105 @@ SELECT * FROM clstr_expression WHERE -a = -3 ORDER BY -a, b; (4 rows) COMMIT; +---------------------------------------------------------------------- +-- +-- REPACK +-- +---------------------------------------------------------------------- +-- REPACK handles individual tables identically to CLUSTER, but it's worth +-- checking if it handles table hierarchies identically as well. +REPACK clstr_tst USING INDEX clstr_tst_c; +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + a | b | c | substring | length +----+-----+------------------+--------------------------------+-------- + 10 | 14 | catorce | | + 18 | 5 | cinco | | + 9 | 4 | cuatro | | + 26 | 19 | diecinueve | | + 12 | 18 | dieciocho | | + 30 | 16 | dieciseis | | + 24 | 17 | diecisiete | | + 2 | 10 | diez | | + 23 | 12 | doce | | + 11 | 2 | dos | | + 25 | 9 | nueve | | + 31 | 8 | ocho | | + 1 | 11 | once | | + 28 | 15 | quince | | + 32 | 6 | seis | xyzzyxyzzyxyzzyxyzzyxyzzyxyzzy | 500000 + 29 | 7 | siete | | + 15 | 13 | trece | | + 22 | 30 | treinta | | + 17 | 32 | treinta y dos | | + 3 | 31 | treinta y uno | | + 5 | 3 | tres | | + 20 | 1 | uno | | + 6 | 20 | veinte | | + 14 | 25 | veinticinco | | + 21 | 24 | veinticuatro | | + 4 | 22 | veintidos | | + 19 | 29 | veintinueve | | + 16 | 28 | veintiocho | | + 27 | 26 | veintiseis | | + 13 | 27 | veintisiete | | + 7 | 23 | veintitres | | + 8 | 21 | veintiuno | | + 0 | 100 | in child table | | + 0 | 100 | in child table 2 | | +(34 rows) + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); +ERROR: insert or update on table "clstr_tst" violates foreign key constraint "clstr_tst_con" +DETAIL: Key (b)=(1111) is not present in table "clstr_tst_s". +SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass +ORDER BY 1; + conname +---------------------- + clstr_tst_a_not_null + clstr_tst_con + clstr_tst_pkey +(3 rows) + +-- Verify partial analyze works +REPACK (ANALYZE) clstr_tst (a); +REPACK (ANALYZE) clstr_tst; +REPACK (VERBOSE) clstr_tst (a); +ERROR: ANALYZE option must be specified when a column list is provided +-- REPACK w/o argument performs no ordering, so we can only check which tables +-- have the relfilenode changed. +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_old AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); +SET SESSION AUTHORIZATION regress_clstr_user; +SET client_min_messages = ERROR; -- order of "skipping" warnings may vary +REPACK; +RESET client_min_messages; +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_new AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been +-- processed because there is nothing like clustering index here. +SELECT o.relname FROM relnodes_old o +JOIN relnodes_new n ON o.relname = n.relname +WHERE o.relfilenode <> n.relfilenode +ORDER BY o.relname; + relname +--------- + clstr_1 + clstr_3 +(2 rows) + +-- concurrently +REPACK (CONCURRENTLY) pg_class; +ERROR: cannot repack relation "pg_class" +HINT: REPACK CONCURRENTLY is not supported for catalog relations. -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 69805d4b9ec53..fce726029a2bb 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -996,6 +996,8 @@ CREATE INDEX collate_test1_idx3 ON collate_test1 ((b COLLATE "C")); -- this is d CREATE INDEX collate_test1_idx4 ON collate_test1 (((b||'foo') COLLATE "POSIX")); CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE "C"); -- fail ERROR: collations are not supported by type integer +LINE 1: CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE ... + ^ CREATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "C")); -- fail ERROR: collations are not supported by type integer LINE 1: ...ATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "C... @@ -1295,6 +1297,14 @@ DROP TABLE test7; CREATE COLLATION testcoll_rulesx (provider = icu, locale = '', rules = '!!wrong!!'); NOTICE: using standard form "und" for ICU locale "" ERROR: could not open collator for locale "und" with rules "!!wrong!!": U_INVALID_FORMAT_ERROR +-- strength specified in the rules +CREATE COLLATION strength_in_rule (provider = icu, locale = 'und', deterministic = false, rules = '[strength 1]'); +SELECT 'a' = 'à' COLLATE strength_in_rule; -- true because of the rule + ?column? +---------- + t +(1 row) + -- nondeterministic collations CREATE COLLATION ctest_det (provider = icu, locale = '', deterministic = true); NOTICE: using standard form "und" for ICU locale "" @@ -1484,6 +1494,13 @@ SELECT array_sort('{a,B}'::text[] COLLATE "C"); {B,a} (1 row) +-- test replace() at the end of the string (bug #19341) +SELECT replace('testX' COLLATE case_insensitive, 'x' COLLATE case_insensitive, 'er'); + replace +--------- + tester +(1 row) + -- test language tags CREATE COLLATION lt_insensitive (provider = icu, locale = 'en-u-ks-level1', deterministic = false); SELECT 'aBcD' COLLATE lt_insensitive = 'AbCd' COLLATE lt_insensitive; @@ -2670,6 +2687,72 @@ DROP TABLE pagg_tab6; RESET enable_partitionwise_aggregate; RESET max_parallel_workers_per_gather; RESET enable_incremental_sort; +-- +-- Test for eager aggregation non-deterministic collation bug +-- +CREATE TABLE eager_agg_t1 (id int, val text COLLATE case_insensitive); +CREATE TABLE eager_agg_t2 (val text COLLATE case_insensitive); +INSERT INTO eager_agg_t1 SELECT 1, 'a' FROM generate_series(1, 50); +INSERT INTO eager_agg_t1 SELECT 1, 'A' FROM generate_series(1, 50); +INSERT INTO eager_agg_t2 VALUES ('A'); +ANALYZE eager_agg_t1; +ANALYZE eager_agg_t2; +-- Ensure that eager aggregation is not used for t1.val due to the +-- non-deterministic collation. +EXPLAIN (COSTS OFF) +SELECT t1.id, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id; + QUERY PLAN +-------------------------------------------------------- + HashAggregate + Group Key: t1.id + -> Nested Loop + Join Filter: ((t1.val)::text = (t2.val)::text) + -> Seq Scan on eager_agg_t2 t2 + -> Seq Scan on eager_agg_t1 t1 +(6 rows) + +-- Ensure it returns 1 row with count = 50 +SELECT t1.id, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id; + id | count +----+------- + 1 | 50 +(1 row) + +-- Ensure that eager aggregation is not used when grouping by a column with +-- non-deterministic collation. +EXPLAIN (COSTS OFF) +SELECT t1.id, t1.val, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id, t1.val; + QUERY PLAN +-------------------------------------------------------- + HashAggregate + Group Key: t1.id, t1.val + -> Nested Loop + Join Filter: ((t1.val)::text = (t2.val)::text) + -> Seq Scan on eager_agg_t2 t2 + -> Seq Scan on eager_agg_t1 t1 +(6 rows) + +-- Ensure it returns 1 row with count = 50 +SELECT t1.id, t1.val, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id, t1.val; + id | val | count +----+-----+------- + 1 | A | 50 +(1 row) + +DROP TABLE eager_agg_t1; +DROP TABLE eager_agg_t2; -- virtual generated columns CREATE TABLE t5 ( a int, @@ -2690,6 +2773,55 @@ SELECT * FROM t5 ORDER BY c ASC, a ASC; 3 | d1 | d1 (3 rows) +-- Check that DEFAULT expressions in SQL/JSON functions use the same collation +-- as the RETURNING type. Mismatched collations should raise an error. +CREATE DOMAIN d1 AS text COLLATE case_insensitive; +CREATE DOMAIN d2 AS text COLLATE "C"; +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT ('C' COLLATE "C") COLLATE case_insensitive ON EMPTY) = 'a'; -- true + ?column? +---------- + t +(1 row) + +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' ON EMPTY) = 'a'; -- true + ?column? +---------- + t +(1 row) + +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C'::d2 ON EMPTY) = 'a'; -- error +ERROR: collation of DEFAULT expression conflicts with RETURNING clause +LINE 1: ...ON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C'::d2 ON... + ^ +DETAIL: "C" versus "case_insensitive" +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' COLLATE "C" ON EMPTY) = 'a'; -- error +ERROR: collation of DEFAULT expression conflicts with RETURNING clause +LINE 1: ...ON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' COLLAT... + ^ +DETAIL: "C" versus "case_insensitive" +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' ON EMPTY) = 'a'; -- true + ?column? +---------- + t +(1 row) + +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE case_insensitive ON EMPTY) = 'a'; -- true + ?column? +---------- + t +(1 row) + +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON EMPTY) = 'a'; -- error +ERROR: collation of DEFAULT expression conflicts with RETURNING clause +LINE 1: ...ON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON... + ^ +DETAIL: "C" versus "case_insensitive" +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE "C" ON EMPTY) = 'a'; -- error +ERROR: collation of DEFAULT expression conflicts with RETURNING clause +LINE 1: ...ON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLAT... + ^ +DETAIL: "C" versus "case_insensitive" +DROP DOMAIN d1, d2; -- cleanup RESET search_path; SET client_min_messages TO warning; diff --git a/src/test/regress/expected/collate.linux.utf8.out b/src/test/regress/expected/collate.linux.utf8.out index fbaab7cdf8330..c6e84c27b692b 100644 --- a/src/test/regress/expected/collate.linux.utf8.out +++ b/src/test/regress/expected/collate.linux.utf8.out @@ -1009,6 +1009,8 @@ CREATE INDEX collate_test1_idx3 ON collate_test1 ((b COLLATE "C")); -- this is d CREATE INDEX collate_test1_idx4 ON collate_test1 (((b||'foo') COLLATE "POSIX")); CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE "C"); -- fail ERROR: collations are not supported by type integer +LINE 1: CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE ... + ^ CREATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "C")); -- fail ERROR: collations are not supported by type integer LINE 1: ...ATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "C... diff --git a/src/test/regress/expected/collate.out b/src/test/regress/expected/collate.out index bf72908fbd39b..25818f09ad25e 100644 --- a/src/test/regress/expected/collate.out +++ b/src/test/regress/expected/collate.out @@ -596,6 +596,8 @@ CREATE INDEX collate_test1_idx3 ON collate_test1 ((b COLLATE "POSIX")); -- this CREATE INDEX collate_test1_idx4 ON collate_test1 (((b||'foo') COLLATE "POSIX")); CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE "POSIX"); -- fail ERROR: collations are not supported by type integer +LINE 1: CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE ... + ^ CREATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "POSIX")); -- fail ERROR: collations are not supported by type integer LINE 1: ...ATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "P... diff --git a/src/test/regress/expected/collate.windows.win1252.out b/src/test/regress/expected/collate.windows.win1252.out index 4644f56b31dc1..2a9e52a6b4a96 100644 --- a/src/test/regress/expected/collate.windows.win1252.out +++ b/src/test/regress/expected/collate.windows.win1252.out @@ -845,6 +845,8 @@ CREATE INDEX collate_test1_idx3 ON collate_test1 ((b COLLATE "C")); -- this is d CREATE INDEX collate_test1_idx4 ON collate_test1 (((b||'foo') COLLATE "POSIX")); CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE "C"); -- fail ERROR: collations are not supported by type integer +LINE 1: CREATE INDEX collate_test1_idx5 ON collate_test1 (a COLLATE ... + ^ CREATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "C")); -- fail ERROR: collations are not supported by type integer LINE 1: ...ATE INDEX collate_test1_idx6 ON collate_test1 ((a COLLATE "C... diff --git a/src/test/regress/expected/compression.out b/src/test/regress/expected/compression.out index 4dd9ee7200d18..09f198149aa4f 100644 --- a/src/test/regress/expected/compression.out +++ b/src/test/regress/expected/compression.out @@ -1,3 +1,7 @@ +-- Default set of tests for TOAST compression, independent on compression +-- methods supported by the build. +CREATE SCHEMA pglz; +SET search_path TO pglz, public; \set HIDE_TOAST_COMPRESSION false -- ensure we get stable results regardless of installation's default SET default_toast_compression = 'pglz'; @@ -6,21 +10,13 @@ CREATE TABLE cmdata(f1 text COMPRESSION pglz); CREATE INDEX idx ON cmdata(f1); INSERT INTO cmdata VALUES(repeat('1234567890', 1000)); \d+ cmdata - Table "public.cmdata" + Table "pglz.cmdata" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+------+-----------+----------+---------+----------+-------------+--------------+------------- f1 | text | | | | extended | pglz | | Indexes: "idx" btree (f1) -CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4); -INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004)); -\d+ cmdata1 - Table "public.cmdata1" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | text | | | | extended | lz4 | | - -- verify stored compression method in the data SELECT pg_column_compression(f1) FROM cmdata; pg_column_compression @@ -28,12 +24,6 @@ SELECT pg_column_compression(f1) FROM cmdata; pglz (1 row) -SELECT pg_column_compression(f1) FROM cmdata1; - pg_column_compression ------------------------ - lz4 -(1 row) - -- decompress data slice SELECT SUBSTR(f1, 200, 5) FROM cmdata; substr @@ -41,16 +31,10 @@ SELECT SUBSTR(f1, 200, 5) FROM cmdata; 01234 (1 row) -SELECT SUBSTR(f1, 2000, 50) FROM cmdata1; - substr ----------------------------------------------------- - 01234567890123456789012345678901234567890123456789 -(1 row) - -- copy with table creation SELECT * INTO cmmove1 FROM cmdata; \d+ cmmove1 - Table "public.cmmove1" + Table "pglz.cmmove1" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+------+-----------+----------+---------+----------+-------------+--------------+------------- f1 | text | | | | extended | | | @@ -61,45 +45,9 @@ SELECT pg_column_compression(f1) FROM cmmove1; pglz (1 row) --- copy to existing table -CREATE TABLE cmmove3(f1 text COMPRESSION pglz); -INSERT INTO cmmove3 SELECT * FROM cmdata; -INSERT INTO cmmove3 SELECT * FROM cmdata1; -SELECT pg_column_compression(f1) FROM cmmove3; - pg_column_compression ------------------------ - pglz - lz4 -(2 rows) - --- test LIKE INCLUDING COMPRESSION -CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION); -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | text | | | | extended | lz4 | | - -DROP TABLE cmdata2; -- try setting compression for incompressible data type CREATE TABLE cmdata2 (f1 int COMPRESSION pglz); ERROR: column data type integer does not support compression --- update using datum from different table -CREATE TABLE cmmove2(f1 text COMPRESSION pglz); -INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004)); -SELECT pg_column_compression(f1) FROM cmmove2; - pg_column_compression ------------------------ - pglz -(1 row) - -UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1; -SELECT pg_column_compression(f1) FROM cmmove2; - pg_column_compression ------------------------ - lz4 -(1 row) - -- test externally stored compressed data CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS 'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g'; @@ -111,21 +59,6 @@ SELECT pg_column_compression(f1) FROM cmdata2; pglz (1 row) -INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000); -SELECT pg_column_compression(f1) FROM cmdata1; - pg_column_compression ------------------------ - lz4 - lz4 -(2 rows) - -SELECT SUBSTR(f1, 200, 5) FROM cmdata1; - substr --------- - 01234 - 79026 -(2 rows) - SELECT SUBSTR(f1, 200, 5) FROM cmdata2; substr -------- @@ -136,21 +69,21 @@ DROP TABLE cmdata2; --test column type update varlena/non-varlena CREATE TABLE cmdata2 (f1 int); \d+ cmdata2 - Table "public.cmdata2" + Table "pglz.cmdata2" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+---------+-----------+----------+---------+---------+-------------+--------------+------------- f1 | integer | | | | plain | | | ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar; \d+ cmdata2 - Table "public.cmdata2" + Table "pglz.cmdata2" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+-------------------+-----------+----------+---------+----------+-------------+--------------+------------- f1 | character varying | | | | extended | | | ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer; \d+ cmdata2 - Table "public.cmdata2" + Table "pglz.cmdata2" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+---------+-----------+----------+---------+---------+-------------+--------------+------------- f1 | integer | | | | plain | | | @@ -160,14 +93,14 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer; ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar; ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz; \d+ cmdata2 - Table "public.cmdata2" + Table "pglz.cmdata2" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+-------------------+-----------+----------+---------+----------+-------------+--------------+------------- f1 | character varying | | | | extended | pglz | | ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain; \d+ cmdata2 - Table "public.cmdata2" + Table "pglz.cmdata2" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+-------------------+-----------+----------+---------+---------+-------------+--------------+------------- f1 | character varying | | | | plain | pglz | | @@ -179,164 +112,47 @@ SELECT pg_column_compression(f1) FROM cmdata2; (1 row) --- test compression with materialized view -CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1; -\d+ compressmv - Materialized view "public.compressmv" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - x | text | | | | extended | | | -View definition: - SELECT f1 AS x - FROM cmdata1; - -SELECT pg_column_compression(f1) FROM cmdata1; - pg_column_compression ------------------------ - lz4 - lz4 -(2 rows) - -SELECT pg_column_compression(x) FROM compressmv; - pg_column_compression ------------------------ - lz4 - lz4 -(2 rows) - --- test compression with partition -CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1); -CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0); -CREATE TABLE cmpart2(f1 text COMPRESSION pglz); -ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1); -INSERT INTO cmpart VALUES (repeat('123456789', 1004)); -INSERT INTO cmpart VALUES (repeat('123456789', 4004)); -SELECT pg_column_compression(f1) FROM cmpart1; - pg_column_compression ------------------------ - lz4 -(1 row) - -SELECT pg_column_compression(f1) FROM cmpart2; - pg_column_compression ------------------------ - pglz -(1 row) - -- test compression with inheritance -CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error -NOTICE: merging multiple inherited definitions of column "f1" -ERROR: column "f1" has a compression method conflict -DETAIL: pglz versus lz4 -CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error -NOTICE: merging column "f1" with inherited definition -ERROR: column "f1" has a compression method conflict -DETAIL: pglz versus lz4 CREATE TABLE cmdata3(f1 text); CREATE TABLE cminh() INHERITS (cmdata, cmdata3); NOTICE: merging multiple inherited definitions of column "f1" -- test default_toast_compression GUC +-- suppress machine-dependent details +\set VERBOSITY terse SET default_toast_compression = ''; ERROR: invalid value for parameter "default_toast_compression": "" -HINT: Available values: pglz, lz4. SET default_toast_compression = 'I do not exist compression'; ERROR: invalid value for parameter "default_toast_compression": "I do not exist compression" -HINT: Available values: pglz, lz4. -SET default_toast_compression = 'lz4'; SET default_toast_compression = 'pglz'; --- test alter compression method -ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4; -INSERT INTO cmdata VALUES (repeat('123456789', 4004)); -\d+ cmdata - Table "public.cmdata" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | text | | | | extended | lz4 | | -Indexes: - "idx" btree (f1) -Child tables: cminh - -SELECT pg_column_compression(f1) FROM cmdata; - pg_column_compression ------------------------ - pglz - lz4 -(2 rows) - +\set VERBOSITY default ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default; \d+ cmdata2 - Table "public.cmdata2" + Table "pglz.cmdata2" Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description --------+-------------------+-----------+----------+---------+---------+-------------+--------------+------------- f1 | character varying | | | | plain | | | --- test alter compression method for materialized views -ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4; -\d+ compressmv - Materialized view "public.compressmv" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - x | text | | | | extended | lz4 | | -View definition: - SELECT f1 AS x - FROM cmdata1; - --- test alter compression method for partitioned tables -ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz; -ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4; --- new data should be compressed with the current compression method -INSERT INTO cmpart VALUES (repeat('123456789', 1004)); -INSERT INTO cmpart VALUES (repeat('123456789', 4004)); -SELECT pg_column_compression(f1) FROM cmpart1; - pg_column_compression ------------------------ - lz4 - pglz -(2 rows) - -SELECT pg_column_compression(f1) FROM cmpart2; - pg_column_compression ------------------------ - pglz - lz4 -(2 rows) - +DROP TABLE cmdata2; -- VACUUM FULL does not recompress SELECT pg_column_compression(f1) FROM cmdata; pg_column_compression ----------------------- pglz - lz4 -(2 rows) +(1 row) VACUUM FULL cmdata; SELECT pg_column_compression(f1) FROM cmdata; pg_column_compression ----------------------- pglz - lz4 -(2 rows) +(1 row) --- test expression index -DROP TABLE cmdata2; -CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4); -CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2)); -INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM -generate_series(1, 50) g), VERSION()); -- check data is ok SELECT length(f1) FROM cmdata; length -------- 10000 - 36036 -(2 rows) - -SELECT length(f1) FROM cmdata1; - length --------- - 10040 - 12449 -(2 rows) +(1 row) SELECT length(f1) FROM cmmove1; length @@ -344,19 +160,6 @@ SELECT length(f1) FROM cmmove1; 10000 (1 row) -SELECT length(f1) FROM cmmove2; - length --------- - 10040 -(1 row) - -SELECT length(f1) FROM cmmove3; - length --------- - 10000 - 10040 -(2 rows) - CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails ERROR: invalid compression method "i_do_not_exist_compression" CREATE TABLE badcompresstbl (a text); diff --git a/src/test/regress/expected/compression_1.out b/src/test/regress/expected/compression_1.out deleted file mode 100644 index 7bd7642b4b94f..0000000000000 --- a/src/test/regress/expected/compression_1.out +++ /dev/null @@ -1,360 +0,0 @@ -\set HIDE_TOAST_COMPRESSION false --- ensure we get stable results regardless of installation's default -SET default_toast_compression = 'pglz'; --- test creating table with compression method -CREATE TABLE cmdata(f1 text COMPRESSION pglz); -CREATE INDEX idx ON cmdata(f1); -INSERT INTO cmdata VALUES(repeat('1234567890', 1000)); -\d+ cmdata - Table "public.cmdata" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | text | | | | extended | pglz | | -Indexes: - "idx" btree (f1) - -CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4); -ERROR: compression method lz4 not supported -DETAIL: This functionality requires the server to be built with lz4 support. -INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004)); -ERROR: relation "cmdata1" does not exist -LINE 1: INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004)); - ^ -\d+ cmdata1 --- verify stored compression method in the data -SELECT pg_column_compression(f1) FROM cmdata; - pg_column_compression ------------------------ - pglz -(1 row) - -SELECT pg_column_compression(f1) FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: SELECT pg_column_compression(f1) FROM cmdata1; - ^ --- decompress data slice -SELECT SUBSTR(f1, 200, 5) FROM cmdata; - substr --------- - 01234 -(1 row) - -SELECT SUBSTR(f1, 2000, 50) FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: SELECT SUBSTR(f1, 2000, 50) FROM cmdata1; - ^ --- copy with table creation -SELECT * INTO cmmove1 FROM cmdata; -\d+ cmmove1 - Table "public.cmmove1" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | text | | | | extended | | | - -SELECT pg_column_compression(f1) FROM cmmove1; - pg_column_compression ------------------------ - pglz -(1 row) - --- copy to existing table -CREATE TABLE cmmove3(f1 text COMPRESSION pglz); -INSERT INTO cmmove3 SELECT * FROM cmdata; -INSERT INTO cmmove3 SELECT * FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: INSERT INTO cmmove3 SELECT * FROM cmdata1; - ^ -SELECT pg_column_compression(f1) FROM cmmove3; - pg_column_compression ------------------------ - pglz -(1 row) - --- test LIKE INCLUDING COMPRESSION -CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION); -ERROR: relation "cmdata1" does not exist -LINE 1: CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION); - ^ -\d+ cmdata2 -DROP TABLE cmdata2; -ERROR: table "cmdata2" does not exist --- try setting compression for incompressible data type -CREATE TABLE cmdata2 (f1 int COMPRESSION pglz); -ERROR: column data type integer does not support compression --- update using datum from different table -CREATE TABLE cmmove2(f1 text COMPRESSION pglz); -INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004)); -SELECT pg_column_compression(f1) FROM cmmove2; - pg_column_compression ------------------------ - pglz -(1 row) - -UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1; - ^ -SELECT pg_column_compression(f1) FROM cmmove2; - pg_column_compression ------------------------ - pglz -(1 row) - --- test externally stored compressed data -CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS -'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g'; -CREATE TABLE cmdata2 (f1 text COMPRESSION pglz); -INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000); -SELECT pg_column_compression(f1) FROM cmdata2; - pg_column_compression ------------------------ - pglz -(1 row) - -INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000); -ERROR: relation "cmdata1" does not exist -LINE 1: INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000); - ^ -SELECT pg_column_compression(f1) FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: SELECT pg_column_compression(f1) FROM cmdata1; - ^ -SELECT SUBSTR(f1, 200, 5) FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: SELECT SUBSTR(f1, 200, 5) FROM cmdata1; - ^ -SELECT SUBSTR(f1, 200, 5) FROM cmdata2; - substr --------- - 79026 -(1 row) - -DROP TABLE cmdata2; ---test column type update varlena/non-varlena -CREATE TABLE cmdata2 (f1 int); -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+---------+-----------+----------+---------+---------+-------------+--------------+------------- - f1 | integer | | | | plain | | | - -ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar; -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+-------------------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | character varying | | | | extended | | | - -ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE int USING f1::integer; -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+---------+-----------+----------+---------+---------+-------------+--------------+------------- - f1 | integer | | | | plain | | | - ---changing column storage should not impact the compression method ---but the data should not be compressed -ALTER TABLE cmdata2 ALTER COLUMN f1 TYPE varchar; -ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION pglz; -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+-------------------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | character varying | | | | extended | pglz | | - -ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain; -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+-------------------+-----------+----------+---------+---------+-------------+--------------+------------- - f1 | character varying | | | | plain | pglz | | - -INSERT INTO cmdata2 VALUES (repeat('123456789', 800)); -SELECT pg_column_compression(f1) FROM cmdata2; - pg_column_compression ------------------------ - -(1 row) - --- test compression with materialized view -CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: ...TE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1; - ^ -\d+ compressmv -SELECT pg_column_compression(f1) FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: SELECT pg_column_compression(f1) FROM cmdata1; - ^ -SELECT pg_column_compression(x) FROM compressmv; -ERROR: relation "compressmv" does not exist -LINE 1: SELECT pg_column_compression(x) FROM compressmv; - ^ --- test compression with partition -CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1); -ERROR: compression method lz4 not supported -DETAIL: This functionality requires the server to be built with lz4 support. -CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0); -ERROR: relation "cmpart" does not exist -CREATE TABLE cmpart2(f1 text COMPRESSION pglz); -ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1); -ERROR: relation "cmpart" does not exist -INSERT INTO cmpart VALUES (repeat('123456789', 1004)); -ERROR: relation "cmpart" does not exist -LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 1004)); - ^ -INSERT INTO cmpart VALUES (repeat('123456789', 4004)); -ERROR: relation "cmpart" does not exist -LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 4004)); - ^ -SELECT pg_column_compression(f1) FROM cmpart1; -ERROR: relation "cmpart1" does not exist -LINE 1: SELECT pg_column_compression(f1) FROM cmpart1; - ^ -SELECT pg_column_compression(f1) FROM cmpart2; - pg_column_compression ------------------------ -(0 rows) - --- test compression with inheritance -CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error -ERROR: relation "cmdata1" does not exist -CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error -NOTICE: merging column "f1" with inherited definition -ERROR: column "f1" has a compression method conflict -DETAIL: pglz versus lz4 -CREATE TABLE cmdata3(f1 text); -CREATE TABLE cminh() INHERITS (cmdata, cmdata3); -NOTICE: merging multiple inherited definitions of column "f1" --- test default_toast_compression GUC -SET default_toast_compression = ''; -ERROR: invalid value for parameter "default_toast_compression": "" -HINT: Available values: pglz. -SET default_toast_compression = 'I do not exist compression'; -ERROR: invalid value for parameter "default_toast_compression": "I do not exist compression" -HINT: Available values: pglz. -SET default_toast_compression = 'lz4'; -ERROR: invalid value for parameter "default_toast_compression": "lz4" -HINT: Available values: pglz. -SET default_toast_compression = 'pglz'; --- test alter compression method -ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4; -ERROR: compression method lz4 not supported -DETAIL: This functionality requires the server to be built with lz4 support. -INSERT INTO cmdata VALUES (repeat('123456789', 4004)); -\d+ cmdata - Table "public.cmdata" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+------+-----------+----------+---------+----------+-------------+--------------+------------- - f1 | text | | | | extended | pglz | | -Indexes: - "idx" btree (f1) -Child tables: cminh - -SELECT pg_column_compression(f1) FROM cmdata; - pg_column_compression ------------------------ - pglz - pglz -(2 rows) - -ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default; -\d+ cmdata2 - Table "public.cmdata2" - Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description ---------+-------------------+-----------+----------+---------+---------+-------------+--------------+------------- - f1 | character varying | | | | plain | | | - --- test alter compression method for materialized views -ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4; -ERROR: relation "compressmv" does not exist -\d+ compressmv --- test alter compression method for partitioned tables -ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz; -ERROR: relation "cmpart1" does not exist -ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4; -ERROR: compression method lz4 not supported -DETAIL: This functionality requires the server to be built with lz4 support. --- new data should be compressed with the current compression method -INSERT INTO cmpart VALUES (repeat('123456789', 1004)); -ERROR: relation "cmpart" does not exist -LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 1004)); - ^ -INSERT INTO cmpart VALUES (repeat('123456789', 4004)); -ERROR: relation "cmpart" does not exist -LINE 1: INSERT INTO cmpart VALUES (repeat('123456789', 4004)); - ^ -SELECT pg_column_compression(f1) FROM cmpart1; -ERROR: relation "cmpart1" does not exist -LINE 1: SELECT pg_column_compression(f1) FROM cmpart1; - ^ -SELECT pg_column_compression(f1) FROM cmpart2; - pg_column_compression ------------------------ -(0 rows) - --- VACUUM FULL does not recompress -SELECT pg_column_compression(f1) FROM cmdata; - pg_column_compression ------------------------ - pglz - pglz -(2 rows) - -VACUUM FULL cmdata; -SELECT pg_column_compression(f1) FROM cmdata; - pg_column_compression ------------------------ - pglz - pglz -(2 rows) - --- test expression index -DROP TABLE cmdata2; -CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4); -ERROR: compression method lz4 not supported -DETAIL: This functionality requires the server to be built with lz4 support. -CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2)); -ERROR: relation "cmdata2" does not exist -INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM -generate_series(1, 50) g), VERSION()); -ERROR: relation "cmdata2" does not exist -LINE 1: INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEX... - ^ --- check data is ok -SELECT length(f1) FROM cmdata; - length --------- - 10000 - 36036 -(2 rows) - -SELECT length(f1) FROM cmdata1; -ERROR: relation "cmdata1" does not exist -LINE 1: SELECT length(f1) FROM cmdata1; - ^ -SELECT length(f1) FROM cmmove1; - length --------- - 10000 -(1 row) - -SELECT length(f1) FROM cmmove2; - length --------- - 10040 -(1 row) - -SELECT length(f1) FROM cmmove3; - length --------- - 10000 -(1 row) - -CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails -ERROR: invalid compression method "i_do_not_exist_compression" -CREATE TABLE badcompresstbl (a text); -ALTER TABLE badcompresstbl ALTER a SET COMPRESSION I_Do_Not_Exist_Compression; -- fails -ERROR: invalid compression method "i_do_not_exist_compression" -DROP TABLE badcompresstbl; -\set HIDE_TOAST_COMPRESSION true diff --git a/src/test/regress/expected/compression_lz4.out b/src/test/regress/expected/compression_lz4.out new file mode 100644 index 0000000000000..068dd7c367446 --- /dev/null +++ b/src/test/regress/expected/compression_lz4.out @@ -0,0 +1,249 @@ +-- Tests for TOAST compression with lz4 +SELECT NOT(enumvals @> '{lz4}') AS skip_test FROM pg_settings WHERE + name = 'default_toast_compression' \gset +\if :skip_test + \echo '*** skipping TOAST tests with lz4 (not supported) ***' + \quit +\endif +CREATE SCHEMA lz4; +SET search_path TO lz4, public; +\set HIDE_TOAST_COMPRESSION false +-- Ensure we get stable results regardless of the installation's default. +-- We rely on this GUC value for a few tests. +SET default_toast_compression = 'pglz'; +-- test creating table with compression method +CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz); +CREATE INDEX idx ON cmdata_pglz(f1); +INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000)); +\d+ cmdata +CREATE TABLE cmdata_lz4(f1 TEXT COMPRESSION lz4); +INSERT INTO cmdata_lz4 VALUES(repeat('1234567890', 1004)); +\d+ cmdata1 +-- verify stored compression method in the data +SELECT pg_column_compression(f1) FROM cmdata_lz4; + pg_column_compression +----------------------- + lz4 +(1 row) + +-- decompress data slice +SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz; + substr +-------- + 01234 +(1 row) + +SELECT SUBSTR(f1, 2000, 50) FROM cmdata_lz4; + substr +---------------------------------------------------- + 01234567890123456789012345678901234567890123456789 +(1 row) + +-- copy with table creation +SELECT * INTO cmmove1 FROM cmdata_lz4; +\d+ cmmove1 + Table "lz4.cmmove1" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | | | + +SELECT pg_column_compression(f1) FROM cmmove1; + pg_column_compression +----------------------- + lz4 +(1 row) + +-- test LIKE INCLUDING COMPRESSION. The GUC default_toast_compression +-- has no effect, the compression method from the table being copied. +CREATE TABLE cmdata2 (LIKE cmdata_lz4 INCLUDING COMPRESSION); +\d+ cmdata2 + Table "lz4.cmdata2" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + f1 | text | | | | extended | lz4 | | + +DROP TABLE cmdata2; +-- copy to existing table +CREATE TABLE cmmove3(f1 text COMPRESSION pglz); +INSERT INTO cmmove3 SELECT * FROM cmdata_pglz; +INSERT INTO cmmove3 SELECT * FROM cmdata_lz4; +SELECT pg_column_compression(f1) FROM cmmove3; + pg_column_compression +----------------------- + pglz + lz4 +(2 rows) + +-- update using datum from different table with LZ4 data. +CREATE TABLE cmmove2(f1 text COMPRESSION pglz); +INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004)); +SELECT pg_column_compression(f1) FROM cmmove2; + pg_column_compression +----------------------- + pglz +(1 row) + +UPDATE cmmove2 SET f1 = cmdata_lz4.f1 FROM cmdata_lz4; +SELECT pg_column_compression(f1) FROM cmmove2; + pg_column_compression +----------------------- + lz4 +(1 row) + +-- test externally stored compressed data +CREATE OR REPLACE FUNCTION large_val_lz4() RETURNS TEXT LANGUAGE SQL AS +'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g'; +CREATE TABLE cmdata2 (f1 text COMPRESSION lz4); +INSERT INTO cmdata2 SELECT large_val_lz4() || repeat('a', 4000); +SELECT pg_column_compression(f1) FROM cmdata2; + pg_column_compression +----------------------- + lz4 +(1 row) + +SELECT SUBSTR(f1, 200, 5) FROM cmdata2; + substr +-------- + 79026 +(1 row) + +DROP TABLE cmdata2; +DROP FUNCTION large_val_lz4; +-- test compression with materialized view +CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_lz4; +\d+ compressmv + Materialized view "lz4.compressmv" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + x | text | | | | extended | | | +View definition: + SELECT f1 AS x + FROM cmdata_lz4; + +SELECT pg_column_compression(f1) FROM cmdata_lz4; + pg_column_compression +----------------------- + lz4 +(1 row) + +SELECT pg_column_compression(x) FROM compressmv; + pg_column_compression +----------------------- + lz4 +(1 row) + +-- test compression with partition +CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1); +CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE cmpart2(f1 text COMPRESSION pglz); +ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1); +INSERT INTO cmpart VALUES (repeat('123456789', 1004)); +INSERT INTO cmpart VALUES (repeat('123456789', 4004)); +SELECT pg_column_compression(f1) FROM cmpart1; + pg_column_compression +----------------------- + lz4 +(1 row) + +SELECT pg_column_compression(f1) FROM cmpart2; + pg_column_compression +----------------------- + pglz +(1 row) + +-- test compression with inheritance +CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_lz4); -- error +NOTICE: merging multiple inherited definitions of column "f1" +ERROR: column "f1" has a compression method conflict +DETAIL: pglz versus lz4 +CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata_pglz); -- error +NOTICE: merging column "f1" with inherited definition +ERROR: column "f1" has a compression method conflict +DETAIL: pglz versus lz4 +CREATE TABLE cmdata3(f1 text); +CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3); +NOTICE: merging multiple inherited definitions of column "f1" +-- test default_toast_compression GUC +SET default_toast_compression = 'lz4'; +-- test alter compression method +ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION lz4; +INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004)); +\d+ cmdata +SELECT pg_column_compression(f1) FROM cmdata_pglz; + pg_column_compression +----------------------- + pglz + lz4 +(2 rows) + +ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz; +-- test alter compression method for materialized views +ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4; +\d+ compressmv + Materialized view "lz4.compressmv" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+------+-----------+----------+---------+----------+-------------+--------------+------------- + x | text | | | | extended | lz4 | | +View definition: + SELECT f1 AS x + FROM cmdata_lz4; + +-- test alter compression method for partitioned tables +ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz; +ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4; +-- new data should be compressed with the current compression method +INSERT INTO cmpart VALUES (repeat('123456789', 1004)); +INSERT INTO cmpart VALUES (repeat('123456789', 4004)); +SELECT pg_column_compression(f1) FROM cmpart1; + pg_column_compression +----------------------- + lz4 + pglz +(2 rows) + +SELECT pg_column_compression(f1) FROM cmpart2; + pg_column_compression +----------------------- + pglz + lz4 +(2 rows) + +-- test expression index +CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4); +CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2)); +INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM +generate_series(1, 50) g), VERSION()); +-- check data is ok +SELECT length(f1) FROM cmdata_pglz; + length +-------- + 10000 + 36036 +(2 rows) + +SELECT length(f1) FROM cmdata_lz4; + length +-------- + 10040 +(1 row) + +SELECT length(f1) FROM cmmove1; + length +-------- + 10040 +(1 row) + +SELECT length(f1) FROM cmmove2; + length +-------- + 10040 +(1 row) + +SELECT length(f1) FROM cmmove3; + length +-------- + 10000 + 10040 +(2 rows) + +\set HIDE_TOAST_COMPRESSION true diff --git a/src/test/regress/expected/compression_lz4_1.out b/src/test/regress/expected/compression_lz4_1.out new file mode 100644 index 0000000000000..198056fa22498 --- /dev/null +++ b/src/test/regress/expected/compression_lz4_1.out @@ -0,0 +1,7 @@ +-- Tests for TOAST compression with lz4 +SELECT NOT(enumvals @> '{lz4}') AS skip_test FROM pg_settings WHERE + name = 'default_toast_compression' \gset +\if :skip_test + \echo '*** skipping TOAST tests with lz4 (not supported) ***' +*** skipping TOAST tests with lz4 (not supported) *** + \quit diff --git a/src/test/regress/expected/compression_pglz.out b/src/test/regress/expected/compression_pglz.out new file mode 100644 index 0000000000000..0ef49d4250684 --- /dev/null +++ b/src/test/regress/expected/compression_pglz.out @@ -0,0 +1,65 @@ +-- +-- Tests for PGLZ compression +-- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_pglz_compress(bytea) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_pglz_decompress(bytea, int4, bool) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); +ERROR: pglz_decompress failed +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with read of a match tag, +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; + ctrl_only_len +--------------- + 0 +(1 row) + +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with read of a match tag, +-- 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Corrupted compressed data. Set control bit with match tag where length +-- nibble is 3 bytes (extended length), no data follows. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +ERROR: pglz_decompress failed +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); +ERROR: pglz_decompress failed +-- Clean up +DROP FUNCTION test_pglz_compress; +DROP FUNCTION test_pglz_decompress; diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index ad6aaab738538..728ef2fd17e0b 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -390,6 +390,84 @@ SELECT * FROM COPY_TBL; 6 | OK | 4 (2 rows) +-- +-- CHECK constraints +-- ALTER TABLE ALTER CONSTRAINT [NOT] ENFORCED +create table parted_ch( + a int, b int, + constraint cc check (a > 10) not enforced, + constraint cc_1 check (b < 17) not enforced +) partition by range(a); +create table parted_ch_1 partition of parted_ch for values from (0) to (10) partition by list(b); +create table parted_ch_11 partition of parted_ch_1 for values in (0, 1, 22,4); +create table parted_ch_12 partition of parted_ch_1 for values in (2); +create table parted_ch_2(b int, a int, + constraint cc check (a > 10) not enforced, + constraint cc_1 check (b < 17) enforced, + constraint cc_2 check( a < 15) not enforced +); +alter table parted_ch attach partition parted_ch_2 for values from (10) to (20); +insert into parted_ch values (1, 22), (9, 1), (16, 16); +alter table parted_ch alter constraint cc_1 enforced; --error +ERROR: check constraint "cc_1" of relation "parted_ch_11" is violated by some row +update parted_ch set b = 4 where b = 22; +alter table parted_ch alter constraint cc_1 enforced; --ok +create or replace view check_constraint_status as +select conname, conrelid::regclass, conenforced, convalidated +from pg_constraint +where conrelid::regclass::text ~* '^parted_ch' and contype = 'c' +order by conname, conrelid::regclass::text collate "C"; +alter table parted_ch alter constraint cc not enforced; --no-op +alter table parted_ch alter constraint cc enforced; --error +ERROR: check constraint "cc" of relation "parted_ch_11" is violated by some row +delete from parted_ch where a = 1; +alter table parted_ch alter constraint cc enforced; --error +ERROR: check constraint "cc" of relation "parted_ch_11" is violated by some row +delete from parted_ch where a = 9; +alter table parted_ch alter constraint cc enforced; +--check these CHECK constraint status +select * from check_constraint_status; + conname | conrelid | conenforced | convalidated +---------+--------------+-------------+-------------- + cc | parted_ch | t | t + cc | parted_ch_1 | t | t + cc | parted_ch_11 | t | t + cc | parted_ch_12 | t | t + cc | parted_ch_2 | t | t + cc_1 | parted_ch | t | t + cc_1 | parted_ch_1 | t | t + cc_1 | parted_ch_11 | t | t + cc_1 | parted_ch_12 | t | t + cc_1 | parted_ch_2 | t | t + cc_2 | parted_ch_2 | f | f +(11 rows) + +alter table parted_ch_2 alter constraint cc_2 enforced; --error +ERROR: check constraint "cc_2" of relation "parted_ch_2" is violated by some row +delete from parted_ch where a = 16; +alter table parted_ch_2 alter constraint cc_2 enforced; +alter table parted_ch_2 alter constraint cc not enforced; +alter table parted_ch_2 alter constraint cc_1 not enforced; +alter table parted_ch_2 alter constraint cc_2 not enforced; +--check these CHECK constraint status again +select * from check_constraint_status; + conname | conrelid | conenforced | convalidated +---------+--------------+-------------+-------------- + cc | parted_ch | t | t + cc | parted_ch_1 | t | t + cc | parted_ch_11 | t | t + cc | parted_ch_12 | t | t + cc | parted_ch_2 | f | f + cc_1 | parted_ch | t | t + cc_1 | parted_ch_1 | t | t + cc_1 | parted_ch_11 | t | t + cc_1 | parted_ch_12 | t | t + cc_1 | parted_ch_2 | f | f + cc_2 | parted_ch_2 | f | f +(11 rows) + +drop table parted_ch; +drop view check_constraint_status; -- -- Primary keys -- @@ -746,8 +824,15 @@ LINE 1: CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED); ^ ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl" +HINT: Only foreign key and check constraints can change enforceability. ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED; ERROR: cannot alter enforceability of constraint "unique_tbl_i_key" of relation "unique_tbl" +HINT: Only foreign key and check constraints can change enforceability. +-- can't make an existing constraint NOT VALID +ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT VALID; +ERROR: constraints cannot be altered to be NOT VALID +LINE 1: ...ABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT VALID; + ^ DROP TABLE unique_tbl; -- -- EXCLUDE constraints @@ -775,6 +860,10 @@ INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2; ERROR: ON CONFLICT DO UPDATE not supported with exclusion constraints +-- fail, because DO SELECT variant requires unique index +INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') + ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *; +ERROR: ON CONFLICT DO SELECT not supported with exclusion constraints -- succeed because c1 doesn't overlap INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>'); -- succeed because c2 doesn't overlap @@ -841,8 +930,12 @@ CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); Not-null constraints: "notnull_tbl1_a_not_null" NOT NULL "a" --- no-op +-- specifying an existing constraint is a no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_tbl1_a_not_null NOT NULL a; +-- but using a different constraint name is not allowed ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; +ERROR: cannot create not-null constraint "nn" on column "a" of table "notnull_tbl1" +DETAIL: A not-null constraint named "notnull_tbl1_a_not_null" already exists for this column. \d+ notnull_tbl1 Table "public.notnull_tbl1" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -991,7 +1084,8 @@ CREATE TABLE ATACC2 () INHERITS (ATACC1); Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | | | plain | | -Inherits: atacc1 +Inherits: + atacc1 DROP TABLE ATACC1, ATACC2; CREATE TABLE ATACC1 (a int); @@ -1002,7 +1096,8 @@ CREATE TABLE ATACC2 () INHERITS (ATACC1); Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | | | plain | | -Inherits: atacc1 +Inherits: + atacc1 DROP TABLE ATACC1, ATACC2; CREATE TABLE ATACC1 (a int); @@ -1013,7 +1108,8 @@ ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT; Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | | | plain | | -Inherits: atacc1 +Inherits: + atacc1 CREATE TABLE ATACC3 (PRIMARY KEY (a)) INHERITS (ATACC1); \d+ ATACC3 @@ -1025,7 +1121,8 @@ Indexes: "atacc3_pkey" PRIMARY KEY, btree (a) Not-null constraints: "atacc3_a_not_null" NOT NULL "a" -Inherits: atacc1 +Inherits: + atacc1 DROP TABLE ATACC1, ATACC2, ATACC3; -- NOT NULL NO INHERIT is not possible on partitioned tables @@ -1053,7 +1150,8 @@ ALTER TABLE ATACC1 ADD CONSTRAINT ditto NOT NULL a; a | integer | | not null | | plain | | Not-null constraints: "ditto" NOT NULL "a" (inherited) -Inherits: atacc2 +Inherits: + atacc2 DROP TABLE ATACC1, ATACC2, ATACC3; -- Can't have two constraints with the same name @@ -1102,7 +1200,8 @@ Indexes: "cnn_primarykey" PRIMARY KEY, btree (b) Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" -Child tables: cnn_pk_child +Child tables: + cnn_pk_child Table "public.cnn_pk_child" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -1111,7 +1210,8 @@ Child tables: cnn_pk_child b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" (inherited) -Inherits: cnn_pk +Inherits: + cnn_pk ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; \d+ cnn_pk* @@ -1122,7 +1222,8 @@ ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" -Child tables: cnn_pk_child +Child tables: + cnn_pk_child Table "public.cnn_pk_child" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -1131,7 +1232,8 @@ Child tables: cnn_pk_child b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" (inherited) -Inherits: cnn_pk +Inherits: + cnn_pk DROP TABLE cnn_pk, cnn_pk_child; -- As above, but create the primary key ahead of time @@ -1147,7 +1249,8 @@ Indexes: "cnn_primarykey" PRIMARY KEY, btree (b) Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" -Child tables: cnn_pk_child +Child tables: + cnn_pk_child Table "public.cnn_pk_child" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -1156,7 +1259,8 @@ Child tables: cnn_pk_child b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" (inherited) -Inherits: cnn_pk +Inherits: + cnn_pk ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; \d+ cnn_pk* @@ -1167,7 +1271,8 @@ ALTER TABLE cnn_pk DROP CONSTRAINT cnn_primarykey; b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" -Child tables: cnn_pk_child +Child tables: + cnn_pk_child Table "public.cnn_pk_child" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -1176,7 +1281,8 @@ Child tables: cnn_pk_child b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" (inherited) -Inherits: cnn_pk +Inherits: + cnn_pk DROP TABLE cnn_pk, cnn_pk_child; -- As above, but create the primary key using a UNIQUE index @@ -1195,7 +1301,8 @@ Indexes: "cnn_primarykey" PRIMARY KEY, btree (b) Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" -Child tables: cnn_pk_child +Child tables: + cnn_pk_child Table "public.cnn_pk_child" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -1204,7 +1311,8 @@ Child tables: cnn_pk_child b | integer | | not null | | plain | | Not-null constraints: "cnn_pk_b_not_null" NOT NULL "b" (inherited) -Inherits: cnn_pk +Inherits: + cnn_pk DROP TABLE cnn_pk, cnn_pk_child; -- Unique constraints don't give raise to not-null constraints, however. @@ -1263,9 +1371,10 @@ Indexes: "notnull_tbl4_pkey" PRIMARY KEY, btree (a) DEFERRABLE INITIALLY DEFERRED Not-null constraints: "notnull_tbl4_a_not_null" NOT NULL "a" -Child tables: notnull_tbl4_cld, - notnull_tbl4_cld2, - notnull_tbl4_cld3 +Child tables: + notnull_tbl4_cld + notnull_tbl4_cld2 + notnull_tbl4_cld3 \d+ notnull_tbl4_lk Table "public.notnull_tbl4_lk" @@ -1302,7 +1411,8 @@ Not-null constraints: a | integer | | not null | | plain | | Not-null constraints: "notnull_tbl4_a_not_null" NOT NULL "a" (inherited) -Inherits: notnull_tbl4 +Inherits: + notnull_tbl4 \d+ notnull_tbl4_cld2 Table "public.notnull_tbl4_cld2" @@ -1313,7 +1423,8 @@ Indexes: "notnull_tbl4_cld2_pkey" PRIMARY KEY, btree (a) DEFERRABLE Not-null constraints: "notnull_tbl4_cld2_a_not_null" NOT NULL "a" (local, inherited) -Inherits: notnull_tbl4 +Inherits: + notnull_tbl4 \d+ notnull_tbl4_cld3 Table "public.notnull_tbl4_cld3" @@ -1324,7 +1435,8 @@ Indexes: "notnull_tbl4_cld3_pkey" PRIMARY KEY, btree (a) DEFERRABLE Not-null constraints: "a_nn" NOT NULL "a" (local, inherited) -Inherits: notnull_tbl4 +Inherits: + notnull_tbl4 -- leave these tables around for pg_upgrade testing -- It's possible to remove a constraint from parents without affecting children @@ -1342,7 +1454,8 @@ ALTER TABLE ONLY notnull_tbl5 ALTER b DROP NOT NULL; Not-null constraints: "ann" NOT NULL "a" "bnn" NOT NULL "b" -Inherits: notnull_tbl5 +Inherits: + notnull_tbl5 CREATE TABLE notnull_tbl6 (a int CONSTRAINT ann NOT NULL, b int CONSTRAINT bnn NOT NULL, check (a > 0)) PARTITION BY LIST (a); @@ -1399,6 +1512,10 @@ ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a); ERROR: cannot create primary key on column "a" DETAIL: The constraint "nn" on column "a" of table "notnull_tbl1", marked NOT VALID, is incompatible with a primary key. HINT: You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT. +-- cannot set column as generated-as-identity if it has an invalid not-null +ALTER TABLE notnull_tbl1 ALTER COLUMN a ADD GENERATED ALWAYS AS IDENTITY; +ERROR: incompatible NOT VALID constraint "nn" on relation "notnull_tbl1" +HINT: You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT. -- ALTER column SET NOT NULL validates an invalid constraint (but this fails -- because of rows with null values) ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; @@ -1659,6 +1776,8 @@ EXECUTE get_nnconstraint_info('{constr_parent3, constr_child3}'); constr_parent3 | constr_parent3_a_not_null | t | t | 0 (2 rows) +COMMENT ON CONSTRAINT constr_parent2_a_not_null ON constr_parent2 IS 'this constraint is invalid'; +COMMENT ON CONSTRAINT constr_parent2_a_not_null ON constr_child2 IS 'this constraint is valid'; DEALLOCATE get_nnconstraint_info; -- end NOT NULL NOT VALID -- Comments @@ -1694,3 +1813,11 @@ DROP TABLE constraint_comments_tbl; DROP DOMAIN constraint_comments_dom; DROP ROLE regress_constraint_comments; DROP ROLE regress_constraint_comments_noaccess; +-- Leave some constraints for the pg_upgrade test to pick up +CREATE DOMAIN constraint_comments_dom AS int; +ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT inv_ck CHECK (value > 0) NOT VALID; +COMMENT ON CONSTRAINT inv_ck ON DOMAIN constraint_comments_dom IS 'comment on invalid constraint'; +-- Create a table that exercises pg_upgrade +CREATE TABLE regress_notnull1 (a integer); +CREATE TABLE regress_notnull2 () INHERITS (regress_notnull1); +ALTER TABLE ONLY regress_notnull2 ALTER COLUMN a SET NOT NULL; diff --git a/src/test/regress/expected/conversion.out b/src/test/regress/expected/conversion.out index 7dd1ef6161f06..01491ac63387b 100644 --- a/src/test/regress/expected/conversion.out +++ b/src/test/regress/expected/conversion.out @@ -506,6 +506,7 @@ insert into gb18030_inputs values ('\x666f6fcff3', 'valid'), ('\x666f6f8431a530', 'valid, no translation to UTF-8'), ('\x666f6f84309c38', 'valid, translates to UTF-8 by mapping function'), + ('\xa6d9', 'valid, changed from version 2000 to 2022'), ('\x666f6f84309c', 'incomplete char '), ('\x666f6f84309c0a', 'incomplete char, followed by newline '), ('\x666f6f84', 'incomplete char at end'), @@ -521,12 +522,13 @@ select description, inbytes, (test_conv(inbytes::text::bytea, 'gb18030', 'gb1803 valid | \x666f6fcff3 | \x666f6fcff3 | | valid, no translation to UTF-8 | \x666f6f8431a530 | \x666f6f8431a530 | | valid, translates to UTF-8 by mapping function | \x666f6f84309c38 | \x666f6f84309c38 | | + valid, changed from version 2000 to 2022 | \xa6d9 | \xa6d9 | | incomplete char | \x666f6f84309c | \x666f6f | \x84309c | invalid byte sequence for encoding "GB18030": 0x84 0x30 0x9c incomplete char, followed by newline | \x666f6f84309c0a | \x666f6f | \x84309c0a | invalid byte sequence for encoding "GB18030": 0x84 0x30 0x9c 0x0a incomplete char at end | \x666f6f84 | \x666f6f | \x84 | invalid byte sequence for encoding "GB18030": 0x84 invalid, NUL byte | \x666f6f84309c3800 | \x666f6f84309c38 | \x00 | invalid byte sequence for encoding "GB18030": 0x00 invalid, NUL byte | \x666f6f84309c0038 | \x666f6f | \x84309c0038 | invalid byte sequence for encoding "GB18030": 0x84 0x30 0x9c 0x00 -(9 rows) +(10 rows) -- Test conversions from GB18030 select description, inbytes, (test_conv(inbytes, 'gb18030', 'utf8')).* from gb18030_inputs; @@ -536,12 +538,13 @@ select description, inbytes, (test_conv(inbytes, 'gb18030', 'utf8')).* from gb18 valid | \x666f6fcff3 | \x666f6fe8b1a1 | | valid, no translation to UTF-8 | \x666f6f8431a530 | \x666f6f | \x8431a530 | character with byte sequence 0x84 0x31 0xa5 0x30 in encoding "GB18030" has no equivalent in encoding "UTF8" valid, translates to UTF-8 by mapping function | \x666f6f84309c38 | \x666f6fefa8aa | | + valid, changed from version 2000 to 2022 | \xa6d9 | \xefb890 | | incomplete char | \x666f6f84309c | \x666f6f | \x84309c | invalid byte sequence for encoding "GB18030": 0x84 0x30 0x9c incomplete char, followed by newline | \x666f6f84309c0a | \x666f6f | \x84309c0a | invalid byte sequence for encoding "GB18030": 0x84 0x30 0x9c 0x0a incomplete char at end | \x666f6f84 | \x666f6f | \x84 | invalid byte sequence for encoding "GB18030": 0x84 invalid, NUL byte | \x666f6f84309c3800 | \x666f6fefa8aa | \x00 | invalid byte sequence for encoding "GB18030": 0x00 invalid, NUL byte | \x666f6f84309c0038 | \x666f6f | \x84309c0038 | invalid byte sequence for encoding "GB18030": 0x84 0x30 0x9c 0x00 -(9 rows) +(10 rows) -- -- ISO-8859-5 @@ -585,16 +588,6 @@ select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'koi8r')).* from i invalid, NUL byte | \xe4dede00 | \xc6cfcf | \x00 | invalid byte sequence for encoding "ISO_8859_5": 0x00 (5 rows) -select description, inbytes, (test_conv(inbytes, 'iso8859_5', 'mule_internal')).* from iso8859_5_inputs; - description | inbytes | result | errorat | error --------------------+------------+----------------+----------+------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid | \xe4dede | \x8bc68bcf8bcf | | - invalid, NUL byte | \x00 | \x | \x00 | invalid byte sequence for encoding "ISO_8859_5": 0x00 - invalid, NUL byte | \xe400dede | \x8bc6 | \x00dede | invalid byte sequence for encoding "ISO_8859_5": 0x00 - invalid, NUL byte | \xe4dede00 | \x8bc68bcf8bcf | \x00 | invalid byte sequence for encoding "ISO_8859_5": 0x00 -(5 rows) - -- -- Big5 -- @@ -627,120 +620,3 @@ select description, inbytes, (test_conv(inbytes, 'big5', 'utf8')).* from big5_in invalid, NUL byte | \x666f6fb64800 | \x666f6fe8b1a1 | \x00 | invalid byte sequence for encoding "BIG5": 0x00 (5 rows) -select description, inbytes, (test_conv(inbytes, 'big5', 'mule_internal')).* from big5_inputs; - description | inbytes | result | errorat | error ---------------------------------+----------------+----------------+----------+------------------------------------------------------ - valid, pure ASCII | \x666f6f | \x666f6f | | - valid | \x666f6fb648 | \x666f6f95e2af | | - valid, no translation to UTF-8 | \x666f6fa27f | \x666f6f95a3c1 | | - invalid, NUL byte | \x666f6fb60048 | \x666f6f | \xb60048 | invalid byte sequence for encoding "BIG5": 0xb6 0x00 - invalid, NUL byte | \x666f6fb64800 | \x666f6f95e2af | \x00 | invalid byte sequence for encoding "BIG5": 0x00 -(5 rows) - --- --- MULE_INTERNAL --- -CREATE TABLE mic_inputs (inbytes bytea, description text); -insert into mic_inputs values - ('\x666f6f', 'valid, pure ASCII'), - ('\x8bc68bcf8bcf', 'valid (in KOI8R)'), - ('\x8bc68bcf8b', 'invalid,incomplete char'), - ('\x92bedd', 'valid (in SHIFT_JIS)'), - ('\x92be', 'invalid, incomplete char)'), - ('\x666f6f95a3c1', 'valid (in Big5)'), - ('\x666f6f95a3', 'invalid, incomplete char'), - ('\x9200bedd', 'invalid, NUL byte'), - ('\x92bedd00', 'invalid, NUL byte'), - ('\x8b00c68bcf8bcf', 'invalid, NUL byte'); --- Test MULE_INTERNAL verification -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'mule_internal')).* from mic_inputs; - description | inbytes | result | errorat | error ----------------------------+------------------+----------------+------------------+-------------------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid (in KOI8R) | \x8bc68bcf8bcf | \x8bc68bcf8bcf | | - invalid,incomplete char | \x8bc68bcf8b | \x8bc68bcf | \x8b | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b - valid (in SHIFT_JIS) | \x92bedd | \x92bedd | | - invalid, incomplete char) | \x92be | \x | \x92be | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0xbe - valid (in Big5) | \x666f6f95a3c1 | \x666f6f95a3c1 | | - invalid, incomplete char | \x666f6f95a3 | \x666f6f | \x95a3 | invalid byte sequence for encoding "MULE_INTERNAL": 0x95 0xa3 - invalid, NUL byte | \x9200bedd | \x | \x9200bedd | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0x00 0xbe - invalid, NUL byte | \x92bedd00 | \x92bedd | \x00 | invalid byte sequence for encoding "MULE_INTERNAL": 0x00 - invalid, NUL byte | \x8b00c68bcf8bcf | \x | \x8b00c68bcf8bcf | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b 0x00 -(10 rows) - --- Test conversions from MULE_INTERNAL -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'koi8r')).* from mic_inputs; - description | inbytes | result | errorat | error ----------------------------+------------------+----------+------------------+--------------------------------------------------------------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid (in KOI8R) | \x8bc68bcf8bcf | \xc6cfcf | | - invalid,incomplete char | \x8bc68bcf8b | \xc6cf | \x8b | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b - valid (in SHIFT_JIS) | \x92bedd | \x | \x92bedd | character with byte sequence 0x92 0xbe 0xdd in encoding "MULE_INTERNAL" has no equivalent in encoding "KOI8R" - invalid, incomplete char) | \x92be | \x | \x92be | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0xbe - valid (in Big5) | \x666f6f95a3c1 | \x666f6f | \x95a3c1 | character with byte sequence 0x95 0xa3 0xc1 in encoding "MULE_INTERNAL" has no equivalent in encoding "KOI8R" - invalid, incomplete char | \x666f6f95a3 | \x666f6f | \x95a3 | invalid byte sequence for encoding "MULE_INTERNAL": 0x95 0xa3 - invalid, NUL byte | \x9200bedd | \x | \x9200bedd | character with byte sequence 0x92 0x00 0xbe in encoding "MULE_INTERNAL" has no equivalent in encoding "KOI8R" - invalid, NUL byte | \x92bedd00 | \x | \x92bedd00 | character with byte sequence 0x92 0xbe 0xdd in encoding "MULE_INTERNAL" has no equivalent in encoding "KOI8R" - invalid, NUL byte | \x8b00c68bcf8bcf | \x | \x8b00c68bcf8bcf | character with byte sequence 0x8b 0x00 in encoding "MULE_INTERNAL" has no equivalent in encoding "KOI8R" -(10 rows) - -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'iso8859-5')).* from mic_inputs; - description | inbytes | result | errorat | error ----------------------------+------------------+----------+------------------+-------------------------------------------------------------------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid (in KOI8R) | \x8bc68bcf8bcf | \xe4dede | | - invalid,incomplete char | \x8bc68bcf8b | \xe4de | \x8b | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b - valid (in SHIFT_JIS) | \x92bedd | \x | \x92bedd | character with byte sequence 0x92 0xbe 0xdd in encoding "MULE_INTERNAL" has no equivalent in encoding "ISO_8859_5" - invalid, incomplete char) | \x92be | \x | \x92be | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0xbe - valid (in Big5) | \x666f6f95a3c1 | \x666f6f | \x95a3c1 | character with byte sequence 0x95 0xa3 0xc1 in encoding "MULE_INTERNAL" has no equivalent in encoding "ISO_8859_5" - invalid, incomplete char | \x666f6f95a3 | \x666f6f | \x95a3 | invalid byte sequence for encoding "MULE_INTERNAL": 0x95 0xa3 - invalid, NUL byte | \x9200bedd | \x | \x9200bedd | character with byte sequence 0x92 0x00 0xbe in encoding "MULE_INTERNAL" has no equivalent in encoding "ISO_8859_5" - invalid, NUL byte | \x92bedd00 | \x | \x92bedd00 | character with byte sequence 0x92 0xbe 0xdd in encoding "MULE_INTERNAL" has no equivalent in encoding "ISO_8859_5" - invalid, NUL byte | \x8b00c68bcf8bcf | \x | \x8b00c68bcf8bcf | character with byte sequence 0x8b 0x00 in encoding "MULE_INTERNAL" has no equivalent in encoding "ISO_8859_5" -(10 rows) - -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'sjis')).* from mic_inputs; - description | inbytes | result | errorat | error ----------------------------+------------------+----------+------------------+-------------------------------------------------------------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid (in KOI8R) | \x8bc68bcf8bcf | \x | \x8bc68bcf8bcf | character with byte sequence 0x8b 0xc6 in encoding "MULE_INTERNAL" has no equivalent in encoding "SJIS" - invalid,incomplete char | \x8bc68bcf8b | \x | \x8bc68bcf8b | character with byte sequence 0x8b 0xc6 in encoding "MULE_INTERNAL" has no equivalent in encoding "SJIS" - valid (in SHIFT_JIS) | \x92bedd | \x8fdb | | - invalid, incomplete char) | \x92be | \x | \x92be | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0xbe - valid (in Big5) | \x666f6f95a3c1 | \x666f6f | \x95a3c1 | character with byte sequence 0x95 0xa3 0xc1 in encoding "MULE_INTERNAL" has no equivalent in encoding "SJIS" - invalid, incomplete char | \x666f6f95a3 | \x666f6f | \x95a3 | invalid byte sequence for encoding "MULE_INTERNAL": 0x95 0xa3 - invalid, NUL byte | \x9200bedd | \x | \x9200bedd | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0x00 0xbe - invalid, NUL byte | \x92bedd00 | \x8fdb | \x00 | invalid byte sequence for encoding "MULE_INTERNAL": 0x00 - invalid, NUL byte | \x8b00c68bcf8bcf | \x | \x8b00c68bcf8bcf | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b 0x00 -(10 rows) - -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'big5')).* from mic_inputs; - description | inbytes | result | errorat | error ----------------------------+------------------+--------------+------------------+-------------------------------------------------------------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid (in KOI8R) | \x8bc68bcf8bcf | \x | \x8bc68bcf8bcf | character with byte sequence 0x8b 0xc6 in encoding "MULE_INTERNAL" has no equivalent in encoding "BIG5" - invalid,incomplete char | \x8bc68bcf8b | \x | \x8bc68bcf8b | character with byte sequence 0x8b 0xc6 in encoding "MULE_INTERNAL" has no equivalent in encoding "BIG5" - valid (in SHIFT_JIS) | \x92bedd | \x | \x92bedd | character with byte sequence 0x92 0xbe 0xdd in encoding "MULE_INTERNAL" has no equivalent in encoding "BIG5" - invalid, incomplete char) | \x92be | \x | \x92be | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0xbe - valid (in Big5) | \x666f6f95a3c1 | \x666f6fa2a1 | | - invalid, incomplete char | \x666f6f95a3 | \x666f6f | \x95a3 | invalid byte sequence for encoding "MULE_INTERNAL": 0x95 0xa3 - invalid, NUL byte | \x9200bedd | \x | \x9200bedd | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0x00 0xbe - invalid, NUL byte | \x92bedd00 | \x | \x92bedd00 | character with byte sequence 0x92 0xbe 0xdd in encoding "MULE_INTERNAL" has no equivalent in encoding "BIG5" - invalid, NUL byte | \x8b00c68bcf8bcf | \x | \x8b00c68bcf8bcf | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b 0x00 -(10 rows) - -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'euc_jp')).* from mic_inputs; - description | inbytes | result | errorat | error ----------------------------+------------------+----------+------------------+---------------------------------------------------------------------------------------------------------------- - valid, pure ASCII | \x666f6f | \x666f6f | | - valid (in KOI8R) | \x8bc68bcf8bcf | \x | \x8bc68bcf8bcf | character with byte sequence 0x8b 0xc6 in encoding "MULE_INTERNAL" has no equivalent in encoding "EUC_JP" - invalid,incomplete char | \x8bc68bcf8b | \x | \x8bc68bcf8b | character with byte sequence 0x8b 0xc6 in encoding "MULE_INTERNAL" has no equivalent in encoding "EUC_JP" - valid (in SHIFT_JIS) | \x92bedd | \xbedd | | - invalid, incomplete char) | \x92be | \x | \x92be | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0xbe - valid (in Big5) | \x666f6f95a3c1 | \x666f6f | \x95a3c1 | character with byte sequence 0x95 0xa3 0xc1 in encoding "MULE_INTERNAL" has no equivalent in encoding "EUC_JP" - invalid, incomplete char | \x666f6f95a3 | \x666f6f | \x95a3 | invalid byte sequence for encoding "MULE_INTERNAL": 0x95 0xa3 - invalid, NUL byte | \x9200bedd | \x | \x9200bedd | invalid byte sequence for encoding "MULE_INTERNAL": 0x92 0x00 0xbe - invalid, NUL byte | \x92bedd00 | \xbedd | \x00 | invalid byte sequence for encoding "MULE_INTERNAL": 0x00 - invalid, NUL byte | \x8b00c68bcf8bcf | \x | \x8b00c68bcf8bcf | invalid byte sequence for encoding "MULE_INTERNAL": 0x8b 0x00 -(10 rows) - diff --git a/src/test/regress/expected/copy.out b/src/test/regress/expected/copy.out index 8d5a06563c44a..1714faab39c2f 100644 --- a/src/test/regress/expected/copy.out +++ b/src/test/regress/expected/copy.out @@ -73,6 +73,189 @@ copy copytest3 to stdout csv header; c1,"col with , comma","col with "" quote" 1,a,1 2,b,2 +--- test copying in JSON mode with various styles +copy (select 1 union all select 2) to stdout with (format json); +{"?column?":1} +{"?column?":2} +copy (select 1 as foo union all select 2) to stdout with (format json); +{"foo":1} +{"foo":2} +copy (values (1), (2)) TO stdout with (format json); +{"column1":1} +{"column1":2} +copy (select 1 union all select 2) to stdout with (format json, force_array true); +[ + {"?column?":1} +,{"?column?":2} +] +copy (values (1), (2)) TO stdout with (format json, force_array true); +[ + {"column1":1} +,{"column1":2} +] +copy copytest to stdout json; +{"style":"DOS","test":"abc\r\ndef","filler":1} +{"style":"Unix","test":"abc\ndef","filler":2} +{"style":"Mac","test":"abc\rdef","filler":3} +{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4} +copy copytest to stdout (format json); +{"style":"DOS","test":"abc\r\ndef","filler":1} +{"style":"Unix","test":"abc\ndef","filler":2} +{"style":"Mac","test":"abc\rdef","filler":3} +{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4} +copy (select * from copytest) to stdout (format json); +{"style":"DOS","test":"abc\r\ndef","filler":1} +{"style":"Unix","test":"abc\ndef","filler":2} +{"style":"Mac","test":"abc\rdef","filler":3} +{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4} +-- all of the following should yield error +copy copytest to stdout (format json, delimiter '|'); +ERROR: cannot specify DELIMITER in JSON mode +copy copytest to stdout (format json, null '\N'); +ERROR: cannot specify NULL in JSON mode +copy copytest to stdout (format json, default '|'); +ERROR: cannot specify DEFAULT in JSON mode +copy copytest to stdout (format json, header); +ERROR: cannot specify HEADER in JSON mode +copy copytest to stdout (format json, header 1); +ERROR: cannot specify HEADER in JSON mode +copy copytest to stdout (format json, quote '"'); +ERROR: COPY QUOTE requires CSV mode +copy copytest to stdout (format json, escape '"'); +ERROR: COPY ESCAPE requires CSV mode +copy copytest to stdout (format json, force_quote *); +ERROR: COPY FORCE_QUOTE requires CSV mode +copy copytest to stdout (format json, force_not_null *); +ERROR: COPY FORCE_NOT_NULL requires CSV mode +copy copytest to stdout (format json, force_null *); +ERROR: COPY FORCE_NULL requires CSV mode +copy copytest to stdout (format json, on_error ignore); +ERROR: COPY ON_ERROR cannot be used with COPY TO +LINE 1: copy copytest to stdout (format json, on_error ignore); + ^ +copy copytest to stdout (format json, reject_limit 1); +ERROR: COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE +copy copytest from stdin(format json); +ERROR: COPY FORMAT JSON is not supported for COPY FROM +-- all of the above should yield error +-- column list with json format +copy copytest (style, test, filler) to stdout (format json); +{"style":"DOS","test":"abc\r\ndef","filler":1} +{"style":"Unix","test":"abc\ndef","filler":2} +{"style":"Mac","test":"abc\rdef","filler":3} +{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4} +-- should fail: force_array requires json format +copy copytest to stdout (format csv, force_array true); +ERROR: COPY FORCE_ARRAY can only be used with JSON mode +-- force_array variants +copy copytest to stdout (format json, force_array); +[ + {"style":"DOS","test":"abc\r\ndef","filler":1} +,{"style":"Unix","test":"abc\ndef","filler":2} +,{"style":"Mac","test":"abc\rdef","filler":3} +,{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4} +] +copy copytest(style, test) to stdout (format json, force_array true); +[ + {"style":"DOS","test":"abc\r\ndef"} +,{"style":"Unix","test":"abc\ndef"} +,{"style":"Mac","test":"abc\rdef"} +,{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb"} +] +copy copytest to stdout (format json, force_array false); +{"style":"DOS","test":"abc\r\ndef","filler":1} +{"style":"Unix","test":"abc\ndef","filler":2} +{"style":"Mac","test":"abc\rdef","filler":3} +{"style":"esc\\ape","test":"a\\r\\\r\\\n\\nb","filler":4} +-- force_array with empty result set +copy (select 1 where false) to stdout (format json, force_array); +[ +] +-- column list with diverse data types +create temp table copyjsontest_types ( + id int, + js json, + jsb jsonb, + arr int[], + n numeric(10,2), + b boolean, + ts timestamp, + t text); +insert into copyjsontest_types values +(1, '{"a":1}', '{"b":2}', '{1,2,3}', 3.14, true, + '2024-01-15 10:30:00', 'hello'), +(2, '[1,null,"x"]', '{"nested":{"k":"v"}}', '{4,5}', -99.99, false, + '2024-06-30 23:59:59', 'world'), +(3, 'null', 'null', '{}', null, null, null, null); +-- full table +copy copyjsontest_types to stdout (format json); +{"id":1,"js":{"a":1},"jsb":{"b": 2},"arr":[1,2,3],"n":3.14,"b":true,"ts":"2024-01-15T10:30:00","t":"hello"} +{"id":2,"js":[1,null,"x"],"jsb":{"nested": {"k": "v"}},"arr":[4,5],"n":-99.99,"b":false,"ts":"2024-06-30T23:59:59","t":"world"} +{"id":3,"js":null,"jsb":null,"arr":[],"n":null,"b":null,"ts":null,"t":null} +-- column subsets exercising each type +copy copyjsontest_types (id, js, jsb) to stdout (format json); +{"id":1,"js":{"a":1},"jsb":{"b": 2}} +{"id":2,"js":[1,null,"x"],"jsb":{"nested": {"k": "v"}}} +{"id":3,"js":null,"jsb":null} +copy copyjsontest_types (id, arr, n, b) to stdout (format json); +{"id":1,"arr":[1,2,3],"n":3.14,"b":true} +{"id":2,"arr":[4,5],"n":-99.99,"b":false} +{"id":3,"arr":[],"n":null,"b":null} +copy copyjsontest_types (jsb, t) to stdout (format json); +{"jsb":{"b": 2},"t":"hello"} +{"jsb":{"nested": {"k": "v"}},"t":"world"} +{"jsb":null,"t":null} +copy copyjsontest_types (id, ts) to stdout (format json); +{"id":1,"ts":"2024-01-15T10:30:00"} +{"id":2,"ts":"2024-06-30T23:59:59"} +{"id":3,"ts":null} +-- single column: json and jsonb +copy copyjsontest_types (js) to stdout (format json); +{"js":{"a":1}} +{"js":[1,null,"x"]} +{"js":null} +copy copyjsontest_types (jsb) to stdout (format json); +{"jsb":{"b": 2}} +{"jsb":{"nested": {"k": "v"}}} +{"jsb":null} +drop table copyjsontest_types; +-- embedded escaped characters +create temp table copyjsontest ( + id bigserial, + f1 text, + f2 timestamptz); +insert into copyjsontest + select g.i, + CASE WHEN g.i % 2 = 0 THEN + 'line with '' in it: ' || g.i::text + ELSE + 'line with " in it: ' || g.i::text + END, + 'Mon Feb 10 17:32:01 1997 PST' + from generate_series(1,5) as g(i); +insert into copyjsontest (f1) values +(E'aaa\"bbb'::text), +(E'aaa\\bbb'::text), +(E'aaa\/bbb'::text), +(E'aaa\bbbb'::text), +(E'aaa\fbbb'::text), +(E'aaa\nbbb'::text), +(E'aaa\rbbb'::text), +(E'aaa\tbbb'::text); +copy copyjsontest to stdout json; +{"id":1,"f1":"line with \" in it: 1","f2":"1997-02-10T17:32:01-08:00"} +{"id":2,"f1":"line with ' in it: 2","f2":"1997-02-10T17:32:01-08:00"} +{"id":3,"f1":"line with \" in it: 3","f2":"1997-02-10T17:32:01-08:00"} +{"id":4,"f1":"line with ' in it: 4","f2":"1997-02-10T17:32:01-08:00"} +{"id":5,"f1":"line with \" in it: 5","f2":"1997-02-10T17:32:01-08:00"} +{"id":1,"f1":"aaa\"bbb","f2":null} +{"id":2,"f1":"aaa\\bbb","f2":null} +{"id":3,"f1":"aaa/bbb","f2":null} +{"id":4,"f1":"aaa\bbbb","f2":null} +{"id":5,"f1":"aaa\fbbb","f2":null} +{"id":6,"f1":"aaa\nbbb","f2":null} +{"id":7,"f1":"aaa\rbbb","f2":null} +{"id":8,"f1":"aaa\tbbb","f2":null} create temp table copytest4 ( c1 int, "colname with tab: " text); @@ -81,6 +264,47 @@ copy copytest4 to stdout (header); c1 colname with tab: \t 1 a 2 b +-- test multi-line header line feature +create temp table copytest5 (c1 int); +copy copytest5 from stdin (format csv, header 2); +copy copytest5 to stdout (header); +c1 +1 +2 +truncate copytest5; +copy copytest5 from stdin (format csv, header 4); +select count(*) from copytest5; + count +------- + 0 +(1 row) + +truncate copytest5; +copy copytest5 from stdin (format csv, header 5); +select count(*) from copytest5; + count +------- + 0 +(1 row) + +-- test header line feature (given as strings) +truncate copytest5; +copy copytest5 from stdin (format csv, header '0'); +select * from copytest5 order by c1; + c1 +---- + 1 + 2 +(2 rows) + +truncate copytest5; +copy copytest5 from stdin (format csv, header '1'); +select * from copytest5 order by c1; + c1 +---- + 2 +(1 row) + -- test copy from with a partitioned table create table parted_copytest ( a int, @@ -224,7 +448,7 @@ alter table header_copytest add column c text; copy header_copytest to stdout with (header match); ERROR: cannot use "match" with HEADER in COPY TO copy header_copytest from stdin with (header wrong_choice); -ERROR: header requires a Boolean value or "match" +ERROR: header requires a Boolean value, an integer value greater than or equal to zero, or the string "match" -- works copy header_copytest from stdin with (header match); copy header_copytest (c, a, b) from stdin with (header match); @@ -350,3 +574,23 @@ COPY copytest_mv(id) TO stdout WITH (header); id 1 DROP MATERIALIZED VIEW copytest_mv; +-- Tests for COPY TO with partitioned tables. +-- The child table pp_2 has a different column order than the root table pp. +-- Check if COPY TO exports tuples as the root table's column order. +CREATE TABLE pp (id int,val int) PARTITION BY RANGE (id); +CREATE TABLE pp_1 (val int, id int) PARTITION BY RANGE (id); +CREATE TABLE pp_2 (id int, val int) PARTITION BY RANGE (id); +ALTER TABLE pp ATTACH PARTITION pp_1 FOR VALUES FROM (1) TO (5); +ALTER TABLE pp ATTACH PARTITION pp_2 FOR VALUES FROM (5) TO (10); +CREATE TABLE pp_15 PARTITION OF pp_1 FOR VALUES FROM (1) TO (5); +CREATE TABLE pp_510 PARTITION OF pp_2 FOR VALUES FROM (5) TO (10); +INSERT INTO pp SELECT g, 10 + g FROM generate_series(1,6) g; +COPY pp TO stdout(header); +id val +1 11 +2 12 +3 13 +4 14 +5 15 +6 16 +DROP TABLE PP; diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out index 64ea33aeae8fd..7600e5239d29c 100644 --- a/src/test/regress/expected/copy2.out +++ b/src/test/regress/expected/copy2.out @@ -81,6 +81,10 @@ COPY x from stdin (on_error ignore, on_error ignore); ERROR: conflicting or redundant options LINE 1: COPY x from stdin (on_error ignore, on_error ignore); ^ +COPY x from stdin (on_error set_null, on_error set_null); +ERROR: conflicting or redundant options +LINE 1: COPY x from stdin (on_error set_null, on_error set_null); + ^ COPY x from stdin (log_verbosity default, log_verbosity verbose); ERROR: conflicting or redundant options LINE 1: COPY x from stdin (log_verbosity default, log_verbosity verb... @@ -92,6 +96,10 @@ COPY x from stdin (format BINARY, null 'x'); ERROR: cannot specify NULL in BINARY mode COPY x from stdin (format BINARY, on_error ignore); ERROR: only ON_ERROR STOP is allowed in BINARY mode +COPY x from stdin (format BINARY, on_error set_null); +ERROR: only ON_ERROR STOP is allowed in BINARY mode +COPY x from stdin (on_error set_null, reject_limit 2); +ERROR: COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE COPY x from stdin (on_error unsupported); ERROR: COPY ON_ERROR "unsupported" not recognized LINE 1: COPY x from stdin (on_error unsupported); @@ -124,6 +132,10 @@ COPY x to stdout (format BINARY, on_error unsupported); ERROR: COPY ON_ERROR cannot be used with COPY TO LINE 1: COPY x to stdout (format BINARY, on_error unsupported); ^ +COPY x to stdout (on_error set_null); +ERROR: COPY ON_ERROR cannot be used with COPY TO +LINE 1: COPY x to stdout (on_error set_null); + ^ COPY x from stdin (log_verbosity unsupported); ERROR: COPY LOG_VERBOSITY "unsupported" not recognized LINE 1: COPY x from stdin (log_verbosity unsupported); @@ -132,6 +144,18 @@ COPY x from stdin with (reject_limit 1); ERROR: COPY REJECT_LIMIT requires ON_ERROR to be set to IGNORE COPY x from stdin with (on_error ignore, reject_limit 0); ERROR: REJECT_LIMIT (0) must be greater than zero +COPY x from stdin with (header -1); +ERROR: a negative integer value cannot be specified for header +COPY x from stdin with (header 2.5); +ERROR: header requires a Boolean value, an integer value greater than or equal to zero, or the string "match" +COPY x to stdout with (header 2); +ERROR: cannot use multi-line header in COPY TO +COPY x to stdout with (header '-1'); +ERROR: a negative integer value cannot be specified for header +COPY x from stdin with (header '2.5'); +ERROR: header requires a Boolean value, an integer value greater than or equal to zero, or the string "match" +COPY x to stdout with (header '2'); +ERROR: cannot use multi-line header in COPY TO -- too many columns in column list: should fail COPY x (a, b, c, d, e, d, c) from stdin; ERROR: column "d" specified more than once @@ -157,6 +181,7 @@ COPY x TO stdout WHERE a = 1; ERROR: WHERE clause not allowed with COPY TO LINE 1: COPY x TO stdout WHERE a = 1; ^ +HINT: Try the COPY (SELECT ... WHERE ...) TO variant. COPY x from stdin WHERE a = 50004; COPY x from stdin WHERE a > 60003; COPY x from stdin WHERE f > 60003; @@ -179,6 +204,9 @@ COPY x from stdin WHERE a = row_number() over(b); ERROR: window functions are not allowed in COPY FROM WHERE conditions LINE 1: COPY x from stdin WHERE a = row_number() over(b); ^ +COPY x from stdin WHERE tableoid = 'x'::regclass; +ERROR: system columns are not supported in COPY FROM WHERE conditions +DETAIL: Column "tableoid" is a system column. -- check results of copy in SELECT * FROM x; a | b | c | d | e @@ -769,6 +797,49 @@ CONTEXT: COPY check_ign_err NOTICE: skipping row due to data type incompatibility at line 8 for column "k": "a" CONTEXT: COPY check_ign_err NOTICE: 6 rows were skipped due to data type incompatibility +CREATE DOMAIN d_int_not_null AS integer NOT NULL CHECK (value > 0); +CREATE DOMAIN d_int_positive_maybe_null AS integer CHECK (value > 0); +CREATE TABLE t_on_error_null (a d_int_not_null, b d_int_positive_maybe_null, c integer); +\pset null NULL +COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail +ERROR: domain d_int_not_null does not allow null values +DETAIL: ON_ERROR SET_NULL cannot be applied because column "a" (domain d_int_not_null) does not accept null values. +CONTEXT: COPY t_on_error_null, line 1, column a: null input +COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail +ERROR: domain d_int_not_null does not allow null values +DETAIL: ON_ERROR SET_NULL cannot be applied because column "a" (domain d_int_not_null) does not accept null values. +CONTEXT: COPY t_on_error_null, line 1, column a: "ss" +COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail +ERROR: domain d_int_not_null does not allow null values +DETAIL: ON_ERROR SET_NULL cannot be applied because column "a" (domain d_int_not_null) does not accept null values. +CONTEXT: COPY t_on_error_null, line 1, column a: "-1" +-- fail, less data. +COPY t_on_error_null FROM STDIN WITH (delimiter ',', on_error set_null); +ERROR: missing data for column "c" +CONTEXT: COPY t_on_error_null, line 1: "1,1" +-- fail, extra data. +COPY t_on_error_null FROM STDIN WITH (delimiter ',', on_error set_null); +ERROR: extra data after last expected column +CONTEXT: COPY t_on_error_null, line 1: "1,2,3,4" +COPY t_on_error_null FROM STDIN WITH (on_error set_null, log_verbosity verbose); -- ok +NOTICE: setting to null due to data type incompatibility at line 1 for column "b": "x1" +CONTEXT: COPY t_on_error_null +NOTICE: setting to null due to data type incompatibility at line 1 for column "c": "yx" +CONTEXT: COPY t_on_error_null +NOTICE: setting to null due to data type incompatibility at line 2 for column "b": "zx" +CONTEXT: COPY t_on_error_null +NOTICE: setting to null due to data type incompatibility at line 3 for column "c": "ea" +CONTEXT: COPY t_on_error_null +NOTICE: in 3 rows, columns were set to null due to data type incompatibility +SELECT * FROM t_on_error_null ORDER BY a; + a | b | c +----+------+------ + 10 | NULL | NULL + 11 | NULL | 12 + 13 | 14 | NULL +(3 rows) + +\pset null '' -- tests for on_error option with log_verbosity and null constraint via domain CREATE DOMAIN dcheck_ign_err2 varchar(15) NOT NULL; CREATE TABLE check_ign_err2 (n int, m int[], k int, l dcheck_ign_err2); @@ -828,6 +899,9 @@ DROP VIEW instead_of_insert_tbl_view; DROP VIEW instead_of_insert_tbl_view_2; DROP FUNCTION fun_instead_of_insert_tbl(); DROP TABLE check_ign_err; +DROP TABLE t_on_error_null; +DROP DOMAIN d_int_not_null; +DROP DOMAIN d_int_positive_maybe_null; DROP TABLE check_ign_err2; DROP DOMAIN dcheck_ign_err2; DROP TABLE hard_err; diff --git a/src/test/regress/expected/copyencoding.out b/src/test/regress/expected/copyencoding.out index cfa2ed6df0081..76ea0e7cf04b2 100644 --- a/src/test/regress/expected/copyencoding.out +++ b/src/test/regress/expected/copyencoding.out @@ -17,6 +17,13 @@ CREATE TABLE copy_encoding_tab (t text); COPY (SELECT E'\u3042') TO :'utf8_csv' WITH (FORMAT csv, ENCODING 'UTF8'); -- Read UTF8 data as LATIN1: no error COPY copy_encoding_tab FROM :'utf8_csv' WITH (FORMAT csv, ENCODING 'LATIN1'); +-- Non-server encodings have distinct code paths. +\set fname :abs_builddir '/results/copyencoding_gb18030.csv' +COPY (SELECT E'\u3042,') TO :'fname' WITH (FORMAT csv, ENCODING 'GB18030'); +COPY copy_encoding_tab FROM :'fname' WITH (FORMAT csv, ENCODING 'GB18030'); +\set fname :abs_builddir '/results/copyencoding_gb18030.data' +COPY (SELECT E'\u3042,') TO :'fname' WITH (FORMAT text, ENCODING 'GB18030'); +COPY copy_encoding_tab FROM :'fname' WITH (FORMAT text, ENCODING 'GB18030'); -- Use client_encoding SET client_encoding TO UTF8; -- U+3042 HIRAGANA LETTER A diff --git a/src/test/regress/expected/create_cast.out b/src/test/regress/expected/create_cast.out index fd4871d94db72..0e69644bca2a0 100644 --- a/src/test/regress/expected/create_cast.out +++ b/src/test/regress/expected/create_cast.out @@ -28,14 +28,16 @@ SELECT casttestfunc('foo'::text); -- fails, as there's no cast ERROR: function casttestfunc(text) does not exist LINE 1: SELECT casttestfunc('foo'::text); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- Try binary coercion cast CREATE CAST (text AS casttesttype) WITHOUT FUNCTION; SELECT casttestfunc('foo'::text); -- doesn't work, as the cast is explicit ERROR: function casttestfunc(text) does not exist LINE 1: SELECT casttestfunc('foo'::text); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. SELECT casttestfunc('foo'::text::casttesttype); -- should work casttestfunc -------------- diff --git a/src/test/regress/expected/create_function_sql.out b/src/test/regress/expected/create_function_sql.out index 963b6f863ff95..42524230d2b62 100644 --- a/src/test/regress/expected/create_function_sql.out +++ b/src/test/regress/expected/create_function_sql.out @@ -304,7 +304,8 @@ CREATE FUNCTION functest_S_xx(x date) RETURNS boolean ERROR: operator does not exist: date > integer LINE 3: RETURN x > 1; ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- tricky parsing CREATE FUNCTION functest_S_15(x int) RETURNS boolean LANGUAGE SQL @@ -454,6 +455,16 @@ DROP TABLE functest3 CASCADE; NOTICE: drop cascades to 2 other objects DETAIL: drop cascades to view functestv3 drop cascades to function functest_s_14() +-- Check reporting of temporary-object dependencies within SQL-standard body +-- (tests elsewhere already cover dependencies on arg and result types) +CREATE TEMP SEQUENCE mytempseq; +CREATE FUNCTION functest_tempseq() RETURNS int + RETURN nextval('mytempseq'); +NOTICE: function "functest_tempseq" will be effectively temporary +DETAIL: It depends on temporary sequence mytempseq. +-- This discards mytempseq and therefore functest_tempseq(). If it fails to, +-- the function will appear in the information_schema tests below. +DISCARD TEMP; -- information_schema tests CREATE FUNCTION functest_IS_1(a int, b int default 1, c text default 'foo') RETURNS int @@ -733,6 +744,22 @@ SELECT double_append(array_append(ARRAY[q1], q2), q3) {4,5,6,4,5,6} (2 rows) +-- Check that we can re-use a SQLFunctionCache after a run-time error. +-- This function will fail with zero-divide at run time (not plan time). +CREATE FUNCTION part_hashint4_error(value int4, seed int8) RETURNS int8 +LANGUAGE SQL STRICT IMMUTABLE PARALLEL SAFE AS +$$ SELECT value + seed + random()::int/0 $$; +-- Put it into an operator class so that FmgrInfo will be cached in relcache. +CREATE OPERATOR CLASS part_test_int4_ops_bad FOR TYPE int4 USING hash AS + FUNCTION 2 part_hashint4_error(int4, int8); +CREATE TABLE pt(i int) PARTITION BY hash (i part_test_int4_ops_bad); +CREATE TABLE p1 PARTITION OF pt FOR VALUES WITH (modulus 4, remainder 0); +INSERT INTO pt VALUES (1); +ERROR: division by zero +CONTEXT: SQL function "part_hashint4_error" statement 1 +INSERT INTO pt VALUES (1); +ERROR: division by zero +CONTEXT: SQL function "part_hashint4_error" statement 1 -- Things that shouldn't work: CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL AS 'SELECT ''not an integer'';'; @@ -773,7 +800,7 @@ CONTEXT: SQL function "test1" during startup RESET check_function_bodies; -- Cleanup DROP SCHEMA temp_func_test CASCADE; -NOTICE: drop cascades to 35 other objects +NOTICE: drop cascades to 38 other objects DETAIL: drop cascades to function functest_a_1(text,date) drop cascades to function functest_a_2(text[]) drop cascades to function functest_a_3() @@ -808,6 +835,9 @@ drop cascades to function create_and_insert() drop cascades to table ddl_test drop cascades to function alter_and_insert() drop cascades to function double_append(anyarray,anyelement) +drop cascades to function part_hashint4_error(integer,bigint) +drop cascades to operator family part_test_int4_ops_bad for access method hash +drop cascades to table pt drop cascades to function test1(anyelement) DROP USER regress_unpriv_user; RESET search_path; diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 9ade7b835e69f..55538c4c41e88 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -32,6 +32,20 @@ COMMENT ON INDEX six_wrong IS 'bad index'; ERROR: relation "six_wrong" does not exist COMMENT ON INDEX six IS 'good index'; COMMENT ON INDEX six IS NULL; +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; + six_comment_is_null +--------------------- + t +(1 row) + +COMMENT ON INDEX six IS 'add the comment back'; +COMMENT ON INDEX six IS ''; -- empty string removes the comment, same as NULL +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; + six_comment_is_null +--------------------- + t +(1 row) + -- -- BTREE partial indices -- @@ -593,7 +607,7 @@ SELECT point(x,x), (SELECT f1 FROM gpolygon_tbl ORDER BY f1 <-> point(x,x) LIMIT QUERY PLAN -------------------------------------------------------------------------------------------- Function Scan on generate_series x - SubPlan 1 + SubPlan expr_1 -> Limit -> Index Scan using ggpolygonind on gpolygon_tbl Order By: (f1 <-> point((x.x)::double precision, (x.x)::double precision)) @@ -1624,8 +1638,8 @@ DROP TABLE cwi_test; -- CREATE TABLE syscol_table (a INT); -- System columns cannot be indexed -CREATE INDEX ON syscolcol_table (ctid); -ERROR: relation "syscolcol_table" does not exist +CREATE INDEX ON syscol_table (ctid); +ERROR: index creation on system columns is not supported -- nor used in expressions CREATE INDEX ON syscol_table ((ctid >= '(1000,0)')); ERROR: index creation on system columns is not supported @@ -1908,11 +1922,11 @@ SELECT * FROM tenk1 EXPLAIN (COSTS OFF) SELECT * FROM tenk1 WHERE thousand = 42 AND (tenthous = 1 OR tenthous = (SELECT 1 + 2) OR tenthous = 42); - QUERY PLAN ----------------------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------------------- Index Scan using tenk1_thous_tenthous on tenk1 - Index Cond: ((thousand = 42) AND (tenthous = ANY (ARRAY[1, (InitPlan 1).col1, 42]))) - InitPlan 1 + Index Cond: ((thousand = 42) AND (tenthous = ANY (ARRAY[1, (InitPlan expr_1).col1, 42]))) + InitPlan expr_1 -> Result (4 rows) @@ -2043,8 +2057,8 @@ SELECT count(*) FROM tenk1 t1 ---------------------------------------------------------------------------- Aggregate -> Index Only Scan using tenk1_thous_tenthous on tenk1 t1 - Filter: ((thousand = 42) OR (thousand = (SubPlan 1))) - SubPlan 1 + Filter: ((thousand = 42) OR (thousand = (SubPlan expr_1))) + SubPlan expr_1 -> Limit -> Index Only Scan using tenk1_thous_tenthous on tenk1 t2 Index Cond: (thousand = (t1.tenthous + 1)) diff --git a/src/test/regress/expected/create_operator.out b/src/test/regress/expected/create_operator.out index d776d9c18c3a7..5c7159665e221 100644 --- a/src/test/regress/expected/create_operator.out +++ b/src/test/regress/expected/create_operator.out @@ -24,6 +24,25 @@ SELECT @#@ 24; 620448401733239439360000 (1 row) +-- Test error cases +select @@##@@ 24; -- no such operator +ERROR: operator does not exist: @@##@@ integer +LINE 1: select @@##@@ 24; + ^ +DETAIL: There is no operator of that name. +set search_path = pg_catalog; +select @#@ 24; -- wrong schema +ERROR: operator does not exist: @#@ integer +LINE 1: select @#@ 24; + ^ +DETAIL: An operator of that name exists, but it is not in the search_path. +reset search_path; +select @#@ 24.0; -- wrong data type +ERROR: operator does not exist: @#@ numeric +LINE 1: select @#@ 24.0; + ^ +DETAIL: No operator of that name accepts the given argument type. +HINT: You might need to add an explicit type cast. -- Test comments COMMENT ON OPERATOR ###### (NONE, int4) IS 'bad prefix'; ERROR: operator does not exist: ###### integer diff --git a/src/test/regress/expected/create_procedure.out b/src/test/regress/expected/create_procedure.out index 45b402e25e79c..f89042cf7987b 100644 --- a/src/test/regress/expected/create_procedure.out +++ b/src/test/regress/expected/create_procedure.out @@ -2,7 +2,7 @@ CALL nonexistent(); -- error ERROR: procedure nonexistent() does not exist LINE 1: CALL nonexistent(); ^ -HINT: No procedure matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no procedure of that name. CALL random(); -- error ERROR: random() is not a procedure LINE 1: CALL random(); @@ -299,7 +299,8 @@ CALL ptest9(1./0.); -- error ERROR: procedure ptest9(numeric) does not exist LINE 1: CALL ptest9(1./0.); ^ -HINT: No procedure matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No procedure of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- check named-parameter matching CREATE PROCEDURE ptest10(OUT a int, IN b int, IN c int) LANGUAGE SQL AS $$ SELECT b - c $$; diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out new file mode 100644 index 0000000000000..bc9a596ec89a4 --- /dev/null +++ b/src/test/regress/expected/create_property_graph.out @@ -0,0 +1,926 @@ +CREATE SCHEMA create_property_graph_tests; +GRANT USAGE ON SCHEMA create_property_graph_tests TO PUBLIC; +SET search_path = create_property_graph_tests; +CREATE SCHEMA create_property_graph_tests_2; +GRANT USAGE ON SCHEMA create_property_graph_tests_2 TO PUBLIC; +CREATE ROLE regress_graph_user1; +CREATE ROLE regress_graph_user2; +CREATE PROPERTY GRAPH g1; +COMMENT ON PROPERTY GRAPH g1 IS 'a graph'; +CREATE PROPERTY GRAPH g1; -- error: duplicate +ERROR: relation "g1" already exists +CREATE TABLE t1 (a int, b text); +CREATE TABLE t2 (i int PRIMARY KEY, j int, k int); +CREATE TABLE t3 (x int, y text, z text); +CREATE TABLE e1 (a int, i int, t text, PRIMARY KEY (a, i)); +CREATE TABLE e2 (a int, x int, t text); +CREATE PROPERTY GRAPH g2 + VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL, t3 KEY (x) LABEL t3l1 LABEL t3l2) + EDGE TABLES ( + e1 + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (i) REFERENCES t2 (i), + e2 KEY (a, x) + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (x, t) REFERENCES t3 (x, y) + ); +-- test dependencies/object descriptions +DROP TABLE t1; -- fail +ERROR: cannot drop table t1 because other objects depend on it +DETAIL: vertex t1 of property graph g2 depends on table t1 +edge e1 of property graph g2 depends on table t1 +edge e2 of property graph g2 depends on table t1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER TABLE t1 DROP COLUMN b; -- non-key column; fail +ERROR: cannot drop column b of table t1 because other objects depend on it +DETAIL: property b of label t1 of vertex t1 of property graph g2 depends on column b of table t1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER TABLE t1 DROP COLUMN a; -- key column; fail +ERROR: cannot drop column a of table t1 because other objects depend on it +DETAIL: vertex t1 of property graph g2 depends on column a of table t1 +edge e1 of property graph g2 depends on column a of table t1 +edge e2 of property graph g2 depends on column a of table t1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- like g2 but assembled with ALTER +CREATE PROPERTY GRAPH g3; +ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL); +ALTER PROPERTY GRAPH g3 + ADD VERTEX TABLES (t3 KEY (x) LABEL t3l1) + ADD EDGE TABLES ( + e1 SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i), + e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) + ); +ALTER PROPERTY GRAPH g3 + ALTER VERTEX TABLE t3 + ADD LABEL t3l2 PROPERTIES ALL COLUMNS + ADD LABEL t3l3 PROPERTIES ALL COLUMNS; +ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3x; -- error +ERROR: property graph "g3" element "t3" has no label "t3l3x" +ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3; +ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2); -- fail +ERROR: cannot drop vertex t2 of property graph g3 because other objects depend on it +DETAIL: edge e1 of property graph g3 depends on vertex t2 of property graph g3 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2) CASCADE; +NOTICE: drop cascades to edge e1 of property graph g3 +ALTER PROPERTY GRAPH g3 DROP EDGE TABLES (e2); +CREATE PROPERTY GRAPH g4 + VERTEX TABLES ( + t1 KEY (a) NO PROPERTIES, + t2 DEFAULT LABEL PROPERTIES (i + j AS i_j, k), + t3 KEY (x) LABEL t3l1 PROPERTIES (x, y AS yy) LABEL t3l2 PROPERTIES (x, z AS zz) + ) + EDGE TABLES ( + e1 + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (i) REFERENCES t2 (i) + PROPERTIES ALL COLUMNS, + e2 KEY (a, x) + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (x, t) REFERENCES t3 (x, y) + PROPERTIES ALL COLUMNS + ); +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 ADD PROPERTIES (k * 2 AS kk); +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 DROP PROPERTIES (k); +CREATE TABLE t11 (a int PRIMARY KEY); +CREATE TABLE t12 (b int PRIMARY KEY); +CREATE TABLE t13 ( + c int PRIMARY KEY, + d int REFERENCES t11, + e int REFERENCES t12 +); +CREATE PROPERTY GRAPH g5 + VERTEX TABLES (t11, t12) + EDGE TABLES (t13 SOURCE t11 DESTINATION t12); +SELECT pg_get_propgraphdef('g5'::regclass); + pg_get_propgraphdef +------------------------------------------------------------------------------------------------------------------- + CREATE PROPERTY GRAPH create_property_graph_tests.g5 + + VERTEX TABLES ( + + t11 KEY (a) PROPERTIES (a), + + t12 KEY (b) PROPERTIES (b) + + ) + + EDGE TABLES ( + + t13 KEY (c) SOURCE KEY (d) REFERENCES t11 (a) DESTINATION KEY (e) REFERENCES t12 (b) PROPERTIES (c, d, e)+ + ) +(1 row) + +-- error cases +CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy); +ERROR: property graphs cannot be unlogged because they do not have storage +CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy); +ERROR: relation "xx" does not exist +CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a)); +ERROR: alias "t1" used more than once as element table +LINE 1: ...Y GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a)... + ^ +ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t3 KEY (x)); -- duplicate alias +ERROR: alias "t3" already exists in property graph "g3" +LINE 1: ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t3 KEY (x)); + ^ +CREATE PROPERTY GRAPH gx + VERTEX TABLES (t1 AS tt KEY (a), t2 KEY (i)) + EDGE TABLES ( + e1 SOURCE t1 DESTINATION t2 + ); +ERROR: source vertex "t1" of edge "e1" does not exist +LINE 4: e1 SOURCE t1 DESTINATION t2 + ^ +CREATE PROPERTY GRAPH gx + VERTEX TABLES (t1 KEY (a), t2 KEY (i)) + EDGE TABLES ( + e1 SOURCE t1 DESTINATION tx + ); +ERROR: destination vertex "tx" of edge "e1" does not exist +LINE 4: e1 SOURCE t1 DESTINATION tx + ^ +COMMENT ON PROPERTY GRAPH gx IS 'not a graph'; +ERROR: relation "gx" does not exist +CREATE PROPERTY GRAPH gx + VERTEX TABLES (t1 KEY (a), t2) + EDGE TABLES ( + e1 SOURCE t1 DESTINATION t2 -- no foreign keys + ); +ERROR: no SOURCE key specified and no suitable foreign key exists for definition of edge "e1" +LINE 4: e1 SOURCE t1 DESTINATION t2 -- no foreign keys + ^ +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) + LABEL foo PROPERTIES (a + 1 AS aa) + LABEL bar PROPERTIES (1 + a AS aa) -- expression mismatch + ); +ERROR: element "t1" property "aa" expression mismatch: (1 + a) vs. (a + 1) +DETAIL: In a property graph element, a property of the same name has to have the same expression in each label. +ALTER PROPERTY GRAPH g2 + ADD VERTEX TABLES ( + t1 AS t1x KEY (a) + LABEL foo PROPERTIES (a + 1 AS aa) + LABEL bar PROPERTIES (1 + a AS aa) -- expression mismatch + ); +ERROR: element "t1x" property "aa" expression mismatch: (1 + a) vs. (a + 1) +DETAIL: In a property graph element, a property of the same name has to have the same expression in each label. +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) PROPERTIES (b AS p1), + t2 PROPERTIES (k AS p1) -- type mismatch + ); +ERROR: property "p1" data type mismatch: text vs. integer +DETAIL: In a property graph, a property of the same name has to have the same data type in each label. +ALTER PROPERTY GRAPH g2 ALTER VERTEX TABLE t1 ADD LABEL foo PROPERTIES (b AS k); -- type mismatch +ERROR: property "k" data type mismatch: integer vs. text +DETAIL: In a property graph, a property of the same name has to have the same data type in each label. +CREATE TABLE t1x (a int, b varchar(10)); +CREATE TABLE t2x (i int, j varchar(15)); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1x KEY (a) PROPERTIES (b AS p1), + t2x KEY (i) PROPERTIES (j AS p1) -- typmod mismatch + ); +ERROR: property "p1" data type mismatch: character varying(10) vs. character varying(15) +DETAIL: In a property graph, a property of the same name has to have the same data type in each label. +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1x KEY (a) PROPERTIES (b::varchar(20) AS p1), + t2x KEY (i) PROPERTIES (j::varchar(25) AS p1) -- typmod mismatch + ); +ERROR: property "p1" data type mismatch: character varying(20) vs. character varying(25) +DETAIL: In a property graph, a property of the same name has to have the same data type in each label. +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1x KEY (a) PROPERTIES (b::varchar(20) AS p1), + t2x KEY (i) PROPERTIES (j::varchar(20) AS p1) -- matching typmods by casting works + ); +DROP PROPERTY GRAPH gx; +DROP TABLE t1x, t2x; +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) LABEL l1 PROPERTIES (a, a AS aa), + t2 KEY (i) LABEL l1 PROPERTIES (i AS a, j AS b, k) -- mismatching number of properties on label + ); +ERROR: mismatching number of properties in definition of label "l1" +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) LABEL l1 PROPERTIES (a, b), + t2 KEY (i) LABEL l1 PROPERTIES (i AS a) -- mismatching number of properties on label + ); +ERROR: mismatching number of properties in definition of label "l1" +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) LABEL l1 PROPERTIES (a, b), + t2 KEY (i) LABEL l1 PROPERTIES (i AS a, j AS j) -- mismatching property names on label + ); +ERROR: mismatching properties names in definition of label "l1" +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t1 ADD LABEL t3l1 PROPERTIES (a AS x, b AS yy, b AS zz); -- mismatching number of properties on label +ERROR: mismatching number of properties in definition of label "t3l1" +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t1 ADD LABEL t3l1 PROPERTIES (a AS x, b AS zz); -- mismatching property names on label +ERROR: mismatching properties names in definition of label "t3l1" +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t1 ADD LABEL t3l1 PROPERTIES (a AS x); -- mismatching number of properties on label +ERROR: mismatching number of properties in definition of label "t3l1" +ALTER PROPERTY GRAPH g1 OWNER TO regress_graph_user1; +SET ROLE regress_graph_user1; +GRANT SELECT ON PROPERTY GRAPH g1 TO regress_graph_user2; +GRANT UPDATE ON PROPERTY GRAPH g1 TO regress_graph_user2; -- fail +ERROR: invalid privilege type UPDATE for property graph +RESET ROLE; +-- collation +CREATE TABLE tc1 (a int, b text); +CREATE TABLE tc2 (a int, b text); +CREATE TABLE tc3 (a int, b text COLLATE "C"); +CREATE TABLE ec1 (ek1 int, ek2 int, eb text); +CREATE TABLE ec2 (ek1 int, ek2 int, eb text COLLATE "POSIX"); +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a), tc2 KEY (a), tc3 KEY (a)); -- fail +ERROR: property "b" collation mismatch: default vs. C +DETAIL: In a property graph, a property of the same name has to have the same collation in each label. +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a), tc2 KEY (a)) + EDGE TABLES ( + ec1 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a), + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + ); -- fail +ERROR: property "eb" collation mismatch: default vs. POSIX +DETAIL: In a property graph, a property of the same name has to have the same collation in each label. +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a) DEFAULT LABEL PROPERTIES (a), tc3 KEY (b)) + EDGE TABLES ( + ec2 KEY (ek1, eb) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (eb) REFERENCES tc3 (b) + ); -- fail +ERROR: collation mismatch in DESTINATION key of edge "ec2": POSIX vs. C +LINE 4: ec2 KEY (ek1, eb) + ^ +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a), tc2 KEY (a)) + EDGE TABLES ( + ec1 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + ); +ALTER PROPERTY GRAPH gc1 ADD VERTEX TABLES (tc3 KEY (a)); -- fail +ERROR: property "b" collation mismatch: default vs. C +DETAIL: In a property graph, a property of the same name has to have the same collation in each label. +ALTER PROPERTY GRAPH gc1 + ADD EDGE TABLES ( + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + ); -- fail +ERROR: property "eb" collation mismatch: default vs. POSIX +DETAIL: In a property graph, a property of the same name has to have the same collation in each label. +ALTER PROPERTY GRAPH gc1 + ADD VERTEX TABLES ( + tc3 KEY (a) DEFAULT LABEL PROPERTIES (a, b COLLATE pg_catalog.DEFAULT AS b) + ); +ALTER PROPERTY GRAPH gc1 + ADD EDGE TABLES ( + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + DEFAULT LABEL PROPERTIES (ek1, ek2, eb COLLATE pg_catalog.DEFAULT AS eb) + ); +DROP PROPERTY GRAPH gc1; +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES ( + tc1 KEY (a) DEFAULT LABEL PROPERTIES (a, b::varchar COLLATE "C" AS b), + tc2 KEY (a) DEFAULT LABEL PROPERTIES (a, (b COLLATE "C")::varchar AS b), + tc3 KEY (a) DEFAULT LABEL PROPERTIES (a, b::varchar AS b) + ) + EDGE TABLES ( + ec1 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + DEFAULT LABEL PROPERTIES (ek1, ek2, eb), + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + DEFAULT LABEL PROPERTIES (ek1, ek2, eb COLLATE pg_catalog.DEFAULT AS eb) + ); +-- type inconsistency check +CREATE TABLE v1 (a int primary key, b text); +CREATE TABLE e(k1 text, k2 text, c text); +CREATE TABLE v2 (m text, n text); +CREATE PROPERTY GRAPH gt + VERTEX TABLES (v1 KEY (a), v2 KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2(m) + ); -- fail +ERROR: no equality operator exists for SOURCE key comparison of edge "e" +LINE 4: e KEY (k1, k2) + ^ +ALTER TABLE e DROP COLUMN k1, ADD COLUMN k1 bigint primary key; +CREATE PROPERTY GRAPH gt + VERTEX TABLES (v1 KEY (a), v2 KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2(m) + ); +-- information schema +SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name; + property_graph_catalog | property_graph_schema | property_graph_name +------------------------+-----------------------------+--------------------- + regression | create_property_graph_tests | g1 + regression | create_property_graph_tests | g2 + regression | create_property_graph_tests | g3 + regression | create_property_graph_tests | g4 + regression | create_property_graph_tests | g5 + regression | create_property_graph_tests | gc1 + regression | create_property_graph_tests | gt +(7 rows) + +SELECT * FROM information_schema.pg_element_tables ORDER BY property_graph_name, element_table_alias; + property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | element_table_kind | table_catalog | table_schema | table_name | element_table_definition +------------------------+-----------------------------+---------------------+---------------------+--------------------+---------------+-----------------------------+------------+-------------------------- + regression | create_property_graph_tests | g2 | e1 | EDGE | regression | create_property_graph_tests | e1 | + regression | create_property_graph_tests | g2 | e2 | EDGE | regression | create_property_graph_tests | e2 | + regression | create_property_graph_tests | g2 | t1 | VERTEX | regression | create_property_graph_tests | t1 | + regression | create_property_graph_tests | g2 | t2 | VERTEX | regression | create_property_graph_tests | t2 | + regression | create_property_graph_tests | g2 | t3 | VERTEX | regression | create_property_graph_tests | t3 | + regression | create_property_graph_tests | g3 | t1 | VERTEX | regression | create_property_graph_tests | t1 | + regression | create_property_graph_tests | g3 | t3 | VERTEX | regression | create_property_graph_tests | t3 | + regression | create_property_graph_tests | g4 | e1 | EDGE | regression | create_property_graph_tests | e1 | + regression | create_property_graph_tests | g4 | e2 | EDGE | regression | create_property_graph_tests | e2 | + regression | create_property_graph_tests | g4 | t1 | VERTEX | regression | create_property_graph_tests | t1 | + regression | create_property_graph_tests | g4 | t2 | VERTEX | regression | create_property_graph_tests | t2 | + regression | create_property_graph_tests | g4 | t3 | VERTEX | regression | create_property_graph_tests | t3 | + regression | create_property_graph_tests | g5 | t11 | VERTEX | regression | create_property_graph_tests | t11 | + regression | create_property_graph_tests | g5 | t12 | VERTEX | regression | create_property_graph_tests | t12 | + regression | create_property_graph_tests | g5 | t13 | EDGE | regression | create_property_graph_tests | t13 | + regression | create_property_graph_tests | gc1 | ec1 | EDGE | regression | create_property_graph_tests | ec1 | + regression | create_property_graph_tests | gc1 | ec2 | EDGE | regression | create_property_graph_tests | ec2 | + regression | create_property_graph_tests | gc1 | tc1 | VERTEX | regression | create_property_graph_tests | tc1 | + regression | create_property_graph_tests | gc1 | tc2 | VERTEX | regression | create_property_graph_tests | tc2 | + regression | create_property_graph_tests | gc1 | tc3 | VERTEX | regression | create_property_graph_tests | tc3 | + regression | create_property_graph_tests | gt | e | EDGE | regression | create_property_graph_tests | e | + regression | create_property_graph_tests | gt | v1 | VERTEX | regression | create_property_graph_tests | v1 | + regression | create_property_graph_tests | gt | v2 | VERTEX | regression | create_property_graph_tests | v2 | +(23 rows) + +SELECT * FROM information_schema.pg_element_table_key_columns ORDER BY property_graph_name, element_table_alias, ordinal_position; + property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | column_name | ordinal_position +------------------------+-----------------------------+---------------------+---------------------+-------------+------------------ + regression | create_property_graph_tests | g2 | e1 | a | 1 + regression | create_property_graph_tests | g2 | e1 | i | 2 + regression | create_property_graph_tests | g2 | e2 | a | 1 + regression | create_property_graph_tests | g2 | e2 | x | 2 + regression | create_property_graph_tests | g2 | t1 | a | 1 + regression | create_property_graph_tests | g2 | t2 | i | 1 + regression | create_property_graph_tests | g2 | t3 | x | 1 + regression | create_property_graph_tests | g3 | t1 | a | 1 + regression | create_property_graph_tests | g3 | t3 | x | 1 + regression | create_property_graph_tests | g4 | e1 | a | 1 + regression | create_property_graph_tests | g4 | e1 | i | 2 + regression | create_property_graph_tests | g4 | e2 | a | 1 + regression | create_property_graph_tests | g4 | e2 | x | 2 + regression | create_property_graph_tests | g4 | t1 | a | 1 + regression | create_property_graph_tests | g4 | t2 | i | 1 + regression | create_property_graph_tests | g4 | t3 | x | 1 + regression | create_property_graph_tests | g5 | t11 | a | 1 + regression | create_property_graph_tests | g5 | t12 | b | 1 + regression | create_property_graph_tests | g5 | t13 | c | 1 + regression | create_property_graph_tests | gc1 | ec1 | ek1 | 1 + regression | create_property_graph_tests | gc1 | ec1 | ek2 | 2 + regression | create_property_graph_tests | gc1 | ec2 | ek1 | 1 + regression | create_property_graph_tests | gc1 | ec2 | ek2 | 2 + regression | create_property_graph_tests | gc1 | tc1 | a | 1 + regression | create_property_graph_tests | gc1 | tc2 | a | 1 + regression | create_property_graph_tests | gc1 | tc3 | a | 1 + regression | create_property_graph_tests | gt | e | k1 | 1 + regression | create_property_graph_tests | gt | e | k2 | 2 + regression | create_property_graph_tests | gt | v1 | a | 1 + regression | create_property_graph_tests | gt | v2 | m | 1 +(30 rows) + +SELECT * FROM information_schema.pg_edge_table_components ORDER BY property_graph_name, edge_table_alias, edge_end DESC, ordinal_position; + property_graph_catalog | property_graph_schema | property_graph_name | edge_table_alias | vertex_table_alias | edge_end | edge_table_column_name | vertex_table_column_name | ordinal_position +------------------------+-----------------------------+---------------------+------------------+--------------------+-------------+------------------------+--------------------------+------------------ + regression | create_property_graph_tests | g2 | e1 | t1 | SOURCE | a | a | 1 + regression | create_property_graph_tests | g2 | e1 | t2 | DESTINATION | i | i | 1 + regression | create_property_graph_tests | g2 | e2 | t1 | SOURCE | a | a | 1 + regression | create_property_graph_tests | g2 | e2 | t3 | DESTINATION | x | x | 1 + regression | create_property_graph_tests | g2 | e2 | t3 | DESTINATION | t | y | 2 + regression | create_property_graph_tests | g4 | e1 | t1 | SOURCE | a | a | 1 + regression | create_property_graph_tests | g4 | e1 | t2 | DESTINATION | i | i | 1 + regression | create_property_graph_tests | g4 | e2 | t1 | SOURCE | a | a | 1 + regression | create_property_graph_tests | g4 | e2 | t3 | DESTINATION | x | x | 1 + regression | create_property_graph_tests | g4 | e2 | t3 | DESTINATION | t | y | 2 + regression | create_property_graph_tests | g5 | t13 | t11 | SOURCE | d | a | 1 + regression | create_property_graph_tests | g5 | t13 | t12 | DESTINATION | e | b | 1 + regression | create_property_graph_tests | gc1 | ec1 | tc1 | SOURCE | ek1 | a | 1 + regression | create_property_graph_tests | gc1 | ec1 | tc2 | DESTINATION | ek2 | a | 1 + regression | create_property_graph_tests | gc1 | ec2 | tc1 | SOURCE | ek1 | a | 1 + regression | create_property_graph_tests | gc1 | ec2 | tc2 | DESTINATION | ek2 | a | 1 + regression | create_property_graph_tests | gt | e | v1 | SOURCE | k1 | a | 1 + regression | create_property_graph_tests | gt | e | v2 | DESTINATION | k2 | m | 1 +(18 rows) + +SELECT * FROM information_schema.pg_element_table_labels ORDER BY property_graph_name, element_table_alias, label_name; + property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | label_name +------------------------+-----------------------------+---------------------+---------------------+------------ + regression | create_property_graph_tests | g2 | e1 | e1 + regression | create_property_graph_tests | g2 | e2 | e2 + regression | create_property_graph_tests | g2 | t1 | t1 + regression | create_property_graph_tests | g2 | t2 | t2 + regression | create_property_graph_tests | g2 | t3 | t3l1 + regression | create_property_graph_tests | g2 | t3 | t3l2 + regression | create_property_graph_tests | g3 | t1 | t1 + regression | create_property_graph_tests | g3 | t3 | t3l1 + regression | create_property_graph_tests | g3 | t3 | t3l2 + regression | create_property_graph_tests | g4 | e1 | e1 + regression | create_property_graph_tests | g4 | e2 | e2 + regression | create_property_graph_tests | g4 | t1 | t1 + regression | create_property_graph_tests | g4 | t2 | t2 + regression | create_property_graph_tests | g4 | t3 | t3l1 + regression | create_property_graph_tests | g4 | t3 | t3l2 + regression | create_property_graph_tests | g5 | t11 | t11 + regression | create_property_graph_tests | g5 | t12 | t12 + regression | create_property_graph_tests | g5 | t13 | t13 + regression | create_property_graph_tests | gc1 | ec1 | ec1 + regression | create_property_graph_tests | gc1 | ec2 | ec2 + regression | create_property_graph_tests | gc1 | tc1 | tc1 + regression | create_property_graph_tests | gc1 | tc2 | tc2 + regression | create_property_graph_tests | gc1 | tc3 | tc3 + regression | create_property_graph_tests | gt | e | e + regression | create_property_graph_tests | gt | v1 | v1 + regression | create_property_graph_tests | gt | v2 | v2 +(26 rows) + +SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_graph_name, element_table_alias, property_name; + property_graph_catalog | property_graph_schema | property_graph_name | element_table_alias | property_name | property_expression +------------------------+-----------------------------+---------------------+---------------------+---------------+-------------------------------------- + regression | create_property_graph_tests | g2 | e1 | a | a + regression | create_property_graph_tests | g2 | e1 | i | i + regression | create_property_graph_tests | g2 | e1 | t | t + regression | create_property_graph_tests | g2 | e2 | a | a + regression | create_property_graph_tests | g2 | e2 | t | t + regression | create_property_graph_tests | g2 | e2 | x | x + regression | create_property_graph_tests | g2 | t1 | a | a + regression | create_property_graph_tests | g2 | t1 | b | b + regression | create_property_graph_tests | g2 | t2 | i | i + regression | create_property_graph_tests | g2 | t2 | j | j + regression | create_property_graph_tests | g2 | t2 | k | k + regression | create_property_graph_tests | g2 | t3 | x | x + regression | create_property_graph_tests | g2 | t3 | y | y + regression | create_property_graph_tests | g2 | t3 | z | z + regression | create_property_graph_tests | g3 | t1 | a | a + regression | create_property_graph_tests | g3 | t1 | b | b + regression | create_property_graph_tests | g3 | t3 | x | x + regression | create_property_graph_tests | g3 | t3 | y | y + regression | create_property_graph_tests | g3 | t3 | z | z + regression | create_property_graph_tests | g4 | e1 | a | a + regression | create_property_graph_tests | g4 | e1 | i | i + regression | create_property_graph_tests | g4 | e1 | t | t + regression | create_property_graph_tests | g4 | e2 | a | a + regression | create_property_graph_tests | g4 | e2 | t | t + regression | create_property_graph_tests | g4 | e2 | x | x + regression | create_property_graph_tests | g4 | t2 | i_j | (i + j) + regression | create_property_graph_tests | g4 | t2 | kk | (k * 2) + regression | create_property_graph_tests | g4 | t3 | x | x + regression | create_property_graph_tests | g4 | t3 | yy | y + regression | create_property_graph_tests | g4 | t3 | zz | z + regression | create_property_graph_tests | g5 | t11 | a | a + regression | create_property_graph_tests | g5 | t12 | b | b + regression | create_property_graph_tests | g5 | t13 | c | c + regression | create_property_graph_tests | g5 | t13 | d | d + regression | create_property_graph_tests | g5 | t13 | e | e + regression | create_property_graph_tests | gc1 | ec1 | eb | eb + regression | create_property_graph_tests | gc1 | ec1 | ek1 | ek1 + regression | create_property_graph_tests | gc1 | ec1 | ek2 | ek2 + regression | create_property_graph_tests | gc1 | ec2 | eb | (eb COLLATE "default") + regression | create_property_graph_tests | gc1 | ec2 | ek1 | ek1 + regression | create_property_graph_tests | gc1 | ec2 | ek2 | ek2 + regression | create_property_graph_tests | gc1 | tc1 | a | a + regression | create_property_graph_tests | gc1 | tc1 | b | ((b)::character varying COLLATE "C") + regression | create_property_graph_tests | gc1 | tc2 | a | a + regression | create_property_graph_tests | gc1 | tc2 | b | ((b)::character varying COLLATE "C") + regression | create_property_graph_tests | gc1 | tc3 | a | a + regression | create_property_graph_tests | gc1 | tc3 | b | (b)::character varying + regression | create_property_graph_tests | gt | e | c | c + regression | create_property_graph_tests | gt | e | k1 | k1 + regression | create_property_graph_tests | gt | e | k2 | k2 + regression | create_property_graph_tests | gt | v1 | a | a + regression | create_property_graph_tests | gt | v1 | b | b + regression | create_property_graph_tests | gt | v2 | m | m + regression | create_property_graph_tests | gt | v2 | n | n +(54 rows) + +SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_name, label_name, property_name; + property_graph_catalog | property_graph_schema | property_graph_name | label_name | property_name +------------------------+-----------------------------+---------------------+------------+--------------- + regression | create_property_graph_tests | g2 | e1 | a + regression | create_property_graph_tests | g2 | e1 | i + regression | create_property_graph_tests | g2 | e1 | t + regression | create_property_graph_tests | g2 | e2 | a + regression | create_property_graph_tests | g2 | e2 | t + regression | create_property_graph_tests | g2 | e2 | x + regression | create_property_graph_tests | g2 | t1 | a + regression | create_property_graph_tests | g2 | t1 | b + regression | create_property_graph_tests | g2 | t2 | i + regression | create_property_graph_tests | g2 | t2 | j + regression | create_property_graph_tests | g2 | t2 | k + regression | create_property_graph_tests | g2 | t3l1 | x + regression | create_property_graph_tests | g2 | t3l1 | y + regression | create_property_graph_tests | g2 | t3l1 | z + regression | create_property_graph_tests | g2 | t3l2 | x + regression | create_property_graph_tests | g2 | t3l2 | y + regression | create_property_graph_tests | g2 | t3l2 | z + regression | create_property_graph_tests | g3 | t1 | a + regression | create_property_graph_tests | g3 | t1 | b + regression | create_property_graph_tests | g3 | t3l1 | x + regression | create_property_graph_tests | g3 | t3l1 | y + regression | create_property_graph_tests | g3 | t3l1 | z + regression | create_property_graph_tests | g3 | t3l2 | x + regression | create_property_graph_tests | g3 | t3l2 | y + regression | create_property_graph_tests | g3 | t3l2 | z + regression | create_property_graph_tests | g4 | e1 | a + regression | create_property_graph_tests | g4 | e1 | i + regression | create_property_graph_tests | g4 | e1 | t + regression | create_property_graph_tests | g4 | e2 | a + regression | create_property_graph_tests | g4 | e2 | t + regression | create_property_graph_tests | g4 | e2 | x + regression | create_property_graph_tests | g4 | t2 | i_j + regression | create_property_graph_tests | g4 | t2 | kk + regression | create_property_graph_tests | g4 | t3l1 | x + regression | create_property_graph_tests | g4 | t3l1 | yy + regression | create_property_graph_tests | g4 | t3l2 | x + regression | create_property_graph_tests | g4 | t3l2 | zz + regression | create_property_graph_tests | g5 | t11 | a + regression | create_property_graph_tests | g5 | t12 | b + regression | create_property_graph_tests | g5 | t13 | c + regression | create_property_graph_tests | g5 | t13 | d + regression | create_property_graph_tests | g5 | t13 | e + regression | create_property_graph_tests | gc1 | ec1 | eb + regression | create_property_graph_tests | gc1 | ec1 | ek1 + regression | create_property_graph_tests | gc1 | ec1 | ek2 + regression | create_property_graph_tests | gc1 | ec2 | eb + regression | create_property_graph_tests | gc1 | ec2 | ek1 + regression | create_property_graph_tests | gc1 | ec2 | ek2 + regression | create_property_graph_tests | gc1 | tc1 | a + regression | create_property_graph_tests | gc1 | tc1 | b + regression | create_property_graph_tests | gc1 | tc2 | a + regression | create_property_graph_tests | gc1 | tc2 | b + regression | create_property_graph_tests | gc1 | tc3 | a + regression | create_property_graph_tests | gc1 | tc3 | b + regression | create_property_graph_tests | gt | e | c + regression | create_property_graph_tests | gt | e | k1 + regression | create_property_graph_tests | gt | e | k2 + regression | create_property_graph_tests | gt | v1 | a + regression | create_property_graph_tests | gt | v1 | b + regression | create_property_graph_tests | gt | v2 | m + regression | create_property_graph_tests | gt | v2 | n +(61 rows) + +SELECT * FROM information_schema.pg_labels ORDER BY property_graph_name, label_name; + property_graph_catalog | property_graph_schema | property_graph_name | label_name +------------------------+-----------------------------+---------------------+------------ + regression | create_property_graph_tests | g2 | e1 + regression | create_property_graph_tests | g2 | e2 + regression | create_property_graph_tests | g2 | t1 + regression | create_property_graph_tests | g2 | t2 + regression | create_property_graph_tests | g2 | t3l1 + regression | create_property_graph_tests | g2 | t3l2 + regression | create_property_graph_tests | g3 | t1 + regression | create_property_graph_tests | g3 | t3l1 + regression | create_property_graph_tests | g3 | t3l2 + regression | create_property_graph_tests | g4 | e1 + regression | create_property_graph_tests | g4 | e2 + regression | create_property_graph_tests | g4 | t1 + regression | create_property_graph_tests | g4 | t2 + regression | create_property_graph_tests | g4 | t3l1 + regression | create_property_graph_tests | g4 | t3l2 + regression | create_property_graph_tests | g5 | t11 + regression | create_property_graph_tests | g5 | t12 + regression | create_property_graph_tests | g5 | t13 + regression | create_property_graph_tests | gc1 | ec1 + regression | create_property_graph_tests | gc1 | ec2 + regression | create_property_graph_tests | gc1 | tc1 + regression | create_property_graph_tests | gc1 | tc2 + regression | create_property_graph_tests | gc1 | tc3 + regression | create_property_graph_tests | gt | e + regression | create_property_graph_tests | gt | v1 + regression | create_property_graph_tests | gt | v2 +(26 rows) + +SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_name, property_name; + property_graph_catalog | property_graph_schema | property_graph_name | property_name | data_type | character_maximum_length | character_octet_length | character_set_catalog | character_set_schema | character_set_name | collation_catalog | collation_schema | collation_name | numeric_precision | numeric_precision_radix | numeric_scale | datetime_precision | interval_type | interval_precision | user_defined_type_catalog | user_defined_type_schema | user_defined_type_name | scope_catalog | scope_schema | scope_name | maximum_cardinality | dtd_identifier +------------------------+-----------------------------+---------------------+---------------+-------------------+--------------------------+------------------------+-----------------------+----------------------+--------------------+-------------------+------------------+----------------+-------------------+-------------------------+---------------+--------------------+---------------+--------------------+---------------------------+--------------------------+------------------------+---------------+--------------+------------+---------------------+---------------- + regression | create_property_graph_tests | g2 | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a + regression | create_property_graph_tests | g2 | b | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | b + regression | create_property_graph_tests | g2 | i | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | i + regression | create_property_graph_tests | g2 | j | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | j + regression | create_property_graph_tests | g2 | k | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | k + regression | create_property_graph_tests | g2 | t | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | t + regression | create_property_graph_tests | g2 | x | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | x + regression | create_property_graph_tests | g2 | y | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | y + regression | create_property_graph_tests | g2 | z | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | z + regression | create_property_graph_tests | g3 | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a + regression | create_property_graph_tests | g3 | b | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | b + regression | create_property_graph_tests | g3 | x | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | x + regression | create_property_graph_tests | g3 | y | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | y + regression | create_property_graph_tests | g3 | z | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | z + regression | create_property_graph_tests | g4 | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a + regression | create_property_graph_tests | g4 | i | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | i + regression | create_property_graph_tests | g4 | i_j | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | i_j + regression | create_property_graph_tests | g4 | kk | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | kk + regression | create_property_graph_tests | g4 | t | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | t + regression | create_property_graph_tests | g4 | x | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | x + regression | create_property_graph_tests | g4 | yy | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | yy + regression | create_property_graph_tests | g4 | zz | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | zz + regression | create_property_graph_tests | g5 | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a + regression | create_property_graph_tests | g5 | b | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | b + regression | create_property_graph_tests | g5 | c | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | c + regression | create_property_graph_tests | g5 | d | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | d + regression | create_property_graph_tests | g5 | e | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | e + regression | create_property_graph_tests | gc1 | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a + regression | create_property_graph_tests | gc1 | b | character varying | | | | | | regression | pg_catalog | C | | | | | | | regression | pg_catalog | varchar | | | | | b + regression | create_property_graph_tests | gc1 | eb | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | eb + regression | create_property_graph_tests | gc1 | ek1 | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | ek1 + regression | create_property_graph_tests | gc1 | ek2 | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | ek2 + regression | create_property_graph_tests | gt | a | integer | | | | | | regression | | | | | | | | | regression | pg_catalog | int4 | | | | | a + regression | create_property_graph_tests | gt | b | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | b + regression | create_property_graph_tests | gt | c | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | c + regression | create_property_graph_tests | gt | k1 | bigint | | | | | | regression | | | | | | | | | regression | pg_catalog | int8 | | | | | k1 + regression | create_property_graph_tests | gt | k2 | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | k2 + regression | create_property_graph_tests | gt | m | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | m + regression | create_property_graph_tests | gt | n | text | | | | | | regression | | | | | | | | | regression | pg_catalog | text | | | | | n +(39 rows) + +SELECT * FROM information_schema.pg_property_graph_privileges WHERE grantee LIKE 'regress%' ORDER BY property_graph_name, grantor, grantee, privilege_type; + grantor | grantee | property_graph_catalog | property_graph_schema | property_graph_name | privilege_type | is_grantable +---------------------+---------------------+------------------------+-----------------------------+---------------------+----------------+-------------- + regress_graph_user1 | regress_graph_user1 | regression | create_property_graph_tests | g1 | SELECT | YES + regress_graph_user1 | regress_graph_user2 | regression | create_property_graph_tests | g1 | SELECT | NO +(2 rows) + +-- test object address functions +SELECT pg_describe_object(classid, objid, objsubid) as obj, + pg_describe_object(refclassid, refobjid, refobjsubid) as reference_graph + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.g2'::regclass + ORDER BY 1, 2; + obj | reference_graph +---------------------------------+------------------- + edge e1 of property graph g2 | property graph g2 + edge e2 of property graph g2 | property graph g2 + label e1 of property graph g2 | property graph g2 + label e2 of property graph g2 | property graph g2 + label t1 of property graph g2 | property graph g2 + label t2 of property graph g2 | property graph g2 + label t3l1 of property graph g2 | property graph g2 + label t3l2 of property graph g2 | property graph g2 + property a of property graph g2 | property graph g2 + property b of property graph g2 | property graph g2 + property i of property graph g2 | property graph g2 + property j of property graph g2 | property graph g2 + property k of property graph g2 | property graph g2 + property t of property graph g2 | property graph g2 + property x of property graph g2 | property graph g2 + property y of property graph g2 | property graph g2 + property z of property graph g2 | property graph g2 + type g2 | property graph g2 + vertex t1 of property graph g2 | property graph g2 + vertex t2 of property graph g2 | property graph g2 + vertex t3 of property graph g2 | property graph g2 +(21 rows) + +SELECT (pg_identify_object_as_address(classid, objid, objsubid)).* + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.g2'::regclass + ORDER BY 1, 2, 3; + type | object_names | object_args +-------------------------+---------------------------------------+------------- + property graph element | {create_property_graph_tests,g2,e1} | {} + property graph element | {create_property_graph_tests,g2,e2} | {} + property graph element | {create_property_graph_tests,g2,t1} | {} + property graph element | {create_property_graph_tests,g2,t2} | {} + property graph element | {create_property_graph_tests,g2,t3} | {} + property graph label | {create_property_graph_tests,g2,e1} | {} + property graph label | {create_property_graph_tests,g2,e2} | {} + property graph label | {create_property_graph_tests,g2,t1} | {} + property graph label | {create_property_graph_tests,g2,t2} | {} + property graph label | {create_property_graph_tests,g2,t3l1} | {} + property graph label | {create_property_graph_tests,g2,t3l2} | {} + property graph property | {create_property_graph_tests,g2,a} | {} + property graph property | {create_property_graph_tests,g2,b} | {} + property graph property | {create_property_graph_tests,g2,i} | {} + property graph property | {create_property_graph_tests,g2,j} | {} + property graph property | {create_property_graph_tests,g2,k} | {} + property graph property | {create_property_graph_tests,g2,t} | {} + property graph property | {create_property_graph_tests,g2,x} | {} + property graph property | {create_property_graph_tests,g2,y} | {} + property graph property | {create_property_graph_tests,g2,z} | {} + type | {create_property_graph_tests.g2} | {} +(21 rows) + +SELECT (pg_identify_object(classid, objid, objsubid)).* + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.g2'::regclass + ORDER BY 1, 2, 3, 4; + type | schema | name | identity +-------------------------+-----------------------------+------+---------------------------------------- + property graph element | | | e1 of create_property_graph_tests.g2 + property graph element | | | e2 of create_property_graph_tests.g2 + property graph element | | | t1 of create_property_graph_tests.g2 + property graph element | | | t2 of create_property_graph_tests.g2 + property graph element | | | t3 of create_property_graph_tests.g2 + property graph label | | | e1 of create_property_graph_tests.g2 + property graph label | | | e2 of create_property_graph_tests.g2 + property graph label | | | t1 of create_property_graph_tests.g2 + property graph label | | | t2 of create_property_graph_tests.g2 + property graph label | | | t3l1 of create_property_graph_tests.g2 + property graph label | | | t3l2 of create_property_graph_tests.g2 + property graph property | | | a of create_property_graph_tests.g2 + property graph property | | | b of create_property_graph_tests.g2 + property graph property | | | i of create_property_graph_tests.g2 + property graph property | | | j of create_property_graph_tests.g2 + property graph property | | | k of create_property_graph_tests.g2 + property graph property | | | t of create_property_graph_tests.g2 + property graph property | | | x of create_property_graph_tests.g2 + property graph property | | | y of create_property_graph_tests.g2 + property graph property | | | z of create_property_graph_tests.g2 + type | create_property_graph_tests | g2 | create_property_graph_tests.g2 +(21 rows) + +\a\t +SELECT pg_get_propgraphdef('g2'::regclass); +CREATE PROPERTY GRAPH create_property_graph_tests.g2 + VERTEX TABLES ( + t1 KEY (a) PROPERTIES (a, b), + t2 KEY (i) PROPERTIES (i, j, k), + t3 KEY (x) LABEL t3l1 PROPERTIES (x, y, z) LABEL t3l2 PROPERTIES (x, y, z) + ) + EDGE TABLES ( + e1 KEY (a, i) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i) PROPERTIES (a, i, t), + e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) PROPERTIES (a, t, x) + ) +SELECT pg_get_propgraphdef('g3'::regclass); +CREATE PROPERTY GRAPH create_property_graph_tests.g3 + VERTEX TABLES ( + t1 KEY (a) PROPERTIES (a, b), + t3 KEY (x) LABEL t3l1 PROPERTIES (x, y, z) LABEL t3l2 PROPERTIES (x, y, z) + ) +SELECT pg_get_propgraphdef('g4'::regclass); +CREATE PROPERTY GRAPH create_property_graph_tests.g4 + VERTEX TABLES ( + t1 KEY (a) NO PROPERTIES, + t2 KEY (i) PROPERTIES ((i + j) AS i_j, (k * 2) AS kk), + t3 KEY (x) LABEL t3l1 PROPERTIES (x, y AS yy) LABEL t3l2 PROPERTIES (x, z AS zz) + ) + EDGE TABLES ( + e1 KEY (a, i) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i) PROPERTIES (a, i, t), + e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) PROPERTIES (a, t, x) + ) +SELECT pg_get_propgraphdef('pg_type'::regclass); -- error +ERROR: "pg_type" is not a property graph +\a\t +-- Test \d variants for property graphs +\dG g1 + List of property graphs + Schema | Name | Type | Owner +-----------------------------+------+----------------+--------------------- + create_property_graph_tests | g1 | property graph | regress_graph_user1 +(1 row) + +\dG+ g1 + List of property graphs + Schema | Name | Type | Owner | Persistence | Size | Description +-----------------------------+------+----------------+---------------------+-------------+---------+------------- + create_property_graph_tests | g1 | property graph | regress_graph_user1 | permanent | 0 bytes | a graph +(1 row) + +\dGx g1 +List of property graphs +-[ RECORD 1 ]----------------------- +Schema | create_property_graph_tests +Name | g1 +Type | property graph +Owner | regress_graph_user1 + +\d g2 + Property Graph "create_property_graph_tests.g2" + Element Alias | Element Table | Element Kind | Source Vertex Alias | Destination Vertex Alias +---------------+--------------------------------+--------------+---------------------+-------------------------- + e1 | create_property_graph_tests.e1 | edge | t1 | t2 + e2 | create_property_graph_tests.e2 | edge | t1 | t3 + t1 | create_property_graph_tests.t1 | vertex | | + t2 | create_property_graph_tests.t2 | vertex | | + t3 | create_property_graph_tests.t3 | vertex | | + +\d g1 + Property Graph "create_property_graph_tests.g1" + Element Alias | Element Table | Element Kind | Source Vertex Alias | Destination Vertex Alias +---------------+---------------+--------------+---------------------+-------------------------- + +\d+ g2 + Property Graph "create_property_graph_tests.g2" + Element Alias | Element Table | Element Kind | Source Vertex Alias | Destination Vertex Alias +---------------+--------------------------------+--------------+---------------------+-------------------------- + e1 | create_property_graph_tests.e1 | edge | t1 | t2 + e2 | create_property_graph_tests.e2 | edge | t1 | t3 + t1 | create_property_graph_tests.t1 | vertex | | + t2 | create_property_graph_tests.t2 | vertex | | + t3 | create_property_graph_tests.t3 | vertex | | +Property graph definition: +CREATE PROPERTY GRAPH create_property_graph_tests.g2 + VERTEX TABLES ( + t1 KEY (a) PROPERTIES (a, b), + t2 KEY (i) PROPERTIES (i, j, k), + t3 KEY (x) LABEL t3l1 PROPERTIES (x, y, z) LABEL t3l2 PROPERTIES (x, y, z) + ) + EDGE TABLES ( + e1 KEY (a, i) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i) PROPERTIES (a, i, t), + e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) PROPERTIES (a, t, x) + ) + +\d+ g1 + Property Graph "create_property_graph_tests.g1" + Element Alias | Element Table | Element Kind | Source Vertex Alias | Destination Vertex Alias +---------------+---------------+--------------+---------------------+-------------------------- +Property graph definition: +CREATE PROPERTY GRAPH create_property_graph_tests.g1 + +\dG g_nonexistent + List of property graphs + Schema | Name | Type | Owner +--------+------+------+------- +(0 rows) + +\dG t11 + List of property graphs + Schema | Name | Type | Owner +--------+------+------+------- +(0 rows) + +\set QUIET 'off' +\dG g_nonexistent +Did not find any property graphs named "g_nonexistent". +\set QUIET 'on' +-- temporary property graph +-- Keep this at the end to avoid test failure due to changing temporary +-- namespace names in information schema query outputs +CREATE TEMPORARY PROPERTY GRAPH g1; -- same name as persistent graph +DROP PROPERTY GRAPH g1; -- drops temporary graph retaining persistent graph +\dG g1 + List of property graphs + Schema | Name | Type | Owner +-----------------------------+------+----------------+--------------------- + create_property_graph_tests | g1 | property graph | regress_graph_user1 +(1 row) + +CREATE TEMPORARY TABLE v2tmp (m text, n text); +CREATE TEMPORARY PROPERTY GRAPH gtmp + VERTEX TABLES (v1 KEY (a), v2tmp KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2tmp(m) + ); +DROP PROPERTY GRAPH gtmp; +CREATE PROPERTY GRAPH gtmp + VERTEX TABLES (v1 KEY (a), v2tmp KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2tmp(m) + ); +NOTICE: property graph "gtmp" will be temporary +ALTER PROPERTY GRAPH g1 + ADD VERTEX TABLES (v2tmp KEY (m)); -- error +ERROR: cannot add temporary element table to non-temporary property graph +LINE 2: ADD VERTEX TABLES (v2tmp KEY (m)); + ^ +DETAIL: Table "v2tmp" is a temporary table. +-- DROP, ALTER SET SCHEMA, ALTER PROPERTY GRAPH RENAME TO +DROP TABLE g2; -- error: wrong object type +ERROR: "g2" is not a table +HINT: Use DROP PROPERTY GRAPH to remove a property graph. +CREATE VIEW vg1 AS SELECT * FROM GRAPH_TABLE(g1 MATCH () COLUMNS (1 AS one)); +DROP PROPERTY GRAPH g1; -- error +ERROR: cannot drop property graph g1 because other objects depend on it +DETAIL: view vg1 depends on property graph g1 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER PROPERTY GRAPH g1 SET SCHEMA create_property_graph_tests_2; +ALTER PROPERTY GRAPH create_property_graph_tests_2.g1 RENAME TO g2; +DROP PROPERTY GRAPH create_property_graph_tests_2.g2 CASCADE; +NOTICE: drop cascades to view vg1 +DROP PROPERTY GRAPH g1; -- error +ERROR: property graph "g1" does not exist +ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (t1 KEY (a)); -- error +ERROR: relation "g1" does not exist +ALTER PROPERTY GRAPH IF EXISTS g1 SET SCHEMA create_property_graph_tests_2; +NOTICE: relation "g1" does not exist, skipping +DROP PROPERTY GRAPH IF EXISTS g1; +NOTICE: property graph "g1" does not exist, skipping +DROP ROLE regress_graph_user1, regress_graph_user2; +-- leave remaining objects behind for pg_upgrade/pg_dump tests diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out index 46d4f9efe997e..5d0f21820edde 100644 --- a/src/test/regress/expected/create_role.out +++ b/src/test/regress/expected/create_role.out @@ -102,6 +102,33 @@ CREATE ROLE regress_rolecreator CREATEROLE; CREATE ROLE regress_hasprivs CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5; -- ok, we should be able to modify a role we created COMMENT ON ROLE regress_hasprivs IS 'some comment'; +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NOT NULL AS has_comment; + has_comment +------------- + t +(1 row) + +COMMENT ON ROLE regress_hasprivs IS NULL; +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NULL AS no_comment; + no_comment +------------ + t +(1 row) + +COMMENT ON ROLE regress_hasprivs IS 'add the comment back'; +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NOT NULL AS has_comment; + has_comment +------------- + t +(1 row) + +COMMENT ON ROLE regress_hasprivs IS ''; -- empty string removes the comment, same as NULL +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NULL AS no_comment; + no_comment +------------ + t +(1 row) + ALTER ROLE regress_hasprivs RENAME TO regress_tenant; ALTER ROLE regress_tenant NOINHERIT NOLOGIN CONNECTION LIMIT 7; -- fail, we should be unable to modify a role we did not create diff --git a/src/test/regress/expected/create_schema.out b/src/test/regress/expected/create_schema.out index 93302a07efc36..bfe211338abe8 100644 --- a/src/test/regress/expected/create_schema.out +++ b/src/test/regress/expected/create_schema.out @@ -5,59 +5,100 @@ CREATE ROLE regress_create_schema_role SUPERUSER; -- Cases where schema creation fails as objects are qualified with a schema -- that does not match with what's expected. --- This checks all the object types that include schema qualifications. +-- This checks most object types that include schema qualifications. CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE SEQUENCE schema_not_existing.seq; ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE SEQUENCE schema_not_existing.seq; + ^ CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE TABLE schema_not_existing.tab (id int); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE TABLE schema_not_existing.tab (id int); + ^ CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE VIEW schema_not_existing.view AS SELECT 1; ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE VIEW schema_not_existing.view AS SELECT 1; + ^ CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE INDEX ON schema_not_existing.tab (id); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE INDEX ON schema_not_existing.tab (id); + ^ CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_existing.tab EXECUTE FUNCTION schema_trig.no_func(); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_exi... + ^ +CREATE SCHEMA AUTHORIZATION regress_create_schema_role + CREATE FUNCTION schema_not_existing.func(int) RETURNS int + AS 'SELECT $1' LANGUAGE sql; +ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) -- Again, with a role specification and no schema names. SET ROLE regress_create_schema_role; CREATE SCHEMA AUTHORIZATION CURRENT_ROLE CREATE SEQUENCE schema_not_existing.seq; ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE SEQUENCE schema_not_existing.seq; + ^ CREATE SCHEMA AUTHORIZATION CURRENT_ROLE CREATE TABLE schema_not_existing.tab (id int); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE TABLE schema_not_existing.tab (id int); + ^ CREATE SCHEMA AUTHORIZATION CURRENT_ROLE CREATE VIEW schema_not_existing.view AS SELECT 1; ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE VIEW schema_not_existing.view AS SELECT 1; + ^ CREATE SCHEMA AUTHORIZATION CURRENT_ROLE CREATE INDEX ON schema_not_existing.tab (id); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE INDEX ON schema_not_existing.tab (id); + ^ CREATE SCHEMA AUTHORIZATION CURRENT_ROLE CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_existing.tab EXECUTE FUNCTION schema_trig.no_func(); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_create_schema_role) +LINE 2: CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_exi... + ^ -- Again, with a schema name and a role specification. CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE CREATE SEQUENCE schema_not_existing.seq; ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_schema_1) +LINE 2: CREATE SEQUENCE schema_not_existing.seq; + ^ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE CREATE TABLE schema_not_existing.tab (id int); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_schema_1) +LINE 2: CREATE TABLE schema_not_existing.tab (id int); + ^ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE CREATE VIEW schema_not_existing.view AS SELECT 1; ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_schema_1) +LINE 2: CREATE VIEW schema_not_existing.view AS SELECT 1; + ^ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE CREATE INDEX ON schema_not_existing.tab (id); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_schema_1) +LINE 2: CREATE INDEX ON schema_not_existing.tab (id); + ^ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_existing.tab EXECUTE FUNCTION schema_trig.no_func(); ERROR: CREATE specifies a schema (schema_not_existing) different from the one being created (regress_schema_1) +LINE 2: CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_exi... + ^ RESET ROLE; +-- Forward references no longer work in general. +CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE + CREATE VIEW abcd_view AS SELECT a FROM abcd + CREATE TABLE abcd (a int); +ERROR: relation "abcd" does not exist +LINE 2: CREATE VIEW abcd_view AS SELECT a FROM abcd + ^ -- Cases where the schema creation succeeds. -- The schema created matches the role name. CREATE SCHEMA AUTHORIZATION regress_create_schema_role @@ -94,5 +135,187 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE DROP SCHEMA regress_schema_1 CASCADE; NOTICE: drop cascades to table regress_schema_1.tab RESET ROLE; +-- Test forward-referencing foreign key clauses. +CREATE SCHEMA regress_schema_fk + CREATE TABLE regress_schema_fk.t2 ( + b int, + a int REFERENCES t1 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t3 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t1 NOT DEFERRABLE) + CREATE TABLE regress_schema_fk.t1 (a int PRIMARY KEY) + CREATE TABLE t3 (a int PRIMARY KEY) + CREATE TABLE t4 ( + b int, + a int REFERENCES t5 NOT DEFERRABLE ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE INITIALLY DEFERRED) + CREATE TABLE t5 (a int, b int, PRIMARY KEY (a)) + CREATE TABLE t6 (a int, b int, PRIMARY KEY (a)); +\d regress_schema_fk.t2 + Table "regress_schema_fk.t2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Foreign-key constraints: + "fk" FOREIGN KEY (a) REFERENCES regress_schema_fk.t1(a) + "t2_a_fkey" FOREIGN KEY (a) REFERENCES regress_schema_fk.t1(a) DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + "t2_a_fkey1" FOREIGN KEY (a) REFERENCES regress_schema_fk.t3(a) DEFERRABLE INITIALLY DEFERRED + +\d regress_schema_fk.t4 + Table "regress_schema_fk.t4" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + b | integer | | | + a | integer | | | +Foreign-key constraints: + "fk" FOREIGN KEY (a) REFERENCES regress_schema_fk.t6(a) DEFERRABLE INITIALLY DEFERRED + "t4_a_fkey" FOREIGN KEY (a) REFERENCES regress_schema_fk.t5(a) + "t4_a_fkey1" FOREIGN KEY (a) REFERENCES regress_schema_fk.t6(a) DEFERRABLE + +DROP SCHEMA regress_schema_fk CASCADE; +NOTICE: drop cascades to 6 other objects +DETAIL: drop cascades to table regress_schema_fk.t2 +drop cascades to table regress_schema_fk.t1 +drop cascades to table regress_schema_fk.t3 +drop cascades to table regress_schema_fk.t4 +drop cascades to table regress_schema_fk.t5 +drop cascades to table regress_schema_fk.t6 +-- Test miscellaneous object types within CREATE SCHEMA. +CREATE SCHEMA regress_schema_misc + CREATE AGGREGATE cs_sum(int4) + ( + SFUNC = int4_sum(int8, int4), + STYPE = int8, + INITCOND = '0' + ) + CREATE COLLATION cs_builtin_c ( PROVIDER = builtin, LOCALE = "C" ) + CREATE DOMAIN cs_positive AS integer CHECK (VALUE > 0) + CREATE FUNCTION cs_add(int4, int4) returns int4 language sql + as 'select $1 + $2' + CREATE OPERATOR + (function = cs_add, leftarg = int4, rightarg = int4) + CREATE PROCEDURE cs_proc(int4, int4) + BEGIN ATOMIC SELECT cs_add($1,$2); END + CREATE TEXT SEARCH CONFIGURATION cs_ts_conf (copy=english) + CREATE TEXT SEARCH DICTIONARY cs_ts_dict (template=simple) + CREATE TEXT SEARCH PARSER cs_ts_prs + (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, + lextypes = prsd_lextype) + CREATE TEXT SEARCH TEMPLATE cs_ts_temp (lexize=dsimple_lexize) + CREATE TYPE regress_schema_misc.cs_enum AS ENUM ('red', 'orange') + CREATE TYPE cs_composite AS (a int, b float8) + CREATE TYPE cs_range AS RANGE (subtype = float8, subtype_diff = float8mi) + -- demonstrate creation of a base type with its I/O functions + CREATE TYPE cs_type + CREATE FUNCTION cs_type_in(cstring) + RETURNS cs_type LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT + AS 'int4in' + CREATE FUNCTION cs_type_out(cs_type) + RETURNS cstring LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT + AS 'int4out' + CREATE TYPE cs_type ( + INPUT = cs_type_in, + OUTPUT = cs_type_out, + LIKE = int4 + ) + GRANT USAGE ON TYPE cs_type TO public +; +NOTICE: return type cs_type is only a shell +NOTICE: argument type cs_type is only a shell +LINE 29: CREATE FUNCTION cs_type_out(cs_type) + ^ +\df regress_schema_misc.cs_add + List of functions + Schema | Name | Result data type | Argument data types | Type +---------------------+--------+------------------+---------------------+------ + regress_schema_misc | cs_add | integer | integer, integer | func +(1 row) + +\df regress_schema_misc.cs_proc + List of functions + Schema | Name | Result data type | Argument data types | Type +---------------------+---------+------------------+------------------------+------ + regress_schema_misc | cs_proc | | IN integer, IN integer | proc +(1 row) + +\da regress_schema_misc.cs_sum + List of aggregate functions + Schema | Name | Result data type | Argument data types | Description +---------------------+--------+------------------+---------------------+------------- + regress_schema_misc | cs_sum | bigint | integer | +(1 row) + +\do regress_schema_misc.+ + List of operators + Schema | Name | Left arg type | Right arg type | Result type | Description +---------------------+------+---------------+----------------+-------------+------------- + regress_schema_misc | + | integer | integer | integer | +(1 row) + +\dO regress_schema_misc.* + List of collations + Schema | Name | Provider | Collate | Ctype | Locale | ICU Rules | Deterministic? +---------------------+--------------+----------+---------+-------+--------+-----------+---------------- + regress_schema_misc | cs_builtin_c | builtin | | | C | | yes +(1 row) + +\dT regress_schema_misc.* + List of data types + Schema | Name | Description +---------------------+-----------------------------------+------------- + regress_schema_misc | regress_schema_misc.cs_composite | + regress_schema_misc | regress_schema_misc.cs_enum | + regress_schema_misc | regress_schema_misc.cs_multirange | + regress_schema_misc | regress_schema_misc.cs_positive | + regress_schema_misc | regress_schema_misc.cs_range | + regress_schema_misc | regress_schema_misc.cs_type | +(6 rows) + +\dF regress_schema_misc.* + List of text search configurations + Schema | Name | Description +---------------------+------------+------------- + regress_schema_misc | cs_ts_conf | +(1 row) + +\dFd regress_schema_misc.* + List of text search dictionaries + Schema | Name | Description +---------------------+------------+------------- + regress_schema_misc | cs_ts_dict | +(1 row) + +\dFp regress_schema_misc.* + List of text search parsers + Schema | Name | Description +---------------------+-----------+------------- + regress_schema_misc | cs_ts_prs | +(1 row) + +\dFt regress_schema_misc.* + List of text search templates + Schema | Name | Description +---------------------+------------+------------- + regress_schema_misc | cs_ts_temp | +(1 row) + +DROP SCHEMA regress_schema_misc CASCADE; +NOTICE: drop cascades to 16 other objects +DETAIL: drop cascades to function regress_schema_misc.cs_sum(integer) +drop cascades to collation regress_schema_misc.cs_builtin_c +drop cascades to type regress_schema_misc.cs_positive +drop cascades to function regress_schema_misc.cs_add(integer,integer) +drop cascades to operator regress_schema_misc.+(integer,integer) +drop cascades to function regress_schema_misc.cs_proc(integer,integer) +drop cascades to text search configuration regress_schema_misc.cs_ts_conf +drop cascades to text search dictionary regress_schema_misc.cs_ts_dict +drop cascades to text search parser regress_schema_misc.cs_ts_prs +drop cascades to text search template regress_schema_misc.cs_ts_temp +drop cascades to type regress_schema_misc.cs_enum +drop cascades to type regress_schema_misc.cs_composite +drop cascades to type regress_schema_misc.cs_range +drop cascades to function regress_schema_misc.cs_type_out(regress_schema_misc.cs_type) +drop cascades to type regress_schema_misc.cs_type +drop cascades to function regress_schema_misc.cs_type_in(cstring) -- Clean up DROP ROLE regress_create_schema_role; diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out index 76604705a93cc..a7a24fa3adcc7 100644 --- a/src/test/regress/expected/create_table.out +++ b/src/test/regress/expected/create_table.out @@ -102,6 +102,18 @@ ERROR: tables declared WITH OIDS are not supported -- but explicitly not adding oids is still supported CREATE TEMP TABLE withoutoid() WITHOUT OIDS; DROP TABLE withoutoid; CREATE TEMP TABLE withoutoid() WITH (oids = false); DROP TABLE withoutoid; +-- temporary tables are ignored by pg_filenode_relation(). +CREATE TEMP TABLE relation_filenode_check(c1 int); +SELECT relpersistence, + pg_filenode_relation (reltablespace, pg_relation_filenode(oid)) + FROM pg_class + WHERE relname = 'relation_filenode_check'; + relpersistence | pg_filenode_relation +----------------+---------------------- + t | +(1 row) + +DROP TABLE relation_filenode_check; -- check restriction with default expressions -- invalid use of column reference in default expressions CREATE TABLE default_expr_column (id int DEFAULT (id)); @@ -149,6 +161,12 @@ ALTER TABLE remember_node_subid ALTER c TYPE bigint; SAVEPOINT q; DROP TABLE remember_node_subid; ROLLBACK TO q; COMMIT; DROP TABLE remember_node_subid; +-- generated NOT NULL constraint names must not collide with explicitly named constraints +CREATE TABLE two_not_null_constraints ( + col integer NOT NULL, + CONSTRAINT two_not_null_constraints_col_not_null CHECK (col IS NOT NULL) +); +DROP TABLE two_not_null_constraints; -- -- Partitioned tables -- @@ -413,10 +431,11 @@ CREATE TABLE part_null PARTITION OF list_parted FOR VALUES IN (null); --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | | | plain | | Partition key: LIST (a) -Partitions: part_null FOR VALUES IN (NULL), - part_p1 FOR VALUES IN (1), - part_p2 FOR VALUES IN (2), - part_p3 FOR VALUES IN (3) +Partitions: + part_null FOR VALUES IN (NULL) + part_p1 FOR VALUES IN (1) + part_p2 FOR VALUES IN (2) + part_p3 FOR VALUES IN (3) -- forbidden expressions for partition bound with list partitioned table CREATE TABLE part_bogus_expr_fail PARTITION OF list_parted FOR VALUES IN (somename); @@ -881,7 +900,8 @@ Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text)) Partition key: RANGE (b) Not-null constraints: "part_c_b_not_null" NOT NULL "b" (local, inherited) -Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10) +Partitions: + part_c_1_10 FOR VALUES FROM (1) TO (10) -- a level-2 partition's constraint will include the parent's expressions \d+ part_c_1_10 @@ -1026,8 +1046,9 @@ create table boolspart_f partition of boolspart for values in (false); --------+---------+-----------+----------+---------+---------+--------------+------------- a | boolean | | | | plain | | Partition key: LIST (a) -Partitions: boolspart_f FOR VALUES IN (false), - boolspart_t FOR VALUES IN (true) +Partitions: + boolspart_f FOR VALUES IN (false) + boolspart_t FOR VALUES IN (true) drop table boolspart; -- partitions mixing temporary and permanent relations diff --git a/src/test/regress/expected/create_table_like.out b/src/test/regress/expected/create_table_like.out index bf34289e9842b..5720d160f051e 100644 --- a/src/test/regress/expected/create_table_like.out +++ b/src/test/regress/expected/create_table_like.out @@ -261,8 +261,9 @@ CREATE TABLE test_like_5c (LIKE test_like_4 INCLUDING ALL) Check constraints: "test_like_4_a_check" CHECK (a > 0) "test_like_5x_p_check" CHECK (p > 0) -Inherits: test_like_5, - test_like_5x +Inherits: + test_like_5 + test_like_5x -- Test updating of column numbers in statistics expressions (bug #18468) CREATE TABLE test_like_6 (a int, c text, b text); @@ -320,6 +321,7 @@ DROP TABLE inhz; -- including storage and comments CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY, b text CHECK (length(b) > 100) NOT ENFORCED); +ALTER TABLE ctlt1 ADD CONSTRAINT cc CHECK (length(b) > 100) NOT VALID; CREATE INDEX ctlt1_b_key ON ctlt1 (b); CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b)); CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1; @@ -332,9 +334,10 @@ COMMENT ON CONSTRAINT ctlt1_a_check ON ctlt1 IS 't1_a_check'; COMMENT ON INDEX ctlt1_pkey IS 'index pkey'; COMMENT ON INDEX ctlt1_b_key IS 'index b_key'; ALTER TABLE ctlt1 ALTER COLUMN a SET STORAGE MAIN; -CREATE TABLE ctlt2 (c text); +CREATE TABLE ctlt2 (c text NOT NULL); ALTER TABLE ctlt2 ALTER COLUMN c SET STORAGE EXTERNAL; COMMENT ON COLUMN ctlt2.c IS 'C'; +COMMENT ON CONSTRAINT ctlt2_c_not_null ON ctlt2 IS 't2_c_not_null'; CREATE TABLE ctlt3 (a text CHECK (length(a) < 5), c text CHECK (length(c) < 7)); ALTER TABLE ctlt3 ALTER COLUMN c SET STORAGE EXTERNAL; ALTER TABLE ctlt3 ALTER COLUMN a SET STORAGE MAIN; @@ -351,9 +354,10 @@ CREATE TABLE ctlt12_storage (LIKE ctlt1 INCLUDING STORAGE, LIKE ctlt2 INCLUDING --------+------+-----------+----------+---------+----------+--------------+------------- a | text | | not null | | main | | b | text | | | | extended | | - c | text | | | | external | | + c | text | | not null | | external | | Not-null constraints: "ctlt1_a_not_null" NOT NULL "a" + "ctlt2_c_not_null" NOT NULL "c" CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDING COMMENTS); \d+ ctlt12_comments @@ -362,13 +366,21 @@ CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDIN --------+------+-----------+----------+---------+----------+--------------+------------- a | text | | not null | | extended | | A b | text | | | | extended | | B - c | text | | | | extended | | C + c | text | | not null | | extended | | C Not-null constraints: "ctlt1_a_not_null" NOT NULL "a" + "ctlt2_c_not_null" NOT NULL "c" + +SELECT conname, description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt12_comments'::regclass; + conname | description +------------------+--------------- + ctlt2_c_not_null | t2_c_not_null +(1 row) CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INHERITS (ctlt1); NOTICE: merging column "a" with inherited definition NOTICE: merging column "b" with inherited definition +NOTICE: merging constraint "cc" with inherited definition NOTICE: merging constraint "ctlt1_a_check" with inherited definition NOTICE: merging constraint "ctlt1_b_check" with inherited definition \d+ ctlt1_inh @@ -378,11 +390,13 @@ NOTICE: merging constraint "ctlt1_b_check" with inherited definition a | text | | not null | | main | | A b | text | | | | extended | | B Check constraints: + "cc" CHECK (length(b) > 100) "ctlt1_a_check" CHECK (length(a) > 2) "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED Not-null constraints: "ctlt1_a_not_null" NOT NULL "a" (local, inherited) -Inherits: ctlt1 +Inherits: + ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt1_inh'::regclass; description @@ -400,14 +414,16 @@ NOTICE: merging multiple inherited definitions of column "a" b | text | | | | extended | | c | text | | | | external | | Check constraints: + "cc" CHECK (length(b) > 100) "ctlt1_a_check" CHECK (length(a) > 2) "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) Not-null constraints: "ctlt1_a_not_null" NOT NULL "a" (inherited) -Inherits: ctlt1, - ctlt3 +Inherits: + ctlt1 + ctlt3 CREATE TABLE ctlt13_like (LIKE ctlt3 INCLUDING CONSTRAINTS INCLUDING INDEXES INCLUDING COMMENTS INCLUDING STORAGE) INHERITS (ctlt1); NOTICE: merging column "a" with inherited definition @@ -421,13 +437,15 @@ NOTICE: merging column "a" with inherited definition Indexes: "ctlt13_like_expr_idx" btree ((a || c)) Check constraints: + "cc" CHECK (length(b) > 100) "ctlt1_a_check" CHECK (length(a) > 2) "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED "ctlt3_a_check" CHECK (length(a) < 5) "ctlt3_c_check" CHECK (length(c) < 7) Not-null constraints: "ctlt1_a_not_null" NOT NULL "a" (inherited) -Inherits: ctlt1 +Inherits: + ctlt1 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt13_like'::regclass; description @@ -447,6 +465,7 @@ Indexes: "ctlt_all_b_idx" btree (b) "ctlt_all_expr_idx" btree ((a || b)) Check constraints: + "cc" CHECK (length(b) > 100) "ctlt1_a_check" CHECK (length(a) > 2) "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED Statistics objects: @@ -490,6 +509,7 @@ Indexes: "pg_attrdef_b_idx" btree (b) "pg_attrdef_expr_idx" btree ((a || b)) Check constraints: + "cc" CHECK (length(b) > 100) "ctlt1_a_check" CHECK (length(a) > 2) "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED Statistics objects: @@ -515,6 +535,7 @@ Indexes: "ctlt1_b_idx" btree (b) "ctlt1_expr_idx" btree ((a || b)) Check constraints: + "cc" CHECK (length(b) > 100) "ctlt1_a_check" CHECK (length(a) > 2) "ctlt1_b_check" CHECK (length(b) > 100) NOT ENFORCED Statistics objects: @@ -529,7 +550,9 @@ NOTICE: drop cascades to table inhe -- LIKE must respect NO INHERIT property of constraints CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null, c int not null no inherit); -CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS); +COMMENT ON CONSTRAINT noinh_con_copy_b_not_null ON noinh_con_copy IS 'not null b'; +COMMENT ON CONSTRAINT noinh_con_copy_c_not_null ON noinh_con_copy IS 'not null c no inherit'; +CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS INCLUDING COMMENTS); \d+ noinh_con_copy1 Table "public.noinh_con_copy1" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -543,6 +566,17 @@ Not-null constraints: "noinh_con_copy_b_not_null" NOT NULL "b" "noinh_con_copy_c_not_null" NOT NULL "c" NO INHERIT +SELECT conname, description +FROM pg_description, pg_constraint c +WHERE classoid = 'pg_constraint'::regclass +AND objoid = c.oid AND c.conrelid = 'noinh_con_copy1'::regclass +ORDER BY conname COLLATE "C"; + conname | description +---------------------------+----------------------- + noinh_con_copy_b_not_null | not null b + noinh_con_copy_c_not_null | not null c no inherit +(2 rows) + -- fail, as partitioned tables don't allow NO INHERIT constraints CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL) PARTITION BY LIST (a); diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out index f551624afb3a3..63cf4b4371d00 100644 --- a/src/test/regress/expected/create_view.out +++ b/src/test/regress/expected/create_view.out @@ -114,6 +114,7 @@ CREATE VIEW v1 AS SELECT * FROM base_table; -- should be created in temp object schema CREATE VIEW v1_temp AS SELECT * FROM temp_table; NOTICE: view "v1_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. -- should be created in temp object schema CREATE TEMP VIEW v2_temp AS SELECT * FROM base_table; -- should be created in temp_views schema @@ -121,11 +122,14 @@ CREATE VIEW temp_view_test.v2 AS SELECT * FROM base_table; -- should fail CREATE VIEW temp_view_test.v3_temp AS SELECT * FROM temp_table; NOTICE: view "v3_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. ERROR: cannot create temporary relation in non-temporary schema -- should fail CREATE SCHEMA test_view_schema CREATE TEMP VIEW testview AS SELECT 1; ERROR: cannot create temporary relation in non-temporary schema +LINE 2: CREATE TEMP VIEW testview AS SELECT 1; + ^ -- joins: if any of the join relations are temporary, the view -- should also be temporary -- should be non-temp @@ -139,12 +143,14 @@ CREATE VIEW v4_temp AS FROM base_table t1, temp_table t2 WHERE t1.id = t2.id; NOTICE: view "v4_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. -- should be temp CREATE VIEW v5_temp AS SELECT t1.a AS t1_a, t2.a AS t2_a, t3.a AS t3_a FROM base_table t1, base_table2 t2, temp_table t3 WHERE t1.id = t2.id and t2.id = t3.id; NOTICE: view "v5_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. -- subqueries CREATE VIEW v4 AS SELECT * FROM base_table WHERE id IN (SELECT id FROM base_table2); CREATE VIEW v5 AS SELECT t1.id, t2.a FROM base_table t1, (SELECT * FROM base_table2) t2; @@ -153,25 +159,33 @@ CREATE VIEW v7 AS SELECT * FROM base_table WHERE NOT EXISTS (SELECT 1 FROM base_ CREATE VIEW v8 AS SELECT * FROM base_table WHERE EXISTS (SELECT 1); CREATE VIEW v6_temp AS SELECT * FROM base_table WHERE id IN (SELECT id FROM temp_table); NOTICE: view "v6_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. CREATE VIEW v7_temp AS SELECT t1.id, t2.a FROM base_table t1, (SELECT * FROM temp_table) t2; NOTICE: view "v7_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. CREATE VIEW v8_temp AS SELECT * FROM base_table WHERE EXISTS (SELECT 1 FROM temp_table); NOTICE: view "v8_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. CREATE VIEW v9_temp AS SELECT * FROM base_table WHERE NOT EXISTS (SELECT 1 FROM temp_table); NOTICE: view "v9_temp" will be a temporary view +DETAIL: It depends on temporary table temp_table. -- a view should also be temporary if it references a temporary view CREATE VIEW v10_temp AS SELECT * FROM v7_temp; NOTICE: view "v10_temp" will be a temporary view +DETAIL: It depends on temporary view v7_temp. CREATE VIEW v11_temp AS SELECT t1.id, t2.a FROM base_table t1, v10_temp t2; NOTICE: view "v11_temp" will be a temporary view +DETAIL: It depends on temporary view v10_temp. CREATE VIEW v12_temp AS SELECT true FROM v11_temp; NOTICE: view "v12_temp" will be a temporary view +DETAIL: It depends on temporary view v11_temp. -- a view should also be temporary if it references a temporary sequence CREATE SEQUENCE seq1; CREATE TEMPORARY SEQUENCE seq1_temp; -CREATE VIEW v9 AS SELECT seq1.is_called FROM seq1; -CREATE VIEW v13_temp AS SELECT seq1_temp.is_called FROM seq1_temp; +CREATE VIEW v9 AS SELECT nextval('seq1'); +CREATE VIEW v13_temp AS SELECT nextval('seq1_temp'); NOTICE: view "v13_temp" will be a temporary view +DETAIL: It depends on temporary sequence seq1_temp. SELECT relname FROM pg_class WHERE relname LIKE 'v_' AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'temp_view_test') @@ -217,15 +231,19 @@ CREATE TEMP TABLE tt (num2 int, value text); CREATE VIEW nontemp1 AS SELECT * FROM t1 CROSS JOIN t2; CREATE VIEW temporal1 AS SELECT * FROM t1 CROSS JOIN tt; NOTICE: view "temporal1" will be a temporary view +DETAIL: It depends on temporary table tt. CREATE VIEW nontemp2 AS SELECT * FROM t1 INNER JOIN t2 ON t1.num = t2.num2; CREATE VIEW temporal2 AS SELECT * FROM t1 INNER JOIN tt ON t1.num = tt.num2; NOTICE: view "temporal2" will be a temporary view +DETAIL: It depends on temporary table tt. CREATE VIEW nontemp3 AS SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num2; CREATE VIEW temporal3 AS SELECT * FROM t1 LEFT JOIN tt ON t1.num = tt.num2; NOTICE: view "temporal3" will be a temporary view +DETAIL: It depends on temporary table tt. CREATE VIEW nontemp4 AS SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num2 AND t2.value = 'xxx'; CREATE VIEW temporal4 AS SELECT * FROM t1 LEFT JOIN tt ON t1.num = tt.num2 AND tt.value = 'xxx'; NOTICE: view "temporal4" will be a temporary view +DETAIL: It depends on temporary table tt. SELECT relname FROM pg_class WHERE relname LIKE 'nontemp%' AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'testviewschm2') @@ -272,6 +290,7 @@ BETWEEN (SELECT d FROM tbl2 WHERE c = 1) AND (SELECT e FROM tbl3 WHERE f = 2) AND EXISTS (SELECT g FROM tbl4 LEFT JOIN tbl3 ON tbl4.h = tbl3.f) AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j); NOTICE: view "mytempview" will be a temporary view +DETAIL: It depends on temporary table tmptbl. SELECT count(*) FROM pg_class where relname LIKE 'mytempview' And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%'); count @@ -1924,7 +1943,8 @@ select 'foo'::text = any((select array['abc','def','foo']::text[])); -- fail ERROR: operator does not exist: text = text[] LINE 1: select 'foo'::text = any((select array['abc','def','foo']::t... ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select 'foo'::text = any((select array['abc','def','foo']::text[])::text[]); ?column? ---------- diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out index 4cbdbdf84d0c5..6b879b0f62a75 100644 --- a/src/test/regress/expected/database.out +++ b/src/test/regress/expected/database.out @@ -2,7 +2,7 @@ CREATE DATABASE regression_tbd ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0; ALTER DATABASE regression_tbd RENAME TO regression_utf8; ALTER DATABASE regression_utf8 SET TABLESPACE regress_tblspace; -ALTER DATABASE regression_utf8 RESET TABLESPACE; +ALTER DATABASE regression_utf8 SET TABLESPACE pg_default; ALTER DATABASE regression_utf8 CONNECTION_LIMIT 123; -- Test PgDatabaseToastTable. Doing this with GRANT would be slow. BEGIN; @@ -10,7 +10,7 @@ UPDATE pg_database SET datacl = array_fill(makeaclitem(10, 10, 'USAGE', false), ARRAY[5e5::int]) WHERE datname = 'regression_utf8'; -- load catcache entry, if nothing else does -ALTER DATABASE regression_utf8 RESET TABLESPACE; +ALTER DATABASE regression_utf8 RENAME TO regression_rename_rolled_back; ROLLBACK; CREATE ROLE regress_datdba_before; CREATE ROLE regress_datdba_after; diff --git a/src/test/regress/expected/database_ddl.out b/src/test/regress/expected/database_ddl.out new file mode 100644 index 0000000000000..97657e52cfa09 --- /dev/null +++ b/src/test/regress/expected/database_ddl.out @@ -0,0 +1,88 @@ +-- +-- Tests for pg_get_database_ddl() +-- +-- To produce stable regression test output, strip locale/collation details +-- from the DDL output. Uses a plain SQL function to avoid a PL/pgSQL +-- dependency. +CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT) +RETURNS TEXT LANGUAGE sql AS $$ +SELECT regexp_replace( + regexp_replace( + regexp_replace( + regexp_replace( + regexp_replace( + ddl_input, + '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)', '', 'gi'), + '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1', '', 'gi'), + '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1', '', 'gi'), + '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1', '', 'gi'), + '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1', '', 'gi') +$$; +CREATE ROLE regress_datdba; +CREATE DATABASE regression_database_ddl + ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0 + OWNER regress_datdba; +ALTER DATABASE regression_database_ddl CONNECTION_LIMIT 123; +ALTER DATABASE regression_database_ddl SET random_page_cost = 2.0; +ALTER ROLE regress_datdba IN DATABASE regression_database_ddl SET random_page_cost = 1.1; +-- Database doesn't exist +SELECT * FROM pg_get_database_ddl('regression_database'); +ERROR: database "regression_database" does not exist +LINE 1: SELECT * FROM pg_get_database_ddl('regression_database'); + ^ +-- NULL value +SELECT * FROM pg_get_database_ddl(NULL); + pg_get_database_ddl +--------------------- +(0 rows) + +-- Invalid option value (should error) +SELECT * FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'invalid'); +ERROR: invalid value for boolean option "owner": invalid +-- Duplicate option (should error) +SELECT * FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'false', 'owner', 'true'); +ERROR: option "owner" is specified more than once +-- Without options +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl'); + ddl_filter +-------------------------------------------------------------------------------------- + CREATE DATABASE regression_database_ddl WITH TEMPLATE = template0 ENCODING = 'UTF8'; + ALTER DATABASE regression_database_ddl OWNER TO regress_datdba; + ALTER DATABASE regression_database_ddl CONNECTION LIMIT = 123; + ALTER DATABASE regression_database_ddl SET random_page_cost TO '2.0'; +(4 rows) + +-- With owner +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'true'); + ddl_filter +-------------------------------------------------------------------------------------- + CREATE DATABASE regression_database_ddl WITH TEMPLATE = template0 ENCODING = 'UTF8'; + ALTER DATABASE regression_database_ddl OWNER TO regress_datdba; + ALTER DATABASE regression_database_ddl CONNECTION LIMIT = 123; + ALTER DATABASE regression_database_ddl SET random_page_cost TO '2.0'; +(4 rows) + +-- Pretty-printed output +\pset format unaligned +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl', 'pretty', 'true', 'tablespace', 'false'); +ddl_filter +CREATE DATABASE regression_database_ddl + WITH TEMPLATE = template0 + ENCODING = 'UTF8'; +ALTER DATABASE regression_database_ddl OWNER TO regress_datdba; +ALTER DATABASE regression_database_ddl CONNECTION LIMIT = 123; +ALTER DATABASE regression_database_ddl SET random_page_cost TO '2.0'; +(4 rows) +\pset format aligned +-- Permission check: revoke CONNECT on database +CREATE ROLE regress_db_ddl_noaccess; +REVOKE CONNECT ON DATABASE regression_database_ddl FROM PUBLIC; +SET ROLE regress_db_ddl_noaccess; +SELECT * FROM pg_get_database_ddl('regression_database_ddl'); -- should fail +ERROR: permission denied for database regression_database_ddl +RESET ROLE; +GRANT CONNECT ON DATABASE regression_database_ddl TO PUBLIC; +DROP ROLE regress_db_ddl_noaccess; +DROP DATABASE regression_database_ddl; +DROP FUNCTION ddl_filter(text); +DROP ROLE regress_datdba; diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out index ba6f05eeb7df6..62a48a523a2d8 100644 --- a/src/test/regress/expected/domain.out +++ b/src/test/regress/expected/domain.out @@ -415,7 +415,8 @@ select row(0,1)::dcomptype; -- fail ERROR: value for domain dcomptype violates check constraint "c1" alter type comptype alter attribute r type varchar; -- fail ERROR: operator does not exist: character varying > double precision -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. alter type comptype alter attribute r type bigint; alter type comptype drop attribute r; -- fail ERROR: cannot drop column r of composite type comptype because other objects depend on it @@ -1019,6 +1020,11 @@ insert into domain_test values (1, 2); -- should fail alter table domain_test add column c str_domain; ERROR: domain str_domain does not allow null values +-- disallow duplicated not-null constraints +create domain int_domain1 as int constraint nn1 not null constraint nn2 not null; +ERROR: redundant NOT NULL constraint definition +LINE 1: ...domain int_domain1 as int constraint nn1 not null constraint... + ^ create domain str_domain2 as text check (value <> 'foo') default 'foo'; -- should fail alter table domain_test add column d str_domain2; diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out new file mode 100644 index 0000000000000..456d32eb13d8a --- /dev/null +++ b/src/test/regress/expected/eager_aggregate.out @@ -0,0 +1,1750 @@ +-- +-- EAGER AGGREGATION +-- Test we can push aggregation down below join +-- +CREATE TABLE eager_agg_t1 (a int, b int, c double precision); +CREATE TABLE eager_agg_t2 (a int, b int, c double precision); +CREATE TABLE eager_agg_t3 (a int, b int, c double precision); +INSERT INTO eager_agg_t1 SELECT i, i, i FROM generate_series(1, 1000) i; +INSERT INTO eager_agg_t2 SELECT i, i%10, i FROM generate_series(1, 1000) i; +INSERT INTO eager_agg_t3 SELECT i%10, i%10, i FROM generate_series(1, 1000) i; +ANALYZE eager_agg_t1; +ANALYZE eager_agg_t2; +ANALYZE eager_agg_t3; +-- +-- Test eager aggregation over base rel +-- +-- Perform scan of a table, aggregate the result, join it to the other table +-- and finalize the aggregation. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +------------------------------------------------------------------ + Finalize GroupAggregate + Output: t1.a, avg(t2.c) + Group Key: t1.a + -> Sort + Output: t1.a, (PARTIAL avg(t2.c)) + Sort Key: t1.a + -> Hash Join + Output: t1.a, (PARTIAL avg(t2.c)) + Hash Cond: (t1.b = t2.b) + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Hash + Output: t2.b, (PARTIAL avg(t2.c)) + -> Partial HashAggregate + Output: t2.b, PARTIAL avg(t2.c) + Group Key: t2.b + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.a, t2.b, t2.c +(18 rows) + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 496 + 2 | 497 + 3 | 498 + 4 | 499 + 5 | 500 + 6 | 501 + 7 | 502 + 8 | 503 + 9 | 504 +(9 rows) + +-- Produce results with sorting aggregation +SET enable_hashagg TO off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +------------------------------------------------------------------------ + Finalize GroupAggregate + Output: t1.a, avg(t2.c) + Group Key: t1.a + -> Sort + Output: t1.a, (PARTIAL avg(t2.c)) + Sort Key: t1.a + -> Hash Join + Output: t1.a, (PARTIAL avg(t2.c)) + Hash Cond: (t1.b = t2.b) + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Hash + Output: t2.b, (PARTIAL avg(t2.c)) + -> Partial GroupAggregate + Output: t2.b, PARTIAL avg(t2.c) + Group Key: t2.b + -> Sort + Output: t2.c, t2.b + Sort Key: t2.b + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.c, t2.b +(21 rows) + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 496 + 2 | 497 + 3 | 498 + 4 | 499 + 5 | 500 + 6 | 501 + 7 | 502 + 8 | 503 + 9 | 504 +(9 rows) + +RESET enable_hashagg; +-- +-- Test eager aggregation over join rel +-- +-- Perform join of tables, aggregate the result, join it to the other table +-- and finalize the aggregation. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +------------------------------------------------------------------------------ + Finalize GroupAggregate + Output: t1.a, avg((t2.c + t3.c)) + Group Key: t1.a + -> Sort + Output: t1.a, (PARTIAL avg((t2.c + t3.c))) + Sort Key: t1.a + -> Hash Join + Output: t1.a, (PARTIAL avg((t2.c + t3.c))) + Hash Cond: (t1.b = t2.b) + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Hash + Output: t2.b, (PARTIAL avg((t2.c + t3.c))) + -> Partial HashAggregate + Output: t2.b, PARTIAL avg((t2.c + t3.c)) + Group Key: t2.b + -> Hash Join + Output: t2.c, t2.b, t3.c + Hash Cond: (t3.a = t2.a) + -> Seq Scan on public.eager_agg_t3 t3 + Output: t3.a, t3.b, t3.c + -> Hash + Output: t2.c, t2.b, t2.a + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.c, t2.b, t2.a +(25 rows) + +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 497 + 2 | 499 + 3 | 501 + 4 | 503 + 5 | 505 + 6 | 507 + 7 | 509 + 8 | 511 + 9 | 513 +(9 rows) + +-- Produce results with sorting aggregation +SET enable_hashagg TO off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +------------------------------------------------------------------------------------ + Finalize GroupAggregate + Output: t1.a, avg((t2.c + t3.c)) + Group Key: t1.a + -> Sort + Output: t1.a, (PARTIAL avg((t2.c + t3.c))) + Sort Key: t1.a + -> Hash Join + Output: t1.a, (PARTIAL avg((t2.c + t3.c))) + Hash Cond: (t1.b = t2.b) + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Hash + Output: t2.b, (PARTIAL avg((t2.c + t3.c))) + -> Partial GroupAggregate + Output: t2.b, PARTIAL avg((t2.c + t3.c)) + Group Key: t2.b + -> Sort + Output: t2.c, t2.b, t3.c + Sort Key: t2.b + -> Hash Join + Output: t2.c, t2.b, t3.c + Hash Cond: (t3.a = t2.a) + -> Seq Scan on public.eager_agg_t3 t3 + Output: t3.a, t3.b, t3.c + -> Hash + Output: t2.c, t2.b, t2.a + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.c, t2.b, t2.a +(28 rows) + +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 497 + 2 | 499 + 3 | 501 + 4 | 503 + 5 | 505 + 6 | 507 + 7 | 509 + 8 | 511 + 9 | 513 +(9 rows) + +RESET enable_hashagg; +-- +-- Test that eager aggregation works for outer join +-- +-- Ensure aggregation can be pushed down to the non-nullable side +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + RIGHT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +------------------------------------------------------------------ + Finalize GroupAggregate + Output: t1.a, avg(t2.c) + Group Key: t1.a + -> Sort + Output: t1.a, (PARTIAL avg(t2.c)) + Sort Key: t1.a + -> Hash Right Join + Output: t1.a, (PARTIAL avg(t2.c)) + Hash Cond: (t1.b = t2.b) + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Hash + Output: t2.b, (PARTIAL avg(t2.c)) + -> Partial HashAggregate + Output: t2.b, PARTIAL avg(t2.c) + Group Key: t2.b + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.a, t2.b, t2.c +(18 rows) + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + RIGHT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 496 + 2 | 497 + 3 | 498 + 4 | 499 + 5 | 500 + 6 | 501 + 7 | 502 + 8 | 503 + 9 | 504 + | 505 +(10 rows) + +-- Ensure aggregation cannot be pushed down to the nullable side +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.b, avg(t2.c) + FROM eager_agg_t1 t1 + LEFT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t2.b ORDER BY t2.b; + QUERY PLAN +------------------------------------------------------------ + Sort + Output: t2.b, (avg(t2.c)) + Sort Key: t2.b + -> HashAggregate + Output: t2.b, avg(t2.c) + Group Key: t2.b + -> Hash Right Join + Output: t2.b, t2.c + Hash Cond: (t2.b = t1.b) + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.a, t2.b, t2.c + -> Hash + Output: t1.b + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.b +(15 rows) + +SELECT t2.b, avg(t2.c) + FROM eager_agg_t1 t1 + LEFT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t2.b ORDER BY t2.b; + b | avg +---+----- + 1 | 496 + 2 | 497 + 3 | 498 + 4 | 499 + 5 | 500 + 6 | 501 + 7 | 502 + 8 | 503 + 9 | 504 + | +(10 rows) + +-- +-- Test that eager aggregation works for parallel plans +-- +SET parallel_setup_cost=0; +SET parallel_tuple_cost=0; +SET min_parallel_table_scan_size=0; +SET max_parallel_workers_per_gather=4; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +--------------------------------------------------------------------------------- + Finalize GroupAggregate + Output: t1.a, avg(t2.c) + Group Key: t1.a + -> Gather Merge + Output: t1.a, (PARTIAL avg(t2.c)) + Workers Planned: 2 + -> Sort + Output: t1.a, (PARTIAL avg(t2.c)) + Sort Key: t1.a + -> Parallel Hash Join + Output: t1.a, (PARTIAL avg(t2.c)) + Hash Cond: (t1.b = t2.b) + -> Parallel Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Parallel Hash + Output: t2.b, (PARTIAL avg(t2.c)) + -> Partial HashAggregate + Output: t2.b, PARTIAL avg(t2.c) + Group Key: t2.b + -> Parallel Seq Scan on public.eager_agg_t2 t2 + Output: t2.a, t2.b, t2.c +(21 rows) + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 496 + 2 | 497 + 3 | 498 + 4 | 499 + 5 | 500 + 6 | 501 + 7 | 502 + 8 | 503 + 9 | 504 +(9 rows) + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; +-- +-- Test eager aggregation with GEQO +-- +SET geqo = on; +SET geqo_threshold = 2; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +------------------------------------------------------------------ + Finalize GroupAggregate + Output: t1.a, avg(t2.c) + Group Key: t1.a + -> Sort + Output: t1.a, (PARTIAL avg(t2.c)) + Sort Key: t1.a + -> Hash Join + Output: t1.a, (PARTIAL avg(t2.c)) + Hash Cond: (t1.b = t2.b) + -> Seq Scan on public.eager_agg_t1 t1 + Output: t1.a, t1.b, t1.c + -> Hash + Output: t2.b, (PARTIAL avg(t2.c)) + -> Partial HashAggregate + Output: t2.b, PARTIAL avg(t2.c) + Group Key: t2.b + -> Seq Scan on public.eager_agg_t2 t2 + Output: t2.a, t2.b, t2.c +(18 rows) + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + a | avg +---+----- + 1 | 496 + 2 | 497 + 3 | 498 + 4 | 499 + 5 | 500 + 6 | 501 + 7 | 502 + 8 | 503 + 9 | 504 +(9 rows) + +RESET geqo; +RESET geqo_threshold; +-- Ensure eager aggregation is not applied because random() is a volatile +-- function +EXPLAIN (COSTS OFF) +SELECT t1.a, avg(t2.c + random()) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +----------------------------------------------------- + GroupAggregate + Group Key: t1.a + -> Sort + Sort Key: t1.a + -> Hash Join + Hash Cond: (t2.b = t1.b) + -> Seq Scan on eager_agg_t2 t2 + -> Hash + -> Seq Scan on eager_agg_t1 t1 +(9 rows) + +EXPLAIN (COSTS OFF) +SELECT t1.a, avg(t2.c) FILTER (WHERE random() > 0.5) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + QUERY PLAN +----------------------------------------------------- + GroupAggregate + Group Key: t1.a + -> Sort + Sort Key: t1.a + -> Hash Join + Hash Cond: (t2.b = t1.b) + -> Seq Scan on eager_agg_t2 t2 + -> Hash + -> Seq Scan on eager_agg_t1 t1 +(9 rows) + +DROP TABLE eager_agg_t1; +DROP TABLE eager_agg_t2; +DROP TABLE eager_agg_t3; +-- +-- Test eager aggregation for partitionwise join +-- +-- Enable partitionwise aggregate, which by default is disabled. +SET enable_partitionwise_aggregate TO true; +-- Enable partitionwise join, which by default is disabled. +SET enable_partitionwise_join TO true; +CREATE TABLE eager_agg_tab1(x int, y int) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab1_p1 PARTITION OF eager_agg_tab1 FOR VALUES FROM (0) TO (5); +CREATE TABLE eager_agg_tab1_p2 PARTITION OF eager_agg_tab1 FOR VALUES FROM (5) TO (10); +CREATE TABLE eager_agg_tab1_p3 PARTITION OF eager_agg_tab1 FOR VALUES FROM (10) TO (15); +CREATE TABLE eager_agg_tab2(x int, y int) PARTITION BY RANGE(y); +CREATE TABLE eager_agg_tab2_p1 PARTITION OF eager_agg_tab2 FOR VALUES FROM (0) TO (5); +CREATE TABLE eager_agg_tab2_p2 PARTITION OF eager_agg_tab2 FOR VALUES FROM (5) TO (10); +CREATE TABLE eager_agg_tab2_p3 PARTITION OF eager_agg_tab2 FOR VALUES FROM (10) TO (15); +INSERT INTO eager_agg_tab1 SELECT i % 15, i % 10 FROM generate_series(1, 1000) i; +INSERT INTO eager_agg_tab2 SELECT i % 10, i % 15 FROM generate_series(1, 1000) i; +ANALYZE eager_agg_tab1; +ANALYZE eager_agg_tab2; +-- When GROUP BY clause matches; full aggregation is performed for each +-- partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Output: t1.x, (sum(t1.y)), (count(*)) + Sort Key: t1.x + -> Append + -> Finalize HashAggregate + Output: t1.x, sum(t1.y), count(*) + Group Key: t1.x + -> Hash Join + Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + Hash Cond: (t2.y = t1.x) + -> Seq Scan on public.eager_agg_tab2_p1 t2 + Output: t2.y + -> Hash + Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1.x, PARTIAL sum(t1.y), PARTIAL count(*) + Group Key: t1.x + -> Seq Scan on public.eager_agg_tab1_p1 t1 + Output: t1.x, t1.y + -> Finalize HashAggregate + Output: t1_1.x, sum(t1_1.y), count(*) + Group Key: t1_1.x + -> Hash Join + Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + Hash Cond: (t2_1.y = t1_1.x) + -> Seq Scan on public.eager_agg_tab2_p2 t2_1 + Output: t2_1.y + -> Hash + Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1_1.x, PARTIAL sum(t1_1.y), PARTIAL count(*) + Group Key: t1_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t1_1 + Output: t1_1.x, t1_1.y + -> Finalize HashAggregate + Output: t1_2.x, sum(t1_2.y), count(*) + Group Key: t1_2.x + -> Hash Join + Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + Hash Cond: (t2_2.y = t1_2.x) + -> Seq Scan on public.eager_agg_tab2_p3 t2_2 + Output: t2_2.y + -> Hash + Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1_2.x, PARTIAL sum(t1_2.y), PARTIAL count(*) + Group Key: t1_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t1_2 + Output: t1_2.x, t1_2.y +(49 rows) + +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + x | sum | count +----+-------+------- + 0 | 10890 | 4356 + 1 | 15544 | 4489 + 2 | 20033 | 4489 + 3 | 24522 | 4489 + 4 | 29011 | 4489 + 5 | 11390 | 4489 + 6 | 15879 | 4489 + 7 | 20368 | 4489 + 8 | 24857 | 4489 + 9 | 29346 | 4489 + 10 | 11055 | 4489 + 11 | 15246 | 4356 + 12 | 19602 | 4356 + 13 | 23958 | 4356 + 14 | 28314 | 4356 +(15 rows) + +-- GROUP BY having other matching key +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.y, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.y ORDER BY t2.y; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Output: t2.y, (sum(t1.y)), (count(*)) + Sort Key: t2.y + -> Append + -> Finalize HashAggregate + Output: t2.y, sum(t1.y), count(*) + Group Key: t2.y + -> Hash Join + Output: t2.y, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + Hash Cond: (t2.y = t1.x) + -> Seq Scan on public.eager_agg_tab2_p1 t2 + Output: t2.y + -> Hash + Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1.x, PARTIAL sum(t1.y), PARTIAL count(*) + Group Key: t1.x + -> Seq Scan on public.eager_agg_tab1_p1 t1 + Output: t1.y, t1.x + -> Finalize HashAggregate + Output: t2_1.y, sum(t1_1.y), count(*) + Group Key: t2_1.y + -> Hash Join + Output: t2_1.y, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + Hash Cond: (t2_1.y = t1_1.x) + -> Seq Scan on public.eager_agg_tab2_p2 t2_1 + Output: t2_1.y + -> Hash + Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1_1.x, PARTIAL sum(t1_1.y), PARTIAL count(*) + Group Key: t1_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t1_1 + Output: t1_1.y, t1_1.x + -> Finalize HashAggregate + Output: t2_2.y, sum(t1_2.y), count(*) + Group Key: t2_2.y + -> Hash Join + Output: t2_2.y, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + Hash Cond: (t2_2.y = t1_2.x) + -> Seq Scan on public.eager_agg_tab2_p3 t2_2 + Output: t2_2.y + -> Hash + Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1_2.x, PARTIAL sum(t1_2.y), PARTIAL count(*) + Group Key: t1_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t1_2 + Output: t1_2.y, t1_2.x +(49 rows) + +SELECT t2.y, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.y ORDER BY t2.y; + y | sum | count +----+-------+------- + 0 | 10890 | 4356 + 1 | 15544 | 4489 + 2 | 20033 | 4489 + 3 | 24522 | 4489 + 4 | 29011 | 4489 + 5 | 11390 | 4489 + 6 | 15879 | 4489 + 7 | 20368 | 4489 + 8 | 24857 | 4489 + 9 | 29346 | 4489 + 10 | 11055 | 4489 + 11 | 15246 | 4356 + 12 | 19602 | 4356 + 13 | 23958 | 4356 + 14 | 28314 | 4356 +(15 rows) + +-- When GROUP BY clause does not match; partial aggregation is performed for +-- each partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.x, sum(t1.x), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; + QUERY PLAN +------------------------------------------------------------------------------------------------------------ + Sort + Output: t2.x, (sum(t1.x)), (count(*)) + Sort Key: t2.x + -> Finalize HashAggregate + Output: t2.x, sum(t1.x), count(*) + Group Key: t2.x + Filter: (avg(t1.x) > '5'::numeric) + -> Append + -> Hash Join + Output: t2.x, (PARTIAL sum(t1.x)), (PARTIAL count(*)), (PARTIAL avg(t1.x)) + Hash Cond: (t2.y = t1.x) + -> Seq Scan on public.eager_agg_tab2_p1 t2 + Output: t2.x, t2.y + -> Hash + Output: t1.x, (PARTIAL sum(t1.x)), (PARTIAL count(*)), (PARTIAL avg(t1.x)) + -> Partial HashAggregate + Output: t1.x, PARTIAL sum(t1.x), PARTIAL count(*), PARTIAL avg(t1.x) + Group Key: t1.x + -> Seq Scan on public.eager_agg_tab1_p1 t1 + Output: t1.x + -> Hash Join + Output: t2_1.x, (PARTIAL sum(t1_1.x)), (PARTIAL count(*)), (PARTIAL avg(t1_1.x)) + Hash Cond: (t2_1.y = t1_1.x) + -> Seq Scan on public.eager_agg_tab2_p2 t2_1 + Output: t2_1.x, t2_1.y + -> Hash + Output: t1_1.x, (PARTIAL sum(t1_1.x)), (PARTIAL count(*)), (PARTIAL avg(t1_1.x)) + -> Partial HashAggregate + Output: t1_1.x, PARTIAL sum(t1_1.x), PARTIAL count(*), PARTIAL avg(t1_1.x) + Group Key: t1_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t1_1 + Output: t1_1.x + -> Hash Join + Output: t2_2.x, (PARTIAL sum(t1_2.x)), (PARTIAL count(*)), (PARTIAL avg(t1_2.x)) + Hash Cond: (t2_2.y = t1_2.x) + -> Seq Scan on public.eager_agg_tab2_p3 t2_2 + Output: t2_2.x, t2_2.y + -> Hash + Output: t1_2.x, (PARTIAL sum(t1_2.x)), (PARTIAL count(*)), (PARTIAL avg(t1_2.x)) + -> Partial HashAggregate + Output: t1_2.x, PARTIAL sum(t1_2.x), PARTIAL count(*), PARTIAL avg(t1_2.x) + Group Key: t1_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t1_2 + Output: t1_2.x +(44 rows) + +SELECT t2.x, sum(t1.x), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; + x | sum | count +---+-------+------- + 0 | 33835 | 6667 + 1 | 39502 | 6667 + 2 | 46169 | 6667 + 3 | 52836 | 6667 + 4 | 59503 | 6667 + 5 | 33500 | 6667 + 6 | 39837 | 6667 + 7 | 46504 | 6667 + 8 | 53171 | 6667 + 9 | 59838 | 6667 +(10 rows) + +-- Check with eager aggregation over join rel +-- full aggregation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + QUERY PLAN +------------------------------------------------------------------------------------------- + Sort + Output: t1.x, (sum((t2.y + t3.y))) + Sort Key: t1.x + -> Append + -> Finalize HashAggregate + Output: t1.x, sum((t2.y + t3.y)) + Group Key: t1.x + -> Hash Join + Output: t1.x, (PARTIAL sum((t2.y + t3.y))) + Hash Cond: (t1.x = t2.x) + -> Seq Scan on public.eager_agg_tab1_p1 t1 + Output: t1.x + -> Hash + Output: t2.x, t3.x, (PARTIAL sum((t2.y + t3.y))) + -> Partial HashAggregate + Output: t2.x, t3.x, PARTIAL sum((t2.y + t3.y)) + Group Key: t2.x + -> Hash Join + Output: t2.y, t2.x, t3.y, t3.x + Hash Cond: (t2.x = t3.x) + -> Seq Scan on public.eager_agg_tab1_p1 t2 + Output: t2.y, t2.x + -> Hash + Output: t3.y, t3.x + -> Seq Scan on public.eager_agg_tab1_p1 t3 + Output: t3.y, t3.x + -> Finalize HashAggregate + Output: t1_1.x, sum((t2_1.y + t3_1.y)) + Group Key: t1_1.x + -> Hash Join + Output: t1_1.x, (PARTIAL sum((t2_1.y + t3_1.y))) + Hash Cond: (t1_1.x = t2_1.x) + -> Seq Scan on public.eager_agg_tab1_p2 t1_1 + Output: t1_1.x + -> Hash + Output: t2_1.x, t3_1.x, (PARTIAL sum((t2_1.y + t3_1.y))) + -> Partial HashAggregate + Output: t2_1.x, t3_1.x, PARTIAL sum((t2_1.y + t3_1.y)) + Group Key: t2_1.x + -> Hash Join + Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x + Hash Cond: (t2_1.x = t3_1.x) + -> Seq Scan on public.eager_agg_tab1_p2 t2_1 + Output: t2_1.y, t2_1.x + -> Hash + Output: t3_1.y, t3_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t3_1 + Output: t3_1.y, t3_1.x + -> Finalize HashAggregate + Output: t1_2.x, sum((t2_2.y + t3_2.y)) + Group Key: t1_2.x + -> Hash Join + Output: t1_2.x, (PARTIAL sum((t2_2.y + t3_2.y))) + Hash Cond: (t1_2.x = t2_2.x) + -> Seq Scan on public.eager_agg_tab1_p3 t1_2 + Output: t1_2.x + -> Hash + Output: t2_2.x, t3_2.x, (PARTIAL sum((t2_2.y + t3_2.y))) + -> Partial HashAggregate + Output: t2_2.x, t3_2.x, PARTIAL sum((t2_2.y + t3_2.y)) + Group Key: t2_2.x + -> Hash Join + Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x + Hash Cond: (t2_2.x = t3_2.x) + -> Seq Scan on public.eager_agg_tab1_p3 t2_2 + Output: t2_2.y, t2_2.x + -> Hash + Output: t3_2.y, t3_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t3_2 + Output: t3_2.y, t3_2.x +(70 rows) + +SELECT t1.x, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + x | sum +----+--------- + 0 | 1437480 + 1 | 2082896 + 2 | 2684422 + 3 | 3285948 + 4 | 3887474 + 5 | 1526260 + 6 | 2127786 + 7 | 2729312 + 8 | 3330838 + 9 | 3932364 + 10 | 1481370 + 11 | 2012472 + 12 | 2587464 + 13 | 3162456 + 14 | 3737448 +(15 rows) + +-- partial aggregation +SET enable_hashagg TO off; +SET max_parallel_workers_per_gather TO 0; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t3.y, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + QUERY PLAN +------------------------------------------------------------------------------------------- + Finalize GroupAggregate + Output: t3.y, sum((t2.y + t3.y)) + Group Key: t3.y + -> Sort + Output: t3.y, (PARTIAL sum((t2.y + t3.y))) + Sort Key: t3.y + -> Append + -> Hash Join + Output: t3.y, (PARTIAL sum((t2.y + t3.y))) + Hash Cond: (t2.x = t1.x) + -> Partial GroupAggregate + Output: t2.x, t3.y, t3.x, PARTIAL sum((t2.y + t3.y)) + Group Key: t2.x, t3.y, t3.x + -> Incremental Sort + Output: t2.y, t2.x, t3.y, t3.x + Sort Key: t2.x, t3.y + Presorted Key: t2.x + -> Merge Join + Output: t2.y, t2.x, t3.y, t3.x + Merge Cond: (t2.x = t3.x) + -> Sort + Output: t2.y, t2.x + Sort Key: t2.x + -> Seq Scan on public.eager_agg_tab1_p1 t2 + Output: t2.y, t2.x + -> Sort + Output: t3.y, t3.x + Sort Key: t3.x + -> Seq Scan on public.eager_agg_tab1_p1 t3 + Output: t3.y, t3.x + -> Hash + Output: t1.x + -> Seq Scan on public.eager_agg_tab1_p1 t1 + Output: t1.x + -> Hash Join + Output: t3_1.y, (PARTIAL sum((t2_1.y + t3_1.y))) + Hash Cond: (t2_1.x = t1_1.x) + -> Partial GroupAggregate + Output: t2_1.x, t3_1.y, t3_1.x, PARTIAL sum((t2_1.y + t3_1.y)) + Group Key: t2_1.x, t3_1.y, t3_1.x + -> Incremental Sort + Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x + Sort Key: t2_1.x, t3_1.y + Presorted Key: t2_1.x + -> Merge Join + Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x + Merge Cond: (t2_1.x = t3_1.x) + -> Sort + Output: t2_1.y, t2_1.x + Sort Key: t2_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t2_1 + Output: t2_1.y, t2_1.x + -> Sort + Output: t3_1.y, t3_1.x + Sort Key: t3_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t3_1 + Output: t3_1.y, t3_1.x + -> Hash + Output: t1_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t1_1 + Output: t1_1.x + -> Hash Join + Output: t3_2.y, (PARTIAL sum((t2_2.y + t3_2.y))) + Hash Cond: (t2_2.x = t1_2.x) + -> Partial GroupAggregate + Output: t2_2.x, t3_2.y, t3_2.x, PARTIAL sum((t2_2.y + t3_2.y)) + Group Key: t2_2.x, t3_2.y, t3_2.x + -> Incremental Sort + Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x + Sort Key: t2_2.x, t3_2.y + Presorted Key: t2_2.x + -> Merge Join + Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x + Merge Cond: (t2_2.x = t3_2.x) + -> Sort + Output: t2_2.y, t2_2.x + Sort Key: t2_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t2_2 + Output: t2_2.y, t2_2.x + -> Sort + Output: t3_2.y, t3_2.x + Sort Key: t3_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t3_2 + Output: t3_2.y, t3_2.x + -> Hash + Output: t1_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t1_2 + Output: t1_2.x +(88 rows) + +SELECT t3.y, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + y | sum +---+--------- + 0 | 1111110 + 1 | 2000132 + 2 | 2889154 + 3 | 3778176 + 4 | 4667198 + 5 | 3334000 + 6 | 4223022 + 7 | 5112044 + 8 | 6001066 + 9 | 6890088 +(10 rows) + +RESET enable_hashagg; +RESET max_parallel_workers_per_gather; +-- try that with GEQO too +SET geqo = on; +SET geqo_threshold = 2; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Output: t1.x, (sum(t1.y)), (count(*)) + Sort Key: t1.x + -> Append + -> Finalize HashAggregate + Output: t1.x, sum(t1.y), count(*) + Group Key: t1.x + -> Hash Join + Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + Hash Cond: (t2.y = t1.x) + -> Seq Scan on public.eager_agg_tab2_p1 t2 + Output: t2.y + -> Hash + Output: t1.x, (PARTIAL sum(t1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1.x, PARTIAL sum(t1.y), PARTIAL count(*) + Group Key: t1.x + -> Seq Scan on public.eager_agg_tab1_p1 t1 + Output: t1.x, t1.y + -> Finalize HashAggregate + Output: t1_1.x, sum(t1_1.y), count(*) + Group Key: t1_1.x + -> Hash Join + Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + Hash Cond: (t2_1.y = t1_1.x) + -> Seq Scan on public.eager_agg_tab2_p2 t2_1 + Output: t2_1.y + -> Hash + Output: t1_1.x, (PARTIAL sum(t1_1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1_1.x, PARTIAL sum(t1_1.y), PARTIAL count(*) + Group Key: t1_1.x + -> Seq Scan on public.eager_agg_tab1_p2 t1_1 + Output: t1_1.x, t1_1.y + -> Finalize HashAggregate + Output: t1_2.x, sum(t1_2.y), count(*) + Group Key: t1_2.x + -> Hash Join + Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + Hash Cond: (t2_2.y = t1_2.x) + -> Seq Scan on public.eager_agg_tab2_p3 t2_2 + Output: t2_2.y + -> Hash + Output: t1_2.x, (PARTIAL sum(t1_2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t1_2.x, PARTIAL sum(t1_2.y), PARTIAL count(*) + Group Key: t1_2.x + -> Seq Scan on public.eager_agg_tab1_p3 t1_2 + Output: t1_2.x, t1_2.y +(49 rows) + +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + x | sum | count +----+-------+------- + 0 | 10890 | 4356 + 1 | 15544 | 4489 + 2 | 20033 | 4489 + 3 | 24522 | 4489 + 4 | 29011 | 4489 + 5 | 11390 | 4489 + 6 | 15879 | 4489 + 7 | 20368 | 4489 + 8 | 24857 | 4489 + 9 | 29346 | 4489 + 10 | 11055 | 4489 + 11 | 15246 | 4356 + 12 | 19602 | 4356 + 13 | 23958 | 4356 + 14 | 28314 | 4356 +(15 rows) + +RESET geqo; +RESET geqo_threshold; +DROP TABLE eager_agg_tab1; +DROP TABLE eager_agg_tab2; +-- +-- Test with multi-level partitioning scheme +-- +CREATE TABLE eager_agg_tab_ml(x int, y int) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab_ml_p1 PARTITION OF eager_agg_tab_ml FOR VALUES FROM (0) TO (10); +CREATE TABLE eager_agg_tab_ml_p2 PARTITION OF eager_agg_tab_ml FOR VALUES FROM (10) TO (20) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab_ml_p2_s1 PARTITION OF eager_agg_tab_ml_p2 FOR VALUES FROM (10) TO (15); +CREATE TABLE eager_agg_tab_ml_p2_s2 PARTITION OF eager_agg_tab_ml_p2 FOR VALUES FROM (15) TO (20); +CREATE TABLE eager_agg_tab_ml_p3 PARTITION OF eager_agg_tab_ml FOR VALUES FROM (20) TO (30) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab_ml_p3_s1 PARTITION OF eager_agg_tab_ml_p3 FOR VALUES FROM (20) TO (25); +CREATE TABLE eager_agg_tab_ml_p3_s2 PARTITION OF eager_agg_tab_ml_p3 FOR VALUES FROM (25) TO (30); +INSERT INTO eager_agg_tab_ml SELECT i % 30, i % 30 FROM generate_series(1, 1000) i; +ANALYZE eager_agg_tab_ml; +-- When GROUP BY clause matches; full aggregation is performed for each +-- partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Output: t1.x, (sum(t2.y)), (count(*)) + Sort Key: t1.x + -> Append + -> Finalize HashAggregate + Output: t1.x, sum(t2.y), count(*) + Group Key: t1.x + -> Hash Join + Output: t1.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + Hash Cond: (t1.x = t2.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t1 + Output: t1.x + -> Hash + Output: t2.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2.x, PARTIAL sum(t2.y), PARTIAL count(*) + Group Key: t2.x + -> Seq Scan on public.eager_agg_tab_ml_p1 t2 + Output: t2.y, t2.x + -> Finalize HashAggregate + Output: t1_1.x, sum(t2_1.y), count(*) + Group Key: t1_1.x + -> Hash Join + Output: t1_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + Hash Cond: (t1_1.x = t2_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 + Output: t1_1.x + -> Hash + Output: t2_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_1.x, PARTIAL sum(t2_1.y), PARTIAL count(*) + Group Key: t2_1.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t2_1 + Output: t2_1.y, t2_1.x + -> Finalize HashAggregate + Output: t1_2.x, sum(t2_2.y), count(*) + Group Key: t1_2.x + -> Hash Join + Output: t1_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + Hash Cond: (t1_2.x = t2_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 + Output: t1_2.x + -> Hash + Output: t2_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_2.x, PARTIAL sum(t2_2.y), PARTIAL count(*) + Group Key: t2_2.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t2_2 + Output: t2_2.y, t2_2.x + -> Finalize HashAggregate + Output: t1_3.x, sum(t2_3.y), count(*) + Group Key: t1_3.x + -> Hash Join + Output: t1_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + Hash Cond: (t1_3.x = t2_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 + Output: t1_3.x + -> Hash + Output: t2_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_3.x, PARTIAL sum(t2_3.y), PARTIAL count(*) + Group Key: t2_3.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t2_3 + Output: t2_3.y, t2_3.x + -> Finalize HashAggregate + Output: t1_4.x, sum(t2_4.y), count(*) + Group Key: t1_4.x + -> Hash Join + Output: t1_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + Hash Cond: (t1_4.x = t2_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 + Output: t1_4.x + -> Hash + Output: t2_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_4.x, PARTIAL sum(t2_4.y), PARTIAL count(*) + Group Key: t2_4.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 + Output: t2_4.y, t2_4.x +(79 rows) + +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + x | sum | count +----+-------+------- + 0 | 0 | 1089 + 1 | 1156 | 1156 + 2 | 2312 | 1156 + 3 | 3468 | 1156 + 4 | 4624 | 1156 + 5 | 5780 | 1156 + 6 | 6936 | 1156 + 7 | 8092 | 1156 + 8 | 9248 | 1156 + 9 | 10404 | 1156 + 10 | 11560 | 1156 + 11 | 11979 | 1089 + 12 | 13068 | 1089 + 13 | 14157 | 1089 + 14 | 15246 | 1089 + 15 | 16335 | 1089 + 16 | 17424 | 1089 + 17 | 18513 | 1089 + 18 | 19602 | 1089 + 19 | 20691 | 1089 + 20 | 21780 | 1089 + 21 | 22869 | 1089 + 22 | 23958 | 1089 + 23 | 25047 | 1089 + 24 | 26136 | 1089 + 25 | 27225 | 1089 + 26 | 28314 | 1089 + 27 | 29403 | 1089 + 28 | 30492 | 1089 + 29 | 31581 | 1089 +(30 rows) + +-- When GROUP BY clause does not match; partial aggregation is performed for +-- each partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.y, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.y ORDER BY t1.y; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Output: t1.y, (sum(t2.y)), (count(*)) + Sort Key: t1.y + -> Finalize HashAggregate + Output: t1.y, sum(t2.y), count(*) + Group Key: t1.y + -> Append + -> Hash Join + Output: t1.y, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + Hash Cond: (t1.x = t2.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t1 + Output: t1.y, t1.x + -> Hash + Output: t2.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2.x, PARTIAL sum(t2.y), PARTIAL count(*) + Group Key: t2.x + -> Seq Scan on public.eager_agg_tab_ml_p1 t2 + Output: t2.y, t2.x + -> Hash Join + Output: t1_1.y, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + Hash Cond: (t1_1.x = t2_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 + Output: t1_1.y, t1_1.x + -> Hash + Output: t2_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_1.x, PARTIAL sum(t2_1.y), PARTIAL count(*) + Group Key: t2_1.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t2_1 + Output: t2_1.y, t2_1.x + -> Hash Join + Output: t1_2.y, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + Hash Cond: (t1_2.x = t2_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 + Output: t1_2.y, t1_2.x + -> Hash + Output: t2_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_2.x, PARTIAL sum(t2_2.y), PARTIAL count(*) + Group Key: t2_2.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t2_2 + Output: t2_2.y, t2_2.x + -> Hash Join + Output: t1_3.y, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + Hash Cond: (t1_3.x = t2_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 + Output: t1_3.y, t1_3.x + -> Hash + Output: t2_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_3.x, PARTIAL sum(t2_3.y), PARTIAL count(*) + Group Key: t2_3.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t2_3 + Output: t2_3.y, t2_3.x + -> Hash Join + Output: t1_4.y, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + Hash Cond: (t1_4.x = t2_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 + Output: t1_4.y, t1_4.x + -> Hash + Output: t2_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_4.x, PARTIAL sum(t2_4.y), PARTIAL count(*) + Group Key: t2_4.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 + Output: t2_4.y, t2_4.x +(67 rows) + +SELECT t1.y, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.y ORDER BY t1.y; + y | sum | count +----+-------+------- + 0 | 0 | 1089 + 1 | 1156 | 1156 + 2 | 2312 | 1156 + 3 | 3468 | 1156 + 4 | 4624 | 1156 + 5 | 5780 | 1156 + 6 | 6936 | 1156 + 7 | 8092 | 1156 + 8 | 9248 | 1156 + 9 | 10404 | 1156 + 10 | 11560 | 1156 + 11 | 11979 | 1089 + 12 | 13068 | 1089 + 13 | 14157 | 1089 + 14 | 15246 | 1089 + 15 | 16335 | 1089 + 16 | 17424 | 1089 + 17 | 18513 | 1089 + 18 | 19602 | 1089 + 19 | 20691 | 1089 + 20 | 21780 | 1089 + 21 | 22869 | 1089 + 22 | 23958 | 1089 + 23 | 25047 | 1089 + 24 | 26136 | 1089 + 25 | 27225 | 1089 + 26 | 28314 | 1089 + 27 | 29403 | 1089 + 28 | 30492 | 1089 + 29 | 31581 | 1089 +(30 rows) + +-- Check with eager aggregation over join rel +-- full aggregation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + QUERY PLAN +---------------------------------------------------------------------------------------------------------- + Sort + Output: t1.x, (sum((t2.y + t3.y))), (count(*)) + Sort Key: t1.x + -> Append + -> Finalize HashAggregate + Output: t1.x, sum((t2.y + t3.y)), count(*) + Group Key: t1.x + -> Hash Join + Output: t1.x, (PARTIAL sum((t2.y + t3.y))), (PARTIAL count(*)) + Hash Cond: (t1.x = t2.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t1 + Output: t1.x + -> Hash + Output: t2.x, t3.x, (PARTIAL sum((t2.y + t3.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2.x, t3.x, PARTIAL sum((t2.y + t3.y)), PARTIAL count(*) + Group Key: t2.x + -> Hash Join + Output: t2.y, t2.x, t3.y, t3.x + Hash Cond: (t2.x = t3.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t2 + Output: t2.y, t2.x + -> Hash + Output: t3.y, t3.x + -> Seq Scan on public.eager_agg_tab_ml_p1 t3 + Output: t3.y, t3.x + -> Finalize HashAggregate + Output: t1_1.x, sum((t2_1.y + t3_1.y)), count(*) + Group Key: t1_1.x + -> Hash Join + Output: t1_1.x, (PARTIAL sum((t2_1.y + t3_1.y))), (PARTIAL count(*)) + Hash Cond: (t1_1.x = t2_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 + Output: t1_1.x + -> Hash + Output: t2_1.x, t3_1.x, (PARTIAL sum((t2_1.y + t3_1.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_1.x, t3_1.x, PARTIAL sum((t2_1.y + t3_1.y)), PARTIAL count(*) + Group Key: t2_1.x + -> Hash Join + Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x + Hash Cond: (t2_1.x = t3_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t2_1 + Output: t2_1.y, t2_1.x + -> Hash + Output: t3_1.y, t3_1.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t3_1 + Output: t3_1.y, t3_1.x + -> Finalize HashAggregate + Output: t1_2.x, sum((t2_2.y + t3_2.y)), count(*) + Group Key: t1_2.x + -> Hash Join + Output: t1_2.x, (PARTIAL sum((t2_2.y + t3_2.y))), (PARTIAL count(*)) + Hash Cond: (t1_2.x = t2_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 + Output: t1_2.x + -> Hash + Output: t2_2.x, t3_2.x, (PARTIAL sum((t2_2.y + t3_2.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_2.x, t3_2.x, PARTIAL sum((t2_2.y + t3_2.y)), PARTIAL count(*) + Group Key: t2_2.x + -> Hash Join + Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x + Hash Cond: (t2_2.x = t3_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t2_2 + Output: t2_2.y, t2_2.x + -> Hash + Output: t3_2.y, t3_2.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t3_2 + Output: t3_2.y, t3_2.x + -> Finalize HashAggregate + Output: t1_3.x, sum((t2_3.y + t3_3.y)), count(*) + Group Key: t1_3.x + -> Hash Join + Output: t1_3.x, (PARTIAL sum((t2_3.y + t3_3.y))), (PARTIAL count(*)) + Hash Cond: (t1_3.x = t2_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 + Output: t1_3.x + -> Hash + Output: t2_3.x, t3_3.x, (PARTIAL sum((t2_3.y + t3_3.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_3.x, t3_3.x, PARTIAL sum((t2_3.y + t3_3.y)), PARTIAL count(*) + Group Key: t2_3.x + -> Hash Join + Output: t2_3.y, t2_3.x, t3_3.y, t3_3.x + Hash Cond: (t2_3.x = t3_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t2_3 + Output: t2_3.y, t2_3.x + -> Hash + Output: t3_3.y, t3_3.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t3_3 + Output: t3_3.y, t3_3.x + -> Finalize HashAggregate + Output: t1_4.x, sum((t2_4.y + t3_4.y)), count(*) + Group Key: t1_4.x + -> Hash Join + Output: t1_4.x, (PARTIAL sum((t2_4.y + t3_4.y))), (PARTIAL count(*)) + Hash Cond: (t1_4.x = t2_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 + Output: t1_4.x + -> Hash + Output: t2_4.x, t3_4.x, (PARTIAL sum((t2_4.y + t3_4.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_4.x, t3_4.x, PARTIAL sum((t2_4.y + t3_4.y)), PARTIAL count(*) + Group Key: t2_4.x + -> Hash Join + Output: t2_4.y, t2_4.x, t3_4.y, t3_4.x + Hash Cond: (t2_4.x = t3_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 + Output: t2_4.y, t2_4.x + -> Hash + Output: t3_4.y, t3_4.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t3_4 + Output: t3_4.y, t3_4.x +(114 rows) + +SELECT t1.x, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + x | sum | count +----+---------+------- + 0 | 0 | 35937 + 1 | 78608 | 39304 + 2 | 157216 | 39304 + 3 | 235824 | 39304 + 4 | 314432 | 39304 + 5 | 393040 | 39304 + 6 | 471648 | 39304 + 7 | 550256 | 39304 + 8 | 628864 | 39304 + 9 | 707472 | 39304 + 10 | 786080 | 39304 + 11 | 790614 | 35937 + 12 | 862488 | 35937 + 13 | 934362 | 35937 + 14 | 1006236 | 35937 + 15 | 1078110 | 35937 + 16 | 1149984 | 35937 + 17 | 1221858 | 35937 + 18 | 1293732 | 35937 + 19 | 1365606 | 35937 + 20 | 1437480 | 35937 + 21 | 1509354 | 35937 + 22 | 1581228 | 35937 + 23 | 1653102 | 35937 + 24 | 1724976 | 35937 + 25 | 1796850 | 35937 + 26 | 1868724 | 35937 + 27 | 1940598 | 35937 + 28 | 2012472 | 35937 + 29 | 2084346 | 35937 +(30 rows) + +-- partial aggregation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t3.y, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------ + Sort + Output: t3.y, (sum((t2.y + t3.y))), (count(*)) + Sort Key: t3.y + -> Finalize HashAggregate + Output: t3.y, sum((t2.y + t3.y)), count(*) + Group Key: t3.y + -> Append + -> Hash Join + Output: t3.y, (PARTIAL sum((t2.y + t3.y))), (PARTIAL count(*)) + Hash Cond: (t1.x = t2.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t1 + Output: t1.x + -> Hash + Output: t2.x, t3.y, t3.x, (PARTIAL sum((t2.y + t3.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2.x, t3.y, t3.x, PARTIAL sum((t2.y + t3.y)), PARTIAL count(*) + Group Key: t2.x, t3.y, t3.x + -> Hash Join + Output: t2.y, t2.x, t3.y, t3.x + Hash Cond: (t2.x = t3.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t2 + Output: t2.y, t2.x + -> Hash + Output: t3.y, t3.x + -> Seq Scan on public.eager_agg_tab_ml_p1 t3 + Output: t3.y, t3.x + -> Hash Join + Output: t3_1.y, (PARTIAL sum((t2_1.y + t3_1.y))), (PARTIAL count(*)) + Hash Cond: (t1_1.x = t2_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 + Output: t1_1.x + -> Hash + Output: t2_1.x, t3_1.y, t3_1.x, (PARTIAL sum((t2_1.y + t3_1.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_1.x, t3_1.y, t3_1.x, PARTIAL sum((t2_1.y + t3_1.y)), PARTIAL count(*) + Group Key: t2_1.x, t3_1.y, t3_1.x + -> Hash Join + Output: t2_1.y, t2_1.x, t3_1.y, t3_1.x + Hash Cond: (t2_1.x = t3_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t2_1 + Output: t2_1.y, t2_1.x + -> Hash + Output: t3_1.y, t3_1.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t3_1 + Output: t3_1.y, t3_1.x + -> Hash Join + Output: t3_2.y, (PARTIAL sum((t2_2.y + t3_2.y))), (PARTIAL count(*)) + Hash Cond: (t1_2.x = t2_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 + Output: t1_2.x + -> Hash + Output: t2_2.x, t3_2.y, t3_2.x, (PARTIAL sum((t2_2.y + t3_2.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_2.x, t3_2.y, t3_2.x, PARTIAL sum((t2_2.y + t3_2.y)), PARTIAL count(*) + Group Key: t2_2.x, t3_2.y, t3_2.x + -> Hash Join + Output: t2_2.y, t2_2.x, t3_2.y, t3_2.x + Hash Cond: (t2_2.x = t3_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t2_2 + Output: t2_2.y, t2_2.x + -> Hash + Output: t3_2.y, t3_2.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t3_2 + Output: t3_2.y, t3_2.x + -> Hash Join + Output: t3_3.y, (PARTIAL sum((t2_3.y + t3_3.y))), (PARTIAL count(*)) + Hash Cond: (t1_3.x = t2_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 + Output: t1_3.x + -> Hash + Output: t2_3.x, t3_3.y, t3_3.x, (PARTIAL sum((t2_3.y + t3_3.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_3.x, t3_3.y, t3_3.x, PARTIAL sum((t2_3.y + t3_3.y)), PARTIAL count(*) + Group Key: t2_3.x, t3_3.y, t3_3.x + -> Hash Join + Output: t2_3.y, t2_3.x, t3_3.y, t3_3.x + Hash Cond: (t2_3.x = t3_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t2_3 + Output: t2_3.y, t2_3.x + -> Hash + Output: t3_3.y, t3_3.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t3_3 + Output: t3_3.y, t3_3.x + -> Hash Join + Output: t3_4.y, (PARTIAL sum((t2_4.y + t3_4.y))), (PARTIAL count(*)) + Hash Cond: (t1_4.x = t2_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 + Output: t1_4.x + -> Hash + Output: t2_4.x, t3_4.y, t3_4.x, (PARTIAL sum((t2_4.y + t3_4.y))), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_4.x, t3_4.y, t3_4.x, PARTIAL sum((t2_4.y + t3_4.y)), PARTIAL count(*) + Group Key: t2_4.x, t3_4.y, t3_4.x + -> Hash Join + Output: t2_4.y, t2_4.x, t3_4.y, t3_4.x + Hash Cond: (t2_4.x = t3_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 + Output: t2_4.y, t2_4.x + -> Hash + Output: t3_4.y, t3_4.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t3_4 + Output: t3_4.y, t3_4.x +(102 rows) + +SELECT t3.y, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + y | sum | count +----+---------+------- + 0 | 0 | 35937 + 1 | 78608 | 39304 + 2 | 157216 | 39304 + 3 | 235824 | 39304 + 4 | 314432 | 39304 + 5 | 393040 | 39304 + 6 | 471648 | 39304 + 7 | 550256 | 39304 + 8 | 628864 | 39304 + 9 | 707472 | 39304 + 10 | 786080 | 39304 + 11 | 790614 | 35937 + 12 | 862488 | 35937 + 13 | 934362 | 35937 + 14 | 1006236 | 35937 + 15 | 1078110 | 35937 + 16 | 1149984 | 35937 + 17 | 1221858 | 35937 + 18 | 1293732 | 35937 + 19 | 1365606 | 35937 + 20 | 1437480 | 35937 + 21 | 1509354 | 35937 + 22 | 1581228 | 35937 + 23 | 1653102 | 35937 + 24 | 1724976 | 35937 + 25 | 1796850 | 35937 + 26 | 1868724 | 35937 + 27 | 1940598 | 35937 + 28 | 2012472 | 35937 + 29 | 2084346 | 35937 +(30 rows) + +-- try that with GEQO too +SET geqo = on; +SET geqo_threshold = 2; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + QUERY PLAN +--------------------------------------------------------------------------------------- + Sort + Output: t1.x, (sum(t2.y)), (count(*)) + Sort Key: t1.x + -> Append + -> Finalize HashAggregate + Output: t1.x, sum(t2.y), count(*) + Group Key: t1.x + -> Hash Join + Output: t1.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + Hash Cond: (t1.x = t2.x) + -> Seq Scan on public.eager_agg_tab_ml_p1 t1 + Output: t1.x + -> Hash + Output: t2.x, (PARTIAL sum(t2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2.x, PARTIAL sum(t2.y), PARTIAL count(*) + Group Key: t2.x + -> Seq Scan on public.eager_agg_tab_ml_p1 t2 + Output: t2.y, t2.x + -> Finalize HashAggregate + Output: t1_1.x, sum(t2_1.y), count(*) + Group Key: t1_1.x + -> Hash Join + Output: t1_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + Hash Cond: (t1_1.x = t2_1.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t1_1 + Output: t1_1.x + -> Hash + Output: t2_1.x, (PARTIAL sum(t2_1.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_1.x, PARTIAL sum(t2_1.y), PARTIAL count(*) + Group Key: t2_1.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s1 t2_1 + Output: t2_1.y, t2_1.x + -> Finalize HashAggregate + Output: t1_2.x, sum(t2_2.y), count(*) + Group Key: t1_2.x + -> Hash Join + Output: t1_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + Hash Cond: (t1_2.x = t2_2.x) + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t1_2 + Output: t1_2.x + -> Hash + Output: t2_2.x, (PARTIAL sum(t2_2.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_2.x, PARTIAL sum(t2_2.y), PARTIAL count(*) + Group Key: t2_2.x + -> Seq Scan on public.eager_agg_tab_ml_p2_s2 t2_2 + Output: t2_2.y, t2_2.x + -> Finalize HashAggregate + Output: t1_3.x, sum(t2_3.y), count(*) + Group Key: t1_3.x + -> Hash Join + Output: t1_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + Hash Cond: (t1_3.x = t2_3.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t1_3 + Output: t1_3.x + -> Hash + Output: t2_3.x, (PARTIAL sum(t2_3.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_3.x, PARTIAL sum(t2_3.y), PARTIAL count(*) + Group Key: t2_3.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s1 t2_3 + Output: t2_3.y, t2_3.x + -> Finalize HashAggregate + Output: t1_4.x, sum(t2_4.y), count(*) + Group Key: t1_4.x + -> Hash Join + Output: t1_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + Hash Cond: (t1_4.x = t2_4.x) + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t1_4 + Output: t1_4.x + -> Hash + Output: t2_4.x, (PARTIAL sum(t2_4.y)), (PARTIAL count(*)) + -> Partial HashAggregate + Output: t2_4.x, PARTIAL sum(t2_4.y), PARTIAL count(*) + Group Key: t2_4.x + -> Seq Scan on public.eager_agg_tab_ml_p3_s2 t2_4 + Output: t2_4.y, t2_4.x +(79 rows) + +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + x | sum | count +----+-------+------- + 0 | 0 | 1089 + 1 | 1156 | 1156 + 2 | 2312 | 1156 + 3 | 3468 | 1156 + 4 | 4624 | 1156 + 5 | 5780 | 1156 + 6 | 6936 | 1156 + 7 | 8092 | 1156 + 8 | 9248 | 1156 + 9 | 10404 | 1156 + 10 | 11560 | 1156 + 11 | 11979 | 1089 + 12 | 13068 | 1089 + 13 | 14157 | 1089 + 14 | 15246 | 1089 + 15 | 16335 | 1089 + 16 | 17424 | 1089 + 17 | 18513 | 1089 + 18 | 19602 | 1089 + 19 | 20691 | 1089 + 20 | 21780 | 1089 + 21 | 22869 | 1089 + 22 | 23958 | 1089 + 23 | 25047 | 1089 + 24 | 26136 | 1089 + 25 | 27225 | 1089 + 26 | 28314 | 1089 + 27 | 29403 | 1089 + 28 | 30492 | 1089 + 29 | 31581 | 1089 +(30 rows) + +RESET geqo; +RESET geqo_threshold; +DROP TABLE eager_agg_tab_ml; diff --git a/src/test/regress/expected/encoding.out b/src/test/regress/expected/encoding.out new file mode 100644 index 0000000000000..2ecd255f182ae --- /dev/null +++ b/src/test/regress/expected/encoding.out @@ -0,0 +1,430 @@ +/* skip test if not UTF8 server encoding */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit +\endif +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_bytea_to_text(bytea) RETURNS text + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_text_to_bytea(text) RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_mblen_func(text, text, text, int) RETURNS int + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_text_to_wchars(text, text) RETURNS int[] + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_wchars_to_text(text, int[]) RETURNS text + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_valid_server_encoding(text) RETURNS boolean + AS :'regresslib' LANGUAGE C STRICT; +CREATE TABLE regress_encoding(good text, truncated text, with_nul text, truncated_with_nul text); +INSERT INTO regress_encoding +VALUES ('café', + 'caf' || test_bytea_to_text('\xc3'), + 'café' || test_bytea_to_text('\x00') || 'dcba', + 'caf' || test_bytea_to_text('\xc300') || 'dcba'); +SELECT good, truncated, with_nul FROM regress_encoding; + good | truncated | with_nul +------+-----------+---------- + café | caf | café +(1 row) + +SELECT length(good) FROM regress_encoding; + length +-------- + 4 +(1 row) + +SELECT substring(good, 3, 1) FROM regress_encoding; + substring +----------- + f +(1 row) + +SELECT substring(good, 4, 1) FROM regress_encoding; + substring +----------- + é +(1 row) + +SELECT regexp_replace(good, '^caf(.)$', '\1') FROM regress_encoding; + regexp_replace +---------------- + é +(1 row) + +SELECT reverse(good) FROM regress_encoding; + reverse +--------- + éfac +(1 row) + +-- invalid short mb character = error +SELECT length(truncated) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT substring(truncated, 1, 3) FROM regress_encoding; + substring +----------- + caf +(1 row) + +SELECT substring(truncated, 1, 4) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT reverse(truncated) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +-- invalid short mb character = silently dropped +SELECT regexp_replace(truncated, '^caf(.)$', '\1') FROM regress_encoding; + regexp_replace +---------------- + caf +(1 row) + +-- PostgreSQL doesn't allow strings to contain NUL. If a corrupted string +-- contains NUL at a character boundary position, some functions treat it as a +-- character while others treat it as a terminator, as implementation details. +-- NUL = terminator +SELECT length(with_nul) FROM regress_encoding; + length +-------- + 4 +(1 row) + +SELECT substring(with_nul, 3, 1) FROM regress_encoding; + substring +----------- + f +(1 row) + +SELECT substring(with_nul, 4, 1) FROM regress_encoding; + substring +----------- + é +(1 row) + +SELECT substring(with_nul, 5, 1) FROM regress_encoding; + substring +----------- + +(1 row) + +SELECT convert_to(substring(with_nul, 5, 1), 'UTF8') FROM regress_encoding; + convert_to +------------ + \x +(1 row) + +SELECT regexp_replace(with_nul, '^caf(.)$', '\1') FROM regress_encoding; + regexp_replace +---------------- + é +(1 row) + +-- NUL = character +SELECT with_nul, reverse(with_nul), reverse(reverse(with_nul)) FROM regress_encoding; + with_nul | reverse | reverse +----------+---------+--------- + café | abcd | café +(1 row) + +-- If a corrupted string contains NUL in the tail bytes of a multibyte +-- character (invalid in all encodings), it is considered part of the +-- character for length purposes. An error will only be raised in code paths +-- that convert or verify encodings. +SELECT length(truncated_with_nul) FROM regress_encoding; + length +-------- + 8 +(1 row) + +SELECT substring(truncated_with_nul, 3, 1) FROM regress_encoding; + substring +----------- + f +(1 row) + +SELECT substring(truncated_with_nul, 4, 1) FROM regress_encoding; + substring +----------- + +(1 row) + +SELECT convert_to(substring(truncated_with_nul, 4, 1), 'UTF8') FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 0x00 +SELECT substring(truncated_with_nul, 5, 1) FROM regress_encoding; + substring +----------- + d +(1 row) + +SELECT regexp_replace(truncated_with_nul, '^caf(.)dcba$', '\1') = test_bytea_to_text('\xc300') FROM regress_encoding; + ?column? +---------- + t +(1 row) + +SELECT reverse(truncated_with_nul) FROM regress_encoding; + reverse +--------- + abcd +(1 row) + +-- unbounded: sequence would overrun the string! +SELECT test_mblen_func('pg_mblen_unbounded', 'UTF8', truncated, 3) +FROM regress_encoding; + test_mblen_func +----------------- + 2 +(1 row) + +-- condition detected when using the length/range variants +SELECT test_mblen_func('pg_mblen_with_len', 'UTF8', truncated, 3) +FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT test_mblen_func('pg_mblen_range', 'UTF8', truncated, 3) +FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +-- unbounded: sequence would overrun the string, if the terminator were really +-- the end of it +SELECT test_mblen_func('pg_mblen_unbounded', 'UTF8', truncated_with_nul, 3) +FROM regress_encoding; + test_mblen_func +----------------- + 2 +(1 row) + +SELECT test_mblen_func('pg_encoding_mblen', 'GB18030', truncated_with_nul, 3) +FROM regress_encoding; + test_mblen_func +----------------- + 2 +(1 row) + +-- condition detected when using the cstr variants +SELECT test_mblen_func('pg_mblen_cstr', 'UTF8', truncated_with_nul, 3) +FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +DROP TABLE regress_encoding; +-- mb<->wchar conversions +CREATE FUNCTION test_encoding(encoding text, description text, input bytea) +RETURNS VOID LANGUAGE plpgsql AS +$$ +DECLARE + prefix text; + len int; + wchars int[]; + round_trip bytea; + result text; +BEGIN + prefix := rpad(encoding || ' ' || description || ':', 28); + + -- XXX could also test validation, length functions and include client + -- only encodings with these test cases + + IF test_valid_server_encoding(encoding) THEN + wchars := test_text_to_wchars(encoding, test_bytea_to_text(input)); + round_trip = test_text_to_bytea(test_wchars_to_text(encoding, wchars)); + if input = round_trip then + result := 'OK'; + elsif length(input) > length(round_trip) and round_trip = substr(input, 1, length(round_trip)) then + result := 'truncated'; + else + result := 'failed'; + end if; + RAISE NOTICE '% % -> % -> % = %', prefix, input, wchars, round_trip, result; + END IF; +END; +$$; +-- No validation is done on the encoding itself, just the length to avoid +-- overruns, so some of the byte sequences below are bogus. They cover +-- all code branches, server encodings only for now. +CREATE TABLE encoding_tests (encoding text, description text, input bytea); +INSERT INTO encoding_tests VALUES + -- LATIN1, other single-byte encodings + ('LATIN1', 'ASCII', 'a'), + ('LATIN1', 'extended', '\xe9'), + -- EUC_JP, EUC_JIS_2004, EUR_KR (for the purposes of wchar conversion): + -- 2 8e (CS2, not used by EUR_KR but arbitrarily considered to have EUC_JP length) + -- 3 8f (CS3, not used by EUR_KR but arbitrarily considered to have EUC_JP length) + -- 2 80..ff (CS1) + ('EUC_JP', 'ASCII', 'a'), + ('EUC_JP', 'CS1, short', '\x80'), + ('EUC_JP', 'CS1', '\x8002'), + ('EUC_JP', 'CS2, short', '\x8e'), + ('EUC_JP', 'CS2', '\x8e02'), + ('EUC_JP', 'CS3, short', '\x8f'), + ('EUC_JP', 'CS3, short', '\x8f02'), + ('EUC_JP', 'CS3', '\x8f0203'), + -- EUC_CN + -- 3 8e (CS2, not used but arbitrarily considered to have length 3) + -- 3 8f (CS3, not used but arbitrarily considered to have length 3) + -- 2 80..ff (CS1) + ('EUC_CN', 'ASCII', 'a'), + ('EUC_CN', 'CS1, short', '\x80'), + ('EUC_CN', 'CS1', '\x8002'), + ('EUC_CN', 'CS2, short', '\x8e'), + ('EUC_CN', 'CS2, short', '\x8e02'), + ('EUC_CN', 'CS2', '\x8e0203'), + ('EUC_CN', 'CS3, short', '\x8f'), + ('EUC_CN', 'CS3, short', '\x8f02'), + ('EUC_CN', 'CS3', '\x8f0203'), + -- EUC_TW: + -- 4 8e (CS2) + -- 3 8f (CS3, not used but arbitrarily considered to have length 3) + -- 2 80..ff (CS1) + ('EUC_TW', 'ASCII', 'a'), + ('EUC_TW', 'CS1, short', '\x80'), + ('EUC_TW', 'CS1', '\x8002'), + ('EUC_TW', 'CS2, short', '\x8e'), + ('EUC_TW', 'CS2, short', '\x8e02'), + ('EUC_TW', 'CS2, short', '\x8e0203'), + ('EUC_TW', 'CS2', '\x8e020304'), + ('EUC_TW', 'CS3, short', '\x8f'), + ('EUC_TW', 'CS3, short', '\x8f02'), + ('EUC_TW', 'CS3', '\x8f0203'), + -- UTF8 + -- 2 c0..df + -- 3 e0..ef + -- 4 f0..f7 (but maximum real codepoint U+10ffff has f4) + -- 5 f8..fb (not supported) + -- 6 fc..fd (not supported) + ('UTF8', 'ASCII', 'a'), + ('UTF8', '2 byte, short', '\xdf'), + ('UTF8', '2 byte', '\xdf82'), + ('UTF8', '3 byte, short', '\xef'), + ('UTF8', '3 byte, short', '\xef82'), + ('UTF8', '3 byte', '\xef8283'), + ('UTF8', '4 byte, short', '\xf7'), + ('UTF8', '4 byte, short', '\xf782'), + ('UTF8', '4 byte, short', '\xf78283'), + ('UTF8', '4 byte', '\xf7828384'), + ('UTF8', '5 byte, unsupported', '\xfb'), + ('UTF8', '5 byte, unsupported', '\xfb82'), + ('UTF8', '5 byte, unsupported', '\xfb8283'), + ('UTF8', '5 byte, unsupported', '\xfb828384'), + ('UTF8', '5 byte, unsupported', '\xfb82838485'), + ('UTF8', '6 byte, unsupported', '\xfd'), + ('UTF8', '6 byte, unsupported', '\xfd82'), + ('UTF8', '6 byte, unsupported', '\xfd8283'), + ('UTF8', '6 byte, unsupported', '\xfd828384'), + ('UTF8', '6 byte, unsupported', '\xfd82838485'), + ('UTF8', '6 byte, unsupported', '\xfd8283848586'); +SELECT COUNT(test_encoding(encoding, description, input)) > 0 +FROM encoding_tests; +NOTICE: LATIN1 ASCII: \x61 -> {97} -> \x61 = OK +NOTICE: LATIN1 extended: \xe9 -> {233} -> \xe9 = OK +NOTICE: EUC_JP ASCII: \x61 -> {97} -> \x61 = OK +NOTICE: EUC_JP CS1, short: \x80 -> {} -> \x = truncated +NOTICE: EUC_JP CS1: \x8002 -> {32770} -> \x8002 = OK +NOTICE: EUC_JP CS2, short: \x8e -> {} -> \x = truncated +NOTICE: EUC_JP CS2: \x8e02 -> {36354} -> \x8e02 = OK +NOTICE: EUC_JP CS3, short: \x8f -> {} -> \x = truncated +NOTICE: EUC_JP CS3, short: \x8f02 -> {} -> \x = truncated +NOTICE: EUC_JP CS3: \x8f0203 -> {9372163} -> \x8f0203 = OK +NOTICE: EUC_CN ASCII: \x61 -> {97} -> \x61 = OK +NOTICE: EUC_CN CS1, short: \x80 -> {} -> \x = truncated +NOTICE: EUC_CN CS1: \x8002 -> {32770} -> \x8002 = OK +NOTICE: EUC_CN CS2, short: \x8e -> {} -> \x = truncated +NOTICE: EUC_CN CS2, short: \x8e02 -> {} -> \x = truncated +NOTICE: EUC_CN CS2: \x8e0203 -> {9306627} -> \x8e0203 = OK +NOTICE: EUC_CN CS3, short: \x8f -> {} -> \x = truncated +NOTICE: EUC_CN CS3, short: \x8f02 -> {} -> \x = truncated +NOTICE: EUC_CN CS3: \x8f0203 -> {9372163} -> \x8f0203 = OK +NOTICE: EUC_TW ASCII: \x61 -> {97} -> \x61 = OK +NOTICE: EUC_TW CS1, short: \x80 -> {} -> \x = truncated +NOTICE: EUC_TW CS1: \x8002 -> {32770} -> \x8002 = OK +NOTICE: EUC_TW CS2, short: \x8e -> {} -> \x = truncated +NOTICE: EUC_TW CS2, short: \x8e02 -> {} -> \x = truncated +NOTICE: EUC_TW CS2, short: \x8e0203 -> {} -> \x = truncated +NOTICE: EUC_TW CS2: \x8e020304 -> {-1912470780} -> \x8e020304 = OK +NOTICE: EUC_TW CS3, short: \x8f -> {} -> \x = truncated +NOTICE: EUC_TW CS3, short: \x8f02 -> {} -> \x = truncated +NOTICE: EUC_TW CS3: \x8f0203 -> {9372163} -> \x8f0203 = OK +NOTICE: UTF8 ASCII: \x61 -> {97} -> \x61 = OK +NOTICE: UTF8 2 byte, short: \xdf -> {} -> \x = truncated +NOTICE: UTF8 2 byte: \xdf82 -> {1986} -> \xdf82 = OK +NOTICE: UTF8 3 byte, short: \xef -> {} -> \x = truncated +NOTICE: UTF8 3 byte, short: \xef82 -> {} -> \x = truncated +NOTICE: UTF8 3 byte: \xef8283 -> {61571} -> \xef8283 = OK +NOTICE: UTF8 4 byte, short: \xf7 -> {} -> \x = truncated +NOTICE: UTF8 4 byte, short: \xf782 -> {} -> \x = truncated +NOTICE: UTF8 4 byte, short: \xf78283 -> {} -> \x = truncated +NOTICE: UTF8 4 byte: \xf7828384 -> {1843396} -> \xf7828384 = OK +NOTICE: UTF8 5 byte, unsupported: \xfb -> {251} -> \xc3bb = failed +NOTICE: UTF8 5 byte, unsupported: \xfb82 -> {251,130} -> \xc3bbc282 = failed +NOTICE: UTF8 5 byte, unsupported: \xfb8283 -> {251,130,131} -> \xc3bbc282c283 = failed +NOTICE: UTF8 5 byte, unsupported: \xfb828384 -> {251,130,131,132} -> \xc3bbc282c283c284 = failed +NOTICE: UTF8 5 byte, unsupported: \xfb82838485 -> {251,130,131,132,133} -> \xc3bbc282c283c284c285 = failed +NOTICE: UTF8 6 byte, unsupported: \xfd -> {253} -> \xc3bd = failed +NOTICE: UTF8 6 byte, unsupported: \xfd82 -> {253,130} -> \xc3bdc282 = failed +NOTICE: UTF8 6 byte, unsupported: \xfd8283 -> {253,130,131} -> \xc3bdc282c283 = failed +NOTICE: UTF8 6 byte, unsupported: \xfd828384 -> {253,130,131,132} -> \xc3bdc282c283c284 = failed +NOTICE: UTF8 6 byte, unsupported: \xfd82838485 -> {253,130,131,132,133} -> \xc3bdc282c283c284c285 = failed +NOTICE: UTF8 6 byte, unsupported: \xfd8283848586 -> {253,130,131,132,133,134} -> \xc3bdc282c283c284c285c286 = failed + ?column? +---------- + t +(1 row) + +-- substring fetches a slice of a toasted value; unused tail of that slice is +-- an incomplete char (bug #19406) +CREATE TABLE toast_3b_utf8 (c text); +INSERT INTO toast_3b_utf8 VALUES (repeat(U&'\2026', 4000)); +SELECT SUBSTRING(c FROM 1 FOR 1) FROM toast_3b_utf8; + substring +----------- + … +(1 row) + +SELECT SUBSTRING(c FROM 4001 FOR 1) FROM toast_3b_utf8; + substring +----------- + +(1 row) + +-- diagnose incomplete char iff within the substring +UPDATE toast_3b_utf8 SET c = c || test_bytea_to_text('\xe280'); +SELECT SUBSTRING(c FROM 4000 FOR 1) FROM toast_3b_utf8; + substring +----------- + … +(1 row) + +SELECT SUBSTRING(c FROM 4001 FOR 1) FROM toast_3b_utf8; +ERROR: invalid byte sequence for encoding "UTF8": 0xe2 0x80 +-- substring needing last byte of its slice_size +ALTER TABLE toast_3b_utf8 RENAME TO toast_4b_utf8; +UPDATE toast_4b_utf8 SET c = repeat(U&'\+01F680', 3000); +SELECT SUBSTRING(c FROM 3000 FOR 1) FROM toast_4b_utf8; + substring +----------- + 🚀 +(1 row) + +DROP TABLE encoding_tests; +DROP TABLE toast_4b_utf8; +DROP FUNCTION test_encoding; +DROP FUNCTION test_wchars_to_text; +DROP FUNCTION test_text_to_wchars; +DROP FUNCTION test_valid_server_encoding; +DROP FUNCTION test_mblen_func; +DROP FUNCTION test_bytea_to_text; +DROP FUNCTION test_text_to_bytea; +-- substring slow path: multi-byte escape char vs. multi-byte pattern char. +SELECT SUBSTRING('a' SIMILAR U&'\00AC' ESCAPE U&'\00A7'); + substring +----------- + +(1 row) + +-- Levenshtein distance metric: exercise character length cache. +SELECT U&"real\00A7_name" FROM (select 1) AS x(real_name); +ERROR: column "real§_name" does not exist +LINE 1: SELECT U&"real\00A7_name" FROM (select 1) AS x(real_name); + ^ +HINT: Perhaps you meant to reference the column "x.real_name". +-- JSON errcontext: truncate long data. +SELECT repeat(U&'\00A7', 30)::json; +ERROR: invalid input syntax for type json +DETAIL: Token "§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§" is invalid. +CONTEXT: JSON data, line 1: ...§§§§§§§§§§§§§§§§§§§§§§§§ diff --git a/src/test/regress/expected/encoding_1.out b/src/test/regress/expected/encoding_1.out new file mode 100644 index 0000000000000..a5b02090901b3 --- /dev/null +++ b/src/test/regress/expected/encoding_1.out @@ -0,0 +1,4 @@ +/* skip test if not UTF8 server encoding */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit diff --git a/src/test/regress/expected/enum.out b/src/test/regress/expected/enum.out index 4d9f36d0d3677..990ce66c7bb17 100644 --- a/src/test/regress/expected/enum.out +++ b/src/test/regress/expected/enum.out @@ -52,6 +52,9 @@ hint | sql_error_code | 22P02 \x +-- check for duplicate enum entries +CREATE TYPE dup_enum AS ENUM ('foo','bar','foo'); +ERROR: enum label "foo" used more than once -- -- adding new values -- diff --git a/src/test/regress/expected/euc_kr.out b/src/test/regress/expected/euc_kr.out new file mode 100644 index 0000000000000..7a61c89a43ad9 --- /dev/null +++ b/src/test/regress/expected/euc_kr.out @@ -0,0 +1,16 @@ +-- This test is about EUC_KR encoding, chosen as perhaps the most prevalent +-- non-UTF8, multibyte encoding as of 2026-01. Since UTF8 can represent all +-- of EUC_KR, also run the test in UTF8. +SELECT getdatabaseencoding() NOT IN ('EUC_KR', 'UTF8') AS skip_test \gset +\if :skip_test +\quit +\endif +-- Exercise is_multibyte_char_in_char (non-UTF8) slow path. +SELECT POSITION( + convert_from('\xbcf6c7d0', 'EUC_KR') IN + convert_from('\xb0fac7d02c20bcf6c7d02c20b1e2bcfa2c20bbee', 'EUC_KR')); + position +---------- + 5 +(1 row) + diff --git a/src/test/regress/expected/euc_kr_1.out b/src/test/regress/expected/euc_kr_1.out new file mode 100644 index 0000000000000..faaac5d635524 --- /dev/null +++ b/src/test/regress/expected/euc_kr_1.out @@ -0,0 +1,6 @@ +-- This test is about EUC_KR encoding, chosen as perhaps the most prevalent +-- non-UTF8, multibyte encoding as of 2026-01. Since UTF8 can represent all +-- of EUC_KR, also run the test in UTF8. +SELECT getdatabaseencoding() NOT IN ('EUC_KR', 'UTF8') AS skip_test \gset +\if :skip_test +\quit diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out index 7b2198eac6f20..065f586310ff6 100644 --- a/src/test/regress/expected/event_trigger.out +++ b/src/test/regress/expected/event_trigger.out @@ -228,9 +228,15 @@ INSERT INTO undroppable_objs VALUES ('table', 'schema_one.table_three'), ('table', 'audit_tbls.schema_two_table_three'); CREATE TABLE dropped_objects ( - type text, - schema text, - object text + object_type text, + schema_name text, + object_name text, + object_identity text, + address_names text[], + address_args text[], + is_temporary bool, + original bool, + normal bool ); -- This tests errors raised within event triggers; the one in audit_tbls -- uses 2nd-level recursive invocation via test_evtrig_dropped_objects(). @@ -268,8 +274,12 @@ BEGIN END IF; INSERT INTO dropped_objects - (type, schema, object) VALUES - (obj.object_type, obj.schema_name, obj.object_identity); + (object_type, schema_name, object_name, + object_identity, address_names, address_args, + is_temporary, original, normal) VALUES + (obj.object_type, obj.schema_name, obj.object_name, + obj.object_identity, obj.address_names, obj.address_args, + obj.is_temporary, obj.original, obj.normal); END LOOP; END $$; @@ -325,42 +335,44 @@ NOTICE: table "audit_tbls_schema_two_table_three" does not exist, skipping NOTICE: table "schema_one_table_one" does not exist, skipping NOTICE: table "schema_one_table two" does not exist, skipping NOTICE: table "schema_one_table_three" does not exist, skipping -SELECT * FROM dropped_objects WHERE schema IS NULL OR schema <> 'pg_toast'; - type | schema | object ---------------+------------+------------------------------------- - table column | schema_one | schema_one.table_one.a - schema | | schema_two - table | schema_two | schema_two.table_two - type | schema_two | schema_two.table_two - type | schema_two | schema_two.table_two[] - table | audit_tbls | audit_tbls.schema_two_table_three - type | audit_tbls | audit_tbls.schema_two_table_three - type | audit_tbls | audit_tbls.schema_two_table_three[] - table | schema_two | schema_two.table_three - type | schema_two | schema_two.table_three - type | schema_two | schema_two.table_three[] - function | schema_two | schema_two.add(integer,integer) - aggregate | schema_two | schema_two.newton(integer) - schema | | schema_one - table | schema_one | schema_one.table_one - type | schema_one | schema_one.table_one - type | schema_one | schema_one.table_one[] - table | schema_one | schema_one."table two" - type | schema_one | schema_one."table two" - type | schema_one | schema_one."table two"[] - table | schema_one | schema_one.table_three - type | schema_one | schema_one.table_three - type | schema_one | schema_one.table_three[] +-- exclude TOAST objects because they have unstable names +SELECT * FROM dropped_objects + WHERE schema_name IS NULL OR schema_name <> 'pg_toast'; + object_type | schema_name | object_name | object_identity | address_names | address_args | is_temporary | original | normal +--------------+-------------+-------------------------+-------------------------------------+---------------------------------------+-------------------+--------------+----------+-------- + table column | schema_one | | schema_one.table_one.a | {schema_one,table_one,a} | {} | f | t | f + schema | | schema_two | schema_two | {schema_two} | {} | f | t | f + table | schema_two | table_two | schema_two.table_two | {schema_two,table_two} | {} | f | f | t + type | schema_two | table_two | schema_two.table_two | {schema_two.table_two} | {} | f | f | f + type | schema_two | _table_two | schema_two.table_two[] | {schema_two.table_two[]} | {} | f | f | f + table | audit_tbls | schema_two_table_three | audit_tbls.schema_two_table_three | {audit_tbls,schema_two_table_three} | {} | f | t | f + type | audit_tbls | schema_two_table_three | audit_tbls.schema_two_table_three | {audit_tbls.schema_two_table_three} | {} | f | f | f + type | audit_tbls | _schema_two_table_three | audit_tbls.schema_two_table_three[] | {audit_tbls.schema_two_table_three[]} | {} | f | f | f + table | schema_two | table_three | schema_two.table_three | {schema_two,table_three} | {} | f | f | t + type | schema_two | table_three | schema_two.table_three | {schema_two.table_three} | {} | f | f | f + type | schema_two | _table_three | schema_two.table_three[] | {schema_two.table_three[]} | {} | f | f | f + function | schema_two | | schema_two.add(integer,integer) | {schema_two,add} | {integer,integer} | f | f | t + aggregate | schema_two | | schema_two.newton(integer) | {schema_two,newton} | {integer} | f | f | t + schema | | schema_one | schema_one | {schema_one} | {} | f | t | f + table | schema_one | table_one | schema_one.table_one | {schema_one,table_one} | {} | f | f | t + type | schema_one | table_one | schema_one.table_one | {schema_one.table_one} | {} | f | f | f + type | schema_one | _table_one | schema_one.table_one[] | {schema_one.table_one[]} | {} | f | f | f + table | schema_one | table two | schema_one."table two" | {schema_one,"table two"} | {} | f | f | t + type | schema_one | table two | schema_one."table two" | {"schema_one.\"table two\""} | {} | f | f | f + type | schema_one | _table two | schema_one."table two"[] | {"schema_one.\"table two\"[]"} | {} | f | f | f + table | schema_one | table_three | schema_one.table_three | {schema_one,table_three} | {} | f | f | t + type | schema_one | table_three | schema_one.table_three | {schema_one.table_three} | {} | f | f | f + type | schema_one | _table_three | schema_one.table_three[] | {schema_one.table_three[]} | {} | f | f | f (23 rows) DROP OWNED BY regress_evt_user; NOTICE: schema "audit_tbls" does not exist, skipping -SELECT * FROM dropped_objects WHERE type = 'schema'; - type | schema | object ---------+--------+------------ - schema | | schema_two - schema | | schema_one - schema | | audit_tbls +SELECT * FROM dropped_objects WHERE object_type = 'schema'; + object_type | schema_name | object_name | object_identity | address_names | address_args | is_temporary | original | normal +-------------+-------------+-------------+-----------------+---------------+--------------+--------------+----------+-------- + schema | | schema_two | schema_two | {schema_two} | {} | f | t | f + schema | | schema_one | schema_one | {schema_one} | {} | f | t | f + schema | | audit_tbls | audit_tbls | {audit_tbls} | {} | f | t | f (3 rows) DROP ROLE regress_evt_user; @@ -378,9 +390,10 @@ BEGIN IF NOT r.normal AND NOT r.original THEN CONTINUE; END IF; - RAISE NOTICE 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + RAISE NOTICE 'NORMAL: orig=% normal=% istemp=% type=% identity=% schema=% name=% addr=% args=%', r.original, r.normal, r.is_temporary, r.object_type, - r.object_identity, r.address_names, r.address_args; + r.object_identity, r.schema_name, r.object_name, + r.address_names, r.address_args; END LOOP; END; $$; CREATE EVENT TRIGGER regress_event_trigger_report_dropped ON sql_drop @@ -403,7 +416,8 @@ CREATE SCHEMA evttrig CREATE TABLE one (col_a SERIAL PRIMARY KEY, col_b text DEFAULT 'forty two', col_c SERIAL) CREATE INDEX one_idx ON one (col_b) CREATE TABLE two (col_c INTEGER CHECK (col_c > 0) REFERENCES one DEFAULT 42) - CREATE TABLE id (col_d int NOT NULL GENERATED ALWAYS AS IDENTITY); + CREATE TABLE id (col_d int NOT NULL GENERATED ALWAYS AS IDENTITY) + CREATE VIEW one_view AS SELECT * FROM two; NOTICE: END: command_tag=CREATE SCHEMA type=schema identity=evttrig NOTICE: END: command_tag=CREATE SEQUENCE type=sequence identity=evttrig.one_col_a_seq NOTICE: END: command_tag=CREATE SEQUENCE type=sequence identity=evttrig.one_col_c_seq @@ -411,12 +425,19 @@ NOTICE: END: command_tag=CREATE TABLE type=table identity=evttrig.one NOTICE: END: command_tag=CREATE INDEX type=index identity=evttrig.one_pkey NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.one_col_a_seq NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.one_col_c_seq +NOTICE: END: command_tag=CREATE INDEX type=index identity=evttrig.one_idx NOTICE: END: command_tag=CREATE TABLE type=table identity=evttrig.two -NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.two NOTICE: END: command_tag=CREATE SEQUENCE type=sequence identity=evttrig.id_col_d_seq NOTICE: END: command_tag=CREATE TABLE type=table identity=evttrig.id NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.id_col_d_seq -NOTICE: END: command_tag=CREATE INDEX type=index identity=evttrig.one_idx +NOTICE: END: command_tag=CREATE VIEW type=view identity=evttrig.one_view +NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.two +-- View with column additions +CREATE OR REPLACE VIEW evttrig.one_view AS SELECT * FROM evttrig.two, evttrig.id; +NOTICE: END: command_tag=CREATE VIEW type=view identity=evttrig.one_view +NOTICE: END: command_tag=CREATE VIEW type=view identity=evttrig.one_view +DROP VIEW evttrig.one_view; +NOTICE: NORMAL: orig=t normal=f istemp=f type=view identity=evttrig.one_view schema=evttrig name=one_view addr={evttrig,one_view} args={} -- Partitioned tables with a partitioned index CREATE TABLE evttrig.parted ( id int PRIMARY KEY) @@ -436,18 +457,18 @@ CREATE TABLE evttrig.part_15_20 PARTITION OF evttrig.part_10_20 (id) FOR VALUES FROM (15) TO (20); NOTICE: END: command_tag=CREATE TABLE type=table identity=evttrig.part_15_20 ALTER TABLE evttrig.two DROP COLUMN col_c; -NOTICE: NORMAL: orig=t normal=f istemp=f type=table column identity=evttrig.two.col_c name={evttrig,two,col_c} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table constraint identity=two_col_c_check on evttrig.two name={evttrig,two,two_col_c_check} args={} +NOTICE: NORMAL: orig=t normal=f istemp=f type=table column identity=evttrig.two.col_c schema=evttrig name= addr={evttrig,two,col_c} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table constraint identity=two_col_c_check on evttrig.two schema=evttrig name= addr={evttrig,two,two_col_c_check} args={} NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.two ALTER TABLE evttrig.one ALTER COLUMN col_b DROP DEFAULT; -NOTICE: NORMAL: orig=t normal=f istemp=f type=default value identity=for evttrig.one.col_b name={evttrig,one,col_b} args={} +NOTICE: NORMAL: orig=t normal=f istemp=f type=default value identity=for evttrig.one.col_b schema=evttrig name= addr={evttrig,one,col_b} args={} NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.one ALTER TABLE evttrig.one DROP CONSTRAINT one_pkey; -NOTICE: NORMAL: orig=t normal=f istemp=f type=table constraint identity=one_pkey on evttrig.one name={evttrig,one,one_pkey} args={} +NOTICE: NORMAL: orig=t normal=f istemp=f type=table constraint identity=one_pkey on evttrig.one schema=evttrig name= addr={evttrig,one,one_pkey} args={} NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.one ALTER TABLE evttrig.one DROP COLUMN col_c; -NOTICE: NORMAL: orig=t normal=f istemp=f type=table column identity=evttrig.one.col_c name={evttrig,one,col_c} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=default value identity=for evttrig.one.col_c name={evttrig,one,col_c} args={} +NOTICE: NORMAL: orig=t normal=f istemp=f type=table column identity=evttrig.one.col_c schema=evttrig name= addr={evttrig,one,col_c} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=default value identity=for evttrig.one.col_c schema=evttrig name= addr={evttrig,one,col_c} args={} NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.one ALTER TABLE evttrig.id ALTER COLUMN col_d SET DATA TYPE bigint; NOTICE: END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.id_col_d_seq @@ -456,26 +477,90 @@ ALTER TABLE evttrig.id ALTER COLUMN col_d DROP IDENTITY, ALTER COLUMN col_d SET DATA TYPE int; NOTICE: END: command_tag=ALTER TABLE type=table identity=evttrig.id DROP INDEX evttrig.one_idx; -NOTICE: NORMAL: orig=t normal=f istemp=f type=index identity=evttrig.one_idx name={evttrig,one_idx} args={} +NOTICE: NORMAL: orig=t normal=f istemp=f type=index identity=evttrig.one_idx schema=evttrig name=one_idx addr={evttrig,one_idx} args={} DROP SCHEMA evttrig CASCADE; NOTICE: drop cascades to 4 other objects DETAIL: drop cascades to table evttrig.one drop cascades to table evttrig.two drop cascades to table evttrig.id drop cascades to table evttrig.parted -NOTICE: NORMAL: orig=t normal=f istemp=f type=schema identity=evttrig name={evttrig} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.one name={evttrig,one} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=sequence identity=evttrig.one_col_a_seq name={evttrig,one_col_a_seq} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=default value identity=for evttrig.one.col_a name={evttrig,one,col_a} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.two name={evttrig,two} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.id name={evttrig,id} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.parted name={evttrig,parted} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_1_10 name={evttrig,part_1_10} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_10_20 name={evttrig,part_10_20} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_10_15 name={evttrig,part_10_15} args={} -NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_15_20 name={evttrig,part_15_20} args={} +NOTICE: NORMAL: orig=t normal=f istemp=f type=schema identity=evttrig schema= name=evttrig addr={evttrig} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.one schema=evttrig name=one addr={evttrig,one} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=sequence identity=evttrig.one_col_a_seq schema=evttrig name=one_col_a_seq addr={evttrig,one_col_a_seq} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=default value identity=for evttrig.one.col_a schema=evttrig name= addr={evttrig,one,col_a} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.two schema=evttrig name=two addr={evttrig,two} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.id schema=evttrig name=id addr={evttrig,id} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.parted schema=evttrig name=parted addr={evttrig,parted} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_1_10 schema=evttrig name=part_1_10 addr={evttrig,part_1_10} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_10_20 schema=evttrig name=part_10_20 addr={evttrig,part_10_20} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_10_15 schema=evttrig name=part_10_15 addr={evttrig,part_10_15} args={} +NOTICE: NORMAL: orig=f normal=t istemp=f type=table identity=evttrig.part_15_20 schema=evttrig name=part_15_20 addr={evttrig,part_15_20} args={} +DROP TABLE a_temp_tbl; +NOTICE: NORMAL: orig=t normal=f istemp=t type=table identity=pg_temp.a_temp_tbl schema=pg_temp name=a_temp_tbl addr={pg_temp,a_temp_tbl} args={} +-- check unfiltered results, too +CREATE OR REPLACE FUNCTION event_trigger_report_dropped() + RETURNS event_trigger + LANGUAGE plpgsql +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + RAISE NOTICE 'DROP: orig=% normal=% istemp=% type=% identity=% schema=% name=% addr=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.schema_name, r.object_name, + r.address_names, r.address_args; + END LOOP; +END; $$; +NOTICE: END: command_tag=CREATE FUNCTION type=function identity=public.event_trigger_report_dropped() +CREATE FUNCTION event_trigger_dummy_trigger() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + RETURN new; +END; $$; +NOTICE: END: command_tag=CREATE FUNCTION type=function identity=public.event_trigger_dummy_trigger() +CREATE TABLE evtrg_nontemp_table (f1 int primary key, f2 int default 42); +NOTICE: END: command_tag=CREATE TABLE type=table identity=public.evtrg_nontemp_table +NOTICE: END: command_tag=CREATE INDEX type=index identity=public.evtrg_nontemp_table_pkey +CREATE TRIGGER evtrg_nontemp_trig + BEFORE INSERT ON evtrg_nontemp_table + EXECUTE FUNCTION event_trigger_dummy_trigger(); +NOTICE: END: command_tag=CREATE TRIGGER type=trigger identity=evtrg_nontemp_trig on public.evtrg_nontemp_table +CREATE POLICY evtrg_nontemp_pol ON evtrg_nontemp_table USING (f2 > 0); +NOTICE: END: command_tag=CREATE POLICY type=policy identity=evtrg_nontemp_pol on public.evtrg_nontemp_table +DROP TABLE evtrg_nontemp_table; +NOTICE: DROP: orig=t normal=f istemp=f type=table identity=public.evtrg_nontemp_table schema=public name=evtrg_nontemp_table addr={public,evtrg_nontemp_table} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=type identity=public.evtrg_nontemp_table schema=public name=evtrg_nontemp_table addr={public.evtrg_nontemp_table} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=type identity=public.evtrg_nontemp_table[] schema=public name=_evtrg_nontemp_table addr={public.evtrg_nontemp_table[]} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=default value identity=for public.evtrg_nontemp_table.f2 schema=public name= addr={public,evtrg_nontemp_table,f2} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=table constraint identity=evtrg_nontemp_table_f1_not_null on public.evtrg_nontemp_table schema=public name= addr={public,evtrg_nontemp_table,evtrg_nontemp_table_f1_not_null} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=table constraint identity=evtrg_nontemp_table_pkey on public.evtrg_nontemp_table schema=public name= addr={public,evtrg_nontemp_table,evtrg_nontemp_table_pkey} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=index identity=public.evtrg_nontemp_table_pkey schema=public name=evtrg_nontemp_table_pkey addr={public,evtrg_nontemp_table_pkey} args={} +NOTICE: DROP: orig=f normal=f istemp=f type=trigger identity=evtrg_nontemp_trig on public.evtrg_nontemp_table schema=public name= addr={public,evtrg_nontemp_table,evtrg_nontemp_trig} args={} +NOTICE: DROP: orig=f normal=t istemp=f type=policy identity=evtrg_nontemp_pol on public.evtrg_nontemp_table schema=public name= addr={public,evtrg_nontemp_table,evtrg_nontemp_pol} args={} +CREATE TEMP TABLE a_temp_tbl (f1 int primary key, f2 int default 42); +NOTICE: END: command_tag=CREATE TABLE type=table identity=pg_temp.a_temp_tbl +NOTICE: END: command_tag=CREATE INDEX type=index identity=pg_temp.a_temp_tbl_pkey +CREATE TRIGGER a_temp_trig + BEFORE INSERT ON a_temp_tbl + EXECUTE FUNCTION event_trigger_dummy_trigger(); +NOTICE: END: command_tag=CREATE TRIGGER type=trigger identity=a_temp_trig on pg_temp.a_temp_tbl +CREATE POLICY a_temp_pol ON a_temp_tbl USING (f2 > 0); +NOTICE: END: command_tag=CREATE POLICY type=policy identity=a_temp_pol on pg_temp.a_temp_tbl DROP TABLE a_temp_tbl; -NOTICE: NORMAL: orig=t normal=f istemp=t type=table identity=pg_temp.a_temp_tbl name={pg_temp,a_temp_tbl} args={} +NOTICE: DROP: orig=t normal=f istemp=t type=table identity=pg_temp.a_temp_tbl schema=pg_temp name=a_temp_tbl addr={pg_temp,a_temp_tbl} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=type identity=pg_temp.a_temp_tbl schema=pg_temp name=a_temp_tbl addr={pg_temp.a_temp_tbl} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=type identity=pg_temp.a_temp_tbl[] schema=pg_temp name=_a_temp_tbl addr={pg_temp.a_temp_tbl[]} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=default value identity=for pg_temp.a_temp_tbl.f2 schema=pg_temp name= addr={pg_temp,a_temp_tbl,f2} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=table constraint identity=a_temp_tbl_f1_not_null on pg_temp.a_temp_tbl schema=pg_temp name= addr={pg_temp,a_temp_tbl,a_temp_tbl_f1_not_null} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=table constraint identity=a_temp_tbl_pkey on pg_temp.a_temp_tbl schema=pg_temp name= addr={pg_temp,a_temp_tbl,a_temp_tbl_pkey} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=index identity=pg_temp.a_temp_tbl_pkey schema=pg_temp name=a_temp_tbl_pkey addr={pg_temp,a_temp_tbl_pkey} args={} +NOTICE: DROP: orig=f normal=f istemp=t type=trigger identity=a_temp_trig on pg_temp.a_temp_tbl schema=pg_temp name= addr={pg_temp,a_temp_tbl,a_temp_trig} args={} +NOTICE: DROP: orig=f normal=t istemp=t type=policy identity=a_temp_pol on pg_temp.a_temp_tbl schema=pg_temp name= addr={pg_temp,a_temp_tbl,a_temp_pol} args={} +DROP FUNCTION event_trigger_dummy_trigger(); +NOTICE: DROP: orig=t normal=f istemp=f type=function identity=public.event_trigger_dummy_trigger() schema=public name= addr={public,event_trigger_dummy_trigger} args={} -- CREATE OPERATOR CLASS without FAMILY clause should report -- both CREATE OPERATOR FAMILY and CREATE OPERATOR CLASS CREATE OPERATOR CLASS evttrigopclass FOR TYPE int USING btree AS STORAGE int; diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out index c53bf9c8aa3cc..74a4d87801e69 100644 --- a/src/test/regress/expected/explain.out +++ b/src/test/regress/expected/explain.out @@ -54,6 +54,14 @@ set jit = off; -- enabled. set track_io_timing = off; -- Simple cases +explain (costs off) select 1 as a, 2 as b having false; + QUERY PLAN +-------------------------- + Result + Replaces: Aggregate + One-Time Filter: false +(3 rows) + select explain_filter('explain select * from int8_tbl i8'); explain_filter --------------------------------------------------------- @@ -85,164 +93,174 @@ select explain_filter('explain (analyze, buffers, format text) select * from int Execution Time: N.N ms (3 rows) -select explain_filter('explain (analyze, buffers, format xml) select * from int8_tbl i8'); - explain_filter --------------------------------------------------------- - + - + - + - Seq Scan + - false + - false + - int8_tbl + - i8 + - N.N + - N.N + - N + - N + - N.N + - N.N + - N.N + - N + - false + - N + - N + - N+ - N+ - N + - N + - N + - N + - N + - N + - + - + - N + - N + - N+ - N+ - N + - N + - N + - N + - N + - N + - + - N.N + - + - + - N.N + - + - -(1 row) - -select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8'); - explain_filter -------------------------------- - - Plan: + - Node Type: "Seq Scan" + - Parallel Aware: false + - Async Capable: false + - Relation Name: "int8_tbl"+ - Alias: "i8" + - Startup Cost: N.N + - Total Cost: N.N + - Plan Rows: N + - Plan Width: N + - Actual Startup Time: N.N + - Actual Total Time: N.N + - Actual Rows: N.N + - Actual Loops: N + - Disabled: false + - Shared Hit Blocks: N + - Shared Read Blocks: N + - Shared Dirtied Blocks: N + - Shared Written Blocks: N + - Local Hit Blocks: N + - Local Read Blocks: N + - Local Dirtied Blocks: N + - Local Written Blocks: N + - Temp Read Blocks: N + - Temp Written Blocks: N + - Planning: + - Shared Hit Blocks: N + - Shared Read Blocks: N + - Shared Dirtied Blocks: N + - Shared Written Blocks: N + - Local Hit Blocks: N + - Local Read Blocks: N + - Local Dirtied Blocks: N + - Local Written Blocks: N + - Temp Read Blocks: N + - Temp Written Blocks: N + - Planning Time: N.N + - Triggers: + - Serialization: + - Time: N.N + - Output Volume: N + - Format: "text" + - Shared Hit Blocks: N + - Shared Read Blocks: N + - Shared Dirtied Blocks: N + - Shared Written Blocks: N + - Local Hit Blocks: N + - Local Read Blocks: N + - Local Dirtied Blocks: N + - Local Written Blocks: N + - Temp Read Blocks: N + - Temp Written Blocks: N + - Execution Time: N.N -(1 row) - select explain_filter('explain (buffers, format text) select * from int8_tbl i8'); explain_filter --------------------------------------------------------- Seq Scan on int8_tbl i8 (cost=N.N..N.N rows=N width=N) (1 row) +\a +select explain_filter('explain (analyze, buffers, io, format xml) select * from int8_tbl i8'); +explain_filter + + + + Seq Scan + false + false + int8_tbl + i8 + N.N + N.N + N + N + N.N + N.N + N.N + N + false + N.N + N + N + N + N + N.N + N.N + N + N + N + N + N + N + N + N + N + N + + + N + N + N + N + N + N + N + N + N + N + + N.N + + + N.N + + +(1 row) +select explain_filter('explain (analyze, serialize, buffers, io, format yaml) select * from int8_tbl i8'); +explain_filter +- Plan: + Node Type: "Seq Scan" + Parallel Aware: false + Async Capable: false + Relation Name: "int8_tbl" + Alias: "i8" + Startup Cost: N.N + Total Cost: N.N + Plan Rows: N + Plan Width: N + Actual Startup Time: N.N + Actual Total Time: N.N + Actual Rows: N.N + Actual Loops: N + Disabled: false + Average Prefetch Distance: N.N + Max Prefetch Distance: N + Prefetch Capacity: N + I/O Count: N + I/O Waits: N + Average I/O Size: N.N + Average I/Os In Progress: N.N + Shared Hit Blocks: N + Shared Read Blocks: N + Shared Dirtied Blocks: N + Shared Written Blocks: N + Local Hit Blocks: N + Local Read Blocks: N + Local Dirtied Blocks: N + Local Written Blocks: N + Temp Read Blocks: N + Temp Written Blocks: N + Planning: + Shared Hit Blocks: N + Shared Read Blocks: N + Shared Dirtied Blocks: N + Shared Written Blocks: N + Local Hit Blocks: N + Local Read Blocks: N + Local Dirtied Blocks: N + Local Written Blocks: N + Temp Read Blocks: N + Temp Written Blocks: N + Planning Time: N.N + Triggers: + Serialization: + Time: N.N + Output Volume: N + Format: "text" + Shared Hit Blocks: N + Shared Read Blocks: N + Shared Dirtied Blocks: N + Shared Written Blocks: N + Local Hit Blocks: N + Local Read Blocks: N + Local Dirtied Blocks: N + Local Written Blocks: N + Temp Read Blocks: N + Temp Written Blocks: N + Execution Time: N.N +(1 row) select explain_filter('explain (buffers, format json) select * from int8_tbl i8'); - explain_filter ------------------------------------- - [ + - { + - "Plan": { + - "Node Type": "Seq Scan", + - "Parallel Aware": false, + - "Async Capable": false, + - "Relation Name": "int8_tbl",+ - "Alias": "i8", + - "Startup Cost": N.N, + - "Total Cost": N.N, + - "Plan Rows": N, + - "Plan Width": N, + - "Disabled": false, + - "Shared Hit Blocks": N, + - "Shared Read Blocks": N, + - "Shared Dirtied Blocks": N, + - "Shared Written Blocks": N, + - "Local Hit Blocks": N, + - "Local Read Blocks": N, + - "Local Dirtied Blocks": N, + - "Local Written Blocks": N, + - "Temp Read Blocks": N, + - "Temp Written Blocks": N + - }, + - "Planning": { + - "Shared Hit Blocks": N, + - "Shared Read Blocks": N, + - "Shared Dirtied Blocks": N, + - "Shared Written Blocks": N, + - "Local Hit Blocks": N, + - "Local Read Blocks": N, + - "Local Dirtied Blocks": N, + - "Local Written Blocks": N, + - "Temp Read Blocks": N, + - "Temp Written Blocks": N + - } + - } + - ] +explain_filter +[ + { + "Plan": { + "Node Type": "Seq Scan", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "int8_tbl", + "Alias": "i8", + "Startup Cost": N.N, + "Total Cost": N.N, + "Plan Rows": N, + "Plan Width": N, + "Disabled": false, + "Shared Hit Blocks": N, + "Shared Read Blocks": N, + "Shared Dirtied Blocks": N, + "Shared Written Blocks": N, + "Local Hit Blocks": N, + "Local Read Blocks": N, + "Local Dirtied Blocks": N, + "Local Written Blocks": N, + "Temp Read Blocks": N, + "Temp Written Blocks": N + }, + "Planning": { + "Shared Hit Blocks": N, + "Shared Read Blocks": N, + "Shared Dirtied Blocks": N, + "Shared Written Blocks": N, + "Local Hit Blocks": N, + "Local Read Blocks": N, + "Local Dirtied Blocks": N, + "Local Written Blocks": N, + "Temp Read Blocks": N, + "Temp Written Blocks": N + } + } +] (1 row) - +\a -- Check expansion of window definitions select explain_filter('explain verbose select sum(unique1) over w, sum(unique2) over (w order by hundred), sum(tenthous) over (w order by hundred) from tenk1 window w as (partition by ten)'); explain_filter diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out index 21c54fc1989c9..9a3c97b15a3ad 100644 --- a/src/test/regress/expected/expressions.out +++ b/src/test/regress/expected/expressions.out @@ -218,7 +218,8 @@ select '(0,0)'::point in ('(0,0,0,0)'::box, point(0,0)); ERROR: operator does not exist: point = box LINE 1: select '(0,0)'::point in ('(0,0,0,0)'::box, point(0,0)); ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- -- Tests for ScalarArrayOpExpr with a hashfn -- diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out index ccbcdf8403fa9..bd142ed481cbb 100644 --- a/src/test/regress/expected/fast_default.out +++ b/src/test/regress/expected/fast_default.out @@ -317,11 +317,72 @@ SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2; 2 | 3 | t | {This,is,abcd,the,real,world} | t (2 rows) +-- test fast default over domains with constraints +CREATE DOMAIN domain5 AS int CHECK(value > 10) DEFAULT 8; +CREATE DOMAIN domain6 as int CHECK(value > 10) DEFAULT random(min=>11, max=>100); +CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12); +CREATE DOMAIN domain8 as int NOT NULL; +CREATE TABLE test_add_domain_col(a int); +-- succeeds despite constraint-violating default because table is empty +ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5; +NOTICE: rewriting table test_add_domain_col for reason 2 +ALTER TABLE test_add_domain_col DROP COLUMN a1; +INSERT INTO test_add_domain_col VALUES(1),(2); +-- tests with non-empty table +ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail +NOTICE: rewriting table test_add_domain_col for reason 2 +ERROR: value for domain domain5 violates check constraint "domain5_check" +ALTER TABLE test_add_domain_col ADD COLUMN b domain8; -- table rewrite, then fail +NOTICE: rewriting table test_add_domain_col for reason 2 +ERROR: domain domain8 does not allow null values +ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail +NOTICE: rewriting table test_add_domain_col for reason 2 +ERROR: value for domain domain5 violates check constraint "domain5_check" +ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite +-- explicit column default expression overrides domain's default +-- expression, so no table rewrite +ALTER TABLE test_add_domain_col ADD COLUMN c domain6 DEFAULT 14; +ALTER TABLE test_add_domain_col ADD COLUMN c1 domain8 DEFAULT 13; -- no table rewrite +SELECT attnum, attname, atthasmissing, atthasdef, attmissingval +FROM pg_attribute +WHERE attnum > 0 AND attrelid = 'test_add_domain_col'::regclass AND attisdropped is false +AND atthasmissing +ORDER BY attnum; + attnum | attname | atthasmissing | atthasdef | attmissingval +--------+---------+---------------+-----------+--------------- + 3 | b | t | t | {12} + 4 | c | t | t | {14} + 5 | c1 | t | t | {13} +(3 rows) + +-- We need to rewrite the table whenever domain default contains volatile expression +ALTER TABLE test_add_domain_col ADD COLUMN d domain6; +NOTICE: rewriting table test_add_domain_col for reason 2 +-- We need to rewrite the table whenever domain constraint expression contains volatile expression +ALTER TABLE test_add_domain_col ADD COLUMN e domain7 default 14; +NOTICE: rewriting table test_add_domain_col for reason 2 +ALTER TABLE test_add_domain_col ADD COLUMN f domain7; +NOTICE: rewriting table test_add_domain_col for reason 2 +-- domain with both volatile and non-volatile CHECK constraints: the +-- volatile one forces a table rewrite +CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0); +ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14; +NOTICE: rewriting table test_add_domain_col for reason 2 +-- virtual generated columns cannot have domain types +ALTER TABLE test_add_domain_col ADD COLUMN h domain5 + GENERATED ALWAYS AS (a + 20) VIRTUAL; -- error +ERROR: virtual generated column "h" cannot have a domain type DROP TABLE t2; +DROP TABLE test_add_domain_col; DROP DOMAIN domain1; DROP DOMAIN domain2; DROP DOMAIN domain3; DROP DOMAIN domain4; +DROP DOMAIN domain5; +DROP DOMAIN domain6; +DROP DOMAIN domain7; +DROP DOMAIN domain8; +DROP DOMAIN domain9; DROP FUNCTION foo(INT); -- Fall back to full rewrite for volatile expressions CREATE TABLE T(pk INT NOT NULL PRIMARY KEY); @@ -368,7 +429,7 @@ SELECT attname, atthasmissing, attmissingval FROM pg_attribute DROP TABLE T; DROP FUNCTION foolme(timestamptz); --- Simple querie +-- Simple queries CREATE TABLE T (pk INT NOT NULL PRIMARY KEY); SELECT set('t'); set diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out new file mode 100644 index 0000000000000..31f772c723d5b --- /dev/null +++ b/src/test/regress/expected/for_portion_of.out @@ -0,0 +1,2100 @@ +-- Tests for UPDATE/DELETE FOR PORTION OF +SET datestyle TO ISO, YMD; +-- Works on non-PK columns +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text NOT NULL +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2020-01-01)', 'one'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = 'one^1'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-01-15) | one + [1,2) | [2018-01-15,2019-01-01) | one^1 + [1,2) | [2019-01-01,2020-01-01) | one +(3 rows) + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-01-15) | one + [1,2) | [2018-01-15,2019-01-01) | one^1 + [1,2) | [2019-01-01,2019-01-15) | one + [1,2) | [2019-01-20,2020-01-01) | one +(4 rows) + +-- With a table alias with AS +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t + SET name = 'one^2'; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t; +-- With a table alias without AS +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t + SET name = 'one^3'; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t; +-- UPDATE with FROM +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02' + SET name = 'one^4' + FROM (SELECT '[1,2)'::int4range) AS t2(id) + WHERE for_portion_of_test.id = t2.id; +-- DELETE with USING +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-03-02' TO '2019-03-03' + USING (SELECT '[1,2)'::int4range) AS t2(id) + WHERE for_portion_of_test.id = t2.id; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-01-15) | one + [1,2) | [2018-01-15,2019-01-01) | one^1 + [1,2) | [2019-01-01,2019-01-15) | one + [1,2) | [2019-01-20,2019-02-01) | one + [1,2) | [2019-02-01,2019-02-03) | one^2 + [1,2) | [2019-02-04,2019-02-05) | one^3 + [1,2) | [2019-02-06,2019-03-01) | one + [1,2) | [2019-03-01,2019-03-02) | one^4 + [1,2) | [2019-03-03,2020-01-01) | one +(9 rows) + +-- Works on more than one range +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid1_at daterange, + valid2_at daterange, + name text NOT NULL +); +INSERT INTO for_portion_of_test (id, valid1_at, valid2_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one'); +UPDATE for_portion_of_test + FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL + SET name = 'foo'; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + id | valid1_at | valid2_at | name +-------+-------------------------+-------------------------+------ + [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2025-01-01) | one + [1,2) | [2018-01-15,2018-02-03) | [2015-01-01,2025-01-01) | foo +(2 rows) + +UPDATE for_portion_of_test + FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL + SET name = 'bar'; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + id | valid1_at | valid2_at | name +-------+-------------------------+-------------------------+------ + [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2018-01-15) | one + [1,2) | [2018-01-02,2018-01-15) | [2018-01-15,2025-01-01) | bar + [1,2) | [2018-01-15,2018-02-03) | [2015-01-01,2018-01-15) | foo + [1,2) | [2018-01-15,2018-02-03) | [2018-01-15,2025-01-01) | bar +(4 rows) + +DELETE FROM for_portion_of_test + FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + id | valid1_at | valid2_at | name +-------+-------------------------+-------------------------+------ + [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2018-01-15) | one + [1,2) | [2018-01-02,2018-01-15) | [2018-01-15,2025-01-01) | bar + [1,2) | [2018-01-15,2018-01-20) | [2015-01-01,2018-01-15) | foo + [1,2) | [2018-01-15,2018-01-20) | [2018-01-15,2025-01-01) | bar +(4 rows) + +DELETE FROM for_portion_of_test + FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + id | valid1_at | valid2_at | name +-------+-------------------------+-------------------------+------ + [1,2) | [2018-01-02,2018-01-15) | [2015-01-01,2018-01-15) | one + [1,2) | [2018-01-02,2018-01-15) | [2018-01-15,2018-01-20) | bar + [1,2) | [2018-01-15,2018-01-20) | [2015-01-01,2018-01-15) | foo + [1,2) | [2018-01-15,2018-01-20) | [2018-01-15,2018-01-20) | bar +(4 rows) + +-- Test with NULLs in the scalar/range key columns. +-- This won't happen if there is a PRIMARY KEY or UNIQUE constraint +-- but FOR PORTION OF shouldn't require that. +DROP TABLE for_portion_of_test; +CREATE UNLOGGED TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', NULL, '1 null'), + ('[1,2)', '(,)', '1 unbounded'), + ('[1,2)', 'empty', '1 empty'), + (NULL, NULL, NULL), + (NULL, daterange('2018-01-01', '2019-01-01'), 'null key'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'NULL to NULL'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------------- + [1,2) | empty | 1 empty + [1,2) | (,) | NULL to NULL + [1,2) | | 1 null + | [2018-01-01,2019-01-01) | NULL to NULL + | | +(5 rows) + +DROP TABLE for_portion_of_test; +-- +-- UPDATE tests +-- +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'), + ('[4,5)', '(,2018-04-01)', 'four'), + ('[5,6)', '(,)', 'five') + ; +\set QUIET false +-- Updating with a missing column fails +UPDATE for_portion_of_test + FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + SET name = 'foo' + WHERE id = '[5,6)'; +ERROR: column "invalid_at" of relation "for_portion_of_test" does not exist +LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + ^ +-- Updating the range fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + SET valid_at = '[1990-01-01,1999-01-01)' + WHERE id = '[5,6)'; +ERROR: cannot update column "valid_at" because it is used in FOR PORTION OF +LINE 3: SET valid_at = '[1990-01-01,1999-01-01)' + ^ +-- The wrong start type fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + SET name = 'nope' + WHERE id = '[3,4)'; +ERROR: could not coerce FOR PORTION OF FROM bound from integer to date +LINE 2: FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + ^ +-- The wrong end type fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + SET name = 'nope' + WHERE id = '[3,4)'; +ERROR: could not coerce FOR PORTION OF TO bound from integer to date +LINE 2: FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + ^ +-- Updating with timestamps reversed fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' + SET name = 'three^1' + WHERE id = '[3,4)'; +ERROR: range lower bound must be less than or equal to range upper bound +-- Updating with a subquery fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' + SET name = 'nope' + WHERE id = '[3,4)'; +ERROR: cannot use subquery in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '201... + ^ +-- Updating with a column fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + SET name = 'nope' + WHERE id = '[3,4)'; +ERROR: cannot use column reference in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + ^ +-- Updating with timestamps equal does nothing +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' + SET name = 'three^0' + WHERE id = '[3,4)'; +UPDATE 0 +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + SET name = 'three^1' + WHERE id = '[3,4)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+--------- + [3,4) | [2018-01-01,2018-06-01) | three + [3,4) | [2018-06-01,) | three^1 +(2 rows) + +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-03-01' + SET name = 'three^2' + WHERE id = '[3,4)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+--------- + [3,4) | [2018-01-01,2018-03-01) | three^2 + [3,4) | [2018-03-01,2018-06-01) | three + [3,4) | [2018-06-01,) | three^1 +(3 rows) + +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-02-01' + SET name = 'four^1' + WHERE id = '[4,5)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------- + [4,5) | (,2018-02-01) | four^1 + [4,5) | [2018-02-01,2018-04-01) | four +(2 rows) + +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO NULL + SET name = 'four^2' + WHERE id = '[4,5)'; +UPDATE 2 +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------- + [4,5) | (,2017-01-01) | four^1 + [4,5) | [2017-01-01,2018-02-01) | four^2 + [4,5) | [2018-02-01,2018-04-01) | four^2 +(3 rows) + +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' + SET name = 'four^3' + WHERE id = '[4,5)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------- + [4,5) | (,2017-01-01) | four^1 + [4,5) | [2017-01-01,2018-02-01) | four^3 + [4,5) | [2018-02-01,2018-04-01) | four^2 +(3 rows) + +-- Updating an enclosed span +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'two^2' + WHERE id = '[2,3)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [2,3) | [2018-01-01,2018-01-05) | two^2 +(1 row) + +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' + SET name = 'five^1' + WHERE id = '[5,6)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------- + [5,6) | (,2018-01-01) | five + [5,6) | [2018-01-01,2019-01-01) | five^1 + [5,6) | [2019-01-01,) | five +(3 rows) + +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' + SET name = 'five^2' + WHERE id = '[5,6)'; +UPDATE 3 +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------- + [5,6) | (,2017-01-01) | five + [5,6) | [2017-01-01,2018-01-01) | five^2 + [5,6) | [2018-01-01,2019-01-01) | five^2 + [5,6) | [2019-01-01,2020-01-01) | five^2 + [5,6) | [2020-01-01,) | five +(5 rows) + +-- Updating multiple enclosed spans +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'one^2' + WHERE id = '[1,2)'; +UPDATE 3 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-02-03) | one^2 + [1,2) | [2018-02-03,2018-03-03) | one^2 + [1,2) | [2018-03-03,2018-04-04) | one^2 +(3 rows) + +-- Updating with a direct target +UPDATE for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) + SET name = 'one^3' + WHERE id = '[1,2)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-02-03) | one^2 + [1,2) | [2018-02-03,2018-03-03) | one^2 + [1,2) | [2018-03-03,2018-03-10) | one^2 + [1,2) | [2018-03-10,2018-03-15) | one^3 + [1,2) | [2018-03-15,2018-04-04) | one^2 +(5 rows) + +-- Updating with a direct target, coerced from a string +UPDATE for_portion_of_test + FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') + SET name = 'one^3' + WHERE id = '[1,2)'; +UPDATE 1 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-02-03) | one^2 + [1,2) | [2018-02-03,2018-03-03) | one^2 + [1,2) | [2018-03-03,2018-03-10) | one^2 + [1,2) | [2018-03-10,2018-03-15) | one^3 + [1,2) | [2018-03-15,2018-03-17) | one^3 + [1,2) | [2018-03-17,2018-04-04) | one^2 +(6 rows) + +-- Updating with a direct target of the wrong range subtype fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (int4range(1, 4)) + SET name = 'one^3' + WHERE id = '[1,2)'; +ERROR: could not coerce FOR PORTION OF target from int4range to daterange +LINE 2: FOR PORTION OF valid_at (int4range(1, 4)) + ^ +-- Updating with a direct target of a non-rangetype fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (4) + SET name = 'one^3' + WHERE id = '[1,2)'; +ERROR: could not coerce FOR PORTION OF target from integer to daterange +LINE 2: FOR PORTION OF valid_at (4) + ^ +-- Updating with a direct target of NULL fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (NULL) + SET name = 'one^3' + WHERE id = '[1,2)'; +ERROR: FOR PORTION OF target was null +LINE 2: FOR PORTION OF valid_at (NULL) + ^ +-- Updating with a direct target of empty does nothing +UPDATE for_portion_of_test + FOR PORTION OF valid_at ('empty') + SET name = 'one^3' + WHERE id = '[1,2)'; +UPDATE 0 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-02-03) | one^2 + [1,2) | [2018-02-03,2018-03-03) | one^2 + [1,2) | [2018-03-03,2018-03-10) | one^2 + [1,2) | [2018-03-10,2018-03-15) | one^3 + [1,2) | [2018-03-15,2018-03-17) | one^3 + [1,2) | [2018-03-17,2018-04-04) | one^2 +(6 rows) + +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-15' TO NULL + SET id = '[6,7)' + WHERE id = '[1,2)'; +UPDATE 5 +SELECT * FROM for_portion_of_test WHERE id IN ('[1,2)', '[6,7)') ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-02-03) | one^2 + [1,2) | [2018-02-03,2018-02-15) | one^2 + [6,7) | [2018-02-15,2018-03-03) | one^2 + [6,7) | [2018-03-03,2018-03-10) | one^2 + [6,7) | [2018-03-10,2018-03-15) | one^3 + [6,7) | [2018-03-15,2018-03-17) | one^3 + [6,7) | [2018-03-17,2018-04-04) | one^2 +(7 rows) + +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2030-01-01' TO NULL + SET name = name || '*'; +UPDATE 2 +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+---------- + [1,2) | [2018-01-02,2018-02-03) | one^2 + [1,2) | [2018-02-03,2018-02-15) | one^2 + [2,3) | [2018-01-01,2018-01-05) | two^2 + [3,4) | [2018-01-01,2018-03-01) | three^2 + [3,4) | [2018-03-01,2018-06-01) | three + [3,4) | [2018-06-01,2030-01-01) | three^1 + [3,4) | [2030-01-01,) | three^1* + [4,5) | (,2017-01-01) | four^1 + [4,5) | [2017-01-01,2018-02-01) | four^3 + [4,5) | [2018-02-01,2018-04-01) | four^2 + [5,6) | (,2017-01-01) | five + [5,6) | [2017-01-01,2018-01-01) | five^2 + [5,6) | [2018-01-01,2019-01-01) | five^2 + [5,6) | [2019-01-01,2020-01-01) | five^2 + [5,6) | [2020-01-01,2030-01-01) | five + [5,6) | [2030-01-01,) | five* + [6,7) | [2018-02-15,2018-03-03) | one^2 + [6,7) | [2018-03-03,2018-03-10) | one^2 + [6,7) | [2018-03-10,2018-03-15) | one^3 + [6,7) | [2018-03-15,2018-03-17) | one^3 + [6,7) | [2018-03-17,2018-04-04) | one^2 +(21 rows) + +\set QUIET true +-- Updating with a shift/reduce conflict +-- (requires a tsrange column) +CREATE UNLOGGED TABLE for_portion_of_test2 ( + id int4range, + valid_at tsrange, + name text +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', '[2000-01-01,2020-01-01)', 'one'); +-- updates [2011-03-01 01:02:00, 2012-01-01) (note 2 minutes) +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE + TO '2012-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +-- TO is used for the bound but not the INTERVAL: +-- syntax error +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR + TO '2014-01-01' + SET name = 'one^2' + WHERE id = '[1,2)'; +ERROR: syntax error at or near "'2014-01-01'" +LINE 4: TO '2014-01-01' + ^ +-- adding parens fixes it +-- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes) +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR) + TO '2016-01-01' + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + id | valid_at | name +-------+-----------------------------------------------+------- + [1,2) | ["2000-01-01 00:00:00","2011-03-01 01:02:00") | one + [1,2) | ["2011-03-01 01:02:00","2012-01-01 00:00:00") | one^1 + [1,2) | ["2012-01-01 00:00:00","2015-03-01 01:00:00") | one + [1,2) | ["2015-03-01 01:00:00","2016-01-01 00:00:00") | one^3 + [1,2) | ["2016-01-01 00:00:00","2020-01-01 00:00:00") | one +(5 rows) + +DROP TABLE for_portion_of_test2; +-- UPDATE FOR PORTION OF in a CTE: +-- The outer query sees the table how it was before the updates, +-- and with no leftovers yet, +-- but it also sees the new values via the RETURNING clause. +-- (We test RETURNING more directly, without a CTE, below.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +WITH update_apr AS ( + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' + SET name = 'Apr 2018' + WHERE id = '[10,11)' + RETURNING id, valid_at, name +) +SELECT * + FROM for_portion_of_test AS t, update_apr + WHERE t.id = update_apr.id; + id | valid_at | name | id | valid_at | name +---------+-------------------------+------+---------+-------------------------+---------- + [10,11) | [2018-01-01,2020-01-01) | ten | [10,11) | [2018-04-01,2018-05-01) | Apr 2018 +(1 row) + +SELECT * FROM for_portion_of_test WHERE id = '[10,11)' ORDER BY id, valid_at; + id | valid_at | name +---------+-------------------------+---------- + [10,11) | [2018-01-01,2018-04-01) | ten + [10,11) | [2018-04-01,2018-05-01) | Apr 2018 + [10,11) | [2018-05-01,2020-01-01) | ten +(3 rows) + +-- UPDATE FOR PORTION OF with current_date +-- (We take care not to make the expectation depend on the timestamp.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[99,100)', '[2000-01-01,)', 'foo'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM current_date TO null + SET name = 'bar' + WHERE id = '[99,100)'; +SELECT name, lower(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date - 1; + name | lower +------+------------ + foo | 2000-01-01 +(1 row) + +SELECT name, upper(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date + 1; + name | upper +------+------- + bar | +(1 row) + +-- UPDATE FOR PORTION OF with clock_timestamp() +-- fails because the function is volatile: +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM clock_timestamp()::date TO null + SET name = 'baz' + WHERE id = '[99,100)'; +ERROR: FOR PORTION OF bounds cannot contain volatile functions +-- clean up: +DELETE FROM for_portion_of_test WHERE id = '[99,100)'; +-- Not visible to UPDATE: +-- Tuples updated/inserted within the CTE are not visible to the main query yet, +-- but neither are old tuples the CTE changed: +-- (This is the same behavior as without FOR PORTION OF.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[11,12)', '[2018-01-01,2020-01-01)', 'eleven'); +WITH update_apr AS ( + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' + SET name = 'Apr 2018' + WHERE id = '[11,12)' + RETURNING id, valid_at, name +) +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + AS t + SET name = 'May 2018' + FROM update_apr AS j + WHERE t.id = j.id; +SELECT * FROM for_portion_of_test WHERE id = '[11,12)' ORDER BY id, valid_at; + id | valid_at | name +---------+-------------------------+---------- + [11,12) | [2018-01-01,2018-04-01) | eleven + [11,12) | [2018-04-01,2018-05-01) | Apr 2018 + [11,12) | [2018-05-01,2020-01-01) | eleven +(3 rows) + +DELETE FROM for_portion_of_test WHERE id IN ('[10,11)', '[11,12)'); +-- UPDATE FOR PORTION OF in a PL/pgSQL function +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +CREATE FUNCTION fpo_update(_id int4range, _target_from date, _target_til date) +RETURNS void LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM $2 TO $3 + SET name = concat(_target_from::text, ' to ', _target_til::text) + WHERE id = $1; +END; +$$; +SELECT fpo_update('[10,11)', '2015-01-01', '2019-01-01'); + fpo_update +------------ + +(1 row) + +SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; + id | valid_at | name +---------+-------------------------+-------------------------- + [10,11) | [2018-01-01,2019-01-01) | 2015-01-01 to 2019-01-01 + [10,11) | [2019-01-01,2020-01-01) | ten +(2 rows) + +-- UPDATE FOR PORTION OF in a compiled SQL function +CREATE FUNCTION fpo_update() +RETURNS text +BEGIN ATOMIC + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = 'one^1' + RETURNING name; +END; +\sf+ fpo_update() + CREATE OR REPLACE FUNCTION public.fpo_update() + RETURNS text + LANGUAGE sql +1 BEGIN ATOMIC +2 UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' SET name = 'one^1'::text +3 RETURNING for_portion_of_test.name; +4 END +CREATE OR REPLACE function fpo_update() +RETURNS text +BEGIN ATOMIC + UPDATE for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) + SET name = 'one^1' + RETURNING name; +END; +\sf+ fpo_update() + CREATE OR REPLACE FUNCTION public.fpo_update() + RETURNS text + LANGUAGE sql +1 BEGIN ATOMIC +2 UPDATE for_portion_of_test FOR PORTION OF valid_at ((daterange('2018-01-15'::date, '2020-01-01'::date) * daterange('2019-01-01'::date, '2022-01-01'::date))) SET name = 'one^1'::text +3 RETURNING for_portion_of_test.name; +4 END +DROP FUNCTION fpo_update(); +DROP TABLE for_portion_of_test; +-- +-- DELETE tests +-- +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'), + ('[4,5)', '(,2018-04-01)', 'four'), + ('[5,6)', '(,)', 'five'), + ('[6,7)', '[2018-01-01,)', 'six'), + ('[7,8)', '(,2018-04-01)', 'seven'), + ('[8,9)', '[2018-01-02,2018-02-03)', 'eight'), + ('[8,9)', '[2018-02-03,2018-03-03)', 'eight'), + ('[8,9)', '[2018-03-03,2018-04-04)', 'eight') + ; +\set QUIET false +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test + FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + WHERE id = '[5,6)'; +ERROR: column "invalid_at" of relation "for_portion_of_test" does not exist +LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + ^ +-- The wrong start type fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + WHERE id = '[3,4)'; +ERROR: could not coerce FOR PORTION OF FROM bound from integer to date +LINE 2: FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + ^ +-- The wrong end type fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + WHERE id = '[3,4)'; +ERROR: could not coerce FOR PORTION OF TO bound from integer to date +LINE 2: FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + ^ +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' + WHERE id = '[3,4)'; +ERROR: range lower bound must be less than or equal to range upper bound +-- Deleting with a subquery fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' + WHERE id = '[3,4)'; +ERROR: cannot use subquery in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '201... + ^ +-- Deleting with a column fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + WHERE id = '[3,4)'; +ERROR: cannot use column reference in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + ^ +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' + WHERE id = '[3,4)'; +DELETE 0 +-- Deleting a finite/open portion with a finite/open target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + WHERE id = '[3,4)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [3,4) | [2018-01-01,2018-06-01) | three +(1 row) + +-- Deleting a finite/open portion with an open/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-03-01' + WHERE id = '[6,7)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[6,7)' ORDER BY id, valid_at; + id | valid_at | name +-------+---------------+------ + [6,7) | [2018-03-01,) | six +(1 row) + +-- Deleting an open/finite portion with an open/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-02-01' + WHERE id = '[4,5)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [4,5) | [2018-02-01,2018-04-01) | four +(1 row) + +-- Deleting an open/finite portion with a finite/open target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO NULL + WHERE id = '[7,8)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[7,8)' ORDER BY id, valid_at; + id | valid_at | name +-------+---------------+------- + [7,8) | (,2017-01-01) | seven +(1 row) + +-- Deleting a finite/finite portion with an exact fit +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-04-01' + WHERE id = '[4,5)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + +-- Deleting an enclosed span +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + WHERE id = '[2,3)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + +-- Deleting an open/open portion with a finite/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' + WHERE id = '[5,6)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + id | valid_at | name +-------+---------------+------ + [5,6) | (,2018-01-01) | five + [5,6) | [2019-01-01,) | five +(2 rows) + +-- Deleting an enclosed span with separate protruding spans +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-03' TO '2018-03-03' + WHERE id = '[1,2)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [2018-01-02,2018-02-03) | one + [1,2) | [2018-03-03,2018-04-04) | one +(2 rows) + +-- Deleting multiple enclosed spans +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + WHERE id = '[8,9)'; +DELETE 3 +SELECT * FROM for_portion_of_test WHERE id = '[8,9)' ORDER BY id, valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + +-- Deleting with a direct target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) + WHERE id = '[1,2)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [2018-01-02,2018-02-03) | one + [1,2) | [2018-03-03,2018-03-10) | one + [1,2) | [2018-03-15,2018-04-04) | one +(3 rows) + +-- Deleting with a direct target, coerced from a string +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') + WHERE id = '[1,2)'; +DELETE 1 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [2018-01-02,2018-02-03) | one + [1,2) | [2018-03-03,2018-03-10) | one + [1,2) | [2018-03-17,2018-04-04) | one +(3 rows) + +-- Deleting with a direct target of the wrong range subtype fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (int4range(1, 4)) + WHERE id = '[1,2)'; +ERROR: could not coerce FOR PORTION OF target from int4range to daterange +LINE 2: FOR PORTION OF valid_at (int4range(1, 4)) + ^ +-- Deleting with a direct target of a non-rangetype fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (4) + WHERE id = '[1,2)'; +ERROR: could not coerce FOR PORTION OF target from integer to daterange +LINE 2: FOR PORTION OF valid_at (4) + ^ +-- Deleting with a direct target of NULL fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (NULL) + WHERE id = '[1,2)'; +ERROR: FOR PORTION OF target was null +LINE 2: FOR PORTION OF valid_at (NULL) + ^ +-- Deleting with a direct target of empty does nothing +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at ('empty') + WHERE id = '[1,2)'; +DELETE 0 +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [2018-01-02,2018-02-03) | one + [1,2) | [2018-03-03,2018-03-10) | one + [1,2) | [2018-03-17,2018-04-04) | one +(3 rows) + +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2030-01-01' TO NULL; +DELETE 2 +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-02-03) | one + [1,2) | [2018-03-03,2018-03-10) | one + [1,2) | [2018-03-17,2018-04-04) | one + [3,4) | [2018-01-01,2018-06-01) | three + [5,6) | (,2018-01-01) | five + [5,6) | [2019-01-01,2030-01-01) | five + [6,7) | [2018-03-01,2030-01-01) | six + [7,8) | (,2017-01-01) | seven +(8 rows) + +\set QUIET true +-- UPDATE ... RETURNING returns only the updated values +-- (not the inserted side values, which are added by a separate "statement"): +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' + SET name = 'three^3' + WHERE id = '[3,4)' + RETURNING *; + id | valid_at | name +-------+-------------------------+--------- + [3,4) | [2018-02-01,2018-02-15) | three^3 +(1 row) + +-- UPDATE ... RETURNING supports NEW and OLD valid_at +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-10' TO '2018-02-20' + SET name = 'three^4' + WHERE id = '[3,4)' + RETURNING OLD.name, NEW.name, OLD.valid_at, NEW.valid_at; + name | name | valid_at | valid_at +---------+---------+-------------------------+------------------------- + three^3 | three^4 | [2018-02-01,2018-02-15) | [2018-02-10,2018-02-15) + three | three^4 | [2018-02-15,2018-06-01) | [2018-02-15,2018-02-20) +(2 rows) + +-- DELETE FOR PORTION OF with current_date +-- (We take care not to make the expectation depend on the timestamp.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[99,100)', '[2000-01-01,)', 'foo'); +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM current_date TO null + WHERE id = '[99,100)'; +SELECT name, lower(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date - 1; + name | lower +------+------------ + foo | 2000-01-01 +(1 row) + +SELECT name, upper(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date + 1; + name | upper +------+------- +(0 rows) + +-- DELETE FOR PORTION OF with clock_timestamp() +-- fails because the function is volatile: +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM clock_timestamp()::date TO null + WHERE id = '[99,100)'; +ERROR: FOR PORTION OF bounds cannot contain volatile functions +-- clean up: +DELETE FROM for_portion_of_test WHERE id = '[99,100)'; +-- DELETE ... RETURNING returns the deleted values, regardless of bounds +-- (not the inserted side values, which are added by a separate "statement"): +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03' + WHERE id = '[3,4)' + RETURNING *; + id | valid_at | name +-------+-------------------------+--------- + [3,4) | [2018-02-01,2018-02-10) | three^3 +(1 row) + +-- DELETE FOR PORTION OF in a PL/pgSQL function +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +CREATE FUNCTION fpo_delete(_id int4range, _target_from date, _target_til date) +RETURNS void LANGUAGE plpgsql AS +$$ +BEGIN + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM $2 TO $3 + WHERE id = $1; +END; +$$; +SELECT fpo_delete('[10,11)', '2015-01-01', '2019-01-01'); + fpo_delete +------------ + +(1 row) + +SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; + id | valid_at | name +---------+-------------------------+------ + [10,11) | [2019-01-01,2020-01-01) | ten +(1 row) + +DELETE FROM for_portion_of_test WHERE id IN ('[10,11)'); +-- DELETE FOR PORTION OF in a compiled SQL function +CREATE FUNCTION fpo_delete() +RETURNS text +BEGIN ATOMIC + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + RETURNING name; +END; +\sf+ fpo_delete() + CREATE OR REPLACE FUNCTION public.fpo_delete() + RETURNS text + LANGUAGE sql +1 BEGIN ATOMIC +2 DELETE FROM for_portion_of_test FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' +3 RETURNING for_portion_of_test.name; +4 END +CREATE OR REPLACE function fpo_delete() +RETURNS text +BEGIN ATOMIC + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) + RETURNING name; +END; +\sf+ fpo_delete() + CREATE OR REPLACE FUNCTION public.fpo_delete() + RETURNS text + LANGUAGE sql +1 BEGIN ATOMIC +2 DELETE FROM for_portion_of_test FOR PORTION OF valid_at ((daterange('2018-01-15'::date, '2020-01-01'::date) * daterange('2019-01-01'::date, '2022-01-01'::date))) +3 RETURNING for_portion_of_test.name; +4 END +DROP FUNCTION fpo_delete(); +-- test domains and CHECK constraints +-- With a domain on a rangetype +CREATE DOMAIN daterange_d AS daterange CHECK (upper(VALUE) <> '2005-05-05'::date); +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at daterange_d, + name text +); +INSERT INTO for_portion_of_test2 VALUES + (1, '[2000-01-01,2020-01-01)', 'one'), + (2, '[2000-01-01,2020-01-01)', 'two'); +INSERT INTO for_portion_of_test2 VALUES + (1, '[2000-01-01,2005-05-05)', 'nope'); +ERROR: value for domain daterange_d violates check constraint "daterange_d_check" +-- UPDATE works: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' + SET name = 'one^1' + WHERE id = 1; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') + SET name = 'one^2' + WHERE id = 1; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; + id | valid_at | name +----+-------------------------+------- + 1 | [2000-01-01,2010-01-01) | one + 1 | [2010-01-01,2010-01-05) | one^1 + 1 | [2010-01-05,2010-01-07) | one + 1 | [2010-01-07,2010-01-09) | one^2 + 1 | [2010-01-09,2020-01-01) | one +(5 rows) + +-- The target is allowed to violate the domain: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + SET name = 'miss' + WHERE id = -1; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') + SET name = 'miss' + WHERE id = -1; +-- test the updated row violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + SET name = 'one^3' + WHERE id = 1; +ERROR: value for domain daterange_d violates check constraint "daterange_d_check" +-- test inserts violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' + SET name = 'one^3' + WHERE id = 1; +ERROR: value for domain daterange_d violates check constraint "daterange_d_check" +-- test updated row violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-11' + SET name = 'one^3' + WHERE id = 1; +ERROR: new row for relation "for_portion_of_test2" violates check constraint "fpo2_check" +DETAIL: Failing row contains (1, [2000-01-01,2001-01-11), one^3). +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' + SET name = 'one^3' + WHERE id = 1; +ERROR: new row for relation "for_portion_of_test2" violates check constraint "fpo2_check" +DETAIL: Failing row contains (1, [2002-02-02,2010-01-01), one). +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; + id | valid_at | name +----+-------------------------+------- + 1 | [2000-01-01,2010-01-01) | one + 1 | [2010-01-01,2010-01-05) | one^1 + 1 | [2010-01-05,2010-01-07) | one + 1 | [2010-01-07,2010-01-09) | one^2 + 1 | [2010-01-09,2020-01-01) | one +(5 rows) + +-- DELETE works: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' + WHERE id = 2; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') + WHERE id = 2; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; + id | valid_at | name +----+-------------------------+------ + 2 | [2000-01-01,2010-01-01) | two + 2 | [2010-01-05,2010-01-07) | two + 2 | [2010-01-09,2020-01-01) | two +(3 rows) + +-- The target is allowed to violate the domain: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + WHERE id = -1; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') + WHERE id = -1; +-- test inserts violating the domain +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' + WHERE id = 2; +ERROR: value for domain daterange_d violates check constraint "daterange_d_check" +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' + WHERE id = 2; +ERROR: new row for relation "for_portion_of_test2" violates check constraint "fpo2_check" +DETAIL: Failing row contains (2, [2002-02-02,2010-01-01), two). +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; + id | valid_at | name +----+-------------------------+------ + 2 | [2000-01-01,2010-01-01) | two + 2 | [2010-01-05,2010-01-07) | two + 2 | [2010-01-09,2020-01-01) | two +(3 rows) + +DROP TABLE for_portion_of_test2; +-- With a domain on a multirangetype +CREATE FUNCTION multirange_lowers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ + SELECT array_agg(lower(r)) FROM UNNEST(mr) u(r); +$$; +CREATE FUNCTION multirange_uppers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ + SELECT array_agg(upper(r)) FROM UNNEST(mr) u(r); +$$; +CREATE DOMAIN datemultirange_d AS datemultirange CHECK (NOT '2005-05-05'::date = ANY (multirange_uppers(VALUE))); +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at datemultirange_d, + name text +); +INSERT INTO for_portion_of_test2 VALUES + (1, '{[2000-01-01,2020-01-01)}', 'one'), + (2, '{[2000-01-01,2020-01-01)}', 'two'); +INSERT INTO for_portion_of_test2 VALUES + (1, '{[2000-01-01,2005-05-05)}', 'nope'); +ERROR: value for domain datemultirange_d violates check constraint "datemultirange_d_check" +-- UPDATE works: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') + SET name = 'one^2' + WHERE id = 1; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; + id | valid_at | name +----+---------------------------------------------------+------- + 1 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | one + 1 | {[2010-01-07,2010-01-09)} | one^2 +(2 rows) + +-- The target is allowed to violate the domain: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + SET name = 'miss' + WHERE id = -1; +-- test the updated row violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + SET name = 'one^3' + WHERE id = 1; +ERROR: value for domain datemultirange_d violates check constraint "datemultirange_d_check" +-- test inserts violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') + SET name = 'one^3' + WHERE id = 1; +ERROR: value for domain datemultirange_d violates check constraint "datemultirange_d_check" +-- test updated row violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2000-01-01,2001-01-11)}') + SET name = 'one^3' + WHERE id = 1; +ERROR: new row for relation "for_portion_of_test2" violates check constraint "fpo2_check" +DETAIL: Failing row contains (1, {[2000-01-01,2001-01-11)}, one^3). +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') + SET name = 'one^3' + WHERE id = 1; +ERROR: new row for relation "for_portion_of_test2" violates check constraint "fpo2_check" +DETAIL: Failing row contains (1, {[2000-01-01,2001-01-01),[2002-02-02,2010-01-07),[2010-01-09,202..., one). +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; + id | valid_at | name +----+---------------------------------------------------+------- + 1 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | one + 1 | {[2010-01-07,2010-01-09)} | one^2 +(2 rows) + +-- DELETE works: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') + WHERE id = 2; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; + id | valid_at | name +----+---------------------------------------------------+------ + 2 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | two +(1 row) + +-- The target is allowed to violate the domain: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + WHERE id = -1; +-- test inserts violating the domain +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') + WHERE id = 2; +ERROR: value for domain datemultirange_d violates check constraint "datemultirange_d_check" +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') + WHERE id = 2; +ERROR: new row for relation "for_portion_of_test2" violates check constraint "fpo2_check" +DETAIL: Failing row contains (2, {[2000-01-01,2001-01-01),[2002-02-02,2010-01-07),[2010-01-09,202..., two). +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; + id | valid_at | name +----+---------------------------------------------------+------ + 2 | {[2000-01-01,2010-01-07),[2010-01-09,2020-01-01)} | two +(1 row) + +DROP TABLE for_portion_of_test2; +-- test on non-range/multirange columns +-- With a direct target and a scalar column +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at date, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '2020-01-01', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('2010-01-01') + SET name = 'one^1'; +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type +LINE 2: FOR PORTION OF valid_at ('2010-01-01') + ^ +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('2010-01-01'); +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type +LINE 2: FOR PORTION OF valid_at ('2010-01-01'); + ^ +DROP TABLE for_portion_of_test2; +-- With a direct target and a non-{,multi}range gistable column without overlaps +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at point, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '0,0', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('1,1') + SET name = 'one^1'; +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type +LINE 2: FOR PORTION OF valid_at ('1,1') + ^ +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('1,1'); +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type +LINE 2: FOR PORTION OF valid_at ('1,1'); + ^ +DROP TABLE for_portion_of_test2; +-- With a direct target and a non-{,multi}range column with overlaps +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at box, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '0,0,4,4', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('1,1,2,2') + SET name = 'one^1'; +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type +LINE 2: FOR PORTION OF valid_at ('1,1,2,2') + ^ +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('1,1,2,2'); +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range or multirange type +LINE 2: FOR PORTION OF valid_at ('1,1,2,2'); + ^ +DROP TABLE for_portion_of_test2; +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows +CREATE FUNCTION dump_trigger() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE '%: % % %:', + TG_NAME, TG_WHEN, TG_OP, TG_LEVEL; + + IF TG_ARGV[0] THEN + RAISE NOTICE ' old: %', (SELECT string_agg(old_table::text, '\n ') FROM old_table); + ELSE + RAISE NOTICE ' old: %', OLD.valid_at; + END IF; + IF TG_ARGV[1] THEN + RAISE NOTICE ' new: %', (SELECT string_agg(new_table::text, '\n ') FROM new_table); + ELSE + RAISE NOTICE ' new: %', NEW.valid_at; + END IF; + + IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; +END; +$$; +-- statement triggers: +CREATE TRIGGER fpo_before_stmt + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_insert_stmt + AFTER INSERT ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_update_stmt + AFTER UPDATE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_delete_stmt + AFTER DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +-- row triggers: +CREATE TRIGGER fpo_before_row + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' + SET name = 'five^3' + WHERE id = '[5,6)'; +NOTICE: fpo_before_stmt: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE UPDATE ROW: +NOTICE: old: [2019-01-01,2030-01-01) +NOTICE: new: [2021-01-01,2022-01-01) +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2019-01-01,2021-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2019-01-01,2021-01-01) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2022-01-01,2030-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2022-01-01,2030-01-01) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_after_update_row: AFTER UPDATE ROW: +NOTICE: old: [2019-01-01,2030-01-01) +NOTICE: new: [2021-01-01,2022-01-01) +NOTICE: fpo_after_update_stmt: AFTER UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' + WHERE id = '[5,6)'; +NOTICE: fpo_before_stmt: BEFORE DELETE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE DELETE ROW: +NOTICE: old: [2022-01-01,2030-01-01) +NOTICE: new: +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2022-01-01,2023-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2022-01-01,2023-01-01) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2024-01-01,2030-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2024-01-01,2030-01-01) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_after_delete_row: AFTER DELETE ROW: +NOTICE: old: [2022-01-01,2030-01-01) +NOTICE: new: +NOTICE: fpo_after_delete_stmt: AFTER DELETE STATEMENT: +NOTICE: old: +NOTICE: new: +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+--------- + [1,2) | [2018-01-02,2018-02-03) | one + [1,2) | [2018-03-03,2018-03-10) | one + [1,2) | [2018-03-17,2018-04-04) | one + [3,4) | [2018-01-01,2018-02-01) | three + [3,4) | [2018-02-01,2018-02-02) | three^3 + [3,4) | [2018-02-03,2018-02-10) | three^3 + [3,4) | [2018-02-10,2018-02-15) | three^4 + [3,4) | [2018-02-15,2018-02-20) | three^4 + [3,4) | [2018-02-20,2018-06-01) | three + [5,6) | (,2018-01-01) | five + [5,6) | [2019-01-01,2021-01-01) | five + [5,6) | [2021-01-01,2022-01-01) | five^3 + [5,6) | [2022-01-01,2023-01-01) | five + [5,6) | [2024-01-01,2030-01-01) | five + [6,7) | [2018-03-01,2030-01-01) | six + [7,8) | (,2017-01-01) | seven +(16 rows) + +-- Triggers with a custom transition table name: +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); +-- statement triggers: +CREATE TRIGGER fpo_before_stmt + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_insert_stmt + AFTER INSERT ON for_portion_of_test + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true); +CREATE TRIGGER fpo_after_update_stmt + AFTER UPDATE ON for_portion_of_test + REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true); +CREATE TRIGGER fpo_after_delete_stmt + AFTER DELETE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false); +-- row triggers: +CREATE TRIGGER fpo_before_row + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + REFERENCING NEW TABLE AS new_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true); +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true); +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false); +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = '2018-01-15_to_2019-01-01'; +NOTICE: fpo_before_stmt: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE UPDATE ROW: +NOTICE: old: [2018-01-01,2020-01-01) +NOTICE: new: [2018-01-15,2019-01-01) +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2018-01-01,2018-01-15) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: ("[1,2)","[2018-01-01,2018-01-15)",one) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: ("[1,2)","[2018-01-01,2018-01-15)",one) +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2019-01-01,2020-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: ("[1,2)","[2019-01-01,2020-01-01)",one) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: ("[1,2)","[2019-01-01,2020-01-01)",one) +NOTICE: fpo_after_update_row: AFTER UPDATE ROW: +NOTICE: old: ("[1,2)","[2018-01-01,2020-01-01)",one) +NOTICE: new: ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01) +NOTICE: fpo_after_update_stmt: AFTER UPDATE STATEMENT: +NOTICE: old: ("[1,2)","[2018-01-01,2020-01-01)",one) +NOTICE: new: ("[1,2)","[2018-01-15,2019-01-01)",2018-01-15_to_2019-01-01) +ROLLBACK; +BEGIN; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; +NOTICE: fpo_before_stmt: BEFORE DELETE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE DELETE ROW: +NOTICE: old: [2018-01-01,2020-01-01) +NOTICE: new: +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2018-01-21,2020-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: ("[1,2)","[2018-01-21,2020-01-01)",one) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: ("[1,2)","[2018-01-21,2020-01-01)",one) +NOTICE: fpo_after_delete_row: AFTER DELETE ROW: +NOTICE: old: ("[1,2)","[2018-01-01,2020-01-01)",one) +NOTICE: new: +NOTICE: fpo_after_delete_stmt: AFTER DELETE STATEMENT: +NOTICE: old: ("[1,2)","[2018-01-01,2020-01-01)",one) +NOTICE: new: +ROLLBACK; +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-02' + SET name = 'NULL_to_2018-01-01'; +NOTICE: fpo_before_stmt: BEFORE UPDATE STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE UPDATE ROW: +NOTICE: old: [2018-01-01,2020-01-01) +NOTICE: new: [2018-01-01,2018-01-02) +NOTICE: fpo_before_stmt: BEFORE INSERT STATEMENT: +NOTICE: old: +NOTICE: new: +NOTICE: fpo_before_row: BEFORE INSERT ROW: +NOTICE: old: +NOTICE: new: [2018-01-02,2020-01-01) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: ("[1,2)","[2018-01-02,2020-01-01)",one) +NOTICE: fpo_after_insert_stmt: AFTER INSERT STATEMENT: +NOTICE: old: +NOTICE: new: ("[1,2)","[2018-01-02,2020-01-01)",one) +NOTICE: fpo_after_update_row: AFTER UPDATE ROW: +NOTICE: old: ("[1,2)","[2018-01-01,2020-01-01)",one) +NOTICE: new: ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01) +NOTICE: fpo_after_update_stmt: AFTER UPDATE STATEMENT: +NOTICE: old: ("[1,2)","[2018-01-01,2020-01-01)",one) +NOTICE: new: ("[1,2)","[2018-01-01,2018-01-02)",NULL_to_2018-01-01) +ROLLBACK; +-- Deferred triggers +-- (must be CONSTRAINT triggers thus AFTER ROW with no transition tables) +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); +CREATE CONSTRAINT TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE CONSTRAINT TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +CREATE CONSTRAINT TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = '2018-01-15_to_2019-01-01'; +COMMIT; +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2018-01-01,2018-01-15) +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2019-01-01,2020-01-01) +NOTICE: fpo_after_update_row: AFTER UPDATE ROW: +NOTICE: old: [2018-01-01,2020-01-01) +NOTICE: new: [2018-01-15,2019-01-01) +BEGIN; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; +COMMIT; +NOTICE: fpo_after_insert_row: AFTER INSERT ROW: +NOTICE: old: +NOTICE: new: [2018-01-21,2019-01-01) +NOTICE: fpo_after_delete_row: AFTER DELETE ROW: +NOTICE: old: [2018-01-15,2019-01-01) +NOTICE: new: +NOTICE: fpo_after_delete_row: AFTER DELETE ROW: +NOTICE: old: [2018-01-01,2018-01-15) +NOTICE: new: +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-02' + SET name = 'NULL_to_2018-01-01'; +COMMIT; +SELECT * FROM for_portion_of_test; + id | valid_at | name +-------+-------------------------+-------------------------- + [1,2) | [2019-01-01,2020-01-01) | one + [1,2) | [2018-01-21,2019-01-01) | 2018-01-15_to_2019-01-01 +(2 rows) + +-- test FOR PORTION OF from triggers during FOR PORTION OF: +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-01,2020-01-01)', 'one'), + ('[2,3)', '[2018-01-01,2020-01-01)', 'two'), + ('[3,4)', '[2018-01-01,2020-01-01)', 'three'), + ('[4,5)', '[2018-01-01,2020-01-01)', 'four'); +CREATE FUNCTION trg_fpo_update() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + IF pg_trigger_depth() = 1 THEN + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-03-01' + SET name = CONCAT(name, '^') + WHERE id = OLD.id; + END IF; + RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; +END; +$$; +CREATE FUNCTION trg_fpo_delete() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + IF pg_trigger_depth() = 1 THEN + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-03-01' TO '2018-04-01' + WHERE id = OLD.id; + END IF; + RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; +END; +$$; +-- UPDATE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + SET name = CONCAT(name, '*') + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [2018-01-01,2018-02-01) | one + [1,2) | [2018-02-01,2018-03-01) | one^ + [1,2) | [2018-03-01,2018-05-01) | one + [1,2) | [2018-05-01,2018-06-01) | one* + [1,2) | [2018-06-01,2020-01-01) | one +(5 rows) + +DROP TRIGGER fpo_after_update_row ON for_portion_of_test; +-- UPDATE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [2,3) | [2018-01-01,2018-02-01) | two + [2,3) | [2018-02-01,2018-03-01) | two^ + [2,3) | [2018-03-01,2018-05-01) | two + [2,3) | [2018-06-01,2020-01-01) | two +(4 rows) + +DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; +-- DELETE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + SET name = CONCAT(name, '*') + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+-------- + [3,4) | [2018-01-01,2018-03-01) | three + [3,4) | [2018-04-01,2018-05-01) | three + [3,4) | [2018-05-01,2018-06-01) | three* + [3,4) | [2018-06-01,2020-01-01) | three +(4 rows) + +DROP TRIGGER fpo_after_update_row ON for_portion_of_test; +-- DELETE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [4,5) | [2018-01-01,2018-03-01) | four + [4,5) | [2018-04-01,2018-05-01) | four + [4,5) | [2018-06-01,2020-01-01) | four +(3 rows) + +DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; +-- Test with multiranges +CREATE TABLE for_portion_of_test2 ( + id int4range NOT NULL, + valid_at datemultirange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'), + ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'), + ('[2,3)', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'), + ('[3,4)', datemultirange(daterange('2018-01-01', null)), 'three'); + ; +-- Updating with FROM/TO +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range type +LINE 2: FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + ^ +-- Updating with multirange +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01'))) + SET name = 'one^1' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; + id | valid_at | name +-------+---------------------------------------------------+------- + [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one + [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1 + [1,2) | {[2018-03-03,2018-03-05)} | one + [1,2) | {[2018-03-05,2018-04-04)} | one^1 +(4 rows) + +-- Updating with string coercion +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2018-03-05,2018-03-10)}') + SET name = 'one^2' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; + id | valid_at | name +-------+---------------------------------------------------+------- + [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one + [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1 + [1,2) | {[2018-03-03,2018-03-05)} | one + [1,2) | {[2018-03-05,2018-03-10)} | one^2 + [1,2) | {[2018-03-10,2018-04-04)} | one^1 +(5 rows) + +-- Updating with the wrong range subtype fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + SET name = 'one^3' + WHERE id = '[1,2)'; +ERROR: could not coerce FOR PORTION OF target from int4multirange to datemultirange +LINE 2: FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + ^ +-- Updating with a non-multirangetype fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (4) + SET name = 'one^3' + WHERE id = '[1,2)'; +ERROR: could not coerce FOR PORTION OF target from integer to datemultirange +LINE 2: FOR PORTION OF valid_at (4) + ^ +-- Updating with NULL fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (NULL) + SET name = 'one^3' + WHERE id = '[1,2)'; +ERROR: FOR PORTION OF target was null +LINE 2: FOR PORTION OF valid_at (NULL) + ^ +-- Updating with empty does nothing +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{}') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; + id | valid_at | name +-------+---------------------------------------------------+------- + [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one + [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1 + [1,2) | {[2018-03-03,2018-03-05)} | one + [1,2) | {[2018-03-05,2018-03-10)} | one^2 + [1,2) | {[2018-03-10,2018-04-04)} | one^1 +(5 rows) + +-- Deleting with FROM/TO +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +ERROR: column "valid_at" of relation "for_portion_of_test2" is not a range type +LINE 2: FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + ^ +-- Deleting with multirange +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15'))) + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; + id | valid_at | name +-------+---------------------------------------------------------------------------+------ + [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-15,2018-05-01)} | two +(1 row) + +-- Deleting with string coercion +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2018-03-05,2018-03-20)}') + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; + id | valid_at | name +-------+---------------------------------------------------------------------------+------ + [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-20,2018-05-01)} | two +(1 row) + +-- Deleting with the wrong range subtype fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + WHERE id = '[2,3)'; +ERROR: could not coerce FOR PORTION OF target from int4multirange to datemultirange +LINE 2: FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + ^ +-- Deleting with a non-multirangetype fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (4) + WHERE id = '[2,3)'; +ERROR: could not coerce FOR PORTION OF target from integer to datemultirange +LINE 2: FOR PORTION OF valid_at (4) + ^ +-- Deleting with NULL fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (NULL) + WHERE id = '[2,3)'; +ERROR: FOR PORTION OF target was null +LINE 2: FOR PORTION OF valid_at (NULL) + ^ +-- Deleting with empty does nothing +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{}') + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + id | valid_at | name +-------+---------------------------------------------------------------------------+------- + [1,2) | {[2018-01-02,2018-01-10),[2018-02-10,2018-03-03)} | one + [1,2) | {[2018-01-10,2018-02-03),[2018-02-04,2018-02-10)} | one^1 + [1,2) | {[2018-03-03,2018-03-05)} | one + [1,2) | {[2018-03-05,2018-03-10)} | one^2 + [1,2) | {[2018-03-10,2018-04-04)} | one^1 + [2,3) | {[2018-01-01,2018-01-15),[2018-02-15,2018-03-01),[2018-03-20,2018-05-01)} | two + [3,4) | {[2018-01-01,)} | three +(7 rows) + +DROP TABLE for_portion_of_test2; +-- Test with a custom range type +CREATE TYPE mydaterange AS range(subtype=date); +CREATE TABLE for_portion_of_test2 ( + id int4range NOT NULL, + valid_at mydaterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-05-01)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'); + ; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10' + SET name = 'one^1' + WHERE id = '[1,2)'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15' + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2018-01-02,2018-01-10) | one + [1,2) | [2018-01-10,2018-02-03) | one^1 + [1,2) | [2018-02-03,2018-02-10) | one^1 + [1,2) | [2018-02-10,2018-03-03) | one + [1,2) | [2018-03-03,2018-04-04) | one + [2,3) | [2018-01-01,2018-01-15) | two + [2,3) | [2018-02-15,2018-05-01) | two + [3,4) | [2018-01-01,) | three +(8 rows) + +DROP TABLE for_portion_of_test2; +DROP TYPE mydaterange; +-- Test FOR PORTION OF against a partitioned table. +-- temporal_partitioned_1 has the same attnums as the root +-- temporal_partitioned_3 has the different attnums from the root +-- temporal_partitioned_5 has the different attnums too, but reversed +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); +CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); +CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)'); +ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL; +ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)'); +ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL; +ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)'); +INSERT INTO temporal_partitioned (id, valid_at, name) VALUES + ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'), + ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'), + ('[5,6)', daterange('2000-01-01', '2010-01-01'), 'five'); +SELECT * FROM temporal_partitioned; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [2000-01-01,2010-01-01) | one + [3,4) | [2000-01-01,2010-01-01) | three + [5,6) | [2000-01-01,2010-01-01) | five +(3 rows) + +-- Update without moving within partition 1 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +-- Update without moving within partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'three^1' + WHERE id = '[3,4)'; +-- Update without moving within partition 5 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'five^1' + WHERE id = '[5,6)'; +-- Move from partition 1 to partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'one^2', + id = '[4,5)' + WHERE id = '[1,2)'; +-- Move from partition 3 to partition 1 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'three^2', + id = '[2,3)' + WHERE id = '[3,4)'; +-- Move from partition 5 to partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'five^2', + id = '[3,4)' + WHERE id = '[5,6)'; +-- Update all partitions at once (each with leftovers) +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+--------- + [1,2) | [2000-01-01,2000-03-01) | one + [1,2) | [2000-03-01,2000-04-01) | one^1 + [1,2) | [2000-04-01,2000-06-01) | one + [1,2) | [2000-07-01,2010-01-01) | one + [2,3) | [2000-06-01,2000-07-01) | three^2 + [3,4) | [2000-01-01,2000-03-01) | three + [3,4) | [2000-03-01,2000-04-01) | three^1 + [3,4) | [2000-04-01,2000-06-01) | three + [3,4) | [2000-06-01,2000-07-01) | five^2 + [3,4) | [2000-07-01,2010-01-01) | three + [4,5) | [2000-06-01,2000-07-01) | one^2 + [5,6) | [2000-01-01,2000-03-01) | five + [5,6) | [2000-03-01,2000-04-01) | five^1 + [5,6) | [2000-04-01,2000-06-01) | five + [5,6) | [2000-07-01,2010-01-01) | five +(15 rows) + +SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+--------- + [1,2) | [2000-01-01,2000-03-01) | one + [1,2) | [2000-03-01,2000-04-01) | one^1 + [1,2) | [2000-04-01,2000-06-01) | one + [1,2) | [2000-07-01,2010-01-01) | one + [2,3) | [2000-06-01,2000-07-01) | three^2 +(5 rows) + +SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; + name | id | valid_at +---------+-------+------------------------- + three | [3,4) | [2000-01-01,2000-03-01) + three^1 | [3,4) | [2000-03-01,2000-04-01) + three | [3,4) | [2000-04-01,2000-06-01) + five^2 | [3,4) | [2000-06-01,2000-07-01) + three | [3,4) | [2000-07-01,2010-01-01) + one^2 | [4,5) | [2000-06-01,2000-07-01) +(6 rows) + +SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; + name | valid_at | id +--------+-------------------------+------- + five | [2000-01-01,2000-03-01) | [5,6) + five^1 | [2000-03-01,2000-04-01) | [5,6) + five | [2000-04-01,2000-06-01) | [5,6) + five | [2000-07-01,2010-01-01) | [5,6) +(4 rows) + +DROP TABLE temporal_partitioned; +RESET datestyle; diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out index cce49e509abe4..d8e4cb12c3d14 100644 --- a/src/test/regress/expected/foreign_data.out +++ b/src/test/regress/expected/foreign_data.out @@ -9,6 +9,10 @@ CREATE FUNCTION test_fdw_handler() RETURNS fdw_handler AS :'regresslib', 'test_fdw_handler' LANGUAGE C; +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; -- Clean up in case a prior regression run failed -- Suppress NOTICE messages when roles don't exist SET client_min_messages TO 'warning'; @@ -106,6 +110,18 @@ ERROR: conflicting or redundant options LINE 1: ...GN DATA WRAPPER test_fdw HANDLER test_fdw_handler HANDLER in... ^ CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler; +-- should preserve dependencies on test_fdw_handler and test_fdw_connection +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +ALTER FOREIGN DATA WRAPPER test_fdw VALIDATOR postgresql_fdw_validator; +WARNING: changing the foreign-data wrapper validator can cause the options for dependent objects to become invalid +DROP FUNCTION test_fdw_handler(); -- ERROR +ERROR: cannot drop function test_fdw_handler() because other objects depend on it +DETAIL: foreign-data wrapper test_fdw depends on function test_fdw_handler() +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP FUNCTION test_fdw_connection(oid, oid, internal); -- ERROR +ERROR: cannot drop function test_fdw_connection(oid,oid,internal) because other objects depend on it +DETAIL: foreign-data wrapper test_fdw depends on function test_fdw_connection(oid,oid,internal) +HINT: Use DROP ... CASCADE to drop the dependent objects too. DROP FOREIGN DATA WRAPPER test_fdw; -- ALTER FOREIGN DATA WRAPPER ALTER FOREIGN DATA WRAPPER foo OPTIONS (nonexistent 'fdw'); -- ERROR @@ -828,10 +844,13 @@ COMMENT ON COLUMN ft1.c1 IS NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c4 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c5 integer DEFAULT 0; ALTER FOREIGN TABLE ft1 ADD COLUMN c6 integer; +ALTER FOREIGN TABLE ft1 ADD COLUMN IF NOT EXISTS c6 integer; +NOTICE: column "c6" of relation "ft1" already exists, skipping ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); +ALTER FOREIGN TABLE ft1 ADD c11 integer; ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0; ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT; ALTER FOREIGN TABLE ft1 ALTER COLUMN c6 SET NOT NULL; @@ -863,6 +882,7 @@ ALTER FOREIGN TABLE ft1 ALTER COLUMN c8 SET STORAGE PLAIN; c8 | text | | | | (p2 'V2') | plain | | c9 | integer | | | | | plain | | c10 | integer | | | | (p1 'v1') | plain | | + c11 | integer | | | | | plain | | Check constraints: "ft1_c2_check" CHECK (c2 <> ''::text) "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date) @@ -897,6 +917,7 @@ ERROR: column "no_column" of relation "ft1" does not exist ALTER FOREIGN TABLE ft1 DROP COLUMN IF EXISTS no_column; NOTICE: column "no_column" of relation "ft1" does not exist, skipping ALTER FOREIGN TABLE ft1 DROP COLUMN c9; +ALTER FOREIGN TABLE ft1 DROP c11; ALTER FOREIGN TABLE ft1 ADD COLUMN c11 serial; ALTER FOREIGN TABLE ft1 SET SCHEMA foreign_schema; ALTER FOREIGN TABLE ft1 SET TABLESPACE ts; -- ERROR @@ -931,6 +952,8 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c4 integer; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c6 integer; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN IF NOT EXISTS c6 integer; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c7 integer NOT NULL; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c8 integer; @@ -939,6 +962,8 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c9 integer; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD c11 integer; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c6 SET NOT NULL; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c7 DROP NOT NULL; @@ -960,10 +985,14 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OWNER TO regress_test_role; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OPTIONS (DROP delimiter, SET quote '~', ADD escape '@'); NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN no_column; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN IF EXISTS no_column; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN c9; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP c11; +NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 SET SCHEMA foreign_schema; NOTICE: relation "doesnt_exist_ft1" does not exist, skipping ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME c1 TO foreign_column_1; @@ -1416,7 +1445,8 @@ CREATE FOREIGN TABLE ft2 () INHERITS (fd_pt1) c3 | date | | | | plain | | Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1429,7 +1459,8 @@ Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 +Inherits: + fd_pt1 DROP FOREIGN TABLE ft2; \d+ fd_pt1 @@ -1469,7 +1500,8 @@ ALTER FOREIGN TABLE ft2 INHERIT fd_pt1; c3 | date | | | | plain | | Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1482,7 +1514,8 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 +Inherits: + fd_pt1 CREATE TABLE ct3() INHERITS(ft2); CREATE FOREIGN TABLE ft3 ( @@ -1505,9 +1538,11 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 -Child tables: ct3, - ft3, FOREIGN +Inherits: + fd_pt1 +Child tables: + ct3 + ft3, FOREIGN \d+ ct3 Table "public.ct3" @@ -1518,7 +1553,8 @@ Child tables: ct3, c3 | date | | | | plain | | Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (inherited) -Inherits: ft2 +Inherits: + ft2 \d+ ft3 Foreign table "public.ft3" @@ -1530,7 +1566,8 @@ Inherits: ft2 Not-null constraints: "ft3_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 -Inherits: ft2 +Inherits: + ft2 -- add attributes recursively ALTER TABLE fd_pt1 ADD COLUMN c4 integer; @@ -1553,7 +1590,8 @@ ALTER TABLE fd_pt1 ADD COLUMN c8 integer; Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" "fd_pt1_c7_not_null" NOT NULL "c7" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1572,9 +1610,11 @@ Not-null constraints: "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 -Child tables: ct3, - ft3, FOREIGN +Inherits: + fd_pt1 +Child tables: + ct3 + ft3, FOREIGN \d+ ct3 Table "public.ct3" @@ -1591,7 +1631,8 @@ Child tables: ct3, Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (inherited) "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) -Inherits: ft2 +Inherits: + ft2 \d+ ft3 Foreign table "public.ft3" @@ -1609,7 +1650,8 @@ Not-null constraints: "ft3_c1_not_null" NOT NULL "c1" (local, inherited) "fd_pt1_c7_not_null" NOT NULL "c7" (inherited) Server: s0 -Inherits: ft2 +Inherits: + ft2 -- alter attributes recursively ALTER TABLE fd_pt1 ALTER COLUMN c4 SET DEFAULT 0; @@ -1639,7 +1681,8 @@ ALTER TABLE fd_pt1 ALTER COLUMN c8 SET STORAGE EXTERNAL; Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" "fd_pt1_c6_not_null" NOT NULL "c6" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1658,9 +1701,11 @@ Not-null constraints: "fd_pt1_c6_not_null" NOT NULL "c6" (inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 -Child tables: ct3, - ft3, FOREIGN +Inherits: + fd_pt1 +Child tables: + ct3 + ft3, FOREIGN -- drop attributes recursively ALTER TABLE fd_pt1 DROP COLUMN c4; @@ -1677,7 +1722,8 @@ ALTER TABLE fd_pt1 DROP COLUMN c8; c3 | date | | | | plain | | Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1690,9 +1736,11 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 -Child tables: ct3, - ft3, FOREIGN +Inherits: + fd_pt1 +Child tables: + ct3 + ft3, FOREIGN -- add constraints recursively ALTER TABLE fd_pt1 ADD CONSTRAINT fd_pt1chk1 CHECK (c1 > 0) NO INHERIT; @@ -1722,7 +1770,8 @@ Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1737,9 +1786,11 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 -Child tables: ct3, - ft3, FOREIGN +Inherits: + fd_pt1 +Child tables: + ct3 + ft3, FOREIGN DROP FOREIGN TABLE ft2; -- ERROR ERROR: cannot drop foreign table ft2 because other objects depend on it @@ -1773,7 +1824,8 @@ Check constraints: "fd_pt1chk2" CHECK (c2 <> ''::text) Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1788,7 +1840,8 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 +Inherits: + fd_pt1 -- drop constraints recursively ALTER TABLE fd_pt1 DROP CONSTRAINT fd_pt1chk1 CASCADE; @@ -1807,7 +1860,8 @@ Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1823,7 +1877,8 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 +Inherits: + fd_pt1 -- VALIDATE CONSTRAINT need do nothing on foreign tables ALTER TABLE fd_pt1 VALIDATE CONSTRAINT fd_pt1chk3; @@ -1838,7 +1893,8 @@ Check constraints: "fd_pt1chk3" CHECK (c2 <> ''::text) Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "c1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1854,7 +1910,8 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "c1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 +Inherits: + fd_pt1 -- changes name of an attribute recursively ALTER TABLE fd_pt1 RENAME COLUMN c1 TO f1; @@ -1873,7 +1930,8 @@ Check constraints: "f2_check" CHECK (f2 <> ''::text) Not-null constraints: "fd_pt1_c1_not_null" NOT NULL "f1" -Child tables: ft2, FOREIGN +Child tables: + ft2, FOREIGN \d+ ft2 Foreign table "public.ft2" @@ -1889,7 +1947,8 @@ Not-null constraints: "ft2_c1_not_null" NOT NULL "f1" (local, inherited) Server: s0 FDW options: (delimiter ',', quote '"', "be quoted" 'value') -Inherits: fd_pt1 +Inherits: + fd_pt1 DROP TABLE fd_pt1 CASCADE; NOTICE: drop cascades to foreign table ft2 @@ -1935,7 +1994,8 @@ CREATE FOREIGN TABLE fd_pt2_1 PARTITION OF fd_pt2 FOR VALUES IN (1) Partition key: LIST (c1) Not-null constraints: "fd_pt2_c1_not_null" NOT NULL "c1" -Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN +Partitions: + fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 Foreign table "public.fd_pt2_1" @@ -2017,7 +2077,8 @@ ALTER TABLE fd_pt2 ATTACH PARTITION fd_pt2_1 FOR VALUES IN (1); Partition key: LIST (c1) Not-null constraints: "fd_pt2_c1_not_null" NOT NULL "c1" -Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN +Partitions: + fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 Foreign table "public.fd_pt2_1" @@ -2049,7 +2110,8 @@ ALTER TABLE fd_pt2_1 ADD CONSTRAINT p21chk CHECK (c2 <> ''); Partition key: LIST (c1) Not-null constraints: "fd_pt2_c1_not_null" NOT NULL "c1" -Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN +Partitions: + fd_pt2_1 FOR VALUES IN (1), FOREIGN \d+ fd_pt2_1 Foreign table "public.fd_pt2_1" diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 4f3f280a439bc..8b3b268de0fb1 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -370,6 +370,53 @@ SELECT * FROM PKTABLE; DROP TABLE FKTABLE; DROP TABLE PKTABLE; -- +-- Check RLS +-- +CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); +-- Insert test data into PKTABLE +INSERT INTO PKTABLE VALUES (1, 'Test1'); +INSERT INTO PKTABLE VALUES (2, 'Test2'); +INSERT INTO PKTABLE VALUES (3, 'Test3'); +-- Grant privileges on PKTABLE/FKTABLE to user regress_foreign_key_user +CREATE USER regress_foreign_key_user NOLOGIN; +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; +GRANT SELECT, INSERT ON FKTABLE TO regress_foreign_key_user; +-- Enable RLS on PKTABLE and Create policies +ALTER TABLE PKTABLE ENABLE ROW LEVEL SECURITY; +CREATE POLICY pktable_view_odd_policy ON PKTABLE TO regress_foreign_key_user USING (ptest1 % 2 = 1); +ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; +SET ROLE regress_foreign_key_user; +INSERT INTO FKTABLE VALUES (3, 5); +INSERT INTO FKTABLE VALUES (2, 5); -- success, REFERENCES are not subject to row security +RESET ROLE; +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; +DROP USER regress_foreign_key_user; +-- +-- Check ACL +-- +CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); +-- Insert test data into PKTABLE +INSERT INTO PKTABLE VALUES (1, 'Test1'); +INSERT INTO PKTABLE VALUES (2, 'Test2'); +INSERT INTO PKTABLE VALUES (3, 'Test3'); +-- Grant usage on PKTABLE to user regress_foreign_key_user +CREATE USER regress_foreign_key_user NOLOGIN; +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; +ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; +-- Inserting into FKTABLE should work +INSERT INTO FKTABLE VALUES (3, 5); +-- Revoke usage on PKTABLE from user regress_foreign_key_user +REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user; +-- Inserting into FKTABLE should fail +INSERT INTO FKTABLE VALUES (2, 6); +ERROR: permission denied for table pktable +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; +DROP USER regress_foreign_key_user; +-- -- Check initial check upon ALTER TABLE -- CREATE TABLE PKTABLE ( ptest1 int, ptest2 int, PRIMARY KEY(ptest1, ptest2) ); @@ -1157,6 +1204,59 @@ INSERT INTO fktable VALUES (500, 1000); ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey" DETAIL: Key (fk)=(1000) is not present in table "pktable". COMMIT; +-- Check that the existing FK trigger is both deferrable and initially deferred +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + conname | tgrel | tgname | tgtype | tgdeferrable | tginitdeferred +-----------------+---------+--------------------------+--------+--------------+---------------- + fktable_fk_fkey | pktable | RI_ConstraintTrigger_a_N | 9 | t | t + fktable_fk_fkey | pktable | RI_ConstraintTrigger_a_N | 17 | t | t + fktable_fk_fkey | fktable | RI_ConstraintTrigger_c_N | 5 | t | t + fktable_fk_fkey | fktable | RI_ConstraintTrigger_c_N | 17 | t | t +(4 rows) + +-- Changing the constraint to NOT ENFORCED drops the associated FK triggers +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED; +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + conname | tgrel | tgname | tgtype | tgdeferrable | tginitdeferred +---------+-------+--------+--------+--------------+---------------- +(0 rows) + +-- Changing it back to ENFORCED will recreate the necessary FK triggers +-- that are deferrable and initially deferred +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey ENFORCED; +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + conname | tgrel | tgname | tgtype | tgdeferrable | tginitdeferred +-----------------+---------+--------------------------+--------+--------------+---------------- + fktable_fk_fkey | pktable | RI_ConstraintTrigger_a_N | 9 | t | t + fktable_fk_fkey | pktable | RI_ConstraintTrigger_a_N | 17 | t | t + fktable_fk_fkey | fktable | RI_ConstraintTrigger_c_N | 5 | t | t + fktable_fk_fkey | fktable | RI_ConstraintTrigger_c_N | 17 | t | t +(4 rows) + +-- Verify that a deferrable, initially deferred foreign key still works +-- as expected after being set to NOT ENFORCED and then re-enabled +BEGIN; +-- doesn't match PK, but no error yet +INSERT INTO fktable VALUES (2, 20); +-- should catch error from INSERT at commit +COMMIT; +ERROR: insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey" +DETAIL: Key (fk)=(20) is not present in table "pktable". DROP TABLE fktable, pktable; -- tricky behavior: according to SQL99, if a deferred constraint is set -- to 'immediate' mode, it should be checked for validity *immediately*, @@ -1359,7 +1459,7 @@ LINE 1: ...e ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE INITIALLY ... ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NO INHERIT; ERROR: constraint "fktable_fk_fkey" of relation "fktable" is not a not-null constraint ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID; -ERROR: FOREIGN KEY constraints cannot be marked NOT VALID +ERROR: constraints cannot be altered to be NOT VALID LINE 1: ...ER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT VALID; ^ ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey ENFORCED NOT ENFORCED; @@ -1750,7 +1850,7 @@ Indexes: Referenced by: TABLE "fk_partitioned_fk" CONSTRAINT "fk_partitioned_fk_a_b_fkey" FOREIGN KEY (a, b) REFERENCES fk_notpartitioned_pk(a, b) --- Check the exsting FK trigger +-- Check the existing FK trigger SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) @@ -1895,29 +1995,76 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass: (5 rows) DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk; --- NOT VALID foreign key on a non-partitioned table referencing a partitioned table +-- NOT VALID and NOT ENFORCED foreign key on a non-partitioned table +-- referencing a partitioned table CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b); CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000); +CREATE TABLE fk_partitioned_pk_2 PARTITION OF fk_partitioned_pk FOR VALUES FROM (1000,1000) TO (2000,2000); CREATE TABLE fk_notpartitioned_fk (b int, a int); -ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID; --- Constraint will be invalid. -SELECT conname, convalidated FROM pg_constraint +INSERT INTO fk_partitioned_pk VALUES(100,100), (1000,1000); +INSERT INTO fk_notpartitioned_fk VALUES(100,100), (1000,1000); +ALTER TABLE fk_notpartitioned_fk ADD CONSTRAINT fk_notpartitioned_fk_a_b_fkey + FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID; +ALTER TABLE fk_notpartitioned_fk ADD CONSTRAINT fk_notpartitioned_fk_a_b_fkey2 + FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED; +-- All constraints will be invalid, and _fkey2 constraints will not be enforced. +SELECT conname, conenforced, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass ORDER BY oid::regclass::text; - conname | convalidated ----------------------------------+-------------- - fk_notpartitioned_fk_a_b_fkey | f - fk_notpartitioned_fk_a_b_fkey_1 | f -(2 rows) + conname | conenforced | convalidated +----------------------------------+-------------+-------------- + fk_notpartitioned_fk_a_b_fkey | t | f + fk_notpartitioned_fk_a_b_fkey_1 | t | f + fk_notpartitioned_fk_a_b_fkey_2 | t | f + fk_notpartitioned_fk_a_b_fkey2 | f | f + fk_notpartitioned_fk_a_b_fkey2_1 | f | f + fk_notpartitioned_fk_a_b_fkey2_2 | f | f +(6 rows) ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey; --- All constraints are now valid. -SELECT conname, convalidated FROM pg_constraint +ALTER TABLE fk_notpartitioned_fk ALTER CONSTRAINT fk_notpartitioned_fk_a_b_fkey2 ENFORCED; +-- All constraints are now valid and enforced. +SELECT conname, conenforced, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass ORDER BY oid::regclass::text; - conname | convalidated ----------------------------------+-------------- - fk_notpartitioned_fk_a_b_fkey | t - fk_notpartitioned_fk_a_b_fkey_1 | t -(2 rows) + conname | conenforced | convalidated +----------------------------------+-------------+-------------- + fk_notpartitioned_fk_a_b_fkey | t | t + fk_notpartitioned_fk_a_b_fkey_1 | t | t + fk_notpartitioned_fk_a_b_fkey_2 | t | t + fk_notpartitioned_fk_a_b_fkey2 | t | t + fk_notpartitioned_fk_a_b_fkey2_1 | t | t + fk_notpartitioned_fk_a_b_fkey2_2 | t | t +(6 rows) + +-- test a self-referential FK +ALTER TABLE fk_partitioned_pk ADD CONSTRAINT selffk FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID; +CREATE TABLE fk_partitioned_pk_3 PARTITION OF fk_partitioned_pk FOR VALUES FROM (2000,2000) TO (3000,3000) + PARTITION BY RANGE (a); +CREATE TABLE fk_partitioned_pk_3_1 PARTITION OF fk_partitioned_pk_3 FOR VALUES FROM (2000) TO (2100); +SELECT conname, conenforced, convalidated FROM pg_constraint +WHERE conrelid = 'fk_partitioned_pk'::regclass AND contype = 'f' +ORDER BY oid::regclass::text; + conname | conenforced | convalidated +------------+-------------+-------------- + selffk | t | f + selffk_1 | t | f + selffk_2 | t | f + selffk_3 | t | f + selffk_3_1 | t | f +(5 rows) + +ALTER TABLE fk_partitioned_pk_2 VALIDATE CONSTRAINT selffk; +ALTER TABLE fk_partitioned_pk VALIDATE CONSTRAINT selffk; +SELECT conname, conenforced, convalidated FROM pg_constraint +WHERE conrelid = 'fk_partitioned_pk'::regclass AND contype = 'f' +ORDER BY oid::regclass::text; + conname | conenforced | convalidated +------------+-------------+-------------- + selffk | t | t + selffk_1 | t | t + selffk_2 | t | t + selffk_3 | t | t + selffk_3_1 | t | t +(5 rows) DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk; -- Test some other exotic foreign key features: MATCH SIMPLE, ON UPDATE/DELETE @@ -3359,3 +3506,209 @@ SET client_min_messages TO warning; DROP SCHEMA fkpart12 CASCADE; RESET client_min_messages; RESET search_path; +-- Exercise the column mapping code with foreign keys. In this test we'll +-- create a partitioned table which has a partition with a dropped column and +-- check to ensure that an UPDATE cascades the changes correctly to the +-- partitioned table. +CREATE SCHEMA fkpart13; +SET search_path TO fkpart13; +CREATE TABLE fkpart13_t1 (a int PRIMARY KEY); +CREATE TABLE fkpart13_t2 ( + part_id int PRIMARY KEY, + column_to_drop int, + FOREIGN KEY (part_id) REFERENCES fkpart13_t1 ON UPDATE CASCADE ON DELETE CASCADE +) PARTITION BY LIST (part_id); +CREATE TABLE fkpart13_t2_p1 PARTITION OF fkpart13_t2 FOR VALUES IN (1); +-- drop the column +ALTER TABLE fkpart13_t2 DROP COLUMN column_to_drop; +-- create a new partition without the dropped column +CREATE TABLE fkpart13_t2_p2 PARTITION OF fkpart13_t2 FOR VALUES IN (2); +CREATE TABLE fkpart13_t3 ( + a int NOT NULL, + FOREIGN KEY (a) + REFERENCES fkpart13_t2 + ON UPDATE CASCADE ON DELETE CASCADE +); +INSERT INTO fkpart13_t1 (a) VALUES (1); +INSERT INTO fkpart13_t2 (part_id) VALUES (1); +INSERT INTO fkpart13_t3 (a) VALUES (1); +-- Test that a cascading update works correctly with the dropped column +UPDATE fkpart13_t1 SET a = 2 WHERE a = 1; +SELECT tableoid::regclass,* FROM fkpart13_t2; + tableoid | part_id +----------------+--------- + fkpart13_t2_p2 | 2 +(1 row) + +SELECT tableoid::regclass,* FROM fkpart13_t3; + tableoid | a +-------------+--- + fkpart13_t3 | 2 +(1 row) + +-- Exercise code in ExecGetTriggerResultRel() as there's been previous issues +-- with ResultRelInfos being returned with the incorrect ri_RootResultRelInfo +WITH cte AS ( + UPDATE fkpart13_t2_p1 SET part_id = part_id +) UPDATE fkpart13_t1 SET a = 2 WHERE a = 1; +DROP SCHEMA fkpart13 CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table fkpart13_t1 +drop cascades to table fkpart13_t2 +drop cascades to table fkpart13_t3 +RESET search_path; +-- Tests foreign key check fast-path no-cache path. +CREATE TABLE fp_pk_alter (a int PRIMARY KEY); +INSERT INTO fp_pk_alter SELECT generate_series(1, 100); +CREATE TABLE fp_fk_alter (a int); +INSERT INTO fp_fk_alter SELECT generate_series(1, 100); +-- Validation path: should succeed +ALTER TABLE fp_fk_alter ADD FOREIGN KEY (a) REFERENCES fp_pk_alter; +INSERT INTO fp_fk_alter VALUES (101); -- should fail (constraint active) +ERROR: insert or update on table "fp_fk_alter" violates foreign key constraint "fp_fk_alter_a_fkey" +DETAIL: Key (a)=(101) is not present in table "fp_pk_alter". +DROP TABLE fp_fk_alter, fp_pk_alter; +-- Separate test: validation catches existing violation +CREATE TABLE fp_pk_alter2 (a int PRIMARY KEY); +INSERT INTO fp_pk_alter2 VALUES (1); +CREATE TABLE fp_fk_alter2 (a int); +INSERT INTO fp_fk_alter2 VALUES (1), (200); -- 200 has no PK match +ALTER TABLE fp_fk_alter2 ADD FOREIGN KEY (a) REFERENCES fp_pk_alter2; -- should fail +ERROR: insert or update on table "fp_fk_alter2" violates foreign key constraint "fp_fk_alter2_a_fkey" +DETAIL: Key (a)=(200) is not present in table "fp_pk_alter2". +DROP TABLE fp_fk_alter2, fp_pk_alter2; +-- Tests that the fast-path handles caching for multiple constraints +CREATE TABLE fp_pk1 (a int PRIMARY KEY); +CREATE TABLE fp_pk2 (b int PRIMARY KEY); +INSERT INTO fp_pk1 VALUES (1); +INSERT INTO fp_pk2 VALUES (1); +CREATE TABLE fp_multi_fk ( + a int REFERENCES fp_pk1, + b int REFERENCES fp_pk2 +); +INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints, one batch +INSERT INTO fp_multi_fk VALUES (1, 2); -- second constraint fails +ERROR: insert or update on table "fp_multi_fk" violates foreign key constraint "fp_multi_fk_b_fkey" +DETAIL: Key (b)=(2) is not present in table "fp_pk2". +DROP TABLE fp_multi_fk, fp_pk1, fp_pk2; +-- Test that fast-path cache handles deferred constraints and SET CONSTRAINTS IMMEDIATE +CREATE TABLE fp_pk_defer (a int PRIMARY KEY); +CREATE TABLE fp_fk_defer (a int REFERENCES fp_pk_defer DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fp_pk_defer VALUES (1), (2); +BEGIN; +INSERT INTO fp_fk_defer VALUES (1); +INSERT INTO fp_fk_defer VALUES (2); +SET CONSTRAINTS ALL IMMEDIATE; -- fires batch callback here +INSERT INTO fp_fk_defer VALUES (3); -- should fail, also tests that cache was cleaned up +ERROR: insert or update on table "fp_fk_defer" violates foreign key constraint "fp_fk_defer_a_fkey" +DETAIL: Key (a)=(3) is not present in table "fp_pk_defer". +COMMIT; +DROP TABLE fp_pk_defer, fp_fk_defer; +-- Subtransaction abort: cached state must be invalidated on ROLLBACK TO +CREATE TABLE fp_pk_subxact (a int PRIMARY KEY); +CREATE TABLE fp_fk_subxact (a int REFERENCES fp_pk_subxact); +INSERT INTO fp_pk_subxact VALUES (1), (2); +BEGIN; +INSERT INTO fp_fk_subxact VALUES (1); +SAVEPOINT sp1; +INSERT INTO fp_fk_subxact VALUES (2); +ROLLBACK TO sp1; +INSERT INTO fp_fk_subxact VALUES (1); +COMMIT; +SELECT * FROM fp_fk_subxact; + a +--- + 1 + 1 +(2 rows) + +DROP TABLE fp_fk_subxact, fp_pk_subxact; +-- FK check must see PK rows inserted by earlier AFTER triggers +-- firing on the same statement +CREATE TABLE fp_pk_cci (a int PRIMARY KEY); +CREATE TABLE fp_fk_cci (a int REFERENCES fp_pk_cci); +CREATE FUNCTION fp_auto_pk() RETURNS trigger AS $$ +BEGIN + RAISE NOTICE 'fp_auto_pk called'; + INSERT INTO fp_pk_cci VALUES (NEW.a); + RETURN NEW; +END $$ LANGUAGE plpgsql; +-- Name sorts before the RI trigger, so fires first per row +CREATE TRIGGER "AAA_auto" AFTER INSERT ON fp_fk_cci + FOR EACH ROW EXECUTE FUNCTION fp_auto_pk(); +-- Should succeed: AAA_auto provisions the PK row before RI check +INSERT INTO fp_fk_cci VALUES (1), (2), (3); +NOTICE: fp_auto_pk called +NOTICE: fp_auto_pk called +NOTICE: fp_auto_pk called +DROP TABLE fp_fk_cci, fp_pk_cci; +DROP FUNCTION fp_auto_pk; +-- Multi-column FK: exercises batched per-row probing with composite keys +CREATE TABLE fp_pk_multi (a int, b int, PRIMARY KEY (a, b)); +INSERT INTO fp_pk_multi SELECT i, i FROM generate_series(1, 100) i; +CREATE TABLE fp_fk_multi (x int, a int, b int, + FOREIGN KEY (a, b) REFERENCES fp_pk_multi); +INSERT INTO fp_fk_multi SELECT i, i, i FROM generate_series(1, 100) i; +INSERT INTO fp_fk_multi VALUES (1, 999, 999); +ERROR: insert or update on table "fp_fk_multi" violates foreign key constraint "fp_fk_multi_a_b_fkey" +DETAIL: Key (a, b)=(999, 999) is not present in table "fp_pk_multi". +DROP TABLE fp_fk_multi, fp_pk_multi; +-- Multi-column FK with columns in different order than PK index. +-- The FK references columns in a different order than they appear in the +-- PK's primary key, which requires mapping constraint key positions to +-- index column positions when building scan keys. +CREATE TABLE fp_pk_order (a int, b text, c int, PRIMARY KEY (a, b, c)); +INSERT INTO fp_pk_order VALUES (1, 'one', 10), (2, 'two', 20); +CREATE TABLE fp_fk_order ( + x int, + c int, + b text, + a int, + FOREIGN KEY (a, c, b) REFERENCES fp_pk_order (a, c, b) -- c and b swapped +); +INSERT INTO fp_fk_order VALUES (1, 10, 'one', 1); -- should succeed +INSERT INTO fp_fk_order VALUES (2, 20, 'two', 2); -- should succeed +INSERT INTO fp_fk_order VALUES (3, 99, 'none', 9); -- should fail +ERROR: insert or update on table "fp_fk_order" violates foreign key constraint "fp_fk_order_a_c_b_fkey" +DETAIL: Key (a, c, b)=(9, 99, none) is not present in table "fp_pk_order". +DROP TABLE fp_fk_order, fp_pk_order; +-- Same-type columns in different order: +CREATE TABLE fp_pk_same (c1 int, c2 int, PRIMARY KEY (c1, c2)); +INSERT INTO fp_pk_same VALUES (1, 2); +CREATE TABLE fp_fk_same (c1 int, c2 int, + FOREIGN KEY (c2, c1) REFERENCES fp_pk_same (c2, c1)); +INSERT INTO fp_fk_same VALUES (1, 2); -- should succeed +INSERT INTO fp_fk_same VALUES (9, 9); -- should fail +ERROR: insert or update on table "fp_fk_same" violates foreign key constraint "fp_fk_same_c2_c1_fkey" +DETAIL: Key (c2, c1)=(9, 9) is not present in table "fp_pk_same". +DROP TABLE fp_fk_same, fp_pk_same; +-- Deferred constraint: batch flushed at COMMIT, not at statement end +CREATE TABLE fp_pk_commit (a int PRIMARY KEY); +CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit + DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fp_pk_commit VALUES (1); +BEGIN; +INSERT INTO fp_fk_commit VALUES (1); +INSERT INTO fp_fk_commit VALUES (1); +INSERT INTO fp_fk_commit VALUES (999); +COMMIT; +ERROR: insert or update on table "fp_fk_commit" violates foreign key constraint "fp_fk_commit_a_fkey" +DETAIL: Key (a)=(999) is not present in table "fp_pk_commit". +DROP TABLE fp_fk_commit, fp_pk_commit; +-- Cross-type FK with bulk insert: int8 FK referencing int4 PK, +-- values cast during array construction +CREATE TABLE fp_pk_cross (a int4 PRIMARY KEY); +INSERT INTO fp_pk_cross SELECT generate_series(1, 200); +CREATE TABLE fp_fk_cross (a int8 REFERENCES fp_pk_cross); +INSERT INTO fp_fk_cross SELECT generate_series(1, 200); +INSERT INTO fp_fk_cross VALUES (999); +ERROR: insert or update on table "fp_fk_cross" violates foreign key constraint "fp_fk_cross_a_fkey" +DETAIL: Key (a)=(999) is not present in table "fp_pk_cross". +DROP TABLE fp_fk_cross, fp_pk_cross; +-- Duplicate FK values: when using the batched SAOP path, every +-- row must be recognized as satisfied, not just the first match +CREATE TABLE fp_pk_dup (a int PRIMARY KEY); +INSERT INTO fp_pk_dup VALUES (1); +CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup); +INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100); +DROP TABLE fp_fk_dup, fp_pk_dup; diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index 16de30ab1910b..7866ae0ebbe8a 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -324,7 +324,8 @@ SELECT * FROM gtest1_1; --------+---------+-----------+----------+------------------------------------ a | integer | | not null | b | integer | | | generated always as (a * 2) stored -Inherits: gtest1 +Inherits: + gtest1 INSERT INTO gtest1_1 VALUES (4); SELECT * FROM gtest1_1; @@ -373,7 +374,8 @@ NOTICE: merging column "b" with inherited definition x | integer | | | | plain | | Not-null constraints: "gtest1_a_not_null" NOT NULL "a" (inherited) -Inherits: gtest1 +Inherits: + gtest1 INSERT INTO gtestx (a, x) VALUES (11, 22); SELECT * FROM gtest1; @@ -424,8 +426,9 @@ DETAIL: User-specified column moved to the position of the inherited column. a | integer | | not null | b | integer | | | generated always as (x + 1) stored x | integer | | | -Inherits: gtest1, - gtesty +Inherits: + gtest1 + gtesty -- test correct handling of GENERATED column that's only in child CREATE TABLE gtestp (f1 int); @@ -502,6 +505,12 @@ COPY gtest1 FROM stdin; COPY gtest1 (a, b) FROM stdin; ERROR: column "b" is a generated column DETAIL: Generated columns cannot be used in COPY. +COPY gtest1 FROM stdin WHERE b <> 10; +ERROR: generated columns are not supported in COPY FROM WHERE conditions +DETAIL: Column "b" is a generated column. +COPY gtest1 FROM stdin WHERE gtest1 IS NULL; +ERROR: generated columns are not supported in COPY FROM WHERE conditions +DETAIL: Column "b" is a generated column. SELECT * FROM gtest1 ORDER BY a; a | b ---+--- @@ -532,6 +541,12 @@ SELECT * FROM gtest3 ORDER BY a; 4 | 12 (4 rows) +-- COPY JSON should exclude generated columns, same as text/CSV +COPY gtest1 TO stdout WITH (FORMAT json); +{"a":1} +{"a":2} +{"a":3} +{"a":4} -- null values CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED); INSERT INTO gtest2 VALUES (1); @@ -679,14 +694,67 @@ INSERT INTO gtest21a (a) VALUES (1); -- ok INSERT INTO gtest21a (a) VALUES (0); -- violates constraint ERROR: null value in column "b" of relation "gtest21a" violates not-null constraint DETAIL: Failing row contains (0, null). -CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +-- also check with table constraint syntax +CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED, CONSTRAINT cc NOT NULL b); +INSERT INTO gtest21ax (a) VALUES (0); -- violates constraint +ERROR: null value in column "b" of relation "gtest21ax" violates not-null constraint +DETAIL: Failing row contains (0, null). +INSERT INTO gtest21ax (a) VALUES (1); --ok +-- SET EXPRESSION supports not null constraint +ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error +ERROR: column "b" of relation "gtest21ax" contains null values +DROP TABLE gtest21ax; +CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b; +INSERT INTO gtest21ax (a) VALUES (0); -- violates constraint +ERROR: null value in column "b" of relation "gtest21ax" violates not-null constraint +DETAIL: Failing row contains (0, null). +DROP TABLE gtest21ax; +CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL; INSERT INTO gtest21b (a) VALUES (1); -- ok -INSERT INTO gtest21b (a) VALUES (0); -- violates constraint +INSERT INTO gtest21b (a) VALUES (2), (0); -- violates constraint ERROR: null value in column "b" of relation "gtest21b" violates not-null constraint DETAIL: Failing row contains (0, null). +INSERT INTO gtest21b (a) VALUES (NULL); -- error +ERROR: null value in column "b" of relation "gtest21b" violates not-null constraint +DETAIL: Failing row contains (null, null). ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- not-null constraint with partitioned table +CREATE TABLE gtestnn_parent ( + f1 int, + f2 bigint, + f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) STORED NOT NULL +) PARTITION BY RANGE (f1); +CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5); +CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default; +-- check the error messages +INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default); -- ok +INSERT INTO gtestnn_parent VALUES (1, 2, default); -- error +ERROR: null value in column "f3" of relation "gtestnn_child" violates not-null constraint +DETAIL: Failing row contains (1, 2, null). +INSERT INTO gtestnn_parent VALUES (2, 10, default); -- error +ERROR: null value in column "f3" of relation "gtestnn_child" violates not-null constraint +DETAIL: Failing row contains (2, 10, null). +ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + nullif(f2, 11)); -- error +ERROR: column "f3" of relation "gtestnn_child" contains null values +INSERT INTO gtestnn_parent VALUES (10, 11, default); -- ok +SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3; + f1 | f2 | f3 +----+----+---- + 2 | 2 | 4 + 3 | 5 | 8 + 10 | 11 | 21 + 14 | 12 | 26 +(4 rows) + +-- test ALTER TABLE ADD COLUMN +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) STORED; -- error +ERROR: column "c" of relation "gtestnn_childdef" contains null values +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) STORED; -- error +ERROR: column "c" of relation "gtestnn_child" contains null values +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) STORED; -- ok -- index constraints CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) STORED UNIQUE); INSERT INTO gtest22a VALUES (2); @@ -847,6 +915,10 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A INSERT INTO gtest24r (a) VALUES (4); -- ok INSERT INTO gtest24r (a) VALUES (6); -- error ERROR: value for domain gtestdomain1 violates check constraint "gtestdomain1_check" +CREATE TABLE gtest24at (a int PRIMARY KEY); +ALTER TABLE gtest24at ADD COLUMN b gtestdomain1 GENERATED ALWAYS AS (a * 2) STORED; -- ok +CREATE TABLE gtest24ata (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +ALTER TABLE gtest24ata ALTER COLUMN b TYPE gtestdomain1; -- ok CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL); CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) STORED); INSERT INTO gtest24nn (a) VALUES (4); -- ok @@ -1074,11 +1146,26 @@ ERROR: cannot use generated column in partition key LINE 1: ...ENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3); ^ DETAIL: Column "f3" is a generated column. +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3)); +ERROR: cannot use generated column in partition key +LINE 1: ...ERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3)); + ^ +DETAIL: Column "f3" is a generated column. CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3 * 3)); ERROR: cannot use generated column in partition key LINE 1: ...ED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3 * 3)); ^ DETAIL: Column "f3" is a generated column. +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((gtest_part_key)); +ERROR: cannot use generated column in partition key +LINE 1: ...ED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((gtest_par... + ^ +DETAIL: Column "f3" is a generated column. +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((gtest_part_key is not null)); +ERROR: cannot use generated column in partition key +LINE 1: ...ED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((gtest_par... + ^ +DETAIL: Column "f3" is a generated column. -- ALTER TABLE ... ADD COLUMN CREATE TABLE gtest25 (a int PRIMARY KEY); INSERT INTO gtest25 VALUES (3), (4); @@ -1154,6 +1241,15 @@ DETAIL: Column "x" is a generated column. ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT; -- error ERROR: column "x" of relation "gtest27" is a generated column HINT: Use ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION instead. +-- test not-null checking during table rewrite +INSERT INTO gtest27 (a, b) VALUES (NULL, NULL); +ALTER TABLE gtest27 + DROP COLUMN x, + ALTER COLUMN a TYPE bigint, + ALTER COLUMN b TYPE bigint, + ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED NOT NULL; -- error +ERROR: column "x" of relation "gtest27" contains null values +DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL; -- It's possible to alter the column types this way: ALTER TABLE gtest27 DROP COLUMN x, @@ -1279,7 +1375,8 @@ Number of child tables: 1 (Use \d+ to list them.) --------+---------+-----------+----------+--------- a | integer | | | b | integer | | | -Inherits: gtest30 +Inherits: + gtest30 DROP TABLE gtest30 CASCADE; NOTICE: drop cascades to table gtest30_1 @@ -1304,7 +1401,8 @@ Number of child tables: 1 (Use \d+ to list them.) --------+---------+-----------+----------+------------------------------------ a | integer | | | b | integer | | | generated always as (a * 2) stored -Inherits: gtest30 +Inherits: + gtest30 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error ERROR: cannot drop generation expression from inherited column @@ -1313,6 +1411,18 @@ CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c te CREATE TABLE gtest31_2 (x int, y gtest31_1); ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar; -- fails ERROR: cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type +-- bug #18970: these cases are unsupported, but make sure they fail cleanly +ALTER TABLE gtest31_2 ADD CONSTRAINT cc CHECK ((y).b IS NOT NULL); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello1'); +ERROR: cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type +ALTER TABLE gtest31_2 DROP CONSTRAINT cc; +CREATE STATISTICS gtest31_2_stat ON ((y).b is not null) FROM gtest31_2; +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello2'); +ERROR: cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type +DROP STATISTICS gtest31_2_stat; +CREATE INDEX gtest31_2_y_idx ON gtest31_2(((y).b)); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello3'); +ERROR: cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type DROP TABLE gtest31_1, gtest31_2; -- Check it for a partitioned table, too CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text) PARTITION BY LIST (a); @@ -1494,6 +1604,40 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); c | integer | | | x | integer | | | generated always as (b * 2) stored +-- rule actions referring to generated columns: +-- NEW.b in a rule action should reflect the generated column's new value +CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); +CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); +CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); +INSERT INTO gtest_rule (a) VALUES (1); +UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET a = (SELECT max(b) FROM gtest_rule); +SELECT * FROM gtest_rule_log; + op | old_b | new_b +-----+-------+------- + INS | | 2 + UPD | 2 | 20 + UPD | 20 | 40 +(3 rows) + +DROP RULE gtest_rule_upd ON gtest_rule; +DROP RULE gtest_rule_ins ON gtest_rule; +DROP TABLE gtest_rule_log; +-- rule quals referring to generated columns: +-- NEW.b in the rule qual should reflect the generated column's new value +CREATE RULE gtest_rule_qual AS ON UPDATE TO gtest_rule WHERE NEW.b > 100 + DO INSTEAD NOTHING; +UPDATE gtest_rule SET a = 100; +SELECT * FROM gtest_rule; + a | b +----+---- + 20 | 40 +(1 row) + +DROP TABLE gtest_rule; -- sanity check of system catalog SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v'); attrelid | attname | attgenerated diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index 6300e7c1d96e1..24d5dbf46ca19 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -318,7 +318,8 @@ SELECT * FROM gtest1_1; --------+---------+-----------+----------+----------------------------- a | integer | | not null | b | integer | | | generated always as (a * 2) -Inherits: gtest1 +Inherits: + gtest1 INSERT INTO gtest1_1 VALUES (4); SELECT * FROM gtest1_1; @@ -367,7 +368,8 @@ NOTICE: merging column "b" with inherited definition x | integer | | | | plain | | Not-null constraints: "gtest1_a_not_null" NOT NULL "a" (inherited) -Inherits: gtest1 +Inherits: + gtest1 INSERT INTO gtestx (a, x) VALUES (11, 22); SELECT * FROM gtest1; @@ -418,8 +420,9 @@ DETAIL: User-specified column moved to the position of the inherited column. a | integer | | not null | b | integer | | | generated always as (x + 1) x | integer | | | -Inherits: gtest1, - gtesty +Inherits: + gtest1 + gtesty -- test correct handling of GENERATED column that's only in child CREATE TABLE gtestp (f1 int); @@ -496,6 +499,12 @@ COPY gtest1 FROM stdin; COPY gtest1 (a, b) FROM stdin; ERROR: column "b" is a generated column DETAIL: Generated columns cannot be used in COPY. +COPY gtest1 FROM stdin WHERE b <> 10; +ERROR: generated columns are not supported in COPY FROM WHERE conditions +DETAIL: Column "b" is a generated column. +COPY gtest1 FROM stdin WHERE gtest1 IS NULL; +ERROR: generated columns are not supported in COPY FROM WHERE conditions +DETAIL: Column "b" is a generated column. SELECT * FROM gtest1 ORDER BY a; a | b ---+--- @@ -526,6 +535,12 @@ SELECT * FROM gtest3 ORDER BY a; 4 | 12 (4 rows) +-- COPY JSON should exclude generated columns, same as text/CSV +COPY gtest1 TO stdout WITH (FORMAT json); +{"a":1} +{"a":2} +{"a":3} +{"a":4} -- null values CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL); INSERT INTO gtest2 VALUES (1); @@ -552,16 +567,12 @@ CREATE TYPE double_int as (a int, b int); CREATE TABLE gtest4 ( a int, b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) VIRTUAL -); -INSERT INTO gtest4 VALUES (1), (6); -SELECT * FROM gtest4; - a | b ----+--------- - 1 | (2,3) - 6 | (12,18) -(2 rows) - -DROP TABLE gtest4; +); -- fails, user-defined type +ERROR: virtual generated column "b" cannot have a user-defined type +DETAIL: Virtual generated columns that make use of user-defined types are not yet supported. +--INSERT INTO gtest4 VALUES (1), (6); +--SELECT * FROM gtest4; +--DROP TABLE gtest4; DROP TYPE double_int; -- using tableoid is allowed CREATE TABLE gtest_tableoid ( @@ -604,9 +615,13 @@ INSERT INTO gtest11 VALUES (1, 10), (2, 20); GRANT SELECT (a, c) ON gtest11 TO regress_user11; CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE SQL; REVOKE ALL ON FUNCTION gf1(int) FROM PUBLIC; -CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL); -INSERT INTO gtest12 VALUES (1, 10), (2, 20); -GRANT SELECT (a, c), INSERT ON gtest12 TO regress_user11; +CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL); -- fails, user-defined function +ERROR: generation expression uses user-defined function +LINE 1: ...nt PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VI... + ^ +DETAIL: Virtual generated columns that make use of user-defined functions are not yet supported. +--INSERT INTO gtest12 VALUES (1, 10), (2, 20); +--GRANT SELECT (a, c), INSERT ON gtest12 TO regress_user11; SET ROLE regress_user11; SELECT a, b FROM gtest11; -- not allowed ERROR: permission denied for table gtest11 @@ -619,15 +634,12 @@ SELECT a, c FROM gtest11; -- allowed SELECT gf1(10); -- not allowed ERROR: permission denied for function gf1 -INSERT INTO gtest12 VALUES (3, 30), (4, 40); -- allowed (does not actually invoke the function) -SELECT a, c FROM gtest12; -- currently not allowed because of function permissions, should arguably be allowed -ERROR: permission denied for function gf1 +--INSERT INTO gtest12 VALUES (3, 30), (4, 40); -- allowed (does not actually invoke the function) +--SELECT a, c FROM gtest12; -- currently not allowed because of function permissions, should arguably be allowed RESET ROLE; -DROP FUNCTION gf1(int); -- fail -ERROR: cannot drop function gf1(integer) because other objects depend on it -DETAIL: column c of table gtest12 depends on function gf1(integer) -HINT: Use DROP ... CASCADE to drop the dependent objects too. -DROP TABLE gtest11, gtest12; +--DROP FUNCTION gf1(int); -- fail +DROP TABLE gtest11; +--DROP TABLE gtest12; DROP FUNCTION gf1(int); DROP USER regress_user11; -- check constraints @@ -636,12 +648,30 @@ INSERT INTO gtest20 (a) VALUES (10); -- ok INSERT INTO gtest20 (a) VALUES (30); -- violates constraint ERROR: new row for relation "gtest20" violates check constraint "gtest20_b_check" DETAIL: Failing row contains (30, virtual). -ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100); -- violates constraint (currently not supported) -ERROR: ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints -DETAIL: Column "b" of relation "gtest20" is a virtual generated column. -ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3); -- ok (currently not supported) -ERROR: ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables with check constraints -DETAIL: Column "b" of relation "gtest20" is a virtual generated column. +ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100); -- violates constraint +ERROR: check constraint "gtest20_b_check" of relation "gtest20" is violated by some row +ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3); -- ok +-- table rewrite should not happen +SELECT pg_relation_filenode('gtest20') AS gtest20_filenode \gset +ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 4), ADD COLUMN c INT DEFAULT 11; +SELECT pg_relation_filenode('gtest20') = :gtest20_filenode AS is_same_file; + is_same_file +-------------- + t +(1 row) + +\d gtest20 + Table "generated_virtual_tests.gtest20" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+----------------------------- + a | integer | | not null | + b | integer | | | generated always as (a * 4) + c | integer | | | 11 +Indexes: + "gtest20_pkey" PRIMARY KEY, btree (a) +Check constraints: + "gtest20_b_check" CHECK (b < 50) + CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); INSERT INTO gtest20a (a) VALUES (10); INSERT INTO gtest20a (a) VALUES (30); @@ -800,6 +830,12 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A ERROR: virtual generated column "b" cannot have a domain type --INSERT INTO gtest24r (a) VALUES (4); -- ok --INSERT INTO gtest24r (a) VALUES (6); -- error +CREATE TABLE gtest24at (a int PRIMARY KEY); +ALTER TABLE gtest24at ADD COLUMN b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL; -- error +ERROR: virtual generated column "b" cannot have a domain type +CREATE TABLE gtest24ata (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); +ALTER TABLE gtest24ata ALTER COLUMN b TYPE gtestdomain1; -- error +ERROR: virtual generated column "b" cannot have a domain type CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL); CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) VIRTUAL); ERROR: virtual generated column "b" cannot have a domain type @@ -973,6 +1009,15 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3; gtest_child3 | 09-13-2016 | 1 | 4 (3 rows) +-- check constraint was validated based on each partitions's generation expression +ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 19); -- error +ERROR: check constraint "cc1" of relation "gtest_child" is violated by some row +ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 66); -- error +ERROR: check constraint "cc1" of relation "gtest_child2" is violated by some row +ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 <> 33); -- error +ERROR: check constraint "cc1" of relation "gtest_child3" is violated by some row +ALTER TABLE gtest_parent ADD CONSTRAINT cc CHECK (f3 < 67); -- ok +ALTER TABLE gtest_parent DROP CONSTRAINT cc; -- alter generation expression of parent and all its children altogether ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION AS (f2 * 2); \d gtest_parent @@ -1027,11 +1072,26 @@ ERROR: cannot use generated column in partition key LINE 1: ...NERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f3); ^ DETAIL: Column "f3" is a generated column. +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3)); +ERROR: cannot use generated column in partition key +LINE 1: ...RATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3)); + ^ +DETAIL: Column "f3" is a generated column. CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3 * 3)); ERROR: cannot use generated column in partition key LINE 1: ...D ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3 * 3)); ^ DETAIL: Column "f3" is a generated column. +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((gtest_part_key)); +ERROR: cannot use generated column in partition key +LINE 1: ...D ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((gtest_par... + ^ +DETAIL: Column "f3" is a generated column. +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((gtest_part_key is not null)); +ERROR: cannot use generated column in partition key +LINE 1: ...D ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((gtest_par... + ^ +DETAIL: Column "f3" is a generated column. -- ALTER TABLE ... ADD COLUMN CREATE TABLE gtest25 (a int PRIMARY KEY); INSERT INTO gtest25 VALUES (3), (4); @@ -1240,7 +1300,8 @@ Number of child tables: 1 (Use \d+ to list them.) --------+---------+-----------+----------+----------------------------- a | integer | | | b | integer | | | generated always as (a * 2) -Inherits: gtest30 +Inherits: + gtest30 DROP TABLE gtest30 CASCADE; NOTICE: drop cascades to table gtest30_1 @@ -1265,7 +1326,8 @@ Number of child tables: 1 (Use \d+ to list them.) --------+---------+-----------+----------+----------------------------- a | integer | | | b | integer | | | generated always as (a * 2) -Inherits: gtest30 +Inherits: + gtest30 ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error ERROR: cannot drop generation expression from inherited column @@ -1274,6 +1336,15 @@ CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c t CREATE TABLE gtest31_2 (x int, y gtest31_1); ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar; -- fails ERROR: cannot alter table "gtest31_1" because column "gtest31_2.y" uses its row type +-- bug #18970 +ALTER TABLE gtest31_2 ADD CONSTRAINT cc CHECK ((y).b IS NOT NULL); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello1'); +ALTER TABLE gtest31_2 DROP CONSTRAINT cc; +CREATE STATISTICS gtest31_2_stat ON ((y).b is not null) FROM gtest31_2; +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello2'); +DROP STATISTICS gtest31_2_stat; +CREATE INDEX gtest31_2_y_idx ON gtest31_2(((y).b)); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello3'); DROP TABLE gtest31_1, gtest31_2; -- Check it for a partitioned table, too CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text) PARTITION BY LIST (a); @@ -1455,6 +1526,40 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); c | integer | | | x | integer | | | generated always as (b * 2) +-- rule actions referring to generated columns: +-- NEW.b in a rule action should reflect the generated column's new value +CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); +CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); +CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); +CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); +INSERT INTO gtest_rule (a) VALUES (1); +UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET a = (SELECT max(b) FROM gtest_rule); +SELECT * FROM gtest_rule_log; + op | old_b | new_b +-----+-------+------- + INS | | 2 + UPD | 2 | 20 + UPD | 20 | 40 +(3 rows) + +DROP RULE gtest_rule_upd ON gtest_rule; +DROP RULE gtest_rule_ins ON gtest_rule; +DROP TABLE gtest_rule_log; +-- rule quals referring to generated columns: +-- NEW.b in the rule qual should reflect the generated column's new value +CREATE RULE gtest_rule_qual AS ON UPDATE TO gtest_rule WHERE NEW.b > 100 + DO INSTEAD NOTHING; +UPDATE gtest_rule SET a = 100; +SELECT * FROM gtest_rule; + a | b +----+---- + 20 | 40 +(1 row) + +DROP TABLE gtest_rule; -- sanity check of system catalog SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v'); attrelid | attname | attgenerated @@ -1466,13 +1571,21 @@ SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT -- -- these tests are specific to generated_virtual.sql -- +-- using user-defined type not yet supported +CREATE TABLE gtest24xxx (a gtestdomain1, b gtestdomain1, c int GENERATED ALWAYS AS (greatest(a, b)) VIRTUAL); -- error +ERROR: generation expression uses user-defined type +LINE 1: ...main1, b gtestdomain1, c int GENERATED ALWAYS AS (greatest(a... + ^ +DETAIL: Virtual generated columns that make use of user-defined types are not yet supported. create table gtest32 ( a int primary key, b int generated always as (a * 2), c int generated always as (10 + 10), - d int generated always as (coalesce(a, 100)) + d int generated always as (coalesce(f, 100)), + e int, + f int ); -insert into gtest32 values (1), (2); +insert into gtest32 (a, f) values (1, 1), (2, 2); analyze gtest32; -- Ensure that nullingrel bits are propagated into the generation expressions explain (costs off) @@ -1531,11 +1644,11 @@ where coalesce(t2.b, 1) = 2; explain (costs off) select t1.a from gtest32 t1 left join gtest32 t2 on t1.a = t2.a where coalesce(t2.b, 1) = 2 or t1.a is null; - QUERY PLAN -------------------------------------------------------------- + QUERY PLAN +----------------------------------------- Hash Left Join Hash Cond: (t1.a = t2.a) - Filter: ((COALESCE((t2.a * 2), 1) = 2) OR (t1.a IS NULL)) + Filter: (COALESCE((t2.a * 2), 1) = 2) -> Seq Scan on gtest32 t1 -> Hash -> Seq Scan on gtest32 t2 @@ -1551,44 +1664,174 @@ where coalesce(t2.b, 1) = 2 or t1.a is null; -- Ensure that the generation expressions are wrapped into PHVs if needed explain (verbose, costs off) select t2.* from gtest32 t1 left join gtest32 t2 on false; - QUERY PLAN ------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------- Nested Loop Left Join - Output: a, (a * 2), (20), (COALESCE(a, 100)) + Output: t2.a, (t2.a * 2), (20), (COALESCE(t2.f, 100)), t2.e, t2.f Join Filter: false -> Seq Scan on generated_virtual_tests.gtest32 t1 - Output: t1.a, t1.b, t1.c, t1.d + Output: t1.a, t1.b, t1.c, t1.d, t1.e, t1.f -> Result - Output: a, 20, COALESCE(a, 100) + Output: t2.a, t2.e, t2.f, 20, COALESCE(t2.f, 100) + Replaces: Scan on t2 One-Time Filter: false -(8 rows) +(9 rows) select t2.* from gtest32 t1 left join gtest32 t2 on false; - a | b | c | d ----+---+---+--- - | | | - | | | + a | b | c | d | e | f +---+---+---+---+---+--- + | | | | | + | | | | | (2 rows) explain (verbose, costs off) -select * from gtest32 t group by grouping sets (a, b, c, d) having c = 20; - QUERY PLAN ------------------------------------------------------ +select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20; + QUERY PLAN +-------------------------------------------------------- HashAggregate - Output: a, ((a * 2)), (20), (COALESCE(a, 100)) + Output: a, ((a * 2)), (20), (COALESCE(f, 100)), e, f Hash Key: t.a Hash Key: (t.a * 2) Hash Key: 20 - Hash Key: COALESCE(t.a, 100) + Hash Key: COALESCE(t.f, 100) + Hash Key: t.e + Hash Key: t.f Filter: ((20) = 20) -> Seq Scan on generated_virtual_tests.gtest32 t - Output: a, (a * 2), 20, COALESCE(a, 100) -(9 rows) + Output: a, (a * 2), 20, COALESCE(f, 100), e, f +(11 rows) + +select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20; + a | b | c | d | e | f +---+---+----+---+---+--- + | | 20 | | | +(1 row) + +-- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded +alter table gtest32 alter column e type bigint using b; +-- Ensure that virtual generated column references within SubLinks that should +-- be transformed into joins can get expanded +explain (costs off) +select 1 from gtest32 t1 where exists + (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2); + QUERY PLAN +------------------------------------- + Nested Loop Semi Join + Join Filter: (t1.a > t2.a) + -> Seq Scan on gtest32 t1 + -> Materialize + -> Seq Scan on gtest32 t2 + Filter: ((a * 2) = 2) +(6 rows) -select * from gtest32 t group by grouping sets (a, b, c, d) having c = 20; - a | b | c | d ----+---+----+--- - | | 20 | +select 1 from gtest32 t1 where exists + (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2); + ?column? +---------- + 1 (1 row) drop table gtest32; +-- Ensure that virtual generated columns in constraint expressions are expanded +create table gtest33 (a int, b int generated always as (a * 2) virtual not null, check (b > 10)); +set constraint_exclusion to on; +-- should get a dummy Result, not a seq scan +explain (costs off) +select * from gtest33 where b < 10; + QUERY PLAN +----------------------------- + Result + Replaces: Scan on gtest33 + One-Time Filter: false +(3 rows) + +-- should get a dummy Result, not a seq scan +explain (costs off) +select * from gtest33 where b is null; + QUERY PLAN +----------------------------- + Result + Replaces: Scan on gtest33 + One-Time Filter: false +(3 rows) + +reset constraint_exclusion; +drop table gtest33; +-- Ensure that EXCLUDED. in INSERT ... ON CONFLICT +-- DO UPDATE is expanded to the generation expression, both for plain and +-- partitioned target relations. +create table gtest34 (id int primary key, a int, + c int generated always as (a * 10) virtual); +insert into gtest34 values (1, 5); +insert into gtest34 values (1, 7) + on conflict (id) do update set a = excluded.c returning *; + id | a | c +----+----+----- + 1 | 70 | 700 +(1 row) + +insert into gtest34 values (1, 2) + on conflict (id) do update set a = gtest34.c + excluded.c returning *; + id | a | c +----+-----+------ + 1 | 720 | 7200 +(1 row) + +insert into gtest34 values (1, 3) + on conflict (id) do update set a = 999 where excluded.c > 20 returning *; + id | a | c +----+-----+------ + 1 | 999 | 9990 +(1 row) + +drop table gtest34; +create table gtest34p (id int primary key, a int, + c int generated always as (a * 10) virtual) + partition by range (id); +create table gtest34p_1 partition of gtest34p for values from (1) to (100); +insert into gtest34p values (1, 5); +insert into gtest34p values (1, 7) + on conflict (id) do update set a = excluded.c returning *; + id | a | c +----+----+----- + 1 | 70 | 700 +(1 row) + +insert into gtest34p values (1, 2) + on conflict (id) do update set a = gtest34p.c + excluded.c returning *; + id | a | c +----+-----+------ + 1 | 720 | 7200 +(1 row) + +drop table gtest34p; +-- Ensure that virtual generated columns work with WHERE CURRENT OF +create table gtest_cursor (id int primary key, a int, b int generated always as (a * 2) virtual); +insert into gtest_cursor values (1, 10), (2, 20), (3, 30); +begin; +declare curs cursor for select * from gtest_cursor order by id for update; +fetch 1 from curs; + id | a | b +----+----+---- + 1 | 10 | 20 +(1 row) + +update gtest_cursor set a = 99 where current of curs; +select * from gtest_cursor order by id; + id | a | b +----+----+----- + 1 | 99 | 198 + 2 | 20 | 40 + 3 | 30 | 60 +(3 rows) + +delete from gtest_cursor where current of curs; +select * from gtest_cursor order by id; + id | a | b +----+----+---- + 2 | 20 | 40 + 3 | 30 | 60 +(2 rows) + +commit; +drop table gtest_cursor; diff --git a/src/test/regress/expected/geometry.out b/src/test/regress/expected/geometry.out index 8be694f46be1d..1d168b21cbca0 100644 --- a/src/test/regress/expected/geometry.out +++ b/src/test/regress/expected/geometry.out @@ -1777,7 +1777,8 @@ SELECT p.f1, l.s, l.s # p.f1 AS intersection ERROR: operator does not exist: lseg # point LINE 1: SELECT p.f1, l.s, l.s # p.f1 AS intersection ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- Length SELECT s, @-@ s FROM LSEG_TBL; s | ?column? diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out new file mode 100644 index 0000000000000..b579e3df63513 --- /dev/null +++ b/src/test/regress/expected/graph_table.out @@ -0,0 +1,1025 @@ +CREATE SCHEMA graph_table_tests; +GRANT USAGE ON SCHEMA graph_table_tests TO PUBLIC; +SET search_path = graph_table_tests; +CREATE TABLE products ( + product_no integer PRIMARY KEY, + name varchar, + price numeric +); +CREATE TABLE customers ( + customer_id integer PRIMARY KEY, + name varchar, + address varchar +); +CREATE TABLE orders ( + order_id integer PRIMARY KEY, + ordered_when date +); +CREATE TABLE order_items ( + order_items_id integer PRIMARY KEY, + order_id integer REFERENCES orders (order_id), + product_no integer REFERENCES products (product_no), + quantity integer +); +CREATE TABLE customer_orders ( + customer_orders_id integer PRIMARY KEY, + customer_id integer REFERENCES customers (customer_id), + order_id integer REFERENCES orders (order_id) +); +CREATE TABLE wishlists ( + wishlist_id integer PRIMARY KEY, + wishlist_name varchar +); +CREATE TABLE wishlist_items ( + wishlist_items_id integer PRIMARY KEY, + wishlist_id integer REFERENCES wishlists (wishlist_id), + product_no integer REFERENCES products (product_no) +); +CREATE TABLE customer_wishlists ( + customer_wishlist_id integer PRIMARY KEY, + customer_id integer REFERENCES customers (customer_id), + wishlist_id integer REFERENCES wishlists (wishlist_id) +); +CREATE PROPERTY GRAPH myshop + VERTEX TABLES ( + products, + customers, + orders + DEFAULT LABEL + LABEL lists PROPERTIES (order_id AS node_id, 'order'::varchar(10) AS list_type), + wishlists + DEFAULT LABEL + LABEL lists PROPERTIES (wishlist_id AS node_id, 'wishlist'::varchar(10) AS list_type) + ) + EDGE TABLES ( + order_items KEY (order_items_id) + SOURCE KEY (order_id) REFERENCES orders (order_id) + DESTINATION KEY (product_no) REFERENCES products (product_no) + DEFAULT LABEL + LABEL list_items PROPERTIES (order_id AS link_id, product_no), + wishlist_items KEY (wishlist_items_id) + SOURCE KEY (wishlist_id) REFERENCES wishlists (wishlist_id) + DESTINATION KEY (product_no) REFERENCES products (product_no) + DEFAULT LABEL + LABEL list_items PROPERTIES (wishlist_id AS link_id, product_no), + customer_orders KEY (customer_orders_id) + SOURCE KEY (customer_id) REFERENCES customers (customer_id) + DESTINATION KEY (order_id) REFERENCES orders (order_id) + DEFAULT LABEL + LABEL cust_lists PROPERTIES (customer_id, order_id AS link_id), + customer_wishlists KEY (customer_wishlist_id) + SOURCE KEY (customer_id) REFERENCES customers (customer_id) + DESTINATION KEY (wishlist_id) REFERENCES wishlists (wishlist_id) + DEFAULT LABEL + LABEL cust_lists PROPERTIES (customer_id, wishlist_id AS link_id) + ); +SELECT customer_name FROM GRAPH_TABLE (xxx MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name)); -- error +ERROR: relation "xxx" does not exist +LINE 1: SELECT customer_name FROM GRAPH_TABLE (xxx MATCH (c IS custo... + ^ +SELECT customer_name FROM GRAPH_TABLE (pg_class MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name)); -- error +ERROR: "pg_class" is not a property graph +LINE 1: SELECT customer_name FROM GRAPH_TABLE (pg_class MATCH (c IS ... + ^ +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (cx.name AS customer_name)); -- error +ERROR: missing FROM-clause entry for table "cx" +LINE 1: ...US')-[IS customer_orders]->(o IS orders) COLUMNS (cx.name AS... + ^ +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.namex AS customer_name)); -- error +ERROR: property "namex" does not exist +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers|employees WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name)); -- error +ERROR: label "employees" does not exist in property graph "myshop" +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders] COLUMNS (c.name AS customer_name)); -- error +ERROR: syntax error at or near "COLUMNS" +LINE 1: ...mers WHERE c.address = 'US')-[IS customer_orders] COLUMNS (c... + ^ +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers), (o IS orders) COLUMNS (c.name AS customer_name)); -- error +ERROR: multiple path patterns in one GRAPH_TABLE clause not supported +SELECT * FROM GRAPH_TABLE (myshop MATCH COLUMNS (1 AS col)); -- error, empty match clause +ERROR: syntax error at or near "COLUMNS" +LINE 1: SELECT * FROM GRAPH_TABLE (myshop MATCH COLUMNS (1 AS col)); + ^ +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)->{1,2}(o IS orders) COLUMNS (c.name AS customer_name)); -- error +ERROR: element pattern quantifier is not supported +SELECT * FROM GRAPH_TABLE (myshop MATCH ((c IS customers)->(o IS orders)) COLUMNS (c.name)); +ERROR: unsupported element pattern kind: "nested path pattern" +LINE 1: SELECT * FROM GRAPH_TABLE (myshop MATCH ((c IS customers)->(... + ^ +-- a property graph can be referenced only from within GRAPH_TABLE clause. +SELECT * FROM myshop; -- error +ERROR: cannot open relation "myshop" +LINE 1: SELECT * FROM myshop; + ^ +DETAIL: This operation is not supported for property graphs. +COPY myshop TO stdout; -- error +ERROR: cannot open relation "myshop" +DETAIL: This operation is not supported for property graphs. +INSERT INTO myshop VALUES (1); -- error +ERROR: cannot open relation "myshop" +LINE 1: INSERT INTO myshop VALUES (1); + ^ +DETAIL: This operation is not supported for property graphs. +INSERT INTO products VALUES + (1, 'product1', 10), + (2, 'product2', 20), + (3, 'product3', 30); +INSERT INTO customers VALUES + (1, 'customer1', 'US'), + (2, 'customer2', 'CA'), + (3, 'customer3', 'GL'); +INSERT INTO orders VALUES + (1, date '2024-01-01'), + (2, date '2024-01-02'), + (3, date '2024-01-03'); +INSERT INTO wishlists VALUES + (1, 'wishlist1'), + (2, 'wishlist2'), + (3, 'wishlist3'); +INSERT INTO order_items (order_items_id, order_id, product_no, quantity) VALUES + (1, 1, 1, 5), + (2, 1, 2, 10), + (3, 2, 1, 7); +INSERT INTO customer_orders (customer_orders_id, customer_id, order_id) VALUES + (1, 1, 1), + (2, 2, 2); +INSERT INTO customer_wishlists (customer_wishlist_id, customer_id, wishlist_id) VALUES + (1, 2, 3), + (2, 3, 1), + (3, 3, 2); +INSERT INTO wishlist_items (wishlist_items_id, wishlist_id, product_no) VALUES + (1, 1, 2), + (2, 1, 3), + (3, 2, 1), + (4, 3, 1); +-- single element path pattern +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (c.name)); + name +----------- + customer1 + customer2 + customer3 +(3 rows) + +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name)); + name +----------- + customer1 +(1 row) + +-- graph element specification without label or variable +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[]->(o IS orders) COLUMNS (c.name AS customer_name)); + customer_name +--------------- + customer1 +(1 row) + +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[co IS customer_orders]->(o IS orders WHERE o.ordered_when = date '2024-01-02') COLUMNS (c.name, c.address)); + name | address +-----------+--------- + customer2 | CA +(1 row) + +SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)-[IS customer_orders]->(c IS customers) COLUMNS (c.name, o.ordered_when)); + name | ordered_when +------+-------------- +(0 rows) + +SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)<-[IS customer_orders]-(c IS customers) COLUMNS (c.name, o.ordered_when)); + name | ordered_when +-----------+-------------- + customer1 | 01-01-2024 + customer2 | 01-02-2024 +(2 rows) + +-- spaces around pattern operators +SELECT * FROM GRAPH_TABLE (myshop MATCH ( o IS orders ) <- [ IS customer_orders ] - (c IS customers) COLUMNS ( c.name, o.ordered_when)); + name | ordered_when +-----------+-------------- + customer1 | 01-01-2024 + customer2 | 01-02-2024 +(2 rows) + +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS cust_lists]->(l IS lists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name, l.list_type)) ORDER BY customer_name, product_name, list_type; + customer_name | product_name | list_type +---------------+--------------+----------- + customer1 | product1 | order + customer1 | product2 | order + customer2 | product1 | order + customer2 | product1 | wishlist + customer3 | product1 | wishlist + customer3 | product2 | wishlist + customer3 | product3 | wishlist +(7 rows) + +-- label disjunction +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders | customer_wishlists ]->(l IS orders | wishlists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name)) ORDER BY customer_name, product_name; + customer_name | product_name +---------------+-------------- + customer1 | product1 + customer1 | product2 + customer2 | product1 + customer2 | product1 + customer3 | product1 + customer3 | product2 + customer3 | product3 +(7 rows) + +-- property not associated with labels queried results in error +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders | customer_wishlists ]->(l IS orders | wishlists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name, l.list_type)) ORDER BY 1, 2, 3; +ERROR: property "list_type" for element variable "l" not found +-- vertex to vertex connection abbreviation +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)->(o IS orders) COLUMNS (c.name, o.ordered_when)) ORDER BY 1; + name | ordered_when +-----------+-------------- + customer1 | 01-01-2024 + customer2 | 01-02-2024 +(2 rows) + +-- lateral test +-- Use table with a column name same as a property in the property graph so as +-- to test resolution preferences. Property references are preferred over +-- lateral table references. +CREATE TABLE x1 (a int, address text); +INSERT INTO x1 VALUES (1, 'one'), (2, 'two'); +SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid)); + a | address | customer_name | cid +---+---------+---------------+----- + 1 | one | customer1 | 1 +(1 row) + +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; + a | customer_name | cid | order_id +---+---------------+-----+---------- + 1 | customer1 | 1 | 1 + 2 | customer1 | 1 | 1 +(2 rows) + +-- non-local property references are not allowed, even if a lateral column +-- reference is available +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers)-[IS customer_orders]->(o IS orders WHERE o.order_id = x1.a) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; -- error +ERROR: non-local element variable reference is not supported +LINE 1: ...customer_orders]->(o IS orders WHERE o.order_id = x1.a) COLU... + ^ +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(x1 IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid, x1.order_id)) g; -- error +ERROR: non-local element variable reference is not supported +LINE 1: ...tomers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS ... + ^ +DROP TABLE x1; +CREATE TABLE v1 ( + id int PRIMARY KEY, + vname varchar(10), + vprop1 int, + vprop2 int +); +CREATE TABLE v2 ( + id1 int, + id2 int, + vname varchar(10), + vprop1 int, + vprop2 int +); +CREATE TABLE v3 ( + id int PRIMARY KEY, + vname varchar(10), + vprop1 int, + vprop2 int +); +-- edge connecting v1 and v2 +CREATE TABLE e1_2 ( + id_1 int, + id_2_1 int, + id_2_2 int, + ename varchar(10), + eprop1 int +); +-- edge connecting v1 and v3 +CREATE TABLE e1_3 ( + id_1 int, + id_3 int, + ename varchar(10), + eprop1 int, + PRIMARY KEY (id_1, id_3) +); +CREATE TABLE e2_3 ( + id_2_1 int, + id_2_2 int, + id_3 int, + ename varchar(10), + eprop1 int +); +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 + LABEL vl1 PROPERTIES (vname, vprop1) + LABEL l1 PROPERTIES (vname AS elname), -- label shared by vertexes as well as edges + v2 KEY (id1, id2) + LABEL vl2 PROPERTIES (vname, vprop2, 'vl2_prop'::varchar(10) AS lprop1) + LABEL vl3 PROPERTIES (vname, vprop1, 'vl2_prop'::varchar(10) AS lprop1) + LABEL l1 PROPERTIES (vname AS elname), + v3 + LABEL vl3 PROPERTIES (vname, vprop1, 'vl3_prop'::varchar(10) AS lprop1) + LABEL l1 PROPERTIES (vname AS elname) + ) + -- edges with differing number of columns in destination keys + EDGE TABLES ( + e1_2 key (id_1, id_2_1, id_2_2) + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL el1 PROPERTIES (eprop1, ename) + LABEL l1 PROPERTIES (ename AS elname), + e1_3 + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_3) REFERENCES v3 (id) + -- order of property names doesn't matter + LABEL el1 PROPERTIES (ename, eprop1) + LABEL l1 PROPERTIES (ename AS elname), + e2_3 key (id_2_1, id_2_2, id_3) + SOURCE KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + DESTINATION KEY (id_3) REFERENCES v3 (id) + -- new property lprop2 not shared by el1 + -- does not share eprop1 from by el1 + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); +INSERT INTO v1 VALUES + (1, 'v11', 10, 100), + (2, 'v12', 20, 200), + (3, 'v13', 30, 300); +INSERT INTO v2 VALUES + (1000, 1, 'v21', 1010, 1100), + (1000, 2, 'v22', 1020, 1200), + (1000, 3, 'v23', 1030, 1300); +INSERT INTO v3 VALUES + (2001, 'v31', 2010, 2100), + (2002, 'v32', 2020, 2200), + (2003, 'v33', 2030, 2300); +INSERT INTO e1_2 VALUES + (1, 1000, 2, 'e121', 10001), + (2, 1000, 1, 'e122', 10002); +INSERT INTO e1_3 VALUES + (1, 2003, 'e131', 10003), + (1, 2001, 'e132', 10004); +INSERT INTO e2_3 VALUES (1000, 2, 2002, 'e231', 10005); +-- empty element path pattern, counts number of edges in the graph +SELECT count(*) FROM GRAPH_TABLE (g1 MATCH ()-[]->() COLUMNS (1 AS one)); + count +------- + 5 +(1 row) + +SELECT count(*) FROM GRAPH_TABLE (g1 MATCH ()->() COLUMNS (1 AS one)); + count +------- + 5 +(1 row) + +-- Project property associated with a label specified in the graph pattern even +-- if it is defined for a graph element through a different label. (Refer +-- section 6.5 of SQL/PGQ standard). For example, vprop1 in the query below. It +-- is defined on v2 through label vl3, but gets exposed in the query through +-- label vl1 which is not associated with v2. v2, in turn, is included because +-- of label vl2. +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1 | vl2) COLUMNS (a.vname, a.vprop1)); + vname | vprop1 +-------+-------- + v11 | 10 + v12 | 20 + v13 | 30 + v21 | 1010 + v22 | 1020 + v23 | 1030 +(6 rows) + +-- vprop2 is associated with vl2 but not vl3 +SELECT src, conn, dest, lprop1, vprop2, vprop1 FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[b IS el1]->(c IS vl2 | vl3) COLUMNS (a.vname AS src, b.ename AS conn, c.vname AS dest, c.lprop1, c.vprop2, c.vprop1)); + src | conn | dest | lprop1 | vprop2 | vprop1 +-----+------+------+----------+--------+-------- + v12 | e122 | v21 | vl2_prop | 1100 | 1010 + v11 | e121 | v22 | vl2_prop | 1200 | 1020 + v11 | e131 | v33 | vl3_prop | | 2030 + v11 | e132 | v31 | vl3_prop | | 2010 +(4 rows) + +-- edges directed in both ways - to and from v2 +SELECT * FROM GRAPH_TABLE (g1 MATCH (v1 IS vl2)-[conn]-(v2) COLUMNS (v1.vname AS v1name, conn.ename AS cname, v2.vname AS v2name)); + v1name | cname | v2name +--------+-------+-------- + v21 | e122 | v12 + v22 | e121 | v11 + v22 | e231 | v32 +(3 rows) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (v1 IS vl2)-(v2) COLUMNS (v1.vname AS v1name, v2.vname AS v2name)); + v1name | v2name +--------+-------- + v21 | v12 + v22 | v11 + v22 | v32 +(3 rows) + +-- Errors +-- vl1 is not associated with property vprop2 +SELECT src, src_vprop2, conn, dest FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[b IS el1]->(c IS vl2 | vl3) COLUMNS (a.vname AS src, a.vprop2 AS src_vprop2, b.ename AS conn, c.vname AS dest)); +ERROR: property "vprop2" for element variable "a" not found +-- property ename is associated with edge labels but not with a vertex label +SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svname, src.ename AS sename)); +ERROR: property "ename" for element variable "src" not found +-- vname is associated vertex labels but not with an edge label +SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (conn.vname AS cvname, conn.ename AS cename)); +ERROR: property "vname" for element variable "conn" not found +-- el1 is associated with only edges, and cannot qualify a vertex +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS el1)-[conn]->(dest) COLUMNS (conn.ename AS cename)); +ERROR: no property graph element of type "vertex" has label "el1" associated with it in property graph "g1" +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS el1 | vl1)-[conn]->(dest) COLUMNS (conn.ename AS cename)); +ERROR: no property graph element of type "vertex" has label "el1" associated with it in property graph "g1" +-- star in COLUMNs is specified but not supported +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.*)); +ERROR: "*" is not supported here +LINE 1: ... = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.*)); + ^ +-- star anywhere else is not allowed as a property reference +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT NULL)-[IS customer_orders]->(o IS orders) COLUMNS (c.name)); +ERROR: "*" not allowed here +LINE 1: ...M GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT... + ^ +-- consecutive element patterns with same kind +SELECT * FROM GRAPH_TABLE (g1 MATCH ()() COLUMNS (1 as one)); +ERROR: adjacent vertex patterns are not supported +LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH ()() COLUMNS (1 as one))... + ^ +SELECT * FROM GRAPH_TABLE (g1 MATCH -> COLUMNS (1 AS one)); +ERROR: path pattern cannot start with an edge pattern +LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH -> COLUMNS (1 AS one)); + ^ +SELECT * FROM GRAPH_TABLE (g1 MATCH ()-[]- COLUMNS (1 AS one)); +ERROR: path pattern cannot end with an edge pattern +LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH ()-[]- COLUMNS (1 AS one... + ^ +SELECT * FROM GRAPH_TABLE (g1 MATCH ()-> ->() COLUMNS (1 AS one)); +ERROR: edge pattern must be preceded by a vertex pattern +LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH ()-> ->() COLUMNS (1 AS ... + ^ +-- non-local element variable reference with element patterns without variable +-- names +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[WHERE a.vprop1 = 10]->(c) COLUMNS (a.vname AS aname, c.vname AS cname)); +ERROR: non-local element variable reference is not supported +LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[WHERE a.vprop1 = 10... + ^ +SELECT * FROM GRAPH_TABLE (g1 MATCH (WHERE b.eprop1 = 10001)-[b]->(c) COLUMNS (b.ename AS bname, c.vname AS cname)); +ERROR: non-local element variable reference is not supported +LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH (WHERE b.eprop1 = 10001)... + ^ +-- select all the properties across all the labels associated with a given type +-- of graph element +SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svname, conn.ename AS cename, dest.vname AS dvname, src.vprop1 AS svp1, src.vprop2 AS svp2, src.lprop1 AS slp1, dest.vprop1 AS dvp1, dest.vprop2 AS dvp2, dest.lprop1 AS dlp1, conn.eprop1 AS cep1, conn.lprop2 AS clp2)); + svname | cename | dvname | svp1 | svp2 | slp1 | dvp1 | dvp2 | dlp1 | cep1 | clp2 +--------+--------+--------+------+------+----------+------+------+----------+-------+-------- + v12 | e122 | v21 | 20 | | | 1010 | 1100 | vl2_prop | 10002 | + v11 | e121 | v22 | 10 | | | 1020 | 1200 | vl2_prop | 10001 | + v11 | e131 | v33 | 10 | | | 2030 | | vl3_prop | 10003 | + v11 | e132 | v31 | 10 | | | 2010 | | vl3_prop | 10004 | + v22 | e231 | v32 | 1020 | 1200 | vl2_prop | 2020 | | vl3_prop | | 100050 +(5 rows) + +-- three label disjunction +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1 | vl2 | vl3)-[conn]->(dest) COLUMNS (src.vname AS svname, conn.ename AS cename, dest.vname AS dvname)); + svname | cename | dvname +--------+--------+-------- + v12 | e122 | v21 + v11 | e121 | v22 + v11 | e131 | v33 + v11 | e132 | v31 + v22 | e231 | v32 +(5 rows) + +-- graph'ical query: find a vertex which is not connected to any other vertex as a source or a destination. +WITH all_connected_vertices AS (SELECT svn, dvn FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svn, dest.vname AS dvn))), + all_vertices AS (SELECT vn FROM GRAPH_TABLE (g1 MATCH (vertex) COLUMNS (vertex.vname AS vn))) +SELECT vn FROM all_vertices EXCEPT (SELECT svn FROM all_connected_vertices UNION SELECT dvn FROM all_connected_vertices) ORDER BY vn; + vn +----- + v13 + v23 +(2 rows) + +-- query all connections using a label shared by vertices and edges +SELECT sn, cn, dn FROM GRAPH_TABLE (g1 MATCH (src IS l1)-[conn IS l1]->(dest IS l1) COLUMNS (src.elname AS sn, conn.elname AS cn, dest.elname AS dn)); + sn | cn | dn +-----+------+----- + v12 | e122 | v21 + v11 | e121 | v22 + v11 | e131 | v33 + v11 | e132 | v31 + v22 | e231 | v32 +(5 rows) + +-- Tests for cyclic path patterns +CREATE TABLE e2_1 ( + id_2_1 int, + id_2_2 int, + id_1 int, + ename varchar(10), + eprop1 int +); +CREATE TABLE e3_2 ( + id_3 int, + id_2_1 int, + id_2_2 int, + ename varchar(10), + eprop1 int +); +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e2_1 KEY (id_2_1, id_2_2, id_1) + SOURCE KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + DESTINATION KEY (id_1) REFERENCES v1 (id) + LABEL el1 PROPERTIES (eprop1, ename) + LABEL l1 PROPERTIES (ename AS elname), + e3_2 KEY (id_3, id_2_1, id_2_2) + SOURCE KEY (id_3) REFERENCES v3 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); +INSERT INTO e1_2 VALUES (3, 1000, 3, 'e123', 10007); +INSERT INTO e2_1 VALUES (1000, 1, 2, 'e211', 10006); +INSERT INTO e2_1 VALUES (1000, 3, 3, 'e212', 10008); +INSERT INTO e3_2 VALUES (2002, 1000, 2, 'e321', 10009); +-- cyclic pattern using WHERE clause in graph pattern, +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(c) WHERE a.vname = c.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v12 | v21 | 20 | 1010 + v13 | v23 | 30 | 1030 + v21 | v12 | 1010 | 20 + v22 | v32 | 1020 | 2020 + v23 | v13 | 1030 | 30 + v32 | v22 | 2020 | 1020 +(6 rows) + +-- cyclic pattern using element patterns with the same variable name +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(a) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v12 | v21 | 20 | 1010 + v13 | v23 | 30 | 1030 + v21 | v12 | 1010 | 20 + v22 | v32 | 1020 | 2020 + v23 | v13 | 1030 | 30 + v32 | v22 | 2020 | 1020 +(6 rows) + +-- cyclic pattern with WHERE clauses in element patterns with the same variable name +SELECT * FROM GRAPH_TABLE (g1 MATCH (a WHERE a.vprop1 < 2000)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 > 20) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v13 | v23 | 30 | 1030 + v22 | v32 | 1020 | 2020 + v23 | v13 | 1030 | 30 +(3 rows) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 between 20 and 2000) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v12 | v21 | 20 | 1010 + v13 | v23 | 30 | 1030 + v22 | v32 | 1020 | 2020 + v23 | v13 | 1030 | 30 +(4 rows) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a WHERE a.vprop1 between 20 and 2000)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 between 20 and 2000) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v12 | v21 | 20 | 1010 + v13 | v23 | 30 | 1030 + v22 | v32 | 1020 | 2020 + v23 | v13 | 1030 | 30 +(4 rows) + +-- labels and elements kinds of element patterns with the same variable name +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS l1)-[a IS l1]->(b IS l1) COLUMNS (a.ename AS aename, b.ename AS bename)) ORDER BY 1, 2; -- error +ERROR: element patterns with same variable name "a" but different element pattern types +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a IS vl2) WHERE a.vname <> b.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; -- error +ERROR: element patterns with same variable name "a" but different label expressions are not supported +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v12 | v21 | 20 | 1010 + v13 | v23 | 30 | 1030 +(2 rows) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(a IS vl1) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + self | through | self_p1 | through_p1 +------+---------+---------+------------ + v12 | v21 | 20 | 1010 + v13 | v23 | 30 | 1030 +(2 rows) + +-- add loop to test edge patterns with same variable name +CREATE TABLE e3_3 ( + src_id int, + dest_id int, + ename varchar(10), + eprop1 int +); +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e3_3 KEY (src_id, dest_id) + SOURCE KEY (src_id) REFERENCES v3 (id) + DESTINATION KEY (dest_id) REFERENCES v3 (id) + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); +INSERT INTO e3_3 VALUES (2003, 2003, 'e331', 10010); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)); + self | loop_name +------+----------- + v33 | e331 +(1 row) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(c)-[b]->(d) COLUMNS (a.vname AS aname, b.ename AS bname, c.vname AS cname, d.vname AS dname)); --error +ERROR: an edge cannot connect more than two vertexes even in a cyclic pattern +-- the looping edge should be reported only once even when edge pattern with any direction is used +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[c]-(a) COLUMNS (a.vname AS self, c.ename AS loop_name)); + self | loop_name +------+----------- + v33 | e331 +(1 row) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-(a) COLUMNS (a.vname AS self)); + self +------ + v33 +(1 row) + +-- test collation specified in the expression +INSERT INTO e3_3 VALUES (2003, 2003, 'E331', 10011); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)) ORDER BY loop_name COLLATE "C" ASC; + self | loop_name +------+----------- + v33 | E331 + v33 | e331 +(2 rows) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b IS el2 WHERE b.ename > 'E331' COLLATE "C"]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)); + self | loop_name +------+----------- + v33 | e331 +(1 row) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) WHERE b.ename > 'E331' COLLATE "C" COLUMNS (a.vname AS self, b.ename AS loop_name)); + self | loop_name +------+----------- + v33 | e331 +(1 row) + +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)) WHERE loop_name > 'E331' COLLATE "C"; + self | loop_name +------+----------- + v33 | e331 +(1 row) + +-- property graph with some of the elements, labels and properties same as the +-- previous one. Test whether components from the specified property graph are +-- used. Also test explicit collation specification in property. +CREATE PROPERTY GRAPH g2 + VERTEX TABLES ( + v1 + LABEL l1 PROPERTIES ('g2.' || vname COLLATE "C" AS elname), + v2 KEY (id1, id2) + LABEL l1 PROPERTIES ('g2.' || vname COLLATE "C" AS elname), + v3 + LABEL l1 PROPERTIES ('g2.' || vname COLLATE "C" AS elname) + ) + EDGE TABLES ( + e1_2 key (id_1, id_2_1, id_2_2) + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname), + e1_3 + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_3) REFERENCES v3 (id) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname), + e2_3 KEY (id_2_1, id_2_2, id_3) + SOURCE KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + DESTINATION KEY (id_3) REFERENCES v3 (id) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname), + e3_3 KEY (src_id, dest_id) + SOURCE KEY (src_id) REFERENCES v3 (id) + DESTINATION KEY (src_id) REFERENCES v3 (id) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname) + ); +SELECT sn, cn, dn FROM GRAPH_TABLE (g2 MATCH (src IS l1)-[conn IS l1]->(dest IS l1) COLUMNS (src.elname AS sn, conn.elname AS cn, dest.elname AS dn)) ORDER BY 1, 2, 3; + sn | cn | dn +--------+---------+-------- + g2.v11 | g2.e121 | g2.v22 + g2.v11 | g2.e131 | g2.v33 + g2.v11 | g2.e132 | g2.v31 + g2.v12 | g2.e122 | g2.v21 + g2.v13 | g2.e123 | g2.v23 + g2.v22 | g2.e231 | g2.v32 + g2.v33 | g2.E331 | g2.v33 + g2.v33 | g2.e331 | g2.v33 +(8 rows) + +SELECT * FROM GRAPH_TABLE (g2 MATCH (a)-[b WHERE b.elname > 'g2.E331']->(a)-[b]->(a) COLUMNS (a.elname AS self, b.elname AS loop_name)); + self | loop_name +--------+----------- + g2.v33 | g2.e331 +(1 row) + +SELECT * FROM GRAPH_TABLE (g2 MATCH (a)-[b]->(a)-[b]->(a) WHERE b.elname > 'g2.E331' COLUMNS (a.elname AS self, b.elname AS loop_name)); + self | loop_name +--------+----------- + g2.v33 | g2.e331 +(1 row) + +SELECT * FROM GRAPH_TABLE (g2 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.elname AS self, b.elname AS loop_name)) WHERE loop_name > 'g2.E331'; + self | loop_name +--------+----------- + g2.v33 | g2.e331 +(1 row) + +-- prepared statements, any changes to the property graph should be reflected in +-- the already prepared statements +PREPARE cyclestmt AS SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS l1)->(b IS l1)->(c IS l1) WHERE a.elname = c.elname COLUMNS (a.elname AS self, b.elname AS through)) ORDER BY self, through; +EXECUTE cyclestmt; + self | through +------+--------- + v12 | v21 + v13 | v23 + v21 | v12 + v22 | v32 + v23 | v13 + v32 | v22 + v33 | v33 + v33 | v33 + v33 | v33 + v33 | v33 +(10 rows) + +ALTER PROPERTY GRAPH g1 DROP EDGE TABLES (e3_2, e3_3); +EXECUTE cyclestmt; + self | through +------+--------- + v12 | v21 + v13 | v23 + v21 | v12 + v23 | v13 +(4 rows) + +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e3_2 KEY (id_3, id_2_1, id_2_2) + SOURCE KEY (id_3) REFERENCES v3 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); +EXECUTE cyclestmt; + self | through +------+--------- + v12 | v21 + v13 | v23 + v21 | v12 + v22 | v32 + v23 | v13 + v32 | v22 +(6 rows) + +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v3 DROP LABEL l1; +EXECUTE cyclestmt; + self | through +------+--------- + v12 | v21 + v13 | v23 + v21 | v12 + v23 | v13 +(4 rows) + +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v3 ADD LABEL l1 PROPERTIES (vname AS elname); +EXECUTE cyclestmt; + self | through +------+--------- + v12 | v21 + v13 | v23 + v21 | v12 + v22 | v32 + v23 | v13 + v32 | v22 +(6 rows) + +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e3_3 KEY (src_id, dest_id) + SOURCE KEY (src_id) REFERENCES v3 (id) + DESTINATION KEY (src_id) REFERENCES v3 (id) + LABEL l2 PROPERTIES (ename AS elname) + ); +PREPARE loopstmt AS SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[e IS l2]->(a) COLUMNS (e.elname AS loop)) ORDER BY loop COLLATE "C" ASC; +EXECUTE loopstmt; + loop +------ + E331 + e331 +(2 rows) + +ALTER PROPERTY GRAPH g1 ALTER EDGE TABLE e3_3 ALTER LABEL l2 DROP PROPERTIES (elname); +EXECUTE loopstmt; -- error +ERROR: property "elname" for element variable "e" not found +ALTER PROPERTY GRAPH g1 ALTER EDGE TABLE e3_3 ALTER LABEL l2 ADD PROPERTIES ((ename || '_new')::varchar(10) AS elname); +EXECUTE loopstmt; + loop +---------- + E331_new + e331_new +(2 rows) + +-- inheritance and partitioning +CREATE TABLE pv (id int, val int); +CREATE TABLE cv1 () INHERITS (pv); +CREATE TABLE cv2 () INHERITS (pv); +INSERT INTO pv VALUES (1, 10); +INSERT INTO cv1 VALUES (2, 20); +INSERT INTO cv2 VALUES (3, 30); +CREATE TABLE pe (id int, src int, dest int, val int); +CREATE TABLE ce1 () INHERITS (pe); +CREATE TABLE ce2 () INHERITS (pe); +INSERT INTO pe VALUES (1, 1, 2, 100); +INSERT INTO ce1 VALUES (2, 2, 3, 200); +INSERT INTO ce2 VALUES (3, 3, 1, 300); +CREATE PROPERTY GRAPH g3 + NODE TABLES ( + pv KEY (id) + ) + RELATIONSHIP TABLES ( + pe KEY (id) + SOURCE KEY(src) REFERENCES pv(id) + DESTINATION KEY(dest) REFERENCES pv(id) + ); +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 10 | 100 | 20 + 20 | 200 | 30 + 30 | 300 | 10 +(3 rows) + +-- temporary property graph +CREATE TEMPORARY PROPERTY GRAPH gtmp + VERTEX TABLES ( + pv KEY (id) + ) + EDGE TABLES ( + pe KEY (id) + SOURCE KEY(src) REFERENCES pv(id) + DESTINATION KEY(dest) REFERENCES pv(id) + ); +SELECT * FROM GRAPH_TABLE (gtmp MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 10 | 100 | 20 + 20 | 200 | 30 + 30 | 300 | 10 +(3 rows) + +CREATE TABLE ptnv (id int PRIMARY KEY, val int) PARTITION BY LIST(id); +CREATE TABLE prtv1 PARTITION OF ptnv FOR VALUES IN (1, 2); +CREATE TABLE prtv2 PARTITION OF ptnv FOR VALUES IN (3); +INSERT INTO ptnv VALUES (1, 10), (2, 20), (3, 30); +CREATE TABLE ptne (id int PRIMARY KEY, src int REFERENCES ptnv(id), dest int REFERENCES ptnv(id), val int) PARTITION BY LIST(id); +CREATE TABLE ptne1 PARTITION OF ptne FOR VALUES IN (1, 2); +CREATE TABLE ptne2 PARTITION OF ptne FOR VALUES IN (3); +INSERT INTO ptne VALUES (1, 1, 2, 100), (2, 2, 3, 200), (3, 3, 1, 300); +CREATE PROPERTY GRAPH g4 + VERTEX TABLES (ptnv) + EDGE TABLES ( + ptne + SOURCE KEY (src) REFERENCES ptnv(id) + DESTINATION KEY (dest) REFERENCES ptnv(id) + ); +SELECT * FROM GRAPH_TABLE (g4 MATCH (s IS ptnv)-[e IS ptne]->(d IS ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 10 | 100 | 20 + 20 | 200 | 30 + 30 | 300 | 10 +(3 rows) + +-- edges from the same vertex in both directions connecting to other vertexes in the same table +SELECT * FROM GRAPH_TABLE (g4 MATCH (s)-[e]-(d) WHERE s.id = 3 COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 30 | 200 | 20 + 30 | 300 | 10 +(2 rows) + +SELECT * FROM GRAPH_TABLE (g4 MATCH (s WHERE s.id = 3)-[e]-(d) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + val | val | val +-----+-----+----- + 30 | 200 | 20 + 30 | 300 | 10 +(2 rows) + +-- ruleutils reverse parsing +CREATE VIEW customers_us AS SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders | customer_wishlists ]->(l IS orders | wishlists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name)) ORDER BY customer_name, product_name; +SELECT pg_get_viewdef('customers_us'::regclass); + pg_get_viewdef +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + SELECT customer_name, + + product_name + + FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE ((c.address)::text = 'US'::text))-[IS customer_orders|customer_wishlists]->(l IS orders|wishlists)-[IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name))+ + ORDER BY customer_name, product_name; +(1 row) + +-- test view/graph nesting +CREATE VIEW customers_view AS SELECT customer_id, 'redacted' || customer_id AS name_redacted, address FROM customers; +SELECT * FROM customers; + customer_id | name | address +-------------+-----------+--------- + 1 | customer1 | US + 2 | customer2 | CA + 3 | customer3 | GL +(3 rows) + +SELECT * FROM customers_view; + customer_id | name_redacted | address +-------------+---------------+--------- + 1 | redacted1 | US + 2 | redacted2 | CA + 3 | redacted3 | GL +(3 rows) + +CREATE PROPERTY GRAPH myshop2 + VERTEX TABLES ( + products, + customers_view KEY (customer_id) LABEL customers, + orders + ) + EDGE TABLES ( + order_items KEY (order_items_id) + SOURCE KEY (order_id) REFERENCES orders (order_id) + DESTINATION KEY (product_no) REFERENCES products (product_no), + customer_orders KEY (customer_orders_id) + SOURCE KEY (customer_id) REFERENCES customers_view (customer_id) + DESTINATION KEY (order_id) REFERENCES orders (order_id) + ); +CREATE VIEW customers_us_redacted AS SELECT * FROM GRAPH_TABLE (myshop2 MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name_redacted AS customer_name_redacted)); +SELECT * FROM customers_us_redacted; + customer_name_redacted +------------------------ + redacted1 +(1 row) + +-- GRAPH_TABLE in UDFs +CREATE FUNCTION out_degree(sname varchar) RETURNS varchar AS $$ +DECLARE + out_degree int; +BEGIN + SELECT count(*) INTO out_degree FROM GRAPH_TABLE (g1 MATCH (src WHERE src.vname = sname)->() COLUMNS (src.vname)); + RETURN out_degree; +END; +$$ LANGUAGE plpgsql; +CREATE FUNCTION direct_connections(sname varchar) +RETURNS TABLE (cname varchar, dname varchar) +AS $$ + SELECT cname, dname FROM GRAPH_TABLE (g1 MATCH (src WHERE src.vname = sname)-[conn]->(dst) COLUMNS (conn.ename AS cname, dst.vname AS dname)); +$$ LANGUAGE SQL; +SELECT sname, out_degree(sname) FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname AS sname)); + sname | out_degree +-------+------------ + v11 | 3 + v12 | 1 + v13 | 1 +(3 rows) + +SELECT sname, cname, dname FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname AS sname)), LATERAL direct_connections(sname); + sname | cname | dname +-------+-------+------- + v11 | e121 | v22 + v11 | e131 | v33 + v11 | e132 | v31 + v12 | e122 | v21 + v13 | e123 | v23 +(5 rows) + +-- GRAPH_TABLE joined to a regular table +SELECT * FROM customers co, GRAPH_TABLE (myshop2 MATCH (cg IS customers WHERE cg.address = co.address)-[IS customer_orders]->(o IS orders) COLUMNS (cg.name_redacted AS customer_name_redacted)) WHERE co.customer_id = 1; + customer_id | name | address | customer_name_redacted +-------------+-----------+---------+------------------------ + 1 | customer1 | US | redacted1 +(1 row) + +-- graph table in a subquery +SELECT * FROM customers co WHERE co.customer_id = (SELECT customer_id FROM GRAPH_TABLE (myshop2 MATCH (cg IS customers WHERE cg.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (cg.customer_id))); + customer_id | name | address +-------------+-----------+--------- + 1 | customer1 | US +(1 row) + +-- query within graph table +SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE src.vprop1 > (SELECT max(v1.vprop1) FROM v1) COLUMNS(src.vname AS sname, dest.vname AS dname)); +ERROR: subqueries within GRAPH_TABLE reference are not supported +SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE out_degree(src.vname) > (SELECT max(out_degree(nname)) FROM GRAPH_TABLE (g1 MATCH (node) COLUMNS (node.vname AS nname))) COLUMNS(src.vname AS sname, dest.vname AS dname)); +ERROR: subqueries within GRAPH_TABLE reference are not supported +-- leave the objects behind for pg_upgrade/pg_dump tests diff --git a/src/test/regress/expected/graph_table_rls.out b/src/test/regress/expected/graph_table_rls.out new file mode 100644 index 0000000000000..0e719c7ebd7fc --- /dev/null +++ b/src/test/regress/expected/graph_table_rls.out @@ -0,0 +1,789 @@ +-- +--Test RLS with GRAPH_TABLE +-- +--This test verifies that Row Level Security (RLS) policies are correctly +--enforced when querying tables underlying property graphs using GRAPH_TABLE. +--graph_table.sql has extensive tests covering interaction of GRAPH_TABLE with +--other query constructs. rowsecurity.sql has extensive coverage of interaction +--of RLS and other features of PostgreSQL. This test along with those two tests +--is sufficient to make sure that all combinations of RLS and GRAPH_TABLE will +--work as expected. +-- Clean up in case a prior regression run failed +-- Suppress NOTICE messages when users/groups don't exist +SET client_min_messages TO 'warning'; +DROP USER IF EXISTS regress_graph_rls_alice; +DROP USER IF EXISTS regress_graph_rls_bob; +DROP USER IF EXISTS regress_graph_rls_carol; +DROP USER IF EXISTS regress_graph_rls_dave; +DROP USER IF EXISTS regress_graph_rls_exempt_user; +DROP ROLE IF EXISTS regress_graph_rls_group1; +DROP ROLE IF EXISTS regress_graph_rls_group2; +DROP SCHEMA IF EXISTS graph_rls_schema CASCADE; +RESET client_min_messages; +-- initial setup +CREATE USER regress_graph_rls_alice NOLOGIN; +CREATE USER regress_graph_rls_bob NOLOGIN; +CREATE USER regress_graph_rls_carol NOLOGIN; +CREATE USER regress_graph_rls_dave NOLOGIN; +CREATE USER regress_graph_rls_exempt_user BYPASSRLS NOLOGIN; +CREATE ROLE regress_graph_rls_group1 NOLOGIN; +CREATE ROLE regress_graph_rls_group2 NOLOGIN; +GRANT regress_graph_rls_group1 TO regress_graph_rls_dave; +GRANT regress_graph_rls_group2 TO regress_graph_rls_bob; +CREATE SCHEMA graph_rls_schema; +GRANT ALL ON SCHEMA graph_rls_schema to public; +SET search_path = graph_rls_schema; +-- setup for leaky-function tests +CREATE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; +SET SESSION AUTHORIZATION regress_graph_rls_alice; +CREATE TABLE users (uid int PRIMARY KEY, pguser name, seclv int); +INSERT INTO users VALUES + (1, 'regress_graph_rls_alice', 99), + (2, 'regress_graph_rls_bob', 1), + (3, 'regress_graph_rls_carol', 2), + (4, 'regress_graph_rls_dave', 3); +GRANT SELECT ON users TO public; +CREATE TABLE document_people ( + did int, + dlevel int, + dtitle text); +INSERT INTO document_people VALUES + ( 1, 2, 'Politicians'), + ( 2, 3, 'Artists'), + ( 3, 1, 'Scientists'), + ( 4, 100, 'Unspeakables'); +GRANT SELECT ON document_people TO public; +CREATE TABLE accessed ( + aid int, + uid int, + did int); +INSERT INTO accessed VALUES + (1, 2, 3), + (2, 3, 1), + (3, 3, 3), + (4, 4, 3), + (5, 1, 1), + (6, 1, 4), + (7, 4, 2); +GRANT SELECT ON accessed TO public; +CREATE PROPERTY GRAPH cabinet + VERTEX TABLES (users KEY (uid), + document_people AS document KEY (did)) + EDGE TABLES (accessed KEY (aid) + SOURCE KEY (uid) REFERENCES users (uid) + DESTINATION KEY (did) REFERENCES document (did)); +GRANT SELECT ON cabinet TO public; +-- +-- Basic RLS tests +-- +ALTER TABLE document_people ENABLE ROW LEVEL SECURITY; +-- user's security level must be higher than or equal to document's +CREATE POLICY p1 ON document_people AS PERMISSIVE + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +-- but Dave isn't allowed to see document titled 'Scientists' +CREATE POLICY p2 ON document_people AS RESTRICTIVE TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); +CREATE POLICY p3 ON document_people AS RESTRICTIVE TO regress_graph_rls_group1 + USING (dlevel < 3); +CREATE POLICY p4 ON document_people AS PERMISSIVE TO regress_graph_rls_group2 + USING (dlevel < 3); +SET row_security TO ON; +-- Use the same query in all the test cases below. Prepare it once and +-- use multiple times. Apart from making the test file shorter and avoiding +-- duplication, it also tests that a prepared statement correctly reflect changes +-- to RLS policies, session user or RLS settings. +PREPARE graph_rls_query AS +SELECT * FROM GRAPH_TABLE (cabinet + MATCH (u IS users)-[a IS accessed]->(d IS document) + WHERE f_leak(d.dtitle) + COLUMNS (u.pguser, a.aid, d.dtitle, d.dlevel)) + ORDER BY 1, 2, 3, 4; +-- viewpoint from regress_graph_rls_bob +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Scientists + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 +(5 rows) + +-- viewpoint from regress_graph_rls_carol +SET SESSION AUTHORIZATION regress_graph_rls_carol; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Scientists + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 +(5 rows) + +-- viewpoint from regress_graph_rls_dave +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_carol | 2 | Politicians | 2 +(2 rows) + +-- RLS policy does not apply to table owner when RLS enabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 +(7 rows) + +SET row_security TO OFF; +-- database superuser does bypass RLS policy when disabled +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 +(7 rows) + +-- database non-superuser with bypass privilege can bypass RLS policy when disabled +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 +(7 rows) + +-- RLS policy does not apply to table owner when RLS disabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 +(7 rows) + +-- When RLS disabled, other users get ERROR. +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error +ERROR: query would be affected by row-level security policy for table "document_people" +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP PROPERTY GRAPH cabinet; +-- +-- Table inheritance +-- +ALTER TABLE document_people ADD COLUMN category text DEFAULT 'People'; +CREATE TABLE document_places ( + did int, + dlevel int, + dtitle text, + category text DEFAULT 'Places'); +INSERT INTO document_places VALUES + ( 5, 1, 'Paris'), + ( 6, 2, 'Tokyo'), + ( 7, 3, 'New York'); +GRANT SELECT ON document_places TO public; +-- Setup inheritance +CREATE TABLE document ( + did int, + dlevel int, + dtitle text); +GRANT SELECT ON document TO public; +ALTER TABLE document_people INHERIT document; +ALTER TABLE document_places INHERIT document; +INSERT INTO accessed VALUES + (11, 2, 5), + (12, 3, 6), + (13, 1, 7), + (14, 4, 5), + (15, 1, 6); +-- Enable RLS and move policies p1 and p2 to parent table but leave p3 and p4 on +-- child table. The policies on child table are not applied when querying parent +-- table. +ALTER TABLE document ENABLE ROW LEVEL SECURITY; +DROP POLICY p1 ON document_people; +DROP POLICY p2 ON document_people; +CREATE POLICY p1 ON document AS PERMISSIVE + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +CREATE POLICY p2 ON document AS RESTRICTIVE TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); +CREATE PROPERTY GRAPH cabinet + VERTEX TABLES (users KEY (uid), document KEY (did)) + EDGE TABLES (accessed KEY (aid) + SOURCE KEY (uid) REFERENCES users (uid) + DESTINATION KEY (did) REFERENCES document (did)); +GRANT SELECT ON cabinet TO public; +SET row_security TO ON; +-- viewpoint from regress_graph_rls_bob +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; +NOTICE: f_leak => Scientists +NOTICE: f_leak => Paris + pguser | aid | dtitle | dlevel +-------------------------+-----+------------+-------- + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 14 | Paris | 1 +(5 rows) + +-- viewpoint from regress_graph_rls_carol +SET SESSION AUTHORIZATION regress_graph_rls_carol; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Scientists +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 14 | Paris | 1 +(9 rows) + +-- viewpoint from regress_graph_rls_dave +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(8 rows) + +-- RLS policy does not apply to table owner when RLS enabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +SET row_security TO OFF; +-- database superuser does bypass RLS policy when disabled +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- database non-superuser with bypass privilege can bypass RLS policy when disabled +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- RLS policy does not apply to table owner when RLS disabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- When RLS disabled, other users get ERROR. +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error +ERROR: query would be affected by row-level security policy for table "document" +-- cleanup +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP PROPERTY GRAPH cabinet; +ALTER TABLE document_people NO INHERIT document; +ALTER TABLE document_places NO INHERIT document; +DROP TABLE document; +-- +-- Partitioned Tables +-- +CREATE TABLE document ( + did int, + dlevel int, + dtitle text, + category text) PARTITION BY LIST (category); +GRANT SELECT ON document TO public; +ALTER TABLE document ATTACH PARTITION document_people FOR VALUES IN ('People'); +ALTER TABLE document ATTACH PARTITION document_places FOR VALUES IN ('Places'); +-- Enable RLS on partitioned table +ALTER TABLE document ENABLE ROW LEVEL SECURITY; +-- create policies on partitioned table +CREATE POLICY p1 ON document AS PERMISSIVE + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +CREATE POLICY p2 ON document AS RESTRICTIVE TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); +CREATE PROPERTY GRAPH cabinet + VERTEX TABLES (users KEY (uid), document KEY (did)) + EDGE TABLES (accessed KEY (aid) + SOURCE KEY (uid) REFERENCES users (uid) + DESTINATION KEY (did) REFERENCES document (did)); +GRANT SELECT ON cabinet TO public; +SET row_security TO ON; +-- viewpoint from regress_graph_rls_bob +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; +NOTICE: f_leak => Scientists +NOTICE: f_leak => Paris + pguser | aid | dtitle | dlevel +-------------------------+-----+------------+-------- + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 14 | Paris | 1 +(5 rows) + +-- viewpoint from regress_graph_rls_carol +SET SESSION AUTHORIZATION regress_graph_rls_carol; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Scientists +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 14 | Paris | 1 +(9 rows) + +-- viewpoint from regress_graph_rls_dave +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(8 rows) + +-- RLS policy does not apply to table owner when RLS enabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +SET row_security TO OFF; +-- database superuser does bypass RLS policy when disabled +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- database non-superuser with bypass privilege can bypass RLS policy when disabled +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- RLS policy does not apply to table owner when RLS disabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- When RLS disabled, other users get ERROR. +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error +ERROR: query would be affected by row-level security policy for table "document" +-- +-- Recursion through GRAPH_TABLE also throws error +-- +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +-- Create a policy on document that references document itself via GRAPH_TABLE +CREATE POLICY pr ON document TO regress_graph_rls_dave + USING (EXISTS (SELECT 1 FROM GRAPH_TABLE (cabinet + MATCH (u IS users)-[a IS accessed]->(d IS document) + WHERE u.pguser = current_user + COLUMNS (a.aid)))); +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error +ERROR: infinite recursion detected in policy for relation "document" +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP POLICY pr ON document; +-- +-- Command specific policy. Since GRAPH_TABLE can be used in only SELECT, test +-- only FOR SELECT policies. +-- +DROP POLICY p1 ON document; +DROP POLICY p2 ON document; +CREATE POLICY p1 ON document AS PERMISSIVE + FOR SELECT + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +CREATE POLICY p2 ON document AS RESTRICTIVE + FOR SELECT TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+-------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(8 rows) + +-- +-- Default deny policy, FORCE ROW LEVEL SECURITY +-- +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP POLICY p1 ON document; +DROP POLICY p2 ON document; +-- default deny policy applies to non-owners, non-rls-exempt and non-superusers +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; + pguser | aid | dtitle | dlevel +--------+-----+--------+-------- +(0 rows) + +-- Deny RLS policy does not apply to table owner, superuser or RLS exempt user +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; +NOTICE: f_leak => Politicians +NOTICE: f_leak => Artists +NOTICE: f_leak => Scientists +NOTICE: f_leak => Unspeakables +NOTICE: f_leak => Paris +NOTICE: f_leak => Tokyo +NOTICE: f_leak => New York + pguser | aid | dtitle | dlevel +-------------------------+-----+--------------+-------- + regress_graph_rls_alice | 5 | Politicians | 2 + regress_graph_rls_alice | 6 | Unspeakables | 100 + regress_graph_rls_alice | 13 | New York | 3 + regress_graph_rls_alice | 15 | Tokyo | 2 + regress_graph_rls_bob | 1 | Scientists | 1 + regress_graph_rls_bob | 11 | Paris | 1 + regress_graph_rls_carol | 2 | Politicians | 2 + regress_graph_rls_carol | 3 | Scientists | 1 + regress_graph_rls_carol | 12 | Tokyo | 2 + regress_graph_rls_dave | 4 | Scientists | 1 + regress_graph_rls_dave | 7 | Artists | 3 + regress_graph_rls_dave | 14 | Paris | 1 +(12 rows) + +-- FORCE ROW LEVEL SECURITY applies RLS to owners too +ALTER TABLE document FORCE ROW LEVEL SECURITY; +EXECUTE graph_rls_query; + pguser | aid | dtitle | dlevel +--------+-----+--------+-------- +(0 rows) + +SET row_security TO OFF; +EXECUTE graph_rls_query; -- error +ERROR: query would be affected by row-level security policy for table "document" +HINT: To disable the policy for the table's owner, use ALTER TABLE NO FORCE ROW LEVEL SECURITY. +-- Clean up +DEALLOCATE graph_rls_query; +-- leave as many objects behind for pg_upgrade/pg_dump tests as possible. The +-- pg_dump test only dumps the regression database, not the global objects like +-- users and roles. Reassign ownership of all objects to superuser and drop +-- users and roles created in this test. Policies can not be reassigned, so drop +-- them explicitly. +RESET SESSION AUTHORIZATION; +REASSIGN OWNED BY regress_graph_rls_alice TO current_user; +DROP USER regress_graph_rls_alice; +DROP USER regress_graph_rls_bob; +DROP USER regress_graph_rls_carol; +DROP USER regress_graph_rls_dave; +DROP USER regress_graph_rls_exempt_user; +DROP POLICY p3 ON document_people; +DROP POLICY p4 ON document_people; +DROP ROLE regress_graph_rls_group1; +DROP ROLE regress_graph_rls_group2; diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out index 35e4cb47ebed5..b08083ec54ce2 100644 --- a/src/test/regress/expected/groupingsets.out +++ b/src/test/regress/expected/groupingsets.out @@ -348,6 +348,43 @@ select a, b, grouping(a, b), sum(t1.v), max(t2.c) | | 3 | 172 | 2 (3 rows) +select a, b, grouping(a, b), sum(t1.v), max(t2.c) + from gstest1 t1 full join gstest2 t2 using (a,b) + group by grouping sets ((a, b), ()); + a | b | grouping | sum | max +---+---+----------+-----+----- + 1 | 1 | 0 | 147 | 2 + 1 | 2 | 0 | 25 | 2 + 1 | 3 | 0 | 14 | + 2 | 2 | 0 | | 2 + 2 | 3 | 0 | 15 | + 3 | 3 | 0 | 16 | + 3 | 4 | 0 | 17 | + 4 | 1 | 0 | 37 | + | | 3 | 271 | 2 +(9 rows) + +-- references in subqueries should work too +select (select a), + (select b), + (select grouping(a, b)), + (select sum(t1.v)), + (select max(t2.c)) + from gstest1 t1 full join gstest2 t2 using (a,b) + group by grouping sets ((a, b), ()); + a | b | grouping | sum | max +---+---+----------+-----+----- + 1 | 1 | 0 | 147 | 2 + 1 | 2 | 0 | 25 | 2 + 1 | 3 | 0 | 14 | + 2 | 2 | 0 | | 2 + 2 | 3 | 0 | 15 | + 3 | 3 | 0 | 16 | + 3 | 4 | 0 | 17 | + 4 | 1 | 0 | 37 | + | | 3 | 271 | 2 +(9 rows) + -- check that functionally dependent cols are not nulled select a, d, grouping(a,b,c) from gstest3 @@ -463,6 +500,101 @@ select x, y || 'y' | 3y (8 rows) +-- check that operands wrapped in PlaceHolderVars are capable of index matching +begin; +set local enable_bitmapscan = off; +explain (costs off) +select x, y + from (select unique1 as x, unique2 as y from tenk1) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + QUERY PLAN +----------------------------------------------------- + Sort + Sort Key: tenk1.unique1, tenk1.unique2 + -> GroupAggregate + Group Key: tenk1.unique1 + Sort Key: tenk1.unique2 + Group Key: tenk1.unique2 + -> Index Scan using tenk1_unique1 on tenk1 + Index Cond: (unique1 = 1) +(8 rows) + +select x, y + from (select unique1 as x, unique2 as y from tenk1) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + x | y +---+------ + 1 | + | 2838 +(2 rows) + +explain (costs off) +select x, y + from (select unique1::oid as x, unique2 as y from tenk1) as t + where x::integer = 1 + group by grouping sets (x, y) + order by 1, 2; + QUERY PLAN +----------------------------------------------------------------- + Sort + Sort Key: ((tenk1.unique1)::oid), tenk1.unique2 + -> GroupAggregate + Group Key: ((tenk1.unique1)::oid) + Sort Key: tenk1.unique2 + Group Key: tenk1.unique2 + -> Sort + Sort Key: ((tenk1.unique1)::oid) + -> Index Scan using tenk1_unique1 on tenk1 + Index Cond: (((unique1)::oid)::integer = 1) +(10 rows) + +select x, y + from (select unique1::oid as x, unique2 as y from tenk1) as t + where x::integer = 1 + group by grouping sets (x, y) + order by 1, 2; + x | y +---+------ + 1 | + | 2838 +(2 rows) + +explain (costs off) +select x, y + from (select t1.unique1 as x, t1.unique2 as y from tenk1 t1, tenk1 t2) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + QUERY PLAN +------------------------------------------------------------------- + Sort + Sort Key: t1.unique1, t1.unique2 + -> GroupAggregate + Group Key: t1.unique1 + Sort Key: t1.unique2 + Group Key: t1.unique2 + -> Nested Loop + -> Index Scan using tenk1_unique1 on tenk1 t1 + Index Cond: (unique1 = 1) + -> Index Only Scan using tenk1_hundred on tenk1 t2 +(10 rows) + +select x, y + from (select t1.unique1 as x, t1.unique2 as y from tenk1 t1, tenk1 t2) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + x | y +---+------ + 1 | + | 2838 +(2 rows) + +rollback; -- check qual push-down rules for a subquery with grouping sets explain (verbose, costs off) select * from ( @@ -504,17 +636,17 @@ select grouping(ss.x) from int8_tbl i1 cross join lateral (select (select i1.q1) as x) ss group by ss.x; - QUERY PLAN ------------------------------------------------- + QUERY PLAN +---------------------------------------------------------- GroupAggregate - Output: GROUPING((SubPlan 1)), ((SubPlan 2)) - Group Key: ((SubPlan 2)) + Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_2)) + Group Key: ((SubPlan expr_2)) -> Sort - Output: ((SubPlan 2)), i1.q1 - Sort Key: ((SubPlan 2)) + Output: ((SubPlan expr_2)), i1.q1 + Sort Key: ((SubPlan expr_2)) -> Seq Scan on public.int8_tbl i1 - Output: (SubPlan 2), i1.q1 - SubPlan 2 + Output: (SubPlan expr_2), i1.q1 + SubPlan expr_2 -> Result Output: i1.q1 (11 rows) @@ -534,22 +666,22 @@ select (select grouping(ss.x)) from int8_tbl i1 cross join lateral (select (select i1.q1) as x) ss group by ss.x; - QUERY PLAN --------------------------------------------- + QUERY PLAN +------------------------------------------------ GroupAggregate - Output: (SubPlan 2), ((SubPlan 3)) - Group Key: ((SubPlan 3)) + Output: (SubPlan expr_1), ((SubPlan expr_3)) + Group Key: ((SubPlan expr_3)) -> Sort - Output: ((SubPlan 3)), i1.q1 - Sort Key: ((SubPlan 3)) + Output: ((SubPlan expr_3)), i1.q1 + Sort Key: ((SubPlan expr_3)) -> Seq Scan on public.int8_tbl i1 - Output: (SubPlan 3), i1.q1 - SubPlan 3 + Output: (SubPlan expr_3), i1.q1 + SubPlan expr_3 -> Result Output: i1.q1 - SubPlan 2 + SubPlan expr_1 -> Result - Output: GROUPING((SubPlan 1)) + Output: GROUPING((SubPlan expr_2)) (14 rows) select (select grouping(ss.x)) @@ -591,16 +723,18 @@ explain (costs off) QUERY PLAN ------------------------------------------------------------ Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan using tenk1_unique1 on tenk1 Index Cond: (unique1 IS NOT NULL) -(5 rows) +(6 rows) -- Views with GROUPING SET queries CREATE VIEW gstest_view AS select a, b, grouping(a,b), sum(c), count(*), max(c) from gstest2 group by rollup ((a,b,c),(c,d)); NOTICE: view "gstest_view" will be a temporary view +DETAIL: It depends on temporary table gstest2. select pg_get_viewdef('gstest_view'::regclass, true); pg_get_viewdef --------------------------------------- @@ -880,7 +1014,7 @@ explain (costs off) Sort Sort Key: "*VALUES*".column1 -> Values Scan on "*VALUES*" - SubPlan 1 + SubPlan expr_1 -> Aggregate Group Key: () Filter: "*VALUES*".column1 @@ -889,7 +1023,8 @@ explain (costs off) -> Seq Scan on gstest2 (10 rows) --- test pushdown of HAVING clause that does not reference any columns that are nullable by grouping sets +-- test pushdown of non-degenerate HAVING clause that does not reference any +-- columns that are nullable by grouping sets explain (costs off) select a, b, count(*) from gstest2 group by grouping sets ((a, b), (a)) having a > 1 and b > 1; QUERY PLAN @@ -910,6 +1045,85 @@ select a, b, count(*) from gstest2 group by grouping sets ((a, b), (a)) having a 2 | 2 | 1 (1 row) +explain (costs off) +select a, b, count(*) from gstest2 group by rollup(a), b having b > 1; + QUERY PLAN +--------------------------------- + GroupAggregate + Group Key: b, a + Group Key: b + -> Sort + Sort Key: b, a + -> Seq Scan on gstest2 + Filter: (b > 1) +(7 rows) + +select a, b, count(*) from gstest2 group by rollup(a), b having b > 1; + a | b | count +---+---+------- + 1 | 2 | 1 + 2 | 2 | 1 + | 2 | 2 +(3 rows) + +-- test pushdown of degenerate HAVING clause +explain (costs off) +select count(*) from gstest2 group by grouping sets (()) having false; + QUERY PLAN +----------------------------------- + Aggregate + Group Key: () + Filter: false + -> Result + Replaces: Scan on gstest2 + One-Time Filter: false +(6 rows) + +select count(*) from gstest2 group by grouping sets (()) having false; + count +------- +(0 rows) + +explain (costs off) +select a, count(*) from gstest2 group by grouping sets ((a), ()) having false; + QUERY PLAN +----------------------------------------- + GroupAggregate + Group Key: a + Group Key: () + Filter: false + -> Sort + Sort Key: a + -> Result + Replaces: Scan on gstest2 + One-Time Filter: false +(9 rows) + +select a, count(*) from gstest2 group by grouping sets ((a), ()) having false; + a | count +---+------- +(0 rows) + +explain (costs off) +select a, b, count(*) from gstest2 group by grouping sets ((a), (b)) having false; + QUERY PLAN +----------------------------------------- + GroupAggregate + Group Key: a + Sort Key: b + Group Key: b + -> Sort + Sort Key: a + -> Result + Replaces: Scan on gstest2 + One-Time Filter: false +(9 rows) + +select a, b, count(*) from gstest2 group by grouping sets ((a), (b)) having false; + a | b | count +---+---+------- +(0 rows) + -- HAVING with GROUPING queries select ten, grouping(ten) from onek group by grouping sets(ten) having grouping(ten) >= 0 @@ -2168,17 +2382,17 @@ order by a, b, c; -- test handling of outer GroupingFunc within subqueries explain (costs off) select (select grouping(v1)) from (values ((select 1))) v(v1) group by cube(v1); - QUERY PLAN -------------------------------- + QUERY PLAN +------------------------------------ MixedAggregate - Hash Key: (InitPlan 3).col1 + Hash Key: (InitPlan expr_3).col1 Group Key: () - InitPlan 1 + InitPlan expr_2 -> Result - InitPlan 3 + InitPlan expr_3 -> Result -> Result - SubPlan 2 + SubPlan expr_1 -> Result (10 rows) @@ -2191,15 +2405,15 @@ select (select grouping(v1)) from (values ((select 1))) v(v1) group by cube(v1); explain (costs off) select (select grouping(v1)) from (values ((select 1))) v(v1) group by v1; - QUERY PLAN ----------------- + QUERY PLAN +------------------- GroupAggregate - InitPlan 1 + InitPlan expr_2 -> Result - InitPlan 3 + InitPlan expr_3 -> Result -> Result - SubPlan 2 + SubPlan expr_1 -> Result (8 rows) @@ -2221,18 +2435,18 @@ order by case when grouping((select t1.v from gstest5 t2 where id = t1.id)) = 0 then (select t1.v from gstest5 t2 where id = t1.id) else null end nulls first; - QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------- Sort - Output: (GROUPING((SubPlan 1))), ((SubPlan 3)), (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END), t1.v - Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST + Output: (GROUPING((SubPlan expr_1))), ((SubPlan expr_3)), (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END), t1.v + Sort Key: (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END) NULLS FIRST -> HashAggregate - Output: GROUPING((SubPlan 1)), ((SubPlan 3)), CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END, t1.v + Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END, t1.v Hash Key: t1.v - Hash Key: (SubPlan 3) + Hash Key: (SubPlan expr_3) -> Seq Scan on pg_temp.gstest5 t1 - Output: (SubPlan 3), t1.v, t1.id - SubPlan 3 + Output: (SubPlan expr_3), t1.v, t1.id + SubPlan expr_3 -> Bitmap Heap Scan on pg_temp.gstest5 t2 Output: t1.v Recheck Cond: (t2.id = t1.id) @@ -2271,18 +2485,18 @@ select grouping((select t1.v from gstest5 t2 where id = t1.id)), from gstest5 t1 group by grouping sets(v, s) order by o nulls first; - QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------- Sort - Output: (GROUPING((SubPlan 1))), ((SubPlan 3)), (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END), t1.v - Sort Key: (CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END) NULLS FIRST + Output: (GROUPING((SubPlan expr_1))), ((SubPlan expr_3)), (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END), t1.v + Sort Key: (CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END) NULLS FIRST -> HashAggregate - Output: GROUPING((SubPlan 1)), ((SubPlan 3)), CASE WHEN (GROUPING((SubPlan 2)) = 0) THEN ((SubPlan 3)) ELSE NULL::integer END, t1.v + Output: GROUPING((SubPlan expr_1)), ((SubPlan expr_3)), CASE WHEN (GROUPING((SubPlan expr_2)) = 0) THEN ((SubPlan expr_3)) ELSE NULL::integer END, t1.v Hash Key: t1.v - Hash Key: (SubPlan 3) + Hash Key: (SubPlan expr_3) -> Seq Scan on pg_temp.gstest5 t1 - Output: (SubPlan 3), t1.v, t1.id - SubPlan 3 + Output: (SubPlan expr_3), t1.v, t1.id + SubPlan expr_3 -> Bitmap Heap Scan on pg_temp.gstest5 t2 Output: t1.v Recheck Cond: (t2.id = t1.id) @@ -2481,4 +2695,71 @@ group by grouping sets((a, b), (a)); 2 | 2 | 4 (4 rows) +-- test handling of SRFs with grouping sets +explain (verbose, costs off) +select generate_series(1, a) as g +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(g) +order by 1; + QUERY PLAN +-------------------------------------------------------------- + Sort + Output: (generate_series(1, "*VALUES*".column1)) + Sort Key: (generate_series(1, "*VALUES*".column1)) + -> MixedAggregate + Output: (generate_series(1, "*VALUES*".column1)) + Hash Key: generate_series(1, "*VALUES*".column1) + Group Key: () + -> ProjectSet + Output: generate_series(1, "*VALUES*".column1) + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1 +(11 rows) + +select generate_series(1, a) as g +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(g) +order by 1; + g +--- + 1 + 2 + +(3 rows) + +explain (verbose, costs off) +select generate_series(1, a) as g, a+b as ab +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(a, ab) +order by 1, 2; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Sort + Output: (generate_series(1, "*VALUES*".column1)), (("*VALUES*".column1 + "*VALUES*".column2)), "*VALUES*".column1 + Sort Key: (generate_series(1, "*VALUES*".column1)), (("*VALUES*".column1 + "*VALUES*".column2)) + -> ProjectSet + Output: generate_series(1, "*VALUES*".column1), (("*VALUES*".column1 + "*VALUES*".column2)), "*VALUES*".column1 + -> MixedAggregate + Output: "*VALUES*".column1, (("*VALUES*".column1 + "*VALUES*".column2)) + Hash Key: "*VALUES*".column1, ("*VALUES*".column1 + "*VALUES*".column2) + Hash Key: "*VALUES*".column1 + Group Key: () + -> Values Scan on "*VALUES*" + Output: ("*VALUES*".column1 + "*VALUES*".column2), "*VALUES*".column1 +(12 rows) + +select generate_series(1, a) as g, a+b as ab +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(a, ab) +order by 1, 2; + g | ab +---+---- + 1 | 2 + 1 | 4 + 1 | + 1 | + 2 | 4 + 2 | +(6 rows) + -- end diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out index 7f9e29c765cf1..3fa2562f231f3 100644 --- a/src/test/regress/expected/guc.out +++ b/src/test/regress/expected/guc.out @@ -31,6 +31,28 @@ SELECT '2006-08-13 12:34:56'::timestamptz; 2006-08-13 12:34:56-07 (1 row) +-- Check handling of list GUCs +SET search_path = 'pg_catalog', Foo, 'Bar', ''; +SHOW search_path; + search_path +---------------------------- + pg_catalog, foo, "Bar", "" +(1 row) + +SET search_path = null; -- means empty list +SHOW search_path; + search_path +------------- + +(1 row) + +SET search_path = null, null; -- syntax error +ERROR: syntax error at or near "," +LINE 1: SET search_path = null, null; + ^ +SET enable_seqscan = null; -- error +ERROR: NULL is an invalid value for enable_seqscan +RESET search_path; -- SET LOCAL has no effect outside of a transaction SET LOCAL vacuum_cost_delay TO 50; WARNING: SET LOCAL can only be used in transaction blocks @@ -689,6 +711,63 @@ select current_schemas(false); reset search_path; -- +-- Test parsing of log_min_messages +-- +SET log_min_messages TO foo; -- fail +ERROR: invalid value for parameter "log_min_messages": "foo" +DETAIL: Unrecognized log level: "foo". +SET log_min_messages TO fatal; +SHOW log_min_messages; + log_min_messages +------------------ + fatal +(1 row) + +SET log_min_messages TO 'fatal'; +SHOW log_min_messages; + log_min_messages +------------------ + fatal +(1 row) + +SET log_min_messages TO 'checkpointer:debug2, autovacuum:debug1'; -- fail +ERROR: invalid value for parameter "log_min_messages": "checkpointer:debug2, autovacuum:debug1" +DETAIL: Default log level was not defined. +SET log_min_messages TO 'debug1, backend:error, fatal'; -- fail +ERROR: invalid value for parameter "log_min_messages": "debug1, backend:error, fatal" +DETAIL: Redundant specification of default log level. +SET log_min_messages TO 'backend:error, debug1, backend:warning'; -- fail +ERROR: invalid value for parameter "log_min_messages": "backend:error, debug1, backend:warning" +DETAIL: Redundant log level specification for process type "backend". +SET log_min_messages TO 'backend:error, foo:fatal, archiver:debug1'; -- fail +ERROR: invalid value for parameter "log_min_messages": "backend:error, foo:fatal, archiver:debug1" +DETAIL: Unrecognized process type "foo". +SET log_min_messages TO 'backend:error, checkpointer:bar, archiver:debug1'; -- fail +ERROR: invalid value for parameter "log_min_messages": "backend:error, checkpointer:bar, archiver:debug1" +DETAIL: Unrecognized log level for process type "checkpointer": "bar". +SET log_min_messages TO 'backend:error, checkpointer:debug3, fatal, archiver:debug2, autovacuum:debug1, walsender:debug3'; +SHOW log_min_messages; + log_min_messages +------------------------------------------------------------------------------------------------- + fatal, archiver:debug2, autovacuum:debug1, backend:error, checkpointer:debug3, walsender:debug3 +(1 row) + +SET log_min_messages TO 'warning, autovacuum:debug1'; +SHOW log_min_messages; + log_min_messages +---------------------------- + warning, autovacuum:debug1 +(1 row) + +SET log_min_messages TO 'autovacuum:debug1, warning'; +SHOW log_min_messages; + log_min_messages +---------------------------- + warning, autovacuum:debug1 +(1 row) + +RESET log_min_messages; +-- -- Tests for function-local GUC settings -- set work_mem = '3MB'; diff --git a/src/test/regress/expected/hash_func.out b/src/test/regress/expected/hash_func.out index 2d625a247bb91..6bc14f57a6bf4 100644 --- a/src/test/regress/expected/hash_func.out +++ b/src/test/regress/expected/hash_func.out @@ -65,6 +65,16 @@ WHERE hashoid(v)::bit(32) != hashoidextended(v, 0)::bit(32) -------+----------+-----------+----------- (0 rows) +SELECT v as value, hashoid8(v)::bit(32) as standard, + hashoid8extended(v, 0)::bit(32) as extended0, + hashoid8extended(v, 1)::bit(32) as extended1 +FROM (VALUES (0), (1), (17), (42), (550273), (207112489)) x(v) +WHERE hashoid8(v)::bit(32) != hashoid8extended(v, 0)::bit(32) + OR hashoid8(v)::bit(32) = hashoid8extended(v, 1)::bit(32); + value | standard | extended0 | extended1 +-------+----------+-----------+----------- +(0 rows) + SELECT v as value, hashchar(v)::bit(32) as standard, hashcharextended(v, 0)::bit(32) as extended0, hashcharextended(v, 1)::bit(32) as extended1 diff --git a/src/test/regress/expected/hash_index.out b/src/test/regress/expected/hash_index.out index 0d4bdb2adefae..0b099b9e771d3 100644 --- a/src/test/regress/expected/hash_index.out +++ b/src/test/regress/expected/hash_index.out @@ -40,6 +40,8 @@ CREATE INDEX hash_name_index ON hash_name_heap USING hash (random name_ops); CREATE INDEX hash_txt_index ON hash_txt_heap USING hash (random text_ops); CREATE INDEX hash_f8_index ON hash_f8_heap USING hash (random float8_ops) WITH (fillfactor=60); +CREATE INDEX hash_i4_partial_index ON hash_i4_heap USING hash (seqno) + WHERE seqno = 9999; -- -- Also try building functional, expressional, and partial indexes on -- tables that already contain data. @@ -131,6 +133,25 @@ SELECT * FROM hash_f8_heap -------+-------- (0 rows) +-- +-- partial hash index +-- +EXPLAIN (COSTS OFF) +SELECT * FROM hash_i4_heap + WHERE seqno = 9999; + QUERY PLAN +-------------------------------------------------------- + Index Scan using hash_i4_partial_index on hash_i4_heap + Index Cond: (seqno = 9999) +(2 rows) + +SELECT * FROM hash_i4_heap + WHERE seqno = 9999; + seqno | random +-------+------------ + 9999 | 1227676208 +(1 row) + -- -- hash index -- grep '^90[^0-9]' hashovfl.data @@ -312,6 +333,27 @@ ROLLBACK; INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 50) as i; CHECKPOINT; VACUUM hash_cleanup_heap; +-- Test cleanup of dead index tuples on single page with INSERT. +TRUNCATE hash_cleanup_heap; +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 1000) as i; +-- This relies on a rollbacked INSERT instead of a DELETE to make the creation +-- of the dead tuples concurrent-safe. +BEGIN; +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 500) as i; +ROLLBACK; +SET enable_seqscan = off; +SET enable_bitmapscan = off; +SELECT count(*) FROM hash_cleanup_heap WHERE keycol = 1; + count +------- + 1000 +(1 row) + +-- This query checks the hash index pages for dead tuples where the data +-- is inserted, and performs a local VACUUM on a single page. +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 200) as i; +RESET enable_seqscan; +RESET enable_bitmapscan; -- Clean up. DROP TABLE hash_cleanup_heap; -- Index on temp table. diff --git a/src/test/regress/expected/horology.out b/src/test/regress/expected/horology.out index b90bfcd794f45..32cf62b674179 100644 --- a/src/test/regress/expected/horology.out +++ b/src/test/regress/expected/horology.out @@ -467,6 +467,15 @@ SELECT timestamp with time zone 'Y2001M12D27H04MM05S06.789-08'; ERROR: invalid input syntax for type timestamp with time zone: "Y2001M12D27H04MM05S06.789-08" LINE 1: SELECT timestamp with time zone 'Y2001M12D27H04MM05S06.789-0... ^ +-- More examples we used to accept and should not +SELECT timestamp with time zone 'J2452271 T X03456-08'; +ERROR: invalid input syntax for type timestamp with time zone: "J2452271 T X03456-08" +LINE 1: SELECT timestamp with time zone 'J2452271 T X03456-08'; + ^ +SELECT timestamp with time zone 'J2452271 T X03456.001e6-08'; +ERROR: invalid input syntax for type timestamp with time zone: "J2452271 T X03456.001e6-08" +LINE 1: SELECT timestamp with time zone 'J2452271 T X03456.001e6-08'... + ^ -- conflicting fields should throw errors SELECT date '1995-08-06 epoch'; ERROR: invalid input syntax for type date: "1995-08-06 epoch" @@ -596,7 +605,8 @@ SELECT date '1991-02-03' - time with time zone '04:05:06 UTC' AS "Subtract Time ERROR: operator does not exist: date - time with time zone LINE 1: SELECT date '1991-02-03' - time with time zone '04:05:06 UTC... ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- -- timestamp, interval arithmetic -- diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out index b00219643b9ad..1e6e020fea836 100644 --- a/src/test/regress/expected/incremental_sort.out +++ b/src/test/regress/expected/incremental_sort.out @@ -1450,21 +1450,23 @@ explain (costs off) select a,b,sum(c) from t group by 1,2 order by 1,2,3 limit 1 set enable_incremental_sort = on; explain (costs off) select a,b,sum(c) from t group by 1,2 order by 1,2,3 limit 1; - QUERY PLAN ----------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------- Limit -> Incremental Sort Sort Key: a, b, (sum(c)) Presorted Key: a, b - -> GroupAggregate + -> Finalize GroupAggregate Group Key: a, b -> Gather Merge Workers Planned: 2 - -> Incremental Sort - Sort Key: a, b - Presorted Key: a - -> Parallel Index Scan using t_a_idx on t -(12 rows) + -> Partial GroupAggregate + Group Key: a, b + -> Incremental Sort + Sort Key: a, b + Presorted Key: a + -> Parallel Index Scan using t_a_idx on t +(14 rows) -- Incremental sort vs. set operations with varno 0 set enable_hashagg to off; @@ -1580,8 +1582,8 @@ from tenk1 t1 join tenk1 t2 on t1.unique1 = t2.unique2 join tenk1 t3 on t2.unique1 = t3.unique1 order by count(*); - QUERY PLAN ------------------------------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------------------------------- Sort Sort Key: (count(*)) -> Finalize Aggregate @@ -1591,10 +1593,10 @@ order by count(*); -> Parallel Hash Join Hash Cond: (t2.unique1 = t3.unique1) -> Parallel Hash Join - Hash Cond: (t1.unique1 = t2.unique2) - -> Parallel Index Only Scan using tenk1_unique1 on tenk1 t1 + Hash Cond: (t2.unique2 = t1.unique1) + -> Parallel Index Scan using tenk1_unique2 on tenk1 t2 -> Parallel Hash - -> Parallel Index Scan using tenk1_unique2 on tenk1 t2 + -> Parallel Index Only Scan using tenk1_unique1 on tenk1 t1 -> Parallel Hash -> Parallel Index Only Scan using tenk1_unique1 on tenk1 t3 (15 rows) @@ -1609,13 +1611,13 @@ from tenk1 t, generate_series(1, 1000); --------------------------------------------------------------------------------- Unique -> Sort - Sort Key: t.unique1, ((SubPlan 1)) + Sort Key: t.unique1, ((SubPlan expr_1)) -> Gather Workers Planned: 2 -> Nested Loop -> Parallel Index Only Scan using tenk1_unique1 on tenk1 t -> Function Scan on generate_series - SubPlan 1 + SubPlan expr_1 -> Index Only Scan using tenk1_unique1 on tenk1 Index Cond: (unique1 = t.unique1) (11 rows) @@ -1628,13 +1630,13 @@ order by 1, 2; QUERY PLAN --------------------------------------------------------------------------- Sort - Sort Key: t.unique1, ((SubPlan 1)) + Sort Key: t.unique1, ((SubPlan expr_1)) -> Gather Workers Planned: 2 -> Nested Loop -> Parallel Index Only Scan using tenk1_unique1 on tenk1 t -> Function Scan on generate_series - SubPlan 1 + SubPlan expr_1 -> Index Only Scan using tenk1_unique1 on tenk1 Index Cond: (unique1 = t.unique1) (10 rows) @@ -1722,3 +1724,43 @@ order by t1.four, t1.two limit 1; -> Seq Scan on tenk1 t2 (12 rows) +-- +-- Test incremental sort for Append/MergeAppend +-- +create table prt_tbl (a int, b int) partition by range (a); +create table prt_tbl_1 partition of prt_tbl for values from (0) to (100); +create table prt_tbl_2 partition of prt_tbl for values from (100) to (200); +insert into prt_tbl select i%200, i from generate_series(1,1000)i; +create index on prt_tbl_1(a); +create index on prt_tbl_2(a, b); +analyze prt_tbl; +set enable_seqscan to off; +set enable_bitmapscan to off; +-- Ensure we get an incremental sort for the subpath of Append +explain (costs off) select * from prt_tbl order by a, b; + QUERY PLAN +------------------------------------------------------------ + Append + -> Incremental Sort + Sort Key: prt_tbl_1.a, prt_tbl_1.b + Presorted Key: prt_tbl_1.a + -> Index Scan using prt_tbl_1_a_idx on prt_tbl_1 + -> Index Only Scan using prt_tbl_2_a_b_idx on prt_tbl_2 +(6 rows) + +-- Ensure we get an incremental sort for the subpath of MergeAppend +explain (costs off) select * from prt_tbl_1 union all select * from prt_tbl_2 order by a, b; + QUERY PLAN +------------------------------------------------------------ + Merge Append + Sort Key: prt_tbl_1.a, prt_tbl_1.b + -> Incremental Sort + Sort Key: prt_tbl_1.a, prt_tbl_1.b + Presorted Key: prt_tbl_1.a + -> Index Scan using prt_tbl_1_a_idx on prt_tbl_1 + -> Index Only Scan using prt_tbl_2_a_b_idx on prt_tbl_2 +(7 rows) + +reset enable_bitmapscan; +reset enable_seqscan; +drop table prt_tbl; diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out index bcf1db11d731d..929feda6fa3ce 100644 --- a/src/test/regress/expected/indexing.out +++ b/src/test/regress/expected/indexing.out @@ -248,7 +248,7 @@ alter index idxpart_a_b_idx attach partition idxpart1_a_b_idx; -- quiet create index idxpart1_2_a_b on idxpart1 (a, b); alter index idxpart_a_b_idx attach partition idxpart1_2_a_b; ERROR: cannot attach index "idxpart1_2_a_b" as a partition of index "idxpart_a_b_idx" -DETAIL: Another index is already attached for partition "idxpart1". +DETAIL: Another index "idxpart1_a_b_idx" is already attached for partition "idxpart1". drop table idxpart; -- make sure everything's gone select indexrelid::regclass, indrelid::regclass @@ -540,6 +540,111 @@ select relname, indisvalid from pg_class join pg_index on indexrelid = oid idxpart_a_idx | t (3 rows) +drop table idxpart; +-- Verify that re-attaching an already-attached partition index can +-- validate the parent index if it was still invalid, including +-- indirect ancestors in subpartitions. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (1000) partition by range (a); +create table idxpart11 partition of idxpart1 for values from (0) to (500); +-- Partitioned table with no partitions +create table idxpart2 partition of idxpart for values from (1000) to (2000) partition by range (a); +-- create parent indexes +create index on only idxpart ((a/b)); +create index on only idxpart1 ((a/b)); +create index on only idxpart2 ((a/b)); +-- fail, leaves behind an invalid index on the leaf partition +insert into idxpart11 values (1, 0); +create index concurrently on idxpart11 ((a/b)); +ERROR: division by zero +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +--------------------+------------ + idxpart11_expr_idx | f + idxpart1_expr_idx | f + idxpart2_expr_idx | t + idxpart_expr_idx | f +(4 rows) + +-- attach the indexes; parents stay invalid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +--------------------+------------ + idxpart11_expr_idx | f + idxpart1_expr_idx | f + idxpart2_expr_idx | t + idxpart_expr_idx | f +(4 rows) + +-- fix the index on the leaf partition +delete from idxpart11 where b = 0; +reindex index concurrently idxpart11_expr_idx; +-- reattach the leaf partition index; parents should now be valid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +--------------------+------------ + idxpart11_expr_idx | t + idxpart1_expr_idx | t + idxpart2_expr_idx | t + idxpart_expr_idx | t +(4 rows) + +drop table idxpart; +-- Verify that re-attaching does not validate the parent when another +-- child index is still invalid. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (500); +create table idxpart2 partition of idxpart for values from (500) to (1000); +create index on only idxpart ((a/b)); +-- create invalid indexes on both children +insert into idxpart1 values (1, 0); +insert into idxpart2 values (501, 0); +create index concurrently on idxpart1 ((a/b)); +ERROR: division by zero +create index concurrently on idxpart2 ((a/b)); +ERROR: division by zero +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +-------------------+------------ + idxpart1_expr_idx | f + idxpart2_expr_idx | f + idxpart_expr_idx | f +(3 rows) + +-- attach both; parent stays invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +-------------------+------------ + idxpart1_expr_idx | f + idxpart2_expr_idx | f + idxpart_expr_idx | f +(3 rows) + +-- fix only idxpart1's index, leave idxpart2's still invalid +delete from idxpart1 where b = 0; +reindex index concurrently idxpart1_expr_idx; +-- re-attach the fixed child; parent should stay invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; + relname | indisvalid +-------------------+------------ + idxpart1_expr_idx | t + idxpart2_expr_idx | f + idxpart_expr_idx | f +(3 rows) + drop table idxpart; -- verify dependency handling during ALTER TABLE DETACH PARTITION create table idxpart (a int) partition by range (a); @@ -972,16 +1077,16 @@ Indexes: drop table idxpart; -- Failing to use the full partition key is not allowed create table idxpart (a int unique, b int) partition by range (a, b); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: UNIQUE constraint on partitioned table must include all partitioning columns DETAIL: UNIQUE constraint on table "idxpart" lacks column "b" which is part of the partition key. create table idxpart (a int, b int unique) partition by range (a, b); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: UNIQUE constraint on partitioned table must include all partitioning columns DETAIL: UNIQUE constraint on table "idxpart" lacks column "a" which is part of the partition key. create table idxpart (a int primary key, b int) partition by range (b, a); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: PRIMARY KEY constraint on partitioned table must include all partitioning columns DETAIL: PRIMARY KEY constraint on table "idxpart" lacks column "b" which is part of the partition key. create table idxpart (a int, b int primary key) partition by range (b, a); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: PRIMARY KEY constraint on partitioned table must include all partitioning columns DETAIL: PRIMARY KEY constraint on table "idxpart" lacks column "a" which is part of the partition key. -- OK if you use them in some other order create table idxpart (a int, b int, c text, primary key (a, b, c)) partition by range (b, c, a); @@ -997,7 +1102,7 @@ create table idxpart (a int4range, b int4range, exclude USING GIST (a with =, b drop table idxpart; -- Not OK more than one equal column: partition keys are a proper superset of constraint create table idxpart (a int4range, b int4range, exclude USING GIST (a with = )) partition by range (a, b); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: EXCLUDE constraint on partitioned table must include all partitioning columns DETAIL: EXCLUDE constraint on table "idxpart" lacks column "b" which is part of the partition key. -- Not OK with just -|- create table idxpart (a int4range, exclude USING GIST (a with -|- )) partition by range (a); @@ -1007,7 +1112,7 @@ create table idxpart (a int4range, b int4range, exclude USING GIST (a with =, b drop table idxpart; -- Not OK with equals and &&, and equals is not the partition key create table idxpart (a int4range, b int4range, c int4range, exclude USING GIST (b with =, c with &&)) partition by range (a); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: EXCLUDE constraint on partitioned table must include all partitioning columns DETAIL: EXCLUDE constraint on table "idxpart" lacks column "a" which is part of the partition key. -- OK more than one equal column and a && column create table idxpart (a int4range, b int4range, c int4range, exclude USING GIST (a with =, b with =, c with &&)) partition by range (a, b); @@ -1022,7 +1127,7 @@ DETAIL: UNIQUE constraints cannot be used when partition keys include expressio -- use ALTER TABLE to add a primary key create table idxpart (a int, b int, c text) partition by range (a, b); alter table idxpart add primary key (a); -- not an incomplete one though -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: PRIMARY KEY constraint on partitioned table must include all partitioning columns DETAIL: PRIMARY KEY constraint on table "idxpart" lacks column "b" which is part of the partition key. alter table idxpart add primary key (a, b); -- this works \d idxpart @@ -1053,7 +1158,7 @@ drop table idxpart; -- use ALTER TABLE to add a unique constraint create table idxpart (a int, b int) partition by range (a, b); alter table idxpart add unique (a); -- not an incomplete one though -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: UNIQUE constraint on partitioned table must include all partitioning columns DETAIL: UNIQUE constraint on table "idxpart" lacks column "b" which is part of the partition key. alter table idxpart add unique (b, a); -- this works \d idxpart @@ -1083,7 +1188,7 @@ drop table idxpart; -- Not OK more than one equal column: partition keys are a proper superset of constraint create table idxpart (a int4range, b int4range) partition by range (a, b); alter table idxpart add exclude USING GIST (a with =); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: EXCLUDE constraint on partitioned table must include all partitioning columns DETAIL: EXCLUDE constraint on table "idxpart" lacks column "b" which is part of the partition key. drop table idxpart; -- Not OK with just -|- @@ -1098,7 +1203,7 @@ drop table idxpart; -- Not OK with equals and &&, and equals is not the partition key create table idxpart (a int4range, b int4range, c int4range) partition by range (a); alter table idxpart add exclude USING GIST (b with =, c with &&); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: EXCLUDE constraint on partitioned table must include all partitioning columns DETAIL: EXCLUDE constraint on table "idxpart" lacks column "a" which is part of the partition key. drop table idxpart; -- OK more than one equal column and a && column @@ -1145,7 +1250,7 @@ drop table idxpart; create table idxpart (a int, b int, primary key (a)) partition by range (a); create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b); -- fail -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: PRIMARY KEY constraint on partitioned table must include all partitioning columns DETAIL: PRIMARY KEY constraint on table "idxpart2" lacks column "b" which is part of the partition key. drop table idxpart; -- Ditto for the ATTACH PARTITION case @@ -1153,7 +1258,7 @@ create table idxpart (a int unique, b int) partition by range (a); create table idxpart1 (a int not null, b int, unique (a, b)) partition by range (a, b); alter table idxpart attach partition idxpart1 for values from (1) to (1000); -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: UNIQUE constraint on partitioned table must include all partitioning columns DETAIL: UNIQUE constraint on table "idxpart1" lacks column "b" which is part of the partition key. DROP TABLE idxpart, idxpart1; -- Multi-layer partitioning works correctly in this case: @@ -1448,7 +1553,7 @@ insert into covidxpart values (4, 1); ERROR: duplicate key value violates unique constraint "covidxpart4_a_b_idx" DETAIL: Key (a)=(4) already exists. create unique index on covidxpart (b) include (a); -- should fail -ERROR: unique constraint on partitioned table must include all partitioning columns +ERROR: UNIQUE constraint on partitioned table must include all partitioning columns DETAIL: UNIQUE constraint on table "covidxpart" lacks column "a" which is part of the partition key. -- check that detaching a partition also detaches the primary key constraint create table parted_pk_detach_test (a int primary key) partition by list (a); @@ -1669,3 +1774,14 @@ reindex index test_pg_index_toast_index; drop index test_pg_index_toast_index; drop function test_pg_index_toast_func; drop table test_pg_index_toast_table; +-- test creation of an index involving a whole-row expression +create table test_pg_wholerow_index (a int, b text, c numeric); +create or replace function row_image(test_pg_wholerow_index) + returns test_pg_wholerow_index as $$select $1$$ language sql immutable; +insert into test_pg_wholerow_index values (1, 'multiplication', 1.0); +create index row_image_index + on test_pg_wholerow_index ((row_image(test_pg_wholerow_index))); +insert into test_pg_wholerow_index values (2, 'addition', 0); +drop index row_image_index; +drop function row_image(test_pg_wholerow_index); +drop table test_pg_wholerow_index; diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index f9b0c415cfdcc..3d8e8d8afd241 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -580,8 +580,9 @@ update some_tab set a = a + 1 where false; Update on public.some_tab -> Result Output: (some_tab.a + 1), NULL::oid, NULL::tid + Replaces: Scan on some_tab One-Time Filter: false -(4 rows) +(5 rows) update some_tab set a = a + 1 where false; explain (verbose, costs off) @@ -592,8 +593,9 @@ update some_tab set a = a + 1 where false returning b, a; Output: some_tab.b, some_tab.a -> Result Output: (some_tab.a + 1), NULL::oid, NULL::tid + Replaces: Scan on some_tab One-Time Filter: false -(5 rows) +(6 rows) update some_tab set a = a + 1 where false returning b, a; b | a @@ -699,8 +701,9 @@ explain update parted_tab set a = 2 where false; -------------------------------------------------------- Update on parted_tab (cost=0.00..0.00 rows=0 width=0) -> Result (cost=0.00..0.00 rows=0 width=10) + Replaces: Scan on parted_tab One-Time Filter: false -(3 rows) +(4 rows) drop table parted_tab; -- Check UPDATE with multi-level partitioned inherited target @@ -792,7 +795,8 @@ Number of child tables: 1 (Use \d+ to list them.) ff1 | integer | | | Check constraints: "p2chk" CHECK (ff1 > 10) -Inherits: p1 +Inherits: + p1 -- Test that child does not override inheritable constraints of the parent create table c2 (constraint p2chk check (ff1 > 10) no inherit) inherits (p1); --fails @@ -987,8 +991,9 @@ create table c2(f3 int) inherits(p1,p2); f3 | integer | | | Check constraints: "p2_f2_check" CHECK (f2 > 0) -Inherits: p1, - p2 +Inherits: + p1 + p2 create table c3 (f4 int) inherits(c1,c2); NOTICE: merging multiple inherited definitions of column "f1" @@ -1004,8 +1009,9 @@ NOTICE: merging multiple inherited definitions of column "f3" f4 | integer | | | Check constraints: "p2_f2_check" CHECK (f2 > 0) -Inherits: c1, - c2 +Inherits: + c1 + c2 drop table p1 cascade; NOTICE: drop cascades to 3 other objects @@ -1026,7 +1032,8 @@ alter table pp1 add column a1 int check (a1 > 0); a1 | integer | | | Check constraints: "pp1_a1_check" CHECK (a1 > 0) -Inherits: pp1 +Inherits: + pp1 create table cc2(f4 float) inherits(pp1,cc1); NOTICE: merging multiple inherited definitions of column "f1" @@ -1042,8 +1049,9 @@ NOTICE: merging multiple inherited definitions of column "a1" f4 | double precision | | | Check constraints: "pp1_a1_check" CHECK (a1 > 0) -Inherits: pp1, - cc1 +Inherits: + pp1 + cc1 alter table pp1 add column a2 int check (a2 > 0); NOTICE: merging definition of column "a2" for child "cc2" @@ -1061,8 +1069,9 @@ NOTICE: merging constraint "pp1_a2_check" with inherited definition Check constraints: "pp1_a1_check" CHECK (a1 > 0) "pp1_a2_check" CHECK (a2 > 0) -Inherits: pp1, - cc1 +Inherits: + pp1 + cc1 drop table pp1 cascade; NOTICE: drop cascades to 2 other objects @@ -1087,8 +1096,9 @@ ALTER TABLE inhts RENAME d TO dd; b | integer | | | | plain | | c | integer | | | | plain | | dd | integer | | | | plain | | -Inherits: inht1, - inhs1 +Inherits: + inht1 + inhs1 DROP TABLE inhts; -- Test for adding a column to a parent table with complex inheritance @@ -1107,8 +1117,9 @@ NOTICE: merging definition of column "j" for child "inhtd" --------+---------+-----------+----------+---------+---------+--------------+------------- i | integer | | | | plain | | j | bigint | | | 1 | plain | | -Child tables: inhtb, - inhtd +Child tables: + inhtb + inhtd \d+ inhtd Table "public.inhtd" @@ -1116,9 +1127,10 @@ Child tables: inhtb, --------+---------+-----------+----------+---------+---------+--------------+------------- i | integer | | | | plain | | j | bigint | | | 1 | plain | | -Inherits: inhta, - inhtb, - inhtc +Inherits: + inhta + inhtb + inhtc DROP TABLE inhta, inhtb, inhtc, inhtd; -- Test for renaming in diamond inheritance @@ -1137,8 +1149,9 @@ ALTER TABLE inht1 RENAME aa TO aaa; x | integer | | | | plain | | y | integer | | | | plain | | z | integer | | | | plain | | -Inherits: inht2, - inht3 +Inherits: + inht2 + inht3 CREATE TABLE inhts (d int) INHERITS (inht2, inhs1); NOTICE: merging multiple inherited definitions of column "b" @@ -1154,8 +1167,9 @@ ERROR: cannot rename inherited column "b" x | integer | | | | plain | | c | integer | | | | plain | | d | integer | | | | plain | | -Inherits: inht2, - inhs1 +Inherits: + inht2 + inhs1 WITH RECURSIVE r AS ( SELECT 'inht1'::regclass AS inhrelid @@ -1201,7 +1215,8 @@ CREATE TABLE test_constraints_inh () INHERITS (test_constraints); val2 | integer | | | | plain | | Indexes: "test_constraints_val1_val2_key" UNIQUE CONSTRAINT, btree (val1, val2) -Child tables: test_constraints_inh +Child tables: + test_constraints_inh ALTER TABLE ONLY test_constraints DROP CONSTRAINT test_constraints_val1_val2_key; \d+ test_constraints @@ -1211,7 +1226,8 @@ ALTER TABLE ONLY test_constraints DROP CONSTRAINT test_constraints_val1_val2_key id | integer | | | | plain | | val1 | character varying | | | | extended | | val2 | integer | | | | plain | | -Child tables: test_constraints_inh +Child tables: + test_constraints_inh \d+ test_constraints_inh Table "public.test_constraints_inh" @@ -1220,7 +1236,8 @@ Child tables: test_constraints_inh id | integer | | | | plain | | val1 | character varying | | | | extended | | val2 | integer | | | | plain | | -Inherits: test_constraints +Inherits: + test_constraints DROP TABLE test_constraints_inh; DROP TABLE test_constraints; @@ -1236,7 +1253,8 @@ CREATE TABLE test_ex_constraints_inh () INHERITS (test_ex_constraints); c | circle | | | | plain | | Indexes: "test_ex_constraints_c_excl" EXCLUDE USING gist (c WITH &&) -Child tables: test_ex_constraints_inh +Child tables: + test_ex_constraints_inh ALTER TABLE test_ex_constraints DROP CONSTRAINT test_ex_constraints_c_excl; \d+ test_ex_constraints @@ -1244,14 +1262,16 @@ ALTER TABLE test_ex_constraints DROP CONSTRAINT test_ex_constraints_c_excl; Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+--------+-----------+----------+---------+---------+--------------+------------- c | circle | | | | plain | | -Child tables: test_ex_constraints_inh +Child tables: + test_ex_constraints_inh \d+ test_ex_constraints_inh Table "public.test_ex_constraints_inh" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+--------+-----------+----------+---------+---------+--------------+------------- c | circle | | | | plain | | -Inherits: test_ex_constraints +Inherits: + test_ex_constraints DROP TABLE test_ex_constraints_inh; DROP TABLE test_ex_constraints; @@ -1278,7 +1298,8 @@ Not-null constraints: id1 | integer | | | | plain | | Foreign-key constraints: "test_foreign_constraints_id1_fkey" FOREIGN KEY (id1) REFERENCES test_primary_constraints(id) -Child tables: test_foreign_constraints_inh +Child tables: + test_foreign_constraints_inh ALTER TABLE test_foreign_constraints DROP CONSTRAINT test_foreign_constraints_id1_fkey; \d+ test_foreign_constraints @@ -1286,14 +1307,16 @@ ALTER TABLE test_foreign_constraints DROP CONSTRAINT test_foreign_constraints_id Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- id1 | integer | | | | plain | | -Child tables: test_foreign_constraints_inh +Child tables: + test_foreign_constraints_inh \d+ test_foreign_constraints_inh Table "public.test_foreign_constraints_inh" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- id1 | integer | | | | plain | | -Inherits: test_foreign_constraints +Inherits: + test_foreign_constraints DROP TABLE test_foreign_constraints_inh; DROP TABLE test_foreign_constraints; @@ -1418,11 +1441,54 @@ order by 1, 2; p1_c3 | inh_check_constraint9 | f | 2 | t | t (38 rows) +-- +-- CHECK constraints +-- ALTER TABLE ALTER CONSTRAINT [NOT] ENFORCED +alter table p1 drop constraint inh_check_constraint1; +alter table p1_c1 drop constraint inh_check_constraint1; +alter table only p1 alter constraint inh_check_constraint3 enforced; --error +ERROR: constraint must be altered on child tables too +HINT: Do not specify the ONLY keyword. +alter table only p1 alter constraint inh_check_constraint3 not enforced; --error +ERROR: constraint must be altered on child tables too +HINT: Do not specify the ONLY keyword. +insert into p1_c1 values(-2); +insert into p1_c3 values(-3); +alter table p1 alter constraint inh_check_constraint3 enforced; --error +ERROR: check constraint "inh_check_constraint3" of relation "p1_c1" is violated by some row +delete from only p1_c1 where f1 = -2; +alter table p1_c1 alter constraint inh_check_constraint3 enforced; --error +ERROR: check constraint "inh_check_constraint3" of relation "p1_c3" is violated by some row +delete from only p1_c3 where f1 = -3; +alter table p1 alter constraint inh_check_constraint3 enforced; --ok +alter table p1 alter constraint inh_check_constraint3 not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'inh_check_constraint3' and contype = 'c' +order by conrelid::regclass::text collate "C"; + conname | conenforced | convalidated | conrelid +-----------------------+-------------+--------------+---------- + inh_check_constraint3 | f | f | p1 + inh_check_constraint3 | f | f | p1_c1 + inh_check_constraint3 | f | f | p1_c2 + inh_check_constraint3 | f | f | p1_c3 +(4 rows) + drop table p1 cascade; NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table p1_c1 drop cascades to table p1_c2 drop cascades to table p1_c3 +--for "no inherit" check constraint, it will not recurse to child table +create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced); +create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced); +alter table p1_c1 inherit p1; +insert into p1_c1 values(-11); +alter table p1 alter constraint p1_a_check enforced; --ok +alter table p1_c1 alter constraint p1_a_check enforced; --error +ERROR: check constraint "p1_a_check" of relation "p1_c1" is violated by some row +drop table p1 cascade; +NOTICE: drop cascades to table p1_c1 -- -- Similarly, check the merging of existing constraints; a parent constraint -- marked as NOT ENFORCED can merge with an ENFORCED child constraint, but the @@ -1431,6 +1497,25 @@ drop cascades to table p1_c3 create table p1(f1 int constraint p1_a_check check (f1 > 0) not enforced); create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) enforced); alter table p1_c1 inherit p1; +insert into p1 values(-1); --ok +insert into p1_c1 values(-1); --error +ERROR: new row for relation "p1_c1" violates check constraint "p1_a_check" +DETAIL: Failing row contains (-1). +alter table p1 alter constraint p1_a_check enforced; --error +ERROR: check constraint "p1_a_check" of relation "p1" is violated by some row +truncate p1; +alter table p1 alter constraint p1_a_check enforced; --ok +alter table p1 alter constraint p1_a_check not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'p1_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; + conname | conenforced | convalidated | conrelid +------------+-------------+--------------+---------- + p1_a_check | f | f | p1 + p1_a_check | f | f | p1_c1 +(2 rows) + drop table p1 cascade; NOTICE: drop cascades to table p1_c1 create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); @@ -1455,7 +1540,8 @@ alter table p1 drop constraint f1_pos; f1 | integer | | | Check constraints: "f1_pos" CHECK (f1 > 0) -Inherits: p1 +Inherits: + p1 drop table p1 cascade; NOTICE: drop cascades to table p1_c1 @@ -1475,8 +1561,9 @@ alter table p1 drop constraint f1_pos; Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- f1 | integer | | | -Inherits: p1, - p2 +Inherits: + p1 + p2 Table "public.p1p2_c2" Column | Type | Collation | Nullable | Default @@ -1484,8 +1571,9 @@ Inherits: p1, f1 | integer | | | Check constraints: "f1_pos" CHECK (f1 > 0) -Inherits: p1, - p2 +Inherits: + p1 + p2 drop table p1, p2 cascade; NOTICE: drop cascades to 2 other objects @@ -1503,8 +1591,9 @@ NOTICE: merging multiple inherited definitions of column "f1" f1 | integer | | | Check constraints: "f1_pos" CHECK (f1 > 0) -Inherits: p1_c1, - p1_c2 +Inherits: + p1_c1 + p1_c2 alter table p1 drop constraint f1_pos; \d p1_c1c2 @@ -1512,8 +1601,9 @@ alter table p1 drop constraint f1_pos; Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- f1 | integer | | | -Inherits: p1_c1, - p1_c2 +Inherits: + p1_c1 + p1_c2 drop table p1 cascade; NOTICE: drop cascades to 3 other objects @@ -1538,9 +1628,10 @@ alter table p1_c2 drop constraint f1_pos; Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- f1 | integer | | | -Inherits: p1_c1, - p1_c2, - p1 +Inherits: + p1_c1 + p1_c2 + p1 drop table p1 cascade; NOTICE: drop cascades to 3 other objects @@ -1755,8 +1846,9 @@ explain (verbose, costs off) select min(1-id) from matest0; QUERY PLAN --------------------------------------------------------------------------------- Result - Output: (InitPlan 1).col1 - InitPlan 1 + Output: (InitPlan minmax_1).col1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit Output: ((1 - matest0.id)) -> Result @@ -1779,7 +1871,7 @@ explain (verbose, costs off) select min(1-id) from matest0; -> Index Scan using matest3i on public.matest3 matest0_4 Output: matest0_4.id, (1 - matest0_4.id) Index Cond: ((1 - matest0_4.id) IS NOT NULL) -(25 rows) +(26 rows) select min(1-id) from matest0; min @@ -1898,10 +1990,11 @@ ORDER BY thousand, tenthous; Merge Append Sort Key: tenk1.thousand, tenk1.tenthous -> Index Only Scan using tenk1_thous_tenthous on tenk1 - -> Sort + -> Incremental Sort Sort Key: tenk1_1.thousand, tenk1_1.thousand + Presorted Key: tenk1_1.thousand -> Index Only Scan using tenk1_thous_tenthous on tenk1 tenk1_1 -(6 rows) +(7 rows) explain (costs off) SELECT thousand, tenthous, thousand+tenthous AS x FROM tenk1 @@ -1942,7 +2035,8 @@ SELECT min(x) FROM QUERY PLAN -------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Merge Append Sort Key: a.unique1 @@ -1950,7 +2044,7 @@ SELECT min(x) FROM Index Cond: (unique1 IS NOT NULL) -> Index Only Scan using tenk1_unique2 on tenk1 b Index Cond: (unique2 IS NOT NULL) -(9 rows) +(10 rows) explain (costs off) SELECT min(y) FROM @@ -1960,7 +2054,8 @@ SELECT min(y) FROM QUERY PLAN -------------------------------------------------------------------- Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Merge Append Sort Key: a.unique1 @@ -1968,7 +2063,7 @@ SELECT min(y) FROM Index Cond: (unique1 IS NOT NULL) -> Index Only Scan using tenk1_unique2 on tenk1 b Index Cond: (unique2 IS NOT NULL) -(9 rows) +(10 rows) -- XXX planner doesn't recognize that index on unique2 is sufficiently sorted explain (costs off) @@ -1982,10 +2077,11 @@ ORDER BY x, y; Merge Append Sort Key: a.thousand, a.tenthous -> Index Only Scan using tenk1_thous_tenthous on tenk1 a - -> Sort + -> Incremental Sort Sort Key: b.unique2, b.unique2 + Presorted Key: b.unique2 -> Index Only Scan using tenk1_unique2 on tenk1 b -(6 rows) +(7 rows) -- exercise rescan code path via a repeatedly-evaluated subquery explain (costs off) @@ -2000,7 +2096,7 @@ FROM generate_series(1, 3) g(i); QUERY PLAN ---------------------------------------------------------------- Function Scan on generate_series g - SubPlan 1 + SubPlan array_1 -> Limit -> Merge Append Sort Key: ((d.d + g.i)) @@ -2040,19 +2136,19 @@ insert into inhpar select x, x::text from generate_series(1,5) x; insert into inhcld select x::text, x from generate_series(6,10) x; explain (verbose, costs off) update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1); - QUERY PLAN --------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------- Update on public.inhpar i Update on public.inhpar i_1 Update on public.inhcld i_2 -> Result - Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), i.tableoid, i.ctid + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i.tableoid, i.ctid -> Append -> Seq Scan on public.inhpar i_1 Output: i_1.f1, i_1.f2, i_1.tableoid, i_1.ctid -> Seq Scan on public.inhcld i_2 Output: i_2.f1, i_2.f2, i_2.tableoid, i_2.ctid - SubPlan 1 + SubPlan multiexpr_1 -> Limit Output: (i.f1), (((i.f2)::text || '-'::text)) -> Seq Scan on public.int4_tbl @@ -2088,21 +2184,21 @@ alter table inhpar attach partition inhcld2 for values from (5) to (100); insert into inhpar select x, x::text from generate_series(1,10) x; explain (verbose, costs off) update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1); - QUERY PLAN ------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------ Update on public.inhpar i Update on public.inhcld1 i_1 Update on public.inhcld2 i_2 -> Append -> Seq Scan on public.inhcld1 i_1 - Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), i_1.tableoid, i_1.ctid - SubPlan 1 + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i_1.tableoid, i_1.ctid + SubPlan multiexpr_1 -> Limit Output: (i_1.f1), (((i_1.f2)::text || '-'::text)) -> Seq Scan on public.int4_tbl Output: i_1.f1, ((i_1.f2)::text || '-'::text) -> Seq Scan on public.inhcld2 i_2 - Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), i_2.tableoid, i_2.ctid + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i_2.tableoid, i_2.ctid (13 rows) update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1); @@ -2187,9 +2283,10 @@ alter table pp1 alter f1 set not null; f4 | double precision | | | | plain | | Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) -Inherits: pp1, - cc1, - cc2 +Inherits: + pp1 + cc1 + cc2 alter table cc3 no inherit pp1; alter table cc3 no inherit cc1; @@ -2219,8 +2316,10 @@ alter table cc1 add column a2 int constraint nn not null; Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) "nn" NOT NULL "a2" -Inherits: pp1 -Child tables: cc2 +Inherits: + pp1 +Child tables: + cc2 \d+ cc2 Table "public.cc2" @@ -2234,8 +2333,9 @@ Child tables: cc2 Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) "nn" NOT NULL "a2" (inherited) -Inherits: pp1, - cc1 +Inherits: + pp1 + cc1 alter table pp1 alter column f1 set not null; \d+ pp1 @@ -2245,8 +2345,9 @@ alter table pp1 alter column f1 set not null; f1 | integer | | not null | | plain | | Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" -Child tables: cc1, - cc2 +Child tables: + cc1 + cc2 \d+ cc1 Table "public.cc1" @@ -2259,8 +2360,10 @@ Child tables: cc1, Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) "nn" NOT NULL "a2" -Inherits: pp1 -Child tables: cc2 +Inherits: + pp1 +Child tables: + cc2 \d+ cc2 Table "public.cc2" @@ -2274,14 +2377,15 @@ Child tables: cc2 Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) "nn" NOT NULL "a2" (inherited) -Inherits: pp1, - cc1 +Inherits: + pp1 + cc1 -- cannot create table with inconsistent NO INHERIT constraint create table cc3 (a2 int not null no inherit) inherits (cc1); NOTICE: moving and merging column "a2" with inherited definition DETAIL: User-specified column moved to the position of the inherited column. -ERROR: cannot define not-null constraint on column "a2" with NO INHERIT +ERROR: cannot define not-null constraint with NO INHERIT on column "a2" DETAIL: The column has an inherited not-null constraint. -- change NO INHERIT status of inherited constraint: no dice, it's inherited alter table cc2 add not null a2 no inherit; @@ -2302,8 +2406,10 @@ alter table cc1 alter column a2 drop not null; a2 | integer | | | | plain | | Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) -Inherits: pp1 -Child tables: cc2 +Inherits: + pp1 +Child tables: + cc2 -- same for cc2 alter table cc2 alter column f1 drop not null; @@ -2319,8 +2425,9 @@ ERROR: cannot drop inherited constraint "pp1_f1_not_null" of relation "cc2" a2 | integer | | | | plain | | Not-null constraints: "pp1_f1_not_null" NOT NULL "f1" (inherited) -Inherits: pp1, - cc1 +Inherits: + pp1 + cc1 -- remove from cc1, should fail again alter table cc1 alter column f1 drop not null; @@ -2332,8 +2439,9 @@ alter table pp1 alter column f1 drop not null; Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- f1 | integer | | | | plain | | -Child tables: cc1, - cc2 +Child tables: + cc1 + cc2 alter table pp1 add primary key (f1); -- Leave these tables around, for pg_upgrade testing @@ -2385,8 +2493,9 @@ alter table inh_pp1 alter column f1 drop not null; f2 | text | | | | extended | | f3 | integer | | | | plain | | f4 | double precision | | | | plain | | -Inherits: inh_pp1, - inh_cc1 +Inherits: + inh_pp1 + inh_cc1 drop table inh_pp1, inh_cc1, inh_cc2; -- Test a not-null addition that must walk down the hierarchy @@ -2410,8 +2519,9 @@ create table inh_child1 () inherits (inh_parent1, inh_parent2); Not-null constraints: "nn" NOT NULL "a" (inherited) "inh_child1_b_not_null" NOT NULL "b" (inherited) -Inherits: inh_parent1, - inh_parent2 +Inherits: + inh_parent1 + inh_parent2 create table inh_child2 (constraint foo not null a) inherits (inh_parent1, inh_parent2); alter table inh_child2 no inherit inh_parent2; @@ -2424,7 +2534,8 @@ alter table inh_child2 no inherit inh_parent2; Not-null constraints: "foo" NOT NULL "a" (local, inherited) "nn" NOT NULL "b" -Inherits: inh_parent1 +Inherits: + inh_parent1 drop table inh_parent1, inh_parent2, inh_child1, inh_child2; -- Test multiple parents with overlapping primary keys @@ -2463,8 +2574,9 @@ Not-null constraints: "inh_parent1_a_not_null" NOT NULL "a" (inherited) "inh_parent1_b_not_null" NOT NULL "b" (inherited) "inh_parent2_d_not_null" NOT NULL "d" (inherited) -Inherits: inh_parent1, - inh_parent2 +Inherits: + inh_parent1 + inh_parent2 drop table inh_parent1, inh_parent2, inh_child; -- NOT NULL NO INHERIT @@ -2488,13 +2600,15 @@ select conrelid::regclass, conname, contype, conkey, Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | | | plain | | -Inherits: inh_nn_parent +Inherits: + inh_nn_parent Table "public.inh_nn_child2" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- a | integer | | | | plain | | -Inherits: inh_nn_parent +Inherits: + inh_nn_parent Table "public.inh_nn_parent" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description @@ -2502,8 +2616,9 @@ Inherits: inh_nn_parent a | integer | | not null | | plain | | Not-null constraints: "inh_nn_parent_a_not_null" NOT NULL "a" NO INHERIT -Child tables: inh_nn_child, - inh_nn_child2 +Child tables: + inh_nn_child + inh_nn_child2 drop table inh_nn_parent, inh_nn_child, inh_nn_child2; CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT); @@ -2530,7 +2645,7 @@ ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" CREATE TABLE inh_nn1 (a int not null); CREATE TABLE inh_nn2 (a int not null no inherit) INHERITS (inh_nn1); NOTICE: merging column "a" with inherited definition -ERROR: cannot define not-null constraint on column "a" with NO INHERIT +ERROR: cannot define not-null constraint with NO INHERIT on column "a" DETAIL: The column has an inherited not-null constraint. CREATE TABLE inh_nn3 (a int not null, b int, not null a no inherit); ERROR: conflicting NO INHERIT declaration for not-null constraint on column "a" @@ -2562,7 +2677,8 @@ alter table inh_parent alter column f1 set not null; f1 | integer | | not null | | plain | | Not-null constraints: "inh_parent_f1_not_null" NOT NULL "f1" -Child tables: inh_child1 +Child tables: + inh_child1 \d+ inh_child1 Table "public.inh_child1" @@ -2571,8 +2687,10 @@ Child tables: inh_child1 f1 | integer | | not null | | plain | | Not-null constraints: "inh_child1_f1_not_null" NOT NULL "f1" (local, inherited) -Inherits: inh_parent -Child tables: inh_child2 +Inherits: + inh_parent +Child tables: + inh_child2 \d+ inh_child2 Table "public.inh_child2" @@ -2581,7 +2699,8 @@ Child tables: inh_child2 f1 | integer | | not null | | plain | | Not-null constraints: "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) -Inherits: inh_child1 +Inherits: + inh_child1 select conrelid::regclass, conname, contype, coninhcount, conislocal from pg_constraint where contype = 'n' and @@ -2615,8 +2734,9 @@ Not-null constraints: f1 | integer | | not null | | plain | | Not-null constraints: "inh_child1_f1_not_null" NOT NULL "f1" -Child tables: inh_child2, - inh_child3 +Child tables: + inh_child2 + inh_child3 \d+ inh_child2 Table "public.inh_child2" @@ -2625,7 +2745,8 @@ Child tables: inh_child2, f1 | integer | | not null | | plain | | Not-null constraints: "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited) -Inherits: inh_child1 +Inherits: + inh_child1 select conrelid::regclass, conname, contype, coninhcount, conislocal from pg_constraint where contype = 'n' and @@ -3085,11 +3206,12 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and /* Should select no rows because range partition key cannot be null */ explain (costs off) select * from range_list_parted where a is null; - QUERY PLAN --------------------------- + QUERY PLAN +--------------------------------------- Result + Replaces: Scan on range_list_parted One-Time Filter: false -(2 rows) +(3 rows) /* Should only select rows from the null-accepting partition */ explain (costs off) select * from range_list_parted where b is null; @@ -3250,15 +3372,16 @@ explain (costs off) select min(a), max(a) from parted_minmax where b = '12345'; QUERY PLAN ------------------------------------------------------------------------------------------------ Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan using parted_minmax1i on parted_minmax1 parted_minmax Index Cond: ((a IS NOT NULL) AND (b = '12345'::text)) - InitPlan 2 + InitPlan minmax_2 -> Limit -> Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax_1 Index Cond: ((a IS NOT NULL) AND (b = '12345'::text)) -(9 rows) +(10 rows) select min(a), max(a) from parted_minmax where b = '12345'; min | max diff --git a/src/test/regress/expected/insert.out b/src/test/regress/expected/insert.out index cf4b5221a8da1..75b8de79fce49 100644 --- a/src/test/regress/expected/insert.out +++ b/src/test/regress/expected/insert.out @@ -567,13 +567,14 @@ from hash_parted order by part; a | text | | | | extended | | b | integer | | | | plain | | Partition key: LIST (lower(a)) -Partitions: part_aa_bb FOR VALUES IN ('aa', 'bb'), - part_cc_dd FOR VALUES IN ('cc', 'dd'), - part_ee_ff FOR VALUES IN ('ee', 'ff'), PARTITIONED, - part_gg FOR VALUES IN ('gg'), PARTITIONED, - part_null FOR VALUES IN (NULL), - part_xx_yy FOR VALUES IN ('xx', 'yy'), PARTITIONED, - part_default DEFAULT, PARTITIONED +Partitions: + part_aa_bb FOR VALUES IN ('aa', 'bb') + part_cc_dd FOR VALUES IN ('cc', 'dd') + part_ee_ff FOR VALUES IN ('ee', 'ff'), PARTITIONED + part_gg FOR VALUES IN ('gg'), PARTITIONED + part_null FOR VALUES IN (NULL) + part_xx_yy FOR VALUES IN ('xx', 'yy'), PARTITIONED + part_default DEFAULT, PARTITIONED -- cleanup drop table range_parted, list_parted; @@ -972,14 +973,15 @@ create table mcrparted8_ge_d partition of mcrparted for values from ('d', minval a | text | | | | extended | | b | integer | | | | plain | | Partition key: RANGE (a, b) -Partitions: mcrparted1_lt_b FOR VALUES FROM (MINVALUE, MINVALUE) TO ('b', MINVALUE), - mcrparted2_b FOR VALUES FROM ('b', MINVALUE) TO ('c', MINVALUE), - mcrparted3_c_to_common FOR VALUES FROM ('c', MINVALUE) TO ('common', MINVALUE), - mcrparted4_common_lt_0 FOR VALUES FROM ('common', MINVALUE) TO ('common', 0), - mcrparted5_common_0_to_10 FOR VALUES FROM ('common', 0) TO ('common', 10), - mcrparted6_common_ge_10 FOR VALUES FROM ('common', 10) TO ('common', MAXVALUE), - mcrparted7_gt_common_lt_d FOR VALUES FROM ('common', MAXVALUE) TO ('d', MINVALUE), - mcrparted8_ge_d FOR VALUES FROM ('d', MINVALUE) TO (MAXVALUE, MAXVALUE) +Partitions: + mcrparted1_lt_b FOR VALUES FROM (MINVALUE, MINVALUE) TO ('b', MINVALUE) + mcrparted2_b FOR VALUES FROM ('b', MINVALUE) TO ('c', MINVALUE) + mcrparted3_c_to_common FOR VALUES FROM ('c', MINVALUE) TO ('common', MINVALUE) + mcrparted4_common_lt_0 FOR VALUES FROM ('common', MINVALUE) TO ('common', 0) + mcrparted5_common_0_to_10 FOR VALUES FROM ('common', 0) TO ('common', 10) + mcrparted6_common_ge_10 FOR VALUES FROM ('common', 10) TO ('common', MAXVALUE) + mcrparted7_gt_common_lt_d FOR VALUES FROM ('common', MAXVALUE) TO ('d', MINVALUE) + mcrparted8_ge_d FOR VALUES FROM ('d', MINVALUE) TO (MAXVALUE, MAXVALUE) \d+ mcrparted1_lt_b Table "public.mcrparted1_lt_b" diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out index fdd0f6c8f258c..34e2e7ee355e1 100644 --- a/src/test/regress/expected/insert_conflict.out +++ b/src/test/regress/expected/insert_conflict.out @@ -2,6 +2,19 @@ -- insert...on conflict do unique index inference -- create table insertconflicttest(key int4, fruit text); +-- invalid clauses +insert into insertconflicttest values (1) on conflict (key int4_ops (fillfactor=10)) do nothing; +ERROR: operator class options are not allowed in ON CONFLICT clause +LINE 1: ...t into insertconflicttest values (1) on conflict (key int4_o... + ^ +insert into insertconflicttest values (1) on conflict (key asc) do nothing; +ERROR: ASC/DESC is not allowed in ON CONFLICT clause +LINE 1: ...t into insertconflicttest values (1) on conflict (key asc) d... + ^ +insert into insertconflicttest values (1) on conflict (key nulls last) do nothing; +ERROR: NULLS FIRST/LAST is not allowed in ON CONFLICT clause +LINE 1: ...t into insertconflicttest values (1) on conflict (key nulls ... + ^ -- These things should work through a view, as well create view insertconflictview as select * from insertconflicttest; -- @@ -61,9 +74,9 @@ explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on con Insert on insertconflicttest Conflict Resolution: UPDATE Conflict Arbiter Indexes: op_index_key, collation_index_key, both_index_key - Conflict Filter: EXISTS(SubPlan 1) + Conflict Filter: EXISTS(SubPlan exists_1) -> Result - SubPlan 1 + SubPlan exists_1 -> Index Only Scan using both_index_expr_key on insertconflicttest ii Index Cond: (key = excluded.key) (8 rows) @@ -236,6 +249,25 @@ explain (costs off, format json) insert into insertconflicttest values (0, 'Bilb ] (1 row) +-- Should display lock strength, if specified +explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select returning *; + QUERY PLAN +--------------------------------------- + Insert on insertconflicttest + Conflict Resolution: SELECT + Conflict Arbiter Indexes: key_index + -> Result +(4 rows) + +explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select for key share returning *; + QUERY PLAN +--------------------------------------------- + Insert on insertconflicttest + Conflict Resolution: SELECT FOR KEY SHARE + Conflict Arbiter Indexes: key_index + -> Result +(4 rows) + -- Fails (no unique index inference specification, required for do update variant): insert into insertconflicttest values (1, 'Apple') on conflict do update set fruit = excluded.fruit; ERROR: ON CONFLICT DO UPDATE requires inference specification or constraint name @@ -291,6 +323,48 @@ ERROR: column "insertconflicttest" of relation "insertconflicttest" does not ex LINE 1: ...3, 'Kiwi') on conflict (key, fruit) do update set insertconf... ^ HINT: SET target columns cannot be qualified with the relation name. +-- +-- DO SELECT tests +-- +delete from insertconflicttest where fruit = 'Apple'; +insert into insertconflicttest values (1, 'Apple') on conflict (key) do select; -- fails +ERROR: ON CONFLICT DO SELECT requires a RETURNING clause +LINE 1: ...nsert into insertconflicttest values (1, 'Apple') on conflic... + ^ +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select returning old, new, i; + old | new | i +-----+-----------+----------- + | (1,Apple) | (1,Apple) +(1 row) + +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select returning old, new, i; + old | new | i +-----------+-----------+----------- + (1,Apple) | (1,Apple) | (1,Apple) +(1 row) + +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Apple' returning *; + key | fruit +-----+------- + 1 | Apple +(1 row) + +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Orange' returning *; + key | fruit +-----+------- +(0 rows) + +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Apple' returning *; + key | fruit +-----+------- +(0 rows) + +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Orange' returning *; + key | fruit +-----+------- + 1 | Apple +(1 row) + drop index key_index; -- -- Composite key tests @@ -735,13 +809,58 @@ insert into selfconflict values (6,1), (6,2) on conflict(f1) do update set f2 = ERROR: ON CONFLICT DO UPDATE command cannot affect row a second time HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values. commit; +begin transaction isolation level read committed; +insert into selfconflict values (7,1), (7,2) on conflict(f1) do select returning *; + f1 | f2 +----+---- + 7 | 1 + 7 | 1 +(2 rows) + +commit; +begin transaction isolation level repeatable read; +insert into selfconflict values (8,1), (8,2) on conflict(f1) do select returning *; + f1 | f2 +----+---- + 8 | 1 + 8 | 1 +(2 rows) + +commit; +begin transaction isolation level serializable; +insert into selfconflict values (9,1), (9,2) on conflict(f1) do select returning *; + f1 | f2 +----+---- + 9 | 1 + 9 | 1 +(2 rows) + +commit; +begin transaction isolation level read committed; +insert into selfconflict values (10,1), (10,2) on conflict(f1) do select for update returning *; +ERROR: ON CONFLICT DO SELECT command cannot affect row a second time +HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values. +commit; +begin transaction isolation level repeatable read; +insert into selfconflict values (11,1), (11,2) on conflict(f1) do select for update returning *; +ERROR: ON CONFLICT DO SELECT command cannot affect row a second time +HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values. +commit; +begin transaction isolation level serializable; +insert into selfconflict values (12,1), (12,2) on conflict(f1) do select for update returning *; +ERROR: ON CONFLICT DO SELECT command cannot affect row a second time +HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values. +commit; select * from selfconflict; f1 | f2 ----+---- 1 | 1 2 | 1 3 | 1 -(3 rows) + 7 | 1 + 8 | 1 + 9 | 1 +(6 rows) drop table selfconflict; -- check ON CONFLICT handling with partitioned tables @@ -752,11 +871,31 @@ insert into parted_conflict_test values (1, 'a') on conflict do nothing; -- index on a required, which does exist in parent insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing; insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test values (1, 'a') on conflict (a) do select returning *; + a | b +---+--- + 1 | a +(1 row) + +insert into parted_conflict_test values (1, 'a') on conflict (a) do select for update returning *; + a | b +---+--- + 1 | a +(1 row) + -- targeting partition directly will work insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing; insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do select returning b; + b +--- + b +(1 row) + -- index on b required, which doesn't exist in parent -insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; +insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; -- fail +ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification +insert into parted_conflict_test values (2, 'b') on conflict (b) do select returning b; -- fail ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification -- targeting partition directly will work insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a; @@ -767,13 +906,31 @@ select * from parted_conflict_test order by a; 2 | b (1 row) --- now check that DO UPDATE works correctly for target partition with --- different attribute numbers +-- now check that DO UPDATE and DO SELECT work correctly for target partition +-- with different attribute numbers create table parted_conflict_test_2 (b char, a int unique); alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3); truncate parted_conflict_test; insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select returning b; + b +--- + b +(1 row) + +insert into parted_conflict_test values (3, 'a') on conflict (a) do select where excluded.b = 'a' returning parted_conflict_test; + parted_conflict_test +---------------------- + (3,b) +(1 row) + +insert into parted_conflict_test values (3, 'a') on conflict (a) do select where parted_conflict_test.b = 'b' returning b; + b +--- + b +(1 row) + -- should see (3, 'b') select * from parted_conflict_test order by a; a | b @@ -787,6 +944,12 @@ create table parted_conflict_test_3 partition of parted_conflict_test for values truncate parted_conflict_test; insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a'; +insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do select returning b; + b +--- + b +(1 row) + -- should see (4, 'b') select * from parted_conflict_test order by a; a | b @@ -800,6 +963,11 @@ create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for va truncate parted_conflict_test; insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a'; +insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do select where parted_conflict_test.b = 'a' returning b; + b +--- +(0 rows) + -- should see (5, 'b') select * from parted_conflict_test order by a; a | b @@ -820,6 +988,59 @@ select * from parted_conflict_test order by a; 4 | b (3 rows) +-- test DO SELECT with multiple rows hitting different partitions +truncate parted_conflict_test; +insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'b'), (4, 'c'); +insert into parted_conflict_test (a, b) values (1, 'x'), (2, 'y'), (4, 'z') + on conflict (a) do select returning *, tableoid::regclass; + a | b | tableoid +---+---+------------------------ + 1 | a | parted_conflict_test_1 + 2 | b | parted_conflict_test_1 + 4 | c | parted_conflict_test_3 +(3 rows) + +-- should see original values (1, 'a'), (2, 'b'), (4, 'c') +select * from parted_conflict_test order by a; + a | b +---+--- + 1 | a + 2 | b + 4 | c +(3 rows) + +-- test DO SELECT with WHERE filtering across partitions +insert into parted_conflict_test (a, b) values (1, 'n') on conflict (a) do select where parted_conflict_test.b = 'a' returning *; + a | b +---+--- + 1 | a +(1 row) + +insert into parted_conflict_test (a, b) values (2, 'n') on conflict (a) do select where parted_conflict_test.b = 'x' returning *; + a | b +---+--- +(0 rows) + +-- test DO SELECT with EXCLUDED in WHERE across partitions with different layouts +insert into parted_conflict_test (a, b) values (3, 't') on conflict (a) do select where excluded.b = 't' returning *; + a | b +---+--- + 3 | t +(1 row) + +-- test DO SELECT FOR UPDATE across different partition layouts +insert into parted_conflict_test (a, b) values (1, 'l') on conflict (a) do select for update returning *; + a | b +---+--- + 1 | a +(1 row) + +insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do select for update returning *; + a | b +---+--- + 3 | t +(1 row) + drop table parted_conflict_test; -- test behavior of inserting a conflicting tuple into an intermediate -- partitioning level diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index f35a0b18c37cb..78bf022f7b4e3 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -2264,11 +2264,12 @@ explain (costs off) select aa, bb, unique1, unique1 from tenk1 right join b_star on aa = unique1 where bb < bb and bb is null; - QUERY PLAN --------------------------- + QUERY PLAN +----------------------------------- Result + Replaces: Join on tenk1, b_star One-Time Filter: false -(2 rows) +(3 rows) select aa, bb, unique1, unique1 from tenk1 right join b_star on aa = unique1 @@ -2374,7 +2375,7 @@ order by t1.unique1; Sort Sort Key: t1.unique1 -> Hash Join - Hash Cond: ((t1.two = t2.two) AND (t1.unique1 = (SubPlan 2))) + Hash Cond: ((t1.two = t2.two) AND (t1.unique1 = (SubPlan expr_1))) -> Bitmap Heap Scan on tenk1 t1 Recheck Cond: (unique1 < 10) -> Bitmap Index Scan on tenk1_unique1 @@ -2384,13 +2385,14 @@ order by t1.unique1; Recheck Cond: (unique1 < 10) -> Bitmap Index Scan on tenk1_unique1 Index Cond: (unique1 < 10) - SubPlan 2 + SubPlan expr_1 -> Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Only Scan using tenk1_unique1 on tenk1 Index Cond: ((unique1 IS NOT NULL) AND (unique1 = t2.unique1)) -(19 rows) +(20 rows) -- Ensure we get the expected result select t1.unique1,t2.unique1 from tenk1 t1 @@ -2655,8 +2657,8 @@ select * from int8_tbl t1 left join (int8_tbl t2 left join int8_tbl t3 full join int8_tbl t4 on false on false) left join int8_tbl t5 on t2.q1 = t5.q1 on t2.q2 = 123; - QUERY PLAN --------------------------------------------------- + QUERY PLAN +---------------------------------------------------- Nested Loop Left Join -> Seq Scan on int8_tbl t1 -> Materialize @@ -2667,9 +2669,10 @@ on t2.q2 = 123; -> Seq Scan on int8_tbl t2 Filter: (q2 = 123) -> Result + Replaces: Join on t3, t4 One-Time Filter: false -> Seq Scan on int8_tbl t5 -(12 rows) +(13 rows) explain (costs off) select * from int8_tbl t1 @@ -2837,20 +2840,22 @@ select x.thousand, x.twothousand, count(*) from tenk1 x inner join tenk1 y on x.thousand = y.thousand group by x.thousand, x.twothousand order by x.thousand desc, x.twothousand; - QUERY PLAN ----------------------------------------------------------------------------------- - GroupAggregate + QUERY PLAN +---------------------------------------------------------------------------------------- + Finalize GroupAggregate Group Key: x.thousand, x.twothousand -> Incremental Sort Sort Key: x.thousand DESC, x.twothousand Presorted Key: x.thousand -> Merge Join Merge Cond: (y.thousand = x.thousand) - -> Index Only Scan Backward using tenk1_thous_tenthous on tenk1 y + -> Partial GroupAggregate + Group Key: y.thousand + -> Index Only Scan Backward using tenk1_thous_tenthous on tenk1 y -> Sort Sort Key: x.thousand DESC -> Seq Scan on tenk1 x -(11 rows) +(13 rows) reset enable_hashagg; reset enable_nestloop; @@ -3075,6 +3080,33 @@ select * from tbl_rs t1 join 3 | 3 | 4 | 4 (6 rows) +-- +-- regression test for bug with parallel-hash-right-semi join +-- +begin; +-- encourage use of parallel plans +set local parallel_setup_cost=0; +set local parallel_tuple_cost=0; +set local min_parallel_table_scan_size=0; +set local max_parallel_workers_per_gather=4; +-- ensure we don't get parallel hash right semi join +explain (costs off) +select * from tenk1 t1 +where exists (select 1 from tenk1 t2 where fivethous = t1.fivethous) +and t1.fivethous < 5; + QUERY PLAN +-------------------------------------------------- + Gather + Workers Planned: 4 + -> Parallel Hash Semi Join + Hash Cond: (t1.fivethous = t2.fivethous) + -> Parallel Seq Scan on tenk1 t1 + Filter: (fivethous < 5) + -> Parallel Hash + -> Parallel Seq Scan on tenk1 t2 +(8 rows) + +rollback; -- -- regression test for bug #13908 (hash join with skew tuples & nbatch increase) -- @@ -3160,6 +3192,31 @@ ORDER BY 1; reset enable_nestloop; -- +-- test that estimate_hash_bucket_stats estimates correctly with skewed data +-- (we should choose to hash the filtered table) +-- +create temp table skewedtable (val int not null, filt int not null); +insert into skewedtable +select + case when g <= 100 then 0 else (g % 100) + 1 end, + g % 10 +from generate_series(1, 1000) g; +analyze skewedtable; +explain (costs off) +select * from skewedtable t1 join skewedtable t2 on t1.val = t2.val +where t1.filt = 5; + QUERY PLAN +---------------------------------------- + Hash Join + Hash Cond: (t2.val = t1.val) + -> Seq Scan on skewedtable t2 + -> Hash + -> Seq Scan on skewedtable t1 + Filter: (filt = 5) +(6 rows) + +drop table skewedtable; +-- -- basic semijoin and antijoin recognition tests -- explain (costs off) @@ -3178,11 +3235,11 @@ where unique1 in (select unique2 from tenk1 b); explain (costs off) select a.* from tenk1 a where unique1 not in (select unique2 from tenk1 b); - QUERY PLAN ------------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------------- Seq Scan on tenk1 a - Filter: (NOT (ANY (unique1 = (hashed SubPlan 1).col1))) - SubPlan 1 + Filter: (NOT (ANY (unique1 = (hashed SubPlan any_1).col1))) + SubPlan any_1 -> Index Only Scan using tenk1_unique2 on tenk1 b (4 rows) @@ -3222,6 +3279,86 @@ where b.unique2 is null; -> Index Only Scan using tenk1_unique2 on tenk1 b (5 rows) +-- check that we avoid de-duplicating columns redundantly +set enable_memoize to off; +explain (costs off) +select 1 from tenk1 +where (hundred, thousand) in (select twothousand, twothousand from onek); + QUERY PLAN +------------------------------------------------- + Hash Join + Hash Cond: (tenk1.hundred = onek.twothousand) + -> Seq Scan on tenk1 + Filter: (hundred = thousand) + -> Hash + -> HashAggregate + Group Key: onek.twothousand + -> Seq Scan on onek +(8 rows) + +reset enable_memoize; +-- +-- more antijoin recognition tests using NOT NULL constraints +-- +begin; +create temp table tbl_anti(a int not null, b int, c int); +-- this is an antijoin, as t2.a is non-null for any matching row +explain (costs off) +select * from tenk1 t1 left join tbl_anti t2 on t1.unique1 = t2.b +where t2.a is null; + QUERY PLAN +---------------------------------- + Hash Right Anti Join + Hash Cond: (t2.b = t1.unique1) + -> Seq Scan on tbl_anti t2 + -> Hash + -> Seq Scan on tenk1 t1 +(5 rows) + +-- this is an antijoin, as t2.a is non-null for any matching row +explain (costs off) +select * from tenk1 t1 left join + (tbl_anti t2 left join tbl_anti t3 on t2.c = t3.c) on t1.unique1 = t2.b +where t2.a is null; + QUERY PLAN +------------------------------------------- + Hash Right Anti Join + Hash Cond: (t2.b = t1.unique1) + -> Merge Left Join + Merge Cond: (t2.c = t3.c) + -> Sort + Sort Key: t2.c + -> Seq Scan on tbl_anti t2 + -> Sort + Sort Key: t3.c + -> Seq Scan on tbl_anti t3 + -> Hash + -> Seq Scan on tenk1 t1 +(12 rows) + +-- this is not an antijoin, as t3.a can be nulled by t2/t3 join +explain (costs off) +select * from tenk1 t1 left join + (tbl_anti t2 left join tbl_anti t3 on t2.c = t3.c) on t1.unique1 = t2.b +where t3.a is null; + QUERY PLAN +------------------------------------------- + Hash Right Join + Hash Cond: (t2.b = t1.unique1) + Filter: (t3.a IS NULL) + -> Merge Left Join + Merge Cond: (t2.c = t3.c) + -> Sort + Sort Key: t2.c + -> Seq Scan on tbl_anti t2 + -> Sort + Sort Key: t3.c + -> Seq Scan on tbl_anti t3 + -> Hash + -> Seq Scan on tenk1 t1 +(13 rows) + +rollback; -- -- regression test for bogus RTE_GROUP entries -- @@ -3253,10 +3390,13 @@ where not exists ( ); QUERY PLAN --------------------------------------------------------- - Hash Anti Join - Hash Cond: (t1.c1 = t2.c2) - -> Seq Scan on tt4x t1 - -> Hash + Merge Anti Join + Merge Cond: (t1.c1 = t2.c2) + -> Sort + Sort Key: t1.c1 + -> Seq Scan on tt4x t1 + -> Sort + Sort Key: t2.c2 -> Merge Right Join Merge Cond: (t5.c1 = t3.c2) -> Merge Join @@ -3277,7 +3417,7 @@ where not exists ( -> Sort Sort Key: t3.c1 -> Seq Scan on tt4x t3 -(24 rows) +(27 rows) -- -- regression test for problems of the sort depicted in bug #3494 @@ -3639,8 +3779,8 @@ from nt3 as nt3 ) as ss2 on ss2.id = nt3.nt2_id where nt3.id = 1 and ss2.b3; - QUERY PLAN ------------------------------------------------ + QUERY PLAN +---------------------------------------------- Nested Loop -> Nested Loop -> Index Scan using nt3_pkey on nt3 @@ -3649,7 +3789,7 @@ where nt3.id = 1 and ss2.b3; Index Cond: (id = nt3.nt2_id) -> Index Only Scan using nt1_pkey on nt1 Index Cond: (id = nt2.nt1_id) - Filter: (nt2.b1 AND (id IS NOT NULL)) + Filter: (nt2.b1 AND true) (9 rows) select nt3.id @@ -3685,11 +3825,11 @@ order by 1,2; Sort Key: t1.q1, t1.q2 -> Hash Left Join Hash Cond: (t1.q2 = t2.q1) - Filter: (1 = (SubPlan 1)) + Filter: (1 = (SubPlan expr_1)) -> Seq Scan on int8_tbl t1 -> Hash -> Seq Scan on int8_tbl t2 - SubPlan 1 + SubPlan expr_1 -> Limit -> Result One-Time Filter: ((42) IS NOT NULL) @@ -3946,6 +4086,59 @@ where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; (1 row) -- variant that isn't quite a star-schema case +explain (verbose, costs off) +select ss1.d1 from + tenk1 as t1 + inner join tenk1 as t2 + on t1.tenthous = t2.ten + inner join + int8_tbl as i8 + left join int4_tbl as i4 + inner join (select 64::information_schema.cardinal_number as d1 + from tenk1 t3, + lateral (select abs(t3.unique1) + random()) ss0(x) + where t3.fivethous < 0) as ss1 + on i4.f1 = ss1.d1 + on i8.q1 = i4.f1 + on t1.tenthous = ss1.d1 +where t1.unique1 < i4.f1; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Nested Loop + Output: (64)::information_schema.cardinal_number + Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer) + -> Seq Scan on public.tenk1 t3 + Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4 + Filter: (t3.fivethous < 0) + -> Nested Loop + Output: t1.tenthous, t2.ten + -> Nested Loop + Output: t1.tenthous, t2.ten, i4.f1 + Join Filter: (t1.unique1 < i4.f1) + -> Hash Join + Output: t1.tenthous, t1.unique1, t2.ten + Hash Cond: (t2.ten = t1.tenthous) + -> Seq Scan on public.tenk1 t2 + Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4 + -> Hash + Output: t1.tenthous, t1.unique1 + -> Nested Loop + Output: t1.tenthous, t1.unique1 + -> Subquery Scan on ss0 + Output: ss0.x, (64)::information_schema.cardinal_number + -> Result + Output: ((abs(t3.unique1))::double precision + random()) + -> Index Scan using tenk1_thous_tenthous on public.tenk1 t1 + Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4 + Index Cond: (t1.tenthous = (((64)::information_schema.cardinal_number))::integer) + -> Seq Scan on public.int4_tbl i4 + Output: i4.f1 + Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer) + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer) +(33 rows) + select ss1.d1 from tenk1 as t1 inner join tenk1 as t2 @@ -4035,6 +4228,196 @@ select * from 1 | 2 | 2 (1 row) +-- This example demonstrates the folly of our old "have_dangerous_phv" logic +begin; +set local from_collapse_limit to 2; +explain (verbose, costs off) +select * from int8_tbl t1 + left join + (select coalesce(t2.q1 + x, 0) from int8_tbl t2, + lateral (select t3.q1 as x from int8_tbl t3, + lateral (select t2.q1, t3.q1 offset 0) s)) + on true; + QUERY PLAN +------------------------------------------------------------------ + Nested Loop Left Join + Output: t1.q1, t1.q2, (COALESCE((t2.q1 + t3.q1), '0'::bigint)) + -> Seq Scan on public.int8_tbl t1 + Output: t1.q1, t1.q2 + -> Materialize + Output: (COALESCE((t2.q1 + t3.q1), '0'::bigint)) + -> Nested Loop + Output: COALESCE((t2.q1 + t3.q1), '0'::bigint) + -> Seq Scan on public.int8_tbl t2 + Output: t2.q1, t2.q2 + -> Nested Loop + Output: t3.q1 + -> Seq Scan on public.int8_tbl t3 + Output: t3.q1, t3.q2 + -> Result + Output: NULL::bigint, NULL::bigint +(16 rows) + +rollback; +-- ... not that the initial replacement didn't have some bugs too +begin; +create temp table t(i int primary key); +explain (verbose, costs off) +select * from t t1 + left join (select 1 as x, * from t t2(i2)) t2ss on t1.i = t2ss.i2 + left join t t3(i3) on false + left join t t4(i4) on t4.i4 > t2ss.x; + QUERY PLAN +---------------------------------------------------------- + Nested Loop Left Join + Output: t1.i, (1), t2.i2, t3.i3, t4.i4 + -> Nested Loop Left Join + Output: t1.i, t2.i2, (1), t3.i3 + Join Filter: false + -> Hash Left Join + Output: t1.i, t2.i2, (1) + Inner Unique: true + Hash Cond: (t1.i = t2.i2) + -> Seq Scan on pg_temp.t t1 + Output: t1.i + -> Hash + Output: t2.i2, (1) + -> Seq Scan on pg_temp.t t2 + Output: t2.i2, 1 + -> Result + Output: t3.i3 + Replaces: Scan on t3 + One-Time Filter: false + -> Memoize + Output: t4.i4 + Cache Key: (1) + Cache Mode: binary + -> Index Only Scan using t_pkey on pg_temp.t t4 + Output: t4.i4 + Index Cond: (t4.i4 > (1)) +(26 rows) + +explain (verbose, costs off) +select * from + (select k from + (select i, coalesce(i, j) as k from + (select i from t union all select 0) + join (select 1 as j limit 1) on i = j) + right join (select 2 as x) on true + join (select 3 as y) on i is not null + ), + lateral (select k as kl limit 1); + QUERY PLAN +------------------------------------------------------------------- + Nested Loop + Output: COALESCE(t.i, (1)), ((COALESCE(t.i, (1)))) + -> Limit + Output: 1 + -> Result + Output: 1 + -> Nested Loop + Output: t.i, ((COALESCE(t.i, (1)))) + -> Result + Output: t.i, COALESCE(t.i, (1)) + -> Append + -> Index Only Scan using t_pkey on pg_temp.t + Output: t.i + Index Cond: (t.i = (1)) + -> Result + Output: 0 + One-Time Filter: ((1) = 0) + -> Limit + Output: ((COALESCE(t.i, (1)))) + -> Result + Output: (COALESCE(t.i, (1))) +(21 rows) + +rollback; +-- PHVs containing SubLinks are quite tricky to get right +explain (verbose, costs off) +select * +from int8_tbl i8 + inner join + (select (select true) as x + from int4_tbl i4, lateral (select i4.f1 as y limit 1) ss1 + where i4.f1 = 0) ss2 on true + right join (select false as z) ss3 on true, + lateral (select i8.q2 as q2l where x limit 1) ss4 +where i8.q2 = 123; + QUERY PLAN +--------------------------------------------------------------------- + Nested Loop + Output: i8.q1, i8.q2, (InitPlan expr_1).col1, false, (i8.q2) + InitPlan expr_1 + -> Result + Output: true + InitPlan expr_2 + -> Result + Output: true + -> Seq Scan on public.int4_tbl i4 + Output: i4.f1 + Filter: (i4.f1 = 0) + -> Nested Loop + Output: i8.q1, i8.q2, (i8.q2) + -> Subquery Scan on ss1 + Output: ss1.y, (InitPlan expr_1).col1 + -> Limit + Output: NULL::integer + -> Result + Output: NULL::integer + -> Nested Loop + Output: i8.q1, i8.q2, (i8.q2) + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + Filter: (i8.q2 = 123) + -> Limit + Output: (i8.q2) + -> Result + Output: i8.q2 + One-Time Filter: ((InitPlan expr_1).col1) +(29 rows) + +explain (verbose, costs off) +select * +from int8_tbl i8 + inner join + (select (select true) as x + from int4_tbl i4, lateral (select 1 as y limit 1) ss1 + where i4.f1 = 0) ss2 on true + right join (select false as z) ss3 on true, + lateral (select i8.q2 as q2l where x limit 1) ss4 +where i8.q2 = 123; + QUERY PLAN +--------------------------------------------------------------------- + Nested Loop + Output: i8.q1, i8.q2, (InitPlan expr_1).col1, false, (i8.q2) + InitPlan expr_1 + -> Result + Output: true + InitPlan expr_2 + -> Result + Output: true + -> Limit + Output: NULL::integer + -> Result + Output: NULL::integer + -> Nested Loop + Output: i8.q1, i8.q2, (i8.q2) + -> Seq Scan on public.int4_tbl i4 + Output: i4.f1, (InitPlan expr_1).col1 + Filter: (i4.f1 = 0) + -> Nested Loop + Output: i8.q1, i8.q2, (i8.q2) + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + Filter: (i8.q2 = 123) + -> Limit + Output: (i8.q2) + -> Result + Output: i8.q2 + One-Time Filter: ((InitPlan expr_1).col1) +(27 rows) + -- Test proper handling of appendrel PHVs during useless-RTE removal explain (costs off) select * from @@ -4102,8 +4485,9 @@ from int4_tbl t1 ------------------------------------------------------------------------------------------------------------------- Result Output: (current_database())::information_schema.sql_identifier, (c.relname)::information_schema.sql_identifier + Replaces: Join on t1, t2, a, c, nc, t, nt, bt, nbt One-Time Filter: false -(3 rows) +(4 rows) -- Test handling of qual pushdown to appendrel members with non-Var outputs explain (verbose, costs off) @@ -4127,6 +4511,57 @@ where ss.x is null; Output: 'bar'::text (12 rows) +-- Test computation of varnullingrels when translating appendrel Var +begin; +create temp table t_append (a int not null, b int); +insert into t_append values (1, 1); +insert into t_append values (2, 3); +explain (verbose, costs off) +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + QUERY PLAN +--------------------------------------------------- + Nested Loop + Output: t1.a, (t1.a) + -> Merge Left Join + Output: t1.a, t2.a + Merge Cond: (t1.a = t2.b) + -> Sort + Output: t1.a + Sort Key: t1.a + -> Seq Scan on pg_temp.t_append t1 + Output: t1.a + -> Sort + Output: t2.a, t2.b + Sort Key: t2.b + -> Seq Scan on pg_temp.t_append t2 + Output: t2.a, t2.b + -> Append + -> Result + Output: t1.a + -> Result + Output: t2.a + One-Time Filter: (t2.a IS NOT NULL) +(21 rows) + +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + a | a +---+--- + 1 | 1 + 1 | 1 + 2 | 2 +(3 rows) + +rollback; -- -- test inlining of immutable functions -- @@ -4175,11 +4610,12 @@ select unique1 from tenk1, lateral f_immutable_int4(1) x where x = unique1; explain (costs off) select unique1 from tenk1, lateral f_immutable_int4(1) x where x in (select 17); - QUERY PLAN --------------------------- + QUERY PLAN +--------------------------- Result + Replaces: Scan on tenk1 One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select unique1, x from tenk1 join f_immutable_int4(1) x on unique1 = x; @@ -4225,11 +4661,12 @@ select unique1, x from tenk1 full join f_immutable_int4(1) x on unique1 = x; -- check that pullup of a const function allows further const-folding explain (costs off) select unique1 from tenk1, f_immutable_int4(1) x where x = 42; - QUERY PLAN --------------------------- + QUERY PLAN +--------------------------- Result + Replaces: Scan on tenk1 One-Time Filter: false -(2 rows) +(3 rows) -- test inlining of immutable functions with PlaceHolderVars explain (costs off) @@ -4590,7 +5027,7 @@ order by fault; explain (costs off) select * from (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys) -left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x +left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x left join unnest(v1ys) as u1(u1y) on u1y = v2y; QUERY PLAN ------------------------------------------------------------- @@ -4606,13 +5043,14 @@ left join unnest(v1ys) as u1(u1y) on u1y = v2y; select * from (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys) -left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x +left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x left join unnest(v1ys) as u1(u1y) on u1y = v2y; v1x | v1ys | v2x | v2y | u1y -----+---------+-----+-----+----- 1 | {10,20} | 1 | 10 | 10 2 | {20,30} | 2 | 20 | 20 -(2 rows) + 2 | {20,30} | 2 | | +(3 rows) -- -- test handling of potential equivalence clauses above outer joins @@ -5085,8 +5523,9 @@ left join Join Filter: false -> Result -> Result + Replaces: Join on c, n One-Time Filter: false -(5 rows) +(6 rows) -- check handling of apparently-commutable outer joins with non-commutable -- joins between them @@ -5280,12 +5719,13 @@ select 1 from right join (select 1 as z) as ss2 on true) on false, lateral (select i4.f1, ss1.n from int8_tbl as i8 limit 1) as ss3; - QUERY PLAN --------------------------- + QUERY PLAN +----------------------------------------------- Result Output: 1 + Replaces: Join on i4, ss3, x1, x2, *RESULT* One-Time Filter: false -(3 rows) +(4 rows) select 1 from int4_tbl as i4 @@ -5314,11 +5754,12 @@ select 1 from t t1 on false where t3.a = coalesce(t5.a,1)) as s2 on true; - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------------------- Result + Replaces: Join on t1, s1, t2, t3, t4, t5 One-Time Filter: false -(2 rows) +(3 rows) rollback; -- @@ -5384,14 +5825,14 @@ select * from (select 1 as id) as xx left join (tenk1 as a1 full join (select 1 as id) as yy on (a1.unique1 = yy.id)) - on (xx.id = coalesce(yy.id)); - QUERY PLAN ---------------------------------------- + on (xx.id = coalesce(yy.id, yy.id)); + QUERY PLAN +------------------------------------------ Nested Loop Left Join -> Result -> Hash Full Join Hash Cond: (a1.unique1 = (1)) - Filter: (1 = COALESCE((1))) + Filter: (1 = COALESCE((1), (1))) -> Seq Scan on tenk1 a1 -> Hash -> Result @@ -5401,7 +5842,7 @@ select * from (select 1 as id) as xx left join (tenk1 as a1 full join (select 1 as id) as yy on (a1.unique1 = yy.id)) - on (xx.id = coalesce(yy.id)); + on (xx.id = coalesce(yy.id, yy.id)); id | unique1 | unique2 | two | four | ten | twenty | hundred | thousand | twothousand | fivethous | tenthous | odd | even | stringu1 | stringu2 | string4 | id ----+---------+---------+-----+------+-----+--------+---------+----------+-------------+-----------+----------+-----+------+----------+----------+---------+---- 1 | 1 | 2838 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 2 | 3 | BAAAAA | EFEAAA | OOOOxx | 1 @@ -5487,13 +5928,13 @@ explain (costs off) select a.unique1, b.unique2 from onek a left join onek b on a.unique1 = b.unique2 where (b.unique2, random() > 0) = any (select q1, random() > 0 from int8_tbl c where c.q1 < b.unique1); - QUERY PLAN ------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------- Hash Join Hash Cond: (b.unique2 = a.unique1) -> Seq Scan on onek b - Filter: (ANY ((unique2 = (SubPlan 1).col1) AND ((random() > '0'::double precision) = (SubPlan 1).col2))) - SubPlan 1 + Filter: (ANY ((unique2 = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan any_1).col2))) + SubPlan any_1 -> Seq Scan on int8_tbl c Filter: (q1 < b.unique1) -> Hash @@ -5715,14 +6156,15 @@ from int4_tbl as t1 inner join int8_tbl as t7 on null) on t5.q1 = t7.q2) on false; - QUERY PLAN --------------------------------- + QUERY PLAN +-------------------------------------------------- Nested Loop Left Join Join Filter: false -> Seq Scan on int4_tbl t1 -> Result + Replaces: Join on t2, t3, t4, t5, t7, t6 One-Time Filter: false -(5 rows) +(6 rows) -- variant with Var rather than PHV coming from t6 explain (costs off) @@ -5737,14 +6179,15 @@ from int4_tbl as t1 inner join int8_tbl as t7 on null) on t5.q1 = t7.q2) on false; - QUERY PLAN --------------------------------- + QUERY PLAN +-------------------------------------------------- Nested Loop Left Join Join Filter: false -> Seq Scan on int4_tbl t1 -> Result + Replaces: Join on t2, t3, t4, t5, t7, t6 One-Time Filter: false -(5 rows) +(6 rows) -- per further discussion of bug #17781 explain (costs off) @@ -5787,6 +6230,45 @@ from int8_tbl t1 -> Seq Scan on onek t4 (13 rows) +-- bug #19460: we need to clean up RestrictInfos more than we had been doing +explain (costs off) +select * from + (select 1::int as id) as lhs +full join + (select dummy_source.id + from (select null::int as id) as dummy_source + left join (select a.id from a where a.id = 42) as sub + on sub.id = dummy_source.id + ) as rhs +on lhs.id = rhs.id; + QUERY PLAN +-------------------------------------- + Hash Full Join + Hash Cond: ((1) = (NULL::integer)) + -> Result + -> Hash + -> Result +(5 rows) + +explain (costs off) +select * from + (select 1::int as id) as lhs +full join + (select dummy_source.id + from (select 2::int as id) as dummy_source + left join (select a.id from a) as sub + on sub.id = dummy_source.id + ) as rhs +on lhs.id = rhs.id; + QUERY PLAN +-------------------------- + Hash Full Join + Hash Cond: ((1) = (2)) + -> Result + -> Hash + -> Result +(5 rows) + -- More tests of correct placement of pseudoconstant quals -- simple constant-false condition explain (costs off) @@ -5794,15 +6276,16 @@ select * from int8_tbl t1 left join (int8_tbl t2 inner join int8_tbl t3 on false left join int8_tbl t4 on t2.q2 = t4.q2) on t1.q1 = t2.q1; - QUERY PLAN --------------------------------------- + QUERY PLAN +-------------------------------------------- Hash Left Join - Hash Cond: (t1.q1 = q1) + Hash Cond: (t1.q1 = t2.q1) -> Seq Scan on int8_tbl t1 -> Hash -> Result + Replaces: Join on t2, t3, t4 One-Time Filter: false -(6 rows) +(7 rows) -- deduce constant-false from an EquivalenceClass explain (costs off) @@ -5810,15 +6293,16 @@ select * from int8_tbl t1 left join (int8_tbl t2 inner join int8_tbl t3 on (t2.q1-t3.q2) = 0 and (t2.q1-t3.q2) = 1 left join int8_tbl t4 on t2.q2 = t4.q2) on t1.q1 = t2.q1; - QUERY PLAN --------------------------------------- + QUERY PLAN +-------------------------------------------- Hash Left Join - Hash Cond: (t1.q1 = q1) + Hash Cond: (t1.q1 = t2.q1) -> Seq Scan on int8_tbl t1 -> Hash -> Result + Replaces: Join on t2, t3, t4 One-Time Filter: false -(6 rows) +(7 rows) -- pseudoconstant based on an outer-level Param explain (costs off) @@ -5831,7 +6315,7 @@ select exists( QUERY PLAN --------------------------------------------------------------------- Seq Scan on int4_tbl x0 - SubPlan 1 + SubPlan exists_1 -> Nested Loop Left Join Join Filter: (t2.q2 = t4.q2) -> Nested Loop Left Join @@ -5858,6 +6342,40 @@ select d.* from d left join (select * from b group by b.id, b.c_id) s Seq Scan on d (1 row) +-- check that join removal works for a left join when joining a subquery +-- that is guaranteed to be unique by GROUPING SETS +explain (costs off) +select d.* from d left join (select 1 as x from b group by ()) s + on d.a = s.x; + QUERY PLAN +--------------- + Seq Scan on d +(1 row) + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets(())) s + on d.a = s.x; + QUERY PLAN +--------------- + Seq Scan on d +(1 row) + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets(()), grouping sets(())) s + on d.a = s.x; + QUERY PLAN +--------------- + Seq Scan on d +(1 row) + +explain (costs off) +select d.* from d left join (select 1 as x from b group by distinct grouping sets((), ())) s + on d.a = s.x; + QUERY PLAN +--------------- + Seq Scan on d +(1 row) + -- similarly, but keying off a DISTINCT clause explain (costs off) select d.* from d left join (select distinct * from b) s @@ -5886,6 +6404,55 @@ select d.* from d left join (select * from b group by b.id, b.c_id) s -> Seq Scan on d (8 rows) +-- join removal is not possible when the GROUP BY contains non-empty grouping +-- sets or multiple empty grouping sets +explain (costs off) +select d.* from d left join (select 1 as x from b group by rollup(x)) s + on d.a = s.x; + QUERY PLAN +--------------------------------- + Hash Left Join + Hash Cond: (d.a = (1)) + -> Seq Scan on d + -> Hash + -> MixedAggregate + Hash Key: 1 + Group Key: () + -> Seq Scan on b +(8 rows) + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets((), ())) s + on d.a = s.x; + QUERY PLAN +----------------------------------------- + Hash Left Join + Hash Cond: (d.a = (1)) + -> Seq Scan on d + -> Hash + -> Append + -> Result + Replaces: Aggregate + -> Result + Replaces: Aggregate +(9 rows) + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets((), grouping sets(()))) s + on d.a = s.x; + QUERY PLAN +----------------------------------------- + Hash Left Join + Hash Cond: (d.a = (1)) + -> Seq Scan on d + -> Hash + -> Append + -> Result + Replaces: Aggregate + -> Result + Replaces: Aggregate +(9 rows) + -- similarly, but keying off a DISTINCT clause explain (costs off) select d.* from d left join (select distinct * from b) s @@ -6057,8 +6624,9 @@ select p.* from QUERY PLAN -------------------------- Result + Replaces: Scan on p One-Time Filter: false -(2 rows) +(3 rows) select p.* from (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k @@ -6074,8 +6642,9 @@ select p.* from QUERY PLAN -------------------------- Result + Replaces: Join on p, x One-Time Filter: false -(2 rows) +(3 rows) -- bug 5255: this is not optimizable by join removal begin; @@ -6140,15 +6709,16 @@ SELECT q2 FROM QUERY PLAN ------------------------------------------------------ Nested Loop Left Join - Output: q2 + Output: int8_tbl.q2 Join Filter: NULL::boolean Filter: (('constant'::text) >= ('constant'::text)) -> Seq Scan on public.int4_tbl Output: int4_tbl.f1 -> Result - Output: q2, 'constant'::text + Output: int8_tbl.q2, 'constant'::text + Replaces: Scan on int8_tbl One-Time Filter: false -(9 rows) +(10 rows) -- join removal bug #17786: check that OR conditions are cleaned up EXPLAIN (COSTS OFF) @@ -6167,8 +6737,9 @@ FROM int4_tbl Filter: ((tenk1.unique1 = (42)) OR (tenk1.unique2 = (42))) -> Seq Scan on tenk1 -> Result + Replaces: Scan on int8_tbl One-Time Filter: false -(9 rows) +(10 rows) rollback; -- another join removal bug: we must clean up correctly when removing a PHV @@ -6258,6 +6829,128 @@ where t1.a = s.c; ---------- (0 rows) +rollback; +-- check handling of semijoins after join removal: we must suppress +-- unique-ification of known-constant values +begin; +create temp table t (a int unique, b int); +insert into t values (1, 2); +explain (verbose, costs off) +select t1.a from t t1 + left join t t2 on t1.a = t2.a + join t t3 on true +where exists (select 1 from t t4 + join t t5 on t4.b = t5.b + join t t6 on t5.b = t6.b + where t1.a = t4.a and t3.a = t5.a and t4.a = 1); + QUERY PLAN +------------------------------------------------------------------------------------ + Nested Loop + Output: t1.a + Inner Unique: true + -> Nested Loop + Output: t1.a, t5.a + -> Index Only Scan using t_a_key on pg_temp.t t1 + Output: t1.a + Index Cond: (t1.a = 1) + -> HashAggregate + Output: t5.a + Group Key: t5.a + -> Hash Join + Output: t5.a + Hash Cond: (t6.b = t4.b) + -> Seq Scan on pg_temp.t t6 + Output: t6.a, t6.b + -> Hash + Output: t4.b, t5.b, t5.a + -> Hash Join + Output: t4.b, t5.b, t5.a + Inner Unique: true + Hash Cond: (t5.b = t4.b) + -> Seq Scan on pg_temp.t t5 + Output: t5.a, t5.b + -> Hash + Output: t4.b, t4.a + -> Index Scan using t_a_key on pg_temp.t t4 + Output: t4.b, t4.a + Index Cond: (t4.a = 1) + -> Index Only Scan using t_a_key on pg_temp.t t3 + Output: t3.a + Index Cond: (t3.a = t5.a) +(32 rows) + +select t1.a from t t1 + left join t t2 on t1.a = t2.a + join t t3 on true +where exists (select 1 from t t4 + join t t5 on t4.b = t5.b + join t t6 on t5.b = t6.b + where t1.a = t4.a and t3.a = t5.a and t4.a = 1); + a +--- + 1 +(1 row) + +rollback; +-- check handling of semijoins if all RHS columns are equated to constants: we +-- should suppress unique-ification in this case. +begin; +create temp table t (a int, b int); +insert into t values (1, 2); +explain (costs off) +select * from t t1, t t2 where exists + (select 1 from t t3 where t1.a = t3.a and t2.b = t3.b and t3.a = 1 and t3.b = 2); + QUERY PLAN +--------------------------------------------- + Nested Loop Semi Join + -> Nested Loop + -> Seq Scan on t t1 + Filter: (a = 1) + -> Materialize + -> Seq Scan on t t2 + Filter: (b = 2) + -> Materialize + -> Seq Scan on t t3 + Filter: ((a = 1) AND (b = 2)) +(10 rows) + +select * from t t1, t t2 where exists + (select 1 from t t3 where t1.a = t3.a and t2.b = t3.b and t3.a = 1 and t3.b = 2); + a | b | a | b +---+---+---+--- + 1 | 2 | 1 | 2 +(1 row) + +rollback; +-- check handling of semijoin unique-ification for child relations if all RHS +-- columns are equated to constants. +begin; +create temp table p (a int, b int) partition by range (a); +create temp table p1 partition of p for values from (0) to (10); +create temp table p2 partition of p for values from (10) to (20); +insert into p values (1, 2); +insert into p values (10, 20); +set enable_partitionwise_join to on; +explain (costs off) +select * from p t1 where exists + (select 1 from p t2 where t1.a = t2.a and t1.a = 1); + QUERY PLAN +------------------------------- + Nested Loop Semi Join + -> Seq Scan on p1 t1 + Filter: (a = 1) + -> Materialize + -> Seq Scan on p1 t2 + Filter: (a = 1) +(6 rows) + +select * from p t1 where exists + (select 1 from p t2 where t1.a = t2.a and t1.a = 1); + a | b +---+--- + 1 | 2 +(1 row) + rollback; -- test cases where we can remove a join, but not a PHV computed at it begin; @@ -6365,10 +7058,10 @@ where ss.a = ss.phv and f1 = 0; QUERY PLAN ------------------------------------ Nested Loop - -> Seq Scan on int4_tbl - Filter: (f1 = 0) -> Seq Scan on parttbl1 parttbl Filter: (a = 12) + -> Seq Scan on int4_tbl + Filter: (f1 = 0) (5 rows) select * from @@ -6496,8 +7189,9 @@ where q1.x = q2.y; QUERY PLAN -------------------------- Result + Replaces: Scan on sj One-Time Filter: false -(2 rows) +(3 rows) -- We can't use a cross-EC generated self join qual because of current logic of -- the generate_join_implied_equalities routine. @@ -6555,7 +7249,7 @@ where t1.a = t2.a; ------------------------------------------ Seq Scan on sj t2 Filter: (a IS NOT NULL) - SubPlan 1 + SubPlan expr_1 -> Result One-Time Filter: (t2.a = t2.a) -> Seq Scan on sj @@ -7321,11 +8015,12 @@ select 1 from emp1 full join on true where false) s on true where false; - QUERY PLAN --------------------------- + QUERY PLAN +---------------------------------- Result + Replaces: Join on emp1, t1, t3 One-Time Filter: false -(2 rows) +(3 rows) select 1 from emp1 full join (select * from emp1 t1 join @@ -7436,6 +8131,22 @@ WHERE q0.a = 1; -> Seq Scan on sj n1 (7 rows) +-- Do not forget to replace relid in bare Var join clause (bug #19435) +ALTER TABLE sl ADD COLUMN bool_col boolean; +EXPLAIN (COSTS OFF) +SELECT 1 AS c1 FROM sl sl1 LEFT JOIN (sl AS sl2 NATURAL JOIN sl AS sl3) + ON sl2.bool_col LEFT JOIN sl AS sl4 ON sl2.bool_col; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------- + Nested Loop Left Join + -> Seq Scan on sl sl1 + -> Nested Loop Left Join + Join Filter: sl3.bool_col + -> Seq Scan on sl sl3 + Filter: (bool_col AND (a IS NOT NULL) AND (b IS NOT NULL) AND (c IS NOT NULL) AND (bool_col IS NOT NULL)) + -> Seq Scan on sl sl4 +(7 rows) + -- Check optimization disabling if it will violate special join conditions. -- Two identical joined relations satisfies self join removal conditions but -- stay in different special join infos. @@ -7554,8 +8265,9 @@ where false; -------------------------- Result Output: 1 + Replaces: Scan on ss One-Time Filter: false -(3 rows) +(4 rows) -- -- Test LATERAL @@ -8169,20 +8881,20 @@ select * from int4_tbl i left join explain (verbose, costs off) select * from int4_tbl i left join - lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; - QUERY PLAN -------------------------------------- + lateral (select coalesce(i, i) from int2_tbl j where i.f1 = j.f1) k on true; + QUERY PLAN +------------------------------------------ Nested Loop Left Join - Output: i.f1, (COALESCE(i.*)) + Output: i.f1, (COALESCE(i.*, i.*)) -> Seq Scan on public.int4_tbl i Output: i.f1, i.* -> Seq Scan on public.int2_tbl j - Output: j.f1, COALESCE(i.*) + Output: j.f1, COALESCE(i.*, i.*) Filter: (i.f1 = j.f1) (7 rows) select * from int4_tbl i left join - lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; + lateral (select coalesce(i, i) from int2_tbl j where i.f1 = j.f1) k on true; f1 | coalesce -------------+---------- 0 | (0) @@ -8484,31 +9196,33 @@ select * from int4_tbl t1, explain (verbose, costs off) select * from int8_tbl i8 left join lateral (select *, i8.q2 from int4_tbl where false) ss on true; - QUERY PLAN --------------------------------------- + QUERY PLAN +---------------------------------------------- Nested Loop Left Join - Output: i8.q1, i8.q2, f1, (i8.q2) + Output: i8.q1, i8.q2, int4_tbl.f1, (i8.q2) Join Filter: false -> Seq Scan on public.int8_tbl i8 Output: i8.q1, i8.q2 -> Result - Output: f1, i8.q2 + Output: int4_tbl.f1, i8.q2 + Replaces: Scan on int4_tbl One-Time Filter: false -(8 rows) +(9 rows) explain (verbose, costs off) select * from int8_tbl i8 left join lateral (select *, i8.q2 from int4_tbl i1, int4_tbl i2 where false) ss on true; - QUERY PLAN ------------------------------------------ + QUERY PLAN +----------------------------------------------- Nested Loop Left Join - Output: i8.q1, i8.q2, f1, f1, (i8.q2) + Output: i8.q1, i8.q2, i1.f1, i2.f1, (i8.q2) -> Seq Scan on public.int8_tbl i8 Output: i8.q1, i8.q2 -> Result - Output: f1, f1, i8.q2 + Output: i1.f1, i2.f1, i8.q2 + Replaces: Join on i1, i2 One-Time Filter: false -(7 rows) +(8 rows) -- check handling of nested appendrels inside LATERAL select * from @@ -8578,8 +9292,8 @@ lateral (select * from int8_tbl t1, where q2 = (select greatest(t1.q1,t2.q2)) and (select v.id=0)) offset 0) ss2) ss where t1.q1 = ss.q2) ss0; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------ Nested Loop Output: "*VALUES*".column1, t1.q1, t1.q2, ss2.q1, ss2.q2 -> Seq Scan on public.int8_tbl t1 @@ -8593,20 +9307,20 @@ lateral (select * from int8_tbl t1, Filter: (t1.q1 = ss2.q2) -> Seq Scan on public.int8_tbl t2 Output: t2.q1, t2.q2 - Filter: (ANY ((t2.q1 = (SubPlan 3).col1) AND ((random() > '0'::double precision) = (SubPlan 3).col2))) - SubPlan 3 + Filter: (ANY ((t2.q1 = (SubPlan any_1).col1) AND ((random() > '0'::double precision) = (SubPlan any_1).col2))) + SubPlan any_1 -> Result Output: t3.q2, (random() > '0'::double precision) - One-Time Filter: (InitPlan 2).col1 - InitPlan 1 + One-Time Filter: (InitPlan expr_2).col1 + InitPlan expr_1 -> Result Output: GREATEST(t1.q1, t2.q2) - InitPlan 2 + InitPlan expr_2 -> Result Output: ("*VALUES*".column1 = 0) -> Seq Scan on public.int8_tbl t3 Output: t3.q1, t3.q2 - Filter: (t3.q2 = (InitPlan 1).col1) + Filter: (t3.q2 = (InitPlan expr_1).col1) (27 rows) select * from (values (0), (1)) v(id), @@ -9113,7 +9827,7 @@ left join j2 on j1.id1 = j2.id1 where j1.id2 = 1; Output: j2.id1, j2.id2 (8 rows) -create unique index j1_id2_idx on j1(id2) where id2 is not null; +create unique index j1_id2_idx on j1(id2) where id2 > 0; -- ensure we don't use a partial unique index as unique proofs explain (verbose, costs off) select * from j1 @@ -9136,12 +9850,14 @@ drop index j1_id2_idx; set enable_nestloop to 0; set enable_hashjoin to 0; set enable_sort to 0; +-- we need additional data to get the partial indexes to be preferred +insert into j1 select 2, i from generate_series(1, 100) i; +insert into j2 select 1, i from generate_series(2, 100) i; +analyze j1; +analyze j2; -- create indexes that will be preferred over the PKs to perform the join create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1; create index j2_id1_idx on j2 (id1) where id1 % 1000 = 1; --- need an additional row in j2, if we want j2_id1_idx to be preferred -insert into j2 values(1,2); -analyze j2; explain (costs off) select * from j1 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1; @@ -9226,23 +9942,20 @@ where exists (select 1 from tenk1 t3 --------------------------------------------------------------------------------- Nested Loop Output: t1.unique1, t2.hundred - -> Hash Join + -> Merge Join Output: t1.unique1, t3.tenthous - Hash Cond: (t3.thousand = t1.unique1) - -> HashAggregate + Merge Cond: (t3.thousand = t1.unique1) + -> Unique Output: t3.thousand, t3.tenthous - Group Key: t3.thousand, t3.tenthous -> Index Only Scan using tenk1_thous_tenthous on public.tenk1 t3 Output: t3.thousand, t3.tenthous - -> Hash + -> Index Only Scan using onek_unique1 on public.onek t1 Output: t1.unique1 - -> Index Only Scan using onek_unique1 on public.onek t1 - Output: t1.unique1 - Index Cond: (t1.unique1 < 1) + Index Cond: (t1.unique1 < 1) -> Index Only Scan using tenk1_hundred on public.tenk1 t2 Output: t2.hundred Index Cond: (t2.hundred = t3.tenthous) -(18 rows) +(15 rows) -- ... unless it actually is unique create table j3 as select unique1, tenthous from onek; @@ -9321,13 +10034,13 @@ SELECT * FROM rescan_bhs t1 LEFT JOIN rescan_bhs t2 ON t1.a IN QUERY PLAN ----------------------------------------------------------- Nested Loop Left Join - Join Filter: (ANY (t1.a = (SubPlan 1).col1)) + Join Filter: (ANY (t1.a = (SubPlan any_1).col1)) -> Bitmap Heap Scan on rescan_bhs t1 -> Bitmap Index Scan on rescan_bhs_a_idx -> Materialize -> Bitmap Heap Scan on rescan_bhs t2 -> Bitmap Index Scan on rescan_bhs_a_idx - SubPlan 1 + SubPlan any_1 -> Result One-Time Filter: (t2.a > 1) -> Bitmap Heap Scan on rescan_bhs t3 @@ -9351,20 +10064,43 @@ CREATE STATISTICS group_tbl_stat (ndistinct) ON a, b FROM group_tbl; ANALYZE group_tbl; EXPLAIN (COSTS OFF) SELECT 1 FROM group_tbl t1 - LEFT JOIN (SELECT a c1, COALESCE(a) c2 FROM group_tbl t2) s ON TRUE + LEFT JOIN (SELECT a c1, COALESCE(a, a) c2 FROM group_tbl t2) s ON TRUE GROUP BY s.c1, s.c2; - QUERY PLAN --------------------------------------------- + QUERY PLAN +------------------------------------------------ Group - Group Key: t2.a, (COALESCE(t2.a)) + Group Key: t2.a, (COALESCE(t2.a, t2.a)) -> Sort - Sort Key: t2.a, (COALESCE(t2.a)) + Sort Key: t2.a, (COALESCE(t2.a, t2.a)) -> Nested Loop Left Join -> Seq Scan on group_tbl t1 -> Seq Scan on group_tbl t2 (7 rows) DROP TABLE group_tbl; +-- Test that we ignore PlaceHolderVars when looking up statistics +EXPLAIN (COSTS OFF) +SELECT t1.unique1 FROM tenk1 t1 LEFT JOIN + (SELECT *, 42 AS phv FROM tenk1 t2) ss ON t1.unique2 = ss.unique2 +WHERE ss.unique1 = ss.phv AND t1.unique1 < 100; + QUERY PLAN +-------------------------------------------------- + Nested Loop + -> Seq Scan on tenk1 t2 + Filter: (unique1 = 42) + -> Index Scan using tenk1_unique2 on tenk1 t1 + Index Cond: (unique2 = t2.unique2) + Filter: (unique1 < 100) +(6 rows) + +SELECT t1.unique1 FROM tenk1 t1 LEFT JOIN + (SELECT *, 42 AS phv FROM tenk1 t2) ss ON t1.unique2 = ss.unique2 +WHERE ss.unique1 = ss.phv AND t1.unique1 < 100; + unique1 +--------- + 42 +(1 row) + -- -- Test for a nested loop join involving index scan, transforming OR-clauses -- to SAOP. diff --git a/src/test/regress/expected/join_hash.out b/src/test/regress/expected/join_hash.out index 4fc34a0e72aba..75009e297208b 100644 --- a/src/test/regress/expected/join_hash.out +++ b/src/test/regress/expected/join_hash.out @@ -53,6 +53,7 @@ $$; -- estimated size. create table simple as select generate_series(1, 20000) AS id, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +insert into simple values (null, null); alter table simple set (parallel_workers = 2); analyze simple; -- Make a relation whose size we will under-estimate. We want stats @@ -76,8 +77,8 @@ insert into extremely_skewed update pg_class set reltuples = 2, relpages = pg_relation_size('extremely_skewed') / 8192 where relname = 'extremely_skewed'; --- Make a relation with a couple of enormous tuples. -create table wide as select generate_series(1, 2) as id, rpad('', 320000, 'x') as t; +-- Make a relation with several enormous tuples. +create table wide as select generate_series(1, 3) as id, rpad('', 320000, 'x') as t; alter table wide set (parallel_workers = 2); -- The "optimal" case: the hash table fits in memory; we plan for 1 -- batch, we stick to that number, and peak memory usage stays within @@ -308,7 +309,7 @@ $$); select count(*) from simple r full outer join simple s using (id); count ------- - 20000 + 20002 (1 row) rollback to settings; @@ -559,6 +560,7 @@ create table join_foo as select generate_series(1, 3) as id, 'xxxxx'::text as t; alter table join_foo set (parallel_workers = 0); create table join_bar as select generate_series(1, 10000) as id, 'xxxxx'::text as t; alter table join_bar set (parallel_workers = 2); +analyze join_foo, join_bar; -- multi-batch with rescan, parallel-oblivious savepoint settings; set enable_parallel_hash = off; @@ -786,7 +788,7 @@ explain (costs off) select count(*) from simple r full outer join simple s using (id); count ------- - 20000 + 20002 (1 row) rollback to settings; @@ -809,7 +811,7 @@ explain (costs off) select count(*) from simple r full outer join simple s using (id); count ------- - 20000 + 20002 (1 row) rollback to settings; @@ -834,7 +836,7 @@ explain (costs off) select count(*) from simple r full outer join simple s using (id); count ------- - 20000 + 20002 (1 row) rollback to settings; @@ -857,7 +859,7 @@ explain (costs off) select count(*) from simple r full outer join simple s on (r.id = 0 - s.id); count ------- - 40000 + 40002 (1 row) rollback to settings; @@ -880,7 +882,7 @@ explain (costs off) select count(*) from simple r full outer join simple s on (r.id = 0 - s.id); count ------- - 40000 + 40002 (1 row) rollback to settings; @@ -905,7 +907,7 @@ explain (costs off) select count(*) from simple r full outer join simple s on (r.id = 0 - s.id); count ------- - 40000 + 40002 (1 row) rollback to settings; @@ -921,7 +923,7 @@ set work_mem = '128kB'; set hash_mem_multiplier = 1.0; explain (costs off) select length(max(s.t)) - from wide left join (select id, coalesce(t, '') || '' as t from wide) s using (id); + from wide left join (select id, coalesce(t, '') || '' as t from wide where id < 3) s using (id); QUERY PLAN ---------------------------------------------------------------- Finalize Aggregate @@ -933,10 +935,11 @@ explain (costs off) -> Parallel Seq Scan on wide -> Parallel Hash -> Parallel Seq Scan on wide wide_1 -(9 rows) + Filter: (id < 3) +(10 rows) select length(max(s.t)) -from wide left join (select id, coalesce(t, '') || '' as t from wide) s using (id); +from wide left join (select id, coalesce(t, '') || '' as t from wide where id < 3) s using (id); length -------- 320000 @@ -946,7 +949,7 @@ select final > 1 as multibatch from hash_join_batches( $$ select length(max(s.t)) - from wide left join (select id, coalesce(t, '') || '' as t from wide) s using (id); + from wide left join (select id, coalesce(t, '') || '' as t from wide where id < 3) s using (id); $$); multibatch ------------ @@ -1031,30 +1034,30 @@ WHERE ------------------------------------------------------------------------------------------------ Hash Join Output: hjtest_1.a, hjtest_2.a, (hjtest_1.tableoid)::regclass, (hjtest_2.tableoid)::regclass - Hash Cond: ((hjtest_1.id = (SubPlan 1)) AND ((SubPlan 2) = (SubPlan 3))) + Hash Cond: ((hjtest_1.id = (SubPlan expr_1)) AND ((SubPlan expr_2) = (SubPlan expr_3))) Join Filter: (hjtest_1.a <> hjtest_2.b) -> Seq Scan on public.hjtest_1 Output: hjtest_1.a, hjtest_1.tableoid, hjtest_1.id, hjtest_1.b - Filter: ((SubPlan 4) < 50) - SubPlan 4 + Filter: ((SubPlan expr_4) < 50) + SubPlan expr_4 -> Result Output: (hjtest_1.b * 5) -> Hash Output: hjtest_2.a, hjtest_2.tableoid, hjtest_2.id, hjtest_2.c, hjtest_2.b -> Seq Scan on public.hjtest_2 Output: hjtest_2.a, hjtest_2.tableoid, hjtest_2.id, hjtest_2.c, hjtest_2.b - Filter: ((SubPlan 5) < 55) - SubPlan 5 + Filter: ((SubPlan expr_5) < 55) + SubPlan expr_5 -> Result Output: (hjtest_2.c * 5) - SubPlan 1 + SubPlan expr_1 -> Result Output: 1 One-Time Filter: (hjtest_2.id = 1) - SubPlan 3 + SubPlan expr_3 -> Result Output: (hjtest_2.c * 5) - SubPlan 2 + SubPlan expr_2 -> Result Output: (hjtest_1.b * 5) (28 rows) @@ -1085,30 +1088,30 @@ WHERE ------------------------------------------------------------------------------------------------ Hash Join Output: hjtest_1.a, hjtest_2.a, (hjtest_1.tableoid)::regclass, (hjtest_2.tableoid)::regclass - Hash Cond: (((SubPlan 1) = hjtest_1.id) AND ((SubPlan 3) = (SubPlan 2))) + Hash Cond: (((SubPlan expr_1) = hjtest_1.id) AND ((SubPlan expr_3) = (SubPlan expr_2))) Join Filter: (hjtest_1.a <> hjtest_2.b) -> Seq Scan on public.hjtest_2 Output: hjtest_2.a, hjtest_2.tableoid, hjtest_2.id, hjtest_2.c, hjtest_2.b - Filter: ((SubPlan 5) < 55) - SubPlan 5 + Filter: ((SubPlan expr_5) < 55) + SubPlan expr_5 -> Result Output: (hjtest_2.c * 5) -> Hash Output: hjtest_1.a, hjtest_1.tableoid, hjtest_1.id, hjtest_1.b -> Seq Scan on public.hjtest_1 Output: hjtest_1.a, hjtest_1.tableoid, hjtest_1.id, hjtest_1.b - Filter: ((SubPlan 4) < 50) - SubPlan 4 + Filter: ((SubPlan expr_4) < 50) + SubPlan expr_4 -> Result Output: (hjtest_1.b * 5) - SubPlan 2 + SubPlan expr_2 -> Result Output: (hjtest_1.b * 5) - SubPlan 1 + SubPlan expr_1 -> Result Output: 1 One-Time Filter: (hjtest_2.id = 1) - SubPlan 3 + SubPlan expr_3 -> Result Output: (hjtest_2.c * 5) (28 rows) diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 5a1eb18aba29e..4e2467852db13 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -1582,6 +1582,38 @@ SELECT jsonb_object_agg(1, NULL::jsonb); SELECT jsonb_object_agg(NULL, '{"a":1}'); ERROR: field name must not be null +SELECT jsonb_object_agg_unique(i, null) OVER (ORDER BY i) + FROM generate_series(1, 10) g(i); + jsonb_object_agg_unique +----------------------------------------------------------------------------------------------------------------- + {"1": null} + {"1": null, "2": null} + {"1": null, "2": null, "3": null} + {"1": null, "2": null, "3": null, "4": null} + {"1": null, "2": null, "3": null, "4": null, "5": null} + {"1": null, "2": null, "3": null, "4": null, "5": null, "6": null} + {"1": null, "2": null, "3": null, "4": null, "5": null, "6": null, "7": null} + {"1": null, "2": null, "3": null, "4": null, "5": null, "6": null, "7": null, "8": null} + {"1": null, "2": null, "3": null, "4": null, "5": null, "6": null, "7": null, "8": null, "9": null} + {"1": null, "2": null, "3": null, "4": null, "5": null, "6": null, "7": null, "8": null, "9": null, "10": null} +(10 rows) + +SELECT jsonb_object_agg_unique_strict(i, null) OVER (ORDER BY i) + FROM generate_series(1, 10) g(i); + jsonb_object_agg_unique_strict +-------------------------------- + {} + {} + {} + {} + {} + {} + {} + {} + {} + {} +(10 rows) + CREATE TEMP TABLE foo (serial_num int, name text, type text); INSERT INTO foo VALUES (847001,'t15','GE1043'); INSERT INTO foo VALUES (847002,'t16','GE1043'); @@ -3099,6 +3131,7 @@ SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; 0 (1 row) +ALTER TABLE testjsonb SET (parallel_workers = 2); CREATE INDEX jidx ON testjsonb USING gin (j); SET enable_seqscan = off; SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}'; @@ -3475,7 +3508,7 @@ SELECT count(*) FROM testjsonb WHERE j = '{"pos":98, "line":371, "node":"CBA", " --gin path opclass DROP INDEX jidx; -CREATE INDEX jidx ON testjsonb USING gin (j jsonb_path_ops); +CREATE INDEX CONCURRENTLY jidx ON testjsonb USING gin (j jsonb_path_ops); SET enable_seqscan = off; SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}'; count @@ -5831,3 +5864,91 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; 12345 (1 row) +-- single argument jsonb functions as jsonb_function(jsonb) and jsonb.jsonb_function +select jsonb_typeof('{"a":1}'::jsonb); + jsonb_typeof +-------------- + object +(1 row) + +select ('{"a":1}'::jsonb).jsonb_typeof; + jsonb_typeof +-------------- + object +(1 row) + +select jsonb_array_length('["a", "b", "c"]'::jsonb); + jsonb_array_length +-------------------- + 3 +(1 row) + +select ('["a", "b", "c"]'::jsonb).jsonb_array_length; + jsonb_array_length +-------------------- + 3 +(1 row) + +select jsonb_object_keys('{"a":1, "b":2}'::jsonb); + jsonb_object_keys +------------------- + a + b +(2 rows) + +select ('{"a":1, "b":2}'::jsonb).jsonb_object_keys; + jsonb_object_keys +------------------- + a + b +(2 rows) + +-- cast jsonb to other types as (jsonb)::type and (jsonb).type +select ('123.45'::jsonb)::numeric; + numeric +--------- + 123.45 +(1 row) + +select ('123.45'::jsonb).numeric; + numeric +--------- + 123.45 +(1 row) + +select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb)::name; + name +-------------------------------------- + [{"name": "alice"}, {"name": "bob"}] +(1 row) + +select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb).name; + name +-------------------------------------- + [{"name": "alice"}, {"name": "bob"}] +(1 row) + +select ('true'::jsonb)::bool; + bool +------ + t +(1 row) + +select ('true'::jsonb).bool; + bool +------ + t +(1 row) + +select ('{"text": "hello"}'::jsonb)::text; + text +------------------- + {"text": "hello"} +(1 row) + +select ('{"text": "hello"}'::jsonb).text; + text +------------------- + {"text": "hello"} +(1 row) + diff --git a/src/test/regress/expected/jsonb_jsonpath.out b/src/test/regress/expected/jsonb_jsonpath.out index 4bcd4e91a2991..afa6c4cb5294b 100644 --- a/src/test/regress/expected/jsonb_jsonpath.out +++ b/src/test/regress/expected/jsonb_jsonpath.out @@ -2723,6 +2723,387 @@ select jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp().string()'); (1 row) rollback; +-- test .ltrim() +select jsonb_path_query('" hello "', '$.ltrim(" ")'); + jsonb_path_query +------------------ + "hello " +(1 row) + +select jsonb_path_query('" hello "', '$.ltrim()'); + jsonb_path_query +------------------ + "hello " +(1 row) + +select jsonb_path_query('"zzzytest"', '$.ltrim("xyz")'); + jsonb_path_query +------------------ + "test" +(1 row) + +select jsonb_path_query('null', '$.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('null', '$.ltrim()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', '$.ltrim()'); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', 'strict $.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('{}', '$.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('[]', 'strict $.ltrim()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('{}', '$.ltrim()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1.23', '$.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('"1.23"', '$.ltrim()'); + jsonb_path_query +------------------ + "1.23" +(1 row) + +select jsonb_path_query('"1.23aaa"', '$.ltrim()'); + jsonb_path_query +------------------ + "1.23aaa" +(1 row) + +select jsonb_path_query('1234', '$.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('true', '$.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('1234', '$.ltrim().type()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query('[2, true]', '$.ltrim()'); +ERROR: jsonpath item method .ltrim() can only be applied to a string +select jsonb_path_query_array('[" maybe ", " yes", " no"]', '$[*].ltrim()'); + jsonb_path_query_array +-------------------------- + ["maybe ", "yes", "no"] +(1 row) + +select jsonb_path_query_array('[" maybe ", " yes", " no"]', '$[*].ltrim().type()'); + jsonb_path_query_array +-------------------------------- + ["string", "string", "string"] +(1 row) + +-- test .rtrim() +select jsonb_path_query('" hello "', '$.rtrim(" ")'); + jsonb_path_query +------------------ + " hello" +(1 row) + +select jsonb_path_query('"testxxzx"', '$.rtrim("xyz")'); + jsonb_path_query +------------------ + "test" +(1 row) + +select jsonb_path_query('" hello "', '$.rtrim()'); + jsonb_path_query +------------------ + " hello" +(1 row) + +-- test .btrim() +select jsonb_path_query('" hello "', '$.btrim(" ")'); + jsonb_path_query +------------------ + "hello" +(1 row) + +select jsonb_path_query('"xyxtrimyyx"', '$.btrim("xyz")'); + jsonb_path_query +------------------ + "trim" +(1 row) + +select jsonb_path_query('" hello "', '$.btrim()'); + jsonb_path_query +------------------ + "hello" +(1 row) + +-- test .lower() +select jsonb_path_query('null', '$.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('null', '$.lower()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', '$.lower()'); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', 'strict $.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('{}', '$.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('[]', 'strict $.lower()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('{}', '$.lower()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1.23', '$.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('"1.23"', '$.lower()'); + jsonb_path_query +------------------ + "1.23" +(1 row) + +select jsonb_path_query('"1.23aaa"', '$.lower()'); + jsonb_path_query +------------------ + "1.23aaa" +(1 row) + +select jsonb_path_query('1234', '$.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('true', '$.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('1234', '$.lower().type()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query('[2, true]', '$.lower()'); +ERROR: jsonpath item method .lower() can only be applied to a string +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].lower()'); + jsonb_path_query_array +------------------------ + ["maybe", "yes", "no"] +(1 row) + +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].lower().type()'); + jsonb_path_query_array +-------------------------------- + ["string", "string", "string"] +(1 row) + +-- test .upper() +select jsonb_path_query('null', '$.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('null', '$.upper()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', '$.upper()'); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', 'strict $.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('{}', '$.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('[]', 'strict $.upper()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('{}', '$.upper()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1.23', '$.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('"1.23"', '$.upper()'); + jsonb_path_query +------------------ + "1.23" +(1 row) + +select jsonb_path_query('"1.23aaa"', '$.upper()'); + jsonb_path_query +------------------ + "1.23AAA" +(1 row) + +select jsonb_path_query('1234', '$.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('true', '$.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('1234', '$.upper().type()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query('[2, true]', '$.upper()'); +ERROR: jsonpath item method .upper() can only be applied to a string +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].upper()'); + jsonb_path_query_array +------------------------ + ["MAYBE", "YES", "NO"] +(1 row) + +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].upper().type()'); + jsonb_path_query_array +-------------------------------- + ["string", "string", "string"] +(1 row) + +-- test .initcap() +select jsonb_path_query('null', '$.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('null', '$.initcap()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', '$.initcap()'); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('[]', 'strict $.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('{}', '$.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('[]', 'strict $.initcap()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('{}', '$.initcap()', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1.23', '$.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('"1.23"', '$.initcap()'); + jsonb_path_query +------------------ + "1.23" +(1 row) + +select jsonb_path_query('"1.23aaa"', '$.initcap()'); + jsonb_path_query +------------------ + "1.23aaa" +(1 row) + +select jsonb_path_query('1234', '$.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('true', '$.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('1234', '$.initcap().type()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('[2, true]', '$.initcap()'); +ERROR: jsonpath item method .initcap() can only be applied to a string +select jsonb_path_query('["maybe yes", "probably no"]', '$.initcap()'); + jsonb_path_query +------------------ + "Maybe Yes" + "Probably No" +(2 rows) + +-- Test .replace() +select jsonb_path_query('null', '$.replace("x", "bye")'); +ERROR: jsonpath item method .replace() can only be applied to a string +select jsonb_path_query('null', '$.replace("x", "bye")', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('["x", "y", "z"]', '$.replace("x", "bye")'); + jsonb_path_query +------------------ + "bye" + "y" + "z" +(3 rows) + +select jsonb_path_query('{}', '$.replace("x", "bye")'); +ERROR: jsonpath item method .replace() can only be applied to a string +select jsonb_path_query('[]', 'strict $.replace("x", "bye")', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('{}', '$.replace("x", "bye")', silent => true); + jsonb_path_query +------------------ +(0 rows) + +select jsonb_path_query('1.23', '$.replace("x", "bye")'); +ERROR: jsonpath item method .replace() can only be applied to a string +select jsonb_path_query('"hello world"', '$.replace("hello","bye")'); + jsonb_path_query +------------------ + "bye world" +(1 row) + +select jsonb_path_query('"hello world"', '$.replace("hello","bye") starts with "bye"'); + jsonb_path_query +------------------ + true +(1 row) + +-- Test .split_part() +select jsonb_path_query('"abc~@~def~@~ghi"', '$.split_part("~@~", 2)'); + jsonb_path_query +------------------ + "def" +(1 row) + +select jsonb_path_query('"abc,def,ghi,jkl"', '$.split_part(",", -2)'); + jsonb_path_query +------------------ + "ghi" +(1 row) + +-- Test string methods play nicely together +select jsonb_path_query('"hello world"', '$.replace("hello","bye").upper()'); + jsonb_path_query +------------------ + "BYE WORLD" +(1 row) + +select jsonb_path_query('"hElLo WorlD"', '$.lower().upper().lower().replace("hello","bye")'); + jsonb_path_query +------------------ + "bye world" +(1 row) + +select jsonb_path_query('"hElLo WorlD"', '$.upper().lower().upper().replace("HELLO", "BYE")'); + jsonb_path_query +------------------ + "BYE WORLD" +(1 row) + +select jsonb_path_query('"hElLo WorlD"', '$.lower().upper().lower().replace("hello","bye") starts with "bye"'); + jsonb_path_query +------------------ + true +(1 row) + +select jsonb_path_query('" hElLo WorlD "', '$.btrim().lower().upper().lower().replace("hello","bye") starts with "bye"'); + jsonb_path_query +------------------ + true +(1 row) + -- Test .time() select jsonb_path_query('null', '$.time()'); ERROR: jsonpath item method .time() can only be applied to a string diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out index fd9bd755f520f..ea971e7985484 100644 --- a/src/test/regress/expected/jsonpath.out +++ b/src/test/regress/expected/jsonpath.out @@ -435,6 +435,192 @@ select '$.string()'::jsonpath; $.string() (1 row) +select '$.replace("hello","bye")'::jsonpath; + jsonpath +-------------------------- + $.replace("hello","bye") +(1 row) + +select '$.lower()'::jsonpath; + jsonpath +----------- + $.lower() +(1 row) + +select '$.upper()'::jsonpath; + jsonpath +----------- + $.upper() +(1 row) + +select '$.lower().upper().lower().replace("hello","bye")'::jsonpath; + jsonpath +-------------------------------------------------- + $.lower().upper().lower().replace("hello","bye") +(1 row) + +select '$.ltrim()'::jsonpath; + jsonpath +----------- + $.ltrim() +(1 row) + +select '$.ltrim("xyz")'::jsonpath; + jsonpath +---------------- + $.ltrim("xyz") +(1 row) + +select '$.rtrim()'::jsonpath; + jsonpath +----------- + $.rtrim() +(1 row) + +select '$.rtrim("xyz")'::jsonpath; + jsonpath +---------------- + $.rtrim("xyz") +(1 row) + +select '$.btrim()'::jsonpath; + jsonpath +----------- + $.btrim() +(1 row) + +select '$.btrim("xyz")'::jsonpath; + jsonpath +---------------- + $.btrim("xyz") +(1 row) + +select '$.initcap()'::jsonpath; + jsonpath +------------- + $.initcap() +(1 row) + +select '$.split_part("~@~", 2)'::jsonpath; + jsonpath +----------------------- + $.split_part("~@~",2) +(1 row) + +-- Parse errors +select '$.replace("hello")'::jsonpath; +ERROR: syntax error at or near ")" of jsonpath input +LINE 1: select '$.replace("hello")'::jsonpath; + ^ +select '$.replace()'::jsonpath; +ERROR: syntax error at or near ")" of jsonpath input +LINE 1: select '$.replace()'::jsonpath; + ^ +select '$.replace("hello","bye","extra")'::jsonpath; +ERROR: syntax error at or near "," of jsonpath input +LINE 1: select '$.replace("hello","bye","extra")'::jsonpath; + ^ +select '$.split_part("~@~")'::jsonpath; +ERROR: syntax error at or near ")" of jsonpath input +LINE 1: select '$.split_part("~@~")'::jsonpath; + ^ +select '$.split_part()'::jsonpath; +ERROR: syntax error at or near ")" of jsonpath input +LINE 1: select '$.split_part()'::jsonpath; + ^ +select '$.split_part("~@~", "hi")'::jsonpath; +ERROR: syntax error at or near """ of jsonpath input +LINE 1: select '$.split_part("~@~", "hi")'::jsonpath; + ^ +select '$.split_part("~@~", 2, "extra")'::jsonpath; +ERROR: syntax error at or near "," of jsonpath input +LINE 1: select '$.split_part("~@~", 2, "extra")'::jsonpath; + ^ +select '$.lower("hi")'::jsonpath; +ERROR: syntax error at or near """ of jsonpath input +LINE 1: select '$.lower("hi")'::jsonpath; + ^ +select '$.upper("hi")'::jsonpath; +ERROR: syntax error at or near """ of jsonpath input +LINE 1: select '$.upper("hi")'::jsonpath; + ^ +select '$.initcap("hi")'::jsonpath; +ERROR: syntax error at or near """ of jsonpath input +LINE 1: select '$.initcap("hi")'::jsonpath; + ^ +select '$.ltrim(42)'::jsonpath; +ERROR: syntax error at or near "42" of jsonpath input +LINE 1: select '$.ltrim(42)'::jsonpath; + ^ +select '$.ltrim("x", "y")'::jsonpath; +ERROR: syntax error at or near "," of jsonpath input +LINE 1: select '$.ltrim("x", "y")'::jsonpath; + ^ +select '$.rtrim(42)'::jsonpath; +ERROR: syntax error at or near "42" of jsonpath input +LINE 1: select '$.rtrim(42)'::jsonpath; + ^ +select '$.rtrim("x", "y")'::jsonpath; +ERROR: syntax error at or near "," of jsonpath input +LINE 1: select '$.rtrim("x", "y")'::jsonpath; + ^ +select '$.trim(42)'::jsonpath; +ERROR: syntax error at or near "(" of jsonpath input +LINE 1: select '$.trim(42)'::jsonpath; + ^ +select '$.trim("x", "y")'::jsonpath; +ERROR: syntax error at or near "(" of jsonpath input +LINE 1: select '$.trim("x", "y")'::jsonpath; + ^ +-- Verify method keywords work as object key names +select '$.lower'::jsonpath; + jsonpath +----------- + $."lower" +(1 row) + +select '$.upper'::jsonpath; + jsonpath +----------- + $."upper" +(1 row) + +select '$.initcap'::jsonpath; + jsonpath +------------- + $."initcap" +(1 row) + +select '$.replace'::jsonpath; + jsonpath +------------- + $."replace" +(1 row) + +select '$.split_part'::jsonpath; + jsonpath +---------------- + $."split_part" +(1 row) + +select '$.ltrim'::jsonpath; + jsonpath +----------- + $."ltrim" +(1 row) + +select '$.rtrim'::jsonpath; + jsonpath +----------- + $."rtrim" +(1 row) + +select '$.btrim'::jsonpath; + jsonpath +----------- + $."btrim" +(1 row) + select '$.time()'::jsonpath; jsonpath ---------- diff --git a/src/test/regress/expected/lock.out b/src/test/regress/expected/lock.out index ad137d3645d0c..f8d53c40871df 100644 --- a/src/test/regress/expected/lock.out +++ b/src/test/regress/expected/lock.out @@ -114,7 +114,7 @@ select relname from pg_locks l, pg_class c ROLLBACK; BEGIN TRANSACTION; LOCK TABLE lock_view6 IN EXCLUSIVE MODE; --- lock_view6 an lock_tbl1 are locked. +-- lock_view6 and lock_tbl1 are locked. select relname from pg_locks l, pg_class c where l.relation = c.oid and relname like '%lock_%' and mode = 'ExclusiveLock' order by relname; diff --git a/src/test/regress/expected/matview.out b/src/test/regress/expected/matview.out index 54939ecc6b08a..0355720dfc6f4 100644 --- a/src/test/regress/expected/matview.out +++ b/src/test/regress/expected/matview.out @@ -294,10 +294,15 @@ DROP MATERIALIZED VIEW IF EXISTS no_such_mv; NOTICE: materialized view "no_such_mv" does not exist, skipping -- make sure invalid combination of options is prohibited REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_tvmm WITH NO DATA; -ERROR: CONCURRENTLY and WITH NO DATA options cannot be used together +ERROR: REFRESH options CONCURRENTLY and WITH NO DATA cannot be used together -- no tuple locks on materialized views SELECT * FROM mvtest_tvvm FOR SHARE; ERROR: cannot lock rows in materialized view "mvtest_tvvm" +-- we don't support temp materialized views, so disallow this case: +CREATE TEMP TABLE mvtest_temp_t (id int NOT NULL, type text NOT NULL, amt numeric NOT NULL); +CREATE MATERIALIZED VIEW mvtest_temp_tm AS SELECT * FROM mvtest_temp_t; +ERROR: materialized views must not use temporary objects +DETAIL: This view depends on temporary table mvtest_temp_t. -- test join of mv and view SELECT type, m.totamt AS mtot, v.totamt AS vtot FROM mvtest_tm m LEFT JOIN mvtest_tv v USING (type) ORDER BY type; type | mtot | vtot @@ -587,7 +592,7 @@ CREATE MATERIALIZED VIEW drop_idx_matview AS NOTICE: index "mvtest_drop_idx" does not exist, skipping CREATE UNIQUE INDEX mvtest_drop_idx ON drop_idx_matview (i); REFRESH MATERIALIZED VIEW CONCURRENTLY drop_idx_matview; -ERROR: could not find suitable unique index on materialized view +ERROR: could not find suitable unique index on materialized view "drop_idx_matview" DROP MATERIALIZED VIEW drop_idx_matview; -- clean up RESET search_path; -- make sure that create WITH NO DATA works via SPI diff --git a/src/test/regress/expected/md5_2.out b/src/test/regress/expected/md5_2.out new file mode 100644 index 0000000000000..4eea7f2bfc360 --- /dev/null +++ b/src/test/regress/expected/md5_2.out @@ -0,0 +1,35 @@ +-- +-- MD5 test suite - from IETF RFC 1321 +-- (see: https://www.rfc-editor.org/rfc/rfc1321) +-- +-- (The md5() function will error in OpenSSL FIPS mode. By keeping +-- this test in a separate file, it is easier to manage variant +-- results.) +select md5('') = 'd41d8cd98f00b204e9800998ecf8427e' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('a') = '0cc175b9c0f1b6a831c399e269772661' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('abc') = '900150983cd24fb0d6963f7d28e17f72' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('message digest') = 'f96b697d7cb7938d525a2f31aaf161d0' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('abcdefghijklmnopqrstuvwxyz') = 'c3fcd3d76192e4007dfb496cca67e13b' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') = 'd174ab98d277d9f5a5611c2c9f419d9f' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('12345678901234567890123456789012345678901234567890123456789012345678901234567890') = '57edf4a22be3c955ac49da2e2107b67a' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5(''::bytea) = 'd41d8cd98f00b204e9800998ecf8427e' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('a'::bytea) = '0cc175b9c0f1b6a831c399e269772661' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('abc'::bytea) = '900150983cd24fb0d6963f7d28e17f72' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('message digest'::bytea) = 'f96b697d7cb7938d525a2f31aaf161d0' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('abcdefghijklmnopqrstuvwxyz'::bytea) = 'c3fcd3d76192e4007dfb496cca67e13b' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'::bytea) = 'd174ab98d277d9f5a5611c2c9f419d9f' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS +select md5('12345678901234567890123456789012345678901234567890123456789012345678901234567890'::bytea) = '57edf4a22be3c955ac49da2e2107b67a' AS "TRUE"; +ERROR: could not compute MD5 hash: disabled for FIPS diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out index 38dfaf021c91d..218972dfab88d 100644 --- a/src/test/regress/expected/memoize.out +++ b/src/test/regress/expected/memoize.out @@ -25,6 +25,7 @@ begin ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N'); ln := regexp_replace(ln, 'loops=\d+', 'loops=N'); ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N'); + ln := regexp_replace(ln, 'Memory: \d+kB', 'Memory: NkB'); return next ln; end loop; end; @@ -261,6 +262,7 @@ CREATE INDEX flt_f_idx ON flt (f); INSERT INTO flt VALUES('-0.0'::float),('+0.0'::float); ANALYZE flt; SET enable_seqscan TO off; +SET enable_material TO off; -- Ensure memoize operates in logical mode SELECT explain_memoize(' SELECT * FROM flt f1 INNER JOIN flt f2 ON f1.f = f2.f;', false); @@ -428,8 +430,8 @@ WHERE unique1 < 3 ---------------------------------------------------------------- Index Scan using tenk1_unique1 on tenk1 t0 Index Cond: (unique1 < 3) - Filter: EXISTS(SubPlan 1) - SubPlan 1 + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 -> Nested Loop -> Index Scan using tenk1_hundred on tenk1 t2 Filter: (t0.two <> four) @@ -454,6 +456,7 @@ WHERE unique1 < 3 (1 row) RESET enable_seqscan; +RESET enable_material; RESET enable_mergejoin; RESET work_mem; RESET hash_mem_multiplier; @@ -500,3 +503,62 @@ RESET max_parallel_workers_per_gather; RESET parallel_tuple_cost; RESET parallel_setup_cost; RESET min_parallel_table_scan_size; +-- Ensure memoize works for ANTI joins +CREATE TABLE tab_anti (a int, b boolean); +INSERT INTO tab_anti SELECT i%3, false FROM generate_series(1,100)i; +ANALYZE tab_anti; +-- Ensure we get a Memoize plan for ANTI join +SELECT explain_memoize(' +SELECT COUNT(*) FROM tab_anti t1 LEFT JOIN +LATERAL (SELECT DISTINCT ON (a) a, b, t1.a AS x FROM tab_anti t2) t2 +ON t1.a+1 = t2.a +WHERE t2.a IS NULL;', false); + explain_memoize +-------------------------------------------------------------------------------------------- + Aggregate (actual rows=1.00 loops=N) + -> Nested Loop Anti Join (actual rows=33.00 loops=N) + -> Seq Scan on tab_anti t1 (actual rows=100.00 loops=N) + -> Memoize (actual rows=0.67 loops=N) + Cache Key: (t1.a + 1), t1.a + Cache Mode: binary + Hits: 97 Misses: 3 Evictions: Zero Overflows: 0 Memory Usage: NkB + -> Subquery Scan on t2 (actual rows=0.67 loops=N) + Filter: ((t1.a + 1) = t2.a) + Rows Removed by Filter: 2 + -> Unique (actual rows=2.67 loops=N) + -> Sort (actual rows=67.33 loops=N) + Sort Key: t2_1.a + Sort Method: quicksort Memory: NkB + -> Seq Scan on tab_anti t2_1 (actual rows=100.00 loops=N) +(15 rows) + +-- And check we get the expected results. +SELECT COUNT(*) FROM tab_anti t1 LEFT JOIN +LATERAL (SELECT DISTINCT ON (a) a, b, t1.a AS x FROM tab_anti t2) t2 +ON t1.a+1 = t2.a +WHERE t2.a IS NULL; + count +------- + 33 +(1 row) + +-- Ensure we do not add memoize node for SEMI join +EXPLAIN (COSTS OFF) +SELECT * FROM tab_anti t1 WHERE t1.a IN + (SELECT a FROM tab_anti t2 WHERE t2.b IN + (SELECT t1.b FROM tab_anti t3 WHERE t2.a > 1 OFFSET 0)); + QUERY PLAN +--------------------------------------------------- + Nested Loop Semi Join + -> Seq Scan on tab_anti t1 + -> Nested Loop Semi Join + Join Filter: (t1.a = t2.a) + -> Seq Scan on tab_anti t2 + -> Subquery Scan on unnamed_subquery + Filter: (t2.b = unnamed_subquery.b) + -> Result + One-Time Filter: (t2.a > 1) + -> Seq Scan on tab_anti t3 +(10 rows) + +DROP TABLE tab_anti; diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out index bcd296682975c..9cb1d87066a3e 100644 --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -1828,29 +1828,29 @@ WHEN MATCHED AND t.c > s.cnt THEN -> Hash Join Output: t.ctid, s.a, s.b, s.c, s.d, s.ctid Hash Cond: (t.a = s.a) - Join Filter: (t.b < (SubPlan 1)) + Join Filter: (t.b < (SubPlan expr_1)) -> Seq Scan on public.tgt t Output: t.ctid, t.a, t.b -> Hash Output: s.a, s.b, s.c, s.d, s.ctid -> Seq Scan on public.src s Output: s.a, s.b, s.c, s.d, s.ctid - SubPlan 1 + SubPlan expr_1 -> Aggregate Output: count(*) -> Seq Scan on public.ref r Output: r.ab, r.cd Filter: ((r.ab = (s.a + s.b)) AND (r.cd = (s.c - s.d))) - SubPlan 4 + SubPlan expr_3 -> Aggregate Output: count(*) -> Seq Scan on public.ref r_2 Output: r_2.ab, r_2.cd Filter: ((r_2.ab = (s.a + s.b)) AND (r_2.cd = (s.c - s.d))) - SubPlan 3 + SubPlan multiexpr_1 -> Result - Output: s.b, (InitPlan 2).col1 - InitPlan 2 + Output: s.b, (InitPlan expr_2).col1 + InitPlan expr_2 -> Aggregate Output: count(*) -> Seq Scan on public.ref r_1 @@ -2426,8 +2426,9 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid Output: t.tid, t.ctid -> Result Output: t.tid, t.ctid + Replaces: Scan on t One-Time Filter: false -(12 rows) +(13 rows) MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid WHEN NOT MATCHED THEN INSERT VALUES (s.sid); @@ -2702,6 +2703,76 @@ SELECT * FROM new_measurement ORDER BY city_id, logdate; 1 | 01-17-2007 | | (2 rows) +-- MERGE into inheritance root table +DROP TRIGGER insert_measurement_trigger ON measurement; +ALTER TABLE measurement ADD CONSTRAINT mcheck CHECK (city_id = 0) NO INHERIT; +EXPLAIN (COSTS OFF) +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 100); + QUERY PLAN +-------------------------------------------------------------------------- + Merge on measurement m + Merge on measurement_y2007m01 m_1 + -> Nested Loop Left Join + -> Result + -> Seq Scan on measurement_y2007m01 m_1 + Filter: ((city_id = 1) AND (logdate = '01-17-2007'::date)) +(6 rows) + +BEGIN; +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 100); +SELECT * FROM ONLY measurement ORDER BY city_id, logdate; + city_id | logdate | peaktemp | unitsales +---------+------------+----------+----------- + 0 | 07-21-2005 | 25 | 35 + 0 | 01-17-2007 | 25 | 100 +(2 rows) + +ROLLBACK; +ALTER TABLE measurement ENABLE ROW LEVEL SECURITY; +ALTER TABLE measurement FORCE ROW LEVEL SECURITY; +CREATE POLICY measurement_p ON measurement USING (peaktemp IS NOT NULL); +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, NULL, 100); -- should fail +ERROR: new row violates row-level security policy for table "measurement" +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 100); -- ok +SELECT * FROM ONLY measurement ORDER BY city_id, logdate; + city_id | logdate | peaktemp | unitsales +---------+------------+----------+----------- + 0 | 07-21-2005 | 25 | 35 + 0 | 01-17-2007 | 25 | 100 +(2 rows) + +MERGE INTO measurement m + USING (VALUES (1, '01-18-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 200) +RETURNING merge_action(), m.*; + merge_action | city_id | logdate | peaktemp | unitsales +--------------+---------+------------+----------+----------- + INSERT | 0 | 01-18-2007 | 25 | 200 +(1 row) + DROP TABLE measurement, new_measurement CASCADE; NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table measurement_y2006m02 diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out index cc517ed5e9007..c3261bff209fb 100644 --- a/src/test/regress/expected/misc_functions.out +++ b/src/test/regress/expected/misc_functions.out @@ -2,46 +2,6 @@ \getenv libdir PG_LIBDIR \getenv dlsuffix PG_DLSUFFIX \set regresslib :libdir '/regress' :dlsuffix --- Function to assist with verifying EXPLAIN which includes costs. A series --- of bool flags allows control over which portions are masked out -CREATE FUNCTION explain_mask_costs(query text, do_analyze bool, - hide_costs bool, hide_row_est bool, hide_width bool) RETURNS setof text -LANGUAGE plpgsql AS -$$ -DECLARE - ln text; - analyze_str text; -BEGIN - IF do_analyze = true THEN - analyze_str := 'on'; - ELSE - analyze_str := 'off'; - END IF; - - -- avoid jit related output by disabling it - SET LOCAL jit = 0; - - FOR ln IN - EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s', - analyze_str, query) - LOOP - IF hide_costs = true THEN - ln := regexp_replace(ln, 'cost=\d+\.\d\d\.\.\d+\.\d\d', 'cost=N..N'); - END IF; - - IF hide_row_est = true THEN - -- don't use 'g' so that we leave the actual rows intact - ln := regexp_replace(ln, 'rows=\d+', 'rows=N'); - END IF; - - IF hide_width = true THEN - ln := regexp_replace(ln, 'width=\d+', 'width=N'); - END IF; - - RETURN NEXT ln; - END LOOP; -END; -$$; -- -- num_nulls() -- @@ -171,12 +131,43 @@ SELECT num_nonnulls(); ERROR: function num_nonnulls() does not exist LINE 1: SELECT num_nonnulls(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given number of arguments. SELECT num_nulls(); ERROR: function num_nulls() does not exist LINE 1: SELECT num_nulls(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given number of arguments. +-- +-- error_on_null() +-- +SELECT error_on_null(1); + error_on_null +--------------- + 1 +(1 row) + +SELECT error_on_null(NULL::int); +ERROR: null value not allowed +SELECT error_on_null(NULL::int[]); +ERROR: null value not allowed +SELECT error_on_null('{1,2,NULL,3}'::int[]); + error_on_null +--------------- + {1,2,NULL,3} +(1 row) + +SELECT error_on_null(ROW(1,NULL::int)); + error_on_null +--------------- + (1,) +(1 row) + +SELECT error_on_null(ROW(NULL,NULL)); + error_on_null +--------------- + (,) +(1 row) + -- -- canonicalize_path() -- @@ -641,142 +632,55 @@ SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g; (4 rows) -- --- Test the SupportRequestRows support function for generate_series_timestamp() --- --- Ensure the row estimate matches the actual rows -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day') g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) -(1 row) - --- As above but with generate_series_timestamp -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMP '2024-02-01', TIMESTAMP '2024-03-01', INTERVAL '1 day') g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) -(1 row) - --- As above but with generate_series_timestamptz_at_zone() -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day', 'UTC') g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) -(1 row) - --- Ensure the estimated and actual row counts match when the range isn't --- evenly divisible by the step -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '7 day') g(s);$$, -true, true, false, true); - explain_mask_costs -------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1) -(1 row) - --- Ensure the estimates match when step is decreasing -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '-1 day') g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) -(1 row) - --- Ensure an empty range estimates 1 row -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '1 day') g(s);$$, -true, true, false, true); - explain_mask_costs -------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) -(1 row) - --- Ensure we get the default row estimate for infinity values -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '-infinity', TIMESTAMPTZ 'infinity', INTERVAL '1 day') g(s);$$, -false, true, false, true); - explain_mask_costs -------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=1000 width=N) -(1 row) - --- Ensure the row estimate behaves correctly when step size is zero. --- We expect generate_series_timestamp() to throw the error rather than in --- the support function. -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '0 day') g(s); -ERROR: step size cannot equal zero +-- Test SupportRequestInlineInFrom request -- --- Test the SupportRequestRows support function for generate_series_numeric() --- --- Ensure the row estimate matches the actual rows -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(1.0, 25.0) g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1) -(1 row) - --- As above but with non-default step -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(1.0, 25.0, 2.0) g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=13 width=N) (actual rows=13.00 loops=1) -(1 row) - --- Ensure the estimates match when step is decreasing -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(25.0, 1.0, -1.0) g(s);$$, -true, true, false, true); - explain_mask_costs ---------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1) -(1 row) - --- Ensure an empty range estimates 1 row -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(25.0, 1.0, 1.0) g(s);$$, -true, true, false, true); - explain_mask_costs -------------------------------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) -(1 row) - --- Ensure we get the default row estimate for error cases (infinity/NaN values --- and zero step size) -SELECT explain_mask_costs($$ -SELECT * FROM generate_series('-infinity'::NUMERIC, 'infinity'::NUMERIC, 1.0) g(s);$$, -false, true, false, true); - explain_mask_costs -------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=1000 width=N) -(1 row) - -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(1.0, 25.0, 'NaN'::NUMERIC) g(s);$$, -false, true, false, true); - explain_mask_costs -------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=1000 width=N) -(1 row) - -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$, -false, true, false, true); - explain_mask_costs -------------------------------------------------------------------- - Function Scan on generate_series g (cost=N..N rows=1000 width=N) +CREATE FUNCTION test_inline_in_from_support_func(internal) + RETURNS internal + AS :'regresslib', 'test_inline_in_from_support_func' + LANGUAGE C STRICT; +CREATE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS SETOF TEXT +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE; +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) + SUPPORT test_inline_in_from_support_func; +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + foo_from_bar +------------------- + doh! + hi de ho neighbor +(2 rows) + +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + foo_from_bar +-------------- + doh! (1 row) +EXPLAIN (COSTS OFF) SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); + QUERY PLAN +---------------------- + Seq Scan on text_tbl +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); + QUERY PLAN +------------------------------- + Seq Scan on text_tbl + Filter: (f1 = 'doh!'::text) +(2 rows) + +DROP FUNCTION foo_from_bar; -- Test functions for control data SELECT count(*) > 0 AS ok FROM pg_control_checkpoint(); ok @@ -889,18 +793,17 @@ SELECT pg_column_toast_chunk_id(a) IS NULL, (1 row) DROP TABLE test_chunk_id; -DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); --- test stratnum support functions -SELECT gist_stratnum_common(7); - gist_stratnum_common ----------------------- - 3 +-- test stratnum translation support functions +SELECT gist_translate_cmptype_common(7); + gist_translate_cmptype_common +------------------------------- + 3 (1 row) -SELECT gist_stratnum_common(3); - gist_stratnum_common ----------------------- - 18 +SELECT gist_translate_cmptype_common(3); + gist_translate_cmptype_common +------------------------------- + 18 (1 row) -- relpath tests @@ -918,3 +821,43 @@ SELECT test_relpath(); SELECT pg_replication_origin_create('regress_' || repeat('a', 505)); ERROR: replication origin name is too long DETAIL: Replication origin names must be no longer than 512 bytes. +-- pg_get_multixact_stats tests +CREATE ROLE regress_multixact_funcs; +-- Access granted for superusers. +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); + null_result +------------- + f +(1 row) + +-- Access revoked. +SET ROLE regress_multixact_funcs; +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); + null_result +------------- + t +(1 row) + +RESET ROLE; +-- Access granted for users with pg_monitor rights. +GRANT pg_monitor TO regress_multixact_funcs; +SET ROLE regress_multixact_funcs; +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); + null_result +------------- + f +(1 row) + +RESET ROLE; +DROP ROLE regress_multixact_funcs; +-- test instr_time nanosecond<->ticks conversion +CREATE FUNCTION test_instr_time() + RETURNS bool + AS :'regresslib' + LANGUAGE C; +SELECT test_instr_time(); + test_instr_time +----------------- + t +(1 row) + diff --git a/src/test/regress/expected/multirangetypes.out b/src/test/regress/expected/multirangetypes.out index c6363ebeb24ca..f5e7df8df4308 100644 --- a/src/test/regress/expected/multirangetypes.out +++ b/src/test/regress/expected/multirangetypes.out @@ -2200,6 +2200,122 @@ SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(-2,0 {[1,2),[4,5)} (1 row) +-- multirange_minus_multi +SELECT multirange_minus_multi(nummultirange(), nummultirange()); + multirange_minus_multi +------------------------ +(0 rows) + +SELECT multirange_minus_multi(nummultirange(), nummultirange(numrange(1,2))); + multirange_minus_multi +------------------------ +(0 rows) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange()); + multirange_minus_multi +------------------------ + {[1,2)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(3,4)), nummultirange()); + multirange_minus_multi +------------------------ + {[1,2),[3,4)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange(numrange(1,2))); + multirange_minus_multi +------------------------ +(0 rows) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange(numrange(2,4))); + multirange_minus_multi +------------------------ + {[1,2)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange(numrange(3,4))); + multirange_minus_multi +------------------------ + {[1,2)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(1,2))); + multirange_minus_multi +------------------------ + {[2,4)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(2,3))); + multirange_minus_multi +------------------------ + {[1,2),[3,4)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(0,8))); + multirange_minus_multi +------------------------ +(0 rows) + +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(0,2))); + multirange_minus_multi +------------------------ + {[2,4)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,8)), nummultirange(numrange(0,2), numrange(3,4))); + multirange_minus_multi +------------------------ + {[2,3),[4,8)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,8)), nummultirange(numrange(2,3), numrange(5,null))); + multirange_minus_multi +------------------------ + {[1,2),[3,5)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0))); + multirange_minus_multi +------------------------ + {[1,2),[4,5)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(2,4))); + multirange_minus_multi +------------------------ + {[1,2),[4,5)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(3,5))); + multirange_minus_multi +------------------------ + {[1,2)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(0,9))); + multirange_minus_multi +------------------------ +(0 rows) + +SELECT multirange_minus_multi(nummultirange(numrange(1,3), numrange(4,5)), nummultirange(numrange(2,9))); + multirange_minus_multi +------------------------ + {[1,2)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(8,9))); + multirange_minus_multi +------------------------ + {[1,2),[4,5)} +(1 row) + +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0), numrange(8,9))); + multirange_minus_multi +------------------------ + {[1,2),[4,5)} +(1 row) + -- intersection SELECT nummultirange() * nummultirange(); ?column? @@ -3096,7 +3212,8 @@ select multirange_of_text(textrange2('a','Z')); -- should fail ERROR: function multirange_of_text(textrange2) does not exist LINE 1: select multirange_of_text(textrange2('a','Z')); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select multirange_of_text(textrange1('a','Z')) @> 'b'::text; ERROR: range lower bound must be less than or equal to range upper bound select unnest(multirange_of_text(textrange1('a','b'), textrange1('d','e'))); @@ -3160,7 +3277,8 @@ select anyarray_anymultirange_func(ARRAY[1,2], nummultirange(numrange(10,20))); ERROR: function anyarray_anymultirange_func(integer[], nummultirange) does not exist LINE 1: select anyarray_anymultirange_func(ARRAY[1,2], nummultirange... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyarray_anymultirange_func(anyarray, anymultirange); -- should fail create function bogus_func(anyelement) @@ -3199,7 +3317,8 @@ select multirangetypes_sql(nummultirange(numrange(1,10)), ARRAY[2,20]); -- matc ERROR: function multirangetypes_sql(nummultirange, integer[]) does not exist LINE 1: select multirangetypes_sql(nummultirange(numrange(1,10)), AR... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. create function anycompatiblearray_anycompatiblemultirange_func(a anycompatiblearray, mr anycompatiblemultirange) returns anycompatible as 'select $1[1] + lower($2);' language sql; select anycompatiblearray_anycompatiblemultirange_func(ARRAY[1,2], multirange(int4range(10,20))); @@ -3219,7 +3338,8 @@ select anycompatiblearray_anycompatiblemultirange_func(ARRAY[1.1,2], multirange( ERROR: function anycompatiblearray_anycompatiblemultirange_func(numeric[], int4multirange) does not exist LINE 1: select anycompatiblearray_anycompatiblemultirange_func(ARRAY... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anycompatiblearray_anycompatiblemultirange_func(anycompatiblearray, anycompatiblemultirange); create function anycompatiblerange_anycompatiblemultirange_func(r anycompatiblerange, mr anycompatiblemultirange) returns anycompatible as 'select lower($1) + lower($2);' language sql; @@ -3234,7 +3354,8 @@ select anycompatiblerange_anycompatiblemultirange_func(numrange(1,2), multirange ERROR: function anycompatiblerange_anycompatiblemultirange_func(numrange, int4multirange) does not exist LINE 1: select anycompatiblerange_anycompatiblemultirange_func(numra... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anycompatiblerange_anycompatiblemultirange_func(anycompatiblerange, anycompatiblemultirange); -- should fail create function bogus_func(anycompatible) diff --git a/src/test/regress/expected/namespace.out b/src/test/regress/expected/namespace.out index dbbda72d3951a..2e582e783c261 100644 --- a/src/test/regress/expected/namespace.out +++ b/src/test/regress/expected/namespace.out @@ -10,13 +10,14 @@ SELECT pg_catalog.set_config('search_path', ' ', false); (1 row) CREATE SCHEMA test_ns_schema_1 - CREATE UNIQUE INDEX abc_a_idx ON abc (a) - CREATE VIEW abc_view AS - SELECT a+1 AS a, b+1 AS b FROM abc CREATE TABLE abc ( a serial, b int UNIQUE - ); + ) + CREATE UNIQUE INDEX abc_a_idx ON abc (a) + CREATE VIEW abc_view AS + SELECT a+1 AS a, b+1 AS b FROM abc +; -- verify that the correct search_path restored on abort SET search_path to public; BEGIN; diff --git a/src/test/regress/expected/nls.out b/src/test/regress/expected/nls.out new file mode 100644 index 0000000000000..465d8c7d0bed3 --- /dev/null +++ b/src/test/regress/expected/nls.out @@ -0,0 +1,53 @@ +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_translation() + RETURNS void + AS :'regresslib' + LANGUAGE C; +-- There's less standardization in locale name spellings than one could wish. +-- While some platforms insist on having a codeset name in lc_messages, +-- fortunately it seems that it need not match the actual database encoding. +-- However, if no es_ES locale is installed at all, this'll fail. +SET lc_messages = 'C'; +do $$ +declare locale text; ok bool; +begin + for locale in values('es_ES'), ('es_ES.UTF-8'), ('es_ES.utf8') + loop + ok = true; + begin + execute format('set lc_messages = %L', locale); + exception when invalid_parameter_value then + ok = false; + end; + exit when ok; + end loop; + -- Don't clutter the expected results with this info, just log it + raise log 'NLS regression test: lc_messages = %', + current_setting('lc_messages'); +end $$; +SELECT test_translation(); +NOTICE: traducido PRId64 = 424242424242 +NOTICE: traducido PRId32 = -1234 +NOTICE: traducido PRIdMAX = -123456789012 +NOTICE: traducido PRIdPTR = -9999 +NOTICE: traducido PRIu64 = 424242424242 +NOTICE: traducido PRIu32 = 4294966062 +NOTICE: traducido PRIuMAX = 123456789012 +NOTICE: traducido PRIuPTR = 9999 +NOTICE: traducido PRIx64 = 62c6d1a9b2 +NOTICE: traducido PRIx32 = fffffb2e +NOTICE: traducido PRIxMAX = 1cbe991a14 +NOTICE: traducido PRIxPTR = 270f +NOTICE: traducido PRIX64 = 62C6D1A9B2 +NOTICE: traducido PRIX32 = FFFFFB2E +NOTICE: traducido PRIXMAX = 1CBE991A14 +NOTICE: traducido PRIXPTR = 270F + test_translation +------------------ + +(1 row) + +RESET lc_messages; diff --git a/src/test/regress/expected/nls_1.out b/src/test/regress/expected/nls_1.out new file mode 100644 index 0000000000000..1498aa62111a8 --- /dev/null +++ b/src/test/regress/expected/nls_1.out @@ -0,0 +1,54 @@ +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_translation() + RETURNS void + AS :'regresslib' + LANGUAGE C; +-- There's less standardization in locale name spellings than one could wish. +-- While some platforms insist on having a codeset name in lc_messages, +-- fortunately it seems that it need not match the actual database encoding. +-- However, if no es_ES locale is installed at all, this'll fail. +SET lc_messages = 'C'; +do $$ +declare locale text; ok bool; +begin + for locale in values('es_ES'), ('es_ES.UTF-8'), ('es_ES.utf8') + loop + ok = true; + begin + execute format('set lc_messages = %L', locale); + exception when invalid_parameter_value then + ok = false; + end; + exit when ok; + end loop; + -- Don't clutter the expected results with this info, just log it + raise log 'NLS regression test: lc_messages = %', + current_setting('lc_messages'); +end $$; +SELECT test_translation(); +NOTICE: NLS is not enabled +NOTICE: translated PRId64 = 424242424242 +NOTICE: translated PRId32 = -1234 +NOTICE: translated PRIdMAX = -123456789012 +NOTICE: translated PRIdPTR = -9999 +NOTICE: translated PRIu64 = 424242424242 +NOTICE: translated PRIu32 = 4294966062 +NOTICE: translated PRIuMAX = 123456789012 +NOTICE: translated PRIuPTR = 9999 +NOTICE: translated PRIx64 = 62c6d1a9b2 +NOTICE: translated PRIx32 = fffffb2e +NOTICE: translated PRIxMAX = 1cbe991a14 +NOTICE: translated PRIxPTR = 270f +NOTICE: translated PRIX64 = 62C6D1A9B2 +NOTICE: translated PRIX32 = FFFFFB2E +NOTICE: translated PRIXMAX = 1CBE991A14 +NOTICE: translated PRIXPTR = 270F + test_translation +------------------ + +(1 row) + +RESET lc_messages; diff --git a/src/test/regress/expected/nls_2.out b/src/test/regress/expected/nls_2.out new file mode 100644 index 0000000000000..54852a39925ce --- /dev/null +++ b/src/test/regress/expected/nls_2.out @@ -0,0 +1,54 @@ +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_translation() + RETURNS void + AS :'regresslib' + LANGUAGE C; +-- There's less standardization in locale name spellings than one could wish. +-- While some platforms insist on having a codeset name in lc_messages, +-- fortunately it seems that it need not match the actual database encoding. +-- However, if no es_ES locale is installed at all, this'll fail. +SET lc_messages = 'C'; +do $$ +declare locale text; ok bool; +begin + for locale in values('es_ES'), ('es_ES.UTF-8'), ('es_ES.utf8') + loop + ok = true; + begin + execute format('set lc_messages = %L', locale); + exception when invalid_parameter_value then + ok = false; + end; + exit when ok; + end loop; + -- Don't clutter the expected results with this info, just log it + raise log 'NLS regression test: lc_messages = %', + current_setting('lc_messages'); +end $$; +SELECT test_translation(); +NOTICE: lc_messages is 'C' +NOTICE: translated PRId64 = 424242424242 +NOTICE: translated PRId32 = -1234 +NOTICE: translated PRIdMAX = -123456789012 +NOTICE: translated PRIdPTR = -9999 +NOTICE: translated PRIu64 = 424242424242 +NOTICE: translated PRIu32 = 4294966062 +NOTICE: translated PRIuMAX = 123456789012 +NOTICE: translated PRIuPTR = 9999 +NOTICE: translated PRIx64 = 62c6d1a9b2 +NOTICE: translated PRIx32 = fffffb2e +NOTICE: translated PRIxMAX = 1cbe991a14 +NOTICE: translated PRIxPTR = 270f +NOTICE: translated PRIX64 = 62C6D1A9B2 +NOTICE: translated PRIX32 = FFFFFB2E +NOTICE: translated PRIXMAX = 1CBE991A14 +NOTICE: translated PRIXPTR = 270F + test_translation +------------------ + +(1 row) + +RESET lc_messages; diff --git a/src/test/regress/expected/numeric.out b/src/test/regress/expected/numeric.out index 072d76ce13173..c58e232a2635c 100644 --- a/src/test/regress/expected/numeric.out +++ b/src/test/regress/expected/numeric.out @@ -1464,9 +1464,21 @@ ERROR: count must be greater than zero SELECT width_bucket(3.5::float8, 3.0::float8, 3.0::float8, 888); ERROR: lower bound cannot equal upper bound SELECT width_bucket('NaN', 3.0, 4.0, 888); -ERROR: operand, lower bound, and upper bound cannot be NaN + width_bucket +-------------- + 889 +(1 row) + +SELECT width_bucket('NaN'::float8, 3.0::float8, 4.0::float8, 888); + width_bucket +-------------- + 889 +(1 row) + +SELECT width_bucket(0, 'NaN', 4.0, 888); +ERROR: lower and upper bounds cannot be NaN SELECT width_bucket(0::float8, 'NaN', 4.0::float8, 888); -ERROR: operand, lower bound, and upper bound cannot be NaN +ERROR: lower and upper bounds cannot be NaN SELECT width_bucket(2.0, 3.0, '-inf', 888); ERROR: lower and upper bounds must be finite SELECT width_bucket(0::float8, '-inf', 4.0::float8, 888); @@ -3860,15 +3872,15 @@ ERROR: factorial of a negative number is undefined -- Tests for pg_lsn() -- SELECT pg_lsn(23783416::numeric); - pg_lsn ------------ - 0/16AE7F8 + pg_lsn +------------ + 0/016AE7F8 (1 row) SELECT pg_lsn(0::numeric); - pg_lsn --------- - 0/0 + pg_lsn +------------ + 0/00000000 (1 row) SELECT pg_lsn(18446744073709551615::numeric); diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out index fc42d418bf154..97227d67a5414 100644 --- a/src/test/regress/expected/object_address.out +++ b/src/test/regress/expected/object_address.out @@ -34,6 +34,7 @@ CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig(); CREATE POLICY genpol ON addr_nsp.gentable; CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$; +CREATE PROPERTY GRAPH addr_nsp.gengraph; CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw; CREATE USER MAPPING FOR regress_addr_user SERVER "integer"; ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user; @@ -50,7 +51,7 @@ CREATE PUBLICATION addr_pub_schema FOR TABLES IN SCHEMA addr_nsp; RESET client_min_messages; CREATE SUBSCRIPTION regress_addr_sub CONNECTION '' PUBLICATION bar WITH (connect = false, slot_name = NONE); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable; -- test some error cases SELECT pg_get_object_address('stone', '{}', '{}'); @@ -65,7 +66,8 @@ DECLARE objtype text; BEGIN FOR objtype IN VALUES ('toast table'), ('index column'), ('sequence column'), - ('toast table column'), ('view column'), ('materialized view column') + ('toast table column'), ('view column'), ('materialized view column'), + ('property graph element'), ('property graph label'), ('property graph property') LOOP BEGIN PERFORM pg_get_object_address(objtype, '{one}', '{}'); @@ -81,6 +83,9 @@ WARNING: error for sequence column: unsupported object type "sequence column" WARNING: error for toast table column: unsupported object type "toast table column" WARNING: error for view column: unsupported object type "view column" WARNING: error for materialized view column: unsupported object type "materialized view column" +WARNING: error for property graph element: unsupported object type "property graph element" +WARNING: error for property graph label: unsupported object type "property graph label" +WARNING: error for property graph property: unsupported object type "property graph property" -- miscellaneous other errors select * from pg_get_object_address('operator of access method', '{btree,integer_ops,1}', '{int4,bool}'); ERROR: operator 1 (int4, bool) of operator family integer_ops for access method btree does not exist @@ -98,7 +103,7 @@ DECLARE BEGIN FOR objtype IN VALUES ('table'), ('index'), ('sequence'), ('view'), - ('materialized view'), ('foreign table'), + ('materialized view'), ('foreign table'), ('property graph'), ('table column'), ('foreign table column'), ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'), ('table constraint'), ('domain constraint'), ('conversion'), ('default value'), @@ -159,6 +164,12 @@ WARNING: error for foreign table,{addr_nsp,zwei},{}: relation "addr_nsp.zwei" d WARNING: error for foreign table,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist WARNING: error for foreign table,{eins,zwei,drei},{}: cross-database references are not implemented: "eins.zwei.drei" WARNING: error for foreign table,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei" +WARNING: error for property graph,{eins},{}: relation "eins" does not exist +WARNING: error for property graph,{eins},{integer}: relation "eins" does not exist +WARNING: error for property graph,{addr_nsp,zwei},{}: relation "addr_nsp.zwei" does not exist +WARNING: error for property graph,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist +WARNING: error for property graph,{eins,zwei,drei},{}: cross-database references are not implemented: "eins.zwei.drei" +WARNING: error for property graph,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei" WARNING: error for table column,{eins},{}: column name must be qualified WARNING: error for table column,{eins},{integer}: column name must be qualified WARNING: error for table column,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist @@ -398,6 +409,7 @@ WITH objects (type, name, args) AS (VALUES ('view', '{addr_nsp, genview}', '{}'), ('materialized view', '{addr_nsp, genmatview}', '{}'), ('foreign table', '{addr_nsp, genftable}', '{}'), + ('property graph', '{addr_nsp, gengraph}', '{}'), ('table column', '{addr_nsp, gentable, b}', '{}'), ('foreign table column', '{addr_nsp, genftable, a}', '{}'), ('aggregate', '{addr_nsp, genaggr}', '{int4}'), @@ -411,7 +423,7 @@ WITH objects (type, name, args) AS (VALUES ('collation', '{default}', '{}'), ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'), ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'), - ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'), + ('conversion', '{pg_catalog, koi8_r_to_utf8}', '{}'), ('default value', '{addr_nsp, gentable, b}', '{}'), ('language', '{plpgsql}', '{}'), -- large object @@ -474,6 +486,7 @@ view|addr_nsp|genview|addr_nsp.genview|t materialized view|addr_nsp|genmatview|addr_nsp.genmatview|t foreign table|addr_nsp|genftable|addr_nsp.genftable|t foreign table column|addr_nsp|genftable|addr_nsp.genftable.a|t +property graph|addr_nsp|gengraph|addr_nsp.gengraph|t role|NULL|regress_addr_user|regress_addr_user|t server|NULL|addr_fserv|addr_fserv|t user mapping|NULL|NULL|regress_addr_user on server integer|t @@ -485,7 +498,7 @@ default value|NULL|NULL|for addr_nsp.gentable.b|t cast|NULL|NULL|(bigint AS integer)|t table constraint|addr_nsp|NULL|a_chk on addr_nsp.gentable|t domain constraint|addr_nsp|NULL|domconstr on addr_nsp.gendomain|t -conversion|pg_catalog|koi8_r_to_mic|pg_catalog.koi8_r_to_mic|t +conversion|pg_catalog|koi8_r_to_utf8|pg_catalog.koi8_r_to_utf8|t language|NULL|plpgsql|plpgsql|t schema|NULL|addr_nsp|addr_nsp|t operator class|pg_catalog|int4_ops|pg_catalog.int4_ops USING btree|t @@ -518,7 +531,7 @@ DROP PUBLICATION addr_pub; DROP PUBLICATION addr_pub_schema; DROP SUBSCRIPTION regress_addr_sub; DROP SCHEMA addr_nsp CASCADE; -NOTICE: drop cascades to 14 other objects +NOTICE: drop cascades to 15 other objects DETAIL: drop cascades to text search dictionary addr_ts_dict drop cascades to text search configuration addr_ts_conf drop cascades to text search template addr_ts_temp @@ -533,6 +546,7 @@ drop cascades to function genaggr(integer) drop cascades to type gendomain drop cascades to function trig() drop cascades to function proc(integer) +drop cascades to property graph gengraph DROP OWNED BY regress_addr_user; DROP USER regress_addr_user; -- @@ -578,6 +592,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES ('pg_event_trigger'::regclass, 0, 0), -- no event trigger ('pg_parameter_acl'::regclass, 0, 0), -- no parameter ACL ('pg_policy'::regclass, 0, 0), -- no policy + ('pg_propgraph_element'::regclass, 0, 0), -- no property graph element + ('pg_propgraph_label'::regclass, 0, 0), -- no property graph label + ('pg_propgraph_property'::regclass, 0, 0), -- no property graph property ('pg_publication'::regclass, 0, 0), -- no publication ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace ('pg_publication_rel'::regclass, 0, 0), -- no publication relation @@ -634,5 +651,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid; ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|NULL +("(""property graph element"",,,)")|("(""property graph element"",,)")|NULL +("(""property graph label"",,,)")|("(""property graph label"",,)")|NULL +("(""property graph property"",,,)")|("(""property graph property"",,)")|NULL -- restore normal output mode \a\t diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out new file mode 100644 index 0000000000000..2e114f1ce7020 --- /dev/null +++ b/src/test/regress/expected/oid8.out @@ -0,0 +1,368 @@ +-- +-- OID8 +-- +CREATE TABLE OID8_TBL(f1 oid8); +INSERT INTO OID8_TBL(f1) VALUES ('1234'); +INSERT INTO OID8_TBL(f1) VALUES ('1235'); +INSERT INTO OID8_TBL(f1) VALUES ('987'); +INSERT INTO OID8_TBL(f1) VALUES ('-1040'); +INSERT INTO OID8_TBL(f1) VALUES ('99999999'); +INSERT INTO OID8_TBL(f1) VALUES ('5 '); +INSERT INTO OID8_TBL(f1) VALUES (' 10 '); +INSERT INTO OID8_TBL(f1) VALUES ('123456789012345678'); +-- UINT64_MAX +INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551615'); +-- leading/trailing hard tab is also allowed +INSERT INTO OID8_TBL(f1) VALUES (' 15 '); +-- bad inputs +INSERT INTO OID8_TBL(f1) VALUES (''); +ERROR: invalid input syntax for type oid8: "" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES (''); + ^ +INSERT INTO OID8_TBL(f1) VALUES (' '); +ERROR: invalid input syntax for type oid8: " " +LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' '); + ^ +INSERT INTO OID8_TBL(f1) VALUES ('asdfasd'); +ERROR: invalid input syntax for type oid8: "asdfasd" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('asdfasd'); + ^ +INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd'); +ERROR: invalid input syntax for type oid8: "99asdfasd" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd'); + ^ +INSERT INTO OID8_TBL(f1) VALUES ('5 d'); +ERROR: invalid input syntax for type oid8: "5 d" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5 d'); + ^ +INSERT INTO OID8_TBL(f1) VALUES (' 5d'); +ERROR: invalid input syntax for type oid8: " 5d" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' 5d'); + ^ +INSERT INTO OID8_TBL(f1) VALUES ('5 5'); +ERROR: invalid input syntax for type oid8: "5 5" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('5 5'); + ^ +INSERT INTO OID8_TBL(f1) VALUES (' - 500'); +ERROR: invalid input syntax for type oid8: " - 500" +LINE 1: INSERT INTO OID8_TBL(f1) VALUES (' - 500'); + ^ +INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485'); +ERROR: value "3908203590239580293850293850329485" is out of range for type oid8 +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('39082035902395802938502938... + ^ +INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934'); +ERROR: value "-1204982019841029840928340329840934" is out of range for type oid8 +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340... + ^ +-- UINT64_MAX + 1 +INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551616'); +ERROR: value "18446744073709551616" is out of range for type oid8 +LINE 1: INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551616'); + ^ +SELECT * FROM OID8_TBL; + f1 +---------------------- + 1234 + 1235 + 987 + 18446744073709550576 + 99999999 + 5 + 10 + 123456789012345678 + 18446744073709551615 + 15 +(10 rows) + +-- Also try it with non-error-throwing API +SELECT pg_input_is_valid('1234', 'oid8'); + pg_input_is_valid +------------------- + t +(1 row) + +SELECT pg_input_is_valid('01XYZ', 'oid8'); + pg_input_is_valid +------------------- + f +(1 row) + +SELECT * FROM pg_input_error_info('01XYZ', 'oid8'); + message | detail | hint | sql_error_code +---------------------------------------------+--------+------+---------------- + invalid input syntax for type oid8: "01XYZ" | | | 22P02 +(1 row) + +SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8'); + pg_input_is_valid +------------------- + f +(1 row) + +SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------+--------+------+---------------- + value "-1204982019841029840928340329840934" is out of range for type oid8 | | | 22003 +(1 row) + +-- Operators +SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234; + f1 +------ + 1234 +(1 row) + +SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234'; + f1 +---------------------- + 1235 + 987 + 18446744073709550576 + 99999999 + 5 + 10 + 123456789012345678 + 18446744073709551615 + 15 +(9 rows) + +SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234'; + f1 +------ + 1234 + 987 + 5 + 10 + 15 +(5 rows) + +SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234'; + f1 +----- + 987 + 5 + 10 + 15 +(4 rows) + +SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234'; + f1 +---------------------- + 1234 + 1235 + 18446744073709550576 + 99999999 + 123456789012345678 + 18446744073709551615 +(6 rows) + +SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234'; + f1 +---------------------- + 1235 + 18446744073709550576 + 99999999 + 123456789012345678 + 18446744073709551615 +(5 rows) + +-- Casts +SELECT 1::int2::oid8; + oid8 +------ + 1 +(1 row) + +SELECT 1::int4::oid8; + oid8 +------ + 1 +(1 row) + +SELECT 1::int8::oid8; + oid8 +------ + 1 +(1 row) + +SELECT 1::oid8::int8; + int8 +------ + 1 +(1 row) + +SELECT 1::oid::oid8; -- ok + oid8 +------ + 1 +(1 row) + +SELECT 1::oid8::oid; -- not ok +ERROR: cannot cast type oid8 to oid +LINE 1: SELECT 1::oid8::oid; + ^ +-- Aggregates +SELECT min(f1), max(f1) FROM OID8_TBL; + min | max +-----+---------------------- + 5 | 18446744073709551615 +(1 row) + +-- Check btree and hash opclasses +EXPLAIN (COSTS OFF) +SELECT DISTINCT (i || '000000000000' || j)::oid8 f + FROM generate_series(1, 10) i, + generate_series(1, 10) j, + generate_series(1, 5) k + WHERE i <= 10 AND j > 0 AND j <= 10 + ORDER BY f; + QUERY PLAN +----------------------------------------------------------------------------------- + Sort + Sort Key: (((((i.i)::text || '000000000000'::text) || (j.j)::text))::oid8) + -> HashAggregate + Group Key: ((((i.i)::text || '000000000000'::text) || (j.j)::text))::oid8 + -> Nested Loop + -> Function Scan on generate_series k + -> Materialize + -> Nested Loop + -> Function Scan on generate_series j + Filter: ((j > 0) AND (j <= 10)) + -> Function Scan on generate_series i + Filter: (i <= 10) +(12 rows) + +SELECT DISTINCT (i || '000000000000' || j)::oid8 f + FROM generate_series(1, 10) i, + generate_series(1, 10) j, + generate_series(1, 5) k + WHERE i <= 10 AND j > 0 AND j <= 10 + ORDER BY f; + f +------------------ + 10000000000001 + 10000000000002 + 10000000000003 + 10000000000004 + 10000000000005 + 10000000000006 + 10000000000007 + 10000000000008 + 10000000000009 + 20000000000001 + 20000000000002 + 20000000000003 + 20000000000004 + 20000000000005 + 20000000000006 + 20000000000007 + 20000000000008 + 20000000000009 + 30000000000001 + 30000000000002 + 30000000000003 + 30000000000004 + 30000000000005 + 30000000000006 + 30000000000007 + 30000000000008 + 30000000000009 + 40000000000001 + 40000000000002 + 40000000000003 + 40000000000004 + 40000000000005 + 40000000000006 + 40000000000007 + 40000000000008 + 40000000000009 + 50000000000001 + 50000000000002 + 50000000000003 + 50000000000004 + 50000000000005 + 50000000000006 + 50000000000007 + 50000000000008 + 50000000000009 + 60000000000001 + 60000000000002 + 60000000000003 + 60000000000004 + 60000000000005 + 60000000000006 + 60000000000007 + 60000000000008 + 60000000000009 + 70000000000001 + 70000000000002 + 70000000000003 + 70000000000004 + 70000000000005 + 70000000000006 + 70000000000007 + 70000000000008 + 70000000000009 + 80000000000001 + 80000000000002 + 80000000000003 + 80000000000004 + 80000000000005 + 80000000000006 + 80000000000007 + 80000000000008 + 80000000000009 + 90000000000001 + 90000000000002 + 90000000000003 + 90000000000004 + 90000000000005 + 90000000000006 + 90000000000007 + 90000000000008 + 90000000000009 + 100000000000001 + 100000000000002 + 100000000000003 + 100000000000004 + 100000000000005 + 100000000000006 + 100000000000007 + 100000000000008 + 100000000000009 + 100000000000010 + 200000000000010 + 300000000000010 + 400000000000010 + 500000000000010 + 600000000000010 + 700000000000010 + 800000000000010 + 900000000000010 + 1000000000000010 +(100 rows) + +-- 3-way compare for btrees +SELECT btoid8cmp(1::oid8, 2::oid8) < 0 AS val_lower; + val_lower +----------- + t +(1 row) + +SELECT btoid8cmp(2::oid8, 2::oid8) = 0 AS val_equal; + val_equal +----------- + t +(1 row) + +SELECT btoid8cmp(2::oid8, 1::oid8) > 0 AS val_higher; + val_higher +------------ + t +(1 row) + +-- oid8 has btree and hash opclasses +CREATE INDEX on OID8_TBL USING btree(f1); +CREATE INDEX ON OID8_TBL USING hash(f1); +DROP TABLE OID8_TBL; diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3e9..d64169b7bf005 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -224,6 +224,7 @@ NOTICE: checking pg_extension {extconfig} => pg_class {oid} NOTICE: checking pg_foreign_data_wrapper {fdwowner} => pg_authid {oid} NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} +NOTICE: checking pg_foreign_data_wrapper {fdwconnection} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} @@ -249,6 +250,11 @@ NOTICE: checking pg_range {rngsubtype} => pg_type {oid} NOTICE: checking pg_range {rngmultitypid} => pg_type {oid} NOTICE: checking pg_range {rngcollation} => pg_collation {oid} NOTICE: checking pg_range {rngsubopc} => pg_opclass {oid} +NOTICE: checking pg_range {rngconstruct2} => pg_proc {oid} +NOTICE: checking pg_range {rngconstruct3} => pg_proc {oid} +NOTICE: checking pg_range {rngmltconstruct0} => pg_proc {oid} +NOTICE: checking pg_range {rngmltconstruct1} => pg_proc {oid} +NOTICE: checking pg_range {rngmltconstruct2} => pg_proc {oid} NOTICE: checking pg_range {rngcanonical} => pg_proc {oid} NOTICE: checking pg_range {rngsubdiff} => pg_proc {oid} NOTICE: checking pg_transform {trftype} => pg_type {oid} @@ -264,5 +270,18 @@ NOTICE: checking pg_publication_rel {prpubid} => pg_publication {oid} NOTICE: checking pg_publication_rel {prrelid} => pg_class {oid} NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} +NOTICE: checking pg_subscription {subserver} => pg_foreign_server {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_propgraph_element {pgepgid} => pg_class {oid} +NOTICE: checking pg_propgraph_element {pgerelid} => pg_class {oid} +NOTICE: checking pg_propgraph_element {pgesrcvertexid} => pg_propgraph_element {oid} +NOTICE: checking pg_propgraph_element {pgedestvertexid} => pg_propgraph_element {oid} +NOTICE: checking pg_propgraph_element_label {pgellabelid} => pg_propgraph_label {oid} +NOTICE: checking pg_propgraph_element_label {pgelelid} => pg_propgraph_element {oid} +NOTICE: checking pg_propgraph_label {pglpgid} => pg_class {oid} +NOTICE: checking pg_propgraph_label_property {plppropid} => pg_propgraph_property {oid} +NOTICE: checking pg_propgraph_label_property {plpellabelid} => pg_propgraph_element_label {oid} +NOTICE: checking pg_propgraph_property {pgppgid} => pg_class {oid} +NOTICE: checking pg_propgraph_property {pgptypid} => pg_type {oid} +NOTICE: checking pg_propgraph_property {pgpcollation} => pg_collation {oid} diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out index 20bf9ea9cdf76..cfdc6b1a17a25 100644 --- a/src/test/regress/expected/opr_sanity.out +++ b/src/test/regress/expected/opr_sanity.out @@ -880,6 +880,15 @@ bytea(integer) bytea(bigint) bytea_larger(bytea,bytea) bytea_smaller(bytea,bytea) +oid8eq(oid8,oid8) +oid8ne(oid8,oid8) +oid8lt(oid8,oid8) +oid8le(oid8,oid8) +oid8gt(oid8,oid8) +oid8ge(oid8,oid8) +btoid8cmp(oid8,oid8) +tid_block(tid) +tid_offset(tid) -- Check that functions without argument are not marked as leakproof. SELECT p1.oid::regprocedure FROM pg_proc p1 JOIN pg_namespace pn @@ -1470,7 +1479,7 @@ WHERE aggfnoid = 0 OR aggtransfn = 0 OR (aggkind = 'n' AND aggnumdirectargs > 0) OR aggfinalmodify NOT IN ('r', 's', 'w') OR aggmfinalmodify NOT IN ('r', 's', 'w') OR - aggtranstype = 0 OR aggtransspace < 0 OR aggmtransspace < 0; + aggtranstype = 0 OR aggmtransspace < 0; ctid | aggfnoid ------+---------- (0 rows) diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out index 5f2c0cf5786e3..c30304b99c79e 100644 --- a/src/test/regress/expected/partition_aggregate.out +++ b/src/test/regress/expected/partition_aggregate.out @@ -13,6 +13,8 @@ SET enable_partitionwise_join TO true; SET max_parallel_workers_per_gather TO 0; -- Disable incremental sort, which can influence selected plans due to fuzz factor. SET enable_incremental_sort TO off; +-- Disable eager aggregation, which can interfere with the generation of partitionwise aggregation. +SET enable_eager_aggregate TO off; -- -- Tests for list partitioned tables. -- @@ -146,13 +148,14 @@ SELECT c, a, count(*) FROM pagg_tab GROUP BY a, c; -- Test when input relation for grouping is dummy EXPLAIN (COSTS OFF) SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c; - QUERY PLAN --------------------------------- + QUERY PLAN +------------------------------------ HashAggregate Group Key: c -> Result + Replaces: Scan on pagg_tab One-Time Filter: false -(4 rows) +(5 rows) SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c; c | sum @@ -161,12 +164,13 @@ SELECT c, sum(a) FROM pagg_tab WHERE 1 = 2 GROUP BY c; EXPLAIN (COSTS OFF) SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c; - QUERY PLAN --------------------------------- + QUERY PLAN +------------------------------------ GroupAggregate -> Result + Replaces: Scan on pagg_tab One-Time Filter: false -(3 rows) +(4 rows) SELECT c, sum(a) FROM pagg_tab WHERE c = 'x' GROUP BY c; c | sum @@ -335,6 +339,37 @@ SELECT a FROM pagg_tab WHERE a < 3 GROUP BY a ORDER BY 1; 2 (3 rows) +-- Test partitionwise aggregation with ordered append path built from fractional paths +EXPLAIN (COSTS OFF) +SELECT count(*) FROM pagg_tab GROUP BY c ORDER BY c LIMIT 1; + QUERY PLAN +------------------------------------------------------------ + Limit + -> Merge Append + Sort Key: pagg_tab.c + -> GroupAggregate + Group Key: pagg_tab.c + -> Sort + Sort Key: pagg_tab.c + -> Seq Scan on pagg_tab_p1 pagg_tab + -> GroupAggregate + Group Key: pagg_tab_1.c + -> Sort + Sort Key: pagg_tab_1.c + -> Seq Scan on pagg_tab_p2 pagg_tab_1 + -> GroupAggregate + Group Key: pagg_tab_2.c + -> Sort + Sort Key: pagg_tab_2.c + -> Seq Scan on pagg_tab_p3 pagg_tab_2 +(18 rows) + +SELECT count(*) FROM pagg_tab GROUP BY c ORDER BY c LIMIT 1; + count +------- + 250 +(1 row) + RESET enable_hashagg; -- ROLLUP, partitionwise aggregation does not apply EXPLAIN (COSTS OFF) @@ -804,15 +839,16 @@ SELECT a.x, b.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x < 20) a FULL JOI -- Empty join relation because of empty outer side, no partitionwise agg plan EXPLAIN (COSTS OFF) SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2; - QUERY PLAN --------------------------------------- + QUERY PLAN +---------------------------------------------- GroupAggregate Group Key: pagg_tab1.y -> Sort Sort Key: pagg_tab1.y -> Result + Replaces: Join on b, pagg_tab1 One-Time Filter: false -(6 rows) +(7 rows) SELECT a.x, a.y, count(*) FROM (SELECT * FROM pagg_tab1 WHERE x = 1 AND x = 2) a LEFT JOIN pagg_tab2 b ON a.x = b.y GROUP BY a.x, a.y ORDER BY 1, 2; x | y | count diff --git a/src/test/regress/expected/partition_info.out b/src/test/regress/expected/partition_info.out index 42b6bc77cad0d..4defa66e5b3df 100644 --- a/src/test/regress/expected/partition_info.out +++ b/src/test/regress/expected/partition_info.out @@ -349,3 +349,10 @@ SELECT pg_partition_root('ptif_li_child'); DROP VIEW ptif_test_view; DROP MATERIALIZED VIEW ptif_test_matview; DROP TABLE ptif_li_parent, ptif_li_child; +-- Test about selection of arbiter indexes for partitioned tables with +-- non-valid index on the parent table +CREATE TABLE pt (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE TABLE p1 PARTITION OF pt FOR VALUES FROM (1) to (2) PARTITION BY RANGE (a); +CREATE TABLE p1_1 PARTITION OF p1 FOR VALUES FROM (1) TO (2); +CREATE UNIQUE INDEX ON ONLY p1 (a); +INSERT INTO p1 VALUES (1) ON CONFLICT (a) DO NOTHING; diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out index d5368186caa9f..38643d41fd767 100644 --- a/src/test/regress/expected/partition_join.out +++ b/src/test/regress/expected/partition_join.out @@ -63,18 +63,21 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = (4 rows) -- inner join with partially-redundant join clauses +-- (avoid a mergejoin, because the planner thinks that a non-partitionwise +-- merge join is the cheapest plan, and we want to test a partitionwise join) +BEGIN; +SET LOCAL enable_mergejoin = false; EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = t2.b ORDER BY t1.a, t2.b; - QUERY PLAN ---------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------- Sort Sort Key: t1.a -> Append - -> Merge Join - Merge Cond: (t1_1.a = t2_1.a) - -> Index Scan using iprt1_p1_a on prt1_p1 t1_1 - -> Sort - Sort Key: t2_1.b + -> Hash Join + Hash Cond: (t1_1.a = t2_1.a) + -> Seq Scan on prt1_p1 t1_1 + -> Hash -> Seq Scan on prt2_p1 t2_1 Filter: (a = b) -> Hash Join @@ -89,7 +92,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = -> Hash -> Seq Scan on prt2_p3 t2_3 Filter: (a = b) -(22 rows) +(21 rows) SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = t2.b ORDER BY t1.a, t2.b; a | c | b | c @@ -101,6 +104,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = 24 | 0024 | 24 | 0024 (5 rows) +COMMIT; -- left outer join, 3-way EXPLAIN (COSTS OFF) SELECT COUNT(*) FROM prt1 t1 @@ -1134,48 +1138,50 @@ EXPLAIN (COSTS OFF) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHERE t1.a = 0 AND t1.b = (t2.a + t2.b)/2) AND t1.b = 0 ORDER BY t1.a; QUERY PLAN --------------------------------------------------------------------------------- - Sort + Merge Append Sort Key: t1.a - -> Append - -> Nested Loop - Join Filter: (t1_2.a = t1_5.b) - -> HashAggregate - Group Key: t1_5.b + -> Nested Loop + Join Filter: (t1_2.a = t1_5.b) + -> Unique + -> Sort + Sort Key: t1_5.b -> Hash Join Hash Cond: (((t2_1.a + t2_1.b) / 2) = t1_5.b) -> Seq Scan on prt1_e_p1 t2_1 -> Hash -> Seq Scan on prt2_p1 t1_5 Filter: (a = 0) - -> Index Scan using iprt1_p1_a on prt1_p1 t1_2 - Index Cond: (a = ((t2_1.a + t2_1.b) / 2)) - Filter: (b = 0) - -> Nested Loop - Join Filter: (t1_3.a = t1_6.b) - -> HashAggregate - Group Key: t1_6.b + -> Index Scan using iprt1_p1_a on prt1_p1 t1_2 + Index Cond: (a = ((t2_1.a + t2_1.b) / 2)) + Filter: (b = 0) + -> Nested Loop + Join Filter: (t1_3.a = t1_6.b) + -> Unique + -> Sort + Sort Key: t1_6.b -> Hash Join Hash Cond: (((t2_2.a + t2_2.b) / 2) = t1_6.b) -> Seq Scan on prt1_e_p2 t2_2 -> Hash -> Seq Scan on prt2_p2 t1_6 Filter: (a = 0) - -> Index Scan using iprt1_p2_a on prt1_p2 t1_3 - Index Cond: (a = ((t2_2.a + t2_2.b) / 2)) - Filter: (b = 0) - -> Nested Loop - Join Filter: (t1_4.a = t1_7.b) - -> HashAggregate - Group Key: t1_7.b + -> Index Scan using iprt1_p2_a on prt1_p2 t1_3 + Index Cond: (a = ((t2_2.a + t2_2.b) / 2)) + Filter: (b = 0) + -> Nested Loop + Join Filter: (t1_4.a = t1_7.b) + -> Unique + -> Sort + Sort Key: t1_7.b -> Nested Loop -> Seq Scan on prt2_p3 t1_7 Filter: (a = 0) -> Index Scan using iprt1_e_p3_ab2 on prt1_e_p3 t2_3 Index Cond: (((a + b) / 2) = t1_7.b) - -> Index Scan using iprt1_p3_a on prt1_p3 t1_4 - Index Cond: (a = ((t2_3.a + t2_3.b) / 2)) - Filter: (b = 0) -(41 rows) + -> Index Scan using iprt1_p3_a on prt1_p3 t1_4 + Index Cond: (a = ((t2_3.a + t2_3.b) / 2)) + Filter: (b = 0) +(43 rows) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1, prt1_e t2 WHERE t1.a = 0 AND t1.b = (t2.a + t2.b)/2) AND t1.b = 0 ORDER BY t1.a; a | b | c @@ -1190,46 +1196,48 @@ EXPLAIN (COSTS OFF) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; QUERY PLAN --------------------------------------------------------------------------- - Sort + Merge Append Sort Key: t1.a - -> Append - -> Nested Loop - -> HashAggregate - Group Key: t1_6.b + -> Nested Loop + -> Unique + -> Sort + Sort Key: t1_6.b -> Hash Semi Join Hash Cond: (t1_6.b = ((t1_9.a + t1_9.b) / 2)) -> Seq Scan on prt2_p1 t1_6 -> Hash -> Seq Scan on prt1_e_p1 t1_9 Filter: (c = 0) - -> Index Scan using iprt1_p1_a on prt1_p1 t1_3 - Index Cond: (a = t1_6.b) - Filter: (b = 0) - -> Nested Loop - -> HashAggregate - Group Key: t1_7.b + -> Index Scan using iprt1_p1_a on prt1_p1 t1_3 + Index Cond: (a = t1_6.b) + Filter: (b = 0) + -> Nested Loop + -> Unique + -> Sort + Sort Key: t1_7.b -> Hash Semi Join Hash Cond: (t1_7.b = ((t1_10.a + t1_10.b) / 2)) -> Seq Scan on prt2_p2 t1_7 -> Hash -> Seq Scan on prt1_e_p2 t1_10 Filter: (c = 0) - -> Index Scan using iprt1_p2_a on prt1_p2 t1_4 - Index Cond: (a = t1_7.b) - Filter: (b = 0) - -> Nested Loop - -> HashAggregate - Group Key: t1_8.b + -> Index Scan using iprt1_p2_a on prt1_p2 t1_4 + Index Cond: (a = t1_7.b) + Filter: (b = 0) + -> Nested Loop + -> Unique + -> Sort + Sort Key: t1_8.b -> Hash Semi Join Hash Cond: (t1_8.b = ((t1_11.a + t1_11.b) / 2)) -> Seq Scan on prt2_p3 t1_8 -> Hash -> Seq Scan on prt1_e_p3 t1_11 Filter: (c = 0) - -> Index Scan using iprt1_p3_a on prt1_p3 t1_5 - Index Cond: (a = t1_8.b) - Filter: (b = 0) -(39 rows) + -> Index Scan using iprt1_p3_a on prt1_p3 t1_5 + Index Cond: (a = t1_8.b) + Filter: (b = 0) +(41 rows) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; a | b | c @@ -1240,11 +1248,12 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN ( 450 | 0 | 0450 (4 rows) --- test merge joins +-- test merge joins, slightly modifying the query to ensure that we still +-- get a fully partitionwise join SET enable_hashjoin TO off; SET enable_nestloop TO off; EXPLAIN (COSTS OFF) -SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; +SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) ORDER BY t1.a; QUERY PLAN ------------------------------------------------------------------ Merge Append @@ -1254,7 +1263,6 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN ( -> Sort Sort Key: t1_3.a -> Seq Scan on prt1_p1 t1_3 - Filter: (b = 0) -> Merge Semi Join Merge Cond: (t1_6.b = (((t1_9.a + t1_9.b) / 2))) -> Sort @@ -1269,7 +1277,6 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN ( -> Sort Sort Key: t1_4.a -> Seq Scan on prt1_p2 t1_4 - Filter: (b = 0) -> Merge Semi Join Merge Cond: (t1_7.b = (((t1_10.a + t1_10.b) / 2))) -> Sort @@ -1284,7 +1291,6 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN ( -> Sort Sort Key: t1_5.a -> Seq Scan on prt1_p3 t1_5 - Filter: (b = 0) -> Merge Semi Join Merge Cond: (t1_8.b = (((t1_11.a + t1_11.b) / 2))) -> Sort @@ -1294,9 +1300,9 @@ SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN ( Sort Key: (((t1_11.a + t1_11.b) / 2)) -> Seq Scan on prt1_e_p3 t1_11 Filter: (c = 0) -(47 rows) +(44 rows) -SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; +SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) ORDER BY t1.a; a | b | c -----+---+------ 0 | 0 | 0000 @@ -1605,26 +1611,28 @@ SELECT avg(t1.a), avg(t2.b), avg(t3.a + t3.b), t1.c, t2.c, t3.c FROM plt1 t1, pl -- joins where one of the relations is proven empty EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a = 1 AND t1.a = 2; - QUERY PLAN --------------------------- + QUERY PLAN +---------------------------- Result + Replaces: Join on t1, t2 One-Time Filter: false -(2 rows) +(3 rows) EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 LEFT JOIN prt2 t2 ON t1.a = t2.b; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Join on t2, prt1 One-Time Filter: false -(2 rows) +(3 rows) EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2 t2 ON t1.a = t2.b, prt1 t3 WHERE t2.b = t3.a; QUERY PLAN -------------------------------------------------- Hash Left Join - Hash Cond: (t2.b = a) + Hash Cond: (t2.b = prt1.a) -> Append -> Hash Join Hash Cond: (t3_1.a = t2_1.b) @@ -1643,17 +1651,18 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 -> Seq Scan on prt2_p3 t2_3 -> Hash -> Result + Replaces: Scan on prt1 One-Time Filter: false -(21 rows) +(22 rows) EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 FULL JOIN prt2 t2 ON t1.a = t2.b WHERE t2.a = 0 ORDER BY t1.a, t2.b; QUERY PLAN -------------------------------------------- Sort - Sort Key: a, t2.b + Sort Key: prt1.a, t2.b -> Hash Left Join - Hash Cond: (t2.b = a) + Hash Cond: (t2.b = prt1.a) -> Append -> Seq Scan on prt2_p1 t2_1 Filter: (a = 0) @@ -1663,8 +1672,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a = 1 AND a = 2) t1 Filter: (a = 0) -> Hash -> Result + Replaces: Scan on prt1 One-Time Filter: false -(14 rows) +(15 rows) -- -- tests for hash partitioned tables. @@ -2238,10 +2248,10 @@ SELECT COUNT(*) FROM prt1_l t1 LEFT JOIN LATERAL -- join with one side empty EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.b = t2.a AND t1.c = t2.c; - QUERY PLAN -------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------------------- Hash Left Join - Hash Cond: ((t2.b = a) AND (t2.a = b) AND ((t2.c)::text = (c)::text)) + Hash Cond: ((t2.b = prt1_l.a) AND (t2.a = prt1_l.b) AND ((t2.c)::text = (prt1_l.c)::text)) -> Append -> Seq Scan on prt2_l_p1 t2_1 -> Seq Scan on prt2_l_p2_p1 t2_2 @@ -2250,8 +2260,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE a = 1 AND a = 2) -> Seq Scan on prt2_l_p3_p2 t2_5 -> Hash -> Result + Replaces: Scan on prt1_l One-Time Filter: false -(11 rows) +(12 rows) -- Test case to verify proper handling of subqueries in a partitioned delete. -- The weird-looking lateral join is just there to force creation of a @@ -4913,27 +4924,27 @@ ANALYZE plt3_adv; -- merged partition when re-called with plt1_adv_p1 for the second list value -- '0001' of that partition EXPLAIN (COSTS OFF) -SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; +SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.a = t3.a AND t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; QUERY PLAN ----------------------------------------------------------------------------------------------- Sort Sort Key: t1.c, t1.a, t2.a, t3.a -> Append -> Hash Full Join - Hash Cond: (t1_1.c = t3_1.c) + Hash Cond: ((t1_1.a = t3_1.a) AND (t1_1.c = t3_1.c)) Filter: (((COALESCE(t1_1.a, 0) % 5) <> 3) AND ((COALESCE(t1_1.a, 0) % 5) <> 4)) -> Hash Left Join - Hash Cond: (t1_1.c = t2_1.c) + Hash Cond: ((t1_1.a = t2_1.a) AND (t1_1.c = t2_1.c)) -> Seq Scan on plt1_adv_p1 t1_1 -> Hash -> Seq Scan on plt2_adv_p1 t2_1 -> Hash -> Seq Scan on plt3_adv_p1 t3_1 -> Hash Full Join - Hash Cond: (t1_2.c = t3_2.c) + Hash Cond: ((t1_2.a = t3_2.a) AND (t1_2.c = t3_2.c)) Filter: (((COALESCE(t1_2.a, 0) % 5) <> 3) AND ((COALESCE(t1_2.a, 0) % 5) <> 4)) -> Hash Left Join - Hash Cond: (t1_2.c = t2_2.c) + Hash Cond: ((t1_2.a = t2_2.a) AND (t1_2.c = t2_2.c)) -> Seq Scan on plt1_adv_p2 t1_2 -> Hash -> Seq Scan on plt2_adv_p2 t2_2 @@ -4941,7 +4952,7 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t -> Seq Scan on plt3_adv_p2 t3_2 (23 rows) -SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; +SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.a = t3.a AND t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; a | c | a | c | a | c ----+------+----+------+----+------ 0 | 0000 | | | | @@ -4950,56 +4961,16 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t 15 | 0000 | | | | 20 | 0000 | | | | 1 | 0001 | | | 1 | 0001 - 1 | 0001 | | | 6 | 0001 - 1 | 0001 | | | 11 | 0001 - 1 | 0001 | | | 16 | 0001 - 1 | 0001 | | | 21 | 0001 - 6 | 0001 | | | 1 | 0001 6 | 0001 | | | 6 | 0001 - 6 | 0001 | | | 11 | 0001 - 6 | 0001 | | | 16 | 0001 - 6 | 0001 | | | 21 | 0001 - 11 | 0001 | | | 1 | 0001 - 11 | 0001 | | | 6 | 0001 11 | 0001 | | | 11 | 0001 - 11 | 0001 | | | 16 | 0001 - 11 | 0001 | | | 21 | 0001 - 16 | 0001 | | | 1 | 0001 - 16 | 0001 | | | 6 | 0001 - 16 | 0001 | | | 11 | 0001 16 | 0001 | | | 16 | 0001 - 16 | 0001 | | | 21 | 0001 - 21 | 0001 | | | 1 | 0001 - 21 | 0001 | | | 6 | 0001 - 21 | 0001 | | | 11 | 0001 - 21 | 0001 | | | 16 | 0001 21 | 0001 | | | 21 | 0001 2 | 0002 | 2 | 0002 | | - 2 | 0002 | 7 | 0002 | | - 2 | 0002 | 12 | 0002 | | - 2 | 0002 | 17 | 0002 | | - 2 | 0002 | 22 | 0002 | | - 7 | 0002 | 2 | 0002 | | 7 | 0002 | 7 | 0002 | | - 7 | 0002 | 12 | 0002 | | - 7 | 0002 | 17 | 0002 | | - 7 | 0002 | 22 | 0002 | | - 12 | 0002 | 2 | 0002 | | - 12 | 0002 | 7 | 0002 | | 12 | 0002 | 12 | 0002 | | - 12 | 0002 | 17 | 0002 | | - 12 | 0002 | 22 | 0002 | | - 17 | 0002 | 2 | 0002 | | - 17 | 0002 | 7 | 0002 | | - 17 | 0002 | 12 | 0002 | | 17 | 0002 | 17 | 0002 | | - 17 | 0002 | 22 | 0002 | | - 22 | 0002 | 2 | 0002 | | - 22 | 0002 | 7 | 0002 | | - 22 | 0002 | 12 | 0002 | | - 22 | 0002 | 17 | 0002 | | 22 | 0002 | 22 | 0002 | | -(55 rows) +(15 rows) DROP TABLE plt1_adv; DROP TABLE plt2_adv; @@ -5224,8 +5195,10 @@ CREATE TABLE fract_t1 PARTITION OF fract_t FOR VALUES FROM ('1000') TO ('2000'); INSERT INTO fract_t (id) (SELECT generate_series(0, 1999)); ANALYZE fract_t; -- verify plan; nested index only scans +-- (avoid merge joins, because the costs of partitionwise and non-partitionwise +-- merge joins tend to be almost equal, and we want this test to be stable) SET max_parallel_workers_per_gather = 0; -SET enable_partitionwise_join = on; +SET enable_mergejoin = off; EXPLAIN (COSTS OFF) SELECT x.id, y.id FROM fract_t x LEFT JOIN fract_t y USING (id) ORDER BY x.id ASC LIMIT 10; QUERY PLAN @@ -5233,14 +5206,14 @@ SELECT x.id, y.id FROM fract_t x LEFT JOIN fract_t y USING (id) ORDER BY x.id AS Limit -> Merge Append Sort Key: x.id - -> Merge Left Join - Merge Cond: (x_1.id = y_1.id) + -> Nested Loop Left Join -> Index Only Scan using fract_t0_pkey on fract_t0 x_1 -> Index Only Scan using fract_t0_pkey on fract_t0 y_1 - -> Merge Left Join - Merge Cond: (x_2.id = y_2.id) + Index Cond: (id = x_1.id) + -> Nested Loop Left Join -> Index Only Scan using fract_t1_pkey on fract_t1 x_2 -> Index Only Scan using fract_t1_pkey on fract_t1 y_2 + Index Cond: (id = x_2.id) (11 rows) EXPLAIN (COSTS OFF) @@ -5357,6 +5330,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM pht1 p1 JOIN pht1 p2 USING (c) LIMIT 1000; -> Seq Scan on pht1_p3 p2_3 (17 rows) +RESET enable_mergejoin; SET max_parallel_workers_per_gather = 1; SET debug_parallel_query = on; -- Partial paths should also be smart enough to employ limits diff --git a/src/test/regress/expected/partition_merge.out b/src/test/regress/expected/partition_merge.out new file mode 100644 index 0000000000000..883110e25d9fc --- /dev/null +++ b/src/test/regress/expected/partition_merge.out @@ -0,0 +1,1097 @@ +-- +-- PARTITIONS_MERGE +-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command +-- +CREATE SCHEMA partitions_merge_schema; +CREATE SCHEMA partitions_merge_schema2; +SET search_path = partitions_merge_schema, public; +-- +-- BY RANGE partitioning +-- +-- +-- Test for error codes +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_dec2021 PARTITION OF sales_range FOR VALUES FROM ('2021-12-01') TO ('2021-12-31'); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_apr_1 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-15'); +CREATE TABLE sales_apr_2 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-15') TO ('2022-05-01'); +ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022; +ERROR: partition with name "sales_feb2022" is already used +LINE 1: ...e MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2... + ^ +-- ERROR: "sales_apr2022" is not a table +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022; +ERROR: "sales_apr2022" is not a table +HINT: ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions. +-- ERROR: can not merge partition "sales_mar2022" together with partition "sales_jan2022" +-- DETAIL: lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022" +-- (space between sections sales_jan2022 and sales_mar2022) +ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022; +ERROR: can not merge partition "sales_mar2022" together with partition "sales_jan2022" +DETAIL: lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022" +HINT: ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent. +-- ERROR: can not merge partition "sales_jan2022" together with partition "sales_dec2021" +-- DETAIL: lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021" +-- (space between sections sales_dec2021 and sales_jan2022) +ALTER TABLE sales_range MERGE PARTITIONS (sales_dec2021, sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022; +ERROR: can not merge partition "sales_jan2022" together with partition "sales_dec2021" +DETAIL: lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021" +HINT: ALTER TABLE ... MERGE PARTITIONS requires the partition bounds to be adjacent. +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions_merge_schema.sales_feb2022) INTO sales_feb_mar_apr2022; +ERROR: partition with name "sales_feb2022" is already used +LINE 1: ...e MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions... + ^ +--ERROR, sales_apr_2 already exists +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_apr_2; +ERROR: relation "sales_apr_2" already exists +CREATE VIEW jan2022v as SELECT * FROM sales_jan2022; +ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022; +ERROR: cannot drop table sales_jan2022 because other objects depend on it +DETAIL: view jan2022v depends on table sales_jan2022 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +DROP VIEW jan2022v; +-- NO ERROR: test for custom partitions order, source partitions not in the search_path +SET search_path = partitions_merge_schema2, public; +ALTER TABLE partitions_merge_schema.sales_range MERGE PARTITIONS ( + partitions_merge_schema.sales_feb2022, + partitions_merge_schema.sales_mar2022, + partitions_merge_schema.sales_jan2022) INTO sales_jan_feb_mar2022; +SET search_path = partitions_merge_schema, public; +PREPARE get_partition_info(regclass[]) AS +SELECT c.oid::pg_catalog.regclass, + c.relpersistence, + c.relkind, + i.inhdetachpending, + pg_catalog.pg_get_expr(c.relpartbound, c.oid) +FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i +WHERE c.oid = i.inhrelid AND i.inhparent = ANY($1) +ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', + c.oid::regclass::text COLLATE "C"; +EXECUTE get_partition_info('{sales_range}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +------------------------------------------------+----------------+---------+------------------+-------------------------------------------------- + partitions_merge_schema2.sales_jan_feb_mar2022 | p | r | f | FOR VALUES FROM ('01-01-2022') TO ('04-01-2022') + sales_apr2022 | p | p | f | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022') + sales_dec2021 | p | r | f | FOR VALUES FROM ('12-01-2021') TO ('12-31-2021') + sales_others | p | r | f | DEFAULT +(4 rows) + +DROP TABLE sales_range; +-- +-- Add rows into partitioned table, then merge partitions +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +SELECT pg_catalog.pg_get_partkeydef('sales_range'::regclass); + pg_get_partkeydef +-------------------- + RANGE (sales_date) +(1 row) + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_range}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +---------------+----------------+---------+------------------+-------------------------------------------------- + sales_apr2022 | p | r | f | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022') + sales_feb2022 | p | r | f | FOR VALUES FROM ('02-01-2022') TO ('03-01-2022') + sales_jan2022 | p | r | f | FOR VALUES FROM ('01-01-2022') TO ('02-01-2022') + sales_mar2022 | p | r | f | FOR VALUES FROM ('03-01-2022') TO ('04-01-2022') + sales_others | p | r | f | DEFAULT +(5 rows) + +-- check schema-qualified name of the new partition +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO partitions_merge_schema2.sales_feb_mar_apr2022; +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_range}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +------------------------------------------------+----------------+---------+------------------+-------------------------------------------------- + partitions_merge_schema2.sales_feb_mar_apr2022 | p | r | f | FOR VALUES FROM ('02-01-2022') TO ('05-01-2022') + sales_jan2022 | p | r | f | FOR VALUES FROM ('01-01-2022') TO ('02-01-2022') + sales_others | p | r | f | DEFAULT +(3 rows) + +SELECT * FROM pg_indexes WHERE tablename = 'sales_feb_mar_apr2022' and schemaname = 'partitions_merge_schema2'; + schemaname | tablename | indexname | tablespace | indexdef +--------------------------+-----------------------+--------------------------------------+------------+------------------------------------------------------------------------------------------------------------------------------ + partitions_merge_schema2 | sales_feb_mar_apr2022 | sales_feb_mar_apr2022_sales_date_idx | | CREATE INDEX sales_feb_mar_apr2022_sales_date_idx ON partitions_merge_schema2.sales_feb_mar_apr2022 USING btree (sales_date) +(1 row) + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +------------------------------------------------+----------------+------------------+--------------+------------ + partitions_merge_schema2.sales_feb_mar_apr2022 | 2 | Smirnoff | 500 | 02-10-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 3 | Ford | 2000 | 04-30-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 4 | Ivanov | 750 | 04-13-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 5 | Deev | 250 | 04-07-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 6 | Poirot | 150 | 02-11-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 7 | Li | 175 | 03-08-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 8 | Ericsson | 185 | 02-23-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 9 | Muller | 250 | 03-11-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 11 | Trump | 380 | 04-06-2022 + partitions_merge_schema2.sales_feb_mar_apr2022 | 12 | Plato | 350 | 03-19-2022 + sales_jan2022 | 1 | May | 1000 | 01-31-2022 + sales_jan2022 | 10 | Halder | 350 | 01-28-2022 + sales_jan2022 | 13 | Gandi | 377 | 01-09-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +-- Use indexscan for testing indexes +SET enable_seqscan = OFF; +EXPLAIN (COSTS OFF) SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022 where sales_date > '2022-01-01'; + QUERY PLAN +-------------------------------------------------------------------------------- + Index Scan using sales_feb_mar_apr2022_sales_date_idx on sales_feb_mar_apr2022 + Index Cond: (sales_date > '01-01-2022'::date) +(2 rows) + +SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022 where sales_date > '2022-01-01'; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 2 | Smirnoff | 500 | 02-10-2022 + 6 | Poirot | 150 | 02-11-2022 + 8 | Ericsson | 185 | 02-23-2022 + 7 | Li | 175 | 03-08-2022 + 9 | Muller | 250 | 03-11-2022 + 12 | Plato | 350 | 03-19-2022 + 11 | Trump | 380 | 04-06-2022 + 5 | Deev | 250 | 04-07-2022 + 4 | Ivanov | 750 | 04-13-2022 + 3 | Ford | 2000 | 04-30-2022 +(10 rows) + +RESET enable_seqscan; +DROP TABLE sales_range; +-- +-- Merge some partitions into DEFAULT partition +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +-- Merge partitions (include DEFAULT partition) into partition with the same +-- name +ALTER TABLE sales_range MERGE PARTITIONS + (sales_jan2022, sales_mar2022, partitions_merge_schema.sales_others) INTO sales_others; +SELECT * FROM sales_others ORDER BY salesperson_id; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 1 | May | 1000 | 01-31-2022 + 7 | Li | 175 | 03-08-2022 + 9 | Muller | 250 | 03-11-2022 + 10 | Halder | 350 | 01-28-2022 + 12 | Plato | 350 | 03-19-2022 + 13 | Gandi | 377 | 01-09-2022 + 14 | Smith | 510 | 05-04-2022 +(7 rows) + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_range}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +---------------+----------------+---------+------------------+-------------------------------------------------- + sales_apr2022 | p | r | f | FOR VALUES FROM ('04-01-2022') TO ('05-01-2022') + sales_feb2022 | p | r | f | FOR VALUES FROM ('02-01-2022') TO ('03-01-2022') + sales_others | p | r | f | DEFAULT +(3 rows) + +DROP TABLE sales_range; +-- +-- Test for: +-- * composite partition key; +-- * GENERATED column; +-- * column with DEFAULT value. +-- +CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT, + sales_date VARCHAR(10) GENERATED ALWAYS AS + (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED, + sales_department VARCHAR(30) DEFAULT 'Sales department') + PARTITION BY RANGE (sales_year, sales_month, sales_day); +CREATE TABLE sales_dec2022 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1); +CREATE TABLE sales_jan2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1); +CREATE TABLE sales_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1); +CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE); +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2021, 12, 7), + ('Manager2', 2021, 12, 8), + ('Manager3', 2022, 1, 1), + ('Manager1', 2022, 2, 4), + ('Manager2', 2022, 1, 2), + ('Manager3', 2022, 2, 1), + ('Manager1', 2022, 3, 3), + ('Manager2', 2022, 3, 4), + ('Manager3', 2022, 5, 1); +SELECT tableoid::regclass, * FROM sales_date; + tableoid | salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department +---------------+------------------+------------+-------------+-----------+------------+------------------ + sales_dec2022 | Manager1 | 2021 | 12 | 7 | 2021.12.07 | Sales department + sales_dec2022 | Manager2 | 2021 | 12 | 8 | 2021.12.08 | Sales department + sales_jan2022 | Manager3 | 2022 | 1 | 1 | 2022.01.01 | Sales department + sales_jan2022 | Manager2 | 2022 | 1 | 2 | 2022.01.02 | Sales department + sales_feb2022 | Manager1 | 2022 | 2 | 4 | 2022.02.04 | Sales department + sales_feb2022 | Manager3 | 2022 | 2 | 1 | 2022.02.01 | Sales department + sales_other | Manager1 | 2022 | 3 | 3 | 2022.03.03 | Sales department + sales_other | Manager2 | 2022 | 3 | 4 | 2022.03.04 | Sales department + sales_other | Manager3 | 2022 | 5 | 1 | 2022.05.01 | Sales department +(9 rows) + +ALTER TABLE sales_date MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_jan_feb2022; +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2022, 1, 10), + ('Manager2', 2022, 2, 10); +SELECT tableoid::regclass, * FROM sales_date; + tableoid | salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department +-------------------+------------------+------------+-------------+-----------+------------+------------------ + sales_dec2022 | Manager1 | 2021 | 12 | 7 | 2021.12.07 | Sales department + sales_dec2022 | Manager2 | 2021 | 12 | 8 | 2021.12.08 | Sales department + sales_jan_feb2022 | Manager3 | 2022 | 1 | 1 | 2022.01.01 | Sales department + sales_jan_feb2022 | Manager2 | 2022 | 1 | 2 | 2022.01.02 | Sales department + sales_jan_feb2022 | Manager1 | 2022 | 2 | 4 | 2022.02.04 | Sales department + sales_jan_feb2022 | Manager3 | 2022 | 2 | 1 | 2022.02.01 | Sales department + sales_jan_feb2022 | Manager1 | 2022 | 1 | 10 | 2022.01.10 | Sales department + sales_jan_feb2022 | Manager2 | 2022 | 2 | 10 | 2022.02.10 | Sales department + sales_other | Manager1 | 2022 | 3 | 3 | 2022.03.03 | Sales department + sales_other | Manager2 | 2022 | 3 | 4 | 2022.03.04 | Sales department + sales_other | Manager3 | 2022 | 5 | 1 | 2022.05.01 | Sales department +(11 rows) + +DROP TABLE sales_date; +-- +-- Test: merge partitions of partitioned table with triggers +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +CREATE TABLE salespeople10_20 PARTITION OF salespeople FOR VALUES FROM (10) TO (20); +CREATE TABLE salespeople20_30 PARTITION OF salespeople FOR VALUES FROM (20) TO (30); +CREATE TABLE salespeople30_40 PARTITION OF salespeople FOR VALUES FROM (30) TO (40); +INSERT INTO salespeople VALUES (1, 'Poirot'); +CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN NULL; +END; +$BODY$; +CREATE TRIGGER salespeople_after_insert_statement_trigger + AFTER INSERT + ON salespeople + FOR EACH STATEMENT + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); +CREATE TRIGGER salespeople_after_insert_row_trigger + AFTER INSERT + ON salespeople + FOR EACH ROW + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (10, 'May'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT +-- 1 trigger should fire here (row): +INSERT INTO salespeople10_20 VALUES (19, 'Ivanov'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40; +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (20, 'Smirnoff'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT +-- 1 trigger should fire here (row): +INSERT INTO salespeople10_40 VALUES (30, 'Ford'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +SELECT * FROM salespeople01_10; + salesperson_id | salesperson_name +----------------+------------------ + 1 | Poirot +(1 row) + +SELECT * FROM salespeople10_40; + salesperson_id | salesperson_name +----------------+------------------ + 10 | May + 19 | Ivanov + 20 | Smirnoff + 30 | Ford +(4 rows) + +DROP TABLE salespeople; +DROP FUNCTION after_insert_row_trigger(); +-- +-- Test: merge partitions with deleted columns +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +-- Create partitions with some deleted columns: +CREATE TABLE salespeople10_20(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)); +CREATE TABLE salespeople20_30(salesperson_id INT PRIMARY KEY, d2 INT, salesperson_name VARCHAR(30)); +CREATE TABLE salespeople30_40(salesperson_id INT PRIMARY KEY, d3 DATE, salesperson_name VARCHAR(30)); +INSERT INTO salespeople10_20 VALUES ('dummy value 1', 19, 'Ivanov'); +INSERT INTO salespeople20_30 VALUES (20, 101, 'Smirnoff'); +INSERT INTO salespeople30_40 VALUES (31, now(), 'Popov'); +ALTER TABLE salespeople10_20 DROP COLUMN d1; +ALTER TABLE salespeople20_30 DROP COLUMN d2; +ALTER TABLE salespeople30_40 DROP COLUMN d3; +ALTER TABLE salespeople ATTACH PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20); +ALTER TABLE salespeople ATTACH PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30); +ALTER TABLE salespeople ATTACH PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40); +INSERT INTO salespeople VALUES + (1, 'Poirot'), + (10, 'May'), + (30, 'Ford'); +ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40; +select * from salespeople; + salesperson_id | salesperson_name +----------------+------------------ + 1 | Poirot + 19 | Ivanov + 10 | May + 20 | Smirnoff + 31 | Popov + 30 | Ford +(6 rows) + +select * from salespeople01_10; + salesperson_id | salesperson_name +----------------+------------------ + 1 | Poirot +(1 row) + +select * from salespeople10_40; + salesperson_id | salesperson_name +----------------+------------------ + 19 | Ivanov + 10 | May + 20 | Smirnoff + 31 | Popov + 30 | Ford +(5 rows) + +DROP TABLE salespeople; +-- +-- Test: merge sub-partitions +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_apr2022_01_10 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'); +CREATE TABLE sales_apr2022_10_20 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20'); +CREATE TABLE sales_apr2022_20_30 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01'); +ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------------+----------------+------------------+--------------+------------ + sales_apr2022_01_10 | 5 | Deev | 250 | 04-07-2022 + sales_apr2022_01_10 | 11 | Trump | 380 | 04-06-2022 + sales_apr2022_10_20 | 4 | Ivanov | 750 | 04-13-2022 + sales_apr2022_20_30 | 3 | Ford | 2000 | 04-30-2022 +(4 rows) + +ALTER TABLE sales_apr2022 MERGE PARTITIONS (sales_apr2022_01_10, sales_apr2022_10_20, sales_apr2022_20_30) INTO sales_apr_all; +SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------+----------------+------------------+--------------+------------ + sales_apr_all | 3 | Ford | 2000 | 04-30-2022 + sales_apr_all | 4 | Ivanov | 750 | 04-13-2022 + sales_apr_all | 5 | Deev | 250 | 04-07-2022 + sales_apr_all | 11 | Trump | 380 | 04-06-2022 +(4 rows) + +DROP TABLE sales_range; +-- +-- BY LIST partitioning +-- +-- +-- Test: specific errors for BY LIST partitioning +-- +CREATE TABLE sales_list +(salesperson_id INT GENERATED ALWAYS AS IDENTITY, + salesperson_name VARCHAR(30), + sales_state VARCHAR(20), + sales_amount INT, + sales_date DATE) +PARTITION BY LIST (sales_state); +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid'); +CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'); +CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; +CREATE TABLE sales_list2 (LIKE sales_list) PARTITION BY LIST (sales_state); +CREATE TABLE sales_nord2 PARTITION OF sales_list2 FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT; +CREATE TABLE sales_external (LIKE sales_list); +CREATE TABLE sales_external2 (vch VARCHAR(5)); +-- ERROR: "sales_external" is not a partition of partitioned table "sales_list" +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all; +ERROR: "sales_external" is not a partition of partitioned table "sales_list" +HINT: ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions. +-- ERROR: "sales_external2" is not a partition of partitioned table "sales_list" +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all; +ERROR: "sales_external2" is not a partition of partitioned table "sales_list" +HINT: ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions. +-- ERROR: relation "sales_nord2" is not a partition of relation "sales_list" +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_nord2, sales_east) INTO sales_all; +ERROR: relation "sales_nord2" is not a partition of relation "sales_list" +HINT: ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions. +DROP TABLE sales_external2; +DROP TABLE sales_external; +DROP TABLE sales_list2; +DROP TABLE sales_list; +-- +-- Test: BY LIST partitioning, MERGE PARTITIONS with data +-- +CREATE TABLE sales_list +(salesperson_id INT GENERATED ALWAYS AS IDENTITY, + salesperson_name VARCHAR(30), + sales_state VARCHAR(20), + sales_amount INT, + sales_date DATE) +PARTITION BY LIST (sales_state); +CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name); +CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state); +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid'); +CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'); +CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; +INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES + ('Trump', 'Beijing', 1000, '2022-03-01'), + ('Smirnoff', 'New York', 500, '2022-03-03'), + ('Ford', 'St. Petersburg', 2000, '2022-03-05'), + ('Ivanov', 'Warsaw', 750, '2022-03-04'), + ('Deev', 'Lisbon', 250, '2022-03-07'), + ('Poirot', 'Berlin', 1000, '2022-03-01'), + ('May', 'Helsinki', 1200, '2022-03-06'), + ('Li', 'Vladivostok', 1150, '2022-03-09'), + ('May', 'Helsinki', 1200, '2022-03-11'), + ('Halder', 'Oslo', 800, '2022-03-02'), + ('Muller', 'Madrid', 650, '2022-03-05'), + ('Smith', 'Kyiv', 350, '2022-03-10'), + ('Gandi', 'Warsaw', 150, '2022-03-08'), + ('Plato', 'Lisbon', 950, '2022-03-05'); +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_list}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +---------------+----------------+---------+------------------+------------------------------------------------------ + sales_central | p | r | f | FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv') + sales_east | p | r | f | FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok') + sales_nord | p | r | f | FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki') + sales_west | p | r | f | FOR VALUES IN ('Lisbon', 'New York', 'Madrid') + sales_others | p | r | f | DEFAULT +(5 rows) + +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central) INTO sales_all; +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_list}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +--------------+----------------+---------+------------------+--------------------------------------------------------------------------------------------------------------- + sales_all | p | r | f | FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Beijing', 'Delhi', 'Vladivostok', 'Warsaw', 'Berlin', 'Kyiv') + sales_nord | p | r | f | FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki') + sales_others | p | r | f | DEFAULT +(3 rows) + +SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +------------+----------------+------------------+----------------+--------------+------------ + sales_all | 1 | Trump | Beijing | 1000 | 03-01-2022 + sales_all | 2 | Smirnoff | New York | 500 | 03-03-2022 + sales_all | 4 | Ivanov | Warsaw | 750 | 03-04-2022 + sales_all | 5 | Deev | Lisbon | 250 | 03-07-2022 + sales_all | 6 | Poirot | Berlin | 1000 | 03-01-2022 + sales_all | 8 | Li | Vladivostok | 1150 | 03-09-2022 + sales_all | 11 | Muller | Madrid | 650 | 03-05-2022 + sales_all | 12 | Smith | Kyiv | 350 | 03-10-2022 + sales_all | 13 | Gandi | Warsaw | 150 | 03-08-2022 + sales_all | 14 | Plato | Lisbon | 950 | 03-05-2022 + sales_nord | 3 | Ford | St. Petersburg | 2000 | 03-05-2022 + sales_nord | 7 | May | Helsinki | 1200 | 03-06-2022 + sales_nord | 9 | May | Helsinki | 1200 | 03-11-2022 + sales_nord | 10 | Halder | Oslo | 800 | 03-02-2022 +(14 rows) + +-- Use indexscan for testing indexes after merging partitions +SET enable_seqscan = OFF; +EXPLAIN (COSTS OFF) SELECT * FROM sales_all WHERE sales_state = 'Warsaw'; + QUERY PLAN +--------------------------------------------------------- + Index Scan using sales_all_sales_state_idx on sales_all + Index Cond: ((sales_state)::text = 'Warsaw'::text) +(2 rows) + +SELECT * FROM sales_all WHERE sales_state = 'Warsaw'; + salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +----------------+------------------+-------------+--------------+------------ + 4 | Ivanov | Warsaw | 750 | 03-04-2022 + 13 | Gandi | Warsaw | 150 | 03-08-2022 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; + QUERY PLAN +-------------------------------------------------------------------- + Index Scan using sales_all_sales_state_idx on sales_all sales_list + Index Cond: ((sales_state)::text = 'Warsaw'::text) +(2 rows) + +SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; + salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +----------------+------------------+-------------+--------------+------------ + 4 | Ivanov | Warsaw | 750 | 03-04-2022 + 13 | Gandi | Warsaw | 150 | 03-08-2022 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; + QUERY PLAN +--------------------------------------------------------------------------------- + Append + -> Index Scan using sales_all_salesperson_name_idx on sales_all sales_list_1 + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Heap Scan on sales_nord sales_list_2 + Recheck Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Index Scan on sales_nord_salesperson_name_idx + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Heap Scan on sales_others sales_list_3 + Recheck Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Index Scan on sales_others_salesperson_name_idx + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) +(11 rows) + +SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; + salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +----------------+------------------+-------------+--------------+------------ + 4 | Ivanov | Warsaw | 750 | 03-04-2022 +(1 row) + +RESET enable_seqscan; +DROP TABLE sales_list; +-- +-- Try to MERGE partitions of another table. +-- +CREATE TABLE t1 (i int, a int, b int, c int) PARTITION BY RANGE (a, b); +CREATE TABLE t1p1 PARTITION OF t1 FOR VALUES FROM (1, 1) TO (1, 2); +CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t); +CREATE TABLE t2pa PARTITION OF t2 FOR VALUES FROM ('A') TO ('C'); +CREATE TABLE t3 (i int, t text); +-- ERROR: relation "t1p1" is not a partition of relation "t2" +ALTER TABLE t2 MERGE PARTITIONS (t1p1, t2pa) INTO t2p; +ERROR: relation "t1p1" is not a partition of relation "t2" +HINT: ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions. +-- ERROR: "t3" is not a partition of partitioned table "t2" +ALTER TABLE t2 MERGE PARTITIONS (t2pa, t3) INTO t2p; +ERROR: "t3" is not a partition of partitioned table "t2" +HINT: ALTER TABLE ... MERGE PARTITIONS can only merge partitions don't have sub-partitions. +DROP TABLE t3; +DROP TABLE t2; +DROP TABLE t1; +-- +-- Check the partition index name if the partition name is the same as one +-- of the merged partitions. +-- +CREATE TABLE t (i int, PRIMARY KEY(i)) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +CREATE INDEX tidx ON t(i); +ALTER TABLE t MERGE PARTITIONS (tp_1_2, tp_0_1) INTO tp_1_2; +-- Indexname values should be 'tp_1_2_pkey' and 'tp_1_2_i_idx'. +\d+ tp_1_2 + Table "partitions_merge_schema.tp_1_2" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + i | integer | | not null | | plain | | +Partition of: t FOR VALUES FROM (0) TO (2) +Partition constraint: ((i IS NOT NULL) AND (i >= 0) AND (i < 2)) +Indexes: + "tp_1_2_pkey" PRIMARY KEY, btree (i) + "tp_1_2_i_idx" btree (i) +Not-null constraints: + "t_i_not_null" NOT NULL "i" (inherited) + +DROP TABLE t; +-- +-- Try to MERGE partitions of temporary table. +-- +BEGIN; +SHOW search_path; + search_path +--------------------------------- + partitions_merge_schema, public +(1 row) + +CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i) ON COMMIT DROP; +CREATE TEMP TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TEMP TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +CREATE TEMP TABLE tp_2_3 PARTITION OF t FOR VALUES FROM (2) TO (3); +CREATE TEMP TABLE tp_3_4 PARTITION OF t FOR VALUES FROM (3) TO (4); +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO pg_temp.tp_0_2; +ALTER TABLE t MERGE PARTITIONS (tp_0_2, tp_2_3) INTO pg_temp.tp_0_3; +-- Partition should be temporary. +EXECUTE get_partition_info('{t}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +--------+----------------+---------+------------------+---------------------------- + tp_0_3 | t | r | f | FOR VALUES FROM (0) TO (3) + tp_3_4 | t | r | f | FOR VALUES FROM (3) TO (4) +(2 rows) + +-- ERROR: cannot create a permanent relation as partition of temporary relation "t" +ALTER TABLE t MERGE PARTITIONS (tp_0_3, tp_3_4) INTO tp_0_4; +ERROR: cannot create a permanent relation as partition of temporary relation "t" +ROLLBACK; +-- +-- Try mixing permanent and temporary partitions. +-- +BEGIN; +SET search_path = partitions_merge_schema, pg_temp, public; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass; + oid | relpersistence +-----+---------------- + t | p +(1 row) + +EXECUTE get_partition_info('{t}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +--------+----------------+---------+------------------+---------------------------- + tp_0_1 | p | r | f | FOR VALUES FROM (0) TO (1) + tp_1_2 | p | r | f | FOR VALUES FROM (1) TO (2) +(2 rows) + +SAVEPOINT s; +SET search_path = pg_temp, partitions_merge_schema, public; +-- Can't merge persistent partitions into a temporary partition +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ERROR: cannot create a temporary relation as partition of permanent relation "t" +ROLLBACK TO SAVEPOINT s; +SET search_path = partitions_merge_schema, public; +-- Can't merge persistent partitions into a temporary partition +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO pg_temp.tp_0_2; +ERROR: cannot create a temporary relation as partition of permanent relation "t" +ROLLBACK; +BEGIN; +SET search_path = pg_temp, partitions_merge_schema, public; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass; + oid | relpersistence +-----+---------------- + t | t +(1 row) + +EXECUTE get_partition_info('{t}'); + oid | relpersistence | relkind | inhdetachpending | pg_get_expr +--------+----------------+---------+------------------+---------------------------- + tp_0_1 | t | r | f | FOR VALUES FROM (0) TO (1) + tp_1_2 | t | r | f | FOR VALUES FROM (1) TO (2) +(2 rows) + +SET search_path = partitions_merge_schema, pg_temp, public; +-- Can't merge temporary partitions into a persistent partition +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ERROR: cannot create a permanent relation as partition of temporary relation "t" +ROLLBACK; +DEALLOCATE get_partition_info; +-- Check the new partition inherits parent's tablespace +SET search_path = partitions_merge_schema, public; +CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace) + PARTITION BY RANGE (i) TABLESPACE regress_tblspace; +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +SELECT tablename, tablespace FROM pg_tables + WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema' + ORDER BY tablename COLLATE "C", tablespace COLLATE "C"; + tablename | tablespace +-----------+------------------ + t | regress_tblspace + tp_0_2 | regress_tblspace +(2 rows) + +SELECT tablename, indexname, tablespace FROM pg_indexes + WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema' + ORDER BY tablename COLLATE "C", indexname COLLATE "C", tablespace COLLATE "C"; + tablename | indexname | tablespace +-----------+-------------+------------------ + t | t_pkey | regress_tblspace + tp_0_2 | tp_0_2_pkey | regress_tblspace +(2 rows) + +DROP TABLE t; +-- Check the new partition inherits parent's table access method +SET search_path = partitions_merge_schema, public; +CREATE ACCESS METHOD partitions_merge_heap TYPE TABLE HANDLER heap_tableam_handler; +CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partitions_merge_heap; +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +SELECT c.relname, a.amname +FROM pg_class c JOIN pg_am a ON c.relam = a.oid +WHERE c.oid IN ('t'::regclass, 'tp_0_2'::regclass) +ORDER BY c.relname COLLATE "C"; + relname | amname +---------+----------------------- + t | partitions_merge_heap + tp_0_2 | partitions_merge_heap +(2 rows) + +DROP TABLE t; +DROP ACCESS METHOD partitions_merge_heap; +-- Test permission checks. The user needs to own the parent table and all +-- the merging partitions to do the merge. +CREATE ROLE regress_partition_merge_alice; +CREATE ROLE regress_partition_merge_bob; +GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_alice; +GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_alice; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- ERROR: must be owner of table t +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ERROR: must be owner of table t +RESET SESSION AUTHORIZATION; +ALTER TABLE t OWNER TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- ERROR: must be owner of table tp_0_1 +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ERROR: must be owner of table tp_0_1 +RESET SESSION AUTHORIZATION; +ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- ERROR: must be owner of table tp_1_2 +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ERROR: must be owner of table tp_1_2 +RESET SESSION AUTHORIZATION; +ALTER TABLE tp_1_2 OWNER TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- Ok: +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +RESET SESSION AUTHORIZATION; +DROP TABLE t; +-- Test: we can't merge partitions with different owners +CREATE TABLE tp_0_1(i int); +ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_alice; +CREATE TABLE tp_1_2(i int); +ALTER TABLE tp_1_2 OWNER TO regress_partition_merge_bob; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +ALTER TABLE t ATTACH PARTITION tp_0_1 FOR VALUES FROM (0) TO (1); +ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2); +-- Owner is 'regress_partition_merge_alice': +\dt tp_0_1 + List of tables + Schema | Name | Type | Owner +-------------------------+--------+-------+------------------------------- + partitions_merge_schema | tp_0_1 | table | regress_partition_merge_alice +(1 row) + +-- Owner is 'regress_partition_merge_bob': +\dt tp_1_2 + List of tables + Schema | Name | Type | Owner +-------------------------+--------+-------+----------------------------- + partitions_merge_schema | tp_1_2 | table | regress_partition_merge_bob +(1 row) + +-- ERROR: partitions being merged have different owners +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ERROR: partitions being merged have different owners +DROP TABLE t; +REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_alice; +REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_bob; +DROP ROLE regress_partition_merge_alice; +DROP ROLE regress_partition_merge_bob; +-- Test for hash partitioned table +CREATE TABLE t (i int) PARTITION BY HASH(i); +CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1); +-- ERROR: partition of hash-partitioned table cannot be merged +ALTER TABLE t MERGE PARTITIONS (tp1, tp2) INTO tp3; +ERROR: partition of hash-partitioned table cannot be merged +-- ERROR: list of partitions to be merged should include at least two partitions +ALTER TABLE t MERGE PARTITIONS (tp1) INTO tp3; +ERROR: list of partitions to be merged should include at least two partitions +DROP TABLE t; +-- Test for merged partition properties: +-- * STATISTICS is empty +-- * COMMENT is empty +-- * DEFAULTS are the same as DEFAULTS for partitioned table +-- * STORAGE is the same as STORAGE for partitioned table +-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table +-- * TRIGGERS are the same as TRIGGERS for partitioned table +\set HIDE_TOAST_COMPRESSION false +CREATE TABLE t +(i int NOT NULL, + t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t', + b bigint, + d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i)); +COMMENT ON COLUMN t.i IS 't1.i'; +CREATE TABLE tp_0_1 +(i int NOT NULL, + t text STORAGE MAIN DEFAULT 'default_tp_0_1', + b bigint, + d date GENERATED ALWAYS as ('2022-02-02') STORED); +ALTER TABLE t ATTACH PARTITION tp_0_1 FOR VALUES FROM (0) TO (1); +COMMENT ON COLUMN tp_0_1.i IS 'tp_0_1.i'; +CREATE TABLE tp_1_2 +(i int NOT NULL, + t text STORAGE MAIN DEFAULT 'default_tp_1_2', + b bigint, + d date GENERATED ALWAYS as ('2022-03-03') STORED); +ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2); +COMMENT ON COLUMN tp_1_2.i IS 'tp_1_2.i'; +CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t; +CREATE STATISTICS tp_0_1_stat (DEPENDENCIES) on i, b from tp_0_1; +CREATE STATISTICS tp_1_2_stat (DEPENDENCIES) on i, b from tp_1_2; +ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0); +ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED; +ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID; +ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID; +INSERT INTO tp_0_1(i, t, b) VALUES(0, DEFAULT, 1); +INSERT INTO tp_1_2(i, t, b) VALUES(1, DEFAULT, 2); +CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS +$BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN new; +END; +$BODY$; +CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW + EXECUTE PROCEDURE trigger_function('t'); +CREATE TRIGGER tp_0_1_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW + EXECUTE PROCEDURE trigger_function('tp_0_1'); +CREATE TRIGGER tp_1_2_before_insert_row_trigger BEFORE INSERT ON tp_1_2 FOR EACH ROW + EXECUTE PROCEDURE trigger_function('tp_1_2'); +\d+ tp_0_1 + Table "partitions_merge_schema.tp_0_1" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+---------+-----------+----------+-------------------------------------------------+---------+-------------+--------------+------------- + i | integer | | not null | | plain | | | tp_0_1.i + t | text | | | 'default_tp_0_1'::text | main | | | + b | bigint | | not null | | plain | | | + d | date | | | generated always as ('02-02-2022'::date) stored | plain | | | +Partition of: t FOR VALUES FROM (0) TO (1) +Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 0) AND (abs(i) < 1)) +Check constraints: + "t_b_check" CHECK (b > 0) + "t_b_check1" CHECK (b > 0) NOT ENFORCED + "t_b_check2" CHECK (b > 0) NOT VALID +Statistics objects: + "partitions_merge_schema.tp_0_1_stat" (dependencies) ON i, b FROM tp_0_1 +Not-null constraints: + "tp_0_1_i_not_null" NOT NULL "i" (inherited) + "t_b_nn" NOT NULL "b" (inherited) NOT VALID +Triggers: + t_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t + tp_0_1_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW EXECUTE FUNCTION trigger_function('tp_0_1') + +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_1; +\d+ tp_0_1 + Table "partitions_merge_schema.tp_0_1" + Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description +--------+---------+-----------+----------+-------------------------------------------------+----------+-------------+--------------+------------- + i | integer | | not null | | plain | | | + t | text | | | 'default_t'::text | extended | pglz | | + b | bigint | | not null | | plain | | | + d | date | | | generated always as ('01-01-2022'::date) stored | plain | | | +Partition of: t FOR VALUES FROM (0) TO (2) +Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 0) AND (abs(i) < 2)) +Check constraints: + "t_b_check" CHECK (b > 0) + "t_b_check1" CHECK (b > 0) NOT ENFORCED + "t_b_check2" CHECK (b > 0) NOT VALID +Not-null constraints: + "t_i_not_null" NOT NULL "i" (inherited) + "t_b_nn" NOT NULL "b" (inherited) NOT VALID +Triggers: + t_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t + +INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3); +NOTICE: trigger(t) called: action = INSERT, when = BEFORE, level = ROW +SELECT tableoid::regclass, * FROM t ORDER BY b; + tableoid | i | t | b | d +----------+---+----------------+---+------------ + tp_0_1 | 0 | default_tp_0_1 | 1 | 01-01-2022 + tp_0_1 | 1 | default_tp_1_2 | 2 | 01-01-2022 + tp_0_1 | 1 | default_t | 3 | 01-01-2022 +(3 rows) + +DROP TABLE t; +DROP FUNCTION trigger_function(); +\set HIDE_TOAST_COMPRESSION true +-- Test MERGE PARTITIONS with not valid foreign key constraint +CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +INSERT INTO t VALUES (0), (1); +CREATE TABLE t_fk (i INT); +INSERT INTO t_fk VALUES (1), (2); +ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT VALID; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +-- Should be NOT VALID FOREIGN KEY +\d tp_0_2 + Table "partitions_merge_schema.tp_0_2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + i | integer | | not null | +Partition of: t FOR VALUES FROM (0) TO (2) +Indexes: + "tp_0_2_pkey" PRIMARY KEY, btree (i) +Referenced by: + TABLE "t_fk" CONSTRAINT "t_fk_i_fkey" FOREIGN KEY (i) REFERENCES t(i) NOT VALID + +-- ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +ALTER TABLE t_fk VALIDATE CONSTRAINT t_fk_i_fkey; +ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +DETAIL: Key (i)=(2) is not present in table "t". +DROP TABLE t_fk; +DROP TABLE t; +-- Test MERGE PARTITIONS with not enforced foreign key constraint +CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +INSERT INTO t VALUES (0), (1); +CREATE TABLE t_fk (i INT); +INSERT INTO t_fk VALUES (1), (2); +ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT ENFORCED; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +-- Should be NOT ENFORCED FOREIGN KEY +\d tp_0_2 + Table "partitions_merge_schema.tp_0_2" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + i | integer | | not null | +Partition of: t FOR VALUES FROM (0) TO (2) +Indexes: + "tp_0_2_pkey" PRIMARY KEY, btree (i) +Referenced by: + TABLE "t_fk" CONSTRAINT "t_fk_i_fkey" FOREIGN KEY (i) REFERENCES t(i) NOT ENFORCED + +-- ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +ALTER TABLE t_fk ALTER CONSTRAINT t_fk_i_fkey ENFORCED; +ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +DETAIL: Key (i)=(2) is not present in table "t". +DROP TABLE t_fk; +DROP TABLE t; +-- Test for recomputation of stored generated columns. +CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789); +INSERT INTO t VALUES (0), (1); +-- Should be 0 because partition identifier for row with i=0 is different from +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + count +------- + 0 +(1 row) + +-- "tab_id" column (stored generated column) with "tableoid" attribute requires +-- recomputation here. +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +-- Should be 1 because partition identifier for row with i=0 is the same as +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + count +------- + 1 +(1 row) + +DROP TABLE t; +-- Test for generated columns (different order of columns in partitioned table +-- and partitions). +CREATE TABLE t (i int, g int GENERATED ALWAYS AS (i + tableoid::int)) PARTITION BY RANGE (i); +CREATE TABLE tp_1 (g int GENERATED ALWAYS AS (i + tableoid::int), i int); +CREATE TABLE tp_2 (g int GENERATED ALWAYS AS (i + tableoid::int), i int); +ALTER TABLE t ATTACH PARTITION tp_1 FOR VALUES FROM (-1) TO (10); +ALTER TABLE t ATTACH PARTITION tp_2 FOR VALUES FROM (10) TO (20); +ALTER TABLE t ADD CHECK (g > 0); +ALTER TABLE t ADD CHECK (i > 0); +INSERT INTO t VALUES (5), (15); +ALTER TABLE t MERGE PARTITIONS (tp_1, tp_2) INTO tp_12; +INSERT INTO t VALUES (16); +-- ERROR: new row for relation "tp_12" violates check constraint "t_i_check" +INSERT INTO t VALUES (0); +ERROR: new row for relation "tp_12" violates check constraint "t_i_check" +DETAIL: Failing row contains (0, virtual). +-- Should be 3 rows: (5), (15), (16): +SELECT i FROM t ORDER BY i; + i +---- + 5 + 15 + 16 +(3 rows) + +-- Should be 1 because for the same tableoid (15 + tableoid) = (5 + tableoid) + 10: +SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5); + count +------- + 1 +(1 row) + +DROP TABLE t; +RESET search_path; +-- +DROP SCHEMA partitions_merge_schema; +DROP SCHEMA partitions_merge_schema2; diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out index d1966cd7d829f..849049f9c51a4 100644 --- a/src/test/regress/expected/partition_prune.out +++ b/src/test/regress/expected/partition_prune.out @@ -627,8 +627,9 @@ explain (costs off) select * from rlp3 where a = 20; /* empty */ QUERY PLAN -------------------------- Result + Replaces: Scan on rlp3 One-Time Filter: false -(2 rows) +(3 rows) -- redundant clauses are eliminated explain (costs off) select * from rlp where a > 1 and a = 10; /* only default */ @@ -670,8 +671,9 @@ explain (costs off) select * from rlp where a = 1 and a = 3; /* empty */ QUERY PLAN -------------------------- Result + Replaces: Scan on rlp One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from rlp where (a = 1 and a = 3) or (a > 1 and a = 15); QUERY PLAN @@ -1254,25 +1256,28 @@ select * from boolpart where a is not unknown; -- check that all partitions are pruned when faced with conflicting clauses explain (costs off) select * from boolpart where a is not unknown and a is unknown; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Scan on boolpart One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from boolpart where a is false and a is unknown; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Scan on boolpart One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from boolpart where a is true and a is unknown; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------ Result + Replaces: Scan on boolpart One-Time Filter: false -(2 rows) +(3 rows) -- inverse boolean partitioning - a seemingly unlikely design, but we've got -- code for it, so we'd better test it. @@ -1568,11 +1573,12 @@ explain (costs off) select * from coercepart where a = any ('{ab,null}'); (2 rows) explain (costs off) select * from coercepart where a = any (null::text[]); - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------- Result + Replaces: Scan on coercepart One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from coercepart where a = all ('{ab}'); QUERY PLAN @@ -1582,25 +1588,28 @@ explain (costs off) select * from coercepart where a = all ('{ab}'); (2 rows) explain (costs off) select * from coercepart where a = all ('{ab,bc}'); - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------- Result + Replaces: Scan on coercepart One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from coercepart where a = all ('{ab,null}'); - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------- Result + Replaces: Scan on coercepart One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from coercepart where a = all (null::text[]); - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------- Result + Replaces: Scan on coercepart One-Time Filter: false -(2 rows) +(3 rows) drop table coercepart; CREATE TABLE part (a INT, b INT) PARTITION BY LIST (a); @@ -1772,8 +1781,9 @@ explain (costs off) select * from lp where a <> 'a' and a is null; QUERY PLAN -------------------------- Result + Replaces: Scan on lp One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from lp where (a <> 'a' and a <> 'd') or a is null; QUERY PLAN @@ -1866,22 +1876,24 @@ create table lparted_by_int2 (a smallint) partition by list (a); create table lparted_by_int2_1 partition of lparted_by_int2 for values in (1); create table lparted_by_int2_16384 partition of lparted_by_int2 for values in (16384); explain (costs off) select * from lparted_by_int2 where a = 100_000_000_000_000; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------------- Result + Replaces: Scan on lparted_by_int2 One-Time Filter: false -(2 rows) +(3 rows) create table rparted_by_int2 (a smallint) partition by range (a); create table rparted_by_int2_1 partition of rparted_by_int2 for values from (1) to (10); create table rparted_by_int2_16384 partition of rparted_by_int2 for values from (10) to (16384); -- all partitions pruned explain (costs off) select * from rparted_by_int2 where a > 100_000_000_000_000; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------------- Result + Replaces: Scan on rparted_by_int2 One-Time Filter: false -(2 rows) +(3 rows) create table rparted_by_int2_maxvalue partition of rparted_by_int2 for values from (16384) to (maxvalue); -- all partitions but rparted_by_int2_maxvalue pruned @@ -1903,21 +1915,21 @@ select * from from int4_tbl touter) ss, asptab where asptab.id > ss.b::int; - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------- Nested Loop -> Seq Scan on int4_tbl touter -> Append -> Index Only Scan using asptab0_pkey on asptab0 asptab_1 - Index Cond: (id > (EXISTS(SubPlan 3))::integer) - SubPlan 4 + Index Cond: (id > (EXISTS(SubPlan exists_3))::integer) + SubPlan exists_4 -> Seq Scan on int4_tbl tinner_2 -> Index Only Scan using asptab1_pkey on asptab1 asptab_2 - Index Cond: (id > (EXISTS(SubPlan 3))::integer) - SubPlan 3 + Index Cond: (id > (EXISTS(SubPlan exists_3))::integer) + SubPlan exists_3 -> Seq Scan on int4_tbl tinner_1 Filter: (f1 = touter.f1) - SubPlan 2 + SubPlan exists_2 -> Seq Scan on int4_tbl tinner (14 rows) @@ -2131,8 +2143,9 @@ explain (costs off) select * from hp where a = 1 and b = 'abcde' and QUERY PLAN -------------------------- Result + Replaces: Scan on hp One-Time Filter: false -(2 rows) +(3 rows) -- -- Test runtime partition pruning @@ -2223,36 +2236,36 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q1 prepare ab_q2 (int, int) as select a from ab where a between $1 and $2 and b < (select 3); explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q2 (2, 2); - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------- Append (actual rows=0.00 loops=1) Subplans Removed: 6 - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Seq Scan on ab_a2_b1 ab_1 (actual rows=0.00 loops=1) - Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1)) + Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan expr_1).col1)) -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1) - Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1)) + Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan expr_1).col1)) -> Seq Scan on ab_a2_b3 ab_3 (never executed) - Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan 1).col1)) + Filter: ((a >= $1) AND (a <= $2) AND (b < (InitPlan expr_1).col1)) (10 rows) -- As above, but swap the PARAM_EXEC Param to the first partition level prepare ab_q3 (int, int) as select a from ab where b between $1 and $2 and a < (select 3); explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q3 (2, 2); - QUERY PLAN ------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------- Append (actual rows=0.00 loops=1) Subplans Removed: 6 - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Seq Scan on ab_a1_b2 ab_1 (actual rows=0.00 loops=1) - Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1)) + Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan expr_1).col1)) -> Seq Scan on ab_a2_b2 ab_2 (actual rows=0.00 loops=1) - Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1)) + Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan expr_1).col1)) -> Seq Scan on ab_a3_b2 ab_3 (never executed) - Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan 1).col1)) + Filter: ((b >= $1) AND (b <= $2) AND (a < (InitPlan expr_1).col1)) (10 rows) -- @@ -2462,23 +2475,23 @@ select explain_parallel_append('execute ab_q5 (33, 44, 55)'); -- Test Parallel Append with PARAM_EXEC Params select explain_parallel_append('select count(*) from ab where (a = (select 1) or a = (select 3)) and b = 2'); - explain_parallel_append ------------------------------------------------------------------------------------------------- + explain_parallel_append +---------------------------------------------------------------------------------------------------------- Aggregate (actual rows=N loops=N) - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=N loops=N) - InitPlan 2 + InitPlan expr_2 -> Result (actual rows=N loops=N) -> Gather (actual rows=N loops=N) Workers Planned: 2 Workers Launched: N -> Parallel Append (actual rows=N loops=N) -> Parallel Seq Scan on ab_a1_b2 ab_1 (actual rows=N loops=N) - Filter: ((b = 2) AND ((a = (InitPlan 1).col1) OR (a = (InitPlan 2).col1))) + Filter: ((b = 2) AND ((a = (InitPlan expr_1).col1) OR (a = (InitPlan expr_2).col1))) -> Parallel Seq Scan on ab_a2_b2 ab_2 (never executed) - Filter: ((b = 2) AND ((a = (InitPlan 1).col1) OR (a = (InitPlan 2).col1))) + Filter: ((b = 2) AND ((a = (InitPlan expr_1).col1) OR (a = (InitPlan expr_2).col1))) -> Parallel Seq Scan on ab_a3_b2 ab_3 (actual rows=N loops=N) - Filter: ((b = 2) AND ((a = (InitPlan 1).col1) OR (a = (InitPlan 2).col1))) + Filter: ((b = 2) AND ((a = (InitPlan expr_1).col1) OR (a = (InitPlan expr_2).col1))) (15 rows) -- Test pruning during parallel nested loop query @@ -2679,65 +2692,65 @@ select * from ab where a = (select max(a) from lprt_a) and b = (select max(a)-1 QUERY PLAN ---------------------------------------------------------------------------- Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Aggregate (actual rows=1.00 loops=1) -> Seq Scan on lprt_a (actual rows=102.00 loops=1) - InitPlan 2 + InitPlan expr_2 -> Aggregate (actual rows=1.00 loops=1) -> Seq Scan on lprt_a lprt_a_1 (actual rows=102.00 loops=1) -> Bitmap Heap Scan on ab_a1_b1 ab_1 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a1_b1_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a1_b2 ab_2 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a1_b2_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a1_b3 ab_3 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a1_b3_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a2_b1 ab_4 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a2_b1_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a2_b2 ab_5 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a2_b2_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a2_b3 ab_6 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a2_b3_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a3_b1 ab_7 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a3_b1_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 -> Bitmap Heap Scan on ab_a3_b2 ab_8 (actual rows=0.00 loops=1) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a3_b2_a_idx (actual rows=0.00 loops=1) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 1 -> Bitmap Heap Scan on ab_a3_b3 ab_9 (never executed) - Recheck Cond: (a = (InitPlan 1).col1) - Filter: (b = (InitPlan 2).col1) + Recheck Cond: (a = (InitPlan expr_1).col1) + Filter: (b = (InitPlan expr_2).col1) -> Bitmap Index Scan on ab_a3_b3_a_idx (never executed) - Index Cond: (a = (InitPlan 1).col1) + Index Cond: (a = (InitPlan expr_1).col1) Index Searches: 0 (61 rows) @@ -2747,45 +2760,45 @@ select * from (select * from ab where a = 1 union all select * from ab) ab where QUERY PLAN ---------------------------------------------------------------------------------- Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Append (actual rows=0.00 loops=1) -> Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0.00 loops=1) Recheck Cond: (a = 1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1) Index Cond: (a = 1) Index Searches: 1 -> Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed) Recheck Cond: (a = 1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Bitmap Index Scan on ab_a1_b2_a_idx (never executed) Index Cond: (a = 1) Index Searches: 0 -> Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed) Recheck Cond: (a = 1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Bitmap Index Scan on ab_a1_b3_a_idx (never executed) Index Cond: (a = 1) Index Searches: 0 -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a1_b2 ab_2 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a1_b3 ab_3 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b2 ab_5 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b3 ab_6 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a3_b2 ab_8 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a3_b3 ab_9 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) (40 rows) -- A case containing a UNION ALL with a non-partitioned child. @@ -2794,47 +2807,47 @@ select * from (select * from ab where a = 1 union all (values(10,5)) union all s QUERY PLAN ---------------------------------------------------------------------------------- Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Append (actual rows=0.00 loops=1) -> Bitmap Heap Scan on ab_a1_b1 ab_11 (actual rows=0.00 loops=1) Recheck Cond: (a = 1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0.00 loops=1) Index Cond: (a = 1) Index Searches: 1 -> Bitmap Heap Scan on ab_a1_b2 ab_12 (never executed) Recheck Cond: (a = 1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Bitmap Index Scan on ab_a1_b2_a_idx (never executed) Index Cond: (a = 1) Index Searches: 0 -> Bitmap Heap Scan on ab_a1_b3 ab_13 (never executed) Recheck Cond: (a = 1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Bitmap Index Scan on ab_a1_b3_a_idx (never executed) Index Cond: (a = 1) Index Searches: 0 -> Result (actual rows=0.00 loops=1) - One-Time Filter: (5 = (InitPlan 1).col1) + One-Time Filter: (5 = (InitPlan expr_1).col1) -> Seq Scan on ab_a1_b1 ab_1 (actual rows=0.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a1_b2 ab_2 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a1_b3 ab_3 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b1 ab_4 (actual rows=0.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b2 ab_5 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b3 ab_6 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a3_b1 ab_7 (actual rows=0.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a3_b2 ab_8 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a3_b3 ab_9 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) (42 rows) -- Another UNION ALL test, but containing a mix of exec init and exec run-time pruning. @@ -2852,27 +2865,27 @@ union all ) ab where a = $1 and b = (select -10); -- Ensure the xy_1 subplan is not pruned. explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q6(1); - QUERY PLAN --------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- Append (actual rows=0.00 loops=1) Subplans Removed: 12 - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Seq Scan on ab_a1_b1 ab_1 (never executed) - Filter: ((a = $1) AND (b = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = (InitPlan expr_1).col1)) -> Seq Scan on ab_a1_b2 ab_2 (never executed) - Filter: ((a = $1) AND (b = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = (InitPlan expr_1).col1)) -> Seq Scan on ab_a1_b3 ab_3 (never executed) - Filter: ((a = $1) AND (b = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = (InitPlan expr_1).col1)) -> Seq Scan on xy_1 (actual rows=0.00 loops=1) - Filter: ((x = $1) AND (y = (InitPlan 1).col1)) + Filter: ((x = $1) AND (y = (InitPlan expr_1).col1)) Rows Removed by Filter: 1 -> Seq Scan on ab_a1_b1 ab_4 (never executed) - Filter: ((a = $1) AND (b = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = (InitPlan expr_1).col1)) -> Seq Scan on ab_a1_b2 ab_5 (never executed) - Filter: ((a = $1) AND (b = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = (InitPlan expr_1).col1)) -> Seq Scan on ab_a1_b3 ab_6 (never executed) - Filter: ((a = $1) AND (b = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = (InitPlan expr_1).col1)) (19 rows) -- Ensure we see just the xy_1 row. @@ -2958,7 +2971,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);'); Update on ab_a1_b1 ab_a1_1 Update on ab_a1_b2 ab_a1_2 Update on ab_a1_b3 ab_a1_3 - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Nested Loop (actual rows=3.00 loops=1) -> Append (actual rows=3.00 loops=1) @@ -2969,11 +2982,11 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);'); Storage: Memory Maximum Storage: NkB -> Append (actual rows=1.00 loops=1) -> Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1.00 loops=1) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b2 ab_a2_2 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) -> Seq Scan on ab_a2_b3 ab_a2_3 (never executed) - Filter: (b = (InitPlan 1).col1) + Filter: (b = (InitPlan expr_1).col1) (20 rows) select tableoid::regclass, * from ab; @@ -3343,12 +3356,12 @@ select * from listp where a = (select null::int); QUERY PLAN ------------------------------------------------------ Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Seq Scan on listp_1_1 listp_1 (never executed) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) -> Seq Scan on listp_2_1 listp_2 (never executed) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) (7 rows) drop table listp; @@ -3390,11 +3403,12 @@ select * from stable_qual_pruning where a < '2000-02-01'::timestamptz; explain (analyze, costs off, summary off, timing off, buffers off) select * from stable_qual_pruning where a = any(array['2010-02-01', '2020-01-01']::timestamp[]); - QUERY PLAN ------------------------------------ + QUERY PLAN +----------------------------------------- Result (actual rows=0.00 loops=1) + Replaces: Scan on stable_qual_pruning One-Time Filter: false -(2 rows) +(3 rows) explain (analyze, costs off, summary off, timing off, buffers off) select * from stable_qual_pruning @@ -3486,14 +3500,14 @@ prepare ps1 as select * from mc3p where a = $1 and abs(b) < (select 3); explain (analyze, costs off, summary off, timing off, buffers off) execute ps1(1); - QUERY PLAN -------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------ Append (actual rows=1.00 loops=1) Subplans Removed: 2 - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Seq Scan on mc3p1 mc3p_1 (actual rows=1.00 loops=1) - Filter: ((a = $1) AND (abs(b) < (InitPlan 1).col1)) + Filter: ((a = $1) AND (abs(b) < (InitPlan expr_1).col1)) (6 rows) deallocate ps1; @@ -3501,16 +3515,16 @@ prepare ps2 as select * from mc3p where a <= $1 and abs(b) < (select 3); explain (analyze, costs off, summary off, timing off, buffers off) execute ps2(1); - QUERY PLAN --------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------- Append (actual rows=2.00 loops=1) Subplans Removed: 1 - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) -> Seq Scan on mc3p0 mc3p_1 (actual rows=1.00 loops=1) - Filter: ((a <= $1) AND (abs(b) < (InitPlan 1).col1)) + Filter: ((a <= $1) AND (abs(b) < (InitPlan expr_1).col1)) -> Seq Scan on mc3p1 mc3p_2 (actual rows=1.00 loops=1) - Filter: ((a <= $1) AND (abs(b) < (InitPlan 1).col1)) + Filter: ((a <= $1) AND (abs(b) < (InitPlan expr_1).col1)) (8 rows) deallocate ps2; @@ -3526,14 +3540,14 @@ select * from boolp where a = (select value from boolvalues where value); QUERY PLAN -------------------------------------------------------------- Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Seq Scan on boolvalues (actual rows=1.00 loops=1) Filter: value Rows Removed by Filter: 1 -> Seq Scan on boolp_f boolp_1 (never executed) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) -> Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) (9 rows) explain (analyze, costs off, summary off, timing off, buffers off) @@ -3541,14 +3555,14 @@ select * from boolp where a = (select value from boolvalues where not value); QUERY PLAN -------------------------------------------------------------- Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Seq Scan on boolvalues (actual rows=1.00 loops=1) Filter: (NOT value) Rows Removed by Filter: 1 -> Seq Scan on boolp_f boolp_1 (actual rows=0.00 loops=1) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) -> Seq Scan on boolp_t boolp_2 (never executed) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) (9 rows) drop table boolp; @@ -3640,23 +3654,24 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from -------------------------------------------------------------------------------------------------- Merge Append (actual rows=20.00 loops=1) Sort Key: ma_test.b - InitPlan 2 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit (actual rows=1.00 loops=1) -> Index Scan using ma_test_p2_b_idx on ma_test_p2 (actual rows=1.00 loops=1) Index Cond: (b IS NOT NULL) Index Searches: 1 -> Index Scan using ma_test_p1_b_idx on ma_test_p1 ma_test_1 (never executed) - Filter: (a >= (InitPlan 2).col1) + Filter: (a >= (InitPlan expr_1).col1) Index Searches: 0 -> Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_2 (actual rows=10.00 loops=1) - Filter: (a >= (InitPlan 2).col1) + Filter: (a >= (InitPlan expr_1).col1) Index Searches: 1 -> Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_3 (actual rows=10.00 loops=1) - Filter: (a >= (InitPlan 2).col1) + Filter: (a >= (InitPlan expr_1).col1) Index Searches: 1 -(18 rows) +(19 rows) reset enable_seqscan; reset enable_sort; @@ -3678,11 +3693,12 @@ explain (costs off) select * from pp_arrpart where a = '{1}'; (2 rows) explain (costs off) select * from pp_arrpart where a = '{1, 2}'; - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------- Result + Replaces: Scan on pp_arrpart One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from pp_arrpart where a in ('{4, 5}', '{1}'); QUERY PLAN @@ -3764,11 +3780,12 @@ explain (costs off) select * from pp_enumpart where a = 'blue'; (2 rows) explain (costs off) select * from pp_enumpart where a = 'black'; - QUERY PLAN --------------------------- + QUERY PLAN +--------------------------------- Result + Replaces: Scan on pp_enumpart One-Time Filter: false -(2 rows) +(3 rows) drop table pp_enumpart; drop type pp_colors; @@ -3785,11 +3802,12 @@ explain (costs off) select * from pp_recpart where a = '(1,1)'::pp_rectype; (2 rows) explain (costs off) select * from pp_recpart where a = '(1,2)'::pp_rectype; - QUERY PLAN --------------------------- + QUERY PLAN +-------------------------------- Result + Replaces: Scan on pp_recpart One-Time Filter: false -(2 rows) +(3 rows) drop table pp_recpart; drop type pp_rectype; @@ -3805,11 +3823,12 @@ explain (costs off) select * from pp_intrangepart where a = '[1,2]'::int4range; (2 rows) explain (costs off) select * from pp_intrangepart where a = '(1,2)'::int4range; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------------- Result + Replaces: Scan on pp_intrangepart One-Time Filter: false -(2 rows) +(3 rows) drop table pp_intrangepart; -- @@ -4024,17 +4043,17 @@ from ( select 1, 1, 1 ) s(a, b, c) where s.a = 1 and s.b = 1 and s.c = (select 1); - QUERY PLAN -------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------ Append - InitPlan 1 + InitPlan expr_1 -> Result -> Seq Scan on p1 p - Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan 1).col1)) + Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan expr_1).col1)) -> Seq Scan on q111 q1 - Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan 1).col1)) + Filter: ((a = 1) AND (b = 1) AND (c = (InitPlan expr_1).col1)) -> Result - One-Time Filter: (1 = (InitPlan 1).col1) + One-Time Filter: (1 = (InitPlan expr_1).col1) (9 rows) select * @@ -4062,18 +4081,18 @@ from ( ) s(a, b, c) where s.a = $1 and s.b = $2 and s.c = (select 1); explain (costs off) execute q (1, 1); - QUERY PLAN ------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------- Append Subplans Removed: 1 - InitPlan 1 + InitPlan expr_1 -> Result -> Seq Scan on p1 p - Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan expr_1).col1)) -> Seq Scan on q111 q1 - Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan 1).col1)) + Filter: ((a = $1) AND (b = $2) AND (c = (InitPlan expr_1).col1)) -> Result - One-Time Filter: ((1 = $1) AND (1 = $2) AND (1 = (InitPlan 1).col1)) + One-Time Filter: ((1 = $1) AND (1 = $2) AND (1 = (InitPlan expr_1).col1)) (10 rows) execute q (1, 1); @@ -4091,11 +4110,11 @@ create table listp2 partition of listp for values in(2) partition by list(b); create table listp2_10 partition of listp2 for values in (10); explain (analyze, costs off, summary off, timing off, buffers off) select * from listp where a = (select 2) and b <> 10; - QUERY PLAN ------------------------------------------------------ + QUERY PLAN +-------------------------------------------------------- Seq Scan on listp1 listp (actual rows=0.00 loops=1) - Filter: ((b <> 10) AND (a = (InitPlan 1).col1)) - InitPlan 1 + Filter: ((b <> 10) AND (a = (InitPlan expr_1).col1)) + InitPlan expr_1 -> Result (never executed) (4 rows) @@ -4125,19 +4144,21 @@ explain (costs off) update listp1 set a = 1 where a = 2; -- constraint exclusion enabled set constraint_exclusion to 'on'; explain (costs off) select * from listp1 where a = 2; - QUERY PLAN --------------------------- + QUERY PLAN +---------------------------- Result + Replaces: Scan on listp1 One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) update listp1 set a = 1 where a = 2; - QUERY PLAN --------------------------------- + QUERY PLAN +---------------------------------- Update on listp1 -> Result + Replaces: Scan on listp1 One-Time Filter: false -(3 rows) +(4 rows) reset constraint_exclusion; reset enable_partition_pruning; @@ -4161,13 +4182,13 @@ select explain_parallel_append('select * from listp where a = (select 1);'); Gather (actual rows=N loops=N) Workers Planned: 2 Workers Launched: N - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=N loops=N) -> Parallel Append (actual rows=N loops=N) -> Seq Scan on listp_12_1 listp_1 (actual rows=N loops=N) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) -> Parallel Seq Scan on listp_12_2 listp_2 (never executed) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) (10 rows) -- Like the above but throw some more complexity at the planner by adding @@ -4184,19 +4205,19 @@ select * from listp where a = (select 2);'); Workers Launched: N -> Parallel Append (actual rows=N loops=N) -> Parallel Append (actual rows=N loops=N) - InitPlan 2 + InitPlan expr_2 -> Result (actual rows=N loops=N) -> Seq Scan on listp_12_1 listp_1 (never executed) - Filter: (a = (InitPlan 2).col1) + Filter: (a = (InitPlan expr_2).col1) -> Parallel Seq Scan on listp_12_2 listp_2 (actual rows=N loops=N) - Filter: (a = (InitPlan 2).col1) + Filter: (a = (InitPlan expr_2).col1) -> Parallel Append (actual rows=N loops=N) - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=N loops=N) -> Seq Scan on listp_12_1 listp_4 (actual rows=N loops=N) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) -> Parallel Seq Scan on listp_12_2 listp_5 (never executed) - Filter: (a = (InitPlan 1).col1) + Filter: (a = (InitPlan expr_1).col1) (18 rows) drop table listp; @@ -4219,23 +4240,23 @@ select * from rangep where b IN((select 1),(select 2)) order by a; QUERY PLAN --------------------------------------------------------------------------------------------------------------- Append (actual rows=0.00 loops=1) - InitPlan 1 + InitPlan expr_1 -> Result (actual rows=1.00 loops=1) - InitPlan 2 + InitPlan expr_2 -> Result (actual rows=1.00 loops=1) -> Merge Append (actual rows=0.00 loops=1) Sort Key: rangep_2.a -> Index Scan using rangep_0_to_100_1_a_idx on rangep_0_to_100_1 rangep_2 (actual rows=0.00 loops=1) - Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1])) + Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1])) Index Searches: 1 -> Index Scan using rangep_0_to_100_2_a_idx on rangep_0_to_100_2 rangep_3 (actual rows=0.00 loops=1) - Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1])) + Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1])) Index Searches: 1 -> Index Scan using rangep_0_to_100_3_a_idx on rangep_0_to_100_3 rangep_4 (never executed) - Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1])) + Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1])) Index Searches: 0 -> Index Scan using rangep_100_to_200_a_idx on rangep_100_to_200 rangep_5 (actual rows=0.00 loops=1) - Filter: (b = ANY (ARRAY[(InitPlan 1).col1, (InitPlan 2).col1])) + Filter: (b = ANY (ARRAY[(InitPlan expr_1).col1, (InitPlan expr_2).col1])) Index Searches: 1 (19 rows) @@ -4524,18 +4545,20 @@ create table hp_contradict_test (a int, b int) partition by hash (a part_test_in create table hp_contradict_test_p1 partition of hp_contradict_test for values with (modulus 2, remainder 0); create table hp_contradict_test_p2 partition of hp_contradict_test for values with (modulus 2, remainder 1); explain (costs off) select * from hp_contradict_test where a is null and a === 1 and b === 1; - QUERY PLAN --------------------------- + QUERY PLAN +---------------------------------------- Result + Replaces: Scan on hp_contradict_test One-Time Filter: false -(2 rows) +(3 rows) explain (costs off) select * from hp_contradict_test where a === 1 and b === 1 and a is null; - QUERY PLAN --------------------------- + QUERY PLAN +---------------------------------------- Result + Replaces: Scan on hp_contradict_test One-Time Filter: false -(2 rows) +(3 rows) drop table hp_contradict_test; drop operator class part_test_int4_ops2 using hash; @@ -4763,7 +4786,7 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o QUERY PLAN ---------------------------------------------------------------------------------------------- Append - -> Subquery Scan on "*SELECT* 1_1" + -> Subquery Scan on unnamed_subquery_2 -> WindowAgg Window: w1 AS (PARTITION BY part_abc.a ORDER BY part_abc.a) -> Append @@ -4780,7 +4803,7 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o -> Index Scan using part_abc_3_2_a_idx on part_abc_3_2 part_abc_4 Index Cond: (a >= (stable_one() + 1)) Filter: (d <= stable_one()) - -> Subquery Scan on "*SELECT* 2" + -> Subquery Scan on unnamed_subquery_1 -> WindowAgg Window: w1 AS (PARTITION BY part_abc_5.a ORDER BY part_abc_5.a) -> Append @@ -4801,3 +4824,135 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o drop view part_abc_view; drop table part_abc; +-- +-- Check that operands wrapped in PlaceHolderVars are matched to partition +-- keys, allowing partition pruning to occur. PlaceHolderVars can be +-- introduced when a subquery's output is used with grouping sets. +-- +create table phv_part (a int, b text) partition by list (a); +create table phv_part_1 partition of phv_part for values in (1); +create table phv_part_2 partition of phv_part for values in (2); +create table phv_part_null partition of phv_part for values in (null); +insert into phv_part values (1, 'one'), (2, 'two'), (null, 'null'); +-- OpExpr: PHV-wrapped operand matched via equal() +explain (costs off) +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + QUERY PLAN +--------------------------------------- + MixedAggregate + Hash Key: phv_part.b + Group Key: phv_part.a + -> Seq Scan on phv_part_1 phv_part + Filter: (a = 1) +(5 rows) + +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + a | b +---+----- + 1 | + | one +(2 rows) + +-- OpExpr with RelabelType: PHV wrapped around a casted column +explain (costs off) +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + QUERY PLAN +------------------------------------------- + HashAggregate + Hash Key: (phv_part.a)::oid + Hash Key: phv_part.b + -> Seq Scan on phv_part_1 phv_part + Filter: (((a)::oid)::integer = 1) +(5 rows) + +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + x | b +---+----- + 1 | + | one +(2 rows) + +-- ScalarArrayOpExpr: IN clause with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + QUERY PLAN +--------------------------------------------------- + HashAggregate + Hash Key: phv_part.a + Hash Key: phv_part.b + -> Seq Scan on phv_part_1 phv_part + Filter: (a = ANY ('{1,NULL}'::integer[])) +(5 rows) + +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + a | b +---+----- + 1 | + | one +(2 rows) + +-- NullTest: IS NULL with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + QUERY PLAN +------------------------------------------ + HashAggregate + Hash Key: phv_part.a + Hash Key: phv_part.b + -> Seq Scan on phv_part_null phv_part + Filter: (a IS NULL) +(5 rows) + +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + a | b +---+------ + | + | null +(2 rows) + +drop table phv_part; +-- BooleanTest: IS TRUE with PHV-wrapped boolean partition key +create table phv_boolpart (a bool, b text) partition by list (a); +create table phv_boolpart_t partition of phv_boolpart for values in (true); +create table phv_boolpart_f partition of phv_boolpart for values in (false); +create table phv_boolpart_null partition of phv_boolpart default; +insert into phv_boolpart values (true, 'yes'), (false, 'no'), (null, 'unknown'); +explain (costs off) +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + QUERY PLAN +----------------------------------------------- + HashAggregate + Hash Key: phv_boolpart.a + Hash Key: phv_boolpart.b + -> Seq Scan on phv_boolpart_t phv_boolpart + Filter: (a IS TRUE) +(5 rows) + +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + a | b +---+----- + t | + | yes +(2 rows) + +drop table phv_boolpart; diff --git a/src/test/regress/expected/partition_split.out b/src/test/regress/expected/partition_split.out new file mode 100644 index 0000000000000..43ca299648ebb --- /dev/null +++ b/src/test/regress/expected/partition_split.out @@ -0,0 +1,1595 @@ +-- +-- PARTITION_SPLIT +-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command +-- +CREATE SCHEMA partition_split_schema; +CREATE SCHEMA partition_split_schema2; +SET search_path = partition_split_schema, public; +-- +-- BY RANGE partitioning +-- +-- +-- Test for error codes +-- +CREATE TABLE sales_range (salesperson_id int, sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +-- ERROR: relation "sales_xxx" does not exist +ALTER TABLE sales_range SPLIT PARTITION sales_xxx INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: relation "sales_xxx" does not exist +-- ERROR: relation "sales_jan2022" already exists +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: relation "sales_jan2022" already exists +-- ERROR: invalid bound specification for a range partition +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '2022-06-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: invalid bound specification for a range partition +LINE 2: (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '202... + ^ +-- ERROR: empty range bound specified for partition "sales_mar2022" +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-02-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: empty range bound specified for partition "sales_mar2022" +LINE 3: PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO... + ^ +DETAIL: Specified lower bound ('03-01-2022') is greater than or equal to upper bound ('02-01-2022'). +--ERROR: list of split partitions should contain at least two items +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-10-01')); +ERROR: list of new partitions should contain at least two partitions +-- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" +LINE 2: (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO... + ^ +HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR: partition with name "sales_feb_mar_apr2022" is already used +-- (We can create partition with the same name as split partition, but can't create two partitions with the same name) +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: partition with name "sales_feb_mar_apr2022" is already used +LINE 3: PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03... + ^ +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: partition with name "sales_feb2022" is already used +LINE 3: PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO... + ^ +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: partition with name "sales_feb2022" is already used +LINE 3: PARTITION partition_split_schema.sales_feb2022 FOR VALUES... + ^ +-- ERROR: ALTER action SPLIT PARTITION cannot be performed on relation "sales_feb_mar_apr2022" +-- DETAIL: This operation is not supported for tables. +ALTER TABLE sales_feb_mar_apr2022 SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: ALTER action SPLIT PARTITION cannot be performed on relation "sales_feb_mar_apr2022" +DETAIL: This operation is not supported for tables. +-- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01')); +ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" +LINE 4: ... sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-0... + ^ +HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR: can not split to partition "sales_mar2022" together with partition "sales_feb2022" +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: can not split to partition "sales_mar2022" together with partition "sales_feb2022" +LINE 3: PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO... + ^ +DETAIL: lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_feb2022" +HINT: ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent. +-- Tests for spaces between partitions, them should be executed without DEFAULT partition +ALTER TABLE sales_range DETACH PARTITION sales_others; +-- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" +LINE 2: (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO... + ^ +HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- Check the source partition not in the search path +SET search_path = partition_split_schema2, public; +ALTER TABLE partition_split_schema.sales_range +SPLIT PARTITION partition_split_schema.sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +SET search_path = partition_split_schema, public; +\d+ sales_range + Partitioned table "partition_split_schema.sales_range" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +----------------+---------+-----------+----------+---------+---------+--------------+------------- + salesperson_id | integer | | | | plain | | + sales_date | date | | | | plain | | +Partition key: RANGE (sales_date) +Partitions: + partition_split_schema2.sales_apr2022 FOR VALUES FROM ('04-01-2022') TO ('05-01-2022') + partition_split_schema2.sales_feb2022 FOR VALUES FROM ('02-01-2022') TO ('03-01-2022') + partition_split_schema2.sales_mar2022 FOR VALUES FROM ('03-01-2022') TO ('04-01-2022') + sales_jan2022 FOR VALUES FROM ('01-01-2022') TO ('02-01-2022') + +DROP TABLE sales_range; +DROP TABLE sales_others; +-- Additional tests for error messages, no default partition +CREATE TABLE sales_range (sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +-- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01')); +ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" +LINE 4: ... sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-0... + ^ +HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +DROP TABLE sales_range; +-- +-- Add rows into partitioned table then split partition +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------+----------------+------------------+--------------+------------ + sales_apr2022 | 3 | Ford | 2000 | 04-30-2022 + sales_apr2022 | 4 | Ivanov | 750 | 04-13-2022 + sales_apr2022 | 5 | Deev | 250 | 04-07-2022 + sales_apr2022 | 11 | Trump | 380 | 04-06-2022 + sales_feb2022 | 2 | Smirnoff | 500 | 02-10-2022 + sales_feb2022 | 6 | Poirot | 150 | 02-11-2022 + sales_feb2022 | 8 | Ericsson | 185 | 02-23-2022 + sales_jan2022 | 1 | May | 1000 | 01-31-2022 + sales_jan2022 | 10 | Halder | 350 | 01-28-2022 + sales_jan2022 | 13 | Gandi | 377 | 01-09-2022 + sales_mar2022 | 7 | Li | 175 | 03-08-2022 + sales_mar2022 | 9 | Muller | 250 | 03-11-2022 + sales_mar2022 | 12 | Plato | 350 | 03-19-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +DROP TABLE sales_range CASCADE; +-- +-- Add split partition, then add rows into partitioned table +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +-- Split partition, also check schema qualification of new partitions +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION partition_split_schema2.sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +\d+ sales_range + Partitioned table "partition_split_schema.sales_range" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +------------------+-----------------------+-----------+----------+---------+----------+--------------+------------- + salesperson_id | integer | | | | plain | | + salesperson_name | character varying(30) | | | | extended | | + sales_amount | integer | | | | plain | | + sales_date | date | | | | plain | | +Partition key: RANGE (sales_date) +Partitions: + partition_split_schema2.sales_mar2022 FOR VALUES FROM ('03-01-2022') TO ('04-01-2022') + sales_apr2022 FOR VALUES FROM ('04-01-2022') TO ('05-01-2022') + sales_feb2022 FOR VALUES FROM ('02-01-2022') TO ('03-01-2022') + sales_jan2022 FOR VALUES FROM ('01-01-2022') TO ('02-01-2022') + sales_others DEFAULT + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------------------------------+----------------+------------------+--------------+------------ + partition_split_schema2.sales_mar2022 | 7 | Li | 175 | 03-08-2022 + partition_split_schema2.sales_mar2022 | 9 | Muller | 250 | 03-11-2022 + partition_split_schema2.sales_mar2022 | 12 | Plato | 350 | 03-19-2022 + sales_apr2022 | 3 | Ford | 2000 | 04-30-2022 + sales_apr2022 | 4 | Ivanov | 750 | 04-13-2022 + sales_apr2022 | 5 | Deev | 250 | 04-07-2022 + sales_apr2022 | 11 | Trump | 380 | 04-06-2022 + sales_feb2022 | 2 | Smirnoff | 500 | 02-10-2022 + sales_feb2022 | 6 | Poirot | 150 | 02-11-2022 + sales_feb2022 | 8 | Ericsson | 185 | 02-23-2022 + sales_jan2022 | 1 | May | 1000 | 01-31-2022 + sales_jan2022 | 10 | Halder | 350 | 01-28-2022 + sales_jan2022 | 13 | Gandi | 377 | 01-09-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +DROP TABLE sales_range CASCADE; +-- +-- Test for: +-- * composite partition key; +-- * GENERATED column; +-- * column with DEFAULT value. +-- +CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT, + sales_date VARCHAR(10) GENERATED ALWAYS AS + (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED, + sales_department VARCHAR(30) DEFAULT 'Sales department') + PARTITION BY RANGE (sales_year, sales_month, sales_day); +CREATE TABLE sales_dec2021 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1); +CREATE TABLE sales_jan_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 3, 1); +CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE); +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2021, 12, 7), + ('Manager2', 2021, 12, 8), + ('Manager3', 2022, 1, 1), + ('Manager1', 2022, 2, 4), + ('Manager2', 2022, 1, 2), + ('Manager3', 2022, 2, 1), + ('Manager1', 2022, 3, 3), + ('Manager2', 2022, 3, 4), + ('Manager3', 2022, 5, 1); +SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid::regclass::text COLLATE "C", sales_year, sales_month, sales_day; + tableoid | salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department +-------------------+------------------+------------+-------------+-----------+------------+------------------ + sales_dec2021 | Manager1 | 2021 | 12 | 7 | 2021.12.07 | Sales department + sales_dec2021 | Manager2 | 2021 | 12 | 8 | 2021.12.08 | Sales department + sales_jan_feb2022 | Manager3 | 2022 | 1 | 1 | 2022.01.01 | Sales department + sales_jan_feb2022 | Manager2 | 2022 | 1 | 2 | 2022.01.02 | Sales department + sales_jan_feb2022 | Manager3 | 2022 | 2 | 1 | 2022.02.01 | Sales department + sales_jan_feb2022 | Manager1 | 2022 | 2 | 4 | 2022.02.04 | Sales department + sales_other | Manager1 | 2022 | 3 | 3 | 2022.03.03 | Sales department + sales_other | Manager2 | 2022 | 3 | 4 | 2022.03.04 | Sales department + sales_other | Manager3 | 2022 | 5 | 1 | 2022.05.01 | Sales department +(9 rows) + +ALTER TABLE sales_date SPLIT PARTITION sales_jan_feb2022 INTO + (PARTITION sales_jan2022 FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1), + PARTITION sales_feb2022 FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1)); +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2022, 1, 10), + ('Manager2', 2022, 2, 10); +SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid::regclass::text COLLATE "C", sales_year, sales_month, sales_day; + tableoid | salesperson_name | sales_year | sales_month | sales_day | sales_date | sales_department +---------------+------------------+------------+-------------+-----------+------------+------------------ + sales_dec2021 | Manager1 | 2021 | 12 | 7 | 2021.12.07 | Sales department + sales_dec2021 | Manager2 | 2021 | 12 | 8 | 2021.12.08 | Sales department + sales_feb2022 | Manager3 | 2022 | 2 | 1 | 2022.02.01 | Sales department + sales_feb2022 | Manager1 | 2022 | 2 | 4 | 2022.02.04 | Sales department + sales_feb2022 | Manager2 | 2022 | 2 | 10 | 2022.02.10 | Sales department + sales_jan2022 | Manager3 | 2022 | 1 | 1 | 2022.01.01 | Sales department + sales_jan2022 | Manager2 | 2022 | 1 | 2 | 2022.01.02 | Sales department + sales_jan2022 | Manager1 | 2022 | 1 | 10 | 2022.01.10 | Sales department + sales_other | Manager1 | 2022 | 3 | 3 | 2022.03.03 | Sales department + sales_other | Manager2 | 2022 | 3 | 4 | 2022.03.04 | Sales department + sales_other | Manager3 | 2022 | 5 | 1 | 2022.05.01 | Sales department +(11 rows) + +DROP TABLE sales_date CASCADE; +-- +-- Test: split DEFAULT partition; use an index on partition key; check index after split +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +SELECT * FROM sales_others; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 2 | Smirnoff | 500 | 02-10-2022 + 3 | Ford | 2000 | 04-30-2022 + 4 | Ivanov | 750 | 04-13-2022 + 5 | Deev | 250 | 04-07-2022 + 6 | Poirot | 150 | 02-11-2022 + 7 | Li | 175 | 03-08-2022 + 8 | Ericsson | 185 | 02-23-2022 + 9 | Muller | 250 | 03-11-2022 + 11 | Trump | 380 | 04-06-2022 + 12 | Plato | 350 | 03-19-2022 + 14 | Smith | 510 | 05-04-2022 +(11 rows) + +SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema' ORDER BY indexname COLLATE "C"; + schemaname | tablename | indexname | tablespace | indexdef +------------------------+--------------+-----------------------------+------------+---------------------------------------------------------------------------------------------------------- + partition_split_schema | sales_others | sales_others_sales_date_idx | | CREATE INDEX sales_others_sales_date_idx ON partition_split_schema.sales_others USING btree (sales_date) +(1 row) + +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'), + PARTITION sales_others DEFAULT); +-- Use indexscan for testing indexes +SET enable_seqscan = OFF; +EXPLAIN (COSTS OFF) SELECT * FROM sales_feb2022 where sales_date > '2022-01-01'; + QUERY PLAN +---------------------------------------------------------------- + Index Scan using sales_feb2022_sales_date_idx on sales_feb2022 + Index Cond: (sales_date > '01-01-2022'::date) +(2 rows) + +SELECT * FROM sales_feb2022 where sales_date > '2022-01-01'; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 2 | Smirnoff | 500 | 02-10-2022 + 6 | Poirot | 150 | 02-11-2022 + 8 | Ericsson | 185 | 02-23-2022 +(3 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_mar2022 where sales_date > '2022-01-01'; + QUERY PLAN +---------------------------------------------------------------- + Index Scan using sales_mar2022_sales_date_idx on sales_mar2022 + Index Cond: (sales_date > '01-01-2022'::date) +(2 rows) + +SELECT * FROM sales_mar2022 where sales_date > '2022-01-01'; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 7 | Li | 175 | 03-08-2022 + 9 | Muller | 250 | 03-11-2022 + 12 | Plato | 350 | 03-19-2022 +(3 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_apr2022 where sales_date > '2022-01-01'; + QUERY PLAN +---------------------------------------------------------------- + Index Scan using sales_apr2022_sales_date_idx on sales_apr2022 + Index Cond: (sales_date > '01-01-2022'::date) +(2 rows) + +SELECT * FROM sales_apr2022 where sales_date > '2022-01-01'; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 11 | Trump | 380 | 04-06-2022 + 5 | Deev | 250 | 04-07-2022 + 4 | Ivanov | 750 | 04-13-2022 + 3 | Ford | 2000 | 04-30-2022 +(4 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_others where sales_date > '2022-01-01'; + QUERY PLAN +--------------------------------------------------------------- + Index Scan using sales_others_sales_date_idx1 on sales_others + Index Cond: (sales_date > '01-01-2022'::date) +(2 rows) + +SELECT * FROM sales_others where sales_date > '2022-01-01'; + salesperson_id | salesperson_name | sales_amount | sales_date +----------------+------------------+--------------+------------ + 14 | Smith | 510 | 05-04-2022 +(1 row) + +RESET enable_seqscan; +SELECT * FROM pg_indexes +WHERE tablename in ('sales_feb2022', 'sales_mar2022', 'sales_apr2022', 'sales_others') +AND schemaname = 'partition_split_schema' +ORDER BY indexname COLLATE "C"; + schemaname | tablename | indexname | tablespace | indexdef +------------------------+---------------+------------------------------+------------+------------------------------------------------------------------------------------------------------------ + partition_split_schema | sales_apr2022 | sales_apr2022_sales_date_idx | | CREATE INDEX sales_apr2022_sales_date_idx ON partition_split_schema.sales_apr2022 USING btree (sales_date) + partition_split_schema | sales_feb2022 | sales_feb2022_sales_date_idx | | CREATE INDEX sales_feb2022_sales_date_idx ON partition_split_schema.sales_feb2022 USING btree (sales_date) + partition_split_schema | sales_mar2022 | sales_mar2022_sales_date_idx | | CREATE INDEX sales_mar2022_sales_date_idx ON partition_split_schema.sales_mar2022 USING btree (sales_date) + partition_split_schema | sales_others | sales_others_sales_date_idx1 | | CREATE INDEX sales_others_sales_date_idx1 ON partition_split_schema.sales_others USING btree (sales_date) +(4 rows) + +DROP TABLE sales_range CASCADE; +-- +-- Test: some cases for splitting DEFAULT partition (different bounds) +-- +CREATE TABLE sales_range (salesperson_id INT, sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +-- sales_error intersects with sales_dec2021 (lower bound) +-- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2021-12-30') TO ('2022-02-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +LINE 3: PARTITION sales_error FOR VALUES FROM ('2021-12-30') TO (... + ^ +DETAIL: lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021" +HINT: ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent. +-- sales_error intersects with sales_feb2022 (upper bound) +-- ERROR: can not split to partition "sales_feb2022" together with partition "sales_error" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2022-01-01') TO ('2022-02-02'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +ERROR: can not split to partition "sales_feb2022" together with partition "sales_error" +LINE 4: PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO... + ^ +DETAIL: lower bound of partition "sales_feb2022" is not equal to the upper bound of partition "sales_error" +HINT: ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent. +-- sales_error intersects with sales_dec2021 (inside bound) +-- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2021-12-10') TO ('2021-12-20'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +LINE 3: PARTITION sales_error FOR VALUES FROM ('2021-12-10') TO (... + ^ +DETAIL: lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021" +HINT: ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent. +-- sales_error intersects with sales_dec2021 (exactly the same bounds) +-- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +LINE 3: PARTITION sales_error FOR VALUES FROM ('2021-12-01') TO (... + ^ +DETAIL: lower bound of partition "sales_error" is not equal to the upper bound of partition "sales_dec2021" +HINT: ALTER TABLE ... SPLIT PARTITION requires the partition bounds to be adjacent. +-- ERROR: can not split DEFAULT partition "sales_others" +-- HINT: To split DEFAULT partition one of the new partition must be DEFAULT. +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_jan2022 FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01')); +ERROR: can not split DEFAULT partition "sales_others" +LINE 2: (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO... + ^ +HINT: To split DEFAULT partition one of the new partition must be DEFAULT. +-- no error: bounds of sales_noerror are between sales_dec2021 and sales_feb2022 +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_noerror FOR VALUES FROM ('2022-01-10') TO ('2022-01-20'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +DROP TABLE sales_range; +CREATE TABLE sales_range (sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +-- no error: bounds of sales_noerror are equal to lower and upper bounds of sales_dec2021 and sales_feb2022 +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_noerror FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +DROP TABLE sales_range; +-- +-- Test: split partition with CHECK and FOREIGN KEY CONSTRAINTs on partitioned table +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)); +INSERT INTO salespeople VALUES (1, 'Poirot'); +CREATE TABLE sales_range ( +salesperson_id INT REFERENCES salespeople(salesperson_id), +sales_amount INT CHECK (sales_amount > 1), +sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb_mar_apr2022'::regclass::oid ORDER BY conname COLLATE "C"; + pg_get_constraintdef | conname | conkey +---------------------------------------------------------------------+---------------------------------+-------- + CHECK ((sales_amount > 1)) | sales_range_sales_amount_check | {2} + FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1} +(2 rows) + +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +-- We should see the same CONSTRAINTs as on sales_feb_mar_apr2022 partition +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb2022'::regclass::oid ORDER BY conname COLLATE "C"; + pg_get_constraintdef | conname | conkey +---------------------------------------------------------------------+---------------------------------+-------- + CHECK ((sales_amount > 1)) | sales_range_sales_amount_check | {2} + FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1} +(2 rows) + +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid ORDER BY conname COLLATE "C"; + pg_get_constraintdef | conname | conkey +---------------------------------------------------------------------+---------------------------------+-------- + CHECK ((sales_amount > 1)) | sales_range_sales_amount_check | {2} + FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1} +(2 rows) + +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid ORDER BY conname COLLATE "C"; + pg_get_constraintdef | conname | conkey +---------------------------------------------------------------------+---------------------------------+-------- + CHECK ((sales_amount > 1)) | sales_range_sales_amount_check | {2} + FOREIGN KEY (salesperson_id) REFERENCES salespeople(salesperson_id) | sales_range_salesperson_id_fkey | {1} +(2 rows) + +-- ERROR: new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check" +INSERT INTO sales_range VALUES (1, 0, '2022-03-11'); +ERROR: new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check" +DETAIL: Failing row contains (1, 0, 03-11-2022). +-- ERROR: insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey" +INSERT INTO sales_range VALUES (-1, 10, '2022-03-11'); +ERROR: insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey" +DETAIL: Key (salesperson_id)=(-1) is not present in table "salespeople". +-- ok +INSERT INTO sales_range VALUES (1, 10, '2022-03-11'); +DROP TABLE sales_range CASCADE; +DROP TABLE salespeople CASCADE; +-- +-- Test: split partition on partitioned table in case of existing FOREIGN KEY reference from another table +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE sales (salesperson_id INT REFERENCES salespeople(salesperson_id), sales_amount INT, sales_date DATE); +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40); +INSERT INTO salespeople VALUES + (1, 'Poirot'), + (10, 'May'), + (19, 'Ivanov'), + (20, 'Smirnoff'), + (30, 'Ford'); +INSERT INTO sales VALUES + (1, 100, '2022-03-01'), + (1, 110, '2022-03-02'), + (10, 150, '2022-03-01'), + (10, 90, '2022-03-03'), + (19, 200, '2022-03-04'), + (20, 50, '2022-03-12'), + (20, 170, '2022-03-02'), + (30, 30, '2022-03-04'); +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name +------------------+----------------+------------------ + salespeople01_10 | 1 | Poirot + salespeople10_40 | 10 | May + salespeople10_40 | 19 | Ivanov + salespeople10_40 | 20 | Smirnoff + salespeople10_40 | 30 | Ford +(5 rows) + +ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO + (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20), + PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30), + PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40)); +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name +------------------+----------------+------------------ + salespeople01_10 | 1 | Poirot + salespeople10_20 | 10 | May + salespeople10_20 | 19 | Ivanov + salespeople20_30 | 20 | Smirnoff + salespeople30_40 | 30 | Ford +(5 rows) + +-- ERROR: insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey" +INSERT INTO sales VALUES (40, 50, '2022-03-04'); +ERROR: insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey" +DETAIL: Key (salesperson_id)=(40) is not present in table "salespeople". +-- ok +INSERT INTO sales VALUES (30, 50, '2022-03-04'); +DROP TABLE sales CASCADE; +DROP TABLE salespeople CASCADE; +-- +-- Test: split partition of partitioned table with triggers +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40); +INSERT INTO salespeople VALUES (1, 'Poirot'); +CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN NULL; +END; +$BODY$; +CREATE TRIGGER salespeople_after_insert_statement_trigger + AFTER INSERT + ON salespeople + FOR EACH STATEMENT + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); +CREATE TRIGGER salespeople_after_insert_row_trigger + AFTER INSERT + ON salespeople + FOR EACH ROW + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (10, 'May'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT +-- 1 trigger should fire here (row): +INSERT INTO salespeople10_40 VALUES (19, 'Ivanov'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO + (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20), + PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30), + PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40)); +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (20, 'Smirnoff'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = STATEMENT +-- 1 trigger should fire here (row): +INSERT INTO salespeople30_40 VALUES (30, 'Ford'); +NOTICE: trigger(salespeople) called: action = INSERT, when = AFTER, level = ROW +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name +------------------+----------------+------------------ + salespeople01_10 | 1 | Poirot + salespeople10_20 | 10 | May + salespeople10_20 | 19 | Ivanov + salespeople20_30 | 20 | Smirnoff + salespeople30_40 | 30 | Ford +(5 rows) + +DROP TABLE salespeople CASCADE; +DROP FUNCTION after_insert_row_trigger(); +-- +-- Test: split partition witch identity column +-- If split partition column is identity column, columns of new partitions are identity columns too. +-- +CREATE TABLE salespeople(salesperson_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE salespeople1_2 PARTITION OF salespeople FOR VALUES FROM (1) TO (2); +-- Create new partition with identity column: +CREATE TABLE salespeople2_5(salesperson_id INT NOT NULL, salesperson_name VARCHAR(30)); +ALTER TABLE salespeople ATTACH PARTITION salespeople2_5 FOR VALUES FROM (2) TO (5); +INSERT INTO salespeople (salesperson_name) VALUES ('Poirot'), ('Ivanov'); +ALTER TABLE salespeople SPLIT PARTITION salespeople2_5 INTO + (PARTITION salespeople2_3 FOR VALUES FROM (2) TO (3), + PARTITION salespeople3_4 FOR VALUES FROM (3) TO (4), + PARTITION salespeople4_5 FOR VALUES FROM (4) TO (5)); +INSERT INTO salespeople (salesperson_name) VALUES ('May'), ('Ford'); +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name +----------------+----------------+------------------ + salespeople1_2 | 1 | Poirot + salespeople2_3 | 2 | Ivanov + salespeople3_4 | 3 | May + salespeople4_5 | 4 | Ford +(4 rows) + +-- check new partitions have identity or not after split partition +SELECT attrelid::regclass, attname, attidentity, attgenerated FROM pg_attribute +WHERE attnum > 0 +AND attrelid::regclass IN ( + 'salespeople2_3'::regclass, 'salespeople', 'salespeople2_3', + 'salespeople1_2', 'salespeople3_4', 'salespeople4_5') +ORDER BY attrelid::regclass::text COLLATE "C", attnum; + attrelid | attname | attidentity | attgenerated +----------------+------------------+-------------+-------------- + salespeople | salesperson_id | a | + salespeople | salesperson_name | | + salespeople1_2 | salesperson_id | a | + salespeople1_2 | salesperson_name | | + salespeople2_3 | salesperson_id | a | + salespeople2_3 | salesperson_name | | + salespeople3_4 | salesperson_id | a | + salespeople3_4 | salesperson_name | | + salespeople4_5 | salesperson_id | a | + salespeople4_5 | salesperson_name | | +(10 rows) + +DROP TABLE salespeople CASCADE; +-- +-- Test: split partition with deleted columns +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +-- Create new partition with some deleted columns: +CREATE TABLE salespeople10_40(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, d2 INT, d3 DATE, salesperson_name VARCHAR(30)); +INSERT INTO salespeople10_40 VALUES + ('dummy value 1', 19, 100, now(), 'Ivanov'), + ('dummy value 2', 20, 101, now(), 'Smirnoff'); +ALTER TABLE salespeople10_40 DROP COLUMN d1; +ALTER TABLE salespeople10_40 DROP COLUMN d2; +ALTER TABLE salespeople10_40 DROP COLUMN d3; +ALTER TABLE salespeople ATTACH PARTITION salespeople10_40 FOR VALUES FROM (10) TO (40); +INSERT INTO salespeople VALUES + (1, 'Poirot'), + (10, 'May'), + (30, 'Ford'); +ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO + (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20), + PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30), + PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40)); +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name +------------------+----------------+------------------ + salespeople01_10 | 1 | Poirot + salespeople10_20 | 10 | May + salespeople10_20 | 19 | Ivanov + salespeople20_30 | 20 | Smirnoff + salespeople30_40 | 30 | Ford +(5 rows) + +DROP TABLE salespeople CASCADE; +-- +-- Test: split sub-partition +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_apr_all PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------+----------------+------------------+--------------+------------ + sales_apr_all | 3 | Ford | 2000 | 04-30-2022 + sales_apr_all | 4 | Ivanov | 750 | 04-13-2022 + sales_apr_all | 5 | Deev | 250 | 04-07-2022 + sales_apr_all | 11 | Trump | 380 | 04-06-2022 + sales_feb2022 | 2 | Smirnoff | 500 | 02-10-2022 + sales_feb2022 | 6 | Poirot | 150 | 02-11-2022 + sales_feb2022 | 8 | Ericsson | 185 | 02-23-2022 + sales_jan2022 | 1 | May | 1000 | 01-31-2022 + sales_jan2022 | 10 | Halder | 350 | 01-28-2022 + sales_jan2022 | 13 | Gandi | 377 | 01-09-2022 + sales_mar2022 | 7 | Li | 175 | 03-08-2022 + sales_mar2022 | 9 | Muller | 250 | 03-11-2022 + sales_mar2022 | 12 | Plato | 350 | 03-19-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +ALTER TABLE sales_apr2022 SPLIT PARTITION sales_apr_all INTO + (PARTITION sales_apr2022_01_10 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'), + PARTITION sales_apr2022_10_20 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20'), + PARTITION sales_apr2022_20_30 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01')); +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------------+----------------+------------------+--------------+------------ + sales_apr2022_01_10 | 5 | Deev | 250 | 04-07-2022 + sales_apr2022_01_10 | 11 | Trump | 380 | 04-06-2022 + sales_apr2022_10_20 | 4 | Ivanov | 750 | 04-13-2022 + sales_apr2022_20_30 | 3 | Ford | 2000 | 04-30-2022 + sales_feb2022 | 2 | Smirnoff | 500 | 02-10-2022 + sales_feb2022 | 6 | Poirot | 150 | 02-11-2022 + sales_feb2022 | 8 | Ericsson | 185 | 02-23-2022 + sales_jan2022 | 1 | May | 1000 | 01-31-2022 + sales_jan2022 | 10 | Halder | 350 | 01-28-2022 + sales_jan2022 | 13 | Gandi | 377 | 01-09-2022 + sales_mar2022 | 7 | Li | 175 | 03-08-2022 + sales_mar2022 | 9 | Muller | 250 | 03-11-2022 + sales_mar2022 | 12 | Plato | 350 | 03-19-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +DROP TABLE sales_range; +-- +-- BY LIST partitioning +-- +-- +-- Test: specific errors for BY LIST partitioning +-- +CREATE TABLE sales_list (sales_state VARCHAR(20)) PARTITION BY LIST (sales_state); +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; +-- ERROR: new partition "sales_east" would overlap with another (not split) partition "sales_nord" +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok', 'Helsinki'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); +ERROR: new partition "sales_east" would overlap with another (not split) partition "sales_nord" +LINE 3: ...FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok', 'Helsinki'... + ^ +-- ERROR: new partition "sales_west" would overlap with another new partition "sales_central" +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Lisbon', 'Kyiv')); +ERROR: new partition "sales_west" would overlap with another new partition "sales_central" +LINE 2: (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York',... + ^ +-- ERROR: new partition "sales_west" cannot have NULL value because split partition "sales_all" does not have +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); +ERROR: new partition "sales_west" cannot have NULL value because split partition "sales_all" does not have +LINE 2: ...s_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL), + ^ +-- ERROR: new partition "sales_west" cannot have this value because split partition "sales_all" does not have +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); +ERROR: new partition "sales_west" cannot have this value because split partition "sales_all" does not have +LINE 2: ...st FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne... + ^ +-- ERROR: new partition cannot be DEFAULT because DEFAULT partition "sales_others" already exists +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'), + PARTITION sales_others2 DEFAULT); +ERROR: new partition cannot be DEFAULT because DEFAULT partition "sales_others" already exists +LINE 5: PARTITION sales_others2 DEFAULT); + ^ +DROP TABLE sales_list; +-- Test for non-symbolic comparison of values (numeric values '0' and '0.0' are equal). +CREATE TABLE t (a numeric) PARTITION BY LIST (a); +CREATE TABLE t1 PARTITION OF t FOR VALUES in ('0', '1'); +-- ERROR: new partition "x" would overlap with another new partition "x1" +ALTER TABLE t SPLIT PARTITION t1 INTO + (PARTITION x FOR VALUES IN ('0'), + PARTITION x1 FOR VALUES IN ('0.0', '1')); +ERROR: new partition "x" would overlap with another new partition "x1" +LINE 2: (PARTITION x FOR VALUES IN ('0'), + ^ +DROP TABLE t; +-- +-- Test: two specific errors for BY LIST partitioning: +-- * new partitions do not have NULL value, which split partition has. +-- * new partitions do not have a value that split partition has. +-- +CREATE TABLE sales_list(sales_state VARCHAR(20)) PARTITION BY LIST (sales_state); +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL); +-- ERROR: new partitions combined partition bounds do not contain value (NULL) but split partition "sales_all" does +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); +ERROR: new partitions combined partition bounds do not contain value (NULL) but split partition "sales_all" does +HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR: new partitions combined partition bounds do not contain value ('Kyiv'::character varying(20)) but split partition "sales_all" does +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', NULL)); +ERROR: new partitions combined partition bounds do not contain value ('Kyiv'::character varying(20)) but split partition "sales_all" does +HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +-- ERROR DEFAULT partition should be one +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'), + PARTITION sales_others DEFAULT, + PARTITION sales_others2 DEFAULT); +ERROR: DEFAULT partition should be one +LINE 6: PARTITION sales_others2 DEFAULT); + ^ +DROP TABLE sales_list; +-- +-- Test: BY LIST partitioning, SPLIT PARTITION with data +-- +CREATE TABLE sales_list +(salesperson_id SERIAL, + salesperson_name VARCHAR(30), + sales_state VARCHAR(20), + sales_amount INT, + sales_date DATE) +PARTITION BY LIST (sales_state); +CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name); +CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state); +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; +INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES + ('Trump', 'Beijing', 1000, '2022-03-01'), + ('Smirnoff', 'New York', 500, '2022-03-03'), + ('Ford', 'St. Petersburg', 2000, '2022-03-05'), + ('Ivanov', 'Warsaw', 750, '2022-03-04'), + ('Deev', 'Lisbon', 250, '2022-03-07'), + ('Poirot', 'Berlin', 1000, '2022-03-01'), + ('May', 'Oslo', 1200, '2022-03-06'), + ('Li', 'Vladivostok', 1150, '2022-03-09'), + ('May', 'Oslo', 1200, '2022-03-11'), + ('Halder', 'Helsinki', 800, '2022-03-02'), + ('Muller', 'Madrid', 650, '2022-03-05'), + ('Smith', 'Kyiv', 350, '2022-03-10'), + ('Gandi', 'Warsaw', 150, '2022-03-08'), + ('Plato', 'Lisbon', 950, '2022-03-05'); +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); +SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +---------------+----------------+------------------+----------------+--------------+------------ + sales_central | 4 | Ivanov | Warsaw | 750 | 03-04-2022 + sales_central | 6 | Poirot | Berlin | 1000 | 03-01-2022 + sales_central | 12 | Smith | Kyiv | 350 | 03-10-2022 + sales_central | 13 | Gandi | Warsaw | 150 | 03-08-2022 + sales_east | 1 | Trump | Beijing | 1000 | 03-01-2022 + sales_east | 8 | Li | Vladivostok | 1150 | 03-09-2022 + sales_nord | 3 | Ford | St. Petersburg | 2000 | 03-05-2022 + sales_nord | 7 | May | Oslo | 1200 | 03-06-2022 + sales_nord | 9 | May | Oslo | 1200 | 03-11-2022 + sales_nord | 10 | Halder | Helsinki | 800 | 03-02-2022 + sales_west | 2 | Smirnoff | New York | 500 | 03-03-2022 + sales_west | 5 | Deev | Lisbon | 250 | 03-07-2022 + sales_west | 11 | Muller | Madrid | 650 | 03-05-2022 + sales_west | 14 | Plato | Lisbon | 950 | 03-05-2022 +(14 rows) + +-- Use indexscan for testing indexes after splitting partition +SET enable_seqscan = OFF; +EXPLAIN (COSTS OFF) SELECT * FROM sales_central WHERE sales_state = 'Warsaw'; + QUERY PLAN +----------------------------------------------------------------- + Index Scan using sales_central_sales_state_idx on sales_central + Index Cond: ((sales_state)::text = 'Warsaw'::text) +(2 rows) + +SELECT * FROM sales_central WHERE sales_state = 'Warsaw'; + salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +----------------+------------------+-------------+--------------+------------ + 4 | Ivanov | Warsaw | 750 | 03-04-2022 + 13 | Gandi | Warsaw | 150 | 03-08-2022 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; + QUERY PLAN +---------------------------------------------------------------------------- + Index Scan using sales_central_sales_state_idx on sales_central sales_list + Index Cond: ((sales_state)::text = 'Warsaw'::text) +(2 rows) + +SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; + salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +----------------+------------------+-------------+--------------+------------ + 4 | Ivanov | Warsaw | 750 | 03-04-2022 + 13 | Gandi | Warsaw | 150 | 03-08-2022 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; + QUERY PLAN +----------------------------------------------------------------------------------------- + Append + -> Index Scan using sales_east_salesperson_name_idx on sales_east sales_list_1 + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Index Scan using sales_central_salesperson_name_idx on sales_central sales_list_2 + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Heap Scan on sales_nord sales_list_3 + Recheck Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Index Scan on sales_nord_salesperson_name_idx + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Index Scan using sales_west_salesperson_name_idx on sales_west sales_list_4 + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Heap Scan on sales_others sales_list_5 + Recheck Cond: ((salesperson_name)::text = 'Ivanov'::text) + -> Bitmap Index Scan on sales_others_salesperson_name_idx + Index Cond: ((salesperson_name)::text = 'Ivanov'::text) +(15 rows) + +SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; + salesperson_id | salesperson_name | sales_state | sales_amount | sales_date +----------------+------------------+-------------+--------------+------------ + 4 | Ivanov | Warsaw | 750 | 03-04-2022 +(1 row) + +RESET enable_seqscan; +DROP TABLE sales_list; +-- +-- Test for: +-- * split DEFAULT partition to partitions with spaces between bounds; +-- * random order of partitions in SPLIT PARTITION command. +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-09'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-07'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_others DEFAULT, + PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'), + PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'), + PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'), + PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10')); +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +-----------------------+----------------+------------------+--------------+------------ + sales_apr2022_1decade | 5 | Deev | 250 | 04-07-2022 + sales_apr2022_1decade | 11 | Trump | 380 | 04-06-2022 + sales_feb2022_1decade | 2 | Smirnoff | 500 | 02-09-2022 + sales_feb2022_1decade | 6 | Poirot | 150 | 02-07-2022 + sales_jan2022_1decade | 13 | Gandi | 377 | 01-09-2022 + sales_mar2022_1decade | 7 | Li | 175 | 03-08-2022 + sales_others | 1 | May | 1000 | 01-31-2022 + sales_others | 3 | Ford | 2000 | 04-30-2022 + sales_others | 4 | Ivanov | 750 | 04-13-2022 + sales_others | 8 | Ericsson | 185 | 02-23-2022 + sales_others | 9 | Muller | 250 | 03-11-2022 + sales_others | 10 | Halder | 350 | 01-28-2022 + sales_others | 12 | Plato | 350 | 03-19-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +DROP TABLE sales_range; +-- +-- Test for: +-- * split non-DEFAULT partition to partitions with spaces between bounds; +-- * random order of partitions in SPLIT PARTITION command. +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-05-01'); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-09'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-07'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'); +ALTER TABLE sales_range SPLIT PARTITION sales_all INTO + (PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'), + PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'), + PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'), + PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'), + PARTITION sales_others DEFAULT); +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +-----------------------+----------------+------------------+--------------+------------ + sales_apr2022_1decade | 5 | Deev | 250 | 04-07-2022 + sales_apr2022_1decade | 11 | Trump | 380 | 04-06-2022 + sales_feb2022_1decade | 2 | Smirnoff | 500 | 02-09-2022 + sales_feb2022_1decade | 6 | Poirot | 150 | 02-07-2022 + sales_jan2022_1decade | 13 | Gandi | 377 | 01-09-2022 + sales_mar2022_1decade | 7 | Li | 175 | 03-08-2022 + sales_others | 1 | May | 1000 | 01-31-2022 + sales_others | 3 | Ford | 2000 | 04-30-2022 + sales_others | 4 | Ivanov | 750 | 04-13-2022 + sales_others | 8 | Ericsson | 185 | 02-23-2022 + sales_others | 9 | Muller | 250 | 03-11-2022 + sales_others | 10 | Halder | 350 | 01-28-2022 + sales_others | 12 | Plato | 350 | 03-19-2022 +(13 rows) + +DROP TABLE sales_range; +-- +-- Test for split non-DEFAULT partition to DEFAULT partition + partitions +-- with spaces between bounds. +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'); +ALTER TABLE sales_range SPLIT PARTITION sales_all INTO + (PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); +INSERT INTO sales_range VALUES (14, 'Smith', 510, '2022-05-04'); +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + tableoid | salesperson_id | salesperson_name | sales_amount | sales_date +---------------+----------------+------------------+--------------+------------ + sales_apr2022 | 3 | Ford | 2000 | 04-30-2022 + sales_apr2022 | 4 | Ivanov | 750 | 04-13-2022 + sales_apr2022 | 5 | Deev | 250 | 04-07-2022 + sales_apr2022 | 11 | Trump | 380 | 04-06-2022 + sales_feb2022 | 2 | Smirnoff | 500 | 02-10-2022 + sales_feb2022 | 6 | Poirot | 150 | 02-11-2022 + sales_feb2022 | 8 | Ericsson | 185 | 02-23-2022 + sales_jan2022 | 1 | May | 1000 | 01-31-2022 + sales_jan2022 | 10 | Halder | 350 | 01-28-2022 + sales_jan2022 | 13 | Gandi | 377 | 01-09-2022 + sales_others | 7 | Li | 175 | 03-08-2022 + sales_others | 9 | Muller | 250 | 03-11-2022 + sales_others | 12 | Plato | 350 | 03-19-2022 + sales_others | 14 | Smith | 510 | 05-04-2022 +(14 rows) + +DROP TABLE sales_range; +-- +-- Try to SPLIT partition of another table. +-- +CREATE TABLE t1(i int, t text) PARTITION BY LIST (t); +CREATE TABLE t1pa PARTITION OF t1 FOR VALUES IN ('A'); +CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t); +-- ERROR: relation "t1pa" is not a partition of relation "t2" +ALTER TABLE t2 SPLIT PARTITION t1pa INTO + (PARTITION t2a FOR VALUES FROM ('A') TO ('B'), + PARTITION t2b FOR VALUES FROM ('B') TO ('C')); +ERROR: relation "t1pa" is not a partition of relation "t2" +HINT: ALTER TABLE ... SPLIT PARTITION can only split partitions don't have sub-partitions. +DROP TABLE t2; +DROP TABLE t1; +-- +-- Try to SPLIT partition of temporary table. +-- +CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i); +CREATE TEMP TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence + FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i + WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass + ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text COLLATE "C"; + oid | pg_get_expr | relpersistence +--------+----------------------------+---------------- + tp_0_2 | FOR VALUES FROM (0) TO (2) | t +(1 row) + +-- ERROR: cannot create a permanent relation as partition of temporary relation "t" +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +ERROR: cannot create a permanent relation as partition of temporary relation "t" +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION pg_temp.tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION pg_temp.tp_1_2 FOR VALUES FROM (1) TO (2)); +-- Partitions should be temporary. +SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence + FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i + WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass + ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text COLLATE "C"; + oid | pg_get_expr | relpersistence +--------+----------------------------+---------------- + tp_0_1 | FOR VALUES FROM (0) TO (1) | t + tp_1_2 | FOR VALUES FROM (1) TO (2) | t +(2 rows) + +DROP TABLE t; +-- Check the new partitions inherit parent's tablespace +CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace) + PARTITION BY RANGE (i) TABLESPACE regress_tblspace; +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +SELECT tablename, tablespace FROM pg_tables + WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema' + ORDER BY tablename COLLATE "C", tablespace COLLATE "C"; + tablename | tablespace +-----------+------------------ + t | regress_tblspace + tp_0_1 | regress_tblspace + tp_1_2 | regress_tblspace +(3 rows) + +SELECT tablename, indexname, tablespace FROM pg_indexes + WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema' + ORDER BY tablename COLLATE "C", indexname COLLATE "C", tablespace COLLATE "C"; + tablename | indexname | tablespace +-----------+-------------+------------------ + t | t_pkey | regress_tblspace + tp_0_1 | tp_0_1_pkey | regress_tblspace + tp_1_2 | tp_1_2_pkey | regress_tblspace +(3 rows) + +DROP TABLE t; +-- Check new partitions inherits parent's table access method +CREATE ACCESS METHOD partition_split_heap TYPE TABLE HANDLER heap_tableam_handler; +CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partition_split_heap; +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +SELECT c.relname, a.amname +FROM pg_class c JOIN pg_am a ON c.relam = a.oid +WHERE c.oid IN ('t'::regclass, 'tp_0_1'::regclass, 'tp_1_2'::regclass) +ORDER BY c.relname COLLATE "C"; + relname | amname +---------+---------------------- + t | partition_split_heap + tp_0_1 | partition_split_heap + tp_1_2 | partition_split_heap +(3 rows) + +DROP TABLE t; +DROP ACCESS METHOD partition_split_heap; +-- Split partition of a temporary table when one of the partitions after +-- split has the same name as the partition being split +CREATE TEMP TABLE t (a int) PARTITION BY RANGE (a); +CREATE TEMP TABLE tp_0 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t SPLIT PARTITION tp_0 INTO + (PARTITION pg_temp.tp_0 FOR VALUES FROM (0) TO (1), + PARTITION pg_temp.tp_1 FOR VALUES FROM (1) TO (2)); +DROP TABLE t; +-- Check defaults and constraints of new partitions +CREATE TABLE t_bigint ( + b bigint, + i int DEFAULT (3+10), + j int DEFAULT 101, + k int GENERATED ALWAYS AS (b+10) STORED +) +PARTITION BY RANGE (b); +CREATE TABLE t_bigint_default PARTITION OF t_bigint DEFAULT; +-- Show defaults/constraints before SPLIT PARTITION +\d+ t_bigint + Partitioned table "partition_split_schema.t_bigint" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------------------------------------+---------+--------------+------------- + b | bigint | | | | plain | | + i | integer | | | 3 + 10 | plain | | + j | integer | | | 101 | plain | | + k | integer | | | generated always as ((b + 10)) stored | plain | | +Partition key: RANGE (b) +Partitions: + t_bigint_default DEFAULT + +\d+ t_bigint_default + Table "partition_split_schema.t_bigint_default" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------------------------------------+---------+--------------+------------- + b | bigint | | | | plain | | + i | integer | | | 3 + 10 | plain | | + j | integer | | | 101 | plain | | + k | integer | | | generated always as ((b + 10)) stored | plain | | +Partition of: t_bigint DEFAULT +No partition constraint + +ALTER TABLE t_bigint SPLIT PARTITION t_bigint_default INTO + (PARTITION t_bigint_01_10 FOR VALUES FROM (0) TO (10), + PARTITION t_bigint_default DEFAULT); +-- Show defaults/constraints after SPLIT PARTITION +\d+ t_bigint_default + Table "partition_split_schema.t_bigint_default" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------------------------------------+---------+--------------+------------- + b | bigint | | | | plain | | + i | integer | | | 3 + 10 | plain | | + j | integer | | | 101 | plain | | + k | integer | | | generated always as ((b + 10)) stored | plain | | +Partition of: t_bigint DEFAULT +Partition constraint: (NOT ((b IS NOT NULL) AND ((b >= '0'::bigint) AND (b < '10'::bigint)))) + +\d+ t_bigint_01_10 + Table "partition_split_schema.t_bigint_01_10" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------------------------------------+---------+--------------+------------- + b | bigint | | | | plain | | + i | integer | | | 3 + 10 | plain | | + j | integer | | | 101 | plain | | + k | integer | | | generated always as ((b + 10)) stored | plain | | +Partition of: t_bigint FOR VALUES FROM ('0') TO ('10') +Partition constraint: ((b IS NOT NULL) AND (b >= '0'::bigint) AND (b < '10'::bigint)) + +DROP TABLE t_bigint; +-- Test permission checks. The user needs to own the parent table and the +-- the partition to split to do the split. +CREATE ROLE regress_partition_split_alice; +CREATE ROLE regress_partition_split_bob; +GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_alice; +GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_bob; +SET SESSION AUTHORIZATION regress_partition_split_alice; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +SET SESSION AUTHORIZATION regress_partition_split_bob; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); --error +ERROR: must be owner of table t +RESET SESSION AUTHORIZATION; +ALTER TABLE t OWNER TO regress_partition_split_bob; +SET SESSION AUTHORIZATION regress_partition_split_bob; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); --error +ERROR: must be owner of table tp_0_2 +RESET SESSION AUTHORIZATION; +ALTER TABLE tp_0_2 OWNER TO regress_partition_split_bob; +SET SESSION AUTHORIZATION regress_partition_split_bob; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); --ok +RESET SESSION AUTHORIZATION; +DROP TABLE t; +-- Test: owner of new partitions should be the same as owner of split partition +CREATE TABLE t (i int) PARTITION BY RANGE (i); +SET SESSION AUTHORIZATION regress_partition_split_alice; +CREATE TABLE tp_0_2(i int); +RESET SESSION AUTHORIZATION; +ALTER TABLE t ATTACH PARTITION tp_0_2 FOR VALUES FROM (0) TO (2); +-- Owner is 'regress_partition_split_alice': +\dt tp_0_2 + List of tables + Schema | Name | Type | Owner +------------------------+--------+-------+------------------------------- + partition_split_schema | tp_0_2 | table | regress_partition_split_alice +(1 row) + +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +-- Owner should be 'regress_partition_split_alice': +\dt tp_0_1 + List of tables + Schema | Name | Type | Owner +------------------------+--------+-------+------------------------------- + partition_split_schema | tp_0_1 | table | regress_partition_split_alice +(1 row) + +\dt tp_1_2 + List of tables + Schema | Name | Type | Owner +------------------------+--------+-------+------------------------------- + partition_split_schema | tp_1_2 | table | regress_partition_split_alice +(1 row) + +DROP TABLE t; +-- Test: index of new partitions should be created with same owner as split +-- partition +SET SESSION AUTHORIZATION regress_partition_split_alice; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_10_20 PARTITION OF t FOR VALUES FROM (10) TO (20); +INSERT INTO t VALUES (11), (16); +CREATE OR REPLACE FUNCTION run_me(integer) RETURNS integer AS $$ +BEGIN + RAISE NOTICE 'you are running me as %', CURRENT_USER; + RETURN $1; +END +$$ LANGUAGE PLPGSQL IMMUTABLE; +-- Owner is 'regress_partition_split_alice': +CREATE INDEX ON t (run_me(i)); +NOTICE: you are running me as regress_partition_split_alice +NOTICE: you are running me as regress_partition_split_alice +RESET SESSION AUTHORIZATION; +-- Owner should be 'regress_partition_split_alice': +ALTER TABLE t SPLIT PARTITION tp_10_20 INTO + (PARTITION tp_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tp_15_20 FOR VALUES FROM (15) TO (20)); +NOTICE: you are running me as regress_partition_split_alice +NOTICE: you are running me as regress_partition_split_alice +DROP TABLE t; +DROP FUNCTION run_me(integer); +REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_alice; +REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_bob; +DROP ROLE regress_partition_split_alice; +DROP ROLE regress_partition_split_bob; +-- Test for hash partitioned table +CREATE TABLE t (i int) PARTITION BY HASH(i); +CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1); +-- ERROR: partition of hash-partitioned table cannot be split +ALTER TABLE t SPLIT PARTITION tp1 INTO + (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0), + PARTITION tp1_2 FOR VALUES WITH (MODULUS 4, REMAINDER 2)); +ERROR: partition of hash-partitioned table cannot be split +-- ERROR: list of new partitions should contain at least two partitions +ALTER TABLE t SPLIT PARTITION tp1 INTO + (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0)); +ERROR: list of new partitions should contain at least two partitions +DROP TABLE t; +-- Test for split partition properties: +-- * STATISTICS is empty +-- * COMMENT is empty +-- * DEFAULTS are the same as DEFAULTS for partitioned table +-- * STORAGE is the same as STORAGE for partitioned table +-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table +-- * TRIGGERS are the same as TRIGGERS for partitioned table +CREATE TABLE t +(i int NOT NULL, + t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t', + b bigint, + d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i)); +COMMENT ON COLUMN t.i IS 't1.i'; +CREATE TABLE tp_x +(i int NOT NULL, + t text STORAGE MAIN DEFAULT 'default_tp_x', + b bigint, + d date GENERATED ALWAYS as ('2022-02-02') STORED); +ALTER TABLE t ATTACH PARTITION tp_x FOR VALUES FROM (0) TO (2); +COMMENT ON COLUMN tp_x.i IS 'tp_x.i'; +CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t; +CREATE STATISTICS tp_x_stat (DEPENDENCIES) on i, b from tp_x; +ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0); +ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED; +ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID; +ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID; +INSERT INTO tp_x(i, t, b) VALUES(0, DEFAULT, 1); +INSERT INTO tp_x(i, t, b) VALUES(1, DEFAULT, 2); +CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS +$BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN new; +END; +$BODY$; +CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW + EXECUTE PROCEDURE trigger_function('t'); +CREATE TRIGGER tp_x_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW + EXECUTE PROCEDURE trigger_function('tp_x'); +\d+ tp_x + Table "partition_split_schema.tp_x" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+-------------------------------------------------+---------+--------------+------------- + i | integer | | not null | | plain | | tp_x.i + t | text | | | 'default_tp_x'::text | main | | + b | bigint | | not null | | plain | | + d | date | | | generated always as ('02-02-2022'::date) stored | plain | | +Partition of: t FOR VALUES FROM (0) TO (2) +Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 0) AND (abs(i) < 2)) +Check constraints: + "t_b_check" CHECK (b > 0) + "t_b_check1" CHECK (b > 0) NOT ENFORCED + "t_b_check2" CHECK (b > 0) NOT VALID +Statistics objects: + "partition_split_schema.tp_x_stat" (dependencies) ON i, b FROM tp_x +Not-null constraints: + "tp_x_i_not_null" NOT NULL "i" (inherited) + "t_b_nn" NOT NULL "b" (inherited) NOT VALID +Triggers: + t_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t + tp_x_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW EXECUTE FUNCTION trigger_function('tp_x') + +ALTER TABLE t SPLIT PARTITION tp_x INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_x FOR VALUES FROM (1) TO (2)); +\d+ tp_x + Table "partition_split_schema.tp_x" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+-------------------------------------------------+----------+--------------+------------- + i | integer | | not null | | plain | | + t | text | | | 'default_t'::text | extended | | + b | bigint | | not null | | plain | | + d | date | | | generated always as ('01-01-2022'::date) stored | plain | | +Partition of: t FOR VALUES FROM (1) TO (2) +Partition constraint: ((abs(i) IS NOT NULL) AND (abs(i) >= 1) AND (abs(i) < 2)) +Check constraints: + "t_b_check" CHECK (b > 0) + "t_b_check1" CHECK (b > 0) NOT ENFORCED + "t_b_check2" CHECK (b > 0) NOT VALID +Not-null constraints: + "t_i_not_null" NOT NULL "i" (inherited) + "t_b_nn" NOT NULL "b" (inherited) NOT VALID +Triggers: + t_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW EXECUTE FUNCTION trigger_function('t'), ON TABLE t + +INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3); +NOTICE: trigger(t) called: action = INSERT, when = BEFORE, level = ROW +SELECT tableoid::regclass, * FROM t ORDER BY tableoid::regclass::text COLLATE "C", b; + tableoid | i | t | b | d +----------+---+--------------+---+------------ + tp_0_1 | 0 | default_tp_x | 1 | 01-01-2022 + tp_x | 1 | default_tp_x | 2 | 01-01-2022 + tp_x | 1 | default_t | 3 | 01-01-2022 +(3 rows) + +DROP TABLE t; +DROP FUNCTION trigger_function(); +-- Test for recomputation of stored generated columns. +CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789); +INSERT INTO t VALUES (0), (1); +-- Should be 1 because partition identifier for row with i=0 is the same as +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + count +------- + 1 +(1 row) + +-- "tab_id" column (stored generated column) with "tableoid" attribute requires +-- recomputation here. +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +-- Should be 0 because partition identifier for row with i=0 is different from +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + count +------- + 0 +(1 row) + +DROP TABLE t; +RESET search_path; +-- +DROP SCHEMA partition_split_schema; +DROP SCHEMA partition_split_schema2; diff --git a/src/test/regress/expected/password_2.out b/src/test/regress/expected/password_2.out new file mode 100644 index 0000000000000..ea1fc7a083c6d --- /dev/null +++ b/src/test/regress/expected/password_2.out @@ -0,0 +1,168 @@ +-- +-- Tests for password types +-- +-- Tests for GUC password_encryption +SET password_encryption = 'novalue'; -- error +ERROR: invalid value for parameter "password_encryption": "novalue" +HINT: Available values: md5, scram-sha-256. +SET password_encryption = true; -- error +ERROR: invalid value for parameter "password_encryption": "true" +HINT: Available values: md5, scram-sha-256. +SET password_encryption = 'md5'; -- ok +SET password_encryption = 'scram-sha-256'; -- ok +-- consistency of password entries +SET password_encryption = 'md5'; +CREATE ROLE regress_passwd1; +ALTER ROLE regress_passwd1 PASSWORD 'role_pwd1'; +ERROR: password encryption failed: disabled for FIPS +CREATE ROLE regress_passwd2; +ALTER ROLE regress_passwd2 PASSWORD 'role_pwd2'; +ERROR: password encryption failed: disabled for FIPS +SET password_encryption = 'scram-sha-256'; +CREATE ROLE regress_passwd3 PASSWORD 'role_pwd3'; +CREATE ROLE regress_passwd4 PASSWORD NULL; +-- check list of created entries +-- +-- The scram secret will look something like: +-- SCRAM-SHA-256$4096:E4HxLGtnRzsYwg==$6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo= +-- +-- Since the salt is random, the exact value stored will be different on every test +-- run. Use a regular expression to mask the changing parts. +SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:$:') as rolpassword_masked + FROM pg_authid + WHERE rolname LIKE 'regress_passwd%' + ORDER BY rolname, rolpassword; + rolname | rolpassword_masked +-----------------+--------------------------------------------------- + regress_passwd1 | + regress_passwd2 | + regress_passwd3 | SCRAM-SHA-256$4096:$: + regress_passwd4 | +(4 rows) + +-- Rename a role +ALTER ROLE regress_passwd2 RENAME TO regress_passwd2_new; +-- md5 entry should have been removed +SELECT rolname, rolpassword + FROM pg_authid + WHERE rolname LIKE 'regress_passwd2_new' + ORDER BY rolname, rolpassword; + rolname | rolpassword +---------------------+------------- + regress_passwd2_new | +(1 row) + +ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2; +-- Change passwords with ALTER USER. With plaintext or already-encrypted +-- passwords. +SET password_encryption = 'md5'; +-- encrypt with MD5 +ALTER ROLE regress_passwd2 PASSWORD 'foo'; +ERROR: password encryption failed: disabled for FIPS +-- already encrypted, use as they are +ALTER ROLE regress_passwd1 PASSWORD 'md5cd3578025fe2c3d7ed1b9a9b26238b70'; +WARNING: setting an MD5-encrypted password +DETAIL: MD5 password support is deprecated and will be removed in a future release of PostgreSQL. +HINT: Refer to the PostgreSQL documentation for details about migrating to another password type. +ALTER ROLE regress_passwd3 PASSWORD 'SCRAM-SHA-256$4096:VLK4RMaQLCvNtQ==$6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; +SET password_encryption = 'scram-sha-256'; +-- create SCRAM secret +ALTER ROLE regress_passwd4 PASSWORD 'foo'; +-- already encrypted with MD5, use as it is +CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023'; +WARNING: setting an MD5-encrypted password +DETAIL: MD5 password support is deprecated and will be removed in a future release of PostgreSQL. +HINT: Refer to the PostgreSQL documentation for details about migrating to another password type. +-- This looks like a valid SCRAM-SHA-256 secret, but it is not +-- so it should be hashed with SCRAM-SHA-256. +CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234'; +-- These may look like valid MD5 secrets, but they are not, so they +-- should be hashed with SCRAM-SHA-256. +-- trailing garbage at the end +CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz'; +-- invalid length +CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz'; +-- Changing the SCRAM iteration count +SET scram_iterations = 1024; +CREATE ROLE regress_passwd9 PASSWORD 'alterediterationcount'; +SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:$:') as rolpassword_masked + FROM pg_authid + WHERE rolname LIKE 'regress_passwd%' + ORDER BY rolname, rolpassword; + rolname | rolpassword_masked +-----------------+--------------------------------------------------- + regress_passwd1 | md5cd3578025fe2c3d7ed1b9a9b26238b70 + regress_passwd2 | + regress_passwd3 | SCRAM-SHA-256$4096:$: + regress_passwd4 | SCRAM-SHA-256$4096:$: + regress_passwd5 | md5e73a4b11df52a6068f8b39f90be36023 + regress_passwd6 | SCRAM-SHA-256$4096:$: + regress_passwd7 | SCRAM-SHA-256$4096:$: + regress_passwd8 | SCRAM-SHA-256$4096:$: + regress_passwd9 | SCRAM-SHA-256$1024:$: +(9 rows) + +-- An empty password is not allowed, in any form +CREATE ROLE regress_passwd_empty PASSWORD ''; +NOTICE: empty string is not a valid password, clearing password +ALTER ROLE regress_passwd_empty PASSWORD 'md585939a5ce845f1a1b620742e3c659e0a'; +WARNING: setting an MD5-encrypted password +DETAIL: MD5 password support is deprecated and will be removed in a future release of PostgreSQL. +HINT: Refer to the PostgreSQL documentation for details about migrating to another password type. +ALTER ROLE regress_passwd_empty PASSWORD 'SCRAM-SHA-256$4096:hpFyHTUsSWcR7O9P$LgZFIt6Oqdo27ZFKbZ2nV+vtnYM995pDh9ca6WSi120=:qVV5NeluNfUPkwm7Vqat25RjSPLkGeoZBQs6wVv+um4='; +NOTICE: empty string is not a valid password, clearing password +SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty'; + rolpassword +------------- + +(1 row) + +-- Test with invalid stored and server keys. +-- +-- The first is valid, to act as a control. The others have too long +-- stored/server keys. They will be re-hashed. +CREATE ROLE regress_passwd_sha_len0 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI='; +CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96RqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI='; +CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; +-- Check that the invalid secrets were re-hashed. A re-hashed secret +-- should not contain the original salt. +SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed + FROM pg_authid + WHERE rolname LIKE 'regress_passwd_sha_len%' + ORDER BY rolname; + rolname | is_rolpassword_rehashed +-------------------------+------------------------- + regress_passwd_sha_len0 | f + regress_passwd_sha_len1 | t + regress_passwd_sha_len2 | t +(3 rows) + +-- Test that valid hashes that are too long are rejected +CREATE ROLE regress_passwd10 PASSWORD 'SCRAM-SHA-256$000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004096:wNFxNSk1hAXBkgub8py3bg==$65zC6E+R0U7tiYTC9+Wtq4Thw6gUDj3eDCINij8TflU=:rC1I7tcVugrHEY2DT0iPjGyjM4aJxkMM9n8WBxtUtHU='; +ERROR: encrypted password is too long +DETAIL: Encrypted passwords must be no longer than 512 bytes. +ALTER ROLE regress_passwd9 PASSWORD 'SCRAM-SHA-256$000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004096:wNFxNSk1hAXBkgub8py3bg==$65zC6E+R0U7tiYTC9+Wtq4Thw6gUDj3eDCINij8TflU=:rC1I7tcVugrHEY2DT0iPjGyjM4aJxkMM9n8WBxtUtHU='; +ERROR: encrypted password is too long +DETAIL: Encrypted passwords must be no longer than 512 bytes. +DROP ROLE regress_passwd1; +DROP ROLE regress_passwd2; +DROP ROLE regress_passwd3; +DROP ROLE regress_passwd4; +DROP ROLE regress_passwd5; +DROP ROLE regress_passwd6; +DROP ROLE regress_passwd7; +DROP ROLE regress_passwd8; +DROP ROLE regress_passwd9; +DROP ROLE regress_passwd_empty; +DROP ROLE regress_passwd_sha_len0; +DROP ROLE regress_passwd_sha_len1; +DROP ROLE regress_passwd_sha_len2; +-- all entries should have been removed +SELECT rolname, rolpassword + FROM pg_authid + WHERE rolname LIKE 'regress_passwd%' + ORDER BY rolname, rolpassword; + rolname | rolpassword +---------+------------- +(0 rows) + diff --git a/src/test/regress/expected/pg_dependencies.out b/src/test/regress/expected/pg_dependencies.out new file mode 100644 index 0000000000000..f619982a691d6 --- /dev/null +++ b/src/test/regress/expected/pg_dependencies.out @@ -0,0 +1,540 @@ +-- Tests for type pg_distinct +-- Invalid inputs +SELECT 'null'::pg_dependencies; +ERROR: malformed pg_dependencies: "null" +LINE 1: SELECT 'null'::pg_dependencies; + ^ +DETAIL: Unexpected scalar has been found. +SELECT '{"a": 1}'::pg_dependencies; +ERROR: malformed pg_dependencies: "{"a": 1}" +LINE 1: SELECT '{"a": 1}'::pg_dependencies; + ^ +DETAIL: Initial element must be an array. +SELECT '[]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[]" +LINE 1: SELECT '[]'::pg_dependencies; + ^ +DETAIL: Item array cannot be empty. +SELECT '{}'::pg_dependencies; +ERROR: malformed pg_dependencies: "{}" +LINE 1: SELECT '{}'::pg_dependencies; + ^ +DETAIL: Initial element must be an array. +SELECT '[null]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[null]" +LINE 1: SELECT '[null]'::pg_dependencies; + ^ +DETAIL: Item list elements cannot be null. +SELECT * FROM pg_input_error_info('null', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------+-----------------------------------+------+---------------- + malformed pg_dependencies: "null" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------+-----------------------------------+------+---------------- + malformed pg_dependencies: "{"a": 1}" | Initial element must be an array. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------+-----------------------------+------+---------------- + malformed pg_dependencies: "[]" | Item array cannot be empty. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('{}', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------+-----------------------------------+------+---------------- + malformed pg_dependencies: "{}" | Initial element must be an array. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[null]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------+------------------------------------+------+---------------- + malformed pg_dependencies: "[null]" | Item list elements cannot be null. | | 22P02 +(1 row) + +-- Invalid keys +SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes_invalid" : [2,3], "dependency" : 4}]" +LINE 1: SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]':... + ^ +DETAIL: Only allowed keys are "attributes", "dependency" and "degree". +SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" ... + ^ +DETAIL: Only allowed keys are "attributes", "dependency" and "degree". +SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "dependency" : 4}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------+----------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes_invalid" : [2,3], "dependency" : 4}]" | Only allowed keys are "attributes", "dependency" and "degree". | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------------------+----------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]" | Only allowed keys are "attributes", "dependency" and "degree". | | 22P02 +(1 row) + +-- Missing keys +SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe... + ^ +DETAIL: Item must contain "degree" key. +SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "degree" : 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_depe... + ^ +DETAIL: Item must contain "dependency" key. +SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_depe... + ^ +DETAIL: Item must contain "degree" key. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------+---------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]" | Item must contain "degree" key. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "degree" : 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------+-------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "degree" : 1.000}]" | Item must contain "dependency" key. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------+---------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4}]" | Item must contain "degree" key. | | 22P02 +(1 row) + +-- Valid keys, too many attributes +SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4... + ^ +DETAIL: The "attributes" key must contain an array of at least 1 and no more than 7 elements. +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]" | The "attributes" key must contain an array of at least 1 and no more than 7 elements. | | 22P02 +(1 row) + +-- Special characters +SELECT '[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]" +LINE 1: SELECT '[{"attributes" : ["\ud83d",3], "dependency" : 4, "de... + ^ +DETAIL: Input data must be valid JSON. +SELECT '[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "\ud83d", "de... + ^ +DETAIL: Input data must be valid JSON. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ... + ^ +DETAIL: Input data must be valid JSON. +SELECT '[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]" +LINE 1: SELECT '[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.25... + ^ +DETAIL: Input data must be valid JSON. +SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_dependencies: "[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +-- Valid keys, invalid values +SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1... + ^ +DETAIL: Unexpected scalar has been found. +SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree... + ^ +DETAIL: Attribute number array cannot be null. +SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : null, "degree... + ^ +DETAIL: Key "dependency" has an incorrect value. +SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree"... + ^ +DETAIL: Key "attributes" has an incorrect value. +SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree"... + ^ +DETAIL: Key "dependency" has an incorrect value. +SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [], "degree":... + ^ +DETAIL: Array has been found at an unexpected location. +SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [null], "degr... + ^ +DETAIL: Array has been found at an unexpected location. +SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "de... + ^ +DETAIL: Array has been found at an unexpected location. +SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.00... + ^ +DETAIL: Unexpected scalar has been found. +SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.... + ^ +DETAIL: Unexpected scalar has been found. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ... + ^ +DETAIL: Input data must be valid JSON. +SELECT * FROM pg_input_error_info('[{"attributes" : null, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : null, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------------------+----------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]" | Attribute number array cannot be null. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------------------+------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]" | Key "dependency" has an incorrect value. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------------+------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]" | Key "attributes" has an incorrect value. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------------+------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]" | Key "dependency" has an incorrect value. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]" | Array has been found at an unexpected location. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]" | Array has been found at an unexpected location. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]" | Array has been found at an unexpected location. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes": [], "dependency": 2, "degree": 1}]" +LINE 1: SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ... + ^ +DETAIL: The "attributes" key must be a non-empty array. +SELECT '[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]" +LINE 1: SELECT '[{"attributes" : {"a": 1}, "dependency" : 4, "degree... + ^ +DETAIL: Value of "attributes" must be an array of attribute numbers. +SELECT * FROM pg_input_error_info('[{"attributes": [], "dependency": 2, "degree": 1}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes": [], "dependency": 2, "degree": 1}]" | The "attributes" key must be a non-empty array. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------------------+--------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]" | Value of "attributes" must be an array of attribute numbers. | | 22P02 +(1 row) + +SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]" +LINE 1: SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependenc... + ^ +DETAIL: Item must contain "attributes" key. +SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]" +LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, ... + ^ +DETAIL: Key "dependency" has an incorrect value: 0. +SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]" +LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9,... + ^ +DETAIL: Key "dependency" has an incorrect value: -9. +SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}]' ::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]" +LINE 1: SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}... + ^ +DETAIL: Item "dependency" with value 2 has been found in the "attributes" list. +SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]" +LINE 1: SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree"... + ^ +DETAIL: Attribute lists can only contain attribute numbers. +SELECT '[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]" +LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : {}, "degree":... + ^ +DETAIL: Value of "dependency" must be an integer. +SELECT '[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]" +LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : 3, "degree": ... + ^ +DETAIL: Value of "degree" must be an integer. +SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]" +LINE 1: SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": ... + ^ +DETAIL: Key "degree" has an incorrect value. +SELECT * FROM pg_input_error_info('[{"dependency" : 4, "degree": "1.2"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------+-------------------------------------+------+---------------- + malformed pg_dependencies: "[{"dependency" : 4, "degree": "1.2"}]" | Item must contain "attributes" key. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------------------------------+---------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]" | Key "dependency" has an incorrect value: 0. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------------------------+----------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]" | Key "dependency" has an incorrect value: -9. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes": [1,2], "dependency": 2, "degree": 1}]' , 'pg_dependencies'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------------------+-------------------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes": [1,2], "dependency": 2, "degree": 1}]" | Item "dependency" with value 2 has been found in the "attributes" list. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------------+-----------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]" | Attribute lists can only contain attribute numbers. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------------+-------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]" | Value of "dependency" must be an integer. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------------+---------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]" | Value of "degree" must be an integer. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------------------+--------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]" | Key "degree" has an incorrect value. | | 22P02 +(1 row) + +-- Funky degree values, which do not fail. +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "NaN"}]'::pg_dependencies; + pg_dependencies +------------------------------------------------------- + [{"attributes": [2], "dependency": 4, "degree": NaN}] +(1 row) + +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies; + pg_dependencies +------------------------------------------------------------- + [{"attributes": [2], "dependency": 4, "degree": -Infinity}] +(1 row) + +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "inf"}]'::pg_dependencies; + pg_dependencies +------------------------------------------------------------ + [{"attributes": [2], "dependency": 4, "degree": Infinity}] +(1 row) + +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies::text::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes": [2], "dependency": 4, "degree": -Infinity}]" +DETAIL: Input data must be valid JSON. +-- Duplicated keys +SELECT '[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "attributes": [1,2], "depend... + ^ +DETAIL: Multiple "attributes" keys are not allowed. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "dependenc... + ^ +DETAIL: Multiple "dependency" keys are not allowed. +SELECT '[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency": 4, "degree": 1... + ^ +DETAIL: Multiple "degree" keys are not allowed. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------------------------------------------------+---------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]" | Multiple "attributes" keys are not allowed. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------------------------------+---------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]" | Multiple "dependency" keys are not allowed. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------------------------------------+-----------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]" | Multiple "degree" keys are not allowed. | | 22P02 +(1 row) + +-- Invalid attnums +SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]" +LINE 1: SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": ... + ^ +DETAIL: Invalid "attributes" element has been found: 0. +SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]" +LINE 1: SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree"... + ^ +DETAIL: Invalid "attributes" element has been found: -9. +SELECT * FROM pg_input_error_info('[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element has been found: 0. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------------+--------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element has been found: -9. | | 22P02 +(1 row) + +-- Duplicated attributes +SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]" +LINE 1: SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": ... + ^ +DETAIL: Invalid "attributes" element has been found: 2 cannot follow 2. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------------------------+-----------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]" | Invalid "attributes" element has been found: 2 cannot follow 2. | | 22P02 +(1 row) + +-- Duplicated attribute lists. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +ERROR: malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]" +LINE 1: SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": ... + ^ +DETAIL: Duplicated "attributes" array has been found: [2, 3] for key "dependency" and value 4. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------+------+---------------- + malformed pg_dependencies: "[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000},+| Duplicated "attributes" array has been found: [2, 3] for key "dependency" and value 4. | | 22P02 + {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]" | | | +(1 row) + +-- Valid inputs +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250}, + {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500}, + {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750}, + {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; + pg_dependencies +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + [{"attributes": [2, 3], "dependency": 4, "degree": 0.250000}, {"attributes": [2, -1], "dependency": 4, "degree": 0.500000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 0.750000}, {"attributes": [2, 3, -1, -2], "dependency": 4, "degree": 1.000000}] +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250}, + {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500}, + {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750}, + {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + message | detail | hint | sql_error_code +---------+--------+------+---------------- + | | | +(1 row) + +-- Partially-covered attribute lists, possible as items with a degree of 0 +-- are discarded. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; + pg_dependencies +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + [{"attributes": [2, 3], "dependency": 4, "degree": 1.000000}, {"attributes": [1, -1], "dependency": 4, "degree": 1.000000}, {"attributes": [2, 3, -1], "dependency": 4, "degree": 1.000000}, {"attributes": [2, 3, -1, -2], "dependency": 4, "degree": 1.000000}] +(1 row) + diff --git a/src/test/regress/expected/pg_lsn.out b/src/test/regress/expected/pg_lsn.out index b27eec7c01503..8ab59b2e44593 100644 --- a/src/test/regress/expected/pg_lsn.out +++ b/src/test/regress/expected/pg_lsn.out @@ -41,9 +41,9 @@ SELECT * FROM pg_input_error_info('16AE7F7', 'pg_lsn'); -- Min/Max aggregation SELECT MIN(f1), MAX(f1) FROM PG_LSN_TBL; - min | max ------+------------------- - 0/0 | FFFFFFFF/FFFFFFFF + min | max +------------+------------------- + 0/00000000 | FFFFFFFF/FFFFFFFF (1 row) DROP TABLE PG_LSN_TBL; @@ -85,21 +85,21 @@ SELECT '0/16AE7F8'::pg_lsn - '0/16AE7F7'::pg_lsn; (1 row) SELECT '0/16AE7F7'::pg_lsn + 16::numeric; - ?column? ------------ - 0/16AE807 + ?column? +------------ + 0/016AE807 (1 row) SELECT 16::numeric + '0/16AE7F7'::pg_lsn; - ?column? ------------ - 0/16AE807 + ?column? +------------ + 0/016AE807 (1 row) SELECT '0/16AE7F7'::pg_lsn - 16::numeric; - ?column? ------------ - 0/16AE7E7 + ?column? +------------ + 0/016AE7E7 (1 row) SELECT 'FFFFFFFF/FFFFFFFE'::pg_lsn + 1::numeric; @@ -111,9 +111,9 @@ SELECT 'FFFFFFFF/FFFFFFFE'::pg_lsn + 1::numeric; SELECT 'FFFFFFFF/FFFFFFFE'::pg_lsn + 2::numeric; -- out of range error ERROR: pg_lsn out of range SELECT '0/1'::pg_lsn - 1::numeric; - ?column? ----------- - 0/0 + ?column? +------------ + 0/00000000 (1 row) SELECT '0/1'::pg_lsn - 2::numeric; -- out of range error @@ -125,9 +125,9 @@ SELECT '0/0'::pg_lsn + ('FFFFFFFF/FFFFFFFF'::pg_lsn - '0/0'::pg_lsn); (1 row) SELECT 'FFFFFFFF/FFFFFFFF'::pg_lsn - ('FFFFFFFF/FFFFFFFF'::pg_lsn - '0/0'::pg_lsn); - ?column? ----------- - 0/0 + ?column? +------------ + 0/00000000 (1 row) SELECT '0/16AE7F7'::pg_lsn + 'NaN'::numeric; @@ -164,107 +164,107 @@ SELECT DISTINCT (i || '/' || j)::pg_lsn f generate_series(1, 5) k WHERE i <= 10 AND j > 0 AND j <= 10 ORDER BY f; - f -------- - 1/1 - 1/2 - 1/3 - 1/4 - 1/5 - 1/6 - 1/7 - 1/8 - 1/9 - 1/10 - 2/1 - 2/2 - 2/3 - 2/4 - 2/5 - 2/6 - 2/7 - 2/8 - 2/9 - 2/10 - 3/1 - 3/2 - 3/3 - 3/4 - 3/5 - 3/6 - 3/7 - 3/8 - 3/9 - 3/10 - 4/1 - 4/2 - 4/3 - 4/4 - 4/5 - 4/6 - 4/7 - 4/8 - 4/9 - 4/10 - 5/1 - 5/2 - 5/3 - 5/4 - 5/5 - 5/6 - 5/7 - 5/8 - 5/9 - 5/10 - 6/1 - 6/2 - 6/3 - 6/4 - 6/5 - 6/6 - 6/7 - 6/8 - 6/9 - 6/10 - 7/1 - 7/2 - 7/3 - 7/4 - 7/5 - 7/6 - 7/7 - 7/8 - 7/9 - 7/10 - 8/1 - 8/2 - 8/3 - 8/4 - 8/5 - 8/6 - 8/7 - 8/8 - 8/9 - 8/10 - 9/1 - 9/2 - 9/3 - 9/4 - 9/5 - 9/6 - 9/7 - 9/8 - 9/9 - 9/10 - 10/1 - 10/2 - 10/3 - 10/4 - 10/5 - 10/6 - 10/7 - 10/8 - 10/9 - 10/10 + f +------------- + 1/00000001 + 1/00000002 + 1/00000003 + 1/00000004 + 1/00000005 + 1/00000006 + 1/00000007 + 1/00000008 + 1/00000009 + 1/00000010 + 2/00000001 + 2/00000002 + 2/00000003 + 2/00000004 + 2/00000005 + 2/00000006 + 2/00000007 + 2/00000008 + 2/00000009 + 2/00000010 + 3/00000001 + 3/00000002 + 3/00000003 + 3/00000004 + 3/00000005 + 3/00000006 + 3/00000007 + 3/00000008 + 3/00000009 + 3/00000010 + 4/00000001 + 4/00000002 + 4/00000003 + 4/00000004 + 4/00000005 + 4/00000006 + 4/00000007 + 4/00000008 + 4/00000009 + 4/00000010 + 5/00000001 + 5/00000002 + 5/00000003 + 5/00000004 + 5/00000005 + 5/00000006 + 5/00000007 + 5/00000008 + 5/00000009 + 5/00000010 + 6/00000001 + 6/00000002 + 6/00000003 + 6/00000004 + 6/00000005 + 6/00000006 + 6/00000007 + 6/00000008 + 6/00000009 + 6/00000010 + 7/00000001 + 7/00000002 + 7/00000003 + 7/00000004 + 7/00000005 + 7/00000006 + 7/00000007 + 7/00000008 + 7/00000009 + 7/00000010 + 8/00000001 + 8/00000002 + 8/00000003 + 8/00000004 + 8/00000005 + 8/00000006 + 8/00000007 + 8/00000008 + 8/00000009 + 8/00000010 + 9/00000001 + 9/00000002 + 9/00000003 + 9/00000004 + 9/00000005 + 9/00000006 + 9/00000007 + 9/00000008 + 9/00000009 + 9/00000010 + 10/00000001 + 10/00000002 + 10/00000003 + 10/00000004 + 10/00000005 + 10/00000006 + 10/00000007 + 10/00000008 + 10/00000009 + 10/00000010 (100 rows) diff --git a/src/test/regress/expected/pg_ndistinct.out b/src/test/regress/expected/pg_ndistinct.out new file mode 100644 index 0000000000000..806621b2ee989 --- /dev/null +++ b/src/test/regress/expected/pg_ndistinct.out @@ -0,0 +1,447 @@ +-- Tests for type pg_ndistinct +-- Invalid inputs +SELECT 'null'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "null" +LINE 1: SELECT 'null'::pg_ndistinct; + ^ +DETAIL: Unexpected scalar has been found. +SELECT '{"a": 1}'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "{"a": 1}" +LINE 1: SELECT '{"a": 1}'::pg_ndistinct; + ^ +DETAIL: Initial element must be an array. +SELECT '[]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[]" +LINE 1: SELECT '[]'::pg_ndistinct; + ^ +DETAIL: Item array cannot be empty. +SELECT '{}'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "{}" +LINE 1: SELECT '{}'::pg_ndistinct; + ^ +DETAIL: Initial element must be an array. +SELECT '[null]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[null]" +LINE 1: SELECT '[null]'::pg_ndistinct; + ^ +DETAIL: Item list elements cannot be null. +SELECT * FROM pg_input_error_info('null', 'pg_ndistinct'); + message | detail | hint | sql_error_code +--------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "null" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "{"a": 1}" | Initial element must be an array. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------+-----------------------------+------+---------------- + malformed pg_ndistinct: "[]" | Item array cannot be empty. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('{}', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "{}" | Initial element must be an array. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[null]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +----------------------------------+------------------------------------+------+---------------- + malformed pg_ndistinct: "[null]" | Item list elements cannot be null. | | 22P02 +(1 row) + +-- Invalid keys +SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes_invalid" : [2,3], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::... + ^ +DETAIL: Only allowed keys are "attributes" and "ndistinct". +SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" :... + ^ +DETAIL: Only allowed keys are "attributes" and "ndistinct". +SELECT '[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "attributes" : [1,3], "ndist... + ^ +DETAIL: Multiple "attributes" keys are not allowed. +SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct"... + ^ +DETAIL: Multiple "ndistinct" keys are not allowed. +SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------+-----------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes_invalid" : [2,3], "ndistinct" : 4}]" | Only allowed keys are "attributes" and "ndistinct". | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------------------+-----------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]" | Only allowed keys are "attributes" and "ndistinct". | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------------------------------+---------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]" | Multiple "attributes" keys are not allowed. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------------------+--------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]" | Multiple "ndistinct" keys are not allowed. | | 22P02 +(1 row) + +-- Missing key +SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3]}]" +LINE 1: SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct; + ^ +DETAIL: Item must contain "ndistinct" key. +SELECT '[{"ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"ndistinct" : 4}]" +LINE 1: SELECT '[{"ndistinct" : 4}]'::pg_ndistinct; + ^ +DETAIL: Item must contain "attributes" key. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3]}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +----------------------------------------------------+------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3]}]" | Item must contain "ndistinct" key. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------+-------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"ndistinct" : 4}]" | Item must contain "attributes" key. | | 22P02 +(1 row) + +-- Valid keys, too many attributes +SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : ... + ^ +DETAIL: The "attributes" key must contain an array of at least 2 and no more than 8 attributes. +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]" | The "attributes" key must contain an array of at least 2 and no more than 8 attributes. | | 22P02 +(1 row) + +-- Special characters +SELECT '[{"\ud83d" : [1, 2], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"\ud83d" : [1, 2], "ndistinct" : 4}]" +LINE 1: SELECT '[{"\ud83d" : [1, 2], "ndistinct" : 4}]'::pg_ndistinc... + ^ +DETAIL: Input data must be valid JSON. +SELECT '[{"attributes" : [1, 2], "\ud83d" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [1, 2], "\ud83d" : 4}]" +LINE 1: SELECT '[{"attributes" : [1, 2], "\ud83d" : 4}]'::pg_ndistin... + ^ +DETAIL: Input data must be valid JSON. +SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]" +LINE 1: SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]'::... + ^ +DETAIL: Input data must be valid JSON. +SELECT '[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]" +LINE 1: SELECT '[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]'::... + ^ +DETAIL: Input data must be valid JSON. +SELECT * FROM pg_input_error_info('[{"\ud83d" : [1, 2], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_ndistinct: "[{"\ud83d" : [1, 2], "ndistinct" : 4}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "\ud83d" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [1, 2], "\ud83d" : 4}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------+--------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]" | Input data must be valid JSON. | | 22P02 +(1 row) + +-- Valid keys, invalid values +SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndisti... + ^ +DETAIL: Unexpected scalar has been found. +SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [], "ndistinct" : 1}]" +LINE 1: SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinc... + ^ +DETAIL: The "attributes" key must be a non-empty array. +SELECT '[{"attributes" : [2], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2], "ndistinct" : 4}]'::pg_ndistin... + ^ +DETAIL: The "attributes" key must contain an array of at least 2 and no more than 8 attributes. +SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,null], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_nd... + ^ +DETAIL: Attribute number array cannot be null. +SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_nd... + ^ +DETAIL: Key "ndistinct" has an incorrect value. +SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndi... + ^ +DETAIL: Key "attributes" has an incorrect value. +SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndi... + ^ +DETAIL: Key "ndistinct" has an incorrect value. +SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndis... + ^ +DETAIL: Array has been found at an unexpected location. +SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_... + ^ +DETAIL: Array has been found at an unexpected location. +SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::p... + ^ +DETAIL: Array has been found at an unexpected location. +SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::p... + ^ +DETAIL: Value of "ndistinct" must be an integer. +SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [0,1], "ndistinct" : 1}]" +LINE 1: SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndist... + ^ +DETAIL: Invalid "attributes" element has been found: 0. +SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]" +LINE 1: SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndi... + ^ +DETAIL: Invalid "attributes" element has been found: -9. +SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct... + ^ +DETAIL: Unexpected scalar has been found. +SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistin... + ^ +DETAIL: Unexpected scalar has been found. +SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : {"a": 1}, "ndistinct" : 1}]" +LINE 1: SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_nd... + ^ +DETAIL: Value of "attributes" must be an array of attribute numbers. +SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]" +LINE 1: SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::... + ^ +DETAIL: Attribute lists can only contain attribute numbers. +SELECT * FROM pg_input_error_info('[{"attributes" : null, "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : null, "ndistinct" : 4}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [], "ndistinct" : 1}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [], "ndistinct" : 1}]" | The "attributes" key must be a non-empty array. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------+-----------------------------------------------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2], "ndistinct" : 4}]" | The "attributes" key must contain an array of at least 2 and no more than 8 attributes. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------+----------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,null], "ndistinct" : 4}]" | Attribute number array cannot be null. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : null}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------+-----------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : null}]" | Key "ndistinct" has an incorrect value. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------+------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,"a"], "ndistinct" : 4}]" | Key "attributes" has an incorrect value. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : "a"}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------+-----------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : "a"}]" | Key "ndistinct" has an incorrect value. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : []}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : []}]" | Array has been found at an unexpected location. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [null]}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [null]}]" | Array has been found at an unexpected location. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [1,null]}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------+-------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : [1,null]}]" | Array has been found at an unexpected location. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +----------------------------------------------------------------------------+------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]" | Value of "ndistinct" must be an integer. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "ndistinct" : 1}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------+--------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [-7,-9], "ndistinct" : 1}]" | Invalid "attributes" element has been found: -9. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : 1, "ndistinct" : 4}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : "a", "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-------------------------------------------------------------------+-----------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : "a", "ndistinct" : 4}]" | Unexpected scalar has been found. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "ndistinct" : 1}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +------------------------------------------------------------------------+--------------------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : {"a": 1}, "ndistinct" : 1}]" | Value of "attributes" must be an array of attribute numbers. | | 22P02 +(1 row) + +SELECT * FROM pg_input_error_info('[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +-----------------------------------------------------------------------------+-----------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]" | Attribute lists can only contain attribute numbers. | | 22P02 +(1 row) + +-- Duplicated attributes +SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndist... + ^ +DETAIL: Invalid "attributes" element has been found: 2 cannot follow 2. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +---------------------------------------------------------------------+-----------------------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,2], "ndistinct" : 4}]" | Invalid "attributes" element has been found: 2 cannot follow 2. | | 22P02 +(1 row) + +-- Duplicated attribute lists. +SELECT '[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,3], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4}, + ^ +DETAIL: Duplicated "attributes" array has been found: [2, 3]. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------+-------------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| Duplicated "attributes" array has been found: [2, 3]. | | 22P02 + {"attributes" : [2,3], "ndistinct" : 4}]" | | | +(1 row) + +-- Partially-covered attribute lists. +SELECT '[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,-1], "ndistinct" : 4}, + {"attributes" : [2,3,-1], "ndistinct" : 4}, + {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct; +ERROR: malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,-1], "ndistinct" : 4}, + {"attributes" : [2,3,-1], "ndistinct" : 4}, + {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]" +LINE 1: SELECT '[{"attributes" : [2,3], "ndistinct" : 4}, + ^ +DETAIL: "attributes" array [2, 3] must be a subset of array [1, 3, -1, -2]. +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,-1], "ndistinct" : 4}, + {"attributes" : [2,3,-1], "ndistinct" : 4}, + {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]', 'pg_ndistinct'); + message | detail | hint | sql_error_code +--------------------------------------------------------------------+---------------------------------------------------------------------+------+---------------- + malformed pg_ndistinct: "[{"attributes" : [2,3], "ndistinct" : 4},+| "attributes" array [2, 3] must be a subset of array [1, 3, -1, -2]. | | 22P02 + {"attributes" : [2,-1], "ndistinct" : 4}, +| | | + {"attributes" : [2,3,-1], "ndistinct" : 4}, +| | | + {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]" | | | +(1 row) + +-- Valid inputs +-- Two attributes. +SELECT '[{"attributes" : [1,2], "ndistinct" : 4}]'::pg_ndistinct; + pg_ndistinct +------------------------------------------ + [{"attributes": [1, 2], "ndistinct": 4}] +(1 row) + +-- Three attributes. +SELECT '[{"attributes" : [2,-1], "ndistinct" : 1}, + {"attributes" : [3,-1], "ndistinct" : 2}, + {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct; + pg_ndistinct +-------------------------------------------------------------------------------------------------------------------------------- + [{"attributes": [2, -1], "ndistinct": 1}, {"attributes": [3, -1], "ndistinct": 2}, {"attributes": [2, 3, -1], "ndistinct": 3}] +(1 row) + +-- Three attributes with only two items. +SELECT '[{"attributes" : [2,-1], "ndistinct" : 1}, + {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct; + pg_ndistinct +--------------------------------------------------------------------------------------- + [{"attributes": [2, -1], "ndistinct": 1}, {"attributes": [2, 3, -1], "ndistinct": 3}] +(1 row) + diff --git a/src/test/regress/expected/plancache.out b/src/test/regress/expected/plancache.out index 4e59188196c96..d58534ca1cd64 100644 --- a/src/test/regress/expected/plancache.out +++ b/src/test/regress/expected/plancache.out @@ -279,10 +279,14 @@ execute pstmt_def_insert(1); drop table pc_list_parted, pc_list_part_null; deallocate pstmt_def_insert; -- Test plan_cache_mode -create table test_mode (a int); +create table test_mode (a int) with (autovacuum_enabled = false); insert into test_mode select 1 from generate_series(1,1000) union all select 2; -create index on test_mode (a); +-- ANALYZE before creating the index. CREATE INDEX scans the table, which may +-- set pages all-visible via on-access pruning. If relallvisible is then updated +-- by ANALYZE, the generic plan may pick an index-only scan instead of the +-- expected sequential scan. analyze test_mode; +create index on test_mode (a); prepare test_mode_pp (int) as select count(*) from test_mode where a = $1; select name, generic_plans, custom_plans from pg_prepared_statements where name = 'test_mode_pp'; diff --git a/src/test/regress/expected/planner_est.out b/src/test/regress/expected/planner_est.out new file mode 100644 index 0000000000000..b62a47552fad4 --- /dev/null +++ b/src/test/regress/expected/planner_est.out @@ -0,0 +1,213 @@ +-- +-- Tests for testing query planner selectivity and width estimates +-- +-- Most selectivity and width estimations rely too heavily on statistics +-- gathered by ANALYZE, or could vary depending on hardware. However, there +-- are a few cases where we can have more certainty about the expected number +-- of rows, or width of rows. This is a good home for such tests. +-- +-- Function to assist with verifying EXPLAIN which includes costs. A series +-- of bool flags allows control over which portions are masked out +CREATE FUNCTION explain_mask_costs(query text, do_analyze bool, + hide_costs bool, hide_row_est bool, hide_width bool) RETURNS setof text +LANGUAGE plpgsql AS +$$ +DECLARE + ln text; + analyze_str text; +BEGIN + IF do_analyze = true THEN + analyze_str := 'on'; + ELSE + analyze_str := 'off'; + END IF; + + -- avoid jit related output by disabling it + SET LOCAL jit = 0; + + FOR ln IN + EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s', + analyze_str, query) + LOOP + IF hide_costs = true THEN + ln := regexp_replace(ln, 'cost=\d+\.\d\d\.\.\d+\.\d\d', 'cost=N..N'); + END IF; + + IF hide_row_est = true THEN + -- don't use 'g' so that we leave the actual rows intact + ln := regexp_replace(ln, 'rows=\d+', 'rows=N'); + END IF; + + IF hide_width = true THEN + ln := regexp_replace(ln, 'width=\d+', 'width=N'); + END IF; + + RETURN NEXT ln; + END LOOP; +END; +$$; +-- +-- Test the SupportRequestRows support function for generate_series_timestamp() +-- +-- Ensure the row estimate matches the actual rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day') g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) +(1 row) + +-- As above but with generate_series_timestamp +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMP '2024-02-01', TIMESTAMP '2024-03-01', INTERVAL '1 day') g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) +(1 row) + +-- As above but with generate_series_timestamptz_at_zone() +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day', 'UTC') g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) +(1 row) + +-- Ensure the estimated and actual row counts match when the range isn't +-- evenly divisible by the step +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '7 day') g(s);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=5 width=N) (actual rows=5.00 loops=1) +(1 row) + +-- Ensure the estimates match when step is decreasing +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '-1 day') g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=30 width=N) (actual rows=30.00 loops=1) +(1 row) + +-- Ensure an empty range estimates 1 row +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '1 day') g(s);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) +(1 row) + +-- Ensure we get the default row estimate for infinity values +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '-infinity', TIMESTAMPTZ 'infinity', INTERVAL '1 day') g(s);$$, +false, true, false, true); + explain_mask_costs +------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1000 width=N) +(1 row) + +-- Ensure the row estimate behaves correctly when step size is zero. +-- We expect generate_series_timestamp() to throw the error rather than in +-- the support function. +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '0 day') g(s); +ERROR: step size cannot equal zero +-- +-- Test the SupportRequestRows support function for generate_series_numeric() +-- +-- Ensure the row estimate matches the actual rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1.0, 25.0) g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1) +(1 row) + +-- As above but with non-default step +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1.0, 25.0, 2.0) g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=13 width=N) (actual rows=13.00 loops=1) +(1 row) + +-- Ensure the estimates match when step is decreasing +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25.0, 1.0, -1.0) g(s);$$, +true, true, false, true); + explain_mask_costs +--------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=25 width=N) (actual rows=25.00 loops=1) +(1 row) + +-- Ensure an empty range estimates 1 row +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25.0, 1.0, 1.0) g(s);$$, +true, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1 width=N) (actual rows=0.00 loops=1) +(1 row) + +-- Ensure we get the default row estimate for error cases (infinity/NaN values +-- and zero step size) +SELECT explain_mask_costs($$ +SELECT * FROM generate_series('-infinity'::NUMERIC, 'infinity'::NUMERIC, 1.0) g(s);$$, +false, true, false, true); + explain_mask_costs +------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1000 width=N) +(1 row) + +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1.0, 25.0, 'NaN'::NUMERIC) g(s);$$, +false, true, false, true); + explain_mask_costs +------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1000 width=N) +(1 row) + +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$, +false, true, false, true); + explain_mask_costs +------------------------------------------------------------------- + Function Scan on generate_series g (cost=N..N rows=1000 width=N) +(1 row) + +-- +-- Test ScalarArrayOpExpr row estimates for <> ALL for arrays with NULLs. We +-- expect the planner to estimate 1 row will match in both of the following +-- tests. +-- +-- Try a const array containing a NULL +SELECT explain_mask_costs($$ +SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 99, NULL]);$$, +false, true, false, true); + explain_mask_costs +--------------------------------------------------------- + Seq Scan on tenk1 (cost=N..N rows=1 width=N) + Filter: (unique1 <> ALL ('{1,2,99,NULL}'::integer[])) +(2 rows) + +-- Try a non-const array containing a NULL +SELECT explain_mask_costs($$ +SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 98, (SELECT 99), NULL]);$$, +false, true, false, true); + explain_mask_costs +------------------------------------------------------------------------------------- + Seq Scan on tenk1 (cost=N..N rows=1 width=N) + Filter: (unique1 <> ALL (ARRAY[1, 2, 98, (InitPlan expr_1).col1, NULL::integer])) + InitPlan expr_1 + -> Result (cost=N..N rows=1 width=N) +(4 rows) + +DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index d8ce39dba3c16..b37b2abaf8057 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -1763,7 +1763,8 @@ select f1(point(3,4)); -- fail for lack of + operator ERROR: operator does not exist: point + integer LINE 1: x + 1 ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. QUERY: x + 1 CONTEXT: PL/pgSQL function f1(anyelement) line 3 at RETURN drop function f1(x anyelement); @@ -1848,7 +1849,8 @@ select f1(int4range(42, 49), 11, 4.5) as fail; -- range type doesn't fit ERROR: function f1(int4range, integer, numeric) does not exist LINE 1: select f1(int4range(42, 49), 11, 4.5) as fail; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function f1(x anycompatiblerange, y anycompatible, z anycompatible); -- fail, can't infer type: create function f1(x anycompatible) returns anycompatiblerange as $$ @@ -1902,7 +1904,8 @@ select x, pg_typeof(x), y, pg_typeof(y) ERROR: function f1(integer, numeric[], integer, numeric) does not exist LINE 2: from f1(11, array[1, 2.2], 42, 34.5); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function f1(a anyelement, b anyarray, c anycompatible, d anycompatible); -- @@ -3072,7 +3075,7 @@ select shadowtest(1); ERROR: function shadowtest(integer) does not exist LINE 1: select shadowtest(1); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no function of that name. reset plpgsql.extra_errors; reset plpgsql.extra_warnings; create or replace function shadowtest(f1 int) @@ -4573,51 +4576,6 @@ CONTEXT: PL/pgSQL expression "1/0" PL/pgSQL function fail() line 3 at RETURN drop function fail(); -- Test handling of string literals. -set standard_conforming_strings = off; -create or replace function strtest() returns text as $$ -begin - raise notice 'foo\\bar\041baz'; - return 'foo\\bar\041baz'; -end -$$ language plpgsql; -WARNING: nonstandard use of \\ in a string literal -LINE 3: raise notice 'foo\\bar\041baz'; - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 4: return 'foo\\bar\041baz'; - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 4: return 'foo\\bar\041baz'; - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -select strtest(); -NOTICE: foo\bar!baz -WARNING: nonstandard use of \\ in a string literal -LINE 1: 'foo\\bar\041baz' - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -QUERY: 'foo\\bar\041baz' - strtest -------------- - foo\bar!baz -(1 row) - -create or replace function strtest() returns text as $$ -begin - raise notice E'foo\\bar\041baz'; - return E'foo\\bar\041baz'; -end -$$ language plpgsql; -select strtest(); -NOTICE: foo\bar!baz - strtest -------------- - foo\bar!baz -(1 row) - -set standard_conforming_strings = on; create or replace function strtest() returns text as $$ begin raise notice 'foo\\bar\041baz\'; diff --git a/src/test/regress/expected/polymorphism.out b/src/test/regress/expected/polymorphism.out index 94eedfe375eea..4f8447ec0d7f2 100644 --- a/src/test/regress/expected/polymorphism.out +++ b/src/test/regress/expected/polymorphism.out @@ -15,7 +15,8 @@ select polyf(point(3,4)); -- fail for lack of + operator ERROR: operator does not exist: point + integer LINE 2: select x + 1 ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. QUERY: select x + 1 @@ -95,7 +96,8 @@ select polyf(int4range(42, 49), 11, 4.5) as fail; -- range type doesn't fit ERROR: function polyf(int4range, integer, numeric) does not exist LINE 1: select polyf(int4range(42, 49), 11, 4.5) as fail; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function polyf(x anycompatiblerange, y anycompatible, z anycompatible); create function polyf(x anycompatiblemultirange, y anycompatible, z anycompatible) returns anycompatiblearray as $$ select array[lower(x), upper(x), y, z] @@ -110,7 +112,8 @@ select polyf(multirange(int4range(42, 49)), 11, 4.5) as fail; -- range type doe ERROR: function polyf(int4multirange, integer, numeric) does not exist LINE 1: select polyf(multirange(int4range(42, 49)), 11, 4.5) as fail... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function polyf(x anycompatiblemultirange, y anycompatible, z anycompatible); -- fail, can't infer type: create function polyf(x anycompatible) returns anycompatiblerange as $$ @@ -176,7 +179,8 @@ select x, pg_typeof(x), y, pg_typeof(y) ERROR: function polyf(integer, numeric[], integer, numeric) does not exist LINE 2: from polyf(11, array[1, 2.2], 42, 34.5); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function polyf(a anyelement, b anyarray, c anycompatible, d anycompatible); create function polyf(anyrange) returns anymultirange @@ -990,7 +994,7 @@ select myleast(); -- fail ERROR: function myleast() does not exist LINE 1: select myleast(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given number of arguments. -- test with variadic call parameter select myleast(variadic array[1,2,3,4,-1]); myleast @@ -1060,17 +1064,20 @@ select formarray(1.1, array[1.2,55.5]); -- fail without variadic ERROR: function formarray(numeric, numeric[]) does not exist LINE 1: select formarray(1.1, array[1.2,55.5]); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select formarray(1, 'x'::text); -- fail, type mismatch ERROR: function formarray(integer, text) does not exist LINE 1: select formarray(1, 'x'::text); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select formarray(1, variadic array['x'::text]); -- fail, type mismatch ERROR: function formarray(integer, text[]) does not exist LINE 1: select formarray(1, variadic array['x'::text]); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function formarray(anyelement, variadic anyarray); -- test pg_typeof() function select pg_typeof(null); -- unknown @@ -1154,7 +1161,7 @@ select dfunc(10, 20, 30); -- fail ERROR: function dfunc(integer, integer, integer) does not exist LINE 1: select dfunc(10, 20, 30); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given number of arguments. drop function dfunc(); -- fail ERROR: function dfunc() does not exist drop function dfunc(int); -- fail @@ -1203,7 +1210,8 @@ select dfunc(); -- fail: which dfunc should be called? int or text ERROR: function dfunc() is not unique LINE 1: select dfunc(); ^ -HINT: Could not choose a best candidate function. You might need to add explicit type casts. +DETAIL: Could not choose a best candidate function. +HINT: You might need to add explicit type casts. select dfunc('Hi'); -- ok dfunc ----------- @@ -1242,17 +1250,20 @@ select dfunc(); -- fail ERROR: function dfunc() is not unique LINE 1: select dfunc(); ^ -HINT: Could not choose a best candidate function. You might need to add explicit type casts. +DETAIL: Could not choose a best candidate function. +HINT: You might need to add explicit type casts. select dfunc(1); -- fail ERROR: function dfunc(integer) is not unique LINE 1: select dfunc(1); ^ -HINT: Could not choose a best candidate function. You might need to add explicit type casts. +DETAIL: Could not choose a best candidate function. +HINT: You might need to add explicit type casts. select dfunc(1, 2); -- fail ERROR: function dfunc(integer, integer) is not unique LINE 1: select dfunc(1, 2); ^ -HINT: Could not choose a best candidate function. You might need to add explicit type casts. +DETAIL: Could not choose a best candidate function. +HINT: You might need to add explicit type casts. select dfunc(1, 2, 3); -- ok dfunc ------- @@ -1310,7 +1321,7 @@ select dfunc(); -- fail ERROR: function dfunc() does not exist LINE 1: select dfunc(); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given number of arguments. select dfunc(10); dfunc ------- @@ -1371,7 +1382,8 @@ select dfunc(1); -- fail ERROR: function dfunc(integer) is not unique LINE 1: select dfunc(1); ^ -HINT: Could not choose a best candidate function. You might need to add explicit type casts. +DETAIL: Could not choose a best candidate function. +HINT: You might need to add explicit type casts. -- but this works since the ambiguous functions aren't preferred anyway select dfunc('Hi'); dfunc @@ -1417,7 +1429,7 @@ select * from dfunc(0); -- fail ERROR: function dfunc(integer) does not exist LINE 1: select * from dfunc(0); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given number of arguments. select * from dfunc(1,2); a | b | c | d ---+---+---+--- @@ -1448,18 +1460,72 @@ select * from dfunc(x := 10, b := 20, c := 30); -- fail, unknown param ERROR: function dfunc(x => integer, b => integer, c => integer) does not exist LINE 1: select * from dfunc(x := 10, b := 20, c := 30); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument names. select * from dfunc(10, 10, a := 20); -- fail, a overlaps positional parameter ERROR: function dfunc(integer, integer, a => integer) does not exist LINE 1: select * from dfunc(10, 10, a := 20); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: In the closest available match, an argument was specified both positionally and by name. select * from dfunc(1,c := 2,d := 3); -- fail, no value for b ERROR: function dfunc(integer, c => integer, d => integer) does not exist LINE 1: select * from dfunc(1,c := 2,d := 3); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: In the closest available match, not all required arguments were supplied. drop function dfunc(int, int, int, int); +create function xleast(x numeric, variadic arr numeric[]) + returns numeric as $$ + select least(x, min(arr[i])) from generate_subscripts(arr, 1) g(i); +$$ language sql; +select xleast(x => 1, variadic arr => array[2,3]); + xleast +-------- + 1 +(1 row) + +select xleast(1, variadic arr => array[2,3]); + xleast +-------- + 1 +(1 row) + +set search_path = pg_catalog; +select xleast(1, variadic arr => array[2,3]); -- wrong schema +ERROR: function xleast(integer, arr => integer[]) does not exist +LINE 1: select xleast(1, variadic arr => array[2,3]); + ^ +DETAIL: A function of that name exists, but it is not in the search_path. +reset search_path; +select xleast(foo => 1, variadic arr => array[2,3]); -- wrong argument name +ERROR: function xleast(foo => integer, arr => integer[]) does not exist +LINE 1: select xleast(foo => 1, variadic arr => array[2,3]); + ^ +DETAIL: No function of that name accepts the given argument names. +select xleast(x => 1, variadic array[2,3]); -- misuse of mixed notation +ERROR: positional argument cannot follow named argument +LINE 1: select xleast(x => 1, variadic array[2,3]); + ^ +select xleast(1, variadic x => array[2,3]); -- misuse of mixed notation +ERROR: function xleast(integer, x => integer[]) does not exist +LINE 1: select xleast(1, variadic x => array[2,3]); + ^ +DETAIL: In the closest available match, an argument was specified both positionally and by name. +select xleast(arr => array[1], variadic x => 3); -- wrong arg is VARIADIC +ERROR: function xleast(arr => integer[], x => integer) does not exist +LINE 1: select xleast(arr => array[1], variadic x => 3); + ^ +HINT: The VARIADIC parameter must be placed last, even when using argument names. +select xleast(arr => array[1], x => 3); -- failed to use VARIADIC +ERROR: function xleast(arr => integer[], x => integer) does not exist +LINE 1: select xleast(arr => array[1], x => 3); + ^ +HINT: This call would be correct if the variadic array were labeled VARIADIC and placed last. +select xleast(arr => 1, variadic x => array[2,3]); -- mixed-up args +ERROR: function xleast(arr => integer, x => integer[]) does not exist +LINE 1: select xleast(arr => 1, variadic x => array[2,3]); + ^ +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. +drop function xleast(x numeric, variadic arr numeric[]); -- test with different parameter types create function dfunc(a varchar, b numeric, c date = current_date) returns table (a varchar, b numeric, c date) as $$ @@ -1499,7 +1565,8 @@ select * from dfunc('Hello World', c := 20, b := '2009-07-25'::date); -- fail ERROR: function dfunc(unknown, c => integer, b => date) does not exist LINE 1: select * from dfunc('Hello World', c := 20, b := '2009-07-25... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function dfunc(varchar, numeric, date); -- test out parameters with named params create function dfunc(a varchar = 'def a', out _a varchar, c numeric = NULL, out _c numeric) @@ -1844,7 +1911,8 @@ select x, pg_typeof(x) from anyctest(11, point(1,2)) x; -- fail ERROR: function anyctest(integer, point) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, point(1,2)) x; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select x, pg_typeof(x) from anyctest('11', '12.3') x; -- defaults to text x | pg_typeof ------+----------- @@ -1872,7 +1940,8 @@ select x, pg_typeof(x) from anyctest(11, array[1,2]) x; -- fail ERROR: function anyctest(integer, integer[]) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, array[1,2]) x; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyctest(anycompatible, anycompatible); create function anyctest(anycompatible, anycompatiblearray) returns anycompatiblearray as $$ @@ -1906,12 +1975,14 @@ select x, pg_typeof(x) from anyctest(11, array[point(1,2)]) x; -- fail ERROR: function anyctest(integer, point[]) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, array[point(1,2)]) ... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select x, pg_typeof(x) from anyctest(11, 12) x; -- fail ERROR: function anyctest(integer, integer) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, 12) x; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyctest(anycompatible, anycompatiblearray); create function anyctest(anycompatible, anycompatiblerange) returns anycompatiblerange as $$ @@ -1933,12 +2004,14 @@ select x, pg_typeof(x) from anyctest(11, 12) x; -- fail ERROR: function anyctest(integer, integer) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, 12) x; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select x, pg_typeof(x) from anyctest(11.2, int4range(4,7)) x; -- fail ERROR: function anyctest(numeric, int4range) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11.2, int4range(4,7)) x... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select x, pg_typeof(x) from anyctest(11.2, '[4,7)') x; -- fail ERROR: could not determine polymorphic type anycompatiblerange because input has type unknown drop function anyctest(anycompatible, anycompatiblerange); @@ -1956,7 +2029,8 @@ select x, pg_typeof(x) from anyctest(int4range(11,12), numrange(4,7)) x; -- fail ERROR: function anyctest(int4range, numrange) does not exist LINE 1: select x, pg_typeof(x) from anyctest(int4range(11,12), numra... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyctest(anycompatiblerange, anycompatiblerange); -- fail, can't infer result type: create function anyctest(anycompatible) @@ -1985,12 +2059,14 @@ select x, pg_typeof(x) from anyctest(11, 12) x; -- fail ERROR: function anyctest(integer, integer) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, 12) x; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select x, pg_typeof(x) from anyctest(11.2, multirange(int4range(4,7))) x; -- fail ERROR: function anyctest(numeric, int4multirange) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11.2, multirange(int4ra... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select x, pg_typeof(x) from anyctest(11.2, '{[4,7)}') x; -- fail ERROR: could not determine polymorphic type anycompatiblemultirange because input has type unknown drop function anyctest(anycompatible, anycompatiblemultirange); @@ -2008,7 +2084,8 @@ select x, pg_typeof(x) from anyctest(multirange(int4range(11,12)), multirange(nu ERROR: function anyctest(int4multirange, nummultirange) does not exist LINE 1: select x, pg_typeof(x) from anyctest(multirange(int4range(11... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyctest(anycompatiblemultirange, anycompatiblemultirange); -- fail, can't infer result type: create function anyctest(anycompatible) @@ -2037,7 +2114,8 @@ select x, pg_typeof(x) from anyctest(array[11], array[1,2]) x; -- fail ERROR: function anyctest(integer[], integer[]) does not exist LINE 1: select x, pg_typeof(x) from anyctest(array[11], array[1,2]) ... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyctest(anycompatiblenonarray, anycompatiblenonarray); create function anyctest(a anyelement, b anyarray, c anycompatible, d anycompatible) @@ -2066,7 +2144,8 @@ select x, pg_typeof(x) from anyctest(11, array[1, 2.2], 42, 34.5) x; -- fail ERROR: function anyctest(integer, numeric[], integer, numeric) does not exist LINE 1: select x, pg_typeof(x) from anyctest(11, array[1, 2.2], 42, ... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyctest(a anyelement, b anyarray, c anycompatible, d anycompatible); create function anyctest(variadic anycompatiblearray) diff --git a/src/test/regress/expected/portals.out b/src/test/regress/expected/portals.out index 06726ed4ab7ae..a66f27e36949e 100644 --- a/src/test/regress/expected/portals.out +++ b/src/test/regress/expected/portals.out @@ -1336,6 +1336,17 @@ FETCH FROM c1; DELETE FROM ucview WHERE CURRENT OF c1; -- fail, views not supported ERROR: WHERE CURRENT OF on a view is not implemented ROLLBACK; +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM ucview; +FETCH FROM c1; + f1 | f2 +----+------- + 13 | three +(1 row) + +UPDATE ucview SET f1 = f1 + 10 WHERE CURRENT OF c1; -- fail, views not supported +ERROR: WHERE CURRENT OF on a view is not implemented +ROLLBACK; -- Check WHERE CURRENT OF with an index-only scan BEGIN; EXPLAIN (costs off) @@ -1472,18 +1483,18 @@ rollback; -- Check handling of non-backwards-scan-capable plans with scroll cursors begin; explain (costs off) declare c1 cursor for select (select 42) as x; - QUERY PLAN ----------------- + QUERY PLAN +------------------- Result - InitPlan 1 + InitPlan expr_1 -> Result (3 rows) explain (costs off) declare c1 scroll cursor for select (select 42) as x; - QUERY PLAN ----------------- + QUERY PLAN +------------------- Materialize - InitPlan 1 + InitPlan expr_1 -> Result -> Result (4 rows) diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out index b79037748b7e6..feae77cb84097 100644 --- a/src/test/regress/expected/predicate.out +++ b/src/test/regress/expected/predicate.out @@ -36,8 +36,9 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL; QUERY PLAN -------------------------- Result + Replaces: Scan on t One-Time Filter: false -(2 rows) +(3 rows) -- Ensure the IS_NULL qual is not reduced to constant-FALSE on nullable -- columns @@ -77,17 +78,18 @@ SELECT * FROM pred_tab t WHERE t.a IS NULL OR t.c IS NULL; QUERY PLAN -------------------------- Result + Replaces: Scan on t One-Time Filter: false -(2 rows) +(3 rows) -- Ensure the OR clause is not reduced to constant-FALSE when not all branches -- are provably false EXPLAIN (COSTS OFF) SELECT * FROM pred_tab t WHERE t.b IS NULL OR t.c IS NULL; - QUERY PLAN ----------------------------------------- + QUERY PLAN +------------------------ Seq Scan on pred_tab t - Filter: ((b IS NULL) OR (c IS NULL)) + Filter: (b IS NULL) (2 rows) -- @@ -139,8 +141,9 @@ SELECT * FROM pred_tab t1 Join Filter: false -> Seq Scan on pred_tab t1 -> Result + Replaces: Scan on t2 One-Time Filter: false -(5 rows) +(6 rows) -- Ensure the IS_NULL qual is not reduced to constant-FALSE when the column is -- nullable by an outer join @@ -209,8 +212,9 @@ SELECT * FROM pred_tab t1 Join Filter: false -> Seq Scan on pred_tab t1 -> Result + Replaces: Scan on t2 One-Time Filter: false -(5 rows) +(6 rows) -- Ensure the OR clause is not reduced to constant-FALSE when a column is -- made nullable from an outer join @@ -231,6 +235,55 @@ SELECT * FROM pred_tab t1 -> Seq Scan on pred_tab t3 (9 rows) +-- +-- Tests for NullTest reduction in EXISTS sublink +-- +-- Ensure the IS_NOT_NULL qual is ignored +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 + LEFT JOIN pred_tab t2 ON EXISTS + (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6 + WHERE t1.a = t3.a AND t6.a IS NOT NULL); + QUERY PLAN +--------------------------------------------------------- + Nested Loop Left Join + Join Filter: EXISTS(SubPlan exists_1) + -> Seq Scan on pred_tab t1 + -> Materialize + -> Seq Scan on pred_tab t2 + SubPlan exists_1 + -> Nested Loop + -> Nested Loop + -> Nested Loop + -> Seq Scan on pred_tab t4 + -> Materialize + -> Seq Scan on pred_tab t3 + Filter: (t1.a = a) + -> Materialize + -> Seq Scan on pred_tab t5 + -> Materialize + -> Seq Scan on pred_tab t6 +(17 rows) + +-- Ensure the IS_NULL qual is reduced to constant-FALSE +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 + LEFT JOIN pred_tab t2 ON EXISTS + (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6 + WHERE t1.a = t3.a AND t6.a IS NULL); + QUERY PLAN +-------------------------------------------- + Nested Loop Left Join + Join Filter: (InitPlan exists_1).col1 + InitPlan exists_1 + -> Result + Replaces: Join on t3, t4, t5, t6 + One-Time Filter: false + -> Seq Scan on pred_tab t1 + -> Materialize + -> Seq Scan on pred_tab t2 +(9 rows) + DROP TABLE pred_tab; -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance -- parents. @@ -361,3 +414,543 @@ SELECT * FROM pred_tab t1 DROP TABLE pred_tab; DROP TABLE pred_tab_notnull; +-- Validate that NullTest quals in constraint expressions are reduced correctly +CREATE TABLE pred_tab1 (a int NOT NULL, b int, + CONSTRAINT check_tab1 CHECK (a IS NULL OR b > 2)); +CREATE TABLE pred_tab2 (a int, b int, + CONSTRAINT check_a CHECK (a IS NOT NULL)); +SET constraint_exclusion TO ON; +-- Ensure that we get a dummy plan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL; + QUERY PLAN +------------------------------------------ + Result + Replaces: Join on pred_tab1, pred_tab2 + One-Time Filter: false +(3 rows) + +-- Ensure that we get a dummy plan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2; + QUERY PLAN +------------------------------------------ + Result + Replaces: Join on pred_tab2, pred_tab1 + One-Time Filter: false +(3 rows) + +RESET constraint_exclusion; +DROP TABLE pred_tab1; +DROP TABLE pred_tab2; +-- Validate that NullTest quals in index expressions and predicate are reduced correctly +CREATE TABLE pred_tab (a int, b int NOT NULL, c int NOT NULL); +INSERT INTO pred_tab SELECT i, i, i FROM generate_series(1, 1000) i; +CREATE INDEX pred_tab_exprs_idx ON pred_tab ((a < 5 AND b IS NOT NULL AND c IS NOT NULL)); +CREATE INDEX pred_tab_pred_idx ON pred_tab (a) WHERE b IS NOT NULL AND c IS NOT NULL; +ANALYZE pred_tab; +-- Ensure that index pred_tab_exprs_idx is used +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE; + QUERY PLAN +------------------------------------------------- + Index Scan using pred_tab_exprs_idx on pred_tab + Index Cond: ((a < 5) = true) +(2 rows) + +SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE; + a | b | c +---+---+--- + 1 | 1 | 1 + 2 | 2 | 2 + 3 | 3 | 3 + 4 | 4 | 4 +(4 rows) + +-- Ensure that index pred_tab_pred_idx is used +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL; + QUERY PLAN +------------------------------------------------ + Index Scan using pred_tab_pred_idx on pred_tab + Index Cond: (a < 3) +(2 rows) + +SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL; + a | b | c +---+---+--- + 1 | 1 | 1 + 2 | 2 | 2 +(2 rows) + +DROP TABLE pred_tab; +-- +-- Test that COALESCE expressions in predicates are simplified using +-- non-nullable arguments. +-- +CREATE TABLE pred_tab (a int NOT NULL, b int, c int); +-- Ensure that constant NULL arguments are dropped +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1; + QUERY PLAN +-------------------------------- + Seq Scan on pred_tab + Filter: (COALESCE(b, a) > 1) +(2 rows) + +-- Ensure that argument "b*a" is dropped +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1; + QUERY PLAN +-------------------------------- + Seq Scan on pred_tab + Filter: (COALESCE(b, a) > 1) +(2 rows) + +-- Ensure that the entire COALESCE expression is replaced by "a" +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1; + QUERY PLAN +---------------------- + Seq Scan on pred_tab + Filter: (a > 1) +(2 rows) + +-- +-- Test detection of non-nullable expressions in predicates +-- +-- CoalesceExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL; + QUERY PLAN +------------------------------------ + Seq Scan on pred_tab + Filter: (COALESCE(b, c) IS NULL) +(2 rows) + +-- MinMaxExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL; + QUERY PLAN +------------------------------------ + Seq Scan on pred_tab + Filter: (GREATEST(b, c) IS NULL) +(2 rows) + +-- CaseExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL; + QUERY PLAN +--------------------------------------------------------- + Seq Scan on pred_tab + Filter: (CASE WHEN (c > 0) THEN b ELSE a END IS NULL) +(2 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL; + QUERY PLAN +--------------------------------------------------------------------- + Seq Scan on pred_tab + Filter: (CASE WHEN (c > 0) THEN a ELSE NULL::integer END IS NULL) +(2 rows) + +-- ArrayExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +-- NullTest +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +-- BooleanTest +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +-- DistinctExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +-- RelabelType +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a::oid) IS NULL; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on pred_tab + One-Time Filter: false +(3 rows) + +DROP TABLE pred_tab; +-- +-- Test optimization of IS [NOT] DISTINCT FROM +-- +CREATE TYPE dist_row_t AS (a int, b int); +CREATE TABLE dist_tab (id int, val_nn int NOT NULL, val_null int, row_nn dist_row_t NOT NULL); +INSERT INTO dist_tab VALUES (1, 10, 10, ROW(1, 1)); +INSERT INTO dist_tab VALUES (2, 20, NULL, ROW(2, 2)); +INSERT INTO dist_tab VALUES (3, 30, 30, ROW(1, NULL)); +CREATE INDEX dist_tab_nn_idx ON dist_tab (val_nn); +ANALYZE dist_tab; +-- Ensure that the predicate folds to constant TRUE +EXPLAIN(COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM NULL::INT; + QUERY PLAN +---------------------- + Seq Scan on dist_tab +(1 row) + +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM NULL::INT; + id +---- + 1 + 2 + 3 +(3 rows) + +-- Ensure that the predicate folds to constant FALSE +EXPLAIN(COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM NULL::INT; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on dist_tab + One-Time Filter: false +(3 rows) + +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM NULL::INT; + id +---- +(0 rows) + +-- Ensure that the predicate is converted to an inequality operator +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM 10; + QUERY PLAN +-------------------------- + Seq Scan on dist_tab + Filter: (val_nn <> 10) +(2 rows) + +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM 10; + id +---- + 2 + 3 +(2 rows) + +-- Ensure that the predicate is converted to an equality operator, and thus can +-- use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM 10; + QUERY PLAN +---------------------------------------------- + Index Scan using dist_tab_nn_idx on dist_tab + Index Cond: (val_nn = 10) +(2 rows) + +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM 10; + id +---- + 1 +(1 row) + +RESET enable_seqscan; +-- Ensure that the predicate is preserved as "IS DISTINCT FROM" +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM 20; + QUERY PLAN +------------------------------------------ + Seq Scan on dist_tab + Filter: (val_null IS DISTINCT FROM 20) +(2 rows) + +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM 20; + id +---- + 1 + 2 + 3 +(3 rows) + +-- Safety check for rowtypes +-- Ensure that the predicate is converted to an inequality operator +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE row_nn IS DISTINCT FROM ROW(1, 5)::dist_row_t; + QUERY PLAN +------------------------------------------- + Seq Scan on dist_tab + Filter: (row_nn <> '(1,5)'::dist_row_t) +(2 rows) + +-- ... and that all 3 rows are returned +SELECT id FROM dist_tab WHERE row_nn IS DISTINCT FROM ROW(1, 5)::dist_row_t; + id +---- + 1 + 2 + 3 +(3 rows) + +-- Ensure that the predicate is converted to an equality operator, and thus +-- mergejoinable or hashjoinable +SET enable_nestloop TO off; +EXPLAIN (COSTS OFF) +SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn; + QUERY PLAN +-------------------------------------- + Hash Join + Hash Cond: (t1.val_nn = t2.val_nn) + -> Seq Scan on dist_tab t1 + -> Hash + -> Seq Scan on dist_tab t2 +(5 rows) + +SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn; + id | val_nn | val_null | row_nn | id | val_nn | val_null | row_nn +----+--------+----------+--------+----+--------+----------+-------- + 1 | 10 | 10 | (1,1) | 1 | 10 | 10 | (1,1) + 2 | 20 | | (2,2) | 2 | 20 | | (2,2) + 3 | 30 | 30 | (1,) | 3 | 30 | 30 | (1,) +(3 rows) + +RESET enable_nestloop; +-- Ensure that the predicate is converted to IS NOT NULL +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT; + QUERY PLAN +---------------------------------- + Seq Scan on dist_tab + Filter: (val_null IS NOT NULL) +(2 rows) + +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT; + id +---- + 1 + 3 +(2 rows) + +-- Ensure that the predicate is converted to IS NULL +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT; + QUERY PLAN +------------------------------ + Seq Scan on dist_tab + Filter: (val_null IS NULL) +(2 rows) + +SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT; + id +---- + 2 +(1 row) + +-- Safety check for rowtypes +-- The predicate is converted to IS NOT NULL, and get_rule_expr prints it as IS +-- DISTINCT FROM because argisrow is false, indicating that we're applying a +-- scalar test +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD; + QUERY PLAN +----------------------------------------------------------- + Seq Scan on dist_tab + Filter: (ROW(val_null, val_null) IS DISTINCT FROM NULL) +(2 rows) + +SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD; + id +---- + 1 + 2 + 3 +(3 rows) + +-- The predicate is converted to IS NULL, and get_rule_expr prints it as IS NOT +-- DISTINCT FROM because argisrow is false, indicating that we're applying a +-- scalar test +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD; + QUERY PLAN +--------------------------------------------------------------- + Seq Scan on dist_tab + Filter: (ROW(val_null, val_null) IS NOT DISTINCT FROM NULL) +(2 rows) + +SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD; + id +---- +(0 rows) + +DROP TABLE dist_tab; +DROP TYPE dist_row_t; +-- +-- Test optimization of BooleanTest (IS [NOT] TRUE/FALSE/UNKNOWN) on +-- non-nullable input +-- +CREATE TABLE bool_tab (id int, flag_nn boolean NOT NULL, flag_null boolean); +INSERT INTO bool_tab VALUES (1, true, true); +INSERT INTO bool_tab VALUES (2, false, NULL); +CREATE INDEX bool_tab_nn_idx ON bool_tab (flag_nn); +ANALYZE bool_tab; +-- Ensure that the predicate folds to constant FALSE +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS UNKNOWN; + QUERY PLAN +------------------------------ + Result + Replaces: Scan on bool_tab + One-Time Filter: false +(3 rows) + +SELECT id FROM bool_tab WHERE flag_nn IS UNKNOWN; + id +---- +(0 rows) + +-- Ensure that the predicate folds to constant TRUE +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT UNKNOWN; + QUERY PLAN +---------------------- + Seq Scan on bool_tab +(1 row) + +SELECT id FROM bool_tab WHERE flag_nn IS NOT UNKNOWN; + id +---- + 1 + 2 +(2 rows) + +-- Ensure that the predicate folds to flag_nn +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS TRUE; + QUERY PLAN +---------------------- + Seq Scan on bool_tab + Filter: flag_nn +(2 rows) + +SELECT id FROM bool_tab WHERE flag_nn IS TRUE; + id +---- + 1 +(1 row) + +-- Ensure that the predicate folds to flag_nn, and thus can use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT FALSE; + QUERY PLAN +---------------------------------------------- + Index Scan using bool_tab_nn_idx on bool_tab + Index Cond: (flag_nn = true) +(2 rows) + +SELECT id FROM bool_tab WHERE flag_nn IS NOT FALSE; + id +---- + 1 +(1 row) + +RESET enable_seqscan; +-- Ensure that the predicate folds to not flag_nn +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS FALSE; + QUERY PLAN +------------------------- + Seq Scan on bool_tab + Filter: (NOT flag_nn) +(2 rows) + +SELECT id FROM bool_tab WHERE flag_nn IS FALSE; + id +---- + 2 +(1 row) + +-- Ensure that the predicate folds to not flag_nn, and thus can use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT TRUE; + QUERY PLAN +---------------------------------------------- + Index Scan using bool_tab_nn_idx on bool_tab + Index Cond: (flag_nn = false) +(2 rows) + +SELECT id FROM bool_tab WHERE flag_nn IS NOT TRUE; + id +---- + 2 +(1 row) + +RESET enable_seqscan; +-- Ensure that the predicate is preserved as a BooleanTest +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; + QUERY PLAN +---------------------------------- + Seq Scan on bool_tab + Filter: (flag_null IS UNKNOWN) +(2 rows) + +SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; + id +---- + 2 +(1 row) + +DROP TABLE bool_tab; diff --git a/src/test/regress/expected/prepared_xacts.out b/src/test/regress/expected/prepared_xacts.out index 515a2ada9d1ab..ac4ebf6ca3650 100644 --- a/src/test/regress/expected/prepared_xacts.out +++ b/src/test/regress/expected/prepared_xacts.out @@ -1,3 +1,7 @@ +SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset +\if :skip_test +\quit +\endif -- -- PREPARED TRANSACTIONS (two-phase commit) -- @@ -263,8 +267,37 @@ SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; ----- (0 rows) +-- Test row-level locks held by prepared transactions +CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text); +INSERT INTO pxtest_rowlock VALUES (1, 'test data'); +BEGIN; +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE; + id | data +----+----------- + 1 | test data +(1 row) + +PREPARE TRANSACTION 'regress_p1'; +-- Should fail because the row is locked +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT; +ERROR: could not obtain lock on row in relation "pxtest_rowlock" +-- Test prepared transactions that participate in multixacts. For +-- that, lock the same row again, creating a multixid. +BEGIN; +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE; + id | data +----+----------- + 1 | test data +(1 row) + +PREPARE TRANSACTION 'regress_p2'; +-- Should fail because the row is locked +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT; +ERROR: could not obtain lock on row in relation "pxtest_rowlock" +ROLLBACK PREPARED 'regress_p1'; +ROLLBACK PREPARED 'regress_p2'; -- Clean up DROP TABLE pxtest2; -DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled -ERROR: table "pxtest3" does not exist +-- pxtest3 was already dropped DROP TABLE pxtest4; +DROP TABLE pxtest_rowlock; diff --git a/src/test/regress/expected/prepared_xacts_1.out b/src/test/regress/expected/prepared_xacts_1.out index 6ad3d11898a71..a21314768c318 100644 --- a/src/test/regress/expected/prepared_xacts_1.out +++ b/src/test/regress/expected/prepared_xacts_1.out @@ -1,266 +1,3 @@ --- --- PREPARED TRANSACTIONS (two-phase commit) --- --- We can't readily test persistence of prepared xacts within the --- regression script framework, unfortunately. Note that a crash --- isn't really needed ... stopping and starting the postmaster would --- be enough, but we can't even do that here. --- create a simple table that we'll use in the tests -CREATE TABLE pxtest1 (foobar VARCHAR(10)); -INSERT INTO pxtest1 VALUES ('aaa'); --- Test PREPARE TRANSACTION -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -UPDATE pxtest1 SET foobar = 'bbb' WHERE foobar = 'aaa'; -SELECT * FROM pxtest1; - foobar --------- - bbb -(1 row) - -PREPARE TRANSACTION 'regress_foo1'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- Test pg_prepared_xacts system view -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- Test ROLLBACK PREPARED -ROLLBACK PREPARED 'regress_foo1'; -ERROR: prepared transaction with identifier "regress_foo1" does not exist -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- Test COMMIT PREPARED -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -INSERT INTO pxtest1 VALUES ('ddd'); -SELECT * FROM pxtest1; - foobar --------- - aaa - ddd -(2 rows) - -PREPARE TRANSACTION 'regress_foo2'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -COMMIT PREPARED 'regress_foo2'; -ERROR: prepared transaction with identifier "regress_foo2" does not exist -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- Test duplicate gids -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -UPDATE pxtest1 SET foobar = 'eee' WHERE foobar = 'ddd'; -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -PREPARE TRANSACTION 'regress_foo3'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -INSERT INTO pxtest1 VALUES ('fff'); --- This should fail, because the gid foo3 is already in use -PREPARE TRANSACTION 'regress_foo3'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -ROLLBACK PREPARED 'regress_foo3'; -ERROR: prepared transaction with identifier "regress_foo3" does not exist -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- Test serialization failure (SSI) -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -UPDATE pxtest1 SET foobar = 'eee' WHERE foobar = 'ddd'; -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - -PREPARE TRANSACTION 'regress_foo4'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -SELECT * FROM pxtest1; - foobar --------- - aaa -(1 row) - --- This should fail, because the two transactions have a write-skew anomaly -INSERT INTO pxtest1 VALUES ('fff'); -PREPARE TRANSACTION 'regress_foo5'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - -ROLLBACK PREPARED 'regress_foo4'; -ERROR: prepared transaction with identifier "regress_foo4" does not exist -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- Clean up -DROP TABLE pxtest1; --- Test detection of session-level and xact-level locks on same object -BEGIN; -SELECT pg_advisory_lock(1); - pg_advisory_lock ------------------- - -(1 row) - -SELECT pg_advisory_xact_lock_shared(1); - pg_advisory_xact_lock_shared ------------------------------- - -(1 row) - -PREPARE TRANSACTION 'regress_foo6'; -- fails -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. --- Test subtransactions -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; - CREATE TABLE pxtest2 (a int); - INSERT INTO pxtest2 VALUES (1); - SAVEPOINT a; - INSERT INTO pxtest2 VALUES (2); - ROLLBACK TO a; - SAVEPOINT b; - INSERT INTO pxtest2 VALUES (3); -PREPARE TRANSACTION 'regress_sub1'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. -CREATE TABLE pxtest3(fff int); --- Test shared invalidation -BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; - DROP TABLE pxtest3; - CREATE TABLE pxtest4 (a int); - INSERT INTO pxtest4 VALUES (1); - INSERT INTO pxtest4 VALUES (2); - DECLARE foo CURSOR FOR SELECT * FROM pxtest4; - -- Fetch 1 tuple, keeping the cursor open - FETCH 1 FROM foo; - a ---- - 1 -(1 row) - -PREPARE TRANSACTION 'regress_sub2'; -ERROR: prepared transactions are disabled -HINT: Set "max_prepared_transactions" to a nonzero value. --- No such cursor -FETCH 1 FROM foo; -ERROR: cursor "foo" does not exist --- Table doesn't exist, the creation hasn't been committed yet -SELECT * FROM pxtest2; -ERROR: relation "pxtest2" does not exist -LINE 1: SELECT * FROM pxtest2; - ^ --- There should be two prepared transactions -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- pxtest3 should be locked because of the pending DROP -begin; -lock table pxtest3 in access share mode nowait; -rollback; --- Disconnect, we will continue testing in a different backend -\c - --- There should still be two prepared transactions -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- pxtest3 should still be locked because of the pending DROP -begin; -lock table pxtest3 in access share mode nowait; -rollback; --- Commit table creation -COMMIT PREPARED 'regress_sub1'; -ERROR: prepared transaction with identifier "regress_sub1" does not exist -\d pxtest2 -SELECT * FROM pxtest2; -ERROR: relation "pxtest2" does not exist -LINE 1: SELECT * FROM pxtest2; - ^ --- There should be one prepared transaction -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- Commit table drop -COMMIT PREPARED 'regress_sub2'; -ERROR: prepared transaction with identifier "regress_sub2" does not exist -SELECT * FROM pxtest3; - fff ------ -(0 rows) - --- There should be no prepared transactions -SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; - gid ------ -(0 rows) - --- Clean up -DROP TABLE pxtest2; -ERROR: table "pxtest2" does not exist -DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled -DROP TABLE pxtest4; -ERROR: table "pxtest4" does not exist +SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset +\if :skip_test +\quit diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index c25062c288f32..0de136128189f 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -113,36 +113,6 @@ CREATE USER regress_priv_user2; CREATE USER regress_priv_user3; CREATE USER regress_priv_user4; CREATE USER regress_priv_user5; --- DROP OWNED should also act on granted and granted-to roles -GRANT regress_priv_user1 TO regress_priv_user2; -GRANT regress_priv_user2 TO regress_priv_user3; -SELECT roleid::regrole, member::regrole FROM pg_auth_members - WHERE roleid IN ('regress_priv_user1'::regrole,'regress_priv_user2'::regrole) - ORDER BY roleid::regrole::text; - roleid | member ---------------------+-------------------- - regress_priv_user1 | regress_priv_user2 - regress_priv_user2 | regress_priv_user3 -(2 rows) - -REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4; -- no effect -SELECT roleid::regrole, member::regrole FROM pg_auth_members - WHERE roleid IN ('regress_priv_user1'::regrole,'regress_priv_user2'::regrole) - ORDER BY roleid::regrole::text; - roleid | member ---------------------+-------------------- - regress_priv_user1 | regress_priv_user2 - regress_priv_user2 | regress_priv_user3 -(2 rows) - -DROP OWNED BY regress_priv_user2; -- removes both grants -SELECT roleid::regrole, member::regrole FROM pg_auth_members - WHERE roleid IN ('regress_priv_user1'::regrole,'regress_priv_user2'::regrole) - ORDER BY roleid::regrole::text; - roleid | member ---------+-------- -(0 rows) - GRANT pg_read_all_data TO regress_priv_user6; GRANT pg_write_all_data TO regress_priv_user7; GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION; @@ -351,7 +321,7 @@ SELECT pg_get_acl(0, 0, 0); -- null (1 row) GRANT TRUNCATE ON atest2 TO regress_priv_user4 GRANTED BY regress_priv_user5; -- error -ERROR: grantor must be current user +ERROR: must inherit privileges of role "regress_priv_user5" SET SESSION AUTHORIZATION regress_priv_user2; SELECT session_user, current_user; session_user | current_user @@ -513,8 +483,6 @@ CREATE VIEW atest12v AS SELECT * FROM atest12 WHERE b <<< 5; CREATE VIEW atest12sbv WITH (security_barrier=true) AS SELECT * FROM atest12 WHERE b <<< 5; -GRANT SELECT ON atest12v TO PUBLIC; -GRANT SELECT ON atest12sbv TO PUBLIC; -- This plan should use nestloop, knowing that few rows will be selected. EXPLAIN (COSTS OFF) SELECT * FROM atest12v x, atest12v y WHERE x.a = y.b; QUERY PLAN @@ -560,9 +528,18 @@ CREATE FUNCTION leak2(integer,integer) RETURNS boolean LANGUAGE plpgsql immutable; CREATE OPERATOR >>> (procedure = leak2, leftarg = integer, rightarg = integer, restrict = scalargtsel); --- This should not show any "leak" notices before failing. +-- These should not show any "leak" notices before failing. EXPLAIN (COSTS OFF) SELECT * FROM atest12 WHERE a >>> 0; ERROR: permission denied for table atest12 +EXPLAIN (COSTS OFF) SELECT * FROM atest12v WHERE a >>> 0; +ERROR: permission denied for view atest12v +EXPLAIN (COSTS OFF) SELECT * FROM atest12sbv WHERE a >>> 0; +ERROR: permission denied for view atest12sbv +-- Now regress_priv_user1 grants access to regress_priv_user2 via the views. +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT SELECT ON atest12v TO PUBLIC; +GRANT SELECT ON atest12sbv TO PUBLIC; +SET SESSION AUTHORIZATION regress_priv_user2; -- These plans should continue to use a nestloop, since they execute with the -- privileges of the view owner. EXPLAIN (COSTS OFF) SELECT * FROM atest12v x, atest12v y WHERE x.a = y.b; @@ -944,6 +921,32 @@ INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set one = 8; -- f ERROR: permission denied for table atest5 INSERT INTO atest5(three) VALUES (4) ON CONFLICT (two) DO UPDATE set three = 10; -- fails (due to INSERT) ERROR: permission denied for table atest5 +-- Check that column level privileges are enforced for ON CONFLICT ... WHERE +-- Ok. we may select one +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.one = 1 RETURNING atest5.two; + two +----- + 2 +(1 row) + +-- Error. No select rights on three +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.three = 1 RETURNING atest5.two; +ERROR: permission denied for table atest5 +-- Check that ON CONFLICT ... SELECT FOR UPDATE/SHARE requires an updatable column +SET SESSION AUTHORIZATION regress_priv_user1; +REVOKE UPDATE (three) ON atest5 FROM regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- fails +ERROR: permission denied for table atest5 +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT UPDATE (three) ON atest5 TO regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- ok + two +----- + 2 +(1 row) + -- Check that the columns in the inference require select privileges INSERT INTO atest5(four) VALUES (4); -- fail ERROR: permission denied for table atest5 @@ -1142,6 +1145,34 @@ ERROR: null value in column "b" of relation "errtst_part_2" violates not-null c DETAIL: Failing row contains (a, b, c) = (aaaa, null, ccc). SET SESSION AUTHORIZATION regress_priv_user1; DROP TABLE errtst; +-- test column-level privileges on the range used in FOR PORTION OF +SET SESSION AUTHORIZATION regress_priv_user1; +CREATE TABLE t1 ( + c1 int4range, + valid_at tsrange, + CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS) +); +-- UPDATE requires select permission on the valid_at column (but not update): +GRANT SELECT (c1) ON t1 TO regress_priv_user2; +GRANT UPDATE (c1) ON t1 TO regress_priv_user2; +GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3; +GRANT UPDATE (c1) ON t1 TO regress_priv_user3; +SET SESSION AUTHORIZATION regress_priv_user2; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +ERROR: permission denied for table t1 +SET SESSION AUTHORIZATION regress_priv_user3; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user1; +-- DELETE requires select permission on the valid_at column: +GRANT DELETE ON t1 TO regress_priv_user2; +GRANT DELETE ON t1 TO regress_priv_user3; +SET SESSION AUTHORIZATION regress_priv_user2; +DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01'; +ERROR: permission denied for table t1 +SET SESSION AUTHORIZATION regress_priv_user3; +DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01'; +SET SESSION AUTHORIZATION regress_priv_user1; +DROP TABLE t1; -- test column-level privileges when involved with DELETE SET SESSION AUTHORIZATION regress_priv_user1; ALTER TABLE atest6 ADD COLUMN three integer; @@ -2172,6 +2203,53 @@ SELECT lo_truncate(lo_open(2001, x'20000'::int), 10); 0 (1 row) +\c - +-- confirm role with privileges of pg_read_all_data can read large objects +SET SESSION AUTHORIZATION regress_priv_user6; +SELECT loread(lo_open(1002, x'40000'::int), 32); + loread +-------- + \x +(1 row) + +SELECT lo_get(1002); + lo_get +-------- + \x +(1 row) + +SELECT lowrite(lo_open(1002, x'20000'::int), 'abcd'); -- to be denied +ERROR: permission denied for large object 1002 +SELECT lo_put(1002, 1, 'abcd'); -- to be denied +ERROR: permission denied for large object 1002 +SELECT lo_truncate(lo_open(1002, x'20000'::int), 0); -- to be denied +ERROR: permission denied for large object 1002 +SELECT lo_unlink(1002); -- to be denied +ERROR: must be owner of large object 1002 +\c - +-- confirm role with privileges of pg_write_all_data can write large objects +GRANT SELECT ON LARGE OBJECT 1002 TO regress_priv_user7; +SET SESSION AUTHORIZATION regress_priv_user7; +SELECT lowrite(lo_open(1002, x'20000'::int), 'abcd'); + lowrite +--------- + 4 +(1 row) + +SELECT lo_put(1002, 1, 'abcd'); + lo_put +-------- + +(1 row) + +SELECT lo_truncate(lo_open(1002, x'20000'::int), 0); + lo_truncate +------------- + 0 +(1 row) + +SELECT lo_unlink(1002); -- to be denied +ERROR: must be owner of large object 1002 -- has_largeobject_privilege function -- superuser \c - @@ -2568,6 +2646,26 @@ SELECT makeaclitem('regress_priv_user1'::regrole, 'regress_priv_user2'::regrole, SELECT makeaclitem('regress_priv_user1'::regrole, 'regress_priv_user2'::regrole, 'SELECT, fake_privilege', FALSE); -- error ERROR: unrecognized privilege type: "fake_privilege" +-- Test quoting and dequoting of user names in ACLs +CREATE ROLE "regress_""quoted"; +SELECT makeaclitem('regress_"quoted'::regrole, 'regress_"quoted'::regrole, + 'SELECT', TRUE); + makeaclitem +------------------------------------------ + "regress_""quoted"=r*/"regress_""quoted" +(1 row) + +SELECT '"regress_""quoted"=r*/"regress_""quoted"'::aclitem; + aclitem +------------------------------------------ + "regress_""quoted"=r*/"regress_""quoted" +(1 row) + +SELECT '""=r*/""'::aclitem; -- used to be misparsed as """" +ERROR: a name must follow the "/" sign +LINE 1: SELECT '""=r*/""'::aclitem; + ^ +DROP ROLE "regress_""quoted"; -- Test non-throwing aclitem I/O SELECT pg_input_is_valid('regress_priv_user1=r/regress_priv_user2', 'aclitem'); pg_input_is_valid @@ -2704,7 +2802,7 @@ SELECT has_largeobject_privilege('regress_priv_user2', 1008, 'SELECT'); -- yes t (1 row) -SELECT has_largeobject_privilege('regress_priv_user6', 1008, 'SELECT'); -- no +SELECT has_largeobject_privilege('regress_priv_user3', 1008, 'SELECT'); -- no has_largeobject_privilege --------------------------- f @@ -3077,9 +3175,100 @@ revoke select on dep_priv_test from regress_priv_user4 cascade; set session role regress_priv_user1; drop table dep_priv_test; +-- +-- Property graphs +-- +set session role regress_priv_user1; +create property graph ptg1 + vertex tables ( + atest5 key (four) + default label properties (four) + label lttc properties (three as lttck), + atest1 key (a) + default label + label lttc properties (a as lttck), + atest2 key (col1) + default label + label ltv properties (col1 as ltvk)); +-- select privileges on property graph as well as table +select * from graph_table (ptg1 match (is atest5) COLUMNS (1 as value)) limit 0; -- ok + value +------- +(0 rows) + +grant select on ptg1 to regress_priv_user2; +set session role regress_priv_user2; +select * from graph_table (ptg1 match (is atest1) COLUMNS (1 as value)) limit 0; -- ok + value +------- +(0 rows) + +-- select privileges on property graph but not table +select * from graph_table (ptg1 match (is atest5) COLUMNS (1 as value)) limit 0; -- fails +ERROR: permission denied for table atest5 +select * from graph_table (ptg1 match (is lttc) COLUMNS (1 as value)) limit 0; -- fails +ERROR: permission denied for table atest5 +set session role regress_priv_user3; +-- select privileges on table but not property graph +select * from graph_table (ptg1 match (is atest1) COLUMNS (1 as value)) limit 0; -- fails +ERROR: permission denied for property graph ptg1 +-- select privileges on neither +select * from graph_table (ptg1 match (is atest5) COLUMNS (1 as value)) limit 0; -- fails +ERROR: permission denied for property graph ptg1 +-- column privileges +set session role regress_priv_user1; +select * from graph_table (ptg1 match (v is lttc) COLUMNS (v.lttck)) limit 0; -- ok + lttck +------- +(0 rows) + +grant select on ptg1 to regress_priv_user4; +set session role regress_priv_user4; +select * from graph_table (ptg1 match (a is atest5) COLUMNS (a.four)) limit 0; -- ok + four +------ +(0 rows) + +select * from graph_table (ptg1 match (v is lttc) COLUMNS (v.lttck)) limit 0; -- fail +ERROR: permission denied for table atest5 +-- access property graph through security definer view +set session role regress_priv_user4; +create view atpgv1 as select * from graph_table (ptg1 match (is atest1) COLUMNS (1 as value)) limit 0; +grant select on atpgv1 to regress_priv_user3; +select * from atpgv1; -- ok + value +------- +(0 rows) + +set session role regress_priv_user3; +select * from atpgv1; -- ok + value +------- +(0 rows) + +set session role regress_priv_user4; +create view atpgv2 as select * from graph_table (ptg1 match (v is ltv) COLUMNS (v.ltvk)) limit 0; +-- though the session user is the owner of the view and also has access to the +-- property graph, it does not have access to a table referenced in the graph +-- pattern +select * from atpgv2; -- fail +ERROR: permission denied for table atest2 +grant select on atpgv2 to regress_priv_user2; +-- The user who otherwise does not have access to the property graph, gets +-- access to it through a security definer view and uses it successfully since +-- it has access to the tables referenced in the graph pattern. +set session role regress_priv_user2; +select * from atpgv2; -- ok + ltvk +------ +(0 rows) + -- clean up \c drop sequence x_seq; +drop view atpgv1; +drop view atpgv2; +drop property graph ptg1; DROP AGGREGATE priv_testagg1(int); DROP FUNCTION priv_testfunc2(int); DROP FUNCTION priv_testfunc4(boolean); @@ -3126,6 +3315,11 @@ DROP USER regress_priv_user6; DROP USER regress_priv_user7; DROP USER regress_priv_user8; -- does not exist ERROR: role "regress_priv_user8" does not exist +-- leave some default ACLs for pg_upgrade's dump-restore test input. +ALTER DEFAULT PRIVILEGES FOR ROLE pg_signal_backend + REVOKE USAGE ON TYPES FROM pg_signal_backend; +ALTER DEFAULT PRIVILEGES FOR ROLE pg_read_all_settings + REVOKE USAGE ON TYPES FROM pg_read_all_settings; -- permissions with LOCK TABLE CREATE USER regress_locktable_user; CREATE TABLE lock_table (a int); @@ -3220,7 +3414,8 @@ REVOKE MAINTAIN ON lock_table FROM regress_locktable_user; DROP TABLE lock_table; DROP USER regress_locktable_user; -- test to check privileges of system views pg_shmem_allocations, --- pg_shmem_allocations_numa and pg_backend_memory_contexts. +-- pg_shmem_allocations_numa, pg_dsm_registry_allocations, and +-- pg_backend_memory_contexts. -- switch to superuser \c - CREATE ROLE regress_readallstats; @@ -3248,6 +3443,12 @@ SELECT has_table_privilege('regress_readallstats','pg_shmem_allocations_numa','S f (1 row) +SELECT has_table_privilege('regress_readallstats','pg_dsm_registry_allocations','SELECT'); -- no + has_table_privilege +--------------------- + f +(1 row) + GRANT pg_read_all_stats TO regress_readallstats; SELECT has_table_privilege('regress_readallstats','pg_aios','SELECT'); -- yes has_table_privilege @@ -3273,6 +3474,12 @@ SELECT has_table_privilege('regress_readallstats','pg_shmem_allocations_numa','S t (1 row) +SELECT has_table_privilege('regress_readallstats','pg_dsm_registry_allocations','SELECT'); -- yes + has_table_privilege +--------------------- + t +(1 row) + -- run query to ensure that functions within views can be executed SET ROLE regress_readallstats; SELECT COUNT(*) >= 0 AS ok FROM pg_aios; @@ -3442,3 +3649,61 @@ SELECT * FROM information_schema.table_privileges t DROP TABLE grantor_test1, grantor_test2, grantor_test3; DROP ROLE regress_grantor1, regress_grantor2, regress_grantor3; +-- GRANTED BY +CREATE ROLE regress_grantor1; +CREATE ROLE regress_grantor2 ROLE regress_grantor1; +CREATE ROLE regress_grantor3 ROLE regress_grantor1; +CREATE ROLE regress_grantor4 ROLE regress_grantor1; +CREATE ROLE regress_grantor5; +CREATE TABLE grantor_test (); +GRANT SELECT ON grantor_test TO regress_grantor2 WITH GRANT OPTION; +GRANT UPDATE ON grantor_test TO regress_grantor3 WITH GRANT OPTION; +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor4 WITH GRANT OPTION; +SET ROLE regress_grantor1; +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor5; +SELECT * FROM information_schema.table_privileges t + WHERE grantor LIKE 'regress_grantor%' ORDER BY ROW(t.*); + grantor | grantee | table_catalog | table_schema | table_name | privilege_type | is_grantable | with_hierarchy +------------------+------------------+---------------+--------------+--------------+----------------+--------------+---------------- + regress_grantor4 | regress_grantor5 | regression | public | grantor_test | SELECT | NO | YES + regress_grantor4 | regress_grantor5 | regression | public | grantor_test | UPDATE | NO | NO +(2 rows) + +REVOKE SELECT, UPDATE ON grantor_test FROM regress_grantor5; +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor5 GRANTED BY regress_grantor2; +WARNING: not all privileges were granted for "grantor_test" +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor5 GRANTED BY regress_grantor3; +WARNING: not all privileges were granted for "grantor_test" +SELECT * FROM information_schema.table_privileges t + WHERE grantor LIKE 'regress_grantor%' ORDER BY ROW(t.*); + grantor | grantee | table_catalog | table_schema | table_name | privilege_type | is_grantable | with_hierarchy +------------------+------------------+---------------+--------------+--------------+----------------+--------------+---------------- + regress_grantor2 | regress_grantor5 | regression | public | grantor_test | SELECT | NO | YES + regress_grantor3 | regress_grantor5 | regression | public | grantor_test | UPDATE | NO | NO +(2 rows) + +REVOKE SELECT, UPDATE ON grantor_test FROM regress_grantor5 GRANTED BY regress_grantor2; +WARNING: not all privileges could be revoked for "grantor_test" +WARNING: not all privileges could be revoked for column "tableoid" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "cmax" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "xmax" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "cmin" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "xmin" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "ctid" of relation "grantor_test" +REVOKE SELECT, UPDATE ON grantor_test FROM regress_grantor5 GRANTED BY regress_grantor3; +WARNING: not all privileges could be revoked for "grantor_test" +WARNING: not all privileges could be revoked for column "tableoid" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "cmax" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "xmax" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "cmin" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "xmin" of relation "grantor_test" +WARNING: not all privileges could be revoked for column "ctid" of relation "grantor_test" +SELECT * FROM information_schema.table_privileges t + WHERE grantor LIKE 'regress_grantor%' ORDER BY ROW(t.*); + grantor | grantee | table_catalog | table_schema | table_name | privilege_type | is_grantable | with_hierarchy +---------+---------+---------------+--------------+------------+----------------+--------------+---------------- +(0 rows) + +RESET ROLE; +DROP TABLE grantor_test; +DROP ROLE regress_grantor1, regress_grantor2, regress_grantor3, regress_grantor4, regress_grantor5; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index cf48ae6d0c2ee..c8f3932edf094 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -160,12 +160,12 @@ LINE 1: SELECT $1, $2 foo4 | bar4 (1 row) --- \close (extended query protocol) -\close -\close: missing required argument -\close '' -\close stmt2 -\close stmt2 +-- \close_prepared (extended query protocol) +\close_prepared +\close_prepared: missing required argument +\close_prepared '' +\close_prepared stmt2 +\close_prepared stmt2 SELECT name, statement FROM pg_prepared_statements ORDER BY name; name | statement -------+---------------- @@ -445,6 +445,8 @@ environment value border 1 columns 0 csv_fieldsep ',' +display_false 'f' +display_true 't' expanded off fieldsep '|' fieldsep_zero off @@ -464,6 +466,36 @@ unicode_border_linestyle single unicode_column_linestyle single unicode_header_linestyle single xheader_width full +-- test the simple display substitution settings +prepare q as select null as n, true as t, false as f; +\pset null '(null)' +\pset display_true 'true' +\pset display_false 'false' +execute q; + n | t | f +--------+------+------- + (null) | true | false +(1 row) + +\pset null +\pset display_true +\pset display_false +execute q; + n | t | f +--------+------+------- + (null) | true | false +(1 row) + +\pset null '' +\pset display_true 't' +\pset display_false 'f' +execute q; + n | t | f +---+---+--- + | t | f +(1 row) + +deallocate q; -- test multi-line headers, wrapping, and newline indicators -- in aligned, unaligned, and wrapped formats prepare q as select array_to_string(array_agg(repeat('x',2*n)),E'\n') as "ab @@ -4666,7 +4698,7 @@ bar 'bar' "bar" \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 - \close stmt1 + \close_prepared stmt1 \conninfo \copy arg1 arg2 arg3 arg4 arg5 arg6 \copyright @@ -4705,6 +4737,7 @@ invalid command \lo \pset arg1 arg2 \q \reset + \restrict test \s arg1 \sendpipeline \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 @@ -4716,6 +4749,7 @@ invalid command \lo \t arg1 \T arg1 \timing arg1 + \unrestrict not_valid \unset arg1 \w arg1 \watch arg1 arg2 @@ -6443,9 +6477,9 @@ List of schemas (0 rows) \dRp "no.such.publication" - List of publications - Name | Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root -------+-------+------------+---------+---------+---------+-----------+-------------------+---------- + List of publications + Name | Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +------+-------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- (0 rows) \dRs "no.such.subscription" diff --git a/src/test/regress/expected/psql_pipeline.out b/src/test/regress/expected/psql_pipeline.out index a30dec088b953..a0816fb10b68e 100644 --- a/src/test/regress/expected/psql_pipeline.out +++ b/src/test/regress/expected/psql_pipeline.out @@ -228,192 +228,6 @@ BEGIN \bind \sendpipeline INSERT INTO psql_pipeline VALUES ($1) \bind 1 \sendpipeline COMMIT \bind \sendpipeline \endpipeline --- COPY FROM STDIN --- with \sendpipeline and \bind -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -COPY psql_pipeline FROM STDIN \bind \sendpipeline -\endpipeline - ?column? ----------- - val1 -(1 row) - --- with semicolon -\startpipeline -SELECT 'val1'; -COPY psql_pipeline FROM STDIN; -\endpipeline - ?column? ----------- - val1 -(1 row) - --- COPY FROM STDIN with \flushrequest + \getresults --- with \sendpipeline and \bind -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -COPY psql_pipeline FROM STDIN \bind \sendpipeline -\flushrequest -\getresults - ?column? ----------- - val1 -(1 row) - -message type 0x5a arrived from server while idle -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -COPY psql_pipeline FROM STDIN; -\flushrequest -\getresults - ?column? ----------- - val1 -(1 row) - -message type 0x5a arrived from server while idle -\endpipeline --- COPY FROM STDIN with \syncpipeline + \getresults --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -COPY psql_pipeline FROM STDIN \bind \sendpipeline -\syncpipeline -\getresults - ?column? ----------- - val1 -(1 row) - -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -COPY psql_pipeline FROM STDIN; -\syncpipeline -\getresults - ?column? ----------- - val1 -(1 row) - -\endpipeline --- COPY TO STDOUT --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -copy psql_pipeline TO STDOUT \bind \sendpipeline -\endpipeline - ?column? ----------- - val1 -(1 row) - -1 \N -2 test2 -20 test2 -3 test3 -30 test3 -4 test4 -40 test4 --- with semicolon -\startpipeline -SELECT 'val1'; -copy psql_pipeline TO STDOUT; -\endpipeline - ?column? ----------- - val1 -(1 row) - -1 \N -2 test2 -20 test2 -3 test3 -30 test3 -4 test4 -40 test4 --- COPY TO STDOUT with \flushrequest + \getresults --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -copy psql_pipeline TO STDOUT \bind \sendpipeline -\flushrequest -\getresults - ?column? ----------- - val1 -(1 row) - -1 \N -2 test2 -20 test2 -3 test3 -30 test3 -4 test4 -40 test4 -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -copy psql_pipeline TO STDOUT; -\flushrequest -\getresults - ?column? ----------- - val1 -(1 row) - -1 \N -2 test2 -20 test2 -3 test3 -30 test3 -4 test4 -40 test4 -\endpipeline --- COPY TO STDOUT with \syncpipeline + \getresults --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -copy psql_pipeline TO STDOUT \bind \sendpipeline -\syncpipeline -\getresults - ?column? ----------- - val1 -(1 row) - -1 \N -2 test2 -20 test2 -3 test3 -30 test3 -4 test4 -40 test4 -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -copy psql_pipeline TO STDOUT; -\syncpipeline -\getresults - ?column? ----------- - val1 -(1 row) - -1 \N -2 test2 -20 test2 -3 test3 -30 test3 -4 test4 -40 test4 -\endpipeline -- Use \parse and \bind_named \startpipeline SELECT $1 \parse '' @@ -740,7 +554,7 @@ SELECT COUNT(*) FROM psql_pipeline \bind \sendpipeline count ------- - 7 + 1 (1 row) -- After an error, pipeline is aborted and requires \syncpipeline to be @@ -750,7 +564,7 @@ SELECT $1 \bind \sendpipeline SELECT $1 \bind 1 \sendpipeline SELECT $1 \parse a \bind_named a 1 \sendpipeline -\close a +\close_prepared a \flushrequest \getresults ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1 @@ -758,7 +572,7 @@ ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1 SELECT $1 \bind 1 \sendpipeline SELECT $1 \parse a \bind_named a 1 \sendpipeline -\close a +\close_prepared a -- Sync allows pipeline to recover. \syncpipeline \getresults @@ -766,7 +580,7 @@ Pipeline aborted, command did not run SELECT $1 \bind 1 \sendpipeline SELECT $1 \parse a \bind_named a 1 \sendpipeline -\close a +\close_prepared a \flushrequest \getresults ?column? diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 4de96c04f9de4..d028e9be866b5 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -34,22 +34,26 @@ ERROR: conflicting or redundant options LINE 1: ...pub_xxx WITH (publish_generated_columns = stored, publish_ge... ^ CREATE PUBLICATION testpub_xxx WITH (publish_generated_columns = foo); -ERROR: publish_generated_columns requires a "none" or "stored" value +ERROR: invalid value for publication parameter "publish_generated_columns": "foo" +DETAIL: Valid values are "none" and "stored". +CREATE PUBLICATION testpub_xxx WITH (publish_generated_columns); +ERROR: invalid value for publication parameter "publish_generated_columns": "" +DETAIL: Valid values are "none" and "stored". \dRp - List of publications - Name | Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------+--------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - testpub_default | regress_publication_user | f | f | t | f | f | none | f - testpub_ins_trunct | regress_publication_user | f | t | f | f | f | none | f + List of publications + Name | Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +--------------------+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + testpub_default | regress_publication_user | f | f | f | t | f | f | none | f + testpub_ins_trunct | regress_publication_user | f | f | t | f | f | f | none | f (2 rows) ALTER PUBLICATION testpub_default SET (publish = 'insert, update, delete'); \dRp - List of publications - Name | Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------+--------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - testpub_default | regress_publication_user | f | t | t | t | f | none | f - testpub_ins_trunct | regress_publication_user | f | t | f | f | f | none | f + List of publications + Name | Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +--------------------+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + testpub_default | regress_publication_user | f | f | t | t | t | f | none | f + testpub_ins_trunct | regress_publication_user | f | f | t | f | f | f | none | f (2 rows) --- adding tables @@ -66,15 +70,15 @@ CREATE TABLE testpub_tbl2 (id serial primary key, data text); -- fail - can't add to for all tables publication ALTER PUBLICATION testpub_foralltables ADD TABLE testpub_tbl2; ERROR: publication "testpub_foralltables" is defined as FOR ALL TABLES -DETAIL: Tables cannot be added to or dropped from FOR ALL TABLES publications. +DETAIL: Tables or sequences cannot be added to or dropped from FOR ALL TABLES publications. -- fail - can't drop from all tables publication ALTER PUBLICATION testpub_foralltables DROP TABLE testpub_tbl2; ERROR: publication "testpub_foralltables" is defined as FOR ALL TABLES -DETAIL: Tables cannot be added to or dropped from FOR ALL TABLES publications. +DETAIL: Tables or sequences cannot be added to or dropped from FOR ALL TABLES publications. -- fail - can't add to for all tables publication ALTER PUBLICATION testpub_foralltables SET TABLE pub_test.testpub_nopk; ERROR: publication "testpub_foralltables" is defined as FOR ALL TABLES -DETAIL: Tables cannot be added to or dropped from FOR ALL TABLES publications. +DETAIL: Tables or sequences cannot be added to or dropped from FOR ALL TABLES publications. -- fail - can't add schema to 'FOR ALL TABLES' publication ALTER PUBLICATION testpub_foralltables ADD TABLES IN SCHEMA pub_test; ERROR: publication "testpub_foralltables" is defined as FOR ALL TABLES @@ -93,10 +97,10 @@ RESET client_min_messages; -- should be able to add schema to 'FOR TABLE' publication ALTER PUBLICATION testpub_fortable ADD TABLES IN SCHEMA pub_test; \dRp+ testpub_fortable - Publication testpub_fortable - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_fortable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.testpub_tbl1" Tables from schemas: @@ -105,20 +109,20 @@ Tables from schemas: -- should be able to drop schema from 'FOR TABLE' publication ALTER PUBLICATION testpub_fortable DROP TABLES IN SCHEMA pub_test; \dRp+ testpub_fortable - Publication testpub_fortable - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_fortable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.testpub_tbl1" -- should be able to set schema to 'FOR TABLE' publication ALTER PUBLICATION testpub_fortable SET TABLES IN SCHEMA pub_test; \dRp+ testpub_fortable - Publication testpub_fortable - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_fortable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test" @@ -129,10 +133,10 @@ CREATE PUBLICATION testpub_forschema FOR TABLES IN SCHEMA pub_test; CREATE PUBLICATION testpub_for_tbl_schema FOR TABLES IN SCHEMA pub_test, TABLE pub_test.testpub_nopk; RESET client_min_messages; \dRp+ testpub_for_tbl_schema - Publication testpub_for_tbl_schema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_for_tbl_schema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "pub_test.testpub_nopk" Tables from schemas: @@ -150,10 +154,10 @@ LINE 1: ...CATION testpub_parsertst FOR TABLES IN SCHEMA foo, test.foo; -- should be able to add a table of the same schema to the schema publication ALTER PUBLICATION testpub_forschema ADD TABLE pub_test.testpub_nopk; \dRp+ testpub_forschema - Publication testpub_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "pub_test.testpub_nopk" Tables from schemas: @@ -162,10 +166,10 @@ Tables from schemas: -- should be able to drop the table ALTER PUBLICATION testpub_forschema DROP TABLE pub_test.testpub_nopk; \dRp+ testpub_forschema - Publication testpub_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test" @@ -176,10 +180,10 @@ ERROR: relation "testpub_nopk" is not part of the publication -- should be able to set table to schema publication ALTER PUBLICATION testpub_forschema SET TABLE pub_test.testpub_nopk; \dRp+ testpub_forschema - Publication testpub_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "pub_test.testpub_nopk" @@ -203,39 +207,340 @@ Not-null constraints: "testpub_tbl2_id_not_null" NOT NULL "id" \dRp+ testpub_foralltables - Publication testpub_foralltables - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | t | t | t | f | f | none | f + Publication testpub_foralltables + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | f | f | none | f | (1 row) -DROP TABLE testpub_tbl2; -DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema; -CREATE TABLE testpub_tbl3 (a int); -CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3); +--------------------------------------------- +-- EXCEPT clause tests for normal tables +--------------------------------------------- SET client_min_messages = 'ERROR'; -CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3; -CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3; +CREATE TABLE testpub_tbl3 (id serial primary key, data text); +-- Specify table list in the EXCEPT clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT (TABLE testpub_tbl1, testpub_tbl2, TABLE testpub_tbl3); +\dRp+ testpub_foralltables_excepttable + Publication testpub_foralltables_excepttable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl1" + "public.testpub_tbl2" + "public.testpub_tbl3" + +-- Specify table in the EXCEPT clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (TABLE testpub_tbl1); +\dRp+ testpub_foralltables_excepttable1 + Publication testpub_foralltables_excepttable1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl1" + +-- Check that the table description shows the publications where it is listed +-- in the EXCEPT clause +\d testpub_tbl1 + Table "public.testpub_tbl1" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+------------------------------------------ + id | integer | | not null | nextval('testpub_tbl1_id_seq'::regclass) + data | text | | | +Indexes: + "testpub_tbl1_pkey" PRIMARY KEY, btree (id) +Publications: + "testpub_foralltables" +Except publications: + "testpub_foralltables_excepttable" + "testpub_foralltables_excepttable1" + +-- fail - first table in the EXCEPT list should use TABLE keyword +CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); +ERROR: syntax error at or near "testpub_tbl1" +LINE 1: ..._foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tb... + ^ +--------------------------------------------- +-- SET ALL TABLES/SEQUENCES +--------------------------------------------- +-- Replace the existing table list in the EXCEPT clause (testpub_tbl1, +-- testpub_tbl2, testpub_tbl3) with table (testpub_tbl2). +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES EXCEPT (TABLE testpub_tbl2); +\dRp+ testpub_foralltables_excepttable + Publication testpub_foralltables_excepttable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl2" + +-- Replace the existing table list in the EXCEPT clause (testpub_tbl2) with a +-- table list containing (testpub_tbl1, testpub_tbl2, testpub_tbl3). +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES EXCEPT (TABLE testpub_tbl1, testpub_tbl2, TABLE testpub_tbl3); +\dRp+ testpub_foralltables_excepttable + Publication testpub_foralltables_excepttable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl1" + "public.testpub_tbl2" + "public.testpub_tbl3" + +-- Clear the table list in the EXCEPT clause, making the publication include all +-- tables. +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES; +\dRp+ testpub_foralltables_excepttable + Publication testpub_foralltables_excepttable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +(1 row) + +-- Create an empty publication for subsequent tests. +CREATE PUBLICATION testpub_forall_tbls_seqs; +-- Enable both puballtables and puballsequences +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL TABLES, ALL SEQUENCES; +\dRp+ testpub_forall_tbls_seqs + Publication testpub_forall_tbls_seqs + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | t | t | t | t | t | none | f | +(1 row) + +-- Explicitly test that SET ALL TABLES resets puballsequences to false +-- Result should be: puballtables = true, puballsequences = false +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL TABLES; +\dRp+ testpub_forall_tbls_seqs + Publication testpub_forall_tbls_seqs + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +(1 row) + +-- Explicitly test that SET ALL SEQUENCES resets puballtables to false +-- Result should be: puballtables = false, puballsequences = true +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL SEQUENCES; +\dRp+ testpub_forall_tbls_seqs + Publication testpub_forall_tbls_seqs + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | t | t | t | t | t | none | f | +(1 row) + +-- fail - SET ALL TABLES/SEQUENCES is not allowed for a 'FOR TABLE' publication +ALTER PUBLICATION testpub_fortable SET ALL TABLES EXCEPT (TABLE testpub_tbl1); +ERROR: publication "testpub_fortable" does not support ALL TABLES operations +DETAIL: This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty. +ALTER PUBLICATION testpub_fortable SET ALL TABLES; +ERROR: publication "testpub_fortable" does not support ALL TABLES operations +DETAIL: This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty. +ALTER PUBLICATION testpub_fortable SET ALL SEQUENCES; +ERROR: publication "testpub_fortable" does not support ALL SEQUENCES operations +DETAIL: This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty. +-- fail - SET ALL TABLES/SEQUENCES is not allowed for a schema publication +ALTER PUBLICATION testpub_forschema SET ALL TABLES EXCEPT (TABLE pub_test.testpub_nopk); +ERROR: publication "testpub_forschema" does not support ALL TABLES operations +DETAIL: This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty. +ALTER PUBLICATION testpub_forschema SET ALL TABLES; +ERROR: publication "testpub_forschema" does not support ALL TABLES operations +DETAIL: This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty. +ALTER PUBLICATION testpub_forschema SET ALL SEQUENCES; +ERROR: publication "testpub_forschema" does not support ALL SEQUENCES operations +DETAIL: This operation requires the publication to be defined as FOR ALL TABLES/SEQUENCES or to be empty. RESET client_min_messages; +DROP TABLE testpub_tbl2, testpub_tbl3; +DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema; +DROP PUBLICATION testpub_forall_tbls_seqs, testpub_foralltables_excepttable, testpub_foralltables_excepttable1; +--------------------------------------------- +-- Tests for inherited tables, and +-- EXCEPT clause tests for inherited tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_tbl_parent (a int); +CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent); +CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent; \dRp+ testpub3 - Publication testpub3 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub3 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: - "public.testpub_tbl3" - "public.testpub_tbl3a" + "public.testpub_tbl_child" + "public.testpub_tbl_parent" +CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent; \dRp+ testpub4 - Publication testpub4 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub4 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: - "public.testpub_tbl3" + "public.testpub_tbl_parent" + +-- List the parent table in the EXCEPT clause (without ONLY or '*') +CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent); +\dRp+ testpub5 + Publication testpub5 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl_child" + "public.testpub_tbl_parent" + +-- EXCEPT with '*': list the table and all its descendants in the EXCEPT clause +CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent *); +\dRp+ testpub6 + Publication testpub6 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl_child" + "public.testpub_tbl_parent" + +-- EXCEPT with ONLY: list the table in the EXCEPT clause, but not its descendants +CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT (TABLE ONLY testpub_tbl_parent); +\dRp+ testpub7 + Publication testpub7 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_tbl_parent" + +RESET client_min_messages; +DROP TABLE testpub_tbl_parent, testpub_tbl_child; +DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7; +--------------------------------------------- +-- EXCEPT clause tests for partitioned tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a); +CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100); +CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT (TABLE testpub_root); +\dRp+ testpub8; + Publication testpub8 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | +Except tables: + "public.testpub_root" + +\d testpub_part1 + Table "public.testpub_part1" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | +Partition of: testpub_root FOR VALUES FROM (0) TO (100) +Except publications: + "testpub8" + +\d testpub_root + Partitioned table "public.testpub_root" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | +Partition key: RANGE (a) +Except publications: + "testpub8" +Number of partitions: 1 (Use \d+ to list them.) + +CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT (TABLE testpub_part1); +ERROR: cannot specify relation "testpub_part1" in the publication EXCEPT clause +DETAIL: This operation is not supported for individual partitions. +CREATE TABLE tab_main (a int) PARTITION BY RANGE(a); +-- Attaching a partition is not allowed if the partitioned table appears in a +-- publication's EXCEPT clause. +ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200); +ERROR: cannot attach table "testpub_root" as partition because it is referenced in publication "testpub8" EXCEPT clause +DETAIL: The publication EXCEPT clause cannot contain tables that are partitions. +HINT: Change the publication's EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES. +RESET client_min_messages; +DROP TABLE testpub_root, testpub_part1, tab_main; +DROP PUBLICATION testpub8; +--- Tests for publications with SEQUENCES +CREATE SEQUENCE regress_pub_seq0; +CREATE SEQUENCE pub_test.regress_pub_seq1; +-- FOR ALL SEQUENCES +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION regress_pub_forallsequences1 FOR ALL SEQUENCES; +RESET client_min_messages; +SELECT pubname, puballtables, puballsequences FROM pg_publication WHERE pubname = 'regress_pub_forallsequences1'; + pubname | puballtables | puballsequences +------------------------------+--------------+----------------- + regress_pub_forallsequences1 | f | t +(1 row) + +\d+ regress_pub_seq0 + Sequence "public.regress_pub_seq0" + Type | Start | Minimum | Maximum | Increment | Cycles? | Cache +--------+-------+---------+---------------------+-----------+---------+------- + bigint | 1 | 1 | 9223372036854775807 | 1 | no | 1 +Publications: + "regress_pub_forallsequences1" + +\dRp+ regress_pub_forallsequences1 + Publication regress_pub_forallsequences1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | t | t | t | t | t | none | f | +(1 row) + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION regress_pub_forallsequences2 FOR ALL SEQUENCES; +RESET client_min_messages; +-- check that describe sequence lists both publications the sequence belongs to +\d+ pub_test.regress_pub_seq1 + Sequence "pub_test.regress_pub_seq1" + Type | Start | Minimum | Maximum | Increment | Cycles? | Cache +--------+-------+---------+---------------------+-----------+---------+------- + bigint | 1 | 1 | 9223372036854775807 | 1 | no | 1 +Publications: + "regress_pub_forallsequences1" + "regress_pub_forallsequences2" + +--- Specifying both ALL TABLES and ALL SEQUENCES +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES; +-- Specifying WITH clause in an ALL SEQUENCES publication will emit a NOTICE. +SET client_min_messages = 'NOTICE'; +ALTER PUBLICATION regress_pub_for_allsequences_alltables SET (publish = 'insert'); +NOTICE: publication parameters are not applicable to sequence synchronization and will be ignored for sequences +ALTER PUBLICATION regress_pub_for_allsequences_alltables SET (publish_generated_columns = 'stored'); +NOTICE: publication parameters are not applicable to sequence synchronization and will be ignored for sequences +RESET client_min_messages; +SELECT pubname, puballtables, puballsequences FROM pg_publication WHERE pubname = 'regress_pub_for_allsequences_alltables'; + pubname | puballtables | puballsequences +----------------------------------------+--------------+----------------- + regress_pub_for_allsequences_alltables | t | t +(1 row) + +\dRp+ regress_pub_for_allsequences_alltables + Publication regress_pub_for_allsequences_alltables + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | t | t | f | f | f | stored | f | +(1 row) -DROP TABLE testpub_tbl3, testpub_tbl3a; -DROP PUBLICATION testpub3, testpub4; +DROP SEQUENCE regress_pub_seq0, pub_test.regress_pub_seq1; +DROP PUBLICATION regress_pub_forallsequences1; +DROP PUBLICATION regress_pub_forallsequences2; +DROP PUBLICATION regress_pub_for_allsequences_alltables; +-- fail - Specifying ALL TABLES more than once +CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL TABLES; +ERROR: invalid publication object list +LINE 1: ...equences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL TABLES... + ^ +DETAIL: ALL TABLES can be specified only once. +-- fail - Specifying ALL SEQUENCES more than once +CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL SEQUENCES; +ERROR: invalid publication object list +LINE 1: ...equences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL SEQUEN... + ^ +DETAIL: ALL SEQUENCES can be specified only once. -- Tests for partitioned tables SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub_forparted; @@ -251,10 +556,10 @@ UPDATE testpub_parted1 SET a = 1; -- only parent is listed as being in publication, not the partition ALTER PUBLICATION testpub_forparted ADD TABLE testpub_parted; \dRp+ testpub_forparted - Publication testpub_forparted - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_forparted + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.testpub_parted" @@ -269,10 +574,10 @@ ALTER TABLE testpub_parted DETACH PARTITION testpub_parted1; UPDATE testpub_parted1 SET a = 1; ALTER PUBLICATION testpub_forparted SET (publish_via_partition_root = true); \dRp+ testpub_forparted - Publication testpub_forparted - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | t + Publication testpub_forparted + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | t | Tables: "public.testpub_parted" @@ -301,10 +606,10 @@ SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub5 FOR TABLE testpub_rf_tbl1, testpub_rf_tbl2 WHERE (c <> 'test' AND d < 5) WITH (publish = 'insert'); RESET client_min_messages; \dRp+ testpub5 - Publication testpub5 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | f | none | f + Publication testpub5 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | f | none | f | Tables: "public.testpub_rf_tbl1" "public.testpub_rf_tbl2" WHERE ((c <> 'test'::text) AND (d < 5)) @@ -317,10 +622,10 @@ Tables: ALTER PUBLICATION testpub5 ADD TABLE testpub_rf_tbl3 WHERE (e > 1000 AND e < 2000); \dRp+ testpub5 - Publication testpub5 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | f | none | f + Publication testpub5 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | f | none | f | Tables: "public.testpub_rf_tbl1" "public.testpub_rf_tbl2" WHERE ((c <> 'test'::text) AND (d < 5)) @@ -336,10 +641,10 @@ Publications: ALTER PUBLICATION testpub5 DROP TABLE testpub_rf_tbl2; \dRp+ testpub5 - Publication testpub5 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | f | none | f + Publication testpub5 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | f | none | f | Tables: "public.testpub_rf_tbl1" "public.testpub_rf_tbl3" WHERE ((e > 1000) AND (e < 2000)) @@ -347,10 +652,10 @@ Tables: -- remove testpub_rf_tbl1 and add testpub_rf_tbl3 again (another WHERE expression) ALTER PUBLICATION testpub5 SET TABLE testpub_rf_tbl3 WHERE (e > 300 AND e < 500); \dRp+ testpub5 - Publication testpub5 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | f | none | f + Publication testpub5 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | f | none | f | Tables: "public.testpub_rf_tbl3" WHERE ((e > 300) AND (e < 500)) @@ -383,10 +688,10 @@ SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub_syntax1 FOR TABLE testpub_rf_tbl1, ONLY testpub_rf_tbl3 WHERE (e < 999) WITH (publish = 'insert'); RESET client_min_messages; \dRp+ testpub_syntax1 - Publication testpub_syntax1 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | f | none | f + Publication testpub_syntax1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | f | none | f | Tables: "public.testpub_rf_tbl1" "public.testpub_rf_tbl3" WHERE (e < 999) @@ -396,10 +701,10 @@ SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub_syntax2 FOR TABLE testpub_rf_tbl1, testpub_rf_schema1.testpub_rf_tbl5 WHERE (h < 999) WITH (publish = 'insert'); RESET client_min_messages; \dRp+ testpub_syntax2 - Publication testpub_syntax2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | f | none | f + Publication testpub_syntax2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | f | none | f | Tables: "public.testpub_rf_tbl1" "testpub_rf_schema1.testpub_rf_tbl5" WHERE (h < 999) @@ -514,26 +819,32 @@ CREATE PUBLICATION testpub6 FOR TABLES IN SCHEMA testpub_rf_schema2; ALTER PUBLICATION testpub6 SET TABLES IN SCHEMA testpub_rf_schema2, TABLE testpub_rf_schema2.testpub_rf_tbl6 WHERE (i < 99); RESET client_min_messages; \dRp+ testpub6 - Publication testpub6 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub6 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "testpub_rf_schema2.testpub_rf_tbl6" WHERE (i < 99) Tables from schemas: "testpub_rf_schema2" -- fail - virtual generated column uses user-defined function +-- (Actually, this already fails at CREATE TABLE rather than at CREATE +-- PUBLICATION, but let's keep the test in case the former gets +-- relaxed sometime.) CREATE TABLE testpub_rf_tbl6 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * testpub_rf_func2()) VIRTUAL); +ERROR: generation expression uses user-defined function +LINE 1: ...RIMARY KEY, x int, y int GENERATED ALWAYS AS (x * testpub_rf... + ^ +DETAIL: Virtual generated columns that make use of user-defined functions are not yet supported. CREATE PUBLICATION testpub7 FOR TABLE testpub_rf_tbl6 WHERE (y > 100); -ERROR: invalid publication WHERE expression -DETAIL: User-defined or built-in mutable functions are not allowed. +ERROR: relation "testpub_rf_tbl6" does not exist -- test that SET EXPRESSION is rejected, because it could affect a row filter SET client_min_messages = 'ERROR'; CREATE TABLE testpub_rf_tbl7 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * 111) VIRTUAL); CREATE PUBLICATION testpub8 FOR TABLE testpub_rf_tbl7 WHERE (y > 100); ALTER TABLE testpub_rf_tbl7 ALTER COLUMN y SET EXPRESSION AS (x * testpub_rf_func2()); -ERROR: ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns on tables that are part of a publication +ERROR: ALTER TABLE / SET EXPRESSION is not supported for virtual generated columns in tables that are part of a publication DETAIL: Column "y" of relation "testpub_rf_tbl7" is a virtual generated column. RESET client_min_messages; DROP TABLE testpub_rf_tbl1; @@ -541,7 +852,7 @@ DROP TABLE testpub_rf_tbl2; DROP TABLE testpub_rf_tbl3; DROP TABLE testpub_rf_tbl4; DROP TABLE testpub_rf_tbl5; -DROP TABLE testpub_rf_tbl6; +--DROP TABLE testpub_rf_tbl6; DROP TABLE testpub_rf_schema1.testpub_rf_tbl5; DROP TABLE testpub_rf_schema2.testpub_rf_tbl6; DROP SCHEMA testpub_rf_schema1; @@ -803,10 +1114,10 @@ CREATE PUBLICATION testpub_table_ins WITH (publish = 'insert, truncate'); RESET client_min_messages; ALTER PUBLICATION testpub_table_ins ADD TABLE testpub_tbl5 (a); -- ok \dRp+ testpub_table_ins - Publication testpub_table_ins - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | f | f | t | none | f + Publication testpub_table_ins + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | f | f | t | none | f | Tables: "public.testpub_tbl5" (a) @@ -996,10 +1307,10 @@ CREATE TABLE testpub_tbl_both_filters (a int, b int, c int, PRIMARY KEY (a,c)); ALTER TABLE testpub_tbl_both_filters REPLICA IDENTITY USING INDEX testpub_tbl_both_filters_pkey; ALTER PUBLICATION testpub_both_filters ADD TABLE testpub_tbl_both_filters (a,c) WHERE (c != 1); \dRp+ testpub_both_filters - Publication testpub_both_filters - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_both_filters + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.testpub_tbl_both_filters" (a, c) WHERE (c <> 1) @@ -1207,10 +1518,10 @@ ERROR: relation "testpub_tbl1" is already member of publication "testpub_fortbl CREATE PUBLICATION testpub_fortbl FOR TABLE testpub_tbl1; ERROR: publication "testpub_fortbl" already exists \dRp+ testpub_fortbl - Publication testpub_fortbl - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_fortbl + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "pub_test.testpub_nopk" "public.testpub_tbl1" @@ -1250,10 +1561,10 @@ Not-null constraints: "testpub_tbl1_id_not_null" NOT NULL "id" \dRp+ testpub_default - Publication testpub_default - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | f | none | f + Publication testpub_default + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------------ + regress_publication_user | f | f | t | t | t | f | none | f | test publication Tables: "pub_test.testpub_nopk" "public.testpub_tbl1" @@ -1324,19 +1635,32 @@ SET ROLE regress_publication_user3; -- fail - new owner must be superuser ALTER PUBLICATION testpub4 owner to regress_publication_user2; -- fail ERROR: permission denied to change owner of publication "testpub4" -HINT: The owner of a FOR TABLES IN SCHEMA publication must be a superuser. +HINT: The owner of a FOR ALL TABLES or ALL SEQUENCES or TABLES IN SCHEMA publication must be a superuser. ALTER PUBLICATION testpub4 owner to regress_publication_user; -- ok SET ROLE regress_publication_user; -DROP PUBLICATION testpub4; +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub5 FOR ALL TABLES; +RESET client_min_messages; +ALTER PUBLICATION testpub5 OWNER TO regress_publication_user3; +SET ROLE regress_publication_user3; +-- fail - SET ALL TABLES/SEQUENCES on a publication requires superuser privileges +ALTER PUBLICATION testpub5 SET ALL TABLES EXCEPT (TABLE testpub_tbl1); -- fail +ERROR: must be superuser to set ALL TABLES +ALTER PUBLICATION testpub5 SET ALL TABLES; -- fail +ERROR: must be superuser to set ALL TABLES +ALTER PUBLICATION testpub5 SET ALL SEQUENCES; -- fail +ERROR: must be superuser to set ALL SEQUENCES +SET ROLE regress_publication_user; +DROP PUBLICATION testpub4, testpub5; DROP ROLE regress_publication_user3; REVOKE CREATE ON DATABASE regression FROM regress_publication_user2; DROP TABLE testpub_parted; DROP TABLE testpub_tbl1; \dRp+ testpub_default - Publication testpub_default - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | f | none | f + Publication testpub_default + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------------ + regress_publication_user | f | f | t | t | t | f | none | f | test publication (1 row) -- fail - must be owner of publication @@ -1346,20 +1670,20 @@ ERROR: must be owner of publication testpub_default RESET ROLE; ALTER PUBLICATION testpub_default RENAME TO testpub_foo; \dRp testpub_foo - List of publications - Name | Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root --------------+--------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - testpub_foo | regress_publication_user | f | t | t | t | f | none | f + List of publications + Name | Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +-------------+--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + testpub_foo | regress_publication_user | f | f | t | t | t | f | none | f (1 row) -- rename back to keep the rest simple ALTER PUBLICATION testpub_foo RENAME TO testpub_default; ALTER PUBLICATION testpub_default OWNER TO regress_publication_user2; \dRp testpub_default - List of publications - Name | Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ------------------+---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - testpub_default | regress_publication_user2 | f | t | t | t | f | none | f + List of publications + Name | Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +-----------------+---------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + testpub_default | regress_publication_user2 | f | f | t | t | t | f | none | f (1 row) -- adding schemas and tables @@ -1375,19 +1699,19 @@ CREATE TABLE "CURRENT_SCHEMA"."CURRENT_SCHEMA"(id int); SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub1_forschema FOR TABLES IN SCHEMA pub_test1; \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" CREATE PUBLICATION testpub2_forschema FOR TABLES IN SCHEMA pub_test1, pub_test2, pub_test3; \dRp+ testpub2_forschema - Publication testpub2_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub2_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1401,44 +1725,44 @@ CREATE PUBLICATION testpub6_forschema FOR TABLES IN SCHEMA "CURRENT_SCHEMA", CUR CREATE PUBLICATION testpub_fortable FOR TABLE "CURRENT_SCHEMA"."CURRENT_SCHEMA"; RESET client_min_messages; \dRp+ testpub3_forschema - Publication testpub3_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub3_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "public" \dRp+ testpub4_forschema - Publication testpub4_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub4_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "CURRENT_SCHEMA" \dRp+ testpub5_forschema - Publication testpub5_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub5_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "CURRENT_SCHEMA" "public" \dRp+ testpub6_forschema - Publication testpub6_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub6_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "CURRENT_SCHEMA" "public" \dRp+ testpub_fortable - Publication testpub_fortable - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_fortable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "CURRENT_SCHEMA.CURRENT_SCHEMA" @@ -1472,10 +1796,10 @@ ERROR: schema "testpub_view" does not exist -- dropping the schema should reflect the change in publication DROP SCHEMA pub_test3; \dRp+ testpub2_forschema - Publication testpub2_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub2_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1483,20 +1807,20 @@ Tables from schemas: -- renaming the schema should reflect the change in publication ALTER SCHEMA pub_test1 RENAME to pub_test1_renamed; \dRp+ testpub2_forschema - Publication testpub2_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub2_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1_renamed" "pub_test2" ALTER SCHEMA pub_test1_renamed RENAME to pub_test1; \dRp+ testpub2_forschema - Publication testpub2_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub2_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1504,10 +1828,10 @@ Tables from schemas: -- alter publication add schema ALTER PUBLICATION testpub1_forschema ADD TABLES IN SCHEMA pub_test2; \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1516,10 +1840,10 @@ Tables from schemas: ALTER PUBLICATION testpub1_forschema ADD TABLES IN SCHEMA non_existent_schema; ERROR: schema "non_existent_schema" does not exist \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1528,10 +1852,10 @@ Tables from schemas: ALTER PUBLICATION testpub1_forschema ADD TABLES IN SCHEMA pub_test1; ERROR: schema "pub_test1" is already member of publication "testpub1_forschema" \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1539,10 +1863,10 @@ Tables from schemas: -- alter publication drop schema ALTER PUBLICATION testpub1_forschema DROP TABLES IN SCHEMA pub_test2; \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" @@ -1550,10 +1874,10 @@ Tables from schemas: ALTER PUBLICATION testpub1_forschema DROP TABLES IN SCHEMA pub_test2; ERROR: tables from schema "pub_test2" are not part of the publication \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" @@ -1561,29 +1885,29 @@ Tables from schemas: ALTER PUBLICATION testpub1_forschema DROP TABLES IN SCHEMA non_existent_schema; ERROR: schema "non_existent_schema" does not exist \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" -- drop all schemas ALTER PUBLICATION testpub1_forschema DROP TABLES IN SCHEMA pub_test1; \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | (1 row) -- alter publication set multiple schema ALTER PUBLICATION testpub1_forschema SET TABLES IN SCHEMA pub_test1, pub_test2; \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1592,10 +1916,10 @@ Tables from schemas: ALTER PUBLICATION testpub1_forschema SET TABLES IN SCHEMA non_existent_schema; ERROR: schema "non_existent_schema" does not exist \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" "pub_test2" @@ -1604,10 +1928,10 @@ Tables from schemas: -- removing the duplicate schemas ALTER PUBLICATION testpub1_forschema SET TABLES IN SCHEMA pub_test1, pub_test1; \dRp+ testpub1_forschema - Publication testpub1_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub1_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" @@ -1686,18 +2010,18 @@ SET client_min_messages = 'ERROR'; CREATE PUBLICATION testpub3_forschema; RESET client_min_messages; \dRp+ testpub3_forschema - Publication testpub3_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub3_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | (1 row) ALTER PUBLICATION testpub3_forschema SET TABLES IN SCHEMA pub_test1; \dRp+ testpub3_forschema - Publication testpub3_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub3_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables from schemas: "pub_test1" @@ -1707,20 +2031,20 @@ CREATE PUBLICATION testpub_forschema_fortable FOR TABLES IN SCHEMA pub_test1, TA CREATE PUBLICATION testpub_fortable_forschema FOR TABLE pub_test2.tbl1, TABLES IN SCHEMA pub_test1; RESET client_min_messages; \dRp+ testpub_forschema_fortable - Publication testpub_forschema_fortable - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_forschema_fortable + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "pub_test2.tbl1" Tables from schemas: "pub_test1" \dRp+ testpub_fortable_forschema - Publication testpub_fortable_forschema - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication testpub_fortable_forschema + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "pub_test2.tbl1" Tables from schemas: @@ -1837,86 +2161,76 @@ DROP SCHEMA sch1 cascade; DROP SCHEMA sch2 cascade; -- ====================================================== -- Test the 'publish_generated_columns' parameter with the following values: --- 'stored', 'none', and the default (no value specified), which defaults to --- 'stored'. +-- 'stored', 'none'. SET client_min_messages = 'ERROR'; CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_generated_columns = stored); \dRp+ pub1 - Publication pub1 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | t | t | t | t | t | stored | f + Publication pub1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | stored | f | (1 row) CREATE PUBLICATION pub2 FOR ALL TABLES WITH (publish_generated_columns = none); \dRp+ pub2 - Publication pub2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | t | t | t | t | t | none | f -(1 row) - -CREATE PUBLICATION pub3 FOR ALL TABLES WITH (publish_generated_columns); -\dRp+ pub3 - Publication pub3 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | t | t | t | t | t | stored | f + Publication pub2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | t | f | t | t | t | t | none | f | (1 row) DROP PUBLICATION pub1; DROP PUBLICATION pub2; -DROP PUBLICATION pub3; -- Test the 'publish_generated_columns' parameter as 'none' and 'stored' for -- different scenarios with/without generated columns in column lists. CREATE TABLE gencols (a int, gen1 int GENERATED ALWAYS AS (a * 2) STORED); -- Generated columns in column list, when 'publish_generated_columns'='none' CREATE PUBLICATION pub1 FOR table gencols(a, gen1) WITH (publish_generated_columns = none); \dRp+ pub1 - Publication pub1 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication pub1 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.gencols" (a, gen1) -- Generated columns in column list, when 'publish_generated_columns'='stored' CREATE PUBLICATION pub2 FOR table gencols(a, gen1) WITH (publish_generated_columns = stored); \dRp+ pub2 - Publication pub2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | stored | f + Publication pub2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | stored | f | Tables: "public.gencols" (a, gen1) -- Generated columns in column list, then set 'publish_generated_columns'='none' ALTER PUBLICATION pub2 SET (publish_generated_columns = none); \dRp+ pub2 - Publication pub2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication pub2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.gencols" (a, gen1) -- Remove generated columns from column list, when 'publish_generated_columns'='none' ALTER PUBLICATION pub2 SET TABLE gencols(a); \dRp+ pub2 - Publication pub2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication pub2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.gencols" (a) -- Add generated columns in column list, when 'publish_generated_columns'='none' ALTER PUBLICATION pub2 SET TABLE gencols(a, gen1); \dRp+ pub2 - Publication pub2 - Owner | All tables | Inserts | Updates | Deletes | Truncates | Generated columns | Via root ---------------------------+------------+---------+---------+---------+-----------+-------------------+---------- - regress_publication_user | f | t | t | t | t | none | f + Publication pub2 + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root | Description +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+----------+------------- + regress_publication_user | f | f | t | t | t | t | none | f | Tables: "public.gencols" (a, gen1) @@ -1924,6 +2238,303 @@ DROP PUBLICATION pub1; DROP PUBLICATION pub2; DROP TABLE gencols; RESET client_min_messages; +-- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY +-- when the target table is published. +CREATE TABLE testpub_insert_onconfl_no_ri (a int unique, b int); +CREATE TABLE testpub_insert_onconfl_parted (a int unique, b int) PARTITION by RANGE (a); +CREATE TABLE testpub_insert_onconfl_part_no_ri PARTITION OF testpub_insert_onconfl_parted FOR VALUES FROM (1) TO (10); +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub1 FOR ALL TABLES; +RESET client_min_messages; +-- fail - missing REPLICA IDENTITY +INSERT INTO testpub_insert_onconfl_no_ri VALUES (1, 1) ON CONFLICT (a) DO UPDATE SET b = 2; +ERROR: cannot update table "testpub_insert_onconfl_no_ri" because it does not have a replica identity and publishes updates +HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE. +-- ok - no updates +INSERT INTO testpub_insert_onconfl_no_ri VALUES (1, 1) ON CONFLICT DO NOTHING; +-- fail - missing REPLICA IDENTITY in partition testpub_insert_onconfl_no_ri +INSERT INTO testpub_insert_onconfl_parted VALUES (1, 1) ON CONFLICT (a) DO UPDATE SET b = 2; +ERROR: cannot update table "testpub_insert_onconfl_part_no_ri" because it does not have a replica identity and publishes updates +HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE. +-- ok - no updates +INSERT INTO testpub_insert_onconfl_parted VALUES (1, 1) ON CONFLICT DO NOTHING; +DROP PUBLICATION pub1; +DROP TABLE testpub_insert_onconfl_no_ri; +DROP TABLE testpub_insert_onconfl_parted; +-- Test that the MERGE command correctly checks REPLICA IDENTITY when the +-- target table is published. +CREATE TABLE testpub_merge_no_ri (a int, b int); +CREATE TABLE testpub_merge_pk (a int primary key, b int); +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub1 FOR ALL TABLES; +RESET client_min_messages; +-- fail - missing REPLICA IDENTITY +MERGE INTO testpub_merge_no_ri USING testpub_merge_pk s ON s.a >= 1 + WHEN MATCHED THEN UPDATE SET b = s.b; +ERROR: cannot update table "testpub_merge_no_ri" because it does not have a replica identity and publishes updates +HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE. +-- fail - missing REPLICA IDENTITY +MERGE INTO testpub_merge_no_ri USING testpub_merge_pk s ON s.a >= 1 + WHEN MATCHED THEN DELETE; +ERROR: cannot delete from table "testpub_merge_no_ri" because it does not have a replica identity and publishes deletes +HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE. +-- ok - insert and do nothing are not restricted +MERGE INTO testpub_merge_no_ri USING testpub_merge_pk s ON s.a >= 1 + WHEN MATCHED THEN DO NOTHING + WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, 0); +-- ok - REPLICA IDENTITY is DEFAULT and table has a PK +MERGE INTO testpub_merge_pk USING testpub_merge_no_ri s ON s.a >= 1 + WHEN MATCHED AND s.a > 0 THEN UPDATE SET b = s.b + WHEN MATCHED THEN DELETE; +DROP PUBLICATION pub1; +DROP TABLE testpub_merge_no_ri; +DROP TABLE testpub_merge_pk; RESET SESSION AUTHORIZATION; DROP ROLE regress_publication_user, regress_publication_user2; DROP ROLE regress_publication_user_dummy; +-- Test pg_get_publication_tables(text[], oid) function +CREATE SCHEMA gpt_test_sch; +CREATE TABLE gpt_test_sch.tbl_sch (id int); +CREATE TABLE tbl_normal (id int); +CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1); +CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10); +CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal; +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch; +CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10); +CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true); +RESET client_min_messages; +CREATE FUNCTION test_gpt(pubnames text[], relname text) +RETURNS TABLE ( + pubname text, + relname name, + attrs text, + qual text +) +BEGIN ATOMIC + SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid) + FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt + JOIN pg_publication p ON p.oid = gpt.pubid + JOIN pg_class c ON c.oid = gpt.relid + ORDER BY p.pubname, c.relname; +END; +SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal'); + pubname | relname | attrs | qual +------------+------------+-------+----------- + pub_normal | tbl_normal | 1 | (id < 10) +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch'); + pubname | relname | attrs | qual +------------+---------+-------+------ + pub_schema | tbl_sch | 1 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent'); + pubname | relname | attrs | qual +-----------------+------------+-------+------------ + pub_part_parent | tbl_parent | 1 2 | (id1 = 10) +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1'); + pubname | relname | attrs | qual +----------------------------+-----------+-------+------ + pub_part_parent_no_viaroot | tbl_part1 | 1 2 3 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1'); + pubname | relname | attrs | qual +---------------+-----------+-------+------ + pub_part_leaf | tbl_part1 | 1 2 3 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent'); + pubname | relname | attrs | qual +---------+------------+-------+------ + pub_all | tbl_parent | 1 2 3 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1'); + pubname | relname | attrs | qual +--------------------+-----------+-------+------ + pub_all_no_viaroot | tbl_part1 | 1 2 3 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent'); + pubname | relname | attrs | qual +-----------------------+------------+-------+------ + pub_part_parent_child | tbl_parent | 1 2 3 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- test for the EXCEPT clause +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal'); + pubname | relname | attrs | qual +----------------+------------+-------+------ + pub_all_except | tbl_normal | 1 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded) + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal'); + pubname | relname | attrs | qual +---------------------------+------------+-------+------ + pub_all_except_no_viaroot | tbl_normal | 1 | +(1 row) + +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded) + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- two rows with different row filter +SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal'); + pubname | relname | attrs | qual +------------+------------+-------+----------- + pub_all | tbl_normal | 1 | + pub_normal | tbl_normal | 1 | (id < 10) +(2 rows) + +-- one row with 'pub_part_parent' +SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent'); + pubname | relname | attrs | qual +-----------------+------------+-------+------------ + pub_part_parent | tbl_parent | 1 2 | (id1 = 10) +(1 row) + +-- no result, tbl_parent is the effective published OID due to pubviaroot +SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1'); + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- no result, non-existent publication +SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal'); + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- no result, non-table object +SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view'); + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- no result, empty publication array +SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal'); + pubname | relname | attrs | qual +---------+---------+-------+------ +(0 rows) + +-- no result, OID 0 as target_relid +SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid); + pubid | relid | attrs | qual +-------+-------+-------+------ +(0 rows) + +-- Clean up +DROP FUNCTION test_gpt(text[], text); +DROP PUBLICATION pub_all; +DROP PUBLICATION pub_all_no_viaroot; +DROP PUBLICATION pub_all_except; +DROP PUBLICATION pub_all_except_no_viaroot; +DROP PUBLICATION pub_schema; +DROP PUBLICATION pub_normal; +DROP PUBLICATION pub_part_leaf; +DROP PUBLICATION pub_part_parent; +DROP PUBLICATION pub_part_parent_no_viaroot; +DROP PUBLICATION pub_part_parent_child; +DROP VIEW gpt_test_view; +DROP TABLE tbl_normal, tbl_parent, tbl_part1; +DROP SCHEMA gpt_test_sch CASCADE; +NOTICE: drop cascades to table gpt_test_sch.tbl_sch +-- stage objects for pg_dump tests +CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int); +CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int); +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION dump_pub_qual_1ct FOR + TABLE ONLY pubme.t0 (c, d) WHERE (c > 0); +CREATE PUBLICATION dump_pub_qual_2ct FOR + TABLE ONLY pubme.t0 (c) WHERE (c > 0), + TABLE ONLY pubme.t1 (c); +CREATE PUBLICATION dump_pub_nsp_1ct FOR + TABLES IN SCHEMA pubme; +CREATE PUBLICATION dump_pub_nsp_2ct FOR + TABLES IN SCHEMA pubme, + TABLES IN SCHEMA pubme2; +CREATE PUBLICATION dump_pub_all FOR + TABLE ONLY pubme.t0, + TABLE ONLY pubme.t1 WHERE (c < 0), + TABLES IN SCHEMA pubme, + TABLES IN SCHEMA pubme2 + WITH (publish_via_partition_root = true); +RESET client_min_messages; diff --git a/src/test/regress/expected/random.out b/src/test/regress/expected/random.out index 43cf88a36341b..7f17b2a1b12f8 100644 --- a/src/test/regress/expected/random.out +++ b/src/test/regress/expected/random.out @@ -536,3 +536,90 @@ SELECT n, random(0, trim_scale(abs(1 - 10.0^(-n)))) FROM generate_series(-20, 20 20 | 0.60795101234744211935 (41 rows) +-- random dates +SELECT random('1979-02-08'::date,'2025-07-03'::date) AS random_date_multiple_years; + random_date_multiple_years +---------------------------- + 04-09-1986 +(1 row) + +SELECT random('4714-11-24 BC'::date,'5874897-12-31 AD'::date) AS random_date_maximum_range; + random_date_maximum_range +--------------------------- + 10-02-2898131 +(1 row) + +SELECT random('1979-02-08'::date,'1979-02-08'::date) AS random_date_empty_range; + random_date_empty_range +------------------------- + 02-08-1979 +(1 row) + +SELECT random('2024-12-31'::date, '2024-01-01'::date); -- fail +ERROR: lower bound must be less than or equal to upper bound +SELECT random('-infinity'::date, '2024-01-01'::date); -- fail +ERROR: lower and upper bounds must be finite +SELECT random('2024-12-31'::date, 'infinity'::date); -- fail +ERROR: lower and upper bounds must be finite +-- random timestamps +SELECT random('1979-02-08'::timestamp,'2025-07-03'::timestamp) AS random_timestamp_multiple_years; + random_timestamp_multiple_years +--------------------------------- + Fri Jan 27 18:52:05.366009 2017 +(1 row) + +SELECT random('4714-11-24 BC'::timestamp,'294276-12-31 23:59:59.999999'::timestamp) AS random_timestamp_maximum_range; + random_timestamp_maximum_range +----------------------------------- + Wed Mar 28 00:45:36.180395 226694 +(1 row) + +SELECT random('2024-07-01 12:00:00.000001'::timestamp, '2024-07-01 12:00:00.999999'::timestamp) AS random_narrow_range; + random_narrow_range +--------------------------------- + Mon Jul 01 12:00:00.999286 2024 +(1 row) + +SELECT random('1979-02-08'::timestamp,'1979-02-08'::timestamp) AS random_timestamp_empty_range; + random_timestamp_empty_range +------------------------------ + Thu Feb 08 00:00:00 1979 +(1 row) + +SELECT random('2024-12-31'::timestamp, '2024-01-01'::timestamp); -- fail +ERROR: lower bound must be less than or equal to upper bound +SELECT random('-infinity'::timestamp, '2024-01-01'::timestamp); -- fail +ERROR: lower and upper bounds must be finite +SELECT random('2024-12-31'::timestamp, 'infinity'::timestamp); -- fail +ERROR: lower and upper bounds must be finite +-- random timestamps with timezone +SELECT random('1979-02-08 +01'::timestamptz,'2025-07-03 +02'::timestamptz) AS random_timestamptz_multiple_years; + random_timestamptz_multiple_years +------------------------------------- + Tue Jun 14 04:41:16.652896 2016 PDT +(1 row) + +SELECT random('4714-11-24 BC +00'::timestamptz,'294276-12-31 23:59:59.999999 +00'::timestamptz) AS random_timestamptz_maximum_range; + random_timestamptz_maximum_range +-------------------------------------- + Wed Mar 26 14:07:16.980265 31603 PDT +(1 row) + +SELECT random('2024-07-01 12:00:00.000001 +04'::timestamptz, '2024-07-01 12:00:00.999999 +04'::timestamptz) AS random_timestamptz_narrow_range; + random_timestamptz_narrow_range +------------------------------------- + Mon Jul 01 01:00:00.835808 2024 PDT +(1 row) + +SELECT random('1979-02-08 +05'::timestamptz,'1979-02-08 +05'::timestamptz) AS random_timestamptz_empty_range; + random_timestamptz_empty_range +-------------------------------- + Wed Feb 07 11:00:00 1979 PST +(1 row) + +SELECT random('2024-01-01 +06'::timestamptz, '2024-01-01 +07'::timestamptz); -- fail +ERROR: lower bound must be less than or equal to upper bound +SELECT random('-infinity'::timestamptz, '2024-01-01 +07'::timestamptz); -- fail +ERROR: lower and upper bounds must be finite +SELECT random('2024-01-01 +06'::timestamptz, 'infinity'::timestamptz); -- fail +ERROR: lower and upper bounds must be finite diff --git a/src/test/regress/expected/rangefuncs.out b/src/test/regress/expected/rangefuncs.out index c21be83aa4aaf..5cc94011e978f 100644 --- a/src/test/regress/expected/rangefuncs.out +++ b/src/test/regress/expected/rangefuncs.out @@ -2130,10 +2130,10 @@ select testrngfunc(); explain (verbose, costs off) select * from testrngfunc(); - QUERY PLAN ----------------------------------------------------------- - Subquery Scan on "*SELECT*" - Output: "*SELECT*"."?column?", "*SELECT*"."?column?_1" + QUERY PLAN +---------------------------------------------------------------------- + Subquery Scan on unnamed_subquery + Output: unnamed_subquery."?column?", unnamed_subquery."?column?_1" -> Unique Output: (1), (2) -> Sort @@ -2178,6 +2178,8 @@ alter table users drop column todrop; create or replace function get_first_user() returns users as $$ SELECT * FROM users ORDER BY userid LIMIT 1; $$ language sql stable; +NOTICE: function "get_first_user" will be effectively temporary +DETAIL: It depends on temporary type users. SELECT get_first_user(); get_first_user ------------------- @@ -2193,6 +2195,8 @@ SELECT * FROM get_first_user(); create or replace function get_users() returns setof users as $$ SELECT * FROM users ORDER BY userid; $$ language sql stable; +NOTICE: function "get_users" will be effectively temporary +DETAIL: It depends on temporary type users. SELECT get_users(); get_users --------------------- diff --git a/src/test/regress/expected/rangetypes.out b/src/test/regress/expected/rangetypes.out index a7cc220bf0d64..e062a4e5c2c81 100644 --- a/src/test/regress/expected/rangetypes.out +++ b/src/test/regress/expected/rangetypes.out @@ -481,6 +481,60 @@ select range_minus(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]')); empty (1 row) +select range_minus_multi('empty'::numrange, numrange(2.0, 3.0)); + range_minus_multi +------------------- +(0 rows) + +select range_minus_multi(numrange(1.1, 2.2), 'empty'::numrange); + range_minus_multi +------------------- + [1.1,2.2) +(1 row) + +select range_minus_multi(numrange(1.1, 2.2), numrange(2.0, 3.0)); + range_minus_multi +------------------- + [1.1,2.0) +(1 row) + +select range_minus_multi(numrange(1.1, 2.2), numrange(2.2, 3.0)); + range_minus_multi +------------------- + [1.1,2.2) +(1 row) + +select range_minus_multi(numrange(1.1, 2.2,'[]'), numrange(2.0, 3.0)); + range_minus_multi +------------------- + [1.1,2.0) +(1 row) + +select range_minus_multi(numrange(1.0, 3.0), numrange(1.5, 2.0)); + range_minus_multi +------------------- + [1.0,1.5) + [2.0,3.0) +(2 rows) + +select range_minus_multi(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]')); + range_minus_multi +------------------- + [10.1,12.2] +(1 row) + +select range_minus_multi(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]')); + range_minus_multi +------------------- +(0 rows) + +select range_minus_multi(numrange(1.0,3.0,'[]'), numrange(1.5,2.0,'(]')); + range_minus_multi +------------------- + [1.0,1.5] + (2.0,3.0] +(2 rows) + select numrange(4.5, 5.5, '[]') && numrange(5.5, 6.5); ?column? ---------- @@ -1630,7 +1684,8 @@ select anyarray_anyrange_func(ARRAY[1,2], numrange(10,20)); ERROR: function anyarray_anyrange_func(integer[], numrange) does not exist LINE 1: select anyarray_anyrange_func(ARRAY[1,2], numrange(10,20)); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anyarray_anyrange_func(anyarray, anyrange); -- should fail create function bogus_func(anyelement) @@ -1669,7 +1724,8 @@ select rangetypes_sql(numrange(1,10), ARRAY[2,20]); -- match failure ERROR: function rangetypes_sql(numrange, integer[]) does not exist LINE 1: select rangetypes_sql(numrange(1,10), ARRAY[2,20]); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. create function anycompatiblearray_anycompatiblerange_func(a anycompatiblearray, r anycompatiblerange) returns anycompatible as 'select $1[1] + lower($2);' language sql; select anycompatiblearray_anycompatiblerange_func(ARRAY[1,2], int4range(10,20)); @@ -1689,7 +1745,8 @@ select anycompatiblearray_anycompatiblerange_func(ARRAY[1.1,2], int4range(10,20) ERROR: function anycompatiblearray_anycompatiblerange_func(numeric[], int4range) does not exist LINE 1: select anycompatiblearray_anycompatiblerange_func(ARRAY[1.1,... ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. drop function anycompatiblearray_anycompatiblerange_func(anycompatiblearray, anycompatiblerange); -- should fail create function bogus_func(anycompatible) diff --git a/src/test/regress/expected/regex.out b/src/test/regress/expected/regex.out index ae0de7307db73..9fd503ffe78df 100644 --- a/src/test/regress/expected/regex.out +++ b/src/test/regress/expected/regex.out @@ -1,8 +1,6 @@ -- -- Regular expression tests -- --- Don't want to have to double backslashes in regexes -set standard_conforming_strings = on; -- Test simple quantified backrefs select 'bbbbb' ~ '^([bc])\1*$' as t; t diff --git a/src/test/regress/expected/regproc.out b/src/test/regress/expected/regproc.out index 97b917502cabb..84c84aef4207f 100644 --- a/src/test/regress/expected/regproc.out +++ b/src/test/regress/expected/regproc.out @@ -192,6 +192,18 @@ SELECT regnamespace('"pg_catalog"'); pg_catalog (1 row) +SELECT regdatabase('template1'); + regdatabase +------------- + template1 +(1 row) + +SELECT regdatabase('"template1"'); + regdatabase +------------- + template1 +(1 row) + SELECT to_regrole('regress_regrole_test'); to_regrole ---------------------- @@ -216,6 +228,132 @@ SELECT to_regnamespace('"pg_catalog"'); pg_catalog (1 row) +SELECT to_regdatabase('template1'); + to_regdatabase +---------------- + template1 +(1 row) + +SELECT to_regdatabase('"template1"'); + to_regdatabase +---------------- + template1 +(1 row) + +-- special "single dash" case +SELECT regproc('-')::oid; + regproc +--------- + 0 +(1 row) + +SELECT regprocedure('-')::oid; + regprocedure +-------------- + 0 +(1 row) + +SELECT regclass('-')::oid; + regclass +---------- + 0 +(1 row) + +SELECT regcollation('-')::oid; + regcollation +-------------- + 0 +(1 row) + +SELECT regtype('-')::oid; + regtype +--------- + 0 +(1 row) + +SELECT regconfig('-')::oid; + regconfig +----------- + 0 +(1 row) + +SELECT regdictionary('-')::oid; + regdictionary +--------------- + 0 +(1 row) + +SELECT regrole('-')::oid; + regrole +--------- + 0 +(1 row) + +SELECT regnamespace('-')::oid; + regnamespace +-------------- + 0 +(1 row) + +SELECT regdatabase('-')::oid; + regdatabase +------------- + 0 +(1 row) + +SELECT to_regproc('-')::oid; + to_regproc +------------ + 0 +(1 row) + +SELECT to_regprocedure('-')::oid; + to_regprocedure +----------------- + 0 +(1 row) + +SELECT to_regclass('-')::oid; + to_regclass +------------- + 0 +(1 row) + +SELECT to_regcollation('-')::oid; + to_regcollation +----------------- + 0 +(1 row) + +SELECT to_regtype('-')::oid; + to_regtype +------------ + 0 +(1 row) + +SELECT to_regrole('-')::oid; + to_regrole +------------ + 0 +(1 row) + +SELECT to_regnamespace('-')::oid; + to_regnamespace +----------------- + 0 +(1 row) + +SELECT to_regdatabase('-')::oid; + to_regdatabase +---------------- + 0 +(1 row) + +-- constant cannot be used here +CREATE TABLE regrole_test (rolid OID DEFAULT 'regress_regrole_test'::regrole); +ERROR: constant of the type regrole cannot be used here +CREATE TABLE regdatabase_test (datid OID DEFAULT 'template1'::regdatabase); +ERROR: constant of the type regdatabase cannot be used here /* If objects don't exist, raise errors. */ DROP ROLE regress_regrole_test; -- without schemaname @@ -305,6 +443,18 @@ SELECT regnamespace('foo.bar'); ERROR: invalid name syntax LINE 1: SELECT regnamespace('foo.bar'); ^ +SELECT regdatabase('Nonexistent'); +ERROR: database "nonexistent" does not exist +LINE 1: SELECT regdatabase('Nonexistent'); + ^ +SELECT regdatabase('"Nonexistent"'); +ERROR: database "Nonexistent" does not exist +LINE 1: SELECT regdatabase('"Nonexistent"'); + ^ +SELECT regdatabase('foo.bar'); +ERROR: invalid name syntax +LINE 1: SELECT regdatabase('foo.bar'); + ^ /* If objects don't exist, return NULL with no error. */ -- without schemaname SELECT to_regoper('||//'); @@ -447,6 +597,24 @@ SELECT to_regnamespace('foo.bar'); (1 row) +SELECT to_regdatabase('Nonexistent'); + to_regdatabase +---------------- + +(1 row) + +SELECT to_regdatabase('"Nonexistent"'); + to_regdatabase +---------------- + +(1 row) + +SELECT to_regdatabase('foo.bar'); + to_regdatabase +---------------- + +(1 row) + -- Test to_regtypemod SELECT to_regtypemod('text'); to_regtypemod @@ -569,6 +737,12 @@ SELECT * FROM pg_input_error_info('no_such_type', 'regtype'); type "no_such_type" does not exist | | | 42704 (1 row) +SELECT * FROM pg_input_error_info('Nonexistent', 'regdatabase'); + message | detail | hint | sql_error_code +---------------------------------------+--------+------+---------------- + database "nonexistent" does not exist | | | 42704 +(1 row) + -- Some cases that should be soft errors, but are not yet SELECT * FROM pg_input_error_info('incorrect type name syntax', 'regtype'); ERROR: syntax error at or near "type" diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out index 9de19b4e3f13d..e3a974f26112e 100644 --- a/src/test/regress/expected/reloptions.out +++ b/src/test/regress/expected/reloptions.out @@ -98,6 +98,24 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; {fillfactor=13,autovacuum_enabled=false} (1 row) +-- Tests for ternary options +-- behave as boolean option: accept unassigned name and truncated value +DROP TABLE reloptions_test; +CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate); +SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + reloptions +------------------------ + {vacuum_truncate=true} +(1 row) + +DROP TABLE reloptions_test; +CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS); +SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + reloptions +------------------------ + {vacuum_truncate=fals} +(1 row) + -- Test vacuum_truncate option DROP TABLE reloptions_test; CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text) diff --git a/src/test/regress/expected/replica_identity.out b/src/test/regress/expected/replica_identity.out index b9b8dde018f08..336b04fa2788d 100644 --- a/src/test/regress/expected/replica_identity.out +++ b/src/test/regress/expected/replica_identity.out @@ -262,7 +262,8 @@ Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) INVALID REPLICA IDENTITY Not-null constraints: "test_replica_identity4_id_not_null" NOT NULL "id" -Partitions: test_replica_identity4_1 FOR VALUES IN (1) +Partitions: + test_replica_identity4_1 FOR VALUES IN (1) ALTER INDEX test_replica_identity4_pkey ATTACH PARTITION test_replica_identity4_1_pkey; @@ -276,7 +277,8 @@ Indexes: "test_replica_identity4_pkey" PRIMARY KEY, btree (id) REPLICA IDENTITY Not-null constraints: "test_replica_identity4_id_not_null" NOT NULL "id" -Partitions: test_replica_identity4_1 FOR VALUES IN (1) +Partitions: + test_replica_identity4_1 FOR VALUES IN (1) -- Dropping the primary key is not allowed if that would leave the replica -- identity as nullable diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out index 341b689f76655..196829e94fa4b 100644 --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -306,6 +306,8 @@ SELECT * FROM foo; -- Check use of a whole-row variable for an inlined set-returning function CREATE FUNCTION foo_f() RETURNS SETOF foo AS $$ SELECT * FROM foo OFFSET 0 $$ LANGUAGE sql STABLE; +NOTICE: function "foo_f" will be effectively temporary +DETAIL: It depends on temporary type foo. UPDATE foo SET f2 = foo_f.f2 FROM foo_f() WHERE foo_f.f1 = foo.f1 RETURNING foo_f; foo_f @@ -548,16 +550,16 @@ INSERT INTO foo VALUES (5, 'subquery test') QUERY PLAN --------------------------------------------------------------- Insert on pg_temp.foo - Output: (SubPlan 1), (SubPlan 2) + Output: (SubPlan expr_1), (SubPlan expr_2) -> Result Output: 5, 'subquery test'::text, 42, '99'::bigint - SubPlan 1 + SubPlan expr_1 -> Aggregate Output: max((old.f4 + x.x)) -> Function Scan on pg_catalog.generate_series x Output: x.x Function Call: generate_series(1, 10) - SubPlan 2 + SubPlan expr_2 -> Aggregate Output: max((new.f4 + x_1.x)) -> Function Scan on pg_catalog.generate_series x_1 @@ -578,26 +580,26 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5 RETURNING (SELECT old.f4 = new.f4), (SELECT max(old.f4 + x) FROM generate_series(1, 10) x) old_max, (SELECT max(new.f4 + x) FROM generate_series(1, 10) x) new_max; - QUERY PLAN ---------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------- Update on pg_temp.foo - Output: (SubPlan 1), (SubPlan 2), (SubPlan 3) + Output: (SubPlan expr_1), (SubPlan expr_2), (SubPlan expr_3) Update on pg_temp.foo foo_1 -> Result Output: '100'::bigint, foo_1.tableoid, foo_1.ctid -> Seq Scan on pg_temp.foo foo_1 Output: foo_1.tableoid, foo_1.ctid Filter: (foo_1.f1 = 5) - SubPlan 1 + SubPlan expr_1 -> Result Output: (old.f4 = new.f4) - SubPlan 2 + SubPlan expr_2 -> Aggregate Output: max((old.f4 + x.x)) -> Function Scan on pg_catalog.generate_series x Output: x.x Function Call: generate_series(1, 10) - SubPlan 3 + SubPlan expr_3 -> Aggregate Output: max((new.f4 + x_1.x)) -> Function Scan on pg_catalog.generate_series x_1 @@ -621,18 +623,18 @@ DELETE FROM foo WHERE f1 = 5 QUERY PLAN --------------------------------------------------------------- Delete on pg_temp.foo - Output: (SubPlan 1), (SubPlan 2) + Output: (SubPlan expr_1), (SubPlan expr_2) Delete on pg_temp.foo foo_1 -> Seq Scan on pg_temp.foo foo_1 Output: foo_1.tableoid, foo_1.ctid Filter: (foo_1.f1 = 5) - SubPlan 1 + SubPlan expr_1 -> Aggregate Output: max((old.f4 + x.x)) -> Function Scan on pg_catalog.generate_series x Output: x.x Function Call: generate_series(1, 10) - SubPlan 2 + SubPlan expr_2 -> Aggregate Output: max((new.f4 + x_1.x)) -> Function Scan on pg_catalog.generate_series x_1 @@ -930,6 +932,8 @@ BEGIN ATOMIC (SELECT count(*) FROM foo WHERE foo = o), (SELECT count(*) FROM foo WHERE foo = n); END; +NOTICE: function "foo_update" will be effectively temporary +DETAIL: It depends on temporary table foo. \sf foo_update CREATE OR REPLACE FUNCTION public.foo_update() RETURNS void @@ -986,3 +990,34 @@ BEGIN ATOMIC WHERE (foo_1.* = n.*)) AS count; END DROP FUNCTION foo_update; +-- Test that the planner does not fold OLD/NEW IS NULL tests to constants +-- based on NOT NULL constraints, since OLD is NULL for INSERT and NEW is +-- NULL for DELETE. +CREATE TEMP TABLE ret_nn (a int NOT NULL); +-- INSERT has no OLD row, should return true +INSERT INTO ret_nn VALUES (1) RETURNING old.a IS NULL; + ?column? +---------- + t +(1 row) + +-- DELETE has no NEW row, should return true +DELETE FROM ret_nn WHERE a = 1 RETURNING new.a IS NULL; + ?column? +---------- + t +(1 row) + +-- MERGE: DELETE should have new.a IS NULL, INSERT should have old.a IS NULL +INSERT INTO ret_nn VALUES (2); +MERGE INTO ret_nn USING (VALUES (2), (3)) AS src(a) ON ret_nn.a = src.a + WHEN MATCHED THEN DELETE + WHEN NOT MATCHED THEN INSERT VALUES (src.a) + RETURNING merge_action(), old.a IS NULL, new.a IS NULL; + merge_action | ?column? | ?column? +--------------+----------+---------- + DELETE | f | t + INSERT | t | f +(2 rows) + +DROP TABLE ret_nn; diff --git a/src/test/regress/expected/role_ddl.out b/src/test/regress/expected/role_ddl.out new file mode 100644 index 0000000000000..575111da55c52 --- /dev/null +++ b/src/test/regress/expected/role_ddl.out @@ -0,0 +1,143 @@ +-- Consistent test results +SET timezone TO 'UTC'; +SET DateStyle TO 'ISO, YMD'; +-- Create test database +CREATE DATABASE regression_role_ddl_test; +-- Basic role +CREATE ROLE regress_role_ddl_test1; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1'); + pg_get_role_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_test1 NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; +(1 row) + +-- Role with LOGIN +CREATE ROLE regress_role_ddl_test2 LOGIN; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2'); + pg_get_role_ddl +----------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_test2 NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS; +(1 row) + +-- Role with multiple privileges +CREATE ROLE regress_role_ddl_test3 + LOGIN + SUPERUSER + CREATEDB + CREATEROLE + CONNECTION LIMIT 5 + VALID UNTIL '2030-12-31 23:59:59+00'; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test3'); + pg_get_role_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_test3 SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN NOREPLICATION NOBYPASSRLS CONNECTION LIMIT 5 VALID UNTIL '2030-12-31 23:59:59+00'; +(1 row) + +-- Role with configuration parameters +CREATE ROLE regress_role_ddl_test4; +ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB'; +ALTER ROLE regress_role_ddl_test4 SET search_path TO myschema, public; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test4'); + pg_get_role_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_test4 NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; + ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB'; + ALTER ROLE regress_role_ddl_test4 SET search_path TO 'myschema', 'public'; +(3 rows) + +-- Role with database-specific configuration +CREATE ROLE regress_role_ddl_test5; +ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET work_mem TO '128MB'; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test5'); + pg_get_role_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_test5 NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; + ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET work_mem TO '128MB'; +(2 rows) + +-- Role with special characters (requires quoting) +CREATE ROLE "regress_role-with-dash"; +SELECT * FROM pg_get_role_ddl('regress_role-with-dash'); + pg_get_role_ddl +--------------------------------------------------------------------------------------------------------------------- + CREATE ROLE "regress_role-with-dash" NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; +(1 row) + +-- Pretty-printed output +\pset format unaligned +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test3', 'pretty', 'true'); +pg_get_role_ddl +CREATE ROLE regress_role_ddl_test3 + SUPERUSER + INHERIT + CREATEROLE + CREATEDB + LOGIN + NOREPLICATION + NOBYPASSRLS + CONNECTION LIMIT 5 + VALID UNTIL '2030-12-31 23:59:59+00'; +(1 row) +\pset format aligned +-- Role with memberships +CREATE ROLE regress_role_ddl_grantor CREATEROLE; +CREATE ROLE regress_role_ddl_group1; +CREATE ROLE regress_role_ddl_group2; +CREATE ROLE regress_role_ddl_member; +GRANT regress_role_ddl_group1 TO regress_role_ddl_grantor WITH ADMIN TRUE; +GRANT regress_role_ddl_group2 TO regress_role_ddl_grantor WITH ADMIN TRUE; +SET ROLE regress_role_ddl_grantor; +GRANT regress_role_ddl_group1 TO regress_role_ddl_member WITH INHERIT TRUE, SET FALSE; +GRANT regress_role_ddl_group2 TO regress_role_ddl_member WITH ADMIN TRUE; +RESET ROLE; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_member'); + pg_get_role_ddl +----------------------------------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_member NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; + GRANT regress_role_ddl_group1 TO regress_role_ddl_member WITH ADMIN FALSE, INHERIT TRUE, SET FALSE GRANTED BY regress_role_ddl_grantor; + GRANT regress_role_ddl_group2 TO regress_role_ddl_member WITH ADMIN TRUE, INHERIT TRUE, SET TRUE GRANTED BY regress_role_ddl_grantor; +(3 rows) + +-- Role with memberships suppressed +SELECT * FROM pg_get_role_ddl('regress_role_ddl_member', 'memberships', 'false'); + pg_get_role_ddl +-------------------------------------------------------------------------------------------------------------------- + CREATE ROLE regress_role_ddl_member NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB NOLOGIN NOREPLICATION NOBYPASSRLS; +(1 row) + +-- Non-existent role (should error) +SELECT * FROM pg_get_role_ddl(9999999::oid); +ERROR: role with OID 9999999 does not exist +-- NULL input (should return no rows) +SELECT * FROM pg_get_role_ddl(NULL); + pg_get_role_ddl +----------------- +(0 rows) + +-- Permission check: revoke SELECT on pg_authid +CREATE ROLE regress_role_ddl_noaccess; +REVOKE SELECT ON pg_authid FROM PUBLIC; +SET ROLE regress_role_ddl_noaccess; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1'); -- should fail +ERROR: permission denied for role regress_role_ddl_test1 +RESET ROLE; +GRANT SELECT ON pg_authid TO PUBLIC; +DROP ROLE regress_role_ddl_noaccess; +-- Cleanup +DROP ROLE regress_role_ddl_test1; +DROP ROLE regress_role_ddl_test2; +DROP ROLE regress_role_ddl_test3; +DROP ROLE regress_role_ddl_test4; +DROP ROLE regress_role_ddl_test5; +DROP ROLE "regress_role-with-dash"; +SET ROLE regress_role_ddl_grantor; +REVOKE regress_role_ddl_group1 FROM regress_role_ddl_member; +REVOKE regress_role_ddl_group2 FROM regress_role_ddl_member; +RESET ROLE; +DROP ROLE regress_role_ddl_member; +DROP ROLE regress_role_ddl_group1; +DROP ROLE regress_role_ddl_group2; +DROP ROLE regress_role_ddl_grantor; +DROP DATABASE regression_role_ddl_test; +-- Reset timezone to default +RESET timezone; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out index 1c4e37d22493d..3a5e82c35bd36 100644 --- a/src/test/regress/expected/rowsecurity.out +++ b/src/test/regress/expected/rowsecurity.out @@ -31,6 +31,310 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool COST 0.0000001 LANGUAGE plpgsql AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; GRANT EXECUTE ON FUNCTION f_leak(text) TO public; +-- +-- Test policies applied by command type +-- +SET SESSION AUTHORIZATION regress_rls_alice; +-- setup source table (for MERGE operations) +CREATE TABLE rls_test_src (a int PRIMARY KEY, b text); +ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY; +GRANT SELECT, UPDATE ON rls_test_src TO public; +INSERT INTO rls_test_src VALUES (1, 'src a'); +-- setup target table with a column set by a BEFORE ROW trigger +-- (policies should always see values set by the trigger) +CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text); +ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY; +GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public; +CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS + $$ BEGIN new.c = upper(new.b); RETURN new; END; $$ + LANGUAGE plpgsql; +CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt + FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c(); +-- setup a complete set of policies that emit NOTICE messages when applied +CREATE FUNCTION rls_test_policy_fn(text, record) RETURNS bool AS + $$ BEGIN RAISE NOTICE '%.%', $1, $2; RETURN true; END; $$ + LANGUAGE plpgsql; +CREATE POLICY sel_pol ON rls_test_src FOR SELECT + USING (rls_test_policy_fn('SELECT USING on rls_test_src', rls_test_src)); +CREATE POLICY upd_pol ON rls_test_src FOR UPDATE + USING (rls_test_policy_fn('UPDATE USING on rls_test_src', rls_test_src)) + WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_src', rls_test_src)); +CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT + USING (rls_test_policy_fn('SELECT USING on rls_test_tgt', rls_test_tgt)); +CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT + WITH CHECK (rls_test_policy_fn('INSERT CHECK on rls_test_tgt', rls_test_tgt)); +CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE + USING (rls_test_policy_fn('UPDATE USING on rls_test_tgt', rls_test_tgt)) + WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_tgt', rls_test_tgt)); +CREATE POLICY del_pol ON rls_test_tgt FOR DELETE + USING (rls_test_policy_fn('DELETE USING on rls_test_tgt', rls_test_tgt)); +-- test policies applied to regress_rls_bob +SET SESSION AUTHORIZATION regress_rls_bob; +-- SELECT, COPY ... TO, and TABLE should only apply SELECT USING policy clause +SELECT * FROM rls_test_src; +NOTICE: SELECT USING on rls_test_src.(1,"src a") + a | b +---+------- + 1 | src a +(1 row) + +COPY rls_test_src TO stdout; +NOTICE: SELECT USING on rls_test_src.(1,"src a") +1 src a +TABLE rls_test_src; +NOTICE: SELECT USING on rls_test_src.(1,"src a") + a | b +---+------- + 1 | src a +(1 row) + +-- SELECT ... FOR UPDATE/SHARE should also apply UPDATE USING policy clause +SELECT * FROM rls_test_src FOR UPDATE; +NOTICE: UPDATE USING on rls_test_src.(1,"src a") +NOTICE: SELECT USING on rls_test_src.(1,"src a") + a | b +---+------- + 1 | src a +(1 row) + +SELECT * FROM rls_test_src FOR NO KEY UPDATE; +NOTICE: UPDATE USING on rls_test_src.(1,"src a") +NOTICE: SELECT USING on rls_test_src.(1,"src a") + a | b +---+------- + 1 | src a +(1 row) + +SELECT * FROM rls_test_src FOR SHARE; +NOTICE: UPDATE USING on rls_test_src.(1,"src a") +NOTICE: SELECT USING on rls_test_src.(1,"src a") + a | b +---+------- + 1 | src a +(1 row) + +SELECT * FROM rls_test_src FOR KEY SHARE; +NOTICE: UPDATE USING on rls_test_src.(1,"src a") +NOTICE: SELECT USING on rls_test_src.(1,"src a") + a | b +---+------- + 1 | src a +(1 row) + +-- plain INSERT should apply INSERT CHECK policy clause +INSERT INTO rls_test_tgt VALUES (1, 'tgt a'); +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A") +-- INSERT ... RETURNING should also apply SELECT USING policy clause +TRUNCATE rls_test_tgt; +INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A") + a | b | c +---+-------+------- + 1 | tgt a | TGT A +(1 row) + +-- UPDATE without WHERE or RETURNING should only apply UPDATE policy clauses +UPDATE rls_test_tgt SET b = 'tgt b'; +NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B") +-- UPDATE with WHERE or RETURNING should also apply SELECT USING policy clause +-- (to both old and new values) +UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1; +NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B") +NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C") +UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *; +NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C") +NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt d","TGT D") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D") + a | b | c +---+-------+------- + 1 | tgt d | TGT D +(1 row) + +-- DELETE without WHERE or RETURNING should only apply DELETE USING policy clause +BEGIN; DELETE FROM rls_test_tgt; ROLLBACK; +NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D") +-- DELETE with WHERE or RETURNING should also apply SELECT USING policy clause +BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK; +NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D") +DELETE FROM rls_test_tgt RETURNING *; +NOTICE: DELETE USING on rls_test_tgt.(1,"tgt d","TGT D") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt d","TGT D") + a | b | c +---+-------+------- + 1 | tgt d | TGT D +(1 row) + +-- INSERT ... ON CONFLICT DO NOTHING with an arbiter clause should apply +-- INSERT CHECK and SELECT USING policy clauses (to new value, whether it +-- conflicts or not) +INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING; +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A") +INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING; +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B") +-- INSERT ... ON CONFLICT DO NOTHING without an arbiter clause only applies +-- INSERT CHECK policy clause +INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT DO NOTHING; +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt b","TGT B") +-- INSERT ... ON CONFLICT DO UPDATE should apply INSERT CHECK and SELECT USING +-- policy clauses to values proposed for insert. In the event of a conflict it +-- should also apply UPDATE and SELECT policies to old and new values, like +-- UPDATE ... WHERE. +BEGIN; +INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b'; +NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A") +INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d'; +NOTICE: INSERT CHECK on rls_test_tgt.(2,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_tgt.(2,"tgt c","TGT C") +NOTICE: UPDATE USING on rls_test_tgt.(2,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(2,"tgt a","TGT A") +NOTICE: UPDATE CHECK on rls_test_tgt.(2,"tgt d","TGT D") +NOTICE: SELECT USING on rls_test_tgt.(2,"tgt d","TGT D") +INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A") + a | b | c +---+-------+------- + 3 | tgt a | TGT A +(1 row) + +INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(3,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_tgt.(3,"tgt c","TGT C") +NOTICE: UPDATE USING on rls_test_tgt.(3,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(3,"tgt a","TGT A") +NOTICE: UPDATE CHECK on rls_test_tgt.(3,"tgt d","TGT D") +NOTICE: SELECT USING on rls_test_tgt.(3,"tgt d","TGT D") + a | b | c +---+-------+------- + 3 | tgt d | TGT D +(1 row) + +ROLLBACK; +-- INSERT ... ON CONFLICT DO SELECT should apply INSERT CHECK and SELECT USING +-- policy clauses to values proposed for insert. In the event of a conflict it +-- should also apply SELECT USING policy clauses to the existing values. +BEGIN; +INSERT INTO rls_test_tgt VALUES (4, 'tgt a') ON CONFLICT (a) DO SELECT RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(4,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(4,"tgt a","TGT A") + a | b | c +---+-------+------- + 4 | tgt a | TGT A +(1 row) + +INSERT INTO rls_test_tgt VALUES (4, 'tgt b') ON CONFLICT (a) DO SELECT RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(4,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_tgt.(4,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_tgt.(4,"tgt a","TGT A") + a | b | c +---+-------+------- + 4 | tgt a | TGT A +(1 row) + +ROLLBACK; +-- INSERT ... ON CONFLICT DO SELECT FOR UPDATE should also apply UPDATE USING +-- policy clauses to the existing values, in the event of a conflict. +BEGIN; +INSERT INTO rls_test_tgt VALUES (5, 'tgt a') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(5,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(5,"tgt a","TGT A") + a | b | c +---+-------+------- + 5 | tgt a | TGT A +(1 row) + +INSERT INTO rls_test_tgt VALUES (5, 'tgt b') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *; +NOTICE: INSERT CHECK on rls_test_tgt.(5,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_tgt.(5,"tgt b","TGT B") +NOTICE: UPDATE USING on rls_test_tgt.(5,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(5,"tgt a","TGT A") + a | b | c +---+-------+------- + 5 | tgt a | TGT A +(1 row) + +ROLLBACK; +-- MERGE should always apply SELECT USING policy clauses to both source and +-- target rows +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN NOT MATCHED THEN DO NOTHING; +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_src.(1,"src a") +-- MERGE ... INSERT should behave like INSERT on target table +-- (SELECT policy applied to target, if RETURNING is specified) +TRUNCATE rls_test_tgt; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a'); +NOTICE: SELECT USING on rls_test_src.(1,"src a") +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A") +TRUNCATE rls_test_tgt; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a') + RETURNING *; +NOTICE: SELECT USING on rls_test_src.(1,"src a") +NOTICE: INSERT CHECK on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A") + a | b | a | b | c +---+-------+---+-------+------- + 1 | src a | 1 | tgt a | TGT A +(1 row) + +-- MERGE ... UPDATE should behave like UPDATE ... WHERE on target table +-- (join clause is like WHERE, so SELECT policies are always applied) +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN UPDATE SET b = 'tgt b'; +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: SELECT USING on rls_test_src.(1,"src a") +NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt a","TGT A") +NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B") +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN UPDATE SET b = 'tgt c' + RETURNING *; +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt b","TGT B") +NOTICE: SELECT USING on rls_test_src.(1,"src a") +NOTICE: UPDATE USING on rls_test_tgt.(1,"tgt b","TGT B") +NOTICE: UPDATE CHECK on rls_test_tgt.(1,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C") + a | b | a | b | c +---+-------+---+-------+------- + 1 | src a | 1 | tgt c | TGT C +(1 row) + +-- MERGE ... DELETE should behave like DELETE ... WHERE on target table +-- (join clause is like WHERE, so SELECT policies are always applied) +BEGIN; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN DELETE; +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_src.(1,"src a") +NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C") +ROLLBACK; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN DELETE + RETURNING *; +NOTICE: SELECT USING on rls_test_tgt.(1,"tgt c","TGT C") +NOTICE: SELECT USING on rls_test_src.(1,"src a") +NOTICE: DELETE USING on rls_test_tgt.(1,"tgt c","TGT C") + a | b | a | b | c +---+-------+---+-------+------- + 1 | src a | 1 | tgt c | TGT C +(1 row) + +-- Tidy up +RESET SESSION AUTHORIZATION; +DROP TABLE rls_test_src, rls_test_tgt; +DROP FUNCTION rls_test_tgt_set_c; +DROP FUNCTION rls_test_policy_fn; -- BASIC Row-Level Security Scenario SET SESSION AUTHORIZATION regress_rls_alice; CREATE TABLE uaccount ( @@ -265,27 +569,27 @@ NOTICE: f_leak => awesome science fiction (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); - QUERY PLAN --------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------- Seq Scan on document - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) - InitPlan 1 + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); - QUERY PLAN --------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------- Hash Join Hash Cond: (category.cid = document.cid) - InitPlan 1 + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) -> Seq Scan on category -> Hash -> Seq Scan on document - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) (9 rows) -- viewpoint from regress_rls_dave @@ -329,27 +633,27 @@ NOTICE: f_leak => awesome technology book (7 rows) EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); - QUERY PLAN -------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------ Seq Scan on document - Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) - InitPlan 1 + Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) (5 rows) EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------ Hash Join Hash Cond: (category.cid = document.cid) - InitPlan 1 + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) -> Seq Scan on category -> Hash -> Seq Scan on document - Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) (9 rows) -- 44 would technically fail for both p2r and p1r, but we should get an error @@ -957,9 +1261,10 @@ Policies: USING ((cid < 55)) Not-null constraints: "part_document_dlevel_not_null" NOT NULL "dlevel" -Partitions: part_document_fiction FOR VALUES FROM (11) TO (12), - part_document_nonfiction FOR VALUES FROM (99) TO (100), - part_document_satire FOR VALUES FROM (55) TO (56) +Partitions: + part_document_fiction FOR VALUES FROM (11) TO (12) + part_document_nonfiction FOR VALUES FROM (99) TO (100) + part_document_satire FOR VALUES FROM (55) TO (56) SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename like '%part_document%' ORDER BY policyname; schemaname | tablename | policyname | permissive | roles | cmd | qual | with_check @@ -986,19 +1291,24 @@ NOTICE: f_leak => my first satire 9 | 11 | 1 | regress_rls_dave | awesome science fiction (4 rows) +COPY part_document TO stdout WITH (DELIMITER ','); +1,11,1,regress_rls_bob,my first novel +6,11,1,regress_rls_carol,great science fiction +9,11,1,regress_rls_dave,awesome science fiction +4,55,1,regress_rls_bob,my first satire EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------- Append - InitPlan 1 + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) -> Seq Scan on part_document_fiction part_document_1 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) -> Seq Scan on part_document_satire part_document_2 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) -> Seq Scan on part_document_nonfiction part_document_3 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) (10 rows) -- viewpoint from regress_rls_carol @@ -1028,19 +1338,30 @@ NOTICE: f_leak => awesome technology book 10 | 99 | 2 | regress_rls_dave | awesome technology book (10 rows) +COPY part_document TO stdout WITH (DELIMITER ','); +1,11,1,regress_rls_bob,my first novel +2,11,2,regress_rls_bob,my second novel +6,11,1,regress_rls_carol,great science fiction +9,11,1,regress_rls_dave,awesome science fiction +4,55,1,regress_rls_bob,my first satire +8,55,2,regress_rls_carol,great satire +3,99,2,regress_rls_bob,my science textbook +5,99,2,regress_rls_bob,my history book +7,99,2,regress_rls_carol,great technology book +10,99,2,regress_rls_dave,awesome technology book EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------- Append - InitPlan 1 + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) -> Seq Scan on part_document_fiction part_document_1 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) -> Seq Scan on part_document_satire part_document_2 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) -> Seq Scan on part_document_nonfiction part_document_3 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) (10 rows) -- viewpoint from regress_rls_dave @@ -1058,12 +1379,17 @@ NOTICE: f_leak => awesome science fiction 9 | 11 | 1 | regress_rls_dave | awesome science fiction (4 rows) +COPY part_document TO stdout WITH (DELIMITER ','); +1,11,1,regress_rls_bob,my first novel +2,11,2,regress_rls_bob,my second novel +6,11,1,regress_rls_carol,great science fiction +9,11,1,regress_rls_dave,awesome science fiction EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); - QUERY PLAN ------------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------------- Seq Scan on part_document_fiction part_document - Filter: ((cid < 55) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) - InitPlan 1 + Filter: ((cid < 55) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) (5 rows) @@ -1137,11 +1463,11 @@ NOTICE: f_leak => awesome science fiction (4 rows) EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); - QUERY PLAN ------------------------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------------------------- Seq Scan on part_document_fiction part_document - Filter: ((cid < 55) AND (dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) - InitPlan 1 + Filter: ((cid < 55) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) (5 rows) @@ -1176,18 +1502,18 @@ NOTICE: f_leak => awesome technology book (11 rows) EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------- Append - InitPlan 1 + InitPlan expr_1 -> Index Scan using uaccount_pkey on uaccount Index Cond: (pguser = CURRENT_USER) -> Seq Scan on part_document_fiction part_document_1 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) -> Seq Scan on part_document_satire part_document_2 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) -> Seq Scan on part_document_nonfiction part_document_3 - Filter: ((dlevel <= (InitPlan 1).col1) AND f_leak(dtitle)) + Filter: ((dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle)) (10 rows) -- only owner can change policies @@ -1437,11 +1763,11 @@ NOTICE: f_leak => 03b26944890929ff751653acb2f2af79 (1 row) EXPLAIN (COSTS OFF) SELECT * FROM only s1 WHERE f_leak(b); - QUERY PLAN ---------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------- Seq Scan on s1 - Filter: ((ANY (a = (hashed SubPlan 1).col1)) AND f_leak(b)) - SubPlan 1 + Filter: ((ANY (a = (hashed SubPlan any_1).col1)) AND f_leak(b)) + SubPlan any_1 -> Seq Scan on s2 Filter: (((x % 2) = 0) AND (y ~~ '%2f%'::text)) (5 rows) @@ -1457,11 +1783,11 @@ NOTICE: f_leak => 03b26944890929ff751653acb2f2af79 (1 row) EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b); - QUERY PLAN ---------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------- Seq Scan on s1 - Filter: ((ANY (a = (hashed SubPlan 1).col1)) AND f_leak(b)) - SubPlan 1 + Filter: ((ANY (a = (hashed SubPlan any_1).col1)) AND f_leak(b)) + SubPlan any_1 -> Seq Scan on s2 Filter: (((x % 2) = 0) AND (y ~~ '%af%'::text)) (5 rows) @@ -1477,11 +1803,11 @@ EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like ------------------------------------------------------------------------- Seq Scan on s2 Filter: (((x % 2) = 0) AND (y ~~ '%28%'::text)) - SubPlan 2 + SubPlan expr_1 -> Limit -> Seq Scan on s1 - Filter: (ANY (a = (hashed SubPlan 1).col1)) - SubPlan 1 + Filter: (ANY (a = (hashed SubPlan any_1).col1)) + SubPlan any_1 -> Seq Scan on s2 s2_1 Filter: (((x % 2) = 0) AND (y ~~ '%af%'::text)) (9 rows) @@ -2114,10 +2440,58 @@ INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel') ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol'; ERROR: new row violates row-level security policy for table "document" -- +-- INSERT ... ON CONFLICT DO SELECT and Row-level security +-- +SET SESSION AUTHORIZATION regress_rls_alice; +DROP POLICY p3_with_all ON document; +CREATE POLICY p1_select_novels ON document FOR SELECT + USING (cid = (SELECT cid from category WHERE cname = 'novel')); +CREATE POLICY p2_insert_own ON document FOR INSERT + WITH CHECK (dauthor = current_user); +CREATE POLICY p3_update_novels ON document FOR UPDATE + USING (cid = (SELECT cid from category WHERE cname = 'novel') AND dlevel = 1) + WITH CHECK (dauthor = current_user); +SET SESSION AUTHORIZATION regress_rls_bob; +-- DO SELECT requires SELECT rights, should succeed for novel +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle; + did | dauthor | dtitle +-----+-----------------+---------------- + 1 | regress_rls_bob | my first novel +(1 row) + +-- DO SELECT requires SELECT rights, should fail for non-novel +INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi') + ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle; +ERROR: new row violates row-level security policy for table "document" +-- DO SELECT with WHERE and EXCLUDED reference +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle; + did | dauthor | dtitle +-----+-----------------+---------------- + 1 | regress_rls_bob | my first novel +(1 row) + +-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel and dlevel = 1 +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle; + did | dauthor | dtitle +-----+-----------------+---------------- + 1 | regress_rls_bob | my first novel +(1 row) + +-- should fail UPDATE USING policy for novel with dlevel = 2 +INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle; +ERROR: new row violates row-level security policy (USING expression) for table "document" +SET SESSION AUTHORIZATION regress_rls_alice; +DROP POLICY p1_select_novels ON document; +DROP POLICY p2_insert_own ON document; +DROP POLICY p3_update_novels ON document; +-- -- MERGE -- RESET SESSION AUTHORIZATION; -DROP POLICY p3_with_all ON document; ALTER TABLE document ADD COLUMN dnotes text DEFAULT ''; -- all documents are readable CREATE POLICY p1 ON document FOR SELECT USING (true); @@ -2717,11 +3091,11 @@ NOTICE: f_leak => bbb (1 row) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------- Seq Scan on z1 - Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 0) AND f_leak(b)) - SubPlan 1 + Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 0) AND f_leak(b)) + SubPlan any_1 -> Seq Scan on z1_blacklist (4 rows) @@ -2735,11 +3109,11 @@ NOTICE: f_leak => bbb (1 row) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------- Seq Scan on z1 - Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 0) AND f_leak(b)) - SubPlan 1 + Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 0) AND f_leak(b)) + SubPlan any_1 -> Seq Scan on z1_blacklist (4 rows) @@ -2907,11 +3281,11 @@ NOTICE: f_leak => bbb (1 row) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------- Seq Scan on z1 - Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 0) AND f_leak(b)) - SubPlan 1 + Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 0) AND f_leak(b)) + SubPlan any_1 -> Seq Scan on z1_blacklist (4 rows) @@ -2933,11 +3307,11 @@ NOTICE: f_leak => aba (1 row) EXPLAIN (COSTS OFF) SELECT * FROM rls_view; - QUERY PLAN ---------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------- Seq Scan on z1 - Filter: ((NOT (ANY (a = (hashed SubPlan 1).col1))) AND ((a % 2) = 1) AND f_leak(b)) - SubPlan 1 + Filter: ((NOT (ANY (a = (hashed SubPlan any_1).col1))) AND ((a % 2) = 1) AND f_leak(b)) + SubPlan any_1 -> Seq Scan on z1_blacklist (4 rows) @@ -3606,8 +3980,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN -------------------------- Result + Replaces: Scan on t1 One-Time Filter: false -(2 rows) +(3 rows) SET SESSION AUTHORIZATION regress_rls_bob; SELECT * FROM t1; @@ -3619,8 +3994,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM t1; QUERY PLAN -------------------------- Result + Replaces: Scan on t1 One-Time Filter: false -(2 rows) +(3 rows) -- -- COPY TO/FROM @@ -4506,7 +4882,7 @@ RESET SESSION AUTHORIZATION; DROP VIEW rls_view; DROP TABLE rls_tbl; DROP TABLE ref_tbl; --- Leaky operator test +-- Leaky operator tests CREATE TABLE rls_tbl (a int); INSERT INTO rls_tbl SELECT x/10 FROM generate_series(1, 100) x; ANALYZE rls_tbl; @@ -4524,15 +4900,87 @@ SELECT * FROM rls_tbl WHERE a <<< 1000; (0 rows) EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900; - QUERY PLAN --------------------------- + QUERY PLAN +----------------------------- Result + Replaces: Scan on rls_tbl One-Time Filter: false -(2 rows) +(3 rows) + +RESET SESSION AUTHORIZATION; +CREATE TABLE rls_child_tbl () INHERITS (rls_tbl); +INSERT INTO rls_child_tbl SELECT x/10 FROM generate_series(1, 100) x; +ANALYZE rls_child_tbl; +CREATE TABLE rls_ptbl (a int) PARTITION BY RANGE (a); +CREATE TABLE rls_part PARTITION OF rls_ptbl FOR VALUES FROM (-100) TO (100); +INSERT INTO rls_ptbl SELECT x/10 FROM generate_series(1, 100) x; +ANALYZE rls_ptbl, rls_part; +ALTER TABLE rls_ptbl ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_part ENABLE ROW LEVEL SECURITY; +GRANT SELECT ON rls_ptbl TO regress_rls_alice; +GRANT SELECT ON rls_part TO regress_rls_alice; +CREATE POLICY p1 ON rls_tbl USING (a < 0); +CREATE POLICY p2 ON rls_ptbl USING (a < 0); +CREATE POLICY p3 ON rls_part USING (a < 0); +SET SESSION AUTHORIZATION regress_rls_alice; +SELECT * FROM rls_tbl WHERE a <<< 1000; + a +--- +(0 rows) + +SELECT * FROM rls_child_tbl WHERE a <<< 1000; +ERROR: permission denied for table rls_child_tbl +SELECT * FROM rls_ptbl WHERE a <<< 1000; + a +--- +(0 rows) + +SELECT * FROM rls_part WHERE a <<< 1000; + a +--- +(0 rows) + +SELECT * FROM (SELECT * FROM rls_tbl UNION ALL + SELECT * FROM rls_tbl) t WHERE a <<< 1000; + a +--- +(0 rows) + +SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL + SELECT * FROM rls_child_tbl) t WHERE a <<< 1000; +ERROR: permission denied for table rls_child_tbl +RESET SESSION AUTHORIZATION; +REVOKE SELECT ON rls_tbl FROM regress_rls_alice; +CREATE VIEW rls_tbl_view AS SELECT * FROM rls_tbl; +ALTER TABLE rls_child_tbl ENABLE ROW LEVEL SECURITY; +GRANT SELECT ON rls_child_tbl TO regress_rls_alice; +CREATE POLICY p4 ON rls_child_tbl USING (a < 0); +SET SESSION AUTHORIZATION regress_rls_alice; +SELECT * FROM rls_tbl WHERE a <<< 1000; +ERROR: permission denied for table rls_tbl +SELECT * FROM rls_tbl_view WHERE a <<< 1000; +ERROR: permission denied for view rls_tbl_view +SELECT * FROM rls_child_tbl WHERE a <<< 1000; + a +--- +(0 rows) + +SELECT * FROM (SELECT * FROM rls_tbl UNION ALL + SELECT * FROM rls_tbl) t WHERE a <<< 1000; +ERROR: permission denied for table rls_tbl +SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL + SELECT * FROM rls_child_tbl) t WHERE a <<< 1000; + a +--- +(0 rows) DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); RESET SESSION AUTHORIZATION; +DROP TABLE rls_part; +DROP TABLE rls_ptbl; +DROP TABLE rls_child_tbl; +DROP VIEW rls_tbl_view; DROP TABLE rls_tbl; -- Bug #16006: whole-row Vars in a policy don't play nice with sub-selects SET SESSION AUTHORIZATION regress_rls_alice; diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index 9168979a6206e..956bc2d02fc82 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -907,6 +907,8 @@ create temp table compos (f1 int, f2 text); create function fcompos1(v compos) returns void as $$ insert into compos values (v); -- fail $$ language sql; +NOTICE: function "fcompos1" will be effectively temporary +DETAIL: It depends on temporary type compos. ERROR: column "f1" is of type integer but expression is of type compos LINE 2: insert into compos values (v); -- fail ^ @@ -914,12 +916,18 @@ HINT: You will need to rewrite or cast the expression. create function fcompos1(v compos) returns void as $$ insert into compos values (v.*); $$ language sql; +NOTICE: function "fcompos1" will be effectively temporary +DETAIL: It depends on temporary type compos. create function fcompos2(v compos) returns void as $$ select fcompos1(v); $$ language sql; +NOTICE: function "fcompos2" will be effectively temporary +DETAIL: It depends on temporary type compos. create function fcompos3(v compos) returns void as $$ select fcompos1(fcompos3.v.*); $$ language sql; +NOTICE: function "fcompos3" will be effectively temporary +DETAIL: It depends on temporary type compos. select fcompos1(row(1,'one')); fcompos1 ---------- @@ -965,7 +973,8 @@ select text(fullname) from fullname; -- error ERROR: function text(fullname) does not exist LINE 1: select text(fullname) from fullname; ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select fullname.text from fullname; -- error ERROR: column fullname.text does not exist LINE 1: select fullname.text from fullname; @@ -987,7 +996,8 @@ select text(row('Jim', 'Beam')); -- error ERROR: function text(record) does not exist LINE 1: select text(row('Jim', 'Beam')); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select (row('Jim', 'Beam')).text; -- error ERROR: could not identify column "text" in record data type LINE 1: select (row('Jim', 'Beam')).text; @@ -1010,6 +1020,8 @@ select last(f) from fullname f; create function longname(fullname) returns text language sql as $$select $1.first || ' ' || $1.last$$; +NOTICE: function "longname" will be effectively temporary +DETAIL: It depends on temporary type fullname. select f.longname from fullname f; longname ---------- @@ -1249,19 +1261,19 @@ with cte(c) as materialized (select row(1, 2)), select * from cte2 as t where (select * from (select c as c1) s where (select (c1).f1 > 0)) is not null; - QUERY PLAN ----------------------------------------------- + QUERY PLAN +--------------------------------------------------- CTE Scan on cte Output: cte.c - Filter: ((SubPlan 3) IS NOT NULL) + Filter: ((SubPlan expr_1) IS NOT NULL) CTE cte -> Result Output: '(1,2)'::record - SubPlan 3 + SubPlan expr_1 -> Result Output: cte.c - One-Time Filter: (InitPlan 2).col1 - InitPlan 2 + One-Time Filter: (InitPlan expr_2).col1 + InitPlan expr_2 -> Result Output: ((cte.c).f1 > 0) (13 rows) @@ -1323,8 +1335,9 @@ where false; -------------------------- Result Output: (a).f1, (a).f2 + Replaces: Scan on ss One-Time Filter: false -(3 rows) +(4 rows) explain (verbose, costs off) with cte(c) as materialized (select row(1, 2)), @@ -1348,11 +1361,12 @@ where false; ----------------------------------- Result Output: (cte.c).f1 + Replaces: Scan on cte One-Time Filter: false CTE cte -> Result Output: '(1,2)'::record -(6 rows) +(7 rows) -- -- Tests for component access / FieldSelect diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 6cf828ca8d0dc..a65a5bf0c4fbc 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1310,14 +1310,16 @@ pg_available_extension_versions| SELECT e.name, e.relocatable, e.schema, e.requires, + e.location, e.comment - FROM (pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, comment) + FROM (pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, location, comment) LEFT JOIN pg_extension x ON (((e.name = x.extname) AND (e.version = x.extversion)))); pg_available_extensions| SELECT e.name, e.default_version, x.extversion AS installed_version, + e.location, e.comment - FROM (pg_available_extensions() e(name, default_version, comment) + FROM (pg_available_extensions() e(name, default_version, location, comment) LEFT JOIN pg_extension x ON ((e.name = x.extname))); pg_backend_memory_contexts| SELECT name, ident, @@ -1340,6 +1342,10 @@ pg_cursors| SELECT name, is_scrollable, creation_time FROM pg_cursor() c(name, statement, is_holdable, is_binary, is_scrollable, creation_time); +pg_dsm_registry_allocations| SELECT name, + type, + size + FROM pg_get_dsm_registry_allocations() pg_get_dsm_registry_allocations(name, type, size); pg_file_settings| SELECT sourcefile, sourceline, seqno, @@ -1458,6 +1464,14 @@ pg_prepared_xacts| SELECT p.transaction, FROM ((pg_prepared_xact() p(transaction, gid, prepared, ownerid, dbid) LEFT JOIN pg_authid u ON ((p.ownerid = u.oid))) LEFT JOIN pg_database d ON ((p.dbid = d.oid))); +pg_publication_sequences| SELECT p.pubname, + n.nspname AS schemaname, + c.relname AS sequencename + FROM pg_publication p, + LATERAL pg_get_publication_sequences((p.pubname)::text) gps(relid), + (pg_class c + JOIN pg_namespace n ON ((n.oid = c.relnamespace))) + WHERE (c.oid = gps.relid); pg_publication_tables| SELECT p.pubname, n.nspname AS schemaname, c.relname AS tablename, @@ -1495,8 +1509,9 @@ pg_replication_slots| SELECT l.slot_name, l.conflicting, l.invalidation_reason, l.failover, - l.synced - FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase, two_phase_at, inactive_since, conflicting, invalidation_reason, failover, synced) + l.synced, + l.slotsync_skip_reason + FROM (pg_get_replication_slots() l(slot_name, plugin, slot_type, datoid, temporary, active, active_pid, xmin, catalog_xmin, restart_lsn, confirmed_flush_lsn, wal_status, safe_wal_size, two_phase, two_phase_at, inactive_since, conflicting, invalidation_reason, failover, synced, slotsync_skip_reason) LEFT JOIN pg_database d ON ((l.datoid = d.oid))); pg_roles| SELECT pg_authid.rolname, pg_authid.rolsuper, @@ -1794,7 +1809,8 @@ pg_stat_all_indexes| SELECT c.oid AS relid, pg_stat_get_numscans(i.oid) AS idx_scan, pg_stat_get_lastscan(i.oid) AS last_idx_scan, pg_stat_get_tuples_returned(i.oid) AS idx_tup_read, - pg_stat_get_tuples_fetched(i.oid) AS idx_tup_fetch + pg_stat_get_tuples_fetched(i.oid) AS idx_tup_fetch, + pg_stat_get_stat_reset_time(i.oid) AS stats_reset FROM (((pg_class c JOIN pg_index x ON ((c.oid = x.indrelid))) JOIN pg_class i ON ((i.oid = x.indexrelid))) @@ -1829,7 +1845,8 @@ pg_stat_all_tables| SELECT c.oid AS relid, pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time, pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time, pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time, - pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time + pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time, + pg_stat_get_stat_reset_time(c.oid) AS stats_reset FROM ((pg_class c LEFT JOIN pg_index i ON ((c.oid = i.indrelid))) LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) @@ -1843,6 +1860,21 @@ pg_stat_archiver| SELECT archived_count, last_failed_time, stats_reset FROM pg_stat_get_archiver() s(archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time, stats_reset); +pg_stat_autovacuum_scores| SELECT s.oid AS relid, + n.nspname AS schemaname, + c.relname, + s.score, + s.xid_score, + s.mxid_score, + s.vacuum_score, + s.vacuum_insert_score, + s.analyze_score, + s.do_vacuum, + s.do_analyze, + s.for_wraparound + FROM ((pg_stat_get_autovacuum_scores() s(oid, score, xid_score, mxid_score, vacuum_score, vacuum_insert_score, analyze_score, do_vacuum, do_analyze, for_wraparound) + JOIN pg_class c ON ((c.oid = s.oid))) + LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))); pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean, pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean, pg_stat_get_buf_alloc() AS buffers_alloc, @@ -1904,7 +1936,8 @@ pg_stat_database_conflicts| SELECT oid AS datid, pg_stat_get_db_conflict_snapshot(oid) AS confl_snapshot, pg_stat_get_db_conflict_bufferpin(oid) AS confl_bufferpin, pg_stat_get_db_conflict_startup_deadlock(oid) AS confl_deadlock, - pg_stat_get_db_conflict_logicalslot(oid) AS confl_active_logicalslot + pg_stat_get_db_conflict_logicalslot(oid) AS confl_active_logicalslot, + pg_stat_get_db_stat_reset_time(oid) AS stats_reset FROM pg_database d; pg_stat_gssapi| SELECT pid, gss_auth AS gss_authenticated, @@ -1934,6 +1967,12 @@ pg_stat_io| SELECT backend_type, fsync_time, stats_reset FROM pg_stat_get_io() b(backend_type, object, context, reads, read_bytes, read_time, writes, write_bytes, write_time, writebacks, writeback_time, extends, extend_bytes, extend_time, hits, evictions, reuses, fsyncs, fsync_time, stats_reset); +pg_stat_lock| SELECT locktype, + waits, + wait_time, + fastpath_exceeded, + stats_reset + FROM pg_stat_get_lock() l(locktype, waits, wait_time, fastpath_exceeded, stats_reset); pg_stat_progress_analyze| SELECT s.pid, s.datid, d.datname, @@ -1954,7 +1993,12 @@ pg_stat_progress_analyze| SELECT s.pid, s.param6 AS child_tables_total, s.param7 AS child_tables_done, (s.param8)::oid AS current_child_table_relid, - ((s.param9)::double precision / (1000000)::double precision) AS delay_time + ((s.param9)::double precision / (1000000)::double precision) AS delay_time, + CASE s.param10 + WHEN 1 THEN 'manual'::text + WHEN 2 THEN 'autovacuum'::text + ELSE NULL::text + END AS started_by FROM (pg_stat_get_progress_info('ANALYZE'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); pg_stat_progress_basebackup| SELECT pid, @@ -1973,36 +2017,30 @@ pg_stat_progress_basebackup| SELECT pid, END AS backup_total, param3 AS backup_streamed, param4 AS tablespaces_total, - param5 AS tablespaces_streamed - FROM pg_stat_get_progress_info('BASEBACKUP'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20); -pg_stat_progress_cluster| SELECT s.pid, - s.datid, - d.datname, - s.relid, - CASE s.param1 - WHEN 1 THEN 'CLUSTER'::text - WHEN 2 THEN 'VACUUM FULL'::text + param5 AS tablespaces_streamed, + CASE param6 + WHEN 1 THEN 'full'::text + WHEN 2 THEN 'incremental'::text ELSE NULL::text + END AS backup_type + FROM pg_stat_get_progress_info('BASEBACKUP'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20); +pg_stat_progress_cluster| SELECT pid, + datid, + datname, + relid, + CASE + WHEN (command = ANY (ARRAY['CLUSTER'::text, 'VACUUM FULL'::text])) THEN command + WHEN (repack_index_relid = (0)::oid) THEN 'VACUUM FULL'::text + ELSE 'CLUSTER'::text END AS command, - CASE s.param2 - WHEN 0 THEN 'initializing'::text - WHEN 1 THEN 'seq scanning heap'::text - WHEN 2 THEN 'index scanning heap'::text - WHEN 3 THEN 'sorting tuples'::text - WHEN 4 THEN 'writing new heap'::text - WHEN 5 THEN 'swapping relation files'::text - WHEN 6 THEN 'rebuilding index'::text - WHEN 7 THEN 'performing final cleanup'::text - ELSE NULL::text - END AS phase, - (s.param3)::oid AS cluster_index_relid, - s.param4 AS heap_tuples_scanned, - s.param5 AS heap_tuples_written, - s.param6 AS heap_blks_total, - s.param7 AS heap_blks_scanned, - s.param8 AS index_rebuild_count - FROM (pg_stat_get_progress_info('CLUSTER'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) - LEFT JOIN pg_database d ON ((s.datid = d.oid))); + phase, + repack_index_relid AS cluster_index_relid, + heap_tuples_scanned, + (heap_tuples_inserted + heap_tuples_updated) AS heap_tuples_written, + heap_blks_total, + heap_blks_scanned, + index_rebuild_count + FROM pg_stat_progress_repack; pg_stat_progress_copy| SELECT s.pid, s.datid, d.datname, @@ -2062,6 +2100,73 @@ pg_stat_progress_create_index| SELECT s.pid, s.param15 AS partitions_done FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); +pg_stat_progress_data_checksums| SELECT s.pid, + s.datid, + d.datname, + CASE s.param1 + WHEN 0 THEN 'enabling'::text + WHEN 1 THEN 'disabling'::text + WHEN 2 THEN 'waiting on temporary tables'::text + WHEN 3 THEN 'waiting on barrier'::text + WHEN 4 THEN 'done'::text + ELSE NULL::text + END AS phase, + CASE s.param2 + WHEN '-1'::integer THEN NULL::bigint + ELSE s.param2 + END AS databases_total, + s.param3 AS databases_done, + CASE s.param4 + WHEN '-1'::integer THEN NULL::bigint + ELSE s.param4 + END AS relations_total, + CASE s.param5 + WHEN '-1'::integer THEN NULL::bigint + ELSE s.param5 + END AS relations_done, + CASE s.param6 + WHEN '-1'::integer THEN NULL::bigint + ELSE s.param6 + END AS blocks_total, + CASE s.param7 + WHEN '-1'::integer THEN NULL::bigint + ELSE s.param7 + END AS blocks_done + FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) + LEFT JOIN pg_database d ON ((s.datid = d.oid))) + ORDER BY s.datid; +pg_stat_progress_repack| SELECT s.pid, + s.datid, + d.datname, + s.relid, + CASE s.param1 + WHEN 1 THEN 'CLUSTER'::text + WHEN 2 THEN 'REPACK'::text + WHEN 3 THEN 'VACUUM FULL'::text + ELSE NULL::text + END AS command, + CASE s.param2 + WHEN 0 THEN 'initializing'::text + WHEN 1 THEN 'seq scanning heap'::text + WHEN 2 THEN 'index scanning heap'::text + WHEN 3 THEN 'sorting tuples'::text + WHEN 4 THEN 'writing new heap'::text + WHEN 5 THEN 'catch-up'::text + WHEN 6 THEN 'swapping relation files'::text + WHEN 7 THEN 'rebuilding index'::text + WHEN 8 THEN 'performing final cleanup'::text + ELSE NULL::text + END AS phase, + (s.param3)::oid AS repack_index_relid, + s.param4 AS heap_tuples_scanned, + s.param5 AS heap_tuples_inserted, + s.param6 AS heap_tuples_updated, + s.param7 AS heap_tuples_deleted, + s.param8 AS heap_blks_total, + s.param9 AS heap_blks_scanned, + s.param10 AS index_rebuild_count + FROM (pg_stat_get_progress_info('REPACK'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) + LEFT JOIN pg_database d ON ((s.datid = d.oid))); pg_stat_progress_vacuum| SELECT s.pid, s.datid, d.datname, @@ -2085,9 +2190,32 @@ pg_stat_progress_vacuum| SELECT s.pid, s.param8 AS num_dead_item_ids, s.param9 AS indexes_total, s.param10 AS indexes_processed, - ((s.param11)::double precision / (1000000)::double precision) AS delay_time + ((s.param11)::double precision / (1000000)::double precision) AS delay_time, + CASE s.param12 + WHEN 1 THEN 'normal'::text + WHEN 2 THEN 'aggressive'::text + WHEN 3 THEN 'failsafe'::text + ELSE NULL::text + END AS mode, + CASE s.param13 + WHEN 1 THEN 'manual'::text + WHEN 2 THEN 'autovacuum'::text + WHEN 3 THEN 'autovacuum_wraparound'::text + ELSE NULL::text + END AS started_by FROM (pg_stat_get_progress_info('VACUUM'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); +pg_stat_recovery| SELECT promote_triggered, + last_replayed_read_lsn, + last_replayed_end_lsn, + last_replayed_tli, + replay_end_lsn, + replay_end_tli, + recovery_last_xact_time, + current_chunk_start_time, + pause_state + FROM pg_stat_get_recovery() s(promote_triggered, last_replayed_read_lsn, last_replayed_end_lsn, last_replayed_tli, replay_end_lsn, replay_end_tli, recovery_last_xact_time, current_chunk_start_time, pause_state) + WHERE (promote_triggered IS NOT NULL); pg_stat_recovery_prefetch| SELECT stats_reset, prefetch, hit, @@ -2129,11 +2257,14 @@ pg_stat_replication_slots| SELECT s.slot_name, s.stream_txns, s.stream_count, s.stream_bytes, + s.mem_exceeded_count, s.total_txns, s.total_bytes, + s.slotsync_skip_count, + s.slotsync_last_skip, s.stats_reset FROM pg_replication_slots r, - LATERAL pg_stat_get_replication_slot((r.slot_name)::text) s(slot_name, spill_txns, spill_count, spill_bytes, stream_txns, stream_count, stream_bytes, total_txns, total_bytes, stats_reset) + LATERAL pg_stat_get_replication_slot((r.slot_name)::text) s(slot_name, spill_txns, spill_count, spill_bytes, stream_txns, stream_count, stream_bytes, mem_exceeded_count, total_txns, total_bytes, slotsync_skip_count, slotsync_last_skip, stats_reset) WHERE (r.datoid IS NOT NULL); pg_stat_slru| SELECT name, blks_zeroed, @@ -2171,17 +2302,19 @@ pg_stat_subscription| SELECT su.oid AS subid, pg_stat_subscription_stats| SELECT ss.subid, s.subname, ss.apply_error_count, - ss.sync_error_count, + ss.sync_seq_error_count, + ss.sync_table_error_count, ss.confl_insert_exists, ss.confl_update_origin_differs, ss.confl_update_exists, + ss.confl_update_deleted, ss.confl_update_missing, ss.confl_delete_origin_differs, ss.confl_delete_missing, ss.confl_multiple_unique_conflicts, ss.stats_reset FROM pg_subscription s, - LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts, stats_reset); + LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_seq_error_count, sync_table_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_deleted, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, confl_multiple_unique_conflicts, stats_reset); pg_stat_sys_indexes| SELECT relid, indexrelid, schemaname, @@ -2190,7 +2323,8 @@ pg_stat_sys_indexes| SELECT relid, idx_scan, last_idx_scan, idx_tup_read, - idx_tup_fetch + idx_tup_fetch, + stats_reset FROM pg_stat_all_indexes WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text)); pg_stat_sys_tables| SELECT relid, @@ -2222,7 +2356,8 @@ pg_stat_sys_tables| SELECT relid, total_vacuum_time, total_autovacuum_time, total_analyze_time, - total_autoanalyze_time + total_autoanalyze_time, + stats_reset FROM pg_stat_all_tables WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text)); pg_stat_user_functions| SELECT p.oid AS funcid, @@ -2230,7 +2365,8 @@ pg_stat_user_functions| SELECT p.oid AS funcid, p.proname AS funcname, pg_stat_get_function_calls(p.oid) AS calls, pg_stat_get_function_total_time(p.oid) AS total_time, - pg_stat_get_function_self_time(p.oid) AS self_time + pg_stat_get_function_self_time(p.oid) AS self_time, + pg_stat_get_function_stat_reset_time(p.oid) AS stats_reset FROM (pg_proc p LEFT JOIN pg_namespace n ON ((n.oid = p.pronamespace))) WHERE ((p.prolang <> (12)::oid) AND (pg_stat_get_function_calls(p.oid) IS NOT NULL)); @@ -2242,7 +2378,8 @@ pg_stat_user_indexes| SELECT relid, idx_scan, last_idx_scan, idx_tup_read, - idx_tup_fetch + idx_tup_fetch, + stats_reset FROM pg_stat_all_indexes WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); pg_stat_user_tables| SELECT relid, @@ -2274,15 +2411,17 @@ pg_stat_user_tables| SELECT relid, total_vacuum_time, total_autovacuum_time, total_analyze_time, - total_autoanalyze_time + total_autoanalyze_time, + stats_reset FROM pg_stat_all_tables WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); pg_stat_wal| SELECT wal_records, wal_fpi, wal_bytes, + wal_fpi_bytes, wal_buffers_full, stats_reset - FROM pg_stat_get_wal() w(wal_records, wal_fpi, wal_bytes, wal_buffers_full, stats_reset); + FROM pg_stat_get_wal() w(wal_records, wal_fpi, wal_bytes, wal_fpi_bytes, wal_buffers_full, stats_reset); pg_stat_wal_receiver| SELECT pid, status, receive_start_lsn, @@ -2360,7 +2499,8 @@ pg_statio_all_indexes| SELECT c.oid AS relid, c.relname, i.relname AS indexrelname, (pg_stat_get_blocks_fetched(i.oid) - pg_stat_get_blocks_hit(i.oid)) AS idx_blks_read, - pg_stat_get_blocks_hit(i.oid) AS idx_blks_hit + pg_stat_get_blocks_hit(i.oid) AS idx_blks_hit, + pg_stat_get_stat_reset_time(i.oid) AS stats_reset FROM (((pg_class c JOIN pg_index x ON ((c.oid = x.indrelid))) JOIN pg_class i ON ((i.oid = x.indexrelid))) @@ -2370,7 +2510,8 @@ pg_statio_all_sequences| SELECT c.oid AS relid, n.nspname AS schemaname, c.relname, (pg_stat_get_blocks_fetched(c.oid) - pg_stat_get_blocks_hit(c.oid)) AS blks_read, - pg_stat_get_blocks_hit(c.oid) AS blks_hit + pg_stat_get_blocks_hit(c.oid) AS blks_hit, + pg_stat_get_stat_reset_time(c.oid) AS stats_reset FROM (pg_class c LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) WHERE (c.relkind = 'S'::"char"); @@ -2384,7 +2525,8 @@ pg_statio_all_tables| SELECT c.oid AS relid, (pg_stat_get_blocks_fetched(t.oid) - pg_stat_get_blocks_hit(t.oid)) AS toast_blks_read, pg_stat_get_blocks_hit(t.oid) AS toast_blks_hit, x.idx_blks_read AS tidx_blks_read, - x.idx_blks_hit AS tidx_blks_hit + x.idx_blks_hit AS tidx_blks_hit, + pg_stat_get_stat_reset_time(c.oid) AS stats_reset FROM ((((pg_class c LEFT JOIN pg_class t ON ((c.reltoastrelid = t.oid))) LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) @@ -2403,14 +2545,16 @@ pg_statio_sys_indexes| SELECT relid, relname, indexrelname, idx_blks_read, - idx_blks_hit + idx_blks_hit, + stats_reset FROM pg_statio_all_indexes WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text)); pg_statio_sys_sequences| SELECT relid, schemaname, relname, blks_read, - blks_hit + blks_hit, + stats_reset FROM pg_statio_all_sequences WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text)); pg_statio_sys_tables| SELECT relid, @@ -2423,7 +2567,8 @@ pg_statio_sys_tables| SELECT relid, toast_blks_read, toast_blks_hit, tidx_blks_read, - tidx_blks_hit + tidx_blks_hit, + stats_reset FROM pg_statio_all_tables WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text)); pg_statio_user_indexes| SELECT relid, @@ -2432,14 +2577,16 @@ pg_statio_user_indexes| SELECT relid, relname, indexrelname, idx_blks_read, - idx_blks_hit + idx_blks_hit, + stats_reset FROM pg_statio_all_indexes WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); pg_statio_user_sequences| SELECT relid, schemaname, relname, blks_read, - blks_hit + blks_hit, + stats_reset FROM pg_statio_all_sequences WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); pg_statio_user_tables| SELECT relid, @@ -2452,12 +2599,15 @@ pg_statio_user_tables| SELECT relid, toast_blks_read, toast_blks_hit, tidx_blks_read, - tidx_blks_hit + tidx_blks_hit, + stats_reset FROM pg_statio_all_tables WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text)); pg_stats| SELECT n.nspname AS schemaname, c.relname AS tablename, + a.attrelid AS tableid, a.attname, + a.attnum, s.stainherit AS inherited, s.stanullfrac AS null_frac, s.stawidth AS avg_width, @@ -2549,8 +2699,10 @@ pg_stats| SELECT n.nspname AS schemaname, WHERE ((NOT a.attisdropped) AND has_column_privilege(c.oid, a.attnum, 'select'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid)))); pg_stats_ext| SELECT cn.nspname AS schemaname, c.relname AS tablename, + s.stxrelid AS tableid, sn.nspname AS statistics_schemaname, s.stxname AS statistics_name, + s.oid AS statistics_id, pg_get_userbyid(s.stxowner) AS statistics_owner, ( SELECT array_agg(a.attname ORDER BY a.attnum) AS array_agg FROM (unnest(s.stxkeys) k(k) @@ -2577,8 +2729,10 @@ pg_stats_ext| SELECT cn.nspname AS schemaname, WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid)))); pg_stats_ext_exprs| SELECT cn.nspname AS schemaname, c.relname AS tablename, + s.stxrelid AS tableid, sn.nspname AS statistics_schemaname, s.stxname AS statistics_name, + s.oid AS statistics_id, pg_get_userbyid(s.stxowner) AS statistics_owner, stat.expr, sd.stxdinherit AS inherited, @@ -2640,7 +2794,31 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname, WHEN ((stat.a).stakind4 = 5) THEN (stat.a).stanumbers4 WHEN ((stat.a).stakind5 = 5) THEN (stat.a).stanumbers5 ELSE NULL::real[] - END AS elem_count_histogram + END AS elem_count_histogram, + CASE + WHEN ((stat.a).stakind1 = 6) THEN (stat.a).stavalues1 + WHEN ((stat.a).stakind2 = 6) THEN (stat.a).stavalues2 + WHEN ((stat.a).stakind3 = 6) THEN (stat.a).stavalues3 + WHEN ((stat.a).stakind4 = 6) THEN (stat.a).stavalues4 + WHEN ((stat.a).stakind5 = 6) THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS range_length_histogram, + CASE + WHEN ((stat.a).stakind1 = 6) THEN (stat.a).stanumbers1[1] + WHEN ((stat.a).stakind2 = 6) THEN (stat.a).stanumbers2[1] + WHEN ((stat.a).stakind3 = 6) THEN (stat.a).stanumbers3[1] + WHEN ((stat.a).stakind4 = 6) THEN (stat.a).stanumbers4[1] + WHEN ((stat.a).stakind5 = 6) THEN (stat.a).stanumbers5[1] + ELSE NULL::real + END AS range_empty_frac, + CASE + WHEN ((stat.a).stakind1 = 7) THEN (stat.a).stavalues1 + WHEN ((stat.a).stakind2 = 7) THEN (stat.a).stavalues2 + WHEN ((stat.a).stakind3 = 7) THEN (stat.a).stavalues3 + WHEN ((stat.a).stakind4 = 7) THEN (stat.a).stavalues4 + WHEN ((stat.a).stakind5 = 7) THEN (stat.a).stavalues5 + ELSE NULL::anyarray + END AS range_bounds_histogram FROM (((((pg_statistic_ext s JOIN pg_class c ON ((c.oid = s.stxrelid))) LEFT JOIN pg_statistic_ext_data sd ON ((s.oid = sd.stxoid))) @@ -3528,6 +3706,61 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name; (3 rows) DROP RULE hat_upsert ON hats; +-- DO SELECT with a WHERE clause +CREATE RULE hat_confsel AS ON INSERT TO hats + DO INSTEAD + INSERT INTO hat_data VALUES ( + NEW.hat_name, + NEW.hat_color) + ON CONFLICT (hat_name) + DO SELECT FOR UPDATE + WHERE excluded.hat_color <> 'forbidden' AND hat_data.* != excluded.* + RETURNING *; +SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename; + definition +-------------------------------------------------------------------------------------- + CREATE RULE hat_confsel AS + + ON INSERT TO public.hats DO INSTEAD INSERT INTO hat_data (hat_name, hat_color) + + VALUES (new.hat_name, new.hat_color) ON CONFLICT(hat_name) DO SELECT FOR UPDATE + + WHERE ((excluded.hat_color <> 'forbidden'::bpchar) AND (hat_data.* <> excluded.*))+ + RETURNING hat_data.hat_name, + + hat_data.hat_color; +(1 row) + +-- fails without RETURNING +INSERT INTO hats VALUES ('h7', 'blue'); +ERROR: ON CONFLICT DO SELECT requires a RETURNING clause +DETAIL: A rule action is INSERT ... ON CONFLICT DO SELECT, which requires a RETURNING clause. +-- works (returns conflicts) +EXPLAIN (costs off) +INSERT INTO hats VALUES ('h7', 'blue') RETURNING *; + QUERY PLAN +------------------------------------------------------------------------------------------------- + Insert on hat_data + Conflict Resolution: SELECT FOR UPDATE + Conflict Arbiter Indexes: hat_data_unique_idx + Conflict Filter: ((excluded.hat_color <> 'forbidden'::bpchar) AND (hat_data.* <> excluded.*)) + -> Result +(5 rows) + +INSERT INTO hats VALUES ('h7', 'blue') RETURNING *; + hat_name | hat_color +------------+------------ + h7 | black +(1 row) + +-- conflicts excluded by WHERE clause +INSERT INTO hats VALUES ('h7', 'forbidden') RETURNING *; + hat_name | hat_color +----------+----------- +(0 rows) + +INSERT INTO hats VALUES ('h7', 'black') RETURNING *; + hat_name | hat_color +----------+----------- +(0 rows) + +DROP RULE hat_confsel ON hats; drop table hats; drop table hat_data; -- test for pg_get_functiondef properly regurgitating SET parameters @@ -3539,6 +3772,7 @@ CREATE FUNCTION func_with_set_params() RETURNS integer SET extra_float_digits TO 2 SET work_mem TO '4MB' SET datestyle to iso, mdy + SET temp_tablespaces to NULL SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' IMMUTABLE STRICT; SELECT pg_get_functiondef('func_with_set_params()'::regprocedure); @@ -3552,6 +3786,7 @@ SELECT pg_get_functiondef('func_with_set_params()'::regprocedure); SET extra_float_digits TO '2' + SET work_mem TO '4MB' + SET "DateStyle" TO 'iso, mdy' + + SET temp_tablespaces TO NULL + SET local_preload_libraries TO 'Mixed/Case', 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'+ AS $function$select 1;$function$ + diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out index bab0cc93ff5ec..34f040beecca5 100644 --- a/src/test/regress/expected/select.out +++ b/src/test/regress/expected/select.out @@ -861,7 +861,6 @@ select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; 11 (1 row) -RESET enable_indexscan; -- check multi-index cases too explain (costs off) select unique1, unique2 from onek2 @@ -908,6 +907,20 @@ select unique1, unique2 from onek2 0 | 998 (2 rows) +RESET enable_indexscan; +-- onek2_u2_prtl should be preferred over this index, but we have to +-- discount the metapage to arrive at that answer +begin; +create index onek2_index_full on onek2 (stringu1, unique2); +explain (costs off) +select unique2 from onek2 + where stringu1 < 'B'::name; + QUERY PLAN +---------------------------------------------- + Index Only Scan using onek2_u2_prtl on onek2 +(1 row) + +rollback; -- -- Test some corner cases that have been known to confuse the planner -- @@ -962,10 +975,11 @@ create table list_parted_tbl (a int,b int) partition by list (a); create table list_parted_tbl1 partition of list_parted_tbl for values in (1) partition by list(b); explain (costs off) select * from list_parted_tbl; - QUERY PLAN --------------------------- + QUERY PLAN +------------------------------------- Result + Replaces: Scan on list_parted_tbl One-Time Filter: false -(2 rows) +(3 rows) drop table list_parted_tbl; diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out index 0185ef661b135..933921d1860b5 100644 --- a/src/test/regress/expected/select_parallel.out +++ b/src/test/regress/expected/select_parallel.out @@ -156,9 +156,9 @@ explain (costs off) -> Parallel Append -> Parallel Seq Scan on part_pa_test_p1 pa2_1 -> Parallel Seq Scan on part_pa_test_p2 pa2_2 - SubPlan 2 + SubPlan expr_1 -> Result - SubPlan 1 + SubPlan expr_2 -> Append -> Seq Scan on part_pa_test_p1 pa1_1 Filter: (a = pa2.a) @@ -302,15 +302,15 @@ alter table tenk2 set (parallel_workers = 0); explain (costs off) select count(*) from tenk1 where (two, four) not in (select hundred, thousand from tenk2 where thousand > 100); - QUERY PLAN ----------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------ Finalize Aggregate -> Gather Workers Planned: 4 -> Partial Aggregate -> Parallel Seq Scan on tenk1 - Filter: (NOT (ANY ((two = (hashed SubPlan 1).col1) AND (four = (hashed SubPlan 1).col2)))) - SubPlan 1 + Filter: (NOT (ANY ((two = (hashed SubPlan any_1).col1) AND (four = (hashed SubPlan any_1).col2)))) + SubPlan any_1 -> Seq Scan on tenk2 Filter: (thousand > 100) (9 rows) @@ -326,11 +326,11 @@ select count(*) from tenk1 where (two, four) not in explain (costs off) select * from tenk1 where (unique1 + random())::integer not in (select ten from tenk2); - QUERY PLAN -------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------- Seq Scan on tenk1 - Filter: (NOT (ANY ((((unique1)::double precision + random()))::integer = (hashed SubPlan 1).col1))) - SubPlan 1 + Filter: (NOT (ANY ((((unique1)::double precision + random()))::integer = (hashed SubPlan any_1).col1))) + SubPlan any_1 -> Seq Scan on tenk2 (4 rows) @@ -343,10 +343,10 @@ alter table tenk2 set (parallel_workers = 2); explain (costs off) select count(*) from tenk1 where tenk1.unique1 = (Select max(tenk2.unique1) from tenk2); - QUERY PLAN ------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------- Aggregate - InitPlan 1 + InitPlan expr_1 -> Finalize Aggregate -> Gather Workers Planned: 2 @@ -355,7 +355,7 @@ explain (costs off) -> Gather Workers Planned: 4 -> Parallel Seq Scan on tenk1 - Filter: (unique1 = (InitPlan 1).col1) + Filter: (unique1 = (InitPlan expr_1).col1) (11 rows) select count(*) from tenk1 @@ -395,17 +395,17 @@ select count((unique1)) from tenk1 where hundred > 1; explain (costs off) select count((unique1)) from tenk1 where hundred = any ((select array_agg(i) from generate_series(1, 100, 15) i)::int[]); - QUERY PLAN ---------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------- Finalize Aggregate - InitPlan 1 + InitPlan expr_1 -> Aggregate -> Function Scan on generate_series i -> Gather Workers Planned: 4 -> Partial Aggregate -> Parallel Index Scan using tenk1_hundred on tenk1 - Index Cond: (hundred = ANY ((InitPlan 1).col1)) + Index Cond: (hundred = ANY ((InitPlan expr_1).col1)) (9 rows) select count((unique1)) from tenk1 @@ -1224,24 +1224,24 @@ ORDER BY 1; -> Append -> Gather Workers Planned: 4 - InitPlan 1 + InitPlan expr_1 -> Limit -> Gather Workers Planned: 4 -> Parallel Seq Scan on tenk1 tenk1_2 Filter: (fivethous = 1) -> Parallel Seq Scan on tenk1 - Filter: (fivethous = (InitPlan 1).col1) + Filter: (fivethous = (InitPlan expr_1).col1) -> Gather Workers Planned: 4 - InitPlan 2 + InitPlan expr_2 -> Limit -> Gather Workers Planned: 4 -> Parallel Seq Scan on tenk1 tenk1_3 Filter: (fivethous = 1) -> Parallel Seq Scan on tenk1 tenk1_1 - Filter: (fivethous = (InitPlan 2).col1) + Filter: (fivethous = (InitPlan expr_2).col1) (23 rows) -- test interaction with SRFs @@ -1254,10 +1254,10 @@ ORDER BY 1, 2, 3; EXPLAIN (VERBOSE, COSTS OFF) SELECT generate_series(1, two), array(select generate_series(1, two)) FROM tenk1 ORDER BY tenthous; - QUERY PLAN ---------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------- ProjectSet - Output: generate_series(1, tenk1.two), ARRAY(SubPlan 1), tenk1.tenthous + Output: generate_series(1, tenk1.two), ARRAY(SubPlan array_1), tenk1.tenthous -> Gather Merge Output: tenk1.two, tenk1.tenthous Workers Planned: 4 @@ -1268,7 +1268,7 @@ SELECT generate_series(1, two), array(select generate_series(1, two)) Sort Key: tenk1.tenthous -> Parallel Seq Scan on public.tenk1 Output: tenk1.tenthous, tenk1.two - SubPlan 1 + SubPlan array_1 -> ProjectSet Output: generate_series(1, tenk1.two) -> Result @@ -1333,11 +1333,11 @@ SELECT 1 FROM tenk1_vw_sec QUERY PLAN ------------------------------------------------------------------- Subquery Scan on tenk1_vw_sec - Filter: ((SubPlan 1) < 100) + Filter: ((SubPlan expr_1) < 100) -> Gather Workers Planned: 4 -> Parallel Index Only Scan using tenk1_unique1 on tenk1 - SubPlan 1 + SubPlan expr_1 -> Aggregate -> Seq Scan on int4_tbl Filter: (f1 < tenk1_vw_sec.unique1) diff --git a/src/test/regress/expected/sequence.out b/src/test/regress/expected/sequence.out index 15925d99c8a38..a0883b110070b 100644 --- a/src/test/regress/expected/sequence.out +++ b/src/test/regress/expected/sequence.out @@ -313,8 +313,7 @@ ALTER SEQUENCE IF EXISTS sequence_test2 RESTART WITH 24 INCREMENT BY 4 MAXVALUE 36 MINVALUE 5 CYCLE; NOTICE: relation "sequence_test2" does not exist, skipping ALTER SEQUENCE serialTest1 CYCLE; -- error, not a sequence -ERROR: cannot open relation "serialtest1" -DETAIL: This operation is not supported for tables. +ERROR: "serialtest1" is not a sequence CREATE SEQUENCE sequence_test2 START WITH 32; CREATE SEQUENCE sequence_test4 INCREMENT BY -1; SELECT nextval('sequence_test2'); @@ -840,10 +839,10 @@ SELECT nextval('test_seq1'); (1 row) -- pg_get_sequence_data -SELECT * FROM pg_get_sequence_data('test_seq1'); - last_value | is_called -------------+----------- - 10 | t +SELECT last_value, is_called, page_lsn <= pg_current_wal_lsn() as lsn FROM pg_get_sequence_data('test_seq1'); + last_value | is_called | lsn +------------+-----------+----- + 10 | t | t (1 row) DROP SEQUENCE test_seq1; diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 625acf3019a38..f3be69838bfc9 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1093,8 +1093,8 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING QUERY PLAN --------------------------------------------------------------------- Result - Output: (InitPlan 1).col1 - InitPlan 1 + Output: (InitPlan expr_1).col1 + InitPlan expr_1 -> Aggregate Output: JSON_ARRAYAGG("*VALUES*".column1 RETURNING jsonb) -> Values Scan on "*VALUES*" @@ -1109,6 +1109,171 @@ CREATE OR REPLACE VIEW public.json_array_subquery_view AS FROM ( SELECT foo.i FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i)) q(a)) AS "json_array" DROP VIEW json_array_subquery_view; +-- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT +create type comp1 as (a int, b date); +create domain d_comp1 as comp1; +create domain mydomain as timestamptz; +create type mydomainrange as range(subtype=mydomain); +create type comp3 as (a int, b mydomainrange); +create table test_mutability( + a text[], b timestamp, c timestamptz, + d date, f1 comp1[], f2 timestamp[], + f3 d_comp1[], + f4 mydomainrange[], + f5 comp3, + f6 mydomainmultirange); +-- JSON_OBJECTAGG, JSON_ARRAYAGG are aggregate functions, cannot be used in index +create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning jsonb)); +ERROR: aggregate functions are not allowed in index expressions +LINE 1: create index xx on test_mutability(json_objectagg(a: b absen... + ^ +create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning json)); +ERROR: aggregate functions are not allowed in index expressions +LINE 1: create index xx on test_mutability(json_objectagg(a: b absen... + ^ +create index xx on test_mutability(json_arrayagg(a returning jsonb)); +ERROR: aggregate functions are not allowed in index expressions +LINE 1: create index xx on test_mutability(json_arrayagg(a returning... + ^ +create index xx on test_mutability(json_arrayagg(a returning json)); +ERROR: aggregate functions are not allowed in index expressions +LINE 1: create index xx on test_mutability(json_arrayagg(a returning... + ^ +-- jsonb: create expression index via json_array +create index on test_mutability(json_array(a returning jsonb)); -- ok +create index on test_mutability(json_array(b returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(b returning jsonb... + ^ +create index on test_mutability(json_array(c returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(c returning jsonb... + ^ +create index on test_mutability(json_array(d returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(d returning jsonb... + ^ +create index on test_mutability(json_array(f1 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f1 returning json... + ^ +create index on test_mutability(json_array(f2 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f2 returning json... + ^ +create index on test_mutability(json_array(f3 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f3 returning json... + ^ +create index on test_mutability(json_array(f4 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f4 returning json... + ^ +create index on test_mutability(json_array(f5 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f5 returning json... + ^ +create index on test_mutability(json_array(f6 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_array(f6 returning json... + ^ +-- jsonb: create expression index via json_object +create index on test_mutability(json_object('hello' value a returning jsonb)); -- ok +create index on test_mutability(json_object('hello' value b returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value b ... + ^ +create index on test_mutability(json_object('hello' value c returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value c ... + ^ +create index on test_mutability(json_object('hello' value d returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value d ... + ^ +create index on test_mutability(json_object('hello' value f1 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f1... + ^ +create index on test_mutability(json_object('hello' value f2 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f2... + ^ +create index on test_mutability(json_object('hello' value f3 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f3... + ^ +create index on test_mutability(json_object('hello' value f4 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f4... + ^ +create index on test_mutability(json_object('hello' value f5 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f5... + ^ +create index on test_mutability(json_object('hello' value f6 returning jsonb)); -- error +ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: create index on test_mutability(json_object('hello' value f6... + ^ +-- data type json doesn't have a default operator class for access method "btree" so +-- we use a generated column to test whether the JSON_ARRAY expression is +-- immutable +alter table test_mutability add column f10 json generated always as (json_array(a returning json)); -- ok +alter table test_mutability add column f11 json generated always as (json_array(b returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(c returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(d returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(f1 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(f2 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(f3 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(f4 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(f5 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f11 json generated always as (json_array(f6 returning json)); -- error +ERROR: generation expression is not immutable +-- data type json doesn't have a default operator class for access method "btree" so +-- we use a generated column to test whether the JSON_OBJECT expression is +-- immutable +alter table test_mutability add column f11 json generated always as (json_object('hello' value a returning json)); -- ok +alter table test_mutability add column f12 json generated always as (json_object('hello' value b returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value c returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value d returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value f1 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value f2 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value f3 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value f4 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value f5 returning json)); -- error +ERROR: generation expression is not immutable +alter table test_mutability add column f12 json generated always as (json_object('hello' value f6 returning json)); -- error +ERROR: generation expression is not immutable +drop table test_mutability; +drop domain d_comp1; +drop type comp3; +drop type mydomainrange; +drop domain mydomain; +drop type comp1; +-- Range/multirange with immutable subtype should be considered immutable +create type range_int as range(subtype=int); +create table test_range_immutable(r range_int, m multirange_int); +create index on test_range_immutable(json_array(r returning jsonb)); -- ok +create index on test_range_immutable(json_array(m returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value r returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value m returning jsonb)); -- ok +drop table test_range_immutable; +drop type range_int; -- IS JSON predicate SELECT NULL IS JSON; ?column? @@ -1148,6 +1313,63 @@ SELECT NULL::bytea IS JSON; SELECT NULL::int IS JSON; ERROR: cannot use type integer in IS JSON predicate +LINE 1: SELECT NULL::int IS JSON; + ^ +-- IS JSON with domain types +CREATE DOMAIN jd1 AS json CHECK ((VALUE ->'a')::text <> '3'); +CREATE DOMAIN jd2 AS jsonb CHECK ((VALUE ->'a') = '1'::jsonb); +CREATE DOMAIN jd3 AS text CHECK (VALUE <> 'a'); +CREATE DOMAIN jd4 AS bytea CHECK (VALUE <> '\x61'); +CREATE DOMAIN jd5 AS date CHECK (VALUE <> NULL); +-- NULLs through domains should return NULL (not error) +SELECT NULL::jd1 IS JSON, NULL::jd2 IS JSON, NULL::jd3 IS JSON, NULL::jd4 IS JSON; + ?column? | ?column? | ?column? | ?column? +----------+----------+----------+---------- + | | | +(1 row) + +SELECT NULL::jd1 IS NOT JSON; + ?column? +---------- + +(1 row) + +-- domain over unsupported base type should error +SELECT NULL::jd5 IS JSON; -- error +ERROR: cannot use type jd5 in IS JSON predicate +LINE 1: SELECT NULL::jd5 IS JSON; + ^ +SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error +ERROR: cannot use type jd5 in IS JSON predicate +LINE 1: SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; + ^ +-- domain constraint violation during cast +SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error +ERROR: value for domain jd2 violates check constraint "jd2_check" +-- view creation and deparsing with domain IS JSON +CREATE VIEW domain_isjson AS +WITH cte(a) AS (VALUES('{"a": 1, "a": 2}')) +SELECT a::jd1 IS JSON WITH UNIQUE KEYS as jd1, + a::jd3 IS JSON WITH UNIQUE KEYS as jd3, + a::jd4 IS JSON WITH UNIQUE KEYS as jd4 +FROM cte; +\sv domain_isjson +CREATE OR REPLACE VIEW public.domain_isjson AS + WITH cte(a) AS ( + VALUES ('{"a": 1, "a": 2}'::text) + ) + SELECT a::jd1 IS JSON WITH UNIQUE KEYS AS jd1, + a::jd3 IS JSON WITH UNIQUE KEYS AS jd3, + a::jd4 IS JSON WITH UNIQUE KEYS AS jd4 + FROM cte +SELECT * FROM domain_isjson; + jd1 | jd3 | jd4 +-----+-----+----- + f | f | f +(1 row) + +DROP VIEW domain_isjson; +DROP DOMAIN jd5, jd4, jd3, jd2, jd1; SELECT '' IS JSON; ?column? ---------- diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out index 5a35aeb7bba3a..57e52e963f685 100644 --- a/src/test/regress/expected/sqljson_queryfuncs.out +++ b/src/test/regress/expected/sqljson_queryfuncs.out @@ -1147,75 +1147,139 @@ INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); ERROR: new row for relation "test_jsonb_constraints" violates check constraint "test_jsonb_constraint4" DETAIL: Failing row contains ({"a": 10}, 1, [1, 2]). DROP TABLE test_jsonb_constraints; --- Test mutabilily of query functions +-- Test mutability of query functions CREATE TABLE test_jsonb_mutability(js jsonb, b int); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.time()')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.tim... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.date()')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.dat... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.time_tz()')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.tim... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.timestamp()')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.tim... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.timestamp_tz()')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.tim... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.date() < $.time_tz())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.date() < $.time())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.time() < $.time())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.time() < $.time_tz())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp() < $.timestamp_tz())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp_tz() < $.timestamp_tz())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.time() < $.datetime("HH:MI TZH"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.date() < $.datetime("HH:MI TZH"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp() < $.datetime("HH:MI TZH"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp() < $.datetime("HH:MI"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp_tz() < $.datetime("HH:MI TZH"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp_tz() < $.datetime("HH:MI"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.date() < $x' PASSING '12:34'::timetz AS x)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.dat... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.date() < $x' PASSING '1234'::int AS x)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.dat... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.timestamp(2) < $.timestamp(3))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime()')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@ < $.datetime())')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime())')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime() < $.datetime("HH:MI TZH"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("HH:MI TZH"))')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI") < $.datetime("YY-MM-DD HH:MI"))')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ? (@.datetime("HH:MI TZH") < $.datetime("YY-MM-DD HH:MI"))')); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a ?... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $x' PASSING '12:34'::timetz AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("HH:MI TZH") < $y' PASSING '12:34'::timetz AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '12:34'::timetz AS x)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.dat... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() < $x' PASSING '1234'::int AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime() ? (@ == $x)' PASSING '12:34'::time AS x)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.dat... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.datetime("YY-MM-DD") ? (@ == $x)' PASSING '2020-07-14'::date AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, ... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, ... + ^ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_VALUE(js, '$' DEFAULT random()::int ON ERROR)); ERROR: functions in index expression must be marked IMMUTABLE +LINE 1: CREATE INDEX ON test_jsonb_mutability (JSON_VALUE(js, '$' DE... + ^ +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.rtrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.ltrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.btrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.lower()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.upper()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.initcap()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.replace("hello", "bye")')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.split_part(",", 2)')); -- DEFAULT expression CREATE OR REPLACE FUNCTION ret_setint() RETURNS SETOF integer AS $$ @@ -1331,6 +1395,10 @@ SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); [123] (1 row) +SELECT JSON_QUERY(jsonb '{"a": 123}', ('$' || '.' || 'a' || NULL)::date WITH WRAPPER); +ERROR: JSON path expression must be of type jsonpath, not of type date +LINE 1: SELECT JSON_QUERY(jsonb '{"a": 123}', ('$' || '.' || 'a' || ... + ^ -- Should fail (invalid path) SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); ERROR: syntax error at or near " " of jsonpath input @@ -1355,6 +1423,10 @@ SELECT json_value('"aaa"', path RETURNING json) FROM jsonpaths; "aaa" (1 row) +SELECT json_value('"aaa"', jsonpaths RETURNING json) FROM jsonpaths; +ERROR: JSON path expression must be of type jsonpath, not of type jsonpaths +LINE 1: SELECT json_value('"aaa"', jsonpaths RETURNING json) FROM js... + ^ -- Test PASSING argument parsing SELECT JSON_QUERY(jsonb 'null', '$xyz' PASSING 1 AS xy); ERROR: could not find jsonpath variable "xyz" diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index 776f1ad0e5347..e5dcb85abd8d4 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -51,6 +51,22 @@ client backend|relation|vacuum client backend|temp relation|normal client backend|wal|init client backend|wal|normal +datachecksum launcher|relation|bulkread +datachecksum launcher|relation|bulkwrite +datachecksum launcher|relation|init +datachecksum launcher|relation|normal +datachecksum launcher|relation|vacuum +datachecksum launcher|temp relation|normal +datachecksum launcher|wal|init +datachecksum launcher|wal|normal +datachecksum worker|relation|bulkread +datachecksum worker|relation|bulkwrite +datachecksum worker|relation|init +datachecksum worker|relation|normal +datachecksum worker|relation|vacuum +datachecksum worker|temp relation|normal +datachecksum worker|wal|init +datachecksum worker|wal|normal io worker|relation|bulkread io worker|relation|bulkwrite io worker|relation|init @@ -95,7 +111,7 @@ walsummarizer|wal|init walsummarizer|wal|normal walwriter|wal|init walwriter|wal|normal -(79 rows) +(95 rows) \a -- ensure that both seqscan and indexscan plans are allowed SET enable_seqscan TO on; @@ -666,16 +682,24 @@ SELECT last_seq_scan, last_idx_scan FROM pg_stat_all_tables WHERE relid = 'test_ (1 row) COMMIT; +SELECT stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; + has_stats_reset +----------------- + f +(1 row) + SELECT pg_stat_reset_single_table_counters('test_last_scan'::regclass); pg_stat_reset_single_table_counters ------------------------------------- (1 row) -SELECT seq_scan, idx_scan FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; - seq_scan | idx_scan -----------+---------- - 0 | 0 +SELECT seq_scan, idx_scan, stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; + seq_scan | idx_scan | has_stats_reset +----------+----------+----------------- + 0 | 0 | t (1 row) -- ensure we start out with exactly one index and sequential scan @@ -850,6 +874,29 @@ FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; 2 | t | 3 | t (1 row) +-- check the stats in pg_stat_all_indexes +SELECT idx_scan, :'test_last_idx' < last_idx_scan AS idx_ok, + stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_indexes WHERE indexrelid = 'test_last_scan_pkey'::regclass; + idx_scan | idx_ok | has_stats_reset +----------+--------+----------------- + 3 | t | f +(1 row) + +-- check that the stats in pg_stat_all_indexes are reset +SELECT pg_stat_reset_single_table_counters('test_last_scan_pkey'::regclass); + pg_stat_reset_single_table_counters +------------------------------------- + +(1 row) + +SELECT idx_scan, stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_indexes WHERE indexrelid = 'test_last_scan_pkey'::regclass; + idx_scan | has_stats_reset +----------+----------------- + 0 | t +(1 row) + ----- -- Test reset of some stats for shared table ----- @@ -877,6 +924,8 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables t (1 row) +-- stats_reset may not be set for datid=0 and shared objects in +-- pg_stat_database, so reset once. SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); pg_stat_reset_single_table_counters ------------------------------------- @@ -890,6 +939,22 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables f (1 row) +SELECT stats_reset AS shared_db_reset_before + FROM pg_stat_database WHERE datid = 0 \gset +-- Second reset for comparison. +SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); + pg_stat_reset_single_table_counters +------------------------------------- + +(1 row) + +SELECT stats_reset > :'shared_db_reset_before'::timestamptz AS has_updated + FROM pg_stat_database WHERE datid = 0; + has_updated +------------- + t +(1 row) + -- set back comment \if :{?description_before} COMMENT ON DATABASE :"datname" IS :'description_before'; @@ -900,7 +965,7 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables -- Test that various stats views are being properly populated ----- -- Test that sessions is incremented when a new session is started in pg_stat_database -SELECT sessions AS db_stat_sessions FROM pg_stat_database WHERE datname = (SELECT current_database()) \gset +SELECT sessions AS db_stat_sessions FROM pg_stat_database WHERE datname = current_database() \gset \c SELECT pg_stat_force_next_flush(); pg_stat_force_next_flush @@ -908,7 +973,7 @@ SELECT pg_stat_force_next_flush(); (1 row) -SELECT sessions > :db_stat_sessions FROM pg_stat_database WHERE datname = (SELECT current_database()); +SELECT sessions > :db_stat_sessions FROM pg_stat_database WHERE datname = current_database(); ?column? ---------- t @@ -926,8 +991,19 @@ DROP TABLE test_stats_temp; -- Checkpoint twice: The checkpointer reports stats after reporting completion -- of the checkpoint. But after a second checkpoint we'll see at least the -- results of the first. -CHECKPOINT; -CHECKPOINT; +-- +-- While at it, test checkpoint options. Note that we don't test MODE SPREAD +-- because it would prolong the test. +CHECKPOINT (WRONG); +ERROR: unrecognized CHECKPOINT option "wrong" +LINE 1: CHECKPOINT (WRONG); + ^ +CHECKPOINT (MODE WRONG); +ERROR: unrecognized value for CHECKPOINT option "mode": "wrong" +LINE 1: CHECKPOINT (MODE WRONG); + ^ +CHECKPOINT (MODE FAST, FLUSH_UNLOGGED FALSE); +CHECKPOINT (FLUSH_UNLOGGED); SELECT num_requested > :rqst_ckpts_before FROM pg_stat_checkpointer; ?column? ---------- @@ -1089,27 +1165,75 @@ SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal; SELECT pg_stat_reset_shared('unknown'); ERROR: unrecognized reset target: "unknown" HINT: Target must be "archiver", "bgwriter", "checkpointer", "io", "recovery_prefetch", "slru", or "wal". --- Test that reset works for pg_stat_database --- Since pg_stat_database stats_reset starts out as NULL, reset it once first so we have something to compare it to +-- Test that reset works for pg_stat_database and pg_stat_database_conflicts +-- Since pg_stat_database stats_reset starts out as NULL, reset it once first so that we +-- have a baseline for comparison. The same for pg_stat_database_conflicts as it shares +-- the same stats_reset as pg_stat_database. SELECT pg_stat_reset(); pg_stat_reset --------------- (1 row) -SELECT stats_reset AS db_reset_ts FROM pg_stat_database WHERE datname = (SELECT current_database()) \gset +SELECT stats_reset AS db_reset_ts FROM pg_stat_database WHERE datname = current_database() \gset +SELECT stats_reset AS dbc_reset_ts FROM pg_stat_database_conflicts WHERE datname = current_database() \gset +SELECT :'db_reset_ts'::timestamptz = :'dbc_reset_ts'::timestamptz; + ?column? +---------- + t +(1 row) + SELECT pg_stat_reset(); pg_stat_reset --------------- (1 row) -SELECT stats_reset > :'db_reset_ts'::timestamptz FROM pg_stat_database WHERE datname = (SELECT current_database()); +SELECT stats_reset > :'db_reset_ts'::timestamptz FROM pg_stat_database WHERE datname = current_database(); + ?column? +---------- + t +(1 row) + +SELECT stats_reset > :'dbc_reset_ts'::timestamptz FROM pg_stat_database_conflicts WHERE datname = current_database(); + ?column? +---------- + t +(1 row) + +-- Test that reset works for pg_statio_all_sequences +-- Use the sequence to accumulate its stats, and reset them once first +-- so that we have a baseline for comparison, similar to the previous test. +-- stats_reset to compare to. +CREATE SEQUENCE test_seq1; +SELECT nextval('test_seq1'); + nextval +--------- + 1 +(1 row) + +SELECT pg_stat_reset_single_table_counters('test_seq1'::regclass); + pg_stat_reset_single_table_counters +------------------------------------- + +(1 row) + +SELECT stats_reset AS seq_reset_ts + FROM pg_statio_all_sequences WHERE relname ='test_seq1' \gset +SELECT pg_stat_reset_single_table_counters('test_seq1'::regclass); + pg_stat_reset_single_table_counters +------------------------------------- + +(1 row) + +SELECT stats_reset > :'seq_reset_ts'::timestamptz + FROM pg_statio_all_sequences WHERE relname ='test_seq1'; ?column? ---------- t (1 row) +DROP SEQUENCE test_seq1; ---- -- pg_stat_get_snapshot_timestamp behavior ---- @@ -1868,4 +1992,52 @@ SELECT * FROM check_estimated_rows('SELECT * FROM table_fillfactor'); (1 row) DROP TABLE table_fillfactor; +-- Test fastpath_exceeded stat +CREATE TABLE part_test (id int) PARTITION BY RANGE (id); +SELECT pg_stat_reset_shared('lock'); + pg_stat_reset_shared +---------------------- + +(1 row) + +-- Create partitions (exceeds number of slots) +DO $$ +DECLARE + max_locks int; +BEGIN + SELECT setting::int INTO max_locks + FROM pg_settings + WHERE name = 'max_locks_per_transaction'; + + FOR i IN 1..(max_locks + 10) LOOP + EXECUTE format( + 'CREATE TABLE part_test_%s PARTITION OF part_test + FOR VALUES FROM (%s) TO (%s)', + i, (i-1)*1000, i*1000 + ); + END LOOP; +END; +$$; +SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset +-- Needs a lock on each partition +SELECT count(*) FROM part_test; + count +------- + 0 +(1 row) + +-- Ensure pending stats are flushed +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation'; + ?column? +---------- + t +(1 row) + +DROP TABLE part_test; -- End of Stats Test diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index 6359e5fb689cb..37070c1a89639 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -25,7 +25,7 @@ begin end; $$; -- Verify failures -CREATE TABLE ext_stats_test (x text, y int, z int); +CREATE TABLE ext_stats_test (x text, y int, z int, w xid); CREATE STATISTICS tst; ERROR: syntax error at or near ";" LINE 1: CREATE STATISTICS tst; @@ -54,9 +54,29 @@ CREATE STATISTICS tst ON (x || 'x'), (x || 'x'), y FROM ext_stats_test; ERROR: duplicate expression in statistics definition CREATE STATISTICS tst (unrecognized) ON x, y FROM ext_stats_test; ERROR: unrecognized statistics kind "unrecognized" +-- unsupported targets +CREATE STATISTICS tst ON a FROM (VALUES (x)) AS foo; +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +CREATE STATISTICS tst ON a FROM foo NATURAL JOIN bar; +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +CREATE STATISTICS tst ON a FROM (SELECT * FROM ext_stats_test) AS foo; +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +CREATE STATISTICS tst ON a FROM ext_stats_test s TABLESAMPLE system (x); +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +CREATE STATISTICS tst ON a FROM XMLTABLE('foo' PASSING 'bar' COLUMNS a text); +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +CREATE STATISTICS tst ON a FROM JSON_TABLE(jsonb '123', '$' COLUMNS (item int)); +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +CREATE FUNCTION tftest(int) returns table(a int, b int) as $$ +SELECT $1, $1+i FROM generate_series(1,5) g(i); +$$ LANGUAGE sql IMMUTABLE STRICT; +CREATE STATISTICS alt_stat2 ON a FROM tftest(1); +ERROR: CREATE STATISTICS only supports relation names in the FROM clause +DROP FUNCTION tftest; -- incorrect expressions CREATE STATISTICS tst ON (y) FROM ext_stats_test; -- single column reference -ERROR: extended statistics require at least 2 columns +ERROR: cannot create extended statistics on a single non-virtual column +DETAIL: Univariate statistics are already built for each individual non-virtual table column. CREATE STATISTICS tst ON y + z FROM ext_stats_test; -- missing parentheses ERROR: syntax error at or near "+" LINE 1: CREATE STATISTICS tst ON y + z FROM ext_stats_test; @@ -65,30 +85,23 @@ CREATE STATISTICS tst ON (x, y) FROM ext_stats_test; -- tuple expression ERROR: syntax error at or near "," LINE 1: CREATE STATISTICS tst ON (x, y) FROM ext_stats_test; ^ -DROP TABLE ext_stats_test; --- statistics on virtual generated column not allowed -CREATE TABLE ext_stats_test1 (x int, y int, z int GENERATED ALWAYS AS (x+y) VIRTUAL, w xid); -CREATE STATISTICS tst on z from ext_stats_test1; -ERROR: statistics creation on virtual generated columns is not supported -CREATE STATISTICS tst on (z) from ext_stats_test1; -ERROR: statistics creation on virtual generated columns is not supported -CREATE STATISTICS tst on (z+1) from ext_stats_test1; -ERROR: statistics creation on virtual generated columns is not supported -CREATE STATISTICS tst (ndistinct) ON z from ext_stats_test1; -ERROR: statistics creation on virtual generated columns is not supported -- statistics on system column not allowed -CREATE STATISTICS tst on tableoid from ext_stats_test1; +CREATE STATISTICS tst on tableoid from ext_stats_test; ERROR: statistics creation on system columns is not supported -CREATE STATISTICS tst on (tableoid) from ext_stats_test1; +CREATE STATISTICS tst on (tableoid) from ext_stats_test; ERROR: statistics creation on system columns is not supported -CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test1; +CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test; ERROR: statistics creation on system columns is not supported -CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test1; +CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test; ERROR: statistics creation on system columns is not supported --- statistics without a less-than operator not supported -CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1; -ERROR: column "w" cannot be used in statistics because its type xid has no default btree operator class -DROP TABLE ext_stats_test1; +-- statistics kinds are not allowed with univariate statistics +CREATE STATISTICS tst (ndistinct) ON (y + z) FROM ext_stats_test; +ERROR: cannot specify statistics kinds when building univariate statistics +-- multivariate statistics without a less-than operator not supported +CREATE STATISTICS tst (ndistinct) ON x, w from ext_stats_test; +ERROR: cannot create multivariate statistics on column "w" +DETAIL: The type xid has no default btree operator class. +DROP TABLE ext_stats_test; -- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it CREATE TABLE ab1 (a INTEGER, b INTEGER, c INTEGER); CREATE STATISTICS IF NOT EXISTS ab1_a_b_stats ON a, b FROM ab1; @@ -103,6 +116,20 @@ ALTER STATISTICS ab1_a_b_stats RENAME TO ab1_a_b_stats_new; ERROR: must be owner of statistics object ab1_a_b_stats RESET SESSION AUTHORIZATION; DROP ROLE regress_stats_ext; +CREATE STATISTICS pg_temp.stats_ext_temp ON a, b FROM ab1; +SELECT regexp_replace(pg_describe_object(tableoid, oid, 0), + 'pg_temp_[0-9]*', 'pg_temp_REDACTED') AS descr, + pg_statistics_obj_is_visible(oid) AS visible + FROM pg_statistic_ext + WHERE stxname = 'stats_ext_temp'; + descr | visible +---------------------------------------------------+--------- + statistics object pg_temp_REDACTED.stats_ext_temp | f +(1 row) + +DROP STATISTICS stats_ext_temp; -- shall fail +ERROR: statistics object "stats_ext_temp" does not exist +DROP STATISTICS pg_temp.stats_ext_temp; CREATE STATISTICS IF NOT EXISTS ab1_a_b_stats ON a, b FROM ab1; NOTICE: statistics object "ab1_a_b_stats" already exists, skipping DROP STATISTICS ab1_a_b_stats; @@ -163,7 +190,10 @@ Statistics objects: "public.ab1_a_b_stats" ON a, b FROM ab1; STATISTICS 0 ANALYZE ab1; -SELECT stxname, stxdndistinct, stxddependencies, stxdmcv, stxdinherit +SELECT stxname, + replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct, + replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies, + stxdmcv, stxdinherit FROM pg_statistic_ext s LEFT JOIN pg_statistic_ext_data d ON (d.stxoid = s.oid) WHERE s.stxname = 'ab1_a_b_stats'; stxname | stxdndistinct | stxddependencies | stxdmcv | stxdinherit @@ -443,13 +473,16 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, ( -- correct command CREATE STATISTICS s10 ON a, b, c FROM ndistinct; ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; - stxkind | stxdndistinct ----------+----------------------------------------------------- - {d,f,m} | {"3, 4": 11, "3, 6": 11, "4, 6": 11, "3, 4, 6": 11} + stxkind | stxdndistinct +---------+--------------------------------------------- + {d,f,m} | [{"attributes": [3, 4], "ndistinct": 11}, + + | {"attributes": [3, 6], "ndistinct": 11}, + + | {"attributes": [4, 6], "ndistinct": 11}, + + | {"attributes": [3, 4, 6], "ndistinct": 11}] (1 row) -- minor improvement, make sure the ctid does not break the matching @@ -525,13 +558,16 @@ INSERT INTO ndistinct (a, b, c, filler1) mod(i,23) || ' dollars and zero cents' FROM generate_series(1,1000) s(i); ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; - stxkind | stxdndistinct ----------+---------------------------------------------------------- - {d,f,m} | {"3, 4": 221, "3, 6": 247, "4, 6": 323, "3, 4, 6": 1000} + stxkind | stxdndistinct +---------+----------------------------------------------- + {d,f,m} | [{"attributes": [3, 4], "ndistinct": 221}, + + | {"attributes": [3, 6], "ndistinct": 247}, + + | {"attributes": [4, 6], "ndistinct": 323}, + + | {"attributes": [3, 4, 6], "ndistinct": 1000}] (1 row) -- correct estimates @@ -590,7 +626,7 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, ( (1 row) DROP STATISTICS s10; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; @@ -674,13 +710,16 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, ( CREATE STATISTICS s10 (ndistinct) ON (a+1), (b+100), (2*c) FROM ndistinct; ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; - stxkind | stxdndistinct ----------+------------------------------------------------------------------- - {d,e} | {"-1, -2": 221, "-1, -3": 247, "-2, -3": 323, "-1, -2, -3": 1000} + stxkind | stxdndistinct +---------+-------------------------------------------------- + {d,e} | [{"attributes": [-1, -2], "ndistinct": 221}, + + | {"attributes": [-1, -3], "ndistinct": 247}, + + | {"attributes": [-2, -3], "ndistinct": 323}, + + | {"attributes": [-1, -2, -3], "ndistinct": 1000}] (1 row) SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY (a+1), (b+100)'); @@ -723,13 +762,16 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b CREATE STATISTICS s10 (ndistinct) ON a, b, (2*c) FROM ndistinct; ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; - stxkind | stxdndistinct ----------+------------------------------------------------------------- - {d,e} | {"3, 4": 221, "3, -1": 247, "4, -1": 323, "3, 4, -1": 1000} + stxkind | stxdndistinct +---------+------------------------------------------------ + {d,e} | [{"attributes": [3, 4], "ndistinct": 221}, + + | {"attributes": [3, -1], "ndistinct": 247}, + + | {"attributes": [4, -1], "ndistinct": 323}, + + | {"attributes": [3, 4, -1], "ndistinct": 1000}] (1 row) SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, b'); @@ -1280,10 +1322,14 @@ SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_dependencies; ANALYZE functional_dependencies; -- print the detected dependencies -SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; - dependencies ------------------------------------------------------------------------------------------------------------- - {"3 => 4": 1.000000, "3 => 6": 1.000000, "4 => 6": 1.000000, "3, 4 => 6": 1.000000, "3, 6 => 4": 1.000000} +SELECT replace(dependencies, '}, ', E'},\n') AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; + dependencies +-------------------------------------------------------------- + [{"attributes": [3], "dependency": 4, "degree": 1.000000}, + + {"attributes": [3], "dependency": 6, "degree": 1.000000}, + + {"attributes": [4], "dependency": 6, "degree": 1.000000}, + + {"attributes": [3, 4], "dependency": 6, "degree": 1.000000},+ + {"attributes": [3, 6], "dependency": 4, "degree": 1.000000}] (1 row) SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE a = 1 AND b = ''1'''); @@ -1622,10 +1668,14 @@ SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FROM functional_dependencies; ANALYZE functional_dependencies; -- print the detected dependencies -SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; - dependencies ------------------------------------------------------------------------------------------------------------------------- - {"-1 => -2": 1.000000, "-1 => -3": 1.000000, "-2 => -3": 1.000000, "-1, -2 => -3": 1.000000, "-1, -3 => -2": 1.000000} +SELECT replace(dependencies, '}, ', E'},\n') AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; + dependencies +----------------------------------------------------------------- + [{"attributes": [-1], "dependency": -2, "degree": 1.000000}, + + {"attributes": [-1], "dependency": -3, "degree": 1.000000}, + + {"attributes": [-2], "dependency": -3, "degree": 1.000000}, + + {"attributes": [-1, -2], "dependency": -3, "degree": 1.000000},+ + {"attributes": [-1, -3], "dependency": -2, "degree": 1.000000}] (1 row) SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1'''); @@ -3097,6 +3147,66 @@ SELECT c0 FROM ONLY expr_stats_incompatible_test WHERE (0 rows) DROP TABLE expr_stats_incompatible_test; +-- multivariate statistics on virtual generated columns +CREATE TABLE virtual_gen_stats (a int, b int, c int GENERATED ALWAYS AS (2*a), d int GENERATED ALWAYS AS (a+b), w xid GENERATED ALWAYS AS (a::text::xid)); +INSERT INTO virtual_gen_stats SELECT mod(i,10), mod(i,10) FROM generate_series(1,100) s(i); +ANALYZE virtual_gen_stats; +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0'); + estimated | actual +-----------+-------- + 1 | 10 +(1 row) + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0'); + estimated | actual +-----------+-------- + 1 | 10 +(1 row) + +CREATE STATISTICS virtual_gen_stats_1 (mcv) ON c, (3*b), d, (d-2*a) FROM virtual_gen_stats; +ANALYZE virtual_gen_stats; +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0'); + estimated | actual +-----------+-------- + 10 | 10 +(1 row) + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0'); + estimated | actual +-----------+-------- + 10 | 10 +(1 row) + +-- univariate statistics on individual virtual generated columns +DROP STATISTICS virtual_gen_stats_1; +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0'); + estimated | actual +-----------+-------- + 1 | 10 +(1 row) + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE w = 0'); + estimated | actual +-----------+-------- + 1 | 10 +(1 row) + +CREATE STATISTICS virtual_gen_stats_single ON c FROM virtual_gen_stats; +CREATE STATISTICS virtual_gen_stats_single_without_less_than ON w FROM virtual_gen_stats; +ANALYZE virtual_gen_stats; +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0'); + estimated | actual +-----------+-------- + 10 | 10 +(1 row) + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE w = 0'); + estimated | actual +-----------+-------- + 10 | 10 +(1 row) + +DROP TABLE virtual_gen_stats; -- Permission tests. Users should not be able to see specific data values in -- the extended statistics, if they lack permission to see those values in -- the underlying table. @@ -3164,45 +3274,45 @@ set search_path to public, stts_s1, stts_s2, tststats; (1 row) \dX+ - List of extended statistics - Schema | Name | Definition | Ndistinct | Dependencies | MCV -----------+------------------------+------------------------------------------------------------------+-----------+--------------+--------- - public | func_deps_stat | (a * 2), upper(b), (c + 1::numeric) FROM functional_dependencies | | defined | - public | mcv_lists_arrays_stats | a, b, c FROM mcv_lists_arrays | | | defined - public | mcv_lists_bool_stats | a, b, c FROM mcv_lists_bool | | | defined - public | mcv_lists_stats | a, b, d FROM mcv_lists | | | defined - public | stts_hoge | col1, col2, col3 FROM stts_t3 | defined | defined | defined - public | stts_t1_a_b_stat | a, b FROM stts_t1 | defined | | - public | stts_t1_a_b_stat1 | a, b FROM stts_t1 | defined | defined | - public | stts_t1_a_b_stat2 | a, b FROM stts_t1 | defined | defined | defined - public | stts_t2_b_c_stat | b, c FROM stts_t2 | defined | defined | defined - stts_s1 | stts_foo | col1, col2 FROM stts_t3 | defined | defined | defined - stts_s2 | stts_yama | col1, col3 FROM stts_t3 | | defined | defined - tststats | priv_test_stats | a, b FROM priv_test_tbl | | | defined + List of extended statistics + Schema | Name | Definition | Ndistinct | Dependencies | MCV | Description +----------+------------------------+------------------------------------------------------------------+-----------+--------------+---------+------------- + public | func_deps_stat | (a * 2), upper(b), (c + 1::numeric) FROM functional_dependencies | | defined | | + public | mcv_lists_arrays_stats | a, b, c FROM mcv_lists_arrays | | | defined | + public | mcv_lists_bool_stats | a, b, c FROM mcv_lists_bool | | | defined | + public | mcv_lists_stats | a, b, d FROM mcv_lists | | | defined | + public | stts_hoge | col1, col2, col3 FROM stts_t3 | defined | defined | defined | + public | stts_t1_a_b_stat | a, b FROM stts_t1 | defined | | | + public | stts_t1_a_b_stat1 | a, b FROM stts_t1 | defined | defined | | + public | stts_t1_a_b_stat2 | a, b FROM stts_t1 | defined | defined | defined | + public | stts_t2_b_c_stat | b, c FROM stts_t2 | defined | defined | defined | + stts_s1 | stts_foo | col1, col2 FROM stts_t3 | defined | defined | defined | + stts_s2 | stts_yama | col1, col3 FROM stts_t3 | | defined | defined | + tststats | priv_test_stats | a, b FROM priv_test_tbl | | | defined | (12 rows) \dX+ stts_t* - List of extended statistics - Schema | Name | Definition | Ndistinct | Dependencies | MCV ---------+-------------------+-------------------+-----------+--------------+--------- - public | stts_t1_a_b_stat | a, b FROM stts_t1 | defined | | - public | stts_t1_a_b_stat1 | a, b FROM stts_t1 | defined | defined | - public | stts_t1_a_b_stat2 | a, b FROM stts_t1 | defined | defined | defined - public | stts_t2_b_c_stat | b, c FROM stts_t2 | defined | defined | defined + List of extended statistics + Schema | Name | Definition | Ndistinct | Dependencies | MCV | Description +--------+-------------------+-------------------+-----------+--------------+---------+------------- + public | stts_t1_a_b_stat | a, b FROM stts_t1 | defined | | | + public | stts_t1_a_b_stat1 | a, b FROM stts_t1 | defined | defined | | + public | stts_t1_a_b_stat2 | a, b FROM stts_t1 | defined | defined | defined | + public | stts_t2_b_c_stat | b, c FROM stts_t2 | defined | defined | defined | (4 rows) \dX+ *stts_hoge - List of extended statistics - Schema | Name | Definition | Ndistinct | Dependencies | MCV ---------+-----------+-------------------------------+-----------+--------------+--------- - public | stts_hoge | col1, col2, col3 FROM stts_t3 | defined | defined | defined + List of extended statistics + Schema | Name | Definition | Ndistinct | Dependencies | MCV | Description +--------+-----------+-------------------------------+-----------+--------------+---------+------------- + public | stts_hoge | col1, col2, col3 FROM stts_t3 | defined | defined | defined | (1 row) \dX+ stts_s2.stts_yama - List of extended statistics - Schema | Name | Definition | Ndistinct | Dependencies | MCV ----------+-----------+-------------------------+-----------+--------------+--------- - stts_s2 | stts_yama | col1, col3 FROM stts_t3 | | defined | defined + List of extended statistics + Schema | Name | Definition | Ndistinct | Dependencies | MCV | Description +---------+-----------+-------------------------+-----------+--------------+---------+------------- + stts_s2 | stts_yama | col1, col3 FROM stts_t3 | | defined | defined | (1 row) create statistics (mcv) ON a, b, (a+b), (a-b) FROM stts_t1; @@ -3275,9 +3385,17 @@ CREATE FUNCTION op_leak(int, int) RETURNS bool LANGUAGE plpgsql; CREATE OPERATOR <<< (procedure = op_leak, leftarg = int, rightarg = int, restrict = scalarltsel); +CREATE FUNCTION op_leak(record, record) RETURNS bool + AS 'BEGIN RAISE NOTICE ''op_leak => %, %'', $1, $2; RETURN $1 < $2; END' + LANGUAGE plpgsql; +CREATE OPERATOR <<< (procedure = op_leak, leftarg = record, rightarg = record, + restrict = scalarltsel); SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied ERROR: permission denied for table priv_test_tbl -SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; +SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; -- Permission denied +ERROR: permission denied for table priv_test_tbl +SELECT * FROM tststats.priv_test_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied ERROR: permission denied for table priv_test_tbl DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied ERROR: permission denied for table priv_test_tbl @@ -3298,10 +3416,17 @@ SELECT * FROM tststats.priv_test_view WHERE a <<< 0 OR b <<< 0; -- Should not le ---+--- (0 rows) +SELECT * FROM tststats.priv_test_view t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak + a | b +---+--- +(0 rows) + DELETE FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not leak -- Grant table access, but hide all data with RLS RESET SESSION AUTHORIZATION; ALTER TABLE tststats.priv_test_tbl ENABLE ROW LEVEL SECURITY; +CREATE POLICY priv_test_tbl_pol ON tststats.priv_test_tbl USING (2 * a < 0); GRANT SELECT, DELETE ON tststats.priv_test_tbl TO regress_stats_user1; -- Should now have direct table access, but see nothing and leak nothing SET SESSION AUTHORIZATION regress_stats_user1; @@ -3310,12 +3435,57 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le ---+--- (0 rows) -SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; +SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; -- Should not leak + a | b +---+--- +(0 rows) + +SELECT * FROM tststats.priv_test_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak a | b ---+--- (0 rows) DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak +-- Create plain inheritance parent table with no access permissions +RESET SESSION AUTHORIZATION; +CREATE TABLE tststats.priv_test_parent_tbl (a int, b int); +ALTER TABLE tststats.priv_test_tbl INHERIT tststats.priv_test_parent_tbl; +-- Should not have access to parent, and should leak nothing +SET SESSION AUTHORIZATION regress_stats_user1; +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied +ERROR: permission denied for table priv_test_parent_tbl +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 OR b <<< 0; -- Permission denied +ERROR: permission denied for table priv_test_parent_tbl +SELECT * FROM tststats.priv_test_parent_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied +ERROR: permission denied for table priv_test_parent_tbl +DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied +ERROR: permission denied for table priv_test_parent_tbl +-- Grant table access to parent, but hide all data with RLS +RESET SESSION AUTHORIZATION; +ALTER TABLE tststats.priv_test_parent_tbl ENABLE ROW LEVEL SECURITY; +CREATE POLICY priv_test_parent_tbl_pol ON tststats.priv_test_parent_tbl USING (2 * a < 0); +GRANT SELECT, DELETE ON tststats.priv_test_parent_tbl TO regress_stats_user1; +-- Should now have direct table access to parent, but see nothing and leak nothing +SET SESSION AUTHORIZATION regress_stats_user1; +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak + a | b +---+--- +(0 rows) + +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 OR b <<< 0; -- Should not leak + a | b +---+--- +(0 rows) + +SELECT * FROM tststats.priv_test_parent_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak + a | b +---+--- +(0 rows) + +DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak -- privilege checks for pg_stats_ext and pg_stats_ext_exprs RESET SESSION AUTHORIZATION; CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT); @@ -3358,15 +3528,55 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x s_expr | {1} (2 rows) +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT, c INT GENERATED ALWAYS AS (b * 2) STORED); +CREATE SCHEMA sts_sch2; +GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch2 +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch2 +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +CREATE STATISTICS sts_sch2.pass1 ON a, b, c FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass2 ON a, b, c FROM sts_sch1.tbl; +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; +ALTER TABLE sts_sch1.tbl ALTER COLUMN c SET EXPRESSION AS (a * 3); -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); +DROP OPERATOR <<< (record, record); +DROP FUNCTION op_leak(record, record); RESET SESSION AUTHORIZATION; DROP TABLE stats_ext_tbl; DROP SCHEMA tststats CASCADE; -NOTICE: drop cascades to 2 other objects -DETAIL: drop cascades to table tststats.priv_test_tbl +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table tststats.priv_test_parent_tbl +drop cascades to table tststats.priv_test_tbl drop cascades to view tststats.priv_test_view +DROP SCHEMA sts_sch1, sts_sch2 CASCADE; +NOTICE: drop cascades to table sts_sch1.tbl DROP USER regress_stats_user1; CREATE TABLE grouping_unique (x integer); INSERT INTO grouping_unique (x) SELECT gs FROM generate_series(1,1000) AS gs; @@ -3455,4 +3665,47 @@ SELECT FROM sb_1 LEFT JOIN sb_2 RESET enable_nestloop; RESET enable_mergejoin; +-- Check that we can use statistics on a bool-valued function. +SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE numeric_lt(y, 1.0)'); + estimated | actual +-----------+-------- + 3333 | 196 +(1 row) + +CREATE STATISTICS extstat_sb_2_small ON numeric_lt(y, 1.0) FROM sb_2; +ANALYZE sb_2; +SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE numeric_lt(y, 1.0)'); + estimated | actual +-----------+-------- + 196 | 196 +(1 row) + +-- Tidy up DROP TABLE sb_1, sb_2 CASCADE; +-- Check statistics generated for range type and expressions. +CREATE TABLE stats_ext_tbl_range(name text, irange int4range); +INSERT INTO stats_ext_tbl_range VALUES + ('red', '[1,7)'::int4range), + ('blue', '[2,8]'::int4range), + ('green', '[3,9)'::int4range); +CREATE STATISTICS stats_ext_range (mcv) + ON irange, (irange + '[4,10)'::int4range) + FROM stats_ext_tbl_range; +ANALYZE stats_ext_tbl_range; +SELECT attnames, most_common_vals + FROM pg_stats_ext + WHERE statistics_name = 'stats_ext_range'; + attnames | most_common_vals +----------+------------------------------------------------------------ + {irange} | {{"[1,7)","[1,10)"},{"[2,9)","[2,10)"},{"[3,9)","[3,10)"}} +(1 row) + +SELECT range_length_histogram, range_empty_frac, range_bounds_histogram + FROM pg_stats_ext_exprs + WHERE statistics_name = 'stats_ext_range'; + range_length_histogram | range_empty_frac | range_bounds_histogram +------------------------+------------------+------------------------------ + {7,8,9} | 0 | {"[1,10)","[2,10)","[3,10)"} +(1 row) + +DROP TABLE stats_ext_tbl_range; diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 48d6392b4ad4f..fd660791ea9c4 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -1,4 +1,170 @@ CREATE SCHEMA stats_import; +-- +-- Convenience view for columns of pg_stats that are stable across test runs. +-- +CREATE VIEW stats_import.pg_stats_stable AS + SELECT schemaname, tablename, attname, inherited, null_frac, avg_width, + n_distinct, most_common_vals::text as most_common_vals, + most_common_freqs, histogram_bounds::text AS histogram_bounds, + correlation, most_common_elems::text AS most_common_elems, + most_common_elem_freqs, elem_count_histogram, + range_length_histogram::text AS range_length_histogram, range_empty_frac, + range_bounds_histogram::text AS range_bounds_histogram + FROM pg_stats; +-- +-- Setup functions for set-difference convenience functions +-- +-- Test to detect any new columns added to pg_statistic. If any columns +-- are added, we may need to update pg_statistic_flat() and the facilities +-- we are testing. +SELECT COUNT(*) FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_statistic'::regclass AND + attnum > 0; + count +------- + 31 +(1 row) + +-- Create a view that is used purely for the type based on pg_statistic. +CREATE VIEW stats_import.pg_statistic_flat_t AS + SELECT + a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, + s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, + s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, + s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, + s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, + s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, + s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, + s.stavalues5::text AS sv5 + FROM pg_statistic s + JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum + WHERE FALSE; +-- Function to retrieve data used for diff comparisons between two +-- relations based on the contents of pg_statistic. +CREATE FUNCTION stats_import.pg_statistic_flat(p_relname text) +RETURNS SETOF stats_import.pg_statistic_flat_t +BEGIN ATOMIC + SELECT a.attname, s.stainherit, s.stanullfrac, s.stawidth, + s.stadistinct, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, + s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, s.stacoll1, s.stacoll2, + s.stacoll3, s.stacoll4, s.stacoll5, s.stanumbers1, s.stanumbers2, + s.stanumbers3, s.stanumbers4, s.stanumbers5, s.stavalues1::text, + s.stavalues2::text, s.stavalues3::text, + s.stavalues4::text, s.stavalues5::text + FROM pg_statistic s + JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum + JOIN pg_class c ON c.oid = a.attrelid + WHERE c.relnamespace = 'stats_import'::regnamespace + AND c.relname = p_relname; +END; +-- Comparison function for pg_statistic. The two relations defined by +-- the function caller are compared. +CREATE FUNCTION stats_import.pg_statistic_get_difference(a text, b text) +RETURNS TABLE (relname text, stats stats_import.pg_statistic_flat_t) +BEGIN ATOMIC + WITH aset AS (SELECT * FROM stats_import.pg_statistic_flat(a)), + bset AS (SELECT * FROM stats_import.pg_statistic_flat(b)) + SELECT a AS relname, a_minus_b::stats_import.pg_statistic_flat_t + FROM (TABLE aset EXCEPT TABLE bset) AS a_minus_b + UNION ALL + SELECT b AS relname, b_minus_a::stats_import.pg_statistic_flat_t + FROM (TABLE bset EXCEPT TABLE aset) AS b_minus_a; +END; +-- Test to detect any new columns added to pg_stats_ext. If any columns +-- are added, we may need to update pg_stats_ext_flat() and the facilities +-- we are testing. +SELECT COUNT(*) FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_stats_ext'::regclass AND + attnum > 0; + count +------- + 17 +(1 row) + +-- Create a view that is used purely for the type based on pg_stats_ext. +CREATE VIEW stats_import.pg_stats_ext_flat_t AS + SELECT inherited, n_distinct, dependencies, most_common_vals, + most_common_freqs, most_common_base_freqs + FROM pg_stats_ext + WHERE FALSE; +-- Function to retrieve data used for diff comparisons between two +-- relations based on the contents of pg_stats_ext. +CREATE FUNCTION stats_import.pg_stats_ext_flat(p_statname text) +RETURNS SETOF stats_import.pg_stats_ext_flat_t +BEGIN ATOMIC + SELECT inherited, n_distinct, dependencies, most_common_vals, + most_common_freqs, most_common_base_freqs + FROM pg_stats_ext + WHERE statistics_schemaname = 'stats_import' + AND statistics_name = p_statname; +END; +-- Comparison function for pg_stats_ext. The two relations defined by +-- the function caller are compared. +CREATE FUNCTION stats_import.pg_stats_ext_get_difference(a text, b text) +RETURNS TABLE (statname text, stats stats_import.pg_stats_ext_flat_t) +BEGIN ATOMIC + WITH aset AS (SELECT * FROM stats_import.pg_stats_ext_flat(a)), + bset AS (SELECT * FROM stats_import.pg_stats_ext_flat(b)) + SELECT a AS relname, a_minus_b::stats_import.pg_stats_ext_flat_t + FROM (TABLE aset EXCEPT TABLE bset) AS a_minus_b + UNION ALL + SELECT b AS relname, b_minus_a::stats_import.pg_stats_ext_flat_t + FROM (TABLE bset EXCEPT TABLE aset) AS b_minus_a; +END; +-- Test to detect any new columns added to pg_stats_ext_exprs. If any columns +-- are added, we may need to update pg_stats_ext_exprs_flat() and the facilities +-- we are testing. +SELECT COUNT(*) FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_stats_ext_exprs'::regclass AND + attnum > 0; + count +------- + 22 +(1 row) + +-- Create a view that is used purely for the type based on pg_stats_ext_exprs. +CREATE VIEW stats_import.pg_stats_ext_exprs_flat_t AS + SELECT inherited, null_frac, avg_width, n_distinct, + most_common_vals::text AS most_common_vals, + most_common_freqs, histogram_bounds::text AS histogram_bounds, + correlation, most_common_elems::text AS most_common_elems, + most_common_elem_freqs, elem_count_histogram, + range_length_histogram::text AS range_length_histogram, + range_empty_frac, range_bounds_histogram::text AS range_bounds_histogram + FROM pg_stats_ext_exprs AS n + WHERE FALSE; +-- Function to retrieve data used for diff comparisons between two +-- relations based on the contents of pg_stats_ext_exprs. +CREATE FUNCTION stats_import.pg_stats_ext_exprs_flat(p_statname text) +RETURNS SETOF stats_import.pg_stats_ext_exprs_flat_t +BEGIN ATOMIC + SELECT inherited, null_frac, avg_width, n_distinct, + most_common_vals::text AS most_common_vals, + most_common_freqs, histogram_bounds::text AS histogram_bounds, + correlation, most_common_elems::text AS most_common_elems, + most_common_elem_freqs, elem_count_histogram, + range_length_histogram::text AS range_length_histogram, + range_empty_frac, range_bounds_histogram::text AS range_bounds_histogram + FROM pg_stats_ext_exprs AS n + WHERE n.statistics_schemaname = 'stats_import' AND + n.statistics_name = p_statname; +END; +-- Comparison function for pg_stats_ext_exprs. The two relations defined by +-- the function caller are compared. +CREATE FUNCTION stats_import.pg_stats_ext_exprs_get_difference(a text, b text) +RETURNS TABLE (statname text, stats stats_import.pg_stats_ext_exprs_flat_t) +BEGIN ATOMIC + WITH aset AS (SELECT * FROM stats_import.pg_stats_ext_exprs_flat(a)), + bset AS (SELECT * FROM stats_import.pg_stats_ext_exprs_flat(b)) + SELECT a AS relname, a_minus_b::stats_import.pg_stats_ext_exprs_flat_t + FROM (TABLE aset EXCEPT TABLE bset) AS a_minus_b + UNION ALL + SELECT b AS relname, b_minus_a::stats_import.pg_stats_ext_exprs_flat_t + FROM (TABLE bset EXCEPT TABLE aset) AS b_minus_a; +END; +-- +-- Schema setup. +-- CREATE TYPE stats_import.complex_type AS ( a integer, b real, @@ -12,6 +178,11 @@ CREATE TABLE stats_import.test( arange int4range, tags text[] ) WITH (autovacuum_enabled = false); +CREATE TABLE stats_import.test_mr( + id INTEGER PRIMARY KEY, + name text, + mrange int4multirange +) WITH (autovacuum_enabled = false); SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 'stats_import', @@ -19,7 +190,7 @@ SELECT 'relpages', 18::integer, 'reltuples', 21::real, 'relallvisible', 24::integer, - 'relallfrozen', 27::integer); + 'relallfrozen', 27::integer); pg_restore_relation_stats --------------------------- t @@ -50,26 +221,26 @@ SELECT pg_clear_relation_stats('stats_import', 'test'); SELECT pg_catalog.pg_restore_relation_stats( 'relname', 'test', 'relpages', 17::integer); -ERROR: "schemaname" cannot be NULL +ERROR: argument "schemaname" must not be null -- error: relname missing SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 'stats_import', 'relpages', 17::integer); -ERROR: "relname" cannot be NULL +ERROR: argument "relname" must not be null --- error: schemaname is wrong type SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 3.6::float, 'relname', 'test', 'relpages', 17::integer); -WARNING: argument "schemaname" has type "double precision", expected type "text" -ERROR: "schemaname" cannot be NULL +WARNING: argument "schemaname" has type double precision, expected type text +ERROR: argument "schemaname" must not be null --- error: relname is wrong type SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 'stats_import', 'relname', 0::oid, 'relpages', 17::integer); -WARNING: argument "relname" has type "oid", expected type "text" -ERROR: "relname" cannot be NULL +WARNING: argument "relname" has type oid, expected type text +ERROR: argument "relname" must not be null -- error: relation not found SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 'stats_import', @@ -88,7 +259,7 @@ SELECT pg_restore_relation_stats( 'schemaname', 'stats_import', 'relname', 'test', NULL, '17'::integer); -ERROR: name at variadic position 5 is NULL +ERROR: name at variadic position 5 is null -- starting stats SELECT relpages, reltuples, relallvisible, relallfrozen FROM pg_class @@ -120,9 +291,9 @@ WHERE relation = 'stats_import.test'::regclass AND SELECT mode FROM pg_locks WHERE relation = 'stats_import.test_i'::regclass AND pid = pg_backend_pid() AND granted; - mode ------------------ - AccessShareLock + mode +-------------------------- + ShareUpdateExclusiveLock (1 row) COMMIT; @@ -132,8 +303,36 @@ CREATE TABLE stats_import.part_child_1 PARTITION OF stats_import.part_parent FOR VALUES FROM (0) TO (10) WITH (autovacuum_enabled = false); +-- This ensures the presence of extended statistics marked with +-- inherited = true. +CREATE STATISTICS stats_import.part_parent_stat + ON i, (i % 2) + FROM stats_import.part_parent; CREATE INDEX part_parent_i ON stats_import.part_parent(i); +INSERT INTO stats_import.part_parent +SELECT g.g +FROM generate_series(0,9) AS g(g); +SELECT COUNT(*) FROM stats_import.part_parent; + count +------- + 10 +(1 row) + +SELECT COUNT(*) FROM stats_import.part_child_1; + count +------- + 10 +(1 row) + ANALYZE stats_import.part_parent; +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + count | inherited +-------+----------- + 1 | t +(1 row) + SELECT relpages FROM pg_class WHERE oid = 'stats_import.part_parent'::regclass; @@ -286,7 +485,7 @@ SELECT pg_restore_relation_stats( 'reltuples', 400.0::real, 'relallvisible', 4::integer, 'relallfrozen', 3::integer); -WARNING: argument "relpages" has type "text", expected type "integer" +WARNING: argument "relpages" has type text, expected type integer pg_restore_relation_stats --------------------------- f @@ -358,7 +557,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'attname', 'id', 'inherited', false::boolean, 'null_frac', 0.1::real); -ERROR: "schemaname" cannot be NULL +ERROR: argument "schemaname" must not be null -- error: schema does not exist SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'nope', @@ -373,7 +572,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'attname', 'id', 'inherited', false::boolean, 'null_frac', 0.1::real); -ERROR: "relname" cannot be NULL +ERROR: argument "relname" must not be null -- error: relname does not exist SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -389,7 +588,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'attname', 'id', 'inherited', false::boolean, 'null_frac', 0.1::real); -ERROR: "relname" cannot be NULL +ERROR: argument "relname" must not be null -- error: NULL attname SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -397,7 +596,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'attname', NULL, 'inherited', false::boolean, 'null_frac', 0.1::real); -ERROR: must specify either attname or attnum +ERROR: must specify either "attname" or "attnum" -- error: attname doesn't exist SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -416,14 +615,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'attnum', 1::smallint, 'inherited', false::boolean, 'null_frac', 0.1::real); -ERROR: cannot specify both attname and attnum +ERROR: cannot specify both "attname" and "attnum" -- error: neither attname nor attnum SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', 'relname', 'test', 'inherited', false::boolean, 'null_frac', 0.1::real); -ERROR: must specify either attname or attnum +ERROR: must specify either "attname" or "attnum" -- error: attribute is system column SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -439,7 +638,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'attname', 'id', 'inherited', NULL::boolean, 'null_frac', 0.1::real); -ERROR: "inherited" cannot be NULL +ERROR: argument "inherited" must not be null -- ok: just the fixed values, with version, no stakinds SELECT pg_catalog.pg_restore_attribute_stats( 'schemaname', 'stats_import', @@ -456,7 +655,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -483,7 +682,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -508,7 +707,7 @@ WARNING: unrecognized argument name: "nope" (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -527,14 +726,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.21::real, 'most_common_freqs', '{0.1,0.2,0.3}'::real[] ); -WARNING: "most_common_vals" must be specified when "most_common_freqs" is specified +WARNING: argument "most_common_vals" must be specified when argument "most_common_freqs" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -553,14 +752,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.21::real, 'most_common_vals', '{1,2,3}'::text ); -WARNING: "most_common_freqs" must be specified when "most_common_vals" is specified +WARNING: argument "most_common_freqs" must be specified when argument "most_common_vals" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -580,15 +779,15 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'most_common_vals', '{2,1,3}'::text, 'most_common_freqs', '{0.2,0.1}'::double precision[] ); -WARNING: argument "most_common_freqs" has type "double precision[]", expected type "real[]" -WARNING: "most_common_freqs" must be specified when "most_common_vals" is specified +WARNING: argument "most_common_freqs" has type double precision[], expected type real[] +WARNING: argument "most_common_freqs" must be specified when argument "most_common_vals" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -615,7 +814,7 @@ WARNING: invalid input syntax for type integer: "four" (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -640,7 +839,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -659,14 +858,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.24::real, 'histogram_bounds', '{1,NULL,3,4}'::text ); -WARNING: "histogram_bounds" array cannot contain NULL values +WARNING: "histogram_bounds" array must not contain null values pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -690,7 +889,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -709,14 +908,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.25::real, 'elem_count_histogram', '{1,1,NULL,1,1,1,1,1}'::real[] ); -WARNING: "elem_count_histogram" array cannot contain NULL values +WARNING: argument "elem_count_histogram" array must not contain null values pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -741,7 +940,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -761,7 +960,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'range_empty_frac', 0.5::real, 'range_length_histogram', '{399,499,Infinity}'::text ); -WARNING: attribute "id" is not a range type +WARNING: column "id" is not a range type DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM. pg_restore_attribute_stats ---------------------------- @@ -769,7 +968,7 @@ DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUN (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -788,14 +987,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.28::real, 'range_length_histogram', '{399,499,Infinity}'::text ); -WARNING: "range_empty_frac" must be specified when "range_length_histogram" is specified +WARNING: argument "range_empty_frac" must be specified when argument "range_length_histogram" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -814,14 +1013,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.29::real, 'range_empty_frac', 0.5::real ); -WARNING: "range_length_histogram" must be specified when "range_empty_frac" is specified +WARNING: argument "range_length_histogram" must be specified when argument "range_empty_frac" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -846,7 +1045,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -865,7 +1064,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.31::real, 'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text ); -WARNING: attribute "id" is not a range type +WARNING: column "id" is not a range type DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM. pg_restore_attribute_stats ---------------------------- @@ -873,7 +1072,7 @@ DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUN (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -897,7 +1096,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -917,7 +1116,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'most_common_elems', '{3,1}'::text, 'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[] ); -WARNING: unable to determine element type of attribute "arange" +WARNING: could not determine element type of column "arange" DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST. pg_restore_attribute_stats ---------------------------- @@ -925,7 +1124,7 @@ DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST. (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -945,7 +1144,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'most_common_elems', '{1,3}'::text, 'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[] ); -WARNING: unable to determine element type of attribute "id" +WARNING: could not determine element type of column "id" DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST. pg_restore_attribute_stats ---------------------------- @@ -953,7 +1152,7 @@ DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST. (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -972,14 +1171,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.34::real, 'most_common_elems', '{one,two}'::text ); -WARNING: "most_common_elem_freqs" must be specified when "most_common_elems" is specified +WARNING: argument "most_common_elem_freqs" must be specified when argument "most_common_elems" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -998,14 +1197,14 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.35::real, 'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[] ); -WARNING: "most_common_elems" must be specified when "most_common_elem_freqs" is specified +WARNING: argument "most_common_elems" must be specified when argument "most_common_elem_freqs" is specified pg_restore_attribute_stats ---------------------------- f (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -1030,7 +1229,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -1049,7 +1248,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.36::real, 'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1}'::real[] ); -WARNING: unable to determine element type of attribute "id" +WARNING: could not determine element type of column "id" DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST. pg_restore_attribute_stats ---------------------------- @@ -1057,7 +1256,7 @@ DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST. (1 row) SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -1067,6 +1266,27 @@ AND attname = 'id'; stats_import | test | id | f | 0.36 | 5 | 0.6 | {2,1,3} | {0.3,0.25,0.05} | {1,2,3,4} | | | | | | | (1 row) +-- test for multiranges +INSERT INTO stats_import.test_mr +VALUES + (1, 'red', '{[1,3),[5,9),[20,30)}'::int4multirange), + (2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange), + (3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange); +-- ensure that we set attribute stats for a multirange +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'attname', 'mrange', + 'inherited', false, + 'range_length_histogram', '{19,29,109}'::text, + 'range_empty_frac', '0'::real, + 'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text +); + pg_restore_attribute_stats +---------------------------- + t +(1 row) + -- -- Test the ability to exactly copy data from one table to an identical table, -- correctly reconstructing the stakind order as well as the staopN and @@ -1084,11 +1304,35 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, UNION ALL SELECT 4, 'four', NULL, int4range(0,100), NULL; CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1)); +CREATE STATISTICS stats_import.test_stat + ON name, comp, lower(arange), array_length(tags,1) + FROM stats_import.test; +CREATE STATISTICS stats_import.test_stat_ndistinct (ndistinct) + ON name, comp + FROM stats_import.test; +CREATE STATISTICS stats_import.test_stat_dependencies (dependencies) + ON name, comp + FROM stats_import.test; +CREATE STATISTICS stats_import.test_stat_mcv (mcv) + ON name, comp + FROM stats_import.test; +CREATE STATISTICS stats_import.test_stat_ndistinct_exprs (ndistinct) + ON lower(name), upper(name) + FROM stats_import.test; +CREATE STATISTICS stats_import.test_stat_dependencies_exprs (dependencies) + ON lower(name), upper(name) + FROM stats_import.test; +CREATE STATISTICS stats_import.test_stat_mcv_exprs (mcv) + ON lower(name), upper(name) + FROM stats_import.test; -- Generate statistics on table with data ANALYZE stats_import.test; CREATE TABLE stats_import.test_clone ( LIKE stats_import.test ) WITH (autovacuum_enabled = false); CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1)); +CREATE STATISTICS stats_import.test_stat_clone + ON name, comp, lower(arange), array_length(tags,1) + FROM stats_import.test_clone; -- -- Copy stats from test to test_clone, and is_odd to is_odd_clone -- @@ -1142,124 +1386,14 @@ ORDER BY c.relname; test_clone | 5 (4 rows) --- check test minus test_clone -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test_clone'::regclass; - attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction ----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+----------- -(0 rows) - --- check test_clone minus test -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test_clone'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test'::regclass; - attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction ----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+----------- -(0 rows) - --- check is_odd minus is_odd_clone -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd_clone'::regclass; - attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction ----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+----------- +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('test', 'test_clone') +\gx (0 rows) --- check is_odd_clone minus is_odd -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd_clone'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd'::regclass; - attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction ----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+----------- +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('is_odd', 'is_odd_clone') +\gx (0 rows) -- attribute stats exist before a clear, but not after @@ -1342,11 +1476,1921 @@ AND attname = 'i'; (1 row) DROP TABLE stats_temp; +-- Tests for pg_clear_extended_stats(). +-- Invalid argument values. +SELECT pg_clear_extended_stats(schemaname => NULL, + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => false); +ERROR: argument "schemaname" must not be null +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => NULL, + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => false); +ERROR: argument "relname" must not be null +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => NULL, + statistics_name => 'stat_bar', + inherited => false); +ERROR: argument "statistics_schemaname" must not be null +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => NULL, + inherited => false); +ERROR: argument "statistics_name" must not be null +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => NULL); +ERROR: argument "inherited" must not be null +-- Missing objects +SELECT pg_clear_extended_stats(schemaname => 'schema_not_exist', + relname => 'test', + statistics_schemaname => 'schema_not_exist', + statistics_name => 'test_stat', + inherited => false); +ERROR: schema "schema_not_exist" does not exist +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'table_not_exist', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +ERROR: relation "stats_import.table_not_exist" does not exist +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'schema_not_exist', + statistics_name => 'test_stat', + inherited => false); +WARNING: could not find schema "schema_not_exist" + pg_clear_extended_stats +------------------------- + +(1 row) + +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'ext_stats_not_exist', + inherited => false); +WARNING: could not find extended statistics object "stats_import.ext_stats_not_exist" + pg_clear_extended_stats +------------------------- + +(1 row) + +-- Incorrect relation/extended stats combination +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat_clone', + inherited => false); +WARNING: could not clear extended statistics object "stats_import.test_stat_clone": incorrect relation "stats_import.test" specified + pg_clear_extended_stats +------------------------- + +(1 row) + +-- Check that records are removed after a valid clear call. +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; + count | inherited +-------+----------- + 1 | f +(1 row) + +SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; + count | inherited +-------+----------- + 2 | f +(1 row) + +BEGIN; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); + pg_clear_extended_stats +------------------------- + +(1 row) + +SELECT mode FROM pg_locks WHERE locktype = 'relation' AND + relation = 'stats_import.test'::regclass AND + pid = pg_backend_pid(); + mode +-------------------------- + ShareUpdateExclusiveLock +(1 row) + +COMMIT; +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; + count | inherited +-------+----------- +(0 rows) + +SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; + count | inherited +-------+----------- + 2 | +(1 row) + +-- And before/after on inherited stats +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + count | inherited +-------+----------- + 1 | t +(1 row) + +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'part_parent', + statistics_schemaname => 'stats_import', + statistics_name => 'part_parent_stat', + inherited => true); + pg_clear_extended_stats +------------------------- + +(1 row) + +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + count | inherited +-------+----------- +(0 rows) + +-- Check that MAINTAIN is required when clearing statistics. +CREATE ROLE regress_test_extstat_clear; +GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_clear; +SET ROLE regress_test_extstat_clear; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +ERROR: permission denied for table test +RESET ROLE; +GRANT MAINTAIN ON stats_import.test TO regress_test_extstat_clear; +SET ROLE regress_test_extstat_clear; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); + pg_clear_extended_stats +------------------------- + +(1 row) + +RESET ROLE; +REVOKE MAINTAIN ON stats_import.test FROM regress_test_extstat_clear; +REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_clear; +DROP ROLE regress_test_extstat_clear; +-- Tests for pg_restore_extended_stats(). +-- Invalid argument values. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', NULL, + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +ERROR: argument "schemaname" must not be null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', NULL, + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +ERROR: argument "relname" must not be null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', NULL, + 'statistics_name', 'test_stat_clone', + 'inherited', false); +ERROR: argument "statistics_schemaname" must not be null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', NULL, + 'inherited', false); +ERROR: argument "statistics_name" must not be null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', NULL); +ERROR: argument "inherited" must not be null +-- Missing objects +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'schema_not_exist', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +ERROR: schema "schema_not_exist" does not exist +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'table_not_exist', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +ERROR: relation "stats_import.table_not_exist" does not exist +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'schema_not_exist', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +WARNING: could not find schema "schema_not_exist" + pg_restore_extended_stats +--------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'ext_stats_not_exist', + 'inherited', false); +WARNING: could not find extended statistics object "stats_import.ext_stats_not_exist" + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Incorrect relation/extended stats combination +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +WARNING: could not restore extended statistics object "stats_import.test_stat_clone": incorrect relation "stats_import.test" specified + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Check that MAINTAIN is required when restoring statistics. +CREATE ROLE regress_test_extstat_restore; +GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_restore; +SET ROLE regress_test_extstat_restore; +-- No data to restore; this fails on a permission failure. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +ERROR: permission denied for table test_clone +RESET ROLE; +GRANT MAINTAIN ON stats_import.test_clone TO regress_test_extstat_restore; +SET ROLE regress_test_extstat_restore; +-- This works, check the lock on the relation while on it. +BEGIN; +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT mode FROM pg_locks WHERE locktype = 'relation' AND + relation = 'stats_import.test_clone'::regclass AND + pid = pg_backend_pid(); + mode +-------------------------- + ShareUpdateExclusiveLock +(1 row) + +COMMIT; +RESET ROLE; +REVOKE MAINTAIN ON stats_import.test_clone FROM regress_test_extstat_restore; +REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_restore; +DROP ROLE regress_test_extstat_restore; +-- ndistinct value doesn't match object definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct); +WARNING: could not validate "pg_ndistinct" object: invalid attribute number 1 found + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Incorrect extended stats kind, ndistinct not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct); +WARNING: cannot specify parameter "n_distinct" +HINT: Extended statistics object "stats_import.test_stat_dependencies" does not support statistics of this type. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Incorrect extended stats kind, dependencies not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 2, "degree": 1.000000}]'::pg_dependencies); +WARNING: cannot specify parameter "dependencies" +HINT: Extended statistics object "stats_import.test_stat_ndistinct" does not support statistics of this type. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: ndistinct +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +-- dependencies value doesn't match definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'dependencies', '[{"attributes": [1], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 1, "degree": 1.000000}]'::pg_dependencies); +WARNING: could not validate "pg_dependencies" object: invalid attribute number 1 found + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: dependencies +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 2, "degree": 1.000000}]'::pg_dependencies); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +-- ndistinct with expressions, invalid attributes. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct_exprs', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,-1], "ndistinct" : 4}]'::pg_ndistinct); +WARNING: could not validate "pg_ndistinct" object: invalid attribute number 1 found + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: ndistinct with expressions. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct_exprs', + 'inherited', false, + 'n_distinct', '[{"attributes" : [-1,-2], "ndistinct" : 4}]'::pg_ndistinct); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +-- dependencies with expressions, invalid attributes. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies_exprs', + 'inherited', false, + 'dependencies', '[{"attributes": [-1], "dependency": 1, "degree": 1.000000}, + {"attributes": [1], "dependency": -1, "degree": 1.000000}]'::pg_dependencies); +WARNING: could not validate "pg_dependencies" object: invalid attribute number 1 found + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: dependencies with expressions +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies_exprs', + 'inherited', false, + 'dependencies', '[{"attributes": [-1], "dependency": -2, "degree": 1.000000}, + {"attributes": [-2], "dependency": -1, "degree": 1.000000}]'::pg_dependencies); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +-- ok: MCV with expressions +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv_exprs', + 'inherited', false, + 'most_common_vals', '{{four,FOUR},{one,NULL},{NULL,TRE},{two,TWO}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.99}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.023,0.087}'::double precision[]); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +-- Check the presence of the restored stats, for each object. +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_ndistinct' AND + e.inherited = false; + n_distinct +------------------------------------------ + [{"attributes": [2, 3], "ndistinct": 4}] +(1 row) + +SELECT replace(e.dependencies, '}, ', E'},\n') AS dependencies +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_dependencies' AND + e.inherited = false; + dependencies +------------------------------------------------------------ + [{"attributes": [2], "dependency": 3, "degree": 1.000000},+ + {"attributes": [3], "dependency": 2, "degree": 1.000000}] +(1 row) + +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_ndistinct_exprs' AND + e.inherited = false; + n_distinct +-------------------------------------------- + [{"attributes": [-1, -2], "ndistinct": 4}] +(1 row) + +SELECT replace(e.dependencies, '}, ', E'},\n') AS dependencies +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_dependencies_exprs' AND + e.inherited = false; + dependencies +-------------------------------------------------------------- + [{"attributes": [-1], "dependency": -2, "degree": 1.000000},+ + {"attributes": [-2], "dependency": -1, "degree": 1.000000}] +(1 row) + +SELECT e.most_common_vals, e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcv_exprs' AND + e.inherited = false \gx +-[ RECORD 1 ]----------+---------------------------------------------- +most_common_vals | {{four,FOUR},{one,NULL},{NULL,TRE},{two,TWO}} +most_common_val_nulls | {{f,f},{f,t},{t,f},{f,f}} +most_common_freqs | {0.25,0.25,0.25,0.99} +most_common_base_freqs | {0.0625,0.0625,0.023,0.087} + +-- Incorrect extended stats kind, mcv not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +WARNING: cannot specify parameters "most_common_vals", "most_common_freqs" or "most_common_base_freqs" +HINT: Extended statistics object "stats_import.test_stat_dependencies" does not support statistics of this type. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCV requires all three parameters +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +WARNING: could not use "most_common_vals", "most_common_freqs" and "most_common_base_freqs": missing one or more parameters + pg_restore_extended_stats +--------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +WARNING: could not use "most_common_vals", "most_common_freqs" and "most_common_base_freqs": missing one or more parameters + pg_restore_extended_stats +--------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]); +WARNING: could not use "most_common_vals", "most_common_freqs" and "most_common_base_freqs": missing one or more parameters + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- most_common_vals that is not 2-D +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{four,NULL}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +WARNING: could not parse array "most_common_vals": incorrect number of dimensions (2 required) + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- most_common_freqs with length not matching with most_common_vals. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +WARNING: could not parse array "most_common_freqs": incorrect number of elements (same as "most_common_vals" required) + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- most_common_base_freqs with length not matching most_common_vals. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625}'::double precision[]); +WARNING: could not parse array "most_common_base_freqs": incorrect number of elements (same as "most_common_vals" required) + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- mcv attributes not matching object definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL,0,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2}, + {tre,"(3,3.3,TRE,03-03-2003,)",-1,3}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]); +WARNING: could not parse array "most_common_vals": found 4 attributes but expected 2 + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: mcv +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT replace(e.most_common_vals::text, '},', E'},\n ') AS mcvs, + e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcv' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+------------------------------------------------------------------ +mcvs | {{four,NULL}, + + | {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"},+ + | {tre,"(3,3.3,TRE,03-03-2003,)"}, + + | {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}} +most_common_val_nulls | {{f,t},{f,f},{f,f},{f,f}} +most_common_freqs | {0.25,0.25,0.25,0.25} +most_common_base_freqs | {0.0625,0.0625,0.0625,0.0625} + +-- Check import of all kinds for multirange. +CREATE STATISTICS stats_import.test_mr_stat + ON name, mrange, ( mrange + '{[10000,10200)}'::int4multirange) + FROM stats_import.test_mr; +CREATE TABLE stats_import.test_mr_clone ( LIKE stats_import.test_mr ) + WITH (autovacuum_enabled = false); +CREATE STATISTICS stats_import.test_mr_stat_clone + ON name, mrange, ( mrange + '{[10000,10200)}'::int4multirange) + FROM stats_import.test_mr_clone; +-- Check for invalid value combinations for range types. +-- Only range_bounds_histogram (other two missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\",\"[11,10200)\"}"}]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram" must be all either strings or all nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Only range_length_histogram and range_empty_frac +-- (range_bounds_histogram missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{10179,10189}", "range_empty_frac": "0"}]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram" must be all either strings or all nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Only range_bounds_histogram and range_empty_frac +-- (range_length_histogram missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\"}", "range_empty_frac": "0"}]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram" must be all either strings or all nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Only range_bounds_histogram and range_length_histogram +-- (range_empty_frac missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\"}", "range_length_histogram": "{10179}"}]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram" must be all either strings or all nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: multirange stats +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'n_distinct', '[{"attributes": [2, 3], "ndistinct": 3}, + {"attributes": [2, -1], "ndistinct": 3}, + {"attributes": [3, -1], "ndistinct": 3}, + {"attributes": [2, 3, -1], "ndistinct": 3}]'::pg_catalog.pg_ndistinct, + 'dependencies', '[{"attributes": [3], "dependency": 2, "degree": 1.000000}, + {"attributes": [3], "dependency": -1, "degree": 1.000000}, + {"attributes": [-1], "dependency": 2, "degree": 1.000000}, + {"attributes": [-1], "dependency": 3, "degree": 1.000000}, + {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, + {"attributes": [2, -1], "dependency": 3, "degree": 1.000000}, + {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}]'::pg_catalog.pg_dependencies, + 'most_common_vals', '{{red,"{[1,3),[5,9),[20,30)}","{[1,3),[5,9),[20,30),[10000,10200)}"}, + {red,"{[11,13),[15,19),[20,30)}","{[11,13),[15,19),[20,30),[10000,10200)}"}, + {red,"{[21,23),[25,29),[120,130)}","{[21,23),[25,29),[120,130),[10000,10200)}"}}'::text[], + 'most_common_freqs', '{0.3333333333333333,0.3333333333333333,0.3333333333333333}'::double precision[], + 'most_common_base_freqs', '{0.1111111111111111,0.1111111111111111,0.1111111111111111}'::double precision[], + 'exprs', '[{ "avg_width": "60", "null_frac": "0", "n_distinct": "-1", + "range_length_histogram": "{10179,10189,10199}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,10200)\",\"[11,10200)\",\"[21,10200)\"}" + }]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct, + replace(e.dependencies, '}, ', E'},\n') AS dependencies, + replace(e.most_common_vals::text, '},', E'},\n ') AS mcvs, + e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_mr_stat' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+---------------------------------------------------------------------------------- +n_distinct | [{"attributes": [2, 3], "ndistinct": 3}, + + | {"attributes": [2, -1], "ndistinct": 3}, + + | {"attributes": [3, -1], "ndistinct": 3}, + + | {"attributes": [2, 3, -1], "ndistinct": 3}] +dependencies | [{"attributes": [3], "dependency": 2, "degree": 1.000000}, + + | {"attributes": [3], "dependency": -1, "degree": 1.000000}, + + | {"attributes": [-1], "dependency": 2, "degree": 1.000000}, + + | {"attributes": [-1], "dependency": 3, "degree": 1.000000}, + + | {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, + + | {"attributes": [2, -1], "dependency": 3, "degree": 1.000000}, + + | {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}] +mcvs | {{red,"{[1,3),[5,9),[20,30)}","{[1,3),[5,9),[20,30),[10000,10200)}"}, + + | {red,"{[11,13),[15,19),[20,30)}","{[11,13),[15,19),[20,30),[10000,10200)}"}, + + | {red,"{[21,23),[25,29),[120,130)}","{[21,23),[25,29),[120,130),[10000,10200)}"}} +most_common_val_nulls | {{f,f,f},{f,f,f},{f,f,f}} +most_common_freqs | {0.3333333333333333,0.3333333333333333,0.3333333333333333} +most_common_base_freqs | {0.1111111111111111,0.1111111111111111,0.1111111111111111} + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram, + e.range_length_histogram, e.range_empty_frac, e.range_bounds_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_mr_stat' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+--------------------------------------------- +expr | (mrange + '{[10000,10200)}'::int4multirange) +null_frac | 0 +avg_width | 60 +n_distinct | -1 +most_common_vals | +most_common_freqs | +histogram_bounds | +correlation | +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | +range_length_histogram | {10179,10189,10199} +range_empty_frac | 0 +range_bounds_histogram | {"[1,10200)","[11,10200)","[21,10200)"} + +-- Incorrect extended stats kind, exprs not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'exprs', '[ { "avg_width": "4" } ]'::jsonb); +WARNING: cannot specify parameter "exprs" +HINT: Extended statistics object "stats_import.test_stat_ndistinct" does not support statistics of this type. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- Invalid exprs, not an array +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '{ "avg_width": "4", "null_frac": "0" }'::jsonb); +WARNING: could not parse "exprs": root-level array required + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- wrong number of exprs +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "avg_width": "4" } ]'::jsonb); +WARNING: could not parse "exprs": incorrect number of elements (1 required) + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- incorrect type of value: should be a string or a NULL. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "null_frac": 1 }, + { "null_frac": "0.25" } ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: Value of element "null_frac" must be type a null or a string. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs null_frac not a float +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "null_frac": "BADNULLFRAC" }, + { "null_frac": "0.25" } ]'::jsonb); +WARNING: invalid input syntax for type real: "BADNULLFRAC" +HINT: Element "null_frac" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs avg_width not an integer +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "avg_width": "BADAVGWIDTH" }, + { "avg_width": "4" } ]'::jsonb); +WARNING: invalid input syntax for type integer: "BADAVGWIDTH" +HINT: Element "avg_width" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs n_dinstinct not a float +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "n_distinct": "BADNDISTINCT" }, + { "n_distinct": "-0.5" } ]'::jsonb); +WARNING: invalid input syntax for type real: "BADNDISTINCT" +HINT: Element "n_distinct" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCV not null, MCF null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1}", "most_common_elems": null }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_vals" and "most_common_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCV not null, MCF missing +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_vals" and "most_common_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCV null, MCF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": null, "most_common_freqs": "{0.5}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_vals" and "most_common_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCV missing, MCF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_freqs": "{0.5}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_vals" and "most_common_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs most_common_vals element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{BADMCV}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); +WARNING: invalid input syntax for type integer: "BADMCV" +HINT: Element "most_common_vals" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs most_common_freqs element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1}", "most_common_freqs": "{BADMCF}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +WARNING: invalid input syntax for type real: "BADMCF" +HINT: Element "most_common_freqs" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs histogram wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "histogram_bounds": "{BADHIST,0}" }, + { "histogram_bounds": null } + ]'::jsonb); +WARNING: invalid input syntax for type integer: "BADHIST" +HINT: Element "histogram_bounds" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs correlation wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "correlation": "BADCORR" }, + { "correlation": "1" } + ]'::jsonb); +WARNING: invalid input syntax for type real: "BADCORR" +HINT: Element "correlation" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- invalid element type in array +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[1, null]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- invalid element in array, as valid jbvBinary. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[null, [{"avg_width" : [1]}]]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -2 + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- only range types can have range_length_histogram, range_empty_frac +-- or range_bounds_histogram. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "range_length_histogram": "{10179,10189,10199}", + "range_empty_frac": "0", + "range_bounds_histogram": "{10200,10200,10200}" + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid data in expression -2 +HINT: "range_length_histogram", "range_empty_frac", and "range_bounds_histogram" can only be set for a range type. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- only array types can have most_common_elems or elem_count_histogram. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "most_common_elems": "{1,2,3}", + "most_common_elem_freqs": "{0.3,0.3,0.4}" + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element type in expression -2 + pg_restore_extended_stats +--------------------------- + f +(1 row) + +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "elem_count_histogram": "{1,2,3,4,5}" + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element type in expression -2 + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: exprs first null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram, + e.range_length_histogram, e.range_empty_frac, e.range_bounds_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_clone' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+---------------------- +expr | lower(arange) +null_frac | +avg_width | +n_distinct | +most_common_vals | +most_common_freqs | +histogram_bounds | +correlation | +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | +range_length_histogram | +range_empty_frac | +range_bounds_histogram | +-[ RECORD 2 ]----------+---------------------- +expr | array_length(tags, 1) +null_frac | 0.25 +avg_width | 4 +n_distinct | -0.5 +most_common_vals | {2} +most_common_freqs | {0.5} +histogram_bounds | +correlation | 1 +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | +range_length_histogram | +range_empty_frac | +range_bounds_histogram | + +-- ok: exprs last null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{1}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + null + ]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_clone' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+---------------------- +expr | lower(arange) +null_frac | 0 +avg_width | 4 +n_distinct | -0.75 +most_common_vals | {1} +most_common_freqs | {0.5} +histogram_bounds | {-1,0} +correlation | -0.6 +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | +-[ RECORD 2 ]----------+---------------------- +expr | array_length(tags, 1) +null_frac | +avg_width | +n_distinct | +most_common_vals | +most_common_freqs | +histogram_bounds | +correlation | +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | + +-- ok: both exprs +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{1}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_clone' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+---------------------- +expr | lower(arange) +null_frac | 0 +avg_width | 4 +n_distinct | -0.75 +most_common_vals | {1} +most_common_freqs | {0.5} +histogram_bounds | {-1,0} +correlation | -0.6 +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | +-[ RECORD 2 ]----------+---------------------- +expr | array_length(tags, 1) +null_frac | 0.25 +avg_width | 4 +n_distinct | -0.5 +most_common_vals | {2} +most_common_freqs | {0.5} +histogram_bounds | +correlation | 1 +most_common_elems | +most_common_elem_freqs | +elem_count_histogram | + +-- A statistics object for testing MCELEM values in expressions +CREATE STATISTICS stats_import.test_stat_mcelem + ON name, (ARRAY[(comp).a, lower(arange)]) + FROM stats_import.test; +-- MCEV not null, MCEF null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,0,1,2,3}", + "most_common_elem_freqs": null + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_elems" and "most_common_elem_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCEV not null, MCEF missing +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,0,1,2,3}" + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_elems" and "most_common_elem_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCEV null, MCEF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": null, + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_elems" and "most_common_elem_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- MCEV missing, MCEF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +WARNING: could not parse "exprs": invalid element in expression -1 +HINT: "most_common_elems" and "most_common_elem_freqs" must be both either strings or nulls. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs most_common_elems element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,BADELEM,1,2,3}", + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +WARNING: invalid input syntax for type integer: "BADELEM" +HINT: Element "most_common_elems" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs most_common_elem_freqs element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,0,1,2,3}", + "most_common_elem_freqs": "{BADELEMFREQ,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +WARNING: invalid input syntax for type real: "BADELEMFREQ" +HINT: Element "most_common_elem_freqs" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- exprs histogram bounds element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "elem_count_histogram": "{BADELEMHIST,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5}" + } + ]'::jsonb); +WARNING: invalid input syntax for type real: "BADELEMHIST" +HINT: Element "elem_count_histogram" in expression -1 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: exprs mcelem +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "33", + "null_frac": "0", + "n_distinct": "-1", + "correlation": "1", + "histogram_bounds": "{\"{1,1}\",\"{2,1}\",\"{3,-1}\",\"{NULL,0}\"}", + "most_common_vals": null, + "most_common_elems": "{-1,0,1,2,3}", + "most_common_freqs": null, + "elem_count_histogram": "{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5}", + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcelem' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +expr | ARRAY[(comp).a, lower(arange)] +null_frac | 0 +avg_width | 33 +n_distinct | -1 +most_common_vals | +most_common_freqs | +histogram_bounds | {"{1,1}","{2,1}","{3,-1}","{NULL,0}"} +correlation | 1 +most_common_elems | {-1,0,1,2,3} +most_common_elem_freqs | {0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25} +elem_count_histogram | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5} + +-- ok, with warning: extra exprs param +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "33", + "null_frac": "0", + "n_distinct": "-1", + "correlation": "1", + "histogram_bounds": "{\"{1,1}\",\"{2,1}\",\"{3,-1}\",\"{NULL,0}\"}", + "most_common_vals": null, + "most_common_elems": "{-1,0,1,2,3}", + "most_common_freqs": null, + "elem_count_histogram": "{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5}", + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}", + "bad_param": "text no one will ever parse" + } + ]'::jsonb); +WARNING: could not import element in expression -1: invalid key name + pg_restore_extended_stats +--------------------------- + f +(1 row) + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcelem' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +expr | ARRAY[(comp).a, lower(arange)] +null_frac | 0 +avg_width | 33 +n_distinct | -1 +most_common_vals | +most_common_freqs | +histogram_bounds | {"{1,1}","{2,1}","{3,-1}","{NULL,0}"} +correlation | 1 +most_common_elems | {-1,0,1,2,3} +most_common_elem_freqs | {0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25} +elem_count_histogram | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5} + +-- ok: tsvector exceptions, test just the collation exceptions +CREATE STATISTICS stats_import.test_stat_tsvec ON (length(name)), (to_tsvector(name)) FROM stats_import.test; +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_tsvec', + 'inherited', false, + 'exprs', '[null, + { + "most_common_elems": "{one,tre,two,four}", + "most_common_elem_freqs": "{0.25,0.25,0.25,0.25,0.25,0.25}" + } + ]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.most_common_elems, e.most_common_elem_freqs +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_tsvec' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+-------------------------------- +expr | length(name) +most_common_elems | +most_common_elem_freqs | +-[ RECORD 2 ]----------+-------------------------------- +expr | to_tsvector(name) +most_common_elems | {one,tre,two,four} +most_common_elem_freqs | {0.25,0.25,0.25,0.25,0.25,0.25} + +-- Test the ability of pg_restore_extended_stats() to import all of the +-- statistic values from an extended statistic object that has been +-- populated via a regular ANALYZE. This checks after the statistics +-- kinds supported by pg_restore_extended_stats(). +-- +-- Note: Keep this test at the bottom of the file, so as the amount of +-- statistics data handled is maximized. +ANALYZE stats_import.test; +-- Copy stats from test_stat to test_stat_clone +SELECT e.statistics_name, + pg_catalog.pg_restore_extended_stats( + 'schemaname', e.statistics_schemaname::text, + 'relname', 'test_clone', + 'statistics_schemaname', e.statistics_schemaname::text, + 'statistics_name', 'test_stat_clone', + 'inherited', e.inherited, + 'n_distinct', e.n_distinct, + 'dependencies', e.dependencies, + 'most_common_vals', e.most_common_vals, + 'most_common_freqs', e.most_common_freqs, + 'most_common_base_freqs', e.most_common_base_freqs, + 'exprs', x.exprs) +FROM pg_stats_ext AS e +CROSS JOIN LATERAL ( + SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object( + 'null_frac', ee.null_frac::text, + 'avg_width', ee.avg_width::text, + 'n_distinct', ee.n_distinct::text, + 'most_common_vals', ee.most_common_vals::text, + 'most_common_freqs', ee.most_common_freqs::text, + 'histogram_bounds', ee.histogram_bounds::text, + 'correlation', ee.correlation::text, + 'most_common_elems', ee.most_common_elems::text, + 'most_common_elem_freqs', ee.most_common_elem_freqs::text, + 'elem_count_histogram', ee.elem_count_histogram::text, + 'range_length_histogram', ee.range_length_histogram::text, + 'range_empty_frac', ee.range_empty_frac::text, + 'range_bounds_histogram', ee.range_bounds_histogram::text))) + FROM pg_stats_ext_exprs AS ee + WHERE ee.statistics_schemaname = e.statistics_schemaname AND + ee.statistics_name = e.statistics_name AND + ee.inherited = e.inherited + ) AS x(exprs) +WHERE e.statistics_schemaname = 'stats_import' +AND e.statistics_name = 'test_stat'; + statistics_name | pg_restore_extended_stats +-----------------+--------------------------- + test_stat | t +(1 row) + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_get_difference('test_stat', 'test_stat_clone') +\gx +(0 rows) + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_exprs_get_difference('test_stat', 'test_stat_clone') +\gx +(0 rows) + +ANALYZE stats_import.test_mr; +-- Copy stats from test_mr_stat to test_mr_stat_clone +SELECT e.statistics_name, + pg_catalog.pg_restore_extended_stats( + 'schemaname', e.statistics_schemaname::text, + 'relname', 'test_mr_clone', + 'statistics_schemaname', e.statistics_schemaname::text, + 'statistics_name', 'test_mr_stat_clone', + 'inherited', e.inherited, + 'n_distinct', e.n_distinct, + 'dependencies', e.dependencies, + 'most_common_vals', e.most_common_vals, + 'most_common_freqs', e.most_common_freqs, + 'most_common_base_freqs', e.most_common_base_freqs, + 'exprs', x.exprs) +FROM pg_stats_ext AS e +CROSS JOIN LATERAL ( + SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object( + 'null_frac', ee.null_frac::text, + 'avg_width', ee.avg_width::text, + 'n_distinct', ee.n_distinct::text, + 'most_common_vals', ee.most_common_vals::text, + 'most_common_freqs', ee.most_common_freqs::text, + 'histogram_bounds', ee.histogram_bounds::text, + 'correlation', ee.correlation::text, + 'most_common_elems', ee.most_common_elems::text, + 'most_common_elem_freqs', ee.most_common_elem_freqs::text, + 'elem_count_histogram', ee.elem_count_histogram::text, + 'range_length_histogram', ee.range_length_histogram::text, + 'range_empty_frac', ee.range_empty_frac::text, + 'range_bounds_histogram', ee.range_bounds_histogram::text))) + FROM pg_stats_ext_exprs AS ee + WHERE ee.statistics_schemaname = e.statistics_schemaname AND + ee.statistics_name = e.statistics_name AND + ee.inherited = e.inherited + ) AS x(exprs) +WHERE e.statistics_schemaname = 'stats_import' +AND e.statistics_name = 'test_mr_stat'; + statistics_name | pg_restore_extended_stats +-----------------+--------------------------- + test_mr_stat | t +(1 row) + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_get_difference('test_mr_stat', 'test_mr_stat_clone') +\gx +(0 rows) + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_exprs_get_difference('test_mr_stat', 'test_mr_stat_clone') +\gx +(0 rows) + +-- range_length_histogram, range_empty_frac, and range_bounds_histogram +-- have been added to pg_stats_ext_exprs in PostgreSQL 19. When dumping +-- expression statistics in a cluster with an older version, these fields +-- are dumped as NULL, pg_restore_extended_stats() authorizing the partial +-- restore state of the extended statistics data. This test emulates such +-- a case by calling pg_restore_extended_stats() with NULL values for all +-- the three range fields, then checks the statistics loaded with some +-- queries. +CREATE TABLE stats_import.test_range_expr_null( + id INTEGER PRIMARY KEY, + name TEXT, + rng int4range NOT NULL +); +INSERT INTO stats_import.test_range_expr_null + SELECT i, 'name_' || (i % 10), int4range(i, i + 10) + FROM generate_series(1, 100) i; +-- Create statistics with a range expression +CREATE STATISTICS stats_import.stat_range_expr_null + ON name, (rng * int4range(50, 150)) + FROM stats_import.test_range_expr_null; +ANALYZE stats_import.test_range_expr_null; +-- Verify range statistics were created +SELECT e.expr, + e.range_length_histogram IS NOT NULL AS has_range_len, + e.range_empty_frac IS NOT NULL AS has_range_empty, + e.range_bounds_histogram IS NOT NULL AS has_range_bounds +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' + AND e.statistics_name = 'stat_range_expr_null'; + expr | has_range_len | has_range_empty | has_range_bounds +----------------------------+---------------+-----------------+------------------ + (rng * int4range(50, 150)) | t | t | t +(1 row) + +-- Import statistics with NULL range fields, simulating dump from +-- older version. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_range_expr_null', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'stat_range_expr_null', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "14", + "null_frac": "0", + "n_distinct": "-1" + } + ]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +-- Verify that range fields are now NULL. +SELECT e.expr, + e.null_frac, + e.range_length_histogram IS NOT NULL AS has_range_len, + e.range_empty_frac IS NOT NULL AS has_range_empty, + e.range_bounds_histogram IS NOT NULL AS has_range_bounds +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' + AND e.statistics_name = 'stat_range_expr_null'; + expr | null_frac | has_range_len | has_range_empty | has_range_bounds +----------------------------+-----------+---------------+-----------------+------------------ + (rng * int4range(50, 150)) | 0 | f | f | f +(1 row) + +-- Trigger statistics loading through some queries. +EXPLAIN (COSTS OFF) +SELECT * FROM stats_import.test_range_expr_null + WHERE (rng * int4range(50, 150)) && '[60,70)'::int4range; + QUERY PLAN +------------------------------------------------------------------- + Seq Scan on test_range_expr_null + Filter: ((rng * '[50,150)'::int4range) && '[60,70)'::int4range) +(2 rows) + +SELECT COUNT(*) FROM stats_import.test_range_expr_null + WHERE (rng * int4range(50, 150)) && '[60,70)'::int4range; + count +------- + 19 +(1 row) + DROP SCHEMA stats_import CASCADE; -NOTICE: drop cascades to 6 other objects -DETAIL: drop cascades to type stats_import.complex_type +NOTICE: drop cascades to 19 other objects +DETAIL: drop cascades to view stats_import.pg_stats_stable +drop cascades to view stats_import.pg_statistic_flat_t +drop cascades to function stats_import.pg_statistic_flat(text) +drop cascades to function stats_import.pg_statistic_get_difference(text,text) +drop cascades to view stats_import.pg_stats_ext_flat_t +drop cascades to function stats_import.pg_stats_ext_flat(text) +drop cascades to function stats_import.pg_stats_ext_get_difference(text,text) +drop cascades to view stats_import.pg_stats_ext_exprs_flat_t +drop cascades to function stats_import.pg_stats_ext_exprs_flat(text) +drop cascades to function stats_import.pg_stats_ext_exprs_get_difference(text,text) +drop cascades to type stats_import.complex_type drop cascades to table stats_import.test +drop cascades to table stats_import.test_mr drop cascades to table stats_import.part_parent drop cascades to sequence stats_import.testseq drop cascades to view stats_import.testview drop cascades to table stats_import.test_clone +drop cascades to table stats_import.test_mr_clone +drop cascades to table stats_import.test_range_expr_null diff --git a/src/test/regress/expected/stats_rewrite.out b/src/test/regress/expected/stats_rewrite.out new file mode 100644 index 0000000000000..93752bab9cb33 --- /dev/null +++ b/src/test/regress/expected/stats_rewrite.out @@ -0,0 +1,355 @@ +-- +-- Test cumulative statistics with relation rewrites +-- +-- Two-phase commit. +-- Table-level stats with VACUUM and rewrite after 2PC commit. +CREATE TABLE test_2pc_timestamp (a int) WITH (autovacuum_enabled = false); +VACUUM ANALYZE test_2pc_timestamp; +SELECT last_analyze AS last_vacuum_analyze + FROM pg_stat_all_tables WHERE relname = 'test_2pc_timestamp' \gset +BEGIN; +ALTER TABLE test_2pc_timestamp ALTER COLUMN a TYPE int; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT last_analyze = :'last_vacuum_analyze'::timestamptz AS same_vacuum_ts + FROM pg_stat_all_tables WHERE relname = 'test_2pc_timestamp'; + same_vacuum_ts +---------------- + t +(1 row) + +DROP TABLE test_2pc_timestamp; +-- Table-level stats with single rewrite after 2PC commit. +CREATE TABLE test_2pc_rewrite_alone (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_rewrite_alone VALUES (1); +BEGIN; +ALTER TABLE test_2pc_rewrite_alone ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_rewrite_alone'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_2pc_rewrite_alone; +-- Table-level stats with rewrite and DMLs after 2PC commit. +CREATE TABLE test_2pc (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc VALUES (1); +BEGIN; +INSERT INTO test_2pc VALUES (1); +INSERT INTO test_2pc VALUES (2); +INSERT INTO test_2pc VALUES (3); +ALTER TABLE test_2pc ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 4 | 0 +(1 row) + +DROP TABLE test_2pc; +-- Table-level stats with multiple rewrites after 2PC commit. +CREATE TABLE test_2pc_multi (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_multi VALUES (1); +BEGIN; +INSERT INTO test_2pc_multi VALUES (1); +INSERT INTO test_2pc_multi VALUES (2); +ALTER TABLE test_2pc_multi ALTER COLUMN a TYPE bigint; +INSERT INTO test_2pc_multi VALUES (3); +INSERT INTO test_2pc_multi VALUES (4); +ALTER TABLE test_2pc_multi ALTER COLUMN a TYPE int; +INSERT INTO test_2pc_multi VALUES (5); +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_multi'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 6 | 0 +(1 row) + +DROP TABLE test_2pc_multi; +-- Table-level stats with single rewrite after 2PC abort. +CREATE TABLE test_2pc_rewrite_alone_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_rewrite_alone_abort VALUES (1); +BEGIN; +ALTER TABLE test_2pc_rewrite_alone_abort ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +ROLLBACK PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_rewrite_alone_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_2pc_rewrite_alone_abort; +-- Table-level stats with rewrite and DMLs after 2PC abort. +CREATE TABLE test_2pc_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_abort VALUES (1); +BEGIN; +INSERT INTO test_2pc_abort VALUES (1); +INSERT INTO test_2pc_abort VALUES (2); +ALTER TABLE test_2pc_abort ALTER COLUMN a TYPE bigint; +INSERT INTO test_2pc_abort VALUES (3); +PREPARE TRANSACTION 'test'; +ROLLBACK PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 1 | 3 +(1 row) + +DROP TABLE test_2pc_abort; +-- Table-level stats with rewrites and subtransactions after 2PC commit. +CREATE TABLE test_2pc_savepoint (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_savepoint VALUES (1); +BEGIN; +SAVEPOINT a; +INSERT INTO test_2pc_savepoint VALUES (1); +INSERT INTO test_2pc_savepoint VALUES (2); +ALTER TABLE test_2pc_savepoint ALTER COLUMN a TYPE bigint; +SAVEPOINT b; +INSERT INTO test_2pc_savepoint VALUES (3); +ALTER TABLE test_2pc_savepoint ALTER COLUMN a TYPE int; +SAVEPOINT c; +INSERT INTO test_2pc_savepoint VALUES (4); +INSERT INTO test_2pc_savepoint VALUES (5); +ROLLBACK TO SAVEPOINT b; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_savepoint'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 3 | 3 +(1 row) + +DROP TABLE test_2pc_savepoint; +-- Table-level stats with single rewrite and VACUUM +CREATE TABLE test_timestamp (a int) WITH (autovacuum_enabled = false); +VACUUM ANALYZE test_timestamp; +SELECT last_analyze AS last_vacuum_analyze + FROM pg_stat_all_tables WHERE relname = 'test_timestamp' \gset +ALTER TABLE test_timestamp ALTER COLUMN a TYPE bigint; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT last_analyze = :'last_vacuum_analyze'::timestamptz AS same_vacuum_ts + FROM pg_stat_all_tables WHERE relname = 'test_timestamp'; + same_vacuum_ts +---------------- + t +(1 row) + +DROP TABLE test_timestamp; +-- Table-level stats with single rewrite. +CREATE TABLE test_alone (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_alone VALUES (1); +BEGIN; +ALTER TABLE test_alone ALTER COLUMN a TYPE bigint; +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_alone'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_alone; +-- Table-level stats with rewrite and DMLs. +CREATE TABLE test (a int) WITH (autovacuum_enabled = false); +INSERT INTO test VALUES (1); +BEGIN; +INSERT INTO test VALUES (1); +INSERT INTO test VALUES (2); +INSERT INTO test VALUES (3); +ALTER TABLE test ALTER COLUMN a TYPE bigint; +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 4 | 0 +(1 row) + +DROP TABLE test; +-- Table-level stats with multiple rewrites and DMLs. +CREATE TABLE test_multi (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_multi VALUES (1); +BEGIN; +INSERT INTO test_multi VALUES (1); +INSERT INTO test_multi VALUES (2); +ALTER TABLE test_multi ALTER COLUMN a TYPE bigint; +INSERT INTO test_multi VALUES (3); +INSERT INTO test_multi VALUES (4); +ALTER TABLE test_multi ALTER COLUMN a TYPE int; +INSERT INTO test_multi VALUES (5); +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_multi'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 6 | 0 +(1 row) + +DROP TABLE test_multi; +-- Table-level stats with rewrite and rollback. +CREATE TABLE test_rewrite_alone_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_rewrite_alone_abort VALUES (1); +BEGIN; +ALTER TABLE test_rewrite_alone_abort ALTER COLUMN a TYPE bigint; +ROLLBACK; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_rewrite_alone_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_rewrite_alone_abort; +-- Table-level stats with rewrite, DMLs and rollback. +CREATE TABLE test_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_abort VALUES (1); +BEGIN; +INSERT INTO test_abort VALUES (1); +INSERT INTO test_abort VALUES (2); +ALTER TABLE test_abort ALTER COLUMN a TYPE bigint; +INSERT INTO test_abort VALUES (3); +ROLLBACK; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 1 | 3 +(1 row) + +DROP TABLE test_abort; +-- Table-level stats with rewrites and subtransactions. +CREATE TABLE test_savepoint (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_savepoint VALUES (1); +BEGIN; +SAVEPOINT a; +INSERT INTO test_savepoint VALUES (1); +INSERT INTO test_savepoint VALUES (2); +ALTER TABLE test_savepoint ALTER COLUMN a TYPE bigint; +SAVEPOINT b; +INSERT INTO test_savepoint VALUES (3); +ALTER TABLE test_savepoint ALTER COLUMN a TYPE int; +SAVEPOINT c; +INSERT INTO test_savepoint VALUES (4); +INSERT INTO test_savepoint VALUES (5); +ROLLBACK TO SAVEPOINT b; +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_savepoint'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 3 | 3 +(1 row) + +DROP TABLE test_savepoint; +-- Table-level stats with tablespace rewrite. +CREATE TABLE test_tbs (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_tbs VALUES (1); +ALTER TABLE test_tbs SET TABLESPACE pg_default; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_tbs'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_tbs; diff --git a/src/test/regress/expected/stats_rewrite_1.out b/src/test/regress/expected/stats_rewrite_1.out new file mode 100644 index 0000000000000..909188b18fd79 --- /dev/null +++ b/src/test/regress/expected/stats_rewrite_1.out @@ -0,0 +1,376 @@ +-- +-- Test cumulative statistics with relation rewrites +-- +-- Two-phase commit. +-- Table-level stats with VACUUM and rewrite after 2PC commit. +CREATE TABLE test_2pc_timestamp (a int) WITH (autovacuum_enabled = false); +VACUUM ANALYZE test_2pc_timestamp; +SELECT last_analyze AS last_vacuum_analyze + FROM pg_stat_all_tables WHERE relname = 'test_2pc_timestamp' \gset +BEGIN; +ALTER TABLE test_2pc_timestamp ALTER COLUMN a TYPE int; +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +COMMIT PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT last_analyze = :'last_vacuum_analyze'::timestamptz AS same_vacuum_ts + FROM pg_stat_all_tables WHERE relname = 'test_2pc_timestamp'; + same_vacuum_ts +---------------- + t +(1 row) + +DROP TABLE test_2pc_timestamp; +-- Table-level stats with single rewrite after 2PC commit. +CREATE TABLE test_2pc_rewrite_alone (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_rewrite_alone VALUES (1); +BEGIN; +ALTER TABLE test_2pc_rewrite_alone ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +COMMIT PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_rewrite_alone'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_2pc_rewrite_alone; +-- Table-level stats with rewrite and DMLs after 2PC commit. +CREATE TABLE test_2pc (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc VALUES (1); +BEGIN; +INSERT INTO test_2pc VALUES (1); +INSERT INTO test_2pc VALUES (2); +INSERT INTO test_2pc VALUES (3); +ALTER TABLE test_2pc ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +COMMIT PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 1 | 3 +(1 row) + +DROP TABLE test_2pc; +-- Table-level stats with multiple rewrites after 2PC commit. +CREATE TABLE test_2pc_multi (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_multi VALUES (1); +BEGIN; +INSERT INTO test_2pc_multi VALUES (1); +INSERT INTO test_2pc_multi VALUES (2); +ALTER TABLE test_2pc_multi ALTER COLUMN a TYPE bigint; +INSERT INTO test_2pc_multi VALUES (3); +INSERT INTO test_2pc_multi VALUES (4); +ALTER TABLE test_2pc_multi ALTER COLUMN a TYPE int; +INSERT INTO test_2pc_multi VALUES (5); +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +COMMIT PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_multi'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 1 | 5 +(1 row) + +DROP TABLE test_2pc_multi; +-- Table-level stats with single rewrite after 2PC abort. +CREATE TABLE test_2pc_rewrite_alone_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_rewrite_alone_abort VALUES (1); +BEGIN; +ALTER TABLE test_2pc_rewrite_alone_abort ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +ROLLBACK PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_rewrite_alone_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_2pc_rewrite_alone_abort; +-- Table-level stats with rewrite and DMLs after 2PC abort. +CREATE TABLE test_2pc_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_abort VALUES (1); +BEGIN; +INSERT INTO test_2pc_abort VALUES (1); +INSERT INTO test_2pc_abort VALUES (2); +ALTER TABLE test_2pc_abort ALTER COLUMN a TYPE bigint; +INSERT INTO test_2pc_abort VALUES (3); +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +ROLLBACK PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 1 | 3 +(1 row) + +DROP TABLE test_2pc_abort; +-- Table-level stats with rewrites and subtransactions after 2PC commit. +CREATE TABLE test_2pc_savepoint (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_savepoint VALUES (1); +BEGIN; +SAVEPOINT a; +INSERT INTO test_2pc_savepoint VALUES (1); +INSERT INTO test_2pc_savepoint VALUES (2); +ALTER TABLE test_2pc_savepoint ALTER COLUMN a TYPE bigint; +SAVEPOINT b; +INSERT INTO test_2pc_savepoint VALUES (3); +ALTER TABLE test_2pc_savepoint ALTER COLUMN a TYPE int; +SAVEPOINT c; +INSERT INTO test_2pc_savepoint VALUES (4); +INSERT INTO test_2pc_savepoint VALUES (5); +ROLLBACK TO SAVEPOINT b; +PREPARE TRANSACTION 'test'; +ERROR: prepared transactions are disabled +HINT: Set "max_prepared_transactions" to a nonzero value. +COMMIT PREPARED 'test'; +ERROR: prepared transaction with identifier "test" does not exist +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_savepoint'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 1 | 5 +(1 row) + +DROP TABLE test_2pc_savepoint; +-- Table-level stats with single rewrite and VACUUM +CREATE TABLE test_timestamp (a int) WITH (autovacuum_enabled = false); +VACUUM ANALYZE test_timestamp; +SELECT last_analyze AS last_vacuum_analyze + FROM pg_stat_all_tables WHERE relname = 'test_timestamp' \gset +ALTER TABLE test_timestamp ALTER COLUMN a TYPE bigint; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT last_analyze = :'last_vacuum_analyze'::timestamptz AS same_vacuum_ts + FROM pg_stat_all_tables WHERE relname = 'test_timestamp'; + same_vacuum_ts +---------------- + t +(1 row) + +DROP TABLE test_timestamp; +-- Table-level stats with single rewrite. +CREATE TABLE test_alone (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_alone VALUES (1); +BEGIN; +ALTER TABLE test_alone ALTER COLUMN a TYPE bigint; +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_alone'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_alone; +-- Table-level stats with rewrite and DMLs. +CREATE TABLE test (a int) WITH (autovacuum_enabled = false); +INSERT INTO test VALUES (1); +BEGIN; +INSERT INTO test VALUES (1); +INSERT INTO test VALUES (2); +INSERT INTO test VALUES (3); +ALTER TABLE test ALTER COLUMN a TYPE bigint; +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 4 | 0 +(1 row) + +DROP TABLE test; +-- Table-level stats with multiple rewrites and DMLs. +CREATE TABLE test_multi (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_multi VALUES (1); +BEGIN; +INSERT INTO test_multi VALUES (1); +INSERT INTO test_multi VALUES (2); +ALTER TABLE test_multi ALTER COLUMN a TYPE bigint; +INSERT INTO test_multi VALUES (3); +INSERT INTO test_multi VALUES (4); +ALTER TABLE test_multi ALTER COLUMN a TYPE int; +INSERT INTO test_multi VALUES (5); +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_multi'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 6 | 0 +(1 row) + +DROP TABLE test_multi; +-- Table-level stats with rewrite and rollback. +CREATE TABLE test_rewrite_alone_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_rewrite_alone_abort VALUES (1); +BEGIN; +ALTER TABLE test_rewrite_alone_abort ALTER COLUMN a TYPE bigint; +ROLLBACK; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_rewrite_alone_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_rewrite_alone_abort; +-- Table-level stats with rewrite, DMLs and rollback. +CREATE TABLE test_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_abort VALUES (1); +BEGIN; +INSERT INTO test_abort VALUES (1); +INSERT INTO test_abort VALUES (2); +ALTER TABLE test_abort ALTER COLUMN a TYPE bigint; +INSERT INTO test_abort VALUES (3); +ROLLBACK; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_abort'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 4 | 1 | 3 +(1 row) + +DROP TABLE test_abort; +-- Table-level stats with rewrites and subtransactions. +CREATE TABLE test_savepoint (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_savepoint VALUES (1); +BEGIN; +SAVEPOINT a; +INSERT INTO test_savepoint VALUES (1); +INSERT INTO test_savepoint VALUES (2); +ALTER TABLE test_savepoint ALTER COLUMN a TYPE bigint; +SAVEPOINT b; +INSERT INTO test_savepoint VALUES (3); +ALTER TABLE test_savepoint ALTER COLUMN a TYPE int; +SAVEPOINT c; +INSERT INTO test_savepoint VALUES (4); +INSERT INTO test_savepoint VALUES (5); +ROLLBACK TO SAVEPOINT b; +COMMIT; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_savepoint'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 6 | 3 | 3 +(1 row) + +DROP TABLE test_savepoint; +-- Table-level stats with tablespace rewrite. +CREATE TABLE test_tbs (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_tbs VALUES (1); +ALTER TABLE test_tbs SET TABLESPACE pg_default; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_tbs'; + n_tup_ins | n_live_tup | n_dead_tup +-----------+------------+------------ + 1 | 1 | 0 +(1 row) + +DROP TABLE test_tbs; diff --git a/src/test/regress/expected/strings.out b/src/test/regress/expected/strings.out index 174f0a68331bd..a49b75fa1f993 100644 --- a/src/test/regress/expected/strings.out +++ b/src/test/regress/expected/strings.out @@ -22,7 +22,6 @@ ERROR: syntax error at or near "' - third line'" LINE 3: ' - third line' ^ -- Unicode escapes -SET standard_conforming_strings TO on; SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; data ------ @@ -142,43 +141,12 @@ SELECT E'wrong: \U002FFFFF'; ERROR: invalid Unicode escape value at or near "\U002FFFFF" LINE 1: SELECT E'wrong: \U002FFFFF'; ^ +-- this is no longer allowed: SET standard_conforming_strings TO off; -SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; -ERROR: unsafe use of string constant with Unicode escapes -LINE 1: SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; - ^ -DETAIL: String constants with Unicode escapes cannot be used when "standard_conforming_strings" is off. -SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061" UESCAPE '*'; -ERROR: unsafe use of string constant with Unicode escapes -LINE 1: SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061... - ^ -DETAIL: String constants with Unicode escapes cannot be used when "standard_conforming_strings" is off. -SELECT U&' \' UESCAPE '!' AS "tricky"; -ERROR: unsafe use of string constant with Unicode escapes -LINE 1: SELECT U&' \' UESCAPE '!' AS "tricky"; - ^ -DETAIL: String constants with Unicode escapes cannot be used when "standard_conforming_strings" is off. -SELECT 'tricky' AS U&"\" UESCAPE '!'; - \ --------- - tricky -(1 row) - -SELECT U&'wrong: \061'; -ERROR: unsafe use of string constant with Unicode escapes -LINE 1: SELECT U&'wrong: \061'; - ^ -DETAIL: String constants with Unicode escapes cannot be used when "standard_conforming_strings" is off. -SELECT U&'wrong: \+0061'; -ERROR: unsafe use of string constant with Unicode escapes -LINE 1: SELECT U&'wrong: \+0061'; - ^ -DETAIL: String constants with Unicode escapes cannot be used when "standard_conforming_strings" is off. -SELECT U&'wrong: +0061' UESCAPE '+'; -ERROR: unsafe use of string constant with Unicode escapes -LINE 1: SELECT U&'wrong: +0061' UESCAPE '+'; - ^ -DETAIL: String constants with Unicode escapes cannot be used when "standard_conforming_strings" is off. +ERROR: non-standard string literals are not supported +-- but this should be acceptable: +SET standard_conforming_strings TO on; +-- or this: RESET standard_conforming_strings; -- bytea SET bytea_output TO hex; @@ -236,6 +204,12 @@ SELECT E'De\\678dBeEf'::bytea; ERROR: invalid input syntax for type bytea LINE 1: SELECT E'De\\678dBeEf'::bytea; ^ +SELECT E'DeAd\\\\BeEf'::bytea; + bytea +---------------------- + \x446541645c42654566 +(1 row) + SELECT reverse(''::bytea); reverse --------- @@ -254,6 +228,64 @@ SELECT reverse('\xabcd'::bytea); \xcdab (1 row) +SELECT ('\x' || repeat(' ', 32))::bytea; + bytea +------- + \x +(1 row) + +SELECT ('\x' || repeat('!', 32))::bytea; +ERROR: invalid hexadecimal digit: "!" +SELECT ('\x' || repeat('/', 34))::bytea; +ERROR: invalid hexadecimal digit: "/" +SELECT ('\x' || repeat('0', 34))::bytea; + bytea +-------------------------------------- + \x0000000000000000000000000000000000 +(1 row) + +SELECT ('\x' || repeat('9', 32))::bytea; + bytea +------------------------------------ + \x99999999999999999999999999999999 +(1 row) + +SELECT ('\x' || repeat(':', 32))::bytea; +ERROR: invalid hexadecimal digit: ":" +SELECT ('\x' || repeat('@', 34))::bytea; +ERROR: invalid hexadecimal digit: "@" +SELECT ('\x' || repeat('A', 34))::bytea; + bytea +-------------------------------------- + \xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +(1 row) + +SELECT ('\x' || repeat('F', 32))::bytea; + bytea +------------------------------------ + \xffffffffffffffffffffffffffffffff +(1 row) + +SELECT ('\x' || repeat('G', 32))::bytea; +ERROR: invalid hexadecimal digit: "G" +SELECT ('\x' || repeat('`', 34))::bytea; +ERROR: invalid hexadecimal digit: "`" +SELECT ('\x' || repeat('a', 34))::bytea; + bytea +-------------------------------------- + \xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +(1 row) + +SELECT ('\x' || repeat('f', 32))::bytea; + bytea +------------------------------------ + \xffffffffffffffffffffffffffffffff +(1 row) + +SELECT ('\x' || repeat('g', 32))::bytea; +ERROR: invalid hexadecimal digit: "g" +SELECT ('\x' || repeat('~', 34))::bytea; +ERROR: invalid hexadecimal digit: "~" SET bytea_output TO escape; SELECT E'\\xDeAdBeEf'::bytea; bytea @@ -291,6 +323,12 @@ SELECT E'De\\123dBeEf'::bytea; DeSdBeEf (1 row) +SELECT E'DeAd\\\\BeEf'::bytea; + bytea +------------ + DeAd\\BeEf +(1 row) + -- Test non-error-throwing API too SELECT pg_input_is_valid(E'\\xDeAdBeE', 'bytea'); pg_input_is_valid @@ -614,6 +652,82 @@ SELECT 'abcdefg' SIMILAR TO '_bcd%' ESCAPE NULL AS null; SELECT 'abcdefg' SIMILAR TO '_bcd#%' ESCAPE '##' AS error; ERROR: invalid escape string HINT: Escape string must be empty or one character. +-- Characters that should be left alone in character classes when a +-- SIMILAR TO regexp pattern is converted to POSIX style. +-- Underscore "_" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '_[_[:alpha:]_]_'; + QUERY PLAN +------------------------------------------------ + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:.[_[:alpha:]_].)$'::text) +(2 rows) + +-- Percentage "%" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '%[%[:alnum:]%]%'; + QUERY PLAN +-------------------------------------------------- + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:.*[%[:alnum:]%].*)$'::text) +(2 rows) + +-- Dot "." +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '.[.[:alnum:].].'; + QUERY PLAN +-------------------------------------------------- + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:\.[.[:alnum:].]\.)$'::text) +(2 rows) + +-- Dollar "$" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '$[$[:alnum:]$]$'; + QUERY PLAN +-------------------------------------------------- + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:\$[$[:alnum:]$]\$)$'::text) +(2 rows) + +-- Opening parenthesis "(" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '()[([:alnum:](]()'; + QUERY PLAN +------------------------------------------------------ + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:(?:)[([:alnum:](](?:))$'::text) +(2 rows) + +-- Caret "^" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '^[^[:alnum:]^[^^][[^^]][\^][[\^]]\^]^'; + QUERY PLAN +------------------------------------------------------------------------ + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:\^[^[:alnum:]^[^^][[^^]][\^][[\^]]\^]\^)$'::text) +(2 rows) + +-- Closing square bracket "]" at the beginning of character class +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '[]%][^]%][^%]%'; + QUERY PLAN +------------------------------------------------ + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:[]%][^]%][^%].*)$'::text) +(2 rows) + +-- Closing square bracket effective after two carets at the beginning +-- of character class. +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '[^^]^'; + QUERY PLAN +--------------------------------------- + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:[^^]\^)$'::text) +(2 rows) + +-- Closing square bracket after an escape sequence at the beginning of +-- a character closes the character class +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '[|a]%' ESCAPE '|'; + QUERY PLAN +--------------------------------------- + Seq Scan on text_tbl + Filter: (f1 ~ '^(?:[\a].*)$'::text) +(2 rows) + -- Test backslash escapes in regexp_replace's replacement string SELECT regexp_replace('1112223333', E'(\\d{3})(\\d{3})(\\d{4})', E'(\\1) \\2-\\3'); regexp_replace @@ -2011,6 +2125,94 @@ SELECT c FROM toasttest; x (1 row) +DROP TABLE toasttest; +-- test with short varlenas (up to 126 data bytes reduced to a 1-byte header) +-- being toasted. +CREATE TABLE toasttest (f1 text, f2 text); +ALTER TABLE toasttest SET (toast_tuple_target = 128); +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE EXTERNAL; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE EXTERNAL; +-- Here, the first value is a varlena large enough to make it toasted and +-- stored uncompressed. The second value is a short varlena, toasted +-- and stored uncompressed. +INSERT INTO toasttest values(repeat('1234', 1000), repeat('5678', 30)); +SELECT reltoastrelid::regclass AS reltoastname FROM pg_class + WHERE oid = 'toasttest'::regclass \gset +-- There should be two values inserted in the toast relation. +SELECT count(*) FROM :reltoastname WHERE chunk_seq = 0; + count +------- + 2 +(1 row) + +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; + f1_data | f2_data +------------+------------ + 1234123412 | 5678567856 +(1 row) + +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; + f1_comp | f2_comp +---------+--------- + | +(1 row) + +TRUNCATE toasttest; +-- test with inline compressible varlenas. +SET default_toast_compression = 'pglz'; +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE MAIN; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE MAIN; +INSERT INTO toasttest values(repeat('1234', 1024), repeat('5678', 1024)); +-- There should be no values in the toast relation. +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; + f1_data | f2_data +------------+------------ + 1234123412 | 5678567856 +(1 row) + +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; + f1_comp | f2_comp +---------+--------- + pglz | pglz +(1 row) + +SELECT count(*) FROM :reltoastname; + count +------- + 0 +(1 row) + +TRUNCATE toasttest; +-- test with external compressed data (default). +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE EXTENDED; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE EXTENDED; +INSERT INTO toasttest values(repeat('1234', 10240), NULL); +-- There should be one value in the toast relation. +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; + f1_data | f2_data +------------+--------- + 1234123412 | +(1 row) + +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; + f1_comp | f2_comp +---------+--------- + pglz | +(1 row) + +SELECT count(*) FROM :reltoastname WHERE chunk_seq = 0; + count +------- + 1 +(1 row) + +RESET default_toast_compression; DROP TABLE toasttest; -- -- test length @@ -2395,6 +2597,319 @@ SELECT decode(encode('\x1234567890abcdef00', 'escape'), 'escape'); \x1234567890abcdef00 (1 row) +-- report an error with a hint listing valid encodings when an invalid encoding is specified +SELECT encode('\x01'::bytea, 'invalid'); -- error +ERROR: unrecognized encoding: "invalid" +HINT: Valid encodings are "base32hex", "base64", "base64url", "escape", and "hex". +SELECT decode('00', 'invalid'); -- error +ERROR: unrecognized encoding: "invalid" +HINT: Valid encodings are "base32hex", "base64", "base64url", "escape", and "hex". +-- +-- base32hex encoding/decoding +-- +SET bytea_output TO hex; +SELECT encode('', 'base32hex'); -- '' + encode +-------- + +(1 row) + +SELECT encode('\x11', 'base32hex'); -- '24======' + encode +---------- + 24====== +(1 row) + +SELECT encode('\x1122', 'base32hex'); -- '24H0====' + encode +---------- + 24H0==== +(1 row) + +SELECT encode('\x112233', 'base32hex'); -- '24H36===' + encode +---------- + 24H36=== +(1 row) + +SELECT encode('\x11223344', 'base32hex'); -- '24H36H0=' + encode +---------- + 24H36H0= +(1 row) + +SELECT encode('\x1122334455', 'base32hex'); -- '24H36H2L' + encode +---------- + 24H36H2L +(1 row) + +SELECT encode('\x112233445566', 'base32hex'); -- '24H36H2LCO======' + encode +------------------ + 24H36H2LCO====== +(1 row) + +SELECT decode('', 'base32hex'); -- '' + decode +-------- + \x +(1 row) + +SELECT decode('24======', 'base32hex'); -- \x11 + decode +-------- + \x11 +(1 row) + +SELECT decode('24H0====', 'base32hex'); -- \x1122 + decode +-------- + \x1122 +(1 row) + +SELECT decode('24H36===', 'base32hex'); -- \x112233 + decode +---------- + \x112233 +(1 row) + +SELECT decode('24H36H0=', 'base32hex'); -- \x11223344 + decode +------------ + \x11223344 +(1 row) + +SELECT decode('24H36H2L', 'base32hex'); -- \x1122334455 + decode +-------------- + \x1122334455 +(1 row) + +SELECT decode('24H36H2LCO======', 'base32hex'); -- \x112233445566 + decode +---------------- + \x112233445566 +(1 row) + +SELECT decode('24h36h2lco', 'base32hex'); -- OK, the encoding is case-insensitive + decode +---------------- + \x112233445566 +(1 row) + +-- Tests for decoding unpadded base32hex strings. Padding '=' are optional. +SELECT decode('24', 'base32hex'); + decode +-------- + \x11 +(1 row) + +SELECT decode('24H', 'base32hex'); + decode +-------- + \x11 +(1 row) + +SELECT decode('24H36', 'base32hex'); + decode +---------- + \x112233 +(1 row) + +SELECT decode('24H36H0', 'base32hex'); + decode +------------ + \x11223344 +(1 row) + +SELECT decode('2', 'base32hex'); -- \x, 5 bits isn't enough for a byte, so nothing is emitted + decode +-------- + \x +(1 row) + +SELECT decode('11=', 'base32hex'); -- OK, non-zero padding bits are accepted (consistent with base64) + decode +-------- + \x08 +(1 row) + +SELECT decode('2=', 'base32hex'); -- error +ERROR: unexpected "=" while decoding base32hex sequence +SELECT decode('=', 'base32hex'); -- error +ERROR: unexpected "=" while decoding base32hex sequence +SELECT decode('W', 'base32hex'); -- error +ERROR: invalid symbol "W" found while decoding base32hex sequence +SELECT decode('24H36H0=24', 'base32hex'); -- error +ERROR: invalid symbol "2" found while decoding base32hex sequence +-- Check round-trip capability of base32hex encoding for multiple random UUIDs. +DO $$ +DECLARE + v1 uuid; + v2 uuid; +BEGIN + FOR i IN 1..10 LOOP + v1 := gen_random_uuid(); + v2 := decode(encode(v1::bytea, 'base32hex'), 'base32hex')::uuid; + + IF v1 != v2 THEN + RAISE EXCEPTION 'base32hex encoding round-trip failed, expected % got %', v1, v2; + END IF; + END LOOP; + RAISE NOTICE 'OK'; +END; +$$; +NOTICE: OK +-- +-- base64url encoding/decoding +-- +-- Simple encoding/decoding +SELECT encode('\x69b73eff', 'base64url'); -- abc-_w + encode +-------- + abc-_w +(1 row) + +SELECT decode('abc-_w', 'base64url'); -- \x69b73eff + decode +------------ + \x69b73eff +(1 row) + +-- Round-trip: decode(encode(x)) = x +SELECT decode(encode('\x1234567890abcdef00', 'base64url'), 'base64url'); -- \x1234567890abcdef00 + decode +---------------------- + \x1234567890abcdef00 +(1 row) + +-- Empty input +SELECT encode('', 'base64url'); -- '' + encode +-------- + +(1 row) + +SELECT decode('', 'base64url'); -- '' + decode +-------- + \x +(1 row) + +-- 1 byte input +SELECT encode('\x01', 'base64url'); -- AQ + encode +-------- + AQ +(1 row) + +SELECT decode('AQ', 'base64url'); -- \x01 + decode +-------- + \x01 +(1 row) + +-- 2 byte input +SELECT encode('\x0102'::bytea, 'base64url'); -- AQI + encode +-------- + AQI +(1 row) + +SELECT decode('AQI', 'base64url'); -- \x0102 + decode +-------- + \x0102 +(1 row) + +-- 3 byte input (no padding needed) +SELECT encode('\x010203'::bytea, 'base64url'); -- AQID + encode +-------- + AQID +(1 row) + +SELECT decode('AQID', 'base64url'); -- \x010203 + decode +---------- + \x010203 +(1 row) + +-- 4 byte input (results in 6 base64 chars) +SELECT encode('\xdeadbeef'::bytea, 'base64url'); -- 3q2-7w + encode +-------- + 3q2-7w +(1 row) + +SELECT decode('3q2-7w', 'base64url'); -- \xdeadbeef + decode +------------ + \xdeadbeef +(1 row) + +-- Round-trip test for all lengths from 0–4 +SELECT encode(decode(encode(E'\\x', 'base64url'), 'base64url'), 'base64url'); + encode +-------- + +(1 row) + +SELECT encode(decode(encode(E'\\x00', 'base64url'), 'base64url'), 'base64url'); + encode +-------- + AA +(1 row) + +SELECT encode(decode(encode(E'\\x0001', 'base64url'), 'base64url'), 'base64url'); + encode +-------- + AAE +(1 row) + +SELECT encode(decode(encode(E'\\x000102', 'base64url'), 'base64url'), 'base64url'); + encode +-------- + AAEC +(1 row) + +SELECT encode(decode(encode(E'\\x00010203', 'base64url'), 'base64url'), 'base64url'); + encode +-------- + AAECAw +(1 row) + +-- Invalid inputs (should ERROR) +-- invalid character '@' +SELECT decode('QQ@=', 'base64url'); +ERROR: invalid symbol "@" found while decoding base64url sequence +-- missing characters (incomplete group) +SELECT decode('QQ', 'base64url'); -- ok (1 byte) + decode +-------- + \x41 +(1 row) + +SELECT decode('QQI', 'base64url'); -- ok (2 bytes) + decode +-------- + \x4102 +(1 row) + +SELECT decode('QQIDQ', 'base64url'); -- ERROR: invalid base64url end sequence +ERROR: invalid base64url end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +-- unexpected '=' at start +SELECT decode('=QQQ', 'base64url'); +ERROR: unexpected "=" while decoding base64url sequence +-- valid base64 padding in base64url (optional, but accepted) +SELECT decode('abc-_w==', 'base64url'); -- should decode to \x69b73eff + decode +------------ + \x69b73eff +(1 row) + -- -- get_bit/set_bit etc -- @@ -2532,90 +3047,6 @@ SELECT '\x8000000000000000'::bytea::int8 AS "-9223372036854775808", -9223372036854775808 | 9223372036854775807 (1 row) --- --- test behavior of escape_string_warning and standard_conforming_strings options --- -set escape_string_warning = off; -set standard_conforming_strings = off; -show escape_string_warning; - escape_string_warning ------------------------ - off -(1 row) - -show standard_conforming_strings; - standard_conforming_strings ------------------------------ - off -(1 row) - -set escape_string_warning = on; -set standard_conforming_strings = on; -show escape_string_warning; - escape_string_warning ------------------------ - on -(1 row) - -show standard_conforming_strings; - standard_conforming_strings ------------------------------ - on -(1 row) - -select 'a\bcd' as f1, 'a\b''cd' as f2, 'a\b''''cd' as f3, 'abcd\' as f4, 'ab\''cd' as f5, '\\' as f6; - f1 | f2 | f3 | f4 | f5 | f6 --------+--------+---------+-------+--------+---- - a\bcd | a\b'cd | a\b''cd | abcd\ | ab\'cd | \\ -(1 row) - -set standard_conforming_strings = off; -select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as f6; -WARNING: nonstandard use of \\ in a string literal -LINE 1: select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3,... - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 1: select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3,... - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 1: select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3,... - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 1: ...bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' ... - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 1: ...'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd'... - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. -WARNING: nonstandard use of \\ in a string literal -LINE 1: ...'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as ... - ^ -HINT: Use the escape string syntax for backslashes, e.g., E'\\'. - f1 | f2 | f3 | f4 | f5 | f6 --------+--------+---------+-------+--------+---- - a\bcd | a\b'cd | a\b''cd | abcd\ | ab\'cd | \\ -(1 row) - -set escape_string_warning = off; -set standard_conforming_strings = on; -select 'a\bcd' as f1, 'a\b''cd' as f2, 'a\b''''cd' as f3, 'abcd\' as f4, 'ab\''cd' as f5, '\\' as f6; - f1 | f2 | f3 | f4 | f5 | f6 --------+--------+---------+-------+--------+---- - a\bcd | a\b'cd | a\b''cd | abcd\ | ab\'cd | \\ -(1 row) - -set standard_conforming_strings = off; -select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as f6; - f1 | f2 | f3 | f4 | f5 | f6 --------+--------+---------+-------+--------+---- - a\bcd | a\b'cd | a\b''cd | abcd\ | ab\'cd | \\ -(1 row) - -reset standard_conforming_strings; -- -- Additional string functions -- diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index 1443e1d929296..7e3cabdb93f52 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -1,6 +1,14 @@ -- -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -31,7 +39,7 @@ ERROR: publication name "foo" used more than once -- ok CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. COMMENT ON SUBSCRIPTION regress_testsub IS 'test subscription'; SELECT obj_description(s.oid, 'pg_subscription') FROM pg_subscription s; obj_description @@ -102,32 +110,32 @@ ERROR: subscription with slot_name = NONE must also set enabled = false -- ok - with slot_name = NONE CREATE SUBSCRIPTION regress_testsub3 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. -- fail ALTER SUBSCRIPTION regress_testsub3 ENABLE; ERROR: cannot enable subscription that does not have a slot name ALTER SUBSCRIPTION regress_testsub3 REFRESH PUBLICATION; -ERROR: ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions +ERROR: ALTER SUBSCRIPTION ... REFRESH PUBLICATION is not allowed for disabled subscriptions -- fail - origin must be either none or any CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false, origin = foo); ERROR: unrecognized origin value: "foo" -- now it works CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false, origin = none); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub3; @@ -140,15 +148,62 @@ ERROR: invalid connection string syntax: invalid connection option "i_dont_exis -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; ERROR: subscription "regress_testsub5" could not connect to the publisher: invalid port number: "-1" +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +GRANT CREATE ON DATABASE REGRESSION TO regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; +-- fail, need USAGE privileges on server +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +ERROR: permission denied for foreign server test_server +RESET SESSION AUTHORIZATION; +GRANT USAGE ON FOREIGN SERVER test_server TO regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; +-- fail, need user mapping +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +ERROR: user mapping not found for user "regress_subscription_user3", server "test_server" +CREATE USER MAPPING FOR regress_subscription_user3 SERVER test_server OPTIONS(user 'foo', password 'secret'); +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +ERROR: foreign data wrapper "test_fdw" does not support subscription connections +DETAIL: Foreign data wrapper must be defined with CONNECTION specified. +RESET SESSION AUTHORIZATION; +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +SET SESSION AUTHORIZATION regress_subscription_user3; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. +DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; +RESET SESSION AUTHORIZATION; +REVOKE USAGE ON FOREIGN SERVER test_server FROM regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; +-- fail, must connect but lacks USAGE on server, as well as user mapping +DROP SUBSCRIPTION regress_testsub6; +ERROR: could not connect to publisher when attempting to drop replication slot "dummy": subscription owner "regress_subscription_user3" does not have permission on foreign server "test_server" +HINT: Use ALTER SUBSCRIPTION ... DISABLE to disable the subscription, and then use ALTER SUBSCRIPTION ... SET (slot_name = NONE) to disassociate it from the slot. +ALTER SUBSCRIPTION regress_testsub6 SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub6; +SET SESSION AUTHORIZATION regress_subscription_user; +REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; +DROP SERVER test_server; +-- fail, FDW is dependent +DROP FUNCTION test_fdw_connection(oid, oid, internal); +ERROR: cannot drop function test_fdw_connection(oid,oid,internal) because other objects depend on it +DETAIL: foreign-data wrapper test_fdw depends on function test_fdw_connection(oid,oid,internal) +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- warn +ALTER FOREIGN DATA WRAPPER test_fdw NO CONNECTION; +WARNING: removing the foreign-data wrapper connection function will cause dependent subscriptions to fail +DROP FUNCTION test_fdw_connection(oid, oid, internal); +DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; ERROR: invalid connection string syntax: missing "=" after "foobar" in connection info string \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false); @@ -157,10 +212,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname'); ALTER SUBSCRIPTION regress_testsub SET (password_required = false); ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | off | dbname=regress_doesnotexist2 | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET (password_required = true); @@ -176,10 +231,10 @@ ERROR: unrecognized subscription parameter: "create_slot" -- ok ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345'); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist2 | 0/12345 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription (1 row) -- ok - with lsn = NONE @@ -188,10 +243,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE); ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0'); ERROR: invalid WAL location (LSN): 0/0 \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist2 | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) BEGIN; @@ -222,11 +277,15 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = local); ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar); ERROR: invalid value for parameter "synchronous_commit": "foobar" HINT: Available values: local, remote_write, remote_apply, on, off. +ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '-1'); +ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '80s'); +ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = 'foobar'); +ERROR: invalid value for parameter "wal_receiver_timeout": "foobar" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------------+---------- - regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | local | dbname=regress_doesnotexist2 | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription (1 row) -- rename back to keep the rest simple @@ -253,21 +312,21 @@ ERROR: binary requires a Boolean value -- now it works CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, binary = true); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (binary = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -277,29 +336,29 @@ ERROR: streaming requires a Boolean value or "parallel" -- now it works CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = true); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication already exists @@ -314,10 +373,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false); ERROR: publication "testpub1" is already in subscription "regress_testsub" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication used more than once @@ -332,17 +391,17 @@ ERROR: publication "testpub3" is not in subscription "regress_testsub" -- ok - delete publications ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION mypub WITH (connect = false, create_slot = false, copy_data = false); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. ALTER SUBSCRIPTION regress_testsub ENABLE; -- fail - ALTER SUBSCRIPTION with refresh is not allowed in a transaction -- block or function @@ -352,12 +411,12 @@ ERROR: ALTER SUBSCRIPTION with refresh cannot run inside a transaction block END; BEGIN; ALTER SUBSCRIPTION regress_testsub REFRESH PUBLICATION; -ERROR: ALTER SUBSCRIPTION ... REFRESH cannot run inside a transaction block +ERROR: ALTER SUBSCRIPTION ... REFRESH PUBLICATION cannot run inside a transaction block END; CREATE FUNCTION func() RETURNS VOID AS $$ ALTER SUBSCRIPTION regress_testsub SET PUBLICATION mypub WITH (refresh = true) $$ LANGUAGE SQL; SELECT func(); -ERROR: ALTER SUBSCRIPTION with refresh cannot be executed from a function +ERROR: ALTER SUBSCRIPTION with refresh cannot be executed from a function or procedure CONTEXT: SQL function "func" statement 1 ALTER SUBSCRIPTION regress_testsub DISABLE; ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -369,21 +428,21 @@ ERROR: two_phase requires a Boolean value -- now it works CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, two_phase = true); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- we can alter streaming when two_phase enabled ALTER SUBSCRIPTION regress_testsub SET (streaming = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -391,12 +450,12 @@ DROP SUBSCRIPTION regress_testsub; -- two_phase and streaming are compatible. CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, streaming = true, two_phase = true); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -407,20 +466,62 @@ ERROR: disable_on_error requires a Boolean value -- now it works CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, disable_on_error = false); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Synchronous commit | Conninfo | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+-----------------------------+---------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | off | dbname=regress_doesnotexist | 0/0 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | +(1 row) + +ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub; +-- fail - retain_dead_tuples must be boolean +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, retain_dead_tuples = foo); +ERROR: retain_dead_tuples requires a Boolean value +-- ok +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, retain_dead_tuples = false); +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. +\dRs+ + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | +(1 row) + +ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub; +-- fail - max_retention_duration must be integer +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = foo); +ERROR: max_retention_duration requires an integer value +-- ok +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = 1000); +NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabled +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. +\dRs+ + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | +(1 row) + +-- ok +ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); +\dRs+ + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -450,7 +551,7 @@ GRANT CREATE ON DATABASE REGRESSION TO regress_subscription_user3; SET SESSION AUTHORIZATION regress_subscription_user3; CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist password=regress_fakepassword' PUBLICATION testpub WITH (connect = false); WARNING: subscription was created, but is not connected -HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. -- we cannot give the subscription away to some random user ALTER SUBSCRIPTION regress_testsub OWNER TO regress_subscription_user; ERROR: must be able to SET ROLE "regress_subscription_user" diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index 40d8056fcea40..a3778c23c3433 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -205,11 +205,11 @@ SELECT f1 AS "Correlated Field" -- Check ROWCOMPARE cases, both correlated and not EXPLAIN (VERBOSE, COSTS OFF) SELECT ROW(1, 2) = (SELECT f1, f2) AS eq FROM SUBSELECT_TBL; - QUERY PLAN ------------------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------------------------------------- Seq Scan on public.subselect_tbl - Output: (((1 = (SubPlan 1).col1) AND (2 = (SubPlan 1).col2))) - SubPlan 1 + Output: (((1 = (SubPlan rowcompare_1).col1) AND (2 = (SubPlan rowcompare_1).col2))) + SubPlan rowcompare_1 -> Result Output: subselect_tbl.f1, subselect_tbl.f2 (5 rows) @@ -229,11 +229,11 @@ SELECT ROW(1, 2) = (SELECT f1, f2) AS eq FROM SUBSELECT_TBL; EXPLAIN (VERBOSE, COSTS OFF) SELECT ROW(1, 2) = (SELECT 3, 4) AS eq FROM SUBSELECT_TBL; - QUERY PLAN ------------------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------------------------------------- Seq Scan on public.subselect_tbl - Output: ((1 = (InitPlan 1).col1) AND (2 = (InitPlan 1).col2)) - InitPlan 1 + Output: ((1 = (InitPlan rowcompare_1).col1) AND (2 = (InitPlan rowcompare_1).col2)) + InitPlan rowcompare_1 -> Result Output: 3, 4 (5 rows) @@ -375,18 +375,18 @@ explain (verbose, costs off) select '42' union all select 43; -- check materialization of an initplan reference (bug #14524) explain (verbose, costs off) select 1 = all (select (select 1)); - QUERY PLAN -------------------------------------------- + QUERY PLAN +------------------------------------------------ Result - Output: (ALL (1 = (SubPlan 2).col1)) - SubPlan 2 + Output: (ALL (1 = (SubPlan all_1).col1)) + SubPlan all_1 -> Materialize - Output: ((InitPlan 1).col1) - InitPlan 1 + Output: ((InitPlan expr_1).col1) + InitPlan expr_1 -> Result Output: 1 -> Result - Output: (InitPlan 1).col1 + Output: (InitPlan expr_1).col1 (10 rows) select 1 = all (select (select 1)); @@ -428,8 +428,8 @@ select * from int4_tbl o where exists QUERY PLAN -------------------------------------- Seq Scan on int4_tbl o - Filter: EXISTS(SubPlan 1) - SubPlan 1 + Filter: EXISTS(SubPlan exists_1) + SubPlan exists_1 -> Limit -> Seq Scan on int4_tbl i Filter: (f1 = o.f1) @@ -676,6 +676,23 @@ select * from ( 0 (1 row) +-- +-- Test cases for interactions between PARAM_EXEC, subplans and array +-- subscripts +-- +-- check that array subscription doesn't conflict with PARAM_EXEC (see #19370) +SELECT (array[1,2])[(SELECT g.i)] FROM generate_series(1, 1) g(i); + array +------- + 1 +(1 row) + +SELECT (array[1,2])[(SELECT g.i):(SELECT g.i + 1)] FROM generate_series(1, 1) g(i); + array +------- + {1,2} +(1 row) + -- -- Test that an IN implemented using a UniquePath does unique-ification -- with the right semantics, as per bug #4113. (Unfortunately we have @@ -707,6 +724,199 @@ select * from numeric_table 3 (4 rows) +-- +-- Test that a semijoin implemented by unique-ifying the RHS can explore +-- different paths of the RHS rel. +-- +create table semijoin_unique_tbl (a int, b int); +insert into semijoin_unique_tbl select i%10, i%10 from generate_series(1,1000)i; +create index on semijoin_unique_tbl(a, b); +analyze semijoin_unique_tbl; +-- Ensure that we get a plan with Unique + IndexScan +explain (verbose, costs off) +select * from semijoin_unique_tbl t1, semijoin_unique_tbl t2 +where (t1.a, t2.a) in (select a, b from semijoin_unique_tbl t3) +order by t1.a, t2.a; + QUERY PLAN +------------------------------------------------------------------------------------------------------ + Nested Loop + Output: t1.a, t1.b, t2.a, t2.b + -> Merge Join + Output: t1.a, t1.b, t3.b + Merge Cond: (t3.a = t1.a) + -> Unique + Output: t3.a, t3.b + -> Index Only Scan using semijoin_unique_tbl_a_b_idx on public.semijoin_unique_tbl t3 + Output: t3.a, t3.b + -> Index Only Scan using semijoin_unique_tbl_a_b_idx on public.semijoin_unique_tbl t1 + Output: t1.a, t1.b + -> Memoize + Output: t2.a, t2.b + Cache Key: t3.b + Cache Mode: logical + -> Index Only Scan using semijoin_unique_tbl_a_b_idx on public.semijoin_unique_tbl t2 + Output: t2.a, t2.b + Index Cond: (t2.a = t3.b) +(18 rows) + +-- Ensure that we can unique-ify expressions more complex than plain Vars +explain (verbose, costs off) +select * from semijoin_unique_tbl t1, semijoin_unique_tbl t2 +where (t1.a, t2.a) in (select a+1, b+1 from semijoin_unique_tbl t3) +order by t1.a, t2.a; + QUERY PLAN +------------------------------------------------------------------------------------------------ + Incremental Sort + Output: t1.a, t1.b, t2.a, t2.b + Sort Key: t1.a, t2.a + Presorted Key: t1.a + -> Merge Join + Output: t1.a, t1.b, t2.a, t2.b + Merge Cond: (t1.a = ((t3.a + 1))) + -> Index Only Scan using semijoin_unique_tbl_a_b_idx on public.semijoin_unique_tbl t1 + Output: t1.a, t1.b + -> Sort + Output: t2.a, t2.b, t3.a, ((t3.a + 1)) + Sort Key: ((t3.a + 1)) + -> Hash Join + Output: t2.a, t2.b, t3.a, (t3.a + 1) + Hash Cond: (t2.a = (t3.b + 1)) + -> Seq Scan on public.semijoin_unique_tbl t2 + Output: t2.a, t2.b + -> Hash + Output: t3.a, t3.b + -> HashAggregate + Output: t3.a, t3.b + Group Key: (t3.a + 1), (t3.b + 1) + -> Seq Scan on public.semijoin_unique_tbl t3 + Output: t3.a, t3.b, (t3.a + 1), (t3.b + 1) +(24 rows) + +-- encourage use of parallel plans +set parallel_setup_cost=0; +set parallel_tuple_cost=0; +set min_parallel_table_scan_size=0; +set max_parallel_workers_per_gather=4; +set enable_indexscan to off; +-- Ensure that we get a parallel plan for the unique-ification +explain (verbose, costs off) +select * from semijoin_unique_tbl t1, semijoin_unique_tbl t2 +where (t1.a, t2.a) in (select a, b from semijoin_unique_tbl t3) +order by t1.a, t2.a; + QUERY PLAN +---------------------------------------------------------------------------------------- + Nested Loop + Output: t1.a, t1.b, t2.a, t2.b + -> Merge Join + Output: t1.a, t1.b, t3.b + Merge Cond: (t3.a = t1.a) + -> Unique + Output: t3.a, t3.b + -> Gather Merge + Output: t3.a, t3.b + Workers Planned: 2 + -> Sort + Output: t3.a, t3.b + Sort Key: t3.a, t3.b + -> HashAggregate + Output: t3.a, t3.b + Group Key: t3.a, t3.b + -> Parallel Seq Scan on public.semijoin_unique_tbl t3 + Output: t3.a, t3.b + -> Materialize + Output: t1.a, t1.b + -> Gather Merge + Output: t1.a, t1.b + Workers Planned: 2 + -> Sort + Output: t1.a, t1.b + Sort Key: t1.a + -> Parallel Seq Scan on public.semijoin_unique_tbl t1 + Output: t1.a, t1.b + -> Memoize + Output: t2.a, t2.b + Cache Key: t3.b + Cache Mode: logical + -> Bitmap Heap Scan on public.semijoin_unique_tbl t2 + Output: t2.a, t2.b + Recheck Cond: (t2.a = t3.b) + -> Bitmap Index Scan on semijoin_unique_tbl_a_b_idx + Index Cond: (t2.a = t3.b) +(37 rows) + +reset enable_indexscan; +reset max_parallel_workers_per_gather; +reset min_parallel_table_scan_size; +reset parallel_tuple_cost; +reset parallel_setup_cost; +drop table semijoin_unique_tbl; +create table unique_tbl_p (a int, b int) partition by range(a); +create table unique_tbl_p1 partition of unique_tbl_p for values from (0) to (5); +create table unique_tbl_p2 partition of unique_tbl_p for values from (5) to (10); +create table unique_tbl_p3 partition of unique_tbl_p for values from (10) to (20); +insert into unique_tbl_p select i%12, i from generate_series(0, 1000)i; +create index on unique_tbl_p1(a); +create index on unique_tbl_p2(a); +create index on unique_tbl_p3(a); +analyze unique_tbl_p; +set enable_partitionwise_join to on; +-- Ensure that the unique-ification works for partition-wise join +-- (Only one of the two joins will be done partitionwise, but that's good +-- enough for our purposes.) +explain (verbose, costs off) +select * from unique_tbl_p t1, unique_tbl_p t2 +where (t1.a, t2.a) in (select a, a from unique_tbl_p t3) +order by t1.a, t2.a; + QUERY PLAN +------------------------------------------------------------------------------------------------ + Merge Join + Output: t1.a, t1.b, t2.a, t2.b + Merge Cond: (t1.a = t2.a) + -> Merge Append + Sort Key: t1.a + -> Nested Loop + Output: t1_1.a, t1_1.b, t3_1.a + -> Unique + Output: t3_1.a + -> Index Only Scan using unique_tbl_p1_a_idx on public.unique_tbl_p1 t3_1 + Output: t3_1.a + -> Index Scan using unique_tbl_p1_a_idx on public.unique_tbl_p1 t1_1 + Output: t1_1.a, t1_1.b + Index Cond: (t1_1.a = t3_1.a) + -> Nested Loop + Output: t1_2.a, t1_2.b, t3_2.a + -> Unique + Output: t3_2.a + -> Index Only Scan using unique_tbl_p2_a_idx on public.unique_tbl_p2 t3_2 + Output: t3_2.a + -> Index Scan using unique_tbl_p2_a_idx on public.unique_tbl_p2 t1_2 + Output: t1_2.a, t1_2.b + Index Cond: (t1_2.a = t3_2.a) + -> Nested Loop + Output: t1_3.a, t1_3.b, t3_3.a + -> Unique + Output: t3_3.a + -> Sort + Output: t3_3.a + Sort Key: t3_3.a + -> Seq Scan on public.unique_tbl_p3 t3_3 + Output: t3_3.a + -> Index Scan using unique_tbl_p3_a_idx on public.unique_tbl_p3 t1_3 + Output: t1_3.a, t1_3.b + Index Cond: (t1_3.a = t3_3.a) + -> Materialize + Output: t2.a, t2.b + -> Append + -> Index Scan using unique_tbl_p1_a_idx on public.unique_tbl_p1 t2_1 + Output: t2_1.a, t2_1.b + -> Index Scan using unique_tbl_p2_a_idx on public.unique_tbl_p2 t2_2 + Output: t2_2.a, t2_2.b + -> Index Scan using unique_tbl_p3_a_idx on public.unique_tbl_p3 t2_3 + Output: t2_3.a, t2_3.b +(44 rows) + +reset enable_partitionwise_join; +drop table unique_tbl_p; -- -- Test case for bug #4290: bogus calculation of subplan param sets -- @@ -773,6 +983,25 @@ select (select (a.*)::text) from view_a a; (42) (1 row) +-- +-- Test case for bug #19037: no relation entry for relid N +-- +explain (costs off) +select (1 = any(array_agg(f1))) = any (select false) from int4_tbl; + QUERY PLAN +---------------------------- + Aggregate + -> Seq Scan on int4_tbl + SubPlan any_1 + -> Result +(4 rows) + +select (1 = any(array_agg(f1))) = any (select false) from int4_tbl; + ?column? +---------- + t +(1 row) + -- -- Check that whole-row Vars reading the result of a subselect don't include -- any junk columns therein @@ -891,11 +1120,11 @@ select * from outer_text where (f1, f2) not in (select * from inner_text); -- explain (verbose, costs off) select 'foo'::text in (select 'bar'::name union all select 'bar'::name); - QUERY PLAN ---------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- Result - Output: (ANY ('foo'::text = (hashed SubPlan 1).col1)) - SubPlan 1 + Output: (ANY ('foo'::text = (hashed SubPlan any_1).col1)) + SubPlan any_1 -> Append -> Result Output: 'bar'::name @@ -915,11 +1144,11 @@ select 'foo'::text in (select 'bar'::name union all select 'bar'::name); -- explain (verbose, costs off) select row(row(row(1))) = any (select row(row(1))); - QUERY PLAN --------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------ Result - Output: (ANY ('("(1)")'::record = (SubPlan 1).col1)) - SubPlan 1 + Output: (ANY ('("(1)")'::record = (SubPlan any_1).col1)) + SubPlan any_1 -> Materialize Output: '("(1)")'::record -> Result @@ -950,7 +1179,8 @@ select * from int8_tbl where q1 in (select c1 from inner_text); ERROR: operator does not exist: bigint = text LINE 1: select * from int8_tbl where q1 in (select c1 from inner_tex... ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. begin; -- make an operator to allow it to succeed create function bogus_int8_text_eq(int8, text) returns boolean @@ -958,11 +1188,11 @@ language sql as 'select $1::text = $2'; create operator = (procedure=bogus_int8_text_eq, leftarg=int8, rightarg=text); explain (costs off) select * from int8_tbl where q1 in (select c1 from inner_text); - QUERY PLAN --------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------ Seq Scan on int8_tbl - Filter: (ANY ((q1)::text = (hashed SubPlan 1).col1)) - SubPlan 1 + Filter: (ANY ((q1)::text = (hashed SubPlan any_1).col1)) + SubPlan any_1 -> Seq Scan on inner_text (4 rows) @@ -979,11 +1209,11 @@ create or replace function bogus_int8_text_eq(int8, text) returns boolean language sql as 'select $1::text = $2 and $1::text = $2'; explain (costs off) select * from int8_tbl where q1 in (select c1 from inner_text); - QUERY PLAN ------------------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------------------- Seq Scan on int8_tbl - Filter: (ANY (((q1)::text = (hashed SubPlan 1).col1) AND ((q1)::text = (hashed SubPlan 1).col1))) - SubPlan 1 + Filter: (ANY (((q1)::text = (hashed SubPlan any_1).col1) AND ((q1)::text = (hashed SubPlan any_1).col1))) + SubPlan any_1 -> Seq Scan on inner_text (4 rows) @@ -1000,11 +1230,11 @@ create or replace function bogus_int8_text_eq(int8, text) returns boolean language sql as 'select $2 = $1::text'; explain (costs off) select * from int8_tbl where q1 in (select c1 from inner_text); - QUERY PLAN -------------------------------------------------- + QUERY PLAN +----------------------------------------------------- Seq Scan on int8_tbl - Filter: (ANY ((SubPlan 1).col1 = (q1)::text)) - SubPlan 1 + Filter: (ANY ((SubPlan any_1).col1 = (q1)::text)) + SubPlan any_1 -> Materialize -> Seq Scan on inner_text (5 rows) @@ -1023,12 +1253,12 @@ rollback; -- to get rid of the bogus operator explain (costs off) select count(*) from tenk1 t where (exists(select 1 from tenk1 k where k.unique1 = t.unique2) or ten < 0); - QUERY PLAN --------------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------------- Aggregate -> Seq Scan on tenk1 t - Filter: ((ANY (unique2 = (hashed SubPlan 2).col1)) OR (ten < 0)) - SubPlan 2 + Filter: ((ANY (unique2 = (hashed SubPlan exists_2).col1)) OR (ten < 0)) + SubPlan exists_2 -> Index Only Scan using tenk1_unique1 on tenk1 k (5 rows) @@ -1048,10 +1278,10 @@ where (exists(select 1 from tenk1 k where k.unique1 = t.unique2) or ten < 0) Aggregate -> Bitmap Heap Scan on tenk1 t Recheck Cond: (thousand = 1) - Filter: (EXISTS(SubPlan 1) OR (ten < 0)) + Filter: (EXISTS(SubPlan exists_1) OR (ten < 0)) -> Bitmap Index Scan on tenk1_thous_tenthous Index Cond: (thousand = 1) - SubPlan 1 + SubPlan exists_1 -> Index Only Scan using tenk1_unique1 on tenk1 k Index Cond: (unique1 = t.unique2) (9 rows) @@ -1073,20 +1303,20 @@ analyze exists_tbl; explain (costs off) select * from exists_tbl t1 where (exists(select 1 from exists_tbl t2 where t1.c1 = t2.c2) or c3 < 0); - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------------- Append -> Seq Scan on exists_tbl_null t1_1 - Filter: (EXISTS(SubPlan 1) OR (c3 < 0)) - SubPlan 1 + Filter: (EXISTS(SubPlan exists_1) OR (c3 < 0)) + SubPlan exists_1 -> Append -> Seq Scan on exists_tbl_null t2_1 Filter: (t1_1.c1 = c2) -> Seq Scan on exists_tbl_def t2_2 Filter: (t1_1.c1 = c2) -> Seq Scan on exists_tbl_def t1_2 - Filter: ((ANY (c1 = (hashed SubPlan 2).col1)) OR (c3 < 0)) - SubPlan 2 + Filter: ((ANY (c1 = (hashed SubPlan exists_2).col1)) OR (c3 < 0)) + SubPlan exists_2 -> Append -> Seq Scan on exists_tbl_null t2_4 -> Seq Scan on exists_tbl_def t2_5 @@ -1122,14 +1352,14 @@ where a.thousand = b.thousand explain (verbose, costs off) select x, x from (select (select now()) as x from (values(1),(2)) v(y)) ss; - QUERY PLAN ------------------------------------------------- + QUERY PLAN +---------------------------------------------------------- Values Scan on "*VALUES*" - Output: (InitPlan 1).col1, (InitPlan 2).col1 - InitPlan 1 + Output: (InitPlan expr_1).col1, (InitPlan expr_2).col1 + InitPlan expr_1 -> Result Output: now() - InitPlan 2 + InitPlan expr_2 -> Result Output: now() (8 rows) @@ -1137,13 +1367,13 @@ explain (verbose, costs off) explain (verbose, costs off) select x, x from (select (select random()) as x from (values(1),(2)) v(y)) ss; - QUERY PLAN ------------------------------------ + QUERY PLAN +---------------------------------------- Subquery Scan on ss Output: ss.x, ss.x -> Values Scan on "*VALUES*" - Output: (InitPlan 1).col1 - InitPlan 1 + Output: (InitPlan expr_1).col1 + InitPlan expr_1 -> Result Output: random() (7 rows) @@ -1154,12 +1384,12 @@ explain (verbose, costs off) QUERY PLAN ---------------------------------------------------------------------- Values Scan on "*VALUES*" - Output: (SubPlan 1), (SubPlan 2) - SubPlan 1 + Output: (SubPlan expr_1), (SubPlan expr_2) + SubPlan expr_1 -> Result Output: now() One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1) - SubPlan 2 + SubPlan expr_2 -> Result Output: now() One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1) @@ -1173,8 +1403,8 @@ explain (verbose, costs off) Subquery Scan on ss Output: ss.x, ss.x -> Values Scan on "*VALUES*" - Output: (SubPlan 1) - SubPlan 1 + Output: (SubPlan expr_1) + SubPlan expr_1 -> Result Output: random() One-Time Filter: ("*VALUES*".column1 = "*VALUES*".column1) @@ -1194,16 +1424,16 @@ where o.ten = 0; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Aggregate - Output: sum((((ANY (i.ten = (hashed SubPlan 1).col1))))::integer) + Output: sum((((ANY (i.ten = (hashed SubPlan any_1).col1))))::integer) -> Nested Loop - Output: ((ANY (i.ten = (hashed SubPlan 1).col1))) + Output: ((ANY (i.ten = (hashed SubPlan any_1).col1))) -> Seq Scan on public.onek o Output: o.unique1, o.unique2, o.two, o.four, o.ten, o.twenty, o.hundred, o.thousand, o.twothousand, o.fivethous, o.tenthous, o.odd, o.even, o.stringu1, o.stringu2, o.string4 Filter: (o.ten = 0) -> Index Scan using onek_unique1 on public.onek i - Output: (ANY (i.ten = (hashed SubPlan 1).col1)), random() + Output: (ANY (i.ten = (hashed SubPlan any_1).col1)), random() Index Cond: (i.unique1 = o.unique1) - SubPlan 1 + SubPlan any_1 -> Seq Scan on public.int4_tbl Output: int4_tbl.f1 Filter: (int4_tbl.f1 <= o.hundred) @@ -1412,7 +1642,7 @@ select * from ---------------------------------------- Values Scan on "*VALUES*" Output: "*VALUES*".column1 - SubPlan 1 + SubPlan any_1 -> Values Scan on "*VALUES*_1" Output: "*VALUES*_1".column1 (5 rows) @@ -1439,12 +1669,12 @@ select * from int4_tbl where --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Nested Loop Semi Join Output: int4_tbl.f1 - Join Filter: (CASE WHEN (ANY (int4_tbl.f1 = (hashed SubPlan 1).col1)) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten) + Join Filter: (CASE WHEN (ANY (int4_tbl.f1 = (hashed SubPlan any_1).col1)) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten) -> Seq Scan on public.int4_tbl Output: int4_tbl.f1 -> Seq Scan on public.tenk1 b Output: b.unique1, b.unique2, b.two, b.four, b.ten, b.twenty, b.hundred, b.thousand, b.twothousand, b.fivethous, b.tenthous, b.odd, b.even, b.stringu1, b.stringu2, b.string4 - SubPlan 1 + SubPlan any_1 -> Index Only Scan using tenk1_unique1 on public.tenk1 a Output: a.unique1 (10 rows) @@ -1467,14 +1697,14 @@ select * from int4_tbl o where (f1, f1) in ------------------------------------------------------------------- Nested Loop Semi Join Output: o.f1 - Join Filter: (o.f1 = "ANY_subquery".f1) + Join Filter: (o.f1 = unnamed_subquery.f1) -> Seq Scan on public.int4_tbl o Output: o.f1 -> Materialize - Output: "ANY_subquery".f1, "ANY_subquery".g - -> Subquery Scan on "ANY_subquery" - Output: "ANY_subquery".f1, "ANY_subquery".g - Filter: ("ANY_subquery".f1 = "ANY_subquery".g) + Output: unnamed_subquery.f1, unnamed_subquery.g + -> Subquery Scan on unnamed_subquery + Output: unnamed_subquery.f1, unnamed_subquery.g + Filter: (unnamed_subquery.f1 = unnamed_subquery.g) -> Result Output: i.f1, ((generate_series(1, 50)) / 10) -> ProjectSet @@ -1695,6 +1925,110 @@ NOTICE: x = 9, y = 13 9 | 3 (3 rows) +-- +-- check that an upper-level qual is not pushed down if it references a grouped +-- Var whose underlying expression contains SRFs +-- +explain (verbose, costs off) +select * from + (select generate_series(1, ten) as g, count(*) from tenk1 group by 1) ss + where ss.g = 1; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Subquery Scan on ss + Output: ss.g, ss.count + Filter: (ss.g = 1) + -> HashAggregate + Output: (generate_series(1, tenk1.ten)), count(*) + Group Key: generate_series(1, tenk1.ten) + -> ProjectSet + Output: generate_series(1, tenk1.ten) + -> Seq Scan on public.tenk1 + Output: tenk1.unique1, tenk1.unique2, tenk1.two, tenk1.four, tenk1.ten, tenk1.twenty, tenk1.hundred, tenk1.thousand, tenk1.twothousand, tenk1.fivethous, tenk1.tenthous, tenk1.odd, tenk1.even, tenk1.stringu1, tenk1.stringu2, tenk1.string4 +(10 rows) + +select * from + (select generate_series(1, ten) as g, count(*) from tenk1 group by 1) ss + where ss.g = 1; + g | count +---+------- + 1 | 9000 +(1 row) + +-- +-- check that an upper-level qual is not pushed down if it references a grouped +-- Var whose underlying expression contains volatile functions +-- +alter function tattle(x int, y int) volatile; +explain (verbose, costs off) +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + QUERY PLAN +------------------------------------------------------------ + Subquery Scan on ss + Output: ss.v, ss.count + Filter: ss.v + -> GroupAggregate + Output: (tattle(3, tenk1.ten)), count(*) + Group Key: (tattle(3, tenk1.ten)) + -> Sort + Output: (tattle(3, tenk1.ten)) + Sort Key: (tattle(3, tenk1.ten)) + -> Bitmap Heap Scan on public.tenk1 + Output: tattle(3, tenk1.ten) + Recheck Cond: (tenk1.unique1 < 3) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (tenk1.unique1 < 3) +(14 rows) + +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; +NOTICE: x = 3, y = 2 +NOTICE: x = 3, y = 1 +NOTICE: x = 3, y = 0 + v | count +---+------- + t | 3 +(1 row) + +-- if we pretend it's stable, we get different results: +alter function tattle(x int, y int) stable; +explain (verbose, costs off) +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + QUERY PLAN +------------------------------------------------------ + GroupAggregate + Output: (tattle(3, tenk1.ten)), count(*) + Group Key: (tattle(3, tenk1.ten)) + -> Sort + Output: (tattle(3, tenk1.ten)) + Sort Key: (tattle(3, tenk1.ten)) + -> Bitmap Heap Scan on public.tenk1 + Output: tattle(3, tenk1.ten) + Recheck Cond: (tenk1.unique1 < 3) + Filter: tattle(3, tenk1.ten) + -> Bitmap Index Scan on tenk1_unique1 + Index Cond: (tenk1.unique1 < 3) +(12 rows) + +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; +NOTICE: x = 3, y = 2 +NOTICE: x = 3, y = 2 +NOTICE: x = 3, y = 1 +NOTICE: x = 3, y = 1 +NOTICE: x = 3, y = 0 +NOTICE: x = 3, y = 0 + v | count +---+------- + t | 3 +(1 row) + drop function tattle(x int, y int); -- -- Test that LIMIT can be pushed to SORT through a subquery that just projects @@ -1764,6 +2098,35 @@ fetch backward all in c1; commit; -- +-- Check that JsonConstructorExpr is treated as non-strict, and thus can be +-- wrapped in a PlaceHolderVar +-- +begin; +create temp table json_tab (a int); +insert into json_tab values (1); +explain (verbose, costs off) +select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false; + QUERY PLAN +------------------------------------------------------ + Nested Loop Left Join + Output: t1.a, (JSON_ARRAY(1, t2.a RETURNING json)) + Join Filter: false + -> Seq Scan on pg_temp.json_tab t1 + Output: t1.a + -> Result + Output: JSON_ARRAY(1, t2.a RETURNING json) + Replaces: Scan on t2 + One-Time Filter: false +(9 rows) + +select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false; + a | json_array +---+------------ + 1 | +(1 row) + +rollback; +-- -- Verify that we correctly flatten cases involving a subquery output -- expression that doesn't need to be wrapped in a PlaceHolderVar -- @@ -2127,30 +2490,30 @@ explain (verbose, costs off) select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; - QUERY PLAN ----------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------- Sort - Output: (COALESCE(t3.q1)), t4.q1, t4.q2 - Sort Key: (COALESCE(t3.q1)), t4.q1, t4.q2 + Output: (COALESCE(t3.q1, t3.q1)), t4.q1, t4.q2 + Sort Key: (COALESCE(t3.q1, t3.q1)), t4.q1, t4.q2 -> Hash Right Join - Output: (COALESCE(t3.q1)), t4.q1, t4.q2 + Output: (COALESCE(t3.q1, t3.q1)), t4.q1, t4.q2 Hash Cond: (t4.q1 = t1.q2) -> Hash Join - Output: (COALESCE(t3.q1)), t4.q1, t4.q2 + Output: (COALESCE(t3.q1, t3.q1)), t4.q1, t4.q2 Hash Cond: (t2.q2 = t4.q1) -> Hash Left Join - Output: t2.q2, (COALESCE(t3.q1)) + Output: t2.q2, (COALESCE(t3.q1, t3.q1)) Hash Cond: (t2.q1 = t3.q2) -> Seq Scan on public.int8_tbl t2 Output: t2.q1, t2.q2 -> Hash - Output: t3.q2, (COALESCE(t3.q1)) + Output: t3.q2, (COALESCE(t3.q1, t3.q1)) -> Seq Scan on public.int8_tbl t3 - Output: t3.q2, COALESCE(t3.q1) + Output: t3.q2, COALESCE(t3.q1, t3.q1) -> Hash Output: t4.q1, t4.q2 -> Seq Scan on public.int8_tbl t4 @@ -2164,7 +2527,7 @@ order by 1, 2, 3; select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; @@ -2201,32 +2564,32 @@ explain (verbose, costs off) select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; - QUERY PLAN ----------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------- Sort - Output: ((COALESCE(t3.q1))), t4.q1, t4.q2 - Sort Key: ((COALESCE(t3.q1))), t4.q1, t4.q2 + Output: ((COALESCE(t3.q1, t3.q1))), t4.q1, t4.q2 + Sort Key: ((COALESCE(t3.q1, t3.q1))), t4.q1, t4.q2 -> Hash Right Join - Output: ((COALESCE(t3.q1))), t4.q1, t4.q2 + Output: ((COALESCE(t3.q1, t3.q1))), t4.q1, t4.q2 Hash Cond: (t4.q1 = t1.q2) -> Nested Loop - Output: t4.q1, t4.q2, ((COALESCE(t3.q1))) + Output: t4.q1, t4.q2, ((COALESCE(t3.q1, t3.q1))) Join Filter: (t2.q2 = t4.q1) -> Hash Left Join - Output: t2.q2, (COALESCE(t3.q1)) + Output: t2.q2, (COALESCE(t3.q1, t3.q1)) Hash Cond: (t2.q1 = t3.q2) -> Seq Scan on public.int8_tbl t2 Output: t2.q1, t2.q2 -> Hash - Output: t3.q2, (COALESCE(t3.q1)) + Output: t3.q2, (COALESCE(t3.q1, t3.q1)) -> Seq Scan on public.int8_tbl t3 - Output: t3.q2, COALESCE(t3.q1) + Output: t3.q2, COALESCE(t3.q1, t3.q1) -> Seq Scan on public.int8_tbl t4 - Output: t4.q1, t4.q2, (COALESCE(t3.q1)) + Output: t4.q1, t4.q2, (COALESCE(t3.q1, t3.q1)) -> Hash Output: t1.q2 -> Seq Scan on public.int8_tbl t1 @@ -2236,7 +2599,7 @@ order by 1, 2, 3; select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; @@ -2543,14 +2906,14 @@ select * from tenk1 A where exists (select 1 from tenk2 B where A.hundred in (select C.hundred FROM tenk2 C WHERE c.odd = b.odd)); - QUERY PLAN ------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------- Nested Loop Semi Join - Join Filter: (ANY (a.hundred = (SubPlan 1).col1)) + Join Filter: (ANY (a.hundred = (SubPlan any_1).col1)) -> Seq Scan on tenk1 a -> Materialize -> Seq Scan on tenk2 b - SubPlan 1 + SubPlan any_1 -> Seq Scan on tenk2 c Filter: (odd = b.odd) (8 rows) @@ -2560,14 +2923,14 @@ WHERE c.odd = b.odd)); explain (costs off) SELECT * FROM tenk1 A LEFT JOIN tenk2 B ON A.hundred in (SELECT c.hundred FROM tenk2 C WHERE c.odd = b.odd); - QUERY PLAN ------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------- Nested Loop Left Join - Join Filter: (ANY (a.hundred = (SubPlan 1).col1)) + Join Filter: (ANY (a.hundred = (SubPlan any_1).col1)) -> Seq Scan on tenk1 a -> Materialize -> Seq Scan on tenk2 b - SubPlan 1 + SubPlan any_1 -> Seq Scan on tenk2 c Filter: (odd = b.odd) (8 rows) @@ -2577,14 +2940,14 @@ ON A.hundred in (SELECT c.hundred FROM tenk2 C WHERE c.odd = b.odd); explain (costs off) SELECT * FROM tenk1 A LEFT JOIN tenk2 B ON B.hundred in (SELECT c.hundred FROM tenk2 C WHERE c.odd = a.odd); - QUERY PLAN ------------------------------------------------------ + QUERY PLAN +--------------------------------------------------------- Nested Loop Left Join - Join Filter: (ANY (b.hundred = (SubPlan 1).col1)) + Join Filter: (ANY (b.hundred = (SubPlan any_1).col1)) -> Seq Scan on tenk1 a -> Materialize -> Seq Scan on tenk2 b - SubPlan 1 + SubPlan any_1 -> Seq Scan on tenk2 c Filter: (odd = a.odd) (8 rows) @@ -2642,15 +3005,16 @@ ON B.hundred in (SELECT min(c.hundred) FROM tenk2 C WHERE c.odd = b.odd); -> Memoize Cache Key: b.hundred, b.odd Cache Mode: binary - -> Subquery Scan on "ANY_subquery" - Filter: (b.hundred = "ANY_subquery".min) + -> Subquery Scan on unnamed_subquery + Filter: (b.hundred = unnamed_subquery.min) -> Result - InitPlan 1 + Replaces: MinMaxAggregate + InitPlan minmax_1 -> Limit -> Index Scan using tenk2_hundred on tenk2 c Index Cond: (hundred IS NOT NULL) Filter: (odd = b.odd) -(16 rows) +(17 rows) -- -- Test VALUES to ARRAY (VtA) transformation @@ -2672,18 +3036,17 @@ EXPLAIN (COSTS OFF) SELECT * FROM onek WHERE (unique1,ten) IN (VALUES (1,1), (20,0), (99,9), (17,99)) ORDER BY unique1; - QUERY PLAN ------------------------------------------------------------------ - Sort - Sort Key: onek.unique1 - -> Nested Loop - -> HashAggregate - Group Key: "*VALUES*".column1, "*VALUES*".column2 + QUERY PLAN +---------------------------------------------------------------- + Nested Loop + -> Unique + -> Sort + Sort Key: "*VALUES*".column1, "*VALUES*".column2 -> Values Scan on "*VALUES*" - -> Index Scan using onek_unique1 on onek - Index Cond: (unique1 = "*VALUES*".column1) - Filter: ("*VALUES*".column2 = ten) -(9 rows) + -> Index Scan using onek_unique1 on onek + Index Cond: (unique1 = "*VALUES*".column1) + Filter: ("*VALUES*".column2 = ten) +(8 rows) EXPLAIN (COSTS OFF) SELECT * FROM onek @@ -2814,8 +3177,9 @@ EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, NULL); QUERY PLAN -------------------------- Result + Replaces: Scan on onek One-Time Filter: false -(2 rows) +(3 rows) EXPLAIN (COSTS OFF) EXECUTE test(NULL, 3.14, '-1.5'); QUERY PLAN @@ -2858,12 +3222,10 @@ SELECT ten FROM onek WHERE unique1 IN (VALUES (1), (2) ORDER BY 1); -> Unique -> Sort Sort Key: "*VALUES*".column1 - -> Sort - Sort Key: "*VALUES*".column1 - -> Values Scan on "*VALUES*" + -> Values Scan on "*VALUES*" -> Index Scan using onek_unique1 on onek Index Cond: (unique1 = "*VALUES*".column1) -(9 rows) +(7 rows) EXPLAIN (COSTS OFF) SELECT ten FROM onek WHERE unique1 IN (VALUES (1), (2) LIMIT 1); @@ -2888,7 +3250,7 @@ WHERE unique1 IN (VALUES (0), ((2 IN (SELECT unique2 FROM onek c -> Seq Scan on onek t -> Values Scan on "*VALUES*" Filter: (t.unique1 = column1) - SubPlan 1 + SubPlan any_1 -> Index Only Scan using onek_unique2 on onek c Index Cond: (unique2 = t.unique1) (7 rows) @@ -2904,7 +3266,7 @@ WHERE unique1 IN (VALUES (0), ((2 IN (SELECT unique2 FROM onek c -> Sort Sort Key: "*VALUES*".column1 -> Values Scan on "*VALUES*" - SubPlan 1 + SubPlan any_1 -> Index Only Scan using onek_unique2 on onek c Filter: ((unique2)::double precision = ANY ('{0.479425538604203,2}'::double precision[])) -> Index Scan using onek_unique1 on onek t @@ -2923,7 +3285,7 @@ SELECT ten FROM onek t WHERE unique1 IN (VALUES (0), ((2 IN -> Sort Sort Key: "*VALUES*".column1 -> Values Scan on "*VALUES*" - SubPlan 1 + SubPlan any_1 -> Result -> Index Scan using onek_unique1 on onek t Index Cond: (unique1 = "*VALUES*".column1) @@ -2961,3 +3323,465 @@ SELECT ten FROM onek t WHERE 1.0::integer IN ((VALUES (1), (3))); Seq Scan on onek t (1 row) +-- +-- Check NOT IN performs an ANTI JOIN when both the outer query's expressions +-- and the sub-select's output columns are provably non-nullable, and the +-- operator itself cannot return NULL for non-null inputs. +-- +BEGIN; +CREATE TEMP TABLE not_null_tab (id int NOT NULL, val int NOT NULL); +CREATE TEMP TABLE null_tab (id int, val int); +-- ANTI JOIN: both sides are defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +----------------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = not_null_tab_1.id) + -> Seq Scan on not_null_tab + -> Hash + -> Seq Scan on not_null_tab not_null_tab_1 +(5 rows) + +-- No ANTI JOIN: outer side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab +WHERE id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +---------------------------------------------------------- + Seq Scan on null_tab + Filter: (NOT (ANY (id = (hashed SubPlan any_1).col1))) + SubPlan any_1 + -> Seq Scan on not_null_tab +(4 rows) + +-- No ANTI JOIN: inner side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM null_tab); + QUERY PLAN +---------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY (id = (hashed SubPlan any_1).col1))) + SubPlan any_1 + -> Seq Scan on null_tab +(4 rows) + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM null_tab WHERE id IS NOT NULL); + QUERY PLAN +---------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = null_tab.id) + -> Seq Scan on not_null_tab + -> Hash + -> Seq Scan on null_tab + Filter: (id IS NOT NULL) +(6 rows) + +-- No ANTI JOIN: outer side is nullable (we don't check outer query quals for now) +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab +WHERE id IS NOT NULL + AND id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +--------------------------------------------------------------------------------- + Seq Scan on null_tab + Filter: ((id IS NOT NULL) AND (NOT (ANY (id = (hashed SubPlan any_1).col1)))) + SubPlan any_1 + -> Seq Scan on not_null_tab +(4 rows) + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is defined NOT NULL +-- and is not nulled by outer join +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t1.id + FROM not_null_tab t1 + LEFT JOIN not_null_tab t2 ON t1.id = t2.id +); + QUERY PLAN +----------------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = t1.id) + -> Seq Scan on not_null_tab + -> Hash + -> Merge Left Join + Merge Cond: (t1.id = t2.id) + -> Sort + Sort Key: t1.id + -> Seq Scan on not_null_tab t1 + -> Sort + Sort Key: t2.id + -> Seq Scan on not_null_tab t2 +(12 rows) + +-- No ANTI JOIN: inner side is defined NOT NULL but is nulled by outer join +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t2.id + FROM not_null_tab t1 + LEFT JOIN not_null_tab t2 ON t1.id = t2.id +); + QUERY PLAN +---------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY (id = (hashed SubPlan any_1).col1))) + SubPlan any_1 + -> Merge Left Join + Merge Cond: (t1.id = t2.id) + -> Sort + Sort Key: t1.id + -> Seq Scan on not_null_tab t1 + -> Sort + Sort Key: t2.id + -> Seq Scan on not_null_tab t2 +(11 rows) + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t2.id + FROM not_null_tab t1 + LEFT JOIN not_null_tab t2 ON t1.id = t2.id + WHERE t2.id IS NOT NULL +); + QUERY PLAN +----------------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = t2.id) + -> Seq Scan on not_null_tab + -> Hash + -> Merge Join + Merge Cond: (t1.id = t2.id) + -> Sort + Sort Key: t1.id + -> Seq Scan on not_null_tab t1 + -> Sort + Sort Key: t2.id + -> Seq Scan on not_null_tab t2 +(12 rows) + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t1.id + FROM null_tab t1 + LEFT JOIN null_tab t2 ON t1.id = t2.id + WHERE t1.id IS NOT NULL +); + QUERY PLAN +---------------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = t1.id) + -> Seq Scan on not_null_tab + -> Hash + -> Merge Left Join + Merge Cond: (t1.id = t2.id) + -> Sort + Sort Key: t1.id + -> Seq Scan on null_tab t1 + Filter: (id IS NOT NULL) + -> Sort + Sort Key: t2.id + -> Seq Scan on null_tab t2 +(13 rows) + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t1.id + FROM null_tab t1 + INNER JOIN null_tab t2 ON t1.id = t2.id + LEFT JOIN null_tab t3 ON TRUE +); + QUERY PLAN +------------------------------------------------- + Merge Anti Join + Merge Cond: (not_null_tab.id = t1.id) + -> Sort + Sort Key: not_null_tab.id + -> Seq Scan on not_null_tab + -> Nested Loop Left Join + -> Merge Join + Merge Cond: (t1.id = t2.id) + -> Sort + Sort Key: t1.id + -> Seq Scan on null_tab t1 + -> Sort + Sort Key: t2.id + -> Seq Scan on null_tab t2 + -> Materialize + -> Seq Scan on null_tab t3 +(16 rows) + +-- ANTI JOIN: outer side is defined NOT NULL and is not nulled by outer join, +-- inner side is defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 ON t1.id = t2.id +WHERE t1.id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +---------------------------------------------------- + Merge Left Join + Merge Cond: (t1.id = t2.id) + -> Sort + Sort Key: t1.id + -> Hash Anti Join + Hash Cond: (t1.id = not_null_tab.id) + -> Seq Scan on not_null_tab t1 + -> Hash + -> Seq Scan on not_null_tab + -> Sort + Sort Key: t2.id + -> Seq Scan on not_null_tab t2 +(12 rows) + +-- No ANTI JOIN: outer side is nulled by outer join +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 ON t1.id = t2.id +WHERE t2.id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +------------------------------------------------------------- + Merge Left Join + Merge Cond: (t1.id = t2.id) + Filter: (NOT (ANY (t2.id = (hashed SubPlan any_1).col1))) + -> Sort + Sort Key: t1.id + -> Seq Scan on not_null_tab t1 + -> Sort + Sort Key: t2.id + -> Seq Scan on not_null_tab t2 + SubPlan any_1 + -> Seq Scan on not_null_tab +(11 rows) + +-- No ANTI JOIN: sublink is in an outer join's ON qual and references the +-- non-nullable side +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 +ON t1.id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +------------------------------------------------------------------ + Nested Loop Left Join + Join Filter: (NOT (ANY (t1.id = (hashed SubPlan any_1).col1))) + -> Seq Scan on not_null_tab t1 + -> Materialize + -> Seq Scan on not_null_tab t2 + SubPlan any_1 + -> Seq Scan on not_null_tab +(7 rows) + +-- ANTI JOIN: outer side is defined NOT NULL and is not nulled by outer join, +-- inner side is defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 +ON t2.id NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +---------------------------------------------------- + Nested Loop Left Join + -> Seq Scan on not_null_tab t1 + -> Materialize + -> Hash Anti Join + Hash Cond: (t2.id = not_null_tab.id) + -> Seq Scan on not_null_tab t2 + -> Hash + -> Seq Scan on not_null_tab +(8 rows) + +-- ANTI JOIN: both sides are defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE (id, val) NOT IN (SELECT id, val FROM not_null_tab); + QUERY PLAN +--------------------------------------------------------------------------------------------------- + Merge Anti Join + Merge Cond: ((not_null_tab.id = not_null_tab_1.id) AND (not_null_tab.val = not_null_tab_1.val)) + -> Sort + Sort Key: not_null_tab.id, not_null_tab.val + -> Seq Scan on not_null_tab + -> Sort + Sort Key: not_null_tab_1.id, not_null_tab_1.val + -> Seq Scan on not_null_tab not_null_tab_1 +(8 rows) + +-- ANTI JOIN: both sides are defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE NOT (id, val) > ANY (SELECT id, val FROM not_null_tab); + QUERY PLAN +------------------------------------------------------------------------------------------------------ + Nested Loop Anti Join + Join Filter: (ROW(not_null_tab.id, not_null_tab.val) > ROW(not_null_tab_1.id, not_null_tab_1.val)) + -> Seq Scan on not_null_tab + -> Materialize + -> Seq Scan on not_null_tab not_null_tab_1 +(5 rows) + +-- No ANTI JOIN: one column of the outer side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1, null_tab t2 +WHERE (t1.id, t2.id) NOT IN (SELECT id, val FROM not_null_tab); + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- + Nested Loop + Join Filter: (NOT (ANY ((t1.id = (hashed SubPlan any_1).col1) AND (t2.id = (hashed SubPlan any_1).col2)))) + -> Seq Scan on not_null_tab t1 + -> Materialize + -> Seq Scan on null_tab t2 + SubPlan any_1 + -> Seq Scan on not_null_tab +(7 rows) + +-- No ANTI JOIN: one column of the inner side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE (id, val) NOT IN (SELECT t1.id, t2.id FROM not_null_tab t1, null_tab t2); + QUERY PLAN +-------------------------------------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY ((id = (SubPlan any_1).col1) AND (val = (SubPlan any_1).col2)))) + SubPlan any_1 + -> Materialize + -> Nested Loop + -> Seq Scan on not_null_tab t1 + -> Materialize + -> Seq Scan on null_tab t2 +(8 rows) + +-- ANTI JOIN: COALESCE(nullable, constant) is non-nullable +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab +WHERE COALESCE(id, -1) NOT IN (SELECT id FROM not_null_tab); + QUERY PLAN +----------------------------------------------------------------------- + Hash Anti Join + Hash Cond: (COALESCE(null_tab.id, '-1'::integer) = not_null_tab.id) + -> Seq Scan on null_tab + -> Hash + -> Seq Scan on not_null_tab +(5 rows) + +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT COALESCE(id, -1) FROM null_tab); + QUERY PLAN +----------------------------------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = COALESCE(null_tab.id, '-1'::integer)) + -> Seq Scan on not_null_tab + -> Hash + -> Seq Scan on null_tab +(5 rows) + +-- ANTI JOIN: GROUP BY (without Grouping Sets) preserves the non-nullability of +-- the column +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM not_null_tab GROUP BY id); + QUERY PLAN +----------------------------------------------------------- + Hash Anti Join + Hash Cond: (not_null_tab.id = not_null_tab_1.id) + -> Seq Scan on not_null_tab + -> Hash + -> HashAggregate + Group Key: not_null_tab_1.id + -> Seq Scan on not_null_tab not_null_tab_1 +(7 rows) + +-- No ANTI JOIN: GROUP BY on a nullable column +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM null_tab GROUP BY id); + QUERY PLAN +---------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY (id = (hashed SubPlan any_1).col1))) + SubPlan any_1 + -> HashAggregate + Group Key: null_tab.id + -> Seq Scan on null_tab +(6 rows) + +-- No ANTI JOIN: Grouping Sets can introduce NULLs +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT id + FROM not_null_tab + GROUP BY GROUPING SETS ((id), (val)) +); + QUERY PLAN +---------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY (id = (hashed SubPlan any_1).col1))) + SubPlan any_1 + -> HashAggregate + Hash Key: not_null_tab_1.id + Hash Key: not_null_tab_1.val + -> Seq Scan on not_null_tab not_null_tab_1 +(7 rows) + +-- create a custom "unsafe" equality operator +CREATE FUNCTION int4eq_unsafe(int4, int4) + RETURNS bool + AS 'int4eq' + LANGUAGE internal IMMUTABLE; +CREATE OPERATOR ?= ( + PROCEDURE = int4eq_unsafe, + LEFTARG = int4, + RIGHTARG = int4 +); +-- No ANTI JOIN: the operator is not safe +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE NOT id ?= ANY (SELECT id FROM not_null_tab); + QUERY PLAN +------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY (id ?= (SubPlan any_1).col1))) + SubPlan any_1 + -> Materialize + -> Seq Scan on not_null_tab not_null_tab_1 +(5 rows) + +-- No ANTI JOIN: the inner side has an unvalidated NOT NULL constraint, so +-- the column might contain NULLs. +CREATE TEMP TABLE notnull_notvalid_tab (id int); +INSERT INTO notnull_notvalid_tab VALUES (NULL); +ALTER TABLE notnull_notvalid_tab ADD CONSTRAINT nn NOT NULL id NOT VALID; +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); + QUERY PLAN +---------------------------------------------------------- + Seq Scan on not_null_tab + Filter: (NOT (ANY (id = (hashed SubPlan any_1).col1))) + SubPlan any_1 + -> Seq Scan on notnull_notvalid_tab +(4 rows) + +-- NOT IN with NULL on inner side should return no rows +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); + id | val +----+----- +(0 rows) + +ROLLBACK; diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca293f..132b56a5864ca 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -29,7 +29,7 @@ select type, name, ident, level, total_bytes >= free_bytes (1 row) -- We can exercise some MemoryContext type stats functions. Most of the --- column values are too platform-dependant to display. +-- column values are too platform-dependent to display. -- Ensure stats from the bump allocator look sane. Bump isn't a commonly -- used context, but it is used in tuplesort.c, so open a cursor to keep -- the tuplesort alive long enough for us to query the context stats. @@ -143,6 +143,13 @@ select count(*) = 0 as ok from pg_stat_wal_receiver; t (1 row) +-- We expect no recovery state in this test (running on primary) +select count(*) = 0 as ok from pg_stat_recovery; + ok +---- + t +(1 row) + -- This is to record the prevailing planner enable_foo settings during -- a regression test run. select name, setting from pg_settings where name like 'enable%'; @@ -151,6 +158,7 @@ select name, setting from pg_settings where name like 'enable%'; enable_async_append | on enable_bitmapscan | on enable_distinct_reordering | on + enable_eager_aggregate | on enable_gathermerge | on enable_group_by_reordering | on enable_hashagg | on @@ -172,7 +180,7 @@ select name, setting from pg_settings where name like 'enable%'; enable_seqscan | on enable_sort | on enable_tidscan | on -(24 rows) +(25 rows) -- There are always wait event descriptions for various types. InjectionPoint -- may be present or absent, depending on history since last postmaster start. @@ -181,7 +189,7 @@ select type, count(*) > 0 as ok FROM pg_wait_events type | ok -----------+---- Activity | t - BufferPin | t + Buffer | t Client | t Extension | t IO | t diff --git a/src/test/regress/expected/tablespace.out b/src/test/regress/expected/tablespace.out index a90e39e57382b..f0dd25cdf0c28 100644 --- a/src/test/regress/expected/tablespace.out +++ b/src/test/regress/expected/tablespace.out @@ -355,8 +355,9 @@ Number of partitions: 2 (Use \d+ to list them.) Partition key: LIST (a) Indexes: "part_a_idx" btree (a), tablespace "regress_tblspace" -Partitions: testschema.part1 FOR VALUES IN (1), - testschema.part2 FOR VALUES IN (2) +Partitions: + testschema.part1 FOR VALUES IN (1) + testschema.part2 FOR VALUES IN (2) \d testschema.part1 Table "testschema.part1" @@ -392,8 +393,9 @@ Tablespace: "regress_tblspace" --------+---------+------+------------+---------+-------------- a | integer | yes | a | plain | btree, for table "testschema.part" -Partitions: testschema.part1_a_idx, - testschema.part2_a_idx +Partitions: + testschema.part1_a_idx + testschema.part2_a_idx Tablespace: "regress_tblspace" -- partitioned rels cannot specify the default tablespace. These fail: @@ -958,6 +960,11 @@ ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default NOTICE: no matching relations in tablespace "regress_tblspace_renamed" found ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; NOTICE: no matching relations in tablespace "regress_tblspace_renamed" found +-- Should fail, contains \n in name +ALTER TABLESPACE regress_tblspace_renamed RENAME TO "invalid +name"; +ERROR: tablespace name "invalid +name" contains a newline or carriage return character -- Should succeed DROP TABLESPACE regress_tblspace_renamed; DROP SCHEMA testschema CASCADE; diff --git a/src/test/regress/expected/tablespace_ddl.out b/src/test/regress/expected/tablespace_ddl.out new file mode 100644 index 0000000000000..e52043273a9c4 --- /dev/null +++ b/src/test/regress/expected/tablespace_ddl.out @@ -0,0 +1,84 @@ +-- +-- Tests for pg_get_tablespace_ddl() +-- +SET allow_in_place_tablespaces = true; +CREATE ROLE regress_tblspc_ddl_user; +-- error: non-existent tablespace by name +SELECT * FROM pg_get_tablespace_ddl('regress_nonexistent_tblsp'); +ERROR: tablespace "regress_nonexistent_tblsp" does not exist +-- error: non-existent tablespace by OID +SELECT * FROM pg_get_tablespace_ddl(0::oid); +ERROR: tablespace with OID 0 does not exist +-- NULL input returns no rows (name variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::name); + pg_get_tablespace_ddl +----------------------- +(0 rows) + +-- NULL input returns no rows (OID variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::oid); + pg_get_tablespace_ddl +----------------------- +(0 rows) + +-- tablespace name requiring quoting +CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT * FROM pg_get_tablespace_ddl('regress_ tblsp'); + pg_get_tablespace_ddl +------------------------------------------------------------------------------- + CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +(1 row) + +DROP TABLESPACE "regress_ tblsp"; +-- tablespace with multiple options +CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION '' + WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', + effective_io_concurrency = '17', maintenance_io_concurrency = '18'); +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp'); + pg_get_tablespace_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; + ALTER TABLESPACE regress_allopt_tblsp SET (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18'); +(2 rows) + +-- pretty-printed output +\pset format unaligned +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'pretty', 'true'); +pg_get_tablespace_ddl +CREATE TABLESPACE regress_allopt_tblsp + OWNER regress_tblspc_ddl_user + LOCATION ''; +ALTER TABLESPACE regress_allopt_tblsp SET (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18'); +(2 rows) +\pset format aligned +-- tablespace with owner suppressed +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'owner', 'false'); + pg_get_tablespace_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLESPACE regress_allopt_tblsp LOCATION ''; + ALTER TABLESPACE regress_allopt_tblsp SET (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18'); +(2 rows) + +DROP TABLESPACE regress_allopt_tblsp; +-- test by OID +CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT oid AS tsid FROM pg_tablespace WHERE spcname = 'regress_oid_tblsp' \gset +SELECT * FROM pg_get_tablespace_ddl(:tsid); + pg_get_tablespace_ddl +-------------------------------------------------------------------------------- + CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +(1 row) + +DROP TABLESPACE regress_oid_tblsp; +-- Permission check: revoke SELECT on pg_tablespace +CREATE TABLESPACE regress_acl_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +CREATE ROLE regress_tblspc_ddl_noaccess; +REVOKE SELECT ON pg_tablespace FROM PUBLIC; +SET ROLE regress_tblspc_ddl_noaccess; +SELECT * FROM pg_get_tablespace_ddl('regress_acl_tblsp'); -- should fail +ERROR: permission denied for tablespace regress_acl_tblsp +RESET ROLE; +GRANT SELECT ON pg_tablespace TO PUBLIC; +DROP TABLESPACE regress_acl_tblsp; +DROP ROLE regress_tblspc_ddl_noaccess; +DROP ROLE regress_tblspc_ddl_user; diff --git a/src/test/regress/expected/temp.out b/src/test/regress/expected/temp.out index 370361543b30c..a50c7ae88a9c8 100644 --- a/src/test/regress/expected/temp.out +++ b/src/test/regress/expected/temp.out @@ -229,7 +229,7 @@ select nonempty(''); ERROR: function nonempty(unknown) does not exist LINE 1: select nonempty(''); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: There is no function of that name. select pg_temp.nonempty(''); ERROR: value for domain nonempty violates check constraint "nonempty_check" -- other syntax matches rules for tables diff --git a/src/test/regress/expected/text.out b/src/test/regress/expected/text.out index 4c65b238e764c..3f9982388baf3 100644 --- a/src/test/regress/expected/text.out +++ b/src/test/regress/expected/text.out @@ -27,7 +27,8 @@ select length(42); ERROR: function length(integer) does not exist LINE 1: select length(42); ^ -HINT: No function matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No function of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- But as a special exception for usability's sake, we still allow implicit -- casting to text in concatenations, so long as the other input is text or -- an unknown literal. So these work: @@ -48,7 +49,8 @@ select 3 || 4.0; ERROR: operator does not exist: integer || numeric LINE 1: select 3 || 4.0; ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. /* * various string functions */ diff --git a/src/test/regress/expected/tid.out b/src/test/regress/expected/tid.out index 083c83a1e1b2c..3497a77688b51 100644 --- a/src/test/regress/expected/tid.out +++ b/src/test/regress/expected/tid.out @@ -42,6 +42,57 @@ SELECT * FROM pg_input_error_info('(0,-1)', 'tid'); invalid input syntax for type tid: "(0,-1)" | | | 22P02 (1 row) +-- tests for tid_block() and tid_offset() +SELECT tid_block('(0,0)'::tid), tid_offset('(0,0)'::tid); + tid_block | tid_offset +-----------+------------ + 0 | 0 +(1 row) + +SELECT tid_block('(0,1)'::tid), tid_offset('(0,1)'::tid); + tid_block | tid_offset +-----------+------------ + 0 | 1 +(1 row) + +SELECT tid_block('(42,7)'::tid), tid_offset('(42,7)'::tid); + tid_block | tid_offset +-----------+------------ + 42 | 7 +(1 row) + +-- max values: blockno uint32 max, offset uint16 max +SELECT tid_block('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid); + tid_block | tid_offset +------------+------------ + 4294967295 | 65535 +(1 row) + +-- (-1,0) wraps to blockno 4294967295 +SELECT tid_block('(-1,0)'::tid); + tid_block +------------ + 4294967295 +(1 row) + +-- NULL handling (strict functions) +SELECT tid_block(NULL::tid), tid_offset(NULL::tid); + tid_block | tid_offset +-----------+------------ + | +(1 row) + +-- round-trip: blockno + offset reconstruct the original TID +SELECT t, tid_block(t), tid_offset(t), + format('(%s,%s)', tid_block(t), tid_offset(t))::tid = t AS roundtrip_ok +FROM (VALUES ('(0,0)'::tid), ('(1,42)'::tid), ('(4294967295,65535)'::tid)) AS v(t); + t | tid_block | tid_offset | roundtrip_ok +--------------------+------------+------------+-------------- + (0,0) | 0 | 0 | t + (1,42) | 1 | 42 | t + (4294967295,65535) | 4294967295 | 65535 | t +(3 rows) + -- tests for functions related to TID handling CREATE TABLE tid_tab (a int); -- min() and max() for TIDs @@ -58,6 +109,21 @@ SELECT max(ctid) FROM tid_tab; (0,2) (1 row) +-- tid_block() and tid_offset() with real table ctid +SELECT ctid, tid_block(ctid), tid_offset(ctid) FROM tid_tab; + ctid | tid_block | tid_offset +-------+-----------+------------ + (0,1) | 0 | 1 + (0,2) | 0 | 2 +(2 rows) + +-- use in WHERE clause +SELECT ctid FROM tid_tab WHERE tid_block(ctid) = 0 AND tid_offset(ctid) = 1; + ctid +------- + (0,1) +(1 row) + TRUNCATE tid_tab; -- Tests for currtid2() with various relation kinds -- Materialized view diff --git a/src/test/regress/expected/tidrangescan.out b/src/test/regress/expected/tidrangescan.out index 721f3b94e0423..89930f7bc7c6d 100644 --- a/src/test/regress/expected/tidrangescan.out +++ b/src/test/regress/expected/tidrangescan.out @@ -297,4 +297,109 @@ FETCH LAST c; COMMIT; DROP TABLE tidrangescan; +-- Tests for parallel TID Range Scans +BEGIN; +SET LOCAL parallel_setup_cost TO 0; +SET LOCAL parallel_tuple_cost TO 0; +SET LOCAL min_parallel_table_scan_size TO 0; +SET LOCAL max_parallel_workers_per_gather TO 4; +CREATE TABLE parallel_tidrangescan (id integer, data text) +WITH (fillfactor = 10); +-- Insert enough tuples such that each page gets 5 tuples with fillfactor = 10 +INSERT INTO parallel_tidrangescan +SELECT i, repeat('x', 100) FROM generate_series(1,200) AS s(i); +-- Ensure there are 40 pages for parallel test +SELECT min(ctid), max(ctid) FROM parallel_tidrangescan; + min | max +-------+-------- + (0,1) | (39,5) +(1 row) + +-- Parallel range scans with upper bound +EXPLAIN (COSTS OFF) +SELECT count(*) FROM parallel_tidrangescan WHERE ctid < '(30,1)'; + QUERY PLAN +-------------------------------------------------------------------- + Finalize Aggregate + -> Gather + Workers Planned: 4 + -> Partial Aggregate + -> Parallel Tid Range Scan on parallel_tidrangescan + TID Cond: (ctid < '(30,1)'::tid) +(6 rows) + +SELECT count(*) FROM parallel_tidrangescan WHERE ctid < '(30,1)'; + count +------- + 150 +(1 row) + +-- Parallel range scans with lower bound +EXPLAIN (COSTS OFF) +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)'; + QUERY PLAN +-------------------------------------------------------------------- + Finalize Aggregate + -> Gather + Workers Planned: 4 + -> Partial Aggregate + -> Parallel Tid Range Scan on parallel_tidrangescan + TID Cond: (ctid > '(10,0)'::tid) +(6 rows) + +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)'; + count +------- + 150 +(1 row) + +-- Parallel range scans with both bounds +EXPLAIN (COSTS OFF) +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)' AND ctid < '(30,1)'; + QUERY PLAN +----------------------------------------------------------------------------------- + Finalize Aggregate + -> Gather + Workers Planned: 4 + -> Partial Aggregate + -> Parallel Tid Range Scan on parallel_tidrangescan + TID Cond: ((ctid > '(10,0)'::tid) AND (ctid < '(30,1)'::tid)) +(6 rows) + +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)' AND ctid < '(30,1)'; + count +------- + 100 +(1 row) + +-- Parallel rescans +EXPLAIN (COSTS OFF) +SELECT t.ctid,t2.c FROM parallel_tidrangescan t, +LATERAL (SELECT count(*) c FROM parallel_tidrangescan t2 WHERE t2.ctid <= t.ctid) t2 +WHERE t.ctid < '(1,0)'; + QUERY PLAN +---------------------------------------------------------------- + Nested Loop + -> Gather + Workers Planned: 4 + -> Parallel Tid Range Scan on parallel_tidrangescan t + TID Cond: (ctid < '(1,0)'::tid) + -> Aggregate + -> Tid Range Scan on parallel_tidrangescan t2 + TID Cond: (ctid <= t.ctid) +(8 rows) + +SELECT t.ctid,t2.c FROM parallel_tidrangescan t, +LATERAL (SELECT count(*) c FROM parallel_tidrangescan t2 WHERE t2.ctid <= t.ctid) t2 +WHERE t.ctid < '(1,0)'; + ctid | c +-------+--- + (0,1) | 1 + (0,2) | 2 + (0,3) | 3 + (0,4) | 4 + (0,5) | 5 +(5 rows) + +ROLLBACK; RESET enable_seqscan; diff --git a/src/test/regress/expected/time.out b/src/test/regress/expected/time.out index 4247fae9412b1..765adeb6e515c 100644 --- a/src/test/regress/expected/time.out +++ b/src/test/regress/expected/time.out @@ -157,7 +157,8 @@ SELECT f1 + time '00:01' AS "Illegal" FROM TIME_TBL; ERROR: operator is not unique: time without time zone + time without time zone LINE 1: SELECT f1 + time '00:01' AS "Illegal" FROM TIME_TBL; ^ -HINT: Could not choose a best candidate operator. You might need to add explicit type casts. +DETAIL: Could not choose a best candidate operator. +HINT: You might need to add explicit type casts. -- -- test EXTRACT -- diff --git a/src/test/regress/expected/timestamp.out b/src/test/regress/expected/timestamp.out index 6aaa19c8f4e46..14a9f5b56a690 100644 --- a/src/test/regress/expected/timestamp.out +++ b/src/test/regress/expected/timestamp.out @@ -591,6 +591,16 @@ SELECT date_trunc( 'week', timestamp '2004-02-29 15:44:17.71393' ) AS week_trunc Mon Feb 23 00:00:00 2004 (1 row) +SELECT date_trunc( 'week', timestamp 'infinity' ) AS inf_trunc; + inf_trunc +----------- + infinity +(1 row) + +SELECT date_trunc( 'timezone', timestamp '2004-02-29 15:44:17.71393' ) AS notsupp_trunc; +ERROR: unit "timezone" not supported for type timestamp without time zone +SELECT date_trunc( 'timezone', timestamp 'infinity' ) AS notsupp_inf_trunc; +ERROR: unit "timezone" not supported for type timestamp without time zone SELECT date_trunc( 'ago', timestamp 'infinity' ) AS invalid_trunc; ERROR: unit "ago" not recognized for type timestamp without time zone -- verify date_bin behaves the same as date_trunc for relevant intervals diff --git a/src/test/regress/expected/timestamptz.out b/src/test/regress/expected/timestamptz.out index 2a69953ff25ed..5dc8a621f6c07 100644 --- a/src/test/regress/expected/timestamptz.out +++ b/src/test/regress/expected/timestamptz.out @@ -760,6 +760,16 @@ SELECT date_trunc( 'week', timestamp with time zone '2004-02-29 15:44:17.71393' Mon Feb 23 00:00:00 2004 PST (1 row) +SELECT date_trunc( 'week', timestamp with time zone 'infinity' ) AS inf_trunc; + inf_trunc +----------- + infinity +(1 row) + +SELECT date_trunc( 'timezone', timestamp with time zone '2004-02-29 15:44:17.71393' ) AS notsupp_trunc; +ERROR: unit "timezone" not supported for type timestamp with time zone +SELECT date_trunc( 'timezone', timestamp with time zone 'infinity' ) AS notsupp_inf_trunc; +ERROR: unit "timezone" not supported for type timestamp with time zone SELECT date_trunc( 'ago', timestamp with time zone 'infinity' ) AS invalid_trunc; ERROR: unit "ago" not recognized for type timestamp with time zone SELECT date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'Australia/Sydney') as sydney_trunc; -- zone name @@ -780,6 +790,14 @@ SELECT date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET Thu Feb 15 20:00:00 2001 PST (1 row) +SELECT date_trunc('timezone', timestamp with time zone 'infinity', 'GMT') AS notsupp_zone_trunc; +ERROR: unit "timezone" not supported for type timestamp with time zone +SELECT date_trunc( 'week', timestamp with time zone 'infinity', 'GMT') AS inf_zone_trunc; + inf_zone_trunc +---------------- + infinity +(1 row) + SELECT date_trunc('ago', timestamp with time zone 'infinity', 'GMT') AS invalid_zone_trunc; ERROR: unit "ago" not recognized for type timestamp with time zone -- verify date_bin behaves the same as date_trunc for relevant intervals diff --git a/src/test/regress/expected/timetz.out b/src/test/regress/expected/timetz.out index cbab6cfe5d7f1..324b1a740e86b 100644 --- a/src/test/regress/expected/timetz.out +++ b/src/test/regress/expected/timetz.out @@ -174,7 +174,8 @@ SELECT f1 + time with time zone '00:01' AS "Illegal" FROM TIMETZ_TBL; ERROR: operator does not exist: time with time zone + time with time zone LINE 1: SELECT f1 + time with time zone '00:01' AS "Illegal" FROM TI... ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- -- test EXTRACT -- diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index f245d7f154924..511e7cfb6ce36 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -1670,7 +1670,7 @@ drop table trigger_ddl_table; drop function trigger_ddl_func(); -- -- Verify behavior of before and after triggers with INSERT...ON CONFLICT --- DO UPDATE +-- DO UPDATE and DO SELECT -- create table upsert (key int4 primary key, color text); create function upsert_before_func() @@ -1745,6 +1745,14 @@ insert into upsert values(8, 'yellow') on conflict (key) do update set color = ' WARNING: before insert (new): (8,yellow) WARNING: before insert (new, modified): (9,"yellow trig modified") WARNING: after insert (new): (9,"yellow trig modified") +insert into upsert values(8, 'blue') on conflict (key) do select for update where upsert.color = 'yellow trig modified' returning old.*, new.*, upsert.*; +WARNING: before insert (new): (8,blue) +WARNING: before insert (new, modified): (9,"blue trig modified") + key | color | key | color | key | color +-----+----------------------+-----+----------------------+-----+---------------------- + 9 | yellow trig modified | 9 | yellow trig modified | 9 | yellow trig modified +(1 row) + select * from upsert; key | color -----+----------------------------- @@ -2280,6 +2288,27 @@ select * from parted; drop table parted; drop function parted_trigfunc(); -- +-- Constraint triggers +-- +create constraint trigger crtr + after insert on foo not valid + for each row execute procedure foo (); +ERROR: constraint triggers cannot be marked NOT VALID +LINE 2: after insert on foo not valid + ^ +create constraint trigger crtr + after insert on foo no inherit + for each row execute procedure foo (); +ERROR: constraint triggers cannot be marked NO INHERIT +LINE 2: after insert on foo no inherit + ^ +create constraint trigger crtr + after insert on foo not enforced + for each row execute procedure foo (); +ERROR: constraint triggers cannot be marked NOT ENFORCED +LINE 2: after insert on foo not enforced + ^ +-- -- Constraint triggers and partitioned tables create table parted_constr_ancestor (a int, b text) partition by range (b); @@ -2294,7 +2323,7 @@ create constraint trigger parted_trig after insert on parted_constr_ancestor deferrable for each row execute procedure trigger_notice_ab(); create constraint trigger parted_trig_two after insert on parted_constr - deferrable initially deferred + deferrable initially deferred enforced for each row when (bark(new.b) AND new.a % 2 = 1) execute procedure trigger_notice_ab(); -- The immediate constraint is fired immediately; the WHEN clause of the @@ -2748,6 +2777,10 @@ NOTICE: trigger = child3_delete_trig, old table = (42,CCC) -- copy into parent sees parent-format tuples copy parent (a, b) from stdin; NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42) +-- check detach/reattach behavior; statement triggers with transition tables +-- should not prevent a table from becoming a partition again +alter table parent detach partition child1; +alter table parent attach partition child1 for values in ('AAA'); -- DML affecting parent sees tuples collected from children even if -- there is no transition table trigger on the children drop trigger child1_insert_trig on child1; @@ -2945,6 +2978,10 @@ NOTICE: trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42) create index on parent(b); copy parent (a, b) from stdin; NOTICE: trigger = parent_insert_trig, new table = (DDD,42) +-- check disinherit/reinherit behavior; statement triggers with transition +-- tables should not prevent a table from becoming an inheritance child again +alter table child1 no inherit parent; +alter table child1 inherit parent; -- DML affecting parent sees tuples collected from children even if -- there is no transition table trigger on the children drop trigger child1_insert_trig on child1; @@ -3004,13 +3041,19 @@ NOTICE: trigger = table1_trig, new table = (42) with wcte as (insert into table1 values (43)) insert into table1 values (44); NOTICE: trigger = table1_trig, new table = (43), (44) +with wcte as (insert into table1 values (45)) + merge into table1 using (values (46)) as v(a) on table1.a = v.a + when not matched then insert values (v.a); +NOTICE: trigger = table1_trig, new table = (45), (46) select * from table1; a ---- 42 44 43 -(3 rows) + 46 + 45 +(5 rows) select * from table2; a @@ -3529,14 +3572,15 @@ alter trigger parenttrig on parent rename to anothertrig; a | integer | | | | plain | | Triggers: parenttrig AFTER INSERT ON child FOR EACH ROW EXECUTE FUNCTION f() -Inherits: parent +Inherits: + parent drop table parent, child; drop function f(); -- Test who runs deferred trigger functions -- setup -create role regress_groot; -create role regress_outis; +create role regress_caller; +create role regress_fn_owner; create function whoami() returns trigger language plpgsql as $$ begin @@ -3544,7 +3588,7 @@ begin return null; end; $$; -alter function whoami() owner to regress_outis; +alter function whoami() owner to regress_fn_owner; create table defer_trig (id integer); grant insert on defer_trig to public; create constraint trigger whoami after insert on defer_trig @@ -3553,23 +3597,23 @@ create constraint trigger whoami after insert on defer_trig execute function whoami(); -- deferred triggers must run as the user that queued the trigger begin; -set role regress_groot; +set role regress_caller; insert into defer_trig values (1); reset role; -set role regress_outis; +set role regress_fn_owner; insert into defer_trig values (2); reset role; commit; -NOTICE: I am regress_groot -NOTICE: I am regress_outis +NOTICE: I am regress_caller +NOTICE: I am regress_fn_owner -- security definer functions override the user who queued the trigger alter function whoami() security definer; begin; -set role regress_groot; +set role regress_caller; insert into defer_trig values (3); reset role; commit; -NOTICE: I am regress_outis +NOTICE: I am regress_fn_owner alter function whoami() security invoker; -- make sure the current user is restored after error create or replace function whoami() returns trigger language plpgsql @@ -3581,11 +3625,11 @@ begin end; $$; begin; -set role regress_groot; +set role regress_caller; insert into defer_trig values (4); reset role; commit; -- error expected -NOTICE: I am regress_groot +NOTICE: I am regress_caller ERROR: division by zero CONTEXT: SQL statement "SELECT 1 / 0" PL/pgSQL function whoami() line 4 at PERFORM @@ -3598,5 +3642,5 @@ select current_user = session_user; -- clean up drop table defer_trig; drop function whoami(); -drop role regress_outis; -drop role regress_groot; +drop role regress_fn_owner; +drop role regress_caller; diff --git a/src/test/regress/expected/tsearch.out b/src/test/regress/expected/tsearch.out index 9fad6c8b04b1e..9287c440709f4 100644 --- a/src/test/regress/expected/tsearch.out +++ b/src/test/regress/expected/tsearch.out @@ -870,6 +870,7 @@ RESET enable_seqscan; RESET enable_indexscan; RESET enable_bitmapscan; DROP INDEX wowidx; +ALTER TABLE test_tsvector SET (parallel_workers = 2); CREATE INDEX wowidx ON test_tsvector USING gin (a); SET enable_seqscan=OFF; -- GIN only supports bitmapscan, so no need to test plain indexscan diff --git a/src/test/regress/expected/tsrf.out b/src/test/regress/expected/tsrf.out index d47b5f6ec5745..c4f7b187f5baf 100644 --- a/src/test/regress/expected/tsrf.out +++ b/src/test/regress/expected/tsrf.out @@ -91,8 +91,9 @@ SELECT unnest(ARRAY[1, 2]) FROM few WHERE false; ProjectSet Output: unnest('{1,2}'::integer[]) -> Result + Replaces: Scan on few One-Time Filter: false -(4 rows) +(5 rows) SELECT unnest(ARRAY[1, 2]) FROM few WHERE false; unnest @@ -107,8 +108,9 @@ SELECT * FROM few f1, ------------------------------------------------ Result Output: f1.id, f1.dataa, f1.datab, ss.unnest + Replaces: Join on f1, ss One-Time Filter: false -(3 rows) +(4 rows) SELECT * FROM few f1, (SELECT unnest(ARRAY[1,2]) FROM few f2 WHERE false OFFSET 0) ss; diff --git a/src/test/regress/expected/tuplesort.out b/src/test/regress/expected/tuplesort.out index 6dd97e7427ae1..fc1321bf44338 100644 --- a/src/test/regress/expected/tuplesort.out +++ b/src/test/regress/expected/tuplesort.out @@ -304,9 +304,9 @@ FROM abbrev_abort_uuids ORDER BY ctid DESC LIMIT 5; id | abort_increasing | abort_decreasing | noabort_increasing | noabort_decreasing -------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------------- - 0 | | | | 20002 | | | | 20003 | | | | + 0 | | | | 10009 | 00000000-0000-0000-0000-000000010008 | 00000000-0000-0000-0000-000000009992 | 00010008-0000-0000-0000-000000010008 | 00009992-0000-0000-0000-000000009992 10008 | 00000000-0000-0000-0000-000000010007 | 00000000-0000-0000-0000-000000009993 | 00010007-0000-0000-0000-000000010007 | 00009993-0000-0000-0000-000000009993 (5 rows) @@ -335,9 +335,9 @@ FROM abbrev_abort_uuids ORDER BY ctid DESC LIMIT 5; id | abort_increasing | abort_decreasing | noabort_increasing | noabort_decreasing -------+--------------------------------------+--------------------------------------+--------------------------------------+-------------------------------------- - 0 | | | | - 20003 | | | | 20002 | | | | + 20003 | | | | + 0 | | | | 9993 | 00000000-0000-0000-0000-000000009992 | 00000000-0000-0000-0000-000000010008 | 00009992-0000-0000-0000-000000009992 | 00010008-0000-0000-0000-000000010008 9994 | 00000000-0000-0000-0000-000000009993 | 00000000-0000-0000-0000-000000010007 | 00009993-0000-0000-0000-000000009993 | 00010007-0000-0000-0000-000000010007 (5 rows) diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out index dd0c52ab08b50..1d21d3eb44678 100644 --- a/src/test/regress/expected/type_sanity.out +++ b/src/test/regress/expected/type_sanity.out @@ -610,7 +610,9 @@ WHERE (is_catalog_text_unique_index_oid(indexrelid) <> -- Look for illegal values in pg_range fields. SELECT r.rngtypid, r.rngsubtype FROM pg_range as r -WHERE r.rngtypid = 0 OR r.rngsubtype = 0 OR r.rngsubopc = 0; +WHERE r.rngtypid = 0 OR r.rngsubtype = 0 OR r.rngsubopc = 0 + OR r.rngconstruct2 = 0 OR r.rngconstruct3 = 0 + OR r.rngmltconstruct0 = 0 OR r.rngmltconstruct1 = 0 OR r.rngmltconstruct2 = 0; rngtypid | rngsubtype ----------+------------ (0 rows) @@ -663,6 +665,61 @@ WHERE r.rngmultitypid IS NULL OR r.rngmultitypid = 0; ----------+------------+--------------- (0 rows) +-- check constructor function arguments and return types +-- +-- proname and prosrc are not required to have these particular +-- values, but this matches what DefineRange() produces and serves to +-- sanity-check the catalog entries for built-in types. +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngconstruct2 JOIN pg_type t ON r.rngtypid = t.oid +WHERE p.pronargs != 2 + OR p.proargtypes[0] != r.rngsubtype OR p.proargtypes[1] != r.rngsubtype + OR p.prorettype != r.rngtypid + OR p.proname != t.typname OR p.prosrc != 'range_constructor2'; + rngtypid | rngsubtype | proname +----------+------------+--------- +(0 rows) + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngconstruct3 JOIN pg_type t ON r.rngtypid = t.oid +WHERE p.pronargs != 3 + OR p.proargtypes[0] != r.rngsubtype OR p.proargtypes[1] != r.rngsubtype OR p.proargtypes[2] != 'pg_catalog.text'::regtype + OR p.prorettype != r.rngtypid + OR p.proname != t.typname OR p.prosrc != 'range_constructor3'; + rngtypid | rngsubtype | proname +----------+------------+--------- +(0 rows) + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct0 JOIN pg_type t ON r.rngmultitypid = t.oid +WHERE p.pronargs != 0 + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor0'; + rngtypid | rngsubtype | proname +----------+------------+--------- +(0 rows) + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct1 JOIN pg_type t ON r.rngmultitypid = t.oid +WHERE p.pronargs != 1 + OR p.proargtypes[0] != r.rngtypid + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor1'; + rngtypid | rngsubtype | proname +----------+------------+--------- +(0 rows) + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct2 JOIN pg_type t ON r.rngmultitypid = t.oid JOIN pg_type t2 ON r.rngtypid = t2.oid +WHERE p.pronargs != 1 + OR p.proargtypes[0] != t2.typarray + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor2'; + rngtypid | rngsubtype | proname +----------+------------+--------- +(0 rows) + +-- ****************************************** -- Create a table that holds all the known in-core data types and leave it -- around so as pg_upgrade is able to test their binary compatibility. CREATE TABLE tab_core_types AS SELECT @@ -702,6 +759,7 @@ CREATE TABLE tab_core_types AS SELECT 'abc'::refcursor, '1 2'::int2vector, '1 2'::oidvector, + '1234'::oid8, format('%I=UC/%I', USER, USER)::aclitem AS aclitem, 'a fat cat sat on a mat and ate a fat rat'::tsvector, 'fat & rat'::tsquery, @@ -711,6 +769,7 @@ CREATE TABLE tab_core_types AS SELECT 'regtype'::regtype type, 'pg_monitor'::regrole, 'pg_class'::regclass::oid, + 'template1'::regdatabase, '(1,1)'::tid, '2'::xid, '3'::cid, '10:20:10,14,15'::txid_snapshot, '10:20:10,14,15'::pg_snapshot, diff --git a/src/test/regress/expected/typed_table.out b/src/test/regress/expected/typed_table.out index 885f085e15421..c0a06bf91458d 100644 --- a/src/test/regress/expected/typed_table.out +++ b/src/test/regress/expected/typed_table.out @@ -41,7 +41,9 @@ ERROR: cannot alter column type of typed table LINE 1: ALTER TABLE persons ALTER COLUMN name TYPE varchar; ^ CREATE TABLE stuff (id int); -ALTER TABLE persons INHERIT stuff; +ALTER TABLE persons INHERIT stuff; -- error +ERROR: cannot change inheritance of typed table +ALTER TABLE persons NO INHERIT stuff; -- error ERROR: cannot change inheritance of typed table CREATE TABLE personsx OF person_type (myname WITH OPTIONS NOT NULL); -- error ERROR: column "myname" does not exist diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out index 96962817ed45a..3a49b35405837 100644 --- a/src/test/regress/expected/union.out +++ b/src/test/regress/expected/union.out @@ -942,7 +942,7 @@ SELECT q1 FROM int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1; ERROR: column "q2" does not exist LINE 1: ... int8_tbl EXCEPT SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1... ^ -DETAIL: There is a column named "q2" in table "*SELECT* 2", but it cannot be referenced from this part of the query. +DETAIL: There is a column named "q2" in table "unnamed_subquery", but it cannot be referenced from this part of the query. -- But this should work: SELECT q1 FROM int8_tbl EXCEPT (((SELECT q2 FROM int8_tbl ORDER BY q2 LIMIT 1))) ORDER BY 1; q1 @@ -1216,6 +1216,167 @@ select event_id drop table events_child, events, other_events; reset enable_indexonlyscan; +-- +-- Test handling of UNION / EXCEPT / INTERSECT with provably empty inputs +-- +-- Ensure the empty UNION input is pruned and de-duplication is done for the +-- remaining relation. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +UNION +SELECT four FROM tenk1 +ORDER BY 1; + QUERY PLAN +-------------------------------------- + Sort + Output: tenk1.four + Sort Key: tenk1.four + -> HashAggregate + Output: tenk1.four + Group Key: tenk1.four + -> Seq Scan on public.tenk1 + Output: tenk1.four +(8 rows) + +-- Validate that the results of the above are correct +SELECT two FROM tenk1 WHERE 1=2 +UNION +SELECT four FROM tenk1 +ORDER BY 1; + two +----- + 0 + 1 + 2 + 3 +(4 rows) + +-- All UNION inputs are proven empty. Ensure the planner provides a +-- const-false Result node +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +UNION +SELECT four FROM tenk1 WHERE 1=2 +UNION +SELECT ten FROM tenk1 WHERE 1=2 +ORDER BY 1; + QUERY PLAN +----------------------------------------------------------------------------------------- + Sort + Output: unnamed_subquery.two + Sort Key: unnamed_subquery.two + -> Result + Output: unnamed_subquery.two + Replaces: Aggregate on unnamed_subquery, unnamed_subquery_1, unnamed_subquery_2 + One-Time Filter: false +(7 rows) + +-- Ensure the planner provides a const-false Result node +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +INTERSECT +SELECT four FROM tenk1 +ORDER BY 1; + QUERY PLAN +--------------------------------------------------------------------- + Sort + Output: unnamed_subquery.two + Sort Key: unnamed_subquery.two + -> Result + Output: unnamed_subquery.two + Replaces: Aggregate on unnamed_subquery, unnamed_subquery_1 + One-Time Filter: false +(7 rows) + +-- As above, with the inputs swapped +EXPLAIN (COSTS OFF, VERBOSE) +SELECT four FROM tenk1 +INTERSECT +SELECT two FROM tenk1 WHERE 1=2 +ORDER BY 1; + QUERY PLAN +--------------------------------------------------------------------- + Sort + Output: unnamed_subquery.four + Sort Key: unnamed_subquery.four + -> Result + Output: unnamed_subquery.four + Replaces: Aggregate on unnamed_subquery, unnamed_subquery_1 + One-Time Filter: false +(7 rows) + +-- Try with both inputs dummy +EXPLAIN (COSTS OFF, VERBOSE) +SELECT four FROM tenk1 WHERE 1=2 +INTERSECT +SELECT two FROM tenk1 WHERE 1=2 +ORDER BY 1; + QUERY PLAN +--------------------------------------------------------------------- + Sort + Output: unnamed_subquery.four + Sort Key: unnamed_subquery.four + -> Result + Output: unnamed_subquery.four + Replaces: Aggregate on unnamed_subquery, unnamed_subquery_1 + One-Time Filter: false +(7 rows) + +-- Ensure the planner provides a const-false Result node when the left input +-- is empty +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +EXCEPT +SELECT four FROM tenk1 +ORDER BY 1; + QUERY PLAN +--------------------------------------------------------------------- + Sort + Output: unnamed_subquery.two + Sort Key: unnamed_subquery.two + -> Result + Output: unnamed_subquery.two + Replaces: Aggregate on unnamed_subquery, unnamed_subquery_1 + One-Time Filter: false +(7 rows) + +-- Ensure the planner only scans the left input when right input is empty +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 +EXCEPT ALL +SELECT four FROM tenk1 WHERE 1=2 +ORDER BY 1; + QUERY PLAN +-------------------------------- + Sort + Output: tenk1.two + Sort Key: tenk1.two + -> Seq Scan on public.tenk1 + Output: tenk1.two +(5 rows) + +-- Try a mixed setop case. Ensure the right-hand UNION child gets removed. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 t1 +EXCEPT +SELECT four FROM tenk1 t2 +UNION +SELECT ten FROM tenk1 dummy WHERE 1=2; + QUERY PLAN +----------------------------------------------- + Unique + Output: t1.two + -> Sort + Output: t1.two + Sort Key: t1.two + -> HashSetOp Except + Output: t1.two + -> Seq Scan on public.tenk1 t1 + Output: t1.two + -> Seq Scan on public.tenk1 t2 + Output: t2.four +(11 rows) + -- Test constraint exclusion of UNION ALL subqueries explain (costs off) SELECT * FROM @@ -1338,14 +1499,14 @@ where q2 = q2; ---------------------------------------------------------- Unique -> Merge Append - Sort Key: "*SELECT* 1".q1 - -> Subquery Scan on "*SELECT* 1" + Sort Key: unnamed_subquery.q1 + -> Subquery Scan on unnamed_subquery -> Unique -> Sort Sort Key: i81.q1, i81.q2 -> Seq Scan on int8_tbl i81 Filter: (q2 IS NOT NULL) - -> Subquery Scan on "*SELECT* 2" + -> Subquery Scan on unnamed_subquery_1 -> Unique -> Sort Sort Key: i82.q1, i82.q2 @@ -1374,14 +1535,14 @@ where -q1 = q2; -------------------------------------------------------- Unique -> Merge Append - Sort Key: "*SELECT* 1".q1 - -> Subquery Scan on "*SELECT* 1" + Sort Key: unnamed_subquery.q1 + -> Subquery Scan on unnamed_subquery -> Unique -> Sort Sort Key: i81.q1, i81.q2 -> Seq Scan on int8_tbl i81 Filter: ((- q1) = q2) - -> Subquery Scan on "*SELECT* 2" + -> Subquery Scan on unnamed_subquery_1 -> Unique -> Sort Sort Key: i82.q1, i82.q2 @@ -1500,3 +1661,20 @@ on true limit 1; -> Result (6 rows) +-- Test handling of Vars with varno 0 in estimate_array_length +explain (verbose, costs off) +select null::int[] union all select null::int[] union all select null::bigint[]; + QUERY PLAN +--------------------------------------------- + Append + -> Result + Output: (NULL::integer[]) + -> Append + -> Result + Output: NULL::integer[] + -> Result + Output: NULL::integer[] + -> Result + Output: NULL::bigint[] +(10 rows) + diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 095df0a670c24..8852160718fbf 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -316,6 +316,21 @@ SELECT * FROM rw_view15; 3 | UNSPECIFIED (6 rows) +INSERT INTO rw_view15 (a) VALUES (3) + ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED' + RETURNING old, new; + old | new +-----------------+----------------- + (3,UNSPECIFIED) | (3,UNSPECIFIED) +(1 row) + +INSERT INTO rw_view15 (a) VALUES (3) + ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING old, new; + old | new +-----------------+----------------- + (3,UNSPECIFIED) | (3,UNSPECIFIED) +(1 row) + SELECT * FROM rw_view15; a | upper ----+------------- @@ -2750,7 +2765,7 @@ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5); --------------------------------------------------------- Insert on base_tbl b -> Result - SubPlan 1 + SubPlan exists_1 -> Index Only Scan using ref_tbl_pkey on ref_tbl r Index Cond: (a = b.a) (5 rows) @@ -2764,7 +2779,7 @@ EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5; -> Seq Scan on base_tbl b -> Hash -> Seq Scan on ref_tbl r - SubPlan 1 + SubPlan exists_1 -> Index Only Scan using ref_tbl_pkey on ref_tbl r_1 Index Cond: (a = b.a) (9 rows) @@ -3167,21 +3182,21 @@ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data); DELETE FROM rw_view1 WHERE id = 1 AND snoop(data); NOTICE: snooped value: Row 1 EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2'); - QUERY PLAN ------------------------------------------------------------ + QUERY PLAN +----------------------------------------------------------------- Insert on base_tbl - InitPlan 1 + InitPlan exists_1 -> Index Only Scan using base_tbl_pkey on base_tbl t Index Cond: (id = 2) -> Result - One-Time Filter: ((InitPlan 1).col1 IS NOT TRUE) + One-Time Filter: ((InitPlan exists_1).col1 IS NOT TRUE) Update on base_tbl - InitPlan 1 + InitPlan exists_1 -> Index Only Scan using base_tbl_pkey on base_tbl t Index Cond: (id = 2) -> Result - One-Time Filter: (InitPlan 1).col1 + One-Time Filter: (InitPlan exists_1).col1 -> Index Scan using base_tbl_pkey on base_tbl Index Cond: (id = 2) (15 rows) @@ -3240,8 +3255,8 @@ SELECT * FROM v1 WHERE a=8; EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6; - QUERY PLAN ------------------------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------------------------ Update on public.t1 Update on public.t1 t1_1 Update on public.t11 t1_2 @@ -3253,8 +3268,8 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6; -> Index Scan using t1_a_idx on public.t1 t1_1 Output: t1_1.tableoid, t1_1.ctid Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7)) - Filter: ((t1_1.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a)) - SubPlan 1 + Filter: ((t1_1.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_1.a) AND leakproof(t1_1.a)) + SubPlan exists_1 -> Append -> Seq Scan on public.t12 t12_1 Filter: (t12_1.a = t1_1.a) @@ -3263,15 +3278,15 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6; -> Index Scan using t11_a_idx on public.t11 t1_2 Output: t1_2.tableoid, t1_2.ctid Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7)) - Filter: ((t1_2.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a)) + Filter: ((t1_2.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_2.a) AND leakproof(t1_2.a)) -> Index Scan using t12_a_idx on public.t12 t1_3 Output: t1_3.tableoid, t1_3.ctid Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7)) - Filter: ((t1_3.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a)) + Filter: ((t1_3.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_3.a) AND leakproof(t1_3.a)) -> Index Scan using t111_a_idx on public.t111 t1_4 Output: t1_4.tableoid, t1_4.ctid Index Cond: ((t1_4.a > 5) AND (t1_4.a < 7)) - Filter: ((t1_4.a <> 6) AND EXISTS(SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a)) + Filter: ((t1_4.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_4.a) AND leakproof(t1_4.a)) (30 rows) UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6; @@ -3287,8 +3302,8 @@ SELECT * FROM t1 WHERE a=100; -- Nothing should have been changed to 100 EXPLAIN (VERBOSE, COSTS OFF) UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8; - QUERY PLAN ------------------------------------------------------------------------------------------ + QUERY PLAN +------------------------------------------------------------------------------------------------ Update on public.t1 Update on public.t1 t1_1 Update on public.t11 t1_2 @@ -3300,8 +3315,8 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8; -> Index Scan using t1_a_idx on public.t1 t1_1 Output: t1_1.a, t1_1.tableoid, t1_1.ctid Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8)) - Filter: (EXISTS(SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a)) - SubPlan 1 + Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_1.a) AND leakproof(t1_1.a)) + SubPlan exists_1 -> Append -> Seq Scan on public.t12 t12_1 Filter: (t12_1.a = t1_1.a) @@ -3310,15 +3325,15 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8; -> Index Scan using t11_a_idx on public.t11 t1_2 Output: t1_2.a, t1_2.tableoid, t1_2.ctid Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8)) - Filter: (EXISTS(SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a)) + Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_2.a) AND leakproof(t1_2.a)) -> Index Scan using t12_a_idx on public.t12 t1_3 Output: t1_3.a, t1_3.tableoid, t1_3.ctid Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8)) - Filter: (EXISTS(SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a)) + Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_3.a) AND leakproof(t1_3.a)) -> Index Scan using t111_a_idx on public.t111 t1_4 Output: t1_4.a, t1_4.tableoid, t1_4.ctid Index Cond: ((t1_4.a > 5) AND (t1_4.a = 8)) - Filter: (EXISTS(SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a)) + Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_4.a) AND leakproof(t1_4.a)) (30 rows) UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8; @@ -3502,10 +3517,10 @@ CREATE RULE v1_upd_rule AS ON UPDATE TO v1 DO INSTEAD CREATE VIEW v2 WITH (security_barrier = true) AS SELECT * FROM v1 WHERE EXISTS (SELECT 1); EXPLAIN (COSTS OFF) UPDATE v2 SET a = 1; - QUERY PLAN --------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------- Update on t1 - InitPlan 1 + InitPlan exists_1 -> Result -> Merge Join Merge Cond: (t1.a = v1.a) @@ -3516,7 +3531,7 @@ EXPLAIN (COSTS OFF) UPDATE v2 SET a = 1; Sort Key: v1.a -> Subquery Scan on v1 -> Result - One-Time Filter: (InitPlan 1).col1 + One-Time Filter: (InitPlan exists_1).col1 -> Seq Scan on t1 t1_1 (14 rows) @@ -3646,7 +3661,7 @@ ERROR: new row violates check option for view "wcowrtest_v2" DETAIL: Failing row contains (2, no such row in sometable). drop view wcowrtest_v, wcowrtest_v2; drop table wcowrtest, sometable; --- Check INSERT .. ON CONFLICT DO UPDATE works correctly when the view's +-- Check INSERT .. ON CONFLICT DO SELECT/UPDATE works correctly when the view's -- columns are named and ordered differently than the underlying table's. create table uv_iocu_tab (a text unique, b float); insert into uv_iocu_tab values ('xyxyxy', 0); @@ -3668,6 +3683,13 @@ select * from uv_iocu_tab; xyxyxy | 1 (1 row) +insert into uv_iocu_view (a, b) values ('xyxyxy', 1) + on conflict (a) do select where uv_iocu_view.c = 2 and excluded.c = 2 returning *; + b | c | a | two +---+---+--------+----- + 1 | 2 | xyxyxy | 2.0 +(1 row) + -- OK to access view columns that are not present in underlying base -- relation in the ON CONFLICT portion of the query insert into uv_iocu_view (a, b) values ('xyxyxy', 3) @@ -3700,6 +3722,38 @@ select * from uv_iocu_tab; drop view uv_iocu_view; drop table uv_iocu_tab; +-- Check UPDATE FOR PORTION OF works correctly +create table uv_fpo_tab (id int4range, valid_at tsrange, b float, + constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps)); +insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0); +create view uv_fpo_view as + select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab; +insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1); +select * from uv_fpo_view order by id, valid_at; + b | c | valid_at | id | two +---+---+---------------------------------------------------------+-------+----- + 1 | 2 | ["Fri Jan 01 00:00:00 2010","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0 + 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0 +(2 rows) + +update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]'; +select * from uv_fpo_view order by id, valid_at; + b | c | valid_at | id | two +---+---+---------------------------------------------------------+-------+----- + 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0 + 2 | 3 | ["Thu Jan 01 00:00:00 2015","Wed Jan 01 00:00:00 2020") | [1,2) | 2.0 + 0 | 1 | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0 +(3 rows) + +delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]'; +select * from uv_fpo_view order by id, valid_at; + b | c | valid_at | id | two +---+---+---------------------------------------------------------+-------+----- + 1 | 2 | ["Fri Jan 01 00:00:00 2010","Thu Jan 01 00:00:00 2015") | [1,2) | 2.0 + 2 | 3 | ["Thu Jan 01 00:00:00 2015","Sun Jan 01 00:00:00 2017") | [1,2) | 2.0 + 0 | 1 | ["Sat Jan 01 00:00:00 2022","Tue Jan 01 00:00:00 2030") | [1,2) | 2.0 +(3 rows) + -- Test whole-row references to the view create table uv_iocu_tab (a int unique, b text); create view uv_iocu_view as @@ -3731,6 +3785,25 @@ select * from uv_iocu_view; Rejected: (y,1,"(1,y)") | 1 | (1,"Rejected: (y,1,""(1,y)"")") (1 row) +explain (costs off) +insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")') + on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------- + Insert on uv_iocu_tab + Conflict Resolution: SELECT + Conflict Arbiter Indexes: uv_iocu_tab_a_key + Conflict Filter: (ROW(uv_iocu_tab.b, uv_iocu_tab.a, (uv_iocu_tab.*)::text) = ROW(excluded.b, excluded.a, (excluded.*)::text)) + -> Result +(5 rows) + +insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")') + on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *; + bb | aa | cc +-------------------------+----+--------------------------------- + Rejected: (y,1,"(1,y)") | 1 | (1,"Rejected: (y,1,""(1,y)"")") +(1 row) + -- Test omitting a column of the base relation delete from uv_iocu_view; insert into uv_iocu_view (aa,bb) values (1,'x'); @@ -3751,6 +3824,13 @@ select * from uv_iocu_view; Rejected: ("table default",1,"(1,""table default"")") | 1 | (1,"Rejected: (""table default"",1,""(1,""""table default"""")"")") (1 row) +insert into uv_iocu_view (aa) values (1) + on conflict (aa) do select returning *; + bb | aa | cc +-------------------------------------------------------+----+--------------------------------------------------------------------- + Rejected: ("table default",1,"(1,""table default"")") | 1 | (1,"Rejected: (""table default"",1,""(1,""""table default"""")"")") +(1 row) + alter view uv_iocu_view alter column bb set default 'view default'; insert into uv_iocu_view (aa) values (1) on conflict (aa) do update set bb = 'Rejected: '||excluded.*; @@ -3760,6 +3840,13 @@ select * from uv_iocu_view; Rejected: ("view default",1,"(1,""view default"")") | 1 | (1,"Rejected: (""view default"",1,""(1,""""view default"""")"")") (1 row) +insert into uv_iocu_view (aa) values (1) + on conflict (aa) do select returning *; + bb | aa | cc +-----------------------------------------------------+----+------------------------------------------------------------------- + Rejected: ("view default",1,"(1,""view default"")") | 1 | (1,"Rejected: (""view default"",1,""(1,""""view default"""")"")") +(1 row) + -- Should fail to update non-updatable columns insert into uv_iocu_view (aa) values (1) on conflict (aa) do update set cc = 'XXX'; @@ -3767,7 +3854,7 @@ ERROR: cannot insert into column "cc" of view "uv_iocu_view" DETAIL: View columns that are not columns of their base relation are not updatable. drop view uv_iocu_view; drop table uv_iocu_tab; --- ON CONFLICT DO UPDATE permissions checks +-- ON CONFLICT DO SELECT/UPDATE permissions checks create user regress_view_user1; create user regress_view_user2; set session authorization regress_view_user1; @@ -3791,6 +3878,16 @@ insert into rw_view1 values ('zzz',2.0,1) insert into rw_view1 values ('zzz',2.0,1) on conflict (aa) do update set cc = 3.0; -- Not allowed ERROR: permission denied for view rw_view1 +insert into rw_view1 values ('yyy',2.0,1) + on conflict (aa) do select for update returning cc; -- Not allowed +ERROR: permission denied for view rw_view1 +insert into rw_view1 values ('yyy',2.0,1) + on conflict (aa) do select for update returning aa, bb; + aa | bb +----+-------- + 1 | yyyxxx +(1 row) + reset session authorization; select * from base_tbl; a | b | c @@ -3807,9 +3904,19 @@ create view rw_view2 as select b as bb, c as cc, a as aa from base_tbl; insert into rw_view2 (aa,bb) values (1,'xxx') on conflict (aa) do update set bb = excluded.bb; -- Not allowed ERROR: permission denied for table base_tbl +insert into rw_view2 (aa,bb) values (1,'xxx') + on conflict (aa) do select returning 1; -- Not allowed +ERROR: permission denied for table base_tbl create view rw_view3 as select b as bb, a as aa from base_tbl; insert into rw_view3 (aa,bb) values (1,'xxx') on conflict (aa) do update set bb = excluded.bb; -- OK +insert into rw_view3 (aa,bb) values (1,'xxx') + on conflict (aa) do select returning aa, bb; -- OK + aa | bb +----+----- + 1 | xxx +(1 row) + reset session authorization; select * from base_tbl; a | b | c @@ -3822,6 +3929,9 @@ create view rw_view4 as select aa, bb, cc FROM rw_view1; insert into rw_view4 (aa,bb) values (1,'yyy') on conflict (aa) do update set bb = excluded.bb; -- Not allowed ERROR: permission denied for view rw_view1 +insert into rw_view4 (aa,bb) values (1,'yyy') + on conflict (aa) do select returning 1; -- Not allowed +ERROR: permission denied for view rw_view1 create view rw_view5 as select aa, bb FROM rw_view1; insert into rw_view5 (aa,bb) values (1,'yyy') on conflict (aa) do update set bb = excluded.bb; -- OK diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out index 1b27d132d7ba1..eef2bac1cbf2f 100644 --- a/src/test/regress/expected/update.out +++ b/src/test/regress/expected/update.out @@ -178,15 +178,15 @@ EXPLAIN (VERBOSE, COSTS OFF) UPDATE update_test t SET (a, b) = (SELECT b, a FROM update_test s WHERE s.a = t.a) WHERE CURRENT_USER = SESSION_USER; - QUERY PLAN --------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------- Update on public.update_test t -> Result - Output: (SubPlan 1).col1, (SubPlan 1).col2, (rescan SubPlan 1), t.ctid + Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), t.ctid One-Time Filter: (CURRENT_USER = SESSION_USER) -> Seq Scan on public.update_test t Output: t.a, t.ctid - SubPlan 1 + SubPlan multiexpr_1 -> Seq Scan on public.update_test s Output: s.b, s.a Filter: (s.a = t.a) diff --git a/src/test/regress/expected/uuid.out b/src/test/regress/expected/uuid.out index 95392003b86ed..9c5dda9e9abb1 100644 --- a/src/test/regress/expected/uuid.out +++ b/src/test/regress/expected/uuid.out @@ -13,7 +13,8 @@ CREATE TABLE guid2 CREATE TABLE guid3 ( id SERIAL, - guid_field UUID + guid_field UUID, + guid_encoded text GENERATED ALWAYS AS (encode(guid_field::bytea, 'base32hex')) STORED ); -- inserting invalid data tests -- too long @@ -226,11 +227,23 @@ SELECT count(DISTINCT guid_field) FROM guid1; (1 row) -- test sortability of v7 +INSERT INTO guid3 (guid_field) VALUES ('00000000-0000-0000-0000-000000000000'::uuid); INSERT INTO guid3 (guid_field) SELECT uuidv7() FROM generate_series(1, 10); +INSERT INTO guid3 (guid_field) VALUES ('ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid); SELECT array_agg(id ORDER BY guid_field) FROM guid3; - array_agg ------------------------- - {1,2,3,4,5,6,7,8,9,10} + array_agg +------------------------------ + {1,2,3,4,5,6,7,8,9,10,11,12} +(1 row) + +-- Test base32hex encoding of UUIDs and its lexicographical sorting property. +-- COLLATE "C" is required to prevent buildfarm failures in non-C locales +-- where natural language collations (such as cs_CZ) would break strict +-- byte-wise ordering. +SELECT array_agg(id ORDER BY guid_encoded COLLATE "C") FROM guid3; + array_agg +------------------------------ + {1,2,3,4,5,6,7,8,9,10,11,12} (1 row) -- Check the timestamp offsets for v7. @@ -305,5 +318,27 @@ SELECT uuid_extract_timestamp('11111111-1111-1111-1111-111111111111'); -- null (1 row) +-- casts +SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid::bytea; + bytea +------------------------------------ + \x5b35380a714349129b55f322699c6770 +(1 row) + +SELECT '\x019a2f859ced7225b99d9c55044a2563'::bytea::uuid; + uuid +-------------------------------------- + 019a2f85-9ced-7225-b99d-9c55044a2563 +(1 row) + +SELECT '\x1234567890abcdef'::bytea::uuid; -- error +ERROR: invalid input length for type uuid +DETAIL: Expected 16 bytes, got 8. +SELECT v = v::bytea::uuid as matched FROM gen_random_uuid() v; + matched +--------- + t +(1 row) + -- clean up DROP TABLE guid1, guid2, guid3 CASCADE; diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out index 0abcc99989e07..d4696bc332557 100644 --- a/src/test/regress/expected/vacuum.out +++ b/src/test/regress/expected/vacuum.out @@ -161,16 +161,14 @@ VACUUM (PARALLEL 2) pvactst; UPDATE pvactst SET i = i WHERE i < 1000; VACUUM (PARALLEL 0) pvactst; -- disable parallel vacuum VACUUM (PARALLEL -1) pvactst; -- error -ERROR: parallel workers for vacuum must be between 0 and 1024 +ERROR: PARALLEL option must be between 0 and 1024 LINE 1: VACUUM (PARALLEL -1) pvactst; ^ VACUUM (PARALLEL 2, INDEX_CLEANUP FALSE) pvactst; VACUUM (PARALLEL 2, FULL TRUE) pvactst; -- error, cannot use both PARALLEL and FULL ERROR: VACUUM FULL cannot be performed in parallel VACUUM (PARALLEL) pvactst; -- error, cannot use PARALLEL option without parallel degree -ERROR: parallel option requires a value between 0 and 1024 -LINE 1: VACUUM (PARALLEL) pvactst; - ^ +ERROR: parallel requires an integer value -- Test parallel vacuum using the minimum maintenance_work_mem with and without -- dead tuples. SET maintenance_work_mem TO 64; @@ -686,3 +684,49 @@ RESET ROLE; DROP TABLE vacowned; DROP TABLE vacowned_parted; DROP ROLE regress_vacuum; +-- Test checking how new toast values are allocated on rewrite. +-- Create table with plain storage (forces inline storage initially). +CREATE TABLE vac_rewrite_toast (id int, f1 TEXT STORAGE plain); +-- Insert tuple large enough to trigger toast storage on rewrite, still +-- small enough to fit on a page. +INSERT INTO vac_rewrite_toast values (1, repeat('a', 7000)); +-- Switch to external storage to force toast table usage. +ALTER TABLE vac_rewrite_toast ALTER COLUMN f1 SET STORAGE EXTERNAL; +-- This second tuple is toasted, its value should still be the +-- same after rewrite. +INSERT INTO vac_rewrite_toast values (2, repeat('a', 7000)); +SELECT pg_column_toast_chunk_id(f1) AS id_2_chunk FROM vac_rewrite_toast + WHERE id = 2 \gset +-- Check initial state of the data. +SELECT id, pg_column_toast_chunk_id(f1) IS NULL AS f1_chunk_null, + substr(f1, 5, 10) AS f1_data, + pg_column_compression(f1) AS f1_comp + FROM vac_rewrite_toast ORDER BY id; + id | f1_chunk_null | f1_data | f1_comp +----+---------------+------------+--------- + 1 | t | aaaaaaaaaa | + 2 | f | aaaaaaaaaa | +(2 rows) + +-- VACUUM FULL forces toast data rewrite. +VACUUM FULL vac_rewrite_toast; +-- Check after rewrite. +SELECT id, pg_column_toast_chunk_id(f1) IS NULL AS f1_chunk_null, + substr(f1, 5, 10) AS f1_data, + pg_column_compression(f1) AS f1_comp + FROM vac_rewrite_toast ORDER BY id; + id | f1_chunk_null | f1_data | f1_comp +----+---------------+------------+--------- + 1 | f | aaaaaaaaaa | + 2 | f | aaaaaaaaaa | +(2 rows) + +-- The same value is reused for the tuple toasted before the rewrite. +SELECT pg_column_toast_chunk_id(f1) = :'id_2_chunk' AS same_chunk + FROM vac_rewrite_toast WHERE id = 2; + same_chunk +------------ + t +(1 row) + +DROP TABLE vac_rewrite_toast; diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out index b86b668f433d0..e6aac27a2a93e 100644 --- a/src/test/regress/expected/window.out +++ b/src/test/regress/expected/window.out @@ -1361,6 +1361,97 @@ SELECT pg_get_viewdef('v_window'); FROM generate_series(now(), (now() + '@ 100 days'::interval), '@ 1 hour'::interval) i(i); (1 row) +-- test overflow frame specifications +SELECT sum(unique1) over (rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + 41 | 4 | 0 + 39 | 2 | 2 + 38 | 1 | 1 + 32 | 6 | 2 + 23 | 9 | 1 + 15 | 8 | 0 + 10 | 5 | 1 + 7 | 3 | 3 + 0 | 7 | 3 + | 0 | 0 +(10 rows) + +SELECT sum(unique1) over (rows between 9223372036854775807 following and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + | 4 | 0 + | 2 | 2 + | 1 | 1 + | 6 | 2 + | 9 | 1 + | 8 | 0 + | 5 | 1 + | 3 | 3 + | 7 | 3 + | 0 | 0 +(10 rows) + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + last_value | unique1 | four +------------+---------+------ + 7 | 0 | 0 + 7 | 8 | 0 + 7 | 4 | 0 + 7 | 5 | 1 + 7 | 9 | 1 + 7 | 1 | 1 + 7 | 6 | 2 + 7 | 2 | 2 + 7 | 3 | 3 + | 7 | 3 +(10 rows) + +-- These test GROUPS mode with an offset large enough to cause overflow when +-- added to currentgroup. Although the overflow doesn't produce visibly wrong +-- results (due to the incremental nature of group pointer advancement), we +-- still need to protect against it as signed integer overflow is undefined +-- behavior in C. +SELECT sum(unique1) over (ORDER BY four groups between current row and 9223372036854775807 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + 45 | 0 | 0 + 45 | 8 | 0 + 45 | 4 | 0 + 33 | 5 | 1 + 33 | 9 | 1 + 33 | 1 | 1 + 18 | 6 | 2 + 18 | 2 | 2 + 10 | 3 | 3 + 10 | 7 | 3 +(10 rows) + +SELECT sum(unique1) over (ORDER BY four groups between 9223372036854775807 following and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + sum | unique1 | four +-----+---------+------ + | 0 | 0 + | 8 | 0 + | 4 | 0 + | 5 | 1 + | 9 | 1 + | 1 | 1 + | 6 | 2 + | 2 | 2 + | 3 | 3 + | 7 | 3 +(10 rows) + -- RANGE offset PRECEDING/FOLLOWING tests SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding), unique1, four @@ -4250,14 +4341,14 @@ SELECT 1 FROM (SELECT ntile(s1.x) OVER () AS c FROM (SELECT (SELECT 1) AS x) AS s1) s WHERE s.c = 1; - QUERY PLAN ----------------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------------- Subquery Scan on s Filter: (s.c = 1) -> WindowAgg Window: w1 AS (ROWS UNBOUNDED PRECEDING) - Run Condition: (ntile((InitPlan 1).col1) OVER w1 <= 1) - InitPlan 1 + Run Condition: (ntile((InitPlan expr_1).col1) OVER w1 <= 1) + InitPlan expr_1 -> Result -> Result (8 rows) @@ -4338,7 +4429,7 @@ WHERE c = 1; Filter: (emp.c = 1) -> WindowAgg Window: w1 AS (ORDER BY empsalary.empno) - InitPlan 1 + InitPlan expr_1 -> Result -> Sort Sort Key: empsalary.empno DESC @@ -4537,6 +4628,22 @@ WHERE first_emp = 1 OR last_emp = 1; sales | 4 | 4800 | 08-08-2007 | 3 | 1 (6 rows) +CREATE INDEX empsalary_salary_empno_idx ON empsalary (salary, empno); +SET enable_seqscan = 0; +-- Ensure no sorting is done and that the IndexScan maintains all pathkeys +-- useful for the final sort order. +EXPLAIN (COSTS OFF) +SELECT salary, empno, row_number() OVER (ORDER BY salary) rn +FROM empsalary +ORDER BY salary, empno; + QUERY PLAN +--------------------------------------------------------------------- + WindowAgg + Window: w1 AS (ORDER BY salary ROWS UNBOUNDED PRECEDING) + -> Index Only Scan using empsalary_salary_empno_idx on empsalary +(3 rows) + +RESET enable_seqscan; -- cleanup DROP TABLE empsalary; -- test user-defined window function with named args and default args @@ -5453,3 +5560,410 @@ SELECT * FROM pg_temp.f(2); {5} (5 rows) +-- IGNORE NULLS tests +CREATE TEMPORARY TABLE planets ( + name text, + distance text, + orbit integer +); +INSERT INTO planets VALUES + ('mercury', 'close', 88), + ('venus', 'close', 224), + ('earth', 'close', NULL), + ('mars', 'close', NULL), + ('jupiter', 'close', 4332), + ('saturn', 'far', 24491), + ('uranus', 'far', NULL), + ('neptune', 'far', 60182), + ('pluto', 'far', 90560), + ('xyzzy', 'far', NULL); +-- test ruleutils +CREATE VIEW planets_view AS +SELECT name, + orbit, + lag(orbit) OVER w AS lag, + lag(orbit) RESPECT NULLS OVER w AS lag_respect, + lag(orbit) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name) +; +NOTICE: view "planets_view" will be a temporary view +DETAIL: It depends on temporary table planets. +SELECT pg_get_viewdef('planets_view'); + pg_get_viewdef +-------------------------------------------------- + SELECT name, + + orbit, + + lag(orbit) OVER w AS lag, + + lag(orbit) OVER w AS lag_respect, + + lag(orbit) IGNORE NULLS OVER w AS lag_ignore+ + FROM planets + + WINDOW w AS (ORDER BY name); +(1 row) + +-- lag +SELECT name, + orbit, + lag(orbit) OVER w AS lag, + lag(orbit) RESPECT NULLS OVER w AS lag_respect, + lag(orbit) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name) +; + name | orbit | lag | lag_respect | lag_ignore +---------+-------+-------+-------------+------------ + earth | | | | + jupiter | 4332 | | | + mars | | 4332 | 4332 | 4332 + mercury | 88 | | | 4332 + neptune | 60182 | 88 | 88 | 88 + pluto | 90560 | 60182 | 60182 | 60182 + saturn | 24491 | 90560 | 90560 | 90560 + uranus | | 24491 | 24491 | 24491 + venus | 224 | | | 24491 + xyzzy | | 224 | 224 | 224 +(10 rows) + +-- lead +SELECT name, + orbit, + lead(orbit) OVER w AS lead, + lead(orbit) RESPECT NULLS OVER w AS lead_respect, + lead(orbit) IGNORE NULLS OVER w AS lead_ignore +FROM planets +WINDOW w AS (ORDER BY name) +; + name | orbit | lead | lead_respect | lead_ignore +---------+-------+-------+--------------+------------- + earth | | 4332 | 4332 | 4332 + jupiter | 4332 | | | 88 + mars | | 88 | 88 | 88 + mercury | 88 | 60182 | 60182 | 60182 + neptune | 60182 | 90560 | 90560 | 90560 + pluto | 90560 | 24491 | 24491 | 24491 + saturn | 24491 | | | 224 + uranus | | 224 | 224 | 224 + venus | 224 | | | + xyzzy | | | | +(10 rows) + +-- first_value +SELECT name, + orbit, + first_value(orbit) RESPECT NULLS OVER w1, + first_value(orbit) IGNORE NULLS OVER w1, + first_value(orbit) RESPECT NULLS OVER w2, + first_value(orbit) IGNORE NULLS OVER w2 +FROM planets +WINDOW w1 AS (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), + w2 AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + name | orbit | first_value | first_value | first_value | first_value +---------+-------+-------------+-------------+-------------+------------- + earth | | | 4332 | | 4332 + jupiter | 4332 | | 4332 | | 4332 + mars | | | 4332 | | 4332 + mercury | 88 | | 4332 | 4332 | 4332 + neptune | 60182 | | 4332 | | 88 + pluto | 90560 | | 4332 | 88 | 88 + saturn | 24491 | | 4332 | 60182 | 60182 + uranus | | | 4332 | 90560 | 90560 + venus | 224 | | 4332 | 24491 | 24491 + xyzzy | | | 4332 | | 224 +(10 rows) + +-- nth_value +SELECT name, + orbit, + nth_value(orbit, 2) RESPECT NULLS OVER w1, + nth_value(orbit, 2) IGNORE NULLS OVER w1, + nth_value(orbit, 2) RESPECT NULLS OVER w2, + nth_value(orbit, 2) IGNORE NULLS OVER w2 +FROM planets +WINDOW w1 AS (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), + w2 AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + name | orbit | nth_value | nth_value | nth_value | nth_value +---------+-------+-----------+-----------+-----------+----------- + earth | | 4332 | 88 | 4332 | + jupiter | 4332 | 4332 | 88 | 4332 | 88 + mars | | 4332 | 88 | 4332 | 88 + mercury | 88 | 4332 | 88 | | 88 + neptune | 60182 | 4332 | 88 | 88 | 60182 + pluto | 90560 | 4332 | 88 | 60182 | 60182 + saturn | 24491 | 4332 | 88 | 90560 | 90560 + uranus | | 4332 | 88 | 24491 | 24491 + venus | 224 | 4332 | 88 | | 224 + xyzzy | | 4332 | 88 | 224 | +(10 rows) + +-- last_value +SELECT name, + orbit, + last_value(orbit) RESPECT NULLS OVER w1, + last_value(orbit) IGNORE NULLS OVER w1, + last_value(orbit) RESPECT NULLS OVER w2, + last_value(orbit) IGNORE NULLS OVER w2 +FROM planets +WINDOW w1 AS (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), + w2 AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + name | orbit | last_value | last_value | last_value | last_value +---------+-------+------------+------------+------------+------------ + earth | | | 224 | | 4332 + jupiter | 4332 | | 224 | 88 | 88 + mars | | | 224 | 60182 | 60182 + mercury | 88 | | 224 | 90560 | 90560 + neptune | 60182 | | 224 | 24491 | 24491 + pluto | 90560 | | 224 | | 24491 + saturn | 24491 | | 224 | 224 | 224 + uranus | | | 224 | | 224 + venus | 224 | | 224 | | 224 + xyzzy | | | 224 | | 224 +(10 rows) + +-- exclude current row +SELECT name, + orbit, + first_value(orbit) IGNORE NULLS OVER w, + last_value(orbit) IGNORE NULLS OVER w, + nth_value(orbit, 2) IGNORE NULLS OVER w, + lead(orbit, 1) IGNORE NULLS OVER w AS lead_ignore, + lag(orbit, 1) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW) +; + name | orbit | first_value | last_value | nth_value | lead_ignore | lag_ignore +---------+-------+-------------+------------+-----------+-------------+------------ + earth | | 4332 | 4332 | | 4332 | + jupiter | 4332 | 88 | 88 | | 88 | + mars | | 4332 | 60182 | 88 | 88 | 4332 + mercury | 88 | 4332 | 90560 | 60182 | 60182 | 4332 + neptune | 60182 | 88 | 24491 | 90560 | 90560 | 88 + pluto | 90560 | 88 | 24491 | 60182 | 24491 | 60182 + saturn | 24491 | 60182 | 224 | 90560 | 224 | 90560 + uranus | | 90560 | 224 | 24491 | 224 | 24491 + venus | 224 | 24491 | 24491 | | | 24491 + xyzzy | | 224 | 224 | | | 224 +(10 rows) + +-- valid and invalid functions +SELECT sum(orbit) OVER () FROM planets; -- succeeds + sum +-------- + 179877 + 179877 + 179877 + 179877 + 179877 + 179877 + 179877 + 179877 + 179877 + 179877 +(10 rows) + +SELECT sum(orbit) RESPECT NULLS OVER () FROM planets; -- fails +ERROR: aggregate functions do not accept RESPECT/IGNORE NULLS +LINE 1: SELECT sum(orbit) RESPECT NULLS OVER () FROM planets; + ^ +SELECT sum(orbit) IGNORE NULLS OVER () FROM planets; -- fails +ERROR: aggregate functions do not accept RESPECT/IGNORE NULLS +LINE 1: SELECT sum(orbit) IGNORE NULLS OVER () FROM planets; + ^ +SELECT row_number() OVER () FROM planets; -- succeeds + row_number +------------ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 +(10 rows) + +SELECT row_number() RESPECT NULLS OVER () FROM planets; -- fails +ERROR: function row_number does not allow RESPECT/IGNORE NULLS +SELECT row_number() IGNORE NULLS OVER () FROM planets; -- fails +ERROR: function row_number does not allow RESPECT/IGNORE NULLS +SELECT rank() OVER () FROM planets; -- succeeds + rank +------ + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +(10 rows) + +SELECT rank() RESPECT NULLS OVER () FROM planets; -- fails +ERROR: function rank does not allow RESPECT/IGNORE NULLS +SELECT rank() IGNORE NULLS OVER () FROM planets; -- fails +ERROR: function rank does not allow RESPECT/IGNORE NULLS +SELECT dense_rank() OVER () FROM planets; -- succeeds + dense_rank +------------ + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +(10 rows) + +SELECT dense_rank() RESPECT NULLS OVER () FROM planets; -- fails +ERROR: function dense_rank does not allow RESPECT/IGNORE NULLS +SELECT dense_rank() IGNORE NULLS OVER () FROM planets; -- fails +ERROR: function dense_rank does not allow RESPECT/IGNORE NULLS +SELECT percent_rank() OVER () FROM planets; -- succeeds + percent_rank +-------------- + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +(10 rows) + +SELECT percent_rank() RESPECT NULLS OVER () FROM planets; -- fails +ERROR: function percent_rank does not allow RESPECT/IGNORE NULLS +SELECT percent_rank() IGNORE NULLS OVER () FROM planets; -- fails +ERROR: function percent_rank does not allow RESPECT/IGNORE NULLS +SELECT cume_dist() OVER () FROM planets; -- succeeds + cume_dist +----------- + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +(10 rows) + +SELECT cume_dist() RESPECT NULLS OVER () FROM planets; -- fails +ERROR: function cume_dist does not allow RESPECT/IGNORE NULLS +SELECT cume_dist() IGNORE NULLS OVER () FROM planets; -- fails +ERROR: function cume_dist does not allow RESPECT/IGNORE NULLS +SELECT ntile(1) OVER () FROM planets; -- succeeds + ntile +------- + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +(10 rows) + +SELECT ntile(1) RESPECT NULLS OVER () FROM planets; -- fails +ERROR: function ntile does not allow RESPECT/IGNORE NULLS +SELECT ntile(1) IGNORE NULLS OVER () FROM planets; -- fails +ERROR: function ntile does not allow RESPECT/IGNORE NULLS +-- test two consecutive nulls +update planets set orbit=null where name='jupiter'; +SELECT name, + orbit, + first_value(orbit) IGNORE NULLS OVER w, + last_value(orbit) IGNORE NULLS OVER w, + nth_value(orbit, 2) IGNORE NULLS OVER w, + lead(orbit, 1) IGNORE NULLS OVER w AS lead_ignore, + lag(orbit, 1) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + name | orbit | first_value | last_value | nth_value | lead_ignore | lag_ignore +---------+-------+-------------+------------+-----------+-------------+------------ + earth | | | | | 88 | + jupiter | | 88 | 88 | | 88 | + mars | | 88 | 60182 | 60182 | 88 | + mercury | 88 | 88 | 90560 | 60182 | 60182 | + neptune | 60182 | 88 | 24491 | 60182 | 90560 | 88 + pluto | 90560 | 88 | 24491 | 60182 | 24491 | 60182 + saturn | 24491 | 60182 | 224 | 90560 | 224 | 90560 + uranus | | 90560 | 224 | 24491 | 224 | 24491 + venus | 224 | 24491 | 224 | 224 | | 24491 + xyzzy | | 224 | 224 | | | 224 +(10 rows) + +-- test partitions +SELECT name, + distance, + orbit, + first_value(orbit) IGNORE NULLS OVER w, + last_value(orbit) IGNORE NULLS OVER w, + nth_value(orbit, 2) IGNORE NULLS OVER w, + lead(orbit, 1) IGNORE NULLS OVER w AS lead_ignore, + lag(orbit, 1) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (PARTITION BY distance ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + name | distance | orbit | first_value | last_value | nth_value | lead_ignore | lag_ignore +---------+----------+-------+-------------+------------+-----------+-------------+------------ + earth | close | | | | | 88 | + jupiter | close | | 88 | 88 | | 88 | + mars | close | | 88 | 224 | 224 | 88 | + mercury | close | 88 | 88 | 224 | 224 | 224 | + venus | close | 224 | 88 | 224 | 224 | | 88 + neptune | far | 60182 | 60182 | 24491 | 90560 | 90560 | + pluto | far | 90560 | 60182 | 24491 | 90560 | 24491 | 60182 + saturn | far | 24491 | 60182 | 24491 | 90560 | | 90560 + uranus | far | | 90560 | 24491 | 24491 | | 24491 + xyzzy | far | | 24491 | 24491 | | | 24491 +(10 rows) + +-- nth_value without nulls +SELECT x, + nth_value(x,2) IGNORE NULLS OVER w +FROM generate_series(1,5) g(x) +WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW); + x | nth_value +---+----------- + 1 | 3 + 2 | 3 + 3 | 2 + 4 | 3 + 5 | 4 +(5 rows) + +SELECT x, + nth_value(x,2) IGNORE NULLS OVER w +FROM generate_series(1,5) g(x) +WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING); + x | nth_value +---+----------- + 1 | 2 + 2 | 2 + 3 | 2 + 4 | 3 + 5 | 4 +(5 rows) + +--cleanup +DROP TABLE planets CASCADE; +NOTICE: drop cascades to view planets_view diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out index 26c8850514007..77ded01b0460a 100644 --- a/src/test/regress/expected/with.out +++ b/src/test/regress/expected/with.out @@ -175,7 +175,8 @@ SELECT n, pg_typeof(n) FROM t; ERROR: operator does not exist: text + integer LINE 4: SELECT n+1 FROM t WHERE n < 10 ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- Deeply nested WITH caused a list-munging problem in v13 -- Detection of cross-references and self-references WITH RECURSIVE w1(c1) AS @@ -2296,6 +2297,48 @@ from int4_tbl; -2147483647 (5 rows) +-- +-- test for bug #19055: interaction of WITH with aggregates +-- +-- For now, we just throw an error if there's a use of a CTE below the +-- semantic level that the SQL standard assigns to the aggregate. +-- It's not entirely clear what we could do instead that doesn't risk +-- breaking more things than it fixes. +select f1, (with cte1(x,y) as (select 1,2) + select count((select i4.f1 from cte1))) as ss +from int4_tbl i4; +ERROR: outer-level aggregate cannot use a nested CTE +LINE 2: select count((select i4.f1 from cte1))) as ss + ^ +DETAIL: CTE "cte1" is below the aggregate's semantic level. +-- +-- test for bug #19106: interaction of WITH with aggregates +-- +-- the initial fix for #19055 was too aggressive and broke this case +explain (verbose, costs off) +with a as ( select id from (values (1), (2)) as v(id) ), + b as ( select max((select sum(id) from a)) as agg ) +select agg from b; + QUERY PLAN +-------------------------------------------- + Aggregate + Output: max((InitPlan expr_1).col1) + InitPlan expr_1 + -> Aggregate + Output: sum("*VALUES*".column1) + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1 + -> Result +(8 rows) + +with a as ( select id from (values (1), (2)) as v(id) ), + b as ( select max((select sum(id) from a)) as agg ) +select agg from b; + agg +----- + 3 +(1 row) + -- -- test for nested-recursive-WITH bug -- @@ -2829,6 +2872,47 @@ SELECT * FROM bug6051_3; --- (0 rows) +-- check that recursive CTE processing doesn't rewrite a CTE more than once +-- (must not try to expand GENERATED ALWAYS IDENTITY columns more than once) +CREATE TEMP TABLE id_alw1 (i int GENERATED ALWAYS AS IDENTITY); +CREATE TEMP TABLE id_alw2 (i int GENERATED ALWAYS AS IDENTITY); +CREATE TEMP VIEW id_alw2_view AS SELECT * FROM id_alw2; +CREATE TEMP TABLE id_alw3 (i int GENERATED ALWAYS AS IDENTITY); +CREATE RULE id_alw3_ins AS ON INSERT TO id_alw3 DO INSTEAD + WITH t1 AS (INSERT INTO id_alw1 DEFAULT VALUES RETURNING i) + INSERT INTO id_alw2_view DEFAULT VALUES RETURNING i; +CREATE TEMP VIEW id_alw3_view AS SELECT * FROM id_alw3; +CREATE TEMP TABLE id_alw4 (i int GENERATED ALWAYS AS IDENTITY); +WITH t4 AS (INSERT INTO id_alw4 DEFAULT VALUES RETURNING i) + INSERT INTO id_alw3_view DEFAULT VALUES RETURNING i; + i +--- + 1 +(1 row) + +SELECT * from id_alw1; + i +--- + 1 +(1 row) + +SELECT * from id_alw2; + i +--- + 1 +(1 row) + +SELECT * from id_alw3; + i +--- +(0 rows) + +SELECT * from id_alw4; + i +--- + 1 +(1 row) + -- check case where CTE reference is removed due to optimization EXPLAIN (VERBOSE, COSTS OFF) SELECT q1 FROM @@ -3168,7 +3252,7 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.k, o.v); Output: o.k, o.v, o.* -> Result Output: 0, 'merge source SubPlan'::text - SubPlan 2 + SubPlan expr_1 -> Limit Output: ((cte_basic.b || ' merge update'::text)) -> CTE Scan on cte_basic @@ -3200,7 +3284,7 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.k, o.v); CTE cte_init -> Result Output: 1, 'cte_init val'::text - InitPlan 2 + InitPlan expr_1 -> Limit Output: ((cte_init.b || ' merge update'::text)) -> CTE Scan on cte_init @@ -3243,11 +3327,11 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.a, o.b || (SELECT merge_source_cte.*::text CTE merge_source_cte -> Result Output: 15, 'merge_source_cte val'::text - InitPlan 2 + InitPlan expr_1 -> CTE Scan on merge_source_cte merge_source_cte_1 Output: ((merge_source_cte_1.b || (merge_source_cte_1.*)::text) || ' merge update'::text) Filter: (merge_source_cte_1.a = 15) - InitPlan 3 + InitPlan expr_2 -> CTE Scan on merge_source_cte merge_source_cte_2 Output: ((merge_source_cte_2.*)::text || ' merge insert'::text) -> Hash Right Join diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out index ea607bed0a412..de2f8bc4786f2 100644 --- a/src/test/regress/expected/without_overlaps.out +++ b/src/test/regress/expected/without_overlaps.out @@ -2,7 +2,7 @@ -- -- We leave behind several tables to test pg_dump etc: -- temporal_rng, temporal_rng2, --- temporal_fk_rng2rng. +-- temporal_fk_rng2rng, temporal_fk2_rng2rng. SET datestyle TO ISO, YMD; -- -- test input parser @@ -80,7 +80,8 @@ CREATE TABLE temporal_rng2 () INHERITS (temporal_rng); ----------+-----------+-----------+----------+--------- id | int4range | | not null | valid_at | daterange | | not null | -Inherits: temporal_rng +Inherits: + temporal_rng DROP TABLE temporal_rng2; DROP TABLE temporal_rng; @@ -100,7 +101,8 @@ CREATE TABLE temporal_rng2 ( valid_at | daterange | | not null | Indexes: "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) -Inherits: temporal_rng +Inherits: + temporal_rng DROP TABLE temporal_rng CASCADE; NOTICE: drop cascades to table temporal_rng2 @@ -120,7 +122,8 @@ ALTER TABLE temporal_rng2 valid_at | daterange | | not null | Indexes: "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) -Inherits: temporal_rng +Inherits: + temporal_rng DROP TABLE temporal_rng2; DROP TABLE temporal_rng; @@ -311,6 +314,45 @@ ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; DROP TABLE temporal_rng3; DROP TYPE textrange2; -- +-- test PRIMARY KEY and UNIQUE constraints' interaction with domains +-- +-- range over domain: +CREATE DOMAIN int4_d as integer check (value <> 10); +CREATE TYPE int4_d_range as range (subtype = int4_d); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4_d_range, + CONSTRAINT temporal_rng4_pk PRIMARY KEY(id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); -- start bound violates domain +ERROR: value for domain int4_d violates check constraint "int4_d_check" +LINE 1: INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); + ^ +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- end bound violates domain +ERROR: value for domain int4_d violates check constraint "int4_d_check" +LINE 1: INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); + ^ +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,5)'); -- overlaps +ERROR: conflicting key value violates exclusion constraint "temporal_rng4_pk" +DETAIL: Key (id, valid_at)=([1,2), [2,5)) conflicts with existing key (id, valid_at)=([1,2), [1,13)). +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[30,)'); -- null bound is okay +DROP TABLE temporal_rng4; +-- domain over range: +CREATE DOMAIN int4range_d AS int4range CHECK (VALUE <> '[10,11)'); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4range_d, + CONSTRAINT temporal_rng4_pk UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- violates domain +ERROR: value for domain int4range_d violates check constraint "int4range_d_check" +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,13)'); -- overlaps +ERROR: conflicting key value violates exclusion constraint "temporal_rng4_pk" +DETAIL: Key (id, valid_at)=([1,2), [2,13)) conflicts with existing key (id, valid_at)=([1,2), [1,13)). +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +DROP TABLE temporal_rng4; +-- -- test ALTER TABLE ADD CONSTRAINT -- CREATE TABLE temporal_rng ( @@ -889,6 +931,36 @@ INSERT INTO temporal3 (id, valid_at, id2, name) ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'), ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar') ; +UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01' + SET name = name || '1'; +UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01' + SET name = name || '2' + WHERE id = '[2,3)'; +SELECT * FROM temporal3 ORDER BY id, valid_at; + id | valid_at | id2 | name +-------+-------------------------+--------+------- + [1,2) | [2000-01-01,2000-05-01) | [7,8) | foo + [1,2) | [2000-05-01,2000-07-01) | [7,8) | foo1 + [1,2) | [2000-07-01,2010-01-01) | [7,8) | foo + [2,3) | [2000-01-01,2000-04-01) | [9,10) | bar + [2,3) | [2000-04-01,2000-05-01) | [9,10) | bar2 + [2,3) | [2000-05-01,2000-06-01) | [9,10) | bar12 + [2,3) | [2000-06-01,2000-07-01) | [9,10) | bar1 + [2,3) | [2000-07-01,2010-01-01) | [9,10) | bar +(8 rows) + +-- conflicting id only: +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[1,2)', daterange('2005-01-01', '2006-01-01'), '[8,9)', 'foo3'); +ERROR: conflicting key value violates exclusion constraint "temporal3_pk" +DETAIL: Key (id, valid_at)=([1,2), [2005-01-01,2006-01-01)) conflicts with existing key (id, valid_at)=([1,2), [2000-07-01,2010-01-01)). +-- conflicting id2 only: +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[3,4)', daterange('2005-01-01', '2010-01-01'), '[9,10)', 'bar3'); +ERROR: conflicting key value violates exclusion constraint "temporal3_uniq" +DETAIL: Key (id2, valid_at)=([9,10), [2005-01-01,2010-01-01)) conflicts with existing key (id2, valid_at)=([9,10), [2000-07-01,2010-01-01)). DROP TABLE temporal3; -- -- test changing the PK's dependencies @@ -912,7 +984,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); @@ -920,26 +992,43 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'), ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'); -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+------- - [1,2) | [2000-01-01,2000-02-01) | one - [1,2) | [2000-02-01,2000-03-01) | one - [3,4) | [2000-01-01,2010-01-01) | three +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; + tableoid | id | valid_at | name +----------+-------+-------------------------+------- + tp1 | [1,2) | [2000-01-01,2000-02-01) | one + tp1 | [1,2) | [2000-02-01,2000-03-01) | one + tp2 | [3,4) | [2000-01-01,2010-01-01) | three (3 rows) -SELECT * FROM tp1 ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+------ - [1,2) | [2000-01-01,2000-02-01) | one - [1,2) | [2000-02-01,2000-03-01) | one -(2 rows) - -SELECT * FROM tp2 ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+------- - [3,4) | [2000-01-01,2010-01-01) | three -(1 row) +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + SET name = 'one2' + WHERE id = '[1,2)'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25' + SET id = '[4,5)' + WHERE name = 'one'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET id = '[2,3)' + WHERE name = 'three'; +DELETE FROM temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + WHERE id = '[3,4)'; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; + tableoid | id | valid_at | name +----------+-------+-------------------------+------- + tp1 | [1,2) | [2000-01-01,2000-01-15) | one + tp1 | [1,2) | [2000-01-15,2000-02-01) | one2 + tp1 | [1,2) | [2000-02-01,2000-02-15) | one2 + tp1 | [1,2) | [2000-02-15,2000-02-20) | one + tp1 | [1,2) | [2000-02-25,2000-03-01) | one + tp1 | [2,3) | [2002-01-01,2003-01-01) | three + tp2 | [3,4) | [2000-01-01,2000-01-15) | three + tp2 | [3,4) | [2000-02-15,2002-01-01) | three + tp2 | [3,4) | [2003-01-01,2010-01-01) | three + tp2 | [4,5) | [2000-02-20,2000-02-25) | one +(10 rows) DROP TABLE temporal_partitioned; -- temporal UNIQUE: @@ -947,7 +1036,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); @@ -955,26 +1044,43 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'), ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'); -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+------- - [1,2) | [2000-01-01,2000-02-01) | one - [1,2) | [2000-02-01,2000-03-01) | one - [3,4) | [2000-01-01,2010-01-01) | three +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; + tableoid | id | valid_at | name +----------+-------+-------------------------+------- + tp1 | [1,2) | [2000-01-01,2000-02-01) | one + tp1 | [1,2) | [2000-02-01,2000-03-01) | one + tp2 | [3,4) | [2000-01-01,2010-01-01) | three (3 rows) -SELECT * FROM tp1 ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+------ - [1,2) | [2000-01-01,2000-02-01) | one - [1,2) | [2000-02-01,2000-03-01) | one -(2 rows) - -SELECT * FROM tp2 ORDER BY id, valid_at; - id | valid_at | name --------+-------------------------+------- - [3,4) | [2000-01-01,2010-01-01) | three -(1 row) +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + SET name = 'one2' + WHERE id = '[1,2)'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25' + SET id = '[4,5)' + WHERE name = 'one'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET id = '[2,3)' + WHERE name = 'three'; +DELETE FROM temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + WHERE id = '[3,4)'; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; + tableoid | id | valid_at | name +----------+-------+-------------------------+------- + tp1 | [1,2) | [2000-01-01,2000-01-15) | one + tp1 | [1,2) | [2000-01-15,2000-02-01) | one2 + tp1 | [1,2) | [2000-02-01,2000-02-15) | one2 + tp1 | [1,2) | [2000-02-15,2000-02-20) | one + tp1 | [1,2) | [2000-02-25,2000-03-01) | one + tp1 | [2,3) | [2002-01-01,2003-01-01) | three + tp2 | [3,4) | [2000-01-01,2000-01-15) | three + tp2 | [3,4) | [2000-02-15,2002-01-01) | three + tp2 | [3,4) | [2003-01-01,2010-01-01) | three + tp2 | [4,5) | [2000-02-20,2000-02-25) | one +(10 rows) DROP TABLE temporal_partitioned; -- ALTER TABLE REPLICA IDENTITY @@ -1426,7 +1532,7 @@ CREATE TABLE temporal_fk_rng2rng ( CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, valid_at) REFERENCES temporal_rng (id, valid_at) ); -ERROR: foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS +ERROR: foreign key must use PERIOD when referencing a primary key using WITHOUT OVERLAPS -- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at) -- FOREIGN KEY part should specify PERIOD CREATE TABLE temporal_fk_rng2rng ( @@ -1755,6 +1861,33 @@ UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2rng". +-- changing an unreferenced part is okay: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' + SET id = '[7,8)' + WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2rng". +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+------------------------- + [5,6) | [2016-02-01,2016-03-01) + [5,6) | [2018-01-01,2018-01-02) + [5,6) | [2018-01-03,2018-02-01) + [7,8) | [2018-01-02,2018-01-03) +(4 rows) + +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+-------------------------+----------- + [3,4) | [2018-01-05,2018-01-10) | [5,6) +(1 row) + -- then delete the objecting FK record and the same PK update succeeds: DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') @@ -1802,6 +1935,42 @@ BEGIN; COMMIT; ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is still referenced from table "temporal_fk_rng2rng". +-- deleting an unreferenced part is okay: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+------------------------- + [5,6) | [2018-01-01,2018-01-02) + [5,6) | [2018-01-03,2018-02-01) +(2 rows) + +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+-------------------------+----------- + [3,4) | [2018-01-05,2018-01-10) | [5,6) +(1 row) + +-- deleting just a part fails: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' +WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_rng" violates foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is still referenced from table "temporal_fk_rng2rng". +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+------------------------- + [5,6) | [2018-01-01,2018-01-02) + [5,6) | [2018-01-03,2018-02-01) +(2 rows) + +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+-------------------------+----------- + [3,4) | [2018-01-05,2018-01-10) | [5,6) +(1 row) + -- then delete the objecting FK record and the same PK delete succeeds: DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); @@ -1818,11 +1987,12 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE RESTRICT; ERROR: unsupported ON DELETE action for foreign key constraint using PERIOD -- --- test ON UPDATE/DELETE options +-- rng2rng test ON UPDATE/DELETE options -- -- test FK referenced updates CASCADE +TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1830,8 +2000,9 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE CASCADE ON UPDATE CASCADE; ERROR: unsupported ON UPDATE action for foreign key constraint using PERIOD -- test FK referenced updates SET NULL -INSERT INTO temporal_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)'); +TRUNCATE temporal_rng, temporal_fk_rng2rng; +INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1839,9 +2010,10 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE SET NULL ON UPDATE SET NULL; ERROR: unsupported ON UPDATE action for foreign key constraint using PERIOD -- test FK referenced updates SET DEFAULT +TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null)); -INSERT INTO temporal_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)'); +INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', ADD CONSTRAINT temporal_fk_rng2rng_fk @@ -1900,7 +2072,7 @@ CREATE TABLE temporal_fk_mltrng2mltrng ( CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, valid_at) REFERENCES temporal_mltrng (id, valid_at) ); -ERROR: foreign key must use PERIOD when referencing a primary using WITHOUT OVERLAPS +ERROR: foreign key must use PERIOD when referencing a primary key using WITHOUT OVERLAPS -- (parent_id, valid_at) REFERENCES (id, PERIOD valid_at) -- FOREIGN KEY part should specify PERIOD CREATE TABLE temporal_fk_mltrng2mltrng ( @@ -2211,6 +2383,22 @@ UPDATE temporal_mltrng SET id = '[7,8)' WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); ERROR: update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng". +-- changing an unreferenced part is okay: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- -- test FK referenced updates RESTRICT -- @@ -2253,6 +2441,19 @@ BEGIN; COMMIT; ERROR: update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng". +-- deleting an unreferenced part is okay: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) +WHERE id = '[5,6)'; +-- deleting just a part fails: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) +WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_mltrng" violates foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)}) is still referenced from table "temporal_fk_mltrng2mltrng". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- -- FK between partitioned tables: ranges -- @@ -2260,7 +2461,7 @@ CREATE TABLE temporal_partitioned_rng ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)'); CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)'); @@ -2382,7 +2583,7 @@ CREATE TABLE temporal_partitioned_mltrng ( id int4range, valid_at datemultirange, name text, - CONSTRAINT temporal_paritioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned_mltrng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned_mltrng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)'); diff --git a/src/test/regress/expected/xid.out b/src/test/regress/expected/xid.out index 835077e9d5785..1ce7826cf9047 100644 --- a/src/test/regress/expected/xid.out +++ b/src/test/regress/expected/xid.out @@ -110,22 +110,26 @@ select '1'::xid < '2'::xid; ERROR: operator does not exist: xid < xid LINE 1: select '1'::xid < '2'::xid; ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select '1'::xid <= '2'::xid; ERROR: operator does not exist: xid <= xid LINE 1: select '1'::xid <= '2'::xid; ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select '1'::xid > '2'::xid; ERROR: operator does not exist: xid > xid LINE 1: select '1'::xid > '2'::xid; ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. select '1'::xid >= '2'::xid; ERROR: operator does not exist: xid >= xid LINE 1: select '1'::xid >= '2'::xid; ^ -HINT: No operator matches the given name and argument types. You might need to add explicit type casts. +DETAIL: No operator of that name accepts the given argument types. +HINT: You might need to add explicit type casts. -- we want them for xid8 though select '1'::xid8 < '2'::xid8, '2'::xid8 < '2'::xid8, '2'::xid8 < '1'::xid8; ?column? | ?column? | ?column? diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out index 103a22a3b1d3c..449733f8ae926 100644 --- a/src/test/regress/expected/xml.out +++ b/src/test/regress/expected/xml.out @@ -971,9 +971,7 @@ BEGIN END IF; EXCEPTION -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function + WHEN undefined_function -- unsupported XML feature OR feature_not_supported THEN RAISE LOG 'skip: %', SQLERRM; diff --git a/src/test/regress/expected/xml_1.out b/src/test/regress/expected/xml_1.out index 73c411118a390..a962fce36b91d 100644 --- a/src/test/regress/expected/xml_1.out +++ b/src/test/regress/expected/xml_1.out @@ -714,9 +714,7 @@ BEGIN END IF; EXCEPTION -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function + WHEN undefined_function -- unsupported XML feature OR feature_not_supported THEN RAISE LOG 'skip: %', SQLERRM; diff --git a/src/test/regress/expected/xml_2.out b/src/test/regress/expected/xml_2.out index a85d95358d901..d7c90725cfc2a 100644 --- a/src/test/regress/expected/xml_2.out +++ b/src/test/regress/expected/xml_2.out @@ -957,9 +957,7 @@ BEGIN END IF; EXCEPTION -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function + WHEN undefined_function -- unsupported XML feature OR feature_not_supported THEN RAISE LOG 'skip: %', SQLERRM; diff --git a/src/test/regress/meson.build b/src/test/regress/meson.build index 1da9e9462a9af..a5f2222e83aaf 100644 --- a/src/test/regress/meson.build +++ b/src/test/regress/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # also used by isolationtester and ecpg tests pg_regress_c = files('pg_regress.c') @@ -57,3 +57,5 @@ tests += { 'dbname': 'regression', }, } + +subdir('po', if_found: libintl) diff --git a/src/test/regress/nls.mk b/src/test/regress/nls.mk new file mode 100644 index 0000000000000..4051c965933dc --- /dev/null +++ b/src/test/regress/nls.mk @@ -0,0 +1,5 @@ +# src/test/regress/nls.mk +CATALOG_NAME = postgresql-regress +GETTEXT_FILES = regress.c +GETTEXT_TRIGGERS = $(BACKEND_COMMON_GETTEXT_TRIGGERS) +GETTEXT_FLAGS = $(BACKEND_COMMON_GETTEXT_FLAGS) diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index a424be2a6bf0f..5d4f910155efa 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t # geometry depends on point, lseg, line, box, path, polygon, circle # horology depends on date, time, timetz, timestamp, timestamptz, interval # ---------- -test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import +test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies oid8 encoding euc_kr # ---------- # Load huge amounts of data @@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi # ---------- # Another group of parallel tests # ---------- -test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse +test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph for_portion_of # ---------- # sanity_check does a vacuum, affecting the sort order of SELECT * @@ -75,12 +75,12 @@ test: brin_bloom brin_multi # ---------- # Another group of parallel tests -# psql depends on create_am -# amutils depends on geometry, create_index_spgist, hash_index, brin # ---------- -test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps generated_virtual +test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions nls sysviews tsrf tid tidscan tidrangescan collate.utf8 collate.icu.utf8 incremental_sort create_role without_overlaps generated_virtual # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other +# psql depends on create_am +# amutils depends on geometry, create_index_spgist, hash_index, brin test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 # ---------- @@ -102,7 +102,7 @@ test: publication subscription # Another group of parallel tests # select_views depends on create_view # ---------- -test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass +test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table # ---------- # Another group of parallel tests (JSON related) @@ -112,7 +112,7 @@ test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson # ---------- # Another group of parallel tests # with depends on create_misc -# NB: temp.sql does reconnects which transiently use 2 connections, +# NB: temp.sql does reconnects which transiently uses 2 connections, # so keep this parallel group to at most 19 tests # ---------- test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml @@ -123,13 +123,20 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # The stats test resets stats, so nothing else needing stats access can be in # this group. # ---------- -test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate numa +test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats predicate numa eager_aggregate graph_table_rls planner_est + +# ---------- +# Another group of parallel tests (compression) +# ---------- +test: compression compression_lz4 compression_pglz cluster # event_trigger depends on create_am and cannot run concurrently with # any test that runs DDL # oidjoins is read-only, though, and should run late for best coverage test: oidjoins event_trigger +test: role_ddl tablespace_ddl database_ddl + # event_trigger_login cannot run concurrently with any other tests because # on-login event handling could catch connection of a concurrent test. test: event_trigger_login diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index 5d85dcc62f0a5..c26efeba1ee3a 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -8,7 +8,7 @@ * * This code is released under the terms of the PostgreSQL License. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/regress/pg_regress.c @@ -84,6 +84,8 @@ const char *pretty_diff_opts = "--strip-trailing-cr -U3"; typedef enum TAPtype { DIAG = 0, + DIAG_DETAIL, + DIAG_END, BAIL, NOTE, NOTE_DETAIL, @@ -131,6 +133,7 @@ static char sockself[MAXPGPATH]; static char socklock[MAXPGPATH]; static StringInfo failed_tests = NULL; static bool in_note = false; +static bool in_diag = false; static _resultmap *resultmap = NULL; @@ -162,6 +165,8 @@ static void psql_end_command(StringInfo buf, const char *database); #define note(...) emit_tap_output(NOTE, __VA_ARGS__) #define note_detail(...) emit_tap_output(NOTE_DETAIL, __VA_ARGS__) #define diag(...) emit_tap_output(DIAG, __VA_ARGS__) +#define diag_detail(...) emit_tap_output(DIAG_DETAIL, __VA_ARGS__) +#define diag_end() emit_tap_output(DIAG_END, "\n"); #define note_end() emit_tap_output(NOTE_END, "\n"); #define bail_noatexit(...) bail_out(true, __VA_ARGS__) #define bail(...) bail_out(false, __VA_ARGS__) @@ -196,7 +201,7 @@ unlimit_core_size(void) void add_stringlist_item(_stringlist **listhead, const char *str) { - _stringlist *newentry = pg_malloc(sizeof(_stringlist)); + _stringlist *newentry = pg_malloc_object(_stringlist); _stringlist *oldentry; newentry->str = pg_strdup(str); @@ -356,7 +361,7 @@ emit_tap_output_v(TAPtype type, const char *fmt, va_list argp) * Bail message is also printed to stderr to aid debugging under a harness * which might otherwise not emit such an important message. */ - if (type == DIAG || type == BAIL) + if (type == DIAG || type == DIAG_DETAIL || type == DIAG_END || type == BAIL) fp = stderr; else fp = stdout; @@ -365,9 +370,12 @@ emit_tap_output_v(TAPtype type, const char *fmt, va_list argp) * If we are ending a note_detail line we can avoid further processing and * immediately return following a newline. */ - if (type == NOTE_END) + if (type == NOTE_END || type == DIAG_END) { - in_note = false; + if (type == NOTE_END) + in_note = false; + else + in_diag = false; fprintf(fp, "\n"); if (logfile) fprintf(logfile, "\n"); @@ -382,7 +390,8 @@ emit_tap_output_v(TAPtype type, const char *fmt, va_list argp) * '#' character. We print the Bail message like this too. */ if ((type == NOTE || type == DIAG || type == BAIL) - || (type == NOTE_DETAIL && !in_note)) + || (type == NOTE_DETAIL && !in_note) + || (type == DIAG_DETAIL && !in_diag)) { fprintf(fp, "# "); if (logfile) @@ -403,6 +412,8 @@ emit_tap_output_v(TAPtype type, const char *fmt, va_list argp) */ if (type == NOTE_DETAIL) in_note = true; + if (type == DIAG_DETAIL) + in_diag = true; /* * If this was a Bail message, the bail protocol message must go to stdout @@ -417,7 +428,7 @@ emit_tap_output_v(TAPtype type, const char *fmt, va_list argp) va_end(argp_logfile); - if (type != NOTE_DETAIL) + if (type != NOTE_DETAIL && type != DIAG_DETAIL) { fprintf(fp, "\n"); if (logfile) @@ -481,7 +492,7 @@ signal_remove_temp(SIGNAL_ARGS) { remove_temp(); - pqsignal(postgres_signal_arg, SIG_DFL); + pqsignal(postgres_signal_arg, PG_SIG_DFL); raise(postgres_signal_arg); } @@ -674,7 +685,7 @@ load_resultmap(void) */ if (string_matches_pattern(host_platform, platform)) { - _resultmap *entry = pg_malloc(sizeof(_resultmap)); + _resultmap *entry = pg_malloc_object(_resultmap); entry->test = pg_strdup(buf); entry->type = pg_strdup(file_type); @@ -693,7 +704,7 @@ static const char * get_expectfile(const char *testname, const char *file) { - char *file_type; + const char *file_type; _resultmap *rm; /* @@ -1414,6 +1425,7 @@ results_differ(const char *testname, const char *resultsfile, const char *defaul int best_line_count; int i; int l; + long startpos; const char *platform_expectfile; /* @@ -1521,21 +1533,80 @@ results_differ(const char *testname, const char *resultsfile, const char *defaul * append to the diffs summary file. */ - /* Write diff header */ difffile = fopen(difffilename, "a"); if (difffile) { + startpos = ftell(difffile); + + /* Write diff header */ fprintf(difffile, "diff %s %s %s\n", pretty_diff_opts, best_expect_file, resultsfile); fclose(difffile); + + /* Run diff */ + snprintf(cmd, sizeof(cmd), + "diff %s \"%s\" \"%s\" >> \"%s\"", + pretty_diff_opts, best_expect_file, resultsfile, difffilename); + run_diff(cmd, difffilename); + + /* + * Reopen the file for reading to emit the diff as TAP diagnostics. We + * can't keep the file open while diff appends to it, because on + * Windows the file lock prevents diff from writing. + */ + difffile = fopen(difffilename, "r"); } - /* Run diff */ - snprintf(cmd, sizeof(cmd), - "diff %s \"%s\" \"%s\" >> \"%s\"", - pretty_diff_opts, best_expect_file, resultsfile, difffilename); - run_diff(cmd, difffilename); + if (difffile) + { + /* + * In case of a crash the diff can be huge and all of the subsequent + * tests will fail with essentially useless diffs too. So to avoid + * flooding the output, while still providing useful info in most + * cases we output only the first 80 lines of the *combined* diff. The + * number 80 is chosen so that we output less than 100 lines of + * diagnostics per pg_regress run. Otherwise if meson is run with the + * --quiet flag only the last 100 lines are shown and usually the most + * useful information is actually in the first few lines. + */ + static int nlines = 0; + const int max_diff_lines = 80; + char line[1024]; + + fseek(difffile, startpos, SEEK_SET); + while (nlines < max_diff_lines && + fgets(line, sizeof(line), difffile)) + { + size_t len = strlen(line); + bool newline_found = (len > 0 && line[len - 1] == '\n'); + + if (newline_found) + line[len - 1] = '\0'; + + diag_detail("%s", line); + if (newline_found) + { + diag_end(); + nlines++; + } + } + + if (in_diag) + { + /* + * If there was no final newline for some reason, we should still + * end the diagnostic. + */ + diag_end(); + nlines++; + } + + if (nlines >= max_diff_lines) + diag("(diff output truncated and silencing output for further failing tests...)"); + + fclose(difffile); + } unlink(diff); return true; @@ -1557,7 +1628,7 @@ wait_for_tests(PID_TYPE * pids, int *statuses, instr_time *stoptimes, int i; #ifdef WIN32 - PID_TYPE *active_pids = pg_malloc(num_tests * sizeof(PID_TYPE)); + PID_TYPE *active_pids = pg_malloc_array(PID_TYPE, num_tests); memcpy(active_pids, pids, num_tests * sizeof(PID_TYPE)); #endif @@ -1968,10 +2039,10 @@ create_database(const char *dbname) */ if (encoding) psql_add_command(buf, "CREATE DATABASE \"%s\" TEMPLATE=template0 ENCODING='%s'%s", dbname, encoding, - (nolocale) ? " LOCALE='C'" : ""); + (nolocale) ? " LOCALE='C' LOCALE_PROVIDER='builtin'" : ""); else psql_add_command(buf, "CREATE DATABASE \"%s\" TEMPLATE=template0%s", dbname, - (nolocale) ? " LOCALE='C'" : ""); + (nolocale) ? " LOCALE='C' LOCALE_PROVIDER='builtin'" : ""); psql_add_command(buf, "ALTER DATABASE \"%s\" SET lc_messages TO 'C';" "ALTER DATABASE \"%s\" SET lc_monetary TO 'C';" @@ -2110,6 +2181,8 @@ regression_main(int argc, char *argv[], progname = get_progname(argv[0]); set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_regress")); + pg_initialize_timing(); + get_restricted_token(); atexit(stop_postmaster); @@ -2401,6 +2474,7 @@ regression_main(int argc, char *argv[], fputs("\n# Configuration added by pg_regress\n\n", pg_conf); fputs("log_autovacuum_min_duration = 0\n", pg_conf); + fputs("log_autoanalyze_min_duration = 0\n", pg_conf); fputs("log_checkpoints = on\n", pg_conf); fputs("log_line_prefix = '%m %b[%p] %q%a '\n", pg_conf); fputs("log_lock_waits = on\n", pg_conf); diff --git a/src/test/regress/pg_regress.h b/src/test/regress/pg_regress.h index 13d5ba25ebbc7..5aa1deeab216e 100644 --- a/src/test/regress/pg_regress.h +++ b/src/test/regress/pg_regress.h @@ -1,7 +1,7 @@ /*------------------------------------------------------------------------- * pg_regress.h --- regression test driver * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/regress/pg_regress.h diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c index 072d1cbc62d08..701f3dd5d9ca5 100644 --- a/src/test/regress/pg_regress_main.c +++ b/src/test/regress/pg_regress_main.c @@ -8,7 +8,7 @@ * * This code is released under the terms of the PostgreSQL License. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/regress/pg_regress_main.c diff --git a/src/test/regress/po/LINGUAS b/src/test/regress/po/LINGUAS new file mode 100644 index 0000000000000..8357fcaaed4be --- /dev/null +++ b/src/test/regress/po/LINGUAS @@ -0,0 +1 @@ +es diff --git a/src/test/regress/po/es.po b/src/test/regress/po/es.po new file mode 100644 index 0000000000000..b3021d57e226b --- /dev/null +++ b/src/test/regress/po/es.po @@ -0,0 +1,159 @@ +# Spanish message translation file for regress test library +# +# Copyright (C) 2025 PostgreSQL Global Development Group +# This file is distributed under the same license as the regress (PostgreSQL) package. +# +# Tom Lane , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: regress (PostgreSQL) 19\n" +"Report-Msgid-Bugs-To: pgsql-bugs@lists.postgresql.org\n" +"POT-Creation-Date: 2025-12-08 13:57-0500\n" +"PO-Revision-Date: 2025-11-19 19:01-0500\n" +"Last-Translator: Tom Lane \n" +"Language-Team: PgSQL-es-Ayuda \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: regress.c:202 +#, c-format +msgid "invalid input syntax for type %s: \"%s\"" +msgstr "la sintaxis de entrada no es válida para tipo %s: «%s»" + +#: regress.c:839 +#, c-format +msgid "test_inline_in_from_support_func called with %d args but expected 3" +msgstr "" + +#: regress.c:847 regress.c:863 +#, c-format +msgid "test_inline_in_from_support_func called with non-Const parameters" +msgstr "" + +#: regress.c:854 regress.c:870 +#, c-format +msgid "test_inline_in_from_support_func called with non-TEXT parameters" +msgstr "" + +#: regress.c:903 +#, c-format +msgid "test_inline_in_from_support_func parsed to more than one node" +msgstr "" + +#: regress.c:914 +#, c-format +msgid "test_inline_in_from_support_func rewrote to more than one node" +msgstr "" + +#: regress.c:921 +#, c-format +msgid "test_inline_in_from_support_func didn't parse to a Query" +msgstr "" + +#: regress.c:1028 +#, c-format +msgid "invalid source encoding name \"%s\"" +msgstr "la codificación de origen «%s» no es válida" + +#: regress.c:1033 +#, c-format +msgid "invalid destination encoding name \"%s\"" +msgstr "la codificación de destino «%s» no es válida" + +#: regress.c:1078 +#, c-format +msgid "default conversion function for encoding \"%s\" to \"%s\" does not exist" +msgstr "no existe el procedimiento por omisión de conversión desde la codificación «%s» a «%s»" + +#: regress.c:1085 +#, c-format +msgid "out of memory" +msgstr "memoria agotada" + +#: regress.c:1086 +#, c-format +msgid "String of %d bytes is too long for encoding conversion." +msgstr "La cadena de %d bytes es demasiado larga para la recodificación." + +#: regress.c:1175 +#, c-format +msgid "translated PRId64 = %" +msgstr "traducido PRId64 = %" + +#: regress.c:1177 +#, c-format +msgid "translated PRId32 = %" +msgstr "traducido PRId32 = %" + +#: regress.c:1179 +#, c-format +msgid "translated PRIdMAX = %" +msgstr "traducido PRIdMAX = %" + +#: regress.c:1181 +#, c-format +msgid "translated PRIdPTR = %" +msgstr "traducido PRIdPTR = %" + +#: regress.c:1184 +#, c-format +msgid "translated PRIu64 = %" +msgstr "traducido PRIu64 = %" + +#: regress.c:1186 +#, c-format +msgid "translated PRIu32 = %" +msgstr "traducido PRIu32 = %" + +#: regress.c:1188 +#, c-format +msgid "translated PRIuMAX = %" +msgstr "traducido PRIuMAX = %" + +#: regress.c:1190 +#, c-format +msgid "translated PRIuPTR = %" +msgstr "traducido PRIuPTR = %" + +#: regress.c:1193 +#, c-format +msgid "translated PRIx64 = %" +msgstr "traducido PRIx64 = %" + +#: regress.c:1195 +#, c-format +msgid "translated PRIx32 = %" +msgstr "traducido PRIx32 = %" + +#: regress.c:1197 +#, c-format +msgid "translated PRIxMAX = %" +msgstr "traducido PRIxMAX = %" + +#: regress.c:1199 +#, c-format +msgid "translated PRIxPTR = %" +msgstr "traducido PRIxPTR = %" + +#: regress.c:1202 +#, c-format +msgid "translated PRIX64 = %" +msgstr "traducido PRIX64 = %" + +#: regress.c:1204 +#, c-format +msgid "translated PRIX32 = %" +msgstr "traducido PRIX32 = %" + +#: regress.c:1206 +#, c-format +msgid "translated PRIXMAX = %" +msgstr "traducido PRIXMAX = %" + +#: regress.c:1208 +#, c-format +msgid "translated PRIXPTR = %" +msgstr "traducido PRIXPTR = %" diff --git a/src/test/regress/po/meson.build b/src/test/regress/po/meson.build new file mode 100644 index 0000000000000..46aa96eeed986 --- /dev/null +++ b/src/test/regress/po/meson.build @@ -0,0 +1,3 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +nls_targets += [i18n.gettext('postgresql-regress-' + pg_version_major.to_string())] diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index 3dbba06902405..2bcb5559a4522 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -6,7 +6,7 @@ * * This code is released under the terms of the PostgreSQL License. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/test/regress/regress.c @@ -27,7 +27,9 @@ #include "catalog/pg_type.h" #include "commands/sequence.h" #include "commands/trigger.h" +#include "common/pg_lzcompress.h" #include "executor/executor.h" +#include "executor/functions.h" #include "executor/spi.h" #include "funcapi.h" #include "mb/pg_wchar.h" @@ -37,8 +39,10 @@ #include "optimizer/plancat.h" #include "parser/parse_coerce.h" #include "port/atomics.h" +#include "portability/instr_time.h" #include "postmaster/postmaster.h" /* for MAX_BACKENDS */ #include "storage/spin.h" +#include "tcop/tcopprot.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/geo_decls.h" @@ -46,6 +50,10 @@ #include "utils/rel.h" #include "utils/typcache.h" +/* define our text domain for translations */ +#undef TEXTDOMAIN +#define TEXTDOMAIN PG_TEXTDOMAIN("postgresql-regress") + #define EXPECT_TRUE(expr) \ do { \ if (!(expr)) \ @@ -196,7 +204,7 @@ widget_in(PG_FUNCTION_ARGS) errmsg("invalid input syntax for type %s: \"%s\"", "widget", str))); - result = (WIDGET *) palloc(sizeof(WIDGET)); + result = palloc_object(WIDGET); result->center.x = atof(coord[0]); result->center.y = atof(coord[1]); result->radius = atof(coord[2]); @@ -370,9 +378,9 @@ make_tuple_indirect(PG_FUNCTION_ARGS) for (i = 0; i < ncolumns; i++) { - struct varlena *attr; - struct varlena *new_attr; - struct varatt_indirect redirect_pointer; + varlena *attr; + varlena *new_attr; + varatt_indirect redirect_pointer; /* only work on existing, not-null varlenas */ if (TupleDescAttr(tupdesc, i)->attisdropped || @@ -381,7 +389,7 @@ make_tuple_indirect(PG_FUNCTION_ARGS) TupleDescAttr(tupdesc, i)->attstorage == TYPSTORAGE_PLAIN) continue; - attr = (struct varlena *) DatumGetPointer(values[i]); + attr = (varlena *) DatumGetPointer(values[i]); /* don't recursively indirect */ if (VARATT_IS_EXTERNAL_INDIRECT(attr)) @@ -392,14 +400,14 @@ make_tuple_indirect(PG_FUNCTION_ARGS) attr = detoast_external_attr(attr); else { - struct varlena *oldattr = attr; + varlena *oldattr = attr; attr = palloc0(VARSIZE_ANY(oldattr)); memcpy(attr, oldattr, VARSIZE_ANY(oldattr)); } /* build indirection Datum */ - new_attr = (struct varlena *) palloc0(INDIRECT_POINTER_SIZE); + new_attr = (varlena *) palloc0(INDIRECT_POINTER_SIZE); redirect_pointer.pointer = attr; SET_VARTAG_EXTERNAL(new_attr, VARTAG_INDIRECT); memcpy(VARDATA_EXTERNAL(new_attr), &redirect_pointer, @@ -432,7 +440,7 @@ PG_FUNCTION_INFO_V1(get_environ); Datum get_environ(PG_FUNCTION_ARGS) { -#if !defined(WIN32) || defined(_MSC_VER) +#if !defined(WIN32) extern char **environ; #endif int nvals = 0; @@ -723,11 +731,18 @@ test_fdw_handler(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } +PG_FUNCTION_INFO_V1(test_fdw_connection); +Datum +test_fdw_connection(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(cstring_to_text("dbname=regress_doesnotexist user=doesnotexist password=secret")); +} + PG_FUNCTION_INFO_V1(is_catalog_text_unique_index_oid); Datum is_catalog_text_unique_index_oid(PG_FUNCTION_ARGS) { - return IsCatalogTextUniqueIndexOid(PG_GETARG_OID(0)); + return BoolGetDatum(IsCatalogTextUniqueIndexOid(PG_GETARG_OID(0))); } PG_FUNCTION_INFO_V1(test_support_func); @@ -803,6 +818,125 @@ test_support_func(PG_FUNCTION_ARGS) PG_RETURN_POINTER(ret); } +PG_FUNCTION_INFO_V1(test_inline_in_from_support_func); +Datum +test_inline_in_from_support_func(PG_FUNCTION_ARGS) +{ + Node *rawreq = (Node *) PG_GETARG_POINTER(0); + + if (IsA(rawreq, SupportRequestInlineInFrom)) + { + /* + * Assume that the target is foo_from_bar; that's safe as long as we + * don't attach this to any other function. + */ + SupportRequestInlineInFrom *req = (SupportRequestInlineInFrom *) rawreq; + StringInfoData sql; + RangeTblFunction *rtfunc = req->rtfunc; + FuncExpr *expr = (FuncExpr *) rtfunc->funcexpr; + Node *node; + Const *c; + char *colname; + char *tablename; + SQLFunctionParseInfoPtr pinfo; + List *raw_parsetree_list; + List *querytree_list; + Query *querytree; + + if (list_length(expr->args) != 3) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func called with %d args but expected 3", list_length(expr->args)))); + PG_RETURN_POINTER(NULL); + } + + /* Get colname */ + node = linitial(expr->args); + if (!IsA(node, Const)) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func called with non-Const parameters"))); + PG_RETURN_POINTER(NULL); + } + + c = (Const *) node; + if (c->consttype != TEXTOID || c->constisnull) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func called with non-TEXT parameters"))); + PG_RETURN_POINTER(NULL); + } + colname = TextDatumGetCString(c->constvalue); + + /* Get tablename */ + node = lsecond(expr->args); + if (!IsA(node, Const)) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func called with non-Const parameters"))); + PG_RETURN_POINTER(NULL); + } + + c = (Const *) node; + if (c->consttype != TEXTOID || c->constisnull) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func called with non-TEXT parameters"))); + PG_RETURN_POINTER(NULL); + } + tablename = TextDatumGetCString(c->constvalue); + + /* Begin constructing replacement SELECT query. */ + initStringInfo(&sql); + appendStringInfo(&sql, "SELECT %s::text FROM %s", + quote_identifier(colname), + quote_identifier(tablename)); + + /* Add filter expression if present. */ + node = lthird(expr->args); + if (!(IsA(node, Const) && ((Const *) node)->constisnull)) + { + /* + * We only filter if $3 is not constant-NULL. This is not a very + * exact implementation of the PL/pgSQL original, but it's close + * enough for demonstration purposes. + */ + appendStringInfo(&sql, " WHERE %s::text = $3", + quote_identifier(colname)); + } + + /* Build a SQLFunctionParseInfo with the parameters of my function. */ + pinfo = prepare_sql_fn_parse_info(req->proc, + (Node *) expr, + expr->inputcollid); + + /* Parse the generated SQL. */ + raw_parsetree_list = pg_parse_query(sql.data); + if (list_length(raw_parsetree_list) != 1) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func parsed to more than one node"))); + PG_RETURN_POINTER(NULL); + } + + /* Analyze the parse tree as if it were a SQL-language body. */ + querytree_list = pg_analyze_and_rewrite_withcb(linitial(raw_parsetree_list), + sql.data, + (ParserSetupHook) sql_fn_parser_setup, + pinfo, NULL); + if (list_length(querytree_list) != 1) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func rewrote to more than one node"))); + PG_RETURN_POINTER(NULL); + } + + querytree = linitial(querytree_list); + if (!IsA(querytree, Query)) + { + ereport(WARNING, (errmsg("test_inline_in_from_support_func didn't parse to a Query"))); + PG_RETURN_POINTER(NULL); + } + + PG_RETURN_POINTER(querytree); + } + + PG_RETURN_POINTER(NULL); +} + PG_FUNCTION_INFO_V1(test_opclass_options_func); Datum test_opclass_options_func(PG_FUNCTION_ARGS) @@ -824,6 +958,8 @@ test_enc_setup(PG_FUNCTION_ARGS) mblen, valid; + if (!PG_VALID_ENCODING(i)) + continue; if (pg_encoding_max_length(i) == 1) continue; pg_encoding_set_invalid(i, buf); @@ -990,6 +1126,145 @@ test_enc_conversion(PG_FUNCTION_ARGS) PG_RETURN_DATUM(HeapTupleGetDatum(tuple)); } +/* Convert bytea to text without validation for corruption tests from SQL. */ +PG_FUNCTION_INFO_V1(test_bytea_to_text); +Datum +test_bytea_to_text(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(PG_GETARG_BYTEA_PP(0)); +} + +/* And the reverse. */ +PG_FUNCTION_INFO_V1(test_text_to_bytea); +Datum +test_text_to_bytea(PG_FUNCTION_ARGS) +{ + PG_RETURN_BYTEA_P(PG_GETARG_TEXT_PP(0)); +} + +/* Corruption tests in C. */ +PG_FUNCTION_INFO_V1(test_mblen_func); +Datum +test_mblen_func(PG_FUNCTION_ARGS) +{ + const char *func = text_to_cstring(PG_GETARG_BYTEA_PP(0)); + const char *encoding = text_to_cstring(PG_GETARG_BYTEA_PP(1)); + text *string = PG_GETARG_BYTEA_PP(2); + int offset = PG_GETARG_INT32(3); + const char *data = VARDATA_ANY(string); + size_t size = VARSIZE_ANY_EXHDR(string); + int result = 0; + + if (strcmp(func, "pg_mblen_unbounded") == 0) + result = pg_mblen_unbounded(data + offset); + else if (strcmp(func, "pg_mblen_cstr") == 0) + result = pg_mblen_cstr(data + offset); + else if (strcmp(func, "pg_mblen_with_len") == 0) + result = pg_mblen_with_len(data + offset, size - offset); + else if (strcmp(func, "pg_mblen_range") == 0) + result = pg_mblen_range(data + offset, data + size); + else if (strcmp(func, "pg_encoding_mblen") == 0) + result = pg_encoding_mblen(pg_char_to_encoding(encoding), data + offset); + else + elog(ERROR, "unknown function"); + + PG_RETURN_INT32(result); +} + +PG_FUNCTION_INFO_V1(test_text_to_wchars); +Datum +test_text_to_wchars(PG_FUNCTION_ARGS) +{ + const char *encoding_name = text_to_cstring(PG_GETARG_BYTEA_PP(0)); + text *string = PG_GETARG_TEXT_PP(1); + const char *data = VARDATA_ANY(string); + size_t size = VARSIZE_ANY_EXHDR(string); + pg_wchar *wchars = palloc(sizeof(pg_wchar) * (size + 1)); + Datum *datums; + int wlen; + int encoding; + + encoding = pg_char_to_encoding(encoding_name); + if (encoding < 0) + elog(ERROR, "unknown encoding name: %s", encoding_name); + + if (size > 0) + { + datums = palloc(sizeof(Datum) * size); + wlen = pg_encoding_mb2wchar_with_len(encoding, + data, + wchars, + size); + Assert(wlen >= 0); + Assert(wlen <= size); + Assert(wchars[wlen] == 0); + + for (int i = 0; i < wlen; ++i) + datums[i] = UInt32GetDatum(wchars[i]); + } + else + { + datums = NULL; + wlen = 0; + } + + PG_RETURN_ARRAYTYPE_P(construct_array_builtin(datums, wlen, INT4OID)); +} + +PG_FUNCTION_INFO_V1(test_wchars_to_text); +Datum +test_wchars_to_text(PG_FUNCTION_ARGS) +{ + const char *encoding_name = text_to_cstring(PG_GETARG_BYTEA_PP(0)); + ArrayType *array = PG_GETARG_ARRAYTYPE_P(1); + Datum *datums; + bool *nulls; + char *mb; + text *result; + int wlen; + int bytes; + int encoding; + + encoding = pg_char_to_encoding(encoding_name); + if (encoding < 0) + elog(ERROR, "unknown encoding name: %s", encoding_name); + + deconstruct_array_builtin(array, INT4OID, &datums, &nulls, &wlen); + + if (wlen > 0) + { + pg_wchar *wchars = palloc(sizeof(pg_wchar) * wlen); + + for (int i = 0; i < wlen; ++i) + { + if (nulls[i]) + elog(ERROR, "unexpected NULL in array"); + wchars[i] = DatumGetInt32(datums[i]); + } + + mb = palloc(pg_encoding_max_length(encoding) * wlen + 1); + bytes = pg_encoding_wchar2mb_with_len(encoding, wchars, mb, wlen); + } + else + { + mb = ""; + bytes = 0; + } + + result = palloc(bytes + VARHDRSZ); + SET_VARSIZE(result, bytes + VARHDRSZ); + memcpy(VARDATA(result), mb, bytes); + + PG_RETURN_TEXT_P(result); +} + +PG_FUNCTION_INFO_V1(test_valid_server_encoding); +Datum +test_valid_server_encoding(PG_FUNCTION_ARGS) +{ + PG_RETURN_BOOL(pg_valid_server_encoding(text_to_cstring(PG_GETARG_TEXT_PP(0))) >= 0); +} + /* Provide SQL access to IsBinaryCoercible() */ PG_FUNCTION_INFO_V1(binary_coercible); Datum @@ -1028,3 +1303,188 @@ test_relpath(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } + +/* + * Simple test to verify NLS support, particularly that the PRI* macros work. + * + * A secondary objective is to verify that 's values for the + * PRI* macros match what our snprintf.c code will do. Therefore, we run + * the ereport() calls even when we know that translation will not happen. + */ +PG_FUNCTION_INFO_V1(test_translation); +Datum +test_translation(PG_FUNCTION_ARGS) +{ +#ifdef ENABLE_NLS + static bool inited = false; + + /* + * Ideally we'd do this bit in a _PG_init() hook. However, it seems best + * that the Solaris hack only get applied in the nls.sql test, so it + * doesn't risk affecting other tests that load this module. + */ + if (!inited) + { + /* + * Solaris' built-in gettext is not bright about associating locales + * with message catalogs that are named after just the language. + * Apparently the customary workaround is for users to set the + * LANGUAGE environment variable to provide a mapping. Do so here to + * ensure that the nls.sql regression test will work. + */ +#if defined(__sun__) + setenv("LANGUAGE", "es_ES.UTF-8:es", 1); +#endif + pg_bindtextdomain(TEXTDOMAIN); + inited = true; + } + + /* + * If nls.sql failed to select a non-C locale, no translation will happen. + * Report that so that we can distinguish this outcome from brokenness. + * (We do this here, not in nls.sql, so as to need only 3 expected files.) + */ + if (strcmp(GetConfigOption("lc_messages", false, false), "C") == 0) + elog(NOTICE, "lc_messages is 'C'"); +#else + elog(NOTICE, "NLS is not enabled"); +#endif + + ereport(NOTICE, + errmsg("translated PRId64 = %" PRId64, (int64) 424242424242)); + ereport(NOTICE, + errmsg("translated PRId32 = %" PRId32, (int32) -1234)); + ereport(NOTICE, + errmsg("translated PRIdMAX = %" PRIdMAX, (intmax_t) -123456789012)); + ereport(NOTICE, + errmsg("translated PRIdPTR = %" PRIdPTR, (intptr_t) -9999)); + + ereport(NOTICE, + errmsg("translated PRIu64 = %" PRIu64, (uint64) 424242424242)); + ereport(NOTICE, + errmsg("translated PRIu32 = %" PRIu32, (uint32) -1234)); + ereport(NOTICE, + errmsg("translated PRIuMAX = %" PRIuMAX, (uintmax_t) 123456789012)); + ereport(NOTICE, + errmsg("translated PRIuPTR = %" PRIuPTR, (uintptr_t) 9999)); + + ereport(NOTICE, + errmsg("translated PRIx64 = %" PRIx64, (uint64) 424242424242)); + ereport(NOTICE, + errmsg("translated PRIx32 = %" PRIx32, (uint32) -1234)); + ereport(NOTICE, + errmsg("translated PRIxMAX = %" PRIxMAX, (uintmax_t) 123456789012)); + ereport(NOTICE, + errmsg("translated PRIxPTR = %" PRIxPTR, (uintptr_t) 9999)); + + ereport(NOTICE, + errmsg("translated PRIX64 = %" PRIX64, (uint64) 424242424242)); + ereport(NOTICE, + errmsg("translated PRIX32 = %" PRIX32, (uint32) -1234)); + ereport(NOTICE, + errmsg("translated PRIXMAX = %" PRIXMAX, (uintmax_t) 123456789012)); + ereport(NOTICE, + errmsg("translated PRIXPTR = %" PRIXPTR, (uintptr_t) 9999)); + + PG_RETURN_VOID(); +} + +/* Verify that pg_ticks_to_ns behaves correct, including overflow */ +PG_FUNCTION_INFO_V1(test_instr_time); +Datum +test_instr_time(PG_FUNCTION_ARGS) +{ + instr_time t; + int64 test_ns[] = {0, 1000, INT64CONST(1000000000000000)}; + int64 max_err; + + /* + * The ns-to-ticks-to-ns roundtrip may lose precision due to integer + * truncation in the fixed-point conversion. The maximum error depends on + * ticks_per_ns_scaled relative to the shift factor. + */ + max_err = (ticks_per_ns_scaled >> TICKS_TO_NS_SHIFT) + 1; + + for (int i = 0; i < lengthof(test_ns); i++) + { + int64 result; + + INSTR_TIME_SET_ZERO(t); + INSTR_TIME_ADD_NANOSEC(t, test_ns[i]); + result = INSTR_TIME_GET_NANOSEC(t); + + if (result < test_ns[i] - max_err || result > test_ns[i]) + elog(ERROR, + "INSTR_TIME_GET_NANOSEC(t) yielded " INT64_FORMAT + ", expected " INT64_FORMAT " (max_err " INT64_FORMAT + ") in file \"%s\" line %u", + result, test_ns[i], max_err, __FILE__, __LINE__); + } + + PG_RETURN_BOOL(true); +} + +/* + * test_pglz_compress + * + * Compress the input using pglz_compress(). Only the "always" strategy is + * currently supported. + * + * Returns the compressed data, or NULL if compression fails. + */ +PG_FUNCTION_INFO_V1(test_pglz_compress); +Datum +test_pglz_compress(PG_FUNCTION_ARGS) +{ + bytea *input = PG_GETARG_BYTEA_PP(0); + char *source = VARDATA_ANY(input); + int32 slen = VARSIZE_ANY_EXHDR(input); + int32 maxout = PGLZ_MAX_OUTPUT(slen); + bytea *result; + int32 clen; + + result = (bytea *) palloc(maxout + VARHDRSZ); + clen = pglz_compress(source, slen, VARDATA(result), + PGLZ_strategy_always); + if (clen < 0) + PG_RETURN_NULL(); + + SET_VARSIZE(result, clen + VARHDRSZ); + PG_RETURN_BYTEA_P(result); +} + +/* + * test_pglz_decompress + * + * Decompress the input using pglz_decompress(). + * + * The second argument is the expected uncompressed data size. The third + * argument is here for the check_complete flag. + * + * Returns the decompressed data, or raises an error if decompression fails. + */ +PG_FUNCTION_INFO_V1(test_pglz_decompress); +Datum +test_pglz_decompress(PG_FUNCTION_ARGS) +{ + bytea *input = PG_GETARG_BYTEA_PP(0); + int32 rawsize = PG_GETARG_INT32(1); + bool check_complete = PG_GETARG_BOOL(2); + char *source = VARDATA_ANY(input); + int32 slen = VARSIZE_ANY_EXHDR(input); + bytea *result; + int32 dlen; + + if (rawsize < 0) + elog(ERROR, "rawsize must not be negative"); + + result = (bytea *) palloc(rawsize + VARHDRSZ); + + dlen = pglz_decompress(source, slen, VARDATA(result), + rawsize, check_complete); + if (dlen < 0) + elog(ERROR, "pglz_decompress failed"); + + SET_VARSIZE(result, dlen + VARHDRSZ); + PG_RETURN_BYTEA_P(result); +} diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql index 277b4b198ccc4..89bb83718e0c5 100644 --- a/src/test/regress/sql/aggregates.sql +++ b/src/test/regress/sql/aggregates.sql @@ -140,6 +140,24 @@ SELECT covar_pop(1::float8,2::float8), covar_samp(3::float8,4::float8); SELECT covar_pop(1::float8,'inf'::float8), covar_samp(3::float8,'inf'::float8); SELECT covar_pop(1::float8,'nan'::float8), covar_samp(3::float8,'nan'::float8); +-- check some cases that formerly had poor roundoff-error behavior +SELECT corr(0.09, g), regr_r2(0.09, g) + FROM generate_series(1, 30) g; +SELECT corr(g, 0.09), regr_r2(g, 0.09), regr_slope(g, 0.09), regr_intercept(g, 0.09) + FROM generate_series(1, 30) g; +SELECT corr(1.3 + g * 1e-16, 1.3 + g * 1e-16) + FROM generate_series(1, 3) g; +SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) + FROM generate_series(1, 3) g; +SELECT corr(1e-100 + g * 1e-105, 1e-100 + g * 1e-105) + FROM generate_series(1, 30) g; + +-- these examples pose definitional questions for NaN inputs, +-- which we resolve by saying that an all-NaN input column is not all equal +SELECT corr(g, 'NaN') FROM generate_series(1, 30) g; +SELECT corr(0.1, 'NaN') FROM generate_series(1, 30) g; +SELECT corr('NaN', 'NaN') FROM generate_series(1, 30) g; + -- test accum and combine functions directly CREATE TABLE regr_test (x float8, y float8); INSERT INTO regr_test VALUES (10,150),(20,250),(30,350),(80,540),(100,200); @@ -148,7 +166,7 @@ FROM regr_test WHERE x IN (10,20,30,80); SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) FROM regr_test; SELECT float8_accum('{4,140,2900}'::float8[], 100); -SELECT float8_regr_accum('{4,140,2900,1290,83075,15050}'::float8[], 200, 100); +SELECT float8_regr_accum('{4,140,2900,1290,83075,15050,100,0}'::float8[], 200, 100); SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) FROM regr_test WHERE x IN (10,20,30); SELECT count(*), sum(x), regr_sxx(y,x), sum(y),regr_syy(y,x), regr_sxy(y,x) @@ -156,12 +174,12 @@ FROM regr_test WHERE x IN (80,100); SELECT float8_combine('{3,60,200}'::float8[], '{0,0,0}'::float8[]); SELECT float8_combine('{0,0,0}'::float8[], '{2,180,200}'::float8[]); SELECT float8_combine('{3,60,200}'::float8[], '{2,180,200}'::float8[]); -SELECT float8_regr_combine('{3,60,200,750,20000,2000}'::float8[], - '{0,0,0,0,0,0}'::float8[]); -SELECT float8_regr_combine('{0,0,0,0,0,0}'::float8[], - '{2,180,200,740,57800,-3400}'::float8[]); -SELECT float8_regr_combine('{3,60,200,750,20000,2000}'::float8[], - '{2,180,200,740,57800,-3400}'::float8[]); +SELECT float8_regr_combine('{3,60,200,750,20000,2000,1,NaN}'::float8[], + '{0,0,0,0,0,0,0,0}'::float8[]); +SELECT float8_regr_combine('{0,0,0,0,0,0,0,0}'::float8[], + '{2,180,200,740,57800,-3400,NaN,1}'::float8[]); +SELECT float8_regr_combine('{3,60,200,750,20000,2000,7,8}'::float8[], + '{2,180,200,740,57800,-3400,7,9}'::float8[]); DROP TABLE regr_test; -- test count, distinct @@ -182,6 +200,11 @@ SELECT newcnt(*) AS cnt_1000 FROM onek; SELECT oldcnt(*) AS cnt_1000 FROM onek; SELECT sum2(q1,q2) FROM int8_tbl; +-- sanity checks +SELECT sum(q1+q2), sum(q1)+sum(q2) FROM int8_tbl; +SELECT sum(q1-q2), sum(q2-q1), sum(q1)-sum(q2) FROM int8_tbl; +SELECT sum(q1*2000), sum(-q1*2000), 2000*sum(q1) FROM int8_tbl; + -- test for outer-level aggregates -- this should work @@ -223,6 +246,16 @@ select array(select sum(x+y) s from generate_series(1,3) y group by y order by s) from generate_series(1,3) x; +-- Test handling of grouping-expression references within sublinks + +select two + four as g, (select f1 from int4_tbl where f1 = (two + four)) +from tenk1 t1 +group by two + four order by 1; + +select q1, (select q1) as ss -- q1 is actually a COALESCE expression here +from int8_tbl i81 full outer join int8_tbl i82 using (q1) +group by q1 order by q1; + -- -- test for bitwise integer aggregates -- @@ -411,11 +444,16 @@ explain (costs off) select max(unique2), generate_series(1,3) as g from tenk1 order by g desc; select max(unique2), generate_series(1,3) as g from tenk1 order by g desc; --- interesting corner case: constant gets optimized into a seqscan +-- two interesting corner cases: both non-null and null constant gets +-- optimized into a seqscan explain (costs off) select max(100) from tenk1; select max(100) from tenk1; +explain (costs off) + select max(null) from tenk1; +select max(null) from tenk1; + -- try it on an inheritance tree create table minmaxtest(f1 int); create table minmaxtest1() inherits (minmaxtest); @@ -544,6 +582,60 @@ drop table t2; drop table t3; drop table p_t1; +-- +-- Test GROUP BY ALL +-- +-- We don't care about the data here, just the proper transformation of the +-- GROUP BY clause, so test some queries and verify the EXPLAIN plans. +-- + +CREATE TEMP TABLE t1 ( + a int, + b int, + c int +); + +-- basic example +EXPLAIN (COSTS OFF) SELECT b, COUNT(*) FROM t1 GROUP BY ALL; + +-- multiple columns, non-consecutive order +EXPLAIN (COSTS OFF) SELECT a, SUM(b), b FROM t1 GROUP BY ALL; + +-- multi columns, no aggregate +EXPLAIN (COSTS OFF) SELECT a + b FROM t1 GROUP BY ALL; + +-- check we detect a non-top-level aggregate +EXPLAIN (COSTS OFF) SELECT a, SUM(b) + 4 FROM t1 GROUP BY ALL; + +-- including grouped column is okay +EXPLAIN (COSTS OFF) SELECT a, SUM(b) + a FROM t1 GROUP BY ALL; + +-- including non-grouped column, not so much +EXPLAIN (COSTS OFF) SELECT a, SUM(b) + c FROM t1 GROUP BY ALL; + +-- all aggregates, should reduce to GROUP BY () +EXPLAIN (COSTS OFF) SELECT COUNT(a), SUM(b) FROM t1 GROUP BY ALL; + +-- likewise with empty target list +EXPLAIN (COSTS OFF) SELECT FROM t1 GROUP BY ALL; + +-- window functions are not to be included in GROUP BY, either +EXPLAIN (COSTS OFF) SELECT a, COUNT(a) OVER (PARTITION BY a) FROM t1 GROUP BY ALL; + +-- all cols +EXPLAIN (COSTS OFF) SELECT *, count(*) FROM t1 GROUP BY ALL; + +-- group by all with grouping element(s) (equivalent to GROUP BY's +-- default behavior, explicit antithesis to GROUP BY DISTINCT) +EXPLAIN (COSTS OFF) SELECT a, count(*) FROM t1 GROUP BY ALL a; + +-- verify deparsing of GROUP BY ALL +CREATE TEMP VIEW v1 AS SELECT b, COUNT(*) FROM t1 GROUP BY ALL; +SELECT pg_get_viewdef('v1'::regclass); + +DROP VIEW v1; +DROP TABLE t1; + -- -- Test GROUP BY matching of join columns that are type-coerced due to USING -- @@ -567,6 +659,12 @@ select f2, count(*) from t1 x(x0,x1) left join (t1 left join t2 using(f2)) on (x0 = 0) group by f2; +-- check that we preserve join alias in GROUP BY expressions +create temp view v1 as +select f1::int from t1 left join t2 using (f1) group by f1; +select pg_get_viewdef('v1'::regclass); + +drop view v1; drop table t1, t2; -- @@ -1049,6 +1147,43 @@ select cleast_agg(4.5,f1) from int4_tbl; select cleast_agg(variadic array[4.5,f1]) from int4_tbl; select pg_typeof(cleast_agg(variadic array[4.5,f1])) from int4_tbl; +-- +-- Test SupportRequestSimplifyAggref code +-- +begin; +create table agg_simplify (a int, not_null_col int not null, nullable_col int); + +-- Ensure count(not_null_col) uses count(*) +explain (costs off, verbose) +select count(not_null_col) from agg_simplify; + +-- Ensure count() uses count(*) +explain (costs off, verbose) +select count('bananas') from agg_simplify; + +-- Ensure count(null) isn't optimized +explain (costs off, verbose) +select count(null) from agg_simplify; + +-- Ensure count(nullable_col) does not use count(*) +explain (costs off, verbose) +select count(nullable_col) from agg_simplify; + +-- Ensure there's no optimization with DISTINCT aggs +explain (costs off, verbose) +select count(distinct not_null_col) from agg_simplify; + +-- Ensure there's no optimization with ORDER BY aggs +explain (costs off, verbose) +select count(not_null_col order by not_null_col) from agg_simplify; + +-- Ensure we don't optimize to count(*) with agglevelsup > 0 +explain (costs off, verbose) +select a from agg_simplify a group by a +having exists (select 1 from onek b where count(a.not_null_col) = b.four); + +rollback; + -- test aggregates with common transition functions share the same states begin work; @@ -1505,15 +1640,6 @@ select v||'a', case when v||'a' = 'aa' then 1 else 0 end, count(*) from unnest(array['a','b']) u(v) group by v||'a' order by 1; --- Make sure that generation of HashAggregate for uniqification purposes --- does not lead to array overflow due to unexpected duplicate hash keys --- see CAFeeJoKKu0u+A_A9R9316djW-YW3-+Gtgvy3ju655qRHR3jtdA@mail.gmail.com -set enable_memoize to off; -explain (costs off) - select 1 from tenk1 - where (hundred, thousand) in (select twothousand, twothousand from onek); -reset enable_memoize; - -- -- Hash Aggregation Spill tests -- diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql index 5e20dc63337c9..528e6f548a4a1 100644 --- a/src/test/regress/sql/alter_generic.sql +++ b/src/test/regress/sql/alter_generic.sql @@ -459,6 +459,40 @@ ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) test_opclass_o ALTER OPERATOR FAMILY alt_opf19 USING btree DROP FUNCTION 5 (int4, int4); DROP OPERATOR FAMILY alt_opf19 USING btree; +-- +-- Property Graph +-- +SET SESSION AUTHORIZATION regress_alter_generic_user1; +CREATE PROPERTY GRAPH alt_graph1; +CREATE PROPERTY GRAPH alt_graph2; +CREATE PROPERTY GRAPH alt_graph3; + +ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph2; -- failed (name conflict) +ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph4; -- OK +ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user2; -- failed (no role membership) +ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user3; -- OK +ALTER PROPERTY GRAPH alt_graph4 SET SCHEMA alt_nsp2; -- OK +ALTER PROPERTY GRAPH alt_nsp2.alt_graph4 RENAME TO alt_graph2; -- OK +ALTER PROPERTY GRAPH alt_graph2 SET SCHEMA alt_nsp2; -- failed (name conflict) + +SET SESSION AUTHORIZATION regress_alter_generic_user2; +CREATE PROPERTY GRAPH alt_graph5; + +ALTER PROPERTY GRAPH alt_graph3 RENAME TO alt_graph5; -- failed (not owner) +ALTER PROPERTY GRAPH alt_graph5 RENAME TO alt_graph6; -- OK +ALTER PROPERTY GRAPH alt_graph3 OWNER TO regress_alter_generic_user2; -- failed (not owner) +ALTER PROPERTY GRAPH alt_graph6 OWNER TO regress_alter_generic_user3; -- failed (no role membership) +ALTER PROPERTY GRAPH alt_graph3 SET SCHEMA alt_nsp2; -- failed (not owner) + +RESET SESSION AUTHORIZATION; + +SELECT nspname, relname, rolname + FROM pg_class c, pg_namespace n, pg_authid a + WHERE c.relnamespace = n.oid AND c.relowner = a.oid + AND n.nspname in ('alt_nsp1', 'alt_nsp2') + AND c.relkind = 'g' + ORDER BY nspname, relname; + -- -- Statistics -- diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql index 5ce9d1e429f81..f5f13bbd3e7f6 100644 --- a/src/test/regress/sql/alter_table.sql +++ b/src/test/regress/sql/alter_table.sql @@ -2202,13 +2202,15 @@ SELECT conname as constraint, obj_description(oid, 'pg_constraint') as comment F -- filenode function call can return NULL for a relation dropped concurrently -- with the call's surrounding query, so ignore a NULL mapped_oid for -- relations that no longer exist after all calls finish. +-- Temporary relations are ignored, as not supported by pg_filenode_relation(). CREATE TEMP TABLE filenode_mapping AS SELECT oid, mapped_oid, reltablespace, relfilenode, relname FROM pg_class, pg_filenode_relation(reltablespace, pg_relation_filenode(oid)) AS mapped_oid -WHERE relkind IN ('r', 'i', 'S', 't', 'm') AND mapped_oid IS DISTINCT FROM oid; - +WHERE relkind IN ('r', 'i', 'S', 't', 'm') + AND relpersistence != 't' + AND mapped_oid IS DISTINCT FROM oid; SELECT m.* FROM filenode_mapping m LEFT JOIN pg_class c ON c.oid = m.oid WHERE c.oid IS NOT NULL OR m.mapped_oid IS NOT NULL; @@ -2329,6 +2331,14 @@ ALTER TABLE test_add_column \d test_add_column ALTER TABLE test_add_column ADD COLUMN IF NOT EXISTS c5 SERIAL CHECK (c5 > 10); +ALTER TABLE test_add_column + ADD c6 integer; -- omit COLUMN +ALTER TABLE test_add_column + ADD IF NOT EXISTS c6 integer; +ALTER TABLE test_add_column + DROP c6; -- omit COLUMN +ALTER TABLE test_add_column + DROP IF EXISTS c6; \d test_add_column* DROP TABLE test_add_column; \d test_add_column* @@ -2395,8 +2405,12 @@ CREATE TABLE nonpartitioned ( a int, b int ); -ALTER TABLE partitioned INHERIT nonpartitioned; -ALTER TABLE nonpartitioned INHERIT partitioned; +ALTER TABLE partitioned INHERIT nonpartitioned; -- fail +ALTER TABLE partitioned NO INHERIT nonpartitioned; -- fail +ALTER TABLE nonpartitioned INHERIT partitioned; -- fail +CREATE TABLE partitioned_p1 PARTITION OF partitioned FOR VALUES FROM (0, 0) TO (10, 100); +ALTER TABLE partitioned_p1 INHERIT nonpartitioned; -- fail +ALTER TABLE partitioned_p1 NO INHERIT nonpartitioned; -- fail -- cannot add NO INHERIT constraint to partitioned tables ALTER TABLE partitioned ADD CONSTRAINT chk_a CHECK (a > 0) NO INHERIT; @@ -2824,14 +2838,17 @@ DROP TABLE part_rpd; -- works fine ALTER TABLE range_parted2 DETACH PARTITION part_rp CONCURRENTLY; \d+ range_parted2 --- constraint should be created -\d part_rp -CREATE TABLE part_rp100 PARTITION OF range_parted2 (CHECK (a>=123 AND a<133 AND a IS NOT NULL)) FOR VALUES FROM (100) to (200); -ALTER TABLE range_parted2 DETACH PARTITION part_rp100 CONCURRENTLY; --- redundant constraint should not be created -\d part_rp100 DROP TABLE range_parted2; +-- Test that hash partitions continue to work after they're concurrently +-- detached (bugs #18371, #19070) +CREATE TABLE hash_parted2 (a int) PARTITION BY HASH(a); +CREATE TABLE part_hp PARTITION OF hash_parted2 FOR VALUES WITH (MODULUS 2, REMAINDER 0); +ALTER TABLE hash_parted2 DETACH PARTITION part_hp CONCURRENTLY; +DROP TABLE hash_parted2; +INSERT INTO part_hp VALUES (1); +DROP TABLE part_hp; + -- Check ALTER TABLE commands for partitioned tables and partitions -- cannot add/drop column to/from *only* the parent @@ -3069,6 +3086,23 @@ drop table attbl, atref; /* End test case for bug #17409 */ +/* Test case for bug #18970 */ + +create table attbl(a int); +create table atref(b attbl check ((b).a is not null)); +alter table attbl alter column a type numeric; -- someday this should work +alter table atref drop constraint atref_b_check; + +create statistics atref_stat on ((b).a is not null) from atref; +alter table attbl alter column a type numeric; -- someday this should work +drop statistics atref_stat; + +create index atref_idx on atref (((b).a)); +alter table attbl alter column a type numeric; -- someday this should work +drop table attbl, atref; + +/* End test case for bug #18970 */ + -- Test that ALTER TABLE rewrite preserves a clustered index -- for normal indexes and indexes on constraints. create table alttype_cluster (a int); diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql index 47d62c1d38d28..82837af7c4a57 100644 --- a/src/test/regress/sql/arrays.sql +++ b/src/test/regress/sql/arrays.sql @@ -528,6 +528,10 @@ select '[2147483646:2147483646]={1}'::int[]; select '[-2147483648:-2147483647]={1,2}'::int[]; -- all of the above should be accepted +-- some day we might allow these cases, but for now they're errors: +select array[]::oidvector; +select array[]::int2vector; + -- tests for array aggregates CREATE TEMP TABLE arraggtest ( f1 INT[], f2 TEXT[][], f3 FLOAT[]); @@ -555,19 +559,22 @@ SELECT max(f1), min(f1), max(f2), min(f2), max(f3), min(f3) FROM arraggtest; -- A few simple tests for arrays of composite types -create type comptype as (f1 int, f2 text); +create type comptype as (f1 int, f2 text, f3 int[]); create table comptable (c1 comptype, c2 comptype[]); -- XXX would like to not have to specify row() construct types here ... insert into comptable - values (row(1,'foo'), array[row(2,'bar')::comptype, row(3,'baz')::comptype]); + values (row(1,'foo',array[10,20]), array[row(2,'bar',array[30,40])::comptype, row(3,'baz',array[50,60])::comptype]); -- check that implicitly named array type _comptype isn't a problem create type _comptype as enum('fooey'); select * from comptable; select c2[2].f2 from comptable; +select c2[2].f3 from comptable; +select c2[2].f3[1:2] from comptable; +select c2[1:2].f3[1:2] from comptable; drop type _comptype; drop table comptable; diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql index 68c61dbc7d19c..6aaaa386abcec 100644 --- a/src/test/regress/sql/btree_index.sql +++ b/src/test/regress/sql/btree_index.sql @@ -143,38 +143,83 @@ SELECT proname, proargtypes, pronamespace ORDER BY proname DESC, proargtypes DESC, pronamespace DESC LIMIT 1; -- --- Add coverage for RowCompare quals whose rhs row has a NULL that ends scan +-- Forwards scan RowCompare qual whose row arg has a NULL that affects our +-- initial positioning strategy -- explain (costs off) SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) < ('abs', NULL) + WHERE (proname, proargtypes) >= ('abs', NULL) AND proname <= 'abs' ORDER BY proname, proargtypes, pronamespace; SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) < ('abs', NULL) + WHERE (proname, proargtypes) >= ('abs', NULL) AND proname <= 'abs' ORDER BY proname, proargtypes, pronamespace; -- --- Add coverage for backwards scan RowCompare quals whose rhs row has a NULL --- that ends scan +-- Forwards scan RowCompare quals whose row arg has a NULL that ends scan -- explain (costs off) SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) > ('abs', NULL) + WHERE proname >= 'abs' AND (proname, proargtypes) < ('abs', NULL) +ORDER BY proname, proargtypes, pronamespace; + +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname >= 'abs' AND (proname, proargtypes) < ('abs', NULL) +ORDER BY proname, proargtypes, pronamespace; + +-- +-- Backwards scan RowCompare qual whose row arg has a NULL that affects our +-- initial positioning strategy +-- +explain (costs off) +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname >= 'abs' AND (proname, proargtypes) <= ('abs', NULL) ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; SELECT proname, proargtypes, pronamespace FROM pg_proc - WHERE proname = 'abs' AND (proname, proargtypes) > ('abs', NULL) + WHERE proname >= 'abs' AND (proname, proargtypes) <= ('abs', NULL) ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; -- --- Add coverage for recheck of > key following array advancement on previous --- (left sibling) page that used a high key whose attribute value corresponding --- to the > key was -inf (due to being truncated when the high key was created). +-- Backwards scan RowCompare qual whose row arg has a NULL that ends scan +-- +explain (costs off) +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE (proname, proargtypes) > ('abs', NULL) AND proname <= 'abs' +ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; + +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE (proname, proargtypes) > ('abs', NULL) AND proname <= 'abs' +ORDER BY proname DESC, proargtypes DESC, pronamespace DESC; + +-- Makes B-Tree preprocessing deal with unmarking redundant keys that were +-- initially marked required (test case relies on current row compare +-- preprocessing limitations) +explain (costs off) +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname = 'zzzzzz' AND (proname, proargtypes) > ('abs', NULL) + AND pronamespace IN (1, 2, 3) AND proargtypes IN ('26 23', '5077') +ORDER BY proname, proargtypes, pronamespace; + +SELECT proname, proargtypes, pronamespace + FROM pg_proc + WHERE proname = 'zzzzzz' AND (proname, proargtypes) > ('abs', NULL) + AND pronamespace IN (1, 2, 3) AND proargtypes IN ('26 23', '5077') +ORDER BY proname, proargtypes, pronamespace; + +-- +-- Performs a recheck of > key following array advancement on previous (left +-- sibling) page that used a high key whose attribute value corresponding to +-- the > key was -inf (due to being truncated when the high key was created). -- -- XXX This relies on the assumption that tenk1_thous_tenthous has a truncated -- high key "(183, -inf)" on the first page that we'll scan. The test will only diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql index b7115f861044d..6746236ffec3b 100644 --- a/src/test/regress/sql/cluster.sql +++ b/src/test/regress/sql/cluster.sql @@ -76,7 +76,6 @@ INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass ORDER BY 1; - SELECT relname, relkind, EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast FROM pg_class c WHERE relname LIKE 'clstr_tst%' ORDER BY relname; @@ -229,10 +228,33 @@ SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM o CLUSTER clstrpart; ALTER TABLE clstrpart SET WITHOUT CLUSTER; ALTER TABLE clstrpart CLUSTER ON clstrpart_idx; +-- and they cannot get an index-ordered REPACK without an explicit index name +REPACK clstrpart USING INDEX; + +-- Check that REPACK sets new relfilenodes: it should process exactly the same +-- tables as CLUSTER did. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart USING INDEX clstrpart_idx; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + +-- And finally the same for REPACK w/o index. +DROP TABLE old_cluster_info; +DROP TABLE new_cluster_info; +CREATE TEMP TABLE old_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +REPACK clstrpart; +CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ; +SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C"; + +-- CONCURRENTLY doesn't like partitioned tables +REPACK (CONCURRENTLY) clstrpart; + DROP TABLE clstrpart; -- Ownership of partitions is checked -CREATE TABLE ptnowner(i int unique) PARTITION BY LIST (i); +CREATE TABLE ptnowner(i int unique not null) PARTITION BY LIST (i); CREATE INDEX ptnowner_i_idx ON ptnowner(i); CREATE TABLE ptnowner1 PARTITION OF ptnowner FOR VALUES IN (1); CREATE ROLE regress_ptnowner; @@ -240,6 +262,8 @@ CREATE TABLE ptnowner2 PARTITION OF ptnowner FOR VALUES IN (2); ALTER TABLE ptnowner1 OWNER TO regress_ptnowner; SET SESSION AUTHORIZATION regress_ptnowner; CLUSTER ptnowner USING ptnowner_i_idx; +ALTER TABLE ptnowner1 REPLICA IDENTITY USING INDEX ptnowner1_i_key; +REPACK (CONCURRENTLY) ptnowner1; RESET SESSION AUTHORIZATION; ALTER TABLE ptnowner OWNER TO regress_ptnowner; CREATE TEMP TABLE ptnowner_oldnodes AS @@ -247,7 +271,12 @@ CREATE TEMP TABLE ptnowner_oldnodes AS JOIN pg_class AS c ON c.oid=tree.relid; SET SESSION AUTHORIZATION regress_ptnowner; CLUSTER ptnowner USING ptnowner_i_idx; +-- still can't repack without a replica identity +ALTER TABLE ptnowner1 REPLICA IDENTITY DEFAULT; +REPACK (CONCURRENTLY) ptnowner1; RESET SESSION AUTHORIZATION; +SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a + JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C"; SELECT a.relname, a.relfilenode=b.relfilenode FROM pg_class a JOIN ptnowner_oldnodes b USING (oid) ORDER BY a.relname COLLATE "C"; DROP TABLE ptnowner; @@ -313,6 +342,60 @@ EXPLAIN (COSTS OFF) SELECT * FROM clstr_expression WHERE -a = -3 ORDER BY -a, b; SELECT * FROM clstr_expression WHERE -a = -3 ORDER BY -a, b; COMMIT; +---------------------------------------------------------------------- +-- +-- REPACK +-- +---------------------------------------------------------------------- + +-- REPACK handles individual tables identically to CLUSTER, but it's worth +-- checking if it handles table hierarchies identically as well. +REPACK clstr_tst USING INDEX clstr_tst_c; + +-- Verify that inheritance link still works +INSERT INTO clstr_tst_inh VALUES (0, 100, 'in child table 2'); +SELECT a,b,c,substring(d for 30), length(d) from clstr_tst; + +-- Verify that foreign key link still works +INSERT INTO clstr_tst (b, c) VALUES (1111, 'this should fail'); + +SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass +ORDER BY 1; + +-- Verify partial analyze works +REPACK (ANALYZE) clstr_tst (a); +REPACK (ANALYZE) clstr_tst; +REPACK (VERBOSE) clstr_tst (a); + +-- REPACK w/o argument performs no ordering, so we can only check which tables +-- have the relfilenode changed. +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_old AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); + +SET SESSION AUTHORIZATION regress_clstr_user; +SET client_min_messages = ERROR; -- order of "skipping" warnings may vary +REPACK; +RESET client_min_messages; + +RESET SESSION AUTHORIZATION; +CREATE TEMP TABLE relnodes_new AS +(SELECT relname, relfilenode +FROM pg_class +WHERE relname IN ('clstr_1', 'clstr_2', 'clstr_3')); + +-- Do the actual comparison. Unlike CLUSTER, clstr_3 should have been +-- processed because there is nothing like clustering index here. +SELECT o.relname FROM relnodes_old o +JOIN relnodes_new n ON o.relname = n.relname +WHERE o.relfilenode <> n.relfilenode +ORDER BY o.relname; + +-- concurrently +REPACK (CONCURRENTLY) pg_class; + -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index dbc190227d022..0bf65a63535a2 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -513,6 +513,10 @@ DROP TABLE test7; CREATE COLLATION testcoll_rulesx (provider = icu, locale = '', rules = '!!wrong!!'); +-- strength specified in the rules +CREATE COLLATION strength_in_rule (provider = icu, locale = 'und', deterministic = false, rules = '[strength 1]'); +SELECT 'a' = 'à' COLLATE strength_in_rule; -- true because of the rule + -- nondeterministic collations @@ -568,6 +572,9 @@ SELECT 'abc' <= 'ABC' COLLATE case_insensitive, 'abc' >= 'ABC' COLLATE case_inse SELECT array_sort('{a,B}'::text[] COLLATE case_insensitive); SELECT array_sort('{a,B}'::text[] COLLATE "C"); +-- test replace() at the end of the string (bug #19341) +SELECT replace('testX' COLLATE case_insensitive, 'x' COLLATE case_insensitive, 'er'); + -- test language tags CREATE COLLATION lt_insensitive (provider = icu, locale = 'en-u-ks-level1', deterministic = false); SELECT 'aBcD' COLLATE lt_insensitive = 'AbCd' COLLATE lt_insensitive; @@ -983,6 +990,51 @@ RESET enable_partitionwise_aggregate; RESET max_parallel_workers_per_gather; RESET enable_incremental_sort; +-- +-- Test for eager aggregation non-deterministic collation bug +-- + +CREATE TABLE eager_agg_t1 (id int, val text COLLATE case_insensitive); +CREATE TABLE eager_agg_t2 (val text COLLATE case_insensitive); + +INSERT INTO eager_agg_t1 SELECT 1, 'a' FROM generate_series(1, 50); +INSERT INTO eager_agg_t1 SELECT 1, 'A' FROM generate_series(1, 50); +INSERT INTO eager_agg_t2 VALUES ('A'); + +ANALYZE eager_agg_t1; +ANALYZE eager_agg_t2; + +-- Ensure that eager aggregation is not used for t1.val due to the +-- non-deterministic collation. +EXPLAIN (COSTS OFF) +SELECT t1.id, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id; + +-- Ensure it returns 1 row with count = 50 +SELECT t1.id, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id; + +-- Ensure that eager aggregation is not used when grouping by a column with +-- non-deterministic collation. +EXPLAIN (COSTS OFF) +SELECT t1.id, t1.val, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id, t1.val; + +-- Ensure it returns 1 row with count = 50 +SELECT t1.id, t1.val, count(t1.val) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.val = t2.val COLLATE "C" +GROUP BY t1.id, t1.val; + +DROP TABLE eager_agg_t1; +DROP TABLE eager_agg_t2; + -- virtual generated columns CREATE TABLE t5 ( a int, @@ -997,6 +1049,19 @@ INSERT INTO t5 (a, b) values (1, 'D1'), (2, 'D2'), (3, 'd1'); -- rewriting.) SELECT * FROM t5 ORDER BY c ASC, a ASC; +-- Check that DEFAULT expressions in SQL/JSON functions use the same collation +-- as the RETURNING type. Mismatched collations should raise an error. +CREATE DOMAIN d1 AS text COLLATE case_insensitive; +CREATE DOMAIN d2 AS text COLLATE "C"; +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT ('C' COLLATE "C") COLLATE case_insensitive ON EMPTY) = 'a'; -- true +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' ON EMPTY) = 'a'; -- true +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C'::d2 ON EMPTY) = 'a'; -- error +SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' COLLATE "C" ON EMPTY) = 'a'; -- error +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' ON EMPTY) = 'a'; -- true +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE case_insensitive ON EMPTY) = 'a'; -- true +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON EMPTY) = 'a'; -- error +SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE "C" ON EMPTY) = 'a'; -- error +DROP DOMAIN d1, d2; -- cleanup RESET search_path; diff --git a/src/test/regress/sql/compression.sql b/src/test/regress/sql/compression.sql index 490595fcfb263..ce5ea37a660ce 100644 --- a/src/test/regress/sql/compression.sql +++ b/src/test/regress/sql/compression.sql @@ -1,3 +1,8 @@ +-- Default set of tests for TOAST compression, independent on compression +-- methods supported by the build. + +CREATE SCHEMA pglz; +SET search_path TO pglz, public; \set HIDE_TOAST_COMPRESSION false -- ensure we get stable results regardless of installation's default @@ -8,53 +13,27 @@ CREATE TABLE cmdata(f1 text COMPRESSION pglz); CREATE INDEX idx ON cmdata(f1); INSERT INTO cmdata VALUES(repeat('1234567890', 1000)); \d+ cmdata -CREATE TABLE cmdata1(f1 TEXT COMPRESSION lz4); -INSERT INTO cmdata1 VALUES(repeat('1234567890', 1004)); -\d+ cmdata1 -- verify stored compression method in the data SELECT pg_column_compression(f1) FROM cmdata; -SELECT pg_column_compression(f1) FROM cmdata1; -- decompress data slice SELECT SUBSTR(f1, 200, 5) FROM cmdata; -SELECT SUBSTR(f1, 2000, 50) FROM cmdata1; -- copy with table creation SELECT * INTO cmmove1 FROM cmdata; \d+ cmmove1 SELECT pg_column_compression(f1) FROM cmmove1; --- copy to existing table -CREATE TABLE cmmove3(f1 text COMPRESSION pglz); -INSERT INTO cmmove3 SELECT * FROM cmdata; -INSERT INTO cmmove3 SELECT * FROM cmdata1; -SELECT pg_column_compression(f1) FROM cmmove3; - --- test LIKE INCLUDING COMPRESSION -CREATE TABLE cmdata2 (LIKE cmdata1 INCLUDING COMPRESSION); -\d+ cmdata2 -DROP TABLE cmdata2; - -- try setting compression for incompressible data type CREATE TABLE cmdata2 (f1 int COMPRESSION pglz); --- update using datum from different table -CREATE TABLE cmmove2(f1 text COMPRESSION pglz); -INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004)); -SELECT pg_column_compression(f1) FROM cmmove2; -UPDATE cmmove2 SET f1 = cmdata1.f1 FROM cmdata1; -SELECT pg_column_compression(f1) FROM cmmove2; - -- test externally stored compressed data CREATE OR REPLACE FUNCTION large_val() RETURNS TEXT LANGUAGE SQL AS 'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g'; CREATE TABLE cmdata2 (f1 text COMPRESSION pglz); INSERT INTO cmdata2 SELECT large_val() || repeat('a', 4000); SELECT pg_column_compression(f1) FROM cmdata2; -INSERT INTO cmdata1 SELECT large_val() || repeat('a', 4000); -SELECT pg_column_compression(f1) FROM cmdata1; -SELECT SUBSTR(f1, 200, 5) FROM cmdata1; SELECT SUBSTR(f1, 200, 5) FROM cmdata2; DROP TABLE cmdata2; @@ -76,76 +55,31 @@ ALTER TABLE cmdata2 ALTER COLUMN f1 SET STORAGE plain; INSERT INTO cmdata2 VALUES (repeat('123456789', 800)); SELECT pg_column_compression(f1) FROM cmdata2; --- test compression with materialized view -CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata1; -\d+ compressmv -SELECT pg_column_compression(f1) FROM cmdata1; -SELECT pg_column_compression(x) FROM compressmv; - --- test compression with partition -CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1); -CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0); -CREATE TABLE cmpart2(f1 text COMPRESSION pglz); - -ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1); -INSERT INTO cmpart VALUES (repeat('123456789', 1004)); -INSERT INTO cmpart VALUES (repeat('123456789', 4004)); -SELECT pg_column_compression(f1) FROM cmpart1; -SELECT pg_column_compression(f1) FROM cmpart2; - -- test compression with inheritance -CREATE TABLE cminh() INHERITS(cmdata, cmdata1); -- error -CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata); -- error CREATE TABLE cmdata3(f1 text); CREATE TABLE cminh() INHERITS (cmdata, cmdata3); -- test default_toast_compression GUC +-- suppress machine-dependent details +\set VERBOSITY terse SET default_toast_compression = ''; SET default_toast_compression = 'I do not exist compression'; -SET default_toast_compression = 'lz4'; SET default_toast_compression = 'pglz'; - --- test alter compression method -ALTER TABLE cmdata ALTER COLUMN f1 SET COMPRESSION lz4; -INSERT INTO cmdata VALUES (repeat('123456789', 4004)); -\d+ cmdata -SELECT pg_column_compression(f1) FROM cmdata; +\set VERBOSITY default ALTER TABLE cmdata2 ALTER COLUMN f1 SET COMPRESSION default; \d+ cmdata2 --- test alter compression method for materialized views -ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4; -\d+ compressmv - --- test alter compression method for partitioned tables -ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz; -ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4; - --- new data should be compressed with the current compression method -INSERT INTO cmpart VALUES (repeat('123456789', 1004)); -INSERT INTO cmpart VALUES (repeat('123456789', 4004)); -SELECT pg_column_compression(f1) FROM cmpart1; -SELECT pg_column_compression(f1) FROM cmpart2; +DROP TABLE cmdata2; -- VACUUM FULL does not recompress SELECT pg_column_compression(f1) FROM cmdata; VACUUM FULL cmdata; SELECT pg_column_compression(f1) FROM cmdata; --- test expression index -DROP TABLE cmdata2; -CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4); -CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2)); -INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM -generate_series(1, 50) g), VERSION()); - -- check data is ok SELECT length(f1) FROM cmdata; -SELECT length(f1) FROM cmdata1; SELECT length(f1) FROM cmmove1; -SELECT length(f1) FROM cmmove2; -SELECT length(f1) FROM cmmove3; CREATE TABLE badcompresstbl (a text COMPRESSION I_Do_Not_Exist_Compression); -- fails CREATE TABLE badcompresstbl (a text); diff --git a/src/test/regress/sql/compression_lz4.sql b/src/test/regress/sql/compression_lz4.sql new file mode 100644 index 0000000000000..3849f8618dee4 --- /dev/null +++ b/src/test/regress/sql/compression_lz4.sql @@ -0,0 +1,129 @@ +-- Tests for TOAST compression with lz4 + +SELECT NOT(enumvals @> '{lz4}') AS skip_test FROM pg_settings WHERE + name = 'default_toast_compression' \gset +\if :skip_test + \echo '*** skipping TOAST tests with lz4 (not supported) ***' + \quit +\endif + +CREATE SCHEMA lz4; +SET search_path TO lz4, public; + +\set HIDE_TOAST_COMPRESSION false + +-- Ensure we get stable results regardless of the installation's default. +-- We rely on this GUC value for a few tests. +SET default_toast_compression = 'pglz'; + +-- test creating table with compression method +CREATE TABLE cmdata_pglz(f1 text COMPRESSION pglz); +CREATE INDEX idx ON cmdata_pglz(f1); +INSERT INTO cmdata_pglz VALUES(repeat('1234567890', 1000)); +\d+ cmdata +CREATE TABLE cmdata_lz4(f1 TEXT COMPRESSION lz4); +INSERT INTO cmdata_lz4 VALUES(repeat('1234567890', 1004)); +\d+ cmdata1 + +-- verify stored compression method in the data +SELECT pg_column_compression(f1) FROM cmdata_lz4; + +-- decompress data slice +SELECT SUBSTR(f1, 200, 5) FROM cmdata_pglz; +SELECT SUBSTR(f1, 2000, 50) FROM cmdata_lz4; + +-- copy with table creation +SELECT * INTO cmmove1 FROM cmdata_lz4; +\d+ cmmove1 +SELECT pg_column_compression(f1) FROM cmmove1; + +-- test LIKE INCLUDING COMPRESSION. The GUC default_toast_compression +-- has no effect, the compression method from the table being copied. +CREATE TABLE cmdata2 (LIKE cmdata_lz4 INCLUDING COMPRESSION); +\d+ cmdata2 +DROP TABLE cmdata2; + +-- copy to existing table +CREATE TABLE cmmove3(f1 text COMPRESSION pglz); +INSERT INTO cmmove3 SELECT * FROM cmdata_pglz; +INSERT INTO cmmove3 SELECT * FROM cmdata_lz4; +SELECT pg_column_compression(f1) FROM cmmove3; + +-- update using datum from different table with LZ4 data. +CREATE TABLE cmmove2(f1 text COMPRESSION pglz); +INSERT INTO cmmove2 VALUES (repeat('1234567890', 1004)); +SELECT pg_column_compression(f1) FROM cmmove2; +UPDATE cmmove2 SET f1 = cmdata_lz4.f1 FROM cmdata_lz4; +SELECT pg_column_compression(f1) FROM cmmove2; + +-- test externally stored compressed data +CREATE OR REPLACE FUNCTION large_val_lz4() RETURNS TEXT LANGUAGE SQL AS +'select array_agg(fipshash(g::text))::text from generate_series(1, 256) g'; +CREATE TABLE cmdata2 (f1 text COMPRESSION lz4); +INSERT INTO cmdata2 SELECT large_val_lz4() || repeat('a', 4000); +SELECT pg_column_compression(f1) FROM cmdata2; +SELECT SUBSTR(f1, 200, 5) FROM cmdata2; +DROP TABLE cmdata2; +DROP FUNCTION large_val_lz4; + +-- test compression with materialized view +CREATE MATERIALIZED VIEW compressmv(x) AS SELECT * FROM cmdata_lz4; +\d+ compressmv +SELECT pg_column_compression(f1) FROM cmdata_lz4; +SELECT pg_column_compression(x) FROM compressmv; + +-- test compression with partition +CREATE TABLE cmpart(f1 text COMPRESSION lz4) PARTITION BY HASH(f1); +CREATE TABLE cmpart1 PARTITION OF cmpart FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE cmpart2(f1 text COMPRESSION pglz); + +ALTER TABLE cmpart ATTACH PARTITION cmpart2 FOR VALUES WITH (MODULUS 2, REMAINDER 1); +INSERT INTO cmpart VALUES (repeat('123456789', 1004)); +INSERT INTO cmpart VALUES (repeat('123456789', 4004)); +SELECT pg_column_compression(f1) FROM cmpart1; +SELECT pg_column_compression(f1) FROM cmpart2; + +-- test compression with inheritance +CREATE TABLE cminh() INHERITS(cmdata_pglz, cmdata_lz4); -- error +CREATE TABLE cminh(f1 TEXT COMPRESSION lz4) INHERITS(cmdata_pglz); -- error +CREATE TABLE cmdata3(f1 text); +CREATE TABLE cminh() INHERITS (cmdata_pglz, cmdata3); + +-- test default_toast_compression GUC +SET default_toast_compression = 'lz4'; + +-- test alter compression method +ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION lz4; +INSERT INTO cmdata_pglz VALUES (repeat('123456789', 4004)); +\d+ cmdata +SELECT pg_column_compression(f1) FROM cmdata_pglz; +ALTER TABLE cmdata_pglz ALTER COLUMN f1 SET COMPRESSION pglz; + +-- test alter compression method for materialized views +ALTER MATERIALIZED VIEW compressmv ALTER COLUMN x SET COMPRESSION lz4; +\d+ compressmv + +-- test alter compression method for partitioned tables +ALTER TABLE cmpart1 ALTER COLUMN f1 SET COMPRESSION pglz; +ALTER TABLE cmpart2 ALTER COLUMN f1 SET COMPRESSION lz4; + +-- new data should be compressed with the current compression method +INSERT INTO cmpart VALUES (repeat('123456789', 1004)); +INSERT INTO cmpart VALUES (repeat('123456789', 4004)); +SELECT pg_column_compression(f1) FROM cmpart1; +SELECT pg_column_compression(f1) FROM cmpart2; + +-- test expression index +CREATE TABLE cmdata2 (f1 TEXT COMPRESSION pglz, f2 TEXT COMPRESSION lz4); +CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2)); +INSERT INTO cmdata2 VALUES((SELECT array_agg(fipshash(g::TEXT))::TEXT FROM +generate_series(1, 50) g), VERSION()); + +-- check data is ok +SELECT length(f1) FROM cmdata_pglz; +SELECT length(f1) FROM cmdata_lz4; +SELECT length(f1) FROM cmmove1; +SELECT length(f1) FROM cmmove2; +SELECT length(f1) FROM cmmove3; + +\set HIDE_TOAST_COMPRESSION true diff --git a/src/test/regress/sql/compression_pglz.sql b/src/test/regress/sql/compression_pglz.sql new file mode 100644 index 0000000000000..a44af02afb7b3 --- /dev/null +++ b/src/test/regress/sql/compression_pglz.sql @@ -0,0 +1,53 @@ +-- +-- Tests for PGLZ compression +-- + +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_pglz_compress(bytea) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_pglz_decompress(bytea, int4, bool) + RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; + +-- Round-trip with pglz: compress then decompress. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, false) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 400, true) = + decode(repeat('abcd', 100), 'escape') AS roundtrip_ok; + +-- Decompression with rawsize too large, fails to fill the destination +-- buffer. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 500, true); + +-- Decompression with rawsize too small, fails with source not fully +-- consumed. +SELECT test_pglz_decompress(test_pglz_compress( + decode(repeat('abcd', 100), 'escape')), 100, true); + +-- Corrupted compressed data. Set control bit with read of a match tag, +-- no data follows. +SELECT length(test_pglz_decompress('\x01'::bytea, 1024, false)) AS ctrl_only_len; +SELECT test_pglz_decompress('\x01'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with read of a match tag, +-- 1 byte follows. +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, false); +SELECT test_pglz_decompress('\x01ff'::bytea, 1024, true); + +-- Corrupted compressed data. Set control bit with match tag where length +-- nibble is 3 bytes (extended length), no data follows. +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, false); +SELECT test_pglz_decompress('\x010f01'::bytea, 1024, true); + +-- Clean up +DROP FUNCTION test_pglz_compress; +DROP FUNCTION test_pglz_decompress; diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index 337baab7ced93..483c1e98372de 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -266,6 +266,58 @@ COPY COPY_TBL FROM :'filename'; SELECT * FROM COPY_TBL; +-- +-- CHECK constraints +-- ALTER TABLE ALTER CONSTRAINT [NOT] ENFORCED +create table parted_ch( + a int, b int, + constraint cc check (a > 10) not enforced, + constraint cc_1 check (b < 17) not enforced +) partition by range(a); +create table parted_ch_1 partition of parted_ch for values from (0) to (10) partition by list(b); +create table parted_ch_11 partition of parted_ch_1 for values in (0, 1, 22,4); +create table parted_ch_12 partition of parted_ch_1 for values in (2); +create table parted_ch_2(b int, a int, + constraint cc check (a > 10) not enforced, + constraint cc_1 check (b < 17) enforced, + constraint cc_2 check( a < 15) not enforced +); +alter table parted_ch attach partition parted_ch_2 for values from (10) to (20); + +insert into parted_ch values (1, 22), (9, 1), (16, 16); + +alter table parted_ch alter constraint cc_1 enforced; --error +update parted_ch set b = 4 where b = 22; +alter table parted_ch alter constraint cc_1 enforced; --ok + +create or replace view check_constraint_status as +select conname, conrelid::regclass, conenforced, convalidated +from pg_constraint +where conrelid::regclass::text ~* '^parted_ch' and contype = 'c' +order by conname, conrelid::regclass::text collate "C"; + +alter table parted_ch alter constraint cc not enforced; --no-op +alter table parted_ch alter constraint cc enforced; --error +delete from parted_ch where a = 1; +alter table parted_ch alter constraint cc enforced; --error +delete from parted_ch where a = 9; +alter table parted_ch alter constraint cc enforced; + +--check these CHECK constraint status +select * from check_constraint_status; + +alter table parted_ch_2 alter constraint cc_2 enforced; --error +delete from parted_ch where a = 16; +alter table parted_ch_2 alter constraint cc_2 enforced; +alter table parted_ch_2 alter constraint cc not enforced; +alter table parted_ch_2 alter constraint cc_1 not enforced; +alter table parted_ch_2 alter constraint cc_2 not enforced; + +--check these CHECK constraint status again +select * from check_constraint_status; +drop table parted_ch; +drop view check_constraint_status; + -- -- Primary keys -- @@ -537,6 +589,9 @@ CREATE TABLE UNIQUE_NOTEN_TBL(i int UNIQUE NOT ENFORCED); ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key ENFORCED; ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT ENFORCED; +-- can't make an existing constraint NOT VALID +ALTER TABLE unique_tbl ALTER CONSTRAINT unique_tbl_i_key NOT VALID; + DROP TABLE unique_tbl; -- @@ -565,6 +620,9 @@ INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') -- fail, because DO UPDATE variant requires unique index INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2; +-- fail, because DO SELECT variant requires unique index +INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>') + ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *; -- succeed because c1 doesn't overlap INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>'); -- succeed because c2 doesn't overlap @@ -620,7 +678,9 @@ DROP TABLE deferred_excl; -- verify constraints created for NOT NULL clauses CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL); \d+ notnull_tbl1 --- no-op +-- specifying an existing constraint is a no-op +ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_tbl1_a_not_null NOT NULL a; +-- but using a different constraint name is not allowed ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; \d+ notnull_tbl1 -- duplicate name @@ -829,6 +889,9 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; -- cannot add primary key on a column with an invalid not-null ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a); +-- cannot set column as generated-as-identity if it has an invalid not-null +ALTER TABLE notnull_tbl1 ALTER COLUMN a ADD GENERATED ALWAYS AS IDENTITY; + -- ALTER column SET NOT NULL validates an invalid constraint (but this fails -- because of rows with null values) ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; @@ -997,6 +1060,9 @@ create table constr_parent3 (a int not null); create table constr_child3 () inherits (constr_parent2, constr_parent3); EXECUTE get_nnconstraint_info('{constr_parent3, constr_child3}'); +COMMENT ON CONSTRAINT constr_parent2_a_not_null ON constr_parent2 IS 'this constraint is invalid'; +COMMENT ON CONSTRAINT constr_parent2_a_not_null ON constr_child2 IS 'this constraint is valid'; + DEALLOCATE get_nnconstraint_info; -- end NOT NULL NOT VALID @@ -1037,3 +1103,14 @@ DROP DOMAIN constraint_comments_dom; DROP ROLE regress_constraint_comments; DROP ROLE regress_constraint_comments_noaccess; + +-- Leave some constraints for the pg_upgrade test to pick up +CREATE DOMAIN constraint_comments_dom AS int; + +ALTER DOMAIN constraint_comments_dom ADD CONSTRAINT inv_ck CHECK (value > 0) NOT VALID; +COMMENT ON CONSTRAINT inv_ck ON DOMAIN constraint_comments_dom IS 'comment on invalid constraint'; + +-- Create a table that exercises pg_upgrade +CREATE TABLE regress_notnull1 (a integer); +CREATE TABLE regress_notnull2 () INHERITS (regress_notnull1); +ALTER TABLE ONLY regress_notnull2 ALTER COLUMN a SET NOT NULL; diff --git a/src/test/regress/sql/conversion.sql b/src/test/regress/sql/conversion.sql index a80d62367a20a..c9a76cb014ffe 100644 --- a/src/test/regress/sql/conversion.sql +++ b/src/test/regress/sql/conversion.sql @@ -298,6 +298,7 @@ insert into gb18030_inputs values ('\x666f6fcff3', 'valid'), ('\x666f6f8431a530', 'valid, no translation to UTF-8'), ('\x666f6f84309c38', 'valid, translates to UTF-8 by mapping function'), + ('\xa6d9', 'valid, changed from version 2000 to 2022'), ('\x666f6f84309c', 'incomplete char '), ('\x666f6f84309c0a', 'incomplete char, followed by newline '), ('\x666f6f84', 'incomplete char at end'), @@ -328,7 +329,6 @@ select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'iso8859-5')).* fr -- Test conversions from ISO-8859-5 select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'utf8')).* from iso8859_5_inputs; select description, inbytes, (test_conv(inbytes, 'iso8859-5', 'koi8r')).* from iso8859_5_inputs; -select description, inbytes, (test_conv(inbytes, 'iso8859_5', 'mule_internal')).* from iso8859_5_inputs; -- -- Big5 @@ -345,29 +345,3 @@ insert into big5_inputs values select description, inbytes, (test_conv(inbytes, 'big5', 'big5')).* from big5_inputs; -- Test conversions from Big5 select description, inbytes, (test_conv(inbytes, 'big5', 'utf8')).* from big5_inputs; -select description, inbytes, (test_conv(inbytes, 'big5', 'mule_internal')).* from big5_inputs; - --- --- MULE_INTERNAL --- -CREATE TABLE mic_inputs (inbytes bytea, description text); -insert into mic_inputs values - ('\x666f6f', 'valid, pure ASCII'), - ('\x8bc68bcf8bcf', 'valid (in KOI8R)'), - ('\x8bc68bcf8b', 'invalid,incomplete char'), - ('\x92bedd', 'valid (in SHIFT_JIS)'), - ('\x92be', 'invalid, incomplete char)'), - ('\x666f6f95a3c1', 'valid (in Big5)'), - ('\x666f6f95a3', 'invalid, incomplete char'), - ('\x9200bedd', 'invalid, NUL byte'), - ('\x92bedd00', 'invalid, NUL byte'), - ('\x8b00c68bcf8bcf', 'invalid, NUL byte'); - --- Test MULE_INTERNAL verification -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'mule_internal')).* from mic_inputs; --- Test conversions from MULE_INTERNAL -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'koi8r')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'iso8859-5')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'sjis')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'big5')).* from mic_inputs; -select description, inbytes, (test_conv(inbytes, 'mule_internal', 'euc_jp')).* from mic_inputs; diff --git a/src/test/regress/sql/copy.sql b/src/test/regress/sql/copy.sql index f0b88a23db853..eaad290b257bc 100644 --- a/src/test/regress/sql/copy.sql +++ b/src/test/regress/sql/copy.sql @@ -82,6 +82,107 @@ this is just a line full of junk that would error out if parsed copy copytest3 to stdout csv header; +--- test copying in JSON mode with various styles +copy (select 1 union all select 2) to stdout with (format json); +copy (select 1 as foo union all select 2) to stdout with (format json); +copy (values (1), (2)) TO stdout with (format json); +copy (select 1 union all select 2) to stdout with (format json, force_array true); +copy (values (1), (2)) TO stdout with (format json, force_array true); +copy copytest to stdout json; +copy copytest to stdout (format json); +copy (select * from copytest) to stdout (format json); + +-- all of the following should yield error +copy copytest to stdout (format json, delimiter '|'); +copy copytest to stdout (format json, null '\N'); +copy copytest to stdout (format json, default '|'); +copy copytest to stdout (format json, header); +copy copytest to stdout (format json, header 1); +copy copytest to stdout (format json, quote '"'); +copy copytest to stdout (format json, escape '"'); +copy copytest to stdout (format json, force_quote *); +copy copytest to stdout (format json, force_not_null *); +copy copytest to stdout (format json, force_null *); +copy copytest to stdout (format json, on_error ignore); +copy copytest to stdout (format json, reject_limit 1); +copy copytest from stdin(format json); +-- all of the above should yield error + +-- column list with json format +copy copytest (style, test, filler) to stdout (format json); + +-- should fail: force_array requires json format +copy copytest to stdout (format csv, force_array true); + +-- force_array variants +copy copytest to stdout (format json, force_array); +copy copytest(style, test) to stdout (format json, force_array true); +copy copytest to stdout (format json, force_array false); + +-- force_array with empty result set +copy (select 1 where false) to stdout (format json, force_array); + +-- column list with diverse data types +create temp table copyjsontest_types ( + id int, + js json, + jsb jsonb, + arr int[], + n numeric(10,2), + b boolean, + ts timestamp, + t text); + +insert into copyjsontest_types values +(1, '{"a":1}', '{"b":2}', '{1,2,3}', 3.14, true, + '2024-01-15 10:30:00', 'hello'), +(2, '[1,null,"x"]', '{"nested":{"k":"v"}}', '{4,5}', -99.99, false, + '2024-06-30 23:59:59', 'world'), +(3, 'null', 'null', '{}', null, null, null, null); + +-- full table +copy copyjsontest_types to stdout (format json); + +-- column subsets exercising each type +copy copyjsontest_types (id, js, jsb) to stdout (format json); +copy copyjsontest_types (id, arr, n, b) to stdout (format json); +copy copyjsontest_types (jsb, t) to stdout (format json); +copy copyjsontest_types (id, ts) to stdout (format json); + +-- single column: json and jsonb +copy copyjsontest_types (js) to stdout (format json); +copy copyjsontest_types (jsb) to stdout (format json); + +drop table copyjsontest_types; + +-- embedded escaped characters +create temp table copyjsontest ( + id bigserial, + f1 text, + f2 timestamptz); + +insert into copyjsontest + select g.i, + CASE WHEN g.i % 2 = 0 THEN + 'line with '' in it: ' || g.i::text + ELSE + 'line with " in it: ' || g.i::text + END, + 'Mon Feb 10 17:32:01 1997 PST' + from generate_series(1,5) as g(i); + +insert into copyjsontest (f1) values +(E'aaa\"bbb'::text), +(E'aaa\\bbb'::text), +(E'aaa\/bbb'::text), +(E'aaa\bbbb'::text), +(E'aaa\fbbb'::text), +(E'aaa\nbbb'::text), +(E'aaa\rbbb'::text), +(E'aaa\tbbb'::text); + +copy copyjsontest to stdout json; + create temp table copytest4 ( c1 int, "colname with tab: " text); @@ -94,6 +195,51 @@ this is just a line full of junk that would error out if parsed copy copytest4 to stdout (header); +-- test multi-line header line feature + +create temp table copytest5 (c1 int); + +copy copytest5 from stdin (format csv, header 2); +this is a first header line. +this is a second header line. +1 +2 +\. +copy copytest5 to stdout (header); + +truncate copytest5; +copy copytest5 from stdin (format csv, header 4); +this is a first header line. +this is a second header line. +1 +2 +\. +select count(*) from copytest5; + +truncate copytest5; +copy copytest5 from stdin (format csv, header 5); +this is a first header line. +this is a second header line. +1 +2 +\. +select count(*) from copytest5; + +-- test header line feature (given as strings) +truncate copytest5; +copy copytest5 from stdin (format csv, header '0'); +1 +2 +\. +select * from copytest5 order by c1; + +truncate copytest5; +copy copytest5 from stdin (format csv, header '1'); +1 +2 +\. +select * from copytest5 order by c1; + -- test copy from with a partitioned table create table parted_copytest ( a int, @@ -375,3 +521,17 @@ COPY copytest_mv(id) TO stdout WITH (header); REFRESH MATERIALIZED VIEW copytest_mv; COPY copytest_mv(id) TO stdout WITH (header); DROP MATERIALIZED VIEW copytest_mv; + +-- Tests for COPY TO with partitioned tables. +-- The child table pp_2 has a different column order than the root table pp. +-- Check if COPY TO exports tuples as the root table's column order. +CREATE TABLE pp (id int,val int) PARTITION BY RANGE (id); +CREATE TABLE pp_1 (val int, id int) PARTITION BY RANGE (id); +CREATE TABLE pp_2 (id int, val int) PARTITION BY RANGE (id); +ALTER TABLE pp ATTACH PARTITION pp_1 FOR VALUES FROM (1) TO (5); +ALTER TABLE pp ATTACH PARTITION pp_2 FOR VALUES FROM (5) TO (10); +CREATE TABLE pp_15 PARTITION OF pp_1 FOR VALUES FROM (1) TO (5); +CREATE TABLE pp_510 PARTITION OF pp_2 FOR VALUES FROM (5) TO (10); +INSERT INTO pp SELECT g, 10 + g FROM generate_series(1,6) g; +COPY pp TO stdout(header); +DROP TABLE PP; diff --git a/src/test/regress/sql/copy2.sql b/src/test/regress/sql/copy2.sql index 45273557ce040..e0810109473b4 100644 --- a/src/test/regress/sql/copy2.sql +++ b/src/test/regress/sql/copy2.sql @@ -67,12 +67,15 @@ COPY x from stdin (force_null (a), force_null (b)); COPY x from stdin (convert_selectively (a), convert_selectively (b)); COPY x from stdin (encoding 'sql_ascii', encoding 'sql_ascii'); COPY x from stdin (on_error ignore, on_error ignore); +COPY x from stdin (on_error set_null, on_error set_null); COPY x from stdin (log_verbosity default, log_verbosity verbose); -- incorrect options COPY x from stdin (format BINARY, delimiter ','); COPY x from stdin (format BINARY, null 'x'); COPY x from stdin (format BINARY, on_error ignore); +COPY x from stdin (format BINARY, on_error set_null); +COPY x from stdin (on_error set_null, reject_limit 2); COPY x from stdin (on_error unsupported); COPY x from stdin (format TEXT, force_quote(a)); COPY x from stdin (format TEXT, force_quote *); @@ -87,9 +90,16 @@ COPY x from stdin (format TEXT, force_null *); COPY x to stdout (format CSV, force_null(a)); COPY x to stdout (format CSV, force_null *); COPY x to stdout (format BINARY, on_error unsupported); +COPY x to stdout (on_error set_null); COPY x from stdin (log_verbosity unsupported); COPY x from stdin with (reject_limit 1); COPY x from stdin with (on_error ignore, reject_limit 0); +COPY x from stdin with (header -1); +COPY x from stdin with (header 2.5); +COPY x to stdout with (header 2); +COPY x to stdout with (header '-1'); +COPY x from stdin with (header '2.5'); +COPY x to stdout with (header '2'); -- too many columns in column list: should fail COPY x (a, b, c, d, e, d, c) from stdin; @@ -158,6 +168,8 @@ COPY x from stdin WHERE a IN (generate_series(1,5)); COPY x from stdin WHERE a = row_number() over(b); +COPY x from stdin WHERE tableoid = 'x'::regclass; + -- check results of copy in SELECT * FROM x; @@ -534,6 +546,42 @@ a {2} 2 8 {8} 8 \. +CREATE DOMAIN d_int_not_null AS integer NOT NULL CHECK (value > 0); +CREATE DOMAIN d_int_positive_maybe_null AS integer CHECK (value > 0); +CREATE TABLE t_on_error_null (a d_int_not_null, b d_int_positive_maybe_null, c integer); + +\pset null NULL +COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail +\N 11 13 +\. + +COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail +ss 11 14 +\. + +COPY t_on_error_null FROM STDIN WITH (on_error set_null); -- fail +-1 11 13 +\. + +-- fail, less data. +COPY t_on_error_null FROM STDIN WITH (delimiter ',', on_error set_null); +1,1 +\. +-- fail, extra data. +COPY t_on_error_null FROM STDIN WITH (delimiter ',', on_error set_null); +1,2,3,4 +\. + +COPY t_on_error_null FROM STDIN WITH (on_error set_null, log_verbosity verbose); -- ok +10 x1 yx +11 zx 12 +13 14 ea +\. + +SELECT * FROM t_on_error_null ORDER BY a; + +\pset null '' + -- tests for on_error option with log_verbosity and null constraint via domain CREATE DOMAIN dcheck_ign_err2 varchar(15) NOT NULL; CREATE TABLE check_ign_err2 (n int, m int[], k int, l dcheck_ign_err2); @@ -603,6 +651,9 @@ DROP VIEW instead_of_insert_tbl_view; DROP VIEW instead_of_insert_tbl_view_2; DROP FUNCTION fun_instead_of_insert_tbl(); DROP TABLE check_ign_err; +DROP TABLE t_on_error_null; +DROP DOMAIN d_int_not_null; +DROP DOMAIN d_int_positive_maybe_null; DROP TABLE check_ign_err2; DROP DOMAIN dcheck_ign_err2; DROP TABLE hard_err; diff --git a/src/test/regress/sql/copyencoding.sql b/src/test/regress/sql/copyencoding.sql index 4e96a4d6505cf..64718245b94f1 100644 --- a/src/test/regress/sql/copyencoding.sql +++ b/src/test/regress/sql/copyencoding.sql @@ -23,6 +23,13 @@ CREATE TABLE copy_encoding_tab (t text); COPY (SELECT E'\u3042') TO :'utf8_csv' WITH (FORMAT csv, ENCODING 'UTF8'); -- Read UTF8 data as LATIN1: no error COPY copy_encoding_tab FROM :'utf8_csv' WITH (FORMAT csv, ENCODING 'LATIN1'); +-- Non-server encodings have distinct code paths. +\set fname :abs_builddir '/results/copyencoding_gb18030.csv' +COPY (SELECT E'\u3042,') TO :'fname' WITH (FORMAT csv, ENCODING 'GB18030'); +COPY copy_encoding_tab FROM :'fname' WITH (FORMAT csv, ENCODING 'GB18030'); +\set fname :abs_builddir '/results/copyencoding_gb18030.data' +COPY (SELECT E'\u3042,') TO :'fname' WITH (FORMAT text, ENCODING 'GB18030'); +COPY copy_encoding_tab FROM :'fname' WITH (FORMAT text, ENCODING 'GB18030'); -- Use client_encoding SET client_encoding TO UTF8; diff --git a/src/test/regress/sql/create_function_sql.sql b/src/test/regress/sql/create_function_sql.sql index 6d1c102d78082..4543273f93aad 100644 --- a/src/test/regress/sql/create_function_sql.sql +++ b/src/test/regress/sql/create_function_sql.sql @@ -241,6 +241,17 @@ SELECT functest_S_14(); DROP TABLE functest3 CASCADE; +-- Check reporting of temporary-object dependencies within SQL-standard body +-- (tests elsewhere already cover dependencies on arg and result types) +CREATE TEMP SEQUENCE mytempseq; + +CREATE FUNCTION functest_tempseq() RETURNS int + RETURN nextval('mytempseq'); + +-- This discards mytempseq and therefore functest_tempseq(). If it fails to, +-- the function will appear in the information_schema tests below. +DISCARD TEMP; + -- information_schema tests @@ -432,6 +443,23 @@ $$ SELECT array_append($1, $2) || array_append($1, $2) $$; SELECT double_append(array_append(ARRAY[q1], q2), q3) FROM (VALUES(1,2,3), (4,5,6)) v(q1,q2,q3); +-- Check that we can re-use a SQLFunctionCache after a run-time error. + +-- This function will fail with zero-divide at run time (not plan time). +CREATE FUNCTION part_hashint4_error(value int4, seed int8) RETURNS int8 +LANGUAGE SQL STRICT IMMUTABLE PARALLEL SAFE AS +$$ SELECT value + seed + random()::int/0 $$; + +-- Put it into an operator class so that FmgrInfo will be cached in relcache. +CREATE OPERATOR CLASS part_test_int4_ops_bad FOR TYPE int4 USING hash AS + FUNCTION 2 part_hashint4_error(int4, int8); + +CREATE TABLE pt(i int) PARTITION BY hash (i part_test_int4_ops_bad); +CREATE TABLE p1 PARTITION OF pt FOR VALUES WITH (modulus 4, remainder 0); + +INSERT INTO pt VALUES (1); +INSERT INTO pt VALUES (1); + -- Things that shouldn't work: CREATE FUNCTION test1 (int) RETURNS int LANGUAGE SQL diff --git a/src/test/regress/sql/create_index.sql b/src/test/regress/sql/create_index.sql index e21ff426519b0..82e4062a215f8 100644 --- a/src/test/regress/sql/create_index.sql +++ b/src/test/regress/sql/create_index.sql @@ -45,6 +45,10 @@ CREATE INDEX six ON shighway USING btree (name text_ops); COMMENT ON INDEX six_wrong IS 'bad index'; COMMENT ON INDEX six IS 'good index'; COMMENT ON INDEX six IS NULL; +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; +COMMENT ON INDEX six IS 'add the comment back'; +COMMENT ON INDEX six IS ''; -- empty string removes the comment, same as NULL +SELECT obj_description('six'::regclass, 'pg_class') IS NULL AS six_comment_is_null; -- -- BTREE partial indices @@ -635,7 +639,7 @@ DROP TABLE cwi_test; CREATE TABLE syscol_table (a INT); -- System columns cannot be indexed -CREATE INDEX ON syscolcol_table (ctid); +CREATE INDEX ON syscol_table (ctid); -- nor used in expressions CREATE INDEX ON syscol_table ((ctid >= '(1000,0)')); diff --git a/src/test/regress/sql/create_operator.sql b/src/test/regress/sql/create_operator.sql index a3096f17df0e2..19d30e1846076 100644 --- a/src/test/regress/sql/create_operator.sql +++ b/src/test/regress/sql/create_operator.sql @@ -22,6 +22,13 @@ CREATE OPERATOR #%# ( -- Test operator created above SELECT @#@ 24; +-- Test error cases +select @@##@@ 24; -- no such operator +set search_path = pg_catalog; +select @#@ 24; -- wrong schema +reset search_path; +select @#@ 24.0; -- wrong data type + -- Test comments COMMENT ON OPERATOR ###### (NONE, int4) IS 'bad prefix'; COMMENT ON OPERATOR ###### (int4, NONE) IS 'bad postfix'; diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql new file mode 100644 index 0000000000000..241f93df30285 --- /dev/null +++ b/src/test/regress/sql/create_property_graph.sql @@ -0,0 +1,365 @@ +CREATE SCHEMA create_property_graph_tests; +GRANT USAGE ON SCHEMA create_property_graph_tests TO PUBLIC; +SET search_path = create_property_graph_tests; +CREATE SCHEMA create_property_graph_tests_2; +GRANT USAGE ON SCHEMA create_property_graph_tests_2 TO PUBLIC; + +CREATE ROLE regress_graph_user1; +CREATE ROLE regress_graph_user2; + +CREATE PROPERTY GRAPH g1; + +COMMENT ON PROPERTY GRAPH g1 IS 'a graph'; + +CREATE PROPERTY GRAPH g1; -- error: duplicate + +CREATE TABLE t1 (a int, b text); +CREATE TABLE t2 (i int PRIMARY KEY, j int, k int); +CREATE TABLE t3 (x int, y text, z text); + +CREATE TABLE e1 (a int, i int, t text, PRIMARY KEY (a, i)); +CREATE TABLE e2 (a int, x int, t text); + +CREATE PROPERTY GRAPH g2 + VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL, t3 KEY (x) LABEL t3l1 LABEL t3l2) + EDGE TABLES ( + e1 + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (i) REFERENCES t2 (i), + e2 KEY (a, x) + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (x, t) REFERENCES t3 (x, y) + ); + +-- test dependencies/object descriptions + +DROP TABLE t1; -- fail +ALTER TABLE t1 DROP COLUMN b; -- non-key column; fail +ALTER TABLE t1 DROP COLUMN a; -- key column; fail + +-- like g2 but assembled with ALTER +CREATE PROPERTY GRAPH g3; +ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL); +ALTER PROPERTY GRAPH g3 + ADD VERTEX TABLES (t3 KEY (x) LABEL t3l1) + ADD EDGE TABLES ( + e1 SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i), + e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) + ); +ALTER PROPERTY GRAPH g3 + ALTER VERTEX TABLE t3 + ADD LABEL t3l2 PROPERTIES ALL COLUMNS + ADD LABEL t3l3 PROPERTIES ALL COLUMNS; +ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3x; -- error +ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3; +ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2); -- fail +ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2) CASCADE; +ALTER PROPERTY GRAPH g3 DROP EDGE TABLES (e2); + +CREATE PROPERTY GRAPH g4 + VERTEX TABLES ( + t1 KEY (a) NO PROPERTIES, + t2 DEFAULT LABEL PROPERTIES (i + j AS i_j, k), + t3 KEY (x) LABEL t3l1 PROPERTIES (x, y AS yy) LABEL t3l2 PROPERTIES (x, z AS zz) + ) + EDGE TABLES ( + e1 + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (i) REFERENCES t2 (i) + PROPERTIES ALL COLUMNS, + e2 KEY (a, x) + SOURCE KEY (a) REFERENCES t1 (a) + DESTINATION KEY (x, t) REFERENCES t3 (x, y) + PROPERTIES ALL COLUMNS + ); + +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 ADD PROPERTIES (k * 2 AS kk); +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 DROP PROPERTIES (k); + +CREATE TABLE t11 (a int PRIMARY KEY); +CREATE TABLE t12 (b int PRIMARY KEY); +CREATE TABLE t13 ( + c int PRIMARY KEY, + d int REFERENCES t11, + e int REFERENCES t12 +); + +CREATE PROPERTY GRAPH g5 + VERTEX TABLES (t11, t12) + EDGE TABLES (t13 SOURCE t11 DESTINATION t12); + +SELECT pg_get_propgraphdef('g5'::regclass); + +-- error cases +CREATE UNLOGGED PROPERTY GRAPH gx VERTEX TABLES (xx, yy); +CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy); +CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a)); +ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t3 KEY (x)); -- duplicate alias +CREATE PROPERTY GRAPH gx + VERTEX TABLES (t1 AS tt KEY (a), t2 KEY (i)) + EDGE TABLES ( + e1 SOURCE t1 DESTINATION t2 + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES (t1 KEY (a), t2 KEY (i)) + EDGE TABLES ( + e1 SOURCE t1 DESTINATION tx + ); +COMMENT ON PROPERTY GRAPH gx IS 'not a graph'; +CREATE PROPERTY GRAPH gx + VERTEX TABLES (t1 KEY (a), t2) + EDGE TABLES ( + e1 SOURCE t1 DESTINATION t2 -- no foreign keys + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) + LABEL foo PROPERTIES (a + 1 AS aa) + LABEL bar PROPERTIES (1 + a AS aa) -- expression mismatch + ); +ALTER PROPERTY GRAPH g2 + ADD VERTEX TABLES ( + t1 AS t1x KEY (a) + LABEL foo PROPERTIES (a + 1 AS aa) + LABEL bar PROPERTIES (1 + a AS aa) -- expression mismatch + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) PROPERTIES (b AS p1), + t2 PROPERTIES (k AS p1) -- type mismatch + ); +ALTER PROPERTY GRAPH g2 ALTER VERTEX TABLE t1 ADD LABEL foo PROPERTIES (b AS k); -- type mismatch + +CREATE TABLE t1x (a int, b varchar(10)); +CREATE TABLE t2x (i int, j varchar(15)); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1x KEY (a) PROPERTIES (b AS p1), + t2x KEY (i) PROPERTIES (j AS p1) -- typmod mismatch + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1x KEY (a) PROPERTIES (b::varchar(20) AS p1), + t2x KEY (i) PROPERTIES (j::varchar(25) AS p1) -- typmod mismatch + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1x KEY (a) PROPERTIES (b::varchar(20) AS p1), + t2x KEY (i) PROPERTIES (j::varchar(20) AS p1) -- matching typmods by casting works + ); +DROP PROPERTY GRAPH gx; +DROP TABLE t1x, t2x; + +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) LABEL l1 PROPERTIES (a, a AS aa), + t2 KEY (i) LABEL l1 PROPERTIES (i AS a, j AS b, k) -- mismatching number of properties on label + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) LABEL l1 PROPERTIES (a, b), + t2 KEY (i) LABEL l1 PROPERTIES (i AS a) -- mismatching number of properties on label + ); +CREATE PROPERTY GRAPH gx + VERTEX TABLES ( + t1 KEY (a) LABEL l1 PROPERTIES (a, b), + t2 KEY (i) LABEL l1 PROPERTIES (i AS a, j AS j) -- mismatching property names on label + ); +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t1 ADD LABEL t3l1 PROPERTIES (a AS x, b AS yy, b AS zz); -- mismatching number of properties on label +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t1 ADD LABEL t3l1 PROPERTIES (a AS x, b AS zz); -- mismatching property names on label +ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t1 ADD LABEL t3l1 PROPERTIES (a AS x); -- mismatching number of properties on label + +ALTER PROPERTY GRAPH g1 OWNER TO regress_graph_user1; +SET ROLE regress_graph_user1; +GRANT SELECT ON PROPERTY GRAPH g1 TO regress_graph_user2; +GRANT UPDATE ON PROPERTY GRAPH g1 TO regress_graph_user2; -- fail +RESET ROLE; + +-- collation + +CREATE TABLE tc1 (a int, b text); +CREATE TABLE tc2 (a int, b text); +CREATE TABLE tc3 (a int, b text COLLATE "C"); + +CREATE TABLE ec1 (ek1 int, ek2 int, eb text); +CREATE TABLE ec2 (ek1 int, ek2 int, eb text COLLATE "POSIX"); + +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a), tc2 KEY (a), tc3 KEY (a)); -- fail +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a), tc2 KEY (a)) + EDGE TABLES ( + ec1 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a), + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + ); -- fail +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a) DEFAULT LABEL PROPERTIES (a), tc3 KEY (b)) + EDGE TABLES ( + ec2 KEY (ek1, eb) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (eb) REFERENCES tc3 (b) + ); -- fail +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES (tc1 KEY (a), tc2 KEY (a)) + EDGE TABLES ( + ec1 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + ); +ALTER PROPERTY GRAPH gc1 ADD VERTEX TABLES (tc3 KEY (a)); -- fail +ALTER PROPERTY GRAPH gc1 + ADD EDGE TABLES ( + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + ); -- fail +ALTER PROPERTY GRAPH gc1 + ADD VERTEX TABLES ( + tc3 KEY (a) DEFAULT LABEL PROPERTIES (a, b COLLATE pg_catalog.DEFAULT AS b) + ); +ALTER PROPERTY GRAPH gc1 + ADD EDGE TABLES ( + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + DEFAULT LABEL PROPERTIES (ek1, ek2, eb COLLATE pg_catalog.DEFAULT AS eb) + ); +DROP PROPERTY GRAPH gc1; +CREATE PROPERTY GRAPH gc1 + VERTEX TABLES ( + tc1 KEY (a) DEFAULT LABEL PROPERTIES (a, b::varchar COLLATE "C" AS b), + tc2 KEY (a) DEFAULT LABEL PROPERTIES (a, (b COLLATE "C")::varchar AS b), + tc3 KEY (a) DEFAULT LABEL PROPERTIES (a, b::varchar AS b) + ) + EDGE TABLES ( + ec1 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + DEFAULT LABEL PROPERTIES (ek1, ek2, eb), + ec2 KEY (ek1, ek2) + SOURCE KEY (ek1) REFERENCES tc1 (a) + DESTINATION KEY (ek2) REFERENCES tc2 (a) + DEFAULT LABEL PROPERTIES (ek1, ek2, eb COLLATE pg_catalog.DEFAULT AS eb) + ); + +-- type inconsistency check + +CREATE TABLE v1 (a int primary key, b text); +CREATE TABLE e(k1 text, k2 text, c text); +CREATE TABLE v2 (m text, n text); +CREATE PROPERTY GRAPH gt + VERTEX TABLES (v1 KEY (a), v2 KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2(m) + ); -- fail +ALTER TABLE e DROP COLUMN k1, ADD COLUMN k1 bigint primary key; +CREATE PROPERTY GRAPH gt + VERTEX TABLES (v1 KEY (a), v2 KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2(m) + ); + +-- information schema + +SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name; +SELECT * FROM information_schema.pg_element_tables ORDER BY property_graph_name, element_table_alias; +SELECT * FROM information_schema.pg_element_table_key_columns ORDER BY property_graph_name, element_table_alias, ordinal_position; +SELECT * FROM information_schema.pg_edge_table_components ORDER BY property_graph_name, edge_table_alias, edge_end DESC, ordinal_position; +SELECT * FROM information_schema.pg_element_table_labels ORDER BY property_graph_name, element_table_alias, label_name; +SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_graph_name, element_table_alias, property_name; +SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_name, label_name, property_name; +SELECT * FROM information_schema.pg_labels ORDER BY property_graph_name, label_name; +SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_name, property_name; +SELECT * FROM information_schema.pg_property_graph_privileges WHERE grantee LIKE 'regress%' ORDER BY property_graph_name, grantor, grantee, privilege_type; + +-- test object address functions +SELECT pg_describe_object(classid, objid, objsubid) as obj, + pg_describe_object(refclassid, refobjid, refobjsubid) as reference_graph + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.g2'::regclass + ORDER BY 1, 2; +SELECT (pg_identify_object_as_address(classid, objid, objsubid)).* + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.g2'::regclass + ORDER BY 1, 2, 3; +SELECT (pg_identify_object(classid, objid, objsubid)).* + FROM pg_depend + WHERE refclassid = 'pg_class'::regclass AND + refobjid = 'create_property_graph_tests.g2'::regclass + ORDER BY 1, 2, 3, 4; + +\a\t +SELECT pg_get_propgraphdef('g2'::regclass); +SELECT pg_get_propgraphdef('g3'::regclass); +SELECT pg_get_propgraphdef('g4'::regclass); + +SELECT pg_get_propgraphdef('pg_type'::regclass); -- error +\a\t + +-- Test \d variants for property graphs +\dG g1 +\dG+ g1 +\dGx g1 +\d g2 +\d g1 +\d+ g2 +\d+ g1 +\dG g_nonexistent +\dG t11 +\set QUIET 'off' +\dG g_nonexistent +\set QUIET 'on' + +-- temporary property graph + +-- Keep this at the end to avoid test failure due to changing temporary +-- namespace names in information schema query outputs +CREATE TEMPORARY PROPERTY GRAPH g1; -- same name as persistent graph +DROP PROPERTY GRAPH g1; -- drops temporary graph retaining persistent graph +\dG g1 +CREATE TEMPORARY TABLE v2tmp (m text, n text); +CREATE TEMPORARY PROPERTY GRAPH gtmp + VERTEX TABLES (v1 KEY (a), v2tmp KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2tmp(m) + ); +DROP PROPERTY GRAPH gtmp; +CREATE PROPERTY GRAPH gtmp + VERTEX TABLES (v1 KEY (a), v2tmp KEY (m)) + EDGE TABLES ( + e KEY (k1, k2) + SOURCE KEY (k1) REFERENCES v1(a) + DESTINATION KEY (k2) REFERENCES v2tmp(m) + ); +ALTER PROPERTY GRAPH g1 + ADD VERTEX TABLES (v2tmp KEY (m)); -- error + + +-- DROP, ALTER SET SCHEMA, ALTER PROPERTY GRAPH RENAME TO + +DROP TABLE g2; -- error: wrong object type +CREATE VIEW vg1 AS SELECT * FROM GRAPH_TABLE(g1 MATCH () COLUMNS (1 AS one)); +DROP PROPERTY GRAPH g1; -- error +ALTER PROPERTY GRAPH g1 SET SCHEMA create_property_graph_tests_2; +ALTER PROPERTY GRAPH create_property_graph_tests_2.g1 RENAME TO g2; +DROP PROPERTY GRAPH create_property_graph_tests_2.g2 CASCADE; +DROP PROPERTY GRAPH g1; -- error +ALTER PROPERTY GRAPH g1 ADD VERTEX TABLES (t1 KEY (a)); -- error +ALTER PROPERTY GRAPH IF EXISTS g1 SET SCHEMA create_property_graph_tests_2; +DROP PROPERTY GRAPH IF EXISTS g1; + +DROP ROLE regress_graph_user1, regress_graph_user2; + +-- leave remaining objects behind for pg_upgrade/pg_dump tests diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql index 4491a28a8ae99..b22f9c6f50aa3 100644 --- a/src/test/regress/sql/create_role.sql +++ b/src/test/regress/sql/create_role.sql @@ -92,6 +92,13 @@ CREATE ROLE regress_hasprivs CREATEROLE LOGIN INHERIT CONNECTION LIMIT 5; -- ok, we should be able to modify a role we created COMMENT ON ROLE regress_hasprivs IS 'some comment'; +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NOT NULL AS has_comment; +COMMENT ON ROLE regress_hasprivs IS NULL; +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NULL AS no_comment; +COMMENT ON ROLE regress_hasprivs IS 'add the comment back'; +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NOT NULL AS has_comment; +COMMENT ON ROLE regress_hasprivs IS ''; -- empty string removes the comment, same as NULL +SELECT shobj_description('regress_hasprivs'::regrole, 'pg_authid') IS NULL AS no_comment; ALTER ROLE regress_hasprivs RENAME TO regress_tenant; ALTER ROLE regress_tenant NOINHERIT NOLOGIN CONNECTION LIMIT 7; diff --git a/src/test/regress/sql/create_schema.sql b/src/test/regress/sql/create_schema.sql index 1b7064247a1c6..ebe05d5110ecf 100644 --- a/src/test/regress/sql/create_schema.sql +++ b/src/test/regress/sql/create_schema.sql @@ -8,7 +8,7 @@ CREATE ROLE regress_create_schema_role SUPERUSER; -- Cases where schema creation fails as objects are qualified with a schema -- that does not match with what's expected. --- This checks all the object types that include schema qualifications. +-- This checks most object types that include schema qualifications. CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE SEQUENCE schema_not_existing.seq; CREATE SCHEMA AUTHORIZATION regress_create_schema_role @@ -20,6 +20,9 @@ CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE SCHEMA AUTHORIZATION regress_create_schema_role CREATE TRIGGER schema_trig BEFORE INSERT ON schema_not_existing.tab EXECUTE FUNCTION schema_trig.no_func(); +CREATE SCHEMA AUTHORIZATION regress_create_schema_role + CREATE FUNCTION schema_not_existing.func(int) RETURNS int + AS 'SELECT $1' LANGUAGE sql; -- Again, with a role specification and no schema names. SET ROLE regress_create_schema_role; CREATE SCHEMA AUTHORIZATION CURRENT_ROLE @@ -47,6 +50,11 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE EXECUTE FUNCTION schema_trig.no_func(); RESET ROLE; +-- Forward references no longer work in general. +CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE + CREATE VIEW abcd_view AS SELECT a FROM abcd + CREATE TABLE abcd (a int); + -- Cases where the schema creation succeeds. -- The schema created matches the role name. CREATE SCHEMA AUTHORIZATION regress_create_schema_role @@ -66,5 +74,103 @@ CREATE SCHEMA regress_schema_1 AUTHORIZATION CURRENT_ROLE DROP SCHEMA regress_schema_1 CASCADE; RESET ROLE; +-- Test forward-referencing foreign key clauses. +CREATE SCHEMA regress_schema_fk + CREATE TABLE regress_schema_fk.t2 ( + b int, + a int REFERENCES t1 DEFERRABLE INITIALLY DEFERRED NOT ENFORCED + REFERENCES t3 DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t1 NOT DEFERRABLE) + + CREATE TABLE regress_schema_fk.t1 (a int PRIMARY KEY) + + CREATE TABLE t3 (a int PRIMARY KEY) + + CREATE TABLE t4 ( + b int, + a int REFERENCES t5 NOT DEFERRABLE ENFORCED + REFERENCES t6 DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT fk FOREIGN KEY (a) REFERENCES t6 DEFERRABLE INITIALLY DEFERRED) + + CREATE TABLE t5 (a int, b int, PRIMARY KEY (a)) + + CREATE TABLE t6 (a int, b int, PRIMARY KEY (a)); + +\d regress_schema_fk.t2 +\d regress_schema_fk.t4 + +DROP SCHEMA regress_schema_fk CASCADE; + +-- Test miscellaneous object types within CREATE SCHEMA. +CREATE SCHEMA regress_schema_misc + CREATE AGGREGATE cs_sum(int4) + ( + SFUNC = int4_sum(int8, int4), + STYPE = int8, + INITCOND = '0' + ) + + CREATE COLLATION cs_builtin_c ( PROVIDER = builtin, LOCALE = "C" ) + + CREATE DOMAIN cs_positive AS integer CHECK (VALUE > 0) + + CREATE FUNCTION cs_add(int4, int4) returns int4 language sql + as 'select $1 + $2' + + CREATE OPERATOR + (function = cs_add, leftarg = int4, rightarg = int4) + + CREATE PROCEDURE cs_proc(int4, int4) + BEGIN ATOMIC SELECT cs_add($1,$2); END + + CREATE TEXT SEARCH CONFIGURATION cs_ts_conf (copy=english) + + CREATE TEXT SEARCH DICTIONARY cs_ts_dict (template=simple) + + CREATE TEXT SEARCH PARSER cs_ts_prs + (start = prsd_start, gettoken = prsd_nexttoken, end = prsd_end, + lextypes = prsd_lextype) + + CREATE TEXT SEARCH TEMPLATE cs_ts_temp (lexize=dsimple_lexize) + + CREATE TYPE regress_schema_misc.cs_enum AS ENUM ('red', 'orange') + + CREATE TYPE cs_composite AS (a int, b float8) + + CREATE TYPE cs_range AS RANGE (subtype = float8, subtype_diff = float8mi) + + -- demonstrate creation of a base type with its I/O functions + + CREATE TYPE cs_type + + CREATE FUNCTION cs_type_in(cstring) + RETURNS cs_type LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT + AS 'int4in' + + CREATE FUNCTION cs_type_out(cs_type) + RETURNS cstring LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT + AS 'int4out' + + CREATE TYPE cs_type ( + INPUT = cs_type_in, + OUTPUT = cs_type_out, + LIKE = int4 + ) + + GRANT USAGE ON TYPE cs_type TO public +; + +\df regress_schema_misc.cs_add +\df regress_schema_misc.cs_proc +\da regress_schema_misc.cs_sum +\do regress_schema_misc.+ +\dO regress_schema_misc.* +\dT regress_schema_misc.* +\dF regress_schema_misc.* +\dFd regress_schema_misc.* +\dFp regress_schema_misc.* +\dFt regress_schema_misc.* + +DROP SCHEMA regress_schema_misc CASCADE; + -- Clean up DROP ROLE regress_create_schema_role; diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql index 37a227148e9c2..80e424e6bdad3 100644 --- a/src/test/regress/sql/create_table.sql +++ b/src/test/regress/sql/create_table.sql @@ -68,6 +68,14 @@ CREATE TABLE withoid() WITH (oids = true); CREATE TEMP TABLE withoutoid() WITHOUT OIDS; DROP TABLE withoutoid; CREATE TEMP TABLE withoutoid() WITH (oids = false); DROP TABLE withoutoid; +-- temporary tables are ignored by pg_filenode_relation(). +CREATE TEMP TABLE relation_filenode_check(c1 int); +SELECT relpersistence, + pg_filenode_relation (reltablespace, pg_relation_filenode(oid)) + FROM pg_class + WHERE relname = 'relation_filenode_check'; +DROP TABLE relation_filenode_check; + -- check restriction with default expressions -- invalid use of column reference in default expressions CREATE TABLE default_expr_column (id int DEFAULT (id)); @@ -97,6 +105,13 @@ SAVEPOINT q; DROP TABLE remember_node_subid; ROLLBACK TO q; COMMIT; DROP TABLE remember_node_subid; +-- generated NOT NULL constraint names must not collide with explicitly named constraints +CREATE TABLE two_not_null_constraints ( + col integer NOT NULL, + CONSTRAINT two_not_null_constraints_col_not_null CHECK (col IS NOT NULL) +); +DROP TABLE two_not_null_constraints; + -- -- Partitioned tables -- diff --git a/src/test/regress/sql/create_table_like.sql b/src/test/regress/sql/create_table_like.sql index 6e21722aaeb95..93389b57dbf95 100644 --- a/src/test/regress/sql/create_table_like.sql +++ b/src/test/regress/sql/create_table_like.sql @@ -130,6 +130,7 @@ DROP TABLE inhz; -- including storage and comments CREATE TABLE ctlt1 (a text CHECK (length(a) > 2) ENFORCED PRIMARY KEY, b text CHECK (length(b) > 100) NOT ENFORCED); +ALTER TABLE ctlt1 ADD CONSTRAINT cc CHECK (length(b) > 100) NOT VALID; CREATE INDEX ctlt1_b_key ON ctlt1 (b); CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b)); CREATE STATISTICS ctlt1_a_b_stat ON a,b FROM ctlt1; @@ -143,9 +144,10 @@ COMMENT ON INDEX ctlt1_pkey IS 'index pkey'; COMMENT ON INDEX ctlt1_b_key IS 'index b_key'; ALTER TABLE ctlt1 ALTER COLUMN a SET STORAGE MAIN; -CREATE TABLE ctlt2 (c text); +CREATE TABLE ctlt2 (c text NOT NULL); ALTER TABLE ctlt2 ALTER COLUMN c SET STORAGE EXTERNAL; COMMENT ON COLUMN ctlt2.c IS 'C'; +COMMENT ON CONSTRAINT ctlt2_c_not_null ON ctlt2 IS 't2_c_not_null'; CREATE TABLE ctlt3 (a text CHECK (length(a) < 5), c text CHECK (length(c) < 7)); ALTER TABLE ctlt3 ALTER COLUMN c SET STORAGE EXTERNAL; @@ -162,6 +164,7 @@ CREATE TABLE ctlt12_storage (LIKE ctlt1 INCLUDING STORAGE, LIKE ctlt2 INCLUDING \d+ ctlt12_storage CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDING COMMENTS); \d+ ctlt12_comments +SELECT conname, description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt12_comments'::regclass; CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INHERITS (ctlt1); \d+ ctlt1_inh SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt1_inh'::regclass; @@ -197,9 +200,19 @@ DROP TABLE ctlt1, ctlt2, ctlt3, ctlt4, ctlt12_storage, ctlt12_comments, ctlt1_in -- LIKE must respect NO INHERIT property of constraints CREATE TABLE noinh_con_copy (a int CHECK (a > 0) NO INHERIT, b int not null, c int not null no inherit); -CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS); + +COMMENT ON CONSTRAINT noinh_con_copy_b_not_null ON noinh_con_copy IS 'not null b'; +COMMENT ON CONSTRAINT noinh_con_copy_c_not_null ON noinh_con_copy IS 'not null c no inherit'; + +CREATE TABLE noinh_con_copy1 (LIKE noinh_con_copy INCLUDING CONSTRAINTS INCLUDING COMMENTS); \d+ noinh_con_copy1 +SELECT conname, description +FROM pg_description, pg_constraint c +WHERE classoid = 'pg_constraint'::regclass +AND objoid = c.oid AND c.conrelid = 'noinh_con_copy1'::regclass +ORDER BY conname COLLATE "C"; + -- fail, as partitioned tables don't allow NO INHERIT constraints CREATE TABLE noinh_con_copy1_parted (LIKE noinh_con_copy INCLUDING ALL) PARTITION BY LIST (a); diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql index ae6841308b9b6..75b8cd5d0fb3f 100644 --- a/src/test/regress/sql/create_view.sql +++ b/src/test/regress/sql/create_view.sql @@ -179,8 +179,8 @@ CREATE VIEW v12_temp AS SELECT true FROM v11_temp; -- a view should also be temporary if it references a temporary sequence CREATE SEQUENCE seq1; CREATE TEMPORARY SEQUENCE seq1_temp; -CREATE VIEW v9 AS SELECT seq1.is_called FROM seq1; -CREATE VIEW v13_temp AS SELECT seq1_temp.is_called FROM seq1_temp; +CREATE VIEW v9 AS SELECT nextval('seq1'); +CREATE VIEW v13_temp AS SELECT nextval('seq1_temp'); SELECT relname FROM pg_class WHERE relname LIKE 'v_' diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql index 46ad2634781ea..4ef361272911e 100644 --- a/src/test/regress/sql/database.sql +++ b/src/test/regress/sql/database.sql @@ -2,7 +2,7 @@ CREATE DATABASE regression_tbd ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0; ALTER DATABASE regression_tbd RENAME TO regression_utf8; ALTER DATABASE regression_utf8 SET TABLESPACE regress_tblspace; -ALTER DATABASE regression_utf8 RESET TABLESPACE; +ALTER DATABASE regression_utf8 SET TABLESPACE pg_default; ALTER DATABASE regression_utf8 CONNECTION_LIMIT 123; -- Test PgDatabaseToastTable. Doing this with GRANT would be slow. @@ -11,7 +11,7 @@ UPDATE pg_database SET datacl = array_fill(makeaclitem(10, 10, 'USAGE', false), ARRAY[5e5::int]) WHERE datname = 'regression_utf8'; -- load catcache entry, if nothing else does -ALTER DATABASE regression_utf8 RESET TABLESPACE; +ALTER DATABASE regression_utf8 RENAME TO regression_rename_rolled_back; ROLLBACK; CREATE ROLE regress_datdba_before; diff --git a/src/test/regress/sql/database_ddl.sql b/src/test/regress/sql/database_ddl.sql new file mode 100644 index 0000000000000..89753ac641174 --- /dev/null +++ b/src/test/regress/sql/database_ddl.sql @@ -0,0 +1,66 @@ +-- +-- Tests for pg_get_database_ddl() +-- + +-- To produce stable regression test output, strip locale/collation details +-- from the DDL output. Uses a plain SQL function to avoid a PL/pgSQL +-- dependency. + +CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT) +RETURNS TEXT LANGUAGE sql AS $$ +SELECT regexp_replace( + regexp_replace( + regexp_replace( + regexp_replace( + regexp_replace( + ddl_input, + '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)', '', 'gi'), + '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1', '', 'gi'), + '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1', '', 'gi'), + '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1', '', 'gi'), + '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1', '', 'gi') +$$; + +CREATE ROLE regress_datdba; +CREATE DATABASE regression_database_ddl + ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0 + OWNER regress_datdba; +ALTER DATABASE regression_database_ddl CONNECTION_LIMIT 123; +ALTER DATABASE regression_database_ddl SET random_page_cost = 2.0; +ALTER ROLE regress_datdba IN DATABASE regression_database_ddl SET random_page_cost = 1.1; + +-- Database doesn't exist +SELECT * FROM pg_get_database_ddl('regression_database'); + +-- NULL value +SELECT * FROM pg_get_database_ddl(NULL); + +-- Invalid option value (should error) +SELECT * FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'invalid'); + +-- Duplicate option (should error) +SELECT * FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'false', 'owner', 'true'); + +-- Without options +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl'); + +-- With owner +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl', 'owner', 'true'); + +-- Pretty-printed output +\pset format unaligned +SELECT ddl_filter(pg_get_database_ddl) FROM pg_get_database_ddl('regression_database_ddl', 'pretty', 'true', 'tablespace', 'false'); +\pset format aligned + +-- Permission check: revoke CONNECT on database +CREATE ROLE regress_db_ddl_noaccess; +REVOKE CONNECT ON DATABASE regression_database_ddl FROM PUBLIC; +SET ROLE regress_db_ddl_noaccess; +SELECT * FROM pg_get_database_ddl('regression_database_ddl'); -- should fail +RESET ROLE; +GRANT CONNECT ON DATABASE regression_database_ddl TO PUBLIC; +DROP ROLE regress_db_ddl_noaccess; + +DROP DATABASE regression_database_ddl; +DROP FUNCTION ddl_filter(text); +DROP ROLE regress_datdba; diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql index b752a63ab5f69..b8f5a6397121a 100644 --- a/src/test/regress/sql/domain.sql +++ b/src/test/regress/sql/domain.sql @@ -602,6 +602,9 @@ insert into domain_test values (1, 2); -- should fail alter table domain_test add column c str_domain; +-- disallow duplicated not-null constraints +create domain int_domain1 as int constraint nn1 not null constraint nn2 not null; + create domain str_domain2 as text check (value <> 'foo') default 'foo'; -- should fail diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql new file mode 100644 index 0000000000000..53d9b377a64d7 --- /dev/null +++ b/src/test/regress/sql/eager_aggregate.sql @@ -0,0 +1,391 @@ +-- +-- EAGER AGGREGATION +-- Test we can push aggregation down below join +-- + +CREATE TABLE eager_agg_t1 (a int, b int, c double precision); +CREATE TABLE eager_agg_t2 (a int, b int, c double precision); +CREATE TABLE eager_agg_t3 (a int, b int, c double precision); + +INSERT INTO eager_agg_t1 SELECT i, i, i FROM generate_series(1, 1000) i; +INSERT INTO eager_agg_t2 SELECT i, i%10, i FROM generate_series(1, 1000) i; +INSERT INTO eager_agg_t3 SELECT i%10, i%10, i FROM generate_series(1, 1000) i; + +ANALYZE eager_agg_t1; +ANALYZE eager_agg_t2; +ANALYZE eager_agg_t3; + + +-- +-- Test eager aggregation over base rel +-- + +-- Perform scan of a table, aggregate the result, join it to the other table +-- and finalize the aggregation. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +-- Produce results with sorting aggregation +SET enable_hashagg TO off; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +RESET enable_hashagg; + + +-- +-- Test eager aggregation over join rel +-- + +-- Perform join of tables, aggregate the result, join it to the other table +-- and finalize the aggregation. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + +-- Produce results with sorting aggregation +SET enable_hashagg TO off; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c + t3.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b + JOIN eager_agg_t3 t3 ON t2.a = t3.a +GROUP BY t1.a ORDER BY t1.a; + +RESET enable_hashagg; + + +-- +-- Test that eager aggregation works for outer join +-- + +-- Ensure aggregation can be pushed down to the non-nullable side +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + RIGHT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + RIGHT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +-- Ensure aggregation cannot be pushed down to the nullable side +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.b, avg(t2.c) + FROM eager_agg_t1 t1 + LEFT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t2.b ORDER BY t2.b; + +SELECT t2.b, avg(t2.c) + FROM eager_agg_t1 t1 + LEFT JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t2.b ORDER BY t2.b; + + +-- +-- Test that eager aggregation works for parallel plans +-- + +SET parallel_setup_cost=0; +SET parallel_tuple_cost=0; +SET min_parallel_table_scan_size=0; +SET max_parallel_workers_per_gather=4; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +RESET parallel_setup_cost; +RESET parallel_tuple_cost; +RESET min_parallel_table_scan_size; +RESET max_parallel_workers_per_gather; + +-- +-- Test eager aggregation with GEQO +-- + +SET geqo = on; +SET geqo_threshold = 2; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +SELECT t1.a, avg(t2.c) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +RESET geqo; +RESET geqo_threshold; + +-- Ensure eager aggregation is not applied because random() is a volatile +-- function +EXPLAIN (COSTS OFF) +SELECT t1.a, avg(t2.c + random()) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +EXPLAIN (COSTS OFF) +SELECT t1.a, avg(t2.c) FILTER (WHERE random() > 0.5) + FROM eager_agg_t1 t1 + JOIN eager_agg_t2 t2 ON t1.b = t2.b +GROUP BY t1.a ORDER BY t1.a; + +DROP TABLE eager_agg_t1; +DROP TABLE eager_agg_t2; +DROP TABLE eager_agg_t3; + + +-- +-- Test eager aggregation for partitionwise join +-- + +-- Enable partitionwise aggregate, which by default is disabled. +SET enable_partitionwise_aggregate TO true; +-- Enable partitionwise join, which by default is disabled. +SET enable_partitionwise_join TO true; + +CREATE TABLE eager_agg_tab1(x int, y int) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab1_p1 PARTITION OF eager_agg_tab1 FOR VALUES FROM (0) TO (5); +CREATE TABLE eager_agg_tab1_p2 PARTITION OF eager_agg_tab1 FOR VALUES FROM (5) TO (10); +CREATE TABLE eager_agg_tab1_p3 PARTITION OF eager_agg_tab1 FOR VALUES FROM (10) TO (15); +CREATE TABLE eager_agg_tab2(x int, y int) PARTITION BY RANGE(y); +CREATE TABLE eager_agg_tab2_p1 PARTITION OF eager_agg_tab2 FOR VALUES FROM (0) TO (5); +CREATE TABLE eager_agg_tab2_p2 PARTITION OF eager_agg_tab2 FOR VALUES FROM (5) TO (10); +CREATE TABLE eager_agg_tab2_p3 PARTITION OF eager_agg_tab2 FOR VALUES FROM (10) TO (15); +INSERT INTO eager_agg_tab1 SELECT i % 15, i % 10 FROM generate_series(1, 1000) i; +INSERT INTO eager_agg_tab2 SELECT i % 10, i % 15 FROM generate_series(1, 1000) i; + +ANALYZE eager_agg_tab1; +ANALYZE eager_agg_tab2; + +-- When GROUP BY clause matches; full aggregation is performed for each +-- partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + +-- GROUP BY having other matching key +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.y, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.y ORDER BY t2.y; + +SELECT t2.y, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.y ORDER BY t2.y; + +-- When GROUP BY clause does not match; partial aggregation is performed for +-- each partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t2.x, sum(t1.x), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; + +SELECT t2.x, sum(t1.x), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t2.x HAVING avg(t1.x) > 5 ORDER BY t2.x; + +-- Check with eager aggregation over join rel +-- full aggregation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + +SELECT t1.x, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + +-- partial aggregation +SET enable_hashagg TO off; +SET max_parallel_workers_per_gather TO 0; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t3.y, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + +SELECT t3.y, sum(t2.y + t3.y) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab1 t2 ON t1.x = t2.x + JOIN eager_agg_tab1 t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + +RESET enable_hashagg; +RESET max_parallel_workers_per_gather; + +-- try that with GEQO too +SET geqo = on; +SET geqo_threshold = 2; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + +SELECT t1.x, sum(t1.y), count(*) + FROM eager_agg_tab1 t1 + JOIN eager_agg_tab2 t2 ON t1.x = t2.y +GROUP BY t1.x ORDER BY t1.x; + +RESET geqo; +RESET geqo_threshold; + +DROP TABLE eager_agg_tab1; +DROP TABLE eager_agg_tab2; + + +-- +-- Test with multi-level partitioning scheme +-- +CREATE TABLE eager_agg_tab_ml(x int, y int) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab_ml_p1 PARTITION OF eager_agg_tab_ml FOR VALUES FROM (0) TO (10); +CREATE TABLE eager_agg_tab_ml_p2 PARTITION OF eager_agg_tab_ml FOR VALUES FROM (10) TO (20) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab_ml_p2_s1 PARTITION OF eager_agg_tab_ml_p2 FOR VALUES FROM (10) TO (15); +CREATE TABLE eager_agg_tab_ml_p2_s2 PARTITION OF eager_agg_tab_ml_p2 FOR VALUES FROM (15) TO (20); +CREATE TABLE eager_agg_tab_ml_p3 PARTITION OF eager_agg_tab_ml FOR VALUES FROM (20) TO (30) PARTITION BY RANGE(x); +CREATE TABLE eager_agg_tab_ml_p3_s1 PARTITION OF eager_agg_tab_ml_p3 FOR VALUES FROM (20) TO (25); +CREATE TABLE eager_agg_tab_ml_p3_s2 PARTITION OF eager_agg_tab_ml_p3 FOR VALUES FROM (25) TO (30); +INSERT INTO eager_agg_tab_ml SELECT i % 30, i % 30 FROM generate_series(1, 1000) i; + +ANALYZE eager_agg_tab_ml; + +-- When GROUP BY clause matches; full aggregation is performed for each +-- partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + +-- When GROUP BY clause does not match; partial aggregation is performed for +-- each partition. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.y, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.y ORDER BY t1.y; + +SELECT t1.y, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.y ORDER BY t1.y; + +-- Check with eager aggregation over join rel +-- full aggregation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + +SELECT t1.x, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t1.x ORDER BY t1.x; + +-- partial aggregation +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t3.y, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + +SELECT t3.y, sum(t2.y + t3.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x + JOIN eager_agg_tab_ml t3 ON t2.x = t3.x +GROUP BY t3.y ORDER BY t3.y; + +-- try that with GEQO too +SET geqo = on; +SET geqo_threshold = 2; + +EXPLAIN (VERBOSE, COSTS OFF) +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + +SELECT t1.x, sum(t2.y), count(*) + FROM eager_agg_tab_ml t1 + JOIN eager_agg_tab_ml t2 ON t1.x = t2.x +GROUP BY t1.x ORDER BY t1.x; + +RESET geqo; +RESET geqo_threshold; + +DROP TABLE eager_agg_tab_ml; diff --git a/src/test/regress/sql/encoding.sql b/src/test/regress/sql/encoding.sql new file mode 100644 index 0000000000000..07d7dc8ff1834 --- /dev/null +++ b/src/test/regress/sql/encoding.sql @@ -0,0 +1,238 @@ +/* skip test if not UTF8 server encoding */ +SELECT getdatabaseencoding() <> 'UTF8' AS skip_test \gset +\if :skip_test +\quit +\endif + +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_bytea_to_text(bytea) RETURNS text + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_text_to_bytea(text) RETURNS bytea + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_mblen_func(text, text, text, int) RETURNS int + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_text_to_wchars(text, text) RETURNS int[] + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_wchars_to_text(text, int[]) RETURNS text + AS :'regresslib' LANGUAGE C STRICT; +CREATE FUNCTION test_valid_server_encoding(text) RETURNS boolean + AS :'regresslib' LANGUAGE C STRICT; + + +CREATE TABLE regress_encoding(good text, truncated text, with_nul text, truncated_with_nul text); +INSERT INTO regress_encoding +VALUES ('café', + 'caf' || test_bytea_to_text('\xc3'), + 'café' || test_bytea_to_text('\x00') || 'dcba', + 'caf' || test_bytea_to_text('\xc300') || 'dcba'); + +SELECT good, truncated, with_nul FROM regress_encoding; + +SELECT length(good) FROM regress_encoding; +SELECT substring(good, 3, 1) FROM regress_encoding; +SELECT substring(good, 4, 1) FROM regress_encoding; +SELECT regexp_replace(good, '^caf(.)$', '\1') FROM regress_encoding; +SELECT reverse(good) FROM regress_encoding; + +-- invalid short mb character = error +SELECT length(truncated) FROM regress_encoding; +SELECT substring(truncated, 1, 3) FROM regress_encoding; +SELECT substring(truncated, 1, 4) FROM regress_encoding; +SELECT reverse(truncated) FROM regress_encoding; +-- invalid short mb character = silently dropped +SELECT regexp_replace(truncated, '^caf(.)$', '\1') FROM regress_encoding; + +-- PostgreSQL doesn't allow strings to contain NUL. If a corrupted string +-- contains NUL at a character boundary position, some functions treat it as a +-- character while others treat it as a terminator, as implementation details. + +-- NUL = terminator +SELECT length(with_nul) FROM regress_encoding; +SELECT substring(with_nul, 3, 1) FROM regress_encoding; +SELECT substring(with_nul, 4, 1) FROM regress_encoding; +SELECT substring(with_nul, 5, 1) FROM regress_encoding; +SELECT convert_to(substring(with_nul, 5, 1), 'UTF8') FROM regress_encoding; +SELECT regexp_replace(with_nul, '^caf(.)$', '\1') FROM regress_encoding; +-- NUL = character +SELECT with_nul, reverse(with_nul), reverse(reverse(with_nul)) FROM regress_encoding; + +-- If a corrupted string contains NUL in the tail bytes of a multibyte +-- character (invalid in all encodings), it is considered part of the +-- character for length purposes. An error will only be raised in code paths +-- that convert or verify encodings. + +SELECT length(truncated_with_nul) FROM regress_encoding; +SELECT substring(truncated_with_nul, 3, 1) FROM regress_encoding; +SELECT substring(truncated_with_nul, 4, 1) FROM regress_encoding; +SELECT convert_to(substring(truncated_with_nul, 4, 1), 'UTF8') FROM regress_encoding; +SELECT substring(truncated_with_nul, 5, 1) FROM regress_encoding; +SELECT regexp_replace(truncated_with_nul, '^caf(.)dcba$', '\1') = test_bytea_to_text('\xc300') FROM regress_encoding; +SELECT reverse(truncated_with_nul) FROM regress_encoding; + +-- unbounded: sequence would overrun the string! +SELECT test_mblen_func('pg_mblen_unbounded', 'UTF8', truncated, 3) +FROM regress_encoding; + +-- condition detected when using the length/range variants +SELECT test_mblen_func('pg_mblen_with_len', 'UTF8', truncated, 3) +FROM regress_encoding; +SELECT test_mblen_func('pg_mblen_range', 'UTF8', truncated, 3) +FROM regress_encoding; + +-- unbounded: sequence would overrun the string, if the terminator were really +-- the end of it +SELECT test_mblen_func('pg_mblen_unbounded', 'UTF8', truncated_with_nul, 3) +FROM regress_encoding; +SELECT test_mblen_func('pg_encoding_mblen', 'GB18030', truncated_with_nul, 3) +FROM regress_encoding; + +-- condition detected when using the cstr variants +SELECT test_mblen_func('pg_mblen_cstr', 'UTF8', truncated_with_nul, 3) +FROM regress_encoding; + +DROP TABLE regress_encoding; + +-- mb<->wchar conversions +CREATE FUNCTION test_encoding(encoding text, description text, input bytea) +RETURNS VOID LANGUAGE plpgsql AS +$$ +DECLARE + prefix text; + len int; + wchars int[]; + round_trip bytea; + result text; +BEGIN + prefix := rpad(encoding || ' ' || description || ':', 28); + + -- XXX could also test validation, length functions and include client + -- only encodings with these test cases + + IF test_valid_server_encoding(encoding) THEN + wchars := test_text_to_wchars(encoding, test_bytea_to_text(input)); + round_trip = test_text_to_bytea(test_wchars_to_text(encoding, wchars)); + if input = round_trip then + result := 'OK'; + elsif length(input) > length(round_trip) and round_trip = substr(input, 1, length(round_trip)) then + result := 'truncated'; + else + result := 'failed'; + end if; + RAISE NOTICE '% % -> % -> % = %', prefix, input, wchars, round_trip, result; + END IF; +END; +$$; +-- No validation is done on the encoding itself, just the length to avoid +-- overruns, so some of the byte sequences below are bogus. They cover +-- all code branches, server encodings only for now. +CREATE TABLE encoding_tests (encoding text, description text, input bytea); +INSERT INTO encoding_tests VALUES + -- LATIN1, other single-byte encodings + ('LATIN1', 'ASCII', 'a'), + ('LATIN1', 'extended', '\xe9'), + -- EUC_JP, EUC_JIS_2004, EUR_KR (for the purposes of wchar conversion): + -- 2 8e (CS2, not used by EUR_KR but arbitrarily considered to have EUC_JP length) + -- 3 8f (CS3, not used by EUR_KR but arbitrarily considered to have EUC_JP length) + -- 2 80..ff (CS1) + ('EUC_JP', 'ASCII', 'a'), + ('EUC_JP', 'CS1, short', '\x80'), + ('EUC_JP', 'CS1', '\x8002'), + ('EUC_JP', 'CS2, short', '\x8e'), + ('EUC_JP', 'CS2', '\x8e02'), + ('EUC_JP', 'CS3, short', '\x8f'), + ('EUC_JP', 'CS3, short', '\x8f02'), + ('EUC_JP', 'CS3', '\x8f0203'), + -- EUC_CN + -- 3 8e (CS2, not used but arbitrarily considered to have length 3) + -- 3 8f (CS3, not used but arbitrarily considered to have length 3) + -- 2 80..ff (CS1) + ('EUC_CN', 'ASCII', 'a'), + ('EUC_CN', 'CS1, short', '\x80'), + ('EUC_CN', 'CS1', '\x8002'), + ('EUC_CN', 'CS2, short', '\x8e'), + ('EUC_CN', 'CS2, short', '\x8e02'), + ('EUC_CN', 'CS2', '\x8e0203'), + ('EUC_CN', 'CS3, short', '\x8f'), + ('EUC_CN', 'CS3, short', '\x8f02'), + ('EUC_CN', 'CS3', '\x8f0203'), + -- EUC_TW: + -- 4 8e (CS2) + -- 3 8f (CS3, not used but arbitrarily considered to have length 3) + -- 2 80..ff (CS1) + ('EUC_TW', 'ASCII', 'a'), + ('EUC_TW', 'CS1, short', '\x80'), + ('EUC_TW', 'CS1', '\x8002'), + ('EUC_TW', 'CS2, short', '\x8e'), + ('EUC_TW', 'CS2, short', '\x8e02'), + ('EUC_TW', 'CS2, short', '\x8e0203'), + ('EUC_TW', 'CS2', '\x8e020304'), + ('EUC_TW', 'CS3, short', '\x8f'), + ('EUC_TW', 'CS3, short', '\x8f02'), + ('EUC_TW', 'CS3', '\x8f0203'), + -- UTF8 + -- 2 c0..df + -- 3 e0..ef + -- 4 f0..f7 (but maximum real codepoint U+10ffff has f4) + -- 5 f8..fb (not supported) + -- 6 fc..fd (not supported) + ('UTF8', 'ASCII', 'a'), + ('UTF8', '2 byte, short', '\xdf'), + ('UTF8', '2 byte', '\xdf82'), + ('UTF8', '3 byte, short', '\xef'), + ('UTF8', '3 byte, short', '\xef82'), + ('UTF8', '3 byte', '\xef8283'), + ('UTF8', '4 byte, short', '\xf7'), + ('UTF8', '4 byte, short', '\xf782'), + ('UTF8', '4 byte, short', '\xf78283'), + ('UTF8', '4 byte', '\xf7828384'), + ('UTF8', '5 byte, unsupported', '\xfb'), + ('UTF8', '5 byte, unsupported', '\xfb82'), + ('UTF8', '5 byte, unsupported', '\xfb8283'), + ('UTF8', '5 byte, unsupported', '\xfb828384'), + ('UTF8', '5 byte, unsupported', '\xfb82838485'), + ('UTF8', '6 byte, unsupported', '\xfd'), + ('UTF8', '6 byte, unsupported', '\xfd82'), + ('UTF8', '6 byte, unsupported', '\xfd8283'), + ('UTF8', '6 byte, unsupported', '\xfd828384'), + ('UTF8', '6 byte, unsupported', '\xfd82838485'), + ('UTF8', '6 byte, unsupported', '\xfd8283848586'); + +SELECT COUNT(test_encoding(encoding, description, input)) > 0 +FROM encoding_tests; + +-- substring fetches a slice of a toasted value; unused tail of that slice is +-- an incomplete char (bug #19406) +CREATE TABLE toast_3b_utf8 (c text); +INSERT INTO toast_3b_utf8 VALUES (repeat(U&'\2026', 4000)); +SELECT SUBSTRING(c FROM 1 FOR 1) FROM toast_3b_utf8; +SELECT SUBSTRING(c FROM 4001 FOR 1) FROM toast_3b_utf8; +-- diagnose incomplete char iff within the substring +UPDATE toast_3b_utf8 SET c = c || test_bytea_to_text('\xe280'); +SELECT SUBSTRING(c FROM 4000 FOR 1) FROM toast_3b_utf8; +SELECT SUBSTRING(c FROM 4001 FOR 1) FROM toast_3b_utf8; +-- substring needing last byte of its slice_size +ALTER TABLE toast_3b_utf8 RENAME TO toast_4b_utf8; +UPDATE toast_4b_utf8 SET c = repeat(U&'\+01F680', 3000); +SELECT SUBSTRING(c FROM 3000 FOR 1) FROM toast_4b_utf8; + +DROP TABLE encoding_tests; +DROP TABLE toast_4b_utf8; +DROP FUNCTION test_encoding; +DROP FUNCTION test_wchars_to_text; +DROP FUNCTION test_text_to_wchars; +DROP FUNCTION test_valid_server_encoding; +DROP FUNCTION test_mblen_func; +DROP FUNCTION test_bytea_to_text; +DROP FUNCTION test_text_to_bytea; + + +-- substring slow path: multi-byte escape char vs. multi-byte pattern char. +SELECT SUBSTRING('a' SIMILAR U&'\00AC' ESCAPE U&'\00A7'); +-- Levenshtein distance metric: exercise character length cache. +SELECT U&"real\00A7_name" FROM (select 1) AS x(real_name); +-- JSON errcontext: truncate long data. +SELECT repeat(U&'\00A7', 30)::json; diff --git a/src/test/regress/sql/enum.sql b/src/test/regress/sql/enum.sql index ecc4878a6782a..803ccad6a6b03 100644 --- a/src/test/regress/sql/enum.sql +++ b/src/test/regress/sql/enum.sql @@ -23,6 +23,9 @@ SELECT * FROM pg_input_error_info('mauve', 'rainbow'); SELECT * FROM pg_input_error_info(repeat('too_long', 32), 'rainbow'); \x +-- check for duplicate enum entries +CREATE TYPE dup_enum AS ENUM ('foo','bar','foo'); + -- -- adding new values -- diff --git a/src/test/regress/sql/euc_kr.sql b/src/test/regress/sql/euc_kr.sql new file mode 100644 index 0000000000000..1851b2a8c1405 --- /dev/null +++ b/src/test/regress/sql/euc_kr.sql @@ -0,0 +1,12 @@ +-- This test is about EUC_KR encoding, chosen as perhaps the most prevalent +-- non-UTF8, multibyte encoding as of 2026-01. Since UTF8 can represent all +-- of EUC_KR, also run the test in UTF8. +SELECT getdatabaseencoding() NOT IN ('EUC_KR', 'UTF8') AS skip_test \gset +\if :skip_test +\quit +\endif + +-- Exercise is_multibyte_char_in_char (non-UTF8) slow path. +SELECT POSITION( + convert_from('\xbcf6c7d0', 'EUC_KR') IN + convert_from('\xb0fac7d02c20bcf6c7d02c20b1e2bcfa2c20bbee', 'EUC_KR')); diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql index 013546b83057b..32e9bb58c5e94 100644 --- a/src/test/regress/sql/event_trigger.sql +++ b/src/test/regress/sql/event_trigger.sql @@ -202,9 +202,15 @@ INSERT INTO undroppable_objs VALUES ('table', 'audit_tbls.schema_two_table_three'); CREATE TABLE dropped_objects ( - type text, - schema text, - object text + object_type text, + schema_name text, + object_name text, + object_identity text, + address_names text[], + address_args text[], + is_temporary bool, + original bool, + normal bool ); -- This tests errors raised within event triggers; the one in audit_tbls @@ -245,8 +251,12 @@ BEGIN END IF; INSERT INTO dropped_objects - (type, schema, object) VALUES - (obj.object_type, obj.schema_name, obj.object_identity); + (object_type, schema_name, object_name, + object_identity, address_names, address_args, + is_temporary, original, normal) VALUES + (obj.object_type, obj.schema_name, obj.object_name, + obj.object_identity, obj.address_names, obj.address_args, + obj.is_temporary, obj.original, obj.normal); END LOOP; END $$; @@ -263,10 +273,12 @@ DROP SCHEMA schema_one, schema_two CASCADE; DELETE FROM undroppable_objs WHERE object_identity = 'schema_one.table_three'; DROP SCHEMA schema_one, schema_two CASCADE; -SELECT * FROM dropped_objects WHERE schema IS NULL OR schema <> 'pg_toast'; +-- exclude TOAST objects because they have unstable names +SELECT * FROM dropped_objects + WHERE schema_name IS NULL OR schema_name <> 'pg_toast'; DROP OWNED BY regress_evt_user; -SELECT * FROM dropped_objects WHERE type = 'schema'; +SELECT * FROM dropped_objects WHERE object_type = 'schema'; DROP ROLE regress_evt_user; @@ -285,9 +297,10 @@ BEGIN IF NOT r.normal AND NOT r.original THEN CONTINUE; END IF; - RAISE NOTICE 'NORMAL: orig=% normal=% istemp=% type=% identity=% name=% args=%', + RAISE NOTICE 'NORMAL: orig=% normal=% istemp=% type=% identity=% schema=% name=% addr=% args=%', r.original, r.normal, r.is_temporary, r.object_type, - r.object_identity, r.address_names, r.address_args; + r.object_identity, r.schema_name, r.object_name, + r.address_names, r.address_args; END LOOP; END; $$; CREATE EVENT TRIGGER regress_event_trigger_report_dropped ON sql_drop @@ -311,7 +324,12 @@ CREATE SCHEMA evttrig CREATE TABLE one (col_a SERIAL PRIMARY KEY, col_b text DEFAULT 'forty two', col_c SERIAL) CREATE INDEX one_idx ON one (col_b) CREATE TABLE two (col_c INTEGER CHECK (col_c > 0) REFERENCES one DEFAULT 42) - CREATE TABLE id (col_d int NOT NULL GENERATED ALWAYS AS IDENTITY); + CREATE TABLE id (col_d int NOT NULL GENERATED ALWAYS AS IDENTITY) + CREATE VIEW one_view AS SELECT * FROM two; + +-- View with column additions +CREATE OR REPLACE VIEW evttrig.one_view AS SELECT * FROM evttrig.two, evttrig.id; +DROP VIEW evttrig.one_view; -- Partitioned tables with a partitioned index CREATE TABLE evttrig.parted ( @@ -337,6 +355,46 @@ DROP INDEX evttrig.one_idx; DROP SCHEMA evttrig CASCADE; DROP TABLE a_temp_tbl; +-- check unfiltered results, too +CREATE OR REPLACE FUNCTION event_trigger_report_dropped() + RETURNS event_trigger + LANGUAGE plpgsql +AS $$ +DECLARE r record; +BEGIN + FOR r IN SELECT * from pg_event_trigger_dropped_objects() + LOOP + RAISE NOTICE 'DROP: orig=% normal=% istemp=% type=% identity=% schema=% name=% addr=% args=%', + r.original, r.normal, r.is_temporary, r.object_type, + r.object_identity, r.schema_name, r.object_name, + r.address_names, r.address_args; + END LOOP; +END; $$; + +CREATE FUNCTION event_trigger_dummy_trigger() + RETURNS trigger + LANGUAGE plpgsql +AS $$ +BEGIN + RETURN new; +END; $$; + +CREATE TABLE evtrg_nontemp_table (f1 int primary key, f2 int default 42); +CREATE TRIGGER evtrg_nontemp_trig + BEFORE INSERT ON evtrg_nontemp_table + EXECUTE FUNCTION event_trigger_dummy_trigger(); +CREATE POLICY evtrg_nontemp_pol ON evtrg_nontemp_table USING (f2 > 0); +DROP TABLE evtrg_nontemp_table; + +CREATE TEMP TABLE a_temp_tbl (f1 int primary key, f2 int default 42); +CREATE TRIGGER a_temp_trig + BEFORE INSERT ON a_temp_tbl + EXECUTE FUNCTION event_trigger_dummy_trigger(); +CREATE POLICY a_temp_pol ON a_temp_tbl USING (f2 > 0); +DROP TABLE a_temp_tbl; + +DROP FUNCTION event_trigger_dummy_trigger(); + -- CREATE OPERATOR CLASS without FAMILY clause should report -- both CREATE OPERATOR FAMILY and CREATE OPERATOR CLASS CREATE OPERATOR CLASS evttrigopclass FOR TYPE int USING btree AS STORAGE int; diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql index 7842d25ded3fe..2f163c64bf6d3 100644 --- a/src/test/regress/sql/explain.sql +++ b/src/test/regress/sql/explain.sql @@ -61,14 +61,18 @@ set track_io_timing = off; -- Simple cases +explain (costs off) select 1 as a, 2 as b having false; select explain_filter('explain select * from int8_tbl i8'); select explain_filter('explain (analyze, buffers off) select * from int8_tbl i8'); select explain_filter('explain (analyze, buffers off, verbose) select * from int8_tbl i8'); select explain_filter('explain (analyze, buffers, format text) select * from int8_tbl i8'); -select explain_filter('explain (analyze, buffers, format xml) select * from int8_tbl i8'); -select explain_filter('explain (analyze, serialize, buffers, format yaml) select * from int8_tbl i8'); select explain_filter('explain (buffers, format text) select * from int8_tbl i8'); + +\a +select explain_filter('explain (analyze, buffers, io, format xml) select * from int8_tbl i8'); +select explain_filter('explain (analyze, serialize, buffers, io, format yaml) select * from int8_tbl i8'); select explain_filter('explain (buffers, format json) select * from int8_tbl i8'); +\a -- Check expansion of window definitions diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql index 068dd0bc8aad0..8b31d317d73d8 100644 --- a/src/test/regress/sql/fast_default.sql +++ b/src/test/regress/sql/fast_default.sql @@ -287,11 +287,62 @@ ORDER BY attnum; SELECT a, b, length(c) = 3 as c_ok, d, e >= 10 as e_ok FROM t2; +-- test fast default over domains with constraints +CREATE DOMAIN domain5 AS int CHECK(value > 10) DEFAULT 8; +CREATE DOMAIN domain6 as int CHECK(value > 10) DEFAULT random(min=>11, max=>100); +CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12); +CREATE DOMAIN domain8 as int NOT NULL; + +CREATE TABLE test_add_domain_col(a int); +-- succeeds despite constraint-violating default because table is empty +ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5; +ALTER TABLE test_add_domain_col DROP COLUMN a1; +INSERT INTO test_add_domain_col VALUES(1),(2); + +-- tests with non-empty table +ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail +ALTER TABLE test_add_domain_col ADD COLUMN b domain8; -- table rewrite, then fail +ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail +ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite + +-- explicit column default expression overrides domain's default +-- expression, so no table rewrite +ALTER TABLE test_add_domain_col ADD COLUMN c domain6 DEFAULT 14; + +ALTER TABLE test_add_domain_col ADD COLUMN c1 domain8 DEFAULT 13; -- no table rewrite +SELECT attnum, attname, atthasmissing, atthasdef, attmissingval +FROM pg_attribute +WHERE attnum > 0 AND attrelid = 'test_add_domain_col'::regclass AND attisdropped is false +AND atthasmissing +ORDER BY attnum; + +-- We need to rewrite the table whenever domain default contains volatile expression +ALTER TABLE test_add_domain_col ADD COLUMN d domain6; + +-- We need to rewrite the table whenever domain constraint expression contains volatile expression +ALTER TABLE test_add_domain_col ADD COLUMN e domain7 default 14; +ALTER TABLE test_add_domain_col ADD COLUMN f domain7; + +-- domain with both volatile and non-volatile CHECK constraints: the +-- volatile one forces a table rewrite +CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0); +ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14; + +-- virtual generated columns cannot have domain types +ALTER TABLE test_add_domain_col ADD COLUMN h domain5 + GENERATED ALWAYS AS (a + 20) VIRTUAL; -- error + DROP TABLE t2; +DROP TABLE test_add_domain_col; DROP DOMAIN domain1; DROP DOMAIN domain2; DROP DOMAIN domain3; DROP DOMAIN domain4; +DROP DOMAIN domain5; +DROP DOMAIN domain6; +DROP DOMAIN domain7; +DROP DOMAIN domain8; +DROP DOMAIN domain9; DROP FUNCTION foo(INT); -- Fall back to full rewrite for volatile expressions @@ -324,7 +375,7 @@ SELECT attname, atthasmissing, attmissingval FROM pg_attribute DROP TABLE T; DROP FUNCTION foolme(timestamptz); --- Simple querie +-- Simple queries CREATE TABLE T (pk INT NOT NULL PRIMARY KEY); SELECT set('t'); diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql new file mode 100644 index 0000000000000..d4062acf1d19f --- /dev/null +++ b/src/test/regress/sql/for_portion_of.sql @@ -0,0 +1,1368 @@ +-- Tests for UPDATE/DELETE FOR PORTION OF + +SET datestyle TO ISO, YMD; + +-- Works on non-PK columns +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text NOT NULL +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2020-01-01)', 'one'); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = 'one^1'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-01-15' TO '2019-01-20'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- With a table alias with AS + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-01' TO '2019-02-03' AS t + SET name = 'one^2'; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-03' TO '2019-02-04' AS t; + +-- With a table alias without AS + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-04' TO '2019-02-05' t + SET name = 'one^3'; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-02-05' TO '2019-02-06' t; + +-- UPDATE with FROM + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2019-03-01' to '2019-03-02' + SET name = 'one^4' + FROM (SELECT '[1,2)'::int4range) AS t2(id) + WHERE for_portion_of_test.id = t2.id; + +-- DELETE with USING + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2019-03-02' TO '2019-03-03' + USING (SELECT '[1,2)'::int4range) AS t2(id) + WHERE for_portion_of_test.id = t2.id; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- Works on more than one range +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid1_at daterange, + valid2_at daterange, + name text NOT NULL +); +INSERT INTO for_portion_of_test (id, valid1_at, valid2_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', '[2015-01-01,2025-01-01)', 'one'); + +UPDATE for_portion_of_test + FOR PORTION OF valid1_at FROM '2018-01-15' TO NULL + SET name = 'foo'; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +UPDATE for_portion_of_test + FOR PORTION OF valid2_at FROM '2018-01-15' TO NULL + SET name = 'bar'; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid1_at FROM '2018-01-20' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid2_at FROM '2018-01-20' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid1_at, valid2_at; + +-- Test with NULLs in the scalar/range key columns. +-- This won't happen if there is a PRIMARY KEY or UNIQUE constraint +-- but FOR PORTION OF shouldn't require that. +DROP TABLE for_portion_of_test; +CREATE UNLOGGED TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', NULL, '1 null'), + ('[1,2)', '(,)', '1 unbounded'), + ('[1,2)', 'empty', '1 empty'), + (NULL, NULL, NULL), + (NULL, daterange('2018-01-01', '2019-01-01'), 'null key'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'NULL to NULL'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +DROP TABLE for_portion_of_test; + +-- +-- UPDATE tests +-- + +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'), + ('[4,5)', '(,2018-04-01)', 'four'), + ('[5,6)', '(,)', 'five') + ; +\set QUIET false + +-- Updating with a missing column fails +UPDATE for_portion_of_test + FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + SET name = 'foo' + WHERE id = '[5,6)'; + +-- Updating the range fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + SET valid_at = '[1990-01-01,1999-01-01)' + WHERE id = '[5,6)'; + +-- The wrong start type fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + SET name = 'nope' + WHERE id = '[3,4)'; + +-- The wrong end type fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + SET name = 'nope' + WHERE id = '[3,4)'; + +-- Updating with timestamps reversed fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' + SET name = 'three^1' + WHERE id = '[3,4)'; + +-- Updating with a subquery fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' + SET name = 'nope' + WHERE id = '[3,4)'; + +-- Updating with a column fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + SET name = 'nope' + WHERE id = '[3,4)'; + +-- Updating with timestamps equal does nothing +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' + SET name = 'three^0' + WHERE id = '[3,4)'; + +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + SET name = 'three^1' + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-03-01' + SET name = 'three^2' + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-02-01' + SET name = 'four^1' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO NULL + SET name = 'four^2' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' + SET name = 'four^3' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Updating an enclosed span +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'two^2' + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' + SET name = 'five^1' + WHERE id = '[5,6)'; +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' + SET name = 'five^2' + WHERE id = '[5,6)'; +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + +-- Updating multiple enclosed spans +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + SET name = 'one^2' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating with a direct target +UPDATE for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating with a direct target, coerced from a string +UPDATE for_portion_of_test + FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating with a direct target of the wrong range subtype fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (int4range(1, 4)) + SET name = 'one^3' + WHERE id = '[1,2)'; + +-- Updating with a direct target of a non-rangetype fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (4) + SET name = 'one^3' + WHERE id = '[1,2)'; + +-- Updating with a direct target of NULL fails +UPDATE for_portion_of_test + FOR PORTION OF valid_at (NULL) + SET name = 'one^3' + WHERE id = '[1,2)'; + +-- Updating with a direct target of empty does nothing +UPDATE for_portion_of_test + FOR PORTION OF valid_at ('empty') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-15' TO NULL + SET id = '[6,7)' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id IN ('[1,2)', '[6,7)') ORDER BY id, valid_at; + +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2030-01-01' TO NULL + SET name = name || '*'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; +\set QUIET true + +-- Updating with a shift/reduce conflict +-- (requires a tsrange column) +CREATE UNLOGGED TABLE for_portion_of_test2 ( + id int4range, + valid_at tsrange, + name text +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', '[2000-01-01,2020-01-01)', 'one'); +-- updates [2011-03-01 01:02:00, 2012-01-01) (note 2 minutes) +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM '2011-03-01'::timestamp + INTERVAL '1:02:03' HOUR TO MINUTE + TO '2012-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; + +-- TO is used for the bound but not the INTERVAL: +-- syntax error +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM '2013-03-01'::timestamp + INTERVAL '1:02:03' HOUR + TO '2014-01-01' + SET name = 'one^2' + WHERE id = '[1,2)'; + +-- adding parens fixes it +-- updates [2015-03-01 01:00:00, 2016-01-01) (no minutes) +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at + FROM ('2015-03-01'::timestamp + INTERVAL '1:02:03' HOUR) + TO '2016-01-01' + SET name = 'one^3' + WHERE id = '[1,2)'; + +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; +DROP TABLE for_portion_of_test2; + +-- UPDATE FOR PORTION OF in a CTE: +-- The outer query sees the table how it was before the updates, +-- and with no leftovers yet, +-- but it also sees the new values via the RETURNING clause. +-- (We test RETURNING more directly, without a CTE, below.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +WITH update_apr AS ( + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' + SET name = 'Apr 2018' + WHERE id = '[10,11)' + RETURNING id, valid_at, name +) +SELECT * + FROM for_portion_of_test AS t, update_apr + WHERE t.id = update_apr.id; +SELECT * FROM for_portion_of_test WHERE id = '[10,11)' ORDER BY id, valid_at; + +-- UPDATE FOR PORTION OF with current_date +-- (We take care not to make the expectation depend on the timestamp.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[99,100)', '[2000-01-01,)', 'foo'); +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM current_date TO null + SET name = 'bar' + WHERE id = '[99,100)'; +SELECT name, lower(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date - 1; +SELECT name, upper(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date + 1; + +-- UPDATE FOR PORTION OF with clock_timestamp() +-- fails because the function is volatile: +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM clock_timestamp()::date TO null + SET name = 'baz' + WHERE id = '[99,100)'; + +-- clean up: +DELETE FROM for_portion_of_test WHERE id = '[99,100)'; + +-- Not visible to UPDATE: +-- Tuples updated/inserted within the CTE are not visible to the main query yet, +-- but neither are old tuples the CTE changed: +-- (This is the same behavior as without FOR PORTION OF.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[11,12)', '[2018-01-01,2020-01-01)', 'eleven'); +WITH update_apr AS ( + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-05-01' + SET name = 'Apr 2018' + WHERE id = '[11,12)' + RETURNING id, valid_at, name +) +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + AS t + SET name = 'May 2018' + FROM update_apr AS j + WHERE t.id = j.id; +SELECT * FROM for_portion_of_test WHERE id = '[11,12)' ORDER BY id, valid_at; +DELETE FROM for_portion_of_test WHERE id IN ('[10,11)', '[11,12)'); + +-- UPDATE FOR PORTION OF in a PL/pgSQL function +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +CREATE FUNCTION fpo_update(_id int4range, _target_from date, _target_til date) +RETURNS void LANGUAGE plpgsql AS +$$ +BEGIN + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM $2 TO $3 + SET name = concat(_target_from::text, ' to ', _target_til::text) + WHERE id = $1; +END; +$$; +SELECT fpo_update('[10,11)', '2015-01-01', '2019-01-01'); +SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; + +-- UPDATE FOR PORTION OF in a compiled SQL function +CREATE FUNCTION fpo_update() +RETURNS text +BEGIN ATOMIC + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = 'one^1' + RETURNING name; +END; +\sf+ fpo_update() +CREATE OR REPLACE function fpo_update() +RETURNS text +BEGIN ATOMIC + UPDATE for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) + SET name = 'one^1' + RETURNING name; +END; +\sf+ fpo_update() +DROP FUNCTION fpo_update(); + +DROP TABLE for_portion_of_test; + +-- +-- DELETE tests +-- + +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at daterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-01-05)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'), + ('[4,5)', '(,2018-04-01)', 'four'), + ('[5,6)', '(,)', 'five'), + ('[6,7)', '[2018-01-01,)', 'six'), + ('[7,8)', '(,2018-04-01)', 'seven'), + ('[8,9)', '[2018-01-02,2018-02-03)', 'eight'), + ('[8,9)', '[2018-02-03,2018-03-03)', 'eight'), + ('[8,9)', '[2018-03-03,2018-04-04)', 'eight') + ; +\set QUIET false + +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test + FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + WHERE id = '[5,6)'; + +-- The wrong start type fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM 1 TO '2020-01-01' + WHERE id = '[3,4)'; + +-- The wrong end type fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2000-01-01' TO 4 + WHERE id = '[3,4)'; + +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' + WHERE id = '[3,4)'; + +-- Deleting with a subquery fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' + WHERE id = '[3,4)'; + +-- Deleting with a column fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM lower(valid_at) TO NULL + WHERE id = '[3,4)'; + +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' + WHERE id = '[3,4)'; + +-- Deleting a finite/open portion with a finite/open target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-06-01' TO NULL + WHERE id = '[3,4)'; +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +-- Deleting a finite/open portion with an open/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-03-01' + WHERE id = '[6,7)'; +SELECT * FROM for_portion_of_test WHERE id = '[6,7)' ORDER BY id, valid_at; + +-- Deleting an open/finite portion with an open/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-02-01' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Deleting an open/finite portion with a finite/open target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2017-01-01' TO NULL + WHERE id = '[7,8)'; +SELECT * FROM for_portion_of_test WHERE id = '[7,8)' ORDER BY id, valid_at; + +-- Deleting a finite/finite portion with an exact fit +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-04-01' + WHERE id = '[4,5)'; +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +-- Deleting an enclosed span +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + +-- Deleting an open/open portion with a finite/finite target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' + WHERE id = '[5,6)'; +SELECT * FROM for_portion_of_test WHERE id = '[5,6)' ORDER BY id, valid_at; + +-- Deleting an enclosed span with separate protruding spans +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-03' TO '2018-03-03' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Deleting multiple enclosed spans +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO NULL + WHERE id = '[8,9)'; +SELECT * FROM for_portion_of_test WHERE id = '[8,9)' ORDER BY id, valid_at; + +-- Deleting with a direct target +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-03-10', '2018-03-15')) + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Deleting with a direct target, coerced from a string +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at ('[2018-03-15,2018-03-17)') + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- Deleting with a direct target of the wrong range subtype fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (int4range(1, 4)) + WHERE id = '[1,2)'; + +-- Deleting with a direct target of a non-rangetype fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (4) + WHERE id = '[1,2)'; + +-- Deleting with a direct target of NULL fails +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (NULL) + WHERE id = '[1,2)'; + +-- Deleting with a direct target of empty does nothing +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at ('empty') + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2030-01-01' TO NULL; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; +\set QUIET true + +-- UPDATE ... RETURNING returns only the updated values +-- (not the inserted side values, which are added by a separate "statement"): +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' + SET name = 'three^3' + WHERE id = '[3,4)' + RETURNING *; + +-- UPDATE ... RETURNING supports NEW and OLD valid_at +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-10' TO '2018-02-20' + SET name = 'three^4' + WHERE id = '[3,4)' + RETURNING OLD.name, NEW.name, OLD.valid_at, NEW.valid_at; + +-- DELETE FOR PORTION OF with current_date +-- (We take care not to make the expectation depend on the timestamp.) +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[99,100)', '[2000-01-01,)', 'foo'); +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM current_date TO null + WHERE id = '[99,100)'; +SELECT name, lower(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date - 1; +SELECT name, upper(valid_at) FROM for_portion_of_test + WHERE id = '[99,100)' AND valid_at @> current_date + 1; + +-- DELETE FOR PORTION OF with clock_timestamp() +-- fails because the function is volatile: +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM clock_timestamp()::date TO null + WHERE id = '[99,100)'; + +-- clean up: +DELETE FROM for_portion_of_test WHERE id = '[99,100)'; + +-- DELETE ... RETURNING returns the deleted values, regardless of bounds +-- (not the inserted side values, which are added by a separate "statement"): +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-02' TO '2018-02-03' + WHERE id = '[3,4)' + RETURNING *; + +-- DELETE FOR PORTION OF in a PL/pgSQL function +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[10,11)', '[2018-01-01,2020-01-01)', 'ten'); +CREATE FUNCTION fpo_delete(_id int4range, _target_from date, _target_til date) +RETURNS void LANGUAGE plpgsql AS +$$ +BEGIN + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM $2 TO $3 + WHERE id = $1; +END; +$$; +SELECT fpo_delete('[10,11)', '2015-01-01', '2019-01-01'); +SELECT * FROM for_portion_of_test WHERE id = '[10,11)'; +DELETE FROM for_portion_of_test WHERE id IN ('[10,11)'); + +-- DELETE FOR PORTION OF in a compiled SQL function +CREATE FUNCTION fpo_delete() +RETURNS text +BEGIN ATOMIC + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + RETURNING name; +END; +\sf+ fpo_delete() +CREATE OR REPLACE function fpo_delete() +RETURNS text +BEGIN ATOMIC + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at (daterange('2018-01-15', '2020-01-01') * daterange('2019-01-01', '2022-01-01')) + RETURNING name; +END; +\sf+ fpo_delete() +DROP FUNCTION fpo_delete(); + + +-- test domains and CHECK constraints + +-- With a domain on a rangetype +CREATE DOMAIN daterange_d AS daterange CHECK (upper(VALUE) <> '2005-05-05'::date); +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at daterange_d, + name text +); +INSERT INTO for_portion_of_test2 VALUES + (1, '[2000-01-01,2020-01-01)', 'one'), + (2, '[2000-01-01,2020-01-01)', 'two'); +INSERT INTO for_portion_of_test2 VALUES + (1, '[2000-01-01,2005-05-05)', 'nope'); +-- UPDATE works: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' + SET name = 'one^1' + WHERE id = 1; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') + SET name = 'one^2' + WHERE id = 1; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- The target is allowed to violate the domain: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + SET name = 'miss' + WHERE id = -1; +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') + SET name = 'miss' + WHERE id = -1; +-- test the updated row violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + SET name = 'one^3' + WHERE id = 1; +-- test inserts violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' + SET name = 'one^3' + WHERE id = 1; +-- test updated row violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-11' + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- DELETE works: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2010-01-01' TO '2010-01-05' + WHERE id = 2; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('[2010-01-07,2010-01-09)') + WHERE id = 2; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +-- The target is allowed to violate the domain: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '1999-01-01' TO '2005-05-05' + WHERE id = -1; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('[1999-01-01,2005-05-05)') + WHERE id = -1; +-- test inserts violating the domain +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2005-05-05' TO '2010-01-01' + WHERE id = 2; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (lower(valid_at) <> '2002-02-02'); +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-02-02' + WHERE id = 2; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +DROP TABLE for_portion_of_test2; + +-- With a domain on a multirangetype +CREATE FUNCTION multirange_lowers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ + SELECT array_agg(lower(r)) FROM UNNEST(mr) u(r); +$$; +CREATE FUNCTION multirange_uppers(mr anymultirange) RETURNS anyarray LANGUAGE sql AS $$ + SELECT array_agg(upper(r)) FROM UNNEST(mr) u(r); +$$; +CREATE DOMAIN datemultirange_d AS datemultirange CHECK (NOT '2005-05-05'::date = ANY (multirange_uppers(VALUE))); +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at datemultirange_d, + name text +); +INSERT INTO for_portion_of_test2 VALUES + (1, '{[2000-01-01,2020-01-01)}', 'one'), + (2, '{[2000-01-01,2020-01-01)}', 'two'); +INSERT INTO for_portion_of_test2 VALUES + (1, '{[2000-01-01,2005-05-05)}', 'nope'); +-- UPDATE works: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') + SET name = 'one^2' + WHERE id = 1; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- The target is allowed to violate the domain: +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + SET name = 'miss' + WHERE id = -1; +-- test the updated row violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + SET name = 'one^3' + WHERE id = 1; +-- test inserts violating the domain +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') + SET name = 'one^3' + WHERE id = 1; +-- test updated row violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (upper(valid_at) <> '2001-01-11'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2000-01-01,2001-01-11)}') + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') + SET name = 'one^3' + WHERE id = 1; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 1 ORDER BY valid_at; +-- DELETE works: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2010-01-07,2010-01-09)}') + WHERE id = 2; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +-- The target is allowed to violate the domain: +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[1999-01-01,2005-05-05)}') + WHERE id = -1; +-- test inserts violating the domain +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2005-05-05,2010-01-01)}') + WHERE id = 2; +-- test inserts violating CHECK constraints +ALTER TABLE for_portion_of_test2 + ADD CONSTRAINT fpo2_check CHECK (NOT '2002-02-02'::date = ANY (multirange_lowers(valid_at))); +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2001-01-01,2002-02-02)}') + WHERE id = 2; +ALTER TABLE for_portion_of_test2 DROP CONSTRAINT fpo2_check; +SELECT * FROM for_portion_of_test2 WHERE id = 2 ORDER BY valid_at; +DROP TABLE for_portion_of_test2; + +-- test on non-range/multirange columns + +-- With a direct target and a scalar column +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at date, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '2020-01-01', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('2010-01-01') + SET name = 'one^1'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('2010-01-01'); +DROP TABLE for_portion_of_test2; + +-- With a direct target and a non-{,multi}range gistable column without overlaps +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at point, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '0,0', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('1,1') + SET name = 'one^1'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('1,1'); +DROP TABLE for_portion_of_test2; + +-- With a direct target and a non-{,multi}range column with overlaps +CREATE TABLE for_portion_of_test2 ( + id integer, + valid_at box, + name text +); +INSERT INTO for_portion_of_test2 VALUES (1, '0,0,4,4', 'one'); +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('1,1,2,2') + SET name = 'one^1'; +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('1,1,2,2'); +DROP TABLE for_portion_of_test2; + +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows + +CREATE FUNCTION dump_trigger() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE '%: % % %:', + TG_NAME, TG_WHEN, TG_OP, TG_LEVEL; + + IF TG_ARGV[0] THEN + RAISE NOTICE ' old: %', (SELECT string_agg(old_table::text, '\n ') FROM old_table); + ELSE + RAISE NOTICE ' old: %', OLD.valid_at; + END IF; + IF TG_ARGV[1] THEN + RAISE NOTICE ' new: %', (SELECT string_agg(new_table::text, '\n ') FROM new_table); + ELSE + RAISE NOTICE ' new: %', NEW.valid_at; + END IF; + + IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; +END; +$$; + +-- statement triggers: + +CREATE TRIGGER fpo_before_stmt + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_stmt + AFTER INSERT ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_update_stmt + AFTER UPDATE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_delete_stmt + AFTER DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +-- row triggers: + +CREATE TRIGGER fpo_before_row + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' + SET name = 'five^3' + WHERE id = '[5,6)'; + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' + WHERE id = '[5,6)'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- Triggers with a custom transition table name: + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test VALUES ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); + +-- statement triggers: + +CREATE TRIGGER fpo_before_stmt + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_stmt + AFTER INSERT ON for_portion_of_test + REFERENCING NEW TABLE AS new_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(false, true); + +CREATE TRIGGER fpo_after_update_stmt + AFTER UPDATE ON for_portion_of_test + REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, true); + +CREATE TRIGGER fpo_after_delete_stmt + AFTER DELETE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT EXECUTE PROCEDURE dump_trigger(true, false); + +-- row triggers: + +CREATE TRIGGER fpo_before_row + BEFORE INSERT OR UPDATE OR DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + REFERENCING NEW TABLE AS new_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, true); + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, true); + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + REFERENCING OLD TABLE AS old_table + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(true, false); + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = '2018-01-15_to_2019-01-01'; +ROLLBACK; + +BEGIN; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; +ROLLBACK; + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-02' + SET name = 'NULL_to_2018-01-01'; +ROLLBACK; + +-- Deferred triggers +-- (must be CONSTRAINT triggers thus AFTER ROW with no transition tables) + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-01,2020-01-01)', 'one'); + +CREATE CONSTRAINT TRIGGER fpo_after_insert_row + AFTER INSERT ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE CONSTRAINT TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +CREATE CONSTRAINT TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE dump_trigger(false, false); + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-01-15' TO '2019-01-01' + SET name = '2018-01-15_to_2019-01-01'; +COMMIT; + +BEGIN; +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-21'; +COMMIT; + +BEGIN; +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM NULL TO '2018-01-02' + SET name = 'NULL_to_2018-01-01'; +COMMIT; + +SELECT * FROM for_portion_of_test; + +-- test FOR PORTION OF from triggers during FOR PORTION OF: + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range, + valid_at daterange, + name text +); +INSERT INTO for_portion_of_test (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-01,2020-01-01)', 'one'), + ('[2,3)', '[2018-01-01,2020-01-01)', 'two'), + ('[3,4)', '[2018-01-01,2020-01-01)', 'three'), + ('[4,5)', '[2018-01-01,2020-01-01)', 'four'); + +CREATE FUNCTION trg_fpo_update() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + IF pg_trigger_depth() = 1 THEN + UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-03-01' + SET name = CONCAT(name, '^') + WHERE id = OLD.id; + END IF; + RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; +END; +$$; + +CREATE FUNCTION trg_fpo_delete() +RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + IF pg_trigger_depth() = 1 THEN + DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-03-01' TO '2018-04-01' + WHERE id = OLD.id; + END IF; + RETURN CASE WHEN 'TG_OP' = 'DELETE' THEN OLD ELSE NEW END; +END; +$$; + +-- UPDATE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + SET name = CONCAT(name, '*') + WHERE id = '[1,2)'; + +SELECT * FROM for_portion_of_test WHERE id = '[1,2)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_update_row ON for_portion_of_test; + +-- UPDATE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_update(); + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + WHERE id = '[2,3)'; + +SELECT * FROM for_portion_of_test WHERE id = '[2,3)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; + +-- DELETE FOR PORTION OF from a trigger fired by UPDATE FOR PORTION OF + +CREATE TRIGGER fpo_after_update_row + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); + +UPDATE for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + SET name = CONCAT(name, '*') + WHERE id = '[3,4)'; + +SELECT * FROM for_portion_of_test WHERE id = '[3,4)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_update_row ON for_portion_of_test; + +-- DELETE FOR PORTION OF from a trigger fired by DELETE FOR PORTION OF + +CREATE TRIGGER fpo_after_delete_row + AFTER DELETE ON for_portion_of_test + FOR EACH ROW EXECUTE PROCEDURE trg_fpo_delete(); + +DELETE FROM for_portion_of_test + FOR PORTION OF valid_at FROM '2018-05-01' TO '2018-06-01' + WHERE id = '[4,5)'; + +SELECT * FROM for_portion_of_test WHERE id = '[4,5)' ORDER BY id, valid_at; + +DROP TRIGGER fpo_after_delete_row ON for_portion_of_test; + +-- Test with multiranges + +CREATE TABLE for_portion_of_test2 ( + id int4range NOT NULL, + valid_at datemultirange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', datemultirange(daterange('2018-01-02', '2018-02-03)'), daterange('2018-02-04', '2018-03-03')), 'one'), + ('[1,2)', datemultirange(daterange('2018-03-03', '2018-04-04)')), 'one'), + ('[2,3)', datemultirange(daterange('2018-01-01', '2018-05-01)')), 'two'), + ('[3,4)', datemultirange(daterange('2018-01-01', null)), 'three'); + ; + +-- Updating with FROM/TO +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +-- Updating with multirange +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-10', '2018-02-10'), daterange('2018-03-05', '2018-05-01'))) + SET name = 'one^1' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; +-- Updating with string coercion +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[2018-03-05,2018-03-10)}') + SET name = 'one^2' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; +-- Updating with the wrong range subtype fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + SET name = 'one^3' + WHERE id = '[1,2)'; +-- Updating with a non-multirangetype fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (4) + SET name = 'one^3' + WHERE id = '[1,2)'; +-- Updating with NULL fails +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at (NULL) + SET name = 'one^3' + WHERE id = '[1,2)'; +-- Updating with empty does nothing +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at ('{}') + SET name = 'one^3' + WHERE id = '[1,2)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[1,2)' ORDER BY valid_at; + +-- Deleting with FROM/TO +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2000-01-01' TO '2010-01-01' + SET name = 'one^1' + WHERE id = '[1,2)'; +-- Deleting with multirange +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-15', '2018-02-15'), daterange('2018-03-01', '2018-03-15'))) + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; +-- Deleting with string coercion +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[2018-03-05,2018-03-20)}') + WHERE id = '[2,3)'; +SELECT * FROM for_portion_of_test2 WHERE id = '[2,3)' ORDER BY valid_at; +-- Deleting with the wrong range subtype fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{[1,4)}'::int4multirange) + WHERE id = '[2,3)'; +-- Deleting with a non-multirangetype fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (4) + WHERE id = '[2,3)'; +-- Deleting with NULL fails +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at (NULL) + WHERE id = '[2,3)'; +-- Deleting with empty does nothing +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at ('{}') + WHERE id = '[2,3)'; + +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + +DROP TABLE for_portion_of_test2; + +-- Test with a custom range type + +CREATE TYPE mydaterange AS range(subtype=date); + +CREATE TABLE for_portion_of_test2 ( + id int4range NOT NULL, + valid_at mydaterange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_test2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test2 (id, valid_at, name) VALUES + ('[1,2)', '[2018-01-02,2018-02-03)', 'one'), + ('[1,2)', '[2018-02-03,2018-03-03)', 'one'), + ('[1,2)', '[2018-03-03,2018-04-04)', 'one'), + ('[2,3)', '[2018-01-01,2018-05-01)', 'two'), + ('[3,4)', '[2018-01-01,)', 'three'); + ; + +UPDATE for_portion_of_test2 + FOR PORTION OF valid_at FROM '2018-01-10' TO '2018-02-10' + SET name = 'one^1' + WHERE id = '[1,2)'; + +DELETE FROM for_portion_of_test2 + FOR PORTION OF valid_at FROM '2018-01-15' TO '2018-02-15' + WHERE id = '[2,3)'; + +SELECT * FROM for_portion_of_test2 ORDER BY id, valid_at; + +DROP TABLE for_portion_of_test2; +DROP TYPE mydaterange; + +-- Test FOR PORTION OF against a partitioned table. +-- temporal_partitioned_1 has the same attnums as the root +-- temporal_partitioned_3 has the different attnums from the root +-- temporal_partitioned_5 has the different attnums too, but reversed + +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE temporal_partitioned_1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); +CREATE TABLE temporal_partitioned_3 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); +CREATE TABLE temporal_partitioned_5 PARTITION OF temporal_partitioned FOR VALUES IN ('[5,6)', '[6,7)'); + +ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_3; +ALTER TABLE temporal_partitioned_3 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_3 ADD COLUMN id int4range NOT NULL, ADD COLUMN valid_at daterange NOT NULL; +ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_3 FOR VALUES IN ('[3,4)', '[4,5)'); + +ALTER TABLE temporal_partitioned DETACH PARTITION temporal_partitioned_5; +ALTER TABLE temporal_partitioned_5 DROP COLUMN id, DROP COLUMN valid_at; +ALTER TABLE temporal_partitioned_5 ADD COLUMN valid_at daterange NOT NULL, ADD COLUMN id int4range NOT NULL; +ALTER TABLE temporal_partitioned ATTACH PARTITION temporal_partitioned_5 FOR VALUES IN ('[5,6)', '[6,7)'); + +INSERT INTO temporal_partitioned (id, valid_at, name) VALUES + ('[1,2)', daterange('2000-01-01', '2010-01-01'), 'one'), + ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'), + ('[5,6)', daterange('2000-01-01', '2010-01-01'), 'five'); + +SELECT * FROM temporal_partitioned; + +-- Update without moving within partition 1 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'one^1' + WHERE id = '[1,2)'; + +-- Update without moving within partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'three^1' + WHERE id = '[3,4)'; + +-- Update without moving within partition 5 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-03-01' TO '2000-04-01' + SET name = 'five^1' + WHERE id = '[5,6)'; + +-- Move from partition 1 to partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'one^2', + id = '[4,5)' + WHERE id = '[1,2)'; + +-- Move from partition 3 to partition 1 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'three^2', + id = '[2,3)' + WHERE id = '[3,4)'; + +-- Move from partition 5 to partition 3 +UPDATE temporal_partitioned FOR PORTION OF valid_at FROM '2000-06-01' TO '2000-07-01' + SET name = 'five^2', + id = '[3,4)' + WHERE id = '[5,6)'; + +-- Update all partitions at once (each with leftovers) + +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +SELECT * FROM temporal_partitioned_1 ORDER BY id, valid_at; +SELECT * FROM temporal_partitioned_3 ORDER BY id, valid_at; +SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at; + +DROP TABLE temporal_partitioned; + +RESET datestyle; diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql index aa147b14a90a0..ed78052b1e253 100644 --- a/src/test/regress/sql/foreign_data.sql +++ b/src/test/regress/sql/foreign_data.sql @@ -13,6 +13,11 @@ CREATE FUNCTION test_fdw_handler() AS :'regresslib', 'test_fdw_handler' LANGUAGE C; +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; + -- Clean up in case a prior regression run failed -- Suppress NOTICE messages when roles don't exist @@ -67,6 +72,13 @@ CREATE FUNCTION invalid_fdw_handler() RETURNS int LANGUAGE SQL AS 'SELECT 1;'; CREATE FOREIGN DATA WRAPPER test_fdw HANDLER invalid_fdw_handler; -- ERROR CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler HANDLER invalid_fdw_handler; -- ERROR CREATE FOREIGN DATA WRAPPER test_fdw HANDLER test_fdw_handler; + +-- should preserve dependencies on test_fdw_handler and test_fdw_connection +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +ALTER FOREIGN DATA WRAPPER test_fdw VALIDATOR postgresql_fdw_validator; +DROP FUNCTION test_fdw_handler(); -- ERROR +DROP FUNCTION test_fdw_connection(oid, oid, internal); -- ERROR + DROP FOREIGN DATA WRAPPER test_fdw; -- ALTER FOREIGN DATA WRAPPER @@ -383,10 +395,12 @@ COMMENT ON COLUMN ft1.c1 IS NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c4 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c5 integer DEFAULT 0; ALTER FOREIGN TABLE ft1 ADD COLUMN c6 integer; +ALTER FOREIGN TABLE ft1 ADD COLUMN IF NOT EXISTS c6 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c7 integer NOT NULL; ALTER FOREIGN TABLE ft1 ADD COLUMN c8 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c9 integer; ALTER FOREIGN TABLE ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); +ALTER FOREIGN TABLE ft1 ADD c11 integer; ALTER FOREIGN TABLE ft1 ALTER COLUMN c4 SET DEFAULT 0; ALTER FOREIGN TABLE ft1 ALTER COLUMN c5 DROP DEFAULT; @@ -419,6 +433,7 @@ ALTER FOREIGN TABLE ft1 OPTIONS (DROP delimiter, SET quote '~', ADD escape '@'); ALTER FOREIGN TABLE ft1 DROP COLUMN no_column; -- ERROR ALTER FOREIGN TABLE ft1 DROP COLUMN IF EXISTS no_column; ALTER FOREIGN TABLE ft1 DROP COLUMN c9; +ALTER FOREIGN TABLE ft1 DROP c11; ALTER FOREIGN TABLE ft1 ADD COLUMN c11 serial; ALTER FOREIGN TABLE ft1 SET SCHEMA foreign_schema; ALTER FOREIGN TABLE ft1 SET TABLESPACE ts; -- ERROR @@ -430,10 +445,12 @@ ALTER FOREIGN TABLE foreign_schema.ft1 RENAME TO foreign_table_1; -- alter noexisting table ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c4 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c6 integer; +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN IF NOT EXISTS c6 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c7 integer NOT NULL; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c8 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c9 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD COLUMN c10 integer OPTIONS (p1 'v1'); +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ADD c11 integer; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c6 SET NOT NULL; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 ALTER COLUMN c7 DROP NOT NULL; @@ -447,8 +464,10 @@ ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT IF EXISTS no_cons ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP CONSTRAINT ft1_c1_check; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OWNER TO regress_test_role; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 OPTIONS (DROP delimiter, SET quote '~', ADD escape '@'); +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN no_column; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN IF EXISTS no_column; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP COLUMN c9; +ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 DROP c11; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 SET SCHEMA foreign_schema; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME c1 TO foreign_column_1; ALTER FOREIGN TABLE IF EXISTS doesnt_exist_ft1 RENAME TO foreign_table_1; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 8159e36302242..7eb86b188f065 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -242,6 +242,70 @@ SELECT * FROM PKTABLE; DROP TABLE FKTABLE; DROP TABLE PKTABLE; +-- +-- Check RLS +-- +CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); + +-- Insert test data into PKTABLE +INSERT INTO PKTABLE VALUES (1, 'Test1'); +INSERT INTO PKTABLE VALUES (2, 'Test2'); +INSERT INTO PKTABLE VALUES (3, 'Test3'); + +-- Grant privileges on PKTABLE/FKTABLE to user regress_foreign_key_user +CREATE USER regress_foreign_key_user NOLOGIN; +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; +GRANT SELECT, INSERT ON FKTABLE TO regress_foreign_key_user; + +-- Enable RLS on PKTABLE and Create policies +ALTER TABLE PKTABLE ENABLE ROW LEVEL SECURITY; +CREATE POLICY pktable_view_odd_policy ON PKTABLE TO regress_foreign_key_user USING (ptest1 % 2 = 1); + +ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; + +SET ROLE regress_foreign_key_user; + +INSERT INTO FKTABLE VALUES (3, 5); +INSERT INTO FKTABLE VALUES (2, 5); -- success, REFERENCES are not subject to row security + +RESET ROLE; + +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; +DROP USER regress_foreign_key_user; + +-- +-- Check ACL +-- +CREATE TABLE PKTABLE ( ptest1 int PRIMARY KEY, ptest2 text ); +CREATE TABLE FKTABLE ( ftest1 int REFERENCES PKTABLE, ftest2 int ); + +-- Insert test data into PKTABLE +INSERT INTO PKTABLE VALUES (1, 'Test1'); +INSERT INTO PKTABLE VALUES (2, 'Test2'); +INSERT INTO PKTABLE VALUES (3, 'Test3'); + +-- Grant usage on PKTABLE to user regress_foreign_key_user +CREATE USER regress_foreign_key_user NOLOGIN; +GRANT SELECT ON PKTABLE TO regress_foreign_key_user; + +ALTER TABLE PKTABLE OWNER to regress_foreign_key_user; + +-- Inserting into FKTABLE should work +INSERT INTO FKTABLE VALUES (3, 5); + +-- Revoke usage on PKTABLE from user regress_foreign_key_user +REVOKE SELECT ON PKTABLE FROM regress_foreign_key_user; + +-- Inserting into FKTABLE should fail +INSERT INTO FKTABLE VALUES (2, 6); + +DROP TABLE FKTABLE; +DROP TABLE PKTABLE; + +DROP USER regress_foreign_key_user; + -- -- Check initial check upon ALTER TABLE -- @@ -784,6 +848,43 @@ INSERT INTO fktable VALUES (500, 1000); COMMIT; +-- Check that the existing FK trigger is both deferrable and initially deferred +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + +-- Changing the constraint to NOT ENFORCED drops the associated FK triggers +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED; +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + +-- Changing it back to ENFORCED will recreate the necessary FK triggers +-- that are deferrable and initially deferred +ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey ENFORCED; +SELECT conname, tgrelid::regclass as tgrel, + regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype, + tgdeferrable, tginitdeferred +FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) +WHERE conrelid = 'fktable'::regclass AND conname = 'fktable_fk_fkey' +ORDER BY tgrelid, tgtype; + +-- Verify that a deferrable, initially deferred foreign key still works +-- as expected after being set to NOT ENFORCED and then re-enabled +BEGIN; + +-- doesn't match PK, but no error yet +INSERT INTO fktable VALUES (2, 20); + +-- should catch error from INSERT at commit +COMMIT; + DROP TABLE fktable, pktable; -- tricky behavior: according to SQL99, if a deferred constraint is set @@ -1296,7 +1397,7 @@ UPDATE fk_notpartitioned_pk SET b = 2504 WHERE a = 2500; -- check psql behavior \d fk_notpartitioned_pk --- Check the exsting FK trigger +-- Check the existing FK trigger SELECT conname, tgrelid::regclass as tgrel, regexp_replace(tgname, '[0-9]+', 'N') as tgname, tgtype FROM pg_trigger t JOIN pg_constraint c ON (t.tgconstraint = c.oid) WHERE tgrelid IN (SELECT relid FROM pg_partition_tree('fk_partitioned_fk'::regclass) @@ -1389,22 +1490,44 @@ WHERE conrelid::regclass::text like 'fk_partitioned_fk%' ORDER BY oid::regclass: DROP TABLE fk_partitioned_fk, fk_notpartitioned_pk; --- NOT VALID foreign key on a non-partitioned table referencing a partitioned table +-- NOT VALID and NOT ENFORCED foreign key on a non-partitioned table +-- referencing a partitioned table CREATE TABLE fk_partitioned_pk (a int, b int, PRIMARY KEY (a, b)) PARTITION BY RANGE (a, b); CREATE TABLE fk_partitioned_pk_1 PARTITION OF fk_partitioned_pk FOR VALUES FROM (0,0) TO (1000,1000); +CREATE TABLE fk_partitioned_pk_2 PARTITION OF fk_partitioned_pk FOR VALUES FROM (1000,1000) TO (2000,2000); CREATE TABLE fk_notpartitioned_fk (b int, a int); -ALTER TABLE fk_notpartitioned_fk ADD FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID; - --- Constraint will be invalid. -SELECT conname, convalidated FROM pg_constraint +INSERT INTO fk_partitioned_pk VALUES(100,100), (1000,1000); +INSERT INTO fk_notpartitioned_fk VALUES(100,100), (1000,1000); +ALTER TABLE fk_notpartitioned_fk ADD CONSTRAINT fk_notpartitioned_fk_a_b_fkey + FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID; +ALTER TABLE fk_notpartitioned_fk ADD CONSTRAINT fk_notpartitioned_fk_a_b_fkey2 + FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT ENFORCED; + +-- All constraints will be invalid, and _fkey2 constraints will not be enforced. +SELECT conname, conenforced, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass ORDER BY oid::regclass::text; ALTER TABLE fk_notpartitioned_fk VALIDATE CONSTRAINT fk_notpartitioned_fk_a_b_fkey; +ALTER TABLE fk_notpartitioned_fk ALTER CONSTRAINT fk_notpartitioned_fk_a_b_fkey2 ENFORCED; --- All constraints are now valid. -SELECT conname, convalidated FROM pg_constraint +-- All constraints are now valid and enforced. +SELECT conname, conenforced, convalidated FROM pg_constraint WHERE conrelid = 'fk_notpartitioned_fk'::regclass ORDER BY oid::regclass::text; +-- test a self-referential FK +ALTER TABLE fk_partitioned_pk ADD CONSTRAINT selffk FOREIGN KEY (a, b) REFERENCES fk_partitioned_pk NOT VALID; +CREATE TABLE fk_partitioned_pk_3 PARTITION OF fk_partitioned_pk FOR VALUES FROM (2000,2000) TO (3000,3000) + PARTITION BY RANGE (a); +CREATE TABLE fk_partitioned_pk_3_1 PARTITION OF fk_partitioned_pk_3 FOR VALUES FROM (2000) TO (2100); +SELECT conname, conenforced, convalidated FROM pg_constraint +WHERE conrelid = 'fk_partitioned_pk'::regclass AND contype = 'f' +ORDER BY oid::regclass::text; +ALTER TABLE fk_partitioned_pk_2 VALIDATE CONSTRAINT selffk; +ALTER TABLE fk_partitioned_pk VALIDATE CONSTRAINT selffk; +SELECT conname, conenforced, convalidated FROM pg_constraint +WHERE conrelid = 'fk_partitioned_pk'::regclass AND contype = 'f' +ORDER BY oid::regclass::text; + DROP TABLE fk_notpartitioned_fk, fk_partitioned_pk; -- Test some other exotic foreign key features: MATCH SIMPLE, ON UPDATE/DELETE @@ -2364,3 +2487,196 @@ SET client_min_messages TO warning; DROP SCHEMA fkpart12 CASCADE; RESET client_min_messages; RESET search_path; + +-- Exercise the column mapping code with foreign keys. In this test we'll +-- create a partitioned table which has a partition with a dropped column and +-- check to ensure that an UPDATE cascades the changes correctly to the +-- partitioned table. +CREATE SCHEMA fkpart13; +SET search_path TO fkpart13; + +CREATE TABLE fkpart13_t1 (a int PRIMARY KEY); + +CREATE TABLE fkpart13_t2 ( + part_id int PRIMARY KEY, + column_to_drop int, + FOREIGN KEY (part_id) REFERENCES fkpart13_t1 ON UPDATE CASCADE ON DELETE CASCADE +) PARTITION BY LIST (part_id); + +CREATE TABLE fkpart13_t2_p1 PARTITION OF fkpart13_t2 FOR VALUES IN (1); + +-- drop the column +ALTER TABLE fkpart13_t2 DROP COLUMN column_to_drop; + +-- create a new partition without the dropped column +CREATE TABLE fkpart13_t2_p2 PARTITION OF fkpart13_t2 FOR VALUES IN (2); + +CREATE TABLE fkpart13_t3 ( + a int NOT NULL, + FOREIGN KEY (a) + REFERENCES fkpart13_t2 + ON UPDATE CASCADE ON DELETE CASCADE +); + +INSERT INTO fkpart13_t1 (a) VALUES (1); +INSERT INTO fkpart13_t2 (part_id) VALUES (1); +INSERT INTO fkpart13_t3 (a) VALUES (1); + +-- Test that a cascading update works correctly with the dropped column +UPDATE fkpart13_t1 SET a = 2 WHERE a = 1; +SELECT tableoid::regclass,* FROM fkpart13_t2; +SELECT tableoid::regclass,* FROM fkpart13_t3; + +-- Exercise code in ExecGetTriggerResultRel() as there's been previous issues +-- with ResultRelInfos being returned with the incorrect ri_RootResultRelInfo +WITH cte AS ( + UPDATE fkpart13_t2_p1 SET part_id = part_id +) UPDATE fkpart13_t1 SET a = 2 WHERE a = 1; + +DROP SCHEMA fkpart13 CASCADE; +RESET search_path; + +-- Tests foreign key check fast-path no-cache path. +CREATE TABLE fp_pk_alter (a int PRIMARY KEY); +INSERT INTO fp_pk_alter SELECT generate_series(1, 100); +CREATE TABLE fp_fk_alter (a int); +INSERT INTO fp_fk_alter SELECT generate_series(1, 100); +-- Validation path: should succeed +ALTER TABLE fp_fk_alter ADD FOREIGN KEY (a) REFERENCES fp_pk_alter; +INSERT INTO fp_fk_alter VALUES (101); -- should fail (constraint active) +DROP TABLE fp_fk_alter, fp_pk_alter; + +-- Separate test: validation catches existing violation +CREATE TABLE fp_pk_alter2 (a int PRIMARY KEY); +INSERT INTO fp_pk_alter2 VALUES (1); +CREATE TABLE fp_fk_alter2 (a int); +INSERT INTO fp_fk_alter2 VALUES (1), (200); -- 200 has no PK match +ALTER TABLE fp_fk_alter2 ADD FOREIGN KEY (a) REFERENCES fp_pk_alter2; -- should fail +DROP TABLE fp_fk_alter2, fp_pk_alter2; + +-- Tests that the fast-path handles caching for multiple constraints +CREATE TABLE fp_pk1 (a int PRIMARY KEY); +CREATE TABLE fp_pk2 (b int PRIMARY KEY); +INSERT INTO fp_pk1 VALUES (1); +INSERT INTO fp_pk2 VALUES (1); +CREATE TABLE fp_multi_fk ( + a int REFERENCES fp_pk1, + b int REFERENCES fp_pk2 +); +INSERT INTO fp_multi_fk VALUES (1, 1); -- two constraints, one batch +INSERT INTO fp_multi_fk VALUES (1, 2); -- second constraint fails +DROP TABLE fp_multi_fk, fp_pk1, fp_pk2; + +-- Test that fast-path cache handles deferred constraints and SET CONSTRAINTS IMMEDIATE +CREATE TABLE fp_pk_defer (a int PRIMARY KEY); +CREATE TABLE fp_fk_defer (a int REFERENCES fp_pk_defer DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fp_pk_defer VALUES (1), (2); + +BEGIN; +INSERT INTO fp_fk_defer VALUES (1); +INSERT INTO fp_fk_defer VALUES (2); +SET CONSTRAINTS ALL IMMEDIATE; -- fires batch callback here +INSERT INTO fp_fk_defer VALUES (3); -- should fail, also tests that cache was cleaned up +COMMIT; +DROP TABLE fp_pk_defer, fp_fk_defer; + +-- Subtransaction abort: cached state must be invalidated on ROLLBACK TO +CREATE TABLE fp_pk_subxact (a int PRIMARY KEY); +CREATE TABLE fp_fk_subxact (a int REFERENCES fp_pk_subxact); +INSERT INTO fp_pk_subxact VALUES (1), (2); +BEGIN; +INSERT INTO fp_fk_subxact VALUES (1); +SAVEPOINT sp1; +INSERT INTO fp_fk_subxact VALUES (2); +ROLLBACK TO sp1; +INSERT INTO fp_fk_subxact VALUES (1); +COMMIT; +SELECT * FROM fp_fk_subxact; +DROP TABLE fp_fk_subxact, fp_pk_subxact; + +-- FK check must see PK rows inserted by earlier AFTER triggers +-- firing on the same statement +CREATE TABLE fp_pk_cci (a int PRIMARY KEY); +CREATE TABLE fp_fk_cci (a int REFERENCES fp_pk_cci); + +CREATE FUNCTION fp_auto_pk() RETURNS trigger AS $$ +BEGIN + RAISE NOTICE 'fp_auto_pk called'; + INSERT INTO fp_pk_cci VALUES (NEW.a); + RETURN NEW; +END $$ LANGUAGE plpgsql; + +-- Name sorts before the RI trigger, so fires first per row +CREATE TRIGGER "AAA_auto" AFTER INSERT ON fp_fk_cci + FOR EACH ROW EXECUTE FUNCTION fp_auto_pk(); + +-- Should succeed: AAA_auto provisions the PK row before RI check +INSERT INTO fp_fk_cci VALUES (1), (2), (3); + +DROP TABLE fp_fk_cci, fp_pk_cci; +DROP FUNCTION fp_auto_pk; + +-- Multi-column FK: exercises batched per-row probing with composite keys +CREATE TABLE fp_pk_multi (a int, b int, PRIMARY KEY (a, b)); +INSERT INTO fp_pk_multi SELECT i, i FROM generate_series(1, 100) i; +CREATE TABLE fp_fk_multi (x int, a int, b int, + FOREIGN KEY (a, b) REFERENCES fp_pk_multi); +INSERT INTO fp_fk_multi SELECT i, i, i FROM generate_series(1, 100) i; +INSERT INTO fp_fk_multi VALUES (1, 999, 999); +DROP TABLE fp_fk_multi, fp_pk_multi; + +-- Multi-column FK with columns in different order than PK index. +-- The FK references columns in a different order than they appear in the +-- PK's primary key, which requires mapping constraint key positions to +-- index column positions when building scan keys. +CREATE TABLE fp_pk_order (a int, b text, c int, PRIMARY KEY (a, b, c)); +INSERT INTO fp_pk_order VALUES (1, 'one', 10), (2, 'two', 20); +CREATE TABLE fp_fk_order ( + x int, + c int, + b text, + a int, + FOREIGN KEY (a, c, b) REFERENCES fp_pk_order (a, c, b) -- c and b swapped +); +INSERT INTO fp_fk_order VALUES (1, 10, 'one', 1); -- should succeed +INSERT INTO fp_fk_order VALUES (2, 20, 'two', 2); -- should succeed +INSERT INTO fp_fk_order VALUES (3, 99, 'none', 9); -- should fail +DROP TABLE fp_fk_order, fp_pk_order; + +-- Same-type columns in different order: +CREATE TABLE fp_pk_same (c1 int, c2 int, PRIMARY KEY (c1, c2)); +INSERT INTO fp_pk_same VALUES (1, 2); +CREATE TABLE fp_fk_same (c1 int, c2 int, + FOREIGN KEY (c2, c1) REFERENCES fp_pk_same (c2, c1)); +INSERT INTO fp_fk_same VALUES (1, 2); -- should succeed +INSERT INTO fp_fk_same VALUES (9, 9); -- should fail +DROP TABLE fp_fk_same, fp_pk_same; + +-- Deferred constraint: batch flushed at COMMIT, not at statement end +CREATE TABLE fp_pk_commit (a int PRIMARY KEY); +CREATE TABLE fp_fk_commit (a int REFERENCES fp_pk_commit + DEFERRABLE INITIALLY DEFERRED); +INSERT INTO fp_pk_commit VALUES (1); +BEGIN; +INSERT INTO fp_fk_commit VALUES (1); +INSERT INTO fp_fk_commit VALUES (1); +INSERT INTO fp_fk_commit VALUES (999); +COMMIT; +DROP TABLE fp_fk_commit, fp_pk_commit; + +-- Cross-type FK with bulk insert: int8 FK referencing int4 PK, +-- values cast during array construction +CREATE TABLE fp_pk_cross (a int4 PRIMARY KEY); +INSERT INTO fp_pk_cross SELECT generate_series(1, 200); +CREATE TABLE fp_fk_cross (a int8 REFERENCES fp_pk_cross); +INSERT INTO fp_fk_cross SELECT generate_series(1, 200); +INSERT INTO fp_fk_cross VALUES (999); +DROP TABLE fp_fk_cross, fp_pk_cross; + +-- Duplicate FK values: when using the batched SAOP path, every +-- row must be recognized as satisfied, not just the first match +CREATE TABLE fp_pk_dup (a int PRIMARY KEY); +INSERT INTO fp_pk_dup VALUES (1); +CREATE TABLE fp_fk_dup (a int REFERENCES fp_pk_dup); +INSERT INTO fp_fk_dup SELECT 1 FROM generate_series(1, 100); +DROP TABLE fp_fk_dup, fp_pk_dup; diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 4ec155f2da989..6746cd4632b6c 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -217,6 +217,10 @@ COPY gtest1 FROM stdin; COPY gtest1 (a, b) FROM stdin; +COPY gtest1 FROM stdin WHERE b <> 10; + +COPY gtest1 FROM stdin WHERE gtest1 IS NULL; + SELECT * FROM gtest1 ORDER BY a; TRUNCATE gtest3; @@ -235,6 +239,9 @@ COPY gtest3 (a, b) FROM stdin; SELECT * FROM gtest3 ORDER BY a; +-- COPY JSON should exclude generated columns, same as text/CSV +COPY gtest1 TO stdout WITH (FORMAT json); + -- null values CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) STORED); INSERT INTO gtest2 VALUES (1); @@ -340,13 +347,47 @@ CREATE TABLE gtest21a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0 INSERT INTO gtest21a (a) VALUES (1); -- ok INSERT INTO gtest21a (a) VALUES (0); -- violates constraint -CREATE TABLE gtest21b (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +-- also check with table constraint syntax +CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED, CONSTRAINT cc NOT NULL b); +INSERT INTO gtest21ax (a) VALUES (0); -- violates constraint +INSERT INTO gtest21ax (a) VALUES (1); --ok +-- SET EXPRESSION supports not null constraint +ALTER TABLE gtest21ax ALTER COLUMN b SET EXPRESSION AS (nullif(a, 1)); --error +DROP TABLE gtest21ax; + +CREATE TABLE gtest21ax (a int PRIMARY KEY, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); +ALTER TABLE gtest21ax ADD CONSTRAINT cc NOT NULL b; +INSERT INTO gtest21ax (a) VALUES (0); -- violates constraint +DROP TABLE gtest21ax; + +CREATE TABLE gtest21b (a int, b int GENERATED ALWAYS AS (nullif(a, 0)) STORED); ALTER TABLE gtest21b ALTER COLUMN b SET NOT NULL; INSERT INTO gtest21b (a) VALUES (1); -- ok -INSERT INTO gtest21b (a) VALUES (0); -- violates constraint +INSERT INTO gtest21b (a) VALUES (2), (0); -- violates constraint +INSERT INTO gtest21b (a) VALUES (NULL); -- error ALTER TABLE gtest21b ALTER COLUMN b DROP NOT NULL; INSERT INTO gtest21b (a) VALUES (0); -- ok now +-- not-null constraint with partitioned table +CREATE TABLE gtestnn_parent ( + f1 int, + f2 bigint, + f3 bigint GENERATED ALWAYS AS (nullif(f1, 1) + nullif(f2, 10)) STORED NOT NULL +) PARTITION BY RANGE (f1); +CREATE TABLE gtestnn_child PARTITION OF gtestnn_parent FOR VALUES FROM (1) TO (5); +CREATE TABLE gtestnn_childdef PARTITION OF gtestnn_parent default; +-- check the error messages +INSERT INTO gtestnn_parent VALUES (2, 2, default), (3, 5, default), (14, 12, default); -- ok +INSERT INTO gtestnn_parent VALUES (1, 2, default); -- error +INSERT INTO gtestnn_parent VALUES (2, 10, default); -- error +ALTER TABLE gtestnn_parent ALTER COLUMN f3 SET EXPRESSION AS (nullif(f1, 2) + nullif(f2, 11)); -- error +INSERT INTO gtestnn_parent VALUES (10, 11, default); -- ok +SELECT * FROM gtestnn_parent ORDER BY f1, f2, f3; +-- test ALTER TABLE ADD COLUMN +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 14) + nullif(f2, 10)) STORED; -- error +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 13) + nullif(f2, 5)) STORED; -- error +ALTER TABLE gtestnn_parent ADD COLUMN c int NOT NULL GENERATED ALWAYS AS (nullif(f1, 4) + nullif(f2, 6)) STORED; -- ok + -- index constraints CREATE TABLE gtest22a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a / 2) STORED UNIQUE); INSERT INTO gtest22a VALUES (2); @@ -419,6 +460,11 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A INSERT INTO gtest24r (a) VALUES (4); -- ok INSERT INTO gtest24r (a) VALUES (6); -- error +CREATE TABLE gtest24at (a int PRIMARY KEY); +ALTER TABLE gtest24at ADD COLUMN b gtestdomain1 GENERATED ALWAYS AS (a * 2) STORED; -- ok +CREATE TABLE gtest24ata (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) STORED); +ALTER TABLE gtest24ata ALTER COLUMN b TYPE gtestdomain1; -- ok + CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL); CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) STORED); INSERT INTO gtest24nn (a) VALUES (4); -- ok @@ -500,7 +546,10 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3; -- generated columns in partition key (not allowed) CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE (f3); +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3)); CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((f3 * 3)); +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((gtest_part_key)); +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) STORED) PARTITION BY RANGE ((gtest_part_key is not null)); -- ALTER TABLE ... ADD COLUMN CREATE TABLE gtest25 (a int PRIMARY KEY); @@ -530,6 +579,14 @@ ALTER TABLE gtest27 ALTER COLUMN x TYPE numeric; SELECT * FROM gtest27; ALTER TABLE gtest27 ALTER COLUMN x TYPE boolean USING x <> 0; -- error ALTER TABLE gtest27 ALTER COLUMN x DROP DEFAULT; -- error +-- test not-null checking during table rewrite +INSERT INTO gtest27 (a, b) VALUES (NULL, NULL); +ALTER TABLE gtest27 + DROP COLUMN x, + ALTER COLUMN a TYPE bigint, + ALTER COLUMN b TYPE bigint, + ADD COLUMN x bigint GENERATED ALWAYS AS ((a + b) * 2) STORED NOT NULL; -- error +DELETE FROM gtest27 WHERE a IS NULL AND b IS NULL; -- It's possible to alter the column types this way: ALTER TABLE gtest27 DROP COLUMN x, @@ -595,6 +652,19 @@ ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') STORED, c text); CREATE TABLE gtest31_2 (x int, y gtest31_1); ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar; -- fails + +-- bug #18970: these cases are unsupported, but make sure they fail cleanly +ALTER TABLE gtest31_2 ADD CONSTRAINT cc CHECK ((y).b IS NOT NULL); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello1'); +ALTER TABLE gtest31_2 DROP CONSTRAINT cc; + +CREATE STATISTICS gtest31_2_stat ON ((y).b is not null) FROM gtest31_2; +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello2'); +DROP STATISTICS gtest31_2_stat; + +CREATE INDEX gtest31_2_y_idx ON gtest31_2(((y).b)); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello3'); + DROP TABLE gtest31_1, gtest31_2; -- Check it for a partitioned table, too @@ -731,6 +801,29 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); \d gtest28* +-- rule actions referring to generated columns: +-- NEW.b in a rule action should reflect the generated column's new value +CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED); +CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); +CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); +CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); +INSERT INTO gtest_rule (a) VALUES (1); +UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET a = (SELECT max(b) FROM gtest_rule); +SELECT * FROM gtest_rule_log; +DROP RULE gtest_rule_upd ON gtest_rule; +DROP RULE gtest_rule_ins ON gtest_rule; +DROP TABLE gtest_rule_log; + +-- rule quals referring to generated columns: +-- NEW.b in the rule qual should reflect the generated column's new value +CREATE RULE gtest_rule_qual AS ON UPDATE TO gtest_rule WHERE NEW.b > 100 + DO INSTEAD NOTHING; +UPDATE gtest_rule SET a = 100; +SELECT * FROM gtest_rule; +DROP TABLE gtest_rule; -- sanity check of system catalog SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v'); diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index b4eedeee2fb27..9c2bb6590b349 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -217,6 +217,10 @@ COPY gtest1 FROM stdin; COPY gtest1 (a, b) FROM stdin; +COPY gtest1 FROM stdin WHERE b <> 10; + +COPY gtest1 FROM stdin WHERE gtest1 IS NULL; + SELECT * FROM gtest1 ORDER BY a; TRUNCATE gtest3; @@ -235,6 +239,9 @@ COPY gtest3 (a, b) FROM stdin; SELECT * FROM gtest3 ORDER BY a; +-- COPY JSON should exclude generated columns, same as text/CSV +COPY gtest1 TO stdout WITH (FORMAT json); + -- null values CREATE TABLE gtest2 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (NULL) VIRTUAL); INSERT INTO gtest2 VALUES (1); @@ -252,11 +259,11 @@ CREATE TYPE double_int as (a int, b int); CREATE TABLE gtest4 ( a int, b double_int GENERATED ALWAYS AS ((a * 2, a * 3)) VIRTUAL -); -INSERT INTO gtest4 VALUES (1), (6); -SELECT * FROM gtest4; +); -- fails, user-defined type +--INSERT INTO gtest4 VALUES (1), (6); +--SELECT * FROM gtest4; -DROP TABLE gtest4; +--DROP TABLE gtest4; DROP TYPE double_int; -- using tableoid is allowed @@ -290,20 +297,21 @@ GRANT SELECT (a, c) ON gtest11 TO regress_user11; CREATE FUNCTION gf1(a int) RETURNS int AS $$ SELECT a * 3 $$ IMMUTABLE LANGUAGE SQL; REVOKE ALL ON FUNCTION gf1(int) FROM PUBLIC; -CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL); -INSERT INTO gtest12 VALUES (1, 10), (2, 20); -GRANT SELECT (a, c), INSERT ON gtest12 TO regress_user11; +CREATE TABLE gtest12 (a int PRIMARY KEY, b int, c int GENERATED ALWAYS AS (gf1(b)) VIRTUAL); -- fails, user-defined function +--INSERT INTO gtest12 VALUES (1, 10), (2, 20); +--GRANT SELECT (a, c), INSERT ON gtest12 TO regress_user11; SET ROLE regress_user11; SELECT a, b FROM gtest11; -- not allowed SELECT a, c FROM gtest11; -- allowed SELECT gf1(10); -- not allowed -INSERT INTO gtest12 VALUES (3, 30), (4, 40); -- allowed (does not actually invoke the function) -SELECT a, c FROM gtest12; -- currently not allowed because of function permissions, should arguably be allowed +--INSERT INTO gtest12 VALUES (3, 30), (4, 40); -- allowed (does not actually invoke the function) +--SELECT a, c FROM gtest12; -- currently not allowed because of function permissions, should arguably be allowed RESET ROLE; -DROP FUNCTION gf1(int); -- fail -DROP TABLE gtest11, gtest12; +--DROP FUNCTION gf1(int); -- fail +DROP TABLE gtest11; +--DROP TABLE gtest12; DROP FUNCTION gf1(int); DROP USER regress_user11; @@ -312,8 +320,13 @@ CREATE TABLE gtest20 (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTU INSERT INTO gtest20 (a) VALUES (10); -- ok INSERT INTO gtest20 (a) VALUES (30); -- violates constraint -ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100); -- violates constraint (currently not supported) -ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3); -- ok (currently not supported) +ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 100); -- violates constraint +ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 3); -- ok +-- table rewrite should not happen +SELECT pg_relation_filenode('gtest20') AS gtest20_filenode \gset +ALTER TABLE gtest20 ALTER COLUMN b SET EXPRESSION AS (a * 4), ADD COLUMN c INT DEFAULT 11; +SELECT pg_relation_filenode('gtest20') = :gtest20_filenode AS is_same_file; +\d gtest20 CREATE TABLE gtest20a (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); INSERT INTO gtest20a (a) VALUES (10); @@ -453,6 +466,11 @@ CREATE TABLE gtest24r (a int PRIMARY KEY, b gtestdomain1range GENERATED ALWAYS A --INSERT INTO gtest24r (a) VALUES (4); -- ok --INSERT INTO gtest24r (a) VALUES (6); -- error +CREATE TABLE gtest24at (a int PRIMARY KEY); +ALTER TABLE gtest24at ADD COLUMN b gtestdomain1 GENERATED ALWAYS AS (a * 2) VIRTUAL; -- error +CREATE TABLE gtest24ata (a int PRIMARY KEY, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); +ALTER TABLE gtest24ata ALTER COLUMN b TYPE gtestdomain1; -- error + CREATE DOMAIN gtestdomainnn AS int CHECK (VALUE IS NOT NULL); CREATE TABLE gtest24nn (a int, b gtestdomainnn GENERATED ALWAYS AS (a * 2) VIRTUAL); --INSERT INTO gtest24nn (a) VALUES (4); -- ok @@ -523,6 +541,13 @@ ALTER TABLE gtest_child ALTER COLUMN f3 SET EXPRESSION AS (f2 * 10); \d gtest_child3 SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3; +-- check constraint was validated based on each partitions's generation expression +ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 19); -- error +ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 < 66); -- error +ALTER TABLE gtest_parent ADD CONSTRAINT cc1 CHECK (f3 <> 33); -- error +ALTER TABLE gtest_parent ADD CONSTRAINT cc CHECK (f3 < 67); -- ok +ALTER TABLE gtest_parent DROP CONSTRAINT cc; + -- alter generation expression of parent and all its children altogether ALTER TABLE gtest_parent ALTER COLUMN f3 SET EXPRESSION AS (f2 * 2); \d gtest_parent @@ -534,7 +559,10 @@ SELECT tableoid::regclass, * FROM gtest_parent ORDER BY 1, 2, 3; -- generated columns in partition key (not allowed) CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE (f3); +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3)); CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((f3 * 3)); +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((gtest_part_key)); +CREATE TABLE gtest_part_key (f1 date NOT NULL, f2 bigint, f3 bigint GENERATED ALWAYS AS (f2 * 2) VIRTUAL) PARTITION BY RANGE ((gtest_part_key is not null)); -- ALTER TABLE ... ADD COLUMN CREATE TABLE gtest25 (a int PRIMARY KEY); @@ -637,6 +665,19 @@ ALTER TABLE gtest30_1 ALTER COLUMN b DROP EXPRESSION; -- error CREATE TABLE gtest31_1 (a int, b text GENERATED ALWAYS AS ('hello') VIRTUAL, c text); CREATE TABLE gtest31_2 (x int, y gtest31_1); ALTER TABLE gtest31_1 ALTER COLUMN b TYPE varchar; -- fails + +-- bug #18970 +ALTER TABLE gtest31_2 ADD CONSTRAINT cc CHECK ((y).b IS NOT NULL); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello1'); +ALTER TABLE gtest31_2 DROP CONSTRAINT cc; + +CREATE STATISTICS gtest31_2_stat ON ((y).b is not null) FROM gtest31_2; +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello2'); +DROP STATISTICS gtest31_2_stat; + +CREATE INDEX gtest31_2_y_idx ON gtest31_2(((y).b)); +ALTER TABLE gtest31_1 ALTER COLUMN b SET EXPRESSION AS ('hello3'); + DROP TABLE gtest31_1, gtest31_2; -- Check it for a partitioned table, too @@ -773,6 +814,29 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED); \d gtest28* +-- rule actions referring to generated columns: +-- NEW.b in a rule action should reflect the generated column's new value +CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL); +CREATE TABLE gtest_rule_log (op text, old_b int, new_b int); +CREATE RULE gtest_rule_upd AS ON UPDATE TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('UPD', OLD.b, NEW.b); +CREATE RULE gtest_rule_ins AS ON INSERT TO gtest_rule + DO ALSO INSERT INTO gtest_rule_log VALUES ('INS', NULL, NEW.b); +INSERT INTO gtest_rule (a) VALUES (1); +UPDATE gtest_rule SET a = 10; +UPDATE gtest_rule SET a = (SELECT max(b) FROM gtest_rule); +SELECT * FROM gtest_rule_log; +DROP RULE gtest_rule_upd ON gtest_rule; +DROP RULE gtest_rule_ins ON gtest_rule; +DROP TABLE gtest_rule_log; + +-- rule quals referring to generated columns: +-- NEW.b in the rule qual should reflect the generated column's new value +CREATE RULE gtest_rule_qual AS ON UPDATE TO gtest_rule WHERE NEW.b > 100 + DO INSTEAD NOTHING; +UPDATE gtest_rule SET a = 100; +SELECT * FROM gtest_rule; +DROP TABLE gtest_rule; -- sanity check of system catalog SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT IN ('', 's', 'v'); @@ -784,14 +848,19 @@ SELECT attrelid, attname, attgenerated FROM pg_attribute WHERE attgenerated NOT -- these tests are specific to generated_virtual.sql -- +-- using user-defined type not yet supported +CREATE TABLE gtest24xxx (a gtestdomain1, b gtestdomain1, c int GENERATED ALWAYS AS (greatest(a, b)) VIRTUAL); -- error + create table gtest32 ( a int primary key, b int generated always as (a * 2), c int generated always as (10 + 10), - d int generated always as (coalesce(a, 100)) + d int generated always as (coalesce(f, 100)), + e int, + f int ); -insert into gtest32 values (1), (2); +insert into gtest32 (a, f) values (1, 1), (2, 2); analyze gtest32; -- Ensure that nullingrel bits are propagated into the generation expressions @@ -829,7 +898,74 @@ select t2.* from gtest32 t1 left join gtest32 t2 on false; select t2.* from gtest32 t1 left join gtest32 t2 on false; explain (verbose, costs off) -select * from gtest32 t group by grouping sets (a, b, c, d) having c = 20; -select * from gtest32 t group by grouping sets (a, b, c, d) having c = 20; +select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20; +select * from gtest32 t group by grouping sets (a, b, c, d, e, f) having c = 20; + +-- Ensure that the virtual generated columns in ALTER COLUMN TYPE USING expression are expanded +alter table gtest32 alter column e type bigint using b; + +-- Ensure that virtual generated column references within SubLinks that should +-- be transformed into joins can get expanded +explain (costs off) +select 1 from gtest32 t1 where exists + (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2); + +select 1 from gtest32 t1 where exists + (select 1 from gtest32 t2 where t1.a > t2.a and t2.b = 2); drop table gtest32; + +-- Ensure that virtual generated columns in constraint expressions are expanded +create table gtest33 (a int, b int generated always as (a * 2) virtual not null, check (b > 10)); +set constraint_exclusion to on; + +-- should get a dummy Result, not a seq scan +explain (costs off) +select * from gtest33 where b < 10; + +-- should get a dummy Result, not a seq scan +explain (costs off) +select * from gtest33 where b is null; + +reset constraint_exclusion; +drop table gtest33; + +-- Ensure that EXCLUDED. in INSERT ... ON CONFLICT +-- DO UPDATE is expanded to the generation expression, both for plain and +-- partitioned target relations. +create table gtest34 (id int primary key, a int, + c int generated always as (a * 10) virtual); +insert into gtest34 values (1, 5); +insert into gtest34 values (1, 7) + on conflict (id) do update set a = excluded.c returning *; +insert into gtest34 values (1, 2) + on conflict (id) do update set a = gtest34.c + excluded.c returning *; +insert into gtest34 values (1, 3) + on conflict (id) do update set a = 999 where excluded.c > 20 returning *; +drop table gtest34; + +create table gtest34p (id int primary key, a int, + c int generated always as (a * 10) virtual) + partition by range (id); +create table gtest34p_1 partition of gtest34p for values from (1) to (100); +insert into gtest34p values (1, 5); +insert into gtest34p values (1, 7) + on conflict (id) do update set a = excluded.c returning *; +insert into gtest34p values (1, 2) + on conflict (id) do update set a = gtest34p.c + excluded.c returning *; +drop table gtest34p; + +-- Ensure that virtual generated columns work with WHERE CURRENT OF +create table gtest_cursor (id int primary key, a int, b int generated always as (a * 2) virtual); +insert into gtest_cursor values (1, 10), (2, 20), (3, 30); + +begin; +declare curs cursor for select * from gtest_cursor order by id for update; +fetch 1 from curs; +update gtest_cursor set a = 99 where current of curs; +select * from gtest_cursor order by id; +delete from gtest_cursor where current of curs; +select * from gtest_cursor order by id; +commit; + +drop table gtest_cursor; diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql new file mode 100644 index 0000000000000..4ff98817420db --- /dev/null +++ b/src/test/regress/sql/graph_table.sql @@ -0,0 +1,585 @@ +CREATE SCHEMA graph_table_tests; +GRANT USAGE ON SCHEMA graph_table_tests TO PUBLIC; +SET search_path = graph_table_tests; + +CREATE TABLE products ( + product_no integer PRIMARY KEY, + name varchar, + price numeric +); + +CREATE TABLE customers ( + customer_id integer PRIMARY KEY, + name varchar, + address varchar +); + +CREATE TABLE orders ( + order_id integer PRIMARY KEY, + ordered_when date +); + +CREATE TABLE order_items ( + order_items_id integer PRIMARY KEY, + order_id integer REFERENCES orders (order_id), + product_no integer REFERENCES products (product_no), + quantity integer +); + +CREATE TABLE customer_orders ( + customer_orders_id integer PRIMARY KEY, + customer_id integer REFERENCES customers (customer_id), + order_id integer REFERENCES orders (order_id) +); + +CREATE TABLE wishlists ( + wishlist_id integer PRIMARY KEY, + wishlist_name varchar +); + +CREATE TABLE wishlist_items ( + wishlist_items_id integer PRIMARY KEY, + wishlist_id integer REFERENCES wishlists (wishlist_id), + product_no integer REFERENCES products (product_no) +); + +CREATE TABLE customer_wishlists ( + customer_wishlist_id integer PRIMARY KEY, + customer_id integer REFERENCES customers (customer_id), + wishlist_id integer REFERENCES wishlists (wishlist_id) +); + +CREATE PROPERTY GRAPH myshop + VERTEX TABLES ( + products, + customers, + orders + DEFAULT LABEL + LABEL lists PROPERTIES (order_id AS node_id, 'order'::varchar(10) AS list_type), + wishlists + DEFAULT LABEL + LABEL lists PROPERTIES (wishlist_id AS node_id, 'wishlist'::varchar(10) AS list_type) + ) + EDGE TABLES ( + order_items KEY (order_items_id) + SOURCE KEY (order_id) REFERENCES orders (order_id) + DESTINATION KEY (product_no) REFERENCES products (product_no) + DEFAULT LABEL + LABEL list_items PROPERTIES (order_id AS link_id, product_no), + wishlist_items KEY (wishlist_items_id) + SOURCE KEY (wishlist_id) REFERENCES wishlists (wishlist_id) + DESTINATION KEY (product_no) REFERENCES products (product_no) + DEFAULT LABEL + LABEL list_items PROPERTIES (wishlist_id AS link_id, product_no), + customer_orders KEY (customer_orders_id) + SOURCE KEY (customer_id) REFERENCES customers (customer_id) + DESTINATION KEY (order_id) REFERENCES orders (order_id) + DEFAULT LABEL + LABEL cust_lists PROPERTIES (customer_id, order_id AS link_id), + customer_wishlists KEY (customer_wishlist_id) + SOURCE KEY (customer_id) REFERENCES customers (customer_id) + DESTINATION KEY (wishlist_id) REFERENCES wishlists (wishlist_id) + DEFAULT LABEL + LABEL cust_lists PROPERTIES (customer_id, wishlist_id AS link_id) + ); + +SELECT customer_name FROM GRAPH_TABLE (xxx MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name)); -- error +SELECT customer_name FROM GRAPH_TABLE (pg_class MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name)); -- error +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (cx.name AS customer_name)); -- error +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.namex AS customer_name)); -- error +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers|employees WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name)); -- error +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders] COLUMNS (c.name AS customer_name)); -- error +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers), (o IS orders) COLUMNS (c.name AS customer_name)); -- error +SELECT * FROM GRAPH_TABLE (myshop MATCH COLUMNS (1 AS col)); -- error, empty match clause +SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)->{1,2}(o IS orders) COLUMNS (c.name AS customer_name)); -- error +SELECT * FROM GRAPH_TABLE (myshop MATCH ((c IS customers)->(o IS orders)) COLUMNS (c.name)); + +-- a property graph can be referenced only from within GRAPH_TABLE clause. +SELECT * FROM myshop; -- error +COPY myshop TO stdout; -- error +INSERT INTO myshop VALUES (1); -- error + +INSERT INTO products VALUES + (1, 'product1', 10), + (2, 'product2', 20), + (3, 'product3', 30); +INSERT INTO customers VALUES + (1, 'customer1', 'US'), + (2, 'customer2', 'CA'), + (3, 'customer3', 'GL'); +INSERT INTO orders VALUES + (1, date '2024-01-01'), + (2, date '2024-01-02'), + (3, date '2024-01-03'); +INSERT INTO wishlists VALUES + (1, 'wishlist1'), + (2, 'wishlist2'), + (3, 'wishlist3'); +INSERT INTO order_items (order_items_id, order_id, product_no, quantity) VALUES + (1, 1, 1, 5), + (2, 1, 2, 10), + (3, 2, 1, 7); +INSERT INTO customer_orders (customer_orders_id, customer_id, order_id) VALUES + (1, 1, 1), + (2, 2, 2); +INSERT INTO customer_wishlists (customer_wishlist_id, customer_id, wishlist_id) VALUES + (1, 2, 3), + (2, 3, 1), + (3, 3, 2); +INSERT INTO wishlist_items (wishlist_items_id, wishlist_id, product_no) VALUES + (1, 1, 2), + (2, 1, 3), + (3, 2, 1), + (4, 3, 1); + +-- single element path pattern +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (c.name)); +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name)); +-- graph element specification without label or variable +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[]->(o IS orders) COLUMNS (c.name AS customer_name)); +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[co IS customer_orders]->(o IS orders WHERE o.ordered_when = date '2024-01-02') COLUMNS (c.name, c.address)); +SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)-[IS customer_orders]->(c IS customers) COLUMNS (c.name, o.ordered_when)); +SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)<-[IS customer_orders]-(c IS customers) COLUMNS (c.name, o.ordered_when)); +-- spaces around pattern operators +SELECT * FROM GRAPH_TABLE (myshop MATCH ( o IS orders ) <- [ IS customer_orders ] - (c IS customers) COLUMNS ( c.name, o.ordered_when)); +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS cust_lists]->(l IS lists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name, l.list_type)) ORDER BY customer_name, product_name, list_type; +-- label disjunction +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders | customer_wishlists ]->(l IS orders | wishlists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name)) ORDER BY customer_name, product_name; +-- property not associated with labels queried results in error +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders | customer_wishlists ]->(l IS orders | wishlists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name, l.list_type)) ORDER BY 1, 2, 3; +-- vertex to vertex connection abbreviation +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers)->(o IS orders) COLUMNS (c.name, o.ordered_when)) ORDER BY 1; + +-- lateral test +-- Use table with a column name same as a property in the property graph so as +-- to test resolution preferences. Property references are preferred over +-- lateral table references. +CREATE TABLE x1 (a int, address text); +INSERT INTO x1 VALUES (1, 'one'), (2, 'two'); +SELECT * FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid)); +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers WHERE x1.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; +-- non-local property references are not allowed, even if a lateral column +-- reference is available +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (x1 IS customers)-[IS customer_orders]->(o IS orders WHERE o.order_id = x1.a) COLUMNS (x1.name AS customer_name, x1.customer_id AS cid, o.order_id)) g; -- error +SELECT x1.a, g.* FROM x1, GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US' AND c.customer_id = x1.a)-[IS customer_orders]->(x1 IS orders) COLUMNS (c.name AS customer_name, c.customer_id AS cid, x1.order_id)) g; -- error +DROP TABLE x1; + +CREATE TABLE v1 ( + id int PRIMARY KEY, + vname varchar(10), + vprop1 int, + vprop2 int +); + +CREATE TABLE v2 ( + id1 int, + id2 int, + vname varchar(10), + vprop1 int, + vprop2 int +); + +CREATE TABLE v3 ( + id int PRIMARY KEY, + vname varchar(10), + vprop1 int, + vprop2 int +); + +-- edge connecting v1 and v2 +CREATE TABLE e1_2 ( + id_1 int, + id_2_1 int, + id_2_2 int, + ename varchar(10), + eprop1 int +); + +-- edge connecting v1 and v3 +CREATE TABLE e1_3 ( + id_1 int, + id_3 int, + ename varchar(10), + eprop1 int, + PRIMARY KEY (id_1, id_3) +); + +CREATE TABLE e2_3 ( + id_2_1 int, + id_2_2 int, + id_3 int, + ename varchar(10), + eprop1 int +); + +CREATE PROPERTY GRAPH g1 + VERTEX TABLES ( + v1 + LABEL vl1 PROPERTIES (vname, vprop1) + LABEL l1 PROPERTIES (vname AS elname), -- label shared by vertexes as well as edges + v2 KEY (id1, id2) + LABEL vl2 PROPERTIES (vname, vprop2, 'vl2_prop'::varchar(10) AS lprop1) + LABEL vl3 PROPERTIES (vname, vprop1, 'vl2_prop'::varchar(10) AS lprop1) + LABEL l1 PROPERTIES (vname AS elname), + v3 + LABEL vl3 PROPERTIES (vname, vprop1, 'vl3_prop'::varchar(10) AS lprop1) + LABEL l1 PROPERTIES (vname AS elname) + ) + -- edges with differing number of columns in destination keys + EDGE TABLES ( + e1_2 key (id_1, id_2_1, id_2_2) + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL el1 PROPERTIES (eprop1, ename) + LABEL l1 PROPERTIES (ename AS elname), + e1_3 + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_3) REFERENCES v3 (id) + -- order of property names doesn't matter + LABEL el1 PROPERTIES (ename, eprop1) + LABEL l1 PROPERTIES (ename AS elname), + e2_3 key (id_2_1, id_2_2, id_3) + SOURCE KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + DESTINATION KEY (id_3) REFERENCES v3 (id) + -- new property lprop2 not shared by el1 + -- does not share eprop1 from by el1 + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); + +INSERT INTO v1 VALUES + (1, 'v11', 10, 100), + (2, 'v12', 20, 200), + (3, 'v13', 30, 300); +INSERT INTO v2 VALUES + (1000, 1, 'v21', 1010, 1100), + (1000, 2, 'v22', 1020, 1200), + (1000, 3, 'v23', 1030, 1300); +INSERT INTO v3 VALUES + (2001, 'v31', 2010, 2100), + (2002, 'v32', 2020, 2200), + (2003, 'v33', 2030, 2300); +INSERT INTO e1_2 VALUES + (1, 1000, 2, 'e121', 10001), + (2, 1000, 1, 'e122', 10002); +INSERT INTO e1_3 VALUES + (1, 2003, 'e131', 10003), + (1, 2001, 'e132', 10004); +INSERT INTO e2_3 VALUES (1000, 2, 2002, 'e231', 10005); + +-- empty element path pattern, counts number of edges in the graph +SELECT count(*) FROM GRAPH_TABLE (g1 MATCH ()-[]->() COLUMNS (1 AS one)); +SELECT count(*) FROM GRAPH_TABLE (g1 MATCH ()->() COLUMNS (1 AS one)); +-- Project property associated with a label specified in the graph pattern even +-- if it is defined for a graph element through a different label. (Refer +-- section 6.5 of SQL/PGQ standard). For example, vprop1 in the query below. It +-- is defined on v2 through label vl3, but gets exposed in the query through +-- label vl1 which is not associated with v2. v2, in turn, is included because +-- of label vl2. +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1 | vl2) COLUMNS (a.vname, a.vprop1)); +-- vprop2 is associated with vl2 but not vl3 +SELECT src, conn, dest, lprop1, vprop2, vprop1 FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[b IS el1]->(c IS vl2 | vl3) COLUMNS (a.vname AS src, b.ename AS conn, c.vname AS dest, c.lprop1, c.vprop2, c.vprop1)); +-- edges directed in both ways - to and from v2 +SELECT * FROM GRAPH_TABLE (g1 MATCH (v1 IS vl2)-[conn]-(v2) COLUMNS (v1.vname AS v1name, conn.ename AS cname, v2.vname AS v2name)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (v1 IS vl2)-(v2) COLUMNS (v1.vname AS v1name, v2.vname AS v2name)); + +-- Errors +-- vl1 is not associated with property vprop2 +SELECT src, src_vprop2, conn, dest FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[b IS el1]->(c IS vl2 | vl3) COLUMNS (a.vname AS src, a.vprop2 AS src_vprop2, b.ename AS conn, c.vname AS dest)); +-- property ename is associated with edge labels but not with a vertex label +SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svname, src.ename AS sename)); +-- vname is associated vertex labels but not with an edge label +SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (conn.vname AS cvname, conn.ename AS cename)); +-- el1 is associated with only edges, and cannot qualify a vertex +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS el1)-[conn]->(dest) COLUMNS (conn.ename AS cename)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS el1 | vl1)-[conn]->(dest) COLUMNS (conn.ename AS cename)); +-- star in COLUMNs is specified but not supported +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.*)); +-- star anywhere else is not allowed as a property reference +SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.* IS NOT NULL)-[IS customer_orders]->(o IS orders) COLUMNS (c.name)); +-- consecutive element patterns with same kind +SELECT * FROM GRAPH_TABLE (g1 MATCH ()() COLUMNS (1 as one)); +SELECT * FROM GRAPH_TABLE (g1 MATCH -> COLUMNS (1 AS one)); +SELECT * FROM GRAPH_TABLE (g1 MATCH ()-[]- COLUMNS (1 AS one)); +SELECT * FROM GRAPH_TABLE (g1 MATCH ()-> ->() COLUMNS (1 AS one)); +-- non-local element variable reference with element patterns without variable +-- names +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[WHERE a.vprop1 = 10]->(c) COLUMNS (a.vname AS aname, c.vname AS cname)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (WHERE b.eprop1 = 10001)-[b]->(c) COLUMNS (b.ename AS bname, c.vname AS cname)); + + +-- select all the properties across all the labels associated with a given type +-- of graph element +SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svname, conn.ename AS cename, dest.vname AS dvname, src.vprop1 AS svp1, src.vprop2 AS svp2, src.lprop1 AS slp1, dest.vprop1 AS dvp1, dest.vprop2 AS dvp2, dest.lprop1 AS dlp1, conn.eprop1 AS cep1, conn.lprop2 AS clp2)); +-- three label disjunction +SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1 | vl2 | vl3)-[conn]->(dest) COLUMNS (src.vname AS svname, conn.ename AS cename, dest.vname AS dvname)); +-- graph'ical query: find a vertex which is not connected to any other vertex as a source or a destination. +WITH all_connected_vertices AS (SELECT svn, dvn FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svn, dest.vname AS dvn))), + all_vertices AS (SELECT vn FROM GRAPH_TABLE (g1 MATCH (vertex) COLUMNS (vertex.vname AS vn))) +SELECT vn FROM all_vertices EXCEPT (SELECT svn FROM all_connected_vertices UNION SELECT dvn FROM all_connected_vertices) ORDER BY vn; +-- query all connections using a label shared by vertices and edges +SELECT sn, cn, dn FROM GRAPH_TABLE (g1 MATCH (src IS l1)-[conn IS l1]->(dest IS l1) COLUMNS (src.elname AS sn, conn.elname AS cn, dest.elname AS dn)); + +-- Tests for cyclic path patterns +CREATE TABLE e2_1 ( + id_2_1 int, + id_2_2 int, + id_1 int, + ename varchar(10), + eprop1 int +); + +CREATE TABLE e3_2 ( + id_3 int, + id_2_1 int, + id_2_2 int, + ename varchar(10), + eprop1 int +); + +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e2_1 KEY (id_2_1, id_2_2, id_1) + SOURCE KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + DESTINATION KEY (id_1) REFERENCES v1 (id) + LABEL el1 PROPERTIES (eprop1, ename) + LABEL l1 PROPERTIES (ename AS elname), + e3_2 KEY (id_3, id_2_1, id_2_2) + SOURCE KEY (id_3) REFERENCES v3 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); + +INSERT INTO e1_2 VALUES (3, 1000, 3, 'e123', 10007); +INSERT INTO e2_1 VALUES (1000, 1, 2, 'e211', 10006); +INSERT INTO e2_1 VALUES (1000, 3, 3, 'e212', 10008); +INSERT INTO e3_2 VALUES (2002, 1000, 2, 'e321', 10009); + +-- cyclic pattern using WHERE clause in graph pattern, +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(c) WHERE a.vname = c.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; +-- cyclic pattern using element patterns with the same variable name +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(a) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; +-- cyclic pattern with WHERE clauses in element patterns with the same variable name +SELECT * FROM GRAPH_TABLE (g1 MATCH (a WHERE a.vprop1 < 2000)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 > 20) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 between 20 and 2000) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; +SELECT * FROM GRAPH_TABLE (g1 MATCH (a WHERE a.vprop1 between 20 and 2000)->(b WHERE b.vprop1 > 20)->(a WHERE a.vprop1 between 20 and 2000) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; +-- labels and elements kinds of element patterns with the same variable name +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS l1)-[a IS l1]->(b IS l1) COLUMNS (a.ename AS aename, b.ename AS bename)) ORDER BY 1, 2; -- error +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a IS vl2) WHERE a.vname <> b.vname COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; -- error +SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1)->(b)->(a) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)->(b)->(a IS vl1) COLUMNS (a.vname AS self, b.vname AS through, a.vprop1 AS self_p1, b.vprop1 AS through_p1)) ORDER BY self, through; + +-- add loop to test edge patterns with same variable name +CREATE TABLE e3_3 ( + src_id int, + dest_id int, + ename varchar(10), + eprop1 int +); + +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e3_3 KEY (src_id, dest_id) + SOURCE KEY (src_id) REFERENCES v3 (id) + DESTINATION KEY (dest_id) REFERENCES v3 (id) + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); + +INSERT INTO e3_3 VALUES (2003, 2003, 'e331', 10010); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(c)-[b]->(d) COLUMNS (a.vname AS aname, b.ename AS bname, c.vname AS cname, d.vname AS dname)); --error +-- the looping edge should be reported only once even when edge pattern with any direction is used +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[c]-(a) COLUMNS (a.vname AS self, c.ename AS loop_name)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-(a) COLUMNS (a.vname AS self)); + +-- test collation specified in the expression +INSERT INTO e3_3 VALUES (2003, 2003, 'E331', 10011); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)) ORDER BY loop_name COLLATE "C" ASC; +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b IS el2 WHERE b.ename > 'E331' COLLATE "C"]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) WHERE b.ename > 'E331' COLLATE "C" COLUMNS (a.vname AS self, b.ename AS loop_name)); +SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.vname AS self, b.ename AS loop_name)) WHERE loop_name > 'E331' COLLATE "C"; + +-- property graph with some of the elements, labels and properties same as the +-- previous one. Test whether components from the specified property graph are +-- used. Also test explicit collation specification in property. +CREATE PROPERTY GRAPH g2 + VERTEX TABLES ( + v1 + LABEL l1 PROPERTIES ('g2.' || vname COLLATE "C" AS elname), + v2 KEY (id1, id2) + LABEL l1 PROPERTIES ('g2.' || vname COLLATE "C" AS elname), + v3 + LABEL l1 PROPERTIES ('g2.' || vname COLLATE "C" AS elname) + ) + EDGE TABLES ( + e1_2 key (id_1, id_2_1, id_2_2) + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname), + e1_3 + SOURCE KEY (id_1) REFERENCES v1 (id) + DESTINATION KEY (id_3) REFERENCES v3 (id) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname), + e2_3 KEY (id_2_1, id_2_2, id_3) + SOURCE KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + DESTINATION KEY (id_3) REFERENCES v3 (id) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname), + e3_3 KEY (src_id, dest_id) + SOURCE KEY (src_id) REFERENCES v3 (id) + DESTINATION KEY (src_id) REFERENCES v3 (id) + LABEL l1 PROPERTIES ('g2.' || ename COLLATE "C" AS elname) + ); +SELECT sn, cn, dn FROM GRAPH_TABLE (g2 MATCH (src IS l1)-[conn IS l1]->(dest IS l1) COLUMNS (src.elname AS sn, conn.elname AS cn, dest.elname AS dn)) ORDER BY 1, 2, 3; +SELECT * FROM GRAPH_TABLE (g2 MATCH (a)-[b WHERE b.elname > 'g2.E331']->(a)-[b]->(a) COLUMNS (a.elname AS self, b.elname AS loop_name)); +SELECT * FROM GRAPH_TABLE (g2 MATCH (a)-[b]->(a)-[b]->(a) WHERE b.elname > 'g2.E331' COLUMNS (a.elname AS self, b.elname AS loop_name)); +SELECT * FROM GRAPH_TABLE (g2 MATCH (a)-[b]->(a)-[b]->(a) COLUMNS (a.elname AS self, b.elname AS loop_name)) WHERE loop_name > 'g2.E331'; + +-- prepared statements, any changes to the property graph should be reflected in +-- the already prepared statements +PREPARE cyclestmt AS SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS l1)->(b IS l1)->(c IS l1) WHERE a.elname = c.elname COLUMNS (a.elname AS self, b.elname AS through)) ORDER BY self, through; +EXECUTE cyclestmt; +ALTER PROPERTY GRAPH g1 DROP EDGE TABLES (e3_2, e3_3); +EXECUTE cyclestmt; +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e3_2 KEY (id_3, id_2_1, id_2_2) + SOURCE KEY (id_3) REFERENCES v3 (id) + DESTINATION KEY (id_2_1, id_2_2) REFERENCES v2 (id1, id2) + LABEL el2 PROPERTIES (ename, eprop1 * 10 AS lprop2) + LABEL l1 PROPERTIES (ename AS elname) + ); +EXECUTE cyclestmt; +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v3 DROP LABEL l1; +EXECUTE cyclestmt; +ALTER PROPERTY GRAPH g1 ALTER VERTEX TABLE v3 ADD LABEL l1 PROPERTIES (vname AS elname); +EXECUTE cyclestmt; +ALTER PROPERTY GRAPH g1 + ADD EDGE TABLES ( + e3_3 KEY (src_id, dest_id) + SOURCE KEY (src_id) REFERENCES v3 (id) + DESTINATION KEY (src_id) REFERENCES v3 (id) + LABEL l2 PROPERTIES (ename AS elname) + ); +PREPARE loopstmt AS SELECT * FROM GRAPH_TABLE (g1 MATCH (a)-[e IS l2]->(a) COLUMNS (e.elname AS loop)) ORDER BY loop COLLATE "C" ASC; +EXECUTE loopstmt; +ALTER PROPERTY GRAPH g1 ALTER EDGE TABLE e3_3 ALTER LABEL l2 DROP PROPERTIES (elname); +EXECUTE loopstmt; -- error +ALTER PROPERTY GRAPH g1 ALTER EDGE TABLE e3_3 ALTER LABEL l2 ADD PROPERTIES ((ename || '_new')::varchar(10) AS elname); +EXECUTE loopstmt; + +-- inheritance and partitioning +CREATE TABLE pv (id int, val int); +CREATE TABLE cv1 () INHERITS (pv); +CREATE TABLE cv2 () INHERITS (pv); +INSERT INTO pv VALUES (1, 10); +INSERT INTO cv1 VALUES (2, 20); +INSERT INTO cv2 VALUES (3, 30); +CREATE TABLE pe (id int, src int, dest int, val int); +CREATE TABLE ce1 () INHERITS (pe); +CREATE TABLE ce2 () INHERITS (pe); +INSERT INTO pe VALUES (1, 1, 2, 100); +INSERT INTO ce1 VALUES (2, 2, 3, 200); +INSERT INTO ce2 VALUES (3, 3, 1, 300); +CREATE PROPERTY GRAPH g3 + NODE TABLES ( + pv KEY (id) + ) + RELATIONSHIP TABLES ( + pe KEY (id) + SOURCE KEY(src) REFERENCES pv(id) + DESTINATION KEY(dest) REFERENCES pv(id) + ); +SELECT * FROM GRAPH_TABLE (g3 MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; +-- temporary property graph +CREATE TEMPORARY PROPERTY GRAPH gtmp + VERTEX TABLES ( + pv KEY (id) + ) + EDGE TABLES ( + pe KEY (id) + SOURCE KEY(src) REFERENCES pv(id) + DESTINATION KEY(dest) REFERENCES pv(id) + ); +SELECT * FROM GRAPH_TABLE (gtmp MATCH (s IS pv)-[e IS pe]->(d IS pv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + +CREATE TABLE ptnv (id int PRIMARY KEY, val int) PARTITION BY LIST(id); +CREATE TABLE prtv1 PARTITION OF ptnv FOR VALUES IN (1, 2); +CREATE TABLE prtv2 PARTITION OF ptnv FOR VALUES IN (3); +INSERT INTO ptnv VALUES (1, 10), (2, 20), (3, 30); +CREATE TABLE ptne (id int PRIMARY KEY, src int REFERENCES ptnv(id), dest int REFERENCES ptnv(id), val int) PARTITION BY LIST(id); +CREATE TABLE ptne1 PARTITION OF ptne FOR VALUES IN (1, 2); +CREATE TABLE ptne2 PARTITION OF ptne FOR VALUES IN (3); +INSERT INTO ptne VALUES (1, 1, 2, 100), (2, 2, 3, 200), (3, 3, 1, 300); +CREATE PROPERTY GRAPH g4 + VERTEX TABLES (ptnv) + EDGE TABLES ( + ptne + SOURCE KEY (src) REFERENCES ptnv(id) + DESTINATION KEY (dest) REFERENCES ptnv(id) + ); +SELECT * FROM GRAPH_TABLE (g4 MATCH (s IS ptnv)-[e IS ptne]->(d IS ptnv) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; +-- edges from the same vertex in both directions connecting to other vertexes in the same table +SELECT * FROM GRAPH_TABLE (g4 MATCH (s)-[e]-(d) WHERE s.id = 3 COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; +SELECT * FROM GRAPH_TABLE (g4 MATCH (s WHERE s.id = 3)-[e]-(d) COLUMNS (s.val, e.val, d.val)) ORDER BY 1, 2, 3; + +-- ruleutils reverse parsing +CREATE VIEW customers_us AS SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders | customer_wishlists ]->(l IS orders | wishlists)-[ IS list_items]->(p IS products) COLUMNS (c.name AS customer_name, p.name AS product_name)) ORDER BY customer_name, product_name; +SELECT pg_get_viewdef('customers_us'::regclass); + +-- test view/graph nesting + +CREATE VIEW customers_view AS SELECT customer_id, 'redacted' || customer_id AS name_redacted, address FROM customers; +SELECT * FROM customers; +SELECT * FROM customers_view; + +CREATE PROPERTY GRAPH myshop2 + VERTEX TABLES ( + products, + customers_view KEY (customer_id) LABEL customers, + orders + ) + EDGE TABLES ( + order_items KEY (order_items_id) + SOURCE KEY (order_id) REFERENCES orders (order_id) + DESTINATION KEY (product_no) REFERENCES products (product_no), + customer_orders KEY (customer_orders_id) + SOURCE KEY (customer_id) REFERENCES customers_view (customer_id) + DESTINATION KEY (order_id) REFERENCES orders (order_id) + ); + +CREATE VIEW customers_us_redacted AS SELECT * FROM GRAPH_TABLE (myshop2 MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name_redacted AS customer_name_redacted)); + +SELECT * FROM customers_us_redacted; + +-- GRAPH_TABLE in UDFs +CREATE FUNCTION out_degree(sname varchar) RETURNS varchar AS $$ +DECLARE + out_degree int; +BEGIN + SELECT count(*) INTO out_degree FROM GRAPH_TABLE (g1 MATCH (src WHERE src.vname = sname)->() COLUMNS (src.vname)); + RETURN out_degree; +END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION direct_connections(sname varchar) +RETURNS TABLE (cname varchar, dname varchar) +AS $$ + SELECT cname, dname FROM GRAPH_TABLE (g1 MATCH (src WHERE src.vname = sname)-[conn]->(dst) COLUMNS (conn.ename AS cname, dst.vname AS dname)); +$$ LANGUAGE SQL; + +SELECT sname, out_degree(sname) FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname AS sname)); +SELECT sname, cname, dname FROM GRAPH_TABLE (g1 MATCH (src IS vl1) COLUMNS (src.vname AS sname)), LATERAL direct_connections(sname); + +-- GRAPH_TABLE joined to a regular table +SELECT * FROM customers co, GRAPH_TABLE (myshop2 MATCH (cg IS customers WHERE cg.address = co.address)-[IS customer_orders]->(o IS orders) COLUMNS (cg.name_redacted AS customer_name_redacted)) WHERE co.customer_id = 1; + +-- graph table in a subquery +SELECT * FROM customers co WHERE co.customer_id = (SELECT customer_id FROM GRAPH_TABLE (myshop2 MATCH (cg IS customers WHERE cg.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (cg.customer_id))); + +-- query within graph table +SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE src.vprop1 > (SELECT max(v1.vprop1) FROM v1) COLUMNS(src.vname AS sname, dest.vname AS dname)); +SELECT sname, dname FROM GRAPH_TABLE (g1 MATCH (src)->(dest) WHERE out_degree(src.vname) > (SELECT max(out_degree(nname)) FROM GRAPH_TABLE (g1 MATCH (node) COLUMNS (node.vname AS nname))) COLUMNS(src.vname AS sname, dest.vname AS dname)); + +-- leave the objects behind for pg_upgrade/pg_dump tests diff --git a/src/test/regress/sql/graph_table_rls.sql b/src/test/regress/sql/graph_table_rls.sql new file mode 100644 index 0000000000000..5837eac402e6e --- /dev/null +++ b/src/test/regress/sql/graph_table_rls.sql @@ -0,0 +1,378 @@ +-- +--Test RLS with GRAPH_TABLE +-- +--This test verifies that Row Level Security (RLS) policies are correctly +--enforced when querying tables underlying property graphs using GRAPH_TABLE. +--graph_table.sql has extensive tests covering interaction of GRAPH_TABLE with +--other query constructs. rowsecurity.sql has extensive coverage of interaction +--of RLS and other features of PostgreSQL. This test along with those two tests +--is sufficient to make sure that all combinations of RLS and GRAPH_TABLE will +--work as expected. + +-- Clean up in case a prior regression run failed + +-- Suppress NOTICE messages when users/groups don't exist +SET client_min_messages TO 'warning'; + +DROP USER IF EXISTS regress_graph_rls_alice; +DROP USER IF EXISTS regress_graph_rls_bob; +DROP USER IF EXISTS regress_graph_rls_carol; +DROP USER IF EXISTS regress_graph_rls_dave; +DROP USER IF EXISTS regress_graph_rls_exempt_user; +DROP ROLE IF EXISTS regress_graph_rls_group1; +DROP ROLE IF EXISTS regress_graph_rls_group2; + +DROP SCHEMA IF EXISTS graph_rls_schema CASCADE; + +RESET client_min_messages; + +-- initial setup +CREATE USER regress_graph_rls_alice NOLOGIN; +CREATE USER regress_graph_rls_bob NOLOGIN; +CREATE USER regress_graph_rls_carol NOLOGIN; +CREATE USER regress_graph_rls_dave NOLOGIN; +CREATE USER regress_graph_rls_exempt_user BYPASSRLS NOLOGIN; +CREATE ROLE regress_graph_rls_group1 NOLOGIN; +CREATE ROLE regress_graph_rls_group2 NOLOGIN; + +GRANT regress_graph_rls_group1 TO regress_graph_rls_dave; +GRANT regress_graph_rls_group2 TO regress_graph_rls_bob; + +CREATE SCHEMA graph_rls_schema; +GRANT ALL ON SCHEMA graph_rls_schema to public; +SET search_path = graph_rls_schema; + +-- setup for leaky-function tests +CREATE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; + +SET SESSION AUTHORIZATION regress_graph_rls_alice; + +CREATE TABLE users (uid int PRIMARY KEY, pguser name, seclv int); +INSERT INTO users VALUES + (1, 'regress_graph_rls_alice', 99), + (2, 'regress_graph_rls_bob', 1), + (3, 'regress_graph_rls_carol', 2), + (4, 'regress_graph_rls_dave', 3); +GRANT SELECT ON users TO public; + +CREATE TABLE document_people ( + did int, + dlevel int, + dtitle text); +INSERT INTO document_people VALUES + ( 1, 2, 'Politicians'), + ( 2, 3, 'Artists'), + ( 3, 1, 'Scientists'), + ( 4, 100, 'Unspeakables'); +GRANT SELECT ON document_people TO public; + +CREATE TABLE accessed ( + aid int, + uid int, + did int); +INSERT INTO accessed VALUES + (1, 2, 3), + (2, 3, 1), + (3, 3, 3), + (4, 4, 3), + (5, 1, 1), + (6, 1, 4), + (7, 4, 2); +GRANT SELECT ON accessed TO public; +CREATE PROPERTY GRAPH cabinet + VERTEX TABLES (users KEY (uid), + document_people AS document KEY (did)) + EDGE TABLES (accessed KEY (aid) + SOURCE KEY (uid) REFERENCES users (uid) + DESTINATION KEY (did) REFERENCES document (did)); +GRANT SELECT ON cabinet TO public; + +-- +-- Basic RLS tests +-- +ALTER TABLE document_people ENABLE ROW LEVEL SECURITY; +-- user's security level must be higher than or equal to document's +CREATE POLICY p1 ON document_people AS PERMISSIVE + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +-- but Dave isn't allowed to see document titled 'Scientists' +CREATE POLICY p2 ON document_people AS RESTRICTIVE TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); +CREATE POLICY p3 ON document_people AS RESTRICTIVE TO regress_graph_rls_group1 + USING (dlevel < 3); +CREATE POLICY p4 ON document_people AS PERMISSIVE TO regress_graph_rls_group2 + USING (dlevel < 3); + +SET row_security TO ON; + +-- Use the same query in all the test cases below. Prepare it once and +-- use multiple times. Apart from making the test file shorter and avoiding +-- duplication, it also tests that a prepared statement correctly reflect changes +-- to RLS policies, session user or RLS settings. +PREPARE graph_rls_query AS +SELECT * FROM GRAPH_TABLE (cabinet + MATCH (u IS users)-[a IS accessed]->(d IS document) + WHERE f_leak(d.dtitle) + COLUMNS (u.pguser, a.aid, d.dtitle, d.dlevel)) + ORDER BY 1, 2, 3, 4; + +-- viewpoint from regress_graph_rls_bob +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; + +-- viewpoint from regress_graph_rls_carol +SET SESSION AUTHORIZATION regress_graph_rls_carol; +EXECUTE graph_rls_query; + +-- viewpoint from regress_graph_rls_dave +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; + +-- RLS policy does not apply to table owner when RLS enabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +EXECUTE graph_rls_query; + +SET row_security TO OFF; +-- database superuser does bypass RLS policy when disabled +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; + +-- database non-superuser with bypass privilege can bypass RLS policy when disabled +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; + +-- RLS policy does not apply to table owner when RLS disabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; + +-- When RLS disabled, other users get ERROR. +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP PROPERTY GRAPH cabinet; + +-- +-- Table inheritance +-- +ALTER TABLE document_people ADD COLUMN category text DEFAULT 'People'; +CREATE TABLE document_places ( + did int, + dlevel int, + dtitle text, + category text DEFAULT 'Places'); +INSERT INTO document_places VALUES + ( 5, 1, 'Paris'), + ( 6, 2, 'Tokyo'), + ( 7, 3, 'New York'); +GRANT SELECT ON document_places TO public; +-- Setup inheritance +CREATE TABLE document ( + did int, + dlevel int, + dtitle text); +GRANT SELECT ON document TO public; +ALTER TABLE document_people INHERIT document; +ALTER TABLE document_places INHERIT document; +INSERT INTO accessed VALUES + (11, 2, 5), + (12, 3, 6), + (13, 1, 7), + (14, 4, 5), + (15, 1, 6); + +-- Enable RLS and move policies p1 and p2 to parent table but leave p3 and p4 on +-- child table. The policies on child table are not applied when querying parent +-- table. +ALTER TABLE document ENABLE ROW LEVEL SECURITY; +DROP POLICY p1 ON document_people; +DROP POLICY p2 ON document_people; +CREATE POLICY p1 ON document AS PERMISSIVE + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +CREATE POLICY p2 ON document AS RESTRICTIVE TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); + +CREATE PROPERTY GRAPH cabinet + VERTEX TABLES (users KEY (uid), document KEY (did)) + EDGE TABLES (accessed KEY (aid) + SOURCE KEY (uid) REFERENCES users (uid) + DESTINATION KEY (did) REFERENCES document (did)); +GRANT SELECT ON cabinet TO public; + +SET row_security TO ON; + +-- viewpoint from regress_graph_rls_bob +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; + +-- viewpoint from regress_graph_rls_carol +SET SESSION AUTHORIZATION regress_graph_rls_carol; +EXECUTE graph_rls_query; + +-- viewpoint from regress_graph_rls_dave +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; + +-- RLS policy does not apply to table owner when RLS enabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +EXECUTE graph_rls_query; + +SET row_security TO OFF; +-- database superuser does bypass RLS policy when disabled +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; + +-- database non-superuser with bypass privilege can bypass RLS policy when disabled +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; + +-- RLS policy does not apply to table owner when RLS disabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; + +-- When RLS disabled, other users get ERROR. +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error + +-- cleanup +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP PROPERTY GRAPH cabinet; +ALTER TABLE document_people NO INHERIT document; +ALTER TABLE document_places NO INHERIT document; +DROP TABLE document; + +-- +-- Partitioned Tables +-- +CREATE TABLE document ( + did int, + dlevel int, + dtitle text, + category text) PARTITION BY LIST (category); +GRANT SELECT ON document TO public; +ALTER TABLE document ATTACH PARTITION document_people FOR VALUES IN ('People'); +ALTER TABLE document ATTACH PARTITION document_places FOR VALUES IN ('Places'); +-- Enable RLS on partitioned table +ALTER TABLE document ENABLE ROW LEVEL SECURITY; +-- create policies on partitioned table +CREATE POLICY p1 ON document AS PERMISSIVE + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +CREATE POLICY p2 ON document AS RESTRICTIVE TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); + +CREATE PROPERTY GRAPH cabinet + VERTEX TABLES (users KEY (uid), document KEY (did)) + EDGE TABLES (accessed KEY (aid) + SOURCE KEY (uid) REFERENCES users (uid) + DESTINATION KEY (did) REFERENCES document (did)); +GRANT SELECT ON cabinet TO public; +SET row_security TO ON; + +-- viewpoint from regress_graph_rls_bob +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; + +-- viewpoint from regress_graph_rls_carol +SET SESSION AUTHORIZATION regress_graph_rls_carol; +EXECUTE graph_rls_query; + +-- viewpoint from regress_graph_rls_dave +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; + +-- RLS policy does not apply to table owner when RLS enabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +EXECUTE graph_rls_query; + +SET row_security TO OFF; +-- database superuser does bypass RLS policy when disabled +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; + +-- database non-superuser with bypass privilege can bypass RLS policy when disabled +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; + +-- RLS policy does not apply to table owner when RLS disabled +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; + +-- When RLS disabled, other users get ERROR. +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error + +-- +-- Recursion through GRAPH_TABLE also throws error +-- +SET SESSION AUTHORIZATION regress_graph_rls_alice; +SET row_security TO ON; +-- Create a policy on document that references document itself via GRAPH_TABLE +CREATE POLICY pr ON document TO regress_graph_rls_dave + USING (EXISTS (SELECT 1 FROM GRAPH_TABLE (cabinet + MATCH (u IS users)-[a IS accessed]->(d IS document) + WHERE u.pguser = current_user + COLUMNS (a.aid)))); +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; -- error +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP POLICY pr ON document; + +-- +-- Command specific policy. Since GRAPH_TABLE can be used in only SELECT, test +-- only FOR SELECT policies. +-- +DROP POLICY p1 ON document; +DROP POLICY p2 ON document; +CREATE POLICY p1 ON document AS PERMISSIVE + FOR SELECT + USING (dlevel <= (SELECT seclv FROM users WHERE pguser = current_user)); +CREATE POLICY p2 ON document AS RESTRICTIVE + FOR SELECT TO regress_graph_rls_dave + USING (dtitle <> 'Scientists'); +SET SESSION AUTHORIZATION regress_graph_rls_dave; +EXECUTE graph_rls_query; + +-- +-- Default deny policy, FORCE ROW LEVEL SECURITY +-- +SET SESSION AUTHORIZATION regress_graph_rls_alice; +DROP POLICY p1 ON document; +DROP POLICY p2 ON document; +-- default deny policy applies to non-owners, non-rls-exempt and non-superusers +SET SESSION AUTHORIZATION regress_graph_rls_bob; +EXECUTE graph_rls_query; +-- Deny RLS policy does not apply to table owner, superuser or RLS exempt user +SET SESSION AUTHORIZATION regress_graph_rls_exempt_user; +EXECUTE graph_rls_query; +RESET SESSION AUTHORIZATION; +EXECUTE graph_rls_query; +SET SESSION AUTHORIZATION regress_graph_rls_alice; +EXECUTE graph_rls_query; +-- FORCE ROW LEVEL SECURITY applies RLS to owners too +ALTER TABLE document FORCE ROW LEVEL SECURITY; +EXECUTE graph_rls_query; +SET row_security TO OFF; +EXECUTE graph_rls_query; -- error + +-- Clean up +DEALLOCATE graph_rls_query; + +-- leave as many objects behind for pg_upgrade/pg_dump tests as possible. The +-- pg_dump test only dumps the regression database, not the global objects like +-- users and roles. Reassign ownership of all objects to superuser and drop +-- users and roles created in this test. Policies can not be reassigned, so drop +-- them explicitly. +RESET SESSION AUTHORIZATION; +REASSIGN OWNED BY regress_graph_rls_alice TO current_user; +DROP USER regress_graph_rls_alice; +DROP USER regress_graph_rls_bob; +DROP USER regress_graph_rls_carol; +DROP USER regress_graph_rls_dave; +DROP USER regress_graph_rls_exempt_user; +DROP POLICY p3 ON document_people; +DROP POLICY p4 ON document_people; +DROP ROLE regress_graph_rls_group1; +DROP ROLE regress_graph_rls_group2; diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql index 38d3cdd0fd89a..a594449b69786 100644 --- a/src/test/regress/sql/groupingsets.sql +++ b/src/test/regress/sql/groupingsets.sql @@ -136,6 +136,19 @@ select a, b, grouping(a, b), sum(t1.v), max(t2.c) from gstest1 t1 join gstest2 t2 using (a,b) group by grouping sets ((a, b), ()); +select a, b, grouping(a, b), sum(t1.v), max(t2.c) + from gstest1 t1 full join gstest2 t2 using (a,b) + group by grouping sets ((a, b), ()); + +-- references in subqueries should work too +select (select a), + (select b), + (select grouping(a, b)), + (select sum(t1.v)), + (select max(t2.c)) + from gstest1 t1 full join gstest2 t2 using (a,b) + group by grouping sets ((a, b), ()); + -- check that functionally dependent cols are not nulled select a, d, grouping(a,b,c) from gstest3 @@ -183,6 +196,52 @@ select x, y || 'y' group by grouping sets (x, y) order by 1, 2; +-- check that operands wrapped in PlaceHolderVars are capable of index matching +begin; + +set local enable_bitmapscan = off; + +explain (costs off) +select x, y + from (select unique1 as x, unique2 as y from tenk1) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +select x, y + from (select unique1 as x, unique2 as y from tenk1) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +explain (costs off) +select x, y + from (select unique1::oid as x, unique2 as y from tenk1) as t + where x::integer = 1 + group by grouping sets (x, y) + order by 1, 2; + +select x, y + from (select unique1::oid as x, unique2 as y from tenk1) as t + where x::integer = 1 + group by grouping sets (x, y) + order by 1, 2; + +explain (costs off) +select x, y + from (select t1.unique1 as x, t1.unique2 as y from tenk1 t1, tenk1 t2) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +select x, y + from (select t1.unique1 as x, t1.unique2 as y from tenk1 t1, tenk1 t2) as t + where x = 1 + group by grouping sets (x, y) + order by 1, 2; + +rollback; + -- check qual push-down rules for a subquery with grouping sets explain (verbose, costs off) select * from ( @@ -290,11 +349,29 @@ explain (costs off) select v.c, (select count(*) from gstest2 group by () having v.c) from (values (false),(true)) v(c) order by v.c; --- test pushdown of HAVING clause that does not reference any columns that are nullable by grouping sets +-- test pushdown of non-degenerate HAVING clause that does not reference any +-- columns that are nullable by grouping sets explain (costs off) select a, b, count(*) from gstest2 group by grouping sets ((a, b), (a)) having a > 1 and b > 1; select a, b, count(*) from gstest2 group by grouping sets ((a, b), (a)) having a > 1 and b > 1; +explain (costs off) +select a, b, count(*) from gstest2 group by rollup(a), b having b > 1; +select a, b, count(*) from gstest2 group by rollup(a), b having b > 1; + +-- test pushdown of degenerate HAVING clause +explain (costs off) +select count(*) from gstest2 group by grouping sets (()) having false; +select count(*) from gstest2 group by grouping sets (()) having false; + +explain (costs off) +select a, count(*) from gstest2 group by grouping sets ((a), ()) having false; +select a, count(*) from gstest2 group by grouping sets ((a), ()) having false; + +explain (costs off) +select a, b, count(*) from gstest2 group by grouping sets ((a), (b)) having false; +select a, b, count(*) from gstest2 group by grouping sets ((a), (b)) having false; + -- HAVING with GROUPING queries select ten, grouping(ten) from onek group by grouping sets(ten) having grouping(ten) >= 0 @@ -703,4 +780,27 @@ select a, b, row_number() over (order by a, b nulls first) from (values (1, 1), (2, 2)) as t (a, b) where a = b group by grouping sets((a, b), (a)); +-- test handling of SRFs with grouping sets +explain (verbose, costs off) +select generate_series(1, a) as g +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(g) +order by 1; + +select generate_series(1, a) as g +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(g) +order by 1; + +explain (verbose, costs off) +select generate_series(1, a) as g, a+b as ab +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(a, ab) +order by 1, 2; + +select generate_series(1, a) as g, a+b as ab +from (values (1, 1), (2, 2)) as t (a, b) +group by rollup(a, ab) +order by 1, 2; + -- end diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql index f65f84a26320a..dfb843fd3aee6 100644 --- a/src/test/regress/sql/guc.sql +++ b/src/test/regress/sql/guc.sql @@ -12,6 +12,15 @@ SHOW vacuum_cost_delay; SHOW datestyle; SELECT '2006-08-13 12:34:56'::timestamptz; +-- Check handling of list GUCs +SET search_path = 'pg_catalog', Foo, 'Bar', ''; +SHOW search_path; +SET search_path = null; -- means empty list +SHOW search_path; +SET search_path = null, null; -- syntax error +SET enable_seqscan = null; -- error +RESET search_path; + -- SET LOCAL has no effect outside of a transaction SET LOCAL vacuum_cost_delay TO 50; SHOW vacuum_cost_delay; @@ -223,6 +232,28 @@ drop schema not_there_initially; select current_schemas(false); reset search_path; +-- +-- Test parsing of log_min_messages +-- + +SET log_min_messages TO foo; -- fail +SET log_min_messages TO fatal; +SHOW log_min_messages; +SET log_min_messages TO 'fatal'; +SHOW log_min_messages; +SET log_min_messages TO 'checkpointer:debug2, autovacuum:debug1'; -- fail +SET log_min_messages TO 'debug1, backend:error, fatal'; -- fail +SET log_min_messages TO 'backend:error, debug1, backend:warning'; -- fail +SET log_min_messages TO 'backend:error, foo:fatal, archiver:debug1'; -- fail +SET log_min_messages TO 'backend:error, checkpointer:bar, archiver:debug1'; -- fail +SET log_min_messages TO 'backend:error, checkpointer:debug3, fatal, archiver:debug2, autovacuum:debug1, walsender:debug3'; +SHOW log_min_messages; +SET log_min_messages TO 'warning, autovacuum:debug1'; +SHOW log_min_messages; +SET log_min_messages TO 'autovacuum:debug1, warning'; +SHOW log_min_messages; +RESET log_min_messages; + -- -- Tests for function-local GUC settings -- diff --git a/src/test/regress/sql/hash_func.sql b/src/test/regress/sql/hash_func.sql index 33756bd288ff3..ce38da72c0bdd 100644 --- a/src/test/regress/sql/hash_func.sql +++ b/src/test/regress/sql/hash_func.sql @@ -48,6 +48,13 @@ FROM (VALUES (0), (1), (17), (42), (550273), (207112489)) x(v) WHERE hashoid(v)::bit(32) != hashoidextended(v, 0)::bit(32) OR hashoid(v)::bit(32) = hashoidextended(v, 1)::bit(32); +SELECT v as value, hashoid8(v)::bit(32) as standard, + hashoid8extended(v, 0)::bit(32) as extended0, + hashoid8extended(v, 1)::bit(32) as extended1 +FROM (VALUES (0), (1), (17), (42), (550273), (207112489)) x(v) +WHERE hashoid8(v)::bit(32) != hashoid8extended(v, 0)::bit(32) + OR hashoid8(v)::bit(32) = hashoid8extended(v, 1)::bit(32); + SELECT v as value, hashchar(v)::bit(32) as standard, hashcharextended(v, 0)::bit(32) as extended0, hashcharextended(v, 1)::bit(32) as extended1 diff --git a/src/test/regress/sql/hash_index.sql b/src/test/regress/sql/hash_index.sql index 219da829816d5..3ebdaeaf17289 100644 --- a/src/test/regress/sql/hash_index.sql +++ b/src/test/regress/sql/hash_index.sql @@ -53,6 +53,9 @@ CREATE INDEX hash_txt_index ON hash_txt_heap USING hash (random text_ops); CREATE INDEX hash_f8_index ON hash_f8_heap USING hash (random float8_ops) WITH (fillfactor=60); +CREATE INDEX hash_i4_partial_index ON hash_i4_heap USING hash (seqno) + WHERE seqno = 9999; + -- -- Also try building functional, expressional, and partial indexes on -- tables that already contain data. @@ -117,6 +120,16 @@ SELECT * FROM hash_f8_heap SELECT * FROM hash_f8_heap WHERE hash_f8_heap.random = '88888888'::float8; +-- +-- partial hash index +-- +EXPLAIN (COSTS OFF) +SELECT * FROM hash_i4_heap + WHERE seqno = 9999; + +SELECT * FROM hash_i4_heap + WHERE seqno = 9999; + -- -- hash index -- grep '^90[^0-9]' hashovfl.data @@ -301,6 +314,23 @@ INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 50) as i; CHECKPOINT; VACUUM hash_cleanup_heap; +-- Test cleanup of dead index tuples on single page with INSERT. +TRUNCATE hash_cleanup_heap; +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 1000) as i; +-- This relies on a rollbacked INSERT instead of a DELETE to make the creation +-- of the dead tuples concurrent-safe. +BEGIN; +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 500) as i; +ROLLBACK; +SET enable_seqscan = off; +SET enable_bitmapscan = off; +SELECT count(*) FROM hash_cleanup_heap WHERE keycol = 1; +-- This query checks the hash index pages for dead tuples where the data +-- is inserted, and performs a local VACUUM on a single page. +INSERT INTO hash_cleanup_heap SELECT 1 FROM generate_series(1, 200) as i; +RESET enable_seqscan; +RESET enable_bitmapscan; + -- Clean up. DROP TABLE hash_cleanup_heap; diff --git a/src/test/regress/sql/horology.sql b/src/test/regress/sql/horology.sql index 1310b43277380..8978249a5dc1e 100644 --- a/src/test/regress/sql/horology.sql +++ b/src/test/regress/sql/horology.sql @@ -102,6 +102,10 @@ SELECT date 'J J 1520447'; SELECT timestamp with time zone 'Y2001M12D27H04M05S06.789+08'; SELECT timestamp with time zone 'Y2001M12D27H04MM05S06.789-08'; +-- More examples we used to accept and should not +SELECT timestamp with time zone 'J2452271 T X03456-08'; +SELECT timestamp with time zone 'J2452271 T X03456.001e6-08'; + -- conflicting fields should throw errors SELECT date '1995-08-06 epoch'; SELECT date '1995-08-06 infinity'; diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sql index f1f8fae56549a..bbe658a7588c9 100644 --- a/src/test/regress/sql/incremental_sort.sql +++ b/src/test/regress/sql/incremental_sort.sql @@ -298,3 +298,27 @@ explain (costs off) select * from (select * from tenk1 order by four) t1 join tenk1 t2 on t1.four = t2.four and t1.two = t2.two order by t1.four, t1.two limit 1; + +-- +-- Test incremental sort for Append/MergeAppend +-- +create table prt_tbl (a int, b int) partition by range (a); +create table prt_tbl_1 partition of prt_tbl for values from (0) to (100); +create table prt_tbl_2 partition of prt_tbl for values from (100) to (200); +insert into prt_tbl select i%200, i from generate_series(1,1000)i; +create index on prt_tbl_1(a); +create index on prt_tbl_2(a, b); +analyze prt_tbl; + +set enable_seqscan to off; +set enable_bitmapscan to off; + +-- Ensure we get an incremental sort for the subpath of Append +explain (costs off) select * from prt_tbl order by a, b; + +-- Ensure we get an incremental sort for the subpath of MergeAppend +explain (costs off) select * from prt_tbl_1 union all select * from prt_tbl_2 order by a, b; + +reset enable_bitmapscan; +reset enable_seqscan; +drop table prt_tbl; diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql index b5cb01c2d7097..3d43af3323cf7 100644 --- a/src/test/regress/sql/indexing.sql +++ b/src/test/regress/sql/indexing.sql @@ -246,6 +246,65 @@ select relname, indisvalid from pg_class join pg_index on indexrelid = oid where relname like 'idxpart%' order by relname; drop table idxpart; +-- Verify that re-attaching an already-attached partition index can +-- validate the parent index if it was still invalid, including +-- indirect ancestors in subpartitions. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (1000) partition by range (a); +create table idxpart11 partition of idxpart1 for values from (0) to (500); +-- Partitioned table with no partitions +create table idxpart2 partition of idxpart for values from (1000) to (2000) partition by range (a); +-- create parent indexes +create index on only idxpart ((a/b)); +create index on only idxpart1 ((a/b)); +create index on only idxpart2 ((a/b)); +-- fail, leaves behind an invalid index on the leaf partition +insert into idxpart11 values (1, 0); +create index concurrently on idxpart11 ((a/b)); +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- attach the indexes; parents stay invalid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- fix the index on the leaf partition +delete from idxpart11 where b = 0; +reindex index concurrently idxpart11_expr_idx; +-- reattach the leaf partition index; parents should now be valid +alter index idxpart1_expr_idx attach partition idxpart11_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +drop table idxpart; + +-- Verify that re-attaching does not validate the parent when another +-- child index is still invalid. +create table idxpart (a int, b int) partition by range (a); +create table idxpart1 partition of idxpart for values from (0) to (500); +create table idxpart2 partition of idxpart for values from (500) to (1000); +create index on only idxpart ((a/b)); +-- create invalid indexes on both children +insert into idxpart1 values (1, 0); +insert into idxpart2 values (501, 0); +create index concurrently on idxpart1 ((a/b)); +create index concurrently on idxpart2 ((a/b)); +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- attach both; parent stays invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +alter index idxpart_expr_idx attach partition idxpart2_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +-- fix only idxpart1's index, leave idxpart2's still invalid +delete from idxpart1 where b = 0; +reindex index concurrently idxpart1_expr_idx; +-- re-attach the fixed child; parent should stay invalid +alter index idxpart_expr_idx attach partition idxpart1_expr_idx; +select relname, indisvalid from pg_class join pg_index on indexrelid = oid + where relname like 'idxpart%' order by relname; +drop table idxpart; + -- verify dependency handling during ALTER TABLE DETACH PARTITION create table idxpart (a int) partition by range (a); create table idxpart1 (like idxpart); @@ -934,3 +993,15 @@ reindex index test_pg_index_toast_index; drop index test_pg_index_toast_index; drop function test_pg_index_toast_func; drop table test_pg_index_toast_table; + +-- test creation of an index involving a whole-row expression +create table test_pg_wholerow_index (a int, b text, c numeric); +create or replace function row_image(test_pg_wholerow_index) + returns test_pg_wholerow_index as $$select $1$$ language sql immutable; +insert into test_pg_wholerow_index values (1, 'multiplication', 1.0); +create index row_image_index + on test_pg_wholerow_index ((row_image(test_pg_wholerow_index))); +insert into test_pg_wholerow_index values (2, 'addition', 0); +drop index row_image_index; +drop function row_image(test_pg_wholerow_index); +drop table test_pg_wholerow_index; diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 699e8ac09c88e..8f986904389c5 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -510,6 +510,38 @@ select conrelid::regclass::text as relname, conname, conislocal, coninhcount, co from pg_constraint where conname like 'inh\_check\_constraint%' order by 1, 2; +-- +-- CHECK constraints +-- ALTER TABLE ALTER CONSTRAINT [NOT] ENFORCED +alter table p1 drop constraint inh_check_constraint1; +alter table p1_c1 drop constraint inh_check_constraint1; + +alter table only p1 alter constraint inh_check_constraint3 enforced; --error +alter table only p1 alter constraint inh_check_constraint3 not enforced; --error + +insert into p1_c1 values(-2); +insert into p1_c3 values(-3); + +alter table p1 alter constraint inh_check_constraint3 enforced; --error +delete from only p1_c1 where f1 = -2; +alter table p1_c1 alter constraint inh_check_constraint3 enforced; --error + +delete from only p1_c3 where f1 = -3; +alter table p1 alter constraint inh_check_constraint3 enforced; --ok +alter table p1 alter constraint inh_check_constraint3 not enforced; --ok +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'inh_check_constraint3' and contype = 'c' +order by conrelid::regclass::text collate "C"; +drop table p1 cascade; + +--for "no inherit" check constraint, it will not recurse to child table +create table p1(f1 int constraint p1_a_check check (f1 > 0) no inherit not enforced); +create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) not enforced); +alter table p1_c1 inherit p1; +insert into p1_c1 values(-11); +alter table p1 alter constraint p1_a_check enforced; --ok +alter table p1_c1 alter constraint p1_a_check enforced; --error drop table p1 cascade; -- @@ -520,6 +552,17 @@ drop table p1 cascade; create table p1(f1 int constraint p1_a_check check (f1 > 0) not enforced); create table p1_c1(f1 int constraint p1_a_check check (f1 > 0) enforced); alter table p1_c1 inherit p1; +insert into p1 values(-1); --ok +insert into p1_c1 values(-1); --error +alter table p1 alter constraint p1_a_check enforced; --error +truncate p1; +alter table p1 alter constraint p1_a_check enforced; --ok +alter table p1 alter constraint p1_a_check not enforced; --ok + +select conname, conenforced, convalidated, conrelid::regclass +from pg_constraint +where conname = 'p1_a_check' and contype = 'c' +order by conrelid::regclass::text collate "C"; drop table p1 cascade; create table p1(f1 int constraint p1_a_check check (f1 > 0) enforced); diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql index 549c46452ec09..a5a84d1d4b8d3 100644 --- a/src/test/regress/sql/insert_conflict.sql +++ b/src/test/regress/sql/insert_conflict.sql @@ -3,6 +3,11 @@ -- create table insertconflicttest(key int4, fruit text); +-- invalid clauses +insert into insertconflicttest values (1) on conflict (key int4_ops (fillfactor=10)) do nothing; +insert into insertconflicttest values (1) on conflict (key asc) do nothing; +insert into insertconflicttest values (1) on conflict (key nulls last) do nothing; + -- These things should work through a view, as well create view insertconflictview as select * from insertconflicttest; @@ -88,6 +93,9 @@ explain (costs off) insert into insertconflicttest values (0, 'Bilberry') on con explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on conflict (key) do update set fruit = excluded.fruit where excluded.fruit != 'Elderberry'; -- Does the same, but JSON format shows "Conflict Arbiter Index" as JSON array: explain (costs off, format json) insert into insertconflicttest values (0, 'Bilberry') on conflict (key) do update set fruit = excluded.fruit where insertconflicttest.fruit != 'Lime' returning *; +-- Should display lock strength, if specified +explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select returning *; +explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select for key share returning *; -- Fails (no unique index inference specification, required for do update variant): insert into insertconflicttest values (1, 'Apple') on conflict do update set fruit = excluded.fruit; @@ -125,6 +133,18 @@ insert into insertconflicttest AS ict values (6, 'Passionfruit') on conflict (ke -- Check helpful hint when qualifying set column with target table insert into insertconflicttest values (3, 'Kiwi') on conflict (key, fruit) do update set insertconflicttest.fruit = 'Mango'; +-- +-- DO SELECT tests +-- +delete from insertconflicttest where fruit = 'Apple'; +insert into insertconflicttest values (1, 'Apple') on conflict (key) do select; -- fails +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select returning old, new, i; +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select returning old, new, i; +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Apple' returning *; +insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Orange' returning *; +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Apple' returning *; +insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Orange' returning *; + drop index key_index; -- @@ -454,6 +474,30 @@ begin transaction isolation level serializable; insert into selfconflict values (6,1), (6,2) on conflict(f1) do update set f2 = 0; commit; +begin transaction isolation level read committed; +insert into selfconflict values (7,1), (7,2) on conflict(f1) do select returning *; +commit; + +begin transaction isolation level repeatable read; +insert into selfconflict values (8,1), (8,2) on conflict(f1) do select returning *; +commit; + +begin transaction isolation level serializable; +insert into selfconflict values (9,1), (9,2) on conflict(f1) do select returning *; +commit; + +begin transaction isolation level read committed; +insert into selfconflict values (10,1), (10,2) on conflict(f1) do select for update returning *; +commit; + +begin transaction isolation level repeatable read; +insert into selfconflict values (11,1), (11,2) on conflict(f1) do select for update returning *; +commit; + +begin transaction isolation level serializable; +insert into selfconflict values (12,1), (12,2) on conflict(f1) do select for update returning *; +commit; + select * from selfconflict; drop table selfconflict; @@ -468,13 +512,17 @@ insert into parted_conflict_test values (1, 'a') on conflict do nothing; -- index on a required, which does exist in parent insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing; insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test values (1, 'a') on conflict (a) do select returning *; +insert into parted_conflict_test values (1, 'a') on conflict (a) do select for update returning *; -- targeting partition directly will work insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing; insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do select returning b; -- index on b required, which doesn't exist in parent -insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; +insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; -- fail +insert into parted_conflict_test values (2, 'b') on conflict (b) do select returning b; -- fail -- targeting partition directly will work insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a; @@ -482,13 +530,16 @@ insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set -- should see (2, 'b') select * from parted_conflict_test order by a; --- now check that DO UPDATE works correctly for target partition with --- different attribute numbers +-- now check that DO UPDATE and DO SELECT work correctly for target partition +-- with different attribute numbers create table parted_conflict_test_2 (b char, a int unique); alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3); truncate parted_conflict_test; insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select returning b; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select where excluded.b = 'a' returning parted_conflict_test; +insert into parted_conflict_test values (3, 'a') on conflict (a) do select where parted_conflict_test.b = 'b' returning b; -- should see (3, 'b') select * from parted_conflict_test order by a; @@ -499,6 +550,7 @@ create table parted_conflict_test_3 partition of parted_conflict_test for values truncate parted_conflict_test; insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a'; +insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do select returning b; -- should see (4, 'b') select * from parted_conflict_test order by a; @@ -509,6 +561,7 @@ create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for va truncate parted_conflict_test; insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b; insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a'; +insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do select where parted_conflict_test.b = 'a' returning b; -- should see (5, 'b') select * from parted_conflict_test order by a; @@ -521,6 +574,26 @@ insert into parted_conflict_test (a, b) values (1, 'b'), (2, 'c'), (4, 'b') on c -- should see (1, 'b'), (2, 'a'), (4, 'b') select * from parted_conflict_test order by a; +-- test DO SELECT with multiple rows hitting different partitions +truncate parted_conflict_test; +insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'b'), (4, 'c'); +insert into parted_conflict_test (a, b) values (1, 'x'), (2, 'y'), (4, 'z') + on conflict (a) do select returning *, tableoid::regclass; + +-- should see original values (1, 'a'), (2, 'b'), (4, 'c') +select * from parted_conflict_test order by a; + +-- test DO SELECT with WHERE filtering across partitions +insert into parted_conflict_test (a, b) values (1, 'n') on conflict (a) do select where parted_conflict_test.b = 'a' returning *; +insert into parted_conflict_test (a, b) values (2, 'n') on conflict (a) do select where parted_conflict_test.b = 'x' returning *; + +-- test DO SELECT with EXCLUDED in WHERE across partitions with different layouts +insert into parted_conflict_test (a, b) values (3, 't') on conflict (a) do select where excluded.b = 't' returning *; + +-- test DO SELECT FOR UPDATE across different partition layouts +insert into parted_conflict_test (a, b) values (1, 'l') on conflict (a) do select for update returning *; +insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do select for update returning *; + drop table parted_conflict_test; -- test behavior of inserting a conflicting tuple into an intermediate diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index cc5128add4df0..fae19113cefba 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -759,6 +759,26 @@ select * from tbl_rs t1 join (select t1.a+t3.a from tbl_rs t3) and t2.a < 5) on true; +-- +-- regression test for bug with parallel-hash-right-semi join +-- + +begin; + +-- encourage use of parallel plans +set local parallel_setup_cost=0; +set local parallel_tuple_cost=0; +set local min_parallel_table_scan_size=0; +set local max_parallel_workers_per_gather=4; + +-- ensure we don't get parallel hash right semi join +explain (costs off) +select * from tenk1 t1 +where exists (select 1 from tenk1 t2 where fivethous = t1.fivethous) +and t1.fivethous < 5; + +rollback; + -- -- regression test for bug #13908 (hash join with skew tuples & nbatch increase) -- @@ -814,6 +834,25 @@ ORDER BY 1; reset enable_nestloop; +-- +-- test that estimate_hash_bucket_stats estimates correctly with skewed data +-- (we should choose to hash the filtered table) +-- + +create temp table skewedtable (val int not null, filt int not null); +insert into skewedtable +select + case when g <= 100 then 0 else (g % 100) + 1 end, + g % 10 +from generate_series(1, 1000) g; +analyze skewedtable; + +explain (costs off) +select * from skewedtable t1 join skewedtable t2 on t1.val = t2.val +where t1.filt = 5; + +drop table skewedtable; + -- -- basic semijoin and antijoin recognition tests -- @@ -839,6 +878,40 @@ explain (costs off) select a.* from tenk1 a left join tenk1 b on a.unique1 = b.unique2 where b.unique2 is null; +-- check that we avoid de-duplicating columns redundantly +set enable_memoize to off; +explain (costs off) +select 1 from tenk1 +where (hundred, thousand) in (select twothousand, twothousand from onek); +reset enable_memoize; + +-- +-- more antijoin recognition tests using NOT NULL constraints +-- + +begin; + +create temp table tbl_anti(a int not null, b int, c int); + +-- this is an antijoin, as t2.a is non-null for any matching row +explain (costs off) +select * from tenk1 t1 left join tbl_anti t2 on t1.unique1 = t2.b +where t2.a is null; + +-- this is an antijoin, as t2.a is non-null for any matching row +explain (costs off) +select * from tenk1 t1 left join + (tbl_anti t2 left join tbl_anti t3 on t2.c = t3.c) on t1.unique1 = t2.b +where t2.a is null; + +-- this is not an antijoin, as t3.a can be nulled by t2/t3 join +explain (costs off) +select * from tenk1 t1 left join + (tbl_anti t2 left join tbl_anti t3 on t2.c = t3.c) on t1.unique1 = t2.b +where t3.a is null; + +rollback; + -- -- regression test for bogus RTE_GROUP entries -- @@ -1277,6 +1350,23 @@ where t1.unique2 < 42 and t1.stringu1 > t2.stringu2; -- variant that isn't quite a star-schema case +explain (verbose, costs off) +select ss1.d1 from + tenk1 as t1 + inner join tenk1 as t2 + on t1.tenthous = t2.ten + inner join + int8_tbl as i8 + left join int4_tbl as i4 + inner join (select 64::information_schema.cardinal_number as d1 + from tenk1 t3, + lateral (select abs(t3.unique1) + random()) ss0(x) + where t3.fivethous < 0) as ss1 + on i4.f1 = ss1.d1 + on i8.q1 = i4.f1 + on t1.tenthous = ss1.d1 +where t1.unique1 < i4.f1; + select ss1.d1 from tenk1 as t1 inner join tenk1 as t2 @@ -1332,6 +1422,64 @@ select * from (select 1 as x) ss1 left join (select 2 as y) ss2 on (true), lateral (select ss2.y as z limit 1) ss3; +-- This example demonstrates the folly of our old "have_dangerous_phv" logic +begin; +set local from_collapse_limit to 2; +explain (verbose, costs off) +select * from int8_tbl t1 + left join + (select coalesce(t2.q1 + x, 0) from int8_tbl t2, + lateral (select t3.q1 as x from int8_tbl t3, + lateral (select t2.q1, t3.q1 offset 0) s)) + on true; +rollback; + +-- ... not that the initial replacement didn't have some bugs too +begin; +create temp table t(i int primary key); + +explain (verbose, costs off) +select * from t t1 + left join (select 1 as x, * from t t2(i2)) t2ss on t1.i = t2ss.i2 + left join t t3(i3) on false + left join t t4(i4) on t4.i4 > t2ss.x; + +explain (verbose, costs off) +select * from + (select k from + (select i, coalesce(i, j) as k from + (select i from t union all select 0) + join (select 1 as j limit 1) on i = j) + right join (select 2 as x) on true + join (select 3 as y) on i is not null + ), + lateral (select k as kl limit 1); + +rollback; + +-- PHVs containing SubLinks are quite tricky to get right +explain (verbose, costs off) +select * +from int8_tbl i8 + inner join + (select (select true) as x + from int4_tbl i4, lateral (select i4.f1 as y limit 1) ss1 + where i4.f1 = 0) ss2 on true + right join (select false as z) ss3 on true, + lateral (select i8.q2 as q2l where x limit 1) ss4 +where i8.q2 = 123; + +explain (verbose, costs off) +select * +from int8_tbl i8 + inner join + (select (select true) as x + from int4_tbl i4, lateral (select 1 as y limit 1) ss1 + where i4.f1 = 0) ss2 on true + right join (select false as z) ss3 on true, + lateral (select i8.q2 as q2l where x limit 1) ss4 +where i8.q2 = 123; + -- Test proper handling of appendrel PHVs during useless-RTE removal explain (costs off) select * from @@ -1379,6 +1527,30 @@ select * from int4_tbl left join ( ) ss(x) on true where ss.x is null; +-- Test computation of varnullingrels when translating appendrel Var +begin; + +create temp table t_append (a int not null, b int); +insert into t_append values (1, 1); +insert into t_append values (2, 3); + +explain (verbose, costs off) +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + +rollback; + -- -- test inlining of immutable functions -- @@ -1554,12 +1726,12 @@ order by fault; explain (costs off) select * from (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys) -left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x +left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x left join unnest(v1ys) as u1(u1y) on u1y = v2y; select * from (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys) -left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x +left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x left join unnest(v1ys) as u1(u1y) on u1y = v2y; -- @@ -1902,13 +2074,13 @@ select * from (select 1 as id) as xx left join (tenk1 as a1 full join (select 1 as id) as yy on (a1.unique1 = yy.id)) - on (xx.id = coalesce(yy.id)); + on (xx.id = coalesce(yy.id, yy.id)); select * from (select 1 as id) as xx left join (tenk1 as a1 full join (select 1 as id) as yy on (a1.unique1 = yy.id)) - on (xx.id = coalesce(yy.id)); + on (xx.id = coalesce(yy.id, yy.id)); -- -- test ability to push constants through outer join clauses @@ -2099,6 +2271,29 @@ from int8_tbl t1 left join onek t4 on t2.q2 < t3.unique2; +-- bug #19460: we need to clean up RestrictInfos more than we had been doing +explain (costs off) +select * from + (select 1::int as id) as lhs +full join + (select dummy_source.id + from (select null::int as id) as dummy_source + left join (select a.id from a where a.id = 42) as sub + on sub.id = dummy_source.id + ) as rhs +on lhs.id = rhs.id; + +explain (costs off) +select * from + (select 1::int as id) as lhs +full join + (select dummy_source.id + from (select 2::int as id) as dummy_source + left join (select a.id from a) as sub + on sub.id = dummy_source.id + ) as rhs +on lhs.id = rhs.id; + -- More tests of correct placement of pseudoconstant quals -- simple constant-false condition @@ -2130,6 +2325,24 @@ explain (costs off) select d.* from d left join (select * from b group by b.id, b.c_id) s on d.a = s.id and d.b = s.c_id; +-- check that join removal works for a left join when joining a subquery +-- that is guaranteed to be unique by GROUPING SETS +explain (costs off) +select d.* from d left join (select 1 as x from b group by ()) s + on d.a = s.x; + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets(())) s + on d.a = s.x; + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets(()), grouping sets(())) s + on d.a = s.x; + +explain (costs off) +select d.* from d left join (select 1 as x from b group by distinct grouping sets((), ())) s + on d.a = s.x; + -- similarly, but keying off a DISTINCT clause explain (costs off) select d.* from d left join (select distinct * from b) s @@ -2143,6 +2356,20 @@ explain (costs off) select d.* from d left join (select * from b group by b.id, b.c_id) s on d.a = s.id; +-- join removal is not possible when the GROUP BY contains non-empty grouping +-- sets or multiple empty grouping sets +explain (costs off) +select d.* from d left join (select 1 as x from b group by rollup(x)) s + on d.a = s.x; + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets((), ())) s + on d.a = s.x; + +explain (costs off) +select d.* from d left join (select 1 as x from b group by grouping sets((), grouping sets(()))) s + on d.a = s.x; + -- similarly, but keying off a DISTINCT clause explain (costs off) select d.* from d left join (select distinct * from b) s @@ -2345,6 +2572,69 @@ where t1.a = s.c; rollback; +-- check handling of semijoins after join removal: we must suppress +-- unique-ification of known-constant values +begin; + +create temp table t (a int unique, b int); +insert into t values (1, 2); + +explain (verbose, costs off) +select t1.a from t t1 + left join t t2 on t1.a = t2.a + join t t3 on true +where exists (select 1 from t t4 + join t t5 on t4.b = t5.b + join t t6 on t5.b = t6.b + where t1.a = t4.a and t3.a = t5.a and t4.a = 1); + +select t1.a from t t1 + left join t t2 on t1.a = t2.a + join t t3 on true +where exists (select 1 from t t4 + join t t5 on t4.b = t5.b + join t t6 on t5.b = t6.b + where t1.a = t4.a and t3.a = t5.a and t4.a = 1); + +rollback; + +-- check handling of semijoins if all RHS columns are equated to constants: we +-- should suppress unique-ification in this case. +begin; + +create temp table t (a int, b int); +insert into t values (1, 2); + +explain (costs off) +select * from t t1, t t2 where exists + (select 1 from t t3 where t1.a = t3.a and t2.b = t3.b and t3.a = 1 and t3.b = 2); + +select * from t t1, t t2 where exists + (select 1 from t t3 where t1.a = t3.a and t2.b = t3.b and t3.a = 1 and t3.b = 2); + +rollback; + +-- check handling of semijoin unique-ification for child relations if all RHS +-- columns are equated to constants. +begin; + +create temp table p (a int, b int) partition by range (a); +create temp table p1 partition of p for values from (0) to (10); +create temp table p2 partition of p for values from (10) to (20); +insert into p values (1, 2); +insert into p values (10, 20); + +set enable_partitionwise_join to on; + +explain (costs off) +select * from p t1 where exists + (select 1 from p t2 where t1.a = t2.a and t1.a = 1); + +select * from p t1 where exists + (select 1 from p t2 where t1.a = t2.a and t1.a = 1); + +rollback; + -- test cases where we can remove a join, but not a PHV computed at it begin; @@ -2889,6 +3179,12 @@ SELECT * FROM (SELECT n2.a FROM sj n1, sj n2 WHERE n1.a <> n2.a) q0, sl WHERE q0.a = 1; +-- Do not forget to replace relid in bare Var join clause (bug #19435) +ALTER TABLE sl ADD COLUMN bool_col boolean; +EXPLAIN (COSTS OFF) +SELECT 1 AS c1 FROM sl sl1 LEFT JOIN (sl AS sl2 NATURAL JOIN sl AS sl3) + ON sl2.bool_col LEFT JOIN sl AS sl4 ON sl2.bool_col; + -- Check optimization disabling if it will violate special join conditions. -- Two identical joined relations satisfies self join removal conditions but -- stay in different special join infos. @@ -3094,9 +3390,9 @@ select * from int4_tbl i left join lateral (select * from int2_tbl j where i.f1 = j.f1) k on true; explain (verbose, costs off) select * from int4_tbl i left join - lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; + lateral (select coalesce(i, i) from int2_tbl j where i.f1 = j.f1) k on true; select * from int4_tbl i left join - lateral (select coalesce(i) from int2_tbl j where i.f1 = j.f1) k on true; + lateral (select coalesce(i, i) from int2_tbl j where i.f1 = j.f1) k on true; explain (verbose, costs off) select * from int4_tbl a, lateral ( @@ -3434,7 +3730,7 @@ explain (verbose, costs off) select * from j1 left join j2 on j1.id1 = j2.id1 where j1.id2 = 1; -create unique index j1_id2_idx on j1(id2) where id2 is not null; +create unique index j1_id2_idx on j1(id2) where id2 > 0; -- ensure we don't use a partial unique index as unique proofs explain (verbose, costs off) @@ -3450,14 +3746,16 @@ set enable_nestloop to 0; set enable_hashjoin to 0; set enable_sort to 0; +-- we need additional data to get the partial indexes to be preferred +insert into j1 select 2, i from generate_series(1, 100) i; +insert into j2 select 1, i from generate_series(2, 100) i; +analyze j1; +analyze j2; + -- create indexes that will be preferred over the PKs to perform the join create index j1_id1_idx on j1 (id1) where id1 % 1000 = 1; create index j2_id1_idx on j2 (id1) where id1 % 1000 = 1; --- need an additional row in j2, if we want j2_id1_idx to be preferred -insert into j2 values(1,2); -analyze j2; - explain (costs off) select * from j1 inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2 where j1.id1 % 1000 = 1 and j2.id1 % 1000 = 1; @@ -3562,11 +3860,21 @@ ANALYZE group_tbl; EXPLAIN (COSTS OFF) SELECT 1 FROM group_tbl t1 - LEFT JOIN (SELECT a c1, COALESCE(a) c2 FROM group_tbl t2) s ON TRUE + LEFT JOIN (SELECT a c1, COALESCE(a, a) c2 FROM group_tbl t2) s ON TRUE GROUP BY s.c1, s.c2; DROP TABLE group_tbl; +-- Test that we ignore PlaceHolderVars when looking up statistics +EXPLAIN (COSTS OFF) +SELECT t1.unique1 FROM tenk1 t1 LEFT JOIN + (SELECT *, 42 AS phv FROM tenk1 t2) ss ON t1.unique2 = ss.unique2 +WHERE ss.unique1 = ss.phv AND t1.unique1 < 100; + +SELECT t1.unique1 FROM tenk1 t1 LEFT JOIN + (SELECT *, 42 AS phv FROM tenk1 t2) ss ON t1.unique2 = ss.unique2 +WHERE ss.unique1 = ss.phv AND t1.unique1 < 100; + -- -- Test for a nested loop join involving index scan, transforming OR-clauses -- to SAOP. diff --git a/src/test/regress/sql/join_hash.sql b/src/test/regress/sql/join_hash.sql index 6b0688ab0a61d..989390e686486 100644 --- a/src/test/regress/sql/join_hash.sql +++ b/src/test/regress/sql/join_hash.sql @@ -57,6 +57,7 @@ $$; -- estimated size. create table simple as select generate_series(1, 20000) AS id, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +insert into simple values (null, null); alter table simple set (parallel_workers = 2); analyze simple; @@ -83,8 +84,8 @@ update pg_class set reltuples = 2, relpages = pg_relation_size('extremely_skewed') / 8192 where relname = 'extremely_skewed'; --- Make a relation with a couple of enormous tuples. -create table wide as select generate_series(1, 2) as id, rpad('', 320000, 'x') as t; +-- Make a relation with several enormous tuples. +create table wide as select generate_series(1, 3) as id, rpad('', 320000, 'x') as t; alter table wide set (parallel_workers = 2); -- The "optimal" case: the hash table fits in memory; we plan for 1 @@ -314,6 +315,7 @@ create table join_foo as select generate_series(1, 3) as id, 'xxxxx'::text as t; alter table join_foo set (parallel_workers = 0); create table join_bar as select generate_series(1, 10000) as id, 'xxxxx'::text as t; alter table join_bar set (parallel_workers = 2); +analyze join_foo, join_bar; -- multi-batch with rescan, parallel-oblivious savepoint settings; @@ -495,14 +497,14 @@ set work_mem = '128kB'; set hash_mem_multiplier = 1.0; explain (costs off) select length(max(s.t)) - from wide left join (select id, coalesce(t, '') || '' as t from wide) s using (id); + from wide left join (select id, coalesce(t, '') || '' as t from wide where id < 3) s using (id); select length(max(s.t)) -from wide left join (select id, coalesce(t, '') || '' as t from wide) s using (id); +from wide left join (select id, coalesce(t, '') || '' as t from wide where id < 3) s using (id); select final > 1 as multibatch from hash_join_batches( $$ select length(max(s.t)) - from wide left join (select id, coalesce(t, '') || '' as t from wide) s using (id); + from wide left join (select id, coalesce(t, '') || '' as t from wide where id < 3) s using (id); $$); rollback to settings; diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 57c11acddfee6..d28ed1c1e851c 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -402,6 +402,12 @@ SELECT jsonb_build_object('{1,2,3}'::int[], 3); SELECT jsonb_object_agg(1, NULL::jsonb); SELECT jsonb_object_agg(NULL, '{"a":1}'); +SELECT jsonb_object_agg_unique(i, null) OVER (ORDER BY i) + FROM generate_series(1, 10) g(i); + +SELECT jsonb_object_agg_unique_strict(i, null) OVER (ORDER BY i) + FROM generate_series(1, 10) g(i); + CREATE TEMP TABLE foo (serial_num int, name text, type text); INSERT INTO foo VALUES (847001,'t15','GE1043'); INSERT INTO foo VALUES (847002,'t16','GE1043'); @@ -851,6 +857,7 @@ SELECT count(*) FROM testjsonb WHERE j @? '$'; SELECT count(*) FROM testjsonb WHERE j @? '$.public'; SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; +ALTER TABLE testjsonb SET (parallel_workers = 2); CREATE INDEX jidx ON testjsonb USING gin (j); SET enable_seqscan = off; @@ -939,7 +946,7 @@ SELECT count(*) FROM testjsonb WHERE j = '{"pos":98, "line":371, "node":"CBA", " --gin path opclass DROP INDEX jidx; -CREATE INDEX jidx ON testjsonb USING gin (j jsonb_path_ops); +CREATE INDEX CONCURRENTLY jidx ON testjsonb USING gin (j jsonb_path_ops); SET enable_seqscan = off; SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}'; @@ -1590,3 +1597,21 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::float8; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int2; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int4; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; + +-- single argument jsonb functions as jsonb_function(jsonb) and jsonb.jsonb_function +select jsonb_typeof('{"a":1}'::jsonb); +select ('{"a":1}'::jsonb).jsonb_typeof; +select jsonb_array_length('["a", "b", "c"]'::jsonb); +select ('["a", "b", "c"]'::jsonb).jsonb_array_length; +select jsonb_object_keys('{"a":1, "b":2}'::jsonb); +select ('{"a":1, "b":2}'::jsonb).jsonb_object_keys; + +-- cast jsonb to other types as (jsonb)::type and (jsonb).type +select ('123.45'::jsonb)::numeric; +select ('123.45'::jsonb).numeric; +select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb)::name; +select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb).name; +select ('true'::jsonb)::bool; +select ('true'::jsonb).bool; +select ('{"text": "hello"}'::jsonb)::text; +select ('{"text": "hello"}'::jsonb).text; diff --git a/src/test/regress/sql/jsonb_jsonpath.sql b/src/test/regress/sql/jsonb_jsonpath.sql index 3e8929a5269bc..d3a38c577918b 100644 --- a/src/test/regress/sql/jsonb_jsonpath.sql +++ b/src/test/regress/sql/jsonb_jsonpath.sql @@ -623,6 +623,112 @@ select jsonb_path_query('"2023-08-15 12:34:56 +5:30"', '$.timestamp_tz().string( select jsonb_path_query('"2023-08-15 12:34:56"', '$.timestamp().string()'); rollback; +-- test .ltrim() +select jsonb_path_query('" hello "', '$.ltrim(" ")'); +select jsonb_path_query('" hello "', '$.ltrim()'); +select jsonb_path_query('"zzzytest"', '$.ltrim("xyz")'); +select jsonb_path_query('null', '$.ltrim()'); +select jsonb_path_query('null', '$.ltrim()', silent => true); +select jsonb_path_query('[]', '$.ltrim()'); +select jsonb_path_query('[]', 'strict $.ltrim()'); +select jsonb_path_query('{}', '$.ltrim()'); +select jsonb_path_query('[]', 'strict $.ltrim()', silent => true); +select jsonb_path_query('{}', '$.ltrim()', silent => true); +select jsonb_path_query('1.23', '$.ltrim()'); +select jsonb_path_query('"1.23"', '$.ltrim()'); +select jsonb_path_query('"1.23aaa"', '$.ltrim()'); +select jsonb_path_query('1234', '$.ltrim()'); +select jsonb_path_query('true', '$.ltrim()'); +select jsonb_path_query('1234', '$.ltrim().type()'); +select jsonb_path_query('[2, true]', '$.ltrim()'); +select jsonb_path_query_array('[" maybe ", " yes", " no"]', '$[*].ltrim()'); +select jsonb_path_query_array('[" maybe ", " yes", " no"]', '$[*].ltrim().type()'); + +-- test .rtrim() +select jsonb_path_query('" hello "', '$.rtrim(" ")'); +select jsonb_path_query('"testxxzx"', '$.rtrim("xyz")'); +select jsonb_path_query('" hello "', '$.rtrim()'); + +-- test .btrim() +select jsonb_path_query('" hello "', '$.btrim(" ")'); +select jsonb_path_query('"xyxtrimyyx"', '$.btrim("xyz")'); +select jsonb_path_query('" hello "', '$.btrim()'); + +-- test .lower() +select jsonb_path_query('null', '$.lower()'); +select jsonb_path_query('null', '$.lower()', silent => true); +select jsonb_path_query('[]', '$.lower()'); +select jsonb_path_query('[]', 'strict $.lower()'); +select jsonb_path_query('{}', '$.lower()'); +select jsonb_path_query('[]', 'strict $.lower()', silent => true); +select jsonb_path_query('{}', '$.lower()', silent => true); +select jsonb_path_query('1.23', '$.lower()'); +select jsonb_path_query('"1.23"', '$.lower()'); +select jsonb_path_query('"1.23aaa"', '$.lower()'); +select jsonb_path_query('1234', '$.lower()'); +select jsonb_path_query('true', '$.lower()'); +select jsonb_path_query('1234', '$.lower().type()'); +select jsonb_path_query('[2, true]', '$.lower()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].lower()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].lower().type()'); + +-- test .upper() +select jsonb_path_query('null', '$.upper()'); +select jsonb_path_query('null', '$.upper()', silent => true); +select jsonb_path_query('[]', '$.upper()'); +select jsonb_path_query('[]', 'strict $.upper()'); +select jsonb_path_query('{}', '$.upper()'); +select jsonb_path_query('[]', 'strict $.upper()', silent => true); +select jsonb_path_query('{}', '$.upper()', silent => true); +select jsonb_path_query('1.23', '$.upper()'); +select jsonb_path_query('"1.23"', '$.upper()'); +select jsonb_path_query('"1.23aaa"', '$.upper()'); +select jsonb_path_query('1234', '$.upper()'); +select jsonb_path_query('true', '$.upper()'); +select jsonb_path_query('1234', '$.upper().type()'); +select jsonb_path_query('[2, true]', '$.upper()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].upper()'); +select jsonb_path_query_array('["maybe", "yes", "no"]', '$[*].upper().type()'); + +-- test .initcap() +select jsonb_path_query('null', '$.initcap()'); +select jsonb_path_query('null', '$.initcap()', silent => true); +select jsonb_path_query('[]', '$.initcap()'); +select jsonb_path_query('[]', 'strict $.initcap()'); +select jsonb_path_query('{}', '$.initcap()'); +select jsonb_path_query('[]', 'strict $.initcap()', silent => true); +select jsonb_path_query('{}', '$.initcap()', silent => true); +select jsonb_path_query('1.23', '$.initcap()'); +select jsonb_path_query('"1.23"', '$.initcap()'); +select jsonb_path_query('"1.23aaa"', '$.initcap()'); +select jsonb_path_query('1234', '$.initcap()'); +select jsonb_path_query('true', '$.initcap()'); +select jsonb_path_query('1234', '$.initcap().type()'); +select jsonb_path_query('[2, true]', '$.initcap()'); +select jsonb_path_query('["maybe yes", "probably no"]', '$.initcap()'); + +-- Test .replace() +select jsonb_path_query('null', '$.replace("x", "bye")'); +select jsonb_path_query('null', '$.replace("x", "bye")', silent => true); +select jsonb_path_query('["x", "y", "z"]', '$.replace("x", "bye")'); +select jsonb_path_query('{}', '$.replace("x", "bye")'); +select jsonb_path_query('[]', 'strict $.replace("x", "bye")', silent => true); +select jsonb_path_query('{}', '$.replace("x", "bye")', silent => true); +select jsonb_path_query('1.23', '$.replace("x", "bye")'); +select jsonb_path_query('"hello world"', '$.replace("hello","bye")'); +select jsonb_path_query('"hello world"', '$.replace("hello","bye") starts with "bye"'); + +-- Test .split_part() +select jsonb_path_query('"abc~@~def~@~ghi"', '$.split_part("~@~", 2)'); +select jsonb_path_query('"abc,def,ghi,jkl"', '$.split_part(",", -2)'); + +-- Test string methods play nicely together +select jsonb_path_query('"hello world"', '$.replace("hello","bye").upper()'); +select jsonb_path_query('"hElLo WorlD"', '$.lower().upper().lower().replace("hello","bye")'); +select jsonb_path_query('"hElLo WorlD"', '$.upper().lower().upper().replace("HELLO", "BYE")'); +select jsonb_path_query('"hElLo WorlD"', '$.lower().upper().lower().replace("hello","bye") starts with "bye"'); +select jsonb_path_query('" hElLo WorlD "', '$.btrim().lower().upper().lower().replace("hello","bye") starts with "bye"'); + -- Test .time() select jsonb_path_query('null', '$.time()'); select jsonb_path_query('true', '$.time()'); diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql index 61a5270d4e8b0..44178d8b45a1a 100644 --- a/src/test/regress/sql/jsonpath.sql +++ b/src/test/regress/sql/jsonpath.sql @@ -78,6 +78,47 @@ select '$.boolean()'::jsonpath; select '$.date()'::jsonpath; select '$.decimal(4,2)'::jsonpath; select '$.string()'::jsonpath; +select '$.replace("hello","bye")'::jsonpath; +select '$.lower()'::jsonpath; +select '$.upper()'::jsonpath; +select '$.lower().upper().lower().replace("hello","bye")'::jsonpath; +select '$.ltrim()'::jsonpath; +select '$.ltrim("xyz")'::jsonpath; +select '$.rtrim()'::jsonpath; +select '$.rtrim("xyz")'::jsonpath; +select '$.btrim()'::jsonpath; +select '$.btrim("xyz")'::jsonpath; +select '$.initcap()'::jsonpath; +select '$.split_part("~@~", 2)'::jsonpath; + +-- Parse errors +select '$.replace("hello")'::jsonpath; +select '$.replace()'::jsonpath; +select '$.replace("hello","bye","extra")'::jsonpath; +select '$.split_part("~@~")'::jsonpath; +select '$.split_part()'::jsonpath; +select '$.split_part("~@~", "hi")'::jsonpath; +select '$.split_part("~@~", 2, "extra")'::jsonpath; +select '$.lower("hi")'::jsonpath; +select '$.upper("hi")'::jsonpath; +select '$.initcap("hi")'::jsonpath; +select '$.ltrim(42)'::jsonpath; +select '$.ltrim("x", "y")'::jsonpath; +select '$.rtrim(42)'::jsonpath; +select '$.rtrim("x", "y")'::jsonpath; +select '$.trim(42)'::jsonpath; +select '$.trim("x", "y")'::jsonpath; + +-- Verify method keywords work as object key names +select '$.lower'::jsonpath; +select '$.upper'::jsonpath; +select '$.initcap'::jsonpath; +select '$.replace'::jsonpath; +select '$.split_part'::jsonpath; +select '$.ltrim'::jsonpath; +select '$.rtrim'::jsonpath; +select '$.btrim'::jsonpath; + select '$.time()'::jsonpath; select '$.time(6)'::jsonpath; select '$.time_tz()'::jsonpath; diff --git a/src/test/regress/sql/lock.sql b/src/test/regress/sql/lock.sql index b88488c6d0ff4..e78d79eee32b1 100644 --- a/src/test/regress/sql/lock.sql +++ b/src/test/regress/sql/lock.sql @@ -85,7 +85,7 @@ select relname from pg_locks l, pg_class c ROLLBACK; BEGIN TRANSACTION; LOCK TABLE lock_view6 IN EXCLUSIVE MODE; --- lock_view6 an lock_tbl1 are locked. +-- lock_view6 and lock_tbl1 are locked. select relname from pg_locks l, pg_class c where l.relation = c.oid and relname like '%lock_%' and mode = 'ExclusiveLock' order by relname; diff --git a/src/test/regress/sql/matview.sql b/src/test/regress/sql/matview.sql index 6704eeae2df84..934426b9ae8c1 100644 --- a/src/test/regress/sql/matview.sql +++ b/src/test/regress/sql/matview.sql @@ -98,6 +98,10 @@ REFRESH MATERIALIZED VIEW CONCURRENTLY mvtest_tvmm WITH NO DATA; -- no tuple locks on materialized views SELECT * FROM mvtest_tvvm FOR SHARE; +-- we don't support temp materialized views, so disallow this case: +CREATE TEMP TABLE mvtest_temp_t (id int NOT NULL, type text NOT NULL, amt numeric NOT NULL); +CREATE MATERIALIZED VIEW mvtest_temp_tm AS SELECT * FROM mvtest_temp_t; + -- test join of mv and view SELECT type, m.totamt AS mtot, v.totamt AS vtot FROM mvtest_tm m LEFT JOIN mvtest_tv v USING (type) ORDER BY type; diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql index c0d47fa875ad9..e39bbb653919f 100644 --- a/src/test/regress/sql/memoize.sql +++ b/src/test/regress/sql/memoize.sql @@ -26,6 +26,7 @@ begin ln := regexp_replace(ln, 'Heap Fetches: \d+', 'Heap Fetches: N'); ln := regexp_replace(ln, 'loops=\d+', 'loops=N'); ln := regexp_replace(ln, 'Index Searches: \d+', 'Index Searches: N'); + ln := regexp_replace(ln, 'Memory: \d+kB', 'Memory: NkB'); return next ln; end loop; end; @@ -138,6 +139,7 @@ INSERT INTO flt VALUES('-0.0'::float),('+0.0'::float); ANALYZE flt; SET enable_seqscan TO off; +SET enable_material TO off; -- Ensure memoize operates in logical mode SELECT explain_memoize(' @@ -217,6 +219,7 @@ WHERE unique1 < 3 WHERE t0.ten = t1.twenty AND t0.two <> t2.four OFFSET 0); RESET enable_seqscan; +RESET enable_material; RESET enable_mergejoin; RESET work_mem; RESET hash_mem_multiplier; @@ -244,3 +247,29 @@ RESET max_parallel_workers_per_gather; RESET parallel_tuple_cost; RESET parallel_setup_cost; RESET min_parallel_table_scan_size; + +-- Ensure memoize works for ANTI joins +CREATE TABLE tab_anti (a int, b boolean); +INSERT INTO tab_anti SELECT i%3, false FROM generate_series(1,100)i; +ANALYZE tab_anti; + +-- Ensure we get a Memoize plan for ANTI join +SELECT explain_memoize(' +SELECT COUNT(*) FROM tab_anti t1 LEFT JOIN +LATERAL (SELECT DISTINCT ON (a) a, b, t1.a AS x FROM tab_anti t2) t2 +ON t1.a+1 = t2.a +WHERE t2.a IS NULL;', false); + +-- And check we get the expected results. +SELECT COUNT(*) FROM tab_anti t1 LEFT JOIN +LATERAL (SELECT DISTINCT ON (a) a, b, t1.a AS x FROM tab_anti t2) t2 +ON t1.a+1 = t2.a +WHERE t2.a IS NULL; + +-- Ensure we do not add memoize node for SEMI join +EXPLAIN (COSTS OFF) +SELECT * FROM tab_anti t1 WHERE t1.a IN + (SELECT a FROM tab_anti t2 WHERE t2.b IN + (SELECT t1.b FROM tab_anti t3 WHERE t2.a > 1 OFFSET 0)); + +DROP TABLE tab_anti; diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql index f7a19c0e7dd7d..2660b19f2389f 100644 --- a/src/test/regress/sql/merge.sql +++ b/src/test/regress/sql/merge.sql @@ -1722,6 +1722,55 @@ WHEN MATCHED THEN DELETE; SELECT * FROM new_measurement ORDER BY city_id, logdate; +-- MERGE into inheritance root table +DROP TRIGGER insert_measurement_trigger ON measurement; +ALTER TABLE measurement ADD CONSTRAINT mcheck CHECK (city_id = 0) NO INHERIT; + +EXPLAIN (COSTS OFF) +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 100); + +BEGIN; +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 100); +SELECT * FROM ONLY measurement ORDER BY city_id, logdate; +ROLLBACK; + +ALTER TABLE measurement ENABLE ROW LEVEL SECURITY; +ALTER TABLE measurement FORCE ROW LEVEL SECURITY; +CREATE POLICY measurement_p ON measurement USING (peaktemp IS NOT NULL); + +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, NULL, 100); -- should fail + +MERGE INTO measurement m + USING (VALUES (1, '01-17-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 100); -- ok +SELECT * FROM ONLY measurement ORDER BY city_id, logdate; + +MERGE INTO measurement m + USING (VALUES (1, '01-18-2007'::date)) nm(city_id, logdate) ON + (m.city_id = nm.city_id and m.logdate=nm.logdate) +WHEN NOT MATCHED THEN INSERT + (city_id, logdate, peaktemp, unitsales) + VALUES (city_id - 1, logdate, 25, 200) +RETURNING merge_action(), m.*; + DROP TABLE measurement, new_measurement CASCADE; DROP FUNCTION measurement_insert_trigger(); diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql index 5f9c77512d160..946ee5726cdd7 100644 --- a/src/test/regress/sql/misc_functions.sql +++ b/src/test/regress/sql/misc_functions.sql @@ -4,47 +4,6 @@ \set regresslib :libdir '/regress' :dlsuffix --- Function to assist with verifying EXPLAIN which includes costs. A series --- of bool flags allows control over which portions are masked out -CREATE FUNCTION explain_mask_costs(query text, do_analyze bool, - hide_costs bool, hide_row_est bool, hide_width bool) RETURNS setof text -LANGUAGE plpgsql AS -$$ -DECLARE - ln text; - analyze_str text; -BEGIN - IF do_analyze = true THEN - analyze_str := 'on'; - ELSE - analyze_str := 'off'; - END IF; - - -- avoid jit related output by disabling it - SET LOCAL jit = 0; - - FOR ln IN - EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s', - analyze_str, query) - LOOP - IF hide_costs = true THEN - ln := regexp_replace(ln, 'cost=\d+\.\d\d\.\.\d+\.\d\d', 'cost=N..N'); - END IF; - - IF hide_row_est = true THEN - -- don't use 'g' so that we leave the actual rows intact - ln := regexp_replace(ln, 'rows=\d+', 'rows=N'); - END IF; - - IF hide_width = true THEN - ln := regexp_replace(ln, 'width=\d+', 'width=N'); - END IF; - - RETURN NEXT ln; - END LOOP; -END; -$$; - -- -- num_nulls() -- @@ -77,6 +36,17 @@ SELECT num_nulls(VARIADIC '{}'::int[]); SELECT num_nonnulls(); SELECT num_nulls(); +-- +-- error_on_null() +-- + +SELECT error_on_null(1); +SELECT error_on_null(NULL::int); +SELECT error_on_null(NULL::int[]); +SELECT error_on_null('{1,2,NULL,3}'::int[]); +SELECT error_on_null(ROW(1,NULL::int)); +SELECT error_on_null(ROW(NULL,NULL)); + -- -- canonicalize_path() -- @@ -267,87 +237,38 @@ EXPLAIN (COSTS OFF) SELECT * FROM tenk1 a JOIN my_gen_series(1,10) g ON a.unique1 = g; -- --- Test the SupportRequestRows support function for generate_series_timestamp() --- - --- Ensure the row estimate matches the actual rows -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day') g(s);$$, -true, true, false, true); - --- As above but with generate_series_timestamp -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMP '2024-02-01', TIMESTAMP '2024-03-01', INTERVAL '1 day') g(s);$$, -true, true, false, true); - --- As above but with generate_series_timestamptz_at_zone() -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day', 'UTC') g(s);$$, -true, true, false, true); - --- Ensure the estimated and actual row counts match when the range isn't --- evenly divisible by the step -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '7 day') g(s);$$, -true, true, false, true); - --- Ensure the estimates match when step is decreasing -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '-1 day') g(s);$$, -true, true, false, true); - --- Ensure an empty range estimates 1 row -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '1 day') g(s);$$, -true, true, false, true); - --- Ensure we get the default row estimate for infinity values -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(TIMESTAMPTZ '-infinity', TIMESTAMPTZ 'infinity', INTERVAL '1 day') g(s);$$, -false, true, false, true); - --- Ensure the row estimate behaves correctly when step size is zero. --- We expect generate_series_timestamp() to throw the error rather than in --- the support function. -SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '0 day') g(s); - --- --- Test the SupportRequestRows support function for generate_series_numeric() +-- Test SupportRequestInlineInFrom request -- --- Ensure the row estimate matches the actual rows -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(1.0, 25.0) g(s);$$, -true, true, false, true); - --- As above but with non-default step -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(1.0, 25.0, 2.0) g(s);$$, -true, true, false, true); - --- Ensure the estimates match when step is decreasing -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(25.0, 1.0, -1.0) g(s);$$, -true, true, false, true); +CREATE FUNCTION test_inline_in_from_support_func(internal) + RETURNS internal + AS :'regresslib', 'test_inline_in_from_support_func' + LANGUAGE C STRICT; --- Ensure an empty range estimates 1 row -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(25.0, 1.0, 1.0) g(s);$$, -true, true, false, true); +CREATE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT) +RETURNS SETOF TEXT +LANGUAGE plpgsql +AS $function$ +DECLARE + sql TEXT; +BEGIN + sql := format('SELECT %I::text FROM %I', colname, tablename); + IF filter IS NOT NULL THEN + sql := CONCAT(sql, format(' WHERE %I::text = $1', colname)); + END IF; + RETURN QUERY EXECUTE sql USING filter; +END; +$function$ STABLE; --- Ensure we get the default row estimate for error cases (infinity/NaN values --- and zero step size) -SELECT explain_mask_costs($$ -SELECT * FROM generate_series('-infinity'::NUMERIC, 'infinity'::NUMERIC, 1.0) g(s);$$, -false, true, false, true); +ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT) + SUPPORT test_inline_in_from_support_func; -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(1.0, 25.0, 'NaN'::NUMERIC) g(s);$$, -false, true, false, true); +SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); +EXPLAIN (COSTS OFF) SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL); +EXPLAIN (COSTS OFF) SELECT * FROM foo_from_bar('f1', 'text_tbl', 'doh!'); -SELECT explain_mask_costs($$ -SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$, -false, true, false, true); +DROP FUNCTION foo_from_bar; -- Test functions for control data SELECT count(*) > 0 AS ok FROM pg_control_checkpoint(); @@ -398,11 +319,10 @@ SELECT pg_column_toast_chunk_id(a) IS NULL, pg_column_toast_chunk_id(b) IN (SELECT chunk_id FROM pg_toast.:toastrel) FROM test_chunk_id; DROP TABLE test_chunk_id; -DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); --- test stratnum support functions -SELECT gist_stratnum_common(7); -SELECT gist_stratnum_common(3); +-- test stratnum translation support functions +SELECT gist_translate_cmptype_common(7); +SELECT gist_translate_cmptype_common(3); -- relpath tests @@ -414,3 +334,25 @@ SELECT test_relpath(); -- pg_replication_origin.roname limit SELECT pg_replication_origin_create('regress_' || repeat('a', 505)); + +-- pg_get_multixact_stats tests +CREATE ROLE regress_multixact_funcs; +-- Access granted for superusers. +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); +-- Access revoked. +SET ROLE regress_multixact_funcs; +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); +RESET ROLE; +-- Access granted for users with pg_monitor rights. +GRANT pg_monitor TO regress_multixact_funcs; +SET ROLE regress_multixact_funcs; +SELECT oldest_multixact IS NULL AS null_result FROM pg_get_multixact_stats(); +RESET ROLE; +DROP ROLE regress_multixact_funcs; + +-- test instr_time nanosecond<->ticks conversion +CREATE FUNCTION test_instr_time() + RETURNS bool + AS :'regresslib' + LANGUAGE C; +SELECT test_instr_time(); diff --git a/src/test/regress/sql/multirangetypes.sql b/src/test/regress/sql/multirangetypes.sql index 41d5524285a39..112334b03ebbe 100644 --- a/src/test/regress/sql/multirangetypes.sql +++ b/src/test/regress/sql/multirangetypes.sql @@ -414,6 +414,28 @@ SELECT nummultirange(numrange(1,3), numrange(4,5)) - nummultirange(numrange(2,9) SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(8,9)); SELECT nummultirange(numrange(1,2), numrange(4,5)) - nummultirange(numrange(-2,0), numrange(8,9)); +-- multirange_minus_multi +SELECT multirange_minus_multi(nummultirange(), nummultirange()); +SELECT multirange_minus_multi(nummultirange(), nummultirange(numrange(1,2))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange()); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(3,4)), nummultirange()); +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange(numrange(1,2))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange(numrange(2,4))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2)), nummultirange(numrange(3,4))); +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(1,2))); +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(2,3))); +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(0,8))); +SELECT multirange_minus_multi(nummultirange(numrange(1,4)), nummultirange(numrange(0,2))); +SELECT multirange_minus_multi(nummultirange(numrange(1,8)), nummultirange(numrange(0,2), numrange(3,4))); +SELECT multirange_minus_multi(nummultirange(numrange(1,8)), nummultirange(numrange(2,3), numrange(5,null))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(2,4))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(3,5))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(0,9))); +SELECT multirange_minus_multi(nummultirange(numrange(1,3), numrange(4,5)), nummultirange(numrange(2,9))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(8,9))); +SELECT multirange_minus_multi(nummultirange(numrange(1,2), numrange(4,5)), nummultirange(numrange(-2,0), numrange(8,9))); + -- intersection SELECT nummultirange() * nummultirange(); SELECT nummultirange() * nummultirange(numrange(1,2)); diff --git a/src/test/regress/sql/namespace.sql b/src/test/regress/sql/namespace.sql index 306cdc2d8c6a1..a75d4f580d3a6 100644 --- a/src/test/regress/sql/namespace.sql +++ b/src/test/regress/sql/namespace.sql @@ -7,15 +7,16 @@ SELECT pg_catalog.set_config('search_path', ' ', false); CREATE SCHEMA test_ns_schema_1 + CREATE TABLE abc ( + a serial, + b int UNIQUE + ) + CREATE UNIQUE INDEX abc_a_idx ON abc (a) CREATE VIEW abc_view AS SELECT a+1 AS a, b+1 AS b FROM abc - - CREATE TABLE abc ( - a serial, - b int UNIQUE - ); +; -- verify that the correct search_path restored on abort SET search_path to public; diff --git a/src/test/regress/sql/nls.sql b/src/test/regress/sql/nls.sql new file mode 100644 index 0000000000000..4433a1fd036a0 --- /dev/null +++ b/src/test/regress/sql/nls.sql @@ -0,0 +1,38 @@ +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_translation() + RETURNS void + AS :'regresslib' + LANGUAGE C; + +-- There's less standardization in locale name spellings than one could wish. +-- While some platforms insist on having a codeset name in lc_messages, +-- fortunately it seems that it need not match the actual database encoding. +-- However, if no es_ES locale is installed at all, this'll fail. +SET lc_messages = 'C'; + +do $$ +declare locale text; ok bool; +begin + for locale in values('es_ES'), ('es_ES.UTF-8'), ('es_ES.utf8') + loop + ok = true; + begin + execute format('set lc_messages = %L', locale); + exception when invalid_parameter_value then + ok = false; + end; + exit when ok; + end loop; + -- Don't clutter the expected results with this info, just log it + raise log 'NLS regression test: lc_messages = %', + current_setting('lc_messages'); +end $$; + +SELECT test_translation(); + +RESET lc_messages; diff --git a/src/test/regress/sql/numeric.sql b/src/test/regress/sql/numeric.sql index b98ae27df5691..640c6d92f4ce7 100644 --- a/src/test/regress/sql/numeric.sql +++ b/src/test/regress/sql/numeric.sql @@ -869,6 +869,8 @@ SELECT width_bucket(5.0::float8, 3.0::float8, 4.0::float8, 0); SELECT width_bucket(5.0::float8, 3.0::float8, 4.0::float8, -5); SELECT width_bucket(3.5::float8, 3.0::float8, 3.0::float8, 888); SELECT width_bucket('NaN', 3.0, 4.0, 888); +SELECT width_bucket('NaN'::float8, 3.0::float8, 4.0::float8, 888); +SELECT width_bucket(0, 'NaN', 4.0, 888); SELECT width_bucket(0::float8, 'NaN', 4.0::float8, 888); SELECT width_bucket(2.0, 3.0, '-inf', 888); SELECT width_bucket(0::float8, '-inf', 4.0::float8, 888); diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql index 1a6c61f49d543..1bbe9457c1ca3 100644 --- a/src/test/regress/sql/object_address.sql +++ b/src/test/regress/sql/object_address.sql @@ -37,6 +37,7 @@ CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig(); CREATE POLICY genpol ON addr_nsp.gentable; CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$; +CREATE PROPERTY GRAPH addr_nsp.gengraph; CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw; CREATE USER MAPPING FOR regress_addr_user SERVER "integer"; ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user; @@ -65,7 +66,8 @@ DECLARE objtype text; BEGIN FOR objtype IN VALUES ('toast table'), ('index column'), ('sequence column'), - ('toast table column'), ('view column'), ('materialized view column') + ('toast table column'), ('view column'), ('materialized view column'), + ('property graph element'), ('property graph label'), ('property graph property') LOOP BEGIN PERFORM pg_get_object_address(objtype, '{one}', '{}'); @@ -90,7 +92,7 @@ DECLARE BEGIN FOR objtype IN VALUES ('table'), ('index'), ('sequence'), ('view'), - ('materialized view'), ('foreign table'), + ('materialized view'), ('foreign table'), ('property graph'), ('table column'), ('foreign table column'), ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'), ('table constraint'), ('domain constraint'), ('conversion'), ('default value'), @@ -163,6 +165,7 @@ WITH objects (type, name, args) AS (VALUES ('view', '{addr_nsp, genview}', '{}'), ('materialized view', '{addr_nsp, genmatview}', '{}'), ('foreign table', '{addr_nsp, genftable}', '{}'), + ('property graph', '{addr_nsp, gengraph}', '{}'), ('table column', '{addr_nsp, gentable, b}', '{}'), ('foreign table column', '{addr_nsp, genftable, a}', '{}'), ('aggregate', '{addr_nsp, genaggr}', '{int4}'), @@ -176,7 +179,7 @@ WITH objects (type, name, args) AS (VALUES ('collation', '{default}', '{}'), ('table constraint', '{addr_nsp, gentable, a_chk}', '{}'), ('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'), - ('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'), + ('conversion', '{pg_catalog, koi8_r_to_utf8}', '{}'), ('default value', '{addr_nsp, gentable, b}', '{}'), ('language', '{plpgsql}', '{}'), -- large object @@ -278,6 +281,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES ('pg_event_trigger'::regclass, 0, 0), -- no event trigger ('pg_parameter_acl'::regclass, 0, 0), -- no parameter ACL ('pg_policy'::regclass, 0, 0), -- no policy + ('pg_propgraph_element'::regclass, 0, 0), -- no property graph element + ('pg_propgraph_label'::regclass, 0, 0), -- no property graph label + ('pg_propgraph_property'::regclass, 0, 0), -- no property graph property ('pg_publication'::regclass, 0, 0), -- no publication ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace ('pg_publication_rel'::regclass, 0, 0), -- no publication relation diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql new file mode 100644 index 0000000000000..44cb79dd5528a --- /dev/null +++ b/src/test/regress/sql/oid8.sql @@ -0,0 +1,87 @@ +-- +-- OID8 +-- + +CREATE TABLE OID8_TBL(f1 oid8); + +INSERT INTO OID8_TBL(f1) VALUES ('1234'); +INSERT INTO OID8_TBL(f1) VALUES ('1235'); +INSERT INTO OID8_TBL(f1) VALUES ('987'); +INSERT INTO OID8_TBL(f1) VALUES ('-1040'); +INSERT INTO OID8_TBL(f1) VALUES ('99999999'); +INSERT INTO OID8_TBL(f1) VALUES ('5 '); +INSERT INTO OID8_TBL(f1) VALUES (' 10 '); +INSERT INTO OID8_TBL(f1) VALUES ('123456789012345678'); +-- UINT64_MAX +INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551615'); +-- leading/trailing hard tab is also allowed +INSERT INTO OID8_TBL(f1) VALUES (' 15 '); + +-- bad inputs +INSERT INTO OID8_TBL(f1) VALUES (''); +INSERT INTO OID8_TBL(f1) VALUES (' '); +INSERT INTO OID8_TBL(f1) VALUES ('asdfasd'); +INSERT INTO OID8_TBL(f1) VALUES ('99asdfasd'); +INSERT INTO OID8_TBL(f1) VALUES ('5 d'); +INSERT INTO OID8_TBL(f1) VALUES (' 5d'); +INSERT INTO OID8_TBL(f1) VALUES ('5 5'); +INSERT INTO OID8_TBL(f1) VALUES (' - 500'); +INSERT INTO OID8_TBL(f1) VALUES ('3908203590239580293850293850329485'); +INSERT INTO OID8_TBL(f1) VALUES ('-1204982019841029840928340329840934'); +-- UINT64_MAX + 1 +INSERT INTO OID8_TBL(f1) VALUES ('18446744073709551616'); + +SELECT * FROM OID8_TBL; + +-- Also try it with non-error-throwing API +SELECT pg_input_is_valid('1234', 'oid8'); +SELECT pg_input_is_valid('01XYZ', 'oid8'); +SELECT * FROM pg_input_error_info('01XYZ', 'oid8'); +SELECT pg_input_is_valid('3908203590239580293850293850329485', 'oid8'); +SELECT * FROM pg_input_error_info('-1204982019841029840928340329840934', 'oid8'); + +-- Operators +SELECT o.* FROM OID8_TBL o WHERE o.f1 = 1234; +SELECT o.* FROM OID8_TBL o WHERE o.f1 <> '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 <= '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 < '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 >= '1234'; +SELECT o.* FROM OID8_TBL o WHERE o.f1 > '1234'; + +-- Casts +SELECT 1::int2::oid8; +SELECT 1::int4::oid8; +SELECT 1::int8::oid8; +SELECT 1::oid8::int8; +SELECT 1::oid::oid8; -- ok +SELECT 1::oid8::oid; -- not ok + +-- Aggregates +SELECT min(f1), max(f1) FROM OID8_TBL; + +-- Check btree and hash opclasses +EXPLAIN (COSTS OFF) +SELECT DISTINCT (i || '000000000000' || j)::oid8 f + FROM generate_series(1, 10) i, + generate_series(1, 10) j, + generate_series(1, 5) k + WHERE i <= 10 AND j > 0 AND j <= 10 + ORDER BY f; + +SELECT DISTINCT (i || '000000000000' || j)::oid8 f + FROM generate_series(1, 10) i, + generate_series(1, 10) j, + generate_series(1, 5) k + WHERE i <= 10 AND j > 0 AND j <= 10 + ORDER BY f; + +-- 3-way compare for btrees +SELECT btoid8cmp(1::oid8, 2::oid8) < 0 AS val_lower; +SELECT btoid8cmp(2::oid8, 2::oid8) = 0 AS val_equal; +SELECT btoid8cmp(2::oid8, 1::oid8) > 0 AS val_higher; + +-- oid8 has btree and hash opclasses +CREATE INDEX on OID8_TBL USING btree(f1); +CREATE INDEX ON OID8_TBL USING hash(f1); + +DROP TABLE OID8_TBL; diff --git a/src/test/regress/sql/opr_sanity.sql b/src/test/regress/sql/opr_sanity.sql index 2fb3a85287816..cd674d7dbca38 100644 --- a/src/test/regress/sql/opr_sanity.sql +++ b/src/test/regress/sql/opr_sanity.sql @@ -847,7 +847,7 @@ WHERE aggfnoid = 0 OR aggtransfn = 0 OR (aggkind = 'n' AND aggnumdirectargs > 0) OR aggfinalmodify NOT IN ('r', 's', 'w') OR aggmfinalmodify NOT IN ('r', 's', 'w') OR - aggtranstype = 0 OR aggtransspace < 0 OR aggmtransspace < 0; + aggtranstype = 0 OR aggmtransspace < 0; -- Make sure the matching pg_proc entry is sensible, too. diff --git a/src/test/regress/sql/partition_aggregate.sql b/src/test/regress/sql/partition_aggregate.sql index ab070fee24496..7c725e2663a58 100644 --- a/src/test/regress/sql/partition_aggregate.sql +++ b/src/test/regress/sql/partition_aggregate.sql @@ -14,6 +14,8 @@ SET enable_partitionwise_join TO true; SET max_parallel_workers_per_gather TO 0; -- Disable incremental sort, which can influence selected plans due to fuzz factor. SET enable_incremental_sort TO off; +-- Disable eager aggregation, which can interfere with the generation of partitionwise aggregation. +SET enable_eager_aggregate TO off; -- -- Tests for list partitioned tables. @@ -74,6 +76,11 @@ EXPLAIN (COSTS OFF) SELECT a FROM pagg_tab WHERE a < 3 GROUP BY a ORDER BY 1; SELECT a FROM pagg_tab WHERE a < 3 GROUP BY a ORDER BY 1; +-- Test partitionwise aggregation with ordered append path built from fractional paths +EXPLAIN (COSTS OFF) +SELECT count(*) FROM pagg_tab GROUP BY c ORDER BY c LIMIT 1; +SELECT count(*) FROM pagg_tab GROUP BY c ORDER BY c LIMIT 1; + RESET enable_hashagg; -- ROLLUP, partitionwise aggregation does not apply diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql index b5060bec7f0e1..2ef65292babc0 100644 --- a/src/test/regress/sql/partition_info.sql +++ b/src/test/regress/sql/partition_info.sql @@ -127,3 +127,11 @@ SELECT pg_partition_root('ptif_li_child'); DROP VIEW ptif_test_view; DROP MATERIALIZED VIEW ptif_test_matview; DROP TABLE ptif_li_parent, ptif_li_child; + +-- Test about selection of arbiter indexes for partitioned tables with +-- non-valid index on the parent table +CREATE TABLE pt (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE TABLE p1 PARTITION OF pt FOR VALUES FROM (1) to (2) PARTITION BY RANGE (a); +CREATE TABLE p1_1 PARTITION OF p1 FOR VALUES FROM (1) TO (2); +CREATE UNIQUE INDEX ON ONLY p1 (a); +INSERT INTO p1 VALUES (1) ON CONFLICT (a) DO NOTHING; diff --git a/src/test/regress/sql/partition_join.sql b/src/test/regress/sql/partition_join.sql index 30f15ee9acb5a..c4549fc1ad8e7 100644 --- a/src/test/regress/sql/partition_join.sql +++ b/src/test/regress/sql/partition_join.sql @@ -35,9 +35,14 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.b = 0 ORDER BY t1.a, t2.b; -- inner join with partially-redundant join clauses +-- (avoid a mergejoin, because the planner thinks that a non-partitionwise +-- merge join is the cheapest plan, and we want to test a partitionwise join) +BEGIN; +SET LOCAL enable_mergejoin = false; EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = t2.b ORDER BY t1.a, t2.b; SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.a AND t1.a = t2.b ORDER BY t1.a, t2.b; +COMMIT; -- left outer join, 3-way EXPLAIN (COSTS OFF) @@ -219,13 +224,14 @@ EXPLAIN (COSTS OFF) SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; --- test merge joins +-- test merge joins, slightly modifying the query to ensure that we still +-- get a fully partitionwise join SET enable_hashjoin TO off; SET enable_nestloop TO off; EXPLAIN (COSTS OFF) -SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; -SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) AND t1.b = 0 ORDER BY t1.a; +SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) ORDER BY t1.a; +SELECT t1.* FROM prt1 t1 WHERE t1.a IN (SELECT t1.b FROM prt2 t1 WHERE t1.b IN (SELECT (t1.a + t1.b)/2 FROM prt1_e t1 WHERE t1.c = 0)) ORDER BY t1.a; EXPLAIN (COSTS OFF) SELECT t1.a, t1.c, t2.b, t2.c, t3.a + t3.b, t3.c FROM (prt1 t1 LEFT JOIN prt2 t2 ON t1.a = t2.b) RIGHT JOIN prt1_e t3 ON (t1.a = (t3.a + t3.b)/2) WHERE t3.c = 0 ORDER BY t1.a, t2.b, t3.a + t3.b; @@ -1155,8 +1161,8 @@ ANALYZE plt3_adv; -- merged partition when re-called with plt1_adv_p1 for the second list value -- '0001' of that partition EXPLAIN (COSTS OFF) -SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; -SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; +SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.a = t3.a AND t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; +SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM (plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a = t2.a AND t1.c = t2.c)) FULL JOIN plt3_adv t3 ON (t1.a = t3.a AND t1.c = t3.c) WHERE coalesce(t1.a, 0) % 5 != 3 AND coalesce(t1.a, 0) % 5 != 4 ORDER BY t1.c, t1.a, t2.a, t3.a; DROP TABLE plt1_adv; DROP TABLE plt2_adv; @@ -1216,8 +1222,10 @@ INSERT INTO fract_t (id) (SELECT generate_series(0, 1999)); ANALYZE fract_t; -- verify plan; nested index only scans +-- (avoid merge joins, because the costs of partitionwise and non-partitionwise +-- merge joins tend to be almost equal, and we want this test to be stable) SET max_parallel_workers_per_gather = 0; -SET enable_partitionwise_join = on; +SET enable_mergejoin = off; EXPLAIN (COSTS OFF) SELECT x.id, y.id FROM fract_t x LEFT JOIN fract_t y USING (id) ORDER BY x.id ASC LIMIT 10; @@ -1240,6 +1248,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM pht1 p1 JOIN pht1 p2 USING (c) LIMIT 100; -- If almost all the data should be fetched - prefer SeqScan EXPLAIN (COSTS OFF) SELECT * FROM pht1 p1 JOIN pht1 p2 USING (c) LIMIT 1000; +RESET enable_mergejoin; SET max_parallel_workers_per_gather = 1; SET debug_parallel_query = on; -- Partial paths should also be smart enough to employ limits diff --git a/src/test/regress/sql/partition_merge.sql b/src/test/regress/sql/partition_merge.sql new file mode 100644 index 0000000000000..a211fee2ad160 --- /dev/null +++ b/src/test/regress/sql/partition_merge.sql @@ -0,0 +1,791 @@ +-- +-- PARTITIONS_MERGE +-- Tests for "ALTER TABLE ... MERGE PARTITIONS ..." command +-- + +CREATE SCHEMA partitions_merge_schema; +CREATE SCHEMA partitions_merge_schema2; +SET search_path = partitions_merge_schema, public; + +-- +-- BY RANGE partitioning +-- + +-- +-- Test for error codes +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_dec2021 PARTITION OF sales_range FOR VALUES FROM ('2021-12-01') TO ('2021-12-31'); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); + +CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_apr_1 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-15'); +CREATE TABLE sales_apr_2 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-15') TO ('2022-05-01'); +ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); + +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_feb2022) INTO sales_feb_mar_apr2022; +-- ERROR: "sales_apr2022" is not a table +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO sales_feb_mar_apr2022; +-- ERROR: can not merge partition "sales_mar2022" together with partition "sales_jan2022" +-- DETAIL: lower bound of partition "sales_mar2022" is not equal to the upper bound of partition "sales_jan2022" +-- (space between sections sales_jan2022 and sales_mar2022) +ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_mar2022) INTO sales_jan_mar2022; +-- ERROR: can not merge partition "sales_jan2022" together with partition "sales_dec2021" +-- DETAIL: lower bound of partition "sales_jan2022" is not equal to the upper bound of partition "sales_dec2021" +-- (space between sections sales_dec2021 and sales_jan2022) +ALTER TABLE sales_range MERGE PARTITIONS (sales_dec2021, sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022; +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, partitions_merge_schema.sales_feb2022) INTO sales_feb_mar_apr2022; +--ERROR, sales_apr_2 already exists +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_jan2022) INTO sales_apr_2; + +CREATE VIEW jan2022v as SELECT * FROM sales_jan2022; +ALTER TABLE sales_range MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_dec_jan_feb2022; +DROP VIEW jan2022v; + +-- NO ERROR: test for custom partitions order, source partitions not in the search_path +SET search_path = partitions_merge_schema2, public; +ALTER TABLE partitions_merge_schema.sales_range MERGE PARTITIONS ( + partitions_merge_schema.sales_feb2022, + partitions_merge_schema.sales_mar2022, + partitions_merge_schema.sales_jan2022) INTO sales_jan_feb_mar2022; +SET search_path = partitions_merge_schema, public; + +PREPARE get_partition_info(regclass[]) AS +SELECT c.oid::pg_catalog.regclass, + c.relpersistence, + c.relkind, + i.inhdetachpending, + pg_catalog.pg_get_expr(c.relpartbound, c.oid) +FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i +WHERE c.oid = i.inhrelid AND i.inhparent = ANY($1) +ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', + c.oid::regclass::text COLLATE "C"; + +EXECUTE get_partition_info('{sales_range}'); + +DROP TABLE sales_range; + +-- +-- Add rows into partitioned table, then merge partitions +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +SELECT pg_catalog.pg_get_partkeydef('sales_range'::regclass); + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_range}'); + +-- check schema-qualified name of the new partition +ALTER TABLE sales_range MERGE PARTITIONS (sales_feb2022, sales_mar2022, sales_apr2022) INTO partitions_merge_schema2.sales_feb_mar_apr2022; + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_range}'); + +SELECT * FROM pg_indexes WHERE tablename = 'sales_feb_mar_apr2022' and schemaname = 'partitions_merge_schema2'; + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +-- Use indexscan for testing indexes +SET enable_seqscan = OFF; + +EXPLAIN (COSTS OFF) SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022 where sales_date > '2022-01-01'; +SELECT * FROM partitions_merge_schema2.sales_feb_mar_apr2022 where sales_date > '2022-01-01'; + +RESET enable_seqscan; + +DROP TABLE sales_range; + +-- +-- Merge some partitions into DEFAULT partition +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); +CREATE TABLE sales_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +-- Merge partitions (include DEFAULT partition) into partition with the same +-- name +ALTER TABLE sales_range MERGE PARTITIONS + (sales_jan2022, sales_mar2022, partitions_merge_schema.sales_others) INTO sales_others; + +SELECT * FROM sales_others ORDER BY salesperson_id; + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_range}'); + +DROP TABLE sales_range; + +-- +-- Test for: +-- * composite partition key; +-- * GENERATED column; +-- * column with DEFAULT value. +-- +CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT, + sales_date VARCHAR(10) GENERATED ALWAYS AS + (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED, + sales_department VARCHAR(30) DEFAULT 'Sales department') + PARTITION BY RANGE (sales_year, sales_month, sales_day); + +CREATE TABLE sales_dec2022 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1); +CREATE TABLE sales_jan2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1); +CREATE TABLE sales_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1); +CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE); + +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2021, 12, 7), + ('Manager2', 2021, 12, 8), + ('Manager3', 2022, 1, 1), + ('Manager1', 2022, 2, 4), + ('Manager2', 2022, 1, 2), + ('Manager3', 2022, 2, 1), + ('Manager1', 2022, 3, 3), + ('Manager2', 2022, 3, 4), + ('Manager3', 2022, 5, 1); + +SELECT tableoid::regclass, * FROM sales_date; + +ALTER TABLE sales_date MERGE PARTITIONS (sales_jan2022, sales_feb2022) INTO sales_jan_feb2022; + +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2022, 1, 10), + ('Manager2', 2022, 2, 10); + +SELECT tableoid::regclass, * FROM sales_date; +DROP TABLE sales_date; + +-- +-- Test: merge partitions of partitioned table with triggers +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); + +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +CREATE TABLE salespeople10_20 PARTITION OF salespeople FOR VALUES FROM (10) TO (20); +CREATE TABLE salespeople20_30 PARTITION OF salespeople FOR VALUES FROM (20) TO (30); +CREATE TABLE salespeople30_40 PARTITION OF salespeople FOR VALUES FROM (30) TO (40); + +INSERT INTO salespeople VALUES (1, 'Poirot'); + +CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN NULL; +END; +$BODY$; + +CREATE TRIGGER salespeople_after_insert_statement_trigger + AFTER INSERT + ON salespeople + FOR EACH STATEMENT + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); + +CREATE TRIGGER salespeople_after_insert_row_trigger + AFTER INSERT + ON salespeople + FOR EACH ROW + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); + +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (10, 'May'); +-- 1 trigger should fire here (row): +INSERT INTO salespeople10_20 VALUES (19, 'Ivanov'); + +ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40; + +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (20, 'Smirnoff'); +-- 1 trigger should fire here (row): +INSERT INTO salespeople10_40 VALUES (30, 'Ford'); + +SELECT * FROM salespeople01_10; +SELECT * FROM salespeople10_40; + +DROP TABLE salespeople; +DROP FUNCTION after_insert_row_trigger(); + +-- +-- Test: merge partitions with deleted columns +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); + +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +-- Create partitions with some deleted columns: +CREATE TABLE salespeople10_20(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)); +CREATE TABLE salespeople20_30(salesperson_id INT PRIMARY KEY, d2 INT, salesperson_name VARCHAR(30)); +CREATE TABLE salespeople30_40(salesperson_id INT PRIMARY KEY, d3 DATE, salesperson_name VARCHAR(30)); + +INSERT INTO salespeople10_20 VALUES ('dummy value 1', 19, 'Ivanov'); +INSERT INTO salespeople20_30 VALUES (20, 101, 'Smirnoff'); +INSERT INTO salespeople30_40 VALUES (31, now(), 'Popov'); + +ALTER TABLE salespeople10_20 DROP COLUMN d1; +ALTER TABLE salespeople20_30 DROP COLUMN d2; +ALTER TABLE salespeople30_40 DROP COLUMN d3; + +ALTER TABLE salespeople ATTACH PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20); +ALTER TABLE salespeople ATTACH PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30); +ALTER TABLE salespeople ATTACH PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40); + +INSERT INTO salespeople VALUES + (1, 'Poirot'), + (10, 'May'), + (30, 'Ford'); + +ALTER TABLE salespeople MERGE PARTITIONS (salespeople10_20, salespeople20_30, salespeople30_40) INTO salespeople10_40; + +select * from salespeople; +select * from salespeople01_10; +select * from salespeople10_40; + +DROP TABLE salespeople; + +-- +-- Test: merge sub-partitions +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); + +CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_apr2022_01_10 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'); +CREATE TABLE sales_apr2022_10_20 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20'); +CREATE TABLE sales_apr2022_20_30 PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01'); +ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); + +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +ALTER TABLE sales_apr2022 MERGE PARTITIONS (sales_apr2022_01_10, sales_apr2022_10_20, sales_apr2022_20_30) INTO sales_apr_all; + +SELECT tableoid::regclass, * FROM sales_apr2022 ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE sales_range; + +-- +-- BY LIST partitioning +-- + +-- +-- Test: specific errors for BY LIST partitioning +-- +CREATE TABLE sales_list +(salesperson_id INT GENERATED ALWAYS AS IDENTITY, + salesperson_name VARCHAR(30), + sales_state VARCHAR(20), + sales_amount INT, + sales_date DATE) +PARTITION BY LIST (sales_state); +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid'); +CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'); +CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; + + +CREATE TABLE sales_list2 (LIKE sales_list) PARTITION BY LIST (sales_state); +CREATE TABLE sales_nord2 PARTITION OF sales_list2 FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_others2 PARTITION OF sales_list2 DEFAULT; + + +CREATE TABLE sales_external (LIKE sales_list); +CREATE TABLE sales_external2 (vch VARCHAR(5)); + +-- ERROR: "sales_external" is not a partition of partitioned table "sales_list" +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external) INTO sales_all; +-- ERROR: "sales_external2" is not a partition of partitioned table "sales_list" +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_external2) INTO sales_all; +-- ERROR: relation "sales_nord2" is not a partition of relation "sales_list" +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_nord2, sales_east) INTO sales_all; + +DROP TABLE sales_external2; +DROP TABLE sales_external; +DROP TABLE sales_list2; +DROP TABLE sales_list; + +-- +-- Test: BY LIST partitioning, MERGE PARTITIONS with data +-- +CREATE TABLE sales_list +(salesperson_id INT GENERATED ALWAYS AS IDENTITY, + salesperson_name VARCHAR(30), + sales_state VARCHAR(20), + sales_amount INT, + sales_date DATE) +PARTITION BY LIST (sales_state); + +CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name); +CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state); + +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_west PARTITION OF sales_list FOR VALUES IN ('Lisbon', 'New York', 'Madrid'); +CREATE TABLE sales_east PARTITION OF sales_list FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'); +CREATE TABLE sales_central PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; + +INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES + ('Trump', 'Beijing', 1000, '2022-03-01'), + ('Smirnoff', 'New York', 500, '2022-03-03'), + ('Ford', 'St. Petersburg', 2000, '2022-03-05'), + ('Ivanov', 'Warsaw', 750, '2022-03-04'), + ('Deev', 'Lisbon', 250, '2022-03-07'), + ('Poirot', 'Berlin', 1000, '2022-03-01'), + ('May', 'Helsinki', 1200, '2022-03-06'), + ('Li', 'Vladivostok', 1150, '2022-03-09'), + ('May', 'Helsinki', 1200, '2022-03-11'), + ('Halder', 'Oslo', 800, '2022-03-02'), + ('Muller', 'Madrid', 650, '2022-03-05'), + ('Smith', 'Kyiv', 350, '2022-03-10'), + ('Gandi', 'Warsaw', 150, '2022-03-08'), + ('Plato', 'Lisbon', 950, '2022-03-05'); + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_list}'); + +ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central) INTO sales_all; + +-- show partitions with conditions: +EXECUTE get_partition_info('{sales_list}'); + +SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +-- Use indexscan for testing indexes after merging partitions +SET enable_seqscan = OFF; + +EXPLAIN (COSTS OFF) SELECT * FROM sales_all WHERE sales_state = 'Warsaw'; +SELECT * FROM sales_all WHERE sales_state = 'Warsaw'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; +SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; +SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; + +RESET enable_seqscan; + +DROP TABLE sales_list; + +-- +-- Try to MERGE partitions of another table. +-- +CREATE TABLE t1 (i int, a int, b int, c int) PARTITION BY RANGE (a, b); +CREATE TABLE t1p1 PARTITION OF t1 FOR VALUES FROM (1, 1) TO (1, 2); +CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t); +CREATE TABLE t2pa PARTITION OF t2 FOR VALUES FROM ('A') TO ('C'); +CREATE TABLE t3 (i int, t text); + +-- ERROR: relation "t1p1" is not a partition of relation "t2" +ALTER TABLE t2 MERGE PARTITIONS (t1p1, t2pa) INTO t2p; +-- ERROR: "t3" is not a partition of partitioned table "t2" +ALTER TABLE t2 MERGE PARTITIONS (t2pa, t3) INTO t2p; + +DROP TABLE t3; +DROP TABLE t2; +DROP TABLE t1; + + +-- +-- Check the partition index name if the partition name is the same as one +-- of the merged partitions. +-- +CREATE TABLE t (i int, PRIMARY KEY(i)) PARTITION BY RANGE (i); + +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); + +CREATE INDEX tidx ON t(i); +ALTER TABLE t MERGE PARTITIONS (tp_1_2, tp_0_1) INTO tp_1_2; + +-- Indexname values should be 'tp_1_2_pkey' and 'tp_1_2_i_idx'. +\d+ tp_1_2 + +DROP TABLE t; + +-- +-- Try to MERGE partitions of temporary table. +-- +BEGIN; +SHOW search_path; +CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i) ON COMMIT DROP; +CREATE TEMP TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TEMP TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +CREATE TEMP TABLE tp_2_3 PARTITION OF t FOR VALUES FROM (2) TO (3); +CREATE TEMP TABLE tp_3_4 PARTITION OF t FOR VALUES FROM (3) TO (4); + +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO pg_temp.tp_0_2; +ALTER TABLE t MERGE PARTITIONS (tp_0_2, tp_2_3) INTO pg_temp.tp_0_3; + +-- Partition should be temporary. +EXECUTE get_partition_info('{t}'); +-- ERROR: cannot create a permanent relation as partition of temporary relation "t" +ALTER TABLE t MERGE PARTITIONS (tp_0_3, tp_3_4) INTO tp_0_4; +ROLLBACK; + +-- +-- Try mixing permanent and temporary partitions. +-- +BEGIN; +SET search_path = partitions_merge_schema, pg_temp, public; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); + +SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass; +EXECUTE get_partition_info('{t}'); +SAVEPOINT s; + +SET search_path = pg_temp, partitions_merge_schema, public; +-- Can't merge persistent partitions into a temporary partition +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; + +ROLLBACK TO SAVEPOINT s; +SET search_path = partitions_merge_schema, public; +-- Can't merge persistent partitions into a temporary partition +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO pg_temp.tp_0_2; +ROLLBACK; + +BEGIN; +SET search_path = pg_temp, partitions_merge_schema, public; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); + +SELECT c.oid::pg_catalog.regclass, c.relpersistence FROM pg_catalog.pg_class c WHERE c.oid = 't'::regclass; +EXECUTE get_partition_info('{t}'); + +SET search_path = partitions_merge_schema, pg_temp, public; + +-- Can't merge temporary partitions into a persistent partition +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +ROLLBACK; + +DEALLOCATE get_partition_info; + +-- Check the new partition inherits parent's tablespace +SET search_path = partitions_merge_schema, public; +CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace) + PARTITION BY RANGE (i) TABLESPACE regress_tblspace; +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +SELECT tablename, tablespace FROM pg_tables + WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema' + ORDER BY tablename COLLATE "C", tablespace COLLATE "C"; +SELECT tablename, indexname, tablespace FROM pg_indexes + WHERE tablename IN ('t', 'tp_0_2') AND schemaname = 'partitions_merge_schema' + ORDER BY tablename COLLATE "C", indexname COLLATE "C", tablespace COLLATE "C"; +DROP TABLE t; + +-- Check the new partition inherits parent's table access method +SET search_path = partitions_merge_schema, public; +CREATE ACCESS METHOD partitions_merge_heap TYPE TABLE HANDLER heap_tableam_handler; +CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partitions_merge_heap; +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +SELECT c.relname, a.amname +FROM pg_class c JOIN pg_am a ON c.relam = a.oid +WHERE c.oid IN ('t'::regclass, 'tp_0_2'::regclass) +ORDER BY c.relname COLLATE "C"; +DROP TABLE t; +DROP ACCESS METHOD partitions_merge_heap; + +-- Test permission checks. The user needs to own the parent table and all +-- the merging partitions to do the merge. +CREATE ROLE regress_partition_merge_alice; +CREATE ROLE regress_partition_merge_bob; +GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_alice; +GRANT ALL ON SCHEMA partitions_merge_schema TO regress_partition_merge_bob; + +SET SESSION AUTHORIZATION regress_partition_merge_alice; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); + +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- ERROR: must be owner of table t +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +RESET SESSION AUTHORIZATION; + +ALTER TABLE t OWNER TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- ERROR: must be owner of table tp_0_1 +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +RESET SESSION AUTHORIZATION; + +ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- ERROR: must be owner of table tp_1_2 +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +RESET SESSION AUTHORIZATION; + +ALTER TABLE tp_1_2 OWNER TO regress_partition_merge_bob; +SET SESSION AUTHORIZATION regress_partition_merge_bob; +-- Ok: +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; +RESET SESSION AUTHORIZATION; + +DROP TABLE t; + +-- Test: we can't merge partitions with different owners +CREATE TABLE tp_0_1(i int); +ALTER TABLE tp_0_1 OWNER TO regress_partition_merge_alice; +CREATE TABLE tp_1_2(i int); +ALTER TABLE tp_1_2 OWNER TO regress_partition_merge_bob; + +CREATE TABLE t (i int) PARTITION BY RANGE (i); + +ALTER TABLE t ATTACH PARTITION tp_0_1 FOR VALUES FROM (0) TO (1); +ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2); + +-- Owner is 'regress_partition_merge_alice': +\dt tp_0_1 +-- Owner is 'regress_partition_merge_bob': +\dt tp_1_2 + +-- ERROR: partitions being merged have different owners +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; + +DROP TABLE t; +REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_alice; +REVOKE ALL ON SCHEMA partitions_merge_schema FROM regress_partition_merge_bob; +DROP ROLE regress_partition_merge_alice; +DROP ROLE regress_partition_merge_bob; + + +-- Test for hash partitioned table +CREATE TABLE t (i int) PARTITION BY HASH(i); +CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1); + +-- ERROR: partition of hash-partitioned table cannot be merged +ALTER TABLE t MERGE PARTITIONS (tp1, tp2) INTO tp3; + +-- ERROR: list of partitions to be merged should include at least two partitions +ALTER TABLE t MERGE PARTITIONS (tp1) INTO tp3; + +DROP TABLE t; + + +-- Test for merged partition properties: +-- * STATISTICS is empty +-- * COMMENT is empty +-- * DEFAULTS are the same as DEFAULTS for partitioned table +-- * STORAGE is the same as STORAGE for partitioned table +-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table +-- * TRIGGERS are the same as TRIGGERS for partitioned table +\set HIDE_TOAST_COMPRESSION false + +CREATE TABLE t +(i int NOT NULL, + t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t', + b bigint, + d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i)); +COMMENT ON COLUMN t.i IS 't1.i'; + +CREATE TABLE tp_0_1 +(i int NOT NULL, + t text STORAGE MAIN DEFAULT 'default_tp_0_1', + b bigint, + d date GENERATED ALWAYS as ('2022-02-02') STORED); +ALTER TABLE t ATTACH PARTITION tp_0_1 FOR VALUES FROM (0) TO (1); +COMMENT ON COLUMN tp_0_1.i IS 'tp_0_1.i'; + +CREATE TABLE tp_1_2 +(i int NOT NULL, + t text STORAGE MAIN DEFAULT 'default_tp_1_2', + b bigint, + d date GENERATED ALWAYS as ('2022-03-03') STORED); +ALTER TABLE t ATTACH PARTITION tp_1_2 FOR VALUES FROM (1) TO (2); +COMMENT ON COLUMN tp_1_2.i IS 'tp_1_2.i'; + +CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t; +CREATE STATISTICS tp_0_1_stat (DEPENDENCIES) on i, b from tp_0_1; +CREATE STATISTICS tp_1_2_stat (DEPENDENCIES) on i, b from tp_1_2; + +ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0); +ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED; +ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID; +ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID; + +INSERT INTO tp_0_1(i, t, b) VALUES(0, DEFAULT, 1); +INSERT INTO tp_1_2(i, t, b) VALUES(1, DEFAULT, 2); +CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS +$BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN new; +END; +$BODY$; + +CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW + EXECUTE PROCEDURE trigger_function('t'); +CREATE TRIGGER tp_0_1_before_insert_row_trigger BEFORE INSERT ON tp_0_1 FOR EACH ROW + EXECUTE PROCEDURE trigger_function('tp_0_1'); +CREATE TRIGGER tp_1_2_before_insert_row_trigger BEFORE INSERT ON tp_1_2 FOR EACH ROW + EXECUTE PROCEDURE trigger_function('tp_1_2'); + +\d+ tp_0_1 +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_1; +\d+ tp_0_1 + +INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3); +SELECT tableoid::regclass, * FROM t ORDER BY b; +DROP TABLE t; +DROP FUNCTION trigger_function(); +\set HIDE_TOAST_COMPRESSION true + + +-- Test MERGE PARTITIONS with not valid foreign key constraint +CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +INSERT INTO t VALUES (0), (1); +CREATE TABLE t_fk (i INT); +INSERT INTO t_fk VALUES (1), (2); +ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT VALID; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; + +-- Should be NOT VALID FOREIGN KEY +\d tp_0_2 +-- ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +ALTER TABLE t_fk VALIDATE CONSTRAINT t_fk_i_fkey; + +DROP TABLE t_fk; +DROP TABLE t; + +-- Test MERGE PARTITIONS with not enforced foreign key constraint +CREATE TABLE t (i INT PRIMARY KEY) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +INSERT INTO t VALUES (0), (1); +CREATE TABLE t_fk (i INT); +INSERT INTO t_fk VALUES (1), (2); + +ALTER TABLE t_fk ADD CONSTRAINT t_fk_i_fkey FOREIGN KEY (i) REFERENCES t NOT ENFORCED; +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; + +-- Should be NOT ENFORCED FOREIGN KEY +\d tp_0_2 +-- ERROR: insert or update on table "t_fk" violates foreign key constraint "t_fk_i_fkey" +ALTER TABLE t_fk ALTER CONSTRAINT t_fk_i_fkey ENFORCED; + +DROP TABLE t_fk; +DROP TABLE t; + + +-- Test for recomputation of stored generated columns. +CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i); +CREATE TABLE tp_0_1 PARTITION OF t FOR VALUES FROM (0) TO (1); +CREATE TABLE tp_1_2 PARTITION OF t FOR VALUES FROM (1) TO (2); +ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789); +INSERT INTO t VALUES (0), (1); + +-- Should be 0 because partition identifier for row with i=0 is different from +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + +-- "tab_id" column (stored generated column) with "tableoid" attribute requires +-- recomputation here. +ALTER TABLE t MERGE PARTITIONS (tp_0_1, tp_1_2) INTO tp_0_2; + +-- Should be 1 because partition identifier for row with i=0 is the same as +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + +DROP TABLE t; + + +-- Test for generated columns (different order of columns in partitioned table +-- and partitions). +CREATE TABLE t (i int, g int GENERATED ALWAYS AS (i + tableoid::int)) PARTITION BY RANGE (i); +CREATE TABLE tp_1 (g int GENERATED ALWAYS AS (i + tableoid::int), i int); +CREATE TABLE tp_2 (g int GENERATED ALWAYS AS (i + tableoid::int), i int); +ALTER TABLE t ATTACH PARTITION tp_1 FOR VALUES FROM (-1) TO (10); +ALTER TABLE t ATTACH PARTITION tp_2 FOR VALUES FROM (10) TO (20); +ALTER TABLE t ADD CHECK (g > 0); +ALTER TABLE t ADD CHECK (i > 0); +INSERT INTO t VALUES (5), (15); + +ALTER TABLE t MERGE PARTITIONS (tp_1, tp_2) INTO tp_12; + +INSERT INTO t VALUES (16); +-- ERROR: new row for relation "tp_12" violates check constraint "t_i_check" +INSERT INTO t VALUES (0); +-- Should be 3 rows: (5), (15), (16): +SELECT i FROM t ORDER BY i; +-- Should be 1 because for the same tableoid (15 + tableoid) = (5 + tableoid) + 10: +SELECT count(*) FROM t WHERE i = 15 AND g IN (SELECT g + 10 FROM t WHERE i = 5); + +DROP TABLE t; + + +RESET search_path; + +-- +DROP SCHEMA partitions_merge_schema; +DROP SCHEMA partitions_merge_schema2; diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql index d93c0c03babad..359a920805621 100644 --- a/src/test/regress/sql/partition_prune.sql +++ b/src/test/regress/sql/partition_prune.sql @@ -1447,3 +1447,74 @@ select min(a) over (partition by a order by a) from part_abc where a >= stable_o drop view part_abc_view; drop table part_abc; + +-- +-- Check that operands wrapped in PlaceHolderVars are matched to partition +-- keys, allowing partition pruning to occur. PlaceHolderVars can be +-- introduced when a subquery's output is used with grouping sets. +-- +create table phv_part (a int, b text) partition by list (a); +create table phv_part_1 partition of phv_part for values in (1); +create table phv_part_2 partition of phv_part for values in (2); +create table phv_part_null partition of phv_part for values in (null); +insert into phv_part values (1, 'one'), (2, 'two'), (null, 'null'); + +-- OpExpr: PHV-wrapped operand matched via equal() +explain (costs off) +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a = 1 + group by grouping sets (a, b); + +-- OpExpr with RelabelType: PHV wrapped around a casted column +explain (costs off) +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + +select * from (select a::oid as x, b from phv_part) t + where x::int = 1 + group by grouping sets (x, b); + +-- ScalarArrayOpExpr: IN clause with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a in (1, null) + group by grouping sets (a, b); + +-- NullTest: IS NULL with PHV-wrapped operand +explain (costs off) +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + +select * from (select a, b from phv_part) t + where a is null + group by grouping sets (a, b); + +drop table phv_part; + +-- BooleanTest: IS TRUE with PHV-wrapped boolean partition key +create table phv_boolpart (a bool, b text) partition by list (a); +create table phv_boolpart_t partition of phv_boolpart for values in (true); +create table phv_boolpart_f partition of phv_boolpart for values in (false); +create table phv_boolpart_null partition of phv_boolpart default; +insert into phv_boolpart values (true, 'yes'), (false, 'no'), (null, 'unknown'); + +explain (costs off) +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + +select * from (select a, b from phv_boolpart) t + where a is true + group by grouping sets (a, b); + +drop table phv_boolpart; diff --git a/src/test/regress/sql/partition_split.sql b/src/test/regress/sql/partition_split.sql new file mode 100644 index 0000000000000..44fcf208ac607 --- /dev/null +++ b/src/test/regress/sql/partition_split.sql @@ -0,0 +1,1134 @@ +-- +-- PARTITION_SPLIT +-- Tests for "ALTER TABLE ... SPLIT PARTITION ..." command +-- + +CREATE SCHEMA partition_split_schema; +CREATE SCHEMA partition_split_schema2; +SET search_path = partition_split_schema, public; + +-- +-- BY RANGE partitioning +-- + +-- +-- Test for error codes +-- +CREATE TABLE sales_range (salesperson_id int, sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +-- ERROR: relation "sales_xxx" does not exist +ALTER TABLE sales_range SPLIT PARTITION sales_xxx INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: relation "sales_jan2022" already exists +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: invalid bound specification for a range partition +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_jan2022 FOR VALUES IN ('2022-05-01', '2022-06-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: empty range bound specified for partition "sales_mar2022" +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-02-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +--ERROR: list of split partitions should contain at least two items +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-10-01')); + +-- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-01-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: partition with name "sales_feb_mar_apr2022" is already used +-- (We can create partition with the same name as split partition, but can't create two partitions with the same name) +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_feb_mar_apr2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: partition with name "sales_feb2022" is already used +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: ALTER action SPLIT PARTITION cannot be performed on relation "sales_feb_mar_apr2022" +-- DETAIL: This operation is not supported for tables. +ALTER TABLE sales_feb_mar_apr2022 SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_jan2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01')); + +-- ERROR: can not split to partition "sales_mar2022" together with partition "sales_feb2022" +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-02-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- Tests for spaces between partitions, them should be executed without DEFAULT partition +ALTER TABLE sales_range DETACH PARTITION sales_others; + +-- ERROR: lower bound of partition "sales_feb2022" is not equal to lower bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-02') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- Check the source partition not in the search path +SET search_path = partition_split_schema2, public; +ALTER TABLE partition_split_schema.sales_range +SPLIT PARTITION partition_split_schema.sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +SET search_path = partition_split_schema, public; +\d+ sales_range + +DROP TABLE sales_range; +DROP TABLE sales_others; + +-- Additional tests for error messages, no default partition +CREATE TABLE sales_range (sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); + +-- ERROR: upper bound of partition "sales_apr2022" is not equal to upper bound of split partition "sales_feb_mar_apr2022" +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-06-01')); + +DROP TABLE sales_range; + +-- +-- Add rows into partitioned table then split partition +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; +DROP TABLE sales_range CASCADE; + +-- +-- Add split partition, then add rows into partitioned table +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +-- Split partition, also check schema qualification of new partitions +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION partition_split_schema.sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION partition_split_schema2.sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); +\d+ sales_range + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE sales_range CASCADE; + +-- +-- Test for: +-- * composite partition key; +-- * GENERATED column; +-- * column with DEFAULT value. +-- +CREATE TABLE sales_date (salesperson_name VARCHAR(30), sales_year INT, sales_month INT, sales_day INT, + sales_date VARCHAR(10) GENERATED ALWAYS AS + (LPAD(sales_year::text, 4, '0') || '.' || LPAD(sales_month::text, 2, '0') || '.' || LPAD(sales_day::text, 2, '0')) STORED, + sales_department VARCHAR(30) DEFAULT 'Sales department') + PARTITION BY RANGE (sales_year, sales_month, sales_day); + +CREATE TABLE sales_dec2021 PARTITION OF sales_date FOR VALUES FROM (2021, 12, 1) TO (2022, 1, 1); +CREATE TABLE sales_jan_feb2022 PARTITION OF sales_date FOR VALUES FROM (2022, 1, 1) TO (2022, 3, 1); +CREATE TABLE sales_other PARTITION OF sales_date FOR VALUES FROM (2022, 3, 1) TO (MAXVALUE, MAXVALUE, MAXVALUE); + +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2021, 12, 7), + ('Manager2', 2021, 12, 8), + ('Manager3', 2022, 1, 1), + ('Manager1', 2022, 2, 4), + ('Manager2', 2022, 1, 2), + ('Manager3', 2022, 2, 1), + ('Manager1', 2022, 3, 3), + ('Manager2', 2022, 3, 4), + ('Manager3', 2022, 5, 1); + +SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid::regclass::text COLLATE "C", sales_year, sales_month, sales_day; + +ALTER TABLE sales_date SPLIT PARTITION sales_jan_feb2022 INTO + (PARTITION sales_jan2022 FOR VALUES FROM (2022, 1, 1) TO (2022, 2, 1), + PARTITION sales_feb2022 FOR VALUES FROM (2022, 2, 1) TO (2022, 3, 1)); + +INSERT INTO sales_date(salesperson_name, sales_year, sales_month, sales_day) VALUES + ('Manager1', 2022, 1, 10), + ('Manager2', 2022, 2, 10); + +SELECT tableoid::regclass, * FROM sales_date ORDER BY tableoid::regclass::text COLLATE "C", sales_year, sales_month, sales_day; + +DROP TABLE sales_date CASCADE; + +-- +-- Test: split DEFAULT partition; use an index on partition key; check index after split +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +SELECT * FROM sales_others; +SELECT * FROM pg_indexes WHERE tablename = 'sales_others' and schemaname = 'partition_split_schema' ORDER BY indexname COLLATE "C"; + +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'), + PARTITION sales_others DEFAULT); + +-- Use indexscan for testing indexes +SET enable_seqscan = OFF; + +EXPLAIN (COSTS OFF) SELECT * FROM sales_feb2022 where sales_date > '2022-01-01'; +SELECT * FROM sales_feb2022 where sales_date > '2022-01-01'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_mar2022 where sales_date > '2022-01-01'; +SELECT * FROM sales_mar2022 where sales_date > '2022-01-01'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_apr2022 where sales_date > '2022-01-01'; +SELECT * FROM sales_apr2022 where sales_date > '2022-01-01'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_others where sales_date > '2022-01-01'; +SELECT * FROM sales_others where sales_date > '2022-01-01'; + +RESET enable_seqscan; + +SELECT * FROM pg_indexes +WHERE tablename in ('sales_feb2022', 'sales_mar2022', 'sales_apr2022', 'sales_others') +AND schemaname = 'partition_split_schema' +ORDER BY indexname COLLATE "C"; + +DROP TABLE sales_range CASCADE; + +-- +-- Test: some cases for splitting DEFAULT partition (different bounds) +-- +CREATE TABLE sales_range (salesperson_id INT, sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +-- sales_error intersects with sales_dec2021 (lower bound) +-- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2021-12-30') TO ('2022-02-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +-- sales_error intersects with sales_feb2022 (upper bound) +-- ERROR: can not split to partition "sales_feb2022" together with partition "sales_error" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2022-01-01') TO ('2022-02-02'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +-- sales_error intersects with sales_dec2021 (inside bound) +-- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2021-12-10') TO ('2021-12-20'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +-- sales_error intersects with sales_dec2021 (exactly the same bounds) +-- ERROR: can not split to partition "sales_error" together with partition "sales_dec2021" +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_error FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +-- ERROR: can not split DEFAULT partition "sales_others" +-- HINT: To split DEFAULT partition one of the new partition must be DEFAULT. +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_jan2022 FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01')); + +-- no error: bounds of sales_noerror are between sales_dec2021 and sales_feb2022 +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_noerror FOR VALUES FROM ('2022-01-10') TO ('2022-01-20'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +DROP TABLE sales_range; + +CREATE TABLE sales_range (sales_date date) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +-- no error: bounds of sales_noerror are equal to lower and upper bounds of sales_dec2021 and sales_feb2022 +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_dec2021 FOR VALUES FROM ('2021-12-01') TO ('2022-01-01'), + PARTITION sales_noerror FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +DROP TABLE sales_range; + +-- +-- Test: split partition with CHECK and FOREIGN KEY CONSTRAINTs on partitioned table +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)); +INSERT INTO salespeople VALUES (1, 'Poirot'); + +CREATE TABLE sales_range ( +salesperson_id INT REFERENCES salespeople(salesperson_id), +sales_amount INT CHECK (sales_amount > 1), +sales_date DATE) PARTITION BY RANGE (sales_date); + +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb_mar_apr2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb_mar_apr2022'::regclass::oid ORDER BY conname COLLATE "C"; + +ALTER TABLE sales_range SPLIT PARTITION sales_feb_mar_apr2022 INTO + (PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_mar2022 FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'), + PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01')); + +-- We should see the same CONSTRAINTs as on sales_feb_mar_apr2022 partition +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_feb2022'::regclass::oid ORDER BY conname COLLATE "C"; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_mar2022'::regclass::oid ORDER BY conname COLLATE "C"; +SELECT pg_get_constraintdef(oid), conname, conkey FROM pg_constraint WHERE conrelid = 'sales_apr2022'::regclass::oid ORDER BY conname COLLATE "C"; + +-- ERROR: new row for relation "sales_mar2022" violates check constraint "sales_range_sales_amount_check" +INSERT INTO sales_range VALUES (1, 0, '2022-03-11'); +-- ERROR: insert or update on table "sales_mar2022" violates foreign key constraint "sales_range_salesperson_id_fkey" +INSERT INTO sales_range VALUES (-1, 10, '2022-03-11'); +-- ok +INSERT INTO sales_range VALUES (1, 10, '2022-03-11'); + +DROP TABLE sales_range CASCADE; +DROP TABLE salespeople CASCADE; + +-- +-- Test: split partition on partitioned table in case of existing FOREIGN KEY reference from another table +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); +CREATE TABLE sales (salesperson_id INT REFERENCES salespeople(salesperson_id), sales_amount INT, sales_date DATE); + +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40); + +INSERT INTO salespeople VALUES + (1, 'Poirot'), + (10, 'May'), + (19, 'Ivanov'), + (20, 'Smirnoff'), + (30, 'Ford'); + +INSERT INTO sales VALUES + (1, 100, '2022-03-01'), + (1, 110, '2022-03-02'), + (10, 150, '2022-03-01'), + (10, 90, '2022-03-03'), + (19, 200, '2022-03-04'), + (20, 50, '2022-03-12'), + (20, 170, '2022-03-02'), + (30, 30, '2022-03-04'); + +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO + (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20), + PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30), + PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40)); + +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +-- ERROR: insert or update on table "sales" violates foreign key constraint "sales_salesperson_id_fkey" +INSERT INTO sales VALUES (40, 50, '2022-03-04'); +-- ok +INSERT INTO sales VALUES (30, 50, '2022-03-04'); + +DROP TABLE sales CASCADE; +DROP TABLE salespeople CASCADE; + +-- +-- Test: split partition of partitioned table with triggers +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); + +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +CREATE TABLE salespeople10_40 PARTITION OF salespeople FOR VALUES FROM (10) TO (40); + +INSERT INTO salespeople VALUES (1, 'Poirot'); + +CREATE OR REPLACE FUNCTION after_insert_row_trigger() RETURNS trigger LANGUAGE 'plpgsql' AS $BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN NULL; +END; +$BODY$; + +CREATE TRIGGER salespeople_after_insert_statement_trigger + AFTER INSERT + ON salespeople + FOR EACH STATEMENT + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); + +CREATE TRIGGER salespeople_after_insert_row_trigger + AFTER INSERT + ON salespeople + FOR EACH ROW + EXECUTE PROCEDURE after_insert_row_trigger('salespeople'); + +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (10, 'May'); +-- 1 trigger should fire here (row): +INSERT INTO salespeople10_40 VALUES (19, 'Ivanov'); + +ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO + (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20), + PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30), + PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40)); + +-- 2 triggers should fire here (row + statement): +INSERT INTO salespeople VALUES (20, 'Smirnoff'); +-- 1 trigger should fire here (row): +INSERT INTO salespeople30_40 VALUES (30, 'Ford'); + +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE salespeople CASCADE; +DROP FUNCTION after_insert_row_trigger(); + +-- +-- Test: split partition witch identity column +-- If split partition column is identity column, columns of new partitions are identity columns too. +-- +CREATE TABLE salespeople(salesperson_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); + +CREATE TABLE salespeople1_2 PARTITION OF salespeople FOR VALUES FROM (1) TO (2); +-- Create new partition with identity column: +CREATE TABLE salespeople2_5(salesperson_id INT NOT NULL, salesperson_name VARCHAR(30)); +ALTER TABLE salespeople ATTACH PARTITION salespeople2_5 FOR VALUES FROM (2) TO (5); + +INSERT INTO salespeople (salesperson_name) VALUES ('Poirot'), ('Ivanov'); + +ALTER TABLE salespeople SPLIT PARTITION salespeople2_5 INTO + (PARTITION salespeople2_3 FOR VALUES FROM (2) TO (3), + PARTITION salespeople3_4 FOR VALUES FROM (3) TO (4), + PARTITION salespeople4_5 FOR VALUES FROM (4) TO (5)); + +INSERT INTO salespeople (salesperson_name) VALUES ('May'), ('Ford'); + +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +-- check new partitions have identity or not after split partition +SELECT attrelid::regclass, attname, attidentity, attgenerated FROM pg_attribute +WHERE attnum > 0 +AND attrelid::regclass IN ( + 'salespeople2_3'::regclass, 'salespeople', 'salespeople2_3', + 'salespeople1_2', 'salespeople3_4', 'salespeople4_5') +ORDER BY attrelid::regclass::text COLLATE "C", attnum; + +DROP TABLE salespeople CASCADE; + +-- +-- Test: split partition with deleted columns +-- +CREATE TABLE salespeople(salesperson_id INT PRIMARY KEY, salesperson_name VARCHAR(30)) PARTITION BY RANGE (salesperson_id); + +CREATE TABLE salespeople01_10 PARTITION OF salespeople FOR VALUES FROM (1) TO (10); +-- Create new partition with some deleted columns: +CREATE TABLE salespeople10_40(d1 VARCHAR(30), salesperson_id INT PRIMARY KEY, d2 INT, d3 DATE, salesperson_name VARCHAR(30)); + +INSERT INTO salespeople10_40 VALUES + ('dummy value 1', 19, 100, now(), 'Ivanov'), + ('dummy value 2', 20, 101, now(), 'Smirnoff'); + +ALTER TABLE salespeople10_40 DROP COLUMN d1; +ALTER TABLE salespeople10_40 DROP COLUMN d2; +ALTER TABLE salespeople10_40 DROP COLUMN d3; + +ALTER TABLE salespeople ATTACH PARTITION salespeople10_40 FOR VALUES FROM (10) TO (40); + +INSERT INTO salespeople VALUES + (1, 'Poirot'), + (10, 'May'), + (30, 'Ford'); + +ALTER TABLE salespeople SPLIT PARTITION salespeople10_40 INTO + (PARTITION salespeople10_20 FOR VALUES FROM (10) TO (20), + PARTITION salespeople20_30 FOR VALUES FROM (20) TO (30), + PARTITION salespeople30_40 FOR VALUES FROM (30) TO (40)); + +SELECT tableoid::regclass, * FROM salespeople ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE salespeople CASCADE; + +-- +-- Test: split sub-partition +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_feb2022 PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'); +CREATE TABLE sales_mar2022 PARTITION OF sales_range FOR VALUES FROM ('2022-03-01') TO ('2022-04-01'); + +CREATE TABLE sales_apr2022 (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_apr_all PARTITION OF sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); +ALTER TABLE sales_range ATTACH PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'); + +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +CREATE INDEX sales_range_sales_date_idx ON sales_range USING btree (sales_date); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +ALTER TABLE sales_apr2022 SPLIT PARTITION sales_apr_all INTO + (PARTITION sales_apr2022_01_10 FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'), + PARTITION sales_apr2022_10_20 FOR VALUES FROM ('2022-04-10') TO ('2022-04-20'), + PARTITION sales_apr2022_20_30 FOR VALUES FROM ('2022-04-20') TO ('2022-05-01')); + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE sales_range; + +-- +-- BY LIST partitioning +-- + +-- +-- Test: specific errors for BY LIST partitioning +-- +CREATE TABLE sales_list (sales_state VARCHAR(20)) PARTITION BY LIST (sales_state); + +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Oslo', 'St. Petersburg', 'Helsinki'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; + +-- ERROR: new partition "sales_east" would overlap with another (not split) partition "sales_nord" +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok', 'Helsinki'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); + +-- ERROR: new partition "sales_west" would overlap with another new partition "sales_central" +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Lisbon', 'Kyiv')); + +-- ERROR: new partition "sales_west" cannot have NULL value because split partition "sales_all" does not have +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', NULL), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); + +-- ERROR: new partition "sales_west" cannot have this value because split partition "sales_all" does not have +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); + +-- ERROR: new partition cannot be DEFAULT because DEFAULT partition "sales_others" already exists +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid', 'Melbourne'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'), + PARTITION sales_others2 DEFAULT); + +DROP TABLE sales_list; + +-- Test for non-symbolic comparison of values (numeric values '0' and '0.0' are equal). +CREATE TABLE t (a numeric) PARTITION BY LIST (a); +CREATE TABLE t1 PARTITION OF t FOR VALUES in ('0', '1'); +-- ERROR: new partition "x" would overlap with another new partition "x1" +ALTER TABLE t SPLIT PARTITION t1 INTO + (PARTITION x FOR VALUES IN ('0'), + PARTITION x1 FOR VALUES IN ('0.0', '1')); +DROP TABLE t; + +-- +-- Test: two specific errors for BY LIST partitioning: +-- * new partitions do not have NULL value, which split partition has. +-- * new partitions do not have a value that split partition has. +-- +CREATE TABLE sales_list(sales_state VARCHAR(20)) PARTITION BY LIST (sales_state); + +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok', NULL); + +-- ERROR: new partitions combined partition bounds do not contain value (NULL) but split partition "sales_all" does +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); + +-- ERROR: new partitions combined partition bounds do not contain value ('Kyiv'::character varying(20)) but split partition "sales_all" does +-- HINT: ALTER TABLE ... SPLIT PARTITION require combined bounds of new partitions must exactly match the bound of the split partition. +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', NULL)); + +-- ERROR DEFAULT partition should be one +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv'), + PARTITION sales_others DEFAULT, + PARTITION sales_others2 DEFAULT); + +DROP TABLE sales_list; + +-- +-- Test: BY LIST partitioning, SPLIT PARTITION with data +-- +CREATE TABLE sales_list +(salesperson_id SERIAL, + salesperson_name VARCHAR(30), + sales_state VARCHAR(20), + sales_amount INT, + sales_date DATE) +PARTITION BY LIST (sales_state); + +CREATE INDEX sales_list_salesperson_name_idx ON sales_list USING btree (salesperson_name); +CREATE INDEX sales_list_sales_state_idx ON sales_list USING btree (sales_state); + +CREATE TABLE sales_nord PARTITION OF sales_list FOR VALUES IN ('Helsinki', 'St. Petersburg', 'Oslo'); +CREATE TABLE sales_all PARTITION OF sales_list FOR VALUES IN ('Warsaw', 'Lisbon', 'New York', 'Madrid', 'Beijing', 'Berlin', 'Delhi', 'Kyiv', 'Vladivostok'); +CREATE TABLE sales_others PARTITION OF sales_list DEFAULT; + +INSERT INTO sales_list (salesperson_name, sales_state, sales_amount, sales_date) VALUES + ('Trump', 'Beijing', 1000, '2022-03-01'), + ('Smirnoff', 'New York', 500, '2022-03-03'), + ('Ford', 'St. Petersburg', 2000, '2022-03-05'), + ('Ivanov', 'Warsaw', 750, '2022-03-04'), + ('Deev', 'Lisbon', 250, '2022-03-07'), + ('Poirot', 'Berlin', 1000, '2022-03-01'), + ('May', 'Oslo', 1200, '2022-03-06'), + ('Li', 'Vladivostok', 1150, '2022-03-09'), + ('May', 'Oslo', 1200, '2022-03-11'), + ('Halder', 'Helsinki', 800, '2022-03-02'), + ('Muller', 'Madrid', 650, '2022-03-05'), + ('Smith', 'Kyiv', 350, '2022-03-10'), + ('Gandi', 'Warsaw', 150, '2022-03-08'), + ('Plato', 'Lisbon', 950, '2022-03-05'); + +ALTER TABLE sales_list SPLIT PARTITION sales_all INTO + (PARTITION sales_west FOR VALUES IN ('Lisbon', 'New York', 'Madrid'), + PARTITION sales_east FOR VALUES IN ('Beijing', 'Delhi', 'Vladivostok'), + PARTITION sales_central FOR VALUES IN ('Warsaw', 'Berlin', 'Kyiv')); + +SELECT tableoid::regclass, * FROM sales_list ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +-- Use indexscan for testing indexes after splitting partition +SET enable_seqscan = OFF; + +EXPLAIN (COSTS OFF) SELECT * FROM sales_central WHERE sales_state = 'Warsaw'; +SELECT * FROM sales_central WHERE sales_state = 'Warsaw'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; +SELECT * FROM sales_list WHERE sales_state = 'Warsaw'; +EXPLAIN (COSTS OFF) SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; +SELECT * FROM sales_list WHERE salesperson_name = 'Ivanov'; + +RESET enable_seqscan; + +DROP TABLE sales_list; + +-- +-- Test for: +-- * split DEFAULT partition to partitions with spaces between bounds; +-- * random order of partitions in SPLIT PARTITION command. +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_others PARTITION OF sales_range DEFAULT; + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-09'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-07'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'), + (14, 'Smith', 510, '2022-05-04'); + +ALTER TABLE sales_range SPLIT PARTITION sales_others INTO + (PARTITION sales_others DEFAULT, + PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'), + PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'), + PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'), + PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10')); + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE sales_range; + +-- +-- Test for: +-- * split non-DEFAULT partition to partitions with spaces between bounds; +-- * random order of partitions in SPLIT PARTITION command. +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-05-01'); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-09'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-07'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'); + +ALTER TABLE sales_range SPLIT PARTITION sales_all INTO + (PARTITION sales_mar2022_1decade FOR VALUES FROM ('2022-03-01') TO ('2022-03-10'), + PARTITION sales_jan2022_1decade FOR VALUES FROM ('2022-01-01') TO ('2022-01-10'), + PARTITION sales_feb2022_1decade FOR VALUES FROM ('2022-02-01') TO ('2022-02-10'), + PARTITION sales_apr2022_1decade FOR VALUES FROM ('2022-04-01') TO ('2022-04-10'), + PARTITION sales_others DEFAULT); + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE sales_range; + +-- +-- Test for split non-DEFAULT partition to DEFAULT partition + partitions +-- with spaces between bounds. +-- +CREATE TABLE sales_range (salesperson_id INT, salesperson_name VARCHAR(30), sales_amount INT, sales_date DATE) PARTITION BY RANGE (sales_date); +CREATE TABLE sales_jan2022 PARTITION OF sales_range FOR VALUES FROM ('2022-01-01') TO ('2022-02-01'); +CREATE TABLE sales_all PARTITION OF sales_range FOR VALUES FROM ('2022-02-01') TO ('2022-05-01'); + +INSERT INTO sales_range VALUES + (1, 'May', 1000, '2022-01-31'), + (2, 'Smirnoff', 500, '2022-02-10'), + (3, 'Ford', 2000, '2022-04-30'), + (4, 'Ivanov', 750, '2022-04-13'), + (5, 'Deev', 250, '2022-04-07'), + (6, 'Poirot', 150, '2022-02-11'), + (7, 'Li', 175, '2022-03-08'), + (8, 'Ericsson', 185, '2022-02-23'), + (9, 'Muller', 250, '2022-03-11'), + (10, 'Halder', 350, '2022-01-28'), + (11, 'Trump', 380, '2022-04-06'), + (12, 'Plato', 350, '2022-03-19'), + (13, 'Gandi', 377, '2022-01-09'); + +ALTER TABLE sales_range SPLIT PARTITION sales_all INTO + (PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'), + PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'), + PARTITION sales_others DEFAULT); + +INSERT INTO sales_range VALUES (14, 'Smith', 510, '2022-05-04'); + +SELECT tableoid::regclass, * FROM sales_range ORDER BY tableoid::regclass::text COLLATE "C", salesperson_id; + +DROP TABLE sales_range; + +-- +-- Try to SPLIT partition of another table. +-- +CREATE TABLE t1(i int, t text) PARTITION BY LIST (t); +CREATE TABLE t1pa PARTITION OF t1 FOR VALUES IN ('A'); +CREATE TABLE t2 (i int, t text) PARTITION BY RANGE (t); + +-- ERROR: relation "t1pa" is not a partition of relation "t2" +ALTER TABLE t2 SPLIT PARTITION t1pa INTO + (PARTITION t2a FOR VALUES FROM ('A') TO ('B'), + PARTITION t2b FOR VALUES FROM ('B') TO ('C')); + +DROP TABLE t2; +DROP TABLE t1; + +-- +-- Try to SPLIT partition of temporary table. +-- +CREATE TEMP TABLE t (i int) PARTITION BY RANGE (i); +CREATE TEMP TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); + +SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence + FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i + WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass + ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text COLLATE "C"; + +-- ERROR: cannot create a permanent relation as partition of temporary relation "t" +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); + +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION pg_temp.tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION pg_temp.tp_1_2 FOR VALUES FROM (1) TO (2)); + +-- Partitions should be temporary. +SELECT c.oid::pg_catalog.regclass, pg_catalog.pg_get_expr(c.relpartbound, c.oid), c.relpersistence + FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i + WHERE c.oid = i.inhrelid AND i.inhparent = 't'::regclass + ORDER BY pg_catalog.pg_get_expr(c.relpartbound, c.oid) = 'DEFAULT', c.oid::pg_catalog.regclass::pg_catalog.text COLLATE "C"; + +DROP TABLE t; + +-- Check the new partitions inherit parent's tablespace +CREATE TABLE t (i int PRIMARY KEY USING INDEX TABLESPACE regress_tblspace) + PARTITION BY RANGE (i) TABLESPACE regress_tblspace; +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +SELECT tablename, tablespace FROM pg_tables + WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema' + ORDER BY tablename COLLATE "C", tablespace COLLATE "C"; +SELECT tablename, indexname, tablespace FROM pg_indexes + WHERE tablename IN ('t', 'tp_0_1', 'tp_1_2') AND schemaname = 'partition_split_schema' + ORDER BY tablename COLLATE "C", indexname COLLATE "C", tablespace COLLATE "C"; +DROP TABLE t; + +-- Check new partitions inherits parent's table access method +CREATE ACCESS METHOD partition_split_heap TYPE TABLE HANDLER heap_tableam_handler; +CREATE TABLE t (i int) PARTITION BY RANGE (i) USING partition_split_heap; +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); +SELECT c.relname, a.amname +FROM pg_class c JOIN pg_am a ON c.relam = a.oid +WHERE c.oid IN ('t'::regclass, 'tp_0_1'::regclass, 'tp_1_2'::regclass) +ORDER BY c.relname COLLATE "C"; +DROP TABLE t; +DROP ACCESS METHOD partition_split_heap; + +-- Split partition of a temporary table when one of the partitions after +-- split has the same name as the partition being split +CREATE TEMP TABLE t (a int) PARTITION BY RANGE (a); +CREATE TEMP TABLE tp_0 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t SPLIT PARTITION tp_0 INTO + (PARTITION pg_temp.tp_0 FOR VALUES FROM (0) TO (1), + PARTITION pg_temp.tp_1 FOR VALUES FROM (1) TO (2)); +DROP TABLE t; + +-- Check defaults and constraints of new partitions +CREATE TABLE t_bigint ( + b bigint, + i int DEFAULT (3+10), + j int DEFAULT 101, + k int GENERATED ALWAYS AS (b+10) STORED +) +PARTITION BY RANGE (b); +CREATE TABLE t_bigint_default PARTITION OF t_bigint DEFAULT; +-- Show defaults/constraints before SPLIT PARTITION +\d+ t_bigint +\d+ t_bigint_default +ALTER TABLE t_bigint SPLIT PARTITION t_bigint_default INTO + (PARTITION t_bigint_01_10 FOR VALUES FROM (0) TO (10), + PARTITION t_bigint_default DEFAULT); +-- Show defaults/constraints after SPLIT PARTITION +\d+ t_bigint_default +\d+ t_bigint_01_10 +DROP TABLE t_bigint; + +-- Test permission checks. The user needs to own the parent table and the +-- the partition to split to do the split. +CREATE ROLE regress_partition_split_alice; +CREATE ROLE regress_partition_split_bob; +GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_alice; +GRANT ALL ON SCHEMA partition_split_schema TO regress_partition_split_bob; + +SET SESSION AUTHORIZATION regress_partition_split_alice; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); + +SET SESSION AUTHORIZATION regress_partition_split_bob; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); --error +RESET SESSION AUTHORIZATION; + +ALTER TABLE t OWNER TO regress_partition_split_bob; +SET SESSION AUTHORIZATION regress_partition_split_bob; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); --error +RESET SESSION AUTHORIZATION; + +ALTER TABLE tp_0_2 OWNER TO regress_partition_split_bob; +SET SESSION AUTHORIZATION regress_partition_split_bob; +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); --ok +RESET SESSION AUTHORIZATION; + +DROP TABLE t; + +-- Test: owner of new partitions should be the same as owner of split partition +CREATE TABLE t (i int) PARTITION BY RANGE (i); + +SET SESSION AUTHORIZATION regress_partition_split_alice; +CREATE TABLE tp_0_2(i int); +RESET SESSION AUTHORIZATION; + +ALTER TABLE t ATTACH PARTITION tp_0_2 FOR VALUES FROM (0) TO (2); + +-- Owner is 'regress_partition_split_alice': +\dt tp_0_2 + +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); + +-- Owner should be 'regress_partition_split_alice': +\dt tp_0_1 +\dt tp_1_2 + +DROP TABLE t; + +-- Test: index of new partitions should be created with same owner as split +-- partition +SET SESSION AUTHORIZATION regress_partition_split_alice; +CREATE TABLE t (i int) PARTITION BY RANGE (i); +CREATE TABLE tp_10_20 PARTITION OF t FOR VALUES FROM (10) TO (20); +INSERT INTO t VALUES (11), (16); +CREATE OR REPLACE FUNCTION run_me(integer) RETURNS integer AS $$ +BEGIN + RAISE NOTICE 'you are running me as %', CURRENT_USER; + RETURN $1; +END +$$ LANGUAGE PLPGSQL IMMUTABLE; + +-- Owner is 'regress_partition_split_alice': +CREATE INDEX ON t (run_me(i)); +RESET SESSION AUTHORIZATION; + +-- Owner should be 'regress_partition_split_alice': +ALTER TABLE t SPLIT PARTITION tp_10_20 INTO + (PARTITION tp_10_15 FOR VALUES FROM (10) TO (15), + PARTITION tp_15_20 FOR VALUES FROM (15) TO (20)); + +DROP TABLE t; +DROP FUNCTION run_me(integer); + +REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_alice; +REVOKE ALL ON SCHEMA partition_split_schema FROM regress_partition_split_bob; +DROP ROLE regress_partition_split_alice; +DROP ROLE regress_partition_split_bob; + +-- Test for hash partitioned table +CREATE TABLE t (i int) PARTITION BY HASH(i); +CREATE TABLE tp1 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 0); +CREATE TABLE tp2 PARTITION OF t FOR VALUES WITH (MODULUS 2, REMAINDER 1); + +-- ERROR: partition of hash-partitioned table cannot be split +ALTER TABLE t SPLIT PARTITION tp1 INTO + (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0), + PARTITION tp1_2 FOR VALUES WITH (MODULUS 4, REMAINDER 2)); + +-- ERROR: list of new partitions should contain at least two partitions +ALTER TABLE t SPLIT PARTITION tp1 INTO + (PARTITION tp1_1 FOR VALUES WITH (MODULUS 4, REMAINDER 0)); + +DROP TABLE t; + + +-- Test for split partition properties: +-- * STATISTICS is empty +-- * COMMENT is empty +-- * DEFAULTS are the same as DEFAULTS for partitioned table +-- * STORAGE is the same as STORAGE for partitioned table +-- * GENERATED and CONSTRAINTS are the same as GENERATED and CONSTRAINTS for partitioned table +-- * TRIGGERS are the same as TRIGGERS for partitioned table + +CREATE TABLE t +(i int NOT NULL, + t text STORAGE EXTENDED COMPRESSION pglz DEFAULT 'default_t', + b bigint, + d date GENERATED ALWAYS as ('2022-01-01') STORED) PARTITION BY RANGE (abs(i)); +COMMENT ON COLUMN t.i IS 't1.i'; + +CREATE TABLE tp_x +(i int NOT NULL, + t text STORAGE MAIN DEFAULT 'default_tp_x', + b bigint, + d date GENERATED ALWAYS as ('2022-02-02') STORED); +ALTER TABLE t ATTACH PARTITION tp_x FOR VALUES FROM (0) TO (2); +COMMENT ON COLUMN tp_x.i IS 'tp_x.i'; + +CREATE STATISTICS t_stat (DEPENDENCIES) on i, b from t; +CREATE STATISTICS tp_x_stat (DEPENDENCIES) on i, b from tp_x; + +ALTER TABLE t ADD CONSTRAINT t_b_check CHECK (b > 0); +ALTER TABLE t ADD CONSTRAINT t_b_check1 CHECK (b > 0) NOT ENFORCED; +ALTER TABLE t ADD CONSTRAINT t_b_check2 CHECK (b > 0) NOT VALID; +ALTER TABLE t ADD CONSTRAINT t_b_nn NOT NULL b NOT VALID; + +INSERT INTO tp_x(i, t, b) VALUES(0, DEFAULT, 1); +INSERT INTO tp_x(i, t, b) VALUES(1, DEFAULT, 2); + +CREATE OR REPLACE FUNCTION trigger_function() RETURNS trigger LANGUAGE 'plpgsql' AS +$BODY$ +BEGIN + RAISE NOTICE 'trigger(%) called: action = %, when = %, level = %', TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL; + RETURN new; +END; +$BODY$; + +CREATE TRIGGER t_before_insert_row_trigger BEFORE INSERT ON t FOR EACH ROW + EXECUTE PROCEDURE trigger_function('t'); +CREATE TRIGGER tp_x_before_insert_row_trigger BEFORE INSERT ON tp_x FOR EACH ROW + EXECUTE PROCEDURE trigger_function('tp_x'); + +\d+ tp_x +ALTER TABLE t SPLIT PARTITION tp_x INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_x FOR VALUES FROM (1) TO (2)); +\d+ tp_x + +INSERT INTO t(i, t, b) VALUES(1, DEFAULT, 3); +SELECT tableoid::regclass, * FROM t ORDER BY tableoid::regclass::text COLLATE "C", b; +DROP TABLE t; +DROP FUNCTION trigger_function(); + + +-- Test for recomputation of stored generated columns. +CREATE TABLE t (i int, tab_id int generated always as (tableoid) stored) PARTITION BY RANGE (i); +CREATE TABLE tp_0_2 PARTITION OF t FOR VALUES FROM (0) TO (2); +ALTER TABLE t ADD CONSTRAINT cc CHECK(tableoid <> 123456789); +INSERT INTO t VALUES (0), (1); + +-- Should be 1 because partition identifier for row with i=0 is the same as +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + +-- "tab_id" column (stored generated column) with "tableoid" attribute requires +-- recomputation here. +ALTER TABLE t SPLIT PARTITION tp_0_2 INTO + (PARTITION tp_0_1 FOR VALUES FROM (0) TO (1), + PARTITION tp_1_2 FOR VALUES FROM (1) TO (2)); + +-- Should be 0 because partition identifier for row with i=0 is different from +-- partition identifier for row with i=1. +SELECT count(*) FROM t WHERE i = 0 AND tab_id IN (SELECT tab_id FROM t WHERE i = 1); + +DROP TABLE t; + + +RESET search_path; + +-- +DROP SCHEMA partition_split_schema; +DROP SCHEMA partition_split_schema2; diff --git a/src/test/regress/sql/pg_dependencies.sql b/src/test/regress/sql/pg_dependencies.sql new file mode 100644 index 0000000000000..7935c540e3c8a --- /dev/null +++ b/src/test/regress/sql/pg_dependencies.sql @@ -0,0 +1,133 @@ +-- Tests for type pg_distinct + +-- Invalid inputs +SELECT 'null'::pg_dependencies; +SELECT '{"a": 1}'::pg_dependencies; +SELECT '[]'::pg_dependencies; +SELECT '{}'::pg_dependencies; +SELECT '[null]'::pg_dependencies; +SELECT * FROM pg_input_error_info('null', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('{}', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[null]', 'pg_dependencies'); + +-- Invalid keys +SELECT '[{"attributes_invalid" : [2,3], "dependency" : 4}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "dependency" : 4}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "dependency" : 4}]', 'pg_dependencies'); + +-- Missing keys +SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "degree" : 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : 4}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "degree" : 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4}]', 'pg_dependencies'); + +-- Valid keys, too many attributes +SELECT '[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + +-- Special characters +SELECT '[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]'::pg_dependencies; +SELECT '[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d",3], "dependency" : 4, "degree": 0.250}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "\ud83d", "degree": 0.250}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": "\ud83d"}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"\ud83d" : [2,3], "dependency" : 4, "degree": 0.250}]', 'pg_dependencies'); + +-- Valid keys, invalid values +SELECT '[{"attributes" : null, "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : null, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : null, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : "a", "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [], "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [null], "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : [1,null], "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : 1, "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : "a", "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": NaN}]', 'pg_dependencies'); + +SELECT '[{"attributes": [], "dependency": 2, "degree": 1}]' ::pg_dependencies; +SELECT '[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes": [], "dependency": 2, "degree": 1}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "dependency" : 4, "degree": "1.2"}]', 'pg_dependencies'); + +SELECT '[{"dependency" : 4, "degree": "1.2"}]'::pg_dependencies; +SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]'::pg_dependencies; +SELECT '[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]'::pg_dependencies; +SELECT '[{"attributes": [1,2], "dependency": 2, "degree": 1}]' ::pg_dependencies; +SELECT '[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]'::pg_dependencies; +SELECT '[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]'::pg_dependencies; +SELECT '[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]'::pg_dependencies; +SELECT '[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"dependency" : 4, "degree": "1.2"}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : 0, "degree": "1.2"}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7], "dependency" : -9, "degree": "1.2"}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes": [1,2], "dependency": 2, "degree": 1}]' , 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1, {}], "dependency" : 1, "degree": "1.2"}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : {}, "degree": 1.0}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 3, "degree": {}}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2], "dependency" : 1, "degree": "a"}]', 'pg_dependencies'); + +-- Funky degree values, which do not fail. +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "NaN"}]'::pg_dependencies; +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies; +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "inf"}]'::pg_dependencies; +SELECT '[{"attributes" : [2], "dependency" : 4, "degree": "-inf"}]'::pg_dependencies::text::pg_dependencies; + +-- Duplicated keys +SELECT '[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]'::pg_dependencies; +SELECT '[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes": [1,2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "dependency": 4, "degree": 1.000}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency": 4, "degree": 1.000, "degree": 1.000}]', 'pg_dependencies'); + +-- Invalid attnums +SELECT '[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies; +SELECT '[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [0,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies'); +SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies'); + +-- Duplicated attributes +SELECT '[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "dependency" : 4, "degree": 0.500}]', 'pg_dependencies'); + +-- Duplicated attribute lists. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); + +-- Valid inputs +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250}, + {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500}, + {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750}, + {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "dependency" : 4, "degree": 0.250}, + {"attributes" : [2,-1], "dependency" : 4, "degree": 0.500}, + {"attributes" : [2,3,-1], "dependency" : 4, "degree": 0.750}, + {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]', 'pg_dependencies'); +-- Partially-covered attribute lists, possible as items with a degree of 0 +-- are discarded. +SELECT '[{"attributes" : [2,3], "dependency" : 4, "degree": 1.000}, + {"attributes" : [1,-1], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3,-1], "dependency" : 4, "degree": 1.000}, + {"attributes" : [2,3,-1,-2], "dependency" : 4, "degree": 1.000}]'::pg_dependencies; diff --git a/src/test/regress/sql/pg_ndistinct.sql b/src/test/regress/sql/pg_ndistinct.sql new file mode 100644 index 0000000000000..319c7d483f826 --- /dev/null +++ b/src/test/regress/sql/pg_ndistinct.sql @@ -0,0 +1,106 @@ +-- Tests for type pg_ndistinct + +-- Invalid inputs +SELECT 'null'::pg_ndistinct; +SELECT '{"a": 1}'::pg_ndistinct; +SELECT '[]'::pg_ndistinct; +SELECT '{}'::pg_ndistinct; +SELECT '[null]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('null', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('{"a": 1}', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('{}', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[null]', 'pg_ndistinct'); +-- Invalid keys +SELECT '[{"attributes_invalid" : [2,3], "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes_invalid" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "invalid" : 3, "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "attributes" : [1,3], "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4, "ndistinct" : 4}]', 'pg_ndistinct'); + +-- Missing key +SELECT '[{"attributes" : [2,3]}]'::pg_ndistinct; +SELECT '[{"ndistinct" : 4}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3]}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"ndistinct" : 4}]', 'pg_ndistinct'); + +-- Valid keys, too many attributes +SELECT '[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes" : [1,2,3,4,5,6,7,8,9], "ndistinct" : 4}]', 'pg_ndistinct'); + +-- Special characters +SELECT '[{"\ud83d" : [1, 2], "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [1, 2], "\ud83d" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]'::pg_ndistinct; +SELECT '[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"\ud83d" : [1, 2], "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "\ud83d" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1, 2], "ndistinct" : "\ud83d"}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : ["\ud83d", 2], "ndistinct" : 1}]', 'pg_ndistinct'); + +-- Valid keys, invalid values +SELECT '[{"attributes" : null, "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [], "ndistinct" : 1}]'::pg_ndistinct; +SELECT '[{"attributes" : [2], "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,null], "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : null}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,"a"], "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : "a"}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : []}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : [null]}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : [1,null]}]'::pg_ndistinct; +SELECT '[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]'::pg_ndistinct; +SELECT '[{"attributes" : [0,1], "ndistinct" : 1}]'::pg_ndistinct; +SELECT '[{"attributes" : [-7,-9], "ndistinct" : 1}]'::pg_ndistinct; +SELECT '[{"attributes" : 1, "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : "a", "ndistinct" : 4}]'::pg_ndistinct; +SELECT '[{"attributes" : {"a": 1}, "ndistinct" : 1}]'::pg_ndistinct; +SELECT '[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes" : null, "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [], "ndistinct" : 1}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2], "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,null], "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : null}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,"a"], "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : "a"}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : []}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [null]}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : [1,null]}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : {"a": 1}}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [-7,-9], "ndistinct" : 1}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : 1, "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : "a", "ndistinct" : 4}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : {"a": 1}, "ndistinct" : 1}]', 'pg_ndistinct'); +SELECT * FROM pg_input_error_info('[{"attributes" : [1, {"a": 1}], "ndistinct" : 1}]', 'pg_ndistinct'); +-- Duplicated attributes +SELECT '[{"attributes" : [2,2], "ndistinct" : 4}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,2], "ndistinct" : 4}]', 'pg_ndistinct'); +-- Duplicated attribute lists. +SELECT '[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,3], "ndistinct" : 4}]', 'pg_ndistinct'); +-- Partially-covered attribute lists. +SELECT '[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,-1], "ndistinct" : 4}, + {"attributes" : [2,3,-1], "ndistinct" : 4}, + {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]'::pg_ndistinct; +SELECT * FROM pg_input_error_info('[{"attributes" : [2,3], "ndistinct" : 4}, + {"attributes" : [2,-1], "ndistinct" : 4}, + {"attributes" : [2,3,-1], "ndistinct" : 4}, + {"attributes" : [1,3,-1,-2], "ndistinct" : 4}]', 'pg_ndistinct'); + +-- Valid inputs +-- Two attributes. +SELECT '[{"attributes" : [1,2], "ndistinct" : 4}]'::pg_ndistinct; +-- Three attributes. +SELECT '[{"attributes" : [2,-1], "ndistinct" : 1}, + {"attributes" : [3,-1], "ndistinct" : 2}, + {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct; +-- Three attributes with only two items. +SELECT '[{"attributes" : [2,-1], "ndistinct" : 1}, + {"attributes" : [2,3,-1], "ndistinct" : 3}]'::pg_ndistinct; diff --git a/src/test/regress/sql/plancache.sql b/src/test/regress/sql/plancache.sql index 4b2f11dcc6418..aed388d03a1d4 100644 --- a/src/test/regress/sql/plancache.sql +++ b/src/test/regress/sql/plancache.sql @@ -180,10 +180,15 @@ deallocate pstmt_def_insert; -- Test plan_cache_mode -create table test_mode (a int); +create table test_mode (a int) with (autovacuum_enabled = false); insert into test_mode select 1 from generate_series(1,1000) union all select 2; -create index on test_mode (a); + +-- ANALYZE before creating the index. CREATE INDEX scans the table, which may +-- set pages all-visible via on-access pruning. If relallvisible is then updated +-- by ANALYZE, the generic plan may pick an index-only scan instead of the +-- expected sequential scan. analyze test_mode; +create index on test_mode (a); prepare test_mode_pp (int) as select count(*) from test_mode where a = $1; select name, generic_plans, custom_plans from pg_prepared_statements diff --git a/src/test/regress/sql/planner_est.sql b/src/test/regress/sql/planner_est.sql new file mode 100644 index 0000000000000..53210d5baad8e --- /dev/null +++ b/src/test/regress/sql/planner_est.sql @@ -0,0 +1,150 @@ +-- +-- Tests for testing query planner selectivity and width estimates +-- +-- Most selectivity and width estimations rely too heavily on statistics +-- gathered by ANALYZE, or could vary depending on hardware. However, there +-- are a few cases where we can have more certainty about the expected number +-- of rows, or width of rows. This is a good home for such tests. +-- + +-- Function to assist with verifying EXPLAIN which includes costs. A series +-- of bool flags allows control over which portions are masked out +CREATE FUNCTION explain_mask_costs(query text, do_analyze bool, + hide_costs bool, hide_row_est bool, hide_width bool) RETURNS setof text +LANGUAGE plpgsql AS +$$ +DECLARE + ln text; + analyze_str text; +BEGIN + IF do_analyze = true THEN + analyze_str := 'on'; + ELSE + analyze_str := 'off'; + END IF; + + -- avoid jit related output by disabling it + SET LOCAL jit = 0; + + FOR ln IN + EXECUTE format('explain (analyze %s, costs on, summary off, timing off, buffers off) %s', + analyze_str, query) + LOOP + IF hide_costs = true THEN + ln := regexp_replace(ln, 'cost=\d+\.\d\d\.\.\d+\.\d\d', 'cost=N..N'); + END IF; + + IF hide_row_est = true THEN + -- don't use 'g' so that we leave the actual rows intact + ln := regexp_replace(ln, 'rows=\d+', 'rows=N'); + END IF; + + IF hide_width = true THEN + ln := regexp_replace(ln, 'width=\d+', 'width=N'); + END IF; + + RETURN NEXT ln; + END LOOP; +END; +$$; + +-- +-- Test the SupportRequestRows support function for generate_series_timestamp() +-- + +-- Ensure the row estimate matches the actual rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day') g(s);$$, +true, true, false, true); + +-- As above but with generate_series_timestamp +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMP '2024-02-01', TIMESTAMP '2024-03-01', INTERVAL '1 day') g(s);$$, +true, true, false, true); + +-- As above but with generate_series_timestamptz_at_zone() +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '1 day', 'UTC') g(s);$$, +true, true, false, true); + +-- Ensure the estimated and actual row counts match when the range isn't +-- evenly divisible by the step +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '7 day') g(s);$$, +true, true, false, true); + +-- Ensure the estimates match when step is decreasing +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '-1 day') g(s);$$, +true, true, false, true); + +-- Ensure an empty range estimates 1 row +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '2024-03-01', TIMESTAMPTZ '2024-02-01', INTERVAL '1 day') g(s);$$, +true, true, false, true); + +-- Ensure we get the default row estimate for infinity values +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(TIMESTAMPTZ '-infinity', TIMESTAMPTZ 'infinity', INTERVAL '1 day') g(s);$$, +false, true, false, true); + +-- Ensure the row estimate behaves correctly when step size is zero. +-- We expect generate_series_timestamp() to throw the error rather than in +-- the support function. +SELECT * FROM generate_series(TIMESTAMPTZ '2024-02-01', TIMESTAMPTZ '2024-03-01', INTERVAL '0 day') g(s); + +-- +-- Test the SupportRequestRows support function for generate_series_numeric() +-- + +-- Ensure the row estimate matches the actual rows +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1.0, 25.0) g(s);$$, +true, true, false, true); + +-- As above but with non-default step +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1.0, 25.0, 2.0) g(s);$$, +true, true, false, true); + +-- Ensure the estimates match when step is decreasing +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25.0, 1.0, -1.0) g(s);$$, +true, true, false, true); + +-- Ensure an empty range estimates 1 row +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25.0, 1.0, 1.0) g(s);$$, +true, true, false, true); + +-- Ensure we get the default row estimate for error cases (infinity/NaN values +-- and zero step size) +SELECT explain_mask_costs($$ +SELECT * FROM generate_series('-infinity'::NUMERIC, 'infinity'::NUMERIC, 1.0) g(s);$$, +false, true, false, true); + +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(1.0, 25.0, 'NaN'::NUMERIC) g(s);$$, +false, true, false, true); + +SELECT explain_mask_costs($$ +SELECT * FROM generate_series(25.0, 2.0, 0.0) g(s);$$, +false, true, false, true); + +-- +-- Test ScalarArrayOpExpr row estimates for <> ALL for arrays with NULLs. We +-- expect the planner to estimate 1 row will match in both of the following +-- tests. +-- + +-- Try a const array containing a NULL +SELECT explain_mask_costs($$ +SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 99, NULL]);$$, +false, true, false, true); + +-- Try a non-const array containing a NULL +SELECT explain_mask_costs($$ +SELECT * FROM tenk1 WHERE unique1 <> ALL (ARRAY[1, 2, 98, (SELECT 99), NULL]);$$, +false, true, false, true); + +DROP FUNCTION explain_mask_costs(text, bool, bool, bool, bool); diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index d413d995d17e0..ae6b67e3e2271 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -3773,28 +3773,6 @@ drop function fail(); -- Test handling of string literals. -set standard_conforming_strings = off; - -create or replace function strtest() returns text as $$ -begin - raise notice 'foo\\bar\041baz'; - return 'foo\\bar\041baz'; -end -$$ language plpgsql; - -select strtest(); - -create or replace function strtest() returns text as $$ -begin - raise notice E'foo\\bar\041baz'; - return E'foo\\bar\041baz'; -end -$$ language plpgsql; - -select strtest(); - -set standard_conforming_strings = on; - create or replace function strtest() returns text as $$ begin raise notice 'foo\\bar\041baz\'; diff --git a/src/test/regress/sql/polymorphism.sql b/src/test/regress/sql/polymorphism.sql index fa57db6559c37..7337fd2d8f1ae 100644 --- a/src/test/regress/sql/polymorphism.sql +++ b/src/test/regress/sql/polymorphism.sql @@ -873,6 +873,26 @@ select * from dfunc(1,c := 2,d := 3); -- fail, no value for b drop function dfunc(int, int, int, int); +create function xleast(x numeric, variadic arr numeric[]) + returns numeric as $$ + select least(x, min(arr[i])) from generate_subscripts(arr, 1) g(i); +$$ language sql; + +select xleast(x => 1, variadic arr => array[2,3]); +select xleast(1, variadic arr => array[2,3]); + +set search_path = pg_catalog; +select xleast(1, variadic arr => array[2,3]); -- wrong schema +reset search_path; +select xleast(foo => 1, variadic arr => array[2,3]); -- wrong argument name +select xleast(x => 1, variadic array[2,3]); -- misuse of mixed notation +select xleast(1, variadic x => array[2,3]); -- misuse of mixed notation +select xleast(arr => array[1], variadic x => 3); -- wrong arg is VARIADIC +select xleast(arr => array[1], x => 3); -- failed to use VARIADIC +select xleast(arr => 1, variadic x => array[2,3]); -- mixed-up args + +drop function xleast(x numeric, variadic arr numeric[]); + -- test with different parameter types create function dfunc(a varchar, b numeric, c date = current_date) returns table (a varchar, b numeric, c date) as $$ diff --git a/src/test/regress/sql/portals.sql b/src/test/regress/sql/portals.sql index fc4cccb96c0fe..196b862c7560d 100644 --- a/src/test/regress/sql/portals.sql +++ b/src/test/regress/sql/portals.sql @@ -508,6 +508,11 @@ DECLARE c1 CURSOR FOR SELECT * FROM ucview; FETCH FROM c1; DELETE FROM ucview WHERE CURRENT OF c1; -- fail, views not supported ROLLBACK; +BEGIN; +DECLARE c1 CURSOR FOR SELECT * FROM ucview; +FETCH FROM c1; +UPDATE ucview SET f1 = f1 + 10 WHERE CURRENT OF c1; -- fail, views not supported +ROLLBACK; -- Check WHERE CURRENT OF with an index-only scan BEGIN; diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql index 9dcb81b1bc52f..0f92bb5243508 100644 --- a/src/test/regress/sql/predicate.sql +++ b/src/test/regress/sql/predicate.sql @@ -115,6 +115,24 @@ SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 LEFT JOIN pred_tab t3 ON t2.a IS NULL OR t2.c IS NULL; +-- +-- Tests for NullTest reduction in EXISTS sublink +-- + +-- Ensure the IS_NOT_NULL qual is ignored +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 + LEFT JOIN pred_tab t2 ON EXISTS + (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6 + WHERE t1.a = t3.a AND t6.a IS NOT NULL); + +-- Ensure the IS_NULL qual is reduced to constant-FALSE +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab t1 + LEFT JOIN pred_tab t2 ON EXISTS + (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6 + WHERE t1.a = t3.a AND t6.a IS NULL); + DROP TABLE pred_tab; -- Validate we handle IS NULL and IS NOT NULL quals correctly with inheritance @@ -183,3 +201,250 @@ SELECT * FROM pred_tab t1 DROP TABLE pred_tab; DROP TABLE pred_tab_notnull; + +-- Validate that NullTest quals in constraint expressions are reduced correctly +CREATE TABLE pred_tab1 (a int NOT NULL, b int, + CONSTRAINT check_tab1 CHECK (a IS NULL OR b > 2)); +CREATE TABLE pred_tab2 (a int, b int, + CONSTRAINT check_a CHECK (a IS NOT NULL)); + +SET constraint_exclusion TO ON; + +-- Ensure that we get a dummy plan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab1, pred_tab2 WHERE pred_tab2.a IS NULL; + +-- Ensure that we get a dummy plan +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2; + +RESET constraint_exclusion; +DROP TABLE pred_tab1; +DROP TABLE pred_tab2; + +-- Validate that NullTest quals in index expressions and predicate are reduced correctly +CREATE TABLE pred_tab (a int, b int NOT NULL, c int NOT NULL); +INSERT INTO pred_tab SELECT i, i, i FROM generate_series(1, 1000) i; +CREATE INDEX pred_tab_exprs_idx ON pred_tab ((a < 5 AND b IS NOT NULL AND c IS NOT NULL)); +CREATE INDEX pred_tab_pred_idx ON pred_tab (a) WHERE b IS NOT NULL AND c IS NOT NULL; +ANALYZE pred_tab; + +-- Ensure that index pred_tab_exprs_idx is used +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE; +SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE; + +-- Ensure that index pred_tab_pred_idx is used +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL; +SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL; + +DROP TABLE pred_tab; + +-- +-- Test that COALESCE expressions in predicates are simplified using +-- non-nullable arguments. +-- +CREATE TABLE pred_tab (a int NOT NULL, b int, c int); + +-- Ensure that constant NULL arguments are dropped +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(NULL, b, NULL, a) > 1; + +-- Ensure that argument "b*a" is dropped +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, a, b*a) > 1; + +-- Ensure that the entire COALESCE expression is replaced by "a" +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(a, b) > 1; + +-- +-- Test detection of non-nullable expressions in predicates +-- + +-- CoalesceExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, a) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE COALESCE(b, c) IS NULL; + +-- MinMaxExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE GREATEST(b, a) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE GREATEST(b, c) IS NULL; + +-- CaseExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a ELSE a END) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN b ELSE a END) IS NULL; + +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (CASE WHEN c > 0 THEN a END) IS NULL; + +-- ArrayExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE ARRAY[b] IS NULL; + +-- NullTest +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (b IS NULL) IS NULL; + +-- BooleanTest +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE ((a > 1) IS TRUE) IS NULL; + +-- DistinctExpr +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a IS DISTINCT FROM b) IS NULL; + +-- RelabelType +EXPLAIN (COSTS OFF) +SELECT * FROM pred_tab WHERE (a::oid) IS NULL; + +DROP TABLE pred_tab; + +-- +-- Test optimization of IS [NOT] DISTINCT FROM +-- + +CREATE TYPE dist_row_t AS (a int, b int); +CREATE TABLE dist_tab (id int, val_nn int NOT NULL, val_null int, row_nn dist_row_t NOT NULL); + +INSERT INTO dist_tab VALUES (1, 10, 10, ROW(1, 1)); +INSERT INTO dist_tab VALUES (2, 20, NULL, ROW(2, 2)); +INSERT INTO dist_tab VALUES (3, 30, 30, ROW(1, NULL)); + +CREATE INDEX dist_tab_nn_idx ON dist_tab (val_nn); + +ANALYZE dist_tab; + +-- Ensure that the predicate folds to constant TRUE +EXPLAIN(COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM NULL::INT; + +-- Ensure that the predicate folds to constant FALSE +EXPLAIN(COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM NULL::INT; + +-- Ensure that the predicate is converted to an inequality operator +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM 10; +SELECT id FROM dist_tab WHERE val_nn IS DISTINCT FROM 10; + +-- Ensure that the predicate is converted to an equality operator, and thus can +-- use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM 10; +SELECT id FROM dist_tab WHERE val_nn IS NOT DISTINCT FROM 10; +RESET enable_seqscan; + +-- Ensure that the predicate is preserved as "IS DISTINCT FROM" +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM 20; +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM 20; + +-- Safety check for rowtypes +-- Ensure that the predicate is converted to an inequality operator +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE row_nn IS DISTINCT FROM ROW(1, 5)::dist_row_t; +-- ... and that all 3 rows are returned +SELECT id FROM dist_tab WHERE row_nn IS DISTINCT FROM ROW(1, 5)::dist_row_t; + +-- Ensure that the predicate is converted to an equality operator, and thus +-- mergejoinable or hashjoinable +SET enable_nestloop TO off; +EXPLAIN (COSTS OFF) +SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn; +SELECT * FROM dist_tab t1 JOIN dist_tab t2 ON t1.val_nn IS NOT DISTINCT FROM t2.val_nn; +RESET enable_nestloop; + +-- Ensure that the predicate is converted to IS NOT NULL +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_null IS DISTINCT FROM NULL::INT; + +-- Ensure that the predicate is converted to IS NULL +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT; +SELECT id FROM dist_tab WHERE val_null IS NOT DISTINCT FROM NULL::INT; + +-- Safety check for rowtypes +-- The predicate is converted to IS NOT NULL, and get_rule_expr prints it as IS +-- DISTINCT FROM because argisrow is false, indicating that we're applying a +-- scalar test +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD; +SELECT id FROM dist_tab WHERE (val_null, val_null) IS DISTINCT FROM NULL::RECORD; + +-- The predicate is converted to IS NULL, and get_rule_expr prints it as IS NOT +-- DISTINCT FROM because argisrow is false, indicating that we're applying a +-- scalar test +EXPLAIN (COSTS OFF) +SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD; +SELECT id FROM dist_tab WHERE (val_null, val_null) IS NOT DISTINCT FROM NULL::RECORD; + +DROP TABLE dist_tab; +DROP TYPE dist_row_t; + +-- +-- Test optimization of BooleanTest (IS [NOT] TRUE/FALSE/UNKNOWN) on +-- non-nullable input +-- +CREATE TABLE bool_tab (id int, flag_nn boolean NOT NULL, flag_null boolean); + +INSERT INTO bool_tab VALUES (1, true, true); +INSERT INTO bool_tab VALUES (2, false, NULL); + +CREATE INDEX bool_tab_nn_idx ON bool_tab (flag_nn); + +ANALYZE bool_tab; + +-- Ensure that the predicate folds to constant FALSE +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS UNKNOWN; +SELECT id FROM bool_tab WHERE flag_nn IS UNKNOWN; + +-- Ensure that the predicate folds to constant TRUE +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT UNKNOWN; +SELECT id FROM bool_tab WHERE flag_nn IS NOT UNKNOWN; + +-- Ensure that the predicate folds to flag_nn +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS TRUE; +SELECT id FROM bool_tab WHERE flag_nn IS TRUE; + +-- Ensure that the predicate folds to flag_nn, and thus can use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT FALSE; +SELECT id FROM bool_tab WHERE flag_nn IS NOT FALSE; +RESET enable_seqscan; + +-- Ensure that the predicate folds to not flag_nn +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS FALSE; +SELECT id FROM bool_tab WHERE flag_nn IS FALSE; + +-- Ensure that the predicate folds to not flag_nn, and thus can use index scan +SET enable_seqscan TO off; +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_nn IS NOT TRUE; +SELECT id FROM bool_tab WHERE flag_nn IS NOT TRUE; +RESET enable_seqscan; + +-- Ensure that the predicate is preserved as a BooleanTest +EXPLAIN (COSTS OFF) +SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; +SELECT id FROM bool_tab WHERE flag_null IS UNKNOWN; + +DROP TABLE bool_tab; diff --git a/src/test/regress/sql/prepared_xacts.sql b/src/test/regress/sql/prepared_xacts.sql index ade3a2672a80a..b0712b153e07f 100644 --- a/src/test/regress/sql/prepared_xacts.sql +++ b/src/test/regress/sql/prepared_xacts.sql @@ -1,3 +1,8 @@ +SELECT current_setting('max_prepared_transactions')::integer < 2 AS skip_test \gset +\if :skip_test +\quit +\endif + -- -- PREPARED TRANSACTIONS (two-phase commit) -- @@ -158,7 +163,32 @@ SELECT * FROM pxtest3; -- There should be no prepared transactions SELECT gid FROM pg_prepared_xacts WHERE gid ~ '^regress_' ORDER BY gid; + +-- Test row-level locks held by prepared transactions +CREATE TABLE pxtest_rowlock (id int PRIMARY KEY, data text); +INSERT INTO pxtest_rowlock VALUES (1, 'test data'); + +BEGIN; +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE; +PREPARE TRANSACTION 'regress_p1'; + +-- Should fail because the row is locked +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT; + +-- Test prepared transactions that participate in multixacts. For +-- that, lock the same row again, creating a multixid. +BEGIN; +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR SHARE; +PREPARE TRANSACTION 'regress_p2'; + +-- Should fail because the row is locked +SELECT * FROM pxtest_rowlock WHERE id = 1 FOR UPDATE NOWAIT; + +ROLLBACK PREPARED 'regress_p1'; +ROLLBACK PREPARED 'regress_p2'; + -- Clean up DROP TABLE pxtest2; -DROP TABLE pxtest3; -- will still be there if prepared xacts are disabled +-- pxtest3 was already dropped DROP TABLE pxtest4; +DROP TABLE pxtest_rowlock; diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql index f337aa67c13f2..95a46854b3770 100644 --- a/src/test/regress/sql/privileges.sql +++ b/src/test/regress/sql/privileges.sql @@ -90,21 +90,6 @@ CREATE USER regress_priv_user3; CREATE USER regress_priv_user4; CREATE USER regress_priv_user5; --- DROP OWNED should also act on granted and granted-to roles -GRANT regress_priv_user1 TO regress_priv_user2; -GRANT regress_priv_user2 TO regress_priv_user3; -SELECT roleid::regrole, member::regrole FROM pg_auth_members - WHERE roleid IN ('regress_priv_user1'::regrole,'regress_priv_user2'::regrole) - ORDER BY roleid::regrole::text; -REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4; -- no effect -SELECT roleid::regrole, member::regrole FROM pg_auth_members - WHERE roleid IN ('regress_priv_user1'::regrole,'regress_priv_user2'::regrole) - ORDER BY roleid::regrole::text; -DROP OWNED BY regress_priv_user2; -- removes both grants -SELECT roleid::regrole, member::regrole FROM pg_auth_members - WHERE roleid IN ('regress_priv_user1'::regrole,'regress_priv_user2'::regrole) - ORDER BY roleid::regrole::text; - GRANT pg_read_all_data TO regress_priv_user6; GRANT pg_write_all_data TO regress_priv_user7; GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION; @@ -346,8 +331,6 @@ CREATE VIEW atest12v AS SELECT * FROM atest12 WHERE b <<< 5; CREATE VIEW atest12sbv WITH (security_barrier=true) AS SELECT * FROM atest12 WHERE b <<< 5; -GRANT SELECT ON atest12v TO PUBLIC; -GRANT SELECT ON atest12sbv TO PUBLIC; -- This plan should use nestloop, knowing that few rows will be selected. EXPLAIN (COSTS OFF) SELECT * FROM atest12v x, atest12v y WHERE x.a = y.b; @@ -369,8 +352,16 @@ CREATE FUNCTION leak2(integer,integer) RETURNS boolean CREATE OPERATOR >>> (procedure = leak2, leftarg = integer, rightarg = integer, restrict = scalargtsel); --- This should not show any "leak" notices before failing. +-- These should not show any "leak" notices before failing. EXPLAIN (COSTS OFF) SELECT * FROM atest12 WHERE a >>> 0; +EXPLAIN (COSTS OFF) SELECT * FROM atest12v WHERE a >>> 0; +EXPLAIN (COSTS OFF) SELECT * FROM atest12sbv WHERE a >>> 0; + +-- Now regress_priv_user1 grants access to regress_priv_user2 via the views. +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT SELECT ON atest12v TO PUBLIC; +GRANT SELECT ON atest12sbv TO PUBLIC; +SET SESSION AUTHORIZATION regress_priv_user2; -- These plans should continue to use a nestloop, since they execute with the -- privileges of the view owner. @@ -574,6 +565,24 @@ INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set three = EXCLU INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set three = EXCLUDED.three; INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set one = 8; -- fails (due to UPDATE) INSERT INTO atest5(three) VALUES (4) ON CONFLICT (two) DO UPDATE set three = 10; -- fails (due to INSERT) +-- Check that column level privileges are enforced for ON CONFLICT ... WHERE +-- Ok. we may select one +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.one = 1 RETURNING atest5.two; +-- Error. No select rights on three +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.three = 1 RETURNING atest5.two; + +-- Check that ON CONFLICT ... SELECT FOR UPDATE/SHARE requires an updatable column +SET SESSION AUTHORIZATION regress_priv_user1; +REVOKE UPDATE (three) ON atest5 FROM regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; + +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- fails + +SET SESSION AUTHORIZATION regress_priv_user1; +GRANT UPDATE (three) ON atest5 TO regress_priv_user4; +SET SESSION AUTHORIZATION regress_priv_user4; + +INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- ok -- Check that the columns in the inference require select privileges INSERT INTO atest5(four) VALUES (4); -- fail @@ -774,6 +783,33 @@ UPDATE errtst SET a = 'aaaa', b = NULL WHERE a = 'aaa'; SET SESSION AUTHORIZATION regress_priv_user1; DROP TABLE errtst; +-- test column-level privileges on the range used in FOR PORTION OF +SET SESSION AUTHORIZATION regress_priv_user1; +CREATE TABLE t1 ( + c1 int4range, + valid_at tsrange, + CONSTRAINT t1pk PRIMARY KEY (c1, valid_at WITHOUT OVERLAPS) +); +-- UPDATE requires select permission on the valid_at column (but not update): +GRANT SELECT (c1) ON t1 TO regress_priv_user2; +GRANT UPDATE (c1) ON t1 TO regress_priv_user2; +GRANT SELECT (c1, valid_at) ON t1 TO regress_priv_user3; +GRANT UPDATE (c1) ON t1 TO regress_priv_user3; +SET SESSION AUTHORIZATION regress_priv_user2; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user3; +UPDATE t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01' SET c1 = '[2,3)'; +SET SESSION AUTHORIZATION regress_priv_user1; +-- DELETE requires select permission on the valid_at column: +GRANT DELETE ON t1 TO regress_priv_user2; +GRANT DELETE ON t1 TO regress_priv_user3; +SET SESSION AUTHORIZATION regress_priv_user2; +DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01'; +SET SESSION AUTHORIZATION regress_priv_user3; +DELETE FROM t1 FOR PORTION OF valid_at FROM '2000-01-01' TO '2001-01-01'; +SET SESSION AUTHORIZATION regress_priv_user1; +DROP TABLE t1; + -- test column-level privileges when involved with DELETE SET SESSION AUTHORIZATION regress_priv_user1; ALTER TABLE atest6 ADD COLUMN three integer; @@ -1375,6 +1411,27 @@ SELECT loread(lo_open(1005, x'40000'::int), 32); SELECT lo_truncate(lo_open(1005, x'20000'::int), 10); -- to be denied SELECT lo_truncate(lo_open(2001, x'20000'::int), 10); +\c - +-- confirm role with privileges of pg_read_all_data can read large objects +SET SESSION AUTHORIZATION regress_priv_user6; + +SELECT loread(lo_open(1002, x'40000'::int), 32); +SELECT lo_get(1002); +SELECT lowrite(lo_open(1002, x'20000'::int), 'abcd'); -- to be denied +SELECT lo_put(1002, 1, 'abcd'); -- to be denied +SELECT lo_truncate(lo_open(1002, x'20000'::int), 0); -- to be denied +SELECT lo_unlink(1002); -- to be denied + +\c - +-- confirm role with privileges of pg_write_all_data can write large objects +GRANT SELECT ON LARGE OBJECT 1002 TO regress_priv_user7; +SET SESSION AUTHORIZATION regress_priv_user7; + +SELECT lowrite(lo_open(1002, x'20000'::int), 'abcd'); +SELECT lo_put(1002, 1, 'abcd'); +SELECT lo_truncate(lo_open(1002, x'20000'::int), 0); +SELECT lo_unlink(1002); -- to be denied + -- has_largeobject_privilege function -- superuser @@ -1544,6 +1601,14 @@ SELECT makeaclitem('regress_priv_user1'::regrole, 'regress_priv_user2'::regrole, SELECT makeaclitem('regress_priv_user1'::regrole, 'regress_priv_user2'::regrole, 'SELECT, fake_privilege', FALSE); -- error +-- Test quoting and dequoting of user names in ACLs +CREATE ROLE "regress_""quoted"; +SELECT makeaclitem('regress_"quoted'::regrole, 'regress_"quoted'::regrole, + 'SELECT', TRUE); +SELECT '"regress_""quoted"=r*/"regress_""quoted"'::aclitem; +SELECT '""=r*/""'::aclitem; -- used to be misparsed as """" +DROP ROLE "regress_""quoted"; + -- Test non-throwing aclitem I/O SELECT pg_input_is_valid('regress_priv_user1=r/regress_priv_user2', 'aclitem'); SELECT pg_input_is_valid('regress_priv_user1=r/', 'aclitem'); @@ -1602,7 +1667,7 @@ ALTER DEFAULT PRIVILEGES GRANT SELECT ON LARGE OBJECTS TO regress_priv_user2; SELECT lo_create(1008); SELECT has_largeobject_privilege('regress_priv_user2', 1008, 'SELECT'); -- yes -SELECT has_largeobject_privilege('regress_priv_user6', 1008, 'SELECT'); -- no +SELECT has_largeobject_privilege('regress_priv_user3', 1008, 'SELECT'); -- no SELECT has_largeobject_privilege('regress_priv_user2', 1008, 'UPDATE'); -- no ALTER DEFAULT PRIVILEGES GRANT ALL ON LARGE OBJECTS TO regress_priv_user2; @@ -1800,6 +1865,60 @@ revoke select on dep_priv_test from regress_priv_user4 cascade; set session role regress_priv_user1; drop table dep_priv_test; +-- +-- Property graphs +-- +set session role regress_priv_user1; +create property graph ptg1 + vertex tables ( + atest5 key (four) + default label properties (four) + label lttc properties (three as lttck), + atest1 key (a) + default label + label lttc properties (a as lttck), + atest2 key (col1) + default label + label ltv properties (col1 as ltvk)); +-- select privileges on property graph as well as table +select * from graph_table (ptg1 match (is atest5) COLUMNS (1 as value)) limit 0; -- ok +grant select on ptg1 to regress_priv_user2; +set session role regress_priv_user2; +select * from graph_table (ptg1 match (is atest1) COLUMNS (1 as value)) limit 0; -- ok +-- select privileges on property graph but not table +select * from graph_table (ptg1 match (is atest5) COLUMNS (1 as value)) limit 0; -- fails +select * from graph_table (ptg1 match (is lttc) COLUMNS (1 as value)) limit 0; -- fails +set session role regress_priv_user3; +-- select privileges on table but not property graph +select * from graph_table (ptg1 match (is atest1) COLUMNS (1 as value)) limit 0; -- fails +-- select privileges on neither +select * from graph_table (ptg1 match (is atest5) COLUMNS (1 as value)) limit 0; -- fails +-- column privileges +set session role regress_priv_user1; +select * from graph_table (ptg1 match (v is lttc) COLUMNS (v.lttck)) limit 0; -- ok +grant select on ptg1 to regress_priv_user4; +set session role regress_priv_user4; +select * from graph_table (ptg1 match (a is atest5) COLUMNS (a.four)) limit 0; -- ok +select * from graph_table (ptg1 match (v is lttc) COLUMNS (v.lttck)) limit 0; -- fail +-- access property graph through security definer view +set session role regress_priv_user4; +create view atpgv1 as select * from graph_table (ptg1 match (is atest1) COLUMNS (1 as value)) limit 0; +grant select on atpgv1 to regress_priv_user3; +select * from atpgv1; -- ok +set session role regress_priv_user3; +select * from atpgv1; -- ok +set session role regress_priv_user4; +create view atpgv2 as select * from graph_table (ptg1 match (v is ltv) COLUMNS (v.ltvk)) limit 0; +-- though the session user is the owner of the view and also has access to the +-- property graph, it does not have access to a table referenced in the graph +-- pattern +select * from atpgv2; -- fail +grant select on atpgv2 to regress_priv_user2; +-- The user who otherwise does not have access to the property graph, gets +-- access to it through a security definer view and uses it successfully since +-- it has access to the tables referenced in the graph pattern. +set session role regress_priv_user2; +select * from atpgv2; -- ok -- clean up @@ -1807,6 +1926,10 @@ drop table dep_priv_test; drop sequence x_seq; +drop view atpgv1; +drop view atpgv2; +drop property graph ptg1; + DROP AGGREGATE priv_testagg1(int); DROP FUNCTION priv_testfunc2(int); DROP FUNCTION priv_testfunc4(boolean); @@ -1849,6 +1972,13 @@ DROP USER regress_priv_user7; DROP USER regress_priv_user8; -- does not exist +-- leave some default ACLs for pg_upgrade's dump-restore test input. +ALTER DEFAULT PRIVILEGES FOR ROLE pg_signal_backend + REVOKE USAGE ON TYPES FROM pg_signal_backend; +ALTER DEFAULT PRIVILEGES FOR ROLE pg_read_all_settings + REVOKE USAGE ON TYPES FROM pg_read_all_settings; + + -- permissions with LOCK TABLE CREATE USER regress_locktable_user; CREATE TABLE lock_table (a int); @@ -1948,7 +2078,8 @@ DROP TABLE lock_table; DROP USER regress_locktable_user; -- test to check privileges of system views pg_shmem_allocations, --- pg_shmem_allocations_numa and pg_backend_memory_contexts. +-- pg_shmem_allocations_numa, pg_dsm_registry_allocations, and +-- pg_backend_memory_contexts. -- switch to superuser \c - @@ -1959,6 +2090,7 @@ SELECT has_table_privilege('regress_readallstats','pg_aios','SELECT'); -- no SELECT has_table_privilege('regress_readallstats','pg_backend_memory_contexts','SELECT'); -- no SELECT has_table_privilege('regress_readallstats','pg_shmem_allocations','SELECT'); -- no SELECT has_table_privilege('regress_readallstats','pg_shmem_allocations_numa','SELECT'); -- no +SELECT has_table_privilege('regress_readallstats','pg_dsm_registry_allocations','SELECT'); -- no GRANT pg_read_all_stats TO regress_readallstats; @@ -1966,6 +2098,7 @@ SELECT has_table_privilege('regress_readallstats','pg_aios','SELECT'); -- yes SELECT has_table_privilege('regress_readallstats','pg_backend_memory_contexts','SELECT'); -- yes SELECT has_table_privilege('regress_readallstats','pg_shmem_allocations','SELECT'); -- yes SELECT has_table_privilege('regress_readallstats','pg_shmem_allocations_numa','SELECT'); -- yes +SELECT has_table_privilege('regress_readallstats','pg_dsm_registry_allocations','SELECT'); -- yes -- run query to ensure that functions within views can be executed SET ROLE regress_readallstats; @@ -2105,3 +2238,37 @@ SELECT * FROM information_schema.table_privileges t DROP TABLE grantor_test1, grantor_test2, grantor_test3; DROP ROLE regress_grantor1, regress_grantor2, regress_grantor3; + +-- GRANTED BY +CREATE ROLE regress_grantor1; +CREATE ROLE regress_grantor2 ROLE regress_grantor1; +CREATE ROLE regress_grantor3 ROLE regress_grantor1; +CREATE ROLE regress_grantor4 ROLE regress_grantor1; +CREATE ROLE regress_grantor5; +CREATE TABLE grantor_test (); +GRANT SELECT ON grantor_test TO regress_grantor2 WITH GRANT OPTION; +GRANT UPDATE ON grantor_test TO regress_grantor3 WITH GRANT OPTION; +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor4 WITH GRANT OPTION; +SET ROLE regress_grantor1; + +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor5; + +SELECT * FROM information_schema.table_privileges t + WHERE grantor LIKE 'regress_grantor%' ORDER BY ROW(t.*); + +REVOKE SELECT, UPDATE ON grantor_test FROM regress_grantor5; +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor5 GRANTED BY regress_grantor2; +GRANT SELECT, UPDATE ON grantor_test TO regress_grantor5 GRANTED BY regress_grantor3; + +SELECT * FROM information_schema.table_privileges t + WHERE grantor LIKE 'regress_grantor%' ORDER BY ROW(t.*); + +REVOKE SELECT, UPDATE ON grantor_test FROM regress_grantor5 GRANTED BY regress_grantor2; +REVOKE SELECT, UPDATE ON grantor_test FROM regress_grantor5 GRANTED BY regress_grantor3; + +SELECT * FROM information_schema.table_privileges t + WHERE grantor LIKE 'regress_grantor%' ORDER BY ROW(t.*); + +RESET ROLE; +DROP TABLE grantor_test; +DROP ROLE regress_grantor1, regress_grantor2, regress_grantor3, regress_grantor4, regress_grantor5; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1a8a83462f022..dcdbd4fc02091 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -68,11 +68,11 @@ SELECT $1, $2 \parse stmt3 -- Multiple \g calls mean multiple executions \bind_named stmt2 'foo3' \g \bind_named stmt3 'foo4' 'bar4' \g --- \close (extended query protocol) -\close -\close '' -\close stmt2 -\close stmt2 +-- \close_prepared (extended query protocol) +\close_prepared +\close_prepared '' +\close_prepared stmt2 +\close_prepared stmt2 SELECT name, statement FROM pg_prepared_statements ORDER BY name; -- \bind (extended query protocol) @@ -219,6 +219,22 @@ select 'drop table gexec_test', 'select ''2000-01-01''::date as party_over' -- show all pset options \pset +-- test the simple display substitution settings +prepare q as select null as n, true as t, false as f; +\pset null '(null)' +\pset display_true 'true' +\pset display_false 'false' +execute q; +\pset null +\pset display_true +\pset display_false +execute q; +\pset null '' +\pset display_true 't' +\pset display_false 'f' +execute q; +deallocate q; + -- test multi-line headers, wrapping, and newline indicators -- in aligned, unaligned, and wrapped formats prepare q as select array_to_string(array_agg(repeat('x',2*n)),E'\n') as "ab @@ -1035,7 +1051,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \C arg1 \c arg1 arg2 arg3 arg4 \cd arg1 - \close stmt1 + \close_prepared stmt1 \conninfo \copy arg1 arg2 arg3 arg4 arg5 arg6 \copyright @@ -1073,6 +1089,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \pset arg1 arg2 \q \reset + \restrict test \s arg1 \sendpipeline \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 @@ -1084,6 +1101,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \t arg1 \T arg1 \timing arg1 + \unrestrict not_valid \unset arg1 \w arg1 \watch arg1 arg2 diff --git a/src/test/regress/sql/psql_pipeline.sql b/src/test/regress/sql/psql_pipeline.sql index 16e1e1e84cd15..6788dceee2e90 100644 --- a/src/test/regress/sql/psql_pipeline.sql +++ b/src/test/regress/sql/psql_pipeline.sql @@ -105,106 +105,6 @@ INSERT INTO psql_pipeline VALUES ($1) \bind 1 \sendpipeline COMMIT \bind \sendpipeline \endpipeline --- COPY FROM STDIN --- with \sendpipeline and \bind -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -COPY psql_pipeline FROM STDIN \bind \sendpipeline -\endpipeline -2 test2 -\. --- with semicolon -\startpipeline -SELECT 'val1'; -COPY psql_pipeline FROM STDIN; -\endpipeline -20 test2 -\. - --- COPY FROM STDIN with \flushrequest + \getresults --- with \sendpipeline and \bind -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -COPY psql_pipeline FROM STDIN \bind \sendpipeline -\flushrequest -\getresults -3 test3 -\. -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -COPY psql_pipeline FROM STDIN; -\flushrequest -\getresults -30 test3 -\. -\endpipeline - --- COPY FROM STDIN with \syncpipeline + \getresults --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -COPY psql_pipeline FROM STDIN \bind \sendpipeline -\syncpipeline -\getresults -4 test4 -\. -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -COPY psql_pipeline FROM STDIN; -\syncpipeline -\getresults -40 test4 -\. -\endpipeline - --- COPY TO STDOUT --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -copy psql_pipeline TO STDOUT \bind \sendpipeline -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -copy psql_pipeline TO STDOUT; -\endpipeline - --- COPY TO STDOUT with \flushrequest + \getresults --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -copy psql_pipeline TO STDOUT \bind \sendpipeline -\flushrequest -\getresults -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -copy psql_pipeline TO STDOUT; -\flushrequest -\getresults -\endpipeline - --- COPY TO STDOUT with \syncpipeline + \getresults --- with \bind and \sendpipeline -\startpipeline -SELECT $1 \bind 'val1' \sendpipeline -copy psql_pipeline TO STDOUT \bind \sendpipeline -\syncpipeline -\getresults -\endpipeline --- with semicolon -\startpipeline -SELECT 'val1'; -copy psql_pipeline TO STDOUT; -\syncpipeline -\getresults -\endpipeline - -- Use \parse and \bind_named \startpipeline SELECT $1 \parse '' @@ -406,21 +306,21 @@ SELECT $1 \bind \sendpipeline SELECT $1 \bind 1 \sendpipeline SELECT $1 \parse a \bind_named a 1 \sendpipeline -\close a +\close_prepared a \flushrequest \getresults -- Pipeline is aborted. SELECT $1 \bind 1 \sendpipeline SELECT $1 \parse a \bind_named a 1 \sendpipeline -\close a +\close_prepared a -- Sync allows pipeline to recover. \syncpipeline \getresults SELECT $1 \bind 1 \sendpipeline SELECT $1 \parse a \bind_named a 1 \sendpipeline -\close a +\close_prepared a \flushrequest \getresults \endpipeline diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 68001de4000fd..642e32fa098da 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -26,6 +26,7 @@ CREATE PUBLICATION testpub_xxx WITH (publish = 'cluster, vacuum'); CREATE PUBLICATION testpub_xxx WITH (publish_via_partition_root = 'true', publish_via_partition_root = '0'); CREATE PUBLICATION testpub_xxx WITH (publish_generated_columns = stored, publish_generated_columns = none); CREATE PUBLICATION testpub_xxx WITH (publish_generated_columns = foo); +CREATE PUBLICATION testpub_xxx WITH (publish_generated_columns); \dRp @@ -104,20 +105,162 @@ SELECT pubname, puballtables FROM pg_publication WHERE pubname = 'testpub_forall \d+ testpub_tbl2 \dRp+ testpub_foralltables -DROP TABLE testpub_tbl2; +--------------------------------------------- +-- EXCEPT clause tests for normal tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_tbl3 (id serial primary key, data text); +-- Specify table list in the EXCEPT clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable FOR ALL TABLES EXCEPT (TABLE testpub_tbl1, testpub_tbl2, TABLE testpub_tbl3); +\dRp+ testpub_foralltables_excepttable +-- Specify table in the EXCEPT clause of a FOR ALL TABLES publication +CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (TABLE testpub_tbl1); +\dRp+ testpub_foralltables_excepttable1 +-- Check that the table description shows the publications where it is listed +-- in the EXCEPT clause +\d testpub_tbl1 +-- fail - first table in the EXCEPT list should use TABLE keyword +CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); + +--------------------------------------------- +-- SET ALL TABLES/SEQUENCES +--------------------------------------------- +-- Replace the existing table list in the EXCEPT clause (testpub_tbl1, +-- testpub_tbl2, testpub_tbl3) with table (testpub_tbl2). +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES EXCEPT (TABLE testpub_tbl2); +\dRp+ testpub_foralltables_excepttable + +-- Replace the existing table list in the EXCEPT clause (testpub_tbl2) with a +-- table list containing (testpub_tbl1, testpub_tbl2, testpub_tbl3). +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES EXCEPT (TABLE testpub_tbl1, testpub_tbl2, TABLE testpub_tbl3); +\dRp+ testpub_foralltables_excepttable + +-- Clear the table list in the EXCEPT clause, making the publication include all +-- tables. +ALTER PUBLICATION testpub_foralltables_excepttable SET ALL TABLES; +\dRp+ testpub_foralltables_excepttable + +-- Create an empty publication for subsequent tests. +CREATE PUBLICATION testpub_forall_tbls_seqs; + +-- Enable both puballtables and puballsequences +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL TABLES, ALL SEQUENCES; +\dRp+ testpub_forall_tbls_seqs + +-- Explicitly test that SET ALL TABLES resets puballsequences to false +-- Result should be: puballtables = true, puballsequences = false +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL TABLES; +\dRp+ testpub_forall_tbls_seqs + +-- Explicitly test that SET ALL SEQUENCES resets puballtables to false +-- Result should be: puballtables = false, puballsequences = true +ALTER PUBLICATION testpub_forall_tbls_seqs SET ALL SEQUENCES; +\dRp+ testpub_forall_tbls_seqs + +-- fail - SET ALL TABLES/SEQUENCES is not allowed for a 'FOR TABLE' publication +ALTER PUBLICATION testpub_fortable SET ALL TABLES EXCEPT (TABLE testpub_tbl1); +ALTER PUBLICATION testpub_fortable SET ALL TABLES; +ALTER PUBLICATION testpub_fortable SET ALL SEQUENCES; + +-- fail - SET ALL TABLES/SEQUENCES is not allowed for a schema publication +ALTER PUBLICATION testpub_forschema SET ALL TABLES EXCEPT (TABLE pub_test.testpub_nopk); +ALTER PUBLICATION testpub_forschema SET ALL TABLES; +ALTER PUBLICATION testpub_forschema SET ALL SEQUENCES; + +RESET client_min_messages; +DROP TABLE testpub_tbl2, testpub_tbl3; DROP PUBLICATION testpub_foralltables, testpub_fortable, testpub_forschema, testpub_for_tbl_schema; +DROP PUBLICATION testpub_forall_tbls_seqs, testpub_foralltables_excepttable, testpub_foralltables_excepttable1; -CREATE TABLE testpub_tbl3 (a int); -CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3); +--------------------------------------------- +-- Tests for inherited tables, and +-- EXCEPT clause tests for inherited tables +--------------------------------------------- SET client_min_messages = 'ERROR'; -CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3; -CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3; -RESET client_min_messages; +CREATE TABLE testpub_tbl_parent (a int); +CREATE TABLE testpub_tbl_child (b text) INHERITS (testpub_tbl_parent); +CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl_parent; \dRp+ testpub3 +CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl_parent; \dRp+ testpub4 +-- List the parent table in the EXCEPT clause (without ONLY or '*') +CREATE PUBLICATION testpub5 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent); +\dRp+ testpub5 +-- EXCEPT with '*': list the table and all its descendants in the EXCEPT clause +CREATE PUBLICATION testpub6 FOR ALL TABLES EXCEPT (TABLE testpub_tbl_parent *); +\dRp+ testpub6 +-- EXCEPT with ONLY: list the table in the EXCEPT clause, but not its descendants +CREATE PUBLICATION testpub7 FOR ALL TABLES EXCEPT (TABLE ONLY testpub_tbl_parent); +\dRp+ testpub7 + +RESET client_min_messages; +DROP TABLE testpub_tbl_parent, testpub_tbl_child; +DROP PUBLICATION testpub3, testpub4, testpub5, testpub6, testpub7; -DROP TABLE testpub_tbl3, testpub_tbl3a; -DROP PUBLICATION testpub3, testpub4; +--------------------------------------------- +-- EXCEPT clause tests for partitioned tables +--------------------------------------------- +SET client_min_messages = 'ERROR'; +CREATE TABLE testpub_root(a int) PARTITION BY RANGE(a); +CREATE TABLE testpub_part1 PARTITION OF testpub_root FOR VALUES FROM (0) TO (100); +CREATE PUBLICATION testpub8 FOR ALL TABLES EXCEPT (TABLE testpub_root); +\dRp+ testpub8; +\d testpub_part1 +\d testpub_root +CREATE PUBLICATION testpub9 FOR ALL TABLES EXCEPT (TABLE testpub_part1); + +CREATE TABLE tab_main (a int) PARTITION BY RANGE(a); +-- Attaching a partition is not allowed if the partitioned table appears in a +-- publication's EXCEPT clause. +ALTER TABLE tab_main ATTACH PARTITION testpub_root FOR VALUES FROM (0) TO (200); + +RESET client_min_messages; +DROP TABLE testpub_root, testpub_part1, tab_main; +DROP PUBLICATION testpub8; + +--- Tests for publications with SEQUENCES +CREATE SEQUENCE regress_pub_seq0; +CREATE SEQUENCE pub_test.regress_pub_seq1; + +-- FOR ALL SEQUENCES +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION regress_pub_forallsequences1 FOR ALL SEQUENCES; +RESET client_min_messages; + +SELECT pubname, puballtables, puballsequences FROM pg_publication WHERE pubname = 'regress_pub_forallsequences1'; +\d+ regress_pub_seq0 +\dRp+ regress_pub_forallsequences1 + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION regress_pub_forallsequences2 FOR ALL SEQUENCES; +RESET client_min_messages; + +-- check that describe sequence lists both publications the sequence belongs to +\d+ pub_test.regress_pub_seq1 + +--- Specifying both ALL TABLES and ALL SEQUENCES +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES; + +-- Specifying WITH clause in an ALL SEQUENCES publication will emit a NOTICE. +SET client_min_messages = 'NOTICE'; +ALTER PUBLICATION regress_pub_for_allsequences_alltables SET (publish = 'insert'); +ALTER PUBLICATION regress_pub_for_allsequences_alltables SET (publish_generated_columns = 'stored'); +RESET client_min_messages; + +SELECT pubname, puballtables, puballsequences FROM pg_publication WHERE pubname = 'regress_pub_for_allsequences_alltables'; +\dRp+ regress_pub_for_allsequences_alltables + +DROP SEQUENCE regress_pub_seq0, pub_test.regress_pub_seq1; +DROP PUBLICATION regress_pub_forallsequences1; +DROP PUBLICATION regress_pub_forallsequences2; +DROP PUBLICATION regress_pub_for_allsequences_alltables; + +-- fail - Specifying ALL TABLES more than once +CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL TABLES; + +-- fail - Specifying ALL SEQUENCES more than once +CREATE PUBLICATION regress_pub_for_allsequences_alltables FOR ALL SEQUENCES, ALL TABLES, ALL SEQUENCES; -- Tests for partitioned tables SET client_min_messages = 'ERROR'; @@ -262,6 +405,9 @@ ALTER PUBLICATION testpub6 SET TABLES IN SCHEMA testpub_rf_schema2, TABLE testpu RESET client_min_messages; \dRp+ testpub6 -- fail - virtual generated column uses user-defined function +-- (Actually, this already fails at CREATE TABLE rather than at CREATE +-- PUBLICATION, but let's keep the test in case the former gets +-- relaxed sometime.) CREATE TABLE testpub_rf_tbl6 (id int PRIMARY KEY, x int, y int GENERATED ALWAYS AS (x * testpub_rf_func2()) VIRTUAL); CREATE PUBLICATION testpub7 FOR TABLE testpub_rf_tbl6 WHERE (y > 100); -- test that SET EXPRESSION is rejected, because it could affect a row filter @@ -276,7 +422,7 @@ DROP TABLE testpub_rf_tbl2; DROP TABLE testpub_rf_tbl3; DROP TABLE testpub_rf_tbl4; DROP TABLE testpub_rf_tbl5; -DROP TABLE testpub_rf_tbl6; +--DROP TABLE testpub_rf_tbl6; DROP TABLE testpub_rf_schema1.testpub_rf_tbl5; DROP TABLE testpub_rf_schema2.testpub_rf_tbl6; DROP SCHEMA testpub_rf_schema1; @@ -894,7 +1040,18 @@ ALTER PUBLICATION testpub4 owner to regress_publication_user2; -- fail ALTER PUBLICATION testpub4 owner to regress_publication_user; -- ok SET ROLE regress_publication_user; -DROP PUBLICATION testpub4; +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION testpub5 FOR ALL TABLES; +RESET client_min_messages; +ALTER PUBLICATION testpub5 OWNER TO regress_publication_user3; +SET ROLE regress_publication_user3; +-- fail - SET ALL TABLES/SEQUENCES on a publication requires superuser privileges +ALTER PUBLICATION testpub5 SET ALL TABLES EXCEPT (TABLE testpub_tbl1); -- fail +ALTER PUBLICATION testpub5 SET ALL TABLES; -- fail +ALTER PUBLICATION testpub5 SET ALL SEQUENCES; -- fail + +SET ROLE regress_publication_user; +DROP PUBLICATION testpub4, testpub5; DROP ROLE regress_publication_user3; REVOKE CREATE ON DATABASE regression FROM regress_publication_user2; @@ -1180,19 +1337,15 @@ DROP SCHEMA sch2 cascade; -- ====================================================== -- Test the 'publish_generated_columns' parameter with the following values: --- 'stored', 'none', and the default (no value specified), which defaults to --- 'stored'. +-- 'stored', 'none'. SET client_min_messages = 'ERROR'; CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_generated_columns = stored); \dRp+ pub1 CREATE PUBLICATION pub2 FOR ALL TABLES WITH (publish_generated_columns = none); \dRp+ pub2 -CREATE PUBLICATION pub3 FOR ALL TABLES WITH (publish_generated_columns); -\dRp+ pub3 DROP PUBLICATION pub1; DROP PUBLICATION pub2; -DROP PUBLICATION pub3; -- Test the 'publish_generated_columns' parameter as 'none' and 'stored' for -- different scenarios with/without generated columns in column lists. @@ -1223,6 +1376,193 @@ DROP PUBLICATION pub2; DROP TABLE gencols; RESET client_min_messages; + +-- Test that the INSERT ON CONFLICT command correctly checks REPLICA IDENTITY +-- when the target table is published. +CREATE TABLE testpub_insert_onconfl_no_ri (a int unique, b int); +CREATE TABLE testpub_insert_onconfl_parted (a int unique, b int) PARTITION by RANGE (a); +CREATE TABLE testpub_insert_onconfl_part_no_ri PARTITION OF testpub_insert_onconfl_parted FOR VALUES FROM (1) TO (10); + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub1 FOR ALL TABLES; +RESET client_min_messages; + +-- fail - missing REPLICA IDENTITY +INSERT INTO testpub_insert_onconfl_no_ri VALUES (1, 1) ON CONFLICT (a) DO UPDATE SET b = 2; + +-- ok - no updates +INSERT INTO testpub_insert_onconfl_no_ri VALUES (1, 1) ON CONFLICT DO NOTHING; + +-- fail - missing REPLICA IDENTITY in partition testpub_insert_onconfl_no_ri +INSERT INTO testpub_insert_onconfl_parted VALUES (1, 1) ON CONFLICT (a) DO UPDATE SET b = 2; + +-- ok - no updates +INSERT INTO testpub_insert_onconfl_parted VALUES (1, 1) ON CONFLICT DO NOTHING; + +DROP PUBLICATION pub1; +DROP TABLE testpub_insert_onconfl_no_ri; +DROP TABLE testpub_insert_onconfl_parted; + +-- Test that the MERGE command correctly checks REPLICA IDENTITY when the +-- target table is published. +CREATE TABLE testpub_merge_no_ri (a int, b int); +CREATE TABLE testpub_merge_pk (a int primary key, b int); + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub1 FOR ALL TABLES; +RESET client_min_messages; + +-- fail - missing REPLICA IDENTITY +MERGE INTO testpub_merge_no_ri USING testpub_merge_pk s ON s.a >= 1 + WHEN MATCHED THEN UPDATE SET b = s.b; + +-- fail - missing REPLICA IDENTITY +MERGE INTO testpub_merge_no_ri USING testpub_merge_pk s ON s.a >= 1 + WHEN MATCHED THEN DELETE; + +-- ok - insert and do nothing are not restricted +MERGE INTO testpub_merge_no_ri USING testpub_merge_pk s ON s.a >= 1 + WHEN MATCHED THEN DO NOTHING + WHEN NOT MATCHED THEN INSERT (a, b) VALUES (0, 0); + +-- ok - REPLICA IDENTITY is DEFAULT and table has a PK +MERGE INTO testpub_merge_pk USING testpub_merge_no_ri s ON s.a >= 1 + WHEN MATCHED AND s.a > 0 THEN UPDATE SET b = s.b + WHEN MATCHED THEN DELETE; + +DROP PUBLICATION pub1; +DROP TABLE testpub_merge_no_ri; +DROP TABLE testpub_merge_pk; + RESET SESSION AUTHORIZATION; DROP ROLE regress_publication_user, regress_publication_user2; DROP ROLE regress_publication_user_dummy; + +-- Test pg_get_publication_tables(text[], oid) function +CREATE SCHEMA gpt_test_sch; +CREATE TABLE gpt_test_sch.tbl_sch (id int); +CREATE TABLE tbl_normal (id int); +CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1); +CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10); +CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal; + +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch; +CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10); +CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true); +CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false); +CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true); +RESET client_min_messages; + +CREATE FUNCTION test_gpt(pubnames text[], relname text) +RETURNS TABLE ( + pubname text, + relname name, + attrs text, + qual text +) +BEGIN ATOMIC + SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid) + FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt + JOIN pg_publication p ON p.oid = gpt.pubid + JOIN pg_class c ON c.oid = gpt.relid + ORDER BY p.pubname, c.relname; +END; + +SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal'); +SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch'); +SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent'); +SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1'); +SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1'); +SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent'); +SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1'); +SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result + +SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent'); +SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result + +-- test for the EXCEPT clause +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal'); +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal'); +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded) +SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result + +-- two rows with different row filter +SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal'); + +-- one row with 'pub_part_parent' +SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent'); + +-- no result, tbl_parent is the effective published OID due to pubviaroot +SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1'); + +-- no result, non-existent publication +SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal'); + +-- no result, non-table object +SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view'); + +-- no result, empty publication array +SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal'); + +-- no result, OID 0 as target_relid +SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid); + +-- Clean up +DROP FUNCTION test_gpt(text[], text); +DROP PUBLICATION pub_all; +DROP PUBLICATION pub_all_no_viaroot; +DROP PUBLICATION pub_all_except; +DROP PUBLICATION pub_all_except_no_viaroot; +DROP PUBLICATION pub_schema; +DROP PUBLICATION pub_normal; +DROP PUBLICATION pub_part_leaf; +DROP PUBLICATION pub_part_parent; +DROP PUBLICATION pub_part_parent_no_viaroot; +DROP PUBLICATION pub_part_parent_child; +DROP VIEW gpt_test_view; +DROP TABLE tbl_normal, tbl_parent, tbl_part1; +DROP SCHEMA gpt_test_sch CASCADE; + +-- stage objects for pg_dump tests +CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int); +CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int); +SET client_min_messages = 'ERROR'; +CREATE PUBLICATION dump_pub_qual_1ct FOR + TABLE ONLY pubme.t0 (c, d) WHERE (c > 0); +CREATE PUBLICATION dump_pub_qual_2ct FOR + TABLE ONLY pubme.t0 (c) WHERE (c > 0), + TABLE ONLY pubme.t1 (c); +CREATE PUBLICATION dump_pub_nsp_1ct FOR + TABLES IN SCHEMA pubme; +CREATE PUBLICATION dump_pub_nsp_2ct FOR + TABLES IN SCHEMA pubme, + TABLES IN SCHEMA pubme2; +CREATE PUBLICATION dump_pub_all FOR + TABLE ONLY pubme.t0, + TABLE ONLY pubme.t1 WHERE (c < 0), + TABLES IN SCHEMA pubme, + TABLES IN SCHEMA pubme2 + WITH (publish_via_partition_root = true); +RESET client_min_messages; diff --git a/src/test/regress/sql/random.sql b/src/test/regress/sql/random.sql index ebfa7539ede25..890f14687ef98 100644 --- a/src/test/regress/sql/random.sql +++ b/src/test/regress/sql/random.sql @@ -277,3 +277,29 @@ SELECT random(-1e30, 1e30) FROM generate_series(1, 10); SELECT random(-0.4, 0.4) FROM generate_series(1, 10); SELECT random(0, 1 - 1e-30) FROM generate_series(1, 10); SELECT n, random(0, trim_scale(abs(1 - 10.0^(-n)))) FROM generate_series(-20, 20) n; + +-- random dates +SELECT random('1979-02-08'::date,'2025-07-03'::date) AS random_date_multiple_years; +SELECT random('4714-11-24 BC'::date,'5874897-12-31 AD'::date) AS random_date_maximum_range; +SELECT random('1979-02-08'::date,'1979-02-08'::date) AS random_date_empty_range; +SELECT random('2024-12-31'::date, '2024-01-01'::date); -- fail +SELECT random('-infinity'::date, '2024-01-01'::date); -- fail +SELECT random('2024-12-31'::date, 'infinity'::date); -- fail + +-- random timestamps +SELECT random('1979-02-08'::timestamp,'2025-07-03'::timestamp) AS random_timestamp_multiple_years; +SELECT random('4714-11-24 BC'::timestamp,'294276-12-31 23:59:59.999999'::timestamp) AS random_timestamp_maximum_range; +SELECT random('2024-07-01 12:00:00.000001'::timestamp, '2024-07-01 12:00:00.999999'::timestamp) AS random_narrow_range; +SELECT random('1979-02-08'::timestamp,'1979-02-08'::timestamp) AS random_timestamp_empty_range; +SELECT random('2024-12-31'::timestamp, '2024-01-01'::timestamp); -- fail +SELECT random('-infinity'::timestamp, '2024-01-01'::timestamp); -- fail +SELECT random('2024-12-31'::timestamp, 'infinity'::timestamp); -- fail + +-- random timestamps with timezone +SELECT random('1979-02-08 +01'::timestamptz,'2025-07-03 +02'::timestamptz) AS random_timestamptz_multiple_years; +SELECT random('4714-11-24 BC +00'::timestamptz,'294276-12-31 23:59:59.999999 +00'::timestamptz) AS random_timestamptz_maximum_range; +SELECT random('2024-07-01 12:00:00.000001 +04'::timestamptz, '2024-07-01 12:00:00.999999 +04'::timestamptz) AS random_timestamptz_narrow_range; +SELECT random('1979-02-08 +05'::timestamptz,'1979-02-08 +05'::timestamptz) AS random_timestamptz_empty_range; +SELECT random('2024-01-01 +06'::timestamptz, '2024-01-01 +07'::timestamptz); -- fail +SELECT random('-infinity'::timestamptz, '2024-01-01 +07'::timestamptz); -- fail +SELECT random('2024-01-01 +06'::timestamptz, 'infinity'::timestamptz); -- fail diff --git a/src/test/regress/sql/rangetypes.sql b/src/test/regress/sql/rangetypes.sql index a5ecdf5372f5d..5c4b0337b7a8a 100644 --- a/src/test/regress/sql/rangetypes.sql +++ b/src/test/regress/sql/rangetypes.sql @@ -107,6 +107,16 @@ select numrange(1.1, 2.2,'[]') - numrange(2.0, 3.0); select range_minus(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]')); select range_minus(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]')); +select range_minus_multi('empty'::numrange, numrange(2.0, 3.0)); +select range_minus_multi(numrange(1.1, 2.2), 'empty'::numrange); +select range_minus_multi(numrange(1.1, 2.2), numrange(2.0, 3.0)); +select range_minus_multi(numrange(1.1, 2.2), numrange(2.2, 3.0)); +select range_minus_multi(numrange(1.1, 2.2,'[]'), numrange(2.0, 3.0)); +select range_minus_multi(numrange(1.0, 3.0), numrange(1.5, 2.0)); +select range_minus_multi(numrange(10.1,12.2,'[]'), numrange(110.0,120.2,'(]')); +select range_minus_multi(numrange(10.1,12.2,'[]'), numrange(0.0,120.2,'(]')); +select range_minus_multi(numrange(1.0,3.0,'[]'), numrange(1.5,2.0,'(]')); + select numrange(4.5, 5.5, '[]') && numrange(5.5, 6.5); select numrange(1.0, 2.0) << numrange(3.0, 4.0); select numrange(1.0, 3.0,'[]') << numrange(3.0, 4.0,'[]'); diff --git a/src/test/regress/sql/regex.sql b/src/test/regress/sql/regex.sql index 56217104ce639..a4f99c1f25f9d 100644 --- a/src/test/regress/sql/regex.sql +++ b/src/test/regress/sql/regex.sql @@ -2,9 +2,6 @@ -- Regular expression tests -- --- Don't want to have to double backslashes in regexes -set standard_conforming_strings = on; - -- Test simple quantified backrefs select 'bbbbb' ~ '^([bc])\1*$' as t; select 'ccc' ~ '^([bc])\1*$' as t; diff --git a/src/test/regress/sql/regproc.sql b/src/test/regress/sql/regproc.sql index 232289ac39823..cfec8f8c754a2 100644 --- a/src/test/regress/sql/regproc.sql +++ b/src/test/regress/sql/regproc.sql @@ -47,11 +47,42 @@ SELECT regrole('regress_regrole_test'); SELECT regrole('"regress_regrole_test"'); SELECT regnamespace('pg_catalog'); SELECT regnamespace('"pg_catalog"'); +SELECT regdatabase('template1'); +SELECT regdatabase('"template1"'); SELECT to_regrole('regress_regrole_test'); SELECT to_regrole('"regress_regrole_test"'); SELECT to_regnamespace('pg_catalog'); SELECT to_regnamespace('"pg_catalog"'); +SELECT to_regdatabase('template1'); +SELECT to_regdatabase('"template1"'); + +-- special "single dash" case + +SELECT regproc('-')::oid; +SELECT regprocedure('-')::oid; +SELECT regclass('-')::oid; +SELECT regcollation('-')::oid; +SELECT regtype('-')::oid; +SELECT regconfig('-')::oid; +SELECT regdictionary('-')::oid; +SELECT regrole('-')::oid; +SELECT regnamespace('-')::oid; +SELECT regdatabase('-')::oid; + +SELECT to_regproc('-')::oid; +SELECT to_regprocedure('-')::oid; +SELECT to_regclass('-')::oid; +SELECT to_regcollation('-')::oid; +SELECT to_regtype('-')::oid; +SELECT to_regrole('-')::oid; +SELECT to_regnamespace('-')::oid; +SELECT to_regdatabase('-')::oid; + +-- constant cannot be used here + +CREATE TABLE regrole_test (rolid OID DEFAULT 'regress_regrole_test'::regrole); +CREATE TABLE regdatabase_test (datid OID DEFAULT 'template1'::regdatabase); /* If objects don't exist, raise errors. */ @@ -88,6 +119,9 @@ SELECT regrole('foo.bar'); SELECT regnamespace('Nonexistent'); SELECT regnamespace('"Nonexistent"'); SELECT regnamespace('foo.bar'); +SELECT regdatabase('Nonexistent'); +SELECT regdatabase('"Nonexistent"'); +SELECT regdatabase('foo.bar'); /* If objects don't exist, return NULL with no error. */ @@ -122,6 +156,9 @@ SELECT to_regrole('foo.bar'); SELECT to_regnamespace('Nonexistent'); SELECT to_regnamespace('"Nonexistent"'); SELECT to_regnamespace('foo.bar'); +SELECT to_regdatabase('Nonexistent'); +SELECT to_regdatabase('"Nonexistent"'); +SELECT to_regdatabase('foo.bar'); -- Test to_regtypemod SELECT to_regtypemod('text'); @@ -147,6 +184,7 @@ SELECT * FROM pg_input_error_info('ng_catalog.abs(numeric)', 'regprocedure'); SELECT * FROM pg_input_error_info('ng_catalog.abs(numeric', 'regprocedure'); SELECT * FROM pg_input_error_info('regress_regrole_test', 'regrole'); SELECT * FROM pg_input_error_info('no_such_type', 'regtype'); +SELECT * FROM pg_input_error_info('Nonexistent', 'regdatabase'); -- Some cases that should be soft errors, but are not yet SELECT * FROM pg_input_error_info('incorrect type name syntax', 'regtype'); diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql index 24fbe0b478d96..680c8bf861485 100644 --- a/src/test/regress/sql/reloptions.sql +++ b/src/test/regress/sql/reloptions.sql @@ -59,6 +59,17 @@ UPDATE pg_class ALTER TABLE reloptions_test RESET (illegal_option); SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; +-- Tests for ternary options + +-- behave as boolean option: accept unassigned name and truncated value +DROP TABLE reloptions_test; +CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate); +SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + +DROP TABLE reloptions_test; +CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS); +SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + -- Test vacuum_truncate option DROP TABLE reloptions_test; diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql index cc99cb53f63c4..b3c8c5df550c8 100644 --- a/src/test/regress/sql/returning.sql +++ b/src/test/regress/sql/returning.sql @@ -408,3 +408,23 @@ END; \sf foo_update DROP FUNCTION foo_update; + +-- Test that the planner does not fold OLD/NEW IS NULL tests to constants +-- based on NOT NULL constraints, since OLD is NULL for INSERT and NEW is +-- NULL for DELETE. +CREATE TEMP TABLE ret_nn (a int NOT NULL); + +-- INSERT has no OLD row, should return true +INSERT INTO ret_nn VALUES (1) RETURNING old.a IS NULL; + +-- DELETE has no NEW row, should return true +DELETE FROM ret_nn WHERE a = 1 RETURNING new.a IS NULL; + +-- MERGE: DELETE should have new.a IS NULL, INSERT should have old.a IS NULL +INSERT INTO ret_nn VALUES (2); +MERGE INTO ret_nn USING (VALUES (2), (3)) AS src(a) ON ret_nn.a = src.a + WHEN MATCHED THEN DELETE + WHEN NOT MATCHED THEN INSERT VALUES (src.a) + RETURNING merge_action(), old.a IS NULL, new.a IS NULL; + +DROP TABLE ret_nn; diff --git a/src/test/regress/sql/role_ddl.sql b/src/test/regress/sql/role_ddl.sql new file mode 100644 index 0000000000000..3d0142242ec4c --- /dev/null +++ b/src/test/regress/sql/role_ddl.sql @@ -0,0 +1,96 @@ +-- Consistent test results +SET timezone TO 'UTC'; +SET DateStyle TO 'ISO, YMD'; + +-- Create test database +CREATE DATABASE regression_role_ddl_test; + +-- Basic role +CREATE ROLE regress_role_ddl_test1; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1'); + +-- Role with LOGIN +CREATE ROLE regress_role_ddl_test2 LOGIN; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2'); + +-- Role with multiple privileges +CREATE ROLE regress_role_ddl_test3 + LOGIN + SUPERUSER + CREATEDB + CREATEROLE + CONNECTION LIMIT 5 + VALID UNTIL '2030-12-31 23:59:59+00'; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test3'); + +-- Role with configuration parameters +CREATE ROLE regress_role_ddl_test4; +ALTER ROLE regress_role_ddl_test4 SET work_mem TO '256MB'; +ALTER ROLE regress_role_ddl_test4 SET search_path TO myschema, public; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test4'); + +-- Role with database-specific configuration +CREATE ROLE regress_role_ddl_test5; +ALTER ROLE regress_role_ddl_test5 IN DATABASE regression_role_ddl_test SET work_mem TO '128MB'; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test5'); + +-- Role with special characters (requires quoting) +CREATE ROLE "regress_role-with-dash"; +SELECT * FROM pg_get_role_ddl('regress_role-with-dash'); + +-- Pretty-printed output +\pset format unaligned +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test3', 'pretty', 'true'); +\pset format aligned + +-- Role with memberships +CREATE ROLE regress_role_ddl_grantor CREATEROLE; +CREATE ROLE regress_role_ddl_group1; +CREATE ROLE regress_role_ddl_group2; +CREATE ROLE regress_role_ddl_member; +GRANT regress_role_ddl_group1 TO regress_role_ddl_grantor WITH ADMIN TRUE; +GRANT regress_role_ddl_group2 TO regress_role_ddl_grantor WITH ADMIN TRUE; +SET ROLE regress_role_ddl_grantor; +GRANT regress_role_ddl_group1 TO regress_role_ddl_member WITH INHERIT TRUE, SET FALSE; +GRANT regress_role_ddl_group2 TO regress_role_ddl_member WITH ADMIN TRUE; +RESET ROLE; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_member'); + +-- Role with memberships suppressed +SELECT * FROM pg_get_role_ddl('regress_role_ddl_member', 'memberships', 'false'); + +-- Non-existent role (should error) +SELECT * FROM pg_get_role_ddl(9999999::oid); + +-- NULL input (should return no rows) +SELECT * FROM pg_get_role_ddl(NULL); + +-- Permission check: revoke SELECT on pg_authid +CREATE ROLE regress_role_ddl_noaccess; +REVOKE SELECT ON pg_authid FROM PUBLIC; +SET ROLE regress_role_ddl_noaccess; +SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1'); -- should fail +RESET ROLE; +GRANT SELECT ON pg_authid TO PUBLIC; +DROP ROLE regress_role_ddl_noaccess; + +-- Cleanup +DROP ROLE regress_role_ddl_test1; +DROP ROLE regress_role_ddl_test2; +DROP ROLE regress_role_ddl_test3; +DROP ROLE regress_role_ddl_test4; +DROP ROLE regress_role_ddl_test5; +DROP ROLE "regress_role-with-dash"; +SET ROLE regress_role_ddl_grantor; +REVOKE regress_role_ddl_group1 FROM regress_role_ddl_member; +REVOKE regress_role_ddl_group2 FROM regress_role_ddl_member; +RESET ROLE; +DROP ROLE regress_role_ddl_member; +DROP ROLE regress_role_ddl_group1; +DROP ROLE regress_role_ddl_group2; +DROP ROLE regress_role_ddl_grantor; + +DROP DATABASE regression_role_ddl_test; + +-- Reset timezone to default +RESET timezone; diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql index 9da967a9ef2f5..6b3566271df54 100644 --- a/src/test/regress/sql/rowsecurity.sql +++ b/src/test/regress/sql/rowsecurity.sql @@ -41,6 +41,161 @@ CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; GRANT EXECUTE ON FUNCTION f_leak(text) TO public; +-- +-- Test policies applied by command type +-- +SET SESSION AUTHORIZATION regress_rls_alice; + +-- setup source table (for MERGE operations) +CREATE TABLE rls_test_src (a int PRIMARY KEY, b text); +ALTER TABLE rls_test_src ENABLE ROW LEVEL SECURITY; +GRANT SELECT, UPDATE ON rls_test_src TO public; +INSERT INTO rls_test_src VALUES (1, 'src a'); + +-- setup target table with a column set by a BEFORE ROW trigger +-- (policies should always see values set by the trigger) +CREATE TABLE rls_test_tgt (a int PRIMARY KEY, b text, c text); +ALTER TABLE rls_test_tgt ENABLE ROW LEVEL SECURITY; +GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON rls_test_tgt TO public; + +CREATE FUNCTION rls_test_tgt_set_c() RETURNS trigger AS + $$ BEGIN new.c = upper(new.b); RETURN new; END; $$ + LANGUAGE plpgsql; +CREATE TRIGGER rls_test_tgt_set_c BEFORE INSERT OR UPDATE ON rls_test_tgt + FOR EACH ROW EXECUTE FUNCTION rls_test_tgt_set_c(); + +-- setup a complete set of policies that emit NOTICE messages when applied +CREATE FUNCTION rls_test_policy_fn(text, record) RETURNS bool AS + $$ BEGIN RAISE NOTICE '%.%', $1, $2; RETURN true; END; $$ + LANGUAGE plpgsql; + +CREATE POLICY sel_pol ON rls_test_src FOR SELECT + USING (rls_test_policy_fn('SELECT USING on rls_test_src', rls_test_src)); +CREATE POLICY upd_pol ON rls_test_src FOR UPDATE + USING (rls_test_policy_fn('UPDATE USING on rls_test_src', rls_test_src)) + WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_src', rls_test_src)); + +CREATE POLICY sel_pol ON rls_test_tgt FOR SELECT + USING (rls_test_policy_fn('SELECT USING on rls_test_tgt', rls_test_tgt)); +CREATE POLICY ins_pol ON rls_test_tgt FOR INSERT + WITH CHECK (rls_test_policy_fn('INSERT CHECK on rls_test_tgt', rls_test_tgt)); +CREATE POLICY upd_pol ON rls_test_tgt FOR UPDATE + USING (rls_test_policy_fn('UPDATE USING on rls_test_tgt', rls_test_tgt)) + WITH CHECK (rls_test_policy_fn('UPDATE CHECK on rls_test_tgt', rls_test_tgt)); +CREATE POLICY del_pol ON rls_test_tgt FOR DELETE + USING (rls_test_policy_fn('DELETE USING on rls_test_tgt', rls_test_tgt)); + +-- test policies applied to regress_rls_bob +SET SESSION AUTHORIZATION regress_rls_bob; + +-- SELECT, COPY ... TO, and TABLE should only apply SELECT USING policy clause +SELECT * FROM rls_test_src; +COPY rls_test_src TO stdout; +TABLE rls_test_src; + +-- SELECT ... FOR UPDATE/SHARE should also apply UPDATE USING policy clause +SELECT * FROM rls_test_src FOR UPDATE; +SELECT * FROM rls_test_src FOR NO KEY UPDATE; +SELECT * FROM rls_test_src FOR SHARE; +SELECT * FROM rls_test_src FOR KEY SHARE; + +-- plain INSERT should apply INSERT CHECK policy clause +INSERT INTO rls_test_tgt VALUES (1, 'tgt a'); + +-- INSERT ... RETURNING should also apply SELECT USING policy clause +TRUNCATE rls_test_tgt; +INSERT INTO rls_test_tgt VALUES (1, 'tgt a') RETURNING *; + +-- UPDATE without WHERE or RETURNING should only apply UPDATE policy clauses +UPDATE rls_test_tgt SET b = 'tgt b'; + +-- UPDATE with WHERE or RETURNING should also apply SELECT USING policy clause +-- (to both old and new values) +UPDATE rls_test_tgt SET b = 'tgt c' WHERE a = 1; +UPDATE rls_test_tgt SET b = 'tgt d' RETURNING *; + +-- DELETE without WHERE or RETURNING should only apply DELETE USING policy clause +BEGIN; DELETE FROM rls_test_tgt; ROLLBACK; + +-- DELETE with WHERE or RETURNING should also apply SELECT USING policy clause +BEGIN; DELETE FROM rls_test_tgt WHERE a = 1; ROLLBACK; +DELETE FROM rls_test_tgt RETURNING *; + +-- INSERT ... ON CONFLICT DO NOTHING with an arbiter clause should apply +-- INSERT CHECK and SELECT USING policy clauses (to new value, whether it +-- conflicts or not) +INSERT INTO rls_test_tgt VALUES (1, 'tgt a') ON CONFLICT (a) DO NOTHING; +INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT (a) DO NOTHING; + +-- INSERT ... ON CONFLICT DO NOTHING without an arbiter clause only applies +-- INSERT CHECK policy clause +INSERT INTO rls_test_tgt VALUES (1, 'tgt b') ON CONFLICT DO NOTHING; + +-- INSERT ... ON CONFLICT DO UPDATE should apply INSERT CHECK and SELECT USING +-- policy clauses to values proposed for insert. In the event of a conflict it +-- should also apply UPDATE and SELECT policies to old and new values, like +-- UPDATE ... WHERE. +BEGIN; +INSERT INTO rls_test_tgt VALUES (2, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b'; +INSERT INTO rls_test_tgt VALUES (2, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d'; +INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = 'tgt b' RETURNING *; +INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *; +ROLLBACK; + +-- INSERT ... ON CONFLICT DO SELECT should apply INSERT CHECK and SELECT USING +-- policy clauses to values proposed for insert. In the event of a conflict it +-- should also apply SELECT USING policy clauses to the existing values. +BEGIN; +INSERT INTO rls_test_tgt VALUES (4, 'tgt a') ON CONFLICT (a) DO SELECT RETURNING *; +INSERT INTO rls_test_tgt VALUES (4, 'tgt b') ON CONFLICT (a) DO SELECT RETURNING *; +ROLLBACK; + +-- INSERT ... ON CONFLICT DO SELECT FOR UPDATE should also apply UPDATE USING +-- policy clauses to the existing values, in the event of a conflict. +BEGIN; +INSERT INTO rls_test_tgt VALUES (5, 'tgt a') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *; +INSERT INTO rls_test_tgt VALUES (5, 'tgt b') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *; +ROLLBACK; + +-- MERGE should always apply SELECT USING policy clauses to both source and +-- target rows +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN NOT MATCHED THEN DO NOTHING; + +-- MERGE ... INSERT should behave like INSERT on target table +-- (SELECT policy applied to target, if RETURNING is specified) +TRUNCATE rls_test_tgt; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a'); +TRUNCATE rls_test_tgt; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN NOT MATCHED THEN INSERT VALUES (1, 'tgt a') + RETURNING *; + +-- MERGE ... UPDATE should behave like UPDATE ... WHERE on target table +-- (join clause is like WHERE, so SELECT policies are always applied) +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN UPDATE SET b = 'tgt b'; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN UPDATE SET b = 'tgt c' + RETURNING *; + +-- MERGE ... DELETE should behave like DELETE ... WHERE on target table +-- (join clause is like WHERE, so SELECT policies are always applied) +BEGIN; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN DELETE; +ROLLBACK; +MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a + WHEN MATCHED THEN DELETE + RETURNING *; + +-- Tidy up +RESET SESSION AUTHORIZATION; +DROP TABLE rls_test_src, rls_test_tgt; +DROP FUNCTION rls_test_tgt_set_c; +DROP FUNCTION rls_test_policy_fn; + -- BASIC Row-Level Security Scenario SET SESSION AUTHORIZATION regress_rls_alice; @@ -362,16 +517,19 @@ SELECT * FROM pg_policies WHERE schemaname = 'regress_rls_schema' AND tablename SET SESSION AUTHORIZATION regress_rls_bob; SET row_security TO ON; SELECT * FROM part_document WHERE f_leak(dtitle) ORDER BY did; +COPY part_document TO stdout WITH (DELIMITER ','); EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); -- viewpoint from regress_rls_carol SET SESSION AUTHORIZATION regress_rls_carol; SELECT * FROM part_document WHERE f_leak(dtitle) ORDER BY did; +COPY part_document TO stdout WITH (DELIMITER ','); EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); -- viewpoint from regress_rls_dave SET SESSION AUTHORIZATION regress_rls_dave; SELECT * FROM part_document WHERE f_leak(dtitle) ORDER BY did; +COPY part_document TO stdout WITH (DELIMITER ','); EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle); -- pp1 ERROR @@ -810,11 +968,51 @@ INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel') INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel') ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol'; +-- +-- INSERT ... ON CONFLICT DO SELECT and Row-level security +-- +SET SESSION AUTHORIZATION regress_rls_alice; +DROP POLICY p3_with_all ON document; + +CREATE POLICY p1_select_novels ON document FOR SELECT + USING (cid = (SELECT cid from category WHERE cname = 'novel')); +CREATE POLICY p2_insert_own ON document FOR INSERT + WITH CHECK (dauthor = current_user); +CREATE POLICY p3_update_novels ON document FOR UPDATE + USING (cid = (SELECT cid from category WHERE cname = 'novel') AND dlevel = 1) + WITH CHECK (dauthor = current_user); + +SET SESSION AUTHORIZATION regress_rls_bob; + +-- DO SELECT requires SELECT rights, should succeed for novel +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle; + +-- DO SELECT requires SELECT rights, should fail for non-novel +INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi') + ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle; + +-- DO SELECT with WHERE and EXCLUDED reference +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle; + +-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel and dlevel = 1 +INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle; + +-- should fail UPDATE USING policy for novel with dlevel = 2 +INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel') + ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle; + +SET SESSION AUTHORIZATION regress_rls_alice; +DROP POLICY p1_select_novels ON document; +DROP POLICY p2_insert_own ON document; +DROP POLICY p3_update_novels ON document; + -- -- MERGE -- RESET SESSION AUTHORIZATION; -DROP POLICY p3_with_all ON document; ALTER TABLE document ADD COLUMN dnotes text DEFAULT ''; -- all documents are readable @@ -2189,7 +2387,7 @@ DROP VIEW rls_view; DROP TABLE rls_tbl; DROP TABLE ref_tbl; --- Leaky operator test +-- Leaky operator tests CREATE TABLE rls_tbl (a int); INSERT INTO rls_tbl SELECT x/10 FROM generate_series(1, 100) x; ANALYZE rls_tbl; @@ -2205,9 +2403,58 @@ CREATE OPERATOR <<< (procedure = op_leak, leftarg = int, rightarg = int, restrict = scalarltsel); SELECT * FROM rls_tbl WHERE a <<< 1000; EXPLAIN (COSTS OFF) SELECT * FROM rls_tbl WHERE a <<< 1000 or a <<< 900; +RESET SESSION AUTHORIZATION; + +CREATE TABLE rls_child_tbl () INHERITS (rls_tbl); +INSERT INTO rls_child_tbl SELECT x/10 FROM generate_series(1, 100) x; +ANALYZE rls_child_tbl; + +CREATE TABLE rls_ptbl (a int) PARTITION BY RANGE (a); +CREATE TABLE rls_part PARTITION OF rls_ptbl FOR VALUES FROM (-100) TO (100); +INSERT INTO rls_ptbl SELECT x/10 FROM generate_series(1, 100) x; +ANALYZE rls_ptbl, rls_part; + +ALTER TABLE rls_ptbl ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls_part ENABLE ROW LEVEL SECURITY; +GRANT SELECT ON rls_ptbl TO regress_rls_alice; +GRANT SELECT ON rls_part TO regress_rls_alice; +CREATE POLICY p1 ON rls_tbl USING (a < 0); +CREATE POLICY p2 ON rls_ptbl USING (a < 0); +CREATE POLICY p3 ON rls_part USING (a < 0); + +SET SESSION AUTHORIZATION regress_rls_alice; +SELECT * FROM rls_tbl WHERE a <<< 1000; +SELECT * FROM rls_child_tbl WHERE a <<< 1000; +SELECT * FROM rls_ptbl WHERE a <<< 1000; +SELECT * FROM rls_part WHERE a <<< 1000; +SELECT * FROM (SELECT * FROM rls_tbl UNION ALL + SELECT * FROM rls_tbl) t WHERE a <<< 1000; +SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL + SELECT * FROM rls_child_tbl) t WHERE a <<< 1000; +RESET SESSION AUTHORIZATION; + +REVOKE SELECT ON rls_tbl FROM regress_rls_alice; +CREATE VIEW rls_tbl_view AS SELECT * FROM rls_tbl; + +ALTER TABLE rls_child_tbl ENABLE ROW LEVEL SECURITY; +GRANT SELECT ON rls_child_tbl TO regress_rls_alice; +CREATE POLICY p4 ON rls_child_tbl USING (a < 0); + +SET SESSION AUTHORIZATION regress_rls_alice; +SELECT * FROM rls_tbl WHERE a <<< 1000; +SELECT * FROM rls_tbl_view WHERE a <<< 1000; +SELECT * FROM rls_child_tbl WHERE a <<< 1000; +SELECT * FROM (SELECT * FROM rls_tbl UNION ALL + SELECT * FROM rls_tbl) t WHERE a <<< 1000; +SELECT * FROM (SELECT * FROM rls_child_tbl UNION ALL + SELECT * FROM rls_child_tbl) t WHERE a <<< 1000; DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); RESET SESSION AUTHORIZATION; +DROP TABLE rls_part; +DROP TABLE rls_ptbl; +DROP TABLE rls_child_tbl; +DROP VIEW rls_tbl_view; DROP TABLE rls_tbl; -- Bug #16006: whole-row Vars in a policy don't play nice with sub-selects diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql index fdd3ff1d161c1..40f5c16e54035 100644 --- a/src/test/regress/sql/rules.sql +++ b/src/test/regress/sql/rules.sql @@ -1205,6 +1205,32 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name; DROP RULE hat_upsert ON hats; +-- DO SELECT with a WHERE clause +CREATE RULE hat_confsel AS ON INSERT TO hats + DO INSTEAD + INSERT INTO hat_data VALUES ( + NEW.hat_name, + NEW.hat_color) + ON CONFLICT (hat_name) + DO SELECT FOR UPDATE + WHERE excluded.hat_color <> 'forbidden' AND hat_data.* != excluded.* + RETURNING *; +SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename; + +-- fails without RETURNING +INSERT INTO hats VALUES ('h7', 'blue'); + +-- works (returns conflicts) +EXPLAIN (costs off) +INSERT INTO hats VALUES ('h7', 'blue') RETURNING *; +INSERT INTO hats VALUES ('h7', 'blue') RETURNING *; + +-- conflicts excluded by WHERE clause +INSERT INTO hats VALUES ('h7', 'forbidden') RETURNING *; +INSERT INTO hats VALUES ('h7', 'black') RETURNING *; + +DROP RULE hat_confsel ON hats; + drop table hats; drop table hat_data; @@ -1217,6 +1243,7 @@ CREATE FUNCTION func_with_set_params() RETURNS integer SET extra_float_digits TO 2 SET work_mem TO '4MB' SET datestyle to iso, mdy + SET temp_tablespaces to NULL SET local_preload_libraries TO "Mixed/Case", 'c:/''a"/path', '', '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789' IMMUTABLE STRICT; SELECT pg_get_functiondef('func_with_set_params()'::regprocedure); diff --git a/src/test/regress/sql/select.sql b/src/test/regress/sql/select.sql index 1d1bf2b9310aa..2dfe88d2054b0 100644 --- a/src/test/regress/sql/select.sql +++ b/src/test/regress/sql/select.sql @@ -221,7 +221,6 @@ SET enable_indexscan TO off; explain (costs off) select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; select unique2 from onek2 where unique2 = 11 and stringu1 < 'B'; -RESET enable_indexscan; -- check multi-index cases too explain (costs off) select unique1, unique2 from onek2 @@ -233,6 +232,16 @@ select unique1, unique2 from onek2 where (unique2 = 11 and stringu1 < 'B') or unique1 = 0; select unique1, unique2 from onek2 where (unique2 = 11 and stringu1 < 'B') or unique1 = 0; +RESET enable_indexscan; + +-- onek2_u2_prtl should be preferred over this index, but we have to +-- discount the metapage to arrive at that answer +begin; +create index onek2_index_full on onek2 (stringu1, unique2); +explain (costs off) +select unique2 from onek2 + where stringu1 < 'B'::name; +rollback; -- -- Test some corner cases that have been known to confuse the planner diff --git a/src/test/regress/sql/sequence.sql b/src/test/regress/sql/sequence.sql index 2c220b60749ea..b334453792924 100644 --- a/src/test/regress/sql/sequence.sql +++ b/src/test/regress/sql/sequence.sql @@ -414,6 +414,6 @@ SELECT nextval('test_seq1'); SELECT nextval('test_seq1'); -- pg_get_sequence_data -SELECT * FROM pg_get_sequence_data('test_seq1'); +SELECT last_value, is_called, page_lsn <= pg_current_wal_lsn() as lsn FROM pg_get_sequence_data('test_seq1'); DROP SEQUENCE test_seq1; diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 343d344d2707f..5b2c46615566f 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -386,6 +386,95 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING DROP VIEW json_array_subquery_view; +-- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT +create type comp1 as (a int, b date); +create domain d_comp1 as comp1; +create domain mydomain as timestamptz; +create type mydomainrange as range(subtype=mydomain); +create type comp3 as (a int, b mydomainrange); +create table test_mutability( + a text[], b timestamp, c timestamptz, + d date, f1 comp1[], f2 timestamp[], + f3 d_comp1[], + f4 mydomainrange[], + f5 comp3, + f6 mydomainmultirange); + +-- JSON_OBJECTAGG, JSON_ARRAYAGG are aggregate functions, cannot be used in index +create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning jsonb)); +create index xx on test_mutability(json_objectagg(a: b absent on null with unique keys returning json)); +create index xx on test_mutability(json_arrayagg(a returning jsonb)); +create index xx on test_mutability(json_arrayagg(a returning json)); + +-- jsonb: create expression index via json_array +create index on test_mutability(json_array(a returning jsonb)); -- ok +create index on test_mutability(json_array(b returning jsonb)); -- error +create index on test_mutability(json_array(c returning jsonb)); -- error +create index on test_mutability(json_array(d returning jsonb)); -- error +create index on test_mutability(json_array(f1 returning jsonb)); -- error +create index on test_mutability(json_array(f2 returning jsonb)); -- error +create index on test_mutability(json_array(f3 returning jsonb)); -- error +create index on test_mutability(json_array(f4 returning jsonb)); -- error +create index on test_mutability(json_array(f5 returning jsonb)); -- error +create index on test_mutability(json_array(f6 returning jsonb)); -- error + +-- jsonb: create expression index via json_object +create index on test_mutability(json_object('hello' value a returning jsonb)); -- ok +create index on test_mutability(json_object('hello' value b returning jsonb)); -- error +create index on test_mutability(json_object('hello' value c returning jsonb)); -- error +create index on test_mutability(json_object('hello' value d returning jsonb)); -- error +create index on test_mutability(json_object('hello' value f1 returning jsonb)); -- error +create index on test_mutability(json_object('hello' value f2 returning jsonb)); -- error +create index on test_mutability(json_object('hello' value f3 returning jsonb)); -- error +create index on test_mutability(json_object('hello' value f4 returning jsonb)); -- error +create index on test_mutability(json_object('hello' value f5 returning jsonb)); -- error +create index on test_mutability(json_object('hello' value f6 returning jsonb)); -- error + +-- data type json doesn't have a default operator class for access method "btree" so +-- we use a generated column to test whether the JSON_ARRAY expression is +-- immutable +alter table test_mutability add column f10 json generated always as (json_array(a returning json)); -- ok +alter table test_mutability add column f11 json generated always as (json_array(b returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(c returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(d returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(f1 returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(f2 returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(f3 returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(f4 returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(f5 returning json)); -- error +alter table test_mutability add column f11 json generated always as (json_array(f6 returning json)); -- error + +-- data type json doesn't have a default operator class for access method "btree" so +-- we use a generated column to test whether the JSON_OBJECT expression is +-- immutable +alter table test_mutability add column f11 json generated always as (json_object('hello' value a returning json)); -- ok +alter table test_mutability add column f12 json generated always as (json_object('hello' value b returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value c returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value d returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value f1 returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value f2 returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value f3 returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value f4 returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value f5 returning json)); -- error +alter table test_mutability add column f12 json generated always as (json_object('hello' value f6 returning json)); -- error + +drop table test_mutability; +drop domain d_comp1; +drop type comp3; +drop type mydomainrange; +drop domain mydomain; +drop type comp1; + +-- Range/multirange with immutable subtype should be considered immutable +create type range_int as range(subtype=int); +create table test_range_immutable(r range_int, m multirange_int); +create index on test_range_immutable(json_array(r returning jsonb)); -- ok +create index on test_range_immutable(json_array(m returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value r returning jsonb)); -- ok +create index on test_range_immutable(json_object('key' value m returning jsonb)); -- ok +drop table test_range_immutable; +drop type range_int; + -- IS JSON predicate SELECT NULL IS JSON; SELECT NULL IS NOT JSON; @@ -395,6 +484,37 @@ SELECT NULL::text IS JSON; SELECT NULL::bytea IS JSON; SELECT NULL::int IS JSON; +-- IS JSON with domain types +CREATE DOMAIN jd1 AS json CHECK ((VALUE ->'a')::text <> '3'); +CREATE DOMAIN jd2 AS jsonb CHECK ((VALUE ->'a') = '1'::jsonb); +CREATE DOMAIN jd3 AS text CHECK (VALUE <> 'a'); +CREATE DOMAIN jd4 AS bytea CHECK (VALUE <> '\x61'); +CREATE DOMAIN jd5 AS date CHECK (VALUE <> NULL); + +-- NULLs through domains should return NULL (not error) +SELECT NULL::jd1 IS JSON, NULL::jd2 IS JSON, NULL::jd3 IS JSON, NULL::jd4 IS JSON; +SELECT NULL::jd1 IS NOT JSON; + +-- domain over unsupported base type should error +SELECT NULL::jd5 IS JSON; -- error +SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error + +-- domain constraint violation during cast +SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error + +-- view creation and deparsing with domain IS JSON +CREATE VIEW domain_isjson AS +WITH cte(a) AS (VALUES('{"a": 1, "a": 2}')) +SELECT a::jd1 IS JSON WITH UNIQUE KEYS as jd1, + a::jd3 IS JSON WITH UNIQUE KEYS as jd3, + a::jd4 IS JSON WITH UNIQUE KEYS as jd4 +FROM cte; +\sv domain_isjson +SELECT * FROM domain_isjson; + +DROP VIEW domain_isjson; +DROP DOMAIN jd5, jd4, jd3, jd2, jd1; + SELECT '' IS JSON; SELECT bytea '\x00' IS JSON; diff --git a/src/test/regress/sql/sqljson_queryfuncs.sql b/src/test/regress/sql/sqljson_queryfuncs.sql index 8d7b225b61217..d218b44ea479b 100644 --- a/src/test/regress/sql/sqljson_queryfuncs.sql +++ b/src/test/regress/sql/sqljson_queryfuncs.sql @@ -357,7 +357,7 @@ INSERT INTO test_jsonb_constraints VALUES ('{"a": 10}', 1); DROP TABLE test_jsonb_constraints; --- Test mutabilily of query functions +-- Test mutability of query functions CREATE TABLE test_jsonb_mutability(js jsonb, b int); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$')); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.a[0]')); @@ -402,6 +402,15 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_VALUE(js, '$' DEFAULT random()::int ON ERROR)); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.rtrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.ltrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.btrim()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.lower()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.upper()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.initcap()')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.replace("hello", "bye")')); +CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$.split_part(",", 2)')); + -- DEFAULT expression CREATE OR REPLACE FUNCTION ret_setint() RETURNS SETOF integer AS $$ @@ -450,6 +459,7 @@ SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +SELECT JSON_QUERY(jsonb '{"a": 123}', ('$' || '.' || 'a' || NULL)::date WITH WRAPPER); -- Should fail (invalid path) SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); @@ -460,6 +470,7 @@ SELECT JSON_QUERY(NULL FORMAT JSON, '$'); -- Test non-const jsonpath CREATE TEMP TABLE jsonpaths (path) AS SELECT '$'; SELECT json_value('"aaa"', path RETURNING json) FROM jsonpaths; +SELECT json_value('"aaa"', jsonpaths RETURNING json) FROM jsonpaths; -- Test PASSING argument parsing SELECT JSON_QUERY(jsonb 'null', '$xyz' PASSING 1 AS xy); diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 232ab8db8fa8b..610fd21fae447 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -312,8 +312,11 @@ SELECT pg_stat_force_next_flush(); SELECT last_seq_scan, last_idx_scan FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; COMMIT; +SELECT stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; SELECT pg_stat_reset_single_table_counters('test_last_scan'::regclass); -SELECT seq_scan, idx_scan FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; +SELECT seq_scan, idx_scan, stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; -- ensure we start out with exactly one index and sequential scan BEGIN; @@ -382,6 +385,17 @@ COMMIT; SELECT seq_scan, :'test_last_seq' = last_seq_scan AS seq_ok, idx_scan, :'test_last_idx' < last_idx_scan AS idx_ok FROM pg_stat_all_tables WHERE relid = 'test_last_scan'::regclass; +-- check the stats in pg_stat_all_indexes +SELECT idx_scan, :'test_last_idx' < last_idx_scan AS idx_ok, + stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_indexes WHERE indexrelid = 'test_last_scan_pkey'::regclass; + +-- check that the stats in pg_stat_all_indexes are reset +SELECT pg_stat_reset_single_table_counters('test_last_scan_pkey'::regclass); + +SELECT idx_scan, stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_all_indexes WHERE indexrelid = 'test_last_scan_pkey'::regclass; + ----- -- Test reset of some stats for shared table ----- @@ -402,9 +416,17 @@ COMMIT; -- check that the stats are reset. SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables WHERE relid = 'pg_shdescription'::regclass; +-- stats_reset may not be set for datid=0 and shared objects in +-- pg_stat_database, so reset once. SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables WHERE relid = 'pg_shdescription'::regclass; +SELECT stats_reset AS shared_db_reset_before + FROM pg_stat_database WHERE datid = 0 \gset +-- Second reset for comparison. +SELECT pg_stat_reset_single_table_counters('pg_shdescription'::regclass); +SELECT stats_reset > :'shared_db_reset_before'::timestamptz AS has_updated + FROM pg_stat_database WHERE datid = 0; -- set back comment \if :{?description_before} @@ -418,10 +440,10 @@ SELECT (n_tup_ins + n_tup_upd) > 0 AS has_data FROM pg_stat_all_tables ----- -- Test that sessions is incremented when a new session is started in pg_stat_database -SELECT sessions AS db_stat_sessions FROM pg_stat_database WHERE datname = (SELECT current_database()) \gset +SELECT sessions AS db_stat_sessions FROM pg_stat_database WHERE datname = current_database() \gset \c SELECT pg_stat_force_next_flush(); -SELECT sessions > :db_stat_sessions FROM pg_stat_database WHERE datname = (SELECT current_database()); +SELECT sessions > :db_stat_sessions FROM pg_stat_database WHERE datname = current_database(); -- Test pg_stat_checkpointer checkpointer-related stats, together with pg_stat_wal SELECT num_requested AS rqst_ckpts_before FROM pg_stat_checkpointer \gset @@ -439,8 +461,13 @@ DROP TABLE test_stats_temp; -- Checkpoint twice: The checkpointer reports stats after reporting completion -- of the checkpoint. But after a second checkpoint we'll see at least the -- results of the first. -CHECKPOINT; -CHECKPOINT; +-- +-- While at it, test checkpoint options. Note that we don't test MODE SPREAD +-- because it would prolong the test. +CHECKPOINT (WRONG); +CHECKPOINT (MODE WRONG); +CHECKPOINT (MODE FAST, FLUSH_UNLOGGED FALSE); +CHECKPOINT (FLUSH_UNLOGGED); SELECT num_requested > :rqst_ckpts_before FROM pg_stat_checkpointer; SELECT wal_bytes > :wal_bytes_before FROM pg_stat_wal; @@ -504,13 +531,33 @@ SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal; -- Test error case for reset_shared with unknown stats type SELECT pg_stat_reset_shared('unknown'); --- Test that reset works for pg_stat_database +-- Test that reset works for pg_stat_database and pg_stat_database_conflicts --- Since pg_stat_database stats_reset starts out as NULL, reset it once first so we have something to compare it to +-- Since pg_stat_database stats_reset starts out as NULL, reset it once first so that we +-- have a baseline for comparison. The same for pg_stat_database_conflicts as it shares +-- the same stats_reset as pg_stat_database. SELECT pg_stat_reset(); -SELECT stats_reset AS db_reset_ts FROM pg_stat_database WHERE datname = (SELECT current_database()) \gset +SELECT stats_reset AS db_reset_ts FROM pg_stat_database WHERE datname = current_database() \gset +SELECT stats_reset AS dbc_reset_ts FROM pg_stat_database_conflicts WHERE datname = current_database() \gset +SELECT :'db_reset_ts'::timestamptz = :'dbc_reset_ts'::timestamptz; SELECT pg_stat_reset(); -SELECT stats_reset > :'db_reset_ts'::timestamptz FROM pg_stat_database WHERE datname = (SELECT current_database()); +SELECT stats_reset > :'db_reset_ts'::timestamptz FROM pg_stat_database WHERE datname = current_database(); +SELECT stats_reset > :'dbc_reset_ts'::timestamptz FROM pg_stat_database_conflicts WHERE datname = current_database(); + +-- Test that reset works for pg_statio_all_sequences + +-- Use the sequence to accumulate its stats, and reset them once first +-- so that we have a baseline for comparison, similar to the previous test. +-- stats_reset to compare to. +CREATE SEQUENCE test_seq1; +SELECT nextval('test_seq1'); +SELECT pg_stat_reset_single_table_counters('test_seq1'::regclass); +SELECT stats_reset AS seq_reset_ts + FROM pg_statio_all_sequences WHERE relname ='test_seq1' \gset +SELECT pg_stat_reset_single_table_counters('test_seq1'::regclass); +SELECT stats_reset > :'seq_reset_ts'::timestamptz + FROM pg_statio_all_sequences WHERE relname ='test_seq1'; +DROP SEQUENCE test_seq1; ---- @@ -925,4 +972,40 @@ SELECT * FROM check_estimated_rows('SELECT * FROM table_fillfactor'); DROP TABLE table_fillfactor; +-- Test fastpath_exceeded stat +CREATE TABLE part_test (id int) PARTITION BY RANGE (id); + +SELECT pg_stat_reset_shared('lock'); + +-- Create partitions (exceeds number of slots) +DO $$ +DECLARE + max_locks int; +BEGIN + SELECT setting::int INTO max_locks + FROM pg_settings + WHERE name = 'max_locks_per_transaction'; + + FOR i IN 1..(max_locks + 10) LOOP + EXECUTE format( + 'CREATE TABLE part_test_%s PARTITION OF part_test + FOR VALUES FROM (%s) TO (%s)', + i, (i-1)*1000, i*1000 + ); + END LOOP; +END; +$$; + +SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset + +-- Needs a lock on each partition +SELECT count(*) FROM part_test; + +-- Ensure pending stats are flushed +SELECT pg_stat_force_next_flush(); + +SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation'; + +DROP TABLE part_test; + -- End of Stats Test diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql index da4f2fe9c938f..3cc6012b822b2 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -28,7 +28,7 @@ end; $$; -- Verify failures -CREATE TABLE ext_stats_test (x text, y int, z int); +CREATE TABLE ext_stats_test (x text, y int, z int, w xid); CREATE STATISTICS tst; CREATE STATISTICS tst ON a, b; CREATE STATISTICS tst FROM sometab; @@ -40,25 +40,32 @@ CREATE STATISTICS tst ON x, x, y, x, x, (x || 'x'), (y + 1), (x || 'x'), (x || ' CREATE STATISTICS tst ON (x || 'x'), (x || 'x'), (y + 1), (x || 'x'), (x || 'x'), (y + 1), (x || 'x'), (x || 'x'), (y + 1) FROM ext_stats_test; CREATE STATISTICS tst ON (x || 'x'), (x || 'x'), y FROM ext_stats_test; CREATE STATISTICS tst (unrecognized) ON x, y FROM ext_stats_test; +-- unsupported targets +CREATE STATISTICS tst ON a FROM (VALUES (x)) AS foo; +CREATE STATISTICS tst ON a FROM foo NATURAL JOIN bar; +CREATE STATISTICS tst ON a FROM (SELECT * FROM ext_stats_test) AS foo; +CREATE STATISTICS tst ON a FROM ext_stats_test s TABLESAMPLE system (x); +CREATE STATISTICS tst ON a FROM XMLTABLE('foo' PASSING 'bar' COLUMNS a text); +CREATE STATISTICS tst ON a FROM JSON_TABLE(jsonb '123', '$' COLUMNS (item int)); +CREATE FUNCTION tftest(int) returns table(a int, b int) as $$ +SELECT $1, $1+i FROM generate_series(1,5) g(i); +$$ LANGUAGE sql IMMUTABLE STRICT; +CREATE STATISTICS alt_stat2 ON a FROM tftest(1); +DROP FUNCTION tftest; -- incorrect expressions CREATE STATISTICS tst ON (y) FROM ext_stats_test; -- single column reference CREATE STATISTICS tst ON y + z FROM ext_stats_test; -- missing parentheses CREATE STATISTICS tst ON (x, y) FROM ext_stats_test; -- tuple expression -DROP TABLE ext_stats_test; --- statistics on virtual generated column not allowed -CREATE TABLE ext_stats_test1 (x int, y int, z int GENERATED ALWAYS AS (x+y) VIRTUAL, w xid); -CREATE STATISTICS tst on z from ext_stats_test1; -CREATE STATISTICS tst on (z) from ext_stats_test1; -CREATE STATISTICS tst on (z+1) from ext_stats_test1; -CREATE STATISTICS tst (ndistinct) ON z from ext_stats_test1; -- statistics on system column not allowed -CREATE STATISTICS tst on tableoid from ext_stats_test1; -CREATE STATISTICS tst on (tableoid) from ext_stats_test1; -CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test1; -CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test1; --- statistics without a less-than operator not supported -CREATE STATISTICS tst (ndistinct) ON w from ext_stats_test1; -DROP TABLE ext_stats_test1; +CREATE STATISTICS tst on tableoid from ext_stats_test; +CREATE STATISTICS tst on (tableoid) from ext_stats_test; +CREATE STATISTICS tst on (tableoid::int+1) from ext_stats_test; +CREATE STATISTICS tst (ndistinct) ON xmin from ext_stats_test; +-- statistics kinds are not allowed with univariate statistics +CREATE STATISTICS tst (ndistinct) ON (y + z) FROM ext_stats_test; +-- multivariate statistics without a less-than operator not supported +CREATE STATISTICS tst (ndistinct) ON x, w from ext_stats_test; +DROP TABLE ext_stats_test; -- Ensure stats are dropped sanely, and test IF NOT EXISTS while at it CREATE TABLE ab1 (a INTEGER, b INTEGER, c INTEGER); @@ -71,6 +78,14 @@ DROP STATISTICS ab1_a_b_stats; ALTER STATISTICS ab1_a_b_stats RENAME TO ab1_a_b_stats_new; RESET SESSION AUTHORIZATION; DROP ROLE regress_stats_ext; +CREATE STATISTICS pg_temp.stats_ext_temp ON a, b FROM ab1; +SELECT regexp_replace(pg_describe_object(tableoid, oid, 0), + 'pg_temp_[0-9]*', 'pg_temp_REDACTED') AS descr, + pg_statistics_obj_is_visible(oid) AS visible + FROM pg_statistic_ext + WHERE stxname = 'stats_ext_temp'; +DROP STATISTICS stats_ext_temp; -- shall fail +DROP STATISTICS pg_temp.stats_ext_temp; CREATE STATISTICS IF NOT EXISTS ab1_a_b_stats ON a, b FROM ab1; DROP STATISTICS ab1_a_b_stats; @@ -105,7 +120,10 @@ ALTER TABLE ab1 ALTER a SET STATISTICS -1; ALTER STATISTICS ab1_a_b_stats SET STATISTICS 0; \d ab1 ANALYZE ab1; -SELECT stxname, stxdndistinct, stxddependencies, stxdmcv, stxdinherit +SELECT stxname, + replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct, + replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies, + stxdmcv, stxdinherit FROM pg_statistic_ext s LEFT JOIN pg_statistic_ext_data d ON (d.stxoid = s.oid) WHERE s.stxname = 'ab1_a_b_stats'; ALTER STATISTICS ab1_a_b_stats SET STATISTICS -1; @@ -277,7 +295,7 @@ CREATE STATISTICS s10 ON a, b, c FROM ndistinct; ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; @@ -318,7 +336,7 @@ INSERT INTO ndistinct (a, b, c, filler1) ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; @@ -344,7 +362,7 @@ SELECT * FROM check_estimated_rows('SELECT COUNT(*) FROM ndistinct GROUP BY a, ( DROP STATISTICS s10; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; @@ -379,7 +397,7 @@ CREATE STATISTICS s10 (ndistinct) ON (a+1), (b+100), (2*c) FROM ndistinct; ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; @@ -403,7 +421,7 @@ CREATE STATISTICS s10 (ndistinct) ON a, b, (2*c) FROM ndistinct; ANALYZE ndistinct; -SELECT s.stxkind, d.stxdndistinct +SELECT s.stxkind, replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct FROM pg_statistic_ext s, pg_statistic_ext_data d WHERE s.stxrelid = 'ndistinct'::regclass AND d.stxoid = s.oid; @@ -688,7 +706,7 @@ CREATE STATISTICS func_deps_stat (dependencies) ON a, b, c FROM functional_depen ANALYZE functional_dependencies; -- print the detected dependencies -SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; +SELECT replace(dependencies, '}, ', E'},\n') AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE a = 1 AND b = ''1'''); @@ -824,7 +842,7 @@ CREATE STATISTICS func_deps_stat (dependencies) ON (a * 2), upper(b), (c + 1) FR ANALYZE functional_dependencies; -- print the detected dependencies -SELECT dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; +SELECT replace(dependencies, '}, ', E'},\n') AS dependencies FROM pg_stats_ext WHERE statistics_name = 'func_deps_stat'; SELECT * FROM check_estimated_rows('SELECT * FROM functional_dependencies WHERE (a * 2) = 2 AND upper(b) = ''1'''); @@ -1561,6 +1579,35 @@ SELECT c0 FROM ONLY expr_stats_incompatible_test WHERE DROP TABLE expr_stats_incompatible_test; +-- multivariate statistics on virtual generated columns +CREATE TABLE virtual_gen_stats (a int, b int, c int GENERATED ALWAYS AS (2*a), d int GENERATED ALWAYS AS (a+b), w xid GENERATED ALWAYS AS (a::text::xid)); +INSERT INTO virtual_gen_stats SELECT mod(i,10), mod(i,10) FROM generate_series(1,100) s(i); +ANALYZE virtual_gen_stats; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0'); + +CREATE STATISTICS virtual_gen_stats_1 (mcv) ON c, (3*b), d, (d-2*a) FROM virtual_gen_stats; +ANALYZE virtual_gen_stats; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0 AND (3*b) = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE d = 0 AND (d-2*a) = 0'); + +-- univariate statistics on individual virtual generated columns +DROP STATISTICS virtual_gen_stats_1; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE w = 0'); + +CREATE STATISTICS virtual_gen_stats_single ON c FROM virtual_gen_stats; +CREATE STATISTICS virtual_gen_stats_single_without_less_than ON w FROM virtual_gen_stats; +ANALYZE virtual_gen_stats; + +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE c = 0'); +SELECT * FROM check_estimated_rows('SELECT * FROM virtual_gen_stats WHERE w = 0'); + +DROP TABLE virtual_gen_stats; + -- Permission tests. Users should not be able to see specific data values in -- the extended statistics, if they lack permission to see those values in -- the underlying table. @@ -1647,8 +1694,15 @@ CREATE FUNCTION op_leak(int, int) RETURNS bool LANGUAGE plpgsql; CREATE OPERATOR <<< (procedure = op_leak, leftarg = int, rightarg = int, restrict = scalarltsel); +CREATE FUNCTION op_leak(record, record) RETURNS bool + AS 'BEGIN RAISE NOTICE ''op_leak => %, %'', $1, $2; RETURN $1 < $2; END' + LANGUAGE plpgsql; +CREATE OPERATOR <<< (procedure = op_leak, leftarg = record, rightarg = record, + restrict = scalarltsel); SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied -SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; +SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; -- Permission denied +SELECT * FROM tststats.priv_test_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied -- Grant access via a security barrier view, but hide all data @@ -1661,19 +1715,51 @@ GRANT SELECT, DELETE ON tststats.priv_test_view TO regress_stats_user1; SET SESSION AUTHORIZATION regress_stats_user1; SELECT * FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not leak SELECT * FROM tststats.priv_test_view WHERE a <<< 0 OR b <<< 0; -- Should not leak +SELECT * FROM tststats.priv_test_view t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak DELETE FROM tststats.priv_test_view WHERE a <<< 0 AND b <<< 0; -- Should not leak -- Grant table access, but hide all data with RLS RESET SESSION AUTHORIZATION; ALTER TABLE tststats.priv_test_tbl ENABLE ROW LEVEL SECURITY; +CREATE POLICY priv_test_tbl_pol ON tststats.priv_test_tbl USING (2 * a < 0); GRANT SELECT, DELETE ON tststats.priv_test_tbl TO regress_stats_user1; -- Should now have direct table access, but see nothing and leak nothing SET SESSION AUTHORIZATION regress_stats_user1; SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak -SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; +SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 OR b <<< 0; -- Should not leak +SELECT * FROM tststats.priv_test_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak +-- Create plain inheritance parent table with no access permissions +RESET SESSION AUTHORIZATION; +CREATE TABLE tststats.priv_test_parent_tbl (a int, b int); +ALTER TABLE tststats.priv_test_tbl INHERIT tststats.priv_test_parent_tbl; + +-- Should not have access to parent, and should leak nothing +SET SESSION AUTHORIZATION regress_stats_user1; +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 OR b <<< 0; -- Permission denied +SELECT * FROM tststats.priv_test_parent_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Permission denied +DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Permission denied + +-- Grant table access to parent, but hide all data with RLS +RESET SESSION AUTHORIZATION; +ALTER TABLE tststats.priv_test_parent_tbl ENABLE ROW LEVEL SECURITY; +CREATE POLICY priv_test_parent_tbl_pol ON tststats.priv_test_parent_tbl USING (2 * a < 0); +GRANT SELECT, DELETE ON tststats.priv_test_parent_tbl TO regress_stats_user1; + +-- Should now have direct table access to parent, but see nothing and leak nothing +SET SESSION AUTHORIZATION regress_stats_user1; +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak +SELECT * FROM tststats.priv_test_parent_tbl WHERE a <<< 0 OR b <<< 0; -- Should not leak +SELECT * FROM tststats.priv_test_parent_tbl t + WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak +DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak + -- privilege checks for pg_stats_ext and pg_stats_ext_exprs RESET SESSION AUTHORIZATION; CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT); @@ -1700,12 +1786,48 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*); +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT, c INT GENERATED ALWAYS AS (b * 2) STORED); +CREATE SCHEMA sts_sch2; +GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass1 ON a, b, c FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl; +CREATE STATISTICS sts_sch2.pass2 ON a, b, c FROM sts_sch1.tbl; + +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; +ALTER TABLE sts_sch1.tbl ALTER COLUMN c SET EXPRESSION AS (a * 3); + -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); +DROP OPERATOR <<< (record, record); +DROP FUNCTION op_leak(record, record); RESET SESSION AUTHORIZATION; DROP TABLE stats_ext_tbl; DROP SCHEMA tststats CASCADE; +DROP SCHEMA sts_sch1, sts_sch2 CASCADE; DROP USER regress_stats_user1; CREATE TABLE grouping_unique (x integer); @@ -1758,4 +1880,31 @@ SELECT FROM sb_1 LEFT JOIN sb_2 RESET enable_nestloop; RESET enable_mergejoin; +-- Check that we can use statistics on a bool-valued function. +SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE numeric_lt(y, 1.0)'); + +CREATE STATISTICS extstat_sb_2_small ON numeric_lt(y, 1.0) FROM sb_2; +ANALYZE sb_2; + +SELECT * FROM check_estimated_rows('SELECT * FROM sb_2 WHERE numeric_lt(y, 1.0)'); + +-- Tidy up DROP TABLE sb_1, sb_2 CASCADE; + +-- Check statistics generated for range type and expressions. +CREATE TABLE stats_ext_tbl_range(name text, irange int4range); +INSERT INTO stats_ext_tbl_range VALUES + ('red', '[1,7)'::int4range), + ('blue', '[2,8]'::int4range), + ('green', '[3,9)'::int4range); +CREATE STATISTICS stats_ext_range (mcv) + ON irange, (irange + '[4,10)'::int4range) + FROM stats_ext_tbl_range; +ANALYZE stats_ext_tbl_range; +SELECT attnames, most_common_vals + FROM pg_stats_ext + WHERE statistics_name = 'stats_ext_range'; +SELECT range_length_histogram, range_empty_frac, range_bounds_histogram + FROM pg_stats_ext_exprs + WHERE statistics_name = 'stats_ext_range'; +DROP TABLE stats_ext_tbl_range; diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index d140733a75028..0bfa3d44cef3b 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -1,5 +1,170 @@ CREATE SCHEMA stats_import; +-- +-- Convenience view for columns of pg_stats that are stable across test runs. +-- +CREATE VIEW stats_import.pg_stats_stable AS + SELECT schemaname, tablename, attname, inherited, null_frac, avg_width, + n_distinct, most_common_vals::text as most_common_vals, + most_common_freqs, histogram_bounds::text AS histogram_bounds, + correlation, most_common_elems::text AS most_common_elems, + most_common_elem_freqs, elem_count_histogram, + range_length_histogram::text AS range_length_histogram, range_empty_frac, + range_bounds_histogram::text AS range_bounds_histogram + FROM pg_stats; + +-- +-- Setup functions for set-difference convenience functions +-- + +-- Test to detect any new columns added to pg_statistic. If any columns +-- are added, we may need to update pg_statistic_flat() and the facilities +-- we are testing. +SELECT COUNT(*) FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_statistic'::regclass AND + attnum > 0; + +-- Create a view that is used purely for the type based on pg_statistic. +CREATE VIEW stats_import.pg_statistic_flat_t AS + SELECT + a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, + s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, + s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, + s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, + s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, + s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, + s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, + s.stavalues5::text AS sv5 + FROM pg_statistic s + JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum + WHERE FALSE; + +-- Function to retrieve data used for diff comparisons between two +-- relations based on the contents of pg_statistic. +CREATE FUNCTION stats_import.pg_statistic_flat(p_relname text) +RETURNS SETOF stats_import.pg_statistic_flat_t +BEGIN ATOMIC + SELECT a.attname, s.stainherit, s.stanullfrac, s.stawidth, + s.stadistinct, s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, + s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, s.stacoll1, s.stacoll2, + s.stacoll3, s.stacoll4, s.stacoll5, s.stanumbers1, s.stanumbers2, + s.stanumbers3, s.stanumbers4, s.stanumbers5, s.stavalues1::text, + s.stavalues2::text, s.stavalues3::text, + s.stavalues4::text, s.stavalues5::text + FROM pg_statistic s + JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum + JOIN pg_class c ON c.oid = a.attrelid + WHERE c.relnamespace = 'stats_import'::regnamespace + AND c.relname = p_relname; +END; + +-- Comparison function for pg_statistic. The two relations defined by +-- the function caller are compared. +CREATE FUNCTION stats_import.pg_statistic_get_difference(a text, b text) +RETURNS TABLE (relname text, stats stats_import.pg_statistic_flat_t) +BEGIN ATOMIC + WITH aset AS (SELECT * FROM stats_import.pg_statistic_flat(a)), + bset AS (SELECT * FROM stats_import.pg_statistic_flat(b)) + SELECT a AS relname, a_minus_b::stats_import.pg_statistic_flat_t + FROM (TABLE aset EXCEPT TABLE bset) AS a_minus_b + UNION ALL + SELECT b AS relname, b_minus_a::stats_import.pg_statistic_flat_t + FROM (TABLE bset EXCEPT TABLE aset) AS b_minus_a; +END; + +-- Test to detect any new columns added to pg_stats_ext. If any columns +-- are added, we may need to update pg_stats_ext_flat() and the facilities +-- we are testing. +SELECT COUNT(*) FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_stats_ext'::regclass AND + attnum > 0; + +-- Create a view that is used purely for the type based on pg_stats_ext. +CREATE VIEW stats_import.pg_stats_ext_flat_t AS + SELECT inherited, n_distinct, dependencies, most_common_vals, + most_common_freqs, most_common_base_freqs + FROM pg_stats_ext + WHERE FALSE; + +-- Function to retrieve data used for diff comparisons between two +-- relations based on the contents of pg_stats_ext. +CREATE FUNCTION stats_import.pg_stats_ext_flat(p_statname text) +RETURNS SETOF stats_import.pg_stats_ext_flat_t +BEGIN ATOMIC + SELECT inherited, n_distinct, dependencies, most_common_vals, + most_common_freqs, most_common_base_freqs + FROM pg_stats_ext + WHERE statistics_schemaname = 'stats_import' + AND statistics_name = p_statname; +END; + +-- Comparison function for pg_stats_ext. The two relations defined by +-- the function caller are compared. +CREATE FUNCTION stats_import.pg_stats_ext_get_difference(a text, b text) +RETURNS TABLE (statname text, stats stats_import.pg_stats_ext_flat_t) +BEGIN ATOMIC + WITH aset AS (SELECT * FROM stats_import.pg_stats_ext_flat(a)), + bset AS (SELECT * FROM stats_import.pg_stats_ext_flat(b)) + SELECT a AS relname, a_minus_b::stats_import.pg_stats_ext_flat_t + FROM (TABLE aset EXCEPT TABLE bset) AS a_minus_b + UNION ALL + SELECT b AS relname, b_minus_a::stats_import.pg_stats_ext_flat_t + FROM (TABLE bset EXCEPT TABLE aset) AS b_minus_a; +END; + +-- Test to detect any new columns added to pg_stats_ext_exprs. If any columns +-- are added, we may need to update pg_stats_ext_exprs_flat() and the facilities +-- we are testing. +SELECT COUNT(*) FROM pg_attribute + WHERE attrelid = 'pg_catalog.pg_stats_ext_exprs'::regclass AND + attnum > 0; + +-- Create a view that is used purely for the type based on pg_stats_ext_exprs. +CREATE VIEW stats_import.pg_stats_ext_exprs_flat_t AS + SELECT inherited, null_frac, avg_width, n_distinct, + most_common_vals::text AS most_common_vals, + most_common_freqs, histogram_bounds::text AS histogram_bounds, + correlation, most_common_elems::text AS most_common_elems, + most_common_elem_freqs, elem_count_histogram, + range_length_histogram::text AS range_length_histogram, + range_empty_frac, range_bounds_histogram::text AS range_bounds_histogram + FROM pg_stats_ext_exprs AS n + WHERE FALSE; + +-- Function to retrieve data used for diff comparisons between two +-- relations based on the contents of pg_stats_ext_exprs. +CREATE FUNCTION stats_import.pg_stats_ext_exprs_flat(p_statname text) +RETURNS SETOF stats_import.pg_stats_ext_exprs_flat_t +BEGIN ATOMIC + SELECT inherited, null_frac, avg_width, n_distinct, + most_common_vals::text AS most_common_vals, + most_common_freqs, histogram_bounds::text AS histogram_bounds, + correlation, most_common_elems::text AS most_common_elems, + most_common_elem_freqs, elem_count_histogram, + range_length_histogram::text AS range_length_histogram, + range_empty_frac, range_bounds_histogram::text AS range_bounds_histogram + FROM pg_stats_ext_exprs AS n + WHERE n.statistics_schemaname = 'stats_import' AND + n.statistics_name = p_statname; +END; + +-- Comparison function for pg_stats_ext_exprs. The two relations defined by +-- the function caller are compared. +CREATE FUNCTION stats_import.pg_stats_ext_exprs_get_difference(a text, b text) +RETURNS TABLE (statname text, stats stats_import.pg_stats_ext_exprs_flat_t) +BEGIN ATOMIC + WITH aset AS (SELECT * FROM stats_import.pg_stats_ext_exprs_flat(a)), + bset AS (SELECT * FROM stats_import.pg_stats_ext_exprs_flat(b)) + SELECT a AS relname, a_minus_b::stats_import.pg_stats_ext_exprs_flat_t + FROM (TABLE aset EXCEPT TABLE bset) AS a_minus_b + UNION ALL + SELECT b AS relname, b_minus_a::stats_import.pg_stats_ext_exprs_flat_t + FROM (TABLE bset EXCEPT TABLE aset) AS b_minus_a; +END; + +-- +-- Schema setup. +-- CREATE TYPE stats_import.complex_type AS ( a integer, b real, @@ -15,6 +180,12 @@ CREATE TABLE stats_import.test( tags text[] ) WITH (autovacuum_enabled = false); +CREATE TABLE stats_import.test_mr( + id INTEGER PRIMARY KEY, + name text, + mrange int4multirange +) WITH (autovacuum_enabled = false); + SELECT pg_catalog.pg_restore_relation_stats( 'schemaname', 'stats_import', @@ -22,7 +193,7 @@ SELECT 'relpages', 18::integer, 'reltuples', 21::real, 'relallvisible', 24::integer, - 'relallfrozen', 27::integer); + 'relallfrozen', 27::integer); -- CREATE INDEX on a table with autovac disabled should not overwrite -- stats @@ -108,10 +279,27 @@ CREATE TABLE stats_import.part_child_1 FOR VALUES FROM (0) TO (10) WITH (autovacuum_enabled = false); +-- This ensures the presence of extended statistics marked with +-- inherited = true. +CREATE STATISTICS stats_import.part_parent_stat + ON i, (i % 2) + FROM stats_import.part_parent; + CREATE INDEX part_parent_i ON stats_import.part_parent(i); +INSERT INTO stats_import.part_parent +SELECT g.g +FROM generate_series(0,9) AS g(g); + +SELECT COUNT(*) FROM stats_import.part_parent; +SELECT COUNT(*) FROM stats_import.part_child_1; + ANALYZE stats_import.part_parent; +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + SELECT relpages FROM pg_class WHERE oid = 'stats_import.part_parent'::regclass; @@ -346,7 +534,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'n_distinct', 0.6::real); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -365,7 +553,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'null_frac', 0.4::real); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -381,7 +569,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( 'nope', 0.5::real); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -398,7 +586,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -415,7 +603,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -433,7 +621,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -451,7 +639,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -468,7 +656,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -485,7 +673,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -501,7 +689,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -518,7 +706,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -535,7 +723,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -553,7 +741,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -570,7 +758,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -587,7 +775,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -604,7 +792,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -621,7 +809,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -637,7 +825,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -655,7 +843,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -673,7 +861,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -690,7 +878,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -707,7 +895,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -724,7 +912,7 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false @@ -741,12 +929,30 @@ SELECT pg_catalog.pg_restore_attribute_stats( ); SELECT * -FROM pg_stats +FROM stats_import.pg_stats_stable WHERE schemaname = 'stats_import' AND tablename = 'test' AND inherited = false AND attname = 'id'; +-- test for multiranges +INSERT INTO stats_import.test_mr +VALUES + (1, 'red', '{[1,3),[5,9),[20,30)}'::int4multirange), + (2, 'red', '{[11,13),[15,19),[20,30)}'::int4multirange), + (3, 'red', '{[21,23),[25,29),[120,130)}'::int4multirange); + +-- ensure that we set attribute stats for a multirange +SELECT pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'attname', 'mrange', + 'inherited', false, + 'range_length_histogram', '{19,29,109}'::text, + 'range_empty_frac', '0'::real, + 'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text +); + -- -- Test the ability to exactly copy data from one table to an identical table, -- correctly reconstructing the stakind order as well as the staopN and @@ -766,6 +972,34 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL; CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1)); +CREATE STATISTICS stats_import.test_stat + ON name, comp, lower(arange), array_length(tags,1) + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_ndistinct (ndistinct) + ON name, comp + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_dependencies (dependencies) + ON name, comp + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_mcv (mcv) + ON name, comp + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_ndistinct_exprs (ndistinct) + ON lower(name), upper(name) + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_dependencies_exprs (dependencies) + ON lower(name), upper(name) + FROM stats_import.test; + +CREATE STATISTICS stats_import.test_stat_mcv_exprs (mcv) + ON lower(name), upper(name) + FROM stats_import.test; + -- Generate statistics on table with data ANALYZE stats_import.test; @@ -774,6 +1008,10 @@ CREATE TABLE stats_import.test_clone ( LIKE stats_import.test ) CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1)); +CREATE STATISTICS stats_import.test_stat_clone + ON name, comp, lower(arange), array_length(tags,1) + FROM stats_import.test_clone; + -- -- Copy stats from test to test_clone, and is_odd to is_odd_clone -- @@ -811,113 +1049,13 @@ AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone') GROUP BY c.relname ORDER BY c.relname; --- check test minus test_clone -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test_clone'::regclass; - --- check test_clone minus test -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test_clone'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'test_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.test'::regclass; - --- check is_odd minus is_odd_clone -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd_clone'::regclass; - --- check is_odd_clone minus is_odd -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd_clone'::regclass -EXCEPT -SELECT - a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct, - s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5, - s.staop1, s.staop2, s.staop3, s.staop4, s.staop5, - s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5, - s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5, - s.stavalues1::text AS sv1, s.stavalues2::text AS sv2, - s.stavalues3::text AS sv3, s.stavalues4::text AS sv4, - s.stavalues5::text AS sv5, 'is_odd_clone' AS direction -FROM pg_statistic s -JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum -WHERE s.starelid = 'stats_import.is_odd'::regclass; +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('test', 'test_clone') +\gx + +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('is_odd', 'is_odd_clone') +\gx -- attribute stats exist before a clear, but not after SELECT COUNT(*) @@ -970,4 +1108,1249 @@ AND tablename = 'stats_temp' AND inherited = false AND attname = 'i'; DROP TABLE stats_temp; + +-- Tests for pg_clear_extended_stats(). +-- Invalid argument values. +SELECT pg_clear_extended_stats(schemaname => NULL, + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => NULL, + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => NULL, + statistics_name => 'stat_bar', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => NULL, + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'schema_foo', + relname => 'rel_foo', + statistics_schemaname => 'schema_foo', + statistics_name => 'stat_bar', + inherited => NULL); +-- Missing objects +SELECT pg_clear_extended_stats(schemaname => 'schema_not_exist', + relname => 'test', + statistics_schemaname => 'schema_not_exist', + statistics_name => 'test_stat', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'table_not_exist', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'schema_not_exist', + statistics_name => 'test_stat', + inherited => false); +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'ext_stats_not_exist', + inherited => false); +-- Incorrect relation/extended stats combination +SELECT pg_clear_extended_stats(schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat_clone', + inherited => false); + +-- Check that records are removed after a valid clear call. +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +BEGIN; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +SELECT mode FROM pg_locks WHERE locktype = 'relation' AND + relation = 'stats_import.test'::regclass AND + pid = pg_backend_pid(); +COMMIT; +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +SELECT COUNT(*), e.inherited FROM pg_stats_ext_exprs AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat' GROUP BY e.inherited; +-- And before/after on inherited stats +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'part_parent', + statistics_schemaname => 'stats_import', + statistics_name => 'part_parent_stat', + inherited => true); +SELECT COUNT(*), e.inherited FROM pg_stats_ext AS e + WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'part_parent_stat' GROUP BY e.inherited; + +-- Check that MAINTAIN is required when clearing statistics. +CREATE ROLE regress_test_extstat_clear; +GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_clear; +SET ROLE regress_test_extstat_clear; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +RESET ROLE; +GRANT MAINTAIN ON stats_import.test TO regress_test_extstat_clear; +SET ROLE regress_test_extstat_clear; +SELECT pg_catalog.pg_clear_extended_stats( + schemaname => 'stats_import', + relname => 'test', + statistics_schemaname => 'stats_import', + statistics_name => 'test_stat', + inherited => false); +RESET ROLE; +REVOKE MAINTAIN ON stats_import.test FROM regress_test_extstat_clear; +REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_clear; +DROP ROLE regress_test_extstat_clear; + +-- Tests for pg_restore_extended_stats(). +-- Invalid argument values. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', NULL, + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', NULL, + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', NULL, + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', NULL, + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', NULL); +-- Missing objects +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'schema_not_exist', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'table_not_exist', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'schema_not_exist', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'ext_stats_not_exist', + 'inherited', false); +-- Incorrect relation/extended stats combination +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); + +-- Check that MAINTAIN is required when restoring statistics. +CREATE ROLE regress_test_extstat_restore; +GRANT ALL ON SCHEMA stats_import TO regress_test_extstat_restore; +SET ROLE regress_test_extstat_restore; +-- No data to restore; this fails on a permission failure. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false); +RESET ROLE; +GRANT MAINTAIN ON stats_import.test_clone TO regress_test_extstat_restore; +SET ROLE regress_test_extstat_restore; +-- This works, check the lock on the relation while on it. +BEGIN; +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); +SELECT mode FROM pg_locks WHERE locktype = 'relation' AND + relation = 'stats_import.test_clone'::regclass AND + pid = pg_backend_pid(); +COMMIT; +RESET ROLE; +REVOKE MAINTAIN ON stats_import.test_clone FROM regress_test_extstat_restore; +REVOKE ALL ON SCHEMA stats_import FROM regress_test_extstat_restore; +DROP ROLE regress_test_extstat_restore; + +-- ndistinct value doesn't match object definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct); +-- Incorrect extended stats kind, ndistinct not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,3], "ndistinct" : 4}]'::pg_ndistinct); +-- Incorrect extended stats kind, dependencies not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 2, "degree": 1.000000}]'::pg_dependencies); + +-- ok: ndistinct +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'n_distinct', '[{"attributes" : [2,3], "ndistinct" : 4}]'::pg_ndistinct); + +-- dependencies value doesn't match definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'dependencies', '[{"attributes": [1], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 1, "degree": 1.000000}]'::pg_dependencies); + +-- ok: dependencies +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'dependencies', '[{"attributes": [2], "dependency": 3, "degree": 1.000000}, + {"attributes": [3], "dependency": 2, "degree": 1.000000}]'::pg_dependencies); + +-- ndistinct with expressions, invalid attributes. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct_exprs', + 'inherited', false, + 'n_distinct', '[{"attributes" : [1,-1], "ndistinct" : 4}]'::pg_ndistinct); + +-- ok: ndistinct with expressions. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct_exprs', + 'inherited', false, + 'n_distinct', '[{"attributes" : [-1,-2], "ndistinct" : 4}]'::pg_ndistinct); + +-- dependencies with expressions, invalid attributes. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies_exprs', + 'inherited', false, + 'dependencies', '[{"attributes": [-1], "dependency": 1, "degree": 1.000000}, + {"attributes": [1], "dependency": -1, "degree": 1.000000}]'::pg_dependencies); + +-- ok: dependencies with expressions +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies_exprs', + 'inherited', false, + 'dependencies', '[{"attributes": [-1], "dependency": -2, "degree": 1.000000}, + {"attributes": [-2], "dependency": -1, "degree": 1.000000}]'::pg_dependencies); + +-- ok: MCV with expressions +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv_exprs', + 'inherited', false, + 'most_common_vals', '{{four,FOUR},{one,NULL},{NULL,TRE},{two,TWO}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.99}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.023,0.087}'::double precision[]); + +-- Check the presence of the restored stats, for each object. +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_ndistinct' AND + e.inherited = false; + +SELECT replace(e.dependencies, '}, ', E'},\n') AS dependencies +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_dependencies' AND + e.inherited = false; + +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_ndistinct_exprs' AND + e.inherited = false; + +SELECT replace(e.dependencies, '}, ', E'},\n') AS dependencies +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_dependencies_exprs' AND + e.inherited = false; + +SELECT e.most_common_vals, e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcv_exprs' AND + e.inherited = false \gx + +-- Incorrect extended stats kind, mcv not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_dependencies', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +-- MCV requires all three parameters +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[]); + +-- most_common_vals that is not 2-D +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{four,NULL}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +-- most_common_freqs with length not matching with most_common_vals. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +-- most_common_base_freqs with length not matching most_common_vals. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625}'::double precision[]); + +-- mcv attributes not matching object definition +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL,0,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")",1,2}, + {tre,"(3,3.3,TRE,03-03-2003,)",-1,3}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")",1,2}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.00390625,0.015625,0.00390625,0.015625}'::double precision[]); + +-- ok: mcv +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcv', + 'inherited', false, + 'most_common_vals', '{{four,NULL}, + {one,"(1,1.1,ONE,01-01-2001,\"{\"\"xkey\"\": \"\"xval\"\"}\")"}, + {tre,"(3,3.3,TRE,03-03-2003,)"}, + {two,"(2,2.2,TWO,02-02-2002,\"[true, 4, \"\"six\"\"]\")"}}'::text[], + 'most_common_freqs', '{0.25,0.25,0.25,0.25}'::double precision[], + 'most_common_base_freqs', '{0.0625,0.0625,0.0625,0.0625}'::double precision[]); + +SELECT replace(e.most_common_vals::text, '},', E'},\n ') AS mcvs, + e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcv' AND + e.inherited = false +\gx + +-- Check import of all kinds for multirange. +CREATE STATISTICS stats_import.test_mr_stat + ON name, mrange, ( mrange + '{[10000,10200)}'::int4multirange) + FROM stats_import.test_mr; + +CREATE TABLE stats_import.test_mr_clone ( LIKE stats_import.test_mr ) + WITH (autovacuum_enabled = false); +CREATE STATISTICS stats_import.test_mr_stat_clone + ON name, mrange, ( mrange + '{[10000,10200)}'::int4multirange) + FROM stats_import.test_mr_clone; + +-- Check for invalid value combinations for range types. +-- Only range_bounds_histogram (other two missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\",\"[11,10200)\"}"}]'::jsonb); +-- Only range_length_histogram and range_empty_frac +-- (range_bounds_histogram missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{10179,10189}", "range_empty_frac": "0"}]'::jsonb); +-- Only range_bounds_histogram and range_empty_frac +-- (range_length_histogram missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\"}", "range_empty_frac": "0"}]'::jsonb); +-- Only range_bounds_histogram and range_length_histogram +-- (range_empty_frac missing) +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'exprs', '[{"range_bounds_histogram": "{\"[1,10200)\"}", "range_length_histogram": "{10179}"}]'::jsonb); + +-- ok: multirange stats +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_mr', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_mr_stat', + 'inherited', false, + 'n_distinct', '[{"attributes": [2, 3], "ndistinct": 3}, + {"attributes": [2, -1], "ndistinct": 3}, + {"attributes": [3, -1], "ndistinct": 3}, + {"attributes": [2, 3, -1], "ndistinct": 3}]'::pg_catalog.pg_ndistinct, + 'dependencies', '[{"attributes": [3], "dependency": 2, "degree": 1.000000}, + {"attributes": [3], "dependency": -1, "degree": 1.000000}, + {"attributes": [-1], "dependency": 2, "degree": 1.000000}, + {"attributes": [-1], "dependency": 3, "degree": 1.000000}, + {"attributes": [2, 3], "dependency": -1, "degree": 1.000000}, + {"attributes": [2, -1], "dependency": 3, "degree": 1.000000}, + {"attributes": [3, -1], "dependency": 2, "degree": 1.000000}]'::pg_catalog.pg_dependencies, + 'most_common_vals', '{{red,"{[1,3),[5,9),[20,30)}","{[1,3),[5,9),[20,30),[10000,10200)}"}, + {red,"{[11,13),[15,19),[20,30)}","{[11,13),[15,19),[20,30),[10000,10200)}"}, + {red,"{[21,23),[25,29),[120,130)}","{[21,23),[25,29),[120,130),[10000,10200)}"}}'::text[], + 'most_common_freqs', '{0.3333333333333333,0.3333333333333333,0.3333333333333333}'::double precision[], + 'most_common_base_freqs', '{0.1111111111111111,0.1111111111111111,0.1111111111111111}'::double precision[], + 'exprs', '[{ "avg_width": "60", "null_frac": "0", "n_distinct": "-1", + "range_length_histogram": "{10179,10189,10199}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,10200)\",\"[11,10200)\",\"[21,10200)\"}" + }]'::jsonb); + +SELECT replace(e.n_distinct, '}, ', E'},\n') AS n_distinct, + replace(e.dependencies, '}, ', E'},\n') AS dependencies, + replace(e.most_common_vals::text, '},', E'},\n ') AS mcvs, + e.most_common_val_nulls, + e.most_common_freqs, e.most_common_base_freqs +FROM pg_stats_ext AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_mr_stat' AND + e.inherited = false +\gx + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram, + e.range_length_histogram, e.range_empty_frac, e.range_bounds_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_mr_stat' AND + e.inherited = false +\gx + +-- Incorrect extended stats kind, exprs not supported +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_ndistinct', + 'inherited', false, + 'exprs', '[ { "avg_width": "4" } ]'::jsonb); + +-- Invalid exprs, not an array +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '{ "avg_width": "4", "null_frac": "0" }'::jsonb); +-- wrong number of exprs +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "avg_width": "4" } ]'::jsonb); +-- incorrect type of value: should be a string or a NULL. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "null_frac": 1 }, + { "null_frac": "0.25" } ]'::jsonb); +-- exprs null_frac not a float +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "null_frac": "BADNULLFRAC" }, + { "null_frac": "0.25" } ]'::jsonb); +-- exprs avg_width not an integer +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "avg_width": "BADAVGWIDTH" }, + { "avg_width": "4" } ]'::jsonb); +-- exprs n_dinstinct not a float +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "n_distinct": "BADNDISTINCT" }, + { "n_distinct": "-0.5" } ]'::jsonb); +-- MCV not null, MCF null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1}", "most_common_elems": null }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +-- MCV not null, MCF missing +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +-- MCV null, MCF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": null, "most_common_freqs": "{0.5}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +-- MCV missing, MCF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_freqs": "{0.5}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +-- exprs most_common_vals element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{BADMCV}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); +-- exprs most_common_freqs element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "most_common_vals": "{1}", "most_common_freqs": "{BADMCF}" }, + { "most_common_vals": "{2}", "most_common_freqs": "{0.5}" } + ]'::jsonb); +-- exprs histogram wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { "histogram_bounds": "{BADHIST,0}" }, + { "histogram_bounds": null } + ]'::jsonb); +-- exprs correlation wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ { "correlation": "BADCORR" }, + { "correlation": "1" } + ]'::jsonb); +-- invalid element type in array +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[1, null]'::jsonb); +-- invalid element in array, as valid jbvBinary. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[null, [{"avg_width" : [1]}]]'::jsonb); +-- only range types can have range_length_histogram, range_empty_frac +-- or range_bounds_histogram. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "range_length_histogram": "{10179,10189,10199}", + "range_empty_frac": "0", + "range_bounds_histogram": "{10200,10200,10200}" + } + ]'::jsonb); +-- only array types can have most_common_elems or elem_count_histogram. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "most_common_elems": "{1,2,3}", + "most_common_elem_freqs": "{0.3,0.3,0.4}" + } + ]'::jsonb); +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "elem_count_histogram": "{1,2,3,4,5}" + } + ]'::jsonb); +-- ok: exprs first null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + null, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram, + e.range_length_histogram, e.range_empty_frac, e.range_bounds_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_clone' AND + e.inherited = false +\gx +-- ok: exprs last null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{1}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + null + ]'::jsonb); + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_clone' AND + e.inherited = false +\gx +-- ok: both exprs +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_clone', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_clone', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "4", + "null_frac": "0", + "n_distinct": "-0.75", + "correlation": "-0.6", + "histogram_bounds": "{-1,0}", + "most_common_vals": "{1}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + }, + { + "avg_width": "4", + "null_frac": "0.25", + "n_distinct": "-0.5", + "correlation": "1", + "histogram_bounds": null, + "most_common_vals": "{2}", + "most_common_elems": null, + "most_common_freqs": "{0.5}", + "elem_count_histogram": null, + "most_common_elem_freqs": null + } + ]'::jsonb); +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_clone' AND + e.inherited = false +\gx + +-- A statistics object for testing MCELEM values in expressions +CREATE STATISTICS stats_import.test_stat_mcelem + ON name, (ARRAY[(comp).a, lower(arange)]) + FROM stats_import.test; + +-- MCEV not null, MCEF null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,0,1,2,3}", + "most_common_elem_freqs": null + } + ]'::jsonb); +-- MCEV not null, MCEF missing +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,0,1,2,3}" + } + ]'::jsonb); +-- MCEV null, MCEF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": null, + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +-- MCEV missing, MCEF not null +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +-- exprs most_common_elems element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,BADELEM,1,2,3}", + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +-- exprs most_common_elem_freqs element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "most_common_elems": "{-1,0,1,2,3}", + "most_common_elem_freqs": "{BADELEMFREQ,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); +-- exprs histogram bounds element wrong type +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "elem_count_histogram": "{BADELEMHIST,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5}" + } + ]'::jsonb); +-- ok: exprs mcelem +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "33", + "null_frac": "0", + "n_distinct": "-1", + "correlation": "1", + "histogram_bounds": "{\"{1,1}\",\"{2,1}\",\"{3,-1}\",\"{NULL,0}\"}", + "most_common_vals": null, + "most_common_elems": "{-1,0,1,2,3}", + "most_common_freqs": null, + "elem_count_histogram": "{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5}", + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}" + } + ]'::jsonb); + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcelem' AND + e.inherited = false +\gx + +-- ok, with warning: extra exprs param +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_mcelem', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "33", + "null_frac": "0", + "n_distinct": "-1", + "correlation": "1", + "histogram_bounds": "{\"{1,1}\",\"{2,1}\",\"{3,-1}\",\"{NULL,0}\"}", + "most_common_vals": null, + "most_common_elems": "{-1,0,1,2,3}", + "most_common_freqs": null, + "elem_count_histogram": "{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1.5}", + "most_common_elem_freqs": "{0.25,0.25,0.5,0.25,0.25,0.25,0.5,0.25}", + "bad_param": "text no one will ever parse" + } + ]'::jsonb); + +SELECT e.expr, e.null_frac, e.avg_width, e.n_distinct, e.most_common_vals, + e.most_common_freqs, e.histogram_bounds, e.correlation, + e.most_common_elems, e.most_common_elem_freqs, e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_mcelem' AND + e.inherited = false +\gx + +-- ok: tsvector exceptions, test just the collation exceptions +CREATE STATISTICS stats_import.test_stat_tsvec ON (length(name)), (to_tsvector(name)) FROM stats_import.test; +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_stat_tsvec', + 'inherited', false, + 'exprs', '[null, + { + "most_common_elems": "{one,tre,two,four}", + "most_common_elem_freqs": "{0.25,0.25,0.25,0.25,0.25,0.25}" + } + ]'::jsonb); +SELECT e.expr, e.most_common_elems, e.most_common_elem_freqs +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_stat_tsvec' AND + e.inherited = false +\gx + +-- Test the ability of pg_restore_extended_stats() to import all of the +-- statistic values from an extended statistic object that has been +-- populated via a regular ANALYZE. This checks after the statistics +-- kinds supported by pg_restore_extended_stats(). +-- +-- Note: Keep this test at the bottom of the file, so as the amount of +-- statistics data handled is maximized. +ANALYZE stats_import.test; + +-- Copy stats from test_stat to test_stat_clone +SELECT e.statistics_name, + pg_catalog.pg_restore_extended_stats( + 'schemaname', e.statistics_schemaname::text, + 'relname', 'test_clone', + 'statistics_schemaname', e.statistics_schemaname::text, + 'statistics_name', 'test_stat_clone', + 'inherited', e.inherited, + 'n_distinct', e.n_distinct, + 'dependencies', e.dependencies, + 'most_common_vals', e.most_common_vals, + 'most_common_freqs', e.most_common_freqs, + 'most_common_base_freqs', e.most_common_base_freqs, + 'exprs', x.exprs) +FROM pg_stats_ext AS e +CROSS JOIN LATERAL ( + SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object( + 'null_frac', ee.null_frac::text, + 'avg_width', ee.avg_width::text, + 'n_distinct', ee.n_distinct::text, + 'most_common_vals', ee.most_common_vals::text, + 'most_common_freqs', ee.most_common_freqs::text, + 'histogram_bounds', ee.histogram_bounds::text, + 'correlation', ee.correlation::text, + 'most_common_elems', ee.most_common_elems::text, + 'most_common_elem_freqs', ee.most_common_elem_freqs::text, + 'elem_count_histogram', ee.elem_count_histogram::text, + 'range_length_histogram', ee.range_length_histogram::text, + 'range_empty_frac', ee.range_empty_frac::text, + 'range_bounds_histogram', ee.range_bounds_histogram::text))) + FROM pg_stats_ext_exprs AS ee + WHERE ee.statistics_schemaname = e.statistics_schemaname AND + ee.statistics_name = e.statistics_name AND + ee.inherited = e.inherited + ) AS x(exprs) +WHERE e.statistics_schemaname = 'stats_import' +AND e.statistics_name = 'test_stat'; + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_get_difference('test_stat', 'test_stat_clone') +\gx + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_exprs_get_difference('test_stat', 'test_stat_clone') +\gx + + +ANALYZE stats_import.test_mr; + +-- Copy stats from test_mr_stat to test_mr_stat_clone +SELECT e.statistics_name, + pg_catalog.pg_restore_extended_stats( + 'schemaname', e.statistics_schemaname::text, + 'relname', 'test_mr_clone', + 'statistics_schemaname', e.statistics_schemaname::text, + 'statistics_name', 'test_mr_stat_clone', + 'inherited', e.inherited, + 'n_distinct', e.n_distinct, + 'dependencies', e.dependencies, + 'most_common_vals', e.most_common_vals, + 'most_common_freqs', e.most_common_freqs, + 'most_common_base_freqs', e.most_common_base_freqs, + 'exprs', x.exprs) +FROM pg_stats_ext AS e +CROSS JOIN LATERAL ( + SELECT jsonb_agg(jsonb_strip_nulls(jsonb_build_object( + 'null_frac', ee.null_frac::text, + 'avg_width', ee.avg_width::text, + 'n_distinct', ee.n_distinct::text, + 'most_common_vals', ee.most_common_vals::text, + 'most_common_freqs', ee.most_common_freqs::text, + 'histogram_bounds', ee.histogram_bounds::text, + 'correlation', ee.correlation::text, + 'most_common_elems', ee.most_common_elems::text, + 'most_common_elem_freqs', ee.most_common_elem_freqs::text, + 'elem_count_histogram', ee.elem_count_histogram::text, + 'range_length_histogram', ee.range_length_histogram::text, + 'range_empty_frac', ee.range_empty_frac::text, + 'range_bounds_histogram', ee.range_bounds_histogram::text))) + FROM pg_stats_ext_exprs AS ee + WHERE ee.statistics_schemaname = e.statistics_schemaname AND + ee.statistics_name = e.statistics_name AND + ee.inherited = e.inherited + ) AS x(exprs) +WHERE e.statistics_schemaname = 'stats_import' +AND e.statistics_name = 'test_mr_stat'; + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_get_difference('test_mr_stat', 'test_mr_stat_clone') +\gx + +SELECT statname, (stats).* +FROM stats_import.pg_stats_ext_exprs_get_difference('test_mr_stat', 'test_mr_stat_clone') +\gx + +-- range_length_histogram, range_empty_frac, and range_bounds_histogram +-- have been added to pg_stats_ext_exprs in PostgreSQL 19. When dumping +-- expression statistics in a cluster with an older version, these fields +-- are dumped as NULL, pg_restore_extended_stats() authorizing the partial +-- restore state of the extended statistics data. This test emulates such +-- a case by calling pg_restore_extended_stats() with NULL values for all +-- the three range fields, then checks the statistics loaded with some +-- queries. +CREATE TABLE stats_import.test_range_expr_null( + id INTEGER PRIMARY KEY, + name TEXT, + rng int4range NOT NULL +); +INSERT INTO stats_import.test_range_expr_null + SELECT i, 'name_' || (i % 10), int4range(i, i + 10) + FROM generate_series(1, 100) i; +-- Create statistics with a range expression +CREATE STATISTICS stats_import.stat_range_expr_null + ON name, (rng * int4range(50, 150)) + FROM stats_import.test_range_expr_null; +ANALYZE stats_import.test_range_expr_null; +-- Verify range statistics were created +SELECT e.expr, + e.range_length_histogram IS NOT NULL AS has_range_len, + e.range_empty_frac IS NOT NULL AS has_range_empty, + e.range_bounds_histogram IS NOT NULL AS has_range_bounds +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' + AND e.statistics_name = 'stat_range_expr_null'; +-- Import statistics with NULL range fields, simulating dump from +-- older version. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_range_expr_null', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'stat_range_expr_null', + 'inherited', false, + 'exprs', '[ + { + "avg_width": "14", + "null_frac": "0", + "n_distinct": "-1" + } + ]'::jsonb); +-- Verify that range fields are now NULL. +SELECT e.expr, + e.null_frac, + e.range_length_histogram IS NOT NULL AS has_range_len, + e.range_empty_frac IS NOT NULL AS has_range_empty, + e.range_bounds_histogram IS NOT NULL AS has_range_bounds +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' + AND e.statistics_name = 'stat_range_expr_null'; +-- Trigger statistics loading through some queries. +EXPLAIN (COSTS OFF) +SELECT * FROM stats_import.test_range_expr_null + WHERE (rng * int4range(50, 150)) && '[60,70)'::int4range; +SELECT COUNT(*) FROM stats_import.test_range_expr_null + WHERE (rng * int4range(50, 150)) && '[60,70)'::int4range; + DROP SCHEMA stats_import CASCADE; diff --git a/src/test/regress/sql/stats_rewrite.sql b/src/test/regress/sql/stats_rewrite.sql new file mode 100644 index 0000000000000..b01ad75f695b7 --- /dev/null +++ b/src/test/regress/sql/stats_rewrite.sql @@ -0,0 +1,220 @@ +-- +-- Test cumulative statistics with relation rewrites +-- + +-- Two-phase commit. +-- Table-level stats with VACUUM and rewrite after 2PC commit. +CREATE TABLE test_2pc_timestamp (a int) WITH (autovacuum_enabled = false); +VACUUM ANALYZE test_2pc_timestamp; +SELECT last_analyze AS last_vacuum_analyze + FROM pg_stat_all_tables WHERE relname = 'test_2pc_timestamp' \gset +BEGIN; +ALTER TABLE test_2pc_timestamp ALTER COLUMN a TYPE int; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT last_analyze = :'last_vacuum_analyze'::timestamptz AS same_vacuum_ts + FROM pg_stat_all_tables WHERE relname = 'test_2pc_timestamp'; +DROP TABLE test_2pc_timestamp; + +-- Table-level stats with single rewrite after 2PC commit. +CREATE TABLE test_2pc_rewrite_alone (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_rewrite_alone VALUES (1); +BEGIN; +ALTER TABLE test_2pc_rewrite_alone ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_rewrite_alone'; +DROP TABLE test_2pc_rewrite_alone; + +-- Table-level stats with rewrite and DMLs after 2PC commit. +CREATE TABLE test_2pc (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc VALUES (1); +BEGIN; +INSERT INTO test_2pc VALUES (1); +INSERT INTO test_2pc VALUES (2); +INSERT INTO test_2pc VALUES (3); +ALTER TABLE test_2pc ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc'; +DROP TABLE test_2pc; + +-- Table-level stats with multiple rewrites after 2PC commit. +CREATE TABLE test_2pc_multi (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_multi VALUES (1); +BEGIN; +INSERT INTO test_2pc_multi VALUES (1); +INSERT INTO test_2pc_multi VALUES (2); +ALTER TABLE test_2pc_multi ALTER COLUMN a TYPE bigint; +INSERT INTO test_2pc_multi VALUES (3); +INSERT INTO test_2pc_multi VALUES (4); +ALTER TABLE test_2pc_multi ALTER COLUMN a TYPE int; +INSERT INTO test_2pc_multi VALUES (5); +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_multi'; +DROP TABLE test_2pc_multi; + +-- Table-level stats with single rewrite after 2PC abort. +CREATE TABLE test_2pc_rewrite_alone_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_rewrite_alone_abort VALUES (1); +BEGIN; +ALTER TABLE test_2pc_rewrite_alone_abort ALTER COLUMN a TYPE bigint; +PREPARE TRANSACTION 'test'; +ROLLBACK PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_rewrite_alone_abort'; +DROP TABLE test_2pc_rewrite_alone_abort; + +-- Table-level stats with rewrite and DMLs after 2PC abort. +CREATE TABLE test_2pc_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_abort VALUES (1); +BEGIN; +INSERT INTO test_2pc_abort VALUES (1); +INSERT INTO test_2pc_abort VALUES (2); +ALTER TABLE test_2pc_abort ALTER COLUMN a TYPE bigint; +INSERT INTO test_2pc_abort VALUES (3); +PREPARE TRANSACTION 'test'; +ROLLBACK PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_abort'; +DROP TABLE test_2pc_abort; + +-- Table-level stats with rewrites and subtransactions after 2PC commit. +CREATE TABLE test_2pc_savepoint (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_2pc_savepoint VALUES (1); +BEGIN; +SAVEPOINT a; +INSERT INTO test_2pc_savepoint VALUES (1); +INSERT INTO test_2pc_savepoint VALUES (2); +ALTER TABLE test_2pc_savepoint ALTER COLUMN a TYPE bigint; +SAVEPOINT b; +INSERT INTO test_2pc_savepoint VALUES (3); +ALTER TABLE test_2pc_savepoint ALTER COLUMN a TYPE int; +SAVEPOINT c; +INSERT INTO test_2pc_savepoint VALUES (4); +INSERT INTO test_2pc_savepoint VALUES (5); +ROLLBACK TO SAVEPOINT b; +PREPARE TRANSACTION 'test'; +COMMIT PREPARED 'test'; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_2pc_savepoint'; +DROP TABLE test_2pc_savepoint; + +-- Table-level stats with single rewrite and VACUUM +CREATE TABLE test_timestamp (a int) WITH (autovacuum_enabled = false); +VACUUM ANALYZE test_timestamp; +SELECT last_analyze AS last_vacuum_analyze + FROM pg_stat_all_tables WHERE relname = 'test_timestamp' \gset +ALTER TABLE test_timestamp ALTER COLUMN a TYPE bigint; +SELECT pg_stat_force_next_flush(); +SELECT last_analyze = :'last_vacuum_analyze'::timestamptz AS same_vacuum_ts + FROM pg_stat_all_tables WHERE relname = 'test_timestamp'; +DROP TABLE test_timestamp; + +-- Table-level stats with single rewrite. +CREATE TABLE test_alone (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_alone VALUES (1); +BEGIN; +ALTER TABLE test_alone ALTER COLUMN a TYPE bigint; +COMMIT; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_alone'; +DROP TABLE test_alone; + +-- Table-level stats with rewrite and DMLs. +CREATE TABLE test (a int) WITH (autovacuum_enabled = false); +INSERT INTO test VALUES (1); +BEGIN; +INSERT INTO test VALUES (1); +INSERT INTO test VALUES (2); +INSERT INTO test VALUES (3); +ALTER TABLE test ALTER COLUMN a TYPE bigint; +COMMIT; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test'; +DROP TABLE test; + +-- Table-level stats with multiple rewrites and DMLs. +CREATE TABLE test_multi (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_multi VALUES (1); +BEGIN; +INSERT INTO test_multi VALUES (1); +INSERT INTO test_multi VALUES (2); +ALTER TABLE test_multi ALTER COLUMN a TYPE bigint; +INSERT INTO test_multi VALUES (3); +INSERT INTO test_multi VALUES (4); +ALTER TABLE test_multi ALTER COLUMN a TYPE int; +INSERT INTO test_multi VALUES (5); +COMMIT; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_multi'; +DROP TABLE test_multi; + +-- Table-level stats with rewrite and rollback. +CREATE TABLE test_rewrite_alone_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_rewrite_alone_abort VALUES (1); +BEGIN; +ALTER TABLE test_rewrite_alone_abort ALTER COLUMN a TYPE bigint; +ROLLBACK; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_rewrite_alone_abort'; +DROP TABLE test_rewrite_alone_abort; + +-- Table-level stats with rewrite, DMLs and rollback. +CREATE TABLE test_abort (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_abort VALUES (1); +BEGIN; +INSERT INTO test_abort VALUES (1); +INSERT INTO test_abort VALUES (2); +ALTER TABLE test_abort ALTER COLUMN a TYPE bigint; +INSERT INTO test_abort VALUES (3); +ROLLBACK; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_abort'; +DROP TABLE test_abort; + +-- Table-level stats with rewrites and subtransactions. +CREATE TABLE test_savepoint (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_savepoint VALUES (1); +BEGIN; +SAVEPOINT a; +INSERT INTO test_savepoint VALUES (1); +INSERT INTO test_savepoint VALUES (2); +ALTER TABLE test_savepoint ALTER COLUMN a TYPE bigint; +SAVEPOINT b; +INSERT INTO test_savepoint VALUES (3); +ALTER TABLE test_savepoint ALTER COLUMN a TYPE int; +SAVEPOINT c; +INSERT INTO test_savepoint VALUES (4); +INSERT INTO test_savepoint VALUES (5); +ROLLBACK TO SAVEPOINT b; +COMMIT; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_savepoint'; +DROP TABLE test_savepoint; + +-- Table-level stats with tablespace rewrite. +CREATE TABLE test_tbs (a int) WITH (autovacuum_enabled = false); +INSERT INTO test_tbs VALUES (1); +ALTER TABLE test_tbs SET TABLESPACE pg_default; +SELECT pg_stat_force_next_flush(); +SELECT n_tup_ins, n_live_tup, n_dead_tup + FROM pg_stat_all_tables WHERE relname = 'test_tbs'; +DROP TABLE test_tbs; diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql index f7b325baadf4d..5ae0e7da31a38 100644 --- a/src/test/regress/sql/strings.sql +++ b/src/test/regress/sql/strings.sql @@ -17,8 +17,6 @@ SELECT 'first line' AS "Illegal comment within continuation"; -- Unicode escapes -SET standard_conforming_strings TO on; - SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061" UESCAPE '*'; SELECT U&'a\\b' AS "a\b"; @@ -50,18 +48,11 @@ SELECT E'wrong: \udb99\u0061'; SELECT E'wrong: \U0000db99\U00000061'; SELECT E'wrong: \U002FFFFF'; +-- this is no longer allowed: SET standard_conforming_strings TO off; - -SELECT U&'d\0061t\+000061' AS U&"d\0061t\+000061"; -SELECT U&'d!0061t\+000061' UESCAPE '!' AS U&"d*0061t\+000061" UESCAPE '*'; - -SELECT U&' \' UESCAPE '!' AS "tricky"; -SELECT 'tricky' AS U&"\" UESCAPE '!'; - -SELECT U&'wrong: \061'; -SELECT U&'wrong: \+0061'; -SELECT U&'wrong: +0061' UESCAPE '+'; - +-- but this should be acceptable: +SET standard_conforming_strings TO on; +-- or this: RESET standard_conforming_strings; -- bytea @@ -76,11 +67,28 @@ SELECT E'De\\000dBeEf'::bytea; SELECT E'De\123dBeEf'::bytea; SELECT E'De\\123dBeEf'::bytea; SELECT E'De\\678dBeEf'::bytea; +SELECT E'DeAd\\\\BeEf'::bytea; SELECT reverse(''::bytea); SELECT reverse('\xaa'::bytea); SELECT reverse('\xabcd'::bytea); +SELECT ('\x' || repeat(' ', 32))::bytea; +SELECT ('\x' || repeat('!', 32))::bytea; +SELECT ('\x' || repeat('/', 34))::bytea; +SELECT ('\x' || repeat('0', 34))::bytea; +SELECT ('\x' || repeat('9', 32))::bytea; +SELECT ('\x' || repeat(':', 32))::bytea; +SELECT ('\x' || repeat('@', 34))::bytea; +SELECT ('\x' || repeat('A', 34))::bytea; +SELECT ('\x' || repeat('F', 32))::bytea; +SELECT ('\x' || repeat('G', 32))::bytea; +SELECT ('\x' || repeat('`', 34))::bytea; +SELECT ('\x' || repeat('a', 34))::bytea; +SELECT ('\x' || repeat('f', 32))::bytea; +SELECT ('\x' || repeat('g', 32))::bytea; +SELECT ('\x' || repeat('~', 34))::bytea; + SET bytea_output TO escape; SELECT E'\\xDeAdBeEf'::bytea; SELECT E'\\x De Ad Be Ef '::bytea; @@ -88,6 +96,7 @@ SELECT E'\\xDe00BeEf'::bytea; SELECT E'DeAdBeEf'::bytea; SELECT E'De\\000dBeEf'::bytea; SELECT E'De\\123dBeEf'::bytea; +SELECT E'DeAd\\\\BeEf'::bytea; -- Test non-error-throwing API too SELECT pg_input_is_valid(E'\\xDeAdBeE', 'bytea'); @@ -197,6 +206,29 @@ SELECT 'abcd\efg' SIMILAR TO '_bcd\%' ESCAPE '' AS true; SELECT 'abcdefg' SIMILAR TO '_bcd%' ESCAPE NULL AS null; SELECT 'abcdefg' SIMILAR TO '_bcd#%' ESCAPE '##' AS error; +-- Characters that should be left alone in character classes when a +-- SIMILAR TO regexp pattern is converted to POSIX style. +-- Underscore "_" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '_[_[:alpha:]_]_'; +-- Percentage "%" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '%[%[:alnum:]%]%'; +-- Dot "." +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '.[.[:alnum:].].'; +-- Dollar "$" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '$[$[:alnum:]$]$'; +-- Opening parenthesis "(" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '()[([:alnum:](]()'; +-- Caret "^" +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '^[^[:alnum:]^[^^][[^^]][\^][[\^]]\^]^'; +-- Closing square bracket "]" at the beginning of character class +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '[]%][^]%][^%]%'; +-- Closing square bracket effective after two carets at the beginning +-- of character class. +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '[^^]^'; +-- Closing square bracket after an escape sequence at the beginning of +-- a character closes the character class +EXPLAIN (COSTS OFF) SELECT * FROM TEXT_TBL WHERE f1 SIMILAR TO '[|a]%' ESCAPE '|'; + -- Test backslash escapes in regexp_replace's replacement string SELECT regexp_replace('1112223333', E'(\\d{3})(\\d{3})(\\d{4})', E'(\\1) \\2-\\3'); SELECT regexp_replace('foobarrbazz', E'(.)\\1', E'X\\&Y', 'g'); @@ -628,6 +660,50 @@ SELECT length(c), c::text FROM toasttest; SELECT c FROM toasttest; DROP TABLE toasttest; +-- test with short varlenas (up to 126 data bytes reduced to a 1-byte header) +-- being toasted. +CREATE TABLE toasttest (f1 text, f2 text); +ALTER TABLE toasttest SET (toast_tuple_target = 128); +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE EXTERNAL; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE EXTERNAL; +-- Here, the first value is a varlena large enough to make it toasted and +-- stored uncompressed. The second value is a short varlena, toasted +-- and stored uncompressed. +INSERT INTO toasttest values(repeat('1234', 1000), repeat('5678', 30)); +SELECT reltoastrelid::regclass AS reltoastname FROM pg_class + WHERE oid = 'toasttest'::regclass \gset +-- There should be two values inserted in the toast relation. +SELECT count(*) FROM :reltoastname WHERE chunk_seq = 0; +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; +TRUNCATE toasttest; +-- test with inline compressible varlenas. +SET default_toast_compression = 'pglz'; +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE MAIN; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE MAIN; +INSERT INTO toasttest values(repeat('1234', 1024), repeat('5678', 1024)); +-- There should be no values in the toast relation. +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; +SELECT count(*) FROM :reltoastname; +TRUNCATE toasttest; +-- test with external compressed data (default). +ALTER TABLE toasttest ALTER COLUMN f1 SET STORAGE EXTENDED; +ALTER TABLE toasttest ALTER COLUMN f2 SET STORAGE EXTENDED; +INSERT INTO toasttest values(repeat('1234', 10240), NULL); +-- There should be one value in the toast relation. +SELECT substr(f1, 5, 10) AS f1_data, substr(f2, 5, 10) AS f2_data + FROM toasttest; +SELECT pg_column_compression(f1) AS f1_comp, pg_column_compression(f2) AS f2_comp + FROM toasttest; +SELECT count(*) FROM :reltoastname WHERE chunk_seq = 0; +RESET default_toast_compression; +DROP TABLE toasttest; + -- -- test length -- @@ -754,6 +830,119 @@ SELECT decode(encode(('\x' || repeat('1234567890abcdef0001', 7))::bytea, SELECT encode('\x1234567890abcdef00', 'escape'); SELECT decode(encode('\x1234567890abcdef00', 'escape'), 'escape'); +-- report an error with a hint listing valid encodings when an invalid encoding is specified +SELECT encode('\x01'::bytea, 'invalid'); -- error +SELECT decode('00', 'invalid'); -- error + +-- +-- base32hex encoding/decoding +-- +SET bytea_output TO hex; + +SELECT encode('', 'base32hex'); -- '' +SELECT encode('\x11', 'base32hex'); -- '24======' +SELECT encode('\x1122', 'base32hex'); -- '24H0====' +SELECT encode('\x112233', 'base32hex'); -- '24H36===' +SELECT encode('\x11223344', 'base32hex'); -- '24H36H0=' +SELECT encode('\x1122334455', 'base32hex'); -- '24H36H2L' +SELECT encode('\x112233445566', 'base32hex'); -- '24H36H2LCO======' + +SELECT decode('', 'base32hex'); -- '' +SELECT decode('24======', 'base32hex'); -- \x11 +SELECT decode('24H0====', 'base32hex'); -- \x1122 +SELECT decode('24H36===', 'base32hex'); -- \x112233 +SELECT decode('24H36H0=', 'base32hex'); -- \x11223344 +SELECT decode('24H36H2L', 'base32hex'); -- \x1122334455 +SELECT decode('24H36H2LCO======', 'base32hex'); -- \x112233445566 + +SELECT decode('24h36h2lco', 'base32hex'); -- OK, the encoding is case-insensitive + +-- Tests for decoding unpadded base32hex strings. Padding '=' are optional. +SELECT decode('24', 'base32hex'); +SELECT decode('24H', 'base32hex'); +SELECT decode('24H36', 'base32hex'); +SELECT decode('24H36H0', 'base32hex'); + +SELECT decode('2', 'base32hex'); -- \x, 5 bits isn't enough for a byte, so nothing is emitted +SELECT decode('11=', 'base32hex'); -- OK, non-zero padding bits are accepted (consistent with base64) + +SELECT decode('2=', 'base32hex'); -- error +SELECT decode('=', 'base32hex'); -- error +SELECT decode('W', 'base32hex'); -- error +SELECT decode('24H36H0=24', 'base32hex'); -- error + +-- Check round-trip capability of base32hex encoding for multiple random UUIDs. +DO $$ +DECLARE + v1 uuid; + v2 uuid; +BEGIN + FOR i IN 1..10 LOOP + v1 := gen_random_uuid(); + v2 := decode(encode(v1::bytea, 'base32hex'), 'base32hex')::uuid; + + IF v1 != v2 THEN + RAISE EXCEPTION 'base32hex encoding round-trip failed, expected % got %', v1, v2; + END IF; + END LOOP; + RAISE NOTICE 'OK'; +END; +$$; + + +-- +-- base64url encoding/decoding +-- + +-- Simple encoding/decoding +SELECT encode('\x69b73eff', 'base64url'); -- abc-_w +SELECT decode('abc-_w', 'base64url'); -- \x69b73eff + +-- Round-trip: decode(encode(x)) = x +SELECT decode(encode('\x1234567890abcdef00', 'base64url'), 'base64url'); -- \x1234567890abcdef00 + +-- Empty input +SELECT encode('', 'base64url'); -- '' +SELECT decode('', 'base64url'); -- '' + +-- 1 byte input +SELECT encode('\x01', 'base64url'); -- AQ +SELECT decode('AQ', 'base64url'); -- \x01 + +-- 2 byte input +SELECT encode('\x0102'::bytea, 'base64url'); -- AQI +SELECT decode('AQI', 'base64url'); -- \x0102 + +-- 3 byte input (no padding needed) +SELECT encode('\x010203'::bytea, 'base64url'); -- AQID +SELECT decode('AQID', 'base64url'); -- \x010203 + +-- 4 byte input (results in 6 base64 chars) +SELECT encode('\xdeadbeef'::bytea, 'base64url'); -- 3q2-7w +SELECT decode('3q2-7w', 'base64url'); -- \xdeadbeef + +-- Round-trip test for all lengths from 0–4 +SELECT encode(decode(encode(E'\\x', 'base64url'), 'base64url'), 'base64url'); +SELECT encode(decode(encode(E'\\x00', 'base64url'), 'base64url'), 'base64url'); +SELECT encode(decode(encode(E'\\x0001', 'base64url'), 'base64url'), 'base64url'); +SELECT encode(decode(encode(E'\\x000102', 'base64url'), 'base64url'), 'base64url'); +SELECT encode(decode(encode(E'\\x00010203', 'base64url'), 'base64url'), 'base64url'); + +-- Invalid inputs (should ERROR) +-- invalid character '@' +SELECT decode('QQ@=', 'base64url'); + +-- missing characters (incomplete group) +SELECT decode('QQ', 'base64url'); -- ok (1 byte) +SELECT decode('QQI', 'base64url'); -- ok (2 bytes) +SELECT decode('QQIDQ', 'base64url'); -- ERROR: invalid base64url end sequence + +-- unexpected '=' at start +SELECT decode('=QQQ', 'base64url'); + +-- valid base64 padding in base64url (optional, but accepted) +SELECT decode('abc-_w==', 'base64url'); -- should decode to \x69b73eff + -- -- get_bit/set_bit etc -- @@ -795,39 +984,6 @@ SELECT '\x80000000'::bytea::int4 AS "-2147483648", '\x7FFFFFFF'::bytea::int4 AS SELECT '\x8000000000000000'::bytea::int8 AS "-9223372036854775808", '\x7FFFFFFFFFFFFFFF'::bytea::int8 AS "9223372036854775807"; --- --- test behavior of escape_string_warning and standard_conforming_strings options --- -set escape_string_warning = off; -set standard_conforming_strings = off; - -show escape_string_warning; -show standard_conforming_strings; - -set escape_string_warning = on; -set standard_conforming_strings = on; - -show escape_string_warning; -show standard_conforming_strings; - -select 'a\bcd' as f1, 'a\b''cd' as f2, 'a\b''''cd' as f3, 'abcd\' as f4, 'ab\''cd' as f5, '\\' as f6; - -set standard_conforming_strings = off; - -select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as f6; - -set escape_string_warning = off; -set standard_conforming_strings = on; - -select 'a\bcd' as f1, 'a\b''cd' as f2, 'a\b''''cd' as f3, 'abcd\' as f4, 'ab\''cd' as f5, '\\' as f6; - -set standard_conforming_strings = off; - -select 'a\\bcd' as f1, 'a\\b\'cd' as f2, 'a\\b\'''cd' as f3, 'abcd\\' as f4, 'ab\\\'cd' as f5, '\\\\' as f6; - -reset standard_conforming_strings; - - -- -- Additional string functions -- diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index 007c9e7037463..6c3d9632e8a3a 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -2,6 +2,17 @@ -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; + CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -85,6 +96,58 @@ CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'i_dont_exist=param' PUBLICATION -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; + +GRANT CREATE ON DATABASE REGRESSION TO regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; + +-- fail, need USAGE privileges on server +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); + +RESET SESSION AUTHORIZATION; +GRANT USAGE ON FOREIGN SERVER test_server TO regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; + +-- fail, need user mapping +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); + +CREATE USER MAPPING FOR regress_subscription_user3 SERVER test_server OPTIONS(user 'foo', password 'secret'); + +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); + +RESET SESSION AUTHORIZATION; +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +SET SESSION AUTHORIZATION regress_subscription_user3; + +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = 'dummy', connect = false); + +DROP USER MAPPING FOR regress_subscription_user3 SERVER test_server; +RESET SESSION AUTHORIZATION; +REVOKE USAGE ON FOREIGN SERVER test_server FROM regress_subscription_user3; +SET SESSION AUTHORIZATION regress_subscription_user3; + +-- fail, must connect but lacks USAGE on server, as well as user mapping +DROP SUBSCRIPTION regress_testsub6; + +ALTER SUBSCRIPTION regress_testsub6 SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub6; + +SET SESSION AUTHORIZATION regress_subscription_user; +REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; + +DROP SERVER test_server; + +-- fail, FDW is dependent +DROP FUNCTION test_fdw_connection(oid, oid, internal); +-- warn +ALTER FOREIGN DATA WRAPPER test_fdw NO CONNECTION; + +DROP FUNCTION test_fdw_connection(oid, oid, internal); + +DROP FOREIGN DATA WRAPPER test_fdw; + -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; @@ -139,6 +202,9 @@ RESET ROLE; ALTER SUBSCRIPTION regress_testsub RENAME TO regress_testsub_foo; ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = local); ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar); +ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '-1'); +ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '80s'); +ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = 'foobar'); \dRs+ @@ -287,6 +353,33 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); DROP SUBSCRIPTION regress_testsub; +-- fail - retain_dead_tuples must be boolean +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, retain_dead_tuples = foo); + +-- ok +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, retain_dead_tuples = false); + +\dRs+ + +ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub; + +-- fail - max_retention_duration must be integer +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = foo); + +-- ok +CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, max_retention_duration = 1000); + +\dRs+ + +-- ok +ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); + +\dRs+ + +ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); +DROP SUBSCRIPTION regress_testsub; + -- let's do some tests with pg_create_subscription rather than superuser SET SESSION AUTHORIZATION regress_subscription_user3; diff --git a/src/test/regress/sql/subselect.sql b/src/test/regress/sql/subselect.sql index fec38ef85a6a6..1a02c3f86c016 100644 --- a/src/test/regress/sql/subselect.sql +++ b/src/test/regress/sql/subselect.sql @@ -340,6 +340,17 @@ select * from ( where not exists (select 1 from tenk1 as b where b.unique2 = 10000) ) ss; + +-- +-- Test cases for interactions between PARAM_EXEC, subplans and array +-- subscripts +-- + +-- check that array subscription doesn't conflict with PARAM_EXEC (see #19370) +SELECT (array[1,2])[(SELECT g.i)] FROM generate_series(1, 1) g(i); +SELECT (array[1,2])[(SELECT g.i):(SELECT g.i + 1)] FROM generate_series(1, 1) g(i); + + -- -- Test that an IN implemented using a UniquePath does unique-ification -- with the right semantics, as per bug #4113. (Unfortunately we have @@ -361,6 +372,75 @@ select * from float_table select * from numeric_table where num_col in (select float_col from float_table); +-- +-- Test that a semijoin implemented by unique-ifying the RHS can explore +-- different paths of the RHS rel. +-- + +create table semijoin_unique_tbl (a int, b int); +insert into semijoin_unique_tbl select i%10, i%10 from generate_series(1,1000)i; +create index on semijoin_unique_tbl(a, b); +analyze semijoin_unique_tbl; + +-- Ensure that we get a plan with Unique + IndexScan +explain (verbose, costs off) +select * from semijoin_unique_tbl t1, semijoin_unique_tbl t2 +where (t1.a, t2.a) in (select a, b from semijoin_unique_tbl t3) +order by t1.a, t2.a; + +-- Ensure that we can unique-ify expressions more complex than plain Vars +explain (verbose, costs off) +select * from semijoin_unique_tbl t1, semijoin_unique_tbl t2 +where (t1.a, t2.a) in (select a+1, b+1 from semijoin_unique_tbl t3) +order by t1.a, t2.a; + +-- encourage use of parallel plans +set parallel_setup_cost=0; +set parallel_tuple_cost=0; +set min_parallel_table_scan_size=0; +set max_parallel_workers_per_gather=4; + +set enable_indexscan to off; + +-- Ensure that we get a parallel plan for the unique-ification +explain (verbose, costs off) +select * from semijoin_unique_tbl t1, semijoin_unique_tbl t2 +where (t1.a, t2.a) in (select a, b from semijoin_unique_tbl t3) +order by t1.a, t2.a; + +reset enable_indexscan; + +reset max_parallel_workers_per_gather; +reset min_parallel_table_scan_size; +reset parallel_tuple_cost; +reset parallel_setup_cost; + +drop table semijoin_unique_tbl; + +create table unique_tbl_p (a int, b int) partition by range(a); +create table unique_tbl_p1 partition of unique_tbl_p for values from (0) to (5); +create table unique_tbl_p2 partition of unique_tbl_p for values from (5) to (10); +create table unique_tbl_p3 partition of unique_tbl_p for values from (10) to (20); +insert into unique_tbl_p select i%12, i from generate_series(0, 1000)i; +create index on unique_tbl_p1(a); +create index on unique_tbl_p2(a); +create index on unique_tbl_p3(a); +analyze unique_tbl_p; + +set enable_partitionwise_join to on; + +-- Ensure that the unique-ification works for partition-wise join +-- (Only one of the two joins will be done partitionwise, but that's good +-- enough for our purposes.) +explain (verbose, costs off) +select * from unique_tbl_p t1, unique_tbl_p t2 +where (t1.a, t2.a) in (select a, a from unique_tbl_p t3) +order by t1.a, t2.a; + +reset enable_partitionwise_join; + +drop table unique_tbl_p; + -- -- Test case for bug #4290: bogus calculation of subplan param sets -- @@ -412,6 +492,15 @@ select (select view_a) from view_a; select (select (select view_a)) from view_a; select (select (a.*)::text) from view_a a; +-- +-- Test case for bug #19037: no relation entry for relid N +-- + +explain (costs off) +select (1 = any(array_agg(f1))) = any (select false) from int4_tbl; + +select (1 = any(array_agg(f1))) = any (select false) from int4_tbl; + -- -- Check that whole-row Vars reading the result of a subselect don't include -- any junk columns therein @@ -863,6 +952,46 @@ select * from (select 9 as x, unnest(array[1,2,3,11,12,13]) as u) ss where tattle(x, u); +-- +-- check that an upper-level qual is not pushed down if it references a grouped +-- Var whose underlying expression contains SRFs +-- +explain (verbose, costs off) +select * from + (select generate_series(1, ten) as g, count(*) from tenk1 group by 1) ss + where ss.g = 1; + +select * from + (select generate_series(1, ten) as g, count(*) from tenk1 group by 1) ss + where ss.g = 1; + +-- +-- check that an upper-level qual is not pushed down if it references a grouped +-- Var whose underlying expression contains volatile functions +-- +alter function tattle(x int, y int) volatile; + +explain (verbose, costs off) +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + +-- if we pretend it's stable, we get different results: +alter function tattle(x int, y int) stable; + +explain (verbose, costs off) +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + +select * from + (select tattle(3, ten) as v, count(*) from tenk1 where unique1 < 3 group by 1) ss + where ss.v; + drop function tattle(x int, y int); -- @@ -920,6 +1049,23 @@ fetch backward all in c1; commit; +-- +-- Check that JsonConstructorExpr is treated as non-strict, and thus can be +-- wrapped in a PlaceHolderVar +-- + +begin; + +create temp table json_tab (a int); +insert into json_tab values (1); + +explain (verbose, costs off) +select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false; + +select * from json_tab t1 left join (select json_array(1, a) from json_tab t2) s on false; + +rollback; + -- -- Verify that we correctly flatten cases involving a subquery output -- expression that doesn't need to be wrapped in a PlaceHolderVar @@ -1041,7 +1187,7 @@ explain (verbose, costs off) select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; @@ -1049,7 +1195,7 @@ order by 1, 2, 3; select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 inner join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; @@ -1059,7 +1205,7 @@ explain (verbose, costs off) select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; @@ -1067,7 +1213,7 @@ order by 1, 2, 3; select ss2.* from int8_tbl t1 left join (int8_tbl t2 left join - (select coalesce(q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join + (select coalesce(q1, q1) as x, * from int8_tbl t3) ss1 on t2.q1 = ss1.q2 left join lateral (select ss1.x as y, * from int8_tbl t4) ss2 on t2.q2 = ss2.q1) on t1.q2 = ss2.q1 order by 1, 2, 3; @@ -1302,3 +1448,202 @@ SELECT * FROM onek t1, lateral (SELECT * FROM onek t2 WHERE t2.ten IN (values (t -- VtA causes the whole expression to be evaluated as a constant EXPLAIN (COSTS OFF) SELECT ten FROM onek t WHERE 1.0::integer IN ((VALUES (1), (3))); + +-- +-- Check NOT IN performs an ANTI JOIN when both the outer query's expressions +-- and the sub-select's output columns are provably non-nullable, and the +-- operator itself cannot return NULL for non-null inputs. +-- + +BEGIN; + +CREATE TEMP TABLE not_null_tab (id int NOT NULL, val int NOT NULL); +CREATE TEMP TABLE null_tab (id int, val int); + +-- ANTI JOIN: both sides are defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM not_null_tab); + +-- No ANTI JOIN: outer side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab +WHERE id NOT IN (SELECT id FROM not_null_tab); + +-- No ANTI JOIN: inner side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM null_tab); + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM null_tab WHERE id IS NOT NULL); + +-- No ANTI JOIN: outer side is nullable (we don't check outer query quals for now) +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab +WHERE id IS NOT NULL + AND id NOT IN (SELECT id FROM not_null_tab); + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is defined NOT NULL +-- and is not nulled by outer join +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t1.id + FROM not_null_tab t1 + LEFT JOIN not_null_tab t2 ON t1.id = t2.id +); + +-- No ANTI JOIN: inner side is defined NOT NULL but is nulled by outer join +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t2.id + FROM not_null_tab t1 + LEFT JOIN not_null_tab t2 ON t1.id = t2.id +); + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t2.id + FROM not_null_tab t1 + LEFT JOIN not_null_tab t2 ON t1.id = t2.id + WHERE t2.id IS NOT NULL +); + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t1.id + FROM null_tab t1 + LEFT JOIN null_tab t2 ON t1.id = t2.id + WHERE t1.id IS NOT NULL +); + +-- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable +-- by qual clause +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT t1.id + FROM null_tab t1 + INNER JOIN null_tab t2 ON t1.id = t2.id + LEFT JOIN null_tab t3 ON TRUE +); + +-- ANTI JOIN: outer side is defined NOT NULL and is not nulled by outer join, +-- inner side is defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 ON t1.id = t2.id +WHERE t1.id NOT IN (SELECT id FROM not_null_tab); + +-- No ANTI JOIN: outer side is nulled by outer join +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 ON t1.id = t2.id +WHERE t2.id NOT IN (SELECT id FROM not_null_tab); + +-- No ANTI JOIN: sublink is in an outer join's ON qual and references the +-- non-nullable side +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 +ON t1.id NOT IN (SELECT id FROM not_null_tab); + +-- ANTI JOIN: outer side is defined NOT NULL and is not nulled by outer join, +-- inner side is defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1 +LEFT JOIN not_null_tab t2 +ON t2.id NOT IN (SELECT id FROM not_null_tab); + +-- ANTI JOIN: both sides are defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE (id, val) NOT IN (SELECT id, val FROM not_null_tab); + +-- ANTI JOIN: both sides are defined NOT NULL +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE NOT (id, val) > ANY (SELECT id, val FROM not_null_tab); + +-- No ANTI JOIN: one column of the outer side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab t1, null_tab t2 +WHERE (t1.id, t2.id) NOT IN (SELECT id, val FROM not_null_tab); + +-- No ANTI JOIN: one column of the inner side is nullable +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE (id, val) NOT IN (SELECT t1.id, t2.id FROM not_null_tab t1, null_tab t2); + +-- ANTI JOIN: COALESCE(nullable, constant) is non-nullable +EXPLAIN (COSTS OFF) +SELECT * FROM null_tab +WHERE COALESCE(id, -1) NOT IN (SELECT id FROM not_null_tab); + +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT COALESCE(id, -1) FROM null_tab); + +-- ANTI JOIN: GROUP BY (without Grouping Sets) preserves the non-nullability of +-- the column +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM not_null_tab GROUP BY id); + +-- No ANTI JOIN: GROUP BY on a nullable column +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM null_tab GROUP BY id); + +-- No ANTI JOIN: Grouping Sets can introduce NULLs +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN ( + SELECT id + FROM not_null_tab + GROUP BY GROUPING SETS ((id), (val)) +); + +-- create a custom "unsafe" equality operator +CREATE FUNCTION int4eq_unsafe(int4, int4) + RETURNS bool + AS 'int4eq' + LANGUAGE internal IMMUTABLE; + +CREATE OPERATOR ?= ( + PROCEDURE = int4eq_unsafe, + LEFTARG = int4, + RIGHTARG = int4 +); + +-- No ANTI JOIN: the operator is not safe +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE NOT id ?= ANY (SELECT id FROM not_null_tab); + +-- No ANTI JOIN: the inner side has an unvalidated NOT NULL constraint, so +-- the column might contain NULLs. +CREATE TEMP TABLE notnull_notvalid_tab (id int); +INSERT INTO notnull_notvalid_tab VALUES (NULL); +ALTER TABLE notnull_notvalid_tab ADD CONSTRAINT nn NOT NULL id NOT VALID; + +EXPLAIN (COSTS OFF) +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); + +-- NOT IN with NULL on inner side should return no rows +SELECT * FROM not_null_tab +WHERE id NOT IN (SELECT id FROM notnull_notvalid_tab); + +ROLLBACK; diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b379..507e400ad4af9 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -18,7 +18,7 @@ select type, name, ident, level, total_bytes >= free_bytes from pg_backend_memory_contexts where level = 1; -- We can exercise some MemoryContext type stats functions. Most of the --- column values are too platform-dependant to display. +-- column values are too platform-dependent to display. -- Ensure stats from the bump allocator look sane. Bump isn't a commonly -- used context, but it is used in tuplesort.c, so open a cursor to keep @@ -76,6 +76,9 @@ select count(*) = 1 as ok from pg_stat_wal; -- We expect no walreceiver running in this test select count(*) = 0 as ok from pg_stat_wal_receiver; +-- We expect no recovery state in this test (running on primary) +select count(*) = 0 as ok from pg_stat_recovery; + -- This is to record the prevailing planner enable_foo settings during -- a regression test run. select name, setting from pg_settings where name like 'enable%'; diff --git a/src/test/regress/sql/tablespace.sql b/src/test/regress/sql/tablespace.sql index dfe3db096e28d..c43a59e595736 100644 --- a/src/test/regress/sql/tablespace.sql +++ b/src/test/regress/sql/tablespace.sql @@ -430,6 +430,10 @@ ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPAC ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default; +-- Should fail, contains \n in name +ALTER TABLESPACE regress_tblspace_renamed RENAME TO "invalid +name"; + -- Should succeed DROP TABLESPACE regress_tblspace_renamed; diff --git a/src/test/regress/sql/tablespace_ddl.sql b/src/test/regress/sql/tablespace_ddl.sql new file mode 100644 index 0000000000000..ee3cc6e2e1e85 --- /dev/null +++ b/src/test/regress/sql/tablespace_ddl.sql @@ -0,0 +1,58 @@ +-- +-- Tests for pg_get_tablespace_ddl() +-- + +SET allow_in_place_tablespaces = true; +CREATE ROLE regress_tblspc_ddl_user; + +-- error: non-existent tablespace by name +SELECT * FROM pg_get_tablespace_ddl('regress_nonexistent_tblsp'); + +-- error: non-existent tablespace by OID +SELECT * FROM pg_get_tablespace_ddl(0::oid); + +-- NULL input returns no rows (name variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::name); + +-- NULL input returns no rows (OID variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::oid); + +-- tablespace name requiring quoting +CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT * FROM pg_get_tablespace_ddl('regress_ tblsp'); +DROP TABLESPACE "regress_ tblsp"; + +-- tablespace with multiple options +CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION '' + WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', + effective_io_concurrency = '17', maintenance_io_concurrency = '18'); +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp'); + +-- pretty-printed output +\pset format unaligned +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'pretty', 'true'); +\pset format aligned + +-- tablespace with owner suppressed +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'owner', 'false'); + +DROP TABLESPACE regress_allopt_tblsp; + +-- test by OID +CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT oid AS tsid FROM pg_tablespace WHERE spcname = 'regress_oid_tblsp' \gset +SELECT * FROM pg_get_tablespace_ddl(:tsid); +DROP TABLESPACE regress_oid_tblsp; + +-- Permission check: revoke SELECT on pg_tablespace +CREATE TABLESPACE regress_acl_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +CREATE ROLE regress_tblspc_ddl_noaccess; +REVOKE SELECT ON pg_tablespace FROM PUBLIC; +SET ROLE regress_tblspc_ddl_noaccess; +SELECT * FROM pg_get_tablespace_ddl('regress_acl_tblsp'); -- should fail +RESET ROLE; +GRANT SELECT ON pg_tablespace TO PUBLIC; +DROP TABLESPACE regress_acl_tblsp; +DROP ROLE regress_tblspc_ddl_noaccess; + +DROP ROLE regress_tblspc_ddl_user; diff --git a/src/test/regress/sql/tid.sql b/src/test/regress/sql/tid.sql index 2602e20eb5a1f..c0a70be5cbdeb 100644 --- a/src/test/regress/sql/tid.sql +++ b/src/test/regress/sql/tid.sql @@ -16,6 +16,22 @@ SELECT pg_input_is_valid('(0,-1)', 'tid'); SELECT * FROM pg_input_error_info('(0,-1)', 'tid'); +-- tests for tid_block() and tid_offset() +SELECT tid_block('(0,0)'::tid), tid_offset('(0,0)'::tid); +SELECT tid_block('(0,1)'::tid), tid_offset('(0,1)'::tid); +SELECT tid_block('(42,7)'::tid), tid_offset('(42,7)'::tid); +-- max values: blockno uint32 max, offset uint16 max +SELECT tid_block('(4294967295,65535)'::tid), tid_offset('(4294967295,65535)'::tid); +-- (-1,0) wraps to blockno 4294967295 +SELECT tid_block('(-1,0)'::tid); +-- NULL handling (strict functions) +SELECT tid_block(NULL::tid), tid_offset(NULL::tid); +-- round-trip: blockno + offset reconstruct the original TID +SELECT t, tid_block(t), tid_offset(t), + format('(%s,%s)', tid_block(t), tid_offset(t))::tid = t AS roundtrip_ok +FROM (VALUES ('(0,0)'::tid), ('(1,42)'::tid), ('(4294967295,65535)'::tid)) AS v(t); + + -- tests for functions related to TID handling CREATE TABLE tid_tab (a int); @@ -24,6 +40,11 @@ CREATE TABLE tid_tab (a int); INSERT INTO tid_tab VALUES (1), (2); SELECT min(ctid) FROM tid_tab; SELECT max(ctid) FROM tid_tab; + +-- tid_block() and tid_offset() with real table ctid +SELECT ctid, tid_block(ctid), tid_offset(ctid) FROM tid_tab; +-- use in WHERE clause +SELECT ctid FROM tid_tab WHERE tid_block(ctid) = 0 AND tid_offset(ctid) = 1; TRUNCATE tid_tab; -- Tests for currtid2() with various relation kinds diff --git a/src/test/regress/sql/tidrangescan.sql b/src/test/regress/sql/tidrangescan.sql index ac09ebb626264..1ac3995e71c28 100644 --- a/src/test/regress/sql/tidrangescan.sql +++ b/src/test/regress/sql/tidrangescan.sql @@ -98,4 +98,48 @@ COMMIT; DROP TABLE tidrangescan; +-- Tests for parallel TID Range Scans +BEGIN; + +SET LOCAL parallel_setup_cost TO 0; +SET LOCAL parallel_tuple_cost TO 0; +SET LOCAL min_parallel_table_scan_size TO 0; +SET LOCAL max_parallel_workers_per_gather TO 4; + +CREATE TABLE parallel_tidrangescan (id integer, data text) +WITH (fillfactor = 10); + +-- Insert enough tuples such that each page gets 5 tuples with fillfactor = 10 +INSERT INTO parallel_tidrangescan +SELECT i, repeat('x', 100) FROM generate_series(1,200) AS s(i); + +-- Ensure there are 40 pages for parallel test +SELECT min(ctid), max(ctid) FROM parallel_tidrangescan; + +-- Parallel range scans with upper bound +EXPLAIN (COSTS OFF) +SELECT count(*) FROM parallel_tidrangescan WHERE ctid < '(30,1)'; +SELECT count(*) FROM parallel_tidrangescan WHERE ctid < '(30,1)'; + +-- Parallel range scans with lower bound +EXPLAIN (COSTS OFF) +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)'; +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)'; + +-- Parallel range scans with both bounds +EXPLAIN (COSTS OFF) +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)' AND ctid < '(30,1)'; +SELECT count(*) FROM parallel_tidrangescan WHERE ctid > '(10,0)' AND ctid < '(30,1)'; + +-- Parallel rescans +EXPLAIN (COSTS OFF) +SELECT t.ctid,t2.c FROM parallel_tidrangescan t, +LATERAL (SELECT count(*) c FROM parallel_tidrangescan t2 WHERE t2.ctid <= t.ctid) t2 +WHERE t.ctid < '(1,0)'; + +SELECT t.ctid,t2.c FROM parallel_tidrangescan t, +LATERAL (SELECT count(*) c FROM parallel_tidrangescan t2 WHERE t2.ctid <= t.ctid) t2 +WHERE t.ctid < '(1,0)'; + +ROLLBACK; RESET enable_seqscan; diff --git a/src/test/regress/sql/timestamp.sql b/src/test/regress/sql/timestamp.sql index 55f80530ea05e..313757ed041a1 100644 --- a/src/test/regress/sql/timestamp.sql +++ b/src/test/regress/sql/timestamp.sql @@ -175,7 +175,9 @@ SELECT d1 - timestamp without time zone '1997-01-02' AS diff FROM TIMESTAMP_TBL WHERE d1 BETWEEN '1902-01-01' AND '2038-01-01'; SELECT date_trunc( 'week', timestamp '2004-02-29 15:44:17.71393' ) AS week_trunc; - +SELECT date_trunc( 'week', timestamp 'infinity' ) AS inf_trunc; +SELECT date_trunc( 'timezone', timestamp '2004-02-29 15:44:17.71393' ) AS notsupp_trunc; +SELECT date_trunc( 'timezone', timestamp 'infinity' ) AS notsupp_inf_trunc; SELECT date_trunc( 'ago', timestamp 'infinity' ) AS invalid_trunc; -- verify date_bin behaves the same as date_trunc for relevant intervals diff --git a/src/test/regress/sql/timestamptz.sql b/src/test/regress/sql/timestamptz.sql index caca3123f13f6..6ace851d16911 100644 --- a/src/test/regress/sql/timestamptz.sql +++ b/src/test/regress/sql/timestamptz.sql @@ -217,15 +217,18 @@ SELECT d1 - timestamp with time zone '1997-01-02' AS diff FROM TIMESTAMPTZ_TBL WHERE d1 BETWEEN '1902-01-01' AND '2038-01-01'; SELECT date_trunc( 'week', timestamp with time zone '2004-02-29 15:44:17.71393' ) AS week_trunc; +SELECT date_trunc( 'week', timestamp with time zone 'infinity' ) AS inf_trunc; +SELECT date_trunc( 'timezone', timestamp with time zone '2004-02-29 15:44:17.71393' ) AS notsupp_trunc; +SELECT date_trunc( 'timezone', timestamp with time zone 'infinity' ) AS notsupp_inf_trunc; SELECT date_trunc( 'ago', timestamp with time zone 'infinity' ) AS invalid_trunc; SELECT date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'Australia/Sydney') as sydney_trunc; -- zone name SELECT date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'GMT') as gmt_trunc; -- fixed-offset abbreviation SELECT date_trunc('day', timestamp with time zone '2001-02-16 20:38:40+00', 'VET') as vet_trunc; -- variable-offset abbreviation +SELECT date_trunc('timezone', timestamp with time zone 'infinity', 'GMT') AS notsupp_zone_trunc; +SELECT date_trunc( 'week', timestamp with time zone 'infinity', 'GMT') AS inf_zone_trunc; SELECT date_trunc('ago', timestamp with time zone 'infinity', 'GMT') AS invalid_zone_trunc; - - -- verify date_bin behaves the same as date_trunc for relevant intervals SELECT str, diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index d3d242dd29b74..ea39817ee3d7f 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1148,7 +1148,7 @@ drop function trigger_ddl_func(); -- -- Verify behavior of before and after triggers with INSERT...ON CONFLICT --- DO UPDATE +-- DO UPDATE and DO SELECT -- create table upsert (key int4 primary key, color text); @@ -1197,6 +1197,7 @@ insert into upsert values(5, 'purple') on conflict (key) do update set color = ' insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color; insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color; insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color; +insert into upsert values(8, 'blue') on conflict (key) do select for update where upsert.color = 'yellow trig modified' returning old.*, new.*, upsert.*; select * from upsert; @@ -1576,6 +1577,19 @@ select * from parted; drop table parted; drop function parted_trigfunc(); +-- +-- Constraint triggers +-- +create constraint trigger crtr + after insert on foo not valid + for each row execute procedure foo (); +create constraint trigger crtr + after insert on foo no inherit + for each row execute procedure foo (); +create constraint trigger crtr + after insert on foo not enforced + for each row execute procedure foo (); + -- -- Constraint triggers and partitioned tables create table parted_constr_ancestor (a int, b text) @@ -1591,7 +1605,7 @@ create constraint trigger parted_trig after insert on parted_constr_ancestor deferrable for each row execute procedure trigger_notice_ab(); create constraint trigger parted_trig_two after insert on parted_constr - deferrable initially deferred + deferrable initially deferred enforced for each row when (bark(new.b) AND new.a % 2 = 1) execute procedure trigger_notice_ab(); @@ -1922,6 +1936,11 @@ BBB 42 CCC 42 \. +-- check detach/reattach behavior; statement triggers with transition tables +-- should not prevent a table from becoming a partition again +alter table parent detach partition child1; +alter table parent attach partition child1 for values in ('AAA'); + -- DML affecting parent sees tuples collected from children even if -- there is no transition table trigger on the children drop trigger child1_insert_trig on child1; @@ -2141,6 +2160,11 @@ copy parent (a, b) from stdin; DDD 42 \. +-- check disinherit/reinherit behavior; statement triggers with transition +-- tables should not prevent a table from becoming an inheritance child again +alter table child1 no inherit parent; +alter table child1 inherit parent; + -- DML affecting parent sees tuples collected from children even if -- there is no transition table trigger on the children drop trigger child1_insert_trig on child1; @@ -2205,6 +2229,10 @@ with wcte as (insert into table1 values (42)) with wcte as (insert into table1 values (43)) insert into table1 values (44); +with wcte as (insert into table1 values (45)) + merge into table1 using (values (46)) as v(a) on table1.a = v.a + when not matched then insert values (v.a); + select * from table1; select * from table2; @@ -2701,8 +2729,8 @@ drop function f(); -- Test who runs deferred trigger functions -- setup -create role regress_groot; -create role regress_outis; +create role regress_caller; +create role regress_fn_owner; create function whoami() returns trigger language plpgsql as $$ begin @@ -2710,7 +2738,7 @@ begin return null; end; $$; -alter function whoami() owner to regress_outis; +alter function whoami() owner to regress_fn_owner; create table defer_trig (id integer); grant insert on defer_trig to public; @@ -2721,10 +2749,10 @@ create constraint trigger whoami after insert on defer_trig -- deferred triggers must run as the user that queued the trigger begin; -set role regress_groot; +set role regress_caller; insert into defer_trig values (1); reset role; -set role regress_outis; +set role regress_fn_owner; insert into defer_trig values (2); reset role; commit; @@ -2732,7 +2760,7 @@ commit; -- security definer functions override the user who queued the trigger alter function whoami() security definer; begin; -set role regress_groot; +set role regress_caller; insert into defer_trig values (3); reset role; commit; @@ -2749,7 +2777,7 @@ end; $$; begin; -set role regress_groot; +set role regress_caller; insert into defer_trig values (4); reset role; commit; -- error expected @@ -2758,5 +2786,5 @@ select current_user = session_user; -- clean up drop table defer_trig; drop function whoami(); -drop role regress_outis; -drop role regress_groot; +drop role regress_fn_owner; +drop role regress_caller; diff --git a/src/test/regress/sql/tsearch.sql b/src/test/regress/sql/tsearch.sql index fbd26cdba459f..dc74aa0c889b3 100644 --- a/src/test/regress/sql/tsearch.sql +++ b/src/test/regress/sql/tsearch.sql @@ -222,6 +222,7 @@ RESET enable_bitmapscan; DROP INDEX wowidx; +ALTER TABLE test_tsvector SET (parallel_workers = 2); CREATE INDEX wowidx ON test_tsvector USING gin (a); SET enable_seqscan=OFF; diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql index c94dd83d3061c..95d5b6e09151a 100644 --- a/src/test/regress/sql/type_sanity.sql +++ b/src/test/regress/sql/type_sanity.sql @@ -451,7 +451,9 @@ WHERE (is_catalog_text_unique_index_oid(indexrelid) <> SELECT r.rngtypid, r.rngsubtype FROM pg_range as r -WHERE r.rngtypid = 0 OR r.rngsubtype = 0 OR r.rngsubopc = 0; +WHERE r.rngtypid = 0 OR r.rngsubtype = 0 OR r.rngsubopc = 0 + OR r.rngconstruct2 = 0 OR r.rngconstruct3 = 0 + OR r.rngmltconstruct0 = 0 OR r.rngmltconstruct1 = 0 OR r.rngmltconstruct2 = 0; -- rngcollation should be specified iff subtype is collatable @@ -491,6 +493,49 @@ SELECT r.rngtypid, r.rngsubtype, r.rngmultitypid FROM pg_range r WHERE r.rngmultitypid IS NULL OR r.rngmultitypid = 0; +-- check constructor function arguments and return types +-- +-- proname and prosrc are not required to have these particular +-- values, but this matches what DefineRange() produces and serves to +-- sanity-check the catalog entries for built-in types. + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngconstruct2 JOIN pg_type t ON r.rngtypid = t.oid +WHERE p.pronargs != 2 + OR p.proargtypes[0] != r.rngsubtype OR p.proargtypes[1] != r.rngsubtype + OR p.prorettype != r.rngtypid + OR p.proname != t.typname OR p.prosrc != 'range_constructor2'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngconstruct3 JOIN pg_type t ON r.rngtypid = t.oid +WHERE p.pronargs != 3 + OR p.proargtypes[0] != r.rngsubtype OR p.proargtypes[1] != r.rngsubtype OR p.proargtypes[2] != 'pg_catalog.text'::regtype + OR p.prorettype != r.rngtypid + OR p.proname != t.typname OR p.prosrc != 'range_constructor3'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct0 JOIN pg_type t ON r.rngmultitypid = t.oid +WHERE p.pronargs != 0 + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor0'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct1 JOIN pg_type t ON r.rngmultitypid = t.oid +WHERE p.pronargs != 1 + OR p.proargtypes[0] != r.rngtypid + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor1'; + +SELECT r.rngtypid, r.rngsubtype, p.proname +FROM pg_range r JOIN pg_proc p ON p.oid = r.rngmltconstruct2 JOIN pg_type t ON r.rngmultitypid = t.oid JOIN pg_type t2 ON r.rngtypid = t2.oid +WHERE p.pronargs != 1 + OR p.proargtypes[0] != t2.typarray + OR p.prorettype != r.rngmultitypid + OR p.proname != t.typname OR p.prosrc != 'multirange_constructor2'; + + +-- ****************************************** + -- Create a table that holds all the known in-core data types and leave it -- around so as pg_upgrade is able to test their binary compatibility. CREATE TABLE tab_core_types AS SELECT @@ -530,6 +575,7 @@ CREATE TABLE tab_core_types AS SELECT 'abc'::refcursor, '1 2'::int2vector, '1 2'::oidvector, + '1234'::oid8, format('%I=UC/%I', USER, USER)::aclitem AS aclitem, 'a fat cat sat on a mat and ate a fat rat'::tsvector, 'fat & rat'::tsquery, @@ -539,6 +585,7 @@ CREATE TABLE tab_core_types AS SELECT 'regtype'::regtype type, 'pg_monitor'::regrole, 'pg_class'::regclass::oid, + 'template1'::regdatabase, '(1,1)'::tid, '2'::xid, '3'::cid, '10:20:10,14,15'::txid_snapshot, '10:20:10,14,15'::pg_snapshot, diff --git a/src/test/regress/sql/typed_table.sql b/src/test/regress/sql/typed_table.sql index 57ce12782b0ac..d0ea968e71f86 100644 --- a/src/test/regress/sql/typed_table.sql +++ b/src/test/regress/sql/typed_table.sql @@ -20,7 +20,8 @@ ALTER TABLE persons DROP COLUMN name; ALTER TABLE persons RENAME COLUMN id TO num; ALTER TABLE persons ALTER COLUMN name TYPE varchar; CREATE TABLE stuff (id int); -ALTER TABLE persons INHERIT stuff; +ALTER TABLE persons INHERIT stuff; -- error +ALTER TABLE persons NO INHERIT stuff; -- error CREATE TABLE personsx OF person_type (myname WITH OPTIONS NOT NULL); -- error diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql index 13700a6bfc4e9..078c858429f6e 100644 --- a/src/test/regress/sql/union.sql +++ b/src/test/regress/sql/union.sql @@ -459,6 +459,78 @@ drop table events_child, events, other_events; reset enable_indexonlyscan; +-- +-- Test handling of UNION / EXCEPT / INTERSECT with provably empty inputs +-- + +-- Ensure the empty UNION input is pruned and de-duplication is done for the +-- remaining relation. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +UNION +SELECT four FROM tenk1 +ORDER BY 1; + +-- Validate that the results of the above are correct +SELECT two FROM tenk1 WHERE 1=2 +UNION +SELECT four FROM tenk1 +ORDER BY 1; + +-- All UNION inputs are proven empty. Ensure the planner provides a +-- const-false Result node +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +UNION +SELECT four FROM tenk1 WHERE 1=2 +UNION +SELECT ten FROM tenk1 WHERE 1=2 +ORDER BY 1; + +-- Ensure the planner provides a const-false Result node +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +INTERSECT +SELECT four FROM tenk1 +ORDER BY 1; + +-- As above, with the inputs swapped +EXPLAIN (COSTS OFF, VERBOSE) +SELECT four FROM tenk1 +INTERSECT +SELECT two FROM tenk1 WHERE 1=2 +ORDER BY 1; + +-- Try with both inputs dummy +EXPLAIN (COSTS OFF, VERBOSE) +SELECT four FROM tenk1 WHERE 1=2 +INTERSECT +SELECT two FROM tenk1 WHERE 1=2 +ORDER BY 1; + +-- Ensure the planner provides a const-false Result node when the left input +-- is empty +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 WHERE 1=2 +EXCEPT +SELECT four FROM tenk1 +ORDER BY 1; + +-- Ensure the planner only scans the left input when right input is empty +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 +EXCEPT ALL +SELECT four FROM tenk1 WHERE 1=2 +ORDER BY 1; + +-- Try a mixed setop case. Ensure the right-hand UNION child gets removed. +EXPLAIN (COSTS OFF, VERBOSE) +SELECT two FROM tenk1 t1 +EXCEPT +SELECT four FROM tenk1 t2 +UNION +SELECT ten FROM tenk1 dummy WHERE 1=2; + -- Test constraint exclusion of UNION ALL subqueries explain (costs off) SELECT * FROM @@ -592,3 +664,7 @@ select * from tenk1 t1 left join lateral (select t1.tenthous from tenk2 t2 union all (values(1))) on true limit 1; + +-- Test handling of Vars with varno 0 in estimate_array_length +explain (verbose, costs off) +select null::int[] union all select null::int[] union all select null::bigint[]; diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index c071fffc11639..f7646999bd46a 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -106,6 +106,12 @@ INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set a = excluded. SELECT * FROM rw_view15; INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set upper = 'blarg'; -- fails SELECT * FROM rw_view15; +INSERT INTO rw_view15 (a) VALUES (3) + ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED' + RETURNING old, new; +INSERT INTO rw_view15 (a) VALUES (3) + ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING old, new; + SELECT * FROM rw_view15; ALTER VIEW rw_view15 ALTER COLUMN upper SET DEFAULT 'NOT SET'; INSERT INTO rw_view15 (a) VALUES (4); -- should fail @@ -1850,7 +1856,7 @@ insert into wcowrtest_v2 values (2, 'no such row in sometable'); drop view wcowrtest_v, wcowrtest_v2; drop table wcowrtest, sometable; --- Check INSERT .. ON CONFLICT DO UPDATE works correctly when the view's +-- Check INSERT .. ON CONFLICT DO SELECT/UPDATE works correctly when the view's -- columns are named and ordered differently than the underlying table's. create table uv_iocu_tab (a text unique, b float); insert into uv_iocu_tab values ('xyxyxy', 0); @@ -1863,6 +1869,8 @@ select * from uv_iocu_tab; insert into uv_iocu_view (a, b) values ('xyxyxy', 1) on conflict (a) do update set b = excluded.b; select * from uv_iocu_tab; +insert into uv_iocu_view (a, b) values ('xyxyxy', 1) + on conflict (a) do select where uv_iocu_view.c = 2 and excluded.c = 2 returning *; -- OK to access view columns that are not present in underlying base -- relation in the ON CONFLICT portion of the query @@ -1881,6 +1889,20 @@ select * from uv_iocu_tab; drop view uv_iocu_view; drop table uv_iocu_tab; +-- Check UPDATE FOR PORTION OF works correctly +create table uv_fpo_tab (id int4range, valid_at tsrange, b float, + constraint pk_uv_fpo_tab primary key (id, valid_at without overlaps)); +insert into uv_fpo_tab values ('[1,1]', '[2020-01-01, 2030-01-01)', 0); +create view uv_fpo_view as + select b, b+1 as c, valid_at, id, '2.0'::text as two from uv_fpo_tab; + +insert into uv_fpo_view (id, valid_at, b) values ('[1,1]', '[2010-01-01, 2020-01-01)', 1); +select * from uv_fpo_view order by id, valid_at; +update uv_fpo_view for portion of valid_at from '2015-01-01' to '2020-01-01' set b = 2 where id = '[1,1]'; +select * from uv_fpo_view order by id, valid_at; +delete from uv_fpo_view for portion of valid_at from '2017-01-01' to '2022-01-01' where id = '[1,1]'; +select * from uv_fpo_view order by id, valid_at; + -- Test whole-row references to the view create table uv_iocu_tab (a int unique, b text); create view uv_iocu_view as @@ -1899,6 +1921,11 @@ insert into uv_iocu_view (aa,bb) values (1,'y') and excluded.bb != '' and excluded.cc is not null; select * from uv_iocu_view; +explain (costs off) +insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")') + on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *; +insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")') + on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *; -- Test omitting a column of the base relation delete from uv_iocu_view; @@ -1911,11 +1938,15 @@ alter table uv_iocu_tab alter column b set default 'table default'; insert into uv_iocu_view (aa) values (1) on conflict (aa) do update set bb = 'Rejected: '||excluded.*; select * from uv_iocu_view; +insert into uv_iocu_view (aa) values (1) + on conflict (aa) do select returning *; alter view uv_iocu_view alter column bb set default 'view default'; insert into uv_iocu_view (aa) values (1) on conflict (aa) do update set bb = 'Rejected: '||excluded.*; select * from uv_iocu_view; +insert into uv_iocu_view (aa) values (1) + on conflict (aa) do select returning *; -- Should fail to update non-updatable columns insert into uv_iocu_view (aa) values (1) @@ -1924,7 +1955,7 @@ insert into uv_iocu_view (aa) values (1) drop view uv_iocu_view; drop table uv_iocu_tab; --- ON CONFLICT DO UPDATE permissions checks +-- ON CONFLICT DO SELECT/UPDATE permissions checks create user regress_view_user1; create user regress_view_user2; @@ -1948,6 +1979,10 @@ insert into rw_view1 values ('zzz',2.0,1) on conflict (aa) do update set bb = rw_view1.bb||'xxx'; -- OK insert into rw_view1 values ('zzz',2.0,1) on conflict (aa) do update set cc = 3.0; -- Not allowed +insert into rw_view1 values ('yyy',2.0,1) + on conflict (aa) do select for update returning cc; -- Not allowed +insert into rw_view1 values ('yyy',2.0,1) + on conflict (aa) do select for update returning aa, bb; reset session authorization; select * from base_tbl; @@ -1960,9 +1995,13 @@ set session authorization regress_view_user2; create view rw_view2 as select b as bb, c as cc, a as aa from base_tbl; insert into rw_view2 (aa,bb) values (1,'xxx') on conflict (aa) do update set bb = excluded.bb; -- Not allowed +insert into rw_view2 (aa,bb) values (1,'xxx') + on conflict (aa) do select returning 1; -- Not allowed create view rw_view3 as select b as bb, a as aa from base_tbl; insert into rw_view3 (aa,bb) values (1,'xxx') on conflict (aa) do update set bb = excluded.bb; -- OK +insert into rw_view3 (aa,bb) values (1,'xxx') + on conflict (aa) do select returning aa, bb; -- OK reset session authorization; select * from base_tbl; @@ -1970,6 +2009,8 @@ set session authorization regress_view_user2; create view rw_view4 as select aa, bb, cc FROM rw_view1; insert into rw_view4 (aa,bb) values (1,'yyy') on conflict (aa) do update set bb = excluded.bb; -- Not allowed +insert into rw_view4 (aa,bb) values (1,'yyy') + on conflict (aa) do select returning 1; -- Not allowed create view rw_view5 as select aa, bb FROM rw_view1; insert into rw_view5 (aa,bb) values (1,'yyy') on conflict (aa) do update set bb = excluded.bb; -- OK diff --git a/src/test/regress/sql/uuid.sql b/src/test/regress/sql/uuid.sql index 465153a03415c..8cc2ad40614ae 100644 --- a/src/test/regress/sql/uuid.sql +++ b/src/test/regress/sql/uuid.sql @@ -13,7 +13,8 @@ CREATE TABLE guid2 CREATE TABLE guid3 ( id SERIAL, - guid_field UUID + guid_field UUID, + guid_encoded text GENERATED ALWAYS AS (encode(guid_field::bytea, 'base32hex')) STORED ); -- inserting invalid data tests @@ -116,9 +117,17 @@ INSERT INTO guid1 (guid_field) VALUES (uuidv7(INTERVAL '1 day')); SELECT count(DISTINCT guid_field) FROM guid1; -- test sortability of v7 +INSERT INTO guid3 (guid_field) VALUES ('00000000-0000-0000-0000-000000000000'::uuid); INSERT INTO guid3 (guid_field) SELECT uuidv7() FROM generate_series(1, 10); +INSERT INTO guid3 (guid_field) VALUES ('ffffffff-ffff-ffff-ffff-ffffffffffff'::uuid); SELECT array_agg(id ORDER BY guid_field) FROM guid3; +-- Test base32hex encoding of UUIDs and its lexicographical sorting property. +-- COLLATE "C" is required to prevent buildfarm failures in non-C locales +-- where natural language collations (such as cs_CZ) would break strict +-- byte-wise ordering. +SELECT array_agg(id ORDER BY guid_encoded COLLATE "C") FROM guid3; + -- Check the timestamp offsets for v7. -- -- generate UUIDv7 values with timestamps ranging from 1970 (the Unix epoch year) @@ -146,6 +155,11 @@ SELECT uuid_extract_timestamp('017F22E2-79B0-7CC3-98C4-DC0C0C07398F') = 'Tuesday SELECT uuid_extract_timestamp(gen_random_uuid()); -- null SELECT uuid_extract_timestamp('11111111-1111-1111-1111-111111111111'); -- null +-- casts +SELECT '5b35380a-7143-4912-9b55-f322699c6770'::uuid::bytea; +SELECT '\x019a2f859ced7225b99d9c55044a2563'::bytea::uuid; +SELECT '\x1234567890abcdef'::bytea::uuid; -- error +SELECT v = v::bytea::uuid as matched FROM gen_random_uuid() v; -- clean up DROP TABLE guid1, guid2, guid3 CASCADE; diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql index a72bdb5b619d9..247b8e23b2357 100644 --- a/src/test/regress/sql/vacuum.sql +++ b/src/test/regress/sql/vacuum.sql @@ -495,3 +495,33 @@ RESET ROLE; DROP TABLE vacowned; DROP TABLE vacowned_parted; DROP ROLE regress_vacuum; + +-- Test checking how new toast values are allocated on rewrite. +-- Create table with plain storage (forces inline storage initially). +CREATE TABLE vac_rewrite_toast (id int, f1 TEXT STORAGE plain); +-- Insert tuple large enough to trigger toast storage on rewrite, still +-- small enough to fit on a page. +INSERT INTO vac_rewrite_toast values (1, repeat('a', 7000)); +-- Switch to external storage to force toast table usage. +ALTER TABLE vac_rewrite_toast ALTER COLUMN f1 SET STORAGE EXTERNAL; +-- This second tuple is toasted, its value should still be the +-- same after rewrite. +INSERT INTO vac_rewrite_toast values (2, repeat('a', 7000)); +SELECT pg_column_toast_chunk_id(f1) AS id_2_chunk FROM vac_rewrite_toast + WHERE id = 2 \gset +-- Check initial state of the data. +SELECT id, pg_column_toast_chunk_id(f1) IS NULL AS f1_chunk_null, + substr(f1, 5, 10) AS f1_data, + pg_column_compression(f1) AS f1_comp + FROM vac_rewrite_toast ORDER BY id; +-- VACUUM FULL forces toast data rewrite. +VACUUM FULL vac_rewrite_toast; +-- Check after rewrite. +SELECT id, pg_column_toast_chunk_id(f1) IS NULL AS f1_chunk_null, + substr(f1, 5, 10) AS f1_data, + pg_column_compression(f1) AS f1_comp + FROM vac_rewrite_toast ORDER BY id; +-- The same value is reused for the tuple toasted before the rewrite. +SELECT pg_column_toast_chunk_id(f1) = :'id_2_chunk' AS same_chunk + FROM vac_rewrite_toast WHERE id = 2; +DROP TABLE vac_rewrite_toast; diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index 02f105f070e8b..305549b104d20 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -330,6 +330,32 @@ CREATE TEMP VIEW v_window AS SELECT pg_get_viewdef('v_window'); +-- test overflow frame specifications +SELECT sum(unique1) over (rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (rows between 9223372036854775807 following and 1 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between current row and 9223372036854775807 following exclude current row), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +-- These test GROUPS mode with an offset large enough to cause overflow when +-- added to currentgroup. Although the overflow doesn't produce visibly wrong +-- results (due to the incremental nature of group pointer advancement), we +-- still need to protect against it as signed integer overflow is undefined +-- behavior in C. +SELECT sum(unique1) over (ORDER BY four groups between current row and 9223372036854775807 following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT sum(unique1) over (ORDER BY four groups between 9223372036854775807 following and unbounded following), + unique1, four +FROM tenk1 WHERE unique1 < 10; + -- RANGE offset PRECEDING/FOLLOWING tests SELECT sum(unique1) over (order by four range between 2::int8 preceding and 1::int2 preceding), @@ -1522,6 +1548,19 @@ SELECT * FROM FROM empsalary) emp WHERE first_emp = 1 OR last_emp = 1; +CREATE INDEX empsalary_salary_empno_idx ON empsalary (salary, empno); + +SET enable_seqscan = 0; + +-- Ensure no sorting is done and that the IndexScan maintains all pathkeys +-- useful for the final sort order. +EXPLAIN (COSTS OFF) +SELECT salary, empno, row_number() OVER (ORDER BY salary) rn +FROM empsalary +ORDER BY salary, empno; + +RESET enable_seqscan; + -- cleanup DROP TABLE empsalary; @@ -1958,3 +1997,165 @@ $$ LANGUAGE SQL STABLE; EXPLAIN (costs off) SELECT * FROM pg_temp.f(2); SELECT * FROM pg_temp.f(2); + +-- IGNORE NULLS tests + +CREATE TEMPORARY TABLE planets ( + name text, + distance text, + orbit integer +); + +INSERT INTO planets VALUES + ('mercury', 'close', 88), + ('venus', 'close', 224), + ('earth', 'close', NULL), + ('mars', 'close', NULL), + ('jupiter', 'close', 4332), + ('saturn', 'far', 24491), + ('uranus', 'far', NULL), + ('neptune', 'far', 60182), + ('pluto', 'far', 90560), + ('xyzzy', 'far', NULL); + +-- test ruleutils +CREATE VIEW planets_view AS +SELECT name, + orbit, + lag(orbit) OVER w AS lag, + lag(orbit) RESPECT NULLS OVER w AS lag_respect, + lag(orbit) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name) +; +SELECT pg_get_viewdef('planets_view'); + +-- lag +SELECT name, + orbit, + lag(orbit) OVER w AS lag, + lag(orbit) RESPECT NULLS OVER w AS lag_respect, + lag(orbit) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name) +; + +-- lead +SELECT name, + orbit, + lead(orbit) OVER w AS lead, + lead(orbit) RESPECT NULLS OVER w AS lead_respect, + lead(orbit) IGNORE NULLS OVER w AS lead_ignore +FROM planets +WINDOW w AS (ORDER BY name) +; + +-- first_value +SELECT name, + orbit, + first_value(orbit) RESPECT NULLS OVER w1, + first_value(orbit) IGNORE NULLS OVER w1, + first_value(orbit) RESPECT NULLS OVER w2, + first_value(orbit) IGNORE NULLS OVER w2 +FROM planets +WINDOW w1 AS (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), + w2 AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + +-- nth_value +SELECT name, + orbit, + nth_value(orbit, 2) RESPECT NULLS OVER w1, + nth_value(orbit, 2) IGNORE NULLS OVER w1, + nth_value(orbit, 2) RESPECT NULLS OVER w2, + nth_value(orbit, 2) IGNORE NULLS OVER w2 +FROM planets +WINDOW w1 AS (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), + w2 AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + +-- last_value +SELECT name, + orbit, + last_value(orbit) RESPECT NULLS OVER w1, + last_value(orbit) IGNORE NULLS OVER w1, + last_value(orbit) RESPECT NULLS OVER w2, + last_value(orbit) IGNORE NULLS OVER w2 +FROM planets +WINDOW w1 AS (ORDER BY name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), + w2 AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + +-- exclude current row +SELECT name, + orbit, + first_value(orbit) IGNORE NULLS OVER w, + last_value(orbit) IGNORE NULLS OVER w, + nth_value(orbit, 2) IGNORE NULLS OVER w, + lead(orbit, 1) IGNORE NULLS OVER w AS lead_ignore, + lag(orbit, 1) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW) +; + +-- valid and invalid functions +SELECT sum(orbit) OVER () FROM planets; -- succeeds +SELECT sum(orbit) RESPECT NULLS OVER () FROM planets; -- fails +SELECT sum(orbit) IGNORE NULLS OVER () FROM planets; -- fails +SELECT row_number() OVER () FROM planets; -- succeeds +SELECT row_number() RESPECT NULLS OVER () FROM planets; -- fails +SELECT row_number() IGNORE NULLS OVER () FROM planets; -- fails +SELECT rank() OVER () FROM planets; -- succeeds +SELECT rank() RESPECT NULLS OVER () FROM planets; -- fails +SELECT rank() IGNORE NULLS OVER () FROM planets; -- fails +SELECT dense_rank() OVER () FROM planets; -- succeeds +SELECT dense_rank() RESPECT NULLS OVER () FROM planets; -- fails +SELECT dense_rank() IGNORE NULLS OVER () FROM planets; -- fails +SELECT percent_rank() OVER () FROM planets; -- succeeds +SELECT percent_rank() RESPECT NULLS OVER () FROM planets; -- fails +SELECT percent_rank() IGNORE NULLS OVER () FROM planets; -- fails +SELECT cume_dist() OVER () FROM planets; -- succeeds +SELECT cume_dist() RESPECT NULLS OVER () FROM planets; -- fails +SELECT cume_dist() IGNORE NULLS OVER () FROM planets; -- fails +SELECT ntile(1) OVER () FROM planets; -- succeeds +SELECT ntile(1) RESPECT NULLS OVER () FROM planets; -- fails +SELECT ntile(1) IGNORE NULLS OVER () FROM planets; -- fails + +-- test two consecutive nulls +update planets set orbit=null where name='jupiter'; +SELECT name, + orbit, + first_value(orbit) IGNORE NULLS OVER w, + last_value(orbit) IGNORE NULLS OVER w, + nth_value(orbit, 2) IGNORE NULLS OVER w, + lead(orbit, 1) IGNORE NULLS OVER w AS lead_ignore, + lag(orbit, 1) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + +-- test partitions +SELECT name, + distance, + orbit, + first_value(orbit) IGNORE NULLS OVER w, + last_value(orbit) IGNORE NULLS OVER w, + nth_value(orbit, 2) IGNORE NULLS OVER w, + lead(orbit, 1) IGNORE NULLS OVER w AS lead_ignore, + lag(orbit, 1) IGNORE NULLS OVER w AS lag_ignore +FROM planets +WINDOW w AS (PARTITION BY distance ORDER BY name ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) +; + +-- nth_value without nulls +SELECT x, + nth_value(x,2) IGNORE NULLS OVER w +FROM generate_series(1,5) g(x) +WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW); +SELECT x, + nth_value(x,2) IGNORE NULLS OVER w +FROM generate_series(1,5) g(x) +WINDOW w AS (ORDER BY x ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING); + +--cleanup +DROP TABLE planets CASCADE; diff --git a/src/test/regress/sql/with.sql b/src/test/regress/sql/with.sql index b1cae1612904b..2255befda8961 100644 --- a/src/test/regress/sql/with.sql +++ b/src/test/regress/sql/with.sql @@ -1097,6 +1097,30 @@ select ( with cte(foo) as ( values(f1) ) values((select foo from cte)) ) from int4_tbl; +-- +-- test for bug #19055: interaction of WITH with aggregates +-- +-- For now, we just throw an error if there's a use of a CTE below the +-- semantic level that the SQL standard assigns to the aggregate. +-- It's not entirely clear what we could do instead that doesn't risk +-- breaking more things than it fixes. +select f1, (with cte1(x,y) as (select 1,2) + select count((select i4.f1 from cte1))) as ss +from int4_tbl i4; + +-- +-- test for bug #19106: interaction of WITH with aggregates +-- +-- the initial fix for #19055 was too aggressive and broke this case +explain (verbose, costs off) +with a as ( select id from (values (1), (2)) as v(id) ), + b as ( select max((select sum(id) from a)) as agg ) +select agg from b; + +with a as ( select id from (values (1), (2)) as v(id) ), + b as ( select max((select sum(id) from a)) as agg ) +select agg from b; + -- -- test for nested-recursive-WITH bug -- @@ -1336,6 +1360,29 @@ COMMIT; SELECT * FROM bug6051_3; +-- check that recursive CTE processing doesn't rewrite a CTE more than once +-- (must not try to expand GENERATED ALWAYS IDENTITY columns more than once) +CREATE TEMP TABLE id_alw1 (i int GENERATED ALWAYS AS IDENTITY); + +CREATE TEMP TABLE id_alw2 (i int GENERATED ALWAYS AS IDENTITY); +CREATE TEMP VIEW id_alw2_view AS SELECT * FROM id_alw2; + +CREATE TEMP TABLE id_alw3 (i int GENERATED ALWAYS AS IDENTITY); +CREATE RULE id_alw3_ins AS ON INSERT TO id_alw3 DO INSTEAD + WITH t1 AS (INSERT INTO id_alw1 DEFAULT VALUES RETURNING i) + INSERT INTO id_alw2_view DEFAULT VALUES RETURNING i; +CREATE TEMP VIEW id_alw3_view AS SELECT * FROM id_alw3; + +CREATE TEMP TABLE id_alw4 (i int GENERATED ALWAYS AS IDENTITY); + +WITH t4 AS (INSERT INTO id_alw4 DEFAULT VALUES RETURNING i) + INSERT INTO id_alw3_view DEFAULT VALUES RETURNING i; + +SELECT * from id_alw1; +SELECT * from id_alw2; +SELECT * from id_alw3; +SELECT * from id_alw4; + -- check case where CTE reference is removed due to optimization EXPLAIN (VERBOSE, COSTS OFF) SELECT q1 FROM diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql index 4aaca242bbece..4833b8ac5f07b 100644 --- a/src/test/regress/sql/without_overlaps.sql +++ b/src/test/regress/sql/without_overlaps.sql @@ -2,7 +2,7 @@ -- -- We leave behind several tables to test pg_dump etc: -- temporal_rng, temporal_rng2, --- temporal_fk_rng2rng. +-- temporal_fk_rng2rng, temporal_fk2_rng2rng. SET datestyle TO ISO, YMD; @@ -180,6 +180,37 @@ ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; DROP TABLE temporal_rng3; DROP TYPE textrange2; +-- +-- test PRIMARY KEY and UNIQUE constraints' interaction with domains +-- + +-- range over domain: +CREATE DOMAIN int4_d as integer check (value <> 10); +CREATE TYPE int4_d_range as range (subtype = int4_d); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4_d_range, + CONSTRAINT temporal_rng4_pk PRIMARY KEY(id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); -- start bound violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- end bound violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,5)'); -- overlaps +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[30,)'); -- null bound is okay +DROP TABLE temporal_rng4; + +-- domain over range: +CREATE DOMAIN int4range_d AS int4range CHECK (VALUE <> '[10,11)'); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4range_d, + CONSTRAINT temporal_rng4_pk UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,13)'); -- overlaps +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +DROP TABLE temporal_rng4; + -- -- test ALTER TABLE ADD CONSTRAINT -- @@ -632,6 +663,20 @@ INSERT INTO temporal3 (id, valid_at, id2, name) ('[1,2)', daterange('2000-01-01', '2010-01-01'), '[7,8)', 'foo'), ('[2,3)', daterange('2000-01-01', '2010-01-01'), '[9,10)', 'bar') ; +UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-05-01' TO '2000-07-01' + SET name = name || '1'; +UPDATE temporal3 FOR PORTION OF valid_at FROM '2000-04-01' TO '2000-06-01' + SET name = name || '2' + WHERE id = '[2,3)'; +SELECT * FROM temporal3 ORDER BY id, valid_at; +-- conflicting id only: +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[1,2)', daterange('2005-01-01', '2006-01-01'), '[8,9)', 'foo3'); +-- conflicting id2 only: +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[3,4)', daterange('2005-01-01', '2010-01-01'), '[9,10)', 'bar3'); DROP TABLE temporal3; -- @@ -659,7 +704,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); @@ -667,9 +712,23 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'), ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'); -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; -SELECT * FROM tp1 ORDER BY id, valid_at; -SELECT * FROM tp2 ORDER BY id, valid_at; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + SET name = 'one2' + WHERE id = '[1,2)'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25' + SET id = '[4,5)' + WHERE name = 'one'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET id = '[2,3)' + WHERE name = 'three'; +DELETE FROM temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + WHERE id = '[3,4)'; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; DROP TABLE temporal_partitioned; -- temporal UNIQUE: @@ -677,7 +736,7 @@ CREATE TABLE temporal_partitioned ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,2)', '[2,3)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,4)', '[4,5)'); @@ -685,9 +744,23 @@ INSERT INTO temporal_partitioned (id, valid_at, name) VALUES ('[1,2)', daterange('2000-01-01', '2000-02-01'), 'one'), ('[1,2)', daterange('2000-02-01', '2000-03-01'), 'one'), ('[3,4)', daterange('2000-01-01', '2010-01-01'), 'three'); -SELECT * FROM temporal_partitioned ORDER BY id, valid_at; -SELECT * FROM tp1 ORDER BY id, valid_at; -SELECT * FROM tp2 ORDER BY id, valid_at; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + SET name = 'one2' + WHERE id = '[1,2)'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2000-02-20' TO '2000-02-25' + SET id = '[4,5)' + WHERE name = 'one'; +UPDATE temporal_partitioned + FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' + SET id = '[2,3)' + WHERE name = 'three'; +DELETE FROM temporal_partitioned + FOR PORTION OF valid_at FROM '2000-01-15' TO '2000-02-15' + WHERE id = '[3,4)'; +SELECT tableoid::regclass, * FROM temporal_partitioned ORDER BY id, valid_at; DROP TABLE temporal_partitioned; -- ALTER TABLE REPLICA IDENTITY @@ -1291,6 +1364,18 @@ COMMIT; -- changing the scalar part fails: UPDATE temporal_rng SET id = '[7,8)' WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +-- changing an unreferenced part is okay: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' + SET id = '[7,8)' + WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; -- then delete the objecting FK record and the same PK update succeeds: DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') @@ -1338,6 +1423,18 @@ BEGIN; DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); COMMIT; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; +-- deleting just a part fails: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; -- then delete the objecting FK record and the same PK delete succeeds: DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); @@ -1356,12 +1453,13 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE RESTRICT; -- --- test ON UPDATE/DELETE options +-- rng2rng test ON UPDATE/DELETE options -- -- test FK referenced updates CASCADE +TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[4,5)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1369,8 +1467,9 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE CASCADE ON UPDATE CASCADE; -- test FK referenced updates SET NULL -INSERT INTO temporal_rng (id, valid_at) VALUES ('[9,10)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01'), '[9,10)'); +TRUNCATE temporal_rng, temporal_fk_rng2rng; +INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1378,9 +1477,10 @@ ALTER TABLE temporal_fk_rng2rng ON DELETE SET NULL ON UPDATE SET NULL; -- test FK referenced updates SET DEFAULT +TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null)); -INSERT INTO temporal_rng (id, valid_at) VALUES ('[12,13)', daterange('2018-01-01', '2021-01-01')); -INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[8,9)', daterange('2018-01-01', '2021-01-01'), '[12,13)'); +INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', ADD CONSTRAINT temporal_fk_rng2rng_fk @@ -1716,6 +1816,20 @@ BEGIN; WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); COMMIT; -- changing the scalar part fails: +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- changing an unreferenced part is okay: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; UPDATE temporal_mltrng SET id = '[7,8)' WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); @@ -1760,6 +1874,17 @@ BEGIN; DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); COMMIT; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) +WHERE id = '[5,6)'; +-- deleting just a part fails: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) +WHERE id = '[5,6)'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- -- FK between partitioned tables: ranges @@ -1769,7 +1894,7 @@ CREATE TABLE temporal_partitioned_rng ( id int4range, valid_at daterange, name text, - CONSTRAINT temporal_paritioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 partition OF temporal_partitioned_rng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)'); CREATE TABLE tp2 partition OF temporal_partitioned_rng FOR VALUES IN ('[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)'); @@ -1899,7 +2024,7 @@ CREATE TABLE temporal_partitioned_mltrng ( id int4range, valid_at datemultirange, name text, - CONSTRAINT temporal_paritioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + CONSTRAINT temporal_partitioned_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) ) PARTITION BY LIST (id); CREATE TABLE tp1 PARTITION OF temporal_partitioned_mltrng FOR VALUES IN ('[1,2)', '[3,4)', '[5,6)', '[7,8)', '[9,10)', '[11,12)', '[13,14)', '[15,16)', '[17,18)', '[19,20)', '[21,22)', '[23,24)'); CREATE TABLE tp2 PARTITION OF temporal_partitioned_mltrng FOR VALUES IN ('[0,1)', '[2,3)', '[4,5)', '[6,7)', '[8,9)', '[10,11)', '[12,13)', '[14,15)', '[16,17)', '[18,19)', '[20,21)', '[22,23)', '[24,25)'); diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql index 0ea4f508837cf..a771a441c36e8 100644 --- a/src/test/regress/sql/xml.sql +++ b/src/test/regress/sql/xml.sql @@ -272,9 +272,7 @@ BEGIN END IF; EXCEPTION -- character with byte sequence 0xc2 0xb0 in encoding "UTF8" has no equivalent in encoding "LATIN8" - WHEN untranslatable_character - -- default conversion function for encoding "UTF8" to "MULE_INTERNAL" does not exist - OR undefined_function + WHEN undefined_function -- unsupported XML feature OR feature_not_supported THEN RAISE LOG 'skip: %', SQLERRM; diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile index e8a1639db2d3d..aa062945fb9f3 100644 --- a/src/test/ssl/Makefile +++ b/src/test/ssl/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/ssl # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/ssl/Makefile diff --git a/src/test/ssl/conf/server-localhost-alt-names.config b/src/test/ssl/conf/server-localhost-alt-names.config new file mode 100644 index 0000000000000..1c41c1ecb7859 --- /dev/null +++ b/src/test/ssl/conf/server-localhost-alt-names.config @@ -0,0 +1,20 @@ +# An OpenSSL format CSR config file for creating a server certificate. +# +# This certificate contains SANs for localhost (DNS, IPv4, and IPv6). + +[ req ] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[ req_distinguished_name ] +OU = PostgreSQL test suite + +# For Subject Alternative Names +[ v3_req ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = localhost +IP.1 = 127.0.0.1 +IP.2 = ::1 diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build index cf8b2b9303a0c..d7e7ce23433ed 100644 --- a/src/test/ssl/meson.build +++ b/src/test/ssl/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'ssl', @@ -7,12 +7,13 @@ tests += { 'tap': { 'env': { 'with_ssl': ssl_library, - 'OPENSSL': openssl.found() ? openssl.path() : '', + 'OPENSSL': openssl.found() ? openssl.full_path() : '', }, 'tests': [ 't/001_ssltests.pl', 't/002_scram.pl', 't/003_sslinfo.pl', + 't/004_sni.pl', ], }, } diff --git a/src/test/ssl/ssl/server-localhost-alt-names.crt b/src/test/ssl/ssl/server-localhost-alt-names.crt new file mode 100644 index 0000000000000..106dc34d7bbbf --- /dev/null +++ b/src/test/ssl/ssl/server-localhost-alt-names.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVjCCAj6gAwIBAgIIICYCJxRTBwAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE +Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl +cnZlciBjZXJ0czAgFw0yNjAyMjcyMjUzMDdaGA8yMDUzMDcxNTIyNTMwN1owIDEe +MBwGA1UECwwVUG9zdGdyZVNRTCB0ZXN0IHN1aXRlMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA3k/aT/OV8sbJrvhtSgz5eNMCuv7RKdUQw+f52DpZTs85 +lTXIRs+l3mXoKRjN1gqzqlHInnJlhxQipqGiJfz4Li8L6jma2yZztFHH+f+YF8Ke +5fCYP1qMxbghqeIRkKgrCEjHUnOhbN5oMi/Ndt9AXWGG/39uk5Xec/Y/J5aZkPVV +blqWYyQQ+4U783lwZs1EUWdfiTVRp8fYADT/2lHjaZaX08vAE5VvCbBv6mPhPfno +F9FIaW+CRuwORisFK8Bd1q/0r5aPZGPi0lokCdaB/cRUHwJK1/HHgyB3N+Lk4swf +z+MfSqj4IaNPW7zn3EV9hgpVwSmB5ES8rzojiGtMDQIDAQABo3AwbjAsBgNVHREE +JTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwHQYDVR0OBBYE +FOZ8KClKVbeYecn8lvAldBXOjQz6MB8GA1UdIwQYMBaAFPKPOmZAUGRIItcugv9W +nsKz7nQKMA0GCSqGSIb3DQEBCwUAA4IBAQDE1FGw20H0Flo3gAGN0ND9G/6wDxWM +MldbXRjqc1E0/+7+Zs6v1jPrNUNEvxy5kHWevUJCIt6y4SYt01JxE4wqEPJ3UBAv +cM0p08mohmN/CHc/lswXx12MZMfaLA1/WRPqvtiGFOrOOPvaRKHO4ORiT1KWmtOO +FgcW9E1Q1iJFK28xdz9NEEBWEurEIr5KGAsCwf9DfQxPJXiS9n98BDI8gPwlse7t +VqyhGVSj+EPbdY2kqkSuPXacdnUGfO6EWo9PFKqhxWMxABLuK0UZzH6/1lMOh1m9 +Mm+gtwO5RLBX22V+KIs1uuDTNcveQ2DsZnMZh7lGD05eHYG9hwnC6GNZ +-----END CERTIFICATE----- diff --git a/src/test/ssl/ssl/server-localhost-alt-names.key b/src/test/ssl/ssl/server-localhost-alt-names.key new file mode 100644 index 0000000000000..4499a11928df0 --- /dev/null +++ b/src/test/ssl/ssl/server-localhost-alt-names.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDeT9pP85Xyxsmu ++G1KDPl40wK6/tEp1RDD5/nYOllOzzmVNchGz6XeZegpGM3WCrOqUciecmWHFCKm +oaIl/PguLwvqOZrbJnO0Ucf5/5gXwp7l8Jg/WozFuCGp4hGQqCsISMdSc6Fs3mgy +L81230BdYYb/f26Tld5z9j8nlpmQ9VVuWpZjJBD7hTvzeXBmzURRZ1+JNVGnx9gA +NP/aUeNplpfTy8ATlW8JsG/qY+E9+egX0Uhpb4JG7A5GKwUrwF3Wr/Svlo9kY+LS +WiQJ1oH9xFQfAkrX8ceDIHc34uTizB/P4x9KqPgho09bvOfcRX2GClXBKYHkRLyv +OiOIa0wNAgMBAAECggEAFchiPkJCV4r12RCbeM2DpjyawGLWcNBhN6jjuLWi6Y9x +d3bRHGsdOAjpMhmtlYLv7sjbrPbNjupAqO4eerVqRfAzLSyeyUlfvfPjcdIC/5UA +x8wGxvJi576ugbxWd0ObD9E9woz07LtwHzbC3ZprbprvRNqiJZDiPp+KuaDOhD7u +6XAM8JilFqfiDN8+xbH2dWdVkdt2OD5wctJbqy6moH9VFVsWsMQr3/vJkSdUPLxa +8ATUubFhO/sqE+KsMZESq5W1Xbj3NwMkvnA92yG9+ED60NPjFzgheZZWSmXe1B/c +XB3G/upvCoHEgKbrnYt05b/ryUbXAZkvi5oL4fp9OwKBgQD4d+Qm4GiKEWvjZ5II +ROfHEyoWOHw9z8ydJIrtOL8ICh5RH8D/v2IaMAacWV5eLoJ7aYC6yIYuWdHQljAi +zltNFrsLFmWXLy91IWfUzIGnFLWeqOmI50vlM8xU54rD/cZ3qtvr2Qk9HHs0dsyB +6cGRf0BPJi04aAEqSZqc8HCXAwKBgQDlDP0MW57bHpqQROQDLIgEX9/rzUNo48Z/ +1f27bCkKP+CpizE9eWvGs5rQmUxCNzWULFxIuBbgsubuVP7jO3piY6bRGnvSE6nD +mW0V1mSypVO22Ci/Q8ekkY2+0ZVp3qLPO/cwtI/Ye8kp4xu41I2XgJE8Mo0hEEyJ +N1/1vUJbrwKBgBp3gukVPG2An5JwpOCWnm3ZP8FwMOPQr8YJb3cHdWng0gvoKwHT +HBsYBIxBBMlZgPKucVT0KT7kuHHUnboHazhR9Iig0R+CmjaK4WmMgz8N+K625XF8 +2dvHYbulkmWAMdTrcVO1IcPNtd4HzY8FHGZoPKxxr51zjrQ3dO3EuumLAoGATho2 +sx8OtPLji2wiP77QhoVWqmYspTh9+Bs00NLZz6fmaImQ+cBMcs3NbXHIYg/HUkYq +FZXIH0iBnCUZYMxoN+J5AHZCYGjaC1tmqfqYDZ54RDHC+y0Wh1QmfDmk9Bu5cmal +LFN1dUEIYCMT0duQiGeLnnYyT2LqZiOesgGd/fsCgYEA2GbKteq+io6HAEt2/yry +xZGaRR8Twg0B8XtD9NHCbgizmZiD/mADgyhkgjUsDIkcMzEt+sA4IK9ORgIYqS+/ +q2eY1QRKpoZgJJfE8dU88B35YGqdZuXENR4I7w+JrKCCCk5jSiwylvsBsi1HX8Qu +EdQBBRiwkRnxQ83hqRI3ymw= +-----END PRIVATE KEY----- diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk index 23aaad0c76671..f32c53a76a138 100644 --- a/src/test/ssl/sslfiles.mk +++ b/src/test/ssl/sslfiles.mk @@ -9,7 +9,7 @@ # The main Makefile in this directory defers to this helper file when # building the sslfiles-related targets. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/ssl/sslfiles.mk @@ -31,6 +31,7 @@ SERVERS := server-cn-and-alt-names \ server-ip-in-dnsname \ server-single-alt-name \ server-multiple-alt-names \ + server-localhost-alt-names \ server-no-names \ server-revoked CLIENTS := client client-dn client-revoked client_ext client-long \ diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 2cb4d0ffd4199..0af887caa639a 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -51,8 +51,15 @@ sub switch_server_cert my $supports_sslcertmode_require = check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1"); +# Set of default settings for SSL parameters in connection string. This +# makes the tests protected against any defaults the environment may have +# in ~/.postgresql/. +my $default_ssl_connstr = + "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid"; + # Allocation of base connection string shared among multiple tests. -my $common_connstr; +my $common_connstr = + "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test"; #### Set up the server. @@ -72,11 +79,16 @@ sub switch_server_cert my $result = $node->safe_psql('postgres', "SHOW ssl_library"); is($result, $ssl_server->ssl_library(), 'ssl_library parameter'); +my $exec_backend = $node->safe_psql('postgres', 'SHOW debug_exec_backend'); +chomp($exec_backend); + $ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR, 'trust'); note "testing password-protected keys"; +# Test a passphrase command which fails to unlock the private key, the server +# should not start at all. switch_server_cert( $node, certfile => 'server-cn-only', @@ -85,20 +97,83 @@ sub switch_server_cert passphrase_cmd => 'echo wrongpassword', restart => 'no'); -$result = $node->restart(fail_ok => 1); +$result = $node->restart( + fail_ok => 1, + log_like => qr/could not load private key file/); is($result, 0, 'restart fails with password-protected key file with wrong password'); +# Test a passphrase command which successfully unlocks the private key but +# which doesn't support reloading. Unlocking the private key will fail when +# reloading and the already existing SSL context will remain in place, with +# connections still accepted. EXEC_BACKEND builds will reload the SSL context +# on each backend startup, so command reloading must be enabled or else +# connections will fail. switch_server_cert( $node, certfile => 'server-cn-only', cafile => 'root+client_ca', keyfile => 'server-password', passphrase_cmd => 'echo secret1', + passphrase_cmd_reload => 'off', restart => 'no'); -$result = $node->restart(fail_ok => 1); +$result = $node->restart( + fail_ok => 1, + log_unlike => qr/could not load private key file/); +is($result, 1, 'restart succeeds with password-protected key file'); + +if ($exec_backend =~ /on/) +{ + $node->connect_fails( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require", + expected_stderr => qr/\Qserver does not support SSL\E/); +} +else +{ + $node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); +} + +# Reloading should fail since we cannot execute the passphrase command +$node->reload(); +my $log_start = $node->wait_for_log( + qr/cannot be reloaded because it requires a passphrase/); + +# Test a passphrase command which successfully unlocks the private key, and +# which can be reloaded. The server should start and connections be accepted. +switch_server_cert( + $node, + certfile => 'server-cn-only', + cafile => 'root+client_ca', + keyfile => 'server-password', + passphrase_cmd => 'echo secret1', + passphrase_cmd_reload => 'on', + restart => 'no'); + +$result = $node->restart( + fail_ok => 1, + log_unlike => qr/could not load private key file/); is($result, 1, 'restart succeeds with password-protected key file'); +$node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); + +# Reloading the config should execute the passphrase reload command and +# successfully reload the private key. +$node->reload(); +$log_start = + $node->wait_for_log(qr/reloading configuration files/, $log_start); +$node->log_check( + "passphrase could reload private key", + $log_start, + log_unlike => [ qr/cannot be reloaded because it requires a passphrase/, ] +); +$node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); # Test compatibility of SSL protocols. # TLSv1.1 is lower than TLSv1.2, so it won't work. @@ -139,15 +214,6 @@ sub switch_server_cert switch_server_cert($node, certfile => 'server-cn-only'); -# Set of default settings for SSL parameters in connection string. This -# makes the tests protected against any defaults the environment may have -# in ~/.postgresql/. -my $default_ssl_connstr = - "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid"; - -$common_connstr = - "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test"; - SKIP: { skip "Keylogging is not supported with LibreSSL", 5 if $libressl; @@ -173,6 +239,13 @@ sub switch_server_cert ok( (@status = stat("$tempdir/key.txt")), "keylog file exists and returned status"); ok(@status && !($status[2] & 0006), "keylog file is not world readable"); + + # Connect should work with an incorrect sslkeylogfile, with the error to + # open the logfile printed to stderr + $node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslkeylogfile=$tempdir/invalid/key.txt sslmode=require", + "connect with server root cert and incorrect sslkeylogfile path", + expected_stderr => qr/could not open/); } # The server should not accept non-SSL connections. @@ -307,11 +380,11 @@ sub switch_server_cert $common_connstr = "$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full"; -$node->connect_ok("$common_connstr host=192.0.2.1", +$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0", "IP address in the Common Name"); $node->connect_fails( - "$common_connstr host=192.000.002.001", + "$common_connstr host=192.000.002.001 sslsni=0", "mismatch between host name and server certificate IP address", expected_stderr => qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/ @@ -321,7 +394,7 @@ sub switch_server_cert # long-standing behavior.) switch_server_cert($node, certfile => 'server-ip-in-dnsname'); -$node->connect_ok("$common_connstr host=192.0.2.1", +$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0", "IP address in a dNSName"); # Test Subject Alternative Names. @@ -741,33 +814,29 @@ sub switch_server_cert # pg_stat_ssl -my $serialno = `$ENV{OPENSSL} x509 -serial -noout -in ssl/client.crt`; -if ($? == 0) -{ - # OpenSSL prints serial numbers in hexadecimal and converting the serial - # from hex requires a 64-bit capable Perl as the serialnumber is based on - # the current timestamp. On 32-bit fall back to checking for it being an - # integer like how we do when grabbing the serial fails. - if ($Config{ivsize} == 8) - { - no warnings qw(portable); +# If the openssl program isn't available, or fails to run, fall back to a +# generic integer match rather than skipping the test. +my $serialno = '\d+'; - $serialno =~ s/^serial=//; - $serialno =~ s/\s+//g; - $serialno = hex($serialno); - } - else +if ($ENV{OPENSSL} ne '') +{ + my $serialstr = `$ENV{OPENSSL} x509 -serial -noout -in ssl/client.crt`; + if ($? == 0) { - $serialno = '\d+'; + # OpenSSL prints serial numbers in hexadecimal and converting the serial + # from hex requires a 64-bit capable Perl as the serialnumber is based on + # the current timestamp. On 32-bit fall back to checking for it being an + # integer like how we do when grabbing the serial fails. + if ($Config{ivsize} == 8) + { + no warnings qw(portable); + + $serialstr =~ s/^serial=//; + $serialstr =~ s/\s+//g; + $serialno = hex($serialstr); + } } } -else -{ - # OpenSSL isn't functioning on the user's PATH. This probably isn't worth - # skipping the test over, so just fall back to a generic integer match. - warn "couldn't run \"$ENV{OPENSSL} x509\" to get client cert serialno"; - $serialno = '\d+'; -} command_like( [ @@ -935,4 +1004,60 @@ sub switch_server_cert qr{Failed certificate data \(unverified\): subject "/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82", serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test client certs"}, ]); +SKIP: +{ + skip "sslmode require not supported in this build", 4 + unless ($supports_sslcertmode_require); + + # Test client CAs + my $connstr = + "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1"; + + switch_server_cert($node, certfile => 'server-cn-only', cafile => ''); + # example.org is unconfigured and should fail. + $node->connect_fails( + "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt" + . sslkey('client.key'), + "host: 'example.org', ca: '': connect with sslcert, no client CA configured", + expected_stderr => + qr/client certificates can only be checked if a root certificate store is available/ + ); + + # example.com uses the client CA. + switch_server_cert( + $node, + certfile => 'server-cn-only', + cafile => 'root+client_ca'); + # example.com is configured and should require a valid client cert. + $node->connect_fails( + "$connstr host=example.com sslcertmode=disable", + "host: 'example.com', ca: 'root+client_ca.crt': connect fails if no client certificate sent", + expected_stderr => qr/connection requires a valid client certificate/ + ); + $node->connect_ok( + "$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt " + . sslkey('client.key'), + "host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent" + ); + + # example.net uses the server CA (which is wrong). + switch_server_cert( + $node, + certfile => 'server-cn-only', + cafile => 'root+server_ca'); + # example.net is configured and should require a client cert, but will + # always fail verification. + $node->connect_fails( + "$connstr host=example.net sslcertmode=disable", + "host: 'example.net', ca: 'root+server_ca.crt': connect fails if no client certificate sent", + expected_stderr => qr/connection requires a valid client certificate/ + ); + + $node->connect_fails( + "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt " + . sslkey('client.key'), + "host: 'example.net', ca: 'root+server_ca.crt': connect with sslcert, client certificate sent", + expected_stderr => qr/unknown ca/); +} + done_testing(); diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl index 60b60b286570b..88c82a4ac1132 100644 --- a/src/test/ssl/t/002_scram.pl +++ b/src/test/ssl/t/002_scram.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test SCRAM authentication and TLS channel binding types diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl index 3c756489cdf5e..56d32cbaa31ee 100644 --- a/src/test/ssl/t/003_sslinfo.pl +++ b/src/test/ssl/t/003_sslinfo.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl new file mode 100644 index 0000000000000..ae4b1a4c803c5 --- /dev/null +++ b/src/test/ssl/t/004_sni.pl @@ -0,0 +1,457 @@ + +# Copyright (c) 2024, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use SSL::Server; + +# This is the hostaddr used to connect to the server. This cannot be a +# hostname, because the server certificate is always for the domain +# postgresql-ssl-regression.test. +my $SERVERHOSTADDR = '127.0.0.1'; +# This is the pattern to use in pg_hba.conf to match incoming connections. +my $SERVERHOSTCIDR = '127.0.0.1/32'; + +if ($ENV{with_ssl} ne 'openssl') +{ + plan skip_all => 'OpenSSL not supported by this build'; +} + +if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/) +{ + plan skip_all => + 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA'; +} + +my $ssl_server = SSL::Server->new(); + +if ($ssl_server->is_libressl) +{ + plan skip_all => 'SNI not supported when building with LibreSSL'; +} + +my $node = PostgreSQL::Test::Cluster->new('primary'); +$node->init; + +# PGHOST is enforced here to set up the node, subsequent connections +# will use a dedicated connection string. +$ENV{PGHOST} = $node->host; +$ENV{PGPORT} = $node->port; +$node->start; + +my $exec_backend = $node->safe_psql('postgres', 'SHOW debug_exec_backend'); +chomp($exec_backend); + +$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR, + $SERVERHOSTCIDR, 'trust'); + +$ssl_server->switch_server_cert($node, certfile => 'server-cn-only'); + +my $connstr = + "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR sslsni=1"; + +############################################################################## +# postgresql.conf +############################################################################## + +# Connect without any hosts configured in pg_hosts.conf, thus using the cert +# and key in postgresql.conf. pg_hosts.conf exists at this point but is empty +# apart from the comments stemming from the sample. +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "pg.conf: connect with correct server CA cert file sslmode=require"); + +$node->connect_fails( + "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg.conf: connect fails without intermediate for sslmode=verify-ca", + expected_stderr => qr/certificate verify failed/); + +# Add an entry in pg_hosts.conf with no default, and reload. Since ssl_sni is +# still 'off' we should still be able to connect using the certificates in +# postgresql.conf +$node->append_conf('pg_hosts.conf', + "example.org server-cn-only.crt server-cn-only.key"); +$node->reload; +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "pg.conf: connect with correct server CA cert file sslmode=require"); + +# Turn on SNI support and remove pg_hosts.conf and reload to make sure a +# missing file is treated like an empty file. +$node->append_conf('postgresql.conf', 'ssl_sni = on'); +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->reload; + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "pg.conf: connect after deleting pg_hosts.conf"); + +############################################################################## +# pg_hosts.conf +############################################################################## + +# Replicate the postgresql.conf configuration into pg_hosts.conf and retry the +# same tests as above. +$node->append_conf('pg_hosts.conf', + "* server-cn-only.crt server-cn-only.key"); +$node->reload; + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "pg_hosts.conf: connect to default, with correct server CA cert file sslmode=require" +); + +$node->connect_fails( + "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to default, fail without intermediate for sslmode=verify-ca", + expected_stderr => qr/certificate verify failed/); + +# Add host entry for example.org which serves the server cert and its +# intermediate CA. The previously existing default host still exists without +# a CA. +$node->append_conf('pg_hosts.conf', + "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt" +); +$node->reload; + +$node->connect_ok( + "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to example.org and verify server CA"); + +$node->connect_ok( + "$connstr host=Example.ORG sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to Example.ORG and verify server CA"); + +$node->connect_fails( + "$connstr host=example.org sslrootcert=invalid sslmode=verify-ca", + "pg_hosts.conf: connect to example.org but without server root cert, sslmode=verify-ca", + expected_stderr => qr/root certificate file "invalid" does not exist/); + +$node->connect_fails( + "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to default and fail to verify CA", + expected_stderr => qr/certificate verify failed/); + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "pg_hosts.conf: connect to default with sslmode=require"); + +# Use multiple hostnames for a single configuration +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + "example.org,example.com,example.net server-cn-only+server_ca.crt server-cn-only.key root_ca.crt" +); +$node->reload; + +$node->connect_ok( + "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to example.org and verify server CA"); +$node->connect_ok( + "$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to example.com and verify server CA"); +$node->connect_ok( + "$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "pg_hosts.conf: connect to example.net and verify server CA"); +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.se", + "pg_hosts.conf: connect to default with sslmode=require", + expected_stderr => qr/unrecognized name/); + +# Test @-inclusion of hostnames. +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + 'example.org,@hostnames.txt server-cn-only+server_ca.crt server-cn-only.key root_ca.crt' +); +$node->append_conf( + 'hostnames.txt', qq{ +example.com +example.net +}); +$node->reload; + +$node->connect_ok( + "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + '@hostnames.txt: connect to example.org and verify server CA'); +$node->connect_ok( + "$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + '@hostnames.txt: connect to example.com and verify server CA'); +$node->connect_ok( + "$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + '@hostnames.txt: connect to example.net and verify server CA'); +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.se", + '@hostnames.txt: connect to default with sslmode=require', + expected_stderr => qr/unrecognized name/); + +# Add an incorrect entry specifying a default entry combined with hostnames +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + "example.org,*,example.net server-cn-only+server_ca.crt server-cn-only.key root_ca.crt" +); +my $result = $node->restart(fail_ok => 1); +is($result, 0, + 'pg_hosts.conf: restart fails with default entry combined with hostnames' +); + +# Add incorrect duplicate entries. +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf( + 'pg_hosts.conf', qq{ +* server-cn-only.crt server-cn-only.key +* server-cn-only.crt server-cn-only.key +}); +$result = $node->restart(fail_ok => 1); +is($result, 0, 'pg_hosts.conf: restart fails with two default entries'); + +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf( + 'pg_hosts.conf', qq{ +/no_sni/ server-cn-only.crt server-cn-only.key +/no_sni/ server-cn-only.crt server-cn-only.key +}); +$result = $node->restart(fail_ok => 1); +is($result, 0, 'pg_hosts.conf: restart fails with two no_sni entries'); + +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf( + 'pg_hosts.conf', qq{ +example.org server-cn-only.crt server-cn-only.key +example.net server-cn-only.crt server-cn-only.key +example.org server-cn-only.crt server-cn-only.key +}); +$result = $node->restart(fail_ok => 1); +is($result, 0, + 'pg_hosts.conf: restart fails with two identical hostname entries'); +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf( + 'pg_hosts.conf', qq{ +example.org server-cn-only.crt server-cn-only.key +example.net,example.com,Example.org server-cn-only.crt server-cn-only.key +}); +$result = $node->restart(fail_ok => 1); +is($result, 0, + 'pg_hosts.conf: restart fails with two identical hostname entries in lists' +); + +# Modify pg_hosts.conf to no longer have the default host entry. +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt" +); +$node->restart; + +# Connecting without a hostname as well as with a hostname which isn't in the +# pg_hosts configuration should fail. +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0", + "pg_hosts.conf: connect to default with sslmode=require", + expected_stderr => qr/handshake failure/); +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.com", + "pg_hosts.conf: connect to default with sslmode=require", + expected_stderr => qr/unrecognized name/); +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example", + "pg_hosts.conf: connect to 'example' with sslmode=require", + expected_stderr => qr/unrecognized name/); + +# Reconfigure with broken configuration for the key passphrase, the server +# should not start up +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on' +); +$result = $node->restart(fail_ok => 1); +is($result, 0, + 'pg_hosts.conf: restart fails with password-protected key when using the wrong passphrase command' +); + +# Reconfigure again but with the correct passphrase set +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on' +); +$result = $node->restart(fail_ok => 1); +is($result, 1, + 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command' +); + +# Make sure connecting works, and try to stress the reload logic by issuing +# subsequent reloads +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost", + "pg_hosts.conf: connect with correct server CA cert file sslmode=require" +); +$node->reload; +$node->reload; +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost", + "pg_hosts.conf: connect with correct server CA cert file after reloads"); +$node->reload; +$node->reload; +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost", + "pg_hosts.conf: connect with correct server CA cert file after more reloads" +); + +# Test reloading a passphrase protected key without reloading support in the +# passphrase hook. Restarting should not give any errors in the log, but the +# subsequent reload should fail with an error regarding reloading. +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off' +); +my $node_loglocation = -s $node->logfile; +$result = $node->restart(fail_ok => 1); +is($result, 1, + 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command' +); +my $log = + PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation); +unlike( + $log, + qr/cannot be reloaded because it requires a passphrase/, + 'log reload failure due to passphrase command reloading'); + +SKIP: +{ + # Passphrase reloads must be enabled on Windows (and EXEC_BACKEND) to + # succeed even without a restart + skip "Passphrase command reload required on Windows and EXEC_BACKEND", 1 + if ($windows_os || $exec_backend =~ /on/); + + $node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost", + "pg_hosts.conf: connect with correct server CA cert file sslmode=require" + ); + # Reloading should fail since the passphrase cannot be reloaded, with an + # error recorded in the log. Since we keep existing contexts around it + # should still work. + $node_loglocation = -s $node->logfile; + $node->reload; + $node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost", + "pg_hosts.conf: connect with correct server CA cert file sslmode=require" + ); + $log = + PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation); + like( + $log, + qr/cannot be reloaded because it requires a passphrase/, + 'log reload failure due to passphrase command reloading'); +} + +# Configure with only non-SNI connections allowed +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + "/no_sni/ server-cn-only.crt server-cn-only.key"); +$node->restart; + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0", + "pg_hosts.conf: only non-SNI connections allowed"); + +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.org", + "pg_hosts.conf: only non-SNI connections allowed, connecting with SNI", + expected_stderr => qr/unrecognized name/); + +# Test client CAs + +# pg_hosts configuration +ok(unlink($node->data_dir . '/pg_hosts.conf')); + +# Neither ssl_ca_file nor the default host should have any effect whatsoever on +# the following tests. +$node->append_conf('postgresql.conf', "ssl_ca_file = 'root+client_ca.crt'"); +$node->append_conf('pg_hosts.conf', + '* server-cn-only.crt server-cn-only.key root+client_ca.crt'); + +# example.org has an unconfigured CA. +$node->append_conf('pg_hosts.conf', + 'example.org server-cn-only.crt server-cn-only.key'); +# example.com uses the client CA. +$node->append_conf('pg_hosts.conf', + 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt'); +# example.net uses the server CA (which is wrong). +$node->append_conf('pg_hosts.conf', + 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt'); + +$node->restart; + +$connstr = + "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1"; + +# example.org is unconfigured and should fail. +$node->connect_fails( + "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt" + . $ssl_server->sslkey('client.key'), + "host: 'example.org', ca: '': connect with sslcert, no client CA configured", + expected_stderr => + qr/client certificates can only be checked if a root certificate store is available/ +); + +# example.com is configured and should require a valid client cert. +$node->connect_fails( + "$connstr host=example.com sslcertmode=disable", + "host: 'example.com', ca: 'root+client_ca.crt': connect fails if no client certificate sent", + expected_stderr => qr/connection requires a valid client certificate/); + +$node->connect_ok( + "$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt " + . $ssl_server->sslkey('client.key'), + "host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent" +); + +# example.net is configured and should require a client cert, but will +# always fail verification. +$node->connect_fails( + "$connstr host=example.net sslcertmode=disable", + "host: 'example.net', ca: 'root+server_ca.crt': connect fails if no client certificate sent", + expected_stderr => qr/connection requires a valid client certificate/); + +$node->connect_fails( + "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt " + . $ssl_server->sslkey('client.key'), + "host: 'example.net', ca: 'root+server_ca.crt': connect with sslcert, client certificate sent", + expected_stderr => qr/unknown ca/); + +# Make sure the global CRL dir interacts properly with per-host trust. +$ssl_server->switch_server_cert( + $node, + certfile => 'server-cn-only', + crldir => 'client-crldir'); + +$node->connect_fails( + "$connstr host=example.com sslcertmode=require sslcert=ssl/client-revoked.crt " + . $ssl_server->sslkey('client-revoked.key'), + "host: 'example.com', ca: 'root+client_ca.crt': connect fails with revoked client cert", + expected_stderr => qr/certificate revoked/); + +# pg_hosts configuration with useless data at EOL +ok(unlink($node->data_dir . '/pg_hosts.conf')); +# example.org has an unconfigured CA. +$node->append_conf('pg_hosts.conf', + 'example.org server-cn-only.crt server-cn-only.key root+client_ca.crt "cmd" on TRAILING_TEXT MORE_TEXT' +); +$result = $node->restart(fail_ok => 1); +is($result, 0, 'pg_hosts.conf: restart fails with extra data at EOL'); +# pg_hosts configuration with useless data at EOL +ok(unlink($node->data_dir . '/pg_hosts.conf')); +# example.org has an unconfigured CA. +$node->append_conf('pg_hosts.conf', + 'example.org server-cn-only.crt server-cn-only.key root+client_ca.crt "cmd" notabooleanvalue' +); +$result = $node->restart(fail_ok => 1); +is($result, 0, + 'pg_hosts.conf: restart fails with non-boolean value in boolean field'); + +done_testing(); diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm index 4159addb700c1..6060771c1a859 100644 --- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm +++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group =pod @@ -72,6 +72,7 @@ sub init chmod(0600, glob "$pgdata/server-*.key") or die "failed to change permissions on server keys: $!"; _copy_files("ssl/root+client_ca.crt", $pgdata); + _copy_files("ssl/root+server_ca.crt", $pgdata); _copy_files("ssl/root_ca.crt", $pgdata); _copy_files("ssl/root+client.crl", $pgdata); mkdir("$pgdata/root+client-crldir") @@ -146,7 +147,8 @@ following parameters are supported: =item cafile => B The CA certificate file to use for the C GUC. If omitted it will -default to 'root+client_ca.crt'. +default to 'root+client_ca.crt'. If empty, no C configuration +parameter will be set. =item certfile => B @@ -181,10 +183,18 @@ sub set_server_cert unless defined $params->{keyfile}; my $sslconf = - "ssl_ca_file='$params->{cafile}.crt'\n" - . "ssl_cert_file='$params->{certfile}.crt'\n" + "ssl_cert_file='$params->{certfile}.crt'\n" . "ssl_key_file='$params->{keyfile}.key'\n" . "ssl_crl_file='$params->{crlfile}'\n"; + if ($params->{cafile} ne "") + { + $sslconf .= "ssl_ca_file='$params->{cafile}.crt'\n"; + } + else + { + $sslconf .= "ssl_ca_file=''\n"; + } + $sslconf .= "ssl_crl_dir='$params->{crldir}'\n" if defined $params->{crldir}; diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm index 96f0f201e9c0b..4c101a265038b 100644 --- a/src/test/ssl/t/SSL/Server.pm +++ b/src/test/ssl/t/SSL/Server.pm @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group =pod @@ -296,6 +296,11 @@ The CRL directory to use. Implementation is SSL backend specific. The passphrase command to use. If not set, an empty passphrase command will be set. +=item passphrase_cmd_reload => B + +Whether or not to allow passphrase command reloading. If set the passphrase +command reload configuration setting will be set to the value. + =item restart => B If set to 'no', the server won't be restarted after updating the settings. @@ -315,17 +320,23 @@ sub switch_server_cert my $pgdata = $node->data_dir; ok(unlink($node->data_dir . '/sslconfig.conf')); - $node->append_conf('sslconfig.conf', "ssl=on"); + $node->append_conf('sslconfig.conf', 'ssl=on'); $node->append_conf('sslconfig.conf', $backend->set_server_cert(\%params)); # use lists of ECDH curves and cipher suites for syntax testing - $node->append_conf('sslconfig.conf', 'ssl_groups=X25519:prime256v1:secp521r1'); + $node->append_conf('sslconfig.conf', + 'ssl_groups=prime256v1:secp521r1'); $node->append_conf('sslconfig.conf', 'ssl_tls13_ciphers=TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256'); $node->append_conf('sslconfig.conf', - "ssl_passphrase_command='" . $params{passphrase_cmd} . "'") + 'ssl_passphrase_command=\'' . $params{passphrase_cmd} . '\'') if defined $params{passphrase_cmd}; + $node->append_conf('sslconfig.conf', + 'ssl_passphrase_command_supports_reload=\'' + . $params{passphrase_cmd_reload} . '\'') + if defined $params{passphrase_cmd_reload}; + return if (defined($params{restart}) && $params{restart} eq 'no'); $node->restart; diff --git a/src/test/subscription/Makefile b/src/test/subscription/Makefile index 50b65d8f6ea21..1b22703dc21cd 100644 --- a/src/test/subscription/Makefile +++ b/src/test/subscription/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/test/subscription # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/test/subscription/Makefile @@ -13,9 +13,11 @@ subdir = src/test/subscription top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -EXTRA_INSTALL = contrib/hstore +EXTRA_INSTALL = contrib/hstore \ + src/test/modules/injection_points export with_icu +export enable_injection_points check: $(prove_check) diff --git a/src/test/subscription/meson.build b/src/test/subscription/meson.build index 586ffba434e11..e71e95c6297eb 100644 --- a/src/test/subscription/meson.build +++ b/src/test/subscription/meson.build @@ -1,11 +1,14 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tests += { 'name': 'subscription', 'sd': meson.current_source_dir(), 'bd': meson.current_build_dir(), 'tap': { - 'env': {'with_icu': icu.found() ? 'yes' : 'no'}, + 'env': { + 'with_icu': icu.found() ? 'yes' : 'no', + 'enable_injection_points': get_option('injection_points') ? 'yes' : 'no', + }, 'tests': [ 't/001_rep_changes.pl', 't/002_types.pl', @@ -42,6 +45,9 @@ tests += { 't/033_run_as_table_owner.pl', 't/034_temporal.pl', 't/035_conflicts.pl', + 't/036_sequences.pl', + 't/037_except.pl', + 't/038_walsnd_shutdown_timeout.pl', 't/100_bugs.pl', ], }, diff --git a/src/test/subscription/t/001_rep_changes.pl b/src/test/subscription/t/001_rep_changes.pl index 916fdb48b3b34..7d41715ed8115 100644 --- a/src/test/subscription/t/001_rep_changes.pl +++ b/src/test/subscription/t/001_rep_changes.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Basic logical replication test use strict; @@ -353,7 +353,8 @@ # Note that the current location of the log file is not grabbed immediately # after reloading the configuration, but after sending one SQL command to # the node so as we are sure that the reloading has taken effect. -my $log_location = -s $node_subscriber->logfile; +my $log_location_pub = -s $node_publisher->logfile; +my $log_location_sub = -s $node_subscriber->logfile; $node_publisher->safe_psql('postgres', "UPDATE tab_full_pk SET b = 'quux' WHERE a = 1"); @@ -363,15 +364,18 @@ $node_publisher->wait_for_catchup('tap_sub'); -my $logfile = slurp_file($node_subscriber->logfile, $log_location); -ok( $logfile =~ - qr/conflict detected on relation "public.tab_full_pk": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(1, quux\); replica identity \(a\)=\(1\)/m, +my $logfile = slurp_file($node_subscriber->logfile, $log_location_sub); +like( + $logfile, + qr/conflict detected on relation "public.tab_full_pk": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated: remote row \(1, quux\), replica identity \(a\)=\(1\)/m, 'update target row is missing'); -ok( $logfile =~ - qr/conflict detected on relation "public.tab_full": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(26\); replica identity full \(25\)/m, +like( + $logfile, + qr/conflict detected on relation "public.tab_full": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated: remote row \(26\), replica identity full \(25\)/m, 'update target row is missing'); -ok( $logfile =~ - qr/conflict detected on relation "public.tab_full_pk": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(2\)/m, +like( + $logfile, + qr/conflict detected on relation "public.tab_full_pk": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted: replica identity \(a\)=\(2\)/m, 'delete target row is missing'); $node_subscriber->append_conf('postgresql.conf', @@ -435,17 +439,35 @@ 22.22|bar|2), 'update works with dropped subscriber column'); +# Verify that GUC settings supplied in the CONNECTION string take effect on +# the publisher's walsender. We do this by enabling log_statement_stats in +# the CONNECTION string later and checking that the publisher's log contains a +# QUERY STATISTICS message. +# +# First, confirm that no such QUERY STATISTICS message appears before enabling +# log_statement_stats. +$logfile = slurp_file($node_publisher->logfile, $log_location_pub); +unlike( + $logfile, + qr/QUERY STATISTICS/, + 'log_statement_stats has not been enabled yet'); +$log_location_pub = -s $node_publisher->logfile; + # check that change of connection string and/or publication list causes # restart of subscription workers. We check the state along with # application_name to ensure that the walsender is (re)started. # # Not all of these are registered as tests as we need to poll for a change # but the test suite will fail nonetheless when something goes wrong. +# +# Enable log_statement_stats as the change of connection string, +# which is also for the above mentioned test of GUC settings passed through +# CONNECTION. my $oldpid = $node_publisher->safe_psql('postgres', "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" ); $node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr sslmode=disable'" + "ALTER SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr options=''-c log_statement_stats=on'''" ); $node_publisher->poll_query_until('postgres', "SELECT pid != $oldpid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" @@ -453,6 +475,16 @@ or die "Timed out while waiting for apply to restart after changing CONNECTION"; +# Check that the expected QUERY STATISTICS message appears, +# which shows that log_statement_stats=on from the CONNECTION string +# was correctly passed through to and honored by the walsender. +$logfile = slurp_file($node_publisher->logfile, $log_location_pub); +like( + $logfile, + qr/QUERY STATISTICS/, + 'log_statement_stats in CONNECTION string had effect on publisher\'s walsender' +); + $oldpid = $node_publisher->safe_psql('postgres', "SELECT pid FROM pg_stat_replication WHERE application_name = 'tap_sub' AND state = 'streaming';" ); @@ -508,14 +540,16 @@ # Note that the current location of the log file is not grabbed immediately # after reloading the configuration, but after sending one SQL command to # the node so that we are sure that the reloading has taken effect. -$log_location = -s $node_publisher->logfile; +$log_location_pub = -s $node_publisher->logfile; $node_publisher->safe_psql('postgres', "INSERT INTO tab_notrep VALUES (11)"); $node_publisher->wait_for_catchup('tap_sub'); -$logfile = slurp_file($node_publisher->logfile, $log_location); -ok($logfile =~ qr/skipped replication of an empty transaction with XID/, +$logfile = slurp_file($node_publisher->logfile, $log_location_pub); +like( + $logfile, + qr/skipped replication of an empty transaction with XID/, 'empty transaction is skipped'); $result = @@ -588,8 +622,9 @@ CREATE PUBLICATION tap_pub2 FOR TABLE skip_wal; ROLLBACK; }); -ok( $reterr =~ - m/WARNING: "wal_level" is insufficient to publish logical changes/, +like( + $reterr, + qr/WARNING: logical decoding must be enabled to publish logical changes/, 'CREATE PUBLICATION while "wal_level=minimal"'); done_testing(); diff --git a/src/test/subscription/t/002_types.pl b/src/test/subscription/t/002_types.pl index 75beb594c58d4..416d0a470f61b 100644 --- a/src/test/subscription/t/002_types.pl +++ b/src/test/subscription/t/002_types.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This tests that more complex datatypes are replicated correctly # by logical replication diff --git a/src/test/subscription/t/003_constraints.pl b/src/test/subscription/t/003_constraints.pl index e333263d604f5..237e113013bae 100644 --- a/src/test/subscription/t/003_constraints.pl +++ b/src/test/subscription/t/003_constraints.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This test checks that constraints work on subscriber use strict; diff --git a/src/test/subscription/t/004_sync.pl b/src/test/subscription/t/004_sync.pl index d5eac05a3b309..85ab501b44248 100644 --- a/src/test/subscription/t/004_sync.pl +++ b/src/test/subscription/t/004_sync.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for logical replication table syncing use strict; @@ -172,6 +172,12 @@ 'postgres', 'SELECT count(*) = 0 FROM pg_replication_slots'), 'DROP SUBSCRIPTION during error can clean up the slots on the publisher'); +# After dropping the subscription, all replication origins, whether created by +# an apply worker or table sync worker, should have been cleaned up. +$result = $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM pg_replication_origin_status"); +is($result, qq(0), 'all replication origins have been cleaned up'); + $node_subscriber->stop('fast'); $node_publisher->stop('fast'); diff --git a/src/test/subscription/t/005_encoding.pl b/src/test/subscription/t/005_encoding.pl index f74a22995d95c..92ce723129130 100644 --- a/src/test/subscription/t/005_encoding.pl +++ b/src/test/subscription/t/005_encoding.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test replication between databases with different encodings use strict; diff --git a/src/test/subscription/t/006_rewrite.pl b/src/test/subscription/t/006_rewrite.pl index 651447e0bc581..83c7a08d8c84e 100644 --- a/src/test/subscription/t/006_rewrite.pl +++ b/src/test/subscription/t/006_rewrite.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test logical replication behavior with heap rewrites use strict; diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl index 7d12bcbddb687..92d10630402e8 100644 --- a/src/test/subscription/t/007_ddl.pl +++ b/src/test/subscription/t/007_ddl.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test some logical replication DDL behavior use strict; @@ -45,8 +45,9 @@ my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres', "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub" ); -ok( $stderr =~ - m/WARNING: publication "non_existent_pub" does not exist on the publisher/, +like( + $stderr, + qr/WARNING: publication "non_existent_pub" does not exist on the publisher/, "Create subscription throws warning for non-existent publication"); # Wait for initial table sync to finish. @@ -56,21 +57,24 @@ ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres', "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub1, non_existent_pub2" ); -ok( $stderr =~ - m/WARNING: publications "non_existent_pub1", "non_existent_pub2" do not exist on the publisher/, +like( + $stderr, + qr/WARNING: publications "non_existent_pub1", "non_existent_pub2" do not exist on the publisher/, "Alter subscription add publication throws warning for non-existent publications" ); # Specifying non-existent publication along with set publication. ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres', "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"); -ok( $stderr =~ - m/WARNING: publication "non_existent_pub" does not exist on the publisher/, +like( + $stderr, + qr/WARNING: publication "non_existent_pub" does not exist on the publisher/, "Alter subscription set publication throws warning for non-existent publication" ); # Cleanup -$node_publisher->safe_psql('postgres', qq[ +$node_publisher->safe_psql( + 'postgres', qq[ DROP PUBLICATION mypub; SELECT pg_drop_replication_slot('mysub'); ]); @@ -86,32 +90,38 @@ sub test_swap my ($table_name, $pubname, $appname) = @_; # Confirms tuples can be replicated - $node_publisher->safe_psql('postgres', "INSERT INTO $table_name VALUES (1);"); + $node_publisher->safe_psql('postgres', + "INSERT INTO $table_name VALUES (1);"); $node_publisher->wait_for_catchup($appname); my $result = - $node_subscriber->safe_psql('postgres', "SELECT a FROM $table_name"); - is($result, qq(1), 'check replication worked well before renaming a publication'); + $node_subscriber->safe_psql('postgres', "SELECT a FROM $table_name"); + is($result, qq(1), + 'check replication worked well before renaming a publication'); # Swap the name of publications; $pubname <-> pub_empty - $node_publisher->safe_psql('postgres', qq[ + $node_publisher->safe_psql( + 'postgres', qq[ ALTER PUBLICATION $pubname RENAME TO tap_pub_tmp; ALTER PUBLICATION pub_empty RENAME TO $pubname; ALTER PUBLICATION tap_pub_tmp RENAME TO pub_empty; ]); # Insert the data again - $node_publisher->safe_psql('postgres', "INSERT INTO $table_name VALUES (2);"); + $node_publisher->safe_psql('postgres', + "INSERT INTO $table_name VALUES (2);"); $node_publisher->wait_for_catchup($appname); # Confirms the second tuple won't be replicated because $pubname does not # contains relations anymore. $result = - $node_subscriber->safe_psql('postgres', "SELECT a FROM $table_name ORDER BY a"); + $node_subscriber->safe_psql('postgres', + "SELECT a FROM $table_name ORDER BY a"); is($result, qq(1), 'check the tuple inserted after the RENAME was not replicated'); # Restore the name of publications because it can be called several times - $node_publisher->safe_psql('postgres', qq[ + $node_publisher->safe_psql( + 'postgres', qq[ ALTER PUBLICATION $pubname RENAME TO tap_pub_tmp; ALTER PUBLICATION pub_empty RENAME TO $pubname; ALTER PUBLICATION tap_pub_tmp RENAME TO pub_empty; @@ -124,7 +134,8 @@ sub test_swap $node_subscriber->safe_psql('postgres', $ddl); # Create publications and a subscription -$node_publisher->safe_psql('postgres', qq[ +$node_publisher->safe_psql( + 'postgres', qq[ CREATE PUBLICATION pub_empty; CREATE PUBLICATION pub_for_tab FOR TABLE test1; CREATE PUBLICATION pub_for_all_tables FOR ALL TABLES; @@ -139,19 +150,20 @@ sub test_swap # Switches a publication which includes all tables $node_subscriber->safe_psql('postgres', - "ALTER SUBSCRIPTION tap_sub SET PUBLICATION pub_for_all_tables;" -); + "ALTER SUBSCRIPTION tap_sub SET PUBLICATION pub_for_all_tables;"); $node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); # Confirms RENAME command works well for ALL TABLES publication test_swap('test2', 'pub_for_all_tables', 'tap_sub'); # Cleanup -$node_publisher->safe_psql('postgres', qq[ +$node_publisher->safe_psql( + 'postgres', qq[ DROP PUBLICATION pub_empty, pub_for_tab, pub_for_all_tables; DROP TABLE test1, test2; ]); -$node_subscriber->safe_psql('postgres', qq[ +$node_subscriber->safe_psql( + 'postgres', qq[ DROP SUBSCRIPTION tap_sub; DROP TABLE test1, test2; ]); diff --git a/src/test/subscription/t/008_diff_schema.pl b/src/test/subscription/t/008_diff_schema.pl index d0d24f0eb96d0..96e3d6f588811 100644 --- a/src/test/subscription/t/008_diff_schema.pl +++ b/src/test/subscription/t/008_diff_schema.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test behavior with different schema on subscriber use strict; diff --git a/src/test/subscription/t/009_matviews.pl b/src/test/subscription/t/009_matviews.pl index 9316fd1bb6d95..9ea2a3ba66611 100644 --- a/src/test/subscription/t/009_matviews.pl +++ b/src/test/subscription/t/009_matviews.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test materialized views behavior use strict; @@ -18,20 +18,20 @@ my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', + q{CREATE TABLE test1 (a int PRIMARY KEY, b text)}); +$node_subscriber->safe_psql('postgres', + q{CREATE TABLE test1 (a int PRIMARY KEY, b text);}); + $node_publisher->safe_psql('postgres', "CREATE PUBLICATION mypub FOR ALL TABLES;"); $node_subscriber->safe_psql('postgres', "CREATE SUBSCRIPTION mysub CONNECTION '$publisher_connstr' PUBLICATION mypub;" ); -$node_publisher->safe_psql('postgres', - q{CREATE TABLE test1 (a int PRIMARY KEY, b text)}); $node_publisher->safe_psql('postgres', q{INSERT INTO test1 (a, b) VALUES (1, 'one'), (2, 'two');}); -$node_subscriber->safe_psql('postgres', - q{CREATE TABLE test1 (a int PRIMARY KEY, b text);}); - $node_publisher->wait_for_catchup('mysub'); # Materialized views are not supported by logical replication, but diff --git a/src/test/subscription/t/010_truncate.pl b/src/test/subscription/t/010_truncate.pl index 3d16c2a800de9..945505d02393a 100644 --- a/src/test/subscription/t/010_truncate.pl +++ b/src/test/subscription/t/010_truncate.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test TRUNCATE use strict; diff --git a/src/test/subscription/t/011_generated.pl b/src/test/subscription/t/011_generated.pl index c7a4c52e4f2a9..af551fa43b454 100644 --- a/src/test/subscription/t/011_generated.pl +++ b/src/test/subscription/t/011_generated.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test generated columns use strict; diff --git a/src/test/subscription/t/012_collation.pl b/src/test/subscription/t/012_collation.pl index 0f2076471e878..1dbdcecaa425f 100644 --- a/src/test/subscription/t/012_collation.pl +++ b/src/test/subscription/t/012_collation.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test collations, in particular nondeterministic ones # (only works with ICU) diff --git a/src/test/subscription/t/013_partition.pl b/src/test/subscription/t/013_partition.pl index 61b0cb4aa1ac1..234d4f003b7b7 100644 --- a/src/test/subscription/t/013_partition.pl +++ b/src/test/subscription/t/013_partition.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test logical replication with partitioned tables use strict; @@ -51,8 +51,7 @@ ); # make a BRIN index to test aminsertcleanup logic in subscriber $node_subscriber1->safe_psql('postgres', - "CREATE INDEX tab1_c_brin_idx ON tab1 USING brin (c)" -); + "CREATE INDEX tab1_c_brin_idx ON tab1 USING brin (c)"); $node_subscriber1->safe_psql('postgres', "CREATE TABLE tab1_1 (b text, c text DEFAULT 'sub1_tab1', a int NOT NULL)" ); @@ -368,17 +367,21 @@ BEGIN $node_publisher->wait_for_catchup('sub2'); my $logfile = slurp_file($node_subscriber1->logfile(), $log_location); -ok( $logfile =~ - qr/conflict detected on relation "public.tab1_2_2": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(null, 4, quux\); replica identity \(a\)=\(4\)/, +like( + $logfile, + qr/conflict detected on relation "public.tab1_2_2": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated: remote row \(null, 4, quux\), replica identity \(a\)=\(4\)/, 'update target row is missing in tab1_2_2'); -ok( $logfile =~ - qr/conflict detected on relation "public.tab1_1": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(1\)/, +like( + $logfile, + qr/conflict detected on relation "public.tab1_1": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted: replica identity \(a\)=\(1\)/, 'delete target row is missing in tab1_1'); -ok( $logfile =~ - qr/conflict detected on relation "public.tab1_2_2": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(4\)/, +like( + $logfile, + qr/conflict detected on relation "public.tab1_2_2": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted: replica identity \(a\)=\(4\)/, 'delete target row is missing in tab1_2_2'); -ok( $logfile =~ - qr/conflict detected on relation "public.tab1_def": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(10\)/, +like( + $logfile, + qr/conflict detected on relation "public.tab1_def": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted: replica identity \(a\)=\(10\)/, 'delete target row is missing in tab1_def'); # Tests for replication using root table identity and schema @@ -781,11 +784,13 @@ BEGIN $node_publisher->wait_for_catchup('sub2'); $logfile = slurp_file($node_subscriber1->logfile(), $log_location); -ok( $logfile =~ - qr/conflict detected on relation "public.tab2_1": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(pub_tab2, quux, 5\); replica identity \(a\)=\(5\)/, +like( + $logfile, + qr/conflict detected on relation "public.tab2_1": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated: remote row \(pub_tab2, quux, 5\), replica identity \(a\)=\(5\)/, 'update target row is missing in tab2_1'); -ok( $logfile =~ - qr/conflict detected on relation "public.tab2_1": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(1\)/, +like( + $logfile, + qr/conflict detected on relation "public.tab2_1": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted: replica identity \(a\)=\(1\)/, 'delete target row is missing in tab2_1'); # Enable the track_commit_timestamp to detect the conflict when attempting @@ -802,9 +807,10 @@ BEGIN $node_publisher->wait_for_catchup('sub_viaroot'); $logfile = slurp_file($node_subscriber1->logfile(), $log_location); -ok( $logfile =~ - qr/conflict detected on relation "public.tab2_1": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified locally in transaction [0-9]+ at .*\n.*Existing local tuple \(yyy, null, 3\); remote tuple \(pub_tab2, quux, 3\); replica identity \(a\)=\(3\)/, - 'updating a tuple that was modified by a different origin'); +like( + $logfile, + qr/conflict detected on relation "public.tab2_1": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified locally in transaction [0-9]+ at .*: local row \(yyy, null, 3\), remote row \(pub_tab2, quux, 3\), replica identity \(a\)=\(3\)./, + 'updating a row that was modified by a different origin'); # The remaining tests no longer test conflict detection. $node_subscriber1->append_conf('postgresql.conf', diff --git a/src/test/subscription/t/014_binary.pl b/src/test/subscription/t/014_binary.pl index 1efa822bc392c..b9dad1c962dd1 100644 --- a/src/test/subscription/t/014_binary.pl +++ b/src/test/subscription/t/014_binary.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Binary mode logical replication test diff --git a/src/test/subscription/t/015_stream.pl b/src/test/subscription/t/015_stream.pl index 03135b1cd6e1a..ac96bc3f00903 100644 --- a/src/test/subscription/t/015_stream.pl +++ b/src/test/subscription/t/015_stream.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test streaming of simple large transaction use strict; diff --git a/src/test/subscription/t/016_stream_subxact.pl b/src/test/subscription/t/016_stream_subxact.pl index 16b6653b4d063..6fe8f2ef359b1 100644 --- a/src/test/subscription/t/016_stream_subxact.pl +++ b/src/test/subscription/t/016_stream_subxact.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test streaming of transaction containing subtransactions use strict; diff --git a/src/test/subscription/t/017_stream_ddl.pl b/src/test/subscription/t/017_stream_ddl.pl index 77913701f0126..990c1129a1121 100644 --- a/src/test/subscription/t/017_stream_ddl.pl +++ b/src/test/subscription/t/017_stream_ddl.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test streaming of large transaction with DDL and subtransactions # diff --git a/src/test/subscription/t/018_stream_subxact_abort.pl b/src/test/subscription/t/018_stream_subxact_abort.pl index 3c0cc3536dc28..31e7bb961549e 100644 --- a/src/test/subscription/t/018_stream_subxact_abort.pl +++ b/src/test/subscription/t/018_stream_subxact_abort.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test streaming of transaction containing multiple subtransactions and rollbacks use strict; diff --git a/src/test/subscription/t/019_stream_subxact_ddl_abort.pl b/src/test/subscription/t/019_stream_subxact_ddl_abort.pl index 8952c11c14c95..9ae3b79f2a92d 100644 --- a/src/test/subscription/t/019_stream_subxact_ddl_abort.pl +++ b/src/test/subscription/t/019_stream_subxact_ddl_abort.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test streaming of transaction with subtransactions, DDLs, DMLs, and # rollbacks diff --git a/src/test/subscription/t/020_messages.pl b/src/test/subscription/t/020_messages.pl index 03bda94971aeb..ac51884915644 100644 --- a/src/test/subscription/t/020_messages.pl +++ b/src/test/subscription/t/020_messages.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests that logical decoding messages use strict; diff --git a/src/test/subscription/t/021_twophase.pl b/src/test/subscription/t/021_twophase.pl index b8e4242d1f121..4404d7b5449e9 100644 --- a/src/test/subscription/t/021_twophase.pl +++ b/src/test/subscription/t/021_twophase.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # logical replication of 2PC test use strict; diff --git a/src/test/subscription/t/022_twophase_cascade.pl b/src/test/subscription/t/022_twophase_cascade.pl index 06148a85039b8..aa2f61b49572a 100644 --- a/src/test/subscription/t/022_twophase_cascade.pl +++ b/src/test/subscription/t/022_twophase_cascade.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test cascading logical replication of 2PC. # diff --git a/src/test/subscription/t/023_twophase_stream.pl b/src/test/subscription/t/023_twophase_stream.pl index e01347ca69950..8bf93aa379b6a 100644 --- a/src/test/subscription/t/023_twophase_stream.pl +++ b/src/test/subscription/t/023_twophase_stream.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test logical replication of 2PC with streaming. use strict; @@ -429,6 +429,51 @@ sub test_streaming $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM test_tab_2"); is($result, qq(1), 'transaction is committed on subscriber'); +# Test the ability to re-apply a transaction when a parallel apply worker fails +# to prepare the transaction due to insufficient max_prepared_transactions +# setting. +$node_subscriber->append_conf( + 'postgresql.conf', qq( +max_prepared_transactions = 0 +debug_logical_replication_streaming = buffered +)); +$node_subscriber->restart; + +$node_publisher->safe_psql( + 'postgres', q{ + BEGIN; + INSERT INTO test_tab_2 values(2); + PREPARE TRANSACTION 'xact'; + COMMIT PREPARED 'xact'; + }); + +$offset = -s $node_subscriber->logfile; + +# Confirm the ERROR is reported because max_prepared_transactions is zero +$node_subscriber->wait_for_log( + qr/ERROR: ( [A-Z0-9]+:)? prepared transactions are disabled/, + $offset); + +# Confirm that the parallel apply worker has encountered an error. The check +# focuses on the worker type as a keyword, since the error message content may +# differ based on whether the leader initially detected the parallel apply +# worker's failure or received a signal from it. +$node_subscriber->wait_for_log( + qr/ERROR: .*logical replication parallel apply worker.*/, + $offset); + +# Set max_prepared_transactions to correct value to resume the replication +$node_subscriber->append_conf('postgresql.conf', + qq(max_prepared_transactions = 10)); +$node_subscriber->restart; + +$node_publisher->wait_for_catchup($appname); + +# Check that transaction is committed on subscriber +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM test_tab_2"); +is($result, qq(2), 'transaction is committed on subscriber after retrying'); + ############################### # check all the cleanup ############################### diff --git a/src/test/subscription/t/024_add_drop_pub.pl b/src/test/subscription/t/024_add_drop_pub.pl index e995d8b383901..63adb1b464c15 100644 --- a/src/test/subscription/t/024_add_drop_pub.pl +++ b/src/test/subscription/t/024_add_drop_pub.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # This test checks behaviour of ALTER SUBSCRIPTION ... ADD/DROP PUBLICATION and # ensures that creating a publication associated with a subscription at a later @@ -108,11 +108,12 @@ my $offset = -s $node_publisher->logfile; -$node_publisher->safe_psql('postgres',"INSERT INTO tab_3 values(1)"); +$node_publisher->safe_psql('postgres', "INSERT INTO tab_3 values(1)"); # Verify that a warning is logged. $node_publisher->wait_for_log( - qr/WARNING: ( [A-Z0-9]+:)? skipped loading publication: tap_pub_3/, $offset); + qr/WARNING: ( [A-Z0-9]+:)? skipped loading publication "tap_pub_3"/, + $offset); $node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub_3 FOR TABLE tab_3"); @@ -128,10 +129,11 @@ # Verify that the insert operation gets replicated to subscriber after # publication is created. -$result = $node_subscriber->safe_psql('postgres', - "SELECT * FROM tab_3"); -is($result, qq(1 -2), 'check that the incremental data is replicated after the publication is created'); +$result = $node_subscriber->safe_psql('postgres', "SELECT * FROM tab_3"); +is( $result, qq(1 +2), + 'check that the incremental data is replicated after the publication is created' +); # shutdown $node_subscriber->stop('fast'); diff --git a/src/test/subscription/t/025_rep_changes_for_schema.pl b/src/test/subscription/t/025_rep_changes_for_schema.pl index 0d074ccda1367..3a4e86b55ef53 100644 --- a/src/test/subscription/t/025_rep_changes_for_schema.pl +++ b/src/test/subscription/t/025_rep_changes_for_schema.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Logical replication tests for schema publications use strict; diff --git a/src/test/subscription/t/026_stats.pl b/src/test/subscription/t/026_stats.pl index 00a1c2fcd48c3..5d457060a0209 100644 --- a/src/test/subscription/t/026_stats.pl +++ b/src/test/subscription/t/026_stats.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for subscription stats. use strict; @@ -21,10 +21,16 @@ sub create_sub_pub_w_errors { - my ($node_publisher, $node_subscriber, $db, $table_name) = @_; - # Initial table setup on both publisher and subscriber. On subscriber we - # create the same tables but with primary keys. Also, insert some data that - # will conflict with the data replicated from publisher later. + my ($node_publisher, $node_subscriber, $db, $table_name, $sequence_name) + = @_; + # Initial table and sequence setup on both publisher and subscriber. + # + # Tables: Created on both nodes, but the subscriber version includes + # primary keys and pre-populated data that will intentionally conflict with + # replicated data from the publisher. + # + # Sequences: Created on both nodes with different INCREMENT values to + # intentionally trigger replication conflicts. $node_publisher->safe_psql( $db, qq[ @@ -32,6 +38,7 @@ sub create_sub_pub_w_errors CREATE TABLE $table_name(a int); ALTER TABLE $table_name REPLICA IDENTITY FULL; INSERT INTO $table_name VALUES (1); + CREATE SEQUENCE $sequence_name; COMMIT; ]); $node_subscriber->safe_psql( @@ -40,35 +47,57 @@ sub create_sub_pub_w_errors BEGIN; CREATE TABLE $table_name(a int primary key); INSERT INTO $table_name VALUES (1); + CREATE SEQUENCE $sequence_name INCREMENT BY 10; COMMIT; ]); # Set up publication. my $pub_name = $table_name . '_pub'; + my $pub_seq_name = $sequence_name . '_pub'; my $publisher_connstr = $node_publisher->connstr . qq( dbname=$db); - $node_publisher->safe_psql($db, - qq(CREATE PUBLICATION $pub_name FOR TABLE $table_name)); + $node_publisher->safe_psql( + $db, + qq[ + CREATE PUBLICATION $pub_name FOR TABLE $table_name; + CREATE PUBLICATION $pub_seq_name FOR ALL SEQUENCES; + ]); # Create subscription. The tablesync for table on subscription will enter into - # infinite error loop due to violating the unique constraint. + # infinite error loop due to violating the unique constraint. The sequencesync + # will also fail due to different sequence increment values on publisher and + # subscriber. my $sub_name = $table_name . '_sub'; $node_subscriber->safe_psql($db, - qq(CREATE SUBSCRIPTION $sub_name CONNECTION '$publisher_connstr' PUBLICATION $pub_name) + qq(CREATE SUBSCRIPTION $sub_name CONNECTION '$publisher_connstr' PUBLICATION $pub_name, $pub_seq_name) ); $node_publisher->wait_for_catchup($sub_name); - # Wait for the tablesync error to be reported. + # Wait for the tablesync and sequencesync error to be reported. $node_subscriber->poll_query_until( $db, qq[ - SELECT sync_error_count > 0 - FROM pg_stat_subscription_stats - WHERE subname = '$sub_name' + SELECT count(1) = 1 FROM pg_stat_subscription_stats + WHERE subname = '$sub_name' AND sync_seq_error_count > 0 AND sync_table_error_count > 0 + ]) + or die + qq(Timed out while waiting for sequencesync errors and tablesync errors for subscription '$sub_name'); + + # Change the sequence INCREMENT value back to the default on the subscriber + # so it doesn't error out. + $node_subscriber->safe_psql($db, + qq(ALTER SEQUENCE $sequence_name INCREMENT 1)); + + # Wait for sequencesync to finish. + $node_subscriber->poll_query_until( + $db, + qq[ + SELECT count(1) = 1 FROM pg_subscription_rel + WHERE srrelid = '$sequence_name'::regclass AND srsubstate = 'r' ]) or die - qq(Timed out while waiting for tablesync errors for subscription '$sub_name'); + qq(Timed out while waiting for subscriber to synchronize data for sequence '$sequence_name'.); # Truncate test_tab1 so that tablesync worker can continue. $node_subscriber->safe_psql($db, qq(TRUNCATE $table_name)); @@ -136,23 +165,26 @@ sub create_sub_pub_w_errors # Create the publication and subscription with sync and apply errors my $table1_name = 'test_tab1'; +my $sequence1_name = 'test_seq1'; my ($pub1_name, $sub1_name) = create_sub_pub_w_errors($node_publisher, $node_subscriber, $db, - $table1_name); + $table1_name, $sequence1_name); -# Apply errors, sync errors, and conflicts are > 0 and stats_reset timestamp is NULL +# Apply errors, sequencesync errors, tablesync errors, and conflicts are > 0 and stats_reset +# timestamp is NULL. is( $node_subscriber->safe_psql( $db, qq(SELECT apply_error_count > 0, - sync_error_count > 0, + sync_seq_error_count > 0, + sync_table_error_count > 0, confl_insert_exists > 0, confl_delete_missing > 0, stats_reset IS NULL FROM pg_stat_subscription_stats WHERE subname = '$sub1_name') ), - qq(t|t|t|t|t), - qq(Check that apply errors, sync errors, and conflicts are > 0 and stats_reset is NULL for subscription '$sub1_name'.) + qq(t|t|t|t|t|t), + qq(Check that apply errors, sequencesync errors, tablesync errors, and conflicts are > 0 and stats_reset is NULL for subscription '$sub1_name'.) ); # Reset a single subscription @@ -160,19 +192,21 @@ sub create_sub_pub_w_errors qq(SELECT pg_stat_reset_subscription_stats((SELECT subid FROM pg_stat_subscription_stats WHERE subname = '$sub1_name'))) ); -# Apply errors, sync errors, and conflicts are 0 and stats_reset timestamp is not NULL +# Apply errors, sequencesync errors, tablesync errors, and conflicts are 0 and +# stats_reset timestamp is not NULL. is( $node_subscriber->safe_psql( $db, qq(SELECT apply_error_count = 0, - sync_error_count = 0, + sync_seq_error_count = 0, + sync_table_error_count = 0, confl_insert_exists = 0, confl_delete_missing = 0, stats_reset IS NOT NULL FROM pg_stat_subscription_stats WHERE subname = '$sub1_name') ), - qq(t|t|t|t|t), - qq(Confirm that apply errors, sync errors, and conflicts are 0 and stats_reset is not NULL after reset for subscription '$sub1_name'.) + qq(t|t|t|t|t|t), + qq(Confirm that apply errors, sequencesync errors, tablesync errors, and conflicts are 0 and stats_reset is not NULL after reset for subscription '$sub1_name'.) ); # Get reset timestamp @@ -198,56 +232,62 @@ sub create_sub_pub_w_errors # Make second subscription and publication my $table2_name = 'test_tab2'; +my $sequence2_name = 'test_seq2'; my ($pub2_name, $sub2_name) = create_sub_pub_w_errors($node_publisher, $node_subscriber, $db, - $table2_name); + $table2_name, $sequence2_name); -# Apply errors, sync errors, and conflicts are > 0 and stats_reset timestamp is NULL +# Apply errors, sequencesync errors, tablesync errors, and conflicts are > 0 +# and stats_reset timestamp is NULL is( $node_subscriber->safe_psql( $db, qq(SELECT apply_error_count > 0, - sync_error_count > 0, + sync_seq_error_count > 0, + sync_table_error_count > 0, confl_insert_exists > 0, confl_delete_missing > 0, stats_reset IS NULL FROM pg_stat_subscription_stats WHERE subname = '$sub2_name') ), - qq(t|t|t|t|t), - qq(Confirm that apply errors, sync errors, and conflicts are > 0 and stats_reset is NULL for sub '$sub2_name'.) + qq(t|t|t|t|t|t), + qq(Confirm that apply errors, sequencesync errors, tablesync errors, and conflicts are > 0 and stats_reset is NULL for sub '$sub2_name'.) ); # Reset all subscriptions $node_subscriber->safe_psql($db, qq(SELECT pg_stat_reset_subscription_stats(NULL))); -# Apply errors, sync errors, and conflicts are 0 and stats_reset timestamp is not NULL +# Apply errors, sequencesync errors, tablesync errors, and conflicts are 0 and +# stats_reset timestamp is not NULL. is( $node_subscriber->safe_psql( $db, qq(SELECT apply_error_count = 0, - sync_error_count = 0, + sync_seq_error_count = 0, + sync_table_error_count = 0, confl_insert_exists = 0, confl_delete_missing = 0, stats_reset IS NOT NULL FROM pg_stat_subscription_stats WHERE subname = '$sub1_name') ), - qq(t|t|t|t|t), - qq(Confirm that apply errors, sync errors, and conflicts are 0 and stats_reset is not NULL for sub '$sub1_name' after reset.) + qq(t|t|t|t|t|t), + qq(Confirm that apply errors, sequencesync errors, tablesync errors, and conflicts are 0 and stats_reset is not NULL for sub '$sub1_name' after reset.) ); is( $node_subscriber->safe_psql( $db, qq(SELECT apply_error_count = 0, - sync_error_count = 0, + sync_seq_error_count = 0, + sync_table_error_count = 0, confl_insert_exists = 0, confl_delete_missing = 0, stats_reset IS NOT NULL FROM pg_stat_subscription_stats WHERE subname = '$sub2_name') ), - qq(t|t|t|t|t), - qq(Confirm that apply errors, sync errors, and conflicts are 0 and stats_reset is not NULL for sub '$sub2_name' after reset.) + qq(t|t|t|t|t|t), + qq(Confirm that apply errors, sequencesync errors, tablesync errors, errors, and conflicts are 0 and stats_reset is not NULL for sub '$sub2_name' after reset.) ); $reset_time1 = $node_subscriber->safe_psql($db, diff --git a/src/test/subscription/t/027_nosuperuser.pl b/src/test/subscription/t/027_nosuperuser.pl index 36af1c16e7f51..322f5b4cc6a14 100644 --- a/src/test/subscription/t/027_nosuperuser.pl +++ b/src/test/subscription/t/027_nosuperuser.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test that logical replication respects permissions use strict; @@ -399,8 +399,9 @@ sub grant_superuser isnt($ret, 0, "non zero exit for subscription whose owner is a non-superuser must specify password parameter of the connection string" ); - ok( $stderr =~ - m/DETAIL: Non-superusers must provide a password in the connection string./, + like( + $stderr, + qr/DETAIL: Non-superusers must provide a password in the connection string./, 'subscription whose owner is a non-superuser must specify password parameter of the connection string' ); diff --git a/src/test/subscription/t/028_row_filter.pl b/src/test/subscription/t/028_row_filter.pl index e2c8367005355..c666ae0048365 100644 --- a/src/test/subscription/t/028_row_filter.pl +++ b/src/test/subscription/t/028_row_filter.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test logical replication behavior with row filtering use strict; diff --git a/src/test/subscription/t/029_on_error.pl b/src/test/subscription/t/029_on_error.pl index 243662e1240c8..7d68759b6cd00 100644 --- a/src/test/subscription/t/029_on_error.pl +++ b/src/test/subscription/t/029_on_error.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for disable_on_error and SKIP transaction features. use strict; @@ -30,7 +30,7 @@ sub test_skip_lsn # ERROR with its CONTEXT when retrieving this information. my $contents = slurp_file($node_subscriber->logfile, $offset); $contents =~ - qr/conflict detected on relation "public.tbl".*\n.*DETAIL:.* Key already exists in unique index "tbl_pkey", modified by .*origin.* transaction \d+ at .*\n.*Key \(i\)=\(\d+\); existing local tuple .*; remote tuple .*\n.*CONTEXT:.* for replication target relation "public.tbl" in transaction \d+, finished at ([[:xdigit:]]+\/[[:xdigit:]]+)/m + qr/conflict detected on relation "public.tbl".*\n.*DETAIL:.* Could not apply remote change.*\n.*Key already exists in unique index "tbl_pkey", modified by .*origin.* in transaction \d+ at .*: key .*, local row .*\n.*CONTEXT:.* for replication target relation "public.tbl" in transaction \d+, finished at ([[:xdigit:]]+\/[[:xdigit:]]+)/m or die "could not get error-LSN"; my $lsn = $1; diff --git a/src/test/subscription/t/030_origin.pl b/src/test/subscription/t/030_origin.pl index 5b82848e5e6d3..5076ebe609bb9 100644 --- a/src/test/subscription/t/030_origin.pl +++ b/src/test/subscription/t/030_origin.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test the CREATE SUBSCRIPTION 'origin' parameter and its interaction with # 'copy_data' parameter. @@ -163,7 +163,7 @@ $node_C->safe_psql('postgres', "UPDATE tab SET a = 33 WHERE a = 32;"); $node_B->wait_for_log( - qr/conflict detected on relation "public.tab": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified by a different origin ".*" in transaction [0-9]+ at .*\n.*Existing local tuple \(32\); remote tuple \(33\); replica identity \(a\)=\(32\)/ + qr/conflict detected on relation "public.tab": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified by a different origin ".*" in transaction [0-9]+ at .*: local row \(32\), remote row \(33\), replica identity \(a\)=\(32\)./ ); $node_B->safe_psql('postgres', "DELETE FROM tab;"); @@ -179,7 +179,7 @@ $node_C->safe_psql('postgres', "DELETE FROM tab WHERE a = 33;"); $node_B->wait_for_log( - qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.*\n.*DETAIL:.* Deleting the row that was modified by a different origin ".*" in transaction [0-9]+ at .*\n.*Existing local tuple \(33\); replica identity \(a\)=\(33\)/ + qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.*\n.*DETAIL:.* Deleting the row that was modified by a different origin ".*" in transaction [0-9]+ at .*: local row \(33\), replica identity \(a\)=\(33\).*/ ); # The remaining tests no longer test conflict detection. diff --git a/src/test/subscription/t/031_column_list.pl b/src/test/subscription/t/031_column_list.pl index e859bcdf4eb8a..ed97d423e5073 100644 --- a/src/test/subscription/t/031_column_list.pl +++ b/src/test/subscription/t/031_column_list.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Test partial-column publication of tables use strict; @@ -1272,8 +1272,9 @@ CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub_mix_1, pub_mix_2; )); -ok( $stderr =~ - qr/cannot use different column lists for table "public.test_mix_1" in different publications/, +like( + $stderr, + qr/cannot use different column lists for table "public.test_mix_1" in different publications/, 'different column lists detected'); # TEST: If the column list is changed after creating the subscription, we diff --git a/src/test/subscription/t/032_subscribe_use_index.pl b/src/test/subscription/t/032_subscribe_use_index.pl index 834c9ad53375f..c755c1a7518dc 100644 --- a/src/test/subscription/t/032_subscribe_use_index.pl +++ b/src/test/subscription/t/032_subscribe_use_index.pl @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # Test logical replication behavior with subscriber using available index use strict; diff --git a/src/test/subscription/t/033_run_as_table_owner.pl b/src/test/subscription/t/033_run_as_table_owner.pl index c6d690009c9ef..f1bf8f430b67f 100644 --- a/src/test/subscription/t/033_run_as_table_owner.pl +++ b/src/test/subscription/t/033_run_as_table_owner.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Test that logical replication respects permissions use strict; diff --git a/src/test/subscription/t/034_temporal.pl b/src/test/subscription/t/034_temporal.pl index 6bbf65672790e..f572a105a0faa 100644 --- a/src/test/subscription/t/034_temporal.pl +++ b/src/test/subscription/t/034_temporal.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # Logical replication tests for temporal tables # @@ -137,6 +137,7 @@ () qq(psql::1: ERROR: cannot update table "temporal_no_key" because it does not have a replica identity and publishes updates HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.), "can't UPDATE temporal_no_key DEFAULT"); +# No need to test again with FOR PORTION OF ($result, $stdout, $stderr) = $node_publisher->psql('postgres', "DELETE FROM temporal_no_key WHERE id = '[3,4)'"); @@ -144,6 +145,13 @@ () qq(psql::1: ERROR: cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), "can't DELETE temporal_no_key DEFAULT"); +($result, $stdout, $stderr) = $node_publisher->psql('postgres', + "DELETE FROM temporal_no_key FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); +is( $stderr, + qq(psql::1: ERROR: cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes +HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), + "can't DELETE FOR PORTION OF temporal_no_key DEFAULT"); $node_publisher->wait_for_catchup('sub1'); @@ -165,16 +173,24 @@ () $node_publisher->safe_psql('postgres', "UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'"); +$node_publisher->safe_psql('postgres', + "UPDATE temporal_pk FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'" +); $node_publisher->safe_psql('postgres', "DELETE FROM temporal_pk WHERE id = '[3,4)'"); +$node_publisher->safe_psql('postgres', + "DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); $node_publisher->wait_for_catchup('sub1'); $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM temporal_pk ORDER BY id, valid_at"); is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a -[2,3)|[2000-01-01,2010-01-01)|b +[2,3)|[2000-01-01,2001-01-01)|b +[2,3)|[2001-01-01,2002-01-01)|c +[2,3)|[2003-01-01,2010-01-01)|b [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk DEFAULT'); # replicate with a unique key: @@ -192,6 +208,7 @@ () qq(psql::1: ERROR: cannot update table "temporal_unique" because it does not have a replica identity and publishes updates HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.), "can't UPDATE temporal_unique DEFAULT"); +# No need to test again with FOR PORTION OF ($result, $stdout, $stderr) = $node_publisher->psql('postgres', "DELETE FROM temporal_unique WHERE id = '[3,4)'"); @@ -199,6 +216,13 @@ () qq(psql::1: ERROR: cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), "can't DELETE temporal_unique DEFAULT"); +($result, $stdout, $stderr) = $node_publisher->psql('postgres', + "DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); +is( $stderr, + qq(psql::1: ERROR: cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes +HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), + "can't DELETE FOR PORTION OF temporal_unique DEFAULT"); $node_publisher->wait_for_catchup('sub1'); @@ -287,16 +311,24 @@ () $node_publisher->safe_psql('postgres', "UPDATE temporal_no_key SET a = 'b' WHERE id = '[2,3)'"); +$node_publisher->safe_psql('postgres', + "UPDATE temporal_no_key FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'" +); $node_publisher->safe_psql('postgres', "DELETE FROM temporal_no_key WHERE id = '[3,4)'"); +$node_publisher->safe_psql('postgres', + "DELETE FROM temporal_no_key FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); $node_publisher->wait_for_catchup('sub1'); $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM temporal_no_key ORDER BY id, valid_at"); is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a -[2,3)|[2000-01-01,2010-01-01)|b +[2,3)|[2000-01-01,2001-01-01)|b +[2,3)|[2001-01-01,2002-01-01)|c +[2,3)|[2003-01-01,2010-01-01)|b [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_no_key FULL'); # replicate with a primary key: @@ -310,16 +342,24 @@ () $node_publisher->safe_psql('postgres', "UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'"); +$node_publisher->safe_psql('postgres', + "UPDATE temporal_pk FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'" +); $node_publisher->safe_psql('postgres', "DELETE FROM temporal_pk WHERE id = '[3,4)'"); +$node_publisher->safe_psql('postgres', + "DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); $node_publisher->wait_for_catchup('sub1'); $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM temporal_pk ORDER BY id, valid_at"); is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a -[2,3)|[2000-01-01,2010-01-01)|b +[2,3)|[2000-01-01,2001-01-01)|b +[2,3)|[2001-01-01,2002-01-01)|c +[2,3)|[2003-01-01,2010-01-01)|b [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk FULL'); # replicate with a unique key: @@ -333,16 +373,24 @@ () $node_publisher->safe_psql('postgres', "UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'"); +$node_publisher->safe_psql('postgres', + "UPDATE temporal_unique FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'" +); $node_publisher->safe_psql('postgres', "DELETE FROM temporal_unique WHERE id = '[3,4)'"); +$node_publisher->safe_psql('postgres', + "DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); $node_publisher->wait_for_catchup('sub1'); $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM temporal_unique ORDER BY id, valid_at"); is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a -[2,3)|[2000-01-01,2010-01-01)|b +[2,3)|[2000-01-01,2001-01-01)|b +[2,3)|[2001-01-01,2002-01-01)|c +[2,3)|[2003-01-01,2010-01-01)|b [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique FULL'); # cleanup @@ -425,16 +473,24 @@ () $node_publisher->safe_psql('postgres', "UPDATE temporal_pk SET a = 'b' WHERE id = '[2,3)'"); +$node_publisher->safe_psql('postgres', + "UPDATE temporal_pk FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'" +); $node_publisher->safe_psql('postgres', "DELETE FROM temporal_pk WHERE id = '[3,4)'"); +$node_publisher->safe_psql('postgres', + "DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); $node_publisher->wait_for_catchup('sub1'); $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM temporal_pk ORDER BY id, valid_at"); is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a -[2,3)|[2000-01-01,2010-01-01)|b +[2,3)|[2000-01-01,2001-01-01)|b +[2,3)|[2001-01-01,2002-01-01)|c +[2,3)|[2003-01-01,2010-01-01)|b [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_pk USING INDEX'); # replicate with a unique key: @@ -448,16 +504,24 @@ () $node_publisher->safe_psql('postgres', "UPDATE temporal_unique SET a = 'b' WHERE id = '[2,3)'"); +$node_publisher->safe_psql('postgres', + "UPDATE temporal_unique FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' SET a = 'c' WHERE id = '[2,3)'" +); $node_publisher->safe_psql('postgres', "DELETE FROM temporal_unique WHERE id = '[3,4)'"); +$node_publisher->safe_psql('postgres', + "DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); $node_publisher->wait_for_catchup('sub1'); $result = $node_subscriber->safe_psql('postgres', "SELECT * FROM temporal_unique ORDER BY id, valid_at"); is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a -[2,3)|[2000-01-01,2010-01-01)|b +[2,3)|[2000-01-01,2001-01-01)|b +[2,3)|[2001-01-01,2002-01-01)|c +[2,3)|[2003-01-01,2010-01-01)|b [4,5)|[2000-01-01,2010-01-01)|a}, 'replicated temporal_unique USING INDEX'); # cleanup @@ -543,6 +607,7 @@ () qq(psql::1: ERROR: cannot update table "temporal_no_key" because it does not have a replica identity and publishes updates HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.), "can't UPDATE temporal_no_key NOTHING"); +# No need to test again with FOR PORTION OF ($result, $stdout, $stderr) = $node_publisher->psql('postgres', "DELETE FROM temporal_no_key WHERE id = '[3,4)'"); @@ -550,6 +615,13 @@ () qq(psql::1: ERROR: cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), "can't DELETE temporal_no_key NOTHING"); +($result, $stdout, $stderr) = $node_publisher->psql('postgres', + "DELETE FROM temporal_no_key FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); +is( $stderr, + qq(psql::1: ERROR: cannot delete from table "temporal_no_key" because it does not have a replica identity and publishes deletes +HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), + "can't DELETE temporal_no_key NOTHING"); $node_publisher->wait_for_catchup('sub1'); @@ -575,6 +647,7 @@ () qq(psql::1: ERROR: cannot update table "temporal_pk" because it does not have a replica identity and publishes updates HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.), "can't UPDATE temporal_pk NOTHING"); +# No need to test again with FOR PORTION OF ($result, $stdout, $stderr) = $node_publisher->psql('postgres', "DELETE FROM temporal_pk WHERE id = '[3,4)'"); @@ -582,6 +655,13 @@ () qq(psql::1: ERROR: cannot delete from table "temporal_pk" because it does not have a replica identity and publishes deletes HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), "can't DELETE temporal_pk NOTHING"); +($result, $stdout, $stderr) = $node_publisher->psql('postgres', + "DELETE FROM temporal_pk FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); +is( $stderr, + qq(psql::1: ERROR: cannot delete from table "temporal_pk" because it does not have a replica identity and publishes deletes +HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), + "can't DELETE temporal_pk NOTHING"); $node_publisher->wait_for_catchup('sub1'); @@ -607,6 +687,7 @@ () qq(psql::1: ERROR: cannot update table "temporal_unique" because it does not have a replica identity and publishes updates HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.), "can't UPDATE temporal_unique NOTHING"); +# No need to test again with FOR PORTION OF ($result, $stdout, $stderr) = $node_publisher->psql('postgres', "DELETE FROM temporal_unique WHERE id = '[3,4)'"); @@ -614,6 +695,13 @@ () qq(psql::1: ERROR: cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), "can't DELETE temporal_unique NOTHING"); +($result, $stdout, $stderr) = $node_publisher->psql('postgres', + "DELETE FROM temporal_unique FOR PORTION OF valid_at FROM '2002-01-01' TO '2003-01-01' WHERE id = '[2,3)'" +); +is( $stderr, + qq(psql::1: ERROR: cannot delete from table "temporal_unique" because it does not have a replica identity and publishes deletes +HINT: To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.), + "can't DELETE FOR PORTION OF temporal_unique NOTHING"); $node_publisher->wait_for_catchup('sub1'); diff --git a/src/test/subscription/t/035_conflicts.pl b/src/test/subscription/t/035_conflicts.pl index 2a7a8239a2966..426ad74cf33aa 100644 --- a/src/test/subscription/t/035_conflicts.pl +++ b/src/test/subscription/t/035_conflicts.pl @@ -1,6 +1,6 @@ -# Copyright (c) 2025, PostgreSQL Global Development Group +# Copyright (c) 2025-2026, PostgreSQL Global Development Group -# Test the conflict detection of conflict type 'multiple_unique_conflicts'. +# Test conflicts in logical replication use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Cluster; @@ -18,7 +18,7 @@ # Create a subscriber node my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); -$node_subscriber->init; +$node_subscriber->init(allows_streaming => 'logical'); $node_subscriber->start; # Create a table on publisher @@ -26,7 +26,8 @@ "CREATE TABLE conf_tab (a int PRIMARY KEY, b int UNIQUE, c int UNIQUE);"); $node_publisher->safe_psql('postgres', - "CREATE TABLE conf_tab_2 (a int PRIMARY KEY, b int UNIQUE, c int UNIQUE);"); + "CREATE TABLE conf_tab_2 (a int PRIMARY KEY, b int UNIQUE, c int UNIQUE);" +); # Create same table on subscriber $node_subscriber->safe_psql('postgres', @@ -77,12 +78,10 @@ # Confirm that this causes an error on the subscriber $node_subscriber->wait_for_log( qr/conflict detected on relation \"public.conf_tab\": conflict=multiple_unique_conflicts.* -.*Key already exists in unique index \"conf_tab_pkey\".* -.*Key \(a\)=\(2\); existing local tuple \(2, 2, 2\); remote tuple \(2, 3, 4\).* -.*Key already exists in unique index \"conf_tab_b_key\".* -.*Key \(b\)=\(3\); existing local tuple \(3, 3, 3\); remote tuple \(2, 3, 4\).* -.*Key already exists in unique index \"conf_tab_c_key\".* -.*Key \(c\)=\(4\); existing local tuple \(4, 4, 4\); remote tuple \(2, 3, 4\)./, +.*Could not apply remote change: remote row \(2, 3, 4\).* +.*Key already exists in unique index \"conf_tab_pkey\", modified in transaction .*: key \(a\)=\(2\), local row \(2, 2, 2\).* +.*Key already exists in unique index \"conf_tab_b_key\", modified in transaction .*: key \(b\)=\(3\), local row \(3, 3, 3\).* +.*Key already exists in unique index \"conf_tab_c_key\", modified in transaction .*: key \(c\)=\(4\), local row \(4, 4, 4\)./, $log_offset); pass('multiple_unique_conflicts detected during insert'); @@ -109,12 +108,10 @@ # Confirm that this causes an error on the subscriber $node_subscriber->wait_for_log( qr/conflict detected on relation \"public.conf_tab\": conflict=multiple_unique_conflicts.* -.*Key already exists in unique index \"conf_tab_pkey\".* -.*Key \(a\)=\(6\); existing local tuple \(6, 6, 6\); remote tuple \(6, 7, 8\).* -.*Key already exists in unique index \"conf_tab_b_key\".* -.*Key \(b\)=\(7\); existing local tuple \(7, 7, 7\); remote tuple \(6, 7, 8\).* -.*Key already exists in unique index \"conf_tab_c_key\".* -.*Key \(c\)=\(8\); existing local tuple \(8, 8, 8\); remote tuple \(6, 7, 8\)./, +.*Could not apply remote change: remote row \(6, 7, 8\), replica identity \(a\)=\(5\).* +.*Key already exists in unique index \"conf_tab_pkey\", modified in transaction .*: key \(a\)=\(6\), local row \(6, 6, 6\).* +.*Key already exists in unique index \"conf_tab_b_key\", modified in transaction .*: key \(b\)=\(7\), local row \(7, 7, 7\).* +.*Key already exists in unique index \"conf_tab_c_key\", modified in transaction .*: key \(c\)=\(8\), local row \(8, 8, 8\)./, $log_offset); pass('multiple_unique_conflicts detected during update'); @@ -137,12 +134,544 @@ $node_subscriber->wait_for_log( qr/conflict detected on relation \"public.conf_tab_2_p1\": conflict=multiple_unique_conflicts.* -.*Key already exists in unique index \"conf_tab_2_p1_pkey\".* -.*Key \(a\)=\(55\); existing local tuple \(55, 2, 3\); remote tuple \(55, 2, 3\).* -.*Key already exists in unique index \"conf_tab_2_p1_a_b_key\".* -.*Key \(a, b\)=\(55, 2\); existing local tuple \(55, 2, 3\); remote tuple \(55, 2, 3\)./, +.*Could not apply remote change: remote row \(55, 2, 3\).* +.*Key already exists in unique index \"conf_tab_2_p1_pkey\", modified in transaction .*: key \(a\)=\(55\), local row \(55, 2, 3\).* +.*Key already exists in unique index \"conf_tab_2_p1_a_b_key\", modified in transaction .*: key \(a, b\)=\(55, 2\), local row \(55, 2, 3\)./, $log_offset); pass('multiple_unique_conflicts detected on a leaf partition during insert'); +############################################################################### +# Setup a bidirectional logical replication between node_A & node_B +############################################################################### + +# Initialize nodes. Enable the track_commit_timestamp on both nodes to detect +# the conflict when attempting to update a row that was previously modified by +# a different origin. + +# node_A. Increase the log_min_messages setting to DEBUG2 to debug test +# failures. Disable autovacuum to avoid generating xid that could affect the +# replication slot's xmin value. +my $node_A = $node_publisher; +$node_A->append_conf( + 'postgresql.conf', + qq{track_commit_timestamp = on + autovacuum = off + log_min_messages = 'debug2'}); +$node_A->restart; + +# node_B +my $node_B = $node_subscriber; +$node_B->append_conf('postgresql.conf', "track_commit_timestamp = on"); +$node_B->restart; + +# Create table on node_A +$node_A->safe_psql('postgres', "CREATE TABLE tab (a int PRIMARY KEY, b int)"); + +# Create the same table on node_B +$node_B->safe_psql('postgres', "CREATE TABLE tab (a int PRIMARY KEY, b int)"); + +my $subname_AB = 'tap_sub_a_b'; +my $subname_BA = 'tap_sub_b_a'; + +# Setup logical replication +# node_A (pub) -> node_B (sub) +my $node_A_connstr = $node_A->connstr . ' dbname=postgres'; +$node_A->safe_psql('postgres', "CREATE PUBLICATION tap_pub_A FOR TABLE tab"); +$node_B->safe_psql( + 'postgres', " + CREATE SUBSCRIPTION $subname_BA + CONNECTION '$node_A_connstr application_name=$subname_BA' + PUBLICATION tap_pub_A + WITH (origin = none, retain_dead_tuples = true)"); + +# node_B (pub) -> node_A (sub) +my $node_B_connstr = $node_B->connstr . ' dbname=postgres'; +$node_B->safe_psql('postgres', "CREATE PUBLICATION tap_pub_B FOR TABLE tab"); +$node_A->safe_psql( + 'postgres', " + CREATE SUBSCRIPTION $subname_AB + CONNECTION '$node_B_connstr application_name=$subname_AB' + PUBLICATION tap_pub_B + WITH (origin = none, copy_data = off)"); + +# Wait for initial table sync to finish +$node_A->wait_for_subscription_sync($node_B, $subname_AB); +$node_B->wait_for_subscription_sync($node_A, $subname_BA); + +is(1, 1, 'Bidirectional replication setup is complete'); + +# Confirm that the conflict detection slot is created on Node B and the xmin +# value is valid. +ok( $node_B->poll_query_until( + 'postgres', + "SELECT xmin IS NOT NULL from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is valid on Node B"); + +################################################## +# Check that the retain_dead_tuples option can be enabled only for disabled +# subscriptions. Validate the NOTICE message during the subscription DDL, and +# ensure the conflict detection slot is created upon enabling the +# retain_dead_tuples option. +################################################## + +# Alter retain_dead_tuples for enabled subscription +my ($cmdret, $stdout, $stderr) = $node_A->psql('postgres', + "ALTER SUBSCRIPTION $subname_AB SET (retain_dead_tuples = true)"); +like( + $stderr, + qr/ERROR: cannot set option \"retain_dead_tuples\" for enabled subscription/, + "altering retain_dead_tuples is not allowed for enabled subscription"); + +# Disable the subscription +$node_A->psql('postgres', "ALTER SUBSCRIPTION $subname_AB DISABLE;"); + +# Wait for the apply worker to stop +$node_A->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE backend_type = 'logical replication apply worker'" +); + +# Enable retain_dead_tuples for disabled subscription +($cmdret, $stdout, $stderr) = $node_A->psql('postgres', + "ALTER SUBSCRIPTION $subname_AB SET (retain_dead_tuples = true);"); +like( + $stderr, + qr/NOTICE: deleted rows to detect conflicts would not be removed until the subscription is enabled/, + "altering retain_dead_tuples is allowed for disabled subscription"); + +# Re-enable the subscription +$node_A->safe_psql('postgres', "ALTER SUBSCRIPTION $subname_AB ENABLE;"); + +# Confirm that the conflict detection slot is created on Node A and the xmin +# value is valid. +ok( $node_A->poll_query_until( + 'postgres', + "SELECT xmin IS NOT NULL from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is valid on Node A"); + +################################################## +# Check the WARNING when changing the origin to ANY, if retain_dead_tuples is +# enabled. This warns of the possibility of receiving changes from origins +# other than the publisher. +################################################## + +($cmdret, $stdout, $stderr) = $node_A->psql('postgres', + "ALTER SUBSCRIPTION $subname_AB SET (origin = any);"); +like( + $stderr, + qr/WARNING: subscription "tap_sub_a_b" enabled retain_dead_tuples but might not reliably detect conflicts for changes from different origins/, + "warn of the possibility of receiving changes from origins other than the publisher" +); + +# Reset the origin to none +$node_A->psql('postgres', + "ALTER SUBSCRIPTION $subname_AB SET (origin = none);"); + +############################################################################### +# Check that dead tuples on node A cannot be cleaned by VACUUM until the +# concurrent transactions on Node B have been applied and flushed on Node A. +# Also, check that an update_deleted conflict is detected when updating a row +# that was deleted by a different origin. +############################################################################### + +# Insert a record +$node_A->safe_psql('postgres', "INSERT INTO tab VALUES (1, 1), (2, 2);"); +$node_A->wait_for_catchup($subname_BA); + +my $result = $node_B->safe_psql('postgres', "SELECT * FROM tab;"); +is($result, qq(1|1 +2|2), 'check replicated insert on node B'); + +# Disable the logical replication from node B to node A +$node_A->safe_psql('postgres', "ALTER SUBSCRIPTION $subname_AB DISABLE"); + +# Wait for the apply worker to stop +$node_A->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE backend_type = 'logical replication apply worker'" +); + +my $log_location = -s $node_B->logfile; + +$node_B->safe_psql('postgres', "UPDATE tab SET b = 3 WHERE a = 1;"); +$node_A->safe_psql('postgres', "DELETE FROM tab WHERE a = 1;"); + +($cmdret, $stdout, $stderr) = $node_A->psql( + 'postgres', qq(VACUUM (verbose) public.tab;) +); + +like( + $stderr, + qr/1 are dead but not yet removable/, + 'the deleted column is non-removable'); + +# Ensure the DELETE is replayed on Node B +$node_A->wait_for_catchup($subname_BA); + +# Check the conflict detected on Node B +my $logfile = slurp_file($node_B->logfile(), $log_location); +like( + $logfile, + qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.* +.*DETAIL:.* Deleting the row that was modified locally in transaction [0-9]+ at .*: local row \(1, 3\), replica identity \(a\)=\(1\)./, + 'delete target row was modified in tab'); + +$log_location = -s $node_A->logfile; + +$node_A->safe_psql( + 'postgres', "ALTER SUBSCRIPTION $subname_AB ENABLE;"); +$node_B->wait_for_catchup($subname_AB); + +$logfile = slurp_file($node_A->logfile(), $log_location); +like( + $logfile, + qr/conflict detected on relation "public.tab": conflict=update_deleted.* +.*DETAIL:.* Could not find the row to be updated: remote row \(1, 3\), replica identity \(a\)=\(1\). +.*The row to be updated was deleted locally in transaction [0-9]+ at .*/, + 'update target row was deleted in tab'); + +# Remember the next transaction ID to be assigned +my $next_xid = $node_A->safe_psql('postgres', "SELECT txid_current() + 1;"); + +# Confirm that the xmin value is advanced to the latest nextXid. If no +# transactions are running, the apply worker selects nextXid as the candidate +# for the non-removable xid. See GetOldestActiveTransactionId(). +ok( $node_A->poll_query_until( + 'postgres', + "SELECT xmin = $next_xid from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is updated on Node A"); + +############################################################################### +# Ensure that the deleted tuple needed to detect an update_deleted conflict is +# accessible via a sequential table scan. +############################################################################### + +# Drop the primary key from tab on node A and set REPLICA IDENTITY to FULL to +# enforce sequential scanning of the table. +$node_A->safe_psql('postgres', "ALTER TABLE tab REPLICA IDENTITY FULL"); +$node_B->safe_psql('postgres', "ALTER TABLE tab REPLICA IDENTITY FULL"); +$node_A->safe_psql('postgres', "ALTER TABLE tab DROP CONSTRAINT tab_pkey;"); + +# Disable the logical replication from node B to node A +$node_A->safe_psql('postgres', "ALTER SUBSCRIPTION $subname_AB DISABLE"); + +# Wait for the apply worker to stop +$node_A->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE backend_type = 'logical replication apply worker'" +); + +$node_B->safe_psql('postgres', "UPDATE tab SET b = 4 WHERE a = 2;"); +$node_A->safe_psql('postgres', "DELETE FROM tab WHERE a = 2;"); + +$log_location = -s $node_A->logfile; + +$node_A->safe_psql( + 'postgres', "ALTER SUBSCRIPTION $subname_AB ENABLE;"); +$node_B->wait_for_catchup($subname_AB); + +$logfile = slurp_file($node_A->logfile(), $log_location); +like( + $logfile, + qr/conflict detected on relation "public.tab": conflict=update_deleted.* +.*DETAIL:.* Could not find the row to be updated: remote row \(2, 4\), replica identity full \(2, 2\).* +.*The row to be updated was deleted locally in transaction [0-9]+ at .*/, + 'update target row was deleted in tab'); + +############################################################################### +# Check that the xmin value of the conflict detection slot can be advanced when +# the subscription has no tables. +############################################################################### + +# Remove the table from the publication +$node_B->safe_psql('postgres', "ALTER PUBLICATION tap_pub_B DROP TABLE tab"); + +$node_A->safe_psql('postgres', + "ALTER SUBSCRIPTION $subname_AB REFRESH PUBLICATION"); + +# Remember the next transaction ID to be assigned +$next_xid = $node_A->safe_psql('postgres', "SELECT txid_current() + 1;"); + +# Confirm that the xmin value is advanced to the latest nextXid. If no +# transactions are running, the apply worker selects nextXid as the candidate +# for the non-removable xid. See GetOldestActiveTransactionId(). +ok( $node_A->poll_query_until( + 'postgres', + "SELECT xmin = $next_xid from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is updated on Node A"); + +# Re-add the table to the publication for further tests +$node_B->safe_psql('postgres', "ALTER PUBLICATION tap_pub_B ADD TABLE tab"); + +$node_A->safe_psql('postgres', + "ALTER SUBSCRIPTION $subname_AB REFRESH PUBLICATION WITH (copy_data = false)"); + +############################################################################### +# Test that publisher's transactions marked with DELAY_CHKPT_IN_COMMIT prevent +# concurrently deleted tuples on the subscriber from being removed. This test +# also acts as a safeguard to prevent developers from moving the commit +# timestamp acquisition before marking DELAY_CHKPT_IN_COMMIT in +# RecordTransactionCommitPrepared. +############################################################################### + +my $injection_points_supported = $node_B->check_extension('injection_points'); + +# This test depends on an injection point to block the prepared transaction +# commit after marking DELAY_CHKPT_IN_COMMIT flag. +if ($injection_points_supported != 0) +{ + $node_B->append_conf('postgresql.conf', + "shared_preload_libraries = 'injection_points' + max_prepared_transactions = 1"); + $node_B->restart; + + # Disable the subscription on Node B for testing only one-way + # replication. + $node_B->psql('postgres', "ALTER SUBSCRIPTION $subname_BA DISABLE;"); + + # Wait for the apply worker to stop + $node_B->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity WHERE backend_type = 'logical replication apply worker'" + ); + + # Truncate the table to cleanup existing dead rows in the table. Then insert + # a new row. + $node_B->safe_psql( + 'postgres', qq( + TRUNCATE tab; + INSERT INTO tab VALUES(1, 1); + )); + + $node_B->wait_for_catchup($subname_AB); + + # Create the injection_points extension on the publisher node and attach to the + # commit-after-delay-checkpoint injection point. + $node_B->safe_psql( + 'postgres', + "CREATE EXTENSION injection_points; + SELECT injection_points_attach('commit-after-delay-checkpoint', 'wait');" + ); + + # Start a background session on the publisher node to perform an update and + # pause at the injection point. + my $pub_session = $node_B->background_psql('postgres'); + $pub_session->query_until( + qr/starting_bg_psql/, + q{ + \echo starting_bg_psql + BEGIN; + UPDATE tab SET b = 2 WHERE a = 1; + PREPARE TRANSACTION 'txn_with_later_commit_ts'; + COMMIT PREPARED 'txn_with_later_commit_ts'; + } + ); + + # Wait until the backend enters the injection point + $node_B->wait_for_event('client backend', 'commit-after-delay-checkpoint'); + + # Confirm the update is suspended + $result = + $node_B->safe_psql('postgres', 'SELECT * FROM tab WHERE a = 1'); + is($result, qq(1|1), 'publisher sees the old row'); + + # Delete the row on the subscriber. The deleted row should be retained due to a + # transaction on the publisher, which is currently marked with the + # DELAY_CHKPT_IN_COMMIT flag. + $node_A->safe_psql('postgres', "DELETE FROM tab WHERE a = 1;"); + + # Get the commit timestamp for the delete + my $sub_ts = $node_A->safe_psql('postgres', + "SELECT timestamp FROM pg_last_committed_xact();"); + + $log_location = -s $node_A->logfile; + + # Confirm that the apply worker keeps requesting publisher status, while + # awaiting the prepared transaction to commit. Thus, the request log should + # appear more than once. + $node_A->wait_for_log( + qr/sending publisher status request message/, + $log_location); + + $log_location = -s $node_A->logfile; + + $node_A->wait_for_log( + qr/sending publisher status request message/, + $log_location); + + # Confirm that the dead tuple cannot be removed + ($cmdret, $stdout, $stderr) = + $node_A->psql('postgres', qq(VACUUM (verbose) public.tab;)); + + like( + $stderr, + qr/1 are dead but not yet removable/, + 'the deleted column is non-removable'); + + $log_location = -s $node_A->logfile; + + # Wakeup and detach the injection point on the publisher node. The prepared + # transaction should now commit. + $node_B->safe_psql( + 'postgres', + "SELECT injection_points_wakeup('commit-after-delay-checkpoint'); + SELECT injection_points_detach('commit-after-delay-checkpoint');" + ); + + # Close the background session on the publisher node + ok($pub_session->quit, "close publisher session"); + + # Confirm that the transaction committed + $result = + $node_B->safe_psql('postgres', 'SELECT * FROM tab WHERE a = 1'); + is($result, qq(1|2), 'publisher sees the new row'); + + # Ensure the UPDATE is replayed on subscriber + $node_B->wait_for_catchup($subname_AB); + + $logfile = slurp_file($node_A->logfile(), $log_location); + like( + $logfile, + qr/conflict detected on relation "public.tab": conflict=update_deleted.* +.*DETAIL:.* Could not find the row to be updated: remote row \(1, 2\), replica identity full \(1, 1\).* +.*The row to be updated was deleted locally in transaction [0-9]+ at .*/, + 'update target row was deleted in tab'); + + # Remember the next transaction ID to be assigned + $next_xid = + $node_A->safe_psql('postgres', "SELECT txid_current() + 1;"); + + # Confirm that the xmin value is advanced to the latest nextXid after the + # prepared transaction on the publisher has been committed. + ok( $node_A->poll_query_until( + 'postgres', + "SELECT xmin = $next_xid from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is updated on subscriber" + ); + + # Get the commit timestamp for the publisher's update + my $pub_ts = $node_B->safe_psql('postgres', + "SELECT pg_xact_commit_timestamp(xmin) from tab where a=1;"); + + # Check that the commit timestamp for the update on the publisher is later than + # or equal to the timestamp of the local deletion, as the commit timestamp + # should be assigned after marking the DELAY_CHKPT_IN_COMMIT flag. + $result = $node_B->safe_psql('postgres', + "SELECT '$pub_ts'::timestamp >= '$sub_ts'::timestamp"); + is($result, qq(t), + "pub UPDATE's timestamp is later than that of sub's DELETE"); + + # Re-enable the subscription for further tests + $node_B->psql('postgres', "ALTER SUBSCRIPTION $subname_BA ENABLE;"); +} + +############################################################################### +# Check that dead tuple retention stops due to the wait time surpassing +# max_retention_duration. +############################################################################### + +# Create a physical slot +$node_B->safe_psql('postgres', + "SELECT * FROM pg_create_physical_replication_slot('blocker');"); + +# Add the inactive physical slot to synchronized_standby_slots +$node_B->append_conf('postgresql.conf', + "synchronized_standby_slots = 'blocker'"); +$node_B->reload; + +# Enable failover to activate the synchronized_standby_slots setting +$node_A->safe_psql('postgres', "ALTER SUBSCRIPTION $subname_AB DISABLE;"); +$node_A->safe_psql('postgres', "ALTER SUBSCRIPTION $subname_AB SET (failover = true);"); +$node_A->safe_psql('postgres', "ALTER SUBSCRIPTION $subname_AB ENABLE;"); + +# Insert a record +$node_B->safe_psql('postgres', "INSERT INTO tab VALUES (5, 5);"); + +# Advance the xid on Node A to trigger the next cycle of oldest_nonremovable_xid +# advancement. +$node_A->safe_psql('postgres', "SELECT txid_current() + 1;"); + +$log_offset = -s $node_A->logfile; + +# Set max_retention_duration to a minimal value to initiate retention stop. +$node_A->safe_psql('postgres', + "ALTER SUBSCRIPTION $subname_AB SET (max_retention_duration = 1);"); + +# Confirm that the retention is stopped +$node_A->wait_for_log( + qr/logical replication worker for subscription "tap_sub_a_b" has stopped retaining the information for detecting conflicts/, + $log_offset); + +ok( $node_A->poll_query_until( + 'postgres', + "SELECT xmin IS NULL from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is invalid on Node A"); + +$result = $node_A->safe_psql('postgres', + "SELECT subretentionactive FROM pg_subscription WHERE subname='$subname_AB';"); +is($result, qq(f), 'retention is inactive'); + +############################################################################### +# Check that dead tuple retention resumes when the max_retention_duration is set +# 0. +############################################################################### + +$log_offset = -s $node_A->logfile; + +# Set max_retention_duration to 0 +$node_A->safe_psql('postgres', + "ALTER SUBSCRIPTION $subname_AB SET (max_retention_duration = 0);"); + +# Drop the physical slot and reset the synchronized_standby_slots setting. We +# change this after setting max_retention_duration to 0, ensuring consistent +# results in the test as the resumption becomes possible immediately after +# resetting synchronized_standby_slots, due to the smaller max_retention_duration +# value of 1ms. +$node_B->safe_psql('postgres', + "SELECT * FROM pg_drop_replication_slot('blocker');"); +$node_B->adjust_conf('postgresql.conf', 'synchronized_standby_slots', "''"); +$node_B->reload; + +# Confirm that the retention resumes +$node_A->wait_for_log( + qr/logical replication worker for subscription "tap_sub_a_b" will resume retaining the information for detecting conflicts +.*DETAIL:.* Retention is re-enabled because max_retention_duration has been set to unlimited.*/, + $log_offset); + +ok( $node_A->poll_query_until( + 'postgres', + "SELECT xmin IS NOT NULL from pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the xmin value of slot 'pg_conflict_detection' is valid on Node A"); + +$result = $node_A->safe_psql('postgres', + "SELECT subretentionactive FROM pg_subscription WHERE subname='$subname_AB';"); +is($result, qq(t), 'retention is active'); + +############################################################################### +# Check that the replication slot pg_conflict_detection is dropped after +# removing all the subscriptions. +############################################################################### + +$node_B->safe_psql( + 'postgres', "DROP SUBSCRIPTION $subname_BA"); + +ok( $node_B->poll_query_until( + 'postgres', + "SELECT count(*) = 0 FROM pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the slot 'pg_conflict_detection' has been dropped on Node B"); + +$node_A->safe_psql( + 'postgres', "DROP SUBSCRIPTION $subname_AB"); + +ok( $node_A->poll_query_until( + 'postgres', + "SELECT count(*) = 0 FROM pg_replication_slots WHERE slot_name = 'pg_conflict_detection'" + ), + "the slot 'pg_conflict_detection' has been dropped on Node A"); + done_testing(); diff --git a/src/test/subscription/t/036_sequences.pl b/src/test/subscription/t/036_sequences.pl new file mode 100644 index 0000000000000..471780a358582 --- /dev/null +++ b/src/test/subscription/t/036_sequences.pl @@ -0,0 +1,224 @@ + +# Copyright (c) 2025-2026, PostgreSQL Global Development Group + +# This tests that sequences are synced correctly to the subscriber +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +# Initialize subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Setup structure on the publisher +my $ddl = qq( + CREATE TABLE regress_seq_test (v BIGINT); + CREATE SEQUENCE regress_s1; + CREATE SEQUENCE "regress'quote"; +); +$node_publisher->safe_psql('postgres', $ddl); + +# Setup the same structure on the subscriber, plus some extra sequences that +# we'll create on the publisher later +$ddl = qq( + CREATE TABLE regress_seq_test (v BIGINT); + CREATE SEQUENCE regress_s1; + CREATE SEQUENCE regress_s2; + CREATE SEQUENCE regress_s3; + CREATE SEQUENCE "regress'quote"; +); +$node_subscriber->safe_psql('postgres', $ddl); + +# Insert initial test data +$node_publisher->safe_psql( + 'postgres', qq( + -- generate a number of values using the sequence + INSERT INTO regress_seq_test SELECT nextval('regress_s1') FROM generate_series(1,100); + INSERT INTO regress_seq_test SELECT nextval('"regress''quote"') FROM generate_series(1,100); +)); + +# Setup logical replication pub/sub +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION regress_seq_pub FOR ALL SEQUENCES"); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION regress_seq_sub CONNECTION '$publisher_connstr' PUBLICATION regress_seq_pub" +); + +# Wait for initial sync to finish +my $synced_query = + "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r');"; +$node_subscriber->poll_query_until('postgres', $synced_query) + or die "Timed out while waiting for subscriber to synchronize data"; + +# Check the initial data on subscriber +my $result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s1; +)); +is($result, '100|t', 'initial test data replicated'); + +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM "regress'quote"; +)); +is($result, '100|t', + 'initial test data replicated for sequence name having quotes'); + +########## +## ALTER SUBSCRIPTION ... REFRESH PUBLICATION should cause sync of new +# sequences of the publisher, but changes to existing sequences should +# not be synced. +########## + +# Create a new sequence 'regress_s2', and update existing sequence 'regress_s1' +$node_publisher->safe_psql( + 'postgres', qq( + CREATE SEQUENCE regress_s2; + INSERT INTO regress_seq_test SELECT nextval('regress_s2') FROM generate_series(1,100); + + -- Existing sequence + INSERT INTO regress_seq_test SELECT nextval('regress_s1') FROM generate_series(1,100); +)); + +# Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION +$result = $node_subscriber->safe_psql( + 'postgres', qq( + ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION; +)); +$node_subscriber->poll_query_until('postgres', $synced_query) + or die "Timed out while waiting for subscriber to synchronize data"; + +$result = $node_publisher->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s1; +)); +is($result, '200|t', 'Check sequence value in the publisher'); + +# Check - existing sequence ('regress_s1') is not synced +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s1; +)); +is($result, '100|t', 'REFRESH PUBLICATION will not sync existing sequence'); + +# Check - newly published sequence ('regress_s2') is synced +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s2; +)); +is($result, '100|t', + 'REFRESH PUBLICATION will sync newly published sequence'); + +########## +# Test: REFRESH SEQUENCES and REFRESH PUBLICATION (copy_data = false) +# +# 1. ALTER SUBSCRIPTION ... REFRESH SEQUENCES should re-synchronize all +# existing sequences, but not synchronize newly added ones. +# 2. ALTER SUBSCRIPTION ... REFRESH PUBLICATION with (copy_data = false) should +# also not update sequence values for newly added sequences. +########## + +# Create a new sequence 'regress_s3', and update the existing sequence +# 'regress_s2'. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE SEQUENCE regress_s3; + INSERT INTO regress_seq_test SELECT nextval('regress_s3') FROM generate_series(1,100); + + -- Existing sequence + INSERT INTO regress_seq_test SELECT nextval('regress_s2') FROM generate_series(1,100); +)); + +# 1. Do ALTER SUBSCRIPTION ... REFRESH SEQUENCES +$result = $node_subscriber->safe_psql( + 'postgres', qq( + ALTER SUBSCRIPTION regress_seq_sub REFRESH SEQUENCES; +)); +$node_subscriber->poll_query_until('postgres', $synced_query) + or die "Timed out while waiting for subscriber to synchronize data"; + +# Check - existing sequences ('regress_s1' and 'regress_s2') are synced +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s1; +)); +is($result, '200|t', 'REFRESH SEQUENCES will sync existing sequences'); +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s2; +)); +is($result, '200|t', 'REFRESH SEQUENCES will sync existing sequences'); + +# Check - newly published sequence ('regress_s3') is not synced +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s3; +)); +is($result, '1|f', + 'REFRESH SEQUENCES will not sync newly published sequence'); + +# 2. Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION with copy_data as false +$result = $node_subscriber->safe_psql( + 'postgres', qq( + ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION WITH (copy_data = false); +)); +$node_subscriber->poll_query_until('postgres', $synced_query) + or die "Timed out while waiting for subscriber to synchronize data"; + +# Check - newly published sequence ('regress_s3') is not synced with copy_data +# as false. +$result = $node_subscriber->safe_psql( + 'postgres', qq( + SELECT last_value, is_called FROM regress_s3; +)); +is($result, '1|f', + 'REFRESH PUBLICATION will not sync newly published sequence with copy_data as false' +); + +########## +# ALTER SUBSCRIPTION ... REFRESH PUBLICATION should report an error when: +# a) sequence definitions differ between the publisher and subscriber, or +# b) a sequence is missing on the publisher. +########## + +# Create a new sequence 'regress_s4' whose START value is not the same in the +# publisher and subscriber. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE SEQUENCE regress_s4 START 1 INCREMENT 2; +)); + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE SEQUENCE regress_s4 START 10 INCREMENT 2; +)); + +my $log_offset = -s $node_subscriber->logfile; + +# Do ALTER SUBSCRIPTION ... REFRESH PUBLICATION +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION regress_seq_sub REFRESH PUBLICATION"); + +# Verify that an error is logged for parameter differences on sequence +# ('regress_s4'). +$node_subscriber->wait_for_log( + qr/WARNING: ( [A-Z0-9]+:)? mismatched or renamed sequence on subscriber \("public.regress_s4"\)/, + $log_offset); + +# Verify that an error is logged for the missing sequence ('regress_s4'). +$node_publisher->safe_psql('postgres', qq(DROP SEQUENCE regress_s4;)); + +$node_subscriber->wait_for_log( + qr/WARNING: ( [A-Z0-9]+:)? missing sequence on publisher \("public.regress_s4"\)/, + $log_offset); + +done_testing(); diff --git a/src/test/subscription/t/037_except.pl b/src/test/subscription/t/037_except.pl new file mode 100644 index 0000000000000..8c58d282eeed0 --- /dev/null +++ b/src/test/subscription/t/037_except.pl @@ -0,0 +1,287 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Logical replication tests for publications with EXCEPT clause +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; + +# Initialize subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +my $result; + +sub test_except_root_partition +{ + my ($pubviaroot) = @_; + + # If the root partitioned table is in the EXCEPT clause, all its + # partitions are excluded from publication, regardless of the + # publish_via_partition_root setting. + $node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub_part FOR ALL TABLES EXCEPT (TABLE root1) WITH (publish_via_partition_root = $pubviaroot); + INSERT INTO root1 VALUES (1), (101); + )); + $node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub_part CONNECTION '$publisher_connstr' PUBLICATION tap_pub_part" + ); + $node_subscriber->wait_for_subscription_sync($node_publisher, + 'tap_sub_part'); + + # Advance the replication slot to ignore changes generated before this point. + $node_publisher->safe_psql('postgres', + "SELECT slot_name FROM pg_replication_slot_advance('test_slot', pg_current_wal_lsn())" + ); + $node_publisher->safe_psql('postgres', + "INSERT INTO root1 VALUES (2), (102)"); + + # Verify that data inserted into the partitioned table is not published when + # it is in the EXCEPT clause. + $result = $node_publisher->safe_psql('postgres', + "SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tap_pub_part')" + ); + $node_publisher->wait_for_catchup('tap_sub_part'); + + # Verify that no rows are replicated to subscriber for root or partitions. + foreach my $table (qw(root1 part1 part2 part2_1)) + { + $result = $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM $table"); + is($result, qq(0), "no rows replicated to subscriber for $table"); + } + + $node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION tap_sub_part"); + $node_publisher->safe_psql('postgres', "DROP PUBLICATION tap_pub_part"); +} + +# ============================================ +# EXCEPT clause test cases for non-partitioned tables and inherited tables. +# ============================================ + +# Create tables on publisher +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE tab1 AS SELECT generate_series(1,10) AS a; + CREATE TABLE parent (a int); + CREATE TABLE child (b int) INHERITS (parent); + CREATE TABLE parent1 (a int); + CREATE TABLE child1 (b int) INHERITS (parent1); +)); + +# Create tables on subscriber +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE TABLE tab1 (a int); + CREATE TABLE parent (a int); + CREATE TABLE child (b int) INHERITS (parent); + CREATE TABLE parent1 (a int); + CREATE TABLE child1 (b int) INHERITS (parent1); +)); + +# Exclude tab1 (non-inheritance case), and also exclude parent and ONLY parent1 +# to verify exclusion behavior for inherited tables, including the effect of +# ONLY in the EXCEPT clause. +$node_publisher->safe_psql('postgres', + "CREATE PUBLICATION tap_pub FOR ALL TABLES EXCEPT (TABLE tab1, parent, only parent1)" +); + +# Create a logical replication slot to help with later tests. +$node_publisher->safe_psql('postgres', + "SELECT pg_create_logical_replication_slot('test_slot', 'pgoutput')"); + +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub" +); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +# Check the table data does not sync for the tables specified in the EXCEPT +# clause. +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1"); +is($result, qq(0), + 'check there is no initial data copied for the tables specified in the EXCEPT clause' +); + +# Insert some data into the table listed in the EXCEPT clause +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO tab1 VALUES(generate_series(11,20)); + INSERT INTO child VALUES(generate_series(11,20), generate_series(11,20)); +)); + +# Verify that data inserted into a table listed in the EXCEPT clause is +# not published. +$result = $node_publisher->safe_psql('postgres', + "SELECT count(*) = 0 FROM pg_logical_slot_get_binary_changes('test_slot', NULL, NULL, 'proto_version', '1', 'publication_names', 'tap_pub')" +); +is($result, qq(t), + 'verify no changes for table listed in the EXCEPT clause are present in the replication slot' +); + +# This should be published because ONLY parent1 was specified in the +# EXCEPT clause, so the exclusion applies only to the parent table and not +# to its child. +$node_publisher->safe_psql('postgres', + "INSERT INTO child1 VALUES(generate_series(11,20), generate_series(11,20))" +); + +# Verify that data inserted into a table listed in the EXCEPT clause is +# not replicated. +$node_publisher->wait_for_catchup('tap_sub'); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1"); +is($result, qq(0), 'check replicated inserts on subscriber'); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM child"); +is($result, qq(0), 'check replicated inserts on subscriber'); +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM child1"); +is($result, qq(10), 'check replicated inserts on subscriber'); + +$node_publisher->safe_psql('postgres', + "CREATE TABLE tab2 AS SELECT generate_series(1,10) AS a"); +$node_subscriber->safe_psql('postgres', "CREATE TABLE tab2 (a int)"); + +# Replace the table list in the EXCEPT clause so that only tab2 is excluded. +$node_publisher->safe_psql('postgres', + "ALTER PUBLICATION tap_pub SET ALL TABLES EXCEPT (TABLE tab2)"); + +# Refresh the subscription so the subscriber picks up the updated +# publication definition and initiates table synchronization. +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub REFRESH PUBLICATION"); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +# Verify that initial table synchronization does not occur for tables +# listed in the EXCEPT clause. +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab2"); +is($result, qq(0), + 'check there is no initial data copied for the tables specified in the EXCEPT clause' +); + +# Verify that table synchronization now happens for tab1. Table tab1 is +# included now since the table list of EXCEPT clause is only (tab2). +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab1"); +is($result, qq(20), + 'check that the data is copied as the tab1 is removed from EXCEPT clause' +); + +# cleanup +$node_subscriber->safe_psql( + 'postgres', qq( + DROP SUBSCRIPTION tap_sub; + TRUNCATE TABLE tab1; + DROP TABLE parent, parent1, child, child1, tab2; +)); +$node_publisher->safe_psql( + 'postgres', qq( + DROP PUBLICATION tap_pub; + TRUNCATE TABLE tab1; + DROP TABLE parent, parent1, child, child1, tab2; +)); + +# ============================================ +# EXCEPT clause test cases for partitioned tables +# ============================================ +# Setup partitioned table and partitions on the publisher that map to normal +# tables on the subscriber. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE root1(a int) PARTITION BY RANGE(a); + CREATE TABLE part1 PARTITION OF root1 FOR VALUES FROM (0) TO (100); + CREATE TABLE part2 PARTITION OF root1 FOR VALUES FROM (100) TO (200) PARTITION BY RANGE(a); + CREATE TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (100) TO (150); +)); + +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE TABLE root1(a int); + CREATE TABLE part1(a int); + CREATE TABLE part2(a int); + CREATE TABLE part2_1(a int); +)); + +# Validate the behaviour with both publish_via_partition_root as true and false +test_except_root_partition('false'); +test_except_root_partition('true'); + +# ============================================ +# Test when a subscription is subscribing to multiple publications +# ============================================ + +# OK when a table is excluded by pub1 EXCEPT clause, but it is included by pub2 +# FOR TABLE. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub1 FOR ALL TABLES EXCEPT (TABLE tab1); + CREATE PUBLICATION tap_pub2 FOR TABLE tab1; + INSERT INTO tab1 VALUES(1); +)); +$node_subscriber->psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2))); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_publisher->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a"); +is( $result, qq(1 +2), + "check replication of a table in the EXCEPT clause of one publication but included by another" +); +$node_publisher->safe_psql( + 'postgres', qq( + DROP PUBLICATION tap_pub2; + TRUNCATE tab1; +)); +$node_subscriber->safe_psql('postgres', qq(TRUNCATE tab1)); + +# OK when a table is excluded by pub1 EXCEPT clause, but it is included by pub2 +# FOR ALL TABLES. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE PUBLICATION tap_pub2 FOR ALL TABLES; + INSERT INTO tab1 VALUES(1); +)); +$node_subscriber->psql('postgres', + "CREATE SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr' PUBLICATION tap_pub1, tap_pub2" +); +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +$node_publisher->safe_psql('postgres', qq(INSERT INTO tab1 VALUES(2))); +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_publisher->safe_psql('postgres', "SELECT * FROM tab1 ORDER BY a"); +is( $result, qq(1 +2), + "check replication of a table in the EXCEPT clause of one publication but included by another" +); + +$node_subscriber->safe_psql('postgres', 'DROP SUBSCRIPTION tap_sub'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub1'); +$node_publisher->safe_psql('postgres', 'DROP PUBLICATION tap_pub2'); + +$node_publisher->stop('fast'); + +done_testing(); diff --git a/src/test/subscription/t/038_walsnd_shutdown_timeout.pl b/src/test/subscription/t/038_walsnd_shutdown_timeout.pl new file mode 100644 index 0000000000000..f4ed5d9785246 --- /dev/null +++ b/src/test/subscription/t/038_walsnd_shutdown_timeout.pl @@ -0,0 +1,201 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Checks that the publisher is able to shut down without +# waiting for sending of all pending data to the subscriber +# with wal_sender_shutdown_timeout set +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; +use Time::HiRes qw(usleep); + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->append_conf( + 'postgresql.conf', + qq(wal_sender_timeout = 1h + wal_sender_shutdown_timeout = 10ms)); +$node_publisher->start; + +# Initialize subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Create publication for test table +$node_publisher->safe_psql( + 'postgres', qq( + CREATE TABLE test_tab (id int PRIMARY KEY); + CREATE PUBLICATION test_pub FOR TABLE test_tab; +)); + +# Create matching table and subscription on subscriber +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_subscriber->safe_psql( + 'postgres', qq( + CREATE TABLE test_tab (id int PRIMARY KEY); + CREATE SUBSCRIPTION test_sub CONNECTION '$publisher_connstr' PUBLICATION test_pub WITH (failover = true); +)); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'test_sub'); + +# Start a background session on the subscriber to run a transaction later +# that will block the logical apply worker on a lock. +my $sub_session = $node_subscriber->background_psql('postgres'); + +# Test that when the logical apply worker is blocked on a lock and replication +# is stalled, shutting down the publisher causes the logical walsender to exit +# due to wal_sender_shutdown_timeout, allowing shutdown to complete. + +# Cause the logical apply worker to block on a lock by running conflicting +# transactions on the publisher and subscriber. +$sub_session->query_safe("BEGIN; INSERT INTO test_tab VALUES (0);"); +$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (0);"); + +my $log_offset = -s $node_publisher->logfile; + +# Verify that the walsender exits due to wal_sender_shutdown_timeout. +$node_publisher->stop('fast'); +ok( $node_publisher->log_contains( + qr/WARNING: .* terminating walsender process due to replication shutdown timeout/, + $log_offset), + "walsender exits due to wal_sender_shutdown_timeout"); + +$sub_session->query_safe("ABORT;"); +$node_publisher->start; +$node_publisher->wait_for_catchup('test_sub'); + +# Test that when the logical apply worker is blocked on a lock, replication +# is stalled, and the logical walsender's output buffer is full, shutting down +# the publisher causes the walsender to exit due to +# wal_sender_shutdown_timeout, allowing shutdown to complete. +# +# This test differs from the previous one in that the walsender's output +# buffer is full (because pending data cannot be transferred). + +# Run a transaction on the subscriber that blocks the logical apply worker +# on a lock. +$sub_session->query_safe("BEGIN; LOCK TABLE test_tab IN EXCLUSIVE MODE;"); + +# Generate enough data to fill the logical walsender's output buffer. +$node_publisher->safe_psql('postgres', + "INSERT INTO test_tab VALUES (generate_series(1, 20000));"); + +# Wait for the logical walsender's output buffer to fill. If the WAL send +# positions do not advance between checks, treat the buffer as full. +my $last_sent_lsn = $node_publisher->safe_psql('postgres', + "SELECT sent_lsn FROM pg_stat_replication WHERE application_name = 'test_sub';" +); + +my $max_attempts = $PostgreSQL::Test::Utils::timeout_default * 10; +while ($max_attempts-- >= 0) +{ + usleep(100_000); + + my $cur_sent_lsn = $node_publisher->safe_psql('postgres', + "SELECT sent_lsn FROM pg_stat_replication WHERE application_name = 'test_sub';" + ); + + my $diff = $node_publisher->safe_psql('postgres', + "SELECT pg_wal_lsn_diff('$cur_sent_lsn', '$last_sent_lsn');"); + last if $diff == 0; + + $last_sent_lsn = $cur_sent_lsn; +} + +$log_offset = -s $node_publisher->logfile; + +# Verify that the walsender exits due to wal_sender_shutdown_timeout. +$node_publisher->stop('fast'); +ok( $node_publisher->log_contains( + qr/WARNING: .* terminating walsender process due to replication shutdown timeout/, + $log_offset), + "walsender with full output buffer exits due to wal_sender_shutdown_timeout" +); + +$sub_session->query_safe("ABORT;"); + +# The next test depends on Perl's `kill`, which apparently is not +# portable to Windows. (It would be nice to use Test::More's `subtest`, +# but that's not in the ancient version we require.) +if ($PostgreSQL::Test::Utils::windows_os) +{ + $node_subscriber->stop('fast'); + done_testing(); + exit; +} + +$node_publisher->start; + +# Test that wal_sender_shutdown_timeout works correctly when both physical +# and logical replication are active, and slot synchronization is running on +# the standby. +# +# In this scenario, the logical apply worker is blocked on a lock and +# the standby's walreceiver is stopped (via SIGSTOP signal), stalling both +# replication streams. Verify that shutting down the publisher (primary) +# causes both physical and logical walsenders to exit due to +# wal_sender_shutdown_timeout, allowing shutdown to complete. +# +# Skip this test on Windows. + +# Create the standby with slot synchronization enabled. +$node_publisher->backup( + 'publisher_backup', + backup_options => [ + '--create-slot', '--slot', + 'test_slot', '-d', + 'dbname=postgres', '--write-recovery-conf' + ]); + +$node_publisher->append_conf('postgresql.conf', + "synchronized_standby_slots = 'test_slot'"); +$node_publisher->reload; + +my $node_standby = PostgreSQL::Test::Cluster->new('standby'); +$node_standby->init_from_backup($node_publisher, 'publisher_backup'); +$node_standby->append_conf( + 'postgresql.conf', + qq(sync_replication_slots = on + hot_standby_feedback = on)); +$node_standby->start; + +# Cause the logical apply worker to block on a lock by running conflicting +# transactions on the publisher and subscriber, stalling logical replication. +$node_publisher->wait_for_catchup('test_sub'); +$sub_session->query_safe("BEGIN; LOCK TABLE test_tab IN EXCLUSIVE MODE;"); +$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (-1); "); + +# Cause the standby's walreceiver to be blocked with SIGSTOP signal, +# stalling physical replication. +$node_standby->poll_query_until('postgres', + "SELECT EXISTS(SELECT 1 FROM pg_stat_wal_receiver)"); +my $receiverpid = $node_standby->safe_psql('postgres', + "SELECT pid FROM pg_stat_wal_receiver"); +like($receiverpid, qr/^[0-9]+$/, "have walreceiver pid $receiverpid"); +kill 'STOP', $receiverpid; + +$log_offset = -s $node_publisher->logfile; + +# Verify that the walsender exits due to wal_sender_shutdown_timeout +# even when both physical and logical replication are stalled. +# wal_sender_shutdown_timeout. +$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (-2);"); +$node_publisher->stop('fast'); +ok( $node_publisher->log_contains( + qr/WARNING: .* terminating walsender process due to replication shutdown timeout/, + $log_offset), + "walsender exits due to wal_sender_shutdown_timeout even when both physical and logical replication are stalled" +); + +kill 'CONT', $receiverpid; +$sub_session->quit; + +$node_subscriber->stop('fast'); +$node_standby->stop('fast'); + +done_testing(); diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 5e3577011833b..a23035e23fed1 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Tests for various bugs found over time use strict; @@ -575,4 +575,34 @@ BEGIN $node_publisher->stop('fast'); $node_subscriber->stop('fast'); +# BUG #18988 +# The bug happened due to a self-deadlock between the DROP SUBSCRIPTION +# command and the walsender process for accessing pg_subscription. This +# occurred when DROP SUBSCRIPTION attempted to remove a replication slot by +# connecting to a newly created database whose caches are not yet +# initialized. +# +# The bug is fixed by reducing the lock-level during DROP SUBSCRIPTION. +$node_publisher->start(); + +$publisher_connstr = $node_publisher->connstr . ' dbname=regress_db'; +$node_publisher->safe_psql( + 'postgres', qq( + CREATE DATABASE regress_db; + CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub WITH (connect=false); +)); + +my ($ret, $stdout, $stderr) = + $node_publisher->psql('postgres', q{DROP SUBSCRIPTION regress_sub1}); + +isnt($ret, 0, "replication slot does not exist: exit code not 0"); +like( + $stderr, + qr/ERROR: could not drop replication slot "regress_sub1" on publisher/, + "could not drop replication slot: error message"); + +$node_publisher->safe_psql('postgres', "DROP DATABASE regress_db"); + +$node_publisher->stop('fast'); + done_testing(); diff --git a/src/timezone/README b/src/timezone/README index dd5d5f9892ad2..1857f03e3ddcf 100644 --- a/src/timezone/README +++ b/src/timezone/README @@ -79,13 +79,6 @@ fixed that.) includes relying on configure's results rather than hand-hacked #defines (see private.h in particular). -* Similarly, avoid relying on features that may not exist on old -systems. In particular this means using Postgres' definitions of the int32 -and int64 typedefs, not int_fast32_t/int_fast64_t. Likewise we use -PG_INT32_MIN/MAX not INT32_MIN/MAX. (Once we desupport all PG versions -that don't require C99, it'd be practical to rely on and remove -this set of diffs; but that day is not yet.) - * Since Postgres is typically built on a system that has its own copy of the functions, we must avoid conflicting with those. This mandates renaming typedef time_t to pg_time_t, and similarly for most @@ -119,13 +112,6 @@ to first run the tzcode source files through a sed filter like this: -e 's|^\*/| */|' \ -e 's/\bregister[ \t]//g' \ -e 's/\bATTRIBUTE_PURE[ \t]//g' \ - -e 's/int_fast32_t/int32/g' \ - -e 's/int_fast64_t/int64/g' \ - -e 's/intmax_t/int64/g' \ - -e 's/INT32_MIN/PG_INT32_MIN/g' \ - -e 's/INT32_MAX/PG_INT32_MAX/g' \ - -e 's/INTMAX_MIN/PG_INT64_MIN/g' \ - -e 's/INTMAX_MAX/PG_INT64_MAX/g' \ -e 's/struct[ \t]+tm\b/struct pg_tm/g' \ -e 's/\btime_t\b/pg_time_t/g' \ -e 's/lineno/lineno_t/g' \ diff --git a/src/timezone/data/tzdata.zi b/src/timezone/data/tzdata.zi index a7fb52f1968f3..c56f67c02f6d2 100644 --- a/src/timezone/data/tzdata.zi +++ b/src/timezone/data/tzdata.zi @@ -1,4 +1,4 @@ -# version 2025b +# version 2025c # This zic input file is in the public domain. R d 1916 o - Jun 14 23s 1 S R d 1916 1919 - O Su>=1 23s 0 - @@ -2951,9 +2951,7 @@ Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 7u -8 1 PDT 1951 S 30 2 -8 - PST 1952 Ap 27 2 -8 1 PDT 1952 S 28 2 --8 - PST 1954 --8 CA P%sT 1961 --8 - PST 1976 +-8 CA P%sT 1967 -8 u P%sT 1996 -8 m P%sT 2001 -8 u P%sT 2002 F 20 diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c index 8eb02ef14691c..ac0376b0c4df4 100644 --- a/src/timezone/localtime.c +++ b/src/timezone/localtime.c @@ -66,7 +66,7 @@ enum r_type { JULIAN_DAY, /* Jn = Julian day */ DAY_OF_YEAR, /* n = day of year */ - MONTH_NTH_DAY_OF_WEEK, /* Mm.n.d = month, week, day of week */ + MONTH_NTH_DAY_OF_WEEK /* Mm.n.d = month, week, day of week */ }; struct rule @@ -75,20 +75,20 @@ struct rule int r_day; /* day number of rule */ int r_week; /* week number of rule */ int r_mon; /* month number of rule */ - int32 r_time; /* transition time of rule */ + int_fast32_t r_time; /* transition time of rule */ }; /* * Prototypes for static functions. */ -static struct pg_tm *gmtsub(pg_time_t const *timep, int32 offset, +static struct pg_tm *gmtsub(pg_time_t const *timep, int_fast32_t offset, struct pg_tm *tmp); static bool increment_overflow(int *ip, int j); -static bool increment_overflow_time(pg_time_t *tp, int32 j); -static int64 leapcorr(struct state const *sp, pg_time_t t); +static bool increment_overflow_time(pg_time_t *tp, int_fast32_t j); +static int_fast64_t leapcorr(struct state const *sp, pg_time_t t); static struct pg_tm *timesub(pg_time_t const *timep, - int32 offset, struct state const *sp, + int_fast32_t offset, struct state const *sp, struct pg_tm *tmp); static bool typesequiv(struct state const *sp, int a, int b); @@ -105,7 +105,7 @@ static struct pg_tm tm; /* Initialize *S to a value based on UTOFF, ISDST, and DESIGIDX. */ static void -init_ttinfo(struct ttinfo *s, int32 utoff, bool isdst, int desigidx) +init_ttinfo(struct ttinfo *s, int_fast32_t utoff, bool isdst, int desigidx) { s->tt_utoff = utoff; s->tt_isdst = isdst; @@ -114,15 +114,15 @@ init_ttinfo(struct ttinfo *s, int32 utoff, bool isdst, int desigidx) s->tt_ttisut = false; } -static int32 +static int_fast32_t detzcode(const char *const codep) { - int32 result; + int_fast32_t result; int i; - int32 one = 1; - int32 halfmaxval = one << (32 - 2); - int32 maxval = halfmaxval - 1 + halfmaxval; - int32 minval = -1 - maxval; + int_fast32_t one = 1; + int_fast32_t halfmaxval = one << (32 - 2); + int_fast32_t maxval = halfmaxval - 1 + halfmaxval; + int_fast32_t minval = -1 - maxval; result = codep[0] & 0x7f; for (i = 1; i < 4; ++i) @@ -134,21 +134,21 @@ detzcode(const char *const codep) * Do two's-complement negation even on non-two's-complement machines. * If the result would be minval - 1, return minval. */ - result -= !TWOS_COMPLEMENT(int32) && result != 0; + result -= !TWOS_COMPLEMENT(int_fast32_t) && result != 0; result += minval; } return result; } -static int64 +static int_fast64_t detzcode64(const char *const codep) { - uint64 result; + uint_fast64_t result; int i; - int64 one = 1; - int64 halfmaxval = one << (64 - 2); - int64 maxval = halfmaxval - 1 + halfmaxval; - int64 minval = -TWOS_COMPLEMENT(int64) - maxval; + int_fast64_t one = 1; + int_fast64_t halfmaxval = one << (64 - 2); + int_fast64_t maxval = halfmaxval - 1 + halfmaxval; + int_fast64_t minval = -TWOS_COMPLEMENT(int_fast64_t) - maxval; result = codep[0] & 0x7f; for (i = 1; i < 8; ++i) @@ -160,7 +160,7 @@ detzcode64(const char *const codep) * Do two's-complement negation even on non-two's-complement machines. * If the result would be minval - 1, return minval. */ - result -= !TWOS_COMPLEMENT(int64) && result != 0; + result -= !TWOS_COMPLEMENT(int_fast64_t) && result != 0; result += minval; } return result; @@ -246,14 +246,14 @@ tzloadbody(char const *name, char *canonname, struct state *sp, bool doextend, return errno; for (stored = 4; stored <= 8; stored *= 2) { - int32 ttisstdcnt = detzcode(up->tzhead.tzh_ttisstdcnt); - int32 ttisutcnt = detzcode(up->tzhead.tzh_ttisutcnt); - int64 prevtr = 0; - int32 prevcorr = 0; - int32 leapcnt = detzcode(up->tzhead.tzh_leapcnt); - int32 timecnt = detzcode(up->tzhead.tzh_timecnt); - int32 typecnt = detzcode(up->tzhead.tzh_typecnt); - int32 charcnt = detzcode(up->tzhead.tzh_charcnt); + int_fast32_t ttisstdcnt = detzcode(up->tzhead.tzh_ttisstdcnt); + int_fast32_t ttisutcnt = detzcode(up->tzhead.tzh_ttisutcnt); + int_fast64_t prevtr = 0; + int_fast32_t prevcorr = 0; + int_fast32_t leapcnt = detzcode(up->tzhead.tzh_leapcnt); + int_fast32_t timecnt = detzcode(up->tzhead.tzh_timecnt); + int_fast32_t typecnt = detzcode(up->tzhead.tzh_typecnt); + int_fast32_t charcnt = detzcode(up->tzhead.tzh_charcnt); char const *p = up->buf + tzheadsize; /* @@ -291,7 +291,7 @@ tzloadbody(char const *name, char *canonname, struct state *sp, bool doextend, timecnt = 0; for (i = 0; i < sp->timecnt; ++i) { - int64 at + int_fast64_t at = stored == 4 ? detzcode(p) : detzcode64(p); sp->types[i] = at <= TIME_T_MAX; @@ -350,8 +350,8 @@ tzloadbody(char const *name, char *canonname, struct state *sp, bool doextend, leapcnt = 0; for (i = 0; i < sp->leapcnt; ++i) { - int64 tr = stored == 4 ? detzcode(p) : detzcode64(p); - int32 corr = detzcode(p + stored); + int_fast64_t tr = stored == 4 ? detzcode(p) : detzcode64(p); + int_fast32_t corr = detzcode(p + stored); p += stored + 4; /* Leap seconds cannot occur before the Epoch. */ @@ -421,6 +421,7 @@ tzloadbody(char const *name, char *canonname, struct state *sp, bool doextend, up->buf[nread - 1] = '\0'; if (tzparse(&up->buf[1], ts, false)) { + /* * Attempt to reuse existing abbreviations. Without this, * America/Anchorage would be right on the edge after 2037 when @@ -583,7 +584,7 @@ tzloadbody(char const *name, char *canonname, struct state *sp, bool doextend, * given name is stored there (the buffer must be > TZ_STRLEN_MAX bytes!). */ int -tzload(const char *name, char *canonname, struct state *sp, bool doextend) +tzload(char const *name, char *canonname, struct state *sp, bool doextend) { union local_storage *lsp = malloc(sizeof *lsp); @@ -707,7 +708,7 @@ getnum(const char *strp, int *const nump, const int min, const int max) */ static const char * -getsecs(const char *strp, int32 *const secsp) +getsecs(const char *strp, int_fast32_t *const secsp) { int num; @@ -719,7 +720,7 @@ getsecs(const char *strp, int32 *const secsp) strp = getnum(strp, &num, 0, HOURSPERDAY * DAYSPERWEEK - 1); if (strp == NULL) return NULL; - *secsp = num * (int32) SECSPERHOUR; + *secsp = num * (int_fast32_t) SECSPERHOUR; if (*strp == ':') { ++strp; @@ -748,7 +749,7 @@ getsecs(const char *strp, int32 *const secsp) */ static const char * -getoffset(const char *strp, int32 *const offsetp) +getoffset(const char *strp, int_fast32_t *const offsetp) { bool neg = false; @@ -835,12 +836,12 @@ getrule(const char *strp, struct rule *const rulep) * effect, calculate the year-relative time that rule takes effect. */ -static int32 +static int_fast32_t transtime(const int year, const struct rule *const rulep, - const int32 offset) + const int_fast32_t offset) { bool leapyear; - int32 value; + int_fast32_t value; int i; int d, m1, @@ -905,7 +906,7 @@ transtime(const int year, const struct rule *const rulep, for (i = 1; i < rulep->r_week; ++i) { if (d + DAYSPERWEEK >= - mon_lengths[(int) leapyear][rulep->r_mon - 1]) + mon_lengths[leapyear][rulep->r_mon - 1]) break; d += DAYSPERWEEK; } @@ -915,7 +916,7 @@ transtime(const int year, const struct rule *const rulep, */ value = d * SECSPERDAY; for (i = 0; i < rulep->r_mon - 1; ++i) - value += mon_lengths[(int) leapyear][i] * SECSPERDAY; + value += mon_lengths[leapyear][i] * SECSPERDAY; break; } @@ -940,8 +941,8 @@ tzparse(const char *name, struct state *sp, bool lastditch) size_t stdlen; size_t dstlen; size_t charcnt; - int32 stdoffset; - int32 dstoffset; + int_fast32_t stdoffset; + int_fast32_t dstoffset; char *cp; bool load_ok; @@ -1033,7 +1034,7 @@ tzparse(const char *name, struct state *sp, bool lastditch) int yearlim; int timecnt; pg_time_t janfirst; - int32 janoffset = 0; + int_fast32_t janoffset = 0; int yearbeg; ++name; @@ -1059,7 +1060,7 @@ tzparse(const char *name, struct state *sp, bool lastditch) do { - int32 yearsecs + int_fast32_t yearsecs = year_lengths[isleap(yearbeg - 1)] * SECSPERDAY; yearbeg--; @@ -1073,17 +1074,17 @@ tzparse(const char *name, struct state *sp, bool lastditch) yearlim = yearbeg + YEARSPERREPEAT + 1; for (year = yearbeg; year < yearlim; year++) { - int32 + int_fast32_t starttime = transtime(year, &start, stdoffset), endtime = transtime(year, &end, dstoffset); - int32 + int_fast32_t yearsecs = (year_lengths[isleap(year)] * SECSPERDAY); bool reversed = endtime < starttime; if (reversed) { - int32 swap = starttime; + int_fast32_t swap = starttime; starttime = endtime; endtime = swap; @@ -1126,9 +1127,9 @@ tzparse(const char *name, struct state *sp, bool lastditch) } else { - int32 theirstdoffset; - int32 theirdstoffset; - int32 theiroffset; + int_fast32_t theirstdoffset; + int_fast32_t theirdstoffset; + int_fast32_t theiroffset; bool isdst; int i; int j; @@ -1290,7 +1291,7 @@ localsub(struct state const *sp, pg_time_t const *timep, result = localsub(sp, &newt, tmp); if (result) { - int64 newy; + int_fast64_t newy; newy = result->tm_year; if (t < sp->ats[0]) @@ -1354,7 +1355,7 @@ pg_localtime(const pg_time_t *timep, const pg_tz *tz) */ static struct pg_tm * -gmtsub(pg_time_t const *timep, int32 offset, +gmtsub(pg_time_t const *timep, int_fast32_t offset, struct pg_tm *tmp) { struct pg_tm *result; @@ -1411,16 +1412,16 @@ leaps_thru_end_of(const int y) } static struct pg_tm * -timesub(const pg_time_t *timep, int32 offset, +timesub(const pg_time_t *timep, int_fast32_t offset, const struct state *sp, struct pg_tm *tmp) { const struct lsinfo *lp; pg_time_t tdays; int idays; /* unsigned would be so 2003 */ - int64 rem; + int_fast64_t rem; int y; const int *ip; - int64 corr; + int_fast64_t corr; bool hit; int i; @@ -1554,7 +1555,7 @@ increment_overflow(int *ip, int j) } static bool -increment_overflow_time(pg_time_t *tp, int32 j) +increment_overflow_time(pg_time_t *tp, int_fast32_t j) { /*---------- * This is like @@ -1570,7 +1571,7 @@ increment_overflow_time(pg_time_t *tp, int32 j) return false; } -static int64 +static int_fast64_t leapcorr(struct state const *sp, pg_time_t t) { struct lsinfo const *lp; diff --git a/src/timezone/meson.build b/src/timezone/meson.build index ad9f6f6b8addb..2d62e8755e4c4 100644 --- a/src/timezone/meson.build +++ b/src/timezone/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group # files to build into backend timezone_sources = files( diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c index 671b4d76237d5..eac988c21e711 100644 --- a/src/timezone/pgtz.c +++ b/src/timezone/pgtz.c @@ -3,7 +3,7 @@ * pgtz.c * Timezone Library Integration Functions * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/timezone/pgtz.c @@ -364,8 +364,8 @@ pg_timezone_initialize(void) * We may not yet know where PGSHAREDIR is (in particular this is true in * an EXEC_BACKEND subprocess). So use "GMT", which pg_tzset forces to be * interpreted without reference to the filesystem. This corresponds to - * the bootstrap default for these variables in guc_tables.c, although in - * principle it could be different. + * the bootstrap default for these variables in guc_parameters.dat, + * although in principle it could be different. */ session_timezone = pg_tzset("GMT"); log_timezone = session_timezone; @@ -396,7 +396,7 @@ struct pg_tzenum pg_tzenum * pg_tzenumerate_start(void) { - pg_tzenum *ret = (pg_tzenum *) palloc0(sizeof(pg_tzenum)); + pg_tzenum *ret = palloc0_object(pg_tzenum); char *startdir = pstrdup(pg_TZDIR()); ret->baselen = strlen(startdir) + 1; diff --git a/src/timezone/pgtz.h b/src/timezone/pgtz.h index 8caac631cab82..852b677d67b5f 100644 --- a/src/timezone/pgtz.h +++ b/src/timezone/pgtz.h @@ -6,7 +6,7 @@ * Note: this file contains only definitions that are private to the * timezone library. Public definitions are in pgtime.h. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * * IDENTIFICATION * src/timezone/pgtz.h @@ -25,7 +25,7 @@ struct ttinfo { /* time type information */ - int32 tt_utoff; /* UT offset in seconds */ + int_fast32_t tt_utoff; /* UT offset in seconds */ bool tt_isdst; /* used to set tm_isdst */ int tt_desigidx; /* abbreviation list index */ bool tt_ttisstd; /* transition is std time */ @@ -35,7 +35,7 @@ struct ttinfo struct lsinfo { /* leap second information */ pg_time_t ls_trans; /* transition time */ - int64 ls_corr; /* correction to apply */ + int_fast64_t ls_corr; /* correction to apply */ }; struct state diff --git a/src/timezone/private.h b/src/timezone/private.h index 39d40e43a9f70..ab89028f3e1fc 100644 --- a/src/timezone/private.h +++ b/src/timezone/private.h @@ -44,10 +44,6 @@ /* Unlike 's isdigit, this also works if c < 0 | c > UCHAR_MAX. */ #define is_digit(c) ((unsigned)(c) - '0' <= 9) -/* PG doesn't currently rely on , so work around strtoimax() */ -#undef strtoimax -#define strtoimax strtoll - /* * Finally, some convenience items. @@ -95,25 +91,25 @@ #define YEARSPERREPEAT 400 /* years before a Gregorian repeat */ #define SECSPERMIN 60 -#define MINSPERHOUR 60 -#define HOURSPERDAY 24 -#define DAYSPERWEEK 7 +#define MINSPERHOUR 60 +#define HOURSPERDAY 24 +#define DAYSPERWEEK 7 #define DAYSPERNYEAR 365 #define DAYSPERLYEAR 366 -#define SECSPERHOUR (SECSPERMIN * MINSPERHOUR) -#define SECSPERDAY ((int32) SECSPERHOUR * HOURSPERDAY) -#define MONSPERYEAR 12 +#define SECSPERHOUR (SECSPERMIN * MINSPERHOUR) +#define SECSPERDAY ((int_fast32_t) SECSPERHOUR * HOURSPERDAY) +#define MONSPERYEAR 12 #define TM_SUNDAY 0 #define TM_MONDAY 1 #define TM_TUESDAY 2 #define TM_WEDNESDAY 3 -#define TM_THURSDAY 4 +#define TM_THURSDAY 4 #define TM_FRIDAY 5 -#define TM_SATURDAY 6 +#define TM_SATURDAY 6 #define TM_JANUARY 0 -#define TM_FEBRUARY 1 +#define TM_FEBRUARY 1 #define TM_MARCH 2 #define TM_APRIL 3 #define TM_MAY 4 @@ -122,8 +118,8 @@ #define TM_AUGUST 7 #define TM_SEPTEMBER 8 #define TM_OCTOBER 9 -#define TM_NOVEMBER 10 -#define TM_DECEMBER 11 +#define TM_NOVEMBER 10 +#define TM_DECEMBER 11 #define TM_YEAR_BASE 1900 @@ -153,7 +149,7 @@ #define AVGSECSPERYEAR 31556952L #define SECSPERREPEAT \ - ((int64) YEARSPERREPEAT * (int64) AVGSECSPERYEAR) + ((int_fast64_t) YEARSPERREPEAT * (int_fast64_t) AVGSECSPERYEAR) #define SECSPERREPEAT_BITS 34 /* ceil(log2(SECSPERREPEAT)) */ #endif /* !defined PRIVATE_H */ diff --git a/src/timezone/tznames/meson.build b/src/timezone/tznames/meson.build index 10a26b397ab0b..290e2c82cf482 100644 --- a/src/timezone/tznames/meson.build +++ b/src/timezone/tznames/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group tznames = files( 'Africa.txt', diff --git a/src/timezone/zic.c b/src/timezone/zic.c index 3b70b8881805f..2f36486a350b5 100644 --- a/src/timezone/zic.c +++ b/src/timezone/zic.c @@ -22,9 +22,11 @@ #define ZIC_VERSION_PRE_2013 '2' #define ZIC_VERSION '3' -typedef int64 zic_t; -#define ZIC_MIN PG_INT64_MIN -#define ZIC_MAX PG_INT64_MAX +typedef int_fast64_t zic_t; +#define ZIC_MIN INT_FAST64_MIN +#define ZIC_MAX INT_FAST64_MAX +#define PRIdZIC PRIdFAST64 +#define SCNdZIC SCNdFAST64 #ifndef ZIC_MAX_ABBR_LEN_WO_WARN #define ZIC_MAX_ABBR_LEN_WO_WARN 6 @@ -47,12 +49,15 @@ typedef int64 zic_t; static ptrdiff_t const PTRDIFF_MAX = MAXVAL(ptrdiff_t, TYPE_BIT(ptrdiff_t)); #endif -/* - * The type for line numbers. In Postgres, use %d to format them; upstream - * uses PRIdMAX but we prefer not to rely on that, not least because it - * results in platform-dependent strings to be translated. - */ -typedef int lineno_t; +/* The minimum alignment of a type, for pre-C11 platforms. */ +#if __STDC_VERSION__ < 201112 +#define _Alignof(type) offsetof(struct { char a; type b; }, b) +#endif + +/* The type for line numbers. Use PRIdMAX to format them; formerly + there was also "#define PRIdLINENO PRIdMAX" and formats used + PRIdLINENO, but xgettext cannot grok that. */ +typedef intmax_t lineno_t; struct rule { @@ -117,18 +122,16 @@ extern int link(const char *target, const char *linkname); (itssymlink(target) ? (errno = ENOTSUP, -1) : link(target, linkname)) #endif -pg_noreturn static void memory_exhausted(const char *msg); -static void verror(const char *string, va_list args) pg_attribute_printf(1, 0); -static void error(const char *string,...) pg_attribute_printf(1, 2); -static void warning(const char *string,...) pg_attribute_printf(1, 2); -pg_noreturn static void usage(FILE *stream, int status); +static void verror(const char *const string, va_list args) pg_attribute_printf(1, 0); +static void error(const char *const string,...) pg_attribute_printf(1, 2); +static void warning(const char *const string,...) pg_attribute_printf(1, 2); static void addtt(zic_t starttime, int type); static int addtype(zic_t utoff, char const *abbr, bool isdst, bool ttisstd, bool ttisut); static void leapadd(zic_t t, int correction, int rolling); static void adjleap(void); static void associate(void); -static void dolink(char const *target, char const *linkname, +static void dolink(const char *target, const char *linkname, bool staysymlink); static char **getfields(char *cp); static zic_t gethms(const char *string, const char *errstring); @@ -407,7 +410,7 @@ static char roll[TZ_MAX_LEAPS]; * Memory allocation. */ -static void +static _Noreturn void memory_exhausted(const char *msg) { fprintf(stderr, _("%s: Memory exhausted: %s\n"), progname, msg); @@ -422,6 +425,17 @@ size_product(size_t nitems, size_t itemsize) return nitems * itemsize; } +static size_t +align_to(size_t size, size_t alignment) +{ + size_t aligned_size = size + alignment - 1; + + aligned_size -= aligned_size % alignment; + if (aligned_size < size) + memory_exhausted(_("alignment overflow")); + return aligned_size; +} + static void * memcheck(void *ptr) { @@ -485,23 +499,23 @@ eat(char const *name, lineno_t num) } static void -verror(const char *string, va_list args) +verror(const char *const string, va_list args) { /* * Match the format of "cc" to allow sh users to zic ... 2>&1 | error -t * "*" -v on BSD systems. */ if (filename) - fprintf(stderr, _("\"%s\", line %d: "), filename, linenum); + fprintf(stderr, _("\"%s\", line %" PRIdMAX ": "), filename, linenum); vfprintf(stderr, string, args); if (rfilename != NULL) - fprintf(stderr, _(" (rule from \"%s\", line %d)"), + fprintf(stderr, _(" (rule from \"%s\", line %" PRIdMAX ")"), rfilename, rlinenum); fprintf(stderr, "\n"); } static void -error(const char *string,...) +error(const char *const string,...) { va_list args; @@ -512,7 +526,7 @@ error(const char *string,...) } static void -warning(const char *string,...) +warning(const char *const string,...) { va_list args; @@ -539,7 +553,7 @@ close_file(FILE *stream, char const *dir, char const *name) } } -static void +static _Noreturn void usage(FILE *stream, int status) { fprintf(stream, @@ -601,7 +615,7 @@ static zic_t comment_leapexpires = -1; static bool timerange_option(char *timerange) { - int64 lo = min_time, + intmax_t lo = min_time, hi = max_time; char *lo_end = timerange, *hi_end; @@ -610,7 +624,7 @@ timerange_option(char *timerange) { errno = 0; lo = strtoimax(timerange + 1, &lo_end, 10); - if (lo_end == timerange + 1 || (lo == PG_INT64_MAX && errno == ERANGE)) + if (lo_end == timerange + 1 || (lo == INTMAX_MAX && errno == ERANGE)) return false; } hi_end = lo_end; @@ -618,9 +632,9 @@ timerange_option(char *timerange) { errno = 0; hi = strtoimax(lo_end + 2, &hi_end, 10); - if (hi_end == lo_end + 2 || hi == PG_INT64_MIN) + if (hi_end == lo_end + 2 || hi == INTMAX_MIN) return false; - hi -= !(hi == PG_INT64_MAX && errno == ERANGE); + hi -= !(hi == INTMAX_MAX && errno == ERANGE); } if (*hi_end || hi < lo || max_time < lo || hi < min_time) return false; @@ -1290,20 +1304,7 @@ infile(const char *name) if (nfields == 0) { if (name == leapsec && *buf == '#') - { - /* - * PG: INT64_FORMAT isn't portable for sscanf, so be content - * with scanning a "long". Once we are requiring C99 in all - * live branches, it'd be sensible to adopt upstream's - * practice of using the macros. But for now, we - * don't actually use this code, and it won't overflow before - * 2038 anyway. - */ - long cl_tmp; - - sscanf(buf, "#expires %ld", &cl_tmp); - comment_leapexpires = cl_tmp; - } + sscanf(buf, "#expires %" SCNdZIC, &comment_leapexpires); } else if (wantcont) { @@ -1364,8 +1365,7 @@ infile(const char *name) static zic_t gethms(char const *string, char const *errstring) { - /* PG: make hh be int not zic_t to avoid sscanf portability issues */ - int hh; + zic_t hh; int sign, mm = 0, ss = 0; @@ -1387,7 +1387,7 @@ gethms(char const *string, char const *errstring) else sign = 1; switch (sscanf(string, - "%d%c%d%c%d%c%1d%*[0]%c%*[0123456789]%c", + "%" SCNdZIC "%c%d%c%d%c%1d%*[0]%c%*[0123456789]%c", &hh, &hhx, &mm, &mmx, &ss, &ssx, &tenths, &xr, &xs)) { default: @@ -1395,19 +1395,19 @@ gethms(char const *string, char const *errstring) break; case 8: ok = '0' <= xr && xr <= '9'; - /* fallthrough */ + pg_fallthrough; case 7: ok &= ssx == '.'; if (ok && noise) warning(_("fractional seconds rejected by" " pre-2018 versions of zic")); - /* fallthrough */ + pg_fallthrough; case 5: ok &= mmx == ':'; - /* fallthrough */ + pg_fallthrough; case 3: ok &= hhx == ':'; - /* fallthrough */ + pg_fallthrough; case 1: break; } @@ -1423,19 +1423,16 @@ gethms(char const *string, char const *errstring) error("%s", errstring); return 0; } - /* Some compilers warn that this test is unsatisfiable for 32-bit ints */ -#if INT_MAX > PG_INT32_MAX if (ZIC_MAX / SECSPERHOUR < hh) { error(_("time overflow")); return 0; } -#endif ss += 5 + ((ss ^ 1) & (xr == '0')) <= tenths; /* Round to even. */ if (noise && (hh > HOURSPERDAY || (hh == HOURSPERDAY && (mm != 0 || ss != 0)))) warning(_("values over 24 hours not handled by pre-2007 versions of zic")); - return oadd(sign * (zic_t) hh * SECSPERHOUR, + return oadd(sign * hh * SECSPERHOUR, sign * (mm * SECSPERMIN + ss)); } @@ -1543,7 +1540,7 @@ inzone(char **fields, int nfields) strcmp(zones[i].z_name, fields[ZF_NAME]) == 0) { error(_("duplicate zone name %s" - " (file \"%s\", line %d)"), + " (file \"%s\", line %" PRIdMAX ")"), fields[ZF_NAME], zones[i].z_filename, zones[i].z_linenum); @@ -1669,9 +1666,7 @@ getleapdatetime(char **fields, int nfields, bool expire_line) const struct lookup *lp; zic_t i, j; - - /* PG: make year be int not zic_t to avoid sscanf portability issues */ - int year; + zic_t year; int month, day; zic_t dayoff, @@ -1681,7 +1676,7 @@ getleapdatetime(char **fields, int nfields, bool expire_line) dayoff = 0; cp = fields[LP_YEAR]; - if (sscanf(cp, "%d%c", &year, &xs) != 1) + if (sscanf(cp, "%" SCNdZIC "%c", &year, &xs) != 1) { /* * Leapin' Lizards! @@ -1830,9 +1825,6 @@ rulesub(struct rule *rp, const char *loyearp, const char *hiyearp, char *ep; char xs; - /* PG: year_tmp is to avoid sscanf portability issues */ - int year_tmp; - if ((lp = byword(monthp, mon_names)) == NULL) { error(_("invalid month name")); @@ -1890,9 +1882,7 @@ rulesub(struct rule *rp, const char *loyearp, const char *hiyearp, progname, lp->l_value); exit(EXIT_FAILURE); } - else if (sscanf(cp, "%d%c", &year_tmp, &xs) == 1) - rp->r_loyear = year_tmp; - else + else if (sscanf(cp, "%" SCNdZIC "%c", &rp->r_loyear, &xs) != 1) { error(_("invalid starting year")); return; @@ -1918,9 +1908,7 @@ rulesub(struct rule *rp, const char *loyearp, const char *hiyearp, progname, lp->l_value); exit(EXIT_FAILURE); } - else if (sscanf(cp, "%d%c", &year_tmp, &xs) == 1) - rp->r_hiyear = year_tmp; - else + else if (sscanf(cp, "%" SCNdZIC "%c", &rp->r_hiyear, &xs) != 1) { error(_("invalid ending year")); return; @@ -1989,7 +1977,7 @@ rulesub(struct rule *rp, const char *loyearp, const char *hiyearp, } static void -convert(const int32 val, char *const buf) +convert(const int_fast32_t val, char *const buf) { int i; int shift; @@ -2011,7 +1999,7 @@ convert64(const zic_t val, char *const buf) } static void -puttzcode(const int32 val, FILE *const fp) +puttzcode(const int_fast32_t val, FILE *const fp) { char buf[4]; @@ -2097,7 +2085,8 @@ writezone(const char *const name, const char *const string, char version, * Allocate the ATS and TYPES arrays via a single malloc, as this is a bit * faster. */ - zic_t *ats = emalloc(MAXALIGN(size_product(nats, sizeof *ats + 1))); + zic_t *ats = emalloc(align_to(size_product(nats, sizeof *ats + 1), + _Alignof(zic_t))); void *typesptr = ats + nats; unsigned char *types = typesptr; struct timerange rangeall, @@ -2201,7 +2190,7 @@ writezone(const char *const name, const char *const string, char version, rangeall.count = timecnt; rangeall.leapcount = leapcnt; range64 = limitrange(rangeall, lo_time, hi_time, ats, types); - range32 = limitrange(range64, PG_INT32_MIN, PG_INT32_MAX, ats, types); + range32 = limitrange(range64, INT32_MIN, INT32_MAX, ats, types); /* * Remove old file, if any, to snap links. @@ -2263,7 +2252,7 @@ writezone(const char *const name, const char *const string, char version, /* * Arguably the default time type in the 32-bit data should be * range32.defaulttype, which is suited for timestamps just before - * PG_INT32_MIN. However, zic traditionally used the time type of + * INT32_MIN. However, zic traditionally used the time type of * the indefinite past instead. Internet RFC 8532 says readers * should ignore 32-bit data, so this discrepancy matters only to * obsolete readers where the traditional type might be more @@ -2271,7 +2260,7 @@ writezone(const char *const name, const char *const string, char version, * value, unless -r specifies a low cutoff that excludes some * 32-bit timestamps. */ - thisdefaulttype = (lo_time <= PG_INT32_MIN + thisdefaulttype = (lo_time <= INT32_MIN ? range64.defaulttype : range32.defaulttype); @@ -2280,8 +2269,8 @@ writezone(const char *const name, const char *const string, char version, toomanytimes = thistimecnt >> 31 >> 1 != 0; thisleapi = range32.leapbase; thisleapcnt = range32.leapcount; - locut = PG_INT32_MIN < lo_time; - hicut = hi_time < PG_INT32_MAX; + locut = INT32_MIN < lo_time; + hicut = hi_time < INT32_MAX; } else { @@ -2476,7 +2465,7 @@ writezone(const char *const name, const char *const string, char version, unsigned char tm = types[i]; char *thisabbrev = &thischars[indmap[desigidx[tm]]]; - fprintf(stdout, "%s\t" INT64_FORMAT "%s\n", + fprintf(stdout, "%s\t%" PRIdFAST64 "%s\n", thisabbrev, utoffs[tm], isdsts[tm] ? "\tD" : ""); @@ -2488,7 +2477,7 @@ writezone(const char *const name, const char *const string, char version, unsigned char tm = defaulttype; char *thisabbrev = &thischars[indmap[desigidx[tm]]]; - fprintf(stdout, "%s\t" INT64_FORMAT "%s\n", + fprintf(stdout, "%s\t%" PRIdFAST64 "%s\n", thisabbrev, utoffs[tm], isdsts[tm] ? "\tD" : ""); @@ -2499,7 +2488,7 @@ writezone(const char *const name, const char *const string, char version, * Output a LO_TIME transition if needed; see limitrange. But do not * go below the minimum representable value for this pass. */ - lo = pass == 1 && lo_time < PG_INT32_MIN ? PG_INT32_MIN : lo_time; + lo = pass == 1 && lo_time < INT32_MIN ? INT32_MIN : lo_time; if (locut) puttzcodepass(lo, fp, pass); @@ -2631,7 +2620,7 @@ doabbr(char *abbr, struct zone const *zp, char const *letters, bool isdst, zic_t save, bool doquotes) { char *cp; - char *slashp; + char const *slashp; size_t len; char const *format = zp->z_format; @@ -3753,7 +3742,7 @@ getfields(char *cp) return array; } -static void +static _Noreturn void time_overflow(void) { error(_("time overflow")); diff --git a/src/tools/PerfectHash.pm b/src/tools/PerfectHash.pm index b8dcfe25e2a82..51b3010ca23dc 100644 --- a/src/tools/PerfectHash.pm +++ b/src/tools/PerfectHash.pm @@ -20,7 +20,7 @@ # not in the set. # # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/tools/PerfectHash.pm diff --git a/src/tools/RELEASE_CHANGES b/src/tools/RELEASE_CHANGES index c0d75c213beae..653318fd770f7 100644 --- a/src/tools/RELEASE_CHANGES +++ b/src/tools/RELEASE_CHANGES @@ -61,6 +61,11 @@ in both master and the branch. * Ports o update ports list in doc/src/sgml/installation.sgml +* Create .abi-compliance-history file with initial entry shortly before the .0 + stamp. The easiest way to do this is to copy it from the previous + REL_*_STABLE branch, remove all entries, and follow the instructions in the + file to add the initial reference point for the major version. + Pre-Beta Tasks ============== @@ -79,8 +84,7 @@ but there may be reasons to do them at other times as well. * Update config.guess and config.sub (from https://savannah.gnu.org/projects/config) -* Update Unicode data: Edit UNICODE_VERSION and CLDR_VERSION in - src/Makefile.global.in, run make update-unicode, and commit. +* Update Unicode data (see src/common/unicode/README) Starting a New Development Cycle @@ -114,10 +118,6 @@ Starting a New Development Cycle * In the newly-made branch, replace "devel" with the branch's major version number in the URLs appearing in the top-level README and Makefile files. -* In the newly-made branch, change src/backend/nodes/gen_node_support.pl - to enforce ABI stability of the NodeTag list (see "ARM ABI STABILITY - CHECK HERE" therein). - * Notify the private committers email list, to ensure all committers are aware of the new branch even if they're not paying close attention to pgsql-hackers. diff --git a/src/tools/add_commit_links.pl b/src/tools/add_commit_links.pl index 710a649203209..d9e657f00546e 100755 --- a/src/tools/add_commit_links.pl +++ b/src/tools/add_commit_links.pl @@ -3,7 +3,7 @@ ################################################################# # add_commit_links.pl -- add commit links to the release notes # -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group # # src/tools/add_commit_links.pl ################################################################# @@ -50,6 +50,8 @@ sub process_file # Get major version number from the file name. $file =~ m/-(\d+)\./; my $major_version = $1; + die "file name $file is not in the expected format\n" + unless defined $major_version; open(my $fh, '<', $file) || die "could not open file $file: $!\n"; open(my $tfh, '>', $tmpfile) || die "could not open file $tmpfile: $!\n"; diff --git a/src/tools/check_bison_recursion.pl b/src/tools/check_bison_recursion.pl index 6dea3a21779a9..390d5ecf7af00 100755 --- a/src/tools/check_bison_recursion.pl +++ b/src/tools/check_bison_recursion.pl @@ -16,7 +16,7 @@ # To use: run bison with the -v switch, then feed the produced y.output # file to this script. # -# Copyright (c) 2011-2025, PostgreSQL Global Development Group +# Copyright (c) 2011-2026, PostgreSQL Global Development Group # # src/tools/check_bison_recursion.pl ################################################################# diff --git a/src/tools/ci/README b/src/tools/ci/README index 12c1e7c308fa9..d183648a8d02b 100644 --- a/src/tools/ci/README +++ b/src/tools/ci/README @@ -82,3 +82,14 @@ defined in .cirrus.yml, by redefining the relevant yaml anchors. Custom compute resources can be provided using - https://cirrus-ci.org/guide/supported-computing-services/ - https://cirrus-ci.org/guide/persistent-workers/ + + +Enabling manual tasks by default +================================ + +Some tasks are not triggered automatically by default, to avoid using up CI +credits too quickly. This can be changed on the repository level, e.g. when +custom compute resources are configured. + +The following repository level environment variables are recognized: +- REPO_CI_AUTOMATIC_TRIGGER_TASKS - space-separated list of (mingw|netbsd|openbsd) diff --git a/src/tools/ci/cores_backtrace.sh b/src/tools/ci/cores_backtrace.sh index 5460741525814..cb325f21156da 100755 --- a/src/tools/ci/cores_backtrace.sh +++ b/src/tools/ci/cores_backtrace.sh @@ -1,12 +1,18 @@ #! /bin/sh -if [ $# -ne 2 ]; then +os=$1 +directory=$2 +executable_directory=$3 + +if [ "$os" != 'openbsd' ] && [ $# -ne 2 ]; then echo "cores_backtrace.sh " exit 1 fi -os=$1 -directory=$2 +if [ "$os" = 'openbsd' ] && [ $# -ne 3 ]; then + echo "cores_backtrace.sh " + exit 1 +fi case $os in freebsd|linux|macos|netbsd|openbsd) @@ -17,6 +23,10 @@ case $os in ;; esac +if [ "$os" = 'openbsd' ]; then + export PATH="${executable_directory}:${PATH}" +fi + first=1 for corefile in $(find "$directory" -type f) ; do if [ "$first" -eq 1 ]; then @@ -26,8 +36,21 @@ for corefile in $(find "$directory" -type f) ; do echo -e '\n\n' fi - if [ "$os" = 'macos' ] || [ "$os" = 'openbsd' ]; then + if [ "$os" = 'macos' ]; then lldb -c $corefile --batch -o 'thread backtrace all' -o 'quit' + elif [ "$os" = 'openbsd' ]; then + # OpenBSD's ELF format doesn't include executable information, so we + # search for the executable manually in . + filename=$(basename "$corefile") + base=$(echo "$filename" | sed 's/\.core.*$//') + binary=$(which "${base}") + + if [ -z "$binary" ]; then + echo "executable ${base} not found in ${PATH}, running 'lldb' without debug information" + lldb -c "$corefile" --batch -o 'thread backtrace all' -o 'quit' + else + lldb "$binary" -c "$corefile" --batch -o 'thread backtrace all' -o 'quit' + fi else auxv=$(gdb --quiet --core ${corefile} --batch -ex 'info auxv' 2>/dev/null) if [ $? -ne 0 ]; then diff --git a/src/tools/ci/gcp_ram_disk.sh b/src/tools/ci/gcp_ram_disk.sh index d48634512ac28..18dbb2037f5dc 100755 --- a/src/tools/ci/gcp_ram_disk.sh +++ b/src/tools/ci/gcp_ram_disk.sh @@ -15,7 +15,12 @@ case "`uname`" in umount /dev/sd0j # unused /usr/obj partition printf "m j\n\n\nswap\nw\nq\n" | disklabel -E sd0 swapon /dev/sd0j - mount -t mfs -o rw,noatime,nodev,-s=8000000 swap $CIRRUS_WORKING_DIR + # Remove the per-process data segment limit so that mount_mfs can allocate + # large memory filesystems. Without this, mount_mfs mmap() may fail with + # "Cannot allocate memory" if the requested size exceeds the current + # datasize limit. + ulimit -d unlimited + mount -t mfs -o rw,noatime,nodev,-s=10000000 swap $CIRRUS_WORKING_DIR ;; esac diff --git a/src/tools/ci/pg_ci_base.conf b/src/tools/ci/pg_ci_base.conf index 9cec5c2910d80..ff05646c90e09 100644 --- a/src/tools/ci/pg_ci_base.conf +++ b/src/tools/ci/pg_ci_base.conf @@ -7,8 +7,9 @@ max_prepared_transactions = 10 # Settings that make logs more useful log_autovacuum_min_duration = 0 +log_autoanalyze_min_duration = 0 log_checkpoints = true log_connections = all log_disconnections = true -log_line_prefix = '%m [%p][%b] %q[%a][%v:%x] ' +log_line_prefix = '%m %b[%p] %q%a ' log_lock_waits = true diff --git a/src/tools/codelines b/src/tools/codelines deleted file mode 100755 index 11e86accf271d..0000000000000 --- a/src/tools/codelines +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# src/tools/codelines - -# This script is used to compute the total number of "C" lines in the release -# This should be run from the top of the Git tree after a 'make distclean' -find . -name '*.[chyl]' | xargs cat| wc -l diff --git a/src/tools/copyright.pl b/src/tools/copyright.pl index 0d2bf7b486df6..6a37dec7da15b 100755 --- a/src/tools/copyright.pl +++ b/src/tools/copyright.pl @@ -2,7 +2,7 @@ ################################################################# # copyright.pl -- update copyright notices throughout the source tree, idempotently. # -# Copyright (c) 2011-2025, PostgreSQL Global Development Group +# Copyright (c) 2011-2026, PostgreSQL Global Development Group # # src/tools/copyright.pl # diff --git a/src/tools/gen_export.pl b/src/tools/gen_export.pl index 86a7742b36981..c91fa42a67519 100644 --- a/src/tools/gen_export.pl +++ b/src/tools/gen_export.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2024-2025, PostgreSQL Global Development Group +# Copyright (c) 2024-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -16,11 +16,12 @@ 'input:s' => \$input, 'output:s' => \$output) or die "wrong arguments"; -if (not( $format eq 'darwin' +if (not( $format eq 'aix' + or $format eq 'darwin' or $format eq 'gnu' or $format eq 'win')) { - die "$0: $format is not yet handled (only darwin, gnu, win are)\n"; + die "$0: $format is not yet handled (only aix, darwin, gnu, win are)\n"; } open(my $input_handle, '<', $input) @@ -30,7 +31,11 @@ or die "$0: could not open output file '$output': $!\n"; -if ($format eq 'gnu') +if ($format eq 'aix') +{ + print $output_handle "#!\n"; +} +elsif ($format eq 'gnu') { print $output_handle "{ global: @@ -55,7 +60,11 @@ } elsif (/^(\S+)\s+(\S+)/) { - if ($format eq 'darwin') + if ($format eq 'aix') + { + print $output_handle "$1\n"; + } + elsif ($format eq 'darwin') { print $output_handle "_$1\n"; } diff --git a/src/tools/gen_keywordlist.pl b/src/tools/gen_keywordlist.pl index 6ec83ff33f9a9..bb03dbb0eb578 100644 --- a/src/tools/gen_keywordlist.pl +++ b/src/tools/gen_keywordlist.pl @@ -21,7 +21,7 @@ # Note that case folding works correctly only for all-ASCII keywords! # # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/tools/gen_keywordlist.pl @@ -71,7 +71,7 @@ * %s.h * List of keywords represented as a ScanKeywordList. * - * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * NOTES @@ -169,7 +169,16 @@ # Emit the struct that wraps all this lookup info into one variable. -printf $kwdef "static " if !$extern; +if ($extern) +{ + # redundant declaration to silence -Wmissing-variable-declarations + printf $kwdef "extern PGDLLIMPORT const ScanKeywordList %s;\n\n", + $varname; +} +else +{ + printf $kwdef "static "; +} printf $kwdef "const ScanKeywordList %s = {\n", $varname; printf $kwdef qq|\t%s_kw_string,\n|, $varname; printf $kwdef qq|\t%s_kw_offsets,\n|, $varname; diff --git a/src/tools/git_changelog b/src/tools/git_changelog index b8bd874f20858..d5f651ecf5b0e 100755 --- a/src/tools/git_changelog +++ b/src/tools/git_changelog @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # # src/tools/git_changelog @@ -59,6 +59,7 @@ require IPC::Open2; # (We could get this from "git branches", but not worth the trouble.) # NB: master must be first! my @BRANCHES = qw(master + REL_18_STABLE REL_17_STABLE REL_16_STABLE REL_15_STABLE REL_14_STABLE REL_13_STABLE REL_12_STABLE REL_11_STABLE REL_10_STABLE REL9_6_STABLE REL9_5_STABLE REL9_4_STABLE REL9_3_STABLE REL9_2_STABLE REL9_1_STABLE REL9_0_STABLE diff --git a/src/tools/ifaddrs/Makefile b/src/tools/ifaddrs/Makefile index 1cb01a8211f2a..006c6c4aa5e1c 100644 --- a/src/tools/ifaddrs/Makefile +++ b/src/tools/ifaddrs/Makefile @@ -2,7 +2,7 @@ # # Makefile for src/tools/ifaddrs # -# Copyright (c) 2003-2025, PostgreSQL Global Development Group +# Copyright (c) 2003-2026, PostgreSQL Global Development Group # # src/tools/ifaddrs/Makefile # diff --git a/src/tools/mark_pgdllimport.pl b/src/tools/mark_pgdllimport.pl index d276250fdb3c1..27dafcc14fe3f 100755 --- a/src/tools/mark_pgdllimport.pl +++ b/src/tools/mark_pgdllimport.pl @@ -15,7 +15,7 @@ # script modifies before committing. This script uses as arguments # a list of the header files to scan for the markings. # -# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group # Portions Copyright (c) 1994, Regents of the University of California # # src/tools/mark_pgdllimport.pl diff --git a/src/tools/msvc_gendef.pl b/src/tools/msvc_gendef.pl index 868aad51b09ca..1a37fe9594705 100644 --- a/src/tools/msvc_gendef.pl +++ b/src/tools/msvc_gendef.pl @@ -1,5 +1,5 @@ -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; @@ -118,9 +118,9 @@ sub writedef { my $isdata = $def->{$f} eq 'data'; - # Strip the leading underscore for win32, but not x64 + # Strip the leading underscore for win32, but not x64 and aarch64 $f =~ s/^_// - unless ($arch eq "x86_64"); + unless ($arch eq "x86_64" || $arch eq "aarch64"); # Emit just the name if it's a function symbol, or emit the name # decorated with the DATA option for variables. @@ -141,7 +141,7 @@ sub writedef sub usage { die("Usage: msvc_gendef.pl --arch --deffile --tempdir files-or-directories\n" - . " arch: x86 | x86_64\n" + . " arch: x86 | x86_64 | aarch64\n" . " deffile: path of the generated file\n" . " tempdir: directory for temporary files\n" . " files or directories: object files or directory containing object files\n" @@ -158,7 +158,7 @@ sub usage 'tempdir:s' => \$tempdir,) or usage(); usage("arch: $arch") - unless ($arch eq 'x86' || $arch eq 'x86_64'); + unless ($arch eq 'x86' || $arch eq 'x86_64' || $arch eq 'aarch64'); my @files; diff --git a/src/tools/pg_bsd_indent/Makefile b/src/tools/pg_bsd_indent/Makefile index 1d6aa76df9474..8ea70f9683261 100644 --- a/src/tools/pg_bsd_indent/Makefile +++ b/src/tools/pg_bsd_indent/Makefile @@ -2,7 +2,7 @@ # # src/tools/pg_bsd_indent/Makefile # -# Copyright (c) 2017-2025, PostgreSQL Global Development Group +# Copyright (c) 2017-2026, PostgreSQL Global Development Group # #------------------------------------------------------------------------- diff --git a/src/tools/pg_bsd_indent/indent.c b/src/tools/pg_bsd_indent/indent.c index 2622cc6227af0..736f350f145c4 100644 --- a/src/tools/pg_bsd_indent/indent.c +++ b/src/tools/pg_bsd_indent/indent.c @@ -229,7 +229,7 @@ main(int argc, char **argv) } if (ps.com_ind <= 1) - ps.com_ind = 2; /* dont put normal comments before column 2 */ + ps.com_ind = 2; /* don't put normal comments before column 2 */ if (block_comment_max_col <= 0) block_comment_max_col = max_col; if (ps.local_decl_indent < 0) /* if not specified by user, set this */ @@ -296,6 +296,7 @@ main(int argc, char **argv) * done earlier. */ force_nl = false; + break; case form_feed: break; case comment: @@ -352,7 +353,7 @@ main(int argc, char **argv) } goto sw_buffer; } - /* FALLTHROUGH */ + pg_fallthrough; default: /* it is the start of a normal statement */ { int remove_newlines; @@ -482,7 +483,7 @@ main(int argc, char **argv) if (verbose) diag2(0, "Line broken"); dump_line(); - ps.want_blank = false; /* dont insert blank at line start */ + ps.want_blank = false; /* don't insert blank at line start */ force_nl = false; } ps.in_stmt = true; /* turn on flag which causes an extra level of @@ -603,7 +604,7 @@ main(int argc, char **argv) force_nl = true;/* must force newline after if */ ps.last_u_d = true; /* inform lexi that a following * operator is unary */ - ps.in_stmt = false; /* dont use stmt continuation + ps.in_stmt = false; /* don't use stmt continuation * indentation */ parse(hd_type); /* let parser worry about if, or whatever */ @@ -749,7 +750,7 @@ main(int argc, char **argv) if (sp_sw) { /* this is a check for an if, while, etc. with * unbalanced parens */ sp_sw = false; - parse(hd_type); /* dont lose the if, or whatever */ + parse(hd_type); /* don't lose the if, or whatever */ } } *e_code++ = ';'; @@ -764,7 +765,7 @@ main(int argc, char **argv) break; case lbrace: /* got a '{' */ - ps.in_stmt = false; /* dont indent the {} */ + ps.in_stmt = false; /* don't indent the {} */ if (!ps.block_init) force_nl = true;/* force other stuff on same line as '{' onto * new line */ @@ -802,7 +803,7 @@ main(int argc, char **argv) } } if (s_code == e_code) - ps.ind_stmt = false; /* dont put extra indentation on line + ps.ind_stmt = false; /* don't put extra indentation on line * with '{' */ if (ps.in_decl && ps.in_or_st) { /* this is either a structure * declaration or an init */ @@ -922,7 +923,7 @@ main(int argc, char **argv) case structure: if (ps.p_l_follow > 0) goto copy_id; - /* FALLTHROUGH */ + pg_fallthrough; case decl: /* we have a declaration type (int, etc.) */ parse(decl); /* let parser worry about indentation */ if (ps.last_token == rparen && ps.tos <= 1) { @@ -1013,7 +1014,7 @@ main(int argc, char **argv) case period: /* treat a period kind of like a binary * operation */ *e_code++ = '.'; /* move the period into line */ - ps.want_blank = false; /* dont put a blank after a period */ + ps.want_blank = false; /* don't put a blank after a period */ break; case comma: diff --git a/src/tools/pg_bsd_indent/io.c b/src/tools/pg_bsd_indent/io.c index 9d64ca1ee56da..62d600bbb1167 100644 --- a/src/tools/pg_bsd_indent/io.c +++ b/src/tools/pg_bsd_indent/io.c @@ -97,7 +97,7 @@ dump_line(void) putc('\n', output); n_real_blanklines = 0; if (ps.ind_level == 0) - ps.ind_stmt = 0; /* this is a class A kludge. dont do + ps.ind_stmt = 0; /* this is a class A kludge. don't do * additional statement indentation if we are * at bracket level 0 */ diff --git a/src/tools/pg_bsd_indent/meson.build b/src/tools/pg_bsd_indent/meson.build index 9909ee46581f7..3d292e8febb35 100644 --- a/src/tools/pg_bsd_indent/meson.build +++ b/src/tools/pg_bsd_indent/meson.build @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2025, PostgreSQL Global Development Group +# Copyright (c) 2022-2026, PostgreSQL Global Development Group pg_bsd_indent_sources = files( 'args.c', diff --git a/src/tools/pg_bsd_indent/parse.c b/src/tools/pg_bsd_indent/parse.c index e707da639c71e..94cea72439378 100644 --- a/src/tools/pg_bsd_indent/parse.c +++ b/src/tools/pg_bsd_indent/parse.c @@ -96,7 +96,7 @@ parse(int tk) /* tk: the code for the construct scanned */ */ ps.i_l_follow = ps.il[ps.tos--]; /* the rest is the same as for dolit and forstmt */ - /* FALLTHROUGH */ + pg_fallthrough; case dolit: /* 'do' */ case forstmt: /* for (...) */ ps.p_stack[++ps.tos] = tk; @@ -303,7 +303,7 @@ reduce(void) case swstmt: /* */ case_ind = ps.cstk[ps.tos - 1]; - /* FALLTHROUGH */ + pg_fallthrough; case decl: /* finish of a declaration */ case elsehead: /* < else> */ diff --git a/src/tools/pg_bsd_indent/pr_comment.c b/src/tools/pg_bsd_indent/pr_comment.c index a9572b39ffe68..7dd0f59e3f7d6 100644 --- a/src/tools/pg_bsd_indent/pr_comment.c +++ b/src/tools/pg_bsd_indent/pr_comment.c @@ -117,7 +117,7 @@ pr_comment(void) if ( /* ps.bl_line && */ (s_lab == e_lab) && (s_code == e_code)) { /* klg: check only if this line is blank */ /* - * If this (*and previous lines are*) blank, dont put comment way + * If this (*and previous lines are*) blank, don't put comment way * out at left */ ps.com_col = (ps.ind_level - ps.unindent_displace) * ps.ind_size + 1; @@ -234,7 +234,7 @@ pr_comment(void) last_bl = NULL; CHECK_SIZE_COM(4); if (ps.box_com || ps.last_nl) { /* if this is a boxed comment, - * we dont ignore the newline */ + * we don't ignore the newline */ if (s_com == e_com) *e_com++ = ' '; if (!ps.box_com && e_com - s_com > 3) { diff --git a/src/tools/pg_bsd_indent/t/001_pg_bsd_indent.pl b/src/tools/pg_bsd_indent/t/001_pg_bsd_indent.pl index bd70c916d9beb..2918371d27ad7 100644 --- a/src/tools/pg_bsd_indent/t/001_pg_bsd_indent.pl +++ b/src/tools/pg_bsd_indent/t/001_pg_bsd_indent.pl @@ -1,7 +1,7 @@ # pg_bsd_indent: some simple tests # The test cases come from FreeBSD upstream, but this test scaffolding is ours. -# Copyright (c) 2017-2025, PostgreSQL Global Development Group +# Copyright (c) 2017-2026, PostgreSQL Global Development Group use strict; use warnings FATAL => 'all'; diff --git a/src/tools/pgflex b/src/tools/pgflex index 3986b06874e75..b8d9aa0086fbb 100755 --- a/src/tools/pgflex +++ b/src/tools/pgflex @@ -48,7 +48,7 @@ os.chdir(args.privatedir) # contents. Set FLEX_TMP_DIR to the target private directory to avoid # that. That environment variable isn't consulted on other platforms, so we # don't even need to make this conditional. -env = {'FLEX_TMP_DIR': args.privatedir} +os.environ['FLEX_TMP_DIR'] = args.privatedir # build flex invocation command = [args.flex, '-o', args.output_file] @@ -58,7 +58,7 @@ command += args.flex_flags command += [args.input_file] # create .c file from .l file -sp = subprocess.run(command, env=env) +sp = subprocess.run(command) if sp.returncode != 0: sys.exit(sp.returncode) diff --git a/src/tools/pginclude/README b/src/tools/pginclude/README index 2f8fe6b78baa5..4e1c486d05156 100644 --- a/src/tools/pginclude/README +++ b/src/tools/pginclude/README @@ -55,8 +55,15 @@ and are skipped by the headerscheck script. The easy way to run the script is to say "make -s headerscheck" in the top-level build directory after completing a build. You should -have included "--with-perl --with-python" in your configure options, -else you're likely to get errors about related headers not being found. +have included at least + + --with-llvm --with-perl --with-python + +in your configure options, else you're likely to get errors about +related headers not being found. + +When using meson, run "meson compile -C build headerscheck" or "ninja +-C build headerscheck". A limitation of the current script is that it doesn't know exactly which headers are for frontend or backend; when in doubt it uses postgres.h as @@ -78,12 +85,15 @@ and are skipped by the script in the --cplusplus mode. The easy way to run the script is to say "make -s cpluspluscheck" in the top-level build directory after completing a build. You should -have included "--with-perl --with-python" in your configure options, -else you're likely to get errors about related headers not being found. +have included at least + + --with-llvm --with-perl --with-python + +in your configure options, else you're likely to get errors about +related headers not being found. -If you are using a non-g++-compatible C++ compiler, you may need to -override the script's CXXFLAGS setting by setting a suitable environment -value. +When using meson, run "meson compile -C build cpluspluscheck" or +"ninja -C build cpluspluscheck". A limitation of the current script is that it doesn't know exactly which headers are for frontend or backend; when in doubt it uses postgres.h as diff --git a/src/tools/pginclude/headerscheck b/src/tools/pginclude/headerscheck index 9e86d0493621e..785d6f867ad09 100755 --- a/src/tools/pginclude/headerscheck +++ b/src/tools/pginclude/headerscheck @@ -13,7 +13,7 @@ # No output if everything is OK, else compiler errors. # # src/tools/pginclude/headerscheck -# Copyright (c) 2009-2025, PostgreSQL Global Development Group +# Copyright (c) 2009-2026, PostgreSQL Global Development Group # option to check for C++ compatibility if [ "$1" = "--cplusplus" ]; then @@ -37,13 +37,13 @@ fi me=`basename $0` -# These switches are g++ specific, you may override if necessary. -CXXFLAGS=${CXXFLAGS:- -fsyntax-only -Wall} - # Pull some info from configure's results. MGLOB="$builddir/src/Makefile.global" CPPFLAGS=`sed -n 's/^CPPFLAGS[ ]*=[ ]*//p' "$MGLOB"` CFLAGS=`sed -n 's/^CFLAGS[ ]*=[ ]*//p' "$MGLOB"` +CXXFLAGS=`sed -n 's/^CXXFLAGS[ ]*=[ ]*//p' "$MGLOB"` +ICU_CFLAGS=`sed -n 's/^ICU_CFLAGS[ ]*=[ ]*//p' "$MGLOB"` +LLVM_CPPFLAGS=`sed -n 's/^LLVM_CPPFLAGS[ ]*=[ ]*//p' "$MGLOB"` CC=`sed -n 's/^CC[ ]*=[ ]*//p' "$MGLOB"` CXX=`sed -n 's/^CXX[ ]*=[ ]*//p' "$MGLOB"` PG_SYSROOT=`sed -n 's/^PG_SYSROOT[ ]*=[ ]*//p' "$MGLOB"` @@ -64,15 +64,17 @@ if $cplusplus; then -I*|-D*) CXXPPFLAGS="$CXXPPFLAGS $flag";; esac done - COMPILER_FLAGS="$CXXPPFLAGS $CXXFLAGS" + COMPILER_FLAGS="$CXXPPFLAGS $CXXFLAGS $ICU_CFLAGS $LLVM_CPPFLAGS" else ext=c COMPILER=${CC:-gcc} - COMPILER_FLAGS="$CPPFLAGS $CFLAGS" + COMPILER_FLAGS="$CPPFLAGS $CFLAGS $ICU_CFLAGS $LLVM_CPPFLAGS" fi -# Create temp directory. -tmp=`mktemp -d /tmp/$me.XXXXXX` +# Create temp directory. Help ccache by using a stable name. Include +# extension to allow running C and C++ checks in parallel. +tmp="tmp_${me}_${ext}" +mkdir -p "$tmp" trap "ret=$?; rm -rf $tmp; exit $ret" 0 1 2 3 15 @@ -85,6 +87,7 @@ do # These files are platform-specific, and c.h will include the # one that's relevant for our current platform anyway. + test "$f" = src/include/port/aix.h && continue test "$f" = src/include/port/cygwin.h && continue test "$f" = src/include/port/darwin.h && continue test "$f" = src/include/port/freebsd.h && continue @@ -97,7 +100,7 @@ do # Additional Windows-specific headers. test "$f" = src/include/port/win32_port.h && continue test "$f" = src/include/port/win32/netdb.h && continue - $cplusplus && test "$f" = src/include/port/win32/sys/resource.h && continue + test "$f" = src/include/port/win32/sys/resource.h && continue test "$f" = src/include/port/win32/sys/socket.h && continue test "$f" = src/include/port/win32_msvc/dirent.h && continue test "$f" = src/include/port/win32_msvc/utime.h && continue @@ -113,26 +116,29 @@ do test "$f" = src/include/port/atomics/generic.h && continue test "$f" = src/include/port/atomics/generic-gcc.h && continue test "$f" = src/include/port/atomics/generic-msvc.h && continue - test "$f" = src/include/port/atomics/generic-sunpro.h && continue # sepgsql.h depends on headers that aren't there on most platforms. test "$f" = contrib/sepgsql/sepgsql.h && continue - # nodetags.h cannot be included standalone: it's just a code fragment. + # These files cannot be included standalone, because they contain + # code fragments. test "$f" = src/include/nodes/nodetags.h && continue test "$f" = src/backend/nodes/nodetags.h && continue + test "$f" = src/include/storage/checksum_block_internal.h && continue # These files are not meant to be included standalone, because # they contain lists that might have multiple use-cases. test "$f" = src/include/access/rmgrlist.h && continue test "$f" = src/include/parser/kwlist.h && continue - test "$f" = src/pl/plpgsql/src/pl_reserved_kwlist.h && continue - test "$f" = src/pl/plpgsql/src/pl_unreserved_kwlist.h && continue - test "$f" = src/interfaces/ecpg/preproc/c_kwlist.h && continue - test "$f" = src/interfaces/ecpg/preproc/ecpg_kwlist.h && continue + test "$f" = src/include/postmaster/proctypelist.h && continue test "$f" = src/include/regex/regerrs.h && continue test "$f" = src/include/storage/lwlocklist.h && continue + test "$f" = src/include/storage/subsystemlist.h && continue test "$f" = src/include/tcop/cmdtaglist.h && continue + test "$f" = src/interfaces/ecpg/preproc/c_kwlist.h && continue + test "$f" = src/interfaces/ecpg/preproc/ecpg_kwlist.h && continue + test "$f" = src/pl/plpgsql/src/pl_reserved_kwlist.h && continue + test "$f" = src/pl/plpgsql/src/pl_unreserved_kwlist.h && continue test "$f" = src/pl/plpgsql/src/plerrcodes.h && continue test "$f" = src/pl/plpython/spiexceptions.h && continue test "$f" = src/pl/tcl/pltclerrcodes.h && continue @@ -150,10 +156,13 @@ do test "$f" = src/include/catalog/syscache_ids.h && continue test "$f" = src/include/catalog/syscache_info.h && continue + test "$f" = src/interfaces/libpq/oauth-debug.h && continue + # We can't make these Bison output files compilable standalone # without using "%code require", which old Bison versions lack. # parser/gram.h will be included by parser/gramparse.h anyway. test "$f" = contrib/cube/cubeparse.h && continue + test "$f" = contrib/pg_plan_advice/pgpa_parser.h && continue test "$f" = contrib/seg/segparse.h && continue test "$f" = src/backend/bootstrap/bootparse.h && continue test "$f" = src/backend/parser/gram.h && continue @@ -198,6 +207,9 @@ do test "$f" = src/bin/pg_dump/pg_dump.h && continue fi + # Help ccache by using a stable name. Remove slashes and dots. + test_file_name=$(printf '%s' "$f" | tr '/.' '__') + # OK, create .c file to include this .h file. { $cplusplus && echo 'extern "C" {' @@ -206,11 +218,16 @@ do # info to guess which, but in some subdirectories there's a # reasonable choice to make, and otherwise we use postgres.h. # Also, those three files should compile with no pre-include, as - # should src/interfaces headers meant to be exposed to clients. + # should files included early by c.h, as well as + # src/interfaces headers meant to be exposed to clients. case "$f" in + src/include/c.h) ;; + src/include/pg_config.h) ;; + src/include/pg_config_manual.h) ;; + src/include/pg_config_os.h) ;; src/include/postgres.h) ;; + src/include/postgres_ext.h) ;; src/include/postgres_fe.h) ;; - src/include/c.h) ;; src/interfaces/libpq/libpq-fe.h) ;; src/interfaces/libpq/libpq-events.h) ;; src/interfaces/ecpg/ecpglib/ecpglib_extern.h) @@ -230,7 +247,7 @@ do esac echo "#include \"$f\"" $cplusplus && echo '};' - } >$tmp/test.$ext + } >$tmp/$test_file_name.$ext # Some subdirectories need extra -I switches. case "$f" in @@ -252,10 +269,12 @@ do if ! $COMPILER $COMPILER_FLAGS -I $builddir -I $srcdir \ -I $builddir/src/include -I $srcdir/src/include \ -I $builddir/src/interfaces/libpq -I $srcdir/src/interfaces/libpq \ - $EXTRAINCLUDES $EXTRAFLAGS -c $tmp/test.$ext -o $tmp/test.o + $EXTRAINCLUDES $EXTRAFLAGS -c "$tmp/$test_file_name.$ext" -o "$tmp/$test_file_name.o" then exit_status=1 fi + + rm -f "$tmp/$test_file_name.$ext" "$tmp/$test_file_name.o" done exit $exit_status diff --git a/src/tools/pgindent/pgindent b/src/tools/pgindent/pgindent index 54e138b598dfe..b2ec5e2914bec 100755 --- a/src/tools/pgindent/pgindent +++ b/src/tools/pgindent/pgindent @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright (c) 2021-2025, PostgreSQL Global Development Group +# Copyright (c) 2021-2026, PostgreSQL Global Development Group # Program to maintain uniform layout style in our C code. # Exit codes: @@ -19,6 +19,21 @@ use File::Temp; use IO::Handle; use Getopt::Long; +# By default Perl's SIGINT/SIGTERM kill the process without running +# END blocks, so File::Temp never gets to clean up. Converting the +# signal into an exit() makes END-block cleanup run. See: +# . Exit codes use the +# 128+signum convention so callers can tell the process was killed by +# a signal. +$SIG{INT} = sub { exit 130; }; # 128 + 2 (SIGINT) +$SIG{TERM} = sub { exit 143; }; # 128 + 15 (SIGTERM) + +# pg_bsd_indent creates a .BAK file that File::Temp doesn't know about. This +# END block makes sure that that file is cleaned up in case someone presses +# Ctrl+C during pgindent. +my $bak_to_cleanup; +END { unlink $bak_to_cleanup if defined $bak_to_cleanup; } + # Update for pg_bsd_indent version my $INDENT_VERSION = "2.1.2"; @@ -73,11 +88,14 @@ if ($sourcedir) # might make them so. For the moment we just hardwire a list of names # to add and a list of names to exclude; eventually this may need to be # easier to configure. Note that the typedefs need trailing newlines. -my @additional = ("bool\n"); +my @additional = map { "$_\n" } qw( + bool regex_t regmatch_t regoff +); my %excluded = map { +"$_\n" => 1 } qw( - ANY FD_SET U abs allocfunc boolean date digit ilist interval iterator other - pointer printfunc reference string timestamp type wrap + FD_SET LookupSet boolean date duration + element_type inquiry iterator other + pointer reference rep string timestamp type wrap ); # globals @@ -221,6 +239,9 @@ sub pre_indent { my $source = shift; + ## Ensure file ends with a newline (pg_bsd_indent messes up otherwise) + $source .= "\n" unless substr($source, -1) eq "\n"; + ## Comments # Convert // comments to /* */ @@ -242,14 +263,6 @@ sub pre_indent # Protect wrapping in CATALOG() $source =~ s!^(CATALOG\(.*)$!/*$1*/!gm; - # Treat a CURL_IGNORE_DEPRECATION() as braces for the purposes of - # indentation. (The recursive regex comes from the perlre documentation; it - # matches balanced parentheses as group $1 and the contents as group $2.) - my $curlopen = '{ /* CURL_IGNORE_DEPRECATION */'; - my $curlclose = '} /* CURL_IGNORE_DEPRECATION */'; - $source =~ - s!^[ \t]+CURL_IGNORE_DEPRECATION(\(((?:(?>[^()]+)|(?1))*)\))!$curlopen$2$curlclose!gms; - return $source; } @@ -264,12 +277,6 @@ sub post_indent $source =~ s!^/\* Open extern "C" \*/$!{!gm; $source =~ s!^/\* Close extern "C" \*/$!}!gm; - # Restore the CURL_IGNORE_DEPRECATION() macro, keeping in mind that our - # markers may have been re-indented. - $source =~ - s!{[ \t]+/\* CURL_IGNORE_DEPRECATION \*/!CURL_IGNORE_DEPRECATION(!gm; - $source =~ s!}[ \t]+/\* CURL_IGNORE_DEPRECATION \*/!)!gm; - ## Comments # Undo change of dash-protected block comments @@ -298,11 +305,14 @@ sub run_indent print $tmp_fh $source; $tmp_fh->close(); + $bak_to_cleanup = "$filename.BAK"; + $$error_message = `$cmd $filename 2>&1`; return "" if ($? || length($$error_message) > 0); - unlink "$filename.BAK"; + unlink $bak_to_cleanup; + $bak_to_cleanup = undef; open(my $src_out, '<', $filename) || die $!; local ($/) = undef; diff --git a/src/tools/pgindent/pgperltidy b/src/tools/pgindent/pgperltidy index 6af27d21d5531..87838d6bde3a4 100755 --- a/src/tools/pgindent/pgperltidy +++ b/src/tools/pgindent/pgperltidy @@ -7,6 +7,12 @@ set -e # set this to override default perltidy program: PERLTIDY=${PERLTIDY:-perltidy} +PERLTIDY_VERSION=20230309 +if ! $PERLTIDY -v | grep -q $PERLTIDY_VERSION; then + echo "You do not appear to have $PERLTIDY version $PERLTIDY_VERSION installed on your system." >&2 + exit 1 +fi + . src/tools/perlcheck/find_perl_files find_perl_files "$@" | xargs $PERLTIDY --profile=src/tools/pgindent/perltidyrc diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a8346cda633ac..9f1dd55213d07 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -6,6 +6,8 @@ ASN1_INTEGER ASN1_OBJECT ASN1_OCTET_STRING ASN1_STRING +ATAlterConstraint +AttrResultArgMap AV A_ArrayExpr A_Const @@ -29,6 +31,8 @@ AddForeignUpdateTargets_function AddrInfo AffixNode AffixNodeData +AfterTriggerBatchCallback +AfterTriggerCallbackItem AfterTriggerEvent AfterTriggerEventChunk AfterTriggerEventData @@ -41,23 +45,25 @@ AfterTriggersTableData AfterTriggersTransData Agg AggClauseCosts +AggClauseInfo AggInfo AggPath AggSplit AggState AggStatePerAgg +AggStatePerAggData AggStatePerGroup AggStatePerGroupData AggStatePerHash +AggStatePerHashData AggStatePerPhase +AggStatePerPhaseData AggStatePerTrans +AggStatePerTransData AggStrategy AggTransInfo Aggref AggregateInstrumentation -AioWorkerControl -AioWorkerSlot -AioWorkerSubmissionQueue AlenState Alias AllocBlock @@ -74,6 +80,7 @@ AlterDatabaseSetStmt AlterDatabaseStmt AlterDefaultPrivilegesStmt AlterDomainStmt +AlterDomainType AlterEnumStmt AlterEventTrigStmt AlterExtensionContentsStmt @@ -87,6 +94,8 @@ AlterOpFamilyStmt AlterOperatorStmt AlterOwnerStmt AlterPolicyStmt +AlterPropGraphElementKind +AlterPropGraphStmt AlterPublicationAction AlterPublicationStmt AlterReplicationSlotCmd @@ -121,6 +130,7 @@ AnlIndexData AnyArrayType Append AppendPath +AppendPathInput AppendRelInfo AppendState ApplyErrorCallbackArg @@ -140,6 +150,9 @@ ArchiveOpts ArchiveShutdownCB ArchiveStartupCB ArchiveStreamState +ArchivedWALFile +ArchivedWAL_hash +ArchivedWAL_iterator ArchiverOutput ArchiverStage ArrayAnalyzeExtraData @@ -152,6 +165,7 @@ ArrayExpr ArrayExprIterState ArrayIOData ArrayIterator +ArrayIteratorData ArrayMapState ArrayMetaState ArraySortCachedInfo @@ -161,7 +175,6 @@ ArrayType AsyncQueueControl AsyncQueueEntry AsyncRequest -ATAlterConstraint AttInMetadata AttStatsSlot AttoptCacheEntry @@ -174,9 +187,10 @@ AttrNumber AttributeOpts AuthRequest AuthToken -AutoPrewarmSharedState AutoPrewarmReadStreamData +AutoPrewarmSharedState AutoVacOpts +AutoVacuumScores AutoVacuumShmemStruct AutoVacuumWorkItem AutoVacuumWorkItemType @@ -222,7 +236,6 @@ BTScanInsertData BTScanKeyPreproc BTScanOpaque BTScanOpaqueData -BTScanPos BTScanPosData BTScanPosItem BTShared @@ -252,6 +265,7 @@ Barrier BaseBackupCmd BaseBackupTargetHandle BaseBackupTargetType +BatchMVCCState BeginDirectModify_function BeginForeignInsert_function BeginForeignModify_function @@ -270,8 +284,9 @@ BitmapAndPath BitmapAndState BitmapHeapPath BitmapHeapScan -BitmapHeapScanInstrumentation BitmapHeapScanDesc +BitmapHeapScanDescData +BitmapHeapScanInstrumentation BitmapHeapScanState BitmapIndexScan BitmapIndexScanState @@ -297,6 +312,7 @@ BlockSampler BlockSamplerData BlockedProcData BlockedProcsData +BlocksReadStreamData BlocktableEntry BloomBuildState BloomFilter @@ -341,13 +357,14 @@ BufFile Buffer BufferAccessStrategy BufferAccessStrategyType -BufferCacheNumaRec -BufferCacheNumaContext +BufferCacheOsPagesContext +BufferCacheOsPagesRec BufferCachePagesContext BufferCachePagesRec BufferDesc BufferDescPadded BufferHeapTupleTableSlot +BufferLockMode BufferLookupEnt BufferManagerRelation BufferStrategyControl @@ -361,6 +378,7 @@ BulkWriteBuffer BulkWriteState BumpBlock BumpContext +ByteaSortSupport CACHESIGN CAC_state CCFastEqualFN @@ -382,6 +400,9 @@ CTEMaterialize CTESearchClause CURL CURLM +CURLMcode +CURLMsg +CURLcode CURLoption CV CachedExpression @@ -410,13 +431,17 @@ CatCacheHeader CatalogId CatalogIdMapEntry CatalogIndexState +ChangeContext ChangeVarNodes_callback ChangeVarNodes_context +ChannelName CheckPoint CheckPointStmt CheckpointStatsData CheckpointerRequest CheckpointerShmemStruct +ChecksumBarrierCondition +ChecksumStateType Chromosome CkptSortItem CkptTsStatus @@ -486,6 +511,7 @@ CompressFileHandle CompressionLocation CompressorState ComputeXidHorizonsResult +ConcurrentChangeKind ConditionVariable ConditionVariableMinimallyPadded ConditionalStack @@ -516,17 +542,18 @@ ConversionLocation ConvertRowtypeExpr CookedConstraint CopyDest +CopyFormat CopyFormatOptions CopyFromRoutine CopyFromState CopyFromStateData -CopyHeaderChoice CopyInsertMethod CopyLogVerbosityChoice CopyMethod CopyMultiInsertBuffer CopyMultiInsertInfo CopyOnErrorChoice +CopySeqResult CopySource CopyStmt CopyToRoutine @@ -555,12 +582,12 @@ CreateOpClassStmt CreateOpFamilyStmt CreatePLangStmt CreatePolicyStmt +CreatePropGraphStmt CreatePublicationStmt CreateRangeStmt CreateReplicationSlotCmd CreateRoleStmt CreateSchemaStmt -CreateSchemaStmtContext CreateSeqStmt CreateStatsStmt CreateStmt @@ -587,7 +614,9 @@ CustomScan CustomScanMethods CustomScanState CycleCtr +DataChecksumsWorkerOperation DBState +DbOidName DCHCacheEntry DEADLOCK_INFO DECountItem @@ -600,12 +629,18 @@ DR_intorel DR_printtup DR_sqlfunction DR_transientrel +DSMREntryType DSMRegistryCtxStruct DSMRegistryEntry DWORD +DataChecksumsStateStruct +DataChecksumsWorkerDatabase +DataChecksumsWorkerResult DataDirSyncMethod DataDumperPtr DataPageDeleteStack +DdlOptType +DdlOption DataTypesUsageChecks DataTypesUsageVersionCheck DatabaseInfo @@ -616,7 +651,6 @@ DatumTupleFields DbInfo DbInfoArr DbLocaleInfo -DbOidName DeClonePtrType DeadLockState DeallocateStmt @@ -624,11 +658,16 @@ DeclareCursorStmt DecodedBkpBlock DecodedXLogRecord DecodingOutputState +DecodingWorker +DecodingWorkerShared DefElem DefElemAction DefaultACLInfo DefineStmt +DefnDumperPtr DeleteStmt +DependenciesParseState +DependenciesSemanticState DependencyGenerator DependencyGeneratorData DependencyType @@ -677,9 +716,8 @@ DumpableObjectType DumpableObjectWithAcl DynamicFileList DynamicZoneAbbrev -EC_KEY -ECDerivesKey ECDerivesEntry +ECDerivesKey EDGE ENGINE EOM_flatten_into_method @@ -697,6 +735,7 @@ EachState Edge EditableObjectType ElementsState +ElidedNode EnableTimeoutParams EndDataPtrType EndDirectModify_function @@ -761,10 +800,12 @@ ExpandedRange ExpandedRecordFieldInfo ExpandedRecordHeader ExplainDirectModify_function +ExplainExtensionOption ExplainForeignModify_function ExplainForeignScan_function ExplainFormat ExplainOneQuery_hook_type +ExplainOptionHandler ExplainSerializeOption ExplainState ExplainStmt @@ -787,11 +828,15 @@ ExtensibleNodeEntry ExtensibleNodeMethods ExtensionControlFile ExtensionInfo +ExtensionLocation +ExtensionSiblingCache ExtensionVersionInfo +FastPathMeta FDWCollateState FD_SET FILE FILETIME +FPI FSMAddress FSMPage FSMPageData @@ -801,12 +846,12 @@ FastPathStrongRelationLockData FdwInfo FdwRoutine FetchDirection +FetchDirectionKeywords FetchStmt FieldSelect FieldStore File FileBackupMethod -FileCopyMethod FileFdwExecutionState FileFdwPlanState FileNameMap @@ -834,6 +879,9 @@ ForBothState ForEachState ForFiveState ForFourState +ForPortionOfClause +ForPortionOfExpr +ForPortionOfState ForThreeState ForeignAsyncConfigureWait_function ForeignAsyncNotify_function @@ -883,6 +931,11 @@ FormData_pg_opfamily FormData_pg_partitioned_table FormData_pg_policy FormData_pg_proc +FormData_pg_propgraph_element +FormData_pg_propgraph_element_label +FormData_pg_propgraph_label +FormData_pg_propgraph_label_property +FormData_pg_propgraph_property FormData_pg_publication FormData_pg_publication_namespace FormData_pg_publication_rel @@ -1071,8 +1124,10 @@ GinPostingList GinPostingTreeScanItem GinQualCounts GinScanEntry +GinScanEntryData GinScanItem GinScanKey +GinScanKeyData GinScanOpaque GinScanOpaqueData GinSegmentInfo @@ -1093,13 +1148,22 @@ GistSplitUnion GistSplitVector GistTsVectorOptions GistVacState +GlobalChannelEntry +GlobalChannelKey GlobalTransaction +GlobalTransactionData GlobalVisHorizonKind GlobalVisState GrantRoleOptions GrantRoleStmt GrantStmt GrantTargetType +GraphElementPattern +GraphElementPatternKind +GraphLabelRef +GraphPattern +GraphPropertyRef +GraphTableParseState Group GroupByOrdering GroupClause @@ -1108,6 +1172,7 @@ GroupPathExtraData GroupResultPath GroupState GroupVarInfo +GroupingExprInfo GroupingFunc GroupingSet GroupingSetData @@ -1158,6 +1223,7 @@ HashAggBatch HashAggSpill HashAllocFunc HashBuildState +HashBulkDeleteStreamPrivate HashCompareFunc HashCopyFunc HashIndexStat @@ -1190,6 +1256,7 @@ HeapCheckContext HeapCheckReadStreamData HeapPageFreeze HeapScanDesc +HeapScanDescData HeapTuple HeapTupleData HeapTupleFields @@ -1199,6 +1266,8 @@ HeapTupleHeader HeapTupleHeaderData HeapTupleTableSlot HistControl +HostsFileLoadResult +HostsLine HotStandbyState I32 ICU_Convert_Func @@ -1211,6 +1280,7 @@ IOContext IOFuncSelector IOObject IOOp +IOStats IO_STATUS_BLOCK IPCompareMethod ITEM @@ -1249,10 +1319,12 @@ IndexClause IndexClauseSet IndexDeleteCounts IndexDeletePrefetchState +IndexDoCheckCallback IndexElem IndexFetchHeapData IndexFetchTableData IndexInfo +IndexInsertState IndexList IndexOnlyScan IndexOnlyScanState @@ -1262,6 +1334,7 @@ IndexPath IndexRuntimeKeyInfo IndexScan IndexScanDesc +IndexScanDescData IndexScanInstrumentation IndexScanState IndexStateFlagsAction @@ -1279,13 +1352,15 @@ InheritableSocket InitSampleScan_function InitializeDSMForeignScan_function InitializeWorkerForeignScan_function +InjIoErrorState InjectionPointCacheEntry InjectionPointCallback InjectionPointCondition InjectionPointConditionType +InjectionPointData InjectionPointEntry -InjectionPointsCtl InjectionPointSharedState +InjectionPointsCtl InlineCodeBlock InsertStmt Instrumentation @@ -1302,7 +1377,6 @@ IntoClause InvalMessageArray InvalidationInfo InvalidationMsgsGroup -IoMethod IoMethodOps IpcMemoryId IpcMemoryKey @@ -1314,7 +1388,6 @@ IsForeignRelUpdatable_function IsForeignScanParallelSafe_function IsoConnInfo IspellDict -Item ItemArray ItemId ItemIdData @@ -1492,8 +1565,7 @@ LLVMOrcResourceTrackerRef LLVMOrcSymbolStringPoolRef LLVMOrcThreadSafeContextRef LLVMOrcThreadSafeModuleRef -LLVMPassManagerBuilderRef -LLVMPassManagerRef +LLVMPassBuilderOptionsRef LLVMTargetMachineRef LLVMTargetRef LLVMTypeRef @@ -1527,6 +1599,7 @@ LWLock LWLockHandle LWLockMode LWLockPadded +LWLockTrancheShmemData LZ4F_compressionContext_t LZ4F_decompressOptions_t LZ4F_decompressionContext_t @@ -1558,6 +1631,7 @@ ListParsedLex ListenAction ListenActionKind ListenStmt +ListenerEntry LoInfo LoadStmt LocalBufferLookupEnt @@ -1582,7 +1656,6 @@ LockTupleMode LockViewRecurse_context LockWaitPolicy LockingClause -LogConnectionOption LogOpts LogStmtLevel LogicalDecodeBeginCB @@ -1607,6 +1680,7 @@ LogicalDecodeStreamStopCB LogicalDecodeStreamTruncateCB LogicalDecodeTruncateCB LogicalDecodingContext +LogicalDecodingCtlData LogicalErrorCallbackState LogicalOutputPluginInit LogicalOutputPluginWriterPrepareWrite @@ -1623,6 +1697,7 @@ LogicalRepRelId LogicalRepRelMapEntry LogicalRepRelation LogicalRepRollbackPreparedTxnData +LogicalRepSequenceInfo LogicalRepStreamAbortData LogicalRepTupleData LogicalRepTyp @@ -1633,6 +1708,7 @@ LogicalSlotInfo LogicalSlotInfoArr LogicalTape LogicalTapeSet +LookupSet LsnReadQueue LsnReadQueueNextFun LsnReadQueueNextStatus @@ -1640,6 +1716,9 @@ LtreeGistOptions LtreeSignature MAGIC MBuf +MCVHashContext +MCVHashEntry +MCVHashTable_hash MCVItem MCVList MEMORY_BASIC_INFORMATION @@ -1657,8 +1736,8 @@ ManyTestResourceKind Material MaterialPath MaterialState -MdfdVec MdPathStr +MdfdVec Memoize MemoizeEntry MemoizeInstrumentation @@ -1672,6 +1751,7 @@ MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextId MemoryContextMethodID MemoryContextMethods MemoryStatsPrintFunc @@ -1706,19 +1786,22 @@ ModifyTablePath ModifyTableState MonotonicFunction MorphOpaque -MsgType MultiAssignRef MultiSortSupport MultiSortSupportData MultiXactId MultiXactMember +MultiXactMemberSlruReadContext MultiXactOffset +MultiXactOffset32 MultiXactStateData MultiXactStatus MultirangeIOData MultirangeParseState MultirangeType NDBOX +NDistinctParseState +NDistinctSemanticState NLSVERSIONINFOEX NODE NTSTATUS @@ -1730,7 +1813,9 @@ Name NameData NameHashEntry NamedArgExpr -NamedLWLockTranche +NamedDSAState +NamedDSHState +NamedDSMState NamedLWLockTrancheRequest NamedTuplestoreScan NamedTuplestoreScanState @@ -1745,11 +1830,15 @@ NextSampleBlock_function NextSampleTuple_function NextValueExpr Node +NodeInstrumentation NodeTag NonEmptyRange +NoneCompressorState +NotNullSource Notification NotificationList NotifyStmt +NotnullHashEntry Nsrt NtDllRoutine NtFlushBuffersFileEx_t @@ -1765,6 +1854,7 @@ NumericSortSupport NumericSumAccum NumericVar OAuthValidatorCallbacks +OAuthValidatorModuleInit OM_uint32 OP OSAPerGroupState @@ -1792,14 +1882,15 @@ OffsetVarNodes_context Oid OidOptions OkeysState +OldMultiXactReader OldToNewMapping OldToNewMappingData OnCommitAction OnCommitItem OnConflictAction +OnConflictActionState OnConflictClause OnConflictExpr -OnConflictSetState OpClassCacheEnt OpExpr OpFamilyMember @@ -1834,7 +1925,6 @@ PGCALL2 PGCRYPTO_SHA_t PGChecksummablePage PGContextVisibility -PGErrorVerbosity PGEvent PGEventConnDestroy PGEventConnReset @@ -1872,7 +1962,6 @@ PGTargetServerType PGTernaryBool PGTransactionStatusType PGVerbosity -PG_Locale_Strategy PG_Lock_Status PG_init_t PGauthData @@ -1884,6 +1973,7 @@ PGdataValue PGlobjfuncs PGnotify PGoauthBearerRequest +PGoauthBearerRequestV2 PGpipelineStatus PGpromptOAuthDevice PGresAttDesc @@ -1904,7 +1994,6 @@ PLpgSQL_exception PLpgSQL_exception_block PLpgSQL_execstate PLpgSQL_expr -PLpgSQL_func_hashkey PLpgSQL_function PLpgSQL_getdiag_kind PLpgSQL_if_elsif @@ -1982,6 +2071,7 @@ PLyScalarToOb PLySubtransactionData PLySubtransactionObject PLyTransformToOb +PLyTrigType PLyTupleToOb PLyUnicode_FromStringAndSize_t PLy_elog_impl_t @@ -2028,6 +2118,9 @@ PVIndStats PVIndVacStatus PVOID PVShared +PVSharedCostParams +PVWorkerUsage +PVWorkerStats PX_Alias PX_Cipher PX_Combo @@ -2102,10 +2195,12 @@ PartitionCmd PartitionDesc PartitionDescData PartitionDirectory +PartitionDirectoryData PartitionDirectoryEntry PartitionDispatch PartitionElem PartitionHashBound +PartitionIndexExtDepEntry PartitionKey PartitionListValue PartitionMap @@ -2121,6 +2216,7 @@ PartitionRangeBound PartitionRangeDatum PartitionRangeDatumKind PartitionScheme +PartitionSchemeData PartitionSpec PartitionStrategy PartitionTupleRouting @@ -2140,6 +2236,8 @@ PatternInfoArray Pattern_Prefix_Status Pattern_Type PendingFsyncEntry +PendingListenAction +PendingListenEntry PendingRelDelete PendingRelSync PendingUnlinkEntry @@ -2155,10 +2253,10 @@ PermutationStepBlockerType PgAioBackend PgAioCtl PgAioHandle -PgAioHandleCallbackID -PgAioHandleCallbackStage PgAioHandleCallbackComplete +PgAioHandleCallbackID PgAioHandleCallbackReport +PgAioHandleCallbackStage PgAioHandleCallbacks PgAioHandleCallbacksEntry PgAioHandleFlags @@ -2171,8 +2269,13 @@ PgAioReturn PgAioTargetData PgAioTargetID PgAioTargetInfo +PgAioUringCaps PgAioUringContext PgAioWaitRef +PgAioWorkerControl +PgAioWorkerSet +PgAioWorkerSlot +PgAioWorkerSubmissionQueue PgArchData PgBackendGSSStatus PgBackendSSLStatus @@ -2200,12 +2303,13 @@ PgStatShared_Backend PgStatShared_BgWriter PgStatShared_Checkpointer PgStatShared_Common +PgStatShared_CustomFixedEntry +PgStatShared_CustomVarEntry PgStatShared_Database PgStatShared_Function PgStatShared_HashEntry -PgStatShared_InjectionPoint -PgStatShared_InjectionPointFixed PgStatShared_IO +PgStatShared_Lock PgStatShared_Relation PgStatShared_ReplSlot PgStatShared_SLRU @@ -2226,22 +2330,25 @@ PgStat_FunctionCallUsage PgStat_FunctionCounts PgStat_HashKey PgStat_IO -PgStat_Kind PgStat_KindInfo PgStat_LocalState +PgStat_Lock +PgStat_LockEntry PgStat_PendingDroppedStatsItem PgStat_PendingIO +PgStat_PendingLock PgStat_SLRUStats PgStat_ShmemControl PgStat_Snapshot PgStat_SnapshotEntry +PgStat_StatCustomFixedEntry +PgStat_StatCustomVarEntry PgStat_StatDBEntry PgStat_StatFuncEntry -PgStat_StatInjEntry -PgStat_StatInjFixedEntry PgStat_StatReplSlotEntry PgStat_StatSubEntry PgStat_StatTabEntry +PgStat_StatsFileOp PgStat_SubXactStatus PgStat_TableCounts PgStat_TableStatus @@ -2264,13 +2371,13 @@ PlanInvalItem PlanRowMark PlanState PlannedStmt +PlannedStmtOrigin PlannerGlobal PlannerInfo PlannerParamItem Point Pointer PolicyInfo -PolyNumAggState Pool PopulateArrayContext PopulateArrayState @@ -2305,6 +2412,7 @@ PrintfArgValue PrintfTarget PrinttupAttrInfo PrivTarget +PrivateRefCountData PrivateRefCountEntry ProcArrayStruct ProcLangInfo @@ -2325,8 +2433,13 @@ ProjectSetState ProjectionInfo ProjectionPath PromptInterruptContext +PropGraphEdge +PropGraphLabelAndProperties +PropGraphProperties +PropGraphVertex ProtocolVersion PrsStorage +PruneFreezeParams PruneFreezeResult PruneReason PruneState @@ -2339,12 +2452,15 @@ PsqlScanStateData PsqlSettings Publication PublicationActions +PublicationAllObjSpec +PublicationAllObjType PublicationDesc PublicationInfo PublicationObjSpec PublicationObjSpecType PublicationPartOpt PublicationRelInfo +PublicationRelKind PublicationSchemaInfo PublicationTable PublishGencolsType @@ -2354,12 +2470,12 @@ PushFilter PushFilterOps PushFunction PyCFunction -PyMappingMethods PyMethodDef PyModuleDef PyObject -PySequenceMethods PyTypeObject +PyType_Slot +PyType_Spec Py_ssize_t QPRS_STATE QTN2QTState @@ -2392,6 +2508,7 @@ RIX RI_CompareHashEntry RI_CompareKey RI_ConstraintInfo +RI_FastPathEntry RI_QueryHashEntry RI_QueryKey RTEKind @@ -2403,6 +2520,7 @@ Range RangeBound RangeBox RangeFunction +RangeGraphTable RangeIOData RangeQueryClause RangeSubselect @@ -2437,6 +2555,7 @@ RecordCacheArrayEntry RecordCacheEntry RecordCompareData RecordIOData +RecoveryConflictReason RecoveryLockEntry RecoveryLockXidEntry RecoveryPauseState @@ -2460,6 +2579,7 @@ ReindexObjectType ReindexParams ReindexStmt ReindexType +RelAggInfo RelFileLocator RelFileLocatorBackend RelFileNumber @@ -2473,6 +2593,7 @@ RelOptInfo RelOptKind RelPathStr RelStatsInfo +RelSyncCallbackFunction RelToCheck RelToCluster RelabelType @@ -2522,8 +2643,12 @@ ReorderBufferTupleCidEnt ReorderBufferTupleCidKey ReorderBufferUpdateProgressTxnCB ReorderTuple -RepOriginId +RepackCommand +RepackDecodingState +RepackStmt ReparameterizeForeignPathByChild_function +ReplOriginId +ReplOriginXactState ReplaceVarsFromTargetList_context ReplaceVarsNoMatchOption ReplaceWrapOption @@ -2554,6 +2679,9 @@ RestrictInfo Result ResultRelInfo ResultState +ResultType +RetainDeadTuplesData +RetainDeadTuplesPhase ReturnSetInfo ReturnStmt ReturningClause @@ -2566,6 +2694,7 @@ RewriteMappingDataEntry RewriteMappingFile RewriteRule RewriteState +RewriteStateData RmgrData RmgrDescData RmgrId @@ -2587,6 +2716,7 @@ RtlNtStatusToDosError_t RuleInfo RuleLock RuleStmt +RunMode RunningTransactions RunningTransactionsData SASLStatus @@ -2625,7 +2755,6 @@ SQLDropObject SQLFunctionCache SQLFunctionCachePtr SQLFunctionHashEntry -SQLFunctionLink SQLFunctionParseInfo SQLFunctionParseInfoPtr SQLValueFunction @@ -2637,6 +2766,7 @@ STARTUPINFO STRLEN SV SYNCHRONIZATION_BARRIER +SYSTEM_INFO SampleScan SampleScanGetSampleSize_function SampleScanState @@ -2667,11 +2797,13 @@ SecLabelStmt SeenRelsEntry SelectLimit SelectStmt +SelectStmtPassthrough Selectivity SelfJoinCandidate SemTPadded SemiAntiJoinFactors SeqScan +SeqScanInstrumentation SeqScanState SeqTable SeqTableData @@ -2697,6 +2829,7 @@ SetConstraintStateData SetConstraintTriggerData SetExprState SetFunctionReturnMode +SetHintBitsState SetOp SetOpCmd SetOpPath @@ -2724,6 +2857,7 @@ SharedIncrementalSortInfo SharedIndexScanInstrumentation SharedInvalCatalogMsg SharedInvalCatcacheMsg +SharedInvalRelSyncMsg SharedInvalRelcacheMsg SharedInvalRelmapMsg SharedInvalSmgrMsg @@ -2734,7 +2868,9 @@ SharedMemoizeInfo SharedRecordTableEntry SharedRecordTableKey SharedRecordTypmodRegistry +SharedSeqScanInstrumentation SharedSortInfo +SharedTidRangeScanInstrumentation SharedTuplestore SharedTuplestoreAccessor SharedTuplestoreChunk @@ -2744,7 +2880,13 @@ Sharedsort ShellTypeInfo ShippableCacheEntry ShippableCacheKey +ShmemAllocatorData +ShmemCallbacks ShmemIndexEnt +ShmemHashOpts +ShmemRequest +ShmemRequestKind +ShmemStructOpts ShutdownForeignScan_function ShutdownInformation ShutdownMode @@ -2760,21 +2902,25 @@ SimpleStats SimpleStringList SimpleStringListCell SingleBoundSortItem +SinglePartitionSpec Size SkipPages SkipSupport SkipSupportData +SkipSupportIncDec SlabBlock SlabContext SlabSlot SlotInvalidationCauseMap SlotNumber SlotSyncCtxStruct -SlruCtl -SlruCtlData +SlotSyncSkipReason +SlruDesc SlruErrorCause +SlruOpts SlruPageStatus SlruScanCallback +SlruSegState SlruShared SlruSharedData SlruWriteAll @@ -2791,6 +2937,7 @@ SortBy SortByDir SortByNulls SortCoordinate +SortCoordinateData SortGroupClause SortItem SortPath @@ -2827,10 +2974,13 @@ SpinDelayStatus SplitInterval SplitLR SplitPageLayout +SplitPartitionContext SplitPoint SplitTextOutputData SplitVar StackElem +StakindFlags +StartBufferIOResult StartDataPtrType StartLOPtrType StartLOsPtrType @@ -2838,6 +2988,7 @@ StartReplicationCmd StartupStatusEnum StatEntry StatExtEntry +StatApproxReadStreamPrivate StateFileChunk StatisticExtInfo StatsBuildData @@ -2860,6 +3011,7 @@ SubLink SubLinkType SubOpts SubPlan +SubPlanRTInfo SubPlanState SubRelInfo SubRemoveRels @@ -2884,11 +3036,13 @@ SubscriptionRelState SummarizerReadLocalXLogPrivate SupportRequestCost SupportRequestIndexCondition +SupportRequestInlineInFrom SupportRequestModifyInPlace SupportRequestOptimizeWindowClause SupportRequestRows SupportRequestSelectivity SupportRequestSimplify +SupportRequestSimplifyAggref SupportRequestWFuncMonotonic Syn SyncOps @@ -2897,9 +3051,10 @@ SyncRepStandbyData SyncRequestHandler SyncRequestType SyncStandbySlotsConfigData -SyncingTablesState +SyncingRelationsState SysFKRelationship SysScanDesc +SysScanDescData SyscacheCallbackFunction SysloggerStartupData SystemRowsSamplerData @@ -2979,8 +3134,10 @@ TableLikeClause TableSampleClause TableScanDesc TableScanDescData +TableScanInstrumentation TableSpaceCacheEntry TableSpaceOpts +TableToProcess TablespaceList TablespaceListCell TapeBlockTrailer @@ -2989,6 +3146,7 @@ TarMethodData TarMethodFile TargetEntry TclExceptionNameMap +Tcl_CmdInfo Tcl_DString Tcl_FileProc Tcl_HashEntry @@ -2996,11 +3154,14 @@ Tcl_HashTable Tcl_Interp Tcl_NotifierProcs Tcl_Obj +Tcl_Size Tcl_Time TempNamespaceStatus +TestDSMRegistryHashEntry TestDSMRegistryStruct TestDecodingData TestDecodingTxnData +TestShmemData TestSpec TestValueType TextFreq @@ -3014,6 +3175,7 @@ TidOpExpr TidPath TidRangePath TidRangeScan +TidRangeScanInstrumentation TidRangeScanState TidScan TidScanState @@ -3032,6 +3194,7 @@ TimeoutId TimeoutType Timestamp TimestampTz +TimingClockSourceType TmFromChar TmToChar ToastAttrInfo @@ -3075,6 +3238,7 @@ TriggerDesc TriggerEvent TriggerFlags TriggerInfo +TriggerInstrumentation TriggerTransition TruncateStmt TsmRoutine @@ -3088,6 +3252,7 @@ TupleHashEntry TupleHashEntryData TupleHashIterator TupleHashTable +TupleHashTableData TupleQueueReader TupleTableSlot TupleTableSlotOps @@ -3124,6 +3289,7 @@ TypeName TzAbbrevCache U32 U8 +UCaseMap UChar UCharIterator UColAttributeValue @@ -3140,7 +3306,7 @@ UnicodeNormalizationForm UnicodeNormalizationQC Unique UniquePath -UniquePathMethod +UniqueRelInfo UniqueState UnlistenStmt UnresolvedTup @@ -3155,24 +3321,27 @@ UpgradeTaskSlotState UpgradeTaskStep UploadManifestCmd UpperRelationKind -UpperUniquePath UserAuth UserContext UserMapping UserOpts +VMCorruptionType VacAttrStats VacAttrStatsP VacDeadItemsInfo VacErrPhase -VacObjFilter VacOptValue +VacuumCutoffs VacuumParams VacuumRelation VacuumStmt ValidIOData ValidateIndexState -ValidatorModuleState ValidatorModuleResult +ValidatorModuleState +ValidatorShutdownCB +ValidatorStartupCB +ValidatorValidateCB ValuesScan ValuesScanState Var @@ -3224,7 +3393,7 @@ WSANETWORKEVENTS WSAPROTOCOL_INFO WaitEvent WaitEventActivity -WaitEventBufferPin +WaitEventBuffer WaitEventClient WaitEventCustomCounterData WaitEventCustomEntryByInfo @@ -3233,7 +3402,12 @@ WaitEventIO WaitEventIPC WaitEventSet WaitEventTimeout +WaitLSNProcInfo +WaitLSNResult +WaitLSNState +WaitLSNType WaitPMResult +WaitStmt WalCloseMethod WalCompression WalInsertClass @@ -3274,6 +3448,7 @@ WindowObjectData WindowStatePerAgg WindowStatePerAggData WindowStatePerFunc +WindowStatePerFuncData WithCheckOption WithClause WordBoundaryNext @@ -3286,9 +3461,9 @@ WorkTableScan WorkTableScanState WorkerInfo WorkerInfoData -WorkerInstrumentation WorkerJobDumpPtrType WorkerJobRestorePtrType +WorkerNodeInstrumentation Working_State WriteBufPtrType WriteBytePtrType @@ -3305,6 +3480,7 @@ X509_NAME X509_NAME_ENTRY X509_STORE X509_STORE_CTX +X86FeatureId XLTW_Oper XLogCtlData XLogCtlInsert @@ -3377,10 +3553,9 @@ _resultmap _stringlist access_vector_t acquireLocksOnSubLinks_context -add_nulling_relids_context addFkConstraintSides +add_nulling_relids_context adjust_appendrel_attrs_context -allocfunc amadjustmembers_function ambeginscan_function ambuild_function @@ -3392,6 +3567,7 @@ amcostestimate_function amendscan_function amestimateparallelscan_function amgetbitmap_function +amgettreeheight_function amgettuple_function aminitparallelscan_function aminsert_function @@ -3402,14 +3578,31 @@ amparallelrescan_function amproperty_function amrescan_function amrestrpos_function -amtranslate_strategy_function amtranslatestrategy; -amtranslate_cmptype_function amtranslatecmptype; +amtranslate_cmptype_function +amtranslate_strategy_function amvacuumcleanup_function amvalidate_function array_iter array_unnest_fctx assign_collations_context +astreamer +astreamer_archive_context +astreamer_extractor +astreamer_gzip_decompressor +astreamer_gzip_writer +astreamer_lz4_frame +astreamer_member +astreamer_ops +astreamer_plain_writer +astreamer_recovery_injector +astreamer_tar_archiver +astreamer_tar_parser +astreamer_verify +astreamer_waldump +astreamer_zstd_frame auth_password_hook_typ +auto_explain_extension_options +auto_explain_option autovac_table av_relation avc_cache @@ -3435,34 +3628,19 @@ bbsink_shell bbsink_state bbsink_throttle bbsink_zstd -astreamer -astreamer_archive_context -astreamer_extractor -astreamer_gzip_decompressor -astreamer_gzip_writer -astreamer_lz4_frame -astreamer_member -astreamer_ops -astreamer_plain_writer -astreamer_recovery_injector -astreamer_tar_archiver -astreamer_tar_parser -astreamer_verify -astreamer_zstd_frame bgworker_main_type bh_node_type binaryheap binaryheap_comparator bitmapword -bits16 -bits32 -bits8 blockreftable_hash blockreftable_iterator bloom_filter boolKEY brin_column_state brin_serialize_callback_type +btree_gin_convert_function +btree_gin_leftmost_function bytea cached_re_str canonicalize_state @@ -3472,6 +3650,8 @@ cb_cleanup_dir cb_options cb_tablespace cb_tablespace_mapping +char16_t +char32_t check_agg_arguments_context check_function_callback check_network_data @@ -3504,6 +3684,9 @@ create_upper_paths_hook_type createdb_failure_params crosstab_HashEnt crosstab_cat_desc +curl_infotype +curl_socket_t +curl_version_info_data datapagemap_iterator_t datapagemap_t dateKEY @@ -3515,9 +3698,8 @@ deparse_columns deparse_context deparse_expr_cxt deparse_namespace -destructor +derives_hash dev_t -digit disassembledLeaf dlist_head dlist_iter @@ -3555,18 +3737,23 @@ dsm_handle dsm_op dsm_segment dsm_segment_detach_callback +duration eLogType ean13 eary ec_matches_callback_type ec_member_foreign_arg ec_member_matches_arg +element_type emit_log_hook_type eval_const_expressions_context exec_thread_arg execution_state exit_function explain_get_index_name_hook_type +explain_per_node_hook_type +explain_per_plan_hook_type +explain_validate_options_hook_type f_smgr fasthash_state fd_set @@ -3575,6 +3762,7 @@ fe_scram_state fe_scram_state_enum fetch_range_request file_action_t +file_content_type_t file_entry_t file_type_t filehash_hash @@ -3597,10 +3785,6 @@ float8 float8KEY floating_decimal_32 floating_decimal_64 -fmAggrefPtr -fmExprContextCallbackFunction -fmNodePtr -fmStringInfo fmgr_hook_type foreign_glob_cxt foreign_loc_cxt @@ -3638,6 +3822,7 @@ gistxlogPageReuse gistxlogPageSplit gistxlogPageUpdate grouping_sets_data +growable_trgm_array gseg_picksplit_item gss_OID_set gss_buffer_desc @@ -3649,8 +3834,6 @@ gss_key_value_set_desc gss_name_t gtrgm_consistent_cache gzFile -hashfunc -hbaPort heap_page_items_state help_handler hlCheck @@ -3671,19 +3854,26 @@ init_function inline_cte_walker_context inline_error_callback_arg ino_t +inquiry instr_time int128 int16 int16KEY +int16_t int2vector int32 int32KEY int32_t int64 int64KEY +int64_t int8 +int8_t int8x16_t +int_fast32_t +int_fast64_t internalPQconninfoOption +intmax_t intptr_t intset_internal_node intset_leaf_node @@ -3713,7 +3903,9 @@ lclContext lclTocEntry leafSegmentInfo leaf_item +libpq_gettext_func libpq_source +libpqsrv_PGresult line_t lineno_t list_sort_comparator @@ -3760,7 +3952,6 @@ memoize_iterator metastring missing_cache_key mix_data_t -mixedStruct mode_t movedb_failure_params multirange_bsearch_comparison @@ -3769,6 +3960,7 @@ mxact mxtruncinfo needs_fmgr_hook_type network_sortsupport_state +nl_item nodeitem normal_rand_fctx nsphash_hash @@ -3786,6 +3978,7 @@ openssl_tls_init_hook_typ ossl_EVP_cipher_func other output_type +overexplain_options pagetable_hash pagetable_iterator pairingheap @@ -3801,11 +3994,11 @@ pe_test_vector pendingPosition pending_label pgParameterStatus +pgoff_t pg_atomic_flag pg_atomic_uint32 pg_atomic_uint64 pg_be_sasl_mech -pg_case_map pg_category_range pg_checksum_context pg_checksum_raw_context @@ -3826,10 +4019,10 @@ pg_enc2name pg_encname pg_fe_sasl_mech pg_funcptr_t +pg_getopt_ctx pg_gssinfo pg_hmac_ctx pg_hmac_errno -pg_int64 pg_local_to_utf_combined pg_locale_t pg_mb_radix_tree @@ -3841,14 +4034,17 @@ pg_regex_t pg_regmatch_t pg_regoff_t pg_saslprep_rc +pg_saslprep_test_context pg_sha1_ctx pg_sha224_ctx pg_sha256_ctx pg_sha384_ctx pg_sha512_ctx +pg_signal_info pg_snapshot pg_special_case pg_stack_base_t +pg_ternary pg_time_t pg_time_usec_t pg_tz @@ -3863,12 +4059,53 @@ pg_unicode_properties pg_unicode_range pg_unicode_recompinfo pg_usec_time_t +pg_utf8_codepoint_range pg_utf_to_local_combined pg_uuid_t -pg_wc_probefunc pg_wchar pg_wchar_tbl pgp_armor_headers_state +pgpa_advice_item +pgpa_advice_tag_type +pgpa_advice_target +pgpa_identifier +pgpa_index_target +pgpa_index_type +pgpa_itm_type +pgpa_jo_outcome +pgpa_join_class +pgpa_join_member +pgpa_join_state +pgpa_join_strategy +pgpa_join_unroller +pgpa_output_context +pgpa_plan_walker_context +pgpa_planner_info +pgpa_planner_state +pgpa_qf_type +pgpa_query_feature +pgpa_scan +pgpa_scan_strategy +pgpa_target_type +pgpa_trove +pgpa_trove_entry +pgpa_trove_entry_element +pgpa_trove_entry_hash +pgpa_trove_entry_key +pgpa_trove_lookup_type +pgpa_trove_result +pgpa_trove_slice +pgpa_unrolled_join +pgsa_entry +pgsa_entry_key +pgsa_saved_entry +pgsa_saved_stash +pgsa_saved_stash_table_hash +pgsa_shared_state +pgsa_stash +pgsa_stash_count +pgsa_stash_name +pgsa_writer_context pgsocket pgsql_thing_t pgssEntry @@ -3886,6 +4123,8 @@ pgthreadlock_t pid_t pivot_field planner_hook_type +planner_setup_hook_type +planner_shutdown_hook_type planstate_tree_walker_callback plperl_array_info plperl_call_data @@ -3898,7 +4137,8 @@ plperl_query_entry plpgsql_CastExprHashEntry plpgsql_CastHashEntry plpgsql_CastHashKey -plpgsql_HashEnt +plpgsql_expr_walker_callback +plpgsql_stmt_walker_callback pltcl_call_state pltcl_interp_desc pltcl_proc_desc @@ -3921,7 +4161,6 @@ printTextLineFormat printTextLineWrap printTextRule printXheaderWidthType -printfunc priv_map process_file_callback_t process_sublinks_context @@ -3948,8 +4187,7 @@ qc_hash_func qsort_arg_comparator qsort_comparator query_pathkeys_callback -radius_attribute -radius_packet +RadixSortInfo rangeTableEntry_used_context rank_context rbt_allocfunc @@ -3959,14 +4197,14 @@ rbt_freefunc reduce_outer_joins_partial_state reduce_outer_joins_pass1_state reduce_outer_joins_pass2_state +refcount_hash +refcount_iterator reference +regc_wc_probefunc regex_arc_t -regex_t regexp regexp_matches_ctx registered_buffer -regmatch_t -regoff_t regproc relopt_bool relopt_enum @@ -3977,14 +4215,18 @@ relopt_kind relopt_parse_elt relopt_real relopt_string +relopt_ternary relopt_type relopt_value relopts_validator +RemoteAttributeMapping remoteConn remoteConnHashEnt remoteDep +RemoteStatsResults remove_nulling_relids_context rendezvousHashEntry +rep replace_rte_variables_callback replace_rte_variables_context report_error_fn @@ -4003,6 +4245,7 @@ rt_node_class_test_elem rt_radix_tree saophash_hash save_buffer +save_locale_t scram_state scram_state_enum script_error_callback_arg @@ -4021,6 +4264,7 @@ shm_mq_result shm_toc shm_toc_entry shm_toc_estimator +shmem_hash_allocator shmem_request_hook_type shmem_startup_hook_type sig_atomic_t @@ -4075,7 +4319,7 @@ storeRes_func stream_stop_callback string substitute_actual_parameters_context -substitute_actual_srf_parameters_context +substitute_actual_parameters_in_from_context substitute_grouped_columns_context substitute_phv_relids_context subxids_array_status @@ -4085,6 +4329,7 @@ tar_file td_entry teSection temp_tablespaces_extra +test128 test_re_flags test_regex_ctx test_shm_mq_header @@ -4130,16 +4375,18 @@ uint32_t uint32x4_t uint64 uint64_t +uint64x2_t uint8 uint8_t uint8x16_t +uint_fast64_t +uintmax_t uintptr_t unicodeStyleBorderFormat unicodeStyleColumnFormat unicodeStyleFormat unicodeStyleRowFormat unicode_linestyle -UniqueRelInfo unit_conversion unlogged_relation_entry utf_local_conversion_func @@ -4151,9 +4398,13 @@ va_list vacuumingOptions validate_string_relopt varatt_expanded +varatt_external +varatt_indirect varattrib_1b varattrib_1b_e varattrib_4b +varlena +vartag_external vbits verifier_context walrcv_alter_slot_fn @@ -4199,6 +4450,8 @@ xl_btree_split xl_btree_unlink_page xl_btree_update xl_btree_vacuum +xl_checkpoint_redo +xl_checksum_state xl_clog_truncate xl_commit_ts_truncate xl_dbase_create_file_copy_rec @@ -4229,7 +4482,6 @@ xl_heap_prune xl_heap_rewrite_mapping xl_heap_truncate xl_heap_update -xl_heap_visible xl_invalid_page xl_invalid_page_key xl_invalidations @@ -4302,6 +4554,3 @@ yyscan_t z_stream z_streamp zic_t -ExplainExtensionOption -ExplainOptionHandler -overexplain_options diff --git a/src/tools/testint128.c b/src/tools/testint128.c deleted file mode 100644 index a25631e277d2e..0000000000000 --- a/src/tools/testint128.c +++ /dev/null @@ -1,170 +0,0 @@ -/*------------------------------------------------------------------------- - * - * testint128.c - * Testbed for roll-our-own 128-bit integer arithmetic. - * - * This is a standalone test program that compares the behavior of an - * implementation in int128.h to an (assumed correct) int128 native type. - * - * Copyright (c) 2017-2025, PostgreSQL Global Development Group - * - * - * IDENTIFICATION - * src/tools/testint128.c - * - *------------------------------------------------------------------------- - */ - -#include "postgres_fe.h" - -/* - * By default, we test the non-native implementation in int128.h; but - * by predefining USE_NATIVE_INT128 to 1, you can test the native - * implementation, just to be sure. - */ -#ifndef USE_NATIVE_INT128 -#define USE_NATIVE_INT128 0 -#endif - -#include "common/int128.h" -#include "common/pg_prng.h" - -/* - * We assume the parts of this union are laid out compatibly. - */ -typedef union -{ - int128 i128; - INT128 I128; - union - { -#ifdef WORDS_BIGENDIAN - int64 hi; - uint64 lo; -#else - uint64 lo; - int64 hi; -#endif - } hl; -} test128; - - -/* - * Control version of comparator. - */ -static inline int -my_int128_compare(int128 x, int128 y) -{ - if (x < y) - return -1; - if (x > y) - return 1; - return 0; -} - -/* - * Main program. - * - * Generates a lot of random numbers and tests the implementation for each. - * The results should be reproducible, since we use a fixed PRNG seed. - * - * You can give a loop count if you don't like the default 1B iterations. - */ -int -main(int argc, char **argv) -{ - long count; - - pg_prng_seed(&pg_global_prng_state, 0); - - if (argc >= 2) - count = strtol(argv[1], NULL, 0); - else - count = 1000000000; - - while (count-- > 0) - { - int64 x = pg_prng_uint64(&pg_global_prng_state); - int64 y = pg_prng_uint64(&pg_global_prng_state); - int64 z = pg_prng_uint64(&pg_global_prng_state); - test128 t1; - test128 t2; - - /* check unsigned addition */ - t1.hl.hi = x; - t1.hl.lo = y; - t2 = t1; - t1.i128 += (int128) (uint64) z; - int128_add_uint64(&t2.I128, (uint64) z); - - if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) - { - printf("%016lX%016lX + unsigned %lX\n", x, y, z); - printf("native = %016lX%016lX\n", t1.hl.hi, t1.hl.lo); - printf("result = %016lX%016lX\n", t2.hl.hi, t2.hl.lo); - return 1; - } - - /* check signed addition */ - t1.hl.hi = x; - t1.hl.lo = y; - t2 = t1; - t1.i128 += (int128) z; - int128_add_int64(&t2.I128, z); - - if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) - { - printf("%016lX%016lX + signed %lX\n", x, y, z); - printf("native = %016lX%016lX\n", t1.hl.hi, t1.hl.lo); - printf("result = %016lX%016lX\n", t2.hl.hi, t2.hl.lo); - return 1; - } - - /* check multiplication */ - t1.i128 = (int128) x * (int128) y; - - t2.hl.hi = t2.hl.lo = 0; - int128_add_int64_mul_int64(&t2.I128, x, y); - - if (t1.hl.hi != t2.hl.hi || t1.hl.lo != t2.hl.lo) - { - printf("%lX * %lX\n", x, y); - printf("native = %016lX%016lX\n", t1.hl.hi, t1.hl.lo); - printf("result = %016lX%016lX\n", t2.hl.hi, t2.hl.lo); - return 1; - } - - /* check comparison */ - t1.hl.hi = x; - t1.hl.lo = y; - t2.hl.hi = z; - t2.hl.lo = pg_prng_uint64(&pg_global_prng_state); - - if (my_int128_compare(t1.i128, t2.i128) != - int128_compare(t1.I128, t2.I128)) - { - printf("comparison failure: %d vs %d\n", - my_int128_compare(t1.i128, t2.i128), - int128_compare(t1.I128, t2.I128)); - printf("arg1 = %016lX%016lX\n", t1.hl.hi, t1.hl.lo); - printf("arg2 = %016lX%016lX\n", t2.hl.hi, t2.hl.lo); - return 1; - } - - /* check case with identical hi parts; above will hardly ever hit it */ - t2.hl.hi = x; - - if (my_int128_compare(t1.i128, t2.i128) != - int128_compare(t1.I128, t2.I128)) - { - printf("comparison failure: %d vs %d\n", - my_int128_compare(t1.i128, t2.i128), - int128_compare(t1.I128, t2.I128)); - printf("arg1 = %016lX%016lX\n", t1.hl.hi, t1.hl.lo); - printf("arg2 = %016lX%016lX\n", t2.hl.hi, t2.hl.lo); - return 1; - } - } - - return 0; -} diff --git a/src/tools/testwrap b/src/tools/testwrap index 02f1951ad7e94..e91296ecd1531 100755 --- a/src/tools/testwrap +++ b/src/tools/testwrap @@ -12,6 +12,8 @@ parser.add_argument('--srcdir', help='source directory of test', type=str) parser.add_argument('--basedir', help='base directory of test', type=str) parser.add_argument('--testgroup', help='test group', type=str) parser.add_argument('--testname', help='test name', type=str) +parser.add_argument('--schedule', help='schedule tests', nargs='*') +parser.add_argument('--tests', help='tests', nargs='*') parser.add_argument('--skip', help='skip test (with reason)', type=str) parser.add_argument('--pg-test-extra', help='extra tests', type=str) parser.add_argument('test_command', nargs='*') @@ -51,6 +53,14 @@ env_dict = {**os.environ, if "PG_TEST_EXTRA" not in env_dict and args.pg_test_extra: env_dict["PG_TEST_EXTRA"] = args.pg_test_extra +if "TESTS" in env_dict: + args.test_command += env_dict["TESTS"].split() +else: + if args.schedule: + args.test_command += ['--schedule', ' '.join(args.schedule)] + if args.tests: + args.test_command.extend(args.tests) + sp = subprocess.Popen(args.test_command, env=env_dict, stdout=subprocess.PIPE) # Meson categorizes a passing TODO test point as bad # (https://github.com/mesonbuild/meson/issues/13183). Remove the TODO diff --git a/src/tools/valgrind.supp b/src/tools/valgrind.supp index 7ea464c809417..d56794b4f7e28 100644 --- a/src/tools/valgrind.supp +++ b/src/tools/valgrind.supp @@ -180,3 +180,50 @@ Memcheck:Cond fun:PyObject_Realloc } + +# NUMA introspection requires touching memory first, and some of it may +# be marked as noaccess (e.g. unpinned buffers). So just ignore that. +{ + pg_numa_touch_mem_if_required + Memcheck:Addr4 + fun:pg_numa_touch_mem_if_required +} + +{ + pg_numa_touch_mem_if_required + Memcheck:Addr8 + fun:pg_numa_touch_mem_if_required +} + + +# Memory-leak suppressions +# Note that a suppression rule will silence complaints about memory blocks +# allocated in matching places, but it won't prevent "indirectly lost" +# complaints about blocks that are only reachable via the suppressed blocks. + +# Suppress complaints about stuff leaked during function cache loading. +# Both the PL/pgSQL and SQL-function parsing processes generate some cruft +# within the function's cache context, which doesn't seem worth the trouble +# to get rid of. Moreover, there are cases where CachedFunction structs +# are intentionally leaked because we're unsure if any fn_extra pointers +# remain. +{ + hide_function_cache_leaks + Memcheck:Leak + match-leak-kinds: definite,possible,indirect + + ... + fun:cached_function_compile +} + +# Suppress complaints about stuff leaked during TS dictionary loading. +# Not very much is typically lost there, and preventing it would +# require a risky API change for TS tmplinit functions. +{ + hide_ts_dictionary_leaks + Memcheck:Leak + match-leak-kinds: definite,possible,indirect + + ... + fun:lookup_ts_dictionary_cache +} diff --git a/src/tools/version_stamp.pl b/src/tools/version_stamp.pl index c3509474d83b2..ea50ee41c691e 100755 --- a/src/tools/version_stamp.pl +++ b/src/tools/version_stamp.pl @@ -3,7 +3,7 @@ ################################################################# # version_stamp.pl -- update version stamps throughout the source tree # -# Copyright (c) 2008-2025, PostgreSQL Global Development Group +# Copyright (c) 2008-2026, PostgreSQL Global Development Group # # src/tools/version_stamp.pl ################################################################# @@ -25,7 +25,7 @@ # Major version is hard-wired into the script. We update it when we branch # a new development version. -my $majorversion = 18; +my $majorversion = 19; # Validate argument and compute derived variables my $minor = shift; diff --git a/src/tools/win32tzlist.pl b/src/tools/win32tzlist.pl index 706b1f78f80b6..dddd93d5077d3 100755 --- a/src/tools/win32tzlist.pl +++ b/src/tools/win32tzlist.pl @@ -2,7 +2,7 @@ # # win32tzlist.pl -- compare Windows timezone information # -# Copyright (c) 2008-2025, PostgreSQL Global Development Group +# Copyright (c) 2008-2026, PostgreSQL Global Development Group # # src/tools/win32tzlist.pl ################################################################# diff --git a/src/tutorial/complex.c b/src/tutorial/complex.c index 6798a9e6ba6c0..46dc54e62d036 100644 --- a/src/tutorial/complex.c +++ b/src/tutorial/complex.c @@ -41,7 +41,7 @@ complex_in(PG_FUNCTION_ARGS) errmsg("invalid input syntax for type %s: \"%s\"", "complex", str))); - result = (Complex *) palloc(sizeof(Complex)); + result = palloc_object(Complex); result->x = x; result->y = y; PG_RETURN_POINTER(result); @@ -73,7 +73,7 @@ complex_recv(PG_FUNCTION_ARGS) StringInfo buf = (StringInfo) PG_GETARG_POINTER(0); Complex *result; - result = (Complex *) palloc(sizeof(Complex)); + result = palloc_object(Complex); result->x = pq_getmsgfloat8(buf); result->y = pq_getmsgfloat8(buf); PG_RETURN_POINTER(result); @@ -108,7 +108,7 @@ complex_add(PG_FUNCTION_ARGS) Complex *b = (Complex *) PG_GETARG_POINTER(1); Complex *result; - result = (Complex *) palloc(sizeof(Complex)); + result = palloc_object(Complex); result->x = a->x + b->x; result->y = a->y + b->y; PG_RETURN_POINTER(result); diff --git a/src/tutorial/complex.source b/src/tutorial/complex.source index 7ae0359dcd91d..c52ee121c8165 100644 --- a/src/tutorial/complex.source +++ b/src/tutorial/complex.source @@ -5,7 +5,7 @@ -- use this new type. -- -- --- Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +-- Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group -- Portions Copyright (c) 1994, Regents of the University of California -- -- src/tutorial/complex.source diff --git a/src/tutorial/funcs.c b/src/tutorial/funcs.c index 4a61177567c2b..80882cf1414d3 100644 --- a/src/tutorial/funcs.c +++ b/src/tutorial/funcs.c @@ -13,6 +13,7 @@ #include "executor/executor.h" /* for GetAttributeByName() */ #include "utils/fmgrprotos.h" /* for text_starts_with() */ #include "utils/geo_decls.h" /* for point type */ +#include "varatt.h" /* for VARDATA/VARSIZE macros */ PG_MODULE_MAGIC; @@ -49,7 +50,7 @@ makepoint(PG_FUNCTION_ARGS) { Point *pointx = PG_GETARG_POINT_P(0); Point *pointy = PG_GETARG_POINT_P(1); - Point *new_point = (Point *) palloc(sizeof(Point)); + Point *new_point = palloc_object(Point); new_point->x = pointx->x; new_point->y = pointy->y; diff --git a/src/tutorial/syscat.source b/src/tutorial/syscat.source index 21ee574fd9eae..e410228e117d5 100644 --- a/src/tutorial/syscat.source +++ b/src/tutorial/syscat.source @@ -4,7 +4,7 @@ -- sample queries to the system catalogs -- -- --- Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +-- Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group -- Portions Copyright (c) 1994, Regents of the University of California -- -- src/tutorial/syscat.source @@ -17,10 +17,6 @@ -- SET search_path TO pg_catalog; --- The LIKE pattern language requires underscores to be escaped, so make --- sure the backslashes are not misinterpreted. -SET standard_conforming_strings TO on; - -- -- lists the names of all database owners and the name of their database(s) -- @@ -169,7 +165,6 @@ SELECT am.amname, n.nspname, opf.opfname, opr.oprname ORDER BY nspname, amname, opfname, oprname; -- --- Reset the search path and standard_conforming_strings to their defaults +-- Reset the search path to default -- RESET search_path; -RESET standard_conforming_strings;